← KernelDUJINLABS.COM

Linux 6.18.37 LTS · x86-64 entry and SMP

x86-64 IDT·syscall 진입과 secondary CPU 기동

IDT exception/IRQ macro, entry_SYSCALL_64, local APIC startup과 start_secondary()를 연결해 CPU가 만든 frame과 Linux가 만든 pt_regs의 경계를 읽습니다.

Series
02 / 4
Baseline
Linux 6.18.37 LTS
Architecture
x86-64
Source
arch/x86/entry/entry_64.S

같은 CPU 진입 코드처럼 보이는 exception, hardware IRQ, syscall과 secondary CPU startup은 어떤 frame과 동기화 규칙을 사용하는가?

x86의 exception과 hardware interrupt는 IDT gate를 통해 들어오지만 모든 vector가 같은 stack frame을 만들지는 않는다. CPU가 error code를 push하는 vector와 Linux stub가 가짜 error code 또는 vector를 push하는 경우를 맞춰야 공통 pt_regs layout을 만들 수 있다.

SYSCALL은 IDT gate가 아니다. CPU가 RCX와 R11에 복귀 RIP와 RFLAGS를 남기고 kernel stack으로 자동 전환하지 않으므로, entry_SYSCALL_64가 user RSP를 per-CPU TSS scratch에 보관하고 kernel CR3와 stack을 직접 선택한다.

secondary CPU는 runtime interrupt entry와도 다르다. BSP가 APIC ID, trampoline, idle stack, initial_code를 준비하고 INIT/SIPI 또는 64-bit wakeup을 보낸 뒤 AP가 secondary_startup_64start_secondary()를 통과한다. online flag는 이 준비가 끝난 뒤 게시된다.

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

경계입력이 경계가 완성하는 상태
IDT IRQvector와 hardware framecommon interrupt C handler
IDT exceptionerror-code 유무와 fault registersexception-specific C handler
SYSCALLRAX number, RCX RIP, R11 flags, user RSPdo_syscall_64()와 SYSRET/IRET
AP startupAPIC ID, trampoline, idle task stackonline secondary CPU

주소와 register를 먼저 그려 본다

그림 1. x86-64 syscall이 직접 만드는 pt_regs frame
+000SS__USER_DS
+008RSPTSS_sp2에 보관한 user stack
+016RFLAGSR11
+024CS__USER_CS
+032RIPRCX
+040orig_axRAX syscall number
+048GPRsPUSH_AND_CLEAR_REGS

SYSCALL instruction이 자동으로 만들지 않는 SS, RSP, RFLAGS, CS, RIP를 entry code가 kernel stack에 구성한다.

그림 2. exception, IRQ와 syscall의 진입 계약 비교

IDT exception / IRQ

CPU gateCS:RIP:RFLAGS와 필요 시 SS:RSP
vector stubvector/error slot 정규화
idtentrykernel context와 pt_regs

SYSCALL

CPU instructionRCX·R11만 보존
entry_SYSCALL_64CR3·stack 직접 전환
returnSYSRET 가능 조건 검사

공통점은 C에 pt_regs *를 넘기는 것이지만 frame 생산자와 복귀 명령이 다르다.

그림 3. BSP가 AP를 online으로 만드는 경로
do_boot_cputrampoline·idle stack·initial_code
INIT/SIPIAPIC wakeup
secondary_startup_64GDT·CR3·per-CPU
start_secondarymicrocode·CPU state
cpuhp syncALIVE handshake
CPU onlineinterrupt와 idle loop

wakeup signal을 보낸 시점과 scheduler가 CPU를 online으로 보는 시점 사이에는 여러 동기화 단계가 있다.

SYSCALL entry가 user context를 pt_regs로 바꾸는 구간

SYSCALL hardware contract의 빈 부분을 assembly가 채운다. 특히 user RSP 보관, kernel CR3, per-CPU stack 선택 순서는 바꿀 수 없다.

원본: arch/x86/entry/entry_64.S 87-150줄

