Embedded : : Linux/: : Device Tree

[Device Tree] 13. FDT 바이너리 포맷 (Flattened Device Tree)

Jay.P Morgan 2026. 3. 4. 17:01

 

 

 

  13.  FDT 바이너리 포맷 (Flattened Device Tree)

 

FDT(Flattened Device Tree)는 우리가 텍스트로 작성한 .dts 파일을 dtc(컴파일러)가 기계가 읽기 좋게 한 줄로 쫙 펴서 만든 바이너리 포맷(.dtb)을 의미합니다.

 

부트로더(U-Boot 등)가 커널을 올릴 때 메모리 특정 주소에 이 DTB를 딱 던져주면, 커널은 이 0과 1의 덩어리를 해석해서 하드웨어 구성을 파악하죠. 이 바이너리가 내부적으로 어떻게 생겼는지 핵심을 짚어드릴게요.

 

  13.1  FDT의 전체 레이아웃 (Layout)

 

FDT 바이너리는 크게 4가지 영역으로 나뉩니다. 데이터의 효율성과 빠른 탐색을 위해 설계되었습니다.

영역 설명
fdt_header 바이너리의 전체 지도. 버전, 각 블록의 위치(offset)와 크기 정보가 담겨 있음.
Memory Reservation Block 커널이 건드리면 안 되는 보호된 메모리 영역 목록.
Structure Block 노드와 프로퍼티의 트리 구조를 선형적으로 펼쳐놓은 본체.
Strings Block 프로퍼티 이름(compatible, reg 등)을 모아놓은 문자열 저장소.

 

 

 

  13.2  헤더 (Header) 구조

 

바이너리의 맨 앞부분입니다. 커널은 가장 먼저 magic 숫자를 보고 "아, 이게 진짜 디바이스 트리구나!"라고 확신합니다.

struct fdt_header {
    uint32_t magic;               /* 0xd00dfeed (그 유명한 먹이주기 매직넘버) */
    uint32_t totalsize;           /* DTB 전체 크기 */
    uint32_t off_dt_struct;       /* Structure Block 시작 위치 */
    uint32_t off_dt_strings;      /* Strings Block 시작 위치 */
    uint32_t off_mem_rsvmap;      /* Memory Reservation Block 시작 위치 */
    uint32_t version;             /* FDT 포맷 버전 */
    uint32_t last_comp_version;   /* 호환 가능한 최소 버전 */
    /* ... 기타 사양 정보 ... */
};

💡위트: FDT의 매직 넘버는 0xd00dfeed (DOOD FEED)입니다. "하드웨어 정보를 커널에게 먹여준다"는 위트가 담겨 있죠.

 

 

  13.3  구조 블록 (Structure Block)의 토큰

 

이 블록은 트리를 전위 순회(Pre-order traversal) 방식으로 펼쳐놓았습니다. 데이터는 **토큰(Token)**이라는 32비트 단위로 구분됩니다.

  • FDT_BEGIN_NODE (0x00000001): 새로운 노드의 시작 (노드 이름이 뒤따름).
  • FDT_PROP (0x00000003): 프로퍼티의 시작 (데이터 크기, 이름 위치, 실제 데이터가 뒤따름).
  • FDT_END_NODE (0x00000002): 현재 노드의 끝.
  • FDT_END (0x00000009): 전체 트리의 끝.

 

 

  13.4  왜 문자열 블록(Strings Block)은 따로 있나요?

 

공간을 아끼기 위해서입니다! 예를 들어 100개의 노드가 각각 status = "okay"라는 프로퍼티를 가진다면, status라는 글자를 100번 저장하는 건 낭비죠. 그래서 status라는 글자는 문자열 블록에 딱 한 번만 저장하고, 구조 블록에서는 그 문자열이 있는 위치(Offset) 번호만 적어둡니다. 일종의 데이터 압축 기술인 셈입니다.

 

 

  13.5  주요 특징 (Technical Details)

 

  • 엔디안(Endianness): 모든 수치는 빅 엔디안(Big-endian)으로 저장됩니다. (Open Firmware의 전통)
  • 정렬(Alignment): 모든 데이터 조각은 4바이트(32비트) 단위로 정렬되어야 합니다. 빈 공간은 0으로 채워집니다.
  • 속도: FDT는 순차적으로 읽기에 최적화되어 있어, 부팅 시 커널이 아주 빠르게 트리를 파싱할 수 있습니다.

 

