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_64와 start_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 entry_SYSCALL_64가 완성한 pt_regs의 바이트 오프셋
+000r15완성 직후 %rsp가 가리키는 시작
+008…+112r14 … di14개의 8바이트 GPR 슬롯; ax는 -ENOSYS로 시작
+120orig_axRAX의 원래 syscall 번호
+128ipRCX의 사용자 복귀 RIP
+136cs__USER_CS
+144flagsR11의 사용자 RFLAGS
+152spTSS_sp2에 저장한 사용자 RSP
+160ss__USER_DS

숫자는 PUSH_AND_CLEAR_REGS 뒤의 %rsp에서 각 필드까지의 바이트 거리다. 스택이 낮은 주소로 자라므로 entry 코드가 ss → sp → flags → cs → ip → orig_ax 순서로 push한 뒤 GPR을 더 쌓으면 r15가 +000에 온다. SYSCALL 명령 자체는 이 스택 프레임을 만들지 않는다. entry_64.S 87–112 · calling.h PUSH_REGS · ptrace.h 97–160. 좁은 화면에서는 그림을 좌우로 스크롤할 수 있다.

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

주요 구문 해설

87SYM_CODE_START(entry_SYSCALL_64)

사용자 프로그램의 64비트 SYSCALL이 들어오는 커널 진입 코드를 정의합니다. CPU가 스택까지 자동 전환해 주지는 않으므로 아래 코드가 커널 스택과 저장 프레임을 만듭니다. 이 표기는 빌드 도구에 코드 심벌과 정렬을 알려 주며 그 자체가 런타임 함수 호출은 아닙니다.

88UNWIND_HINT_ENTRY

스택 추적 도구에 일반 함수 호출과 다른 진입 상태를 알려 주는 메타데이터입니다. SYSCALL로 진입한 프레임을 C 함수의 call 프레임으로 오해하지 않게 합니다.

89ENDBR

IBT 분기 보호를 사용하는 빌드에서 유효한 간접 분기 도착점을 나타내는 ENDBR 명령을 배치합니다. 설정에 따라 매크로의 실제 출력은 달라집니다.

91swapgs

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

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

현재 CPU에서 실행 중인 task의 커널 스택 상단을 rsp에 읽습니다. 사용자 rsp는 앞서 per-CPU 임시 공간에 보관했습니다.

97SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL)

커널 스택을 확보한 위치에 전역 내부 레이블을 붙입니다. SYM_INNER_LABEL은 새 함수를 호출하거나 만드는 것이 아니라 기존 코드 안의 주소에 이름을 부여합니다.

98ANNOTATE_NOENDBR

이 내부 레이블에 별도의 ENDBR이 필요하지 않음을 objtool에 표시합니다. 이 지시자 자체로 분기하거나 보안 기능을 끄지 않습니다.

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

사용자 데이터·스택 세그먼트 선택자를 pt_regs.ss 자리로 넣습니다. SYSCALL이 자동으로 만들지 않은 복귀 프레임을 소프트웨어가 구성합니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

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

SYSCALL이 r11에 보존한 사용자 RFLAGS를 스택에 넣습니다. 커널에서 바뀐 현재 플래그가 아니라 복귀시킬 사용자 상태입니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

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

사용자 코드 세그먼트 선택자를 pt_regs.cs 위치에 저장합니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

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

SYSCALL이 rcx에 저장한 사용자 복귀 주소를 pt_regs.ip 위치에 넣습니다. 사용자 코드의 다음 실행 위치입니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

106SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)

하드웨어 예외 프레임에 대응하는 필드 구성이 끝난 위치에 이름을 붙입니다. 여기까지의 프레임도 실제로는 push 명령으로 만든 것입니다.

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

원래 시스템 호출 번호를 pt_regs.orig_ax에 저장합니다. 처리 결과가 들어갈 ax 필드와 구분하여 재시작·추적 등에 원래 번호를 남깁니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

109PUSH_AND_CLEAR_REGS rax=$-ENOSYS

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

112movq %rsp, %rdi

