1. 程式人生 > >GNU-ld連結指令碼淺析

GNU-ld連結指令碼淺析

0. Contents

1. 概論
2. 基本概念
3. 指令碼格式
4. 簡單例子
5. 簡單指令碼命令
6. 對符號的賦值
7. SECTIONS命令
8. MEMORY命令
9. PHDRS命令
10. VERSION命令
11. 指令碼內的表示式
12. 暗含的連線指令碼


1. 概論

每一個連結過程都由連結指令碼(linker script, 一般以lds作為檔案的字尾名)控制. 連結指令碼主要用於規定如何把輸入檔案內的section放入輸出檔案內, 並控制輸出檔案內各部分在程式地址空間內的佈局. 但你也可以用連線命令做一些其他事情.

聯結器有個預設的內建連線指令碼, 可用ld --verbose檢視. 連線選項-r和-N可以影響預設的連線指令碼(如何影響?).

-T選項用以指定自己的連結指令碼, 它將代替預設的連線指令碼。你也可以使用<暗含的連線指令碼>以增加自定義的連結命令.

以下沒有特殊說明,聯結器指的是靜態聯結器.


2. 基本概念

連結器把一個或多個輸入檔案合成一個輸出檔案.

輸入檔案: 目標檔案或連結指令碼檔案. 
輸出檔案: 目標檔案或可執行檔案.

目標檔案(包括可執行檔案)具有固定的格式, 在UNIX或GNU/Linux平臺下, 一般為ELF格式. 若想了解更多, 可參考 UNIX/Linux平臺可執行檔案格式分析

有時把輸入檔案內的section稱為輸入section(input section), 把輸出檔案內的section稱為輸出section(output sectin).

目標檔案的每個section至少包含兩個資訊: 名字和大小. 大部分section還包含與它相關聯的一塊資料, 稱為section contents(section內容). 一個section可被標記為“loadable(可載入的)”或“allocatable(可分配的)”. 

loadable section: 在輸出檔案執行時, 相應的section內容將被載入程序地址空間中.

allocatable section: 內容為空的section可被標記為“可分配的”. 在輸出檔案執行時, 在程序地址空間中空出大小同section指定大小的部分. 某些情況下, 這塊記憶體必須被置零.

如果一個section不是“可載入的”或“可分配的”, 那麼該section通常包含了除錯資訊. 可用objdump -h命令檢視相關資訊.

每個“可載入的”或“可分配的”輸出section通常包含兩個地址: VMA(virtual memory address虛擬記憶體地址或程式地址空間地址)和LMA(load memory address載入記憶體地址或程序地址空間地址). 通常VMA和LMA是相同的.

在目標檔案中, loadable或allocatable的輸出section有兩種地址: VMA(virtual Memory Address)和LMA(Load Memory Address). VMA是執行輸出檔案時section所在的地址, 而LMA是載入輸出檔案時section所在的地址. 一般而言, 某section的VMA == LMA. 但在嵌入式系統中, 經常存在載入地址和執行地址不同的情況: 比如將輸出檔案載入到開發板的flash中(由LMA指定), 而在執行時將位於flash中的輸出檔案複製到SDRAM中(由VMA指定).

可這樣來理解VMA和LMA, 假設:
(1) .data section對應的VMA地址是0x08050000, 該section內包含了3個32位全域性變數, i、j和k, 分別為1,2,3.
(2) .text section內包含由"printf( "j=%d ", j );"程式片段產生的程式碼.

連線時指定.data section的VMA為0x08050000, 產生的printf指令是將地址為0x08050004處的4位元組內容作為一個整數打印出來。

如果.data section的LMA為0x08050000,顯然結果是j=2
如果.data section的LMA為0x08050004,顯然結果是j=1

還可這樣理解LMA:
.text section內容的開始處包含如下兩條指令(intel i386指令是10位元組,每行對應5位元組):

jmp 0x08048285

movl $0x1,%eax

如果.text section的LMA為0x08048280, 那麼在程序地址空間內0x08048280處為“jmp 0x08048285”指令, 0x08048285處為movl $0x1,%eax指令. 假設某指令跳轉到地址0x08048280, 顯然它的執行將導致%eax暫存器被賦值為1.

