為了便于與英文原文對照學習與理解(部分翻譯可能不準確),本文中的每個子章節標題和參考使用的都是官方手冊英文原稱,命令及命令列選項統一使用斜體書寫,高頻小節會用藍色字體標出,
3 Linker Scripts
每個鏈接都由一個鏈接腳本控制,該腳本使用聯結器命令語言撰寫,
鏈接腳本的主要目的是描述如何將輸入檔案中的各個部分映射到輸出檔案中,并控制輸出檔案的記憶體布局,大多數鏈接腳本僅此而已,但是,必要時,聯結器腳本也可以使用下面描述的命令來指導聯結器執行更多的其它操作,
聯結器通常使用一個鏈接腳本,如果沒有為其提供,聯結器將會使用默認的編譯在聯結器執行檔案內部的腳本,可以使用命令 ’– verbose ’ 顯示默認的鏈接腳本,某些命令列選項,例如 ’-r ’,’-N ’ 會影響默認的鏈接腳本,
你可以通過在命令列使用 ’-T ’ 命令使用自己的腳本,如果使用此命令,你的鏈接腳本將會替代默認鏈接腳本,
也可以通過將腳本作為聯結器輸入檔案隱式的使用鏈接腳本,參考Implicit Linker Scripts,
- Basic Script Concepts: 聯結器腳本的基本概念
- Script Format: 聯結器腳本的格式
- Simple Example: 簡單的聯結器腳本例子
- Simple Commands: 簡單的聯結器腳本命令
- Assignments: 為符號指定數值
- SECTIONS: 段命令
- MEMORY: 記憶體命令
- PHDRS: PHDRS命令
- VERSION: 版本命令
- Expressions: 鏈接腳本的運算式
- Implicit Linker Scripts: 隱式鏈接腳本
3.1 Basic Linker Script Concepts
為了描述鏈接腳本語言,我們需要定義一些基本概念和詞匯,
聯結器將輸入檔案(一個或多個)合并為一個輸出檔案,輸出檔案和每個輸入檔案都采用一種特殊的資料格式,稱為目標檔案格式,每個檔案稱為目標檔案,輸出檔案通常稱為可執行檔案,但出于我們的目的,我們也將其稱為目標檔案,每個目標檔案都有一個段(section)串列,有時把輸入檔案的段稱作輸入段,類似的,輸出檔案的段稱作輸出段,
目標檔案中的每個段都有名稱和大小,大多數段還具有關聯的資料塊,稱為段內容,一個段可能被標記為可加載(loadable),這意味著在運行輸出檔案時,段內容需要先加載到記憶體中,一個沒有內容的段是可分配的,這意味著應該在記憶體中預留一個區域,但是這里不需要加載任何東西(在某些情況下,該記憶體必須清零),既不可裝載也不可分配的部分通常包含某種除錯資訊,
每個可加載或可分配的輸出段都有兩個地址,第一個是 VMA 或稱為 虛擬記憶體地址 ,這是運行輸出檔案時該段將具有的地址,第二個是 LMA ,即 加載記憶體地址 ,這是段將會被加載的地址,在大多數情況下,這兩個地址是相同的,當然它們也可能不同,一個示例是將資料段加載到ROM中,然后在程式啟動時將其復制到RAM中(此技術通常用于初始化基于ROM的系統中的全域變數),在這種情況下,ROM地址將是LMA,而RAM地址將是VMA,
您可以將 objdump程式與 ’ -h '選項一起使用,以查看目標檔案中的各個部分,
每個目標檔案還具有一個符號串列,稱為符號表,符號可以是定義的也可以是未定義的,每個符號都有一個名稱,每個定義的符號都有一個地址,以及其他資訊,如果將C或C ++程式編譯到目標檔案中,則將會將所有定義過的函式和全域變數以及靜態變數作為已定義符號,輸入檔案中參考的每個未定義函式或全域變數都將成為未定義符號,
您可以使用 nm 程式或帶有 ‘-t’ 選項的 objdump 程式在目標檔案中查看符號,
3.2 Linker Script Format
鏈接腳本是文本檔案,
一個聯結器腳本是一系列的命令,每個命令都是一個關鍵字,可能后面還跟有一個引數,或者一個符號的賦值,使用分號分割命令,空格通常被忽略,
類似于檔案名或者格式名的字串可以直接輸入,如果檔案名含有一個字符例如逗號(逗號被用來分割檔案名),你可以將檔案名放在雙引號內部, 但是禁止在檔案名內使用雙引號字符 ,
你可以像C語言一樣在鏈接腳本內包含注釋,由’/’和’/’劃分,和C一樣,注釋在句法上被當作空格,
3.3 Simple Linker Script Example
大多數的鏈接腳本非常簡單,
最簡單的鏈接腳本只有一個命令:’SECTIONS ’ , 您可以使用 ’SECTIONS ’ 命令來描述輸出檔案的記憶體布局,
’SECTIONS ’ 命令功能非常強大, 在這里,我們將描述它的一個簡單用法, 假設您的程式僅包含代碼,初始化資料和未初始化資料, 它們分別位于“ .text ”,“.data ”和“ .bss ”段中, 我們進一步假設這些是唯一將會出現在輸入檔案中的段,
在此示例中,假設代碼應在地址 0x10000 處加載,資料應從地址 0x8000000 開始,下面的鏈接腳本將會執行如下操作:
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
您將 ’SECTIONS ’ 命令作為關鍵字 ’SECTIONS ’ 撰寫,然后在花括號中包含一系列符號的賦值和輸出段的描述,
上例中 ’SECTIONS ’ 命令中的第一行設定特殊符號 “. ” 的值,即位置計數器,如果未通過其他方式指定輸出段的地址(稍后將介紹其他方式),地址就會被設定為位置計數器的當前值,然后將位置計數器增加輸出段的大小,在‘SECTIONS ’命令的開頭,位置計數器的值為 ‘ 0 ’ ,
第二行定義了一個輸出段“ .text ”, 冒號是必需的語法 ,現在可以忽略它,在輸出段名稱后面的花括號中,列出應放置在此輸出段中的輸入段的名稱, “ ” 是與任何檔案名匹配的通配符,運算式 ‘ *(.text) ’ 表示所有輸入檔案中的所有 ‘.text*’ 輸入段,
由于在定義輸出段 ‘.text’ 時位置計數器為‘0x10000 ’,因此鏈接程式會將輸出檔案中 ‘.text’ 段的地址設定為‘0x10000 ’,
剩下的行定義了定義輸出檔案中的‘.data ’ 和‘.bss ’ 段,聯結器會將‘.data ’ 輸出段放置在地址’0x8000000 ’處,在聯結器放置‘.data ’ 段后,位置計數器為’0x8000000 ’加上‘.data ’ 段的大小,因此‘.bss ’ 輸出段在記憶體中將會緊緊挨在‘.data ’段后面,
聯結器將通過增加位置計數器(如有必要)來確保每個輸出部分具有所需的對齊方式,在此示例中, ‘.text’ 和‘.data ’ 段的指定地址可以滿足任何對齊方式約束,但聯結器可能必須在‘.data ’ 和‘.bss ’ 段之間創建一個小的間隙,
如上,這就是一個簡單完整的鏈接腳本,
3.4 Simple Linker Script Commands
在本節中,我們將介紹一些簡單的鏈接腳本命令,
- Entry Point : 設定入口點
- File Commands : 處理檔案的命令
- Format Commands : 處理目標檔案格式的命令
- REGION_ALIAS : 為記憶體區域分配別名
- Miscellaneous Commands : 其它鏈接腳本命令
3.4.1 Setting the Entry Point
在程式中執行的第一條指令稱為入口點, 您可以使用 ENTRY 聯結器腳本命令來設定入口點, 引數是符號名稱:
ENTRY(symbol)
有幾種設定入口點的方法, 聯結器將通過依次嘗試以下每種方法來設定入口點,并在其中一種成功后停止:
- ‘-e ’輸入命令列選項;
- 鏈接描腳本中的 ENTRY(symbol) 命令;
- 目標專用符號值(如果已定義); 對于許多目標來說是 start 符號,但是例如基于PE和BeOS的系統檢查可能的輸入符號串列,并與找到的第一個符號匹配,
- ‘.text ’ 部段的第一個位元組的地址(如果存在);
- 地址0,
3.4.2 Commands Dealing with Files
以下是聯結器腳本處理檔案的幾個常用命令:
(1)INCLUDE filename
在命令處包含鏈接腳本檔案 filename ,將在當前目錄以及 -L 選項指定的任何目錄中搜索檔案,INCLUDE 呼叫嵌套最多10個級別,
可以直接把 INCLUDE 放到頂層、 MEMORY 或者 SECTIONS 命令中,或者在輸出段的描述中,
(2)INPUT(file, file, …) / INPUT(file file …)
INPUT 命令指示鏈接程式在鏈接中包含指定的檔案,就好像它們是在命令列上命名的一樣,
例如,如果您始終希望在每次執行鏈接時都包含 subr.o,但又不想將其放在每個鏈接命令列中,則可以在鏈接腳本中放置 ‘INPUT (subr.o) ’,
實際上,您可以在鏈接描述檔案中列出所有輸入檔案,然后僅用‘-T ’選項呼叫鏈接腳本,
如果配置了sysroot 前綴,且檔案名以‘/ ’符開頭,并且正在處理的腳本位于sysroot 前綴內,則將在sysroot 前綴中查找檔案名,也可以通過指定 = 作為檔案名路徑中的第一個字符,或在檔案名路徑前加上 $ SYSROOT 來強制使用sysroot 前綴,另請參閱命令列選項中對‘-L ’ 的描述(Command-line Options),
如果未使用 sysroot 前綴,則聯結器將嘗試打開包含聯結器腳本的目錄中的檔案,如果沒有找到,聯結器將搜索當前目錄,如果仍未找到,聯結器將搜索庫的搜索路徑,
如果您使用 ‘INPUT (-lfile) ’ ,則 ld 會將名稱轉換為 libfile.a,就像命令列引數‘-l ’一樣,
當您在隱式鏈接腳本中使用 INPUT 命令時,檔案在鏈接腳本檔案被包含的時刻才會被加入,這可能會影響庫的搜索,
(3)GROUP(file, file, …) / GROUP(file file …)
GROUP 命令類似于 INPUT,不同之處在于,所有file指出的名字都應該為庫,并且所有庫將會被重復搜索直到沒有新的未定義參考被創建, 請參閱命令列選項中 ‘-(’ 的說明(Command-line Options),
(4)AS_NEEDED(file, file, …) / AS_NEEDED(file file …)
此構造只能出現在 INPUT 或 GROUP 命令以及其他檔案名中,命令中的檔案將會以類似于直接出現在 INPUT 或 GROUP 命令中的檔案一樣處理,除了ELF共享庫,ELF共享庫僅在真正需要使用時才被添加,這個構造實質上為其中列出的所有檔案啟用了 -as-needed 選項,為了恢復以前編譯環境,之后需設定 --no-as-needed,
(5)OUTPUT(filename)
OUTPUT 命令為輸出檔案命名, 在鏈接腳本中使用 OUTPUT(filename)與在命令列中使用 ‘-o filename’ 一樣(請參閱Command-line Options), 如果兩者都使用,則命令列選項優先,
您可以使用 OUTPUT 命令為輸出檔案定義默認名稱,以此替代默認名稱a.out,
(6)SEARCH_DIR(path)
SEARCH_DIR 命令添加一個 ld 搜索庫的路徑,使用 SEARCH_DIR(path) 與在命令列上使用 ’ -L path ’ 完全一樣(參見Command-line Options),如果同時使用了這兩條路徑,那么聯結器將會搜索所有路徑,首先搜索使用命令列選項指定的路徑,
(7)STARTUP(filename)
STARTUP 命令與 INPUT 命令類似,除了filename將成為要鏈接的第一個輸入檔案,就像它是在命令列中首先指定的一樣,在一些把第一個檔案當作入口點的系統上這個命令非常有效,
3.4.3 Commands Dealing with Object File Formats
有兩個聯結器腳本命令可以用來處理物件檔案格式:
OUTPUT_FORMAT(bfdname)
OUTPUT_FORMAT(default, big, little)
OUTPUT_FORMAT 命令使用BFD格式的命名方式(請參見BFD),使用 OUTPUT_FORMAT(bfdname) 與在命令列上使用 ‘–oformat bfdname ’ 完全相同(請參見Command-line Options),如果兩者都使用,則命令列選項優先,
您可以將OUTPUT_格式與三個引數一起使用,以根據 ’ -EB ’ 和 ‘-EL’ 命令列選項使用不同的格式,這允許聯結器腳本根據所需的endianness設定輸出格式,
如果未使用 ’ -EB ’ 和 ‘-EL’ ',那么輸出格式將會使用第一個引數作為默認值,如果使用 ’ -EB ',輸出格式將是第二個引數 big,如果使用 ‘-EL’ ',輸出格式將是第三個引數,little,
例如,MIPS ELF目標的默認聯結器腳本使用以下命令:
OUTPUT_FORMAT(elf32-bigmips, elf32-bigmips, elf32-littlemips)
這說明輸出檔案的默認格式是 ‘elf32-bigmips’,但如果用戶使用’-EL’ '命令列選項,則將以‘elf32-littlemips’格式創建輸出檔案,
TARGET(bfdname)
TARGET 命令設定讀取輸入檔案時的BFD格式,這將影響后面的 INPUT 和 GROUP 命令,此命令類似使用命令列指令 ‘-b bfdname’ (參見Command-line Options),如果使用了TARGET命令,但OUTPUT_FORMAT命令沒使用,則最后的TARGET命令還被用來設定輸出檔案的格式(參見BFD),
3.4.4 Assign alias names to memory regions
可以為MEMORY命令創建的記憶體區域提供別名, 每個名稱最多對應一個存盤區域
REGION_ALIAS(alias, region)
REGION_ALIAS 函式為 記憶體區域創建別名 ,這允許靈活地將輸出部分映射到記憶體指定區域,下面有一個例子,
假設我們有一個用于嵌入式系統的應用程式,它帶有各種記憶體存盤設備,它們都有一個通用的,易失性記憶體RAM,允許代碼執行或資料存盤,一些可能有一個只讀的、非易失性記憶體ROM,允許代碼執行和只讀資料訪問,最后一個是只讀、非易失性存盤器ROM2,允許對只讀資料段讀取,不允許代碼執行,現在有四個輸出段:
- .text :程式代碼
- .rodata :只讀資料
- .data :可讀寫且需要初始化資料
- .bss :可讀寫的置零初始化資料
目標是提供一個聯結器腳本檔案,該檔案包含定義系統無關的輸出段的部分,和將輸出段映射到系統上可用記憶體區域的系統相關部分,我們的嵌入式系統有三種不同的記憶體設定A、B和C:
Section Variant A Variant B Variant C
.text RAM ROM ROM
.rodata RAM ROM ROM2
.data RAM RAM/ROM RAM/ROM2
.bss RAM RAM RAM
RAM/ROM或RAM/ROM2表示將此段分別加載到區域ROM或ROM2中,請注意,三個設定的.data段的起始地址都位于.rodata段的末尾,
接下來是處理輸出段的基本鏈接腳本, 它包含描述記憶體布局的系統相關鏈接 cmds.memory 檔案:
INCLUDE linkcmds.memory
SECTIONS
{
.text :
{
(.text)
} > REGION_TEXT
.rodata :
{
(.rodata)
rodata_end = .;
} > REGION_RODATA
.data : AT (rodata_end)
{
data_start = .;
(.data)
} > REGION_DATA
data_size = SIZEOF(.data);
data_load_start = LOADADDR(.data);
.bss :
{
(.bss)
} > REGION_BSS
}
現在我們需要三個不同的 linkcmds.memory 來定義記憶體區域以及別名,下面是A,B,C不同的 linkcmds.memory :
A :所有都存入RAM
MEMORY { RAM : ORIGIN = 0, LENGTH = 4M }
REGION_ALIAS("REGION_TEXT", RAM);
REGION_ALIAS("REGION_RODATA", RAM);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
B :代碼和只讀資料存入ROM,可讀寫資料放入RAM,一個已初始化了的資料的鏡像被加載到ROM,并在系統啟動的時候讀入RAM
MEMORY { ROM : ORIGIN = 0, LENGTH = 3M RAM : ORIGIN = 0x10000000, LENGTH = 1M }
REGION_ALIAS("REGION_TEXT", ROM);
REGION_ALIAS("REGION_RODATA", ROM);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
C :代碼放入ROM,只讀資料放入ROM2,可讀寫資料放入RAM,一個已初始化了的資料的鏡像被加載到ROM2,并在系統啟動的時候讀入RAM
MEMORY { ROM : ORIGIN = 0, LENGTH = 2M ROM2 : ORIGIN = 0x10000000, LENGTH = 1M RAM : ORIGIN = 0x20000000, LENGTH = 1M }
REGION_ALIAS("REGION_TEXT", ROM);
REGION_ALIAS("REGION_RODATA", ROM2);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
如有必要,可以撰寫通用的系統初始化程式以將.data段從ROM或ROM2復制到RAM:
#include <string.h>extern char data_start [];
extern char data_size [];
extern char data_load_start [];
void copy_data(void)
{
if (data_start != data_load_start)
{
memcpy(data_start, data_load_start, (size_t) data_size);
}
}
3.4.5 Other Linker Script Commands
還有一些其他聯結器腳本命令:
- ASSERT(exp, message)
注意此斷言會在最終鏈接階段之前進行檢查,這表示,在段內使用PROVIDE的定義如果用戶沒有為其設定值,此運算式將無法通過檢測,唯一的例外是PROVIDE的符號剛剛參考了’.’,因此,一個如下斷言:
確保 exp 不為零, 如果為零,則退出鏈接并顯示錯誤代碼,并列印一些相關的資訊,
請注意,在鏈接的最后階段發生之前會檢查斷言, 這意味著,如果用戶沒有為這些符號設定值,則涉及段定義中提供的符號的運算式將失敗, 該規則的唯一例外是僅參考點的提供的符號, 因此,這樣的斷言:
.stack :
{
PROVIDE (__stack = .);
PROVIDE (__stack_size = 0x100);
ASSERT ((__stack > (_end + __stack_size)), "Error: No room left for the stack");
}
如果沒有在其他地方定義stack_size,則會失敗,在段外定義的符號會在此前被求值,可以在ASSERTions 使用它們,因此:
PROVIDE (__stack_size = 0x100);
.stack :
{
PROVIDE (__stack = .);
ASSERT ((__stack > (_end + __stack_size)), "Error: No room left for the stack");
}
將會作業,
-
EXTERN(symbol symbol …)
強制將符號作為未定義符號輸入到輸出檔案中, 這樣做可能會例如觸發標準庫中其他模塊的鏈接, 您可以為每個 EXTERN 列出幾個符號,并且可以多次使用 EXTERN, 此命令與 ‘-u ’ 命令列選項具有相同的作用, -
FORCE_COMMON_ALLOCATION
這個命令與’ -d ’ 命令列選項具有相同的效果:即便是使用了’-r’ 的重定位輸出檔案,也讓 ld 為普通符號分配空間, -
INHIBIT_COMMON_ALLOCATION
這個命令與命令列選項 ‘–no-define-common’ 具有相同的效果 : 讓 ld 不為普通符號分配空間,即便是一個非可重定位輸出檔案, -
FORCE_GROUP_ALLOCATION
這個命令與命令列選項 ‘–force-group-allocation’ 具有相同的效果 : 使ld place 段組成員像普通的輸入段一樣,并且即使指定了可重定位的輸出檔案(’ -r ')也可以洗掉段組, -
INSERT [ AFTER | BEFORE ] output_section
此命令通常在‘-T ’ 指定的腳本中使用,用來增強默認的SECTIONS,例如,重復占位程式段,它將把所有此前的鏈接腳本的宣告插入output_section的后面(或者前面),并且使 ’-T ’不要覆寫默認鏈接腳本,實際插入點類似于孤兒段,參見Location Counter,插入發生在聯結器把輸入段映射到輸出段后,在插入前,因為’-T ’的腳本在默認腳本之前被決議,在’-T’腳本中的宣告會先于默認內部腳本的宣告而執行,特別是,將對默認腳本中的’-T ’輸出段進行輸入段分配,下例為’-T ’腳本使用INSERT可能的情況:
SECTIONS
{
OVERLAY :
{
.ov1 { ov1*(.text) }
.ov2 { ov2*(.text) }
}
}
INSERT AFTER .text;
- NOCROSSREFS(section section …)
此命令可能被用來告訴 ld,如果參考了section的引數就報錯,
在特定的程式型別中,比如使用覆寫技術的嵌入式系統,當一個段被加載到記憶體中,另一個段不會被加載,任何兩個段之間直接的參考都會帶來錯誤,例如,如果一個段中的代碼呼叫另一個段中的函式,將會產生錯誤,
NOCROSSREFS 命令列出了一系列輸出段的名字,如果 ld 檢測到任何段間交叉參考,將會報告錯誤并回傳非零退出碼,注意NOCROSSREFS使用輸出段名稱,而不是輸入段名稱,
- NOCROSSREFS_TO(tosection fromsection …)
此命令可能被用來告訴 ld,從其他段串列中對某個段的任何參考就會引發錯誤,
當需要確保兩個或多個輸出段完全獨立,但是在某些情況下需要單向依賴時,NOCROSSREFS 命令很有用, 例如,在多核應用程式中,可能存在可以從每個核呼叫的共享代碼,但是出于安全考慮,絕不能回呼,
NOCROSSREFS_TO 命令攜帶(給出)輸出段名稱的串列, 其他任何部分都不能參考第一部分, 如果 ld 從其他任何部分中檢測到對第一部分的任何參考,它將報告錯誤并回傳非零退出狀態, 請注意,NOCROSSREFS_TO 命令使用輸出段名稱,而不是輸入段名稱,
-
OUTPUT_ARCH(bfdarch)
指定一個特定的輸出機器架構,該引數是BFD庫使用的名稱之一(請參閱BFD),通過使用帶有 ’ -f ’ 選項的objdump程式,您可以看到目標檔案的體系結構, -
LD_FEATURE(string)
此命令可用于修改 ld 行為,如果字串是“SANE_EXPR”,那么腳本中的絕對符號和數字將被在任何地方當作數字對待,請參考 Expression Section,
3.5 Assigning Values to Symbols
可以給聯結器腳本中的符號賦值,這會定義符號并將其放入具有全域作用域的符號表中,
- Simple Assignments 簡單賦值
- HIDDEN 隱藏
- PROVIDE PROVIDE
- PROVIDE_HIDDEN PROVIDE_HIDDEN
- Source Code Reference 如何在源代碼中使用一個鏈接腳本定義的符號
3.5.1 Simple Assignments
您可以使用任何C賦值運算子來賦值符號:
symbol = expression ;
symbol += expression ;
symbol -= expression ;
symbol *= expression ;
symbol /= expression ;
symbol <<= expression ;
symbol >>= expression ;
symbol &= expression ;
symbol |= expression ;
第一種情況將運算式的值賦值給符號, 在其他情況下,必須先定義符號,并相應地調整符號的值,
特殊符號名稱 ‘. ’ 表示位置計數器, 您只能在 SECTIONS 命令中使用它, 請參閱 Location Counter,
運算式后面的分號不能省略,
運算式定義如下; 請參閱Expressions,
你在寫運算式賦值陳述句時,可以把它們作為單獨的部分,也可以作為 ’SECTIONS’ 命令中的一個陳述句,或者作為 ’SECTIONS’ 命令中輸出段描述的一個部分,
符號的有效作用區域由運算式所在的段決定,Expression Section,
下面是是三個不同位置為符號賦值的示例:
floating_point = 0;
SECTIONS
{
.text :
{
*(.text)
_etext = .;
}
_bdata = (. + 3) & ~ 3;
.data : { *(.data) }
}
在本例中,符號 ‘floating_point’ 將被定義為零,符號 ’ _etext ’ 將被設定為緊隨 ’.text’ 最后一個輸入段后面的地址,符號’ _bdata '將被定義為在 ’.text’ 輸出段后面的一個4位元組向上對齊的地址,
3.5.2 HIDDEN
語法HIDDEN(symbol = expression)為ELF目標的埠定義一個符號,符號將被隱藏并且不會被匯出,
下面是Simple Assignments的例子,使用HIDDEN重寫:
HIDDEN(floating_point = 0);
SECTIONS
{
.text :
{
*(.text)
HIDDEN(_etext = .);
}
HIDDEN(_bdata = (. + 3) & ~ 3);
.data : { *(.data) }
}
在本例中,這三個符號在此模塊之外都不可見
3.5.3 PROVIDE
在某些情況下,僅當一個符號被參考了卻沒有定義在任何鏈接目標中,才需要為鏈接腳本定義一個符號,例如,傳統聯結器定義了符號‘etext’,然而,ANSI C要求用戶能夠使用’ etext '作為函式名而不會引發錯誤,PROVIDE關鍵字可以用來定義一個符號,比如‘etext’ ,只有當它被參考但沒有被定義時才使用,語法是 PROVIDE(symbol = expression),
下面是一個使用提供定義‘etext’的例子:
SECTIONS
{
.text :
{
*(.text)
_etext = .;
PROVIDE(etext = .);
}
}
在本例中,如果程式定義了’ _etext ‘(帶有前導下劃線),聯結器將給出重復定義錯誤,另一方面,如果程式定義了’ etext ‘(沒有前導下劃線),聯結器會默認使用程式中的定義,如果程式參考了’ etext '但沒有定義它,聯結器將使用聯結器腳本中的定義,
注意 -PROVIDE指令將考慮定義一個普通符號,即使這樣的符號可以與PROVIDE將創建的符號組合在一起,當考慮建構式和解構式串列符號時,這一點尤其重要,因為它們通常被定義為普通符號,
3.5.4 PROVIDE_HIDDEN
與 PROVIDE 類似,對于ELF目標的埠,符號將被隱藏且不會被輸出,
3.5.5 Source Code Reference
從源代碼獲取聯結器腳本定義的變數并不直觀, 特別是,特別是鏈接腳本中的符號與高級語言定義的變數宣告不同的時候,將使用一個沒有值的變數替代它,
在進一步討論之前,必須注意,當編譯器將源代碼中的名稱存盤在符號表中時,它們通常會將它們轉換為不同的名稱, 例如,Fortran編譯器通常在前面或后面加上下劃線,而C ++則執行大量的 ‘name mangling ’, 因此,在源代碼中使用的變數名稱與在鏈接腳本中定義的相同變數的名稱之間可能會有差異, 例如,在C語言中,鏈接腳本變數可能稱為:
extern int foo;
但是在聯結器腳本中,它可能被定義為:
_foo = 1000;
然而,在其余的例子中,假設沒有發生名稱轉換,
當一個符號用高級語言,比如C語言,宣告了一個符號,會發生兩件事,首先,編譯器在程式記憶體中保留足夠的空間來保存符號的值,第二種方法是編譯器在程式的符號表中創建一個條目,用來保存符號的地址,例如下面的C宣告:
int foo = 1000;
在符號表中創建一個名為’ foo '的條目,這個入口保存了一個‘int’ 大小的記憶體塊的地址,數字1000最初存盤在這里,
當程式參考一個符號時,編譯器生成的代碼首先訪問符號表以查找該符號的記憶體塊地址,然后代碼從該記憶體塊讀取值,所以:
foo = 1;
在符號表中查找符號’ foo ',獲取與該符號關聯的地址,然后將值1寫入該地址,而:
int * a = & foo;
在符號表中查找符號’ foo ',獲取它的地址,然后將這個地址復制到與變數 ’ a ’ 相關聯的記憶體塊中,
相比之下,聯結器腳本符號宣告在符號表中創建一個條目,但不給它們分配任何記憶體,因此,它們是一個沒有值的地址,例如聯結器腳本定義:
foo = 1000;
在符號表中創建一個名為’ foo '的條目,該條目保存記憶體位置1000的地址,但地址1000上沒有存盤任何特殊內容,這意味著您無法訪問鏈接程式腳本定義的符號的值-它沒有值,您所能做的就是訪問聯結器腳本定義符號的地址,
因此,當您在源代碼中使用聯結器腳本定義的符號時,您應該始侄訓取該符號的地址,并且永遠不要嘗試使用它的值,例如,假設你想把記憶體的 .ROM 拷貝到 .FLASH 中,聯結器腳本包含了這些宣告:
start_of_ROM = .ROM;
end_of_ROM = .ROM + sizeof (.ROM);
start_of_FLASH = .FLASH;
那么執行復制的C源代碼為:
extern char start_of_ROM, end_of_ROM, start_of_FLASH;
memcpy (& start_of_FLASH, & start_of_ROM, & end_of_ROM - & start_of_ROM);
注意 ‘&’ 運算子的使用,上面是正確的代碼,一種替換是,把符號被當作一個陣列變數的名稱,因此代碼變成了:
extern char start_of_ROM[], end_of_ROM[], start_of_FLASH[];
memcpy (start_of_FLASH, start_of_ROM, end_of_ROM - start_of_ROM);
注意此時不需要運算子 ’&’ 了,
3.6 SECTIONS Command
SECTIONS 命令告訴聯結器如何將輸入段映射到輸出段,以及如何將輸出段放在記憶體中,
SECTIONS 命令的格式為:
SECTIONS
{
sections-command
sections-command
…
}
每個 sections-command 命令可能是下面之一:
- ENTRY 命令(請參閱Entry command)
- 符號賦值(請參閱Assignments)
- 輸出段的描述
- overlay描述
為了方便在這些命令中使用位置計數器,在SECTIONS 命令中允許使用 ENTRY 命令和符號賦值, 這也可以使鏈接描述檔案更容易理解,因為你可以在更有意義的地方使用這些命令來控制輸出檔案的布局,
輸出段描述和覆寫在下面將會分析,
如果在鏈接腳本中未使用 SECTIONS 命令,則聯結器將會照輸入文本的順序,將每個輸入部段放置到名稱相同的輸出段中,例如,如果所有輸入段出現在第一個檔案中,輸出檔案的段的順序將會與第一個輸入檔案保持一致,第一個段被放在地址0,
- Output Section Description 輸出段描述
- Output Section Name 輸出段名稱
- Output Section Address 輸出段地址
- Input Section 輸入段描述
- Output Section Data 輸出段資料
- Output Section Keywords 輸出段關鍵字
- Output Section Discarding 輸出段忽略的內容
- Output Section Attributes 輸出段屬性
- Overlay Description Overlay description
3.6.1 Output Section Description
輸出段的完整描述如下所示:
section [address] [(type)] :
[AT(lma)]
[ALIGN(section_align) | ALIGN_WITH_INPUT]
[SUBALIGN(subsection_align)]
[constraint]
{
output-section-command
output-section-command
…
} [>region] [AT>lma_region] [:phdr :phdr …] [=fillexp] [,]
大部分的可選段屬性在多數輸出段不需要使用,
SECTION 邊上的空格是必須的,這樣段名就沒有歧義了,冒號和花括號也是必需的,如果使用了fillexp,并且下一個section -命令看起來像是運算式的延續,則可能需要在末尾使用逗號,換行符和其他空格是可選的,
當 fillexp 使用且接下來的 sections-command 看起來像是運算式的延續的時候,可能需要在后面加上逗號,
每個 output-section-command 可以是下列命令之一:
符號賦的值(參見Assignments)
輸入段描述(參見Input Section)
直接包參考的資料值(參見Output Section Data)
特殊的輸出段關鍵字(參見Output Section Keywords))
3.6.2 Output Section Name
輸出段的名字是 section ,section必須滿足輸出格式的規定,在只支持有限段數目的格式中,例如 a.out ,名稱必須是該格式所支持的名稱之一(例如a.out ,只允許’.text’,’.data’,’.bss’),如果輸出格式支持任意數量的段,但是只有數字而不是名稱(Oasys 就是這種情況),則名稱應該以帶引號的數字字串的形式提供,一個段的名字可以由任意字符序列組成,但一個含有許多特殊字符(如逗號)的名稱必須用引號括起來,
名稱為 ‘/DISCARD/’ 的輸出段 ,有特殊含義; 參考Output Section Discarding.
3.6.3 Output Section Address
address 是輸出段VMA(虛擬記憶體地址)的運算式,此地址是可選引數,但如果提供了該地址,則輸出地址就會被精確的設定為指定的值,
如果沒有指定輸出地址,那么則依照下面的幾種方式嘗試選擇一個地址,此地址將被調整以適應輸出段的對齊要求,輸出段的對齊要求是所有輸入節中含有的對齊要求中最嚴格的一個,
輸出段地址探索如下:
-
如果為該段設定了一個輸出記憶體區域,那么它將被添加到該區域中,其地址將是該區域中的下一個空閑地址,
-
如果使用 MEMORY 命令創建記憶體區域串列,那么將選擇具有與該段兼容屬性的第一個區域來包含該區域,該部分的輸出地址將是該區域中的下一個空閑地址;MEMORY ,
-
如果沒有指定記憶體區域,或者沒有與段匹配的記憶體區域,則輸出地址將基于位置計數器的當前值,
例如:
.text . : { *(.text) }
和
.text : { *(.text) }
有著細微的不同, 第一個將‘.text’ 輸出段的地址設定為位置計數器的當前值, 第二個引數會將其設定為位置計數器的當前值,但是該值與所有‘.text’ 輸入段中最嚴格的對齊方式對齊,
address 可以是任意運算式; 例如,如果要在0x10位元組(16位元組)邊界上對齊段,以使節地址的最低四位為零,則可以執行以下操作:
.text ALIGN(0x10) : { *(.text) }
之所以這樣做,是因為 ALIGN 回傳的當前位置計數器向上對齊到指定的值,
為段指定地址將會改變位置計數器的值,前提是該段是非空的(空的段被忽略),
3.6.4 Input Section Description
最常見的輸出段命令(output-section-command)是輸入段描述,
輸入段描述是鏈接腳本最基本的操作, 您可以使用輸出段來告訴聯結器如何在記憶體中布置程式, 您可以使用輸入段描述來告訴聯結器如何將輸入檔案映射到您的記憶體布局中,
- Input Section Basics 基本的輸入段
- Input Section Wildcards 輸入段通配符模板
- Input Section Common 普通符號的輸入段
- Input Section Keep 輸入段與垃圾回收
- Input Section Example 輸入段例子
3.6.4.1 Input Section Basics
輸入段說明由一個檔案名和一個括號中的段名串列(可選)組成,
檔案名和段名可以是通配符,我們將在下面進一步描述(請參閱Input Section Wildcards),
最常見的輸入段描述是在輸出段中包括所有具有特定名稱的輸入段, 例如,把所有輸入段放入’.text’段,可以這么寫:
*(.text)
這里的 ‘*’ 是一個通配符,它可以用來匹配任何檔案名,要排除與檔案名通配符匹配的檔案串列,可以使用 EXCLUDE_FILE 來匹配除 EXCLUDE_FILE串列中指定的檔案以外的所有檔案,例如:
EXCLUDE_FILE (*crtend.o *otherfile.o) *(.ctors)
將導致包括除 crtend.o 和 otherfile.o 以外的所有檔案的所有 .ctors 段,EXCLUDE_FILE 也可以放在段的串列中,例如:
*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors)
其結果與前面的示例相同,如果段串列包含多個段,則支持 EXCLUDE_FILE 的兩個語法非常有用,如下所述,
有兩種方法可以包含多個段:
*(.text .rdata)
*(.text) *(.rdata)
兩種方法的區別是輸入段的 ’.text’ 和 ’.rata’ 段出現在輸出段中的順序,第一個例子里,他們將被混合在一起,按照聯結器找到它們的順序存放,另一個例子中,所有 ’.text’ 輸入段將會先出現,后面是 ’.rdata’ 輸入段,
將EXCLUDE_FILE與多個段一起使用時,這個排除命令僅僅對緊隨其后的段有效,例如:
*(EXCLUDE_FILE (*somefile.o) .text .rdata)
將導致包含除 somefile.o 以外的所有檔案的所有‘.text’段,而包括somefile.o在內的所有檔案的所有‘.rdata’ 段都將被包含,要從somefile.o中排除‘.rdata’ 段部分,可以將示例修改為:
*(EXCLUDE_FILE (*somefile.o) .text EXCLUDE_FILE (*somefile.o) .rdata)
或者,將EXCLUDE_FILE放在段串列之外(在選擇輸入檔案之前),將導致排除操作對所有段有效,因此,前一示例可以重寫為:
EXCLUDE_FILE (*somefile.o) *(.text .rdata)
你可以指定一個檔案名來包含特定檔案的段,如果一個或者多個你的檔案需要被放在記憶體中的特定位置,你可能需要這么做,例如:
data.o(.data)
如果想使用段標志來選擇輸入檔案的段,可以使用INPUT_SECTION_FLAGS,
下面是一個為ELF段使用段頭標志的簡單示例:
SECTIONS {
.text : { INPUT_SECTION_FLAGS (SHF_MERGE & SHF_STRINGS) *(.text) }
.text2 : { INPUT_SECTION_FLAGS (!SHF_WRITE) *(.text) }
}
在本例中,輸出段 ‘.text’ 將被由那些與 *(.text) 能匹配的段(名字)且段頭部標志設定了SHF_MERGE和SHF_STRINGS的段構成,輸出段 ‘.text2’ 由那些與 *(.text) 能匹配的段(名字)且段頭部標志未設定SHF_WRITE的段構成,
你也可以指出特別的關聯庫名稱的檔案,命令是[ 庫匹配模板:與檔案匹配的模式 ],冒號兩邊不能有空格,
- ‘archive:file’ 在庫中尋找能夠匹配的檔案
- ‘archive:’ 匹配整個庫
- ‘:file’ 匹配檔案但不匹配庫
- ‘archive’ 和 ‘file’ 中的一個或兩個都可以包含shell通配符,在基于DOS的檔案系統上,聯結器會假定一個單字跟著一個冒號是一個特殊的驅動符,因此 ‘c:myfile.o’ 是一個檔案的特殊使用,而不是關聯庫’c’的 ’myfile.o’ 檔案,‘archive:file’:可以使用在EXCLUDE_FILE串列中,但不能出現在其他鏈接腳本內部,例如,你不能使用 ‘archive:file’從 INPUT命令中取出一個庫相關的檔案,
如果你使用一個檔案名而不指出段串列,則所有的輸入檔案的段將被放入輸出段,通常不會這么做,但有些場合比較有用,例如:
data.o
當你使用一個檔案名且不是 ‘archive:file’特殊命令,并且不含任何通配符,聯結器將先查看你是否在命令列上或者在INPUT命令里指定了該檔案,如果沒有這么做,聯結器嘗試將檔案當作輸入檔案打開,就像檔案出現在了命令列一樣,注意與INPUT命令有區別,因為聯結器不會在庫檔案路徑搜索檔案,
3.6.4.2 Input Section Wildcard Patterns
在輸入段描述中,檔案名和段名都可以使用通配符模式,
在許多示例中看到的檔案名’ * '是一個簡單的檔案名通配符模式,
通配符模式類似于Unix shell使用的那些模式,
- ‘*’ 匹配任意數量字符
- ‘?’ 匹配任意單字
- ‘[chars]’ 匹配任何字符的單個實體;‘-’ 字符可被用來指出一個字符的范圍,例如 ‘[a-z]’ 可以用來匹配所有小寫字母
- ‘\’ 參考后面的字符
當檔案名與通配符匹配時,通配符將不匹配 ‘/’ 字符(在Unix上用于分隔目錄名),由單個 ‘*’ 字符組成的模式是除外;它將始終匹配任何檔案名,無論它是否包含 ‘/’ ,在段名稱中,通配符將匹配 ‘/’ 字符,
檔案名通配符模式只匹配在命令列或輸入命令中顯式指定的檔案,聯結器不會搜索目錄以擴展通配符,
如果一個檔案名匹配多個通配符,或者一個檔案名被顯示指定了,且又被通配符匹配了,則聯結器將使用聯結器腳本中的第一個匹配項,例如,例如,下面的輸入段描述可能有錯誤,因為 data.o 的規則不會被應用:
.data : { *(.data) }
.data1 : { data.o(.data) }
通常情況下,聯結器將按照鏈接程序中出現通配符的順序放置檔案和段,您可以通過使用SORT_BY_NAME 關鍵字來更改此行為,該關鍵字出現在括號中的通配符模式之前(例如,SORT_BY_NAME(.text*)),當使用 SORT_BY_NAME 關鍵字時,聯結器將按名稱按升序對檔案或段進行排序,然后將它們放入輸出檔案中,
SORT_BY_ALIGNMENT 對齊方式類似于 SORT_BY_NAME. SORT_BY_ALIGNMENT 將在將段放入輸出檔案之前,按對齊方式的降序對段進行排序,大的對齊被放在小的對齊前面可以減少所需的填充量,
SORT_BY_INIT_PRIORITY 與 SORT_BY_NAME 相似,區別是 SORT_BY_INIT_PRIORITY把段按照GCC的嵌入在段名稱的 init_priority 數字屬性值升序排列后放入輸出檔案,.init_array.NNNNN 和 .fini_array.NNNNN, NNNNN 是init_priority , .ctors.NNNNN 和 .dtors.NNNNN, NNNNN 是65535減去 init_priority ,
SORT 是 SORT_BY_NAME 的別名,
當聯結器腳本中有嵌套的段排序命令時,段排序命令最多可以有1個嵌套級別,
(1)SORT_BY_NAME (SORT_BY_ALIGNMENT (wildcard section pattern)) ,它將首先按名稱對輸入部分進行排序,如果兩個部分同名,則按對齊方式排序,
(2)SORT_BY_ALIGNMENT (SORT_BY_NAME (wildcard section pattern)),它將首先按對齊方式對輸入段進行排序,如果兩個段具有相同的對齊方式,則按名稱排序,
(3)*SORT_BY_NAME (SORT_BY_NAME (wildcard section pattern))*與 SORT_BY_NAME (wildcard section pattern) 相同,
(4)SORT_BY_ALIGNMENT (SORT_BY_ALIGNMENT (wildcard section pattern)) 與 SORT_BY_ALIGNMENT (wildcard section pattern) 相同,
(5)除此之外,其它所有嵌套段排序命令都是無效的,
當同時使用命令列段排序選項和聯結器腳本段排序命令時,段排序命令總是優先于命令列選項,
如果聯結器腳本中的段排序命令不是嵌套的,那么命令列選項將使段排序命令被視為嵌套的排序命令,
(1)SORT_BY_NAME (wildcard section pattern ) 與 –sort-sections alignment 連用等價于SORT_BY_NAME (SORT_BY_ALIGNMENT (wildcard section pattern)) ,
(2)SORT_BY_ALIGNMENT (wildcard section pattern) 與 –sort-section name 連用等價于
SORT_BY_ALIGNMENT (SORT_BY_NAME (wildcard section pattern)),
如果聯結器腳本中的段排序命令是嵌套的,那么命令列選項將被忽略,
SORT_NONE 通過忽略命令列部段排序選項來禁用段排序,
如果您對輸入段的去向感到困惑, 可以使用 ’ -M ’ 聯結器選項來生成映射檔案 ,映射檔案精確地顯示了如何將輸入段映射到輸出段,
下面這個示例展示了通配符如何被用來分隔檔案,這個鏈接腳本指引聯結器把所有 ‘.text’ 段放在’ ‘.text’ 里,以及所有 ’.bss’ 段放到 ’.bss’ 中,聯結器將會把所有以大寫字母開頭的檔案的 ’.data’ 段放入 ’.DATA’ ,其他檔案的 ’.data’ 段放入 ’.data’ ,
SECTIONS {
.text : { *(.text) }
.DATA : { [A-Z]*(.data) }
.data : { *(.data) }
.bss : { *(.bss) }
}
3.6.4.3 Input Section for Common Symbols
普通符號需要一個特別的標記,因為很多目標檔案格式中沒有特定的普通符號輸入段,聯結器把普通符號當作位于一個名為 ’COMMON’ 的輸入段中,
像使用其它檔案名與段一樣,你也可以使用檔案名與 ’COMMON’ 段的組合,通過這種方法把一個特定檔案的普通符號放入一個段內,同時把其它輸入檔案的普通符號放入另一個段內,
大多數情況下,輸入檔案的普通符號會被放到輸出檔案的 ’.bss’ 段里面,例如:
.bss { *(.bss) *(COMMON) }
有些目標檔案格式含有多種普通符號的型別,例如,MIPS ELF目標檔案把標準普通符號和小型普通符號區分開來,在這種情況下,聯結器會為另一個型別的普通符號使用其它的特殊段名稱,在MIPS ELF中,聯結器為普通符號使用 ’COMMON’ 以及為小型普通符號使用 ’.scommon’ ,這樣就可以把不同型別的普通符號映射到記憶體中的不同位置,
有時在老的鏈接腳本中能看見 ’[COMMON]’ ,這個標記現在已廢棄,它等價于’*(COMMON)’ ,
3.6.4.4 Input Section and Garbage Collection
使用了鏈接時垃圾收集(‘–gc-sections’)的功能,在把段標記為不應被消除非常常用,此功能通過把一個輸入段的通配符入口使用 KEEP() 實作,類似于 KEEP((.init)) 或KEEP(SORT_BY_NAME()(.ctors)),
3.6.4.5 Input Section Example
下面是一個完整的鏈接腳本的例子,它告訴聯結器從 all.o 讀取所有段,把它們放到輸出段 ’outputa’ 的開頭位置,’outputa’ 的起始地址為 ’0x10000’ ,所有檔案 foo.o 中的 ’.input1’ 段緊跟其后,所有檔案 foo.o 中的 ’input2’ 段放入輸出檔案的 ’outputb’ 中,跟著是 foo1.o 中的 ’input1’ 段,所有其它的 ’.input1” 和 .input2’ 段被放入輸出段 ’outputc’ ,
SECTIONS {
outputa 0x10000 :
{
all.o
foo.o (.input1)
}
outputb :
{
foo.o (.input2)
foo1.o (.input1)
}
outputc :
{
*(.input1)
*(.input2)
}
}
如果輸出段的名稱與輸入段的名稱相同,并且可以表示為C識別符號,那么聯結器將自動看到 PROVIDE兩個符號:余下的*__start_SECNAME* 和 _stop_SECNAME,其中SECNAME是段的名稱,它們分別指示輸出段的開始地址和結束地址,注意:大多數段名不能表示為C識別符號,因為它們包含 ‘.’ 字符,
3.6.5 Output Section Data
你可以通過使用輸出段命令BYTE, SHORT, LONG, QUAD, 或者 SQUAD在輸出段顯式的包含幾個位元組的資料,每個關鍵字后面跟著一個括號包裹的運算式指出需要存盤的數值(參照Expressions),運算式的值被存盤在當前位置計數器值的地方,
BYTE, SHORT, LONG, QUAD命令分別存盤1,2,4,8位元組,在存盤位元組后,位置計數器會按照存盤的位元組數增加,
例如,下面將會存盤一個單位元組資料1,然后存盤一個符號為 ’addr’ 四位元組資料的值:
BYTE(1)
LONG(addr)
當使用64位主機或目標時,QUAD 和SQUAD是相同的;它們都存盤一個8位元組或64位的值,主機和目標都是32位時,運算式被當作32位計算,在這種情況下QUAD存盤一個32位的值,并使用0擴展到64位,SQUAD保存32位值并使用符號位擴展到64位,
如果輸出檔案的目標檔案格式顯式的指定 endiannes,在正常的情況下,值將按照大小端存盤,當物件檔案格式沒有顯式的指定 endianness,例如,S-records,值將被按照第一個輸入目標檔案的大小端存盤,
注意 - 這些命令僅在段描述內部作業,因此下面的例子會使聯結器產生錯誤:
SECTIONS { .text : { *(.text) } LONG(1) .data : { *(.data) } }
而下面這是可行的:
SECTIONS { .text : { *(.text) ; LONG(1) } .data : { *(.data) } }
您可以使用 FILL 命令設定當前段的填充模式,該命令后面跟著一個括號包裹的運算式,所有其它沒有被特別指定段的記憶體區域(例如因為對齊需要而留出來的縫隙)按照運算式的值填充,如果有必要可以重復填充,一個FILL陳述句僅會覆寫它本身在段定義中出現的位置后面的所有記憶體區域;通過使用不同的FILL宣告,你可以在一個輸出段中使用不同的填充模板,
這個例子顯示了如何使用 ’0x90’ 填充未定義記憶體區域:
FILL(0x90909090)
FILL命令類似 ’=fillexp’ 輸出段屬性,但其僅影響FILL命令后面的段,而不是整個段,如果同時使用,FILL命令為高優先級,參考 Output Section Fill獲取更多填充細節,
3.6.6 Output Section Keywords
這里有兩個關鍵字可以作為輸出段的命令:
CREATE_OBJECT_SYMBOLS
此命令告訴聯結器為每個輸入檔案創建一個符號,每個符號的名字為對應輸入檔案的名字,每個符號出現的位置位于包含CREATE_OBJECT_SYMBOLS命令的輸出段中,
這個命令常常是 a.out 目標檔案格式特有的, 它一般不為其它的目標檔案格式所使用,
CONSTRUCTORS
當鏈接時使用 a.out 目標檔案的格式,聯結器使用一個特殊構造集來支持C++ 全域建構式和解構式,在鏈接不支持任意段的檔案格式時,例如 ECOFF 和 XCOFF ,聯結器將會通過名字自動識別C++全域建構式和解構式,對于這些格式的目標檔案,CONSTRUCTORS命令告訴聯結器把建構式資訊放到出現 CONSTRUCTORS 命令的輸出段中,其它檔案格式中CONSTRUCTORS命令被忽略,
符號__CTOR_LIST__ 標記全域建構式的開始,符號__CTOR_END__標記結束,同樣的,__DTOR_LIST__和__DTOR_END__分別標記全域解構式的開始和結束,第一個串列中的字是入口的數量,后面是每個建構式或者解構式的地址,最后是一個全零的字,編譯器必須安排實際運行代碼,對于這些目標檔案格式,GNU C++通常從一個 __main 子程式中呼叫建構式,而對 __main 的呼叫自動被插入到 main 的啟動代碼中,GNU C++通常使用 atexit 運行解構式,或者直接從函式 exit 中退出,
對于COFF或者ELF等支持任意段名字的目標檔案格式,GNU C++通常把全域建構式和解構式放入 .ctors 和 .dtors 段,把下面的代碼放入你的鏈接腳本,將會創建GUN C++運行時期望的表,
__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__ = .;
如果你正在使用GUN C++支持的初始化優先級,初始化優先級提供了一些對全域建構式運行順序的控制,則你必須在鏈接時對建構式排序以保證它們以正確的順序執行,當你使用CONSTRUCTORS 命令,使用 ‘SORT_BY_NAME(CONSTRUCTORS)’ 替換它,當使用 .ctors 和 .dtors 段,使用 ‘(SORT_BY_NAME(.ctors))’ 和’ (SORT_BY_NAME(.dtors))’ 取代 ‘(.ctors)’ 和’ ‘(.dtors)’ ,
通常編譯器和聯結器會自動處理這些問題,您不需要關心它們,但是,在你自己寫鏈接腳本且正在使用C++的時候,你可能需要考慮這些,
3.6.7 Output Section Discarding
聯結器通常不會創建沒有內容的輸出段,這是為了方便參考那些有可能出現或者不出現任何輸入檔案中的段,例如:
.foo : { *(.foo) }
只有在至少有一個輸入檔案含有 ’.foo’ 段且 ’.foo’ 段不為空的時候才會在輸出檔案創建一個 ’.foo’ 段,其它鏈接腳本指出在一個段中間分配空間也會創建輸出段,賦值也一樣即使賦值沒有創建空間,除了‘. = 0’, ‘. = . + 0’, ‘. = sym’, ‘. = . + sym’ 和‘. = ALIGN (. != 0, expr, 1)’ 其中 ’sym’ 是一個值為0的已定義絕對符號,因此你可以強制一個空的輸出段使用 ‘. = .’,
聯結器將忽略為丟棄的輸出段進行地址賦值(請參見Output Section Address),除非聯結器腳本在輸出段中定義符號,在這種情況下,聯結器將遵守地址賦值,有可能更新 ’.’ 的值,即便段被拋棄了,
特殊輸出段名稱 ’/DISCARD/’ 可被用來拋棄輸入段,一個被分派到名為 ’/DISCARD/’ 的輸出段的輸入段將不會被包含在輸出檔案中,
3.6.8 Output Section Discarding
我們在前面展示了輸出部分的完整描述如下:
section [address] [(type)] :
[AT(lma)]
[ALIGN(section_align) | ALIGN_WITH_INPUT]
[SUBALIGN(subsection_align)]
[constraint]
{
output-section-command
output-section-command
…
} [>region] [AT>lma_region] [:phdr :phdr …] [=fillexp]
我們已經描述了section, address, and output-section-command命令,在本節中,我們將描述其余的段屬性,
- Output Section Type: 輸出段型別
- Output Section LMA: 輸出段LMA —加載地址
- Forced Output Alignment: 強制輸出對齊
- Forced Input Alignment: 強制輸入對齊
- Output Section Constraint: 輸出段限制
- Output Section Region: 輸出段區域
- Output Section Phdr: 輸出段phdr
- Output Section Fill: 輸出段填充
3.6.8.1 Output Section Type
每個輸出段可以有一個型別,型別是圓括號中的關鍵字,定義了以下型別:
NOLOAD
此段應標記為不可加載,以便在程式運行時不會將其加載到記憶體中,
DSECT
COPY
INFO
OVERLAY
支持這些型別名稱是為了向后兼容,而且很少使用,它們都具有相同的效果:該段應該標記為不可分配,以便在程式運行時不會為該段分配記憶體,
聯結器通常根據映射到輸出段的輸入段設定輸出段的屬性,您可以使用 section 型別來覆寫它,例如,在下面的腳本示例中,’ ROM ’ 部分位于記憶體位置 ’ 0 ',在程式運行時不需要加載它,
SECTIONS {
ROM 0 (NOLOAD) : { … }
…
}
3.6.8.2 Output Section LMA
每個段有一個虛擬地址(VMA)和一個加載地址(LMA);參見 Basic Script Concepts,虛擬地址由前面描述的 Output Section Address指定,加載地址由 AT 或 AT> 關鍵字指定,指定加載地址是可選的,
AT 關鍵字把一個運算式當作自己的引數,這將指定段的實際加載地址 ,關鍵字 AT> 使用記憶體區域的名字作為引數,參考MEMORY,段的加載地址被設定為該區域的當前空閑位置,并且按照段對齊要求對齊,
如果沒有為可分配段使用 AT 和 AT>,聯結器會使用下面的方式嘗試來決定加載地址:
- 如果段有一個特定的VMA地址,則LMA也使用該地址,
- 如果段為不可分配的則LMA被設定為它的VMA,
否則如果可以找到符合當前段的一個記憶體區域,且此區域至少包含了一個段,則設定LMA在那里,如此VMA和LMA的區別類似于VMA和LMA在該區域的上一個段的區別, - 如果沒有宣告記憶體區域且默認區域覆寫了整個地址空間,則采用前面的步驟,
- 如果找不到合適的區域或者沒有前面存在的段,則LMA被設定為等于VMA,
這些功能旨在使構建ROM映像變得容易,例如,以下聯結器腳本創建三個輸出段:一個名為“.text”,從0x1000開始;一個名為“.mdata”,即使其VMA為0x2000,也加載在“.text”節的末尾;另一個名為“.bss”,用于在地址0x3000保存未初始化的資料,符號’_data’被定義為值0x2000,這表明位置計數器保存VMA值,而不是LMA值,
SECTIONS
{
.text 0x1000 : { *(.text) _etext = . ; }
.mdata 0x2000 :
AT ( ADDR (.text) + SIZEOF (.text) )
{ _data = . ; *(.data); _edata = . ; }
.bss 0x3000 :
{ _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
}
此鏈接腳本的運行時初始化代碼應該類似于下面的形式,把初始化資料從ROM鏡像復制到運行時地址,注意這些代碼是如何利用聯結器腳本定義的符號的,
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;
3.6.8.3 Forced Output Alignment
你可以使用ALIGN增加輸出段的對齊,作為替換,你可以通過ALIGN_WITH_INPUT屬性強制VMA與LMA自始至終保持它們之間的區別,
您可以使用ALIGN來增加輸出段的對齊方式,作為一種替代方法,您可以使用ALIGN_WITH_INPUT屬性在整個輸出段保持VMA和LMA之間的差異,
3.6.8.4 Forced Input Alignment
您可以使用SUBALIGN來強制輸出段中的輸入段對齊,指定的值將覆寫輸入段提供的任何對齊方式,無論比原來大還是小,
3.6.8.5 Output Section Constraint
通過分別使用關鍵字 ONLY_IF_RO 和ONLY_IF_RW,可以指定只有在所有輸入段都是只讀或所有輸入段都是讀寫的情況下才創建輸出段,
3.6.8.6 Output Section Region
可以使用 ’>region’ 把一個段指定到此前設定的記憶體區域內,參見MEMORY,
下面是一個例子:
MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }
SECTIONS { ROM : { *(.text) } >rom }
3.6.8.7 Output Section Phdr
您可以使用 ':phdr ’ 將一個段分配給先前定義的程式段,參見 PHDRS,如果一個段被分配給一個或多個段,那么所有后續分配的段也將被分配給這些段,除非它們顯式地使用 :phdr 修飾符,您可以使用:NONE來告訴聯結器根本不要將該段放在任何段中,
這里有一個簡單的例子:
PHDRS { text PT_LOAD ; }
SECTIONS { .text : { *(.text) } :text }
3.6.8.8 Output Section Fill
你可以使用’=fillexp’為整個段設定填充模板,fillexp是一個運算式(參考Expressions),任何其它的未被特殊指定的輸出段的記憶體區域(例如,因為對其輸入段產生的縫隙)將會被用fillexp的值填充,如果有需要可以重復填充,如果運算式是一個簡單的hex數字,例如一個十六進制數字由’0x’開頭且結尾沒有 ’k’ 或 ’M’,則一個任意長的十六進制數字可以被用來給填充模板賦值,前面的0同樣成為模板的一部分,在其它情況中,包含額外的括號或者一個一元+,填充模板為運算式值的最低4個有意義的位元組,在所有情況中,數字總是大端的,
你也可以使用FILL命令設定填充值(參考Output Section Data),
這里有一個簡單的例子:
SECTIONS { .text : { *(.text) } =0x90909090 }
3.6.9 Overlay Description
覆寫描述提供了一種簡單的方法來描述將作為單個記憶體映像的一部分加載但將在相同記憶體地址上運行的段,在運行時,某種型別的覆寫管理器將根據需要從運行時記憶體地址復制覆寫的段,可能通過簡單地操作尋址位來實作,這種方法可能很有用,例如,當某個記憶體區域比另一個區域更快時,
覆寫描述使用OVERLAY命令,OVERLAY命令和SECTIONS命令一起使用,就像一個輸出段描述符,完整的OVERLAY命令的語意如下:
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] [,]
除了OVERLAY(關鍵字),以及每個段都必須有一個名字(上面的secname1和secname2),所有的部分都是可選的,除了OVERLAY中不能為段定義地址和記憶體區域,使用OVERLAY結構定義的段類似于那些普通的SECTIONS中的結構(參考SECTIONS),
結尾的逗號可能會被使用,如果使用了 fill 且下一個 sections-command 看起來像是運算式的延續,
所有的段都使用同樣的開始地址定義,所有段的載入地址都被排布,使它們在記憶體中從整個’OVERLAY’的載入地址開始都是連續的(就像普通的段定義,載入地址是可選的,預設的就是開始地址;開始地址也是可選的,預設是當前的位置計數器的值),
如果使用了關鍵字NOCROSSREFS,并且在任何段間有互相參考,聯結器將會產生一個錯誤報告,因為所有的段運行在同樣的地址,直接參考其它的段通常沒有任何意義,參考NOCROSSREFS,
每個伴隨OVERLAY的段,聯結器自動提供兩個符號,符號*__load_start_secname被定義為段的起始地址,符號__load_stop_secname被定義為段結束地址,任何不符合C定義的伴隨secname*的字符都將被移除,C(或者匯編)代碼可以使用這些符號在需要時搬移復蓋代碼,
覆寫之后,位置計數器的值設定為覆寫的起始值加上最大段的長度,
下面是例子,請記住這應該放在SECTIONS結構內,
OVERLAY 0x1000 : AT (0x4000)
{
.text0 { o1/*.o(.text) }
.text1 { o2/*.o(.text) }
}
這將把 ’.text0’ 和 ’.text1’ 的起始地址設定為地址 0x1000,’.text0’ 的加載地址為 0x4000,’.text1’ 會加載到 ’.text0’ 后面,下面的符號如果被參考則會被定義: __load_start_text0, __load_stop_text0, __load_start_text1, __load_stop_text1,
C代碼拷貝覆寫.text1到覆寫區域可能像下面的形式,
extern char __load_start_text1, __load_stop_text1;
memcpy ((char *) 0x1000, &__load_start_text1,
&__load_stop_text1 - &__load_start_text1);
注意’OVERLAY’命令只是為了語法上的便利,因為它所做的所有事情都可以用更加基本的命令加以代替,上面的例子可以用下面的寫法:
.text0 0x1000 : AT (0x4000) { o1/*.o(.text) }
PROVIDE (__load_start_text0 = LOADADDR (.text0));
PROVIDE (__load_stop_text0 = LOADADDR (.text0) + SIZEOF (.text0));
.text1 0x1000 : AT (0x4000 + SIZEOF (.text0)) { o2/*.o(.text) }
PROVIDE (__load_start_text1 = LOADADDR (.text1));
PROVIDE (__load_stop_text1 = LOADADDR (.text1) + SIZEOF (.text1));
. = 0x1000 + MAX (SIZEOF (.text0), SIZEOF (.text1));
3.7 MEMORY Command
聯結器的默認配置允許分配所有可用記憶體,您可以使用 MEMORY 命令來多載它,
MEMORY 命令描述目標中記憶體塊的位置和大小,您可以使用它來描述聯結器可以使用哪些記憶體區域,以及聯結器必須避免使用哪些記憶體區域,你可以把段放到特定的記憶體區域里,聯結器將會基于記憶體區域設定段地址,如果區域趨于飽和將會產生警告資訊,聯結器不會為了把段更好的放入記憶體區域而打亂段的順序,
聯結器腳本可能包含 MEMORY 命令的許多用法,但是,定義的所有記憶體塊都被視為在單個 MEMORY 命令中指定的,記憶體的語法是:
MEMORY
{
name [(attr)] : ORIGIN = origin, LENGTH = len
…
}
name 是聯結器腳本中用于參考記憶體區域的名稱,區域名稱在聯結器腳本之外沒有任何意義,區域名稱存盤在單獨的名稱空間中,不會與符號名、檔案名或段沖突,每個記憶體區域在 MEMORY 命令中必須有一個不同的名稱,但是你此后可以使用REGION_ALIAS命令為已存在的記憶體區域添加別名,
attr 字符是一個可選的屬性串列,用于指定是否對聯結器腳本中未顯式映射的輸入段使用特定的記憶體區域,如 SECTIONS中所述,如果不為某些輸入段指定輸出段,則聯結器將創建一個與輸入段同名的輸出段,如果定義區域屬性,聯結器將使用它們為它創建的輸出段選擇記憶體區域,
attr 字串只能使用下面的字符組成:
- ‘R’ 只讀段
- ‘W’ 讀寫段
- ‘X’ 可執行段
- ‘A’ 可分配段
- ‘I’ 已初始化段
- ‘L’ 類似于’I’
- ‘!’ 反轉其后面的所有屬性
如果一個未映射段匹配了上面除 ’!’ 之外的一個屬性,它就會被放入該記憶體區域,’!’ 屬性對該測驗取反,所以只有當它不匹配上面列出的行何屬性時,一個未映射段才會被放入到記憶體區域,
origin 是記憶體區域起始地址的數值運算式,運算式的計算結果必須為常量,并且不能包含任何符號,關鍵字ORIGIN可以縮寫為org 或 o(但不能是ORG),
len 是記憶體區域的位元組大小的運算式,與原始運算式一樣,運算式必須僅為數值,并且必須計算為常量,關鍵字長度可以縮寫為 len 或 l,
在下面的示例中,我們指定有兩個記憶體區域可供分配:一個從“0”開始空間大小為256k位元組,另一個從“0x40000000”開始空間大小為4M位元組,聯結器將把未顯式映射到記憶體區域的每個部分放入“rom” 記憶體區域,這些部分要么是只讀的,要么是可執行的,聯結器會將未顯式映射到記憶體區域的其他部分放入 “ram” 記憶體區域,
MEMORY
{
rom (rx) : ORIGIN = 0, LENGTH = 256K
ram (!rx) : org = 0x40000000, l = 4M
}
定義記憶體區域后,可以使用 ‘>region’ 輸出段屬性指引聯結器把特殊輸出段放到該記憶體區域,例如,如果您有一個名為 ‘mem’ 的記憶體區域,你可以在輸出段定義中使用 ’>mem’,請參見Output Section Region,如果沒有為輸出段指定地址,聯結器將把地址設定為記憶體區域內的下一個可用地址,如果指向某個記憶體區域的組合輸出段對于該區域來說太大,則聯結器將發出錯誤訊息,
可以通過 ORIGIN(memory) 和 LENGTH(memory) 函式獲得記憶體區域的起始地址以及長度:
_fstack = ORIGIN(ram) + LENGTH(ram) - 4;
3.8 PHDRS Command
ELF物件檔案格式使用程式頭,類似于段,程式頭描述了如何將程式加載到記憶體中,您可以使用帶有 ’ -p ’ 選項的 objdump 程式將它們列印出來,
當您在本地運行ELF程式時,系統加載程式將讀取程式頭以確定如何加載程式,只有當程式頭設定正確時,這才會作業,本手冊沒有詳細描述系統加載程式如何解釋程式頭;有關更多資訊,請參見ELF ABI,
默認的聯結器將會創建合適的程式頭部,但是,有些情況下,你可能需要更加精確地指定程程式頭,可以使用 PHDRS 命令達到此目的,當聯結器在聯結器腳本中看到PHDRS命令時,它將只創建指定的程式頭,
聯結器僅在創建ELF輸出檔案時才會關注PHDRS命令,其他情況下聯結器將會忽視PHDRS,
下面是PHDRS的語法,PHDRS, FILEHDR, AT, FLAGS都是關鍵字:
PHDRS
{
name type [ FILEHDR ] [ PHDRS ] [ AT ( address ) ]
[ FLAGS ( flags ) ] ;
}
name 僅用于聯結器腳本的 SECTIONS 命令中的參考,它不會被放到輸出檔案中,程式頭名稱存盤在單獨的名稱空間中,不會與符號名稱、或者段名產生沖突,每個程式頭必須有一個不同的名稱,頭按照順序執行,且通常將它們以上升的加載順序映射到段,
具體的程式頭型別描述系統加載程式將從檔案加載的頭部段,在聯結器腳本中,可以通過放置可再分配輸出段在頭部段內來指定頭部段的內容,您可以使用’:phdr '輸出段屬性將段放在特定的段中,請參閱Output Section Phdr,
將某一個段放在多于一個的段中是很正常的,這僅僅意味著一個記憶體段包含另一個記憶體段,可以為每個應當包含段的頭部段重復使用 ’:phdr’ 命令,
如果使用 ‘:phdr’ 將段放在一個或多個段中,則聯結器會將所有未指定 ‘:phdr’ 的后續可分配段放在同一段中, 這是為了方便起見,因為通常將一整套連續段放在單個段中, 您可以使用:NONE覆寫默認段,并告訴聯結器不要將該段放在任何段中,
您可以在程式頭型別之后使用 FILEHDR 和 PHDRS 關鍵字來進一步描述段的內容, FILEHDR關鍵字意味著該段應包含ELF檔案頭, PHDRS關鍵字意味著該段應包括ELF程式頭本身, 如果應用于可加載段(PT_LOAD),則所有先前的可加載段都必須具有以下關鍵字之一,
型別可以是以下之一, 數字表示關鍵字的值,
- PT_NULL (0)表示未使用的程式頭,
- PT_LOAD (1)表示此程式頭描述了要從檔案中加載的段,
- PT_DYNAMIC (2)表示可以找到動態鏈接資訊的段,
- PT_INTERP (3)表示可以在其中找到程式解釋器名稱的段,
- PT_NOTE (4)表示包含注釋資訊的段,
- PT_SHLIB (5)保留的程式頭型別,由ELF ABI定義但未指定,
- PT_PHDR (6)表示可以在其中找到程式頭的段,
- PT_TLS(7)指示包含執行緒本地存盤的段,
- expression 該運算式給出程式頭的數字型別, 這可以用于上面未定義的型別,
您可以使用 AT 運算式指定將段加載到記憶體中的特定地址, 這與 AT 作為輸出段使用屬性時的方法一樣(參考Output Section LMA),程式頭的AT命令會覆寫輸出段屬性,
聯結器通常會根據組成段的段來設定段標志, 您可以使用 FLAGS 關鍵字來顯式指定段標志, 標志的值必須是整數, 它用于設定程式頭的 p_flags 欄位,
下面是一個PHDRS例子, 顯示了在本機ELF系統上使用的一組典型的程式頭,
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
…
}
3.9 VERSION Command
使用ELF時,聯結器支持符號版本,符號版本僅在使用共享庫時有用,當動態聯結器運行的程式可能已鏈接到共享庫的早期版本時,動態聯結器可以使用符號版本來選擇函式的特定版本,
可以在主聯結器腳本中直接包含版本腳本,也可以將版本腳本作為隱式聯結器腳本提供,您也可以使用’–version script’聯結器選項,
VERSION命令的語法是:
VERSION { version-script-commands }
版本腳本命令的格式與solaris2.5中 Sun聯結器使用的格式相同,版本腳本定義了一個版本節點樹,您可以在版本腳本中指定節點名稱和相互依賴關系,可以指定將哪些符號系結到哪個版本節點,還可以把一組指定的符號限定到本地范圍,這樣在共享庫的外面它們就不是全域可見的了,
演示版本腳本語言的最簡單方法是使用幾個示例:
VERS_1.1 { global: foo1; local: old*; original*; new*; };VERS_1.2 {
foo2;
} VERS_1.1;
VERS_2.0 {
bar1; bar2;
extern "C++" {
ns::*;
"f(int, double)";
};
} VERS_1.2;
這個示例版本腳本定義了三個版本節點,定義的第一個版本節點是 VERS_1.1’;它沒有其他依賴項,腳本將符號 ‘foo1’ 系結到 ‘VERS_1.1’ ,腳本把一些符號縮減到區域可見,因此在共享庫外部它們將是不可見的;這是使用通配符模式完成的,因此以’old’,’original’,’new’開頭的符號將被匹配上,通配符模板與shell匹配檔案名時使用的方法一致,但是,如果把特指的符號名放在雙引號中,則名字被按照字面意思處理,而不是正則運算式模板,
接下來,版本腳本定義節點 ‘VERS_1.2’ ,此節點依賴于 ‘VERS_1.1’,腳本將符號 ‘foo2’系結到版本節點 ‘VERS_1.2’,
最后,版本腳本定義節點 ‘VERS_2.0’ ,此節點依賴于 ‘VERS_1.2’ ,腳本將符號 ‘bar1’ 和 ‘bar2’ 系結到版本節點 ‘VERS_2.0’,
當聯結器發現在庫中定義的符號沒有特別系結到版本節點時,它將有效地將其系結到庫的未指定的基本版本,通過在版本腳本的某個地方使用 ‘*global: ;’,可以將所有其他未指定的符號系結到給定的版本節點,注意,在全域規范中使用通配符有點瘋狂,除了在最后一個版本節點上,其他地方的全域通配符可能會意外地將符號添加到為舊版本匯出的集合中,這是錯誤的,因為舊版本應該有一套固定的符號,
版本節點的名字沒有什么特殊含義,但會給人閱讀帶來便利,‘2.0’版本也可以出現在 ‘1.1’ 和 ‘1.2’ 之間,然而,這將是一種令人困惑的撰寫版本腳本的方法,
節點名可以省略,前提是它是版本腳本中唯一的版本節點,這樣的版本腳本不為符號指定任何版本,只選擇哪些符號將全域可見,哪些符號不可見,
{ global: foo; bar; local: *; };
當您將應用程式鏈接到具有版本化符號的共享庫時,應用程式本身知道它需要每個符號的哪個版本,還知道它需要鏈接到的每個共享庫中的哪個版本節點,因此,在運行時,動態加載程式可以快速檢查以確保鏈接到的庫實際上提供了應用程式決議所有動態符號所需的所有版本節點,以這種方式,動態聯結器可以確定地知道它所需要的所有外部符號將是可決議的,而不必搜索每個符號參考,
符號版本控制實際上是一種更為復雜的方法,可以進行SunOS所做的次要版本檢查,這里要解決的基本問題是,對外部函式的參考通常根據需要進行系結,而不是在應用程式啟動時全部系結,如果共享庫過期,則可能缺少所需的介面;當應用程式嘗試使用該介面時,它可能會突然意外地失敗,使用符號版本控制,如果應用程式使用的庫太舊,用戶在啟動程式時會收到警告,
Sun的版本控制方法有幾個GNU擴展,其中第一項功能是將符號系結到源檔案中定義符號的版本節點,而不是在版本控制腳本中,這主要是為了減輕庫維護的作業量,你可以這樣做:
__asm__(".symver original_foo,foo@VERS_1.1");
在C源檔案中,這會將函式‘original_foo’ 重命名為系結到版本節點 ‘VERS’1.1’ 的 ‘foo’ 的別名,‘local:’ 指令可用于阻止匯出符號 ‘original_foo’ ,‘.symver’ 指令優先于版本腳本,
第二個GNU擴展允許同一個函式的多個版本出現在給定的共享庫中,通過這種方式,您可以在不增加共享庫的主要版本號的情況下對介面進行不兼容的更改,同時仍然允許與舊介面鏈接的應用程式繼續運行,
為此,必須在源檔案中使用多個‘.symver’ 指令,下面是一個例子:
__asm__(".symver original_foo,foo@");
__asm__(".symver old_foo,foo@VERS_1.1");
__asm__(".symver old_foo1,foo@VERS_1.2");
__asm__(".symver new_foo,foo@@VERS_2.0");
在本例中,’foo@’ 表示符號 ’foo’ 系結到沒有指定基礎版本的符號版本,源檔案包含此例子將會定義四個C函式:’original_foo’, ‘old_foo’, ‘old_foo1’, ‘new_foo’,
當給定符號有多個定義時,需要使用某種方法指定對該符號的外部參考將系結到的默認版本,您可以使用‘.symver’指令 的’ foo@@VERS_2.0 '型別的’來完成此操作,以這種方式,只能將符號的一個版本宣告為默認值;否則,您將實際上擁有同一符號的多個定義,
如果你希望系結共享庫中的一個符號到特定版本,只需很方便的使用別名(例如,’old_foo’),或者可以用 ’.symver’ 指令指定一個系結到外部函式的特定版本,
也可以指定版本腳本使用的語言:
VERSION extern "lang" { version-script-commands }
支持的 ‘lang ’是‘C’、‘C++’ 和 ‘Java’,聯結器會在鏈接時遍歷符號串列,并根據‘lang ’將它們與‘version-script-commands’中指定的模式進行匹配,默認的 ‘lang ’是‘C’,
被分解的名字可能含有空格以及其他特殊字符,按照上面說的,可以使用正則運算式模板匹配分解的名字,或者可以使用雙引號包裹的字串來精確匹配字串,在后一種情況中,注意位于版本腳本和分解輸出間一個小的不同(比如空格)將會引起不匹配,分解器創建的字串在未來可能會改變,即便將被重新組合的名字本身沒變,在升級版本時你需要檢查所有的版本指令是否都按照你期待的那樣作業,
3.10 Expressions in Linker Scripts
聯結器腳本語言中的運算式的語法與C運算式的語法相同,所有運算式都被計算為整數,所有運算式都以相同的大小計算,如果主機和目標都是32位,則為32位,否則為64位,
可以在運算式中使用和設定符號值,
聯結器定義了幾個用于運算式的特殊用途內建函式,
- Constants: 常數
- Symbolic Constants: 符號常量
- Symbols: 符號名稱
- Orphan Sections: 孤兒段
- ocation Counter: 位置計數器
- Operators: 運算子號
- Evaluation: 求值
- Expression Section: 運算式的段
- Builtin Functions: 內建函式
3.10.1 Constants
所有的常量都是整數,
與C中一樣,聯結器認為以 ‘0’ 開頭的整數是八進制數,以 ‘0x’ 或 ‘0X’開頭的整數是十六進制數,另外,聯結器接受后綴 ‘h’ 或 ‘H’ 表示十六進制,‘o’ 或 ‘O’ 表示八進制,‘b’ 或 ‘B’ 表示二進制,‘d’ 或 ‘D’ 表示十進制,任何沒有前綴或后綴的整數值都被認為是小數,
此外,您可以使用后綴 K 和 M 分別將一個常數縮放為1024或1024*1024,例如,以下均為同一數量:
_fourk_1 = 4K;
_fourk_2 = 4096;
_fourk_3 = 0x1000;
_fourk_4 = 10000o;
注意,K 和 M 后綴不能與前面的其他系數同時使用,
3.10.2 Symbolic Constants
可以通過使用 CONSTANT(name) 運算子來參考特定于目標的常量,其中 name為:
MAXPAGESIZE:目標的最大頁面大小,
COMMONPAGESIZE:目標的默認頁大小,
例如:
.text ALIGN (CONSTANT (MAXPAGESIZE)) : { *(.text) }
將會創建一個對齊到目標支持的最大頁邊界的代碼段,
3.10.3 Symbol Names
除引號外,符號名稱以字母、下劃線或句點開始,可以包括字母、數字、下劃線、句點和連字符,非參考符號名稱不能與任何關鍵字沖突,你可以指定一個包含奇數字符的符號或與關鍵字同名的符號,用雙引號包圍符號名稱:
"SECTION" = 9;
"with a space" = "also with a space" + 10;
由于符號可以包含許多非字母字符,用空格分隔符號是最安全的,例如,‘A-B’ 是一個符號,而 ‘A - B’ 是一個包含減法的運算式,
3.10.4 Orphan Sections
輸出檔案中沒有顯式放置在聯結器檔案中的段,聯結器仍將通過查找或創建適當的輸出段將這些段復制到輸出檔案中,以便在其中放置孤立的輸入段,
如果孤立輸入段的名稱與現有輸出段的名稱完全匹配,則孤立輸入段將放置在該輸出段的末尾,
如果沒有具有匹配名稱的輸出段,則將創建新的輸出段,每個新的輸出段都將具有與其中放置的孤立段相同的名稱,如果有多個具有相同名稱的孤立段,這些將被合并到一個新的輸出段中,
如果創建新的輸出節段來保存孤立的輸入段,則聯結器必須決定將這些新輸出段相對于現有輸出節的位置,在大多數現代目標上,聯結器試圖將孤立段放在同一屬性的段之后,例如代碼與資料、可加載與不可加載等,如果找不到具有匹配屬性的段,或者目標缺少此支持,則孤兒段將放在檔案的末尾,
命令列選項 ‘–orphan-handling’ 和 ‘–unique’ (請參Command-line Options)可以用于控制孤兒放在哪個輸出段,
3.10.5 The Location Counter
特殊的聯結器變數 ‘.’ 始終包含當前輸出位置計數器,因為 ’.’ 經常當作一個輸出段的地址使用,因此它只能位于SECTIONS命令中以一個運算式形式出現,任何普通符號可以出現在運算式中的位置都可以使用 ’.’,
為 ’.’ 賦值將會使得位置計數器移動,這可以用來在輸出段中創建空的區域,位置計數器不能在一個輸出段內向回移動,也不能在段外回退,如果這么做了將會創建重疊的LMA,
SECTIONS
{
output :
{
file1(.text)
. = . + 1000;
file2(.text)
. += 1000;
file3(.text)
} = 0x12345678;
}
在上面的例子里,檔案 file1 的 ’text’ 段位于輸出段 output 的起始位置,其后有個1000位元組的縫隙,此后 file2 的 ’.text’ 段出現在輸出段內,其后也有1000位元組的縫隙,最后是 file3 的 ’.text’ 段,標記 ’=0x12345678’ 指定了應當向縫隙中填充的內容(參考Output Section Fill),
注:’.’ 實際上是指從當前包含物件開始的位元組偏移量,通常為 SECTIONS 宣告,起始地址為0,因此 ’.’ 可以被當作一個絕對地址使用,但是如果 ’.’ 被在段描述符內使用,它表示從該段開始的偏移地址,不是一個絕對地址,因此在下面腳本中:
SECTIONS
{
. = 0x100
.text: {
*(.text)
. = 0x200
}
. = 0x500
.data: {
*(.data)
. += 0x600
}
}
‘.text’ 段將會被安排到起始地址0x100,實際大小為0x200位元組,即便 ’.text’ 輸入段沒有足夠的資料填充該區域(反之如果資料過多,將會產生一個錯誤,因為將會嘗試向前回退 ’.’ ),段 ’.data’ 將會從0x500開始,并且輸出段會有額外的0x600位元組空余空間在輸入段’.text’,
如果聯結器需要放置孤兒段,則將符號設定為輸出段陳述句外部的位置計數器的值可能會導致意外的值,例如,給定如下:
SECTIONS { start_of_text = . ; .text: { *(.text) } end_of_text = . ;start_of_data <span >=</span> <span >.</span> <span >;</span> <span >.</span>data<span >:</span> <span >{<!-- --></span> <span >*</span><span >(</span><span >.</span>data<span >)</span> <span >}</span> end_of_data <span >=</span> <span >.</span> <span >;</span>
}
如果聯結器需要放置一些輸入段,例如 ’.rodata’ 沒有在腳本中提及,可能會被選擇放到 ’.text’ 和 ’.data’ 段中間,你可能會覺得聯結器應該把 ’.rodata’ 放在上面腳本的空行處,但空行對于聯結器來說沒有任何實際意義,同樣的,聯結器也不會把符號名與段聯系起來,實際上,它假設所有定義或者其他宣告屬于前面的輸出段,除了特殊情況設定 ’.’,例如,聯結器將會類似于下面的腳本放置孤兒段:
SECTIONS { start_of_text = . ; .text: { *(.text) } end_of_text = . ;start_of_data <span >=</span> <span >.</span> <span >;</span> <span >.</span>rodata<span >:</span> <span >{<!-- --></span> <span >*</span><span >(</span><span >.</span>rodata<span >)</span> <span >}</span> <span >.</span>data<span >:</span> <span >{<!-- --></span> <span >*</span><span >(</span><span >.</span>data<span >)</span> <span >}</span> end_of_data <span >=</span> <span >.</span> <span >;</span>
}
這能符合或者不符合腳本作者對于 start_of_data 的設定意圖,一種影響孤兒段放置的辦法是為位置計數器指定自身的值,聯結器會認為一個 ’.’ 的設定是設定一個后面段的起始地址,因此該段應為一個組,因此可以這么寫:
SECTIONS { start_of_text = . ; .text: { *(.text) } end_of_text = . ;<span >.</span> <span >=</span> <span >.</span> <span >;</span> start_of_data <span >=</span> <span >.</span> <span >;</span> <span >.</span>data<span >:</span> <span >{<!-- --></span> <span >*</span><span >(</span><span >.</span>data<span >)</span> <span >}</span> end_of_data <span >=</span> <span >.</span> <span >;</span>
}
這樣以來,孤兒段 ’.rodata’ 將會被放置在 end_of_text 和 start_of_data之間,
3.10.6 Operators
聯結器識別標準的C算術運算子集,具有標準系結和優先級級別:
precedence associativity Operators Notes
(highest)
1 left ! - ~ (1)
2 left * / %
3 left + -
4 left >> <<
5 left == != > < <= >=
6 left &
7 left |
8 left &&
9 left ||
10 right ? :
11 right &= += -= *= /= (2)
(lowest)
注意:(1)前綴運算子(2)參見 Assignments,
3.10.7 Evaluation
聯結器惰性地計算運算式,它只在絕對必要時才會去計算運算式的值,
聯結器需要一些資訊,例如第一部分的起始地址的值,以及記憶體區域的來源和長度,才能夠完成所有的鏈接作業,這些值會在聯結器讀鏈接腳本的時候立即計算,
但是其他的值(例如符號值)在存盤分配之后才能知道或者需要,這種值將會推遲計算,直到符號賦值運算式的其他資訊(例如輸出段的大小)都可獲得后,
磁區的大小在分配之后才能知道,因此依賴它的賦值都將在分配后才會執行,
某些運算式(例如依賴于位置計數器 ‘.’ 的運算式)必須在段分配期間求值,
如果運算式的結果是必需的,但該值不可用,則會產生錯誤,例如,下面這樣的腳本:
SECTIONS
{
.text 9+this_isnt_constant :
{ *(.text) }
}
會導致錯誤訊息:‘non constant expression for initial address’
3.10.8 The Section of an Expression
地址和符號可以是段相對的,也可以是絕對的,段的相對符號是可重定位的,如果使用 ‘-r’ 選項請求可重定位輸出,則進一步的鏈接操作可能會更改段相對符號的值,另一方面,絕對符號將在任何進一步的鏈路操作中保持相同的值,
聯結器運算式中的某些術語是地址,對于段相關符號和回傳地址的內置函式(如ADDR、LOADADDR、ORIGIN 和 SEGMENT_START),都是如此,其他術語只是數字,或者是回傳非地址值(如長度)的內置函式,一個復雜的問題是,除非您設定LD_FEATURE(“SANE_EXPR”)(請參見Miscellaneous Commands),否則數字和絕對符號將根據其位置進行不同的處理,以與舊版本的LD兼容,出現在輸出段定義之外的運算式將所有數字視為絕對地址,出現在輸出段定義中的運算式將絕對符號視為數字,如果給定了LD_FEATURE(“SANE_EXPR”),則任何位置的絕對符號和數字都被簡單的當作數字,
在下面這個簡單的例子中:
SECTIONS
{
. = 0x100;
__executable_start = 0x100;
.data :
{
. = 0x10;
__data_start = 0x10;
*(.data)
}
…
}
在上述兩個賦值例子中:’.’ 和 ’__executable_start’ 都被設定為絕對地址0x100,在后兩個賦值中,’.’ 和 ’__data_start’ 被設定為相對于 ’.data’ 的0x10,
對于涉及數字、相對地址和絕對地址的運算式,ld采用以下規則求值:
-
對絕對地址或數字進行一元運算,對兩個絕對地址或兩個數字進行二進制運算,或在一個絕對地址和一個數字之間進行二元運算,在數值上應用運算子,
-
對一個相對地址的一元運算,以及對同一部分中的兩個相對地址或一個相對地址與一個數字之間的兩個相對地址的二進制運算,將運算子應用于地址的偏移部分,
-
其他二進制操作,即不在同一段中的兩個相對地址之間,或相對地址和絕對地址之間,在應用運算子之前,首先將任何非絕對項轉換為絕對地址,
每個子運算式的結果部分如下:
- 只有數字參與的運算子結果為數字,
- 比較運算’&&’和’||’的結果也是數字,
- 對同一部分中的兩個相對地址或兩個絕對地址(在上述轉換之后)進行的其他二進制算術和邏輯操作的結果,當LD_FEATURE(“SANE_EXPR”)或在輸出部分定義內時,也是一個數字,但在其他情況下是絕對地址,
- 對相對地址或一個相對地址和一個數字進行其他操作的結果是,在相對運算元的同一部分中有一個相對地址,
- 對絕對地址的其他操作(在上述轉換之后)的結果是一個絕對地址,
可以使用內建函式ABSOLUTE來強制一個本來是相對地址的運算式變為絕對地址,例如,要創建一個設定為輸出段‘.data’結尾地址的絕對符號:
SECTIONS
{
.data : { *(.data) _edata = ABSOLUTE(.); }
}
如果不使用’ABSOLUTE’,’_edata’將會為’.data’段的相對地址,
使用LOADADDR也會強制一個運算式變為絕對地址,因為此特殊內建函式回傳一個絕對地址,
3.10.9 Builtin Functions
聯結器腳本語言包括許多用于聯結器腳本運算式的內建函式,
- ABSOLUTE(exp)
回傳運算式exp的絕對值(不可重定位,非負),主要用于在段定義中為符號賦絕對值,其中符號值通常是段相對的,參見ABSOLUTE(exp), - ADDR(section)
回傳名為 ’section’ 的段的地址(VMA),你的腳本必須事先為該段定義了位置,在下面的例子里,start_of_output_1, symbol_1, symbol_2分配了同樣的值,除了symbol_1將是相對于.output1段的,而其他兩個值是絕對的:
SECTIONS { …
.output1 :
{
start_of_output_1 = ABSOLUTE(.);
…
}
.output :
{
symbol_1 = ADDR(.output1);
symbol_2 = start_of_output_1;
}
… }
- ALIGN(align)
- ALIGN(exp,align)
回傳位置計數器(.)或任意運算式對齊到下一個align指定邊界的值,單運算元ALIGN并不會改變位置計數器的值,它只是對其進行算術運算,兩個運算元ALIGN允許任意運算式向上對齊(ALIGN(ALIGN)等價于ALIGN(絕對(.),ALIGN)),
下面是一個示例,它將輸出 .data段對齊到上一段之后的下一個0x2000位元組邊界,并將該段中的一個變數設定為輸入段之后的下一個0x8000位元組邊界:
SECTIONS { …
.data ALIGN(0x2000): {
*(.data)
variable = ALIGN(0x8000);
}
… }
在本例中,ALIGN的第一次使用指定了段的位置,因為它被用作段定義的可選地址屬性(參見Output Section Address),ALIGN的第二種用法用于定義符號的值,
內建函式NEXT與ALIGN密切相關,
- ALIGNOF(section)
如果section已分配,回傳名為section的對齊位元組,如果段還沒被分配,聯結器會報錯,下面的例子里,.output段的對齊存盤在該段的第一個值,
SECTIONS{ …
.output {
LONG (ALIGNOF (.output))
…
}
… }
-
BLOCK(exp)
這是ALIGN的同義詞,用于與舊的聯結器腳本兼容,在設定輸出段的地址時最常見, -
DATA_SEGMENT_ALIGN(maxpagesize, commonpagesize)
它等于任何一個
(ALIGN(maxpagesize) + (. & (maxpagesize - 1)))
或者
(ALIGN(maxpagesize)
+ ((. + commonpagesize - 1) & (maxpagesize - commonpagesize)))
這取決于后者對資料段(運算式結果和DATA_SEGMENT_END之間的區域)使用的commonpagesize大小的頁面是否比前者更少,如果使用后一種形式
如果后面的形式被使用了,表示著保存commonpagesize位元組的運行時記憶體,花費的代價最多浪費commonpagesize大小的磁盤空間,
此運算式只能直接在SECTIONS命令中使用,不能在任何輸出段描述中使用,并且只能在聯結器腳本中使用一次,commonpagesize應該小于或等于maxpagesize,并且應該是物件希望優化的系統頁面大小,同時仍在系統頁面大小達到maxpagesize時運行,但是請注意,如果系統頁面大小大于commonpagesize,則‘-z relro’保護將無效,
例如:
. = DATA_SEGMENT_ALIGN(0x10000, 0x2000);
- DATA_SEGMENT_END(exp)
此命令為DATA_SEGMENT_ALIGN運算定義了資料段的結尾,
. = DATA_SEGMENT_END(.);
- DATA_SEGMENT_RELRO_END(offset, exp)
此命令為使用 ’-z relro’ 命令的情況定義了PT_GNU_RELRO段的結尾,當 ’-z relro’ 選項不存在時,DATA_SEGMENT_RELRO_END不做任何事情,否則將填充DATA_SEGMENT_ALIGN,以便exp + 偏移量與DATA_SEGMENT_ALIGN給定的commonpagesize引數對齊,如果它出現在聯結器腳本中,那么它必須放在DATA_SEGMENT_ALIGN和DATA_SEGMENT_END之間,計算為第二個引數加上PT_GNU_RELRO段末尾由于節對齊而需要的任何填充,
. = DATA_SEGMENT_RELRO_END(24, .);
- DEFINED(symbol)
如果符號在聯結器全域符號表中,并且在腳本中定義的陳述句之前定義,則回傳1,否則回傳0,可以使用此函式為符號提供默認值,例如,以下腳本片段演示如何將全域符號 ‘begin’ 設定為 ‘.text’ 段中的第一個位置,但如果名為 ‘begin’ 的符號已經存在,則其值將被保留,
SECTIONS { …
.text : {
begin = DEFINED(begin) ? begin : . ;
…
}
…
}
-
LENGTH(memory)
回傳名為 memory 記憶體區域的長度, -
LOADADDR(section)
回傳名為 section 的段的LMA絕對地址(參見Output Section LMA), -
LOG2CEIL(exp)
回傳 exp 的二進制對數,取整為無窮大,LOG2CEIL(0)回傳0, -
MAX(exp1, exp2)
回傳 exp1 和 exp2 的最大值, -
MIN(exp1, exp2)
回傳 exp1 和 exp2 的最小值, -
NEXT(exp)
回傳下一個未分配的地址,它是 exp 的倍數,此函式與 A*LIGN(exp)*密切相關;除非使用 MEMORY 命令為輸出檔案定義不連續記憶體,否則這兩個函式是等效的, -
ORIGIN(memory)
回傳名為 memory 的記憶體區域的起始地址, -
SEGMENT_START(segment, default)
回傳命名段的基址,如果已經為此段指定了顯式值(使用命令列 ‘-T’ 選項),則將回傳該值,否則該值將為默認值,目前,’-T’命令列選項只能用于設定 “text” 、“data” 和 “bss” 段的基址,但你可以使用SEGMENT_START搭配任何段名字, -
SIZEOF(section)
回傳名為 section 段的位元組數,如果段還沒被分配就是用函式求值,將會產生錯誤,下面是一個例子,symbol_1 和 symbol_2 的值相同:
SECTIONS{ …
.output {
.start = . ;
…
.end = . ;
}
symbol_1 = .end - .start ;
symbol_2 = SIZEOF(.output);
… }
- SIZEOF_HEADERS
- sizeof_headers
回傳輸出檔案頭的大小(以位元組為單位),這是一個會出現在輸出檔案的起始位置的資訊,如果您愿意,您可以在設定第一段的起始地址時使用此數字,以方便分頁,
生成ELF輸出檔案時,如果聯結器腳本使用 SIZEOF_HEADERS 內建函式,則聯結器必須在確定所有節地址和大小之前計算程式頭的數量,如果聯結器后來發現它需要額外的程式頭,它將報告一個錯誤“沒有足夠的空間來容納程式頭”,要避免此錯誤,必須避免使用 SIZEOF_HEADERS 函式,或者必須重新撰寫聯結器腳本以避免強制聯結器使用其他程式頭,或者必須使用 PHDRS 命令自己定義程式頭(請參見 PHDRS),
3.11 Implicit Linker Scripts
如果你指定了一個鏈接輸入檔案,而聯結器無法將其識別為一個目標檔案或者庫檔案,它將嘗試將該檔案作為聯結器腳本讀取,如果無法將檔案決議為聯結器腳本,則聯結器將報告錯誤,
隱式聯結器腳本不會替換默認聯結器腳本,
通常,隱式聯結器腳本只包含符號分配,或 INPUT、GROUP 或 VERSION 命令,
讀取任何輸入檔案時,由于隱式聯結器腳本將在命令列中讀取隱式聯結器腳本的位置讀取,這會影響庫的搜索,
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/525845.html
標籤:嵌入式
上一篇:如何找到埠的行程號