DTS가 사람이 읽는 설명서라면, FDT(DTB)는 커널이 한 눈에 읽어 내려갈 수 있도록 최적화된 바이너리 리포트입니다.

 

 

DTB 파일은 Flattened Device Tree(FDT) 바이너리 포맷으로 저장됩니다. 부트로더가 이 바이너리를 메모리에 로드하고, 커널의 unflatten_device_tree()가 파싱하여 struct device_node 트리를 구축합니다.

 

 

 

  13.6  FDT 실전: boot-early stage

 

코드는 리눅스 커널이 부팅될 때, 부트로더로부터 넘겨받은 FDT(Flattened Device Tree) 바이너리 덩어리를 어떻게 해석하고 자신의 데이터 구조로 변환하는지를 보여주는 핵심 로직입니다.

단순한 데이터 구조를 넘어, 리눅스 커널 부팅의 "Early Stage(초기 단계)"가 어떻게 돌아가는지 조목조목 짚어드릴게요.

 
  /* ===== FDT 헤더 구조체 (include/linux/libfdt_env.h → scripts/dtc/libfdt/) ===== */
 
  struct fdt_header {
         fdt32_t magic;                         /* 0xD00DFEED (big-endian) */
         fdt32_t totalsize;                     /* DTB 전체 크기 (bytes) */
         fdt32_t off_dt_struct;             /* Structure Block 시작 오프셋 */
         fdt32_t off_dt_strings;           /* Strings Block 시작 오프셋 */
         fdt32_t off_mem_rsvmap;     /* Memory Reservation Block 오프셋 */
         fdt32_t version;                       /* 포맷 버전 (현재 17) */
         fdt32_t last_comp_version;   /* 호환 가능한 최소 버전 (16) */
         fdt32_t boot_cpuid_phys;     /* 부팅 CPU의 physical ID */
         fdt32_t size_dt_strings;         /* Strings Block 크기 */
         fdt32_t size_dt_struct;           /* Structure Block 크기 */
  };
 
  /* FDT는 모두 big-endian으로 저장됨 — cpu_to_fdt32() / fdt32_to_cpu() 로 변환 */
 
  /* ===== Structure Block 토큰 ===== */
  #define FDT_BEGIN_NODE  0x00000001    /* 노드 시작 + 이름(NUL종료, 4-byte 정렬) */
  #define FDT_END_NODE      0x00000002   /* 노드 종료 */
  #define FDT_PROP                0x00000003   /* 프로퍼티: len(u32) + nameoff(u32) + data */
  #define FDT_NOP                  0x00000004   /* 무시 (편집 시 패딩용) */
  #define FDT_END                  0x00000009   /* Structure Block 종료 */
 
  /* ===== Memory Reservation Block =====
   * 커널이 사용하면 안 되는 물리 메모리 영역 (예: DTB 자체, 펌웨어 영역)
   * { uint64_t address; uint64_t size; } 쌍의 배열
   * address=0, size=0 엔트리로 종료
   *
   * 참고: reserved-memory DT 노드와 다름!
   * - Memory Reservation Block: FDT 바이너리 레벨, early boot에서 처리
   * - reserved-memory 노드: DT 노드 레벨, memblock 서브시스템에서 처리
   */
 
  /* ===== FDT 프로퍼티 인코딩 예시 =====
   *
   * DTS: compatible = "myvendor,my-soc-uart", "snps,dw-apb-uart";
   *
   * Structure Block에 저장되는 바이너리:
   * [FDT_PROP] ← 0x00000003
   * [len = 39] ← 두 문자열 + NUL 포함 길이
   * [nameoff = 0] ← Strings Block에서 "compatible" 오프셋
   * "myvendor,my-soc-uart\0snps,dw-apb-uart\0" ← 실제 데이터
   * [padding] ← 4-byte 정렬 맞춤
   *
   * DTS: reg = <0x1c28000 0x400>;
   *
   * [FDT_PROP]
   * [len = 8] ← u32 × 2 = 8 bytes
   * [nameoff = 11] ← Strings Block에서 "reg" 오프셋
   * [0x01C28000] [0x00000400] ← big-endian u32 값들
   */
 
  /* ===== 커널에서 FDT 직접 접근 (early boot) ===== */
  #include <linux/of_fdt.h>
 
  /* early_init_dt_scan(): 부팅 초기에 FDT에서 핵심 정보 추출 */
  void __init early_init_dt_scan_nodes(void)
  {
         /* chosen 노드에서 bootargs, initrd 위치 추출 */
         early_init_dt_scan_chosen(boot_command_line);
 
         /* /memory 노드에서 물리 메모리 범위 추출 → memblock에 등록 */
         early_init_dt_scan_memory();
 
         /* root 노드에서 #address-cells, #size-cells 가져오기 */
         early_init_dt_scan_root();
  }
 
  /* unflatten: FDT 바이너리 → struct device_node 트리 변환 */
  void __init unflatten_device_tree(void)
  {
         /* 1차 패스: 필요한 메모리 크기 계산 */
         /* 2차 패스: device_node + property 구조체 할당 및 연결 */
         __unflatten_device_tree(initial_boot_params, NULL,
                                                    &of_root, early_init_dt_alloc_memory_arch, false);
         /* of_root: 전역 루트 device_node 포인터 */
         /* /proc/device-tree/와 /sys/firmware/devicetree/base/로 노출 */
  }
 

 


