QUESTION
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에서 실행 중이지 않아야 한다. 이 문장을 기준으로 코드를 위에서 아래로 읽으면, 함수 이름을 외우는 대신 어느 시점에 어떤 상태를 신뢰할 수 있는지 판단할 수 있다.
STRUCTURE
객체와 주소가 놓이는 구조
relocated image / .rela.dyn / trap frame를 중심에 놓고 왼쪽의 입력이 어떤 검사를 거쳐 오른쪽 결과로 공개되는지 표시했다. 실제 디버깅에서는 각 블록의 주소와 크기를 로그에 대입한다.
CALL PATH
실행 흐름
화살표는 단순 호출 순서만 뜻하지 않는다. 각 단계가 성공을 반환할 때 다음 단계가 읽을 수 있는 상태가 무엇인지 함께 확인한다. 오류 반환이 발생하면 바로 다음 화살표로 진행하지 않고 해당 단계의 정리 경로를 따라간다.
STATE LEDGER
단계별 입력과 출력
각 단계의 입력과 처리 후 바뀌는 상태를 정리한 표다. 사용 중인 소스 버전에서 다른 필드를 변경한다면 그에 맞춰 표도 수정한다.
| # | 단계 | 진입 시 신뢰할 상태 | 성공 뒤 남아야 할 상태 | 다음 소비자 |
|---|---|---|---|---|
| 01 | new stack gd destination | boot hart relocation과 software-interrupt 기반 secondary 대기 문맥 | s2 new SP | copy text and data |
| 02 | copy text and data | new stack gd destination 완료 상태 | s3 new gd | apply .rela.dyn |
| 03 | apply .rela.dyn | copy text and data 완료 상태 | s4 destination | relocate tvec |
| 04 | relocate tvec | apply .rela.dyn 완료 상태 | rela entries | clear BSS |
| 05 | clear BSS | relocate tvec 완료 상태 | new trap vector | relocate secondary harts |
| 06 | relocate secondary harts | clear BSS 완료 상태 | secondary IPI | cache maintenance |
| 07 | cache maintenance | relocate secondary harts 완료 상태 | relocated runtime | board_init_r |
| 08 | board_init_r | cache maintenance 완료 상태 | relocated runtime | 최종 DRAM의 relocated RISC-V U-Boot 또는 다음 stage |
분석 범위의 전제와 비교해, 다음 단계에 잘못된 입력을 넘긴 부분부터 조사한다.
VISUAL WALKTHROUGH
주소와 객체의 이동을 그림으로 읽기
아래 그림은 주소, 객체를 사용하는 기간, 제어권을 넘기는 시점을 중심으로 정리했다. 실제 주소와 크기는 사용 중인 보드의 bdinfo, 맵 파일과 링커 심벌로 확인한다.
크기는 개념 비율이다. 실제 분석에서는 map, symbol과 runtime 출력의 시작·끝 주소를 대입한다.
호출 이름만 나열하지 않고, 각 단계가 다음 단계에 넘기는 상태를 기준으로 실행 순서를 표시한다.
두 열의 객체는 이름이 비슷해도 생성 시점, 사용 가능한 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 재개
UPSTREAM SOURCE
원본 코드
아래 코드는 U-Boot v2026.07의 arch/riscv/cpu/start.S에서 269-460줄을 그대로 가져온 것이다. 설명을 위해 실제 코드를 가짜 의사 코드로 바꾸지 않았다.
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
READING CONTRACT
발췌 범위와 읽기 기준
원문 269-460줄에는 앞뒤 문맥이 포함됩니다. 기준 구문 relocate_code:은 L283에 있습니다. 발췌 첫 줄을 함수 정의의 시작으로 해석하지 않습니다.
image byte copy와 pointer relocation, trap-vector relocation, secondary hart relocation은 서로 다른 작업이다. 각 단계가 새 주소를 게시하는 순서와 cache/fence가 다른 hart의 instruction fetch보다 앞서는지 확인한다.
relocate_code:a0=new SP, a1=new gd, a2=destination을 받아 RISC-V image copy와 ELF relocation을 시작한다.
SREG t5, 0(t3)계산된 새 pointer를 destination image의 relocation target slot에 XLEN 폭으로 기록한다.
SREG t5, 0(t3)계산된 새 pointer를 destination image의 relocation target slot에 XLEN 폭으로 기록한다.
add t0, t0, t6old image의 trap_entry link address를 destination handler address로 바꾼다. 바로 뒤 CSR write 전에 mode bit 처리도 확인한다.
csrw MODE_PREFIX(tvec), t0trap vector에도 relocation offset을 반영해 새 image handler를 가리키게 한다.
RELATED SOURCE
함께 읽어야 하는 원본 코드
첫 코드 조각만으로 동작이 완성되지 않는 경우 호출 매크로, 객체 정의와 실제 실행 목록을 같은 페이지에 묶었다. 각 조각은 같은 기준 commit에서 가져왔다.
01. trap frame을 저장하고 handle_trap으로 넘기는 entry
arch/riscv/cpu/mtrap.S 18-103줄이다.
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줄이다.
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줄이다.
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줄이다.
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}
주요 구문 해설
return epc;assembly 복귀 코드가 CSR EPC에 다시 쓸 재개 주소를 반환한다. breakpoint semihosting 경로는 이 값을 4만큼 전진시킬 수 있다.
is_irq = (cause & MCAUSE_INT);cause 최상위 bit만 분리해 asynchronous interrupt 여부를 만든다. 나머지 bit는 독립적인 cause number다.
irq = (cause & ~MCAUSE_INT);interrupt marker를 제거해 external/timer interrupt switch에 사용할 작은 cause code를 얻는다.
return epc;assembly 복귀 코드가 CSR EPC에 다시 쓸 재개 주소를 반환한다. breakpoint semihosting 경로는 이 값을 4만큼 전진시킬 수 있다.
DETAILS
내부 동작을 더 깊게 읽기
진입 조건을 먼저 고정한다
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보다 앞서는지 확인한다.
중심 객체의 생성과 공개를 나눈다
이 글의 중심 객체는 relocated image / .rela.dyn / trap frame다. 메모리를 확보한 시점, 필드를 채운 시점, 전역 list나 다른 subsystem에 공개한 시점을 구분한다. 공개 뒤 오류가 발생한다면 목록에서 제거하고 child, buffer, reference를 역순으로 정리하는지 확인한다.
빌드 산출물 관점에서는 DRAM의 relocated RISC-V U-Boot 안에 해당 symbol과 section이 실제로 포함되었는지도 map과 objdump로 검증한다.
주소, 크기와 정렬을 계산한다
부트 코드에서는 주소 계산 오류를 주의해야 한다. 원본과 목적지의 주소 범위, 헤더에 기록된 데이터 크기, 블록·페이지 단위를 표로 정리하고 각 구간의 끝 주소를 계산한다. 끝 주소가 start + size - 1처럼 마지막 바이트를 가리키는지, 구간 바로 다음 주소를 가리키는지는 API 규약에서 확인한다.
성공 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 완료 콜백, 셸 명령에서도 이 상태를 읽는다면 메모리 접근 순서와 잠금 범위를 함께 확인한다.
제어권을 넘기기 전에 확인할 조건을 정리한다
정상 경로는 board_init_r에서 끝난다. 이 단계가 함수 반환인지, scheduler 전환인지, 다른 image로의 비복귀 분기인지 구분한다. 비복귀 handoff라면 cache clean/invalidate, interrupt disable, 장치 quiesce, argument register와 FDT 또는 image address가 최종 점검 항목이다.
호출한 함수로 돌아오는 경로라면 오류와 일부 처리만 완료된 상태를 구분하는지 확인한다. 이어서 다음 후보를 시도할지, 복구 절차를 실행할지 살펴본다.
IMPLEMENTATION NOTES
구현을 읽을 때 놓치기 쉬운 부분
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 오류를 분리할 수 있다.
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에서 읽는다.
.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 분석의 가장 직접적인 검증이다.
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 문제다.
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가 다르다.
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의 실제 구현을 확인한다.
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 완료 여부를 객관적으로 판정할 수 있다.
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가 중복 가산된다.
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 세 가지를 대조한다.
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가 빠졌을 때 생긴다.
OBJECT LIFETIME
객체와 수명
| 대상 | 만들어지는 시점 | 유효 범위 | 확인할 조건 |
|---|---|---|---|
relocated image / .rela.dyn / trap frame | copy text and data | board_init_r 또는 오류 정리 완료까지 | 진입 전제와 상태 계약 |
| 입력 buffer / metadata | new stack gd destination | parse와 검증이 끝날 때까지 | 길이, 정렬, 소유권, 변조 가능성 |
| 등록된 list / descriptor | apply .rela.dyn | 하위 시스템을 종료하거나 다음 이미지에 제어권을 넘길 때까지 | 중복 등록, 해제된 객체를 가리키는 포인터, 오류 후 자원 정리 |
| hardware 또는 persistent state | 실제 write/probe가 완료된 뒤 | reset 또는 명시적 해제·갱신까지 | 쓰기 완료, 시간 초과, 전원 차단, 이전 상태 복원 |
DRAM의 relocated RISC-V U-Boot | link/image 생성 시점 | 다음 stage가 새 image로 교체할 때까지 | 적재 주소, 진입점, 섹션, 헤더의 일치 여부 |
FAILURE PATH
실패 지점과 증상
| # | 조건 | 관찰되는 증상 | 먼저 볼 단계 |
|---|---|---|---|
| 01 | rela type 처리 오류 | 특정 global pointer만 old address | copy text and data |
| 02 | tvec 미보정 | relocation 후 fault가 old handler로 진입 | apply .rela.dyn |
| 03 | secondary IPI 순서 오류 | old code 또는 0 stack 실행 | relocate tvec |
| 04 | cache maintenance 누락 | hart별 stale instruction | clear BSS |
로그가 끊긴 함수뿐 아니라 그 함수에 주소, 크기, 플래그를 전달한 앞 단계도 조사한다. 오류 처리 후 등록된 객체나 하드웨어 설정이 그대로 남아 있는지도 확인한다.
EVIDENCE
소스 밖에서 확인할 증거
소스에서 파악한 동작을 실제 빌드 결과와 대상 보드의 로그로 확인한다. 아래 명령의 보드 이름과 툴체인 접두사는 사용 중인 빌드 환경에 맞게 바꾼다.
| # | 목적 | 명령 또는 계측 | 판정 기준 |
|---|---|---|---|
| 01 | 전처리 결과 | make <board>_defconfig && make V=1 | 선택된 CONFIG와 실제 compile/link 명령을 보관한다. |
| 02 | symbol과 section | ${CROSS_COMPILE}nm -n u-boot | grep 'relocate_code' | 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에서 RELATIVE entry 손계산
copy text and data진입 전후에 s2 new SP의 주소·크기·반환값과 timestamp를 함께 남긴다. - 02trap_entry frame에서 cause/epc/tval과 x1-x31 확인
apply .rela.dyn진입 전후에 s3 new gd의 주소·크기·반환값과 timestamp를 함께 남긴다. - 03분기 직전 PC/SP/gp/tvec/gd snapshot
relocate tvec진입 전후에 s4 destination의 주소·크기·반환값과 timestamp를 함께 남긴다. - 04secondary_hart_loop와 IPI handler timestamp 대조
clear BSS진입 전후에 rela entries의 주소·크기·반환값과 timestamp를 함께 남긴다.
PRIMARY REFERENCES