완성된 pt_regs 주소를 첫 C 인자 rdi에 넣습니다. 시스템 호출 처리는 이 프레임에서 사용자 인자와 복귀 상태를 읽습니다.

114movslq %eax, %rsi

시스템 호출 번호의 하위 32비트를 부호 확장해 두 번째 인자 rsi에 넣습니다. 번호를 C의 int로 취급하는 규칙을 적용합니다.

117IBRS_ENTER

필요한 CPU에서는 커널 진입 시 IBRS 계열 간접 분기 추측 완화 처리를 수행합니다. rax를 사용할 수 있어 원래 시스템 호출 번호를 저장한 뒤에 배치되어 있습니다.

118UNTRAIN_RET

CPU의 선택된 보안 완화 방식에 따라 복귀 주소 예측 상태를 정리하는 코드를 펼칩니다. 일반적인 함수 복귀 동작은 아닙니다.

119CLEAR_BRANCH_HISTORY

해당 CPU에 필요한 경우 분기 이력을 정리하여 사용자 실행의 예측 상태가 커널 실행에 영향을 주는 것을 줄입니다. 적용 여부는 CPU 기능과 선택된 완화 방식에 따릅니다.

121call do_syscall_64 /* returns with IRQs disabled */

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

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

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

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 접근의 전제다.

137syscall_return_via_sysret:

빠른 SYSRET 복귀 경로에 붙인 레이블입니다. 소스 주석대로 이 레이블로 직접 점프하는 용도보다 성능 프로파일에서 경로를 구분하는 데 쓰입니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.

138IBRS_EXIT

커널 진입 때 적용한 IBRS 관련 상태를 사용자 복귀에 맞게 정리합니다. 구체적인 명령은 선택된 보안 완화 방식에 따라 달라집니다.

139POP_REGS pop_rdi=0

저장했던 일반 레지스터들을 복원하되 rdi는 남겨 둡니다. rdi와 rsp는 곧 스택을 바꾸는 작업에 사용한 뒤 마지막에 복원합니다.

145movq %rsp, %rdi

현재 저장 프레임의 위치를 rdi에 보관합니다. rsp를 다른 스택으로 바꾼 후에도 남은 사용자 rsp·rdi 값을 찾아야 하기 때문입니다.

146movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp

CPU별 TSS의 sp0에 지정된 trampoline 스택으로 rsp를 바꿉니다. 사용자 페이지 테이블로 전환하는 마지막 구간에서도 접근할 수 있는 스택입니다.

147UNWIND_HINT_END_OF_STACK

새 trampoline 스택에서 이전 일반 호출 프레임을 계속 추적하지 않도록 경계를 표시합니다. CPU가 실행하는 스택 정리 명령은 아닙니다.

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

원래 프레임에서 사용자 rsp를 읽어 trampoline 스택에 복사합니다. RSP-RDI는 현재 rdi가 가리키는 프레임 위치를 기준으로 한 필드 거리입니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

150pushq (%rdi) /* RDI */

원래 사용자 rdi도 trampoline 스택에 복사합니다. 이후 사용자 페이지 테이블로 바꾼 뒤 rdi와 rsp를 순서대로 꺼낼 수 있습니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

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

주요 구문 해설

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

원본 주석의 이어지는 줄입니다. 인터럽트별 진입 코드는 벡터 번호를 일반 IDT 예외의 error-code 자리와 같은 위치에 넣고 공통 처리로 이동한다고 설명합니다.

376.macro idtentry_irq vector cfunc

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

377.p2align CONFIG_X86_L1_CACHE_SHIFT

인터럽트 진입 코드의 시작을 2^CONFIG_X86_L1_CACHE_SHIFT 바이트 경계에 맞춥니다. 자주 실행되는 진입점의 캐시 배치를 위한 어셈블 시점 정렬입니다.

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

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

379.endm

idtentry_irq 매크로 정의를 끝냅니다. 실행 중 예외 처리가 끝나거나 함수가 반환하는 줄은 아닙니다.

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

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

397.macro idtentry_mce_db vector asmsym cfunc

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

398SYM_CODE_START(\asmsym)

매크로 인자로 받은 이름의 예외 진입 심벌을 정의합니다. \asmsym은 확장 시 실제 어셈블리 진입점 이름으로 바뀝니다. 이 표기는 빌드 도구에 코드 심벌과 정렬을 알려 주며 그 자체가 런타임 함수 호출은 아닙니다.

399UNWIND_HINT_IRET_ENTRY

스택 추적 도구에 이 진입점에 IRET 형식의 하드웨어 복귀 프레임이 있다고 알립니다.

400ENDBR

IBT를 사용하는 빌드에서 이 예외 진입점을 유효한 분기 도착점으로 표시하는 ENDBR을 배치합니다.

401ASM_CLAC

SMAP 사용 환경에서 AC 비트를 내려 커널의 사용자 메모리 접근 허용 상태를 닫습니다. 예외 발생 전에 사용자 복사를 하고 있었더라도 핸들러는 기본 제한 상태로 시작합니다.

402cld

DF를 지워 예외 핸들러의 C 코드가 기대하는 주소 증가 방향의 문자열 연산 상태를 맞춥니다.

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

시스템 호출 재시작 대상이 아님을 나타내는 -1을 orig_ax 자리에 넣습니다. #MC·#DB 예외 진입은 시스템 호출 번호를 받은 경로가 아닙니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.

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

저장된 CS 선택자의 하위 2비트, 즉 이전 권한 수준을 검사합니다. 커널에서 발생했는지 사용자 모드에서 발생했는지 구분합니다.

411jnz .Lfrom_usermode_switch_stack_\@

CS의 권한 비트가 0이 아니면 사용자 모드 진입 경로로 이동합니다. 그렇지 않으면 상태 전환 도중에도 발생할 수 있는 커널 예외용 paranoid 경로를 택합니다.

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

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

414call paranoid_entry

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

416UNWIND_HINT_REGS

paranoid_entry가 레지스터 프레임을 준비한 상태를 스택 추적 메타데이터에 알립니다.

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

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

420call \cfunc

매크로의 cfunc 인자로 지정한 C 예외 핸들러를 호출합니다. rdi에는 직전에 준비한 pt_regs 포인터가 들어 있습니다.

422jmp paranoid_exit

C 핸들러 종료 뒤 paranoid_exit로 이동합니다. 이 경로는 진입할 때 판별·저장한 GS 등의 상태를 맞춰 중단된 커널 실행으로 돌아갑니다.

425.Lfrom_usermode_switch_stack_\@:

사용자 모드에서 발생한 #MC·#DB 예외가 도착하는 레이블입니다. \@는 매크로를 여러 번 확장해도 레이블 이름이 충돌하지 않게 합니다.

426idtentry_body noist_\cfunc, has_error_code=0

일반 task 스택으로 전환하는 진입 처리와 noist_ C 핸들러 호출 코드를 펼칩니다. 사용자 복귀에 필요한 작업도 일반 진입·복귀 경로를 따릅니다.

428_ASM_NOKPROBE(\asmsym)

이 예외 진입 심벌을 kprobe 설치 금지 대상으로 등록합니다. 아직 실행 환경을 정리 중인 코드에 탐침 예외가 끼어드는 것을 막기 위한 표시입니다.

429SYM_CODE_END(\asmsym)

이번 매크로 확장이 만든 예외 진입 심벌의 범위를 끝냅니다. 실제 예외 복귀 명령은 앞서 선택한 복귀 경로에 있습니다.

430.endm

#MC·#DB 진입 코드를 생성하는 idtentry_mce_db 매크로 정의를 마칩니다.

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);

주요 구문 해설

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

지정한 APIC ID의 보조 CPU를 깨우기 위해 초기 task·스택과 진입 정보를 준비하는 함수입니다. 실제 기동은 사용 가능한 APIC 드라이버 방식이나 INIT 방식으로 요청합니다.

844unsigned long start_ip = real_mode_header->trampoline_start;

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

845int ret;

CPU 기동 방식의 성공·실패 결과를 담을 ret를 선언합니다. 아래의 여러 시작 방법이 이 변수에 결과를 기록합니다.

