天天看點

Linux下的lds連結腳本詳解【轉】

轉自:http://www.cnblogs.com/li-hao/p/4107964.html

轉載自:http://linux.chinaunix.net/techdoc/beginner/2009/08/12/1129972.shtml

一、 概論

每一個連結過程都由連結腳本(linker script, 一般以lds作為檔案的字尾名)控制. 連結腳本主要用于規定如何把輸入檔案内的section放入輸出檔案内, 并控制輸出檔案内各部分在程式位址空間内的布局. 但你也可以用連接配接指令做一些其他事情.

連接配接器有個預設的内置連接配接腳本, 可用ld –verbose檢視. 連接配接選項-r和-N可以影響預設的連接配接腳本(如何影響?).

-T選項用以指定自己的連結腳本, 它将代替預設的連接配接腳本。你也可以使用以增加自定義的連結指令.

以下沒有特殊說明,連接配接器指的是靜态連接配接器.

二、基本概念

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

輸入檔案: 目标檔案或連結腳本檔案.

輸出檔案: 目标檔案或可執行檔案.

目标檔案(包括可執行檔案)具有固定的格式, 在UNIX或GNU/Linux平台下, 一般為ELF格式

有時把輸入檔案内的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位址是0×08050000, 該section内包含了3個32位全局變量, i、j和k, 分别為1,2,3.

(2) .text section内包含由”printf( “j=%d “, j );”程式片段産生的代碼.

連接配接時指定.data section的VMA為0×08050000, 産生的printf指令是将位址為0×08050004處的4位元組内容作為一個整數列印出來。

如果.data section的LMA為0×08050000,顯然結果是j=2

如果.data section的LMA為0×08050004,顯然結果是j=1

還可這樣了解LMA:

.text section内容的開始處包含如下兩條指令(intel i386指令是10位元組,每行對應5位元組):

jmp 0×08048285

movl $0×1,%eax

如果.text section的LMA為0×08048280, 那麼在程序位址空間内0×08048280處為“jmp 0×08048285”指令, 0×08048285處為movl $0×1,%eax指令. 假設某指令跳轉到位址0×08048280, 顯然它的執行将導緻%eax寄存器被指派為1.

如果.text section的LMA為0×08048285, 那麼在程序位址空間内0×08048285處為“jmp 0×08048285”指令, 0×0804828a處為movl $0×1,%eax指令. 假設某指令跳轉到位址0×08048285, 顯然它的執行又跳轉到程序位址空間内0×08048285處, 造成死循環.

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

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

三、 腳本格式

連結腳本由一系列指令組成, 每個指令由一個關鍵字(一般在其後緊跟相關參數)或一條對符号的指派語句組成. 指令由分号‘;’分隔開.

檔案名或格式名内如果包含分号’;'或其他分隔符, 則要用引号‘”’将名字全稱引用起來. 無法處理含引号的檔案名.

/* */之間的是注釋。

四、 簡單例子

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

以下腳本将輸出檔案的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的大小. 注意: 此處沒有考慮對齊限制.

五、 簡單腳本指令

ENTRY(SYMBOL) :将符号SYMBOL的值設定成入口位址。

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

ld有多種方法設定程序入口位址, 按一下順序: (編号越前, 優先級越高)

1, ld指令行的-e選項

2, 連接配接腳本的ENTRY(SYMBOL)指令

3, 如果定義了start符号, 使用start符号值

4, 如果存在.text section, 使用.text section的第一位元組的位置值

5, 使用值0

INCLUDE filename : 包含其他名為filename的連結腳本

相當于c程式内的的#include指令, 用以包含另一個連結腳本.

腳本搜尋路徑由-L選項指定. INCLUDE指令可以嵌套使用, 最大深度為10. 即: 檔案1内INCLUDE檔案2, 檔案2内INCLUDE檔案3… , 檔案10内INCLUDE檔案11. 那麼檔案11内不能再出現 INCLUDE指令了.

INPUT(files): 将括号内的檔案做為連結過程的輸入檔案

ld首先在目前目錄下尋找該檔案, 如果沒找到, 則在由-L指定的搜尋路徑下搜尋. file可以為 -lfile形式,就象指令行的-l選項一樣. 如果該指令出現在暗含的腳本内, 則該指令内的file在連結過程中的順序由該暗含的腳本在指令行内的順序決定.

GROUP(files) : 指定需要重複搜尋符号定義的多個輸入檔案

file必須是庫檔案, 且file檔案作為一組被ld重複掃描,直到不在有新的未定義的引用出現。

