首页 > 其他分享 >在freertos中对于分配线程栈空间的估算(建议收藏!!!)

在freertos中对于分配线程栈空间的估算(建议收藏!!!)

时间:2024-11-21 22:43:43浏览次数:3  
标签:估算 freertos 占用 函数调用 线程 函数 寄存器 空间 字节

一、宏观估算方法

宏观上,依据任务函数及其调用函数来综合确定栈空间需求。任务函数的栈帧包含局部变量存储与寄存器使用等元素。例如,有如下简单的 C 语言代码表示的任务函数 TaskFunction(请注意:我们分析的都是任务函数,而不是线程创建函数(xTaskCreate等))

void FunctionB(int param) {
    int localVarB1 = 5;  // 占用 4 字节栈空间
    int localVarB2;
    localVarB2 = localVarB1 * 2;
}

void FunctionA(int num) {
    int localVarA = 10;  // 占用 4 字节栈空间
    FunctionB(num);
}

void TaskFunction() {
    int localVar = 3;   // 占用 4 字节栈空间
    FunctionA(localVar);
}

在这个例子中,分析各个函数的栈帧:

  • FunctionB 函数有两个 int 类型局部变量 localVarB1 和 localVarB2,每个 int 占 4 字节,共占用 2 * 4 = 8 字节栈空间。此外,在函数调用过程中还会保存一些寄存器(如 lr 等),假设保存寄存器共占用 16 字节(这取决于具体的架构和编译器行为,后面会讲原因),那么 FunctionB 的栈帧大小约为 8 + 16 = 24 字节。

  • FunctionA 函数有一个 int 类型局部变量 localVarA 占用 4 字节栈空间,并且调用了 FunctionB。当调用 FunctionB 时,会将返回地址(存储在 lr 寄存器中)压入栈,假设保存寄存器和返回地址共占用 12 字节,那么加上 localVarA 的 4 字节,FunctionA 自身占用 4 + 12 = 16 字节,再加上 FunctionB 的栈帧大小 24 字节,FunctionA 及其调用 FunctionB 总共的栈帧需求约为 16 + 24 = 40 字节。

  • TaskFunction 函数有一个 int 类型局部变量 localVar 占用 4 字节栈空间,并且调用了 FunctionA。当调用 FunctionA 时,同样会有保存寄存器和返回地址的操作,假设这部分占用 10 字节,那么 TaskFunction 自身占用 4 + 10 = 14 字节,再加上 FunctionA 及其调用 FunctionB 的 40 字节,整个 TaskFunction 及其嵌套调用函数的栈帧总和约为 14 + 40 = 54 字节。

所以,在为执行 TaskFunction 的线程分配栈空间时,就需要考虑至少 54 字节或更多(一般来说,分配栈空间的时候都要保守些,例如这里考虑到了54字节,那么就分配多一点),以确保程序正常运行,避免栈溢出等问题。

假设的依据:

  1. 返回地址占用空间
    • 在 ARM 架构中,返回地址是函数调用完成后应该返回的指令地址。这个地址通常是 32 位(4 字节),因为程序计数器(PC)是 32 位的,它存储的是下一条要执行的指令的地址。当一个函数(如FunctionA)调用另一个函数(如FunctionB)时,调用指令(如BL指令)会把返回地址压入栈中,这就占用了 4 字节的栈空间。
  2. 寄存器保存占用空间
    • 在函数调用过程中,需要保存一些寄存器的值。具体保存哪些寄存器以及保存的方式因编译器和架构而异。
    • 一般来说,至少会保存链接寄存器(lr),用于存储返回地址,这部分已经在前面提到。此外,可能还会保存其他通用寄存器,如r4 - r7等。假设保存了r4 - r7这 4 个寄存器(这是一个简单的假设情况,实际可能因编译器优化等因素而不同),在 32 位 ARM 架构中,每个寄存器占 4 字节,那么这 4 个寄存器就占用了字节。
    • 但是,编译器可能会采用一些优化策略。例如,它可能只会保存部分寄存器,或者通过复用寄存器等方式减少保存的寄存器数量。在这里假设保存寄存器(除返回地址对应的寄存器外)总共占用了字节,这是综合考虑了编译器可能的优化行为和实际函数调用过程中的寄存器保存情况。

