自学内容网 自学内容网

JZ2440开发板——代码重定位

以下内容源于韦东山课程的学习与整理,如有侵权请告知删除。 

目录

一、段的概念、重定位的引入

1.1 Nand Flash启动时的重定位

1.2 Nor Flash启动时的重定位

1.2.1 不能直接写Nor Flash

1.2.2 解决.bin文件体积过大问题 

1.2.3 烧写现象与分析 

1.3 段的概念

(1)代码段

(2)数据段

(3)只读数据段

(4)bss段

(5)comment段

(6)bss、comment段不占空间

二、链接脚本的引入与测试

2.1 将数据段放在SDRAM中

2.1.1 出现黑洞问题 

2.2.2 解决黑洞问题

2.3.3 引入链接脚本

2.3.4 重定位的写法

(1)重定位分析

(2)重定位写法 

(3)测试与验证

三、链接脚本的解析

3.1 链接脚本语法 

3.2 加载地址 vs 运行地址

3.2.1 .elf格式的文件

3.2.2 .bin格式的文件

3.3 清零bss段

四、拷贝代码与链接脚本的改进

4.1 ldrb、strb ==> ldr、str

4.2 在链接脚本中添加 .ALIGN(4) 

4.2.1 部分数据段内容被清零 

4.2.2 部分数据段被清零的原因

4.2.3 在链接脚本添加.ALIGN(4)

4.2.4 uboot中的链接脚本与拷贝代码

五、代码重定位和位置无关码

5.1 位置无关码的含义

5.2 修改链接脚本

5.3 修改start.S文件

5.3.1 实现复制.text、.data、.rodata段

5.3.2 由反汇编文件分析启动过程

5.3.3 bl main —>ldr pc,=main

5.4 怎么写位置无关码

5.5 sdram_init函数中的位置无关码

六、重定位、清除bss段的C函数实现

6.1 汇编文件给C函数传入参数

6.2 C函数从链接脚本获得参数

6.3 C函数如何使用链接脚本中的变量


一、段的概念、重定位的引入

S3C2440的CPU可以直接给SDRAM发送命令、给Nor Flash发送命令、给4K的片上SDRAM发送命令、给Nand Flash控制器发送命令,但是不能直接给Nand Flsh发送命令。如果把.bin文件烧写到Nand Flsh上,CPU是无法直接从Nand Flsh中取代码执行的。

1.1 Nand Flash启动时的重定位

那为什么还可以从Nand Flash启动?这是因为:

1)上电后,硬件会自动把Nand Flsh前4K复制到片内内存SRAM;

2)然后CPU从片内内存SRAM的0地址开始运行(Nand启动时片内内存SRAM的基地址是0)。

从Nand Flash启动时,如果.bin文件大于4K,则前4K的代码需要把整个程序读出来放到SDRAM,即代码重定位。

1.2 Nor Flash启动时的重定位

1.2.1 不能直接写Nor Flash

如果以Nor Flash启动,则CPU认为0地址在Nor Flash上面,片内内存SRAM的基地址就变成了0x40000000。

局部变量是存放在栈中的。文件start.S中已经根据启动方式来设置栈指向(如下代码所示),而无论哪种启动方式,栈都指向SRAM,而SRAM可以像内存一样操作,所以无论哪种启动方式,局部变量是可读可写的。所以你直接修改局部变量是没有问题的。

    /* 设置内存: sp 栈 */
/* 分辨是nor/nand启动
 * 写0到0地址, 再读出来
 * 如果得到0, 表示0地址上的内容被修改了, 它对应ram, 这就是nand启动
 * 否则就是nor启动
 */
mov r1, #0
ldr r0, [r1] /* 读出原来的值备份 */
str r1, [r1] /* 0->[0] */ 
ldr r2, [r1] /* r2=[0] */
cmp r1, r2   /* r1==r2? 如果相等表示是NAND启动 */
ldr sp, =0x40000000+4096 /* 先假设是nor启动 */
moveq sp, #4096  /* nand启动 */
streq r0, [r1]   /* 恢复原来的值 */

但是全局变量、静态变量是包含在.bin文件中并烧写到Nor Flash上的,由于Nor Flash的特性是“ 可以像内存一样读,但不能像内存一样直接写 ”,因此可以直接读全局变量、静态变量,但不能直接修改它们。

从Nor Flash启动时,如果代码中需要修改全局变量、静态变量,则需要把全局变量、静态变量重定位放到SDRAM中。

比如执行以下几条汇编指令,其中str命令表示要写数据到Nor Flash中(因为是在Nor Flash中),但对于Nor Flash其实是无效的。

mov r0,#0
ldr r1,[r0] @读有效,因为可以像内存一样读
str r1,[r0] @写无效,因为不能像内存一样写

由此可知,无论是从Nand Flash启动,还是从Nor Flash启动,都需要考虑代码的重定位。 

1.2.2 解决.bin文件体积过大问题 

这里举一个“不能直接写Nor Flash”的具体例子,关键代码文件内容如下所示:

//main.c文件内容

#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"

char g_Char = 'A';
const char g_Char2 = 'B';
int g_A = 0;
int g_B;

int main(void)
{
uart0_init();

while (1)
{
putchar(g_Char);
g_Char++;         /* nor启动时, 此代码无效 */
delay(1000000);
}

return 0;
}
# Makefile文件内容

all:
arm-linux-gcc -c -o led.o led.c
arm-linux-gcc -c -o uart.o uart.c
arm-linux-gcc -c -o init.o init.c
arm-linux-gcc -c -o main.o main.c
arm-linux-gcc -c -o start.o start.S
arm-linux-ld -Ttext 0 start.o led.o uart.o init.o main.o -o sdram.elf
arm-linux-objcopy -O binary -S sdram.elf sdram.bin
arm-linux-objdump -D sdram.elf > sdram.dis
clean:
rm *.bin *.o *.elf *.dis

我们编译上面代码得到.bin文件,分别烧写到Nor Flash和Nand Flash中,观察对应的现象。

(1)首先发现编译得到的.bin文件显示过大,为34KB,但是我们只添加几个全局变量而已,体积较以前剧增几十倍,这显然不对:

为何会这样?通过查看反汇编文件sdram.dis,发现.data段被放在了0x00008478这个地址,前后跨度过大导致.bin文件体积过大:

为解决这个问题,在Makefile文件的arm-linux-ld命令中添加 “-Tdata 0x800” 选项,表示把.data数据段放在地址0x800开始的区域:

