U-Boot · source analysis

RISC-V relocation, trap frame과 secondary hart

relocate_code가 U-Boot image와 `.rela.dyn`, BSS, tvec를 새 주소로 옮기고 secondary hart와 cache를 동기화한 뒤 board_init_r로 넘어가는 경계를 읽습니다.

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

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

RISC-V image를 새 주소로 복사한 뒤 pointer, trap vector와 다른 hart의 PC까지 같은 주소 체계로 맞추는 순서는 무엇인가?

image byte copy와 pointer relocation, trap-vector relocation, secondary hart relocation은 서로 다른 작업이다. 각 단계가 새 주소를 게시하는 순서와 cache/fence가 다른 hart의 instruction fetch보다 앞서는지 확인한다.

board_init_r로 분기할 때 PC, SP, gp, tvec, gd와 모든 relocation target이 destination image를 가리키고 secondary hart가 old image에서 실행 중이지 않아야 한다. 이 문장을 기준으로 코드를 위에서 아래로 읽으면, 함수 이름을 외우는 대신 어느 시점에 어떤 상태를 신뢰할 수 있는지 판단할 수 있다.

객체와 주소가 놓이는 구조

그림 1. RISC-V relocation, trap frame과 secondary hart에서 입력, 내부 상태, 출력의 경계
입력과 전제내부 상태외부로 공개되는 결과
01s2 new SP
02s3 new gd
03s4 destination
04rela entries
05new trap vector
06secondary IPI
07relocated runtime

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

실행 흐름

그림 2. 정상 경로의 주요 호출과 상태 전달
new stack gd destination
copy text and data
apply .rela.dyn
relocate tvec
clear BSS
relocate secondary harts
cache maintenance
board_init_r

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

단계별 입력과 출력

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

#단계진입 시 신뢰할 상태성공 뒤 남아야 할 상태다음 소비자
01new stack gd destinationboot hart relocation과 software-interrupt 기반 secondary 대기 문맥s2 new SPcopy text and data
02copy text and datanew stack gd destination 완료 상태s3 new gdapply .rela.dyn
03apply .rela.dyncopy text and data 완료 상태s4 destinationrelocate tvec
04relocate tvecapply .rela.dyn 완료 상태rela entriesclear BSS
05clear BSSrelocate tvec 완료 상태new trap vectorrelocate secondary harts
06relocate secondary hartsclear BSS 완료 상태secondary IPIcache maintenance
07cache maintenancerelocate secondary harts 완료 상태relocated runtimeboard_init_r
08board_init_rcache maintenance 완료 상태relocated runtime최종 DRAM의 relocated RISC-V U-Boot 또는 다음 stage

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

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

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

그림 3. RISC-V image relocation의 source와 destination

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

높은 주소↓ 주소 감소
new stack + new gds2/s3에 보존된 runtime state
destination text/rodata/data_start부터 __bss_start까지 copy
.rela.dyn targetsdestination base로 pointer 보정
new BSSdestination에서 zero clear
old image + old tvec새 tvec 게시 뒤 수명 종료
낮은 주소예약 cursor 진행 방향
그림 4. relocate_code가 board_init_r까지 닫는 순서

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

01s2/s3/s4 인자 보존
02image copy
03.rela.dyn 순회
04tvec + reloc_off
05BSS clear
06secondary IPI
07cache fence
08jr board_init_r
그림 5. trap 전후 register frame

두 열의 객체는 이름이 비슷해도 생성 시점, 사용 가능한 API와 해제 책임이 다르다.

Trap entry

01x1-x31 stack 저장

02cause/epc/tval 읽기

03frame pointer를 a3로 전달

04handle_trap 호출

05새 epc 반환

Trap return

01epc CSR 갱신

02x1-x31 역순 복원

03stack frame 해제

04mret 또는 sret

05중단된 context 재개

원본 코드

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

