U-Boot v2026.07 · AArch64 · verified source walk

ARM64 부팅 분석: reset부터 재배치·BSS까지

Boot ROM이나 이전 firmware가 U-Boot로 분기한 순간에는 U-Boot의 C 함수가 사용할 stack, global data, BSS가 아직 보장되지 않습니다. generic ARMv8 entry는 실행 주소를 점검하고, 현재 Exception Level에 맞는 vector base와 system register를 설정한 뒤, primary CPU만 _main으로 보냅니다. 이어서 stack과 global data를 만들고, 실행 중인 자신을 다른 주소로 복사합니다. 같은 주소 예제를 유지하면서 명령어가 바꾸는 레지스터와 메모리, 다음 단계가 그 결과를 사용하는 이유를 추적합니다.

Baseline
U-Boot v2026.07
Primary source
arch/arm/cpu/armv8/start.S
Entry
_start / reset
Handoff
_main → relocate_code → board_init_r

핵심 질문

AArch64 CPU가 U-Boot의 첫 명령에 도달한 뒤 C 초기화 함수로 들어가기 전까지 무엇을 보장해야 할까요? 답은 단순히 reset vector에서 main으로 점프한다가 아닙니다. 실행 위치, Exception Level, vector base, cache와 coherency, multi-core 역할, incoming register의 소유권을 순서대로 확정해야 합니다.

ARM32 vector table과 ARM64 entry는 다릅니다

AArch64 entry의 원문은 arch/arm/lib/vectors.S의 ARM32 .word vector table이 아닙니다. ARM32의 software-interrupt, prefetch-abort, FIQ label은 이 ARM64 reset 경로의 근거가 될 수 없습니다. 분석 기준 파일은 arch/arm/cpu/armv8/start.S입니다.

1. 쉬운 모델: 아직 운영체제도, 평범한 프로그램도 아니다

reset 직후 코드는 호텔 로비가 아니라 공사 현장 입구에 가깝습니다. stack이 없으니 C 지역 변수를 쓸 수 없고, BSS가 지워졌다는 보장도 없으며, CPU가 EL3·EL2·EL1 중 어디에서 시작했는지도 platform contract에 달려 있습니다. 초기 assembly는 앞으로 사용할 규칙 자체를 먼저 만듭니다.

PC와 image

ROM이 실제로 적재한 주소와 linker가 가정한 주소를 구분합니다. position-independent build는 relocation을 직접 고칩니다.

Exception Level

switch_el이 현재 EL에 맞는 VBAR와 control register 경로를 선택합니다.

CPU 역할

primary CPU는 초기화를 진행하고 secondary CPU는 spin table이나 parking protocol에서 release를 기다릴 수 있습니다.

C 경계

_main으로 갔다고 바로 board_init_f()가 아닙니다. crt0_64.S가 stack과 global data를 먼저 준비합니다.

ROM / SPL / TF-Aimage 검증·적재·분기
platform마다 계약이 다름
start.SPC 보정·EL별 register·vector
CPU 역할 분리
crt0_64.S · _mainSP·x18/gd → board_init_f
재배치·BSS → board_init_r
그림 1. 앞 stage의 stack이 존재하더라도 U-Boot가 계속 사용해도 된다는 계약은 별도다. _main도 C 함수가 아니라 C 환경을 만드는 assembly다.근거: start.S:19–208, crt0_64.S:18–64.

2. reset entry가 받는 것과 아직 보장하지 않는 것

항목entry에서의 의미검증 방법
PCROM·TF-A·이전 stage가 선택한 실제 실행 주소debugger PC, image header의 load/entry, ELF symbol을 함께 비교
CurrentELEL3, EL2, EL1 중 platform이 넘긴 현재 수준mrs xN, CurrentEL 또는 debugger system register
x0–x3board/previous-stage contract에 따른 boot parameterSoC boot protocol과 board의 save_boot_params override 확인
SPgeneric entry가 신뢰하는 C stack이 아님_main의 초기 stack 계산과 SRAM 범위 확인
VBAR_ELx이전 firmware 값일 수 있음새 vectors 주소를 쓴 시점 전후로 읽기
MMU/cachegeneric source 주석은 little-endian, MMU와 I/D cache disabled를 초기 상태로 둠SCTLR_ELx, platform handoff 문서, 실제 register dump 대조

incoming x0를 무조건 Linux DTB pointer라고 부르면 안 됩니다.

Linux arm64 boot protocol에서는 kernel 진입 x0가 DTB 주소이지만, 지금은 U-Boot가 받는 단계입니다. x0–x3의 의미는 ROM, TF-A, SPL, board override 계약에 달려 있습니다. U-Boot가 나중에 Linux로 넘길 x0와 이 entry의 x0는 시간도 소유자도 다릅니다.

3. 전체 흐름

  1. _start 분기 형식 선택Linux image header, SoC boot0 hook, 일반 b reset 중 build configuration에 맞는 시작 형식을 사용합니다.
  2. boot parameter 보존 기회reset은 save_boot_params로 분기합니다. weak 구현도 BLOBLIST 설정에서는 x0–x3를 저장하며, board가 override할 수 있습니다.
  3. position-independent fixupruntime _start와 linked text base의 차이를 구해 .rela.dyn의 relative relocation을 고칩니다.
  4. 현재 EL별 CPU stateswitch_el로 VBAR_EL3/2/1과 FP/SIMD 접근, SCR/HCR/CPACR 계열 상태를 맞춥니다.
  5. errata와 low-level initcore별 workaround와 GIC/coherency 등 processor/platform low-level path를 실행합니다.
  6. primary CPU 선별secondary CPU는 spin/parking protocol로 빠지고 primary만 계속 진행합니다.
  7. C runtime 경계로 이동primary는 SPsel을 정하고 bl _main으로 들어갑니다. stack과 gd_t 준비는 다음 파일의 책임입니다.

3.1 하나의 image를 끝까지 따라가기 위한 조건

설정마다 달라지는 경로를 하나의 실행처럼 섞으면 주소 계산이 맞지 않습니다. 중심 예제는 U-Boot proper, AArch64, PIE 활성, 일반 b reset entry, EL2(E2H=0), primary CPU, 실제 재배치 수행으로 고정합니다. SPL/XPL, EL3, secondary와 skip-relocation은 해당 지점에서 갈라지는 별도 경로로 설명합니다.

다음 주소는 특정 보드의 dump가 아니라 원문 계산을 손으로 따라가기 위한 가상 메모리 배치입니다. L은 ELF에 기록된 링크 기준, R은 지금 실행 중인 image, D는 board_init_f()가 정한 목적지입니다. 세 영역이 접근 가능하고 복사 영역끼리 겹치지 않는다고 가정합니다. 실제 장비에서는 같은 칸을 ELF/map과 debugger 값으로 바꿉니다.

링크 기준 L

0x40000000

+0x0000 · _start
+0x0800 · vectors
+0x4100 · relocation_return
+0x8000 · pointer 저장 위치
+0x12000 · pointer의 대상

현재 image R

0x40200000

0x40200000 · 실행 시작
0x40200800 · 첫 VBAR
0x40204100 · 이전 복귀 위치
0x40208000 → 0x40212000
0x40212000 · 현재 대상

새 image D

0x48000000

0x48000000 · 복사 시작
0x48000800 · 새 VBAR
0x48004100 · 새 복귀 위치
0x48008000 → 0x48012000
0x48012000 · 새 대상
그림 2. 주소 지도. symbol의 image 내부 offset은 같고 base만 바뀐다. 마지막 줄의 relocation 입력과 BSS가 같은 offset을 갖는 이유는 linker overlay에서 확인한다. 그림은 주소 관계를 나타내며 영역 크기는 축척이 아니다.원문: u-boot.lds:142–166. 숫자 주소는 설명용 배치.

이 예제의 복사 구간은 [base, base+0x30000), relocation 항목은 24바이트짜리 2개, BSS는 [base+0x30000, base+0x30100)로 둡니다. vectors의 0x800 정렬과 image base의 0x1000 정렬을 함께 만족시켰습니다. sizeof(gd_t)와 각 GD 필드 offset은 이 지도에서 정하지 않습니다. 그것은 빌드 설정에 따라 결정됩니다.

assembly 표기부터 구분한다

