x86_64 명령어셋 레퍼런스

x86_64(AMD64/Intel 64) 아키텍처의 명령어를 AT&T 문법 기준으로 종합 정리합니다. CISC 설계 철학과 가변 길이 인코딩, 16개 범용 레지스터(Register)와 RFLAGS, SSE/AVX/AVX-512 SIMD 레지스터, 주소 지정 모드, 데이터 전송·산술·논리·분기·스택·시스템·원자적(Atomic)·SIMD 명령어 카테고리 표, REX/VEX/EVEX 인코딩 다이어그램, 커널 핵심 명령어(SYSCALL, SWAPGS, LOCK CMPXCHG, INVLPG) 고급까지 Linux 커널 개발에 필요한 x86_64 ISA 전체를 다룹니다.

전제 조건: 어셈블리(Assembly) 종합을 먼저 읽으세요. x86_64 명령어 레퍼런스는 AT&T 문법, 레지스터 규약, 피연산자 크기 접미사에 대한 기본 이해가 필요합니다.
일상 비유: x86_64는 스위스 아미 나이프와 같습니다. 하나의 도구(명령어)가 다양한 모드와 접두사를 조합해 수백 가지 동작을 수행할 수 있지만, 그만큼 복잡합니다. CISC 설계 특유의 풍부한 명령어 세트를 카테고리별로 정리하면 전체 구조가 보입니다.

핵심 요약

  • CISC 아키텍처 — 가변 길이 명령어(1~15바이트), 메모리 피연산자 직접 사용, 풍부한 주소 모드.
  • AT&T 문법 — 소스→목적지 순서, % 레지스터 접두사, $ 즉시값 접두사, b/w/l/q 크기 접미사.
  • 16개 범용 레지스터 — RAX-RDI(기존 8개) + R8-R15(AMD64 확장), 64/32/16/8-bit 접근 가능.
  • SIMD 확장 — SSE(XMM, 128-bit) → AVX(YMM, 256-bit) → AVX-512(ZMM, 512-bit + 마스크 레지스터).
  • LOCK 접두사 — 원자적 메모리 연산의 핵심. CMPXCHG, XADD 등과 결합.

단계별 이해

  1. 레지스터 맵 파악
    16개 범용 레지스터의 이름 규칙(RAX/EAX/AX/AL)과 System V ABI 용도를 먼저 익힙니다.
  2. AT&T 문법 숙지
    접미사(b/w/l/q), 접두사(%/$), 소스→목적지 순서를 반드시 구분합니다.
  3. 주소 모드 이해
    displacement(base,index,scale) SIB 형식이 x86_64 주소 지정의 핵심입니다.
  4. 카테고리별 명령어 학습
    데이터 전송 → 산술 → 논리 → 분기 → 시스템 순서로 진행하면 체계적입니다.

아키텍처 개요

x86_64(AMD64)는 2003년 AMD가 IA-32(x86)를 64-bit로 확장한 CISC 아키텍처입니다. 8086(1978) → 80386(1985, 32-bit) → AMD64(2003, 64-bit) 순으로 발전했으며, 하위 호환성을 유지하면서 레지스터와 주소 공간(Address Space)을 확장했습니다.

특성x86_64
설계 철학CISC (Complex Instruction Set Computer)
명령어 길이가변 (1~15바이트)
엔디언리틀 엔디언 (Little-Endian)
범용 레지스터16개 (64-bit)
주소 공간가상 48-bit (256TB), 물리 최대 52-bit
동작 모드Long Mode (64-bit), Compatibility Mode (32-bit), Legacy Mode
페이지(Page) 크기4KB, 2MB, 1GB
명령어 접두사REX, VEX, EVEX, LOCK, REP, 세그먼트 오버라이드
Long Mode: 64-bit 모드에서는 기본 오퍼랜드 크기가 32-bit이며, REX.W 접두사로 64-bit 연산을 지정합니다. 주소 크기는 기본 64-bit입니다.

동작 모드 전환 다이어그램

x86_64 프로세서는 전원 인가 후 Real Mode에서 시작하며, 제어 레지스터 비트를 조작해 단계적으로 Long Mode(64-bit)까지 전환합니다. 리눅스 커널 부트 과정에서 이 전환은 arch/x86/boot/compressed/head_64.S에서 수행됩니다.

x86_64 동작 모드 전환 (Power-On → Long Mode) Real Mode 16-bit 주소 1MB 메모리 (전원 인가 시 시작) CR0.PE=1 Protected Mode 32-bit 주소 4GB 메모리, 특권 링 (GDT/IDT 설정 필요) CR4.PAE=1 EFER.LME=1 CR0.PG=1 Long Mode (IA-32e) 64-bit 가상 주소 256TB (48-bit), 4-level 페이징 16개 범용 레지스터 64-bit Mode CS.L=1, CS.D=0 커널 + 64-bit 앱 Compat Mode CS.L=0 32-bit 앱 Legacy Mode EFER.LME=0 (Long Mode 미전환) 전환 순서 (리눅스 부트): 1. GDT 설정 + CR0.PE=1 (Protected Mode 진입) 2. CR4.PAE=1 (PAE 활성) → CR3=PML4 (4-level 페이지 테이블 설정) → EFER.LME=1 → CR0.PG=1 (Long Mode 활성) 3. CS.L=1인 코드 세그먼트로 far jump → 64-bit Mode 실행
주의: Long Mode 진입 전에 반드시 4-level 페이징(PML4)을 설정해야 합니다. CR0.PG=1과 EFER.LME=1이 동시에 활성화되면 프로세서는 Long Mode로 전환됩니다. 페이지 테이블(Page Table)이 올바르지 않으면 즉시 트리플 폴트가 발생합니다.

레지스터 셋

범용 레지스터 (64-bit / 32-bit / 16-bit / 8-bit)

64-bit32-bit16-bit8-bit (Low)8-bit (High)System V ABI 용도
%rax%eax%ax%al%ah반환값 (1st)
%rbx%ebx%bx%bl%bhCallee-saved
%rcx%ecx%cx%cl%ch4th 인자
%rdx%edx%dx%dl%dh3rd 인자, 반환값 (2nd)
%rsi%esi%si%sil2nd 인자
%rdi%edi%di%dil1st 인자
%rbp%ebp%bp%bpl프레임 포인터 (Callee-saved)
%rsp%esp%sp%spl스택 포인터
%r8%r8d%r8w%r8b5th 인자
%r9%r9d%r9w%r9b6th 인자
%r10%r10d%r10w%r10bCaller-saved
%r11%r11d%r11w%r11bCaller-saved
%r12%r12d%r12w%r12bCallee-saved
%r13%r13d%r13w%r13bCallee-saved
%r14%r14d%r14w%r14bCallee-saved
%r15%r15d%r15w%r15bCallee-saved

레지스터 중첩 구조 (RAX 예시)

x86_64 범용 레지스터는 하위 호환성을 위해 중첩 구조를 가집니다. 64-bit RAX 안에 32-bit EAX가, 그 안에 16-bit AX가, AX는 다시 상위 AH와 하위 AL로 나뉩니다.

RAX 레지스터 비트 구조 (64-bit) 63 32 31 16 15 8 7 0 RAX (64-bit) 상위 32-bit (32-bit 쓰기 시 0) EAX (32-bit) 비트 31-16 AX (16-bit) AH (비트 15-8) AL (비트 7-0) movl %eax, ... 실행 시 RAX의 비트 63-32가 자동으로 0이 됨 (제로 확장)
32-bit 쓰기 주의: x86_64에서 32-bit 레지스터(EAX, EBX 등)에 쓰면 상위 32-bit이 자동으로 0이 됩니다. 예: movl $1, %eax → RAX = 0x00000001. 반면 16-bit/8-bit 쓰기는 상위 비트에 영향을 주지 않습니다. 예: movw $1, %ax → RAX의 상위 48-bit은 그대로 유지됩니다. R8-R15 레지스터에서는 AH 같은 상위 8-bit 접근이 불가능합니다.

RFLAGS 레지스터 주요 비트

비트약어이름설명
0CFCarry Flag부호 없는 연산 올림/빌림
2PFParity Flag결과 하위 바이트의 짝수 패리티
6ZFZero Flag결과가 0이면 세트
7SFSign Flag결과의 최상위 비트 (부호)
8TFTrap Flag단일 스텝 디버깅(Debugging)
9IFInterrupt Flag인터럽트(Interrupt) 활성화/비활성화
10DFDirection Flag문자열 명령어 방향 (0=증가, 1=감소)
11OFOverflow Flag부호 있는 연산 오버플로

특수/시스템 레지스터

레지스터설명
%rip명령어 포인터 (Instruction Pointer)
%cs, %ds, %es, %fs, %gs, %ss세그먼트 레지스터 (Long Mode에서 FS/GS만 유효)
CR0제어 레지스터: PE(보호모드), PG(페이징), WP(쓰기 보호(Write Protection)) 등
CR2Page Fault 선형 주소
CR3페이지 디렉토리 베이스 (PML4 물리 주소(Physical Address))
CR4확장 기능: PAE, PSE, OSXSAVE, PCIDE, SMEP, SMAP 등
MSR EFERExtended Feature Enable: LME(Long Mode Enable), SCE(SYSCALL Enable), NXE
MSR LSTARSYSCALL 진입 주소 (64-bit)
MSR STARSYSCALL/SYSRET CS/SS 세그먼트
MSR FMASKSYSCALL 시 RFLAGS 마스크

SIMD 레지스터

확장레지스터크기개수
SSEXMM0-XMM15128-bit16
AVXYMM0-YMM15256-bit16
AVX-512ZMM0-ZMM31512-bit32
AVX-512 마스크k0-k764-bit8 (k0은 암묵적 all-ones)
SSE 제어MXCSR32-bit1 (라운딩 모드, 예외 마스크)

SIMD 레지스터 중첩 구조

SIMD 레지스터는 SSE → AVX → AVX-512 확장에 따라 중첩 구조를 가집니다. ZMM의 하위 256-bit은 YMM이고, YMM의 하위 128-bit은 XMM입니다.

SIMD 레지스터 중첩 구조 (ZMM0 예시) 511 256 255 128 127 0 ZMM0 (512-bit) — AVX-512 YMM0 (256-bit) — AVX XMM0 (128-bit) — SSE 요소 패킹 예시 (XMM 128-bit 기준) float x 4 (PS) 32b 32b 32b 32b double x 2 (PD) 64b 64b int32 x 4 (D) int16 x 8 (W) int8 x 16 (B) int64 x 2 (Q) VEX 128-bit 명령어 실행 시 YMM 상위 128-bit 자동 제로화 | SSE 레거시 명령어는 상위 비트 보존 (성능 저하 가능)
커널에서의 SIMD: 리눅스 커널은 기본적으로 SIMD 레지스터를 사용하지 않습니다. 커널 공간(Kernel Space)에서 XMM/YMM/ZMM 레지스터를 사용하려면 반드시 kernel_fpu_begin() / kernel_fpu_end()로 FPU 상태를 저장/복원해야 합니다. 이는 주로 암호화(aesni-intel), CRC 계산, RAID XOR 등에서 활용됩니다.

주소 지정 모드

x86_64 AT&T 문법에서 메모리 피연산자는 displacement(base, index, scale) 형식을 사용합니다.

모드AT&T 문법계산예제
즉시값$imm상수 값movq $42, %rax
레지스터%reg레지스터 값movq %rbx, %rax
직접 메모리addr[addr]movq 0x1000, %rax
간접(%reg)[reg]movq (%rdi), %rax
오프셋(Offset)disp(%reg)[reg + disp]movq 8(%rbp), %rax
인덱스(%base,%idx)[base + idx]movq (%rax,%rcx), %rdx
SIBdisp(%base,%idx,s)[base + idx*s + disp]movq 16(%rdi,%rsi,8), %rax
RIP 상대symbol(%rip)[RIP + offset]movq var(%rip), %rax
크기 접미사: b(byte, 8-bit), w(word, 16-bit), l(long, 32-bit), q(quad, 64-bit). 예: movb, movw, movl, movq.

ModR/M 및 SIB 바이트 구조

x86_64 메모리 피연산자의 인코딩에서 ModR/M 바이트는 주소 모드와 레지스터를 지정하고, SIB(Scale-Index-Base) 바이트는 복합 주소 계산(base + index * scale + displacement)을 인코딩합니다. ModR/M의 R/M 필드가 100(RSP 인코딩)이면 SIB 바이트가 뒤따릅니다.

ModR/M 바이트 (1 byte = 8 bits) 7 6 5 4 3 2 1 0 Mod (2 bits) Reg/Opcode (3 bits) R/M (3 bits) Mod: 00=간접 01=disp8, 10=disp32 11=레지스터 R/M=100 (RSP) → SIB 사용 SIB 바이트 (Scale-Index-Base, 1 byte) 7 6 5 4 3 2 1 0 Scale (2 bits) Index (3 bits) Base (3 bits) Scale: 00=x1 01=x2, 10=x4 11=x8 유효 주소 = Base + (Index * Scale) + Displacement 예: 16(%rdi,%rsi,8) → Mod=01(disp8), R/M=100(SIB), Scale=11(x8), Index=RSI, Base=RDI, Disp=16
RIP 상대 주소: x86_64 Long Mode에서는 Mod=00, R/M=101 조합이 RIP 상대 주소를 의미합니다 (32-bit에서는 절대 주소). movq var(%rip), %rax에서 어셈블러는 현재 RIP부터 var까지의 오프셋을 32-bit displacement로 인코딩합니다. 이 방식은 Position-Independent Code(PIC)에 필수적이며, 커널의 KASLR(Kernel Address Space Layout Randomization)에서도 활용됩니다.

데이터 전송 명령어

명령어AT&T 문법설명동작
MOVmovq %rax, %rbx데이터 이동dst ← src
MOVSXmovslq %eax, %rbx부호 확장 이동dst ← sign_extend(src)
MOVZXmovzbl %al, %eax제로 확장 이동dst ← zero_extend(src)
LEAleaq 8(%rdi,%rsi,4), %rax유효 주소 계산dst ← effective_address (메모리 접근 없음)
XCHGxchgq %rax, %rbx값 교환 (메모리 시 암묵적 LOCK)tmp ← dst; dst ← src; src ← tmp
BSWAPbswap %eax바이트 순서(Byte Order) 반전엔디언 변환
CMOVcccmovzq %rbx, %rax조건부 이동조건 충족 시 dst ← src
MOVSmovsq문자열 복사[RDI] ← [RSI]; RSI/RDI 갱신
LODSlodsq문자열 로드RAX ← [RSI]; RSI 갱신
STOSstosq문자열 저장[RDI] ← RAX; RDI 갱신
PUSHpushq %rax스택 푸시RSP -= 8; [RSP] ← src
POPpopq %rax스택 팝dst ← [RSP]; RSP += 8
MOV vs LEA vs MOVSX/MOVZX 비교:
  • MOV — 메모리에서 값을 읽어서 레지스터에 저장합니다. movq (%rdi), %rax는 RDI가 가리키는 주소의 8바이트 값을 RAX에 로드합니다.
  • LEA — 메모리를 접근하지 않고 주소 자체를 계산합니다. leaq 8(%rdi,%rsi,4), %rax는 RDI + RSI*4 + 8 결과를 RAX에 저장하며, 플래그를 변경하지 않습니다. 산술 연산의 대용으로 자주 사용됩니다.
  • MOVSX (movslq) — 작은 크기의 부호 있는 값을 큰 레지스터로 부호 확장합니다. 커널에서 int(32-bit) → long(64-bit) 변환에 필수적입니다.
  • MOVZX (movzbl) — 작은 크기의 부호 없는 값을 제로 확장합니다. u8u64 변환 등에 사용됩니다.
