https://www.jianshu.com/p/91c5dc0a8bb9
背景:
最近在某个RTOS上遇到一个系统BUG,几经折腾,终于将其斩于马下。结局美好,过程却很曲折,在分析定位问题的时候,顺便把ARM上C函数调用stack frame机制捋了一遍,记录并分享一下。概念:栈:1)从数据结构的角度来理解,栈是一种描述先进后出的数据结构;2)从进程的内存空间角度来理解,栈是一种特殊的内存段,用于存放局部变量、函数参数、返回值等;第一种角度,用来描述本身的特性,第二种角度,是将这种数据结构的特性用于实际的内存空间中。栈帧:每个进程都会有自己的栈空间,而进程中的各个函数也会维护自己本身的一个栈的区域,这个区域就是栈帧。那么一个函数的栈帧的区域是如何来界定的呢?当然,我首先会普及ARM的几个特殊寄存器功能。R11:frame pointer,FP寄存器R12:IP寄存器,用于暂存SPR13:stack pointer,SP寄存器R14:link register,LR寄存器R15:PC寄存器而在ARM上,函数的栈帧是由SP寄存器和FP寄存器来界定的,相信你应该见过下边这张比较经典的图了:
上图描述的是main函数调用func1函数的栈帧情况,从图可知,当main函数调用func1函数时,func1函数会先将PC、LR、SP、FP四个寄存器压到栈上边,其中SP和FP的值分别指向main函数栈帧的两个边界,LR的值保存的是func1调用结束之后的返回值,PC值表示的是当前执行到的指令地址,放置的是进入func1后的指令地址。紧接着就会在栈上分配一片区域,用于放置局部变量等。 如果func1中还调用了func2子函数,那么也会为func2创建一个栈帧,并且func2的SP和FP会指向func1栈帧的两个边界。这样当函数返回的时候,参数进行出栈,也能找到Caller函数,这个也就是backtrace的原理了。示例: 反汇编分析某段代码,如下图所示:
红色部分,表明进入到函数时先将几个特殊的寄存器压栈黄色部分,sub sp, sp, #16,表明开辟一个4 x 32bit大小的栈区域蓝色部分,将传入的参数压栈,在ARM ATPCS中规定,寄存器R0-R3用来传参绿色部分,调用子函数 那么,我们顺道看看子函数的栈帧区域吧:
从图中可以看出,机制是一样的,当最终queue_push函数调用结束后,栈上的数据进行出栈,根据fp和ip,便能找到workflow_gather_input函数的栈帧了。 当然,并不是所有函数调用都需要先push {fp, ip, lr, pc},当子函数调用过程中,并不会去改变这些值的时候,就不需要压栈,说白了,压栈的目的就是为了在使用完的时候能恢复原来的状态。我会再次提供一个例子:
strlen函数中没有子函数的调用,所以进入函数后,直接就在栈上分配4 * 32bit大小的区域了。 栈里边能分析出每个参数的值,以及函数调用时的传参,这对分析与定位问题很有帮助。成熟的系统可能会提供一堆工具来dump stack,并去分析调用关系,但是在RTOS上,很多却并不完善,需要一定的低层知识去分析才能解决问题。
作者:Loyen
链接:https://www.jianshu.com/p/91c5dc0a8bb9
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。