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

发布于 2020-07-03 12:07:33

接上一

//.S文件找到相应位置(可以根据.map来辅助查找)

//1.main.c中main函数对应的代码(看到了前面说过的$Super$$main操作,可见编译器的看到的main和我们看到的main不一样)
//编译器看到的main=$Sub$$main+$Super$$main+main,它把这三个组合在一起,KEIL把这三个接口都开放给我们
//其中,$Super$$main没有什么实际作用,作用就是从跳回main,而$Sub$$main和main是实打实的函数
//关于这一点,可以留意:凡是i.$Sub$$main或i.main这种带i.前缀的标号,都是从.c/.s中编译出来的函数
i.main
$Super$$main
    0x00030868:    b51c        ..      PUSH     {r2-r4,lr}
    0x0003086a:    482c        ,H      LDR      r0,[pc,#176] ; [0x3091c] = 0x1b0c
    0x0003086c:    4448        HD      ADD      r0,r0,r9
    0x0003086e:    6801        .h      LDR      r1,[r0,#0]
    0x00030870:    2000        .       MOVS     r0,#0
    0x00030872:    4788        .G      BLX      r1
    0x00030874:    4829        )H      LDR      r0,[pc,#164] ; [0x3091c] = 0x1b0c
    0x00030876:    4448        HD      ADD      r0,r0,r9
    0x00030878:    6841        Ah      LDR      r1,[r0,#4]
    0x0003087a:    2000        .       MOVS     r0,#0
    0x0003087c:    4788        .G      BLX      r1
;;省略其他代码
$d
    0x0003091c:    00001b0c    ....    DCD    6924
;;省略其他代码
    
//2.以下是main.c中形成的动态初始化代码(用于在运行时进行rwpi的最后操作:实体化)
.text
__sta__dyninit
    0x00001d40:    4803        .H      LDR      r0,[pc,#12] ; [0x1d50] = 0x21ac3
    0x00001d42:    4478        xD      ADD      r0,r0,pc
    0x00001d44:    4903        .I      LDR      r1,[pc,#12] ; [0x1d54] = 0x1b0c
    0x00001d46:    4449        ID      ADD      r1,r1,r9
    0x00001d48:    6008        .`      STR      r0,[r1,#0]
    0x00001d4a:    6048        H`      STR      r0,[r1,#4]
    0x00001d4c:    4770        pG      BX       lr
$d
    0x00001d4e:    0000        ..      DCW    0
    0x00001d50:    00021ac3    ....    DCD    137923
    0x00001d54:    00001b0c    ....    DCD    6924
;;省略其他代码

//3.如何调用的__sta__dyninit
//__main -> __rt_entry -> __rt_lib_init -> __cpp_initialize__aeabi_
//这部操作在__scatterload之后执行的
.text
__cpp_initialize__aeabi_
    0x00002598:    b570        p.      PUSH     {r4-r6,lr}
    0x0000259a:    4c06        .L      LDR      r4,[pc,#24] ; [0x25b4] = 0x35ae0
    0x0000259c:    447c        |D      ADD      r4,r4,pc
    0x0000259e:    4d06        .M      LDR      r5,[pc,#24] ; [0x25b8] = 0x35b10
    0x000025a0:    447d        }D      ADD      r5,r5,pc
    0x000025a2:    e003        ..      B        0x25ac ; __cpp_initialize__aeabi_ + 20
    0x000025a4:    6820         h      LDR      r0,[r4,#0]
    0x000025a6:    4420         D      ADD      r0,r0,r4
    0x000025a8:    4780        .G      BLX      r0
    0x000025aa:    1d24        $.      ADDS     r4,r4,#4
    0x000025ac:    42ac        .B      CMP      r4,r5
    0x000025ae:    d1f9        ..      BNE      0x25a4 ; __cpp_initialize__aeabi_ + 12
    0x000025b0:    bd70        p.      POP      {r4-r6,pc}
$d
    0x000025b2:    0000        ..      DCW    0
    0x000025b4:    00035ae0    .Z..    DCD    219872
    0x000025b8:    00035b10    .[..    DCD    219920
;;省略其他代码
.init_array
Region$$Table$$Limit
SHT$$INIT_ARRAY$$Base
    0x00038080:    fffc8161    a...    DCD    4294738273
.init_array
    0x00038084:    fffc81a1    ....    DCD    4294738337
.init_array
    0x00038088:    fffc838d    ....    DCD    4294738829
.init_array
    0x0003808c:    fffc83bd    ....    DCD    4294738877
.init_array
    0x00038090:    fffc8425    %...    DCD    4294738981
.init_array
    0x00038094:    fffc8d3d    =...    DCD    4294741309
.init_array
    0x00038098:    fffc9109    ....    DCD    4294742281
.init_array
    0x0003809c:    fffc9481    ....    DCD    4294743169
.init_array
    0x000380a0:    fffc95fd    ....    DCD    4294743549
.init_array
    0x000380a4:    fffc96fd    ....    DCD    4294743805
.init_array
    0x000380a8:    fffc9881    ....    DCD    4294744193
.init_array
    0x000380ac:    fffc9c29    )...    DCD    4294745129
.init_array
    0x000380b0:    fffc9c91    ....    DCD    4294745233    ;;注意,这个就是我的main函数中的需要动态加载的数据
.init_array
SHT$$INIT_ARRAY$$Limit

//4.开始分析
a. 在编译链接期间,使用地址无关编译技术,所以导致了我们的函数实际运行的地址是不确定的,所以是无法简单的通过之前见到的分散
加载来实现数据的“搬移”来实现ram中变量初始化。只能等.bin文件加载到flash中,开始执行了才能确定。
b. 所以链接器链接了一个__cpp_initialize__aeabi_()函数(位于KEIL提供的库"../clib/arm_runtime.c"中。额外啰嗦一句,
负责分散加载的__scatter()位于库"../clib/angel/scatter.s"中,这是一个汇编文件,所以这一导致了__scatter()可以不用使
用栈,可以早早就执行,而__cpp_initialize__aeabi_()必须等到栈初始化好了才能进行,所以必须在__scatter()之后)。
c. 然后我们来看一下__cpp_initialize__aeabi_()如何执行到每一个文件中的__sta__dyninit()。
还是老套路,不嫌麻烦,再分析一次,不过这次我们直接写对应的c代码(因为这个函数本来就是c编译出来的,而且看它的汇编代码也很明
显看出痕迹:进入函数后保护函数内要用的寄存器和lr,然后全程没用r0~r3做中间变量,然后最后返回时,直接从栈中弹出lr到pc中)
int32_t FuncTable[] @"SHT$$INIT_ARRAY$$Base" = 
{
    0xfffc8161,
    0xfffc81a1,
    0xfffc838d,
    0xfffc83bd,
    0xfffc8425,
    0xfffc8d3d,
    0xfffc9109,
    0xfffc9481,
    0xfffc95fd,
    0xfffc96fd,
    0xfffc9881,
    0xfffc9c29,
    0xfffc9c91, //main.c中的__sta__dyninit()的相对与"此处"的偏移
};
void __cpp_initialize__aeabi_(void)
{
    int32_t start;
    int32_t end;
    void (*func)(void);
    
    //由于Cortex-M的流水线机制,这里的pc实际为当前执行指令+4
    start = 0x00035ae0 + 0x000025a0; //=0x00038080  //r4
                                     //=&FuncTable
    end   = 0x00035b10 + 0x000025a4; //=0x000380b4  //r5
                                     //=&FuncTable + sizeof(FuncTable)
    while(1)
    {
        if(start == end)
        {
            break;
        }
        else
        {
            func  = *start;
            func += start; //FuncTable中存放的实际是偏移,相对与FuncTable中相应元素的偏移
            func();
            
            start += 4;
        }
    }
}
不妨来计算以下:
.init_array
    0x000380b0:    fffc9c91    ....    DCD    4294745233    ;;注意,这个就是我的main函数中的需要动态加载的数据
都是补码,直接相加即可:0xfffc9c91 + 0x000380b0 = 0x00001d41(发现就是__sta__dyninit()的首地址,只不过bit[0]=1)
关于bit[0]=0/1的问题,熟悉ARM的指令集的很容易明白。我这里随便找了一篇博文,不明白的可以看看:
https://blog.csdn.net/xinianbuxiu/article/details/52718178
d. 以上就完成了ram的动态加载,总结来说,就是函数的实际地址只有在运行期间才会确定,因此只能在运行后对这些变量来赋值。
e. 然后我们在main函数里使用这个变量,就很简单:加上r9这个基地址去使用就可以了。
f. 关于动态加载,在地址无关编译这节会详细说明,这里只是介绍一个简单流程,知道所有的数据可以被有理有据的被分配空间和初始化
就可以了,然后知道在main函数执行前完成了那些工作就可以了。

2.分散加载机制总结

原本分散加载指的仅仅是__scatter(),而我习惯把动态加载也算在分散加载范畴,因为它们都是对ram进行初始化操作嘛。无论怎么划分都无所谓,大家知道这个过程就可以。

C语言中的变量类型编译后的位置分散加载器的操作备注
局部变量××局部变量是分配在栈中的
.bss__scatterload_zeroinit()
.bss__scatterload_zeroinit()
全局变量.data不一定根据不同要求操作不同
未初始化的全局变量.bss__scatterload_zeroinit()
初始化但初始值全0.bss__scatterload_zeroinit()
初始化但初始值非全0.data__decompress()只能说KEIL是这样,其他的我没有分析,不做定论
初始化但初始值各种各样.data__scatterload_copy()如果使用地址无关编译,这个另说
const修饰的全局变量.ro×直接分配在flash或者rom中,直接使用
static修饰的变量.data/.bss参考上面对.data/.bss的相关操作在C语言中,static本质只是对编译器检查服务的,本质和全局变量没什么区别

特别说明:

  • KEIL通过分散加载文件进行配置,一般一次分配rw、zi、heap、stack(其中zi+heap+stack=.bss),之所以stack在heap后,是因为arm中栈一般是向下生长的,而heap又不是每次都能用完且又是相上生长,所以heap和stack可以做到最大程度的共用二者的空间。
  • 以上都是分析KEIL的情况,IAR和GCC我就没有特别分析,本质都是一样的,这些很底层的东西,很难有天翻地覆的变化,大家如果有兴趣可以自己分析。
  • 以上只是分析了没有使用地址无关编译时的分散加载过程(虽然我的示例代码的确使用了地址无关编译,本质没有区别的;同时这样也好,大家可以自己动手实践一遍,增强理解)。

三、地址无关编译

关于地址无关编译,我之前写了两篇随笔,简单介绍了一下:

[[随笔]ELF文件、编译/链接、静态链接、动态链接](https://blog.csdn.net/weixin_39869569/article/details/104041494).
[[随笔]C语言动态加载、rwpi和ropi、lwp轻量型进程初步解读](https://blog.csdn.net/weixin_39869569/article/details/104041494).

本篇文章,主要用实例介绍一下ropirwpi,然后解释一下为什么使用地址无关编译之后,无法使用const的函数指针数组问题,以及如果非要这么用的一个曲线救国的方法。

1.ropi介绍

主要是针对const变量由分散加载器初始化的数据(放在flash中的内容,一般不包括函数)。
官方解释:

ROPI = Read-Only Position Independence. This concerns everything that is readonly in the ELF output from the linker. Note that this includes const data and data initializers, i.e. typically everything that is put in FLASH.

主要说明了以下3点:
1. const data也就是const修饰的变量;
2. data initializers也就是数据初始化器(我理解的是为动态加载器__sta__dyninit());
3. 函数;
以上3点即包括了在flash中的所有数据(everything that is put in FLASH)。

所以裸机下没有任何问题,因为裸机下我们一般要指定我们的程序运行在flash的哪个位置(简单的像是.hex文件,每条记录都是带有地址的,这就是为什么.hex可以转化为.bin文件,反过来大概率是不行的,原因就是.bin文件纯指令代码,没有地址信息。类似于水合成石油,这辈子是不可能的,因为水里面没有碳元素。核聚变吧,聚一聚就出来碳元素了)。
而加上操作系统加持之后就不一样了,程序在那运行,程序员说的不算,操作系统说的算。而且一般都会把程序放到一些类似磁盘啊、外部flash等存储介质中,只有使用到时才拿出来,要么放到内存中运行,要么放到内部flash中运行。(当然你如果非要说linux下可以指定一些.so库的运行位置,那我无话可说,那本质也是os指定每次都把这个.so库放到同一个位置)。因此必须引入地址无关编译,把地址分配的权利交给os来分配。
实现ropi的前提是:程序块中的函数、变量位置,彼此之间是相对的,否则一切免谈。 也不是免谈,是那种情况一时半会我也解释不清,是我太菜了(有兴趣可以搜索.got和.plt)。

我们只分析基于这种前提下的ropi:

篇幅有限,接下一篇

0 条评论

发布
问题

分享
好友