QUESTION
x86는 왜 board_init_f 안에서 relocation을 끝내며 CAR stack의 수명은 정확히 어느 줄에서 끝나는가?
ARM처럼 board_init_f가 돌아온 뒤 assembly relocate_code를 찾으면 안 된다. x86는 C initcall 목록에서 copy, fixup, BSS clear와 jump를 완료하며 trampoline은 새 stack을 활성화한 뒤에만 CAR를 해제한다.
destination image의 code byte, relocation target, BSS, gd/GDT와 stack이 모두 새 DRAM 주소 체계에 일치한 뒤 old CAR와 source image의 수명을 끝내야 한다. 이 문장을 기준으로 코드를 위에서 아래로 읽으면, 함수 이름을 외우는 대신 어느 시점에 어떤 상태를 신뢰할 수 있는지 판단할 수 있다.
STRUCTURE
객체와 주소가 놓이는 구조
relocated x86 image / new stack / new gd를 중심에 놓고 왼쪽의 입력이 어떤 검사를 거쳐 오른쪽 결과로 공개되는지 표시했다. 실제 디버깅에서는 각 블록의 주소와 크기를 로그에 대입한다.
CALL PATH
실행 흐름
화살표는 단순 호출 순서만 뜻하지 않는다. 각 단계가 성공을 반환할 때 다음 단계가 읽을 수 있는 상태가 무엇인지 함께 확인한다. 오류 반환이 발생하면 바로 다음 화살표로 진행하지 않고 해당 단계의 정리 경로를 따라간다.
STATE LEDGER
단계별 입력과 출력
각 단계의 입력과 처리 후 바뀌는 상태를 정리한 표다. 사용 중인 소스 버전에서 다른 필드를 변경한다면 그에 맞춰 표도 수정한다.
| # | 단계 | 진입 시 신뢰할 상태 | 성공 뒤 남아야 할 상태 | 다음 소비자 |
|---|---|---|---|---|
| 01 | board_init_f reservation | board_init_f의 마지막 x86 initcall과 비복귀 trampoline | source image | copy_uboot_to_ram |
| 02 | copy_uboot_to_ram | board_init_f reservation 완료 상태 | gd->relocaddr | ELF relocation fixups |
| 03 | ELF relocation fixups | copy_uboot_to_ram 완료 상태 | .rela.dyn | clear BSS |
| 04 | clear BSS | ELF relocation fixups 완료 상태 | new BSS | jump_to_copy |
| 05 | jump_to_copy | clear BSS 완료 상태 | new gd and GDT | new DRAM stack |
| 06 | new DRAM stack | jump_to_copy 완료 상태 | runtime stack | car_uninit |
| 07 | car_uninit | new DRAM stack 완료 상태 | runtime stack | board_init_f_r |
| 08 | board_init_f_r | car_uninit 완료 상태 | runtime stack | 최종 DRAM의 relocated U-Boot proper 또는 다음 stage |
분석 범위의 전제와 비교해, 다음 단계에 잘못된 입력을 넘긴 부분부터 조사한다.
VISUAL WALKTHROUGH
주소와 객체의 이동을 그림으로 읽기
아래 그림은 주소, 객체를 사용하는 기간, 제어권을 넘기는 시점을 중심으로 정리했다. 실제 주소와 크기는 사용 중인 보드의 bdinfo, 맵 파일과 링커 심벌로 확인한다.
크기는 개념 비율이다. 실제 분석에서는 map, symbol과 runtime 출력의 시작·끝 주소를 대입한다.
호출 이름만 나열하지 않고, 각 단계가 다음 단계에 넘기는 상태를 기준으로 실행 순서를 표시한다.
왼쪽 생산자가 가운데 상태를 완성하고, 오른쪽 소비자가 그 상태를 처음 사용하는 관계다.
reserve_uboot→gd->relocaddr→copy_uboot_to_ramcopy→destination bytes→ELF fixup.rela.dyn→new absolute pointers→runtime Cclear_bss→zero-init globals→board_init_f_rnew stack→CAR 독립 call frame→car_uninitUPSTREAM SOURCE
원본 코드
아래 코드는 U-Boot v2026.07의 arch/x86/cpu/start.S에서 173-224줄을 그대로 가져온 것이다. 설명을 위해 실제 코드를 가짜 의사 코드로 바꾸지 않았다.
173 post_code(POST_START_DONE)
174 xorl %eax, %eax
175
176 /* Enter, U-Boot! */
177 call board_init_f
178
179 /* indicate (lack of) progress */
180 movw $0x85, %ax
181 jmp die
182
183.globl board_init_f_r_trampoline
184.type board_init_f_r_trampoline, @function
185board_init_f_r_trampoline:
186 /*
187 * SDRAM has been initialised, U-Boot code has been copied into
188 * RAM, BSS has been cleared and relocation adjustments have been
189 * made. It is now time to jump into the in-RAM copy of U-Boot
190 *
191 * %eax = Address of top of new stack
192 */
193
194 /* Stack grows down from top of SDRAM */
195 movl %eax, %esp
196
197 /* See if we need to disable CAR */
198 call car_uninit
199
200 /* Re-enter U-Boot by calling board_init_f_r() */
201 call board_init_f_r
202
203#ifdef CONFIG_TPL
204.globl jump_to_spl
205.type jump_to_spl, @function
206jump_to_spl:
207 /* Reset stack to the top of CAR space */
208 movl $(CONFIG_SYS_CAR_ADDR + CONFIG_SYS_CAR_SIZE - 4), %esp
209#ifdef CONFIG_DCACHE_RAM_MRC_VAR_SIZE
210 subl $CONFIG_DCACHE_RAM_MRC_VAR_SIZE, %esp
211#endif
212
213 jmp *%eax
214#endif
215
216die:
217 hlt
218 jmp die
219 hlt
220
221WEAK(car_uninit)
222 ret
223ENDPROC(car_uninit)
224
READING CONTRACT
발췌 범위와 읽기 기준
원문 173-224줄에는 앞뒤 문맥이 포함됩니다. 기준 구문 board_init_f_r_trampoline:은 L185에 있습니다. 발췌 첫 줄을 함수 정의의 시작으로 해석하지 않습니다.
ARM처럼 board_init_f가 돌아온 뒤 assembly relocate_code를 찾으면 안 된다. x86는 C initcall 목록에서 copy, fixup, BSS clear와 jump를 완료하며 trampoline은 새 stack을 활성화한 뒤에만 CAR를 해제한다.
board_init_f_r_trampoline:복사와 relocation fixup이 끝난 destination image에서 새 stack으로 runtime을 재개하는 32-bit trampoline이다.
movl %eax, %espcaller가 EAX로 넘긴 DRAM stack top을 활성 stack으로 만든다. 이후 old CAR stack은 더 이상 사용하지 않는다.
call car_uninitstack을 DRAM으로 옮긴 뒤 Cache-As-RAM을 해제한다. 두 줄의 순서를 바꾸면 return address부터 사라진다.
call board_init_f_rnew stack과 relocated image에서 runtime C 초기화의 후반부로 들어간다.
RELATED SOURCE
함께 읽어야 하는 원본 코드
첫 코드 조각만으로 동작이 완성되지 않는 경우 호출 매크로, 객체 정의와 실제 실행 목록을 같은 페이지에 묶었다. 각 조각은 같은 기준 commit에서 가져왔다.
01. U-Boot image byte를 gd->relocaddr로 복사
arch/x86/lib/relocate.c 19-59줄이다.
19#include <asm/global_data.h>
20#include <asm/u-boot-x86.h>
21#include <asm/sections.h>
22#include <elf.h>
23
24DECLARE_GLOBAL_DATA_PTR;
25
26int copy_uboot_to_ram(void)
27{
28 size_t len = (uintptr_t)__data_end - (uintptr_t)__text_start;
29
30 if (gd->flags & GD_FLG_SKIP_RELOC)
31 return 0;
32 memcpy((void *)gd->relocaddr, (void *)__text_start, len);
33
34 return 0;
35}
36
37#ifndef CONFIG_EFI_APP
38int clear_bss(void)
39{
40 ulong dst_addr = (ulong)__bss_start + gd->reloc_off;
41 size_t len = (uintptr_t)__bss_end - (uintptr_t)__bss_start;
42
43 if (gd->flags & GD_FLG_SKIP_RELOC)
44 return 0;
45 memset((void *)dst_addr, 0x00, len);
46
47 return 0;
48}
49#endif
50
51#if CONFIG_IS_ENABLED(X86_64)
52static void do_elf_reloc_fixups64(unsigned int text_base, uintptr_t size,
53 Elf64_Rela *re_src, Elf64_Rela *re_end)
54{
55 Elf64_Addr *offset_ptr_rom, *last_offset = NULL;
56 Elf64_Addr *offset_ptr_ram;
57
58 do {
59 unsigned long long type = ELF64_R_TYPE(re_src->r_info);
주요 구문 해설
unsigned long long type = ELF64_R_TYPE(re_src->r_info);ELF64 r_info에서 relocation type을 분리한다. 이 구현은 R_X86_64_RELATIVE만 지원하므로 다른 type은 같은 식으로 고치지 않는다.
02. x86-64 ELF relocation entry를 새 image에 적용
arch/x86/lib/relocate.c 47-120줄이다.
47 return 0;
48}
49#endif
50
51#if CONFIG_IS_ENABLED(X86_64)
52static void do_elf_reloc_fixups64(unsigned int text_base, uintptr_t size,
53 Elf64_Rela *re_src, Elf64_Rela *re_end)
54{
55 Elf64_Addr *offset_ptr_rom, *last_offset = NULL;
56 Elf64_Addr *offset_ptr_ram;
57
58 do {
59 unsigned long long type = ELF64_R_TYPE(re_src->r_info);
60
61 if (type != R_X86_64_RELATIVE) {
62 printf("%s: unsupported relocation type 0x%llx "
63 "at %p, ", __func__, type, re_src);
64 printf("offset = 0x%llx\n", re_src->r_offset);
65 continue;
66 }
67
68 /* Get the location from the relocation entry */
69 offset_ptr_rom = (Elf64_Addr *)(uintptr_t)re_src->r_offset;
70
71 /* Check that the location of the relocation is in .text */
72 if (offset_ptr_rom >= (Elf64_Addr *)(uintptr_t)text_base &&
73 offset_ptr_rom > last_offset) {
74 /* Switch to the in-RAM version */
75 offset_ptr_ram = (Elf64_Addr *)((ulong)offset_ptr_rom +
76 gd->reloc_off);
77
78 /* Check that the target points into .text */
79 if (*offset_ptr_ram >= text_base &&
80 *offset_ptr_ram <= text_base + size) {
81 *offset_ptr_ram = gd->reloc_off +
82 re_src->r_addend;
83 } else {
84 debug(" %p: %lx: rom reloc %lx, ram %p, value %lx, limit %lX\n",
85 re_src, (ulong)re_src->r_info,
86 (ulong)re_src->r_offset, offset_ptr_ram,
87 (ulong)*offset_ptr_ram, text_base + size);
88 }
89 } else {
90 debug(" %p: %lx: rom reloc %lx, last %p\n", re_src,
91 (ulong)re_src->r_info, (ulong)re_src->r_offset,
92 last_offset);
93 }
94 last_offset = offset_ptr_rom;
95
96 } while (++re_src < re_end);
97}
98#else
99static void do_elf_reloc_fixups32(unsigned int text_base, uintptr_t size,
100 Elf32_Rel *re_src, Elf32_Rel *re_end)
101{
102 Elf32_Addr *offset_ptr_rom, *last_offset = NULL;
103 Elf32_Addr *offset_ptr_ram;
104
105 do {
106 unsigned int type = ELF32_R_TYPE(re_src->r_info);
107
108 if (type != R_386_RELATIVE) {
109 printf("%s: unsupported relocation type 0x%x "
110 "at %p, ", __func__, type, re_src);
111 printf("offset = 0x%x\n", re_src->r_offset);
112 continue;
113 }
114
115 /* Get the location from the relocation entry */
116 offset_ptr_rom = (Elf32_Addr *)(uintptr_t)re_src->r_offset;
117
118 /* Check that the location of the relocation is in .text */
119 if (offset_ptr_rom >= (Elf32_Addr *)(uintptr_t)text_base &&
120 offset_ptr_rom > last_offset) {
주요 구문 해설
unsigned long long type = ELF64_R_TYPE(re_src->r_info);ELF64 r_info에서 relocation type을 분리한다. 이 구현은 R_X86_64_RELATIVE만 지원하므로 다른 type은 같은 식으로 고치지 않는다.
offset_ptr_ram = (Elf64_Addr *)((ulong)offset_ptr_rom +link-time patch 주소에 gd->reloc_off를 더해 destination image에서 실제로 쓸 8-byte word 위치를 만든다.
*offset_ptr_ram = gd->reloc_off +R_X86_64_RELATIVE addend는 link-time address이므로 signed relocation offset을 더해 destination runtime address로 바꾼다.
03. x86 relocation initcall의 실제 순서
common/board_f.c 990-1051줄이다.
990 CONFIG_IS_ENABLED(OF_INITIAL_DTB_READONLY)
991 INITCALL(reloc_fdt);
992 INITCALL(fix_fdt);
993#endif
994 INITCALL(reserve_bootstage);
995 INITCALL(reserve_bloblist);
996 INITCALL(reserve_arch);
997 INITCALL(reserve_stacks);
998 INITCALL(dram_init_banksize);
999 INITCALL(show_dram_config);
1000 WATCHDOG_RESET();
1001 INITCALL(setup_bdinfo);
1002 INITCALL(display_new_sp);
1003 WATCHDOG_RESET();
1004#if !CONFIG_IS_ENABLED(OF_BOARD_FIXUP) || \
1005 !CONFIG_IS_ENABLED(INITIAL_DTB_READONLY)
1006 INITCALL(reloc_fdt);
1007#endif
1008 INITCALL(reloc_bootstage);
1009 INITCALL(reloc_bloblist);
1010 INITCALL(setup_reloc);
1011#if CONFIG_IS_ENABLED(X86) || CONFIG_IS_ENABLED(ARC)
1012 INITCALL(copy_uboot_to_ram);
1013 INITCALL(do_elf_reloc_fixups);
1014#endif
1015 INITCALL(clear_bss);
1016 /*
1017 * Deregister all cyclic functions before relocation, so that
1018 * gd->cyclic_list does not contain any references to pre-relocation
1019 * devices. Drivers will register their cyclic functions anew when the
1020 * devices are probed again.
1021 *
1022 * This should happen as late as possible so that the window where a
1023 * watchdog device is not serviced is as small as possible.
1024 */
1025 INITCALL(cyclic_unregister_all);
1026#if !CONFIG_IS_ENABLED(ARM) && !CONFIG_IS_ENABLED(SANDBOX)
1027 INITCALL(jump_to_copy);
1028#endif
1029}
1030
1031void board_init_f(ulong boot_flags)
1032{
1033 struct board_f boardf;
1034
1035 gd->flags = boot_flags;
1036 gd->flags &= ~GD_FLG_HAVE_CONSOLE;
1037 gd->boardf = &boardf;
1038
1039 initcall_run_f();
1040
1041#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
1042 !defined(CONFIG_EFI_APP) && !CONFIG_IS_ENABLED(X86_64) && \
1043 !defined(CONFIG_ARC)
1044 /* NOTREACHED - jump_to_copy() does not return */
1045 hang();
1046#endif
1047}
1048
1049#if defined(CONFIG_X86) || defined(CONFIG_ARC)
1050/*
1051 * For now this code is only used on x86.
04. 64-bit 새 stack과 gd를 받는 trampoline
arch/x86/cpu/start64.S 26-49줄이다.
26 call board_init_f
27 call board_init_f_r
28
29 /* Should not return here */
30 jmp .
31
32.globl board_init_f_r_trampoline64
33.type board_init_f_r_trampoline64, @function
34board_init_f_r_trampoline64:
35 /*
36 * SDRAM has been initialised, U-Boot code has been copied into
37 * RAM, BSS has been cleared and relocation adjustments have been
38 * made. It is now time to jump into the in-RAM copy of U-Boot
39 *
40 * %eax = Address of top of new stack
41 */
42
43 /* Stack grows down from top of SDRAM */
44 movq %rsi, %rsp
45
46 /* New gd is in rdi */
47
48 /* Re-enter U-Boot by calling board_init_f_r() */
49 call board_init_f_r
주요 구문 해설
call board_init_f_rnew stack과 relocated image에서 runtime C 초기화의 후반부로 들어간다.
call board_init_f_rnew stack과 relocated image에서 runtime C 초기화의 후반부로 들어간다.
05. 새 gd와 stack을 architecture trampoline으로 넘기는 jump_to_copy
common/board_f.c 744-785줄이다.
744{
745 return board_fix_fdt((void *)gd->fdt_blob);
746}
747#endif
748
749/* ARM calls relocate_code from its crt0.S */
750#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX)
751
752static int jump_to_copy(void)
753{
754 if (gd->flags & GD_FLG_SKIP_RELOC)
755 return 0;
756 /*
757 * x86 is special, but in a nice way. It uses a trampoline which
758 * enables the dcache if possible.
759 *
760 * For now, other archs use relocate_code(), which is implemented
761 * similarly for all archs. When we do generic relocation, hopefully
762 * we can make all archs enable the dcache prior to relocation.
763 */
764#if defined(CONFIG_X86) || defined(CONFIG_ARC)
765 /*
766 * SDRAM and console are now initialised. The final stack can now
767 * be setup in SDRAM. Code execution will continue in Flash, but
768 * with the stack in SDRAM and Global Data in temporary memory
769 * (CPU cache)
770 */
771 arch_setup_gd(gd->new_gd);
772# if CONFIG_IS_ENABLED(X86_64)
773 board_init_f_r_trampoline64(gd->new_gd, gd->start_addr_sp);
774# else
775 board_init_f_r_trampoline(gd->start_addr_sp);
776# endif
777#else
778 relocate_code(gd->start_addr_sp, gd->new_gd, gd->relocaddr);
779#endif
780
781 return 0;
782}
783#endif
784
785/* Record the board_init_f() bootstage (after arch_cpu_init()) */
주요 구문 해설
static int jump_to_copy(void)pre-relocation initcall의 마지막 비복귀 경계를 C 코드에서 정의한다. skip-reloc flag가 아니면 새 gd와 stack을 architecture trampoline에 넘긴다.
arch_setup_gd(gd->new_gd);x86의 FS/GDT 기반 gd access가 임시 gd가 아니라 DRAM의 새 global data를 가리키도록 바꾼다.
board_init_f_r_trampoline64(gd->new_gd, gd->start_addr_sp);SysV x86-64 ABI로 RDI=new gd, RSI=new stack을 넘긴다. trampoline은 RSP를 바꾼 뒤 CAR 수명을 종료한다.
DETAILS
내부 동작을 더 깊게 읽기
진입 조건을 먼저 고정한다
board_init_f reservation에서 들어온 실행은 board_init_f의 마지막 x86 initcall과 비복귀 trampoline에 놓여 있다. 이때 interrupt, MMU/cache, stack, heap 중 무엇이 이미 준비되었는지 소스의 호출자까지 올라가 확인한다. 같은 함수라도 SPL, relocation 전후, app thread처럼 호출 문맥이 달라지면 허용되는 API와 지연 시간이 달라진다.
ARM처럼 board_init_f가 돌아온 뒤 assembly relocate_code를 찾으면 안 된다. x86는 C initcall 목록에서 copy, fixup, BSS clear와 jump를 완료하며 trampoline은 새 stack을 활성화한 뒤에만 CAR를 해제한다.
중심 객체의 생성과 공개를 나눈다
이 글의 중심 객체는 relocated x86 image / new stack / new gd다. 메모리를 확보한 시점, 필드를 채운 시점, 전역 list나 다른 subsystem에 공개한 시점을 구분한다. 공개 뒤 오류가 발생한다면 목록에서 제거하고 child, buffer, reference를 역순으로 정리하는지 확인한다.
빌드 산출물 관점에서는 DRAM의 relocated U-Boot proper 안에 해당 symbol과 section이 실제로 포함되었는지도 map과 objdump로 검증한다.
주소, 크기와 정렬을 계산한다
부트 코드에서는 주소 계산 오류를 주의해야 한다. 원본과 목적지의 주소 범위, 헤더에 기록된 데이터 크기, 블록·페이지 단위를 표로 정리하고 각 구간의 끝 주소를 계산한다. 끝 주소가 start + size - 1처럼 마지막 바이트를 가리키는지, 구간 바로 다음 주소를 가리키는지는 API 규약에서 확인한다.
성공 flag와 실제 완료 시점을 맞춘다
copy_uboot_to_ram → ELF relocation fixups → clear BSS → jump_to_copy → new DRAM stack → car_uninit 구간에서는 부분 초기화 상태가 생길 수 있다. flag, list insertion, callback 등록, storage write 완료 중 무엇이 성공의 기준인지 찾는다. hardware write나 DMA가 포함되면 함수 반환과 장치 완료가 같은 시점인지도 확인한다.
다른 CPU, 인터럽트 처리기, USB 완료 콜백, 셸 명령에서도 이 상태를 읽는다면 메모리 접근 순서와 잠금 범위를 함께 확인한다.
제어권을 넘기기 전에 확인할 조건을 정리한다
정상 경로는 board_init_f_r에서 끝난다. 이 단계가 함수 반환인지, scheduler 전환인지, 다른 image로의 비복귀 분기인지 구분한다. 비복귀 handoff라면 cache clean/invalidate, interrupt disable, 장치 quiesce, argument register와 FDT 또는 image address가 최종 점검 항목이다.
호출한 함수로 돌아오는 경로라면 오류와 일부 처리만 완료된 상태를 구분하는지 확인한다. 이어서 다음 후보를 시도할지, 복구 절차를 실행할지 살펴본다.
IMPLEMENTATION NOTES
구현을 읽을 때 놓치기 쉬운 부분
x86 relocation은 board_init_f의 C initcall 목록 안에서 끝난다
common/board_f.c의 x86/ARC 조건부 목록은 copy_uboot_to_ram(), do_elf_reloc_fixups(), clear_bss(), jump_to_copy()를 차례로 실행한다. ARM의 assembly relocation 경로를 찾는 대신, 분석 대상 .config로 전처리한 initcall 목록에서 이 함수들이 실제로 남는지 확인해야 한다.
jump_to_copy()는 gd->start_addr_sp, 새 gd, gd->relocaddr를 architecture trampoline에 넘긴다. 그 순간부터 old CAR stack과 ROM/flash copy는 정상 runtime의 소유자가 아니다.
copy_uboot_to_ram은 LMA에서 relocaddr로 image byte를 옮긴다
source는 현재 실행 image의 linker/runtime 위치이고 destination은 board_init_f reservation 단계에서 정한 gd->relocaddr다. copy 범위는 text만이 아니라 relocation 전 image의 실제 size와 section 경계를 따라야 한다. source와 destination overlap 여부도 방향과 memcpy 계약에 영향을 준다.
copy 직후 destination byte가 같다는 것만으로 실행 가능하지 않다. absolute pointer와 relocation entry, BSS, stack, gd, GDT가 새 주소 체계에 맞아야 한다.
ELF relocation entry는 복사된 image 안의 pointer를 새 base로 고친다
do_elf_reloc_fixups64()는 dynamic relocation table을 순회해 relocation type과 addend를 읽고 destination image의 target word를 갱신한다. text byte copy와 pointer fixup을 분리해 봐야 '코드는 옮겼는데 왜 old address를 읽는가'를 설명할 수 있다.
gd->reloc_off는 무조건 모든 pointer에 더하는 값이 아니다. relocation table이 지정한 대상과 type 계약만 수정해야 하며, runtime에 이미 새 address로 생성된 pointer를 다시 보정하면 이중 relocation이 된다.
BSS는 복사하는 구간이 아니라 새 runtime에서 0으로 만드는 구간이다
BSS는 file image에 payload byte가 없으므로 copy source에서 가져올 내용이 없다. relocation destination의 __bss_start부터 __bss_end를 clear해야 C의 zero-initialization 계약이 성립한다. old image의 BSS를 복사하는 방식으로 이해하면 size 계산과 persistent garbage 문제를 놓친다.
BSS clear가 relocation fixup 대상이나 새 stack/gd를 덮지 않도록 linker symbol과 reservation map을 대조한다. 특히 linker script 변경 뒤에는 __bss_start, __bss_end, relocation table 순서를 다시 확인한다.
32-bit trampoline과 64-bit trampoline의 argument register가 다르다
32-bit board_init_f_r_trampoline은 %eax의 new stack top을 %esp로 옮기고 car_uninit() 후 board_init_f_r()을 호출한다. 64-bit board_init_f_r_trampoline64는 %rsi를 %rsp로 사용하고 %rdi에 new gd가 들어온다. source 주석의 %eax 문구만 믿지 말고 실제 instruction과 caller ABI를 기준으로 읽어야 한다.
새 stack을 선택하기 전에 old stack의 return address에 의존하는 일반 return을 해서는 안 된다. trampoline은 비복귀 경계이며 이후 backtrace의 바닥도 여기에서 끊긴다.
CAR uninit은 새 DRAM stack으로 바꾼 뒤에 해야 한다
CAR가 현재 stack을 담고 있는데 먼저 car_uninit()을 호출하면 함수 call 자체의 return address와 local data가 사라진다. 그래서 trampoline은 %esp를 새 DRAM stack으로 옮긴 다음 CAR를 해제한다. 이 두 줄의 순서는 x86 relocation에서 가장 직접적인 lifetime 경계다.
CAR 해제 뒤 old gd, early malloc object, FSP temporary pointer 중 무엇을 계속 참조하는지 점검한다. 필요한 metadata는 HOB list처럼 DRAM 수명이 보장되는 영역으로 이전되어야 한다.
relocation 완료 증거는 PC 하나가 아니라 다섯 주소의 일치다
board_init_f_r 진입에서 PC가 gd->relocaddr range에 있고 SP가 gd->start_addr_sp 아래에 있으며 gd가 new DRAM copy를 가리켜야 한다. GDT/FS base, BSS zero, relocation target pointer까지 합쳐야 정상 runtime을 증명할 수 있다.
GDB script로 PC, SP, gd, __bss_start, relocation table start/end를 한 번에 출력하고 old CAR range를 poison하거나 접근 금지로 바꾸면 stale reference를 찾기 쉽다.
reloc_off를 source와 destination의 차이로 직접 계산한다
source image base가 0xfff00000, destination gd->relocaddr=0x7f000000이면 signed 차이는 reloc_off = 0x7f000000 - 0xfff00000 = -0x80f00000이다. symbol board_init_r의 old runtime address가 0xfff48210이면 새 target은 0x7f048210이다. unsigned 32-bit 출력만 보면 wrap된 큰 양수처럼 보이므로 pointer width와 signed 표현을 함께 적는다.
같은 offset을 PC, linker symbol, relocation entry target에 무조건 더하면 안 된다. 코드 분기 target 계산, ELF relocation type, 이미 destination 기준으로 생성한 runtime pointer의 소유자를 구분한다.
R_X86_64_RELATIVE 한 건을 addend부터 새 memory word까지 계산한다
link base가 0xfff00000, destination base가 0x7f000000이면 gd->reloc_off=-0x80f00000이다. relocation offset 0xfff5a120의 patch 주소는 0x7f05a120이고, addend가 link-time address 0xfff31f40이면 기록할 64-bit 값은 addend + reloc_off = 0x7f031f40이다. readelf -rW u-boot의 offset/type/addend와 fixup 전후 word를 이 식으로 대조한다.
symbol lookup이 필요 없는 RELATIVE와 symbol value를 소비하는 다른 relocation을 같은 식으로 처리하면 안 된다. unsupported type을 만났을 때 로그와 return path가 무엇인지도 함께 확인한다.
CAR를 끄기 직전 old/new 객체의 소유권 표를 만든다
PC는 destination image, SP와 gd는 DRAM, GDT/FS base도 DRAM, BSS는 zero, HOB/FDT pointer는 보존 영역을 가리켜야 한다. 반대로 old CAR stack frame, early malloc object와 source image의 temporary pointer는 이 지점에서 만료된다.
GDB에서 old CAR range에 hardware watchpoint를 걸거나 emulator에서 접근 금지로 바꾼 뒤 board_init_f_r까지 진행한다. stale read가 잡히면 단순 pointer 값이 아니라 그 pointer를 누가 언제 만들었는지 producer까지 거슬러 올라간다.
OBJECT LIFETIME
객체와 수명
| 대상 | 만들어지는 시점 | 유효 범위 | 확인할 조건 |
|---|---|---|---|
relocated x86 image / new stack / new gd | copy_uboot_to_ram | board_init_f_r 또는 오류 정리 완료까지 | 진입 전제와 상태 계약 |
| 입력 buffer / metadata | board_init_f reservation | parse와 검증이 끝날 때까지 | 길이, 정렬, 소유권, 변조 가능성 |
| 등록된 list / descriptor | ELF relocation fixups | 하위 시스템을 종료하거나 다음 이미지에 제어권을 넘길 때까지 | 중복 등록, 해제된 객체를 가리키는 포인터, 오류 후 자원 정리 |
| hardware 또는 persistent state | 실제 write/probe가 완료된 뒤 | reset 또는 명시적 해제·갱신까지 | 쓰기 완료, 시간 초과, 전원 차단, 이전 상태 복원 |
DRAM의 relocated U-Boot proper | link/image 생성 시점 | 다음 stage가 새 image로 교체할 때까지 | 적재 주소, 진입점, 섹션, 헤더의 일치 여부 |
FAILURE PATH
실패 지점과 증상
| # | 조건 | 관찰되는 증상 | 먼저 볼 단계 |
|---|---|---|---|
| 01 | copy 범위 오류 | destination text/data 일부 손상 | copy_uboot_to_ram |
| 02 | relocation type 또는 offset 오류 | old image pointer 참조 | ELF relocation fixups |
| 03 | BSS clear 범위 오류 | new gd/stack 덮음 | clear BSS |
| 04 | CAR 조기 해제 | return address와 local frame 소실 | jump_to_copy |
로그가 끊긴 함수뿐 아니라 그 함수에 주소, 크기, 플래그를 전달한 앞 단계도 조사한다. 오류 처리 후 등록된 객체나 하드웨어 설정이 그대로 남아 있는지도 확인한다.
EVIDENCE
소스 밖에서 확인할 증거
소스에서 파악한 동작을 실제 빌드 결과와 대상 보드의 로그로 확인한다. 아래 명령의 보드 이름과 툴체인 접두사는 사용 중인 빌드 환경에 맞게 바꾼다.
| # | 목적 | 명령 또는 계측 | 판정 기준 |
|---|---|---|---|
| 01 | 전처리 결과 | make <board>_defconfig && make V=1 | 선택된 CONFIG와 실제 compile/link 명령을 보관한다. |
| 02 | symbol과 section | ${CROSS_COMPILE}nm -n u-boot | grep 'board_init_f_r_trampoline' | symbol 주소가 기대한 stage와 image에 들어갔는지 확인한다. |
| 03 | 어셈블리 대조 | ${CROSS_COMPILE}objdump -drS u-boot | C 한 줄이 실제 branch, relocation, memory access로 어떻게 바뀌었는지 본다. |
| 04 | 주소 배치 | bdinfo | relocaddr, malloc, FDT, stack과 DRAM bank 주소를 한 번에 기록한다. |
| 05 | runtime 관찰 | dm tree; dm uclass; printenv | driver model과 환경이 이 단계에서 실제로 공개되었는지 확인한다. |
LAB
직접 확인할 실험
- 01readelf -rW와 relocation target 몇 개를 손계산
copy_uboot_to_ram진입 전후에 source image의 주소·크기·반환값과 timestamp를 함께 남긴다. - 02copy 전후 image hash 비교
ELF relocation fixups진입 전후에 gd->relocaddr의 주소·크기·반환값과 timestamp를 함께 남긴다. - 03board_init_f_r 진입에서 PC/SP/gd/FS base 동시 기록
clear BSS진입 전후에 .rela.dyn의 주소·크기·반환값과 timestamp를 함께 남긴다. - 04CAR를 uninit 직후 poison해 stale pointer 탐지
jump_to_copy진입 전후에 new BSS의 주소·크기·반환값과 timestamp를 함께 남긴다.
PRIMARY REFERENCES