天天看點

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的位址:

#include <stdio.h>

int a = 100;

int main(void)

{

    printf( "&a=0x%p ", &a );

    return 0;

}

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 = .;

}

_bdata = (. + 3) & ~ 4;

.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;

while (dst < &_edata) {

*dst++ = *src++;

}

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

...

. = . + 0x1000;

.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;

_fourk_2 = 4096;

_fourk_3 = 0x1000;

_fourk_4 = 01000;

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》

原文見:http://blog.csdn.net/yili_xie/article/details/5692007