U-boot (1)

이진욱·2026년 6월 12일

심심해서해본것들

목록 보기
4/5

U-Boot를 설명하기 전에 먼저 시스템이 부팅되는 흐름을 살펴보자.

전원이 인가되면 CPU는 리셋 상태에서 깨어나 정해진 부트 주소의 코드를 실행한다. 일반적으로 SoC 내부의 Boot ROM이 가장 먼저 실행되며, 이 코드는 외부 Flash, eMMC, SD card, SPI NOR/NAND Flash 등에 저장된 다음 단계의 부트로더를 찾아 메모리로 로드한다.

부트로더는 CPU 실행 환경, 클럭, DRAM, UART, 저장장치, 네트워크 등 최소한의 하드웨어를 초기화하고, 운영체제 커널과 Device Tree를 메모리에 올린 뒤 커널에 필요한 부팅 인자를 전달하고 제어권을 넘긴다.

U-Boot는 이러한 부트로더의 대표적인 구현 중 하나이며, 특히 Linux를 실행하는 임베디드 SoC나 MPU 기반 시스템에서 널리 사용된다. U-Boot는 보통 Flash, eMMC, SD card 같은 비휘발성 저장장치에 저장되어 있고, 시스템 부팅 과정에서 Boot ROM 또는 이전 단계 부트로더에 의해 실행된다.

U-Boot 이미지 생성

