第九章 重定位
9.1 段的概念
段是程序的组成元素。将整个程序分成一个一个段,并且给每个段起一个名字,然后在链接时就可以用这个名字来指示这些段,使得这些段排布在合适的位置。
程序的段包括
- 代码段(.text):存放代码指令
- 只读数据段(.rodata):存放有初始值并且const修饰的全局类变量(全局变量或static修饰的局部变量)
- 数据段(.data):存放有初始值的全局类变量
- 零初始化段(.bss):存放没有初始值或初始值为0的全局类变量
- 注释段(.comment):存放注释
注意:
- bss段和注释段不保存在bin/elf文件中
- 注释段里面的机器码是用来表示文字的
下面将通过一个实例来直观地感受程序中的段,该实例的工程代码放在裸机Git仓库 NoosProgramProject/(9_重定位/001_segment) 文件夹下。
9.1.1 步骤1:在主函数文件中创建不同属性的全局变量
程序文件:main.c
代码语言:javascript复制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
代码语言:javascript复制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控制链接过程
代码语言:javascript复制# 使用链接脚本
$(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
代码语言:javascript复制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**指定。否则在编译时将使用默认的链接脚本。
代码语言:javascript复制#使用链接脚本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
需要注意,对于结构较为简单的程序,也可以使用默认的链接脚本,并手动指定不同段在输出文件中的位置。
代码语言:javascript复制#将所有程序的.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
链接脚本的结构为
代码语言:javascript复制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
代码语言:javascript复制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段。
分体式链接脚本实例:
代码语言:javascript复制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
代码语言:javascript复制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
代码语言:javascript复制37 int main (void)
38 {
39 Uart_Init(); //初始化uart串口
40
41 printf("g_intA = 0xxnr", g_intA); //打印g_intA的值
42 printf("g_intB = 0xxnr", 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段成功。
代码语言:javascript复制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中手动配置。
代码语言:javascript复制./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
代码语言:javascript复制37 int main (void)
38 {
39 Uart_Init(); //初始化uart串口
40
41 printf("nr");
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的值。
代码语言:javascript复制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段的起始加载地址。
代码语言:javascript复制. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
data_load_addr = .; //将当前地址存储在变量中(大概的值为0x8010xxxx)
将.data段的运行地址(runtime address)设定为0x900000。加载地址由变量data_load_addr确定。这样设置后,在.bin文件中.data段仍旧存储在.rodata段之后。但在程序运行时,CPU会从0x900000开始的空间内读取.data段的值。
代码语言:javascript复制.data 0x900000 : AT(data_load_addr)
下面我们将重定位后.data段的起始地址存储在变量data_start,重定位后的.data段的结束地址存储在变量data_end,这两个变量将供汇编文件调用。
代码语言:javascript复制{
data_start = . ; //addr = 0x900000
*(.data)
data_end = . ; //addr = 0x900000 SIZEOF(.data)
}
修改后的链接脚本如下所示
链接脚本imx6ull.lds
代码语言:javascript复制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
代码语言:javascript复制 /* 设置栈 */
ldr sp,=0x80200000
/* 重定位data段 */
bl copy_data
/* 清除bss段 */
bl clean_bss
实现copy_data函数
汇编文件:start.S
代码语言:javascript复制 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
代码语言:javascript复制 .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
代码语言:javascript复制 /* 从汇编得到参数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;
}
}
代码语言:javascript复制 /* 从汇编得到参数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
代码语言:javascript复制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.1
9.4.1.4 步骤3:参考章节《4-1.4编译程序》编译程序
9.4.1.5 步骤4:参考章节《4-1.4映像文件烧写、运行》烧写、运行程序
程序成功运行,在终端成功输出字符串
代码语言:javascript复制ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
123456789:;<=>?@A
9.4.2 C函数直接调取链接脚本变量
上一节中C函数需要通过汇编文件传入参数,在这一节我们将进一步改进C函数,使得C函数跳过汇编文件,直接从链接脚本中调用所需变量。
工程文件放在**裸机Git仓库 NoosProgramProject/(9_重定位/006_relocate_data_with_c_modified)**目录内。
9.4.2.1 步骤1:修改汇编文件,改为直接调用C函数
汇编文件:start.S
代码语言:javascript复制 .text
.global _start
_start:
/* 设置栈 */
ldr sp,=0x80200000
/* 重定位data段 */
bl copy_data
/* 清除bss段 */
bl clean_bss
/* 跳转到主函数 */
bl main
halt:
b halt
9.4.2.2 步骤2:修改init.c 通过函数来获取参数
程序文件:init.c
代码语言:javascript复制12 void copy_data (void)
13 {
14 /* 从链接脚本中直接获得参数 data_load_addr, data_start, data_end */
15 extern int data_load_addr, data_start, data_end;
16
17 volatile unsigned int *dest = (volatile unsigned int *)&data_start;
18 volatile unsigned int *end = (volatile unsigned int *)&data_end;
19 volatile unsigned int *src = (volatile unsigned int *)&data_load_addr;
20
21 /* 重定位数据 */
22 while (dest < end)
23 {
24 *dest = *src ;
25 }
26 }
代码语言:javascript复制38 void clean_bss(void)
39 {
40 /* 从lds文件中获得 __bss_start, __bss_end */
41 extern int __bss_end, __bss_start;
42
43 volatile unsigned int *start = (volatile unsigned int *)&__bss_start;
44 volatile unsigned int *end = (volatile unsigned int *)&__bss_end;
45
46 while (start <= end)
47 {
48 *start = 0;
49 }
50 }
9.4.2.3 步骤3:参考章节《4-1.4编译程序》编译程序
9.4.2.4 步骤4:参考章节《4-1.4映像文件烧写、运行》烧写、运行程序
程序成功运行,在终端成功输出字符串
代码语言:javascript复制ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
123456789:;<=>?@A
9.4.3 总结:如何在C函数中使用链接脚本变量
结合上面的例子,我们来总结一下如何在C函数中使用链接脚本中定义的变量
1. 在C函数中声明该变量为外部变量,用extern修饰,例如: extern int _start;
2. 使用取址符号(&)得到该变量的值,例如:int * p = & _ start; //p的值为lds文件中_start的值
为什么在汇编文件中可以直接使用链接脚本中的变量,而在C函数中需要加上取址符号呢?
原因:C函数中定义一个全局变量int g_i = 10;,程序中必然有4字节的空间留出来给这个变量g_i,然而链接脚本中的变量并像全局变量一样都保存在.bin文件中。如果我们在C程序中只用到链接脚本变量a1, a2, a3,那么程序只保存这3个变量。在编译程序时,会将这些要用到的变量保存在symbol_table符号表中,如下图所示:
[外链图片转存中…(img-tuKna5Mp-1642060139839)]
从上图中我们注意到:
对于全局变量来说,地址里面存储的是变量存放的地址;可以通过&g_i得到变量存储的地址addr。
对于链接脚本变量来说,地址里面存储的是变量的值;为了保持代码的一致性,通过&a1得到变量的值value。
9.5 重定位全部代码
9.5.1 C函数实现重定位全部代码
虽然100ask_imx6ull的映像文件会自动被重定位到DDR3内存上,但对于一些采用其他芯片的板子,这一部分的操作需要我们手动去完成。例如S3C2440 上电后,因为硬件的限制,.bin文件的前4k程序需要将整个程序重定位到大小能够执行整个程序的SDRAM上。
为了涉及到代码重定位所需知识,在这一节中我们将重定位整个.bin文件到片内RAM上。
工程文件放在裸机Git仓库 NoosProgramProject/(9_重定位/007_relocate_all_with_c) 目录下。
需要注意,虽然将全部代码重定位到片内RAM上可以加快命令的执行、数据的读取写入,但是这样的做法并不适合体积较大的程序,因为片内RAM只有128KB空间。
9.5.1.1 步骤1:修改链接脚本
1. 修改链接地址为0x900000
2. 删除与.data段相关的链接脚本变量。
3. 添加变量_load_addr并将它的值设置为Makefile中entry地址的值,供C函数调用。
链接脚本:imx6ull.lds
代码语言:javascript复制01 SECTIONS {
02 _load_addr = 0x80100000;
03
04 . = 0x900000;
05
06 . = ALIGN(4);
07 .text :
08 {
09 *(.text)
10 }
11
12 . = ALIGN(4);
13 .rodata : { *(.rodata) }
14
15 . = ALIGN(4);
16 .data : { *(.data) }
17
18 . = ALIGN(4);
19 __bss_start = .;
20 .bss : { *(.bss) *(.COMMON) }
21 __bss_end = .;
22 }
9.5.1.2 步骤2:修改init.c
重定位全部代码和重定位.data段原理相同。
在这里只需要修改copy_data函数中调用的外部变量。
程序文件:init.c
代码语言:javascript复制12 void copy_data (void)
13 {
14 /* 从链接脚本中获得参数 _start, __bss_start, */
15 extern int _load_addr, _start, __bss_start;
16
17 volatile unsigned int *dest = (volatile unsigned int *)&_start; //_start = 0x900000
18 volatile unsigned int *end = (volatile unsigned int *)&__bss_start; //__bss_start = 0x9xxxxx
19 volatile unsigned int *src = (volatile unsigned int *)&_load_addr; //_load_addr = 0x80100000
20
21 /* 重定位数据 */
22 while (dest < end)
23 {
24 *dest = *src ;
25 }
26 }
9.5.1.3 步骤3:修改汇编文件
重定位之后,需要使用绝对跳转命令ldr pc, = xxx,跳转到重定位后的地址。
汇编文件:start.S
代码语言:javascript复制16 /* 跳转到主函数 */
17 // bl main /* 相对跳转,程序仍在DDR3内存中执行 */
18 ldr pc, =main /* 绝对跳转,程序在片内RAM中执行 */
9.5.1.4 步骤3:参考章节《4-1.4编译程序》编译程序
9.5.1.5 步骤4:参考章节《4-1.4映像文件烧写、运行》烧写、运行程序
程序成功运行,在终端成功输出字符串
代码语言:javascript复制ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
123456789:;<=>?@A
9.5.2 位置无关码
查看上述程序的反汇编发现,在重定位函数copy_data执行之前,已经涉及到了片内RAM上的地址,但此时片内RAM上并没有重定位的程序,那为什么程序还能正常运行呢?
反汇编文件:relocate.dis
代码语言:javascript复制07 00900000 <_start>:
08 900000: e59fd00c ldr sp, [pc, #12] ; 900014 <halt 0x4>
09 900004: fa00016f blx 9005c8 <copy_data>
10 900008: fb000180 blx 900612 <clean_bss>
11 90000c: e59ff004 ldr pc, [pc, #4] ; 900018 <halt 0x8>
答:这里的地址只是一个相对地址,是为了方便理解程序才这样标识。实际上CPU执行的第一条命令还是从0x80100000读取的,这是由entry决定的。
第9行和第10行中的blx命令跳转到的地址是一个相对地址(pc offset),而不是绝对地址(0x9005c8)。在反汇编文件中这样写,只是起到方便查看的作用,并不是真的跳转。
下面我们来分析一下实际板子上电后,程序是如何执行的
1. 程序被自动重定位到0x80100000,并从这个地址开始执行第一条指令,此时pc = 0x80100000 8 = 0x80100008。
2. 计算可得offset = 0x9005c8 8 – 0x900004 = 0x5cc,所以反汇编文件中的第二条指令(存放在0x80100004)实际上是跳转到地址0x801005c8执行copy_data函数。
3. 在执行完copy_data和clean_bss函数后,执行绝对跳转命令ldr pc, =main,使得pc = 0x90000c 8 4 = 0x900018,所以之后的程序会跳到片内RAM中执行。
那么我们应该如何写位置无关码呢?
答:使用相对跳转命令 b或bl,并注意
- 重定位之前,不可使用绝对地址
a) 不可访问全局类变量(全局变量或static修饰的局部变量)
b) 不可访问有初始值的数组(初始值放在rodata里,需要绝对地址来访问)
- 重定位之后,使用ldr pc = xxx,跳转到绝对地址(runtime address)