如果.text section的LMA為0x08048285, 那麼在程序地址空間內0x08048285處為“jmp 0x08048285”指令, 0x0804828a處為movl $0x1,%eax指令. 假設某指令跳轉到地址0x08048285, 顯然它的執行又跳轉到程序地址空間內0x08048285處, 造成死迴圈.

符號(symbol): 每個目標檔案都有符號表(SYMBOL TABLE), 包含已定義的符號(對應全域性變數和static變數和定義的函式的名字)和未定義符號(未定義的函式的名字和引用但沒定義的符號)資訊.

符號值: 每個符號對應一個地址, 即符號值(這與c程式內變數的值不一樣, 某種情況下可以把它看成變數的地址). 可用nm命令檢視它們. (nm的使用方法可參考本blog的GNU binutils筆記)


3. 指令碼格式
連結指令碼由一系列命令組成, 每個命令由一個關鍵字(一般在其後緊跟相關引數)或一條對符號的賦值語句組成. 命令由分號‘;’分隔開.

檔名或格式名內如果包含分號';'或其他分隔符, 則要用引號‘"’將名字全稱引用起來. 無法處理含引號的檔名.
/* */之間的是註釋。


4. 簡單例子
在介紹連結描述檔案的命令之前, 先看看下述的簡單例子:

以下指令碼將輸出檔案的text section定位在0x10000, data section定位在0x8000000:

SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}

解釋一下上述的例子: 
. = 0x10000 : 把定位器符號置為0x10000 (若不指定, 則該符號的初始值為0).

.text : { *(.text) } : 將所有(*符號代表任意輸入檔案)輸入檔案的.text section合併成一個.text section, 該section的地址由定位器符號的值指定, 即0x10000.

. = 0x8000000 :把定位器符號置為0x8000000
.data : { *(.data) } : 將所有輸入檔案的.text section合併成一個.data section, 該section的地址被置為0x8000000.

.bss : { *(.bss) } : 將所有輸入檔案的.bss section合併成一個.bss section,該section的地址被置為0x8000000+.data section的大小.

聯結器每讀完一個section描述後, 將定位器符號的值*增加*該section的大小. 注意: 此處沒有考慮對齊約束.


5. 簡單指令碼命令
- 1 -
ENTRY(SYMBOL): 將符號SYMBOL的值設定成入口地址。

入口地址(entry point): 程序執行的第一條使用者空間的指令在程序地址空間的地址)

ld有多種方法設定程序入口地址, 按一下順序: (編號越前, 優先順序越高)
1, ld命令列的-e選項
2, 連線指令碼的ENTRY(SYMBOL)命令
3, 如果定義了start符號, 使用start符號值
4, 如果存在.text section, 使用.text section的第一位元組的位置值
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必須是庫檔案, 且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, 則使用第2個BFD格式; 若有命令列選項-EL,則使用第3個BFD格式.否則預設選第一個BFD格式.

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 ...):檢查列出的輸出section,如果發現他們之間有相互引用,則報錯。對於某些系統,特別是記憶體較緊張的嵌入式系統,某些section是不能同時存在記憶體中的,所以他們之間不能相互引用。

OUTPUT_ARCH(BFDARCH):設定輸出檔案的machine architecture(體系結構),BFDARCH為被BFD庫使用的名字之一。可以用命令objdump -f檢視。

可通過 man -S 1 ld檢視ld的聯機幫助, 裡面也包括了對這些命令的介紹.


6. 對符號的賦值
在目標檔案內定義的符號可以在連結指令碼內被賦值. (注意和C語言中賦值的不同!) 此時該符號被定義為全域性的. 每個符號都對應了一個地址, 此處的賦值是更改這個符號對應的地址.

e.g. 通過下面的程式檢視變數a的地址:
/* a.c */
#include <stdio.h>
int a = 100;
int main(void)
{
    printf( "&a=0x%p ", &a );
    return 0;
}

/* a.lds */
a = 3;

gcc -Wall -o a-without-lds a.c
&a = 0x8049598

gcc -Wall -o a-with-lds a.c a.lds
&a = 0x3

注意: 對符號的賦值只對全域性變數起作用!

一些簡單的賦值語句
能使用任何c語言內的賦值操作:

SYMBOL = EXPRESSION ;
SYMBOL += EXPRESSION ;
SYMBOL -= EXPRESSION ;
SYMBOL *= EXPRESSION ;
SYMBOL /= EXPRESSION ;
SYMBOL <<= EXPRESSION ;
SYMBOL >>= EXPRESSION ;
SYMBOL &= EXPRESSION ;
SYMBOL |= EXPRESSION ;

除了第一類表示式外, 使用其他表示式需要SYMBOL被定義於某目標檔案。
. 是一個特殊的符號,它是定位器,一個位置指標,指向程式地址空間內的某位置(或某section內的偏移,如果它在SECTIONS命令內的某section描述內),該符號只能在SECTIONS命令內使用。
注意:賦值語句包含4個語法元素:符號名、操作符、表示式、分號;一個也不能少。
被賦值後,符號所屬的section被設值為表示式EXPRESSION所屬的SECTION(參看11. 指令碼內的表示式)
賦值語句可以出現在連線指令碼的三處地方:SECTIONS命令內,SECTIONS命令內的section描述內和全域性位置;如下,
floating_point = 0; /* 全域性位置 */
SECTIONS
{
.text :
{
*(.text)
_etext = .; /* section描述內 */
}
_bdata = (. + 3) & ~ 4; /* SECTIONS命令內 */
.data : { *(.data) }
}

PROVIDE關鍵字
該關鍵字用於定義這類符號:在目標檔案內被引用,但沒有在任何目標檔案內被定義的符號。
例子:
SECTIONS
{
.text :
{
*(.text)
_etext = .;
PROVIDE(etext = .);
}
}
當目標檔案內引用了etext符號,確沒有定義它時,etext符號對應的地址被定義為.text section之後的第一個位元組的地址。


7. SECTIONS命令
SECTIONS命令告訴ld如何把輸入檔案的sections對映到輸出檔案的各個section: 如何將輸入section合為輸出section; 如何把輸出section放入程式地址空間(VMA)和程序地址空間(LMA).該命令格式如下:

SECTIONS
{
SECTIONS-COMMAND
SECTIONS-COMMAND
...
}

SECTION-COMMAND有四種:
(1) ENTRY命令
(2) 符號賦值語句
(3) 一個輸出section的描述(output section description)
(4) 一個section疊加描述(overlay description)

如果整個連線指令碼內沒有SECTIONS命令, 那麼ld將所有同名輸入section合成為一個輸出section內, 各輸入section的順序為它們被聯結器發現的順序.

如果某輸入section沒有在SECTIONS命令中提到, 那麼該section將被直接拷貝成輸出section。

輸出section描述
輸出section描述具有如下格式:

SECTION [ADDRESS] [(TYPE)] : [AT(LMA)]
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
...
} [>REGION] [AT>LMA_REGION] [:PHDR :PHDR ...] [=FILLEXP]

[ ]內的內容為可選選項, 一般不需要.
SECTION:section名字
SECTION左右的空白、圓括號、冒號是必須的,換行符和其他空格是可選的。
每個OUTPUT-SECTION-COMMAND為以下四種之一,
符號賦值語句
一個輸入section描述
直接包含的資料值
一個特殊的輸出section關鍵字

輸出section名字(SECTION):
輸出section名字必須符合輸出檔案格式要求,比如:a.out格式的檔案只允許存在.text、.data和.bss section名。而有的格式只允許存在數字名字,那麼此時應該用引號將所有名字內的數字組合在一起;另外,還有一些格式允許任何序列的字元存在於 section名字內,此時如果名字內包含特殊字元(比如空格、逗號等),那麼需要用引號將其組合在一起。

輸出section地址(ADDRESS):
ADDRESS是一個表示式,它的值用於設定VMA。如果沒有該選項且有REGION選項,那麼聯結器將根據REGION設定VMA;如果也沒有 REGION選項,那麼聯結器將根據定位符號‘.’的值設定該section的VMA,將定位符號的值調整到滿足輸出section對齊要求後的值,輸出 section的對齊要求為:該輸出section描述內用到的所有輸入section的對齊要求中最嚴格的。
例子:
.text . : { *(.text) }

.text : { *(.text) }
這兩個描述是截然不同的,第一個將.text section的VMA設定為定位符號的值,而第二個則是設定成定位符號的修調值,滿足對齊要求後的。
ADDRESS可以是一個任意表達式,比如ALIGN(0x10)這將把該section的VMA設定成定位符號的修調值,滿足16位元組對齊後的。
注意:設定ADDRESS值,將更改定位符號的值。

