임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
#한 줄 요약
“링커 스크립트는 바이너리의 지도입니다.” 각 섹션이 Flash와 RAM 어디에 가는지, C++ 객체는 어떻게 배치되는지를 정의합니다.
#어떤 문제를 푸는가
ELF 파일은 섹션의 집합입니다. .text, .rodata, .data, .bss, .init_array 등은 자동으로 어딘가에 배치되지 않습니다. 링커 스크립트가 어느 메모리의 어느 주소에 두는지 결정합니다.
벤더(STM32, NXP)가 기본 링커 스크립트를 제공하지만, C++가 추가하는 섹션(.init_array, .fini_array, .gnu.linkonce.*)이나 프로젝트 특화 영역(외부 SDRAM, CCM RAM, DMA buffer)을 다루려면 직접 이해해야 합니다.
전형적인 STM32F4의 메모리 레이아웃은 다음과 같이 배치됩니다.
#링커 스크립트의 두 핵심 — MEMORY와 SECTIONS
#MEMORY — 사용 가능한 메모리 영역
MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 192K CCM (rwx) : ORIGIN = 0x10000000, LENGTH = 64K}각 영역은 다음과 같이 구성됩니다.
- 이름(
FLASH,RAM,CCM) - 권한(
r=read,w=write,x=execute) - 시작 주소(
ORIGIN) - 크기(
LENGTH)
STM32F407 예시는 다음과 같습니다.
- FLASH는 0x08000000부터 1MB입니다
- RAM은 0x20000000부터 192KB(main SRAM)입니다
- CCM은 0x10000000부터 64KB(Core-Coupled Memory, CPU 전용 빠른 RAM)입니다
#SECTIONS — 섹션의 배치
SECTIONS { .isr_vector : { KEEP(*(.isr_vector)) } >FLASH
.text : { *(.text*) *(.rodata*) } >FLASH
.data : { *(.data*) } >RAM AT >FLASH
.bss : { *(.bss*) *(COMMON) } >RAM}>FLASH는 VMA(Virtual Memory Address)이고, AT >FLASH는 LMA(Load Memory Address)입니다. 차이는 .data 섹션에서 중요해집니다.
#VMA vs LMA — .data 섹션의 이중성
.data는 초기값 있는 mutable 변수입니다. 런타임에는 RAM에 있어야 하지만 초기값은 Flash에 저장되어야 합니다(RAM은 전원이 꺼지면 사라집니다).
int counter = 42; // 초기값 42가 Flash, 런타임 사용은 RAM링커 스크립트는 다음과 같습니다.
.data : { _sdata = .; /* RAM 시작 주소 */ *(.data*) _edata = .; /* RAM 끝 주소 */} >RAM AT >FLASH /* VMA=RAM, LMA=FLASH */
_sidata = LOADADDR(.data); /* Flash 위치 */Reset_Handler에서 Flash의 _sidata에서 RAM의 _sdata로 복사합니다(Part 1-06 참조).
#C++가 추가하는 섹션
C 코드 빌드와 다른 C++ 특유의 섹션들입니다.
#.init_array — static 생성자 포인터
.init_array : { PROVIDE_HIDDEN(__init_array_start = .); KEEP(*(SORT(.init_array.*))) KEEP(*(.init_array)) PROVIDE_HIDDEN(__init_array_end = .);} >FLASHKEEP은--gc-sections가 제거하지 않도록 보호합니다SORT는 초기화 우선순위에 따라 정렬합니다PROVIDE_HIDDEN은 symbol을 노출하지만 dynamic symbol table에는 들어가지 않습니다
__libc_init_array가 __init_array_start부터 __init_array_end까지 함수 포인터를 차례로 호출합니다. 자세한 흐름은 Part 1-06에서 다룹니다.
#.fini_array — 소멸자 포인터
.fini_array : { PROVIDE_HIDDEN(__fini_array_start = .); KEEP(*(SORT(.fini_array.*))) KEEP(*(.fini_array)) PROVIDE_HIDDEN(__fini_array_end = .);} >FLASH__libc_fini_array가 호출합니다. 임베디드에서는 main이 끝나지 않아 보통 호출되지 않습니다. -fno-use-cxa-atexit를 추가하면 공간이 절약됩니다.
#.gnu.linkonce.* 또는 .text.* — 템플릿 인스턴스
같은 템플릿이 여러 TU에서 인스턴스화되면 링커가 중복을 제거합니다. C++17 이후로는 대부분 자동으로 처리됩니다.
#.eh_frame — 예외 unwind table
/DISCARD/ : { *(.eh_frame*) *(.ARM.extab*) *(.ARM.exidx*)}-fno-exceptions 환경에서는 완전히 제거됩니다. 수 KB가 절약됩니다.
#완성 링커 스크립트 — STM32F407 예시
C++ 임베디드 표준 스크립트입니다.
/* STM32F407 1MB Flash, 192KB RAM */
MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K CCM (rwx) : ORIGIN = 0x10000000, LENGTH = 64K}
/* stack은 RAM 끝에서 시작 (downward 성장) */_estack = ORIGIN(RAM) + LENGTH(RAM);
ENTRY(Reset_Handler)
SECTIONS { /* 0. Vector table — Flash 시작 */ .isr_vector : { KEEP(*(.isr_vector)) . = ALIGN(4); } >FLASH
/* 1. Code — Flash */ .text : { . = ALIGN(4); *(.text) *(.text*) *(.rodata) *(.rodata*) *(.glue_7) /* ARM/Thumb interworking */ *(.glue_7t) KEEP(*(.eh_frame)) /* keep if exceptions; else /DISCARD/ */ . = ALIGN(4); _etext = .; } >FLASH
/* 2. ARM exception index (예외 끄면 DISCARD) */ .ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) } >FLASH .ARM : { __exidx_start = .; *(.ARM.exidx*) __exidx_end = .; } >FLASH
/* 3. C++ static 생성자 — Flash */ .preinit_array : { PROVIDE_HIDDEN(__preinit_array_start = .); KEEP(*(.preinit_array*)) PROVIDE_HIDDEN(__preinit_array_end = .); } >FLASH
.init_array : { PROVIDE_HIDDEN(__init_array_start = .); KEEP(*(SORT(.init_array.*))) KEEP(*(.init_array)) PROVIDE_HIDDEN(__init_array_end = .); } >FLASH
.fini_array : { PROVIDE_HIDDEN(__fini_array_start = .); KEEP(*(SORT(.fini_array.*))) KEEP(*(.fini_array)) PROVIDE_HIDDEN(__fini_array_end = .); } >FLASH
/* 4. .data — VMA=RAM, LMA=FLASH */ _sidata = LOADADDR(.data);
.data : { . = ALIGN(4); _sdata = .; *(.data) *(.data*) . = ALIGN(4); _edata = .; } >RAM AT >FLASH
/* 5. .bss — RAM */ .bss : { . = ALIGN(4); _sbss = .; __bss_start__ = _sbss; *(.bss) *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; __bss_end__ = _ebss; } >RAM
/* 6. heap (newlib sbrk가 _end 사용) */ ._user_heap_stack : { . = ALIGN(8); PROVIDE(end = .); PROVIDE(_end = .); . = . + _Min_Heap_Size; . = . + _Min_Stack_Size; . = ALIGN(8); } >RAM
/* 7. CCM RAM에 임의 데이터 배치 (선택) */ .ccmram : { . = ALIGN(4); _siccmram = LOADADDR(.ccmram); _sccmram = .; *(.ccmram) *(.ccmram*) . = ALIGN(4); _eccmram = .; } >CCM AT >FLASH
/* 8. 불필요 섹션 제거 */ /DISCARD/ : { libc.a (*) libm.a (*) libgcc.a (*) }}
_Min_Heap_Size = 0x200; /* 512 bytes */_Min_Stack_Size = 0x400; /* 1 KB */#Custom 섹션 — 특정 데이터를 특정 위치에
C++에서 특정 변수를 특정 메모리 영역에 두고 싶을 때는 __attribute__((section(...)))를 씁니다.
// 큰 DMA buffer를 CCM RAM에__attribute__((section(".ccmram")))uint8_t dma_buffer[4096];
// 부트 시점에 .text 옆 const table__attribute__((section(".text.const_lut")))const uint8_t lookup_table[256] = { /* ... */ };링커 스크립트가 .ccmram 섹션을 CCM RAM에 배치합니다. 컴파일러는 해당 변수를 그 섹션에 넣습니다.
DMA buffer를 CCM에 두는 흔한 케이스는 주의가 필요합니다. CCM은 DMA가 접근하지 못합니다(peripheral bus와 연결되어 있지 않습니다). DMA용은 일반 SRAM에 둡니다.
#C++ 객체의 지정된 위치 배치
C++ 객체도 같은 attribute로 위치를 지정할 수 있습니다.
// 큰 lookup 객체를 Flash에 직접__attribute__((section(".rodata.lookups")))const std::array<uint16_t, 1024> sin_table = { /* compile-time computed */ };
// CCM에 두는 buffer pool__attribute__((section(".ccmram")))alignas(8) uint8_t packet_pool[8192];constexpr로 생성된 const data는 .rodata에 자동으로 배치됩니다. 별도 지정이 필요 없는 경우가 많습니다.
#Symbol 정의 — Reset_Handler가 사용
링커 스크립트가 symbol을 정의하면 C/C++ 코드에서 extern으로 참조할 수 있습니다.
/* 링커 스크립트 */_sdata = .;*(.data*)_edata = .;// C++에서 사용extern "C" { extern uint32_t _sdata; extern uint32_t _edata; extern uint32_t _sidata;}
void copy_data() { uint32_t* src = &_sidata; uint32_t* dst = &_sdata; while (dst < &_edata) *dst++ = *src++;}주소 자체가 의미를 가지므로 &를 사용합니다(변수 값이 아니라 위치이기 때문입니다).
#Memory Map 생성
링커 옵션 -Wl,-Map=file.map이 모든 섹션과 심볼의 배치 정보를 텍스트로 출력합니다.
arm-none-eabi-g++ ... -Wl,-Map=firmware.map -o firmware.elffirmware.map 내용은 다음과 같습니다.
Memory ConfigurationName Origin Length AttributesFLASH 0x08000000 0x100000 rxRAM 0x20000000 0x20000 rwxCCM 0x10000000 0x10000 rwx
Linker script and memory map....text 0x08000000 0x4a3c 0x08000000 _stext = . *(.text*) .text 0x08000000 0x0034 build/startup.o 0x08000000 Reset_Handler .text 0x08000034 0x0080 build/main.o 0x08000034 main ....init_array 0x08004b40 0x20 0x08004b40 PROVIDE_HIDDEN (__init_array_start = .) *(.init_array) .init_array 0x08004b40 0x18 build/main.o .init_array 0x08004b58 0x04 build/logger.o .init_array 0x08004b5c 0x04 build/timer.o 0x08004b60 PROVIDE_HIDDEN (__init_array_end = .)어느 .o 파일이 어느 섹션에 얼마나 기여했는지 정확히 보입니다. 크기 분석의 핵심 도구입니다(Part 1-04 참조).
#자주 보는 함정과 안티패턴
#1. .init_array에 KEEP 없음
--gc-sections가 static 생성자 함수를 제거합니다. 객체가 zero-init만으로 시작해 잘못된 동작을 합니다. KEEP이 필수입니다.
#2. AT >FLASH 누락
.data 초기값이 Flash에 들어가지 않습니다. RAM의 초기값이 garbage가 됩니다. Reset_Handler의 .data copy가 garbage를 복사합니다.
#3. Stack pointer 미정의
vector table의 첫 entry가 invalid가 됩니다. CPU가 random 주소를 SP로 사용해 즉시 crash가 납니다. _estack = ORIGIN(RAM) + LENGTH(RAM)이 필수입니다.
#4. Heap과 Stack 영역 충돌
Heap은 위로 자라고 Stack은 아래로 자랍니다. 만나면 조용히 데이터 corruption이 발생합니다. _Min_Heap_Size와 _Min_Stack_Size를 명시합니다.
#5. DMA buffer를 CCM에 두기
CCM은 CPU 전용 RAM입니다. DMA controller가 접근하지 못해 bus fault가 납니다. DMA는 AHB로 접근 가능한 일반 SRAM에 둡니다.
#6. 예외 사용하면서 .eh_frame을 /DISCARD/
-fexceptions와 DISCARD를 함께 쓰면 런타임에 예외 정보가 없어 unwind가 실패하고 crash가 납니다. 둘 중 하나로 통일합니다.
#7. 외부 SDRAM 미설정으로 access
external memory는 MMU나 FMC 초기화가 필요합니다. 링커 스크립트만으로는 주소 할당만 하고, 실제 access는 SystemInit 이후에 가능합니다.
#측정 — 링커 스크립트 변경 효과
CCM RAM에 큰 buffer를 옮겨 main RAM을 절약한 사례입니다.
# Before: 일반 RAM.bss 32 KB (4KB DMA buffer 포함)
# After: DMA buffer를 CCM에.bss 28 KB.ccmram 4 KBmain SRAM에 4KB의 여유가 생깁니다. RTOS task stack 추가에 활용할 수 있습니다.
#ld 스크립트 디버깅 — --print-memory-usage
arm-none-eabi-g++ ... -Wl,--print-memory-usage
Memory region Used Size Region Size %age Used FLASH: 42688 B 1 MB 4.07% RAM: 30432 B 128 KB 23.21% CCM: 4096 B 64 KB 6.25%CI에 추가해 영역별 사용량을 추적합니다.
#정리
- 링커 스크립트는 MEMORY 영역 정의와 SECTIONS 배치 두 부분으로 구성됩니다.
- C++가 추가하는 섹션은
.init_array,.fini_array,.eh_frame이며KEEP과--gc-sections의 상호작용에 주의합니다. .data는 VMA를 RAM에, LMA를 FLASH에 둡니다. Reset_Handler가 부팅 시 복사합니다.- 큰 buffer는 custom 섹션으로 CCM이나 SDRAM에 배치할 수 있습니다. 단 DMA buffer는 일반 SRAM에 두어야 합니다.
-Wl,-Map으로 완전한 배치 정보를 얻고, CI에는--print-memory-usage를 추가해 영역별 사용량을 추적합니다.
#관련 항목
- Part 1-06: 스타트업 코드 — Reset_Handler가 사용하는 symbol
- Part 1-04: 코드 크기 분석 — .map 파일과 size 명령
- Part 3-03: Pool Allocator 구현 — custom 섹션에 pool 배치
#다음 글
Part 1-08: C++ 표준 선택 — C++11/14/17/20/23 중 어느 표준을 골라야 하는지 임베디드 관점에서 기능을 비교합니다.
Embedded C++ for Real Systems · 8 of 41
- 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
- 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
- 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
- 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
- 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
- 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
- 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
- 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
- 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
- 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
- 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
- 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
- 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
- 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
- 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
- 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
- 17CRTP 패턴 분석 — vtable 없는 정적 다형성
- 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
- 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
- 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
- 21Custom Allocator 기초 — std::allocator 인터페이스 분석
- 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
- 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
- 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
- 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
- 26std::expected 분석 — C++23 결과 타입과 에러 전파
- 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
- 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
- 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
- 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
- 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
- 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
- 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
- 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
- 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
- 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
- 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
- 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
- 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
- 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
- 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교
관련 글
C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
Reset에서 main까지 — vector table, .data 복사, .bss 초기화, __libc_init_array가 호출하는 C++ static 생성자.
임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교
범용 HAL 구조 — 벤더 종속성 격리, 다중 보드/MCU 지원, 시리즈 마무리.
Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
UART, SPI, I2C — peripheral을 type-safe class로. Blocking, interrupt, DMA 패턴.