/* 커널에서 자주 보이는 패턴 */

/* 1. LEA로 구조체 멤버 주소 계산 (메모리 접근 없음) */
/*    leaq offset(%rdi), %rax   — container_of() 매크로 결과 */

/* 2. MOVSX: syscall 번호(int) → 테이블 인덱스(long) */
/*    movslq %eax, %rax  — 32-bit 부호 있는 값을 64-bit로 확장 */

/* 3. MOVZX: 바이트 읽기 → 레지스터 제로 확장 */
/*    movzbl (%rdi), %eax  — 1바이트 로드, EAX 제로 확장 (RAX 상위도 0) */

/* 4. CMOVcc: 조건부 이동으로 분기 제거 */
int clamp_min(int val, int min_val) {
    return val < min_val ? min_val : val;
    /* 컴파일 결과:
       cmpl %esi, %edi      // val - min_val
       cmovll %esi, %edi    // val < min_val이면 edi = min_val
       movl %edi, %eax      // return
    */
}

산술 명령어

명령어AT&T 문법설명플래그 영향
ADDaddq %rax, %rbx덧셈CF, ZF, SF, OF, PF
SUBsubq %rax, %rbx뺄셈CF, ZF, SF, OF, PF
ADCadcq %rax, %rbx올림 포함 덧셈CF, ZF, SF, OF, PF
SBBsbbq %rax, %rbx빌림 포함 뺄셈CF, ZF, SF, OF, PF
INCincq %rax1 증가 (CF 미변경)ZF, SF, OF, PF
DECdecq %rax1 감소 (CF 미변경)ZF, SF, OF, PF
NEGnegq %rax2의 보수 부정CF, ZF, SF, OF, PF
MULmulq %rbx부호 없는 곱셈CF, OF (RDX:RAX ← RAX * src)
IMULimulq %rbx, %rax부호 있는 곱셈 (2/3 오퍼랜드)CF, OF
DIVdivq %rbx부호 없는 나눗셈RAX ← 몫, RDX ← 나머지
IDIVidivq %rbx부호 있는 나눗셈RAX ← 몫, RDX ← 나머지
CQOcqoRAX 부호를 RDX로 확장
CDQcdqEAX 부호를 EDX로 확장
LEA를 이용한 산술 트릭: LEA는 주소 계산 명령어지만, 플래그를 변경하지 않으면서 덧셈과 곱셈을 동시에 수행할 수 있어 커널 코드에서 최적화 용도로 광범위하게 사용됩니다.
  • leaq (%rdi,%rdi,1), %raxrax = rdi * 2 (SHL보다 유연)
  • leaq (%rdi,%rdi,2), %raxrax = rdi * 3
  • leaq (%rdi,%rdi,4), %raxrax = rdi * 5
  • leaq (%rdi,%rdi,8), %raxrax = rdi * 9
  • leaq 7(%rdi,%rdi,8), %raxrax = rdi * 9 + 7
  • leaq (,%rdi,8), %raxrax = rdi * 8 (shift 대체)
/* GCC가 자주 생성하는 LEA 패턴 */

/* x * 3 */
leaq    (%rdi,%rdi,2), %rax    /* rax = rdi + rdi*2 = rdi*3 */

/* x * 5 + 1 */
leaq    1(%rdi,%rdi,4), %rax   /* rax = rdi + rdi*4 + 1 = rdi*5 + 1 */

/* x * 12 (두 단계) */
leaq    (%rdi,%rdi,2), %rax    /* rax = rdi*3 */
shlq    $2, %rax               /* rax = rdi*3*4 = rdi*12 */

/* 주의: LEA는 RFLAGS를 변경하지 않으므로 */
/* CMP/TEST 뒤의 조건 분기를 방해하지 않음 */
cmpq    $0, %rsi
leaq    1(%rdi), %rdi          /* rdi++, 플래그 보존 (INC는 ZF 변경) */
je      .Lzero                  /* 위의 CMP 결과로 분기 */

논리/시프트/비트 조작 명령어

명령어AT&T 문법설명플래그 영향
ANDandq %rax, %rbx비트 ANDZF, SF, PF (CF=OF=0)
ORorq %rax, %rbx비트 ORZF, SF, PF (CF=OF=0)
XORxorq %rax, %rax비트 XOR (셀프 XOR = 0)ZF, SF, PF (CF=OF=0)
NOTnotq %rax비트 반전
TESTtestq %rax, %rbx비트 AND (결과 저장 안 함)ZF, SF, PF (CF=OF=0)
SHL/SALshlq $3, %rax좌측 시프트CF(마지막 밀려난 비트), ZF, SF
SHRshrq $1, %rax논리 우측 시프트CF, ZF, SF
SARsarq $1, %rax산술 우측 시프트 (부호 보존)CF, ZF, SF
ROL/RORrolq $4, %rax좌측/우측 순환 시프트CF, OF
BTbtq $5, %rax비트 테스트CF ← 지정 비트
BTS/BTR/BTCbtsq $5, %rax비트 테스트 후 세트/리셋/토글CF ← 이전 비트
BSFbsfq %rbx, %rax최하위 세트 비트 검색ZF (src=0이면 세트)
BSRbsrq %rbx, %rax최상위 세트 비트 검색ZF
POPCNTpopcntq %rbx, %rax세트 비트 수 카운트ZF (CF=SF=OF=PF=0)
LZCNTlzcntq %rbx, %rax선행 제로 비트 수CF, ZF
TZCNTtzcntq %rbx, %rax후행 제로 비트 수CF, ZF
PEXTpextq %rcx, %rbx, %rax병렬 비트 추출 (BMI2)
PDEPpdepq %rcx, %rbx, %rax병렬 비트 배치 (BMI2)

비교/분기 명령어

명령어AT&T 문법설명조건
CMPcmpq %rax, %rbxrbx - rax (결과 저장 안 함)CF, ZF, SF, OF 세트
TESTtestq %rax, %raxrax & rax (zero 검사)ZF, SF, PF 세트
JE/JZje label같으면 분기ZF=1
JNE/JNZjne label다르면 분기ZF=0
JG/JNLEjg label부호 있는 >ZF=0 && SF=OF
JGE/JNLjge label부호 있는 >=SF=OF
JL/JNGEjl label부호 있는 <SF≠OF
JLE/JNGjle label부호 있는 <=ZF=1 || SF≠OF
JA/JNBEja label부호 없는 >CF=0 && ZF=0
JAE/JNBjae label부호 없는 >=CF=0
JB/JNAEjb label부호 없는 <CF=1
JBE/JNAjbe label부호 없는 <=CF=1 || ZF=1
JS/JNSjs label부호 비트SF=1 / SF=0
JO/JNOjo label오버플로OF=1 / OF=0
JMPjmp label무조건 분기
LOOPloop labelRCX-- 후 0이 아니면 분기

분기 예측(Branch Prediction)과 커널 최적화

현대 x86_64 프로세서는 분기 예측 유닛(BPU)을 사용해 조건 분기의 결과를 투기적으로 예측합니다. 예측이 맞으면 파이프라인(Pipeline)이 지연(Latency) 없이 계속 진행되지만, 예측 실패(misprediction) 시 10~20 사이클의 페널티가 발생합니다.

likely() / unlikely() 매크로(Macro): 리눅스 커널은 GCC의 __builtin_expect()를 활용해 분기 예측 힌트를 제공합니다. 이 매크로는 코드 레이아웃을 변경하여 예상되는 경로(hot path)를 직선으로 배치하고, 예외 경로(cold path)를 별도 위치로 분리합니다.
  • likely(cond)__builtin_expect(!!(cond), 1) — 조건이 참일 가능성이 높음
  • unlikely(cond)__builtin_expect(!!(cond), 0) — 조건이 거짓일 가능성이 높음 (에러 경로 등)
/* include/linux/compiler.h */
#define likely(x)   __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

/* 사용 예시 — 페이지 폴트 핸들러 */
if (unlikely(fault_signal_pending(fault, regs))) {
    /* 시그널 처리 (cold path — 거의 실행되지 않음) */
    return;
}
/* 정상 처리 (hot path — 대부분 여기로 진행) */
/* unlikely() 적용 시 GCC가 생성하는 코드 레이아웃 */
/* hot path가 직선(fall-through)이 되도록 배치 */

.Lhot_path:
    testq   %rax, %rax
    jne     .Lcold_path         /* unlikely: 분기 안 하는 것이 일반적 */
    /* ... 정상 처리 (fall-through) ... */
    ret

    /* cold path는 함수 끝으로 분리 (I-cache 효율) */
    .section .text.unlikely
.Lcold_path:
    /* ... 에러 처리 ... */
    jmp     .Lerror_handler
Spectre 완화: 분기 예측의 투기적 실행(Speculative Execution)은 보안 취약점(Spectre v1/v2)의 원인이 됩니다. 커널은 LFENCE 삽입, retpoline(간접 분기 보호), IBRS/IBPB(Indirect Branch Prediction Barrier) 등의 완화 기법을 사용합니다. array_index_nospec() 매크로는 LFENCE를 사용해 투기적 배열 접근을 방지합니다.

스택/함수 호출 명령어

명령어AT&T 문법설명동작
PUSHpushq %rax스택 푸시RSP -= 8; [RSP] ← src
POPpopq %rax스택 팝dst ← [RSP]; RSP += 8
CALLcall func함수 호출PUSH RIP; RIP ← target
RETret함수 복귀POP RIP
ENTERenter $N, $0스택 프레임(Stack Frame) 설정PUSH RBP; RBP ← RSP; RSP -= N
LEAVEleave스택 프레임 해제RSP ← RBP; POP RBP
System V AMD64 ABI 호출 규약(Calling Convention):
  • 정수 인자: RDI, RSI, RDX, RCX, R8, R9 (순서대로 6개)
  • 반환값: RAX (+ RDX 128-bit)
  • Callee-saved: RBX, RBP, R12-R15
  • Caller-saved: RAX, RCX, RDX, RSI, RDI, R8-R11
  • 스택 16-byte 정렬 필수 (CALL 직전)
  • Red Zone: RSP 아래 128바이트 (리프 함수용, 커널에서는 비활성화)

시스템/특권 명령어

명령어설명특권 수준
SYSCALL시스템 콜(System Call) 진입 (MSR LSTAR → RIP)Ring 3 → Ring 0
SYSRET시스템 콜 복귀 (RCX → RIP)Ring 0 → Ring 3
INT n소프트웨어 인터럽트 (IDT[n])Ring 3
IRET/IRETQ인터럽트/예외 복귀Ring 0
CLI인터럽트 비활성화 (IF=0)Ring 0
STI인터럽트 활성화 (IF=1)Ring 0
HLT프로세서 정지 (인터럽트 대기)Ring 0
LGDT/SGDTGDT 로드/저장Ring 0 / Ring 3
LIDT/SIDTIDT 로드/저장Ring 0 / Ring 3
LLDT/LTRLDT/TSS 로드Ring 0
SWAPGSGS 베이스와 MSR_KERNEL_GS_BASE 교환Ring 0
WRMSR/RDMSRMSR 쓰기/읽기 (ECX=번호)Ring 0
RDTSC타임스탬프 카운터 읽기Ring 3 (CR4.TSD로 제한 가능)
RDTSCPRDTSC + 프로세서 ID (IA32_TSC_AUX)Ring 3
CPUIDCPU 정보 쿼리Ring 3
INVLPGTLB 엔트리 무효화(Invalidation)Ring 0
MOV CRn제어 레지스터 접근Ring 0
MOV DRn디버그 레지스터 접근Ring 0
IN/OUTI/O 포트 입출력(I/O)Ring 0 (또는 IOPL)
WBINVD캐시(Cache) 라이트백 + 무효화Ring 0
CLFLUSH캐시 라인(Cache Line) 플러시(Flush)Ring 3
CLFLUSHOPT최적화된 캐시 라인 플러시Ring 3
CLWB캐시 라인 라이트백 (무효화 없음)Ring 3
MFENCE메모리 펜스 (전체 직렬화(Serialization))Ring 3
LFENCE로드 펜스 + 명령어 직렬화Ring 3
SFENCE스토어 펜스Ring 3
PAUSE스핀-웨이트 루프 힌트Ring 3

SYSCALL/SYSRET 메커니즘 상세

SYSCALL은 인터럽트(INT 0x80)보다 훨씬 빠른 시스템 콜 진입 방법입니다. 스택 전환이나 IDT 조회 없이 MSR에 미리 설정된 값으로 즉시 커널 코드로 점프합니다. 이 과정에서 하드웨어가 자동으로 수행하는 레지스터 조작을 이해하는 것이 중요합니다.

SYSCALL / SYSRET 시스템 콜 흐름 User Space (Ring 3) 사용자 프로세스 SYSCALL SYSRET RIP ← RCX Kernel Space (Ring 0) entry_SYSCALL_64 SWAPGS + 레지스터 저장 sys_call_table RAX = syscall 번호 시스콜 핸들러 sys_read() 등 SYSRET SYSCALL 하드웨어 동작: RCX ← RIP | R11 ← RFLAGS | RIP ← MSR_LSTAR CS ← MSR_STAR[47:32] | SS ← MSR_STAR[47:32]+8 SYSRET 하드웨어 동작: RIP ← RCX | RFLAGS ← R11 CS ← MSR_STAR[63:48]+16 | SS ← MSR_STAR[63:48]+8 커널 부트 시 MSR 초기화 (arch/x86/kernel/cpu/common.c): MSR_LSTAR = entry_SYSCALL_64 /* SYSCALL 시 점프할 커널 진입점 */ MSR_STAR = (USER_CS-16)<<48 | KERNEL_CS<<32 /* CS/SS 세그먼트 */ MSR_FMASK = X86_EFLAGS_IF | X86_EFLAGS_TF | ... /* SYSCALL 시 클리어할 RFLAGS */
SYSRET 보안 주의: SYSRET은 비표준(non-canonical) RCX 값에 대해 Ring 0에서 #GP 예외를 발생시킵니다. 이는 잠재적 보안 취약점(CVE-2014-4699)의 원인이 됩니다. 리눅스 커널은 이를 방지하기 위해 RCX가 표준 주소인지 반드시 검증한 후 SYSRET을 실행합니다. 비표준 주소가 감지되면 더 안전하지만 느린 IRETQ 경로를 사용합니다.

원자적/동기화 명령어

LOCK 접두사는 다음 명령어의 메모리 연산을 원자적으로 만듭니다. 캐시 라인 락 또는 버스(Bus) 락을 통해 멀티코어 환경에서 원자성을 보장합니다.

