一、宏观估算方法
宏观上,依据任务函数及其调用函数来综合确定栈空间需求。任务函数的栈帧包含局部变量存储与寄存器使用等元素。例如,有如下简单的 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字节,那么就分配多一点),以确保程序正常运行,避免栈溢出等问题。
假设的依据:
- 返回地址占用空间
- 在 ARM 架构中,返回地址是函数调用完成后应该返回的指令地址。这个地址通常是 32 位(4 字节),因为程序计数器(PC)是 32 位的,它存储的是下一条要执行的指令的地址。当一个函数(如
FunctionA
)调用另一个函数(如FunctionB
)时,调用指令(如BL
指令)会把返回地址压入栈中,这就占用了 4 字节的栈空间。
- 在 ARM 架构中,返回地址是函数调用完成后应该返回的指令地址。这个地址通常是 32 位(4 字节),因为程序计数器(PC)是 32 位的,它存储的是下一条要执行的指令的地址。当一个函数(如
- 寄存器保存占用空间
- 在函数调用过程中,需要保存一些寄存器的值。具体保存哪些寄存器以及保存的方式因编译器和架构而异。
- 一般来说,至少会保存链接寄存器(
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