JZ2440裸板開發練習#6 鏈接腳本與重定位

上一節完成了SDRAM的控制,現在我們可以對SDRAM進行讀寫操作了,因此用SDRAM做一下代碼重定位的內容。一些內容已經在https://blog.csdn.net/G_METHOD/article/details/104508545中提及,如重定位是什麼,爲何要進行重定位,還有鏈接腳本中使用到的地址的區別,因此此處會簡單一些描述。

當我們的程序不能在原本的目標地址執行的時候,就需要進行代碼重定位,而代碼重定位做的就是把程序從當前不能正常運行的地址處搬到運行時地址達到程序正常運行的目的。

重定位一般包含段複製以及bss段清除,bss段是全局變量中無初始值或者初始值爲0的變量合集,作爲bss段而非data段存在可以節省bin文件大小。而爲重定位做導航的是鏈接腳本,如源地址,目標地址等都可以由鏈接腳本給出,同時鏈接腳本還能做內存映射安排,隨心所欲安排段的位置以及內存分佈的相對位置,留下自己所以希望的空洞,鏈接腳本具體內容可以細緻讀一讀http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html#IDX237,可以更加清晰的認識和使用鏈接腳本,這裏精簡地說一下最核心的部分。

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

這是我們要用到的鏈接腳本最核心的地方,SECTIONS爲固定格式,一個鏈接腳本必須有且只能至多一個,SECTION內我們可以自由安排段的內存映射。secname是段名,一般有.data,.text,.rodata,.bss等,必須爲鏈接器認識的段名字。start處填寫我們期望程序的運行時地址,如果未指定,則按順序以及前面的段長度直接給定。BLOCK(align)只我們可設置該段的起始地址處於對齊地址處,從而保證程序的絕對正常運行,這裏的對齊不是唯一的方式後文將提及另外一種方式。NOLOAD表示該段處於可執行文件中,讀取elf可以查看到該段,但是程序運行時不加載入內存中,由於我們是裸板程序,存儲位置處即加載地址,因此沒有說不加載這一概念,個人認爲這裏只針對elf等應用層加載。需要注意的是 :AT ,AT前面的 : 兩側是必須要有空格的。AT(ldadr)指示程序的加載地址,在bin文件中,這一特性可以利用來控制bin的大小,如text和data以及bss緊挨着排列放置,但是重定位時各自到各自的運行時地址,如果沒有指定AT(ldadr)這一地址,加載地址默認與運行時地址start一致。contents是我們自由安排相同段內內容順序的地方。後面的region和phdr以及fill由於未使用這裏就不多贅述,在上述第二個網站鏈接的文檔中有細緻描述。

以JZ2440爲例輸出如下鏈接腳本,SDRAM的起始地址在0x30000000,將其作爲起始地址,這裏用到了 . 符號,. 符號表示當前位置的地址,僅在SECTIONS內可用,作爲右值使用則將當前地址賦值給變量,如下面的鏈接腳本中的__copy_start = . 。當作爲左值使用時,用於指定當前地址,需要注意的是相關賦值操作都需要加上 ;符號結尾。ALGIN(4) 表示地址按照4字節對齊,由於arm以4字節爲基準步進取值解碼執行,當地址不爲4字節對齊時,強制進行對齊,如果我們不進行對齊,則可能出現數據段在0x3002處,此時需要對該數據區域寫,直接進行對齊變成對地址0x3000執行賦值操作,此時如果0x3000地址存在有效的代碼時,將會覆蓋,從而導致程序運行異常,因此保險起見,應該對每個段進行對齊。

linker.lds
-------------------------
SECTIONS{
	. = 0x30000000;
	. = ALIGN(4);
	
	_copy_start = .;
	.text : { *(.text) }
	
	. = ALIGN(4);
	.rodata :{ *(.rodata) }
	
	. = ALIGN(4);
	.data : { *(.data) }
	
	. = ALIGN(4);
	_copy_end = .; 
	
	_bss_start = . ;
	.bss : {
		*(.bss)
		*(.COMMON)
	}
	_bss_end = .;
}

鏈接腳本的contents部分描述我們將什麼內容放置在段中,通配符 * 表示所有的文件,這裏的文件是指執行 ld -T linker.lds xxx.o yyy.o 指定的目標文件,各個目標文件在內存中的位置和順序與ld執行時指定目標文件的順序有關,如果按照上述方式排列,則順序爲xxx.o 再yyy.o,當然也可以在contents中指定目標文件順序,比如我們需要將Start.S這一啓動文件放置在其他文件之前,可以這樣

