어셈블리 종합 (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까지 다룹니다.
핵심 요약
- 레지스터 규약 — x86_64/ARM64에서 인자, 반환값, 보존 레지스터를 먼저 구분합니다.
- 인라인 asm 제약 — 입력/출력 오퍼랜드와 clobber를 정확히 선언해 오동작을 방지합니다.
- 진입/탈출 경로 — syscall/인터럽트에서 pt_regs가 어떻게 구성되는지 추적합니다.
- 메모리 배리어 — x86 TSO와 ARM64 약한 메모리 모델 차이를 구분해 적용합니다.
- CPUID 기반 분기 — 피처 비트 해석으로 런타임 최적화 경로를 결정합니다.
단계별 이해
- 가설 수립
문제와 개선 목표를 수치로 정의합니다. - 제약 분석
호환성, 안정성, 보안 제약을 먼저 확인합니다. - 실험 적용
최소 변경으로 효과와 부작용을 측정합니다. - 정식 반영
검증된 변경만 문서화해 반영합니다.
인라인 어셈블리 개요
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;
}
오퍼랜드 제약조건
| 제약 | x86 의미 | 설명 |
|---|---|---|
"r" | 범용 레지스터 | 컴파일러가 선택 |
"a" | eax/rax | accumulator |
"b" | ebx/rbx | base register |
"c" | ecx/rcx | counter register |
"d" | edx/rdx | data register |
"m" | 메모리 참조 | 메모리 피연산자 |
"i" | 즉시값 | 상수 |
"=" | 출력 전용 | 쓰기만 |
"+" | 입출력 | 읽기+쓰기 |
"S" | esi/rsi | source index |
"D" | edi/rdi | destination 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/FP | 128비트 벡터/부동소수점 |
"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 문법 | 유효 주소 | 사용 예 |
|---|---|---|---|---|
| 즉시값 | $42 | 42 | (상수 오퍼랜드) | movq $0, %rax |
| 레지스터 | %rax | rax | (레지스터 내용) | addq %rbx, %rax |
| 직접 메모리 | label | [label] | 절대/RIP-상대 주소 | movq jiffies, %rax |
| 간접 메모리 | (%rax) | [rax] | base | movq (%rdi), %rax |
| Base+Disp | 8(%rax) | [rax+8] | base + displacement | movq 16(%rbp), %rsi |
| Base+Index | (%rax,%rcx) | [rax+rcx] | base + index | movq (%rdi,%rsi), %rax |
| SIB 전체 | (%rax,%rcx,8) | [rax+rcx*8] | base + index×scale | movq (%rdi,%rsi,8), %rax |
| SIB+Disp | 0x10(%rax,%rcx,4) | [rax+rcx*4+0x10] | base + index×scale + disp | movl 4(%rbx,%rdx,4), %eax |
| RIP-상대 | label(%rip) | [rip+label] | RIP + disp32 | movq 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가지 조합을 제공합니다.
| mod | r/m=100 | r/m=101 | r/m=기타 |
|---|---|---|---|
00 | SIB (변위 없음) | RIP+disp32 (64비트) | [reg] (변위 없음) |
01 | SIB+disp8 | [rbp]+disp8 | [reg]+disp8 |
10 | SIB+disp32 | [rbp]+disp32 | [reg]+disp32 |
11 | rsp (레지스터) | rbp (레지스터) | 레지스터 직접 |
SIB 바이트 상세
SIB(Scale-Index-Base) 바이트는 ModR/M에서 r/m=100일 때 사용되며, 복잡한 주소 계산을 제공합니다.
변위와 즉시값 인코딩
| 크기 | ModRM mod | 범위 | 바이트 | 규칙 |
|---|---|---|---|---|
| 변위 없음 | mod=00 | — | 0 | 레지스터 간접 기본 |
| disp8 | mod=01 | -128 ~ +127 | 1 | 부호 확장하여 64비트로 |
| disp32 | mod=10 | -2³¹ ~ +2³¹-1 | 4 | 부호 확장하여 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비트 고정 폭입니다. 오퍼랜드 인코딩이 명령어 비트 내에 고정 위치로 배치됩니다.
RISC-V 오퍼랜드 형식
RISC-V는 6가지 기본 명령어 형식을 정의합니다. 모든 형식은 32비트이며, opcode가 하위 7비트에 고정됩니다.
오퍼랜드 형식 실전 함정
| 함정 | 증상 | 원인 | 해결 |
|---|---|---|---|
| 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, 스케줄러 통계, 디버그 훅처럼 "평소에는 꺼져 있고, 켜더라도 오래 유지되는" 기능에 특히 잘 맞습니다.
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에서 데이터 캐시 접근과 조건 분기 일부를 통째로 없앨 수 있습니다.
| 방식 | 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를 참조하는 모든 사이트가 한 번에 패칭됩니다.
__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() 계열 텍스트 패칭 루틴으로 이해하면 됩니다.
따라서 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 volatile | asm goto |
|---|---|---|
| 제어 흐름 | fall-through만 | C 레이블로 분기 가능 |
| 출력 오퍼랜드 | 사용 가능 | GCC 14 미만: 불가 |
| 컴파일러 최적화 | 표준 데이터 흐름 | 제한적 (다중 경로) |
| 사용 사례 | 값 계산, 레지스터 접근 | 조건 분기, Static Key |
| 커널 사용 빈도 | 매우 높음 | Static Key, error 경로 |
레지스터 심화
x86-64, ARM64, RISC-V의 레지스터 구조를 시스템 레지스터까지 포함하여 심층 정리합니다.
x86-64 GPR 완전 맵
시스템 레지스터 CR/DR/MSR
| 레지스터 | 용도 | 핵심 비트 |
|---|---|---|
CR0 | 시스템 제어 | PE(보호모드), PG(페이징), WP(쓰기보호), NE(x87 오류) |
CR2 | Page 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-XMM15 | 128비트 | 16개 | SSE | 비트 1 |
| YMM0-YMM15 (상위) | 256비트 | 16개 | AVX | 비트 2 |
| ZMM0-ZMM31 | 512비트 | 32개 | AVX-512 | 비트 5,6,7 |
| k0-k7 (opmask) | 64비트 | 8개 | AVX-512 | 비트 5 |
| TMM0-TMM7 (타일) | 가변 (~1KB) | 8개 | AMX | 비트 17,18 |
| MXCSR | 32비트 | 1개 | SSE | 비트 1 |
ARM64 시스템 레지스터
| 레지스터 | EL | 용도 | 핵심 비트 |
|---|---|---|---|
SCTLR_EL1 | EL1 | 시스템 제어 | M(MMU), C(캐시), I(명령어캐시), WXN, SA |
TCR_EL1 | EL1 | 변환 제어 | T0SZ/T1SZ(가상주소폭), TG0/TG1(페이지크기), IPS |
TTBR0_EL1 | EL1 | 유저 페이지 테이블 | ASID + 페이지 테이블 물리 주소 |
TTBR1_EL1 | EL1 | 커널 페이지 테이블 | 커널 주소 공간 PGD |
MAIR_EL1 | EL1 | 메모리 속성 | 8개 속성 인덱스 (Normal/Device/Non-cacheable) |
ESR_EL1 | EL1 | 예외 증후군 | EC(예외 클래스), ISS(명령어별 정보) |
FAR_EL1 | EL1 | Fault 주소 | Data/Instruction Abort 주소 |
VBAR_EL1 | EL1 | 벡터 테이블 주소 | 예외 벡터 테이블 베이스 (2KB 정렬) |
TPIDR_EL1 | EL1 | 스레드 포인터 (커널) | current task_struct 포인터 |
CNTVCT_EL0 | EL0 | 가상 카운터 | 시스템 타이머 (RDTSC 등가) |
RISC-V CSR 완전 맵
| 주소 범위 | 권한 | 용도 | 예시 |
|---|---|---|---|
0x000-0x0FF | U-mode (읽기/쓰기) | 사용자 CSR | ustatus, fflags, frm, fcsr |
0xC00-0xC1F | U-mode (읽기 전용) | 사용자 카운터 | cycle, time, instret |
0x100-0x1FF | S-mode (읽기/쓰기) | 감독자 CSR | sstatus, sie, stvec, sscratch, sepc, scause, stval, sip, satp |
0x300-0x3FF | M-mode (읽기/쓰기) | 머신 CSR | mstatus, misa, mie, mtvec, mscratch, mepc, mcause, mip |
0xF11-0xF14 | M-mode (읽기 전용) | 머신 ID | mvendorid, 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 시스템 콜 규약
/* 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: 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 등의 동작을 정확히 이해해야 합니다.
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 연산 인라인 어셈블리
/* 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, %eax | rax, eax |
| 즉시값 접두사 | $42, $0xff | 42, 0ffh |
| 크기 접미사 | movl, movq, movb | mov DWORD, mov QWORD |
| 메모리 참조 | disp(%base,%index,scale) | [base + index*scale + disp] |
| 간접 점프 | *%rax | jmp rax |
GCC에서 Intel 문법을 사용하려면 -masm=intel 옵션을 지정하거나, 인라인 어셈블리 내에서 .intel_syntax noprefix 지시자를 사용합니다. 그러나 커널 코드는 AT&T 문법이 표준이므로 AT&T에 익숙해지는 것이 중요합니다.
x86 명령어 인코딩
커널 어셈블리를 디버깅하거나 바이너리 패칭(ALTERNATIVE, static key)을 이해하려면 x86 명령어의 바이트 수준 인코딩을 알아야 합니다. REX, VEX, EVEX 접두사와 ModRM/SIB 바이트 구조를 이해하면 objdump 출력을 정확히 해석할 수 있습니다.
레거시 접두사
| 그룹 | 접두사 바이트 | 의미 | 커널 사용 예 |
|---|---|---|---|
| 그룹 1 | F0 | LOCK — 원자적 메모리 연산 | lock cmpxchg, lock xadd |
| 그룹 1 | F2 | REPNE/REPNZ | rep ret (AMD 분기 예측 힌트) |
| 그룹 1 | F3 | REP/REPE/REPZ | rep movsb (memcpy ERMS) |
| 그룹 2 | 2E/3E/26/64/65 | 세그먼트 오버라이드 | %gs:0x28 (스택 카나리) |
| 그룹 3 | 66 | 오퍼랜드 크기 전환 | 32→16비트 오퍼랜드 |
| 그룹 4 | 67 | 주소 크기 전환 | 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) | 어셈블리 | 사용처 |
|---|---|---|---|
| 1B | 90 | nop | 일반 NOP |
| 2B | 66 90 | xchg %ax,%ax | 단순 패딩 |
| 3B | 0f 1f 00 | nopl (%rax) | ALTERNATIVE 기본 |
| 4B | 0f 1f 40 00 | nopl 0(%rax) | endbr64 대체 |
| 5B | 0f 1f 44 00 00 | nopl 0(%rax,%rax,1) | static key, ftrace |
| 6B | 66 0f 1f 44 00 00 | nopw 0(%rax,%rax,1) | 정렬 패딩 |
| 7B | 0f 1f 80 00 00 00 00 | nopl 0(%rax) | 긴 패딩 |
| 8B | 0f 1f 84 00 00 00 00 00 | nopl 0(%rax,%rax,1) | 긴 패딩 |
명령어 길이 제한: x86 명령어는 최대 15바이트입니다. 이를 초과하면 #UD(Undefined Opcode) 예외가 발생합니다. 커널의 명령어 디코더(arch/x86/lib/insn.c)는 이 규칙을 따르며, insn_decode()로 임의 위치의 명령어를 파싱할 수 있습니다.
REX 접두사 상세 비트 레이아웃
REX 접두사(0x40-0x4F)는 64비트 모드에서 레지스터 확장과 64비트 오퍼랜드 크기를 위해 사용됩니다.
| REX 바이트 | WRXB | 의미 | 예시 |
|---|---|---|---|
0x40 | 0000 | REX만 (SPL/BPL/SIL/DIL 접근) | mov %spl, %al |
0x41 | 0001 | B=1: r/m 확장 | mov %r8d, %eax |
0x44 | 0100 | R=1: reg 확장 | mov %eax, %r8d |
0x48 | 1000 | W=1: 64비트 | mov %rax, %rbx |
0x49 | 1001 | W+B: 64비트 + rm 확장 | mov %rax, %r8 |
0x4C | 1100 | W+R: 64비트 + reg 확장 | mov %r8, %rax |
0x4D | 1101 | W+R+B: 양쪽 모두 R8+ | mov %r8, %r9 |
VEX 접두사 2/3바이트 상세
VEX(Vector Extensions) 접두사는 AVX 명령어에 사용되며, 레거시 접두사(66/F2/F3)와 REX를 대체합니다.
| 형식 | 바이트 | 필드 | 사용 조건 |
|---|---|---|---|
| 2바이트 VEX | C5 [RvvvvLpp] | R̃, vvvv, L, pp | 0F 맵, REX.X=0, REX.B=0 |
| 3바이트 VEX | C4 [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 다음) | 7 | R̃ | REX.R 반전 |
| 6 | X̃ | REX.X 반전 | |
| 5 | B̃ | REX.B 반전 | |
| 4 | R̃' | REX.R 상위 확장 (ZMM16-31) | |
| P1 | 7 | W | 오퍼랜드 크기 |
| 6:3 | vvvv | 추가 소스 레지스터 | |
| 2:0 | pp | 레거시 접두사 대체 | |
| P2 | 7 | z | 제로 마스킹 (1=zero, 0=merge) |
| 6:5 | L'L | 벡터 길이 (00=128, 01=256, 10=512) | |
| 4 | b | 브로드캐스트 / 라운딩 / SAE | |
| 2:0 | aaa | 마스크 레지스터 (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 /r | MOV r/m, r | 1바이트 | 레지스터 이동 |
8B /r | MOV r, r/m | 1바이트 | 레지스터 이동 |
01 /r | ADD r/m, r | 1바이트 | 산술 연산 |
39 /r | CMP r/m, r | 1바이트 | 비교 |
E8 cd | CALL rel32 | 1바이트 | 함수 호출 |
C3 | RET | 1바이트 | 함수 복귀 |
E9 cd | JMP rel32 | 1바이트 | 무조건 점프 |
FF /2 | CALL r/m64 | 1바이트 | 간접 호출 |
0F 05 | SYSCALL | 2바이트 (0F) | 시스템 콜 진입 |
0F 07 | SYSRET | 2바이트 (0F) | 시스템 콜 복귀 |
0F B0 /r | CMPXCHG r/m8, r8 | 2바이트 (0F) | atomic CAS |
0F B1 /r | CMPXCHG r/m, r | 2바이트 (0F) | atomic CAS |
0F 38 F1 /r | CRC32 | 3바이트 (0F38) | 체크섬 |
0F 3A 0F /r | PALIGNR | 3바이트 (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) 완전 가이드를 참고하세요.
섹션 지시자
.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_startproc | CFI 블록 시작 (FDE 생성) | 함수 시작 |
.cfi_endproc | CFI 블록 종료 | 함수 끝 |
.cfi_def_cfa reg, off | CFA = reg + off | 프레임 포인터 설정 후 |
.cfi_def_cfa_offset off | CFA 오프셋만 변경 | PUSH/SUB RSP 후 |
.cfi_def_cfa_register reg | CFA 레지스터만 변경 | MOV RSP,RBP 후 |
.cfi_offset reg, off | reg이 CFA+off에 저장됨 | PUSH 후 |
.cfi_register old, new | old 값이 new에 있음 | 레지스터 이동 후 |
.cfi_restore reg | reg 복원됨 (이전 규칙) | POP 후 |
.cfi_remember_state | 현재 CFI 상태 스택에 저장 | 분기 전 |
.cfi_restore_state | 저장된 CFI 상태 복원 | 분기 합류점 |
.cfi_adjust_cfa_offset n | CFA 오프셋 += n | 동적 스택 조정 |
.cfi_signal_frame | 시그널 프레임 표시 | 시그널 핸들러 진입 |
.cfi_undefined reg | reg 값 미정의 | 파괴된 레지스터 |
/* 전형적인 함수 프롤로그/에필로그 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 엔트리 구조 — 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 언와인더는 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를 따릅니다. 함수 호출 시 레지스터 사용 규칙을 정확히 알아야 합니다.
레지스터 역할
| 레지스터 | 용도 | 호출 후 보존 |
|---|---|---|
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번째 인자 | rcx | r10 (rcx는 syscall이 덮어씀) |
| 호출 방식 | call 명령어 | syscall 명령어 |
| 리턴 | ret | sysret (또는 iretq) |
| 변경 레지스터 | caller-saved | rcx, r11 추가 변경 |
| 컨텍스트 | 동일 권한 수준 | 유저→커널 권한 전환 |
Windows x64 ABI 비교
Windows x64 호출 규약은 System V ABI와 크게 다릅니다. UEFI 펌웨어나 크로스 플랫폼 코드를 이해할 때 필요합니다.
| 항목 | System V AMD64 (Linux) | Windows x64 |
|---|---|---|
| 정수 인자 1-4 | RDI, RSI, RDX, RCX | RCX, RDX, R8, R9 |
| 정수 인자 5+ | R8, R9, 스택 | 스택 (32B 이후) |
| FP 인자 | XMM0-XMM7 | XMM0-XMM3 |
| 반환값 | RAX, RDX | RAX |
| Caller-saved | RAX,RCX,RDX,RSI,RDI,R8-R11 | RAX,RCX,RDX,R8-R11 |
| Callee-saved | RBX,RBP,R12-R15 | RBX,RBP,RDI,RSI,R12-R15 |
| Shadow Space | 없음 | 32바이트 (항상 예약) |
| Red Zone | 128바이트 | 없음 |
| 스택 정렬 | 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 메커니즘을 이해해야 합니다.
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~x17 | PLT/veneer (IP0, IP1) | 비보존 |
x18 | 플랫폼 예약 레지스터 | 플랫폼 ABI 의존 (일반 용도 사용 금지) |
x19~x28 | callee-saved | 보존 |
x29 (FP) | 프레임 포인터 | 보존 |
x30 (LR) | 링크 레지스터 (리턴 주소) | 비보존 |
SP | 스택 포인터 | 보존 |
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=1 | JE/JZ |
NE | 다름 | Z=0 | JNE/JNZ |
CS/HS | 캐리 세트/부호없는 ≥ | C=1 | JC/JAE |
CC/LO | 캐리 클리어/부호없는 < | C=0 | JNC/JB |
MI | 음수 | N=1 | JS |
PL | 양수 또는 0 | N=0 | JNS |
VS | 오버플로 | V=1 | JO |
VC | 오버플로 없음 | V=0 | JNO |
HI | 부호없는 > | C=1 ∧ Z=0 | JA |
LS | 부호없는 ≤ | C=0 ∨ Z=1 | JBE |
GE | 부호있는 ≥ | N=V | JGE |
LT | 부호있는 < | N≠V | JL |
GT | 부호있는 > | Z=0 ∧ N=V | JG |
LE | 부호있는 ≤ | Z=1 ∨ N≠V | JLE |
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 주요 명령어 패턴
/* 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_64 | ARM64 | RISC-V |
|---|---|---|---|
| 시스템 콜 명령어 | syscall | svc #0 | ecall |
| 콜 번호 레지스터 | rax | x8 | a7 (x17) |
| 인자 레지스터 | rdi,rsi,rdx,r10,r8,r9 | x0-x5 | a0-a5 |
| 반환값 | rax | x0 | a0 |
| 인터럽트 비활성화 | cli | msr daifset,#2 | csrc sstatus,SIE |
| TLB 플러시 | invlpg | tlbi | sfence.vma |
| Atomic 패턴 | LOCK 접두사 | LDXR/STXR | LR/SC 또는 AMO |
| 메모리 모델 | TSO (강한) | 약한 | RVWMO (약한) |
| 배리어 | mfence/lfence | dmb/dsb/isb | fence |
| 리턴 주소 | 스택 push | LR (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, rs2 | add rd, rd, rs2 | x1-x31 | 16 |
C.ADDI rd, imm | addi rd, rd, imm | x1-x31, imm≠0 | 16 |
C.ADDI16SP imm | addi x2, x2, imm | x2(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 offset | jal x0, offset | 무조건 점프 | 16 |
C.BEQZ rs1', off | beq rs1, x0, off | x8-x15만 | 16 |
C.BNEZ rs1', off | bne rs1, x0, off | x8-x15만 | 16 |
C.MV rd, rs2 | add rd, x0, rs2 | x1-x31 | 16 |
C.JALR rs1 | jalr x1, 0(rs1) | x1-x31 | 16 |
C.NOP | addi x0, x0, 0 | — | 16 |
/* 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이면 -march에 c가 포함되어 컴파일러가 자동으로 압축 명령어를 생성합니다.
커널 진입/탈출 코드
커널의 가장 중요한 어셈블리 코드는 유저 공간↔커널 공간 전환을 담당하는 진입/탈출 코드입니다. 시스템 콜, 인터럽트, 예외 처리 시 정확한 컨텍스트 저장/복원이 필수입니다.
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)을 정확히 이해해야 커널 크래시를 디버깅할 수 있습니다.
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는 성능을 위해 메모리 연산 순서를 재배치합니다. 멀티프로세서 환경에서 올바른 동작을 보장하려면 메모리 배리어가 필수입니다.
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 | 바이트 복사 | RSI | RDI | RCX (감소) |
REP MOVSQ | 8바이트 복사 | RSI | RDI | RCX (감소) |
REP STOSB | 바이트 채우기 | AL | RDI | RCX (감소) |
REP STOSQ | 8바이트 채우기 | RAX | RDI | RCX (감소) |
REPE CMPSB | 바이트 비교 (같은 동안) | RSI | RDI | RCX (감소) |
REPNE SCASB | 바이트 검색 (다른 동안) | AL | RDI | RCX (감소) |
/* 기본 메모리 복사: 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 | 크기 | 성능 | 도입 |
|---|---|---|---|---|
| ERMS | CPUID.7.0:EBX[9] | ≥ 256B 최적 | 대량 복사 시 REP MOVSQ보다 빠름 | Ivy Bridge |
| FSRM | CPUID.7.0:EDX[4] | ≤ 128B 최적 | 작은 복사에서도 빠른 REP MOVSB | Ice Lake |
| FZRM | CPUID.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 어셈블리
커널의 memcpy와 memset은 CPU 피처에 따라 런타임에 최적 구현이 선택됩니다.
copy_to_user/copy_from_user
유저↔커널 메모리 복사는 페이지 폴트 발생 가능성과 SMAP 보호를 고려해야 합니다. 예외 테이블과 STAC/CLAC로 안전하게 처리합니다.
/* 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=0 | src=0이면 dst 미정의 |
BSR dst, src | 최상위 1비트 위치 찾기 (MSB→LSB) | ZF=1 if src=0 | src=0이면 dst 미정의 |
BT base, offset | 비트 테스트 (CF = bit[offset]) | CF = tested bit | 메모리 오퍼랜드 시 느림 |
BTS base, offset | 비트 테스트 후 세트 | CF = old bit | LOCK 가능 (atomic) |
BTR base, offset | 비트 테스트 후 리셋 | CF = old bit | LOCK 가능 |
BTC base, offset | 비트 테스트 후 보수 | CF = old bit | LOCK 가능 |
TZCNT dst, src | 후행 0 카운트 (BSF 대체) | CF=1 if src=0, ZF | BMI1 필요, src=0도 정의됨 |
LZCNT dst, src | 선행 0 카운트 (BSR 대체) | CF=1 if src=0, ZF | ABM/LZCNT 필요 |
POPCNT dst, src | 1비트 개수 카운트 | ZF | POPCNT 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 명령어
| 확장 | 명령어 | 동작 | 인코딩 |
|---|---|---|---|
| BMI1 | ANDN r, v, rm | r = ~v & rm | VEX.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 | |
| BMI2 | BZHI r, rm, v | 비트 인덱스부터 상위 0으로 클리어 | VEX.NDS |
PDEP r, v, rm | Parallel Bit Deposit | VEX.NDS | |
PEXT r, v, rm | Parallel Bit Extract | VEX.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에 정의된 커널 비트 연산 함수들의 어셈블리 구현입니다.
/* 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 전용 명령어
| 명령어 | Opcode | 동작 | 커널 사용처 |
|---|---|---|---|
CLI | FA | 인터럽트 비활성화 (IF=0) | local_irq_disable() |
STI | FB | 인터럽트 활성화 (IF=1) | local_irq_enable() |
HLT | F4 | 프로세서 대기 (인터럽트까지) | idle 루프 |
INVLPG m | 0F 01 /7 | TLB 엔트리 무효화 | flush_tlb_one() |
WRMSR | 0F 30 | MSR 쓰기 (ECX=addr, EDX:EAX=val) | wrmsrl() |
RDMSR | 0F 32 | MSR 읽기 (ECX=addr → EDX:EAX) | rdmsrl() |
LGDT m | 0F 01 /2 | GDT 레지스터 로드 | 부팅/CPU 초기화 |
LIDT m | 0F 01 /3 | IDT 레지스터 로드 | 부팅/IDT 설정 |
SWAPGS | 0F 01 F8 | GS.base ↔ IA32_KERNEL_GS_BASE | syscall/인터럽트 진입 |
STAC | 0F 01 CB | AC 플래그 세트 (SMAP 해제) | copy_from_user 등 |
CLAC | 0F 01 CA | AC 플래그 클리어 (SMAP 활성) | 유저 접근 완료 후 |
가상화 명령어
Intel VT-x(VMX)는 하드웨어 가상화를 위한 명령어 세트로, KVM의 핵심 기반입니다.
| 명령어 | 동작 | 모드 |
|---|---|---|
VMXON mem | VMX 활성화 (VMXON 영역 포인터) | Root |
VMXOFF | VMX 비활성화 | Root |
VMLAUNCH | 현재 VMCS로 게스트 최초 진입 | Root→Non-root |
VMRESUME | 현재 VMCS로 게스트 재진입 | Root→Non-root |
VMREAD r, field | VMCS 필드 읽기 | Root |
VMWRITE field, r | VMCS 필드 쓰기 | Root |
VMPTRLD mem | VMCS 포인터 로드 (현재 VMCS 설정) | Root |
VMPTRST mem | 현재 VMCS 포인터 저장 | Root |
VMCALL | VM Exit 트리거 (hypercall) | Non-root |
INVEPT | EPT TLB 무효화 | Root |
INVVPID | VPID 기반 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) | ENCLU | EENTER (EAX=2) | Enclave 진입 |
| EEXIT (EAX=4) | Enclave 탈출 | ||
| ERESUME (EAX=3) | Enclave 재진입 (인터럽트 후) | ||
| EGETKEY (EAX=1) | 봉인 키 획득 | ||
| EREPORT (EAX=0) | 원격 증명 보고서 생성 | ||
| Ring 0 (ENCLS) | ENCLS | ECREATE (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 상세 동작
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 지원 */
| 완화 기법 | 대상 취약점 | 메커니즘 | 성능 비용 | 커널 설정 |
|---|---|---|---|---|
| Retpoline | Spectre v2 | 간접 호출 → call+ret thunk | 중간 (2-8%) | CONFIG_RETPOLINE |
| IBRS/eIBRS | Spectre v2 | MSR로 간접 분기 제한 | 낮음 (eIBRS) | spectre_v2= |
| IBPB | Spectre v2 | 분기 예측 버퍼 플러시 | 낮음 | 컨텍스트 스위치 시 |
| STIBP | Spectre v2 (SMT) | 형제 스레드 간 예측 격리 | 낮음 | SMT 환경 |
| LFENCE/배리어 | Spectre v1 | array_index_nospec() | 매우 낮음 | 자동 적용 |
| KPTI | Meltdown | 유저/커널 페이지 테이블 분리 | 높음 (5-30%) | CONFIG_PAGE_TABLE_ISOLATION |
| SLS | Straight-Line Spec | ret/jmp 뒤 int3 삽입 | 매우 낮음 | CONFIG_SLS |
| CET-IBT | JOP/COP 공격 | endbr64 타겟 검증 | 매우 낮음 | CONFIG_X86_KERNEL_IBT |
| CET-SS | ROP 공격 | 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 | 비트 | 동작 | 사용 시점 |
|---|---|---|---|---|
| IBRS | IA32_SPEC_CTRL (0x48) | 비트 0 | 간접 분기 예측 제한 | 커널 진입 시 설정 |
| STIBP | IA32_SPEC_CTRL (0x48) | 비트 1 | 형제 스레드 간 예측 격리 | SMT 활성 시 설정 |
| SSBD | IA32_SPEC_CTRL (0x48) | 비트 2 | 투기적 저장 바이패스 비활성화 | 프로세스별 선택적 |
| IBPB | IA32_PRED_CMD (0x49) | 비트 0 | 간접 분기 예측기 배리어 | 컨텍스트 전환 시 |
| eIBRS | IA32_SPEC_CTRL (0x48) | 비트 0 | Enhanced 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 취약점에 대한 완화로, 유저 모드와 커널 모드에서 서로 다른 페이지 테이블을 사용합니다.
/* 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-12126 | Store Buffer | VERW |
| MLPDS | CVE-2018-12127 | Load Port | VERW |
| MSBDS (Zombieload) | CVE-2018-12130 | Fill Buffer | VERW |
| MDSUM | CVE-2019-11091 | 복합 | VERW |
| TAA | CVE-2019-11135 | TSX 비동기 중단 | TSX 비활성 또는 VERW |
| MMIO Stale Data | CVE-2022-21123/25/66 | MMIO 레지스터 | 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 | 결과 | 직렬화 | 권한 |
|---|---|---|---|---|
RDTSC | 0F 31 | EDX:EAX = TSC | 비직렬화 | Ring 0/3 (CR4.TSD에 따라) |
RDTSCP | 0F 01 F9 | EDX:EAX = TSC, ECX = IA32_TSC_AUX | 읽기 전 직렬화 | Ring 0/3 |
RDPMC | 0F 33 | EDX: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을 직접 설정하여 하드웨어 이벤트를 카운트합니다.
/* 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_EL0 | MRS/MSR | PMU 제어 (활성화, 리셋, 카운터 수) |
PMCNTENSET_EL0 | MSR | 카운터 활성화 (비트 세트) |
PMCNTENCLR_EL0 | MSR | 카운터 비활성화 (비트 클리어) |
PMEVCNTR<n>_EL0 | MRS/MSR | 이벤트 카운터 n 값 |
PMEVTYPER<n>_EL0 | MSR | 이벤트 카운터 n 이벤트 타입 |
PMCCNTR_EL0 | MRS | 사이클 카운터 (64비트) |
PMOVSCLR_EL0 | MSR | 오버플로 상태 클리어 |
PMINTENSET_EL1 | MSR | 인터럽트 활성화 |
PMUSERENR_EL0 | MSR | 유저 공간 접근 허용 |
/* 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 | 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)가 결정합니다. 어셈블리 코드에서 선언한 섹션, 심볼, 정렬이 링커 스크립트와 정확히 맞물려야 부팅·예외 처리·모듈 로딩이 정상 동작합니다.
링커 스크립트 매크로
커널은 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..percpu | Per-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 경고를 출력합니다. 어셈블리 코드에서도 같은 규칙이 적용됩니다.
참고자료
공식 매뉴얼 및 문서
- GNU Assembler (GAS) 매뉴얼 —
info as또는 GNU Binutils 문서에서 GAS 지시자, 매크로, 아키텍처별 옵션을 확인할 수 있습니다. - GCC 인라인 어셈블리 — GCC 공식 문서의 "Extended Asm" 섹션에서 제약조건, clobber, goto 레이블 등 상세 문법을 다룹니다.
- Intel 64 and IA-32 Architectures Software Developer's Manual — Intel의 공식 x86 ISA 레퍼런스. 전체 명령어 세트, 시스템 프로그래밍 가이드를 포함합니다.
- ARM Architecture Reference Manual (ARMv8-A) — ARM 공식 AArch64 ISA 레퍼런스. A64 명령어 세트, 시스템 레지스터를 다룹니다.
추천 서적
- "Programming from the Ground Up" (Jonathan Bartlett) — x86 어셈블리 입문서. AT&T 문법 기반으로 Linux에서의 시스템 프로그래밍을 다룹니다. 무료 PDF 공개.
- "Professional Assembly Language" (Richard Blum) — GAS(GNU Assembler)를 이용한 x86 어셈블리 프로그래밍 가이드.
- "Linux Kernel Development" (Robert Love) — 커널 내부 구조와 함께 아키텍처 의존 코드를 설명합니다.
커널 소스 내 어셈블리 참고 위치
| 경로 | 설명 |
|---|---|
arch/x86/entry/entry_64.S | x86_64 시스템 콜, 인터럽트 진입점 |
arch/x86/lib/ | memcpy, memset 등 최적화된 어셈블리 구현 |
arch/x86/include/asm/ | 인라인 어셈블리 헤더 (atomic, bitops, barrier) |
arch/arm64/kernel/entry.S | ARM64 예외/인터럽트 진입점 |
arch/arm64/lib/ | ARM64 문자열/메모리 함수 어셈블리 |
include/linux/compiler.h | READ_ONCE, WRITE_ONCE, barrier 매크로 |
include/asm-generic/barrier.h | 아키텍처 공통 배리어 정의 |
커널 어셈블리 코드를 읽을 때는 arch/ 디렉토리 아래의 아키텍처별 코드부터 시작하세요. include/asm/ 헤더에 있는 인라인 어셈블리는 C 코드와 자연스럽게 통합되어 있어 이해하기 더 쉽습니다. Bootlin Elixir 크로스 레퍼런스를 활용하면 심볼 정의를 빠르게 찾을 수 있습니다.
CPU 매뉴얼과 어셈블리 개발 실무
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을 참조합니다
관련 문서
어셈블리와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.