需要注意的是,这 12 字节的占用只是一个假设的估算值(看使用者的功力了),实际的占用空间可能会因编译器的具体实现、架构的特性、函数的具体逻辑(如是否使用了特殊的指令或寄存器)等因素而有所不同。

二、微观估算方法(以 Keil 工具为例)

前言:如何在keil中生成程序相应的反汇编文件

fromelf --text -a -c --output=xxx.dis xxx.axf

为什么使用上面这个指令,大家可以查一下,这里就不细讲了,下面是操作步骤

里面的test.dis,读者们想改成啥都可以。然后按下OK,在编译一下,就可以在文件中找到对应的.dis文件了

(一)通过寄存器使用情况估算

以 Keil 工具为例,可使用 fromelf 工具生成反汇编文件来进行微观层面的观察分析。其中一个重要的方面是查看寄存器的使用情况。

0x08002cac:    b570        p.      PUSH     {r4 - r6,lr}

从这条指令可以看出,PUSH 操作将 r4 - r6 这三个寄存器以及 lr 寄存器压入栈帧。在常见的 32 位 ARM 架构中,每个寄存器占用 4 字节的栈空间,所以这一步就使用了 4 * 4 = 16 字节的栈空间。就好比一个小盒子,每个寄存器是一个小物件,将这四个小物件放入盒子就占用了一定的空间。

(二)通过栈指针相关指令估算

另一个关键的微观估算依据是与栈指针 sp 相关的指令。例如:

0x08002cae:    b08c       ..      SUB      sp,sp,#0x30

这条汇编指令在 ARM 架构下对栈指针 sp 进行操作。SUB 作为减法指令,它使栈指针 sp 从当前所指向的栈顶地址减去一个偏移量。这里的偏移量 #0x30 转换为十进制是 48。这意味着在栈向低地址生长的常见情形下,栈顶地址向下移动 48 字节。形象地说,就像是在一个栈空间的 “货架” 上,原本栈顶的位置是最高层货架,执行这条指令后,栈顶位置下移到了更低的 48 字节处的 “货架”,从新的栈顶开始往后的 48 字节空间就被预留出来。这部分空间可供函数内部存储局部变量、临时数据等。例如,若函数中有一个数组需要存储在栈上,就可能会使用这片新分配出来的空间。而在函数执行结束前,通常会有相应的指令(如 ADD sp,sp,#0x30将栈指针恢复到原来的位置,如同把 “货架” 重新归位,从而释放这片为函数临时分配的栈空间。

通过宏观和微观两种估算方法的综合运用,可以更精准地为线程分配合适的栈空间,避免因栈空间分配不当而引发的程序错误。

 0x08002ccc:    f7fdfd1e    ....    BL       HAL_GPIO_Init ; 0x800070c

对于这个特定的PassiveBuzzer_Init函数,如果函数调用不是嵌套很深,并且在调HAL_GPIO_Init等函数后,这些函数能够及时返回,释放它们临时占用的返回地址空间(4 字节),那么可能不需要为每次函数调用额外添加 4 字节来分配栈空间。


然而,如果这个函数内部存在复杂的嵌套调用结构,例如HAL_GPIO_Init函数内部又调用了其他函数,而这些函数也会占用栈空间,并且在最深处的函数调用还没有返回时,栈空间的占用就会累积。在这种情况下,就需要考虑所有这些函数调用可能占用的额外空间,包括存储返回地址的空间、被调用函数自身的栈帧空间(如局部变量存储、寄存器保存等),以避免栈溢出。


