05:2440----代码重定义
目录
一:引入
1:基本概念
2:NAND启动
3:NOR启动
4:变量
5:实验证明
A:代码+makefile
B:NOR启动
C:NAND启动
D:内存空间
二:链接脚本
1:NOR
2:NAND
3:解决方法
A:尝试解决
B:方法一解决
A:简单实现
B:通用方法
C:方法二解决
D:链接脚本解释
三:bbs文件解决
四:拷贝代码和链接脚本的改进
五:代码重定位与位置无关码
A:代码重定义
B:位置无关码
六:C语言实现
一:引入
1:基本概念
CPU发出的指令通过内存控制器可以直接到达内存类设备(SRAM, SDARM,NOR FLASH)。但是不能直接到达NAND FLASH
2440给我们配置了4K的SRAM和基地址为0x3000 0000的SDRAM。还有其他的许多外设。
具体的我们参考我04:2440---内存控制器所写的内容
2:NAND启动
由于CPU不能直接访问我们的NAND FLASH, 所以在NAND FLASH启动的时候,2440的硬件会把NAND前4K的内容复制到SRAM中去。 CPU直接访问SRAM,CPU从SRAM0地址取出命令执行,这样做达到了NAND FLASH启动读取写入数据的目的。
NAND启动的时候NOR不可访问。SRAM的基地址为0。
当bin文件超过4K的时候======》因为我们配置的2440SRAM只有4K,所以这个时候我们不能在复制到SRAM中去了(SRAM内存不够了)。我们使用代码重定义,把他复制到SDRAM中去,(CPU也可以直接访问内存类设备SDARM)。
3:NOR启动
NOR的基地址为0,但是SRAM的基地址为0x4000 0000(NAND FLASH复制到SRAM的内存)
CPU读取出NOR第一个指令(前4个字节)执行,CPU继续在读取出其他的指令在执行;一边读取一边执行
NOR可以向内存一样读,但是不能向内存一样写。(如果非要写,必须发出一定的格式)
4:变量
通常来说,我们经常说的写入是指写入数据段,而不是代码段。
局部变量放在栈中------指向SRAM可读可写
全局变量放在bin文件里面,写在NOR上面,直接写入无效,NOR不能写
为什么全局变量无法直接写入NOR FLASH中
全局变量不能直接写入NOR FLASH中的原因主要有以下几点:
- 存储机制不同:全局变量通常存储在RAM(随机存取存储器)中,而NOR FLASH是按照块或页进行存储的,其读取和写入操作需要按照特定的时序和命令进行。
- 写入命令不同:NOR FLASH使用特殊的命令集进行擦除和编程操作,而全局变量的写入操作不符合这些命令规范。
- 数据格式不同:全局变量的数据格式可能与NOR FLASH的存储格式不兼容,导致无法正确写入。
- 存储空间限制:NOR FLASH的存储空间通常是有限的,直接写入全局变量可能会超出其容量限制。
为了将全局变量写入NOR FLASH中,您需要使用特定的FLASH API或库函数,这些函数封装了正确的NOR FLASH擦除和编程命令,并确保数据格式正确。此外,还需要遵循特定的编程时序和数据传输方式。
NOR FLASH不能在全局变量所处的数据段(data)中直接写入
NOR FLASH不能直接写的意思是它不支持直接在已有的数据上进行修改。NOR FLASH是一种非易失性存储器,类似于常规的NOR门电路,具有直接读取和执行代码的能力。然而,它不支持随机写入,即不能直接在已有的数据上写入新的数据。如果要写入新的数据,必须先擦除整个块或页,然后再进行写入。因此,在使用NOR FLASH时,需要考虑到它的这种特性,并正确地处理数据的写入和擦除操作
我们经常说的写入是写入数据段还是代码段
通常来说,我们经常说的写入是指写入数据段,而不是代码段。
数据段是用来存储程序中使用的变量、数组、结构和其他数据结构的内存区域,在程序运行期间需要频繁地被写入和读取。因此,当我们说“写入”时,通常是指向数据段的写入操作。
而代码段是用来存储程序代码的内存区域,通常是只读的,不需要频繁地被写入。在程序运行期间,代码段只需要被读取,不需要进行写入操作。
当然,具体的情况可能会因不同的系统和程序而有所不同。例如,在某些嵌入式系统中,代码段也可能需要进行修改和更新,这时候就需要进行写入操作。因此,具体情况还需要根据具体的系统和程序来确定。
5:实验证明
A:代码+makefile
#include "uart.h"
#include "sc2440_so.h"
#include "NOR.h"
#include "led.h"
char g_Char = 'A';
const char g_Char2 = 'B';
int g_A = 0;
int g_B;
void delay(volatile int d)
{
while (d--);
}
int main(void)
{
UART_init();
puts("The UART function is normal:\n\r");
while (1)
{
putchar(g_Char);
g_Char++;
delay(1000000);
}
return 0;
}
发现问题---bin文件出奇的大,我们查看反汇编文件
代码段的位置0
发现数据段位置有些不太好, 我们修改makefile文件改变他data段的地址
all:
arm-linux-gcc -c -o uart.o uart.c
arm-linux-gcc -c -o main.o main.c
arm-linux-gcc -c -o start.o start.S
arm-linux-gcc -c -o led.o led.c
arm-linux-gcc -c -o NOR.o NOR.c
arm-linux-gcc -c -o uart.o uart.c
arm-linux-ld -Ttext 0 -Tdata 0x800 start.o led.o uart.o NOR.o main.o -o uart.elf
arm-linux-objcopy -O binary -S uart.elf uart.bin
arm-linux-objdump -D uart.elf > uart.dis
clean:
rm *.bin *.o *.elf *.dis
修改代码段起始的地址,使他起始地址为0x800---这样不会时bin文件太大
bin文件的大小为: 0x81
B:NOR启动
从这里我们可以看到全局变量在NOR无法写入,只能读
C:NAND启动
我们可以看到些在nand上面的数据可以正常的写入。
D:内存空间
可以看到初始值为0的全局变量,无初始值的全局变量和注释的地址为0x804和0x808,而我们的bin文件的大小只有0x801。所以这两个并不在我们的bin文件里面
二:链接脚本
1:NOR
一上电,我们的CPU从零开始取代码来执行,它要操作到gchar g_Char的时候,读完全没问题,可以读到这个a加加的时候,他要把这个值加加后的值写到。800这里去,他写不进去,所以说再次读出来的时候仍然是原来的,所以说如果我们的程序从诺启动的话,它会一直输出
为了解决我们全局变量无法直接写入NOR中去。我们可以将g-char代码重定义到SDARM中去。提供2种解决方法
解决方法:
2:NAND
当我们实验NAND FLASH启动的时候,NOR不可见。NAND中的4K内存被复制到SARM中去,CPU直接访问我们的SARM。
3:解决方法
A:尝试解决
makefile
all:
arm-linux-gcc -c -o uart.o uart.c
arm-linux-gcc -c -o main.o main.c
arm-linux-gcc -c -o start.o start.S
arm-linux-gcc -c -o led.o led.c
arm-linux-gcc -c -o NOR.o NOR.c
arm-linux-gcc -c -o uart.o uart.c
arm-linux-ld -Ttext 0 -Tdata 0x30000000 start.o led.o uart.o NOR.o main.o -o uart.elf
arm-linux-objcopy -O binary -S uart.elf uart.bin
arm-linux-objdump -D uart.elf > uart.dis
clean:
rm *.bin *.o *.elf *.dis
我们修改makefile文件使我们的数据段开始地址为-----0x3000 0000。直接代码重定义指向我们的SDARM,这样我们就可以修改全局变量
发现我们的bin文件巨大,数据段也确实是从0x3000 0000开始的
问题的原因:
代码段和数据段中间出现的巨大的黑洞。我们解决的方法在上面以及写了。现在我们来实施
使用代码重定义----解决NOR数据段(data)无法写入的问题。
B:方法一解决
A:简单实现
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 NOR.o NOR.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 NOR.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
使用链接脚本设置我们,代码段和数据段的起始地址
* `-T sdram.lds`: 指定链接脚本为`sdram.lds`。链接脚本定义了如何组织和布局各个段(section)在输出文件中的位置。
SDRAM.lds
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x700) { *(.data) }
.bss : { *(.bss) *(.COMMON) }
}
.text 0 : { *(.text) }
: 这一行告诉链接器将所有的.text
section 放置在地址 0。这是非常不常见的,因为通常程序会从更高的地址开始执行。这个地址可能适用于某些特殊的硬件或者模拟器。.rodata : { *(.rodata) }
: 这一行将所有的.rodata
section 放置在同一地址。这是只读数据段,通常包含不会在程序执行期间改变的数据。.data 0x30000000 : AT(0x700) { *(.data) }
: 这一行稍微复杂一些。它将所有的.data
section 放置在地址0x30000000
,但是在链接过程中,这些 section 的虚拟地址将被设置为0x700
。这意味着程序在运行时将使用虚拟地址0x700
来访问这些数据,但是在内部,它们仍将在物理地址0x30000000
。这种做法通常用于实现内存保护和隔离。.bss : { *(.bss) *(.COMMON) }
: 这一行将所有的.bss
section 和.COMMON
section 放置在同一地址。.bss
section 通常包含程序运行期间会被初始化为零的变量。.COMMON
是一个不太常见的 section 类型,通常用于在多个目标文件之间共享未初始化数据。请补充缺失的
END
标签,并确保这个链接脚本与你的目标文件和编译器兼容。如果你有任何其他问题或需要进一步的解释,请随时提问。
strat.S
bl sdram_init
/* 重定位data段 */
mov r1, #0x700
ldr r0, [r1]
mov r1, #0x30000000
str r0, [r1]
bl main
上面的这种方法,只能复制0x700处的一位数据,不太通用,下面写一个更加通用的复制方法:在b中我们使用同用的方法来解决
B:通用方法
makefile文件不改变
SDRAM.lds
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800)
{
data_load_addr = LOADADDR(.data);
data_start = . ;
*(.data)
data_end = . ;
}
.bss : { *(.bss) *(.COMMON) }
}
这段代码看起来像是链接脚本的一部分,通常在嵌入式系统或操作系统内核的编译过程中使用。这个脚本定义了几个段(sections)在内存中的位置。让我们逐行地解析它:
SECTIONS {
: 开始定义链接脚本的sections部分。.text 0 : { *(.text) }
: 这一行定义了.text
段,该段通常包含程序的代码。0
是该段在内存中的初始地址,{ *(.text) }
表示链接器应将所有的.text
段链接到该地址。.rodata : { *(.rodata) }
: 这一行定义了.rodata
段,该段通常包含程序中不可修改的数据,如字符串常量。链接器会将所有的.rodata
段链接到该地址。.data 0x30000000 : AT(0x800) { ... }
: 这一行定义了.data
段,该段通常包含初始化的全局变量。0x30000000
是该段在内存中的初始地址,AT(0x800)
指定了该段在内存中的最终地址(重定位地址),然后的大括号内部定义了数据加载的地址和起始/结束标记。data_load_addr = LOADADDR(.data);
: 这行代码将.data
段的加载地址存储在变量data_load_addr
中。data_start = .;
: 这行代码将当前位置标记为.data
段的开始。*(.data);
: 这行代码告诉链接器将所有的.data
段链接到之前定义的地址(.data
段的开始和结束已经在之前定义)。data_end = .;
: 这行代码将当前位置标记为.data
段的结束。.bss : { *(.bss) *(.COMMON) }
: 这一行定义了.bss
段,该段通常包含未初始化的全局变量。大括号内的内容告诉链接器将所有的.bss
段和.COMMON
段链接到该地址。}
: 结束定义sections部分。这个链接脚本的主要目的是告诉链接器如何组织和定位代码和数据在内存中。
strat.S
bl SDARMA_init
/* 重定位data段 */
ldr r1, =data_load_addr /* data段在bin文件中的地址, 加载地址 */
ldr r2, =data_start /* data段在重定位地址, 运行时的地址 */
ldr r3, =data_end /* data段结束地址 */
cpy:
ldrb r4, [r1]
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
bne cpy
C:方法二解决
见下面的代码重定位与无关码
D:链接脚本解释
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
secname :段名
start :起始地址:运行时的地址(runtime addr);重定位地址(relocate addr)
AT ( ldadr ) :可有可无(load addr:加载地址) 不写时LoadAddr = runtime addr
{ contents } 的内容:
start.o //内容为start.o文件
*(.text)所有的代码段文件
start.o *(.text)文件
① 运行地址<—>链接地址:他们两个是等价的,只是两种不同的说法。
运行地址:程序在SRAM、SDRAM中执行时的地址。就是执行这条指令时,PC应该等于这个地址,换句话说,PC等于这个地址时,这条指令应该保存在这个地址内。
② 加载地址<—>存储地址:他们两个是等价的,也是两种不同的说法。
加载地址:程序保存在Nand flash中的地址。
三:bbs文件解决
我们发现了一个问题在SDRAM.lds文件中 bbs(初始化为0的全局变量或者没有给值得全局变量)如果太多了怎么办?我们把bbs在得区域直接设置为0就ok了。
bbs不在bin文件中。
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 NOR.o NOR.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 NOR.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
sdram.bin
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
bl SDARMA_init
/* 重定位data段 */
ldr r1, =data_load_addr /* data段在bin文件中的地址, 加载地址 */
ldr r2, =data_start /* data段在重定位地址, 运行时的地址 */
ldr r3, =data_end /* data段结束地址 */
cpy:
ldrb r4, [r1]
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
bne cpy
/* 清除BSS段 */
ldr r1, =bss_start
ldr r2, =bss_end
mov r3, #0
clean:
strb r3, [r1]
add r1, r1, #1
cmp r1, r2
bne clean
bl main
四:拷贝代码和链接脚本的改进
我们在汇编文件(start.S )中,使用了下面的代码导硬件的访问效率低下,一次只能访问一个字节。
ldrb r4, [r1]
strb r4, [r2]
读: 我们知道这SDRAM是32位的,那么你读SDRAM写SDRAM的时候,一次性操作只能够以32位作为最小单位来访问。这个CPU想去ldrb读一个字节,它会把这个命令发给这内存控制器。这个内存控制器从里面读到四个字节的数据,然后挑出CPU。
写:CPU把这个地址和数据发给内存控制器。这个内存控制器把32位的数据发给SDARM。同时,这个内存控制器还会发出数据屏蔽信号dqm DQ mdq mdq mdq m比如说我这个CPU只想写一个字节,那么它会发出三条dqm数据屏蔽信号。屏蔽掉不需要写的其他三个字节,这样最终只会写SDRAM里面的一个字节。
改进方法:
使用ldr 和 str一次读取4个字节
读取:假设读取16字节的数据,需要我们执行4次(1byte=8bit,一次读取4个字节4*4=16),NOR为16位,每访问一次=读取2个字节。访问硬件次数=16byte/2byte=8次
写入:原理一样
所以在这个里面我们只需要访问硬件12次,节约的效率
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 NOR.o NOR.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 NOR.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
sdram.lds
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 = .;
}
注意 使其按4字节边界对齐---- . = ALIGN(4); 这个代码要写否则可能在汇编中,把bbs段清理位0的时候,也会把data段设置为0。
strat.S
bl SDARMA_init
/* 重定位data段 */
ldr r1, =data_load_addr /* data段在bin文件中的地址, 加载地址 */
ldr r2, =data_start /* data段在重定位地址, 运行时的地址 */
ldr r3, =data_end /* data段结束地址 */
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, =bss_end
mov r3, #0
clean:
str r3, [r1]
add r1, r1, #4
cmp r1, r2
ble clean
bl main
五:代码重定位与位置无关码
A:代码重定义
makefile文件不变
这个也就是方法2
一个程序,由代码段、只读数据段、数据段、bss段等组成。
程序一开始可以烧在Nor Flash上面,运行时代码段仍可以在Nor Flash运行,但对于数据段,就必须把数据段移到SDRAM中,因为只要在SDRAM里面,数据段的变量才能被写操作(CPU无法直接访问NOR FLASH,NOR 不是内存类设备),把程序从一个位置移动到另一个位置,把这个过程就称为重定位。
前面的例子,我们只是重定位了数据段,这里我们再尝试重定位整个代码。
先梳理下把整个程序复制到SDRAM需要哪些技术细节:
1. 把程序从Flash复制到运行地址,链接脚本中就要指定运行地址为SDRAM地址;
2. 编译链接生成的bin文件,需要在SDRAM地址上运行,但上电后却必须先在0地址运行,这就要求重定位之前的代码与位置无关(是位置无关码)
SDRAM.lds
SECTIONS
{
. = 0x30000000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
SECTIONS
: 这是在告诉链接器接下来的部分定义了各个段的位置。段的定义是链接器脚本中的基本构造,它们决定了程序在内存中的布局。{
: 开始定义段。. = 0x30000000;
: 这行设置了当前地址(或者说“dot”)为0x30000000。这是内存地址的一个例子,你的程序将从这个地址开始存放数据。. = ALIGN(4);
: 这会使地址以4字节为单位对齐。也就是说,如果你的地址不是4的倍数,这行代码会移动地址使其成为4的倍数。.text :
:定义一个名为.text
的段,这个段主要用于存放程序的代码部分。{
: 开始定义.text
段的内容。*(.text)
: 这会包含所有.text
段的内容,也就是说,所有的代码段都会被合并在一起。}
: 结束定义.text
段的内容。. = ALIGN(4);
: 同上,这会使地址以4字节为单位对齐。.rodata : { *(.rodata) }
: 定义一个名为.rodata
的只读数据段,用于存放程序中不可修改的数据。. = ALIGN(4);
: 同上,这会使地址以4字节为单位对齐。.data : { *(.data) }
: 定义一个名为.data
的可读写数据段,用于存放程序中可修改的数据。. = ALIGN(4);
: 同上,这会使地址以4字节为单位对齐。__bss_start = .;
: 定义一个名为__bss_start
的符号,并将其设置为当前的地址。这个符号通常用于初始化未初始化的全局变量。.bss : { *(.bss) *(.COMMON) }
: 定义一个名为.bss
的未初始化数据段,用于存放程序中未初始化的全局变量。_end = .;
: 定义一个名为_end
的符号,并将其设置为当前的地址。这个符号通常用于表示程序的结束地址。}
: 结束前述指令和整个段的定义。
我们写的这个链接脚本,称为一体式链接脚本,对比前面的分体式链接脚本区别在于代码段和数据段的存放位置是否是分开的。
例如现在的一体式链接脚本的代码段后面依次就是只读数据段、数据段、bss段,都是连续在一起的。
分体式链接脚本则是代码段、只读数据段,中间相关很远之后才是数据段、bss段。
我们以后的代码更多的采用一体式链接脚本,原因如下:
1. 分体式链接脚本适合单片机,单片机自带有flash,不需要再将代码复制到内存占用空间。而我们的嵌入式系统内存非常大,没必要节省这点空间,并且有些嵌入式系统没有Nor Flash等可以直接运行代码的Flash,就需要从Nand Flash或者SD卡复制整个代码到内存;
2. JTAG等调试器一般只支持一体式链接脚本
start.S
/* 重定位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
halt:
b halt
梳理下把整个程序复制到SDRAM需要哪些技术细节:
1. 把程序从Flash复制到运行地址,链接脚本中就要指定运行地址为SDRAM地址;
2. 编译链接生成的bin文件,需要在SDRAM地址上运行,但上电后却必须先在0地址运行,这就要求重定位之前的代码与位置无关(是位置无关码)----------在NOR FLASH的test段的代码,负责把整个程序放在SDARM里面。所以必须先从0地址开始也必须写的是位置无关码
在生成的bin文件里,代码保存的位置是0x30000000。随后烧写到NOR Flash的0地址,但代码的结构没有变化。之后再重定位到SDRAM。
B:位置无关码
概念的补充
位置无关码(Position Independent Code,PIC)是一种在计算机程序中实现不依赖于特定内存地址的指令代码的技术。它通过使用相对寻址和间接寻址的方式,使得程序可以在内存的任意位置运行,而不需要改变代码本身。
相对寻址是指使用相对于当前指令的地址偏移量来访问内存中的数据或指令。间接寻址是指使用指针或引用来访问内存中的数据或指令。
位置无关码的主要优点是提高了代码的可移植性和灵活性。由于程序可以在任意内存位置运行,因此可以将代码加载到不同的地址空间中,而无需修改代码。这使得程序可以更容易地在不同的操作系统和硬件平台上运行,同时也方便了代码的共享和重用。此外,位置无关码还能提供一定程度的安全性,因为它使得恶意代码更难以利用特定的内存布局进行攻击。
在现代计算机体系结构中,位置无关码已经成为编程的标准实践,并得到了广泛应用。许多编程语言和编译器都提供了对位置无关码的支持,例如C语言中的函数指针就可以用于实现位置无关码。另外,操作系统和动态链接器也提供了加载和执行位置无关码的功能。
反汇编:
3000005c: eb000106 bl 30000478 <sdram_init>
30000060: e3a01000 mov r1, #0 ; 0x0
30000064: e59f204c ldr r2, [pc, #76] ; 300000b8 <.text+0xb8>
30000068: e59f304c ldr r3, [pc, #76] ; 300000bc <.text+0xbc>
这里的bl 30000478不是跳转到30000478,这个时候sdram并未初始化;
为了验证,我们做另一个实验,修改连接脚本sdram.lds, 链接地址改为0x32000478,编译,查看反汇编
3000005c: eb000106 bl 30000478 <sdram_init>
30000060: e3a01000 mov r1, #0 ; 0x0
30000064: e59f204c ldr r2, [pc, #76] ; 300000b8 <.text+0xb8>
30000068: e59f304c ldr r3, [pc, #76] ; 300000bc <.text+0xbc>
可以看到现在变成了bl 30000478,但两个的机器码eb000106都是一样的,机器码一样,执行的内容肯定都是一样的。
因此这里并不是跳转到显示的地址,而是跳转到: pc + offset,这个由链接器决定。(PC(Program Counter)是计算机的指令计数器,它记录了当前要执行的指令的地址。)
假设程序从0x30000000执行,当前指令地址:0x3000005c ,那么就是跳到0x30000478;如果程序从0运行,当前指令地址:0x5c 调到:0x00000478
跳转到某个地址并不是由bl指令所决定,而是由当前pc值决定。反汇编显示这个值只是为了方便读代码。
重点:
反汇编文件里, B或BL 某个值,只是起到方便查看的作用,并不是真的跳转。
写位置无关码
//bl main /*bl相对跳转,程序仍在NOR/sram执行*/
ldr pc, =main /*绝对跳转,跳到SDRAM*/
- 相对跳转:相对跳转指令使用的是相对地址,它基于当前位置进行跳转。例如,“bl main”就是一种相对跳转指令,它会将程序计数器(pc)的值增加main函数在内存中的地址与当前位置的相对偏移量,从而实现跳转。这种跳转方式在程序仍在NOR/SRAM执行时使用。
- 绝对跳转:绝对跳转指令使用的是绝对地址,它直接将程序计数器(pc)设置为目标地址,从而使程序跳转到该地址处执行。例如,“ldr pc, =main”就是一种绝对跳转指令,它会将pc设置为main函数在SDRAM中的地址,从而实现跳转。这种跳转方式在需要跳转到SDRAM执行时使用。
六:C语言实现
我们使用c语言来实现代码重定位和清除bbs段
bl sdram_init
//bl sdram_init2 /* 用到有初始值的数组, 不是位置无关码 */
/* 重定位text, rodata, data段整个程序 */
bl copy2sdram
/* 清除BSS段 */
bl clean_bss
//bl main /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
注意这三个函数,要在main函数前调用。所以只能在汇编语言里面调用,不能在c语言中的main函数调用;否则会导致程序出现无法预计的错误。
sdarm.lds
SECTIONS
{
. = 0x30000000;
__code_start = .;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
c语言实现
/* /* 重定位text, rodata, data段整个程序 */
void copy2sdram(void)
{
/*
__code_start:代码段的开始;
__bss_start:代码段的结束,即bbs段的开始
功能:把0地址的数据复制到 __code_start:代码段的开始
*/
extern int __code_start, __bss_start;/*声明外部变量---sdram.lds文件中的*/
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;
src++;
dest++;
}
}
void clean_bss(void)
{
/*
功能:把bbs段的数据清零;
(没有被定义的全局变量和为0的全局变量)
*/
extern int _end, __bss_start;
volatile unsigned int* dest = (volatile unsigned int*)&__bss_start;
volatile unsigned int* end = (volatile unsigned int*)&_end;
while (dest <= end)
{
*dest++ = 0;
}
}