鏈接器的核心工作就是符號表解析和重定位,鏈接命令文件則使得編程者可以給鏈接器提供必要的指導和輔助信息。多數時候,由於集成開發環境的存在,開發者無需了解鏈接命令文件的編寫,使用默認配置即可。但若需要對計算機系統存儲空間實行更精細化的管理,讀懂鏈接命令文件並能稍作修改則顯得很有必要。
段(section)
編譯器生成可重分配地址的代碼塊和數據塊,這些塊被叫做“段”。通過段名對代碼塊和數據塊的標識,連接器就能在鏈接的時候根據鏈接規則(默認規則或鏈接命令文件制定)將代碼塊和數據塊分配到指定的存儲空間中。
圖1 將段組合到可執行文件中
匯編器有5個偽指令支持標識匯編語言程序各個部分應歸屬的段:.text .data .sect .bss .usect。編程者也可以創建任一種段的子段,得以更精細地控制存儲器區域。
初始化段:.text .data .sect創建初始化段。
-
.text:代碼區間。
-
.data:已初始化的全局和靜態變量。
-
.sect:創建類似於.text .data 的命名段,同時用於創建子段。
未初始化段:.bss .usect指令創建未初始化段。
-
.bss:未初始化的全局和靜態變量,以及被初始化為0的全局和靜態變量。
-
.usect:創建類似於.bss的命名段,同時用於創建子段。
含有原始數據的段,歸類為初始化的,意味着目標文件含有該段的存儲器實際內容映像;默認情況下,.bss段和.usect偽指令定義的段沒有原始數據,它們在存儲器映射圖里占據空間,但沒有實際內容。每次使用bss和usect偽指指令時,匯編器就在.bss或該命名段內預留增補的空間。在目標文件里,一個未初始化段有正常的段頭,也可以含有定義在其內的符號,但沒有存於段內的存儲器映像。
命名段是用戶創建的,可以像.text .data .bss段一樣使用它們。
子段是較大段內一些比較小的段,子段使用戶更細致地控制存儲器空間,一個子段可以單獨分配地址,也可以與同一基段的其它子段分配在一起。子段用基段名加冒號加子段名來標識,對子段命名的語法如下:
symbol .usect "section name:subsection name",size in bytes [,alignment [,bank offset]]
.sect "section name:subsection name"
-mo選項使得編譯器把一個文件中的每一個函數放入它自己的子段中。這樣,只有被應用程序調用的函數,才被連接到最后的可執行函數中,這可以導致整個代碼的尺寸減小。但是,如果一個文件中幾乎所有的函數都被引用,使用-mo編譯器選項,也能導致整個代碼尺寸的增加。
加載時(load-time)和運行時(run-time)
“加載時”和“運行時”是兩個容易讓人產生迷惑的概念,理解它們需要了解代碼在硬件層面的詳細被執行過程。
在掉電時,包含運行代碼的可執行文件一般都存放於非易失的ROM或磁盤中,但由於這些存儲器的訪問速率受到限制,在實際上電運行時,還需要專門的一段加載代碼(或稱加載器),將需要的代碼和數據段復制到主存中,然后通過跳轉到程序的第一條指令或入口點,來運行該代碼。這個將程序復制到內存,並開始運行的過程叫做加載。
加載地址確定了加載器把段的原始數據放置的位置,任何對這個段的引用涉及的是它的運行地址,運行時必須把這個段從加載地址復制到運行地址。這也就能理解為什么計算機術語上把諸如stdio、string等庫文件稱作運行時庫(run-time lib),因為它們在運行時才被加載到和調用代碼一起運行。
鏈接命令文件語法
連接器命令文件是ASCII碼文件,包括一個或多個下述信息:
-
輸入文件。如目標文件、文檔庫、其它命令文件(如果一個命令文件調用另一個命令文件作為輸入,這個語句必須是調用文件最后的語句,連接器不從被調用的命令文件返回)。
-
連接器選項。它能以與命令行同樣的方式應用於命令文件。
-
MEMORY和SECTION連接器偽指令(只能在命令文件里使用這些偽指令,不能在命令行上使用它們)。
-
賦值語句。它定義全局符號並給它們賦值。
下列名字作為連接器偽指令的關鍵字被保留,在命令文件里不要使用它們作為符號或段名:
一個完整的簡單鏈接命令文件示例如下:
a.obj b.obj c.obj /* Input filenames */
--output_file=prog.out /* Options */
--map_file=prog.map
MEMORY /* MEMORY directive */
{
FAST_MEM: origin = 0x0100 length = 0x0100
SLOW_MEM: origin = 0x7000 length = 0x1000
}
SECTIONS /* SECTIONS directive */
{
.text: > SLOW_MEM
.data: > SLOW_MEM
.bss: > FAST_MEM
}
3.1 MEMORY偽指令
MEMORY定義一個目標系統的存儲器映像圖。用戶給存儲器各部分命名,制定他們的起始地址和長度。它的通用語法如下:
MEMORY
{
name 1 [( attr )] : origin = expression , length = expression [, fill = constant]
..
name n [( attr )] : origin = expression , length = expression [, fill = constant]
}
其中name為一段存儲區的名字;attr定義該存儲區的屬性,如可讀、可寫、可執行、可初始化;origin為該存儲區的起始地址;length為存儲區長度,以字節為單位;fill選項指定該存儲區的空閑區域用什么constant來填充。一個使用例子如下:
MEMORY
{
FAST_MEM (RW) : o = 0x00000020, l = 0x00001000, f = 0xFFFFFFFF
}
由MEMORY偽指令定義的存儲器是已配置的,沒有用MEMORY偽指令顯式地計入的存儲器是未配置的。連接器不把程序任何一段放到未配置的存儲器里。
如果不使用MEMORY偽指令,連接器則使用一個默認的基於處理器結構的存儲器模型。這個模型假定系統內全部地址空間存在且可使用。
3.2 SECTIONS偽指令
SECTIONS告訴連接器怎樣把輸入段組合成輸出段,以及把輸出段放在存儲器的什么位置。
SECTIONS
{
name : [property [, property] [, property] . . . ]
name : [property [, property] [, property] . . . ]
name : [property [, property] [, property] . . . ]
}
其中name為輸出段名,SECTIONS偽指令的作用就是將輸入段(基段或子段)重新組合到一個輸出段(基段或子段),並指定該輸出段的加載地址、運行地址、填充值等屬性。
property則是可選的命令選項,這些命令選項有:
鏈接器給每個輸出段在目標存儲器內分配兩個地址:加載時地址和運行時地址。一般情況下它們是同一個,可以認為每個段僅有單一的地址。如果加載和運行的地址是分離的,跟隨關鍵字load后的所有參數,應用於加載定位;跟隨關鍵字run后的所有參數,應用於運行定位。
未初始化段不加載,因此有意義的僅僅是運行地址。如果對未初始化段的加載地址和運行地址二者都指定,連接器發出警告並忽略加載地址。如果只指定一個地址,連接器把它作為運行地址對待,而不管稱它是加載或是運行。
用戶可以為輸出段提供一個指定的起始地址,但這種地址綁定與邊界對齊(alignment)和指定存儲器(named memory)不兼容,如果使用了邊界對齊(alignment)和指定存儲器(named memory),將不能綁定段地址。如果試圖這樣做,連接器將發出錯誤信息。
輸出段能以兩種方法組成:
-
作為SECTIONS偽指令定義的結果;
-
把SECTIONS偽指令未定義的同名輸入段組合到一個輸出段。如果對子段沒有顯式地指定,子段將被組合到具有同一基段名的段內。
連接器允許在SECTIONS偽指令內任意嵌套GROUP和UNION語句。
3.3 SECTIONS偽指令內的UNION語句
UNION語句嵌套在SECTIONS指令內使用,它提供一種方法,把幾個段定位到同一運行地址。UNION占據與它最大成員一樣大的空間。UNION的成員保持為獨立段,它們只是簡單地作為一個單位定位在一起。
未初始化段不加載,不需要加載地址。
UNION: run = FAST_MEM
{
.bss:part1: { file1.obj(.bss) }
.bss:part2: { file2.obj(.bss) }
}
但如果初始化段是UNION的成員,它的加載定位必須分別指定,也即UNION共享地址只是對於運行地址而言,加載地址不能共享。
UNION run = FAST_MEM
{
.text:part1: load = SLOW_MEM, { file1.obj(.text) }
.text:part2: load = SLOW_MEM, { file2.obj(.text) }
}
3.4 SECTIONS偽指令內的GROUP 語句
GROUP語句嵌套在SECTIONS指令內使用,用於強制幾個輸出段連續定位。例如下面的語句,使用GROUP強制連接器將.data段和term_rec段相鄰定位,其中.data定位到地址0x1000,term_rec緊隨其后:
SECTIONS
{
.text /* Normal output section */
.bss /* Normal output section */
GROUP 0x00001000 : /* Specify a group of sections */
{
.data /* First section in the group */
term_rec /* Allocated immediately after .data */
}
}
3.5 原點“.”符號
一個用原點“.”標記的特殊符號,代表在地址分配期間的段程序計數器(SPC)的當前值,SPC保持跟蹤段內當前地址。符號“.”指的是段的當前運行地址,而不是當前加載地址。
3.6 一個SECTIONS偽指令分配存儲的例子
/**************************************************/
/* Sample command file with SECTIONS directive */
/**************************************************/
file1.obj file2.obj /* Input files */
--output_file=prog.out /* Options */
SECTIONS
{
.text: load = EXT_MEM, run = 0x00000800
.const: load = FAST_MEM
.bss: load = SLOW_MEM
.vectors: load = 0x00000000
{
t1.obj(.intvec1)
t2.obj(.intvec2)
endvec = .;
}
.data:alpha: align = 16
.data:beta: align = 16
}
鏈接器並不是一定需要連接器偽指令,如果沒有使用它們,連接器將使用目標處理器默認的分配代碼方案。
內存區域不夠時的解決辦法
在鏈接過程中經常會出現由於存儲區空間不足導致段分配失敗的提示,同時連接器會給出未使用空間的大小和需要空間的大小。這一現象可表現出兩種情況:一是未使用 空間>需要空間;二是需要空間>未使用 空間。
第二種情況其實很好理解,第一種情況是怎么回事呢?實際上,段的空間的分配是並不是我們想象中的連續的一個緊挨一個,由於數據對齊的需要以及內存頁的適配,都會在內存中產生一些空隙(hole),使得實際所需要的內存空間超過了根據變量大小計算出來的理論值。這樣做的目的是為了優化數據頁(DP)寄存器的加載,達到減小代碼尺寸和優化程序性能的目的。
那么,一旦出現存儲區空間不足的提示,我們該如何重新調整段的分配來解決這個問題呢?於一個單一的段而言,有三個辦法可以嘗試:
1. 查看編譯后生成的.map文件,其中顯示了每一個存儲區的空間使用情況,另尋找一個空間大小足夠,且內存屬性相似的存儲區,將該段分配到該區;
2. 標注多個備選存儲區。操作符“| ”用來為段指定多個存儲器區域,如果輸出段不能成功地分配到任一個所指定的存儲器區域,連接器發出一個錯誤信息。
.text : > FLASHA | FLASHC | FLASHD
這個例子中連接器將首先嘗試將.text段分配給FLASHA,如果不成功,則依次嘗試FLASHC和FLASHD,直到分配成功,否則報錯誤提示。
3. 將段分割分配到多個存儲區。操作符“>> ”標明輸出段能被分裂裝到指定的存儲區域內,前提是幾個內存區域的總長度要滿足要求。
.text : >> FLASHA | FLASHC | FLASHD
這個例子中,如果.text段不能完整地分配到FLASHA,則連接器會將剩余的部分繼續分配到FLASHC,甚至分配到FLASHD中。
參考文獻
【1】田黎育,何佩琨,朱夢宇. TMS320C6000系列DSP編程工具與指南[M].北京:清華大學出版社 2006.
【2】TMS320C6000 Assembly Language Tools v7.4--SPRU186W,2012.
【3】Beginner's Guide to Linkers.
【4】Linkers and Loaders,John Levine.
·END·
歡迎來我的微信公眾號做客:信號君
專注於信號處理知識、高性能計算、現代處理器&計算機體系
技術成長 | 讀書筆記 | 認知升級
幸會~