← KernelDUJINLABS.COM

Linux 6.18.37 LTS · RISC-V trap and SMP

RISC-V trap frame과 SBI secondary hart 기동

handle_exception, scause/sepc/stval, sscratch, secondary_start_sbismp_callin()을 연결해 user/kernel trap과 보조 hart online 경계를 읽습니다.

Series
04 / 4
Baseline
Linux 6.18.37 LTS
Architecture
RISC-V 64
Source
arch/riscv/kernel/entry.S

trap이 발생했을 때 kernel은 원래 thread pointer와 stack을 어떻게 찾고, scause의 interrupt bit와 exception code를 어디에서 분리하는가?

RISC-V는 stvec가 가리키는 entry로 trap을 보내고 scause, sepc, stval, sstatus에 원인을 남긴다. Linux는 sscratchtp를 교환해 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()으로 들어간다.

들어오는 상태와 내보내는 상태

경계입력이 경계가 완성하는 상태
User trapsscratch=kernel tp, tp=user TLSkernel tp와 task stack
Kernel trapsscratch=0, tp=current현재 kernel stack 또는 overflow stack
Interrupt causescause MSB=1do_irq와 irqchip
Exception causescause MSB=0exception vector table
SBI hart starta0=hartid, a1=boot datasmp_callin과 online CPU

주소와 register를 먼저 그려 본다

그림 1. user trap에서 만들어지는 RISC-V pt_regs
+000rax1
+008gp/t0...general registers
+016user spTASK_TI_USER_SP
+024statussstatus/mstatus
+032epctrap 전 instruction
+040badaddrstval/mtval
+048causescause/mcause
+056tpuser thread pointer

register save뿐 아니라 status, epc, bad address, cause와 원래 user tp를 한 frame에 고정한다.

그림 2. scause 한 register에서 두 경로를 고르는 방식

MSB = 1

negative signed valuebge s4, zero가 성립하지 않음
do_irqirqchip이 pending source 확인
ret_from_exceptionframe 복원

MSB = 0

non-negativeexception path
excp_vect_tablecause × pointer size
page fault/syscall/illegal개별 C handler

최상위 bit는 interrupt 표식이고 나머지 index는 exception 또는 interrupt 원인을 나타낸다.

그림 3. SBI HSM으로 secondary hart를 online시키는 경로
__cpu_upidle task와 CPU number
SBI hart_startentry PA와 boot data
secondary_start_sbitp/sp 복원
SATP + stveckernel address와 trap
smp_callintopology·IPI·online
completionboot CPU 대기 해제

boot CPU의 cpu_start 요청과 secondary hart의 completion 사이를 분리한다.

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줄 주석

96SYM_CODE_START(handle_exception)

linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.

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*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

102csrrw tp, CSR_SCRATCH, tp

tp와 sscratch를 원자적으로 교환한다. user 진입이면 새 tp가 kernel task pointer가 되고, kernel 진입이면 0을 받아 별도 복구 경로로 간다.

103bnez tp, .Lsave_context

직전에 만든 값이나 flag를 검사해 경로를 나눈다. 실패 분기가 어디에서 멈추는지까지 따라가야 조건의 의미가 완성된다.

