嵌入式系统那些事-文件视角下的arm汇编

0 背景

        上一篇文章我们从指令集的视角解读arm架构,本篇从指令集组成的汇编文件入手对arm架构进行解读。汇编是跟体系结构强相关的一种编程语言,现在实际开发中比较少用汇编直接进行开发,更多的是在定位诸如踩内存等问题时,才会进行反汇编操作,此时就需要知道简单的arm架构下的汇编文件结构和语法,才能更快地抓住汇编文件中的关键信息。本文首先从文件的视角,列举出汇编文件中重要的元素和应用场景;然后从整个文件转化流程的视角,总结了哪些文件可以转成汇编语言,转化后的差异在哪里,得到基本的arm汇编文件结构;最后以反汇编定位踩内存问题,说明arm汇编文件的价值。

1 文件视角下的arm汇编

        如下图所示是汇编文件的核心元素和应用场景的总结,汇编文件一般的扩展名为.S或者.s的形式,跟我们常用的高级语言如C语言一样,有自己的一套指令和伪指令,并且按照一定的语法格式和组织方式形成自己独特的文件结构。按照笔者的经验,当我们拿到一个汇编文件后,可以先从这个文件结构入手,把握整体脉络,抓住其中关键的流程和信息即可,至于说指令完全可以通过查手册的方式得到,因此不需要特意去记,只需要知道简单的即可,后面笔者将会通过c编译成汇编后的文件和反汇编文件,给出文件的结构。在汇编文件中除了指令之外,还有非常重要的元素就是存储指令和数据的寄存器以及内存,在汇编文件中会大量清楚地看到寄存器和内存的交互过程,包括读写数据,计算数据,地址跳转等等。在此处我们最关注的是寄存器中记录的地址信息,还有内存堆栈的信息,如何跟我们的代码关联到一起,汇编文件最终会以二进制文件的方式跑在设备上,那一行行地代码是如何进行映射、怎么执行的。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbGludXNfYmVu,size_20,color_FFFFFF,t_70,g_se,x_16

        在把握了汇编文件的结构、基础指令和代码段地址映射关系后,我们就可以对执行的文件进行反汇编,根据段错误或者栈溢出等踩内存的提示信息,获取调用栈或者寄存器的地址信息,然后将其映射到反汇编文件的地址上,进而定位到具体的代码行。此处要借助一些工具和手段,如进行反汇编的objdump,显示段错误的dmesg,地址到行号转换的addr2line(前提是在编译时有代码行,通常我们的实际项目中很少会加这个代码行号,因为对代码空间有一定影响),利用gdb获得调用栈,根据反汇编的代码进行堆栈图的绘制等,通过这两者的结合就可以较为方便地定位踩内存的问题。网上这方面的案例有很多,但是大多数都是直接使用linux的调试指令手动定位,笔者的经验,上面的这些工具完全可以放到我们的应用层代码中,最好是集成到中间件中,当抛出段错误或者栈溢出后能够自动采用这些工具搜集堆栈信息然后保存到日志,方便快速定位问题。

2 反汇编文件解读

2.1汇编文件生成过程

        在我们的嵌入式开发中,有两个相逆的过程,一个是从我们的c语言文件(或者混编的文件)到最终的可执行二进制文件的过程,这是我们最常见的;还有一个则是由我们的中间文件或者可执行的二进制文件反汇编到汇编文件,通常是在逆向工程或者是定位踩内存等问题时使用,如下图所示,就是这两个过程的总结,蓝色虚线是正向过程,红色实线是逆向过程。正向的过程是为了得到可在机器上执行的二进制,逆向的过程则是为了从当前执行出错的代码定位到具体的函数或者代码行。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbGludXNfYmVu,size_20,color_FFFFFF,t_70,g_se,x_16

        在正向过程中,c语言文件首先通过gcc -o的方式生成目标文件;然后这些目标文件可以通过静态链接或者动态链接的方式,生成可重定位的.a或者.so文件;最后与运行时库如libc.so等共同生成可执行的bin文件,这就是整个编译链接的全流程。在逆向过程中,c语言文件可以通过gcc -S的方式生成汇编文件,每个中间文件和最终的bin文件都可以通过objdump -d的方式生成反汇编后的文件。此时我们就有个疑问,这些文件生成的汇编文件是不是一样的呢?哪个文件是我们在踩内存问题时真正应该去反汇编的呢?反汇编文件中哪些指令或数据是最关键的呢?带着这三个问题,我们继续分析。