OUTPUT(FILENAME) : 定義輸出檔案的名字

同ld的-o選項, 不過-o選項的優先級更高. 是以它可以用來定義預設的輸出檔案名. 如a.out

SEARCH_DIR(PATH) :定義搜尋路徑,

同ld的-L選項, 不過由-L指定的路徑要比它定義的優先被搜尋。

STARTUP(filename) : 指定filename為第一個輸入檔案

在連結過程中, 每個輸入檔案是有順序的. 此指令設定檔案filename為第一個輸入檔案。

OUTPUT_FORMAT(BFDNAME) : 設定輸出檔案使用的BFD格式

同ld選項-o format BFDNAME, 不過ld選項優先級更高.

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的聯機幫助, 裡面也包括了對這些指令的介紹.

六、 對符号的指派

在目标檔案内定義的符号可以在連結腳本内被指派. (注意和C語言中指派的不同!) 此時該符号被定義為全局的. 每個符号都對應了一個位址, 此處的指派是更改這個符号對應的位址.

舉例. 通過下面的程式檢視變量a的位址:

a.c檔案

/* a.c */

#include <stdio.h>

int a = 100;

int main()

printf( "&a=%p\n", &a );

return 0;

a.lds檔案

/* a.lds */

a = 3;

編譯指令:

$ gcc -Wall -o a-without-lds.exe a.c

運作結果:

&a = 0×601020

$ gcc -Wall -o a-with-lds.exe a.c a.lds

&a = 0×3

注意: 對符号的指派隻對全局變量起作用!

對于一些簡單的指派語句,我們可以使用任何c語言文法的指派操作:

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描述内和全局位置。

示例1:

floating_point = 0; /* 全局位置 */

.text :

*(.text)

_etext = .; /* section描述内 */

_bdata = (. + 3) & ~ 4; /* SECTIONS指令内 */

PROVIDE關鍵字

該關鍵字用于定義這類符号:在目标檔案内被引用,但沒有在任何目标檔案内被定義的符号。

示例2:

_etext = .;

PROVIDE(etext = .);

這裡,當目标檔案内引用了etext符号,卻沒有定義它時,etext符号對應的位址被定義為.text section之後的第一個位元組的位址。

七、 SECTIONS指令

SECTIONS指令告訴ld如何把輸入檔案的sections映射到輸出檔案的各個section: 如何将輸入section合為輸出section; 如何把輸出section放入程式位址空間(VMA)和程序位址空間(LMA).

該指令格式如下:

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。

7.1、輸出section描述(基本)

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

SECTION-NAME [ADDRESS] [(TYPE)] : [AT(LMA)]

OUTPUT-SECTION-COMMAND

} [>REGION] [AT>LMA_REGION] [:PHDR HDR ...] [=FILLEXP]

[ ]内的内容為可選選項, 一般不需要.

SECTION-NAME:section名字.SECTION-NAME左右的空白、圓括号、冒号是必須的,換行符和其他空格是可選的。

7.1.1、輸出section名字

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

7.1.2、輸出section位址

輸出section位址[ADDRESS]是一個表達式,它的值用于設定VMA。如果沒有該選項且有REGION選項,那麼連接配接器将根據REGION設定VMA;如果也沒有REGION選項,那麼連接配接器将根據定位符号‘.’的值設定該section的VMA,将定位符号的值調整到滿足輸出section對齊要求後的值,這時輸出 section的對齊要求為:該輸出section描述内用到的所有輸入section的對齊要求中最嚴格的對齊要求。

例子:

.text . : { *(.text) }和.text : { *(.text) }

這兩個描述是截然不同的,第一個将.text section的VMA設定為定位符号的值,而第二個則是設定成定位符号的修調值,滿足對齊要求後的。

ADDRESS可以是一個任意表達式,比如,ALIGN(0×10)這将把該section的VMA設定成定位符号的修調值,滿足16位元組對齊後的。

注意:設定ADDRESS值,将更改定位符号的值。

7.1.3、輸出section描述

輸出section描述OUTPUT-SECTION-COMMAND為以下四種之一:

(1).符号指派語句

(2).輸入section描述

(3).直接包含的資料值

(4).一些特殊的輸出section關鍵字

7.1.3.1、符号指派語

符号指派語句已經在《Linux下的lds連結腳本基礎(一)》前文介紹過,這裡就不累述。

