这个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;
}
在 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, #0
sub sp, sp, #20
str r0, [fp, #-16]
str r1, [fp, #-20]
mov r3, #0
str r3, [fp, #-8]
ldr r2, [fp, #-16]
ldr r3, [fp, #-20]
add r3, r2, r3
str r3, [fp, #-8]
ldr r3, [fp, #-8]
mov r0, r3
add sp, fp, #0
@ sp needed
ldr fp, [sp], #4
bx 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 = 0
push {fp, lr}
add fp, sp, #4
sub sp, sp, #16
mov r3, #10
str r3, [fp, #-8]
mov r3, #20
str r3, [fp, #-12]
ldr r1, [fp, #-12]
ldr r0, [fp, #-8]
bl _Z3sumii
str r0, [fp, #-16]
mov r3, #0
mov r0, r3
sub sp, fp, #4
@ sp needed
pop {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(嵌入式应用程序二进制接口)属性。它们用于设置编译器和链接器的配置。
具体来说:
属性编号 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调试中很重要:
给定以下两个函数声明:
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, #0
sub sp, sp, #20
str r0, [fp, #-16]
str r1, [fp, #-20]
mov r3, #0
str r3, [fp, #-8]
ldr r2, [fp, #-16]
ldr r3, [fp, #-20]
add r3, r2, r3
str r3, [fp, #-8]
ldr r3, [fp, #-8]
mov r0, r3
add sp, fp, #0
@ sp needed
ldr fp, [sp], #4
bx lr
.cantunwind
.fnend
.size _Z3sumii, .-_Z3sumii
这段代码展示了函数调用时ARM架构下的堆栈管理机制,包括函数调用时的参数传递、局部变量存储以及返回地址处理等。
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]。
push
等同于STMFD
指令,寄存器按编号升序压入从高到低的地址[citation:4][citation:6]。建立新栈帧:add fp, sp, #4
将帧指针设置为当前sp+4的位置,指向保存的fp位置,用于后续访问局部变量和参数[citation:1]。
分配局部变量空间:sub sp, sp, #16
将栈指针下移16字节,为局部变量a、b和ret分配空间[citation:5]。
@ 局部变量初始化
mov r3, #10 @ 将立即数10存入r3
str r3, [fp, #-8] @ 将r3的值存入fp-8的位置(变量a)
mov r3, #20 @ 将立即数20存入r3
str r3, [fp, #-12] @ 将r3的值存入fp-12的位置(变量b)
操作分析:
@ 调用sum函数前的参数准备
ldr r1, [fp, #-12] @ 将变量b的值加载到r1(第二个参数)
ldr r0, [fp, #-8] @ 将变量a的值加载到r0(第一个参数)
bl _Z3sumii @ 调用sum函数(会修改lr)
操作分析:
bl
指令会跳转到sum函数,同时将返回地址(下一条指令地址)存入lr。 @ 函数收尾(epilogue)
str r0, [fp, #-16] @ 将sum返回值存入fp-16的位置(变量ret)
mov r3, #0 @ 准备返回值0
mov r0, r3 @ 将返回值存入r0
sub sp, fp, #4 @ 恢复栈指针(sp = fp - 4)
@ sp needed
pop {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]。_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-16
str r1, [fp, #-20] @ 存储第二个参数b到fp-20
mov r3, #0 @ 初始化temp为0
str r3, [fp, #-8] @ 存储temp到fp-8
操作分析:
@ 计算a + b
ldr r2, [fp, #-16] @ 加载a到r2
ldr r3, [fp, #-20] @ 加载b到r3
add r3, r2, r3 @ 计算a + b
str r3, [fp, #-8] @ 存储结果到temp
@ 函数收尾(epilogue)
ldr r3, [fp, #-8] @ 加载temp到r3(返回值)
mov r0, r3 @ 将返回值存入r0
add sp, fp, #0 @ 恢复栈指针(sp = fp)
@ sp needed
ldr fp, [sp], #4 @ 从栈中恢复fp并更新sp(post-indexed加载)
bx lr @ 返回调用者
操作分析:
add sp, fp, #0
将sp恢复到fp的位置。ldr fp, [sp], #4
使用后索引(post-indexed)方式从栈中恢复fp,同时sp加4[citation:6]。bx lr
跳转到lr保存的返回地址。在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的参数和局部变量
...
低地址