명령어AT&T 문법설명
LOCK ADDlock addq $1, (%rdi)원자적 덧셈
LOCK SUBlock subq $1, (%rdi)원자적 뺄셈
LOCK INC/DEClock incq (%rdi)원자적 증가/감소
LOCK AND/OR/XORlock andq %rax, (%rdi)원자적 비트 연산
LOCK BTS/BTR/BTClock btsq $5, (%rdi)원자적 비트 테스트-세트/리셋/토글
LOCK XADDlock xaddq %rax, (%rdi)원자적 교환-덧셈 (old → rax)
LOCK CMPXCHGlock cmpxchgq %rcx, (%rdi)원자적 비교-교환 (CAS)
LOCK CMPXCHG16Block cmpxchg16b (%rdi)128-bit CAS (RDX:RAX vs [mem])
XCHGxchgq %rax, (%rdi)교환 (메모리 시 암묵적 LOCK)

LOCK CMPXCHG 패턴 (Compare-And-Swap)

/* atomic_cmpxchg: [rdi]가 rsi이면 rdx로 교체 */
atomic_cmpxchg:
    movq    %rsi, %rax          /* RAX ← expected (old) */
    lock cmpxchgq %rdx, (%rdi) /* if [RDI]==RAX then [RDI]←RDX */
                                  /* RAX ← [RDI] (이전 값) */
    ret                         /* RAX에 이전 값 반환 */
LOCK 접두사 구현 방식: LOCK 접두사의 원자성 보장 메커니즘은 메모리 주소의 정렬 상태에 따라 다릅니다.
  • 캐시 라인 락 (Cache Line Lock) — 대상 메모리가 단일 캐시 라인 내에 정렬되어 있으면, MESI 프로토콜의 Exclusive/Modified 상태를 이용해 해당 캐시 라인만 잠급니다. 다른 코어는 이 캐시 라인에 대한 Invalidate 요청이 완료될 때까지 대기합니다. 이 방식이 대부분의 경우에 사용되며 성능이 좋습니다.
  • 버스 락 (Bus Lock) — 대상이 캐시 라인 경계를 걸쳐 있거나(split lock), 캐시 불가능 메모리인 경우 #LOCK 시그널(Signal)로 전체 메모리 버스를 잠급니다. 이는 시스템 전체 성능에 심각한 영향을 줍니다.
커널은 CONFIG_SPLIT_LOCK_DETECT 옵션으로 split lock을 감지하고 경고합니다.

XCHG 기반 스핀락(Spinlock) 구현

XCHG는 메모리 피연산자와 사용될 때 암묵적으로 LOCK이 적용됩니다. 이 특성을 이용한 스핀락은 가장 기본적인 커널 동기화 방식입니다.

/* 단순 스핀락 구현 (Test-and-Set) */
/* RDI = lock 주소, lock=0 (해제), lock=1 (획득) */

spin_lock:
    movl    $1, %eax
.Lretry:
    xchgl   %eax, (%rdi)          /* 원자적 교환 (암묵적 LOCK) */
    testl   %eax, %eax             /* 이전 값이 0이었으면 → 락 획득 성공 */
    jnz     .Lspin                  /* 아니면 스핀 */
    ret

.Lspin:
    pause                           /* 스핀 루프 힌트 (전력 절감 + 파이프라인) */
    cmpl    $0, (%rdi)              /* Test: 락이 해제되었는지 읽기만 (LOCK 없이) */
    jne     .Lspin                  /* 아직 잠겨있으면 계속 스핀 */
    jmp     .Lretry                 /* 해제된 것 같으면 Set 재시도 */

spin_unlock:
    movl    $0, (%rdi)              /* 단순 쓰기 (x86 스토어 순서 보장) */
    ret
Test-and-Test-and-Set (TTAS): 위 코드에서 .Lspin 루프가 일반 cmpl(LOCK 없음)로 먼저 확인하는 이유는 LOCK 접두사가 캐시 라인을 Exclusive로 가져오기 때문입니다. 여러 코어가 동시에 XCHG를 시도하면 캐시 라인이 계속 바운싱(bouncing)됩니다. 먼저 Shared 상태에서 읽기만 수행하면 캐시 효율이 크게 개선됩니다.

XADD 기반 참조 카운팅

LOCK XADD는 원자적으로 값을 더하면서 이전 값을 반환합니다. 이는 참조 카운팅에서 이전 카운트 확인이 필요할 때 핵심적으로 사용됩니다.

/* 커널 refcount 감소 (arch/x86/include/asm/refcount.h 기반) */
static inline bool refcount_dec_and_test(refcount_t *r)
{
    int val = -1;  /* 감소할 값 */

    asm volatile(LOCK_PREFIX "xaddl %0, %1"
                 : "+r"(val), "+m"(r->refs.counter)
                 :: "memory");

    /* val = 이전 값, counter는 이미 감소됨 */
    /* 이전 값이 1이었으면 → 감소 후 0 → 마지막 참조 해제 */
    return val == 1;
}

/* 사용 패턴 */
if (refcount_dec_and_test(&obj->refcnt)) {
    /* 참조 카운트 0 → 객체 해제 */
    kfree(obj);
}

SIMD/벡터 명령어

SSE 명령어 (128-bit XMM)

명령어설명동작
MOVAPS/MOVUPS정렬/비정렬 팩 단정도 이동XMM ← 128-bit 메모리/XMM
MOVAPD/MOVUPD정렬/비정렬 팩 배정도 이동XMM ← 128-bit 메모리/XMM
ADDPS/ADDPD팩 단정도/배정도 덧셈각 요소 병렬 덧셈
SUBPS/SUBPD팩 단정도/배정도 뺄셈각 요소 병렬 뺄셈
MULPS/MULPD팩 단정도/배정도 곱셈각 요소 병렬 곱셈
DIVPS/DIVPD팩 단정도/배정도 나눗셈각 요소 병렬 나눗셈
MINPS/MAXPS팩 최솟값/최댓값각 요소 min/max
SQRTPS팩 제곱근각 요소 sqrt
CMPPS팩 비교조건 충족 시 0xFFFFFFFF, 아니면 0
SHUFPS팩 셔플즉시값으로 요소 재배치(Relocation)
UNPCKLPS/UNPCKHPS하위/상위 인터리브두 벡터의 요소 교차 배치
PAND/POR/PXOR팩 정수 비트 논리128-bit 논리 연산
PADDB/W/D/Q팩 정수 덧셈바이트/워드/더블워드/쿼드워드 요소별
PMULLW/PMULLD팩 정수 곱셈 (하위)워드/더블워드 요소별 곱셈
PCMPEQB/PCMPGTB팩 바이트 비교 (같음/큼)요소별 비교 → 마스크
PSHUFD더블워드 셔플즉시값으로 4개 요소 재배치
PSHUFB바이트 셔플 (SSSE3)마스크 벡터로 바이트 재배치

AVX 명령어 (256-bit YMM)

AVX 변경점: VEX 접두사 사용, V 접두사 명령어(VADDPS 등), 3-오퍼랜드 형식(비파괴적), 256-bit 확장. 128-bit VEX 명령어는 YMM 상위 128-bit을 자동 제로화합니다.
명령어설명
VMOVAPS/VMOVUPS256-bit 정렬/비정렬 이동
VADDPS/VADDPD3-오퍼랜드 팩 덧셈: dst ← src1 + src2
VFMADD132PS/213/231FMA (Fused Multiply-Add): a*b+c 단일 명령
VBROADCASTSS스칼라를 모든 요소에 복제
VPERM2F128128-bit 레인 순열
VEXTRACTF128YMM에서 128-bit 레인 추출
VINSERTF128YMM에 128-bit 레인 삽입
VZEROALL/VZEROUPPERYMM 상위 제로화 (SSE↔AVX 전환 시)

AVX-512 명령어 (512-bit ZMM)

AVX-512 특징: EVEX 접두사(4바이트), 512-bit ZMM0-31, 마스크 레지스터 k1-k7(k0=전체), 브로드캐스트 {1toN}, 임베디드 라운딩 {rn-sae}, 32개 레지스터.
명령어설명
VMOVAPS zmm{k1}{z}마스크 적용 512-bit 이동 ({z}=제로 마스킹)
VADDPS zmm, zmm, zmm{k1}마스크 적용 512-bit 덧셈
VADDPS zmm, zmm, m512/m32bcst브로드캐스트 로드 후 덧셈
VPCMPD k1{k2}, zmm, zmm, imm팩 정수 비교 → 마스크 레지스터
VPCOMPRESSD마스크 기반 압축 저장
VPEXPANDD마스크 기반 확장 로드
VPERMD/VPERMQ크로스-레인 순열
VCONFLICTD충돌 감지 (히스토그램/scatter용)

커널 핵심 명령어

SYSCALL/SYSRET 진입 경로

x86_64 Linux에서 시스템 콜은 SYSCALL 명령어로 진입합니다. MSR_LSTAR에 저장된 entry_SYSCALL_64 주소로 점프합니다.

/* arch/x86/entry/entry_64.S — entry_SYSCALL_64 핵심 경로 */
entry_SYSCALL_64:
    swapgs                       /* GS를 커널 per-cpu로 교환 */
    movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
    movq    PER_CPU_VAR(cpu_current_top_of_stack), %rsp
    /* pt_regs 구조체 저장 */
    pushq   $__USER_DS            /* SS */
    pushq   PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* RSP */
    pushq   %r11                    /* RFLAGS (SYSCALL이 R11에 저장) */
    pushq   $__USER_CS              /* CS */
    pushq   %rcx                    /* RIP (SYSCALL이 RCX에 저장) */
    pushq   %rax                    /* syscall 번호 */
    /* ... 나머지 레지스터 저장, 시스콜 디스패치 ... */

컨텍스트 전환 시 레지스터 저장/복원

시스템 콜이나 인터럽트로 커널에 진입할 때, 하드웨어와 소프트웨어가 역할을 분담하여 사용자 레지스터를 저장합니다. SYSCALL은 최소한의 레지스터만 하드웨어가 저장하고, 나머지는 커널 진입 코드(entry_SYSCALL_64)가 수동으로 저장합니다.

SYSCALL 진입 시 레지스터 저장 분담 하드웨어 (SYSCALL 명령어) RCX ← RIP (복귀 주소) R11 ← RFLAGS RIP ← MSR_LSTAR CS/SS ← MSR_STAR RSP는 변경되지 않음! (커널이 수동 교체) 소프트웨어 (entry_SYSCALL_64) 1. SWAPGS (GS 교환) 2. RSP 교환 (커널 스택) 3. push SS, RSP (유저) 4. push R11 (RFLAGS) 5. push CS, RCX (RIP) 6. push RAX (syscall 번호) 7. 나머지 범용 레지스터 pt_regs 구조체 R15, R14, R13, R12 RBP, RBX R11, R10, R9, R8 RAX (orig_rax) RCX (= user RIP) RDX, RSI, RDI --- 하드웨어 프레임 --- RIP (= RCX) CS RFLAGS (= R11) RSP (user) SS 비교: 인터럽트/예외 진입 인터럽트 시 하드웨어가 자동 push: SS, RSP, RFLAGS, CS, RIP (+ Error Code, 일부 예외) IRETQ로 복귀 (5개 값 자동 pop) TSS.RSP0에서 커널 스택 자동 로드 SYSCALL보다 느리지만 더 안전

SWAPGS와 per-cpu 데이터 접근 패턴

SWAPGS는 GS 베이스 레지스터와 MSR_KERNEL_GS_BASE를 교환합니다. 커널 진입 시 per-cpu 데이터 접근을 위해 사용되며, 복귀 시 다시 교환합니다. GS 베이스를 통해 현재 CPU의 per-cpu 영역에 빠르게 접근할 수 있습니다.

/* SWAPGS + per-cpu 접근 패턴 */

/* 1. 커널 진입 시: GS를 커널 per-cpu 영역으로 교환 */
/*    SWAPGS → GS.base = 현재 CPU의 per-cpu 오프셋 */

/* 2. per-cpu 변수 접근 (GS 세그먼트 오버라이드) */
/*    movq %gs:offset, %rax  — per-cpu 변수 읽기 */

/* arch/x86/include/asm/percpu.h — 간략화 */
#define this_cpu_read_8(pcp) ({                           \
    u64 val;                                               \
    asm volatile("movq %%gs:%1, %0"                       \
                 : "=r"(val)                               \
                 : "m"(pcp));                               \
    val;                                                     \
})

/* 사용 예: 현재 CPU의 current task 가져오기 */
static inline struct task_struct *get_current(void)
{
    return this_cpu_read_8(current_task);
    /* 컴파일 결과: movq %gs:current_task, %rax */
}

/* 3. 커널 복귀 시: SWAPGS로 GS를 사용자 공간 값으로 복원 */
/*    SWAPGS → GS.base = 사용자 프로세스의 GS (TLS 등) */
SWAPGS 타이밍 주의: SWAPGS는 커널 진입/복귀 시 정확히 한 번만 실행되어야 합니다. 중첩 인터럽트나 예외 상황에서 이미 커널 모드인지 확인하지 않고 SWAPGS를 실행하면 GS가 꼬이게 됩니다. 커널은 CS 세그먼트 값(유저 vs 커널)을 검사하여 SWAPGS 실행 여부를 결정합니다: testb $3, CS(%rsp) — CS의 RPL(Ring Privilege Level) 비트가 3(유저)이면 SWAPGS 필요.

LOCK CMPXCHG — 커널 atomic_cmpxchg

/* arch/x86/include/asm/cmpxchg.h — 간략화 */
static inline u64 __cmpxchg(volatile void *ptr, u64 old, u64 new)
{
    u64 prev;
    asm volatile(LOCK_PREFIX "cmpxchgq %2, %1"
                 : "=a"(prev), "+m"(*(volatile u64 *)ptr)
                 : "r"(new), "0"(old)
                 : "memory");
    return prev;
}

INVLPG — TLB 무효화

/* 단일 페이지 TLB 엔트리 무효화 */
static inline void __invlpg(unsigned long addr)
{
    asm volatile("invlpg (%0)" :: "r"(addr) : "memory");
}

WRMSR/RDMSR — MSR 접근

static inline void wrmsr(u32 msr, u32 low, u32 high)
{
    asm volatile("wrmsr"
                 :: "c"(msr), "a"(low), "d"(high)
                 : "memory");
}

명령어 인코딩

x86_64 명령어는 가변 길이로, 최대 15바이트까지 가능합니다. 레거시 접두사, REX, VEX, EVEX 네 가지 인코딩 체계가 있습니다.

x86_64 Legacy + REX 인코딩 포맷 Legacy Prefix (0-4) REX (0-1 byte) Opcode (1-3 bytes) ModR/M (0-1 byte) SIB (0-1 byte) Displacement (0/1/2/4 bytes) Immediate (0/1/2/4 bytes) 0100 W R X B W=64bit R=reg X=idx B=base VEX / EVEX 접두사 구조 VEX 2-byte: C5h R | vvvv | L | pp VEX 3-byte: C4h R|X|B|mmmmm | W|vvvv|L|pp EVEX 4-byte: 62h R|X|B|R'|mm | W|vvvv|1|pp | z|L'|L|b|V'|aaa L=벡터 길이(0=128,1=256) | pp=오퍼랜드 접두사 | vvvv=추가 소스 레지스터 | aaa=마스크(k0-k7) | z=제로 마스킹 | b=브로드캐스트 mm=오피코드 맵 | W=오퍼랜드 크기 | R/X/B/R'/V'=레지스터 확장 비트

