重定位

位置无关编码 (PIC,position independent code):汇编源文件被编码成二进制可执行程序时编码方式与位置(内存地址)无关。

位置有关编码:汇编源码编码成二进制可执行程序后和内存地址是有关的。

我们在设计一个程序时,会给这个程序指定一个运行地址(链接地址)。就是说我们在编译程序时其实心里是知道我们程序将来被运行时的地址(运行地址)的,而且必须给编译器链接器指定这个地址(链接地址)才行。

最后得到的二进制程序理论上是和你指定的运行地址有关的,将来这个程序被执行时必须放在当时编译链接时给定的那个地址(链接地址)下才行,否则不能运行(就叫位置有关代码)。但是有个别特别的指令他可以跟指定的地址(链接地址)没有关系,也就是说这些代码实际运行时不管放在哪里都能正常运行。

运行地址:由运行时决定的(编译链接时是无法绝对确定运行时地址的)。

链接地址:由程序员在编译链接的过程中,通过Makefile-Ttext xxx或者在链接脚本中指定的。程序员事先会预知自己的程序的执行要求,并且有一个期望的执行地址,并且会用这个地址来做链接地址。

举例:Linux 中的应用程序。gcc hello.c -o hello,这时使用默认的链接地址就是0x0,所以应用程序都是链接在0x0地址的。因为应用程序运行在操作系统的一个进程中,在这个进程中这个应用程序独享 4G 的虚拟地址空间。所以应用程序都可以链接到 0 地址,因为每个进程都是从 0 地址开始的。(编译时可以不给定链接地址而都使用0x0

编译链接过程

每个过程的作用

  • 预编译:预编译器执行。替换宏定义,删除注释等工作。
  • 编译:编译器来执行。把源码.c .S编程机器码.o文件。
  • 链接:链接器来执行。把.o文件中的各函数(段)按照一定规则(链接脚本来指定)累积在一起,形成可执行文件。
  • strip:strip 是把可执行程序中的符号信息给拿掉,以节省空间。(Debug 版本和 Release 版本)
  • objcopy:由可执行程序生成可烧录的镜像bin文件。

编译后生成的段

段就是程序的一部分,我们把整个程序的所有东西分成了一个一个的段,给每个段起个名字,然后在链接时就可以用这个名字来指示这些段。也就是说给段命名就是为了在链接脚本中用段名来让段放在合适的位置。

段名分为 2 种:一种是编译器链接器内部定好的,一种是程序员自己指定的、自定义的段名。 已有段名:

  • 代码段:(.text),又叫文本段,代码段其实就是函数编译后生成的东西
  • 数据段:(.data),数据段就是 C 语言中有显式初始化为非 0 的全局变量
  • bss 段:(.bss),又叫 ZI(zero initial)段,就是零初始化段,对应 C 语言中初始化为 0 的全局变量。
  • 自定义段名:段名由程序员自己定义,段的属性和特征也由程序员自己定义。

C 语言中全局变量如果未显式初始化,值是 0。本质就是 C 语言把这类全局变量放在了 bss 段,从而保证了为 0。 C 运行时环境如何保证显式初始化为非 0 的全局变量的值在 main 之前就被赋值了?就是因为它把这类变量放在了.data 段中,而.data 段会在 main 执行之前被处理(初始化)。

链接脚本

链接脚本做什么事?

链接脚本其实是个规则文件,他是程序员用来指挥链接器工作的。链接器会参考链接脚本,并且使用其中规定的规则来处理.o文件中那些段,将其链接成一个可执行程序。

链接脚本的关键内容有 2 部分:段名 + 地址(作为链接地址的内存地址)。把段,放到一个地址的意思。

链接脚本就像是一个从上到下顺序执行的一个代码

  • . 表示当前位置
  • = 表示赋值
  • * 表示通配符

链接脚本里的符号,可以在汇编源码里引用。

一个简易示例:

SECTIONS
{
    . = 0xd0024000; # 当前地址为0xd0024000
    
    .text : {
        start.o
        * (.text)   # 所有的text段
    }
            
    .data : {
        * (.data)
    }
    
    bss_start = .;  # bss_start的值为当前地址,是执行到这里的地址,不是最上面. = 0xd0024000的地址
    .bss : {
        * (.bss)
    }
    
    bss_end  = .;    
}

怎么做?

任务:在 SRAM 中将代码从 0xd0020010 重定位到 0xd0024000

第一点:通过链接脚本将代码链接到 0xd0024000 重定位代码的作用就是:在PIC执行完之前(在代码中第一句位置有关码执行之前)必须将整个代码搬移到0xd0024000位置去执行,这就是重定位。

第二点:dnw 下载时将 bin 文件下载到 0xd0020010 这样就能完成,下载代码与运行代码位置不同。

第三点:代码执行时通过代码前段的少量位置无关码将整个代码搬移到 0xd0024000。

第四点:使用一个长跳转跳转到 0xd0024000 处的代码继续执行,重定位完成。

长跳转:一种跳转指令,类似于分支指令 B,BL 等作用的指令,跳转指令通过给 PC(r15)赋一个新值来完成代码跳转。当我们执行完重定位后,实际上 SRAM 中有两份代码的镜像(一份是我们下载到 0xd0020010 处的,一份是重定位到 0xd0024000 处的),这两份代码内容完全相同。

短跳转:短跳转指令可以实现向前或向后 32MB 的地址空间跳转。

当链接地址和运行地址相同是,短跳转和长跳转实际效果一样。但是当链接地址和运行地址不同时,短跳转和长跳转就有差异了,这时候段跳转执行的是运行地址处的那一份,而长跳转执行的是链接地址的那一份。

重定位实际就是在运行地址处执行一段位置无关码 PIC,让这段 PIC(也就是重定位代码)从运行地址处把整个程序镜像拷贝一份到链接地址处,完了之后使用一句长跳转指令从运行地址处直接跳转到链接地址处去执行同一个函数(led_blink),这样就实现了重定位之后的无缝连接。

汇编代码:

/*
 * 文件名:    led.s    
 * 作者:    朱老师(朱友鹏)
 * 描述:    演示重定位(在SRAM内部重定位)
 */

#define WTCON        0xE2700000

#define SVC_STACK    0xd0037d80

.global _start                    // 把_start链接属性改为外部,这样其他文件就可以看见_start了
_start:
    // 第1步:关看门狗(向WTCON的bit5写入0即可)
    ldr r0, =WTCON
    ldr r1, =0x0
    str r1, [r0]
    
    // 第2步:设置SVC栈
    ldr sp, =SVC_STACK
    
    // 第3步:开/关icache
    mrc p15,0,r0,c1,c0,0;            // 读出cp15的c1到r0中
    //bic r0, r0, #(1<<12)            // bit12 置0  关icache
    orr r0, r0, #(1<<12)            // bit12 置1  开icache
    mcr p15,0,r0,c1,c0,0;
    
    // 第4步:重定位
    adr r0, _start          // adr加载时就叫短加载,此处adr指令用于加载_start当前运行地址,详解见正文    
    ldr r1, =_start         // ldr加载时如果目标寄存器是pc就叫长跳转,如果目标寄存器是r1等就叫长加载    
                            // 此处ldr指令用于加载_start的链接地址:0xd0024000

    // bss段的起始地址
    ldr r2, =bss_start    // 就是我们重定位代码的结束地址,重定位只需重定位代码段和数据段即可
                        // 该符号在链接脚本里定义
    cmp r0, r1            // 比较_start的运行时地址和链接地址是否相等
    beq clean_bss        // 如果相等说明不需要重定位,所以跳过copy_loop,直接到clean_bss
                        // 如果不相等说明需要重定位,那么会顺序执行下面的copy_loop进行重定位
                        // 重定位完成后继续执行clean_bss。

// 用汇编来实现的一个while循环
copy_loop:
    ldr r3, [r0], #4    // 源   r0内容写入r3,然后r0自增4
    str r3, [r1], #4    // 目的 r3内容写入r1,然后r1自增4
                        // 这两句代码就完成了4个字节内容的拷贝
    cmp r1, r2            // r1和r2都是用ldr加载的,都是链接地址,所以r1不断+4总能等于r2
    bne copy_loop

// 清bss段,其实就是在链接地址处把bss段全部清零
clean_bss:
    ldr r0, =bss_start                    
    ldr r1, =bss_end
    cmp r0, r1                // 如果r0等于r1,说明bss段为空,直接继续执行下面的代码
    beq run_on_dram            // 清除bss完之后的地址
    mov r2, #0

clear_loop:
    str r2, [r0], #4        // 先将r2中的值放入r0所指向的内存地址(r0中的值作为内存地址),
    cmp r0, r1                // 然后r0 = r0 + 4
    bne clear_loop

//    清理完bss段后重定位就结束了。然后当前的状况是:
//    1、当前运行地址还在0xd0020010开头的(重定位前的)那一份代码中运行着。
//    2、此时SRAM中已经有了2份代码,1份在d0020010开头,另一份在d0024000开头的位置。
//    然后就要长跳转了。

run_on_dram:    
    // 长跳转到led_blink开始第二阶段
    ldr pc, =led_blink                // ldr指令实现长跳转,把led_blink的值,写入pc寄存器
    
    // 从这里之后就可以开始调用C程序了
    //bl led_blink                    // bl指令实现短跳转
    
    // 汇编最后的这个死循环不能丢
    b .
    

adr与 ldr 伪指令的区别:ldradr都是伪指令

  • adr短加载,指令加载符号地址,加载的是运行时地址;
  • ldr长加载,指令在加载符号地址时,加载的是链接地址;

重定位就是汇编代码中的copy_loop函数,代码的作用是使用循环结构来逐句复制代码到链接地址。 复制的源地址是 SRAM 的0xd0020010,复制目标地址是 SRAM 的0xd0024000,复制长度是bss_start减去_start,所以复制的长度就是整个重定位需要重定位的长度,也就是整个程序中代码段 + 数据段的长度。bss段(bss 段中就是 0 初始化的全局变量)不需要重定位。

清除bss段是为了满足 C 语言的运行时要求(C 语言要求显式初始化为 0 的全局变量,或者未显式初始化的全局变量的值为 0,实际上 C 语言编译器就是通过清bss段来实现 C 语言的这个特性的)。一般情况下我们的程序是不需要负责清零bss段的(C 语言编译器和链接器会帮我们的程序自动添加一段头程序,这段程序会在我们的 main 函数之前运行,这段代码就负责清除bss)。但是在我们代码重定位了之后,因为编译器帮我们附加的代码只是帮我们清除了运行地址那一份代码中的bss,而未清除重定位地址处开头的那一份代码的bss,所以重定位之后需要自己去清除bss