[编译链接]单片机分散加载机制、地址无关编译的分析(一)

发布于 2020-07-03 11:53:53

一、前言

  • 前段时间写了一篇博文简单的说明了地址无关编译中的ropi和rwpi技术,简单的用一个我目前正在使用的os进行说明(基于rt-thread),部分细节没有讲清楚,因此用此篇文章来进一步介绍地址无关编译,争取能够分析透彻。
  • 个人比较喜欢技术分享,认为技术不应该有壁垒,应该是开放的、互相进步的。本来计划6月份参与rt-thread的一个测评计划的,但由于自身一些不可描述的原因只能放弃了,所以拿出这篇文章(这篇文章之前在论坛中也出现过,当时只是一个链接,最近rt论坛推出了文章功能,所以顺便来尝尝鲜)。
  • 由于单篇篇幅限制10000字符,所以无奈只能把文章分割(由此分割的七零八落,大家多担待)。

二、单片机的分散加载机制

  • 本次仍旧以单片机为例,因为单片机我比较熟悉,再加上单片机比较低端(相比FPGA、DSP、Intel等CPU,确实比较简单)。实际上是因为我只会单片机,菜鸟本菜实锤。
  • 以下我以基于ARM Cortex-M4架构的一款国产单片机作为分析对象(大家可以类比到stm32f407,工作原因,再具体的不方便透露)。

1.分散加载

分散加载实际上规定了单片机(或者机器,后面不再赘述)在正式上 电执行前后,如何准备好运行时的环境,这个环境指主要指的是变量地址如何分配。
可能说变量地址如何分配不太恰当,但是我实在是找不到一个名词来描述这个场景:从flash或者说是rom中把变量搬移到ram空间。

之所以说“搬移”,是基于以下几点考虑:

  • ram是易失性的存储介质,掉电数据就会丢失,而单片机的运行环境又是随时都都会掉电的(好比stm32下程序到sram中,下载调试快速,一掉电就凉凉),因此必须先把这些数据在flash或者rom中存一份,每次上电从rom中复制一份出来,我习惯把这种复制称为“搬移”;
  • 并且这种“搬移”不是无脑的“复制”,针对数据的不同特性(如赋初始值的、无需赋初始值的、赋初始值但初始值为0,等等),这个“复制方式”是不同的,即有的是复制、有的是解压缩、有的是赋0初始化,等等。所以我觉得叫“搬移”比较且当。

以下尽可能详细的介绍一下这种“搬移过程”。另外说明一下,我不太喜欢贴图片,所以我之后大部分会以代码的形式来展示示例。

a. 首先,先生成一个.S文件,即反汇编

以下都是以KEIL为例!

  • 使用KEIL自带的fromelf.exe工具(这个工具位于KEIL的安装目录下,例如:D:\MDK-ARM V5.25\Keil_v5\ARM\ARM\ARMCC\binD:\MDK-ARM V5.25\Keil_v5\ARM\ARM\ARMCLANG\bin,这两个为两个不同的编译链下的工具,对于Cortex-M来说,二者没有区别,详细大家可以百度:“armcc和armclang的区别。”)。
  • 然后在KEIL的IDE中编译链接生成一个.axf文件。
  • 使用命令:fromelf -c !L --output @L.S来生成.S文件(注意,在生成.axf文件时候把调试信息勾选上,这样生成的.S文件比较容易阅读,具体方法如下图所示)。关于为什么要勾选Debug Infomation大家可以去学习以下axf文件的格式内容,关于fromelf文件的用法、格式大家可以去百度一下。

如果你完全按照我的这个操作流程,你会得到一个与.axf文件同名的.S文件。

b.介绍一下上电执行过程

裸机下的上电过程,裸机我就不单独摆程序了,这里只是简单叙述一下流程:

  • 上电后会触发复位电路的复位机制(本质就是一个阻容,上电的伊始电容导通,触发了一段时间的低电平,随后电容充满电,维持高电平)
  • 复位后,ARM内核的PC指针跳转到0x0000_0000执行。
  • 根据ARM手册的规定,0x0000_0000顺序放栈指针、复位中断服务函数地址,等等。
  • 设置了栈顶,取得了复位中断服务函数的地址,然后去执行(这个在启动文件中可以找到,一般以startup_xxx.s命名)。
  • 进入复位中断服务函数,会执行__main()函数(这个不是我们见到的main()函数,这个是编译器内置函数,用于调用一些初始化操作函数,然后再调用我们的main函数,交回使用权)。
  • __main()函数中,一般会有两个函数:__scatterload()__rt_entry(),一个用于分散加载,一个用于系统库的初始化(如__rt_lib_init(),这个函数内实现了动态加载过程)。
  • 然后由__rt_entry()调用main()函数,跳转到我们的c语言世界。
  • 在KEIL中,还有一个骚操作:$Sub$$main$Super$$main,这哥俩用于在main()函数之前调用一些操作,毕竟之前的那些操作都是编译器来处理,我们干预不了,只能干瞪眼($Sub$$main$Super$$main,这个在rt-thread的源码中有使用示例,这里不赘述)。
  • 以上就完成了整个上电过程。

c.分散加载过程

