QUESTION
SPL의 image 선택과 적재 단계에서 실제로 바뀌는 상태는 무엇인가?
SPL은 U-Boot가 반드시 거치는 단계가 아니다. Boot ROM의 적재 주소·크기 제한, DDR 초기화 주체와 보드의 후속 이미지·검증 정책에 따라 ROM → U-Boot proper 또는 ROM → SPL → U-Boot proper 같은 경로를 사용한다. 공식 부트 단계 문서에서 TPL/VPL/SPL은 선택 단계다. 아래 loader 분석은 SPL을 사용하는 빌드에 관한 것이며, SPL이 Linux나 다른 펌웨어를 적재하는 구성도 있다.
NXP의 2020년 i.MX 포팅 가이드 §3.2.1에서는 i.MX 6Quad 구성 예의 ROM이 이미지 헤더에 있는 DCD 명령으로 DDR을 초기화한다. 같은 문서의 i.MX 8M Quad/Mini/Nano/Plus BSP 경로는 SPL에서 DDR controller/PHY 초기화와 training을 수행한다. 이는 해당 가이드의 구성 대비이며 모든 i.MX 6·8 보드에 적용되는 부팅 경로를 뜻하지 않는다.
SPL은 크기 제약 때문에 full U-Boot와 같은 기능을 가정할 수 없다. loader 등록, 우선순위, fallback과 spl_image_info 필드가 채워지는 순간을 본다.
선택된 loader는 검증된 크기와 주소를 제공하고 제어권 이전 전에 cache와 실행 환경을 정리해야 한다. 이 문장을 기준으로 코드를 위에서 아래로 읽으면, 함수 이름을 외우는 대신 어느 시점에 어떤 상태를 신뢰할 수 있는지 판단할 수 있다.
STRUCTURE
객체와 주소가 놓이는 구조
struct spl_image_info / spl_boot_device를 중심에 놓고 왼쪽의 입력이 어떤 검사를 거쳐 오른쪽 결과로 공개되는지 표시했다. 실제 디버깅에서는 각 블록의 주소와 크기를 로그에 대입한다.
CALL PATH
실행 흐름
앞선 TPL/VPL을 사용하는 경우 그 인계 단계는 이 그림에서 생략했다. TPL/VPL/SPL 자체는 보드 구성에 따라 생략될 수 있고, SPL의 다음 대상도 U-Boot proper로 고정되지 않는다. 화살표는 단계 간 제어권 인계와 SPL 내부 처리를 함께 요약한 것이며 모두 함수 호출·반환을 뜻하지 않는다.
선택된 loader가 실패하면 구성된 부트 순서에 따라 다음 후보를 시도할 수 있다. 모든 후보가 실패한 경우와 이미지를 적재한 뒤 진입점으로 제어권을 넘기는 경우를 구분한다.
STATE LEDGER
단계별 입력과 출력
각 단계의 입력과 처리 후 바뀌는 상태를 정리한 표다. 사용 중인 소스 버전에서 다른 필드를 변경한다면 그에 맞춰 표도 수정한다.
| # | 단계 | 진입 시 신뢰할 상태 | 성공 뒤 남아야 할 상태 | 다음 소비자 |
|---|---|---|---|---|
| 01 | ROM | SoC가 정한 부트 조건 | 해당 구성의 첫 단계 이미지와 진입 주소 | SPL 또는 앞선 TPL/VPL 경로 |
| 02 | SPL 진입 | ROM 또는 앞선 단계의 인계 상태 | SPL 실행 환경과 보드가 정한 부트 후보 순서 | boot device 선택 |
| 03 | boot device | 부트 후보 순서와 빌드에 포함된 loader | 시도할 매체와 해당 loader | image loader |
| 04 | image loader | 매체·형식에 맞는 읽기 조건 | 적재된 이미지와 spl_image_info의 주소·진입점·OS 정보 | 이 예의 U-Boot proper 진입 경로 |
| 05 | U-Boot proper | SPL이 넘긴 실행 상태와 인수 | 부팅 명령이 준비한 후속 이미지와 인수 | 선택된 OS 또는 후속 펌웨어(부팅 명령 후) |
분석 범위의 전제와 비교해, 다음 단계에 잘못된 입력을 넘긴 부분부터 조사한다.
VISUAL WALKTHROUGH
주소와 객체의 이동을 그림으로 읽기
아래 그림은 주소, 객체를 사용하는 기간, 제어권을 넘기는 시점을 중심으로 정리했다. 실제 주소와 크기는 사용 중인 보드의 bdinfo, 맵 파일과 링커 심벌로 확인한다.
medium read와 image 해석, 검증, 적재를 한 단계로 뭉치지 않는다.
압축 image라면 compressed source와 decompressed output을 모두 그린다.
안전
01SPL text/stack
02read buffer
03검증 범위
04빈 destination
05next-stage image
위험
01SPL text/stack
02read buffer
03destination overlap
04copy 중 source 훼손
05entry 이전 corruption
UPSTREAM SOURCE
원본 코드
아래 코드는 U-Boot v2026.07의 common/spl/spl.c에서 648-736줄을 그대로 가져온 것이다. 설명을 위해 실제 코드를 가짜 의사 코드로 바꾸지 않았다.
648 }
649
650 ret = spl_load_image(spl_image, loader);
651 if (!ret) {
652 spl_image->boot_device = bootdev;
653 return 0;
654 }
655 printf("Error: %d\n", ret);
656 }
657
658 if (!CONFIG_IS_ENABLED(SILENT_CONSOLE)) {
659 if (CONFIG_IS_ENABLED(SHOW_ERRORS))
660 printf(PHASE_PROMPT
661 "Unsupported Boot Device %d\n",
662 bootdev);
663 else
664 printf(PHASE_PROMPT
665 "Unsupported Boot Device!\n");
666 }
667 }
668
669 return ret;
670}
671
672void board_init_r(gd_t *dummy1, ulong dummy2)
673{
674 u32 spl_boot_list[] = {
675 BOOT_DEVICE_NONE,
676 BOOT_DEVICE_NONE,
677 BOOT_DEVICE_NONE,
678 BOOT_DEVICE_NONE,
679 BOOT_DEVICE_NONE,
680 };
681 spl_jump_to_image_t jumper = &jump_to_image;
682 struct spl_image_info spl_image;
683 int ret, os;
684 void *fdt;
685
686 debug(">>" PHASE_PROMPT "board_init_r()\n");
687
688 spl_set_bd();
689
690 if (IS_ENABLED(CONFIG_SPL_SYS_MALLOC)) {
691 mem_malloc_init(SPL_SYS_MALLOC_START, SPL_SYS_MALLOC_SIZE);
692 gd->flags |= GD_FLG_FULL_MALLOC_INIT;
693 }
694 if (!(gd->flags & GD_FLG_SPL_INIT)) {
695 if (spl_init())
696 hang();
697 }
698 timer_init();
699 if (CONFIG_IS_ENABLED(BLOBLIST)) {
700 ret = bloblist_init();
701 if (ret) {
702 debug("%s: Failed to set up bloblist: ret=%d\n",
703 __func__, ret);
704 puts(PHASE_PROMPT "Cannot set up bloblist\n");
705 hang();
706 }
707 }
708 if (CONFIG_IS_ENABLED(HANDOFF)) {
709 int ret;
710
711 ret = setup_spl_handoff();
712 if (ret) {
713 puts(PHASE_PROMPT "Cannot set up SPL handoff\n");
714 hang();
715 }
716 }
717
718 if (CONFIG_IS_ENABLED(SOC_INIT))
719 spl_soc_init();
720
721 if (IS_ENABLED(CONFIG_SPL_WATCHDOG) && CONFIG_IS_ENABLED(WDT))
722 initr_watchdog();
723
724 if (IS_ENABLED(CONFIG_SPL_OS_BOOT) || CONFIG_IS_ENABLED(HANDOFF) ||
725 IS_ENABLED(CONFIG_SPL_ATF) || IS_ENABLED(CONFIG_SPL_NET))
726 dram_init_banksize();
727
728 if (IS_ENABLED(CONFIG_SPL_LMB))
729 lmb_init();
730
731 if (CONFIG_IS_ENABLED(PCI) && !(gd->flags & GD_FLG_DM_DEAD)) {
732 ret = pci_init();
733 if (ret)
734 puts(PHASE_PROMPT "Cannot initialize PCI\n");
735 /* Don't fail. We still can try other boot methods. */
736 }
READING CONTRACT
발췌 범위와 읽기 기준
원문 648-736줄에는 앞뒤 문맥이 포함됩니다. 기준 구문 void board_init_r(gd_t *dummy1, ulong dummy2)은 L672에 있습니다. 발췌 첫 줄을 함수 정의의 시작으로 해석하지 않습니다.
SPL은 크기 제약 때문에 full U-Boot와 같은 기능을 가정할 수 없다. loader 등록, 우선순위, fallback과 spl_image_info 필드가 채워지는 순간을 본다.
DETAILS
내부 동작을 더 깊게 읽기
진입 조건을 먼저 고정한다
ROM에서 들어온 실행은 부트 CPU의 동기 실행 문맥에 놓여 있다. 이때 interrupt, MMU/cache, stack, heap 중 무엇이 이미 준비되었는지 소스의 호출자까지 올라가 확인한다. 같은 함수라도 SPL, relocation 전후, app thread처럼 호출 문맥이 달라지면 허용되는 API와 지연 시간이 달라진다.
SPL은 크기 제약 때문에 full U-Boot와 같은 기능을 가정할 수 없다. loader 등록, 우선순위, fallback과 spl_image_info 필드가 채워지는 순간을 본다.
중심 객체의 생성과 공개를 나눈다
이 글의 중심 객체는 struct spl_image_info / spl_boot_device다. 메모리를 확보한 시점, 필드를 채운 시점, 전역 list나 다른 subsystem에 공개한 시점을 구분한다. 공개 뒤 오류가 발생한다면 목록에서 제거하고 child, buffer, reference를 역순으로 정리하는지 확인한다.
symbol과 section은 ELF인 spl/u-boot-spl과 spl/u-boot-spl.map에서 확인한다. spl/u-boot-spl.bin은 별도 바이너리 산출물이므로 ELF의 심볼 정보를 그대로 갖는다고 가정하지 않는다. 실제 부트 이미지는 보드에 따라 헤더나 다른 구성 요소를 더해 만들 수 있다.
주소, 크기와 정렬을 계산한다
부트 코드에서는 주소 계산 오류를 주의해야 한다. 원본과 목적지의 주소 범위, 헤더에 기록된 데이터 크기, 블록·페이지 단위를 표로 정리하고 각 구간의 끝 주소를 계산한다. 끝 주소가 start + size - 1처럼 마지막 바이트를 가리키는지, 구간 바로 다음 주소를 가리키는지는 API 규약에서 확인한다.
성공 flag와 실제 완료 시점을 맞춘다
TPL or SPL → boot device → image loader 구간에서는 부분 초기화 상태가 생길 수 있다. flag, list insertion, callback 등록, storage write 완료 중 무엇이 성공의 기준인지 찾는다. hardware write나 DMA가 포함되면 함수 반환과 장치 완료가 같은 시점인지도 확인한다.
다른 CPU, 인터럽트 처리기, USB 완료 콜백, 셸 명령에서도 이 상태를 읽는다면 메모리 접근 순서와 잠금 범위를 함께 확인한다.
제어권을 넘기기 전에 확인할 조건을 정리한다
정상 경로는 U-Boot proper에서 끝난다. 이 단계가 함수 반환인지, scheduler 전환인지, 다른 image로의 비복귀 분기인지 구분한다. 비복귀 handoff라면 cache clean/invalidate, interrupt disable, 장치 quiesce, argument register와 FDT 또는 image address가 최종 점검 항목이다.
호출한 함수로 돌아오는 경로라면 오류와 일부 처리만 완료된 상태를 구분하는지 확인한다. 이어서 다음 후보를 시도할지, 복구 절차를 실행할지 살펴본다.
IMPLEMENTATION NOTES
구현을 읽을 때 놓치기 쉬운 부분
SPL loader는 boot device를 image reader로 추상화한다
ROM이나 board code가 고른 boot device는 MMC, SPI, NAND, USB 같은 loader로 연결된다. loader는 block 단위와 byte offset, header 위치를 spl_load_info 계약에 맞춰 제공해야 하며 medium별 alignment 차이를 공통 image parser에 노출하지 않아야 한다.
동일한 partition 이름이라도 hardware partition, raw offset, filesystem file 경로가 될 수 있다. 실제 loader가 선택한 source와 offset을 로그에 남겨야 다른 medium의 성공 결과와 섞이지 않는다.
spl_image_info는 검증 결과와 실행 계획을 함께 품는다
image type, load address, entry point, payload size, OS와 flags가 parser에서 채워진다. header가 선언한 값과 board가 강제한 값 중 어느 쪽이 우선하는지, FIT configuration을 쓸 때 선택된 subimage 값이 어디에 반영되는지 확인한다.
적재 목적지가 현재 SPL text, stack, BSS, malloc 또는 FDT와 겹치지 않아야 한다. 압축 image는 compressed source와 decompressed destination 두 구간을 모두 계산한다.
fallback은 오류 종류에 따라 달라져야 한다
매체가 없다는 오류, image가 없다는 오류, signature가 틀린 오류를 같은 fallback으로 처리하면 보안 정책을 우회할 수 있다. verified boot 실패 뒤 unsigned 다른 medium으로 넘어가는 것이 제품 요구인지 명시해야 한다.
각 loader에 짧은 read, CRC 오류, timeout을 주입하고 다음 후보로 이동하는 return code를 기록한다. watchdog service가 긴 storage retry 동안 유지되는지도 측정한다.
OBJECT LIFETIME
객체와 수명
| 대상 | 만들어지는 시점 | 유효 범위 | 확인할 조건 |
|---|---|---|---|
struct spl_image_info / spl_boot_device | TPL or SPL | U-Boot proper 또는 오류 정리 완료까지 | 진입 전제와 상태 계약 |
| 입력 buffer / metadata | ROM | parse와 검증이 끝날 때까지 | 길이, 정렬, 소유권, 변조 가능성 |
| 등록된 list / descriptor | boot device | 하위 시스템을 종료하거나 다음 이미지에 제어권을 넘길 때까지 | 중복 등록, 해제된 객체를 가리키는 포인터, 오류 후 자원 정리 |
| hardware 또는 persistent state | 실제 write/probe가 완료된 뒤 | reset 또는 명시적 해제·갱신까지 | 쓰기 완료, 시간 초과, 전원 차단, 이전 상태 복원 |
spl/u-boot-spl.bin | SPL 빌드의 바이너리 생성 시점 | 배포용 파일의 수명과 적재된 SPL 메모리의 수명은 별개 | ELF/map과 바이너리 크기·적재 주소·보드별 이미지 헤더의 관계 |
FAILURE PATH
실패 지점과 증상
| # | 조건 | 관찰되는 증상 | 먼저 볼 단계 |
|---|---|---|---|
| 01 | boot device 오판 | 잘못된 매체 접근 | TPL or SPL |
| 02 | image size 신뢰 | DRAM overwrite | boot device |
| 03 | fallback 순서 오류 | 복구 장치 무시 | image loader |
로그가 끊긴 함수뿐 아니라 그 함수에 주소, 크기, 플래그를 전달한 앞 단계도 조사한다. 오류 처리 후 등록된 객체나 하드웨어 설정이 그대로 남아 있는지도 확인한다.
EVIDENCE
소스 밖에서 확인할 증거
소스에서 파악한 동작을 실제 빌드 결과와 대상 보드의 로그로 확인한다. 아래 명령의 보드 이름과 툴체인 접두사는 사용 중인 빌드 환경에 맞게 바꾸고, 별도 출력 디렉터리를 쓰면 그 디렉터리에서 ELF 경로를 지정한다. 이 글에서 특정 보드의 빌드·실행을 재현한 것은 아니다.
| # | 목적 | 명령 또는 계측 | 판정 기준 |
|---|---|---|---|
| 01 | 빌드 설정과 명령 | make <board>_defconfig && make V=1 | 선택된 CONFIG와 실제 compile/link 명령을 보관한다. |
| 02 | SPL symbol | ${CROSS_COMPILE}nm -n spl/u-boot-spl | grep ' board_init_r$' | nm의 심볼명은 C 함수 선언문이 아니다. 해당 보드 구성에서 심볼의 존재와 주소를 확인한다. |
| 03 | SPL 어셈블리 대조 | ${CROSS_COMPILE}objdump -drS spl/u-boot-spl | ELF의 명령·재배치를 대조한다. -S 소스 줄 병기는 디버그 정보와 소스 파일을 찾을 수 있는지에 달려 있다. |
| 04 | proper의 주소 배치 | bdinfo | 해당 명령을 포함한 U-Boot proper에서 확인한다. 이 출력이 앞선 SPL의 당시 주소 배치를 그대로 뜻하지는 않는다. |
| 05 | proper의 runtime 관찰 | dm tree; dm uclass; printenv | 해당 명령을 포함한 U-Boot proper에서 확인한다. SPL 내부 상태는 해당 단계의 로그·계측으로 별도 확인한다. |
LAB
직접 확인할 실험
- 01SPL 로그에 boot device와 loader 이름 추가
TPL or SPL진입 전후에 SRAM SPL의 주소·크기·반환값과 timestamp를 함께 남긴다. - 02이미지 헤더 길이 변조 후 거부 확인
boot device진입 전후에 loader methods의 주소·크기·반환값과 timestamp를 함께 남긴다. - 03주 장치 read 실패를 주입해 fallback 추적
image loader진입 전후에 spl_image_info의 주소·크기·반환값과 timestamp를 함께 남긴다.
PRIMARY REFERENCES