commitece349ade2973e220f524ce59e59711cc919263fanchor · L283relocate_code:
269	mv	a0, zero
270	mv	a1, zero
271	j	board_init_r
272#endif
273
274#if !defined(CONFIG_XPL_BUILD)
275/*
276 * void relocate_code(addr_sp, gd, addr_moni)
277 *
278 * This "function" does not return, instead it continues in RAM
279 * after relocating the monitor code.
280 *
281 */
282.globl relocate_code
283relocate_code:
284	mv	s2, a0			/* save addr_sp */
285	mv	s3, a1			/* save addr of gd */
286	mv	s4, a2			/* save addr of destination */
287
288/*
289 *Set up the stack
290 */
291stack_setup:
292#if CONFIG_IS_ENABLED(SMP)
293	/* tp: hart id */
294	slli	t0, tp, CONFIG_STACK_SIZE_SHIFT
295	sub	sp, s2, t0
296#else
297	mv	sp, s2
298#endif
299
300	la	t0, _start
301	sub	t6, s4, t0		/* t6 <- relocation offset */
302	beq	t0, s4, clear_bss	/* skip relocation */
303
304	mv	t1, s4			/* t1 <- scratch for copy_loop */
305	la	t2, __bss_start		/* t2 <- source end address */
306
307copy_loop:
308	LREG	t5, 0(t0)
309	addi	t0, t0, REGBYTES
310	SREG	t5, 0(t1)
311	addi	t1, t1, REGBYTES
312	blt	t0, t2, copy_loop
313
314/*
315 * Update dynamic relocations after board_init_f
316 */
317fix_rela_dyn:
318	la	t1, __rel_dyn_start
319	la	t2, __rel_dyn_end
320	beq	t1, t2, clear_bss
321	add	t1, t1, t6		/* t1 <- rela_dyn_start in RAM */
322	add	t2, t2, t6		/* t2 <- rela_dyn_end in RAM */
323
3246:
325	LREG	t5, REGBYTES(t1)	/* t5 <-- relocation info:type */
326	li	t3, R_RISCV_RELATIVE	/* reloc type R_RISCV_RELATIVE */
327	bne	t5, t3, 8f		/* skip non-RISCV_RELOC entries */
328	LREG	t3, 0(t1)
329	LREG	t5, (REGBYTES * 2)(t1)	/* t5 <-- addend */
330	add	t5, t5, t6		/* t5 <-- location to fix up in RAM */
331	add	t3, t3, t6		/* t3 <-- location to fix up in RAM */
332	SREG	t5, 0(t3)
333	j	10f
334
3358:
336	la	t4, __dyn_sym_start
337	add	t4, t4, t6
338
3399:
340	srli	t0, t5, SYM_INDEX	/* t0 <--- sym table index */
341	andi	t5, t5, 0xFF		/* t5 <--- relocation type */
342	li	t3, RELOC_TYPE
343	bne	t5, t3, 10f		/* skip non-addned entries */
344
345	LREG	t3, 0(t1)
346	li	t5, SYM_SIZE
347	mul	t0, t0, t5
348	add	s5, t4, t0
349	LREG	t0, (REGBYTES * 2)(t1)	/* t0 <-- addend */
350	LREG	t5, REGBYTES(s5)
351	add	t5, t5, t0
352	add	t5, t5, t6		/* t5 <-- location to fix up in RAM */
353	add	t3, t3, t6		/* t3 <-- location to fix up in RAM */
354	SREG	t5, 0(t3)
35510:
356	addi	t1, t1, (REGBYTES * 3)
357	blt	t1, t2, 6b
358
359/*
360 * trap update
361*/
362	la	t0, trap_entry
363	add	t0, t0, t6
364	csrw	MODE_PREFIX(tvec), t0
365
366clear_bss:
367	la	t0, __bss_start		/* t0 <- rel __bss_start in FLASH */
368	add	t0, t0, t6		/* t0 <- rel __bss_start in RAM */
369	la	t1, __bss_end		/* t1 <- rel __bss_end in FLASH */
370	add	t1, t1, t6		/* t1 <- rel __bss_end in RAM */
371	beq	t0, t1, relocate_secondary_harts
372
373clbss_l:
374	SREG	zero, 0(t0)		/* clear loop... */
375	addi	t0, t0, REGBYTES
376	blt	t0, t1, clbss_l
377
378relocate_secondary_harts:
379#if CONFIG_IS_ENABLED(SMP)
380	/* send relocation IPI */
381	la	t0, secondary_hart_relocate
382	add	a0, t0, t6
383
384	/* store relocation offset */
385	mv	s5, t6
386
387	mv	a1, s2
388	mv	a2, s3
389	mv	a3, zero
390	jal	smp_call_function
391
392	/* hang if relocation of secondary harts has failed */
393	beqz	a0, 1f
394	mv	a1, a0
395	la	a0, secondary_harts_relocation_error
396	jal	printf
397	jal	hang
398
399	/* restore relocation offset */
4001:	mv	t6, s5
401#endif
402
403/*
404 * We are done. Do not return, instead branch to second part of board
405 * initialization, now running from RAM.
406 */
407call_board_init_r:
408	jal	invalidate_icache_all
409	jal	flush_dcache_all
410	la	t0, board_init_r        /* offset of board_init_r() */
411	add	t4, t0, t6		/* real address of board_init_r() */
412/*
413 * setup parameters for board_init_r
414 */
415	mv	a0, s3			/* gd_t */
416	mv	a1, s4			/* dest_addr */
417	mv	s0, zero		/* fp == NULL */
418
419/*
420 * jump to it ...
421 */
422	jr	t4			/* jump to board_init_r() */
423#endif /* !defined(CONFIG_XPL_BUILD) */
424
425#if CONFIG_IS_ENABLED(SMP)
426hart_out_of_bounds_loop:
427	/* Harts in this loop are out of bounds, increase CONFIG_NR_CPUS. */
428	wfi
429	j	hart_out_of_bounds_loop
430
431/* SMP relocation entry */
432secondary_hart_relocate:
433	/* a1: new sp */
434	/* a2: new gd */
435	/* tp: hart id */
436
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

