리눅스 ARM64에서 기본적으로 커널의 어떤 옵션을 활성화할지는 DTB에 정의하고 커널 로드시에 DTB를 참조해서 동적으로 옵션을 활성화 한다.
디바이스 트리는 보통 디바이스 트리 스크립트언어로 작성하고 이를 컴파일해서 바이너리 파일인 DTB(FDT)로 만들어서 사용하게 된다.
과정을 정리하면 Device Tree script를 컴파일해서 DTB(FDT)라는 바이너리 파일로만들고 커널에서는 해당 바이너리 파일을 파싱해서 slab cache로 변환 후 필요한 옵션들을 트리 format으로 가져와서 사용한다.
1. Makefile
기본적으로 device tree를 작성하는 .dts파일은 Boot 시에 Makefile에 추가되어 참조된다. 때문에 board에 맞는 DTB를 작성하고 해당 파일을 Make file에 연결해주어야 한다. Makefile의 경로는 Raspi3(broadcom vendor chipset) 기준으로 다음과 같다. <arch/arm64/boot/dts/broadcom/Makefile>
# SPDX-License-Identifier: GPL-2.0 dtb-$(CONFIG_ARCH_BCM2835) += bcm2711-rpi-400.dtb \ bcm2711-rpi-4-b.dtb \ bcm2837-rpi-3-a-plus.dtb \ bcm2837-rpi-3-b.dtb \ bcm2837-rpi-3-b-plus.dtb \ bcm2837-rpi-cm3-io3.dtb subdir-y += bcm4908 subdir-y += northstar2 subdir-y += stingray |
ARM64 부터는 chipset 제조사별로 코드를 개별구현하여 무분별한 코드가 생성되는 것을 막기위해 디바이스트리를 통해 모든 제조사를 공통 인터페이스로만들고 공통 인터페이스에서 벗어나 제조사별로 유니크한 구현이 필요한 경우 arch/arm64/Kconfig.platfomrs파일을 통해 구분할 수 있도록 정의되어 있다.
config ARCH_BCM2835 bool "Broadcom BCM2835 family" select TIMER_OF select GPIOLIB select MFD_CORE select PINCTRL select PINCTRL_BCM2835 select ARM_AMBA select ARM_GIC select ARM_TIMER_SP804 select BRCMSTB_L2_IRQ help This enables support for the Broadcom BCM2837 and BCM2711 SoC. These SoCs are used in the Raspberry Pi 3 and 4 devices. |
위는 broadcom 2835 raspi 3 에 적용하는 uniqe한 config값이다. 사용하는 config값들은 bool값으로 broadcom2835에 맞춰 사용하고자하는 config값을 셋업 하는것 같다.(이후에 해당 값이 set되었는지로 추가 작업이 진행되고 있는게 맞는지는 확인해볼 필요가 있을 것 같다)
2. DTB 구조
Device Tree Script를 컴파일해서 생성한 DTB는 바이너리 데이터로 크게 4가지 block으로 구성된다.
FDT_Header, memory_reservaction, structure, string block
추가로 위 구조내 데이터는 ARM64의 경우 대부분 Little Endian으로 구성된다.
1) fdt_header
DTB 파일을 hex editor로 열면 가장먼저 FDT Header값이 나온다
FDT Header 값은 위 구조체로 이루어져 있으며 실제적으로 DTB의시작을 알리는 magic number이후 total size, structe block의 시작 offset, stirng blobk의 시작 offset, mem_reservation_block의 시작 offset등 FDT 구조체의 다른 멤버들의 시작 주소와 사이즈 등의 정보를 담고 있다.
2) memory_reservation_block
struct fdt_reserve_entry { uint64_t address; uint64_t size; }; |
memory reservation은 부트로더에서 사용하는, 커널 로드에 사용하는 주소 값등을 user가 접근하지 못하도록 하기 위함이다. 중요한 config가 설정된 부분을 reservation block으로 지정하여 user의 접근을 막는다.
3) structure block
structure block은 5개의 토큰으로 구성되어있으며 실제적으로 Tree구조 뼈대로 각 노드들을 나타내는 block이다.
FDT_BEGIN_NODE, FDT_PROP, FDT_NOP, FDT_END_NODE, FDT_END
FDT_BEGIN_NODE는 모든 노드의 시작을 알리는 토큰이다. 각 노드의 맨 앞에 오게되며 FDT_BEGIN_NODE 뒤에 바로 Node의 이름이 오게된다. 여기서 가장 처음에 오는 노드는 root 노드로 이름이 NULL값인 00 00 00 00 이다.
FDT_PROP 은 해당 노드의 속성값을 나타낸다. 속성 값은 string block을 이용해서 나타내기 때문에
struct { uint32_t len; uint32_t nameoff; } |
FDT_PROP에서는 위와같이 속성 값을 나타내는 string block의 offset과 길이 값을 가지고 있다.
FDT_END_NODE는 Node의 끝 부분을 알린다. FDT_NOP은 NULL값으로 파싱 때 무시한다.
마지막으로 FDT_END는 struct block의 끝임을 알린다.
결과적으로 정리를하면 항상 반드시 루트 노드가 존재하고 아래에 device장치에 따른 노드들이 하위로 존재하는 tree구조를 가지기 때문에 아래와 같은 구조를 가지게 된다.
FDT_BEGIN_NODE [null(root 노드이름)] FDT_PROP FDT_BEGIN_NODE [node이름(test)] FDT_PROP FDT_END_NODE FDT_END_NODE FDT_END
struct node는 아래와 같은 트리 구조를 가진다.(FDT_END_NODE없이 FDT_BEGIN_NODE가 온 것은 뒤 노드가 앞서 노드의 하위 노드임을 의미한다)
-null(root)
-- test
4) string block
string block은 string 값으로 prop값들을 정의하며 맨 마지막에 null값을 줌으로써 각 노드에 따른 string 값의끝을 알린다.
3. DTB early 처리
DTB는 바이너리파일로 위에서 언급한바와 같이 slab cache로 변환해서 커널에서 참조 및 사용한다고 했다 하지만 여기서 일부 옵션들은 slab cache를 생성하기 전에 먼저 필요한 값들이 존재한다. (boot_commaond_line, initrd_start 등) 이러한 값들을 elary 항목이라고 하며 early 항목에 한해서는 slab cache의 변환과정 없이 직접 DTB 바이너리 파싱을 통해 직접적으로 값을 가져와서 활용한다.
static void __init setup_machine_fdt(phys_addr_t dt_phys) { int size; void *dt_virt = fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL); const char *name; if (dt_virt) memblock_reserve(dt_phys, size); if (!dt_virt || !early_init_dt_scan(dt_virt)) { pr_crit("\n" "Error: invalid device tree blob at physical address %pa (virtual address 0x%p)\n" "The dtb must be 8-byte aligned and must not exceed 2 MB in size\n" "\nPlease check your bootloader.", &dt_phys, dt_virt); while (true) cpu_relax(); } /* Early fixups are done, map the FDT as read-only now */ fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO); name = of_flat_dt_get_machine_name(); if (!name) return; pr_info("Machine model: %s\n", name); dump_stack_set_arch_desc("%s (DT)", name); } |
위 function은 arch/arm64/kernel/setup.c 에 존재하는 FDT early처리를 위한 functino이다.
먼저 dt_virt로 DTB를 virtual address로 할당을 진행한다(head.S에서 레지스터에 FDT 주소 값을 저장해두었기 때문에 해당 값을 이용)
그 후에 memory_reservation 값을 가져와서 reservation동작을 수행한다.
이후 early_init_dt_scan function을 통해 early값들을 설정한다.
/* Retrieve various information from the /chosen node */ rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line); |
DTB 구조의 chosen node에는 bootargs에대한 값이 들어있기 때문에 해당 node를 scan해서 boot_command_line이 무엇인지 확보할 수 있다.
위 과정을 통해서 부트 파리미터, initrd위치와 크기, memblock에 물리 메모리 시작 주소 및 크기 정보 저장 등의 과정을 진행한다
- memblock
리눅스 커널에서는 다양한 메모리 기법들이 존재한다. 이 중에서 mm_init()을 통한 정규 메모리 기법들의 사용 준비가 되지 않은 상태에서 사용하는 즉 리눅스 커널 부팅 초반에 사용하는 메모리 기법이 존재하는데 이게 바로 memblock이다..
memblock의 경우 위와 같은 중첩 구조체 형식으로 되어있다. 먼저 사용가능한 메모리 영역인 memory 멤버변수 구조체, 예약된 사용불가능한 reserved가 존재하고 각각의 구조체 객체는 regions 라는 memblock_region 구조체 객체를 통해 offset(base)와 size를 정의함으로써 메모리를 관리한다.
위에서 물리 메모리 시작 주소 및 크기 정보 저장등을 memblock에 진행하는 것은 사용가능한 메모리영역의 offset과 size등을 설정하는 과정이라고 볼 수 있다.
위 early과정들은 drivers/of/fdt.c 파일 내에 각각의 function들로 정의되어있다.
early_linit_dt_scan_chosen // chosen early처리
of_scan_flat_dt // root node찾기
early_init_dt_check_for_initrd() // initrd early 처리
early_init_dt_scan_root // size_cells, address_cells 속성을 읽어 저장
early_init_dt_scan_memory : memory 노드를 읽어 memblock 초기화
early_init_dt_add_memory_arch : memblock에 필요한 memory 추가
4. Device Tree expanded format
Device Tree expanded format은 DTB 바이너리 파일에서 of_ 로 시작하는 API를 통해 구조체형식으로 각각의 노드를 정리한 데이터 포맷을 얘기한다
각각의 노드는 구조체 형식으로 정리되고 노드 안에 속성 값도 속성 값을 나타내는 구조체로 정리가 된다. 이렇게 구조체 형식으로 Tree를 만들어
바이너리 -> Tree 구조체로 (expanded format)으로 drivers/of/fdt.c에서 변환된다.
void__initunflatten_device_tree(void) {__unflatten_device_tree(initial_boot_params,NULL,&of_root,early_init_dt_alloc_memory_arch,false);/* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */of_alias_scan(early_init_dt_alloc_memory_arch);unittest_unflatten_overlay_base();} |
__unflatten_device_tree는 실제적으로 구조체를 alloc으로 할당하고 노드를 변환하는 과정을 나타낸다.
그리고 alias_scan은 of_alias를 /aliases 노드를 가리키도록, of_chosen이 chosen 노드를 가리키도록 설정한다.
위처럼 기본적인 root, 특수한 노드를 먼저 변환하고 이후에는 unflatten_dt_node function을 통해 DTB의 일반적인 노드들을 파싱하여 변환한다.
- ref
https://devicetree-specification.readthedocs.io/en/v0.2/flattened-format.html
https://elixir.bootlin.com/linux/v5.14.16/source
https://kernel.bz/boardPost/118682/2
'OS > Linux' 카테고리의 다른 글
[Linux Kernel] Kernel 분석(v5.14.16) - Page Table (1) (0) | 2021.12.06 |
---|---|
[Linux Kernel] objdump, arm-eabi-addr2line (0) | 2021.12.04 |
[Linux Kernel] Kernel 분석(v5.14.16) - entry.S (0) | 2021.11.14 |
[Linux Kernel] Kernel 분석(v5.14.16) - head.S (0) | 2021.11.07 |
[Linux Kernel] DT (디바이스 트리) (0) | 2021.01.09 |