所以,简单地说为每次BL指令对应的函数调用添加 4 字节来分配栈空间是一种比较保守的做法,可以在一定程度上避免栈溢出,但如果能够确定函数调用的具体情况(如调用深度、被调用函数的栈帧使用等),可以更精确地分配栈空间。


总的来说,还需要我们不断的去学习来使我们能够更好的来进行相对来说更加精确的估算栈空间的分配,这里起到的只是一个抛砖引玉的作用,相信读者们都能够做到比作者领悟的更好和深刻

最后,希望大家能够一键三连,让我们一起进步!!!

标签:估算,freertos,占用,函数调用,线程,函数,寄存器,空间,字节
From: https://blog.csdn.net/2401_83606346/article/details/143955036

相关文章

  • 多线程编程入门Thread_Task_async_await简单秒懂
    `usingSystem;usingSystem.Collections.Generic;usingSystem.ComponentModel;usingSystem.Data;usingSystem.Drawing;usingSystem.Linq;usingSystem.Text;usingSystem.Threading;usingSystem.Threading.Tasks;usingSystem.Windows.Forms;namespace多线程编......
  • Winform跨线程访问报错问题解决
    `usingSystem;usingSystem.Collections.Generic;usingSystem.ComponentModel;usingSystem.Data;usingSystem.Drawing;usingSystem.Linq;usingSystem.Text;usingSystem.Threading;usingSystem.Threading.Tasks;usingSystem.Windows.Forms;namespaceWinf......
  • 【鸿蒙基于API 13实战开发】—— 进程模型&线程模型分析
    ......
  • MySQL 主从复制之多线程复制
    目录一、MySQL多线程复制的背景二、MySQL5.5主从复制1、原理2、部署主从复制2.1、主节点安装配置MySQL5.52.2、从节点安装配置MySQL5.53、检查主从库server_id和log_bin配置4、创建主从复制用户5、获取主库的二进制日志文件和位置6、配置从库连接主库参数并启动从库复制......
  • 2.1_6 线程的实现方式和多线程模型
    目录1、用户级线程历史背景代码实现​用户级线程的优缺点2、内核级线程概念内核级线程的优缺点3、多线程模型一对一模型多对一模型多对多模型总览1、用户级线程历史背景早期的操作系统(如:早期Unix)只支持进程,不支持线程。当时的“线程”是由线程库实现的 ......
  • @Slf4j实现多线程场景下每个线程日志独立输出
    1.配置logbak-spring.xml<?xmlversion="1.0"encoding="UTF-8"?><configurationscan="true"scanPeriod="5seconds"><!--定义日志文件的存储路径--><propertyname="LOGS_PATH"value="./l......
  • Java中常用的线程安全单例模式实现
    在Java中,实现线程安全的单例模式有多种方式。以下是几种常用的线程安全单例模式实现:1.饿汉式(线程安全,类加载时初始化)特点:简单且线程安全,但如果实例过于占用资源且程序可能不使用它,会造成内存浪费。publicclassSingleton{privatestaticfinalSingletonINSTANCE......
  • 详解线程的三大特性:原子性、可见性和有序性
    在多线程编程中,理解线程的原子性、可见性和有序性是构建正确并发程序的基础。以下是它们的详细解释:1.原子性(Atomicity)定义原子性指的是操作不可被中断,要么全部执行完成,要么完全不执行。特性原子性操作在执行时不会被其他线程干扰。如果多个线程同时访问共享资......
  • Java线程池创建
    ......
  • Qt - 多线程之并发(QtConcurrent)
    一、什么是QtConcurrent?Concurrent是并发的意思,而QtConcurrent同std一样,是一个命名空间(namespace)。提供了一些高级的API,使得在编写多线程的时候,无需使用低级线程原语,如读写锁,等待条件或信号。使用QtConcurrent编写的程序会根据可用的处理器内核数自动调整使用的线程数。对于QtC......