87SYM_CODE_START(entry_SYSCALL_64)
88	UNWIND_HINT_ENTRY
89	ENDBR
90 
91	swapgs
92	/* tss.sp2 is scratch space. */
93	movq	%rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
94	SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
95	movq	PER_CPU_VAR(cpu_current_top_of_stack), %rsp
96 
97SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL)
98	ANNOTATE_NOENDBR
99 
100	/* Construct struct pt_regs on stack */
101	pushq	$__USER_DS				/* pt_regs->ss */
102	pushq	PER_CPU_VAR(cpu_tss_rw + TSS_sp2)	/* pt_regs->sp */
103	pushq	%r11					/* pt_regs->flags */
104	pushq	$__USER_CS				/* pt_regs->cs */
105	pushq	%rcx					/* pt_regs->ip */
106SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)
107	pushq	%rax					/* pt_regs->orig_ax */
108 
109	PUSH_AND_CLEAR_REGS rax=$-ENOSYS
110 
111	/* IRQs are off. */
112	movq	%rsp, %rdi
113	/* Sign extend the lower 32bit as syscall numbers are treated as int */
114	movslq	%eax, %rsi
115 
116	/* clobbers %rax, make sure it is after saving the syscall nr */
117	IBRS_ENTER
118	UNTRAIN_RET
119	CLEAR_BRANCH_HISTORY
120 
121	call	do_syscall_64		/* returns with IRQs disabled */
122 
123	/*
124	 * Try to use SYSRET instead of IRET if we're returning to
125	 * a completely clean 64-bit userspace context.  If we're not,
126	 * go to the slow exit path.
127	 * In the Xen PV case we must use iret anyway.
128	 */
129 
130	ALTERNATIVE "testb %al, %al; jz swapgs_restore_regs_and_return_to_usermode", \
131		"jmp swapgs_restore_regs_and_return_to_usermode", X86_FEATURE_XENPV
132 
133	/*
134	 * We win! This label is here just for ease of understanding
135	 * perf profiles. Nothing jumps here.
136	 */
137syscall_return_via_sysret:
138	IBRS_EXIT
139	POP_REGS pop_rdi=0
140 
141	/*
142	 * Now all regs are restored except RSP and RDI.
143	 * Save old stack pointer and switch to trampoline stack.
144	 */
145	movq	%rsp, %rdi
146	movq	PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
147	UNWIND_HINT_END_OF_STACK
148 
149	pushq	RSP-RDI(%rdi)	/* RSP */
150	pushq	(%rdi)		/* RDI */

87-150줄 주석

87SYM_CODE_START(entry_SYSCALL_64)

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

88UNWIND_HINT_ENTRY

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

89ENDBR

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

90(빈 줄)

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

91swapgs

user GS base와 kernel per-CPU GS base를 교환한다. 이후 PER_CPU_VAR 접근의 전제다.

92/* tss.sp2 is scratch space. */

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

93movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)

SYSCALL이 kernel stack으로 자동 전환하지 않으므로 user RSP를 per-CPU scratch에 먼저 보관한다.

94SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp

KPTI가 활성화된 경우 user page table에서 kernel page table로 이동한다.

95movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp

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

96(빈 줄)

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

97SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL)

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

98ANNOTATE_NOENDBR

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

99(빈 줄)

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

100/* Construct struct pt_regs on stack */

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

101pushq $__USER_DS /* pt_regs->ss */

현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.

102pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */

SYSCALL이 kernel stack으로 자동 전환하지 않으므로 user RSP를 per-CPU scratch에 먼저 보관한다.

103pushq %r11 /* pt_regs->flags */

현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.

104pushq $__USER_CS /* pt_regs->cs */

현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.

105pushq %rcx /* pt_regs->ip */

현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.

106SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)

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

107pushq %rax /* pt_regs->orig_ax */

현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.

108(빈 줄)

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

109PUSH_AND_CLEAR_REGS rax=$-ENOSYS

나머지 general-purpose register를 pt_regs 순서로 쌓고 지정한 register를 정리한다.

110(빈 줄)

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

111/* IRQs are off. */

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

112movq %rsp, %rdi

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

113/* Sign extend the lower 32bit as syscall numbers are treated as int */

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

114movslq %eax, %rsi

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

115(빈 줄)

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

116/* clobbers %rax, make sure it is after saving the syscall nr */

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

117IBRS_ENTER

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

118UNTRAIN_RET

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

119CLEAR_BRANCH_HISTORY

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

120(빈 줄)

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

121call do_syscall_64 /* returns with IRQs disabled */

C dispatcher에 pt_regs *와 sign-extended syscall number를 넘긴다.

122(빈 줄)

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

123/*

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

124* Try to use SYSRET instead of IRET if we're returning to

복귀 context가 canonical address와 flag 제약을 만족할 때만 빠른 SYSRET 경로를 쓴다. 아니면 IRET 경로로 간다.

125* a completely clean 64-bit userspace context. If we're not,

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

126* go to the slow exit path.

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

127* In the Xen PV case we must use iret anyway.

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

128*/

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

129(빈 줄)

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

130ALTERNATIVE "testb %al, %al; jz swapgs_restore_regs_and_return_to_usermode", \

user GS base와 kernel per-CPU GS base를 교환한다. 이후 PER_CPU_VAR 접근의 전제다.

131"jmp swapgs_restore_regs_and_return_to_usermode", X86_FEATURE_XENPV

user GS base와 kernel per-CPU GS base를 교환한다. 이후 PER_CPU_VAR 접근의 전제다.

132(빈 줄)

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

133/*

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

134* We win! This label is here just for ease of understanding

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

135* perf profiles. Nothing jumps here.

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

136*/

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

137syscall_return_via_sysret:

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

138IBRS_EXIT

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

139POP_REGS pop_rdi=0

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

140(빈 줄)

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

141/*

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

142* Now all regs are restored except RSP and RDI.

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

143* Save old stack pointer and switch to trampoline stack.

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

144*/

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

145movq %rsp, %rdi

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

146movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp

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

147UNWIND_HINT_END_OF_STACK

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

148(빈 줄)

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

