使用gnu arm toolchain 编译stm32固件时,链接脚本与链接器之间的交互细节
问题:
arm-none-eabi
gcc -c -mcpu=cortex-m3 -mthumb -DUSE_HAL_DRIVER -DSTM32F103x6 -ICore/Inc -IDrivers/STM32F1xx_HAL_Driver/Inc -IDrivers/STM32F1xx_HAL_Driver/Inc/Legacy -IDrivers/CMSIS/Device/ST/STM32F1xx/Include -IDrivers/CMSIS/Include -IAPP -Og -Wall -fdata-sections -ffunction-sections -g -gdwarf-2 -MMD -MP -MF"build/usart.d" -Wa,-a,-ad,-alms=build/usart.lst Core/Src/usart.c -o build/usart.o
arm-none-eabi-gcc -x assembler-with-cpp -c -mcpu=cortex-m3 -mthumb -DUSE_HAL_DRIVER -DSTM32F103x6 -ICore/Inc -IDrivers/STM32F1xx_HAL_Driver/Inc -IDrivers/STM32F1xx_HAL_Driver/Inc/Legacy -IDrivers/CMSIS/Device/ST/STM32F1xx/Include -IDrivers/CMSIS/Include -IAPP -Og -Wall -fdata-sections -ffunction-sections -g -gdwarf-2 -MMD -MP -MF"build/startup_stm32f103x6.d" startup_stm32f103x6.s -o build/startup_stm32f103x6.o
arm-none-eabi-gcc build/led_blink.o build/scheduler.o build/uart.o build/printf.o build/main.o build/gpio.o build/stm32f1xx_it.o build/stm32f1xx_hal_msp.o build/stm32f1xx_hal_gpio_ex.o build/stm32f1xx_hal_tim.o build/stm32f1xx_hal_tim_ex.o build/stm32f1xx_hal.o build/stm32f1xx_hal_rcc.o build/stm32f1xx_hal_rcc_ex.o build/stm32f1xx_hal_gpio.o build/stm32f1xx_hal_dma.o build/stm32f1xx_hal_cortex.o build/stm32f1xx_hal_pwr.o build/stm32f1xx_hal_flash.o build/stm32f1xx_hal_flash_ex.o build/stm32f1xx_hal_exti.o build/stm32f1xx_hal_uart.o build/system_stm32f1xx.o build/sysmem.o build/syscalls.o build/tim.o build/usart.o build/startup_stm32f103x6.o -mcpu=cortex-m3 -mthumb -specs=nano.specs -TSTM32F103C6Tx_FLASH.ld -lc -lm -lnosys -Wl,-Map=build/YLAD.map,–cref -Wl,–gc-sections -o build/YLAD.elf
D:/gnu-arm/arm-gnu-toolchain-14.3.rel1-mingw-w64-x86_64-arm-none-eabi/bin/…/lib/gcc/arm-none-eabi/14.3.1/…/…/…/…/arm-none-eabi/bin/ld.exe: warning: build/YLAD.elf has a LOAD segment with RWX permissions
arm-none-eabi-size build/YLAD.elf
text data bss dec hex filename
7820 44 1740 9604 2584 build/YLAD.elf
arm-none-eabi-objcopy -O ihex build/YLAD.elf build/YLAD.hex
arm-none-eabi-objcopy -O binary -S build/YLAD.elf build/YLAD.bin解释一下每个命令的作用?链接脚本与链接器之间的交互细节?这两个问题逐一详细回答!
以下是回答:
gimini回答:好的,這是一份針對您提供的嵌入式編譯與連結命令的詳細解釋。
這些命令是典型的 ARM Cortex-M 微控制器 開發流程,涵蓋了編譯 (compilation)、連結 (linking) 與 格式轉換 (format conversion) 三個主要階段。
命令解析 (Command Breakdown)
這裡我會逐一解釋每條命令以及其重要參數的作用。🧐
1. 編譯 C 原始碼 (.c -> .o)
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb ... Core/Src/usart.c -o build/usart.o
這條命令的作用是將 C 語言原始碼檔案 (usart.c) 編譯成一個物件檔 (object file) (usart.o)。
arm-none-eabi-gcc: 這是針對 ARM 架構的 GCC 編譯器工具鏈。none-eabi指的是它用於「裸機 (bare-metal)」環境,沒有作業系統,使用嵌入式應用二進位介面 (EABI)。-c: 告訴編譯器只編譯,不進行連結。這是產生.o物件檔的關鍵參數。-mcpu=cortex-m3 -mthumb: 指定目標 CPU 為 Cortex-M3,並使用 Thumb 指令集。Thumb 是 ARM 架構下的 16/32 位元混合指令集,能提供很好的程式碼密度 (code density),節省 Flash 空間。-D...: 定義前置處理器宏 (preprocessor macros)。例如-DUSE_HAL_DRIVER相當於在程式碼開頭寫了#define USE_HAL_DRIVER。-I...: 指定標頭檔 (header file) 的搜尋路徑。編譯器會去這些目錄下尋找#include <...>的檔案。-Og: 優化等級 (optimization level)。-Og是「為除錯而優化」,它在不影響除錯體驗的前提下進行一些基礎優化。-Wall: 開啟所有常用警告 (Warnings all),幫助你寫出更穩健的程式碼。-fdata-sections -ffunction-sections: 這兩個參數非常重要。它們告訴編譯器將每個函式和每個全域變數都分別放在獨立的區段 (section) 中。這為後續連結器進行「垃圾回收」提供了基礎。-g -gdwarf-2: 產生除錯資訊 (DWARF-2 格式),這些資訊對於使用 GDB 等工具進行原始碼級別的除錯至關重要。-MMD -MP -MF"build/usart.d": 產生依賴性檔案 (.d)。這個檔案記錄了usart.c依賴了哪些標頭檔,供make這類建構工具判斷哪些檔案需要重新編譯。-Wa,-a,-ad,-alms=build/usart.lst: 將參數傳遞給組譯器 (assembler)。這裡的作用是產生一個列表檔案 (.lst),該檔案混合了 C 原始碼、對應的組合語言以及機器碼,非常適合用來分析編譯結果。-o build/usart.o: 指定輸出的物件檔名稱與路徑。
2. 組譯啟動檔案 (.s -> .o)
arm-none-eabi-gcc -x assembler-with-cpp -c ... startup_stm32f103x6.s -o build/startup_stm32f103x6.o
這條命令與上一條非常相似,但它是用來處理組合語言原始碼 (.s) 的,通常是晶片的啟動檔案。
-x assembler-with-cpp: 明確告訴 GCC,後面的輸入檔是組合語言,並且需要先經過 C 前置處理器 (cpp) 處理。這讓組合語言也能使用#define、#include等指令。startup_stm32f103x6.s: 這是啟動檔案,它包含了微控制器上電後最先執行的程式碼,例如:設定堆疊指標 (SP)、初始化中斷向量表、從 Flash 複製資料到 RAM、清零 BSS 段,最後再跳轉到 C 語言的main函式。
3. 連結 (Linking) (.o -> .elf)
arm-none-eabi-gcc build/led_blink.o ... build/startup_stm32f103x6.o ... -o build/YLAD.elf
這是整個流程的核心步驟。它將前面產生的所有 .o 物件檔以及系統函式庫連結在一起,產生一個最終的可執行檔 (.elf)。
build/led_blink.o ...: 輸入所有專案的物件檔。-specs=nano.specs: 指定使用 newlib-nano 函式庫。這是 newlib C 標準函式庫的一個輕量化版本,專為資源受限的嵌入式系統設計,可以大幅縮小程式碼體積。-TSTM32F103C6Tx_FLASH.ld: 指定連結腳本 (Linker Script)。這是一個極其重要的檔案,它告訴連結器如何將程式碼和資料佈局到微控制器的物理記憶體中(詳見下一節)。-lc -lm -lnosys: 連結標準函式庫。-lc(標準 C 函式庫)、-lm(數學函式庫)、-lnosys(一個提供空系統呼叫 (syscall) 存根的函式庫,因為我們沒有作業系統)。-Wl,...: 將逗號後面的參數直接傳遞給連結器 (ld)。-Map=build/YLAD.map: 產生一個 Map 檔案。這個檔案詳細記錄了每個函式、變數的最終記憶體位址和大小,是分析記憶體使用的利器。--cref: 在 Map 檔案中加入一個交互參考表 (cross-reference table)。--gc-sections: 垃圾回收 (Garbage Collect Sections)。連結器會掃描所有區段,如果一個區段 (例如某個函式) 從未被其他程式碼引用,它就會被丟棄,不會被包含在最終的.elf檔中。這就是-ffunction-sections和-fdata-sections發揮作用的地方,它們能有效地縮小最終的韌體大小。
-o build/YLAD.elf: 指定輸出的最終可執行檔名稱。ELF (Executable and Linkable Format) 是一種包含程式碼、資料、符號表、除錯資訊等的標準格式。
Linker Warning 解釋
ld.exe: warning: build/YLAD.elf has a LOAD segment with RWX permissions
這個警告指出,你的 .elf 檔案中有一個記憶體區段 (segment) 同時擁有讀 ®、寫 (W) 和執行 (X) 的權限。在有記憶體保護單元 (MPU) 或作業系統的環境中,這是一個嚴重的安全隱患,因為它允許程式碼被修改(例如緩衝區溢位攻擊)。在簡單的裸機 Cortex-M 應用中,這通常是連結腳本的預設配置導致的,雖然不理想,但通常可以忽略。
4. 檢查尺寸 (size)
arm-none-eabi-size build/YLAD.elf
這條命令會讀取 .elf 檔案並報告其各個主要區段的大小。
text: 程式碼區段。包含函式、指令和唯讀常數。它最終會被燒錄到 Flash。data: 已初始化資料區段。包含已賦予非零初值的全域變數和靜態變數。它儲存在 Flash 中,在啟動時由啟動程式碼複製到 RAM。bss: 未初始化資料區段。包含未賦予初值或初值為零的全域變數和靜態變數。它不佔用 Flash 空間,只在.elf中記錄其大小。啟動時,啟動程式碼會在 RAM 中為其分配空間並清零。dec/hex: 各區段大小總和的十進位和十六進位表示。
💡 重要
- Flash 使用量 ≈
text+data- RAM 使用量 ≈
data+bss
5. 格式轉換 (objcopy)
arm-none-eabi-objcopy -O ihex build/YLAD.elf build/YLAD.hex
arm-none-eabi-objcopy -O binary -S build/YLAD.elf build/YLAD.bin
.elf 檔案包含大量除錯資訊,不適合直接燒錄到晶片。objcopy 工具可以將其轉換為燒錄器能識別的格式。
-O ihex: 輸出 Intel HEX (.hex) 格式。這是一種基於 ASCII 的文本格式,包含了位址資訊和資料,是燒錄器最常用的格式之一。-O binary: 輸出原始二進位 (.bin) 格式。這是一個純粹的二進位檔案,不包含任何位址或元數據,就是記憶體的「映像 (image)」。-S: 在轉換為 binary 格式時,移除所有符號和重定位資訊。
連結腳本 (Linker Script) 與連結器 (Linker) 的交互 📚🗺️
連結器和連結腳本之間的關係,可以比喻為建築師(連結器) 和 建築藍圖(連結腳本)。連結器擁有所有的建築材料(.o 物件檔中的各個區段),而連結腳本告訴它如何將這些材料搭建成一棟完整的建築(最終的可執行檔),並確保這棟建築能被正確地放置在指定的土地上(微控制器的記憶體)。
交互細節如下:
-
定義記憶體佈局 (The Blueprint’s Foundation)
- 連結腳本首先使用
MEMORY命令來定義目標微控制器的物理記憶體地圖。 - 腳本內容範例 (
STM32F103C6Tx_FLASH.ld):MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 10K FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K } - 交互: 連結器讀取這段定義,從而得知有兩個主要的記憶體區域:一個名為
FLASH,從位址0x08000000開始,唯讀可執行;另一個名為RAM,從0x20000000開始,可讀可寫可執行(RWX 就是前面警告的來源)。
- 連結腳本首先使用
-
定義區段佈局規則 (The Construction Rules)
- 接著,連結腳本使用
SECTIONS命令來設定詳細的規則,告訴連結器如何組合輸入的區段 (.text,.data等),形成輸出的區段,並將它們放置到MEMORY中定義好的區域。 - 腳本內容範例:
SECTIONS { /* 1. The program code and other data into FLASH */ .text : { . = ALIGN(4); *(.isr_vector) /* Interrupt vector table first */ *(.text) /* .text sections (code) */ *(.text.*) /* .text subsections (code) */ *(.rodata) /* .rodata sections (read-only data) */ *(.rodata.*) /* .rodata subsections (read-only data) */ . = ALIGN(4); _etext = .; /* Define a symbol for end of code */ } >FLASH /* 2. Initialized data sections into RAM */ .data : { _sdata = .; /* Define a symbol for start of data */ *(.data) *(.data.*) _edata = .; /* Define a symbol for end of data */ } >RAM AT> FLASH /* VMA is in RAM, LMA is in FLASH */ /* 3. Uninitialized data sections into RAM */ .bss : { _sbss = .; /* Define a symbol for start of bss */ *(.bss) *(.bss.*) _ebss = .; /* Define a symbol for end of bss */ } >RAM }
- 接著,連結腳本使用
-
執行映射與放置 (The Construction Process)
- Step 1: 收集材料: 連結器讀取所有
.o檔案,並將它們的區段(例如main.o的.text.main,usart.o的.text.USART_Init)全部收集起來。 - Step 2: 遵循規則:
- 當處理到腳本中的
.text輸出區段時,連結器會找到所有輸入檔中的.isr_vector、.text、.text.*、.rodata等區段,將它們依序拼接在一起。 >FLASH: 這條指令告訴連結器,將拼接好的整個.text輸出區段放置到MEMORY中定義的FLASH區域。
- 當處理到腳本中的
- Step 3: 處理 VMA 和 LMA:
- 對於
.data區段,>RAM AT> FLASH是一個關鍵指令。 VMA (Virtual Memory Address): 程式執行時的位址。>RAM表示.data區段在執行時應該位於 RAM 中。LMA (Load Memory Address): 程式載入時的位址 (即燒錄到晶片時的位置)。AT> FLASH表示.data區段的初始內容(那些非零的初始值)需要被儲存在 FLASH 中。- 交互: 連結器會將
.data的內容放在.elf檔案的 FLASH 位址部分,但會標記其 VMA 為 RAM 位址。啟動程式碼 (startup.s) 會使用連結器定義的_sdata、_edata等符號,在啟動時執行一個迴圈,將這部分資料從 FLASH 複製到 RAM。
- 對於
- Step 4: 處理 BSS:
- 對於
.bss區段,>RAM表示它在執行時也位於 RAM 中。因為它沒有初始值,所以沒有 LMA,也不會佔用 FLASH 空間。 - 交互: 連結器只在
.elf中記錄.bss區段的大小。啟動程式碼會使用_sbss和_ebss符號,在 RAM 中將這塊區域清零。
- 對於
- Step 5: 垃圾回收 (Garbage Collection) 🗑️: 在放置每個區段之前,如果
--gc-sections選項被啟用,連結器會檢查該區段是否被任何其他需要保留的區段所引用。如果沒有,這個區段(例如一個未被呼叫的函式)就會被直接丟棄,從而節省空間。
- Step 1: 收集材料: 連結器讀取所有
總結來說,連結器是執行者,而連結腳本是指導者。連結腳本提供了記憶體的物理約束和內容佈局的邏輯規則,連結器則根據這些規則,精確地將程式的各個部分組裝起來,解析符號引用,並生成一個可以在特定硬體上正確執行的最終檔案。
更多推荐

所有评论(0)