舉例. 經過下面的程序查看變量a的地址:
被賦值後,符號所屬的section被設值爲表達式EXPRESSION所屬的SECTION(參看11. 腳本內的表達式)
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。
7.一、輸出section描述(基本)
輸出section描述具備以下格式:
SECTION-NAME
[
ADDRESS
] [(
TYPE
)]
:
[
AT
(
LMA
)]
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
…
} [
>REGION
] [AT>LMA_REGION] [:PHDR HDR ...] [=
FILLEXP
]
[ ]內的內容爲可選選項, 通常不須要.
SECTION-NAME:section名字.
SECTION-NAME左右的空白、圓括號、冒號是必須的,換行符和其餘空格是可選的。
7.1.一、輸出section名字
輸出section名字SECTION-NAME
必須符合輸出文件格式要求,好比:
a.out格式的文件只容許存在.text、.data和.bss section名。
而有的格式只容許存在
數字名字
,那麼此時應該用引號將全部名字內的數字組合在一塊兒;另外,還有一些格式容許任何序列的字符存在於section名字內,此時若是名字內包含特殊字符(好比空格、逗號等),那麼須要用引號將其組合在一塊兒。
7.1.二、輸出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.三、輸出section描述
輸出section描述OUTPUT-SECTION-COMMAND爲如下四種之一:
(1).
符號賦值語句
(2).
輸入section描述
(3).
直接包含的數據值
(4).
一些特殊的輸出section關鍵字
7.1.3.一、符號賦值語
符號賦值語句
已經在《
Linux下的lds連接腳本基礎(一)》前文介紹過,這裏就不累述。
7.1.3.二、輸入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
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.三、直接包含數據值
能夠顯示地在輸出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.四、特殊的輸出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)
LONG
(0)
__DTOR_END__
= .;
若是使用GNU C++提供的初始化優先級支持(它能控制每一個全局構造函數調用的前後順序),那麼
請在鏈接腳本內把CONSTRUCTORS替換成SORT (CONSTRUCTS),把*(.ctors)換成*(SORT(.ctors)),把*(.dtors)換成*(SORT(.dtors))。通常來講,默認的鏈接腳本已做好的這些工做。
修改定位器
咱們能夠對定位器符合。進行賦值來修改定位器的值。
示例
SECTIONS
{
.
= SIZEOF_HEADERS;
.text : { *(.text) }
.
= 0×10000;
.data : { *(.data) }
.
= 0×8000000;
.bss : { *(.bss) }
}
輸出section的丟棄
對於
.foo: { *(.foo) },若是沒有任何一個輸入文件包含.foo section,那麼鏈接器將不會建立.foo輸出section。可是若是在這些輸出section描述內包含了非輸入section描述命令(如符號賦值語句),那麼鏈接器將老是建立該輸出section。
另外,有一個特殊的輸出section,名爲
/DISCARD/
,
被該section引用的任何輸入section將不會出如今輸出文件內
,這就是DISCARD的意思吧。若是/DISCARD/ section被它本身引用呢?想一想看。
7.二、輸出section描述(進階)
咱們再回顧如下輸出section描述的文法:
SECTION-NAME
[
ADDRESS
] [(
TYPE
)]
:
[
AT
(
LMA
)]
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
…
} [>REGION] [AT>LMA_REGION] [
:
PHDR HDR ...
] [=
FILLEXP
]
前面咱們介紹了
SECTION、
ADDRESS、
OUTPUT-SECTION-COMMAND相關信息,下面咱們將介紹其餘屬性。
7.2.一、輸出section的類型
能夠經過
[(
TYPE
)]
設置輸出section的類型。
若是沒有指定TYPE類型,那麼鏈接器根據輸出section引用的輸入section的類型設置該輸出section的類型。它能夠爲如下五種值,
NOLOAD :該section在程序運行時,不被載入內存。
DSECT,COPY,INFO,OVERLAY :這些類型不多被使用,爲了向後兼容才被保留下來。這種類型的section必須被標記爲「
不可加載的」,以便在程序運行不爲它們分配內存。
默認值是多少呢?Puzzle!
7.2.二、輸出section的LMA
默認狀況下,LMA等於VMA,但能夠經過
[
AT
(
LMA
)]
項,即關鍵字
AT()
指定LMA。
用關鍵字AT()指定,括號內包含表達式,表達式的值用於設置LMA。若是不用AT()關鍵字,那麼可用
AT>
LMA_REGION表
達式設置指定該section加載地址的範圍。這個屬性主要用於構件ROM境象。
例子,
SECTIONS
{
.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.三、設置輸出section所在的程序段
能夠經過
[
:
PHDR HDR ...
]
項將輸出section放入預先定義的程序段(program segment)內。若是某個輸出section設置了它所在的一個或多個程序段,那麼接下來定義的輸出section的默認程序段與該輸出 section的相同。除非再次顯示地指定。例子,
PHDRS
{
text
PT_LOAD ; }
SECTIONS
{ .text : { *(.text) } :
text
}
能夠經過
:NONE指定鏈接器不把該section放入任何程序段內。詳情請查看PHDRS命令
7.2.四、設置輸出section的填充模版
這個在前面提到過,任何輸出section描述內的未指定的內存區域,鏈接器用該模版填充該區域。咱們能夠經過
[=
FILLEXP
]
項設置填充值。用法:
=FILEEXP
,前兩字節有效,當區域大於兩字節時,重複使用這兩字節以將其填滿。例子,
SECTIONS { .text : { *(.text) } =
0×9090
}
7.三、覆蓋圖(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。這裏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);
8、 內存區域命令
在默認情形下,鏈接器能夠爲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放在該區域內下一個能使用位置。
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能夠是如下八種形式,
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設置
示例
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 */
…
. = . + 0×1000;
/* move to a new page in memory */
.data
: { *(.data) }
:data
.dynamic
: { *(.dynamic) }
:data
:dynamic
…
}
10、版本號命令
當使用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文件進行了升級,更改以下:
int
getVersion
()
{
return
101;
}
這裏我對getVersion()進行了更改,其返回值的意義也進行改變,也就是它和前不兼容:
爲了程序的安全,咱們把b.lds更改成,
VER1.0
{
};
VER2.0
{
getVersion;
};
而後生成新的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.二、參看鏈接的符號的版本
對上面生成的app.exe執行如下命令:
nm
app.exe
| grep getVersion
結果
U new_true@@VER1.0
用nm命令發現app鏈接到VER1.0版本的getVersion
10.三、 GNU的擴充
在GNU中,容許在程序文件內綁定 *符號* 到 *帶版本號的別名符號*
文件b.c內容以下,
int
old_getVersion
()
{
return 1;
}
int
new_getVersion
()
{
return 101;
}
__asm__
(".symver
old_getVersion
,
getVersion
@
VER1.0
");
__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
{
};
版本控制文件內必須包含版本
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文件:
#include <stdio.h>
__asm__
(".symver
getVersion
,
getVersion@VER1.0
");
extern int
getVersion()
;
int
main()
{
printf("Version=%p\n",
getVersion()
);
return
0;
}
再次編譯鏈接b.c和app.c
結果:
Version=0x1
說明這次app.exe的確是鏈接的VER1.0的getVersion,即
old
_getVersion
()
11、 表達式
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.一、符號名
沒有被引號」"包圍的符號,以字母、下劃線或’.'開頭,可包含字母、下劃線、’.'和’-'。當符號名被引號包圍時,符號名能夠與關鍵字相同。如,
「SECTION」=9;
「with a space」 = 「also with a space」 + 10;
11.二、定位符號’.'
只在SECTIONS命令內有效,表明一個程序地址空間內的地址。
注意:在鏈接時,
當定位符用在SECTIONS命令的
輸出section描述內
時,它表明的是該section的當前**
偏移**,而不是程序地址空間的絕對地址。固然當程序載入後,符號最後的地址仍是程序地址空間的絕對地址。
示例11.2_1:
SECTIONS
{
output
:
{
file1(.text)
.
=
.
+ 1000;
file2(.text)
.
+= 1000;
file3(.text)
} = 0×1234;
}
其中因爲對定位符的賦值而產生的空隙由0×1234填充。其餘的內容應該容易理解吧。
示例11.2_2:
SECTIONS
{
.
= 0×100
.text
: {
*(.text)
.
=
0×200
}
.
= 0×500
.data
: {
*(.data)
.
+= 0×600
}
}
.text section在程序地址空間的開始位置是
0x100
示例11.2_3
文件src\a.c
#include <stdio.h>
int a = 100;
int b=0;
int c=0;
int d=1;
int main()
{
printf( "&a=%p\n", &a );
printf( "&b=%p\n", &b );
printf( "&c=%p\n", &c );
printf( "&d=%p\n", &d );
return 0;
}
文件lds\a.lds
a = 10;
/* 全局位置 */
SECTIONS
{
b
= 11;
.text
:
{
*(.text)
c = .;
/* section描述內 */
. =
10000;
d = .;
}
_bdata = (. + 3) & ~ 4;
/* SECTIONS命令內 */
.data : { *(.data) }
}
在沒有使用
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.三、表達式的操做符
在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.四、表達式的計算
鏈接器延遲計算大部分表達式的值。
可是,對待與鏈接過程緊密相關的表達式,鏈接器會當即計算表達式,若是不能計算則報錯。好比,
對於section的VMA地址、內存區域塊的開始地址和大小,與其相關的表達式應該當即被計算。
例子,
SECTIONS
{
.text
9+this_isnt_constant
:
{ *(.text) }
}
這個例子中,9+this_isnt_constant表達式的值用於設置.text section的VMA地址,所以須要當即運算,可是因爲this_isnt_constant變量的值不肯定,因此此時鏈接器沒法確立表達式的值,此時鏈接器會報錯。
10.五、相對值與絕對值
在輸出
section描述內
的表達式,鏈接器取其
相對值
,相對與該section的開始位置的偏移
在
SECTIONS命令內且非輸出section描述內
的表達式,鏈接器取其
絕對值
經過ABSOLUTE關鍵字能夠將相對值轉化成絕對值,即在原來值的基礎上加上表達式所在section的VMA值。
示例
SECTIONS
{
.data
: { *(.data) ;_edata =
ABSOLUTE
(
.
); }
}
該例子中,_edata符號的值是.data section的末尾位置(絕對值,在程序地址空間內)。
10.六、內建函數
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命令去定義你本身的程序頭
12、 暗含的鏈接腳本
輸入文件能夠是目標文件,也能夠是鏈接腳本,此時的鏈接腳本被稱爲 暗含的鏈接腳本
若是鏈接器不認識某個輸入文件,那麼該文件被看成鏈接腳本被解析。更進一步,若是發現它的格式又不是鏈接腳本的格式,那麼鏈接器報錯。
一個暗含的鏈接腳本不會替換默認的鏈接腳本,僅僅是增長新的鏈接而已。
通常來講,暗含的鏈接腳本符號分配命令,或INPUT、GROUP、VERSION命令。
在鏈接命令行中,每一個輸入文件的順序都被固定好了,暗含的鏈接腳本在鏈接命令行內佔住一個位置,這個位置決定了由該鏈接腳本指定的輸入文件在鏈接過程當中的順序。
典型的暗含的鏈接腳本是libc.so文件,在GNU/linux內通常存在/usr/lib目錄下。