U-Boot · source analysis

RISC-V hart entry, boot lottery와 per-hart stack

여러 hart가 `_start`에 들어온 뒤 hart ID와 FDT를 보존하고 trap vector, per-hart stack, global data를 만든 다음 한 hart만 board_init_f를 실행하는 과정을 읽습니다.

표의 버전은 소스 분석 기준이며 최신 보안 버전 권고가 아닙니다. TF-A LTS 2.14.3 이후에는 보안 수정이 있습니다. 제품 적용 시 공식 변경 기록과 사용 플랫폼의 errata를 확인하세요.

Series
21 / 22
Baseline
v2026.07
Commit
ece349ade297
Source
arch/riscv/cpu/start.S:33-246 (발췌 범위)

동시에 깨어난 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를 관찰해야 한다. 이 문장을 기준으로 코드를 위에서 아래로 읽으면, 함수 이름을 외우는 대신 어느 시점에 어떤 상태를 신뢰할 수 있는지 판단할 수 있다.

객체와 주소가 놓이는 구조

그림 1. RISC-V hart entry, boot lottery와 per-hart stack에서 입력, 내부 상태, 출력의 경계
입력과 전제내부 상태외부로 공개되는 결과
01a0 hartid and a1 FDT
02tp and s1 preserve
03per-hart stack slots
04hart_lottery
05gd publication
06secondary_hart_loop

per-hart stack / hart_lottery / gd_t를 중심에 놓고 왼쪽의 입력이 어떤 검사를 거쳐 오른쪽 결과로 공개되는지 표시했다. 실제 디버깅에서는 각 블록의 주소와 크기를 로그에 대입한다.

실행 흐름

그림 2. 정상 경로의 주요 호출과 상태 전달
firmware or reset
hartid and FDT preserve
trap vector and interrupt mask
per-hart stack
hart lottery
boot hart gd
secondary wait
board_init_f

화살표는 단순 호출 순서만 뜻하지 않는다. 각 단계가 성공을 반환할 때 다음 단계가 읽을 수 있는 상태가 무엇인지 함께 확인한다. 오류 반환이 발생하면 바로 다음 화살표로 진행하지 않고 해당 단계의 정리 경로를 따라간다.

단계별 입력과 출력

각 단계의 입력과 처리 후 바뀌는 상태를 정리한 표다. 사용 중인 소스 버전에서 다른 필드를 변경한다면 그에 맞춰 표도 수정한다.

#단계진입 시 신뢰할 상태성공 뒤 남아야 할 상태다음 소비자
01firmware or resetMMU와 일반 scheduler가 없는 multi-hart 초기 문맥a0 hartid and a1 FDThartid and FDT preserve
02hartid and FDT preservefirmware or reset 완료 상태tp and s1 preservetrap vector and interrupt mask
03trap vector and interrupt maskhartid and FDT preserve 완료 상태per-hart stack slotsper-hart stack
04per-hart stacktrap vector and interrupt mask 완료 상태hart_lotteryhart lottery
05hart lotteryper-hart stack 완료 상태gd publicationboot hart gd
06boot hart gdhart lottery 완료 상태secondary_hart_loopsecondary wait
07secondary waitboot hart gd 완료 상태secondary_hart_loopboard_init_f
08board_init_fsecondary wait 완료 상태secondary_hart_loop최종 RISC-V U-Boot pre-relocation image 또는 다음 stage

분석 범위의 전제와 비교해, 다음 단계에 잘못된 입력을 넘긴 부분부터 조사한다.

주소와 객체의 이동을 그림으로 읽기

아래 그림은 주소, 객체를 사용하는 기간, 제어권을 넘기는 시점을 중심으로 정리했다. 실제 주소와 크기는 사용 중인 보드의 bdinfo, 맵 파일과 링커 심벌로 확인한다.

그림 3. hart ID로 나뉘는 초기 stack slot

크기는 개념 비율이다. 실제 분석에서는 map, symbol과 runtime 출력의 시작·끝 주소를 대입한다.

높은 주소↓ 주소 감소
boot hart stackwinner의 board_init_f call chain
hart 1 stackCONFIG_STACK_SIZE 고정 간격
hart 2 stackhart ID 기반 offset
all-hart reservationgd와 malloc_f가 침범하지 않음
gd + early mallocwinner만 만들고 게시
낮은 주소예약 cursor 진행 방향
그림 4. lottery winner와 secondary hart의 실행 경로

두 열의 객체는 이름이 비슷해도 생성 시점, 사용 가능한 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 뒤 합류

그림 5. 여러 hart가 하나의 runtime으로 모이는 순서

호출 이름만 나열하지 않고, 각 단계가 다음 단계에 넘기는 상태를 기준으로 실행 순서를 표시한다.

01all harts _start
02trap/IE 정리
03per-hart SP
04atomic lottery
05winner publishes gd
06secondary wait
07relocation handoff

원본 코드

아래 코드는 U-Boot v2026.07의 arch/riscv/cpu/start.S에서 33-246줄을 그대로 가져온 것이다. 설명을 위해 실제 코드를 가짜 의사 코드로 바꾸지 않았다.