!!!main
    __main
        0x000000c0:    f000f802    ....    BL       __scatterload ; 0xc8
        0x000000c4:    f000f883    ....    BL       __rt_entry ; 0x1ce
    !!!scatter
    __scatterload
    __scatterload_rt2
    __scatterload_rt2_thumb_only
        0x000000c8:    a00a        ..      ADR      r0,{pc}+0x2c ; 0xf4
        0x000000ca:    e8900c00    ....    LDM      r0,{r10,r11}
        0x000000ce:    4482        .D      ADD      r10,r10,r0
        0x000000d0:    4483        .D      ADD      r11,r11,r0
        0x000000d2:    f1aa0701    ....    SUB      r7,r10,#1
    __scatterload_null
        0x000000d6:    45da        .E      CMP      r10,r11
        0x000000d8:    d101        ..      BNE      0xde ; __scatterload_null + 8
        0x000000da:    f000f878    ..x.    BL       __rt_entry ; 0x1ce
        0x000000de:    f2af0e09    ....    ADR      lr,{pc}-7 ; 0xd7
        0x000000e2:    e8ba000f    ....    LDM      r10!,{r0-r3}
        0x000000e6:    f0130f01    ....    TST      r3,#1
        0x000000ea:    bf18        ..      IT       NE
        0x000000ec:    1afb        ..      SUBNE    r3,r7,r3
        0x000000ee:    f0430301    C...    ORR      r3,r3,#1
        0x000000f2:    4718        .G      BX       r3
    $d
        0x000000f4:    00037f6c    l...    DCD    229228
        0x000000f8:    00037f8c    ....    DCD    229260

;;中间的代码省略...

Region$$Table$$Base
        0x00038060:    00000055    U...    DCD    85
        0x00038064:    00000002    ....    DCD    2
        0x00038068:    00001b38    8...    DCD    6968
        0x0003806c:    00037f63    c...    DCD    229219
        0x00038070:    00000819    ....    DCD    2073
        0x00038074:    00001b3a    :...    DCD    6970
        0x00038078:    0000f5d0    ....    DCD    62928
        0x0003807c:    00037eeb    .~..    DCD    229099

上述代码直接为从__main()开始的过程,即跳转到__main()函数中,先执行__scatterload()函数,这里就是分散加载的过程(实际的过程可能比这个要繁琐些,繁琐在哪,后面会介绍到)。

以下是分析:

  • 执行到__main()函数处(实际上在汇编世界里,这就是一个标号,函数和变量没有太大区别,无非都是再地址空间里的一些有特殊含义的二进制数据)。
  • 跳转执行0x000000c0: f000f802 .... BL __scatterload ; 0xc8,大家只需要看到BL 0xc8就可以,其他的信息,都是些杂七杂八的信息(前面说了生成的.axf中要带有调试信息,所以这里会看到BL __scatterload ; 0xc8字样,__scatterload() 实际上就是从.axf文件中的符号表中提取出来的)。
  • 然后执行__scatterload()函数(下面的代码和上一节的是一摸一样的)。
!!!main
    __main
        0x000000c0:    f000f802    ....    BL       __scatterload ; 0xc8
        0x000000c4:    f000f883    ....    BL       __rt_entry ; 0x1ce
    !!!scatter
    __scatterload
    __scatterload_rt2
    __scatterload_rt2_thumb_only
        0x000000c8:    a00a        ..      ADR      r0,{pc}+0x2c ; 0xf4
        0x000000ca:    e8900c00    ....    LDM      r0,{r10,r11}
        0x000000ce:    4482        .D      ADD      r10,r10,r0
        0x000000d0:    4483        .D      ADD      r11,r11,r0
        0x000000d2:    f1aa0701    ....    SUB      r7,r10,#1
    __scatterload_null
        0x000000d6:    45da        .E      CMP      r10,r11
        0x000000d8:    d101        ..      BNE      0xde ; __scatterload_null + 8
        0x000000da:    f000f878    ..x.    BL       __rt_entry ; 0x1ce
        0x000000de:    f2af0e09    ....    ADR      lr,{pc}-7 ; 0xd7
        0x000000e2:    e8ba000f    ....    LDM      r10!,{r0-r3}
        0x000000e6:    f0130f01    ....    TST      r3,#1
        0x000000ea:    bf18        ..      IT       NE
        0x000000ec:    1afb        ..      SUBNE    r3,r7,r3
        0x000000ee:    f0430301    C...    ORR      r3,r3,#1
        0x000000f2:    4718        .G      BX       r3
    $d
        0x000000f4:    00037f6c    l...    DCD    229228
        0x000000f8:    00037f8c    ....    DCD    229260

;;中间的代码省略...

Region$$Table$$Base
        0x00038060:    00000055    U...    DCD    85
        0x00038064:    00000002    ....    DCD    2
        0x00038068:    00001b38    8...    DCD    6968
        0x0003806c:    00037f63    c...    DCD    229219
        0x00038070:    00000819    ....    DCD    2073
        0x00038074:    00001b3a    :...    DCD    6970
        0x00038078:    0000f5d0    ....    DCD    62928
        0x0003807c:    00037eeb    .~..    DCD    229099

很清楚的看到0x000000c80x000000d2这块代码,实际上是获取了Region$$Table$$Base这个数据块的起始地址(r10是始地址,r11是末地址+1,r7是始地址-1。只不过用了重定位技术,在链接时才知道Region$$Table$$Base的地址,然后回来修改$d处的值,所以这块代码使用了相对地址偏移,简单来说就是编译的时候$d这里填充的0,链接时才知道了真正的值)。
然后顺序执行到__scatterload_null()处,__scatterload_nul()l本质是一个循环,仿佛的利用Region$$Table$$Base中的数据来执行,为了方便理解,我写了一个相同功能的C代码:

篇幅有限,接下一篇

0 条评论

发布
问题

分享
好友