부트로더 디버깅 기법 — DEBUG·JTAG·serial·post-mortem 분석
부트로더가 죽으면 대개 조용히 죽습니다. OS도 없고, 시리얼도 아직 안 살아 있고, gdb도 못 붙는 상태에서 무엇이 잘못됐는지 알아야 합니다. 이번 장은 이 시리즈의 마지막으로, 처음 출력 한 글자부터 post-mortem 분석까지 부트로더 디버깅의 도구 상자를 정리합니다.
#한 줄 요약
침묵하는 부트로더는 DEBUG_UART로 첫 글자를 끌어내고, JTAG으로 멈춘 위치를 잡고, bdinfo·md·mw로 메모리 상태를 읽고, cmdline에서 panic 단서를 후추출하는 네 단계의 도구로 해부합니다.
#DEBUG_UART — 첫 글자가 안 나올 때
가장 이른 시점의 출력입니다. SPL이 띄우는 콘솔도 아직 동작하지 않을 때, 핀mux·클럭 설정 직후 바로 putc()로 글자를 찍습니다.
# defconfigCONFIG_DEBUG_UART=yCONFIG_DEBUG_UART_NS16550=yCONFIG_DEBUG_UART_BASE=0x30890000 # 보드의 UART 베이스CONFIG_DEBUG_UART_CLOCK=24000000 # UART 입력 클럭CONFIG_DEBUG_UART_BAUDRATE=115200CONFIG_DEBUG_UART_ANNOUNCE=y # 활성 시 "<debug_uart>" 인사CONFIG_DEBUG_UART_SKIP_INIT=n # 핀mux/클럭 직접 초기화코드에서는 보드 초기화 맨 앞에서 호출합니다.
#include <debug_uart.h>
void board_init_f(ulong dummy){ /* Earliest possible point */ debug_uart_init(); printascii("Hello from SPL\n");
arch_cpu_init(); init_uart_clk(1); ...}printascii만 쓸 수 있다는 점이 제약입니다. printf는 아직 동작하지 않습니다. 그러나 한 글자라도 나오는지가 첫 갈림길입니다.
- 글자가 나옴 — UART 핀mux·클럭·전원이 살아 있음.
- 글자가 안 나옴 — 의심:
UART_BASE잘못, 클럭 분주 다름, 핀mux 미설정. - 글자가 깨짐 — baudrate divisor 계산이 틀림.
#bdinfo·md·mw — 살아 있는 U-Boot 안에서
일단 U-Boot 콘솔이 떴으면 가장 자주 쓰는 명령 셋입니다.
=> bdinfoarch_number = 0x00000000boot_params = 0x40000100DRAM bank = 0x00000000-> start = 0x40000000-> size = 0x40000000baudrate = 115200 bpsrelocaddr = 0xbff7c000reloc off = 0x7fd7c000fdt_blob = 0xbef8b790reloc off이 의외로 자주 보는 값입니다. U-Boot은 DDR 위쪽으로 재배치되어 동작합니다. 디버깅 시 0x40200000(text base)와 0xbff7c000(relocaddr) 사이의 변환이 필요합니다.
md(memory display)와 mw(memory write)로 메모리를 직접 확인합니다.
=> md.l 0x40000000 440000000: 00000000 00000000 00000000 00000000 ................
=> mw.l 0x40000000 0xdeadbeef=> md.l 0x40000000 440000000: deadbeef 00000000 00000000 00000000 ................
=> md.b 0x40000000 1640000000: ef be ad de 00 00 00 00 00 00 00 00 00 00 00 00 ................이게 DDR이 살아 있는지의 첫 확인입니다. write 후 read가 다르면 DDR 또는 캐시 문제입니다.
=> mtest 0x90000000 0x90100000Testing 90000000 ... 90100000:Pattern AA55AA55 Writing... Reading...Done.mtest로 조금 더 넓은 영역에서 패턴 테스트를 합니다. 부팅 직후 random crash가 보이면 우선 mtest로 DDR 의심을 풀어 둡니다.
#printenv·setenv·saveenv — env 디버깅
env는 부팅 흐름의 데이터가 다 들어 있는 곳입니다.
=> printenvarch=armbaudrate=115200board=boardxbootcmd=run distro_bootcmdbootargs=console=ttyS0,115200 root=/dev/mmcblk0p2 ro...
Environment size: 1234/4092 bytes부팅이 autoboot 도중 멈추면 bootcmd를 한 줄씩 직접 실행해 봅니다.
=> setenv stdout serial # 혹시 다른 콘솔로 가지 않는지=> setenv stderr serial=> run bootcmdenv가 어디에 저장되어 있는지도 확인합니다.
=> env infoEnvironment is in MMC, OK, not flushed=> env saveSaving Environment to MMC...Writing to MMC(0)... OKenv 저장 위치 자체가 unreliable한 보드도 있습니다. 그런 보드는 redundant env(두 사본) 설정을 권합니다.
#JTAG·OpenOCD·gdb-multiarch
시리얼이 아예 안 나오거나, hang하는 정확한 명령어 주소를 알고 싶을 때 JTAG입니다.
# OpenOCD 띄우기 (J-Link로 imx8mm)openocd -f interface/jlink.cfg -f target/imx8mm.cfgOpen On-Chip Debugger 0.12.0Info : J-Link OB-K22-SiFive compiled Apr 4 2023Info : J-Link initialization done.Info : imx8mm.cpu0: hardware has 6 breakpoints, 4 watchpointsInfo : starting gdb server for imx8mm.cpu0 on 3333Info : Listening on port 3333 for gdb connections다른 터미널에서 gdb를 붙입니다.
gdb-multiarch u-boot(gdb) target remote :3333Remote debugging using :33330x0000000000910010 in ?? ()
(gdb) info regx0 0x0 0x1 0x0 0x2 0x0 0x3 0x0 0...pc 0x910010 0x910010sp 0x912000 0x912000
(gdb) bt#0 0x0000000000910010 in spl_dram_init () at board/myvendor/boardx/spl.c:42#1 0x000000000091012c in board_init_f (dummy=0) at board/myvendor/boardx/spl.c:71이 시점부터는 일반 gdb 디버깅입니다. break board_init_f·step·watch로 한 줄씩 확인할 수 있습니다.
(gdb) break dram_initBreakpoint 1 at 0x910128: file board/myvendor/boardx/boardx.c, line 16.(gdb) continueContinuing.
Breakpoint 1, dram_init () at board/myvendor/boardx/boardx.c:1616 gd->ram_size = imx_ddr_size();(gdb) p gd->ram_size$1 = 0(gdb) stepJTAG의 강점은 DDR이 동작하기 전에도 코어를 멈출 수 있다는 점입니다. DDR 의심이 있을 때 immediate한 답을 줍니다.
#post-mortem — 죽은 시스템에서 단서 추출
production 시스템이 부트 도중 패닉했을 때는 ROM의 reset 직후라 RAM이 깨끗합니다. 하지만 부트 후 한참 뒤 죽었으면 RAM에 단서가 남아 있습니다.
U-Boot에서 가장 자주 쓰는 post-mortem 흐름입니다.
- reboot 직후 U-Boot prompt에 멈춤.
md.l <kernel-log-region>으로 dmesg 잔재 확인.- cmdline에
panic=0이 있었으면 kernel이 멈춰 있을 수도 있음. mtest로 DDR 무결성 확인.printenv에서 마지막 bootcmd 추적.
Linux는 pstore·ramoops로 재부팅 후에도 panic 로그를 RAM에 남겨 두는 기능이 있습니다. 그 영역을 U-Boot이 건드리지 않게 DT에 reserved-memory로 박아 둡니다.
reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges;
ramoops@bf000000 { compatible = "ramoops"; reg = <0x0 0xbf000000 0x0 0x100000>; record-size = <0x4000>; console-size = <0x4000>; };};이러면 panic 후 cat /sys/fs/pstore/dmesg-ramoops-0으로 바로 직전의 panic 메시지를 읽을 수 있습니다.
#흔한 부팅 실패 매트릭스
실전에서 자주 만나는 증상과 첫 의심 지점입니다.
| 증상 | 첫 의심 | 두 번째 의심 |
|---|---|---|
| 시리얼에 한 글자도 안 나옴 | UART 베이스·클럭 잘못, 핀mux 미설정 | DEBUG_UART 미활성, baudrate 설정 |
| 깨진 글자만 나옴 | baudrate divisor 오계산 | UART 입력 클럭 값 |
| SPL은 떴는데 “DDR init fail” | DDR timing 잘못, refresh rate | DDR PHY calibration 실패, 전압 |
| DDR은 떴는데 proper U-Boot 점프 직후 hang | 캐시 incoherent, MMU 설정 | text base 충돌, relocation |
| U-Boot은 떴는데 MMC 초기화 실패 | usdhc pinmux, 전원 regulator | bus-width·voltage switching |
bootm이 “Bad Magic Number” | uImage 헤더 손상, raw Image를 bootm으로 부름 | FIT 서명 검증 실패 |
| 커널 점프 후 침묵 | earlycon 누락, ABI 위반 | DTB 자리 충돌, initrd_high 미설정 |
| 커널 부팅 후 “no console” | console= cmdline 누락 | stdout-path가 disable된 노드를 가리킴 |
| 부팅 후 random panic | DDR timing 불안정, 캐시 coherency | 클럭 안정성, 전원 sequencing |
부트로더 디버깅의 60%가 이 표 안에서 해결됩니다. 첫 단계에서 이 증상이 어디에 해당하는지를 먼저 분류하면 시간이 줄어듭니다.
#디버깅 cheat sheet
자주 쓰는 명령을 한 자리에 모았습니다.
정보:
- bdinfo # 보드 정보·메모리 맵
- version # U-Boot 버전
- coninfo # 콘솔 device 정보
- env info # env 저장 상태
- fdt addr ${fdt_addr_r}; fdt print / # DTB 트리
메모리:
- md.l
# 32-bit 단위 read - mw.l
# 32-bit 단위 write - mtest
# 패턴 테스트 - cmp.l
# 두 영역 비교
저장 매체:
- mmc list / mmc info / mmc part
- mmc read
- fatls mmc 0
- ext4ls mmc 0 /boot/
env:
- printenv [var]
- setenv var “value”
- saveenv
- env default -f -a # factory env로 복구
부팅:
- bootm
# uImage/FIT - booti
ramdisk:size# ARM64 raw Image - bootz
ramdisk:size # ARM32 zImage - bootefi
[fdt] # EFI PE - reset # 재부팅
진단:
- go
# raw 점프 (디버깅 전용) - bdinfo
- date # RTC 확인
- i2c probe # I2C 버스 스캔
- usb start; usb tree
#시리즈 마무리
이 시리즈에서 다룬 22편을 한 줄씩 추리면 이렇습니다.
- Ch 1~3: 부트로더가 무엇이고, 어떤 단계로 동작하며, 어떻게 빌드되는가.
- Ch 4~7: SPL, DDR, 스토리지, 환경 변수.
- Ch 8~11: 콘솔, 부팅 명령, 스크립팅, distro_bootcmd.
- Ch 12~15: 네트워크 부팅, USB, MMC·NAND, FIT 이미지.
- Ch 16~18: Verified Boot, A/B, EFI.
- Ch 19~20: 커널 인계, OTA 프레임워크.
- Ch 21~22: 새 보드 포팅, 디버깅.
부트로더는 작은 OS입니다. 메모리 관리도, 드라이버도, 명령 인터프리터도 다 들어 있습니다. 이 시리즈를 다 읽었다면 임의의 보드의 부팅 흐름을 처음부터 끝까지 따라갈 수 있는 지도가 생긴 셈입니다.
#다음 시리즈 추천
부트로더와 가까운 인접 영역의 시리즈입니다.
| 시리즈 | 이어지는 지점 |
|---|---|
| BSP Development | 부트로더 위로 커널·드라이버·rootfs 전체 BSP 구성 |
| Buildroot Practical | 부트로더·커널·rootfs 빌드 시스템과 OTA 패키징 |
| Embedded Security | Secure Boot·HSM·TPM·anti-rollback의 위 시점 |
| Practical RTOS Internals | 부트로더 없는·또는 minimal한 시스템의 시작 |
BSP 시리즈는 이 시리즈와 짝으로 읽으면 좋습니다. 같은 보드를 BSP 관점에서 다시 보면 부트로더의 산출물이 BSP에 어떻게 흡수되는지가 더 선명해집니다.
#자주 하는 실수
디버깅 자체의 실수입니다.
- DEBUG_UART를 켜 두고 production 빌드를 만든다. 부팅 직후 잡음이 콘솔에 섞여 양산 출하 시 깨끗하지 않습니다.
- JTAG으로 멈춰 놓은 채로 재부팅한다. OpenOCD가 잡고 있는 코어는 정상 재부팅을 못 합니다.
reset run또는 OpenOCD 종료 후 reboot. mw로 reserved 영역에 쓴다. secure region이나 TZASC 보호 영역에 쓰면 abort. address validation 필수.go <addr>을 함부로 쓴다. 인자·스택·캐시 정리가 전혀 안 된 상태로 점프하니, 디버깅용 raw 진입 외에는 쓰지 않습니다.- post-mortem에서 ramoops 영역을 U-Boot이 zero-fill한다. reserved-memory로 박혀 있어야 보존됩니다.
printenv만 보고 env 저장은 안 한다. 변경한 변수는saveenv전까지 부팅 사이 보존되지 않습니다.
#정리
- DEBUG_UART는 부트로더보다 이른 시점에 첫 글자를 끌어내는 마지막 보루입니다.
- bdinfo·md·mw·mtest는 살아 있는 U-Boot 안에서 메모리 상태를 직접 본 첫 번째 도구입니다.
- JTAG + OpenOCD + gdb-multiarch는 DDR 이전 코드까지 멈춰 잡을 수 있게 해 줍니다.
- post-mortem은 ramoops 영역을 reserved-memory로 박아 재부팅 후에도 panic 로그를 읽을 수 있게 합니다.
- 부팅 실패 매트릭스(콘솔 안 나옴 → UART, DDR 실패 → timing, 커널 침묵 → ABI)가 첫 분류의 빠른 지름길입니다.
- env는 부팅 흐름의 데이터입니다. 흐름이 막히면 env부터 봅니다.
- 이 시리즈를 마치면 임의의 임베디드 보드의 부팅을 추적·진단·수정할 수 있는 도구가 갖춰집니다.
#관련 항목
Bootloader Internals · 22 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 인식까지.
U-Boot Distro Boot — extlinux·boot.scr 표준화 분석
보드별 다른 부트 스크립트를 표준화 — U-Boot Distro Boot, extlinux.conf, boot.scr의 차이와 선택.
SPL·TPL 내부 해부 — 가장 작은 부트 단계의 동작 추적
SPL과 TPL의 정확한 역할, SRAM 안에 들어가는 코드 구조, DDR이 없는 환경에서 어떻게 동작하는가.