gcc之-fomit-frame-point
Kernel里的dump_stack(), oops打印出来的backstrace调用链表是怎样实现的呢?
大家都知道当发生函数调用的时候,函数的参数传递,返回值传递都要遵循一定的规则,在ARM体系架构下,这个规则叫做Procedure Call Standard for the ARM Architecture。在这个规则里规定了函数调用的时候,返回地址在LR里面,第一到第四个参数在r0~r3里面,第五到第八个参数在Stack里面,返回值在r0里面。这是基本规则,C编译器产生汇编指令是必须遵循这些规则,这也是ABI(Application Binary Interface)的一部分。另外,为了实现连续的函数调用,如fun_a()->func_b()->func_c(),每个函数的入口处必须先把LR压到stack里,否则func_b()调了func_c()之后,LR变成了func_c()的返回地址,而func_b()返回地址就丢失了。
有了以上这些原则,可以追溯函数的调用历史。即只要找到堆栈里的LR就知道是那个从那个函数调用过来的。要知道stack里LR的位置就必须要知道当前函数stack开始的地方,否则stack里那个位置存放了LR。通用的定位每个函数stack开始的地方的方法是在编译的时候让编译器在目标代码中嵌入stack frame(栈帧)。此外,gcc还能够在elf文件中生成unwind table,unwind table也能实现追踪call stack。下面主要讲述stack frame。
什么是Stack frame:
stack frame(栈帧)是用来追踪代码的调用过程和调用时的参数,通过读取stack frame信息可以知道到执行到当前位置的函数调用链表(call stack)。
Stack frame的本质是每次函数调用都在stack里记录一个frame,每一次函数调用叫做一个frame。ARM里的fp寄存器就是用来指证当前所在函数的frame的,fp总是指向当前函数堆栈的frame。
Stack frame的产生方法:
stack frame是由编译器产生的,也就是gcc在生成可执行代码时,在每个函数入口的地方“放置”一个stack frame。在调用gcc编译时可以指定参数来要求gcc产生或不产生stack frame,这个参数是:
-fomit-frame-point:让gcc不产生stack frame
-fno-omit-frame-pointer:让gcc产生stack frame
如果不指定这两个参数,是否产生stack frame取决于gcc default是omit-frame-point,还是 no-omit-frame-pointer。
Stack frame是什么样的
通过一个简单的C程序来看一下在binary里,stack frame究竟是怎样的
test.c:
static int func_b(int a, int b, int c, int d, int e, int f) { return a + b + c + d + e + f; } static int func_c(int a, int b) { return a - b; } static int func_a(int x, int y) { int a = 3; int b = 4; int c = 5; int d = 6; int e = 7; int f = 8; int ret; ret = func_b(a, b, c, d, e, f); ret = func_c(a, ret); return ret; } int main(int argc, char * argv[]) { int a = 1; int b = 2; int ret; ret = func_a(a, b); return ret; } |
编译有stack frame的binary:
arm-none-linux-gnueabi-gcc -fno-omit-frame-pointer test.c -o test.no-omit-frame-pointer
编译无stack frame的binary:
arm-none-linux-gnueabi-gcc -fomit-frame-pointer test.c -o test.omit-frame-pointer
分别objdump以上编译所得的两个binary:
arm-none-linux-gnueabi-objdump -S test.no-omit-frame-pointer > test.no-omit-frame-pointer.objdump
arm-none-linux-gnueabi-objdump -S test.omit-frame-pointer > test.omit-frame-pointer.objdump
打开两个dump出来的assembly (只关注我们的main(), func_a(), func_b()三个函数,忽略libc的部分):
test.no-omit-frame-pointer.objdump:
000084e4 <<func_b>>: 84e4: e52db004 push {fp} ; (str fp, [sp, #-4]!) 84e8: e28db000 add fp, sp, #0 84ec: e24dd014 sub sp, sp, #20 84f0: e50b0008 str r0, [fp, #-8] 84f4: e50b100c str r1, [fp, #-12] 84f8: e50b2010 str r2, [fp, #-16] 84fc: e50b3014 str r3, [fp, #-20] ; 0xffffffec 8500: e51b2008 ldr r2, [fp, #-8] 8504: e51b300c ldr r3, [fp, #-12] 8508: e0822003 add r2, r2, r3 850c: e51b3010 ldr r3, [fp, #-16] 8510: e0822003 add r2, r2, r3 8514: e51b3014 ldr r3, [fp, #-20] ; 0xffffffec 8518: e0822003 add r2, r2, r3 851c: e59b3004 ldr r3, [fp, #4] 8520: e0822003 add r2, r2, r3 8524: e59b3008 ldr r3, [fp, #8] 8528: e0823003 add r3, r2, r3 852c: e1a00003 mov r0, r3 8530: e28bd000 add sp, fp, #0 8534: e8bd0800 ldmfd sp!, {fp} 8538: e12fff1e bx lr 0000853c <<func_c>>: 853c: e52db004 push {fp} ; (str fp, [sp, #-4]!) 8540: e28db000 add fp, sp, #0 8544: e24dd00c sub sp, sp, #12 8548: e50b0008 str r0, [fp, #-8] 854c: e50b100c str r1, [fp, #-12] 8550: e51b2008 ldr r2, [fp, #-8] 8554: e51b300c ldr r3, [fp, #-12] 8558: e0633002 rsb r3, r3, r2 855c: e1a00003 mov r0, r3 8560: e28bd000 add sp, fp, #0 8564: e8bd0800 ldmfd sp!, {fp} 8568: e12fff1e bx lr 0000856c <<func_a>>: 856c: e92d4800 push {fp, lr} 8570: e28db004 add fp, sp, #4 8574: e24dd030 sub sp, sp, #48 ; 0x30 8578: e50b0028 str r0, [fp, #-40] ; 0xffffffd8 857c: e50b102c str r1, [fp, #-44] ; 0xffffffd4 8580: e3a03003 mov r3, #3 8584: e50b3008 str r3, [fp, #-8] 8588: e3a03004 mov r3, #4 858c: e50b300c str r3, [fp, #-12] 8590: e3a03005 mov r3, #5 8594: e50b3010 str r3, [fp, #-16] 8598: e3a03006 mov r3, #6 859c: e50b3014 str r3, [fp, #-20] ; 0xffffffec 85a0: e3a03007 mov r3, #7 85a4: e50b3018 str r3, [fp, #-24] ; 0xffffffe8 85a8: e3a03008 mov r3, #8 85ac: e50b301c str r3, [fp, #-28] ; 0xffffffe4 85b0: e51b3018 ldr r3, [fp, #-24] ; 0xffffffe8 85b4: e58d3000 str r3, [sp] 85b8: e51b301c ldr r3, [fp, #-28] ; 0xffffffe4 85bc: e58d3004 str r3, [sp, #4] 85c0: e51b0008 ldr r0, [fp, #-8] 85c4: e51b100c ldr r1, [fp, #-12] 85c8: e51b2010 ldr r2, [fp, #-16] 85cc: e51b3014 ldr r3, [fp, #-20] ; 0xffffffec 85d0: ebffffc3 bl 84e4 <<func_b>> 85d4: e50b0020 str r0, [fp, #-32] ; 0xffffffe0 85d8: e51b0008 ldr r0, [fp, #-8] 85dc: e51b1020 ldr r1, [fp, #-32] ; 0xffffffe0 85e0: ebffffd5 bl 853c <<func_c>> 85e4: e50b0020 str r0, [fp, #-32] ; 0xffffffe0 85e8: e51b3020 ldr r3, [fp, #-32] ; 0xffffffe0 85ec: e1a00003 mov r0, r3 85f0: e24bd004 sub sp, fp, #4 85f4: e8bd8800 pop {fp, pc} 000085f8 <<main>>: 85f8: e92d4800 push {fp, lr} 85fc: e28db004 add fp, sp, #4 8600: e24dd018 sub sp, sp, #24 8604: e50b0018 str r0, [fp, #-24] ; 0xffffffe8 8608: e50b101c str r1, [fp, #-28] ; 0xffffffe4 860c: e3a03001 mov r3, #1 8610: e50b3008 str r3, [fp, #-8] 8614: e3a03002 mov r3, #2 8618: e50b300c str r3, [fp, #-12] 861c: e51b0008 ldr r0, [fp, #-8] 8620: e51b100c ldr r1, [fp, #-12] 8624: ebffffd0 bl 856c <<func_a>> 8628: e50b0010 str r0, [fp, #-16] 862c: e51b3010 ldr r3, [fp, #-16] 8630: e1a00003 mov r0, r3 8634: e24bd004 sub sp, fp, #4 8638: e8bd8800 pop {fp, pc} |
test.omit-frame-pointer.objdump:
000084e4 <<func_b>>: 84e4: e24dd010 sub sp, sp, #16 84e8: e58d000c str r0, [sp, #12] 84ec: e58d1008 str r1, [sp, #8] 84f0: e58d2004 str r2, [sp, #4] 84f4: e58d3000 str r3, [sp] 84f8: e59d200c ldr r2, [sp, #12] 84fc: e59d3008 ldr r3, [sp, #8] 8500: e0822003 add r2, r2, r3 8504: e59d3004 ldr r3, [sp, #4] 8508: e0822003 add r2, r2, r3 850c: e59d3000 ldr r3, [sp] 8510: e0822003 add r2, r2, r3 8514: e59d3010 ldr r3, [sp, #16] 8518: e0822003 add r2, r2, r3 851c: e59d3014 ldr r3, [sp, #20] 8520: e0823003 add r3, r2, r3 8524: e1a00003 mov r0, r3 8528: e28dd010 add sp, sp, #16 852c: e12fff1e bx lr 00008530 <<func_c>>: 8530: e24dd008 sub sp, sp, #8 8534: e58d0004 str r0, [sp, #4] 8538: e58d1000 str r1, [sp] 853c: e59d2004 ldr r2, [sp, #4] 8540: e59d3000 ldr r3, [sp] 8544: e0633002 rsb r3, r3, r2 8548: e1a00003 mov r0, r3 854c: e28dd008 add sp, sp, #8 8550: e12fff1e bx lr 00008554 <<func_a>>: 8554: e52de004 push {lr} ; (str lr, [sp, #-4]!) 8558: e24dd034 sub sp, sp, #52 ; 0x34 855c: e58d000c str r0, [sp, #12] 8560: e58d1008 str r1, [sp, #8] 8564: e3a03003 mov r3, #3 8568: e58d302c str r3, [sp, #44] ; 0x2c 856c: e3a03004 mov r3, #4 8570: e58d3028 str r3, [sp, #40] ; 0x28 8574: e3a03005 mov r3, #5 8578: e58d3024 str r3, [sp, #36] ; 0x24 857c: e3a03006 mov r3, #6 8580: e58d3020 str r3, [sp, #32] 8584: e3a03007 mov r3, #7 8588: e58d301c str r3, [sp, #28] 858c: e3a03008 mov r3, #8 8590: e58d3018 str r3, [sp, #24] 8594: e59d301c ldr r3, [sp, #28] 8598: e58d3000 str r3, [sp] 859c: e59d3018 ldr r3, [sp, #24] 85a0: e58d3004 str r3, [sp, #4] 85a4: e59d002c ldr r0, [sp, #44] ; 0x2c 85a8: e59d1028 ldr r1, [sp, #40] ; 0x28 85ac: e59d2024 ldr r2, [sp, #36] ; 0x24 85b0: e59d3020 ldr r3, [sp, #32] 85b4: ebffffca bl 84e4 <<func_b>> 85b8: e58d0014 str r0, [sp, #20] 85bc: e59d002c ldr r0, [sp, #44] ; 0x2c 85c0: e59d1014 ldr r1, [sp, #20] 85c4: ebffffd9 bl 8530 <<func_c>> 85c8: e58d0014 str r0, [sp, #20] 85cc: e59d3014 ldr r3, [sp, #20] 85d0: e1a00003 mov r0, r3 85d4: e28dd034 add sp, sp, #52 ; 0x34 85d8: e8bd8000 ldmfd sp!, {pc} 000085dc <<main>>: 85dc: e52de004 push {lr} ; (str lr, [sp, #-4]!) 85e0: e24dd01c sub sp, sp, #28 85e4: e58d0004 str r0, [sp, #4] 85e8: e58d1000 str r1, [sp] 85ec: e3a03001 mov r3, #1 85f0: e58d3014 str r3, [sp, #20] 85f4: e3a03002 mov r3, #2 85f8: e58d3010 str r3, [sp, #16] 85fc: e59d0014 ldr r0, [sp, #20] 8600: e59d1010 ldr r1, [sp, #16] 8604: ebffffd2 bl 8554 <<func_a>> 8608: e58d000c str r0, [sp, #12] 860c: e59d300c ldr r3, [sp, #12] 8610: e1a00003 mov r0, r3 8614: e28dd01c add sp, sp, #28 8618: e8bd8000 ldmfd sp!, {pc} |
可以看到有stack frame的binary (test.no-omit-frame-pointer.objdump) 中每个main(), func_a(), func_b()入口的地 方都有push fp, lr和add fp, sp, #4;而无stack frame的binary (test.omit-frame-pointer.objdump) 中函数入口的地方并没有这两句指令。push fp的作用是把调用函数(caller)的fp保存到被调用函数(callee)的stack开始处,随后add fp, sp, #4将fp指向被调用函数(callee) 的stack开始的地方。fp总是指向当前函数的stack开始的地方,通过当前函数stack里保存的caller的fp可以追溯到caller的堆栈。通过fp的逐级连接就可以找到调用链上每个函数的stack,从而每个函数的LR(返回地址)都可以从stack里获得,只要在symbol table里搜索一下LR的值,就可以知道caller是哪个函数了。有了stack frame不但可以通过stack里的LR得到caller的地址,还可以知道函数调用时的参数,回想一下前面说的‘第一到第四个参数在r0~r3里面,第五到第八个参数在Stack里面’。具体分析一下上面的汇编代码,结合 Procedure Call Standard for the ARM Architecture规定的调用原则,绘制出stack frame的结构如下:
从上面的例子可以看到一个函数(如func_a())的stack frame里,开始的地方放的是fp, lr(如果是最后一级的被调用函数,如func_b,func_c则lr无需保存到stack),然后是函数的内部变量,之后是该函数的参数1~参数4,最后是其调用的函数(本例中是func_b())的参数5~参数8。在本例中如果在func_b()中要追溯call stack,从lr可以知道哪个函数调用了它,fp寄存器保存了func_b的stack frame的起始地址,该地址上存放了上一级stack frame的起始地址(caller的stack frame),该地址向stack缩小方向存放了caller调用func_b()的时候传递的参数5~8(如果有参数5~8的话),该地址向stack伸长方向到存放调用下一个函数的参数5~8之前的位置存放了caller调用func_b()的时候传递的参数1~4。