Linker Script STM32: MEMORY, sections và tại sao .data cần hai địa chỉ

Giải thích linker script STM32 căn bản: MEMORY region, .text/.data/.bss, LMA vs VMA, startup copy.

6 phút đọc
STM32 / Firmware cover

Linker script (.ld) là file quyết định firmware của bạn nằm ở đâu trong bộ nhớ. Không phải compiler, không phải IDE - linker script mới là thứ nói cho toolchain biết địa chỉ nào chứa code, địa chỉ nào chứa data, vùng nào là RAM, vùng nào là Flash.

Khi bạn flash firmware lên STM32 và nó chạy đúng, linker script đang làm việc đằng sau lặng lẽ. Khi firmware chạy được trên board nhưng một số biến global bị sai giá trị - rất có thể linker script hoặc startup code đang có vấn đề.

Cấu trúc cơ bản

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 128K
  RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 36K
}

SECTIONS
{
  .text : { ... } > FLASH
  .data : { ... } > RAM AT> FLASH
  .bss  : { ... } > RAM
}

Hai khối chính: MEMORY khai báo vùng nhớ vật lý, SECTIONS map từng loại dữ liệu vào vùng nhớ tương ứng.

MEMORY - khai báo địa chỉ thật của chip

ORIGIN là địa chỉ bắt đầu, LENGTH là kích thước. Với STM32G0B1:

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
  RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 144K
}

Attribute rx = readable + executable. xrw = executable + readable + writable.

Giá trị này không tùy tiện - phải khớp với datasheet chip. Nếu khai báo sai LENGTH, linker sẽ không báo lỗi nếu code vẫn nhỏ hơn, nhưng firmware có thể ghi đè vùng nhớ của chip khác trên PCB nếu dùng external bus.

Các section quan trọng

Mục Giá trị Ghi chú
.text Code và hằng số read-only Nằm trong Flash. CPU fetch instruction từ đây. Bao gồm vector table, tất cả function, const global.
.rodata Read-only data Thường merge vào .text. String literal, const array, lookup table.
.data Biến global/static có giá trị khởi tạo Lưu trong Flash (LMA), copy sang RAM (VMA) khi startup. Ví dụ: `int x = 5;`
.bss Biến global/static không khởi tạo hoặc = 0 Không chiếm space trong Flash. Startup code zero-fill vùng này trong RAM. Ví dụ: `int y;` hoặc `int y = 0;`
.stack Call stack của chương trình Nằm trong RAM. Địa chỉ đầu stack thường là giá trị đầu tiên trong vector table (MSP).
.heap Dynamic allocation (malloc) Nằm trong RAM. Embedded thường tránh dùng hoặc giới hạn kỹ.

LMA vs VMA - tại sao .data cần hai địa chỉ

Đây là điểm dễ gây nhầm nhất.

LMA (Load Memory Address) - địa chỉ lưu trữ của data trong file firmware, nằm trong Flash.
VMA (Virtual Memory Address) - địa chỉ thực thi khi runtime, nằm trong RAM.

Tại sao .data cần hai địa chỉ? Vì biến global có giá trị khởi tạo phải:

  1. Được lưu trong Flash khi bạn flash firmware (LMA) - vì Flash là non-volatile
  2. Được copy sang RAM khi startup (VMA) - vì CPU chỉ có thể ghi/đọc RAM, không ghi được Flash trực tiếp
.data :
{
  _sdata = .;           /* ký hiệu = địa chỉ đầu RAM */
  *(.data .data.*)
  _edata = .;           /* ký hiệu = địa chỉ cuối RAM */
} >RAM AT> FLASH
                /* ^VMA   ^LMA */

/* Linker tự tính _sidata = địa chỉ LMA trong Flash */

>RAM AT> FLASH có nghĩa: VMA trong RAM, LMA trong Flash.

Sau đó, startup code chịu trách nhiệm copy:

/* Trong startup_stm32g0xx.s hoặc main trước khi gọi main() */
extern uint32_t _sdata, _edata, _sidata;

uint32_t *src = &_sidata;  /* LMA - nguồn trong Flash */
uint32_t *dst = &_sdata;   /* VMA - đích trong RAM */
while (dst < &_edata) {
    *dst++ = *src++;
}

Nếu startup code không chạy (ví dụ: reset handler bị lỗi trước khi copy), biến global có giá trị khởi tạo sẽ chứa rác.

.bss - tiết kiệm Flash bằng cách không lưu zero

Biến int counter = 0; không cần lưu giá trị 0 trong Flash vì startup code sẽ zero-fill toàn bộ .bss:

extern uint32_t _sbss, _ebss;
uint32_t *p = &_sbss;
while (p < &_ebss) {
    *p++ = 0;
}

Kết quả: .bss không chiếm byte nào trong file .bin flash, nhưng chiếm RAM khi chạy. Với firmware có nhiều buffer lớn khởi tạo zero, đây là tiết kiệm Flash đáng kể.

Chia Flash thành nhiều vùng

Khi cần dành một phần Flash để lưu data (config, EEPROM emulation), khai báo thêm MEMORY region:

MEMORY
{
  /* STM32G0B1: 512K Flash, dual bank - mỗi bank 256K */
  FLASH_APP  (rx)  : ORIGIN = 0x08000000, LENGTH = 240K  /* Program */
  FLASH_DATA (r)   : ORIGIN = 0x0803C000, LENGTH = 16K   /* EEPROM emu */
  RAM        (xrw) : ORIGIN = 0x20000000, LENGTH = 144K
}

SECTIONS
{
  .text  : { ... } > FLASH_APP
  .data  : { ... } > RAM AT> FLASH_APP
  .bss   : { ... } > RAM

  /* Section riêng cho EEPROM emulation - linker không đụng vào */
  .eeprom_data (NOLOAD) :
  {
    . = ALIGN(2048);  /* Align theo page size */
    KEEP(*(.eeprom_data))
  } > FLASH_DATA
}

(NOLOAD) nói linker không load section này vào RAM - nó sống trong Flash.
KEEP(...) ngăn linker optimize bỏ section này dù không có code reference tới.

Sau đó trong C:

/* Đặt biến vào section cụ thể */
__attribute__((section(".eeprom_data")))
const uint8_t EepromPage[2048] = { [0 ... 2047] = 0xFF };

Kiểm tra kết quả linker

# Xem size từng section
arm-none-eabi-size build/firmware.elf

# Xem map file - nơi từng symbol đặt vào
arm-none-eabi-nm -S --size-sort build/firmware.elf | tail -20

# Xem toàn bộ map (nếu build tạo .map file)
cat build/firmware.map | grep -A3 ".data"

Checklist khi sửa linker script

Bài liên quan

Tài liệu tham khảo

Thấy nội dung này hữu ích?

Lưu lại hoặc chia sẻ cho người cũng đang học firmware, BIOS/UEFI và embedded systems.

Nội dung liên quan

Một số bài viết, ghi chú hoặc project có liên quan đến nội dung bạn vừa đọc.

Biến note thành bài viết hoàn chỉnh

Notes là nơi ghi nhanh khái niệm.