arm-linux-ld -Ttext 0 -Tdata 0x800  start.o led.o uart.o init.o main.o -o sdram.elf

重新编译,再来查看.bin文件大小:

1.2.3 烧写现象与分析 

(1)将.bin文件烧写到Nand Flash中运行,发现串口输出如下:

从Nand Flash启动,则CPU认为0地址在SRAM上。上电后,Nand Flash前4K代码被硬件复制到SRAM中(.bin文件被全部复制,因为它小于4KB),CPU上电后从SRAM的0地址开始执行,它读取SRAM上的代码,由于SRAM是可读可写的,因此可以进行g_Char++修改变量,下次读取的数据就依次增加了,所以输出“ABCD……”。

(2)将.bin文件烧写到Nor Flash中运行,发现串口会不断地输出A。

从Nor Flash启动,则CPU认为0地址在Nor Flash上,g_Char被放在0x800这个地方。CPU上电后从0地址开始执行,它能读取NorFlash上的代码,打印出g_Char的值'A'。但当进行g_Char++时,由于写操作操作无效,下次读取的数据仍然是'A'。所以不断输出‘A’字符。

查看反汇编文件,看看如何处理g_Char++的:使用了“strb r3 [r2]”语句,而Nor Flash无法像内存一样直接写,因此修改不会起效。

1.3 段的概念

由反汇编文件sdram.dis文件可知,一个程序包括以下几个段:

(1)代码段

代码段用.text表示,用于存放指令。

(2)数据段

数据段用.data表示,用于存放(初值为非零的)全局变量。

char g_Char = 'A';//初始化值为非零的全局变量

(3)只读数据段

只读数据段用.rodata表示,用于存放(只读的)全局变量。

const char g_Char2 = 'B'; //用const修饰,表示只读的全局变量

(4)bss段

bss段用.bss表示,用于存放(初值为零、或者无初值的)全局变量。

int g_A = 0;
int g_B;

(5)comment段

comment段用.commnent表示,用于存放一些字符串。

这个段并非我理解的“用来存放程序员写的注释”(毕竟注释在预编译阶段就去掉了),而是提供了一些编译信息。

比如用winhex输入这些十六进制数字后,部分显示如下(00,47,43,43,对应'G''C''C'):

(6)bss、comment段不占空间

由1.2.2最后可知,编译后得到的.bin文件大小是2049Byte(即0x801)。

我们再来看一下反汇编文件:.data数据段(其实就g_Char这一个全局变量)位于地址0x800,而0~0x800的长度为0x801,刚好就是.bin文件的大小。这说明,接下来的.bss段和.comment段是不被保存在.bin文件中的,或者说不占据存储空间的。

这其实很合理,你想想,如果我要定义一百万个初始化值为0的全局变量,难道这个.bin文件就需要保存一百万个0吗?这显然没有必要。另外comment段也不影响程序的运行,没必要放在.bin文件中。

二、链接脚本的引入与测试

2.1 将数据段放在SDRAM中

从Nor Flash启动时,为了解决不能修改Nor Flash中的全局变量这个问题,我们可以把全局变量所在的数据段放在SDRAM中,这样应该就可以修改全局变量了,因为SDRAM是可读可写的。如下所示:

2.1.1 出现黑洞问题 

为了实现这种效果,我们在arm-linux-gcc命令中添加 -Tdata 0x30000000 选项,表示指定数据段位于0x30000000:

arm-linux-ld -Ttext 0 -Tdata 0x30000000  start.o led.o uart.o init.o main.o -o sdram.elf

由.bin文件的反汇编文件可知,.data数据段的确放在了0x30000000地址: 

但.bin文件居然高达769多MB(从0地址到0x30000000地址,文件大小有700多MB):

xjh@ubuntu:~/iot/embedded_basic/jz2440/armBareMachine/002_013_002$ ll sdram.bin -h
-rwxrwxr-x 1 xjh xjh 769M 九月 29 16:50 sdram.bin*
xjh@ubuntu:~/iot/embedded_basic/jz2440/armBareMachine/002_013_002$ 

这是因为代码段、数据段之间有间隔,我们称之为黑洞。

2.2.2 解决黑洞问题

解决黑洞问题有以下两个方法:

方法一

  • 1)让.bin文件从0地址开始(通过设置代码段的运行地址为0x0),让数据段和代码段在.bin文件中的地址尽量靠近(通过设置数据段的加载地址为AT(0x800),运行地址为0x30000000)。
  • 2)把.bin文件烧写到 Nor Flash 的0地址处。
  • 3)运行时再把数据段从0x800复制到SDRAM的0x3000000位置。

方法二

  • 1)让.bin文件从0x30000000开始(通过设置代码段的运行地址为0x30000000),数据段位于地址(0x30000000+代码段+n)处。
  • 2)把.bin文件烧写到 Nor Flash 的0地址处。
  • 3)运行时再把整个程序(包括代码段、数据段等内容)从0地址复制到SDRAM的0x30000000位置。

这两个方法的区别是前者只重定位了数据段,后者重定位了数据段和代码段。

2.3.3 引入链接脚本

如何实现方法一?这些复杂的功能,通过“在使用链接命令时指定参数”这种简单方式已经无法实现,需要使用到链接脚本。我们可以在 Makefile 文件中使用 “-T xxx.lds” 选项来指定链接脚本xxx.lds,使用链接脚本来指导链接过程。

all:
arm-linux-gcc -c -o led.o led.c
arm-linux-gcc -c -o uart.o uart.c
arm-linux-gcc -c -o init.o init.c
arm-linux-gcc -c -o main.o main.c
arm-linux-gcc -c -o start.o start.S
#arm-linux-ld -Ttext 0 -Tdata 0x30000000  start.o led.o uart.o init.o main.o -o sdram.elf
arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
arm-linux-objcopy -O binary -S sdram.elf sdram.bin
arm-linux-objdump -D sdram.elf > sdram.dis
clean:
rm *.bin *.o *.elf *.dis

链接脚本的语法如下(描述可以参考文档Using LD,the GNU linker,或者配套书籍P40):

SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  { contents } >region :phdr =fill
...
}

据此,我们依次排列代码段、只读数据段、数据段、.bss段、.common段,得到如下链接脚本(注意一下AT的含义):

SECTIONS {
   .text   0  : { *(.text) } //0表示放在0地址,*(.text)表示所有文件的代码段
   .rodata  : { *(.rodata) } //*(.rodata)表示所有文件的只读数据段
   .data 0x30000000 : AT(0x800) { *(.data) } //数据段放在0x800,但运行时是在0x30000000
   .bss  : { *(.bss) *(.COMMON) }//所有文件的bss段、comment段
}

