宏观上,依据任务函数及其调用函数来综合确定栈空间需求。任务函数的栈帧包含局部变量存储与寄存器使用等元素。例如,有如下简单的 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字节,那么就分配多一点),以确保程序正常运行,避免栈溢出等问题。
FunctionA)调用另一个函数(如FunctionB)时,调用指令(如BL指令)会把返回地址压入栈中,这就占用了 4 字节的栈空间。lr),用于存储返回地址,这部分已经在前面提到。此外,可能还会保存其他通用寄存器,如r4 - r7等。假设保存了r4 - r7这 4 个寄存器(这是一个简单的假设情况,实际可能因编译器优化等因素而不同),在 32 位 ARM 架构中,每个寄存器占 4 字节,那么这 4 个寄存器就占用了字节。需要注意的是,这 12 字节的占用只是一个假设的估算值(看使用者的功力了),实际的占用空间可能会因编译器的具体实现、架构的特性、函数的具体逻辑(如是否使用了特殊的指令或寄存器)等因素而有所不同。
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 字节来分配栈空间是一种比较保守的做法,可以在一定程度上避免栈溢出,但如果能够确定函数调用的具体情况(如调用深度、被调用函数的栈帧使用等),可以更精确地分配栈空间。
总的来说,还需要我们不断的去学习来使我们能够更好的来进行相对来说更加精确的估算栈空间的分配,这里起到的只是一个抛砖引玉的作用,相信读者们都能够做到比作者领悟的更好和深刻
最后,诚望诸位不吝赐下一键三连,以资鼓励与襄助,使知识之光辉得以更盛,创作之热忱得以长燃,于技术探索之途携手共进,共铸不凡!