인코딩 실전 예제

실제 명령어가 어떻게 바이트로 인코딩되는지 두 가지 예제를 통해 살펴봅니다.

명령어 인코딩 예제 예제 1: movq $42, %rax → movabs (64-bit 즉시값 이동), 총 10바이트 48h REX.W B8h Opcode+rd 2Ah imm[0] 00h imm[1] 00h imm[2] ... 00h x5 imm[3..7] 0100 1000 W=1 (64-bit) R=0 B=0 B8 + 0 (RAX) MOV reg, imm64 예제 2: movq %rbx, 8(%rdi) → 메모리에 레지스터 저장, 총 4바이트 48h REX.W 89h MOV r/m, r 5Fh ModR/M 08h disp8 (=8) 0101 1111 Mod=01(disp8) Reg=011(RBX) R/M=111(RDI) 인코딩 참고 REX: 0100 WRXB (4-bit 고정 + 4 플래그) B8+rd: 레지스터가 opcode에 내장 RAX=0, RCX=1, RDX=2, RBX=3 RSP=4, RBP=5, RSI=6, RDI=7 R8-R15: REX.B=1 + 하위 3비트 리틀 엔디언: 42=0x2A → 2A 00 00 ...
최적화 힌트: GCC/LLVM은 가능하면 짧은 인코딩을 선택합니다. movq $42, %rax는 실제로 movl $42, %eax(5바이트)로 대체 가능합니다 — 32-bit 쓰기가 자동으로 상위 32-bit를 제로화하므로 결과는 동일하지만 5바이트 절약됩니다. xorl %eax, %eax(2바이트)는 movq $0, %rax(10바이트)보다 효율적인 레지스터 제로화 방법입니다. 또한 xorl은 현대 프로세서에서 제로화 관용어(zeroing idiom)로 인식되어 실행 유닛을 사용하지 않고 레지스터 파일에서 직접 처리됩니다.

최근 아키텍처 확장 (v6.14+)

Intel APX — Advanced Performance Extensions (v6.16+)

커널 6.16에서 Intel APX(Advanced Performance Extensions) 지원이 추가되었습니다. APX는 x86-64의 주요 한계를 해소하는 ISA 확장으로, 다음과 같은 핵심 기능을 제공합니다:

기능설명효과
32개 GPR기존 16개 → 32개 범용 레지스터 (R16~R31)레지스터 스필/리로드 감소, 함수 호출 오버헤드(Overhead) 감소
REX2 접두사새로운 2바이트 REX 접두사 (확장 레지스터 인코딩)R16~R31 접근을 위한 인코딩 지원
NDD (New Data Destination)3-오퍼랜드 형식 (ADD r1, r2, r3)소스 보존 연산 — MOV+연산 패턴 제거
NF (No Flags)플래그 레지스터 미수정 옵션플래그 의존성 제거, 비순차 실행 개선
커널에서의 의미: 32개 레지스터는 커널의 인터럽트 핸들러(Handler), 시스템 콜 경로 등에서 레지스터 압박을 줄입니다. NDD/NF는 컴파일러가 더 효율적인 코드를 생성할 수 있게 합니다. APX는 EVEX 인코딩을 확장하므로 기존 바이너리와 완전 호환됩니다.

4096 CPU 코어 지원 (v6.14+)

커널 6.14에서 x86_64의 최대 CPU 코어 수 제한이 2048에서 4096으로 확장되었습니다 (CONFIG_NR_CPUS 최댓값 증가). 이는 AMD EPYC/Intel Xeon의 고코어 서버와 멀티소켓 시스템의 확장성을 지원합니다.

관련 최적화: 4096 코어 지원과 함께 per-CPU 자료구조의 확장성 개선, SLUB sheaves(v6.18) 등의 락 경합(Contention) 완화 최적화가 병행되고 있습니다.

시스템 레지스터

x86-64의 시스템 레지스터는 CPU 모드 전환, 페이징, 디버깅, 성능 모니터링 등 커널 동작의 핵심을 제어합니다. Control Register(CR0-CR4), Debug Register(DR0-DR7), MSR(Model-Specific Register)의 비트 필드를 정확히 이해해야 커널 초기화 코드와 예외 처리를 분석할 수 있습니다.

x86-64 시스템 레지스터 구조 CR0 — 시스템 제어 bit 0 (PE): Protected Mode Enable bit 1 (MP): Monitor Coprocessor bit 2 (EM): FPU Emulation bit 3 (TS): Task Switched (FPU lazy) bit 16 (WP): Write Protect bit 18 (AM): Alignment Mask bit 29 (NW): Not Write-through bit 30 (CD): Cache Disable bit 31 (PG): Paging Enable CR3 — 페이지 테이블 기준 bit 3 (PWT): Page Write-Through bit 4 (PCD): Page Cache Disable bit 12-51: PML4/PML5 물리 주소 PCID 활성화 시 (CR4.PCIDE=1): bit 0-11: PCID (Process Context ID) CR4 — 기능 활성화 bit 4 (PSE): Page Size Ext (4MB) bit 5 (PAE): Physical Addr Ext bit 7 (PGE): Page Global Enable bit 9 (OSFXSR): SSE 지원 bit 10 (OSXMMEXCPT): SSE 예외 bit 13 (VMXE): VMX Enable bit 14 (SMXE): SMX Enable bit 16 (FSGSBASE): RD/WR FS/GS bit 17 (PCIDE): PCID Enable bit 18 (OSXSAVE): XSAVE Enable bit 20 (SMEP): 커널의 유저코드실행차단 bit 21 (SMAP): 커널의 유저메모리접근차단 bit 23 (CET): Control-flow Enforce bit 12 (LA57): 5-Level Paging EFER (MSR 0xC0000080) bit 0 (SCE): SYSCALL Enable bit 8 (LME): Long Mode Enable bit 10 (LMA): Long Mode Active (R/O) bit 11 (NXE): No-Execute Enable Long Mode 전환: LME=1 → CR0.PG=1 → LMA=1 주요 MSR: 커널 핵심 MSR 레지스터 IA32_STAR (0xC0000081) SYSCALL 세그먼트 셀렉터 IA32_LSTAR (0xC0000082) SYSCALL 진입점 (RIP) IA32_FMASK (0xC0000084) SYSCALL RFLAGS 마스크 IA32_GS_BASE (0xC0000101) GS 세그먼트 베이스 IA32_KERNEL_GS (0xC0000102) SWAPGS용 커널 GS IA32_SPEC_CTRL (0x48) Spectre 완화 제어 IA32_PRED_CMD (0x49) IBPB 트리거 IA32_APIC_BASE (0x1B) LAPIC 기준 주소 IA32_PAT (0x277) Page Attribute Table IA32_PERF_CTL (0x199) P-state 제어 IA32_TSC_DEADLINE (0x6E0) TSC 데드라인 타이머 IA32_PMCx / IA32_PERFEVTSELx 성능 카운터 IA32_MTRR* (0x200+) 메모리 타입 범위 IA32_VMX_* (0x480+) VMX 제어 정보 rdmsr: ECX=MSR번호 → EDX:EAX=값 | wrmsr: ECX=MSR번호, EDX:EAX=값

디버그 레지스터 (DR0-DR7)

레지스터용도커널 사용
DR0-DR3하드웨어 브레이크포인트 주소 (4개)ptrace(PTRACE_POKEUSER) — perf hw_breakpoint
DR6디버그 상태 (어떤 BP가 트리거됐는지)#DB 예외 핸들러에서 읽기
DR7디버그 제어 (활성화, 조건, 길이)bit 0-7: BP 활성화, bit 16-31: 조건/길이
/* DR7 비트 필드 */
/* bit 0,2,4,6 (L0-L3): 로컬 BP 활성화 */
/* bit 1,3,5,7 (G0-G3): 글로벌 BP 활성화 */
/* bit 16-17 (R/W0): DR0 조건 (00=실행, 01=쓰기, 10=I/O, 11=읽기/쓰기) */
/* bit 18-19 (LEN0): DR0 길이 (00=1B, 01=2B, 10=8B, 11=4B) */
/* bit 20-23: DR1 조건/길이, bit 24-27: DR2, bit 28-31: DR3 */

/* 커널 hw_breakpoint 사용 예 */
/* perf_event_create_kernel_counter() → arch_install_hw_breakpoint() */
/* → set_debugreg(addr, 0)  // DR0에 주소 설정 */
/* → set_debugreg(ctrl, 7)  // DR7에 조건 설정 */

성능 모니터링 카운터 (PMC)

MSR이름용도
0x186-0x189IA32_PERFEVTSELx이벤트 선택 (EventSelect, UMask, USR, OS, EN)
0xC1-0xC4IA32_PMCx카운터 값 (48비트)
0x38DIA32_FIXED_CTR_CTRL고정 카운터 제어
0x309-0x30BIA32_FIXED_CTRx고정 카운터 (inst_retired, cpu_clk, ref_clk)
0x38FIA32_PERF_GLOBAL_CTRL전체 카운터 활성화
0x390IA32_PERF_GLOBAL_STATUS오버플로 상태 (NMI 트리거)
/* PERFEVTSELx 비트 필드 */
/* bit 0-7   : EventSelect (이벤트 코드) */
/* bit 8-15  : UMask (이벤트 세분류) */
/* bit 16    : USR (유저 모드 카운트) */
/* bit 17    : OS (커널 모드 카운트) */
/* bit 20    : INT (오버플로 시 인터럽트/NMI) */
/* bit 22    : EN (카운터 활성화) */
/* bit 23    : INV (조건 반전) */
/* bit 24-31 : CMASK (카운트 마스크) */

/* 커널 perf 서브시스템 사용 흐름 */
/* perf_event_open() → x86_pmu_hw_config() */
/* → wrmsrl(MSR_P6_EVNTSEL0, config) → wrmsrl(MSR_P6_PERFCTR0, 0) */
/* 카운터 오버플로 → NMI → intel_pmu_handle_irq() → 샘플 저장 */

가상화(Virtualization) 확장 (VMX) 레지스터

구분설명커널 사용
VMCSVirtual Machine Control Structure (4KB)KVM: vmcs_write*()
VMXON/VMXOFFVMX 모드 진입/탈출kvm_cpu_vmxon()
VMLAUNCH/VMRESUME게스트 실행 시작/재개vmx_vcpu_run()
VMPTRLD/VMPTRST현재 VMCS 로드/저장vCPU 스위칭 시
VMREAD/VMWRITEVMCS 필드 읽기/쓰기게스트 레지스터, 제어 필드
EPTExtended Page Table (2단계 주소 변환(Address Translation))게스트 물리→호스트 물리
VPIDVirtual Processor ID (TLB 태깅)VM 전환 시 TLB 보존
ℹ️

MTRR과 PAT: IA32_MTRR* MSR은 물리 메모리(Physical Memory) 범위별 캐시 정책(UC/WC/WT/WB/WP)을 설정합니다. IA32_PAT는 페이지 테이블 엔트리의 PWT/PCD/PAT 비트 조합으로 8가지 메모리 타입 중 하나를 선택합니다. 커널은 memtype_reserve()로 디바이스 메모리의 MTRR/PAT 정합성을 관리합니다.

SIMD/벡터 확장

x86-64의 SIMD 확장은 SSE → AVX → AVX-512 → AMX로 발전해왔습니다. 커널은 암호화(AES-NI), 체크섬(CRC32c), RAID(XOR/P+Q), 문자열 연산에서 SIMD를 활용합니다. 각 세대의 레지스터 폭, 명령어 형식, 커널 사용 패턴을 이해해야 합니다.

x86 SIMD 확장 세대별 비교 SSE/SSE2-4.2 XMM0-15 (128비트) 4×float / 2×double 16×byte / 8×word SSE4.2: CRC32, PCMPSTR AES-NI: AESENC/AESDEC 접두사: 66/F2/F3 + 0F 2-오퍼랜드 (파괴적) 커널: CRC32c, AES-NI AVX / AVX2 YMM0-15 (256비트) 8×float / 4×double 32×byte / 16×word AVX2: 정수 256비트 FMA: 융합 곱셈-덧셈 VEX 접두사 (C4/C5) 3-오퍼랜드 (비파괴적) 커널: RAID6, SHA AVX-512 ZMM0-31 (512비트) 16×float / 8×double 64×byte / 32×word k0-k7 마스크 레지스터 32개 벡터 레지스터 EVEX 접두사 (4바이트) 마스크+머지/제로링 커널: RAID6, 암호화 AMX (12세대+) TMM0-7 (타일 8KB) 16×1024 행렬 타일 BF16/INT8 연산 TDPBF16PS: 타일 곱 TILELOADD/TILESTORED TILECFG: 타일 구성 XCR0 bit 17-18 AI/ML 워크로드 커널 SIMD 활용 상세 암호화 (crypto/) AES-NI: aesenc/aesenclast — arch/x86/crypto/aesni-intel_asm.S SHA-NI: sha256rnds2 — arch/x86/crypto/sha256_ni_asm.S PCLMULQDQ: GCM 갈루아 곱셈 — arch/x86/crypto/ghash-clmulni-intel_asm.S ChaCha20: AVX2/AVX-512 — arch/x86/crypto/chacha-avx2-x86_64.S RAID (lib/raid6/) SSE: raid6_sse2x2_gen_syndrome() — XOR + GF(2^8) 곱셈 AVX2: raid6_avx2x2_gen_syndrome() — 256비트 병렬 P+Q AVX-512: raid6_avx512x2_gen_syndrome() — 512비트 최대 처리 체크섬 CRC32c: crc32c-intel (SSE4.2 crc32 명령어) IP 체크섬: csum_partial() SSE2 최적화 문자열 연산 memcpy: REP MOVSB (ERMS/FSRM) memset: REP STOSB / AVX non-temporal clear_page: REP STOSQ (64비트 fill) FPU 상태 관리 kernel_fpu_begin() → XSAVEOPT → SIMD 사용 kernel_fpu_end() → XRSTOR → preempt_enable() ⚠ IRQ/softirq에서 사용 불가 (sleep 가능)
세대레지스터레지스터 수인코딩커널 주요 용도
SSEXMM128비트16레거시 (66/F2/F3)AES-NI, CRC32c
AVXYMM256비트16VEX (C4/C5)RAID6, SHA
AVX-512ZMM+k512비트32+8EVEX (62)RAID6, ChaCha20
AMXTMM8KB 타일8VEX유저공간 AI/ML
⚠️

AVX-512 주파수 감소: AVX-512 사용 시 일부 CPU(Skylake 서버 등)에서 코어 주파수가 낮아집니다(License Level 1→2→3). 커널의 RAID6는 이를 고려하여 raid6_avx512x2_gen_syndrome()이 항상 최적은 아닐 수 있으며, lib/raid6/algos.c에서 부팅 시 벤치마크로 최적 구현을 선택합니다.

