U-Boot 환경 변수와 bootcmd — 부팅 시나리오 정의하기
#한 줄 요약
환경 변수는 U-Boot의 가변 상태 전부입니다. 부트 시나리오, 커널 인자, 네트워크 설정, MAC 주소까지 모두 environment에 들어가고, bootcmd가 그 변수들을 엮어 부트를 완성합니다.
10~12장에서 본 부트 미디어별 명령들은 결국 어떤 변수들의 조합으로 환경에 박힙니다. bootcmd는 그 조합의 entry point이고, distroboot는 표준화된 fallback 사슬입니다. 부트 동작을 바꾸고 싶다면 코드를 다시 빌드하지 않고도 환경 변수만으로 충분합니다.
이 글에서는 환경 변수의 저장 위치, 주요 변수의 의미, bootcmd의 구조, distro_bootcmd의 자동 fallback, 그리고 redundancy까지 정리합니다.
#환경은 어디에 저장되나
부트 ROM이 적재한 U-Boot 바이너리에는 default environment가 박혀 있습니다. saveenv로 수정하면 그 변경분이 persistent storage에 저장됩니다. 저장 위치는 빌드 시 CONFIG_ENV_IS_IN_*로 정합니다.
CONFIG_ENV_IS_IN_MMC=y # eMMC/SD의 raw offsetCONFIG_ENV_IS_IN_SPI_FLASH=y # SPI NOR의 erase blockCONFIG_ENV_IS_IN_NAND=y # NAND의 UBI 볼륨 또는 rawCONFIG_ENV_IS_IN_FAT=y # FAT 파일 시스템의 uboot.env 파일CONFIG_ENV_IS_IN_EXT4=y # ext4 파일 시스템의 uboot.env 파일CONFIG_ENV_IS_NOWHERE=y # 메모리에만 (saveenv 안됨)eMMC를 쓰는 보드의 환경은 user area의 특정 offset이나 boot partition에 있습니다.
#define CONFIG_ENV_OFFSET 0x400000 /* 4MiB offset */#define CONFIG_ENV_SIZE 0x10000 /* 64KiB */#define CONFIG_SYS_MMC_ENV_DEV 0#define CONFIG_SYS_MMC_ENV_PART 1 /* boot partition 1 */저장 위치는 erase size 단위로 정렬해야 합니다. SPI Flash의 sector size가 4KB면 offset도 4KB의 배수여야 합니다. 그렇지 않으면 erase가 다른 데이터를 같이 지웁니다.
#기본 명령들
=> printenvarch=arm64baudrate=115200board=myboardbootargs=console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwaitbootcmd=run distro_bootcmdbootdelay=2fdt_addr_r=0x43000000fdtfile=myboard.dtbkernel_addr_r=0x40400000loadaddr=0x40400000serverip=192.168.1.10...
=> printenv bootargsbootargs=console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait
=> setenv bootargs "console=ttyS0,115200 root=/dev/mmcblk0p2 ro quiet"=> saveenvSaving Environment to MMC... Writing to MMC(0)... OK
=> editenv bootargsedit: console=ttyS0,115200 root=/dev/mmcblk0p2 ro quietsetenv var ""은 변수를 삭제합니다. saveenv는 전체 environment를 한 번에 직렬화해 저장합니다. 변수 하나 변경에도 전체가 다시 쓰입니다.
#핵심 변수들
| 변수 | 역할 |
|---|---|
bootcmd | 자동 부트에서 실행되는 명령 |
bootargs | 커널에 넘기는 command line |
bootdelay | 자동 부트 전 대기 시간(초). -1이면 자동 부트 안 함 |
loadaddr | 일반 데이터 적재 주소 |
kernel_addr_r | 커널 적재 주소 |
fdt_addr_r | DTB 적재 주소 |
ramdisk_addr_r | initramfs 적재 주소 |
serverip / ipaddr | TFTP 서버와 자기 IP |
ethaddr | MAC 주소 (보통 OTP fuse나 환경 변수에 박음) |
fdtfile | DTB 파일 이름 (보드별) |
boot_targets | distroboot가 시도할 미디어 목록 |
이 변수들은 U-Boot 코드에서 기본값이 들어 있습니다. 보드별 헤더에서 CONFIG_EXTRA_ENV_SETTINGS로 추가합니다.
#define CONFIG_EXTRA_ENV_SETTINGS \ "kernel_addr_r=0x40400000\0" \ "fdt_addr_r=0x43000000\0" \ "ramdisk_addr_r=0x44000000\0" \ "fdtfile=myboard.dtb\0" \ "boot_targets=mmc0 usb0 pxe dhcp\0" \ "console=ttyS0,115200\0" \ "bootargs_default=console=${console} root=/dev/mmcblk0p2 rw rootwait\0"각 항목 끝의 \0이 변수 구분자입니다. \0 두 개가 연속되면 목록의 끝입니다.
#bootcmd의 구조
가장 단순한 bootcmd는 한 줄짜리 명령입니다.
bootcmd=mmc dev 0; ext4load mmc 0:1 ${kernel_addr_r} /boot/Image; \ ext4load mmc 0:1 ${fdt_addr_r} /boot/${fdtfile}; \ booti ${kernel_addr_r} - ${fdt_addr_r}그러나 보드별 차이, 부트 실패 시 fallback, 멀티 OS 지원 같은 요구가 늘면 함수 단위로 쪼개야 합니다.
bootcmd=run try_mmc || run try_usb || run try_dhcp
try_mmc=mmc dev 0 && ext4load mmc 0:1 ${kernel_addr_r} /boot/Image && \ ext4load mmc 0:1 ${fdt_addr_r} /boot/${fdtfile} && \ booti ${kernel_addr_r} - ${fdt_addr_r}
try_usb=usb start && load usb 0:1 ${kernel_addr_r} /boot/Image && \ load usb 0:1 ${fdt_addr_r} /boot/${fdtfile} && \ booti ${kernel_addr_r} - ${fdt_addr_r}
try_dhcp=dhcp ${kernel_addr_r} && booti ${kernel_addr_r} - ${fdt_addr_r}이런 식의 수작업 환경 변수가 보드별로 누적되면 관리가 어려워집니다. 그래서 도입된 것이 distroboot입니다.
#distro_bootcmd — 표준 부트 시나리오
distroboot는 표준화된 부트 시나리오 라이브러리입니다. include/config_distro_bootcmd.h에 정의되어 있고, 보드는 CONFIG_DISTRO_DEFAULTS=y를 켜기만 하면 됩니다.
boot_targets=mmc0 mmc1 nvme0 scsi0 usb0 pxe dhcp
bootcmd=run distro_bootcmd
distro_bootcmd= setenv scriptaddr 0x40200000; for target in ${boot_targets}; do run bootcmd_${target}; done
bootcmd_mmc0= setenv devnum 0; run mmc_boot
mmc_boot= mmc dev ${devnum}; if mmc rescan; then run scan_dev_for_boot_part; fi
scan_dev_for_boot_part= part list mmc ${devnum} -bootable devplist; for distro_bootpart in ${devplist}; do if load mmc ${devnum}:${distro_bootpart} ${scriptaddr} boot.scr; then source ${scriptaddr}; exit; fi done흐름이 한눈에 들어옵니다.
boot_targets을 순서대로 시도한다.- 각 미디어에서 bootable 파티션을 찾는다.
boot.scr나extlinux/extlinux.conf가 있으면 그것을 실행한다.- 한 미디어가 실패하면 다음으로 fall-through.
boot.scr는 10장에서 본 파일 시스템 안의 스크립트입니다. 부트 시나리오를 파일로 두면, U-Boot 환경을 건드리지 않고 OS만 새로 배포할 수 있습니다.
# /boot/boot.cmd (사람이 읽는 원본)setenv bootargs "console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait"load mmc ${devnum}:${distro_bootpart} ${kernel_addr_r} /boot/Imageload mmc ${devnum}:${distro_bootpart} ${fdt_addr_r} /boot/${fdtfile}booti ${kernel_addr_r} - ${fdt_addr_r}
# 빌드mkimage -A arm64 -O linux -T script -C none -d boot.cmd boot.scr#환경 변수 redundancy
저장 미디어가 부트 도중 전원이 꺼지면 environment가 깨질 수 있습니다. erase 중에 전원이 빠지면 sector가 all-FF 상태로 남아 다음 부트에서 environment를 못 읽습니다.
대책은 redundancy입니다. 두 카피를 두고, 한쪽이 망가져도 다른쪽으로 부트합니다.
CONFIG_ENV_OFFSET=0x400000CONFIG_ENV_OFFSET_REDUND=0x410000CONFIG_SYS_REDUNDAND_ENVIRONMENT=y저장 시 번갈아 가며 새 카피를 쓰고, 둘 다 유효하면 flag byte로 최신을 구분합니다. 한쪽이 깨졌다면 다른 쪽을 그대로 씁니다.
양산 보드는 redundancy를 기본으로 켭니다. 양산 중에 한 번의 갑작스러운 power loss로도 출하 후 부트가 안 될 수 있기 때문입니다.
#환경 변수의 default 처리
OTA나 공장 초기화 후 environment를 default로 되돌리고 싶을 때는 다음 절차를 씁니다.
=> env default -a## Resetting to default environment=> saveenv-a는 all입니다. 특정 변수만 default로 돌리려면 env default ipaddr serverip처럼 씁니다.
코드 측에서 default를 정의하는 곳은 include/env_default.h와 보드별 CONFIG_EXTRA_ENV_SETTINGS입니다. 빌드 후 변경할 수 없는 baseline입니다.
#자주 하는 실수
saveenv후 erase boundary에 다른 데이터가 있습니다. SPI Flash에서 env가 끝나는 sector에 부트로더 일부가 걸쳐 있으면saveenv가 부트로더를 지웁니다. layout을 항상 erase size 단위로.bootargs에서 변수 expansion 시점이 헷갈립니다.setenv bootargs "root=${root_dev}"은 setenv 시점에${root_dev}가 펼쳐집니다. 부트 시점에 펼치고 싶다면 single-quote처럼 escape하거나run안에서 펼치세요.- MAC 주소가 매번 random입니다.
ethaddr이 비어 있으면 U-Boot가 random MAC을 만듭니다. DHCP 서버의 고정 매핑이 깨집니다.ethaddr을 fuse나 env에 박아 두세요. - redundant env를 켜고 원래 offset만 채웠습니다. 두 카피 모두 각각의 위치에 초기 값이 있어야 합니다. 빌드 산출물의
u-boot.env를 두 위치에 모두 굽으세요. boot.scr의 mkimage-T script형식을 빠뜨립니다. plain text로 두면source가 실행하지 못합니다. 반드시mkimage -T script로 헤더를 붙이세요.
#정리
- 환경은 U-Boot의 가변 상태 전부입니다. 부트 시나리오, 커널 인자, 네트워크가 모두 들어갑니다.
- 저장 위치는 빌드 시
CONFIG_ENV_IS_IN_*로 정하고, erase size 단위로 정렬합니다. - 핵심 변수는
bootcmd,bootargs,loadaddr,*_addr_r,boot_targets,ethaddr입니다. bootcmd는 짧으면 한 줄, 길면 함수 단위 분해입니다.- distro_bootcmd는 표준 fall-through 라이브러리이고,
boot_targets만 정하면 끝입니다. - 양산 보드는 redundancy를 켜서 power loss로 인한 env 손상을 막습니다.
- 변수 expansion 시점, MAC 영구화, erase 정렬이 1차 실수입니다.
#다음 장 예고
다음 글에서는 모던 U-Boot의 bootflow/bootmeth를 봅니다. distro_bootcmd의 거대한 env 스크립트를 C 코드 기반의 모듈로 대체하는 새 모델입니다.
#관련 항목
- Ch 10: 스토리지 부트 —
boot.scr패턴 - Ch 11: 네트워크 부트 — distroboot의 pxe/dhcp target
- Ch 14: bootflow / bootmeth — distro_bootcmd의 후계자
- BSP Ch 6: U-Boot 포팅 — 보드별 환경 설정
- U-Boot Distro Bootcmd 문서
Bootloader Internals · 13 of 37
- 1ROM부터 init까지 — 임베디드 부팅 단계의 빈자리 분석
- 2Das U-Boot vs TF-A vs EDK II — 임베디드 부트로더 생태계 비교
- 3U-Boot 빌드 시스템 분석 — Kconfig·Makefile·defconfig 동작 추적
- 4ARM 임베디드 부트 4단계 분해 — BL1·SPL·TPL·U-Boot Proper의 역할
- 5U-Boot Falcon Mode — SPL이 U-Boot Proper 없이 커널 직접 부팅
- 6Device Tree DTB 부트로더 처리 — 로딩 시점과 fixup 메커니즘 추적
- 7U-Boot Driver Model 내부 — uclass·driver·device 추상화 구조
- 8U-Boot 보드 초기화 시퀀스 — board_init_f와 board_init_r 분리 이유
- 9DDR Controller 프로그래밍과 PHY Training — SPL의 가장 어려운 작업
- 10임베디드 스토리지 부팅 분석 — MMC·SCSI·NAND·SPI Flash 비교
- 11임베디드 네트워크 부팅 — TFTP·PXE·BOOTP 시퀀스 분석
- 12U-Boot USB 부팅 — fastboot·UMS·USB host 메커니즘
- 13U-Boot 환경 변수와 bootcmd — 부팅 시나리오 정의하기
- 14Modern U-Boot bootflow / bootmeth — 새 추상화 레이어 분석
- 15FIT image 구조 분석 — multi-image·hash·configuration 추적
- 16U-Boot Verified Boot — RSA 서명과 public key 임베딩 흐름
- 17임베디드 A/B 부팅 이중화 — OTA 안전성을 위한 부트 슬롯 설계
- 18U-Boot의 EFI 호환 분석 — bootefi 명령과 EFI loader 동작 원리
- 19Linux Boot ABI — ARM/ARM64 커널 진입 규약 추적
- 20임베디드 펌웨어 업데이트 — RAUC vs SWUpdate 비교
- 21새 보드 U-Boot 포팅 실전 — defconfig 작성부터 첫 부팅까지
- 22부트로더 디버깅 기법 — DEBUG·JTAG·serial·post-mortem 분석
- 23SoC BootROM·eFuse·OTP — 부팅의 0단계 분석
- 24SPL·TPL 내부 해부 — 가장 작은 부트 단계의 동작 추적
- 25ARM Trusted Firmware-A 통합 — BL1·BL2·BL31·BL32·BL33 흐름
- 26DDR Training과 PHY Calibration — 보드별 파라미터 튜닝
- 27임베디드 Chain of Trust — 다단계 서명 검증의 전체 흐름
- 28임베디드 Flash Layout 설계 — partition·NAND·eMMC·UBI 비교
- 29U-Boot Distro Boot — extlinux·boot.scr 표준화 분석
- 30부트로더 CI 구축 — build matrix와 자동 부팅 테스트
- 31TF-A BL31 EL3 Runtime 분석 — PSCI·SDEI·RAS dispatcher 추적
- 32PSCI와 SMCCC ABI — ARM 표준 SMC 호출 규약 분석
- 33ARM64 Secondary Core Bring-up — PSCI CPU_ON 호출부터 EL1 진입까지
- 34U-Boot PCIe Enumeration — 부트로더가 디바이스를 찾는 흐름 분석
- 35EFI·UEFI에서 CXL 초기화 — CEDT 생성과 HDM Decoder 사전 설정
- 36부트 시 메모리 토폴로지 결정 — DDR + CXL.mem 통합 인식
- 37UEFI Secure Boot 인증서 만료 — 2011→2023 CA 롤오버와 PQC 대비
관련 글
U-Boot PCIe Enumeration — 부트로더가 디바이스를 찾는 흐름 분석
U-Boot PCIe 열거 과정 — Root Complex 초기화·Config Space scan·BAR sizing·resource 할당, CXL DVSEC 인식까지.
U-Boot Distro Boot — extlinux·boot.scr 표준화 분석
보드별 다른 부트 스크립트를 표준화 — U-Boot Distro Boot, extlinux.conf, boot.scr의 차이와 선택.
SPL·TPL 내부 해부 — 가장 작은 부트 단계의 동작 추적
SPL과 TPL의 정확한 역할, SRAM 안에 들어가는 코드 구조, DDR이 없는 환경에서 어떻게 동작하는가.
이 글을 참조하는 글 (8)
- U-Boot Distro Boot — extlinux·boot.scr 표준화 분석— Bootloader Internals
- 임베디드 Flash Layout 설계 — partition·NAND·eMMC·UBI 비교— Bootloader Internals
- 임베디드 A/B 부팅 이중화 — OTA 안전성을 위한 부트 슬롯 설계— Bootloader Internals
- Modern U-Boot bootflow / bootmeth — 새 추상화 레이어 분석— Bootloader Internals
- U-Boot USB 부팅 — fastboot·UMS·USB host 메커니즘— Bootloader Internals
- 임베디드 네트워크 부팅 — TFTP·PXE·BOOTP 시퀀스 분석— Bootloader Internals
- 임베디드 스토리지 부팅 분석 — MMC·SCSI·NAND·SPI Flash 비교— Bootloader Internals
- U-Boot Falcon Mode — SPL이 U-Boot Proper 없이 커널 직접 부팅— Bootloader Internals