7.1.3.2、輸入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 {

.data1 : { data.o(.data) }

data.o檔案的.data section在第一個OUTPUT-SECTION-COMMAND指令内被使用了,那麼在第二個OUTPUT-SECTION-COMMAND指令内将不會再被使用,也就是說即使連接配接器不報錯,輸出檔案的.data1 section的内容也是空的。

再次強調:連接配接器依次掃描每個OUTPUT-SECTION-COMMAND指令内的檔案名,任何一個檔案的任何一個section都隻能使用一次。

讀者可以用-M連接配接指令選項來産生一個map檔案,它包含了所有輸入section到輸出section的組合資訊。

再看個例子,

.DATA : { [A-Z]*(.data) }

這個例子中說明,所有檔案的輸入.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相關例子:

outputa 0×10000 :

all.o

foo.o (.input1)

outputb :

foo.o (.input2)

foo1.o (.input1)

outputc :

*(.input1)

*(.input2)

本例中,将all.o檔案的所有section和foo.o檔案的所有(一個檔案内可以有多個同名section).input1 section依次放入輸出outputasection内,該section的VMA是0×10000;将foo.o檔案的所有.input2 section和foo1.o檔案的所有.input1 section依次放入輸出outputb section内,該section的VMA是目前定位器符号的修調值(對齊後);将其他檔案(非all.o、foo.o、foo1.o)檔案的. input1section和.input2 section放入輸出outputc section内。

7.1.3.3、直接包含資料值

可以顯示地在輸出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(0×9090)。在輸出section描述中可以有=FILEEXP屬性,它的作用如同FILE()指令,但是FILE指令隻作用于該FILE指令之後的section區域,而=FILEEXP屬性作用于整個輸出section區域,且FILE指令的優先級更高!!!

7.1.3.4、特殊的輸出section關鍵字

在輸出section描述OUTPUT-SECTION-COMMAND中還可以使用一些特殊的輸出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)

__DTOR_END__ = .;

如果使用GNU C++提供的初始化優先級支援(它能控制每個全局構造函數調用的先後順序),那麼請在連接配接腳本内把CONSTRUCTORS替換成SORT (CONSTRUCTS),把*(.ctors)換成*(SORT(.ctors)),把*(.dtors)換成*(SORT(.dtors))。一般來說,預設的連接配接腳本已作好的這些工作。

修改定位器

我們可以對定位器符合。進行指派來修改定位器的值。

示例

. = SIZEOF_HEADERS;

輸出section的丢棄

對于.foo: { *(.foo) },如果沒有任何一個輸入檔案包含.foo section,那麼連接配接器将不會建立.foo輸出section。但是如果在這些輸出section描述内包含了非輸入section描述指令(如符号指派語句),那麼連接配接器将總是建立該輸出section。

另外,有一個特殊的輸出section,名為/DISCARD/,被該section引用的任何輸入section将不會出現在輸出檔案内,這就是DISCARD的意思吧。如果/DISCARD/ section被它自己引用呢?想想看。

7.2、輸出section描述(進階)

我們再回顧以下輸出section描述的文法:

前面我們介紹了SECTION、ADDRESS、OUTPUT-SECTION-COMMAND相關資訊,下面我們将介紹其他屬性。

7.2.1、輸出section的類型

可以通過[(TYPE)]設定輸出section的類型。如果沒有指定TYPE類型,那麼連接配接器根據輸出section引用的輸入section的類型設定該輸出section的類型。它可以為以下五種值,

NOLOAD :該section在程式運作時,不被載入記憶體。

DSECT,COPY,INFO,OVERLAY :這些類型很少被使用,為了向後相容才被保留下來。這種類型的section必須被标記為“不可加載的”,以便在程式運作不為它們配置設定記憶體。

預設值是多少呢?Puzzle!

7.2.2、輸出section的LMA 

預設情況下,LMA等于VMA,但可以通過[AT(LMA)]項,即關鍵字AT()指定LMA。

用關鍵字AT()指定,括号内包含表達式,表達式的值用于設定LMA。如果不用AT()關鍵字,那麼可用AT>LMA_REGION表達式設定指定該section加載位址的範圍。這個屬性主要用于構件ROM境象。

.text 0×1000 : {_etext = . ;*(.text);  }

.mdata 0×2000 :

AT ( ADDR (.text) + SIZEOF (.text) )

{ _data = . ; *(.data); _edata = . ; }

.bss 0×3000 :

{ _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 rom }

7.2.3、設定輸出section所在的程式段

可以通過[:PHDR HDR ...]項将輸出section放入預先定義的程式段(program segment)内。如果某個輸出section設定了它所在的一個或多個程式段,那麼接下來定義的輸出section的預設程式段與該輸出 section的相同。除非再次顯示地指定。例子,

