面试真题 | uboot 与 代码重定位

1、为什么需要重定位?

大部分的程序是不需要重定位的,但是有时候需要。

最常见的例子就是我们的UBOOT,因为我们的UBOOT有200多KB,但是我们开始BL0的地方只有96KB。

所以我们需要在96KB之前进行重定位,使开发板能够进行重定位。如果代码不是位置无关码,代码必须放在链接地址开始的地方,程序才可以正常运行,否则的话当PC去访问、执行某个变量名、函数名对应地址上的代码时就会找不到,接着程序无疑就是跑飞。

2、什么是重定位?

  • 重定位:把代码搬移到你想要的地址,本来程序是运行在运行地址处的,你可以通过重定位搬移到链接地址处。

  • 链接地址: 编译器对代码中的变量名、函数名等东西进行一个地址的编排,赋予这些抽象的东西一个地址,然后在程序中访问这些变量名、函数名就是在访问一些地址,这些地址我们称之为编译地址。

  • 运行地址:是指程序指令真正运行的地址,是由用户指定的,用户将运行地址烧录到哪里,也就是PC当前执行指令所在的实际地址,就是运行的地址。也就是真实在程序中运行的地址。

3、重定位的基础知识

(1)大部分指令是位置有关编码。

  • 位置无关码(PIC, position independent code):

汇编源文件被编码成二进制可执行程序后与位置无关。有些特别的指令,可以跟地址没有关系。也就是说这些代码实际运行时,不管放在哪里都能正常运行。

  • 位置有关码:

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

PS: 我们在设计一个程序时,会给这个程序指定一个运行地址。就是说我们在写程序时,其实我们是知道我们程序将来被运行的地址的。 必须给编译器和链接器指定这个地址才行,最后得到二进制程序。 理论上和你指定的运行地址是有关的,这就叫做位置有关代码。

(2) 对于位置有关码来说:最终执行时的运行地址和编译链接时给定的链接地址必须相同,否则一定会出错。

如果编译时 使用-Ttext 0x0来指定链接地址是0x0,这意味着我们认为这个程序将来会放在这个内存地址中运行。但是实际上我们运行的地址是下载在开发板的地址0xd0020010。因为是位置无关码,所以运行程序来是没有什么问题的。而且我们开发板对这些程序进行了映射,所以说这是一个偶然的情况。

(3)我们再来分析一下S5PV210的启动过程。

官方建议的启动过程(假定你的Bootloader为80KB)

  • 开机启动,执行BL0,BL0会加载外部启动设备中的bootloader的前16KB到SRAM,

(BL0是厂家事先固化好的程序)就是BootRom

  • 校验BL1,运行BL1

  • BL1在运行时,初始化外部DDR,加载剩余的64kb代码到 BL2中 ( 64 = 80 - 16)

  • 运行BL2,初始化DDR,并且将OS搬运到DDR

  • 执行OS,启动完成。

UBOOT实际上的启动的方法:

(由于 BL2 的空间也太小了,使用起来非常有局限,所以uboot的设计者干脆在BL1以后,连同BL2与OS有关的直接放到DDR上运行了)

  • 先开机上电,BL0运行,BL0会加载外部启动设备中的UBOOT的前16KB(BL1)到SRAM中去运行,

  • BL1运行会初始化DDR。然后将整个UBOOT,搬运到我们的DDR中。

  • 从SRAM中直接长跳转到DDR中继续执行我们的UBOOT。直到UBOOT完全启动。

  • 长跳转的意思就是从SRAM中跳转到DDR中。UBOOT启动后在命令行中去执行OS。

4、从源码到可执行程序的步骤:预编译、编译,链接、[strip]

  • 预编译: 比如C中的宏定义就是由预编译器处理的,注释等也是由预编译器处理的。

  • 编译: 编译器来执行,把源码中的.c/.s文件转换为.o文件。

  • 链接: 链接器来执行,把.o文件中的各种函数(段)按照一定的规则(即使不用用链接脚本指定,链接器也有默认的固定的顺序)链接到一起,形成可执行程序。

链接的本质是规则文件,它指明了一种行动的规则,它是我们程序员用来指挥链接器工作的一种语言。

链接器会参考链接脚本来处理我们.o文件哪些段,将其链接成一个可执行程序。

  • strip: 把可执行程序中的符号信息给拿掉,以节省空间,一般可以节省3分之一的空间。这样就从 elf 文件 转换 为 bin 文件

5、链接脚本存在的意义

链接脚本用来指定编译时的一些选项,使得程序能够按照开发者的意志进行指定的排布,也为了在某些特定的场合满足需求。

1.代码段 (.text)

代码段(code segment/text segment)通常是指用来存放 程序执行代码 的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于 只读 , 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些 只读的常数变量 ,例如字符串常量等。程序段为程序代码在内存中的映射。一个程序可以在内存中多有个副本。

