还剩4页未读,继续阅读
文本内容:
本文主要介绍在平台回溯函数调用栈的方法arm backtraceI二匕卓
一、月京嵌入式设备开发过程中,难免会遇到各种死机问题这类问题的定位一直是开发人员的噩梦死机问题常见定位手段如下根据打印/日志信息梳理业务逻辑,排查代码;•设备死机的时候输出函数调用栈,结合符号文件表/反汇编文件定位问题;•backtrace输出死机时的内存镜像利用还原案发现场〃•coredump,gdb三种定位手段中,第一种是最基本的,提供的信息也最少;第二种能够给出函数调用关系,但是一般无法给出各个参数的值;第三种不仅能够给出函数调用关系,还能查看各个参数的值但是后两种方法对编译工具链、系统都有一定的要求不过大部分嵌入式实时操作系统不支持生成下面主要介绍RTOScoredump,backtraceobacktrace
二、做最方便的就是使用自带的功能,编译的时候加上backtrace gccbacktrace选项该选项对性能无影响但是会使可执行文件略微变大,异常处理函-funwind-tables数中调用相关函数即可输出函数调用栈,但是这依赖于你所用的编译工具链是否支持另外一种方式就是集成第三方的工具如,但是这取决于该工具是否支持你的系unwind统很多开源软件对的支持并不好,特别是在一些商用的闭源上,移植开源RTOS RTOS软件更是比较困难下面介绍一种不依赖于第三方工具,不依赖编译工具链的方法backtrace栈帧函数调用过程是栈伸缩的过程调用函数的时候入参、寄存器和局部变量入栈,栈空间增长,
1.函数返回的时候栈收缩每个函数都有自己的栈空间,被称为栈帧,在被调用的时候创建,在返回的时候销毁函数调用的时候,栈帧如下图所示main fund函数调用过程中涉及四个重要的寄存器、、和注意,每个栈帧中的、PC LR SP FP PCO、和都是寄存器的历史值,并非当前值寄存器和寄存器均指向代码段,LRSP FPPC LR其中代表代码当前执行到哪里了,代表当前函数返回后,要回到哪里去继续执行PC LR和用来维护栈空间,其中指向栈顶,指向上一个栈帧的栈顶SP FP SP FP上图中蓝灰色部分是函数的栈帧,绿色部分是的栈帧左边的标有、main funclSP FP的箭头分别指向的栈顶和的栈顶右边的两条折线代表函数返回的时funcl mainfund候(栈收缩),和将要指向的地方SPFP如此看来,栈是通过和寄存器串成一串的,每个单元就是一个栈帧(也就是一个函FP SP数调用过程)又由于是指向调用函数的(即寄存器的历史值,通过LR PCaddr2line工具或者把可执行文件反汇编,可以看到中的落在函数中,并且指向调用fund LR main的下一条语句)那么,如果能得到每个栈帧中的值,就能得到整个的函数调用fund LR链我们可以根据和寄存器回溯函数调用过程,以上图为例函数的栈中保存了FPSPfund函数的栈信息(绿色部分的和),通过这两个值,我们可以知道函数main SPFP main的栈起始地址(也就是寄存器的值),以及栈顶FP(也就是寄存器的值)得到了函数的栈帧,就很容易从里面提取寄存器的SP mainLR)值了(向下偏移个字节即为,也就知道了谁调用了函数以此类推,可FP4LRmain以得到一个完整的函数调用链(一般回溯到函数或者线程入口函数就没必要继续了)main实际上,回溯过程中我们并不需要知道栈顶只要就够了SP,FP编译优化仔细考虑下,栈帧中保存的值是没啥用的,有就够了另外,并非每个函数都需要PCLR
2.(具体哪些函数的栈帧中有哪些函数中没有,由编译器根据编译选项决定),那么FP FP也没必要为它重新开辟一个栈帧,继续在调用者的栈帧上运行即可编译选项就是优化寄存器的,这样可以把寄存器省下gcc-fomit-frame-pointer FPFP,来在其他地方使用,可以提高运行效率平台最新版本的编译器都是默认打开该选项arm的另外,编译的优化选项如、等也会对影响栈帧布局gcc102这么看来,通过寄存器回溯的方式行不通了!!!FP通过追踪栈变化回溯函数由于函数调用完毕要返回,所以在调用函数的时候,寄存器的值必须入栈,也就是说,LR
3.每个栈帧中肯定包含寄存器的历史值,并且位于当前栈帧的栈底另外,指针LR LRSP我们肯定是可以拿到的下面以一个例子看下如何回溯下面的代码中调用fund func3,入参是中访问非法内存param NULL,func3导致程序异常int b;};int func3sti非法访问*/tr,char*src_str{int len;int i;param-fori=0;ib;i++strcpydst_str,src_str;len=return a+b+strlensrc_str;len;}void fundchar*src_str{int result=0;char str
[8];if src_str==NULL{return;}if!strcmpsrc_str,backtrace{result=func3NULL,4,3,str,111;}else if★个远大于容量!strcmpsrc str,n nstack_protector{/321,strresult=func24,3,str,return;}程序异常的栈信息如下:可以看到问题出现时的寄存器取值为寄存PC OxaO2f8524,SP器的值为通过反汇编代码可以看到,指针落在函数内部另外看到Oxa84f26d8PC func3func3中入栈了个寄存器,每个寄存器个字节,卜的值(也就是栈底)应该位于44Oxa84f26d8(当前的栈顶)向上偏移个字节的地方,也就是的地方,从栈信息中可120xa84f26e4以看到这里的数值是,指向函数OxaO2f85d8funcl从下图的汇编代码中可以看到,栈增长了个字节,并且有两个寄存器入栈本栈fund16(帧中的位置应该在栈帧中的位置)的基础上向上偏移个lr0xa84f26e4func3lr24字节,也就是,从栈信息中可以看到这里的数值是指向函数Oxa84f26fc OxaO2f8a70,同样的方法,可以得到再往上一级调用是指向这样我们就得func2,OxaO29d404,func0到了下面的调用链func-func2-fund-func
3.程序实现在异常处理函数中,根据以上思路,添加自定义的函数,可以实现函数调用栈4backtrace回溯实现过程中需要根据指针遍历代码区,识别每个函数中的栈相关操作指令,计算卜位置,pc依次循环另外需要判断回溯的终止点这种方法最大的优点就是对可执行文件大小、程序性能都不会产生影响,无副作用局限性在于整个机制是基于栈的,如果栈被破坏了,就完了
三、栈保护如果代码中出现类似下面的情况,栈会被破坏,上面提到的回溯机制也将会失效这种问题,死机的地方一般不是出问题的地方,打印出来的指针也是乱七八糟的,对定位问题很不pc利为了能在出问题的时候就把异常抛出,可以引入的编译选项-gcc fstack-它们会在栈帧之间插入protector/-fstack-protector-all/-fstack-protector-strong,特定的内容作为安全边界,函数返回的时候对该边界做校验,如果发现被修改就抛出异常我们可以在异常处理函数中把指针打出来,从而知道死在那里了同时还可以根据PC SP指针把栈内容打出来一部分,观察被踩的区域,结合代码人工排查遗憾的是某些使用的编译工具链不支持栈保护编译选项,好在资参考资料中给出RTOS3了一种实现方法栈保护有一定的局限性,并非所有的栈溢出问题都能被检测到,另外开启后执行的指令增多了,对性能或多或少会有影响内部调试版本可以使用该方法定位相关问题
四、参考资料
1.https://stackoverflow.com/questions/15752188/arm-link-register-and-frame-pointer
2.https://yosefk.com/blog/getting-the-call-stack-without-a-frame-pointer.html
3.http://antoinealb.net/programming/2016/06/01/stack-smashing-protector-on-microcontrollers.html。
个人认证
优秀文档
获得点赞 0