Linux Boot ABI — ARM/ARM64 커널 진입 규약 추적
부트로더는 커널을 그냥 점프하지 않습니다. 커널은 점프 시점의 레지스터 값과 CPU 모드에 강한 요구가 있습니다. 그 약속이 boot ABI입니다. 약속을 어기면 커널이 첫 명령어에서 panic하거나, 더 흔하게는 아무 출력도 없이 멈춥니다.
#한 줄 요약
Linux는 부트로더에게 “DTB 주소를 특정 레지스터에 박고, MMU 끄고, 인터럽트 막고, 캐시는 비워서 점프하라”고 명시합니다. ABI 어기면 panic 없이 죽습니다.
#AArch64 boot ABI
가장 자주 만나는 ABI입니다. Linux Documentation/arm64/booting.rst에 명시되어 있습니다.
점프 시점 요구사항:
레지스터:
- x0 = DTB(또는 ACPI tables) 물리 주소
- x1 = 0 (reserved)
- x2 = 0 (reserved)
- x3 = 0 (reserved)
- PC = Image.start + 0x80
CPU 상태:
- EL = EL1 (또는 EL2 — Linux가 자체 처리)
- Endian = little endian
- Interrupts = masked (DAIF.I = DAIF.F = 1)
- MMU = OFF
- D-cache = OFF (단, kernel 이미지·DTB·initramfs 영역은 flushed)
- I-cache = invalidated 후 OFF 또는 ON
DTB:
- 8-byte aligned
- kernel image와 동일한 512MB 영역 안 또는 그 이하
- 크기 < 2MB
booti가 이 모두를 처리합니다. 직접 점프할 일은 없지만, 왜 멈추는지 알려면 ABI를 알아야 합니다.
// arch/arm/lib/bootm.c (요지)static void boot_jump_linux(struct bootm_headers *images, int flag){ void (*kernel_entry)(int zero, int arch, uint params); unsigned long machid = gd->bd->bi_arch_number; void *fdt = images->ft_addr;
kernel_entry = (void *)images->ep;
/* clean cache, disable mmu, mask irq */ cleanup_before_linux();
/* AArch64는 별도 헬퍼 */ armv8_switch_to_el1(0, (u64)fdt, 0, 0, (u64)kernel_entry, ES_TO_AARCH64);}armv8_switch_to_el1이 EL2에서 EL1으로 떨어뜨리고, x0에 fdt 주소를 박고, 나머지 x1·x2·x3을 0으로 클리어한 뒤 점프합니다.
#AArch32 boot ABI
ARMv7 시대의 ABI는 형태가 약간 다릅니다.
레지스터:
- r0 = 0 (boot 모드 flag, 보통 0)
- r1 = machine type (machid, DT만 쓰면 ~0)
- r2 = ATAGS 또는 DTB 물리 주소
CPU 상태:
- SVC mode
- Interrupts = disabled (CPSR.I = CPSR.F = 1)
- MMU = OFF
- D-cache = OFF
- I-cache = ON 또는 OFF (둘 다 허용)
ATAGS / DTB:
- ATAGS: 옛 인터페이스, 메모리 맵·cmdline 등을 구조체 리스트로
- DTB: 현재 표준, r2가 가리키는 주소에 둠
ATAGS는 이제는 거의 안 보이지만, BSP가 오래된 보드면 마주칠 수 있습니다. 둘 중 어느 것인지는 처음 4바이트로 구분합니다. DTB는 0xd00dfeed magic으로 시작합니다.
#bootm vs booti vs bootefi
이름이 다 비슷해 헷갈리지만 받는 이미지 포맷이 다릅니다.
| 명령 | 받는 포맷 | ABI 처리 |
|---|---|---|
bootm | uImage 또는 FIT (legacy + 검증) | 헤더에서 entry·load·os 읽고 분기 |
booti | raw Image (ARM64) 또는 zImage (ARM32) | AArch64 native header 읽음 |
bootz | zImage (ARM32) | zImage 압축 풀어 점프 |
bootefi | EFI PE/COFF | EFI Boot Services 띄우고 EFI entry로 점프 |
booti가 가장 단순합니다. ARM64 Image의 첫 64바이트는 self-describing header라 entry point와 load offset이 그 안에 있습니다.
// include/linux/arm-image.h (요지)struct arm64_image_header { __le32 code0; /* Executable code */ __le32 code1; /* Executable code */ __le64 text_offset; /* Image load offset from start of RAM */ __le64 image_size; /* Effective Image size */ __le64 flags; /* kernel flags */ __le64 res2; __le64 res3; __le64 res4; __le32 magic; /* Magic number ARM\x64 */ __le32 res5;};magic이 ARM\x64(0x644D5241)인지 확인해 ABI 분기합니다.
#DTB의 자리
DTB는 커널 이미지와 같은 512MB 영역 또는 그 위쪽에 둡니다. 흔한 실수는 DTB를 ramdisk 바로 뒤에 두고 두 영역이 충돌하는 경우입니다. U-Boot 환경에서 정렬을 명시해 두는 편이 안전합니다.
=> printenvkernel_addr_r=0x40080000fdt_addr_r=0x44000000ramdisk_addr_r=0x46000000fdt_high=0xffffffffffffffffinitrd_high=0xfffffffffffffffffdt_high·initrd_high를 0xffffffff...로 두면 U-Boot이 그 자리 그대로 쓰고 재배치하지 않습니다. 보드에 따라 자동 재배치가 다른 영역을 침범할 수 있어 명시하는 편이 안전합니다.
#initramfs 전달
initramfs는 cpio.gz 형태로 메모리에 두고, 그 주소와 크기를 DTB의 chosen 노드에 박습니다.
chosen { linux,initrd-start = <0x00000000 0x46000000>; linux,initrd-end = <0x00000000 0x46800000>; bootargs = "console=ttyS0,115200 root=/dev/ram0 rdinit=/sbin/init";};U-Boot의 bootm/booti는 ramdisk를 인자로 받으면 이 노드를 자동으로 채웁니다.
=> booti ${kernel_addr_r} ${ramdisk_addr_r}:${filesize} ${fdt_addr_r}세 번째 인자가 fdt 주소, 두 번째 인자가 addr:size 포맷의 ramdisk입니다.
#bootargs와 cmdline
cmdline은 DTB의 chosen/bootargs에 박힙니다. U-Boot env의 bootargs가 자동으로 그곳에 복사됩니다.
=> setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 ro rootwait \ ftrace=function ftrace_filter=raw_local_irq*'=> saveenv=> bootm ${kernel_addr_r}부팅 디버깅에 자주 쓰는 인자입니다.
| cmdline | 효과 |
|---|---|
earlycon=ns16550a,mmio32,0x9000000,115200 | DTB 보기 전 매우 이른 콘솔 |
loglevel=8 | 모든 printk 출력 |
initcall_debug | initcall 단위 로그 |
ftrace=function | function tracer 활성 |
nosmp | SMP 비활성, 첫 부팅 디버깅에 |
panic=0 | panic 시 reboot 안 하고 멈춤 |
pause_on_oops=300 | oops 후 300초 멈춤 |
#RISC-V boot ABI
RISC-V도 비슷한 구조입니다. Documentation/riscv/boot.rst를 따릅니다.
레지스터:
- a0 = boot hartid (어느 hart가 첫 부팅을 맡았는지)
- a1 = DTB 물리 주소
CPU 상태:
- S-mode (Linux는 S-mode에서 동작; M-mode는 OpenSBI 등 firmware가 차지)
- MMU = OFF
- Interrupts = disabled
DTB:
- 8-byte aligned
흥미로운 점은 RISC-V는 firmware가 항상 있다는 가정입니다. OpenSBI가 M-mode를 들고 있고, U-Boot은 S-mode에서 동작하다 Linux로 인계합니다. ARM에서는 ATF가 비슷한 역할을 합니다.
#x86 boot protocol
x86은 historical reasons로 더 복잡합니다. Real Mode zImage 헤더, 32-bit boot protocol, 64-bit boot protocol이 있습니다.
real-mode boot protocol:
- zImage 헤더의 boot_params 구조체를 ES 둠
32-bit boot protocol (CONFIG_KERNEL_NORELOC=n 이전):
- EBX = 0
- EBP = 0
- EDI = 0
- ESI = boot_params 물리 주소
- EFLAGS.IF = 0
64-bit boot protocol:
- RSI = boot_params
- CR0.PE = 1 (protected)
- CR4.PAE = 1 (Physical Address Extension)
- EFER.LMA = 1 (long mode active)
- paging = on (identity mapped first 4GB)
임베디드에서 x86은 드물지만, intel atom 보드를 만지면 다시 볼 일이 있습니다.
#점프 후 첫 출력이 안 나올 때
ABI 위반은 콘솔이 완전히 침묵하는 형태로 나옵니다. 가장 자주 만나는 원인 셋입니다.
- DTB가 cache에 있고 메모리에는 안 박혔다.
cleanup_before_linux()가 D-cache flush를 빠뜨렸거나, FIT의 load address가 cacheable·writeback 영역인데 flush 누락. - cmdline에
earlycon이 없어서, 커널이 DT의 chosen/stdout-path를 보기 전까지는 콘솔이 묵묵부답. - DTB 주소를 x1·x2·x3에도 박았다. AArch64 ABI는 x0만 사용하고 x1~x3은 반드시 0이어야 합니다. 0이 아니면 미래 확장에서 다른 의미로 해석될 수 있습니다.
earlycon을 박고 다시 부팅하면 90% 이상의 케이스에서 어디서 죽는지가 보입니다.
#자주 하는 실수
ABI 관련 흔한 실수입니다.
- DTB를 ramdisk 뒤에 두고 두 영역이 겹친다.
fdt_high·initrd_high로 자리를 명시해야 안전합니다. - cleanup_before_linux 호출 전에 점프한다. D-cache·MMU가 켜진 채로 들어가 커널이 첫 페이지 테이블 설정에서 멈춥니다.
bootm에 raw Image를 넘긴다. uImage 헤더가 없어 magic 검사에서 실패합니다. raw Image는booti로.- AArch64 커널을 EL3에서 진입시킨다. U-Boot이 EL1까지 내려놓아야 합니다. ATF가 있는 시스템은 ATF가 처리.
- bootargs가 너무 길어 DTB의 chosen 노드 자리를 넘긴다. 1024바이트 안에서 정리.
- ATAGS와 DTB를 동시에 박는다. ARMv7에서 둘 다 r2로 전달할 수는 없습니다. 커널이 magic으로 자동 판별하지만, 보드 코드가 ATAGS 가정으로 짜여 있으면 충돌합니다.
#정리
- AArch64 ABI는
x0=DTB, x1=x2=x3=0, EL1, MMU/cache off, IRQ masked다섯이 핵심입니다. - AArch32는
r0=0, r1=machid, r2=DTB(또는 ATAGS), SVC mode형태입니다. bootm은 uImage·FIT,booti는 raw ARM64 Image,bootz는 zImage,bootefi는 EFI PE를 받습니다.- DTB는 커널 이미지와 같은 512MB 영역에 두고,
fdt_high=0xff...로 재배치를 막는 편이 안전합니다. - initramfs 주소·크기는
chosen/linux,initrd-start·linux,initrd-end에 박힙니다. - cmdline은
chosen/bootargs로 전달되며,earlycon이 부팅 무중계 콘솔의 핵심입니다. - ABI 위반은 panic 없이 침묵으로 나타나니, 콘솔이 안 나올 때 ABI를 먼저 의심해야 합니다.
#다음 장 예고
다음 장은 부트로더의 한 단계 위에 있는 펌웨어 업데이트 프레임워크를 봅니다. RAUC와 SWUpdate가 A/B 슬롯·서명·다운로드·진행 보고를 한 묶음으로 처리하는 방식입니다.
#관련 항목
Bootloader Internals · 19 of 37
- 1ROM부터 init까지 — 임베디드 부팅 단계의 빈자리 분석
- 2Das U-Boot vs TF-A vs EDK II — 임베디드 부트로더 생태계 비교
- 3U-Boot 빌드 시스템 분석 — Kconfig·Makefile·defconfig 동작 추적
- 4ARM 임베디드 부트 4단계 분해 — BL1·SPL·TPL·U-Boot Proper의 역할
- 5U-Boot Falcon Mode — SPL이 U-Boot Proper 없이 커널 직접 부팅
- 6Device Tree DTB 부트로더 처리 — 로딩 시점과 fixup 메커니즘 추적
- 7U-Boot Driver Model 내부 — uclass·driver·device 추상화 구조
- 8U-Boot 보드 초기화 시퀀스 — board_init_f와 board_init_r 분리 이유
- 9DDR Controller 프로그래밍과 PHY Training — SPL의 가장 어려운 작업
- 10임베디드 스토리지 부팅 분석 — MMC·SCSI·NAND·SPI Flash 비교
- 11임베디드 네트워크 부팅 — TFTP·PXE·BOOTP 시퀀스 분석
- 12U-Boot USB 부팅 — fastboot·UMS·USB host 메커니즘
- 13U-Boot 환경 변수와 bootcmd — 부팅 시나리오 정의하기
- 14Modern U-Boot bootflow / bootmeth — 새 추상화 레이어 분석
- 15FIT image 구조 분석 — multi-image·hash·configuration 추적
- 16U-Boot Verified Boot — RSA 서명과 public key 임베딩 흐름
- 17임베디드 A/B 부팅 이중화 — OTA 안전성을 위한 부트 슬롯 설계
- 18U-Boot의 EFI 호환 분석 — bootefi 명령과 EFI loader 동작 원리
- 19Linux Boot ABI — ARM/ARM64 커널 진입 규약 추적
- 20임베디드 펌웨어 업데이트 — RAUC vs SWUpdate 비교
- 21새 보드 U-Boot 포팅 실전 — defconfig 작성부터 첫 부팅까지
- 22부트로더 디버깅 기법 — DEBUG·JTAG·serial·post-mortem 분석
- 23SoC BootROM·eFuse·OTP — 부팅의 0단계 분석
- 24SPL·TPL 내부 해부 — 가장 작은 부트 단계의 동작 추적
- 25ARM Trusted Firmware-A 통합 — BL1·BL2·BL31·BL32·BL33 흐름
- 26DDR Training과 PHY Calibration — 보드별 파라미터 튜닝
- 27임베디드 Chain of Trust — 다단계 서명 검증의 전체 흐름
- 28임베디드 Flash Layout 설계 — partition·NAND·eMMC·UBI 비교
- 29U-Boot Distro Boot — extlinux·boot.scr 표준화 분석
- 30부트로더 CI 구축 — build matrix와 자동 부팅 테스트
- 31TF-A BL31 EL3 Runtime 분석 — PSCI·SDEI·RAS dispatcher 추적
- 32PSCI와 SMCCC ABI — ARM 표준 SMC 호출 규약 분석
- 33ARM64 Secondary Core Bring-up — PSCI CPU_ON 호출부터 EL1 진입까지
- 34U-Boot PCIe Enumeration — 부트로더가 디바이스를 찾는 흐름 분석
- 35EFI·UEFI에서 CXL 초기화 — CEDT 생성과 HDM Decoder 사전 설정
- 36부트 시 메모리 토폴로지 결정 — DDR + CXL.mem 통합 인식
- 37UEFI Secure Boot 인증서 만료 — 2011→2023 CA 롤오버와 PQC 대비
관련 글
U-Boot PCIe Enumeration — 부트로더가 디바이스를 찾는 흐름 분석
U-Boot PCIe 열거 과정 — Root Complex 초기화·Config Space scan·BAR sizing·resource 할당, CXL DVSEC 인식까지.
ARM64 Secondary Core Bring-up — PSCI CPU_ON 호출부터 EL1 진입까지
ARM64 secondary CPU 깨우기 — spin-table 옛 방식과 PSCI CPU_ON 표준 방식, secondary_startup 어셈블리, percpu 초기화, hotplug 흐름.
PSCI와 SMCCC ABI — ARM 표준 SMC 호출 규약 분석
SMCCC 호출 규약과 PSCI v1.1 ABI — function ID 구조, fast vs yielding, CPU_ON·CPU_SUSPEND, Linux PSCI driver.
이 글을 참조하는 글 (8)
- 부트 시 메모리 토폴로지 결정 — DDR + CXL.mem 통합 인식— Bootloader Internals
- EFI·UEFI에서 CXL 초기화 — CEDT 생성과 HDM Decoder 사전 설정— Bootloader Internals
- U-Boot PCIe Enumeration — 부트로더가 디바이스를 찾는 흐름 분석— Bootloader Internals
- U-Boot Distro Boot — extlinux·boot.scr 표준화 분석— Bootloader Internals
- ARM Trusted Firmware-A 통합 — BL1·BL2·BL31·BL32·BL33 흐름— Bootloader Internals
- 부트로더 디버깅 기법 — DEBUG·JTAG·serial·post-mortem 분석— Bootloader Internals
- U-Boot의 EFI 호환 분석 — bootefi 명령과 EFI loader 동작 원리— Bootloader Internals
- Device Tree DTB 부트로더 처리 — 로딩 시점과 fixup 메커니즘 추적— Bootloader Internals