104(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

105.Lrestore_kernel_tpsp:

분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.

106csrr tp, CSR_SCRATCH

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

107(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

108#ifdef CONFIG_64BIT

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

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*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

118new_vmalloc_check

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

119#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

120(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

121REG_S sp, TASK_TI_KERNEL_SP(tp)

current task에 저장된 kernel stack top을 읽어 trap frame을 놓을 공간을 만든다.

122(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

123#ifdef CONFIG_VMAP_STACK

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

124addi sp, sp, -(PT_SIZE_ON_STACK)

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

125srli sp, sp, THREAD_SHIFT

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

126andi sp, sp, 0x1

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

127bnez sp, handle_kernel_stack_overflow

VMAP stack guard를 넘은 경우 손상된 task stack 대신 per-CPU overflow stack으로 전환한다.

128REG_L sp, TASK_TI_KERNEL_SP(tp)

current task에 저장된 kernel stack top을 읽어 trap frame을 놓을 공간을 만든다.

129#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

130(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

131.Lsave_context:

분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.

132REG_S sp, TASK_TI_USER_SP(tp)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

133REG_L sp, TASK_TI_KERNEL_SP(tp)

current task에 저장된 kernel stack top을 읽어 trap frame을 놓을 공간을 만든다.

134addi sp, sp, -(PT_SIZE_ON_STACK)

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

135REG_S x1, PT_RA(sp)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

136REG_S x3, PT_GP(sp)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

137REG_S x5, PT_T0(sp)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

138save_from_x6_to_x31

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

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*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

147li t0, SR_SUM | SR_FS_VS

handler가 실수로 user memory, FPU, vector state를 사용하지 못하도록 status bit를 내린다.

148(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

149REG_L s0, TASK_TI_USER_SP(tp)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

150csrrc s1, CSR_STATUS, t0

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

151csrr s2, CSR_EPC

trap이 끝난 뒤 재개하거나 수정할 instruction address를 frame에 보존한다.

152csrr s3, CSR_TVAL

page fault address나 illegal instruction 보조 정보처럼 cause별 추가 값을 보존한다.

153csrr s4, CSR_CAUSE

MSB interrupt bit와 cause number를 보존한다.

154csrr s5, CSR_SCRATCH

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

155REG_S s0, PT_SP(sp)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

156REG_S s1, PT_STATUS(sp)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

157REG_S s2, PT_EPC(sp)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

158REG_S s3, PT_BADADDR(sp)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

159REG_S s4, PT_CAUSE(sp)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

160REG_S s5, PT_TP(sp)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

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*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

166csrw CSR_SCRATCH, x0

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

167(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

168/* Load the global pointer */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

169load_global_pointer

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

170(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

171/* Load the kernel shadow call stack pointer if coming from userspace */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

172scs_load_current_if_task_changed s5

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

173(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

174#ifdef CONFIG_RISCV_ISA_V_PREEMPTIVE

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

175move a0, sp

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

176call riscv_v_context_nesting_start

하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.

177#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

178move a0, sp /* pt_regs */

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

179(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

180/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

181* MSB of cause differentiates between

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

182* interrupts and exceptions

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

183*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

184bge s4, zero, 1f

signed compare로 scause MSB를 검사한다. 음수면 interrupt, 0 이상이면 synchronous exception이다.

185(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

186/* Handle interrupts */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

187call do_irq

하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.

188j ret_from_exception

현재 함수로 돌아오지 않는 제어 이전이다. 다음 진입점이 요구하는 register와 주소 공간을 이 줄 직전에 모두 완성해야 한다.

1891:

분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.

190/* Handle other exceptions */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

191slli t0, s4, RISCV_LGPTR

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

192la t1, excp_vect_table

exception cause를 pointer-size만큼 이동해 해당 C handler address를 읽는다.

193la t2, excp_vect_table_end

exception cause를 pointer-size만큼 이동해 해당 C handler address를 읽는다.

194add t0, t1, t0

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

195/* Check if exception code lies within bounds */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

196bgeu t0, t2, 3f

직전에 만든 값이나 flag를 검사해 경로를 나눈다. 실패 분기가 어디에서 멈추는지까지 따라가야 조건의 의미가 완성된다.

197REG_L t1, 0(t0)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

1982: jalr t1

하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.

199j ret_from_exception

현재 함수로 돌아오지 않는 제어 이전이다. 다음 진입점이 요구하는 register와 주소 공간을 이 줄 직전에 모두 완성해야 한다.

2003:

분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.

201(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

202la t1, do_trap_unknown

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

203j 2b

현재 함수로 돌아오지 않는 제어 이전이다. 다음 진입점이 요구하는 register와 주소 공간을 이 줄 직전에 모두 완성해야 한다.

204SYM_CODE_END(handle_exception)

linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.

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줄 주석

128.global secondary_start_sbi

SBI가 physical entry address로 시작하는 secondary hart 전용 symbol이다.

129secondary_start_sbi:

SBI가 physical entry address로 시작하는 secondary hart 전용 symbol이다.

130/* Mask all interrupts */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

131csrw CSR_IE, zero

외부 비동기 진입을 막는다. 이후 코드가 예외 벡터와 stack을 완성하기 전에는 interrupt handler가 실행되어서는 안 된다.

132csrw CSR_IP, zero

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

133(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

134#ifndef CONFIG_RISCV_M_MODE

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

135/* Enable time CSR */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

136li t0, 0x2

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

137csrw CSR_SCOUNTEREN, t0

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

138#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

139(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

140/* Load the global pointer */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

141load_global_pointer

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

142(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

143/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

144* Disable FPU & VECTOR to detect illegal usage of

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

145* floating point or vector in kernel space

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

146*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

147li t0, SR_FS_VS

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

148csrc CSR_STATUS, t0

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

149(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

150/* Set trap vector to spin forever to help debug */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

151la a3, .Lsecondary_park

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

152csrw CSR_TVEC, a3

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

153(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

154/* a0 contains the hartid & a1 contains boot data */

SBI HSM contract에 따라 a0에는 hart ID, a1에는 kernel이 준비한 per-hart boot data가 들어 있다.

155li a2, SBI_HART_BOOT_TASK_PTR_OFFSET

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

156XIP_FIXUP_OFFSET a2

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

157add a2, a2, a1

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

158REG_L tp, (a2)

boot data에서 이 hart의 idle task pointer를 복원해 current 기반을 만든다.

159li a3, SBI_HART_BOOT_STACK_PTR_OFFSET

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

160XIP_FIXUP_OFFSET a3

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

161add a3, a3, a1

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

162REG_L sp, (a3)

이 hart 전용 kernel stack을 복원한다.

163(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

164.Lsecondary_start_common:

분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.

165(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

166#ifdef CONFIG_MMU

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

167/* Enable virtual memory and relocate to virtual address */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

168la a0, swapper_pg_dir

boot CPU가 완성한 kernel page-table root를 secondary hart에도 활성화한다.

169XIP_FIXUP_OFFSET a0

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

170call relocate_enable_mmu

하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.

171#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

172call .Lsetup_trap_vector

하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.

173scs_load_current

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

174call smp_callin

architecture assembly 준비를 끝내고 C CPU hotplug/online 단계로 들어간다.

175#endif /* CONFIG_SMP */

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

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줄 주석

174static int start_secondary_cpu(int cpu, struct task_struct *tidle)

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

175{

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

176if (cpu_ops->cpu_start)

SBI HSM 또는 spinwait 등 선택된 boot method의 시작 함수를 호출한다.

177return cpu_ops->cpu_start(cpu, tidle);

SBI HSM 또는 spinwait 등 선택된 boot method의 시작 함수를 호출한다.

178(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

179return -EOPNOTSUPP;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

180}

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

181(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

182int __cpu_up(unsigned int cpu, struct task_struct *tidle)

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

183{

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

184int ret = 0;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

185tidle->thread_info.cpu = cpu;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

186(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

187ret = start_secondary_cpu(cpu, tidle);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

188if (!ret) {

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

189wait_for_completion_timeout(&cpu_running,

secondary가 C online 경로에 도달하는 데 1초 제한을 둔다. timeout과 start request 자체의 오류를 구분한다.

190msecs_to_jiffies(1000));

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

191(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

192if (!cpu_online(cpu)) {

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

193pr_crit("CPU%u: failed to come online\n", cpu);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

194ret = -EIO;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

195}

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

196} else {

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

197pr_crit("CPU%u: failed to start\n", cpu);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

198}

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

199(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

200return ret;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

201}

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

202(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

203void __init smp_cpus_done(unsigned int max_cpus)

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

204{

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

205}

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

206(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

207/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

208* C entry point for a secondary processor.

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

209*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

210asmlinkage __visible void smp_callin(void)

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

211{

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

212struct mm_struct *mm = &init_mm;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

213unsigned int curr_cpuid = smp_processor_id();

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

214(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

215if (has_vector()) {

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

216/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

217* Return as early as possible so the hart with a mismatching

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

218* vlen won't boot.

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

219*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

220if (riscv_v_setup_vsize())

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

221return;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

222}

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

223(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

224/* All kernel threads share the same mm context. */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

225mmgrab(mm);

idle kernel thread가 init_mm을 active_mm으로 참조하도록 수명을 보존한다.

226current->active_mm = mm;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

227(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

228store_cpu_topology(curr_cpuid);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

229notify_cpu_starting(curr_cpuid);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

230(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

231riscv_ipi_enable();

이 hart가 reschedule, call-function, TLB shootdown 같은 IPI를 받을 수 있게 local interrupt source를 연다.

232(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

233numa_add_cpu(curr_cpuid);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

234(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

235pr_debug("CPU%u: Booted secondary hartid %lu\n", curr_cpuid,

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

236cpuid_to_hartid_map(curr_cpuid));

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

237(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

238set_cpu_online(curr_cpuid, true);

scheduler와 다른 subsystem이 이 CPU를 사용 가능한 것으로 관찰하는 게시 지점이다.

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*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

244local_flush_icache_all();

offline 동안 무시된 remote instruction-cache flush를 online 직전에 보충한다.

245local_flush_tlb_all();

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

246complete(&cpu_running);

boot CPU의 __cpu_up() 대기를 해제한다.

247/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

248* Disable preemption before enabling interrupts, so we don't try to

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

249* schedule a CPU that hasn't actually started yet.

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

250*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

251local_irq_enable();

preemption과 online 준비를 맞춘 뒤 local interrupt를 허용한다.

252cpu_startup_entry(CPUHP_AP_ONLINE_IDLE);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

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줄 주석

65static int sbi_cpu_start(unsigned int cpuid, struct task_struct *tidle)

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

66{

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

67unsigned long boot_addr = __pa_symbol(secondary_start_sbi);

firmware가 MMU-off hart의 PC에 넣을 수 있도록 secondary assembly entry의 physical address를 계산한다.

68unsigned long hartid = cpuid_to_hartid_map(cpuid);

Linux logical CPU 번호와 firmware가 식별하는 sparse hart ID를 변환한다. 둘을 같은 숫자로 가정하면 다른 hart를 시작할 수 있다.

69unsigned long hsm_data;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

70struct sbi_hart_boot_data *bdata = &boot_data[cpuid];

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

71(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

72/* Make sure tidle is updated */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

73smp_mb();

idle task 초기화가 boot data publication보다 먼저 보이도록 하고, boot data store가 SBI start request보다 먼저 보이도록 두 경계를 만든다.

74bdata->task_ptr = tidle;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

75bdata->stack_ptr = task_pt_regs(tidle);

idle task kernel stack의 pt_regs 위치를 secondary가 초기 SP로 사용할 값으로 게시한다.

76/* Make sure boot data is updated */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

77smp_mb();

idle task 초기화가 boot data publication보다 먼저 보이도록 하고, boot data store가 SBI start request보다 먼저 보이도록 두 경계를 만든다.

78hsm_data = __pa(bdata);

SBI가 MMU-off 상태의 a1에 넣을 boot data physical address로 바꾼다. secondary는 SATP 활성화 전 이 주소를 읽는다.

79return sbi_hsm_hart_start(hartid, boot_addr, hsm_data);

hart ID, physical entry, physical opaque data 세 값을 firmware HSM state machine에 넘긴다.

80}

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

숫자를 넣어 끝까지 계산한다

01

scause를 interrupt bit와 code로 분리한다

RV64에서 supervisor timer interrupt의 scause=0x8000000000000005, load page fault의 scause=0x000000000000000d를 비교한다.

  1. timer interruptbit63=1이므로 interrupt이고 code는 하위 63bit의 5다. do_irq에서 timer irqchip 경로로 간다.
  2. load page faultbit63=0이므로 synchronous exception이고 code 13으로 exception vector table을 찾는다.
  3. sepctimer에서는 중단된 instruction, page fault에서는 fault를 일으킨 load instruction 주소다.
  4. stvalpage fault에서는 접근한 virtual address가 유효하지만 interrupt에서는 원인 해석에 쓰지 않는다.

판정로그에는 interrupt/code, sepc, stval, sstatus.SPP를 함께 남겨 user/kernel 출처와 재개 지점을 동시에 판별한다.

02

SBI HSM에 넘기는 logical CPU와 hart ID를 구분한다

Linux logical CPU 2가 DT의 hart ID 7에 대응하고, secondary_start_sbi PA=0x80200100, boot data PA=0x81002000이라고 가정한다.

  1. mappingcpuid_to_hartid_map(2)의 결과 7을 HSM hartid에 사용한다.
  2. start addressMMU-off firmware가 사용할 수 있도록 entry는 virtual symbol이 아닌 physical 0x80200100이다.
  3. opaque argumenta1에는 boot data physical address 0x81002000이 들어가며 그 안의 task/SP store가 먼저 visible해야 한다.
  4. completionHSM call 성공과 Linux CPU online은 다르다. secondary의 smp_callin() completion까지 측정한다.

판정timeout 분석에서는 logical CPU, hart ID, HSM return, secondary a0/a1, completion timestamp를 같은 행에 기록한다.

코드에서 놓치기 쉬운 경계

01

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을 쓰고 있음을 표시한다.

02

pt_regs는 복귀에 필요한 최소 architecture 상태의 소유자다

general registers 외에도 status, epc, bad address, cause가 frame에 들어간다. signal, ptrace, page fault, syscall은 이 frame을 읽거나 일부 값을 바꿀 수 있다.

handler가 frame pointer를 장기 보관하면 안 된다. trap return과 함께 stack frame 수명이 끝나기 때문이다.

03

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 조건으로 나눈다는 점이 중요하다.

04

interrupt bit와 cause number를 분리해서 기록한다

scause를 10진수 한 값으로만 찍으면 최상위 interrupt bit 때문에 매우 큰 숫자로 보인다. is_interrupt = scause >> (XLEN-1), code = scause & ~(1UL << (XLEN-1))로 나눠야 한다.

sepc와 stval의 의미는 cause code마다 달라지므로 세 register를 한 묶음으로 남긴다.

05

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 사이를 구분할 수 있다.

06

CPU online 게시 전 cache와 TLB 부채를 갚는다

offline CPU에는 remote flush가 생략될 수 있으므로 smp_callin()이 local I-cache와 TLB를 모두 비운 뒤 completion과 interrupt enable로 간다. 이 순서를 바꾸면 오래된 translation이나 instruction을 실행할 수 있다.

online bit, completion, IRQ enable은 비슷해 보여도 서로 다른 관찰자를 깨우는 세 경계다.

멈춘 위치보다 먼저 볼 값

조건관찰되는 증상첫 확인 값
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 순서

직접 확인할 실험

  1. 01
    trap entry breakpoint에서 sscratch, tp, sp, scause, sepc, stval을 user page fault와 timer interrupt 각각 기록한다.

    명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.

  2. 02
    trace-cmd record -e irq -e ipi로 local IRQ와 IPI를 분리해 수집한다.

    명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.

  3. 03
    OpenSBI HSM debug log와 Linux CPU%u: Booted secondary hartid 로그의 hart ID를 대조한다.

    명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.

  4. 04
    CPU hotplug를 반복하면서 __cpu_up, secondary_start_sbi, smp_callin, completion의 timestamp를 기록한다.

    명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.

기준 원문