어셈블리 종합 (Assembly)

커널 개발에서 필요한 어셈블리 지식을 ABI·성능·안전성 관점으로 심층 정리합니다. GCC 인라인 어셈블리 제약조건과 clobber 규칙, 오퍼랜드 형식(ModR/M·SIB·RIP-상대), asm goto와 Static Key 패칭, x86-64/ARM64/RISC-V 레지스터 심화, REX/VEX/EVEX 인코딩 상세, CFI/ORC 스택 해제, 문자열 연산(ERMS/FSRM)과 copy_user, 비트 조작(BSF/BMI), 특권 명령어(VMX/SGX/SMAP), Spectre/Meltdown/MDS 완화(IBRS/KPTI/VERW), 성능 카운터(PMU/RDTSC), 호출 규약 심화(Red Zone/Windows x64), AVX-512/AMX까지 다룹니다.

전제 조건: 커널 아키텍처메모리 배리어 문서를 먼저 읽으세요. 어셈블리 문맥은 ABI, 레지스터 규약, 배리어 의미가 결합되므로 C 코드와 1:1 대응으로 추적하는 습관이 중요합니다.
일상 비유: 이 주제는 회로도 레벨 정비와 비슷합니다. 겉으로 같은 기능이라도 신호선 연결이 다르면 결과가 달라지듯이, 명령어 순서와 제약 조건이 동작을 결정합니다.

핵심 요약

  • 레지스터 규약 — x86_64/ARM64에서 인자, 반환값, 보존 레지스터를 먼저 구분합니다.
  • 인라인 asm 제약 — 입력/출력 오퍼랜드와 clobber를 정확히 선언해 오동작을 방지합니다.
  • 진입/탈출 경로 — syscall/인터럽트에서 pt_regs가 어떻게 구성되는지 추적합니다.
  • 메모리 배리어 — x86 TSO와 ARM64 약한 메모리 모델 차이를 구분해 적용합니다.
  • CPUID 기반 분기 — 피처 비트 해석으로 런타임 최적화 경로를 결정합니다.

단계별 이해

  1. 가설 수립
    문제와 개선 목표를 수치로 정의합니다.
  2. 제약 분석
    호환성, 안정성, 보안 제약을 먼저 확인합니다.
  3. 실험 적용
    최소 변경으로 효과와 부작용을 측정합니다.
  4. 정식 반영
    검증된 변경만 문서화해 반영합니다.
관련 표준: System V AMD64 ABI (x86-64 호출 규약), ARM AAPCS (AArch64 호출 규약), Intel SDM (명령어 레퍼런스) — 커널 어셈블리 코드가 따르는 ABI 및 ISA 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

인라인 어셈블리 개요

Linux 커널은 성능 최적화, 하드웨어 직접 제어, 원자적 연산 등을 위해 인라인 어셈블리를 사용합니다. GCC의 asm (또는 __asm__) 키워드를 사용합니다.

GCC 인라인 어셈블리 문법

asm volatile (
    "assembly template"            /* 어셈블리 코드 */
    : output operands              /* 출력 (optional) */
    : input operands               /* 입력 (optional) */
    : clobbers                     /* 변경되는 레지스터/메모리 */
);

/* 예: x86 rdtsc (TSC 읽기) */
static inline u64 rdtsc(void)
{
    u32 lo, hi;
    asm volatile ("rdtsc" : "=a"(lo), "=d"(hi));
    return ((u64)hi << 32) | lo;
}

/* 예: CR3 레지스터 읽기 (현재 페이지 테이블) */
static inline unsigned long read_cr3(void)
{
    unsigned long val;
    asm volatile ("mov %%cr3, %0" : "=r"(val));
    return val;
}
GCC 인라인 어셈블리 구조 asm volatile ( "assembly template" /* 어셈블리 명령어 문자열 */ ① 어셈블리 코드 %0 %1... 로 오퍼랜드 참조 · %%rax = 리터럴 레지스터 이름 : "=r"(result), "=a"(lo) /* 출력 (optional) */ ② 출력 오퍼랜드 "=r" 쓰기전용 · "=a" rax · "+r" 읽기+쓰기 → C 변수로 결과 반환 : "r"(val), "0"(old) /* 입력 (optional) */ ③ 입력 오퍼랜드 "r" 범용 · "a" rax · "m" 메모리 · "i" 즉시값 · "0" 동일 레지스터 : "memory", "rcx"); /* 클러버 (optional) */ ④ 클러버 목록 변경된 레지스터 명시 · "memory" = 메모리 배리어 · "cc" = 플래그 데이터 흐름 C 입력 변수 unsigned long old; unsigned long new; volatile void *ptr; ③ 입력 오퍼랜드 ③ 입력 "r","a","m" asm volatile ( ... ) ① 어셈블리 명령어 실행 레지스터 ↔ %0 %1 %2 매핑 cmpxchg · rdtsc · cli · sti ① 어셈블리 코드 실행 ② 출력 "=r","=a" C 출력 변수 unsigned long prev; u32 lo, hi; unsigned long val; ② 출력 오퍼랜드 ④ 클러버: "memory", "rcx", "cc" 컴파일러에 변경 레지스터/메모리 통보 → 값 보존 불필요 volatile 키워드 • 코드 제거/재정렬 방지 • 하드웨어 부작용 보존 (I/O, CSR) • __asm__ __volatile__ 동등 (커널에서 선호하는 표기) 오퍼랜드 참조 방법 • %0, %1, %2 — 순서 기반 참조 • %[name] — 이름 기반 참조 • %%rax — 리터럴 레지스터 이름 • "0" — 첫 번째와 같은 레지스터 "memory" 클러버 효과 • 컴파일러 소프트웨어 배리어 • 캐시된 변수 값 재로드 강제 • "cc" = RFLAGS 변경 통보 • CLI/STI, lock, xchg에 필수

오퍼랜드 제약조건