輸入section描述:
最常見的輸出section描述命令是輸入section描述。
輸入section描述是最基本的連線指令碼描述。
輸入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
下面看聯結器是如何找到對應的檔案的。
當FILENAME是一個特定的檔名時,聯結器會檢視它是否在連線命令列內出現或在INPUT命令中出現。
當FILENAME是一個字串模式時,聯結器僅僅只檢視它是否在連線命令列內出現。
注意:如果聯結器發現某檔案在INPUT命令內出現,那麼它會在-L指定的路徑內搜尋該檔案。

字串模式內可存在以下萬用字元:
* :表示任意多個字元
? :表示任意一個字元
[CHARS] :表示任意一個CHARS內的字元,可用-號表示範圍,如:a-z
:表示引用下一個緊跟的字元

在檔名內,萬用字元不匹配資料夾分隔符/,但當字串模式僅包含萬用字元*時除外。
任何一個檔案的任意section只能在SECTIONS命令內出現一次。看如下例子,
SECTIONS {
.data : { *(.data) }
.data1 : { data.o(.data) }
}
data.o檔案的.data section在第一個OUTPUT-SECTION-COMMAND命令內被使用了,那麼在第二個OUTPUT-SECTION-COMMAND命令內將不會再被使用,也就是說即使聯結器不報錯,輸出檔案的.data1 section的內容也是空的。
再次強調:聯結器依次掃描每個OUTPUT-SECTION-COMMAND命令內的檔名,任何一個檔案的任何一個section都只能使用一次。
讀者可以用-M連線命令選項來產生一個map檔案,它包含了所有輸入section到輸出section的組合資訊。
再看個例子,
SECTIONS {
.text : { *(.text) }
.DATA : { [A-Z]*(.data) }
.data : { *(.data) }
.bss : { *(.bss) }
}
這個例子中說明,所有檔案的輸入.text section組成輸出.text section;所有以大寫字母開頭的檔案的.data section組成輸出.DATA section,其他檔案的.data section組成輸出.data section;所有檔案的輸入.bss section組成輸出.bss section。
可以用SORT()關鍵字對滿足字串模式的所有名字進行遞增排序,如SORT(.text*)。
通用符號(common symbol)的輸入section:
在許多目標檔案格式中,通用符號並沒有佔用一個section。聯結器認為:輸入檔案的所有通用符號在名為COMMON的section內。
例子,
.bss { *(.bss) *(COMMON) }
這個例子中將所有輸入檔案的所有通用符號放入輸出.bss section內。可以看到COMMOM section的使用方法跟其他section的使用方法是一樣的。
有些目標檔案格式把通用符號分成幾類。例如,在MIPS elf目標檔案格式中,把通用符號分成standard common symbols(標準通用符號)和small common symbols(微通用符號,不知道這麼譯對不對?),此時聯結器認為所有standard common symbols在COMMON section內,而small common symbols在.scommon section內。
在一些以前的連線指令碼內可以看見[COMMON],相當於*(COMMON),不建議繼續使用這種陳舊的方式。
輸入section和垃圾回收:
在連線命令列內使用了選項--gc-sections後,聯結器可能將某些它認為沒用的section過濾掉,此時就有必要強制聯結器保留一些特定的 section,可用KEEP()關鍵字達此目的。如KEEP(*(.text))或KEEP(SORT(*)(.text))
最後看個簡單的輸入section相關例子:
SECTIONS {
outputa 0x10000 :
{
all.o
foo.o (.input1)
}
outputb :
{
foo.o (.input2)
foo1.o (.input1)
}
outputc :
{
*(.input1)
*(.input2)
}
}
本例中,將all.o檔案的所有section和foo.o檔案的所有(一個檔案內可以有多個同名section).input1 section依次放入輸出outputa section內,該section的VMA是0x10000;將foo.o檔案的所有.input2 section和foo1.o檔案的所有.input1 section依次放入輸出outputb section內,該section的VMA是當前定位器符號的修調值(對齊後);將其他檔案(非all.o、foo.o、foo1.o)檔案的. input1 section和.input2 section放入輸出outputc section內。