commitece349ade2973e220f524ce59e59711cc919263fanchor · L41_start:
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

발췌 범위와 읽기 기준

원문 33-246줄에는 앞뒤 문맥이 포함됩니다. 기준 구문 _start:은 L41에 있습니다. 발췌 첫 줄을 함수 정의의 시작으로 해석하지 않습니다.

RISC-V에서는 여러 hart가 동시에 같은 entry에 도착할 수 있다. stack을 hart별로 분리하고 atomic lottery의 winner만 global 초기화를 실행하며, loser는 winner가 게시한 상태를 기다린다.

41_start:

ROM 또는 previous stage가 넘긴 모든 hart가 도착할 수 있는 RISC-V U-Boot entry다.

43 csrr a0, CSR_MHARTID

M-mode build에서 현재 hardware hart ID를 읽어 boot argument a0를 만든다.

50 mv tp, a0

hart ID를 tp에 보존한다. 아직 Linux식 current pointer가 아니라 초기 per-hart 계산 값이다.

51 mv s1, a1

firmware가 넘긴 FDT pointer를 relocation 전 호출 사이에 보존한다.

58 mv gp, zero

global pointer를 일부러 무효화해 너무 이른 gp-relative global 접근을 막는다.

65 csrw MODE_PREFIX(tvec), t0

현재 privilege mode의 trap vector를 early trap entry로 설정한다.

103 slli t1, tp, CONFIG_STACK_SIZE_SHIFT

raw hart ID에 2^CONFIG_STACK_SIZE_SHIFT를 곱해 hart별 stack slot offset을 만든다. sparse hart ID에서는 reservation 상한을 반드시 확인한다.

154 amoswap.w s2, t1, 0(t0)

hart lottery 값을 원자적으로 교환해 global 초기화를 실행할 한 hart를 고른다.

166 bnez tp, secondary_hart_loop

winner가 아닌 hart는 global board 초기화에 진입하지 않고 자신에게 새 실행 상태가 게시될 때까지 secondary loop로 간다.

180 amoswap.w.rl zero, zero, 0(t0)

boot hart가 shared slot에 release semantic으로 0을 게시해 앞선 gd/stack store가 secondary보다 먼저 보이게 한다.

203 amoswap.w.rl zero, zero, 0(t0)

boot hart가 shared slot에 release semantic으로 0을 게시해 앞선 gd/stack store가 secondary보다 먼저 보이게 한다.

함께 읽어야 하는 원본 코드

첫 코드 조각만으로 동작이 완성되지 않는 경우 호출 매크로, 객체 정의와 실제 실행 목록을 같은 페이지에 묶었다. 각 조각은 같은 기준 commit에서 가져왔다.

01. winner와 secondary를 가르는 hart lottery 구간

arch/riscv/cpu/start.S 128-234줄이다.

filearch/riscv/cpu/start.Sanchor · L152la t0, hart_lottery
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

주요 구문 해설

154 amoswap.w s2, t1, 0(t0)

hart lottery 값을 원자적으로 교환해 global 초기화를 실행할 한 hart를 고른다.

166 bnez tp, secondary_hart_loop

winner가 아닌 hart는 global board 초기화에 진입하지 않고 자신에게 새 실행 상태가 게시될 때까지 secondary loop로 간다.

180 amoswap.w.rl zero, zero, 0(t0)

boot hart가 shared slot에 release semantic으로 0을 게시해 앞선 gd/stack store가 secondary보다 먼저 보이게 한다.

203 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줄이다.

filearch/riscv/cpu/start.Sanchor · L449secondary_hart_loop:
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줄이다.

filearch/riscv/include/asm/global_data.hanchor · L20struct arch_global_data
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줄이다.

filearch/riscv/cpu/cpu.canchor · L751void arch_setup_gd(gd_t *new_gd)
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}

주요 구문 해설

751void arch_setup_gd(gd_t *new_gd)

RISC-V의 gd 전용 register인 tp를 새 global data pointer로 교체하는 architecture hook이다.

753 set_gd(new_gd);

compiler가 기대하는 tp/gd binding을 실제 new_gd 주소로 갱신한다. 이후 gd-> access의 주소 생산자가 바뀐다.

내부 동작을 더 깊게 읽기

01

진입 조건을 먼저 고정한다

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가 게시한 상태를 기다린다.

02

중심 객체의 생성과 공개를 나눈다

이 글의 중심 객체는 per-hart stack / hart_lottery / gd_t다. 메모리를 확보한 시점, 필드를 채운 시점, 전역 list나 다른 subsystem에 공개한 시점을 구분한다. 공개 뒤 오류가 발생한다면 목록에서 제거하고 child, buffer, reference를 역순으로 정리하는지 확인한다.

빌드 산출물 관점에서는 RISC-V U-Boot pre-relocation image 안에 해당 symbol과 section이 실제로 포함되었는지도 map과 objdump로 검증한다.

03

주소, 크기와 정렬을 계산한다