GCC 인라인 어셈블리 제약조건(Constraint) 참조 레지스터 제약 "r"범용 레지스터 (컴파일러 선택) "a"rax/eax — accumulator "c"rcx/ecx — counter "d"rdx/edx — data "b"rbx/ebx — base "A"rax:rdx 쌍 (128비트 결과) "S"rsi/esi — source index "D"rdi/edi — destination index "q"byte-able (a/b/c/d) "x"XMM 레지스터 "y"YMM/XMM 레지스터 (x86) "f"x87 FPU 스택 레지스터 메모리 / 즉시값 "m"메모리 참조 (어떤 주소든) "o"오프셋 가능 메모리 "g"범용 (레지스터, 메모리, 상수) "p"유효 주소 (포인터) "X"무엇이든 허용 (don't care) "i"즉시 상수 (컴파일 타임) "n"정수 상수 (알려진 값) "e"32비트 부호확장 상수 "Z"32비트 부호없는 정수 상수 "E","F"부동소수점 상수 출력 수정자 (Modifier) "="출력 전용 (write-only) "+"입출력 (read-write) "&"early clobber (입력 이전 변경) "%"교환 가능 오퍼랜드 쌍 조합 예시: : "=r"(out) 출력 전용 레지스터 : "+m"(*ptr) 메모리 읽기+쓰기 : "=&r"(tmp) early clobber : "=a"(ret) rax에 출력 매칭 제약 / 클러버 선언 "0"~"9"N번 오퍼랜드와 같은 위치 "cc"조건 코드(RFLAGS) 변경됨 "mem"메모리 전체 배리어 효과 "rax"…특정 레지스터 손상 선언 클러버 예시: ::: "memory" 메모리 배리어 ::: "cc" 플래그 변경 ::: "rax","rcx" 레지스터 ::: "xmm0","xmm1" SIMD
제약x86 의미설명
"r"범용 레지스터컴파일러가 선택
"a"eax/raxaccumulator
"b"ebx/rbxbase register
"c"ecx/rcxcounter register
"d"edx/rdxdata register
"m"메모리 참조메모리 피연산자
"i"즉시값상수
"="출력 전용쓰기만
"+"입출력읽기+쓰기
"S"esi/rsisource index
"D"edi/rdidestination index
"A"edx:eax 쌍64/128비트 결과 (rdtsc 등)
"q"a/b/c/d 레지스터바이트 접근 가능 레지스터
"Q"a/b/c/d (상위)ah/bh/ch/dh 접근
"R"레거시 레지스터REX 불필요 레지스터
"x"XMM 레지스터SSE 벡터
"y"YMM 레지스터AVX 벡터
"f"x87 FPU 스택부동소수점
"o"오프셋 메모리오프셋 가능 메모리 참조
"g"범용레지스터, 메모리, 즉시값 허용
"n"정수 상수컴파일 타임 알려진 정수
"e"32비트 부호확장 상수64비트 모드에서 -2³¹~2³¹-1
"Z"32비트 부호없는 상수0~2³²-1
"p"유효 주소포인터 (lea 사용 가능)
"X"모든 것제약 없음 (don't care)
"&"early clobber입력 읽기 전 출력 변경됨
"%"교환 가능이 오퍼랜드와 다음 교환 허용
"0"-"9"매칭 제약N번 오퍼랜드와 동일 레지스터

ARM64 제약조건

제약ARM64 의미설명
"r"x0-x30 범용 레지스터64비트 범용
"w"v0-v31 SIMD/FP128비트 벡터/부동소수점
"m"메모리 참조base+offset 주소
"I"12비트 즉시값 (0-4095)ADD/SUB 즉시값
"K"32비트 논리 즉시값AND/ORR/EOR 비트 패턴
"L"64비트 논리 즉시값64비트 비트마스크 패턴
"M"MOV 즉시값MOVZ/MOVN으로 로드 가능

RISC-V 제약조건

제약RISC-V 의미설명
"r"x0-x31 정수 레지스터범용 레지스터
"f"f0-f31 부동소수점F/D 확장 필요
"m"메모리 참조base+offset
"I"12비트 부호 확장 즉시값-2048~2047
"J"0 즉시값x0(zero) 레지스터와 동등
"K"5비트 즉시값 (0-31)시프트 양
"A"주소 (레지스터 간접)AMO 명령어용
💡

제약조건 디버깅: 잘못된 제약조건은 컴파일 오류나 미묘한 런타임 버그를 유발합니다. gcc -S -o - file.c로 생성된 어셈블리를 확인하여 오퍼랜드가 의도한 레지스터에 배치되었는지 검증하세요. "=&r"(early clobber)를 빠뜨리면 입력과 출력이 같은 레지스터를 공유하여 데이터 손상이 발생할 수 있습니다.

오퍼랜드 형식 심화

CPU 명령어의 오퍼랜드 인코딩을 이해하면 objdump 출력을 해석하고, 바이너리 패칭(ALTERNATIVE, static key)의 동작을 정확히 파악할 수 있습니다. x86의 가변 길이 CISC 인코딩, ARM64의 고정 폭 32비트 인코딩, RISC-V의 6가지 명령어 형식을 비교합니다.

x86 주소 지정 모드

모드AT&T 문법Intel 문법유효 주소사용 예
즉시값$4242(상수 오퍼랜드)movq $0, %rax
레지스터%raxrax(레지스터 내용)addq %rbx, %rax
직접 메모리label[label]절대/RIP-상대 주소movq jiffies, %rax
간접 메모리(%rax)[rax]basemovq (%rdi), %rax
Base+Disp8(%rax)[rax+8]base + displacementmovq 16(%rbp), %rsi
Base+Index(%rax,%rcx)[rax+rcx]base + indexmovq (%rdi,%rsi), %rax
SIB 전체(%rax,%rcx,8)[rax+rcx*8]base + index×scalemovq (%rdi,%rsi,8), %rax
SIB+Disp0x10(%rax,%rcx,4)[rax+rcx*4+0x10]base + index×scale + dispmovl 4(%rbx,%rdx,4), %eax
RIP-상대label(%rip)[rip+label]RIP + disp32movq jiffies(%rip), %rax
/* AT&T 주소 지정 모드 문법: displacement(base, index, scale)
 * 유효 주소 = base + index × scale + displacement
 * scale: 1, 2, 4, 8
 * displacement: 8비트 또는 32비트 부호 확장 */

/* objdump 출력 예시: */
/* ffffffff81001234:  48 8b 04 cd 00 00 00 00    mov 0x0(,%rcx,8),%rax */
/*   ↑ REX.W   ↑ MOV r,r/m   ↑ ModRM+SIB    ↑ disp32=0 */
/*   ModRM: 00 000 100 → mod=00, reg=rax, rm=100(SIB)  */
/*   SIB:   11 001 101 → scale=8, index=rcx, base=disp32(no base) */

ModR/M 바이트 상세

ModR/M 바이트는 x86 명령어의 오퍼랜드를 지정하는 핵심 바이트입니다. 3개 필드(mod, reg, r/m)로 256가지 조합을 제공합니다.

ModR/M 바이트 구조 (비트 7:0) mod (7:6) 2비트: 주소 모드 reg (5:3) 3비트: 레지스터 또는 opcode 확장 r/m (2:0) 3비트: 레지스터 또는 메모리 00: [r/m] (메모리, 변위 없음) 01: [r/m]+disp8 (8비트 변위) 10: [r/m]+disp32 (32비트 변위) 11: 레지스터 직접 (메모리 아님) 000=rax 001=rcx 010=rdx 011=rbx 100=SIB 101=RIP† 110=rsi 111=rdi † mod=00, rm=101 → RIP-상대 (64비트) rm=100 → SIB 바이트 필요
modr/m=100r/m=101r/m=기타
00SIB (변위 없음)RIP+disp32 (64비트)[reg] (변위 없음)
01SIB+disp8[rbp]+disp8[reg]+disp8
10SIB+disp32[rbp]+disp32[reg]+disp32
11rsp (레지스터)rbp (레지스터)레지스터 직접

SIB 바이트 상세

SIB(Scale-Index-Base) 바이트는 ModR/M에서 r/m=100일 때 사용되며, 복잡한 주소 계산을 제공합니다.

SIB 바이트 구조 (비트 7:0) scale (7:6) 00=×1 01=×2 10=×4 11=×8 index (5:3) 인덱스 레지스터 (RSP=100=없음) base (2:0) 베이스 레지스터 (mod에 따라 다름) 유효 주소 = base + index × scale + displacement 특수: index=100(RSP) → 인덱스 없음 base=101+mod=00 → disp32 only

변위와 즉시값 인코딩

크기ModRM mod범위바이트규칙
변위 없음mod=000레지스터 간접 기본
disp8mod=01-128 ~ +1271부호 확장하여 64비트로
disp32mod=10-2³¹ ~ +2³¹-14부호 확장하여 64비트로
/* 즉시값 크기 규칙 */
/* MOV r64, imm64:       10바이트 (REX.W + B8+rd + imm64) — movabs */
/* MOV r/m64, imm32:     7바이트 (REX.W + C7 /0 + imm32, 부호확장) */
/* ADD r/m64, imm32:     7바이트 (REX.W + 81 /0 + imm32, 부호확장) */
/* ADD r/m64, imm8:      4바이트 (REX.W + 83 /0 + imm8, 부호확장) */

/* 커널에서의 최적화: GCC는 가능한 짧은 인코딩을 선택 */
/* movq $0, %rax   → 48 c7 c0 00 00 00 00 (7B, imm32 부호확장) */
/* xorq %rax, %rax → 48 31 c0             (3B, 더 짧고 빠름) */
/* addq $1, %rax   → 48 83 c0 01          (4B, imm8 부호확장) */

RIP-상대 주소 지정

64비트 모드에서 mod=00, r/m=101은 RIP-상대 주소를 의미합니다. 위치 독립 코드(PIC)와 커널 전역변수 접근에 핵심적입니다.

/* RIP-상대 주소 지정 */
/* 커널 전역변수 접근 */
    movq jiffies(%rip), %rax    /* RIP-상대: 위치 독립적 */

/* objdump 출력 예시: */
/* ffffffff81001000:  48 8b 05 f9 0f 00 00  mov 0xfff9(%rip),%rax */
/*   유효 주소 = RIP(다음 명령어) + 0x0ff9 = ffffffff81002000 */
/*   → jiffies 변수 주소 */

/* 32비트 모드에서 mod=00, r/m=101은 절대 주소(disp32)
 * 64비트 모드에서 mod=00, r/m=101은 RIP+disp32
 * → 32비트 코드를 64비트로 포팅 시 주의 필요 */

/* 커널은 KASLR 활성화 시 모든 전역변수를 RIP-상대로 접근
 * → 코드와 데이터가 함께 재배치되어도 올바르게 동작 */
ℹ️

32비트 절대 주소 금지: 64비트 커널에서는 절대 주소 사용을 피합니다. KASLR이 커널을 무작위 주소에 로드하므로, 컴파일 타임 절대 주소는 무효합니다. GCC의 -mcmodel=kernel은 RIP-상대 주소를 기본으로 생성합니다.

ARM64 오퍼랜드 형식

ARM64(AArch64)는 모든 명령어가 32비트 고정 폭입니다. 오퍼랜드 인코딩이 명령어 비트 내에 고정 위치로 배치됩니다.

ARM64 명령어 인코딩 (32비트 고정폭) sf (31) 크기 (0=32/1=64) opcode (30:21) 명령어 종류 Rm/imm (20:5) 소스2 또는 즉시값 Rn (9:5) 소스1 Rd (4:0) 목적지 메모리 주소 모드: [Xn], [Xn, #imm], [Xn, Xm], [Xn, Xm, LSL #n], [Xn, #imm]! (pre-index), [Xn], #imm (post-index) 즉시값: 12비트 (ADD/SUB), 16비트 시프트 (MOVZ/MOVK), 논리 비트마스크 (AND/ORR), PC-상대 오프셋 (B/BL: ±128MB) 모든 명령어 32비트 고정 → 디코딩 단순 · 파이프라인 효율 · 코드 밀도는 x86보다 낮음 (RVC 확장으로 보완)

RISC-V 오퍼랜드 형식

RISC-V는 6가지 기본 명령어 형식을 정의합니다. 모든 형식은 32비트이며, opcode가 하위 7비트에 고정됩니다.

RISC-V 6가지 명령어 형식 (32비트) R: funct7 [31:25] rs2 [24:20] rs1 [19:15] f3 [14:12] rd [11:7] opcode [6:0] add, sub, and, or, xor, sll, srl I: imm[11:0] [31:20] rs1 f3 rd opcode addi, lw, ld, jalr, ecall S: imm[11:5] rs2 rs1 f3 imm[4:0] opcode sw, sd, sb, sh B: beq, bne, blt, bge (±4KB) U: imm[31:12] — 20비트 상위 즉시값 rd opcode lui, auipc J: imm[20|10:1|11|19:12] — 20비트 점프 오프셋 (±1MB) rd opcode jal (±1MB) 핵심: opcode는 항상 비트 [6:0]에 고정 → 하드웨어 디코더 단순화 · 파이프라인 효율성 즉시값 비트가 형식마다 분산(특히 B/J 타입) → 하드웨어 배선 최적화, 소프트웨어 복잡도 증가 32비트 상수 로드: lui rd, imm20 + addi rd, rd, imm12 (2 명령어 조합)

오퍼랜드 형식 실전 함정

함정증상원인해결
AT&T 오퍼랜드 순서역방향 복사mov src, dst (AT&T) vs mov dst, src (Intel)AT&T = "src → dst" 기억
접미사 누락잘못된 크기mov (%rax), %eax vs movl명시적 크기 접미사 사용
RIP-상대 혼동잘못된 주소32비트 절대 vs 64비트 RIP-상대label(%rip) 사용
SIB base=rbp예상외 인코딩mod=00, base=101 → disp32 필요disp8=0 사용 (mod=01)
REX 접두사 위치디코딩 오류REX는 opcode 직전이어야 함레거시 접두사 뒤, opcode 앞
즉시값 부호확장잘못된 값imm32가 64비트로 부호확장됨movabs로 64비트 즉시값
ARM64 즉시값 범위어셈블 오류12비트 범위 초과movz+movk 조합
RISC-V B-type 범위링크 오류±4KB 초과 분기역전된 조건+무조건 점프
⚠️

인라인 asm에서의 오퍼랜드 크기: %0 참조 시 오퍼랜드 크기가 C 변수 타입에 따라 결정됩니다. u32 변수에 대해 movq를 사용하면 레지스터 크기 불일치로 잘못된 코드가 생성될 수 있습니다. %q0(quad), %w0(word), %b0(byte), %h0(high byte) 크기 수정자를 활용하세요.

asm goto 패턴

GCC의 asm goto 확장은 인라인 어셈블리에서 C 레이블로의 분기를 가능하게 합니다. 커널의 Static Key(jump_label) 메커니즘의 핵심 기반입니다.

asm goto 문법

/* asm goto 기본 문법 */
asm goto (
    "assembly template"
    :                             /* 출력 오퍼랜드 없음 (GCC 제한) */
    : input operands              /* 입력 */
    : clobbers                    /* 클러버 */
    : label1, label2, ...         /* C 레이블 목록 */
);

/* 레이블 참조: %l0, %l1, ... (순서 기반) */
/* 또는 %l[labelname] (이름 기반) */

/* 예: 조건부 분기 */
asm goto (
    "cmpq %0, %1\n\t"
    "jg %l[greater]"
    : : "r"(a), "r"(b)
    : "cc"
    : greater
);
/* a <= b 경로 */
return 0;

greater:
/* a > b 경로 */
return 1;
ℹ️

asm goto 출력 제한: GCC 14 이전에서는 asm goto에 출력 오퍼랜드를 사용할 수 없었습니다 (제어 흐름 분석 복잡도). GCC 14+에서는 asm goto에 출력 오퍼랜드가 허용됩니다. 커널은 호환성을 위해 출력이 필요한 경우 별도 패턴을 사용합니다.

Static Key와 asm goto

Static Key(jump label)는 asm goto와 코드 패칭을 결합해, 거의 변하지 않는 조건을 핫 패스 안에 넣으면서도 기본 경로의 부담을 극단적으로 줄이는 메커니즘입니다. 일반 if (flag)는 매 호출마다 메모리에서 flag를 읽고, 값을 검사하고, 조건 분기를 수행합니다. 반면 Static Key는 기본 상태에서 그 자리에 5바이트 NOP 또는 5바이트 JMP를 직접 놓아 둡니다.

핵심은 비용의 위치를 바꾸는 것입니다. 상태가 바뀌지 않는 대부분의 시간에는 분기 선택 비용을 거의 없애고, 상태를 켜거나 끌 때만 텍스트 패칭 비용을 한 번 지불합니다. 그래서 tracepoint, 스케줄러 통계, 디버그 훅처럼 "평소에는 꺼져 있고, 켜더라도 오래 유지되는" 기능에 특히 잘 맞습니다.

권장 API: 커널 공식 문서는 direct struct static_key 사용과 static_key_true(), static_key_false()를 deprecated로 봅니다. 새 코드는 DEFINE_STATIC_KEY_TRUE, DEFINE_STATIC_KEY_FALSE, static_branch_likely(), static_branch_unlikely() 조합을 사용하는 것이 맞습니다.

왜 일반 bool 분기보다 빠른가

일반 분기는 "값을 읽고 판단"합니다. Static Key는 "기계어를 바꿔서 아예 다른 길로 만든다"는 점이 다릅니다. 즉, 조건 값이 메모리에 존재하는 것이 아니라, 분기 명령 자체가 텍스트 섹션에 박혀 있습니다. 이 차이 때문에 disabled fast path에서 데이터 캐시 접근과 조건 분기 일부를 통째로 없앨 수 있습니다.

일반 flag 분기와 Static Key의 fast path 비교 일반 if (flag) 1. 메모리에서 flag 읽기 2. test/cmp + 조건 분기 + 예측 3. fast path 또는 slow path 진입 매 호출마다 데이터 캐시 접근과 조건 분기가 따라붙습니다. flag가 많아질수록 캐시라인 압박과 branch miss 비용이 누적됩니다. Static Key 1. 텍스트 섹션에 5바이트 슬롯 확보 2. 비활성: NOP으로 곧바로 통과 활성: JMP로 out-of-line slow path 진입 3. steady-state fast path에서 flag load 없음 비용은 상태 변경 시의 텍스트 패칭으로 이동합니다. 즉, "자주 읽고 거의 안 바뀌는 조건"일수록 효과가 큽니다.
방식fast path에서 매번 하는 일상태 변경 비용적합한 조건
일반 if (flag)메모리 load + test + 조건 분기거의 없음자주 바뀌는 상태, 요청마다 달라지는 정책
likely() / unlikely()분기 자체는 유지, 코드 배치만 힌트없음분포는 치우쳤지만 런타임 토글이 없는 조건
Static Key기본 상태에서 NOP 또는 JMP 1개높음, 패치 시점에만 지불tracepoint, 통계, 진단 훅처럼 거의 고정된 조건

컴파일부터 런타임 패칭까지의 파이프라인

Static Key는 C 매크로 하나로 끝나는 마법이 아닙니다. 컴파일러는 asm goto 기반 분기 슬롯을 만들고, 어셈블러/링커는 각 분기 사이트에 대한 메타데이터를 __jump_table에 모읍니다. 그리고 런타임에 static_branch_enable() 또는 static_branch_disable()가 호출되면, 해당 key를 참조하는 모든 사이트가 한 번에 패칭됩니다.

Static Key 파이프라인: 소스 코드 → __jump_table → 런타임 패칭 1. 소스 코드 DEFINE_STATIC_KEY_FALSE(key) if (static_branch_unlikely(...)) slow_path(); 2. 빌드 결과 site A: nopl ... site B: nopl ... slow path는 out-of-line 배치 3. __jump_table entry 1: { code, target, key } entry 2: { code, target, key } 동일 key의 모든 site 추적 4. 토글 static_branch_ enable/disable 모든 site 패칭 동일 key를 참조하는 사이트가 3개라면? RX fast path NOP 또는 JMP TX fast path NOP 또는 JMP 오류 통계 경로 NOP 또는 JMP 중요: key 하나를 켜거나 끄면 관련 branch site 전체가 함께 전환됩니다.
__jump_table 메타데이터무엇을 담는가왜 중요한가
code패칭할 명령어 시작 위치x86에서는 보통 5바이트 NOP/JMP 슬롯의 첫 바이트를 가리킵니다.
target분기할 C 레이블 또는 out-of-line 블록 위치활성화 시 이 주소로 JMP가 만들어집니다.
key와 분기 메타데이터어느 Static Key에 속한 site인지 식별동일 key를 참조하는 여러 site를 한 번에 찾아 패칭할 수 있습니다.
/* 권장 Static Key 사용 패턴 */
static DEFINE_STATIC_KEY_FALSE(net_trace_key);

void fast_rx(struct sk_buff *skb)
{
    /* 기본 false 상태에서는 이 자리의 branch slot이 NOP이다 */
    if (static_branch_unlikely(&net_trace_key))
        trace_rx_packet(skb);

    consume_skb(skb);
}

void trace_rx_enable(void)
{
    /* 동일 key를 참조하는 모든 site를 true 방향으로 패칭 */
    static_branch_enable(&net_trace_key);
}

void trace_rx_disable(void)
{
    static_branch_disable(&net_trace_key);
}
/* 개념 설명용 x86 출력 예시: 기본 false + static_branch_unlikely */
.Lsite:
    nopl 0x0(%rax,%rax,1)          /* 5바이트 NOP, fast path는 그냥 통과 */
    /* __jump_table에는 { .Lsite, .Lslow_path, &net_trace_key }가 기록됨 */
    ...
.Lafter_fast:
    ret

.Lslow_path:
    call trace_rx_packet
    jmp .Lafter_fast

/* enable 후 같은 슬롯 */
.Lsite:
    jmp .Lslow_path                /* 같은 5바이트 자리가 JMP rel32로 교체 */
아키텍처 지원: HAVE_ARCH_JUMP_LABEL이 없는 아키텍처에서는 Static Key API가 일반 load-test-branch 시퀀스로 폴백합니다. 즉, API는 동일해도 실제 성능 모델은 아키텍처의 jump label 지원 여부에 따라 달라질 수 있습니다.

권장 API와 상태 전이

DEFINE_STATIC_KEY_TRUE/FALSE는 "초기 상태"를 정하고, static_branch_likely/unlikely()는 "어느 경로를 기본 코드 배치로 둘지"를 표현합니다. 이름만 보면 분기 예측 힌트처럼 보이지만, 여기서는 런타임에 패칭될 branch slot의 기본 모양을 고르는 의미가 더 큽니다.

API역할언제 쓰는가
DEFINE_STATIC_KEY_FALSE(key)기본 false로 시작하는 전역 key 선언디버그, trace, 통계처럼 평소에는 꺼져 있는 기능
DEFINE_STATIC_KEY_TRUE(key)기본 true로 시작하는 전역 key 선언거의 항상 켜져 있지만 가끔 꺼야 하는 기능
static_branch_unlikely(&key)false 상태의 fast path를 fall-through로 배치disabled 경로를 가장 싸게 만들고 싶을 때
static_branch_likely(&key)true 상태의 fast path를 straight-line으로 배치enabled 상태가 기본인 기능
static_branch_enable(&key)key를 true 방향으로 강제 전환sysctl, debugfs, 초기화 루틴에서 기능 켜기
static_branch_disable(&key)key를 false 방향으로 강제 전환기능 끄기
static_branch_inc(&key) / static_branch_dec(&key)참조 카운트 기반 공유 토글여러 사용자가 같은 기능을 함께 켜고 끌 때
static_key_enabled() / static_key_count()현재 상태와 카운트 조회동일한 보호 락 아래에서 진단 정보 읽기

실전 판단 기준은 단순합니다. 상태가 자주 바뀌면 일반 분기, 상태가 거의 안 바뀌고 읽기만 매우 많으면 Static Key입니다. 바꿔 말하면 Static Key는 "읽기 최적화용"이지 "자주 토글하는 스위치"가 아닙니다.

x86에서 안전하게 패칭되는 이유

x86에서는 보통 NOP와 JMP를 같은 길이(대표적으로 5바이트)로 맞춰 둡니다. 이렇게 해야 실행 중인 코드를 다른 명령 길이로 뒤틀지 않고 같은 슬롯 안에서 교체할 수 있습니다. 실제 구현 세부는 아키텍처 코드가 담당하지만, x86의 대표 경로는 arch_jump_label_transform*()text_poke_bp() 계열 텍스트 패칭 루틴으로 이해하면 됩니다.

x86 패칭 순서의 개념도 1. steady-state 0f 1f 44 00 00 nopl 0(%rax,%rax,1) CPU들은 같은 5바이트 슬롯을 그냥 직선 경로로 통과합니다. 2. 패치 요청 static_branch_enable() 동일 key의 모든 site를 순회하며 패치 계획 수립 3. 텍스트 패칭 INT3 또는 동등한 보호 장치로 다른 CPU의 실행 창을 제어 나머지 바이트 기록 후 첫 opcode 바이트를 최종 교체 4. 새 상태 e9 xx xx xx xx jmp .Lslow_path 이후 들어오는 CPU는 바뀐 경로를 그대로 사용 설명 편의를 위한 단순화입니다. 실제 잠금, 동기화, 코어 간 가시성 처리는 arch 텍스트 패칭 코드가 맡습니다. 핵심은 "동일 길이 슬롯을 안전하게 교체"하는 것입니다.

따라서 Static Key의 본질은 "분기 예측을 더 잘하자"가 아니라 분기 자체를 다른 기계어로 교체하자에 가깝습니다. enabled 상태에서도 JMP 자체의 비용과 slow path의 I-cache 부담은 남아 있으므로, 완전한 공짜는 아니지만 disabled hot path를 매우 싸게 만들 수 있다는 점이 중요합니다.

실전에서 자주 틀리는 부분

오해 또는 실수왜 문제인가올바른 판단
likely()와 Static Key를 같은 것으로 이해전자는 분기 힌트, 후자는 런타임 코드 패칭입니다.배치 힌트가 필요하면 likely(), 코드 자체를 바꿔야 하면 Static Key를 씁니다.
자주 켜고 끄는 상태에 적용패칭 비용과 동기화 비용이 누적되어 오히려 손해입니다.읽기 횟수는 많고 상태 변화는 드문 조건에만 사용합니다.
지역 변수나 동적 객체에 key를 넣음Static Key는 전역적 site 목록과 연결되어야 하므로 수명과 주소가 안정적이어야 합니다.정적 저장 수명(global/static)으로 선언합니다.
초기값과 likely/unlikely 조합을 뒤섞음fast path 방향이 뒤집혀 의도와 반대로 배치될 수 있습니다."기본 상태에서 어느 경로를 직선 경로로 둘 것인가"를 먼저 정하고 API를 고릅니다.
CPU hotplug notifier 같은 특수 문맥에서 무심코 토글패칭 경로가 잡는 락과 충돌해 교착 위험이 있습니다.정말 필요한 경우에만 cpuslocked 계열 API를 사용하고, 문맥을 분명히 관리합니다.
enabled 상태도 0비용이라고 가정활성 상태에서는 JMP와 out-of-line 코드 진입 비용이 남습니다.Static Key의 강점은 주로 "disabled fast path 최소화"에 있다는 점을 기억합니다.

일반 인라인 어셈블리와 비교

특성일반 asm volatileasm goto
제어 흐름fall-through만C 레이블로 분기 가능
출력 오퍼랜드사용 가능GCC 14 미만: 불가
컴파일러 최적화표준 데이터 흐름제한적 (다중 경로)
사용 사례값 계산, 레지스터 접근조건 분기, Static Key
커널 사용 빈도매우 높음Static Key, error 경로

레지스터 심화

x86-64, ARM64, RISC-V의 레지스터 구조를 시스템 레지스터까지 포함하여 심층 정리합니다.

x86-64 GPR 완전 맵

x86-64 GPR 계층 (RAX 예시) RAX (64비트) — 비트 63:0 EAX (32비트) — 비트 31:0 AX (16비트) — 비트 15:0 AH (15:8) AL (7:0) 상위 32비트 (63:32) 주의: EAX 쓰기 시 상위 32비트 자동 0 클리어 AX/AH/AL 쓰기 시 상위 비트 유지 16개 GPR 인코딩 번호 (REX 확장 포함) 0=RAX 1=RCX 2=RDX 3=RBX 4=RSP 5=RBP 6=RSI 7=RDI 8=R8 9=R9 10=R10 11=R11 12=R12 13=R13 14=R14 15=R15 (REX 필요)

시스템 레지스터 CR/DR/MSR

레지스터용도핵심 비트
CR0시스템 제어PE(보호모드), PG(페이징), WP(쓰기보호), NE(x87 오류)
CR2Page Fault 주소마지막 #PF 발생 주소 (전체 64비트)
CR3페이지 테이블 베이스PML4/PGD 물리 주소, PCID (비트 11:0)
CR4확장 기능 제어PAE, PSE, PCIDE, SMEP, SMAP, FSGSBASE, OSXSAVE
CR8태스크 우선순위 (TPR)APIC 우선순위 레벨 (64비트 모드)
DR0-DR3하드웨어 브레이크포인트 주소4개 감시점 주소
DR6디버그 상태어떤 브레이크포인트가 트리거되었는지
DR7디버그 제어브레이크포인트 활성화, 조건(실행/쓰기/I-O/RW), 길이
/* 주요 MSR (Model Specific Register) */
/* IA32_EFER (0xC0000080): 확장 기능 활성 레지스터 */
/*   SCE(bit 0): SYSCALL 활성, LME(bit 8): Long Mode 활성 */
/*   NXE(bit 11): NX 비트 활성 */

/* IA32_LSTAR (0xC0000082): SYSCALL 진입점 */
/*   → entry_SYSCALL_64 주소 */

/* IA32_STAR (0xC0000081): SYSCALL/SYSRET 세그먼트 */
/*   [47:32]=커널 CS/SS, [63:48]=유저 CS/SS */

/* IA32_KERNEL_GS_BASE (0xC0000102): SWAPGS용 */
/*   커널 GS base ↔ 유저 GS base 교환 */

/* IA32_TSC_AUX (0xC0000103): RDTSCP 보조 */
/*   보통 CPU 번호 저장 */

SIMD 레지스터 맵

레지스터개수확장XSAVE 컴포넌트
XMM0-XMM15128비트16개SSE비트 1
YMM0-YMM15 (상위)256비트16개AVX비트 2
ZMM0-ZMM31512비트32개AVX-512비트 5,6,7
k0-k7 (opmask)64비트8개AVX-512비트 5
TMM0-TMM7 (타일)가변 (~1KB)8개AMX비트 17,18
MXCSR32비트1개SSE비트 1

ARM64 시스템 레지스터

레지스터EL용도핵심 비트
SCTLR_EL1EL1시스템 제어M(MMU), C(캐시), I(명령어캐시), WXN, SA
TCR_EL1EL1변환 제어T0SZ/T1SZ(가상주소폭), TG0/TG1(페이지크기), IPS
TTBR0_EL1EL1유저 페이지 테이블ASID + 페이지 테이블 물리 주소
TTBR1_EL1EL1커널 페이지 테이블커널 주소 공간 PGD
MAIR_EL1EL1메모리 속성8개 속성 인덱스 (Normal/Device/Non-cacheable)
ESR_EL1EL1예외 증후군EC(예외 클래스), ISS(명령어별 정보)
FAR_EL1EL1Fault 주소Data/Instruction Abort 주소
VBAR_EL1EL1벡터 테이블 주소예외 벡터 테이블 베이스 (2KB 정렬)
TPIDR_EL1EL1스레드 포인터 (커널)current task_struct 포인터
CNTVCT_EL0EL0가상 카운터시스템 타이머 (RDTSC 등가)

RISC-V CSR 완전 맵

주소 범위권한용도예시
0x000-0x0FFU-mode (읽기/쓰기)사용자 CSRustatus, fflags, frm, fcsr
0xC00-0xC1FU-mode (읽기 전용)사용자 카운터cycle, time, instret
0x100-0x1FFS-mode (읽기/쓰기)감독자 CSRsstatus, sie, stvec, sscratch, sepc, scause, stval, sip, satp
0x300-0x3FFM-mode (읽기/쓰기)머신 CSRmstatus, misa, mie, mtvec, mscratch, mepc, mcause, mip
0xF11-0xF14M-mode (읽기 전용)머신 IDmvendorid, marchid, mimpid, mhartid
/* RISC-V CSR 접근 */
static inline unsigned long csr_read(unsigned long csr)
{
    unsigned long val;
    asm volatile ("csrr %0, %1" : "=r"(val) : "i"(csr));
    return val;
}

/* 주요 S-mode CSR */
/* satp: 페이지 테이블 모드(Sv39/Sv48) + ASID + PPN */
/* sstatus: SIE(인터럽트활성), SPP(이전권한), SUM(유저접근) */
/* scause: 예외 원인 코드 (MSB: 인터럽트/예외 구분) */
ℹ️

RISC-V CSR 접근 명령어: csrr rd, csr(읽기), csrw csr, rs1(쓰기), csrrw(읽기+쓰기), csrrs(읽기+비트세트), csrrc(읽기+비트클리어). 즉시값 변형: csrrwi, csrrsi, csrrci (5비트 즉시값).

시스템 콜 ABI

x86_64 시스템 콜 레지스터 배치 (syscall / sysretq) 번호 & 반환값 rax 64비트 범용 레지스터 ▼ 입력: 시스템 콜 번호 (write(2), read(0), …) ▲ 출력: 반환값 (오류 시 -errno 음수) 인자 레지스터 (1 ~ 6번째) rdi 인자 1 (fd, ptr, ...) rsi 인자 2 (buf, ...) rdx 인자 3 (count, ...) r10 인자 4 ※ 함수 호출 시 rcx r8 인자 5 r9 인자 6 syscall이 손상하는 레지스터 rcx RIP(복귀주소) 저장 (sysretq 시 복원) r11 RFLAGS 저장 (sysretq 시 복원) 나머지 (rbx, rbp, r12~r15): 커널이 보존 (callee-saved) rsp: 커널 스택으로 교체됨 ARM64: x8=번호, x0-x5=인자 1-6, x0=반환값 | RISC-V: a7=번호, a0-a5=인자, a0=반환값

x86_64 시스템 콜 규약

/* x86_64 System V ABI - 시스템 콜 */
/*   syscall number: rax               */
/*   arguments:      rdi, rsi, rdx, r10, r8, r9 */
/*   return value:   rax                */
/*   clobbered:      rcx, r11           */

/* 시스템 콜 진입점 (entry_SYSCALL_64) */
SYM_CODE_START(entry_SYSCALL_64)
    swapgs                          /* GS base를 커널용으로 교체 */
    movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
    movq    PER_CPU_VAR(cpu_current_top_of_stack), %rsp
    pushq   $__USER_DS              /* user SS */
    pushq   PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* user RSP */
    pushq   %r11                    /* user RFLAGS */
    pushq   $__USER_CS              /* user CS */
    pushq   %rcx                    /* user RIP */
    /* ... */
SYM_CODE_END(entry_SYSCALL_64)

커널에서 자주 사용하는 어셈블리 패턴

커널에서 자주 사용하는 어셈블리 패턴 cmpxchg / LOCK lock; cmpxchgq %1, %2 원자적 비교·교환 (CAS) ZF=1 성공, ZF=0 실패 → atomic_cmpxchg(), mutex_lock() LOCK prefix: 버스 잠금 → SMP 원자성 pause asm volatile("pause" ::: "memory") 스핀 루프 전력 절약 힌트 파이프라인 flush → CPU 발열↓ → cpu_relax(), spin_until_cond() ARM64: yield 명령어 동일 효과 cli / sti asm volatile("cli" ::: "memory") cli: 인터럽트 비활성화 (IF=0) sti: 인터럽트 재활성화 (IF=1) → native_irq_disable/enable() ⚠ 커널 전용, 유저 공간 금지 rdtsc / rdtscp rdtsc → edx:eax = TSC 타임스탬프 카운터 읽기 rdtscp: 직렬화 포함 (권장) → sched_clock(), ktime_get() Invariant TSC: 주파수 불변 보장 cpuid cpuid → eax/ebx/ecx/edx CPU 기능/모델 식별 eax=leaf, ecx=sub-leaf 지정 → boot_cpu_has(X86_FEATURE_*) 직렬화 효과: 파이프라인 flush xchg / bts / btr xchgq %0, mem (암묵 LOCK) xchg: 교환 (항상 원자적) bts: bit test & set → test_and_set_bit() CF=이전 비트값 (결과 확인) ud2 / BUG() asm volatile("ud2") 의도적 정의 미설정 명령어 #UD 예외 → 커널 패닉 유발 → BUG(), BUG_ON(cond) 커널 소스 위치 자동 추적 가능 int3 / nop / hlt asm volatile("int3") int3: 소프트웨어 중단점 (#BP) nop: 패딩 / 코드 홀 채움 hlt: CPU 정지 (idle loop) → kgdb, ftrace, kprobe 활용
/* cmpxchg: Compare and exchange (atomic) */
static inline unsigned long cmpxchg_local(
    volatile void *ptr, unsigned long old, unsigned long new)
{
    unsigned long prev;
    asm volatile ("cmpxchgq %2, %1"
        : "=a"(prev), "+m"(*(unsigned long *)ptr)
        : "r"(new), "0"(old)
        : "memory");
    return prev;
}

/* CLI/STI: 인터럽트 비활성화/활성화 */
static inline void native_irq_disable(void)
{
    asm volatile ("cli" ::: "memory");
}

static inline void native_irq_enable(void)
{
    asm volatile ("sti" ::: "memory");
}
⚠️

인라인 어셈블리에서 "memory" clobber를 빠뜨리면 컴파일러가 메모리 접근을 재정렬하여 미묘한 버그가 발생할 수 있습니다. 메모리를 수정하는 어셈블리에는 반드시 포함하세요.

Atomic 연산 어셈블리 심화

커널의 모든 동기화 기본 요소(spinlock, atomic_t, refcount_t, bit 연산)는 궁극적으로 어셈블리 원자 명령어에 의존합니다. LOCK 접두사, CMPXCHG 루프, XADD, 128비트 CMPXCHG16B 등의 동작을 정확히 이해해야 합니다.

x86_64 Atomic 연산 어셈블리 패턴 LOCK CMPXCHG (CAS) EAX = expected 값 lock cmpxchg %ecx,(%rdx) if (*rdx == eax) { *rdx = ecx; ZF=1 } else { eax = *rdx; ZF=0 } → spinlock, atomic_cmpxchg LOCK XADD (Fetch-and-Add) lock xadd %eax,(%rdx) temp = *rdx; *rdx = *rdx + eax; eax = temp; /* 이전값 반환 */ → atomic_fetch_add() → 루프 없이 단일 명령어! → rwsem, refcount CMPXCHG16B (128비트 CAS) RDX:RAX = expected (128b) RCX:RBX = new value (128b) lock cmpxchg16b (%rdi) 128비트 비교 + 교환 → ABA 문제 해결 → 128비트 포인터+카운터 → 락프리 큐, percpu_ref Bit 연산 (BTS/BTR/BTC) lock bts $3, (%rdi) CF = bit3; bit3 = 1 lock btr $3, (%rdi) CF = bit3; bit3 = 0 lock btc $3, (%rdi) CF = bit3; bit3 ^= 1 → set_bit/clear_bit/test_and_set_bit → 비트맵, 플래그, spinlock CMPXCHG 루프 패턴 (범용 atomic RMW) old = atomic_read(v); do { new = old OP operand; } while (!atomic_try_cmpxchg(v, &old, new)); try_cmpxchg: ZF 기반 → 불필요한 비교 제거 실패 시 old 자동 갱신 → mov 제거 커널 6.1+: cmpxchg() 대신 try_cmpxchg() 선호 → 코드 더 작고 분기 예측에 유리

atomic_t 연산 구현

/* arch/x86/include/asm/atomic.h — atomic_t 핵심 연산 */

/* atomic_read: 단순 volatile 읽기 (자연 정렬 = 원자적) */
static inline int arch_atomic_read(const atomic_t *v)
{
    return __READ_ONCE(v->counter);  /* volatile 접근 */
}

/* atomic_add: LOCK ADD */
static inline void arch_atomic_add(int i, atomic_t *v)
{
    asm volatile (LOCK_PREFIX "addl %1,%0"
                 : "+m" (v->counter)
                 : "ir" (i)
                 : "memory");
}

/* atomic_fetch_add: LOCK XADD — 이전값 반환 */
static inline int arch_atomic_fetch_add(int i, atomic_t *v)
{
    return xadd(&v->counter, i);  /* lock xadd */
}

/* atomic_cmpxchg: LOCK CMPXCHG */
static inline int arch_atomic_cmpxchg(atomic_t *v, int old, int new)
{
    return cmpxchg(&v->counter, old, new);
}

/* try_cmpxchg: ZF 기반 (커널 6.1+ 선호 패턴) */
static inline bool arch_atomic_try_cmpxchg(atomic_t *v, int *old, int new)
{
    return try_cmpxchg(&v->counter, old, new);
    /* 성공: true, *old 불변 */
    /* 실패: false, *old = 현재값 (eax에서 자동 갱신) */
}

/* cmpxchg 매크로의 인라인 어셈블리 */
#define __raw_cmpxchg(ptr, old, new, size, lock) ({     \
    __typeof__(*(ptr)) __ret;                               \
    __typeof__(*(ptr)) __old = (old);                       \
    __typeof__(*(ptr)) __new = (new);                       \
    asm volatile (lock "cmpxchg" #size " %[new], %[ptr]" \
                 : "=a" (__ret), [ptr] "+m" (*(ptr))     \
                 : [new] "r" (__new), "0" (__old)          \
                 : "memory");                               \
    __ret;                                                   \
})

Ticket Spinlock / Queued Spinlock 어셈블리

/* 고전적 Ticket Spinlock (개념적 구현) */
/* owner|next가 16비트씩 합쳐진 32비트 값 */
arch_spin_lock:
    movl    $0x00010000, %eax     /* next 필드에 1 추가 */
    lock xadd %eax, (%rdi)        /* fetch_add: eax = 이전값 */
    movzwl  %ax, %ecx             /* owner = 하위 16비트 */
    shrl    $16, %eax             /* ticket = 상위 16비트 */
    cmpl    %eax, %ecx
    je      .Lgot_lock            /* 내 ticket == owner → 획득 */
.Lspin:
    pause                          /* CPU 힌트: 스핀 대기 */
    movzwl  (%rdi), %ecx          /* owner 폴링 */
    cmpl    %eax, %ecx
    jne     .Lspin
.Lgot_lock:
    RET

/* Queued Spinlock (MCS 기반, 현재 커널 기본) */
/* arch/x86/include/asm/qspinlock.h */
/* 4바이트: locked:pending:tail (8:8:16 비트) */
/* Fast path: LOCK CMPXCHG로 locked=0 → locked=1 */
/* Slow path: MCS 큐에 노드 추가 → queued_spin_lock_slowpath() */

LOCK 접두사 인코딩과 메모리 순서

LOCK 접두사(0xF0)는 특정 명령어에만 사용할 수 있으며, 메모리 순서에 중요한 영향을 미칩니다.

LOCK 가능 명령어동작커널 사용처
ADD, SUB, ADC, SBB산술 연산atomic_add, atomic_sub
AND, OR, XOR논리 연산atomic_and, atomic_or
INC, DEC, NEG, NOT단항 연산atomic_inc, atomic_dec
XCHG교환 (암묵적 LOCK)xchg(), spinlock
CMPXCHG, CMPXCHG8B/16B비교 후 교환atomic_cmpxchg, cmpxchg
XADD교환 후 덧셈atomic_fetch_add
BTS, BTR, BTC비트 테스트+변경test_and_set_bit
/* LOCK의 메모리 순서 보장 */
/* LOCK 접두사가 있는 명령어는 다음을 보장:
 * 1. Full memory barrier (read+write 양방향)
 * 2. 원자적 read-modify-write
 * 3. 캐시 라인 단위 잠금 (MESI 프로토콜)
 *
 * 따라서 LOCK ADD는 MFENCE와 동등한 순서 보장:
 * → smp_mb() 구현에 LOCK ADD 사용 (MFENCE보다 빠름) */

/* arch/x86/include/asm/barrier.h */
#define __smp_mb()  asm volatile("lock; addl $0,-4(%%rsp)" ::: "memory", "cc")
/* 스택 최상위에 0을 더함 (무효 연산) → 부작용은 full barrier */

/* 허용되지 않는 명령어에 LOCK 사용 시 → #UD 예외 */
/* 예: LOCK MOV → Undefined Opcode */
⚠️

LOCK vs MFENCE 성능: LOCK ADD $0, -4(%rsp)MFENCE보다 일반적으로 빠릅니다. MFENCE는 모든 이전 로드/스토어의 가시성을 보장하지만, 파이프라인을 더 많이 정지시킵니다. 커널은 smp_mb()에서 LOCK ADD를 사용합니다. 단, LFENCE는 투기적 실행 직렬화 용도로도 사용됩니다(Spectre 완화).

Per-CPU 변수 접근 어셈블리

Linux 커널의 Per-CPU 변수는 캐시 경합 없이 CPU별 데이터를 관리합니다. x86_64에서는 %gs 세그먼트 베이스를 활용하여 단일 명령어로 현재 CPU의 데이터에 접근합니다.

Per-CPU 변수 접근 메커니즘 x86_64: GS 세그먼트 기반 MSR_GS_BASE → Per-CPU 영역 시작 mov %gs:var_offset, %rax = *(GS_BASE + var_offset) → 단일 명령어, LOCK 불필요 swapgs: 유저↔커널 GS 교환 wrmsr(MSR_GS_BASE, addr) ARM64: TPIDR_EL1 기반 TPIDR_EL1 → Per-CPU 오프셋 mrs x0, tpidr_el1 ldr x1, [x0, #var_offset] → 2 명령어 (x86보다 1개 더) RISC-V: tp (x4) 레지스터 사용 lw a0, var_offset(tp) Per-CPU 메모리 레이아웃 CPU 0 영역 GS_BASE[0] CPU 1 영역 GS_BASE[1] CPU 2 영역 GS_BASE[2] ... CPU N 영역 GS_BASE[N] 각 영역 동일 레이아웃: current_task, irq_count, softirq_pending, runqueues, ... __per_cpu_offset[cpu] = GS_BASE[cpu] — 링커 심볼 __per_cpu_start

Per-CPU 연산 인라인 어셈블리

/* arch/x86/include/asm/percpu.h — this_cpu_* 연산 */

/* this_cpu_read: GS 세그먼트로 직접 읽기 */
#define this_cpu_read_8(pcp) ({                          \
    u64 __val;                                            \
    asm volatile ("movq %%gs:%1, %0"                    \
                 : "=r" (__val)                            \
                 : "m" (pcp));                              \
    __val;                                                  \
})

