第三節 連結器ld
ld軟體的作用是把各種目標檔案(.o檔案)和庫檔案連結在一起,並定位元據和函數地址,最終產生執行程式。ld軟體識別一種用連結命令語言(Linker Command Language)表示的連結描述(Linker Script)檔案來顯式地控制連結的過程。通過BFD(Binary Format Description)庫,ld可以讀取和操作COFF、ELF、a.out等各種執行檔案格式的目標檔案。ld的一個簡單使用例子如下:
$ld -o hello /lib/crt0.o hello.o -lc
它的意思是把crt0.o、hello.o和庫檔案libc連結起來產生可執行程式hello。這裡的crt0.o是應用程式編譯串連時需要的開機檔案,在程式串連階段被連結,主要工作是初始化應用程式棧,初始化程式的運行環境和在程式退出時清除和釋放資源。libc庫中包含了C程式中常用的函數實現,如printf、gets等。
gcc可以間接地調用ld,這主要是通過給gcc傳遞一個參數-Wl(是“ld”的“l”,而不是“123”的“1”)來完成的,放到Wl後面的參數gcc不會處理,而是交給ld進行處理。例如:
$gcc -Wl,--startgroup foo.o bar.o -Wl--endgroup
等同於執行如下的ld命令:
$ld --startgroup foo.o bar.o --endgroup
ld具有許多選項,執行如下命令:
$ld --help
就可以列出ld常用的選項(下面的選項列表中,“,”表示“或”的意思):
連結命令語言(Linker Scripts)表示的連結描述檔案顯式地控制了ld的連結過程。ld命令選項-T FILE, --script FILE指定了連結描述檔案名稱。連結描述檔案描述了各個輸入檔案的各節如何映射到輸出檔案的各節中,並控制輸出檔案中各個節或符號的記憶體布局。如果不指定連結描述檔案,則ld會使用一個預設的描述檔案來產生執行檔案。
目標檔案是由多個節(Section)構成的,如data節一般儲存了有初值的全域變數,text節儲存了執行代碼,bss節儲存了無初值的全域變數。目標檔案中的每節有名字和大小,而且節可以標識為loadable,這表示這個節中的內容可以載入到記憶體中,如果節不是loadable或allocatable,則這種節可能包含調試資訊。通過如下命令可以看到執行程式test的各個節的資訊:
$objdump -h test
每個有loadable或allocatable標識的輸出節有兩種地址,一種地址是VMA(Virtual Memory Address),這種地址是輸出檔案運行時節的運行地址;另一種地址是LMA(Load Memory Address),這種地址是載入輸出檔案時節的載入地址。一般情況下,這兩種地址是相同的,但在嵌入式系統中,經常存在運行地址和載入地址不一致的情況,例如把輸出檔案載入到開發板的Flash儲存空間中(地址由LMA指定),但運行時,要把Flash儲存空間中的輸出檔案複製到SDRAM中運行(地址由VMA指定)。
每個目標檔案有許多符號,每個符號有一個名字和一個地址,一個符號可以是定義的(Defined)或未定義的(Undefined)。一個符號可以是函數名、全域變數、靜態變數等。可通過nm命令或objdump -t命令來查看目標檔案或執行檔案中符號資訊。
連結描述檔案是一個文字檔,它主要由一系列的命令組成,每個命令可以是一個帶參數的關鍵字或指派陳述式。各個命令通過分號分隔。“/*”和“*/”之間的字元是注釋。
連結描述檔案的命令主要包括如下幾類:
設定進入點(Entry Point)的命令(可執行程式的第一條執行指令稱為進入點)
處理檔案的命令
處理檔案格式的命令
其它命令
下面就一些常用命令進行介紹。
ENTRY(Symbol):設定symbol的值為可執行程式的進入點。ld有多種方法設定可執行程式的進入點,它按如下的順序來確定程式的進入點:
1) ld的命令列選項-e指定的值
2) ld的連結描述檔案中的ENTRY(symbol)指定的值
3) .text節的起始地址
4) 進入點為0
INCLUDE filename:包含其他名為filename的連結描述檔案
INPUT(file, file, …):指定多個輸入檔案名稱
GROUP(file, file, …):需要重複搜尋符號定義的多個輸入檔案名稱
OUTPUT(filename):指定輸出檔案名
SEARCH_DIR(path):指定輸入檔案搜尋路徑
STARTUP(filename):指定第一個連結的輸入檔案名稱
OUTPUT_FORMAT(bfdname):指定輸出檔案的BFD格式
TARGET(bfdname):指定輸入檔案的BFD格式
簡單指派陳述式(或稱為符號賦值命令):
symbol = expression;
symbol += expression;
symbol -= expression;
symbol *= expression;
symbol /= expression;
symbol <<= expression;
symbol >>= expression;
symbol &= expression;
symbol |= expression;
PROVIDE(symbol=expression):定義了一個變數symbol,且它的值為expression,這個變數可以被輸入檔案引用。
MEMORY命令:MEMORY命令在用於嵌入式系統的連結描述檔案中經常出現,它描述了各個記憶體塊的起始地址和大小,它的格式如下:
MEMORY
{
name[(attr)]:ORIGIN = origin,LENGTH = len
…
}
name可以理解為描述的記憶體塊名字(region)。len表示這個記憶體塊的大小。attr表示記憶體塊的屬性,它的值可以是:
R:唯讀
W:可讀/寫
X:可執行
A:可分配
I:用於初始化的記憶體,初始化後,可以回收為空白閑記憶體
!:表示非,和上述各個值配合使用。
SECTIONS命令告訴ld如何把輸入檔案的各個輸出節映射到輸出檔案的各個輸入節中,SECTIONS命令的格式如下:
SECTIONS
{
sections-command
sections-command
…
}
每個SECTIONS相關的命令可以是如下命令:
ENTRY命令
符號賦值(Symbol Assignment)命令
輸出檔案節描述(Output Section Description)
覆蓋描述(Overlay Description)
輸出檔案節描述的格式如下:
section [address][(type)] : [AT(lma)]
{
output-section-command
output-secton-command
…
}[>region][AT>lma_region][:phdr :phdr …][=fillexp]
上述“[”和“]”之間的屬性是可選的,一般可以不用。常用屬性介紹如下:
1、address:指定輸出節的起始地址
2、type:每個輸出節可以有一個屬性type,屬性的格式如下:
(type_keyword)
type_keyword可以是如下值:
NOLOAD:表示這個節在輸出檔案運行時是不載入到記憶體中的。(Not loadable)
DESCT、COPY、INFO、OVERLAY:這幾個值的意思都是一樣的,都表示這個節在輸出檔案運行時是不分配記憶體的(Not Allocatable)
3、>region:表示輸出節的起始地址和大小由MEMORY命令中的region定義指定。下面是一個簡單的例子:
MEMORY{rom:ORIGIN=0x1000,LENGTH=0x1000}
SECTION{ROM:{*(.text)} > rom}
輸出檔案節描述可以包括如下命令:
符號賦值(symbol assignment)命令
輸入檔案節描述
輸出節資料值定義
輸出節關鍵字(一般很少用到)
輸出檔案節的名字一般包括.text、.data、.bss等常見的節名字,也可以定義其他一些特定的名字。如果輸出節的名字是“/DISCARD/”,它表示所有映射到這個輸出節的輸入節都不會在輸出檔案中存在。輸入檔案描述的格式如下:
filename(Sectionname)
filename可以由萬用字元“*”表示,即表示所有的輸入檔案,也可以是多個或單個輸入檔案的名字。Sectionname是輸入檔案中輸入節的名字。下面是一些例子:
data.o(.data)
*(.text)
*(.init)
連結描述檔案例子
連結描述檔案一般都比較簡單,可能只包含一個連結命令:SECTIONS。假設ld要輸出的一個執行程式包含初始化資料的.data節,未初始化資料的.bss節和包含代碼的.text節,而且ld處理的輸入目標檔案也只包含這3個節,則這個簡單的連結描述檔案例子如下所示:
SECTIONS
{
.=0x1000000;
.text:{*(.text)}
.=0x8000000;
.data:{*(.data)}
.bss:{*(.bss)}
}
第一行是關鍵字SECTIONS,緊接著是一系列的符號賦值,而輸出檔案中的節描述包含在“(”和“)”中。
大括弧中的第一條語句是一個指派陳述式,它表示起始的載入地址為0x1000000,這裡“.”表示當前位置的地址。第二條語句定義了輸出檔案的.text節包含的內容,*(.text)表示把所有輸入檔案中的.text收集起來,構成輸出檔案的.text內容。“*”表示ld命令列上描述的所有輸入檔案。第三條語句是一個地址指派陳述式,它定義了輸入檔案接下來的.data節的起始地址為0x8000000。而輸入檔案的.bss節的起始地址為0x8000000加上所有輸入檔案的.data節的大小總和。
下面是用於描述用於某個嵌入式裝置的uClinux核心布局的連結描述檔案(“//”後的語句是附加的注釋,在實際的檔案中並不存在)
//標註嵌入式裝置中各個記憶體塊的地址劃分情況
MEMORY
{
//表示flash中斷向量表的起始地址為0x01000000,長度為0x00400
romvec:ORIGIN=0x01000000,LENGTH=0x00400
//標註flash的起始地址為0x01000400,長度為0x011fffff - 0x01000400
flash:ORIGIN=0x01000400,LENGTH=0x011fffff - 0x01000400
//標註flash的結束的位置在0x011fffff
eflash:ORIGIN=0x011fffff,LENGTH=1
//標註ram中中斷向量表的起始地址為0x00000000,長度為1024
ramvec:ORIGIN=0x00000000,LENGTH=1024
//標註ram其他的可用的記憶體,長度為4M-1K
ram:ORIGIN=0x00000400,LENGTH=0x003fffff - 0x00000400
//標註ram記憶體的結束位置
eram:ORIGIN=0x003fffff,LENGTH=1
}
SECTIONS
{
//定義輸出檔案的.ramvec節
.ramvec:
{
//設定一個變數_ramvec來代表當前的位置,即ramvec節的開始處
_ramvec=.;
}>ramvec
//把該節定義到MEMORY中定義的ramvec所代表記憶體塊中,即從0x00000000開始的1024
//位元組
//定義輸出檔案的.data節
.data:
{
//設定一個變數_data_start來代表當前的位置,即data節的開始處
_data_start=.;
//把所有輸入檔案中的.data節的資料放在此處
*(.data)
//定義edata變數的值為.data節的結束位址
edata=.;
//把edata按16位對齊
edata=ALIGN(0x10);
}>ram
//把.data節的內容放到ram定義的MEMORY中。
//定義未初始化資料的.bss節
.bss
{
//記錄該節的起始位置
_bss_start=ALIGN(0x10);
//記錄.bss節的結束位置
_data_end=ALIGN(0x10);
//把所有.bss資料放在此處
*(.bss)
//把所有COMMON的資料放在此處
*(COMMON)
//記錄.bss節結束的位置,16位對齊
end=ALIGN(0x10);
//記錄.bss節結束的位置,16位對齊
_end=ALIGN(0x10);
}>ram
//把.bss節的內容放到MEMORY中定義的ram所代表的記憶體塊中
//定義輸出檔案中的.eram節
.eram:
{
//記錄該節結束的位置
_ramend=.;
}eram
//把.eram節的內容放到MEMORY中定義的eram所代表的記憶體塊中
//定義輸出檔案的.romvec節,用於記錄flash中中斷向量表
.romvec:
{
//記錄該節的位置
_romvec=.;
}>romvec
//將該節內容放到MEMORY中定義的romvec中
//定義輸出檔案的.text節,即程式段
.text:
{
//記錄.text的起始位置
text_start=.;
//將所有輸入檔案的.text節的內容放到這裡
*(.text)
//記錄.text節結束的位置
_etext=.;
//標誌.text節中的資料結束的位置
_data_rom_start=ALIGN(4);
}>flash
//把.text節的內容放到MEMORY中定義的flash表示的記憶體塊中
//定義flash結束的節
.eflash:
{
//記錄當前的位置
_flashend=.;
}>eflash
//記錄該節放入MEMORY中的位置
}
//描述檔案結束