부트 코드에서는 주소 계산 오류를 주의해야 한다. 원본과 목적지의 주소 범위, 헤더에 기록된 데이터 크기, 블록·페이지 단위를 표로 정리하고 각 구간의 끝 주소를 계산한다. 끝 주소가 start + size - 1처럼 마지막 바이트를 가리키는지, 구간 바로 다음 주소를 가리키는지는 API 규약에서 확인한다.

04

성공 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 완료 콜백, 셸 명령에서도 이 상태를 읽는다면 메모리 접근 순서와 잠금 범위를 함께 확인한다.

05

제어권을 넘기기 전에 확인할 조건을 정리한다

정상 경로는 board_init_f에서 끝난다. 이 단계가 함수 반환인지, scheduler 전환인지, 다른 image로의 비복귀 분기인지 구분한다. 비복귀 handoff라면 cache clean/invalidate, interrupt disable, 장치 quiesce, argument register와 FDT 또는 image address가 최종 점검 항목이다.

호출한 함수로 돌아오는 경로라면 오류와 일부 처리만 완료된 상태를 구분하는지 확인한다. 이어서 다음 후보를 시도할지, 복구 절차를 실행할지 살펴본다.

구현을 읽을 때 놓치기 쉬운 부분

01

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에서 확인한다.

02

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을 검색한다.

03

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를 함께 본다.

04

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 제약이 필요하다.

05

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를 사용하는지 확인한다.

06

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 양쪽에서 대조한다.

07

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 게시를 마치는 최악 시간에 의해 결정된다.

08

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 아래로 벗어날 수 있다.

09

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의 완성된 데이터를 읽는다.

10

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의 어느 부분이 긴지 판단하지 않는다.

객체와 수명

대상만들어지는 시점유효 범위확인할 조건
per-hart stack / hart_lottery / gd_thartid and FDT preserveboard_init_f 또는 오류 정리 완료까지진입 전제와 상태 계약
입력 buffer / metadatafirmware or resetparse와 검증이 끝날 때까지길이, 정렬, 소유권, 변조 가능성
등록된 list / descriptortrap vector and interrupt mask하위 시스템을 종료하거나 다음 이미지에 제어권을 넘길 때까지중복 등록, 해제된 객체를 가리키는 포인터, 오류 후 자원 정리
hardware 또는 persistent state실제 write/probe가 완료된 뒤reset 또는 명시적 해제·갱신까지쓰기 완료, 시간 초과, 전원 차단, 이전 상태 복원
RISC-V U-Boot pre-relocation imagelink/image 생성 시점다음 stage가 새 image로 교체할 때까지적재 주소, 진입점, 섹션, 헤더의 일치 여부

실패 지점과 증상

#조건관찰되는 증상먼저 볼 단계
01hart ID 범위 오류stack reservation 밖 접근hartid and FDT preserve
02lottery/visibility 오류둘 이상의 hart가 global inittrap vector and interrupt mask
03FDT pointer 훼손board identification 실패per-hart stack
04early trap의 stack 부재원인 기록 중 재 faulthart lottery

로그가 끊긴 함수뿐 아니라 그 함수에 주소, 크기, 플래그를 전달한 앞 단계도 조사한다. 오류 처리 후 등록된 객체나 하드웨어 설정이 그대로 남아 있는지도 확인한다.

소스 밖에서 확인할 증거

소스에서 파악한 동작을 실제 빌드 결과와 대상 보드의 로그로 확인한다. 아래 명령의 보드 이름과 툴체인 접두사는 사용 중인 빌드 환경에 맞게 바꾼다.

#목적명령 또는 계측판정 기준
01전처리 결과make <board>_defconfig && make V=1선택된 CONFIG와 실제 compile/link 명령을 보관한다.
02symbol과 section${CROSS_COMPILE}nm -n u-boot | grep '_start'symbol 주소가 기대한 stage와 image에 들어갔는지 확인한다.
03어셈블리 대조${CROSS_COMPILE}objdump -drS u-bootC 한 줄이 실제 branch, relocation, memory access로 어떻게 바뀌었는지 본다.
04주소 배치bdinforelocaddr, malloc, FDT, stack과 DRAM bank 주소를 한 번에 기록한다.
05runtime 관찰dm tree; dm uclass; printenvdriver model과 환경이 이 단계에서 실제로 공개되었는지 확인한다.

직접 확인할 실험

  1. 01
    모든 early log에 mhartid와 cycle counter 추가

    hartid and FDT preserve 진입 전후에 a0 hartid and a1 FDT의 주소·크기·반환값과 timestamp를 함께 남긴다.

  2. 02
    hart별 SP range와 CONFIG_STACK_SIZE 대조

    trap vector and interrupt mask 진입 전후에 tp and s1 preserve의 주소·크기·반환값과 timestamp를 함께 남긴다.

  3. 03
    lottery variable watchpoint로 winner 확인

    per-hart stack 진입 전후에 per-hart stack slots의 주소·크기·반환값과 timestamp를 함께 남긴다.

  4. 04
    secondary wakeup 전후 shared pointer와 fence 기록

    hart lottery 진입 전후에 hart_lottery의 주소·크기·반환값과 timestamp를 함께 남긴다.

원문과 다음 글