QUESTION
firmware가 넘긴 a0=hartid, a1=DTB가 언제 kernel virtual address, 정상 trap vector와 C stack을 얻는가?
RISC-V Linux의 entry contract는 register 두 개로 시작한다. a0에는 boot hart ID, a1에는 DTB의 물리 주소가 들어온다. S-mode kernel이라면 보통 OpenSBI 같은 firmware가 이 계약을 만들고, M-mode kernel은 mhartid와 PMP를 직접 다룬다.
setup_vm()은 MMU가 꺼진 물리 주소 실행 중 early page table을 작성한다. 이어 relocate_enable_mmu()가 return address와 trap vector를 virtual address로 보정하고, 먼저 trampoline_pg_dir, 그 다음 실제 kernel page table을 SATP에 넣는다.
첫 SATP write 직후 fetch가 가상 주소로 바뀔 수 있으므로 trampoline은 현재 실행 superpage를 물리/가상 양쪽에서 같은 내용으로 보이게 만든다. 이 겹침이 없으면 정상 trap handler도 console도 준비되기 전에 멈춘다.
ENTRY CONTRACT
들어오는 상태와 내보내는 상태
| 경계 | 입력 | 이 경계가 완성하는 상태 |
|---|---|---|
| Firmware handoff | a0=hartid, a1=DTB PA | interrupt masked entry |
| setup_vm | MMU off, writable early page tables | trampoline_pg_dir와 early_pg_dir |
| relocate_enable_mmu | page-table root PA | virtual RA, GP, SATP |
| start_kernel | tp=init_task, kernel stack, trap vector | generic Linux init |
ARCHITECTURE FIGURES
주소와 register를 먼저 그려 본다
trampoline은 주소 공간을 건너는 한 발짜리 다리이고, early/kernel page table이 이후 실행 범위를 담당한다.
firmware가 넘김
kernel이 재배치
firmware argument는 page-table 생성과 C 환경 복구 사이에 보존되어야 한다.
kernel image의 physical load range와 link-time virtual range는 page table에서 같은 byte로 연결된다.
SOURCE 01
RISC-V Image header와 loader-visible entry
첫 instruction과 64-byte header는 UEFI와 일반 bootloader가 image 크기, load offset, magic을 판별하는 ABI다.
원본: arch/riscv/kernel/head.S 21-70줄
21SYM_CODE_START(_start)
22 /*
23 * Image header expected by Linux boot-loaders. The image header data
24 * structure is described in asm/image.h.
25 * Do not modify it without modifying the structure and all bootloaders
26 * that expects this header format!!
27 */
28#ifdef CONFIG_EFI
29 /*
30 * This instruction decodes to "MZ" ASCII required by UEFI.
31 */
32 c.li s4,-13
33 j _start_kernel
34#else
35 /* jump to start kernel */
36 j _start_kernel
37 /* reserved */
38 .word 0
39#endif
40 .balign 8
41#ifdef CONFIG_RISCV_M_MODE
42 /* Image load offset (0MB) from start of RAM for M-mode */
43 .dword 0
44#else
45#if __riscv_xlen == 64
46 /* Image load offset(2MB) from start of RAM */
47 .dword 0x200000
48#else
49 /* Image load offset(4MB) from start of RAM */
50 .dword 0x400000
51#endif
52#endif
53 /* Effective size of kernel image */
54 .dword _end - _start
55 .dword __HEAD_FLAGS
56 .word RISCV_HEADER_VERSION
57 .word 0
58 .dword 0
59 .ascii RISCV_IMAGE_MAGIC
60 .balign 4
61 .ascii RISCV_IMAGE_MAGIC2
62#ifdef CONFIG_EFI
63 .word pe_head_start - _start
64pe_head_start:
65
66 __EFI_PE_HEADER
67#else
68 .word 0
69#endif
70 주요 구문 해설
SYM_CODE_START(_start)커널 이미지의 첫 코드 심벌 _start를 정의합니다. 이 위치의 명령과 헤더 필드를 부트로더가 읽으므로, 뒤의 _start_kernel 실행부와 구분합니다. 이 표기는 빌드 도구에 코드 심벌과 정렬을 알려 주며 그 자체가 런타임 함수 호출은 아닙니다.
#ifdef CONFIG_EFIEFI 지원 빌드에서는 이미지 첫 명령의 바이트를 PE/COFF의 MZ 표시로도 읽히게 만듭니다. 이어지는 분기는 실제 커널 시작으로 연결하며, EFI가 없으면 일반 시작 분기와 예약 필드를 사용합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
c.li s4,-13compressed instruction byte가 ASCII MZ로 해석되도록 만들어 UEFI PE/COFF image signature를 만족하면서 실제로는 무해한 register load를 수행한다.
j _start_kernelEFI용 MZ 식별 명령 다음에서 실제 초기화 코드 _start_kernel로 이동합니다. 그 사이의 이미지 헤더를 CPU 명령으로 실행하지 않도록 건너뜁니다.
#elseEFI 헤더를 만들지 않는 이미지입니다. 바로 _start_kernel로 분기하고 뒤의 예약 필드를 0으로 채워 일반 RISC-V Image 헤더 배치를 유지합니다.
j _start_kernelEFI를 사용하지 않는 이미지도 _start_kernel로 바로 이동합니다. 헤더의 크기·적재 위치 필드는 이 분기 뒤에 데이터로 놓입니다.
.word 0헤더의 예약된 4바이트를 0으로 채웁니다. 실행 명령이 아니라 부트로더와 약속된 이미지 형식의 일부입니다.
#endif28행에서 시작한 선택 구간을 마칩니다. EFI 지원 빌드에서는 이미지 첫 명령의 바이트를 PE/COFF의 MZ 표시로도 읽히게 만듭니다. 이어지는 분기는 실제 커널 시작으로 연결하며, EFI가 없으면 일반 시작 분기와 예약 필드를 사용합니다.
.balign 8다음 헤더 필드가 8바이트 경계에서 시작하도록 출력 위치를 맞춥니다. CPU가 실행 중에 주소를 정렬하는 명령은 아닙니다.
#ifdef CONFIG_RISCV_M_MODEM-mode Linux는 가장 높은 machine 권한에서 실행하는 구성입니다. 이 이미지의 적재 오프셋은 RAM 시작 기준 0으로 기록하며, 일반 S-mode 커널은 비트수에 따라 2MiB 또는 4MiB를 기록합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
.dword 0M-mode 커널의 적재 오프셋을 64비트 값 0으로 기록합니다. 이 형식에서는 RAM 시작점으로부터 별도 오프셋을 요구하지 않습니다.
#elseM-mode가 아닌 일반 S-mode 이미지입니다. 바로 아래에서 RV64/RV32에 따라 부트로더가 사용할 RAM 기준 적재 오프셋을 다르게 기록합니다.
#if __riscv_xlen == 64S-mode 이미지가 RV64인지 RV32인지에 따라 부트로더가 사용할 적재 오프셋을 고릅니다. RV64는 2MiB, RV32는 4MiB로 기록하여 해당 초기 매핑의 정렬 요구를 맞춥니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
.dword 0x20000064-bit S-mode kernel이 RAM 시작에서 2MiB offset에 적재되기를 기대하는 Image header field다.
#elseRV32 S-mode 이미지의 적재 오프셋을 4MiB로 기록합니다. RV64 분기의 2MiB 값을 그대로 쓰는 것이 아니라 32비트 초기 매핑의 요구에 맞춥니다.
.dword 0x40000032비트 S-mode 이미지의 적재 오프셋 4MiB를 헤더에 기록합니다. 바로 위의 RV64 경로는 2MiB를 사용합니다.
#endif45행에서 시작한 선택 구간을 마칩니다. S-mode 이미지가 RV64인지 RV32인지에 따라 부트로더가 사용할 적재 오프셋을 고릅니다. RV64는 2MiB, RV32는 4MiB로 기록하여 해당 초기 매핑의 정렬 요구를 맞춥니다.
#endif41행에서 시작한 선택 구간을 마칩니다. M-mode Linux는 가장 높은 machine 권한에서 실행하는 구성입니다. 이 이미지의 적재 오프셋은 RAM 시작 기준 0으로 기록하며, 일반 S-mode 커널은 비트수에 따라 2MiB 또는 4MiB를 기록합니다.
.dword _end - _start최종 image의 유효 크기를 linker symbol 차이로 기록한다.
.dword __HEAD_FLAGS이미지의 바이트 순서 등을 나타내는 __HEAD_FLAGS를 64비트 헤더 필드에 넣습니다. CPU 상태 레지스터에 쓰는 값은 아닙니다.
.word RISCV_HEADER_VERSION부트로더가 헤더 형식을 구분할 수 있도록 RISCV_HEADER_VERSION을 32비트 값으로 기록합니다.
.word 0헤더의 예약된 32비트 필드를 0으로 둡니다. 현재 형식에서 사용하지 않는 자리입니다.
.dword 0헤더의 예약된 64비트 필드를 0으로 둡니다. 뒤에는 RISC-V 이미지임을 식별하는 문자열이 이어집니다.
.ascii RISCV_IMAGE_MAGICbootloader가 RISC-V Linux Image인지 확인하는 magic field다.
.balign 4두 번째 이미지 식별 문자열이 4바이트 경계에 놓이도록 패딩을 넣습니다. 헤더 필드의 위치를 맞추는 어셈블러 지시자입니다.
.ascii RISCV_IMAGE_MAGIC2bootloader가 RISC-V Linux Image인지 확인하는 magic field다.
#ifdef CONFIG_EFIEFI 빌드에서는 Image 헤더에 PE/COFF 헤더의 상대 위치를 넣고 실제 EFI 헤더도 출력합니다. 비EFI 빌드는 해당 필드를 0으로 두며 추가 헤더를 넣지 않습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
.word pe_head_start - _start이미지 시작부터 PE 헤더까지의 바이트 거리를 기록합니다. UEFI 로더는 이 오프셋으로 PE 형식의 정보를 찾습니다.
pe_head_start:PE 헤더가 시작되는 위치에 이름을 붙입니다. 앞줄의 pe_head_start - _start 계산에 쓰이는 주소입니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
__EFI_PE_HEADEREFI PE/COFF 헤더를 만드는 매크로를 펼칩니다. UEFI가 이미지의 진입점과 코드·데이터 섹션을 해석하는 데 필요한 정보를 배치합니다.
#elseEFI 지원이 없으므로 PE/COFF 헤더를 추가하지 않고 해당 상대 위치 필드를 0으로 둡니다. 앞의 공통 Linux Image 정보는 그대로 유지됩니다.
.word 0EFI를 사용하지 않으면 PE 헤더 오프셋 필드를 0으로 둡니다. 이 이미지에는 UEFI용 PE 헤더가 없다는 뜻입니다.
#endif62행에서 시작한 선택 구간을 마칩니다. EFI 빌드에서는 Image 헤더에 PE/COFF 헤더의 상대 위치를 넣고 실제 EFI 헤더도 출력합니다. 비EFI 빌드는 해당 필드를 0으로 두며 추가 헤더를 넣지 않습니다.
SOURCE 02
return address를 보정하고 trampoline에서 kernel SATP로 교체
relocate_enable_mmu()는 page-table root를 받아 현재 물리 실행을 link-time virtual 실행으로 바꾼다.
원본: arch/riscv/kernel/head.S 73-126줄
73 .global relocate_enable_mmu
74relocate_enable_mmu:
75 /* Relocate return address */
76 la a1, kernel_map
77 XIP_FIXUP_OFFSET a1
78 REG_L a1, KERNEL_MAP_VIRT_ADDR(a1)
79 la a2, _start
80 sub a1, a1, a2
81 add ra, ra, a1
82
83 /* Point stvec to virtual address of intruction after satp write */
84 la a2, 1f
85 add a2, a2, a1
86 csrw CSR_TVEC, a2
87
88 /* Compute satp for kernel page tables, but don't load it yet */
89 srl a2, a0, PAGE_SHIFT
90 la a1, satp_mode
91 XIP_FIXUP_OFFSET a1
92 REG_L a1, 0(a1)
93 or a2, a2, a1
94
95 /*
96 * Load trampoline page directory, which will cause us to trap to
97 * stvec if VA != PA, or simply fall through if VA == PA. We need a
98 * full fence here because setup_vm() just wrote these PTEs and we need
99 * to ensure the new translations are in use.
100 */
101 la a0, trampoline_pg_dir
102 XIP_FIXUP_OFFSET a0
103 srl a0, a0, PAGE_SHIFT
104 or a0, a0, a1
105 sfence.vma
106 csrw CSR_SATP, a0
107.align 2
1081:
109 /* Set trap vector to spin forever to help debug */
110 la a0, .Lsecondary_park
111 csrw CSR_TVEC, a0
112
113 /* Reload the global pointer */
114 load_global_pointer
115
116 /*
117 * Switch to kernel page tables. A full fence is necessary in order to
118 * avoid using the trampoline translations, which are only correct for
119 * the first superpage. Fetching the fence is guaranteed to work
120 * because that first superpage is translated the same way.
121 */
122 csrw CSR_SATP, a2
123 sfence.vma
124
125 ret
126#endif /* CONFIG_MMU */주요 구문 해설
.global relocate_enable_mmurelocate_enable_mmu를 다른 코드에서도 참조할 수 있는 전역 심벌로 내보냅니다. 이 지시자 자체가 MMU를 켜지는 않습니다.
relocate_enable_mmu:페이지 테이블을 선택하고 물리 주소 실행에서 가상 주소 실행으로 넘어가는 루틴의 시작 위치입니다. a0에는 사용할 페이지 테이블의 주소가 들어옵니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
la a1, kernel_map커널의 물리·가상 주소 배치를 담은 kernel_map의 현재 접근 가능한 주소를 a1에 넣습니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
XIP_FIXUP_OFFSET a1XIP 빌드에서는 a1이 플래시의 링크 위치가 아니라 RAM에 있는 kernel_map 데이터를 가리키도록 보정합니다. 일반 빌드에서는 이 매크로가 비어 있습니다.
REG_L a1, KERNEL_MAP_VIRT_ADDR(a1)kernel_map.virt_addr를 읽어 a1에 넣습니다. 이것이 MMU 전환 후 커널이 실행될 가상 시작 주소입니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
la a2, _start현재 MMU 전환 전 환경에서 _start의 주소를 a2에 넣습니다. 다음 줄에서 목표 가상 주소와의 차이를 계산합니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
sub a1, a1, a2목표 가상 시작 주소에서 현재 _start 주소를 빼서 주소 보정량을 만듭니다. 이어서 복귀 주소 ra에도 이 차이를 더합니다.
add ra, ra, a1함수 복귀 주소를 physical-to-virtual offset만큼 옮긴다. MMU enable 뒤 ret가 virtual code로 돌아가게 한다.
la a2, 1f첫 SATP 전환 직후의 숫자 레이블 1 주소를 a2에 넣습니다. 1f의 f는 앞으로 나오는 1번 레이블이라는 뜻입니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
add a2, a2, a1전환 후 사용할 가상 주소 보정량을 더합니다. 이 주소를 트랩 벡터에 넣어 물리 주소와 가상 주소가 다른 경우에도 전환을 이어갑니다.
csrw CSR_TVEC, a2첫 SATP write가 예상대로 fall-through하지 않을 때도 virtual continuation으로 제어를 옮길 임시 trap target을 둔다.
srl a2, a0, PAGE_SHIFT인자로 받은 페이지 테이블 주소를 PAGE_SHIFT만큼 오른쪽으로 밀어 SATP의 PPN, 즉 물리 페이지 번호를 만듭니다.
la a1, satp_mode선택한 주소 변환 방식이 저장된 satp_mode의 주소를 a1에 넣습니다. RV64에서는 Sv39·Sv48·Sv57 등의 선택이 이 값에 반영됩니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
XIP_FIXUP_OFFSET a1XIP에서는 satp_mode를 RAM 쪽 주소로 보정합니다. 플래시에 놓인 초기 데이터 복사본 대신 현재 값을 읽기 위한 처리입니다.
REG_L a1, 0(a1)선택된 SATP 모드 값을 a1으로 읽습니다. 바로 앞에서 만든 페이지 테이블 PPN과 합칠 값입니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
or a2, a2, a1페이지 테이블 PPN과 모드 비트를 OR하여 최종 SATP 값을 a2에 보관합니다. 여기서는 아직 SATP 레지스터를 바꾸지 않습니다.
la a0, trampoline_pg_dir현재 instruction이 있는 첫 superpage를 주소 전환 양쪽에서 접근 가능하게 하는 최소 page table이다.
XIP_FIXUP_OFFSET a0XIP에서는 임시 trampoline_pg_dir가 실제로 놓인 RAM 주소로 a0를 보정합니다.
srl a0, a0, PAGE_SHIFT임시 페이지 테이블 주소에서 페이지 내부 오프셋을 제거하여 SATP에 넣을 물리 페이지 번호를 만듭니다.
or a0, a0, a1임시 페이지 테이블 PPN에 앞서 읽은 주소 변환 모드 비트를 합칩니다. 다음 SATP 쓰기에 사용할 값입니다.
sfence.vma새 SATP와 PTE를 이후 fetch/load가 확실히 사용하도록 translation cache를 정리한다.
csrw CSR_SATP, a0trampoline root를 활성화하는 첫 주소 공간 전환이다.
.align 2트랩 도착점인 다음 레이블을 4바이트 경계에 맞춥니다. RISC-V의 .align 2는 2의 2제곱 바이트 정렬을 뜻합니다.
1:임시 주소 변환을 켠 뒤 실행을 이어가는 위치입니다. 물리·가상 주소가 같으면 순서대로 도착하고, 다르면 미리 설정한 트랩 벡터를 통해 도착합니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
la a0, .Lsecondary_park예상하지 못한 트랩이 발생했을 때 멈춰 있을 .Lsecondary_park 주소를 a0에 준비합니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
csrw CSR_TVEC, a0트랩 벡터를 정지 루프로 바꿉니다. MMU 전환용 임시 트랩 주소의 역할은 끝났으며, 정상 예외 벡터는 호출자가 나중에 설치합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
load_global_pointervirtual address 실행으로 바뀐 뒤 gp를 새 주소 기준으로 다시 적재한다.
csrw CSR_SATP, a2최소 trampoline을 실제 kernel page table로 교체한다.
sfence.vma새 SATP와 PTE를 이후 fetch/load가 확실히 사용하도록 translation cache를 정리한다.
ret가상 주소로 보정해 둔 ra로 돌아갑니다. 일반 함수 복귀이며, 예외 복귀 명령 sret처럼 권한 모드나 상태 레지스터를 복원하지는 않습니다.
#endif /* CONFIG_MMU */72행에서 시작한 선택 구간을 마칩니다. MMU가 있는 커널은 초기 페이지 표와 SATP를 설치한 뒤 가상 주소로 실행을 옮길 relocate_enable_mmu를 만듭니다. MMU가 없는 커널에는 주소 변환을 켜는 이 함수가 없습니다.
SOURCE 03
boot hart의 BSS, stack, setup_vm과 start_kernel 진입
boot hart를 고른 뒤 BSS, tp, stack과 DTB argument를 준비하고 page table을 만든다. 최종적으로 trap vector를 설치한 뒤 generic start_kernel로 tail call한다.
원본: arch/riscv/kernel/head.S 203-340줄
203SYM_CODE_START(_start_kernel)
204 /* Mask all interrupts */
205 csrw CSR_IE, zero
206 csrw CSR_IP, zero
207
208#ifdef CONFIG_RISCV_M_MODE
209 /* flush the instruction cache */
210 fence.i
211
212 /* Reset all registers except ra, a0, a1 */
213 call reset_regs
214
215 /*
216 * Setup a PMP to permit access to all of memory. Some machines may
217 * not implement PMPs, so we set up a quick trap handler to just skip
218 * touching the PMPs on any trap.
219 */
220 la a0, .Lpmp_done
221 csrw CSR_TVEC, a0
222
223 li a0, -1
224 csrw CSR_PMPADDR0, a0
225 li a0, (PMP_A_NAPOT | PMP_R | PMP_W | PMP_X)
226 csrw CSR_PMPCFG0, a0
227.align 2
228.Lpmp_done:
229
230 /*
231 * The hartid in a0 is expected later on, and we have no firmware
232 * to hand it to us.
233 */
234 csrr a0, CSR_MHARTID
235#else
236 /* Enable time CSR */
237 li t0, 0x2
238 csrw CSR_SCOUNTEREN, t0
239#endif /* CONFIG_RISCV_M_MODE */
240
241 /* Load the global pointer */
242 load_global_pointer
243
244 /*
245 * Disable FPU & VECTOR to detect illegal usage of
246 * floating point or vector in kernel space
247 */
248 li t0, SR_FS_VS
249 csrc CSR_STATUS, t0
250
251#ifdef CONFIG_RISCV_BOOT_SPINWAIT
252 li t0, CONFIG_NR_CPUS
253 blt a0, t0, .Lgood_cores
254 tail .Lsecondary_park
255.Lgood_cores:
256
257 /* The lottery system is only required for spinwait booting method */
258#ifndef CONFIG_XIP_KERNEL
259 /* Pick one hart to run the main boot sequence */
260 la a3, hart_lottery
261 li a2, 1
262 amoadd.w a3, a2, (a3)
263 bnez a3, .Lsecondary_start
264
265#else
266 /* hart_lottery in flash contains a magic number */
267 la a3, hart_lottery
268 mv a2, a3
269 XIP_FIXUP_OFFSET a2
270 XIP_FIXUP_FLASH_OFFSET a3
271 lw t1, (a3)
272 amoswap.w t0, t1, (a2)
273 /* first time here if hart_lottery in RAM is not set */
274 beq t0, t1, .Lsecondary_start
275
276#endif /* CONFIG_XIP */
277#endif /* CONFIG_RISCV_BOOT_SPINWAIT */
278
279#ifdef CONFIG_XIP_KERNEL
280 la sp, _end + THREAD_SIZE
281 XIP_FIXUP_OFFSET sp
282 mv s0, a0
283 mv s1, a1
284 call __copy_data
285
286 /* Restore a0 & a1 copy */
287 mv a0, s0
288 mv a1, s1
289#endif
290
291#ifndef CONFIG_XIP_KERNEL
292 /* Clear BSS for flat non-ELF images */
293 la a3, __bss_start
294 la a4, __bss_stop
295 ble a4, a3, .Lclear_bss_done
296.Lclear_bss:
297 REG_S zero, (a3)
298 add a3, a3, RISCV_SZPTR
299 blt a3, a4, .Lclear_bss
300.Lclear_bss_done:
301#endif
302 la a2, boot_cpu_hartid
303 XIP_FIXUP_OFFSET a2
304 REG_S a0, (a2)
305
306 /* Initialize page tables and relocate to virtual addresses */
307 la tp, init_task
308 la sp, init_thread_union + THREAD_SIZE
309 XIP_FIXUP_OFFSET sp
310 addi sp, sp, -PT_SIZE_ON_STACK
311 scs_load_init_stack
312#ifdef CONFIG_BUILTIN_DTB
313 la a0, __dtb_start
314 XIP_FIXUP_OFFSET a0
315#else
316 mv a0, a1
317#endif /* CONFIG_BUILTIN_DTB */
318 /* Set trap vector to spin forever to help debug */
319 la a3, .Lsecondary_park
320 csrw CSR_TVEC, a3
321 call setup_vm
322#ifdef CONFIG_MMU
323 la a0, early_pg_dir
324 XIP_FIXUP_OFFSET a0
325 call relocate_enable_mmu
326#endif /* CONFIG_MMU */
327
328 call .Lsetup_trap_vector
329 /* Restore C environment */
330 la tp, init_task
331 la sp, init_thread_union + THREAD_SIZE
332 addi sp, sp, -PT_SIZE_ON_STACK
333 scs_load_current
334
335#ifdef CONFIG_KASAN
336 call kasan_early_init
337#endif
338 /* Start the kernel */
339 call soc_early_init
340 tail start_kernel주요 구문 해설
SYM_CODE_START(_start_kernel)이미지 헤더를 건너온 부팅 실행 코드의 시작입니다. 이곳에서 인터럽트·레지스터·초기 메모리 환경을 준비합니다. 이 표기는 빌드 도구에 코드 심벌과 정렬을 알려 주며 그 자체가 런타임 함수 호출은 아닙니다.
csrw CSR_IE, zero초기 trap vector와 per-hart stack이 준비될 때까지 interrupt enable을 모두 내린다.
csrw CSR_IP, zero인터럽트 pending CSR에 0을 써서 초기화 가능한 pending 비트를 정리합니다. 외부 하드웨어가 계속 발생시키는 인터럽트 원인까지 제거하는 동작은 아닙니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
#ifdef CONFIG_RISCV_M_MODEM-mode 커널은 instruction cache 정리와 초기 레지스터·물리 접근 설정을 직접 수행하고 MHARTID에서 hart 번호를 읽습니다. S-mode 커널은 펌웨어가 제공한 시작 상태를 사용하고 아래 분기에서 사용자 time CSR 접근을 허용합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
fence.i이 hart의 이후 명령 인출이 앞선 메모리 변경을 반영하도록 fence.i를 실행합니다. M-mode 직접 부팅에서 명령 실행 환경을 정리하는 부분입니다.
call reset_regsM-mode 부팅에서 부팅 인자 a0·a1과 복귀 주소 ra를 보존하고 나머지 일반 레지스터 및 scratch CSR을 초기화합니다. 빌드 설정과 MISA의 확장 지원 비트를 확인한 뒤 부동소수점 레지스터·FCSR 및 벡터 레지스터도 초기화합니다. 이때 켠 FS·VS 상태는 호출자가 뒤에서 다시 끕니다.
la a0, .Lpmp_donePMP 설정 중 예외가 발생하면 건너뛸 .Lpmp_done 주소를 a0에 넣습니다. PMP가 없는 구현에서도 초기화를 계속하려는 처리입니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
csrw CSR_TVEC, a0PMP 설정을 마친 위치를 임시 트랩 벡터로 지정합니다. 아래 PMP CSR 접근에서 트랩이 나면 그 뒤로 넘어갑니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
li a0, -1모든 비트가 1인 값을 a0에 준비합니다. PMP의 NAPOT 인코딩으로 넓은 물리 주소 영역을 표현할 때 사용합니다.
csrw CSR_PMPADDR0, a0PMP 첫 주소 레지스터에 준비한 값을 씁니다. 바로 다음 설정과 함께 물리 메모리에 대한 접근 허용 영역을 만듭니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
li a0, (PMP_A_NAPOT | PMP_R | PMP_W | PMP_X)NAPOT 주소 방식과 읽기·쓰기·실행 허용 비트를 합쳐 a0에 준비합니다. NAPOT은 크기가 2의 거듭제곱이고 그 크기에 맞게 정렬된 영역을 표현합니다.
csrw CSR_PMPCFG0, a0첫 PMP 영역의 설정을 적용합니다. 가능한 메모리 범위를 읽기·쓰기·실행할 수 있도록 열어 커널 초기화를 진행합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
.align 2PMP 설정을 마친 뒤의 트랩 도착점을 4바이트 경계에 맞춥니다.
.Lpmp_done:PMP 설정이 끝났거나, PMP 접근 예외로 설정을 건너뛰었을 때 합류하는 위치입니다. 다음에는 실제 hart ID를 읽습니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
csrr a0, CSR_MHARTIDM-mode kernel은 firmware argument 대신 hardware CSR에서 현재 hart ID를 직접 읽는다.
#elseS-mode 커널은 펌웨어가 전달한 hart 번호를 사용하고 SCOUNTEREN으로 사용자 time CSR 읽기를 허용합니다. M-mode처럼 MHARTID를 직접 읽거나 물리 접근 보호를 이 구간에서 준비하지 않습니다.
li t0, 0x2사용자 모드의 시간 카운터 읽기를 허용할 TM 비트 값 2를 준비합니다.
csrw CSR_SCOUNTEREN, t0SCOUNTEREN 전체에 2를 써서 TM 비트만 1로 두고, cycle·instret 등 다른 카운터의 허용 비트는 0으로 만듭니다. 상위 권한의 허용 조건도 충족되어 있으면 사용자 모드가 time CSR을 읽을 수 있습니다. 타이머 인터럽트를 켜는 설정은 아닙니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
#endif /* CONFIG_RISCV_M_MODE */208행에서 시작한 선택 구간을 마칩니다. M-mode 커널은 instruction cache 정리와 초기 레지스터·물리 접근 설정을 직접 수행하고 MHARTID에서 hart 번호를 읽습니다. S-mode 커널은 펌웨어가 제공한 시작 상태를 사용하고 아래 분기에서 사용자 time CSR 접근을 허용합니다.
load_global_pointerShadow Call Stack을 끈 빌드에서는 전역 데이터 접근 기준인 __global_pointer$를 gp에 넣습니다. 켠 빌드에서는 gp를 복귀 주소 전용 스택 포인터로 사용하므로 이 매크로는 비어 있으며, 뒤의 scs_load_init_stack에서 gp를 준비합니다.
li t0, SR_FS_VSFS·VS 상태 필드를 지울 마스크를 만듭니다. 두 필드는 부동소수점과 벡터 레지스터 사용 상태를 나타냅니다.
csrc CSR_STATUS, t0FS·VS를 꺼 두어 초기 커널 코드에서 의도하지 않은 부동소수점·벡터 사용을 감지합니다. 이후 이를 사용하는 경로가 필요한 상태를 따로 관리합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
#ifdef CONFIG_RISCV_BOOT_SPINWAITspinwait 부팅은 여러 hart가 함께 진입한 뒤 한 hart를 원자 연산으로 골라 주 초기화를 실행하는 방식입니다. 이 설정에서는 CPU 수 범위를 검사하고 나머지 hart를 보조 시작 경로로 보냅니다. SBI HSM으로 순서 있게 시작하는 경로와 구분합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
li t0, CONFIG_NR_CPUS이 빌드에서 지원하는 CPU 수의 상한 CONFIG_NR_CPUS를 t0에 넣습니다. spinwait 방식에서 hart ID가 허용 범위를 벗어나는지 검사할 때 씁니다.
blt a0, t0, .Lgood_coresa0의 hart ID가 상한보다 작으면 부팅 CPU 선출 단계로 이동합니다. 범위를 벗어나면 다음 줄의 정지 루프로 갑니다.
tail .Lsecondary_park허용 범위를 벗어난 hart를 정지 루프로 보냅니다. tail은 복귀 주소를 새로 만들지 않으므로 이 위치로 돌아오지 않습니다.
.Lgood_cores:CPU 수 범위 검사를 통과한 hart들이 모이는 위치입니다. 다음 원자적 연산으로 주 부팅 hart 하나를 고릅니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
#ifndef CONFIG_XIP_KERNELRAM에서 실행하는 일반 커널은 hart_lottery 값에 원자적으로 1을 더해 최초 진입 hart를 고릅니다. 플래시에서 직접 실행하는 XIP 빌드는 플래시의 초기 표식과 RAM 사본을 구분하는 아래 대안을 사용합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
la a3, hart_lotteryspinwait boot 방식에서 단 하나의 hart만 global 초기화를 수행하도록 원자적 lottery를 사용한다.
li a2, 1부팅 CPU 선출 카운터에 더할 값 1을 준비합니다.
amoadd.w a3, a2, (a3)hart_lottery에 1을 원자적으로 더하고 이전 값을 a3에 받습니다. 여러 hart가 동시에 실행해도 이전 값 0을 받은 하나만 주 부팅 경로에 남습니다.
bnez a3, .Lsecondary_start이전 선출 카운터가 0이 아니면 이미 다른 hart가 부팅을 맡았으므로 보조 hart 대기 경로로 이동합니다.
#elseXIP 빌드의 부트 hart 선출입니다. 플래시에 있는 초기 표식과 RAM의 lottery 위치를 각각 보정한 뒤 원자 교환으로 한 hart만 주 초기화를 맡게 합니다.
/* hart_lottery in flash contains a magic number */spinwait boot 방식에서 단 하나의 hart만 global 초기화를 수행하도록 원자적 lottery를 사용한다.
la a3, hart_lotteryspinwait boot 방식에서 단 하나의 hart만 global 초기화를 수행하도록 원자적 lottery를 사용한다.
mv a2, a3XIP 이미지의 hart_lottery 링크 주소를 a2에도 복사합니다. a2는 RAM 복사본, a3는 플래시 초기값을 가리키도록 각각 보정합니다. MV는 레지스터의 값만 복사하며, 그 값이 주소여도 가리키는 메모리 객체를 복사하지 않습니다.
XIP_FIXUP_OFFSET a2a2를 RAM에 있는 선출 변수의 주소로 바꿉니다. 읽기 전용 플래시 대신 이 RAM 변수에 원자적 갱신을 수행합니다.
XIP_FIXUP_FLASH_OFFSET a3a3를 플래시에 실제로 저장된 선출 변수 초기값의 위치로 보정합니다. 링크상의 데이터 주소와 이미지 적재 위치가 다르기 때문입니다.
lw t1, (a3)플래시에서 선출용 초기 표식 값을 32비트로 읽습니다. 다음 원자적 교환에서 RAM 변수에 기록할 값입니다.
amoswap.w t0, t1, (a2)RAM의 선출 변수에 표식 값을 원자적으로 기록하고 이전 값을 t0에 받습니다. 다른 hart가 이미 표식을 기록했는지 한 번의 연산으로 판별합니다.
/* first time here if hart_lottery in RAM is not set */spinwait boot 방식에서 단 하나의 hart만 global 초기화를 수행하도록 원자적 lottery를 사용한다.
beq t0, t1, .Lsecondary_start이전 값이 이미 표식과 같으면 다른 hart가 먼저 선출되었다는 뜻이므로 보조 hart 경로로 이동합니다.
#endif /* CONFIG_XIP */258행에서 시작한 선택 구간을 마칩니다. RAM에서 실행하는 일반 커널은 hart_lottery 값에 원자적으로 1을 더해 최초 진입 hart를 고릅니다. 플래시에서 직접 실행하는 XIP 빌드는 플래시의 초기 표식과 RAM 사본을 구분하는 아래 대안을 사용합니다.
#endif /* CONFIG_RISCV_BOOT_SPINWAIT */251행에서 시작한 선택 구간을 마칩니다. spinwait 부팅은 여러 hart가 함께 진입한 뒤 한 hart를 원자 연산으로 골라 주 초기화를 실행하는 방식입니다. 이 설정에서는 CPU 수 범위를 검사하고 나머지 hart를 보조 시작 경로로 보냅니다. SBI HSM으로 순서 있게 시작하는 경로와 구분합니다.
#ifdef CONFIG_XIP_KERNELXIP는 코드를 플래시에서 직접 실행하고 수정할 데이터는 RAM에 두는 방식입니다. 켜지면 RAM 스택을 준비하고 __copy_data로 데이터 초기값을 복사한 뒤 보존한 hart 번호와 DTB 포인터를 되돌립니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
la sp, _end + THREAD_SIZEXIP 데이터 복사 중 사용할 임시 스택의 상단 주소를 sp에 만듭니다. 이어서 RAM 주소로 보정합니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
XIP_FIXUP_OFFSET sp임시 스택 주소를 RAM 위치로 바꿉니다. 함수 호출이 스택에 쓰기 때문에 플래시 주소를 그대로 사용할 수 없습니다.
mv s0, a0hart ID인 a0를 호출 후에도 보존되는 s0에 저장합니다. 데이터 복사 함수가 인자 레지스터를 사용해도 잃지 않기 위해서입니다. MV는 레지스터의 값만 복사하며, 그 값이 주소여도 가리키는 메모리 객체를 복사하지 않습니다.
mv s1, a1부트로더가 넘긴 DTB 주소 a1을 s1에 보존합니다. DTB는 하드웨어 구성을 담은 장치 트리 데이터입니다. MV는 레지스터의 값만 복사하며, 그 값이 주소여도 가리키는 메모리 객체를 복사하지 않습니다.
call __copy_dataXIP 커널의 쓰기 가능한 데이터가 RAM에서 사용되도록 초기 데이터 복사 루틴을 호출합니다. 코드는 플래시에서 실행하더라도 변경되는 데이터는 RAM에 있어야 합니다.
mv a0, s0데이터 복사 전에 보관한 hart ID를 a0로 되돌립니다. MV는 레지스터의 값만 복사하며, 그 값이 주소여도 가리키는 메모리 객체를 복사하지 않습니다.
mv a1, s1보관한 DTB 주소를 a1으로 되돌려 이후 초기 메모리 설정에 넘길 수 있게 합니다. MV는 레지스터의 값만 복사하며, 그 값이 주소여도 가리키는 메모리 객체를 복사하지 않습니다.
#endif279행에서 시작한 선택 구간을 마칩니다. XIP는 코드를 플래시에서 직접 실행하고 수정할 데이터는 RAM에 두는 방식입니다. 켜지면 RAM 스택을 준비하고 __copy_data로 데이터 초기값을 복사한 뒤 보존한 hart 번호와 DTB 포인터를 되돌립니다.
#ifndef CONFIG_XIP_KERNEL일반 RAM 실행 이미지는 부팅 전에 BSS를 직접 0으로 지웁니다. XIP 빌드는 앞의 데이터 복사 경로에서 메모리 준비를 하므로 이 일반 이미지용 BSS 지우기를 중복 수행하지 않습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
la a3, __bss_startflat Image에서 zero-init global의 C 언어 계약을 assembly가 직접 만족시킨다.
la a4, __bss_stop초기값이 없는 전역 변수 영역 BSS의 끝 주소를 a4에 넣습니다. a3의 시작 주소부터 이 경계 전까지 0으로 채웁니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
ble a4, a3, .Lclear_bss_doneBSS의 끝이 시작보다 크지 않으면 지울 영역이 없으므로 초기화 루프를 건너뜁니다.
.Lclear_bss:BSS를 한 번에 레지스터 폭만큼 0으로 채우는 반복문의 시작점입니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
REG_S zero, (a3)a3가 가리키는 BSS 위치에 0을 저장합니다. RV32에서는 4바이트, RV64에서는 8바이트를 씁니다.
add a3, a3, RISCV_SZPTR다음 BSS 위치로 이동하도록 a3에 포인터 크기를 더합니다.
blt a3, a4, .Lclear_bss아직 BSS 끝 주소에 도달하지 않았다면 다시 0을 저장합니다. 끝 주소 자체는 저장 범위에 포함되지 않습니다.
.Lclear_bss_done:BSS 초기화가 끝난 지점입니다. 이제 0으로 초기화되어야 하는 전역 변수들을 사용할 수 있습니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
#endif291행에서 시작한 선택 구간을 마칩니다. 일반 RAM 실행 이미지는 부팅 전에 BSS를 직접 0으로 지웁니다. XIP 빌드는 앞의 데이터 복사 경로에서 메모리 준비를 하므로 이 일반 이미지용 BSS 지우기를 중복 수행하지 않습니다.
la a2, boot_cpu_hartidgeneric SMP 코드가 사용할 대표 boot hart ID를 전역 위치에 게시한다.
XIP_FIXUP_OFFSET a2XIP 빌드에서는 boot_cpu_hartid 변수가 놓인 RAM 주소로 a2를 보정합니다.
REG_S a0, (a2)주 부팅 hart의 ID인 a0를 boot_cpu_hartid에 저장합니다. 이후 논리 CPU와 하드웨어 hart를 대응시킬 때 필요한 초기 정보입니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
la tp, init_tasktp가 초기 task인 init_task를 가리키게 준비합니다. RISC-V 커널은 tp를 현재 task를 찾는 기준으로 사용합니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
la sp, init_thread_union + THREAD_SIZEboot task의 kernel stack top을 만들고 pt_regs 예약분을 뺀다.
XIP_FIXUP_OFFSET spXIP 빌드에서 초기 커널 스택 상단을 실제 RAM 주소로 보정합니다.
addi sp, sp, -PT_SIZE_ON_STACK스택 상단에서 예외 레지스터 저장 영역 PT_SIZE_ON_STACK만큼 공간을 남깁니다. 스택은 낮은 주소 쪽으로 내려가며 사용됩니다.
scs_load_init_stackShadow Call Stack 사용 시 초기 복귀 주소 전용 스택을 gp에 준비합니다. 일반 스택의 데이터와 복귀 주소 보관을 분리하는 기능입니다.
#ifdef CONFIG_BUILTIN_DTBDTB를 내장했다면 __dtb_start를 setup_vm의 입력으로 선택합니다. 내장하지 않았다면 부트로더가 a1으로 전달한 DTB 주소를 사용하며, 두 경로 모두 같은 초기 매핑 함수로 이어집니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
la a0, __dtb_start커널 이미지에 함께 넣은 DTB의 시작 주소를 a0에 준비합니다. 다음 setup_vm 호출의 DTB 인자입니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
XIP_FIXUP_OFFSET a0XIP에서는 내장 DTB를 접근할 수 있도록 주소 보정을 적용합니다.
#else커널에 DTB를 내장하지 않았으므로 부트로더가 a1에 준 물리 주소를 a0에 옮깁니다. setup_vm이 이 DTB에 접근할 초기 주소 변환을 준비합니다.
mv a0, a1내장 DTB를 사용하지 않는 경우 부트로더가 a1으로 넘긴 DTB 물리 주소를 첫 인자 a0로 옮깁니다. MV는 레지스터의 값만 복사하며, 그 값이 주소여도 가리키는 메모리 객체를 복사하지 않습니다.
#endif /* CONFIG_BUILTIN_DTB */312행에서 시작한 선택 구간을 마칩니다. DTB를 내장했다면 __dtb_start를 setup_vm의 입력으로 선택합니다. 내장하지 않았다면 부트로더가 a1으로 전달한 DTB 주소를 사용하며, 두 경로 모두 같은 초기 매핑 함수로 이어집니다.
la a3, .Lsecondary_park초기 페이지 테이블을 만드는 도중 예외가 발생하면 멈출 정지 루프의 주소를 a3에 준비합니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
csrw CSR_TVEC, a3초기 MMU 준비 구간의 임시 트랩 벡터를 정지 루프로 설정합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
call setup_vmMMU-off C helper가 DTB와 kernel 배치를 바탕으로 early page table을 작성한다.
#ifdef CONFIG_MMUMMU 지원 구성은 setup_vm이 만든 early_pg_dir를 사용해 SATP를 설정하고 가상 주소 실행으로 옮깁니다. MMU가 없으면 페이지 표 전환 호출을 생략합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
la a0, early_pg_dir작성된 초기 page-table root를 relocate_enable_mmu에 넘긴다.
XIP_FIXUP_OFFSET a0XIP에서는 early_pg_dir의 실제 RAM 주소를 a0에 준비합니다. 주 부팅 hart가 처음 사용하는 커널 페이지 테이블입니다.
call relocate_enable_mmu초기 페이지 테이블을 사용해 MMU를 켜고 가상 주소 실행으로 전환합니다. 이 함수는 전환 후 돌아올 ra도 가상 주소로 보정합니다.
#endif /* CONFIG_MMU */322행에서 시작한 선택 구간을 마칩니다. MMU 지원 구성은 setup_vm이 만든 early_pg_dir를 사용해 SATP를 설정하고 가상 주소 실행으로 옮깁니다. MMU가 없으면 페이지 표 전환 호출을 생략합니다.
call .Lsetup_trap_vector주소 변환이 준비된 뒤 정상 예외 벡터를 설치합니다. 이후 예외는 초기 정지 루프 대신 커널 예외 진입 코드로 들어갑니다.
la tp, init_task가상 주소 환경에서 초기 task 주소를 tp에 다시 읽습니다. MMU 전환 전 주소를 계속 사용하는 일을 피합니다. LA는 심벌의 주소를 만드는 의사 명령이며, 해당 심벌이 가리키는 메모리의 내용까지 읽지는 않습니다.
la sp, init_thread_union + THREAD_SIZEboot task의 kernel stack top을 만들고 pt_regs 예약분을 뺀다.
addi sp, sp, -PT_SIZE_ON_STACK가상 주소로 다시 구한 초기 스택 상단에서 예외 프레임 공간을 남깁니다. 이후 C 코드가 사용할 정상 스택 위치를 완성합니다.
scs_load_current현재 task의 Shadow Call Stack 포인터를 다시 읽습니다. 기능을 끈 빌드에서는 이 매크로가 명령을 만들지 않습니다.
#ifdef CONFIG_KASANKASAN은 잘못된 커널 메모리 접근을 찾기 위해 실제 메모리에 대응하는 shadow 정보를 검사합니다. 지원 빌드에서는 일반 커널 C 코드로 들어가기 전에 초기 shadow 매핑을 마련하며, 꺼지면 이 호출과 검사 지원이 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
call kasan_early_initKASAN은 메모리 접근을 검사할 때 별도의 shadow 영역을 읽습니다. 이 초기화는 그 영역을 공통 kasan_early_shadow_page에 연결해, 검사 코드가 들어간 초기 부팅 함수를 실행할 수 있게 합니다. 아직 주소별 접근 가능 상태를 따로 기록하는 완전한 shadow 배치는 아니며, 실제 메모리와 커널 영역의 shadow는 뒤의 kasan_init에서 준비합니다.
#endif335행에서 시작한 선택 구간을 마칩니다. KASAN은 잘못된 커널 메모리 접근을 찾기 위해 실제 메모리에 대응하는 shadow 정보를 검사합니다. 지원 빌드에서는 일반 커널 C 코드로 들어가기 전에 초기 shadow 매핑을 마련하며, 꺼지면 이 호출과 검사 지원이 없습니다.
call soc_early_init공통 start_kernel에 들어가기 전 SoC별 초기화 훅을 호출합니다. 플랫폼이 등록한 초기 준비 작업을 이 시점에 수행합니다.
tail start_kernel정상 virtual address, stack, task pointer, trap vector를 갖춘 뒤 generic kernel init으로 비복귀 전환한다.
SOURCE 04
setup_vm이 kernel PA·VA offset과 early page-table 한계를 계산하는 구간
head.S의 첫 C 호출은 MMU-off 제약 아래에서 kernel mapping을 수치로 만든다. KASLR offset, physical load address, virtual link address와 early PUD 범위를 한 함수에서 결정한다.
원본: arch/riscv/mm/init.c 1119-1204줄
1119asmlinkage void __init setup_vm(uintptr_t dtb_pa)
1120{
1121 pmd_t __maybe_unused fix_bmap_spmd, fix_bmap_epmd;
1122
1123#ifdef CONFIG_RANDOMIZE_BASE
1124 if (!__pi_set_nokaslr_from_cmdline(dtb_pa)) {
1125 u64 kaslr_seed = __pi_get_kaslr_seed_zkr(dtb_pa);
1126 u32 kernel_size = (uintptr_t)(&_end) - (uintptr_t)(&_start);
1127 u32 nr_pos;
1128
1129 if (kaslr_seed == 0)
1130 kaslr_seed = __pi_get_kaslr_seed(dtb_pa);
1131 /*
1132 * Compute the number of positions available: we are limited
1133 * by the early page table that only has one PUD and we must
1134 * be aligned on PMD_SIZE.
1135 */
1136 nr_pos = (PUD_SIZE - kernel_size) / PMD_SIZE;
1137
1138 kernel_map.virt_offset = (kaslr_seed % nr_pos) * PMD_SIZE;
1139 }
1140#endif
1141
1142 kernel_map.virt_addr = KERNEL_LINK_ADDR + kernel_map.virt_offset;
1143
1144#ifdef CONFIG_XIP_KERNEL
1145 kernel_map.xiprom = (uintptr_t)CONFIG_XIP_PHYS_ADDR;
1146 kernel_map.xiprom_sz = (uintptr_t)(&_exiprom) - (uintptr_t)(&_xiprom);
1147
1148 phys_ram_base = CONFIG_PHYS_RAM_BASE;
1149#ifdef CONFIG_SPARSEMEM_VMEMMAP
1150 vmemmap_start_pfn = round_down(phys_ram_base, VMEMMAP_ADDR_ALIGN) >> PAGE_SHIFT;
1151#endif
1152 kernel_map.phys_addr = (uintptr_t)CONFIG_PHYS_RAM_BASE;
1153 kernel_map.size = (uintptr_t)(&_end) - (uintptr_t)(&_start);
1154
1155 kernel_map.va_kernel_xip_text_pa_offset = kernel_map.virt_addr - kernel_map.xiprom;
1156 kernel_map.va_kernel_xip_data_pa_offset = kernel_map.virt_addr - kernel_map.phys_addr
1157 + (uintptr_t)&_sdata - (uintptr_t)&_start;
1158#else
1159 kernel_map.phys_addr = (uintptr_t)(&_start);
1160 kernel_map.size = (uintptr_t)(&_end) - kernel_map.phys_addr;
1161 kernel_map.va_kernel_pa_offset = kernel_map.virt_addr - kernel_map.phys_addr;
1162#endif
1163
1164#if defined(CONFIG_64BIT) && !defined(CONFIG_XIP_KERNEL)
1165 set_satp_mode(dtb_pa);
1166 set_mmap_rnd_bits_max();
1167#endif
1168
1169 /*
1170 * In 64-bit, we defer the setup of va_pa_offset to setup_bootmem,
1171 * where we have the system memory layout: this allows us to align
1172 * the physical and virtual mappings and then make use of PUD/P4D/PGD
1173 * for the linear mapping. This is only possible because the kernel
1174 * mapping lies outside the linear mapping.
1175 * In 32-bit however, as the kernel resides in the linear mapping,
1176 * setup_vm_final can not change the mapping established here,
1177 * otherwise the same kernel addresses would get mapped to different
1178 * physical addresses (if the start of dram is different from the
1179 * kernel physical address start).
1180 */
1181 kernel_map.va_pa_offset = IS_ENABLED(CONFIG_64BIT) ?
1182 0UL : PAGE_OFFSET - kernel_map.phys_addr;
1183
1184 memory_limit = KERN_VIRT_SIZE;
1185
1186 /* Sanity check alignment and size */
1187 BUG_ON((PAGE_OFFSET % PGDIR_SIZE) != 0);
1188 BUG_ON((kernel_map.phys_addr % PMD_SIZE) != 0);
1189
1190#ifdef CONFIG_64BIT
1191 /*
1192 * The last 4K bytes of the addressable memory can not be mapped because
1193 * of IS_ERR_VALUE macro.
1194 */
1195 BUG_ON((kernel_map.virt_addr + kernel_map.size) > ADDRESS_SPACE_END - SZ_4K);
1196#endif
1197
1198#ifdef CONFIG_RELOCATABLE
1199 /*
1200 * Early page table uses only one PUD, which makes it possible
1201 * to map PUD_SIZE aligned on PUD_SIZE: if the relocation offset
1202 * makes the kernel cross over a PUD_SIZE boundary, raise a bug
1203 * since a part of the kernel would not get mapped.
1204 */주요 구문 해설
asmlinkage void __init setup_vm(uintptr_t dtb_pa)DTB 물리 주소를 받아 초기 커널 매핑을 만드는 함수의 정의입니다. 아직 정상 가상 주소 환경이 아니므로 이후 코드가 실행될 주소와 페이지 테이블을 먼저 준비합니다.
pmd_t __maybe_unused fix_bmap_spmd, fix_bmap_epmd;초기 fixmap 범위의 시작·끝 PMD 값을 담을 변수를 선언합니다. PMD는 페이지 테이블의 중간 단계이며, __maybe_unused는 설정에 따라 변수가 쓰이지 않을 때의 경고를 억제합니다.
#ifdef CONFIG_RANDOMIZE_BASEKASLR 지원 빌드는 커널 가상 배치 위치를 무작위로 바꾸어 주소 예측을 어렵게 합니다. 여기서는 nokaslr 명령행 여부와 하드웨어·DTB seed를 확인하여 오프셋을 정하며, 설정이 켜져도 nokaslr나 seed 부재에 따라 무작위화를 사용하지 않을 수 있습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
if (!__pi_set_nokaslr_from_cmdline(dtb_pa)) {명령줄에 KASLR을 끄는 nokaslr가 없을 때만 커널 가상 배치의 무작위 오프셋을 선택합니다. KASLR은 커널 주소를 부팅마다 바꿔 주소 예측을 어렵게 하는 기능입니다.
u64 kaslr_seed = __pi_get_kaslr_seed_zkr(dtb_pa);Zkr 난수 기능을 이용하는 초기 헬퍼에서 배치용 seed를 얻습니다. 아래에서는 이 값이 0이면 DTB의 seed를 가져오는 경로를 사용합니다.
u32 kernel_size = (uintptr_t)(&_end) - (uintptr_t)(&_start);커널 시작부터 끝까지의 바이트 크기를 계산합니다. 초기 페이지 테이블의 매핑 범위 안에 커널 전체가 들어갈 위치만 후보로 삼기 위해서입니다.
u32 nr_pos;커널을 놓을 수 있는 정렬된 위치의 개수를 저장할 변수를 선언합니다. 이후 난수 seed를 이 개수로 나눈 나머지로 위치를 고릅니다.
if (kaslr_seed == 0)앞서 얻은 난수 seed가 0인지 검사합니다. 0이면 다른 공급 경로인 DTB에서 seed를 구합니다.
kaslr_seed = __pi_get_kaslr_seed(dtb_pa);DTB에서 KASLR seed를 가져옵니다. 초기 헬퍼는 정상 가상 주소 전환 전에 사용할 수 있도록 위치 독립 코드로 마련되어 있습니다.
nr_pos = (PUD_SIZE - kernel_size) / PMD_SIZE;early page table 한 PUD 안에서 kernel 전체가 PMD 경계를 넘지 않고 이동할 수 있는 후보 개수를 계산한다.
kernel_map.virt_offset = (kaslr_seed % nr_pos) * PMD_SIZE;seed를 후보 수로 나눈 나머지에 PMD_SIZE를 곱해 2MiB 단위 virtual KASLR offset을 만든다.
#endif1123행에서 시작한 선택 구간을 마칩니다. KASLR 지원 빌드는 커널 가상 배치 위치를 무작위로 바꾸어 주소 예측을 어렵게 합니다. 여기서는 nokaslr 명령행 여부와 하드웨어·DTB seed를 확인하여 오프셋을 정하며, 설정이 켜져도 nokaslr나 seed 부재에 따라 무작위화를 사용하지 않을 수 있습니다.
kernel_map.virt_addr = KERNEL_LINK_ADDR + kernel_map.virt_offset;seed를 후보 수로 나눈 나머지에 PMD_SIZE를 곱해 2MiB 단위 virtual KASLR offset을 만든다.
#ifdef CONFIG_XIP_KERNEL플래시 직접 실행 XIP에서는 코드의 플래시 물리 주소와 데이터의 RAM 주소를 따로 기록합니다. 일반 RAM 실행처럼 하나의 가상·물리 오프셋을 모든 영역에 적용할 수 없어 코드·데이터용 오프셋도 분리합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
kernel_map.xiprom = (uintptr_t)CONFIG_XIP_PHYS_ADDR;플래시에서 직접 실행하는 XIP 코드 영역의 물리 시작 주소를 기록합니다. RAM에 놓이는 변경 가능한 데이터 영역과 구분합니다.
kernel_map.xiprom_sz = (uintptr_t)(&_exiprom) - (uintptr_t)(&_xiprom);_xiprom부터 _exiprom까지의 차이로 플래시 코드 영역의 크기를 바이트 단위로 구합니다.
phys_ram_base = CONFIG_PHYS_RAM_BASE;XIP 빌드가 정한 RAM 물리 시작 주소를 phys_ram_base에 기록합니다. 쓰기 가능한 커널 데이터와 선형 매핑의 기준이 됩니다.
#ifdef CONFIG_SPARSEMEM_VMEMMAPSPARSEMEM_VMEMMAP은 각 물리 페이지의 struct page를 연속 가상 배열로 다루는 메모리 모델입니다. XIP RAM 시작을 정해진 정렬로 내린 PFN을 계산하여 그 메타데이터 배열의 기준을 맞춥니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
vmemmap_start_pfn = round_down(phys_ram_base, VMEMMAP_ADDR_ALIGN) >> PAGE_SHIFT;RAM 시작 주소를 vmemmap 정렬 단위로 내림한 뒤 페이지 번호로 변환합니다. vmemmap은 각 물리 페이지의 struct page 정보를 연속된 가상 주소로 다루는 영역입니다.
#endif1149행에서 시작한 선택 구간을 마칩니다. SPARSEMEM_VMEMMAP은 각 물리 페이지의 struct page를 연속 가상 배열로 다루는 메모리 모델입니다. XIP RAM 시작을 정해진 정렬로 내린 PFN을 계산하여 그 메타데이터 배열의 기준을 맞춥니다.
kernel_map.phys_addr = (uintptr_t)CONFIG_PHYS_RAM_BASE;XIP 커널의 RAM 데이터 쪽 물리 기준 주소를 기록합니다. 플래시 코드 기준 주소인 xiprom과 서로 다른 값일 수 있습니다.
kernel_map.size = (uintptr_t)(&_end) - (uintptr_t)(&_start);링커 심벌 _start와 _end의 차이를 커널 이미지 크기로 저장합니다. 메모리 배치와 매핑 범위 계산에 사용합니다.
kernel_map.va_kernel_xip_text_pa_offset = kernel_map.virt_addr - kernel_map.xiprom;link address에 virtual KASLR offset을 더한 실제 kernel VA 시작점이다.
kernel_map.va_kernel_xip_data_pa_offset = kernel_map.virt_addr - kernel_map.phys_addrlink address에 virtual KASLR offset을 더한 실제 kernel VA 시작점이다.
+ (uintptr_t)&_sdata - (uintptr_t)&_start;앞줄의 가상·물리 주소 차이에 이미지 시작부터 데이터 시작까지의 거리를 더합니다. XIP 데이터는 코드와 다른 저장 위치를 쓰므로 데이터 전용 주소 변환량이 필요합니다.
#else코드와 데이터가 RAM에 함께 적재된 일반 이미지입니다. 실제 _start 주소와 이미지 크기를 기록하고 하나의 가상·물리 오프셋을 계산합니다. XIP의 플래시·RAM 이중 오프셋이 필요 없습니다.
kernel_map.phys_addr = (uintptr_t)(&_start);일반 RAM 실행 커널에서는 현재 _start의 주소를 커널 물리 시작 주소로 기록합니다. 이 함수는 MMU 전환 전 초기 환경에서 실행됩니다.
kernel_map.size = (uintptr_t)(&_end) - kernel_map.phys_addr;커널 끝 주소에서 물리 시작 주소를 빼서 매핑할 이미지 크기를 계산합니다.
kernel_map.va_kernel_pa_offset = kernel_map.virt_addr - kernel_map.phys_addr;link address에 virtual KASLR offset을 더한 실제 kernel VA 시작점이다.
#endif1144행에서 시작한 선택 구간을 마칩니다. 플래시 직접 실행 XIP에서는 코드의 플래시 물리 주소와 데이터의 RAM 주소를 따로 기록합니다. 일반 RAM 실행처럼 하나의 가상·물리 오프셋을 모든 영역에 적용할 수 없어 코드·데이터용 오프셋도 분리합니다.
#if defined(CONFIG_64BIT) && !defined(CONFIG_XIP_KERNEL)RV64이면서 XIP가 아닌 커널은 실제 하드웨어가 지원하는 SATP 주소 변환 모드를 선택하고 그 주소 폭에 맞는 mmap 무작위화 비트 한도를 정합니다. RV32나 고정 XIP 배치에는 이 동적 모드 선택을 적용하지 않습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
set_satp_mode(dtb_pa);DTB와 hardware capability를 바탕으로 Sv39/Sv48/Sv57 후보를 검증한다. 지원되지 않는 mode를 SATP에 남기면 WARL 동작으로 write가 무시될 수 있다.
set_mmap_rnd_bits_max();선택된 가상 주소 공간 크기에 맞춰 사용자 mmap 주소 무작위화에 쓸 비트 수의 상한을 설정합니다. 커널 자체 배치인 KASLR 오프셋과는 별도 설정입니다.
#endif1164행에서 시작한 선택 구간을 마칩니다. RV64이면서 XIP가 아닌 커널은 실제 하드웨어가 지원하는 SATP 주소 변환 모드를 선택하고 그 주소 폭에 맞는 mmap 무작위화 비트 한도를 정합니다. RV32나 고정 XIP 배치에는 이 동적 모드 선택을 적용하지 않습니다.
kernel_map.va_pa_offset = IS_ENABLED(CONFIG_64BIT) ?일반 RAM의 선형 매핑에 사용할 가상·물리 주소 차이를 64비트 여부에 따라 정합니다. 64비트 커널은 실제 DRAM 배치를 확인한 뒤 계산하려고 여기서는 미룹니다.
0UL : PAGE_OFFSET - kernel_map.phys_addr;64비트이면 임시로 0을 넣고, 32비트이면 PAGE_OFFSET - 커널 물리 시작 주소를 저장합니다. 32비트 커널은 커널 이미지 자체가 선형 매핑 안에 있어 초기부터 같은 대응을 유지해야 합니다.
memory_limit = KERN_VIRT_SIZE;커널 가상 주소로 선형 매핑할 수 있는 크기를 초기 메모리 사용 한도로 둡니다. 물리 RAM이 더 크더라도 주소 공간의 매핑 능력을 넘겨 사용할 수는 없습니다.
BUG_ON((PAGE_OFFSET % PGDIR_SIZE) != 0);선형 매핑 시작 주소 PAGE_OFFSET이 최상위 페이지 테이블 항목의 범위에 맞게 정렬되었는지 검사합니다. 나머지가 있으면 매핑 전제가 깨졌으므로 BUG로 중단합니다.
BUG_ON((kernel_map.phys_addr % PMD_SIZE) != 0);초기 superpage mapping의 전제인 2MiB physical alignment를 명시적으로 검사한다.
#ifdef CONFIG_64BITRV64 커널 이미지가 가상 주소 공간 끝의 예약된 4KiB에 걸리지 않는지 검사합니다. 그 마지막 범위는 오류 포인터 판별에 사용되므로 정상 매핑으로 쓸 수 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
BUG_ON((kernel_map.virt_addr + kernel_map.size) > ADDRESS_SPACE_END - SZ_4K);link address에 virtual KASLR offset을 더한 실제 kernel VA 시작점이다.
#endif1190행에서 시작한 선택 구간을 마칩니다. RV64 커널 이미지가 가상 주소 공간 끝의 예약된 4KiB에 걸리지 않는지 검사합니다. 그 마지막 범위는 오류 포인터 판별에 사용되므로 정상 매핑으로 쓸 수 없습니다.
#ifdef CONFIG_RELOCATABLE재배치 가능한 커널은 적재 오프셋 때문에 이미지가 초기 페이지 표의 한 PUD 범위를 넘지 않는지 검사합니다. 재배치를 지원한다고 초기 표가 임의 위치·크기를 모두 매핑할 수 있는 것은 아니므로 이 제약을 확인합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
WORKED EXAMPLES
숫자를 넣어 끝까지 계산한다
Sv39 SATP 값을 실제 PPN으로 조립한다
early_pg_dir physical address가 0x80204000, ASID가 0이고 Sv39 mode를 선택했다고 가정한다.
- PPN
0x80204000 >> 12 = 0x80204가 SATP.PPN에 들어간다. - MODERV64 SATP.MODE의 Sv39 값 8을 bit 63:60에 두므로 mode 부분은
0x8000000000000000이다. - SATPASID 0이면 최종 값은
0x8000000000080204다. - orderingPTE store 뒤
sfence.vma, SATP write, 필요한sfence.vma를 거쳐 이전 translation을 제거한다.
판정SATP dump는 16진수 한 줄로 끝내지 말고 MODE, ASID, PPN으로 분해하고 PPN을 다시 12bit 이동해 실제 root page 주소와 대조한다.
kernel PA와 link VA 사이의 relocation offset
kernel load PA=0x80200000, KERNEL_LINK_ADDR=0xffffffff80000000, virtual KASLR offset=0x04000000을 예로 든다.
- runtime VA
0xffffffff80000000 + 0x04000000 = 0xffffffff84000000 - VA-PA offset
0xffffffff84000000 - 0x80200000 = 0xffffffff03e00000 - symbol conversionlink symbol에 virtual KASLR offset을 반영한 뒤 이 offset으로 PA와 VA를 왕복한다.
- superpage conditionload PA
0x80200000은 2MiB 정렬이므로 PMD leaf mapping 전제를 만족한다.
판정첫 SATP 전환 전에는 PA, 전환 후에는 VA가 같은 instruction byte를 가리키는지 PTE physical frame까지 확인한다.
DEEP DIVE
코드에서 놓치기 쉬운 경계
RISC-V Image header는 실행 코드와 metadata가 같은 64-byte 안에 공존한다
첫 instruction은 CPU가 실행할 수 있어야 하고 UEFI가 볼 때는 MZ signature여야 한다. 뒤 필드는 load offset, image size, flags, magic, PE header offset을 고정 layout으로 제공한다.
header를 바꿀 때는 kernel만 rebuild해 보는 것으로 부족하다. U-Boot booti, EFI stub, image parser가 같은 offset과 endian을 기대하는지 확인해야 한다.
S-mode와 M-mode kernel의 책임이 다르다
S-mode kernel은 machine-mode firmware가 timer, IPI, hart start 같은 서비스를 SBI로 제공한다고 본다. M-mode kernel은 PMP를 열고 mhartid를 직접 읽는 코드가 포함된다.
bring-up 문서에는 현재 privilege mode, OpenSBI version, delegation된 exception/interrupt를 먼저 적어야 CSR fault의 위치를 해석할 수 있다.
setup_vm은 일반 C 함수처럼 보여도 실행 제약이 크다
MMU-off에서 호출되므로 absolute addressing, sanitizer, ftrace instrumentation과 일반 allocator를 사용할 수 없다. page table은 statically reserved early arrays에 기록되고 DTB pointer도 물리 주소다.
compiler option 변화로 instrumentation이 들어가면 source는 같아도 첫 C call에서 virtual address를 참조할 수 있으므로 disassembly 검사가 필요하다.
trampoline page table은 첫 instruction fetch를 보장한다
SATP에 새 root를 쓰는 순간부터 다음 fetch 주소 해석이 바뀐다. 현재 physical PC와 보정된 virtual PC가 같은 code byte를 가리키는 mapping을 첫 superpage에 만들어 전환을 연속적으로 보이게 한다.
첫 sfence.vma는 PTE write의 가시성을, 두 번째는 실제 kernel root 전환 뒤 trampoline translation 제거를 보장한다.
gp와 tp는 일반 임시 register가 아니다
gp는 small data addressing의 기준이고 tp는 Linux에서 current task/per-CPU 접근의 기반으로 쓰인다. virtual relocation 뒤 gp를 다시 적재하고 boot stack 준비 시 tp를 init_task에 맞춰야 C 코드가 정상 global/task state를 본다.
GDB에서 함수 call이 시작되기 전 gp/tp가 symbol table의 예상 범위에 있는지 확인하면 초기 C fault를 빠르게 구분할 수 있다.
DTB는 setup_vm에서 읽은 뒤에도 수명이 계속된다
a1의 물리 DTB는 page-table mode와 memory topology를 정하는 입력이며 이후 early DT scan까지 보존되어야 한다. kernel image, BSS clear, initrd와 겹치면 MMU 전환은 성공해도 setup_arch에서 깨질 수 있다.
부트로더 memory map 그림에는 DTB의 start/end와 reservation 여부를 반드시 넣는다.
FAILURE PATH
멈춘 위치보다 먼저 볼 값
| 조건 | 관찰되는 증상 | 첫 확인 값 |
|---|---|---|
| a0/a1 계약 위반 | boot hart가 잘못 선택되거나 DTB magic 검사 전에 fault가 난다. | firmware 직전 register dump |
| trampoline mapping 누락 | 첫 SATP write 직후 instruction page fault 또는 park loop로 들어간다. | PC superpage의 PA/VA PTE |
| early_pg_dir 정렬/주소 오류 | 두 번째 SATP 전환 뒤 global 또는 stack 접근이 실패한다. | SATP PPN과 PTE flags |
| gp/tp 복구 오류 | 첫 C helper나 current 접근에서 엉뚱한 global을 읽는다. | gp, tp, init_task symbol |
| DTB overlap | setup_vm 이후 machine model 또는 memory scan이 비정상이다. | DTB start/end reservation |
LAB
직접 확인할 실험
- 01OpenSBI 직전과 Linux
_start에서 a0, a1, privilege mode, satp, stvec를 기록한다.명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
- 02QEMU GDB에서 첫/두 번째
csrw satp에 breakpoint를 두고 PC의 physical/virtual PTE를 직접 walk한다.명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
- 03
readelf -lW vmlinux로_start,_end, page-table arrays의 section과 load address를 확인한다.명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
- 04부팅 옵션
earlycon을 사용하되 첫 출력 이전 구간은 SBI debug console 또는 GDB checkpoint로 따로 계측한다.명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
PRIMARY REFERENCES