본문으로 건너뛰기
Bootloader Internals · 6/37

Device Tree DTB 부트로더 처리 — 로딩 시점과 fixup 메커니즘 추적

· Hawk · 6분 읽기

#한 줄 요약

“U-Boot은 두 개의 DTB를 다룹니다.” — 하나는 자기 자신의 driver model이 사용하는 control DTB, 다른 하나는 커널에 넘기는 OS DTB. 같은 .dtb일 수도 있고 다를 수도 있습니다. 런타임 fixup은 OS DTB만 대상입니다.

Device Tree는 Linux의 발명입니다만 U-Boot도 전적으로 차용합니다. U-Boot 2.0 시대(2014년경)부터 Driver Model이 DT 기반으로 동작하기 시작했고, 지금은 DT 없이 동작하는 U-Boot이 거의 없습니다. 동시에 U-Boot은 커널에 넘길 DT도 다뤄야 하므로 “두 개의 DTB”가 공존합니다.

#control DTB vs OS DTB

핵심 개념을 먼저 정리합니다.

control DTB

  • U-Boot 자기 자신의 driver model이 사용
  • U-Boot binary 내부에 embed되거나, 별도 binary
  • U-Boot의 MMC, UART, GPIO 등을 정의

OS DTB

  • 커널에 넘기는 DT
  • 부트 미디어의 파일(예: imx8mp-evk.dtb)
  • 커널의 모든 device를 정의
  • U-Boot이 런타임에 fixup 가능

같은 .dtb일 수도 있고 서로 다른 .dtb일 수도 있습니다. NXP i.MX는 같은 dtb를 쓰는 경향이고, 일부 SoC는 U-Boot용 dtb를 별도로 빌드합니다.

#control DTB의 세 가지 방법

U-Boot은 control DTB를 어떻게 가져오는지에 따라 세 가지 옵션이 있습니다.

#CONFIG_OF_EMBED — U-Boot binary에 embed

CONFIG_OF_EMBED=y

U-Boot binary의 .data 섹션에 DTB를 직접 박아 넣습니다. 빌드 시점에 결정됩니다.

u-boot.bin (1.2 MB)
├── .text (코드)
├── .data
│ └── (embedded DTB, 50 KB)
└── ...

장점: DTB가 분리된 파일이 아님. SPL이 적재할 게 적습니다. 단점: DTB 수정 시 U-Boot 재빌드.

#CONFIG_OF_SEPARATE — 별도 binary

CONFIG_OF_SEPARATE=y

U-Boot binary와 DTB가 별도 파일입니다. 빌드 후 concat합니다.

u-boot.bin (1.2 MB)
u-boot.dtb (50 KB)
u-boot-dtb.bin = u-boot.bin + u-boot.dtb ← 이걸 부트 미디어에

장점: DTB만 수정해 교체 가능. 단점: 빌드 단계에서 concat 필요.

#CONFIG_OF_BOARD — 런타임에 가져옴

CONFIG_OF_BOARD=y

U-Boot이 부트 시점에 DTB를 어딘가에서 가져옵니다. 보통 전 단계가 메모리에 적재해 준 DTB.

/* 보드 코드가 정의해야 함 */
void *board_fdt_blob_setup(int *err)
{
/* 0x40000000에 SPL이 적재해 둔 DTB */
*err = 0;
return (void *)0x40000000;
}

QEMU virt가 이 방식입니다. QEMU가 메모리에 DTB를 준비해 두고, U-Boot이 그 주소를 받아 갑니다.

#DTB가 언제 사용되는가

U-Boot이 control DTB를 읽는 시점board_init_f 매우 초반입니다.

/* common/board_f.c (간략화) */
static const init_fnc_t init_sequence_f[] = {
setup_mon_len,
fdtdec_setup, /* ← DTB 위치 확정 */
initf_malloc,
arch_cpu_init,
initf_dm, /* ← Driver Model 초기화, DT 파싱 */
...
};

fdtdec_setup()DTB의 위치를 결정하고, initf_dm()DT를 파싱해 driver 인스턴스를 만듭니다.