2.3.4 重定位的写法

(1)重定位分析

重新编译,将得到的.bin文件烧写到Nor Flash中,启动后显示乱码。

由“.data 0x30000000 : AT(0x800) { *(.data) }”可知,要从0x30000000处获取数据段,但我们并没有在0x30000000处准备好数据段。

反汇编文件非常明朗地解释了原因:putchar函数的传参是g_Char,我们发现302行有这个字符;往上看到“mov r0,r3”,说明r0是要传入putchar函数的参数,即r0的值就是g_Char,而r0是由r3赋予的;往上看到“ldrb r3,[r3]”以及“ldr r3, [pc, #44]”,表明r3的值是[pc, #44],执行0x440地址的代码时PC=0x440+8=0x448,而0x448+#44=0x474,[0x474]的内容是0x3000000,所以最后可知 r0 等于0x3000000这个地址中的内容。但我们并没有在0x30000000处准备好数据段。因此需要重定位数据段,即将位于0x800的数据段复制到0x30000000处。

(2)重定位写法 

由于数据段中只有g_Char这个全局变量,我们可以在start.S文件中加入下面的代码:

bl sdram_init//注意重定位到内存之前,一定要先初始化内存

/* 重定位data段 */ 
// 把g_Char这个数据从0x800拷贝到0x30000000即可,因为data段就这么一个数据
mov r1, #0x800
ldr r0, [r1]
mov r1, #0x30000000
str r0, [r1]

bl main

重烧编译烧到到Nor中,发现串口不断输出“ABCD……”。

上面是特殊情形的重定位写法(只需重定位1字节长度的数据;并且通过反汇编文件知道是从0x800这个地方复制到0x30000000这个地方),下面是一种通用的重定位写法(不限制重定位的数据长度;也不需要知道从哪个具体地址开始复制、复制到哪个具体的地址 ):

1)链接脚本的修改

SECTIONS {
   .text   0  : { *(.text) }
   .rodata  : { *(.rodata) }
   .data 0x30000000 : AT(0x800) 
   { 
      data_load_addr = LOADADDR(.data);
      data_start = . ;//等于当前位置
      *(.data) //等于数据段的大小
      data_end = . ;//等于当前数据段
   }
   .bss  : { *(.bss) *(.COMMON) }
}

这里的“data_load_addr = LOADADDR(.data);”,从参考文档Using LD,the GNU linker可知,它表示数据段在.bin文件中的地址。

2)start.S文件的修改

重定位代码的过程示意图如下:首先记录下r3这个地址,让它保持不动,作为复制的终点;然后将r1地址的内容[r1]复制到r2地址处,然后通过r1=r1+1和r2=r2+1来更新r1和r2;最后判断更新后的r2地址是否等于r3,如果相等则说明复制完成,不相等则需要继续复制。

bl sdram_init

/* 重定位data段 */
ldr r1, =data_load_addr  /* data段在bin文件中的地址(加载地址)*/
ldr r2, =data_start  /* data段在重定位地址, 运行时的地址 */
ldr r3, =data_end      /* data段结束地址 */

cpy:
ldrb r4, [r1]   //注意这里是ldrb、strb,命令的后缀是b,表示读或写1个字节
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
bne cpy

bl main
(3)测试与验证

为了验证上面的重定位写法可行,这里修改main.c文件,在里面多添加几个全局变量(则需要重定位的.data数据段就变长了,可以区别于只需要重定位g_Char的这种情形) :

#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"

char g_Char = 'A';
char g_Char3 = 'a';
const char g_Char2 = 'B';
int g_A = 0;
int g_B;

int main(void)
{
uart0_init();
while (1)
{
putchar(g_Char);
g_Char++;

putchar(g_Char3);
g_Char3++;
delay(1000000);
}
return 0;
}

如果重定位成功,串口应该输出“AaBbCcDdEeFf……”,编译烧写运行的结果的确如此(最好利用JLink来烧写Nor Flash,因为uboot提供的烧写Nor Flah功能'o'选项好像有问题):

我们来看一下反汇编代码中cpy部分的内容:

三、链接脚本的解析

3.1 链接脚本语法 

更翔实内容,见参考文档Using LD,the GNU linker,或者配套书籍P40。

链接脚本的语法是:

SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  { contents } >region :phdr =fill
...
}

(1)secname:表示段名。

(2)start:表示起始地址。它是运行(时的)地址(runtime addr),也叫重定位(后的)地址(relocate addr)。

(3)AT( ldadr ):表示加载地址(load addr),也就是在.bin文件中的地址。它可有可无,如果没有则表示加载地址等于运行地址。

(4){ contents }:表示这个段存放着什么内容。

  • {  *(.text)  } ,表示这个段存放着所有文件的代码段。
  • {  start.o (.text) },表示这个段存放着start.o文件的代码段。
  • {  start.o (.text)     *(.text)  },表示这个段开始位置存放着start.o文件的代码段,后面存放其他文件的代码段。

这里值得一提的是,如果代码段的内容是 {  *(.text)  },那到底哪个文件的代码段在前面,哪个文件的代码段在后面呢?

这是由Makefile文件中arm-linux-ld命令里.o文件的顺序来决定的:

arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
                          //1     //2   //3    //4    //5

另外经过测试,发现链接脚本里的注释,只能以/* */的形式,不能以 # 或 // 这样的形式。

3.2 加载地址 vs 运行地址

3.2.1 .elf格式的文件

(1)Makefile使用arm-linux-ld命令链接生成一个.elf格式的文件,该文件中含有地址信息(比如说加载地址)。

(2)我们可以使用加载器,把.elf文件加载到内存中。对于裸板来说,JTAG调试工具就是加载器,因为它可以把.elf文件加载到内存中。另外应用程序也可以是加载器,因为通过运行这个应用程序,把.elf文件解析一下,读到内存加载地址处。

(3)最后运行程序。

(4)如果你指定了加载地址,而且加载地址与运行时地址又不相等时,那么程序本身需要包含有重定位代码。

总结下来就是,程序运行时,应该位于运行时地址(或者叫链接地址、重定位地址)。

3.2.2 .bin格式的文件

裸板?

(1)使用arm-linux-objcopy命令,由.elf格式文件生成一个.bin格式的文件,后者没有地址信息。

(2)没有加载器,需要硬件机制来启动(什么意思)。