.text : { Start.o(.text) *(.text) }

中間使用空格分隔開。

這裏示例程序的代碼重定位涉及到段複製和bss段清除,因此在對應段的位置定義相應的變量以供重定位程序使用。需要再說的是在SECTIONS中定義的變量,在elf中是以符號表中沒有值的符號存在的,拓展一下,全局變量和函數都會在符號表中存儲,符號表中對應符號存在對應地址,該地址指向的區域是變量的初始值,函數的情況較爲複雜這裏不做拓展,而鏈接腳本的變量在這裏存儲,但是也有相應的地址,但是沒有值,相應的,地址就是鏈接腳本變量的值,因此我們使用的時候應該將其當做一個外部符號來使用,但是僅僅對其取地址,如下,使用32位指針類型指向該地址進行相應操作可以較快完成該階段(硬件特性和軟件層面的實現決定)。

extern uint32_t _bss_start;
uint32_t *target_start_address = &_bss_start;

關於符號表和鏈接腳本變量及C語言中如何使用鏈接腳本變量的詳細知識可以查看https://sourceware.org/ml/binutils/2007-07/msg00154.html,基於上述知識,輸出代碼如下:

Start.S
----------------
.text
.global _start

_start:
	MOV R0,#0
	LDR R1,[R0]
	
	STR R0,[R0]
	LDR R2,[R0]
	
	CMP R2,R0
	LDR SP,=0x40000000+4096
	MOVEQ SP,#4096
	STREQ R1,[R0]
	
	bl HardwareInitAll          //相對跳轉
	ldr pc,=main                //絕對跳轉,到SDRAM上運行
halt:
    b halt
s3c2440.c
------------------------
#include "s3c2440.h"

....

static void CopySegmentForReloacte(void)
{
	extern uint32_t _copy_start,_copy_end;
	uint32_t *source_start = 0;
	uint32_t *target_start = &_copy_start;
	uint32_t *target_end = &_copy_end;

	while(target_start <= target_end)
	{
		*target_start++ = *source_start++;
	}
}

static void CleanBssSegment(void)
{
	extern uint32_t _bss_start,_bss_end;
	uint32_t *target_start_address = &_bss_start;
	uint32_t *target_end = &_bss_end;

	while(target_start_address <= target_end)
	{
		*target_start_address++ = 0;
	}
}


void HardwareInitAll(void)
{
	WatchDogDisable();
	ClockDevideConfig();
	ChangeModeToAsynchronous();
	MPLLConfig();
	MemoryControllerInit();
	CopySegmentForReloacte();
	CleanBssSegment();
}

main.c
------
#include <stdint.h>

#include "s3c2440.h"
#include "led.h"
#include "uart.h"

char test_char = 'A';

int main()
{
	UartInit();

	while(1)
	{
		putc(test_char);
		if(++test_char > 'Z') { test_char = 'A'; }
		Delay(1000);
	}
	return 0;
}

makefile中,使用 -T選項指定鏈接腳本,如下

al:
        arm-linux-gcc -o Start.o -c Start.S
        arm-linux-gcc -o main.o -c main.c
        arm-linux-gcc -o s3c2440.o -c s3c2440.c
        arm-linux-gcc -o uart.o -c uart.c
        arm-linux-gcc -o led.o -c led.c
        arm-linux-gcc -o key.o -c key.c
        arm-linux-ld -T linker.lds -o led.elf Start.o s3c2440.o led.o key.o uart.o main.o
        arm-linux-objcopy -O binary -S led.elf led.bin
        arm-linux-objdump -D led.elf > led.dis

clean:
        rm *.dis *.o *.bin *.elf

編譯後燒寫到NorFlash中,如果串口正常依次輸出A-Z即重定位成功。

Start.S中bl相對跳轉到初始化函數中進行初始化和重定位,因爲未完成重定位前運行時地址沒有可執行的有效指令數據,智能通過相對跳轉執行NorFlash上的程序,當重定位程序把運行時地址即SDRAM上的數據都準備好後,採用ldr絕對跳轉正式跳轉到SDRAM上運行。由此可以總結出,重定位代碼前禁止使用位置相關代碼,只能使用位置無關碼進行程序的執行。

此處留空,以後補充位置無關碼的判別。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章