在輸出section存放資料命令:
能夠顯示地在輸出section內填入你想要填入的資訊(這樣是不是可以自己通過連線指令碼寫程式?當然是簡單的程式)。
BYTE(EXPRESSION) 1 位元組
SHORT(EXPRESSION) 2 位元組
LOGN(EXPRESSION) 4 位元組
QUAD(EXPRESSION) 8 位元組
SQUAD(EXPRESSION) 64位處理器的程式碼時,8 位元組
輸出檔案的位元組順序big endianness 或little endianness,可以由輸出目標檔案的格式決定;如果輸出目標檔案的格式不能決定位元組順序,那麼位元組順序與第一個輸入檔案的位元組順序相同。
如:BYTE(1)、LANG(addr)。
注意,這些命令只能放在輸出section描述內,其他地方不行。
錯誤:SECTIONS { .text : { *(.text) } LONG(1) .data : { *(.data) } }
正確:SECTIONS { .text : { *(.text) LONG(1) } .data : { *(.data) } }
在當前輸出section內可能存在未描述的儲存區域(比如由於對齊造成的空隙),可以用FILL(EXPRESSION)命令決定這些儲存區域的內容, EXPRESSION的前兩位元組有效,這兩位元組在必要時可以重複被使用以填充這類儲存區域。如FILE(0x9090)。在輸出section描述中可以有=FILEEXP屬性,它的作用如同FILE()命令,但是FILE命令只作用於該FILE指令之後的section區域,而=FILEEXP屬性作用於整個輸出section區域,且FILE命令的優先順序更高!!!

輸出section內命令的關鍵字:
CREATE_OBJECT_SYMBOLS :為每個輸入檔案建立一個符號,符號名為輸入檔案的名字。每個符號所在的section是出現該關鍵字的section。
CONSTRUCTORS :與c++內的(全域性物件的)建構函式和(全域性對像的)解構函式相關,下面將它們簡稱為全域性構造和全域性析構。
對於a.out目標檔案格式,聯結器用一些不尋常的方法實現c++的全域性構造和全域性析構。當聯結器生成的目標檔案格式不支援任意section名字時,比如說ECOFF、XCOFF格式,聯結器將通過名字來識別全域性構造和全域性析構,對於這些檔案格式,聯結器把與全域性構造和全域性析構的相關資訊放入出現 CONSTRUCTORS關鍵字的輸出section內。
符號__CTORS_LIST__表示全域性構造資訊的的開始處,__CTORS_END__表示全域性構造資訊的結束處。
符號__DTORS_LIST__表示全域性構造資訊的的開始處,__DTORS_END__表示全域性構造資訊的結束處。
這兩塊資訊的開始處是一字長的資訊,表示該塊資訊有多少項資料,然後以值為零的一字長資料結束。
一般來說,GNU C++在函式__main內安排全域性構造程式碼的執行,而__main函式被初始化程式碼(在main函式呼叫之前執行)呼叫。是不是對於某些目標檔案格式才這樣???
對於支援任意section名的目標檔案格式,比如COFF、ELF格式,GNU C++將全域性構造和全域性析構資訊分別放入.ctors section和.dtors section內,然後在連線指令碼內加入如下,
__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))。一般來說,預設的連線指令碼已作好的這些工作。

輸出section的丟棄:
例子,.foo { *(.foo) },如果沒有任何一個輸入檔案包含.foo section,那麼聯結器將不會建立.foo輸出section。但是如果在這些輸出section描述內包含了非輸入section描述命令(如符號賦值語句),那麼聯結器將總是建立該輸出section。
有一個特殊的輸出section,名為/DISCARD/,被該section引用的任何輸入section將不會出現在輸出檔案內,這就是DISCARD的意思吧。如果/DISCARD/ section被它自己引用呢?想想看。

輸出section屬性:
終於講到這裡了,呵呵。
我們再回顧以下輸出section描述的文法:
SECTION [ADDRESS] [(TYPE)] : [AT(LMA)]
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
...
} [>REGION] [AT>LMA_REGION] [:PHDR :PHDR ...] [=FILLEXP]
前面我們瀏覽了SECTION、ADDRESS、OUTPUT-SECTION-COMMAND相關資訊,下面我們將瀏覽其他屬性。

