嵌入式連結器指令稿(linker script)
想像你搬進一棟空建築,必須一間一間決定每件家具確切要放哪——而且有些房間只能放特定東西(保險庫放貴重物、衣櫥放衣服)。在有主機的 PC 上,作業系統的載入器替你做了多數這類決定。在微控制器上沒有載入器,所以「你」必須告訴連結器,程式的每個部分確切住在晶片記憶體的何處。連結器指令稿就是那張平面圖。(連結器指令稿的一般概念見 Field l;這裡聚焦嵌入式的特點。)
嵌入式連結器指令稿有兩個主要部分。第一是 MEMORY 區塊,為晶片的實體區段命名並給出確切位址與大小——例如 FLASH 起於 0x08000000、容量 256 KiB,RAM 起於 0x20000000、容量 64 KiB。第二是 SECTIONS 區塊,把各類輸出放進那些區段:向量表與程式碼(.text)與唯讀常數(.rodata)放進 FLASH;可寫資料(.data、.bss)與堆疊放進 RAM。嵌入式的關鍵轉折在 .data:它的執行期位置是 RAM(變數住的地方),但載入位置是 FLASH(初始值存放處,好在斷電後依然存在)。連結器指令稿用「虛擬位址 vs 載入位址」這一對來表達(在 GNU ld 裡是 AT> 關鍵字),它也「匯出」像 _sdata、_edata、_sbss、_ebss、_sidata 這些符號,供啟動程式碼讀取,好知道從哪複製、從哪清零。它也能把某個區段釘在固定位址——這對於把向量表放到重置硬體查看之處至關重要。
它之所以重要,是因為沒有作業系統時,連結器指令稿是唯一把你抽象的區段對映到真實晶片的東西——把某個區段大小弄錯,連結器會報錯(區段溢位),或更糟,東西默默落到錯誤的記憶體。要內化的誠實提醒:連結器指令稿與啟動程式碼是一對。指令稿定義邊界符號;啟動程式碼「使用」它們。若你只改其一而不改另一——例如你新增了一個記憶體區段,但啟動迴圈仍用舊的 _ebss——你的 .bss 清零或 .data 複製就會出錯,而故障會在離成因很遠的地方才現形。
/* 最小 Cortex-M 連結器指令稿(GNU ld)*/ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K } SECTIONS { .isr_vector : { KEEP(*(.isr_vector)) } > FLASH /* 釘住向量表 */ .text : { *(.text*) *(.rodata*) } > FLASH .data : { _sdata = .; *(.data*) _edata = .; } > RAM AT> FLASH _sidata = LOADADDR(.data); /* .data 在快閃裡的副本 */ .bss : { _sbss = .; *(.bss*) _ebss = .; } > RAM }
MEMORY 為晶片區段命名;SECTIONS 把程式碼放進 FLASH、資料放進 RAM,並讓 .data 的載入副本在 FLASH(AT> FLASH),同時匯出供啟動程式碼用的邊界符號。
連結器指令稿與啟動程式碼是一對:指令稿「匯出」_sdata/_sbss/_sidata,啟動程式碼「使用」它們。只改其一不改另一,你的 .bss 清零或 .data 複製就會默默出錯。