QUESTION
trap이 발생했을 때 kernel은 원래 thread pointer와 stack을 어떻게 찾고, scause의 interrupt bit와 exception code를 어디에서 분리하는가?
RISC-V는 stvec가 가리키는 entry로 trap을 보내고 scause, sepc, stval, sstatus에 원인을 남긴다. Linux는 sscratch와 tp를 교환해 user에서 왔는지 kernel에서 왔는지 판별하고 task의 kernel stack에 pt_regs를 만든다.
scause의 최상위 bit는 interrupt 여부이고 나머지는 cause code다. assembly는 signed compare로 최상위 bit를 검사해 interrupt면 do_irq, exception이면 excp_vect_table[cause]로 보낸다.
secondary hart는 trap 진입과 별개의 초기화 경로를 가진다. SBI HSM이 secondary_start_sbi에 hartid와 boot data를 넘기고, assembly가 task pointer와 stack을 복원한 뒤 SATP와 stvec를 준비해 smp_callin()으로 들어간다.
ENTRY CONTRACT
들어오는 상태와 내보내는 상태
| 경계 | 입력 | 이 경계가 완성하는 상태 |
|---|---|---|
| User trap | sscratch=kernel tp, tp=user TLS | kernel tp와 task stack |
| Kernel trap | sscratch=0, tp=current | 현재 kernel stack 또는 overflow stack |
| Interrupt cause | scause MSB=1 | do_irq와 irqchip |
| Exception cause | scause MSB=0 | exception vector table |
| SBI hart start | a0=hartid, a1=boot data | smp_callin과 online CPU |
ARCHITECTURE FIGURES
주소와 register를 먼저 그려 본다
register save뿐 아니라 status, epc, bad address, cause와 원래 user tp를 한 frame에 고정한다.
MSB = 1
MSB = 0
최상위 bit는 interrupt 표식이고 나머지 index는 exception 또는 interrupt 원인을 나타낸다.
boot CPU의 cpu_start 요청과 secondary hart의 completion 사이를 분리한다.
SOURCE 01
handle_exception의 user/kernel 판별과 cause dispatch
sscratch 교환으로 출처를 판별하고 task kernel stack에 register frame을 만든 뒤 interrupt와 exception을 나눈다.
원본: arch/riscv/kernel/entry.S 96-204줄
96SYM_CODE_START(handle_exception)
97 /*
98 * If coming from userspace, preserve the user thread pointer and load
99 * the kernel thread pointer. If we came from the kernel, the scratch
100 * register will contain 0, and we should continue on the current TP.
101 */
102 csrrw tp, CSR_SCRATCH, tp
103 bnez tp, .Lsave_context
104
105.Lrestore_kernel_tpsp:
106 csrr tp, CSR_SCRATCH
107
108#ifdef CONFIG_64BIT
109 /*
110 * The RISC-V kernel does not eagerly emit a sfence.vma after each
111 * new vmalloc mapping, which may result in exceptions:
112 * - if the uarch caches invalid entries, the new mapping would not be
113 * observed by the page table walker and an invalidation is needed.
114 * - if the uarch does not cache invalid entries, a reordered access
115 * could "miss" the new mapping and traps: in that case, we only need
116 * to retry the access, no sfence.vma is required.
117 */
118 new_vmalloc_check
119#endif
120
121 REG_S sp, TASK_TI_KERNEL_SP(tp)
122
123#ifdef CONFIG_VMAP_STACK
124 addi sp, sp, -(PT_SIZE_ON_STACK)
125 srli sp, sp, THREAD_SHIFT
126 andi sp, sp, 0x1
127 bnez sp, handle_kernel_stack_overflow
128 REG_L sp, TASK_TI_KERNEL_SP(tp)
129#endif
130
131.Lsave_context:
132 REG_S sp, TASK_TI_USER_SP(tp)
133 REG_L sp, TASK_TI_KERNEL_SP(tp)
134 addi sp, sp, -(PT_SIZE_ON_STACK)
135 REG_S x1, PT_RA(sp)
136 REG_S x3, PT_GP(sp)
137 REG_S x5, PT_T0(sp)
138 save_from_x6_to_x31
139
140 /*
141 * Disable user-mode memory access as it should only be set in the
142 * actual user copy routines.
143 *
144 * Disable the FPU/Vector to detect illegal usage of floating point
145 * or vector in kernel space.
146 */
147 li t0, SR_SUM | SR_FS_VS
148
149 REG_L s0, TASK_TI_USER_SP(tp)
150 csrrc s1, CSR_STATUS, t0
151 csrr s2, CSR_EPC
152 csrr s3, CSR_TVAL
153 csrr s4, CSR_CAUSE
154 csrr s5, CSR_SCRATCH
155 REG_S s0, PT_SP(sp)
156 REG_S s1, PT_STATUS(sp)
157 REG_S s2, PT_EPC(sp)
158 REG_S s3, PT_BADADDR(sp)
159 REG_S s4, PT_CAUSE(sp)
160 REG_S s5, PT_TP(sp)
161
162 /*
163 * Set the scratch register to 0, so that if a recursive exception
164 * occurs, the exception vector knows it came from the kernel
165 */
166 csrw CSR_SCRATCH, x0
167
168 /* Load the global pointer */
169 load_global_pointer
170
171 /* Load the kernel shadow call stack pointer if coming from userspace */
172 scs_load_current_if_task_changed s5
173
174#ifdef CONFIG_RISCV_ISA_V_PREEMPTIVE
175 move a0, sp
176 call riscv_v_context_nesting_start
177#endif
178 move a0, sp /* pt_regs */
179
180 /*
181 * MSB of cause differentiates between
182 * interrupts and exceptions
183 */
184 bge s4, zero, 1f
185
186 /* Handle interrupts */
187 call do_irq
188 j ret_from_exception
1891:
190 /* Handle other exceptions */
191 slli t0, s4, RISCV_LGPTR
192 la t1, excp_vect_table
193 la t2, excp_vect_table_end
194 add t0, t1, t0
195 /* Check if exception code lies within bounds */
196 bgeu t0, t2, 3f
197 REG_L t1, 0(t0)
1982: jalr t1
199 j ret_from_exception
2003:
201
202 la t1, do_trap_unknown
203 j 2b
204SYM_CODE_END(handle_exception)96-204줄 주석
SYM_CODE_START(handle_exception)linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* If coming from userspace, preserve the user thread pointer and load원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* the kernel thread pointer. If we came from the kernel, the scratch원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* register will contain 0, and we should continue on the current TP.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
csrrw tp, CSR_SCRATCH, tptp와 sscratch를 원자적으로 교환한다. user 진입이면 새 tp가 kernel task pointer가 되고, kernel 진입이면 0을 받아 별도 복구 경로로 간다.
bnez tp, .Lsave_context직전에 만든 값이나 flag를 검사해 경로를 나눈다. 실패 분기가 어디에서 멈추는지까지 따라가야 조건의 의미가 완성된다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
.Lrestore_kernel_tpsp:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
csrr tp, CSR_SCRATCHRISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_64BIT빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* The RISC-V kernel does not eagerly emit a sfence.vma after each원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* new vmalloc mapping, which may result in exceptions:원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* - if the uarch caches invalid entries, the new mapping would not be원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* observed by the page table walker and an invalidation is needed.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* - if the uarch does not cache invalid entries, a reordered access원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* could "miss" the new mapping and traps: in that case, we only need원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* to retry the access, no sfence.vma is required.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
new_vmalloc_check이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
REG_S sp, TASK_TI_KERNEL_SP(tp)current task에 저장된 kernel stack top을 읽어 trap frame을 놓을 공간을 만든다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_VMAP_STACK빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
addi sp, sp, -(PT_SIZE_ON_STACK)주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
srli sp, sp, THREAD_SHIFT주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
andi sp, sp, 0x1이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
bnez sp, handle_kernel_stack_overflowVMAP stack guard를 넘은 경우 손상된 task stack 대신 per-CPU overflow stack으로 전환한다.
REG_L sp, TASK_TI_KERNEL_SP(tp)current task에 저장된 kernel stack top을 읽어 trap frame을 놓을 공간을 만든다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
.Lsave_context:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
REG_S sp, TASK_TI_USER_SP(tp)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
REG_L sp, TASK_TI_KERNEL_SP(tp)current task에 저장된 kernel stack top을 읽어 trap frame을 놓을 공간을 만든다.
addi sp, sp, -(PT_SIZE_ON_STACK)주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
REG_S x1, PT_RA(sp)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
REG_S x3, PT_GP(sp)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
REG_S x5, PT_T0(sp)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
save_from_x6_to_x31이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Disable user-mode memory access as it should only be set in the원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* actual user copy routines.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Disable the FPU/Vector to detect illegal usage of floating point원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* or vector in kernel space.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
li t0, SR_SUM | SR_FS_VShandler가 실수로 user memory, FPU, vector state를 사용하지 못하도록 status bit를 내린다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
REG_L s0, TASK_TI_USER_SP(tp)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
csrrc s1, CSR_STATUS, t0RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
csrr s2, CSR_EPCtrap이 끝난 뒤 재개하거나 수정할 instruction address를 frame에 보존한다.
csrr s3, CSR_TVALpage fault address나 illegal instruction 보조 정보처럼 cause별 추가 값을 보존한다.
csrr s4, CSR_CAUSEMSB interrupt bit와 cause number를 보존한다.
csrr s5, CSR_SCRATCHRISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
REG_S s0, PT_SP(sp)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
REG_S s1, PT_STATUS(sp)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
REG_S s2, PT_EPC(sp)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
REG_S s3, PT_BADADDR(sp)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
REG_S s4, PT_CAUSE(sp)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
REG_S s5, PT_TP(sp)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Set the scratch register to 0, so that if a recursive exception원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* occurs, the exception vector knows it came from the kernel원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
csrw CSR_SCRATCH, x0RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Load the global pointer */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
load_global_pointer이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Load the kernel shadow call stack pointer if coming from userspace */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
scs_load_current_if_task_changed s5이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_RISCV_ISA_V_PREEMPTIVE빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
move a0, sp이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
call riscv_v_context_nesting_start하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
move a0, sp /* pt_regs */이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* MSB of cause differentiates between원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* interrupts and exceptions원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
bge s4, zero, 1fsigned compare로 scause MSB를 검사한다. 음수면 interrupt, 0 이상이면 synchronous exception이다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Handle interrupts */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
call do_irq하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
j ret_from_exception현재 함수로 돌아오지 않는 제어 이전이다. 다음 진입점이 요구하는 register와 주소 공간을 이 줄 직전에 모두 완성해야 한다.
1:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
/* Handle other exceptions */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
slli t0, s4, RISCV_LGPTR주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
la t1, excp_vect_tableexception cause를 pointer-size만큼 이동해 해당 C handler address를 읽는다.
la t2, excp_vect_table_endexception cause를 pointer-size만큼 이동해 해당 C handler address를 읽는다.
add t0, t1, t0주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
/* Check if exception code lies within bounds */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
bgeu t0, t2, 3f직전에 만든 값이나 flag를 검사해 경로를 나눈다. 실패 분기가 어디에서 멈추는지까지 따라가야 조건의 의미가 완성된다.
REG_L t1, 0(t0)XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.
2: jalr t1하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
j ret_from_exception현재 함수로 돌아오지 않는 제어 이전이다. 다음 진입점이 요구하는 register와 주소 공간을 이 줄 직전에 모두 완성해야 한다.
3:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
la t1, do_trap_unknown주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.
j 2b현재 함수로 돌아오지 않는 제어 이전이다. 다음 진입점이 요구하는 register와 주소 공간을 이 줄 직전에 모두 완성해야 한다.
SYM_CODE_END(handle_exception)linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.
SOURCE 02
secondary_start_sbi가 task와 stack을 받아 MMU를 켜는 경로
SBI HSM이 시작한 hart는 interrupt를 막고 boot data에서 idle task pointer와 stack pointer를 꺼낸 뒤 boot CPU가 만든 page table을 사용한다.
원본: arch/riscv/kernel/head.S 128-175줄
128 .global secondary_start_sbi
129secondary_start_sbi:
130 /* Mask all interrupts */
131 csrw CSR_IE, zero
132 csrw CSR_IP, zero
133
134#ifndef CONFIG_RISCV_M_MODE
135 /* Enable time CSR */
136 li t0, 0x2
137 csrw CSR_SCOUNTEREN, t0
138#endif
139
140 /* Load the global pointer */
141 load_global_pointer
142
143 /*
144 * Disable FPU & VECTOR to detect illegal usage of
145 * floating point or vector in kernel space
146 */
147 li t0, SR_FS_VS
148 csrc CSR_STATUS, t0
149
150 /* Set trap vector to spin forever to help debug */
151 la a3, .Lsecondary_park
152 csrw CSR_TVEC, a3
153
154 /* a0 contains the hartid & a1 contains boot data */
155 li a2, SBI_HART_BOOT_TASK_PTR_OFFSET
156 XIP_FIXUP_OFFSET a2
157 add a2, a2, a1
158 REG_L tp, (a2)
159 li a3, SBI_HART_BOOT_STACK_PTR_OFFSET
160 XIP_FIXUP_OFFSET a3
161 add a3, a3, a1
162 REG_L sp, (a3)
163
164.Lsecondary_start_common:
165
166#ifdef CONFIG_MMU
167 /* Enable virtual memory and relocate to virtual address */
168 la a0, swapper_pg_dir
169 XIP_FIXUP_OFFSET a0
170 call relocate_enable_mmu
171#endif
172 call .Lsetup_trap_vector
173 scs_load_current
174 call smp_callin
175#endif /* CONFIG_SMP */128-175줄 주석
.global secondary_start_sbiSBI가 physical entry address로 시작하는 secondary hart 전용 symbol이다.
secondary_start_sbi:SBI가 physical entry address로 시작하는 secondary hart 전용 symbol이다.
/* Mask all interrupts */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
csrw CSR_IE, zero외부 비동기 진입을 막는다. 이후 코드가 예외 벡터와 stack을 완성하기 전에는 interrupt handler가 실행되어서는 안 된다.
csrw CSR_IP, zeroRISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifndef CONFIG_RISCV_M_MODE빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* Enable time CSR */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
li t0, 0x2이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
csrw CSR_SCOUNTEREN, t0RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.
#endif빌드 설정에 따라 실제 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 연산과 다르다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* 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 연산과 다르다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* a0 contains the hartid & a1 contains boot data */SBI HSM contract에 따라 a0에는 hart ID, a1에는 kernel이 준비한 per-hart boot data가 들어 있다.
li a2, SBI_HART_BOOT_TASK_PTR_OFFSET이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
XIP_FIXUP_OFFSET a2이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
add a2, a2, a1주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
REG_L tp, (a2)boot data에서 이 hart의 idle task pointer를 복원해 current 기반을 만든다.
li a3, SBI_HART_BOOT_STACK_PTR_OFFSET이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
XIP_FIXUP_OFFSET a3이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
add a3, a3, a1주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.
REG_L sp, (a3)이 hart 전용 kernel stack을 복원한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
.Lsecondary_start_common:분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
#ifdef CONFIG_MMU빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
/* Enable virtual memory and relocate to virtual address */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
la a0, swapper_pg_dirboot CPU가 완성한 kernel page-table root를 secondary hart에도 활성화한다.
XIP_FIXUP_OFFSET a0이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
call relocate_enable_mmu하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
#endif빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
call .Lsetup_trap_vector하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.
scs_load_current이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
call smp_callinarchitecture assembly 준비를 끝내고 C CPU hotplug/online 단계로 들어간다.
#endif /* CONFIG_SMP */빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.
SOURCE 03
boot CPU 요청과 secondary hart의 online 게시
__cpu_up()은 architecture cpu_ops를 통해 hart를 시작하고 completion을 기다린다. secondary의 smp_callin()은 IPI, topology와 cache/TLB를 준비한 뒤 online bit를 게시한다.
원본: arch/riscv/kernel/smpboot.c 174-252줄
174static int start_secondary_cpu(int cpu, struct task_struct *tidle)
175{
176 if (cpu_ops->cpu_start)
177 return cpu_ops->cpu_start(cpu, tidle);
178
179 return -EOPNOTSUPP;
180}
181
182int __cpu_up(unsigned int cpu, struct task_struct *tidle)
183{
184 int ret = 0;
185 tidle->thread_info.cpu = cpu;
186
187 ret = start_secondary_cpu(cpu, tidle);
188 if (!ret) {
189 wait_for_completion_timeout(&cpu_running,
190 msecs_to_jiffies(1000));
191
192 if (!cpu_online(cpu)) {
193 pr_crit("CPU%u: failed to come online\n", cpu);
194 ret = -EIO;
195 }
196 } else {
197 pr_crit("CPU%u: failed to start\n", cpu);
198 }
199
200 return ret;
201}
202
203void __init smp_cpus_done(unsigned int max_cpus)
204{
205}
206
207/*
208 * C entry point for a secondary processor.
209 */
210asmlinkage __visible void smp_callin(void)
211{
212 struct mm_struct *mm = &init_mm;
213 unsigned int curr_cpuid = smp_processor_id();
214
215 if (has_vector()) {
216 /*
217 * Return as early as possible so the hart with a mismatching
218 * vlen won't boot.
219 */
220 if (riscv_v_setup_vsize())
221 return;
222 }
223
224 /* All kernel threads share the same mm context. */
225 mmgrab(mm);
226 current->active_mm = mm;
227
228 store_cpu_topology(curr_cpuid);
229 notify_cpu_starting(curr_cpuid);
230
231 riscv_ipi_enable();
232
233 numa_add_cpu(curr_cpuid);
234
235 pr_debug("CPU%u: Booted secondary hartid %lu\n", curr_cpuid,
236 cpuid_to_hartid_map(curr_cpuid));
237
238 set_cpu_online(curr_cpuid, true);
239
240 /*
241 * Remote cache and TLB flushes are ignored while the CPU is offline,
242 * so flush them both right now just in case.
243 */
244 local_flush_icache_all();
245 local_flush_tlb_all();
246 complete(&cpu_running);
247 /*
248 * Disable preemption before enabling interrupts, so we don't try to
249 * schedule a CPU that hasn't actually started yet.
250 */
251 local_irq_enable();
252 cpu_startup_entry(CPUHP_AP_ONLINE_IDLE);174-252줄 주석
static int start_secondary_cpu(int cpu, struct task_struct *tidle)이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
{이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
if (cpu_ops->cpu_start)SBI HSM 또는 spinwait 등 선택된 boot method의 시작 함수를 호출한다.
return cpu_ops->cpu_start(cpu, tidle);SBI HSM 또는 spinwait 등 선택된 boot method의 시작 함수를 호출한다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
return -EOPNOTSUPP;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
}이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
int __cpu_up(unsigned int cpu, struct task_struct *tidle)이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
{이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
int ret = 0;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
tidle->thread_info.cpu = cpu;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
ret = start_secondary_cpu(cpu, tidle);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
if (!ret) {이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
wait_for_completion_timeout(&cpu_running,secondary가 C online 경로에 도달하는 데 1초 제한을 둔다. timeout과 start request 자체의 오류를 구분한다.
msecs_to_jiffies(1000));이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
if (!cpu_online(cpu)) {이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
pr_crit("CPU%u: failed to come online\n", cpu);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
ret = -EIO;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
}이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
} else {이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
pr_crit("CPU%u: failed to start\n", cpu);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
}이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
return ret;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
}이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
void __init smp_cpus_done(unsigned int max_cpus)이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
{이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
}이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* C entry point for a secondary processor.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
asmlinkage __visible void smp_callin(void)이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
{이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
struct mm_struct *mm = &init_mm;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
unsigned int curr_cpuid = smp_processor_id();이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
if (has_vector()) {이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Return as early as possible so the hart with a mismatching원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* vlen won't boot.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
if (riscv_v_setup_vsize())이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
return;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
}이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* All kernel threads share the same mm context. */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
mmgrab(mm);idle kernel thread가 init_mm을 active_mm으로 참조하도록 수명을 보존한다.
current->active_mm = mm;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
store_cpu_topology(curr_cpuid);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
notify_cpu_starting(curr_cpuid);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
riscv_ipi_enable();이 hart가 reschedule, call-function, TLB shootdown 같은 IPI를 받을 수 있게 local interrupt source를 연다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
numa_add_cpu(curr_cpuid);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
pr_debug("CPU%u: Booted secondary hartid %lu\n", curr_cpuid,이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
cpuid_to_hartid_map(curr_cpuid));이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
set_cpu_online(curr_cpuid, true);scheduler와 다른 subsystem이 이 CPU를 사용 가능한 것으로 관찰하는 게시 지점이다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Remote cache and TLB flushes are ignored while the CPU is offline,원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* so flush them both right now just in case.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
local_flush_icache_all();offline 동안 무시된 remote instruction-cache flush를 online 직전에 보충한다.
local_flush_tlb_all();이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
complete(&cpu_running);boot CPU의 __cpu_up() 대기를 해제한다.
/*원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* Disable preemption before enabling interrupts, so we don't try to원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
* schedule a CPU that hasn't actually started yet.원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
*/원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
local_irq_enable();preemption과 online 준비를 맞춘 뒤 local interrupt를 허용한다.
cpu_startup_entry(CPUHP_AP_ONLINE_IDLE);이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
SOURCE 04
SBI HSM 호출 직전 task·stack을 physical boot data로 게시하는 구간
boot CPU는 hart를 깨우기 전에 secondary가 읽을 idle task와 stack pointer를 기록하고 memory barrier를 둔다. SBI에는 kernel virtual pointer가 아니라 entry와 boot data의 physical address를 전달한다.
원본: arch/riscv/kernel/cpu_ops_sbi.c 65-80줄
65static int sbi_cpu_start(unsigned int cpuid, struct task_struct *tidle)
66{
67 unsigned long boot_addr = __pa_symbol(secondary_start_sbi);
68 unsigned long hartid = cpuid_to_hartid_map(cpuid);
69 unsigned long hsm_data;
70 struct sbi_hart_boot_data *bdata = &boot_data[cpuid];
71
72 /* Make sure tidle is updated */
73 smp_mb();
74 bdata->task_ptr = tidle;
75 bdata->stack_ptr = task_pt_regs(tidle);
76 /* Make sure boot data is updated */
77 smp_mb();
78 hsm_data = __pa(bdata);
79 return sbi_hsm_hart_start(hartid, boot_addr, hsm_data);
80}65-80줄 주석
static int sbi_cpu_start(unsigned int cpuid, struct task_struct *tidle)이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
{이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
unsigned long boot_addr = __pa_symbol(secondary_start_sbi);firmware가 MMU-off hart의 PC에 넣을 수 있도록 secondary assembly entry의 physical address를 계산한다.
unsigned long hartid = cpuid_to_hartid_map(cpuid);Linux logical CPU 번호와 firmware가 식별하는 sparse hart ID를 변환한다. 둘을 같은 숫자로 가정하면 다른 hart를 시작할 수 있다.
unsigned long hsm_data;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
struct sbi_hart_boot_data *bdata = &boot_data[cpuid];이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
(빈 줄)빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.
/* Make sure tidle is updated */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
smp_mb();idle task 초기화가 boot data publication보다 먼저 보이도록 하고, boot data store가 SBI start request보다 먼저 보이도록 두 경계를 만든다.
bdata->task_ptr = tidle;이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
bdata->stack_ptr = task_pt_regs(tidle);idle task kernel stack의 pt_regs 위치를 secondary가 초기 SP로 사용할 값으로 게시한다.
/* Make sure boot data is updated */원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.
smp_mb();idle task 초기화가 boot data publication보다 먼저 보이도록 하고, boot data store가 SBI start request보다 먼저 보이도록 두 경계를 만든다.
hsm_data = __pa(bdata);SBI가 MMU-off 상태의 a1에 넣을 boot data physical address로 바꾼다. secondary는 SATP 활성화 전 이 주소를 읽는다.
return sbi_hsm_hart_start(hartid, boot_addr, hsm_data);hart ID, physical entry, physical opaque data 세 값을 firmware HSM state machine에 넘긴다.
}이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.
WORKED EXAMPLES
숫자를 넣어 끝까지 계산한다
scause를 interrupt bit와 code로 분리한다
RV64에서 supervisor timer interrupt의 scause=0x8000000000000005, load page fault의 scause=0x000000000000000d를 비교한다.
- timer interruptbit63=1이므로 interrupt이고 code는 하위 63bit의 5다.
do_irq에서 timer irqchip 경로로 간다. - load page faultbit63=0이므로 synchronous exception이고 code 13으로 exception vector table을 찾는다.
- sepctimer에서는 중단된 instruction, page fault에서는 fault를 일으킨 load instruction 주소다.
- stvalpage fault에서는 접근한 virtual address가 유효하지만 interrupt에서는 원인 해석에 쓰지 않는다.
판정로그에는 interrupt/code, sepc, stval, sstatus.SPP를 함께 남겨 user/kernel 출처와 재개 지점을 동시에 판별한다.
SBI HSM에 넘기는 logical CPU와 hart ID를 구분한다
Linux logical CPU 2가 DT의 hart ID 7에 대응하고, secondary_start_sbi PA=0x80200100, boot data PA=0x81002000이라고 가정한다.
- mapping
cpuid_to_hartid_map(2)의 결과 7을 HSM hartid에 사용한다. - start addressMMU-off firmware가 사용할 수 있도록 entry는 virtual symbol이 아닌 physical
0x80200100이다. - opaque argumenta1에는 boot data physical address
0x81002000이 들어가며 그 안의 task/SP store가 먼저 visible해야 한다. - completionHSM call 성공과 Linux CPU online은 다르다. secondary의
smp_callin()completion까지 측정한다.
판정timeout 분석에서는 logical CPU, hart ID, HSM return, secondary a0/a1, completion timestamp를 같은 행에 기록한다.
DEEP DIVE
코드에서 놓치기 쉬운 경계
sscratch는 user TLS를 저장하는 register가 아니다
user mode에서는 tp가 TLS를 가리키므로 kernel은 sscratch에 current task pointer를 준비해 둔다. trap 첫 줄에서 둘을 교환하면 한 명령으로 kernel current를 얻고 user tp를 보존할 수 있다.
kernel 안에서 trap이 중첩될 때 sscratch를 0으로 두는 것은 이미 kernel tp와 stack을 쓰고 있음을 표시한다.
pt_regs는 복귀에 필요한 최소 architecture 상태의 소유자다
general registers 외에도 status, epc, bad address, cause가 frame에 들어간다. signal, ptrace, page fault, syscall은 이 frame을 읽거나 일부 값을 바꿀 수 있다.
handler가 frame pointer를 장기 보관하면 안 된다. trap return과 함께 stack frame 수명이 끝나기 때문이다.
new_vmalloc_check는 모든 page fault를 처리하지 않는다
RISC-V 구현이 invalid PTE를 cache했거나 access가 page-table update보다 먼저 관찰된 특수한 kernel vmalloc fault를 빠르게 재시도하는 경로다. 일반 user page fault는 exception table의 page-fault handler로 간다.
sfence.vma가 필요한 CPU와 단순 retry로 충분한 CPU를 feature 조건으로 나눈다는 점이 중요하다.
interrupt bit와 cause number를 분리해서 기록한다
scause를 10진수 한 값으로만 찍으면 최상위 interrupt bit 때문에 매우 큰 숫자로 보인다. is_interrupt = scause >> (XLEN-1), code = scause & ~(1UL << (XLEN-1))로 나눠야 한다.
sepc와 stval의 의미는 cause code마다 달라지므로 세 register를 한 묶음으로 남긴다.
SBI는 interrupt controller가 아니라 firmware 호출 경계다
SBI HSM은 hart start/stop을 제공하고 SBI IPI extension은 remote hart에 IPI를 요청할 수 있다. 실제 local interrupt pending과 irqchip dispatch는 Linux의 RISC-V interrupt architecture에서 별도로 처리된다.
OpenSBI log와 Linux IPI trace를 같은 timestamp 축에 놓으면 firmware request와 kernel handler 사이를 구분할 수 있다.
CPU online 게시 전 cache와 TLB 부채를 갚는다
offline CPU에는 remote flush가 생략될 수 있으므로 smp_callin()이 local I-cache와 TLB를 모두 비운 뒤 completion과 interrupt enable로 간다. 이 순서를 바꾸면 오래된 translation이나 instruction을 실행할 수 있다.
online bit, completion, IRQ enable은 비슷해 보여도 서로 다른 관찰자를 깨우는 세 경계다.
FAILURE PATH
멈춘 위치보다 먼저 볼 값
| 조건 | 관찰되는 증상 | 첫 확인 값 |
|---|---|---|
| sscratch 초기화 오류 | user trap에서 tp가 0이거나 user TLS를 current로 오해해 stack 전환이 깨진다. | stvec 설치와 sscratch 값 |
| pt_regs offset 불일치 | sepc/scause 또는 GPR 복원이 바뀌어 sret 직후 재 fault가 난다. | asm offset과 struct pt_regs |
| cause decoding 오류 | external interrupt를 exception handler로 보내거나 반대로 처리한다. | scause MSB와 code |
| SBI boot data 오류 | secondary hart가 0 stack 또는 잘못된 task pointer를 읽고 park된다. | a0/a1과 boot data fields |
| online 이전 flush 누락 | secondary에서만 stale instruction 또는 TLB fault가 재현된다. | local flush와 completion 순서 |
LAB
직접 확인할 실험
- 01trap entry breakpoint에서 sscratch, tp, sp, scause, sepc, stval을 user page fault와 timer interrupt 각각 기록한다.
명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
- 02
trace-cmd record -e irq -e ipi로 local IRQ와 IPI를 분리해 수집한다.명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
- 03OpenSBI HSM debug log와 Linux
CPU%u: Booted secondary hartid로그의 hart ID를 대조한다.명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
- 04CPU hotplug를 반복하면서
__cpu_up,secondary_start_sbi,smp_callin, completion의 timestamp를 기록한다.명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.
PRIMARY REFERENCES