LD說明文件--3.LD連結指令碼
原文
注:文中大部分文章參考引用都是自身的引用,為了不產生混淆,各個章節標題使用英文原稱,同時參考引用也用英文原稱。
每個連結都由一個連結指令碼控制。這個指令碼由連結命令語言編寫。
連結指令碼的主要目的是描述輸入檔案中的段應當如何對映到輸出檔案中,並控制輸出檔案的記憶體佈局。多數連結指令碼都執行類似功能。但是,如果需要,連結指令碼也可以使用下面所描述的命令指揮連結器進行很多其他操作。
連結器通常使用一個連結指令碼。如果沒有為其提供一個,連結器將會使用預設的編譯在連結器執行檔案內部的指令碼。可以使用命令’–verbose’顯示預設的連結指令碼。一些命令列選項,例如’-r’,’-N’會影響預設的連結指令碼。
你可以通過在命令列使用’-T’命令使用自己的指令碼。如果使用此命令,你的連結指令碼將會替代預設連結指令碼。
也可以通過將指令碼作為連結器輸入檔案隱式的使用連結指令碼,參考Implicit Linker Scripts。
- Basic Script Concepts: 基本連結器指令碼概念
- Script Format: 連結器指令碼格式
- Simple Example: 簡單的連結器指令碼例子
- Simple Commands: 簡單的連結器指令碼命令
- Assignments: 為符號指定數值
- SECTIONS: 段命令
- MEMORY: 記憶體命令
- PHDRS: PHDRS命令
- VERSION: 版本命令
- Expressions: 連結指令碼的表示式
- Implicit Linker Scripts: 隱式連結指令碼
3.1 Basic Linker Script Concepts
為了描述連結指令碼語言,我們需要定義一些基本概念和詞彙。
連結器將許多輸入檔案組合成一個輸出檔案。輸出檔案和每個輸入檔案都有一個特定的已知格式成為目標檔案格式。每個檔案都被稱為目標檔案。輸出檔案通常叫做可執行檔案,但我們仍將其稱為目標檔案。每個目標檔案在其他東西之間,都有一個段列表。有時把輸入檔案的段稱作輸入段,類似的,輸出檔案的段稱作輸出段。
每個目標檔案中的段都有名字和大小。多數段還有一個相關的資料塊,稱為 段內容。一個段可能被標記為可載入,表示當輸出檔案執行時,段內容需要先載入到記憶體中。一個沒有內容的段可能是可分配段,即在記憶體中留出一段空間(有時還需要清零)。一個即不是載入又不是可分配的段,通常含有一些除錯資訊。
每個載入或可分配輸出段有兩個地址。第一個地址為VMA,或者叫做虛地址。這是當輸出檔案執行時段所擁有的地址。第二個地址是LMA,或者叫載入記憶體地址。這是段將會被載入的地址。一個它們會產生區別的例子是,當一個數據段載入到ROM, 此後在程式啟動時被複制到RAM中(這個技術通常被用來初始化全域性變數)。此種情況下,ROM使用LMA地址,RAM使用VMA地址。
如果想檢視目標檔案中的段,可以用objdump程式的’-h’選項。
每個目標檔案還有一個符號列表,稱為符號列表。一個符號可能是被定義的或者未定義的。每個符號都有一個名字,且所有已定義的符號在其他資訊中間都有一個地址。如果將一個c或者c++程式編譯成目標檔案,會將所有定義過的函式和全域性變數以及靜態變數作為已定義符號。所有輸入檔案引用的未定義的函式或者全域性變數會成為未定義符號。
你可以參看目標檔案中的符號,使用nm程式或使用objdump程式的’-t’選項。
3.2 Linker Script Format
連結指令碼是文字的檔案。
一個連結器指令碼是一系列的命令。每個命令都是一個關鍵字,可能後面還跟有一個引數,或者一個符號的賦值。使用分號分割命令,空格通常被忽略。
類似於檔名或者格式名的字串可以直接輸入。如果檔名含有一個字元例如逗號,(逗號被用來分割檔名)你可以將檔名放在雙引號內部。這裡禁止檔名內使用雙引號字元。
你可以像C語言一樣在連結指令碼內包含註釋,由’/*’和’*/’劃分。和C一樣,註釋在句法上被當作空格。
3.3 Simple Linker Script Example
多數指令碼連結都很簡單。
一個最簡單的可能的指令碼只有一個命令:’SECTIONS’。你使用’SECTIONS’命令描述輸出檔案的記憶體佈局。
‘SECTIONS’命令是一個非常強大的命令。這裡我們會描述它的一個簡單應用。假設你的程式由程式碼,初始資料段,以及未初始資料構成。這些將對應被放在’.text’,’.data’,以及’.bss’段中。我們進一步假設這些是唯一將會出現在輸入檔案中的段。
在這個例子裡,我們設定程式碼應該被載入到地址0x10000,資料應該由地址0x8000000起始,下面的連結指令碼將會如此執行:
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
輸入的文字’SECTIONS’作為命令字’SECTIONS’,後面跟隨著用花括號包圍的一系列符號賦值以及輸出段的描述。
在上面的例子中’SECTIONS’命令內部的第一行設定了特殊符號’.’的值,’.’是一個位置計數器。如果你不用其他方式指出輸出段的地址(其他方法後面會討論),地址就會被位置計數器的當前值所設定。位置計數器此後會依據輸出段的大小而增加。在’SECTIONS’命令一開始,位置計數器的值為’0’。
第二行定義了一個輸出段,’text’。語法上所需要的冒號在現在暫時可以被忽略。在輸出段後面的花括號內,你列出了應當被放入這個輸出段的輸入段名稱。’*’是一個萬用字元,可以與所有檔名匹配。表示式’*(.text)’表示所有輸入檔案的’.text’輸入段。
因為在’.text’被定義的時候位置計數器的值是’0x10000’,連結器將會把輸出檔案’.text’的段地址設定為’0x10000’。
剩下的行定義了輸出檔案的’.data’和’.bss’段。連結器將會把’.data’輸出段定為在地址’0x8000000’。在連結器放置’.data’段後,位置計數器為’0x8000000’加上’.data’段的大小。因此’.bss’輸出段在記憶體中將會緊緊挨在’.data’段後面。
連結器會保證每個輸出段依照要求對齊,如果有必要的話,會增加位置計數器。在上面的例子中,段’.text’和’.data’段可以正確的符合任何對齊的限定條件,而連結器可能會在’.data’和’.bss’段之間建立一個小縫(為了使’.bss’段對齊)。
如上,這是一個簡單完整的連結指令碼。
3.4 Simple Linker Script Commands
本章我們將介紹一些簡單的指令碼命令。
- Entry Point: 設定入口點
- File Commands: 控制檔案的命令
- Format Commands: 控制目標檔案格式的命令
- REGION_ALIAS: 為記憶體區域設定別名
- Miscellaneous Commands: 其他連結指令碼命令
3.4.1 Setting the Entry Point
第一個在程式中執行的指令被稱為入口點(entry point)。可以用ENTRY指令碼命令設定入口點,引數是一個符號名:
ENTRY(symbol)
這裡有幾種方法設定入口點。連結器會依照下面的方法依次嘗試設定入口點,直到其中一種方法成功:
- 命令列的’-e’選項指定的值
- 指令碼中的ENTRY(symbol)命令
- 一個目標約定的特殊符號(如果有定義的話);例如大多數目標符號為start,但PE和BeOS系統會檢查一個可能入口符號列表,以第一個碰到的為準 - ‘.text’的第一個位元組的地址,如果存在的話
- 地址0
注:也就是說,優先順序為:命令>指令碼檔案>自定義start
3.4.2 Commands Dealing with Files
一些指令碼命令用來處理檔案。
INCLUDE filename
在命令處包含連結指令碼檔案’filename’。檔案將會在當前目錄搜尋,以及任何’-L’命令列命令指定的路徑。INCLUDE可以巢狀呼叫10層。
可以直接把INCLUDE放到頂層,MEMORY或者SECTIONS命令中,或者在輸出段描述中。
**INPUT(file, file, …)
INPUT(file file …)**
INPUT命令引導連結器包含列出的檔案,與在命令列上使用的一樣。
例如,如果每次連結時你總是想包含subr.o,但不想麻煩的輸入每個連結命令,那麼你可以把’INPUT(subr.o)’放入你的連結指令碼。
事實上,如果你願意,可以把所有輸入檔案列在連結指令碼內,然後僅使用’-T’命令呼叫連結器。
在sysroot字首被設定的情況下,且filename以’/’字元開始,且正在執行的指令碼也處於sysroot字首範圍內,filename將會在sysroot字首範圍內查詢。否則連結器會嘗試在當前目錄開啟。如果沒有找到,連結器會搜尋庫搜尋路徑。sysroot字首也可以通過把filename的第一個字元設定為’=’強制使用(’=’替換為sysroot)。參照Command Line Options的’-L’命令。
如果使用’INPUT (-lfile)’ld將會將名字轉化為libfile.a,就像命令列引數’-l’。
當你使用INPUT命令在隱式連結指令碼中,檔案在連結指令碼檔案被包含的時刻才會被加入。這可能會影響庫的搜尋。
**GROUP(file, file, …)
GROUP(file file …)**
GROUP命令與INPUT命令勒斯,除了所有file指出的名字都應該為庫,並且所有庫將會被重複搜尋直到沒有新的未定義引用被建立。參見Command Line Options對於’-(‘的描述。
**AS_NEEDED(file, file, …)
AS_NEEDED(file file …)**
此構造僅可以出現在INPUT或GROUP命令中,位於其他命令中間。此命令中的檔案將會以類似於直接出現在INPUT或者GROUP命令中的檔案一樣處理,除了ELF共享庫,ELF共享庫僅在真正需要使用時才被新增。這個構造本質上使能了列表中檔案的’–as-needed’選項,並且恢復此前的–as-needed設定,此後的–no-as-needed。
OUTPUT(filename)
OUTPUT命令為輸出檔案命名。使用指令碼中的OUTPUT(filename)與命令列的’-o filename’類似(參見Command Line Options)。如果同時設定了,命令列的命令有效。
你可以使用OUTPUT命令定義一個預設的輸出檔名來替代通常預設的名稱a.out。
SEARCH_DIR(path)
SEARCH_DIR命令新增一個ld搜尋庫的路徑。使用SEARCH_DIR(path)與命令列的’-L path’類似(參見Command Line Options)。如果都使用了,連結器將會搜尋所有路徑。命令列給出的路徑會優先搜尋。
STARTUP(filename)
STARTUP命令類似於INPUT命令,除了filename將作為首個被連結的輸入檔案處理,就像被在命令列第一個給出一樣。在一些把第一個檔案當作入口點的系統上這個命令非常有效。
3.4.3 Commands Dealing with Object File Formats
有一對處理目標檔案格式的指令碼命令。
**OUTPUT_FORMAT(bfdname)
OUTPUT_FORMAT(default, big, little)**
The OUTPUT_FORMAT命令使用BFD格式的命名方式(參見BFD)。使用OUTPUT_FORMAT(bfdname)類似於命令列的’–oformat bfdname’(參考Command Line Options)。如果都使用了,以命令列為準。
可以使用三引數OUTPUT_FORMAT命令來使用不同的基於命令列’-EB’和’-EL’的格式。此命令允許連結指令碼設定輸出格式需要的大小端。
如果即沒有’-EB’也沒有’-EL’被使用,那麼輸出格式將會使用第一個引數。如果使用了’-EB’,輸出格式將是第二個引數,大端。如果使用了’-EL’,輸出格式將是第三個引數,小端。
例如MIPS ELF目標預設的連結指令碼使用如下的命令:
OUTPUT_FORMAT(elf32-bigmips, elf32-bigmips, elf32-littlemips)
這表示預設輸出格式為’elf32-bigmips’,但如果在命令列輸入了’-EL’命令,輸出檔案將以’elf32-littlemips’格式輸出。
TARGET(bfdname)
TARGET命令設定讀取輸入檔案時的BFD格式。這將影響後面的INPUT和GROUP命令。此命令類似使用命令列指令’-b bfdname’(參見Command Line Options)。如果使用了TARGET命令,但OUTPUT_FORMAT命令沒使用,則最後的TARGET命令還被用來設定輸出檔案的格式。(參見BFD)
3.4.4 Assign alias names to memory regions
可以為MEMORY命令建立的記憶體區域提供別名。每個名字最多指代一個區域。
REGION_ALIAS(alias, region)
REGION_ALIAS函式為記憶體區域建立一個別名。這允許了輸出段靈活的對映到記憶體區域。下面是一個例子:
假設有一個含有很多記憶體儲存裝置的嵌入式系統的應用。每個記憶體裝置都有特殊的目的,易失記憶體RAM可以存放可執行程式碼或者資料。一些裝置可能是隻讀的,非易失性記憶體ROM允許儲存可執行程式碼和只讀資料。最後的是一個只讀的,非易失的記憶體ROM2,允許只讀資料段讀取,不允許指定程式碼段儲存。現在有四個輸出段:
- .text 程式程式碼
- .rodata 只讀資料
- .data 可讀寫且需要初始化資料
- .bss 可讀寫的置零初始化資料
目標是提供一個連結命令檔案含有系統無關的定義輸出段的部分,以及系統相關的把輸出段對映到系統有效記憶體區域的部分。我們的嵌入式系統含有三個不同的記憶體設定A,B,C:
Section Variant A Variant B Variant C
.text RAM ROM ROM
.rodata RAM ROM ROM2
.data RAM RAM/ROM RAM/ROM2
.bss RAM RAM RAM
標記RAM/ROM或者RAM/ROM2表示此段被分別載入到區域ROM或者ROM2。注意三個設定的.data段的起始地址都位於.rodata段的末尾。
下面是基本連結指令碼處理輸出段。其含有系統相關的linkcmds.memory檔案,檔案描述了記憶體佈局:
INCLUDE linkcmds.memory
SECTIONS
{
.text :
{
*(.text)
} > REGION_TEXT
.rodata :
{
*(.rodata)
rodata_end = .;
} > REGION_RODATA
.data : AT (rodata_end)
{
data_start = .;
*(.data)
} > REGION_DATA
data_size = SIZEOF(.data);
data_load_start = LOADADDR(.data);
.bss :
{
*(.bss)
} > REGION_BSS
}
現在我們需要三個不同的linkcmds.memory來定義記憶體區域以及別名。下面是A,B,C不同的linkcmds.memory:
A
所有都存入RAM
MEMORY
{
RAM : ORIGIN = 0, LENGTH = 4M
}
REGION_ALIAS("REGION_TEXT", RAM);
REGION_ALIAS("REGION_RODATA", RAM);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
B
程式碼和只讀資料存入ROM。可讀寫資料放入RAM。一個已初始化了的資料的映象被載入到ROM,並在系統啟動的時候讀入RAM。
MEMORY
{
ROM : ORIGIN = 0, LENGTH = 3M
RAM : ORIGIN = 0x10000000, LENGTH = 1M
}
REGION_ALIAS("REGION_TEXT", ROM);
REGION_ALIAS("REGION_RODATA", ROM);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
C
程式碼放入ROM,只讀資料放入ROM2。可讀寫資料放入RAM。一個已初始化了的資料的映象被載入到ROM2,並在系統啟動的時候讀入RAM。
MEMORY
{
ROM : ORIGIN = 0, LENGTH = 2M
ROM2 : ORIGIN = 0x10000000, LENGTH = 1M
RAM : ORIGIN = 0x20000000, LENGTH = 1M
}
REGION_ALIAS("REGION_TEXT", ROM);
REGION_ALIAS("REGION_RODATA", ROM2);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
這裡可以依據需要可以寫一個普通的系統初始化流程將.data段從ROM或者ROM2拷貝到RAM:
#include <string.h>
extern char data_start [];
extern char data_size [];
extern char data_load_start [];
void copy_data(void)
{
if (data_start != data_load_start)
{
memcpy(data_start, data_load_start, (size_t) data_size);
}
}
注:目前分析,應該是AT命令把讀寫資料.data段即載入到ROM又在RAM分配了空間。
3.4.5 Other Linker Script Commands
這裡有幾個其它的連結指令碼命令。
ASSERT(exp, message)
確保exp表示式為非零的。如果是零,則報錯退出。
注意此斷言會在最終連結階段之前進行檢查。這表示,在段內使用PROVIDE的定義如果使用者沒有為其設定值,此表示式將無法通過檢測。唯一的例外是PROVIDE的符號剛剛引用了’.’。因此,一個如下斷言:
.stack :
{
PROVIDE (__stack = .);
PROVIDE (__stack_size = 0x100);
ASSERT ((__stack > (_end + __stack_size)), "Error: No room left for the stack");
}
如果沒有在別的地方定義__stack_size將會失敗。符號在段外定義的PROVIDE會在此前被求值,因此他們可以被ASSERT。因此:
PROVIDE (__stack_size = 0x100);
.stack :
{
PROVIDE (__stack = .);
ASSERT ((__stack > (_end + __stack_size)), "Error: No room left for the stack");
}
將會工作。
EXTERN(symbol symbol …)
強制符號在輸出檔案中作為未定義符號。這樣做了,可能會引發從標準庫中連線一些額外的模組。你可以為每一個’EXTERN’列出幾個符號,而且你可以多次使用’EXTERN’。 這個命令跟’-u’命令列選項具有相同的效果。
FORCE_COMMON_ALLOCATION
此命令類似於命令列命令’-d’:即便是使用了’-r’的重定位輸出檔案,也讓ld為普通符號分配空間。
INHIBIT_COMMON_ALLOCATION
此命令與命令列命令’–no-define-common’效果類似:讓ld不為普通符號分配空間,即便是一個非可重定位輸出檔案。
INSERT [ AFTER | BEFORE ] output_section
此命令在’-T’指定的指令碼中典型應用是增強預設的SECTIONS,例如,重複佔位程式段。它將把所有此前的連結指令碼的宣告插入output_section的後面(或者前面),並且使’-T’不要覆蓋預設連結指令碼。實際插入點類似於孤兒段。參見Location Counter。插入發生在連結器把輸入段對映到輸出段後。在插入前,因為’-T’的指令碼在預設指令碼之前被解析,在’-T’指令碼中的宣告會先於預設內部指令碼的宣告而執行。特別的,會先於預設指令碼把輸入段的宣告被製成’-T’指定的輸出段。下例為’-T’指令碼使用INSERT可能的情況:
SECTIONS
{
OVERLAY :
{
.ov1 { ov1*(.text) }
.ov2 { ov2*(.text) }
}
}
INSERT AFTER .text;
NOCROSSREFS(section section …)
此命令可能被用來告訴ld,如果引用了section的引數就報錯。
在特定的程式型別中,比如使用覆蓋技術的嵌入式系統,當一個段被載入到記憶體中,另一個段不會被載入。任何兩個段之間直接的引用都會帶來錯誤。例如,如果一個段中的程式碼呼叫另一個段中的函式,將會產生錯誤。
NOCROSSREFS列出了一系列輸出段的名字。如果ld檢測到任何段間交叉引用,將會報告錯誤並返回非零退出碼。注意NOCROSSREFS使用輸出段名稱,而不是輸入段名稱。
OUTPUT_ARCH(bfdarch)
指定一個特定的輸出機器結構。引數為BFD庫定義的名字之一(參考BFD)。可以使用objdump程式的’-f’指令檢視一個目標檔案的結構。
LD_FEATURE(string)
此命令用來控制ld行為。如果string是”SANE_EXPR”則指令碼中的絕對符號和數字將被在任何地方當作數字對待。參考Expression Section。
3.5 Assigning Values to Symbols
在連結指令碼中,可以為符號賦值。這將定義符號並將其放入全域性的符號表內。
- Simple Assignments: 簡單的賦值
- HIDDEN: HIDDEN
- PROVIDE: PROVIDE
- PROVIDE_HIDDEN: PROVIDE_HIDDEN
- Source Code Reference: 如何在原始碼中使用一個連結指令碼定義的符號。
3.5.1 Simple Assignments
可以使用任何C的賦值操作符號為符號賦值:
symbol = expression ;
symbol += expression ;
symbol -= expression ;
symbol *= expression ;
symbol /= expression ;
symbol <<= expression ;
symbol >>= expression ;
symbol &= expression ;
symbol |= expression ;
第一種情況將會把表示式的值賦給符號。其他情況裡,符號必須已經定義過,此後符號的值會被相應調整。
特殊符號’.’代表位置計數器。你可以在SECTIONS命令中使用它,參考Location Counter。
表示式後面的分號不能省略。
後面會有表示式的定義,參考Expressions。
你在寫表示式賦值的時候,可以把它們作為單獨的部分,也可以作為’SECTIONS’命令中的一個語句,或者作為’SECTIONS’命令中輸出段描述的一個部分。
符號的有效區域由表示式所在的段決定,更多資訊參考Expression Section。
下面是表示三種不同的使用符號賦值的地方:
floating_point = 0;
SECTIONS
{
.text :
{
*(.text)
_etext = .;
}
_bdata = (. + 3) & ~ 3;
.data : { *(.data) }
}
在這個例子裡,’floating_point’將被設定為0。符號’_etext’被設定為緊隨’.text’最後一個輸入段後面的地址。符號’_bdata’將被定義為在’.text’輸出段後面的一個4位元組向上對齊的地址。
3.5.2 HIDDEN
為ELF目標的埠定義一個符號,符號將被隱藏並且不會被匯出。語法是HIDDEN(symbol = expression)。
這是上面簡單賦值的例子,使用HIDDEN重寫:
HIDDEN(floating_point = 0);
SECTIONS
{
.text :
{
*(.text)
HIDDEN(_etext = .);
}
HIDDEN(_bdata = (. + 3) & ~ 3);
.data : { *(.data) }
}
這裡此三個符號出了這個模組就不可見了。
3.5.3 PROVIDE
有些情況下,僅當一個符號被引用了卻沒有定義在任何連結目標中,才需要為連結指令碼定義一個符號。例如,傳統連結器定義符號’etext’。但是,ANSI C需要使用者可以自由使用’etext’作為一個函式名稱且不會引發錯誤。只有當符號被引用卻沒被定義的時候,PROVIDE關鍵字可以定義一個符號,比如’etext’。語法為PROVIDE(symbol = expression)。
下面是一個使用PROVIDE定義’etext’的例子:
SECTIONS
{
.text :
{
*(.text)
_etext = .;
PROVIDE(etext = .);
}
}
在這個例子中,如果程式定義了’_etext’,連結器將給出重複定義錯誤。然而另一方面,如果程式定義了’etext’,連結器將會預設使用程式中的定義。如果程式已用了’etext’但沒有定義它,連結器將使用連結指令碼中的定義。
3.5.4 PROVIDE_HIDDEN
類似PROVIDE。對於ELF目標的埠,符號將被隱藏且不會被輸出。
3.5.5 Source Code Reference
從原始碼獲得一個指令碼定義的變數值不是直觀的。特別是一個指令碼符號與一個高階語言定義的變數宣告不符的時候,將使用一個沒有值的變數替代它。
在更深入前,需要注意的一點是,編譯器常常把原始碼的名稱轉變為不同的名字再存入符號表中。例如Fortran編譯器通常在前面或者後面加一個下劃線,而C++ 偏愛額外的’name mangling(命名粉碎)’。因此在原始碼中定義的變數名稱與連結指令碼中定義的變數可能會有區別。例如在一個C語言中,一個連結指令碼變數可能被認為是:
extern int foo;
但在指令碼中可能被定義為:
_foo = 1000;
後面的例子中,假設沒有發生名字轉換。
當一個高階語言,比如C語言,聲明瞭一個符號,會發生兩件事。第一是編譯器在程式記憶體中保留足夠的空間來保持這個符號。第二是編譯器在符號表中建立一個入口,用來保持符號的地址,例如符號表含有儲存符號值的記憶體塊的地址。因此例如下面的C宣告,在檔案中為:
int foo = 1000;
在符號表建立了一個名為’foo’的入口。此入口儲存了一個’int’大小的記憶體塊的地址,塊內數字1000被初始化儲存。
當一個程式引用一個符號,編譯器生成的程式碼會首先存取符號表來查詢符號的記憶體塊的地址,此後程式碼從記憶體塊中讀取值。因此:
foo = 1;
在符號表中查詢’foo’,得到符號相關的地址,此後將1寫入改地址。反之:
int * a = & foo;
查詢符號符號表內的’foo’,獲取它的地址,此後複製地址的值到與變數’a’相關的地址去。
連結指令碼的符號宣告,相對來說,在符號表中建立一個入口,但此時並不指派任何記憶體給它們。因此它們是一個地址但沒有值。例如連結指令碼定義:
foo = 1000;
在符號表建立一個符號稱為’foo’,並保持了記憶體地址1000,但沒有任何特殊的東西被儲存在地址1000。這表示你不能存取連結指令碼定義符號的值–它們沒有任何值–所有你可以做的僅為存取連結指令碼定義符號的地址。
因此,當你在原始碼中使用一個連結指令碼定義的符號時你應該總是使用符號的地址,永遠不要嘗試使用它的值。例如假設你想把.rom段的內容複製到.FLASH段中,且連結指令碼含有以下宣告:
start_of_ROM = .ROM;
end_of_ROM = .ROM + sizeof (.ROM);
start_of_FLASH = .FLASH;
C原始碼執行這個複製應當類似於:
extern char start_of_ROM, end_of_ROM, start_of_FLASH;
memcpy (& start_of_FLASH, & start_of_ROM, & end_of_ROM - & start_of_ROM);
注意操作符’&’的使用。上面是正確的程式碼。一種替換是,把符號被當作一個數組變數的名稱,因此程式碼變成了:
extern char start_of_ROM[], end_of_ROM[], start_of_FLASH[];
memcpy (start_of_FLASH, start_of_ROM, end_of_ROM - start_of_ROM);
注意此時不需要操作符’&’了。
3.6 SECTIONS Command
SECTIONS命令告訴連結器如何將輸入段對映到輸出段,以及如何把輸出段放入記憶體中。
SECTIONS命令的格式為:
SECTIONS
{
sections-command
sections-command
...
}
每個sections-command命令可能是下面之一:
- 一個入口命令(參考 Entry command)
- 一個符號定義 (參考 Assignments)
- 一個輸出段的描述
- 一個重複描述
‘ENTRY’命令和符號賦值在’SECTIONS’命令中是允許的,這是為了方便在這些命令中使用位置計數器。這也可以讓連線指令碼更容易理解,因為你可以在更有意義的地方使用這些命令來控制輸出檔案的佈局。
輸出段描述和重疊將在後面分析。
如果你在連結指令碼中不使用SECTIONS命令,連結器將會把所有輸入段依照碰到的順序分別放在一個獨立名稱的輸出段中。例如,如果所有輸入段出現在第一個檔案中,輸出檔案的段的順序將會與第一個輸入檔案保持一致。第一個段被放在地址0。
- Output Section Description: 輸出段描述
- Output Section Name: 輸出段名稱
- Output Section Address: 輸出段地址
- Input Section: 輸入段描述
- Output Section Data: 輸出段資料
- Output Section Keywords: 輸出段關鍵字
- Output Section Discarding: 輸出段拋棄內容
- Output Section Attributes: 輸出段屬性
- Overlay Description: 重疊描述
3.6.1 Output Section Description
完整的輸出段描述看起來像下面這樣(一個sections-command):
section [address] [(type)] :
[AT(lma)]
[ALIGN(section_align) | ALIGN_WITH_INPUT]
[SUBALIGN(subsection_align)]
[constraint]
{
output-section-command
output-section-command
...
} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp] [,]
多數輸出段不需要使用多數的可選段屬性。
SECTION邊上的空格是必須的,所以段名稱是明確的。冒號跟花括號也是必須的。最後的逗號如果使用了fillexp,且下一個段命令看起來像是表示式的延續的時候可能會需要。斷行和其他的空格是可選的。
每個output-section-command可能是下面之一:
- 一個符號定義(參考 Assignments)
- 一個輸入段描述(參考 Input Section)
- 直接包含的資料值(參考 Output Section Data)
- 一個特殊輸出段關鍵字(參考 Output Section Keywords)
3.6.2 Output Section Name
輸出段的名字是section。section必須符合你的輸出格式的規定。在僅支援一個有限段數目的格式中,例如a.out,名字必須為該格式支援的段名稱之一(例如a.out格式僅允許’.text’,’.data’,’.bss’)。如果輸出格式支援任意數量的段,但名稱僅能為編號(例如Oasys),名字應該以雙引號包裹的數字字串形式提供。一個段名字可能由任何字元組成,但一個含有許多特殊字元例如逗號等的名字需要被雙引號括起來。
輸出段名稱’/DISCARD/’有特殊含義; 參考Output Section Discarding.
3.6.3 Output Section Address
地址(address)是一個輸出段VMA(虛地址)的表示式。此地址為可選引數,但如果給出了地址,則輸出地址就會被精確的設定到給定值。
如果輸出的地址沒有給定,則依照下面的嘗試選擇一個地址。此地址將會被調整到符合輸出端要求的對齊地址。輸出段的對齊要求是所有輸入節中含有的對齊要求中最嚴格的一個。
輸出段地址探索如下:
- 如果為段設定了記憶體區域,則段被放如該區域,並且段地址為區域中的下一個空閒位置。
- 如果使用MEMORY命令建立了一個記憶體區域列表,此時第一個屬性匹配段的區域被選擇來載入段,段地址為區域中的下一個空閒位置。參見MEMORY。
- 如果沒有指定的記憶體區域,或者沒有匹配段的,則輸出地址將會基於當前位置計數器的值。
例如:
.text . : { *(.text) }
以及
.text : { *(.text) }
有著精細的差別。第一個將會把’.text’的地址設定為位置計數器。第二個將會依照所有’.text’輸入段中最嚴格的對齊要求,設定地址為當前位置計數器對齊的值。(第一個有address,第二個沒有,啟動了探索機制)
地址可以是任何表示式;參考Expressions。例如,如果你想在0x10位元組邊界上對齊段,因此段地址最低的四個位(原文是bit,不是byte,有的翻譯有錯誤)為0,你可類似如下面這麼做:
.text ALIGN(0x10) : { *(.text) }
此程式碼可以工作,因為ALIGN將會返回位置計數器依照引數向上對齊的值。
為段指定地址將會改變位置計數器的值,如果該段不是空段的話。(空段被忽略)
3.6.4 Input Section Description
最常見的輸出段命令(output-section-command)是輸入段描述。
輸入段描述是最基本的連結指令碼操作。你使用輸出段告訴連結器如何把程式放到記憶體中。你使用輸入段描述告訴連結器如何把輸入檔案對映到你的記憶體佈局。
- Input Section Basics: 基本的輸入段
- Input Section Wildcards: 輸入段萬用字元模板
- Input Section Common: 普通符號的輸入段
- Input Section Keep: 輸入段與垃圾回收
- Input Section Example: 輸入段例子
3.6.4.1 Input Section Basics
一個輸入段描述由跟隨在段名稱後面括號包含的一個可選的檔名稱列表構成。
檔名和段名稱可以為萬用字元,我們將在後面解釋(參考Input Section Wildcards)。
最普通的輸入段描述為在一個特定輸出段內包含所有輸入段。例如,把所有輸入段放入’.text’段,可以這麼寫:
*(.text)
此處的’*’是一個萬用字元,可以匹配任何檔名。如果想從萬用字元匹配的檔案列表中排除一系列檔案,可以使用EXCLUDE_FILE。例如:
*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors)
會讓除了以crtend.o和otherfile.o結尾的所有檔案的.ctors段被包含。
有兩種方法包含更多的段:
*(.text .rdata)
*(.text) *(.rdata)
兩種方法的區別是輸入段的’.text’和’.rata’段出現在輸出中的順序。第一個例子裡,他們將被混合在一起,按照連結器找到它們的順序存放。另一個例子中,所有’.text’輸入段將會先出現,後面是’.rdata’輸入段。
你可以指定一個檔名來包含特定檔案的段。如果一個或者多個你的檔案需要被放在記憶體中的特定位置,你可能需要這麼做。例如:
data.o(.data)
如果想使用段標誌來選擇輸入檔案的段,可以使用INPUT_SECTION_FLAGS。
下面是一個例子,使用ELF的段頭部標記:
SECTIONS {
.text : { INPUT_SECTION_FLAGS (SHF_MERGE & SHF_STRINGS) *(.text) }
.text2 : { INPUT_SECTION_FLAGS (!SHF_WRITE) *(.text) }
}
在此例中,輸出段’.text’將被由那些匹配名字(.text)且段頭部標誌設定了SHF_MERGE和SHF_STRINGS的段構成。輸出段’.text2’由那些匹配名字(.text)且段頭部標誌未設定SHF_WRITE的段構成。
你也可以指出特別的關聯庫名稱的檔案,命令是 庫匹配模板:檔名模板 ,冒號兩邊不能有空格。
‘archive:file’
匹配檔案和庫
‘archive:’
匹配整個庫
‘:file’
匹配檔案但不匹配庫。
‘archive’和’file’都可以含有shell的萬用字元。在基於DOS的系統裡,連結器會假定一個單字跟著一個冒號是一個特殊的驅動符,因此’c:myfile.o’是一個檔案的特殊使用,而不是關聯庫’c’的’myfile.o’檔案。’archive:file’可以使用在EXCLUDE_FILE列表中,但不能出現在其他連結指令碼內部。例如,你不能使用’archive:file’從INPUT命令中取出一個庫相關的檔案。
如果你使用一個檔名而不指出段列表,則所有的輸入檔案的段將被放入輸出段。通常不會這麼做,但有些場合比較有用,例如:
data.o
當你使用一個檔名且不是'archive:file’特殊命令,並且不含任何萬用字元,連結器將先檢視你是否在命令列上或者在INPUT命令裡指定了改檔案。如果沒有這麼做,連結器將嘗試將檔案當作輸入檔案開啟,就像檔案出現在了命令列一樣。注意與INPUT命令有區別,因為連結器不會在庫檔案路徑搜尋檔案。
3.6.4.2 Input Section Wildcard Patterns
在一個輸入段描述中,檔名和段名都可以使用萬用字元。
許多例子中的檔名’*’是一個最簡單的檔名萬用字元。
萬用字元模板類似於Unix shell中使用的那樣。
- ‘*’匹配任意數量字元
- ‘?’匹配任意單字
- ‘[chars]’匹配一個簡單的所有chars包含的字元;’-‘字元可被用來指出一個字元的範圍,例如’[a-z]’可以用來匹配所有小寫字母
- ‘\’引用後面的字元
當一個檔名與萬用字元進行匹配,萬用字元不會匹配一個’/’字元(被Unix用來分隔目錄)。一個僅含’*’的模板是例外,其將永遠匹配任和檔名,無論其是否含有’/’。在段名稱部分,萬用字元會匹配’/’字元。
檔名萬用字元僅對那些顯示在命令列或者INPUT命令中指定的檔案進行匹配。連結器不會搜尋目錄擴張匹配範圍。
如果一個名字被多個萬用字元匹配上,或者一個檔名被顯示指定了,且又被萬用字元匹配了,連結器將會使用連結指令碼中第一個匹配的。例如,下面的輸入段描述可能有錯誤,因為data.o的規則不會被應用:
.data : { *(.data) }
.data1 : { data.o(.data) }
通常,連結器會把匹配的檔案和段按照發現的順序放置。可以使用關鍵字SORT_BY_NAME改變這一行為,此命令在括號包裹的萬用字元模板前出現(如SORT_BY_NAME(.text*))。如果使用了SORT_BY_NAME關鍵字,連結器會把檔案或者段的名字按照上升順序排序後放入輸出檔案。
SORT_BY_ALIGNMENT與SORT_BY_NAME非常相似,區別是SORT_BY_ALIGNMENT對段的對齊需求使用降序方式排序放入輸出檔案中。大的對齊被放在小的對齊前面,這樣可以減少為了對齊需要的額外空間。
SORT_BY_INIT_PRIORITY與SORT_BY_NAME相似,區別是SORT_BY_INIT_PRIORITY把段按照GCC的嵌入在段名稱的init_priority數字屬性值升序排列後放入輸出檔案。
SORT是SORT_BY_NAME的別名。
當連結指令碼中有網狀排序結構時,最多允許1級的網結構用作段排序命令。
- 1.SORT_BY_NAME (SORT_BY_ALIGNMENT (wildcard section pattern))。將會先把輸入段按照名字排序,此後如果兩個段名字相同按照對齊方式排序。
- SORT_BY_ALIGNMENT (SORT_BY_NAME (wildcard section pattern))。將會先把輸入段按照對齊方式排序,此後如果兩個段名字相同按照名字排序。
- SORT_BY_NAME (SORT_BY_NAME (wildcard section pattern))。被當作SORT_BY_NAME (wildcard section pattern)。
- SORT_BY_ALIGNMENT (SORT_BY_ALIGNMENT (wildcard section pattern))。被當作SORT_BY_ALIGNMENT (wildcard section pattern)。
- 其他所有網狀段排序命令都為無效命令。
當命令列段排序選項和連結指令碼段排序命令都被使用時,排序命令優先於命令列選項。
如果指令碼中的段排序命令不是網狀的,則命令列選項將使段排序被當作網狀排序使用。
- SORT_BY_NAME (wildcard section pattern ) 與 –sort-sections alignment 等價於 SORT_BY_NAME (SORT_BY_ALIGNMENT (wildcard section pattern)).
- SORT_BY_ALIGNMENT (wildcard section pattern) 與 –sort-section name 等價於 SORT_BY_ALIGNMENT (SORT_BY_NAME (wildcard section pattern)).
如果指令碼的排序命令已經是網狀的,則命令列選項被忽略。
SORT_NONE 禁止段排序,忽略命令列的排序選項。
如果你對輸入段被放置到何處感到困惑,使用’-M’連結選項來生成對映檔案,對映檔案詳細的說明了輸入段具體被對映到輸出段的哪裡。
這個例子顯示了萬用字元如何被用來分隔檔案。這個連結指令碼指引連結器把所有’.text’段放在’.text’裡,以及所有’.bss’放到’.bss’中。連結器將會把所有以大寫字母開頭的檔案的’.data’段放入’.DATA’,其他檔案的’.data’段放入’.data’。
SECTIONS {
.text : { *(.text) }
.DATA : { [A-Z]*(.data) }
.data : { *(.data) }
.bss : { *(.bss) }
}
3.6.4.3 Input Section for Common Symbols
普通符號需要一個特別的標記,因為很多目標檔案格式中沒有特定的普通符號輸入段。連結器把普通符號當作位於一個名為’COMMON’的輸入段內。
你可以使用檔名與’COMMON’段的組合就像使用其它檔名與段一樣。你可以用這種方法把一個特定檔案的普通符號放入一個段內,同時把其它輸入檔案的普通符號放入另一個段內。
大多數情況下,輸入檔案的普通符號會被放到輸出檔案的’.bss’段裡面。例如:
.bss { *(.bss) *(COMMON) }
有些目標檔案格式含有多種普通符號的型別。例如,MIPS ELF目標檔案把標準普通符號和小普通符號區分開來。在這種情況下,連結器會為另一個型別的普通符號使用其它的特殊段名稱。在MIPS ELF中,連結器為普通符號使用’COMMON’以及為小普通符號使用’.scommon’。這樣就可以把不同型別的普通符號對映到記憶體中的不同位置。
有時在老的連結指令碼中能看見’[COMMON]’。這個標記現在已廢棄。它等價於’*(COMMON)’。
3.6.4.4 Input Section and Garbage Collection
當使能了連結時垃圾收集(‘–gc-sections’),把段標記為不應被消除非常常用。此功能通過把一個輸入段的萬用字元入口使用KEEP()實現,類似於KEEP((.init))或KEEP(SORT_BY_NAME()(.ctors))。
3.6.4.5 Input Section Example
下面是一個完整的連結指令碼的例子。它告訴連結器從all.o讀取所有段,把它們放到輸出段’outputa’的開頭位置,’outputa’的起始地址為’0x10000’。所有檔案foo.o中的’.input1’段緊跟其後。所有檔案foo.o中的’input2’段放入輸出檔案的’outputb’中,跟著是foo1.o中的’input1’段。所有其它的’.input1”.input2’段被放入輸出段’outputc’。
SECTIONS {
outputa 0x10000 :
{
all.o
foo.o (.input1)
}
outputb :
{
foo.o (.input2)
foo1.o (.input1)
}
outputc :
{
*(.input1)
*(.input2)
}
}
3.6.5 Output Section Data
你可以通過使用輸出段命令BYTE, SHORT, LONG, QUAD, 或者 SQUAD在輸出段顯式的包含幾個位元組的資料。每個關鍵字後面跟著一個括號包裹的表示式指出需要儲存的數值(參照Expressions)。表示式的值被儲存在當前位置計數器值的地方。
BYTE, SHORT, LONG, QUAD命令分別儲存1,2,4,8位元組。在儲存位元組後,位置計數器會按照儲存的位元組數增加。
例如,下面將會儲存一個單位元組資料1,然後儲存一個四位元組資料,值為符號’addr’的值:
BYTE(1)
LONG(addr)
使用64位主機或目標時,QUAD和SQUAD是一樣的,都是儲存8個位元組,或64位的值。當主機和目標都是32位時,表示式被當作32位計算。在這種情況下QUAD儲存一個32位的值,並使用0擴充套件到64位,SQUAD儲存32位值並使用符號位擴充套件到64位。
如果輸出檔案的目標檔案格式有顯示的大小端,在正常的情況下,值將按照大小端儲存。當目標檔案格式沒有顯示的大小端,確實有這種情況,例如,S-records,值將被按照第一個輸入目標檔案的大小端儲存。
注意這些命令僅在段描述內部工作,因此下面的例子會使連結器產生錯誤:
SECTIONS { .text : { *(.text) } LONG(1) .data : { *(.data) } }
下面是正確的例子:
SECTIONS { .text : { *(.text) ; LONG(1) } .data : { *(.data) } }
可以使用FILL命令來設定當前段的填充模板。該命令後面跟著一個括號包裹的表示式。所有其它沒有被特別指定段的記憶體區域(例如因為對齊需要而留出來的縫隙)按照表達式的值填充,如果有必要可以重複填充。FILL宣告僅會覆蓋它本身在段定義中出現的位置後面的所有記憶體區域;通過使用不同的FILL宣告,你可以在一個輸出段中使用不同的填充模板。
這個例子顯示瞭如何使用’0x90’填充未定義記憶體區域:
FILL(0x90909090)
FILL命令類似’=fillexp’輸出段屬性,但其僅影響FILL命令後面的段,而不是整個段。如果同時使用,FILL命令為高優先順序。參考See Output Section Fill獲取更多填充細節。
3.6.6 Output Section Keywords
有兩個關鍵字可以作為輸出段的命令。
CREATE_OBJECT_SYMBOLS
此命令告訴連結器為每個輸入檔案建立一個符號。每個符號的名字為對應輸入檔案的名字。每個符號出現的位置位於包含CREATE_OBJECT_SYMBOLS命令的輸出段中。
這個命令一直是a.out目標檔案格式特有的。 它一般不為其它的目標檔案格式所使用。
CONSTRUCTORS
當連結時使用a.out目標檔案的格式,連結器使用一個特殊構造集來支援C++ 全域性建構函式和解構函式。在連結不支援任意段的檔案格式時,例如ECOFF和XCOFF,連結器將會通過名字自動識別C++全域性建構函式和解構函式。對於這些格式的目標檔案,CONSTRUCTORS明令告訴連結器把建構函式資訊放到出現CONSTRUCTORS命令的輸出段中。其它檔案格式中CONSTRUCTORS命令被忽略。
符號__CTOR_LIST__標記全域性建構函式的開始,符號__CTOR_END__標記結束。同樣的__DTOR_LIST__和__DTOR_END__標記全域性解構函式的開始和結束。第一個列表中的字是入口的數量,後面是每個建構函式或者解構函式的地址,最後是一個全零的字。編譯器必須安排實際執行程式碼。對於這些目標檔案格式,GNU C++通常從一個’__main’子程式中呼叫建構函式,而對’__main’的呼叫自動被插入到`main’的啟動程式碼中。GNU C++通常使用’atexit’執行解構函式,或者直接從函式’exit’中執行。
對於COFF或者ELF等支援任意段名字的目標檔案格式,GNU C++通常把全域性建構函式和解構函式放入.ctors和.dtors段。把下面的程式碼放入你的連結指令碼,將會建立GUN C++執行時期望的表。
__CTOR_LIST__ = .;
LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2)
*(.ctors)
LONG(0)
__CTOR_END__ = .;
__DTOR_LIST__ = .;
LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2)
*(.dtors)
LONG(0)
__DTOR_END__ = .;
如果你正在使用GUN C++支援的初始化優先順序,初始化優先順序提供了一些對全域性建構函式執行順序的控制,則你必須在連結時對建構函式排序以保證它們以正確的順序執行。當你使用CONSTRUCTORS命令,使用’SORT_BY_NAME(CONSTRUCTORS)’替換它。當使用.ctors和.dtors段,使用’*(SORT_BY_NAME(.ctors))’和’*(SORT_BY_NAME(.dtors))’取代’*(.ctors)’和’*(.dtors)’。
通常編譯器和連結器將會自動處理這些問題,並且你無需自己關注這些。但是,在你自己寫連結指令碼且正在使用C++的時候,你可能需要考慮這些。
3.6.7 Output Section Discarding
連結器通常不會建立沒有內容的輸出段。這是為了方便引用那些有可能出現或者不出現任何輸入檔案中的段。例如:
.foo : { *(.foo) }
將會僅當至少有一個輸入檔案含有’.foo’段且’.foo’段不為空的時候才會在輸出檔案建立一個’.foo’段。其它連結指令碼指出在一個段中間分配空間也會建立輸出段。此外也會為’.’分配,即便此分配沒有空間,除了’. = 0’,;. = . + 0’,;. = sym’,’. = . + sym’,’. = ALIGN (. != 0, expr, 1)’其中’sym’是一個值為0的已定義絕對符號。因此你可以強制一個空的輸出段使用’.=.’。
連結器會忽略在拋棄的輸出段內的地址設定(參考Output Section Address),除非連結指令碼在輸出段內定義了符號。這種情況下連結器會遵守地址賦值,有可能更新’.’即便段被拋棄了。
特殊輸出段名稱’/DISCARD/’可能被用來拋棄輸入段。一個被分派到名為’/DISCARD/’的輸出段的輸入段將不會被包含在輸出檔案中。
3.6.8 Output Section Attributes
此前我們顯示了完整的輸出段描述看起來像這樣:
section [address] [(type)] :
[AT(lma)]
[ALIGN(section_align)]
[SUBALIGN(subsection_align)]
[constraint]
{
output-section-command
output-section-command
...
} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]
我們已經解釋了section, address,以及output-section-command。在這章裡我們將解釋剩下的段屬性。
- Output Section Type: 輸出段型別
- Output Section LMA: 輸出段LMA(載入地址)
- Forced Output Alignment: 強制輸出對齊
- Forced Input Alignment: 強制輸入對齊
- Output Section Constraint: 輸出段限制
- Output Section Region: 輸出段區域
- Output Section Phdr: 輸出段phdr
- Output Section Fill: 輸出段填充
3.6.8.1 Output Section Type
每個輸出段都可能有個型別。型別為括號中的一個關鍵字。下面是已定義的型別:
NOLOAD
該段被標記為不要載入,因此程式執行時其將不會被載入到記憶體中。
**DSECT
COPY
INFO
OVERLAY**
這些型別名為了向下相容,很少被使用。他們都具有同樣的效果:段應被標記為不可分配,因此程式執行時不會為此段分配記憶體。
連結器通常基於對映到輸出段的輸入段的屬性設定屬性。你可以使用段型別過載這個屬性。例如,在下面的指令碼例子裡,’ROM’段被定位在地址0,且在程式執行時不會被載入。
SECTIONS {
ROM 0 (NOLOAD) : { ... }
...
}
3.6.8.2 Output Section LMA
每個段都有一個虛擬地址(VMA)以及一個載入地址(LMA);參考Basic Script Concepts。虛地址參見前面的Output Section Address。載入地址由AT或者AT>關鍵字設定。指出載入地址為可選的命令。
AT關鍵字把一個表示式當作自己的引數。這將指定段的實際載入地址。關鍵字AT>使用記憶體區域的名字作為引數。參考MEMORY。段的載入地址被設定為該區域的當前空閒位置,並且按照段對齊要求對齊。
如果沒有為可分配段使用AT和AT>,連結器會使用下面的嘗試方式來決定載入地址:
- 如果段有一個特定的VMA地址,則LMA也使用該地址。
- 如果段為不可分配的則LMA被設定為它的VMA。
- 否則如果可以找到符合當前段的一個記憶體區域,且此區域至少包含了一個段,則設定LMA在那裡。如此VMA和LMA的區別類似於VMA和LMA在該區域的上一個段的區別。
- 如果沒有宣告記憶體區域且預設區域覆蓋了整個地址空間,則採用前面的步驟。
- 如果找不到合適的區域或者沒有前面存在的段,則LMA被設定為等於VMA。
這個特性被設計成方便建立一個ROM映象。例如,下面的連結指令碼建立了三個輸出段:一個叫做’.text’從地址’0x1000’處開始,一個叫’.mdata’,儘管它的VMA是’0x2000’,它會被載入到’.text’段的後面,最後一個叫做’.bss’是用來放置未初始化的資料的,其地址從’0x3000’處開始。符號’_data’被定義為值’0x2000’, 它表示定位計數器的值是VMA的值,而不是LMA。
SECTIONS
{
.text 0x1000 : { *(.text) _etext = . ; }
.mdata 0x2000 :
AT ( ADDR (.text) + SIZEOF (.text) )
{ _data = . ; *(.data); _edata = . ; }
.bss 0x3000 :
{ _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
}
此連結指令碼的執行時初始化程式碼應該類似於下面的形式,把初始化資料從ROM映象複製到執行時地址。注意這些程式碼是如何利用好連線指令碼定義的符號的。
extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext;
char *dst = &_data;
/* ROM has data at end of text; copy it. */
while (dst < &_edata)
*dst++ = *src++;
/* Zero bss. */
for (dst = &_bstart; dst< &_bend; dst++)
*dst = 0;
3.6.8.3 Forced Output Alignment
你可以使用ALIGN增加輸出段的對齊。作為替換,你可以通過ALIGN_WITH_INPUT屬性強制VMA與LMA自始至終保持它們之間的區別。
3.6.8.4 Forced Input Alignment
你可以使用SUBALIGN強制輸入段依照輸出段對齊。給出的值將會過載輸入段的設定,無論比原來大還是小。
3.6.8.5 Output Section Constraint
你可以特定一個輸出段只有在所有輸入段都為只讀的情況下才能生成,或者所有輸入段都是可讀寫的,分別對應ONLY_IF_RO和ONLY_IF_RW。
3.6.8.6 Output Section Region
可以使用’>region’把一個段指定到此前設定的記憶體區域內。參見MEMORY。
下面是一個例子:
MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }
SECTIONS { ROM : { *(.text) } >rom }
3.6.8.7 Output Section Phdr
你可以使用’:phdr’把一個段指定到此前定義的程式段內。參考PHDRS。如果一個段被分派到一個或者更多的片斷中,則所有的後續可分配段將被同樣分配到這些地方,除非顯式使用了:phdr修飾語。你可以使用:NONE告訴連結器不要把段放到任何片斷中。
下面是一個例子:
PHDRS { text PT_LOAD ; }
SECTIONS { .text : { *(.text) } :text }
3.6.8.8 Output Section Fill
你可以使用’=fillexp’為整個段設定填充模板。fillexp是一個表示式(參考Expressions)。任何其它的未被特殊指定的輸出段的記憶體區域(例如,因為對其輸入段產生的縫隙)將會被用fillexp的值填充,如果有需要可以重複填充。如果表示式是一個簡單的hex數字,例如一個十六進位制數字由’0x’開頭且結尾沒有’k’或’M’,則一個任意長的十六進位制數字可以被用來給填充模板賦值,前面的0同樣成為模板的一部分。在其它情況中,包含額外的括號或者一個一元+,填充模板為表示式值的最低4個有意義的位元組。在所有情況中,數字總是大端的。
你也可以使用FILL命令設定填充值(參考Output Section Data)。
下面是一個例子:
SECTIONS { .text : { *(.text) } =0x90909090 }
3.6.9 Overlay Description
一個覆蓋描述提供了一種簡單的方法用於描述一個要被作為一個單獨記憶體映像的一部分載入記憶體,但是卻要在同一個記憶體地址執行的段。在執行時,一些種類的覆蓋管理器將會根據需要把覆蓋段複製進入或者移出執行時記憶體,可能僅是簡單的處理記憶體位。這個功能可能很有用,例如,當某個記憶體區域比其它區域快的多。
覆蓋描述使用OVERLAY命令。OVERLAY命令和SECTIONS命令一起使用,就像一個輸出段描述符。完整的OVERLAY命令的語義如下:
OVERLAY [start] : [NOCROSSREFS] [AT ( ldaddr )]
{
secname1
{
output-section-command
output-section-command
...
} [:phdr...] [=fill]
secname2
{
output-section-command
output-section-command
...
} [:phdr...] [=fill]
...
} [>region] [:phdr...] [=fill] [,]
所有的部分都是可選的,除了OVERLAY(關鍵字),以及每個段都必須有一個名字(上面的secname1和secname2)。使用OVERLAY結構定義的段類似於那些普通的SECTIONS中的結構(參考SECTIONS),除了OVERLAY中不能為段定義地址和記憶體區域。
結尾的逗號可能會被使用,如果使用了fill且下一個sections-command看起來像是表示式的延續。
所有的段都使用同樣的開始地址定義。所有段的載入地址都被排布,使它們在記憶體中從整個’OVERLAY’的載入地址開始都是連續的(就像普通的段定義,載入地址是可選的,預設的就是開始地址;開始地址也是可選的,預設是當前的位置計數器的值)。
如果使用了關鍵字NOCROSSREFS,並且在任何段間有互相引用,連結器將會產生一個錯誤報告。因為所有的段執行在同樣的地址,直接引用其它的段通常沒有任何意義。參考NOCROSSREFS。
每個伴隨OVERLAY的段,連結器自動提供兩個符號。符號__load_start_secname被定義為段的起始地址。符號__load_stop_secname被定義為段結束地址。任何不符合C定義的伴隨secname的字元都將被移除。C(或者彙編)程式碼可以使用這些符號在需要時搬移覆蓋程式碼。
覆蓋之後,位置計數器的值設定為覆蓋的起始值加上最大段的長度。
下面是例子,請記住這應該放在SECTIONS結構內。
OVERLAY 0x1000 : AT (0x4000)
{
.text0 { o1/*.o(.text) }
.text1 { o2/*.o(.text) }
}
這將把’.text0’和’.text1’的起始地址設定為地址0x1000。’.text0’的載入地址為0x4000,’.text1’會載入到’.text0’後面。下面的符號如果被引用則會被定義: __load_start_text0, __load_stop_text0, __load_start_text1, __load_stop_text1。
C程式碼拷貝覆蓋.text1到覆蓋區域可能像下面的形式。
extern char __load_start_text1, __load_stop_text1;
memcpy ((char *) 0x1000, &__load_start_text1,
&__load_stop_text1 - &__load_start_text1);
注意’OVERLAY’命令只是為了語法上的便利,因為它所做的所有事情都可以用更加基本的命令加以代替。上面的例子可以用下面的寫法:
.text0 0x1000 : AT (0x4000) { o1/*.o(.text) }
PROVIDE (__load_start_text0 = LOADADDR (.text0));
PROVIDE (__load_stop_text0 = LOADADDR (.text0) + SIZEOF (.text0));
.text1 0x1000 : AT (0x4000 + SIZEOF (.text0)) { o2/*.o(.text) }
PROVIDE (__load_start_text1 = LOADADDR (.text1));
PROVIDE (__load_stop_text1 = LOADADDR (.text1) + SIZEOF (.text1));
. = 0x1000 + MAX (SIZEOF (.text0), SIZEOF (.text1));
3.7 MEMORY Command
連結器預設的設定允許分配所有可用的記憶體。你通過MEMORY命令可以過載這些。
MEMORY命令描述了一個記憶體塊在目標中的位置和大小。你可以使用它描述一個可能會在連結器中使用的記憶體區域,以及那些必須避免使用的記憶體區域。此後你可以把段放到特定的記憶體區域裡。連結器將會基於記憶體區域設定段地址,如果區域趨於飽和將會產生警告資訊。連結器不會為了把段更好的放入記憶體區域而打亂段的順序。
一個連結指令碼可能含有許多MEMORY命令,但是,所有定義的記憶體塊都被當作他們是在一個MEMORY命令中定義的一樣。MEMORY的語法是:
MEMORY
{
name [(attr)] : ORIGIN = origin, LENGTH = len
...
}
name是連結指令碼用來引用記憶體區域的名字。區域名在連結指令碼外部沒有任何意義。區域名被儲存在一個獨立的名字空間,且不會與符號名,檔名,或者段名起衝突。每個記憶體區域必須在MEMORY命令中有一個不同的名字。但是你此後可以使用REGION_ALIAS命令為已存在的記憶體區域新增別名。
attr字元是一個可選的屬性列表,用來決定是否讓一個指令碼中沒有顯式指定對映的輸入段使用一個特定的記憶體區域。就