149pushq RSP-RDI(%rdi) /* RSP */

현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.

150pushq (%rdi) /* RDI */

현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.

IDT IRQ와 특수 exception entry macro

vector stub가 error-code slot을 정규화한 뒤 idtentry body로 들어간다. #MC와 #DB는 kernel의 임의 상태를 끊을 수 있어 paranoid entry를 사용한다.

원본: arch/x86/entry/entry_64.S 367-430줄

367/*
368 * Interrupt entry/exit.
369 *
370 + The interrupt stubs push (vector) onto the stack, which is the error_code
371 * position of idtentry exceptions, and jump to one of the two idtentry points
372 * (common/spurious).
373 *
374 * common_interrupt is a hotpath, align it to a cache line
375 */
376.macro idtentry_irq vector cfunc
377	.p2align CONFIG_X86_L1_CACHE_SHIFT
378	idtentry \vector asm_\cfunc \cfunc has_error_code=1
379.endm
380 
381/**
382 * idtentry_mce_db - Macro to generate entry stubs for #MC and #DB
383 * @vector:		Vector number
384 * @asmsym:		ASM symbol for the entry point
385 * @cfunc:		C function to be called
386 *
387 * The macro emits code to set up the kernel context for #MC and #DB
388 *
389 * If the entry comes from user space it uses the normal entry path
390 * including the return to user space work and preemption checks on
391 * exit.
392 *
393 * If hits in kernel mode then it needs to go through the paranoid
394 * entry as the exception can hit any random state. No preemption
395 * check on exit to keep the paranoid path simple.
396 */
397.macro idtentry_mce_db vector asmsym cfunc
398SYM_CODE_START(\asmsym)
399	UNWIND_HINT_IRET_ENTRY
400	ENDBR
401	ASM_CLAC
402	cld
403 
404	pushq	$-1			/* ORIG_RAX: no syscall to restart */
405 
406	/*
407	 * If the entry is from userspace, switch stacks and treat it as
408	 * a normal entry.
409	 */
410	testb	$3, CS-ORIG_RAX(%rsp)
411	jnz	.Lfrom_usermode_switch_stack_\@
412 
413	/* paranoid_entry returns GS information for paranoid_exit in EBX. */
414	call	paranoid_entry
415 
416	UNWIND_HINT_REGS
417 
418	movq	%rsp, %rdi		/* pt_regs pointer */
419 
420	call	\cfunc
421 
422	jmp	paranoid_exit
423 
424	/* Switch to the regular task stack and use the noist entry point */
425.Lfrom_usermode_switch_stack_\@:
426	idtentry_body noist_\cfunc, has_error_code=0
427 
428_ASM_NOKPROBE(\asmsym)
429SYM_CODE_END(\asmsym)
430.endm

367-430줄 주석

367/*

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

368* Interrupt entry/exit.

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

369*

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

370+ The interrupt stubs push (vector) onto the stack, which is the error_code

현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.

371* position of idtentry exceptions, and jump to one of the two idtentry points

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

372* (common/spurious).

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

373*

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

374* common_interrupt is a hotpath, align it to a cache line

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

375*/

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

376.macro idtentry_irq vector cfunc

각 IRQ vector용 stub를 cache-line 정렬하고 공통 idtentry body로 연결하는 macro다.

377.p2align CONFIG_X86_L1_CACHE_SHIFT

assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.

378idtentry \vector asm_\cfunc \cfunc has_error_code=1

IRQ stub가 vector를 error-code 위치에 push했으므로 공통 코드에는 slot이 이미 있는 것으로 알린다.

379.endm

assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.

380(빈 줄)

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

381/**

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

382* idtentry_mce_db - Macro to generate entry stubs for #MC and #DB

#MC와 #DB처럼 NMI에 가까운 조건을 처리하는 별도 진입 macro다.

383* @vector: Vector number

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

384* @asmsym: ASM symbol for the entry point

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

385* @cfunc: C function to be called

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

386*

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

387* The macro emits code to set up the kernel context for #MC and #DB

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

388*

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

389* If the entry comes from user space it uses the normal entry path

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

390* including the return to user space work and preemption checks on

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

391* exit.

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

392*

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

393* If hits in kernel mode then it needs to go through the paranoid

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

394* entry as the exception can hit any random state. No preemption

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

395* check on exit to keep the paranoid path simple.

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

396*/

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

397.macro idtentry_mce_db vector asmsym cfunc

#MC와 #DB처럼 NMI에 가까운 조건을 처리하는 별도 진입 macro다.

398SYM_CODE_START(\asmsym)

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

399UNWIND_HINT_IRET_ENTRY

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

400ENDBR

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

401ASM_CLAC

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

402cld

문자열 명령의 방향을 증가 방향으로 고정한다. 펌웨어가 남긴 DF 상태를 C ABI가 기대하는 값으로 되돌리는 줄이다.

403(빈 줄)

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

404pushq $-1 /* ORIG_RAX: no syscall to restart */

현재 stack에 값을 쌓는다. 이 줄을 읽을 때는 SP가 가리키는 메모리가 실제로 쓰기 가능한지와 ABI 정렬을 함께 확인한다.

