QUESTION
동시에 깨어난 RISC-V hart 중 하나만 global 초기화를 수행하고 나머지가 안전한 stack에서 기다리게 하는 장치는 무엇인가?
RISC-V에서는 여러 hart가 동시에 같은 entry에 도착할 수 있다. stack을 hart별로 분리하고 atomic lottery의 winner만 global 초기화를 실행하며, loser는 winner가 게시한 상태를 기다린다.
어느 hart가 winner가 되더라도 각 hart stack이 겹치지 않고, boot hart가 gd와 FDT를 완성한 뒤에만 secondary가 그 pointer를 관찰해야 한다. 이 문장을 기준으로 코드를 위에서 아래로 읽으면, 함수 이름을 외우는 대신 어느 시점에 어떤 상태를 신뢰할 수 있는지 판단할 수 있다.
STRUCTURE
객체와 주소가 놓이는 구조
per-hart stack / hart_lottery / gd_t를 중심에 놓고 왼쪽의 입력이 어떤 검사를 거쳐 오른쪽 결과로 공개되는지 표시했다. 실제 디버깅에서는 각 블록의 주소와 크기를 로그에 대입한다.
CALL PATH
실행 흐름
화살표는 단순 호출 순서만 뜻하지 않는다. 각 단계가 성공을 반환할 때 다음 단계가 읽을 수 있는 상태가 무엇인지 함께 확인한다. 오류 반환이 발생하면 바로 다음 화살표로 진행하지 않고 해당 단계의 정리 경로를 따라간다.
STATE LEDGER
단계별 입력과 출력
각 단계의 입력과 처리 후 바뀌는 상태를 정리한 표다. 사용 중인 소스 버전에서 다른 필드를 변경한다면 그에 맞춰 표도 수정한다.
| # | 단계 | 진입 시 신뢰할 상태 | 성공 뒤 남아야 할 상태 | 다음 소비자 |
|---|---|---|---|---|
| 01 | firmware or reset | MMU와 일반 scheduler가 없는 multi-hart 초기 문맥 | a0 hartid and a1 FDT | hartid and FDT preserve |
| 02 | hartid and FDT preserve | firmware or reset 완료 상태 | tp and s1 preserve | trap vector and interrupt mask |
| 03 | trap vector and interrupt mask | hartid and FDT preserve 완료 상태 | per-hart stack slots | per-hart stack |
| 04 | per-hart stack | trap vector and interrupt mask 완료 상태 | hart_lottery | hart lottery |
| 05 | hart lottery | per-hart stack 완료 상태 | gd publication | boot hart gd |
| 06 | boot hart gd | hart lottery 완료 상태 | secondary_hart_loop | secondary wait |
| 07 | secondary wait | boot hart gd 완료 상태 | secondary_hart_loop | board_init_f |
| 08 | board_init_f | secondary wait 완료 상태 | secondary_hart_loop | 최종 RISC-V U-Boot pre-relocation image 또는 다음 stage |
분석 범위의 전제와 비교해, 다음 단계에 잘못된 입력을 넘긴 부분부터 조사한다.
VISUAL WALKTHROUGH
주소와 객체의 이동을 그림으로 읽기
아래 그림은 주소, 객체를 사용하는 기간, 제어권을 넘기는 시점을 중심으로 정리했다. 실제 주소와 크기는 사용 중인 보드의 bdinfo, 맵 파일과 링커 심벌로 확인한다.
크기는 개념 비율이다. 실제 분석에서는 map, symbol과 runtime 출력의 시작·끝 주소를 대입한다.
두 열의 객체는 이름이 비슷해도 생성 시점, 사용 가능한 API와 해제 책임이 다르다.
Winner hart
01atomic 이전 값으로 승리
02gd와 malloc_f 생성
03FDT와 boot hart 기록
04board_init_f 실행
05relocation 주도
Secondary hart
01자기 stack 유지
02global init 반복 금지
03gp와 trap 상태 준비
04shared pointer/IPI 대기
05relocation 뒤 합류
호출 이름만 나열하지 않고, 각 단계가 다음 단계에 넘기는 상태를 기준으로 실행 순서를 표시한다.
UPSTREAM SOURCE
원본 코드
아래 코드는 U-Boot v2026.07의 arch/riscv/cpu/start.S에서 33-246줄을 그대로 가져온 것이다. 설명을 위해 실제 코드를 가짜 의사 코드로 바꾸지 않았다.
33#endif
34
35.section .data
36secondary_harts_relocation_error:
37 .ascii "Relocation of secondary harts has failed, error %d\n"
38
39.section .text
40.globl _start
41_start:
42#if CONFIG_IS_ENABLED(RISCV_MMODE)
43 csrr a0, CSR_MHARTID
44#endif
45
46 /*
47 * Save hart id and dtb pointer. The thread pointer register is not
48 * modified by C code. It is used by secondary_hart_loop.
49 */
50 mv tp, a0
51 mv s1, a1
52
53 /*
54 * Set the global data pointer to a known value in case we get a very
55 * early trap. The global data pointer will be set its actual value only
56 * after it has been initialized.
57 */
58 mv gp, zero
59
60 /*
61 * Set the trap handler. This must happen after initializing gp because
62 * the handler may use it.
63 */
64 la t0, trap_entry
65 csrw MODE_PREFIX(tvec), t0
66
67 /*
68 * Mask all interrupts. Interrupts are disabled globally (in m/sstatus)
69 * for U-Boot, but we will need to read m/sip to determine if we get an
70 * IPI
71 */
72 csrw MODE_PREFIX(ie), zero
73
74#if CONFIG_IS_ENABLED(SMP)
75 /* check if hart is within range */
76 /* tp: hart id */
77 li t0, CONFIG_NR_CPUS
78 bge tp, t0, hart_out_of_bounds_loop
79
80 /* set xSIE bit to receive IPIs */
81#if CONFIG_IS_ENABLED(RISCV_MMODE)
82 li t0, MIE_MSIE
83#else
84 li t0, SIE_SSIE
85#endif
86 csrs MODE_PREFIX(ie), t0
87#endif
88
89/*
90 * Set stackpointer in internal/ex RAM to call board_init_f
91 */
92call_board_init_f:
93#if CONFIG_IS_ENABLED(HAVE_INIT_STACK)
94 li t0, CONFIG_VAL(STACK)
95#else
96 li t0, SYS_INIT_SP_ADDR
97#endif
98 and t0, t0, -16 /* force 16 byte alignment */
99
100 /* setup stack */
101#if CONFIG_IS_ENABLED(SMP)
102 /* tp: hart id */
103 slli t1, tp, CONFIG_STACK_SIZE_SHIFT
104 sub sp, t0, t1
105#else
106 mv sp, t0
107#endif
108/*
109 * Now sp points to the right stack belonging to current CPU.
110 * It's essential before any function call, otherwise, we get data-race.
111 */
112
113/* clear stack if necessary */
114#if CONFIG_IS_ENABLED(ZERO_MEM_BEFORE_USE)
115clear_stack:
116 li t1, 1
117 slli t1, t1, CONFIG_STACK_SIZE_SHIFT
118 sub t1, sp, t1
119clear_stack_loop:
120 SREG zero, 0(t1) /* t1 is always 16 byte aligned */
121 addi t1, t1, REGBYTES
122 blt t1, sp, clear_stack_loop
123#endif
124
125call_board_init_f_0:
126 /* find top of reserve space */
127#if CONFIG_IS_ENABLED(SMP)
128 li t1, CONFIG_NR_CPUS
129#else
130 li t1, 1
131#endif
132 slli t1, t1, CONFIG_STACK_SIZE_SHIFT
133 sub a0, t0, t1 /* t1 -> size of all CPU stacks */
134 jal board_init_f_alloc_reserve
135
136 /*
137 * Save global data pointer for later. We don't set it here because it
138 * is not initialized yet.
139 */
140 mv s0, a0
141
142
143 /* Configure proprietary settings and customized CSRs of harts */
144call_harts_early_init:
145 jal harts_early_init
146
147#if !CONFIG_IS_ENABLED(XIP)
148 /*
149 * Pick hart to initialize global data and run U-Boot. The other harts
150 * wait for initialization to complete.
151 */
152 la t0, hart_lottery
153 li t1, 1
154 amoswap.w s2, t1, 0(t0)
155 bnez s2, wait_for_gd_init
156#else
157 /*
158 * FIXME: gp is set before it is initialized. If an XIP U-Boot ever
159 * encounters a pending IPI on boot it is liable to jump to whatever
160 * memory happens to be in ipi_data.addr on boot. It may also run into
161 * problems if it encounters an exception too early (because printf/puts
162 * accesses gd).
163 */
164 mv gp, s0
165#if CONFIG_IS_ENABLED(RISCV_MMODE)
166 bnez tp, secondary_hart_loop
167#endif
168#endif
169
170 mv a0, s0
171 jal board_init_f_init_reserve
172
173 SREG s1, GD_FIRMWARE_FDT_ADDR(gp)
174 /* save the boot hart id to global_data */
175 SREG tp, GD_BOOT_HART(gp)
176
177#if !CONFIG_IS_ENABLED(XIP)
178#ifdef CONFIG_AVAILABLE_HARTS
179 la t0, available_harts_lock
180 amoswap.w.rl zero, zero, 0(t0)
181#endif
182
183wait_for_gd_init:
184 /*
185 * Set the global data pointer only when gd_t has been initialized.
186 * This was already set by arch_setup_gd on the boot hart, but all other
187 * harts' global data pointers gets set here.
188 */
189 mv gp, s0
190#ifdef CONFIG_AVAILABLE_HARTS
191 la t0, available_harts_lock
192 li t1, 1
1931: amoswap.w.aq t1, t1, 0(t0)
194 bnez t1, 1b
195
196 /* register available harts in the available_harts mask */
197 li t1, 1
198 sll t1, t1, tp
199 LREG t2, GD_AVAILABLE_HARTS(gp)
200 or t2, t2, t1
201 SREG t2, GD_AVAILABLE_HARTS(gp)
202
203 amoswap.w.rl zero, zero, 0(t0)
204#endif
205
206 /*
207 * Continue on hart lottery winner, others branch to
208 * secondary_hart_loop.
209 */
210 bnez s2, secondary_hart_loop
211#endif
212
213#ifdef CONFIG_DEBUG_UART
214 jal debug_uart_init
215#endif
216
217 mv a0, zero /* a0 <-- boot_flags = 0 */
218 la t5, board_init_f
219 jalr t5 /* jump to board_init_f() */
220
221#ifdef CONFIG_XPL_BUILD
222spl_clear_bss:
223 la t0, __bss_start
224 la t1, __bss_end
225 beq t0, t1, spl_stack_gd_setup
226
227spl_clear_bss_loop:
228 SREG zero, 0(t0)
229 addi t0, t0, REGBYTES
230 blt t0, t1, spl_clear_bss_loop
231
232spl_stack_gd_setup:
233 jal spl_relocate_stack_gd
234
235 /* skip setup if we did not relocate */
236 beqz a0, spl_call_board_init_r
237 mv s0, a0
238
239 /* setup stack on main hart */
240#if CONFIG_IS_ENABLED(SMP)
241 /* tp: hart id */
242 slli t0, tp, CONFIG_STACK_SIZE_SHIFT
243 sub sp, s0, t0
244#else
245 mv sp, s0
246#endif
READING CONTRACT
발췌 범위와 읽기 기준
원문 33-246줄에는 앞뒤 문맥이 포함됩니다. 기준 구문 _start:은 L41에 있습니다. 발췌 첫 줄을 함수 정의의 시작으로 해석하지 않습니다.
RISC-V에서는 여러 hart가 동시에 같은 entry에 도착할 수 있다. stack을 hart별로 분리하고 atomic lottery의 winner만 global 초기화를 실행하며, loser는 winner가 게시한 상태를 기다린다.
_start:ROM 또는 previous stage가 넘긴 모든 hart가 도착할 수 있는 RISC-V U-Boot entry다.
csrr a0, CSR_MHARTIDM-mode build에서 현재 hardware hart ID를 읽어 boot argument a0를 만든다.
mv tp, a0hart ID를 tp에 보존한다. 아직 Linux식 current pointer가 아니라 초기 per-hart 계산 값이다.
mv s1, a1firmware가 넘긴 FDT pointer를 relocation 전 호출 사이에 보존한다.
mv gp, zeroglobal pointer를 일부러 무효화해 너무 이른 gp-relative global 접근을 막는다.
csrw MODE_PREFIX(tvec), t0현재 privilege mode의 trap vector를 early trap entry로 설정한다.
slli t1, tp, CONFIG_STACK_SIZE_SHIFTraw hart ID에 2^CONFIG_STACK_SIZE_SHIFT를 곱해 hart별 stack slot offset을 만든다. sparse hart ID에서는 reservation 상한을 반드시 확인한다.
amoswap.w s2, t1, 0(t0)hart lottery 값을 원자적으로 교환해 global 초기화를 실행할 한 hart를 고른다.
bnez tp, secondary_hart_loopwinner가 아닌 hart는 global board 초기화에 진입하지 않고 자신에게 새 실행 상태가 게시될 때까지 secondary loop로 간다.
amoswap.w.rl zero, zero, 0(t0)boot hart가 shared slot에 release semantic으로 0을 게시해 앞선 gd/stack store가 secondary보다 먼저 보이게 한다.
amoswap.w.rl zero, zero, 0(t0)boot hart가 shared slot에 release semantic으로 0을 게시해 앞선 gd/stack store가 secondary보다 먼저 보이게 한다.
RELATED SOURCE
함께 읽어야 하는 원본 코드
첫 코드 조각만으로 동작이 완성되지 않는 경우 호출 매크로, 객체 정의와 실제 실행 목록을 같은 페이지에 묶었다. 각 조각은 같은 기준 commit에서 가져왔다.
01. winner와 secondary를 가르는 hart lottery 구간
arch/riscv/cpu/start.S 128-234줄이다.
128 li t1, CONFIG_NR_CPUS
129#else
130 li t1, 1
131#endif
132 slli t1, t1, CONFIG_STACK_SIZE_SHIFT
133 sub a0, t0, t1 /* t1 -> size of all CPU stacks */
134 jal board_init_f_alloc_reserve
135
136 /*
137 * Save global data pointer for later. We don't set it here because it
138 * is not initialized yet.
139 */
140 mv s0, a0
141
142
143 /* Configure proprietary settings and customized CSRs of harts */
144call_harts_early_init:
145 jal harts_early_init
146
147#if !CONFIG_IS_ENABLED(XIP)
148 /*
149 * Pick hart to initialize global data and run U-Boot. The other harts
150 * wait for initialization to complete.
151 */
152 la t0, hart_lottery
153 li t1, 1
154 amoswap.w s2, t1, 0(t0)
155 bnez s2, wait_for_gd_init
156#else
157 /*
158 * FIXME: gp is set before it is initialized. If an XIP U-Boot ever
159 * encounters a pending IPI on boot it is liable to jump to whatever
160 * memory happens to be in ipi_data.addr on boot. It may also run into
161 * problems if it encounters an exception too early (because printf/puts
162 * accesses gd).
163 */
164 mv gp, s0
165#if CONFIG_IS_ENABLED(RISCV_MMODE)
166 bnez tp, secondary_hart_loop
167#endif
168#endif
169
170 mv a0, s0
171 jal board_init_f_init_reserve
172
173 SREG s1, GD_FIRMWARE_FDT_ADDR(gp)
174 /* save the boot hart id to global_data */
175 SREG tp, GD_BOOT_HART(gp)
176
177#if !CONFIG_IS_ENABLED(XIP)
178#ifdef CONFIG_AVAILABLE_HARTS
179 la t0, available_harts_lock
180 amoswap.w.rl zero, zero, 0(t0)
181#endif
182
183wait_for_gd_init:
184 /*
185 * Set the global data pointer only when gd_t has been initialized.
186 * This was already set by arch_setup_gd on the boot hart, but all other
187 * harts' global data pointers gets set here.
188 */
189 mv gp, s0
190#ifdef CONFIG_AVAILABLE_HARTS
191 la t0, available_harts_lock
192 li t1, 1
1931: amoswap.w.aq t1, t1, 0(t0)
194 bnez t1, 1b
195
196 /* register available harts in the available_harts mask */
197 li t1, 1
198 sll t1, t1, tp
199 LREG t2, GD_AVAILABLE_HARTS(gp)
200 or t2, t2, t1
201 SREG t2, GD_AVAILABLE_HARTS(gp)
202
203 amoswap.w.rl zero, zero, 0(t0)
204#endif
205
206 /*
207 * Continue on hart lottery winner, others branch to
208 * secondary_hart_loop.
209 */
210 bnez s2, secondary_hart_loop
211#endif
212
213#ifdef CONFIG_DEBUG_UART
214 jal debug_uart_init
215#endif
216
217 mv a0, zero /* a0 <-- boot_flags = 0 */
218 la t5, board_init_f
219 jalr t5 /* jump to board_init_f() */
220
221#ifdef CONFIG_XPL_BUILD
222spl_clear_bss:
223 la t0, __bss_start
224 la t1, __bss_end
225 beq t0, t1, spl_stack_gd_setup
226
227spl_clear_bss_loop:
228 SREG zero, 0(t0)
229 addi t0, t0, REGBYTES
230 blt t0, t1, spl_clear_bss_loop
231
232spl_stack_gd_setup:
233 jal spl_relocate_stack_gd
234
주요 구문 해설
amoswap.w s2, t1, 0(t0)hart lottery 값을 원자적으로 교환해 global 초기화를 실행할 한 hart를 고른다.
bnez tp, secondary_hart_loopwinner가 아닌 hart는 global board 초기화에 진입하지 않고 자신에게 새 실행 상태가 게시될 때까지 secondary loop로 간다.
amoswap.w.rl zero, zero, 0(t0)boot hart가 shared slot에 release semantic으로 0을 게시해 앞선 gd/stack store가 secondary보다 먼저 보이게 한다.
amoswap.w.rl zero, zero, 0(t0)boot hart가 shared slot에 release semantic으로 0을 게시해 앞선 gd/stack store가 secondary보다 먼저 보이게 한다.
02. secondary hart가 software interrupt를 기다리는 loop
arch/riscv/cpu/start.S 437-465줄이다.
437 /* setup stack */
438 slli t0, tp, CONFIG_STACK_SIZE_SHIFT
439 sub sp, a1, t0
440
441 /* update global data pointer */
442 mv gp, a2
443#endif
444
445/*
446 * Interrupts are disabled globally, but they can still be read from m/sip. The
447 * wfi function will wake us up if we get an IPI, even if we do not trap.
448 */
449secondary_hart_loop:
450 wfi
451
452#if CONFIG_IS_ENABLED(SMP)
453 csrr t0, MODE_PREFIX(ip)
454#if CONFIG_IS_ENABLED(RISCV_MMODE)
455 andi t0, t0, MIE_MSIE
456#else
457 andi t0, t0, SIE_SSIE
458#endif
459 beqz t0, secondary_hart_loop
460
461 mv a0, tp
462 jal handle_ipi
463#endif
464
465 j secondary_hart_loop
03. per-hart stack 크기와 CPU 개수를 제공하는 설정
arch/riscv/include/asm/global_data.h 15-65줄이다.
15#include <asm/u-boot.h>
16#include <compiler.h>
17#include <config.h>
18
19/* Architecture-specific global data */
20struct arch_global_data {
21 long boot_hart; /* boot hart id */
22 phys_addr_t firmware_fdt_addr;
23#if CONFIG_IS_ENABLED(RISCV_ACLINT)
24 void __iomem *aclint; /* aclint base address */
25#endif
26#ifdef CONFIG_ANDES_PLICSW
27 void __iomem *plicsw; /* andes plicsw base address */
28#endif
29#if CONFIG_IS_ENABLED(SMP)
30 struct ipi_data ipi[CONFIG_NR_CPUS];
31#endif
32#if !CONFIG_IS_ENABLED(XIP)
33#ifdef CONFIG_AVAILABLE_HARTS
34 ulong available_harts;
35#endif
36#endif
37#if CONFIG_IS_ENABLED(ACPI)
38 ulong table_start; /* Start address of ACPI tables */
39 ulong table_end; /* End address of ACPI tables */
40 ulong table_start_high; /* Start address of high ACPI tables */
41 ulong table_end_high; /* End address of high ACPI tables */
42#endif
43#ifdef CONFIG_SMBIOS
44 ulong smbios_start; /* Start address of SMBIOS table */
45#endif
46 struct resume_data *resume;
47};
48
49#include <asm-generic/global_data.h>
50
51#if defined(__clang__) || CONFIG_IS_ENABLED(LTO)
52
53#define DECLARE_GLOBAL_DATA_PTR
54#define gd get_gd()
55
56static inline gd_t *get_gd(void)
57{
58 gd_t *gd_ptr;
59
60 __asm__ volatile ("mv %0, gp\n" : "=r" (gd_ptr));
61
62 return gd_ptr;
63}
64
65#else
04. boot hart와 secondary의 tp에 gd pointer를 설치
arch/riscv/cpu/cpu.c 744-754줄이다.
744 disable_interrupts();
745
746 cache_flush();
747
748 return 0;
749}
750
751void arch_setup_gd(gd_t *new_gd)
752{
753 set_gd(new_gd);
754}
주요 구문 해설
void arch_setup_gd(gd_t *new_gd)RISC-V의 gd 전용 register인 tp를 새 global data pointer로 교체하는 architecture hook이다.
set_gd(new_gd);compiler가 기대하는 tp/gd binding을 실제 new_gd 주소로 갱신한다. 이후 gd-> access의 주소 생산자가 바뀐다.
DETAILS
내부 동작을 더 깊게 읽기
진입 조건을 먼저 고정한다
firmware or reset에서 들어온 실행은 MMU와 일반 scheduler가 없는 multi-hart 초기 문맥에 놓여 있다. 이때 interrupt, MMU/cache, stack, heap 중 무엇이 이미 준비되었는지 소스의 호출자까지 올라가 확인한다. 같은 함수라도 SPL, relocation 전후, app thread처럼 호출 문맥이 달라지면 허용되는 API와 지연 시간이 달라진다.
RISC-V에서는 여러 hart가 동시에 같은 entry에 도착할 수 있다. stack을 hart별로 분리하고 atomic lottery의 winner만 global 초기화를 실행하며, loser는 winner가 게시한 상태를 기다린다.
중심 객체의 생성과 공개를 나눈다
이 글의 중심 객체는 per-hart stack / hart_lottery / gd_t다. 메모리를 확보한 시점, 필드를 채운 시점, 전역 list나 다른 subsystem에 공개한 시점을 구분한다. 공개 뒤 오류가 발생한다면 목록에서 제거하고 child, buffer, reference를 역순으로 정리하는지 확인한다.
빌드 산출물 관점에서는 RISC-V U-Boot pre-relocation image 안에 해당 symbol과 section이 실제로 포함되었는지도 map과 objdump로 검증한다.
주소, 크기와 정렬을 계산한다
부트 코드에서는 주소 계산 오류를 주의해야 한다. 원본과 목적지의 주소 범위, 헤더에 기록된 데이터 크기, 블록·페이지 단위를 표로 정리하고 각 구간의 끝 주소를 계산한다. 끝 주소가 start + size - 1처럼 마지막 바이트를 가리키는지, 구간 바로 다음 주소를 가리키는지는 API 규약에서 확인한다.
성공 flag와 실제 완료 시점을 맞춘다
hartid and FDT preserve → trap vector and interrupt mask → per-hart stack → hart lottery → boot hart gd → secondary wait 구간에서는 부분 초기화 상태가 생길 수 있다. 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
구현을 읽을 때 놓치기 쉬운 부분
RISC-V U-Boot entry의 a0와 a1은 Linux entry와 비슷하지만 같은 계약은 아니다
M-mode에서는 _start가 mhartid를 읽어 a0에 넣고, firmware가 들어오는 경로에서는 a0의 hart ID와 a1의 FDT pointer를 보존한다. Linux와 register 모양은 비슷해도 U-Boot는 먼저 자체 gd, stack, hart lottery와 relocation을 완성해야 한다.
초기 code가 a0/a1을 임시 계산에 사용하기 전에 tp와 s1 등에 복사하는 줄을 따라가야 한다. FDT pointer가 relocation source/destination이나 BSS clear와 겹치지 않는지도 memory map에서 확인한다.
gp를 0으로 만드는 것은 실수로 relocation 전 global을 쓰는 일을 조기에 드러낸다
초기 entry는 global pointer를 바로 유효하게 만들지 않고 0으로 둔다. small-data access가 너무 일찍 발생하면 조용히 잘못된 global을 읽는 대신 fault나 명확한 오동작으로 드러난다. 이후 relocation 단계와 secondary path에서 load_global_pointer가 책임 있게 gp를 설정한다.
compiler가 예상하지 못한 global access를 assembly helper나 instrumented C 함수에 삽입하지 않았는지 disassembly에서 gp-relative instruction을 검색한다.
trap vector와 interrupt mask는 stack보다 먼저 정리한다
tvec는 trap_entry를 가리키고 IE/IP CSR을 정리한다. 아직 per-hart stack 계산이 끝나지 않았으므로 비동기 interrupt는 막지만, synchronous exception이 발생하면 최소 trap code로 원인을 남길 준비는 한다.
M-mode와 S-mode build는 CSR prefix와 return instruction이 달라진다. 실제 U-Boot가 어느 privilege mode로 link됐는지 .config, objdump, firmware delegation state를 함께 본다.
per-hart stack은 하나의 top에서 hart ID만큼 일정 간격으로 내려간다
초기 stack top을 16-byte로 정렬하고 hartid * CONFIG_STACK_SIZE를 빼서 각 hart의 slot을 고른다. 다시 전체 CPU 수만큼 예약 cursor를 내리므로 boot hart의 gd/early malloc과 secondary stack range가 겹치지 않는다.
DT가 보고한 hart ID가 0부터 조밀하지 않을 수 있다는 점을 board code가 어떻게 처리하는지 확인한다. 단순 hart ID 곱셈이 reservation 범위를 넘는 platform은 별도 mapping이나 최대 hart ID 제약이 필요하다.
hart lottery는 boot CPU 번호 선택이 아니라 global 초기화 단일 실행을 보장한다
여러 hart가 동시에 _start에 들어올 수 있으므로 atomic swap/add의 이전 값을 보고 한 hart만 gd 생성과 board_init_f로 간다. 나머지는 winner가 게시할 stack, gd 또는 IPI를 기다린다. hart ID가 가장 작은 CPU가 반드시 이기는 것은 아니다.
lottery variable이 BSS clear 대상과 어떤 순서로 초기화되는지, cache coherence가 아직 완전히 설정되지 않은 platform에서 atomic이 어느 memory attribute를 사용하는지 확인한다.
gd pointer 게시와 secondary_hart_loop의 소비를 한 쌍으로 본다
winner는 board_init_f_init_reserve로 gd를 만들고 boot hart/FDT를 기록한다. secondary는 자신에게 배정된 pointer가 유효해질 때까지 대기하고, software interrupt 또는 shared slot을 통해 다음 경로를 받는다.
pointer 저장 뒤 fence와 IPI 순서가 잘못되면 secondary가 wakeup은 받았지만 0 또는 오래된 stack/gd를 읽는다. producer의 store와 consumer의 load를 source 양쪽에서 대조한다.
boot hart와 secondary hart의 로그를 같은 UART 순서로 해석하지 않는다
여러 hart가 동시에 POST/debug UART를 사용하면 byte가 섞이고 timestamp 순서도 실제 memory publication 순서와 다를 수 있다. 각 로그에 mhartid와 cycle counter를 붙여야 한다.
초기 latency는 winner의 DRAM init 시간과 secondary의 wait 시간을 분리한다. secondary wait upper bound는 boot hart가 relocation과 per-hart data 게시를 마치는 최악 시간에 의해 결정된다.
per-hart SP를 실제 hart ID와 reservation 크기로 계산한다
초기 stack top이 0x88000000, CONFIG_STACK_SIZE_SHIFT=14라면 slot 크기는 1 << 14 = 0x4000이다. 공식이 SP(h)=top-(hartid << 14)라면 hart 0은 0x88000000, hart 1은 0x87ffc000, hart 7은 0x87fe4000을 사용한다. 8 hart 전체 reservation은 최소 0x20000이고 각 SP는 ABI의 16-byte 정렬을 만족한다.
DT에 hart 0과 7만 존재해도 raw hart ID를 곱하면 중간 slot까지 포함한 범위를 예약해야 한다. logical CPU index로 압축하는 구현인지, maximum hart ID를 제한하는 구현인지 source를 확인하지 않으면 hart 7 stack이 gd/malloc_f 아래로 벗어날 수 있다.
amoswap의 반환값으로 winner 한 명을 증명한다
hart_lottery 초기값이 0이고 각 hart가 새 값 1을 amoswap.w로 쓴다면 정확히 한 hart만 old value 0을 받는다. 그 hart가 boot path로 가고 나머지는 old value 1을 받아 secondary path로 간다. winner의 hart ID가 가장 작다는 보장은 없다.
atomicity만으로 뒤의 gd/FDT publication ordering이 자동 보장되는 것은 아니다. instruction의 aq/rl bit와 별도 fence, IPI send 전 store ordering을 같이 봐야 secondary가 winner의 완성된 데이터를 읽는다.
secondary 대기 시간은 boot hart의 가장 긴 선행 경로로 제한된다
secondary wait upper bound는 Tlottery + Tdram + Treserve + Tcopy + Tfixup + Tpublish + Tipi다. 10MHz cycle counter에서 entry 2,000 cycle, DRAM 6,000,000, relocation 800,000, publish/IPI 20,000이면 약 682.2ms다.
각 hart의 cycle counter가 동기화됐다는 보장이 없으면 boot hart가 게시한 sequence number와 local delta를 함께 기록한다. UART byte 순서만으로 682ms의 어느 부분이 긴지 판단하지 않는다.
OBJECT LIFETIME
객체와 수명
| 대상 | 만들어지는 시점 | 유효 범위 | 확인할 조건 |
|---|---|---|---|
per-hart stack / hart_lottery / gd_t | hartid and FDT preserve | board_init_f 또는 오류 정리 완료까지 | 진입 전제와 상태 계약 |
| 입력 buffer / metadata | firmware or reset | parse와 검증이 끝날 때까지 | 길이, 정렬, 소유권, 변조 가능성 |
| 등록된 list / descriptor | trap vector and interrupt mask | 하위 시스템을 종료하거나 다음 이미지에 제어권을 넘길 때까지 | 중복 등록, 해제된 객체를 가리키는 포인터, 오류 후 자원 정리 |
| hardware 또는 persistent state | 실제 write/probe가 완료된 뒤 | reset 또는 명시적 해제·갱신까지 | 쓰기 완료, 시간 초과, 전원 차단, 이전 상태 복원 |
RISC-V U-Boot pre-relocation image | link/image 생성 시점 | 다음 stage가 새 image로 교체할 때까지 | 적재 주소, 진입점, 섹션, 헤더의 일치 여부 |
FAILURE PATH
실패 지점과 증상
| # | 조건 | 관찰되는 증상 | 먼저 볼 단계 |
|---|---|---|---|
| 01 | hart ID 범위 오류 | stack reservation 밖 접근 | hartid and FDT preserve |
| 02 | lottery/visibility 오류 | 둘 이상의 hart가 global init | trap vector and interrupt mask |
| 03 | FDT pointer 훼손 | board identification 실패 | per-hart stack |
| 04 | early trap의 stack 부재 | 원인 기록 중 재 fault | hart lottery |
로그가 끊긴 함수뿐 아니라 그 함수에 주소, 크기, 플래그를 전달한 앞 단계도 조사한다. 오류 처리 후 등록된 객체나 하드웨어 설정이 그대로 남아 있는지도 확인한다.
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
직접 확인할 실험
- 01모든 early log에 mhartid와 cycle counter 추가
hartid and FDT preserve진입 전후에 a0 hartid and a1 FDT의 주소·크기·반환값과 timestamp를 함께 남긴다. - 02hart별 SP range와 CONFIG_STACK_SIZE 대조
trap vector and interrupt mask진입 전후에 tp and s1 preserve의 주소·크기·반환값과 timestamp를 함께 남긴다. - 03lottery variable watchpoint로 winner 확인
per-hart stack진입 전후에 per-hart stack slots의 주소·크기·반환값과 timestamp를 함께 남긴다. - 04secondary wakeup 전후 shared pointer와 fence 기록
hart lottery진입 전후에 hart_lottery의 주소·크기·반환값과 timestamp를 함께 남긴다.
PRIMARY REFERENCES