2.2 反汇编文件解读

        笔者以经典的hello world输出为例,将上图这两个过程的所有文件都恢复成汇编文件的形式,然后将汇编文件中最关键的指令和数据抓取出来,横向对比如下图所示。图中包含了hello world的c语言文件,汇编文件,反汇编的文件(包括.o/.a/.so/.bin)。

        汇编文件中hello world信息被放置在.rodata的只读数据段,代码段.text中定义了main函数的全局变量,通过call指令调用printf函数,最终通过ret返回,这个结构比较简单,理解上不是很难。接下来的目标文件和静态链接库文件的反汇编文件,开头的文件格式即是elf文件,也是我们后续要重点解读的文件,同样的只读数据段和代码段,但是其中的内容则是通过地址偏移得到,是可重定位的。再接下来的动态链接文件,会发现跟上面的静态链接文件类似,不过偏移地址量更大,因为有其他的链接文件被加入进来,我们平时看到动态链接文件一般也比静态链接文件大些,也是因为导入动态链接文件需要额外的负荷。所以在实际项目中需要综合可重用性,大小选择合适的目标文件。最后就是可执行的二进制文件了,此处设备的基地址被加入进来,同时程序执行真正的入口_start也被引入,像printf这样的c语言库函数,也会将libc库一起链接进来,注意此处并非将libc动态链接库直接加到bin文件中,而只是提供了可访问的地址入口_libc_start_main。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbGludXNfYmVu,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbGludXNfYmVu,size_20,color_FFFFFF,t_70,g_se,x_16

        至此,上述的汇编文件和反汇编文件解读完毕,一个最简单的arm汇编文件结构也呈现出来,笔者将其总结如下图所示。我们前面提到的三个问题的答案也可以进一步解答。由c代码和中间文件生成的汇编文件虽然结构上类似,但是每个阶段的文件中包含的信息是不同的,在踩内存的场景下,我们运行的是bin文件,bin文件中包含了基地址和偏移地址,包含了完整的运行时库,因此可以作为踩内存问题反汇编的基文件。在程序踩内存崩溃后,通常会有调用栈信息或者寄存器信息打印出来,其中包含的地址信息可以重新计算映射到反汇编文件上,进而确定出错的代码行或函数。下一节将详细介绍定位过程。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbGludXNfYmVu,size_20,color_FFFFFF,t_70,g_se,x_16

3 基于反汇编定位踩内存问题

       如下图所示,ptr1和ptr2两个指针分别指向一段内存空间,且二者是连续的,假设此时访问ptr1时,长度超过了可访问界限,在读数据的情况下,会读取到乱码,在写的情况下,就可能访问越界,导致栈溢出。还有些场景未必会报栈溢出,只是临近的数据莫名其妙被修改了。定位这样的踩内存问题我们一般的思路是在被踩内存的附近加上保护,当有外部的函数过来越界访问这段被保护的地址时,就会产生调用栈,就知道哪个函数过来踩了这段内存。如果函数内部自踩的话,就会有堆栈信息打印出来,得到踩内存的地址,通过地址信息的转换,加上反汇编文件,就可以定位到具体的代码行。下面将通过具体的例子介绍所使用的手段和方法。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbGludXNfYmVu,size_20,color_FFFFFF,t_70,g_se,x_16

例1 栈溢出

        如下图展示了栈溢出的场景,通常是由于访问越界导致的。看c代码很显然memset初始化buf的长度超了,执行后,就会报栈溢出,此时并没有堆栈信息打印出来,我们就可以借助gdb工具的bt获取调用栈,如果前面编译时有加-g,就可以看到出错的行号;如果刚好没有加,那就需要借助调用栈中的地址信息推断,首先出错的地址是0x400731这个地址是我们自己的函数地址,最终的地址是0x7f9ecba11200这样的地址说明是使用库函数时栈溢出了,我们就可以在反汇编后的文件中找到这两个地址附近的代码就可以定位到具体的库函数。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbGludXNfYmVu,size_20,color_FFFFFF,t_70,g_se,x_16

例2 段错误

        访问空指针也是一种常见的踩内存问题,如下图所示,指针buf没有初始化就被使用了,此时系统会报段错误的提示信息,同样没有调用栈。这个问题怎么定位呢?此时我们的思路还是要得到出错的地址,借助dmesg,我们就可以看到在执行到哪个地址时出错了,即ip寄存器存储的地址,出错的文件是libc库,这个库的基地址和大小也都可以得到,错误码是6表示用户态写错误,有了上面的两个地址信息,就可以得到在libc库的偏移地址0x172b69处的函数执行错误,最后通过ldd获取libc库文件的位置,再通过addr2line工具,就可以得到出错的库函数memset。这个例子并没有用反汇编的方式确定代码行,感兴趣的可以尝试用反汇编的方式去定位一下。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbGludXNfYmVu,size_20,color_FFFFFF,t_70,g_se,x_16

        上面两个例子只是踩内存问题的常见的定位手段,在很多场景下需要更加深入的研读反汇编文件,通过不断的实验尝试才能得到真正踩内存的地方。

4 小结

        本文笔者从汇编语言的视角介绍了arm架构,通过对比解读不同的反汇编文件,试图带领读者抓住最关键的汇编文件结构和地址信息,并且用两个实例介绍了地址这个关键信息的价值,希望对读者有所启发。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值