天天看點

讀懂CCS連結指令檔案(.cmd)

連結器的核心工作就是符号表解析和重定位,連結指令檔案則使得程式設計者可以給連結器提供必要的指導和輔助資訊。多數時候,由于內建開發環境的存在,開發者無需了解連結指令檔案的編寫,使用預設配置即可。但若需要對計算機系統存儲空間實行更精細化的管理,讀懂連結指令檔案并能稍作修改則顯得很有必要。

段(section)

編譯器生成可重配置設定位址的代碼塊和資料塊,這些塊被叫做“段”。通過段名對代碼塊和資料塊的辨別,連接配接器就能在連結的時候根據連結規則(預設規則或連結指令檔案制定)将代碼塊和資料塊配置設定到指定的存儲空間中。

讀懂CCS連結指令檔案(.cmd)

圖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碼檔案,包括一個或多個下述資訊:

  1. 輸入檔案。如目标檔案、文檔庫、其它指令檔案(如果一個指令檔案調用另一個指令檔案作為輸入,這個語句必須是調用檔案最後的語句,連接配接器不從被調用的指令檔案傳回)。
  2. 連接配接器選項。它能以與指令行同樣的方式應用于指令檔案。
  3. MEMORY和SECTION連接配接器僞指令(隻能在指令檔案裡使用這些僞指令,不能在指令行上使用它們)。
  4. 指派語句。它定義全局符号并給它們指派。

下列名字作為連接配接器僞指令的關鍵字被保留,在指令檔案裡不要使用它們作為符号或段名:

讀懂CCS連結指令檔案(.cmd)

一個完整的簡單連結指令檔案示例如下:

a.obj b.obj c.obj

--output_file=prog.out

--map_file=prog.map

MEMORY

{

    FAST_MEM: origin = 0x0100 length = 0x0100

    SLOW_MEM: origin = 0x7000 length = 0x1000

}

SECTIONS

{

    .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則是可選的指令選項,這些指令選項有:

讀懂CCS連結指令檔案(.cmd)

連結器給每個輸出段在目标存儲器内配置設定兩個位址:加載時位址和運作時位址。一般情況下它們是同一個,可以認為每個段僅有單一的位址。如果加載和運作的位址是分離的,跟随關鍵字load後的所有參數,應用于加載定位;跟随關鍵字run後的所有參數,應用于運作定位。

未初始化段不加載,是以有意義的僅僅是運作位址。如果對未初始化段的加載位址和運作位址二者都指定,連接配接器發出警告并忽略加載位址。如果隻指定一個位址,連接配接器把它作為運作位址對待,而不管稱它是加載或是運作。

使用者可以為輸出段提供一個指定的起始位址,但這種位址綁定與邊界對齊(alignment)和指定存儲器(named memory)不相容,如果使用了邊界對齊(alignment)和指定存儲器(named memory),将不能綁定段位址。如果試圖這樣做,連接配接器将發出錯誤資訊。

輸出段能以兩種方法組成:

  1. 作為SECTIONS僞指令定義的結果;
  2. 把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

    .bss

    GROUP 0x00001000 :

    {

        .data

        term_rec

    }

}

3.5 原點“.”符号

一個用原點“.”标記的特殊符号,代表在位址配置設定期間的段程式計數器(SPC)的目前值,SPC保持跟蹤段内目前位址。符号“.”指的是段的目前運作位址,而不是目前加載位址。

讀懂CCS連結指令檔案(.cmd)

3.6 一個SECTIONS僞指令配置設定存儲的例子

file1.obj file2.obj

--output_file=prog.out

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

}

讀懂CCS連結指令檔案(.cmd)

連結器并不是一定需要連接配接器僞指令,如果沒有使用它們,連接配接器将使用目标處理器預設的配置設定代碼方案。

記憶體區域不夠時的解決辦法

在連結過程中經常會出現由于存儲區空間不足導緻段配置設定失敗的提示,同時連接配接器會給出未使用空間的大小和需要空間的大小。這一現象可表現出兩種情況:一是未使用 空間>需要空間;二是需要空間>未使用 空間。

第二種情況其實很好了解,第一種情況是怎麼回事呢?實際上,段的空間的配置設定是并不是我們想象中的連續的一個緊挨一個,由于資料對齊的需要以及記憶體頁的适配,都會在記憶體中産生一些空隙(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·

想進一步跟蹤本部落格動态,歡迎關注我的個人微信訂閱号:信号君

讀懂CCS連結指令檔案(.cmd)

鄭重·專業·有料