前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >arm平台根据栈进行backtrace的方法

arm平台根据栈进行backtrace的方法

作者头像
coderhuo
发布2018-08-29 15:18:53
4.9K0
发布2018-08-29 15:18:53
举报
文章被收录于专栏:coderhuocoderhuo

本文主要介绍在arm平台回溯函数调用栈(backtrace)的方法。

一、 背景

嵌入式设备开发过程中,难免会遇到各种死机问题。这类问题的定位一直是开发人员的噩梦。 死机问题常见定位手段如下:

  • 根据打印/日志信息梳理业务逻辑,排查代码;
  • 设备死机的时候输出函数调用栈(backtrace),结合符号文件表/反汇编文件定位问题;
  • 输出死机时的内存镜像(coredump),利用gdb还原“案发现场”。

三种定位手段中,第一种是最基本的,提供的信息也最少; 第二种能够给出函数调用关系,但是一般无法给出各个参数的值;第三种不仅能够给出函数调用关系,还能查看各个参数的值。但是后两种方法对编译工具链、系统都有一定的要求。

不过大部分嵌入式实时操作系统(RTOS)不支持生成coredump,下面主要介绍backtrace。

二、 backtrace

做backtrace最方便的就是使用gcc自带的backtrace功能,编译的时候加上-funwind-tables选项(该选项对性能无影响但是会使可执行文件略微变大),异常处理函数中调用相关函数即可输出函数调用栈,但是这依赖于你所用的编译工具链是否支持。

另外一种方式就是集成第三方的工具(如unwind),但是这取决于该工具是否支持你的系统。很多开源软件对RTOS的支持并不好,特别是在一些商用的闭源RTOS上,移植开源软件更是比较困难。

下面介绍一种不依赖于第三方工具,不依赖编译工具链的backtrace方法。

1. 栈帧

函数调用过程是栈伸缩的过程。调用函数的时候入参、寄存器和局部变量入栈,栈空间增长,函数返回的时候栈收缩。每个函数都有自己的栈空间,被称为栈帧,在被调用的时候创建,在返回的时候销毁。函数main调用func1的时候,栈帧如下图所示:

函数调用过程中涉及四个重要的寄存器:PC、LR、SP和FP。注意,每个栈帧中的PC、LR、SP和FP都是寄存器的历史值,并非当前值。PC寄存器和LR寄存器均指向代码段, 其中PC代表代码当前执行到哪里了,LR代表当前函数返回后,要回到哪里去继续执行。SP和FP用来维护栈空间,其中SP指向栈顶,FP指向上一个栈帧的栈顶。

上图中蓝灰色部分是main函数的栈帧,绿色部分是func1的栈帧。左边的标有SP、FP的箭头分别指向func1的栈顶和main的栈顶。右边的两条折线代表函数func1返回的时候(栈收缩),SP和FP将要指向的地方。

如此看来,栈是通过FP和SP寄存器串成一串的,每个单元就是一个栈帧(也就是一个函数调用过程)。又由于LR是指向调用函数的(即PC寄存器的历史值,通过addr2line工具或者把可执行文件反汇编,可以看到func1中的LR落在main函数中,并且指向调用func1的下一条语句)。那么,如果能得到每个栈帧中的LR值,就能得到整个的函数调用链。

我们可以根据FP和SP寄存器回溯函数调用过程,以上图为例:函数func1的栈中保存了main函数的栈信息(绿色部分的SP和FP),通过这两个值,我们可以知道main函数的栈起始地址(也就是FP寄存器的值), 以及栈顶(也就是SP寄存器的值)。得到了main函数的栈帧,就很容易从里面提取LR寄存器的值了(FP向下偏移4个字节即为LR),也就知道了谁调用了main函数。以此类推,可以得到一个完整的函数调用链(一般回溯到 main函数或者线程入口函数就没必要继续了)。实际上,回溯过程中我们并不需要知道栈顶SP,只要FP就够了。

2. 编译优化

仔细考虑下,栈帧中保存的PC值是没啥用的,有LR就够了。另外,并非每个函数都需要FP(具体哪些函数的栈帧中有FP哪些函数中没有,由编译器根据编译选项决定),那么也没必要为它重新开辟一个栈帧,继续在调用者的栈帧上运行即可。