(3)如果你指定了加载地址,而且加载地址与运行时地址又不相等时,那么程序本身需要包含有重定位代码。

3.3 清零bss段

bin文件和elf文件,都不会保存bss段,见1.3(6)的描述,这可以节约几十个甚至上千个全局变量的存储空间。

那如何保证bss中的内容为0呢?方法是,程序运行时会把bss段对应的空间清零。

为了说明清零bss段的意义,这里做一个实验:以16进制方式,把2.3.4(3)的代码中全局变量g_A打印出来,如下所示:

// 给一个整数,打印成类似于0xABCDEF12这样的形式
void printHex(unsigned int val)
{
int i;
unsigned char arr[8];

/* 先取出每一位的值 */
for (i = 0; i < 8; i++)
{
arr[i] = val & 0xf;
val >>= 4;   /* arr[0] = 2, arr[1] = 1, arr[2] = 0xF */
}

/* 打印 */
puts("0x");
for (i = 7; i >=0; i--)
{
if (arr[i] >= 0 && arr[i] <= 9) //数字0~9
putchar(arr[i] + '0');
else if(arr[i] >= 0xA && arr[i] <= 0xF) //字母A~F
putchar(arr[i] - 0xA + 'A');
}
}
#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"

char g_Char = 'A';
char g_Char3 = 'a';
const char g_Char2 = 'B';
int g_A = 0;
int g_B;

int main(void)
{
uart0_init();

puts("\n\rg_A = ");
printHex(g_A);
puts("\n\r");

while (1)
{
putchar(g_Char);
g_Char++;

putchar(g_Char3);
g_Char3++;
delay(1000000);
}


return 0;
}

编译烧写运行,发现 g_A 并不等于我们预期值0,而是等于莫名奇妙的值,如下所示,这正是因为没有清bss段而导致的问题。

因此我们需要清理bss段。如何清理bss段呢?首先我们需要知道bss段的起始地址和终止地址,为此我们先修改一下链接脚本:

SECTIONS {
   .text   0  : { *(.text) }
   .rodata  : { *(.rodata) }
   .data 0x30000000 : AT(0x800) 
   { 
      data_load_addr = LOADADDR(.data);
      data_start = . ;
      *(.data) 
      data_end = . ;
   }
   
   bss_start = .;
   .bss  : { *(.bss) *(.COMMON) }
   bss_end = .;
}

然后在start.S文件中添加清除bss段的代码:

    bl sdram_init

/* 重定位data段 */
//篇幅缘故,省略这部份代码

/* 清除BSS段 */
ldr r1, =bss_start
ldr r2, =bss_end
mov r3, #0
clean:
strb r3, [r1]  //ldrb r1,[r3]好像也行?
add r1, r1, #1
cmp r1, r2
bne clean

bl main

重新编译烧写运行,可见全局变量g_A的值为0。

四、拷贝代码与链接脚本的改进

4.1 ldrb、strb ==> ldr、str

2.3.4(2)中的拷贝代码如下:使用Idrb命令从的Nor Flash中读取1字节数据,再用strb命令将1字节数据写到SDRAM里面,循环反复。

bl sdram_init

/* 重定位data段 */
ldr r1, =data_load_addr  /* data段在bin文件中的地址(加载地址)*/
ldr r2, =data_start  /* data段在重定位地址, 运行时的地址 */
ldr r3, =data_end      /* data段结束地址 */

cpy:
ldrb r4, [r1]   //注意这里是ldrb、strb,命令的后缀是b,表示读或写1个字节
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
bne cpy

bl main

由前面章节内容可知,JZ2440上的Nor Flash是16位的,SDRAM是32位的。

假设现在需要复制16字节的数据,则一共需要从Nor Flash中读取16字节的数据,写16字节的数据到SDRAM中:

对于读取数据,采用Idrb命令每次只能读取1字节数据,16字节数据则CPU需要发出16次ldrb命令;内存控制器每次收到ldrb命令后就去访问Nor Flash硬件,因此需要访问Nor Flash硬件16次。

对于写入数据,采用strb命令每次只能写入1字节数据,16字节数据则CPU需要发出16次strb命令,内存控制器每次收到strb命令后就去访问SDRAM硬件,因此需要访问SDRAM硬件16次。

这样总共需要访问32次

现在对其进行改进:

改用 Idr 命令从Nor Flash中读:Idr命令每次读取4字节数据,16字节数据则CPU需要发出4次ldr命令。由于Nor Flash是16位的,内存控制器每次收到ldr命令时,需要拆分成两次来访问Nor Flash,因此需要访问Nor Flash硬件8次。(CPU:快给我4字节的数据!内存控制器:NND,Nor那家伙不给力啊,一次只能给我2字节的数据,我只好分两次向它要数据,然后再合成4字节数据呈送给CPU大爷了!)

改用 str 命令写 SDRAM:str命令每次写入4字节数据,16字节数据则CPU需要发出4次str命令;由于SDRAM是32位的,要写入的数据又刚好是4字节32位的,所以内存控制器每次收到str命令后直接向32位的SDRAM写入数据,因此需要访问SDRAM硬件4次。(CPU:写4字节的数据到SDRAM中!内存控制器:SDRAM那家伙刚好有32个入口,可以让4字节数据同时进入!一次就完成了命令,美滋滋啊!)

这样总共需要访问12次。

硬件访问是很耗时的,改进代码后,减少了硬件访问的次数,极大地提高了效率。 

根据上面的描述,我们改进拷贝代码、清除bss段的代码:

/* 重定位data段 */
ldr r1, =data_load_addr  /* data段在bin文件中的地址, 加载地址 */
ldr r2, =data_start  /* data段在重定位地址, 运行时的地址 */
ldr r3, =data_end      /* data段结束地址 */

cpy:                 // 代码改进之处
ldr r4, [r1]     //ldrb —> ldr
str r4, [r2]     //strb —> str
add r1, r1, #4   //#1 —> #4
add r2, r2, #4   //#1 —> #4
cmp r2, r3
ble cpy          //bne —> ble(表示r1≤r2)

/* 清除BSS段 */
ldr r1, =bss_start
ldr r2, =bss_end
mov r3, #0
clean:
str r3, [r1]     //strb —> str
add r1, r1, #4   //#1 —> #4
cmp r1, r2
ble clean        //bne —> ble(表示r1≤r2)

bl main