표기읽는 방법이 글에서의 차이
adr x0, labelPC-relative로 label의 주소를 계산현재 R에서 실행하면 R 기준 주소를 얻음
ldr x1, _TEXT_BASElabel에 놓인 64-bit 값을 읽음label의 주소가 아니라 .quad CONFIG_TEXT_BASE에 든 L을 읽음
[x2], #16기존 x2 주소에서 접근한 뒤 x2에 16을 더함post-index: relocation cursor가 앞으로 이동
[sp, #-16]!sp에서 16을 빼고 그 새 주소에 접근pre-index: stack에 아래 방향으로 저장
1f / 0b앞쪽의 다음 1: / 뒤쪽의 직전 0:숫자 label은 파일 안에서 재사용 가능
b / bl / ret / eret분기 / LR을 남기는 분기 / LR로 복귀 / 예외 복귀이 네 가지를 모두 “함수 호출”로 읽지 않음

3.2 첫 분기에서는 stack 대신 label이 복귀 위치를 정한다

reset은 bl save_boot_params가 아니라 b save_boot_params입니다. 따라서 x30(LR)에 복귀 주소를 새로 만들지 않습니다. weak 구현 끝도 ret이 아니라 b save_boot_params_ret입니다. 두 label을 직접 이어 둔 것이므로 이전 firmware가 LR에 무엇을 남겼는지는 이 왕복에 필요하지 않습니다.

start.S:387–396 · weak 기본 구현 전체

WEAK(save_boot_params)
#if (IS_ENABLED(CONFIG_BLOBLIST))
	/* Calculate the PC-relative address of saved_args */
	adrp	x9, saved_args
	add	x9, x9, :lo12:saved_args
	stp 	x0, x1, [x9]
	stp 	x2, x3, [x9, #16]
#endif
	b	save_boot_params_ret	/* back to my caller */
ENDPROC(save_boot_params)
진입 x0, x1, x2, x3앞 stage가 정한 값
x30은 이 왕복에 사용하지 않음
BLOBLIST 사용 시saved_args +0: x0
+8: x1 · +16: x2 · +24: x3
save_boot_params_ret이후 x0는 정렬 검사에 사용
x0–x3의 보존을 기대하지 않음
그림 3. 인자 소유권의 이동. x9가 가리키는 32바이트는 start.S:398–406의 .data에 존재한다. 일반 C 코드의 BSS 사용 허가가 아니라 초기 assembly가 명시적으로 관리하는 예외적인 저장 공간이다.원문: start.S:387–406.

BLOBLIST가 꺼져 있고 board override도 없다면 저장 자체를 하지 않습니다. 반대로 켜져 있으면 stp 두 번이 실제 메모리를 쓰므로 image의 해당 data 영역이 writable이어야 합니다. 또한 이 코드의 ADRP/lo12 사용은 뒤의 정렬 검사보다 먼저입니다. 정렬 오류가 발견된다고 해서 “그때까지 아무 메모리도 건드리지 않았다”는 뜻은 아닙니다.

뒤의 bl apply_core_errata와 bl lowlevel_init은 다른 계약입니다. 각각 LR을 x29에 보관하고 끝에서 LR을 복구합니다(start.S:212–224, 301–357). stack에 push하지 않고 register를 임시 보관소로 쓰는 것입니다. 이 구간에 임의의 C 호출을 끼우면 호출 대상의 stack 사용과 x29 보존 여부까지 깨질 수 있습니다. bl 명령 자체가 stack을 만들거나 인자를 자동 저장해 주지는 않습니다.

4. v2026.07 원문으로 확인하기

진입 형식과 reset branch · start.S 19–59, 일부 생략

.globl _start

_start:
#if defined(CONFIG_LINUX_KERNEL_IMAGE_HEADER)
#include <asm/boot0-linux-kernel-header.h>
#elif defined(CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK)
#include <asm/arch/boot0.h>
#else
	b reset
#endif

.align 3
.globl _TEXT_BASE
_TEXT_BASE:
	.quad CONFIG_TEXT_BASE
...
reset:
	/* Allow the board to save important registers */
	b	save_boot_params

_start가 언제나 첫 명령에서 같은 reset label로 가는 것은 아닙니다. image header와 SoC hook은 ROM이 요구하는 배치와 선행 명령을 삽입할 수 있습니다. disassembly에서 실제 _start를 확인해야 하는 이유입니다.

4 KiB 정렬 검사와 PIE relocation · start.S 59–105, 일부 생략

save_boot_params_ret:
#if CONFIG_POSITION_INDEPENDENT && !defined(CONFIG_XPL_BUILD)
	/* Verify that we're 4K aligned. */
	adr	x0, _start
	ands	x0, x0, #0xfff
	b.eq	1f
0:
	wfi
	b	0b
1:
	...
pie_fixup:
	adr	x0, _start        /* Runtime value of _start */
	ldr	x1, _TEXT_BASE    /* Linked value of _start */
	subs	x9, x0, x1       /* Run-vs-link offset */
	beq	pie_fixup_done
	...
	str	x4, [x0]
#endif

v2026.07은 ADRP + ADD의 low 12-bit relocation 조건 때문에 position-independent U-Boot가 4 KiB 정렬 주소에 적재됐는지 먼저 확인합니다. 어긋나면 console도 stack도 없는 시점이라 wfi loop로 멈춥니다. UART가 조용한 것이 곧 `_start` 미도달을 뜻하지 않는 대표 사례입니다.

현재 Exception Level별 vector와 control state · start.S 107–139, 일부 생략

#if defined(CONFIG_ARMV8_SPL_EXCEPTION_VECTORS) || !defined(CONFIG_XPL_BUILD)
.macro set_vbar, regname, reg
	msr \regname, \reg
.endm
	adr	x0, vectors
#else
.macro set_vbar, regname, reg
.endm
#endif

switch_el x1, 3f, 2f, 1f
3:	set_vbar vbar_el3, x0
	mrs	x0, scr_el3
	orr	x0, x0, #0xf
	msr	scr_el3, x0
	msr	cptr_el3, xzr
	b	0f
2:	mrs	x1, hcr_el2
	tbnz	x1, #HCR_EL2_E2H_BIT, 1f
	orr	x1, x1, #HCR_EL2_AMO_EL2
	msr	hcr_el2, x1
	set_vbar vbar_el2, x0
	...
	b	0f
1:	set_vbar vbar_el1, x0
	...
0:

현재 EL을 확인하고, EL2에서는 HCR_EL2.E2H까지 확인해 register 설정 경로를 고릅니다. build에서 exception vector를 제외한 XPL stage라면 set_vbar macro는 빈 동작입니다. 따라서 source에 VBAR write가 보인다는 사실과 실제 빌드 image에 그 명령이 있다는 사실을 구분해야 합니다.

primary CPU만 _main으로 · start.S 165–208, ACPI parking 경로 등 일부 생략

bl	apply_core_errata
bl	lowlevel_init

#if defined(CONFIG_ARMV8_SPIN_TABLE) && !defined(CONFIG_XPL_BUILD)
	branch_if_master x0, master_cpu
	b	spin_table_secondary_jump
#elif defined(CONFIG_ARMV8_MULTIENTRY)
	branch_if_master x0, master_cpu
slave_cpu:
	wfe
	ldr	x1, =CPU_RELEASE_ADDR
	ldr	x0, [x1]
	cbz	x0, slave_cpu
	br	x0
#endif

master_cpu:
	msr	SPSel, #1       /* make sure we use SP_ELx */
	bl	_main

bl 자체는 stack을 요구하지 않지만 호출되는 assembly가 사용할 register 계약은 지켜야 합니다. primary가 _main으로 가기 직전에 SP_ELx를 선택하고, 실제 stack 주소와 global data pointer 구성은 arch/arm/lib/crt0_64.S가 담당합니다.

4.1 링크 주소, 적재 주소, 재배치 주소를 분리해서 계산한다

linker가 가정한 image base를 L, 이전 firmware가 실제 적재한 base를 R, 이후 DRAM으로 옮길 목적지를 D라고 부르겠습니다. start.S의 PIE fixup은 R에서 동작하도록 일부 absolute pointer를 고칩니다. 뒤의 relocate_code는 D로 image를 옮깁니다. 둘을 모두 relocation이라고 부르더라도 시점과 목적이 다릅니다.

L · 0x40000000ELF 링크 기준
pointer slot: L+0x8000
R · 0x40200000처음 실행하는 위치
PIE delta = R − L
D · 0x48000000board_init_f가 정한 목적지
후속 relocation에서 사용
relative relocation 입력PIE가 쓰는 주소그곳에 쓸 값
r_offset = 0x40008000
addend = 0x40012000
type = 1027
r_offset + (R−L)
= 0x40208000
addend + (R−L)
= 0x40212000
그림 4. pointer 내용뿐 아니라 pointer를 저장할 위치도 고친다. 위 주소는 계산 검증용 가상 배치이며 특정 SoC의 memory map이 아니다.근거: start.S:83–105의 relative relocation loop. type 1027(R_AARCH64_RELATIVE)만 이 경로에서 처리.

relative relocation 한 항목의 실제 처리 · start.S:92–104, 주석 생략

pie_fix_loop:
	ldp	x0, x1, [x2], #16
	ldr	x4, [x2], #8
	cmp	w1, #1027
	bne	pie_skip_reloc
	add	x0, x0, x9
	add	x4, x4, x9
	str	x4, [x0]
pie_skip_reloc:
	cmp	x2, x3
	b.lo	pie_fix_loop

항목은 offset, info, addend의 세 64-bit 값입니다. 첫 load는 16바이트, 다음 load는 8바이트만큼 cursor를 옮기므로 다음 항목으로 24바이트 진행합니다. type이 다르면 store를 생략하지만 cursor는 이미 다음 항목을 가리킵니다. R=L이면 delta가 0이므로 loop 자체를 생략합니다. 모든 ELF relocation type을 해결하는 일반 dynamic linker의 동작과는 범위가 다릅니다.

4 KiB 정렬도 같은 맥락입니다. ADRP는 page 기준을 만들고 ADD :lo12:는 링크 시 계산된 하위 12비트를 더합니다. image를 page 안에서 어긋나게 옮기면 이 조합의 전제가 깨집니다. entry의 _start & 0xfff 검사는 그 전에 정지시킵니다. PC가 그 WFI loop 안에 있다면 이미 U-Boot entry에 도착해 배치 오류를 검출한 것입니다.

relocation 이후 return 주소는 왜 두 번 보정하는가

crt0_64.S에서 adr lr, relocation_return은 R 기준 주소를 얻습니다. PIE 경로는 여기에 L−R을 더해 링크 기준으로 되돌린 다음 gd->reloc_off인 D−L을 더합니다. 합치면 lr_new = lr_runtime + (D−R)입니다. 초반 PIE loop의 x9는 R−L인데 이 구간의 x9는 L−R이므로 이름만 보고 같은 부호라고 추정하면 돌아갈 주소가 어긋납니다.

진입 상태검사/분기원문에서 도달하는 설정
EL3switch_el → 3:VBAR_EL3, SCR_EL3, CPTR_EL3
EL2, E2H=02: → HCR 설정VBAR_EL2, CPTR_EL2
EL2, E2H=1tbnz → 1:VBAR_EL1 / CPACR_EL1 명령 경로
EL1switch_el → 1:VBAR_EL1, CPACR_EL1
그림 5. 코드 분기는 EL 자체를 낮추는 명령이 아니다. E2H=1 경로의 register 의미는 VHE의 alias 규칙과 함께 읽어야 한다.근거: start.S:107–139. XPL에서 set_vbar macro가 비어 있는 조건은 별도 적용.

msr daifclr, #0x4는 SError mask를 내리는 명령입니다. 이를 모든 interrupt enable로 번역하면 IRQ/FIQ까지 해제했다고 오해합니다. 또한 vector 설치가 stack의 유효성을 보장하지는 않습니다. 예외 entry가 SP에 register를 저장할 때 사용할 메모리가 준비되어 있어야 진단 출력까지 갈 수 있습니다.

4.2 PIE loop 한 바퀴를 레지스터 값으로 실행해 본다

start.S:84–91에서 x9는 R−L = 0x00200000, x2는 R+0x30000 = 0x40230000, x3는 두 항목 뒤인 0x40230030이 됩니다. 첫 relocation 항목을 다음처럼 두겠습니다. 두 번째는 type 검사에서 건너뛰는 동작을 관찰하기 위한 가상 비-relative 항목입니다. 이것이 실제 U-Boot 산출물에서 허용되는 relocation 유형이라는 뜻은 아닙니다.

+0x00 · r_offset0x40008000고칠 slot의 링크 주소 · 8바이트
+0x08 · r_info0x0000000000000403하위 32비트 type=1027 · 8바이트
+0x10 · r_addend0x40012000새 pointer 값 계산의 기준 · 8바이트

cursor x2: 0x40230000 → 0x40230010 → 0x40230018. 첫 ldp가 두 칸, 다음 ldr가 한 칸을 읽는다.

그림 6. 24바이트 relocation record. CPU register와 별개로 메모리에 놓인 입력 데이터다. x0에 들어가는 것은 record 자체의 위치가 아니라 record에 적힌 r_offset이다.원문: start.S:88–100. 값은 설명용 첫 record.
원문 명령실행 직후 바뀐 값그 값이 필요한 이유
ldp x0, x1, [x2], #16x0=0x40008000
x1=0x403
x2=0x40230010
쓰기 대상의 링크 주소와 relocation 종류를 읽음. 아직 대상 메모리에 쓰지 않음.
ldr x4, [x2], #8x4=0x40012000
x2=0x40230018
addend를 읽고 다음 record로 cursor 이동.
cmp w1, #1027
bne pie_skip_reloc
0x403=1027이므로 Z=1
이번 분기는 하지 않음
w1은 x1의 하위 32비트다. 이 비교는 x1 값을 바꾸지 않고 flags만 바꿈.
add x0, x0, x9x0=0x40208000링크 주소를 현재 image의 실제 slot 주소로 변환.
add x4, x4, x9x4=0x40212000slot에 넣을 pointer도 현재 image를 가리키도록 변환.
str x4, [x0]M64[0x40208000]=0x40212000이때 비로소 메모리가 바뀜. record의 r_offset/r_addend는 그대로.
cmp x2, x3
b.lo pie_fix_loop
0x40230018 < 0x40230030unsigned 주소 비교 결과 아래이므로 다음 항목을 읽음.

두 번째 항목의 type이 1027이 아니면 bne가 taken되어 두 add와 str을 건너뜁니다. 그래도 두 load가 먼저 끝났으므로 x2는 0x40230030으로 진행했습니다. 마지막 cmp는 같음이 되어 b.lo가 taken되지 않습니다. “지원하지 않는 항목을 skip한다”와 “그 항목의 relocation을 해결했다”는 전혀 다릅니다.

기존 pointer에 delta를 더하지 않는 이유

이 loop는 ldr x4, [고칠 slot]로 기존 pointer를 읽지 않습니다. record의 addend를 읽어 새 값을 만듭니다. 현재 slot이 무엇이었는지와 관계없이 M64[r_offset+delta] = addend+delta로 덮어씁니다. 이 사실이 뒤의 두 번째 재배치에서도 중요합니다. 앞에서 한 번 고친 pointer에 무작정 delta를 또 더하는 방식이 아닙니다.

잘못된 image에는 어떤 일이 생기는가

이 코드는 범용 ELF 검증기가 아닙니다. loop 앞에서 table 크기, r_offset의 쓰기 가능 범위, type의 전체 지원 여부를 검증하지 않습니다. 첫 load도 loop 끝의 경계 검사보다 앞에 있습니다. 따라서 손상된 relocation table이 안전한 오류 메시지로 끝나리라 기대하면 안 됩니다. ROM의 image 검증과 linker가 만든 정상 산출물이라는 전제를 별도로 지켜야 합니다.

정렬 검사의 ands x0, x0, #0xfff는 x0 자체를 하위 12비트로 바꾸고 flags도 바꿉니다. 예제 R=0x40200000이면 x0=0, Z=1이라 통과합니다. R=0x40200100이면 x0=0x100, Z=0이라 WFI loop에 남습니다. 통과한 뒤 adr x0, _start를 다시 하는 이유는 검사 과정에서 원래 base가 x0에서 사라졌기 때문입니다.

4.3 EL을 알아내는 분기와 EL을 바꾸는 동작은 다르다

switch_el이라는 이름만 보면 EL을 전환하는 함수처럼 보이지만, 실제로는 다음 assembly macro입니다. CurrentEL을 읽어 비교할 뿐이며 eret도 privilege 전환도 없습니다.

macro.h:70–76 · switch_el 정의 전체

.macro	switch_el, xreg, el3_label, el2_label, el1_label
	mrs	\xreg, CurrentEL
	cmp	\xreg, #0x8
	b.gt	\el3_label
	b.eq	\el2_label
	b.lt	\el1_label
.endm

이 코드가 전제로 하는 EL1/2/3의 값은 각각 0x4/0x8/0xc입니다. 예제는 EL2이므로 x1=0x8, cmp 뒤 Z=1, b.eq 2f가 선택됩니다. 이어 mrs x1, hcr_el2가 x1을 덮어쓰고 E2H bit를 검사합니다. x1은 “EL을 영구 보관하는 변수”가 아니라 그때 필요한 값을 담는 scratch register입니다.

명령/설정변하는 것변하지 않거나 별도로 필요한 것
set_vbar vbar_el2, x0x0=R+vectors offset인 예제에서는 VBAR_EL2=0x40200800handler용 stack, gd, UART까지 준비되는 것은 아님.
orr x1, x1, #HCR_EL2_AMO_EL2기존 HCR의 다른 비트를 유지하면서 AMO를 설정SError routing 설정과 PSTATE의 마스크 해제는 별개.
mov x0, #0x33ff
msr cptr_el2, x0
이 E2H=0 경로의 FP/SIMD trap 제어값을 기록벡터 레지스터 내용을 초기화하는 코드는 아님. 다른 접근 제어·상위 EL trap까지 모두 없앤다는 뜻도 아님.
msr daifclr, #0x4PSTATE.A, 즉 SError mask를 해제IRQ/FIQ mask를 모두 해제하는 명령이 아님.
msr SPSel, #1현재 EL의 SP_ELx를 선택SP_ELx에 유효한 RAM 주소를 넣는 작업은 아직 _main의 책임.

EL3 경로의 SCR_EL3 |= 0xf 역시 SCR의 NS/IRQ/FIQ/EA 비트를 설정하는 것이지 즉시 Non-secure EL2로 내려가는 명령은 아닙니다. EL2의 E2H=1 분기가 EL1 이름의 register 명령으로 가는 것도 VHE access 규칙에 따른 경로입니다. 이후 c_runtime_cpu_setup()은 CurrentEL에 따라 VBAR를 다시 기록하며, 첫 설정 코드와 완전히 같은 분기 구조는 아닙니다.

CONFIG_COUNTER_FREQUENCY가 있으면 무조건 CNTFRQ를 쓰는 것도 아닙니다. branch_if_not_highest_el은 EL2/EL1에서 ID_AA64PFR0_EL1의 상위 EL 구현 여부를 확인합니다(macro.h:81–94). 상위 EL이 구현되어 있는데 현재 더 낮은 EL이면 주파수 기록을 건너뜁니다. start.S:147의 ISB까지 마친 뒤 core-specific SMPEN/errata 경로로 이어집니다.

5. 예외 벡터는 무엇이며 두 번 설정될 수 있나

AArch64 vector table은 현재 EL에서 사용하는 SP, lower EL의 AArch64/AArch32 진입, synchronous exception, IRQ, FIQ, SError 조합을 고정 slot으로 나눕니다. VBAR_ELx는 table base를 가리키며 architecture alignment 조건을 만족해야 합니다. U-Boot의 vectors symbol과 handler 본체는 ARMv8 exception source에 있습니다.

reset 초반 설정

아직 relocation과 C runtime이 끝나기 전, 현재 실행 위치에서 접근 가능한 vectors를 VBAR_ELx에 씁니다.

c_runtime_cpu_setup()

relocation 이후 runtime 주소를 기준으로 adr x0, vectors를 다시 계산해 현재 EL의 VBAR를 갱신합니다.

예외가 났는데 엉뚱한 handler로 가면 ESR_ELx만 볼 것이 아니라 VBAR_ELx가 pre-relocation 주소인지, relocated address인지 먼저 확인해야 합니다. exception handler가 정상이어도 vector base가 오래된 주소면 원래 fault보다 더 이른 단계에서 디버깅이 끊깁니다.

VBAR 기준 offset · slot 간격 0x80, 전체 base 정렬 0x800

예외 발생 문맥SyncIRQFIQSError
현재 EL, SP_EL00x0000x0800x1000x180
현재 EL, SP_ELx0x2000x2800x3000x380
낮은 EL, AArch640x4000x4800x5000x580
낮은 EL, AArch320x6000x6800x7000x780
그림 7. architecture는 16개 slot을 정의하지만 이 U-Boot 파일은 현재 EL용 앞 8개만 구현한다. 초록 행은 SP_ELx 선택 뒤 현재 EL 예외의 entry다. 회색 행에는 이 파일의 handler가 없다.근거: exceptions.S:14–42, 142–165. offset = 문맥 번호×0x200 + 예외 종류×0x80.

EL2에서 SP_EL2를 사용하다 synchronous exception이 나면 해당 vector base의 +0x200 entry가 대상입니다. 그 entry는 x29/x30을 stack에 저장하고 공통 save routine을 호출한 다음 do_sync로 갑니다. SP가 잘못되어 첫 store부터 fault가 나면 원래 예외 정보를 출력하지 못할 수 있습니다. 그래서 무출력 fault는 ELR뿐 아니라 VBAR, SP, stack memory의 접근 가능성을 함께 확인합니다.

5.1 예외 한 번에 만들어지는 272바이트를 복원한다

VBAR에 주소를 쓰는 단계와 실제 예외 처리는 다릅니다. 예외 처리를 별도 가지로 따라가 보겠습니다. 예제 CPU가 EL2에서 SP_EL2를 사용하고, synchronous exception이 같은 EL로 전달되며, 예외 직전 SP를 S=0x47fef000이라고 하겠습니다. CPU는 ELR_EL2/SPSR_EL2 등 예외 상태를 기록하고 VBAR_EL2+0x200의 명령으로 갑니다. 일반 레지스터 전체를 RAM에 자동 저장해 주는 것은 아닙니다.

exceptions.S:143–147 · 현재 EL, SP_ELx의 synchronous entry

	.align	7		 /* Current EL (SP_ELx) Synchronous Handler */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_sync
	b	exception_exit

첫 stp가 SP를 S−16으로 내리고 기존 x29/x30을 저장합니다. 그다음 bl _exception_entry가 x30을 덮어쓰므로 저장 순서가 바뀌면 원래 LR을 잃습니다. _exception_entry는 x27/x28부터 x1/x2까지 14쌍을 추가 저장합니다. 여기까지 SP=S−240입니다. 이어 b _save_el_regs로 가므로 이번에는 LR을 덮어쓰지 않습니다.

_save_el_regs는 이미 저장한 x1–x3를 scratch로 재사용해 ESR/ELR/SPSR을 읽습니다. 아직 저장하지 않은 원래 x0는 유지하다가 stp x1, x0로 ESR과 함께 저장합니다. 마지막 stp x3, x2가 SPSR/ELR을 넣고, x0를 새 SP로 바꿉니다. 따라서 C 함수의 첫 인자는 원래 x0가 아니라 struct pt_regs *입니다.

저장 순서

① x29/x30 · 16 bytes

② x27/x28 … x1/x2 · 224 bytes

③ ESR / 원래 x0 · 16 bytes

④ SPSR / ELR · 16 bytes

합계 272 = 0x110 bytes
F = S−0x110
= 0x47feeef0

완성된 frame · 낮은 주소부터

F+0x018regs[0] · 원래 x0
F+0x020 … +0x0f8regs[1] … regs[28]
F+0x100regs[29]
F+0x108regs[30] · 원래 LR
F+0x110 = S예외 직전 SP 경계
그림 8. assembly가 만든 메모리와 C 구조체의 대응. push 순서를 역순으로 읽으면 spsr, elr, esr, regs[31]과 정확히 일치한다. C 함수가 추가로 쓰는 stack은 이 frame보다 아래쪽이다. Linux의 pt_regs 배치를 가져온 그림이 아니다.원문: exceptions.S:39–93, proc-armv/ptrace.h:96–101.

첫 번째 ret와 마지막 eret는 서로 다른 곳으로 간다

_save_el_regs 끝의 ret은 예외 전 프로그램으로 돌아가지 않습니다. bl _exception_entry가 LR에 남긴 위치, 즉 vector 안의 bl do_sync로 돌아옵니다. C 함수가 반환한다면 b exception_exit가 실행됩니다. exit는 frame의 ELR을 system register에 복원하고 GP register를 pop한 뒤 eret로 예외 전 문맥에 복귀합니다.

여기에는 중요한 제한이 있습니다. 이 버전의 exit는 저장된 SPSR word를 xzr로 버리고 SPSR_ELx를 frame에서 다시 쓰지 않습니다(exceptions.S:104–112). 따라서 C에서 regs->spsr만 바꾸면 복귀 PSTATE도 바뀐다고 생각하면 틀립니다. ELR은 frame에서 재설정되고, SPSR_ELx는 hardware register에 남은 값에 의존하는 경로입니다. 이 코드를 범용 nested-exception context switch로 해석해서도 안 됩니다.

그런데 do_sync는 보통 돌아오지 않는다

interrupts_64.c:195–206의 기본 do_sync()는 진단을 출력하고 panic()으로 갑니다. SEMIHOSTING_FALLBACK이 활성이고 해당 trap을 처리한 경우에만 먼저 반환합니다. 그 처리기는 frame의 x0를 −1로, ELR을 다음 명령 위치로 바꿉니다. 따라서 assembly에 복귀 코드가 있다는 사실을 “data abort도 고친 뒤 자동 재실행한다”로 설명하면 안 됩니다.

예외 직전 SP가 RAM 밖이면 첫 stp부터 실패할 수 있습니다. SP만 맞아도 아직 x18/gd나 console 준비 전이라면 C 진단 함수가 성공한다는 보장은 없습니다. 무출력 상태의 원인을 판단할 때에는 vector 도달, frame 저장, C 진단 진입, UART 출력을 각각 분리해야 합니다.

5.2 C를 실행할 수 있는 환경도 두 단계다

_main은 초기 stack 주소를 고르고 bic sp, x0, #0xf로 16바이트 정렬합니다. board_init_f_alloc_reserve()가 필요한 공간을 예약한 뒤 SP와 x18을 설정하고 board_init_f_init_reserve()가 초기 gd 영역을 준비합니다. gd를 가리키는 x18은 일반 scratch register처럼 사용하면 안 됩니다.

초기 RAM · SRAM 등설정에서 고른 초기 top
예약된 gd / 초기 예약 공간x18이 gd를 가리킴
후속 인계 정보를 보존
SP · 아래 방향으로 성장board_init_f의 local 변수
일반 writable global / BSS이 단계에서 사용 가능하다고 가정하지 않음
후속 RAM · 계산된 배치gd→start_addr_sp, new_gd, relocaddr
새 SP, 새 gdcrt0가 SP와 x18을 전환
relocate_code → CPU runtime setup보정된 return 주소로 계속 실행
BSS clear → board_init_rwritable data와 zero BSS 준비
그림 9. 개념 배치이며 크기·주소 간격은 축척이 아니다. board_init_f는 제한된 C 환경에서 실행하고 일반 BSS가 준비되는 경계는 이후다.근거: crt0_64.S:25–62, 67–139, 157–175. 일반 U-Boot proper 경로이며 XPL·SKIP_RELOC은 조건부.

왜 board_init_f에서 값을 gd에 저장하는가

DRAM 배치와 relocation 목적지는 초기 단계에서 계산해야 하지만 일반 global variable의 초기값과 수명이 아직 보장되지 않습니다. gd는 단계들을 이어 주도록 명시적으로 예약하고 초기화한 저장 공간입니다. board_init_f에 임시 static 변수를 추가하는 변경은 SRAM 용량뿐 아니라 C runtime 계약을 바꾸는 문제입니다.

BSS를 너무 일찍 지우면 무엇을 다시 확인해야 하는가

gd와 stack의 초기 위치는 설정에 따라 다르고 BSS 주소도 image 위치와 relocation에 의존합니다. zero loop를 startup 첫머리로 옮기려면 현재 실행 image와 임시 배치의 관계를 다시 검증해야 합니다. 원문은 새 runtime 환경 준비 뒤 BSS를 8바이트씩 __bss_end까지 지웁니다.

모든 build가 DRAM 재배치를 하는가

!CONFIG_XPL_BUILD guard와 GD_FLG_SKIP_RELOC 검사가 있습니다. XPL의 stack/gd 이동도 조건부입니다. 일반 U-Boot proper 그림을 SPL이나 vendor fork에 그대로 적용하지 말고 실제 .config와 disassembly로 활성 경로를 좁힌 뒤 주소를 계산합니다.

5.3 stack 위에서 gd 공간을 빼고, x18이 가리키게 한다

stack은 함수를 호출할 때 잠시 쓰는 공간이고, gd는 여러 초기화 단계를 지나도 남겨야 할 정보를 담는 구조체입니다. 둘 다 초기 RAM에 있지만 수명이 다릅니다. board_init_f_alloc_reserve()라는 이름은 동적 heap 할당처럼 들리지만, 실제로는 top에서 필요한 크기를 뺀 주소를 계산합니다.

common/init/board_init.c:81–93 · 예약 주소 계산 전체

ulong board_init_f_alloc_reserve(ulong top)
{
	/* Reserve early malloc arena */
#ifndef CFG_MALLOC_F_ADDR
#if CONFIG_IS_ENABLED(SYS_MALLOC_F)
	top -= CONFIG_VAL(SYS_MALLOC_F_LEN);
#endif
#endif
	/* LAST : reserve GD (rounded up to a multiple of 16 bytes) */
	top = rounddown(top-sizeof(struct global_data), 16);

	return top;
}

여기서는 별도 CFG_MALLOC_F_ADDR가 없고 early malloc을 사용한다고 가정합니다. 초기 top T=0x41000000, malloc 크기 M=0x1000, 설명용 gd 크기 G=0x238이라면 다음과 같습니다. G는 실제 빌드에서 측정한 크기가 아닙니다. 정확한 크기와 offset은 lib/asm-offsets.c가 sizeof/offsetof로 생성한 결과를 확인해야 합니다.

주소 계산

T−M = 0x40fff000

T−M−G = 0x40ffedc8

16-byte rounddown
B = 0x40ffedc0

함수 반환 x0 = B
호출자가 SP=B, x18=B로 변경

높은 주소부터

0x41000000초기 top · T
[0x40ffeff8, 0x40fff000)정렬 여유 · 8 bytes
[B, 0x40ffeff8)gd 본체 · 가정한 0x238 bytes
0x40ffedc0새 SP / x18 → gd
그림 10. 예약과 사용의 분리. 계산 함수가 실행 중일 때 SP를 임의로 바꾸면 자기 자신의 C frame이 깨질 수 있다. 그래서 계산 함수가 반환한 뒤 assembly가 SP를 바꾸고, 그다음 초기화 함수를 호출한다.원문: board_init.c:57–93, crt0_64.S:85–91.

crt0_64.S:85–91 · 계산 결과를 실제 실행 상태에 반영

	bic	sp, x0, #0xf	/* 16-byte alignment for ABI compliance */
	mov	x0, sp
	bl	board_init_f_alloc_reserve
	mov	sp, x0
	/* set up gd here, outside any C code */
	mov	x18, x0
	bl	board_init_f_init_reserve

첫 bic로 초기 SP를 확보했기 때문에 주소 계산용 C 함수를 호출할 수 있습니다. 그러나 gd가 필요한 일반 초기화 함수까지 아무 것이나 호출해도 된다는 뜻은 아닙니다. 반환 뒤 mov sp, x0가 새 경계를 적용하고 mov x18, x0가 gd를 연결합니다. ARM64의 DECLARE_GLOBAL_DATA_PTR는 x18을 gd로 쓰거나 x18을 읽는 구현입니다(asm/global_data.h:115–140). x18을 일반 임시 register로 훼손하면 이후의 모든 gd 필드 접근이 잘못됩니다.

board_init_f_init_reserve()는 B부터 gd 크기만큼 0으로 초기화합니다. 다음으로 G를 16바이트 올림한 0x240만큼 더해 B+0x240=0x40fff000을 malloc base로 기록합니다. 정렬 때문에 gd의 실제 끝과 malloc의 시작 사이에 여유가 남는 것이 정상입니다. gd를 지우는 이 작업과 나중에 image의 일반 BSS를 지우는 작업은 서로 다릅니다.

board_init_f 반환 뒤에는 x18의 의미도 새 주소로 바뀐다

board_init_f()는 미래의 stack, gd, image 목적지를 정하고 setup_reloc()에서 새 gd에 기존 구조체 내용을 복사합니다(common/board_f.c:705–739). 다음 assembly는 그 정보를 소비합니다. 아래에서 GD_*는 구조체 필드의 값이 아니라 생성된 바이트 offset입니다.

crt0_64.S:106–108 · 이전 gd를 통해 새 SP와 새 gd를 읽는다

	ldr	x0, [x18, #GD_START_ADDR_SP]	/* x0 <- gd->start_addr_sp */
	bic	sp, x0, #0xf	/* 16-byte alignment for ABI compliance */
	ldr	x18, [x18, #GD_NEW_GD]		/* x18 <- gd->new_gd */
이전 gd · Bstart_addr_sp=0x47fef000
new_gd=0x47fff000
relocaddr=0x48000000
reloc_off=0x08000000
setup_relocmemcpy(new_gd, gd, sizeof(gd_t))
새 주소에 실제 내용을 먼저 복사
crt0의 pointer 전환SP=0x47fef000
x18=0x47fff000
이후 [x18+offset]는 새 gd
그림 11. 복사와 포인터 변경은 다른 사건. x18의 새 값을 load하는 주소 계산에는 아직 이전 x18이 사용된다. ldr x18, [x18, #GD_NEW_GD]가 끝난 다음 명령부터 새 gd를 본다. 주소와 필드 값은 중심 예제의 가정이다.원문: board_f.c:705–739, crt0_64.S:106–112.

GD_FLG_SKIP_RELOC 검사도 이 전환 뒤에 있습니다. 즉 skip이면 SP와 gd까지 예전 상태로 남는 것이 아닙니다. 새 gd의 flags를 읽어 image 복사를 건너뛰고 relocation_return으로 바로 갑니다. 반대로 일반 경로에서는 LR을 새 image의 복귀 주소로 만든 뒤 복사 루틴에 들어갑니다.

5.4 실행 중인 image를 복사하고, 새 사본으로 돌아간다

새 SP와 gd는 준비됐지만 PC는 여전히 R의 명령을 실행합니다. 새 image D로 이동하려면 세 가지가 모두 맞아야 합니다. 명령어/데이터 복사, absolute pointer 보정, 제어 흐름 이동입니다. 그중 하나만 성공해도 충분한 것이 아닙니다.

먼저 미래의 LR을 만들어 둔다

crt0_64.S의 동작계산결과
114: adr lr, relocation_returnR+0x4100LR=0x40204100
117–120: R을 계산하고 L을 읽어 뺌x9=L−R=−0x0020000064-bit x9=0xffffffffffe00000
121: add lr, lr, x9(R+0x4100)+(L−R)LR=0x40004100
129: gd→reloc_off 읽기x9=D−L=0x08000000이전 x9와 부호·의미가 달라짐
130: add lr, lr, x9(L+0x4100)+(D−L)LR=0x48004100
131–132: 목적지와 분기x0=D, b relocate_codePC는 아직 R의 relocate_code, LR은 D의 복귀점

이 마지막 분기를 bl relocate_code로 바꾸면 CPU가 LR을 R의 다음 명령 주소로 덮어써 버립니다. 그래서 소스는 LR을 직접 만든 뒤 b로 들어갑니다. 재배치 함수가 반환할 때에는 같은 파일의 같은 label처럼 보이더라도 다른 메모리 주소의 사본에서 계속 실행됩니다. crt0_64.S:120의 주석 문구보다 실제 피연산자 sub x9, x9, x0를 보고 L−R임을 판단해야 합니다.

relocate_64.S:22–25 · 미래의 복귀 주소와 목적지를 stack에 보관

ENTRY(relocate_code)
	stp	x29, x30, [sp, #-32]!	/* create a stack frame */
	mov	x29, sp
	str	x0, [sp, #16]

SP는 32바이트 내려갑니다. +0에는 이전 x29, +8에는 미래의 LR, +16에는 복사 시작 D가 놓입니다. +24는 나중에 복사 종료 주소를 넣는 자리입니다. 본문에서 x0를 cursor로 증가시키더라도 원래 목적지를 잃지 않게 보존합니다. 마지막의 ldp x29, x30, [sp], #32; ret이 이 LR을 되살려 D+0x4100으로 갑니다.

복사 거리와 pointer 보정 거리는 다르다

relocate_64.S:29–32는 현재 __image_copy_start의 주소 R을 계산하여 D−R을 검사합니다. 같으면 이동할 필요가 없다는 경로입니다. 일반 복사 경로에서는 _TEXT_BASE에서 다시 L을 읽어 x9를 D−L로 바꿉니다(43–44행). relocation record의 주소들이 L 기준이기 때문입니다. 단순히 “x9는 relocation offset”이라고만 외우면 이 두 차이를 놓칩니다.

relocate_64.S:50–55 · 16바이트씩 앞으로 복사

copy_loop:
	ldp	x10, x11, [x1], #16	/* copy from source address [x1] */
	stp	x10, x11, [x0], #16	/* copy to   target address [x0] */
	cmp	x1, x2			/* until source end address [x2] */
	b.lo	copy_loop
	str	x0, [sp, #24]

첫 반복에서 x1=R, x0=D이며 source의 16바이트가 destination에 저장됩니다. 반복 후 x1=R+16, x0=D+16입니다. 예제의 0x30000바이트를 복사하면 12,288회 반복 후 x1=0x40230000, x0=0x48030000이 됩니다. 이 루프는 뒤에서 앞으로 복사하는 분기를 가진 memmove()가 아닙니다. destination이 source를 덮으며 전진하는 겹침 배치를 안전하다고 가정할 수 없습니다. 끝 경계의 정렬도 실제 linker 산출물과 함께 확인해야 합니다.

① PIE 직후 · R

M64[R+0x8000]0x40212000

→ R+0x12000

현재 image 내부를 가리킴

② byte copy 직후 · D

→ 아직 R+0x12000

pointer 비트까지 그대로 복사됨

③ fixloop 직후 · D

M64[D+0x8000]0x48012000

→ D+0x12000

addend+(D−L)로 다시 기록

그림 12. 메모리를 복사하는 것과 참조를 옮기는 것. ②의 pointer는 읽을 수 있는 값이어도 새 image 내부를 가리키지 않는다. R에 있는 relocation table을 읽어 D의 slot을 고친다. 기존 slot 값에 D−L을 더하는 것이 아니라 record의 addend에서 다시 계산한다.원문: relocate_64.S:46–77.

relocate_64.S:64–77 · 복사본에 적용하는 두 번째 relative fixup

fixloop:
	ldp	x0, x1, [x2], #16	/* (x0,x1) <- (SRC location, fixup) */
	ldr	x4, [x2], #8		/* x4 <- addend */
	and	x1, x1, #0xffffffff
	cmp	x1, #R_AARCH64_RELATIVE
	bne	fixnext

	/* relative fix: store addend plus offset at dest location */
	add	x0, x0, x9
	add	x4, x4, x9
	str	x4, [x0]
fixnext:
	cmp	x2, x3
	b.lo	fixloop

같은 첫 record에 x9=D−L=0x08000000을 적용하면 쓰기 대상은 0x48008000, 쓸 값은 0x48012000입니다. 잘못해서 ②의 기존 값 0x40212000에 D−L을 더하면 0x48212000이 되어 R−L만큼 과하게 이동합니다. 실제 코드는 이렇게 누적해서 고치지 않습니다.

cache 처리 후 ret가 실행 위치를 옮긴다

relocate_done은 현재 EL의 SCTLR을 읽습니다. 이 구현은 C bit(bit 2)가 0이면 cache flush 분기를 건너뜁니다. C=1이면 I bit(bit 12)에 따라 I-cache invalidate를 수행하고, stack에 저장한 destination 시작/끝으로 D-cache range flush와 L3 flush helper를 호출합니다(relocate_64.S:79–95). 이것은 이 파일의 조건 분기를 설명한 것이며 모든 CPU에 통용되는 임의의 self-modifying-code 절차가 아닙니다.

R: crt0LR=D+0x4100 준비
b relocate_code
R: relocate_codeLR을 stack에 보존
R → D copy, D fixup
LR 복원 후 ret
D: relocation_returnPC=D+0x4100
c_runtime_cpu_setup 호출
VBAR=D+0x800
그림 13. PC와 LR은 잠시 서로 다른 image를 가리킨다. relocation 루틴 실행 중의 PC는 R, 보존된 LR은 D다. D로 복귀한 뒤 PC-relative adr x0, vectors가 D 기준 주소를 얻는다.원문: crt0_64.S:114–139, start.S:371–385.

따라서 crt0의 bl c_runtime_cpu_setup 옆에 있는 “still call old routine”이라는 주석만 읽고 이전 R의 vector를 계속 쓴다고 결론 내리면 안 됩니다. 정상 재배치 경로에서는 새 image에서 상대 분기를 수행하고, 새 PC 기준 vector 주소를 다시 씁니다. SKIP_RELOC 경로는 이동하지 않았으므로 계속 현재 image 기준입니다.

5.5 BSS를 일찍 지우면 왜 relocation 정보가 사라지는가

BSS는 0으로 시작해야 하는 일반 전역/정적 변수의 영역입니다. 부팅 image에 그 0을 전부 담는 대신 실행 준비가 끝날 때 메모리를 지웁니다. 이 U-Boot linker script에는 더 구체적인 공간 재사용이 있습니다. .bss ADDR(.rela.dyn) (OVERLAY)가 BSS의 시작 주소를 relocation table의 시작과 같게 둡니다.

arch/arm/cpu/armv8/u-boot.lds:142–161 · 복사 범위, relocation table, BSS의 경계

	. = ALIGN(8);
	__image_copy_end = .;

	.rela.dyn ALIGN(8) : {
		__rel_dyn_start = .;
		*(.rela*)
		__rel_dyn_end = .;
		. = ALIGN(8);
	}

	_end = .;

	/*
	 * arch/arm/lib/crt0_64.S assumes __bss_start - __bss_end % 8 == 0
	 */
	.bss ADDR(.rela.dyn) (OVERLAY) : {
		__bss_start = .;
		*(.bss*)
		. = ALIGN(8);
		__bss_end = .;
R의 주소 공간
[R, R+0x30000)
copy_loop가 D로 복사
D의 주소 공간
[D, D+0x30000)
명령·데이터와 보정된 pointer
[D+0x30000, D+0x30100)
clear_loop로 BSS를 0으로 만듦
그림 14. 주소 중첩과 시간 분리. 예제에서 relocation table은 복사 끝 뒤에 있으므로 D로 복사하는 구간에 포함되지 않는다. R의 table을 소비한 후 D의 같은 offset을 BSS로 쓴다. 재배치를 생략해 R=D라면 같은 물리 메모리도 수명이 끝난 뒤 BSS로 재사용된다.원문: u-boot.lds:142–166, relocate_64.S:46–77.

예를 들어 초기 단계에서 R+0x30000부터 BSS라고 생각하고 지우면 첫 record의 r_offset, r_info, addend가 모두 0이 됩니다. 그러면 뒤의 fixloop가 type 1027을 찾지 못하거나, 손상된 정보로 잘못된 주소를 다루게 됩니다. “BSS는 0이어야 한다”는 사실만으로 zero loop를 reset 앞으로 옮길 수 없는 이유입니다.

crt0_64.S:161–173 · 새 image의 BSS를 지운 뒤 board_init_r에 인계

	adrp	x0, __bss_start
	add	x0, x0, #:lo12:__bss_start
	adrp	x1, __bss_end
	add	x1, x1, #:lo12:__bss_end
clear_loop:
	str	xzr, [x0], #8
	cmp	x0, x1
	b.lo	clear_loop

	/* call board_init_r(gd_t *id, ulong dest_addr) */
	mov	x0, x18				/* gd_t */
	ldr	x1, [x18, #GD_RELOCADDR]	/* dest_addr */
	b	board_init_r			/* PC relative jump */

이제 명령은 D에서 실행되므로 x0=0x48030000, x1=0x48030100입니다. XZR은 0을 읽는 register라서 매 store가 8바이트를 0으로 만듭니다. 예제에서는 32번 뒤 x0=x1이 되어 종료합니다. 마지막 실제 store 위치는 0x480300f8이며 끝 주소 자체에는 쓰지 않습니다. linker가 끝을 8바이트에 맞추는 이유와 store 폭이 연결됩니다. 이 loop 역시 후행 검사이므로 범용 0길이 memset으로 복사해 쓰면 안 됩니다.

마지막에는 x0=x18=새 gd, x1=gd→relocaddr=D로 맞춰 board_init_r(gd, D)에 분기합니다. entry에서 받은 x0와는 완전히 다른 의미입니다. 이 지점까지 와야 앞에서 따로 준비한 stack, gd, image data, BSS가 최종 C 실행 환경으로 합쳐집니다.

6. primary와 secondary CPU는 같은 길을 가지 않는다

multi-entry platform에서는 여러 CPU가 같은 entry에 도달할 수 있습니다. 모든 CPU가 BSS를 지우고 DRAM을 초기화하면 race가 발생하므로 branch_if_master 이후 역할을 분리합니다. secondary는 spin table, ACPI parking protocol, platform-specific release path 중 build configuration에 맞는 곳에서 기다립니다.

경로primarysecondary
공통 초반EL별 state, errata, low-level initialization의 공통/분기 부분 수행
대기 정책계속 진행wfe, release address polling 또는 parking area
C runtime_main 진입OS나 primary가 지정한 주소로 release될 때까지 진입하지 않음

master는 “먼저 도착한 CPU”가 아니다

macro.h:135–144의 기본 branch_if_master는 MULTIENTRY일 때 MPIDR_EL1을 읽고 bits [63:40]과 [31:24]를 지운 뒤 0인지 확인합니다. 남는 것은 Aff3, Aff2, Aff1, Aff0입니다. 모든 affinity가 0인 CPU를 master로 정하는 규칙이지 lock 경쟁의 승자를 뽑는 알고리즘이 아닙니다. MULTIENTRY가 없으면 macro는 곧바로 master label로 분기합니다.

macro.h:135–144 · primary 선택의 실제 조건

.macro	branch_if_master, xreg, master_label
#ifdef CONFIG_ARMV8_MULTIENTRY
	mrs	\xreg, mpidr_el1
	and	\xreg, \xreg,  0xffffffffff	/* clear bits [63:40] */
	and	\xreg, \xreg, ~0x00ff000000	/* also clear bits [31:24] */
	cbz	\xreg, \master_label
#else
	b	\master_label
#endif
.endm
MPIDR 예제mask 후 값결과
0x00000000800000000master
0x00000000800000011 · Aff0=1secondary
0x00000001800000000x100000000 · Aff3=1secondary · 하위 8비트만 보면 오판

lowlevel_init 안의 기다림과 뒤의 WFE는 같은 단계가 아니다

기본 lowlevel_init()은 GIC 설정이 있을 때 master에 distributor 초기화를 맡기고 CPU별 초기화는 각 CPU가 수행하게 합니다. MULTIENTRY secondary는 설정에 따라 gic_wait_for_interrupt()를 거쳐 EL2, 선택적으로 EL1로 내려가는 helper를 호출합니다(start.S:301–357). 그 이후 reset의 분기에서 spin-table/ACPI parking/release-word 경로 중 하나로 갑니다. 첫 GIC 대기와 아래 WFE loop를 하나의 “secondary 대기” 상자로 합치면 초기 동기화와 최종 release를 구분할 수 없습니다.

이 글은 receiver 쪽을 확인한 것입니다. board override, GIC register별 구현, release를 쓰는 platform 측 cache/barrier 절차까지 모두 검증했다는 뜻은 아닙니다. 특히 affinity가 모두 0인 CPU가 부팅 참여 집합에 없으면 이 기본 master 규칙을 그대로 적용할 수 있는지 platform 설계를 확인해야 합니다.

순서release를 수행하는 주체secondary CPU검증 조건
1secondary용 진입 code·stack·인자 준비WFE 또는 platform parking진입 주소가 실행 가능
2release 주소를 공유 위치에 기록아직 이전 값을 볼 수 있음cache/coherency 계약 확인
3platform이 요구하는 cache maintenance·barrier·event깨워져 공유 주소를 다시 읽음event 자체가 cache flush는 아님
4후속 handshake 대기주소가 0 아니면 br x0임의 반환 주소 없이 제어 인계
그림 15. CONFIG_ARMV8_MULTIENTRY의 대기 loop를 이해하기 위한 release 계약이다. sender 코드는 이 짧은 loop에 없으며, 정확한 barrier·cache 작업은 platform의 공유 메모리 속성에 따라 달라진다.근거: start.S:177–208. ACPI parking과 DT spin-table은 별도 조건부 경로.

WFE가 깨어났다는 사실은 메모리에서 원하는 주소를 읽었다는 뜻이 아닙니다. event와 data visibility는 서로 다른 문제이므로 loop는 release word를 읽고 0이면 다시 기다립니다. 또한 br x0는 link register를 만드는 함수 호출이 아닙니다. release 주소가 요구하는 stack, register, cache 상태를 sender와 receiver가 함께 맞춰야 합니다. PSCI로 secondary를 켜는 Linux 경로를 이 U-Boot loop와 동일시해서는 안 됩니다.

7. 실무에서 깨뜨리면 안 되는 조건

  • PIE alignment: position-independent v2026.07 ARM64 U-Boot는 startup의 ADRP/ADD relocation 때문에 4 KiB 정렬을 요구합니다.
  • incoming register ownership: x0–x3를 scratch로 쓰기 전에 board의 save_boot_params 또는 bloblist 저장 규약을 확인합니다.
  • VBAR and EL pairing: EL2의 E2H=0 경로에는 VBAR_EL2가 필요합니다. E2H=1의 VHE 경로에는 EL1 이름의 register 접근에 alias 규칙이 적용되므로 명령 이름만으로 잘못된 EL의 VBAR를 썼다고 단정하지 않습니다.
  • link address versus runtime address: ELF symbol, raw binary offset, ROM load address, relocation destination은 서로 다른 값일 수 있습니다.
  • single-writer early init: BSS, DRAM, global data를 여러 CPU가 동시에 초기화하지 않도록 primary election 경계를 유지합니다.
  • C ABI boundary: _main의 stack과 x18 global data 준비 전에는 평범한 C 함수 호출 전제를 적용하지 않습니다.
  • barrier semantics: system register나 release address 갱신의 isb, dsb, event protocol을 임의로 제거하지 않습니다.

8. 첫 UART 출력 전 실패를 나누는 법

증상가능한 원인결정적인 확인
PC가 entry 근처에 없음ROM image header, offset, authentication, load addressROM log와 실제 PC, final packed image map
wfi loopPIE image의 4 KiB misalignment_start & 0xfff, ELF/link address, runtime PC
첫 system register access에서 exception예상과 다른 CurrentEL 또는 trap policyCurrentEL, ESR_ELx, ELR_ELx, SCR/HCR
예외 handler가 실행되지 않음VBAR가 이전 firmware/이전 relocation 주소VBAR_ELx와 vectors runtime symbol 비교
secondary만 멈춤release address, cache coherency, event 전달 오류mailbox 값, cache visibility, WFE/SEV sequence
_main 직후 data abort초기 SP 또는 x18/gd 배치 오류SP_ELx, x18, SRAM top과 gd_t 범위

9. 재현 가능한 디버깅 절차

  • 산출물 확인: aarch64-linux-gnu-readelf -hW u-boot로 entry를, nm -n u-boot로 _start, vectors, _main 순서를 기록합니다.
  • 실제 명령 확인: aarch64-linux-gnu-objdump -dr u-boot에서 preprocessor 결과가 반영된 분기와 relocation을 봅니다. source의 비활성 #if 경로를 실행 경로로 착각하지 않습니다.
  • QEMU 정지 시작: 해당 board build가 QEMU virt를 지원한다면 -S -s로 첫 명령 전에 멈추고 GDB에서 PC, CurrentEL, SCTLR, VBAR를 기록합니다.
  • hardware 무출력 분석: SRAM의 안전한 scratch word나 GPIO에 단계 signature를 남겨 reset, lowlevel_init, _main 도달을 이분 탐색합니다.
  • 최종 image 기준: ELF만 보지 말고 binman map, vendor header, padding을 포함한 ROM 입력 파일에서 entry offset을 다시 계산합니다.
  • 보드 계약 보존: SoC TRM, Boot ROM 문서, TF-A handoff 문서, defconfig와 linker map을 같은 실험 기록에 묶습니다.

UART가 조용할 때 PC와 메모리로 가설을 제거한다

PC가 실제 _start 범위인가?아니오: ROM 입력·header·authentication·entry 주소 확인
정렬 검사 WFI loop인가?예: R의 하위 12비트와 image padding 확인
system register 접근에서 예외인가?CurrentEL·HCR·VBAR·ESR/ELR을 같은 시점에 확보
_main은 도달했는가?SP 정렬·예약 RAM 범위·x18·gd 필드 확인
relocation 이후에만 실패하는가?PC/LR/VBAR가 R과 D 중 어느 image를 가리키는지 분리
그림 16. 각 질문은 다음에 읽을 증거를 지정한다. 마지막 UART 문자열만으로 실패 함수를 단정하지 않는다.위 start.S·crt0_64.S의 분기와 메모리 경계에서 도출한 진단 절차. 특정 보드의 실측 결과가 아니다.

host에서 ELF의 주소와 relocation 확인 · toolchain prefix는 환경에 맞춘다

aarch64-linux-gnu-readelf -hW u-boot
aarch64-linux-gnu-readelf -lW u-boot
aarch64-linux-gnu-readelf -rW u-boot
aarch64-linux-gnu-nm -n u-boot
aarch64-linux-gnu-objdump -dr u-boot

실제 PC가 R 기준인데 debugger가 L 기준 symbol을 사용하면 breakpoint가 엉뚱한 곳에 걸립니다. symbol 주소에 R−L을 반영한 뒤 reset, pie_fixup, master_cpu, _main, relocation_return에 순서대로 멈춥니다. D로 옮긴 다음에는 symbol 기준도 갱신합니다. debugger가 register를 어떤 이름으로 노출하는지는 target에 따라 달라지므로 없는 register 이름을 읽은 오류와 실제 hardware fault를 구분합니다.

각 정지점에서 PC, SP, x18, CurrentEL, 사용 중인 VBAR, link/runtime base를 같은 기록에 남깁니다. fault가 있으면 ESR·ELR·SPSR과 가능한 경우 FAR도 추가합니다. SP & 0xf == 0만 만족해도 주소가 RAM 밖이면 잘못된 stack입니다. 정렬, 접근 가능 범위, 다른 초기 예약 영역과의 겹침을 함께 검사합니다.

이 페이지의 hex 계산은 자동 검증한 설명용 예제입니다. 실제 board나 QEMU 부팅 성공, fault register dump, 성능 수치를 얻었다고 주장하지 않습니다. 실험 결과를 추가할 때에는 defconfig, compiler, 최종 image hash와 원본 dump를 함께 남겨 같은 명령과 주소를 다시 확인할 수 있어야 합니다.

10. 이 글의 범위와 예외

이 글은 upstream U-Boot v2026.07의 generic arch/arm/cpu/armv8/start.S를 기준으로 합니다. SoC가 CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK, vendor fork, SPL/XPL 전용 entry, TF-A BL33 handoff를 사용하면 실제 첫 명령과 register 계약이 달라집니다. 최종 binary의 disassembly가 source 설명보다 우선합니다.

secure boot authentication, DRAM training의 내부, board_init_f/r의 board별 초기화, Linux kernel handoff는 별도 단계입니다. 이 페이지는 reset entry에서 crt0_64.S, gd 예약/복사, relocate_64.S, linker의 BSS overlay, board_init_r 인자 준비까지 연결합니다. 실장 보드의 boot 성공이나 모든 configuration의 실행 검증을 대신하지는 않습니다.

원문과 검증 범위

이번 분석에서 추가로 대조한 구현 원문

검증 기준

U-Boot tag v2026.07, 2026-09-24 KST 확인. 보존 원문 해시, 명시한 줄 범위와 코드 발췌의 일치, 문서 내 링크, 설명용 주소·stack·copy/BSS 계산을 검사합니다. 이는 소스와 설명의 정적 대조이며 ARM 명령 실행기나 QEMU/실장 보드 측정 결과가 아닙니다. 외부 블로그의 그림을 복제하지 않고 위 원문의 동작을 별도로 도식화했습니다.