问题:

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 物件檔中的各個區段),而連結腳本告訴它如何將這些材料搭建成一棟完整的建築(最終的可執行檔),並確保這棟建築能被正確地放置在指定的土地上(微控制器的記憶體)。

交互細節如下:

  1. 定義記憶體佈局 (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 就是前面警告的來源)。
  2. 定義區段佈局規則 (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
      }
      
  3. 執行映射與放置 (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 選項被啟用,連結器會檢查該區段是否被任何其他需要保留的區段所引用。如果沒有,這個區段(例如一個未被呼叫的函式)就會被直接丟棄,從而節省空間。

總結來說,連結器是執行者,而連結腳本是指導者。連結腳本提供了記憶體的物理約束和內容佈局的邏輯規則,連結器則根據這些規則,精確地將程式的各個部分組裝起來,解析符號引用,並生成一個可以在特定硬體上正確執行的最終檔案。

更多推荐