/* this_cpu_add: GS 세그먼트 + 원자적 덧셈 (IRQ 안전) */
#define this_cpu_add_8(pcp, val) do {                     \
    asm volatile ("addq %1, %%gs:%0"                    \
                 : "+m" (pcp)                              \
                 : "re" ((u64)(val)));                    \
} while (0)

/* this_cpu_cmpxchg: GS 세그먼트 + CMPXCHG */
#define this_cpu_cmpxchg_8(pcp, old, new) ({             \
    u64 __ret;                                            \
    asm volatile ("cmpxchgq %2, %%gs:%1"               \
                 : "=a" (__ret), "+m" (pcp)               \
                 : "r" ((u64)(new)), "0" ((u64)(old))     \
                 : "memory");                               \
    __ret;                                                  \
})

/* 주요 Per-CPU 변수 (커널에서 가장 자주 접근) */
/* current_task    — 현재 실행 중인 태스크 */
/* cpu_number      — 현재 CPU 번호 */
/* irq_count       — 인터럽트 중첩 카운터 */
/* __preempt_count — 선점 카운터 */
/* cpu_tlbstate     — TLB 상태 */
/* gdt_page        — Per-CPU GDT */

/* 'current' 매크로 = this_cpu_read(current_task) */
/* → movq %gs:current_task, %rax (단일 명령어!) */
💡

this_cpu_* vs __this_cpu_*: this_cpu_*는 선점 비활성화를 자동 수행하고, __this_cpu_*는 호출자가 이미 선점을 비활성화했다고 가정합니다. 선점 가능 구간에서 __this_cpu_*를 사용하면 다른 CPU에서 재개되어 잘못된 CPU의 데이터를 접근할 수 있습니다. x86에서는 __this_cpu_*%gs 세그먼트를 사용하므로 동작은 하지만, 의미적으로 잘못된 결과를 얻게 됩니다.

AT&T vs Intel 문법 비교

x86 어셈블리에는 두 가지 주요 문법이 있습니다. Linux 커널과 GCC는 기본적으로 AT&T 문법을 사용하지만, Intel 문법도 지원합니다. 두 문법의 핵심 차이를 이해하는 것이 중요합니다.

특성AT&T 문법 (GAS 기본)Intel 문법 (NASM 등)
오퍼랜드 순서src, dst (소스 먼저)dst, src (목적지 먼저)
레지스터 접두사%rax, %eaxrax, eax
즉시값 접두사$42, $0xff42, 0ffh
크기 접미사movl, movq, movbmov DWORD, mov QWORD
메모리 참조disp(%base,%index,scale)[base + index*scale + disp]
간접 점프*%raxjmp rax
AT&T 문법 (GAS · Linux 커널 기본) vs Intel 문법 (NASM · GCC -masm=intel) movq %rsi, %rdi src 먼저 · %=레지스터 접두사 · q=64-bit mov rdi, rsi dst 먼저 · 접두사 없음 · 크기 자동 추론 순서 ↕ addl $1, %eax $=즉시값 접두사 · %=레지스터 접두사 · l=32-bit add eax, 1 접두사 없음 · 크기는 피연산자 크기로 추론 $% 제거 movl 8(%rbp), %eax disp(%base) 형식 · 스택에서 32-bit 로드 mov eax, [rbp+8] [base+disp] 형식 · 대괄호로 메모리 표현 ()→[] leaq (%rdi,%rsi,4), %rax (%base,%idx,scale) · rax = rdi + rsi×4 lea rax, [rdi+rsi*4] [base+idx*scale] · 수식 형태로 표기 ()→[] cmpq $0, (%rdi) q 접미사=64-bit · (%rdi)=포인터 역참조 cmp QWORD PTR [rdi], 0 QWORD PTR=64-bit 명시 · [rdi]=메모리 참조 크기자 ① 오퍼랜드 순서 AT&T: op src, dst Intel: op dst, src 방향이 정반대 — 변환 시 반드시 주의 b=byte w=word l=dword q=qword (접미사는 AT&T만 필수) ② 레지스터·즉시값 AT&T: %rax $42 Intel: rax 42 % = 레지스터 접두사 (AT&T 전용) $ = 즉시값 접두사 (AT&T 전용) Intel은 문맥으로 구분 ③ 메모리 참조 AT&T: d(%b,%i,s) Intel: [b+i*s+d] d=변위 b=베이스 i=인덱스 s=스케일 예: 8(%rbp,%rsi,4) → [rbp+rsi*4+8] ④ 크기 지정자 AT&T: movb/w/l/q Intel: BYTE/WORD/DWORD/QWORD PTR AT&T: 명령어 접미사로 크기 결정 Intel: 메모리 오퍼랜드에 PTR 명시 크기 명확할 때는 생략 가능
ℹ️

GCC에서 Intel 문법을 사용하려면 -masm=intel 옵션을 지정하거나, 인라인 어셈블리 내에서 .intel_syntax noprefix 지시자를 사용합니다. 그러나 커널 코드는 AT&T 문법이 표준이므로 AT&T에 익숙해지는 것이 중요합니다.

x86 명령어 인코딩

커널 어셈블리를 디버깅하거나 바이너리 패칭(ALTERNATIVE, static key)을 이해하려면 x86 명령어의 바이트 수준 인코딩을 알아야 합니다. REX, VEX, EVEX 접두사와 ModRM/SIB 바이트 구조를 이해하면 objdump 출력을 정확히 해석할 수 있습니다.

x86-64 명령어 인코딩 구조 레거시 접두사 0-4 바이트 REX 접두사 0-1 바이트 Opcode 명령어 코드 1-3 바이트 ModRM 주소 모드 0-1 바이트 SIB 스케일/인덱스 0-1 바이트 Disp 변위 0/1/2/4 바이트 Imm 즉시값 0/1/2/4/8 바이트 최대 15B REX 접두사 (0100 WRXB) bit 3 (W): 1=64비트 오퍼랜드 크기 bit 2 (R): ModRM reg 필드 확장 (R8-R15) bit 1 (X): SIB index 필드 확장 bit 0 (B): ModRM r/m 또는 SIB base 확장 ModRM 바이트 (mod:reg:r/m) bit 7:6 (mod): 00=메모리, 01=[+disp8], 10=[+disp32], 11=레지스터 bit 5:3 (reg): 레지스터 또는 opcode 확장 bit 2:0 (r/m): 메모리/레지스터 (100=SIB) VEX 접두사 (AVX) 2바이트 VEX: C5 [RvvvvLpp] 3바이트 VEX: C4 [RXBmmmmm] [WvvvvLpp] vvvv: 추가 소스 레지스터 (3-오퍼랜드) L: 0=128비트(XMM), 1=256비트(YMM) EVEX 접두사 (AVX-512) 4바이트: 62 [P1] [P2] [P3] 32개 레지스터 (ZMM0-ZMM31) LL: 00=128, 01=256, 10=512비트 aaa: 마스크 레지스터 (k1-k7) 인코딩 예시 mov %rax,(%rbx) → 48 89 03 REX.W(48) + opcode(89) + ModRM(03: mod=00,reg=000,r/m=011) mov %r12,8(%rsp) → 4c 89 64 24 08 REX.WR(4c) + 89 + ModRM(64) + SIB(24: rsp) + disp8(08) lock cmpxchg → f0 48 0f b1 0b LOCK(f0) + REX.W(48) + 0f b1(opcode) + ModRM(0b) nop (5B) → 0f 1f 44 00 00 nopl 0(%rax,%rax,1) — static key 패칭용

레거시 접두사

그룹접두사 바이트의미커널 사용 예
그룹 1F0LOCK — 원자적 메모리 연산lock cmpxchg, lock xadd
그룹 1F2REPNE/REPNZrep ret (AMD 분기 예측 힌트)
그룹 1F3REP/REPE/REPZrep movsb (memcpy ERMS)
그룹 22E/3E/26/64/65세그먼트 오버라이드%gs:0x28 (스택 카나리)
그룹 366오퍼랜드 크기 전환32→16비트 오퍼랜드
그룹 467주소 크기 전환64→32비트 주소

LOCK 접두사와 버스 동작

/* LOCK 접두사의 하드웨어 동작 */
/*                                                                     */
/* 현대 CPU (Intel Nehalem+):                                          */
/*   캐시 라인이 단일 코어에 있으면 → 캐시 락 (MESI Exclusive/Modified) */
/*   캐시 라인을 넘거나 UC 메모리 → 버스 락 (전체 메모리 접근 차단)     */
/*                                                                     */
/* LOCK은 암묵적 full memory barrier (mfence와 동등)                   */

/* LOCK 허용 명령어 (이외 명령어에 LOCK 사용 시 #UD 예외) */
/* ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCHG8B/16B, */
/* DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, XCHG     */

/* XCHG는 암묵적 LOCK (접두사 불필요) */
xchg %rax, (%rbx)            /* 항상 atomic, 항상 full barrier */

/* 커널에서의 LOCK 사용 */
static inline void atomic_inc(atomic_t *v)
{
    asm volatile (LOCK_PREFIX "incl %0"
                 : "+m" (v->counter));
}

/* SMP에서만 LOCK: UP에서는 nop */
#ifdef CONFIG_SMP
#define LOCK_PREFIX "lock ; "
#else
#define LOCK_PREFIX ""
#endif

NOP 인코딩과 패칭

커널의 ALTERNATIVE, static key, ftrace 모두 특정 길이의 NOP으로 코드를 채우고 런타임에 패칭합니다. Intel은 다양한 길이의 NOP 인코딩을 제공합니다.

길이인코딩 (hex)어셈블리사용처
1B90nop일반 NOP
2B66 90xchg %ax,%ax단순 패딩
3B0f 1f 00nopl (%rax)ALTERNATIVE 기본
4B0f 1f 40 00nopl 0(%rax)endbr64 대체
5B0f 1f 44 00 00nopl 0(%rax,%rax,1)static key, ftrace
6B66 0f 1f 44 00 00nopw 0(%rax,%rax,1)정렬 패딩
7B0f 1f 80 00 00 00 00nopl 0(%rax)긴 패딩
8B0f 1f 84 00 00 00 00 00nopl 0(%rax,%rax,1)긴 패딩
ℹ️

명령어 길이 제한: x86 명령어는 최대 15바이트입니다. 이를 초과하면 #UD(Undefined Opcode) 예외가 발생합니다. 커널의 명령어 디코더(arch/x86/lib/insn.c)는 이 규칙을 따르며, insn_decode()로 임의 위치의 명령어를 파싱할 수 있습니다.

REX 접두사 상세 비트 레이아웃

REX 접두사(0x40-0x4F)는 64비트 모드에서 레지스터 확장과 64비트 오퍼랜드 크기를 위해 사용됩니다.

REX 접두사 비트 레이아웃 (0100 WRXB) 0100 비트 7:4 (고정) W 비트 3: 64비트 크기 R 비트 2: ModRM.reg 확장 X 비트 1: SIB.index 확장 B 비트 0: ModRM.rm/SIB.base W=0: 기본 오퍼랜드 크기 (32비트) W=1: 64비트 오퍼랜드 크기 R: reg 필드를 4비트로 확장 (R8-R15 접근) X: SIB index를 4비트로 확장 B: rm/base를 4비트로 확장
REX 바이트WRXB의미예시
0x400000REX만 (SPL/BPL/SIL/DIL 접근)mov %spl, %al
0x410001B=1: r/m 확장mov %r8d, %eax
0x440100R=1: reg 확장mov %eax, %r8d
0x481000W=1: 64비트mov %rax, %rbx
0x491001W+B: 64비트 + rm 확장mov %rax, %r8
0x4C1100W+R: 64비트 + reg 확장mov %r8, %rax
0x4D1101W+R+B: 양쪽 모두 R8+mov %r8, %r9

VEX 접두사 2/3바이트 상세

VEX(Vector Extensions) 접두사는 AVX 명령어에 사용되며, 레거시 접두사(66/F2/F3)와 REX를 대체합니다.

형식바이트필드사용 조건
2바이트 VEXC5 [RvvvvLpp]R̃, vvvv, L, pp0F 맵, REX.X=0, REX.B=0
3바이트 VEXC4 [R̃X̃B̃mmmmm] [WvvvvLpp]R̃X̃B̃, mmmmm, W, vvvv, L, pp모든 경우
/* VEX 필드 설명:
 * pp: 00=없음, 01=66, 10=F3, 11=F2 (레거시 접두사 대체)
 * mmmmm: 00001=0F, 00010=0F38, 00011=0F3A (opcode 맵)
 * vvvv: 추가 소스 레지스터 (비파괴적 3-operand)
 * L: 0=128비트(XMM), 1=256비트(YMM)
 * W: 0=32비트, 1=64비트 (opcode에 따라 다른 의미)
 * R̃X̃B̃: REX.RXB의 반전 (1의 보수) */

/* 예: vaddpd %ymm2, %ymm1, %ymm0 */
/* C5 F5 58 C2 */
/* C5: 2-byte VEX prefix */
/* F5: R̃=1(reg=ymm0), vvvv=1110(~ymm1=ymm1), L=1(256bit), pp=01(66) */
/* 58: ADDPD opcode */
/* C2: ModRM: mod=11 reg=000(ymm0) rm=010(ymm2) */

EVEX 4바이트 접두사 상세

EVEX(Enhanced VEX)는 AVX-512 명령어에 사용되며, 마스킹, 브로드캐스트, 512비트 벡터를 지원합니다.

바이트비트필드설명
P0 (0x62 다음)7REX.R 반전
6REX.X 반전
5REX.B 반전
4R̃'REX.R 상위 확장 (ZMM16-31)
P17W오퍼랜드 크기
6:3vvvv추가 소스 레지스터
2:0pp레거시 접두사 대체
P27z제로 마스킹 (1=zero, 0=merge)
6:5L'L벡터 길이 (00=128, 01=256, 10=512)
4b브로드캐스트 / 라운딩 / SAE
2:0aaa마스크 레지스터 (k0-k7)
ℹ️

APX (Advanced Performance Extensions): Intel의 차세대 확장 APX는 EVEX를 재활용하여 GPR을 32개(R16-R31)로 확장하고, 3-operand 비파괴적 형식, 새로운 조건부 명령어(CCMP, CTEST), NDD(New Data Destination) 등을 추가합니다. 커널 지원은 향후 추가될 예정입니다.

Opcode 맵 (1/2/3바이트)

Opcode명령어커널 사용처
89 /rMOV r/m, r1바이트레지스터 이동
8B /rMOV r, r/m1바이트레지스터 이동
01 /rADD r/m, r1바이트산술 연산
39 /rCMP r/m, r1바이트비교
E8 cdCALL rel321바이트함수 호출
C3RET1바이트함수 복귀
E9 cdJMP rel321바이트무조건 점프
FF /2CALL r/m641바이트간접 호출
0F 05SYSCALL2바이트 (0F)시스템 콜 진입
0F 07SYSRET2바이트 (0F)시스템 콜 복귀
0F B0 /rCMPXCHG r/m8, r82바이트 (0F)atomic CAS
0F B1 /rCMPXCHG r/m, r2바이트 (0F)atomic CAS
0F 38 F1 /rCRC323바이트 (0F38)체크섬
0F 3A 0F /rPALIGNR3바이트 (0F3A)SIMD 정렬

인코딩 실전 연습

커널에서 자주 사용하는 명령어 5개를 바이트 수준에서 분석합니다.

/* 1. mov %rax, %rbx → 48 89 C3 */
/*    48: REX.W (64비트 오퍼랜드) */
/*    89: MOV r/m, r opcode */
/*    C3: ModRM = 11 000 011 → mod=11(reg) reg=000(rax) rm=011(rbx) */

/* 2. lock cmpxchg %rcx, (%rdx) → F0 48 0F B1 0A */
/*    F0: LOCK 접두사 */
/*    48: REX.W (64비트) */
/*    0F B1: CMPXCHG opcode */
/*    0A: ModRM = 00 001 010 → mod=00(mem) reg=001(rcx) rm=010(rdx) */

/* 3. movq $0x12345678, %rdi → 48 C7 C7 78 56 34 12 */
/*    48: REX.W (64비트) */
/*    C7: MOV r/m, imm32 opcode */
/*    C7: ModRM = 11 000 111 → mod=11(reg) reg=000(/0) rm=111(rdi) */
/*    78 56 34 12: imm32 (리틀 엔디안, 부호 확장 → 64비트) */

/* 4. addl $1, %gs:0x10(%rbx) → 65 83 43 10 01 */
/*    65: GS 세그먼트 접두사 (Per-CPU) */
/*    83: ADD r/m, imm8 opcode */
/*    43: ModRM = 01 000 011 → mod=01(mem+disp8) reg=000(/0) rm=011(rbx) */
/*    10: disp8 = 0x10 */
/*    01: imm8 = 1 */

/* 5. jmp *%rax → FF E0 */
/*    FF: JMP r/m64 opcode */
/*    E0: ModRM = 11 100 000 → mod=11(reg) reg=100(/4 JMP) rm=000(rax) */
/*    참고: 64비트 모드에서 REX.W 불필요 (기본 64비트) */
💡

인코딩 검증: echo "mov %rax, %rbx" | as -o /dev/null -al로 어셈블러 출력을 확인하거나, objdump -d의 왼쪽 16진수 열을 보면 인코딩을 직접 확인할 수 있습니다. ndisasm이나 cstool x64로 역방향(바이트→명령어) 디코딩도 가능합니다.

GNU Assembler (GAS) 지시자

GNU Assembler는 Linux 커널의 어셈블리 파일(.S)을 처리합니다. 자주 사용되는 GAS 지시자를 이해해야 커널의 어셈블리 코드를 읽을 수 있습니다. 지시자 전체 참조와 CFI 지시자, 커맨드라인 옵션, 아키텍처별 확장은 GNU Assembler (as) 완전 가이드를 참고하세요.

GNU Assembler (GAS) 지시자 분류 섹션 지시자 .section .text 코드 섹션 (실행 가능) .section .bss 미초기화 데이터 .init.text 초기화 코드 (부팅 후 해제) .pushsection 섹션 스택에 임시 전환 .section .data 초기화 데이터 .section .rodata 읽기 전용 데이터 .section "flags" a=할당 x=실행 w=쓰기 .popsection 이전 섹션으로 복귀 심볼과 정렬 .global sym 전역 심볼 선언 .type sym, @function 심볼 타입 지정 .align N N바이트 경계 정렬 .p2align N 2^N 바이트 정렬 .local sym 로컬 심볼 (비공개) .size sym, .-sym 심볼 크기 자동 계산 .balign N 강제 경계 정렬 .weak sym 약한 참조 심볼 매크로와 조건부 어셈블리 .macro name [args] 매크로 정의 시작 .endm 매크로 정의 종료 .if expr / .else / .endif 조건부 어셈블리 .rept N / .endr N회 반복 실행 \arg 매크로 인자 참조 arg:req / arg=val 필수 / 기본값 인자 .ifdef / .ifndef 심볼 정의 조건 .irp var, list / .endr 리스트 반복 데이터 지시자 .byte / .word 1 / 2바이트 상수 .long / .quad 4 / 8바이트 상수 .ascii "str" 문자열 (NUL 없음) .fill N, S, V N개 S바이트를 V로 채움 .asciz / .string NUL 종료 문자열 .float / .double IEEE 부동소수점 .space N N바이트 공간 확보 .set sym, expr 심볼값 정의 SYM_* 커널 매크로 (include/linux/linkage.h) SYM_FUNC_START(name) C ABI 함수 시작 선언 SYM_FUNC_END(name) .global + .type @function + .size SYM_CODE_START(name) 비표준 호출 규약 코드 시작 SYM_CODE_END(name) 커널 진입점·예외 핸들러 등 SYM_DATA_START(name) 전역 데이터 심볼 시작 SYM_DATA_END(name) .global + .type @object + .size

섹션 지시자

.section .text            /* 코드 섹션 (실행 가능) */
.section .data            /* 초기화된 데이터 */
.section .bss             /* 초기화되지 않은 데이터 */
.section .rodata          /* 읽기 전용 데이터 */
.section .init.text, "ax" /* 초기화 코드 (부팅 후 해제) */

/* 커널 전용 섹션 예시 */
.pushsection .altinstructions, "a"   /* 대안 명령어 섹션 */
.popsection                          /* 이전 섹션으로 복귀 */

심볼과 정렬

.global  my_function      /* 전역 심볼 선언 */
.local   helper_func      /* 로컬 심볼 (외부 비공개) */
.type    my_function, @function  /* 함수 타입 정보 */
.size    my_function, . - my_function  /* 함수 크기 */

.align   16               /* 16바이트 정렬 */
.balign  4096             /* 4096바이트 경계 정렬 */
.p2align 4                /* 2^4 = 16바이트 정렬 */

매크로와 조건부 어셈블리

/* 매크로 정의 */
.macro SAVE_REGS
    pushq   %rbp
    pushq   %rbx
    pushq   %r12
    pushq   %r13
    pushq   %r14
    pushq   %r15
.endm

.macro RESTORE_REGS
    popq    %r15
    popq    %r14
    popq    %r13
    popq    %r12
    popq    %rbx
    popq    %rbp
.endm

/* 매크로 매개변수 */
.macro PUSH_AND_CLEAR reg:req
    pushq   \reg
    xorq    \reg, \reg
.endm

/* 조건부 어셈블리 */
.if CONFIG_X86_64
    movq    %rsp, %rdi
.else
    movl    %esp, %eax
.endif

/* 반복 */
.rept 8
    nop                      /* 8번 반복 */
.endr

데이터 지시자

.byte    0x90             /* 1바이트 */
.word    0x1234           /* 2바이트 */
.long    0x12345678       /* 4바이트 */
.quad    0x123456789abcdef0 /* 8바이트 */
.ascii   "Hello"          /* 문자열 (NUL 없음) */
.asciz   "Hello"          /* NUL 종료 문자열 */
.fill    256, 1, 0        /* 256바이트를 0으로 채움 */
.space   4096             /* 4096바이트 공간 확보 */

/* 커널에서 자주 보는 SYM_* 매크로 (include/linux/linkage.h) */
SYM_FUNC_START(my_asm_func)     /* 함수 시작 선언 */
    /* ... 코드 ... */
    retq
SYM_FUNC_END(my_asm_func)       /* 함수 종료 + 크기 기록 */

SYM_CODE_START(entry_point)      /* 비표준 호출 규약 코드 */
SYM_CODE_END(entry_point)

SYM_DATA_START(my_data)          /* 데이터 심볼 */
    .long 42
SYM_DATA_END(my_data)

CFI 지시자 (DWARF 언와인드 정보)

CFI(Call Frame Information) 지시자는 DWARF 언와인드 정보를 생성합니다. 스택 트레이스, 예외 처리, perf callchain에 필수입니다. 커널에서는 SYM_FUNC_START/SYM_FUNC_END가 내부적으로 CFI 지시자를 포함합니다. CFI 지시자 전체 참조(25개)는 GNU Assembler — CFI 지시자 섹션을 참고하세요.

.cfi_startproc              /* 함수 시작 — CFI 정보 블록 시작 */
    pushq   %rbp
    .cfi_def_cfa_offset 16  /* CFA = rsp + 16 */
    .cfi_offset %rbp, -16   /* saved rbp는 CFA-16 위치 */
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp  /* CFA 기준 레지스터를 rbp로 변경 */
    /* ... 함수 본문 ... */
    leave
    .cfi_def_cfa %rsp, 8    /* 복귀: CFA = rsp + 8 */
    ret
.cfi_endproc                /* CFI 정보 블록 종료 */

.altmacro 모드

.altmacro는 GAS의 대체 매크로 모드로, 문자열 조작과 수식 평가 기능을 확장합니다.

.altmacro

/* LOCAL: 고유 레이블 생성 */
.macro SAVE_REG reg
    LOCAL label_done
    push \reg
    test \reg, \reg
    jz label_done
    /* ... 추가 처리 ... */
label_done:
.endm

/* 꺾쇠 괄호 문자열과 수식 */
.macro ENTRY_N n
    .globl entry_\n
    entry_\n:
    push %(8*\n)(%rsp)     /* 수식 평가: offset = 8*n */
.endm

/* % 연산자로 수식을 텍스트로 변환 */
.macro DEFINE_ISR num
    SYM_CODE_START(isr_%num)
    pushq $%num              /* 즉시값으로 벡터 번호 */
    jmp common_interrupt
    SYM_CODE_END(isr_%num)
.endm

.irp / .irpc 반복 지시자

커널은 .irp.irpc로 레지스터 저장/복원 등 반복 코드를 생성합니다.

/* .irp: 값 목록 반복 */
.irp reg, rax, rbx, rcx, rdx, rsi, rdi, r8, r9, r10, r11, r12, r13, r14, r15
    pushq %\reg
.endr

/* 커널 entry 코드에서 pt_regs 저장에 활용 */
/* arch/x86/entry/calling.h */
.macro PUSH_AND_CLEAR_REGS rdx=%rdx rcx=%rcx rax=%rax save_ret=0
    pushq %rdi
    pushq %rsi
    pushq \rdx
    pushq \rcx
    pushq \rax
    pushq %r8
    pushq %r9
    pushq %r10
    pushq %r11
    pushq %rbx
    pushq %rbp
    pushq %r12
    pushq %r13
    pushq %r14
    pushq %r15
    /* XOR로 레지스터 클리어 (정보 누출 방지) */
    xorl %edx, %edx
    xorl %ecx, %ecx
    ...