TYPE :每個輸出section都有一個型別,如果沒有指定TYPE型別,那麼聯結器根據輸出section引用的輸入section的型別設定該輸出section的型別。它可以為以下五種值,
NOLOAD :該section在程式執行時,不被載入記憶體。
DSECT,COPY,INFO,OVERLAY :這些型別很少被使用,為了向後相容才被保留下來。這種型別的section必須被標記為“不可載入的”,以便在程式執行不為它們分配記憶體。

輸出section的LMA :預設情況下,LMA等於VMA,但可以通過關鍵字AT()指定LMA。
用關鍵字AT()指定,括號內包含表示式,表示式的值用於設定LMA。如果不用AT()關鍵字,那麼可用AT>LMA_REGION表示式設定指定該section載入地址的範圍。
這個屬性主要用於構件ROM境象。
例子,
SECTIONS
{
.text 0x1000 : { *(.text) _etext = . ; }
.mdata 0x2000 :
AT ( ADDR (.text) + SIZEOF (.text) )
{ _data = . ; *(.data); _edata = . ; }
.bss 0x3000 :
{ _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
}
程式如下,
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;

此程式將處於ROM內的已初始化資料拷貝到該資料應在的位置(VMA地址),並將為初始化資料置零。
讀者應該認真的自己分析以上連線指令碼和程式的作用。

輸出section區域:可以將輸出section放入預先定義的記憶體區域內,例子,
MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }
SECTIONS { ROM : { *(.text) } >rom }

輸出section所在的程式段:可以將輸出section放入預先定義的程式段(program segment)內。如果某個輸出section設定了它所在的一個或多個程式段,那麼接下來定義的輸出section的預設程式段與該輸出 section的相同。除非再次顯示地指定。例子,
PHDRS { text PT_LOAD ; }
SECTIONS { .text : { *(.text) } :text }
可以通過:NONE指定聯結器不把該section放入任何程式段內。詳情請檢視PHDRS命令

輸出section的填充模版:這個在前面提到過,任何輸出section描述內的未指定的記憶體區域,聯結器用該模版填充該區域。用法:=FILEEXP,前兩位元組有效,當區域大於兩位元組時,重複使用這兩位元組以將其填滿。例子,
SECTIONS { .text : { *(.text) } =0x9090 }