lib/fdtdec.c
int fdtdec_setup(void)
{
#if CONFIG_IS_ENABLED(OF_EMBED)
gd->fdt_blob = __dtb_dt_begin;
#elif CONFIG_IS_ENABLED(OF_SEPARATE)
gd->fdt_blob = &_end;
#elif CONFIG_IS_ENABLED(OF_BOARD)
gd->fdt_blob = board_fdt_blob_setup(&err);
#endif
return 0;
}

gd->fdt_blobcontrol DTB의 메모리 주소입니다. 이 시점부터 모든 코드가 DT를 읽을 수 있습니다.

#fdt 명령

U-Boot의 명령 인터프리터는 런타임에 DTB를 조작할 수 있는 fdt 명령군을 제공합니다.

=> help fdt
fdt - flattened device tree utility commands
Usage:
fdt addr <addr> - Set the fdt location to <addr>
fdt move <fdt> <newaddr> - Copy the fdt to <newaddr>
fdt resize [<extrasize>] - Resize fdt to size + padding
fdt print <path> - Recursive print starting at <path>
fdt list <path> - Print one level starting at <path>
fdt get value <var> <path> <prop>
fdt set <path> <prop> [<val>]
fdt mknode <path> <node>
fdt rm <path> [<prop>]
fdt chosen [<start> [<end>]]
fdt fixup

#기본 사용

=> load mmc 0:1 0x43000000 imx8mp-evk.dtb
26580 bytes read in 9 ms (2.8 MiB/s)
=> fdt addr 0x43000000
=> fdt print /
/ {
compatible = "fsl,imx8mp-evk", "fsl,imx8mp";
model = "NXP i.MX8MPlus EVK";
#address-cells = <0x02>;
#size-cells = <0x02>;
aliases { ... };
chosen { ... };
cpus { ... };
memory@40000000 { ... };
...
};
=> fdt print /chosen
chosen {
stdout-path = "serial0:115200n8";
bootargs = "";
};
=> fdt set /chosen bootargs "console=ttymxc1,115200 root=/dev/mmcblk0p2 rw"
=> fdt print /chosen
chosen {
stdout-path = "serial0:115200n8";
bootargs = "console=ttymxc1,115200 root=/dev/mmcblk0p2 rw";
};

bootargschosen 노드에 들어갑니다. 커널이 이 값을 commandline으로 읽습니다.

#노드 추가

=> fdt mknode /soc my-extra-device
=> fdt set /soc/my-extra-device compatible "vendor,my-driver"
=> fdt set /soc/my-extra-device reg "<0x30890000 0x1000>"
=> fdt set /soc/my-extra-device status "okay"

이렇게 만든 DT를 커널에 넘기면, 커널이 해당 device를 인식합니다.

#런타임 fixup

DTB의 특정 필드부트 시점에 확정되어야 합니다. 빌드 시점에 미리 적어 두기 어렵습니다.

필드빌드 시 알 수 있는가
메모리 크기아니오 (보드별, 옵션별로 다름)
MAC 주소아니오 (개체별)
Serial number아니오 (개체별)
부트 디바이스아니오 (boot mode 따라)
Kernel cmdline아니오 (부트 정책에 따라)

U-Boot이 런타임에 이 필드들을 DT에 주입합니다. 이 과정이 fixup입니다.

arch/arm/lib/bootm-fdt.c
int arch_fixup_fdt(void *blob)
{
int ret;
/* 메모리 크기 fixup */
ret = fdt_fixup_memory_banks(blob,
gd->bd->bi_dram[0].start,
gd->bd->bi_dram[0].size,
CONFIG_NR_DRAM_BANKS);
if (ret)
return ret;
/* PSCI 노드 fixup */
fdt_psci(blob);
return 0;
}

fdt_fixup_memory_banks()가 DT의 /memory@xxx 노드에 실제 DDR 크기를 씁니다.

[Build-time DTB]
memory@40000000 {
device_type = "memory";
reg = <0 0x40000000 0 0x80000000>; ← 2GB로 박혀 있음
};
[Runtime fixup 후]
memory@40000000 {
device_type = "memory";
reg = <0 0x40000000 0 0x100000000>; ← 4GB로 수정됨 (실제 RAM)
};