발췌 범위와 읽기 기준

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

image byte copy와 pointer relocation, trap-vector relocation, secondary hart relocation은 서로 다른 작업이다. 각 단계가 새 주소를 게시하는 순서와 cache/fence가 다른 hart의 instruction fetch보다 앞서는지 확인한다.

283relocate_code:

a0=new SP, a1=new gd, a2=destination을 받아 RISC-V image copy와 ELF relocation을 시작한다.

332 SREG t5, 0(t3)

계산된 새 pointer를 destination image의 relocation target slot에 XLEN 폭으로 기록한다.

354 SREG t5, 0(t3)

계산된 새 pointer를 destination image의 relocation target slot에 XLEN 폭으로 기록한다.

363 add t0, t0, t6

old image의 trap_entry link address를 destination handler address로 바꾼다. 바로 뒤 CSR write 전에 mode bit 처리도 확인한다.

364 csrw MODE_PREFIX(tvec), t0

trap vector에도 relocation offset을 반영해 새 image handler를 가리키게 한다.

함께 읽어야 하는 원본 코드

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

01. trap frame을 저장하고 handle_trap으로 넘기는 entry

arch/riscv/cpu/mtrap.S 18-103줄이다.

filearch/riscv/cpu/mtrap.Sanchor · L31trap_entry:
18#define SREG		sw
19#define REGBYTES	4
20#else
21#define LREG		ld
22#define SREG		sd
23#define REGBYTES	8
24#endif
25
26	.text
27
28	/* trap entry */
29	.align 6
30	.global trap_entry
31trap_entry:
32	addi sp, sp, -32 * REGBYTES
33	SREG x1,   1 * REGBYTES(sp)
34	SREG x2,   2 * REGBYTES(sp)
35	SREG x3,   3 * REGBYTES(sp)
36	SREG x4,   4 * REGBYTES(sp)
37	SREG x5,   5 * REGBYTES(sp)
38	SREG x6,   6 * REGBYTES(sp)
39	SREG x7,   7 * REGBYTES(sp)
40	SREG x8,   8 * REGBYTES(sp)
41	SREG x9,   9 * REGBYTES(sp)
42	SREG x10, 10 * REGBYTES(sp)
43	SREG x11, 11 * REGBYTES(sp)
44	SREG x12, 12 * REGBYTES(sp)
45	SREG x13, 13 * REGBYTES(sp)
46	SREG x14, 14 * REGBYTES(sp)
47	SREG x15, 15 * REGBYTES(sp)
48	SREG x16, 16 * REGBYTES(sp)
49	SREG x17, 17 * REGBYTES(sp)
50	SREG x18, 18 * REGBYTES(sp)
51	SREG x19, 19 * REGBYTES(sp)
52	SREG x20, 20 * REGBYTES(sp)
53	SREG x21, 21 * REGBYTES(sp)
54	SREG x22, 22 * REGBYTES(sp)
55	SREG x23, 23 * REGBYTES(sp)
56	SREG x24, 24 * REGBYTES(sp)
57	SREG x25, 25 * REGBYTES(sp)
58	SREG x26, 26 * REGBYTES(sp)
59	SREG x27, 27 * REGBYTES(sp)
60	SREG x28, 28 * REGBYTES(sp)
61	SREG x29, 29 * REGBYTES(sp)
62	SREG x30, 30 * REGBYTES(sp)
63	SREG x31, 31 * REGBYTES(sp)
64	csrr a0, MODE_PREFIX(cause)
65	csrr a1, MODE_PREFIX(epc)
66	csrr a2, MODE_PREFIX(tval)
67	mv a3, sp
68	jal handle_trap
69	csrw MODE_PREFIX(epc), a0
70
71	LREG x1,   1 * REGBYTES(sp)
72	LREG x3,   3 * REGBYTES(sp)
73	LREG x4,   4 * REGBYTES(sp)
74	LREG x5,   5 * REGBYTES(sp)
75	LREG x6,   6 * REGBYTES(sp)
76	LREG x7,   7 * REGBYTES(sp)
77	LREG x8,   8 * REGBYTES(sp)
78	LREG x9,   9 * REGBYTES(sp)
79	LREG x10, 10 * REGBYTES(sp)
80	LREG x11, 11 * REGBYTES(sp)
81	LREG x12, 12 * REGBYTES(sp)
82	LREG x13, 13 * REGBYTES(sp)
83	LREG x14, 14 * REGBYTES(sp)
84	LREG x15, 15 * REGBYTES(sp)
85	LREG x16, 16 * REGBYTES(sp)
86	LREG x17, 17 * REGBYTES(sp)
87	LREG x18, 18 * REGBYTES(sp)
88	LREG x19, 19 * REGBYTES(sp)
89	LREG x20, 20 * REGBYTES(sp)
90	LREG x21, 21 * REGBYTES(sp)
91	LREG x22, 22 * REGBYTES(sp)
92	LREG x23, 23 * REGBYTES(sp)
93	LREG x24, 24 * REGBYTES(sp)
94	LREG x25, 25 * REGBYTES(sp)
95	LREG x26, 26 * REGBYTES(sp)
96	LREG x27, 27 * REGBYTES(sp)
97	LREG x28, 28 * REGBYTES(sp)
98	LREG x29, 29 * REGBYTES(sp)
99	LREG x30, 30 * REGBYTES(sp)
100	LREG x31, 31 * REGBYTES(sp)
101	LREG x2,   2 * REGBYTES(sp)
102	addi sp, sp, 32 * REGBYTES
103	MODE_PREFIX(ret)