3.3节的代码经过上述改进后,重新编译烧写运行。发现只输出“g_A = 0x00000000”,没有后续的输出(根据下面的分析,是因为清除bss段时,把全局变量g_Char、g_Char3也清零了。编号0开始的是一些不可见字符,所以这里不见有后续输出,时间久一点应该会输出可见字符)。

4.2 在链接脚本中添加 .ALIGN(4) 

4.2.1 部分数据段内容被清零 

为了找出问题的原因,我们在main.c程序添加 “以16进制输出某字符的ASCII编号” 的代码(#if 1 和 #endif 之间的代码),避免由于不可见字符导致的输出不可见情形。如下所示:

#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"

char g_Char = 'A';
char g_Char3 = 'a';
const char g_Char2 = 'B';
int g_A = 0;
int g_B;

int main(void)
{
        uart0_init();

        puts("\n\rg_A = ");
        printHex(g_A);//以十六进制输出数值
        puts("\n\r");

        while (1) 
        {
#if 1           
                puts("\n\rg_Char = ");
                printHex(g_Char);//输出某个字符的ASCII编号(以16进制形式)
                puts("\n\r");

                puts("\n\rg_Char3 = ");
                printHex(g_Char3);
                puts("\n\r");
#endif
                putchar(g_Char);
                g_Char++;

                putchar(g_Char3);
                g_Char3++;
                delay(1000000);
        }

        return 0;
}

我们预期输出如下(为了直观调了一下间隔):

g_A = 0x00000000

g_Char = 0x00000041   //g_Char = 'A',而'A'的ASCII16进制编号是0x00000041
g_Char3 = 0x00000061  //g_Char3 = 'a',而'a'的ASCII16进制编号是0x00000061
Aa

g_Char = 0x00000042    //g_Char = 'B',而'A'的ASCII16进制编号是0x00000042
g_Char3 = 0x00000062   //g_Char = 'b',而'A'的ASCII16进制编号是0x00000061
Bb

//省略其他

但实际输出如下:g_Char、g_Char3的第一个输出居然是0x00000000、0x00000000,而不是我们期待的0x00000041、0x00000061。至于为什么一开始没有类似Aa、Bb这样的输出,是因为一开始输出的是不可见的字符,自增到0x00000021时才开始有类似的输出。

g_A = 0x00000000

g_Char = 0x00000000  //第一个输出居然是0x00000000
g_Char3 = 0x00000000 //第一个输出居然是0x00000000
                     //这里是不可见字符
g_Char = 0x00000001
g_Char3 = 0x00000001

//省略其他

g_Char = 0x00000021
g_Char3 = 0x00000021
!!  //到这里才有显示

g_Char = 0x00000022
g_Char3 = 0x00000022
""

g_Char = 0x00000023
g_Char3 = 0x00000023
##

//省略其他

按理g_Char、g_Char3的第一个输出应该是0x00000041、0x00000061,这会却是0x00000000、0x00000000。

猜测是全局变量g_Char、g_Char3被破坏了。被谁破坏了?回顾一下,3.3节还是正常的,而将ldrb、strb、#1==> ldr、str、#4之后就出错了。拷贝代码部分应该不会出错,那可能是清除bss段有问题:清除bss段时,把数据段中的g_Char、g_Char3也意外地清除了。

为了测试是否如此,我们先注释掉start.S中的清理命令(清零操作中,实际干活的语句是 str r3,[r1] ):

clean:
        //str r3, [r1]  //注释掉这语句,使得清bss不起作用
        add r1, r1, #4
        cmp r1, r2
        ble clean

然后重新编译烧写运行,可见g_Char、g_Char3显示正确,如下所示: 

这说明:清除bss段时,不仅仅把bss段清零了,也把数据段中的g_Char、g_Char3清零了。

4.2.2 部分数据段被清零的原因

这是怎么回事?我们看一下没注释掉 “str r3,[r1]” 时的反汇编文件:

根据上图,“str r3,[r1]” 其实就是“ str 0,[0x30000002] ”(忽略语法错误,这里仅为了说明)。

str命令是以4字节为单位来存的(4字节对齐),但0x30000002这个地址并不是4字节对齐的。所以对于“str 0,[0x30000002] ”,str会把数值0存放到向4取整的地址,即存放到0x30000000地址处,而非0x30000002地址处。“str 0,[0x30000002] ”等同于“str 0,[0x30000000] ”。

由于0x30000000这个地址是.data数据区的开始位置,而.data数据区存放着g_Char、g_Char3(各占一个字节,分别位于0x30000000、0x30000001),“str 0,[0x30000000] ”意味着把数值0存放到0x30000000~0x30000003,这就把g_Char、g_Char3给清零了。

4.2.3 在链接脚本添加.ALIGN(4)

由上面分析可知,问题出现在.bss段紧挨着.data数据段之后存放。由于.data数据段的结束地址是0x30000001(含),那么.bss段的开始地址就应该是0x30000002。所以反汇编文件中清零语句“str r3,[r1]”中的r1=0x30000002。由于0x30000002不是向4取整的,而 str 命令需要4字节对齐的缘故,“str r3,[0x30000002]”被等同于“str r3,[0x30000000]”。

因此,解决方案就是让.bss段的开始地址向4取整,即让.bss段的开始地址与.data数据段的末地址间隔一些地址空间,而非紧挨着.data数据段,以达到向4取整的目的。

这可以通过在链接脚本中,在.bss段之前添加“. = ALIGN(4);”语句来实现,如下所示:

SECTIONS {
   .text   0  : { *(.text) }
   .rodata  : { *(.rodata) }
   .data 0x30000000 : AT(0x800) 
   {   
      data_load_addr = LOADADDR(.data);
          . = ALIGN(4);
      data_start = . ; 
      *(.data) 
      data_end = . ; 
   }   
   
   . = ALIGN(4);  /*添加这一语句即可*/
   bss_start = .;
   .bss  : { *(.bss) *(.COMMON) }
   bss_end = .;
}

将注释掉的清零语句重新打开、调试语句设为#if 0,重新编译烧写运行,发现解决了4.1末尾的问题:

此时我们看一下反汇编文件,可见此时的.bss段的开始地址已经变为0x30000004:

同样的问题也会出在代码重定位这里,如何保证data段起始地址也是向4对齐?也是使用ALIGN(4)向4取整(不过.data段的运行地址0x30000000,加载地址0x800,都是4字节对齐的,不需要再使用ALIGN(4)向4取整,不放心的话加上也行)。

4.2.4 uboot中的链接脚本与拷贝代码

u-boot是裸机的集大成者,我们可以看见它的链接脚本也是类似的:

其拷贝代码如下(位于/cpu/arm920t/start.S文件中):

五、代码重定位和位置无关码

程序是由代码段、只读数据段、数据段、bss段等组成的。程序一开始烧在Nor Flash上面,运行时代码段可以在Nor Flash运行,但必须把数据段从Nor Flash移到SDRAM中,这是因为在SDRAM中数据段里的变量才能被写操作。把程序从一个位置移动到另一个位置,这个过程就称为重定位。

前面(第二三四节)我们只是重定位了数据段(2.2.2中的方法一),这里我们尝试重定位整个程序(2.2.2中的方法二)。

5.1 位置无关码的含义

把整个程序复制到SDRAM所需要的技术细节:

1)在链接脚本中指定运行地址为SDRAM上的某个地址。

