| 摘自:http://bbs.kaspersky.com.cn/thread-281159-1-1.html
ld.info鏈接腳本解讀 ? [Unix/Linux學堂]
最近在看Linux內核時,總是遇到一些和連接腳本相關的東東,搞得人一頭霧水,終于下定決心把它搞明白,寫下一點心得,希望對和我一樣的人有所幫助! ? ?? ?
連接腳本的格式 ====================
連接腳本是文本文件.
你寫了一系列的命令作為一個連接腳本. 每一個命令是一個帶有參數的關鍵字,或者是一個對符號的賦值. 你可 以用分號分隔命令. 空格一般被忽略.
文件名或格式名之類的字符串一般可以被直接鍵入. 如果文件名含有特殊字符,比如一般作為分隔文件名用的逗 號, 你可以把文件名放到雙引號中. 文件名中間無法使用雙引號.
你可以象在C語言中一樣,在連接腳本中使用注釋, 用'/*'和'*/'隔開. 就像在C中,注釋在語法上等同于空格.
簡單的連接腳本示例 ============================
許多腳本是相當的簡單的.
可能的最簡單的腳本只含有一個命令: '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'節之間創建一個小的缺口.
就這樣,這是一個簡單但完整的連接腳本.
簡單的連接腳本命令. =============================
在本章中,我們會描述一些簡單的腳本命令.
設置入口點. -----------------------
在運行一個程序時第一個被執行到的指令稱為"入口點". 你可以使用'ENTRY'連接腳本命令來設置入口點.參數 是一個符號名: ? ? ENTRY(SYMBOL)
有多種不同的方法來設置入口點.連接器會通過按順序嘗試以下的方法來設置入口點, 如果成功了,就會停止.
??* `-e'入口命令行選項;
??* 連接腳本中的`ENTRY(SYMBOL)'命令;
??* 如果定義了start, 就使用start的值;
??* 如果存在,就使用'.text'節的首地址;
??* 地址`0'.
處理文件的命令. ---------------------------
有幾個處理文件的連接腳本命令.
`INCLUDE FILENAME' 在當前點包含連接腳本文件FILENAME. 在當前路徑下或用'-L'選項指定的所有路徑下搜索這個文件, 你可以嵌套使用'INCLUDE'達10層.
`INPUT(FILE, FILE, ...)' `INPUT(FILE FILE ...)' 'INPUT'命令指示連接器在連接時包含文件, 就像它們是在命令行上指定的一樣.
比如,如果你在連接的時候總是要包含文件'subr.o',但是你對每次連接時要在命令行上輸入感到厭煩 , 你就可以在你的連接腳本中輸入'INPUT (subr.o).
事實上,如果你喜歡,你可以把你所有的輸入文件列在連接腳本中, 然后在連接的時候什么也不需要, 只要一個'-T'選項就夠了.
在一個'系統根前綴'被配置的情況下, 一個文件名如果以'/'字符打頭, 并且腳本也存放在系統根 前綴的某個子目錄下, 文件名就會被在系統根前綴下搜索. 否則連接器就會企圖打開當前目錄下的文 件. 如果沒有發現, 連接器會通過檔案庫搜索路徑進行搜索.
如果你使用了'INPUT (-lFILE)', 'ld'會把文件名轉換為'libFILE.a', 就象命令行參數'-l'一樣.
當你在一個隱式連接腳本中使用'INPUT'命令的時候, 文件就會在連接時連接腳本文件被包含的點上 被包含進來. 這會影響到檔案搜索.
`GROUP(FILE, FILE, ...)' `GROUP(FILE FILE ...)' 除了文件必須全是檔案文件之外, 'GROUP'命令跟'INPUT'相似, 它們會被反復搜索,直至沒有未定義 的引用被創建.
`OUTPUT(FILENAME)' 'OUTPUT'命令命名輸出文件. 在連接腳本中使用'OUTPUT(FILENAME)'命令跟在命令行中使用'-o FILENAME'命令是完全等效的. 如果兩個都使用了, 那命令行選項優先.
你可以使用'OUTPUT'命令為輸出文件創建一個缺省的文件名,而不是常用的'a.out'.
`SEARCH_DIR(PATH)' `SEARCH_DIR'命令給'ld'用于搜索檔案文件的路徑中再增加新的路徑. 使用`SEARCH_DIR(PATH)'跟在 命令行上使用'-L PATH'選項是完全等效的. 如果兩個都使用了, 那連接器會兩個路徑都搜索. 用命 令行選項指定的路徑首先被搜索.
`STARTUP(FILENAME)' 除了FILENAME會成為第一個被連接的輸入文件, 'STARTUP'命令跟'INPUT'命令完全相似, 就象這個文 件是在命令行上第一個被指定的文件一樣. 如果在一個系統中, 入口點總是存在于第一個文件中,那 這個就很有用.
處理目標文件格式的命令. -----------------------------------------
有兩個處理目標文件格式的連接腳本命令.
`OUTPUT_formAT(BFDNAME)' `OUTPUT_formAT(DEFAULT, BIG, LITTLE)' `OUTPUT_formAT'命令為輸出文件使用的BFD格式命名. 使用`OUTPUT_formAT(BFDNAME)'跟在命令行上 使用'-oformat BFDNAME'是完全等效的. 如果兩個都使用了, 命令行選項優先.
你可在使用`OUTPUT_formAT'時帶有三個參數以使用不同的基于'-EB'和'-EL'的命令行選項的格式.
如果'-EB'和'-EL'都沒有使用, 那輸出格式會是第一個參數DEFAULT, 如果使用了'-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'相似. 如果使用了'TARGET'命令但`OUTPUT_formAT'沒有指定, 最后的 'TARGET'命令也被用來設置輸出文件的格式.
??
其它的連接腳本命令. ----------------------------
還有一些其它的連接腳本命令.
`ASSERT(EXP, MESSAGE)' 確保EXP不等于零,如果等于零, 連接器就會返回一個錯誤碼退出,并打印出MESSAGE.
`EXTERN(SYMBOL SYMBOL ...)' 強制SYMBOL作為一個無定義的符號輸入到輸出文件中去. 這樣做了,可能會引發從標準庫中連接一些 節外的庫. 你可以為每一個EXTERN'列出幾個符號, 而且你可以多次使用'EXTERN'. 這個命令跟'-u' 命令行選項具有相同的效果.
`FORCE_COMMON_ALLOCATION' 這個命令跟命令行選項'-d'具有相同的效果: 就算指定了一個可重定位的輸出文件('-r'),也讓'ld' 為普通符號分配空間.
`INHIBIT_COMMON_ALLOCATION' 這個命令跟命令行選項`--no-define-common'具有相同的效果: 就算是一個不可重位輸出文件, 也讓 'ld'忽略為普通符號分配的空間.
`NOCROSSREFS(SECTION SECTION ...)' 這個命令在遇到在某些特定的節之間引用的時候會產生一條錯誤信息.
在某些特定的程序中, 特別是在使用覆蓋技術的嵌入式系統中, 當一個節被載入內存時,另外一個節 就不會在內存中. 任何在兩個節之間的直接引用都會是一個錯誤. 比如, 如果節1中的代碼調用了另 一個節中的一個函數,這就會產生一個錯誤.
`NOCROSSREFS'命令帶有一個輸出節名字的列表. 如果'ld'遇到任何在這些節之間的交叉引用, 它就 會報告一個錯誤,并返回一個非零退出碼. 注意, `NOCROSSREFS'命令使用輸出節名,而不是輸入節名.
`OUTPUT_ARCH(BFDARCH)' 指定一個特定的輸出機器架構. 這個參數是BFD庫中使用的一個名字. 你可以通過使用帶有'-f'選項 的'objdump'程序來查看一個目標文件的架構.
為符號賦值. ===========================
你可以在一個連接腳本中為一個符號賦一個值. 這會把一個符號定義為一個全局符號.
簡單的賦值. ------------------
你可以使用所有的C賦值符號為一個符號賦值.
`SYMBOL = EXPRESSION ;' `SYMBOL += EXPRESSION ;' `SYMBOL -= EXPRESSION ;' `SYMBOL *= EXPRESSION ;' `SYMBOL /= EXPRESSION ;' `SYMBOL <<= EXPRESSION ;' `SYMBOL >>= EXPRESSION ;' `SYMBOL &= EXPRESSION ;' `SYMBOL |= EXPRESSION ;'
第一個情況會把SYMBOL定義為值EXPRESSION. 其它情況下, SYMBOL必須是已經定義了的, 而值會作出相應的調 整.
特殊符號名'.'表示定位計數器. 你只可以在'SECTIONS'命令中使用它.
EXPRESSION后面的分號是必須的.
表達式下面會定義.
你在寫表達式賦值的時候,可以把它們作為單獨的部分,也可以作為'SECTIONS'命令中的一個語句,或者作為 'SECTIONS'命令中輸出節描述的一個部分.
符號所在的節會被設置成表達式所在的節.
下面是一個關于在三處地方使用符號賦值的例子:
? ? floating_point = 0; ? ? SECTIONS ? ? { ? ?? ?.text : ? ?? ???{ ? ?? ?? ? *(.text) ? ?? ?? ? _etext = .; ? ?? ???} ? ?? ?_bdata = (. + 3) & ~ 3; ? ?? ?.data : { *(.data) } ? ? }
在這個例子中, 符號`floating_point'被定義為零. 符號'-etext'會被定義為前面一個'.text'節尾部的地址. 而符號'_bdata'會被定義為'.text'輸出節后面的一個向上對齊到4字節邊界的一個地址值.
PROVIDE -------
在某些情況下, 一個符號被引用到的時候只在連接腳本中定義,而不在任何一個被連接進來的目標文件中定 義. 這種做法是比較明智的. 比如, 傳統的連接器定義了一個符號'etext'. 但是, ANSI C需要用戶能夠把 'etext'作為一個函數使用而不會產生錯誤. 'PROVIDE'關鍵字可以被用來定義一個符號, 比如'etext', 這個 定義只在它被引用到的時候有效,而在它被定義的時候無效.語法是 `PROVIDE(SYMBOL = EXPRESSION)'.
下面是一個關于使用'PROVIDE'定義'etext'的例子:
? ? SECTIONS ? ? { ? ?? ?.text : ? ?? ???{ ? ?? ?? ? *(.text) ? ?? ?? ? _etext = .; ? ?? ?? ? PROVIDE(etext = .); ? ?? ???} ? ? }
在這個例子中, 如果程序定義了一個'_etext'(帶有一個前導下劃線), 連接器會給出一個重定義錯誤. 如果, 程序定義了一個'etext'(不帶前導下劃線), 連接器會默認使用程序中的定義. 如果程序引用了'etext'但不 定義它, 連接器會使用連接腳本中的定義.
SECTIONS命令 ================
'SECTIONS'命令告訴連接器如何把輸入節映射到輸出節, 并如何把輸出節放入到內存中.
'SECTIONS'命令的格式如下:
? ? SECTIONS ? ? { ? ?? ?SECTIONS-COMMAND ? ?? ?SECTIONS-COMMAND ? ?? ?... ? ? }
每一個SECTIONS-COMMAND可能是如下的一種:
??* 一個'ENTRY'命令.
??* 一個符號賦值.
??* 一個輸出節描述.
??* 一個重疊描述.
'ENTRY'命令和符號賦值在'SECTIONS'命令中是允許的, 這是為了方便在這些命令中使用定位計數器. 這也可 以讓連接腳本更容易理解, 因為你可以在更有意義的地方使用這些命令來控制輸出文件的布局.
輸出節描述和重疊描述在下面描述.
如果你在連接腳本中不使用'SECTIONS'命令, 連接器會按在輸入文件中遇到的節的順序把每一個輸入節放到同 名的輸出節中. 如果所有的輸入節都在第一個文件中存在,那輸出文件中的節的順序會匹配第一個輸入文件中 的節的順序. 第一個節會在地址零處.
輸出節描述 --------------------------
一個完整的輸出節的描述應該是這個樣子的:
? ? SECTION [ADDRESS] [(TYPE)] : [AT(LMA)] ? ?? ?{ ? ?? ???OUTPUT-SECTION-COMMAND ? ?? ???OUTPUT-SECTION-COMMAND ? ?? ???... ? ?? ?} [>REGION] [AT>LMA_REGION] [:PHDR :PHDR ...] [=FILLEXP]
大多數輸出節不使用這里的可選節屬性.
SECTION邊上的空格是必須的, 所以節名是明確的. 冒號跟花括號也是必須的. 斷行和其他的空格是可選的.
每一個OUTPUT-SECTION-COMMAND可能是如下的情況:
??* 一個符號賦值.
??* 一個輸入節描述.
??* 直接包含的數據值.
??* 一個特定的輸出節關鍵字. ?? 輸出節名. -------------------
輸出節的名字是SECTION. SECTION必須滿足你的輸出格式的約束. 在一個只支持限制數量的節的格式中,比如 'a.out',這個名字必須是格式支持的節名中的一個(比如, 'a.out'只允許'.text', '.data'或'.bss').如果 輸出格式支持任意數量的節, 但是只支持數字,而沒有名字(就像Oasys中的情況), 名字應當以一個雙引號中的 數值串的形式提供.一個節名可以由任意數量的字符組成,但是一個含有任意非常用字符(比如逗號)的字句必須 用雙引號引起來.
輸出節描述 --------------------------
ADDRESS是關于輸出節中VMS的一個表達式. 如果你不提供ADDRESS, 連接器會基于REGION(如果存在)設置它,或 者基于定位計數器的當前值.
如果你提供了ADDRESS, 那輸出節的地址會被精確地設為這個值. 如果你既不提供ADDRESS也不提供REGION, 那 輸出節的地址會被設為當前的定位計數器向上對齊到輸出節需要的對齊邊界的值. 輸出節的對齊要求是所有輸 入節中含有的對齊要求中最嚴格的一個.
比如: ? ? .text . : { *(.text) }
和 ? ? .text : { *(.text) }
有細微的不同. 第一個會把'.text'輸出節的地址設為當前定位計數器的值. 第二個會把它設為定位計數器的 當前值向上對齊到'.text'輸入節中對齊要求最嚴格的一個邊界.
ADDRESS可以是任意表達式; 比如,如果你需要把節對齊對0x10字節邊界,這樣就可以讓低四字節的節地址值為 零, 你可以這樣做:
? ? .text ALIGN(0x10) : { *(.text) }
這個語句可以正常工作,因為'ALIGN'返回當前的定位計數器,并向上對齊到指定的值.
指定一個節的地址會改變定位計數器的值.
輸入節描述 -------------------------
最常用的輸出節命令是輸入節描述.
輸入節描述是最基本的連接腳本操作. 你使用輸出節來告訴連接器在內存中如何布局你的程序. 你使用輸入節 來告訴連接器如何把輸入文件映射到你的內存中.
輸入節基礎 ---------------------------
一個輸入節描述由一個文件名后跟有可選的括號中的節名列表組成.
文件名和節名可以通配符形式出現, 這個我們以后再介紹.
最常用的輸入節描述是包含在輸出節中的所有具有特定名字的輸入節. 比如, 包含所有輸入'.text'節,你可以 這樣寫:
? ? *(.text)
這里,'*'是一個通配符,匹配所有的文件名. 為把一部分文件排除在匹配的名字通配符之外, EXCLUDE_FILE可 以用來匹配所有的除了在EXCLUDE_FILE列表中指定的文件.比如:
? ? (*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors))
會讓除了`crtend.o'文件和`otherfile.o'文件之外的所有的文件中的所有的.ctors節被包含進來.
有兩種方法包含多于一個的節:
? ? *(.text .rdata) ? ? *(.text) *(.rdata)
上面兩句的區別在于'.text'和'.rdata'輸入節的輸出節中出現的順序不同. 在第一個例子中, 兩種節會交替 出現,并以連接器的輸入順序排布. 在第二個例子中,所有的'.text'輸入節會先出現,然后是所有的'.rdata'節.
你可以指定文件名,以從一個特定的文件中包含節. 如果一個或多個你的文件含有特殊的數據在內存中需要特 殊的定位,你可以這樣做. 比如:
? ? data.o(.data)
如果你使用一個不帶有節列表的文件名, 那輸入文件中的所有的節會被包含到輸出節中. 通常不會這樣做, 但 是在某些場合下這個可能非常有用. 比如:
? ? data.o ? ? 當你使用一個不含有任何通配符的文件名時, 連接器首先會查看你是否在連接命令行上指定了文件名或者在 'INPUT'命令中. 如果你沒有, 連接器會試圖把這個文件作為一個輸入文件打開, 就像它在命令行上出現一樣. 注意這跟'INPUT'命令不一樣, 因為連接器會在檔案搜索路徑中搜索文件.
輸入節通配符 ---------------------------------
在一個輸入節描述中, 文件名或者節名,或者兩者同時都可以是通配符形式.
文件名通配符'*'在很多例子中都可以看到,這是一個簡單的文件名通配符形式.
通配符形式跟Unix Shell中使用的一樣.
`*' 匹配任意數量的字符.
`?' 匹配單個字符.
`[CHARS]' 匹配CHARS中的任意單個字符; 字符'-'可以被用來指定字符的方訌, 比如[a-z]匹配任意小字字符.
`\' 轉義其后的字符.
當一個文件名跟一個通配符匹配時, 通配符字符不會匹配一個'/'字符(在UNIX系統中用來分隔目錄名), 一個 含有單個'*'字符的形式是個例外; 它總是匹配任意文件名, 不管它是否含有'/'. 在一個節名中, 通配符字 符會匹配'/'字符.
文件名通配符只匹配那些在命令行或在'INPUT'命令上顯式指定的文件. 連接器不會通過搜索目錄來展開通配 符.
如果一個文件名匹配多于一個通配符, 或者如果一個文件名顯式出現同時又匹配了一個通配符, 連接器會使用 第一次匹配到的連接腳本. 比如, 下面的輸入節描述序列很可能就是錯誤的,因為'data.o'規則沒有被使用:
? ? .data : { *(.data) } ? ? .data1 : { data.o(.data) }
通常, 連接器會把匹配通配符的文件和節按在連接中被看到的順序放置. 你可以通過'SORT'關鍵字改變它, 它 出現在括號中的通配符之前(比如, 'SORT(.text*)'). 當'SORT'關鍵字被使用時, 連接器會在把文件和節放到 輸出文件中之前按名字順序重新排列它們.
如果你對于輸入節被放置到哪里去了感到很困惑, 那可以使用'-M'連接選項來產生一個位圖文件. 位圖文件會 精確顯示輸入節是如何被映射到輸出節中的.
這個例子顯示了通配符是如何被用來區分文件的. 這個連接腳本指示連接器把所有的'.text'節放到'.text'中, 把所有的'.bss'節放到'.bss'. 連接器會把所有的來自文件名以一個大寫字母開始的文件中的'.data'節放進'.DATA'節中; 對于所有其他文件, 連接器會把'.data'節放進'.data'節中.
? ? SECTIONS { ? ?? ?.text : { *(.text) } ? ?? ?.DATA : { [A-Z]*(.data) } ? ?? ?.data : { *(.data) } ? ?? ?.bss : { *(.bss) } ? ? }
輸入節中的普通符號. -----------------------------------
對于普通符號,需要一個特殊的標識, 因為在很多目標格式中, 普通符號沒有一個特定的輸入節. 連接器會把 普通符號處理成好像它們在一個叫做'COMMON'的節中.
你可能像使用帶有其他輸入節的文件名一樣使用帶有'COMMON'節的文件名。你可以通過這個把來自一個特定輸 入文件的普通符號放入一個節中,同時把來自其它輸入文件的普通符號放入另一個節中。
在大多數情況下,輸入文件中的普通符號會被放到輸出文件的'.bss'節中。比如:
? ? .bss { *(.bss) *(COMMON) }
有些目標文件格式具有多于一個的普通符號。比如,MIPS ELF目標文件格式區分標準普通符號和小普通符號。 在這種情況下,連接器會為其他類型的普通符號使用一個不同的特殊節名。 在MIPS ELF的情況中, 連接器 為標準普通符號使用'COMMON',并且為小普通符號使用'.common'。這就允許你把不同類型的普通符號映射到 內存的不同位置。
在一些老的連接腳本上,你有時會看到'[COMMON]'。這個符號現在已經過時了, 它等效于'*(COMMON)'。
輸入節和垃圾收集 ---------------------------------------
當連接時垃圾收集正在使用中時('--gc-sections'),這在標識那些不應該被排除在外的節時非常有用。這 是通過在輸入節的通配符入口外面加上'KEEP()'實現的,比如'KEEP(*(.init))'或者'KEEP(SORT(*)(.sorts)) '。
輸入節示例 ---------------------
接下來的例子是一個完整的連接腳本。它告訴連接器去讀取文件'all.o'中的所有節,并把它們放到輸出節 '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) ? ?? ???} ? ? } ? ? 輸出節數據 -------------------
你可以通過使用輸出節命令'BYTE','SHORT','LONG','QUAD',或者'SQUAD'在輸出節中顯式包含幾個字節的數據 每一個關鍵字后面都跟上一個圓括號中的要存入的值。表達式的值被存在當前的定位計數器的值處。
‘BYTE’,‘SHORT’,‘LONG’‘QUAD’命令分別存儲一個,兩個,四個,八個字節。存入字節后,定位計 數器的值加上被存入的字節數。
比如,下面的命令會存入一字節的內容1,后面跟上四字節,其內容是符號'addr'的值。
? ? BYTE(1) ? ? LONG(addr)
當使用64位系統時,‘QUAD’和‘SQUAD’是相同的;它們都會存儲8字節,或者說是64位的值。而如果軟硬件 系統都是32位的,一個表達式就會被作為32位計算。在這種情況下,‘QUAD’存儲一個32位值,并把它零擴展 到64位, 而‘SQUAD’會把32位值符號擴展到64位。
如果輸出文件的目標文件格式有一個顯式的endianness,它在正常的情況下,值就會被以這種endianness存儲 當一個目標文件格式沒有一個顯式的endianness時, 值就會被以第一個輸入目標文件的endianness存儲。
注意, 這些命令只在一個節描述內部才有效,而不是在它們之間, 所以,下面的代碼會使連接器產生一個錯 誤信息:
? ? SECTIONS { .text : { *(.text) } LONG(1) .data : { *(.data) } }
而這個才是有效的:
? ? SECTIONS { .text : { *(.text) ; LONG(1) } .data : { *(.data) } }
你可能使用‘FILL’命令來為當前節設置填充樣式。它后面跟有一個括號中的表達式。任何未指定的節內內存 區域(比如,因為輸入節的對齊要求而造成的裂縫)會以這個表達式的值進行填充。一個'FILL'語句會覆蓋到 它本身在節定義中出現的位置后面的所有內存區域;通過引入多個‘FILL’語句,你可以在輸出節的不同位置 擁有不同的填充樣式。
這個例子顯示如何在未被指定的內存區域填充'0x90':
? ? FILL(0x90909090)
‘FILL’命令跟輸出節的‘=FILLEXP’屬性相似,但它只影響到節內跟在‘FILL’命令后面的部分,而不是 整個節。如果兩個都用到了,那‘FILL’命令優先。
輸出節關鍵字 -----------------------
有兩個關鍵字作為輸出節命令的形式出現。
`CREATE_OBJECT_SYMBOLS' 這個命令告訴連接器為每一個輸入文件創建一個符號。而符號的名字正好就是相關輸入文件的名字。 而每一個符號的節就是`CREATE_OBJECT_SYMBOLS'命令出現的那個節。
這個命令一直是a.out目標文件格式特有的。 它一般不為其它的目標文件格式所使用。
`CONSTRUCTORS' 當使用a.out目標文件格式進行連接的時候, 連接器使用一組不常用的結構以支持C++的全局構造函 數和析構函數。當連接不支持專有節的目標文件格式時, 比如ECOFF和XCOFF,連接器會自動辯識C++ 全局構造函數和析構函數的名字。對于這些目標文件格式,‘CONSTRUCTORS’命令告訴連接器把構造 函數信息放到‘CONSTRUCTORS’命令出現的那個輸出節中。對于其它目標文件格式,‘CONSTRUCTORS’ 命令被忽略。
符號`__CTOR_LIST__'標識全局構造函數的開始,而符號`__DTOR_LIST'標識結束。這個列表的第一個 WORD是入口的數量,緊跟在后面的是每一個構造函數和析構函數的地址,再然后是一個零WORD。編譯 器必須安排如何實際運行代碼。對于這些目標文件格式,GNU C++通常從一個`__main'子程序中調用 構造函數,而對`__main'的調用自動被插入到`main'的啟動代碼中。GNU C++通常使用'atexit'運行 析構函數,或者直接從函數'exit'中運行。
對于像‘COFF’或‘ELF’這樣支持專有節名的目標文件格式,GNU C++通常會把全局構造函數與析構 函數的地址值放到'.ctors'和'.dtors'節中。把下面的代碼序列放到你的連接腳本中去,這樣會構建 出GNU 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__ = .;
如果你正使用GNU C++支持來進行優先初始化,那它提供一些可以控制全局構造函數運行順序的功能, 你必須在連接時給構造函數排好序以保證它們以正確的順序被執行。當使用'CONSTRUCTORS'命令時, 替代為`SORT(CONSTRUCTORS)'。當使用'.ctors'和'dtors'節時,使用`*(SORT(.ctors))'和 `*(SORT(.dtors))' 而不是`*(.ctors)'和`*(.dtors)'。
通常,編譯器和連接器會自動處理這些事情,并且你不必親自關心這些事情。但是,當你正在使用 C++,并自己編寫連接腳本時,你可能就要考慮這些事情了。
輸出節的丟棄。 -------------------------
連接器不會創建那些不含有任何內容的輸出節。這是為了引用那些可能出現或不出現在任何輸入文件中的輸入 節時方便。比如:
? ? .foo { *(.foo) }
如果至少在一個輸入文件中有'.foo'節,它才會在輸出文件中創建一個'.foo'節
如果你使用了其它的而不是一個輸入節描述作為一個輸出節命令,比如一個符號賦值,那這個輸出節總是被 創建,即使沒有匹配的輸入節也會被創建。
一個特殊的輸出節名`/DISCARD/'可以被用來丟棄輸入節。任何被分配到名為`/DISCARD/'的輸出節中的輸入 節不包含在輸出文件中。
輸出節屬性 -------------------------
上面,我們已經展示了一個完整的輸出節描述,看下去就象這樣:
? ? SECTION [ADDRESS] [(TYPE)] : [AT(LMA)] ? ?? ?{ ? ?? ???OUTPUT-SECTION-COMMAND ? ?? ???OUTPUT-SECTION-COMMAND ? ?? ???... ? ?? ?} [>REGION] [AT>LMA_REGION] [:PHDR :PHDR ...] [=FILLEXP]
我們已經介紹了SECTION, ADDRESS, 和OUTPUT-SECTION-COMMAND. 在這一節中,我們將介紹余下的節屬性。
輸出節類型 ...................
每一個輸出節可以有一個類型。類型是一個放在括號中的關鍵字,已定義的類型如下所示:
`NOLOAD' 這個節應當被標式詎不可載入,所以當程序運行時,它不會被載入到內存中。
`DSECT' `COPY' `INFO' `OVERLAY' 支持這些類型名只是為了向下兼容,它們很少使用。它們都具有相同的效果:這個節應當被標式詎不 可分配,所以當程序運行時,沒有內存為這個節分配。
連接器通常基于映射到輸出節的輸入節來設置輸出節的屬性。你可以通過使用節類型來重設這個屬性, 比如,在下面的腳本例子中,‘ROM’節被定址在內存地址零處,并且在程序運行時不需要被載入。 ‘ROM’節的內容會正常出現在連接輸出文件中。
? ? SECTIONS { ? ?? ?ROM 0 (NOLOAD) : { ... } ? ?? ?... ? ? }
輸出節LMA ..................
每一個節有一個虛地址(VMA)和一個載入地址(LMA);出現在輸出節描述中的地址表達式設置VMS
連接器通常把LMA跟VMA設成相等。你可以通過使用‘AT’關鍵字改變這個。跟在關鍵字‘AT’后面的表達式 LMA指定節的載入地址。或者,通過`AT>LMA_REGION'表達式, 你可以為節的載入地址指定一個內存區域。
這個特性是為了便于建立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;
輸出節區域 .....................
你可以通過使用`>REGION'把一個節賦給前面已經定義的一個內存區域。
這里有一個簡單的例子:
? ? MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 } ? ? SECTIONS { ROM : { *(.text) } >rom }
輸出節Phdr ...................
你可以通過使用`:PHDR'把一個節賦給前面已定義的一個程序段。如果一個節被賦給一個或多個段,那后來分 配的節都會被賦給這些段,除非它們顯式使用了':PHDR'修飾符。你可以使用':NONE'來告訴連接器不要把節 放到任何一個段中。
這兒有一個簡單的例子:
? ? PHDRS { text PT_LOAD ; } ? ? SECTIONS { .text : { *(.text) } :text }
輸出段填充 ...................
你可以通過使用'=FILLEXP'為整個節設置填充樣式。FILLEXP是一個表達式。任何沒有指定的輸出段內的內存 區域(比如,因為輸入段的對齊要求而產生的裂縫)會被填入這個值。如果填充表達式是一個簡單的十六進制 值,比如,一個以'0x'開始的十六進制數字組成的字符串,并且尾部不是'k'或'M',那一個任意的十六進制數 字長序列可以被用來指定填充樣式;前導零也變為樣式的一部分。對于所有其他的情況,包含一個附加的括號 或一元操作符'+',那填充樣式是表達式的最低四字節的值。在所有的情況下,數值是big-endian.
你還可以通過在輸出節命令中使用'FILL'命令來改變填充值。
這里是一個簡單的例子: ? ? SECTIONS { .text : { *(.text) } =0x90909090 }
覆蓋描述 -------------------
一個覆蓋描述提供一個簡單的描述辦法,以描述那些要被作為一個單獨內存映像的一部分載入內存,但是卻要 在同一內存地址運行的節。在運行時,一些覆蓋管理機制會把要被覆蓋的節按需要拷入或拷出運行時內存地址, 并且多半是通過簡單地處理內存位。 這個方法可能非常有用,比如在一個特定的內存區域比另一個快時。
覆蓋是通過‘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’結構中的節定義跟通常的‘SECTIONS’結構中的節定義是完全相同的,除了一點,就是在‘OVERLAY’ 中沒有地址跟內存區域的定義。
節都被定義為同一個開始地址。所有節的載入地址都被排布,使它們在內存中從整個'OVERLAY'的載入地址開 始都是連續的(就像普通的節定義,載入地址是可選的,缺省的就是開始地址;開始地址也是可選的,缺省的 是當前的定位計數器的值。)
如果使用了關鍵字`NOCROSSREFS', 并且在節之間存在引用,連接器就會報告一個錯誤。因為節都運行在同一 個地址上,所以一個節直接引用另一個節中的內容是錯誤的。
對于'OVERLAY'中的每一個節,連接器自動定義兩個符號。符號`__load_start_SECNAME'被定義為節的開始載 入地址。符號`__load_stop_SECNAME'被定義為節的最后載入地址。SECNAME中的不符合C規定的任何字符都將 被刪除。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'.
拷貝'.text1'到覆蓋區域的C代碼看上去可能會像下面這樣:
? ?? ?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) } ? ?? ?__load_start_text0 = LOADADDR (.text0); ? ?? ?__load_stop_text0 = LOADADDR (.text0) + SIZEOF (.text0); ? ?? ?.text1 0x1000 : AT (0x4000 + SIZEOF (.text0)) { o2/*.o(.text) } ? ?? ?__load_start_text1 = LOADADDR (.text1); ? ?? ?__load_stop_text1 = LOADADDR (.text1) + SIZEOF (.text1); ? ?? ?. = 0x1000 + MAX (SIZEOF (.text0), SIZEOF (.text1)); |
|