#MAC 주소 fixup

이더넷 MAC 주소는 공장 fuse 또는 EEPROM에서 읽어 DT에 주입합니다.

/* board/<vendor>/<board>/<board>.c */
int ft_board_setup(void *blob, struct bd_info *bd)
{
u8 mac[6];
int offset;
/* fuse 또는 EEPROM에서 MAC 읽기 */
read_mac_from_fuse(mac);
/* DT의 ethernet 노드 찾기 */
offset = fdt_path_offset(blob, "/soc/ethernet@30be0000");
if (offset < 0)
return offset;
/* mac-address 프로퍼티 설정 */
fdt_setprop(blob, offset, "mac-address", mac, 6);
return 0;
}

ft_board_setup()은 U-Boot이 커널로 점프 직전에 호출하는 훅 함수입니다. 보드 코드에서 원하는 fixup을 자유롭게 합니다.

#chosen 노드 — bootargs와 stdout

/chosen 노드는 커널에 전달하는 명령줄과 console 정보가 들어갑니다.

/ {
chosen {
bootargs = "console=ttymxc1,115200 root=/dev/mmcblk0p2 rw";
stdout-path = "serial0:115200n8";
linux,initrd-start = <0x46000000>;
linux,initrd-end = <0x46f00000>;
};
};

bootargs는 U-Boot의 환경 변수 bootargs부트 직전에 여기에 복사됩니다.

common/fdt_support.c
void fdt_chosen(void *fdt)
{
int nodeoffset;
const char *bootargs;
nodeoffset = fdt_find_or_add_subnode(fdt, 0, "chosen");
bootargs = env_get("bootargs");
if (bootargs)
fdt_setprop(fdt, nodeoffset, "bootargs", bootargs,
strlen(bootargs) + 1);
}

bootibootm 명령이 내부적으로 fdt_chosen()을 호출합니다.

#부트 흐름에서의 DTB

전체 부트에서 DTB가 어떻게 전파되는지 봅니다.

두 DTB 흐름 — SPL/U-Boot의 control DTB와 커널로 넘기는 OS DTB

control DTB와 OS DTB는 다른 메모리 위치에 있습니다.

0x40080000 - U-Boot Proper의 control DTB (embed인 경우 binary 끝에)
0x43000000 - OS DTB (mmc에서 load한 곳)

#같은 DT를 양쪽에 쓰기

NXP의 i.MX 8M Plus EVK처럼 같은 imx8mp-evk.dtbU-Boot 자기 자신용커널용 양쪽에 쓰는 경우가 흔합니다. arch/arm/dts/imx8mp-evk.dts가 kernel과 U-Boot의 정의를 모두 포함하도록 작성됩니다.

/* arch/arm/dts/imx8mp-evk.dts (U-Boot의 dts) */
#include "imx8mp.dtsi" /* 커널과 공유 */
/ {
model = "NXP i.MX8MPlus EVK";
compatible = "fsl,imx8mp-evk", "fsl,imx8mp";
/* 커널이 모르는 U-Boot 전용 노드 */
binman: binman { ... };
};
&uart2 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart2>;
status = "okay";
};

binman 같은 U-Boot 전용 노드는 커널이 무시합니다. status를 okay/disabled로 양쪽이 각자 사용하는 device만 활성화하는 패턴도 일반적입니다.

#U-Boot의 device tree overlay

U-Boot은 device tree overlay(.dtbo)도 지원합니다. 부트 시점에 base DToverlay를 합쳐 최종 DT를 만들 수 있습니다.

=> load mmc 0:1 0x43000000 imx8mp-evk.dtb
=> load mmc 0:1 0x44000000 my-overlay.dtbo
=> fdt addr 0x43000000
=> fdt resize 8192
=> fdt apply 0x44000000
=> booti 0x40480000 - 0x43000000

fdt apply가 overlay를 base에 merge합니다. 같은 하드웨어 base + 다른 페리페럴을 가진 변종 보드에 유용합니다.

#fdtoverlay 도구

호스트에서 미리 overlay를 적용한 결과 .dtb를 만들 수도 있습니다.

