Skip to main content

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.1

9.4.1.4 步骤3:参考章节《4-1.4编译程序》编译程序

9.4.1.5 步骤4:参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 程序成功运行,在终端成功输出字符串

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

 .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

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 }
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映像文件烧写、运行》烧写、运行程序

​ 程序成功运行,在终端成功输出字符串

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符号表中,如下图所示:

​ 从上图中我们注意到:

​ 对于全局变量来说,地址里面存储的是变量存放的地址;可以通过&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

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

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

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映像文件烧写、运行》烧写、运行程序

​ 程序成功运行,在终端成功输出字符串

ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
123456789:;<=>?@A

9.5.2 位置无关码

​ 查看上述程序的反汇编发现,在重定位函数copy_data执行之前,已经涉及到了片内RAM上的地址,但此时片内RAM上并没有重定位的程序,那为什么程序还能正常运行呢?

​ 反汇编文件:relocate.dis

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)