링커 스크립트 설명: 베어 메탈에서 메모리 레이아웃 제어
Source: Dev.to
최소 링크 스크립트의 구조
링크 스크립트는 두 개의 주요 블록, MEMORY와 SECTIONS 로 구성됩니다.
ENTRY(_start)
MEMORY
{
RAM (rwx) : ORIGIN = 0x60000000, LENGTH = 128M
}
SECTIONS
{
. = 0x60000000;
.text : {
*(.text)
} > RAM
}
각 구성 요소는 고유한 목적을 가집니다.
ENTRY 지시자
ENTRY(_start)는 프로그램의 진입점 심볼을 지정합니다. ELF 파일이 로드될 때, 이 심볼의 주소가 디버거와 로더가 인식하는 시작점이 됩니다. 베어‑메탈 코드에서는 _start가 리셋 벡터가 제어를 넘긴 뒤 실행되는 첫 번째 명령과 일치해야 합니다.
Note: QEMU는
-kernel옵션을 사용할 때 vexpress‑a9에 대한 아키텍처 리셋 벡터를 읽지 않으며, CPU의 PC를 ELF 진입점 주소로 설정합니다.
MEMORY 블록
MEMORY 블록은 사용 가능한 주소 영역과 그 속성을 선언합니다.
| Name | Attributes | ORIGIN | LENGTH |
|---|---|---|---|
| FLASH | rx | 0x00000000 | 64M |
| RAM | rwx | 0x60000000 | 128M |
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 64M
RAM (rwx): ORIGIN = 0x60000000, LENGTH = 128M
}
속성은 권고 사항이며, 쓰기 가능한 섹션이 읽기 전용 영역에 배치될 경우 링커가 경고합니다.
SECTIONS 블록
SECTIONS 블록은 최종 바이너리의 메모리 레이아웃을 지정합니다. 각 항목은 입력 섹션(오브젝트 파일)과 출력 섹션(최종 바이너리)을 매핑하고 메모리 위치를 할당합니다.
SECTIONS
{
.text : {
*(.text)
} > FLASH
.rodata : {
*(.rodata*)
} > FLASH
}
.text : { … }은.text라는 이름의 출력 섹션을 정의합니다.*(.text)은 모든 오브젝트 파일의.text입력 섹션을 포함합니다.> FLASH은 출력 섹션을FLASH메모리 영역에 배정한다는 뜻입니다.
와일드카드(*)는 모든 입력 파일에 매치되며, startup.o(.text) 와 같이 더 구체적인 패턴도 사용할 수 있습니다.
위치 카운터: .
링커는 위치 카운터 라는 암시적 변수를 점(.)으로 표시합니다. 이는 메모리 내 현재 위치를 추적하며 섹션이 배치될 때 자동으로 증가합니다.
SECTIONS
{
. = 0.60000000; /* 위치 카운터 설정 */
.text : {
*(.text)
} > RAM /* .text 가 0x60000000에 배치 */
.rodata : {
*(.rodata*)
} > RAM /* .rodata 가 .text 뒤에 배치 */
}
위치 카운터로부터 심볼을 만들 수도 있습니다:
_text_start = .; /* .text 시작 주소 */
.text : { *(.text) } > RAM
_text_end = .; /* .text 끝 주소 */
이러한 심볼은 실제 저장 공간을 차지하지 않으며, 어셈블리나 C 코드에서 주소로만 사용됩니다.
VMA vs LMA: 가상 메모리 주소와 로드 메모리 주소
각 출력 섹션은 두 개의 주소를 가집니다:
- VMA (Virtual Memory Address): 실행 중 섹션이 위치하는 주소.
- LMA (Load Memory Address): 섹션이 처음 저장되는 주소(보통 비휘발성 플래시).
간단한 경우 VMA와 LMA는 동일합니다. .data 와 같은 섹션은 초기 내용이 플래시(LMA)에 저장되지만 실행 시 RAM(VMA)에서 동작합니다. 시작 코드가 플래시에서 RAM으로 데이터를 복사합니다.
.data : {
*(.data)
} > RAM AT > FLASH /* VMA는 RAM, LMA는 FLASH */
> RAM 은 VMA를, AT > FLASH 은 LMA를 지정합니다.
링커 정의 심볼
링커는 위치 카운터나 기타 표현식을 이름에 할당함으로써 심볼을 만들 수 있습니다. 이러한 심볼은 주소만을 갖습니다.
SECTIONS
{
. = 0x60000000;
_text_start = .; /* .text 시작 주소 */
.text : { *(.text) } > RAM
_text_end = .; /* .text 끝 주소 */
_stack_top = ORIGIN(RAM) + LENGTH(RAM); /* 사용자 정의 심볼 */
}
어셈블리에서는 = 의사연산자를 사용해 참조합니다:
ldr r0, =_text_start @ 심볼 주소를 r0에 로드
ldr sp, =_stack_top @ 스택 최상위 주소를 sp에 로드
이 의사연산자는 리터럴 풀을 이용한 LDR 명령을 생성하며, 링커가 링크 시점에 심볼 주소를 해결합니다.
맵 파일
링커는 -Map=output.map 옵션으로 맵 파일을 생성할 수 있습니다. 이 파일에는 섹션, 심볼, 메모리 영역의 배치가 상세히 기록됩니다. 섹션이 기대한 위치에 배치됐는지, 크기 초과가 없는지 확인하는 데 매우 유용합니다.
검증: 링커가 만든 결과 확인
단계 1: 디스어셈블리 확인
arm-none-eabi-objdump -d firmware.elf
진입점(_start)을 찾아보고 코드가 의도한 주소(예: 0x60000000)에 존재하는지 확인합니다.
단계 2: 섹션 헤더 확인
arm-none-eabi-objdump -h firmware.elf
출력에는 각 섹션의 VMA, LMA, 크기 및 소속 메모리 영역이 표시됩니다.
단계 3: 맵 파일 확인
firmware.map 파일을 열어 _text_start, _text_end, _stack_top 같은 심볼을 검색합니다. 주소가 스크립트에 정의된 레이아웃과 일치하는지 확인합니다.
검증: QEMU 로드 후 GDB 로 검사
qemu-system-arm -M vexpress-a9 -kernel firmware.elf -nographic -S -s
다른 터미널에서:
arm-none-eabi-gdb firmware.elf
(gdb) target remote :1234
(gdb) info files # 로드된 섹션과 진입점 표시
(gdb) break _start
(gdb) continue
PC가 ENTRY(_start) 로 정의된 주소에서 시작되는지, 메모리 영역에 기대한 데이터가 들어 있는지 확인합니다.
정렬 지시자
링크 스크립트는 아키텍처 제약을 만족시키기 위해 정렬 지시자를 지원합니다:
.text ALIGN(4) : { *(.text) } > FLASH
ALIGN(4) 은 .text 시작을 4바이트 경계에 맞춥니다. 위치 카운터 자체를 정렬할 수도 있습니다:
. = ALIGN(0x1000); /* 다음 4 KB 경계로 정렬 */
올바른 정렬은 워드 정렬 접근을 요구하는 CPU에서 발생할 수 있는 오류를 방지합니다.
결론
링크 스크립트를 사용하면 베어‑메탈 시스템에서 코드와 데이터가 정확히 어디에 위치할지 결정적으로 제어할 수 있습니다. 메모리 영역을 정의하고, 위치 카운터를 활용하며, VMA/LMA 를 지정하고, 링커 정의 심볼을 만들면 하드웨어가 요구하는 메모리 맵을 정확히 구현할 수 있습니다. 디스어셈블리, 섹션 헤더, 맵 파일, 런타임 검사(QEMU + GDB) 등을 통해 레이아웃을 검증하면 장치를 플래시하기 전에 올바른 배치를 확신할 수 있습니다.