注:以下内容學習于韋東山老師arm裸機第一期視訊教程
一.段的概念和重定位的引入
1.1 重定位的引入
2440架構圖如下
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISM0kzMwYDN4ETOwYDM4EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
CPU發出的位址可以直接到達SDRAM,SRAM,NOR但是無法直接到達NAND
是以我們的程式可以直接放在NOR,SDRAM直接運作,假設我們把程式燒錄到NAND中,CPU無法直接從NAND取位址運作.
1.1.1 但是我們仍然可以設定為NAND啟動,NAND啟動時(SRAM的位址對應0,是以NAND啟動時NOR無法通路):
前4K會被複制到SRAM,然後CPU從0位址運作,就對應SRAM
如果bin檔案超過4K怎麼辦?
前面的4K需要将整個代碼讀出來放到SDRAM(這就是重定位)
1.1.2 NOR啟動時,0位址對應NOR上面,SRAM的位址對應0x40000000
NOR可以向記憶體一樣的讀,但是不能像記憶體一樣的寫=>需要發出特定的指令才可以寫.
mov r0, #0
ldr r1, [r0] /* 可以讀 */
str r1, [r0] /* 無效 */
是以引入問題,當程式中含有需要寫的變量(局部變量在棧中,棧指向SRAM,讀寫沒有問題)
但是全局變量,靜态變量是包含在bin檔案中燒到NOR中,直接寫修改變量無效
是以,我們需要将這些全局變量,靜态變量重定位放在SDRAM中
示例碼如下:
#include "my_printf.h"
#include "uart.h"
#include "sdram.h"
#include "SetTacc.h"
char g_cA = 'A';
int main()
{
char c;
Uart0Init();
SdramInit();
puts("");
while (1)
{
putchar(g_cA);
g_cA++; /* nor啟動時代碼無效 */
}
return 0;
}
分别燒寫到NOR和NAND,燒些到NAND會列印出ABCD,NOR啟動隻會列印出AAA
NAND啟動需要重定位-> 将全部代碼重定位到SDRAM中
NOR啟動需要重定位-> 将全局變量,靜态變量重定位到SRAM中
1.2 段的概念
程式至少包含代碼段和資料段,資料段中存放(全局變量)
在main.c中定義下面幾個變量
char g_cA = 'A';
const g_cB = 'B';
int g_iA = 0;
int g_iB;
編譯後檢視反彙編檔案,如下圖
是以可以看出程式還包含bss段,rodata段,最後的common段表示注釋段
總結,代碼中的段:
代碼段 .text
資料段 .data
隻讀資料段 .rodata
bss段 .bss,未初始化或初始化為0的全局變量
注釋段 .common
二.連結腳本的引入與簡單測試
2.1 修改Makefile使得全局變量存放到SDRAM,0x3000000中
修改Makefile
objs = uart.o main.o start.o SetTacc.o my_printf.o lib1funcs.o sdram.o
A = test
all:$(objs)
arm-linux-ld -Ttext -Tdata 0x30000000 0 $^ -o $(A).elf
arm-linux-objcopy -O binary -S $(A).elf $(A).bin
arm-linux-objdump -D $(A).elf > $(A).dis
%.o:%.c
arm-linux-gcc -c -o [email protected] $<
%.o:%.S
arm-linux-gcc -c -o [email protected] $<
clean:
rm *.o *.elf *.bin
但是這麼編譯出來的bin檔案有800多M,這是因為代碼段和資料段之間有一個很大的間隔.
2.2 兩種解決辦法:
2.2.1
a.bin檔案中,讓全局變量和代碼段在一起,連結在0位址
b.燒寫bin檔案在NOR的0位址
c.運作時前面的代碼将全局變量複制到0x30000000處
2.2.2
a.讓代碼段的連結位址在0x30000000開始,全局變量在緊接着後面開始
b.燒寫bin檔案在NOR的0位址
c.運作時前面的代碼将代碼段和全局變量全部複制到0x30000000
2.3 連結腳本
修改Makefile,在連結時指定連結腳本
objs = uart.o main.o start.o SetTacc.o my_printf.o lib1funcs.o sdram.o
A = test
all:$(objs)
#arm-linux-ld -Ttext 0 -Tdata 0x800 $^ -o $(A).elf
arm-linux-ld -T sdram.lds $^ -o $(A).elf
arm-linux-objcopy -O binary -S $(A).elf $(A).bin
arm-linux-objdump -D $(A).elf > $(A).dis
%.o:%.c
arm-linux-gcc -c -o [email protected] $<
%.o:%.S
arm-linux-gcc -c -o [email protected] $<
clean:
rm *.o *.elf *.bin
/* 連結腳本如下 */
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x1000) { *(.data) }
.bss : { *(.bss) *(.COMMON) }
}
這樣代碼段會放在0位址,在0x800的地方放了全局變量,但是main函數中通路全局變量時是以0x30000000的位址來通路的
我們并沒有設定0x30000000的記憶體數值是A,我們的代碼中缺少了重定位.
修改代碼Start.S,在執行main函數之前需要進行重定位data段,在前面還要初始化sdram
/* 重定位了1個位元組,0x1000是我們看反彙編确定的位址,并不通用 */
mov r1, #0x1000
ldr r0, [r1]
mov r1, #0x30000000
str r0, [r1]
/* 我們需要得到通用的重定位的辦法 */
修改sdram.lds如下
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x1000)
{
data_load_addr = LOADADDR(.data)
data_start = .;
*(.data)
data_end = .;
}
.bss : { *(.bss) *(.COMMON) }
}
修改Start.S重定位代碼
ldr r1, =data_load_addr /* data段在bin檔案中的位址,加載位址 */
ldr r2, =data_start /* 重定位位址,運作時的位址 */
ldr r3, =data_end /* data段的結束位址 */
str r0, [r1]
cpy:
ldrb r4, [r1]
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
bne cpy
三.連結腳本的解析
連結腳本的格式
SECTIONS {
secname(段的名字,可以随便寫) start->(起始位址,運作時的位址,重定位後的位址) AT(ldadr)->(可以寫,可以不寫,加載位址,如果不寫加載位址等于重定位位址)
{contents}
}
contents,内容:
格式:a. start.o(指定整個檔案)
b. *(.text)
c. start.o *(.text) (start.o放在最前面,然後是 剩下所有檔案的text段)
對于elf格式檔案
1.連結得到elf格式的檔案,裡面含有位址資訊(例如加載位址)
2.使用加載器(對于裸闆就是JTAG調試工具,對于應用程式加載器本身是一個應用程式)把elf檔案解析,讀入記憶體(讀到加載位址)
3.運作程式
4.如果loadaddr不等于加載位址,程式本身需要重定位代碼
以上面的例子為例,data段的運作位址在0x30000000,在取值是就回去0x300000000取值,但是指定了資料段在0x1000,是以需要将資料段拷貝到0x30000000去
核心: 程式運作時的位址應位于運作時位址(重定位後的位址)(連結位址)
對于bin檔案
1.elf->bin
2.硬體機制啟動
3.如果bin檔案所在位置不等于運作時位址程式本身實作重定位
解析連結腳本
SECTIONS {
.text 0 : { *(.text) } /* 加載位址等于運作位址,所有檔案的代碼段排在前面,按照Makefile中檔案的順序來排,也可以在連結腳本中指定誰派在前面 */
.rodata : { *(.rodata) } /* 所有檔案的隻讀資料段 */
.data 0x30000000 : AT(0x1000) /* 加載位址等于0x1000,運作位址等于0x30000000,會導緻data段在bin檔案中處于0x10000位置 */
{ /* 我們需要将資料段複制到0x30000000的位置,由前面的代碼段來拷貝 */
data_load_addr = LOADADDR(.data)
data_start = .;
*(.data)
data_end = .;
}
.bss : { *(.bss) *(.COMMON) } /* bss段緊接着data段排放,放在0x3xxxxxxx,bin檔案中不存放bss段 */
} /* 程式運作時把bss段的資料清0 */
首先将被設定為0的全局變量的值列印出來,發現數值是一個亂碼,是以需要把bss段的資料清0,修改start.S與連結腳本
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x1000)
{
data_load_addr = LOADADDR(.data)
data_start = .;
*(.data)
data_end = .;
}
.bss_start = .;
.bss : { *(.bss) *(.COMMON) }
.bss_end = .;
}
/* Start.S清除bss段 */
ldr r1, = bss_start
ldr r2, = bss_end
mov r2, #0
clean:
strb r2, [r1]
add r1, r1, #1
cmp r1, r2
bne clean
四.拷貝代碼與連結腳本的改進
4.1 拷貝代碼的拷貝(将資料段拷貝代SDRAM)
ldr r1, =data_load_addr /* data段在bin檔案中的位址,加載位址 */
ldr r2, =data_start /* 重定位位址,運作時的位址 */
ldr r3, =data_end /* data段的結束位址 */
str r0, [r1]
cpy:
ldrb r4, [r1] /* 每次讀取一個位元組,但是我們的SDRAM是32位的,NOR是16位的,這樣做效率會很低 */
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
bne cpy
在上面拷貝的時候,我們每次從源位址讀取一個位元組,寫入一個位元組,但是我們的SDRAM是32位的,NOR是16位的,這樣做效率很低
ldrb從NOR中得到資料,strb來寫SDRAM,假設複制16位元組,ldrb需要執行16次通路NOR16次,strb執行16次,通路SDRAM16次,共32次
當CPU要讀一個位元組的時候,将位址發給記憶體控制器,記憶體控制器讀SDRAM會讀出4個位元組,從中挑出需要的1個位元組傳回給CPU
當CPU要寫一個位元組的時候,将位址和資料發送給記憶體控制器,記憶體控制器将32位的資料發送給SDRAM,同時會向SDRAM發送資料屏蔽信号(三條)DQM,會屏蔽掉不需要寫的三個位元組
改進方法:使用ldr 從NOR中讀,使用str 寫入SDRAM中
ldr從NOR中讀,假設複制16位元組,執行4次,通路8次,記憶體控制器會通路兩次NOR,因為一次隻能夠得到兩個位元組
str寫SDRAM,執行4次,通路硬體4次,一次可以寫入4個位元組
修改代碼如下
ldr r1, =data_load_addr /* data段在bin檔案中的位址,加載位址 */
ldr r2, =data_start /* 重定位位址,運作時的位址 */
ldr r3, =data_end /* data段的結束位址 */
str r0, [r1]
cpy:
ldr r4, [r1] /* 每次讀取一個位元組,但是我們的SDRAM是32位的,NOR是16位的,這樣做效率會很低 */
str r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2, r3
ble cpy
/* Start.S清除bss段 */
ldr r1, = bss_start
ldr r2, = bss_end /* 檢視反彙編,r1 = 0x30000002 r2 = 0x3000000c */
mov r2, #0
clean:
str r2, [r1] /* 清0的時候以4位元組清0,但是0x30000002并不是4位元組對齊 */
/* 會将0存放到0x30000000處,str 0, [0x30000000],會破壞别的資料 */
add r1, r1, #4
cmp r1, r2
ble clean
4.2 連結腳本的改進
修改連結腳本使bss段向4取整
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x1000)
{
data_load_addr = LOADADDR(.data)
. = ALIGN(4); /* 向4取整 */
data_start = .;
*(.data)
data_end = .;
}
. = ALIGN(4); /* 向4取整 */
.bss_start = .;
.bss : { *(.bss) *(.COMMON) }
.bss_end = .;
}
五.代碼重定位與位置無關碼
5.1 将整個程式的代碼段和資料段重定位
5.1.1 在連結腳本中指定runtime addr指定為SDRAM的位址
5.1.2 前面的代碼需要将整個代碼拷貝到SDRAM中
5.1.3 程式應該位于0x30000000位址,但是剛開始位于0位址,仍然可以運作,是以重定位之前的代碼與位置無關,即用位置無關碼寫成
5.1.4 修改連結腳本如下:
SECTIONS {
. = 0x30000000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata :
{
*(.rodata)
}
. = ALIGN(4);
.data :
{
*(.data)
}
. = ALIGN(4);
bss_start = .;
.bss :
{
*(.bss) *(.COMMON)
}
bss_end = .;
}
一般都是使用上面這種代碼段和資料段放在一起的連結腳本
5.1.5 修改Start.S對資料段的重定位為對代碼段,資料段,rodata段整個程式重定位
/* 重定位text, data, rodata段 */
mov r1, #0
ldr r2, =_start
ldr r3, =bss_start
cpy:
ldr r4, [r1]
str r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2, r3
ble cpy
5.2 分析啟動代碼
5.2.1 在重定位代碼之前,需要調用sdram_init函數來初始化sdram,對應的反彙編如下圖
bl 30000c40 <SdramInit>
這一句的意思并不是調到0x30000c40,這時候sdram沒有初始化,跳過去執行肯定GG
修改連結腳本中的. = 0x30000000修改為 .=0x32000000
再次編譯檢視反彙編檔案如下圖
兩次跳轉的機器完全一緻,這并不是調到0x30000c40位址,而是跳到目前PC值+offset(連結器算出來的)
具體跳到哪裡由目前PC值決定
假設程式從0x30000000執行,目前指令的位址320001ec,那麼程式就會跳到0x30000c40執行
如果程式從0執行,目前指令的位址是0x1ec,那麼程式就會跳到0xc40
如果程式從0x32000000執行,目前指令的位址是0x320001ec,那麼程式就會跳到0x32000c40
注意:
5.2.1 反彙編檔案裡的b/bl xxxx隻是友善檢視的作用,不是調到這個位址
5.2.2 跳到哪裡由PC值+offset決定
5.2.2 注意到在調用main函數時使用bl main,檢視反彙編
30000230: ebffffc2 bl 30000140 <main>
由于是bl跳轉指令,程式從0開始執行,是以會跳到0x230位址,但是之前已經将代碼拷貝到SDRAM中去了,拷貝到0x30000230
是以這時不對的,我們想讓程式調到SDRAM的話必須使用絕對跳轉如下
ldr PC, =main
反彙編如下
30000230: e59ff01c ldr pc, [pc, #28] ; 30000254 <.text+0x254>
5.3 怎麼寫位置無關碼-> 不使用絕對位址,看反彙編有沒有用到絕對位址
5.3.1 使用相對跳轉指令(b/bl)
5.3.2 重定位之前不可使用絕對位址,不可以通路全局變量,靜态變量
5.3.3 重定位之後,使用ldr pc, =xxx來跳轉,跳轉到runtime addr
5.3.4 不可通路有初始值數組,因為初始值會存放在rodata或者data中,使用絕對位址來通路具體見下面的例子
5.4 sdram_init函數的寫法
5.4.1 在sdram_init函數中,直接對寄存器進行指派,沒有通路任何全局變量,靜态變量
5.4.2 修改sdram_init函數如下:
void SdramInit()
{
unsigned int arr[] = {
0x22000000,
0x00000700,
0x00000700,
0x00000700,
0x00000700,
0x00000700,
0x00000700,
0x18001,
0x18001,
0x8404f5,
0xb1,
0x20,
0x20,
};
volatile unsigned int *p = (volatile unsigned int*)0x48000000;
int i;
for (i = 0; i < 13; i++)
{
*p = array[i];
p++;
}
}
這個函數編譯燒寫後并沒有任何反應,表示這個函數并不是位置無關的
反彙編代碼如下
30000c40 <SdramInit>:
30000c40: e1a0c00d mov ip, sp
30000c44: e92dd800 stmdb sp!, {fp, ip, lr, pc}
30000c48: e24cb004 sub fp, ip, #4 ; 0x4
30000c4c: e24dd03c sub sp, sp, #60 ; 0x3c
30000c50: e59f3088 ldr r3, [pc, #136] ; 30000ce0 <.text+0xce0>
/* 讀30000ce0的值,依賴于PC值,如果是0位址運作就是賭0xce0處的值 */
/* 讀到30000eb4, r3 = 0x30000eb4 */
30000c54: e24be040 sub lr, fp, #64 ; 0x40
30000c58: e1a0c003 mov ip, r3
/* ip = r3 = 0x30000eb4 */
30000c5c: e8bc000f ldmia ip!, {r0, r1, r2, r3}
/* 讀0x30000eb4的資料,加載到r0,r1,r2,r3上去,但是sdram沒有初始化,程式會死掉 */
/* 0x30000eb4存放了寄存器的初始值 */
/* 初始值儲存在rodata裡面,用初始值來初始化數組,而數組儲存在棧裡面 */
30000c60: e8ae000f stmia lr!, {r0, r1, r2, r3}
30000c64: e8bc000f ldmia ip!, {r0, r1, r2, r3}
30000c68: e8ae000f stmia lr!, {r0, r1, r2, r3}
30000c6c: e8bc000f ldmia ip!, {r0, r1, r2, r3}
30000c70: e8ae000f stmia lr!, {r0, r1, r2, r3}
30000c74: e59c3000 ldr r3, [ip]
30000c78: e58e3000 str r3, [lr]
30000c7c: e3a03312 mov r3, #1207959552 ; 0x48000000
30000c80: e50b3044 str r3, [fp, #-68]
30000c84: e3a03000 mov r3, #0 ; 0x0
30000c88: e50b3048 str r3, [fp, #-72]
30000c8c: e51b3048 ldr r3, [fp, #-72]
30000c90: e353000c cmp r3, #12 ; 0xc
30000c94: ca00000f bgt 30000cd8 <SdramInit+0x98>
30000c98: e51b1044 ldr r1, [fp, #-68]
30000c9c: e51b3048 ldr r3, [fp, #-72]
30000ca0: e3e02033 mvn r2, #51 ; 0x33
30000ca4: e1a03103 mov r3, r3, lsl #2
30000ca8: e24b000c sub r0, fp, #12 ; 0xc
30000cac: e0833000 add r3, r3, r0
30000cb0: e0833002 add r3, r3, r2
30000cb4: e5933000 ldr r3, [r3]
30000cb8: e5813000 str r3, [r1]
30000cbc: e51b3044 ldr r3, [fp, #-68]
30000cc0: e2833004 add r3, r3, #4 ; 0x4
30000cc4: e50b3044 str r3, [fp, #-68]
30000cc8: e51b3048 ldr r3, [fp, #-72]
30000ccc: e2833001 add r3, r3, #1 ; 0x1
30000cd0: e50b3048 str r3, [fp, #-72]
30000cd4: eaffffec b 30000c8c <SdramInit+0x4c>
30000cd8: e24bd00c sub sp, fp, #12 ; 0xc
30000cdc: e89da800 ldmia sp, {fp, sp, pc}
30000ce0: 30000eb4 strcch r0, [r0], -r4
六.重定位_清除BSS段的C函數實作
6.1 彙編傳遞參數
mov r0, #0
ldr r1, =_start
ldr r2, =bss_start
sub r2, r2, r1
bl Copy2Sdram
void Copy2Sdram(volatile unsigned int *src, volatile unsigned int *dest, unsigned int len)
{
unsigned int i = 0;
while (i < len)
{
*dest++ = *src++;
i += 4;
}
}
ldr r0, =bss_start
ldr r1, =bss_end
bl CleanBss
void CleanBss(volatile unsigned int *start, volatile unsigned int *end)
{
while (start < end)
{
*start++ = 0;
}
}
6.2 C語言直接獲得位址參數
void Copy2Sdram(void)
{
extern unsigned int code_addr, bss_start;
volatile unsigned int *dest = (volatile unsigned int *)&code_addr;
volatile unsigned int *src = (volatile unsigned int *)0;
volatile unsigned int *end = (volatile unsigned int *)&bss_start;
while (dest < end)
{
*dest++ = *src++;
}
}
void CleanBss(void)
{
extern unsigned int bss_start, bss_end;
volatile unsigned int *start = (volatile unsigned int *)&bss_start;
volatile unsigned int *end = (volatile unsigned int *)&bss_end;
while (start < end)
{
*start++ = 0;
}
}
C函數如何使用lds檔案中的變量abc(彙編檔案中可以直接使用)?
a.在C函數中生命該變量未extern類型,比如extern int abc;
b.使用時,要取址,比如
int *p = &abc; //p的值即為lds檔案中abc的值
6.3 C函數中使用連結腳本變量需要生命,彙編檔案中可以直接使用的原因:
C函數中聲明某個變量,必須聲明,例如int g_i,那麼程式必然有4位元組儲存這個變量
假設lds檔案中有100W個,a1,a2....等變量,C程式完全沒有必要存儲這些變量
編譯程式時有一個symbol tabel(符号表),儲存着變量的名字和位址
對于連結腳本的變量也使用符号表,儲存着lds變量(準确來說是常量,裡面的值是固定的)的名字和值(注意,不是位址)
對于普通變量,使用&g_i 得到addr,為了保持代碼的一緻,對于lds中的a1,使用&a1得到裡面的值
符号表裡面的值在連結時确定,符号表不會存放到程式中去
結論:
6.3.1 C程式中不儲存lds檔案中的變量
6.3.2 借助符号表來儲存lds中常量的值,使用時加上"&"得到它的值