gcc编译选项-fomit-frame-pointer就是优化FP寄存器的,这样可以把FP寄存器省下来在其他地方使用,可以提高运行效率,arm平台最新版本的编译器都是默认打开该选项的。另外,gcc编译的优化选项如 o1、o2等也会对影响栈帧布局。

这么看来 ,通过FP寄存器回溯的方式行不通了!!!

3. 通过追踪栈变化回溯函数

由于函数调用完毕要返回,所以在调用函数的时候,LR寄存器的值必须入栈,也就是说,每个栈帧中肯定包含LR寄存器的历史值,并且LR位于当前栈帧的栈底。另外,SP指针我们肯定是可以拿到的。下面以一个例子看下如何回溯:

下面的代码中func1调用func3, 入参param是NULL, func3中访问非法内存导致程序异常。

struct obj
{
    int a;
    int b;
};

int func3(struct obj *param, int a, int b, char *dst_str, char *src_str)
{
    int len;
    int i;

    param->a = param->b;	/* 非法访问 */

    for (i = 0; i < b; i++)
    {
        a--;
    }
    
    strcpy(dst_str, src_str);
    len = strlen(src_str);
    
    return (a + b + len);
}

void func1(char *src_str)
{
    int result = 0;
    char str[8];

    if (src_str == NULL)
    {
        return;
    }

    if (!strcmp(src_str, "backtrace"))
	{
        result = func3(NULL, 4, 3, str, "111");
	}
	else if (!strcmp(src_str, "stack_protector"))
	{
	    /* 32 个1, 远大于str 容量*/
        result = func2(4, 3, str, "11111111111111111111111111111111");	
	}
    
    return;
}

程序异常的栈信息如下:

可以看到问题出现时的PC寄存器取值为0xa02f8524, SP寄存器的值为0xa84f26d8。通过反汇编代码可以看到,PC指针落在函数func3内部。另外看到func3中入栈了4个寄存器,每个寄存器4个字节,lr的值(也就是栈底)应该位于0xa84f26d8(当前的栈顶)向上偏移12个字节的地方,也就是0xa84f26e4的地方,从栈信息中可以看到这里的数值是0xa02f85d8,指向函数func1。

从下图func1的汇编代码中可以看到,栈增长了16个字节,并且有两个寄存器入栈。本栈帧中lr的位置应该在0xa84f26e4(func3栈帧中lr的位置)的基础上向上偏移24个字节,也就是0xa84f26fc,从栈信息中可以看到这里的数值是0xa02f8a70, 指向函数func2,同样的方法,可以得到再往上一级调用是0xa029d404, 指向func。这样我们就得到了下面的调用链:

func –> func2 –> func1 –> func3

4. 程序实现

在异常处理函数中,根据以上思路,添加自定义的backtrace函数,可以实现函数调用栈回溯。

实现过程中需要根据pc指针遍历代码区,识别每个函数中的栈相关操作指令,计算lr位置,依次循环。另外需要判断回溯的终止点。

这种方法最大的优点就是对可执行文件大小、程序性能都不会产生影响,无副作用。 局限性在于整个机制是基于栈的,如果栈被破坏了,就完了

三、 栈保护

如果代码中出现类似下面的情况,栈会被破坏,上面提到的回溯机制也将会失效。这种问题,死机的地方一般不是出问题的地方,打印出来的pc指针也是乱七八糟的,对定位问题很不利。

void func(void)
{
	char str[4];
	memset(str, 0, 100);
	return;
}

为了能在出问题的时候就把异常抛出,可以引入gcc的编译选项-fstack-protector/-fstack-protector-all/-fstack-protector-strong,它们会在栈帧之间插入特定的内容作为安全边界,函数返回的时候对该边界做校验,如果发现被修改就抛出异常。我们可以在异常处理函数中把PC指针打出来,从而知道死在那里了。同时还可以根据SP指针把栈内容打出来一部分,观察被踩的区域,结合代码人工排查。

遗憾的是某些RTOS使用的编译工具链不支持栈保护编译选项,好在资参考资料3中给出了一种实现方法。

栈保护有一定的局限性,并非所有的栈溢出问题都能被检测到,另外开启后执行的指令增多了,对性能或多或少会有影响。内部调试版本可以使用该方法定位相关问题。

四、 参考资料

  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
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-11-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、 背景
  • 二、 backtrace
    • 1. 栈帧
      • 2. 编译优化
        • 3. 通过追踪栈变化回溯函数
          • 4. 程序实现
          • 三、 栈保护
          • 四、 参考资料
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档