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

发布于 2020-07-03 13:44:35

接上一篇

老习惯,列代码:

a.const data的处理

//1.c语言中的代码
const long TestData[4] = {1, 2, 3, 4};
int main( void )
{
    return (int)TestData[0];
}

//2.以下是对应的汇编代码
i.main
$Super$$main
    0x000062cc:    4801        .H      LDR      r0,[pc,#4] ; [0x62d4] = 0x5456
    0x000062ce:    4478        xD      ADD      r0,r0,pc
    0x000062d0:    6800        .h      LDR      r0,[r0,#0]
    0x000062d2:    4770        pG      BX       lr
$d
    0x000062d4:    00005456    VT..    DCD    21590
;;省略其他代码

//3.const数据在flash中的位置
.constdata
TestData
    0x0000b728:    00000001    ....    DCD    1
    0x0000b72c:    00000002    ....    DCD    2
    0x0000b730:    00000003    ....    DCD    3
    0x0000b734:    00000004    ....    DCD    4

通过分析会发现:0x000062cc~0x000062d0对应的访问TestData[0]的操作。
发现借助了pc$d这个表,通过计算发现0x000062ce行执行完r0的值为0x5456 + pc = 0x0000b728,对应的Testdata[0]

可以发现,这个偏移是相对与调用处的那条指令而言的,而不是$d表的首地址或者其他的什么。

b.data initializers的处理

.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

;;省略其他代码

i.StartThread_Entry
StartThread_Entry
    0x00023808:    b510        ..      PUSH     {r4,lr}
    0x0002380a:    4604        .F      MOV      r4,r0
    0x0002380c:    f7e5fcaa    ....    BL       CheckSysLimit ; 0x9164

通过分析会发现:0x00001d40~0x00001d42对应的获取StartThread_Entry()函数的起始地址的操作。
发现借助了pc$d这个表,通过计算发现0x00001d42行执行完r0的值为0x21ac3 + pc = 0x00023809,对应的StartThread_Entry()起始地址 + bit[0]=1

c.函数的调用

//1.调用库里面的函数示例
    0x000308d6:    a023        #.      ADR      r0,{pc}+0x8e ; 0x30964
    0x000308d8:    f7d1fae4    ....    BL       __2printf ; 0x1ea4

//2.调用用户自定义程序示例
    0x000242c8:    f004fa04    ....    BL       api_InitSysWatchVariable ; 0x286d4
    0x000242cc:    f004f9b0    ....    BL       api_InitLcdVariable ; 0x28630

//3.被调用函数的地址
i.api_InitLcdVariable
api_InitLcdVariable
    0x00028630:    2001        .       MOVS     r0,#1
;;省略其他代码

可以发现无论调用库函数还是自定义的函数,都不会使用pc来作为基地址(没有额外的±pc)。
但实际上,BL指令本身就是一个相对跳转,计算方法如下:

首先这个BL是一个Thumb指令,是2个字节的,虽然长的4个字节,很容易被误解,其实这个长跳转就是两个跳转指令组合成的格式如下:

bit[15]bit[14]bit[11]bit[12][bit11]bit[10:0]
11110/1地址偏移
0,偏移高位
1,偏移低位

所以:

  1. 每两个字节为一组,判断bit[11]:bit[11]=0的为高偏移/bit[11]=1的为低偏移;
  2. 去掉去掉高5位;
  3. 高偏移左移12位,低偏移左移1位,然后相加;
  4. 加上BL指令所在的pc,然后加4;
  5. 完毕;
则对于下述指令:
0x000242cc:    f004f9b0    ....    BL       api_InitLcdVariable ; 0x28630

1.先判断指令中的地址偏移字段
0xf004, bit[11]=0, 为地址高位偏移 
0xf9b0, bit[11]=1, 为地址低位偏移
2.各自取低11位
0xf004 -> 0x004
0xf9b0 -> 0x1b0
3.拼接
0x004<<12 +  0x1b0<<1 = 0x004000 + 0x360 = 0x004360
4.加上指令所在的偏移
0x004360 + (0x000242cc + 4) = 0x28630(为api_InitLcdVariable 函数的地址)

总结:函数调用本身就是使用的地址无关操作,而ro和动态加载的操作通过加入pc偏移来实现。

2.rwpi介绍

rwpi就比较好理解了:对于rw的数据使用,或者直接说是ram中的数据的使用。
因为运行时数据在ram中的位置也是不确定的,也是只有运行时才知道os把这块数据分配在ram空间的哪里。
处理方式很简单,但os分配ram时把这块空间的基地址获取出来,保存在一个地方,使用的时候加上就可以了。

同样,给一个官方说明:

RWPI = Read-Write Position Independence. This concerns everything that is readwrite in the ELF output from the linker.

everything that is readwrite无非就是包括以下几个方面:
1. rw数据
2. zi数据

首先rwpi的机制是:

  • 编译时把所有变量的访问改成r9+偏移的方式,这个偏移在编译的时候给定0,然后很自然得到每个变量的偏移地址了。
  • 运行时,os先分配一块空间,把基地址保存到r9中,然后再执行分散加载的那一套操作。

a.rw数据的处理

b.zi数据的处理

统一用一个程序来作为示例:

//1.c语言中的代码
long TestData1[4] = {1, 2, 3, 4};
long TestData2[4];

int main( void )
{
    long dump = TestData2[0] + TestData1[0];
    return (int)dump;
}

//2.以下是对应的汇编代码
i.main
$Super$$main
    0x000062cc:    4903        .I      LDR      r1,[pc,#12] ; [0x62dc] = 0x3640
    0x000062ce:    4449        ID      ADD      r1,r1,r9
    0x000062d0:    6809        .h      LDR      r1,[r1,#0]
    0x000062d2:    4a03        .J      LDR      r2,[pc,#12] ; [0x62e0] = 0x1a48
    0x000062d4:    444a        JD      ADD      r2,r2,r9
    0x000062d6:    6812        .h      LDR      r2,[r2,#0]
    0x000062d8:    1888        ..      ADDS     r0,r1,r2
    0x000062da:    4770        pG      BX       lr
$d
    0x000062dc:    00003640    @6..    DCD    13888
    0x000062e0:    00001a48    H...    DCD    6728

在对代码分析的时候,r9给定0,然后解析.map文件进行分析。
可以得出:

  • 0x000062cc~0x000062d0行对应的是访问ram空间地址r9+0x00003640的地址,对应的TestData2[0],这个地址很明显在.bss里;
  • 0x000062d2~0x000062d6行对应的是访问ram空间地址r9+0x00001a48的地址,对应的TestData1[0],这个地址在.data中;
//.map文件
TestData1        0x0000d600   Data          16  main.o(.data)

//加载视图和运行视图的对应关系
        0x0000bb67:    XXXXXXXX    ....    DCD    XXXXXX ;;bin文件中的base地址    
Region$$Table$$Base
        0x0000bb68:    00000051    Q...    DCD    81
        0x0000bb6c:    00000002    ....    DCD    2
        0x0000bb70:    00001a74    t...    DCD    6772
        0x0000bb74:    0000ba6b    k...    DCD    47723

//很容易得出.data在.bin中位置
** Section #3 'data' (SHT_PROGBITS) [SHF_ALLOC + SHF_WRITE]
    Size   : 1992 bytes (alignment 4)
    Address: 0x0000bbb8

//然后做转换运算
用.map文件中TestData1对应的0x0000d600 - 0x0000bbb8 = 0x1a48
这个值 = &TestData1[0]; //赋初始值的全局变量

3.为什么使用地址无关编译之后,无法使用const的函数指针数组

根据前面的示例,不难看出,无论是rwpi还是ropi,都只是对变量访问的一些处理,仅仅是在访问时通过给定一个基地址,然后通过加上偏移地址的方式来使用。 这个基地址分为两种:ram空间的用r9,flash空间的用pc。除此之外,没有做其他的处理。

然后我们来分析:

  • 在编译阶段,完成的是对每一个文件的翻译工作(根据情况分配到code、ro、rw、zi中,形成不同的段)。

    • 对于外部的符号,只要由声明的操作,则会在每一个函数的汇编代码后面生成一个重定位表(前面见到的$d表),将指令中的调用指向这个重定位表的每一项;同时还要生成一个重定位表的段,用于在链接阶段进行重定位时作为参考。
  • 链接的初期,将所有文件的相同的段形成一个整体的段(即所有的rw段整合成一个rw段...)。
  • 链接的后期,根据重定位段对编译阶段形成的那个$d进行修改。

而当使用了const的函数指针数组时,这个数组显然要放到flash中,正常说编译阶段是可以通过的(假设说只根据编译阶段完成的任务来判断,而不是实际编译器的操作),这时候不知道数组的初始值,编译器生成一个重定位表同时用个临时数据占位,这都是可行的。
然后是链接的初期,整合所有的段形成一个段,这一步也是没问题的,因为就是一个拼接而已。
最后是链接的后期,就是重定位阶段,理论上也是没有问题,访问普通的变量是没有问题的,但对于访问其中的函数指针来说这一步有些小问题:

  • 在地址无关编译操作下,根据ropi的处理流程,所有的对const变量使用是pc+偏移的操作来实现访问的。
  • 所以使用这个const变量的那块代码肯定是可以正确访问到的,但是访问到的函数指针这个值就不是正确的了,因为重定位进行的时候这个函数指针的值不是实际运行的时候的那个,而flash又不能想ram那样动态加载,所以无法通过pc来作为基地址来相对访问。
  • 但是ram中(非const修饰的全局变量)可以使用函数指针,通过动态加载把函数的绝对地址赋给了函数指针(这个可以在本篇博客中搜一下__sta__dyninit(),一目了然)。

所以造成这个问题的原因就是flash的特殊性:不可改(起码是不是像ram那样的可修改,如果你偏要说flash可擦写,我也没办法。我的意思是,flash终究是flash,ram终究是ram,二者虽然一些性质相似,但终究是两种东西,否则还区分个什么劲)。

总之,在地址无关编译时,const变量中不能出现在只有在运行时才能确定数据!如将函数指针和变量的地址赋给const变量!这个变量的地址包括ro、rw和zi。

4."治标不治本"的解决方法

所以编译器根本就不会允许const函数指针数组这种语法出现,直接报错了,所以无能为力。所有的调用处没有特殊处理,即这个变量里面的是绝对地址,调用处直接使用,根本不会再加上pc(除非我们在使用处人为的去获取pc,但这个的前提还得是能通过编译器的编译,然后链接器链接上一个相对于某个已知位置的相对偏移)。
所以,目前只能仿照ram的动态加载机制,改写const函数指针数组,通过一个辅助函数来获取函数指针(利用了函数调用本身就是使用相对地址的这一特性)。

void test1(char a) {
    printf("this is dunc:test1 %c\n", a);
}
void test2(char a) {
    printf("this is dunc:test2 %c\n", a);
}
void test3(char a) {
    printf("this is dunc:test3 %c\n", a);
}

typedef struct
{
    char     c;
    int     f;
}_T_Test;

const _T_Test test[3] = 
{
    {    'a',    1, },
    {    'b',     2, },
    {    'c',     3, },
};

typedef void (*Func)(char);
Func Select(int idx)
{
    switch(idx)
    {
        case 1:
            return test1;
        case 2:
            return test2;
        case 3:
            return test3;
        default:
            return 0;
    }
}

int main(void)
{
    Select(test[0].f)(test[0].c);
    Select(test[1].f)(test[1].c);
    Select(test[2].f)(test[2].c);
    
    return 0;
}

如果大家有什么更好的方法,欢迎一起交流。

四、总结

  1. 分散加载机制,本质就是从flash或rom等非易失存储介质中把数据“搬移”到ram中,不同的是对于不同类型的数据会进行不同的处理,甚至有的还需要动态加载。
  2. 动态加载时,离不开os(注意单片机上的os毕竟不是windows上的os,能力有限,但原理相通)。
  3. 使用地址无关编译,就是使用ropirwpi两种技术,其本质是对变量进行地址无关的处理(因为函数天生就是地址无关的),从而引进了对ro的处理,对rw和zi的处理,甚至是对函数指针数组的处理,原理就是利用相对寻址,利用代码和数据本身的相对地址不变这一特性来实现的。
0 条评论

发布
问题

分享
好友