2)bin文件烧写到Flash上面,上电后先从0x0地址开始运行,前面部分的代码需要将整个程序复制到SDRAM中的运行地址。这就要求前面部分的代码(“重定位操作”前的代码,以及“重定位操作”自身的代码)是位置无关码。

这里解释一下位置无关码的含义。比如上面,链接脚本指定程序的运行地址为SDRAM上的某个地址,但是程序烧写在0地址时,前面部分的代码(意思同上)也仍然可以运行起来。这说明这部份代码与具体地址无关,也就是说你把它们放在哪个地址都能运行起来。

5.2 修改链接脚本

我们参考u-boot的链接脚本来修改链接脚本:

/*一体式链接脚本*/
SECTIONS
{
        . = 0x30000000; /*这表明运行之前你应该把程序复制到这个位置*/

        . = ALIGN(4);
        .text      :   
        {
          *(.text)
        }

        . = ALIGN(4);
        .rodata : { *(.rodata) }

        . = ALIGN(4);
        .data : { *(.data) }

        . = ALIGN(4);
        __bss_start = .;
        .bss : { *(.bss) *(.COMMON) } /*COMMON段是没有初值的段?也放在这里吧*/
        _end = .;
}

/*分体式链接脚本*/
/*
SECTIONS {
   .text   0  : { *(.text) }
   .rodata  : { *(.rodata) }
   .data 0x30000000 : AT(0x800) 
   { 
      data_load_addr = LOADADDR(.data);
      data_start = . ;
      *(.data) 
      data_end = . ;
   }
   
   bss_start = .;
   .bss  : { *(.bss) *(.COMMON) }
   bss_end = .;
}
*/

上面的这个链接脚本,我们称之为一体式链接脚本。对比前面的分体式链接脚本(比如3.3节中的链接脚本),两者的区别在于代码段和数据段的存放位置是否是分开的(是否分开很远)。

一体式链接脚本的代码段后面,依次就是只读数据段、数据段、bss段,都是连续在一起的;分体式链接脚本则是代码段、只读数据段,中间相关很远之后才是数据段、bss段。

我们以后的代码一般采用一体式链接脚本,原因如下:

1)分体式链接脚本适合单片机,有些单片机自带 Flash(Nor Flash或者其他可以直接运行代码的Flash),不需要再把 Flash上的整个程序复制到内存中(复制某些段即可,因此使用分体式链接脚本),因为这样会浪费内存。但是我们的嵌入式系统的内存非常大,没必要节省这一点空间,并且有些嵌入式系统没有Nor Flash等可以直接运行代码的Flash,此时就需要从 Nand Flash 或者SD卡复制整个代码到内存(因此需要用到一体式链接脚本)。

2)JTAG等调试器一般只支持一体式链接脚本。

5.3 修改start.S文件

改好链接脚本后,接下来修改启动文件即start.S文件。

5.3.1 实现复制.text、.data、.rodata段

以前我们只复制了.data数据段,现在我们需要复制.text、.data、.rodata段(不需要复制bss段)。

/* 重定位text, rodata, data段整个程序 */
mov r1, #0
ldr r2, =_start     /* 第1条指令运行时的地址 */
ldr r3, =__bss_start    /* bss段的起始地址 */

cpy:
ldr r4, [r1]
str r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2, r3
ble cpy


/* 清除BSS段 */
ldr r1, =__bss_start
ldr r2, =_end
mov r3, #0
clean:
str r3, [r1]
add r1, r1, #4
cmp r1, r2
ble clean

bl main  

以下是对上面代码的一些修改说明:

(1)mov r1, #0 :表示我们从0地址开始复制。当Nor Flash启动时,0地址在Nor Flash上;当Nand Flash启动时,0地址在SRAM上。

(2)ldr r2, =_start :由链接脚本可知运行地址是0x30000000,因此从0地址开始所复制的内容,要存到0x30000000地址处。为了不在程序中写死这个地址,可以改用第一条指令的地址(它其实就是运行地址,因为由链接脚本可知运行地址处存放的是代码段,而代码段存放的就是指令),即_start。

(3)ldr r3, =__bss_start:不需要复制bss段,因此到达bss段的起始地址后就可以停止复制了。

(4)ldr r1, =__bss_start 及 ldr r2, =_end:bss段的起始地址与结束地址。

将修改后的程序重新编译烧写运行,发现和预期的结果一样:

5.3.2 由反汇编文件分析启动过程

这似乎可以证明上面的操作是正确与无遗漏的,但真是如此吗?我们结合.bin文件的反汇编文件,来分析一下程序的启动流程:

注意,其中bl 30000478,并不表示跳转到0x30000478。为什么呢?因为0x30000478这个地址位于SDRAM中,但SDRAM此时还没被初始化,里面还没有指令,所以不可能跳到0x30000478这个地址而自寻死亡的。

为了证明不是跳转到0x30000478,我们修改一下链接脚本,把运行地址改为0x32000000,重新编译后查看反汇编文件。结果此处显示bl 32000478,但机器码完全一致,如下所示。如果bl 30000478表示跳转到0x30000478,bl 32000478表示跳转到0x32000478,那两个要跳转的地址不一样,按理机器码不会相同,这里却相同,因此反汇编文件中的bl xxx并不表示跳转到地址xxx。

那到底跳转到哪里?跳转到“当前PC值 + 某个固定的offset”这个位置。其中offset是由链接器帮忙推算出来的,当前PC值也就是当前指令所在的位置(不过好像要+8,三级流水线,则pc=当前指令位置+8。不过下面为了描述简单,PC=当前指令所在的位置)。

举几个例子说明一下:

1)假设程序从0x0开始运行,那么执行到eb000105这条命令时,这条命令所处的位置是0x5c,则PC值等于当前指令所在的位置=0x5c,执行完eb000105这条命令之后,它会跳到0x00000478

2)假设程序从0x30000000开始执行,那么执行到eb000105这条命令的时候,这条命令所处的位置是0x3000005c,则PC值等于当前指令所在的位置=0x3000005c,执行完eb000105这条命令之后,它会跳到0x30000478

3)假设程序从0x32000000开始执行,执行到eb000105这条命令的时候,这条命令所处的位置是0x3200005c,执行完eb000105这条命令之后,它会跳到0x32000478

4)这里再举一个(开始地址的末尾没那么多0的)例子:假设程序从0x30000004开始执行,那么执行到eb000105这条命令的时候,这条命令所处的位置是0x30000060,则PC值等于当前指令所在的位置=0x30000060,执行完eb000105这条命令之后,它跳到0x3000047c

用一个图来描述这四种情形,如下所示:

反汇编文件里面的“B xxx”或者“BL xxx”语句,只是起到方便查看的作用,并不是真的跳转xxx地址处。比如看到“ bl 30000478 ”,我可以很方便地知道“接下来是要执行反汇编文件中0x30000478处的代码”,但并不是说真要跳到0x30000478这个地址。具体跳到哪个地址,由当前PC值+offset来决定。

或者可以这样理解:反汇编文件中所标示的一切地址,是根据链接脚本来确定的“假地址”。如果整个程序是在链接脚本指定的运行时地址处开始运行的,那么一切符合反汇编文件中的地址关系,“B xxx”就真的是跳转到地址xxx;如果整个程序不是在链接脚本指定的运行时地址处开始运行的,那么“B xxx”就不是跳转到地址xxx(具体跳转到哪里,由当前PC值+offset决定,而当前PC值与程序开始运行时的地址有关),而是表明接下来要执行反汇编文件中所标示的xxx地址处的代码。

从上面几个例子可知,虽然我们的程序指定运行时地址为0x30000000,但是我们把它放在0地址也可以运行,这是为什么?因为它进行跳转的时候,使用的是偏移地址(以当前PC为起点的偏移量,即上图中的offset),因此它仍然可以跳到正确的代码位置,跟指定的运行时地址没有关系(上图中无论开始地址是哪个,offset的大小都是一样的,只是PC值不一样而已)。这也告诉我们,如果以后需要写与运行时地址无关的代码时,可以使用B或者BL指令。

5.3.3 bl main —>ldr pc,=main

start.S文件末尾有一条语句“bl main”,我们分析一下反汇编文件,看是否需要修改它。

可见执行“bl main”时,是跳转到0x05c4(虽然上图显示是0x300005c4,但那是程序开始运行时地址为0x30000000时所对应的跳转地址。现在程序开始运行时地址为0,那么真正跳转到的应该是0x05c4这个地址)。也就是说,执行 “bl main” 语句后会跳转到 Nor Flash 中的main函数,而不是跳转到被重定位到SDRAM中的main函数,如下所示:

那如何跳转到SDRAM中的main函数呢?我们必须使用绝对地址,如下所示:

//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr pc, =main  /* 绝对跳转(使用绝对地址跳转), 跳到SDRAM */

修改之后重新编译烧写运行,发现与5.3.1的显示结果一样,而且输出速度更快一些:

这不难理解,毕竟经过“ldr pc, =main”修改后程序是在SDRAM中运行的,而使用“bl main”则程序是在Nor Flash中运行。

我们看一下修改之后的反汇编文件:

5.4 怎么写位置无关码

所谓位置无关码,即那部分代码放在哪里都可以运行,无需放在指定的运行时地址。

那怎么写位置无关码呢?一句话总结,就是不使用绝对地址。如何知道有没有使用到绝对地址?最根本的方法是看反汇编文件。

1)使用相对跳转指令,即B或BL指令。

2)重定位之前,不可以使用绝对地址,因此不可以访问全局变量、静态变量(因为访问全局变量时,会使用运行时地址来访问,也就是绝对地址),不可以访问有初始值的数组(因为其初始值保存在rodata或data段中,需要使用绝对地址来访问)。

3)重定位之后,可以使用ldr pc,=xxx 跳转到运行时地址。

5.5 sdram_init函数中的位置无关码

(1)下面是 sdram_init 函数的代码,可见没有使用到全局变量、静态变量,它是位置无关的。

void sdram_init(void)
{
BWSCON = 0x22000000;

BANKCON6 = 0x18001;
BANKCON7 = 0x18001;

REFRESH  = 0x8404f5;

BANKSIZE = 0xb1;

MRSRB6   = 0x20;
MRSRB7   = 0x20;
}

(2)下面换一种写法sdram_init2,然后在start.S文件中将“bl sdram_init”改为“bl sdram_init2” 。重新编译烧写运行,发现没有任何输出。

void sdram_init2(void)
{
unsigned int arr[] = {
0x22000000, //BWSCON
0x00000700, //BANKCON0
0x00000700, //BANKCON1
0x00000700, //BANKCON2
0x00000700, //BANKCON3
0x00000700, //BANKCON4
0x00000700, //BANKCON5
0x18001, //BANKCON6
0x18001, //BANKCON7
0x8404f5, //REFRESH,HCLK=12MHz:0x008e07a3,HCLK=100MHz:0x008e04f4
 0xb1,//BANKSIZE
 0x20,//MRSRB6
 0x20,//MRSRB7

};
volatile unsigned int * p = (volatile unsigned int *)0x48000000;
int i;

for (i = 0; i < 13; i++)
{
*p = arr[i];
p++;
}

}

我们查看一下反汇编代码:

ldmia ip!,{r0,r1,r2,r3},大致意思是把ip这个地址(0x30000708,是一个内存地址)开始的内容,读到r0~r3寄存器中。 

由下图可知,数组arr的初始值存储在.rodata段。这些.rodata段中的数据,需要使用绝对地址来访问。所以要写位置无关码的话,代码中不能出现带有初始化值的数组(有初始化值则保存在.rodata段,访问它们需要使用到绝对地址,而位置无关码不能出现绝对地址)。

六、重定位、清除bss段的C函数实现

前面使用汇编程序实现了重定位和清bss段,本节我们将使用C语言,实现重定位和清除bss段。

6.1 汇编文件给C函数传入参数

1、在start.S文件中,删除原来的汇编代码,改为调用C函数:原来的:

//**************************************************原来的************/
/* 重定位text, rodata, data段整个程序 */
mov r1, #0
ldr r2, =_start     /* 第1条指令运行时的地址 */
ldr r3, =__bss_start    /* bss段的起始地址 */
cpy:
ldr r4, [r1]
str r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2, r3
ble cpy


/* 清除BSS段 */
ldr r1, =__bss_start
ldr r2, =_end
mov r3, #0
clean:
str r3, [r1]
add r1, r1, #4
cmp r1, r2
ble clean

//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr pc, =main  /* 绝对跳转, 跳到SDRAM */


//**************************************************修改后************/
    //bl sdram_init2 /* 用到有初始值的数组, 不是位置无关码 */
bl sdram_init

/* 重定位text, rodata, data段整个程序 */
mov r0, #0
ldr r1, =_start     /* 第1条指令运行时的地址 */
ldr r2, =__bss_start    /* bss段的起始地址 */
sub r2, r2, r1
    //r0,r1,r2传参
bl copy2sdram  /* src, dest, len */

/* 清除BSS段 */
ldr r0, =__bss_start
ldr r1, =_end
    //r0,r1传参
bl clean_bss  /* start, end */

//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr pc, =main  /* 绝对跳转, 跳到SDRAM */

bl sdram_init
//bl sdram_init2 /* 用到有初始值的数组, 不是位置无关码 */

2、在init.c中文,实现copy2sdram、clen_bss函数:

void copy2sdram(volatile unsigned int *src, volatile unsigned int *dest, \
                                                     unsigned int len)  
{
unsigned int i = 0;

while (i < len)
{
*dest++ = *src++;
i += 4;
}
}

void clean_bss(volatile unsigned int *start, volatile unsigned int *end)
{
while (start <= end)
{
*start++ = 0;
        /*
            *start=0;
             start++;
        */
}
}

重新编译烧写运行(烧写到Nor Flash或者Nand Flash中都可以),发现输出正常:

6.2 C函数从链接脚本获得参数

如果不想汇编传入参数,而是C函数从链接脚本中获得参数,我们可以这样做:

1、修改start.S文件,跳转到C函数时不需要准备任何参数:

bl sdram_init
//bl sdram_init2 /* 用到有初始值的数组, 不是位置无关码 */

/* 重定位text, rodata, data段整个程序 */
bl copy2sdram

/* 清除BSS段 */
bl clean_bss

//bl main  /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr pc, =main  /* 绝对跳转, 跳到SDRAM */

2、修改链接脚本,让__code_start等于当前地址(这里是0x30000000):

SECTIONS
{
. = 0x30000000;

__code_start = .; /*添加这么一行*/

. = ALIGN(4);
.text      :
{
  *(.text)
}

. = ALIGN(4);
.rodata : { *(.rodata) }

. = ALIGN(4);
.data : { *(.data) }

. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}

3、修改init.c中的copy2sdram、clen_bss函数:

void copy2sdram(void)
{
/* 要从lds文件中获得 __code_start, __bss_start
 * 然后从0地址把数据复制到__code_start
 */

extern int __code_start, __bss_start;

volatile unsigned int *dest = (volatile unsigned int *)&__code_start;
volatile unsigned int *end = (volatile unsigned int *)&__bss_start;
volatile unsigned int *src = (volatile unsigned int *)0;

while (dest < end)
{
*dest++ = *src++;
}
}

void clean_bss(void)
{
/* 要从lds文件中获得 __bss_start, _end
 */
extern int _end, __bss_start;

volatile unsigned int *start = (volatile unsigned int *)&__bss_start;
volatile unsigned int *end = (volatile unsigned int *)&_end;


while (start <= end)
{
*start++ = 0;
}
}

重新编译烧写运行(烧写到Nor Flash或者Nand Flash中都可以),发现输出正常:

 

我们看一下反汇编文件,可见的确是从地址0x30000000处开始存放复制的数据。

 

 

6.3 C函数如何使用链接脚本中的变量

这里说明一下,C函数如何使用链接脚本中的变量。

1)在C函数中,将该变量声明为外部变量类型,比如上面的“ extern int _end, __bss_start;”。

2)使用链接脚本中的变量时要取址,也就是在该变量前加“&”符号,比如上面的代码:

volatile unsigned int *dest = (volatile unsigned int *)&__code_start;

觉不觉得奇怪?由链接脚本可知 __code_start 的值已经是0x30000000了,按理直接把它强制类型转换为 (volatile unsigned int *) 类型,也就是写成(volatile unsigned int *)__code_start,然后赋值给dest指针即可,不需要用到取地址符“&”。那这里为什么加“&”呢?另外又注意到,汇编文件中却可以直接使用链接脚本中的变量,根本不需要加取地址符“&”,这是为什么呢?

这里解释一下原因(参见Nick Clifton - Re: linker script symbols to c source variables):

C程序中如果定义了一个全局变量“int g_i;”,则必然为它分配4字节的储存空间。假设链接脚本中有成几百万个变量(a1,a2 …),如果C程序全部为它们分配空间,那样就太占据空间了,因此C程序是不会为链接脚本中的变量分配存储空间的。那万一需要使用到链接脚本中的某些变量,如何确定这些变量的值呢?原来,在编译时会生成一个符号表“symbol table”,它保存了C程序中的变量和它们的地址,以及链接脚本中的常量和它们的值(常量的值在链接时确定),这个符号表的内容大致如下所示:

如何使用symbol table符号表?对于常规变量g_i,其名字下面一格的数据表示这个变量的地址,需要使用&g_i来得到;对于链接脚本中的常量a1,其名字下面一格的数据表示这个常量的值,为了保持代码的一致,也是使用&a1得到它。

这只是编译器的一个技巧问题或者规定,没必要深究,使用时记住以下三点即可:

1)链接脚本中定义再多的变量也不影响C程序的体积。

2)借助符号表来保存链接脚本中的变量,使用时要加上"&"以得到变量的值。

3)在C程序中用到链接脚本中的变量时,要将该变量声明为外部变量,任何类型都可以(即extern int、extern char等等都行)。

extern int __code_start, __bss_start;
    //extern char __code_start, __bss_start;
    //extern usigned char __code_start, __bss_start;
   
volatile unsigned int *dest = (volatile unsigned int *)&__code_start;
volatile unsigned int *end = (volatile unsigned int *)&__bss_start;
volatile unsigned int *src = (volatile unsigned int *)0;


原文地址:https://blog.csdn.net/oqqHuTu12345678/article/details/142629837

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!