Linker Script Reference
링커 스크립트 레퍼런스
Linker Script Reference 소개
Linker Script Reference는 임베디드 펌웨어 개발에 사용되는 GNU ld 링커 스크립트 언어를 검색 가능하게 정리한 치트 시트입니다. MEMORY, SECTIONS, 심볼, ENTRY, ALIGN, 디버깅의 6개 카테고리로 구성되어 STM32, nRF52 등 ARM Cortex-M 마이크로컨트롤러의 플래시와 RAM에 컴파일된 바이너리가 어떻게 배치되는지 정의하는 모든 단계를 다룹니다.
임베디드 시스템 엔지니어, 펌웨어 개발자, 전자공학 학생들이 베어메탈 또는 RTOS 기반 프로젝트의 .ld 파일을 작성하거나 디버깅할 때 이 레퍼런스를 활용합니다. MEMORY 섹션은 ORIGIN/LENGTH 선언, 접근 속성 플래그(r, w, x), 다중 FLASH/SRAM 뱅크 설정을 다루며, SECTIONS 카테고리는 .text(실행 코드), .data(초기값 있는 변수, LMA는 플래시, VMA는 RAM), .bss(0으로 초기화되는 변수)를 올바른 메모리 영역에 배치하는 방법을 설명합니다.
심볼 섹션은 위치 카운터(.), PROVIDE()로 기본값 심볼 제공, 스타트업 코드에서 참조하는 _sdata/_edata/_sbss/_ebss 심볼 패턴, 섹션의 로드 주소를 가져오는 LOADADDR(), 크기 계산에 사용하는 SIZEOF()를 다룹니다. 디버깅 카테고리에는 --print-memory-usage, arm-none-eabi-size, arm-none-eabi-nm 심볼 테이블 조회, .map 파일 생성 등 실무에서 바로 사용할 수 있는 도구들이 포함됩니다.
주요 기능
- MEMORY 영역: ORIGIN/LENGTH 선언, 접근 플래그(rx, rwx, !w), 다중 FLASH/SRAM 뱅크 설정
- SECTIONS: ISR 벡터 KEEP, .text 배치, .data의 VMA/LMA 분리(> RAM AT> FLASH), .bss 초기화 패턴
- 위치 카운터(.)로 섹션 경계 계산 및 _sdata/_edata/_sbss/_ebss 스타트업 심볼 정의
- PROVIDE()로 C 스타트업 코드가 extern으로 참조하는 기본값 심볼 안전하게 제공
- ENTRY(Reset_Handler), KEEP(*(.isr_vector)), STARTUP()으로 진입점과 링크 순서 제어
- ALIGN(n)으로 바이트 경계 정렬, 스택/힙 영역 정의, MPU용 2의 거듭제곱 정렬
- 디버깅: --print-memory-usage 플래그, arm-none-eabi-size, arm-none-eabi-nm 심볼 테이블
- -Map=output.map으로 .map 파일 생성해 섹션별·파일별 메모리 기여도 확인
자주 묻는 질문
링커 스크립트란 무엇이고 누가 사용하나요?
링커 스크립트(.ld 파일)는 GNU ld 링커에게 컴파일된 바이너리의 각 섹션을 타겟 디바이스의 메모리 맵 어디에 배치할지 알려줍니다. 플래시 시작 주소, RAM 시작 주소, 스택 크기, 힙 위치를 명시적으로 정의해야 하는 마이크로컨트롤러 베어메탈 개발자들이 작성합니다.
VMA와 LMA의 차이는 무엇인가요?
VMA(Virtual Memory Address)는 런타임에 섹션이 실제로 위치하는 주소입니다. LMA(Load Memory Address)는 섹션 데이터가 저장되는 플래시 주소입니다. .data 섹션의 초기값은 플래시(LMA)에 저장되고, 스타트업 코드가 RAM(VMA)으로 복사합니다. 링커 스크립트에서 > RAM AT> FLASH로 표현하고, LOADADDR(.data)로 LMA를 가져옵니다.
링커 스크립트에서 KEEP(*(.isr_vector))가 필요한 이유는?
GCC의 --gc-sections 옵션을 활성화하면 링커는 참조되지 않는 섹션을 제거합니다. 인터럽트 벡터 테이블은 C 코드가 아닌 하드웨어에서 참조하므로 링커가 불필요하다고 판단해 제거할 수 있습니다. KEEP()으로 감싸면 가비지 컬렉션에서 제외되어 항상 바이너리에 포함됩니다.
PROVIDE(symbol = value)는 어떤 역할을 하나요?
해당 심볼이 다른 곳에서 아직 정의되지 않은 경우에만 심볼을 제공합니다. _stack_size나 _heap_size처럼 스타트업 코드가 기대하는 기본값을 설정할 때 유용합니다. C 코드나 다른 오브젝트 파일에서 같은 심볼이 정의되어 있으면 PROVIDE 정의는 조용히 무시됩니다.
펌웨어의 플래시와 RAM 사용량을 어떻게 확인하나요?
GCC 링커 플래그에 -Wl,--print-memory-usage를 추가하면 빌드 후 영역별 사용량 요약이 출력됩니다. arm-none-eabi-size firmware.elf로 text(코드+rodata), data(초기화 변수), bss(0 초기화 변수) 크기를 확인할 수 있습니다. -Wl,-Map=output.map으로 .map 파일을 생성하면 오브젝트 파일별 기여도를 자세히 분석할 수 있습니다.
스택을 8바이트 정렬해야 하는 이유는?
ARM AAPCS(프로시저 호출 표준)는 공개 함수 경계에서 스택 포인터가 8바이트 정렬되어야 한다고 요구합니다. 정렬이 맞지 않으면 64비트 데이터 타입이나 NEON/FPU 명령어 처리 시 하드 폴트가 발생할 수 있습니다. 링커 스크립트에서 . = ALIGN(8)로 스택 영역 전 정렬을 보장합니다.
MPU 정렬 요구사항은 무엇인가요?
ARM Cortex-M MPU는 보호 영역의 크기와 동일한 2의 거듭제곱 경계로 자연 정렬되어야 합니다. 예를 들어 256바이트 권한 데이터 영역은 256의 배수 주소에서 시작해야 합니다. 링커 스크립트에서 ALIGN(256)으로 이 요구사항을 강제합니다.
바이너리 크기를 줄이려면 디버그 섹션을 어떻게 제거하나요?
링커 스크립트에 /DISCARD/ 섹션을 추가하고 *(.ARM.exidx*), *(.comment), *(.debug_*) 등의 패턴을 나열합니다. 예외 언와인딩 테이블과 디버그 정보가 제거되어 릴리스 빌드의 플래시 사용량을 줄일 수 있습니다.