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》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章