Home » 朱老师嵌入式 » 重定位引入和链接脚本-笔记

重定位引入和链接脚本-笔记

编 辑:Y ┊ 时 间:2022年04月08日 ┊ 访问: 20 次

链接脚本

我们之前的裸机程序中,Makefile中用 -Ttext 0x0 来指定链接地址是0x0。这意味着我们认为这个程序将来会放在0x0这个内存地址去运行。
但是实际上我们运行时的地址是0xd0020010(我们用dnw下载时指定的下载地址)。这两个地址看似不同,但是实际相同。这是因为S5PV210内部做了映射,把SRAM映射到了0x0地址去。

分清楚这两个概念:
链接地址:链接时指定的地址(指定方式为:Makefile中用-Ttext,或者链接脚本)
运行地址:程序实际运行时地址(指定方式:由实际运行时被加载到内存的哪个位置说了算)

链接地址由什么决定

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

2、210中的裸机程序。运行地址由我们下载时确定,下载时下载到0xd0020010,所以就从这里开始运行。(这个下载地址也不是我们随意定的,是iROM中的BL0加载BL1时事先指定好的地址,这是由CPU的设计决定的)。所以理论上我们编译链接时应该将地址指定到0xd0020010,但是实际上我们在之前裸机程序中都是使用位置无关码PIC,所以链接地址可以是0。

程序段的概念

程序段的概念:代码段、数据段、bss段(ZI段)、自定义段
段就是程序的一部分,我们把整个程序的所有东西分成了一个一个的段,给每个段起个名字,然后在链接时就可以用这个名字来指示这些段。也就是说给段命名就是为了在链接脚本中用段名来让段站在核实的位置。

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

先天性段名:

代码段:(.text),又叫文本段,代码段其实就是函数编译后生成的东西
数据段:(.data),数据段就是C语言中有显式初始化为非0的全局变量
bss段:(.bss),又叫ZI(zero initial)段,就是零初始化段,对应C语言中初始化为0的全局变量。

后天性段名

段名由程序员自己定义,段的属性和特征也由程序员自己定义。

链接脚本的作用

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

链接脚本的关键内容有2部分:段名 + 地址(作为链接地址的内存地址)
链接脚本的理解:
SECTIONS {} 这个是整个链接脚本
. 点号在链接脚本中代表当前位置。
= 等号代表赋值

link.lds文件

SECTIONS
{
    . = 0xd0024000;
    
    .text : {
        start.o
        * (.text)
    }
            
    .data : {
        * (.data)
    }
    
    bss_start = .; 
    .bss : {
        * (.bss)
    }
    
    bss_end  = .;    
}

各个段含义

text 代码段
data 数据段
bss 段

bss_start = .; 这个在代码中是在给bss_start赋值,在汇编是可以引用的
“.表示顺序任意排列”

什么是重定位

  1. 通过链接脚本将代码链接到0xd0024000
  2. dnw下载时将bin文件下载到0xd0020010

第一点加上第二点,就保证了:代码实际下载运行在0xd0020010,但是却被链接在0xd0024000。从而为重定位奠定了基础。
当我们把代码链接地址设置为0xd0024000时,实际隐含意思就是我这个代码将来必须放在0xd0024000位置才能正确执行。如果实际运行地址不是这个地址就要出事(除非代码是PIC位置无关码),当以上都明白了后,就知道重定位代码的作用就是:在PIC执行完之前(在代码中第一句位置有关码执行之前)必须将整个代码搬移到0xd0024000位置去执行,这就是重定位。

  1. 代码执行时通过代码前段的少量位置无关码将整个代码搬移到0xd0024000
  2. 使用一个长跳转跳转到0xd0024000处的代码继续执行,重定位完成

Makefile

通过-T引入文件

arm-linux-ld -Tlink.lds -o $(OUT_DIR)$(OUT_NAME).elf $^

之前是:

arm-linux-ld -Ttext 0x0 -o $(OUT_DIR)$(OUT_NAME).elf $^

跳转指令

长跳转

长跳转:首先这句代码是一句跳转指令(ARM中的跳转指令就是类似于分支指令B、BL等作用的指令),跳转指令通过给PC(r15)赋一个新值来完成代码段的跳转执行。长跳转指的是跳转到的地址和当前地址差异比较大,跳转的范围比较宽广

长短链接的重点

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

adr与ldr伪指令的区别

ldr和adr都是伪指令,区别是ldr是长加载、adr是短加载。

为什么需要跳转

当我们执行完代码重定位后,实际上在SRAM中有2份代码的镜像(一份是我们下载到0xd0020010处开头的,另一份是重定位代码复制到0xd0024000处开头的),这两份内容完全相同,仅仅地址不同。重定位之后使用ldr pc, =led_blink这句长跳转直接从0xd0020010处代码跳转到0xd0024000开头的那一份代码的led_blink函数处去执行。(实际上此时在SRAM中有2个led_blink函数镜像,两个都能执行,如果短跳转bl led_blink则执行的就是0xd0020010开头的这一份,如果长跳转ldr pc, =led_blink则执行的是0xd0024000开头处的这一份)。

领悟

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

重定位总结:

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

汇编入口

重点:adr指令加载符号地址,加载的是运行时地址;ldr指令在加载符号地址时,加载的是链接地址。

重定位

使用adr r0, _start 把_start加载到r0
使用ldr r1, =_start把_start加载到r1,ldr指令用于加载_start的链接地址:0xd0024000

