9_重定位
Posted 韦东山
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了9_重定位相关的知识,希望对你有一定的参考价值。
第九章 重定位
9.1 段的概念
段是程序的组成元素。将整个程序分成一个一个段,并且给每个段起一个名字,然后在链接时就可以用这个名字来指示这些段,使得这些段排布在合适的位置。
程序的段包括
- 代码段(.text):存放代码指令
- 只读数据段(.rodata):存放有初始值并且const修饰的全局类变量(全局变量或static修饰的局部变量)
- 数据段(.data):存放有初始值的全局类变量
- 零初始化段(.bss):存放没有初始值或初始值为0的全局类变量
- 注释段(.comment):存放注释
注意:
- bss段和注释段不保存在bin/elf文件中
- 注释段里面的机器码是用来表示文字的
下面将通过一个实例来直观地感受程序中的段,该实例的工程代码放在裸机Git仓库 NoosProgramProject/(9_重定位/001_segment) 文件夹下。
9.1.1 步骤1:在主函数文件中创建不同属性的全局变量
程序文件:main.c
05 char g_charA = 'A'; //存储在 .data段
06 const char g_charB = 'B'; //存储在 .rodata段
07 const char g_charC; //存储在 .bss段
08 int g_intA = 0; //存储在 .bss段
09 int g_intB; //存储在 .bss段
9.1.2 步骤2:创建链接脚本
这里先用着链接脚本,具体如何使用会在《章节9-1.2 链接脚本分析》中详细说明
链接脚本:imx6ull.lds
SECTIONS
. = 0x80100000;
. = ALIGN(4);
.text :
*(.text)
. = ALIGN(4);
.rodata : *(.rodata)
. = ALIGN(4);
.data : *(.data)
. = ALIGN(4);
__bss_start = .;
.bss : *(.bss) *(.COMMON)
__bss_end = .;
9.1.3 步骤3:在Makefile文件中指明使用链接脚本imx6ull.lds控制链接过程
# 使用链接脚本
$(LD) -T imx6ull.lds -g start.o uart.o main.o my_printf.o -o relocate.elf -lgcc -L/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/6.2.1
9.1.4 步骤4:参考章节《4-1.4-1.4.4 编译程序》编译程序并查看反汇编文件relocate.dis
打开反汇编文件发现
- 在反汇编文件中程序的地址从0x80100000开始
- 整个程序被分为不同的段,每个段以Disassembly of section …作为开始
- 段落之间的地址是连续的,并且从低地址到高地址,段依次为:代码段、只读数据段、数据段、bss段、注释段(注意bss段和注释段不包含在elf/bin文件中)
反汇编文件:relocate.dis
relocate.elf: file format elf32-littlearm
Disassembly of section .text: //代码段
80100000 <_start>:
80100000: e59fd028 ldr sp, [pc, #40] ; 80100030 <clean+0x14>
80100004: eb000001 bl 80100010 <clean_bss>
80100008: fb000070 blx 801001d2 <main>
……(省略)
Disassembly of section .rodata: //只读数据段
8010086c <g_charB>:
8010086c: 00000042 andeq r0, r0, r2, asr #32
……(省略)
Disassembly of section .data: //数据段
8010098c <g_charA>:
8010098c: 00000041 andeq r0, r0, r1, asr #32
80100990 <hex_tab>:
80100990: 33323130 teqcc r2, #48, 2
80100994: 37363534 ; <UNDEFINED> instruction: 0x37363534
80100998: 62613938 rsbvs r3, r1, #56, 18 ; 0xe0000
8010099c: 66656463 strbtvs r6, [r5], -r3, ror #8
Disassembly of section .bss: //bss段,不保存在.bin文件中
801009a0 <__bss_start>:
801009a0: 00000000 andeq r0, r0, r0
801009a4 <IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA>:
801009a4: 00000000 andeq r0, r0, r0
801009a8 <g_intA>:
801009a8: 00000000 andeq r0, r0, r0
801009ac <g_intB>:
801009ac: 00000000 andeq r0, r0, r0
801009b0 <g_charC>:
...
……(省略)
Disassembly of section .comment: //comment段,不保存在.bin文件中
……(省略)
9.2 链接脚本解析
顾名思义,链接脚本控制程序的链接过程,它规定如何把输入文件内的段放入输出文件, 并控制输出文件内的各部分在程序地址空间内的布局。
此节配套的源码在**裸机Git仓库 NoosProgramProject/(9_重定位/02_clean_bss)**目录内。
为了在链接时使用链接脚本,需要在Makefile用**-T filename.lds**指定。否则在编译时将使用默认的链接脚本。
#使用链接脚本imx6ull.lds
$(LD) -T imx6ull.lds -g start.o uart.o main.o my_printf.o -o relocate.elf -lgcc -L/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/6.2.1
需要注意,对于结构较为简单的程序,也可以使用默认的链接脚本,并手动指定不同段在输出文件中的位置。
#将所有程序的.text段放在一起,起始地址设置为0x80100000
#将所有程序的.data段放在一起,起始地址设置为0x80102000
$(LD) -Ttext 0x80100000 -Tdata 0x80102000 -g start.o uart.o main.o my_printf.o -o relocate.elf -lgcc -L/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/6.2.1
默认的链接脚本无法进行一些段的复杂操作,所以下面的程序中我们一律使用链接脚本。
9.2.1 链接脚本语法
本章节中所有的知识都来源于GNU官方文档:
http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html
链接脚本的结构为
SECTIONS
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
contents >region :phdr =fill
...
- secname:段的名称
- start:段的运行地址(runtime addr),也称为重定位地址(relocation addr)
- AT ( ldadr ):ldadr是段的加载地址(load addr);AT是链接脚本函数,用于将该段的加载地址设定为ldadr;如果不添加这个选项,默认的加载地址等于运行地址。
- 其他的链接脚本函数我们之后用到了再讲,想进一步了解可以参考上面的官方文档
- contents : 用来表示段的起始结束;content为该段包含的内容,可以由用户自己指定。
- BLOCK(align) (NOLOAD),>region :phdr =fill:很少用到不深入讲解
依照上述的结构我们来分析本章节中1.1.2中的链接脚本imx6ull.lds
9.2.2 解析链接脚本
链接脚本:imx6ull.lds
01 SECTIONS
02 . = 0x80100000; //设定链接地址为0x80100000
03
04 . = ALIGN(4); //将当前地址以4字节为标准对齐
05 .text : //创建段,其名称为 .text
06 //.text包含的内容为所有链接文件的数据段
07 *(.text) // *:表示所有文件
08
09
10 . = ALIGN(4); //将当前地址以4字节为标准对齐
11 .rodata : *(.rodata) //.rodata存放在.text之后,包含所有链接文件的只读数据段
12
13 . = ALIGN(4);
14 .data : *(.data) //.data存放在.rodata之后,包含所有链接文件的只读数据段
15
16 . = ALIGN(4);
17 __bss_start = .; //将当前地址的值存储为变量__bss_start
18 .bss : *(.bss) *(.COMMON) //.bss存放在.data段之后, 包含所有文件的bss段和注释段
19 __bss_end = .; //将当前地址的值存储为变量__bss_end
20
根据上述链接脚本的配置,.bin文件中的数据结构如下图所示:
上面我们写的链接脚本称为一体式链接脚本,与之相对的是分体式链接脚本,区别在于代码段(.text)和数据段(.data)的存放位置是否是分开的。
例如现在的一体式链接脚本的代码段后面依次就是只读数据段、数据段、bss段,都是连续在一起的。 分体式链接脚本则是代码段、只读数据段,中间间隔很远之后才是数据段、bss段。
分体式链接脚本实例:
SECTIONS
. = 0x80100000; //设置链接地址为0x80100000,这也是.text段的起始地址
. = ALIGN(4);
.text :
*(.text)
. = ALIGN(4);
.rodata : *(.rodata) //假设rodata段的结束地址为0x8010xxxx
. = ALIGN(4);
.data 0x80800000 : *(.data) //指定data段的起始地址为0x80200000,和rodata段之间较大间隔
……(省略)
之后的代码更多的采用一体式链接脚本,原因如下:
1. 分体式链接脚本适合单片机,因为单片机自带有flash,不需要将代码复制到内存占用空间。而我们的嵌入式系统内存非常大,没必要节省这点空间,并且有些嵌入式系统没有可以直接运行代码的Flash,就需要从存储设备如Nand Flash或者SD卡复制整个代码到内存;
2. JTAG等调试器一般只支持一体式链接脚本;
9.2.3 清除bss段
之前提到过bin文件中并不会保存bss段的值,因为这些值都是0,保存这些值没有意义并会使得bin文件臃肿。
当程序运行涉及到bss段上的数据时,CPU会从bss段对应的内存地址去读取对应的值,为了确保从这段内存地址上读取到的bss段数值为0,在程序运行前需要将这一段内存地址上的数据清零,即清除bss段。
这一节将通过汇编清除bss段数据,相关的工程代码放在目录 002_clean_bss
9.2.3.1 步骤1:修改汇编文件
我们在汇编文件中实现清除bss段,具体思路就是将bss段对应的地址读取,并将地址上的数据依次清零。
汇编文件:start.S
01
02 .text
03 .global _start
04
05 _start:
06
07 /* 设置栈 */
08 ldr sp,=0x80200000
09
10 /* 清除bss段 */
11 bl clean_bss
12
13 /* 跳转到主函数 */
14 bl main
15
16 halt:
17 b halt
18
19 clean_bss:
20 ldr r1, =__bss_start //将链接脚本变量__bss_start变量保存于r1
21 ldr r2, =__bss_end //将链接脚本变量__bss_end变量保存于r2
22 mov r3, #0
23 clean:
24 strb r3, [r1] //将当前地址下的数据清零
25 add r1, r1, #1 //将r1内存储的地址+1
26 cmp r1, r2 //相等:清零操作结束;否则继续执行clean函数清零bss段
27 bne clean
28
29 mov pc, lr
9.2.3.2 步骤2:在主函数汇中添加测试代码
主函数中打印存放在bss段内数据的值。
程序文件:main.c
37 int main (void)
38
39 Uart_Init(); //初始化uart串口
40
41 printf("g_intA = 0x%08x\\n\\r", g_intA); //打印g_intA的值
42 printf("g_intB = 0x%08x\\n\\r", g_intB); //打印g_intB的值
43
44 return 0;
45
46
9.2.3.3 步骤3:参考章节《4-1.4编译程序》编译程序
9.2.3.4 步骤4:参考章节《3-1.4映像文件烧写、运行》烧写、运行程序
最终在终端中输出结果如下,保存在bss段中的变量g_intA, g_intB的值都为0,表明清除bss段成功。
g_intA = 0x00000000
g_intB = 0x00000000
9.3 重定位的引入
9.3.1 什么是重定位
接触过S3C2440的朋友应该很熟悉,在程序运行之前我们需要手动将.bin文件上的全部代码从Nor Flash或SRAM拷贝到SDRAM上。对于imx6ull来说,这部分拷贝代码的操作由Boot Rom自动完成,板子上电后Boot Rom会将映像文件从启动设备(TF卡、eMMC)自动拷贝到DDR3内存上。上述拷贝代码的过程就是重定位。
那么Boot Rom应该将映像文件拷贝到内存的哪个位置呢?这部分内容已经在章节《3-1.2 IMX6ULL启动流程 》中详细讨论过了。简而言之100ask_imx6ull的映像文件包含多个部分,其中.bin文件的起始地址由地址entry决定,需要在Makefile中手动配置。
./tools/mkimage -n ./tools/imximage.cfg.cfgtmp -T imximage -e **0x80100000** -d relocate.bin relocate.imx
按照上述的配置,整个映像文件被自动重定位到DDR3内存上,其中.bin文件的起始地址为0x80100000。重定位结束后,CPU会从这个地址读取第一条指令开始执行程序。
9.3.2 汇编重定位data段
下面我们将通过一个实例来说明为什么要重定位data段以及如何通过汇编重定位data段。
在002_clean_bss代码的基础上,在主函数中添加测试代码,不断地打印data段中的数据g_charA。该程序放在**裸机Git仓库 NoosProgramProject/(9_重定位/003_without_relocation)**文件夹内。
程序文件:main.c
37 int main (void)
38
39 Uart_Init(); //初始化uart串口
40
41 printf("\\n\\r");
42 /* 在串口上输出g_charA */
43 while (1)
44
45 PutChar(g_charA);
46 g_charA++;
47 delay(1000000);
48
49
50 return 0;
51
9.3.2.1 步骤1:参考章节《4-1.4编译程序》编译程序
9.3.2.2 步骤2:参考章节《3-1.4映像文件烧写、运行》烧写、运行程序
最终在终端上成功打印字符g_charA的值。
ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz|▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
123456789:;<=>?@A
在程序运行时,CPU需要不断地访问DDR3内存来获取g_charA的值,访问DDR3会花费大量的时间,那么如何提升访问的效率呢?
答:在程序运行先前将data段的数据重定位到imx6ull的片内RAM上,因为CPU访问片内RAM的速度远快于访问DDR3的速度。
下面我们将通过汇编重定位data段。该实例保存在**裸机Git仓库 NoosProgramProject/(9_重定位/004_manual_relocate_data)**文件夹内。
9.3.2.3 步骤1:参考芯片手册确定片内RAM的位置
参考资料:芯片手册《Chapter 2: Memory Maps》
参考芯片手册得到片内RAM的地址为:0x900000 ~ 0x91FFFF。所以我们将.data段重定位后的地址设置为0x900000。
9.3.2.4 步骤2:修改链接脚本
创建一个变量用来存储.data段的起始加载地址。
. = ALIGN(4);
.rodata : *(.rodata)
. = ALIGN(4);
data_load_addr = .; //将当前地址存储在变量中(大概的值为0x8010xxxx)
将.data段的运行地址(runtime address)设定为0x900000。加载地址由变量data_load_addr确定。这样设置后,在.bin文件中.data段仍旧存储在.rodata段之后。但在程序运行时,CPU会从0x900000开始的空间内读取.data段的值。
.data 0x900000 : AT(data_load_addr)
下面我们将重定位后.data段的起始地址存储在变量data_start,重定位后的.data段的结束地址存储在变量data_end,这两个变量将供汇编文件调用。
data_start = . ; //addr = 0x900000
*(.data)
data_end = . ; //addr = 0x900000+SIZEOF(.data)
修改后的链接脚本如下所示
链接脚本imx6ull.lds
SECTIONS
. = 0x80100000;
. = ALIGN(4);
.text :
*(.text)
. = ALIGN(4);
.rodata : *(.rodata)
. = ALIGN(4);
data_load_addr = .;
.data 0x900000 : AT(data_load_addr)
data_start = . ;
*(.data)
data_end = . ;
. = ALIGN(4);
__bss_start = .;
.bss : *(.bss) *(.COMMON)
__bss_end = .;
通过上述操作,CPU虽然会去片内RAM中读取.data段数据,但实际上片内RAM并没有准备好.data段的数据,如下图所示。下面我们将通过汇编将DDR3内存上的.data段数据重定位到片内RAM上。
9.3.2.5 步骤3:修改汇编文件重定位.data段
设置完栈后直接跳转到copy_data函数重定位data段
汇编文件:start.S
/* 设置栈 */
ldr sp,=0x80200000
/* 重定位data段 */
bl copy_data
/* 清除bss段 */
bl clean_bss
实现copy_data函数
汇编文件:start.S
copy_data:
/* 重定位data段 */
ldr r1, =data_load_addr /* data段的加载地址, 从链接脚本中得到, 0x8010xxxx */
ldr r2, =data_start /* data段重定位地址, 从链接脚本中得到, 0x900000 */
ldr r3, =data_end /* data段结束地址, 从链接脚本中得到,0x90xxxx */
cpy:
ldr r4, [r1] /* 从r1读到r4 */
str r4, [r2] /* r4存放到r2 */
add r1, r1, #4 /* r1+1 */
add r2, r2, #4 /* r2+1 */
cmp r2, r3 /* r2 r3比较 */
bne cpy /* 如果不等则继续拷贝 */
mov pc, lr /* 跳转回调用copy_data函数之前的地址 */
9.3.2.6 步骤3:参考章节《4-1.4编译程序》编译程序
9.3.2.7 步骤4:参考章节《4-1.4映像文件烧写、运行》烧写、运行程序
将目录**裸机Git仓库 NoosProgramProject/中(9_重定位/003_without_relocation)和目录裸机Git仓库 NoosProgramProject/中(9_重定位/004_manual_relocate_data)**中的程序分别烧录、运行,发现重定位data段后终端上打印字符的速度明显变快。
9.4 C函数重定位data段和清除bss段
到目前为止我们已经通过汇编实现了重定位data段和清除bss段。为了让汇编程序更加简洁,这一节中我们将通过C语言实现重定位data段和清除bss段。
9.4.1 通过汇编传递链接脚本变量
这一小节中我们将通过汇编文件获得链接脚本中的变量,再将这些变量传递给C函数。工程文件放在裸机Git仓库 NoosProgramProject/(9_重定位/005_relocate_data_with_c)目录内。
9.4.1.1 步骤1:修改汇编文件
打开start.S将之前的汇编函数copy_data, clean_bss删除,改为直接调用C函数。在调用对应的C函数之前,需要通过寄存器r0~r4将C函数的参数准备好。
汇编文件:start.S
.text
.global _start
_start:
/* 设置栈 */
ldr sp,=0x80200000
/* 重定位data段 */
ldr r0, =data_load_addr /* data段的加载地址 (0x8010....) */
ldr r1, =data_start /* data段重定位地址, 0x900000 */
ldr r2, =data_end /* data段结束地址(重定位后地址 0x90....) */
sub r2, r2, r1 /* r2的值为data段的长度 */
bl copy_data /* 跳转到函数copy_data并将r1,r2,r3作为函数参数传入 */
/* 清除bss段 */
ldr r0, =__bss_start
ldr r1, =__bss_end
bl clean_bss /* 跳转到函数clean_bss并将r0, r1作为函数参数传入*/
/* 跳转到主函数 */
bl main
halt:
b halt
9.4.1.2 步骤2:创建程序文件init.c实现copy_data, clean_bss函数
程序文件:init.c
/* 从汇编得到参数src, dest, len的值 */
void copy_data (volatile unsigned int *src, volatile unsigned int *dest, unsigned int len)
unsigned int i = 0;
while (i < len)
*dest++ = *src++;
i += 4;
/* 从汇编得到参数start, end的值 */
void clean_bss (volatile unsigned int *start, volatile unsigned int *end)
while (start <= end)
*start++ = 0;
需要注意的是,上述两个函数的参数都是从汇编文件传入
- 对于copy_data函数来说,参数src, dest, len分别对应汇编文件中r1, r2, r3的值
- 对于clean_bss函数来说,参数start, end分别对应汇编文件中r0, r1的值
9.4.1.3 步骤3:修改Makefile
修改Makefile文件,编译init.c并链接init.o
文件:Makefile
08 relocate.img : start.S uart.c main.c my_printf.c init.c
09 $(CC) -nostdlib -g -c -o start.o start.S
10 $(CC) -nostdlib -g -c -o uart.o uart.c
11 $(CC) -nostdlib -g -c -o main.o main.c
12 $(CC) -nostdlib -g -c -o my_printf.o my_printf.c
13 $(CC) -nostdlib -g -c -o init.o init.c
14
15 $(LD) -T imx6ull.lds -g start.o uart.o main.o my_printf.o init.o -o relocate.elf -lgcc -L/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/6.2“重定位 R_X86_64_32S 反对”链接错误
R_X86_64_32S 和 R_X86_64_64 重定位是啥意思?