847#ifdef CONFIG_X86_64

64비트 커널이면서 APIC가 64비트 보조 CPU 깨우기 방식을 제공하면 그 방식의 trampoline 시작 주소를 사용합니다. 설정만 확인하는 것이 아니라 callback 존재도 확인하여 지원되지 않는 시작 경로를 고르지 않습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

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

847행에서 시작한 선택 구간을 마칩니다. 64비트 커널이면서 APIC가 64비트 보조 CPU 깨우기 방식을 제공하면 그 방식의 trampoline 시작 주소를 사용합니다. 설정만 확인하는 것이 아니라 callback 존재도 확인하여 지원되지 않는 시작 경로를 고르지 않습니다.

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

보조 CPU의 idle task 스택에서 레지스터 프레임이 놓일 위치를 초기 스택 포인터로 저장합니다. 보조 CPU가 시작할 때 사용할 스택 기준입니다.

853initial_code = (unsigned long)start_secondary;

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

855if (IS_ENABLED(CONFIG_X86_32)) {

32비트 x86 빌드에서 필요한 초기 GDT·스택 설정을 선택합니다. 64비트 빌드는 뒤의 별도 CPU 선택 정보 경로를 사용합니다.

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

대상 CPU용 GDT 주소를 초기 descriptor에 기록합니다. 보조 CPU가 자기 세그먼트 표를 올바르게 읽도록 준비하는 32비트 경로입니다.

857initial_stack = idle->thread.sp;

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

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

64비트 경로에서 병렬 CPU 기동을 사용하지 않는 경우인지 검사합니다. 단일 기동에서는 시작할 논리 CPU 번호를 공용 제어 값에 직접 기록합니다.

859smpboot_control = cpu;

현재 깨울 논리 CPU 번호를 smpboot_control에 저장합니다. 초기 어셈블리 코드가 어느 CPU의 초기 자료를 사용할지 찾는 데 쓰입니다.

863init_espfix_ap(cpu);

이 CPU에 필요한 espfix 보조 스택 등 상태를 준비합니다. 일부 x86 사용자 스택 복귀의 특수한 세그먼트 처리 문제를 보완하는 기능입니다.

866announce_cpu(cpu, apicid);

시작할 논리 CPU와 APIC ID를 부팅 로그에 알립니다. 실제 CPU를 깨우는 함수와 구분되는 진행 상황 출력입니다.

872if (x86_platform.legacy.warm_reset) {

플랫폼이 구식 warm-reset 벡터 설정을 필요로 하는지 검사합니다. 필요한 경우에만 아래 초기 진입 주소를 firmware용 위치에 기록합니다.

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

warm-reset 코드와 벡터를 설정하는 경로에 들어왔음을 디버그 출력합니다.

876smpboot_setup_warm_reset_vector(start_ip);

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

880if (APIC_INTEGRATED(boot_cpu_apic_version)) {

CPU에 통합된 local APIC인지 검사합니다. 이 경우 시작 요청 전 오류 상태 레지스터를 정리하는 동작을 수행합니다.

881apic_write(APIC_ESR, 0);

APIC 오류 상태 레지스터 ESR에 0을 씁니다. 새 CPU 시작 요청에 이전 오류 상태가 섞이지 않도록 준비합니다.

882apic_read(APIC_ESR);

ESR을 읽어 앞선 오류 상태 정리 절차를 완료합니다. 이 읽기의 결과는 별도로 보관하지 않습니다.

886smp_mb();

앞에서 기록한 task·스택·기동 정보가 CPU 깨우기 요청보다 앞서 관찰되도록 메모리 순서를 보장합니다. 보조 CPU가 미완성된 부트 데이터를 읽는 일을 막습니다.

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)

64비트 직접 기동 방법이 없을 때 일반 APIC 드라이버의 CPU 시작 함수가 있는지 검사합니다.

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

APIC 드라이버가 제공하는 시작 함수를 호출하고 결과를 ret에 저장합니다. APIC ID·진입 주소·논리 CPU 번호를 넘깁니다.

899else

드라이버 전용 시작 방법이 없으면 기본 INIT 기반 기동 경로를 사용합니다.

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

INIT 및 필요한 STARTUP IPI를 이용해 보조 CPU를 깨우고 전송 결과를 받습니다. 이 반환만으로 정상 커널의 online 단계까지 끝났다는 뜻은 아닙니다.

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 

주요 구문 해설

232static void notrace __noendbr start_secondary(void *unused)

보조 CPU가 어셈블리 초기화를 마친 뒤 들어오는 C 함수입니다. notrace와 __noendbr는 이 특수한 초기 진입에 일반 함수 계측이나 불필요한 분기 도착 코드를 넣지 않도록 합니다.

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

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

239cr4_init();

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

245if (IS_ENABLED(CONFIG_X86_32)) {

32비트 빌드에서만 초기 페이지 테이블을 정상 커널 페이지 테이블로 바꾸는 구간을 실행합니다. 64비트 경로는 앞의 어셈블리에서 이미 필요한 표를 설정했습니다.

247load_cr3(swapper_pg_dir);

커널의 swapper_pg_dir를 가리키도록 CR3를 설정합니다. 초기 부팅용 주소 변환 표에서 벗어나는 32비트 경로입니다.

248__flush_tlb_all();

이 CPU의 오래된 TLB 항목을 정리합니다. 새 페이지 테이블을 적용한 뒤 예전 주소 변환을 사용하지 않도록 합니다.

251cpu_init_exception_handling(false);

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

269load_ucode_ap();

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

276cpuhp_ap_sync_alive();

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

278cpu_init();

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

279fpu__init_cpu();

현재 보조 CPU의 부동소수점·확장 상태 관련 하드웨어 설정을 초기화합니다. 이후 task별 FPU 상태 저장·복원이 정상 작동할 기반을 마련합니다.

280rcutree_report_cpu_starting(raw_smp_processor_id());

RCU에 이 CPU가 시작하고 있음을 알립니다. RCU가 CPU의 관찰·진행 상태를 온라인 전환 과정에 맞춰 관리하게 합니다.

281x86_cpuinit.early_percpu_clock_init();

플랫폼이 지정한 CPU별 초기 시계 초기화 함수를 호출합니다. CPU가 사용할 시간 측정 환경을 준비합니다.

283ap_starting();

보조 CPU 시작 단계의 공통 처리로 들어갑니다. CPU 식별과 topology·local APIC 등 뒤의 online 전환에 필요한 준비를 진행합니다.

286check_tsc_sync_target();

제어 CPU와 현재 보조 CPU 사이에서 TSC가 맞는지 검사합니다. TSC는 CPU 시간 측정에 쓰이는 카운터이며, CPU 간 차이는 시간 계산 오류를 만들 수 있습니다.

293ap_calibrate_delay();

현재 보조 CPU의 짧은 지연 루프에 필요한 값을 보정합니다. 바로 앞 TSC 동기화 결과를 활용해 불필요한 재측정을 줄일 수 있습니다.

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

주요 구문 해설

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

대상 APIC ID로 INIT와 STARTUP 절차를 보내 보조 CPU를 시작하는 기본 구현입니다. 진입 주소는 start_eip로 받으며 전송 오류 상태를 반환합니다.

697unsigned long send_status = 0, accept_status = 0;

IPI 전송 완료 상태와 APIC 수신 오류 상태를 각각 0으로 초기화합니다. 두 종류의 실패를 마지막에 함께 판정합니다.

698int num_starts, j, maxlvt;

STARTUP 전송 횟수, 반복 인덱스, local APIC의 LVT 범위 정보를 담을 정수 변수들을 선언합니다.

700preempt_disable();

이 기동 절차가 진행되는 동안 현재 task가 다른 CPU로 옮겨가거나 선점되지 않게 합니다. 현재 CPU의 local APIC를 사용하는 일련의 처리입니다.

701maxlvt = lapic_get_maxlvt();

local APIC가 제공하는 LVT 항목 범위 정보를 읽습니다. 아래 ESR 접근에서 오래된 APIC의 erratum 조건을 구분하는 데도 사용합니다.

702send_init_sequence(phys_apicid);

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

704mb();

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

712if (APIC_INTEGRATED(boot_cpu_apic_version))

통합 local APIC라면 STARTUP IPI를 보내는 방식으로 기동합니다. 다음 줄은 최대 전송 횟수를 2로 설정합니다.

713num_starts = 2;

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

714else

통합 APIC가 아닌 오래된 방식에서는 STARTUP IPI 반복을 사용하지 않습니다.

715num_starts = 0;

STARTUP IPI 반복 횟수를 0으로 지정합니다. 이미 앞의 INIT 및 해당 하드웨어의 기존 기동 절차를 따르는 경우입니다.

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

예정된 STARTUP IPI 반복 횟수를 디버그 로그에 출력합니다.

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

설정한 횟수만큼 STARTUP IPI를 보냅니다. 매 회 전송·수신 상태를 확인하며 오류가 있으면 아래에서 반복을 중단합니다.

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

현재 몇 번째 STARTUP IPI를 보내는지 기록합니다. 반복 인덱스는 1부터 시작합니다.

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

local APIC의 LVT 범위가 조건을 만족할 때만 ESR에 쓰도록 합니다. 소스에 명시된 Pentium erratum 3AP를 고려한 분기입니다.

725apic_write(APIC_ESR, 0);

이번 STARTUP 전송 전에 ESR에 0을 써 오류 상태를 정리합니다.

726apic_read(APIC_ESR);

ESR을 읽어 오류 상태 정리 절차를 이어갑니다. 이 시점의 결과는 성공·실패 판정 변수에 저장하지 않습니다.

727pr_debug("After apic_write\n");

ESR 초기화 지점을 통과했다는 디버그 로그입니다. CPU 기동 완료 통지는 아닙니다.

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

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

737phys_apicid);

앞줄의 ICR 쓰기 함수에 대상 하드웨어 APIC ID를 넘깁니다. Linux 논리 CPU 번호가 아니라 IPI를 실제로 받을 프로세서의 식별자입니다.

742if (init_udelay == 0)

초기 기동 지연을 생략하는 설정인지 검사합니다. 이 경우 아래의 짧은 대기 시간을 사용하고, 아니면 더 긴 하드웨어 안정화 시간을 둡니다.

743udelay(10);

STARTUP IPI를 다른 CPU가 받아들일 시간을 주기 위해 10마이크로초 동안 busy wait합니다. task를 재우는 밀리초 단위 대기가 아닙니다.

744else

짧은 지연 조건에 해당하지 않으면 오래된 하드웨어를 고려한 더 긴 대기를 사용합니다.

745udelay(300);

STARTUP IPI 전송 뒤 300마이크로초 동안 기다립니다. 이후 전송 레지스터의 완료 상태를 확인합니다.

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

STARTUP 전송 뒤 첫 대기 지점을 지났음을 디버그 출력합니다.

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

local APIC의 ICR 전송 완료를 확인하기 직전의 디버그 로그입니다. 대상 CPU의 전체 초기화 완료를 기다린다는 뜻은 아닙니다.

750send_status = safe_apic_wait_icr_idle();

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

755if (init_udelay == 0)

ICR 전송 완료 검사 뒤에도 수신 CPU가 요청을 처리할 시간을 얼마나 더 줄지 선택합니다.

756udelay(10);

짧은 지연 설정이면 10마이크로초를 더 기다립니다. 아래 ESR 판정 전에 수신 측 진행 시간을 확보합니다.

757else

짧은 지연을 사용하지 않는 경우의 분기입니다. 바로 다음 줄에서 200마이크로초를 더 기다린 뒤 APIC 오류 상태를 확인합니다.

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

01

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

user RIP=0x00007f0012345678, user RSP=0x00007fffffffdc00, RFLAGS=0x246, RAX=60인 exit 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=60을 이 순서로 push한다. 완성된 pt_regs의 시작 주소 기준 필드 순서는 그림 1처럼 반대 방향이다.
  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 번호를 함께 기록한다.

기준 원문

↑