PHDRS { text PT_LOAD ; }

SECTIONS { .text : { *(.text) } :text }

可以通過:NONE指定連接配接器不把該section放入任何程式段内。詳情請檢視PHDRS指令

7.2.4、設定輸出section的填充模版

這個在前面提到過,任何輸出section描述内的未指定的記憶體區域,連接配接器用該模版填充該區域。我們可以通過[=FILLEXP]項設定填充值。用法:=FILEEXP,前兩位元組有效,當區域大于兩位元組時,重複使用這兩位元組以将其填滿。例子,

SECTIONS { .text : { *(.text) } =0×9090 }

7.3、覆寫圖(overlay)描述

覆寫圖描述使兩個或多個不同的section占用同一塊程式位址空間。覆寫圖管理代碼負責将section的拷入和拷出。考慮這種情況,當某存儲塊的通路速度比其他存儲塊要快時,那麼如果将section拷到該存儲塊來執行或通路,那麼速度将會有所提高,覆寫圖描述就很适合這種情形。文法如下,

OVERLAY [START] : [NOCROSSREFS] [AT ( LDADDR )]

SECNAME1

} [:PHDR...] [=FILL]

SECNAME2

} [>REGION] [:PHDR...] [=FILL]

由以上文法可以看出,同一覆寫圖内的section具有相同的VMA。這裡VMA由[START] 決定。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 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);

八、 記憶體區域指令

在預設情形下,連接配接器可以為section在程式位址空間内配置設定任意位置的存儲區域。并通過輸出section描述的> REGION屬性顯示地将該輸出section限定于在程式位址空間内的某塊存儲區域,當存儲區域大小不能滿足要求時,連接配接器會報告該錯誤。

你也可以用MEMORY指令讓在SECTIONS指令内*未*引用的selection配置設定在程式位址空間内的某個存儲區域内。

注意:以下存儲區域指的是在程式位址空間内的。

MEMORY指令的文法如下,