Terminal window
fdtoverlay -i imx8mp-evk.dtb \
-o imx8mp-evk-with-camera.dtb \
camera-overlay.dtbo
# imx8mp-evk-with-camera.dtb를 부트 미디어에 굽기

런타임 overlay는 부트 시간을 늘리므로 양산용은 호스트에서 미리 합쳐 굽는 것이 일반적입니다.

#자주 하는 실수

#fdt addr 안 하고 fdt 명령

=> fdt print /
fdt is not set

fdt addr <주소>작업 대상 DTB의 위치를 먼저 지정해야 합니다.

#fdt resize 안 하고 큰 fixup

DTB는 내부 공간이 빠듯하게 잡혀 있습니다. 노드/프로퍼티를 추가하면 공간 부족으로 fixup이 조용히 실패합니다.

=> fdt resize 4096 ← 4KB 여유 공간 추가
=> fdt set /chosen bootargs "..."

#control DTB와 OS DTB 혼동

U-Boot 명령 인터프리터에서 fdt set을 할 때 어느 DTB를 수정하는지 헷갈리기 쉽습니다. fdt addr <주소>명시해야 안전.

[control DTB 수정 — 비추천, U-Boot이 동작 중 사용]
=> fdt addr ${fdtcontroladdr}
[OS DTB 수정 — 정상]
=> load mmc 0:1 0x43000000 imx8mp-evk.dtb
=> fdt addr 0x43000000
=> fdt set ...

#booti3번째 인자- 잊음

booti <kernel_addr> [<initrd>] <fdt_addr>. initrd가 없으면 *그 자리에 -*를 써야 합니다.

=> booti 0x40480000 0x43000000 ← 잘못. fdt가 initrd로 해석됨
=> booti 0x40480000 - 0x43000000 ← OK

#MAC 주소 fixup이 bootcmd 안에서 실행 안 됨

ft_board_setup()bootm/booti 직전에 호출됩니다. 그 이전에 fdt print로 본 DTB에는 MAC fixup이 아직 안 들어가 있을 수 있습니다.

Terminal window
=> fdt print /soc/ethernet@30be0000
ethernet@30be0000 {
mac-address = [00 00 00 00 00 00]; # 아직 fixup 전
};
=> booti ...
# 부팅 후 ip a → 실제 MAC이 들어가 있음

#fdt_get_property() vs fdt_getprop()

U-Boot 코드에서 둘 다 보이지만 반환 타입이 다름. fdt_getprop()데이터 포인터를 반환합니다. 새 코드는 fdt_getprop() 권장.

#빌드 시 dtc not found

HOSTCC scripts/dtc/dtc.c
make: dtc: Command not found

Ubuntu/Debian: sudo apt install device-tree-compiler. U-Boot이 호스트 dtc도 빌드해 두지만 일부 환경에서는 시스템 dtc를 요구합니다.

#정리

  • U-Boot은 control DTB(자기 자신용)와 OS DTB(커널용)의 두 DT를 다룹니다.
  • control DTB는 CONFIG_OF_EMBED(embed), CONFIG_OF_SEPARATE(별도 binary), CONFIG_OF_BOARD(런타임) 중 한 방식으로 얻습니다.
  • gd->fdt_blob이 control DTB의 메모리 주소입니다. fdtdec_setup()이 board_init_f 초반에 확정.
  • fdt 명령군은 런타임에 DTB를 조작합니다. fdt addr로 대상 DTB를 지정한 뒤 fdt set, fdt mknode 등 사용.
  • 런타임 fixup빌드 시점에 모를 정보(메모리 크기, MAC, serial)를 DT에 주입합니다. ft_board_setup()이 보드 훅.
  • /chosen 노드는 bootargs와 stdout-path가 들어갑니다. 환경 변수 bootargs가 자동으로 복사됩니다.
  • fdt applydevice tree overlay를 런타임에 합칠 수 있습니다. 양산은 호스트에서 미리 합치는 편이 일반적.
  • booti <kernel> - <fdt>처럼 *initrd 자리에 -*를 꼭 넣습니다.

#다음 편

