从汇编指令看函数调用堆栈的详细过程
1、C++代码
这个C++源码实现了一个简单的加法函数,并在主函数中调用该函数来计算两个整数的和。
int sum(int a,int b)
{int temp=0;temp=a+b;return temp;
}int main()
{int a=10;int b=20;int ret=sum(a,b);return 0;
}
2、汇编代码
在 ARM Cortex-A9 平台上,编译后的 C++ 源代码的汇编代码如下:
.cpu cortex-a9.eabi_attribute 28, 1.eabi_attribute 20, 1.eabi_attribute 21, 1.eabi_attribute 23, 3.eabi_attribute 24, 1.eabi_attribute 25, 1.eabi_attribute 26, 2.eabi_attribute 30, 6.eabi_attribute 34, 1.eabi_attribute 18, 4.file "main.cpp".text.align 2.global _Z3sumii.syntax unified.arm.fpu neon.type _Z3sumii, %function
_Z3sumii:.fnstart
.LFB0:@ args = 0, pretend = 0, frame = 16@ frame_needed = 1, uses_anonymous_args = 0@ link register save eliminated.str fp, [sp, #-4]!add fp, sp, #0sub sp, sp, #20str r0, [fp, #-16]str r1, [fp, #-20]mov r3, #0str r3, [fp, #-8]ldr r2, [fp, #-16]ldr r3, [fp, #-20]add r3, r2, r3str r3, [fp, #-8]ldr r3, [fp, #-8]mov r0, r3add sp, fp, #0@ sp neededldr fp, [sp], #4bx lr.cantunwind.fnend.size _Z3sumii, .-_Z3sumii.align 2.global main.syntax unified.arm.fpu neon.type main, %function
main:.fnstart
.LFB1:@ args = 0, pretend = 0, frame = 16@ frame_needed = 1, uses_anonymous_args = 0push {fp, lr}add fp, sp, #4sub sp, sp, #16mov r3, #10str r3, [fp, #-8]mov r3, #20str r3, [fp, #-12]ldr r1, [fp, #-12]ldr r0, [fp, #-8]bl _Z3sumiistr r0, [fp, #-16]mov r3, #0mov r0, r3sub sp, fp, #4@ sp neededpop {fp, pc}.cantunwind.fnend.size main, .-main.ident "GCC: (GNU) 7.3.0".section .note.GNU-stack,"",%progbits
3、汇编代码分析
汇编代码的开头
.cpu cortex-a9
.eabi_attribute 28, 1
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 2
.eabi_attribute 30, 6
.eabi_attribute 34, 1
.eabi_attribute 18, 4
.file "main.cpp"
这些行指定了目标 CPU 和一些 EABI(嵌入式应用程序二进制接口)属性。它们用于设置编译器和链接器的配置。
具体来说:
- .eabi_attribute 是 ARM 汇编中的一个伪指令,用于设置 EABI 属性。
- 28 是属性编号,表示该属性的类型。
- 1 是该属性的值,表示特定的设置或选项。
属性编号 28 代表 TAG_CPU_unaligned_access,用于指示代码是否允许非对齐的内存访问。这个属性的值 1 表示允许非对齐的内存访问。
属性编号 20 代表 TAG_ABI_FP_rounding,用于指示浮点运算的舍入模式支持。这个属性的值 1 表示支持 IEEE 754 标准规定的最近偶数舍入模式(也称为“最接近舍入”或“银行家舍入”)。
属性编号 21 代表 TAG_ABI_FP_denormal,描述浮点运算如何处理非正规化(denormal)数。值 1 表示支持 IEEE 754 标准的非正规化数。
属性编号 23 代表 TAG_ABI_FP_number_model,描述浮点数模型。值 3 表示 IEEE 754 标准的浮点数模型。
属性编号 24 代表 TAG_ABI_align_needed,描述是否需要特定的对齐。值 1 表示需要特定的对齐要求。
属性编号 25 代表 TAG_ABI_align_preserved,描述是否保留特定的对齐。值 1 表示保留特定的对齐要求。
属性编号 26 代表 TAG_ABI_enum_size,描述枚举类型的大小。值 2 表示枚举类型的大小是 32 位。
属性编号 30 代表 TAG_ABI_HardFP_use,描述硬件浮点运算的使用。值 6 表示硬件浮点运算使用的是 VFPv3-D16(Vector Floating Point v3 with 16 double-precision registers)。
属性编号 34 代表 TAG_CPU_unaligned_access,描述是否允许非对齐的内存访问。值 1 表示允许非对齐的内存访问。
属性编号 18 代表 TAG_ABI_PCS_wchar_t,描述 wchar_t 类型的大小。值 4 表示 wchar_t 类型的大小是 4 字节。
sum函数.rodata 段
.text.align 2.global _Z3sumii.syntax unified.arm.fpu neon.type _Z3sumii, %function
这部分代码定义了一些只读数据段 (.rodata),并声明了一些全局符号,包括 _Z3sumii(即 sum 函数)。
这里解释一下sum函数生成符号_Z3sumii的规则,这在gdb调试中很重要:
- _Z 是编译器生成的符号名称的前缀,用于标识这是一个函数名。
- 3 表示函数名的长度,即后续有三个字符构成函数名。
- sum 是函数的实际名称。
- ii 表示函数 sum 接受两个整型参数(int a, int b)。
_Z3sumii也称为函数签名(signature),由上可知函数签名由函数的名称、参数类型及其顺序组成。函数的返回类型不包括在签名中。
给定以下两个函数声明:
int sum(int a, int b);
void sum(int a, int b);
这两个函数的签名都是相同的,因为它们具有相同的函数名称和参数列表(即两个整型参数 int a 和 int b)。在函数签名中,返回类型(int 和 void)并不影响其唯一性。
所以,从函数签名的角度来看,这两个函数的签名是相同的,因此可以说它们的"符号"相同。
sum 函数实现
_Z3sumii:.fnstart
.LFB0:@ args = 0, pretend = 0, frame = 16@ frame_needed = 1, uses_anonymous_args = 0@ link register save eliminated.str fp, [sp, #-4]!add fp, sp, #0sub sp, sp, #20str r0, [fp, #-16]str r1, [fp, #-20]mov r3, #0str r3, [fp, #-8]ldr r2, [fp, #-16]ldr r3, [fp, #-20]add r3, r2, r3str r3, [fp, #-8]ldr r3, [fp, #-8]mov r0, r3add sp, fp, #0@ sp neededldr fp, [sp], #4bx lr.cantunwind.fnend.size _Z3sumii, .-_Z3sumii
3. ARM汇编代码中的堆栈操作过程分析
这段代码展示了函数调用时ARM架构下的堆栈管理机制,包括函数调用时的参数传递、局部变量存储以及返回地址处理等。
1. 主函数(main)的堆栈操作
main:
.LFB1:@ 函数序言(prologue)push {fp, lr} @ 将帧指针(fp)和链接寄存器(lr)压栈保存add fp, sp, #4 @ 设置新的帧指针(fp = sp + 4)sub sp, sp, #16 @ 在栈上分配16字节空间用于局部变量
操作分析:
-
保存调用现场:
push {fp, lr}
将当前帧指针(fp)和链接寄存器(lr)压入堆栈。这是ARM架构中典型的函数开场操作,用于保存调用者的帧指针和返回地址[citation:1][citation:5]。- 在满递减堆栈(FD)模式下,
push
等同于STMFD
指令,寄存器按编号升序压入从高到低的地址[citation:4][citation:6]。 - 压入顺序是fp(即r11)先入栈,lr(r14)后入栈,sp会递减8字节(假设每个寄存器4字节)。
- 在满递减堆栈(FD)模式下,
-
建立新栈帧:
add fp, sp, #4
将帧指针设置为当前sp+4的位置,指向保存的fp位置,用于后续访问局部变量和参数[citation:1]。 -
分配局部变量空间:
sub sp, sp, #16
将栈指针下移16字节,为局部变量a、b和ret分配空间[citation:5]。
@ 局部变量初始化mov r3, #10 @ 将立即数10存入r3str r3, [fp, #-8] @ 将r3的值存入fp-8的位置(变量a)mov r3, #20 @ 将立即数20存入r3str r3, [fp, #-12] @ 将r3的值存入fp-12的位置(变量b)
操作分析:
- 通过fp相对寻址方式([fp, #-8]和[fp, #-12])将局部变量a和b存储在栈上[citation:2]。
@ 调用sum函数前的参数准备ldr r1, [fp, #-12] @ 将变量b的值加载到r1(第二个参数)ldr r0, [fp, #-8] @ 将变量a的值加载到r0(第一个参数)bl _Z3sumii @ 调用sum函数(会修改lr)
操作分析:
- 按照ARM调用约定,前4个参数通过r0-r3传递[citation:1][citation:5]。
bl
指令会跳转到sum函数,同时将返回地址(下一条指令地址)存入lr。
@ 函数收尾(epilogue)str r0, [fp, #-16] @ 将sum返回值存入fp-16的位置(变量ret)mov r3, #0 @ 准备返回值0mov r0, r3 @ 将返回值存入r0sub sp, fp, #4 @ 恢复栈指针(sp = fp - 4)@ sp neededpop {fp, pc} @ 恢复fp并从栈中弹出返回地址到pc
操作分析:
- 恢复栈指针:
sub sp, fp, #4
将sp恢复到压入fp前的状态。 - 恢复调用现场:
pop {fp, pc}
恢复保存的fp,并将保存的返回地址(lr)直接弹出到pc,实现函数返回[citation:5]。- 这里利用了
pop
指令可以指定pc的特性,相当于同时执行了pop {fp, lr}
和bx lr
[citation:5]。
- 这里利用了
2. sum函数的堆栈操作
_Z3sumii:
.LFB0:@ 函数序言(prologue)str fp, [sp, #-4]! @ 将fp压栈并更新sp(pre-indexed存储)add fp, sp, #0 @ 设置新的帧指针(fp = sp)sub sp, sp, #20 @ 分配20字节栈空间(局部变量和参数)
操作分析:
-
保存帧指针:
str fp, [sp, #-4]!
使用预索引(pre-indexed)方式将fp压栈,同时sp减4[citation:6]。!
表示写回,即sp = sp - 4,这是满递减堆栈的典型操作[citation:1][citation:4]。
-
建立新栈帧:
add fp, sp, #0
将fp设置为当前sp值,用于访问局部变量和参数。 -
分配局部变量空间:
sub sp, sp, #20
分配20字节栈空间(可能由于对齐要求)[citation:5]。
@ 存储参数和局部变量str r0, [fp, #-16] @ 存储第一个参数a到fp-16str r1, [fp, #-20] @ 存储第二个参数b到fp-20mov r3, #0 @ 初始化temp为0str r3, [fp, #-8] @ 存储temp到fp-8
操作分析:
- 函数参数r0和r1被保存到栈上,这是非优化编译的典型特征(保存参数到栈帧)[citation:1]。
@ 计算a + bldr r2, [fp, #-16] @ 加载a到r2ldr r3, [fp, #-20] @ 加载b到r3add r3, r2, r3 @ 计算a + bstr r3, [fp, #-8] @ 存储结果到temp
@ 函数收尾(epilogue)ldr r3, [fp, #-8] @ 加载temp到r3(返回值)mov r0, r3 @ 将返回值存入r0add sp, fp, #0 @ 恢复栈指针(sp = fp)@ sp neededldr fp, [sp], #4 @ 从栈中恢复fp并更新sp(post-indexed加载)bx lr @ 返回调用者
操作分析:
- 准备返回值:将计算结果通过r0返回(ARM调用约定)[citation:5]。
- 恢复栈指针:
add sp, fp, #0
将sp恢复到fp的位置。 - 恢复帧指针:
ldr fp, [sp], #4
使用后索引(post-indexed)方式从栈中恢复fp,同时sp加4[citation:6]。 - 函数返回:
bx lr
跳转到lr保存的返回地址。
3. 堆栈布局分析
在main函数调用sum函数时,堆栈的典型布局如下(地址从高到低):
高地址
...
main调用者的栈帧
保存的fp <-- main函数的fp初始指向这里
保存的lr
局部变量a [fp-8]
局部变量b [fp-12]
局部变量ret [fp-16]
...
低地址
sum函数被调用时,堆栈布局变为:
高地址
...
main调用者的栈帧
保存的fp <-- main函数的fp指向这里
保存的lr
局部变量a
局部变量b
局部变量ret
保存的fp <-- sum函数的fp初始指向这里
sum的参数和局部变量
...
低地址