函数调用过程的汇编代码分析(arm64指令集),顺便关注一下栈空间的分配与回收。
下述C语言程序包含了一个简单的add()函数,该函数接受两个整型参数,计算并返回两者的和。
main()函数调用执行了add()函数来计算3+2的和,并将计算结果打印出来。
1 | #include <stdio.h> |
为了从机器指令层面理解上述函数调用过程中的传参、跳转、返回等行为,我们在浏览器中访问了下述网站:
在粘贴相关代码并选择ARM64 gcc 9.3选项后,我们得到下述汇编语言指令序列:
1 | add(int, int): |
第2 ~ 11行:函数add()的实体
第2行:将栈指针(stack pointer)寄存器sp的值减去32,结果仍存储到sp中。显然,该指令事实上为自动变量分配了栈空间。
第10行:在函数返回前,将栈指针(stack pointer)寄存器的值增加32,结果仍存储到sp中。显然,该指令事实上回收了栈空间,回收的数量与分配的数量一致。
第3行:将寄存器w0中的32位数值存储到内存地址sp+12处,结合后续代码可以知道,sp+12处存储的即是形参a。
第4行:将寄存器w1中的32位数值存储到内存地址sp+8处,结合后续代码可以知道,sp+8处存储的即是形参b。
第5行:将存储于sp+12处的形参a值装入寄存器w1。
第6行:将存储于sp+8处的形参b值装入寄存器w0。
第7行:将寄存器w1和w0的值相加,结果存入w0,此处事实上相加的即是形参a和b。
第8行:将寄存器w0中的32位数值存入地址sp+28处。可以推测,sp+28处存储的即为自动变量t。
第9行:将sp+28处的自动变量t装入寄存器w0, 结合后续代码可知,w0寄存器被用于向函数的调用者返回计算结果。
第11行:返回x30寄存器所标识的地址继续执行,此时,x30寄存器所标识的地址为上述代码的第22行。
第14 ~ 29行:函数main()的实体
第17行:将整数3存入w0寄存器。
第18行:将w0中的32位值存入sp+28处。合理推测,sp+28处为变量x的存放位置。
第19行:将整数2存入w1寄存器,结合add()代码可知,add()通过w1寄存器获得参数b。
第20行:将sp+28处的变量x的值存入寄存器w0。结合add()代码可知,add()通过w0寄存器获得参数a。
第 21行:在准备好函数调用的参数(a,b分别在w0和w1寄存器内)后,branch & link指令完成两件工作:
- 将pc+4的值存入x30寄存器,其中,pc表示当前指令地址(第21行),pc+4即指向当前指令的下一条,也就是第22行。
- 跳转到add()函数函数,即第2行。
当add()函数完成任务后,第11行ret指令将返回x30寄存器标识的地址继续执行,即返回代码的第22行。
第22行:将w0寄存器的23位数值装入内存地址sp+28处。结合add()函数代码可知,此时的w0寄存器事实上存储的是add()函数的计算结果。这行代码事实上取得了add()函数的返回值,并将其存入x变量(sp+28)。
在代码的后续部分,我们还看到main()函数对printf()函数的调用… 略。
总结
- 函数执行过程中,确实通过修改sp(栈顶指针)的值来分配和回收栈空间,栈空间用于存储自动变量。
- 在寄存器够用的情况下,程序会尽量通过寄存器传参,并获取返回值。因为寄存器的访问速度比内存快得多。