1. FDT 헤더: "바이너리 지도의 범례"

fdt_header는 DTB 파일의 맨 앞부분입니다. 여기서 가장 중요한 것은 오프셋(Offset) 정보입니다.

  • Magic Number (0xD00DFEED): 커널이 "이 데이터는 가짜가 아니다"라고 확신하는 지점입니다.
  • Endianness: FDT는 무조건 Big-endian입니다. ARM64 같은 Little-endian 아키텍처에서도 데이터를 읽을 때 반드시 fdt32_to_cpu() 같은 함수로 뒤집어서 읽어야 합니다.

2. 프로퍼티 인코딩: "공간 절약의 묘미"

예시로 들어주신 compatible과 reg 인코딩 부분은 FDT가 얼마나 효율적으로 데이터를 압축하는지 보여줍니다.

  • String List: compatible처럼 여러 문자열이 올 때는 각 문자열 끝에 \0을 붙여서 쭉 이어 붙입니다.
  • Strings Block: "compatible", "reg" 같은 속성 이름들은 별도의 저장소(Strings Block)에 한 번만 저장하고, nameoff라는 인덱스 값으로 가리키게 하여 중복 저장을 막습니다.

3. Memory Reservation Block vs reserved-memory 노드

코드 주석에도 나와 있듯이, 이 둘은 "누가 처리하느냐"의 차이입니다.

  • Reservation Block: 커널이 채 부팅되기도 전, 매우 초기 단계에서 "여긴 절대 건드리지 마!"라고 물리 메모리 영역을 못 박는 용도입니다. (예: DTB가 놓인 자리)
  • reserved-memory 노드: 커널의 메모리 관리 시스템(CMA 등)이 올라온 뒤, 특정 용도(드라이버용 전용 메모리 등)를 위해 예약하는 논리적 설정입니다.

4. 커널의 2단계 처리 과정

1단계: Early Scan (early_init_dt_scan_nodes)

커널이 전체 트리를 메모리에 올리기엔 너무 이른 시점(MMU도 켜지기 전 등)에 수행합니다.

  • chosen: "어떤 콘솔을 쓸까?", "부팅 인자(bootargs)는 뭐지?"를 먼저 파악합니다.
  • memory: "내가 쓸 수 있는 RAM이 어디서부터 어디까지지?"를 알아내어 Memblock 시스템에 등록합니다.

2단계: Unflatten (unflatten_device_tree)

바이너리 형태인 FDT를 커널 내부의 링크드 리스트 구조인 struct device_node로 바꾸는 "변신" 과정입니다.

  • Pass 1: 필요한 메모리 양을 미리 계산합니다.
  • Pass 2: 계산된 만큼 메모리를 한 번에 할당받아 트리를 구성합니다.
  • 이 과정이 끝나면 우리가 흔히 보는 /proc/device-tree를 통해 텍스트 형태로 하드웨어 정보를 확인할 수 있게 됩니다.

💡 핵심 요약

이 코드는 "바이너리(DTB) → 초기 스캔(Early Scan) → 객체화(Unflatten)"로 이어지는 커널의 하드웨어 인식 파이프라인을 설명하고 있습니다.

결국 커널은 이 과정을 거쳐야만 "아, 내 주위에 UART가 있고, 메모리가 이만큼 있구나!"라고 자아를 성찰하게 되는 것이죠.

 

 

 

참고: https://www.minzkn.com/linuxkernel/pages/device-tree.html#dt-compile