2.数据段(.data)

数据段就是C语言中有显示的初始化为非0的全局变量。数据段(data segment)通常是指用来存放程序中 已初始化 的 全局变量 的一块内存区域。数据段属于静态内存分配。

3.BSS段(.bss) ,又叫做ZI段,零初始化段,

通常是指用来存放程序中未初始化或初始化为0的全局变量和静态变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。特点是可读写的,在程序执行之前BSS段会自动清0。bss段的存放是指为其预留空间(占位符)BSS段在可执行文件中时候不占磁盘空间,要运行的时候才分配空间并清0.

4.自定义段

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

在移植 uboot 时,接触到一个概念叫做 位置无关码,那么与它对应的就是位置有关码。提到这两个概念就还得提一提链接地址、加载地址。

链接地址,链接脚本里指定的,理论上程序运行时所处的地址。在编译时,编译器会根据链接地址来翻译位置有关码。

加载地址,程序运行时,实际所处的地址。

位置无关码,位置有关码,是相对于一条指令的正常目的来说的。比如 ldr r0 ,=标号,它的正常目的是取得标号处的地址,对于这个目的,它是位置有关码,运行的地址不对就获取不到正确的标号地址,其实它无论在哪都是获取的程序加载地址等于链接地址时,标号的地址,如果你就是想要这个值,那么用这条指令是非常正确的,就不用理会什么位置无关码,位置有关码的概念了,这一点非常重要。

因此,当加载地址不等于链接地址时,并不是不可以用位置无关码,而是要看你用位置无关码是否达到了你想要的目的。

位置无关码,依赖于程序当前运行的PC值,进行相对的跳转,导致的结果就是,无论代码在哪,总能达到指令的正常目的,因此是位置无关的。

位置有关码,不依赖当前PC值,是绝对跳转,只有程序运行在链接地址处时,才能达到指令的正常目的,因此是位置有关系的。

下面,我们来看常用的汇编指令以及C语言中哪些操作是位置有关码,哪些是位置无关码。


SECTIONS {  
   . = 0x33f80000;  
   .text : { *(.text) }  
     
   . = ALIGN(4);  
   .rodata : {*(.rodata*)}   
     
   . = ALIGN(4);  
   .data : { *(.data) }  
     
   . = ALIGN(4);  
   __bss_start = .;  
   .bss : { *(.bss)  *(COMMON) }  
   __bss_end = .;  
}  


.text  
.global _start  
_start:  
 
   bl close_watch_dog      /* 相对跳转,位置无关 */  
   bl _start  
   adr r0, close_watch_dog /* 获取标号地址,位置无关 */  
     
   ldr r0, SMRDATA         /* 获取标号处的值,位置无关 */  
 
   ldr r0, =0x12345678  
   ldr r0, =SMRDATA        /* 获取标号地址,位置有关 */  
   ldr r0, =main           /* 获取函数名的地址,位置有关 */  
   ldr r0 ,=__bss_start    /* 获取链接脚本里标号的地址,位置有关 */  
 
     
close_watch_dog:  
   mov r1, #0  
   str r1, [r0]  
   mov pc, lr  
     
SMRDATA:  
   .word  0x22111120 


int a;  
void abc(){  
    a = 2;  
}  
int main(){  
    int b;  
    a =1 ;  
    b =1 ;  
    abc();  
    return 0;  
}


如果加载地址为 0 ,那么代码将按照下面的顺序排放

00000000 <_start>:  
00000000:   eb000006    bl  33f80020 <close_watch_dog>  
00000004:   ebfffffd    bl  33f80000 <_start>  
00000008:   e28f0010    add r0, pc, #16  
0000000c:   e59f0018    ldr r0, [pc, #24]   ;   
00000010:   e59f0018    ldr r0, [pc, #24]   ;   
00000014:   e59f0018    ldr r0, [pc, #24]   ;   
00000018:   e59f0018    ldr r0, [pc, #24]   ;   
0000001c:   e59f0018    ldr r0, [pc, #24]   ;   
 
00000020 <close_watch_dog>:  
00000020:   e3a01000    mov r1, #0  
00000024:   e5801000    str r1, [r0]  
00000028:   e1a0f00e    mov pc, lr  
 
0000002c <SMRDATA>:  
0000002c:   22111120    andscs  r1, r1, #8  
00000030:   12345678    eorsne  r5, r4, #12

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

ARM/Linux嵌入式真题 文章被收录于专栏

让实战与真题助你offer满天飞!!! 每周更新!!! 励志做最全ARM/Linux嵌入式面试必考必会的题库。 励志讲清每一个知识点,找到每个问题最好的答案。 让你学懂,掌握,融会贯通。 因为技术知识工作中也会用到,所以踏实学习哦!!!

全部评论

相关推荐

3 10 评论
分享
牛客网
牛客企业服务