405(빈 줄)

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

406/*

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

407* If the entry is from userspace, switch stacks and treat it as

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

408* a normal entry.

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

409*/

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

410testb $3, CS-ORIG_RAX(%rsp)

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

411jnz .Lfrom_usermode_switch_stack_\@

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

412(빈 줄)

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

413/* paranoid_entry returns GS information for paranoid_exit in EBX. */

GS와 context가 정상 kernel/user 경계에 있다는 가정을 버리고 안전하게 현재 상태를 판별한다.

414call paranoid_entry

GS와 context가 정상 kernel/user 경계에 있다는 가정을 버리고 안전하게 현재 상태를 판별한다.

415(빈 줄)

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

416UNWIND_HINT_REGS

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

417(빈 줄)

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

418movq %rsp, %rdi /* pt_regs pointer */

완성된 pt_regs frame pointer를 첫 C argument로 넘긴다.

419(빈 줄)

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

420call \cfunc

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

421(빈 줄)

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

422jmp paranoid_exit

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

423(빈 줄)

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

424/* Switch to the regular task stack and use the noist entry point */

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

425.Lfrom_usermode_switch_stack_\@:

assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.

426idtentry_body noist_\cfunc, has_error_code=0

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

427(빈 줄)

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

428_ASM_NOKPROBE(\asmsym)

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

429SYM_CODE_END(\asmsym)

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

430.endm

assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.

BSP의 do_boot_cpu와 AP의 start_secondary

control CPU가 AP가 실행할 trampoline과 C entry, idle stack을 게시하고 architecture wakeup callback을 호출한다.

원본: arch/x86/kernel/smpboot.c 842-900줄