MEMORY {

NAME1 [(ATTR)] : ORIGIN = ORIGIN1, LENGTH = LEN1

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 = 0×40000000, l = 4M

此例中,把在SECTIONS指令内*未*引用的且具有讀屬性或寫屬性的輸入section放入rom區域内,把其他未引用的輸入section放入 ram。如果某輸出section要被放入某記憶體區域内,而該輸出section又沒有指明ADDRESS屬性,那麼連接配接器将該輸出section放在該區域内下一個能使用位置。

九、 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可以是以下八種形式,

PT_NULL 0

表示未被使用的程式段

PT_LOAD 1

表示該程式段在程式運作時應該被加載

PT_DYNAMIC 

表示該程式段包含動态連接配接資訊

PT_INTERP 3

表示該程式段内包含程式加載器的名字,在linux下常見的程式加載器是ld-linux.so.2

PT_NOTE 4

表示該程式段内包含程式的說明資訊

PT_SHLIB 5

一個保留的程式頭類型,沒有在ELF ABI文檔内定義

PT_PHDR 6

表示該程式段包含程式頭資訊。

EXPRESSION 表達式值

以上每個類型都對應一個數字,該表達式定義一個使用者自定的程式頭。

在TYPE屬性後存在FILEHDR關鍵字,表示該段包含ELF檔案頭資訊;存在PHDRS關鍵字,表示該段包含ELF程式頭資訊。

AT(ADDRESS)屬性定義該程式段的加載位置(LMA),該屬性将**覆寫**該程式段内的section的AT()屬性。

預設情況下,連接配接器會根據該程式段包含的section的屬性(什麼屬性?好象在輸出section描述内沒有看到)設定FLAGS标志,該标志用于設定程式段描述的p_flags域。

下面看一個典型的PHDRS設定

headers PT_PHDR PHDRS ;

interp PT_INTERP ;

text PT_LOAD FILEHDR PHDRS ;

data PT_LOAD ;

dynamic PT_DYNAMIC ;

.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

十、版本号指令

當使用ELF目标檔案格式時,連接配接器支援帶版本号的符号。版本号也隻限于ELF檔案格式。

讀者可以發現僅僅在共享庫中,符号的版本号屬性才有意義。動态加載器使用符号的版本号為應用程式選擇共享庫内的一個函數的特定實作版本。

可以在連接配接腳本内直接使用版本号指令,也可以将版本号指令實作于一個特定版本号描述檔案(用連接配接選項–version-script指定該檔案)。

該指令的文法如下,

VERSION { version-script-commands }

 以下讨論用gcc

10.1. 帶版本号的符号的定義(共享庫内)

檔案b.c内容如下,

int getVersion()

return 1;

寫連接配接器的版本控制腳本,本例中為b.lds,内容如下

VER1.0{

getVersion;

};

VER2.0{

$gcc -c b.c

$gcc -shared -Wl,--version-script=b.lds -o libb.so b.o

可以在{}内填入要綁定的符号,本例中getVersion符号就與VER1.0綁定了。

那麼如果有一個應用程式連接配接到該庫的getVersion符号,那麼它連接配接的就是VER1.0版本的getVersion符号

如果我們對b.c檔案進行了更新,更改如下:

return 101;

這裡我對getVersion()進行了更改,其傳回值的意義也進行改變,也就是它和前不相容:

為了程式的安全,我們把b.lds更改為,

然後生成新的libb.so檔案。

這時如果我們運作app.exe(它已經連接配接到VER1.0版本的getVersion()),就會發現該應用程式不能運作了。

提示資訊如下:

./app.exe: relocation error: ./app.exe: symbol getVersion, version VER1.0 not defined in file libb.so with link time reference

因為庫内沒有VER1.0版本的getVersion(),隻有VER2.0版本的getVersion()。

10.2、參看連接配接的符号的版本

對上面生成的app.exe執行以下指令:

nm app.exe | grep getVersion

結果

U new_true@@VER1.0

用nm指令發現app連接配接到VER1.0版本的getVersion

10.3、 GNU的擴充

在GNU中,允許在程式檔案内綁定 *符号* 到 *帶版本号的别名符号*

int old_getVersion()

int new_getVersion()

__asm__(".symver old_getVersion,[email protected]");

__asm__(".symver new_getVersion,getVersion@@VER2.0");

其中,對于VER1.0版本号的getVersion别名符号是old_getVersion;

對于VER2.0版本号的getVersion别名符号是new_getVersion,

在連接配接時,預設的版本号為VER2.0

供連接配接器用的版本控制腳本b.lds内容如下,

版本控制檔案内必須包含版本VER1.0和版本VER2.0的定義,因為在b.c檔案内有對他們的引用

再次執行以下指令編譯連接配接b.c和app.c

gcc -c src/b.c

gcc -shared -Wl,--version-script=./lds/b.lds -o libb.so b.o

gcc -o app.exe ./src/app.c libb.so

運作:

./app.exe

結果:

Version=0x65

說明app.exe的确是連接配接的VER2.0的getVersion,即new_getVersion()

我們再對app.c進行修改,以使它連接配接的VER1.0的getVersion,即old_getVersion()

app.c檔案:

__asm__(".symver getVersion,[email protected]");

extern int getVersion();

printf("Version=%p\n", getVersion());

再次編譯連接配接b.c和app.c

Version=0x1

說明此次app.exe的确是連接配接的VER1.0的getVersion,即old_getVersion()

十一、 表達式

lds中表達式的文法與C語言的表達式文法一緻,表達式的值都是整型,如果ld的運作主機和生成檔案的目标機都是32位,則表達式是32位資料,否則是64位資料。

以下是一些常用的表達式:

_fourk_1 = 4K; /* K、M機關 */

_fourk_2 = 4096; /* 整數 */

_fourk_3 = 0×1000; /* 16 進位 */

_fourk_4 = 01000; /* 8 進位 */

注意:1K=1024 1M=1024*1024

11.1、符号名

沒有被引号”"包圍的符号,以字母、下劃線或’.'開頭,可包含字母、下劃線、’.'和’-'。當符号名被引号包圍時,符号名可以與關鍵字相同。如,

“SECTION”=9;

“with a space” = “also with a space” + 10;

11.2、定位符号’.'

隻在SECTIONS指令内有效,代表一個程式位址空間内的位址。

注意:在連接配接時,當定位符用在SECTIONS指令的輸出section描述内時,它代表的是該section的目前**偏移**,而不是程式位址空間的絕對位址。當然當程式載入後,符号最後的位址還是程式位址空間的絕對位址。

示例11.2_1:

output :

file1(.text)

. = . + 1000;

file2(.text)

. += 1000;

file3(.text)

} = 0×1234;

其中由于對定位符的指派而産生的空隙由0×1234填充。其他的内容應該容易了解吧。

示例11.2_2:

. = 0×100

.text: {

. = 0×200

. = 0×500

.data: {

*(.data)

. += 0×600

.text section在程式位址空間的開始位置是0x100

示例11.2_3

檔案src\a.c

int b=0;

int c=0;

int d=1;

printf( "&b=%p\n", &b );

printf( "&c=%p\n", &c );

printf( "&d=%p\n", &d );

檔案lds\a.lds

a = 10; /* 全局位置 */

b = 11;

c = .; /* section描述内 */

. = 10000;

d = .;

在沒有使用a.lds情況下編譯

gcc -Wall -o a-without-lds.exe ./src/a.c

運作./a-without-lds.exe

&a=0x601020

&b=0x601038

&c=0x60103c

&d=0x601024

在使用a.lds情況下編譯

gcc -Wall -o a-with-lds.exe ./src/a.c ./lds/a.lds

運作./a-with-lds.exe

&a=0xa

&b=0xb

&c=0x400638

&d=0x402b20

10.3、表達式的操作符

在lds中,表達式的操作符與C語言一緻。

優先級 結合順序 操作符

1 left ! – ~ (1)

2 left * / %

3 left + -

4 left >>  =

5 left &

6 left |

7 left &&

8 left ||

9 right ? :

10 right &= += -= *= /= (2)

(1)表示字首符,(2)表示指派符。

10.4、表達式的計算

連接配接器延遲計算大部分表達式的值。

但是,對待與連接配接過程緊密相關的表達式,連接配接器會立即計算表達式,如果不能計算則報錯。比如,對于section的VMA位址、記憶體區域塊的開始位址和大小,與其相關的表達式應該立即被計算。

.text 9+this_isnt_constant :

{ *(.text) }

這個例子中,9+this_isnt_constant表達式的值用于設定.text section的VMA位址,是以需要立即運算,但是由于this_isnt_constant變量的值不确定,是以此時連接配接器無法确立表達式的值,此時連接配接器會報錯。

10.5、相對值與絕對值

在輸出section描述内的表達式,連接配接器取其相對值,相對與該section的開始位置的偏移

在SECTIONS指令内且非輸出section描述内的表達式,連接配接器取其絕對值

通過ABSOLUTE關鍵字可以将相對值轉化成絕對值,即在原來值的基礎上加上表達式所在section的VMA值。

.data : { *(.data) ;_edata = ABSOLUTE(.); }

該例子中,_edata符号的值是.data section的末尾位置(絕對值,在程式位址空間内)。

10.6、内建函數

lds中有以下一些内建函數:

ABSOLUTE(EXP) :轉換成絕對值

ADDR(SECTION) :傳回某section的VMA值。

ALIGN(EXP) :傳回定位符’.'的按照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 :傳回輸出檔案頭部的位元組數。這些資訊出現在輸出檔案的開始處。當設定第一個段的開始位址時,你可以使用這個數字。如果你選擇了加速分頁,當産生一個ELF輸出檔案時,如果連結器腳本使用SIZEOF_HEADERS内建函數,連接配接器必須在它

算出所有段位址和長度之前計算程式頭部的數值。如果連接配接器後來發現它需要附加程式頭,它将報告一個“not enough room for 

program headers”錯誤。為了避免這樣的錯誤,你必須避免使用SIZEOF_HEADERS函數,或者你必須修改你的連接配接器腳本去避免強制

連接配接器去使用附加程式頭,或者你必須使用PHDRS指令去定義你自己的程式頭

十二、 暗含的連接配接腳本

輸入檔案可以是目标檔案,也可以是連接配接腳本,此時的連接配接腳本被稱為 暗含的連接配接腳本

如果連接配接器不認識某個輸入檔案,那麼該檔案被當作連接配接腳本被解析。更進一步,如果發現它的格式又不是連接配接腳本的格式,那麼連接配接器報錯。

一個暗含的連接配接腳本不會替換預設的連接配接腳本,僅僅是增加新的連接配接而已。

一般來說,暗含的連接配接腳本符号配置設定指令,或INPUT、GROUP、VERSION指令。

在連接配接指令行中,每個輸入檔案的順序都被固定好了,暗含的連接配接腳本在連接配接指令行内占住一個位置,這個位置決定了由該連接配接腳本指定的輸入檔案在連接配接過程中的順序。

典型的暗含的連接配接腳本是libc.so檔案,在GNU/linux内一般存在/usr/lib目錄下。

本文轉自張昺華-sky部落格園部落格,原文連結:http://www.cnblogs.com/sky-heaven/p/8275886.html,如需轉載請自行聯系原作者