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 21-70줄 주석
SYM_CODE_START(_start)linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Image header expected by Linux boot-loaders. The image header data원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* structure is described in asm/image.h.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Do not modify it without modifying the structure and all bootloaders원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* that expects this header format!!원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
#ifdef CONFIG_EFI빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* This instruction decodes to "MZ" ASCII required by UEFI.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
c.li s4,-13compressed instruction byte가 ASCII MZ로 해석되도록 만들어 UEFI PE/COFF image signature를 만족하면서 실제로는 무해한 register load를 수행한다.
j _start_kernel현재 함수로 돌아오지 않는 제어 이전이다. 다음 진입점이 요구하는 register와 주소 공간을 이 줄 직전에 모두 완성해야 한다.
#else빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* jump to start kernel */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
j _start_kernel현재 함수로 돌아오지 않는 제어 이전이다. 다음 진입점이 요구하는 register와 주소 공간을 이 줄 직전에 모두 완성해야 한다.
/* reserved */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
.word 0assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
.balign 8linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
#ifdef CONFIG_RISCV_M_MODE빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* Image load offset (0MB) from start of RAM for M-mode */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
.dword 0assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.
#else빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
#if __riscv_xlen == 64빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* Image load offset(2MB) from start of RAM */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
.dword 0x20000064-bit S-mode kernel이 RAM 시작에서 2MiB offset에 적재되기를 기대하는 Image header field다.
#else빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* Image load offset(4MB) from start of RAM */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
.dword 0x400000assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* Effective size of kernel image */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
.dword _end - _start최종 image의 유효 크기를 linker symbol 차이로 기록한다.
.dword __HEAD_FLAGSassembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.
.word RISCV_HEADER_VERSIONassembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.
.word 0assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.
.dword 0assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.
.ascii RISCV_IMAGE_MAGICbootloader가 RISC-V Linux Image인지 확인하는 magic field다.
.balign 4linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
.ascii RISCV_IMAGE_MAGIC2bootloader가 RISC-V Linux Image인지 확인하는 magic field다.
#ifdef CONFIG_EFI빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
.word pe_head_start - _startassembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.
pe_head_start:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
__EFI_PE_HEADER이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
#else빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
.word 0assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
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 */73-126줄 주석
.global relocate_enable_mmulinker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
relocate_enable_mmu:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
/* Relocate return address */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
la a1, kernel_map주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
XIP_FIXUP_OFFSET a1이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
REG_L a1, KERNEL_MAP_VIRT_ADDR(a1)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
la a2, _start주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
sub a1, a1, a2주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
add ra, ra, a1함수 복귀 주소를 physical-to-virtual offset만큼 옮긴다. MMU enable 뒤 ret가 virtual code로 돌아가게 한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Point stvec to virtual address of intruction after satp write */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
la a2, 1f주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
add a2, a2, a1주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
csrw CSR_TVEC, a2첫 SATP write가 예상대로 fall-through하지 않을 때도 virtual continuation으로 제어를 옮길 임시 trap target을 둔다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Compute satp for kernel page tables, but don't load it yet */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
srl a2, a0, PAGE_SHIFT이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
la a1, satp_mode주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
XIP_FIXUP_OFFSET a1이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
REG_L a1, 0(a1)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
or a2, a2, a1주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Load trampoline page directory, which will cause us to trap to원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* stvec if VA != PA, or simply fall through if VA == PA. We need a원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* full fence here because setup_vm() just wrote these PTEs and we need원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* to ensure the new translations are in use.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
la a0, trampoline_pg_dir현재 instruction이 있는 첫 superpage를 주소 전환 양쪽에서 접근 가능하게 하는 최소 page table이다.
XIP_FIXUP_OFFSET a0이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
srl a0, a0, PAGE_SHIFT이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
or a0, a0, a1주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
sfence.vma새 SATP와 PTE를 이후 fetch/load가 확실히 사용하도록 translation cache를 정리한다.
csrw CSR_SATP, a0trampoline root를 활성화하는 첫 주소 공간 전환이다.
.align 2linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
1:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
/* Set trap vector to spin forever to help debug */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
la a0, .Lsecondary_park주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
csrw CSR_TVEC, a0RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Reload the global pointer */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
load_global_pointervirtual address 실행으로 바뀐 뒤 gp를 새 주소 기준으로 다시 적재한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Switch to kernel page tables. A full fence is necessary in order to원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* avoid using the trampoline translations, which are only correct for원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* the first superpage. Fetching the fence is guaranteed to work원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* because that first superpage is translated the same way.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
csrw CSR_SATP, a2최소 trampoline을 실제 kernel page table로 교체한다.
sfence.vma새 SATP와 PTE를 이후 fetch/load가 확실히 사용하도록 translation cache를 정리한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
ret이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
#endif /* CONFIG_MMU */빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
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_kernel203-340줄 주석
SYM_CODE_START(_start_kernel)linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
/* Mask all interrupts */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
csrw CSR_IE, zero초기 trap vector와 per-hart stack이 준비될 때까지 interrupt enable을 모두 내린다.
csrw CSR_IP, zeroRISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_RISCV_M_MODE빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* flush the instruction cache */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
fence.i메모리 또는 instruction fetch의 관찰 순서를 강제한다. 단순 compiler barrier가 아니라 hart가 보는 상태를 바꾸는 명령이다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Reset all registers except ra, a0, a1 */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
call reset_regs하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Setup a PMP to permit access to all of memory. Some machines may원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* not implement PMPs, so we set up a quick trap handler to just skip원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* touching the PMPs on any trap.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
la a0, .Lpmp_done주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
csrw CSR_TVEC, a0RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
li a0, -1이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
csrw CSR_PMPADDR0, a0RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
li a0, (PMP_A_NAPOT | PMP_R | PMP_W | PMP_X)이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
csrw CSR_PMPCFG0, a0RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
.align 2linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
.Lpmp_done:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* The hartid in a0 is expected later on, and we have no firmware원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* to hand it to us.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
csrr a0, CSR_MHARTIDM-mode kernel은 firmware argument 대신 hardware CSR에서 현재 hart ID를 직접 읽는다.
#else빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* Enable time CSR */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
li t0, 0x2이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
csrw CSR_SCOUNTEREN, t0RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
#endif /* CONFIG_RISCV_M_MODE */빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Load the global pointer */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
load_global_pointer이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Disable FPU & VECTOR to detect illegal usage of원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* floating point or vector in kernel space원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
li t0, SR_FS_VS이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
csrc CSR_STATUS, t0RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_RISCV_BOOT_SPINWAIT빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
li t0, CONFIG_NR_CPUS이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
blt a0, t0, .Lgood_cores직전에 만든 값이나 flag를 검사해 경로를 나눈다. 실패 분기가 어디에서 멈추는지까지 따라가야 조건의 의미가 완성된다.
tail .Lsecondary_park현재 함수로 돌아오지 않는 제어 이전이다. 다음 진입점이 요구하는 register와 주소 공간을 이 줄 직전에 모두 완성해야 한다.
.Lgood_cores:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* The lottery system is only required for spinwait booting method */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
#ifndef CONFIG_XIP_KERNEL빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* Pick one hart to run the main boot sequence */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
la a3, hart_lotteryspinwait boot 방식에서 단 하나의 hart만 global 초기화를 수행하도록 원자적 lottery를 사용한다.
li a2, 1이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
amoadd.w a3, a2, (a3)원자적 read-modify-write로 여러 hart 사이의 선택 또는 게시를 직렬화한다. 반환값은 갱신 전 값이라는 점이 분기 조건에 중요하다.
bnez a3, .Lsecondary_start직전에 만든 값이나 flag를 검사해 경로를 나눈다. 실패 분기가 어디에서 멈추는지까지 따라가야 조건의 의미가 완성된다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#else빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* hart_lottery in flash contains a magic number */spinwait boot 방식에서 단 하나의 hart만 global 초기화를 수행하도록 원자적 lottery를 사용한다.
la a3, hart_lotteryspinwait boot 방식에서 단 하나의 hart만 global 초기화를 수행하도록 원자적 lottery를 사용한다.
mv a2, a3주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
XIP_FIXUP_OFFSET a2이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
XIP_FIXUP_FLASH_OFFSET a3이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
lw t1, (a3)이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
amoswap.w t0, t1, (a2)원자적 read-modify-write로 여러 hart 사이의 선택 또는 게시를 직렬화한다. 반환값은 갱신 전 값이라는 점이 분기 조건에 중요하다.
/* first time here if hart_lottery in RAM is not set */spinwait boot 방식에서 단 하나의 hart만 global 초기화를 수행하도록 원자적 lottery를 사용한다.
beq t0, t1, .Lsecondary_start이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#endif /* CONFIG_XIP */빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
#endif /* CONFIG_RISCV_BOOT_SPINWAIT */빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_XIP_KERNEL빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
la sp, _end + THREAD_SIZE주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
XIP_FIXUP_OFFSET sp이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
mv s0, a0주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
mv s1, a1주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
call __copy_data하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Restore a0 & a1 copy */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
mv a0, s0주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
mv a1, s1주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifndef CONFIG_XIP_KERNEL빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* Clear BSS for flat non-ELF images */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
la a3, __bss_startflat Image에서 zero-init global의 C 언어 계약을 assembly가 직접 만족시킨다.
la a4, __bss_stop주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
ble a4, a3, .Lclear_bss_done이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
.Lclear_bss:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
REG_S zero, (a3)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
add a3, a3, RISCV_SZPTR주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
blt a3, a4, .Lclear_bss직전에 만든 값이나 flag를 검사해 경로를 나눈다. 실패 분기가 어디에서 멈추는지까지 따라가야 조건의 의미가 완성된다.
.Lclear_bss_done:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
la a2, boot_cpu_hartidgeneric SMP 코드가 사용할 대표 boot hart ID를 전역 위치에 게시한다.
XIP_FIXUP_OFFSET a2이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
REG_S a0, (a2)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Initialize page tables and relocate to virtual addresses */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
la tp, init_task주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
la sp, init_thread_union + THREAD_SIZEboot task의 kernel stack top을 만들고 pt_regs 예약분을 뺀다.
XIP_FIXUP_OFFSET sp이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
addi sp, sp, -PT_SIZE_ON_STACK주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
scs_load_init_stack이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
#ifdef CONFIG_BUILTIN_DTB빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
la a0, __dtb_start주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
XIP_FIXUP_OFFSET a0이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
#else빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
mv a0, a1주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
#endif /* CONFIG_BUILTIN_DTB */빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* Set trap vector to spin forever to help debug */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
la a3, .Lsecondary_park주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
csrw CSR_TVEC, a3RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
call setup_vmMMU-off C helper가 DTB와 kernel 배치를 바탕으로 early page table을 작성한다.
#ifdef CONFIG_MMU빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
la a0, early_pg_dir작성된 초기 page-table root를 relocate_enable_mmu에 넘긴다.
XIP_FIXUP_OFFSET a0이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
call relocate_enable_mmu하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
#endif /* CONFIG_MMU */빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
call .Lsetup_trap_vector하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
/* Restore C environment */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
la tp, init_task주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
la sp, init_thread_union + THREAD_SIZEboot task의 kernel stack top을 만들고 pt_regs 예약분을 뺀다.
addi sp, sp, -PT_SIZE_ON_STACK주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
scs_load_current이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_KASAN빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
call kasan_early_init하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* Start the kernel */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
call soc_early_init하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
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 */1119-1204줄 주석
asmlinkage void __init setup_vm(uintptr_t dtb_pa)이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
{이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
pmd_t __maybe_unused fix_bmap_spmd, fix_bmap_epmd;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_RANDOMIZE_BASE빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
if (!__pi_set_nokaslr_from_cmdline(dtb_pa)) {이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
u64 kaslr_seed = __pi_get_kaslr_seed_zkr(dtb_pa);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
u32 kernel_size = (uintptr_t)(&_end) - (uintptr_t)(&_start);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
u32 nr_pos;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
if (kaslr_seed == 0)이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
kaslr_seed = __pi_get_kaslr_seed(dtb_pa);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Compute the number of positions available: we are limited원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* by the early page table that only has one PUD and we must원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* be aligned on PMD_SIZE.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
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을 만든다.
}이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
kernel_map.virt_addr = KERNEL_LINK_ADDR + kernel_map.virt_offset;seed를 후보 수로 나눈 나머지에 PMD_SIZE를 곱해 2MiB 단위 virtual KASLR offset을 만든다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_XIP_KERNEL빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
kernel_map.xiprom = (uintptr_t)CONFIG_XIP_PHYS_ADDR;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
kernel_map.xiprom_sz = (uintptr_t)(&_exiprom) - (uintptr_t)(&_xiprom);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
phys_ram_base = CONFIG_PHYS_RAM_BASE;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
#ifdef CONFIG_SPARSEMEM_VMEMMAP빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
vmemmap_start_pfn = round_down(phys_ram_base, VMEMMAP_ADDR_ALIGN) >> PAGE_SHIFT;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
kernel_map.phys_addr = (uintptr_t)CONFIG_PHYS_RAM_BASE;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
kernel_map.size = (uintptr_t)(&_end) - (uintptr_t)(&_start);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
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;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
#else빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
kernel_map.phys_addr = (uintptr_t)(&_start);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
kernel_map.size = (uintptr_t)(&_end) - kernel_map.phys_addr;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
kernel_map.va_kernel_pa_offset = kernel_map.virt_addr - kernel_map.phys_addr;link address에 virtual KASLR offset을 더한 실제 kernel VA 시작점이다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#if defined(CONFIG_64BIT) && !defined(CONFIG_XIP_KERNEL)빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
set_satp_mode(dtb_pa);DTB와 hardware capability를 바탕으로 Sv39/Sv48/Sv57 후보를 검증한다. 지원되지 않는 mode를 SATP에 남기면 WARL 동작으로 write가 무시될 수 있다.
set_mmap_rnd_bits_max();이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* In 64-bit, we defer the setup of va_pa_offset to setup_bootmem,원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* where we have the system memory layout: this allows us to align원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* the physical and virtual mappings and then make use of PUD/P4D/PGD원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* for the linear mapping. This is only possible because the kernel원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* mapping lies outside the linear mapping.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* In 32-bit however, as the kernel resides in the linear mapping,원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* setup_vm_final can not change the mapping established here,원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* otherwise the same kernel addresses would get mapped to different원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* physical addresses (if the start of dram is different from the원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* kernel physical address start).원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
kernel_map.va_pa_offset = IS_ENABLED(CONFIG_64BIT) ?이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
0UL : PAGE_OFFSET - kernel_map.phys_addr;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
memory_limit = KERN_VIRT_SIZE;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Sanity check alignment and size */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
BUG_ON((PAGE_OFFSET % PGDIR_SIZE) != 0);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
BUG_ON((kernel_map.phys_addr % PMD_SIZE) != 0);초기 superpage mapping의 전제인 2MiB physical alignment를 명시적으로 검사한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_64BIT빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* The last 4K bytes of the addressable memory can not be mapped because원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* of IS_ERR_VALUE macro.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
BUG_ON((kernel_map.virt_addr + kernel_map.size) > ADDRESS_SPACE_END - SZ_4K);link address에 virtual KASLR offset을 더한 실제 kernel VA 시작점이다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_RELOCATABLE빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Early page table uses only one PUD, which makes it possible원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* to map PUD_SIZE aligned on PUD_SIZE: if the relocation offset원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* makes the kernel cross over a PUD_SIZE boundary, raise a bug원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* since a part of the kernel would not get mapped.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
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