842static int do_boot_cpu(u32 apicid, unsigned int cpu, struct task_struct *idle)
843{
844	unsigned long start_ip = real_mode_header->trampoline_start;
845	int ret;
846 
847#ifdef CONFIG_X86_64
848	/* If 64-bit wakeup method exists, use the 64-bit mode trampoline IP */
849	if (apic->wakeup_secondary_cpu_64)
850		start_ip = real_mode_header->trampoline_start64;
851#endif
852	idle->thread.sp = (unsigned long)task_pt_regs(idle);
853	initial_code = (unsigned long)start_secondary;
854 
855	if (IS_ENABLED(CONFIG_X86_32)) {
856		early_gdt_descr.address = (unsigned long)get_cpu_gdt_rw(cpu);
857		initial_stack  = idle->thread.sp;
858	} else if (!(smpboot_control & STARTUP_PARALLEL_MASK)) {
859		smpboot_control = cpu;
860	}
861 
862	/* Enable the espfix hack for this CPU */
863	init_espfix_ap(cpu);
864 
865	/* So we see what's up */
866	announce_cpu(cpu, apicid);
867 
868	/*
869	 * This grunge runs the startup process for
870	 * the targeted processor.
871	 */
872	if (x86_platform.legacy.warm_reset) {
873 
874		pr_debug("Setting warm reset code and vector.\n");
875 
876		smpboot_setup_warm_reset_vector(start_ip);
877		/*
878		 * Be paranoid about clearing APIC errors.
879		*/
880		if (APIC_INTEGRATED(boot_cpu_apic_version)) {
881			apic_write(APIC_ESR, 0);
882			apic_read(APIC_ESR);
883		}
884	}
885 
886	smp_mb();
887 
888	/*
889	 * Wake up a CPU in difference cases:
890	 * - Use a method from the APIC driver if one defined, with wakeup
891	 *   straight to 64-bit mode preferred over wakeup to RM.
892	 * Otherwise,
893	 * - Use an INIT boot APIC message
894	 */
895	if (apic->wakeup_secondary_cpu_64)
896		ret = apic->wakeup_secondary_cpu_64(apicid, start_ip, cpu);
897	else if (apic->wakeup_secondary_cpu)
898		ret = apic->wakeup_secondary_cpu(apicid, start_ip, cpu);
899	else
900		ret = wakeup_secondary_cpu_via_init(apicid, start_ip, cpu);

842-900줄 주석

842static int do_boot_cpu(u32 apicid, unsigned int cpu, struct task_struct *idle)

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

843{

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

844unsigned long start_ip = real_mode_header->trampoline_start;

real-mode 또는 64-bit AP wakeup에서 처음 실행할 low-memory entry를 선택한다.

845int ret;

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

846(빈 줄)

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

847#ifdef CONFIG_X86_64

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

848/* If 64-bit wakeup method exists, use the 64-bit mode trampoline IP */

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

849if (apic->wakeup_secondary_cpu_64)

platform/APIC implementation이 64-bit direct wakeup을 지원하면 별도 trampoline을 사용한다.

850start_ip = real_mode_header->trampoline_start64;

real-mode 또는 64-bit AP wakeup에서 처음 실행할 low-memory entry를 선택한다.

851#endif

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

852idle->thread.sp = (unsigned long)task_pt_regs(idle);

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

853initial_code = (unsigned long)start_secondary;

AP의 assembly startup 끝에서 간접 호출할 C entry를 start_secondary로 지정한다.

854(빈 줄)

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

855if (IS_ENABLED(CONFIG_X86_32)) {

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

856early_gdt_descr.address = (unsigned long)get_cpu_gdt_rw(cpu);

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

857initial_stack = idle->thread.sp;

32비트 경로가 사용할 AP별 idle task stack을 게시한다.

858} else if (!(smpboot_control & STARTUP_PARALLEL_MASK)) {

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

859smpboot_control = cpu;

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

860}

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

861(빈 줄)

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

862/* Enable the espfix hack for this CPU */

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

863init_espfix_ap(cpu);

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

864(빈 줄)

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

865/* So we see what's up */

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

866announce_cpu(cpu, apicid);

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

867(빈 줄)

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

868/*

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

869* This grunge runs the startup process for

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

870* the targeted processor.

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

871*/

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

872if (x86_platform.legacy.warm_reset) {

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

873(빈 줄)

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

874pr_debug("Setting warm reset code and vector.\n");

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

875(빈 줄)

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

876smpboot_setup_warm_reset_vector(start_ip);

legacy wakeup이 요구하는 warm-reset vector를 firmware-visible 위치에 기록한다.

877/*

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

878* Be paranoid about clearing APIC errors.

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

879*/

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

880if (APIC_INTEGRATED(boot_cpu_apic_version)) {

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

881apic_write(APIC_ESR, 0);

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

882apic_read(APIC_ESR);

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

883}

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

884}

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

885(빈 줄)

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

886smp_mb();

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

887(빈 줄)

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

888/*

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

889* Wake up a CPU in difference cases:

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

890* - Use a method from the APIC driver if one defined, with wakeup

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

891* straight to 64-bit mode preferred over wakeup to RM.

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

892* Otherwise,

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

893* - Use an INIT boot APIC message

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

894*/

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

895if (apic->wakeup_secondary_cpu_64)

platform/APIC implementation이 64-bit direct wakeup을 지원하면 별도 trampoline을 사용한다.

896ret = apic->wakeup_secondary_cpu_64(apicid, start_ip, cpu);

platform/APIC implementation이 64-bit direct wakeup을 지원하면 별도 trampoline을 사용한다.

897else if (apic->wakeup_secondary_cpu)

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

898ret = apic->wakeup_secondary_cpu(apicid, start_ip, cpu);

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

899else

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

900ret = wakeup_secondary_cpu_via_init(apicid, start_ip, cpu);

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

AP가 ALIVE를 게시하기 전 수행하는 최소 초기화

AP는 exception handling과 microcode를 준비한 뒤 CPU hotplug 동기화 지점에 도착한다. 이 전에 일반 subsystem 작업을 넣지 말라는 원본 주석이 핵심 계약이다.

원본: arch/x86/kernel/smpboot.c 232-294줄

232static void notrace __noendbr start_secondary(void *unused)
233{
234	/*
235	 * Don't put *anything* except direct CPU state initialization
236	 * before cpu_init(), SMP booting is too fragile that we want to
237	 * limit the things done here to the most necessary things.
238	 */
239	cr4_init();
240 
241	/*
242	 * 32-bit specific. 64-bit reaches this code with the correct page
243	 * table established. Yet another historical divergence.
244	 */
245	if (IS_ENABLED(CONFIG_X86_32)) {
246		/* switch away from the initial page table */
247		load_cr3(swapper_pg_dir);
248		__flush_tlb_all();
249	}
250 
251	cpu_init_exception_handling(false);
252 
253	/*
254	 * Load the microcode before reaching the AP alive synchronization
255	 * point below so it is not part of the full per CPU serialized
256	 * bringup part when "parallel" bringup is enabled.
257	 *
258	 * That's even safe when hyperthreading is enabled in the CPU as
259	 * the core code starts the primary threads first and leaves the
260	 * secondary threads waiting for SIPI. Loading microcode on
261	 * physical cores concurrently is a safe operation.
262	 *
263	 * This covers both the Intel specific issue that concurrent
264	 * microcode loading on SMT siblings must be prohibited and the
265	 * vendor independent issue`that microcode loading which changes
266	 * CPUID, MSRs etc. must be strictly serialized to maintain
267	 * software state correctness.
268	 */
269	load_ucode_ap();
270 
271	/*
272	 * Synchronization point with the hotplug core. Sets this CPUs
273	 * synchronization state to ALIVE and spin-waits for the control CPU to
274	 * release this CPU for further bringup.
275	 */
276	cpuhp_ap_sync_alive();
277 
278	cpu_init();
279	fpu__init_cpu();
280	rcutree_report_cpu_starting(raw_smp_processor_id());
281	x86_cpuinit.early_percpu_clock_init();
282 
283	ap_starting();
284 
285	/* Check TSC synchronization with the control CPU. */
286	check_tsc_sync_target();
287 
288	/*
289	 * Calibrate the delay loop after the TSC synchronization check.
290	 * This allows to skip the calibration when TSC is synchronized
291	 * across sockets.
292	 */
293	ap_calibrate_delay();
294 

232-294줄 주석

232static void notrace __noendbr start_secondary(void *unused)

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

233{

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

234/*

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

235* Don't put *anything* except direct CPU state initialization

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

236* before cpu_init(), SMP booting is too fragile that we want to

per-CPU GDT/TSS와 일반 CPU 상태를 runtime 사용 가능하게 만든다.

237* limit the things done here to the most necessary things.

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

238*/

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

239cr4_init();

AP의 CR4 feature bit를 boot CPU 정책과 맞춘다.

240(빈 줄)

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

241/*

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

242* 32-bit specific. 64-bit reaches this code with the correct page

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

243* table established. Yet another historical divergence.

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

244*/

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

245if (IS_ENABLED(CONFIG_X86_32)) {

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

246/* switch away from the initial page table */

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

247load_cr3(swapper_pg_dir);

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

248__flush_tlb_all();

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

249}

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

250(빈 줄)

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

251cpu_init_exception_handling(false);

AP별 TSS, IDT 관련 exception 처리 기반을 준비한다.

252(빈 줄)

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

253/*

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

254* Load the microcode before reaching the AP alive synchronization

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

255* point below so it is not part of the full per CPU serialized

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

256* bringup part when "parallel" bringup is enabled.

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

257*

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

258* That's even safe when hyperthreading is enabled in the CPU as

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

259* the core code starts the primary threads first and leaves the

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

260* secondary threads waiting for SIPI. Loading microcode on

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

261* physical cores concurrently is a safe operation.

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

262*

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

263* This covers both the Intel specific issue that concurrent

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

264* microcode loading on SMT siblings must be prohibited and the

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

265* vendor independent issue`that microcode loading which changes

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

266* CPUID, MSRs etc. must be strictly serialized to maintain

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

267* software state correctness.

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

268*/

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

269load_ucode_ap();

CPUID와 MSR 의미가 이후 software state와 일치하도록 ALIVE 게시 전에 microcode를 올린다.

270(빈 줄)

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

271/*

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

272* Synchronization point with the hotplug core. Sets this CPUs

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

273* synchronization state to ALIVE and spin-waits for the control CPU to

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

274* release this CPU for further bringup.

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

275*/

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

276cpuhp_ap_sync_alive();

AP가 최소 architecture 준비를 마쳤음을 control CPU에 알리고 다음 단계 허가를 기다린다.

277(빈 줄)

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

278cpu_init();

per-CPU GDT/TSS와 일반 CPU 상태를 runtime 사용 가능하게 만든다.

279fpu__init_cpu();

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

280rcutree_report_cpu_starting(raw_smp_processor_id());

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

281x86_cpuinit.early_percpu_clock_init();

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

282(빈 줄)

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

283ap_starting();

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

284(빈 줄)

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

285/* Check TSC synchronization with the control CPU. */

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

286check_tsc_sync_target();

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

287(빈 줄)

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

288/*

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

289* Calibrate the delay loop after the TSC synchronization check.

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

290* This allows to skip the calibration when TSC is synchronized

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

291* across sockets.

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

292*/

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

293ap_calibrate_delay();

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

294(빈 줄)

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

INIT deassert와 두 번의 SIPI가 AP의 첫 instruction 주소를 만드는 구간

BSP가 start_eip를 그대로 전송하는 것이 아니라 4KiB page number로 바꿔 STARTUP IPI vector에 넣는 코드다. AP는 vector << 12를 real-mode 시작 물리 주소로 복원한다.

원본: arch/x86/kernel/smpboot.c 695-757줄

695static int wakeup_secondary_cpu_via_init(u32 phys_apicid, unsigned long start_eip, unsigned int cpu)
696{
697	unsigned long send_status = 0, accept_status = 0;
698	int num_starts, j, maxlvt;
699 
700	preempt_disable();
701	maxlvt = lapic_get_maxlvt();
702	send_init_sequence(phys_apicid);
703 
704	mb();
705 
706	/*
707	 * Should we send STARTUP IPIs ?
708	 *
709	 * Determine this based on the APIC version.
710	 * If we don't have an integrated APIC, don't send the STARTUP IPIs.
711	 */
712	if (APIC_INTEGRATED(boot_cpu_apic_version))
713		num_starts = 2;
714	else
715		num_starts = 0;
716 
717	/*
718	 * Run STARTUP IPI loop.
719	 */
720	pr_debug("#startup loops: %d\n", num_starts);
721 
722	for (j = 1; j <= num_starts; j++) {
723		pr_debug("Sending STARTUP #%d\n", j);
724		if (maxlvt > 3)		/* Due to the Pentium erratum 3AP.  */
725			apic_write(APIC_ESR, 0);
726		apic_read(APIC_ESR);
727		pr_debug("After apic_write\n");
728 
729		/*
730		 * STARTUP IPI
731		 */
732 
733		/* Target chip */
734		/* Boot on the stack */
735		/* Kick the second */
736		apic_icr_write(APIC_DM_STARTUP | (start_eip >> 12),
737			       phys_apicid);
738 
739		/*
740		 * Give the other CPU some time to accept the IPI.
741		 */
742		if (init_udelay == 0)
743			udelay(10);
744		else
745			udelay(300);
746 
747		pr_debug("Startup point 1\n");
748 
749		pr_debug("Waiting for send to finish...\n");
750		send_status = safe_apic_wait_icr_idle();
751 
752		/*
753		 * Give the other CPU some time to accept the IPI.
754		 */
755		if (init_udelay == 0)
756			udelay(10);
757		else

695-757줄 주석

695static int wakeup_secondary_cpu_via_init(u32 phys_apicid, unsigned long start_eip, unsigned int cpu)

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

696{

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

697unsigned long send_status = 0, accept_status = 0;

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

698int num_starts, j, maxlvt;

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

699(빈 줄)

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

700preempt_disable();

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

701maxlvt = lapic_get_maxlvt();

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

702send_init_sequence(phys_apicid);

대상 APIC ID에 INIT assert/deassert를 보내 AP를 초기 상태로 되돌린다. 이 과정은 AP가 Linux trampoline을 실행했다는 증거가 아니다.

703(빈 줄)

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

704mb();

trampoline data와 AP별 stack/code pointer 게시가 INIT/SIPI보다 앞서 관찰되도록 ordering 경계를 둔다.

705(빈 줄)

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

706/*

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

707* Should we send STARTUP IPIs ?

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

708*

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

709* Determine this based on the APIC version.

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

710* If we don't have an integrated APIC, don't send the STARTUP IPIs.

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

711*/

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

712if (APIC_INTEGRATED(boot_cpu_apic_version))

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

713num_starts = 2;

integrated APIC에는 Pentium 계열 호환성을 위해 SIPI를 두 번 보낼 수 있다. AP가 두 번 부팅된다는 뜻이 아니라 첫 SIPI 수신 실패를 견디는 protocol이다.

714else

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

715num_starts = 0;

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

716(빈 줄)

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

717/*

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

718* Run STARTUP IPI loop.

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

719*/

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

720pr_debug("#startup loops: %d\n", num_starts);

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

721(빈 줄)

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

722for (j = 1; j <= num_starts; j++) {

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

723pr_debug("Sending STARTUP #%d\n", j);

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

724if (maxlvt > 3) /* Due to the Pentium erratum 3AP. */

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

725apic_write(APIC_ESR, 0);

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

726apic_read(APIC_ESR);

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

727pr_debug("After apic_write\n");

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

728(빈 줄)

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

729/*

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

730* STARTUP IPI

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

731*/

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

732(빈 줄)

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

733/* Target chip */

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

734/* Boot on the stack */

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

735/* Kick the second */

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

736apic_icr_write(APIC_DM_STARTUP | (start_eip >> 12),

STARTUP vector field는 물리 주소의 12비트 오른쪽 이동값이다. 따라서 trampoline은 4KiB 정렬이어야 하고 1MiB 아래에서 표현 가능해야 한다.

737phys_apicid);

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

738(빈 줄)

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

739/*

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

740* Give the other CPU some time to accept the IPI.

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

741*/

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

742if (init_udelay == 0)

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

743udelay(10);

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

744else

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

745udelay(300);

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

746(빈 줄)

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

747pr_debug("Startup point 1\n");

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

748(빈 줄)

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

749pr_debug("Waiting for send to finish...\n");

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

750send_status = safe_apic_wait_icr_idle();

ICR send pending이 내려가는 것만 확인한다. AP의 C entry 도달은 뒤의 ALIVE/completion handshake로 별도 확인한다.

751(빈 줄)

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

752/*

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

753* Give the other CPU some time to accept the IPI.

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

754*/

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

755if (init_udelay == 0)

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

756udelay(10);

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

757else

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

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

01

SYSCALL 직후 CPU가 보존한 값과 Linux가 추가하는 frame

user RIP=0x00007f0012345678, user RSP=0x00007fffffffdc00, RFLAGS=0x246, RAX=60exit syscall을 예로 든다.

  1. hardware resultSYSCALL은 RCX에 user RIP, R11에 RFLAGS를 저장하지만 SS:RSP frame과 kernel stack 전환은 만들지 않는다.
  2. entry scratchswapgs 뒤 user RSP를 per-CPU TSS_sp2에 저장하고 kernel CR3·kernel stack을 선택한다.
  3. synthetic frameSS, 저장된 user RSP, R11, CS, RCX, orig_ax=60pt_regs 순서로 push한다.
  4. return choiceRCX가 canonical이고 R11에 SYSRET 금지 flag가 없을 때만 빠른 SYSRET를 사용한다.

판정SYSCALL을 IDT interrupt처럼 설명하면 자동 stack switch와 error-code slot을 잘못 가정하게 된다. frame의 생산자를 hardware와 entry assembly로 나눠 적는다.

02

SIPI vector와 AP 시작 주소 계산

trampoline 물리 주소가 0x00090000, 대상 APIC ID가 0x04라고 가정한다.

  1. vector0x00090000 >> 12 = 0x90; STARTUP IPI vector field에는 0x90이 들어간다.
  2. AP reconstructionAP는 vector를 12bit 왼쪽 이동해 첫 physical PC 0x00090000을 얻는다.
  3. 표현 범위8bit vector이므로 시작 주소는 1MiB 아래의 4KiB 경계여야 한다.
  4. 완료 판정ICR idle은 전송 완료일 뿐이다. secondary_startup_64 checkpoint와 ALIVE completion을 별도로 본다.

판정AP 정지 분석에서는 start_eip, ICR low/high, target APIC ID, trampoline data의 initial stack/code를 한 시점에 덤프한다.

코드에서 놓치기 쉬운 경계

01

hardware frame과 pt_regs를 같은 것으로 부르지 않는다

CPU는 privilege 전환 여부와 vector 종류에 따라 서로 다른 최소 frame을 만든다. Linux entry stub는 vector/error slot과 general registers를 추가해 C가 공통 구조로 읽을 수 있는 pt_regs를 완성한다.

crash dump에서 stack을 해석할 때 현재 RSP가 hardware frame 시작인지 완성된 pt_regs 시작인지 먼저 판별해야 offset을 틀리지 않는다.

02

error code가 있는 exception과 없는 exception을 정규화한다

#PF, #GP처럼 CPU가 error code를 push하는 exception과 #DE처럼 push하지 않는 exception이 있다. idtentry macro의 has_error_code와 generated stub가 이 차이를 흡수한다.

vector table만 보고 stack slot을 가정하지 말고 생성 macro가 넘긴 parameter와 disassembly를 확인한다.

03

KPTI와 swapgs는 보안 경계이자 ordering 경계다

user page table에서는 kernel mapping이 제한될 수 있으므로 per-CPU data와 kernel stack을 쓰기 전에 GS와 CR3를 순서대로 바꾼다. speculation mitigation macro도 register frame을 만든 뒤 C 호출 전에 배치된다.

entry 코드의 한 줄을 이동할 때는 functional correctness뿐 아니라 transient execution에서 어떤 주소가 노출되는지도 함께 본다.

04

SYSRET은 무조건 빠른 복귀가 아니다

SYSRET은 canonical RIP/RSP와 flag에 제약이 있고 일부 CPU errata도 있다. ptrace, signal, modified registers처럼 context가 깨끗하지 않으면 IRET 기반 slow path가 필요하다.

성능 측정에서는 syscall entry 비용과 return-to-user work, IRET fallback 비율을 분리해야 한다.

05

local APIC vector와 Linux IRQ number는 같은 번호가 아니다

CPU가 받은 vector는 IDT slot이고 irqdomain/core가 관리하는 Linux IRQ number와 직접 일치하지 않을 수 있다. IOAPIC/MSI routing, vector allocation, descriptor lookup을 분리해 추적한다.

장치 /proc/interrupts 번호만으로 IDT vector를 추정하지 말고 APIC/irq tracepoint와 handler entry를 같이 본다.

06

AP startup은 lock 하나가 아니라 handshake들의 연쇄다

BSP는 trampoline data와 stack을 먼저 게시하고 wakeup을 보낸다. AP는 microcode와 exception state를 준비한 뒤 ALIVE를 알리고, control CPU가 다음 hotplug state를 열어야 scheduler와 interrupt를 사용할 수 있다.

timeout이 나면 SIPI 전송, trampoline 도달, secondary_startup_64, start_secondary, ALIVE completion 중 마지막으로 관찰된 지점을 나눠야 한다.

멈춘 위치보다 먼저 볼 값

조건관찰되는 증상첫 확인 값
swapgs/CR3 순서 오류첫 per-CPU 접근이나 kernel stack 전환에서 page fault 또는 잘못된 CPU data를 읽는다.GS base, CR3, TSS_sp2
frame layout 불일치C handler가 vector, RIP, error code를 잘못 해석해 2차 fault가 난다.stub push와 pt_regs offset
EOI/IRQ routing 오류한 vector가 반복 진입하거나 반대로 다시 발생하지 않는다.APIC ISR/IRR와 handler return
AP startup 게시 순서 오류SIPI는 보냈지만 AP가 초기 stack 또는 initial_code를 0으로 읽는다.trampoline data와 memory visibility
AP ALIVE timeoutCPU online 실패 로그가 나고 hotplug state가 진행되지 않는다.secondary_startup_64와 cpuhp sync

직접 확인할 실험

  1. 01
    perf record -e irq:irq_handler_entry,irq:irq_handler_exit로 Linux IRQ와 handler 시간을 기록한다.

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

  2. 02
    ftrace function graph에서 do_syscall_64, common_interrupt, start_secondary를 서로 다른 trace로 수집한다.

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

  3. 03
    QEMU -d int와 GDB로 IDT vector, CS, error code, CR2를 page fault 한 건에 대해 대조한다.

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

  4. 04
    dmesg -w의 CPU bring-up 로그와 APIC ID, /sys/devices/system/cpu/online을 함께 기록한다.

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

기준 원문