.endm

조건부 어셈블리 심화

커널 어셈블리 파일은 C 전처리기와 GAS 조건부 지시자를 모두 활용합니다.

/* C 전처리기 (#ifdef) — .S 파일은 cpp 통과 */
#ifdef CONFIG_SMP
    lock
#endif
    addl $1, (%rdi)

/* GAS 조건부 어셈블리 */
.ifdef HAVE_FEATURE_X
    /* 피처 X 활용 코드 */
.else
    /* 폴백 코드 */
.endif

/* .if: 수식 기반 조건 */
.if (\size == 8)
    movq (%rsi), %rax
.elseif (\size == 4)
    movl (%rsi), %eax
.elseif (\size == 2)
    movw (%rsi), %ax
.else
    movb (%rsi), %al
.endif

/* .ifc: 문자열 비교 */
.ifc \reg, rsp
    .error "RSP는 사용할 수 없습니다"
.endif
💡

.S vs .s: 커널의 어셈블리 파일은 .S 확장자를 사용하여 C 전처리기(cpp)를 먼저 통과합니다. #include, #define, #ifdef 등 C 전처리 지시자를 사용할 수 있어 커널 설정(CONFIG_*)에 따른 조건부 코드가 가능합니다. 소문자 .s는 전처리 없이 직접 어셈블됩니다.

CFI와 스택 해제

CFI(Call Frame Information) 지시자는 DWARF 디버깅 정보의 핵심으로, 스택 트레이스와 예외 처리에 필수입니다. 커널은 DWARF CFI 외에 자체적인 ORC 언와인더를 사용합니다.

DWARF CFI 지시자 상세

지시자동작사용 시점
.cfi_startprocCFI 블록 시작 (FDE 생성)함수 시작
.cfi_endprocCFI 블록 종료함수 끝
.cfi_def_cfa reg, offCFA = reg + off프레임 포인터 설정 후
.cfi_def_cfa_offset offCFA 오프셋만 변경PUSH/SUB RSP 후
.cfi_def_cfa_register regCFA 레지스터만 변경MOV RSP,RBP 후
.cfi_offset reg, offreg이 CFA+off에 저장됨PUSH 후
.cfi_register old, newold 값이 new에 있음레지스터 이동 후
.cfi_restore regreg 복원됨 (이전 규칙)POP 후
.cfi_remember_state현재 CFI 상태 스택에 저장분기 전
.cfi_restore_state저장된 CFI 상태 복원분기 합류점
.cfi_adjust_cfa_offset nCFA 오프셋 += n동적 스택 조정
.cfi_signal_frame시그널 프레임 표시시그널 핸들러 진입
.cfi_undefined regreg 값 미정의파괴된 레지스터
/* 전형적인 함수 프롤로그/에필로그 CFI */
my_function:
.cfi_startproc
    /* 초기 CFA: RSP + 8 (복귀 주소) */
    pushq %rbp
    .cfi_def_cfa_offset 16    /* CFA = RSP + 16 */
    .cfi_offset %rbp, -16     /* RBP가 CFA-16에 저장됨 */
    movq %rsp, %rbp
    .cfi_def_cfa_register %rbp /* CFA = RBP + 16 */

    pushq %rbx
    .cfi_offset %rbx, -24     /* RBX가 CFA-24에 저장됨 */
    pushq %r12
    .cfi_offset %r12, -32     /* R12가 CFA-32에 저장됨 */
    subq $16, %rsp            /* 지역 변수 공간 */

    /* ... 함수 본문 ... */

    addq $16, %rsp
    popq %r12
    .cfi_restore %r12
    popq %rbx
    .cfi_restore %rbx
    popq %rbp
    .cfi_restore %rbp
    .cfi_def_cfa %rsp, 8
    ret
.cfi_endproc

ORC 언와인더

커널은 DWARF 대신 ORC(Oops Rewind Capability) 형식을 사용합니다. DWARF보다 빠르고 단순하며, objtool이 자동 생성합니다.

ORC vs DWARF 언와인더 DWARF (.eh_frame) • 가변 길이 인코딩 (LEB128) • 스택 머신 기반 표현식 • CIE + FDE 구조 (계층적) ✗ 파싱이 복잡하고 느림 ✗ 신뢰성 문제 (컴파일러 버그) ✗ 커널 인라인 asm 추적 어려움 데이터 크기: ~3-5MB (vmlinux) 유저스페이스/C 라이브러리 표준 ORC (.orc_unwind / .orc_unwind_ip) • 고정 크기 엔트리 (6바이트) • 단순 조회 (이진 검색) • objtool이 자동 생성 ✓ 빠른 언와인드 (~5배) ✓ 높은 신뢰성 ✓ 어셈블리 코드도 정확히 추적 데이터 크기: ~1-2MB (vmlinux) Linux 커널 전용 (4.14+)
/* ORC 엔트리 구조 — arch/x86/include/asm/orc_types.h */
struct orc_entry {
    s16 sp_offset;     /* CFA에서 SP까지 오프셋 */
    s16 bp_offset;     /* CFA에서 BP까지 오프셋 */
    unsigned sp_reg:4; /* CFA 기준 레지스터 (SP/BP/등) */
    unsigned bp_reg:4; /* BP 저장 위치 레지스터 */
    unsigned type:3;   /* CALL/REGS/IRET 타입 */
    unsigned signal:1; /* 시그널 프레임 여부 */
}; /* 총 6바이트 */

/* .orc_unwind_ip: IP → ORC 엔트리 매핑 (상대 주소, 4바이트씩)
 * .orc_unwind:    ORC 엔트리 배열 (6바이트씩)
 * → IP로 이진 검색 → 해당 ORC 엔트리 → CFA 계산 → 스택 해제 */
ℹ️

objtool: tools/objtool/은 커널 빌드 시 각 오브젝트 파일을 분석하여 ORC 데이터를 자동 생성합니다. 또한 스택 프레임 일관성 검증, 비정상 코드 패턴 경고, static call 사이트 검출 등도 수행합니다. CONFIG_UNWINDER_ORC=y(기본값)로 ORC를 사용합니다.

.eh_frame vs .debug_frame

항목.eh_frame.debug_frame
용도런타임 예외 처리 (C++ 등)디버거 전용
ELF 섹션 타입SHT_PROGBITS (로드됨)SHT_PROGBITS (비로드)
런타임 접근필요 (언와인드에 사용)불필요 (디버그 정보)
PT_GNU_EH_FRAME있음 (.eh_frame_hdr 연결)없음
크기실행 파일에 포함strip 시 제거 가능
커널 사용제한적 (모듈 로더)디버깅 시만

스택 해제 과정 단계별

ORC 기반 스택 해제 과정 1. IP 획득 현재 RIP 또는 스택의 복귀주소 2. ORC 조회 .orc_unwind_ip에서 이진 검색 3. CFA 계산 CFA = sp_reg + sp_offset 4. 레지스터 복원 RBP = *(CFA + bp_offset) 5. 다음 프레임 RSP = CFA RIP = *(CFA-8) 반복 (스택 바닥까지) 예시 스택 트레이스 출력: Call Trace: schedule+0x42/0x90 → __schedule+0x3a1/0x5c0 → schedule_idle+0x1e/0x40 → do_idle+0x15f/0x1d0 → cpu_startup_entry+0x19/0x20
💡

스택 트레이스 디버깅: ORC 언와인더는 dump_stack(), WARN(), BUG(), oops 메시지에서 호출됩니다. Call Trace: 다음의 ? 표시 프레임은 ORC가 추측한 것이고, ? 없는 프레임은 확실합니다. CONFIG_FRAME_POINTER=y와 달리 ORC는 -fno-omit-frame-pointer 없이도 동작하므로 성능 페널티가 없습니다.

x86_64 호출 규약 심화

Linux 커널은 x86_64에서 System V AMD64 ABI를 따릅니다. 함수 호출 시 레지스터 사용 규칙을 정확히 알아야 합니다.

x86_64 System V ABI — 스택 프레임과 레지스터 역할 스택 프레임 레이아웃 (호출자→피호출자) [rbp+16+] 호출자 프레임 (스택 인자 등) [rbp+8] 리턴 주소 (call이 push) ret 시 여기서 꺼내어 rip에 적재 [rbp+0] 저장된 rbp (pushq %rbp) ← rbp = rsp (movq %rsp,%rbp 후) ← rbp 기준점 [rbp-8] 로컬 변수 / callee-saved 보존 subq $32,%rsp 로 공간 확보 [rbp-16] 로컬 변수 2 … [rbp-32] 로컬 변수 3 (pushq %rbx 등) ← rsp (현재 스택 포인터) Red Zone (128 바이트) [rsp-1] ~ [rsp-128] 유저 공간: 임시 저장 사용 가능 커널: -mno-red-zone → 사용 불가 스택 증가↑ (주소↓) 프롤로그: pushq %rbp → movq %rsp,%rbp → subq $N,%rsp 에필로그: popq %rbx → leave (= movq %rbp,%rsp; popq %rbp) → retq Caller-saved (비보존 — caller가 저장) rdi,rsi,rdx,rcx 인자 1-4 r8, r9 인자 5-6 rax 반환값 (상위: rdx) r10, r11 임시 (syscall: rcx+r11 손상) xmm0~7 부동소수점 인자/반환 call 전에 caller가 저장하지 않으면 소실됨 Callee-saved (보존 — callee가 저장·복원) rbx 범용 보존 레지스터 rbp 프레임 포인터 (선택적) r12,r13,r14,r15 범용 보존 레지스터 rsp 스택 포인터 (반드시 복원) 함수 반환 전 원래 값으로 복원해야 함

레지스터 역할

레지스터용도호출 후 보존
rdi, rsi, rdx, rcx, r8, r9함수 인자 1~6비보존 (caller-saved)
rax반환값비보존
rdx반환값 (128비트 시 상위)비보존
r10, r11임시비보존
rbx, rbp, r12~r15범용보존 (callee-saved)
rsp스택 포인터보존
xmm0~xmm7부동소수점 인자/반환비보존

스택 프레임과 Red Zone

/*
 * 표준 함수 프롤로그/에필로그
 *
 * 스택 레이아웃:
 *   [rbp+16]  두 번째 스택 인자 (있는 경우)
 *   [rbp+8]   리턴 주소
 *   [rbp]     이전 rbp (프레임 포인터)
 *   [rbp-8]   로컬 변수 시작
 */
SYM_FUNC_START(example_func)
    pushq   %rbp                /* 프레임 포인터 저장 */
    movq    %rsp, %rbp          /* 새 프레임 설정 */
    subq    $32, %rsp           /* 로컬 변수 공간 */
    pushq   %rbx                /* callee-saved 보존 */
    pushq   %r12

    /* ... 함수 본문 ... */

    popq    %r12                /* callee-saved 복원 */
    popq    %rbx
    leave                       /* = movq %rbp,%rsp; popq %rbp */
    retq
SYM_FUNC_END(example_func)
⚠️

Red Zone 주의: System V ABI에서 유저 공간 코드는 rsp 아래 128바이트를 임시 저장소로 사용할 수 있습니다 (Red Zone). 그러나 커널 코드에서는 Red Zone이 비활성화됩니다 (-mno-red-zone). 인터럽트가 언제든 발생하여 스택을 덮어쓸 수 있기 때문입니다.

시스템 콜 vs 일반 함수 호출

특성일반 함수 호출 (call)시스템 콜 (syscall)
4번째 인자rcxr10 (rcx는 syscall이 덮어씀)
호출 방식call 명령어syscall 명령어
리턴retsysret (또는 iretq)
변경 레지스터caller-savedrcx, r11 추가 변경
컨텍스트동일 권한 수준유저→커널 권한 전환

Windows x64 ABI 비교

Windows x64 호출 규약은 System V ABI와 크게 다릅니다. UEFI 펌웨어나 크로스 플랫폼 코드를 이해할 때 필요합니다.

항목System V AMD64 (Linux)Windows x64
정수 인자 1-4RDI, RSI, RDX, RCXRCX, RDX, R8, R9
정수 인자 5+R8, R9, 스택스택 (32B 이후)
FP 인자XMM0-XMM7XMM0-XMM3
반환값RAX, RDXRAX
Caller-savedRAX,RCX,RDX,RSI,RDI,R8-R11RAX,RCX,RDX,R8-R11
Callee-savedRBX,RBP,R12-R15RBX,RBP,RDI,RSI,R12-R15
Shadow Space없음32바이트 (항상 예약)
Red Zone128바이트없음
스택 정렬16바이트 (call 전)16바이트 (call 전)
ℹ️

Shadow Space: Windows x64는 호출자가 항상 32바이트의 "shadow space"를 스택에 예약합니다. 피호출자는 이 공간에 레지스터 인자를 저장할 수 있습니다. EFI 스텁(drivers/firmware/efi/)이 UEFI 런타임 서비스를 호출할 때 이 규약을 따라야 합니다.

가변 인자 (Variadic) 어셈블리

System V ABI에서 가변 인자 함수는 AL 레지스터에 사용된 벡터 레지스터 수를 전달합니다.

/* 가변 인자 호출 시 AL = 벡터 레지스터 수 */
/* printf("%.2f %d", 3.14, 42) 호출 시: */
/*   RDI = format string pointer */
/*   XMM0 = 3.14 (double) */
/*   RSI = 42 */
/*   AL = 1 (SSE 레지스터 1개 사용) */

/* va_list 구조 (System V ABI) */
typedef struct {
    unsigned int gp_offset;    /* 다음 GP 레지스터 오프셋 */
    unsigned int fp_offset;    /* 다음 FP 레지스터 오프셋 */
    void *overflow_arg_area;   /* 스택 오버플로 인자 */
    void *reg_save_area;       /* 레지스터 저장 영역 */
} va_list[1];
/* 프롤로그에서 RDI,RSI,RDX,RCX,R8,R9와 XMM0-XMM7을 저장 */

Red Zone 상세와 커널 금지 이유

System V ABI의 Red Zone은 RSP 아래 128바이트를 함수가 자유롭게 사용할 수 있는 영역입니다. 커널에서는 반드시 비활성화(-mno-red-zone)해야 합니다.

/* Red Zone: RSP 아래 128바이트 */
/*
 * ┌──────────────────┐ ← RSP
 * │                  │
 * │   Red Zone       │  128바이트
 * │   (함수가 사용)   │  스택 조정 없이 사용 가능
 * │                  │
 * └──────────────────┘ ← RSP - 128
 *
 * 리프 함수(다른 함수 호출 안 하는)에서 활용:
 * - SUB RSP 없이 지역변수 사용 가능
 * - 성능 이점: 스택 포인터 조정 명령어 절약
 */

/* 커널에서 금지하는 이유:
 * 1. 인터럽트가 현재 스택에 데이터 푸시
 *    → Red Zone 내용 덮어씀 → 데이터 손상
 * 2. NMI가 언제든 발생 가능
 *    → 스택에 즉시 쓰기 → Red Zone 파괴
 * 3. 예외(#PF, #GP)가 스택에 에러코드 푸시
 *
 * 커널 빌드 플래그:
 * KBUILD_CFLAGS += -mno-red-zone
 */

커널 SIMD/FPU 어셈블리

Linux 커널은 성능이 중요한 경로(암호화, RAID, 체크섬, 문자열 연산)에서 SIMD/FPU 명령어를 사용합니다. 커널 모드에서 FPU를 사용하려면 kernel_fpu_begin()/kernel_fpu_end()로 FPU 상태를 보호해야 하며, XSAVE/XRSTOR 메커니즘을 이해해야 합니다.

커널 SIMD/FPU 사용 구조 kernel_fpu_begin() / kernel_fpu_end() kernel_fpu_begin(): 1. preempt_disable() 2. fpregs_lock() 3. 현재 태스크 FPU 상태 저장 (XSAVE) 4. CR0.TS 클리어 (FPU 활성화) kernel_fpu_end(): 1. FPU 상태 복원 (XRSTOR) 2. fpregs_unlock() + preempt_enable() XSAVE 영역 레이아웃 x87 (512B) SSE (XMM) XSAVE 헤더(64B) AVX (YMM 상위 128b) AVX-512 (ZMM/k 마스크) MPX (제거됨) PKRU / AMX / CET-U XCR0: 활성 컴포넌트 비트마스크 XSAVEOPT: 수정된 컴포넌트만 저장 XSAVES: supervisor 상태 포함 (CET-SS) 커널 SIMD 사용처 암호화 AES-NI, SHA-NI PCLMULQDQ (GCM) RAID XOR/P+Q 계산 AVX2/AVX-512 체크섬 CRC32c (SSE4.2) csum_partial SSE 문자열 memcpy/memset REP MOVSB/STOSB 압축 zlib deflate AVX 최적화 ⚠ 인터럽트/softirq 컨텍스트에서는 kernel_fpu_begin() 사용 불가 (sleep 가능) → 대안: irq_fpu_usable() 확인 후 소프트웨어 폴백 ARM64: fpsimd_save_state() / NEON 사용 시 kernel_neon_begin()/end()

kernel_fpu_begin/end 사용 패턴

/* arch/x86/crypto/aesni-intel_glue.c — AES-NI 사용 예 */
static int aesni_encrypt(struct crypto_tfm *tfm, u8 *dst, const u8 *src)
{
    struct crypto_aes_ctx *ctx = crypto_tfm_ctx(tfm);

    if (!irq_fpu_usable()) {
        /* 인터럽트 컨텍스트 → 소프트웨어 폴백 */
        aes_encrypt(ctx, dst, src);
        return 0;
    }

    kernel_fpu_begin();           /* 선점 비활성 + FPU 상태 저장 */
    aesni_enc(ctx, dst, src);     /* AES-NI 어셈블리 호출 */
    kernel_fpu_end();             /* FPU 상태 복원 + 선점 활성 */
    return 0;
}

/* arch/x86/crypto/aesni-intel_asm.S — AES-NI 어셈블리 (핵심) */
SYM_FUNC_START(aesni_enc)
    movups  (%rsi), %xmm0        /* 평문 로드 → XMM0 */
    movups  (%rdi), %xmm1        /* 라운드 키 0 */
    pxor    %xmm1, %xmm0         /* AddRoundKey */

    /* 9~13 라운드 (키 크기에 따라) */
    movups  0x10(%rdi), %xmm1
    aesenc  %xmm1, %xmm0         /* AES-NI: 단일 라운드 암호화 */
    movups  0x20(%rdi), %xmm1
    aesenc  %xmm1, %xmm0
    /* ... 반복 ... */

    movups  0xa0(%rdi), %xmm1
    aesenclast %xmm1, %xmm0      /* 최종 라운드 */
    movups  %xmm0, (%rdx)         /* 암호문 저장 */
    RET
SYM_FUNC_END(aesni_enc)

XSAVE/XRSTOR 메커니즘

/* FPU/SIMD 상태 저장/복원 — arch/x86/include/asm/fpu/internal.h */

/* XSAVE: XCR0에 설정된 컴포넌트만 저장 */
static inline void xsave(struct fpu *fpu)
{
    u64 mask = fpu->state_mask;
    asm volatile (
        "xsaveopt %[buf]"       /* 수정된 컴포넌트만 저장 (최적화) */
        : [buf] "=m" (fpu->fpstate->regs)
        : "a" ((u32)mask), "d" ((u32)(mask >> 32))
        : "memory"
    );
}

/* XRSTOR: 저장된 컴포넌트 복원 */
static inline void xrstor(struct fpu *fpu)
{
    u64 mask = fpu->state_mask;
    asm volatile (
        "xrstor %[buf]"
        :: [buf] "m" (fpu->fpstate->regs),
           "a" ((u32)mask), "d" ((u32)(mask >> 32))
        : "memory"
    );
}

/* XCR0 비트 (활성 컴포넌트) */
/* bit 0: x87 FPU         (필수, 항상 1) */
/* bit 1: SSE (XMM0-15)   (128비트) */
/* bit 2: AVX (YMM0-15)   (256비트 상위 절반) */
/* bit 5: AVX-512 opmask  (k0-k7 마스크 레지스터) */
/* bit 6: AVX-512 ZMM_Hi  (ZMM0-15 상위 256비트) */
/* bit 7: AVX-512 Hi-ZMM  (ZMM16-31 전체) */
/* bit 9: PKRU             (Protection Key) */
/* bit 17-18: AMX          (타일 구성/데이터) */

RAID XOR / P+Q SIMD 어셈블리

/* lib/raid6/avx2.c — AVX2 RAID6 P+Q 계산 */
/* P = 단순 XOR, Q = GF(2^8) 곱셈 + XOR */

SYM_FUNC_START(raid6_avx2x2_gen_syndrome)
    kernel_fpu_begin_mask(KFPU_256BIT)

    /* YMM 레지스터로 256비트 (32바이트) 동시 처리 */
    vmovdqa  (%rdi), %ymm0       /* 디스크 데이터 32B 로드 */
    vmovdqa  32(%rdi), %ymm1     /* 다음 32B */

    vpxor    %ymm0, %ymm4, %ymm4 /* P ^= data */
    vpxor    %ymm1, %ymm5, %ymm5

    /* Q 계산: GF(2^8) 곱셈 = shift + conditional XOR */
    vpcmpgtb %ymm6, %ymm2, %ymm3 /* MSB 검사 → 마스크 */
    vpaddb   %ymm2, %ymm2, %ymm2 /* Q <<= 1 */
    vpand    %ymm7, %ymm3, %ymm3 /* 마스크 & 0x1d (다항식) */
    vpxor    %ymm3, %ymm2, %ymm2 /* Q ^= polynomial */
    vpxor    %ymm0, %ymm2, %ymm2 /* Q ^= data */
    /* ... */

    kernel_fpu_end()
SYM_FUNC_END(raid6_avx2x2_gen_syndrome)
💡

성능 팁: kernel_fpu_begin()/end()는 XSAVE/XRSTOR로 수백 바이트를 저장/복원하므로 오버헤드가 있습니다. 소량 데이터에는 소프트웨어 구현이 더 빠를 수 있습니다. 커널의 crypto 서브시스템은 simd_register_skciphers_compat()로 자동 폴백을 처리합니다.

AVX-512 커널 활용 예시

AVX-512는 512비트(64바이트) 벡터 연산을 제공하며, 커널에서는 RAID 패리티 연산, 암호화 가속, 체크섬 계산에 활용됩니다.

요소설명레지스터
데이터 레지스터ZMM0-ZMM31 (512비트)32개
마스크 레지스터k0-k7 (64비트 opmask)8개 (k0=무조건)
폭 접근XMM (128), YMM (256), ZMM (512)동일 물리 레지스터
/* RAID6 P+Q 패리티: AVX-512 — arch/x86/lib/raid6/avx512.c */
/* kernel_fpu_begin() 호출 후 사용 */

    /* 512비트 XOR: P 패리티 */
    vmovdqa64  (%rsi), %zmm0        /* 디스크 1 데이터 (64B) */
    vpxorq     (%rdx), %zmm0, %zmm0 /* XOR 디스크 2 */
    vpxorq     (%rcx), %zmm0, %zmm0 /* XOR 디스크 3 */
    vmovdqa64  %zmm0, (%rdi)        /* P 패리티 저장 */

    /* 3-way 논리 연산: VPTERNLOGD (진리표 기반) */
    vpternlogd $0x96, %zmm2, %zmm1, %zmm0
    /* imm8=0x96 = XOR(a,b,c): P = D1 ^ D2 ^ D3 */
    /* VPTERNLOGD 한 명령어로 3-way XOR 완료 */

    /* opmask 사용 예: 조건부 연산 */
    vpcmpd    $4, %zmm1, %zmm0, %k1  /* k1 = zmm0 != zmm1 */
    vmovdqa32 %zmm2, %zmm0{%k1}      /* k1 마스크: 다른 원소만 업데이트 */
ℹ️

AVX-512와 주파수 스로틀링: 일부 Intel CPU에서 AVX-512 사용 시 코어 주파수가 낮아집니다(License Level 1→2→3). 커널은 짧은 구간에서만 AVX-512를 사용하고 VZEROUPPER로 상태를 클리어하여 성능 영향을 최소화합니다. Ice Lake 이후 CPU에서는 스로틀링이 크게 완화되었습니다.

AMX (Advanced Matrix Extensions) 개요

Intel AMX는 행렬(타일) 연산을 위한 확장으로, AI/ML 추론 가속에 사용됩니다. Sapphire Rapids 이후 서버 CPU에서 지원합니다.

요소설명
타일 레지스터TMM0-TMM7 (각 최대 1KB, 설정에 따라 가변)
TILECFG타일 행/열 크기 설정 레지스터 (64바이트)
XSAVE 컴포넌트XTILECFG (비트 17), XTILEDATA (비트 18)
주요 명령어TILELOADD, TILESTORED, TDPBSSD, TDPBF16PS, TDPFP16PS
/* AMX 사용 패턴 (커널 내부) */
/* AMX 타일 데이터는 XSAVE/XRSTOR로 관리 */
/* 커널은 XFD(Extended Feature Disable)로 첫 사용 시 lazy 할당 */

/* AMX 타일 설정 */
struct tile_config {
    u8 palette_id;       /* 타일 팔레트 (1) */
    u8 start_row;        /* 재시작 행 */
    u8 reserved[14];
    u16 colsb[8];        /* 각 타일의 열 수 (바이트) */
    u8 padding[16];
    u8 rows[8];          /* 각 타일의 행 수 */
    u8 padding2[8];
} __packed;  /* 총 64바이트 */

/* LDTILECFG: 타일 설정 로드 */
asm volatile ("ldtilecfg (%0)" : : "r"(&config) : "memory");

/* TILELOADD: 타일 데이터 로드 (행렬) */
/* TDPBSSD: 타일 곱셈-덧셈 (int8 → int32) */
/* TILESTORED: 결과 저장 */
/* TILERELEASE: 타일 해제 (INIT 상태로) */
💡

커널 AMX 지원: Linux 5.16+에서 AMX를 지원합니다. XFD(Extended Feature Disable)를 통해 첫 AMX 사용 시 #NM 예외가 발생하고, 커널이 XSTATE 영역을 동적 할당합니다. 타일 데이터(최대 8KB)는 프로세스별로 관리되며, 컨텍스트 전환 시 XSAVE/XRSTOR로 저장/복원됩니다.

ARM64 어셈블리

Linux 커널은 ARM64 (AArch64)도 광범위하게 지원합니다. ARM64 어셈블리의 기본을 알면 멀티 아키텍처 코드를 이해하는 데 도움이 됩니다.

ARM64 레지스터

레지스터용도호출 규약
x0~x7함수 인자/반환값비보존
x8간접 결과 레지스터비보존
x9~x15임시 레지스터비보존
x16~x17PLT/veneer (IP0, IP1)비보존
x18플랫폼 예약 레지스터플랫폼 ABI 의존 (일반 용도 사용 금지)
x19~x28callee-saved보존
x29 (FP)프레임 포인터보존
x30 (LR)링크 레지스터 (리턴 주소)비보존
SP스택 포인터보존
x86_64 — System V AMD64 ABI ARM64 — AArch64 AAPCS64 함수 인자 (caller-saved) 함수 인자 & 반환값 (caller-saved) rdi rsi rdx rcx r8 r9 x0 x1 x2 x3 x4 x5 x6 x7 반환값 간접 결과 + 임시 (caller-saved) rax 정수 반환값 rdx 128-bit 반환 상위 (rdx:rax) x8 간접 결과 x9 – x15 임시 레지스터 ×7 임시 (caller-saved) 링커 예약 + 플랫폼 예약 r10 r11 x16 IP0 (링커) x17 IP1 (링커) x18 플랫폼 예약 보존 (callee-saved) 보존 (callee-saved) rbx rbp r12 r13 r14 r15 x19 – x28 callee-saved ×10 특수 레지스터 특수 레지스터 rsp 스택 포인터 · call 직전 16-byte 정렬 필수 x29 FP 프레임 포인터 x30 LR 리턴 주소 레지스터 SP 스택 포인터 함수 인자 반환값 임시 caller-saved 보존 callee-saved 특수·링커·플랫폼 플랫폼 예약 (사용 금지) 핵심 차이점 ① 인자 레지스터 수 x86_64: 6개 rdi rsi rdx rcx r8 r9 ARM64: 8개 x0 – x7 7번째 이상: 스택 전달 (양쪽 right-to-left 순서) ARM64가 2개 더 많아 스택 사용 빈도 ↓ syscall 4번째: rcx→r10 / x3 ② 반환값 레지스터 x86_64 정수: rax 128-bit: rdx:rax ARM64 정수: x0 128-bit: x0:x1 큰 구조체: x86_64 → 스택 간접 ARM64 → x8 간접 참조 ③ 반환 주소 저장 x86_64: 스택 call → [rsp]에 ret_addr push ret → pop && jump ARM64: x30 (LR) bl → x30 = ret_addr ret → br x30 리프 함수: LR 저장 불필요 → 스택 접근 감소 → 성능 이점 ④ 시스템 콜 x86_64 명령어: syscall 번호: rax 4번째 인자: r10 (rcx 아님) ARM64 명령어: svc #0 번호: x8 4번째 인자: x3 x86: syscall이 rcx 덮어씀 → rcx→r10 재배치 필요

ARM64 주요 명령어

/* 산술 연산 */
add     x0, x1, x2              /* x0 = x1 + x2 */
sub     x0, x1, #16             /* x0 = x1 - 16 */
madd    x0, x1, x2, x3          /* x0 = x1*x2 + x3 */

/* 메모리 접근: LDP/STP (쌍 로드/저장) */
stp     x29, x30, [sp, #-16]!  /* FP, LR 스택에 저장 (pre-index) */
ldp     x29, x30, [sp], #16    /* FP, LR 복원 (post-index) */

/* 조건부 실행 */
cmp     x0, #0
csel    x1, x2, x3, eq          /* if (x0==0) x1=x2 else x1=x3 */
cset    x0, ne                   /* x0 = (조건 NE이면 1, 아니면 0) */

/* PC 상대 주소 계산 */
adrp    x0, my_symbol            /* x0 = 페이지 주소 (4KB 정렬) */
add     x0, x0, :lo12:my_symbol  /* 하위 12비트 오프셋 추가 */

/* 비트 조작 */
ubfx    x0, x1, #4, #8         /* x1의 비트[11:4] 추출 → x0 */
bfi     x0, x1, #8, #4         /* x1의 하위 4비트를 x0[11:8]에 삽입 */

ARM64 커널 어셈블리 예시

/* ARM64 인라인 어셈블리: 현재 Exception Level 읽기 */
static inline u64 read_currentel(void)
{
    u64 val;
    asm volatile ("mrs %0, CurrentEL" : "=r"(val));
    return (val >> 2) & 3;  /* EL0=0, EL1=1, EL2=2 */
}

/* ARM64 인라인 어셈블리: 데이터 캐시 클린 + 무효화 */
static inline void dc_civac(unsigned long addr)
{
    asm volatile ("dc civac, %0" :: "r"(addr) : "memory");
}

/* ARM64 인라인 어셈블리: 명령어 배리어 */
static inline void isb(void)
{
    asm volatile ("isb" ::: "memory");
}

ARM64 인라인 어셈블리

ARM64(AArch64) GCC 인라인 어셈블리는 x86과 다른 제약조건과 레지스터 참조 규칙을 사용합니다.

제약의미비고
"r"범용 레지스터 (x0-x30)64비트
"w"SIMD/FP 레지스터 (v0-v31)128비트
"m"메모리 참조base+offset
"I"12비트 즉시값 (0-4095)ADD/SUB
"K"32비트 논리 즉시값AND/ORR/EOR 패턴
"L"64비트 논리 즉시값비트마스크 패턴
"M"MOV 32비트 즉시값MOVZ/MOVN
/* ARM64 인라인 어셈블리 레지스터 참조 */
u64 val;
asm volatile ("mrs %0, cntvct_el0" : "=r"(val));
/* %0 → x 레지스터 (64비트), %w0 → w 레지스터 (32비트) */

/* 시스템 레지스터 읽기/쓰기 */
static inline u64 read_sysreg(void)
{
    u64 val;
    asm volatile ("mrs %0, sctlr_el1" : "=r"(val));
    return val;
}

/* 32비트 레지스터 접근 (%w 접두사) */
u32 lo;
asm volatile ("mrs %w0, cntfrq_el0" : "=r"(lo));
/* %w0 = w 레지스터 (32비트 하위), %x0 = x 레지스터 (64비트) */

/* 메모리 배리어와 결합 */
asm volatile ("dmb ish" ::: "memory");
asm volatile ("dsb sy" ::: "memory");
asm volatile ("isb" ::: "memory");

ARM64 조건 코드

ARM64는 NZCV(Negative, Zero, Carry, oVerflow) 플래그를 사용하며, 조건부 선택 명령어로 분기 없이 조건부 실행을 구현합니다.

접미사의미조건x86 등가
EQ같음Z=1JE/JZ
NE다름Z=0JNE/JNZ
CS/HS캐리 세트/부호없는 ≥C=1JC/JAE
CC/LO캐리 클리어/부호없는 <C=0JNC/JB
MI음수N=1JS
PL양수 또는 0N=0JNS
VS오버플로V=1JO
VC오버플로 없음V=0JNO
HI부호없는 >C=1 ∧ Z=0JA
LS부호없는 ≤C=0 ∨ Z=1JBE
GE부호있는 ≥N=VJGE
LT부호있는 <N≠VJL
GT부호있는 >Z=0 ∧ N=VJG
LE부호있는 ≤Z=1 ∨ N≠VJLE
AL항상무조건JMP
/* ARM64 조건부 선택 명령어 — 분기 없는 조건부 실행 */
    cmp  x0, x1
    csel x2, x3, x4, gt   /* x2 = (x0 > x1) ? x3 : x4 */
    csinc x2, x3, x4, eq  /* x2 = (x0 == x1) ? x3 : x4+1 */
    csinv x2, x3, x4, ne  /* x2 = (x0 != x1) ? x3 : ~x4 */
    csneg x2, x3, x4, lt  /* x2 = (x0 < x1) ? x3 : -x4 */

/* x86 CMOV 등가:
 * x86: cmovg %ecx, %eax  → 조건부 이동
 * arm64: csel x0, x1, x0, gt → 더 유연 (두 소스 선택)
 *
 * ARM64 CSEL은 x86 CMOV보다 유연:
 *  - 두 개의 서로 다른 소스 레지스터 선택 가능
 *  - CSINC/CSINV/CSNEG로 산술 변환 결합 가능
 */
💡

커널에서의 조건부 선택: ARM64 커널은 분기 예측 실패를 줄이기 위해 CSEL을 적극 활용합니다. 예를 들어 test_and_clear_bit의 반환값 처리나, 에러 코드 선택에서 분기 대신 CSEL을 사용합니다.

RISC-V 어셈블리

RISC-V는 오픈 ISA로 Linux 커널에서 지원이 빠르게 확대되고 있습니다. x86/ARM64와 다른 설계 철학(최소 명령어, CSR 기반 시스템 제어, 확장 모듈형)을 이해하면 멀티 아키텍처 커널 코드를 효과적으로 읽을 수 있습니다.

RISC-V 레지스터 & 특권 레벨 범용 레지스터 (RV64I) x0 (zero) 항상 0 x16 (a6) 인자 7 x1 (ra) 리턴 주소 x17 (a7) 인자 8/syscall# x2 (sp) 스택 포인터 x18 (s2) callee-saved x3 (gp) 글로벌 포인터 x19 (s3) callee-saved x4 (tp) 스레드 포인터 x20-x27 s4-s11 saved x5 (t0) 임시/alt link x28-x31 t3-t6 temp x6-x7 t1-t2 임시 x8 (s0/fp) 프레임 포인터 x9 (s1) callee-saved x10-x11 a0-a1 인자/반환 x12-x15 a2-a5 인자 a0-a7: 함수 인자 (syscall: a7=번호, a0-a5=인자) s0-s11: callee-saved / t0-t6: caller-saved 특권 레벨 (Privilege Levels) M-mode (Machine) — 펌웨어/SBI (최고 특권) S-mode (Supervisor) — 리눅스 커널 U-mode (User) — 유저 프로세스 ecall: U→S (syscall), S→M (SBI call) 주요 CSR (Control/Status Registers) sstatus S-mode 상태 (SIE,SPP,SUM) stvec 트랩 핸들러 주소 sepc 예외 발생 PC scause 트랩 원인 코드 stval 트랩 관련 값 (fault addr) sip/sie 인터럽트 pending/enable satp 페이지 테이블 기준 (mode+PPN) csrr rd, csr → CSR 읽기 csrw csr, rs → CSR 쓰기 csrrs rd, csr, rs → 읽고 비트 설정 csrrc rd, csr, rs → 읽고 비트 클리어 sfence.vma — TLB 플러시

RISC-V 주요 명령어 패턴

/* RISC-V 기본 명령어 (RV64I) */

/* 산술/논리 */
add   a0, a1, a2       /* a0 = a1 + a2 */
addi  a0, a1, 42       /* a0 = a1 + 42 */
addw  a0, a1, a2       /* 32비트 덧셈 (W 접미사) */
slli  a0, a1, 3        /* a0 = a1 << 3 */
and   a0, a1, a2       /* a0 = a1 & a2 */

/* 메모리 접근 (Load/Store만 — RISC 원칙) */
ld    a0, 0(sp)        /* 64비트 load */
lw    a0, 4(sp)        /* 32비트 load (부호 확장) */
lwu   a0, 4(sp)        /* 32비트 load (제로 확장) */
lb    a0, 0(a1)        /* 1바이트 load */
sd    a0, 0(sp)        /* 64비트 store */
sw    a0, 4(sp)        /* 32비트 store */

/* 분기 */
beq   a0, a1, label    /* a0 == a1 이면 점프 */
bne   a0, zero, label  /* a0 != 0 이면 점프 */
blt   a0, a1, label    /* a0 < a1 (부호) 이면 점프 */
bltu  a0, a1, label    /* a0 < a1 (비부호) 이면 점프 */

/* 함수 호출 */
jal   ra, func         /* call: ra에 복귀 주소 저장, func로 점프 */
jalr  zero, ra, 0      /* ret: ra로 복귀 (rd=zero이면 반환값 없음) */

/* 큰 상수 로딩 */
lui   a0, %hi(symbol)  /* 상위 20비트 로드 */
addi  a0, a0, %lo(symbol) /* 하위 12비트 추가 */

/* PC 상대 주소 */
auipc a0, %pcrel_hi(sym) /* a0 = PC + (상위 20비트 << 12) */
addi  a0, a0, %pcrel_lo(sym)

RISC-V Atomic 확장 (A)

/* RV64A — Atomic Memory Operations */

/* Load-Reserved / Store-Conditional (LL/SC 패턴) */
/* ARM의 LDXR/STXR, MIPS의 LL/SC와 유사 */
.Lretry:
    lr.d    a0, (a1)           /* load-reserved: a0 = *a1, 예약 세트 */
    add     a2, a0, a3         /* 새 값 계산 */
    sc.d    a4, a2, (a1)       /* store-conditional: *a1 = a2, a4=성공(0)/실패(!=0) */
    bnez    a4, .Lretry        /* 실패하면 재시도 */

/* AMO (Atomic Memory Operations) — 단일 명령어 atomic */
amoadd.d  a0, a1, (a2)        /* a0 = *a2; *a2 += a1 (atomic) */
amoswap.d a0, a1, (a2)        /* a0 = *a2; *a2 = a1 (atomic swap) */
amoand.d  a0, a1, (a2)        /* a0 = *a2; *a2 &= a1 */
amoor.d   a0, a1, (a2)        /* a0 = *a2; *a2 |= a1 */
amomax.d  a0, a1, (a2)        /* a0 = *a2; *a2 = max(*a2, a1) */

/* 메모리 순서 접미사: .aq (acquire), .rl (release), .aqrl (both) */
amoadd.d.aqrl a0, a1, (a2)    /* full barrier atomic add */
lr.d.aq   a0, (a1)            /* acquire load-reserved */
sc.d.rl   a0, a1, (a2)        /* release store-conditional */

RISC-V 커널 어셈블리 예시

/* arch/riscv/kernel/entry.S — 시스템 콜 / 트랩 진입 */
SYM_CODE_START(handle_exception)
    /* 1. 커널 스택으로 전환 (sscratch ↔ sp 교환) */
    csrrw   sp, sscratch, sp   /* sscratch에 유저 sp 저장, 커널 sp 로드 */

    /* 유저에서 왔는지 확인 */
    bnez    sp, .Lsave_context /* sp != 0 → 유저 모드 (sscratch에 커널 sp 저장됨) */
    csrr    sp, sscratch       /* sp == 0 → 이미 커널 모드, sp 복원 */

.Lsave_context:
    /* 2. pt_regs 구조체 저장 */
    addi    sp, sp, -(PT_SIZE) /* pt_regs 크기만큼 스택 확보 */
    sd      x1, PT_RA(sp)      /* ra 저장 */
    sd      x3, PT_GP(sp)      /* gp 저장 */
    sd      x5, PT_T0(sp)      /* t0 저장 */
    /* ... 나머지 레지스터 저장 ... */
    sd      x10, PT_A0(sp)     /* a0 (syscall 첫 인자) */
    sd      x17, PT_A7(sp)     /* a7 (syscall 번호) */

    /* 3. CSR 저장 */
    csrr    s1, sepc           /* 예외 발생 PC */
    csrr    s2, sstatus        /* 상태 레지스터 */
    csrr    s3, stval          /* 트랩 값 (fault 주소 등) */
    csrr    s4, scause         /* 트랩 원인 */
    sd      s1, PT_EPC(sp)
    sd      s2, PT_STATUS(sp)

    /* 4. 트랩 원인 분기 */
    blt     s4, zero, .Lhandle_interrupt  /* scause < 0 → 인터럽트 */
    /* scause >= 0 → 예외 (syscall: cause=8, page fault: cause=13/15) */

    li      t0, EXC_SYSCALL    /* cause == 8? */
    beq     s4, t0, .Lhandle_syscall
    /* 기타 예외 처리... */

.Lhandle_syscall:
    addi    s1, s1, 4          /* sepc += 4 (ecall 다음 명령어) */
    sd      s1, PT_EPC(sp)
    mv      a0, sp             /* pt_regs * 전달 */
    call    do_trap_ecall_u    /* C 핸들러 */
    j       ret_from_exception
SYM_CODE_END(handle_exception)

/* RISC-V 인라인 어셈블리 예시 (커널) */
static inline void local_irq_disable(void)
{
    asm volatile ("csrc sstatus, %0" :: "r"(SR_SIE) : "memory");
}

static inline void local_flush_tlb_all(void)
{
    asm volatile ("sfence.vma" ::: "memory");
}
비교 항목x86_64ARM64RISC-V
시스템 콜 명령어syscallsvc #0ecall
콜 번호 레지스터raxx8a7 (x17)
인자 레지스터rdi,rsi,rdx,r10,r8,r9x0-x5a0-a5
반환값raxx0a0
인터럽트 비활성화climsr daifset,#2csrc sstatus,SIE
TLB 플러시invlpgtlbisfence.vma
Atomic 패턴LOCK 접두사LDXR/STXRLR/SC 또는 AMO
메모리 모델TSO (강한)약한RVWMO (약한)
배리어mfence/lfencedmb/dsb/isbfence
리턴 주소스택 pushLR (x30)ra (x1)
ℹ️

RISC-V 확장: 커널은 riscv_isa_extension_available()로 런타임 확장 검사를 수행합니다. 주요 확장: M(곱셈), A(atomic), F/D(FP), C(압축 명령어 16비트), V(벡터), Zicbom(캐시 블록 관리), Svnapot(64KB 페이지). alternatives 메커니즘으로 확장 유무에 따른 코드 패칭도 지원합니다.

RVC 압축 명령어

RISC-V C(Compressed) 확장은 자주 사용하는 명령어의 16비트 인코딩을 제공하여 코드 밀도를 향상시킵니다. 커널에서는 CONFIG_RISCV_ISA_C로 활성화합니다.

압축 명령어32비트 등가레지스터 제한비트
C.ADD rd, rs2add rd, rd, rs2x1-x3116
C.ADDI rd, immaddi rd, rd, immx1-x31, imm≠016
C.ADDI16SP immaddi x2, x2, immx2(sp) 전용16
C.LW rd', off(rs1')lw rd, off(rs1)x8-x15만16
C.LD rd', off(rs1')ld rd, off(rs1)x8-x15만 (RV64)16
C.SW rs2', off(rs1')sw rs2, off(rs1)x8-x15만16
C.J offsetjal x0, offset무조건 점프16
C.BEQZ rs1', offbeq rs1, x0, offx8-x15만16
C.BNEZ rs1', offbne rs1, x0, offx8-x15만16
C.MV rd, rs2add rd, x0, rs2x1-x3116
C.JALR rs1jalr x1, 0(rs1)x1-x3116
C.NOPaddi x0, x0, 016
/* RVC 압축 명령어 예시 — 함수 프롤로그/에필로그 */
func:
    c.addi  sp, -16     /* 스택 프레임 할당 (16비트) */
    c.sd    ra, 8(sp)    /* 복귀 주소 저장 (16비트) */
    c.sd    s0, 0(sp)    /* callee-saved 레지스터 저장 */
    ...
    c.ld    ra, 8(sp)    /* 복귀 주소 복원 */
    c.ld    s0, 0(sp)
    c.addi  sp, 16
    c.jr    ra           /* jalr x0, 0(ra) */

/* 32비트 등가 코드보다 코드 크기 ~25-30% 절감 */
/* 제한: x8-x15(s0-s7, a0-a7 중 일부)만 사용 가능한 명령어 존재 */
ℹ️

RVC 정렬: C 확장 활성 시 명령어는 2바이트 정렬이면 충분합니다 (4바이트 정렬 불필요). 하위 2비트가 11이 아니면 16비트 명령어, 11이면 32비트 이상입니다. 커널의 CONFIG_RISCV_ISA_C=y이면 -marchc가 포함되어 컴파일러가 자동으로 압축 명령어를 생성합니다.

커널 진입/탈출 코드

커널의 가장 중요한 어셈블리 코드는 유저 공간↔커널 공간 전환을 담당하는 진입/탈출 코드입니다. 시스템 콜, 인터럽트, 예외 처리 시 정확한 컨텍스트 저장/복원이 필수입니다.

커널 진입/탈출 코드 — 유저↔커널 전환 진입 경로 ──→ ←── 탈출 경로 유저 공간 syscall 실행 rcx ← 다음 RIP r11 ← RFLAGS sysretq 후 복귀 rcx → RIP 복원 r11 → RFLAGS 복원 rax = 반환값 do_syscall_64() rdi = pt_regs* rsi = syscall 번호 → sys_xxx() 호출 반환값 → rax (에러: -errno) → 복귀 경로 선택 (sysretq / iretq) ① swapgs GS 교체 ② RSP 저장 per-CPU ③ 스택 전환 커널 RSP ④ pt_regs 컨텍스트 push ⑤ PUSH 레지스터 저장 ⑥ POP_REGS 레지스터 복원 ⑦ swapgs GS 복원 ⑧ RSP 복원 사용자 RSP ⑨ sysretq 유저 복귀 pt_regs 스택 레이아웃 (커널 진입 후) ← 커널 스택 상단 (high address) syscall 수동 push / 인터럽트 CPU 자동 push ss (+160) sp / RSP (user) (+152) flags / RFLAGS (+144) cs (+136) ip / RIP (반환 주소) (+128) orig_ax (syscall 번호 / -1) (+120) PUSH_AND_CLEAR_REGS di / si / dx / cx (+112~+88) ax / RAX — 반환값 (+80) r8 / r9 / r10 / r11 (+72~+48) bx / bp (+40/+32) r12 / r13 / r14 / r15 (+24~+0) ← RSP (low address) — do_syscall_64(regs, nr) 호출 시점 syscall 진입: 소프트웨어가 5개 필드 수동 push 인터럽트: CPU 하드웨어가 자동으로 push • swapgs: GS base 교체 (유저 → 커널 per-CPU 영역) • RSP: cpu_current_top_of_stack에서 커널 스택 로드 • RIP/RFLAGS: syscall이 rcx, r11에 자동 저장 시스템 콜 번호 (원본) · 인터럽트는 에러 코드 또는 $-1 PUSH_AND_CLEAR_REGS — 저장 + 0 초기화 • Spectre 완화: 유저 데이터를 커널 코드로 전달 차단 • r15 ~ rdi 전부 저장 후 0으로 클리어 • do_syscall_64(regs, nr): rdi=pt_regs*, rsi=syscall# • 복귀 시 POP_REGS로 역순 복원 → sysretq • 인터럽트 복귀: iretq (권한 레벨 전환 포함)

x86_64 시스템 콜 진입 (상세)

/* arch/x86/entry/entry_64.S - 시스템 콜 진입 흐름 */
SYM_CODE_START(entry_SYSCALL_64)
    /* 1. GS base 교체: 유저 → 커널 per-CPU */
    swapgs

    /* 2. 유저 RSP를 per-CPU에 임시 저장 */
    movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)

    /* 3. 커널 스택으로 전환 */
    movq    PER_CPU_VAR(cpu_current_top_of_stack), %rsp

    /* 4. 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에 저장) */

    /* 5. 에러 코드 + 범용 레지스터 저장 */
    pushq   $-1                     /* 에러 코드 자리 (syscall은 없음) */
    PUSH_AND_CLEAR_REGS rax=$__NR_syscall_max

    /* 6. C 함수 호출: do_syscall_64(regs, nr) */
    movq    %rsp, %rdi              /* 첫 번째 인자: pt_regs 포인터 */
    movslq  %eax, %rsi              /* 두 번째 인자: syscall 번호 */
    call    do_syscall_64
    /* ... 복귀 경로 ... */
SYM_CODE_END(entry_SYSCALL_64)

컨텍스트 저장/복원 (pt_regs)

/* arch/x86/include/asm/ptrace.h */
struct pt_regs {
    unsigned long r15, r14, r13, r12;
    unsigned long bp;    /* rbp */
    unsigned long bx;    /* rbx */
    unsigned long r11, r10, r9, r8;
    unsigned long ax;    /* rax - syscall 반환값 */
    unsigned long cx, dx, si, di;
    unsigned long orig_ax;  /* syscall 번호 원본 */
    unsigned long ip;    /* RIP */
    unsigned long cs;
    unsigned long flags; /* RFLAGS */
    unsigned long sp;    /* RSP */
    unsigned long ss;
};

인터럽트 진입 경로

인터럽트는 시스템 콜과 달리 어디서든 발생할 수 있으므로 더 신중한 컨텍스트 저장이 필요합니다. x86_64에서 CPU는 자동으로 SS, RSP, RFLAGS, CS, RIP를 IST(Interrupt Stack Table) 또는 커널 스택에 푸시합니다.

/* 인터럽트 진입 매크로 (개념적) */
.macro idtentry_irq vector cfunc
SYM_CODE_START(asm_\cfunc)
    /* CPU가 자동 저장: SS, RSP, RFLAGS, CS, RIP */
    pushq   $~(\vector)          /* 에러 코드 위치에 벡터 저장 */
    call    error_entry           /* 범용 레지스터 저장, swapgs */
    movq    %rsp, %rdi           /* pt_regs 포인터 */
    movl    \vector, %esi        /* IRQ 번호 */
    call    \cfunc               /* C 핸들러 호출 */
    jmp     error_return         /* 레지스터 복원, iretq */
SYM_CODE_END(asm_\cfunc)
.endm

예외/NMI 처리 어셈블리

커널의 예외·인터럽트·NMI 처리는 어셈블리 진입점에서 시작됩니다. CPU가 자동으로 저장하는 레지스터, 에러 코드 유무, 스택 전환 메커니즘(IST)을 정확히 이해해야 커널 크래시를 디버깅할 수 있습니다.