我们使用cmp r0, r1 比较_start的运行时地址和链接地址是否相等
通过beq指令判断,beq 如果相等说明不需要重定位,所以跳过复制的函数,直接到清理函数
如果不相等说明需要重定位,那么直接执行下面的复制的函数进行重定位
重定位完成后继续执行清理函数。

对应相关的代码

    //重定位
    // adr指令用于加载_start当前运行地址
    adr r0, _start          // adr加载时就叫短加载        
    // ldr指令用于加载_start的链接地址:0xd0024000
    ldr r1, =_start         // ldr加载时如果目标寄存器是pc就叫长跳转,如果目标寄存器是r1等就叫长加载    
    // bss段的起始地址
    ldr r2, =bss_start        // 就是我们重定位代码的结束地址,重定位只需重定位代码段和数据段即可
    cmp r0, r1                // 比较_start的运行时地址和链接地址是否相等
    beq clean_bss            // 如果相等说明不需要重定位,所以跳过copy_loop,直接到clean_bss
    // 如果不相等说明需要重定位,那么直接执行下面的copy_loop进行重定位
    // 重定位完成后继续执行clean_bss。

复制一份内容到新的链接地址

说明

r0 加载的是短链接的地址 (符号地址)
r1 加载的是长连接地址 (链接地址)

代码:

copy_loop:
    ldr r3, [r0], #4    // 源
    str r3, [r1], #4    // 目的   这两句代码就完成了4个字节内容的拷贝
    cmp r1, r2            // r1和r2都是用ldr加载的,都是链接地址,所以r1不断+4总能等于r2
    bne copy_loop

分析

只要知道adr和ldr分别用于加载运行地址和链接地址,从而可以判断是否需要重定位即可;

如何知道代码长度?

ldr r3, [r0], #4    // 源
str r3, [r1], #4    // 目的   

这两句代码就完成了4个字节内容的拷贝

r2 是bss段的起始地址,就是我们重定位代码的结束地址,重定位只需重定位代码段
ldr r2, =bss_start // 就是我们重定位代码的结束地址
r1和r2都是用ldr加载的,都是链接地址,所以r1不断+4总能等于r2
cmp r1, r2

使用ldr 每次+4个地址来获取源内容,再使用str每次+4个地址来拷贝到链接地址
如果两个相等了,则跳出循环

bss_start编译器会自动推算
bss_start就是 代码段和数据段长度+起始地址

简述重定位

重定位就是汇编代码中的copy_loop函数,代码的作用是使用循环结构来逐句复制代码到链接地址。

复制的源地址是SRAM的0xd0020010,复制目标地址是SRAM的0xd0024000,复制长度是bss_start减去_start
所以复制的长度就是整个重定位需要重定位的长度,也就是整个程序中代码段+数据段的长度。
bss段(bss段中就是0初始化的全局变量)不需要重定位。

清bss段

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

说人话就是

清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开头的位置。
    然后就要长跳转了。

长跳转

ldr指令实现长跳转

ldr pc, =led_blink

同样的bl指令实现短跳转
bl led_blink

代码:

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

现在就讲完了相关的代码

整理一下

  1. 先设置一下链接脚本
  2. Makefile中一定要设置一下链接脚本
  3. 在汇编的文件设置一下重定位的代码
    这里就是

    • ldr和adr分别加载一下当前运行的地址
    • 获取bss段的起始地址
    • cmp 比较一下长跳转和短跳转获取的地址是否相同
    • 使用汇编编写一个复制循环
    • 把短链接地址的内容复制到重定位的地址
    • 清理bss段
    • 获取bss开始段和结束段
    • 清理
    • 使用长跳转到我们设定的入口指令

相关代码:

// 定义看门狗设置寄存器
#define WTCON 0xE2700000
#define SVC_STACK 0xd0037D80

.global _start
_start:
    // 关闭看门狗
    ldr r0, =WTCON
    ldr r1, =0x0
    str r1, [r0]

    ldr sp, =SVC_STACK
    // 从这里之后就可以开始调用C程序了

    // 开/关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;
    
    // 重定位
    adr r0, _start   // adr加载时就叫短加载
    ldr r1, =_start  // ldr加载时如果目标寄存器是pc就叫长跳转
    //如果目标寄存器是r1等就叫长加载
    ldr r2, =bss_start

    //bss段的起始地址
    ldr r2, =bss_start
    //就是我们重定位代码的结束地址
    //重定位只需重定位代码段和数据段即可
    cmp r0, r1            
    // 比较_start的运行时地址和链接地址是否相等
    beq clean_bss

// 
copy_loop:
    ldr r3, [r0], #4    //源
    str r3, [r1], #4    //目标
    cmp r1, r2
    bne copy_loop

    //清除bss段
    //就是在链接地址处把bss段全部清零
clean_bss:
    ldr r0, =bss_start
    ldr r1, =bss_end
    cmp r0, r1
    beq run_on_dram // 清除bss完之后的地址
    mov r2, #0
clean_loop:
    str r2, [r0], #4
    cmp r0, r1
    bne clear_loop

run_on_dram:
    ldr pc, =start_main
    // 从这里之后就可以开始调用C程序了
    // bl start_main //C语言实现的一个函数
    
    // 汇编最后的这个死循环不能丢
    b .



Copyright © 2026 Y 版权所有.网站运行:13年238天21小时23分