System V AMD64 ABI 호출 규약

System V AMD64 ABI는 리눅스, BSD, macOS 등 대부분의 Unix 계열 운영체제에서 사용하는 x86_64 호출 규약입니다. 커널과 유저 공간 모두 이 ABI를 따르며, 함수 인자 전달, 반환값, 스택 정렬, callee-saved 레지스터 규칙을 정의합니다.

역할레지스터설명
1번째 인자RDI정수/포인터 인자 1
2번째 인자RSI정수/포인터 인자 2
3번째 인자RDX정수/포인터 인자 3
4번째 인자RCX정수/포인터 인자 4
5번째 인자R8정수/포인터 인자 5
6번째 인자R9정수/포인터 인자 6
반환값RAX (+ RDX)128-bit 반환 시 RDX:RAX
Caller-savedRAX, RCX, RDX, RSI, RDI, R8-R11함수 호출 시 보존 불보장
Callee-savedRBX, RBP, R12-R15호출된 함수가 반드시 보존
스택 포인터RSP16-byte 정렬 필수 (CALL 전)
FP 인자 1-8XMM0-XMM7부동소수점/SSE 인자

레지스터 할당 다이어그램

System V AMD64 ABI에서 정수 인자 6개와 부동소수점 인자 8개는 레지스터로 전달되고, 초과분은 스택에 push됩니다. 반환값은 RAX(정수)와 XMM0(부동소수점)을 사용합니다.

System V AMD64 ABI 레지스터 할당 System V AMD64 ABI — 레지스터 역할 분류 인자 전달 레지스터 (Caller 설정) arg1 RDI arg2 RSI arg3 RDX arg4 RCX arg5 R8 arg6 R9 arg7+ → 스택 (RSP+8, RSP+16, ...) 스택 (오른쪽→왼쪽 push 순서) Callee-saved (호출된 함수가 보존) RBX RBP R12 R13 R14 R15 반환: RAX (+ RDX for 128-bit) 부동소수점 반환: XMM0 (+ XMM1) Red Zone (RSP 아래 128바이트) 리프 함수가 스택 조정 없이 사용 가능 — 커널에서는 -mno-red-zone으로 비활성화 (인터럽트 보호) RSP-128 ~ RSP-1: 시그널/인터럽트가 이 영역을 덮어쓸 수 있으므로 커널 금지 스택 정렬: CALL 직전 RSP ≡ 0 (mod 16) CALL이 return address(8바이트)를 push → 함수 진입 시 RSP ≡ 8 (mod 16) → PUSH RBP로 16-byte 복원

C → 어셈블리 매핑(Mapping)

C 함수와 그에 대응하는 어셈블리 코드를 비교하면 ABI 규약이 어떻게 적용되는지 명확히 보입니다.

/* C 소스 */
long example(long a, long b, long c, long d, long e, long f) {
    return a + b + c + d + e + f;
}

/* GCC -O2 출력 (AT&T 문법) */
example:
    leaq    (%rdi,%rsi), %rax    # a(RDI) + b(RSI) → RAX
    addq    %rdx, %rax           # + c(RDX)
    addq    %rcx, %rax           # + d(RCX)
    addq    %r8, %rax            # + e(R8)
    addq    %r9, %rax            # + f(R9)
    retq                         # RAX에 결과 반환

Red Zone 사용 예

리프 함수(다른 함수를 호출하지 않는 함수)는 Red Zone을 활용해 RSP 조정 없이 로컬 변수를 저장할 수 있습니다. 커널에서는 -mno-red-zone으로 비활성화합니다.

/* Red Zone을 사용하는 리프 함수 (유저 공간) */
leaf_func:
    # RSP를 조정하지 않고 Red Zone에 저장
    movq    %rbx, -8(%rsp)     # callee-saved 백업 (Red Zone)
    movq    %rdi, %rbx           # 인자를 RBX에 저장
    movq    %rbx, %rax           # 반환값 설정
    movq    -8(%rsp), %rbx     # callee-saved 복원
    retq

/* 커널에서는 인터럽트가 RSP 아래를 덮어쓰므로
   arch/x86/Makefile: KBUILD_CFLAGS += -mno-red-zone */

16-byte 스택 정렬

ABI는 CALL 직전에 RSP가 16-byte 경계에 정렬되어야 합니다. SSE 명령어(MOVAPS 등)는 정렬되지 않은 접근 시 #GP 예외를 발생시킵니다.

/* 스택 정렬 예시: 7개 인자 함수 호출 */
caller:
    subq    $8, %rsp           # 16-byte 정렬 패딩
    pushq   $42                 # 7번째 인자 (스택)
    movq    $6, %r9            # 6번째 인자
    movq    $5, %r8            # 5번째 인자
    movq    $4, %rcx           # 4번째 인자
    movq    $3, %rdx           # 3번째 인자
    movq    $2, %rsi           # 2번째 인자
    movq    $1, %rdi           # 1번째 인자
    callq   target_func        # CALL이 return addr push → RSP ≡ 0 mod 16
    addq    $16, %rsp          # 스택 정리 (8 패딩 + 8 인자)
    retq
커널 ABI 차이: 시스템 콜은 유저 공간 ABI와 인자 매핑이 다릅니다. 유저 공간에서는 RDI, RSI, RDX, RCX, R8, R9 순서이지만, SYSCALL 명령어가 RCX를 덮어쓰므로 시스템 콜 4번째 인자는 R10을 사용합니다.

4/5-레벨 페이징 워크

x86_64의 가상 주소 변환은 CR3 레지스터에서 시작하여 계층적 페이지 테이블을 순회합니다. 기본 4-레벨 페이징은 48-bit 가상 주소(256TB)를 지원하며, 5-레벨 페이징(LA57)은 57-bit 가상 주소(128PB)까지 확장합니다.

레벨구조체(Struct)VA 비트 범위커버 크기커널 매크로
PML5 (5-레벨만)pgd_t → p4d_t[56:48]256TB / 엔트리pgd_offset()
PML4p4d_t → pud_t[47:39]512GB / 엔트리p4d_offset()
PDPTpud_t → pmd_t[38:30]1GB / 엔트리pud_offset()
PDpmd_t → pte_t[29:21]2MB / 엔트리pmd_offset()
PTpte_t[20:12]4KB / 엔트리pte_offset_kernel()
Offset[11:0]4KB 페이지 내 오프셋

4/5-레벨 페이징 워크 다이어그램

가상 주소의 각 9-bit 필드가 해당 테이블의 인덱스로 사용되어, CR3에서 최종 물리 주소까지 단계적으로 변환됩니다.

x86_64 4/5-레벨 페이징 워크 4-Level Page Table Walk (48-bit VA) VA [63:48] sign-ext PML4 [47:39] PDPT [38:30] PD [29:21] PT [20:12] Off [11:0] CR3 PML4 Table 512 entries × 8B [47:39] → index PDPT 512 entries × 8B [38:30] → index Page Dir 512 entries × 8B [29:21] → index PT 512 × 8B [20:12]→idx Physical Addr 5-Level Paging (LA57) — 57-bit VA 확장 [63:57] sign PML5 [56:48] PML4 [47:39] PDPT [38:30] PD [29:21] PT [20:12] Offset [11:0] CR3→PML5 PML4 PDPT PD PT Physical Page + Off PTE 주요 비트: P(0) Present | R/W(1) Read/Write | U/S(2) User/Supervisor | PWT(3) | PCD(4) A(5) Accessed | D(6) Dirty | PS(7) Page Size | G(8) Global | NX(63) No-Execute Huge Pages: PDPT entry PS=1 → 1GB page (PD/PT 스킵) | PD entry PS=1 → 2MB page (PT 스킵)

CR3 레지스터 포맷

CR3은 최상위 페이지 테이블의 물리 주소를 가리키며, PCID(Process Context ID)를 포함하여 TLB 플러시를 최적화합니다.

/* CR3 레지스터 포맷 (PCID 활성 시) */
/*
 * Bit 63:    PCID 무효화 제어 (INVPCID)
 *            0 = CR3 로드 시 PCID에 해당하는 TLB 엔트리 무효화
 *            1 = TLB 무효화 생략 (noflush)
 * Bit 62:12  PML4/PML5 물리 주소 (4KB 정렬)
 * Bit 11:0   PCID (12-bit, 최대 4096 프로세스 구분)
 */

/* arch/x86/include/asm/tlbflush.h */
#define X86_CR3_PCID_NOFLUSH   (1UL << 63)  /* TLB 무효화 생략 */
#define X86_CR3_PCID_MASK      0xFFF           /* PCID 12-bit */

/* KPTI: 커널/유저 페이지 테이블 분리 시 CR3 전환 */
/* 유저 PGD = 커널 PGD + PAGE_SIZE (연속 배치) */
#define PTI_USER_PGTABLE_BIT   PAGE_SHIFT
#define PTI_USER_PGTABLE_MASK  (1 << PTI_USER_PGTABLE_BIT)

커널 페이지 테이블 매크로

커널은 5단계 추상화 매크로를 사용하여 4-레벨과 5-레벨 페이징을 통합 관리합니다. 4-레벨 시스템에서는 p4d 레벨이 fold되어 PGD와 동일하게 동작합니다.

/* arch/x86/include/asm/pgtable.h — 페이지 테이블 워크 매크로 */

/* 1단계: PGD (= PML5 or PML4) */
pgd_t *pgd = pgd_offset(mm, addr);     /* mm→pgd + pgd_index(addr) */

/* 2단계: P4D (5-레벨에서만 실제 테이블, 4-레벨에서는 fold) */
p4d_t *p4d = p4d_offset(pgd, addr);     /* 4-레벨: (p4d_t *)pgd */

/* 3단계: PUD (= PDPT) */
pud_t *pud = pud_offset(p4d, addr);     /* p4d_page_vaddr(*p4d) + pud_index(addr) */

/* 4단계: PMD (= PD) */
pmd_t *pmd = pmd_offset(pud, addr);     /* pud_page_vaddr(*pud) + pmd_index(addr) */

/* 5단계: PTE (= PT entry) */
pte_t *pte = pte_offset_kernel(pmd, addr);

/* Huge page 확인 */
if (pud_large(*pud))    /* 1GB huge page (PS 비트) */
if (pmd_large(*pmd))    /* 2MB huge page (PS 비트) */

/* 5-레벨 감지 */
if (pgtable_l5_enabled())
    /* CR4.LA57 = 1, PML5 활성 */
TLB 최적화: PCID를 활용하면 컨텍스트 전환 시 TLB 전체 플러시를 피할 수 있습니다. 커널은 cpu_tlbstate에서 최대 6개 PCID를 라운드 로빈(Round Robin)으로 할당하며, INVPCID 명령어로 특정 PCID의 엔트리만 선택적으로 무효화합니다.

커널 어셈블리 실전 예제

리눅스 커널은 성능이 중요하거나 하드웨어 접근이 필요한 경로에서 직접 어셈블리를 사용합니다. 시스템 콜 진입점(Entry Point), 컨텍스트 전환, 원자적 연산(Atomic Operation), 유저 공간 데이터 복사 등이 대표적입니다.

시스템 콜 진입점 (entry_SYSCALL_64)

SYSCALL 명령어가 실행되면 프로세서는 Ring 0으로 전환하고 LSTAR MSR이 가리키는 entry_SYSCALL_64로 점프합니다. 이 코드는 유저 레지스터를 저장하고 커널 스택으로 전환합니다.

/* arch/x86/entry/entry_64.S — 시스템 콜 진입 */
SYM_CODE_START(entry_SYSCALL_64)
    swapgs                               # GS 베이스를 커널 per-CPU로 전환
    movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
                                          # 유저 RSP를 TSS에 임시 저장
    SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp # KPTI: 커널 페이지 테이블로 전환
    movq    PER_CPU_VAR(pcpu_hot + X86_top_of_stack), %rsp
                                          # 커널 스택 포인터 로드

    /* pt_regs 구조체에 레지스터 저장 */
    pushq   $__USER_DS                    # pt_regs→ss
    pushq   PER_CPU_VAR(cpu_tss_rw + TSS_sp2)  # pt_regs→sp (유저 RSP)
    pushq   %r11                          # pt_regs→flags (SYSCALL이 저장한 RFLAGS)
    pushq   $__USER_CS                    # pt_regs→cs
    pushq   %rcx                          # pt_regs→ip (SYSCALL이 저장한 RIP)
    pushq   %rax                          # pt_regs→orig_ax (시스템 콜 번호)

    PUSH_AND_CLEAR_REGS rax=$-ENOSYS     # 나머지 레지스터 저장 + 클리어
    movq    %rsp, %rdi                    # pt_regs 포인터를 1번째 인자로
    callq   do_syscall_64                # C 핸들러 호출

컨텍스트 전환 (__switch_to_asm)

스케줄러(Scheduler)가 태스크(Task)를 전환할 때 callee-saved 레지스터와 스택 포인터를 교체합니다.

/* arch/x86/entry/entry_64.S — 컨텍스트 전환 */
SYM_FUNC_START(__switch_to_asm)
    /* callee-saved 레지스터 저장 (prev 태스크) */
    pushq   %rbp
    pushq   %rbx
    pushq   %r12
    pushq   %r13
    pushq   %r14
    pushq   %r15

    /* 스택 포인터 교체 */
    movq    %rsp, TASK_threadsp(%rdi)     # prev→thread.sp = RSP
    movq    TASK_threadsp(%rsi), %rsp     # RSP = next→thread.sp

    /* STACKLEAK: 스택 카나리 포이즌 */
#ifdef CONFIG_STACKPROTECTOR
    movq    TASK_stack_canary(%rsi), %rbx
    movq    %rbx, PER_CPU_VAR(fixed_percpu_data) + FIXED_stack_canary
#endif

    /* callee-saved 레지스터 복원 (next 태스크) */
    popq    %r15
    popq    %r14
    popq    %r13
    popq    %r12
    popq    %rbx
    popq    %rbp
    jmp     __switch_to                  # C 함수로 FPU, TLS 등 나머지 전환
SYM_FUNC_END(__switch_to_asm)

LOCK CMPXCHG 원자적 연산

커널의 atomic_cmpxchg()LOCK CMPXCHG로 구현되어 멀티코어 환경에서 원자적 비교-교환을 수행합니다.

/* arch/x86/include/asm/cmpxchg.h — atomic compare-and-exchange */
/*
 * CMPXCHG: if (*ptr == old) { *ptr = new; ZF=1; } else { EAX = *ptr; ZF=0; }
 * LOCK 접두사: 버스 락/캐시 락으로 원자성 보장
 */
static __always_inline unsigned long
__cmpxchg(volatile void *ptr, unsigned long old, unsigned long new, int size)
{
    unsigned long prev;
    switch (size) {
    case 8:
        asm volatile("lock; cmpxchgq %1, %2"
                     : "=a"(prev)                /* output: RAX = prev */
                     : "r"(new), "m"(*((u64 *)ptr)),
                       "0"(old)                  /* input: RAX = old */
                     : "memory");
        return prev;
    }
}