Ch 7: Driver Model — uclass, driver, device에서는 U-Boot의 driver model을 정리합니다. control DTB가 어떻게 driver 인스턴스로 변환되는지, uclass·driver·udevice 삼각 구조와 compatible 기반 binding을 봅니다.

#관련 항목

Bootloader Internals · 6 of 37

  1. 1ROM부터 init까지 — 임베디드 부팅 단계의 빈자리 분석
  2. 2Das U-Boot vs TF-A vs EDK II — 임베디드 부트로더 생태계 비교
  3. 3U-Boot 빌드 시스템 분석 — Kconfig·Makefile·defconfig 동작 추적
  4. 4ARM 임베디드 부트 4단계 분해 — BL1·SPL·TPL·U-Boot Proper의 역할
  5. 5U-Boot Falcon Mode — SPL이 U-Boot Proper 없이 커널 직접 부팅
  6. 6Device Tree DTB 부트로더 처리 — 로딩 시점과 fixup 메커니즘 추적
  7. 7U-Boot Driver Model 내부 — uclass·driver·device 추상화 구조
  8. 8U-Boot 보드 초기화 시퀀스 — board_init_f와 board_init_r 분리 이유
  9. 9DDR Controller 프로그래밍과 PHY Training — SPL의 가장 어려운 작업
  10. 10임베디드 스토리지 부팅 분석 — MMC·SCSI·NAND·SPI Flash 비교
  11. 11임베디드 네트워크 부팅 — TFTP·PXE·BOOTP 시퀀스 분석
  12. 12U-Boot USB 부팅 — fastboot·UMS·USB host 메커니즘
  13. 13U-Boot 환경 변수와 bootcmd — 부팅 시나리오 정의하기
  14. 14Modern U-Boot bootflow / bootmeth — 새 추상화 레이어 분석
  15. 15FIT image 구조 분석 — multi-image·hash·configuration 추적
  16. 16U-Boot Verified Boot — RSA 서명과 public key 임베딩 흐름
  17. 17임베디드 A/B 부팅 이중화 — OTA 안전성을 위한 부트 슬롯 설계
  18. 18U-Boot의 EFI 호환 분석 — bootefi 명령과 EFI loader 동작 원리
  19. 19Linux Boot ABI — ARM/ARM64 커널 진입 규약 추적
  20. 20임베디드 펌웨어 업데이트 — RAUC vs SWUpdate 비교
  21. 21새 보드 U-Boot 포팅 실전 — defconfig 작성부터 첫 부팅까지
  22. 22부트로더 디버깅 기법 — DEBUG·JTAG·serial·post-mortem 분석
  23. 23SoC BootROM·eFuse·OTP — 부팅의 0단계 분석
  24. 24SPL·TPL 내부 해부 — 가장 작은 부트 단계의 동작 추적
  25. 25ARM Trusted Firmware-A 통합 — BL1·BL2·BL31·BL32·BL33 흐름
  26. 26DDR Training과 PHY Calibration — 보드별 파라미터 튜닝
  27. 27임베디드 Chain of Trust — 다단계 서명 검증의 전체 흐름
  28. 28임베디드 Flash Layout 설계 — partition·NAND·eMMC·UBI 비교
  29. 29U-Boot Distro Boot — extlinux·boot.scr 표준화 분석
  30. 30부트로더 CI 구축 — build matrix와 자동 부팅 테스트
  31. 31TF-A BL31 EL3 Runtime 분석 — PSCI·SDEI·RAS dispatcher 추적
  32. 32PSCI와 SMCCC ABI — ARM 표준 SMC 호출 규약 분석
  33. 33ARM64 Secondary Core Bring-up — PSCI CPU_ON 호출부터 EL1 진입까지
  34. 34U-Boot PCIe Enumeration — 부트로더가 디바이스를 찾는 흐름 분석
  35. 35EFI·UEFI에서 CXL 초기화 — CEDT 생성과 HDM Decoder 사전 설정
  36. 36부트 시 메모리 토폴로지 결정 — DDR + CXL.mem 통합 인식
  37. 37UEFI Secure Boot 인증서 만료 — 2011→2023 CA 롤오버와 PQC 대비