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 重定位是啥意思?

重定位被截断以适应:R_386_8 针对“.rodata”

重定位 R_X86_64_32S 对 `.data' 在制作共享对象时不能使用;使用 gcc 重新编译 -fPIC

代码重定位

arm重定位