copy_user_generic 유저 메모리 복사

유저 공간 데이터를 커널로 복사할 때는 페이지 폴트(Page Fault)가 발생할 수 있어 _ASM_EXTABLE로 예외 처리를 등록합니다.

/* arch/x86/lib/copy_user_64.S — 유저 메모리 복사 (REP MOVSB) */
SYM_FUNC_START(copy_user_generic_unrolled)
    ASM_STAC                              # SMAP 해제 (유저 메모리 접근 허용)
    movq    %rdx, %rcx                   # count → RCX
1:  rep movsb                            # RSI(src)→RDI(dst), RCX 바이트
    ASM_CLAC                              # SMAP 복원
    xorl    %eax, %eax                   # 성공: 잔여 바이트 0 반환
    RET

/* 페이지 폴트 발생 시 복구 핸들러 */
.Lcopy_user_fault:
    movq    %rcx, %rax                   # 복사하지 못한 잔여 바이트 반환
    ASM_CLAC
    RET

    /* 예외 테이블: 1:에서 폴트 → .Lcopy_user_fault로 점프 */
    _ASM_EXTABLE_UA(1b, .Lcopy_user_fault)
SYM_FUNC_END(copy_user_generic_unrolled)

Per-CPU 변수 접근

x86_64에서 per-CPU 변수는 GS 세그먼트를 통해 접근합니다. 커널 모드에서 GS 베이스는 MSR_GS_BASE에 per-CPU 영역 시작 주소를 저장합니다.

/* arch/x86/include/asm/percpu.h — Per-CPU 접근 */
/* GS 세그먼트 베이스 = per-CPU 영역 시작 주소
 * __per_cpu_offset[cpu]에 해당하는 값이 GS 베이스로 설정됨
 * 변수 접근 시 %gs:offset 으로 직접 메모리 참조 */

#define this_cpu_read_8(pcp) ({              \
    u64 val;                                  \
    asm volatile("movq %%gs:%1, %0"           \
                 : "=r"(val)                   \
                 : "m"(pcp));                   \
    val; })

#define this_cpu_inc_8(pcp)                   \
    asm volatile("incq %%gs:%0"                \
                 : "+m"(pcp))

/* 사용 예: current 매크로 (현재 태스크 포인터) */
#define current this_cpu_read_stable(pcpu_hot.current_task)

ALTERNATIVE 매크로 (조건부 패칭)

CPU 기능에 따라 부팅 시 코드를 동적으로 패칭합니다. 기본 코드와 대체 코드를 함께 배치하고, 런타임에 CPU 기능을 확인하여 패칭합니다.

/* arch/x86/include/asm/alternative.h — ALTERNATIVE 매크로 */
/*
 * 기본 코드(oldinstr)를 배치하고, 대체 코드(newinstr)는
 * .altinstructions 섹션에 메타데이터와 함께 기록.
 * 부팅 시 apply_alternatives()가 CPU feature 확인 후
 * 기본 코드를 대체 코드로 덮어씀.
 */

/* 예: SWAPGS 대신 WRGSBASE 사용 (FSGSBASE 지원 시) */
ALTERNATIVE "swapgs",                       \
            "wrgsbase %rax",                 \
            X86_FEATURE_FSGSBASE

/* 예: REP MOVSB 대신 ERMS 버전 사용 */
ALTERNATIVE "rep movsb",                     \
            "call enhanced_rep_movsb",        \
            X86_FEATURE_ERMS

/* 이중 대체: feature1 없으면 기본, feature1이면 alt1, feature2면 alt2 */
ALTERNATIVE_2 "mfence; lfence",              \
              "lfence", X86_FEATURE_LFENCE_RDTSC, \
              "nop",    X86_FEATURE_SERIALIZE
주의: 커널 어셈블리에서는 RET 매크로를 사용하며, 이는 Spectre v2 완화를 위한 retpoline 코드로 대체될 수 있습니다. 직접 retq를 쓰면 보안 취약점이 될 수 있습니다.

세그멘테이션과 GDT/TSS

x86_64 Long Mode에서 세그멘테이션은 대부분 비활성화되지만, GDT(Global Descriptor Table)는 여전히 필수입니다. 커널/유저 코드 세그먼트의 특권 레벨, TSS(Task State Segment)의 스택 포인터, SYSCALL/SYSRET의 MSR 설정 등에서 세그먼트 디스크립터가 사용됩니다.

GDT 엔트리셀렉터DPL용도
NULL0x00필수 NULL 디스크립터
__KERNEL_CS0x100커널 코드 세그먼트 (CS.L=1)
__KERNEL_DS0x180커널 데이터 세그먼트
__USER_CS (32)0x233유저 32-bit 코드 (Compat Mode)
__USER_DS0x2B3유저 데이터 세그먼트
__USER_CS0x333유저 64-bit 코드 세그먼트
TSS0x400태스크 상태 세그먼트 (16바이트)
Per-CPU0GS 베이스 (per-CPU 데이터)

GDT 레이아웃 다이어그램

GDT는 per-CPU로 할당되며, 커널/유저 세그먼트와 TSS를 포함합니다. Long Mode에서 세그먼트의 base/limit은 무시되지만(FS/GS 제외), DPL과 타입 필드는 여전히 검증됩니다.

GDT 레이아웃과 세그먼트 디스크립터 포맷 Per-CPU GDT 레이아웃 (x86_64 Long Mode) 0x00: NULL Descriptor 0x10: __KERNEL_CS (DPL=0, L=1, Code) 0x18: __KERNEL_DS (DPL=0, Data) 0x23: __USER_CS32 (DPL=3, Compat) 0x2B: __USER_DS (DPL=3, Data) 0x33: __USER_CS (DPL=3, L=1, Code) 0x40: TSS (16-byte System Descriptor) IST 스택 포인터, IO 비트맵 포함 64-bit 코드 세그먼트 디스크립터 (8바이트) 비트 [63:56] Base[31:24] (Long Mode: 무시) 비트 [55:52] G=1 | D=0 | L=1 | AVL (L=1 → 64-bit mode) 비트 [47:44] P=1 | DPL[1:0] | S=1 (DPL: 특권 레벨 0 or 3) 비트 [43:40] Type: 1011 (Code, R, A) 비트 [39:0] Base/Limit (Long Mode: 대부분 무시) SYSCALL/SYSRET MSR 구성 STAR (MSR 0xC0000081): [63:48]=SYSRET CS/SS 베이스, [47:32]=SYSCALL CS/SS 베이스 LSTAR (MSR 0xC0000082): SYSCALL 진입점 RIP (→ entry_SYSCALL_64 주소) SFMASK (MSR 0xC0000084): SYSCALL 시 클리어할 RFLAGS 비트 (IF, TF, DF, AC 등) SYSCALL: CS=STAR[47:32], SS=STAR[47:32]+8 | SYSRET: CS=STAR[63:48]+16, SS=STAR[63:48]+8

GDT 초기화

부팅 초기에 per-CPU GDT를 설정하고 LGDT로 프로세서에 로드합니다.

