最近在看u-boot、osekOS的啟動代碼,其中涉及到lds文件,通過參考其他網友的文章,希望對lds文件有個明晰的認識,為了鞏固及加深影響,特將相關博客內容重寫一遍。 原始文章: http://linux.chinaunix.net/techdoc/beginner/2009/08/12/112 ...
最近在看u-boot、osekOS的啟動代碼,其中涉及到lds文件,通過參考其他網友的文章,希望對lds文件有個明晰的認識,為了鞏固及加深影響,特將相關博客內容重寫一遍。
原始文章: http://linux.chinaunix.net/techdoc/beginner/2009/08/12/1129972.shtml。原始文章講解更清楚,不懂的地方可以返回參考。
一、概念
1.1 lds文件
lds文件與scatter文件相似,定義了整個程式編譯之後的連接過程,都是決定一個可執行程式的各個段的存儲位置,以及入口地址,這也是鏈接定位的作用。
每一個鏈接過程都由鏈接腳本(linker script, 一般以lds作為文件的尾碼名)控制。鏈接器通過鏈接腳本中指定的規則把一個或多個輸入文件(可以是目標文件或鏈接腳本文件)的section合成一個輸出文件(可以是目標文件或可執行文件)。
1.2 section
為了區分不同文件的section,目標文件中的每個section需要至少包含名字和大小,大部分還包含與其相關聯的一塊數據section contents。一個section可被標記為“loadable” (可載入的,在輸出文件運行時,該section被載入進程地址空間)或“allocatable” (可分配的,輸出文件運行時,在進程地址空間中預留section大小的部分)。
在目標文件中, loadable或allocatable的輸出section有兩種地址: VMA(virtual Memory Address,進程執行時使用的地址)和LMA(Load Memory Address,載入到進程地址空間的地址)。一般而言, 某section的VMA = LMA. 但在嵌入式系統中, 經常存在載入地址和執行地址不同的情況: 比如將輸出文件載入到開發板的flash中(由LMA指定), 而在運行時將位於flash中的輸出文件複製到SDRAM中(由VMA指定)。
1.3 符號
每個目標文件都有符號表(SYMBOL TABLE), 包含已定義的符號(對應全局變數和static變數和定義的函數的名字)和未定義符號(未定義的函數的名字和引用但沒定義的符號)信息。每個符號對應一個地址, 即符號值(這與c程式內變數的值不一樣, 某種情況下可以把它看成變數的地址)。
二、語法
2.1 腳本格式
鏈接腳本由一系列命令組成, 每個命令由一個關鍵字(一般在其後緊跟相關參數)或一條對符號的賦值語句組成。命令由分號‘;’分隔開。文件名或格式名內如果包含分號’;'或其他分隔符, 則要用引號‘”’將名字全稱引用起來,無法處理含引號的文件名。
/* */之間的是註釋。
GNU官方網站上對.lds文件形式的完整描述: 腳本命令; 腳本命令; … SECTIONS { ... secname start BLOCK(align) (TYPE) : AT ( ldadr ) { contents }[>REGION] [AT>LMA_REGION] [:PHDR HDR ...] [=FILLEXP] ... }
說明:secname和contents是必須的,前者用來命名這個段,後者用來確定代碼中的什麼部分放在這個段,以下是對這個描述中的一些關鍵字的解釋。
1、腳本命令:參見2.3節,可以設置記憶體段、輸出格式、入口地址等。
2、secname:輸出section段名。
3、start BLOCK:是段的重定位地址,本段連接(運行)的地址,如果代碼中有位置無關指令,程式運行時這個段必須放在這個地址上。start可以用任意一種描述地址的符號來描述。設置了輸出section的VMA地址,其中align將定位符號的值調整到滿足輸出section對齊要求後的值。
4、TYPE:輸出section類型,指明是否在程式運行時載入記憶體。
5、AT(ldadr):定義本段存儲(載入)的地址,預設情況下,LMA等於VMA,但可以通過關鍵字AT()指定LMA。用關鍵字AT()指定,括弧內包含表達式,表達式的值用於設置LMA。如果不用AT()關鍵字,那麼可用AT>LMA_REGION表達式設置指定該section載入地址的範圍。通過這個選項可以控制各段分別保存於輸出文件中不同的位置。
6、contents:決定哪些內容放在本段,可以是整個目標文件,也可以是目標文件中的某段(代碼段、數據段等),詳情參考2.4節。
7、region:如果start沒有定義,則以region地址為VMA,如果region也沒定義,以定位符號‘.’的值確定VMA。
8、phdr:沒在具體的實例中看到過。
9、FILLEXP:指明空閑空間(如按align對齊後的空閑部分)的填充欄位。
2.2 符號及表達式
2.2.1 符號
沒有被引號“”包圍的符號,以字母、下劃線或‘.’開頭,可包含字母、下劃線、’.'和’-'。當符號名被引號包圍時,符號名可以與關鍵字相同。
(1). 是一個特殊的符號,它是定位器,一個位置指針,指向程式地址空間內的某位置(或某section內的偏移,如果它在SECTIONS命令內的某section描述內),該符號只能在SECTIONS命令內使用。
(2)PROVIDE關鍵字:用於定義在目標文件內被引用,但沒有在任何目標文件內被定義的符號。
2.2.2 表達式
(1)賦值
在目標文件內定義的符號可以在鏈接腳本內被賦值,但只有對全局變數賦值才有效,而且此處的賦值是更改這個符號對應的地址,而不是變數的值。賦值語句可以出現在連接腳本的三處地方:SECTIONS命令內,SECTIONS命令內的section描述內和全局位置;常用賦值操作有:
SYMBOL = EXPRESSION ; SYMBOL += EXPRESSION ; SYMBOL -= EXPRESSION ; SYMBOL *= EXPRESSION ; SYMBOL /= EXPRESSION ; SYMBOL >= EXPRESSION ; SYMBOL &= EXPRESSION ; SYMBOL |= EXPRESSION ;
註意:除了第一類表達式外, 使用其他表達式需要SYMBOL被定義於某目標文件。
(2)運算符優先順序
優先順序 結合順序 操作符 1 left ! – ~ (1) 2 left * / % 3 left + - 4 left >> = 6 left & 7 left | 8 left && 9 left || 10 right ? : 11 right &= += -= *= /= (2)
(3)內建函數
ABSOLUTE(EXP) :轉換成絕對值 ADDR(SECTION) :返回某section的VMA值。 ALIGN(EXP) :返回定位符’.'的修調值,對齊後的值,(. + EXP – 1) & ~(EXP – 1) BLOCK(EXP) :如同ALIGN(EXP),為了向前相容。 DEFINED(SYMBOL) :如果符號SYMBOL在全局符號表內,且被定義了,那麼返回1,否則返回0。 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 :返回輸出文件的文件頭大小(還是程式頭大小),用以確定第一個section的開始地址(在文件內)。
2.3 腳本命令
2.3.1 簡單命令
命令 |
說明 |
備註 |
ENTRY(SYMBOL) |
將符號SYMBOL的值設置成入口地址(進程執行的第一條用戶空間的指令在進程地址空間的地址)。 |
進程入口地址優先順序:ld命令行的-e選項>連接腳本的ENTRY(SYMBOL)命令>如果定義了start符號, 使用start符號值>如果存在.text section, 使用.text section的第一位元組的位置值>使用值0。 |
INCLUDE filename |
包含其他名為filename的鏈接腳本 |
可以嵌套使用, 最大深度為10。 |
INPUT(files) |
將括弧內的文件做為鏈接過程的輸入文件。 |
ld首先在當前目錄下尋找該文件, 如果沒有, 則在由-L指定的搜索路徑下搜索 |
GROUP(files) |
指定需要重覆搜索符號定義的多個輸入文件 |
file必須是庫文件,被ld重覆掃描,直到不在有新的未定義的引用出現。 |
OUTPUT(FILENAME) |
定義輸出文件的名字 |
|
SEARCH_DIR(PATH) |
定義搜索路徑 |
|
STARTUP(filename) |
指定filename為第一個輸入文件 |
鏈接過程中, 每個輸入文件是有順序的,此命令設置filename為第一個輸入文件。 |
TARGET(BFDNAME) |
設置輸入文件的BFD格式。 |
|
OUTPUT_FORMAT(BFDNAME) |
設置輸出文件使用的BFD格式。 |
|
OUTPUT_FORMAT(DEFAULT,BIG,LITTLE) |
定義三種輸出文件的格式(大小端)。 |
若有命令行選項-EB, 則使用第2個BFD格式; -EL,則使用第3個BFD格式.否則預設第一個。 |
ASSERT(EXP, MESSAGE) |
如果EXP不為真,終止連接過程
|
|
EXTERN(SYMBOL SYMBOL …) |
在輸出文件中增加未定義的符號 |
|
FORCE_COMMON_ALLOCATION |
為common symbol(通用符號)分配空間,即使用了-r連接選項也為其分配 |
|
NOCROSSREFS(SECTION SECTION …) |
檢查列出的輸出section,如果發現他們之間有相互引用,則報錯。 |
對於某些系統,特別是記憶體較緊張的嵌入式系統,某些section是不能同時存在記憶體中的,所以他們之間不能相互引用。 |
OUTPUT_ARCH(BFDARCH) |
設置輸出文件的machine architecture(體繫結構) |
BFDARCH為被BFD庫使用的名字之一。 |
man -S 1 ld |
查看ld的聯機幫助, 裡面也包括了對以上命令的介紹. |
|
2.3.2 其它命令
(1)記憶體區域命令
在預設情形下,連接器可以為section分配任意位置的存儲區域。你也可以用MEMORY命令定義存儲區域,並通過輸出section描述的> REGION屬性顯示地將該輸出section限定於某塊存儲區域,當存儲區域大小不能滿足要求時,連接器會報告該錯誤。
MEMORY命令的文法如下: MEMORY { NAME1 [(ATTR)] : ORIGIN = ORIGIN1, LENGTH = LEN2 NAME2 [(ATTR)] : ORIGIN = ORIGIN2, LENGTH = LEN2 … }
說明如下:
NAME :存儲區域的名字,這個名字可以與符號名、文件名、section名重覆,因為它處於一個獨立的名字空間。
ATTR :定義該存儲區域的屬性,在講述SECTIONS命令時提到,當某輸入section沒有在SECTIONS命令內引用時,連接器會把該輸入 section直接拷貝成輸出section,然後將該輸出section放入記憶體區域內。如果設置了記憶體區域設置了ATTR屬性,那麼該區域只接受滿足該屬性的section(怎麼判斷該section是否滿足?輸出section描述內好象沒有記錄該section的讀寫執行屬性)。
ATTR屬性內可以出現以下7個字元:
R 只讀section W 讀/寫section X 可執行section A ‘可分配的’section I 初始化了的section L 同I ! 不滿足該字元之後的任何一個屬性的section
ORIGIN :關鍵字,區域的開始地址,可簡寫成org或o
LENGTH :關鍵字,區域的大小,可簡寫成len或l
(2)PHDRS命令
在連接腳本內不指定PHDRS命令時,連接器能夠很好的創建程式頭,但是有時需要更精確的描述程式頭,那麼PAHDRS命令就派上用場了。
註意:一旦在連接腳本內使用了PHDRS命令,那麼連接器**僅會**創建PHDRS命令指定的信息,所以使用時須謹慎。
PHDRS命令文法如下:
PHDRS
{
NAME TYPE [ FILEHDR ] [ PHDRS ] [ AT ( ADDRESS ) ]
[ FLAGS ( FLAGS ) ] ;
}
其中FILEHDR、PHDRS、AT、FLAGS為關鍵字。
NAME :為程式段名,此名字可以與符號名、section名、文件名重覆,因為它在一個獨立的名字空間內。此名字只能在SECTIONS命令內使用。
一個程式段可以由多個‘可載入’的section組成。通過輸出section描述的屬性:PHDRS可以將輸出section加入一個程式段,: PHDRS中的PHDRS為程式段名。在一個輸出section描述內可以多次使用:PHDRS命令,也即可以將一個section加入多個程式段。
如果在一個輸出section描述內指定了:PHDRS屬性,那麼其後的輸出section描述將預設使用該屬性,除非它也定義了:PHDRS屬性。顯然當多個輸出section屬於同一程式段時可簡化書寫。
在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),該屬性將**覆蓋**該程式段內的section的AT()屬性。
預設情況下,連接器會根據該程式段包含的section的屬性(什麼屬性?好象在輸出section描述內沒有看到)設置FLAGS標誌,該標誌用於設置程式段描述的p_flags域。
(3)版本號命令
請參看原文介紹
2.4 SECTIONS詳解
SECTIONS命令告訴ld如何把輸入文件的sections映射到輸出文件的各個section: 如何將輸入section合為輸出section; 如何把輸出section放入程式地址空間(VMA)和進程地址空間(LMA),格式如下:
SECTIONS { ENTRY命令/符號賦值語句/一個輸出section的描述(output section description)/一個section疊加描述(overlay description) }
其中,一個輸出section描述的格式為:
SECTION [ADDRESS] [(TYPE)] : [AT(LMA)] { 符號賦值語句/一個輸入section描述/直接包含的數據值/一個特殊的輸出section關鍵字 } [>REGION] [AT>LMA_REGION] [:PHDR HDR ...] [=FILLEXP]
說明如下:
(1)輸出section名字(SECTION):
輸出section名字必須符合輸出文件格式要求,比如:a.out格式的文件只允許存在.text、.data和.bss section名。而有的格式只允許存在數字名字,那麼此時應該用引號將所有名字內的數字組合在一起;另外,還有一些格式允許任何序列的字元存在於 section名字內,此時如果名字內包含特殊字元(比如空格、逗號等),那麼需要用引號將其組合在一起。
(2)輸出section地址(ADDRESS):
ADDRESS是一個表達式,它的值用於設置VMA。如果沒有該選項且有REGION選項,那麼連接器將根據REGION設置VMA;如果也沒有 REGION選項,那麼連接器將根據定位符號‘.’的值設置該section的VMA,將定位符號的值調整到滿足輸出section對齊要求後的值,輸出 section的對齊要求為:該輸出section描述內用到的所有輸入section的對齊要求中最嚴格的。
註意:設置ADDRESS值,將更改定位符號的值。
(3)TYPE :
每個輸出section都有一個類型,如果沒有指定TYPE類型,那麼連接器根據輸出section引用的輸入section的類型設置該輸出section的類型。它可以為以下五種值,
NOLOAD :該section在程式運行時,不被載入記憶體。
DSECT,COPY,INFO,OVERLAY :這些類型很少被使用,為了向後相容才被保留下來。這種類型的section必須被標記為“不可載入的”,以便在程式運行不為它們分配記憶體。
(4)輸出section的LMA :
預設情況下,LMA等於VMA,但可以通過關鍵字AT()指定LMA。
用關鍵字AT()指定,括弧內包含表達式,表達式的值用於設置LMA。如果不用AT()關鍵字,那麼可用AT>LMA_REGION表達式設置指定該section載入地址的範圍。
這個屬性主要用於構件ROM境象。
(5)輸入section描述:
最常見的輸出section描述命令是輸入section描述。輸入section描述是最基本的連接腳本描述。
1)輸入section描述基礎:
基本語法:FILENAME([EXCLUDE_FILE (FILENAME1 FILENAME2 ...) SECTION1 SECTION2 ...)
FILENAME文件名,可以是一個特定的文件的名字,也可以是一個字元串模式。
SECTION名字,可以是一個特定的section名字,也可以是一個字元串模式
例如:
*(.text) :表示所有輸入文件的.text section (*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors)) :表示除crtend.o、otherfile.o文件外的所有輸入文件的.ctors section。 data.o(.data) :表示data.o文件的.data section data.o :表示data.o文件的所有section *(.text .data) :表示所有文件的.text section和.data section,順序是:第一個文件的.text section,第一個文件的.data section,第二個文件的.text section,第二個文件的.data section,... *(.text) *(.data) :表示所有文件的.text section和.data section,順序是:第一個文件的.text section,第二個文件的.text section,...,最後一個文件的.text section,第一個文件的.data section,第二個文件的.data section,...,最後一個文件的.data section
2)字元串模式內可存在以下通配符:
* :表示任意多個字元 ? :表示任意一個字元 [CHARS] :表示任意一個CHARS內的字元,可用-號表示範圍,如:a-z :表示引用下一個緊跟的字元 SORT():對滿足字元串模式的所有名字進行遞增排序,如SORT(.text*)。
在文件名內,通配符不匹配文件夾分隔符/,但當字元串模式僅包含通配符*時除外。另外,任何一個文件的任意section只能在SECTIONS命令內出現一次。
3)通用符號(common symbol)的輸入section
在許多目標文件格式中,通用符號並沒有占用一個section。連接器認為:輸入文件的所有通用符號在名為COMMON的section內。
4)輸入section和垃圾回收
在連接命令行內使用了選項–gc-sections後,連接器可能將某些它認為沒用的section過濾掉,此時就有必要強制連接器保留一些特定的 section,可用KEEP()關鍵字達此目的。如KEEP(*(.text))或KEEP(SORT(*)(.text))。
5)在輸出section存放數據命令
能夠顯示地在輸出section內填入你想要填入的信息(這樣是不是可以自己通過連接腳本寫程式?當然是簡單的程式)。
BYTE(EXPRESSION) 1 位元組 SHORT(EXPRESSION) 2 位元組 LOGN(EXPRESSION) 4 位元組 QUAD(EXPRESSION) 8 位元組 SQUAD(EXPRESSION) 64位處理器的代碼時,8 位元組 註意,這些命令只能放在輸出section描述內,其他地方不行。 錯誤:SECTIONS { .text : { *(.text) } LONG(1) .data : { *(.data) } } 正確:SECTIONS { .text : { *(.text) LONG(1) } .data : { *(.data) } }
6)FILL(EXPRESSION)和=FILEEXP屬性
在當前輸出section內可能存在未描述的存儲區域(比如由於對齊造成的空隙),可以用FILL(EXPRESSION)命令決定這些存儲區域的內容, EXPRESSION的前兩位元組有效,這兩位元組在必要時可以重覆被使用以填充這類存儲區域。如FILE(0×9090)。在輸出section描述中可以 有=FILEEXP屬性,它的作用如同FILE()命令,但是FILE命令只作用於該FILE指令之後的section區域,而=FILEEXP屬性作用 於整個輸出section區域,且FILE命令的優先順序更高!!!
7)輸出section內命令的關鍵字
CREATE_OBJECT_SYMBOLS :為每個輸入文件建立一個符號,符號名為輸入文件的名字。每個符號所在的section是出現該關鍵字的section。
CONSTRUCTORS :與c++內的(全局對象的)構造函數和(全局對像的)析構函數相關。具體介紹可參看原文 http://linux.chinaunix.net/techdoc/beginner/2009/08/12/1129972.shtml。
8)輸出section的丟棄
例子,.foo { *(.foo) },如果沒有任何一個輸入文件包含.foo section,那麼連接器將不會創建.foo輸出section。但是如果在這些輸出section描述內包含了非輸入section描述命令(如符號 賦值語句),那麼連接器將總是創建該輸出section。
有一個特殊的輸出section,名為/DISCARD/,被該section引用的任何輸入section將不會出現在輸出文件內,
(6)覆蓋圖(overlay)描述
覆蓋圖描述使兩個或多個不同的section占用同一塊程式地址空間。覆蓋圖管理代碼負責將section的拷入和拷出。文法如下,
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] … }
由以上文法可以看出,同一覆蓋圖內的section具有相同的VMA。SECNAME2的LMA為SECTNAME1的LMA加上SECNAME1的大 小,同理計算SECNAME2,3,4…的LMA。SECNAME1的LMA由LDADDR決定,如果它沒有被指定,那麼由START決定,如果它也沒有 被指定,那麼由當前定位符號的值決定。
NOCROSSREFS關鍵字指定各section之間不能交叉引用,否則報錯。
對於OVERLAY描述的每個section,連接器將定義兩個符號__load_start_SECNAME和__load_stop_SECNAME,這兩個符號的值分別代表SECNAME section的LMA地址的開始和結束。
連接器處理完OVERLAY描述語句後,將定位符號的值加上所有覆蓋圖內section大小的最大值。
2.5 暗含的鏈接腳本
輸入文件可以是目標文件,也可以是連接腳本,此時的連接腳本被稱為暗含的連接腳本。如果連接器不認識某個輸入文件,那麼該文件被當作連接腳本被解析。一個暗含的連接腳本不會替換預設的連接腳本,僅僅是增加新的連接而已。在連接命令行中,每個輸入文件的順序都被固定好了,暗含的連接腳本在連接命令行內占住一個位置,這個位置決定了由該連接腳本指定的輸入文件在連接過程中的順序。
三、實例
例1:
以下腳本將輸出文件的text section定位在0×10000, data section定位在0×8000000:
SECTIONS { . = 0×10000; .text : { *(.text) } . = 0×8000000; .data : { *(.data) } .bss : { *(.bss) } }
解釋一下上述的例子:
. = 0×10000 : 把定位器符號置為0×10000 (若不指定, 則該符號的初始值為0).
.text : { *(.text) } : 將所有(*符號代表任意輸入文件)輸入文件的.text section合併成一個.text section, 該section的地址由定位器符號的值指定, 即0×10000.
. = 0×8000000 :把定位器符號置為0×8000000
.data : { *(.data) } : 將所有輸入文件的.data section合併成一個.data section, 該section的地址被置為0×8000000.
.bss : { *(.bss) } : 將所有輸入文件的.bss section合併成一個.bss section,該section的地址被置為0×8000000+.data section的大小.
連接器每讀完一個section描述後, 將定位器符號的值*增加*該section的大小. 註意: 此處沒有考慮對齊約束。
例2
SECTIONS{ … OVERLAY 0×1000 : AT (0×4000) { .text0 { o1/*.o(.text) } .text1 { o2/*.o(.text) } } … }
.text0 section和.text1 section的VMA地址是0×1000,.text0 section載入於地址0×4000,.text1 section緊跟在其後。
程式代碼,拷貝.text1 section代碼:
extern char __load_start_text1, __load_stop_text1; memcpy ((char *) 0×1000, &__load_start_text1, &__load_stop_text1 – &__load_start_text1);
例3
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 … }
例4
MEMORY { rom (rx) : ORIGIN = 0, LENGTH = 256K ram (!rx) : org = 0×40000000, l = 4M }