02. secondary hart의 IPI 대기와 dispatch

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. RISC-V U-Boot linker section과 relocation table 배치

arch/riscv/cpu/u-boot.lds 46-89줄이다.

filearch/riscv/cpu/u-boot.ldsanchor · L64.rela.dyn
46	. = ALIGN(4);
47
48	__u_boot_list : {
49		KEEP(*(SORT(__u_boot_list*)));
50	}
51
52	. = ALIGN(8);
53
54	.efi_runtime_rel : {
55		__efi_runtime_rel_start = .;
56		*(.rel*.efi_runtime)
57		*(.rel*.efi_runtime.*)
58		__efi_runtime_rel_stop = .;
59	}
60
61	__image_copy_end = .;
62
63	/DISCARD/ : { *(.rela.plt*) }
64	.rela.dyn : {
65		__rel_dyn_start = .;
66		*(.rela*)
67		__rel_dyn_end = .;
68	}
69
70	. = ALIGN(8);
71
72	.dynsym : {
73		__dyn_sym_start = .;
74		*(.dynsym)
75		__dyn_sym_end = .;
76		. = ALIGN(8);
77	}
78
79	_end = .;
80
81	.bss : {
82		__bss_start = .;
83		*(.bss*)
84		. = ALIGN(8);
85		__bss_end = .;
86	}
87}
88
89ASSERT(_end % 8 == 0, "_end must be 8-byte aligned for device tree");

04. cause, epc, tval을 해석하는 C trap dispatcher

arch/riscv/lib/interrupts.c 177-248줄이다.

