GNU-ld連接腳本淺析

0. Contents

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


1. 概論php

每個連接過程都由連接腳本(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,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位數據。 
可以在表達式內使用符號的值,設置符號的值。
下面看六項表達式相關內容,

常表達式:
_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目錄下。
相關文章
相關標籤/搜索