Linux v6.18.37 · 개념과 코드 읽기

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

이 코드는 어떤 문제를 푸나요?

x86 SYSCALL은 일반 IDT 진입과 다르게 사용자 RSP에서 커널 스택으로 자동 전환하지 않습니다. v6.18.37 entry_SYSCALL_64의 시작 부분을 읽으며 Linux가 빠진 프레임을 직접 만드는 이유를 설명합니다. 이 발췌는 함수 전체가 아니라 커널 C 호출 전의 프레임 구성 구간입니다.

읽을 범위: v6.18.37 · arch/x86/entry/entry_64.S · entry_SYSCALL_64의 프레임 구성 구간 87–121행입니다. 아래에 이 범위의 원문과 각 줄의 설명을 실었습니다. 주제 전체의 흐름과 다른 경로는 기존 분석에서 함께 읽으실 수 있습니다.

먼저 알아둘 개념

SWAPGS

커널 per-CPU 자료에 접근할 GS 기준을 준비합니다.

RCX와 R11

SYSCALL이 남긴 사용자 복귀 RIP와 RFLAGS를 담습니다. 일반 C 인수 레지스터로 읽으면 안 됩니다.

SYM_INNER_LABEL

함수·코드 내부의 이름 붙은 위치를 만들며 C 호출을 실행하지는 않습니다.

처음 읽을 때

사용자 SP를 보관하기 전에는 그것을 scratch로 사용할 수 없다는 순서를 설명하십시오.

더 깊이 살펴볼 때

KPTI의 CR3 전환, unwind·IBT 메타데이터, 실제 하드웨어 프레임과 소프트웨어 합성 프레임의 차이를 검토하십시오.

그림으로 보는 변화

x86-64 IDT·syscall 진입과 secondary CPU 기동의 단계별 개념 그림
각 단계에 화살표 의미와 생략 범위를 표시했습니다. 주소·숫자 예제는 실제 장치 값을 뜻하지 않습니다.
1단계 설명

1단계 고정

GIF 원본 열기

1. 사용자 SP 보관

SWAPGS 후 TSS scratch 영역에 사용자 RSP를 남깁니다.

화살표는 사용자 스택 위치 값의 보존입니다.

2. 커널 스택 선택

커널 CR3와 현재 task의 커널 스택을 선택합니다.

주소 공간 전환과 SP 전환을 각각 나타냅니다.

3. pt_regs 구성

SS·SP·flags·CS·IP와 일반 레지스터를 스택에 쌓습니다.

화살표는 메모리 저장이며 아래쪽은 낮아지는 RSP 방향입니다.

entry_SYSCALL_64의 프레임 구성 구간를 한 줄씩 읽기

줄 번호는 v6.18.37 원문 기준입니다. 주석·빈 줄을 포함한 함수 전체를 먼저 보고, 그 아래에서 각 줄을 설명합니다.

SYM_CODE_START(entry_SYSCALL_64)
	UNWIND_HINT_ENTRY
	ENDBR

	swapgs
	/* tss.sp2 is scratch space. */
	movq	%rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
	SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
	movq	PER_CPU_VAR(cpu_current_top_of_stack), %rsp

SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL)
	ANNOTATE_NOENDBR

	/* Construct struct pt_regs on stack */
	pushq	$__USER_DS				/* pt_regs->ss */
	pushq	PER_CPU_VAR(cpu_tss_rw + TSS_sp2)	/* pt_regs->sp */
	pushq	%r11					/* pt_regs->flags */
	pushq	$__USER_CS				/* pt_regs->cs */
	pushq	%rcx					/* pt_regs->ip */
SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)
	pushq	%rax					/* pt_regs->orig_ax */

	PUSH_AND_CLEAR_REGS rax=$-ENOSYS

	/* IRQs are off. */
	movq	%rsp, %rdi
	/* Sign extend the lower 32bit as syscall numbers are treated as int */
	movslq	%eax, %rsi

	/* clobbers %rax, make sure it is after saving the syscall nr */
	IBRS_ENTER
	UNTRAIN_RET
	CLEAR_BRANCH_HISTORY

	call	do_syscall_64		/* returns with IRQs disabled */
SYM_CODE_START(entry_SYSCALL_64)

SYSCALL 진입 코드의 전역 기호 시작을 표시합니다. 이 발췌는 전체 함수 중 C 호출 전 구간입니다.

	UNWIND_HINT_ENTRY

스택 추적 도구가 진입 시점의 unwind 상태를 알도록 힌트를 남깁니다. 레지스터를 저장하는 CPU 명령이 아닙니다.

	ENDBR

IBT 구성에 맞는 간접 분기 도착점 표시를 둡니다. 실제 생성 형태는 빌드 설정과 매크로 정의에 따릅니다.

	swapgs

GS 기준을 커널 per-CPU 자료 접근에 맞게 전환합니다. 뒤의 PER_CPU_VAR 접근을 위한 준비입니다.

	movq	%rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)

사용자 RSP를 per-CPU TSS의 scratch 필드 sp2에 보관합니다. 아직 커널 스택을 선택하기 전의 사용자 스택 위치입니다.

	SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp

필요한 구성에서 커널 CR3로 바꾸며 RSP를 임시 레지스터로 허용합니다. 그래서 사용자 RSP를 앞에서 먼저 저장했습니다.

	movq	PER_CPU_VAR(cpu_current_top_of_stack), %rsp

현재 task의 커널 스택 상단을 실제 RSP에 넣습니다. 이후 push는 이 스택에 기록됩니다.

SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL)

안전한 커널 스택이 마련된 내부 위치에 전역 라벨을 붙입니다. 독립 함수 호출은 아닙니다.

	ANNOTATE_NOENDBR

이 내부 지점에 ENDBR이 없음을 분석 도구에 표시합니다.

	pushq	$__USER_DS				/* pt_regs->ss */

사용자 SS 값을 푸시하여 pt_regs의 하드웨어 프레임 모양을 소프트웨어로 구성합니다.

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

TSS scratch에 보관한 원래 사용자 RSP를 프레임에 넣습니다.

	pushq	%r11					/* pt_regs->flags */

SYSCALL이 R11에 남긴 사용자 RFLAGS를 보존합니다.

	pushq	$__USER_CS				/* pt_regs->cs */

사용자 CS 선택자를 프레임에 기록합니다.

	pushq	%rcx					/* pt_regs->ip */

SYSCALL이 RCX에 남긴 사용자 복귀 RIP를 기록합니다.

SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)

하드웨어 프레임 모양의 항목을 완성한 내부 지점에 이름을 붙입니다. 앞의 push가 실제로 하드웨어에서 자동 실행됐다는 뜻은 아닙니다.

	pushq	%rax					/* pt_regs->orig_ax */

사용자 RAX에 있던 syscall 번호를 orig_ax로 남깁니다. 뒤에서 RAX를 반환값 용도로 바꿔도 원래 번호를 추적할 수 있습니다.

	PUSH_AND_CLEAR_REGS rax=$-ENOSYS

나머지 레지스터를 저장하고 작업용 값을 정리하는 매크로입니다. RAX 초기 결과를 -ENOSYS로 두어 미구현 호출의 기본 오류값을 마련합니다.

	movq	%rsp, %rdi

현재 RSP, 즉 pt_regs의 주소를 C 첫 인수 RDI에 넣습니다.

	movslq	%eax, %rsi

시스템 호출 번호는 int로 해석하므로 EAX의 32비트 값을 부호 확장하여 두 번째 C 인수 RSI에 넣습니다.

	IBRS_ENTER

커널 진입에 필요한 분기 예측 완화 처리를 적용합니다. RAX를 사용할 수 있으므로 원래 번호 저장 뒤에 위치합니다.

	UNTRAIN_RET

지원·설정 조건에 맞는 복귀 예측 완화 매크로를 실행합니다.

	CLEAR_BRANCH_HISTORY

분기 이력 관련 완화를 적용합니다. 매크로가 모든 시스템에서 동일한 긴 명령열을 만든다고 가정하지 않습니다.

	call	do_syscall_64		/* returns with IRQs disabled */

준비된 regs와 syscall 번호로 do_syscall_64를 호출합니다. 주석처럼 돌아올 때 IRQ는 차단된 상태이며 사용자 복귀 선택은 다음 구간에서 계속됩니다.

함께 생각해 볼 질문

SYSCALL이 이 pt_regs를 자동으로 모두 만듭니까?

아닙니다. 아래 push 명령은 Linux가 프레임을 직접 만드는 부분입니다.

R11을 먼저 자유롭게 사용해도 됩니까?

복귀 flags를 보존하기 전에 덮어쓰면 원래 사용자 상태를 잃습니다.

보조 CPU 기동도 같은 입구를 씁니까?

아닙니다. secondary startup은 별도의 부팅 경로입니다.

출처와 읽은 범위

Linux stable v6.18.37 · arch/x86/entry/entry_64.S

해당 버전 원본 파일 · 기존 코드 분석 · 설명 원고

맨 위로 ↑