filearch/riscv/lib/interrupts.canchor · L185ulong handle_trap(ulong cause
177/*
178 * disable interrupts
179 */
180int disable_interrupts(void)
181{
182	return 0;
183}
184
185ulong handle_trap(ulong cause, ulong epc, ulong tval, struct pt_regs *regs)
186{
187	ulong is_irq, irq;
188
189	/* An UEFI application may have changed gd. Restore U-Boot's gd. */
190	efi_restore_gd();
191
192	if (cause == CAUSE_BREAKPOINT &&
193	    CONFIG_IS_ENABLED(SEMIHOSTING_FALLBACK)) {
194		ulong pre_addr = epc - 4, post_addr = epc + 4;
195
196		/* Check for prior and post addresses to be in same page. */
197		if ((pre_addr & ~(PAGE_SIZE - 1)) ==
198			(post_addr & ~(PAGE_SIZE - 1))) {
199			u32 pre = *(u32 *)pre_addr;
200			u32 post = *(u32 *)post_addr;
201
202			/* Check for semihosting, i.e.:
203			 * slli    zero,zero,0x1f
204			 * ebreak
205			 * srai    zero,zero,0x7
206			 */
207			if (pre == 0x01f01013 && post == 0x40705013) {
208				disable_semihosting();
209				epc += 4;
210				return epc;
211			}
212		}
213	}
214
215	is_irq = (cause & MCAUSE_INT);
216	irq = (cause & ~MCAUSE_INT);
217
218	if (is_irq) {
219		switch (irq) {
220		case IRQ_M_EXT:
221		case IRQ_S_EXT:
222			external_interrupt(0);	/* handle external interrupt */
223			break;
224		case IRQ_M_TIMER:
225		case IRQ_S_TIMER:
226			timer_interrupt(0);	/* handle timer interrupt */
227			break;
228		default:
229			_exit_trap(cause, epc, tval, regs);
230			break;
231		};
232	} else {
233		_exit_trap(cause, epc, tval, regs);
234	}
235
236	return epc;
237}
238
239/*
240 *Entry Point for PLIC Interrupt Handler
241 */
242__attribute__((weak)) void external_interrupt(struct pt_regs *regs)
243{
244}
245
246__attribute__((weak)) void timer_interrupt(struct pt_regs *regs)
247{
248}

주요 구문 해설

210 return epc;

assembly 복귀 코드가 CSR EPC에 다시 쓸 재개 주소를 반환한다. breakpoint semihosting 경로는 이 값을 4만큼 전진시킬 수 있다.

215 is_irq = (cause & MCAUSE_INT);

cause 최상위 bit만 분리해 asynchronous interrupt 여부를 만든다. 나머지 bit는 독립적인 cause number다.

216 irq = (cause & ~MCAUSE_INT);

interrupt marker를 제거해 external/timer interrupt switch에 사용할 작은 cause code를 얻는다.

236 return epc;

assembly 복귀 코드가 CSR EPC에 다시 쓸 재개 주소를 반환한다. breakpoint semihosting 경로는 이 값을 4만큼 전진시킬 수 있다.

내부 동작을 더 깊게 읽기

01

진입 조건을 먼저 고정한다

new stack gd destination에서 들어온 실행은 boot hart relocation과 software-interrupt 기반 secondary 대기 문맥에 놓여 있다. 이때 interrupt, MMU/cache, stack, heap 중 무엇이 이미 준비되었는지 소스의 호출자까지 올라가 확인한다. 같은 함수라도 SPL, relocation 전후, app thread처럼 호출 문맥이 달라지면 허용되는 API와 지연 시간이 달라진다.

image byte copy와 pointer relocation, trap-vector relocation, secondary hart relocation은 서로 다른 작업이다. 각 단계가 새 주소를 게시하는 순서와 cache/fence가 다른 hart의 instruction fetch보다 앞서는지 확인한다.

02

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

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

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

03

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

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

04

성공 flag와 실제 완료 시점을 맞춘다

copy text and data → apply .rela.dyn → relocate tvec → clear BSS → relocate secondary harts → cache maintenance 구간에서는 부분 초기화 상태가 생길 수 있다. flag, list insertion, callback 등록, storage write 완료 중 무엇이 성공의 기준인지 찾는다. hardware write나 DMA가 포함되면 함수 반환과 장치 완료가 같은 시점인지도 확인한다.

다른 CPU, 인터럽트 처리기, USB 완료 콜백, 셸 명령에서도 이 상태를 읽는다면 메모리 접근 순서와 잠금 범위를 함께 확인한다.

05

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

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

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

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

01

relocate_code의 s2, s3, s4는 새 stack, gd, destination을 끝까지 보존한다

RISC-V ABI의 saved register를 사용해 new SP, new gd, destination을 copy와 relocation loop 사이에 보존한다. 함수 호출과 macro가 임시 register를 덮어도 최종 board_init_r argument를 복구할 수 있어야 한다.

GDB에서 entry 직후 s2/s3/s4와 gd->relocaddr, gd->start_addr_sp를 비교하면 caller argument 오류와 내부 copy 오류를 분리할 수 있다.

02

copy 범위는 _start부터 __bss_start까지이며 BSS는 별도로 0으로 만든다

linker image byte가 존재하는 text/rodata/data를 destination으로 옮기고, file payload가 없는 BSS는 copy 뒤 새 주소에서 clear한다. source/destination overlap과 copy 방향은 address 비교로 검증해야 한다.

SPL/TPL 또는 XIP 설정에서는 section 배치가 달라질 수 있으므로 symbol 차이를 고정 상수로 쓰지 않고 최종 ELF에서 읽는다.

03

.rela.dyn은 R_RISCV_RELATIVE와 absolute symbol relocation을 구분한다

relocation loop는 entry type을 읽어 relative relocation이면 addend에 destination base를 더하고, R_RISCV_32/64이면 symbol value와 relocation offset을 조합한다. 지원하지 않는 type을 조용히 같은 방식으로 처리하면 pointer 일부만 손상될 수 있다.

readelf -rW u-boot 출력의 offset, type, addend를 몇 개 골라 relocation 전 source word와 relocation 후 destination word를 계산해 본다. 이 계산이 source 분석의 가장 직접적인 검증이다.

04

tvec도 relocation offset만큼 옮겨야 새 image에서 trap을 받는다

copy와 fixup 도중 trap vector는 old image의 trap_entry를 가리킨다. old image를 더 이상 정상 runtime으로 사용하지 않기 전에 tvec에 relocation offset을 반영해 새 handler address로 바꾼다.

이 줄이 빠지면 relocation 후 fault가 발생했을 때 old flash/SRAM handler로 되돌아가 새 stack/gd와 섞인다. 정상 부팅에서는 숨고 오류 경로에서만 나타나는 전형적인 stale entry 문제다.

05

trap_entry의 register frame은 32개 GPR과 cause, epc, tval을 묶는다

mtrap.S는 stack에 32 register 폭의 frame을 잡고 x1부터 x31을 저장한다. cause, epc, tval과 frame pointer를 C handle_trap에 넘기며 반환된 epc를 CSR에 다시 쓴 뒤 register를 복원하고 mret/sret한다.

trap handler가 stack이 없는 시점에 호출될 가능성을 따져야 한다. 초기 tvec 설치 전후, per-hart stack 선택 전후의 exception은 같은 handler라도 frame을 놓을 안전한 memory가 다르다.

06

secondary relocation은 boot hart copy 완료 뒤 IPI와 fence로 이어진다

boot hart가 destination image와 BSS를 완성한 뒤 secondary hart에 relocation 정보를 게시하고 IPI를 보낸다. secondary는 secondary_hart_loop에서 software interrupt를 확인하고 handler가 제공한 새 실행 위치로 이동한다.

I-cache invalidate와 D-cache flush는 copy byte가 다른 hart의 instruction fetch에 보이기 전에 끝나야 한다. coherent system이라고 가정해 barrier를 빼지 말고 architecture cache helper의 실제 구현을 확인한다.

07

board_init_r로 가는 jr은 relocation 함수의 마지막 소유권 이전이다

새 board_init_r 주소를 destination offset으로 계산하고 a0에 new gd, a1에 destination을 넣은 뒤 jr로 비복귀 분기한다. 이후 old stack, old gd, source image를 runtime 객체처럼 참조해서는 안 된다.

분기 직전 PC target, SP, gp, tvec, gd, BSS sample, relocation target sample을 한 번에 기록하면 relocation 완료 여부를 객관적으로 판정할 수 있다.

08

R_RISCV_RELATIVE entry 한 건을 destination 기준으로 계산한다

source base가 0x80000000, destination base가 0x87e00000이면 t6=0x07e00000이다. relocation offset 0x8006c018의 patch address는 0x87e6c018, link-time addend 0x8002a430에 t6를 더한 기록 값은 0x87e2a430이다. RV64라면 8-byte word 전체를 기록하고 alignment와 endianness를 확인한다.

readelf -rW u-boot 결과에서 선택한 entry를 relocation loop 직전/직후 memory와 비교한다. source word가 아니라 destination word가 바뀌어야 하며, 같은 entry가 두 번 적용되면 base가 중복 가산된다.

09

trap frame offset은 XLEN과 pt_regs 정의로 검산한다

RV64에서 register slot이 8 byte이고 x1부터 x31까지 31개를 저장하면 GPR 영역만 248 byte다. cause, epc, tval과 helper argument slot이 추가되므로 실제 frame size는 mtrap.S의 PT_SIZE_ON_STACK과 C struct pt_regs offset을 기준으로 계산해야 한다.

예를 들어 epc slot을 잘못 8 byte 밀면 C handler는 cause를 epc로 읽고 return 시 잘못된 PC로 mret/sret한다. assembly macro를 펼친 offset, generated asm-offsets와 debugger memory dump 세 가지를 대조한다.

10

tvec와 I-cache publication을 secondary hart 관점에서 확인한다

old trap_entry=0x80008100, relocation offset=0x07e00000이면 새 tvec는 0x87e08100이다. 이 source는 정렬된 trap_entry + t6를 그대로 쓰므로 하위 2bit가 0인 direct mode를 새로 선택한다. vectored mode를 유지하도록 확장한다면 기존 mode bit를 주소 계산과 분리해 다시 합쳐야 한다.

boot hart의 data copy 완료, relocation fixup, D-cache clean, fence.i 또는 remote I-cache invalidation, new entry/tvec 게시, IPI의 순서를 표로 만든다. secondary가 IPI는 받았지만 old instruction을 실행하는 문제는 이 순서 중 cache visibility가 빠졌을 때 생긴다.

객체와 수명

대상만들어지는 시점유효 범위확인할 조건
relocated image / .rela.dyn / trap framecopy text and databoard_init_r 또는 오류 정리 완료까지진입 전제와 상태 계약
입력 buffer / metadatanew stack gd destinationparse와 검증이 끝날 때까지길이, 정렬, 소유권, 변조 가능성
등록된 list / descriptorapply .rela.dyn하위 시스템을 종료하거나 다음 이미지에 제어권을 넘길 때까지중복 등록, 해제된 객체를 가리키는 포인터, 오류 후 자원 정리
hardware 또는 persistent state실제 write/probe가 완료된 뒤reset 또는 명시적 해제·갱신까지쓰기 완료, 시간 초과, 전원 차단, 이전 상태 복원
DRAM의 relocated RISC-V U-Bootlink/image 생성 시점다음 stage가 새 image로 교체할 때까지적재 주소, 진입점, 섹션, 헤더의 일치 여부

실패 지점과 증상

#조건관찰되는 증상먼저 볼 단계
01rela type 처리 오류특정 global pointer만 old addresscopy text and data
02tvec 미보정relocation 후 fault가 old handler로 진입apply .rela.dyn
03secondary IPI 순서 오류old code 또는 0 stack 실행relocate tvec
04cache maintenance 누락hart별 stale instructionclear BSS

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

소스 밖에서 확인할 증거

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

#목적명령 또는 계측판정 기준
01전처리 결과make <board>_defconfig && make V=1선택된 CONFIG와 실제 compile/link 명령을 보관한다.
02symbol과 section${CROSS_COMPILE}nm -n u-boot | grep 'relocate_code'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
    readelf -rW에서 RELATIVE entry 손계산

    copy text and data 진입 전후에 s2 new SP의 주소·크기·반환값과 timestamp를 함께 남긴다.

  2. 02
    trap_entry frame에서 cause/epc/tval과 x1-x31 확인

    apply .rela.dyn 진입 전후에 s3 new gd의 주소·크기·반환값과 timestamp를 함께 남긴다.

  3. 03
    분기 직전 PC/SP/gp/tvec/gd snapshot

    relocate tvec 진입 전후에 s4 destination의 주소·크기·반환값과 timestamp를 함께 남긴다.

  4. 04
    secondary_hart_loop와 IPI handler timestamp 대조

    clear BSS 진입 전후에 rela entries의 주소·크기·반환값과 timestamp를 함께 남긴다.

원문과 다음 글