看到一篇文章《冬之焱:谈谈Linux内核的栈回溯与妙用》,来自微信公众号"Linux阅码场"。文章主要写了Linux Backtrace的方法,里面提到ARM栈时,有这么一个图:
文章认为除了unwind模式,arm函数调用后都会压入PC,LR,SP,FP(即R15,R14,R13,R11)几个寄存器;但是,在平常ARM汇编代码中,很少能看到函数调用会压栈这么多寄存器。
实际上,压栈哪些寄存器,很大程度上是由编译选项决定的,下面是相关验证。代码很简单,就是在main 函数中调用了zperf_main进行测试:
1. gcc默认编译,无任何选项:
arm-linux-gnueabi-gcc -o test test.c
压栈了寄存器R4,R11和R14,R4为zperf_main函数中会改变的通用寄存器,R11作为FP指针使用(程序中不会改变),R14作为LR。
2. 加编译选项 -O0
与不加选项完全一致,说明不加选项默认就是O0优化
3. 加编译选项 -O1 或者编译选项-O(两者一致)
arm-linux-gnueabi-gcc -O1 -o test1 test.c
压栈了寄存器R3-R11和R14,此时R14作为LR保存,R3-R11都是作为通用寄存器保存,R11并不作为FP,可以看到后面程序会将它作为通用寄存器使用。
4. 加编译选项 -O2
arm-linux-gnueabi-gcc -O2 -o test2 test.c
压栈了寄存器R3-R10和R14,此时R14作为LR保存,R3-R10都是作为通用寄存器保存,相比O1优化了R11的保存恢复。
5. 加编译选项 -O3
arm-linux-gnueabi-gcc -O3 -o test3 test.c
由于程序比较简单,编译后与O2完全一致。
6. 加编译选项 -fomit-frame-pointer
该选项的作用,在gcc手册中是这么描述的:
Don't keep the frame pointer in a register for functions that don't need one. This avoids the instructions to save, set up and restore frame pointers; it also makes an extra register available in many functions. It also makes debugging impossible on some machines。
简单来说就是通过不保存FP来优化程序性能。
arm-linux-gnueabi-gcc -fomit-frame-pointer -o testf test.c
与不开优化选项的程序相比,可以看到这段代码已不再保存FP。
事实上gcc的所有级别的优化(-O1, -O2, -O3等)都会打开-fomit-frame-pointer,该选项的功能是函数调用时不保存frame指针,在ARM上就是fp,故我们无法按照APCS中的约定来回溯调用栈。但是GDB中仍然可以使用bt命令看到调用栈,为什么?得知GDB v6之后都是支持DWARF2的,也就意味着它可以不依赖fp来回溯调用栈(详见http://gcc.gnu.org/ml/gcc/2003-10/msg00322.html)。
7. 加编译选项 -mapcs
arm-linux-gnueabi-gcc -mapcs -o testm test.c
这个选项使程序严格遵守ARM Procedure Call Standard(ARM过程调用标准规范)中关于arm寄存器的使用、过程调用时出栈和入栈的约定。
可以看到,此时程序才严格按照图1的规律,每个函数调用都会压栈PC,LR,SP,FP作为寄存器栈帧进行保存。