링커 스크립트 설명: 베어 메탈에서 메모리 레이아웃 제어

발행: (2025년 12월 13일 오후 05:23 GMT+9)
9 min read
원문: Dev.to

Source: Dev.to

최소 링크 스크립트의 구조

링크 스크립트는 두 개의 주요 블록, MEMORYSECTIONS 로 구성됩니다.

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 블록은 사용 가능한 주소 영역과 그 속성을 선언합니다.

NameAttributesORIGINLENGTH
FLASHrx0x0000000064M
RAMrwx0x60000000128M
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) 등을 통해 레이아웃을 검증하면 장치를 플래시하기 전에 올바른 배치를 확신할 수 있습니다.

Back to Blog

관련 글

더 보기 »