覆蓋圖(overlay)描述:
覆蓋圖描述使兩個或多個不同的section佔用同一塊程式地址空間。覆蓋圖管理程式碼負責將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大小的最大值。
看個例子吧,
SECTIONS{
...

OVERLAY 0x1000 : AT (0x4000)
{
.text0 { o1/*.o(.text) }
.text1 { o2/*.o(.text) }
}
...
}
.text0 section和.text1 section的VMA地址是0x1000,.text0 section加載於地址0x4000,.text1 section緊跟在其後。
程式程式碼,拷貝.text1 section程式碼,
extern char __load_start_text1, __load_stop_text1;
memcpy ((char *) 0x1000, &__load_start_text1,
&__load_stop_text1 - &__load_start_text1);


8. 記憶體區域命令
---------------

注意:以下儲存區域指的是在程式地址空間內的。
在預設情形下,聯結器可以為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

例子,
MEMORY
{
rom (rx) : ORIGIN = 0, LENGTH = 256K
ram (!rx) : org = 0x40000000, l = 4M
}
此例中,把在SECTIONS命令內*未*引用的且具有讀屬性或寫屬性的輸入section放入rom區域內,把其他未引用的輸入section放入 ram。如果某輸出section要被放入某記憶體區域內,而該輸出section又沒有指明ADDRESS屬性,那麼聯結器將該輸出section放在該區域內下一個能使用位置。


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 :為程式段名,此名字可以與符號名、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域。
下面看一個典型的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 */
...
. = . + 0x1000; /* 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,[email protected]" );
__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,[email protected]" );
int true();
int main()
{
printf( "%d ", true );
}

$ gcc app.c libb.so
$ LD_LIBRARY_PATH=. ./app
1
$ nm app | grep true
U [email protected]
$

顯然,連線到了版本號為VER1.0的別名符號true。其中只有一個@表示,該版本不是預設的版本




我的疑問:
版本控制指令碼檔案中,各版本號節點之間的依賴關係


英文搜尋關鍵字:
.symver 
versioned symbol
version a shared library

參考:
info ld, Scripts node
===================== 結束 ==================================


11. 表示式
----------

表示式的文法與C語言的表示式文法一致,表示式的值都是整型,如果ld的執行主機和生成檔案的目標機都是32位,則表示式是32位資料,否則是64位資料。 
能夠在表示式內使用符號的值,設定符號的值。
下面看六項表示式相關內容,

常表示式:
_fourk_1 = 4K; /* K、M單位 */
_fourk_2 = 4096; /* 整數 */
_fourk_3 = 0x1000; /* 16 進位 */
_fourk_4 = 01000; /* 8 進位 */
1K=1024 1M=1024*1024
符號名:
沒有被引號""包圍的符號,以字母、下劃線或'.'開頭,可包含字母、下劃線、'.'和'-'。當符號名被引號包圍時,符號名可以與關鍵字相同。如,
"SECTION"=9
"with a space" = "also with a space" + 10;
定位符號'.':
只在SECTIONS命令內有效,代表一個程式地址空間內的地址。
注意:當定位符用在SECTIONS命令的輸出section描述內時,它代表的是該section的當前**偏移**,而不是程式地址空間的絕對地址。
先看個例子,
SECTIONS
{
output :
{
file1(.text)
. = . + 1000;
file2(.text)
. += 1000;
file3(.text)
} = 0x1234;
}
其中由於對定位符的賦值而產生的空隙由0x1234填充。其他的內容應該容易理解吧。
再看個例子,
SECTIONS
{
. = 0x100
.text: {
*(.text)
. = 0x200
}
. = 0x500
.data: {
*(.data)
. += 0x600
}
} .text section在程式地址空間的開始位置是0x
表示式的操作符:
與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)表示賦值符。
表示式的計算:
聯結器延遲計算大部分表示式的值。
但是,對待與連線過程緊密相關的表示式,聯結器會立即計算表示式,如果不能計算則報錯。比如,對於section的VMA地址、記憶體區域塊的開始地址和大小,與其相關的表示式應該立即被計算。
例子,
SECTIONS
{
.text 9+this_isnt_constant :
{ *(.text) }
}
這個例子中,9+this_isnt_constant表示式的值用於設定.text section的VMA地址,因此需要立即運算,但是由於this_isnt_constant變數的值不確定,所以此時聯結器無法確立表示式的值,此時聯結器會報錯。
相對值與絕對值:
在輸出section描述內的表示式,聯結器取其相對值,相對與該section的開始位置的偏移
在SECTIONS命令內且非輸出section描述內的表示式,聯結器取其絕對值
通過ABSOLUTE關鍵字可以將相對值轉化成絕對值,即在原來值的基礎上加上表達式所在section的VMA值。
例子,
SECTIONS
{
.data : { *(.data) _edata = ABSOLUTE(.); }
}
該例子中,_edata符號的值是.data section的末尾位置(絕對值,在程式地址空間內)。
內建函式:
ABSOLUTE(EXP) :轉換成絕對值
ADDR(SECTION) :返回某section的VMA值。
ALIGN(EXP) :返回定位符'.'的修調值,對齊後的值,(. + EXP - 1) & ~(EXP - 1)
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 :返回輸出檔案的檔案頭大小(還是程式頭大小),用以確定第一個section的開始地址(在檔案內)。???


12. 暗含的連線指令碼
輸入檔案可以是目標檔案,也可以是連線指令碼,此時的連線指令碼被稱為 暗含的連線指令碼
如果聯結器不認識某個輸入檔案,那麼該檔案被當作連線指令碼被解析。更進一步,如果發現它的格式又不是連線指令碼的格式,那麼聯結器報錯。
一個暗含的連線指令碼不會替換預設的連線指令碼,僅僅是增加新的連線而已。
一般來說,暗含的連線指令碼符號分配命令,或INPUT、GROUP、VERSION命令。
在連線命令列中,每個輸入檔案的順序都被固定好了,暗含的連線指令碼在連線命令列內佔住一個位置,這個位置決定了由該連線指令碼指定的輸入檔案在連線過程中的順序。
典型的暗含的連線指令碼是libc.so檔案,在GNU/linux內一般存在/usr/lib目錄下。


References
1, gnu ld線上手冊

2, 程式的連結和裝入及Linux下動態連結的實現

3, UNIX/Linux平臺可執行檔案格式分析

4, John R. Levine.《Linkers & Loaders》