/* arch/x86/kernel/cpu/common.c — GDT 초기화 */
DEFINE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page) = { .gdt = {
    [GDT_ENTRY_KERNEL_CS]       = GDT_ENTRY_INIT(0xa09b, 0, 0xfffff),
    /* flags: G=1, L=1, P=1, DPL=0, S=1, Type=Code R/A */
    [GDT_ENTRY_KERNEL_DS]       = GDT_ENTRY_INIT(0xc093, 0, 0xfffff),
    [GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(0xa0fb, 0, 0xfffff),
    /* flags: G=1, L=1, P=1, DPL=3, S=1, Type=Code R/A */
    [GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(0xc0f3, 0, 0xfffff),
}};

/* arch/x86/kernel/cpu/common.c — LGDT 로드 */
static inline void load_gdt(const struct desc_ptr *dtr)
{
    asm volatile("lgdt %0" :: "m"(*dtr));
}

TSS 설정

TSS는 Long Mode에서 IST(Interrupt Stack Table)와 IO 비트맵(Bitmap)을 제공합니다. 특권 레벨 전환 시 사용할 커널 스택을 지정합니다.

/* arch/x86/include/asm/processor.h — TSS 구조체 */
struct tss_struct {
    struct x86_hw_tss      x86_tss;       /* 하드웨어 TSS */
    struct x86_io_bitmap   io_bitmap;     /* IO 포트 비트맵 */
} __aligned(PAGE_SIZE);

struct x86_hw_tss {
    u32     reserved1;
    u64     sp0;           /* Ring 3→0 전환 시 커널 스택 */
    u64     sp1;           /* Ring 1 스택 (미사용) */
    u64     sp2;           /* Ring 2 스택 (SYSCALL 임시 저장용) */
    u64     reserved2;
    u64     ist[7];       /* IST 1~7: 전용 인터럽트 스택 */
    /* IST1: #DF (Double Fault)
     * IST2: #NMI
     * IST3: #DB (Debug)
     * IST4: #MC (Machine Check) */
    u32     reserved3;
    u16     reserved4;
    u16     io_bitmap_base;  /* IO 비트맵 오프셋 */
} __packed;

SYSCALL/SYSRET MSR 설정

SYSCALL/SYSRET 명령어의 동작을 제어하는 MSR을 부팅 시 초기화합니다.

/* arch/x86/kernel/cpu/common.c — SYSCALL MSR 초기화 */
void syscall_init(void)
{
    /* STAR: SYSCALL/SYSRET CS/SS 베이스 */
    wrmsrl(MSR_STAR,
           ((u64)__USER32_CS << 48) |    /* SYSRET CS base (유저 복귀) */
           ((u64)__KERNEL_CS << 32));    /* SYSCALL CS base (커널 진입) */

    /* LSTAR: SYSCALL 진입점 */
    wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

    /* SFMASK: SYSCALL 시 클리어할 RFLAGS 비트
     * IF=0 (인터럽트 비활성), TF=0 (트레이스 비활성),
     * DF=0 (방향 플래그 클리어), AC=0 (SMAP 활성 유지) */
    wrmsrl(MSR_SYSCALL_MASK,
           X86_EFLAGS_TF | X86_EFLAGS_DF | X86_EFLAGS_IF |
           X86_EFLAGS_IOPL | X86_EFLAGS_AC | X86_EFLAGS_NT);
}
SYSRET 취약점: Intel CPU에서 비정규(non-canonical) RIP로 SYSRET이 실행되면 #GP가 Ring 0에서 발생하여 보안 취약점(CVE-2014-4699)이 됩니다. Linux 커널은 SYSRET 전에 RIP가 정규 주소인지 검증합니다.

인터럽트/예외 처리 흐름

x86_64에서 인터럽트와 예외는 IDT(Interrupt Descriptor Table)를 통해 처리됩니다. 프로세서는 인터럽트 벡터 번호로 IDT를 색인하여 게이트 디스크립터에서 핸들러 주소를 가져옵니다. 예외(0~31번)는 동기적이고, 외부 인터럽트(32~255번)는 APIC을 통해 비동기적으로 전달됩니다.

벡터이름타입IST설명
0#DEFaultDivision Error
1#DBFault/TrapIST3Debug (하드웨어 브레이크포인트)
2NMIInterruptIST2Non-Maskable Interrupt
3#BPTrapBreakpoint (INT3)
6#UDFaultInvalid Opcode
8#DFAbortIST1Double Fault
13#GPFaultGeneral Protection
14#PFFaultPage Fault (에러코드: P/W/U/R/I)
18#MCAbortIST4Machine Check
32~255IRQInterrupt외부 장치 인터럽트 (APIC)

인터럽트 처리 흐름 다이어그램

하드웨어 인터럽트가 도착하면 Local APIC → IDT → 핸들러 체인 → EOI 순서로 처리됩니다.

x86_64 인터럽트 처리 흐름 인터럽트 처리 흐름 (IDT → Handler → EOI) Device I/O APIC Local APIC 벡터 → CPU CPU: IDT Lookup IDTR → gate[vector] IST/DPL 확인, 스택 전환 asm_common_interrupt PUSH_REGS, SWAPGS(유저), SWITCH_TO_KERNEL_CR3 common_interrupt() → __handle_irq_event_percpu() irqaction 체인 순회 → handler(irq, dev_id) APIC EOI (apic_eoi()) irqreturn (복귀 경로) softirq 처리, 스케줄 확인, IRET 프로세서 자동 동작 (인터럽트/예외 진입): 1. SS:RSP 로드 (TSS.sp0 또는 IST) → 2. 이전 SS, RSP, RFLAGS, CS, RIP push 3. 에러 코드 push (#PF, #GP 등) → 4. Gate의 Target CS:RIP로 점프 복귀 경로 체크: softirq 대기? → do_softirq | 리스케줄 필요? → preempt_schedule_irq | 시그널? → do_signal IRETQ: RIP, CS, RFLAGS, RSP, SS 복원 유저 복귀 시: SWITCH_TO_USER_CR3 (KPTI), SWAPGS, IRETQ

IDT 게이트 디스크립터

64-bit IDT 게이트는 16바이트 크기로, 핸들러 주소 64-bit 전체와 IST 인덱스, DPL을 포함합니다.

/* arch/x86/include/asm/desc_defs.h — IDT 게이트 디스크립터 */
struct gate_struct {
    u16     offset_low;     /* 핸들러 주소 [15:0] */
    u16     segment;        /* 코드 세그먼트 셀렉터 (__KERNEL_CS) */
    struct idt_bits bits;
    u16     offset_middle;  /* 핸들러 주소 [31:16] */
    u32     offset_high;    /* 핸들러 주소 [63:32] */
    u32     reserved;
};

struct idt_bits {
    u16     ist  : 3;       /* IST 인덱스 (0=미사용, 1~7=전용 스택) */
    u16     zero : 5;
    u16     type : 4;       /* 0xE=Interrupt Gate, 0xF=Trap Gate */
    u16     dpl  : 2;       /* DPL (0=커널만, 3=유저 INT3 허용) */
    u16     p    : 1;       /* Present */
};
/* Interrupt Gate: IF 자동 클리어 (인터럽트 비활성)
 * Trap Gate: IF 유지 (예외 처리 중 인터럽트 가능) */

인터럽트 진입 어셈블리

모든 인터럽트 벡터는 매크로로 생성된 스텁에서 공통 핸들러로 점프합니다.

/* arch/x86/entry/entry_64.S — 인터럽트 진입 */
SYM_CODE_START(asm_common_interrupt)
    UNWIND_HINT_ENTRY
    ASM_CLAC                              # SMAP: 유저 접근 불가로 설정
    pushq   $-1                           # pt_regs→orig_ax = -1 (인터럽트 표시)
    PUSH_AND_CLEAR_REGS                   # 범용 레지스터 전체 저장
    testb   $3, CS(%rsp)                  # CS의 RPL 확인
    jz      1f                             # 커널 → SWAPGS 불필요
    swapgs                                # 유저에서 왔으면 GS 전환
    SWITCH_TO_KERNEL_CR3 scratch_reg=%rdi # KPTI 커널 CR3
1:  movq    %rsp, %rdi                    # pt_regs → 1번째 인자
    movq    %rsp, %rsi
    callq   common_interrupt              # C 핸들러 호출
    jmp     ret_from_intr                # 복귀 경로
SYM_CODE_END(asm_common_interrupt)

APIC EOI 시퀀스

인터럽트 처리 완료 후 Local APIC에 EOI(End Of Interrupt)를 보내 다음 인터럽트를 수신 가능하게 합니다.

/* arch/x86/kernel/apic/apic.c — APIC EOI */
static inline void apic_eoi(void)
{
    /* EOI 레지스터에 0을 쓰면 가장 높은 우선순위의 ISR 비트 클리어 */
    apic_write(APIC_EOI, 0);
}

/* x2APIC 모드: MSR을 통한 EOI (더 빠름) */
static inline void x2apic_eoi(void)
{
    wrmsrl(APIC_BASE_MSR + (APIC_EOI >> 4), 0);
}

/* 인터럽트 처리 완료 흐름 */
void handle_edge_irq(struct irq_desc *desc)
{
    handle_irq_event(desc);     /* handler 체인 실행 */
    cond_unmask_eoi_irq(desc, chip); /* chip→irq_eoi() → apic_eoi() */
}
IST(Interrupt Stack Table): NMI, Double Fault, Machine Check 등 치명적 예외는 현재 스택이 손상되었을 수 있으므로 IST를 통해 별도 스택에서 처리합니다. IST 스택은 per-CPU로 할당되며 TSS에 주소가 기록됩니다.

CET (IBT/Shadow Stack)

CET(Control-flow Enforcement Technology)는 Intel이 도입한 하드웨어 제어 흐름 무결성(CFI) 기술로, IBT(Indirect Branch Tracking)와 Shadow Stack 두 가지 메커니즘으로 구성됩니다. 리눅스 커널 6.2+에서 IBT, 6.6+에서 유저 공간 Shadow Stack을 지원합니다.

기능메커니즘보호 대상커널 지원
IBT간접 분기 대상에 ENDBR 필수JOP/COP 공격CONFIG_X86_KERNEL_IBT (v6.2)
Shadow Stack별도 스택에 return address 저장ROP 공격CONFIG_X86_USER_SHADOW_STACK (v6.6)

CET 동작 흐름 다이어그램

IBT는 간접 호출 대상이 ENDBR64로 시작하는지 검증하고, Shadow Stack은 CALL/RET 시 반환 주소를 이중 검증합니다.

CET: IBT와 Shadow Stack 동작 흐름 IBT (Indirect Branch Tracking) CALL *%rax 대상 함수 첫 명령어 ENDBR64? YES 실행 계속 NO #CP 예외 Control Protection Exception (벡터 21) Shadow Stack CALL target Return Addr Push 일반 스택 + Shadow Stack (하드웨어가 자동 이중 저장) RET 주소 비교 Stack addr == Shadow addr? 일치 RET 실행 불일치 → #CP 예외 (ROP 탐지) 활성화 제어: CR4.CET=1 (CET 전역 활성) | MSR_IA32_U_CET: Shadow Stack 유저 | MSR_IA32_S_CET: Shadow Stack 커널 | MSR_IA32_PL3_SSP: 유저 SSP IBT: CR4.CET + ENDBR 검증 자동 | Shadow Stack: PTE bit 60 (Dirty=0, Write=1 → Shadow Page 식별)

ENDBR64 어노테이션

IBT가 활성화되면 모든 간접 분기 대상은 ENDBR64로 시작해야 합니다. 커널은 objtool로 검증합니다.

/* arch/x86/include/asm/ibt.h — IBT 어노테이션 */
#ifdef CONFIG_X86_KERNEL_IBT
/* ENDBR64 = F3 0F 1E FA (4바이트 NOP on non-CET CPUs) */
#define __noendbr  __attribute__((nocf_check))

/* 어셈블리에서의 ENDBR */
SYM_FUNC_START(my_function)
    endbr64                  # IBT landing pad — 간접 호출 허용 지점
    pushq   %rbp
    ...

/* C 함수: 컴파일러가 자동으로 ENDBR64 삽입 (-fcf-protection=branch)
 * 함수 포인터로 호출될 수 있는 모든 함수에 필수
 * objtool이 누락된 ENDBR을 감지하여 빌드 경고 */

/* FineIBT (v6.2+): kCFI + IBT 결합
 * 간접 호출 전에 타입 해시 검증 → ENDBR에서 해시 비교
 * JOP 공격에 대한 더 강력한 보호 */
#else
#define __noendbr
#endif

Shadow Stack 설정

Shadow Stack은 CR4.CET와 MSR로 활성화하며, 유저 공간에서 map_shadow_stack 시스템 콜로 Shadow Stack 메모리를 할당합니다.

/* arch/x86/kernel/shstk.c — Shadow Stack 설정 */
int shstk_setup(void)
{
    struct thread_shstk *shstk = &current->thread.shstk;
    unsigned long addr, size;

    /* Shadow Stack 페이지 할당 (VM_SHADOW_STACK 플래그)
     * PTE: Dirty=0, Write=1 조합은 Shadow Stack 전용
     * 일반 소프트웨어가 이 페이지에 쓰기 불가 */
    size = PAGE_ALIGN(min_t(unsigned long, rlimit(RLIMIT_STACK), SZ_4G));
    addr = do_mmap(NULL, 0, size,
                   PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, 0,
                   &unused, NULL);

    set_clr_bits_msrl(MSR_IA32_U_CET, CET_SHSTK_EN, 0);
    wrmsrl(MSR_IA32_PL3_SSP, addr + size);   # 유저 SSP 설정 */
    shstk->base = addr;
    shstk->size = size;
    return 0;
}

/* CR4.CET 활성화 */
cr4_set_bits(X86_CR4_CET);
커널 Shadow Stack: 현재(v6.14) 커널 자체의 Shadow Stack은 비활성 상태입니다. 커널은 retpoline과 IBT/FineIBT로 제어 흐름을 보호합니다. 유저 공간 Shadow Stack만 map_shadow_stack(2)arch_prctl(ARCH_SHSTK_*)을 통해 지원됩니다.

alternatives 패칭

리눅스 커널의 alternatives 시스템은 부팅 시 CPU 기능에 따라 명령어를 동적으로 교체합니다. 기본 코드(모든 CPU에서 동작)와 대체 코드(특정 기능 지원 시 사용)를 함께 빌드하고, apply_alternatives()가 런타임에 패칭합니다.

매크로대체 수용도
ALTERNATIVE1단일 기능 분기
ALTERNATIVE_22이중 기능 분기 (우선순위(Priority): alt2 > alt1)
ALTERNATIVE_TERNARY2feature ? alt1 : alt2
static_cpu_has()1C 코드에서 NOP 패딩(Padding) 분기

ALTERNATIVE 매크로 구조

.altinstructions 섹션에 메타데이터를 기록하고, .altinstr_replacement에 대체 코드를 배치합니다.

/* arch/x86/include/asm/alternative.h — ALTERNATIVE 내부 구조 */
struct alt_instr {
    s32 instr_offset;      /* 원본 명령어 위치 (.text 내) */
    s32 repl_offset;       /* 대체 명령어 위치 (.altinstr_replacement) */
    u16 cpuid;             /* X86_FEATURE_* (CPU 기능 비트) */
    u8  instrlen;          /* 원본 명령어 길이 */
    u8  replacementlen;    /* 대체 명령어 길이 */
};

/* ALTERNATIVE 매크로 확장 예:
 * .section .text
 * 770:  oldinstr            ← 기본 코드 (모든 CPU)
 * 771:  .skip (pad)         ← NOP 패딩 (대체 코드가 더 길 경우)
 *
 * .section .altinstr_replacement
 * 772:  newinstr            ← 대체 코드
 *
 * .section .altinstructions
 *   .long 770b - .          ← instr_offset (상대)
 *   .long 772b - .          ← repl_offset (상대)
 *   .word X86_FEATURE_XXX   ← 필요한 CPU 기능
 *   .byte 771b - 770b       ← instrlen
 *   .byte 773f - 772b       ← replacementlen
 */

apply_alternatives 패칭 과정

부팅 초기에 apply_alternatives().altinstructions 섹션을 순회하며 CPU 기능을 확인하고, 지원되는 기능의 대체 코드로 원본을 덮어씁니다.

/* arch/x86/kernel/alternative.c — 패칭 엔진 */
void __init_or_module apply_alternatives(struct alt_instr *start,
                                         struct alt_instr *end)
{
    struct alt_instr *a;
    u8 *instr, *replacement;

    for (a = start; a < end; a++) {
        instr = (u8 *)&a->instr_offset + a->instr_offset;
        replacement = (u8 *)&a->repl_offset + a->repl_offset;

        if (!boot_cpu_has(a->cpuid))
            continue;                   /* CPU가 해당 기능 미지원 → 스킵 */

        /* text_poke_early(): .text 영역에 직접 쓰기
         * 대체 코드를 원본 위치에 복사
         * 잔여 공간은 NOP으로 채움 */
        text_poke_early(instr, replacement, a->replacementlen);
        add_nops(instr + a->replacementlen,
                 a->instrlen - a->replacementlen);
    }
}

static_cpu_has와 NOP 패딩

C 코드에서 static_cpu_has()는 조건 분기를 NOP으로 패칭하여 분기 예측 오버헤드를 제거합니다.

/* arch/x86/include/asm/cpufeature.h — static_cpu_has */
/* 부팅 시: 해당 기능이 있으면 JMP를 2-byte NOP으로 교체
 *          없으면 JMP를 유지하여 대체 경로로 분기 */

static __always_inline bool _static_cpu_has(u16 bit)
{
    asm goto(
        ALTERNATIVE_TERNARY(
            "jmp %l[t_no]",               /* 기본: no로 점프 */
            %P[cap],                          /* X86_FEATURE_* */
            "",                               /* 기능 있음: NOP (fall-through → true) */
            "jmp %l[t_no]")                  /* 기능 없음: 점프 유지 */
        : : [cap] "i"(bit) : : t_no);
    return true;
t_no:
    return false;
}

/* 사용 예: FSGSBASE 지원 시 RDGSBASE 사용 */
if (static_cpu_has(X86_FEATURE_FSGSBASE))
    gs_base = rdgsbase();           /* 패칭 후: NOP → fall-through */
else
    gs_base = rdmsrl(MSR_GS_BASE);  /* 패칭 전/미지원: MSR 읽기 */
모듈 패칭: 커널 모듈(Kernel Module)도 로드 시 apply_alternatives()가 실행됩니다. 모듈의 .altinstructions 섹션을 순회하여 현재 CPU에 맞게 패칭합니다. 이는 module_finalize()에서 수행됩니다.

마이크로아키텍처 최적화

최신 x86_64 프로세서는 CISC 명령어를 내부적으로 µop(마이크로 연산)으로 분해하여 처리합니다. 프론트엔드(명령어 페치/디코드)와 백엔드(실행/은퇴) 파이프라인의 특성을 이해하면 커널 핫패스의 성능을 최적화할 수 있습니다.

구성 요소Zen 4Golden Cove설명
파이프라인 단계~19단계~20단계분기 예측 실패 패널티
디코드 폭4 µop/cycle6 µop/cycle프론트엔드 대역폭(Bandwidth)
µop 캐시6,144 엔트리4,096 엔트리L0 µop 캐시
실행 포트6개12개백엔드 병렬 실행 유닛
ROB 크기320 엔트리512 엔트리Reorder Buffer (비순차 실행 윈도우)
L1I 캐시32KB32KB명령어 캐시
분기 예측기TAGETAGE분기 히스토리 기반 예측

파이프라인 단계 다이어그램

x86_64 프로세서의 파이프라인은 프론트엔드, 스케줄러, 백엔드로 구분되며, 각 단계에서 최적화 기회가 있습니다.

x86_64 마이크로아키텍처 파이프라인 µop 파이프라인 단계와 최적화 포인트 프론트엔드 (Frontend) Branch Predict Fetch 16B/cycle Decode 4-6 µop µop$ Cache 최적화: 루프 정렬(32B), 핫패스 인라인, 분기 힌트(likely/unlikely) 백엔드 (Backend) Rename RAT Schedule RS Execute 6-12 port Retire ROB 최적화: 데이터 의존성 최소화, 포트 압력 분산, 메모리 접근 패턴 분기 예측 최적화 • likely()/unlikely() → JCC 방향 힌트 • __cold → 비핫 경로를 .text.unlikely로 • static_branch → NOP/JMP 패칭 • 분기 실패 패널티: ~15-20 사이클 µop 캐시 최적화 • 루프를 32B 경계에 정렬 • 핫패스 크기를 µop$ 용량 내로 유지 • ALTERNATIVE로 짧은 명령어 선택 • NOP: 최적 크기 선택 (1~15바이트) 메모리 최적화 • PREFETCH 명령어 (프리페치) • 캐시라인(64B) 정렬 구조체 • Store→Load 전달 (동일 주소) • LFENCE/SFENCE 최소화 리눅스 커널 마이크로아키텍처 활용 • __cacheline_aligned: 구조체/필드를 L1 캐시라인(64B) 경계 정렬 → false sharing 방지 • ____cacheline_internodealigned_in_smp: NUMA 노드 간 캐시라인 바운싱 방지 • static_branch_likely/unlikely: jump label → NOP/JMP 패칭으로 분기 예측 오버헤드 제거 (tracepoint, 보안 체크 등)

분기 예측 힌트

커널은 likely()/unlikely() 매크로로 GCC에 분기 방향 힌트를 제공합니다.

/* include/linux/compiler.h — 분기 예측 힌트 */
#define likely(x)    __builtin_expect(!!(x), 1)
#define unlikely(x)  __builtin_expect(!!(x), 0)

/* GCC는 likely 경로를 fall-through(JCC not-taken)로 배치
 * → 분기 예측기의 정적 예측(forward not-taken)과 일치
 * → 파이프라인 플러시 최소화 */

/* 사용 예: 페이지 폴트 핸들러 */
if (likely(!(fault & VM_FAULT_ERROR))) {
    /* 정상 경로: 대부분의 페이지 폴트는 성공 */
    return;
}
if (unlikely(fault & VM_FAULT_OOM)) {
    /* OOM: 매우 드문 경로 → .text.unlikely 섹션으로 분리 */
    pagefault_out_of_memory();
}

루프 정렬과 NOP

핫 루프를 캐시라인/µop 캐시 경계에 정렬하면 프론트엔드 대역폭을 극대화할 수 있습니다.

/* 커널 루프 정렬 예시 */

/* .balign 지시자: 다음 명령어를 지정 바이트 경계에 정렬
 * 최적 NOP 패딩 삽입 (ideal_nops 테이블에서 선택) */
    .balign 32                  # 32바이트 경계 정렬 (µop 캐시 라인)
.Lloop:
    movq    (%rsi), %rax
    movq    %rax, (%rdi)
    addq    $8, %rsi
    addq    $8, %rdi
    decq    %rcx
    jnz     .Lloop              # 루프 상단이 정렬되어 fetch 효율 최적

/* arch/x86/kernel/alternative.c — 최적 NOP 선택 */
/* ideal_nops[N]: N바이트 NOP 패딩
 * 1: 0x90 (NOP)
 * 2: 66 90 (66 NOP)
 * 3: 0F 1F 00 (NOP DWORD ptr [RAX])
 * ...
 * 8+: 0F 1F 84 00 00 00 00 00 (long NOP) */

µop 캐시 고려사항

µop 캐시(DSB)에 핫 코드가 완전히 적재되면 디코더를 우회하여 처리량(Throughput)이 향상됩니다.

/* µop 캐시 효율성 체크 (perf 이벤트) */
# DSB(µop 캐시) vs MITE(레거시 디코더) 비율 확인
$ perf stat -e idq.dsb_uops,idq.mite_uops ./workload

/* 커널 핫패스 최적화 원칙:
 * 1. 함수 크기를 µop 캐시 way(~32B 윈도우)에 맞춤
 * 2. 드문 에러 경로를 __cold로 분리 (out-of-line)
 * 3. 복잡한 명령어(예: DIV)는 다수 µop → 대안 사용
 *    (곱셈+시프트로 나눗셈 대체: reciprocal_divide)
 * 4. LOCK 접두사: 캐시라인 정렬 데이터에서만 사용
 *    (split lock은 ~100x 느림 → 커널 split_lock_detect) */

/* 예: reciprocal division (나눗셈을 곱셈으로 대체) */
static inline u32 reciprocal_divide(u32 a, struct reciprocal_value R)
{
    /* DIV(~30 µop) 대신 MUL+SHR(2 µop)로 대체 */
    return (u32)(((u64)a * R.m) >> R.sh);
}
Split Lock Detection: 커널은 CONFIG_X86_SPLIT_LOCK_DETECT로 캐시라인 경계를 넘는 원자적 연산(split lock)을 감지합니다. Split lock은 시스템 전체 버스 락을 유발하여 성능이 극도로 저하되므로, 커널은 이를 경고하거나 SIGBUS로 프로세스(Process)를 종료할 수 있습니다.

VMX/SVM 명령어

x86_64 가상화는 Intel VT-x(VMX)와 AMD-V(SVM) 두 가지 하드웨어 확장으로 지원됩니다. 하이퍼바이저(KVM)는 VMX/SVM 명령어로 가상 머신을 생성하고 VM Entry/Exit 전환을 수행합니다.

기능Intel VMXAMD SVM설명
활성화VMXONVMRUN 직접VMX root 모드 진입
제어 구조VMCS (4KB)VMCB (4KB)VM 상태/제어 정보
VM 진입VMLAUNCH/VMRESUMEVMRUN게스트 실행 시작
VM 탈출VM-Exit (자동)#VMEXIT (자동)하이퍼바이저로 복귀
메모리 가상화EPTNPT (RVI)2단계 주소 변환
인터럽트 가상화Posted InterruptsAVICVM-Exit 없는 인터럽트 전달
CPUID 필드CPUID.1:ECX.VMX[5]CPUID.8000_0001h:ECX.SVM[2]기능 탐지

VMCS/VMCB 구조와 VM Entry/Exit 흐름

VMCS(Intel)와 VMCB(AMD)는 게스트 상태, 호스트 상태, 제어 필드를 포함하는 4KB 구조체입니다.

VMCS/VMCB 구조와 VM Entry/Exit 흐름 VM Entry/Exit 흐름과 VMCS/VMCB 구조 Host (KVM) VMX Root Mode VMXON (VMX 활성화) VMCS/VMCB 설정 vcpu_run() 루프 VM Entry VMLAUNCH /VMRESUME Guest VM VMX Non-Root Mode 게스트 커널/유저 실행 가상 하드웨어 접근 트랩 조건 발생! VM Exit exit_reason → handle_exit() VMCS (Intel) / VMCB (AMD) Guest State: CR0/3/4, RSP, RIP, RFLAGS, segs Host State: CR0/3/4, RSP, RIP, segs (복귀용) VM-Exec Control: 트랩할 이벤트 비트맵 VM-Exit Info: exit_reason, qual, intr_info EPT/NPT Pointer: 2단계 페이지 테이블 주소 주요 VM-Exit 사유: • CPUID(10) • HLT(12) • INVLPG(14) • CR 접근(28) • I/O(30) • MSR 읽기/쓰기(31/32) • EPT violation(48) • 외부 인터럽트(1) • NMI(0) • #PF(14, EPT 미스) • VMCALL(18, hypercall) • XSETBV(55) • APIC 접근(44) KVM: handle_exit()에서 exit_reason별 핸들러 디스패치 → 에뮬레이션/가상화 처리 → VMRESUME로 복귀 VM Entry/Exit 비용 (대략적): • VM Entry: ~400-800 사이클 (레지스터 로드, TLB 전환, 모드 전환) • VM Exit: ~600-1200 사이클 (상태 저장, 호스트 복원) → KVM 최적화: VMCS caching, EPT, Posted Interrupts • Nested Virtualization: L0→L1→L2 이중 VM-Exit 시 ~2배 오버헤드 (VMCS shadowing으로 완화)

VMXON/VMLAUNCH 시퀀스

KVM이 VMX를 초기화하고 게스트를 실행하는 과정입니다.

/* arch/x86/kvm/vmx/vmx.c — VMX 초기화 및 게스트 실행 */

/* 1. VMXON: VMX root 모드 진입 */
static int vmx_hardware_enable(void)
{
    /* CR4.VMXE = 1 (VMX 활성화) */
    cr4_set_bits(X86_CR4_VMXE);

    /* VMXON: 4KB VMXON 영역 물리 주소 전달 */
    asm volatile("vmxon %0" : : "m"(phys_addr) : "memory", "cc");
    return 0;
}

/* 2. VMLAUNCH/VMRESUME: 게스트 실행 */
static noinstr void vmx_vcpu_enter_exit(struct kvm_vcpu *vcpu)
{
    /* 호스트 레지스터 저장 */
    asm volatile(
        "pushq %%rbp\n\t"
        "pushq %%rcx\n\t"
        /* ... 호스트 callee-saved 저장 ... */

        /* 게스트 레지스터 로드 */
        "movq %c[rax](%[regs]), %%rax\n\t"
        "movq %c[rbx](%[regs]), %%rbx\n\t"
        /* ... 나머지 레지스터 ... */

        /* VM Entry! */
        "cmpb $0, %[launched]\n\t"
        "jne .Lvmresume\n\t"
        "vmlaunch\n\t"             # 첫 실행
        "jmp .Lvmfail\n\t"
    ".Lvmresume:\n\t"
        "vmresume\n\t"             # 재실행
        /* VM-Exit 시 여기로 복귀 → 게스트 레지스터 저장 */
    );
}

VMCS 필드 읽기/쓰기

VMCS는 VMREAD/VMWRITE 명령어로만 접근 가능한 구조화된 데이터입니다.

/* arch/x86/kvm/vmx/vmcs.h — VMCS 필드 접근 */
static __always_inline unsigned long vmcs_readl(unsigned long field)
{
    unsigned long value;
    asm volatile("vmread %1, %0"
                 : "=r"(value) : "r"(field) : "cc");
    return value;
}

static __always_inline void vmcs_writel(unsigned long field, unsigned long value)
{
    asm volatile("vmwrite %1, %0"
                 :: "r"(field), "rm"(value) : "cc");
}

/* 주요 VMCS 필드 */
#define GUEST_RIP          0x681E   /* 게스트 명령어 포인터 */
#define GUEST_RSP          0x681C   /* 게스트 스택 포인터 */
#define GUEST_CR3          0x6802   /* 게스트 CR3 (EPT 사용 시 GPA) */
#define VM_EXIT_REASON     0x4402   /* Exit 사유 (16-bit) */
#define EXIT_QUALIFICATION 0x6400   /* Exit 상세 정보 */
#define EPT_POINTER        0x201A   /* EPT PML4 물리 주소 */

/* VM-Exit 후 사유 확인 */
u32 reason = vmcs_read32(VM_EXIT_REASON);
kvm_vmx_exit_handlers[reason](vcpu);  /* 핸들러 디스패치 */
AMD SVM 차이: SVM에서는 VMCB가 일반 메모리로 접근 가능하며(VMREAD/VMWRITE 불필요), VMRUN %rax 하나로 게스트 진입과 상태 로드가 동시에 수행됩니다. KVM은 arch/x86/kvm/svm/에서 SVM 경로를 별도 구현합니다.

디버그 레지스터/하드웨어 브레이크포인트

x86_64는 4개의 디버그 주소 레지스터(DR0-DR3)와 상태/제어 레지스터(DR6/DR7)를 통해 하드웨어 브레이크포인트를 지원합니다. 소프트웨어 브레이크포인트(INT3)와 달리 코드 수정 없이 메모리 접근/실행을 모니터링할 수 있어 watchpoint와 커널 디버깅에 필수적입니다.

레지스터용도설명
DR0-DR3브레이크포인트 주소모니터링할 선형 주소 (4개 동시)
DR4-DR5예약/별칭CR4.DE=0이면 DR6/DR7 별칭
DR6디버그 상태#DB 발생 시 어떤 조건이 트리거되었는지
DR7디버그 제어각 브레이크포인트의 활성/타입/크기 설정

DR7 제어 레지스터 비트 레이아웃

비트 범위필드BP #값 의미
[1:0]L0, G0BP0Local/Global 활성
[3:2]L1, G1BP1Local/Global 활성
[5:4]L2, G2BP2Local/Global 활성
[7:6]L3, G3BP3Local/Global 활성
[17:16]R/W0BP000=실행, 01=데이터 쓰기, 10=I/O, 11=데이터 R/W
[19:18]LEN0BP000=1B, 01=2B, 10=8B, 11=4B
[21:20]R/W1BP1(동일 인코딩)
[23:22]LEN1BP1(동일 인코딩)
[25:24]R/W2BP2(동일 인코딩)
[27:26]LEN2BP2(동일 인코딩)
[29:28]R/W3BP3(동일 인코딩)
[31:30]LEN3BP3(동일 인코딩)

DR0-DR3 주소 설정

디버그 주소 레지스터에 감시할 선형 주소를 설정합니다. 커널은 set_debugreg() 매크로로 접근합니다.

/* arch/x86/include/asm/debugreg.h — 디버그 레지스터 접근 */
#define set_debugreg(value, reg)                       \
    asm volatile("movq %0, %%db" #reg                  \
                 :: "r"((unsigned long)(value)))

#define get_debugreg(var, reg)                         \
    asm volatile("movq %%db" #reg ", %0"                \
                 : "=r"(var))

/* 하드웨어 브레이크포인트 설정 예 */
void hw_breakpoint_set(int bp_num, unsigned long addr)
{
    switch (bp_num) {
    case 0: set_debugreg(addr, 0); break;  /* DR0 */
    case 1: set_debugreg(addr, 1); break;  /* DR1 */
    case 2: set_debugreg(addr, 2); break;  /* DR2 */
    case 3: set_debugreg(addr, 3); break;  /* DR3 */
    }
}

DR7 구성 (타입, 크기, 활성화)

DR7은 각 브레이크포인트의 타입(실행/읽기/쓰기), 크기(1/2/4/8바이트), 로컬/글로벌 활성 비트를 제어합니다.

/* arch/x86/kernel/hw_breakpoint.c — DR7 설정 */
unsigned long encode_dr7(int drnum, unsigned len, unsigned type)
{
    unsigned long dr7 = 0;

    /* Local enable 비트 (DR7[2*drnum]) */
    dr7 |= (1UL << (drnum * 2));

    /* R/W 필드: 비트 [16+4*drnum : 17+4*drnum]
     * 00 = 실행 (X)
     * 01 = 데이터 쓰기만 (W)
     * 10 = I/O 읽기/쓰기 (CR4.DE=1 필요)
     * 11 = 데이터 읽기+쓰기 (RW) */
    dr7 |= ((unsigned long)type << (16 + drnum * 4));

    /* LEN 필드: 비트 [18+4*drnum : 19+4*drnum]
     * 00 = 1바이트
     * 01 = 2바이트
     * 10 = 8바이트 (x86_64에서만)
     * 11 = 4바이트 */
    dr7 |= ((unsigned long)len << (18 + drnum * 4));

    return dr7;
}

/* DR7 적용 */
unsigned long dr7;
get_debugreg(dr7, 7);
dr7 |= encode_dr7(0, DR7_LEN_4, DR7_BREAK_ON_WRITE);  /* BP0: 4B 쓰기 감시 */
set_debugreg(dr7, 7);

perf_event 하드웨어 브레이크포인트 API

유저 공간에서는 perf_event_open()을 통해 하드웨어 브레이크포인트를 설정할 수 있으며, 커널 내부에서는 register_wide_hw_breakpoint()를 사용합니다.

/* kernel/events/hw_breakpoint.c — 커널 hw_breakpoint API */
struct perf_event * register_wide_hw_breakpoint(
    struct perf_event_attr *attr,
    perf_overflow_handler_t triggered,
    void *context)
{
    /* 모든 CPU에서 하드웨어 브레이크포인트 등록
     * attr→bp_type: HW_BREAKPOINT_W, HW_BREAKPOINT_RW, HW_BREAKPOINT_X
     * attr→bp_len: HW_BREAKPOINT_LEN_1/2/4/8
     * attr→bp_addr: 감시할 가상 주소
     * triggered: #DB 발생 시 호출되는 콜백 */
}

/* 사용 예: 커널 변수 감시 (watchpoint) */
struct perf_event_attr attr = {
    .type           = PERF_TYPE_BREAKPOINT,
    .bp_type        = HW_BREAKPOINT_W,       /* 쓰기 감시 */
    .bp_addr        = (unsigned long)&watched_var,
    .bp_len         = HW_BREAKPOINT_LEN_8,   /* 8바이트 */
    .disabled       = 0,
};
struct perf_event *bp = register_wide_hw_breakpoint(
    &attr, my_bp_handler, NULL);

/* #DB 핸들러: DR6에서 트리거된 브레이크포인트 확인 */
void my_bp_handler(struct perf_event *bp,
                    struct perf_sample_data *data,
                    struct pt_regs *regs)
{
    pr_info("watchpoint hit at %pS\n", (void *)regs->ip);
    /* DR6 비트: B0-B3 → 어떤 DR0-DR3가 트리거
     * BD(13) → DR 접근 감지, BS(14) → 단일 스텝, BT(15) → 태스크 전환 */
}
ptrace와 하드웨어 브레이크포인트: GDB는 ptrace(PTRACE_POKEUSER)로 디버그 레지스터를 설정합니다. watch 명령어는 소프트웨어 watchpoint(느림)를 사용하지만, rwatch/awatch는 DR0-DR3 하드웨어 watchpoint를 사용하여 성능 저하 없이 메모리 접근을 모니터링합니다. 단, 4개까지만 동시 설정 가능합니다.
가상화와 디버그 레지스터: KVM은 게스트 디버그 레지스터를 VMCS/VMCB에서 관리합니다. 호스트와 게스트가 동시에 하드웨어 브레이크포인트를 사용하면 VM-Exit 시 DR 교체가 필요하며, MOV DR VM-Exit 인터셉트로 처리합니다.

참고 링크

다음 학습: