임베디드 시스템 빌더, Yocto [1]

나우히즈·2025년 10월 18일

Yocto는 무엇이고 왜 필요하게 되었을까?

미리 설명하자면, 임베디드 리눅스 시스템을 제품에 맞게 구성하는데 불편함이 있었기 때문이다.
하드웨어를 구성한 뒤, 그 하드웨어에 맞는 툴체인을 구성한다. 그리고 부트로더로 커널을 메모리로 로드하고 커널 빌드를 진행. 파일시스템을 구성해야 기본적인 임베디드 리눅스 시스템이 구성된다.

이러한 복잡한 과정을 손쉽게 지원하도록 여러가지 자동 빌드 시스템들이 나오게 되었고, 그 중 Yocto를 보편적으로 쓴다고 알고 있다.

임베디드 리눅스 시스템을 구성하는 보편적인 단계


✅ 기본 순서

  1. 하드웨어 (Hardware/BSP)

    • CPU 아키텍처(ARM, x86, RISC-V 등)와 주변 장치를 파악.
    • 부트로더, 커널, 드라이버, 파일시스템이 모두 이 하드웨어를 기준으로 설정됨.
  2. 툴체인 (Cross Toolchain)

    • 개발 PC(Host)에서 타깃용(ARM 등) 바이너리를 빌드하기 위한 컴파일러 세트.
    • gcc, glibc/musl, binutils, sysroot, headers 포함.
    • Yocto/Buildroot는 이걸 자동으로 만들지만, 수작업 시엔 Linaro 등에서 미리 빌드된 툴체인을 사용.
  3. 부트로더 (Bootloader)

    • 보드 전원이 켜질 때 가장 먼저 실행되는 코드.
    • 하드웨어 초기화 후 커널을 메모리에 로드.
    • 대표적으로 U-Boot가 있고 이걸 사용해 볼 것임.
    • SoC 벤더가 제공하는 보드 초기화 코드를 기반으로 커스터마이징함.
  4. 커널 빌드 (Linux Kernel)

    • 하드웨어 드라이버와 설정을 담은 커널을 구성(make menuconfig).
    • Device Tree(.dts/.dtb) 작성으로 하드웨어 맵 정의.
    • 부트로더에서 로드할 수 있도록 커널 이미지(zImage, Image, uImage)를 생성.
  5. 파일시스템 (Root Filesystem)

    • 사용자 공간 환경(rootfs).
    • BusyBox, glibc/musl, 시스템 데몬, 앱, 설정 파일, 라이브러리 포함.
    • init 시스템(sysvinit, systemd 등)과 함께 커널 부팅 후 동작.

툴체인

하드웨어 별 CPU의 어셈블리어가 다르기 때문에, 각 하드웨어 환경에 맞게 컴파일해주는 컴파일러 세트가 필요하다.

  • toolchain = compiler + assembler + linker + libc + headers + binutils + debugger(gdb)
  • 현재 내 PC환경에서 적용되는 툴체인은 네이티브 툴체인(native toolchain)
  • 현재 내 PC환경과 다른 시스템을 타겟하는 경우, 툴체인을 크로스 툴체인(cross toolchain)

우분투(x86)의 툴체인으로는 라즈베리파이 하드웨어(ARM64) 환경에 사용할 수 있는 바이너리를 만들지 못하므로, 해당 환경에 알맞는 툴체인을 가져와 빌드하였던 것이다.

대부분의 툴체인은 SoC 또는 보드 벤더사에서 이미 제공하는 경우가 있다. 혹은 Yocto를 통한 자동빌드로 툴체인을 구성할 수 있음.


부트로더

시스템이 동작할 수 있도록, 전원이 들어오면 커널을 메모리에 로드하고 실행시키는 역할.

마찬가지로 아키텍처마다 부트로더는 다르게 동작한다.
부트로더가 커널을 실행시키기 위해 전달해야 하는 레지스터 설정, 메모리 레이아웃, 파라미터 포맷(ATAG, DTB 등), 그리고 CPU 초기화 상태(MMU, 모드 전환 등)가 아키텍처마다 다르기 때문.

  1. x86/x86_64는 전통적으로 BIOS/UEFI 기반으로 부팅돼.
    → 그래서 GRUB 같은 부트로더가 많이 쓰임.
    → BIOS 인터럽트 호출, MBR 파티션 구조 등 x86 전용 부팅 규약에 맞춰 동작.

  2. ARM, ARM64 계열은 이런 BIOS가 없고,
    SoC 제조사가 제공하는 ROM 코드 → 2단계 부트로더(U-Boot 등) 체계로 동작해.
    → 따라서 U-Boot, Barebox, ATF + U-Boot 구조가 흔함.

  3. RISC-V는 OpenSBI → U-Boot → Linux 구조로 부팅돼.
    (OpenSBI가 일종의 펌웨어/하이퍼바이저 역할을 함)

즉, 각 아키텍처의 부팅 초기 환경과 인터페이스가 다르기 때문에, 그에 맞는 부트로더가 필요.

임베디드 장치에서는 보통 ARM 하드웨어를 주로 사용하니, (그리고 라즈베리파이가 ARM64) U-Boot 부트로더를 통해 장비를 부팅시켜볼 것이다.

부트로더의 역할

  • 타겟 시스템 초기화
  • 타겟 시스템 동작 환경 설정
    • 네트워크 부팅이 필요한 경우, IP 주소 설정과 같은 세팅 필요
    • 혹은 개발머신 - 타겟 시스템 간 scp를 통해 통신을 위해 필요
  • 시스템 운영체제(커널) 로드
  • 플래시 메모리 관리
  • 모니터 기능

부트로더 동작 순서

⭐️ 전체 흐름: ROM 코드 → SPL → TPL → 커널

1단계: ROM 코드 (BootROM 단계)

  • 위치: SoC 내부에 있는 고정된 ROM 영역에 존재 (하드웨어적으로 내장됨, 수정 불가)
  • 역할:
    • 전원이 켜지면 CPU가 처음 실행하는 코드
    • 주로 간단한 초기화 (시스템 클럭, 핀 설정, 일부 하드웨어 초기화)
    • SPL(Secondary Program Loader)를 SRAM 또는 내부 메모리로 로드하는 것
    • 이후 SPL로 점프 (제어권 넘김)

2단계: SPL (Secondary Program Loader)

  • 위치: ROM 코드에 의해 SRAM에 로드됨
  • 역할:
    • SoC의 경우 보통 초기 DRAM 컨트롤러가 초기화되지 않은 상태이므로
    • SPL은 메모리 컨트롤러(DRAM 컨트롤러)를 초기화하고,
    • 그 이후, 좀 더 큰 프로그램(TPL 등)을 DRAM으로 로드할 준비를 함
    • SPL이 TPL을 DRAM으로 로드함
    • SPL에서 TPL로 점프 (제어권 넘김)

3단계: TPL (Tertiary Program Loader)

  • 위치: DRAM에 SPL이 로드함
  • 역할:
    • 커널 부팅 준비 단계
    • Kernel image, FDT (Flattened Device Tree), initramfs 등을 DRAM으로 로드
      • Kernel image: 실제 OS 커널
      • FDT: 디바이스 트리 → 어떤 하드웨어가 있는지 커널에 알려주는 데이터 구조
      • initramfs: 초기 램 디스크, 루트 파일 시스템처럼 사용
  • 마지막으로:
    • TPL이 커널을 실행 (jump to kernel), 이후 제어권은 운영체제 커널로 넘어감
    • 이제부터는 부트로더가 아니라 운영체제 단계

위 과정을 진행하여 부트로더는 커널에게 제어권을 넘긴다.

부트로더가 커널로 제어를 넘길 때 전달해야 하는 정보는 아래와 같음

항목의미커널이 필요한 이유
Machine Number어떤 보드인지 식별 (DTB 미지원 시)보드별 초기화 코드 선택
HW 기본 정보RAM, 클럭 등 감지 정보메모리/타이머 설정
커널 명령줄커널 실행 옵션(/proc/cmdline)루트FS, 콘솔 등 지정
DTB 위치/크기하드웨어 구성 트리드라이버 초기화 정보
initrd 위치/크기임시 루트FS초기 부팅 환경 구성

디바이스트리

임베디드 리눅스 시스템에서 커널이 하드웨어를 이해하기 위한 “지도” 역할

1. 디바이스 트리가 생긴 배경

예전(ARMv7 이전)에는 커널이 보드별 하드웨어 정보 및 초기화 코드를 직접 포함

  • 보드가 많아질수록 커널 코드가 기하급수적으로 늘어남
  • 커널 빌드 시 하드웨어 종속성이 너무 강해짐
  • 하나의 커널 이미지로 여러 보드를 지원하기 어려움

그래서 생긴 게 Device Tree (DT) — “하드웨어 구성을 커널 코드가 아니라 외부 데이터로 분리"

  • 하드웨어와 커널 코드의 분리 -> 간소화
  • 하나의 커널로 여러 보드 지원 가능
  • 커널 리빌드 없이 장치 구성 변경 가능 (DT만 수정)
  • 유지보수성과 이식성 향상

2. 디바이스 트리의 구조

텍스트 기반 소스 파일(.dts, Device Tree Source)
-> 빌드 시 바이너리(.dtb, Device Tree Blob) 로 변환
-> 부트로더가 커널로 전달.

확장자설명사용 시점
.dtsDevice Tree Source (텍스트)사람이 편집
.dtsiinclude용 하위 트리 (공통 부분)코드 재사용
.dtbDevice Tree Blob (바이너리)부트로더 → 커널 전달 시

기본 구성

/ {
    model = "Raspberry Pi 4 Model B";
    compatible = "raspberrypi,4-model-b";

    memory@0 {
        device_type = "memory";
        reg = <0x0 0x40000000>;   // 시작 주소, 크기
    };

    cpus {
        cpu@0 {
            compatible = "arm,cortex-a72";
            reg = <0>;
        };
    };

    soc {
        serial@7e201000 {
            compatible = "brcm,bcm2835-uart";
            reg = <0x7e201000 0x1000>;
            interrupts = <2>;
        };
    };
};

📘 주요 구성 요소

요소설명
노드(node)하드웨어 장치 하나를 표현 (serial@7e201000)
프로퍼티(property)해당 장치의 속성 (reg, interrupts, compatible)
레지스터(reg)장치의 물리 주소와 크기 (<base size>)
compatible어떤 드라이버가 이 장치를 다룰 수 있는지 식별
interrupts인터럽트 번호 정보
aliases / chosen루트 노드 하위의 특수 노드 (부트 파라미터 등)

3. 부트로더와의 관계

부트로더(U-Boot 등)는 부팅 시 커널로 DTB 파일의 메모리 주소를 넘겨줘.
커널은 이걸 읽어들여 “어떤 장치가 있고, 어떤 드라이버를 올려야 하는지”를 결정함.

bootz 0x80000000 - 0x81000000
# 커널: 0x80000000
# DTB:   0x81000000

4. 커널과 디바이스 트리

  1. 커널 부팅 시 DTB를 파싱함
  2. 각 노드의 compatible 프로퍼티를 보고,
    자신이 지원하는 드라이버와 매칭
  3. 해당 장치의 레지스터 주소, 인터럽트 번호 등으로
    드라이버 초기화 수행

예:
DTB에 compatible = "brcm,bcm2835-uart" →
커널은 of_match_table에 같은 문자열을 가진 드라이버를 찾아 UART 초기화

  • /boot/ 디렉터리 아래 .dtb 파일 형태로 저장되어 있음
    (ex: /boot/bcm2711-rpi-4-b.dtb)
  • 소스는 커널 트리 내 arch/arm/boot/dts/ 에 존재

U-Boot

U-Boot Manual : https://docs.u-boot.org/en/latest/

U-Boot 내에서 사용하는 쉘 커맨드도 굉장히 다양하게 존재한다.

그 중 몇 가지 명령어에 대해 사용해보고 어떤 기능을 하는지 알아보았음

  • bootm : 운영체제 부팅을하는데 이미지 파일을 사용하는 명령어
    유사한 걸로 bootz가 있는데, zimageformat 형식을 받을 수 있음.
    → 이미지 파일을 압축한 걸로 부팅.

  • echo : uboot의 환경변수를 확인하는 용도로 사용한다. (bash 문법과 유사)

  • md : memory context를 덤프뜨는데 사용하는 명령어.

U-Boot도 커널과 같은 ‘Kconfig 기반 빌드 시스템’을 사용한다
본인 보드 아키텍처에 맞게 스타터, 클럭 수 등을 uboot로 설정할 수 있음.

커널 소스코드 다운 후 make menuconfig 를 통해 gui 환경에서 옵션을 선택했듯, uboot도 마찬가지로 선택 가능하다.

여기서 Space/Enter 키로 설정을 켜거나 끄면 그 결과가 .config 파일에 반영됨.

이 .config는 결국 U-Boot 소스 빌드 시, Makefile이 읽어서 어떤 드라이버를 포함할지, 어떤 초기화 코드를 빌드할지를 결정.

U-Boot의 빌드 시스템 구조

U-Boot도 리눅스 커널처럼 다음의 구조를 공유합니다.

구성요소역할
Makefile전체 빌드 플로우 관리 (make all, make clean 등)
Kconfig설정 옵션 정의 (CPU, 클럭, 부트 장치 등)
defconfig특정 보드의 기본 설정값(초기 config)
.config실제 빌드에 사용되는 최종 설정 파일 (Kconfig에서 생성됨)

즉, U-Boot은 커널과 동일한 Kconfig 메커니즘으로 설정 값을 관리하고,
make를 수행하면 scripts/kconfig/ 내부의 같은 툴(conf, menuconfig)을 이용해
사용자가 선택한 설정 → .config로 반영하는 구조.

기본 빌드 순서

보드에 맞는 defconfig를 먼저 선택해서 초기값을 로드

예를 들어 라즈베리파이용 U-Boot 빌드라면:

make rpi_4_defconfig
  • 이 명령은 configs/rpi_4_defconfig 파일을 읽어
    그 안의 옵션들을 .config로 복사.
  • .config 파일은 실제 빌드 시 U-Boot이 참고하는 설정 테이블.

단계설명
1단계어셈블리 초기화 (start.S) - 레지스터/메모리 설정
2단계메모리 컨트롤러 설정, U-Boot Relocate
3단계CPU, 보드, 인터럽트, 환경변수, 시리얼 초기화
4단계DRAM, Flash, 디바이스 초기화
5단계메인 루프 진입, 명령어 처리 및 커널 부트 준비
  1. ARM 보드는 BIOS/UEFI가 없음

    → 메모리 초기화 직접 해야 함

  2. 보드마다 하드웨어 다름

    → 보드별 초기화 필요 (board_init)

  3. 리얼 콘솔 꼭 필요 (디버깅용)

    → serial_init 필수

  4. U-Boot Relocate 필요 (SRAM이 작기 때문에 DRAM으로 이동, SPL -> TPL)

    → 고성능 환경 준비

  5. 메인 루프에서 커널을 로드하거나 CLI 명령 처리 가능


커널 구성과 빌드

부트로더가 커널을 메모리로 잘 로드했다면 제어권을 커널로 넘기게 된다.

$ make menuconfig : 빌드할 커널에 대한 세팅 진행. 필요한 모듈을 선택한다.
$ make ARCH=*** ####_defconfig : *** 아키텍처에 대해 #### 보드의 기본세팅으로 빌드

커널 모듈

  • 필요한 기능, 장치 드라이버를 모듈화하여 커널에 내재하지 않고 외부에서 불러와 커널에 박는다.
  • 커널 버에 맞는 모듈 세팅이 필요

모듈 빌드

make -j# ARCH=arm64 dtbs CROSS_COMPILE=aarch64-linux-gnu- modules

➡️ 커널 모듈(.ko) 파일을 컴파일하는 단계

항목의미
makeGNU Make를 실행
-j#병렬 빌드 (CPU 코어 수만큼 # 입력, 예: -j8)
ARCH=arm64ARM 64비트 아키텍처용 커널을 빌드
CROSS_COMPILE=aarch64-linux-gnu-aarch64 크로스컴파일러 사용 (aarch64-linux-gnu-gcc, ld, as 등)
dtbs디바이스 트리 바이너리(.dtb)도 함께 빌드 (선택사항)
modules커널 모듈(.ko)만 별도로 빌드하라는 의미

즉, .config 설정에 따라 선택된 커널 모듈들만 컴파일하는 make 명령.

결과적으로 drivers/, fs/, net/ 등의 서브디렉터리 아래에 .ko 파일들이 생성.
생성된 .ko 파일은 insmod 명령을 통해 커널에 삽입할 수 있음.

🔹 두 번째 명령

make -j# ARCH=arm64 dtbs CROSS_COMPILE=aarch64-linux-gnu- modules_install

➡️ 빌드된 모듈을 시스템의 루트파일시스템에 설치하는 단계

항목의미
modules_install/lib/modules/<kernel-version>/ 디렉토리에 모듈들을 복사
kernel-versionuname -r과 동일한 버전명 (예: 6.1.36)
설치 경로보통 INSTALL_MOD_PATH 변수로 변경 가능

예를 들어 루트FS를 SD카드 이미지에 미리 준비했다면:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \
     INSTALL_MOD_PATH=/path/to/rootfs modules_install

→ /path/to/rootfs/lib/modules/6.1.36/ 에 .ko 파일들이 설치.

단계명령설명
①make modules커널 모듈(드라이버 등)을 컴파일
②make modules_install빌드된 모듈을 /lib/modules/<ver>/ 경로에 설치

루트파일시스템 구성

아주 간단한, 소형의 임베디드 시스템 구현을 위한 루트파일시스템은 busybox 라는 툴을 통해 구성.

최소한의 루트 파일시스템 구성요소

  • init
  • shell(bash)
  • daemon
  • 공유 라이브러리
  • 설정 파일 (/etc)
  • 장치 노드(/dev)
  • /proc
  • /sys
  • 커널 모듈 (/lib/modules/<커널버전>)

마무리

이렇게 시스템을 구성하는데에는
하드웨어 구성부터 툴체인, 부트로더, 커널 빌드, 루트파일시스템까지 많은 부분을 신경써야했다.
이를 yocto를 이용하면 손쉽게 해결할 수 있다.
다음 시간엔 본격적으로 yocto를 알아보자

0개의 댓글