嵌入式软件堆栈使用情况的估算方法 - 堆栈操作汇编代码分析(ARM Cortex-M处理器)

在嵌入式软件开发工作中,正确理解堆栈的使用是非常重要的,在无RTOS的嵌入式软件开发工作中,主堆栈溢出是容易忽视的错误,而在RTOS多任务嵌入式软件开发工作中,除要考虑主堆栈以外还需要考虑进程栈,需要给不同任务分配合适大小的任务堆栈。

基于以上问题,本文第一节介绍了嵌入式软件堆栈使用情况估算方法。为了方便理解这种估算方法,本文第二节将基于ARM Cortex-M处理器来对堆栈操作的汇编代码进行分析。

1 、堆栈使用情况估算方法

在处理器和编译器固定的情况下,栈的大小由需栈保存的寄存器数量,函数参数,局部变量,函数调用深度来决定。

ARM架构的C语言编译器遵循AAPCS(ARM架构过程调用标准),寄存器分成调用者保存寄存器和被调用者保存寄存器,对于调用者保存寄存器来说,函数调用可以直接使用来进行运算而不需要保存它们到栈空间,如r0到r3寄存器。而被调用者保存寄器如在函数中使用时的话,则必须进行压栈和出栈操作,如r4到r11寄存器。

在第二节的汇编代码中可看到,r7起到了记录栈指针偏移的作用,每次调用函数都需压栈保存,而r0,r1,r2则不需要。

需栈保存的寄存器数量通常不能由C语言源代码直接分析得出。为了估算堆栈的使用情况,可以假设一个函数调用时寄存器可能占用栈的最大值N,第二节的示例代码因为函数调用比较简单,栈操作只保存两个寄存器,实际大于此数,N取值需留足够余量,另外应了解中断调用情况,此时处理器会自动压栈相应的寄存器。

对于函数参数和局部变量而言,函数参数和局部变量的数据类型和数量是可以由C语言源代码看出来的。可根据C语言源代码来估算。假设嵌套调用第n层函数的局部变量占用栈大小为Xn,函数参数占用栈大小为Yn。需要注意的是编译器本身就有对齐和留余量的需求,比如在32位处理器的情况下,函数参数是2个int变量时汇编预留的栈使用实际值为8字节,但函数参数为3个int变量时汇编预留的栈使用实际值可以是20字节,因此上述X和Y的估值需要余量 。

通过汇编代码才能确定栈使用情况的精确值,只根据C语言源代码估算的话,以上参数取值都需留余量,估计值可大不可小,保证堆栈不溢出。

简言之,如有嵌套调用n层的话,栈的使用值可按以下公式进行估算:

n*N + X1 + Y1 + X2 +Y2 …+Xn+Yn
( 其中,n:嵌套函数调用深度; N: 函数调用时寄存器可能占用栈的估算最大值,确定一个最大估计值,第二节示例函数比较简单所实际值是8个字节,实际情况会大于8个字节,N取值可在此基础上估计一个余量(如按16或20个字节计算); Xn :嵌套调用第n层函数的局部变量占用栈大小,根据函数的局部变量类型和数量; Yn:嵌套调用第n层函数参数用栈大小,根据函数的函数参数的类型和数量 )

当前有许多开发工具链是可以进行栈分析进而生成栈使用的报表的,实际工作也可以通过栈布局和将栈空间填充成特定数据来感知栈使用异常,但是在软件设计之初能够估算出一个最差情况下栈使用大小值总是好的。

总之,处理器和编译器固定的情况下,调用函数需保存的系统寄存器数量,函数参数,局部变量和函数调用深度等4个因素来共同决定了堆栈的使用情况。以下将通过汇编代码的分析来做进一步的说明。

2、栈操作汇编代码分析

一个简单的C语言程序:

int add(int a,int b)
{
    return a+b;
}
int main(void)
{
	int c[2];
    c[0] = add(3,4);
	return c[0];
}

对以上程序进行编译(基于Cortex-M0+ ARM处理器,编译器是arm-none-eabi-gcc)得到汇编代码分析如下:

2.1 add函数汇编代码分析

得到的add函数汇编代码可以分成以下三大部分进行说明:
1、add函数的压栈

00000acc:   push    {r7, lr}
00000ace:   sub     sp, #8
00000ad0:   add     r7, sp, #0
00000ad2:   str     r0, [r7, #4]
00000ad4:   str     r1, [r7, #0]

push {r7, lr} 指令压栈r7,lr寄存器,lr记录add函数返回的地址。
sub sp, #8 指令移动栈指针,add子函数没有局部变量,栈指针移动的偏移量和add函数参数变量多少相对应,r7记录了当前的栈指针偏移 , r7+0地址处存储r1即int b变量, r7+4地址处存储r0即int a变量。

2、add运算

00000ad6:   ldr     r2, [r7, #4]
00000ad8:   ldr     r3, [r7, #0]
00000ada:   adds    r3, r2, r3

3、add函数的出栈和返回

00000adc:   movs    r0, r3
00000ade:   mov     sp, r7
00000ae0:   add     sp, #8
00000ae2:   pop     {r7, pc}

以上汇编代码将运算结果放在r0处,然后执行出栈操作,pc指针指向返回地址,返回到main主函数。

2.2 main函数汇编代码分析

得到main的函数汇编代码同样可分成三大部分进行说明:

1、main函数的压栈

00000ae4:   push    {r7, lr}
00000ae6:   sub     sp, #8
00000ae8:   add     r7, sp, #0

push {r7, lr}指令,压栈r7,lr寄存器,sub sp, #8移动栈指针,main主函数没有函数参数,栈指针移动的偏移量和主函数的局部变量多少相对应,r7记录了当前的栈指针偏移 , r7+0地址处存储C[0], r7+4地址处存储即C[1]。

2、调用add函数

00000aea:   movs    r1, #4
00000aec:   movs    r0, #3
00000aee:   bl      0xacc <add>
00000af2:   movs    r2, r0
00000af4:   movs    r3, r7
00000af6:   str     r2, [r3, #0]

r0,r1分别记录了add函数的参数,从add函数返回值在r0,最终存储于r7+0地址处即C[0]在堆栈的位置。

3、main函数出栈和返回

00000af8:   movs    r3, r7
00000afa:   ldr     r3, [r3, #0]
00000afc:   movs    r0, r3
00000afe:   mov     sp, r7
00000b00:   add     sp, #8
00000b02:   pop     {r7, pc} 

主函数的返回值也是C[0],以上汇编的意义将C[0]取出来又放在r0, 局部变量出栈add sp, #8,系统寄存器出栈pop {r7, pc} 。

2.3 示例的堆栈实际使用情况计算

从以上代码示例的分析,可以看出的函数压栈和出栈操作,分成两大类:
1、寄存器的压栈和出栈,以上汇编代码都采用了push和pop指令;
2、函数参数和局部变量的压栈和出栈,以上汇编代码均采用直接移动sp指针的形式。

以上示例的函数调用深度为2
main主函数使用栈的实际使用情况为(寄存器 r7 lr),(局部变量int C[2]) ,无函数参数,即 N1=8,X1=8 Y1=0 ;
add子函数使用栈的实际使用情况(寄存器 r7 lr),无局部变量,(函数参数int a, int b) ,取N2=8,X2=0 Y2=8 ;

栈使用情况实际值计算为:(N1+N2)+X1+Y1+X2+Y2=32。

2.4 改变局部变量后的汇编代码分析

进一步,修改main主函数,局部变量由int c[2]改成int c[16],然后观察汇编代码栈操作指令的变化。
修改后,C语言代码如下:

int add(int a,int b)
{
    return a+b;
}
int main(void)
{
	int c[16];
    c[0] = add(3,4);
	return c[0];
}

修改后,对应汇编代码如下:

          add:
00000acc:   push    {r7, lr}
00000ace:   sub     sp, #8
00000ad0:   add     r7, sp, #0
00000ad2:   str     r0, [r7, #4]
00000ad4:   str     r1, [r7, #0]
00000ad6:   ldr     r2, [r7, #4]
00000ad8:   ldr     r3, [r7, #0]
00000ada:   adds    r3, r2, r3
00000adc:   movs    r0, r3
00000ade:   mov     sp, r7
00000ae0:   add     sp, #8
00000ae2:   pop     {r7, pc}
          main:
00000ae4:   push    {r7, lr}
00000ae6:   sub     sp, #64 ; 0x40 
00000ae8:   add     r7, sp, #0
00000aea:   movs    r1, #4
00000aec:   movs    r0, #3
00000aee:   bl      0xacc <add>
00000af2:   movs    r2, r0
00000af4:   movs    r3, r7
00000af6:   str     r2, [r3, #0]
00000af8:   movs    r3, r7
00000afa:   ldr     r3, [r3, #0]
00000afc:   movs    r0, r3
00000afe:   mov     sp, r7
00000b00:   add     sp, #64 ; 0x40
00000b02:   pop     {r7, pc}

比较改变前后的两段汇编代码,可以发现变化只有两处:00000ae6地址处,汇编代码由" sub sp, #8"改变成" sub sp, #64 “;00000b00地址处,汇编代码由” add sp, #8"改变成" add sp, #64 "。随着局部变量定义由int c[2]改变为int c[16],sp栈指针偏移量由8改变为64,可见随着局部变量的增加,使用的堆栈也相应增加。

同理,基于以上简单的C语言程序,还可以修改add函数的函数参数个数,如修改add(int a ,int b)为 add(int a ,int b, int c),观察函数参数变化对堆栈大小的影响; 也可以在add函数内再调用一个子函数或者设计一个递归函数,然后进行嵌套函数调用,观察堆栈大小和栈指针的变化;这里不再一一进行说明,大家可以自行尝试。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值