x86_64 예외/인터럽트/NMI 진입 비교 CPU 자동 push RSP+40: SS RSP+32: RSP (이전) RSP+24: RFLAGS RSP+16: CS RSP+8 : RIP RSP+0 : Error Code (에러 코드는 일부 예외만) CPL 변경 시 → TSS.RSP0 IST 설정 시 → TSS.ISTn 주요 예외 벡터 #DE (0) Divide Error [no err] #DB (1) Debug [no err] NMI (2) Non-Maskable Int [IST1] #BP (3) Breakpoint [no err] #DF (8) Double Fault [IST2,err=0] #TS (10) Invalid TSS [err] #NP (11) Seg Not Present [err] #SS (12) Stack-Segment Fault[err] #GP (13) General Protection [err] #PF (14) Page Fault [err=fault] #MC (18) Machine Check [IST3] IST (Interrupt Stack Table) TSS 내 7개 전용 스택 포인터 IST1: NMI 스택 IST2: Double Fault 스택 IST3: MCE 스택 IST4: Debug 스택 → 중첩 불가 예외에 전용 스택 → 커널 스택 오버플로에서도 동작 → per-CPU: cpu_entry_area 내 #PF Page Fault 에러 코드 비트 bit 0 (P) : 0=not-present, 1=protection bit 1 (W/R) : 0=read, 1=write bit 2 (U/S) : 0=supervisor, 1=user bit 3 (RSVD): 1=reserved bit set bit 4 (I/D) : 1=instruction fetch bit 5 (PK) : 1=protection key violation bit 6 (SS) : 1=shadow stack access CR2 = fault 주소 (CPU가 자동 저장) NMI 중첩 문제와 해결 NMI는 IST1 사용 → 고정 스택 문제: NMI 중 NMI → 같은 IST 스택! 해결: 1) IRET 실행 전까지 NMI 블록 2) 커널 스택으로 복사 후 처리 3) repeat_nmi 로 재진입 처리 arch/x86/entry/entry_64.S: asm_exc_nmi → 약 300줄의 복잡한 NMI 처리

Page Fault 핸들러 어셈블리 경로

Page fault는 커널에서 가장 빈번한 예외입니다. 유저 공간 메모리 접근, 지연 매핑(lazy allocation), Copy-on-Write 모두 #PF를 경유합니다.

/* arch/x86/entry/entry_64.S — Page Fault 진입 */
SYM_CODE_START(asm_exc_page_fault)
    endbr64
    /* CPU가 자동 push: SS, RSP, RFLAGS, CS, RIP, error_code */

    /* 1. KPTI: 유저→커널 페이지 테이블 전환 */
    ALTERNATIVE "jmp .Lpf_no_kpti", "", X86_FEATURE_PTI
    pushq   %rax
    movq    %cr3, %rax
    andq    $(~PTI_USER_PGTABLE_AND_PCID_MASK), %rax
    movq    %rax, %cr3          /* 커널 페이지 테이블로 전환 */
    popq    %rax
.Lpf_no_kpti:

    /* 2. 에러 코드를 orig_rax 위치에 저장 */
    pushq   %rax                /* pt_regs.orig_ax = error_code */

    /* 3. 전체 레지스터 저장 (pt_regs 구성) */
    PUSH_AND_CLEAR_REGS
    movq    %rsp, %rdi          /* 첫 번째 인자: pt_regs * */
    movq    ORIG_RAX(%rsp), %rsi /* 두 번째 인자: error_code */

    /* 4. C 핸들러 호출 */
    call    exc_page_fault      /* mm/fault.c */

    /* 5. 복귀 */
    jmp     error_return
SYM_CODE_END(asm_exc_page_fault)

/* mm/fault.c — C 레벨 핸들러 (간략) */
void exc_page_fault(struct pt_regs *regs, unsigned long error_code)
{
    unsigned long address = read_cr2();  /* fault 주소 */

    if (user_mode(regs))
        handle_user_fault(regs, error_code, address);
    else
        handle_kernel_fault(regs, error_code, address);
        /* → fixup_exception() → __ex_table 검색 → 복구 또는 oops */
}

General Protection Fault (#GP) 경로

#GP는 세그먼트 위반, 비정규 주소 접근, 특권 명령어 실행 등 다양한 원인으로 발생합니다. 커널에서 #GP가 발생하면 대부분 oops로 이어집니다.

/* #GP 에러 코드 해석 */
/* error_code = 0         : 일반적 보호 위반 */
/* error_code = selector  : 세그먼트 관련 위반 */
/*   bit 0 (EXT): 외부 이벤트로 인한 예외 */
/*   bit 1 (IDT): IDT의 디스크립터 참조 */
/*   bit 2 (TI) : 0=GDT, 1=LDT */
/*   bit 15:3   : 세그먼트 셀렉터 인덱스 */

/* 커널 #GP 발생 시 oops 출력 예시 */
/* general protection fault, probably for non-canonical address 0xdead... */
/* CPU: 3 PID: 1234 Comm: myapp Not tainted */
/* RIP: 0010:some_function+0x42/0x100 */
/* RSP: 0018:ffffc90000003e00 EFLAGS: 00010246 */
/* Code: 48 8b 07 48 85 c0 74 0a ... */

NMI 핸들러 어셈블리

NMI는 마스크 불가능하므로 커널의 어떤 지점에서든 발생할 수 있습니다. 심지어 다른 예외 처리 중이나 swapgs 도중에도 발생 가능하여, 특별한 주의가 필요합니다.

/* arch/x86/entry/entry_64.S — NMI 핸들러 (핵심 흐름) */
SYM_CODE_START(asm_exc_nmi)
    /* NMI는 IST1 스택으로 진입 */

    /* 1. 중첩 NMI 감지 */
    /*    IST 스택 top에 마커를 두고 재진입 여부 판단 */
    testb   $3, CS-RIP+8(%rsp)    /* 유저 모드에서 왔나? */
    jnz     .Lnmi_from_user

.Lnmi_from_kernel:
    /* 2. 커널 모드 NMI: 커널 스택이 유효한지 확인 */
    /*    이전 NMI 처리 중이면 repeat_nmi로 분기 */
    movq    %rsp, %rdx
    cmpq    nmi_stack_top(%rip), %rdx
    ja      .Lnmi_nested

    /* 3. 스택 프레임을 커널 스택으로 복사 */
    /*    IST 스택은 NMI 고유이므로 장기 사용 불가 */
    pushq   %rdx
    PUSH_AND_CLEAR_REGS

    /* 4. C 핸들러 호출 */
    movq    %rsp, %rdi
    call    exc_nmi                /* perf, watchdog, kgdb 등 */

    /* 5. 복귀: IRET으로만 (sysret 사용 불가) */
    POP_REGS
    addq    $8, %rsp              /* error code skip */
    iretq                          /* IRET 실행 → NMI 블록 해제 */
SYM_CODE_END(asm_exc_nmi)

/* NMI 용도 */
/* - perf: PMU 카운터 오버플로 → 샘플링 */
/* - watchdog: hard lockup 감지 (hrtimer → perf NMI) */
/* - kgdb: 원격 디버거 breakpoint */
/* - MCE: 일부 Machine Check도 NMI로 전달 */

Machine Check Exception (#MC) 처리

MCE는 하드웨어 오류(ECC 메모리, CPU 내부, 버스 등)를 보고합니다. IST3 스택을 사용하며, 복구 가능/불가능 여부에 따라 처리가 달라집니다.

/* MCE 처리 흐름 */
/* 1. CPU가 #MC 예외 발생 → IST3 스택으로 전환 */
/* 2. asm_exc_machine_check → exc_machine_check() */
/* 3. MSR에서 MCE 뱅크 읽기 (IA32_MCi_STATUS/ADDR/MISC) */

/* MCE 뱅크 읽기 어셈블리 */
static inline u64 mce_rdmsrl(u32 msr)
{
    u64 val;
    asm volatile (
        "1: rdmsr\n"
        "2:\n"
        ".pushsection __ex_table,\"a\"\n"
        "  .balign 4\n"
        "  .long 1b - ., 2b - .\n"       /* 실패 시 건너뜀 */
        ".popsection\n"
        : "=A" (val)
        : "c" (msr)
    );
    return val;
}

/* IA32_MCi_STATUS 비트 필드 */
/* bit 63 (VAL)  : 유효한 에러 */
/* bit 62 (OVER) : 오버플로 (이전 에러 손실) */
/* bit 61 (UC)   : 복구 불가능 (Uncorrected) */
/* bit 60 (EN)   : 에러 보고 활성화 */
/* bit 59 (MISCV): MISC 레지스터 유효 */
/* bit 58 (ADDRV): ADDR 레지스터 유효 */
/* bit 57 (PCC)  : 프로세서 컨텍스트 손상 → 패닉 */
/* bit 56 (S)    : Signaling (SRAR/SRAO 구분) */
/* bit 55 (AR)   : Action Required (즉시 조치 필요) */
⚠️

Double Fault 주의: 커널 스택 오버플로 시 #PF → 같은 스택에서 다시 #PF → #DF(Double Fault)가 발생합니다. IST2 스택을 사용하므로 스택 상태와 무관하게 처리됩니다. Double Fault는 무조건 패닉이지만, 스택 트레이스를 통해 원인(깊은 재귀, 대형 지역 변수 등)을 파악할 수 있습니다.

어셈블리 메모리 배리어

현대 CPU는 성능을 위해 메모리 연산 순서를 재배치합니다. 멀티프로세서 환경에서 올바른 동작을 보장하려면 메모리 배리어가 필수입니다.

어셈블리 메모리 배리어 — 재배치 방지 메커니즘 배리어 없음 — CPU 재배치 발생 가능 CPU 0 CPU 1 STORE A = 1 ↕ 재배치? STORE B = 1 LOAD B LOAD A → 0 ! ❌ STORE A가 B보다 늦게 전파될 수 있음 → A=0 관찰 버그 배리어 있음 — 순서 보장 CPU 0 CPU 1 STORE A = 1 wmb() ←→ rmb() STORE B = 1 LOAD B LOAD A = 1 ✓ ✓ STORE A 반드시 STORE B 이전에 전파 → A=1 보장 배리어 종류 전체 배리어 mb() Load + Store — 이전 mb() — 이 선을 못 넘음 Load + Store — 이후 x86: mfence ARM64: dmb sy 로드 배리어 rmb() Load — 이전 (Store는 자유) rmb() — Load만 이 선을 못 넘음 Load — 이후 x86: lfence ARM64: dmb ld 스토어 배리어 wmb() Store — 이전 (Load는 자유) wmb() — Store만 이 선을 못 넘음 Store — 이후 x86: sfence ARM64: dmb st 아키텍처별 구현 매크로 x86 구현 ARM64 구현 효과 mb() asm volatile("mfence" ::: "memory") asm volatile("dmb sy" ::: "memory") 전체 배리어 rmb() asm volatile("lfence" ::: "memory") asm volatile("dmb ld" ::: "memory") 로드 배리어 wmb() asm volatile("sfence" ::: "memory") asm volatile("dmb st" ::: "memory") 스토어 배리어 smp_mb() lock; addl $0, -4(%%rsp) asm volatile("dmb ish" ::: "memory") SMP 전체 배리어 barrier() asm volatile("" ::: "memory") asm volatile("" ::: "memory") 컴파일러 배리어만

x86 메모리 배리어

x86은 비교적 강한 메모리 순서를 보장하는 TSO (Total Store Ordering) 모델을 사용합니다. 그러나 일부 상황에서는 명시적 배리어가 필요합니다.

/* x86 하드웨어 배리어 명령어 */
mfence                          /* 전체 배리어: 모든 load/store 완료 보장 */
lfence                          /* 로드 배리어: 이전 load 완료 후 다음 실행 */
sfence                          /* 스토어 배리어: 이전 store 완료 보장 */

/* lock 접두사: 원자적 RMW + 전체 배리어 효과 */
lock incl (%rdi)                /* 원자적 증가 + 암묵적 mfence */
lock cmpxchgl %eax, (%rdi)      /* 원자적 CAS */
lock xaddl %eax, (%rdi)         /* 원자적 교환 후 덧셈 */
/* 커널 배리어 매크로 (arch/x86/include/asm/barrier.h) */
#define mb()     asm volatile("mfence" ::: "memory")
#define rmb()    asm volatile("lfence" ::: "memory")
#define wmb()    asm volatile("sfence" ::: "memory")

/* SMP 배리어: UP에서는 컴파일러 배리어만 */
#define smp_mb()   asm volatile("lock; addl $0,-4(%%rsp)" ::: "memory", "cc")
#define smp_rmb()  barrier()  /* x86 TSO: load는 순서 보장 */
#define smp_wmb()  barrier()  /* x86 TSO: store는 순서 보장 */

/* 컴파일러 배리어 (하드웨어 배리어 아님!) */
#define barrier() asm volatile("" ::: "memory")

ARM64 메모리 배리어

ARM64는 x86보다 약한 메모리 순서 모델을 사용하므로 배리어가 더 중요합니다.

/* ARM64 배리어 명령어 */
dmb     sy                      /* Data Memory Barrier (전체) */
dmb     ish                     /* Inner Shareable 도메인 */
dmb     ishld                   /* Inner Shareable, Load만 */
dmb     ishst                   /* Inner Shareable, Store만 */

dsb     sy                      /* Data Synchronization Barrier */
dsb     ish                     /* 모든 이전 메모리 접근 완료 보장 */

isb                             /* Instruction Synchronization Barrier */
                                /* 파이프라인 플러시, 시스템 레지스터 변경 후 필요 */
/* ARM64 커널 배리어 (arch/arm64/include/asm/barrier.h) */
#define mb()     asm volatile("dmb sy" ::: "memory")
#define rmb()    asm volatile("dmb ld" ::: "memory")
#define wmb()    asm volatile("dmb st" ::: "memory")

#define smp_mb()   asm volatile("dmb ish" ::: "memory")
#define smp_rmb()  asm volatile("dmb ishld" ::: "memory")
#define smp_wmb()  asm volatile("dmb ishst" ::: "memory")

/* ARM64 Acquire/Release 의미론 (LDAR/STLR) */
/* smp_load_acquire → LDAR: 이후 load/store가 앞으로 재배치 안 됨 */
/* smp_store_release → STLR: 이전 load/store가 뒤로 재배치 안 됨 */
💡

x86의 TSO 모델에서는 smp_rmb()smp_wmb()가 컴파일러 배리어만으로 충분하지만, ARM64에서는 실제 하드웨어 배리어 명령어가 필요합니다. 이것이 커널이 아키텍처별 배리어 매크로를 제공하는 이유입니다.

문자열/메모리 연산 어셈블리

x86의 REP 접두사 문자열 연산과 ERMS/FSRM 최적화는 커널의 memcpy, memset, copy_to_user 등 핵심 메모리 연산의 기반입니다.

REP MOVSB/STOSB/CMPSB

REP 접두사는 RCX 카운터가 0이 될 때까지 문자열 명령어를 반복 실행합니다. 방향 플래그(DF)가 주소 증감 방향을 결정합니다.

명령어동작소스목적지카운터
REP MOVSB바이트 복사RSIRDIRCX (감소)
REP MOVSQ8바이트 복사RSIRDIRCX (감소)
REP STOSB바이트 채우기ALRDIRCX (감소)
REP STOSQ8바이트 채우기RAXRDIRCX (감소)
REPE CMPSB바이트 비교 (같은 동안)RSIRDIRCX (감소)
REPNE SCASB바이트 검색 (다른 동안)ALRDIRCX (감소)
/* 기본 메모리 복사: RDI=dest, RSI=src, RCX=count */
    cld               /* DF=0: 주소 증가 방향 */
    rep movsb        /* while(RCX--) *RDI++ = *RSI++ */

/* 메모리 채우기: RDI=dest, AL=value, RCX=count */
    cld
    rep stosb        /* while(RCX--) *RDI++ = AL */

/* 8바이트 단위 복사 (더 효율적) */
    mov   %rcx, %rax
    shr   $3, %rcx     /* count / 8 */
    rep movsq          /* 8바이트씩 복사 */
    mov   %rax, %rcx
    and   $7, %rcx     /* 나머지 바이트 */
    rep movsb          /* 1바이트씩 나머지 복사 */
⚠️

방향 플래그(DF) 주의: 커널은 항상 CLD(DF=0, 주소 증가)를 가정합니다. System V ABI도 함수 진입 시 DF=0을 요구합니다. STD로 DF=1을 설정한 후에는 반드시 CLD로 복원해야 합니다. 커널 코드에서 DF=1인 상태로 인터럽트가 발생하면 심각한 버그가 됩니다.

ERMS/FSRM 최적화

Enhanced REP MOVSB/STOSB(ERMS)와 Fast Short REP MOV(FSRM)는 마이크로아키텍처 수준에서 REP MOV 성능을 크게 향상시킵니다.

기능CPUID크기성능도입
ERMSCPUID.7.0:EBX[9]≥ 256B 최적대량 복사 시 REP MOVSQ보다 빠름Ivy Bridge
FSRMCPUID.7.0:EDX[4]≤ 128B 최적작은 복사에서도 빠른 REP MOVSBIce Lake
FZRMCPUID.7.0:EDX[10]모든 크기제로 길이 REP MOV 최적화Sapphire Rapids
/* arch/x86/lib/memcpy_64.S — ALTERNATIVE 기반 memcpy 선택 */
SYM_FUNC_START(__memcpy)
    ALTERNATIVE_2 "jmp memcpy_orig",       \
                   "jmp memcpy_erms", X86_FEATURE_ERMS, \
                   "jmp memcpy_fsrm", X86_FEATURE_FSRM

/* ERMS 경로: 단순하지만 대량 데이터에 최적 */
SYM_FUNC_START_LOCAL(memcpy_erms)
    movq %rdi, %rax   /* 반환값 = dest */
    movq %rdx, %rcx   /* count */
    rep movsb         /* ERMS 최적화 경로 */
    RET
SYM_FUNC_END(memcpy_erms)

커널 memcpy/memset 어셈블리

커널의 memcpymemset은 CPU 피처에 따라 런타임에 최적 구현이 선택됩니다.

커널 memcpy 결정 트리 memcpy(dest, src, n) X86_FEATURE_FSRM? Yes REP MOVSB No X86_FEATURE_ERMS? Yes REP MOVSB (ERMS) No memcpy_orig (레지스터 기반) memcpy_orig 전략 1. 정렬 맞춤 (unaligned head) 2. 64B 루프 (MOV 레지스터 8개) 3. 8B 루프 (REP MOVSQ) 4. 나머지 바이트 (REP MOVSB) ALTERNATIVE 매크로로 런타임 패칭

copy_to_user/copy_from_user

유저↔커널 메모리 복사는 페이지 폴트 발생 가능성과 SMAP 보호를 고려해야 합니다. 예외 테이블과 STAC/CLAC로 안전하게 처리합니다.

copy_from_user 흐름 (SMAP 활성) access_ok() 주소 범위 검증 STAC SMAP 해제 (AC=1) REP MOVSB 유저→커널 복사 CLAC SMAP 복원 (AC=0) 반환 (복사된 바이트) 실패 시 미복사 바이트 수 #PF 예외 예외 테이블 (_ASM_EXTABLE) 1. #PF 핸들러에서 fault 주소 확인 2. __ex_table에서 fixup 주소 검색 3. fixup 코드로 점프 (나머지 0으로 채움) 4. CLAC + 미복사 바이트 수 반환 → -EFAULT로 변환 (상위 호출자)
/* arch/x86/lib/copy_user_64.S */
SYM_FUNC_START(copy_user_generic_unrolled)
    stac                    /* SMAP 해제: 유저 메모리 접근 허용 */
    cmpl $8, %edx
    jb   .Lbyte_copy

    /* 정렬 맞춤 */
    movl %edi, %ecx
    andl $7, %ecx
    jz   .Lqword_copy
    subl $8, %ecx
    negl %ecx
    subl %ecx, %edx
0:  movb (%rsi), %al       /* ← fault 가능 지점 */
1:  movb %al, (%rdi)
    ...

    clac                    /* SMAP 복원 */
    RET

    /* 예외 테이블 엔트리 */
    _ASM_EXTABLE_UA(0b, .Lcopy_user_handle_tail)
    _ASM_EXTABLE_UA(1b, .Lcopy_user_handle_tail)

.Lcopy_user_handle_tail:
    clac                    /* 반드시 CLAC */
    movl %edx, %ecx         /* 남은 바이트 수 */
    xorl %eax, %eax
    rep stosb              /* 나머지를 0으로 채움 */
    movl %edx, %eax         /* 미복사 바이트 수 반환 */
    RET
SYM_FUNC_END(copy_user_generic_unrolled)
💡

_ASM_EXTABLE 매크로: _ASM_EXTABLE(fault_addr, fixup_addr).section __ex_table에 {fault 주소, fixup 주소} 쌍을 기록합니다. 페이지 폴트 핸들러가 fault IP를 이 테이블에서 검색하여 fixup 코드로 분기합니다. _ASM_EXTABLE_UA는 유저 접근(User Access)용 변형입니다.

비트 조작 명령어

커널은 비트맵, 스케줄러 우선순위, 자원 할당 등에서 비트 스캔·조작 명령어를 광범위하게 사용합니다. x86의 전통적인 BSF/BSR부터 BMI1/BMI2 확장까지 다룹니다.

BSF/BSR/BT/BTC/BTR/BTS

명령어동작플래그주의사항
BSF dst, src최하위 1비트 위치 찾기 (LSB→MSB)ZF=1 if src=0src=0이면 dst 미정의
BSR dst, src최상위 1비트 위치 찾기 (MSB→LSB)ZF=1 if src=0src=0이면 dst 미정의
BT base, offset비트 테스트 (CF = bit[offset])CF = tested bit메모리 오퍼랜드 시 느림
BTS base, offset비트 테스트 후 세트CF = old bitLOCK 가능 (atomic)
BTR base, offset비트 테스트 후 리셋CF = old bitLOCK 가능
BTC base, offset비트 테스트 후 보수CF = old bitLOCK 가능
TZCNT dst, src후행 0 카운트 (BSF 대체)CF=1 if src=0, ZFBMI1 필요, src=0도 정의됨
LZCNT dst, src선행 0 카운트 (BSR 대체)CF=1 if src=0, ZFABM/LZCNT 필요
POPCNT dst, src1비트 개수 카운트ZFPOPCNT CPUID 필요
/* BSF/BSR 커널 사용 패턴 */
static inline unsigned long __ffs(unsigned long word)
{
    asm("rep; bsf %1,%0"    /* rep bsf = tzcnt (BMI 지원 시) */
        : "=r"(word)
        : "rm"(word));
    return word;
}

static inline unsigned long __fls(unsigned long word)
{
    asm("bsr %1,%0"
        : "=r"(word)
        : "rm"(word));
    return word;
}

/* __ffs vs ffs 차이:
 * __ffs(x): 0-indexed, x=0이면 미정의 (호출자가 보장)
 * ffs(x):   1-indexed, x=0이면 0 반환 (POSIX)
 * ffz(x):   ~x에서 첫 번째 0비트 위치 */
⚠️

BSF/BSR과 TZCNT/LZCNT 호환성: rep bsf는 BMI1 지원 CPU에서 tzcnt로 해석되어 src=0일 때도 정의된 결과(오퍼랜드 비트 크기)를 반환합니다. BMI1 미지원 CPU에서는 rep이 무시되어 일반 bsf로 동작합니다. 커널은 이 트릭을 의도적으로 사용합니다.

BMI1/BMI2 명령어

확장명령어동작인코딩
BMI1ANDN r, v, rmr = ~v & rmVEX.NDS
BEXTR r, rm, v비트필드 추출VEX.NDS
BLSI r, rm최하위 1비트 격리 (r = rm & -rm)VEX.NDD
BLSMSK r, rm최하위 1비트까지 마스크VEX.NDD
BLSR r, rm최하위 1비트 리셋VEX.NDD
BMI2BZHI r, rm, v비트 인덱스부터 상위 0으로 클리어VEX.NDS
PDEP r, v, rmParallel Bit DepositVEX.NDS
PEXT r, v, rmParallel Bit ExtractVEX.NDS
MULX r1, r2, rm플래그 미변경 곱셈VEX.NDD
RORX r, rm, imm플래그 미변경 회전VEX
ℹ️

BMI 주의사항: BMI1/BMI2는 VEX 인코딩을 사용하므로 kernel_fpu_begin() 없이 사용할 수 있습니다 (GPR 대상 VEX 명령어는 FPU 상태를 변경하지 않음). 단, AMD Zen/Zen2에서 PDEP/PEXT가 매우 느리므로 (마이크로코드 에뮬레이션) CPU별 분기가 필요할 수 있습니다.

커널 비트 연산 함수

include/asm-generic/bitops/arch/x86/include/asm/bitops.h에 정의된 커널 비트 연산 함수들의 어셈블리 구현입니다.

커널 find_first_bit 알고리즘 find_first_bit(addr, size) 비트맵에서 첫 번째 1 비트 찾기 unsigned long *p = addr while (*p == 0) p++ 워드 단위 스캔 __ffs(*p) BSF/TZCNT 워드 내 비트 위치 반환: (p-addr)*64 + bit 절대 비트 위치 Atomic 비트 연산 (LOCK 접두사) set_bit(nr, addr) → LOCK BTS [addr + nr/64], nr%64 clear_bit(nr, addr) → LOCK BTR [addr + nr/64], nr%64 test_and_set_bit(nr, addr) → LOCK BTS [addr + nr/64], nr%64; SBB result (CF=old bit)
/* arch/x86/include/asm/bitops.h */
static __always_inline void set_bit(long nr, volatile unsigned long *addr)
{
    asm volatile (
        LOCK_PREFIX "btsq %1,%0"
        : "+m"(*(volatile long *)
          (((unsigned long)addr) + (nr >> 6) * 8))
        : "Ir"(nr & 63)
        : "memory"
    );
}

static __always_inline bool test_and_set_bit(long nr, volatile unsigned long *addr)
{
    GEN_BINARY_RMWcc(LOCK_PREFIX "btsq",
                     *addr, "Ir", nr, "%0", c);
    /* CF(carry flag)가 이전 비트 값 → setc로 반환 */
}

특권 명령어

Ring 0(커널 모드)에서만 실행할 수 있는 특권 명령어들은 커널의 하드웨어 제어, 메모리 관리, 가상화 지원의 핵심입니다.

Ring 0 전용 명령어

x86 특권 레벨과 명령어 접근 Ring 0 (커널 모드) 시스템 테이블: LGDT, LIDT, LLDT, LTR, SGDT, SIDT 제어 레지스터: MOV CR0-CR4, MOV DR0-DR7 MSR 접근: RDMSR, WRMSR 인터럽트/메모리: CLI, STI, HLT, INVLPG, INVPCID I/O (IOPL=0): IN, OUT, INS, OUTS SWAPGS, CLTS, LMSW, STAC, CLAC Ring 3 (유저 모드) 조건부 허용 (CR4/MSR 설정에 따라): RDTSC (CR4.TSD=0 일 때) RDPMC (CR4.PCE=1 일 때) RDFSBASE/WRFSBASE (CR4.FSGSBASE=1) 항상 허용: CPUID, RDTSCP, XSAVE/XRSTOR SYSCALL, SYSENTER (커널 진입) 특권 명령어 실행 시: #GP(0) — General Protection Fault → SIGSEGV (프로세스 종료)
명령어Opcode동작커널 사용처
CLIFA인터럽트 비활성화 (IF=0)local_irq_disable()
STIFB인터럽트 활성화 (IF=1)local_irq_enable()
HLTF4프로세서 대기 (인터럽트까지)idle 루프
INVLPG m0F 01 /7TLB 엔트리 무효화flush_tlb_one()
WRMSR0F 30MSR 쓰기 (ECX=addr, EDX:EAX=val)wrmsrl()
RDMSR0F 32MSR 읽기 (ECX=addr → EDX:EAX)rdmsrl()
LGDT m0F 01 /2GDT 레지스터 로드부팅/CPU 초기화
LIDT m0F 01 /3IDT 레지스터 로드부팅/IDT 설정
SWAPGS0F 01 F8GS.base ↔ IA32_KERNEL_GS_BASEsyscall/인터럽트 진입
STAC0F 01 CBAC 플래그 세트 (SMAP 해제)copy_from_user 등
CLAC0F 01 CAAC 플래그 클리어 (SMAP 활성)유저 접근 완료 후

가상화 명령어

Intel VT-x(VMX)는 하드웨어 가상화를 위한 명령어 세트로, KVM의 핵심 기반입니다.

명령어동작모드
VMXON memVMX 활성화 (VMXON 영역 포인터)Root
VMXOFFVMX 비활성화Root
VMLAUNCH현재 VMCS로 게스트 최초 진입Root→Non-root
VMRESUME현재 VMCS로 게스트 재진입Root→Non-root
VMREAD r, fieldVMCS 필드 읽기Root
VMWRITE field, rVMCS 필드 쓰기Root
VMPTRLD memVMCS 포인터 로드 (현재 VMCS 설정)Root
VMPTRST mem현재 VMCS 포인터 저장Root
VMCALLVM Exit 트리거 (hypercall)Non-root
INVEPTEPT TLB 무효화Root
INVVPIDVPID 기반 TLB 무효화Root
/* arch/x86/kvm/vmx/vmx.c — KVM VMX 진입 */
static void vmx_vcpu_run(struct kvm_vcpu *vcpu)
{
    /* 게스트 레지스터 복원 */
    asm volatile (
        /* 호스트 레지스터 저장 */
        "push %%" _ASM_BP "\n\t"
        "push %%" _ASM_DX "\n\t"
        ...
        /* 게스트 레지스터 로드 */
        "mov %c[rax](%0), %%" _ASM_AX "\n\t"
        "mov %c[rcx](%0), %%" _ASM_CX "\n\t"
        ...
        /* VM Entry */
        "jne 1f\n\t"
        "vmlaunch\n\t"          /* 최초 진입 */
        "jmp 2f\n\t"
        "1: vmresume\n\t"       /* 재진입 */
        "2: "
        /* VM Exit: 여기로 복귀 */
        ...
    );
}

SGX 명령어

Intel SGX(Software Guard Extensions)는 하드웨어 격리된 Enclave를 제공합니다. 최신 Intel CPU에서는 SGX가 단계적으로 폐지(deprecated)되고 있습니다.

명령어리프 함수동작
Ring 3 (ENCLU)ENCLUEENTER (EAX=2)Enclave 진입
EEXIT (EAX=4)Enclave 탈출
ERESUME (EAX=3)Enclave 재진입 (인터럽트 후)
EGETKEY (EAX=1)봉인 키 획득
EREPORT (EAX=0)원격 증명 보고서 생성
Ring 0 (ENCLS)ENCLSECREATE (EAX=0)Enclave 생성
EADD (EAX=1)페이지 추가
EINIT (EAX=2)Enclave 초기화
EEXTEND (EAX=6)페이지 측정 확장
ℹ️

SGX 현황: Intel은 11세대(Rocket Lake) 이후 클라이언트 CPU에서 SGX를 제거했으며, 서버 CPU(Xeon)에서만 지원합니다. 커널의 SGX 드라이버(arch/x86/kernel/cpu/sgx/)는 /dev/sgx_enclave/dev/sgx_provision 디바이스를 제공합니다. TDX(Trust Domain Extensions)가 SGX의 후속 기술로 자리잡고 있습니다.

SMAP/SMEP: STAC/CLAC 상세

SMAP(Supervisor Mode Access Prevention)과 SMEP(Supervisor Mode Execution Prevention)는 커널이 유저 메모리를 의도치 않게 접근/실행하는 것을 방지합니다.

/* SMAP 제어 */
static inline void stac(void)
{
    /* STAC: Set AC Flag → SMAP 일시 해제 */
    /* 커널이 유저 메모리 접근 허용 */
    alternative("", __stringify(stac), X86_FEATURE_SMAP);
}

static inline void clac(void)
{
    /* CLAC: Clear AC Flag → SMAP 복원 */
    /* 유저 메모리 접근 시 #PF 발생 */
    alternative("", __stringify(clac), X86_FEATURE_SMAP);
}

/* 사용 패턴 */
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
{
    if (access_ok(from, n)) {
        stac();          /* 유저 접근 허용 */
        n = raw_copy_from_user(to, from, n);
        clac();          /* 유저 접근 차단 복원 */
    }
    return n;
}

/* CR4 비트:
 * CR4.SMAP (비트 21): Supervisor Mode Access Prevention
 *   - 1: Ring 0에서 AC=0일 때 유저 페이지 접근 → #PF
 * CR4.SMEP (비트 20): Supervisor Mode Execution Prevention
 *   - 1: Ring 0에서 유저 페이지 코드 실행 → #PF
 *   - SMEP는 AC 플래그와 무관 (항상 차단)
 */
⚠️

STAC/CLAC 쌍 필수: stac()clac()를 빠뜨리면 커널 전체에서 SMAP 보호가 해제된 상태로 실행됩니다. 특히 예외 경로에서 clac()이 누락되지 않도록 주의해야 합니다. copy_user 계열 함수의 fixup 코드는 반드시 CLAC를 포함합니다.

보안 완화 어셈블리

Spectre, Meltdown 등 투기적 실행 취약점이 발견된 이후, 커널은 어셈블리 수준에서 다양한 완화 기법을 적용합니다. ALTERNATIVE 매크로, Static Key / Jump Label, Retpoline, IBRS/IBPB 등은 모두 어셈블리와 직결됩니다.

커널 보안 완화 어셈블리 기법 Retpoline (Spectre v2) 간접 분기 → call+ret 시퀀스 call .Ltarget .Lspec_trap: lfence; jmp .Lspec_trap .Ltarget: mov %rax,(%rsp); ret → RSB 오염으로 투기 실행 차단 → CONFIG_RETPOLINE=y ALTERNATIVE 매크로 부팅 시 CPU 피처 감지→코드 패칭 ALTERNATIVE "nop;nop;nop", "lfence", X86_FEATURE_LFENCE 부팅 전: nop;nop;nop (3바이트) 부팅 후: lfence (3바이트 패칭) .altinstructions 섹션에 메타데이터 apply_alternatives()가 부팅 시 적용 Static Key / Jump Label 조건 분기 → nop 또는 jmp 패칭 DEFINE_STATIC_KEY_FALSE(key); 비활성: .byte 0x0f,0x1f,0x44, 0x00,0x00 (5B nop) 활성화: e9 xx xx xx xx (jmp) text_poke_bp()로 라이브 패칭 → 분기 예측 오버헤드 제거 IBRS / IBPB / STIBP MSR 기반 간접 분기 제어 /* IBPB: 분기 예측 버퍼 무효화 */ wrmsr PRED_CMD, IBPB_BIT /* IBRS: 간접 분기 제한 */ wrmsr SPEC_CTRL, IBRS_BIT 컨텍스트 스위치마다 IBPB 발행 eIBRS: 커널 진입 시 자동 적용 spectre_v2= 부트 파라미터로 제어 SLS / BHI 완화 직선 투기 실행 차단 /* SLS: ret/jmp 뒤에 int3 삽입 */ ret; int3 /* CC 바이트 */ /* BHI: 분기 히스토리 무효화 */ BHI_MITIGATION_CALL CONFIG_SLS=y 활성화 objtool이 누락 검증 -mharden-sls=all 컴파일러 옵션 SMAP/SMEP/CET 하드웨어 보호 + 어셈블리 제어 /* SMAP: 커널의 유저 메모리 접근 */ stac /* AC 플래그 세트: 허용 */ clac /* AC 플래그 클리어: 차단 */ /* CET: Shadow Stack */ endbr64 /* 간접 분기 타겟 표시 */ SMEP: 커널의 유저 코드 실행 차단 CR4.SMAP/SMEP 비트로 제어 IBT: 간접 분기 추적 (CET)

Retpoline 상세 동작

Retpoline은 간접 호출(call *%rax)을 call+ret 시퀀스로 변환하여 CPU의 간접 분기 예측기(BTB)가 투기적으로 공격자 제어 주소를 실행하는 것을 방지합니다.

/* arch/x86/lib/retpoline.S — __x86_indirect_thunk_rax */
SYM_FUNC_START(__x86_indirect_thunk_rax)
    /* 1. 반환 주소를 스택에 push하면서 .Lspec_trap으로 점프 */
    call    .Ltarget               /* RSB에 .Ltarget 주소 기록 */

.Lspec_trap:
    /* 2. 투기적 실행이 여기로 올 경우 무한 루프에 갇힘 */
    lfence                         /* 투기 실행 직렬화 (Intel) */
    jmp     .Lspec_trap            /* 실제로는 도달 불가 */

.Ltarget:
    /* 3. 스택 top의 반환 주소를 목표 주소로 교체 */
    movq    %rax, (%rsp)           /* call이 push한 .Lspec_trap을 덮어씀 */
    /* 4. ret로 간접 점프 → RSB 예측은 .Lspec_trap, 실제는 %rax */
    RET                            /* ret; int3 (SLS 완화 포함) */
SYM_FUNC_END(__x86_indirect_thunk_rax)

/* 컴파일러가 생성하는 호출 코드 (retpoline 모드) */
/* call *%rax  →  call __x86_indirect_thunk_rax */

ALTERNATIVE 매크로 구조

커널은 부팅 시점에 apply_alternatives()를 호출하여 .altinstructions 섹션의 메타데이터를 순회하면서, CPU 피처 비트에 따라 코드를 런타임 패칭합니다.

/* arch/x86/include/asm/alternative.h — ALTERNATIVE 매크로 */
#define ALTERNATIVE(oldinstr, newinstr, feature)          \
    "661:\n\t" oldinstr "\n662:\n"                        \
    ".pushsection .altinstructions,\"a\"\n"               \
    "  .long 661b - .\n"     /* 원본 명령어 오프셋 */   \
    "  .long 663f - .\n"     /* 대체 명령어 오프셋 */   \
    "  .word " __stringify(feature) "\n" /* CPU 피처 */ \
    "  .byte 662b-661b\n"    /* 원본 길이 */           \
    "  .byte 664f-663f\n"    /* 대체 길이 */           \
    ".popsection\n"                                       \
    ".pushsection .altinstr_replacement,\"ax\"\n"         \
    "663:\n\t" newinstr "\n664:\n"                        \
    ".popsection\n"

/* 사용 예: SMAP이 지원되면 nop → clac으로 패칭 */
static inline void clac(void)
{
    alternative("nop; nop; nop",                /* 기본: 3바이트 nop */
                "clac",                         /* SMAP 지원 시 */
                X86_FEATURE_SMAP);
}

/* 다중 대안 예: 3가지 구현 중 선택 */
ALTERNATIVE_2(
    "call memcpy_orig",                     /* 기본 구현 */
    "call memcpy_erms", X86_FEATURE_ERMS,   /* REP MOVSB 최적화 */
    "call memcpy_fsrm", X86_FEATURE_FSRM    /* Fast Short REP MOV */
)

Static Key 어셈블리 메커니즘

앞의 Static Key와 asm goto, 컴파일부터 런타임 패칭까지의 파이프라인, x86에서 안전하게 패칭되는 이유 절이 전체 메커니즘을 설명했다면, 아래 코드는 그 내용을 x86 바이트 수준에서 다시 압축한 요약입니다.

/* include/linux/jump_label.h + arch/x86/kernel/jump_label.c */

/* C 코드 */
DEFINE_STATIC_KEY_FALSE(my_feature);

void hot_path(void)
{
    /* 비활성 상태: 5바이트 NOP으로 컴파일됨 */
    if (static_branch_unlikely(&my_feature)) {
        do_extra_work();
    }
    /* 핫 패스는 분기 예측 비용 0 */
}

/* 생성되는 어셈블리 (비활성) */
    .byte 0x0f,0x1f,0x44,0x00,0x00   /* 5바이트 NOP (nopl 0(%rax,%rax,1)) */
    /* __jump_table 섹션에 엔트리 기록 */

/* static_branch_enable(&my_feature) 호출 후 패칭 */
    .byte 0xe9,xx,xx,xx,xx           /* 5바이트 JMP rel32 */

/* text_poke_bp() 3단계 안전 패칭 */
/*  1) int3 (0xCC) 1바이트 삽입 → 다른 CPU가 실행하면 breakpoint */
/*  2) 나머지 4바이트 패칭 (int3 뒤이므로 안전) */
/*  3) int3 → jmp opcode(0xE9)로 교체 + sync_core() */

CET: endbr64와 간접 분기 추적 (IBT)

Intel CET (Control-flow Enforcement Technology)의 IBT 모드에서는 모든 간접 분기 타겟에 endbr64 명령어가 있어야 합니다. 없으면 #CP (Control Protection) 예외가 발생합니다.

/* 커널 6.2+: CONFIG_X86_KERNEL_IBT=y */

/* 모든 함수 진입점에 endbr64 자동 삽입 */
SYM_FUNC_START(my_function)
    endbr64                        /* f3 0f 1e fa — 간접 분기 타겟 표시 */
    pushq   %rbp
    ...

/* 인터럽트/예외 핸들러 진입점도 동일 */
SYM_CODE_START(asm_exc_page_fault)
    endbr64
    ...

/* IBT가 미지원이면 ALTERNATIVE로 nop4로 패칭 */
/* endbr64 (4B) → nopl 0(%rax,%rax,1) (4B) */

/* Shadow Stack (CET-SS): CALL은 shadow stack에도 리턴 주소 push */
/* RET 시 일반 스택과 shadow stack 비교 → 불일치 시 #CP 예외 */
/* 커널은 CONFIG_X86_USER_SHADOW_STACK으로 유저 공간 CET-SS 지원 */
완화 기법대상 취약점메커니즘성능 비용커널 설정
RetpolineSpectre v2간접 호출 → call+ret thunk중간 (2-8%)CONFIG_RETPOLINE
IBRS/eIBRSSpectre v2MSR로 간접 분기 제한낮음 (eIBRS)spectre_v2=
IBPBSpectre v2분기 예측 버퍼 플러시낮음컨텍스트 스위치 시
STIBPSpectre v2 (SMT)형제 스레드 간 예측 격리낮음SMT 환경
LFENCE/배리어Spectre v1array_index_nospec()매우 낮음자동 적용
KPTIMeltdown유저/커널 페이지 테이블 분리높음 (5-30%)CONFIG_PAGE_TABLE_ISOLATION
SLSStraight-Line Specret/jmp 뒤 int3 삽입매우 낮음CONFIG_SLS
CET-IBTJOP/COP 공격endbr64 타겟 검증매우 낮음CONFIG_X86_KERNEL_IBT
CET-SSROP 공격Shadow Stack 반환 주소 검증매우 낮음CONFIG_X86_USER_SHADOW_STACK
SMAP커널 → 유저 접근AC 플래그 (stac/clac)매우 낮음자동 (CPU 지원 시)
SMEP커널의 유저 코드 실행CR4.SMEP 비트없음자동 (CPU 지원 시)
ℹ️

완화 상태 확인: cat /sys/devices/system/cpu/vulnerabilities/*로 현재 시스템의 각 취약점별 완화 상태를 확인할 수 있습니다. dmesg | grep -i spectre로 부팅 시 적용된 완화 기법도 확인하세요.

Spectre/Meltdown 완화 심화

2018년 이후 발견된 투기적 실행 취약점들은 커널 어셈블리 수준에서 다양한 완화 기법을 필요로 합니다. 각 취약점별로 MSR 제어, 코드 패칭, 페이지 테이블 분리 등 구체적인 어셈블리 메커니즘을 이해해야 합니다.

IBRS/IBPB/STIBP 상세

간접 분기 예측 공격(Spectre v2)을 완화하기 위한 핵심 MSR 기반 메커니즘입니다.

기능MSR비트동작사용 시점
IBRSIA32_SPEC_CTRL (0x48)비트 0간접 분기 예측 제한커널 진입 시 설정
STIBPIA32_SPEC_CTRL (0x48)비트 1형제 스레드 간 예측 격리SMT 활성 시 설정
SSBDIA32_SPEC_CTRL (0x48)비트 2투기적 저장 바이패스 비활성화프로세스별 선택적
IBPBIA32_PRED_CMD (0x49)비트 0간접 분기 예측기 배리어컨텍스트 전환 시
eIBRSIA32_SPEC_CTRL (0x48)비트 0Enhanced IBRS (진입/탈출 토글 불필요)항상 활성
/* arch/x86/include/asm/nospec-branch.h */
static inline void __wrmsr_ibrs(u64 val)
{
    asm volatile (
        "wrmsr"
        : : "c"(MSR_IA32_SPEC_CTRL),
            "a"((u32)val),
            "d"((u32)(val >> 32))
        : "memory"
    );
}

/* 커널 진입 시 IBRS 설정 */
static inline void indirect_branch_prediction_barrier(void)
{
    asm volatile (
        ALTERNATIVE("" , "wrmsr", X86_FEATURE_USE_IBPB)
        : : "a"(1), "c"(MSR_IA32_PRED_CMD), "d"(0)
        : "memory"
    );
}
ℹ️

eIBRS vs Retpoline: Enhanced IBRS가 지원되는 CPU에서는 Retpoline이 불필요합니다. eIBRS는 커널 모드에서 간접 분기 예측이 사용자 모드 학습 데이터를 사용하지 않도록 하드웨어에서 보장합니다. spectre_v2=eibrs 커널 파라미터로 제어합니다.

KPTI 어셈블리

KPTI(Kernel Page Table Isolation)는 Meltdown 취약점에 대한 완화로, 유저 모드와 커널 모드에서 서로 다른 페이지 테이블을 사용합니다.

KPTI: CR3 스위칭 흐름 유저 모드 CR3 = 유저 PGD 커널 매핑: 최소한 (entry text) 유저 매핑: 전체 비트 12 SET (PCID 비트) 노출 커널 주소: entry/exit code만 syscall sysret Trampoline Stack 1. 유저 CR3 저장 2. 비트 12 클리어 3. MOV CR3 (커널 PGD) 4. 커널 스택으로 전환 IST/trampoline 스택 사용 커널 모드 CR3 = 커널 PGD 커널 매핑: 전체 유저 매핑: 전체 비트 12 CLEAR 전체 커널 주소 공간 접근 CR3 레이아웃 (KPTI 활성) 비트 63:13 — PGD 물리 주소 (4KB 정렬) 비트 12 비트 11:0 — PCID (if CR4.PCIDE=1) 비트 12 = 0: 커널 PGD (전체 매핑) 비트 12 = 1: 유저 PGD (제한 매핑) 커널 PGD와 유저 PGD는 연속 8KB 할당 → 비트 12 토글로 전환 PCID 지원 시 NOFLUSH 비트(63)로 TLB 플러시 최소화
/* arch/x86/entry/calling.h — KPTI CR3 스위치 매크로 */
.macro SWITCH_TO_KERNEL_CR3 scratch_reg:req
    mov     %cr3, \scratch_reg
    andq    $(~PTI_USER_PGTABLE_AND_PCID_MASK), \scratch_reg
    mov     \scratch_reg, %cr3
.endm

.macro SWITCH_TO_USER_CR3_NOSTACK scratch_reg:req scratch_reg2:req
    mov     %cr3, \scratch_reg
    movq    PER_CPU_VAR(cpu_tlbstate + TLB_STATE_user_pcid_flush_mask), \scratch_reg2
    orq     \scratch_reg2, \scratch_reg
    mov     \scratch_reg, %cr3
.endm

/* PTI_USER_PGTABLE_AND_PCID_MASK = PAGE_SIZE (0x1000) */
/* 커널 PGD + 0x1000 = 유저 PGD (연속 할당) */
⚠️

KPTI 성능 영향: CR3 스위칭은 syscall/인터럽트마다 발생하므로 성능 오버헤드가 있습니다 (시스템 콜 집약 워크로드에서 ~5-30%). PCID(Process Context ID) 지원 CPU에서는 TLB 플러시를 최소화하여 오버헤드를 ~1-5%로 줄입니다. NOFLUSH 비트(CR3 비트 63)를 설정하면 MOV CR3가 TLB를 플러시하지 않습니다.

MDS/TAA/MMIO 완화

Microarchitectural Data Sampling(MDS) 계열 취약점은 CPU 내부 버퍼(Store Buffer, Fill Buffer, Load Port)의 잔류 데이터를 투기적 실행으로 읽어내는 공격입니다.

취약점CVE영향 버퍼완화 방법
MFBDS (Fallout)CVE-2018-12126Store BufferVERW
MLPDSCVE-2018-12127Load PortVERW
MSBDS (Zombieload)CVE-2018-12130Fill BufferVERW
MDSUMCVE-2019-11091복합VERW
TAACVE-2019-11135TSX 비동기 중단TSX 비활성 또는 VERW
MMIO Stale DataCVE-2022-21123/25/66MMIO 레지스터VERW + FB_CLEAR
/* arch/x86/include/asm/nospec-branch.h */
/* VERW는 MD_CLEAR가 지원되는 CPU에서 내부 버퍼를 클리어 */
.macro CLEAR_CPU_BUFFERS
    ALTERNATIVE "", "verw _requests(%rip)", X86_FEATURE_CLEAR_CPU_BUF
.endm

/* _requests는 유효한 DS 세그먼트 셀렉터를 가리킴 */
/* VERW 명령어: 쓰기 가능 여부 확인 + 부작용으로 버퍼 클리어 */

/* 커널→유저 복귀 시 버퍼 클리어 */
SYM_FUNC_START(entry_SYSCALL_64)
    ...
    CLEAR_CPU_BUFFERS      /* 유저 복귀 직전 */
    sysretq
SYM_FUNC_END(entry_SYSCALL_64)

L1TF/L1D Flush

L1 Terminal Fault(L1TF)는 PTE의 Present 비트가 0일 때 PFN 필드가 L1D 캐시 태그 검사에 사용되는 취약점입니다. VM exit 시 L1D 캐시를 플러시하여 게스트 간 데이터 누출을 방지합니다.

/* L1D 플러시 — IA32_FLUSH_CMD MSR (0x10B) */
static inline void l1d_flush_hw(void)
{
    asm volatile (
        "wrmsr"
        : : "c"(0x10B),    /* IA32_FLUSH_CMD */
            "a"(1),        /* L1D_FLUSH bit */
            "d"(0)
        : "memory"
    );
}

/* PTE 역전 (L1TF 완화) — Present=0인 PTE의 PFN을 무효화 */
/* arch/x86/include/asm/pgtable.h */
static inline pte_t pte_set_flags(pte_t pte, pteval_t set)
{
    /* Present=0이면 물리 주소 비트를 반전 → L1D 히트 방지 */
    return __pte(native_pte_val(pte) | set);
}
💡

조건부 L1D 플러시: VM entry마다 무조건 플러시하면 성능이 크게 저하됩니다. 커널은 kvm_x86_ops.l1d_flush를 통해 SMT 형제 스레드가 신뢰되지 않은 코드를 실행할 때만 플러시합니다. l1tf=flush 또는 l1tf=flush,nosmt로 제어합니다.

SSBD/PSFD

Speculative Store Bypass(SSB, Spectre v4)는 저장(store) 후 같은 주소의 로드(load)가 투기적으로 이전 값을 읽는 취약점입니다.

/* SSBD — IA32_SPEC_CTRL.SSBD (비트 2) */
static inline void ssbd_set(void)
{
    u64 val = spec_ctrl_current() | SPEC_CTRL_SSBD;
    wrmsrl(MSR_IA32_SPEC_CTRL, val);
}

/* 프로세스별 SSBD 제어 — prctl() */
/* prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_STORE_BYPASS, PR_SPEC_DISABLE) */
/* 스레드 구조체에 TIF_SSBD 플래그 설정 → 컨텍스트 전환 시 MSR 업데이트 */

/* PSFD — Predictive Store Forwarding Disable */
/* AMD Zen 3+에서 지원, 예측적 스토어 포워딩 비활성화 */
/* IA32_SPEC_CTRL 비트 7 (AMD specific) */
ℹ️

완화 상태 종합 확인: cat /sys/devices/system/cpu/vulnerabilities/*로 현재 시스템의 각 취약점별 완화 상태를 확인할 수 있습니다. grep . /sys/devices/system/cpu/vulnerabilities/*로 한 번에 모든 항목을 볼 수 있으며, dmesg | grep -iE "spectre|meltdown|mds|l1tf|tsx"로 부팅 시 적용된 완화 기법을 확인하세요.

성능 카운터 어셈블리

x86과 ARM64의 하드웨어 성능 카운터(PMU)를 어셈블리 수준에서 설정하고 읽는 방법을 다룹니다. 커널의 perf 서브시스템이 사용하는 저수준 메커니즘을 이해합니다.

RDTSC/RDTSCP/RDPMC

타임스탬프 카운터와 성능 카운터를 직접 읽는 명령어입니다.

명령어Opcode결과직렬화권한
RDTSC0F 31EDX:EAX = TSC비직렬화Ring 0/3 (CR4.TSD에 따라)
RDTSCP0F 01 F9EDX:EAX = TSC, ECX = IA32_TSC_AUX읽기 전 직렬화Ring 0/3
RDPMC0F 33EDX:EAX = PMC[ECX]비직렬화Ring 0/3 (CR4.PCE에 따라)
/* 정확한 TSC 읽기 패턴 */
static inline u64 rdtsc_ordered(void)
{
    /* RDTSCP: 이전 명령어 완료 후 TSC 읽기 보장 */
    u32 lo, hi;
    asm volatile (
        "rdtscp"
        : "=a"(lo), "=d"(hi)
        : : "rcx"     /* ECX = IA32_TSC_AUX 클러버 */
    );
    return ((u64)hi << 32) | lo;
}

/* RDTSC + LFENCE 패턴 (RDTSCP 미지원 CPU용) */
static inline u64 rdtsc_lfence(void)
{
    u32 lo, hi;
    asm volatile (
        "lfence\n\t"
        "rdtsc"
        : "=a"(lo), "=d"(hi)
        : : "memory"
    );
    return ((u64)hi << 32) | lo;
}

/* 커널에서 TSC 기반 시간 측정 */
/* ktime_get_ns() → clocksource_tsc → rdtsc_ordered() */
⚠️

TSC 인배리언트: TSC invariant(CPUID.80000007H:EDX[8])가 지원되지 않는 오래된 CPU에서는 TSC가 주파수 변경이나 C-state에서 멈출 수 있습니다. 커널은 tsc_reliable 플래그로 이를 추적하며, 불안정한 TSC가 감지되면 HPET 등 대체 클럭소스로 전환합니다.

PMU 설정 어셈블리

Intel/AMD Performance Monitoring Unit의 MSR을 직접 설정하여 하드웨어 이벤트를 카운트합니다.

x86 PMU 아키텍처 IA32_PERFEVTSELx (0x186 + x) 비트 7:0 Event Select (이벤트 번호) 비트 15:8 Unit Mask (이벤트 세분류) 비트 16 USR | 비트 17 OS | 비트 20 INT 비트 22 EN (카운터 활성화) 비트 31:24 CMASK (조건부 카운트) IA32_PMCx (0xC1 + x) 48비트 카운터 (하드웨어) RDPMC로 사용자 공간에서 읽기 가능 오버플로 시 PMI (NMI) 인터럽트 IA32_PERF_GLOBAL_STATUS에서 오버플로 확인 Fixed 카운터: 명령어/사이클/참조사이클 주요 하드웨어 이벤트 (Intel) Event=0xC0 UMask=0x00 INST_RETIRED.ANY — 리타이어된 명령어 수 Event=0x3C UMask=0x00 CPU_CLK_UNHALTED.THREAD — 비정지 사이클 Event=0x2E UMask=0x41 LONGEST_LAT_CACHE.MISS — LLC 미스 Event=0xD1 UMask=0x01 MEM_LOAD_RETIRED.L1_HIT — L1D 히트 Event=0xC5 UMask=0x20 BR_MISP_RETIRED.ALL_BRANCHES — 분기 예측 실패
/* PMU 직접 설정 예제 */
static inline void pmu_start_counting(u32 event, u32 umask, int counter)
{
    u64 evtsel = event | (umask << 8) |
                (1ULL << 16) |   /* USR: 유저 모드 카운트 */
                (1ULL << 17) |   /* OS: 커널 모드 카운트 */
                (1ULL << 22);    /* EN: 카운터 활성화 */

    /* 카운터 초기화 */
    wrmsrl(MSR_IA32_PMC0 + counter, 0);
    /* 이벤트 선택 및 활성화 */
    wrmsrl(MSR_IA32_PERFEVTSEL0 + counter, evtsel);
}

/* 카운터 읽기 */
static inline u64 pmu_read_counter(int counter)
{
    u64 val;
    rdmsrl(MSR_IA32_PMC0 + counter, val);
    return val;
}

/* NMI 기반 샘플링: 오버플로 시 PMI 발생 */
/* perf_event → hw_perf_event_init() → pmu->enable() */

perf_event 연동

커널의 perf 서브시스템은 하드웨어 PMU 드라이버를 추상화하여 perf_event_open() 시스템 콜로 통합 인터페이스를 제공합니다.

/* perf_event 오픈 흐름 */
/* 사용자: perf_event_open(attr, pid, cpu, ...) */
/* ↓ */
/* 커널: perf_event_alloc() */
/*   → pmu->event_init(event)  // 이벤트 유효성 검사 */
/*   → pmu->add(event, ...)    // 카운터 할당 + MSR 설정 */
/*   → pmu->start(event)       // 카운팅 시작 */
/* ↓ */
/* NMI: perf_event_nmi_handler() */
/*   → pmu->handle_irq(...)    // 오버플로 처리 */
/*   → perf_event_overflow()   // 샘플 기록 */

/* arch/x86/events/core.c — x86 PMU 드라이버 등록 */
static struct pmu pmu = {
    .pmu_enable     = x86_pmu_enable,
    .pmu_disable    = x86_pmu_disable,
    .event_init     = x86_pmu_event_init,
    .add            = x86_pmu_add,
    .del            = x86_pmu_del,
    .start          = x86_pmu_start,
    .stop           = x86_pmu_stop,
    .read           = x86_pmu_read,
};

ARM64 PMU 레지스터

ARM64의 PMU(Performance Monitors Extension)는 시스템 레지스터를 통해 접근합니다.

레지스터접근설명
PMCR_EL0MRS/MSRPMU 제어 (활성화, 리셋, 카운터 수)
PMCNTENSET_EL0MSR카운터 활성화 (비트 세트)
PMCNTENCLR_EL0MSR카운터 비활성화 (비트 클리어)
PMEVCNTR<n>_EL0MRS/MSR이벤트 카운터 n 값
PMEVTYPER<n>_EL0MSR이벤트 카운터 n 이벤트 타입
PMCCNTR_EL0MRS사이클 카운터 (64비트)
PMOVSCLR_EL0MSR오버플로 상태 클리어
PMINTENSET_EL1MSR인터럽트 활성화
PMUSERENR_EL0MSR유저 공간 접근 허용
/* ARM64 PMU 카운터 읽기 */
static inline u64 arm64_read_pmu_counter(int idx)
{
    u64 val;
    asm volatile ("mrs %0, pmevcntr0_el0" : "=r"(val));
    return val;
}

/* 사이클 카운터 읽기 */
static inline u64 arm64_read_ccnt(void)
{
    u64 val;
    asm volatile ("mrs %0, pmccntr_el0" : "=r"(val));
    return val;
}

/* PMU 초기화 */
static inline void arm64_pmu_enable(void)
{
    u64 val;
    asm volatile ("mrs %0, pmcr_el0" : "=r"(val));
    val |= 1;  /* E 비트: PMU 활성화 */
    asm volatile ("msr pmcr_el0, %0" : : "r"(val));
}
ℹ️

perf와 PMU 사용: perf stat -e cycles,instructions,cache-misses ./program으로 하드웨어 카운터를 쉽게 사용할 수 있습니다. 커널 함수 레벨에서는 perf top으로 핫스팟을 실시간 확인하고, perf record -g로 콜 그래프와 함께 샘플을 기록합니다.

어셈블리 디버깅

커널 어셈블리 코드를 디버깅하는 것은 C 코드보다 어렵지만, 적절한 도구를 활용하면 효율적으로 문제를 찾을 수 있습니다.

커널 어셈블리 디버깅 도구 비교 objdump 정적 디스어셈블 | 빌드 후 오프라인 분석 objdump -d vmlinux 전체 커널 디스어셈블 objdump -d -S -l vmlinux C 소스와 어셈블리 혼합 objdump -d my_module.ko 커널 모듈 디스어셈블 ✓ 빠름, QEMU 불필요 ✗ 정적 분석만 (실행 상태 불가) 적합: 코드 구조 파악, 함수 크기 확인 GDB + QEMU 동적 디버깅 | 브레이크포인트·레지스터 실시간 qemu-system-x86_64 -s -S ... QEMU 중단 대기 모드 시작 gdb vmlinux → target remote :1234 GDB 원격 연결 stepi / nexti / info registers 명령어 단위 스텝, 레지스터 확인 ✓ 정확한 실행 경로, 메모리 덤프 ✗ QEMU 환경 구성 필요, 느림 적합: 버그 재현, 크래시 원인 추적 ftrace function_graph 런타임 추적 | 실제 하드웨어에서 호출 흐름 echo function_graph > current_tracer function_graph 트레이서 설정 echo func > set_graph_function 특정 함수 하위만 추적 cat trace 호출 트리 + 실행 시간 확인 ✓ 실제 커널, 오버헤드 적음 ✗ 어셈블리 레벨 아닌 함수 단위 적합: 함수 호출 경로·지연 시간 분석 선택 가이드: 코드 구조 파악 → objdump | 실행 흐름/버그 재현 → GDB+QEMU | 성능/호출 경로 분석 → ftrace

objdump로 디스어셈블

# 커널 이미지 디스어셈블 (특정 함수)
objdump -d vmlinux | grep -A 30 '<do_syscall_64>:'

# 인라인 어셈블리 확인: C 소스와 어셈블리 혼합 출력
objdump -d -S -l vmlinux | less

# 특정 섹션만 디스어셈블
objdump -d -j .text vmlinux

# 모듈 디스어셈블
objdump -d my_module.ko

# 릴로케이션 정보 확인
objdump -r my_module.ko

GDB 디스어셈블

# QEMU + GDB로 커널 디버깅
qemu-system-x86_64 -kernel bzImage -s -S ...

# 다른 터미널에서
gdb vmlinux
(gdb) target remote :1234
(gdb) disassemble do_syscall_64       # 함수 디스어셈블
(gdb) x/20i $rip                      # 현재 위치부터 20개 명령어
(gdb) display/i $pc                   # 매 스텝마다 현재 명령어 표시
(gdb) stepi                           # 명령어 단위 스텝 (si)
(gdb) nexti                           # call 건너뛰기 (ni)
(gdb) info registers                  # 레지스터 확인
(gdb) p/x $rax                        # 특정 레지스터 값

# Intel 문법으로 전환
(gdb) set disassembly-flavor intel

ftrace function_graph로 흐름 추적

# function_graph tracer로 함수 호출 흐름 시각화
cd /sys/kernel/debug/tracing
echo function_graph > current_tracer
echo do_syscall_64 > set_graph_function   # 특정 함수 하위만
echo 1 > tracing_on
# ... 작업 수행 ...
cat trace

# 출력 예시:
#  0)               |  do_syscall_64() {
#  0)   0.150 us    |    syscall_enter_from_user_mode();
#  0)               |    __x64_sys_write() {
#  0)               |      ksys_write() {
#  0)   0.087 us    |        __fdget_pos();
#  0)               |        vfs_write() {
#  0)   ...

링커 스크립트 & 어셈블리 연동

커널 이미지(vmlinux)의 최종 레이아웃은 링커 스크립트(vmlinux.lds.S)가 결정합니다. 어셈블리 코드에서 선언한 섹션, 심볼, 정렬이 링커 스크립트와 정확히 맞물려야 부팅·예외 처리·모듈 로딩이 정상 동작합니다.

링커 스크립트 ↔ 어셈블리 섹션 매핑 어셈블리 소스 (.S) .section .head.text,"ax" SYM_CODE_START(startup_64) .section .init.text,"ax" SYM_FUNC_START(start_kernel) .section .text,"ax" SYM_FUNC_START(do_syscall_64) .section .rodata,"a" sys_call_table: .section .data,"aw" init_thread_union: .section .bss,"aw",@nobits empty_zero_page: .section .altinstructions,"a" ALTERNATIVE "nop","lfence" .pushsection __ex_table,"a" vmlinux.lds.S _stext = .; HEAD_TEXT INIT_TEXT _text = .; TEXT_TEXT _etext = .; RO_DATA(PAGE_SIZE) _sdata = .; DATA_DATA _edata = .; BSS_SECTION(0, 0, 0) _end = .; EXCEPTION_TABLE(16) ALTINSTRUCTIONS vmlinux (ELF) .text (RX) — 커널 코드 .rodata (R) — 읽기 전용 .data (RW) — 초기화 데이터 .bss (RW) — 제로 초기화 .init.text — 부팅 후 해제 __ex_table — 예외 테이블 .altinstructions — 패칭 .percpu — Per-CPU 데이터

링커 스크립트 매크로

커널은 include/asm-generic/vmlinux.lds.h에 정의된 매크로를 통해 섹션 배치를 표준화합니다. 어셈블리 코드에서 사용하는 섹션 이름과 이 매크로가 정확히 대응해야 합니다.

링커 매크로수집 섹션용도
HEAD_TEXT.head.text커널 진입점 (startup_64, _start)
INIT_TEXT.init.text초기화 코드 (부팅 후 해제)
TEXT_TEXT.text, .text.*일반 커널 코드
RO_DATA.rodata, .rodata.*읽기 전용 데이터
DATA_DATA.data, .data.*초기화된 쓰기 가능 데이터
BSS_SECTION.bss, .bss.*제로 초기화 데이터
EXCEPTION_TABLE__ex_table예외 복구 테이블
ALTINSTRUCTIONS.altinstructions런타임 명령어 패칭 메타데이터
PERCPU_SECTION.data..percpuPer-CPU 변수 템플릿
NOTES.note.*ELF 노트 (빌드 ID 등)

SYM_* 매크로와 링커 심볼 가시성

커널 5.5+에서 도입된 SYM_* 매크로 계열은 어셈블리 심볼의 타입·바인딩·정렬을 표준화하고, 링커가 올바른 심볼 테이블을 생성하도록 합니다.

/* include/linux/linkage.h — SYM_* 매크로 정의 */

/* 함수 심볼: .type @function, ALIGN, GLOBAL */
SYM_FUNC_START(name)       /* 전역 함수, objtool 검증 대상 */
SYM_FUNC_START_LOCAL(name) /* 지역 함수 (.L 접두어 없이도 LOCAL) */
SYM_FUNC_START_WEAK(name)  /* 약한 바인딩 (아키텍처별 오버라이드) */
SYM_FUNC_END(name)         /* .size 계산, CFI 마무리 */

/* 코드 심볼: 함수가 아닌 코드 (진입점, 인터럽트 핸들러) */
SYM_CODE_START(name)       /* objtool이 스택 검증 건너뜀 */
SYM_CODE_END(name)

/* 데이터 심볼 */
SYM_DATA_START(name)       /* 전역 데이터 */
SYM_DATA(name, data...)    /* 단일 데이터 항목 */
SYM_DATA_END(name)

/* 실제 확장 예시: SYM_FUNC_START(memcpy) */
    .globl memcpy              /* 전역 심볼 */
    .p2align 4                 /* 16바이트 정렬 */
    .type memcpy, @function    /* ELF 함수 타입 */
memcpy:
    .cfi_startproc             /* DWARF 언와인드 시작 */

예외 테이블 섹션 (.pushsection __ex_table)

커널에서 유저 공간 메모리에 접근하는 어셈블리 코드는 page fault 시 복구 경로를 예외 테이블에 등록해야 합니다. .pushsection을 사용해 현재 섹션을 벗어나지 않고 테이블 엔트리를 삽입합니다.

/* arch/x86/lib/copy_user_64.S — 유저 메모리 복사 */
SYM_FUNC_START(copy_user_generic_unrolled)
    ...
1:  movq (%rsi), %r8           /* 유저 메모리 읽기 → page fault 가능 */
2:  movq 8(%rsi), %r9
    ...
3:  movq %r8, (%rdi)           /* 유저 메모리 쓰기 → page fault 가능 */
4:  movq %r9, 8(%rdi)
    ...

/* 예외 발생 시 복구 경로를 __ex_table에 등록 */
    .pushsection __ex_table, "a"
    .balign 4
    .long 1b - .               /* fault 명령어 오프셋 (상대) */
    .long .Lcopy_user_handle_tail - .  /* 복구 핸들러 오프셋 */
    .long 2b - .
    .long .Lcopy_user_handle_tail - .
    .long 3b - .
    .long .Lcopy_user_handle_tail - .
    .long 4b - .
    .long .Lcopy_user_handle_tail - .
    .popsection                /* 원래 섹션(.text)으로 복귀 */

/* 복구 핸들러: 남은 바이트 수를 반환 */
.Lcopy_user_handle_tail:
    movl %ecx, %edx            /* 복사 못한 바이트 수 */
    jmp copy_user_handle_tail
SYM_FUNC_END(copy_user_generic_unrolled)

__init / __initdata 섹션 어셈블리

부팅 시에만 필요한 코드와 데이터는 .init.text/.init.data 섹션에 배치하여 부팅 완료 후 free_initmem()으로 메모리를 해제합니다.

/* 어셈블리에서 __init 섹션 사용 */
    .section .init.text, "ax"
SYM_CODE_START(startup_64)
    /* 부팅 초기 코드: 페이지 테이블 설정, BSP 초기화 */
    leaq _text(%rip), %rdi
    ...
SYM_CODE_END(startup_64)

    .section .init.data, "aw"
    .balign 8
early_gdt:                     /* 부팅 시 임시 GDT — 나중에 해제됨 */
    .quad 0x0000000000000000   /* null descriptor */
    .quad 0x00af9a000000ffff   /* 64-bit code */
    .quad 0x00cf92000000ffff   /* 64-bit data */

/* C 코드에서의 __init 매크로 → 같은 섹션으로 매핑 */
/* #define __init  __section(".init.text") __cold __latent_entropy __noinitretpoline */
/* #define __initdata  __section(".init.data") */
⚠️

.init 섹션 참조 위반: 일반 .text 섹션의 코드가 .init.text의 함수를 호출하면 부팅 후 해제된 메모리를 참조하게 됩니다. 커널 빌드 시 modpost가 이런 교차 참조를 검출하여 WARNING: ... .text refers to .init.text 경고를 출력합니다. 어셈블리 코드에서도 같은 규칙이 적용됩니다.

참고자료

공식 매뉴얼 및 문서

추천 서적

커널 소스 내 어셈블리 참고 위치

경로설명
arch/x86/entry/entry_64.Sx86_64 시스템 콜, 인터럽트 진입점
arch/x86/lib/memcpy, memset 등 최적화된 어셈블리 구현
arch/x86/include/asm/인라인 어셈블리 헤더 (atomic, bitops, barrier)
arch/arm64/kernel/entry.SARM64 예외/인터럽트 진입점
arch/arm64/lib/ARM64 문자열/메모리 함수 어셈블리
include/linux/compiler.hREAD_ONCE, WRITE_ONCE, barrier 매크로
include/asm-generic/barrier.h아키텍처 공통 배리어 정의
ℹ️

커널 어셈블리 코드를 읽을 때는 arch/ 디렉토리 아래의 아키텍처별 코드부터 시작하세요. include/asm/ 헤더에 있는 인라인 어셈블리는 C 코드와 자연스럽게 통합되어 있어 이해하기 더 쉽습니다. Bootlin Elixir 크로스 레퍼런스를 활용하면 심볼 정의를 빠르게 찾을 수 있습니다.

CPU 매뉴얼과 어셈블리 개발 실무

CPU 아키텍처 매뉴얼 구조와 커널 코드 연관 Intel SDM (x86) Vol. 1 기본 아키텍처 레지스터, 메모리 모델, 예외/인터럽트 Vol. 2 명령어 레퍼런스 (A-Z) Opcode, 플래그, LOCK, REX prefix Vol. 3 시스템 프로그래밍 페이징, APIC, VMX, MSR, XSAVE Vol. 4 Model Specific Registers MSR 주소·비트 필드 전체 목록 커널 연관 파일: arch/x86/include/asm/msr-index.h arch/x86/include/asm/cpufeatures.h arch/x86/kernel/cpu/common.c ARM ARM (AArch64) Part A Application Level (A64 ISA) A64 명령어, 레지스터, SIMD/FP Part B System Level Architecture 예외 레벨, MMU, GIC, 메모리 모델 Part C System Registers (A-Z) SCTLR_EL1, TCR_EL1, DAIF 등 Part D Debug Architecture 하드웨어 브레이크포인트, ETM 커널 연관 파일: arch/arm64/include/asm/sysreg.h arch/arm64/include/asm/esr.h arch/arm64/kernel/entry.S RISC-V ISA (리눅스 지원) Unpriv. Unprivileged ISA RV64I, M/A/F/D/C/V 확장, ABI Priv. Privileged ISA CSR, Machine/Supervisor mode, 트랩 주요 CSR Control & Status Registers sstatus, sie, stvec, satp, sepc Debug Debug Specification 하드웨어 트리거, OpenOCD, JTAG 커널 연관 파일: arch/riscv/include/asm/csr.h arch/riscv/kernel/entry.S arch/riscv/include/asm/ptrace.h

Intel SDM 읽는 법 — 명령어 레퍼런스 예시

Intel SDM Vol. 2의 각 명령어 항목은 다음과 같은 구조로 되어 있으며, 인라인 어셈블리 작성 시 반드시 확인해야 할 핵심 정보를 담고 있습니다:

/* Intel SDM Vol.2 명령어 항목 구조 예시: CMPXCHG */

/* Opcode    | Instruction          | Description */
/* 0F B0 /r  | CMPXCHG r/m8, r8     | Compare AL with r/m8 */
/* 0F B1 /r  | CMPXCHG r/m32, r32   | Compare EAX with r/m32 */
/* REX.W + 0F B1 /r | CMPXCHG r/m64, r64 | Compare RAX with r/m64 */

/* 핵심 확인 사항:
 * 1. Flags Affected: ZF (성공 시 set), CF/PF/AF/SF/OF
 * 2. #UD: LOCK prefix 없이 메모리 오퍼랜드에 사용 시 동작 정의
 * 3. LOCK prefix: 메모리 오퍼랜드에 LOCK 사용 가능 (atomic)
 * 4. 64-bit mode: REX.W prefix로 64비트 확장
 */

/* 커널에서의 실제 사용 — arch/x86/include/asm/cmpxchg.h */
static 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)
            : "r"(new), "m"(*(u64 *)ptr), "0"(old)
            : "memory");
        return prev;
    }
}

