1. 概論
每一個鏈接過程都由鏈接腳本(linker script, 一般以lds作為文件的后綴名)控制. 鏈接腳本主要用於規定如何把輸入文件內的段放入輸出文件內, 並控制輸出文件內各部分在程序地址空間內的布局. 但你也可以用連接命令做一些其他事情.
2. 基本概念
鏈接器把一個或多個輸入文件合成一個輸出文件.
輸入文件: 目標文件或鏈接腳本文件.
輸出文件: 目標文件或可執行文件.
目標文件(包括可執行文件)具有固定的格式, 在UNIX或GNU/Linux平台下, 一般為ELF格式. 若想了解更多, 可參考 UNIX/Linux平台可執行文件格式分析
有時把輸入文件內的段稱為輸入段(input 段), 把輸出文件內的段稱為輸出段(output sectin).
目標文件的每個段至少包含兩個信息: 名字和大小. 大部分段還包含與它相關聯的一塊數據, 稱為段 contents(段內容). 一個段可被標記為“loadable(可加載的)”或“allocatable(可分配的)”.
loadable 段: 在輸出文件運行時, 相應的段內容將被載入進程地址空間中,如.text段、.data段。
allocatable 段: 內容為空的段可被標記為“可分配的”. 在輸出文件運行時, 在進程地址空間中空出大小同段指定大小的部分. 某些情況下, 這塊內存必須被置零。如.bss段。
如果一個段不是“可加載的”或“可分配的”,那么該段通常包含了調試信息.,可用objdump -h命令查看相關信息。
- 每個“可加載的”或“可分配的”輸出段通常包含兩個地址
第一個是'VMA'或稱為虛擬內存地址. 這是當輸出文件運行時,段所擁有的地址.
第二個是‘LMA', 或稱為載入內存地址. 這個段即將要載入的內存地址.
一般而言, 某段的VMA == LMA。 但在嵌入式系統中, 經常存在加載地址和執行地址不同的情況: 比如將輸出文件加載到開發板的flash中(由LMA指定), 而在運行時將位於flash中的輸出文件復制到SDRAM中(由VMA指定).
符號(symbol): 每個目標文件都有符號表(SYMBOL TABLE),包含已定義的符號(對應全局變量和static變量和定義的函數的名字)和未定義符號(未定義的函數的名字和引用但沒定義的符號)信息.
符號值: 每個符號對應一個地址, 即符號值(這與c程序內變量的值不一樣, 某種情況下可以把它看成變量的地址). 可用nm命令查看它們.
3. 腳本格式
鏈接腳本由一系列命令組成, 每個命令由一個關鍵字(一般在其后緊跟相關參數)或一條對符號的賦值語句組成. 命令由分號‘;’分隔開.
文件名或格式名內如果包含分號’;'或其他分隔符, 則要用引號‘”’將名字全稱引用起來. 無法處理含引號的文件名.
/* */之間的是注釋。
4. 簡單例子
在介紹鏈接描述文件的命令之前, 先看看下述的簡單例子:
以下腳本將輸出文件的text 段定位在0×10000, data 段定位在0×8000000:
SECTIONS
{
. = 0×10000;
.text : { *(.text) }
. = 0×8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
解釋一下上述的例子:
. = 0×10000 : 把定位器符號置為0×10000 (若不指定, 則該符號的初始值為0).
.text : { *(.text) } : 將所有(*符號代表任意輸入文件)輸入文件的.text 段合並成一個.text 段, 該段的地址由定位器符號的值指定, 即0×10000.
. = 0×8000000 :把定位器符號置為0×8000000
.data : { *(.data) } : 將所有輸入文件的.data 段合並成一個.data 段, 該段的地址被置為0×8000000.
.bss : { *(.bss) } : 將所有輸入文件的.bss 段合並成一個.bss 段,該段的地址被置為0×8000000+.data 段的大小.
連接器每讀完一個段描述后, 將定位器符號的值*增加*該段的大小. 注意: 此處沒有考慮對齊約束.
5. 簡單腳本命令
- 1 -ENTRY(SYMBOL) : 將符號SYMBOL的值設置成入口地址。
入口地址(entry point): 進程執行的第一條用戶空間的指令在進程地址空間的地址)
ld有多種方法設置進程入口地址, 按一下順序: (編號越前, 優先級越高)
1, ld命令行的-e選項
2, 連接腳本的ENTRY(SYMBOL)命令
3, 如果定義了start符號, 使用start符號值
4, 如果存在.text 段, 使用.text 段的第一字節的位置值
5, 使用值0
- 2 -INCLUDE filename : 包含其他名為filename的鏈接腳本
相當於c程序內的的#include指令, 用以包含另一個鏈接腳本.
腳本搜索路徑由-L選項指定. INCLUDE指令可以嵌套使用, 最大深度為10. 即: 文件1內INCLUDE文件2, 文件2內INCLUDE文件3… , 文件10內INCLUDE文件11. 那么文件11內不能再出現 INCLUDE指令了.
- 3 -INPUT(files): 將括號內的文件做為鏈接過程的輸入文件
ld首先在當前目錄下尋找該文件,如果沒找到, 則在由-L指定的搜索路徑下搜索。 file可以為 -lfile形式,就象命令行的-l選項一樣, 如果該命令出現在暗含的腳本內,則該命令內的file在鏈接過程中的順序由該暗含的腳本在命令行內的順序決定.
- 4 -GROUP(files) : 指定需要重復搜索符號定義的多個輸入文件
除了file必須是庫文件以外,該命令與INPUT相似, 且file文件作為一組被ld重復掃描,直到不在有新的未定義的引用出現。
- 5 -OUTPUT(FILENAME) : 定義輸出文件的名字
同ld的-o選項, 不過-o選項的優先級更高. 所以它可以用來定義默認的輸出文件名. 如a.out
- 6 -SEARCH_DIR(PATH) :定義搜索路徑,
同ld的-L選項, 不過由-L指定的路徑要比它定義的優先被搜索。
- 7 -STARTUP(filename) : 指定filename為第一個輸入文件
在鏈接過程中, 每個輸入文件是有順序的. 此命令設置文件filename為第一個輸入文件。就象這個文件是在命令行上第一個被指定的文件一樣, 如果在一個系統中,,入口點總是存在於第一個文件中,那這個就很有用。
- 8 – OUTPUT_FORMAT(BFDNAME) : 設置輸出文件使用的BFD格式
同ld選項-o format BFDNAME, 不過ld選項優先級更高.
- 9 -OUTPUT_FORMAT(DEFAULT,BIG,LITTLE) : 定義三種輸出文件的格式(大小端)
對於此命令,要在命令行中使用-EB或-EL選項來指定不同的輸出文件格式
如果'-EB'和'-EL'都沒有使用, 那輸出格式會是第一個參數 DEFAULT,
如果使用了'-EB',輸出格式會是第二個參數 BIG,
如果使用了'-EL', 輸出格式會是第三個參數, LITTLE.
比如:缺省的基於 MIPS ELF 平台連接腳本使用如下命令:
OUTPUT_formAT(elf32-bigmips, elf32-bigmips, elf32-littlemips)
這表示缺省的輸出文件格式是'elf32-bigmips', 但是當用戶使用'-EL'命令行選項的時候, 輸出文件就會被以`elf32-littlemips'格式創建.
- 10- TARGET(BFDNAME):設置輸入文件的BFD格式
同ld選項-b BFDNAME. 若使用了TARGET命令, 但未使用OUTPUT_FORMAT命令, 則最用一個TARGET命令設置的BFD格式將被作為輸出文件的BFD格式.
- 其他鏈接腳本命令:
ASSERT(EXP, MESSAGE): 如果EXP不為真,終止連接過程
EXTERN(SYMBOL SYMBOL …):在輸出文件中增加未定義的符號,如同連接器選項-u
FORCE_COMMON_ALLOCATION:為common symbol(通用符號)分配空間,即使用了-r連接選項也為其分配
NOCROSSREFS(SECTION SECTION …):檢查列出的輸出段,如果發現他們之間有相互引用,則報錯。對於某些系統,特別是內存較緊張的嵌入式系統,某些段是不能同時存在內存中的,所以他們之間不能相互引用。
OUTPUT_ARCH(BFDARCH):設置輸出文件的machine architecture(體系結構),BFDARCH為被BFD庫使用的名字之一。可以用命令objdump -f查看。
可通過 man -S 1 ld查看ld的聯機幫助, 里面也包括了對這些命令的介紹.
6. 對符號的賦值
在目標文件內定義的符號可以在鏈接腳本內被賦值. (注意和C語言中賦值的不同!) 此時該符號被定義為全局的, 每個符號都對應了一個地址, 此處的賦值是更改這個符號對應的地址.
注意:在鏈接腳本中給符號賦值,賦的是符號對應的地址值,而不是普通的值。
e.g. 通過下面的程序查看變量a的地址:
/* a.c */
#include
int a = 100;
int main(void)
{
printf( “&a=0x%p “, &a );
return 0;
}
$ gcc -Wall -o a-without-lds a.c
&a = 0×8049598
/* a.lds */
a = 3;
$ gcc -Wall -o a-with-lds a.c a.lds
&a = 0×3
注意: 對符號的賦值只對全局變量起作用!
一些簡單的賦值語句
能使用任何c語言內的賦值操作:
SYMBOL = EXPRESSION ;
SYMBOL += EXPRESSION ;
SYMBOL -= EXPRESSION ;
SYMBOL *= EXPRESSION ;
SYMBOL /= EXPRESSION ;
SYMBOL <<= EXPRESSION ;
SYMBOL >>= EXPRESSION ;
SYMBOL &= EXPRESSION ;
SYMBOL |= EXPRESSION ;
第一個情況會把 SYMBOL 定義為值 EXPRESSION.
其它情況下, SYMBOL 必須是已經定義了的,而值會作出相應的調整.
. 是一個特殊的符號,它是定位器,一個位置指針,指向程序地址空間內的某位置,該符號只能在SECTIONS命令內使用
注意:賦值語句包含4個語法元素:符號名、操作符、表達式、分號;一個也不能少。
被賦值后,符號所屬的段被設置為表達式EXPRESSION所屬的段(參看11. 腳本內的表達式)
賦值語句可以出現在連接腳本的三處地方:單獨的部分,SECTIONS命令內,SECTIONS命令內的段描述內;如下,
floating_point = 0; /* 全局位置 */
SECTIONS
{
.text :
{
*(.text)
_etext = . ; /* 段描述內 */
}
_bdata = (. + 3) & ~ 4; /* SECTIONS命令內 */
.data : { *(.data) }
}
PROVIDE關鍵字
該關鍵字用於定義這類符號:在目標文件內被引用,但沒有在任何目標文件內被定義的符號。
例子:
SECTIONS
{
.text :
{
*(.text)
_etext = .;
PROVIDE(etext = .);
}
}
在這個例子中,如果程序定義了一個'_etext'(帶有一個前導下划線),,連接器會給出一個重定義錯誤。當程序內引用etext符號時,如果程序內定義了etext,則默認程序中的定義。如果沒定義,etext符號對應的地址被定義為.text 段之后的第一個字節的地址。
7. SECTIONS命令
SECTIONS命令告訴連接器如何把輸入段映射到輸出段, 並如何把輸出段放入到內存中.
該命令格式如下:
SECTIONS
{
SECTIONS-COMMAND
SECTIONS-COMMAND
…
}
SECTION-COMMAND有四種:
(1) ENTRY命令
(2) 符號賦值語句
(3) 一個輸出段的描述(output 段 description)
(4) 一個段疊加描述(overlay description)
- 'ENTRY'命令和符號賦值在'SECTIONS'命令中是允許的, 這是為了方便在這些命令中使用定位計數器. 這也可以讓連接腳本更容易理解, 因為你可以在更有意義的地方使用這些命令來控制輸出文件的布局.
如果整個連接腳本內沒有SECTIONS命令, 那么鏈接器將所有同名輸入段合成為一個輸出段內, 各輸入段的順序為它們被連接器發現的順序.
如果某輸入段沒有在SECTIONS命令中提到,那么該段將被直接拷貝成輸出段。
- 輸出段描述和重疊描述在下面描述.
輸出段描述具有如下格式:
SECTION [ADDRESS] [(TYPE)] : [AT(LMA)]
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
…
} [>REGION] [AT>LMA_REGION] [:PHDR HDR...] [=FILLEXP]
注意:這里SECTION和SECTIONS命令不一樣。SECTION是SECTIONS命令內的一個輸出段描述符
[ ]內的內容為可選選項, 一般不需要.
SECTION:段名字
SECTION左右的空白、圓括號、冒號是必須的,換行符和其他空格是可選的。
每個OUTPUT-SECTION-COMMAND為以下四種之一,
- 符號賦值語句
- 一個輸入段描述
- 直接包含的數據值
- 一個特殊的輸出段關鍵字
(1)輸出段名字(SECTION):
輸出段名字必須符合輸出文件格式要求,比如:a.out格式的文件只允許存在.text、.data和.bss 段名。
而有的格式只允許存在數字名字,那么此時應該用引號將所有名字內的數字組合在一起;
另外,還有一些格式允許任何序列的字符存在於 段名字內,此時如果名字內包含特殊字符(比如空格、逗號等),那么需要用引號將其組合在一起。
- 輸出段地址(ADDRESS):
ADDRESS是一個表達式,它的值用於設置VMA。
如果你不提供 ADDRESS, 連接器會基於 REGION(如果存在)設置它,或者基於定位計數器的當前值.
如果你提供了 ADDRESS, 那輸出段的地址會被精確地設為這個值.
如果你既不提供 ADDRESS 也不提供 REGION, 那輸出節段的地址會被設為當前的定位計數器向上對齊到輸出段需要的對齊邊界的值.
例子:
.text . : { *(.text) }
和
.text : { *(.text) }
第一個會把'.text'輸出段的地址,設為當前定位計數器的值.
第二個會把它設為定位計數器的當前值向上對齊到'.text'輸入段中對齊要求最嚴格的一個邊界.
ADDRESS可以是一個任意表達式:
比如,如果你需要把節對齊一個字的邊界,這樣就可以讓低四字節的節地址值為零, 你可以這樣做:
.text ALIGN(0x10) : { *(.text) }
這個語句可以正常工作,因為'ALIGN'返回 定位計數器對齊到0x10邊界后的值 。
指定一個節的地址會改變定位計數器的值。
(2)輸入段描述:
最常見的輸出段描述命令是輸入段描述。
輸入段描述是最基本的連接腳本描述。
輸入段描述基礎:
一個輸入段描述,由一個文件名后跟有可選的括號中的段名列表組成。文件名和段名可以通配符形式出現。
例如:*(.text) 包含所有文件的.text段
基本語法:FILENAME([EXCLUDE_FILE (FILENAME1 FILENAME2 ...) SECTION1 SECTION2 ...)
FILENAME文件名,可以是一個特定的文件的名字,也可以是一個字符串模式。
SECTION名字,可以是一個特定的段名字,也可以是一個字符串模式
例子是最能說明問題的:
*(.text) : 表示所有輸入文件的.text 段
(*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors)) :表示除crtend.o、otherfile.o文件外的所有輸入文件的.ctors 段。
data.o(.data) : 表示data.o文件的.data 段
data.o : 表示data.o文件的所有段
*(.text .data) : 表示所有文件的.text 段和.data 段,順序是:第一個文件的.text 段,第一個文件的.data 段,第二個文件的.text 段,第二個文件的.data 段,...
*(.text) *(.data) : 表示所有文件的.text 段和.data 段,順序是:第一個文件的.text 段,第二個文件的.text 段,...,最后一個文件的.text 段,第一個文件的.data 段,第二個文件的.data 段,...,最后一個文件的.data 段
下面看連接器是如何找到對應的文件的。
當FILENAME是一個特定的文件名時,連接器會查看它是否在連接命令行內出現或在INPUT命令中出現。
當FILENAME是一個字符串模式時,連接器僅僅只查看它是否在連接命令行內出現。
注意:如果連接器發現某文件在INPUT命令內出現,那么它會在-L指定的路徑內搜尋該文件。
(3)字符串模式內可存在以下通配符:
* :表示任意多個字符
? :表示任意一個字符
[CHARS] :表示任意一個CHARS內的字符,可用-號表示范圍,如:a-z
:表示引用下一個緊跟的字符
在文件名內,通配符不匹配文件夾分隔符/,但當字符串模式僅包含通配符*時除外。
任何一個文件的任意段只能在SECTIONS命令內出現一次。看如下例子,
SECTIONS {
.data : { *(.data) }
.data1 : { data.o(.data) }
}
data.o文件的.data 段在第一個OUTPUT-SECTION-COMMAND命令內被使用了,那么在第二個OUTPUT-SECTION-COMMAND命令內將不會再被使用,也就是說即使連接器不報錯,輸出文件的.data1 段的內容也是空的。
再次強調:連接器依次掃描每個OUTPUT-SECTION-COMMAND命令內的文件名,任何一個文件的任何一個段都只能使用一次。
讀者可以用-M連接命令選項來產生一個map文件,它包含了所有輸入段到輸出段的組合信息。
再看個例子,
SECTIONS {
.text : { *(.text) }
.DATA : { [A-Z]*(.data) }
.data : { *(.data) }
.bss : { *(.bss) }
}
這個例子中說明,所有文件的輸入.text 段組成輸出.text 段;所有以大寫字母開頭的文件的.data 段組成輸出.DATA 段,其他文件的.data 段組成輸出.data 段;所有文件的輸入.bss 段組成輸出.bss 段。
可以用SORT()關鍵字對滿足字符串模式的所有名字進行遞增排序,如SORT(.text*)。
(4)通用符號(common symbol)的輸入段:
在許多目標文件格式中,通用符號並沒有占用一個段。連接器認為:輸入文件的所有通用符號在名為COMMON的段內。
例子:
.bss { *(.bss) *(COMMON) }
這個例子中將所有輸入文件的所有通用符號放入輸出.bss 段內。可以看到COMMOM 段的使用方法跟其他段的使用方法是一樣的。
有些目標文件格式具有多於一個的普通符號。 比如, MIPS ELF 目標文件格式區分標准普通符號和小普通符號。
1)、在 MIPS ELF 的情況中, 連接器為標准普通符號使用COMMON, 並且為小普通符號使用.common。這就允許你把不同類型的普通符號映射到內存的不同位置。
2)、在一些老的連接腳本上,你有時會看到[COMMON]。這個符號現在已經過時了, 它等效於*(COMMON),不建議繼續使用這種陳舊的方式。
(5)輸入段和垃圾回收:
在連接命令行內使用了選項 --gc-sections后,連接器可能將某些它認為沒用的段過濾掉,此時就有必要強制連接器保留一些特定的段,可用KEEP()關鍵字達此目的。如KEEP(*(.text))或KEEP(SORT(*)(.text))
最后看個簡單的輸入段相關例子:
SECTIONS {
outputa 0×10000 :
{
all.o
foo.o (.input1)
}
outputb :
{
foo.o (.input2)
foo1.o (.input1)
}
outputc :
{
*(.input1)
*(.input2)
}
}
它告訴連接器去讀取文件'all.o'中的所有段,並把它們放到輸出段
'outputa'的開始位置處, 該輸出段是從位置'0x10000'處開始的。
從文件'foo.o'中來的所有段'.input1'在同一個輸出段中緊密排列。
從文件'foo.o'中來的所有段'.input2'全部放入到輸出段'outputb'中, 后面跟上從'foo1.o'中來的段'.input1'。
來自所有文件的所有余下的'.input1'和'.input2'節被寫入到輸出段'outputc'中。
(6)在輸出段存放數據命令:
能夠顯示地在輸出段內填入你想要填入的信息(這樣是不是可以自己通過連接腳本寫程序?當然是簡單的程序)。
BYTE(EXPRESSION) 1 字節
SHORT(EXPRESSION) 2 字節
LOGN(EXPRESSION) 4 字節
QUAD(EXPRESSION) 8 字節
SQUAD(EXPRESSION) 64位處理器的代碼時,8 字節
輸出文件的字節順序big endianness 或little endianness,可以由輸出目標文件的格式決定;如果輸出目標文件的格式不能決定字節順序,那么字節順序與第一個輸入文件的字節順序相同。
當使用 64 位系統時,‘QUAD’和‘SQUAD’是相同的;它們都會存儲 8 字段,或者說是 64 位的值。而如果軟硬件系統都是 32 位的,一個表達式就會被作為 32 位計算。在這種情況下,‘QUAD’存儲一個 32 位值,並把它零擴展到 64 位, 而‘SQUAD’會把 32 位值符號擴展到 64 位。
如:BYTE(1)、LANG(addr)。
注意,這些命令只能放在輸出段描述內,其他地方不行。
錯誤:SECTIONS { .text : { *(.text) } LONG(1) .data : { *(.data) } }
正確:SECTIONS { .text : { *(.text) LONG(1) } .data : { *(.data) } }
在當前輸出段內可能存在未描述的存儲區域(比如由於對齊造成的空隙),可以用FILL(EXPRESSION)命令決定這些存儲區域的內容, EXPRESSION的前兩字節有效,這兩字節在必要時可以重復被使用以填充這類存儲區域。如FILE(0×9090)。在輸出段描述中可以有=FILEEXP屬性,它的作用如同FILE()命令,但是FILE命令只作用於該FILE指令之后的段區域,而=FILEEXP屬性作用於整個輸出段區域,且FILE命令的優先級更高!!!
這個例子顯示如何在未被指定的內存區域填充'0x90':
FILL(0x90909090)
(7)輸出段內命令的關鍵字:
有兩個關鍵字作為輸出段命令的形式出現:
CREATE_OBJECT_SYMBOLS :為每個輸入文件建立一個符號,符號名為輸入文件的名字。每個符號所在的段就是’CREATE_OBJECT_SYMBOLS'命令出現的那個段。
CONSTRUCTORS :與c++內的(全局對象的)構造函數和(全局對像的)析構函數相關,下面將它們簡稱為全局構造和全局析構。
對於a.out目標文件格式,連接器用一些不尋常的方法實現c++的全局構造和全局析構。當連接器生成的目標文件格式不支持任意段名字時,比如說ECOFF、XCOFF格式,連接器將通過名字來識別全局構造和全局析構,對於這些文件格式,連接器把與全局構造和全局析構的相關信息放入出現 CONSTRUCTORS關鍵字的輸出段內。
符號__CTORS_LIST__表示全局構造信息的的開始處,__CTORS_END__表示全局構造信息的結束處。
符號__DTORS_LIST__表示全局構造信息的的開始處,__DTORS_END__表示全局構造信息的結束處。
這兩塊信息的開始處是一字長的信息,表示該塊信息有多少項數據,然后以值為零的一字長數據結束。
一般來說,GNU C++在函數__main內安排全局構造代碼的運行,而__main函數被初始化代碼(在main函數調用之前執行)調用。是不是對於某些目標文件格式才這樣???
對於支持任意段名的目標文件格式,比如COFF、ELF格式,GNU C++將全局構造和全局析構信息分別放入.ctors 段和.dtors 段內,然后在連接腳本內加入如下,
__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__ = .;
如果使用GNU C++提供的初始化優先級支持(它能控制每個全局構造函數調用的先后順序),那么請在連接腳本內把CONSTRUCTORS替換成SORT (CONSTRUCTS),把*(.ctors)換成*(SORT(.ctors)),把*(.dtors)換成*(SORT(.dtors))。一般來說,默認的連接腳本已作好的這些工作。
(8)輸出段的丟棄:
1)連接器不會創建那些不含有任何內容的輸出段。 這是為了引用那些可能出現或不出現在任何輸入文件中的輸入段時方便。比如:
.foo { *(.foo) }
如果至少在一個輸入文件中有'.foo'段,它才會在輸出文件中創建一個'.foo'段
如果你使用了其它的而不是一個輸入段描述作為一個輸出段命令, 比如一個符號賦值, 那這個輸出段總是被創建,即使沒有匹配的輸入段也會被創建。
2)一個特殊的輸出段名`/DISCARD/'可以被用來丟棄輸入段。
任何被分配到名為`/DISCARD/'的輸出段中的輸入段不包含在輸出文件中。
(9)輸出段屬性:
我們再回顧以下輸出段描述的文法:
SECTION [ADDRESS] [(TYPE)] : [AT(LMA)]
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
…
} [>REGION] [AT>LMA_REGION] [:PHDR HDR...] [=FILLEXP]
前面我們瀏覽了SECTION、ADDRESS、OUTPUT-SECTION-COMMAND相關信息,下面我們將瀏覽其他屬性。
- TYPE :
每個輸出段都有一個類型,如果沒有指定TYPE類型,那么連接器根據輸出段引用的輸入段的類型設置該輸出段的類型。它可以為以下五種值,
NOLOAD :該段在程序運行時,不被載入內存。
DSECT,COPY,INFO,OVERLAY :這些類型很少被使用,為了向后兼容才被保留下來。這種類型的段必須被標記為“不可加載的”,以便在程序運行不為它們分配內存。如.bss段
- 輸出段的LMA :
默認情況下,LMA等於VMA,但可以通過關鍵字AT()指定LMA。
用關鍵字AT()指定,括號內包含表達式,表達式的值用於設置LMA。如果不用AT()關鍵字,那么可用AT>LMA_REGION表達式設置指定該段加載地址的范圍。
這個屬性主要用於構件ROM境象。
下面的連接腳本創建了三個輸出段:
一個叫做‘.text’從地址‘0x1000’處開始,
一個叫‘.mdata’,盡管它的 VMA 是'0x2000',它會被載入到'.text'段的后面,
最后一個叫做‘.bss’是用來放置未初始化的數據的,其地址從'0x3000'處開始。
符號'_data'被定義為值'0x2000', 它表示定位計數器的值是 VMA 的值,而不是 LMA。
例:
SECTIONS
{
.text 0×1000 : { *(.text) _etext = . ; }
.mdata 0×2000 : AT ( ADDR (.text) + SIZEOF (.text) )
{ _data = . ; *(.data); _edata = . ; }
.bss 0×3000 :
{ _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;
- 輸出段區域:
可以將輸出段放入預先定義的內存區域內,例子,
MEMORY { rom : ORIGIN = 0×1000, LENGTH = 0×1000 }
SECTIONS { ROM : { *(.text) } >rom }
- 輸出段所在的程序段:
可以將輸出段放入預先定義的程序段(program segment)內。如果某個輸出段設置了它所在的一個或多個程序段,那么接下來定義的輸出段的默認程序段與該輸出 段的相同。除非再次顯示地指定。例子,
PHDRS { text PT_LOAD ; }
SECTIONS { .text : { *(.text) } :text }
可以通過:NONE指定連接器不把該段放入任何程序段內。詳情請查看PHDRS命令
- 輸出段的填充模版:
這個在前面提到過,任何輸出段描述內的未指定的內存區域(比如,因為輸入段的對齊要求而產生的裂縫),連接器用該模版填充該區域。用法:=FILEEXP,前兩字節有效,當區域大於兩字節時,重復使用這兩字節以將其填滿。例子,
SECTIONS { .text : { *(.text) } =0×9090 }
(10)覆蓋圖(overlay)描述:
覆蓋圖描述使兩個或多個不同的段占用同一塊程序地址空間。覆蓋圖管理代碼負責將段的拷入和拷出。考慮這種情況,當某存儲塊的訪問速度比其他存儲塊要快時,那么如果將段拷到該存儲塊來執行或訪問,那么速度將會有所提高,覆蓋圖描述就很適合這種情形。文法如下,
SECTIONS {
…
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]
…
}
由以上文法可以看出,同一覆蓋圖內的段具有相同的VMA。SECNAME2的LMA為SECTNAME1的LMA加上SECNAME1的大小,同理計算SECNAME2,3,4…的LMA。SECNAME1的LMA由LDADDR決定,如果它沒有被指定,那么由START決定,如果它也沒有被指定,那么由當前定位符號的值決定。
在 OVERLAY’結構中的段定義跟通常的‘SECTIONS’結構中的段定義是完全相同的,除了一點,就是在‘OVERLAY’中沒有地址跟內存區域的定義。
NOCROSSREFS關鍵字指定各段之間不能交叉引用,否則報錯。
對於OVERLAY描述的每個段,連接器將定義兩個符號__load_start_SECNAME和__load_stop_SECNAME,這兩個符號的值分別代表SECNAME 段的LMA地址的開始和結束。
連接器處理完OVERLAY描述語句后,將定位符號的值加上所有覆蓋圖內段大小的最大值。
看個例子吧,
SECTIONS{
…
OVERLAY 0×1000 : AT (0×4000)
{
.text0 { o1/*.o(.text) }
.text1 { o2/*.o(.text) }
}
…
}
.text0 段和.text1 段的VMA地址是0×1000,.text0 段加載於地址0×4000,.text1 段緊跟在其后。
程序代碼,拷貝.text1 段代碼,
extern char __load_start_text1, __load_stop_text1;
memcpy ((char *) 0×1000, &__load_start_text1,
&__load_stop_text1 – &__load_start_text1);
8. 內存區域命令
—————
注意:以下存儲區域指的是在程序地址空間內的。
在默認情形下,連接器可以為段分配任意位置的存儲區域。你也可以用MEMORY命令定義存儲區域,並通過輸出段描述的> REGION屬性顯示地將該輸出段限定於某塊存儲區域,當存儲區域大小不能滿足要求時,連接器會報告該錯誤。
MEMORY命令的文法如下,
MEMORY {
NAME1 [(ATTR)] : ORIGIN = ORIGIN1, LENGTH = LEN2
NAME2 [(ATTR)] : ORIGIN = ORIGIN2, LENGTH = LEN2
…
}
NAME :存儲區域的名字,這個名字可以與符號名、文件名、段名重復,因為它處於一個獨立的名字空間。
ATTR :定義該存儲區域的屬性,在講述SECTIONS命令時提到,當某輸入段沒有在SECTIONS命令內引用時,連接器會把該輸入 段直接拷貝成輸出段,然后將該輸出段放入內存區域內。如果設置了內存區域設置了ATTR屬性,那么該區域只接受滿足該屬性的段(怎么判斷該段是否滿足?輸出段描述內好象沒有記錄該段的讀寫執行屬性)。ATTR屬性內可以出現以下7個字符,
R 只讀段
W 讀/寫段
X 可執行段
A 可分配的’段
I 初始化了的段
L 同I
! 不滿足該字符之后的任何一個屬性的段
ORIGIN :關鍵字,區域的開始地址,可簡寫成org或o
LENGTH :關鍵字,區域的大小,可簡寫成len或l
例:
MEMORY
{
rom (rx) : ORIGIN = 0, LENGTH = 256K
ram (!rx) : org = 0×40000000, l = 4M
}
SECTIONS { ROM : { *(.text) } >rom }
此例中,把在SECTIONS命令內*未*引用的且具有讀屬性或寫屬性的輸入段放入rom區域內,把其他未引用的輸入段放入 ram。如果某輸出段要被放入某內存區域內,而該輸出段又沒有指明ADDRESS屬性,那么連接器將該輸出段放在該區域內下一個能使用位置。
9. PHDRS命令
————
該命令僅在產生ELF目標文件時有效。
ELF目標文件格式用program headers程序頭(程序頭內包含一個或多個segment程序段描述)來描述程序如何被載入內存。可以用objdump -p命令查看。
當在本地ELF系統運行ELF目標文件格式的程序時,系統加載器通過讀取程序頭信息以知道如何將程序加載到內存。要了解系統加載器如何解析程序頭,請參考ELF ABI文檔。
在連接腳本內不指定PHDRS命令時,連接器能夠很好的創建程序頭,但是有時需要更精確的描述程序頭,那么PAHDRS命令就派上用場了。
注意:一旦在連接腳本內使用了PHDRS命令,那么連接器**僅會**創建PHDRS命令指定的信息,所以使用時須謹慎。
PHDRS命令文法如下,
PHDRS
{
NAME TYPE [ FILEHDR ] [ PHDRS ] [ AT ( ADDRESS ) ]
[ FLAGS ( FLAGS ) ] ;
}
其中FILEHDR、PHDRS、AT、FLAGS為關鍵字。
NAME :為程序段名,此名字可以與符號名、段名、文件名重復,因為它在一個獨立的名字空間內。此名字只能在SECTIONS命令內使用。
一個程序段可以由多個‘可加載’的段組成。通過輸出段描述的屬性:PHDRS可以將輸出段加入一個程序段,: PHDRS中的PHDRS為程序段名。在一個輸出段描述內可以多次使用:PHDRS命令,也即可以將一個段加入多個程序段。
如果在一個輸出段描述內指定了:PHDRS屬性,那么其后的輸出段描述將默認使用該屬性,除非它也定義了:PHDRS屬性。顯然當多個輸出段屬於同一程序段時可簡化書寫。
在TYPE屬性后存在FILEHDR關鍵字,表示該段包含ELF文件頭信息;存在PHDRS關鍵字,表示該段包含ELF程序頭信息。
TYPE可以是以下八種形式,
PT_NULL 0
表示未被使用的程序段
PT_LOAD 1
表示該程序段在程序運行時應該被加載
PT_DYNAMIC 2
表示該程序段包含動態連接信息
PT_INTERP 3
表示該程序段內包含程序加載器的名字,在linux下常見的程序加載器是ld-linux.so.2
PT_NOTE 4
表示該程序段內包含程序的說明信息
PT_SHLIB 5
一個保留的程序頭類型,沒有在ELF ABI文檔內定義
PT_PHDR 6
表示該程序段包含程序頭信息。
EXPRESSION 表達式值
以上每個類型都對應一個數字,該表達式定義一個用戶自定的程序頭。
AT(ADDRESS)屬性定義該程序段的加載位置(LMA),該屬性將**覆蓋**該程序段內的段的AT()屬性。
默認情況下,連接器會根據該程序段包含的段的屬性(什么屬性?好象在輸出段描述內沒有看到)設置FLAGS標志,該標志用於設置程序段描述的p_flags域。
下面看一個典型的PHDRS設置,
PHDRS
{
headers PT_PHDR PHDRS ;
interp PT_INTERP ;
text PT_LOAD FILEHDR PHDRS ;
data PT_LOAD ;
dynamic PT_DYNAMIC ;
}
SECTIONS
{
. = SIZEOF_HEADERS;
.interp : { *(.interp) } :text :interp
.text : { *(.text) } :text
.rodata : { *(.rodata) } /* defaults to :text */
…
. = . + 0×1000; /* move to a new page in memory */
.data : { *(.data) } :data
.dynamic : { *(.dynamic) } :data :dynamic
…
}
10. 版本號命令
————–
當使用ELF目標文件格式時,連接器支持帶版本號的符號。
讀者可以發現僅僅在共享庫中,符號的版本號屬性才有意義。
動態加載器使用符號的版本號為應用程序選擇共享庫內的一個函數的特定實現版本。
可以在連接腳本內直接使用版本號命令,也可以將版本號命令實現於一個特定版本號描述文件(用連接選項–version-script指定該文件)。
該命令的文法如下,
VERSION { version-script-commands }
以下內容直接拷貝於以前的文檔,
===================== 開始 ==================================
內容簡介
———
0 前提
1 帶版本號的符號的定義
2 連接到帶版本的符號
3 GNU擴充
4 我的疑問
5 英文搜索關鍵字
6 我的參考
0. 前提
– 只限於ELF文件格式
– 以下討論用gcc
1. 帶版本號的符號的定義(共享庫內)
文件b.c內容如下,
int old_true()
{
return 1;
}
int new_true()
{
return 2;
}
寫連接器的版本控制腳本,本例中為b.lds,內容如下
VER1.0{
new_true;
};
VER2.0{
};
$gcc -c b.c
$gcc -shared -Wl,–version-script=b.lds -o libb.so b.o
可以在{}內填入要綁定的符號,本例中new_true符號就與VER1.0綁定了。
那么如果有一個應用程序連接到該庫的new_true符號,那么它連接的就是VER1.0版本的new_true符號
如果把b.lds更改為,
VER1.0{
};
VER2.0{
new_true;
};
然后在生成libb.so文件,在運行那個連接到VER1.0版本的new_true符號的應用程序,可以發現該應用程序不能運行了,
因為庫內沒有VER1.0版本的new_true,只有VER2.0版本的new_true。
2. 連接到帶版本的符號
寫一個簡單的應用(名為app)連接到libb.so,應用符號new_true
假設libb.so的版本控制文件為,
VER1.0{
};
VER2.0{
new_true;
};
$ nm app | grep new_true
U new_true@@VER1.0
$
用nm命令發現app連接到VER1.0版本的new_true
3. GNU的擴充
它允許在程序文件內綁定 *符號* 到 *帶版本號的別名符號*
文件b.c內容如下,
int old_true()
{
return 1;
}
int new_true()
{
return 2;
}
__asm__( “.symver old_true,true@VER1.0″ );
__asm__( “.symver new_true,true@@VER2.0″ );
其中,帶版本號的別名符號是true,其默認的版本號為VER2.0
供連接器用的版本控制腳本b.lds內容如下,
VER1.0{
};
VER2.0{
};
版本控制文件內必須包含版本VER1.0和版本VER2.0的定義,因為在b.c文件內有對他們的引用
****** 假定libb.so與app.c在同一目錄下 ********
以下應用程序app.c連接到該庫,
int true();
int main()
{
printf( “%d “, true );
}
$ gcc app.c libb.so
$ LD_LIBRARY_PATH=. ./app
2
$ nm app | grep true
U true@@VER2.0
$
很明顯,程序app使用的是VER2.0版本的別名符號true,如果在b.c內沒有指明別名符號true的默認版本,
那么gcc app.c libb.so將出現連接錯誤,提示true沒有定義。
也可以在程序內指定特定版本的別名符號true,程序如下,
__asm__( “.symver true,true@VER1.0″ );
int true();
int main()
{
printf( “%d “, true );
}
$ gcc app.c libb.so
$ LD_LIBRARY_PATH=. ./app
1
$ nm app | grep true
U true@VER1.0
$
顯然,連接到了版本號為VER1.0的別名符號true。其中只有一個@表示,該版本不是默認的版本
我的疑問:
版本控制腳本文件中,各版本號節點之間的依賴關系
英文搜索關鍵字:
.symver
versioned symbol
version a shared library
參考:
info ld, Scripts node
===================== 結束 ==================================
11. 表達式
———-
表達式的文法與C語言的表達式文法一致,表達式的值都是整型,如果ld的運行主機和生成文件的目標機都是32位,則表達式是32位數據,否則是64位數據。
能夠在表達式內使用符號的值,設置符號的值。
下面看六項表達式相關內容,
另外,你可以使用'K'和'M'后綴作為常數的度量單位,下面的三個常數表示同一個值。
_fourk_1 = 4K;
_fourk_2 = 4096;
_fourk_3 = 0x1000;
常表達式:
_fourk_1 = 4K; /* K、M單位 */
_fourk_2 = 4096; /* 整數 */
_fourk_3 = 0×1000; /* 16 進位 */
_fourk_4 = 01000; /* 8 進位 */
- 符號名:
沒有被引號”"包圍的符號,以字母、下划線或’.'開頭,可包含字母、下划線、’.'和’-'。
當符號名被引號包圍時,符號名可以與關鍵字相同。如:
“SECTION”=9
“with a space” = “also with a space” + 10;
- 定位符號’.':
只在SECTIONS命令內有效,代表一個程序地址空間內的地址。
注意:當定位符用在SECTIONS命令的輸出段描述內時,它代表的是該段的當前**偏移**,而不是程序地址空間的絕對地址。
先看個例子,
SECTIONS
{
output :
{
file1(.text)
. = . + 1000;
file2(.text)
. += 1000;
file3(.text)
} = 0×1234;
}
其中由於對定位符的賦值而產生的空隙由0×1234填充。其他的內容應該容易理解吧。
再看個例子,
SECTIONS
{
. = 0×100
.text: {
*(.text)
. = 0×200
}
. = 0×500
.data: {
*(.data)
. += 0×600
}
}
.text 段在程序地址空間的開始位置是0x100,.text段的結束地址不是絕對地址0x200,而是相對.text段的結束地址再加上0x200。
- 表達式的操作符:
與C語言一致。
優先級 結合順序 操作符
1 left ! – ~ (1)
2 left * / %
3 left + -
4 left >> <<
5 left == != > < <= >=
6 left &
7 left |
8 left &&
9 left ||
10 right ? :
11 right &= += -= *= /= (2)
(1)表示前綴符,(2)表示賦值符。
- 表達式的計算:
連接器延遲計算大部分表達式的值。
但是,對待與連接過程緊密相關的表達式,連接器會立即計算表達式,如果不能計算則報錯。比如,對於段的VMA地址、內存區域塊的開始地址和大小,與其相關的表達式應該立即被計算。
例子,
SECTIONS
{
.text 9+this_isnt_constant :
{ *(.text) }
}
這個例子中,9+this_isnt_constant表達式的值用於設置.text 段的VMA地址,因此需要立即運算,但是由於this_isnt_constant變量的值不確定,所以此時連接器無法確立表達式的值,此時連接器會報錯。
- 相對值與絕對值:
1、在輸出段描述內的表達式,連接器取其相對值,相對與該段的開始位置的偏移
2、在SECTIONS命令內且非輸出段描述內的表達式,連接器取其絕對值
通過ABSOLUTE關鍵字可以將相對值轉化成絕對值,即在原來值的基礎上加上表達式所在段的VMA值。
例子,
SECTIONS
{
.data : { *(.data) _edata = ABSOLUTE(.); }
}
該例子中,_edata符號的值是.data 段的末尾位置(絕對值,在程序地址空間內)。
- 內建函數:
ABSOLUTE(EXP) :把EXP轉換成絕對值
ADDR(SECTION) : 返回某段的VMA值。你的腳本之前必須已經定義了這個段的地址,如ADDR(.text),返回.text段的地址
ALIGN(EXP) : 返回定位計數器'.'對齊到下一個EXP 指定的邊界后的值。‘ALIGN’不改變定位計數器的值,它只是在定位計數器上面作了一個算術運算。
BLOCK(EXP) : 如同ALIGN(EXP),為了向前兼容。
DEFINED(SYMBOL) :如果符號SYMBOL在全局符號表內,且被定義了,那么返回1,否則返回0。例子,
SECTIONS { …
.text : {
begin = DEFINED(begin) ? begin : . ;
…
}
…
}
LOADADDR(SECTION) : 返回SECTION的LMA
MAX(EXP1,EXP2) : 返回大者
MIN(EXP1,EXP2) : 返回小者
NEXT(EXP) : 返回下一個能被使用的地址,該地址是EXP的倍數,類似於ALIGN(EXP)。除非使用了MEMORY命令定義了一些非連續的內存塊,否則NEXT(EXP)與ALIGH(EXP)一定相同。
SIZEOF(SECTION) :返回SECTION的大小。當SECTION沒有被分配時,即此時SECTION的大小還不能確定時,連接器會報錯。
SIZEOF_HEADERS :
sizeof_headers :返回輸出文件的文件頭大小(還是程序頭大小),用以確定第一個段的開始地址(在文件內)。
12. 暗含的連接腳本
輸入文件可以是目標文件,也可以是連接腳本,此時的連接腳本被稱為 暗含的連接腳本
如果連接器不認識某個輸入文件,那么該文件被當作連接腳本被解析。更進一步,如果發現它的格式又不是連接腳本的格式,那么連接器報錯。
一個暗含的連接腳本不會替換默認的連接腳本,僅僅是增加新的連接而已。
一般來說,暗含的連接腳本符號分配命令,或INPUT、GROUP、VERSION命令。
在連接命令行中,每個輸入文件的順序都被固定好了,暗含的連接腳本在連接命令行內占住一個位置,這個位置決定了由該連接腳本指定的輸入文件在連接過程中的順序。
典型的暗含的連接腳本是libc.so文件,在GNU/linux內一般存在/usr/lib目錄下。
References
2.鏈接腳本分析
以u-boot.lds為例,位於根文件夾下/board/samsung/x210內,它是U-boot的總鏈接腳本。
- 本段最開始指定了輸出的格式,然后指定輸出的架構為arm架構
- 指定整個程序的入口地址,可以認為是第一句指令,_start是start.S的第一個lable
- 值得注意的是,程序入口並不代表它位於存儲介質的起始位置。一般起始位置存放的是16字節校驗頭和異常向量表
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") /*OUTPUT_FORMAT("elf32-arm", "elf32-arm", "elf32-arm")*//*這句是注釋*/ OUTPUT_ARCH(arm) ENTRY(_start)
- 1
- 2
- 3
- 4
- SECTIONS表示正式開始地址划分
- .的意思是當前地址,這句將當前地址(代碼段起始地址)設為0x00000000,但是其實這個地址會被config.mk用-Ttext $(TEXT_BASE)指定的虛擬地址0xc3e00000(由頂層Makefile填充給config.mk)覆蓋掉
SECTIONS { . = 0x00000000; . = ALIGN(4); .text :
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- .text表示開始代碼段的鏈接
- 代碼段的鏈接順序很重要,首先start.o必須在第一個
- 由於uboot需要重定位,故所有和重定位有關的代碼必須鏈接在最前面,作為16kb的bl1。而其他所有的.o文件就往后任意鏈接了
.text : { cpu/s5pc11x/start.o (.text) cpu/s5pc11x/s5pc110/cpu_init.o (.text) board/samsung/x210/lowlevel_init.o (.text) cpu/s5pc11x/onenand_cp.o (.text) cpu/s5pc11x/nand_cp.o (.text) cpu/s5pc11x/movi.o (.text) common/secure_boot.o (.text) common/ace_sha1.o (.text) cpu/s5pc11x/pmic.o (.text) *(.text) }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- . = ALIGN(4)的意思是將當前地址(代碼段結束地址)四字節對齊,然后將其作為只讀數據段的起始地址(存放只讀的全局變量)
- 同理,對數據段(存放全局變量)和got段進行相同設置
. = ALIGN(4); .rodata : { *(.rodata) } . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); .got : { *(.got) }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 設置自定義段u_boot_cmd,里面存放着的是一個個命令結構體(結構體內都是命令的信息),它們是緊挨着的,其實有點像結構體數組,只不過是亂序的。寫出 __u_boot_cmd_start, __u_boot_cmd_endt的地址是為了要在源碼中引用這兩個地址,由此來使用命令結構體
- 然后設置mmudata段
- 最后設置bss段(存放初始值為0的全局變量),寫出 __bss_start,_end就是為了要在.s或.c中引用這兩個地址
__u_boot_cmd_start = .; .u_boot_cmd : { *(.u_boot_cmd) } __u_boot_cmd_end = .; . = ALIGN(4); .mmudata : { *(.mmudata) } . = ALIGN(4); __bss_start = .; .bss : { *(.bss) } _end = .; }
轉自https://blog.csdn.net/qq_28992301/article/details/51814005