U-Boot 오픈 소스의 주요 디렉터리를 살펴보면 다음과 같다.

  1. /arch 디렉터리
    디렉터리 내부에는 arm, x86, riscv, mips, powerpc, m68k, microblaze, nios2, arc, sh, xtensa, sandbox 디렉터리로 분류되어 있으며, 각 디렉터리는 CPU 아키텍처에 따라 구현 코드를 구분한다. 여기에는 reset entry, low-level init, MMU/cache, exception, relocation 등 아키텍처 의존 코드가 포함된다. 단, sandbox는 실제 CPU 아키텍처라기보다 호스트 환경에서 U-Boot를 테스트하기 위한 가상 아키텍처이다.

  2. /board 디렉터리
    디렉터리 내부에는 vendor에 따라 보드 코드가 분류되어 있으며, /configs 디렉터리 안에 존재하는 각각의 defconfig를 통해 빌드할 board 설정을 선택할 수 있다. 하나의 board 디렉터리에 여러 defconfig가 연결될 수 있으며, 반대로 하나의 defconfig가 여러 보드 변형을 처리하는 경우도 있다. make _defconfig 명령을 실행하면 Kconfig 시스템이 해당 defconfig와 Kconfig 규칙을 읽고 최종 설정 파일인 .config를 생성한다. 이후 빌드 과정에서는 .config에 정의된 설정값을 기준으로 board에 맞는 아키텍처 코드, SoC 코드, 보드 코드, 드라이버, 명령어 등을 선택하여 U-Boot 빌드 산출물을 생성한다.

  3. /api 디렉터리
    이는 U-Boot 위에서 실행되는 standalone application에게 U-Boot의 일부 기능을 빌려주기 위한 선택 기능을 모아놓은 디렉터리이다. standalone application이란 U-Boot가 이미 초기화해둔 환경 위에서 OS 없이 직접 load되어 실행되는 작은 bare-metal 프로그램으로, U-Boot 자체에 의존적이다. /api는 이러한 standalone application이 console, environment, timer, storage, network, display 같은 일부 U-Boot 기능을 syscall 형태로 호출할 수 있도록 한다. 단, 외부 PC가 U-Boot에 접속하기 위한 API는 아니며, 하드웨어 테스트 자체를 모아둔 디렉터리도 아니다.

  4. /boot 디렉터리
    /boot 디렉터리는 /api와 반대로 U-Boot가 다음 단계의 OS, Kernel, Payload를 찾아서 올리는 부트 로직을 모아놓은 영역이다. U-Boot가 올라온 뒤 FIT image, legacy image, Linux kernel Image, EFI binary, extlinux, PXE, bootflow 등을 처리하고, 필요한 경우 Device Tree를 수정한 뒤 최종적으로 OS나 payload로 제어를 넘기는 공통 부팅 프레임워크이다.

  5. /env 디렉터리
    /env는 U-Boot environment 변수 시스템을 관리하는 계층이다. 여기서 environment 변수란 bootcmd, bootargs, ipaddr, serverip, ethaddr, loadaddr, kernel_addr_r, fdt_addr_r처럼 U-Boot가 실행 중 사용하는 런타임 설정값을 의미한다. /configs가 빌드 전에 어떤 U-Boot를 만들지 결정하는 설정이라면, /env는 빌드된 U-Boot가 실행된 뒤 어떤 방식으로 부팅하고 동작할지 결정하는 설정을 관리한다. 또한 SPI flash, MMC, NAND, FAT, EXT4 등 다양한 저장 위치에 environment를 저장하고 불러오는 backend도 포함한다.

  6. /cmd 디렉터리
    이 디렉터리는 U-Boot 콘솔에서 사용자가 입력하는 명령어들의 구현을 모아놓은 영역이다. 예를 들어 bootm, booti, bootefi, env, mmc, usb, sf, nand, tftpboot, ping, gpio, i2c 같은 명령어들이 여기에 구현된다. 따라서 U-Boot shell에서 사용할 수 있는 명령어 기능은 .config 설정에 따라 선택적으로 빌드되며, 그 구현 코드는 주로 /cmd 디렉터리에 위치한다.

  7. /common 디렉터리
    /common은 특정 보드나 특정 아키텍처에 한정되지 않는 U-Boot의 공통 초기화 및 메인 흐름을 담당한다. 대표적으로 board_init_f(), board_init_r(), main_loop() 같은 초기화 흐름과 CLI, console, autoboot 관련 공통 코드가 포함된다. 즉 아키텍처 코드와 보드 코드가 초기 실행 환경을 만든 뒤, U-Boot 전체 동작을 이어가는 중심 공통 코드 영역이다.

  8. /drivers 디렉터리
    이 디렉터리는 U-Boot에서 사용하는 장치 드라이버들을 모아놓은 영역이다. serial, MMC, SPI, USB, Ethernet, GPIO, I2C, PCI, MTD, NAND, watchdog, PHY 등 실제 하드웨어를 제어하는 코드가 포함된다. 어떤 드라이버가 빌드에 포함될지는 .config의 CONFIG_* 값과 Device Tree 설정에 의해 결정된다.

  9. /dts 또는 /arch//dts 영역
    이 영역에는 Device Tree Source가 존재하며, 보드의 하드웨어 구성을 기술한다. CPU, memory, UART, I2C, SPI, MMC, Ethernet, GPIO, flash, regulator 같은 장치 정보가 여기에 표현된다. U-Boot의 Driver Model은 Device Tree를 기반으로 장치를 생성하고 적절한 드라이버와 연결한다.

  10. /include 디렉터리
    이 디렉터리는 U-Boot 전체에서 사용하는 공통 헤더 파일을 모아놓은 영역이다. 보드 설정 헤더, 아키텍처별 헤더, Driver Model 관련 헤더, environment 관련 헤더, 각종 구조체와 매크로 정의가 포함된다. C 코드 전반에서 공유되는 인터페이스가 이 디렉터리에 존재한다.

  11. /lib 디렉터리
    /lib는 여러 서브시스템에서 공통으로 사용하는 라이브러리 코드를 모아놓은 영역이다. 문자열 처리, CRC, 압축, 암호화 일부, UUID, list, hash, FDT helper 등 보드와 무관하게 재사용되는 기능들이 포함된다.

  12. /fs 디렉터리
    이 디렉터리는 U-Boot가 저장장치에서 파일을 읽기 위해 사용하는 파일시스템 코드를 포함한다. FAT, EXT4, SquashFS, UBIFS, Btrfs 등의 파일시스템 지원 코드가 존재하며, SD/eMMC/USB/SATA/NVMe 등에서 kernel, device tree, initrd, boot script를 읽을 때 사용된다.

  13. /net 디렉터리
    /net은 U-Boot의 네트워크 스택을 담당한다. DHCP, BOOTP, TFTP, ARP, ping, DNS, NFS 등 네트워크 기반 부팅과 파일 전송에 필요한 기능이 포함된다. tftpboot, dhcp, ping 같은 명령어는 이 네트워크 계층과 연결되어 동작한다.

  14. /post 디렉터리
    /post는 Power-On Self Test 기능을 담당하는 디렉터리이다. 메모리, I2C, RTC, flash, CPU 등 하드웨어 상태를 검사하는 진단 테스트 코드가 포함된다. 이는 초기화 자체를 담당한다기보다는, 부팅 중 또는 수동 실행 시 시스템 상태를 검사하는 프레임워크이다. 단, 모든 보드에서 사용되는 것은 아니며 CONFIG_POST 설정이 필요하다.

  15. /tools 디렉터리
    이 디렉터리는 U-Boot 바이너리 안에서 실행되는 코드가 아니라, 호스트 PC에서 실행되는 이미지 생성 및 변환 도구들을 포함한다. 대표적으로 mkimage, dumpimage, mkenvimage, envcrc 등이 있으며, FIT image, legacy uImage, boot script image, environment image 등을 생성하거나 분석할 때 사용된다.

  16. /scripts 디렉터리
    /scripts는 U-Boot 콘솔에 입력할 스크립트를 정의하는 디렉터리가 아니라, U-Boot를 빌드하고 검사하고 설정 파일을 생성하기 위한 호스트-side 스크립트와 빌드 보조 도구를 모아놓은 영역이다. Kconfig 처리, Device Tree Compiler, checkpatch, get_maintainer, 빌드 Makefile 조각 등이 여기에 포함된다.

  17. /test 디렉터리
    이 디렉터리는 U-Boot의 자동화 테스트 코드를 포함한다. sandbox 기반 테스트, Python pytest 기반 테스트, 유닛 테스트 등이 위치하며, 기능 변경 후 U-Boot의 동작을 검증하는 데 사용된다.

