x86_64 명령어셋 레퍼런스
x86_64(AMD64/Intel 64) 아키텍처의 명령어를 AT&T 문법 기준으로 종합 정리합니다. CISC 설계 철학과 가변 길이 인코딩, 16개 범용 레지스터(Register)와 RFLAGS, SSE/AVX/AVX-512 SIMD 레지스터, 주소 지정 모드, 데이터 전송·산술·논리·분기·스택·시스템·원자적(Atomic)·SIMD 명령어 카테고리 표, REX/VEX/EVEX 인코딩 다이어그램, 커널 핵심 명령어(SYSCALL, SWAPGS, LOCK CMPXCHG, INVLPG) 고급까지 Linux 커널 개발에 필요한 x86_64 ISA 전체를 다룹니다.
핵심 요약
- CISC 아키텍처 — 가변 길이 명령어(1~15바이트), 메모리 피연산자 직접 사용, 풍부한 주소 모드.
- AT&T 문법 — 소스→목적지 순서, % 레지스터 접두사, $ 즉시값 접두사, b/w/l/q 크기 접미사.
- 16개 범용 레지스터 — RAX-RDI(기존 8개) + R8-R15(AMD64 확장), 64/32/16/8-bit 접근 가능.
- SIMD 확장 — SSE(XMM, 128-bit) → AVX(YMM, 256-bit) → AVX-512(ZMM, 512-bit + 마스크 레지스터).
- LOCK 접두사 — 원자적 메모리 연산의 핵심. CMPXCHG, XADD 등과 결합.
단계별 이해
- 레지스터 맵 파악
16개 범용 레지스터의 이름 규칙(RAX/EAX/AX/AL)과 System V ABI 용도를 먼저 익힙니다. - AT&T 문법 숙지
접미사(b/w/l/q), 접두사(%/$), 소스→목적지 순서를 반드시 구분합니다. - 주소 모드 이해
displacement(base,index,scale) SIB 형식이 x86_64 주소 지정의 핵심입니다. - 카테고리별 명령어 학습
데이터 전송 → 산술 → 논리 → 분기 → 시스템 순서로 진행하면 체계적입니다.
아키텍처 개요
x86_64(AMD64)는 2003년 AMD가 IA-32(x86)를 64-bit로 확장한 CISC 아키텍처입니다. 8086(1978) → 80386(1985, 32-bit) → AMD64(2003, 64-bit) 순으로 발전했으며, 하위 호환성을 유지하면서 레지스터와 주소 공간(Address Space)을 확장했습니다.
| 특성 | x86_64 |
|---|---|
| 설계 철학 | CISC (Complex Instruction Set Computer) |
| 명령어 길이 | 가변 (1~15바이트) |
| 엔디언 | 리틀 엔디언 (Little-Endian) |
| 범용 레지스터 | 16개 (64-bit) |
| 주소 공간 | 가상 48-bit (256TB), 물리 최대 52-bit |
| 동작 모드 | Long Mode (64-bit), Compatibility Mode (32-bit), Legacy Mode |
| 페이지(Page) 크기 | 4KB, 2MB, 1GB |
| 명령어 접두사 | REX, VEX, EVEX, LOCK, REP, 세그먼트 오버라이드 |
동작 모드 전환 다이어그램
x86_64 프로세서는 전원 인가 후 Real Mode에서 시작하며, 제어 레지스터 비트를 조작해 단계적으로 Long Mode(64-bit)까지 전환합니다. 리눅스 커널 부트 과정에서 이 전환은 arch/x86/boot/compressed/head_64.S에서 수행됩니다.
레지스터 셋
범용 레지스터 (64-bit / 32-bit / 16-bit / 8-bit)
| 64-bit | 32-bit | 16-bit | 8-bit (Low) | 8-bit (High) | System V ABI 용도 |
|---|---|---|---|---|---|
| %rax | %eax | %ax | %al | %ah | 반환값 (1st) |
| %rbx | %ebx | %bx | %bl | %bh | Callee-saved |
| %rcx | %ecx | %cx | %cl | %ch | 4th 인자 |
| %rdx | %edx | %dx | %dl | %dh | 3rd 인자, 반환값 (2nd) |
| %rsi | %esi | %si | %sil | — | 2nd 인자 |
| %rdi | %edi | %di | %dil | — | 1st 인자 |
| %rbp | %ebp | %bp | %bpl | — | 프레임 포인터 (Callee-saved) |
| %rsp | %esp | %sp | %spl | — | 스택 포인터 |
| %r8 | %r8d | %r8w | %r8b | — | 5th 인자 |
| %r9 | %r9d | %r9w | %r9b | — | 6th 인자 |
| %r10 | %r10d | %r10w | %r10b | — | Caller-saved |
| %r11 | %r11d | %r11w | %r11b | — | Caller-saved |
| %r12 | %r12d | %r12w | %r12b | — | Callee-saved |
| %r13 | %r13d | %r13w | %r13b | — | Callee-saved |
| %r14 | %r14d | %r14w | %r14b | — | Callee-saved |
| %r15 | %r15d | %r15w | %r15b | — | Callee-saved |
레지스터 중첩 구조 (RAX 예시)
x86_64 범용 레지스터는 하위 호환성을 위해 중첩 구조를 가집니다. 64-bit RAX 안에 32-bit EAX가, 그 안에 16-bit AX가, AX는 다시 상위 AH와 하위 AL로 나뉩니다.
movl $1, %eax → RAX = 0x00000001. 반면 16-bit/8-bit 쓰기는 상위 비트에 영향을 주지 않습니다.
예: movw $1, %ax → RAX의 상위 48-bit은 그대로 유지됩니다.
R8-R15 레지스터에서는 AH 같은 상위 8-bit 접근이 불가능합니다.
RFLAGS 레지스터 주요 비트
| 비트 | 약어 | 이름 | 설명 |
|---|---|---|---|
| 0 | CF | Carry Flag | 부호 없는 연산 올림/빌림 |
| 2 | PF | Parity Flag | 결과 하위 바이트의 짝수 패리티 |
| 6 | ZF | Zero Flag | 결과가 0이면 세트 |
| 7 | SF | Sign Flag | 결과의 최상위 비트 (부호) |
| 8 | TF | Trap Flag | 단일 스텝 디버깅(Debugging) |
| 9 | IF | Interrupt Flag | 인터럽트(Interrupt) 활성화/비활성화 |
| 10 | DF | Direction Flag | 문자열 명령어 방향 (0=증가, 1=감소) |
| 11 | OF | Overflow Flag | 부호 있는 연산 오버플로 |
특수/시스템 레지스터
| 레지스터 | 설명 |
|---|---|
| %rip | 명령어 포인터 (Instruction Pointer) |
| %cs, %ds, %es, %fs, %gs, %ss | 세그먼트 레지스터 (Long Mode에서 FS/GS만 유효) |
| CR0 | 제어 레지스터: PE(보호모드), PG(페이징), WP(쓰기 보호(Write Protection)) 등 |
| CR2 | Page Fault 선형 주소 |
| CR3 | 페이지 디렉토리 베이스 (PML4 물리 주소(Physical Address)) |
| CR4 | 확장 기능: PAE, PSE, OSXSAVE, PCIDE, SMEP, SMAP 등 |
| MSR EFER | Extended Feature Enable: LME(Long Mode Enable), SCE(SYSCALL Enable), NXE |
| MSR LSTAR | SYSCALL 진입 주소 (64-bit) |
| MSR STAR | SYSCALL/SYSRET CS/SS 세그먼트 |
| MSR FMASK | SYSCALL 시 RFLAGS 마스크 |
SIMD 레지스터
| 확장 | 레지스터 | 크기 | 개수 |
|---|---|---|---|
| SSE | XMM0-XMM15 | 128-bit | 16 |
| AVX | YMM0-YMM15 | 256-bit | 16 |
| AVX-512 | ZMM0-ZMM31 | 512-bit | 32 |
| AVX-512 마스크 | k0-k7 | 64-bit | 8 (k0은 암묵적 all-ones) |
| SSE 제어 | MXCSR | 32-bit | 1 (라운딩 모드, 예외 마스크) |
SIMD 레지스터 중첩 구조
SIMD 레지스터는 SSE → AVX → AVX-512 확장에 따라 중첩 구조를 가집니다. ZMM의 하위 256-bit은 YMM이고, YMM의 하위 128-bit은 XMM입니다.
kernel_fpu_begin() / kernel_fpu_end()로 FPU 상태를 저장/복원해야 합니다.
이는 주로 암호화(aesni-intel), CRC 계산, RAID XOR 등에서 활용됩니다.
주소 지정 모드
x86_64 AT&T 문법에서 메모리 피연산자는 displacement(base, index, scale) 형식을 사용합니다.
| 모드 | AT&T 문법 | 계산 | 예제 |
|---|---|---|---|
| 즉시값 | $imm | 상수 값 | movq $42, %rax |
| 레지스터 | %reg | 레지스터 값 | movq %rbx, %rax |
| 직접 메모리 | addr | [addr] | movq 0x1000, %rax |
| 간접 | (%reg) | [reg] | movq (%rdi), %rax |
| 오프셋(Offset) | disp(%reg) | [reg + disp] | movq 8(%rbp), %rax |
| 인덱스 | (%base,%idx) | [base + idx] | movq (%rax,%rcx), %rdx |
| SIB | disp(%base,%idx,s) | [base + idx*s + disp] | movq 16(%rdi,%rsi,8), %rax |
| RIP 상대 | symbol(%rip) | [RIP + offset] | movq var(%rip), %rax |
b(byte, 8-bit), w(word, 16-bit), l(long, 32-bit), q(quad, 64-bit). 예: movb, movw, movl, movq.
ModR/M 및 SIB 바이트 구조
x86_64 메모리 피연산자의 인코딩에서 ModR/M 바이트는 주소 모드와 레지스터를 지정하고, SIB(Scale-Index-Base) 바이트는 복합 주소 계산(base + index * scale + displacement)을 인코딩합니다. ModR/M의 R/M 필드가 100(RSP 인코딩)이면 SIB 바이트가 뒤따릅니다.
Mod=00, R/M=101 조합이 RIP 상대 주소를 의미합니다 (32-bit에서는 절대 주소).
movq var(%rip), %rax에서 어셈블러는 현재 RIP부터 var까지의 오프셋을 32-bit displacement로 인코딩합니다.
이 방식은 Position-Independent Code(PIC)에 필수적이며, 커널의 KASLR(Kernel Address Space Layout Randomization)에서도 활용됩니다.
데이터 전송 명령어
| 명령어 | AT&T 문법 | 설명 | 동작 |
|---|---|---|---|
| MOV | movq %rax, %rbx | 데이터 이동 | dst ← src |
| MOVSX | movslq %eax, %rbx | 부호 확장 이동 | dst ← sign_extend(src) |
| MOVZX | movzbl %al, %eax | 제로 확장 이동 | dst ← zero_extend(src) |
| LEA | leaq 8(%rdi,%rsi,4), %rax | 유효 주소 계산 | dst ← effective_address (메모리 접근 없음) |
| XCHG | xchgq %rax, %rbx | 값 교환 (메모리 시 암묵적 LOCK) | tmp ← dst; dst ← src; src ← tmp |
| BSWAP | bswap %eax | 바이트 순서(Byte Order) 반전 | 엔디언 변환 |
| CMOVcc | cmovzq %rbx, %rax | 조건부 이동 | 조건 충족 시 dst ← src |
| MOVS | movsq | 문자열 복사 | [RDI] ← [RSI]; RSI/RDI 갱신 |
| LODS | lodsq | 문자열 로드 | RAX ← [RSI]; RSI 갱신 |
| STOS | stosq | 문자열 저장 | [RDI] ← RAX; RDI 갱신 |
| PUSH | pushq %rax | 스택 푸시 | RSP -= 8; [RSP] ← src |
| POP | popq %rax | 스택 팝 | dst ← [RSP]; RSP += 8 |
- MOV — 메모리에서 값을 읽어서 레지스터에 저장합니다.
movq (%rdi), %rax는 RDI가 가리키는 주소의 8바이트 값을 RAX에 로드합니다. - LEA — 메모리를 접근하지 않고 주소 자체를 계산합니다.
leaq 8(%rdi,%rsi,4), %rax는 RDI + RSI*4 + 8 결과를 RAX에 저장하며, 플래그를 변경하지 않습니다. 산술 연산의 대용으로 자주 사용됩니다. - MOVSX (movslq) — 작은 크기의 부호 있는 값을 큰 레지스터로 부호 확장합니다. 커널에서
int(32-bit) →long(64-bit) 변환에 필수적입니다. - MOVZX (movzbl) — 작은 크기의 부호 없는 값을 제로 확장합니다.
u8→u64변환 등에 사용됩니다.
/* 커널에서 자주 보이는 패턴 */
/* 1. LEA로 구조체 멤버 주소 계산 (메모리 접근 없음) */
/* leaq offset(%rdi), %rax — container_of() 매크로 결과 */
/* 2. MOVSX: syscall 번호(int) → 테이블 인덱스(long) */
/* movslq %eax, %rax — 32-bit 부호 있는 값을 64-bit로 확장 */
/* 3. MOVZX: 바이트 읽기 → 레지스터 제로 확장 */
/* movzbl (%rdi), %eax — 1바이트 로드, EAX 제로 확장 (RAX 상위도 0) */
/* 4. CMOVcc: 조건부 이동으로 분기 제거 */
int clamp_min(int val, int min_val) {
return val < min_val ? min_val : val;
/* 컴파일 결과:
cmpl %esi, %edi // val - min_val
cmovll %esi, %edi // val < min_val이면 edi = min_val
movl %edi, %eax // return
*/
}
산술 명령어
| 명령어 | AT&T 문법 | 설명 | 플래그 영향 |
|---|---|---|---|
| ADD | addq %rax, %rbx | 덧셈 | CF, ZF, SF, OF, PF |
| SUB | subq %rax, %rbx | 뺄셈 | CF, ZF, SF, OF, PF |
| ADC | adcq %rax, %rbx | 올림 포함 덧셈 | CF, ZF, SF, OF, PF |
| SBB | sbbq %rax, %rbx | 빌림 포함 뺄셈 | CF, ZF, SF, OF, PF |
| INC | incq %rax | 1 증가 (CF 미변경) | ZF, SF, OF, PF |
| DEC | decq %rax | 1 감소 (CF 미변경) | ZF, SF, OF, PF |
| NEG | negq %rax | 2의 보수 부정 | CF, ZF, SF, OF, PF |
| MUL | mulq %rbx | 부호 없는 곱셈 | CF, OF (RDX:RAX ← RAX * src) |
| IMUL | imulq %rbx, %rax | 부호 있는 곱셈 (2/3 오퍼랜드) | CF, OF |
| DIV | divq %rbx | 부호 없는 나눗셈 | RAX ← 몫, RDX ← 나머지 |
| IDIV | idivq %rbx | 부호 있는 나눗셈 | RAX ← 몫, RDX ← 나머지 |
| CQO | cqo | RAX 부호를 RDX로 확장 | — |
| CDQ | cdq | EAX 부호를 EDX로 확장 | — |
leaq (%rdi,%rdi,1), %rax→rax = rdi * 2(SHL보다 유연)leaq (%rdi,%rdi,2), %rax→rax = rdi * 3leaq (%rdi,%rdi,4), %rax→rax = rdi * 5leaq (%rdi,%rdi,8), %rax→rax = rdi * 9leaq 7(%rdi,%rdi,8), %rax→rax = rdi * 9 + 7leaq (,%rdi,8), %rax→rax = rdi * 8(shift 대체)
/* GCC가 자주 생성하는 LEA 패턴 */
/* x * 3 */
leaq (%rdi,%rdi,2), %rax /* rax = rdi + rdi*2 = rdi*3 */
/* x * 5 + 1 */
leaq 1(%rdi,%rdi,4), %rax /* rax = rdi + rdi*4 + 1 = rdi*5 + 1 */
/* x * 12 (두 단계) */
leaq (%rdi,%rdi,2), %rax /* rax = rdi*3 */
shlq $2, %rax /* rax = rdi*3*4 = rdi*12 */
/* 주의: LEA는 RFLAGS를 변경하지 않으므로 */
/* CMP/TEST 뒤의 조건 분기를 방해하지 않음 */
cmpq $0, %rsi
leaq 1(%rdi), %rdi /* rdi++, 플래그 보존 (INC는 ZF 변경) */
je .Lzero /* 위의 CMP 결과로 분기 */
논리/시프트/비트 조작 명령어
| 명령어 | AT&T 문법 | 설명 | 플래그 영향 |
|---|---|---|---|
| AND | andq %rax, %rbx | 비트 AND | ZF, SF, PF (CF=OF=0) |
| OR | orq %rax, %rbx | 비트 OR | ZF, SF, PF (CF=OF=0) |
| XOR | xorq %rax, %rax | 비트 XOR (셀프 XOR = 0) | ZF, SF, PF (CF=OF=0) |
| NOT | notq %rax | 비트 반전 | — |
| TEST | testq %rax, %rbx | 비트 AND (결과 저장 안 함) | ZF, SF, PF (CF=OF=0) |
| SHL/SAL | shlq $3, %rax | 좌측 시프트 | CF(마지막 밀려난 비트), ZF, SF |
| SHR | shrq $1, %rax | 논리 우측 시프트 | CF, ZF, SF |
| SAR | sarq $1, %rax | 산술 우측 시프트 (부호 보존) | CF, ZF, SF |
| ROL/ROR | rolq $4, %rax | 좌측/우측 순환 시프트 | CF, OF |
| BT | btq $5, %rax | 비트 테스트 | CF ← 지정 비트 |
| BTS/BTR/BTC | btsq $5, %rax | 비트 테스트 후 세트/리셋/토글 | CF ← 이전 비트 |
| BSF | bsfq %rbx, %rax | 최하위 세트 비트 검색 | ZF (src=0이면 세트) |
| BSR | bsrq %rbx, %rax | 최상위 세트 비트 검색 | ZF |
| POPCNT | popcntq %rbx, %rax | 세트 비트 수 카운트 | ZF (CF=SF=OF=PF=0) |
| LZCNT | lzcntq %rbx, %rax | 선행 제로 비트 수 | CF, ZF |
| TZCNT | tzcntq %rbx, %rax | 후행 제로 비트 수 | CF, ZF |
| PEXT | pextq %rcx, %rbx, %rax | 병렬 비트 추출 (BMI2) | — |
| PDEP | pdepq %rcx, %rbx, %rax | 병렬 비트 배치 (BMI2) | — |
비교/분기 명령어
| 명령어 | AT&T 문법 | 설명 | 조건 |
|---|---|---|---|
| CMP | cmpq %rax, %rbx | rbx - rax (결과 저장 안 함) | CF, ZF, SF, OF 세트 |
| TEST | testq %rax, %rax | rax & rax (zero 검사) | ZF, SF, PF 세트 |
| JE/JZ | je label | 같으면 분기 | ZF=1 |
| JNE/JNZ | jne label | 다르면 분기 | ZF=0 |
| JG/JNLE | jg label | 부호 있는 > | ZF=0 && SF=OF |
| JGE/JNL | jge label | 부호 있는 >= | SF=OF |
| JL/JNGE | jl label | 부호 있는 < | SF≠OF |
| JLE/JNG | jle label | 부호 있는 <= | ZF=1 || SF≠OF |
| JA/JNBE | ja label | 부호 없는 > | CF=0 && ZF=0 |
| JAE/JNB | jae label | 부호 없는 >= | CF=0 |
| JB/JNAE | jb label | 부호 없는 < | CF=1 |
| JBE/JNA | jbe label | 부호 없는 <= | CF=1 || ZF=1 |
| JS/JNS | js label | 부호 비트 | SF=1 / SF=0 |
| JO/JNO | jo label | 오버플로 | OF=1 / OF=0 |
| JMP | jmp label | 무조건 분기 | — |
| LOOP | loop label | RCX-- 후 0이 아니면 분기 | — |
분기 예측(Branch Prediction)과 커널 최적화
현대 x86_64 프로세서는 분기 예측 유닛(BPU)을 사용해 조건 분기의 결과를 투기적으로 예측합니다. 예측이 맞으면 파이프라인(Pipeline)이 지연(Latency) 없이 계속 진행되지만, 예측 실패(misprediction) 시 10~20 사이클의 페널티가 발생합니다.
__builtin_expect()를 활용해 분기 예측 힌트를 제공합니다.
이 매크로는 코드 레이아웃을 변경하여 예상되는 경로(hot path)를 직선으로 배치하고, 예외 경로(cold path)를 별도 위치로 분리합니다.
likely(cond)→__builtin_expect(!!(cond), 1)— 조건이 참일 가능성이 높음unlikely(cond)→__builtin_expect(!!(cond), 0)— 조건이 거짓일 가능성이 높음 (에러 경로 등)
/* include/linux/compiler.h */
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
/* 사용 예시 — 페이지 폴트 핸들러 */
if (unlikely(fault_signal_pending(fault, regs))) {
/* 시그널 처리 (cold path — 거의 실행되지 않음) */
return;
}
/* 정상 처리 (hot path — 대부분 여기로 진행) */
/* unlikely() 적용 시 GCC가 생성하는 코드 레이아웃 */
/* hot path가 직선(fall-through)이 되도록 배치 */
.Lhot_path:
testq %rax, %rax
jne .Lcold_path /* unlikely: 분기 안 하는 것이 일반적 */
/* ... 정상 처리 (fall-through) ... */
ret
/* cold path는 함수 끝으로 분리 (I-cache 효율) */
.section .text.unlikely
.Lcold_path:
/* ... 에러 처리 ... */
jmp .Lerror_handler
LFENCE 삽입, retpoline(간접 분기 보호), IBRS/IBPB(Indirect Branch Prediction Barrier) 등의 완화 기법을 사용합니다.
array_index_nospec() 매크로는 LFENCE를 사용해 투기적 배열 접근을 방지합니다.
스택/함수 호출 명령어
| 명령어 | AT&T 문법 | 설명 | 동작 |
|---|---|---|---|
| PUSH | pushq %rax | 스택 푸시 | RSP -= 8; [RSP] ← src |
| POP | popq %rax | 스택 팝 | dst ← [RSP]; RSP += 8 |
| CALL | call func | 함수 호출 | PUSH RIP; RIP ← target |
| RET | ret | 함수 복귀 | POP RIP |
| ENTER | enter $N, $0 | 스택 프레임(Stack Frame) 설정 | PUSH RBP; RBP ← RSP; RSP -= N |
| LEAVE | leave | 스택 프레임 해제 | RSP ← RBP; POP RBP |
- 정수 인자: RDI, RSI, RDX, RCX, R8, R9 (순서대로 6개)
- 반환값: RAX (+ RDX 128-bit)
- Callee-saved: RBX, RBP, R12-R15
- Caller-saved: RAX, RCX, RDX, RSI, RDI, R8-R11
- 스택 16-byte 정렬 필수 (CALL 직전)
- Red Zone: RSP 아래 128바이트 (리프 함수용, 커널에서는 비활성화)
시스템/특권 명령어
| 명령어 | 설명 | 특권 수준 |
|---|---|---|
| SYSCALL | 시스템 콜(System Call) 진입 (MSR LSTAR → RIP) | Ring 3 → Ring 0 |
| SYSRET | 시스템 콜 복귀 (RCX → RIP) | Ring 0 → Ring 3 |
| INT n | 소프트웨어 인터럽트 (IDT[n]) | Ring 3 |
| IRET/IRETQ | 인터럽트/예외 복귀 | Ring 0 |
| CLI | 인터럽트 비활성화 (IF=0) | Ring 0 |
| STI | 인터럽트 활성화 (IF=1) | Ring 0 |
| HLT | 프로세서 정지 (인터럽트 대기) | Ring 0 |
| LGDT/SGDT | GDT 로드/저장 | Ring 0 / Ring 3 |
| LIDT/SIDT | IDT 로드/저장 | Ring 0 / Ring 3 |
| LLDT/LTR | LDT/TSS 로드 | Ring 0 |
| SWAPGS | GS 베이스와 MSR_KERNEL_GS_BASE 교환 | Ring 0 |
| WRMSR/RDMSR | MSR 쓰기/읽기 (ECX=번호) | Ring 0 |
| RDTSC | 타임스탬프 카운터 읽기 | Ring 3 (CR4.TSD로 제한 가능) |
| RDTSCP | RDTSC + 프로세서 ID (IA32_TSC_AUX) | Ring 3 |
| CPUID | CPU 정보 쿼리 | Ring 3 |
| INVLPG | TLB 엔트리 무효화(Invalidation) | Ring 0 |
| MOV CRn | 제어 레지스터 접근 | Ring 0 |
| MOV DRn | 디버그 레지스터 접근 | Ring 0 |
| IN/OUT | I/O 포트 입출력(I/O) | Ring 0 (또는 IOPL) |
| WBINVD | 캐시(Cache) 라이트백 + 무효화 | Ring 0 |
| CLFLUSH | 캐시 라인(Cache Line) 플러시(Flush) | Ring 3 |
| CLFLUSHOPT | 최적화된 캐시 라인 플러시 | Ring 3 |
| CLWB | 캐시 라인 라이트백 (무효화 없음) | Ring 3 |
| MFENCE | 메모리 펜스 (전체 직렬화(Serialization)) | Ring 3 |
| LFENCE | 로드 펜스 + 명령어 직렬화 | Ring 3 |
| SFENCE | 스토어 펜스 | Ring 3 |
| PAUSE | 스핀-웨이트 루프 힌트 | Ring 3 |
SYSCALL/SYSRET 메커니즘 상세
SYSCALL은 인터럽트(INT 0x80)보다 훨씬 빠른 시스템 콜 진입 방법입니다. 스택 전환이나 IDT 조회 없이 MSR에 미리 설정된 값으로 즉시 커널 코드로 점프합니다. 이 과정에서 하드웨어가 자동으로 수행하는 레지스터 조작을 이해하는 것이 중요합니다.
원자적/동기화 명령어
LOCK 접두사는 다음 명령어의 메모리 연산을 원자적으로 만듭니다. 캐시 라인 락 또는 버스(Bus) 락을 통해 멀티코어 환경에서 원자성을 보장합니다.
| 명령어 | AT&T 문법 | 설명 |
|---|---|---|
| LOCK ADD | lock addq $1, (%rdi) | 원자적 덧셈 |
| LOCK SUB | lock subq $1, (%rdi) | 원자적 뺄셈 |
| LOCK INC/DEC | lock incq (%rdi) | 원자적 증가/감소 |
| LOCK AND/OR/XOR | lock andq %rax, (%rdi) | 원자적 비트 연산 |
| LOCK BTS/BTR/BTC | lock btsq $5, (%rdi) | 원자적 비트 테스트-세트/리셋/토글 |
| LOCK XADD | lock xaddq %rax, (%rdi) | 원자적 교환-덧셈 (old → rax) |
| LOCK CMPXCHG | lock cmpxchgq %rcx, (%rdi) | 원자적 비교-교환 (CAS) |
| LOCK CMPXCHG16B | lock cmpxchg16b (%rdi) | 128-bit CAS (RDX:RAX vs [mem]) |
| XCHG | xchgq %rax, (%rdi) | 교환 (메모리 시 암묵적 LOCK) |
LOCK CMPXCHG 패턴 (Compare-And-Swap)
/* atomic_cmpxchg: [rdi]가 rsi이면 rdx로 교체 */
atomic_cmpxchg:
movq %rsi, %rax /* RAX ← expected (old) */
lock cmpxchgq %rdx, (%rdi) /* if [RDI]==RAX then [RDI]←RDX */
/* RAX ← [RDI] (이전 값) */
ret /* RAX에 이전 값 반환 */
- 캐시 라인 락 (Cache Line Lock) — 대상 메모리가 단일 캐시 라인 내에 정렬되어 있으면, MESI 프로토콜의 Exclusive/Modified 상태를 이용해 해당 캐시 라인만 잠급니다. 다른 코어는 이 캐시 라인에 대한 Invalidate 요청이 완료될 때까지 대기합니다. 이 방식이 대부분의 경우에 사용되며 성능이 좋습니다.
- 버스 락 (Bus Lock) — 대상이 캐시 라인 경계를 걸쳐 있거나(split lock), 캐시 불가능 메모리인 경우 #LOCK 시그널(Signal)로 전체 메모리 버스를 잠급니다. 이는 시스템 전체 성능에 심각한 영향을 줍니다.
CONFIG_SPLIT_LOCK_DETECT 옵션으로 split lock을 감지하고 경고합니다.
XCHG 기반 스핀락(Spinlock) 구현
XCHG는 메모리 피연산자와 사용될 때 암묵적으로 LOCK이 적용됩니다. 이 특성을 이용한 스핀락은 가장 기본적인 커널 동기화 방식입니다.
/* 단순 스핀락 구현 (Test-and-Set) */
/* RDI = lock 주소, lock=0 (해제), lock=1 (획득) */
spin_lock:
movl $1, %eax
.Lretry:
xchgl %eax, (%rdi) /* 원자적 교환 (암묵적 LOCK) */
testl %eax, %eax /* 이전 값이 0이었으면 → 락 획득 성공 */
jnz .Lspin /* 아니면 스핀 */
ret
.Lspin:
pause /* 스핀 루프 힌트 (전력 절감 + 파이프라인) */
cmpl $0, (%rdi) /* Test: 락이 해제되었는지 읽기만 (LOCK 없이) */
jne .Lspin /* 아직 잠겨있으면 계속 스핀 */
jmp .Lretry /* 해제된 것 같으면 Set 재시도 */
spin_unlock:
movl $0, (%rdi) /* 단순 쓰기 (x86 스토어 순서 보장) */
ret
.Lspin 루프가 일반 cmpl(LOCK 없음)로 먼저 확인하는 이유는
LOCK 접두사가 캐시 라인을 Exclusive로 가져오기 때문입니다. 여러 코어가 동시에 XCHG를 시도하면 캐시 라인이 계속 바운싱(bouncing)됩니다.
먼저 Shared 상태에서 읽기만 수행하면 캐시 효율이 크게 개선됩니다.
XADD 기반 참조 카운팅
LOCK XADD는 원자적으로 값을 더하면서 이전 값을 반환합니다. 이는 참조 카운팅에서 이전 카운트 확인이 필요할 때 핵심적으로 사용됩니다.
/* 커널 refcount 감소 (arch/x86/include/asm/refcount.h 기반) */
static inline bool refcount_dec_and_test(refcount_t *r)
{
int val = -1; /* 감소할 값 */
asm volatile(LOCK_PREFIX "xaddl %0, %1"
: "+r"(val), "+m"(r->refs.counter)
:: "memory");
/* val = 이전 값, counter는 이미 감소됨 */
/* 이전 값이 1이었으면 → 감소 후 0 → 마지막 참조 해제 */
return val == 1;
}
/* 사용 패턴 */
if (refcount_dec_and_test(&obj->refcnt)) {
/* 참조 카운트 0 → 객체 해제 */
kfree(obj);
}
SIMD/벡터 명령어
SSE 명령어 (128-bit XMM)
| 명령어 | 설명 | 동작 |
|---|---|---|
| MOVAPS/MOVUPS | 정렬/비정렬 팩 단정도 이동 | XMM ← 128-bit 메모리/XMM |
| MOVAPD/MOVUPD | 정렬/비정렬 팩 배정도 이동 | XMM ← 128-bit 메모리/XMM |
| ADDPS/ADDPD | 팩 단정도/배정도 덧셈 | 각 요소 병렬 덧셈 |
| SUBPS/SUBPD | 팩 단정도/배정도 뺄셈 | 각 요소 병렬 뺄셈 |
| MULPS/MULPD | 팩 단정도/배정도 곱셈 | 각 요소 병렬 곱셈 |
| DIVPS/DIVPD | 팩 단정도/배정도 나눗셈 | 각 요소 병렬 나눗셈 |
| MINPS/MAXPS | 팩 최솟값/최댓값 | 각 요소 min/max |
| SQRTPS | 팩 제곱근 | 각 요소 sqrt |
| CMPPS | 팩 비교 | 조건 충족 시 0xFFFFFFFF, 아니면 0 |
| SHUFPS | 팩 셔플 | 즉시값으로 요소 재배치(Relocation) |
| UNPCKLPS/UNPCKHPS | 하위/상위 인터리브 | 두 벡터의 요소 교차 배치 |
| PAND/POR/PXOR | 팩 정수 비트 논리 | 128-bit 논리 연산 |
| PADDB/W/D/Q | 팩 정수 덧셈 | 바이트/워드/더블워드/쿼드워드 요소별 |
| PMULLW/PMULLD | 팩 정수 곱셈 (하위) | 워드/더블워드 요소별 곱셈 |
| PCMPEQB/PCMPGTB | 팩 바이트 비교 (같음/큼) | 요소별 비교 → 마스크 |
| PSHUFD | 더블워드 셔플 | 즉시값으로 4개 요소 재배치 |
| PSHUFB | 바이트 셔플 (SSSE3) | 마스크 벡터로 바이트 재배치 |
AVX 명령어 (256-bit YMM)
| 명령어 | 설명 |
|---|---|
| VMOVAPS/VMOVUPS | 256-bit 정렬/비정렬 이동 |
| VADDPS/VADDPD | 3-오퍼랜드 팩 덧셈: dst ← src1 + src2 |
| VFMADD132PS/213/231 | FMA (Fused Multiply-Add): a*b+c 단일 명령 |
| VBROADCASTSS | 스칼라를 모든 요소에 복제 |
| VPERM2F128 | 128-bit 레인 순열 |
| VEXTRACTF128 | YMM에서 128-bit 레인 추출 |
| VINSERTF128 | YMM에 128-bit 레인 삽입 |
| VZEROALL/VZEROUPPER | YMM 상위 제로화 (SSE↔AVX 전환 시) |
AVX-512 명령어 (512-bit ZMM)
| 명령어 | 설명 |
|---|---|
| VMOVAPS zmm{k1}{z} | 마스크 적용 512-bit 이동 ({z}=제로 마스킹) |
| VADDPS zmm, zmm, zmm{k1} | 마스크 적용 512-bit 덧셈 |
| VADDPS zmm, zmm, m512/m32bcst | 브로드캐스트 로드 후 덧셈 |
| VPCMPD k1{k2}, zmm, zmm, imm | 팩 정수 비교 → 마스크 레지스터 |
| VPCOMPRESSD | 마스크 기반 압축 저장 |
| VPEXPANDD | 마스크 기반 확장 로드 |
| VPERMD/VPERMQ | 크로스-레인 순열 |
| VCONFLICTD | 충돌 감지 (히스토그램/scatter용) |
커널 핵심 명령어
SYSCALL/SYSRET 진입 경로
x86_64 Linux에서 시스템 콜은 SYSCALL 명령어로 진입합니다. MSR_LSTAR에 저장된 entry_SYSCALL_64 주소로 점프합니다.
/* arch/x86/entry/entry_64.S — entry_SYSCALL_64 핵심 경로 */
entry_SYSCALL_64:
swapgs /* GS를 커널 per-cpu로 교환 */
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
/* pt_regs 구조체 저장 */
pushq $__USER_DS /* SS */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* RSP */
pushq %r11 /* RFLAGS (SYSCALL이 R11에 저장) */
pushq $__USER_CS /* CS */
pushq %rcx /* RIP (SYSCALL이 RCX에 저장) */
pushq %rax /* syscall 번호 */
/* ... 나머지 레지스터 저장, 시스콜 디스패치 ... */
컨텍스트 전환 시 레지스터 저장/복원
시스템 콜이나 인터럽트로 커널에 진입할 때, 하드웨어와 소프트웨어가 역할을 분담하여 사용자 레지스터를 저장합니다. SYSCALL은 최소한의 레지스터만 하드웨어가 저장하고, 나머지는 커널 진입 코드(entry_SYSCALL_64)가 수동으로 저장합니다.
SWAPGS와 per-cpu 데이터 접근 패턴
SWAPGS는 GS 베이스 레지스터와 MSR_KERNEL_GS_BASE를 교환합니다. 커널 진입 시 per-cpu 데이터 접근을 위해 사용되며, 복귀 시 다시 교환합니다. GS 베이스를 통해 현재 CPU의 per-cpu 영역에 빠르게 접근할 수 있습니다.
/* SWAPGS + per-cpu 접근 패턴 */
/* 1. 커널 진입 시: GS를 커널 per-cpu 영역으로 교환 */
/* SWAPGS → GS.base = 현재 CPU의 per-cpu 오프셋 */
/* 2. per-cpu 변수 접근 (GS 세그먼트 오버라이드) */
/* movq %gs:offset, %rax — per-cpu 변수 읽기 */
/* arch/x86/include/asm/percpu.h — 간략화 */
#define this_cpu_read_8(pcp) ({ \
u64 val; \
asm volatile("movq %%gs:%1, %0" \
: "=r"(val) \
: "m"(pcp)); \
val; \
})
/* 사용 예: 현재 CPU의 current task 가져오기 */
static inline struct task_struct *get_current(void)
{
return this_cpu_read_8(current_task);
/* 컴파일 결과: movq %gs:current_task, %rax */
}
/* 3. 커널 복귀 시: SWAPGS로 GS를 사용자 공간 값으로 복원 */
/* SWAPGS → GS.base = 사용자 프로세스의 GS (TLS 등) */
testb $3, CS(%rsp) — CS의 RPL(Ring Privilege Level) 비트가 3(유저)이면 SWAPGS 필요.
LOCK CMPXCHG — 커널 atomic_cmpxchg
/* arch/x86/include/asm/cmpxchg.h — 간략화 */
static inline u64 __cmpxchg(volatile void *ptr, u64 old, u64 new)
{
u64 prev;
asm volatile(LOCK_PREFIX "cmpxchgq %2, %1"
: "=a"(prev), "+m"(*(volatile u64 *)ptr)
: "r"(new), "0"(old)
: "memory");
return prev;
}
INVLPG — TLB 무효화
/* 단일 페이지 TLB 엔트리 무효화 */
static inline void __invlpg(unsigned long addr)
{
asm volatile("invlpg (%0)" :: "r"(addr) : "memory");
}
WRMSR/RDMSR — MSR 접근
static inline void wrmsr(u32 msr, u32 low, u32 high)
{
asm volatile("wrmsr"
:: "c"(msr), "a"(low), "d"(high)
: "memory");
}
명령어 인코딩
x86_64 명령어는 가변 길이로, 최대 15바이트까지 가능합니다. 레거시 접두사, REX, VEX, EVEX 네 가지 인코딩 체계가 있습니다.
인코딩 실전 예제
실제 명령어가 어떻게 바이트로 인코딩되는지 두 가지 예제를 통해 살펴봅니다.
movq $42, %rax는 실제로 movl $42, %eax(5바이트)로 대체 가능합니다 — 32-bit 쓰기가 자동으로 상위 32-bit를 제로화하므로 결과는 동일하지만 5바이트 절약됩니다.
xorl %eax, %eax(2바이트)는 movq $0, %rax(10바이트)보다 효율적인 레지스터 제로화 방법입니다.
또한 xorl은 현대 프로세서에서 제로화 관용어(zeroing idiom)로 인식되어 실행 유닛을 사용하지 않고 레지스터 파일에서 직접 처리됩니다.
최근 아키텍처 확장 (v6.14+)
Intel APX — Advanced Performance Extensions (v6.16+)
커널 6.16에서 Intel APX(Advanced Performance Extensions) 지원이 추가되었습니다. APX는 x86-64의 주요 한계를 해소하는 ISA 확장으로, 다음과 같은 핵심 기능을 제공합니다:
| 기능 | 설명 | 효과 |
|---|---|---|
| 32개 GPR | 기존 16개 → 32개 범용 레지스터 (R16~R31) | 레지스터 스필/리로드 감소, 함수 호출 오버헤드(Overhead) 감소 |
| REX2 접두사 | 새로운 2바이트 REX 접두사 (확장 레지스터 인코딩) | R16~R31 접근을 위한 인코딩 지원 |
| NDD (New Data Destination) | 3-오퍼랜드 형식 (ADD r1, r2, r3) | 소스 보존 연산 — MOV+연산 패턴 제거 |
| NF (No Flags) | 플래그 레지스터 미수정 옵션 | 플래그 의존성 제거, 비순차 실행 개선 |
4096 CPU 코어 지원 (v6.14+)
커널 6.14에서 x86_64의 최대 CPU 코어 수 제한이 2048에서 4096으로 확장되었습니다 (CONFIG_NR_CPUS 최댓값 증가). 이는 AMD EPYC/Intel Xeon의 고코어 서버와 멀티소켓 시스템의 확장성을 지원합니다.
시스템 레지스터
x86-64의 시스템 레지스터는 CPU 모드 전환, 페이징, 디버깅, 성능 모니터링 등 커널 동작의 핵심을 제어합니다. Control Register(CR0-CR4), Debug Register(DR0-DR7), MSR(Model-Specific Register)의 비트 필드를 정확히 이해해야 커널 초기화 코드와 예외 처리를 분석할 수 있습니다.
디버그 레지스터 (DR0-DR7)
| 레지스터 | 용도 | 커널 사용 |
|---|---|---|
DR0-DR3 | 하드웨어 브레이크포인트 주소 (4개) | ptrace(PTRACE_POKEUSER) — perf hw_breakpoint |
DR6 | 디버그 상태 (어떤 BP가 트리거됐는지) | #DB 예외 핸들러에서 읽기 |
DR7 | 디버그 제어 (활성화, 조건, 길이) | bit 0-7: BP 활성화, bit 16-31: 조건/길이 |
/* DR7 비트 필드 */
/* bit 0,2,4,6 (L0-L3): 로컬 BP 활성화 */
/* bit 1,3,5,7 (G0-G3): 글로벌 BP 활성화 */
/* bit 16-17 (R/W0): DR0 조건 (00=실행, 01=쓰기, 10=I/O, 11=읽기/쓰기) */
/* bit 18-19 (LEN0): DR0 길이 (00=1B, 01=2B, 10=8B, 11=4B) */
/* bit 20-23: DR1 조건/길이, bit 24-27: DR2, bit 28-31: DR3 */
/* 커널 hw_breakpoint 사용 예 */
/* perf_event_create_kernel_counter() → arch_install_hw_breakpoint() */
/* → set_debugreg(addr, 0) // DR0에 주소 설정 */
/* → set_debugreg(ctrl, 7) // DR7에 조건 설정 */
성능 모니터링 카운터 (PMC)
| MSR | 이름 | 용도 |
|---|---|---|
0x186-0x189 | IA32_PERFEVTSELx | 이벤트 선택 (EventSelect, UMask, USR, OS, EN) |
0xC1-0xC4 | IA32_PMCx | 카운터 값 (48비트) |
0x38D | IA32_FIXED_CTR_CTRL | 고정 카운터 제어 |
0x309-0x30B | IA32_FIXED_CTRx | 고정 카운터 (inst_retired, cpu_clk, ref_clk) |
0x38F | IA32_PERF_GLOBAL_CTRL | 전체 카운터 활성화 |
0x390 | IA32_PERF_GLOBAL_STATUS | 오버플로 상태 (NMI 트리거) |
/* PERFEVTSELx 비트 필드 */
/* bit 0-7 : EventSelect (이벤트 코드) */
/* bit 8-15 : UMask (이벤트 세분류) */
/* bit 16 : USR (유저 모드 카운트) */
/* bit 17 : OS (커널 모드 카운트) */
/* bit 20 : INT (오버플로 시 인터럽트/NMI) */
/* bit 22 : EN (카운터 활성화) */
/* bit 23 : INV (조건 반전) */
/* bit 24-31 : CMASK (카운트 마스크) */
/* 커널 perf 서브시스템 사용 흐름 */
/* perf_event_open() → x86_pmu_hw_config() */
/* → wrmsrl(MSR_P6_EVNTSEL0, config) → wrmsrl(MSR_P6_PERFCTR0, 0) */
/* 카운터 오버플로 → NMI → intel_pmu_handle_irq() → 샘플 저장 */
가상화(Virtualization) 확장 (VMX) 레지스터
| 구분 | 설명 | 커널 사용 |
|---|---|---|
| VMCS | Virtual Machine Control Structure (4KB) | KVM: vmcs_write*() |
| VMXON/VMXOFF | VMX 모드 진입/탈출 | kvm_cpu_vmxon() |
| VMLAUNCH/VMRESUME | 게스트 실행 시작/재개 | vmx_vcpu_run() |
| VMPTRLD/VMPTRST | 현재 VMCS 로드/저장 | vCPU 스위칭 시 |
| VMREAD/VMWRITE | VMCS 필드 읽기/쓰기 | 게스트 레지스터, 제어 필드 |
| EPT | Extended Page Table (2단계 주소 변환(Address Translation)) | 게스트 물리→호스트 물리 |
| VPID | Virtual Processor ID (TLB 태깅) | VM 전환 시 TLB 보존 |
MTRR과 PAT: IA32_MTRR* MSR은 물리 메모리(Physical Memory) 범위별 캐시 정책(UC/WC/WT/WB/WP)을 설정합니다. IA32_PAT는 페이지 테이블 엔트리의 PWT/PCD/PAT 비트 조합으로 8가지 메모리 타입 중 하나를 선택합니다. 커널은 memtype_reserve()로 디바이스 메모리의 MTRR/PAT 정합성을 관리합니다.
SIMD/벡터 확장
x86-64의 SIMD 확장은 SSE → AVX → AVX-512 → AMX로 발전해왔습니다. 커널은 암호화(AES-NI), 체크섬(CRC32c), RAID(XOR/P+Q), 문자열 연산에서 SIMD를 활용합니다. 각 세대의 레지스터 폭, 명령어 형식, 커널 사용 패턴을 이해해야 합니다.
| 세대 | 레지스터 | 폭 | 레지스터 수 | 인코딩 | 커널 주요 용도 |
|---|---|---|---|---|---|
| SSE | XMM | 128비트 | 16 | 레거시 (66/F2/F3) | AES-NI, CRC32c |
| AVX | YMM | 256비트 | 16 | VEX (C4/C5) | RAID6, SHA |
| AVX-512 | ZMM+k | 512비트 | 32+8 | EVEX (62) | RAID6, ChaCha20 |
| AMX | TMM | 8KB 타일 | 8 | VEX | 유저공간 AI/ML |
AVX-512 주파수 감소: AVX-512 사용 시 일부 CPU(Skylake 서버 등)에서 코어 주파수가 낮아집니다(License Level 1→2→3). 커널의 RAID6는 이를 고려하여 raid6_avx512x2_gen_syndrome()이 항상 최적은 아닐 수 있으며, lib/raid6/algos.c에서 부팅 시 벤치마크로 최적 구현을 선택합니다.
System V AMD64 ABI 호출 규약
System V AMD64 ABI는 리눅스, BSD, macOS 등 대부분의 Unix 계열 운영체제에서 사용하는 x86_64 호출 규약입니다. 커널과 유저 공간 모두 이 ABI를 따르며, 함수 인자 전달, 반환값, 스택 정렬, callee-saved 레지스터 규칙을 정의합니다.
| 역할 | 레지스터 | 설명 |
|---|---|---|
| 1번째 인자 | RDI | 정수/포인터 인자 1 |
| 2번째 인자 | RSI | 정수/포인터 인자 2 |
| 3번째 인자 | RDX | 정수/포인터 인자 3 |
| 4번째 인자 | RCX | 정수/포인터 인자 4 |
| 5번째 인자 | R8 | 정수/포인터 인자 5 |
| 6번째 인자 | R9 | 정수/포인터 인자 6 |
| 반환값 | RAX (+ RDX) | 128-bit 반환 시 RDX:RAX |
| Caller-saved | RAX, RCX, RDX, RSI, RDI, R8-R11 | 함수 호출 시 보존 불보장 |
| Callee-saved | RBX, RBP, R12-R15 | 호출된 함수가 반드시 보존 |
| 스택 포인터 | RSP | 16-byte 정렬 필수 (CALL 전) |
| FP 인자 1-8 | XMM0-XMM7 | 부동소수점/SSE 인자 |
레지스터 할당 다이어그램
System V AMD64 ABI에서 정수 인자 6개와 부동소수점 인자 8개는 레지스터로 전달되고, 초과분은 스택에 push됩니다. 반환값은 RAX(정수)와 XMM0(부동소수점)을 사용합니다.
C → 어셈블리 매핑(Mapping)
C 함수와 그에 대응하는 어셈블리 코드를 비교하면 ABI 규약이 어떻게 적용되는지 명확히 보입니다.
/* C 소스 */
long example(long a, long b, long c, long d, long e, long f) {
return a + b + c + d + e + f;
}
/* GCC -O2 출력 (AT&T 문법) */
example:
leaq (%rdi,%rsi), %rax # a(RDI) + b(RSI) → RAX
addq %rdx, %rax # + c(RDX)
addq %rcx, %rax # + d(RCX)
addq %r8, %rax # + e(R8)
addq %r9, %rax # + f(R9)
retq # RAX에 결과 반환
Red Zone 사용 예
리프 함수(다른 함수를 호출하지 않는 함수)는 Red Zone을 활용해 RSP 조정 없이 로컬 변수를 저장할 수 있습니다. 커널에서는 -mno-red-zone으로 비활성화합니다.
/* Red Zone을 사용하는 리프 함수 (유저 공간) */
leaf_func:
# RSP를 조정하지 않고 Red Zone에 저장
movq %rbx, -8(%rsp) # callee-saved 백업 (Red Zone)
movq %rdi, %rbx # 인자를 RBX에 저장
movq %rbx, %rax # 반환값 설정
movq -8(%rsp), %rbx # callee-saved 복원
retq
/* 커널에서는 인터럽트가 RSP 아래를 덮어쓰므로
arch/x86/Makefile: KBUILD_CFLAGS += -mno-red-zone */
16-byte 스택 정렬
ABI는 CALL 직전에 RSP가 16-byte 경계에 정렬되어야 합니다. SSE 명령어(MOVAPS 등)는 정렬되지 않은 접근 시 #GP 예외를 발생시킵니다.
/* 스택 정렬 예시: 7개 인자 함수 호출 */
caller:
subq $8, %rsp # 16-byte 정렬 패딩
pushq $42 # 7번째 인자 (스택)
movq $6, %r9 # 6번째 인자
movq $5, %r8 # 5번째 인자
movq $4, %rcx # 4번째 인자
movq $3, %rdx # 3번째 인자
movq $2, %rsi # 2번째 인자
movq $1, %rdi # 1번째 인자
callq target_func # CALL이 return addr push → RSP ≡ 0 mod 16
addq $16, %rsp # 스택 정리 (8 패딩 + 8 인자)
retq
RDI, RSI, RDX, RCX, R8, R9 순서이지만, SYSCALL 명령어가 RCX를 덮어쓰므로 시스템 콜 4번째 인자는 R10을 사용합니다.
4/5-레벨 페이징 워크
x86_64의 가상 주소 변환은 CR3 레지스터에서 시작하여 계층적 페이지 테이블을 순회합니다. 기본 4-레벨 페이징은 48-bit 가상 주소(256TB)를 지원하며, 5-레벨 페이징(LA57)은 57-bit 가상 주소(128PB)까지 확장합니다.
| 레벨 | 구조체(Struct) | VA 비트 범위 | 커버 크기 | 커널 매크로 |
|---|---|---|---|---|
| PML5 (5-레벨만) | pgd_t → p4d_t | [56:48] | 256TB / 엔트리 | pgd_offset() |
| PML4 | p4d_t → pud_t | [47:39] | 512GB / 엔트리 | p4d_offset() |
| PDPT | pud_t → pmd_t | [38:30] | 1GB / 엔트리 | pud_offset() |
| PD | pmd_t → pte_t | [29:21] | 2MB / 엔트리 | pmd_offset() |
| PT | pte_t | [20:12] | 4KB / 엔트리 | pte_offset_kernel() |
| Offset | — | [11:0] | 4KB 페이지 내 오프셋 | — |
4/5-레벨 페이징 워크 다이어그램
가상 주소의 각 9-bit 필드가 해당 테이블의 인덱스로 사용되어, CR3에서 최종 물리 주소까지 단계적으로 변환됩니다.
CR3 레지스터 포맷
CR3은 최상위 페이지 테이블의 물리 주소를 가리키며, PCID(Process Context ID)를 포함하여 TLB 플러시를 최적화합니다.
/* CR3 레지스터 포맷 (PCID 활성 시) */
/*
* Bit 63: PCID 무효화 제어 (INVPCID)
* 0 = CR3 로드 시 PCID에 해당하는 TLB 엔트리 무효화
* 1 = TLB 무효화 생략 (noflush)
* Bit 62:12 PML4/PML5 물리 주소 (4KB 정렬)
* Bit 11:0 PCID (12-bit, 최대 4096 프로세스 구분)
*/
/* arch/x86/include/asm/tlbflush.h */
#define X86_CR3_PCID_NOFLUSH (1UL << 63) /* TLB 무효화 생략 */
#define X86_CR3_PCID_MASK 0xFFF /* PCID 12-bit */
/* KPTI: 커널/유저 페이지 테이블 분리 시 CR3 전환 */
/* 유저 PGD = 커널 PGD + PAGE_SIZE (연속 배치) */
#define PTI_USER_PGTABLE_BIT PAGE_SHIFT
#define PTI_USER_PGTABLE_MASK (1 << PTI_USER_PGTABLE_BIT)
커널 페이지 테이블 매크로
커널은 5단계 추상화 매크로를 사용하여 4-레벨과 5-레벨 페이징을 통합 관리합니다. 4-레벨 시스템에서는 p4d 레벨이 fold되어 PGD와 동일하게 동작합니다.
/* arch/x86/include/asm/pgtable.h — 페이지 테이블 워크 매크로 */
/* 1단계: PGD (= PML5 or PML4) */
pgd_t *pgd = pgd_offset(mm, addr); /* mm→pgd + pgd_index(addr) */
/* 2단계: P4D (5-레벨에서만 실제 테이블, 4-레벨에서는 fold) */
p4d_t *p4d = p4d_offset(pgd, addr); /* 4-레벨: (p4d_t *)pgd */
/* 3단계: PUD (= PDPT) */
pud_t *pud = pud_offset(p4d, addr); /* p4d_page_vaddr(*p4d) + pud_index(addr) */
/* 4단계: PMD (= PD) */
pmd_t *pmd = pmd_offset(pud, addr); /* pud_page_vaddr(*pud) + pmd_index(addr) */
/* 5단계: PTE (= PT entry) */
pte_t *pte = pte_offset_kernel(pmd, addr);
/* Huge page 확인 */
if (pud_large(*pud)) /* 1GB huge page (PS 비트) */
if (pmd_large(*pmd)) /* 2MB huge page (PS 비트) */
/* 5-레벨 감지 */
if (pgtable_l5_enabled())
/* CR4.LA57 = 1, PML5 활성 */
cpu_tlbstate에서 최대 6개 PCID를 라운드 로빈(Round Robin)으로 할당하며, INVPCID 명령어로 특정 PCID의 엔트리만 선택적으로 무효화합니다.
커널 어셈블리 실전 예제
리눅스 커널은 성능이 중요하거나 하드웨어 접근이 필요한 경로에서 직접 어셈블리를 사용합니다. 시스템 콜 진입점(Entry Point), 컨텍스트 전환, 원자적 연산(Atomic Operation), 유저 공간 데이터 복사 등이 대표적입니다.
시스템 콜 진입점 (entry_SYSCALL_64)
SYSCALL 명령어가 실행되면 프로세서는 Ring 0으로 전환하고 LSTAR MSR이 가리키는 entry_SYSCALL_64로 점프합니다. 이 코드는 유저 레지스터를 저장하고 커널 스택으로 전환합니다.
/* arch/x86/entry/entry_64.S — 시스템 콜 진입 */
SYM_CODE_START(entry_SYSCALL_64)
swapgs # GS 베이스를 커널 per-CPU로 전환
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
# 유저 RSP를 TSS에 임시 저장
SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp # KPTI: 커널 페이지 테이블로 전환
movq PER_CPU_VAR(pcpu_hot + X86_top_of_stack), %rsp
# 커널 스택 포인터 로드
/* pt_regs 구조체에 레지스터 저장 */
pushq $__USER_DS # pt_regs→ss
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) # pt_regs→sp (유저 RSP)
pushq %r11 # pt_regs→flags (SYSCALL이 저장한 RFLAGS)
pushq $__USER_CS # pt_regs→cs
pushq %rcx # pt_regs→ip (SYSCALL이 저장한 RIP)
pushq %rax # pt_regs→orig_ax (시스템 콜 번호)
PUSH_AND_CLEAR_REGS rax=$-ENOSYS # 나머지 레지스터 저장 + 클리어
movq %rsp, %rdi # pt_regs 포인터를 1번째 인자로
callq do_syscall_64 # C 핸들러 호출
컨텍스트 전환 (__switch_to_asm)
스케줄러(Scheduler)가 태스크(Task)를 전환할 때 callee-saved 레지스터와 스택 포인터를 교체합니다.
/* arch/x86/entry/entry_64.S — 컨텍스트 전환 */
SYM_FUNC_START(__switch_to_asm)
/* callee-saved 레지스터 저장 (prev 태스크) */
pushq %rbp
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
pushq %r15
/* 스택 포인터 교체 */
movq %rsp, TASK_threadsp(%rdi) # prev→thread.sp = RSP
movq TASK_threadsp(%rsi), %rsp # RSP = next→thread.sp
/* STACKLEAK: 스택 카나리 포이즌 */
#ifdef CONFIG_STACKPROTECTOR
movq TASK_stack_canary(%rsi), %rbx
movq %rbx, PER_CPU_VAR(fixed_percpu_data) + FIXED_stack_canary
#endif
/* callee-saved 레지스터 복원 (next 태스크) */
popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
jmp __switch_to # C 함수로 FPU, TLS 등 나머지 전환
SYM_FUNC_END(__switch_to_asm)
LOCK CMPXCHG 원자적 연산
커널의 atomic_cmpxchg()는 LOCK CMPXCHG로 구현되어 멀티코어 환경에서 원자적 비교-교환을 수행합니다.
/* arch/x86/include/asm/cmpxchg.h — atomic compare-and-exchange */
/*
* CMPXCHG: if (*ptr == old) { *ptr = new; ZF=1; } else { EAX = *ptr; ZF=0; }
* LOCK 접두사: 버스 락/캐시 락으로 원자성 보장
*/
static __always_inline unsigned long
__cmpxchg(volatile void *ptr, unsigned long old, unsigned long new, int size)
{
unsigned long prev;
switch (size) {
case 8:
asm volatile("lock; cmpxchgq %1, %2"
: "=a"(prev) /* output: RAX = prev */
: "r"(new), "m"(*((u64 *)ptr)),
"0"(old) /* input: RAX = old */
: "memory");
return prev;
}
}
copy_user_generic 유저 메모리 복사
유저 공간 데이터를 커널로 복사할 때는 페이지 폴트(Page Fault)가 발생할 수 있어 _ASM_EXTABLE로 예외 처리를 등록합니다.
/* arch/x86/lib/copy_user_64.S — 유저 메모리 복사 (REP MOVSB) */
SYM_FUNC_START(copy_user_generic_unrolled)
ASM_STAC # SMAP 해제 (유저 메모리 접근 허용)
movq %rdx, %rcx # count → RCX
1: rep movsb # RSI(src)→RDI(dst), RCX 바이트
ASM_CLAC # SMAP 복원
xorl %eax, %eax # 성공: 잔여 바이트 0 반환
RET
/* 페이지 폴트 발생 시 복구 핸들러 */
.Lcopy_user_fault:
movq %rcx, %rax # 복사하지 못한 잔여 바이트 반환
ASM_CLAC
RET
/* 예외 테이블: 1:에서 폴트 → .Lcopy_user_fault로 점프 */
_ASM_EXTABLE_UA(1b, .Lcopy_user_fault)
SYM_FUNC_END(copy_user_generic_unrolled)
Per-CPU 변수 접근
x86_64에서 per-CPU 변수는 GS 세그먼트를 통해 접근합니다. 커널 모드에서 GS 베이스는 MSR_GS_BASE에 per-CPU 영역 시작 주소를 저장합니다.
/* arch/x86/include/asm/percpu.h — Per-CPU 접근 */
/* GS 세그먼트 베이스 = per-CPU 영역 시작 주소
* __per_cpu_offset[cpu]에 해당하는 값이 GS 베이스로 설정됨
* 변수 접근 시 %gs:offset 으로 직접 메모리 참조 */
#define this_cpu_read_8(pcp) ({ \
u64 val; \
asm volatile("movq %%gs:%1, %0" \
: "=r"(val) \
: "m"(pcp)); \
val; })
#define this_cpu_inc_8(pcp) \
asm volatile("incq %%gs:%0" \
: "+m"(pcp))
/* 사용 예: current 매크로 (현재 태스크 포인터) */
#define current this_cpu_read_stable(pcpu_hot.current_task)
ALTERNATIVE 매크로 (조건부 패칭)
CPU 기능에 따라 부팅 시 코드를 동적으로 패칭합니다. 기본 코드와 대체 코드를 함께 배치하고, 런타임에 CPU 기능을 확인하여 패칭합니다.
/* arch/x86/include/asm/alternative.h — ALTERNATIVE 매크로 */
/*
* 기본 코드(oldinstr)를 배치하고, 대체 코드(newinstr)는
* .altinstructions 섹션에 메타데이터와 함께 기록.
* 부팅 시 apply_alternatives()가 CPU feature 확인 후
* 기본 코드를 대체 코드로 덮어씀.
*/
/* 예: SWAPGS 대신 WRGSBASE 사용 (FSGSBASE 지원 시) */
ALTERNATIVE "swapgs", \
"wrgsbase %rax", \
X86_FEATURE_FSGSBASE
/* 예: REP MOVSB 대신 ERMS 버전 사용 */
ALTERNATIVE "rep movsb", \
"call enhanced_rep_movsb", \
X86_FEATURE_ERMS
/* 이중 대체: feature1 없으면 기본, feature1이면 alt1, feature2면 alt2 */
ALTERNATIVE_2 "mfence; lfence", \
"lfence", X86_FEATURE_LFENCE_RDTSC, \
"nop", X86_FEATURE_SERIALIZE
RET 매크로를 사용하며, 이는 Spectre v2 완화를 위한 retpoline 코드로 대체될 수 있습니다. 직접 retq를 쓰면 보안 취약점이 될 수 있습니다.
세그멘테이션과 GDT/TSS
x86_64 Long Mode에서 세그멘테이션은 대부분 비활성화되지만, GDT(Global Descriptor Table)는 여전히 필수입니다. 커널/유저 코드 세그먼트의 특권 레벨, TSS(Task State Segment)의 스택 포인터, SYSCALL/SYSRET의 MSR 설정 등에서 세그먼트 디스크립터가 사용됩니다.
| GDT 엔트리 | 셀렉터 | DPL | 용도 |
|---|---|---|---|
| NULL | 0x00 | — | 필수 NULL 디스크립터 |
| __KERNEL_CS | 0x10 | 0 | 커널 코드 세그먼트 (CS.L=1) |
| __KERNEL_DS | 0x18 | 0 | 커널 데이터 세그먼트 |
| __USER_CS (32) | 0x23 | 3 | 유저 32-bit 코드 (Compat Mode) |
| __USER_DS | 0x2B | 3 | 유저 데이터 세그먼트 |
| __USER_CS | 0x33 | 3 | 유저 64-bit 코드 세그먼트 |
| TSS | 0x40 | 0 | 태스크 상태 세그먼트 (16바이트) |
| Per-CPU | — | 0 | GS 베이스 (per-CPU 데이터) |
GDT 레이아웃 다이어그램
GDT는 per-CPU로 할당되며, 커널/유저 세그먼트와 TSS를 포함합니다. Long Mode에서 세그먼트의 base/limit은 무시되지만(FS/GS 제외), DPL과 타입 필드는 여전히 검증됩니다.
GDT 초기화
부팅 초기에 per-CPU GDT를 설정하고 LGDT로 프로세서에 로드합니다.
/* arch/x86/kernel/cpu/common.c — GDT 초기화 */
DEFINE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page) = { .gdt = {
[GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT(0xa09b, 0, 0xfffff),
/* flags: G=1, L=1, P=1, DPL=0, S=1, Type=Code R/A */
[GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT(0xc093, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(0xa0fb, 0, 0xfffff),
/* flags: G=1, L=1, P=1, DPL=3, S=1, Type=Code R/A */
[GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(0xc0f3, 0, 0xfffff),
}};
/* arch/x86/kernel/cpu/common.c — LGDT 로드 */
static inline void load_gdt(const struct desc_ptr *dtr)
{
asm volatile("lgdt %0" :: "m"(*dtr));
}
TSS 설정
TSS는 Long Mode에서 IST(Interrupt Stack Table)와 IO 비트맵(Bitmap)을 제공합니다. 특권 레벨 전환 시 사용할 커널 스택을 지정합니다.
/* arch/x86/include/asm/processor.h — TSS 구조체 */
struct tss_struct {
struct x86_hw_tss x86_tss; /* 하드웨어 TSS */
struct x86_io_bitmap io_bitmap; /* IO 포트 비트맵 */
} __aligned(PAGE_SIZE);
struct x86_hw_tss {
u32 reserved1;
u64 sp0; /* Ring 3→0 전환 시 커널 스택 */
u64 sp1; /* Ring 1 스택 (미사용) */
u64 sp2; /* Ring 2 스택 (SYSCALL 임시 저장용) */
u64 reserved2;
u64 ist[7]; /* IST 1~7: 전용 인터럽트 스택 */
/* IST1: #DF (Double Fault)
* IST2: #NMI
* IST3: #DB (Debug)
* IST4: #MC (Machine Check) */
u32 reserved3;
u16 reserved4;
u16 io_bitmap_base; /* IO 비트맵 오프셋 */
} __packed;
SYSCALL/SYSRET MSR 설정
SYSCALL/SYSRET 명령어의 동작을 제어하는 MSR을 부팅 시 초기화합니다.
/* arch/x86/kernel/cpu/common.c — SYSCALL MSR 초기화 */
void syscall_init(void)
{
/* STAR: SYSCALL/SYSRET CS/SS 베이스 */
wrmsrl(MSR_STAR,
((u64)__USER32_CS << 48) | /* SYSRET CS base (유저 복귀) */
((u64)__KERNEL_CS << 32)); /* SYSCALL CS base (커널 진입) */
/* LSTAR: SYSCALL 진입점 */
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
/* SFMASK: SYSCALL 시 클리어할 RFLAGS 비트
* IF=0 (인터럽트 비활성), TF=0 (트레이스 비활성),
* DF=0 (방향 플래그 클리어), AC=0 (SMAP 활성 유지) */
wrmsrl(MSR_SYSCALL_MASK,
X86_EFLAGS_TF | X86_EFLAGS_DF | X86_EFLAGS_IF |
X86_EFLAGS_IOPL | X86_EFLAGS_AC | X86_EFLAGS_NT);
}
SYSRET이 실행되면 #GP가 Ring 0에서 발생하여 보안 취약점(CVE-2014-4699)이 됩니다. Linux 커널은 SYSRET 전에 RIP가 정규 주소인지 검증합니다.
인터럽트/예외 처리 흐름
x86_64에서 인터럽트와 예외는 IDT(Interrupt Descriptor Table)를 통해 처리됩니다. 프로세서는 인터럽트 벡터 번호로 IDT를 색인하여 게이트 디스크립터에서 핸들러 주소를 가져옵니다. 예외(0~31번)는 동기적이고, 외부 인터럽트(32~255번)는 APIC을 통해 비동기적으로 전달됩니다.
| 벡터 | 이름 | 타입 | IST | 설명 |
|---|---|---|---|---|
| 0 | #DE | Fault | — | Division Error |
| 1 | #DB | Fault/Trap | IST3 | Debug (하드웨어 브레이크포인트) |
| 2 | NMI | Interrupt | IST2 | Non-Maskable Interrupt |
| 3 | #BP | Trap | — | Breakpoint (INT3) |
| 6 | #UD | Fault | — | Invalid Opcode |
| 8 | #DF | Abort | IST1 | Double Fault |
| 13 | #GP | Fault | — | General Protection |
| 14 | #PF | Fault | — | Page Fault (에러코드: P/W/U/R/I) |
| 18 | #MC | Abort | IST4 | Machine Check |
| 32~255 | IRQ | Interrupt | — | 외부 장치 인터럽트 (APIC) |
인터럽트 처리 흐름 다이어그램
하드웨어 인터럽트가 도착하면 Local APIC → IDT → 핸들러 체인 → EOI 순서로 처리됩니다.
IDT 게이트 디스크립터
64-bit IDT 게이트는 16바이트 크기로, 핸들러 주소 64-bit 전체와 IST 인덱스, DPL을 포함합니다.
/* arch/x86/include/asm/desc_defs.h — IDT 게이트 디스크립터 */
struct gate_struct {
u16 offset_low; /* 핸들러 주소 [15:0] */
u16 segment; /* 코드 세그먼트 셀렉터 (__KERNEL_CS) */
struct idt_bits bits;
u16 offset_middle; /* 핸들러 주소 [31:16] */
u32 offset_high; /* 핸들러 주소 [63:32] */
u32 reserved;
};
struct idt_bits {
u16 ist : 3; /* IST 인덱스 (0=미사용, 1~7=전용 스택) */
u16 zero : 5;
u16 type : 4; /* 0xE=Interrupt Gate, 0xF=Trap Gate */
u16 dpl : 2; /* DPL (0=커널만, 3=유저 INT3 허용) */
u16 p : 1; /* Present */
};
/* Interrupt Gate: IF 자동 클리어 (인터럽트 비활성)
* Trap Gate: IF 유지 (예외 처리 중 인터럽트 가능) */
인터럽트 진입 어셈블리
모든 인터럽트 벡터는 매크로로 생성된 스텁에서 공통 핸들러로 점프합니다.
/* arch/x86/entry/entry_64.S — 인터럽트 진입 */
SYM_CODE_START(asm_common_interrupt)
UNWIND_HINT_ENTRY
ASM_CLAC # SMAP: 유저 접근 불가로 설정
pushq $-1 # pt_regs→orig_ax = -1 (인터럽트 표시)
PUSH_AND_CLEAR_REGS # 범용 레지스터 전체 저장
testb $3, CS(%rsp) # CS의 RPL 확인
jz 1f # 커널 → SWAPGS 불필요
swapgs # 유저에서 왔으면 GS 전환
SWITCH_TO_KERNEL_CR3 scratch_reg=%rdi # KPTI 커널 CR3
1: movq %rsp, %rdi # pt_regs → 1번째 인자
movq %rsp, %rsi
callq common_interrupt # C 핸들러 호출
jmp ret_from_intr # 복귀 경로
SYM_CODE_END(asm_common_interrupt)
APIC EOI 시퀀스
인터럽트 처리 완료 후 Local APIC에 EOI(End Of Interrupt)를 보내 다음 인터럽트를 수신 가능하게 합니다.
/* arch/x86/kernel/apic/apic.c — APIC EOI */
static inline void apic_eoi(void)
{
/* EOI 레지스터에 0을 쓰면 가장 높은 우선순위의 ISR 비트 클리어 */
apic_write(APIC_EOI, 0);
}
/* x2APIC 모드: MSR을 통한 EOI (더 빠름) */
static inline void x2apic_eoi(void)
{
wrmsrl(APIC_BASE_MSR + (APIC_EOI >> 4), 0);
}
/* 인터럽트 처리 완료 흐름 */
void handle_edge_irq(struct irq_desc *desc)
{
handle_irq_event(desc); /* handler 체인 실행 */
cond_unmask_eoi_irq(desc, chip); /* chip→irq_eoi() → apic_eoi() */
}
CET (IBT/Shadow Stack)
CET(Control-flow Enforcement Technology)는 Intel이 도입한 하드웨어 제어 흐름 무결성(CFI) 기술로, IBT(Indirect Branch Tracking)와 Shadow Stack 두 가지 메커니즘으로 구성됩니다. 리눅스 커널 6.2+에서 IBT, 6.6+에서 유저 공간 Shadow Stack을 지원합니다.
| 기능 | 메커니즘 | 보호 대상 | 커널 지원 |
|---|---|---|---|
| IBT | 간접 분기 대상에 ENDBR 필수 | JOP/COP 공격 | CONFIG_X86_KERNEL_IBT (v6.2) |
| Shadow Stack | 별도 스택에 return address 저장 | ROP 공격 | CONFIG_X86_USER_SHADOW_STACK (v6.6) |
CET 동작 흐름 다이어그램
IBT는 간접 호출 대상이 ENDBR64로 시작하는지 검증하고, Shadow Stack은 CALL/RET 시 반환 주소를 이중 검증합니다.
ENDBR64 어노테이션
IBT가 활성화되면 모든 간접 분기 대상은 ENDBR64로 시작해야 합니다. 커널은 objtool로 검증합니다.
/* arch/x86/include/asm/ibt.h — IBT 어노테이션 */
#ifdef CONFIG_X86_KERNEL_IBT
/* ENDBR64 = F3 0F 1E FA (4바이트 NOP on non-CET CPUs) */
#define __noendbr __attribute__((nocf_check))
/* 어셈블리에서의 ENDBR */
SYM_FUNC_START(my_function)
endbr64 # IBT landing pad — 간접 호출 허용 지점
pushq %rbp
...
/* C 함수: 컴파일러가 자동으로 ENDBR64 삽입 (-fcf-protection=branch)
* 함수 포인터로 호출될 수 있는 모든 함수에 필수
* objtool이 누락된 ENDBR을 감지하여 빌드 경고 */
/* FineIBT (v6.2+): kCFI + IBT 결합
* 간접 호출 전에 타입 해시 검증 → ENDBR에서 해시 비교
* JOP 공격에 대한 더 강력한 보호 */
#else
#define __noendbr
#endif
Shadow Stack 설정
Shadow Stack은 CR4.CET와 MSR로 활성화하며, 유저 공간에서 map_shadow_stack 시스템 콜로 Shadow Stack 메모리를 할당합니다.
/* arch/x86/kernel/shstk.c — Shadow Stack 설정 */
int shstk_setup(void)
{
struct thread_shstk *shstk = ¤t->thread.shstk;
unsigned long addr, size;
/* Shadow Stack 페이지 할당 (VM_SHADOW_STACK 플래그)
* PTE: Dirty=0, Write=1 조합은 Shadow Stack 전용
* 일반 소프트웨어가 이 페이지에 쓰기 불가 */
size = PAGE_ALIGN(min_t(unsigned long, rlimit(RLIMIT_STACK), SZ_4G));
addr = do_mmap(NULL, 0, size,
PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, 0,
&unused, NULL);
set_clr_bits_msrl(MSR_IA32_U_CET, CET_SHSTK_EN, 0);
wrmsrl(MSR_IA32_PL3_SSP, addr + size); # 유저 SSP 설정 */
shstk->base = addr;
shstk->size = size;
return 0;
}
/* CR4.CET 활성화 */
cr4_set_bits(X86_CR4_CET);
retpoline과 IBT/FineIBT로 제어 흐름을 보호합니다. 유저 공간 Shadow Stack만 map_shadow_stack(2)과 arch_prctl(ARCH_SHSTK_*)을 통해 지원됩니다.
alternatives 패칭
리눅스 커널의 alternatives 시스템은 부팅 시 CPU 기능에 따라 명령어를 동적으로 교체합니다. 기본 코드(모든 CPU에서 동작)와 대체 코드(특정 기능 지원 시 사용)를 함께 빌드하고, apply_alternatives()가 런타임에 패칭합니다.
| 매크로 | 대체 수 | 용도 |
|---|---|---|
| ALTERNATIVE | 1 | 단일 기능 분기 |
| ALTERNATIVE_2 | 2 | 이중 기능 분기 (우선순위(Priority): alt2 > alt1) |
| ALTERNATIVE_TERNARY | 2 | feature ? alt1 : alt2 |
| static_cpu_has() | 1 | C 코드에서 NOP 패딩(Padding) 분기 |
ALTERNATIVE 매크로 구조
.altinstructions 섹션에 메타데이터를 기록하고, .altinstr_replacement에 대체 코드를 배치합니다.
/* arch/x86/include/asm/alternative.h — ALTERNATIVE 내부 구조 */
struct alt_instr {
s32 instr_offset; /* 원본 명령어 위치 (.text 내) */
s32 repl_offset; /* 대체 명령어 위치 (.altinstr_replacement) */
u16 cpuid; /* X86_FEATURE_* (CPU 기능 비트) */
u8 instrlen; /* 원본 명령어 길이 */
u8 replacementlen; /* 대체 명령어 길이 */
};
/* ALTERNATIVE 매크로 확장 예:
* .section .text
* 770: oldinstr ← 기본 코드 (모든 CPU)
* 771: .skip (pad) ← NOP 패딩 (대체 코드가 더 길 경우)
*
* .section .altinstr_replacement
* 772: newinstr ← 대체 코드
*
* .section .altinstructions
* .long 770b - . ← instr_offset (상대)
* .long 772b - . ← repl_offset (상대)
* .word X86_FEATURE_XXX ← 필요한 CPU 기능
* .byte 771b - 770b ← instrlen
* .byte 773f - 772b ← replacementlen
*/
apply_alternatives 패칭 과정
부팅 초기에 apply_alternatives()가 .altinstructions 섹션을 순회하며 CPU 기능을 확인하고, 지원되는 기능의 대체 코드로 원본을 덮어씁니다.
/* arch/x86/kernel/alternative.c — 패칭 엔진 */
void __init_or_module apply_alternatives(struct alt_instr *start,
struct alt_instr *end)
{
struct alt_instr *a;
u8 *instr, *replacement;
for (a = start; a < end; a++) {
instr = (u8 *)&a->instr_offset + a->instr_offset;
replacement = (u8 *)&a->repl_offset + a->repl_offset;
if (!boot_cpu_has(a->cpuid))
continue; /* CPU가 해당 기능 미지원 → 스킵 */
/* text_poke_early(): .text 영역에 직접 쓰기
* 대체 코드를 원본 위치에 복사
* 잔여 공간은 NOP으로 채움 */
text_poke_early(instr, replacement, a->replacementlen);
add_nops(instr + a->replacementlen,
a->instrlen - a->replacementlen);
}
}
static_cpu_has와 NOP 패딩
C 코드에서 static_cpu_has()는 조건 분기를 NOP으로 패칭하여 분기 예측 오버헤드를 제거합니다.
/* arch/x86/include/asm/cpufeature.h — static_cpu_has */
/* 부팅 시: 해당 기능이 있으면 JMP를 2-byte NOP으로 교체
* 없으면 JMP를 유지하여 대체 경로로 분기 */
static __always_inline bool _static_cpu_has(u16 bit)
{
asm goto(
ALTERNATIVE_TERNARY(
"jmp %l[t_no]", /* 기본: no로 점프 */
%P[cap], /* X86_FEATURE_* */
"", /* 기능 있음: NOP (fall-through → true) */
"jmp %l[t_no]") /* 기능 없음: 점프 유지 */
: : [cap] "i"(bit) : : t_no);
return true;
t_no:
return false;
}
/* 사용 예: FSGSBASE 지원 시 RDGSBASE 사용 */
if (static_cpu_has(X86_FEATURE_FSGSBASE))
gs_base = rdgsbase(); /* 패칭 후: NOP → fall-through */
else
gs_base = rdmsrl(MSR_GS_BASE); /* 패칭 전/미지원: MSR 읽기 */
apply_alternatives()가 실행됩니다. 모듈의 .altinstructions 섹션을 순회하여 현재 CPU에 맞게 패칭합니다. 이는 module_finalize()에서 수행됩니다.
마이크로아키텍처 최적화
최신 x86_64 프로세서는 CISC 명령어를 내부적으로 µop(마이크로 연산)으로 분해하여 처리합니다. 프론트엔드(명령어 페치/디코드)와 백엔드(실행/은퇴) 파이프라인의 특성을 이해하면 커널 핫패스의 성능을 최적화할 수 있습니다.
| 구성 요소 | Zen 4 | Golden Cove | 설명 |
|---|---|---|---|
| 파이프라인 단계 | ~19단계 | ~20단계 | 분기 예측 실패 패널티 |
| 디코드 폭 | 4 µop/cycle | 6 µop/cycle | 프론트엔드 대역폭(Bandwidth) |
| µop 캐시 | 6,144 엔트리 | 4,096 엔트리 | L0 µop 캐시 |
| 실행 포트 | 6개 | 12개 | 백엔드 병렬 실행 유닛 |
| ROB 크기 | 320 엔트리 | 512 엔트리 | Reorder Buffer (비순차 실행 윈도우) |
| L1I 캐시 | 32KB | 32KB | 명령어 캐시 |
| 분기 예측기 | TAGE | TAGE | 분기 히스토리 기반 예측 |
파이프라인 단계 다이어그램
x86_64 프로세서의 파이프라인은 프론트엔드, 스케줄러, 백엔드로 구분되며, 각 단계에서 최적화 기회가 있습니다.
분기 예측 힌트
커널은 likely()/unlikely() 매크로로 GCC에 분기 방향 힌트를 제공합니다.
/* include/linux/compiler.h — 분기 예측 힌트 */
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
/* GCC는 likely 경로를 fall-through(JCC not-taken)로 배치
* → 분기 예측기의 정적 예측(forward not-taken)과 일치
* → 파이프라인 플러시 최소화 */
/* 사용 예: 페이지 폴트 핸들러 */
if (likely(!(fault & VM_FAULT_ERROR))) {
/* 정상 경로: 대부분의 페이지 폴트는 성공 */
return;
}
if (unlikely(fault & VM_FAULT_OOM)) {
/* OOM: 매우 드문 경로 → .text.unlikely 섹션으로 분리 */
pagefault_out_of_memory();
}
루프 정렬과 NOP
핫 루프를 캐시라인/µop 캐시 경계에 정렬하면 프론트엔드 대역폭을 극대화할 수 있습니다.
/* 커널 루프 정렬 예시 */
/* .balign 지시자: 다음 명령어를 지정 바이트 경계에 정렬
* 최적 NOP 패딩 삽입 (ideal_nops 테이블에서 선택) */
.balign 32 # 32바이트 경계 정렬 (µop 캐시 라인)
.Lloop:
movq (%rsi), %rax
movq %rax, (%rdi)
addq $8, %rsi
addq $8, %rdi
decq %rcx
jnz .Lloop # 루프 상단이 정렬되어 fetch 효율 최적
/* arch/x86/kernel/alternative.c — 최적 NOP 선택 */
/* ideal_nops[N]: N바이트 NOP 패딩
* 1: 0x90 (NOP)
* 2: 66 90 (66 NOP)
* 3: 0F 1F 00 (NOP DWORD ptr [RAX])
* ...
* 8+: 0F 1F 84 00 00 00 00 00 (long NOP) */
µop 캐시 고려사항
µop 캐시(DSB)에 핫 코드가 완전히 적재되면 디코더를 우회하여 처리량(Throughput)이 향상됩니다.
/* µop 캐시 효율성 체크 (perf 이벤트) */
# DSB(µop 캐시) vs MITE(레거시 디코더) 비율 확인
$ perf stat -e idq.dsb_uops,idq.mite_uops ./workload
/* 커널 핫패스 최적화 원칙:
* 1. 함수 크기를 µop 캐시 way(~32B 윈도우)에 맞춤
* 2. 드문 에러 경로를 __cold로 분리 (out-of-line)
* 3. 복잡한 명령어(예: DIV)는 다수 µop → 대안 사용
* (곱셈+시프트로 나눗셈 대체: reciprocal_divide)
* 4. LOCK 접두사: 캐시라인 정렬 데이터에서만 사용
* (split lock은 ~100x 느림 → 커널 split_lock_detect) */
/* 예: reciprocal division (나눗셈을 곱셈으로 대체) */
static inline u32 reciprocal_divide(u32 a, struct reciprocal_value R)
{
/* DIV(~30 µop) 대신 MUL+SHR(2 µop)로 대체 */
return (u32)(((u64)a * R.m) >> R.sh);
}
CONFIG_X86_SPLIT_LOCK_DETECT로 캐시라인 경계를 넘는 원자적 연산(split lock)을 감지합니다. Split lock은 시스템 전체 버스 락을 유발하여 성능이 극도로 저하되므로, 커널은 이를 경고하거나 SIGBUS로 프로세스(Process)를 종료할 수 있습니다.
VMX/SVM 명령어
x86_64 가상화는 Intel VT-x(VMX)와 AMD-V(SVM) 두 가지 하드웨어 확장으로 지원됩니다. 하이퍼바이저(KVM)는 VMX/SVM 명령어로 가상 머신을 생성하고 VM Entry/Exit 전환을 수행합니다.
| 기능 | Intel VMX | AMD SVM | 설명 |
|---|---|---|---|
| 활성화 | VMXON | VMRUN 직접 | VMX root 모드 진입 |
| 제어 구조 | VMCS (4KB) | VMCB (4KB) | VM 상태/제어 정보 |
| VM 진입 | VMLAUNCH/VMRESUME | VMRUN | 게스트 실행 시작 |
| VM 탈출 | VM-Exit (자동) | #VMEXIT (자동) | 하이퍼바이저로 복귀 |
| 메모리 가상화 | EPT | NPT (RVI) | 2단계 주소 변환 |
| 인터럽트 가상화 | Posted Interrupts | AVIC | VM-Exit 없는 인터럽트 전달 |
| CPUID 필드 | CPUID.1:ECX.VMX[5] | CPUID.8000_0001h:ECX.SVM[2] | 기능 탐지 |
VMCS/VMCB 구조와 VM Entry/Exit 흐름
VMCS(Intel)와 VMCB(AMD)는 게스트 상태, 호스트 상태, 제어 필드를 포함하는 4KB 구조체입니다.
VMXON/VMLAUNCH 시퀀스
KVM이 VMX를 초기화하고 게스트를 실행하는 과정입니다.
/* arch/x86/kvm/vmx/vmx.c — VMX 초기화 및 게스트 실행 */
/* 1. VMXON: VMX root 모드 진입 */
static int vmx_hardware_enable(void)
{
/* CR4.VMXE = 1 (VMX 활성화) */
cr4_set_bits(X86_CR4_VMXE);
/* VMXON: 4KB VMXON 영역 물리 주소 전달 */
asm volatile("vmxon %0" : : "m"(phys_addr) : "memory", "cc");
return 0;
}
/* 2. VMLAUNCH/VMRESUME: 게스트 실행 */
static noinstr void vmx_vcpu_enter_exit(struct kvm_vcpu *vcpu)
{
/* 호스트 레지스터 저장 */
asm volatile(
"pushq %%rbp\n\t"
"pushq %%rcx\n\t"
/* ... 호스트 callee-saved 저장 ... */
/* 게스트 레지스터 로드 */
"movq %c[rax](%[regs]), %%rax\n\t"
"movq %c[rbx](%[regs]), %%rbx\n\t"
/* ... 나머지 레지스터 ... */
/* VM Entry! */
"cmpb $0, %[launched]\n\t"
"jne .Lvmresume\n\t"
"vmlaunch\n\t" # 첫 실행
"jmp .Lvmfail\n\t"
".Lvmresume:\n\t"
"vmresume\n\t" # 재실행
/* VM-Exit 시 여기로 복귀 → 게스트 레지스터 저장 */
);
}
VMCS 필드 읽기/쓰기
VMCS는 VMREAD/VMWRITE 명령어로만 접근 가능한 구조화된 데이터입니다.
/* arch/x86/kvm/vmx/vmcs.h — VMCS 필드 접근 */
static __always_inline unsigned long vmcs_readl(unsigned long field)
{
unsigned long value;
asm volatile("vmread %1, %0"
: "=r"(value) : "r"(field) : "cc");
return value;
}
static __always_inline void vmcs_writel(unsigned long field, unsigned long value)
{
asm volatile("vmwrite %1, %0"
:: "r"(field), "rm"(value) : "cc");
}
/* 주요 VMCS 필드 */
#define GUEST_RIP 0x681E /* 게스트 명령어 포인터 */
#define GUEST_RSP 0x681C /* 게스트 스택 포인터 */
#define GUEST_CR3 0x6802 /* 게스트 CR3 (EPT 사용 시 GPA) */
#define VM_EXIT_REASON 0x4402 /* Exit 사유 (16-bit) */
#define EXIT_QUALIFICATION 0x6400 /* Exit 상세 정보 */
#define EPT_POINTER 0x201A /* EPT PML4 물리 주소 */
/* VM-Exit 후 사유 확인 */
u32 reason = vmcs_read32(VM_EXIT_REASON);
kvm_vmx_exit_handlers[reason](vcpu); /* 핸들러 디스패치 */
VMRUN %rax 하나로 게스트 진입과 상태 로드가 동시에 수행됩니다. KVM은 arch/x86/kvm/svm/에서 SVM 경로를 별도 구현합니다.
디버그 레지스터/하드웨어 브레이크포인트
x86_64는 4개의 디버그 주소 레지스터(DR0-DR3)와 상태/제어 레지스터(DR6/DR7)를 통해 하드웨어 브레이크포인트를 지원합니다. 소프트웨어 브레이크포인트(INT3)와 달리 코드 수정 없이 메모리 접근/실행을 모니터링할 수 있어 watchpoint와 커널 디버깅에 필수적입니다.
| 레지스터 | 용도 | 설명 |
|---|---|---|
| DR0-DR3 | 브레이크포인트 주소 | 모니터링할 선형 주소 (4개 동시) |
| DR4-DR5 | 예약/별칭 | CR4.DE=0이면 DR6/DR7 별칭 |
| DR6 | 디버그 상태 | #DB 발생 시 어떤 조건이 트리거되었는지 |
| DR7 | 디버그 제어 | 각 브레이크포인트의 활성/타입/크기 설정 |
DR7 제어 레지스터 비트 레이아웃
| 비트 범위 | 필드 | BP # | 값 의미 |
|---|---|---|---|
| [1:0] | L0, G0 | BP0 | Local/Global 활성 |
| [3:2] | L1, G1 | BP1 | Local/Global 활성 |
| [5:4] | L2, G2 | BP2 | Local/Global 활성 |
| [7:6] | L3, G3 | BP3 | Local/Global 활성 |
| [17:16] | R/W0 | BP0 | 00=실행, 01=데이터 쓰기, 10=I/O, 11=데이터 R/W |
| [19:18] | LEN0 | BP0 | 00=1B, 01=2B, 10=8B, 11=4B |
| [21:20] | R/W1 | BP1 | (동일 인코딩) |
| [23:22] | LEN1 | BP1 | (동일 인코딩) |
| [25:24] | R/W2 | BP2 | (동일 인코딩) |
| [27:26] | LEN2 | BP2 | (동일 인코딩) |
| [29:28] | R/W3 | BP3 | (동일 인코딩) |
| [31:30] | LEN3 | BP3 | (동일 인코딩) |
DR0-DR3 주소 설정
디버그 주소 레지스터에 감시할 선형 주소를 설정합니다. 커널은 set_debugreg() 매크로로 접근합니다.
/* arch/x86/include/asm/debugreg.h — 디버그 레지스터 접근 */
#define set_debugreg(value, reg) \
asm volatile("movq %0, %%db" #reg \
:: "r"((unsigned long)(value)))
#define get_debugreg(var, reg) \
asm volatile("movq %%db" #reg ", %0" \
: "=r"(var))
/* 하드웨어 브레이크포인트 설정 예 */
void hw_breakpoint_set(int bp_num, unsigned long addr)
{
switch (bp_num) {
case 0: set_debugreg(addr, 0); break; /* DR0 */
case 1: set_debugreg(addr, 1); break; /* DR1 */
case 2: set_debugreg(addr, 2); break; /* DR2 */
case 3: set_debugreg(addr, 3); break; /* DR3 */
}
}
DR7 구성 (타입, 크기, 활성화)
DR7은 각 브레이크포인트의 타입(실행/읽기/쓰기), 크기(1/2/4/8바이트), 로컬/글로벌 활성 비트를 제어합니다.
/* arch/x86/kernel/hw_breakpoint.c — DR7 설정 */
unsigned long encode_dr7(int drnum, unsigned len, unsigned type)
{
unsigned long dr7 = 0;
/* Local enable 비트 (DR7[2*drnum]) */
dr7 |= (1UL << (drnum * 2));
/* R/W 필드: 비트 [16+4*drnum : 17+4*drnum]
* 00 = 실행 (X)
* 01 = 데이터 쓰기만 (W)
* 10 = I/O 읽기/쓰기 (CR4.DE=1 필요)
* 11 = 데이터 읽기+쓰기 (RW) */
dr7 |= ((unsigned long)type << (16 + drnum * 4));
/* LEN 필드: 비트 [18+4*drnum : 19+4*drnum]
* 00 = 1바이트
* 01 = 2바이트
* 10 = 8바이트 (x86_64에서만)
* 11 = 4바이트 */
dr7 |= ((unsigned long)len << (18 + drnum * 4));
return dr7;
}
/* DR7 적용 */
unsigned long dr7;
get_debugreg(dr7, 7);
dr7 |= encode_dr7(0, DR7_LEN_4, DR7_BREAK_ON_WRITE); /* BP0: 4B 쓰기 감시 */
set_debugreg(dr7, 7);
perf_event 하드웨어 브레이크포인트 API
유저 공간에서는 perf_event_open()을 통해 하드웨어 브레이크포인트를 설정할 수 있으며, 커널 내부에서는 register_wide_hw_breakpoint()를 사용합니다.
/* kernel/events/hw_breakpoint.c — 커널 hw_breakpoint API */
struct perf_event * register_wide_hw_breakpoint(
struct perf_event_attr *attr,
perf_overflow_handler_t triggered,
void *context)
{
/* 모든 CPU에서 하드웨어 브레이크포인트 등록
* attr→bp_type: HW_BREAKPOINT_W, HW_BREAKPOINT_RW, HW_BREAKPOINT_X
* attr→bp_len: HW_BREAKPOINT_LEN_1/2/4/8
* attr→bp_addr: 감시할 가상 주소
* triggered: #DB 발생 시 호출되는 콜백 */
}
/* 사용 예: 커널 변수 감시 (watchpoint) */
struct perf_event_attr attr = {
.type = PERF_TYPE_BREAKPOINT,
.bp_type = HW_BREAKPOINT_W, /* 쓰기 감시 */
.bp_addr = (unsigned long)&watched_var,
.bp_len = HW_BREAKPOINT_LEN_8, /* 8바이트 */
.disabled = 0,
};
struct perf_event *bp = register_wide_hw_breakpoint(
&attr, my_bp_handler, NULL);
/* #DB 핸들러: DR6에서 트리거된 브레이크포인트 확인 */
void my_bp_handler(struct perf_event *bp,
struct perf_sample_data *data,
struct pt_regs *regs)
{
pr_info("watchpoint hit at %pS\n", (void *)regs->ip);
/* DR6 비트: B0-B3 → 어떤 DR0-DR3가 트리거
* BD(13) → DR 접근 감지, BS(14) → 단일 스텝, BT(15) → 태스크 전환 */
}
ptrace(PTRACE_POKEUSER)로 디버그 레지스터를 설정합니다. watch 명령어는 소프트웨어 watchpoint(느림)를 사용하지만, rwatch/awatch는 DR0-DR3 하드웨어 watchpoint를 사용하여 성능 저하 없이 메모리 접근을 모니터링합니다. 단, 4개까지만 동시 설정 가능합니다.
MOV DR VM-Exit 인터셉트로 처리합니다.
참고 링크
- Intel 64 and IA-32 Architectures Software Developer Manuals (Intel SDM) — x86_64 전체 명령어 인코딩, 오퍼랜드, 동작을 정의하는 공식 레퍼런스입니다
- AMD64 Architecture Programmer's Manual — AMD64 명령어셋 확장 및 호환성 정보를 제공합니다
- 커널 소스: arch/x86/include/asm/asm.h — 커널 어셈블리 매크로 및 인라인 어셈블리 헬퍼 정의입니다
- 커널 소스: arch/x86/lib/atomic64_cx8_32.S — CMPXCHG8B 기반 원자적 연산 어셈블리 구현입니다
- 커널 소스: arch/x86/entry/entry_64.S — SYSCALL/SYSRET 진입점, 인터럽트 핸들러 어셈블리입니다
- 커널 소스: arch/x86/include/asm/special_insns.h — CR/DR 레지스터 접근, WBINVD 등 특권 명령어 래퍼입니다
- 커널 문서: x86_64 Memory Map (Documentation/arch/x86/x86_64/mm.rst) — x86_64 가상 메모리 레이아웃 문서입니다
- 커널 문서: Kernel entries on x86-64 (Documentation/arch/x86/entry_64.rst) — 시스템 콜, 인터럽트 진입 경로 설명입니다
- LWN.net: Software alternatives for x86 instructions — 커널 alternatives 메커니즘으로 런타임 명령어 패칭하는 방법입니다
- LWN.net: Linear Address Masking (LAM) — x86_64 주소 마스킹 확장 명령어 지원 논의입니다
- 커널 소스: arch/x86/include/asm/barrier.h — MFENCE/LFENCE/SFENCE 메모리 배리어 매크로 정의입니다
- 커널 소스: arch/x86/include/asm/percpu.h — GS 세그먼트 기반 per-CPU 변수 접근 명령어 구현입니다
- OSDev Wiki: X86-64 Instruction Encoding — REX/VEX/EVEX 접두사와 명령어 인코딩 형식 참고 자료입니다
관련 문서
- 어셈블리 종합 — GCC 인라인 어셈블리, AT&T/Intel 문법, 호출 규약
- SIMD 명령과 커널 개발 — 커널 공간 SIMD 사용, FPU 컨텍스트
- CPUID 명령어 — CPU 기능 탐지, Leaf 구조
- GNU Assembler (as) — GAS 지시자, 섹션, 매크로
- ARM64 명령어셋 (ISA) — ARM64 RISC 아키텍처 비교
- RISC-V 명령어셋 (ISA) — RISC-V 모듈형 ISA
- MIPS 명령어셋 (ISA) — MIPS 전통 RISC