QUESTION
cache와 DRAM을 믿을 수 없는 x86 CPU가 어떤 memory를 stack과 gd로 사용해 첫 C 초기화에 도달하는가?
x86는 DRAM 전 stack을 Cache-As-RAM 또는 platform temporary RAM에 둔다. _x86boot_start와 _start, TPL/SPL/U-Boot proper의 실제 선택을 먼저 고정하고 CAR top에서 stack, gd와 malloc_f가 차지하는 주소를 계산한다.
board_init_f에 들어갈 때 writable stack, 유효한 FS 기반 gd pointer, 보존된 BIST/table/FDT metadata가 CAR 범위 안에서 서로 겹치지 않아야 한다. 이 문장을 기준으로 코드를 위에서 아래로 읽으면, 함수 이름을 외우는 대신 어느 시점에 어떤 상태를 신뢰할 수 있는지 판단할 수 있다.
STRUCTURE
객체와 주소가 놓이는 구조
CAR stack / gd_t / x86 GDT를 중심에 놓고 왼쪽의 입력이 어떤 검사를 거쳐 오른쪽 결과로 공개되는지 표시했다. 실제 디버깅에서는 각 블록의 주소와 크기를 로그에 대입한다.
CALL PATH
실행 흐름
화살표는 단순 호출 순서만 뜻하지 않는다. 각 단계가 성공을 반환할 때 다음 단계가 읽을 수 있는 상태가 무엇인지 함께 확인한다. 오류 반환이 발생하면 바로 다음 화살표로 진행하지 않고 해당 단계의 정리 경로를 따라간다.
STATE LEDGER
단계별 입력과 출력
각 단계의 입력과 처리 후 바뀌는 상태를 정리한 표다. 사용 중인 소스 버전에서 다른 필드를 변경한다면 그에 맞춰 표도 수정한다.
| # | 단계 | 진입 시 신뢰할 상태 | 성공 뒤 남아야 할 상태 | 다음 소비자 |
|---|---|---|---|---|
| 01 | cold reset or previous loader | DRAM과 정상 exception handler가 없는 32-bit 초기 문맥 | BIST and table pointer | 32-bit protected entry |
| 02 | 32-bit protected entry | cold reset or previous loader 완료 상태 | segment registers | GDT and blank IDT |
| 03 | GDT and blank IDT | 32-bit protected entry 완료 상태 | Cache-As-RAM range | car_init |
| 04 | car_init | GDT and blank IDT 완료 상태 | temporary stack | CAR stack |
| 05 | CAR stack | car_init 완료 상태 | global_data | gd and malloc_f |
| 06 | gd and malloc_f | CAR stack 완료 상태 | pre-relocation init | board_init_f |
| 07 | board_init_f | gd and malloc_f 완료 상태 | pre-relocation init | 최종 x86 U-Boot pre-relocation image 또는 다음 stage |
분석 범위의 전제와 비교해, 다음 단계에 잘못된 입력을 넘긴 부분부터 조사한다.
VISUAL WALKTHROUGH
주소와 객체의 이동을 그림으로 읽기
아래 그림은 주소, 객체를 사용하는 기간, 제어권을 넘기는 시점을 중심으로 정리했다. 실제 주소와 크기는 사용 중인 보드의 bdinfo, 맵 파일과 링커 심벌로 확인한다.
크기는 개념 비율이다. 실제 분석에서는 map, symbol과 runtime 출력의 시작·끝 주소를 대입한다.
두 열의 객체는 이름이 비슷해도 생성 시점, 사용 가능한 API와 해제 책임이 다르다.
Cold reset `_start`
01start16에서 protected mode 진입
02EAX의 BIST 보존
03ECX table pointer
04GDT/segment 재확인
05car_init으로 이동
Fail-safe `_x86boot_start`
01coreboot/EFI 등 previous loader
02BIST는 0으로 간주
03cache 상속 상태 제거
0432-bit bootstrap 재정리
05label 1에서 공통 경로
호출 이름만 나열하지 않고, 각 단계가 다음 단계에 넘기는 상태를 기준으로 실행 순서를 표시한다.
UPSTREAM SOURCE
원본 코드
아래 코드는 U-Boot v2026.07의 arch/x86/cpu/start.S에서 29-174줄을 그대로 가져온 것이다. 설명을 위해 실제 코드를 가짜 의사 코드로 바꾸지 않았다.
29#include <generated/asm-offsets.h>
30#include <linux/linkage.h>
31
32.section .text.start
33.code32
34.globl _start
35.type _start, @function
36.globl _x86boot_start
37_x86boot_start:
38 /*
39 * This is the fail-safe 32-bit bootstrap entry point.
40 *
41 * This code is used when booting from another boot loader like
42 * coreboot or EFI. So we repeat some of the same init found in
43 * start16.
44 */
45 cli
46 cld
47
48 /* Turn off cache (this might require a 486-class CPU) */
49 movl %cr0, %eax
50 orl $(X86_CR0_NW | X86_CR0_CD), %eax
51 movl %eax, %cr0
52 wbinvd
53
54 /*
55 * Zero the BIST (Built-In Self Test) value since we don't have it.
56 * It must be 0 or the previous loader would have reported an error.
57 */
58 movl $0, %ebp
59
60 jmp 1f
61
62 /* Add a way for tools to discover the _start entry point */
63 .align 4
64 .long 0x12345678
65_start:
66 /* This is the 32-bit cold-reset entry point, coming from start16 */
67
68 /* Save BIST */
69 movl %eax, %ebp
701:
71
72 /* Save table pointer */
73 movl %ecx, %esi
74
75#ifdef CONFIG_X86_LOAD_FROM_32_BIT
76 lgdt gdt_ptr2
77#endif
78
79 /* Load the segment registers to match the GDT loaded in start16.S */
80 movl $(X86_GDT_ENTRY_32BIT_DS * X86_GDT_ENTRY_SIZE), %eax
81 movw %ax, %fs
82 movw %ax, %ds
83 movw %ax, %gs
84 movw %ax, %es
85 movw %ax, %ss
86
87 /* Clear the interrupt vectors */
88 lidt blank_idt_ptr
89
90#ifdef CONFIG_USE_EARLY_BOARD_INIT
91 /*
92 * Critical early platform init - generally not used, we prefer init
93 * to happen later when we have a console, in case something goes
94 * wrong.
95 */
96 jmp early_board_init
97.globl early_board_init_ret
98early_board_init_ret:
99#endif
100
101 post_code(POST_START)
102
103 /* Initialise Cache-As-RAM */
104 jmp car_init
105.globl car_init_ret
106car_init_ret:
107#ifdef CONFIG_USE_CAR
108 /*
109 * We now have CONFIG_SYS_CAR_SIZE bytes of Cache-As-RAM (or SRAM,
110 * or fully initialised SDRAM - we really don't care which)
111 * starting at CONFIG_SYS_CAR_ADDR to be used as a temporary stack
112 * and early malloc() area. The MRC requires some space at the top.
113 *
114 * Stack grows down from top of CAR. We have:
115 *
116 * top-> CONFIG_SYS_CAR_ADDR + CONFIG_SYS_CAR_SIZE
117 * MRC area
118 * global_data with x86 global descriptor table
119 * early malloc area
120 * stack
121 * bottom-> CONFIG_SYS_CAR_ADDR
122 */
123 movl $(CONFIG_SYS_CAR_ADDR + CONFIG_SYS_CAR_SIZE - 4), %esp
124#ifdef CONFIG_DCACHE_RAM_MRC_VAR_SIZE
125 subl $CONFIG_DCACHE_RAM_MRC_VAR_SIZE, %esp
126#endif
127#else
128 /*
129 * Instructions for FSP1, but not FSP2:
130 * U-Boot enters here twice. For the first time it comes from
131 * car_init_done() with esp points to a temporary stack and esi
132 * set to zero. For the second time it comes from fsp_init_done()
133 * with esi holding the HOB list address returned by the FSP.
134 */
135#endif
136 /* Set up global data */
137 mov %esp, %eax
138 call board_init_f_alloc_reserve
139 mov %eax, %esp
140 call board_init_f_init_reserve
141
142#ifdef CONFIG_DEBUG_UART
143 call debug_uart_init
144#endif
145
146 /* Get address of global_data */
147 mov %fs:0, %edx
148#if defined(CONFIG_USE_HOB) && !defined(CONFIG_USE_CAR)
149 /* Store the HOB list if we have one */
150 test %esi, %esi
151 jz skip_hob
152 movl %esi, GD_HOB_LIST(%edx)
153
154#ifdef CONFIG_HAVE_FSP
155 /*
156 * After fsp_init() returns, the stack has already been switched to a
157 * place within system memory as defined by CONFIG_FSP_TEMP_RAM_ADDR.
158 * Enlarge the size of malloc() pool before relocation since we have
159 * plenty of memory now.
160 */
161 subl $CONFIG_FSP_SYS_MALLOC_F_LEN, %esp
162 movl %esp, GD_MALLOC_BASE(%edx)
163#endif
164skip_hob:
165#else
166 /* Store table pointer */
167 movl %esi, GD_TABLE(%edx)
168#endif
169 /* Store BIST */
170 movl %ebp, GD_BIST(%edx)
171
172 /* Set parameter to board_init_f() to boot flags */
173 post_code(POST_START_DONE)
174 xorl %eax, %eax
READING CONTRACT
발췌 범위와 읽기 기준
원문 29-174줄에는 앞뒤 문맥이 포함됩니다. 기준 구문 _start:은 L37에 있습니다. 발췌 첫 줄을 함수 정의의 시작으로 해석하지 않습니다.
x86는 DRAM 전 stack을 Cache-As-RAM 또는 platform temporary RAM에 둔다. _x86boot_start와 _start, TPL/SPL/U-Boot proper의 실제 선택을 먼저 고정하고 CAR top에서 stack, gd와 malloc_f가 차지하는 주소를 계산한다.
_start:16-bit cold-reset path가 32-bit protected mode를 만든 뒤 도착하는 고정 entry다. fail-safe _x86boot_start와 label 1에서 합쳐진다.
movl %eax, %ebpcold reset 경로에서 firmware/CPU BIST 결과를 callee-saved EBP에 보존한다.
movl %ecx, %esicoreboot table 또는 firmware가 넘긴 table pointer를 board_init_f 전까지 보존한다.
lidt blank_idt_ptrlimit/base가 0인 IDT를 적재한다. 정상 handler를 준비한 것이 아니라 예기치 않은 interrupt를 차단하는 초기 상태다.
jmp car_initplatform별 Cache-As-RAM 또는 temporary RAM 생산자로 비복귀 진입한다.
movl $(CONFIG_SYS_CAR_ADDR + CONFIG_SYS_CAR_SIZE - 4), %espCAR top에서 32-bit stack을 아래 방향으로 시작한다.
call board_init_f_alloc_reserve현재 stack cursor 아래에 gd와 malloc_f 등 early runtime 영역을 예약한다.
mov %fs:0, %edxx86 segment 기반 global-data pointer를 읽는다. FS selector/GDT가 이 주소 계산의 일부다.
RELATED SOURCE
함께 읽어야 하는 원본 코드
첫 코드 조각만으로 동작이 완성되지 않는 경우 호출 매크로, 객체 정의와 실제 실행 목록을 같은 페이지에 묶었다. 각 조각은 같은 기준 commit에서 가져왔다.
01. 64-bit loader가 기존 stack에서 직접 시작하는 entry
arch/x86/cpu/start64.S 7-49줄이다.
7 */
8
9#include <config.h>
10
11.section .text.start
12.code64
13.globl _start
14.type _start, @function
15_start:
16 /* Set up memory using the existing stack */
17 mov %rsp, %rdi
18 call board_init_f_alloc_reserve
19
20 mov %rax, %rsp
21 mov %rsp, %rdi
22
23 call board_init_f_init_reserve
24
25 xor %rdi, %rdi
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
주요 구문 해설
_start:16-bit cold-reset path가 32-bit protected mode를 만든 뒤 도착하는 고정 entry다. fail-safe _x86boot_start와 label 1에서 합쳐진다.
call board_init_f_alloc_reserve현재 stack cursor 아래에 gd와 malloc_f 등 early runtime 영역을 예약한다.
call board_init_fDRAM 발견, top-down reservation과 x86 copy/fixup initcall을 실행한다. 정상 경로는 이 call로 돌아오지 않는다.
02. CAR top에서 gd와 malloc_f를 예약하는 공통 helper
common/init/board_init.c 74-135줄이다.
74 * alignment constraint of the architecture for which we are bulding.
75 *
76 * - GD is allocated last, so that the return value of this functions is
77 * both the bottom of the reserved area and the address of GD, should
78 * the calling context need it.
79 */
80
81ulong board_init_f_alloc_reserve(ulong top)
82{
83 /* Reserve early malloc arena */
84#ifndef CFG_MALLOC_F_ADDR
85#if CONFIG_IS_ENABLED(SYS_MALLOC_F)
86 top -= CONFIG_VAL(SYS_MALLOC_F_LEN);
87#endif
88#endif
89 /* LAST : reserve GD (rounded up to a multiple of 16 bytes) */
90 top = rounddown(top-sizeof(struct global_data), 16);
91
92 return top;
93}
94
95/*
96 * Initialize reserved space (which has been safely allocated on the C
97 * stack from the C runtime environment handling code).
98 *
99 * Notes:
100 *
101 * Actual reservation was done by the caller; the locations from base
102 * to base+size-1 (where 'size' is the value returned by the allocation
103 * function above) can be accessed freely without risk of corrupting the
104 * C runtime environment.
105 *
106 * IMPORTANT:
107 *
108 * Upon return from the allocation function above, on some architectures
109 * the caller will set gd to the lowest reserved location. Therefore, in
110 * this initialization function, the global data MUST be placed at base.
111 *
112 * ALSO IMPORTANT:
113 *
114 * On some architectures, gd will already be good when entering this
115 * function. On others, it will only be good once arch_setup_gd() returns.
116 * Therefore, global data accesses must be done:
117 *
118 * - through gd_ptr if before the call to arch_setup_gd();
119 *
120 * - through gd once arch_setup_gd() has been called.
121 *
122 * Do not use 'gd->' until arch_setup_gd() has been called!
123 *
124 * IMPORTANT TOO:
125 *
126 * Initialization for each "chunk" (GD, early malloc arena...) ends with
127 * an incrementation line of the form 'base += <some size>'. The last of
128 * these incrementations seems useless, as base will not be used any
129 * more after this incrementation; but if/when a new "chunk" is appended,
130 * this increment will be essential as it will give base right value for
131 * this new chunk (which will have to end with its own incrementation
132 * statement). Besides, the compiler's optimizer will silently detect
133 * and remove the last base incrementation, therefore leaving that last
134 * (seemingly useless) incrementation causes no code increase.
135 */
03. x86이 FS segment에 gd pointer를 연결하는 초기화
arch/x86/cpu/i386/cpu.c 118-167줄이다.
118
119 gdt.len = (num_entries * X86_GDT_ENTRY_SIZE) - 1;
120 gdt.ptr = (ulong)boot_gdt;
121
122 asm volatile("lgdtl %0\n" : : "m" (gdt));
123}
124
125void arch_setup_gd(gd_t *new_gd)
126{
127 u64 *gdt_addr;
128
129 gdt_addr = new_gd->arch.gdt;
130
131 /*
132 * CS: code, read/execute, 4 GB, base 0
133 *
134 * Some OS (like VxWorks) requires GDT entry 1 to be the 32-bit CS
135 */
136 gdt_addr[X86_GDT_ENTRY_UNUSED] = GDT_ENTRY(0xc09b, 0, 0xfffff);
137 gdt_addr[X86_GDT_ENTRY_32BIT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff);
138
139 /* DS: data, read/write, 4 GB, base 0 */
140 gdt_addr[X86_GDT_ENTRY_32BIT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff);
141
142 /*
143 * FS: data, read/write, sizeof (Global Data Pointer),
144 * base (Global Data Pointer)
145 */
146 new_gd->arch.gd_addr = new_gd;
147 gdt_addr[X86_GDT_ENTRY_32BIT_FS] = GDT_ENTRY(0x8093,
148 (ulong)&new_gd->arch.gd_addr,
149 sizeof(new_gd->arch.gd_addr) - 1);
150
151 /* 16-bit CS: code, read/execute, 64 kB, base 0 */
152 gdt_addr[X86_GDT_ENTRY_16BIT_CS] = GDT_ENTRY(0x009b, 0, 0x0ffff);
153
154 /* 16-bit DS: data, read/write, 64 kB, base 0 */
155 gdt_addr[X86_GDT_ENTRY_16BIT_DS] = GDT_ENTRY(0x0093, 0, 0x0ffff);
156
157 gdt_addr[X86_GDT_ENTRY_16BIT_FLAT_CS] = GDT_ENTRY(0x809b, 0, 0xfffff);
158 gdt_addr[X86_GDT_ENTRY_16BIT_FLAT_DS] = GDT_ENTRY(0x8093, 0, 0xfffff);
159 gdt_addr[X86_GDT_ENTRY_64BIT_CS] = GDT_ENTRY(0xaf9b, 0, 0xfffff);
160 gdt_addr[X86_GDT_ENTRY_64BIT_TS1] = GDT_ENTRY(0x8980, 0, 0xfffff);
161 gdt_addr[X86_GDT_ENTRY_64BIT_TS2] = 0;
162
163 load_gdt(gdt_addr, X86_GDT_NUM_ENTRIES);
164 load_ds(X86_GDT_ENTRY_32BIT_DS);
165 load_es(X86_GDT_ENTRY_32BIT_DS);
166 load_gs(X86_GDT_ENTRY_32BIT_DS);
167 load_ss(X86_GDT_ENTRY_32BIT_DS);
DETAILS
내부 동작을 더 깊게 읽기
진입 조건을 먼저 고정한다
cold reset or previous loader에서 들어온 실행은 DRAM과 정상 exception handler가 없는 32-bit 초기 문맥에 놓여 있다. 이때 interrupt, MMU/cache, stack, heap 중 무엇이 이미 준비되었는지 소스의 호출자까지 올라가 확인한다. 같은 함수라도 SPL, relocation 전후, app thread처럼 호출 문맥이 달라지면 허용되는 API와 지연 시간이 달라진다.
x86는 DRAM 전 stack을 Cache-As-RAM 또는 platform temporary RAM에 둔다. _x86boot_start와 _start, TPL/SPL/U-Boot proper의 실제 선택을 먼저 고정하고 CAR top에서 stack, gd와 malloc_f가 차지하는 주소를 계산한다.
중심 객체의 생성과 공개를 나눈다
이 글의 중심 객체는 CAR stack / gd_t / x86 GDT다. 메모리를 확보한 시점, 필드를 채운 시점, 전역 list나 다른 subsystem에 공개한 시점을 구분한다. 공개 뒤 오류가 발생한다면 목록에서 제거하고 child, buffer, reference를 역순으로 정리하는지 확인한다.
빌드 산출물 관점에서는 x86 U-Boot pre-relocation image 안에 해당 symbol과 section이 실제로 포함되었는지도 map과 objdump로 검증한다.
주소, 크기와 정렬을 계산한다
부트 코드에서는 주소 계산 오류를 주의해야 한다. 원본과 목적지의 주소 범위, 헤더에 기록된 데이터 크기, 블록·페이지 단위를 표로 정리하고 각 구간의 끝 주소를 계산한다. 끝 주소가 start + size - 1처럼 마지막 바이트를 가리키는지, 구간 바로 다음 주소를 가리키는지는 API 규약에서 확인한다.
성공 flag와 실제 완료 시점을 맞춘다
32-bit protected entry → GDT and blank IDT → car_init → CAR stack → gd and malloc_f 구간에서는 부분 초기화 상태가 생길 수 있다. flag, list insertion, callback 등록, storage write 완료 중 무엇이 성공의 기준인지 찾는다. hardware write나 DMA가 포함되면 함수 반환과 장치 완료가 같은 시점인지도 확인한다.
다른 CPU, 인터럽트 처리기, USB 완료 콜백, 셸 명령에서도 이 상태를 읽는다면 메모리 접근 순서와 잠금 범위를 함께 확인한다.
제어권을 넘기기 전에 확인할 조건을 정리한다
정상 경로는 board_init_f에서 끝난다. 이 단계가 함수 반환인지, scheduler 전환인지, 다른 image로의 비복귀 분기인지 구분한다. 비복귀 handoff라면 cache clean/invalidate, interrupt disable, 장치 quiesce, argument register와 FDT 또는 image address가 최종 점검 항목이다.
호출한 함수로 돌아오는 경로라면 오류와 일부 처리만 완료된 상태를 구분하는지 확인한다. 이어서 다음 후보를 시도할지, 복구 절차를 실행할지 살펴본다.
IMPLEMENTATION NOTES
구현을 읽을 때 놓치기 쉬운 부분
x86의 첫 entry는 하나가 아니라 cold reset, fail-safe 32-bit, SPL/TPL 경로로 갈린다
arch/x86/cpu/start.S의 _x86boot_start는 coreboot나 EFI처럼 다른 loader가 이미 32-bit 환경을 만든 경우를 위한 fail-safe entry이고, _start는 start16에서 넘어오는 cold-reset 경로다. 두 경로는 label 1에서 합쳐지지만 BIST와 table pointer의 출처가 다르다. 현재 board가 TPL, SPL, U-Boot proper 중 어느 stage에 이 파일을 쓰는지도 source 상단 주석과 최종 map에서 확인해야 한다.
64-bit loader가 arch/x86/cpu/start64.S로 직접 들어오는 설정은 또 다른 계약이다. 그 경로는 기존 %rsp가 유효하다고 전제하고 CAR 초기화 없이 board_init_f_alloc_reserve()부터 시작한다. 함수 이름이 같아도 진입 mode와 stack 생산자가 다르므로 한 개의 reset 흐름도로 합치면 안 된다.
cache를 끄는 동작과 Cache-As-RAM을 만드는 동작은 모순이 아니다
fail-safe entry는 CR0.NW/CD를 세우고 wbinvd로 이전 loader의 cache state를 정리한다. 이후 car_init은 platform별 MTRR, cache way 또는 FSP temporary RAM 계약으로 특정 physical range를 stack과 early malloc에 쓸 수 있게 만든다. 앞의 cache disable은 불명확한 상속 상태 제거이고 뒤의 CAR은 제어된 임시 memory 생성이다.
QEMU, coreboot, Intel FSP, Slim Bootloader는 car_init 구현이 서로 다르다. start.S만 읽고 cache line이 실제 DRAM처럼 write-back되는 과정을 추정하지 말고 link된 arch/x86/lib/*/car.S와 호출 후 %esp가 가리키는 range를 함께 확인한다.
blank IDT는 예외를 처리하는 표가 아니라 예외가 나면 즉시 멈추게 하는 상태다
lidt blank_idt_ptr는 limit와 base가 0인 IDT를 적재한다. 아직 정상 handler와 stack frame을 만들 수 없는 시점에 잘못된 interrupt vector를 따라가는 것을 막지만, #PF나 #GP가 발생하면 double/triple fault와 reset로 이어질 수 있다. 따라서 이 구간의 무출력 reset은 예외가 없었다는 증거가 아니다.
초기 디버깅에서는 serial보다 POST code, hardware debugger, QEMU -d int,cpu_reset이 더 직접적이다. _start, car_init_ret, board_init_f 직전에 breakpoint를 두고 CR0, GDTR, IDTR, ESP를 기록한다.
CAR top에서 stack, MRC area, gd와 early malloc의 실제 배치를 계산한다
CONFIG_SYS_CAR_ADDR + CONFIG_SYS_CAR_SIZE - 4가 첫 stack top이고, 필요하면 MRC variable 영역만큼 더 내린다. board_init_f_alloc_reserve()는 이 cursor에서 gd_t, 초기 GDT와 malloc_f 공간을 정렬해 예약하고 새 stack pointer를 반환한다. 단순히 CAR size가 크다고 안전한 것이 아니라 가장 깊은 call chain과 early allocation의 합이 bottom을 넘지 않아야 한다.
map에 표시된 sizeof(gd_t)와 .config의 SYS_MALLOC_F_LEN, DCACHE_RAM_MRC_VAR_SIZE를 대입해 각 start/end를 표로 만들 수 있다. stack watermark 또는 poison pattern을 CAR bottom부터 채우면 실제 최대 사용량을 확인할 수 있다.
x86의 gd pointer는 ARM64 x18과 다르게 FS segment를 통해 읽힌다
board_init_f_init_reserve()가 초기 global data를 만든 뒤 x86 assembly는 %fs:0에서 gd 주소를 읽는다. segment selector와 GDT base가 틀리면 일반 pointer처럼 보이는 첫 gd access부터 잘못된 주소를 사용한다. 이 때문에 segment register load, GDT descriptor와 gd 초기화를 같은 경계로 봐야 한다.
HOB를 쓰는 FSP 경로에서는 %esi가 firmware hand-off block list를 보존하고, 일반 경로에서는 table pointer를 GD_TABLE에 저장한다. 같은 register가 build option에 따라 다른 metadata를 전달한다.
board_init_f가 돌아오면 정상 경로가 아니다
x86에서는 board_init_f()의 initcall 목록 안에 copy, ELF relocation fixup, BSS clear와 jump_to_copy()가 포함될 수 있다. 정상 경로는 board_init_f_r_trampoline으로 비복귀 이동한다. 따라서 call board_init_f 다음의 POST 0x85와 die는 성공 continuation이 아니라 예상하지 못한 return을 잡는 경계다.
ARM64의 crt0_64.S가 board_init_f() 반환 뒤 relocate_code를 호출하는 구조와 가장 크게 다른 지점이다. architecture별 initcall_run_f() 전처리 결과를 보지 않으면 relocation 책임을 잘못 배치하게 된다.
초기 latency는 CAR 준비와 DRAM training을 나눠 측정한다
CAR 생성은 platform register/FSP 호출에 따라 시간이 달라지고, DRAM training은 이후 board_init_f initcall에서 더 큰 지연을 만들 수 있다. reset부터 첫 console까지 한 구간으로만 재면 어느 단계가 느린지 알 수 없다.
POST code와 TSC를 _start, car_init_ret, dram_init, jump_to_copy, board_init_f_r에 기록해 구간별 upper bound를 만든다. cold boot, warm reset, training cache 사용 여부도 분리한다.
CAR 128KiB 예제로 stack, gd와 malloc_f의 경계를 직접 계산한다
CONFIG_SYS_CAR_ADDR=0xfef00000, CONFIG_SYS_CAR_SIZE=0x20000이면 첫 ESP는 0xfef1fffc다. 여기서 16-byte 정렬 뒤 gd_t=0x1c0, 초기 GDT=0x40, SYS_MALLOC_F_LEN=0x4000을 차례로 내리면 early runtime cursor는 대략 0xfef1be00 아래가 된다. MRC variable 0x4000을 CAR 상단에 따로 둔다면 그만큼을 stack top 계산 전에 먼저 빼야 한다.
남은 [CAR bottom, runtime cursor)가 call frame과 local variable의 실제 stack budget이다. 예제 값만 고정해서 쓰지 말고 최종 sizeof(struct global_data), Kconfig 값과 objdump의 stack usage를 대입한다. CAR를 0xa5로 채운 뒤 가장 낮게 바뀐 주소를 찾으면 정적 계산과 실제 high-water의 차이도 얻을 수 있다.
FS selector, descriptor base와 fs:0의 최종 주소를 한 식으로 검산한다
예를 들어 FS selector가 0x18이면 GDT index는 0x18 >> 3 = 3이다. index 3 descriptor의 base가 0xfef1be00이고 instruction이 %fs:0을 읽으면 linear address도 0xfef1be00이다. 그 위치에 저장된 word가 new gd pointer여야 C의 gd macro가 기대한 값을 얻는다.
selector의 RPL/TI bit, GDTR.base, descriptor base field, %fs:offset 네 값을 분리해 기록한다. %fs 숫자만 맞아도 descriptor가 old CAR/GDT copy를 가리키면 relocation 뒤 stale gd를 읽는다.
x86 early boot의 시간 상한은 stage별 최대값의 합으로 둔다
부팅 상한을 Tentry + Tcar + Tdram + Treloc + Thandoff로 나눈다. 3.0GHz TSC에서 CAR 구간이 9,000,000 cycle이면 3ms이고 DRAM training이 1,500,000,000 cycle이면 500ms다. console 출력 시각 하나만 보면 503ms 전부를 assembly startup 지연으로 오해할 수 있다.
각 checkpoint는 CPU frequency가 고정되기 전일 수 있으므로 invariant TSC 지원 여부를 확인하거나 platform timer와 교차 검증한다. cold/warm 각각 100회에서 maximum과 percentile을 따로 보관하면 training cache miss 같은 긴 tail을 찾을 수 있다.
OBJECT LIFETIME
객체와 수명
| 대상 | 만들어지는 시점 | 유효 범위 | 확인할 조건 |
|---|---|---|---|
CAR stack / gd_t / x86 GDT | 32-bit protected entry | board_init_f 또는 오류 정리 완료까지 | 진입 전제와 상태 계약 |
| 입력 buffer / metadata | cold reset or previous loader | parse와 검증이 끝날 때까지 | 길이, 정렬, 소유권, 변조 가능성 |
| 등록된 list / descriptor | GDT and blank IDT | 하위 시스템을 종료하거나 다음 이미지에 제어권을 넘길 때까지 | 중복 등록, 해제된 객체를 가리키는 포인터, 오류 후 자원 정리 |
| hardware 또는 persistent state | 실제 write/probe가 완료된 뒤 | reset 또는 명시적 해제·갱신까지 | 쓰기 완료, 시간 초과, 전원 차단, 이전 상태 복원 |
x86 U-Boot pre-relocation image | link/image 생성 시점 | 다음 stage가 새 image로 교체할 때까지 | 적재 주소, 진입점, 섹션, 헤더의 일치 여부 |
FAILURE PATH
실패 지점과 증상
| # | 조건 | 관찰되는 증상 | 먼저 볼 단계 |
|---|---|---|---|
| 01 | blank IDT 구간의 fault | triple fault와 즉시 reset | 32-bit protected entry |
| 02 | CAR 크기 부족 | stack과 gd 또는 malloc_f 충돌 | GDT and blank IDT |
| 03 | FS/GDT 오류 | 첫 gd access에서 잘못된 주소 | car_init |
| 04 | BIST/table pointer 훼손 | platform handoff metadata 소실 | CAR stack |
로그가 끊긴 함수뿐 아니라 그 함수에 주소, 크기, 플래그를 전달한 앞 단계도 조사한다. 오류 처리 후 등록된 객체나 하드웨어 설정이 그대로 남아 있는지도 확인한다.
EVIDENCE
소스 밖에서 확인할 증거
소스에서 파악한 동작을 실제 빌드 결과와 대상 보드의 로그로 확인한다. 아래 명령의 보드 이름과 툴체인 접두사는 사용 중인 빌드 환경에 맞게 바꾼다.
| # | 목적 | 명령 또는 계측 | 판정 기준 |
|---|---|---|---|
| 01 | 전처리 결과 | make <board>_defconfig && make V=1 | 선택된 CONFIG와 실제 compile/link 명령을 보관한다. |
| 02 | symbol과 section | ${CROSS_COMPILE}nm -n u-boot | grep '_start' | 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
직접 확인할 실험
- 01QEMU에서 _start, car_init_ret, board_init_f에 breakpoint
32-bit protected entry진입 전후에 BIST and table pointer의 주소·크기·반환값과 timestamp를 함께 남긴다. - 02CAR 전체를 pattern으로 채워 stack high-water 측정
GDT and blank IDT진입 전후에 segment registers의 주소·크기·반환값과 timestamp를 함께 남긴다. - 03GD 주소와 FS base/GDT descriptor 대조
car_init진입 전후에 Cache-As-RAM range의 주소·크기·반환값과 timestamp를 함께 남긴다. - 04POST code에 TSC를 붙여 CAR와 DRAM training 시간 분리
CAR stack진입 전후에 temporary stack의 주소·크기·반환값과 timestamp를 함께 남긴다.
PRIMARY REFERENCES