ARM ARM 읽는 법 — 시스템 레지스터

ARM 커널 코드에서는 시스템 레지스터 접근이 빈번합니다. ARM ARM의 시스템 레지스터 장은 각 레지스터의 비트 필드와 접근 조건을 정의합니다:

/* ARM ARM에서 SCTLR_EL1 레지스터 비트 필드 확인 후 커널 코드 이해 */
/* arch/arm64/include/asm/sysreg.h */
#define SCTLR_EL1_MMU_EN    (1 << 0)   /* M bit: MMU enable */
#define SCTLR_EL1_ALIGN     (1 << 1)   /* A bit: Alignment check */
#define SCTLR_EL1_DCACHE    (1 << 2)   /* C bit: Data cache enable */
#define SCTLR_EL1_WXN       (1 << 19)  /* WXN: Write implies XN */
#define SCTLR_EL1_ICACHE    (1 << 12)  /* I bit: Instruction cache */

/* ARM64 어셈블리에서 시스템 레지스터 접근 */
asm volatile("mrs %0, sctlr_el1" : "=r"(val));   /* 읽기 */
asm volatile("msr sctlr_el1, %0" :: "r"(val));    /* 쓰기 */
asm volatile("isb");  /* Instruction Synchronization Barrier 필수 */

RISC-V Privileged ISA — CSR 접근

/* RISC-V Privileged ISA에서 CSR 정의 확인 후 커널 코드 이해 */
/* arch/riscv/include/asm/csr.h */
#define CSR_SSTATUS     0x100   /* Supervisor Status */
#define CSR_SIE         0x104   /* Supervisor Interrupt Enable */
#define CSR_STVEC       0x105   /* Supervisor Trap Vector */
#define CSR_SATP        0x180   /* Supervisor Address Translation */

/* RISC-V CSR 접근 인라인 어셈블리 */
#define csr_read(csr)  ({                 unsigned long __v;                      asm volatile("csrr %0, " __stringify(csr)         : "=r"(__v) :: "memory");          __v; })

/* satp 레지스터: 페이지 테이블 모드(Sv39/48/57) + ASID + PPN */
/* Privileged ISA Vol.II Section 4.1.11 참조 */
#define SATP_MODE_SV39  (8UL << 60)
#define SATP_MODE_SV48  (9UL << 60)
#define SATP_MODE_SV57  (10UL << 60)
매뉴얼-코드 매핑 요령:
  • 커널 헤더의 #define 상수는 거의 항상 해당 아키텍처 매뉴얼의 비트 필드 정의와 1:1 대응합니다
  • 커널 소스의 주석에 "See Intel SDM Vol. 3, Section X.Y" 같은 참조가 있으면 반드시 확인하세요
  • arch/x86/include/asm/msr-index.h의 MSR 정의는 Intel SDM Vol. 4에서 직접 가져온 것입니다
  • AMD 전용 MSR은 같은 파일에 MSR_AMD64_* 접두어로 정의되어 있으며, AMD APM을 참조합니다

어셈블리와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.