먼저 U-boot를 설치하자.

https://source.denx.de/u-boot/u-boot

먼저 U-Boot 소스 코드를 내려받는다. 위 주소에서 U-Boot 저장소를 clone하여 Ubuntu 환경에 소스 코드를 준비했다.

U-Boot 프로젝트는 여러 디렉토리로 구성되어 있다.

이 중 board 디렉토리에는 벤더 및 보드별 초기화 코드가 들어 있다.

예를 들어 ti, nxp, nvidia 같은 디렉토리는 각 벤더의 보드들을 위한 코드를 포함한다.
nxp 디렉토리 안에는 NXP 계열 SoC를 사용하는 여러 보드들의 초기화 코드가 존재한다.

arch 디렉토리는 CPU 아키텍처별 코드를 담고 있다.

예를 들어 ARM, RISC-V, x86, MIPS 등의 아키텍처 관련 코드가 이곳에 위치한다. 여기서 말하는 arch는 특정 SoC가 아니라 CPU 명령어 집합 또는 프로세서 아키텍처에 가까운 개념이다.

configs 디렉토리에는 보드별 기본 설정 파일인 defconfig들이 존재한다. U-Boot를 빌드할 때는 먼저 내가 사용할 보드 또는 실행 환경에 맞는 defconfig를 선택한다.

예를 들어 실제 NXP 보드를 대상으로 한다면 해당 보드의 defconfig를 사용하고, QEMU에서 ARM64 환경을 테스트하려면 qemu_arm64_defconfig 같은 설정을 사용할 수 있다.

U-Boot 빌드 과정은 대략 다음과 같다.

1. 사용할 보드 또는 가상 플랫폼에 맞는 defconfig를 선택한다.
2. 필요한 크로스 컴파일 툴체인을 준비한다.
3. make 명령으로 U-Boot를 빌드한다.
4. 생성된 u-boot.bin, u-boot.elf, u-boot.itb 등의 바이너리를 보드의 부팅 매체에 기록하거나 QEMU에서 실행한다.

실제 보드에서는 생성된 U-Boot 바이너리를 SPI Flash, eMMC, SD card, NAND Flash 등 보드가 사용하는 부팅 장치에 기록한다. 이후 SoC 내부 Boot ROM이 해당 부팅 장치에서 U-Boot를 읽어 실행하게 된다.

이번에는 실제 보드에 바로 올리기 전에 QEMU를 이용해 ARM64 가상 환경에서 U-Boot를 실행해볼 예정이다. QEMU는 실제 SoC 전체를 완벽히 가상화하는 것이 아니라, 특정 CPU와 가상 보드/장치를 에뮬레이션하여 U-Boot의 기본 동작을 테스트할 수 있게 해준다.

qemu_arm64_defconfig는 실제 특정 SoC 보드를 위한 설정이라기보다, QEMU가 제공하는 ARM64 virt 머신에서 U-Boot를 실행하기 위한 기본 설정 파일이다. U-Boot의 configs 디렉토리 안에 존재하며, make 명령을 통해 이 defconfig를 현재 빌드 설정인 .config로 변환할 수 있다.

먼저 빌드에 필요한 패키지와 ARM64 크로스 컴파일 툴체인을 설치하고 U-Boot 소스 디렉토리에서 아래 명령을 실행한다.

위 명령은 configs/qemu_arm64_defconfig 파일을 기반으로 U-Boot 루트 디렉토리에 .config 파일을 생성한다. 이 단계는 실제 소스 코드를 컴파일하는 단계가 아니라, 어떤 보드/플랫폼을 대상으로 어떤 기능을 포함할지 선택하는 설정 단계이다.

설정이 완료되면 ARM64용 크로스 컴파일러를 사용하여 U-Boot를 빌드한다.

여기서 CROSS_COMPILE=aarch64-linux-gnu-는 U-Boot 빌드 시스템이 ARM64용 컴파일러와 링커를 사용하도록 지정하는 옵션이다. 예를 들어 내부적으로 aarch64-linux-gnu-gcc, aarch64-linux-gnu-ld 같은 도구들이 사용된다.

-j$(nproc)는 현재 Ubuntu 환경의 CPU 코어 수만큼 병렬로 빌드하겠다는 의미이다. 이를 사용하면 빌드 시간을 줄일 수 있다.

빌드가 완료되면 u-boot.bin, u-boot.elf, u-boot.dtb 등의 결과물이 생성된다. 이 중 qemu_arm64_defconfig 환경에서는 보통 u-boot.bin 또는 u-boot.elf를 QEMU에서 실행하여 U-Boot가 정상적으로 동작하는지 확인할 수 있다.

현재 linux 환경에서 u-boot 파일을 실행해보자

빌드 결과물 중 u-boot 파일에 실행 권한을 주고 ./u-boot 명령으로 실행해보면 Exec format error가 발생한다. 이 에러는 현재 실행하려는 파일이 지금의 Linux 사용자 공간에서 실행 가능한 형식이 아니기 때문에 발생한다.

처음에는 Windows에서 설치한 실행 파일을 macOS에서 실행할 수 없는 것과 비슷하게 이해할 수 있다. 운영체제마다 실행 파일 포맷과 실행 환경이 다르기 때문이다. 예를 들어 Windows는 PE, macOS는 Mach-O, Linux는 ELF 형식을 사용한다.

하지만 U-Boot의 경우에는 이보다 더 중요한 이유가 있다. U-Boot는 Linux 위에서 실행되는 일반 애플리케이션이 아니라, 운영체제가 실행되기 전에 동작하는 부트로더이다. 즉, printf나 파일 입출력처럼 Linux 커널이 제공하는 시스템 콜을 사용하는 프로그램이 아니라, CPU가 부팅 과정에서 직접 실행하는 bare-metal 코드에 가깝다.

또한 qemu_arm64_defconfig와 aarch64-linux-gnu- 크로스 컴파일러를 사용해 빌드했다면, 생성된 U-Boot는 ARM64 아키텍처용 바이너리이다. 현재 Ubuntu가 x86_64 PC 위에서 동작하고 있다면 x86_64 CPU는 ARM64 명령어를 직접 실행할 수 없다. 따라서 ./u-boot처럼 현재 Linux 셸에서 실행하면 Exec format error가 발생한다.

즉, 이 에러의 원인은 크게 두 가지로 볼 수 있다.

  1. 현재 시스템은 x86_64 Linux인데, 빌드한 U-Boot는 ARM64용이다.
  2. U-Boot는 Linux 사용자 공간 프로그램이 아니라, 부팅 과정에서 실행되는 부트로더 이미지이다.

따라서 ARM64용으로 빌드된 U-Boot는 현재 x86_64 Linux 환경에서 직접 실행할 수 없다. CPU 아키텍처가 다르기 때문에 현재 PC가 해당 명령어를 해석할 수 없고, U-Boot 자체도 Linux 사용자 공간에서 실행되는 일반 프로그램이 아니라 부팅 단계에서 실행되는 부트로더이기 때문이다. 그러므로 빌드된 U-Boot는 ARM64 실제 보드에 올리거나, QEMU와 같은 ARM64 에뮬레이션 환경에서 실행해야 한다.


VxWorks 커널 이미지 생성

기존 VxWorks 5.x/6.x에서는 VxWorks 7과 같은 형태의 VSB(VxWorks Source Build) 프로젝트 구조가 명확히 분리되어 있지 않았다. VxWorks 7 이후에는 OS 소스 빌드 영역인 VSB와 최종 커널 이미지 구성 영역인 VIP(VxWorks Image Project)가 분리되었다.

VSB는 VxWorks의 커널, 라이브러리, 드라이버, 네트워크 스택, 파일시스템 등 OS 구성 요소를 소스 레벨에서 구성하고 빌드하는 프로젝트 단위이다. 즉, 타깃 보드와 제품 요구사항에 맞는 VxWorks OS 기반 라이브러리와 빌드 환경을 생성하는 단계라고 볼 수 있다. 이 단계에서는 CPU 아키텍처, 툴체인, BSP, 포함할 OS 기능의 기반 등이 결정된다. VSB는 프로젝트 구성에 따라 용량이 크고 빌드 시간이 오래 걸릴 수 있다.

VIP는 VSB에서 생성된 빌드 산출물을 기반으로 실제 타깃에서 부팅할 VxWorks 이미지를 구성하고 생성하는 프로젝트이다. VIP에서는 VSB가 제공하는 OS 기반 위에서 최종 이미지에 포함할 컴포넌트와 설정을 선택한다. 그 결과물로 vxWorks, vxWorks.bin, 또는 U-Boot에서 로드 가능한 uVxWorks와 같은 커널 이미지가 생성된다.


DRAM load

빌드 결과물로 생성된 uVxWorks 이미지를 U-Boot 콘솔 명령어를 통해 RAM에 적재한 뒤 실행한다. 먼저 uVxWorks 이미지를 TFTP 서버가 참조하는 디렉토리로 복사한다. 이후 QEMU 또는 타깃 보드 실행 시 해당 디렉토리를 TFTP 서버 경로로 지정하면, U-Boot에서 네트워크를 통해 uVxWorks 이미지를 다운로드할 수 있다.

RAM 크기를 2GB로 설정하고 tftp 디렉토리(uVxWorks가 저장된 디렉토리)를 tftp 가상화 주소로 지정한다.

U-Boot에서는 먼저 타깃 보드의 IP 주소와 TFTP 서버의 IP 주소를 설정한다.

=> setenv ipaddr 10.0.2.15
=> setenv serverip 10.0.2.2

setenv ipaddr 10.0.2.15 명령은 타깃 보드의 IP 주소를 설정하는 명령이고, setenv serverip 10.0.2.2 명령은 TFTP 서버의 IP 주소를 설정하는 명령이다. QEMU user-mode networking 환경에서는 일반적으로 10.0.2.15가 게스트 주소, 10.0.2.2가 호스트/TFTP 서버 주소로 사용된다. 다만 이 값은 실제 네트워크 환경에 따라 달라질 수 있으므로, 물리 보드나 다른 가상 네트워크 환경에서는 해당 환경에 맞게 수정해야 한다.

=> tftp boot ${loadaddr} uVxWorks

이후 tftpboot ${loadaddr} uVxWorks 명령을 입력한다. 이 명령은 TFTP 서버에서 uVxWorks 파일을 가져와 RAM의 ${loadaddr} 주소에 적재하는 명령이다. 여기서 ${loadaddr}는 U-Boot 환경 변수로, 커널 이미지를 로드할 메모리 주소를 의미한다.

TFTP 다운로드가 완료되면 VxWorks 커널 이미지는 DRAM에 적재된다. 이후 이미지 포맷과 U-Boot 설정에 맞는 부팅 명령을 사용하여 커널을 실행한다. 예를 들어 U-Boot 이미지 형식이면 bootm ${loadaddr}를 사용할 수 있고, VxWorks 전용 부팅 명령을 지원하는 환경에서는 bootvx ${loadaddr}를 사용할 수 있다.

현재 목표는 NXP LS1046ARDB 보드에서 VxWorks 커널 이미지를 정상적으로 동작시키는 것이다. 다만 현재 사용 중인 QEMU 에뮬레이터 환경에서는 LS1046ARDB 보드를 직접 지원하지 않기 때문에, 생성한 VxWorks 커널 이미지를 QEMU 상에서 그대로 실행하기는 어렵다. 따라서 이후에는 실제 LS1046ARDB 보드 환경에서 U-Boot를 통해 커널 이미지를 RAM에 적재하고 부팅을 검증하는 과정이 필요하다.

profile
수용적인 자세로 끈임없이 성장해나가는 개발자 이진욱입니다.

0개의 댓글