커널 아키텍처 (Kernel Architecture)
리눅스 커널 아키텍처 심층 분석. x86_64, ARM64, RISC-V 아키텍처별 부팅 과정, 주소 공간, 특권 레벨, 커널 소스 트리 구조를 다룹니다.
핵심 요약
- 단계 분리 — 펌웨어, 부트로더, 커널 초기화 경계를 구분합니다.
- 하드웨어 기술 — ACPI/DT 등 기술 정보가 어디서 소비되는지 확인합니다.
- 신뢰 체인 — Secure Boot 등 검증 체인을 흐름으로 이해합니다.
- 실패 지점 — 부팅 로그에서 단계별 실패 단서를 빠르게 찾습니다.
- 호환성 관점 — 플랫폼 차이에 따른 초기화 분기를 함께 점검합니다.
단계별 이해
- 부팅 단계 식별
현재 이슈가 어느 단계에서 발생하는지 먼저 고정합니다. - 입력 데이터 확인
펌웨어/테이블/이미지 메타데이터를 점검합니다. - 전환 경계 검증
단계 간 인자 전달과 상태 인계를 추적합니다. - 플랫폼별 재검증
다른 하드웨어 조건에서도 동일하게 동작하는지 확인합니다.
x86_64, ARM64, RISC-V 아키텍처별 커널 구조, 부팅 과정, 주소 공간 레이아웃을 상세히 다룹니다.
리눅스 커널 개요 (Linux Kernel Overview)
리눅스 커널은 1991년 Linus Torvalds가 처음 공개한 이래, 현재 세계에서 가장 널리 사용되는 운영체제 커널입니다. 서버, 데스크탑, 임베디드 기기, 스마트폰(Android), 슈퍼컴퓨터에 이르기까지 거의 모든 컴퓨팅 영역에서 동작합니다. 커널은 하드웨어와 사용자 공간(user space) 사이에서 추상화 계층 역할을 하며, 다음과 같은 핵심 기능을 담당합니다:
- 프로세스 관리 (Process Management) - 프로세스 생성, 스케줄링, 종료, 시그널 처리
- 메모리 관리 (Memory Management) - 가상 메모리, 페이지 테이블, 물리 메모리 할당
- 파일시스템 (Filesystem) - VFS를 통한 다양한 파일시스템 지원
- 디바이스 드라이버 (Device Drivers) - 하드웨어 추상화 및 제어
- 네트워킹 (Networking) - TCP/IP 스택, 소켓, 패킷 필터링
- 보안 (Security) - LSM, SELinux, capabilities, seccomp
모놀리식 vs 마이크로커널 (Monolithic vs Microkernel)
운영체제 커널 설계에는 크게 두 가지 접근 방식이 있습니다. 모놀리식 커널(Monolithic Kernel)은 모든 핵심 서비스(프로세스 관리, 메모리 관리, 파일시스템, 드라이버 등)가 하나의 커다란 커널 이미지 안에서 동일한 주소 공간(커널 공간)에서 실행됩니다. 반면 마이크로커널(Microkernel)은 최소한의 기능만 커널에 포함하고 나머지는 사용자 공간 서버로 분리합니다.
리눅스는 모놀리식 커널입니다. 그러나 순수한 모놀리식이 아닌, 동적으로 적재 가능한 커널 모듈(Loadable Kernel Module, LKM)을 지원하여 모듈화의 유연성을 확보합니다. 이를 "모듈형 모놀리식(Modular Monolithic)" 커널이라고도 합니다. 커널 모듈에 대한 자세한 내용은 커널 모듈 (Kernel Module) 문서를 참고하세요.
x86_64 아키텍처 (x86_64 Architecture)
x86_64(또는 AMD64, Intel 64)는 데스크탑과 서버 환경에서 가장 널리 사용되는 아키텍처입니다. x86의 32비트 아키텍처를 64비트로 확장한 것으로, 리눅스 커널에서 가장 오랫동안 지원해 온 아키텍처 중 하나입니다.
부팅 과정 (Boot Process)
x86_64 시스템의 부팅 과정은 펌웨어에서 시작하여 커널이 완전히 초기화될 때까지 여러 단계를 거칩니다. 현대 시스템에서는 UEFI가 표준이지만, 레거시 BIOS도 여전히 지원됩니다.
CONFIG_EFI_STUB=y 설정이 필요합니다.
UEFI에 대한 자세한 내용은 UEFI 문서, 부팅 과정 전반은
부팅 과정 (Boot Process) 문서를 참고하세요.
커널의 x86_64 엔트리 포인트는 어셈블리로 작성되어 있습니다. 다음은 핵심 부팅 코드의 간략화된 예시입니다:
/* arch/x86/kernel/head_64.S - x86_64 커널 엔트리 포인트 (간략화) */
SYM_CODE_START_NOALIGN(startup_64)
/* 세그먼트 레지스터 초기화 */
xorl %eax, %eax
movl %eax, %ds
movl %eax, %es
movl %eax, %ss
movl %eax, %fs
movl %eax, %gs
/* 초기 페이지 테이블 설정 (Identity mapping) */
leaq early_top_pgt(%rip), %rax
movq %rax, %cr3
/* 스택 포인터 설정 */
leaq init_thread_union+THREAD_SIZE(%rip), %rsp
/* C 코드로 점프 */
call x86_64_start_kernel
SYM_CODE_END(startup_64)
주소 공간 레이아웃 (Address Space Layout)
x86_64에서는 48비트 가상 주소 공간(256TB)을 사용합니다. 이 공간은 사용자 영역(하위)과 커널 영역(상위)으로 명확하게 분리됩니다. 최신 커널에서는 5-level paging을 통해 57비트(128PB) 주소 공간도 지원합니다. 다음 다이어그램은 4-level paging 기준 주소 공간 레이아웃을 보여줍니다:
아키텍처별 주소 변환 경로 비교
가상 주소를 물리 주소로 변환하는 핵심 경로는 세 아키텍처 모두 유사하지만, 제어 레지스터와 페이지 테이블 포맷이 다릅니다. 아래 다이어그램은 x86_64, ARM64, RISC-V의 변환 경로를 한 화면에서 비교합니다.
세그먼테이션과 페이징 (Segmentation & Paging)
x86_64에서 세그먼테이션은 사실상 flat model로 사용됩니다. 모든 세그먼트의 베이스가 0이고
리미트가 최대값으로 설정되어, 세그먼테이션은 사실상 비활성화된 상태입니다. 그러나 GDT(Global Descriptor Table)는
여전히 존재하며, 커널/사용자 모드 전환과 TSS(Task State Segment)를 위해 필수적입니다.
페이징은 4단계 페이지 테이블을 사용합니다 (5단계 페이징은 CONFIG_X86_5LEVEL=y로 활성화):
- PGD (Page Global Directory) - 512 엔트리, CR3가 가리킴
- PUD (Page Upper Directory) - 512 엔트리
- PMD (Page Middle Directory) - 512 엔트리
- PTE (Page Table Entry) - 512 엔트리, 최종 물리 페이지 매핑
링 구조 (Protection Rings)
x86_64는 4개의 특권 레벨(Ring 0 ~ Ring 3)을 제공하지만, 리눅스에서는 실제로 Ring 0(커널 모드)과 Ring 3(사용자 모드) 두 레벨만 사용합니다. Ring 1과 Ring 2는 사용되지 않으며, 가상화 확장(VT-x)에서는 Ring -1(VMX root mode)이라는 개념이 추가됩니다.
/* arch/x86/include/asm/segment.h - GDT 세그먼트 정의 */
/*
* x86_64 GDT 레이아웃 (간략화):
* Entry 0: NULL 디스크립터 (CPU 요구사항)
* Entry 1: Kernel 32-bit Code Segment (호환 모드용)
* Entry 2: Kernel 64-bit Code Segment
* Entry 3: Kernel Data Segment
* Entry 4: User 32-bit Code Segment (ia32 compat)
* Entry 5: User Data Segment
* Entry 6: User 64-bit Code Segment
* Entry 7+: TSS, TLS 등
*/
#define GDT_ENTRY_KERNEL32_CS 1
#define GDT_ENTRY_KERNEL_CS 2
#define GDT_ENTRY_KERNEL_DS 3
#define GDT_ENTRY_DEFAULT_USER32_CS 4
#define GDT_ENTRY_DEFAULT_USER_DS 5
#define GDT_ENTRY_DEFAULT_USER_CS 6
/* 세그먼트 셀렉터 값 (index << 3 | RPL) */
#define __KERNEL32_CS (GDT_ENTRY_KERNEL32_CS * 8) /* 0x08, Ring 0 */
#define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8) /* 0x10, Ring 0 */
#define __KERNEL_DS (GDT_ENTRY_KERNEL_DS * 8) /* 0x18, Ring 0 */
#define __USER32_CS (GDT_ENTRY_DEFAULT_USER32_CS * 8 + 3) /* 0x23, Ring 3 */
#define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3) /* 0x2B, Ring 3 */
#define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3) /* 0x33, Ring 3 */
SYSCALL 명령어를 사용합니다.
이 명령어는 Ring 3에서 Ring 0으로 전환하며, MSR_LSTAR 레지스터에 저장된 시스템 콜 핸들러 주소로 점프합니다.
SYSRET으로 사용자 모드로 복귀합니다. 이전의 int 0x80 방식보다 훨씬 빠릅니다.
시스템 콜에 대한 자세한 내용은 시스템 콜 (System Call) 문서를 참고하세요.
ARM64 아키텍처 (ARM64 / AArch64 Architecture)
ARM64(AArch64)는 모바일 기기부터 서버, 슈퍼컴퓨터까지 빠르게 확산되고 있는 아키텍처입니다. Apple Silicon(M1/M2/M3), AWS Graviton, Ampere Altra 등 고성능 ARM64 프로세서가 서버 시장에서도 중요한 위치를 차지하고 있습니다.
Exception Level (EL0 ~ EL3)
ARM64는 x86의 Ring 구조 대신 Exception Level(EL)이라는 4단계 특권 모델을 사용합니다. 각 EL은 명확한 역할을 가지며, 하위 EL에서 상위 EL로의 전환은 exception 발생 시에만 가능합니다.
- EL0 (User/Application) - 사용자 애플리케이션이 실행되는 레벨. 가장 낮은 특권.
- EL1 (OS Kernel) - 운영체제 커널이 실행되는 레벨. 리눅스 커널은 여기서 동작합니다.
- EL2 (Hypervisor) - 하이퍼바이저가 실행되는 레벨. KVM이 이 레벨을 사용합니다.
- EL3 (Secure Monitor) - ARM TrustZone의 Secure Monitor. 보안 세계와 일반 세계 전환을 관리합니다.
ARM64 부팅 과정 (Boot Process)
ARM64의 부팅 과정은 x86과 상당히 다릅니다. 대부분의 ARM64 시스템은 Device Tree를 사용하여 하드웨어 구성 정보를 커널에 전달합니다. 부팅 프로토콜은 커널 이미지의 시작점에 명시된 규약을 따릅니다.
MMIO와 Device Tree
ARM 시스템에서 하드웨어 레지스터에 접근하는 기본 방식은 MMIO(Memory-Mapped I/O)입니다.
x86의 Port I/O(in/out 명령어)와 달리, ARM은 메모리 주소에 하드웨어 레지스터를 매핑하고
일반 메모리 접근 명령어(LDR/STR)로 제어합니다.
Device Tree(DT)는 하드웨어 구성을 기술하는 데이터 구조입니다. DTS(Device Tree Source) 파일로 작성하고, DTC(Device Tree Compiler)로 DTB(Device Tree Blob) 바이너리로 컴파일합니다. 커널은 부팅 시 DTB를 파싱하여 하드웨어 정보를 인식합니다.
/* 간단한 Device Tree 예시 (arch/arm64/boot/dts/example.dts) */
/dts-v1/;
/ {
model = "Example ARM64 Board";
compatible = "vendor,example-board";
#address-cells = <2>;
#size-cells = <2>;
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x40000000>; /* 1GB @ 0x80000000 */
};
uart0: serial@9000000 {
compatible = "arm,pl011", "arm,primecell";
reg = <0x0 0x09000000 0x0 0x1000>; /* MMIO 영역 */
interrupts = <0 1 4>; /* GIC SPI #1, level */
clock-names = "uartclk", "apb_pclk";
};
gic: interrupt-controller@8000000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x0 0x08000000 0x0 0x10000>, /* GICD */
<0x0 0x080A0000 0x0 0xF60000>; /* GICR */
};
};
arch/arm64/boot/dts/ 디렉토리에 위치하며,
제조사별로 하위 디렉토리가 구성됩니다. make dtbs 명령으로 모든 DTB 파일을 빌드할 수 있습니다.
dtc -I dtb -O dts 명령으로 DTB를 다시 DTS로 역컴파일할 수도 있습니다.
Device Tree에 대한 자세한 내용은 Device Tree 문서를 참고하세요.
RISC-V 아키텍처 (RISC-V Architecture)
RISC-V는 UC Berkeley에서 설계한 오픈 소스 ISA(Instruction Set Architecture)로, 라이센스 비용 없이 누구나 자유롭게 구현할 수 있습니다. 리눅스 커널은 RISC-V를 공식적으로 지원하며, SiFive, StarFive 등의 칩에서 이미 리눅스가 동작합니다.
특권 모드 (Privilege Modes)
RISC-V는 3단계 특권 모드를 정의합니다. 각 모드는 CSR(Control and Status Register) 접근 권한이 다릅니다.
- Machine Mode (M-mode) - 가장 높은 특권. 하드웨어에 직접 접근 가능. SBI 구현(OpenSBI)이 여기서 동작합니다.
- Supervisor Mode (S-mode) - 리눅스 커널이 실행되는 모드. 페이지 테이블과 인터럽트를 관리합니다.
- User Mode (U-mode) - 사용자 애플리케이션 모드. 가장 낮은 특권.
SBI (Supervisor Binary Interface)
SBI는 S-mode(커널)와 M-mode(펌웨어) 사이의 표준 인터페이스입니다. 리눅스 커널은 SBI 호출을 통해 타이머 설정, 프로세서 간 인터럽트(IPI) 전송, 콘솔 출력 등의 기계 수준 작업을 수행합니다. OpenSBI가 가장 널리 사용되는 SBI 구현체입니다.
/* arch/riscv/include/asm/sbi.h - SBI 호출 인터페이스 (간략화) */
/* SBI Extension IDs */
#define SBI_EXT_TIME 0x54494D45 /* "TIME" */
#define SBI_EXT_IPI 0x735049 /* "sPI" */
#define SBI_EXT_RFENCE 0x52464E43 /* "RFNC" */
#define SBI_EXT_HSM 0x48534D /* "HSM" */
/* SBI 호출 수행 (ecall 명령어 사용) */
struct sbiret {
long error;
long value;
};
static inline struct sbiret sbi_ecall(int ext, int fid,
unsigned long arg0, unsigned long arg1,
unsigned long arg2, unsigned long arg3)
{
struct sbiret ret;
register unsigned long a0 asm("a0") = arg0;
register unsigned long a1 asm("a1") = arg1;
register unsigned long a6 asm("a6") = fid;
register unsigned long a7 asm("a7") = ext;
asm volatile("ecall"
: "+r"(a0), "+r"(a1)
: "r"(a6), "r"(a7)
: "memory");
ret.error = a0;
ret.value = a1;
return ret;
}
RISC-V 부팅 과정 (Boot Process)
RISC-V의 부팅 과정은 ARM64와 유사하게 여러 펌웨어 단계를 거칩니다. 현재 가장 일반적인 조합은 ZSBL → OpenSBI → U-Boot → Linux입니다.
특권 레벨 비교 (Privilege Level Comparison)
세 아키텍처의 특권 레벨 구조를 비교하면 설계 철학의 차이를 명확히 알 수 있습니다. 다음 다이어그램은 x86_64, ARM64, RISC-V의 특권 레벨을 나란히 비교합니다:
위 다이어그램에서 주목할 점은 리눅스 커널이 각 아키텍처에서 다른 특권 레벨에서 동작한다는 것입니다:
- x86_64: Ring 0에서 동작. Ring -1(VMX)은 하이퍼바이저 전용.
- ARM64: EL1에서 동작. EL2는 하이퍼바이저, EL3는 보안 모니터.
- RISC-V: S-mode에서 동작. M-mode는 SBI 펌웨어가 담당.
CONFIG_ARM64_VHE=y가 활성화되면,
리눅스 커널이 EL2에서 직접 실행될 수 있습니다. 이를 통해 KVM 호스트 커널이 EL2에서 동작하고,
게스트 OS가 EL1에서 실행되어 가상화 전환 오버헤드가 크게 줄어듭니다.
시스템 콜/예외 진입 경로 비교
사용자 공간에서 커널 공간으로 전환되는 공통 경로는 시스템 콜, 인터럽트, 예외입니다. 아키텍처별 진입 명령과 복귀 명령은 다르지만, 커널이 트랩 프레임을 저장하고 핸들러를 호출한 뒤 사용자 공간으로 복귀한다는 큰 흐름은 동일합니다.
- 시스템 콜 진입 명령: x86_64는
SYSCALL, ARM64는SVC, RISC-V는ecall을 사용합니다. - 트랩 벡터: x86_64(IDT), ARM64(VBAR_EL1), RISC-V(stvec)가 첫 진입 지점을 결정합니다.
- 복귀 명령: x86_64(
SYSRET/IRETQ), ARM64(ERET), RISC-V(sret)로 복귀합니다.
start_kernel() - 커널 초기화 진입점 (Kernel Init Entry)
모든 아키텍처에서 아키텍처별 초기화가 완료되면 start_kernel() 함수가 호출됩니다.
이 함수는 init/main.c에 정의되어 있으며, 커널의 공통 초기화를 수행하는 핵심 함수입니다.
/* init/main.c - start_kernel() 함수 (핵심 흐름 간략화) */
asmlinkage __visible void __init start_kernel(void)
{
/* 아키텍처 의존적 초기화 (이전 단계에서 일부 수행) */
setup_arch(&command_line); /* 아키텍처별 설정 */
/* 부팅 초기 메모리 할당자 (memblock) */
setup_per_cpu_areas(); /* Per-CPU 영역 설정 */
/* 핵심 서브시스템 초기화 */
trap_init(); /* 예외/인터럽트 벡터 설정 */
mm_core_init(); /* 메모리 관리 초기화 */
sched_init(); /* 스케줄러 초기화 */
init_IRQ(); /* 인터럽트 컨트롤러 설정 */
time_init(); /* 타이머 초기화 */
console_init(); /* 콘솔 초기화 */
/* VFS 및 기타 서브시스템 */
vfs_caches_init(); /* VFS 캐시 초기화 */
signals_init(); /* 시그널 초기화 */
proc_root_init(); /* procfs 초기화 */
/* 나머지 초기화 - kernel_init 스레드 생성 */
arch_call_rest_init(); /* rest_init() → kernel_init() */
/*
* rest_init()에서:
* 1. kernel_init 커널 스레드 생성 (PID 1의 전신)
* 2. kthreadd 커널 스레드 생성 (PID 2)
* 3. 현재 스레드는 idle 스레드(PID 0)가 됨
*
* kernel_init()에서:
* 1. initcall 실행 (드라이버 초기화 등)
* 2. /sbin/init 또는 /init 실행 → PID 1 프로세스
*/
}
start_kernel()은 인터럽트가 비활성화된 상태에서 실행됩니다.
이 함수가 실행되는 동안에는 단일 CPU만 활성화되어 있으며, 나머지 CPU(Secondary CPU)는
smp_init()이 호출될 때까지 대기 상태입니다.
인터럽트에 대한 자세한 내용은 인터럽트 (Interrupt) 문서를,
스케줄러 초기화는 프로세스 스케줄러 (Process Scheduler) 문서를 참고하세요.
커널 소스 트리 구조 (Kernel Source Tree Structure)
리눅스 커널 소스 코드는 기능별로 잘 정리된 디렉토리 구조를 가지고 있습니다. 커널 개발을 시작할 때 이 구조를 이해하는 것이 매우 중요합니다.
drivers/ 디렉토리가 전체 소스의 약 60% 이상을 차지합니다.
커널의 핵심 로직은 kernel/, mm/, fs/, net/에 집중되어 있으며,
이 디렉토리들의 코드를 이해하면 커널의 핵심 동작 원리를 파악할 수 있습니다.
소스 코드 탐색에는 Bootlin Elixir Cross-referencer가
매우 유용합니다.
arch/ 디렉토리 상세 (Architecture Directory Detail)
arch/ 디렉토리 아래의 각 아키텍처 디렉토리는 비슷한 하위 구조를 가집니다.
이는 커널의 아키텍처 추상화 설계 원칙을 반영합니다:
arch/<arch>/kernel/- 프로세스 전환, 시스템 콜, SMP, 시그널 처리 등 핵심 아키텍처 코드arch/<arch>/mm/- 페이지 테이블 관리, TLB 관리, 캐시 관리 등 메모리 관련 코드arch/<arch>/include/asm/- 아키텍처별 헤더 파일 (인라인 어셈블리, 레지스터 정의 등)arch/<arch>/boot/- 부트 코드, 부트 이미지 생성 스크립트arch/<arch>/configs/- 기본 커널 설정 파일 (defconfig)arch/<arch>/Kconfig- 아키텍처별 Kconfig 옵션 정의
커널은 이러한 구조를 통해 아키텍처 독립적인 코드(kernel/, mm/ 등)와
아키텍처 의존적인 코드(arch/)를 깔끔하게 분리합니다. 새로운 아키텍처를 지원하려면
주로 arch/ 아래에 해당 아키텍처 디렉토리를 추가하면 됩니다.
빌드 설정 예시 (Build Configuration)
각 아키텍처의 기본 설정 파일(defconfig)로 빠르게 커널을 빌드할 수 있습니다:
# x86_64 기본 설정으로 커널 빌드
make x86_64_defconfig
make -j$(nproc)
# ARM64 크로스 컴파일
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
make defconfig
make -j$(nproc) Image dtbs
# RISC-V 크로스 컴파일
export ARCH=riscv
export CROSS_COMPILE=riscv64-linux-gnu-
make defconfig
make -j$(nproc)
# QEMU로 빌드된 커널 테스트 (x86_64)
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd /path/to/initramfs.cpio.gz \
-append "console=ttyS0" \
-nographic
요약 (Summary)
이 문서에서는 리눅스 커널이 지원하는 주요 세 아키텍처의 핵심 개념을 살펴보았습니다. 각 아키텍처의 특성을 다음 표로 정리합니다:
| 항목 | x86_64 | ARM64 | RISC-V |
|---|---|---|---|
| 특권 레벨 | Ring 0-3 (+ VMX) | EL0-EL3 | U/S/M mode |
| 커널 실행 레벨 | Ring 0 | EL1 (VHE: EL2) | S-mode |
| 시스템 콜 방식 | SYSCALL/SYSRET | SVC 명령어 | ECALL 명령어 |
| 부팅 펌웨어 | BIOS/UEFI | BootROM + TF-A | ZSBL + OpenSBI |
| HW 정보 전달 | ACPI/E820 | Device Tree / ACPI | Device Tree |
| 주소 공간 | 48/57-bit VA | 48/52-bit VA | Sv39/Sv48/Sv57 |
| 페이지 크기 | 4KB (기본) | 4KB / 16KB / 64KB | 4KB (기본) |
| I/O 방식 | Port I/O + MMIO | MMIO | MMIO |
| 인터럽트 컨트롤러 | APIC (LAPIC + I/O APIC) | GIC (v2/v3/v4) | PLIC / APLIC+IMSIC |
| 가상화 | VT-x / AMD-V | EL2 + VHE | H-extension |
아키텍처별 성능 최적화 (Architecture-Specific Performance Optimization)
각 아키텍처는 고유한 성능 특성과 최적화 포인트를 가지고 있습니다. 커널 개발자는 타겟 아키텍처의 특성을 이해하고 이에 맞는 최적화를 적용해야 합니다.
x86_64 최적화 포인트
| 최적화 영역 | 기법 | 설명 |
|---|---|---|
| 캐시 라인 | 64바이트 정렬 | 핫 패스 데이터는 __cacheline_aligned 사용하여 false sharing 방지 |
| 분기 예측 | likely/unlikely | likely(), unlikely() 매크로로 힌트 제공 |
| TLB | Huge Pages | 2MB/1GB huge pages 사용으로 TLB miss 감소 (CONFIG_HUGETLBFS) |
| NUMA | 로컬 메모리 할당 | numa_node_id()로 현재 노드 확인 후 로컬 메모리 할당 |
| SIMD | kernel_fpu_begin() | 커널에서 SSE/AVX 사용 시 kernel_fpu_begin/end()로 FPU 상태 보호 |
| 시스템 콜 | vDSO | gettimeofday() 등은 vDSO로 커널 진입 없이 처리 |
/* x86_64 최적화 예제: 캐시 라인 정렬 */
struct hot_data {
spinlock_t lock;
u64 counter;
void *ptr;
} __cacheline_aligned; /* 64바이트 경계에 정렬 */
/* 분기 예측 힌트 */
if (unlikely(error_condition)) {
handle_error();
return -EINVAL;
}
/* likely는 정상 경로에 사용 */
if (likely(ptr != NULL)) {
fast_path(ptr);
}
ARM64 최적화 포인트
| 최적화 영역 | 기법 | 설명 |
|---|---|---|
| 메모리 순서 | 약한 순서 모델 | 명시적 메모리 배리어 필요 (dmb, dsb, isb) |
| Exclusive 연산 | LDXR/STXR | Atomic 연산 시 Load-Exclusive/Store-Exclusive 페어 사용 |
| TLB | ASID | Address Space ID로 컨텍스트 스위치 시 TLB flush 최소화 |
| 캐시 | 64바이트 라인 | 대부분의 ARM64는 64바이트 캐시 라인 사용 |
| NEON | SIMD 최적화 | 암호화, 압축 등에서 NEON 명령어 활용 (커널 모드 사용 제한적) |
/* ARM64 메모리 배리어 예제 */
/* 데이터 동기화 배리어 */
WRITE_ONCE(data->value, new_val);
smp_wmb(); /* Write Memory Barrier */
WRITE_ONCE(data->ready, 1);
/* 읽기 측 */
if (READ_ONCE(data->ready)) {
smp_rmb(); /* Read Memory Barrier */
val = READ_ONCE(data->value);
}
RISC-V 최적화 포인트
| 최적화 영역 | 기법 | 설명 |
|---|---|---|
| 확장 기능 | ISA 확장 감지 | 런타임에 사용 가능한 확장 감지 후 최적화 경로 선택 |
| Atomic 연산 | AMO 명령어 | A 확장의 LR/SC, AMO 명령어로 락 프리 자료구조 구현 |
| 메모리 순서 | FENCE 명령 | 약한 메모리 모델, 명시적 FENCE 필요 |
| 벡터 연산 | RVV (Vector) | V 확장 사용 시 대량 데이터 처리 성능 향상 |
perf 도구를 사용하세요.
perf stat -e cycles,instructions,cache-misses,branch-misses로 주요 메트릭을 측정할 수 있습니다.
자세한 내용은 개발 도구 (Development Tools) 문서를 참고하세요.
실전 디버깅 팁 (Debugging Tips)
아키텍처별 커널 디버깅은 각 플랫폼의 특성을 이해하고 적절한 도구를 사용해야 효과적입니다.
공통 디버깅 기법
/* 커널 디버그 출력 */
pr_debug("Variable x = %d\\n", x); /* DEBUG 빌드에만 출력 */
pr_info("Info message\\n"); /* 정보성 메시지 */
pr_warn("Warning: %s\\n", msg); /* 경고 */
pr_err("Error: %d\\n", errno); /* 에러 */
/* WARN 및 BUG 매크로 */
WARN_ON(condition); /* 조건 만족 시 경고 + 백트레이스 */
WARN_ONCE(condition, "message"); /* 최초 1회만 경고 */
BUG_ON(critical_error); /* 치명적 에러 시 패닉 (사용 자제) */
/* 동적 디버그 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
pr_debug("Entry: func=%s\\n", __func__);
x86_64 특화 디버깅
| 도구/기법 | 용도 | 사용 예 |
|---|---|---|
| KGDB | GDB를 통한 원격 디버깅 | kgdboc=ttyS0,115200 kgdbwait 커널 파라미터 |
| Intel PT | 프로세서 트레이스 | perf record -e intel_pt// |
| MSR 읽기 | 모델 특정 레지스터 확인 | rdmsr 0x1a0 (IA32_MISC_ENABLE) |
| APIC 디버그 | 인터럽트 문제 추적 | /proc/interrupts, apic=debug |
ARM64 특화 디버깅
| 도구/기법 | 용도 | 사용 예 |
|---|---|---|
| JTAG | 하드웨어 디버거 | ARM DS-5, DSTREAM, OpenOCD |
| Device Tree 확인 | HW 구성 검증 | /proc/device-tree/, dtc -I fs /proc/device-tree |
| CoreSight | 온칩 디버그/트레이스 | ETM (Embedded Trace Macrocell) 활용 |
| earlycon | 초기 부팅 로그 | earlycon=pl011,0x09000000 |
RISC-V 특화 디버깅
| 도구/기법 | 용도 | 사용 예 |
|---|---|---|
| OpenOCD | JTAG 디버깅 | RISC-V 보드에 연결하여 GDB 원격 디버깅 |
| Spike 시뮬레이터 | ISA 시뮬레이터 | 하드웨어 없이 RISC-V 커널 테스트 |
| SBI 디버깅 | SBI 호출 추적 | CONFIG_RISCV_SBI_V01 로그 확인 |
CONFIG_DEBUG_* 옵션들을 비활성화하세요.
디버그 옵션은 성능에 상당한 영향을 미칩니다. 개발 및 테스트 환경에서만 활성화하는 것이 좋습니다.
자세한 내용은 디버깅 (Debugging) 문서를 참고하세요.
자주 묻는 질문 (FAQ)
Q1. 어떤 아키텍처를 선택해야 하나요?
A: 사용 목적에 따라 다릅니다:
- x86_64: 데스크탑, 서버, 클라우드 환경. 가장 성숙한 생태계와 도구 지원
- ARM64: 모바일, 임베디드, 저전력 서버 (AWS Graviton, Apple Silicon). 전력 효율 우수
- RISC-V: 연구, 교육, 새로운 HW 프로젝트. 오픈 ISA로 라이선스 비용 없음
Q2. 크로스 컴파일은 어떻게 하나요?
A: ARCH와 CROSS_COMPILE 변수를 설정합니다:
# ARM64 크로스 컴파일 (x86_64 호스트)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
# RISC-V 크로스 컴파일
make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig
make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j$(nproc)
자세한 내용은 빌드 시스템 (Build System) 문서를 참고하세요.
Q3. 32비트 커널도 여전히 사용되나요?
A: 예, 특정 임베디드 환경에서는 여전히 사용됩니다. ARM32 (AArch32), x86 (i386), MIPS32 등은 레거시 시스템이나 자원 제약 환경에서 사용되지만, 새로운 프로젝트는 대부분 64비트 아키텍처를 선택합니다. 주요 배포판들은 x86 32비트 지원을 점차 종료하고 있습니다.
Q4. ARM64의 big.LITTLE은 무엇인가요?
A: 고성능 코어(big)와 저전력 코어(LITTLE)를 조합한 이기종 멀티프로세싱(HMP) 아키텍처입니다.
리눅스 커널은 Energy Aware Scheduling (EAS)를 통해 워크로드에 따라 적절한 코어에 태스크를 할당합니다.
최신 ARM 시스템은 DynamIQ 기술을 사용하여 더 유연한 코어 조합을 지원합니다.
자세한 내용은 프로세스 스케줄러 (Process Scheduler) 문서를 참고하세요.
Q5. 페이지 테이블 레벨은 어떻게 결정되나요?
A: 가상 주소 공간 크기에 따라 결정됩니다:
- x86_64: 4-level (48-bit VA), 5-level (57-bit VA,
CONFIG_X86_5LEVEL) - ARM64: 3-level/4-level (VA/페이지 크기 설정에 따라 다름, 일반적으로 48-bit VA 구성 사용)
- RISC-V: Sv39 (3-level), Sv48 (4-level), Sv57 (5-level)
레벨이 많을수록 주소 공간은 넓어지지만 페이지 테이블 워킹 오버헤드가 증가합니다. 자세한 내용은 메모리 관리 (Memory Management) 및 메모리 관리 심화 (Advanced Memory Management) 문서를 참고하세요.
Q6. 아키텍처별 시스템 콜 차이는?
A: 시스템 콜 번호와 진입 메커니즘이 다릅니다:
- x86_64:
syscall명령어, 번호는arch/x86/entry/syscalls/syscall_64.tbl - ARM64:
svc #0명령어, 번호는include/uapi/asm-generic/unistd.h - RISC-V:
ecall명령어, 번호는 주로asm-generic/unistd.h를 사용해 ARM64와 많은 항목을 공유
모든 아키텍처는 공통 시스템 콜 인터페이스를 제공하지만, 일부 아키텍처별 시스템 콜이 존재할 수 있습니다. 자세한 내용은 시스템 콜 (System Call) 문서를 참고하세요.
Q7. 가상화 오버헤드는 얼마나 되나요?
A: 하드웨어 가상화 지원 여부에 따라 크게 다릅니다:
- Hardware-assisted (VT-x, AMD-V, ARM VHE, RISC-V H-ext): 2~10% 오버헤드 (워크로드 의존)
- Paravirtualization (Xen PV): 하드웨어 지원 없을 때 사용, 오버헤드 더 높음
- I/O 가상화: 네트워크, 디스크 I/O에서 오버헤드 큼 → virtio, SR-IOV로 완화
현대 시스템에서는 하드웨어 가상화 지원이 표준이므로 오버헤드가 크지 않습니다. 자세한 내용은 가상화 (KVM) 문서를 참고하세요.
CPU 제조사 아키텍처 매뉴얼 (Architecture Software Developer's Manuals)
리눅스 커널 개발에서 각 CPU 제조사의 공식 아키텍처 매뉴얼은 가장 권위 있고 정확한 참조 문서입니다. 커널 코드의 아키텍처 의존 부분(arch/ 디렉토리)을 이해하거나 수정할 때 반드시 해당 매뉴얼을 참조해야 합니다.
Intel® 64 and IA-32 Architectures Software Developer's Manual (Intel SDM)
| 볼륨 | 내용 | 커널 개발 활용 |
|---|---|---|
| Vol. 1 | 기본 아키텍처 | 데이터 타입, 실행 환경, 명령어 개요, x87 FPU, SSE/AVX |
| Vol. 2 (A-Z) | 명령어 세트 레퍼런스 | 개별 명령어의 opcode, 동작, 예외 조건 — 인라인 어셈블리 작성 시 필수 |
| Vol. 3 (A-D) | 시스템 프로그래밍 가이드 | 보호 모드, 페이징, 인터럽트/예외, MSR, APIC, VT-x — 커널 개발 핵심 볼륨 |
| Vol. 4 | MSR (Model-Specific Register) | CPU 모델별 MSR 목록, 성능 카운터, 전력 관리 레지스터 |
| Optimization Manual | 최적화 레퍼런스 | 마이크로아키텍처 세부사항, 분기 예측, 캐시 동작, SIMD 최적화 가이드 |
arch/x86/ 코드를 분석할 때 Vol. 3이 가장 자주 참조됩니다.
특히 페이지 테이블 구조(Chapter 4), 인터럽트/예외 처리(Chapter 6), APIC(Chapter 10),
VT-x(Chapter 23-33)는 커널 개발자의 필독 장입니다.
Intel SDM은 5,000페이지 이상이므로 전체를 읽기보다 필요한 챕터를 색인으로 찾아 참조하는 방식이 효율적입니다.
AMD64 Architecture Programmer's Manual (AMD APM)
| 볼륨 | 내용 | 커널 개발 활용 |
|---|---|---|
| Vol. 1 | 애플리케이션 프로그래밍 | 레지스터, 데이터 타입, 명령어 개요 |
| Vol. 2 | 시스템 프로그래밍 | Long Mode, 페이징, 시스템 콜(SYSCALL/SYSRET), SMM, AMD-V(SVM) |
| Vol. 3 | 범용/SIMD 명령어 | x86-64 명령어 인코딩, SSE/AVX 상세 |
| Vol. 4 | 128/256-bit 미디어 명령어 | XOP, FMA4 등 AMD 전용 확장 |
| Vol. 5 | 64-bit 미디어/x87 명령어 | 레거시 FPU, 3DNow! 명령어 |
arch/x86/kvm/svm/ 디렉토리는
AMD APM Vol. 2의 SVM 챕터를 직접 구현한 것입니다.
또한 AMD의 SEV(Secure Encrypted Virtualization)와 SME(Secure Memory Encryption)는 AMD 고유 기능입니다.
ARM Architecture Reference Manual (ARM ARM)
| 문서 | 내용 | 커널 개발 활용 |
|---|---|---|
| ARMv8-A ARM (DDI 0487) | AArch64/AArch32 ISA 레퍼런스 | A64 명령어, 시스템 레지스터, 예외 모델, MMU, GIC 인터페이스 |
| ARMv9-A ARM | ARMv9 확장 포함 | SVE2, MTE(Memory Tagging), RME(Realm Management), CCA |
| ARM Cortex-A TRM | 코어별 Technical Reference Manual | 캐시 구조, TLB, 분기 예측기, 구현 정의(IMPLEMENTATION DEFINED) 동작 |
| GIC Architecture Spec | Generic Interrupt Controller | GICv2/v3/v4 인터럽트 분배, LPI, ITS — drivers/irqchip/irq-gic-* |
| SMMU Architecture Spec | System MMU (IOMMU) | DMA 주소 변환, 디바이스 격리 — drivers/iommu/arm/arm-smmu-* |
| AMBA/AXI/ACE Spec | 버스 프로토콜 | 캐시 일관성(coherency), 배리어 동작, DMA 전송 특성 |
RISC-V Specifications
| 문서 | 내용 | 커널 개발 활용 |
|---|---|---|
| Unprivileged ISA (Volume I) | 기본 정수 ISA + 표준 확장 | RV64I, M/A/F/D/C 확장, 원자적 명령어(AMO), 벡터(V) 확장 |
| Privileged ISA (Volume II) | 특권 아키텍처 | M/S/U 모드, CSR, 페이지 테이블(Sv39/48/57), 인터럽트/예외, H 확장(가상화) |
| SBI Specification | Supervisor Binary Interface | OpenSBI와의 인터페이스, 타이머/IPI/리모트 fence 호출 |
| PLIC Specification | Platform-Level Interrupt Controller | 외부 인터럽트 라우팅, 우선순위 — drivers/irqchip/irq-sifive-plic.c |
| AIA Specification | Advanced Interrupt Architecture | APLIC + IMSIC, MSI 기반 인터럽트 — 차세대 RISC-V 인터럽트 체계 |
arch/riscv/ 하위의 구현이 스펙의 각 챕터와 직접 대응됩니다.
특히 Privileged ISA Vol. II의 페이지 테이블과 예외 처리 챕터는 커널 개발의 핵심 참조 문서입니다.
매뉴얼 효과적 활용법
| 상황 | 참조 문서 | 참조 섹션 |
|---|---|---|
| 페이지 테이블 워크 디버깅 | Intel SDM Vol. 3 Ch. 4 / ARM ARM D5 / RISC-V Priv. Ch. 4 | 페이지 테이블 엔트리 형식, 비트 필드 의미 |
| 인터럽트 핸들러 작성 | Intel SDM Vol. 3 Ch. 6, 10 / GIC Spec / PLIC Spec | IDT 구조, APIC 프로그래밍, EOI 처리 |
| 메모리 배리어 선택 | Intel SDM Vol. 3 Ch. 8 / ARM ARM B2 / RISC-V Unpriv. Ch. 14 | 메모리 순서 모델, fence/barrier 명령어 |
| KVM 가상화 구현 | Intel SDM Vol. 3 Ch. 23-33 / AMD APM Vol. 2 / ARM ARM D1 | VMCS/VMCB 구조, VM entry/exit, EPT/NPT, EL2 |
| 전력 관리 (cpuidle/cpufreq) | Intel SDM Vol. 3 Ch. 14 / ACPI Spec / ARM DEN0024A | C-state, P-state, MWAIT, WFI |
| 보안 기능 구현 | 각 제조사 보안 가이드 | Intel CET, ARM MTE/PAC/BTI, AMD SEV/SME |
- ACPI Specification — 전원 관리, 디바이스 열거, 테이블(DSDT/SSDT) — x86과 ARM64 서버 모두 사용
- UEFI Specification — EFI stub, Boot Services, Runtime Services
- PCI Express Base Specification — PCIe 구성 공간, MSI/MSI-X, AER, SR-IOV
- IOMMU Specification — Intel VT-d Spec / AMD IOMMU Spec / ARM SMMU Spec
- Devicetree Specification — ARM/RISC-V 하드웨어 기술, 바인딩 규칙
- 각 SoC 벤더 데이터시트 — Qualcomm, Samsung, Broadcom, SiFive 등 벤더별 구현 상세
x86 CPU 실행 모드 심화 (Operating Modes)
x86 프로세서는 리셋 후 Real Mode에서 시작하여 여러 모드를 거쳐 Long Mode에 도달합니다. 리눅스 커널 부팅 과정은 이 모드 전환을 순차적으로 수행하며, 각 모드의 특성을 이해하는 것은 부트로더 코드와 커널 초기화 코드를 분석하는 데 필수적입니다.
모드 전환 흐름
Real Mode (리얼 모드)
| 특성 | 설명 |
|---|---|
| 비트 폭 | 16비트 레지스터, 20비트 주소 (세그먼트:오프셋 = 세그먼트×16 + 오프셋) |
| 주소 공간 | 1MB (0x00000 ~ 0xFFFFF), A20 게이트로 확장 가능 |
| 보호 기능 | 없음 — 모든 코드가 전체 메모리/I/O 포트 접근 가능 |
| 인터럽트 | IVT(Interrupt Vector Table) at 0x0000 (256 × 4바이트) |
| 커널 사용 | 부트로더 초기 단계, BIOS 서비스 호출, A20 활성화 |
; arch/x86/boot/header.S — 리눅스 부팅 초기 (Real Mode)
; BIOS가 이 코드를 0x7C00에 로드하여 실행
; A20 게이트 활성화 (20번째 주소 라인)
; A20이 비활성이면 1MB 이상 주소에 접근 불가
in al, 0x92 ; Fast A20 (System Port)
or al, 2
out 0x92, al
; Protected Mode로 전환 준비
lgdt [gdt_ptr] ; GDT 로드
mov eax, cr0
or eax, 1 ; PE (Protection Enable) 비트 설정
mov cr0, eax ; → Protected Mode 진입!
jmp 0x08:pm_entry ; far jump로 CS 갱신 (GDT 셀렉터 0x08)
Protected Mode (보호 모드)
Protected Mode는 Intel 80286에서 도입되고 80386에서 완성된 32비트 실행 모드입니다. Real Mode의 한계(1MB 주소, 보호 없음)를 극복하여 4GB 주소 공간, 하드웨어 메모리 보호, 특권 레벨(Ring)을 제공합니다. 리눅스 32비트 커널(i386)은 이 모드에서 동작하며, 64비트 부팅에서도 Long Mode 진입 전 필수 경유 단계입니다.
| 특성 | 설명 |
|---|---|
| 비트 폭 | 32비트 레지스터 (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, EIP, EFLAGS) |
| 주소 공간 | 4GB 선형 주소 (PAE 시 물리 64GB), 세그먼테이션 + 페이징 2단계 변환 |
| 보호 기능 | Ring 0~3 특권 레벨, 세그먼트별 접근 권한, 페이지별 R/W + U/S 보호 |
| 핵심 자료구조 | GDT, LDT, IDT, TSS, 페이지 디렉토리/테이블 |
| 진입 조건 | GDT 설정 + CR0.PE=1 + far jump (CS 갱신) |
| 커널 사용 | 32비트 리눅스 커널 전체, 64비트 부팅의 중간 단계, UEFI CSM 호환 |
Protected Mode 진입 상세
Real Mode에서 Protected Mode로 전환하려면 다음 단계를 정확한 순서로 수행해야 합니다. 하나라도 빠지면 Triple Fault로 시스템이 리셋됩니다.
; Protected Mode 진입 전체 흐름 (arch/x86/boot/pmjump.S 기반)
; ① 인터럽트 비활성화 — 모드 전환 중 인터럽트 금지
cli
; ② A20 게이트 활성화 — 1MB 이상 메모리 접근 가능하게
in al, 0x92 ; Fast A20 (System Port)
or al, 2
out 0x92, al
; ③ GDT 로드 — 세그먼트 디스크립터 테이블 설정
lgdt [gdt_ptr] ; GDTR에 GDT base + limit 적재
; ④ CR0.PE = 1 — Protection Enable 비트 설정
mov eax, cr0
or eax, 1 ; PE 비트 (bit 0)
mov cr0, eax ; → 이 순간 Protected Mode 진입!
; ⑤ far jump — CS를 GDT 코드 세그먼트 셀렉터로 갱신
; 이것이 없으면 명령어 프리페치 큐에 Real Mode 코드가 남아 있음
jmp 0x08:pm_entry ; GDT[1] = 코드 세그먼트 (셀렉터 0x08)
; ⑥ 데이터 세그먼트 레지스터 재적재
pm_entry:
.code32
mov ax, 0x10 ; GDT[2] = 데이터 세그먼트 (셀렉터 0x10)
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, stack_top ; 32비트 스택 설정
세그먼테이션 (Segmentation)
Protected Mode의 핵심 메커니즘은 세그먼테이션입니다. 모든 메모리 접근은 세그먼트 셀렉터를 통해 GDT/LDT의 디스크립터를 참조하여 선형 주소(Linear Address)로 변환됩니다.
GDT 엔트리 구조
/* GDT 엔트리 구조 (Intel SDM Vol.3 Section 3.4.5) */
struct gdt_entry {
u16 limit_low; /* 세그먼트 크기 [15:0] (바이트 0-1) */
u16 base_low; /* 베이스 주소 [15:0] (바이트 2-3) */
u8 base_mid; /* 베이스 주소 [23:16] (바이트 4) */
u8 access; /* P | DPL(2) | S | Type(4) (바이트 5) */
u8 granularity; /* G | D/B | L | AVL | Limit[19:16] (바이트 6) */
u8 base_high; /* 베이스 주소 [31:24] (바이트 7) */
} __attribute__((packed));
/* Access 바이트 비트 분해:
* bit 7 : P (Present) — 1이면 세그먼트 유효, 0이면 #NP 예외
* bit 6-5 : DPL (Descriptor Privilege Level) — 00=Ring 0 ~ 11=Ring 3
* bit 4 : S (System) — 1=코드/데이터, 0=시스템(TSS, Call Gate 등)
* bit 3-0 : Type — S=1일 때:
* 코드: bit3=1, bit2=Conforming, bit1=Readable, bit0=Accessed
* 데이터: bit3=0, bit2=Expand-down, bit1=Writable, bit0=Accessed
*/
/* Granularity 바이트 비트 분해:
* bit 7 : G (Granularity) — 0=바이트, 1=4KB 단위 (Limit × 4KB)
* bit 6 : D/B (Default size) — 코드: 0=16bit, 1=32bit / 스택: 0=SP, 1=ESP
* bit 5 : L (Long mode) — 1=64bit 코드 (Long Mode 전용, D=0 필수)
* bit 4 : AVL (Available) — OS가 자유롭게 사용
* bit 3-0 : Limit [19:16] — Limit 상위 4비트
*/
Linux GDT 레이아웃
/* arch/x86/include/asm/segment.h — Linux GDT 레이아웃 */
/* 셀렉터 값 = (인덱스 << 3) | TI | RPL */
#define GDT_ENTRY_KERNEL32_CS 1 /* 0x08: Ring 0 32-bit 코드 */
#define GDT_ENTRY_KERNEL_CS 2 /* 0x10: Ring 0 64-bit 코드 */
#define GDT_ENTRY_KERNEL_DS 3 /* 0x18: Ring 0 데이터 */
#define GDT_ENTRY_DEFAULT_USER32_CS 4 /* 0x23: Ring 3 32-bit 코드 (RPL=3) */
#define GDT_ENTRY_DEFAULT_USER_DS 5 /* 0x2B: Ring 3 데이터 */
#define GDT_ENTRY_DEFAULT_USER_CS 6 /* 0x33: Ring 3 64-bit 코드 */
#define GDT_ENTRY_TSS 8 /* 0x40: TSS (16바이트, 2슬롯) */
#define GDT_ENTRY_LDT 10 /* 0x50: LDT (16바이트, 2슬롯) */
#define GDT_ENTRY_TLS_MIN 12 /* 0x60: TLS 엔트리 시작 */
#define GDT_ENTRY_PER_CPU 15 /* 0x78: per-CPU 데이터 */
/* Linux의 Flat 세그먼트 설정:
* 코드/데이터 세그먼트 모두 Base=0, Limit=0xFFFFF, G=1
* → 선형 주소 0x00000000 ~ 0xFFFFFFFF (4GB) 전체 커버
* → 세그먼테이션을 사실상 무력화하고 페이징에 보호를 위임 */
Protected Mode 페이징 (2-Level)
페이징이 활성화되면(CR0.PG=1) 선형 주소는 페이지 디렉토리(PD)와 페이지 테이블(PT) 2단계를 거쳐 물리 주소로 변환됩니다.
| PTE/PDE 비트 | 의미 | 커널 활용 |
|---|---|---|
| P (bit 0) | Present — 1이면 유효, 0이면 Page Fault (#PF) | 요구 페이징(demand paging), swap 감지 |
| R/W (bit 1) | Read/Write — 0이면 읽기 전용 | CoW(Copy-on-Write), 코드 페이지 보호 |
| U/S (bit 2) | User/Supervisor — 0이면 Ring 0만 접근 | 커널/유저 공간 분리 |
| A (bit 5) | Accessed — CPU가 접근 시 자동 설정 | LRU 페이지 교체 알고리즘 |
| D (bit 6) | Dirty — 쓰기 발생 시 자동 설정 | 페이지 writeback 결정 |
| PS (bit 7, PDE) | Page Size — 1이면 4MB(또는 2MB) 대형 페이지 | 커널 직접 매핑, 대용량 메모리 |
| G (bit 8) | Global — TLB flush 시 유지 | 커널 매핑 TLB 보존 (CR4.PGE 필요) |
Ring 전환 메커니즘
Protected Mode의 Ring 전환(유저 → 커널, 커널 → 유저)은 인터럽트/예외, SYSENTER/SYSEXIT, Call Gate를 통해 수행됩니다. Ring 전환 시 CPU는 자동으로 TSS에서 새 Ring의 스택(SS:ESP)을 로드합니다.
/* Ring 전환 시 CPU 자동 동작:
*
* Ring 3 → Ring 0 (인터럽트/예외):
* 1. TSS에서 Ring 0의 SS0:ESP0 로드
* 2. 이전 SS:ESP를 새 스택에 push
* 3. EFLAGS push
* 4. CS:EIP push (복귀 주소)
* 5. 에러 코드 push (해당 시)
* 6. IDT에서 새 CS:EIP 로드 → 핸들러 진입
*
* Ring 0 → Ring 3 (IRET):
* 1. 스택에서 EIP, CS, EFLAGS pop
* 2. CS의 RPL이 현재 CPL보다 높으면 (유저 복귀)
* 3. 스택에서 ESP, SS 추가 pop
* 4. Ring 3 코드 실행 재개
*/
/* SYSENTER/SYSEXIT (Pentium II+, 32비트):
* SYSENTER: Ring 3 → Ring 0 (MSR에서 CS/EIP/ESP 로드)
* IA32_SYSENTER_CS (0x174) → 커널 CS
* IA32_SYSENTER_ESP (0x175) → 커널 ESP
* IA32_SYSENTER_EIP (0x176) → 커널 진입점
* SYSEXIT: Ring 0 → Ring 3 (ECX=유저 ESP, EDX=유저 EIP)
*/
wrmsrl(MSR_IA32_SYSENTER_CS, __KERNEL_CS);
wrmsrl(MSR_IA32_SYSENTER_ESP, tss->x86_tss.sp0);
wrmsrl(MSR_IA32_SYSENTER_EIP,
(unsigned long)entry_SYSENTER_32);
Protected Mode의 TSS 역할: Protected Mode에서 TSS는 주로 Ring 전환 시 스택 포인터(SS0:ESP0, SS1:ESP1, SS2:ESP2)를 제공하고, I/O Permission Bitmap으로 유저 공간 I/O 포트 접근을 제어합니다. x86 하드웨어 태스크 스위칭(TR 셀렉터 변경)은 리눅스에서 사용하지 않으며, 소프트웨어 컨텍스트 스위칭만 사용합니다.
PAE (Physical Address Extension)
- PAE 모드: 32비트 CPU에서 4GB 이상 물리 메모리 접근 */
- CR4.PAE=1로 활성화 */
- 페이지 테이블 엔트리가 32비트 → 64비트로 확장 */
- 물리 주소: 36비트 → 최대 64GB */
- PAE 페이지 테이블 구조:
- PDPT (4 엔트리) → PD (512 엔트리) → PT (512 엔트리) → 4KB 페이지
- CR3 → PDPT (32바이트, 4 × 8바이트 엔트리)
- PAE는 NX(No-Execute) 비트의 전제 조건 */
- PTE bit 63 = NX: 해당 페이지의 코드 실행 금지 */
- → 스택/힙 실행 방지 (DEP/W^X) */
- Long Mode 전환에도 PAE 활성화 필수 (전제 조건) */
Long Mode (IA-32e Mode / 64-bit Mode)
Long Mode는 AMD64(Intel에서는 IA-32e Mode)로 명명된 64비트 실행 모드입니다. 64비트 범용 레지스터, 최대 256TB(48비트) 또는 128PB(57비트) 가상 주소 공간, 4-/5-level 페이징, SYSCALL/SYSRET 고속 시스템 콜, NX(No-Execute) 비트 등을 제공합니다. 현대 리눅스 커널(x86_64)은 이 모드에서 동작합니다.
| 특성 | 설명 |
|---|---|
| 비트 폭 | 64비트 GPR (RAX~R15), 64비트 RIP, 64비트 RFLAGS |
| 가상 주소 | 48비트 (4-level) = 256TB, 57비트 (5-level, LA57) = 128PB |
| 물리 주소 | 최대 52비트 = 4PB (실제 CPU에 따라 40~52비트) |
| 페이징 | 4-level (PML4→PDPT→PD→PT) 필수, 5-level (PML5) 선택 |
| 세그먼테이션 | 사실상 비활성 — CS/SS DPL만 유효, 나머지 base/limit 무시, FS/GS base만 MSR로 설정 |
| 시스템 콜 | SYSCALL/SYSRET (고속, MSR 기반), INT 0x80 호환 가능 |
| 보안 기능 | NX 비트, SMEP, SMAP, PKU, CET, PCID |
| 진입 전제 | Protected Mode + PAE + PML4 + EFER.LME=1 + CR0.PG=1 |
Long Mode 진입 상세
Protected Mode에서 Long Mode로의 전환은 정확한 순서가 필수입니다. 순서가 틀리면 #GP(General Protection Fault) 또는 Triple Fault가 발생합니다.
; arch/x86/boot/compressed/head_64.S — Long Mode 전환 (간략화)
.code32
startup_32:
; ① 페이징이 켜져 있으면 끔 (UEFI에서 올 때)
movl %cr0, %eax
andl $~X86_CR0_PG, %eax
movl %eax, %cr0
; ② PAE 활성화
movl %cr4, %eax
orl $X86_CR4_PAE, %eax
movl %eax, %cr4
; ③ Identity-mapped PML4 페이지 테이블 구성
; 가상주소 = 물리주소 (전환 직후 코드가 같은 주소에서 실행되도록)
leal pgtable(%ebx), %edi
xorl %eax, %eax
movl $(BOOT_INIT_PGT_SIZE/4), %ecx
rep stosl ; 0으로 초기화
; PML4[0] → PDPT
leal pgtable + 0x1007(%ebx), %eax
movl %eax, pgtable + 0(%ebx)
; PDPT[0] → PD (2MB 대형 페이지 사용)
leal pgtable + 0x2007(%ebx), %eax
movl %eax, pgtable + 0x1000(%ebx)
; ④ CR3에 PML4 물리 주소 적재
leal pgtable(%ebx), %eax
movl %eax, %cr3
; ⑤ EFER.LME = 1 (Long Mode Enable)
movl $MSR_EFER, %ecx
rdmsr
btsl $_EFER_LME, %eax ; bit 8
wrmsr
; ⑥ CR0.PG = 1 → Long Mode 활성화!
; 이 순간 EFER.LMA=1이 자동으로 설정됨
movl %cr0, %eax
orl $X86_CR0_PG, %eax
movl %eax, %cr0
; ⑦ far JMP → 64-bit 코드 세그먼트 (CS.L=1)
; 이 JMP는 CPU의 명령어 디코더를 64-bit 모드로 전환
ljmpl $__KERNEL_CS, $startup_64
; === 여기서부터 64-bit 코드 ===
.code64
startup_64:
; 64-bit 레지스터 사용 가능
xorq %rax, %rax ; 64-bit zero
movq %rax, %ds ; Long Mode에서 DS/ES/SS base=0 강제
movq %rax, %es
movq %rax, %ss
movq %rax, %fs
movq %rax, %gs
4-Level 페이징 (PML4)
Long Mode에서 페이징은 필수이며, 최소 4단계 페이지 테이블을 사용합니다. 각 테이블은 512개 엔트리(각 8바이트)로 구성되어 4KB를 차지합니다.
Protected Mode vs Long Mode 페이징 비교
| 항목 | Protected Mode (non-PAE) | Protected Mode (PAE) | Long Mode (4-level) | Long Mode (5-level) |
|---|---|---|---|---|
| 가상 주소 비트 | 32 | 32 | 48 | 57 |
| 물리 주소 비트 | 32 (4GB) | 36 (64GB) | 최대 52 (4PB) | 최대 52 (4PB) |
| PTE 크기 | 4바이트 | 8바이트 | 8바이트 | 8바이트 |
| 테이블 레벨 | 2 (PD→PT) | 3 (PDPT→PD→PT) | 4 (PML4→PDPT→PD→PT) | 5 (PML5→PML4→...) |
| 엔트리/테이블 | 1024 | 512 (PDPT: 4) | 512 | 512 |
| 기본 페이지 | 4KB | 4KB | 4KB | 4KB |
| 대형 페이지 | 4MB (PSE) | 2MB | 2MB, 1GB | 2MB, 1GB |
| NX 비트 | 없음 | 있음 (bit 63) | 있음 (bit 63) | 있음 (bit 63) |
| CR3 가리킴 | PD | PDPT | PML4 | PML5 |
레지스터 확장
Long Mode에서는 기존 8개 범용 레지스터가 64비트로 확장되고, R8~R15 8개 레지스터가 추가됩니다. REX 프리픽스가 이 확장 레지스터 접근을 가능하게 합니다.
| 32-bit (Protected) | 64-bit (Long) | 추가 레지스터 | 용도 (System V ABI) |
|---|---|---|---|
| EAX → RAX | RAX (64-bit) | R8 | 5번째 함수 인자 |
| EBX → RBX | RBX (callee-saved) | R9 | 6번째 함수 인자 |
| ECX → RCX | RCX (4번째 인자) | R10 | static chain (caller-saved) |
| EDX → RDX | RDX (3번째 인자) | R11 | SYSCALL RFLAGS 저장 |
| ESI → RSI | RSI (2번째 인자) | R12 | callee-saved |
| EDI → RDI | RDI (1번째 인자) | R13 | callee-saved |
| EBP → RBP | RBP (frame pointer) | R14 | callee-saved |
| ESP → RSP | RSP (stack pointer) | R15 | callee-saved |
| EIP → RIP | RIP (64-bit) | RIP-relative 주소 지정 모드 추가 | |
Long Mode에서의 세그먼테이션 변화
Long Mode는 세그먼테이션을 대부분 비활성화합니다. Protected Mode에서 핵심이었던 세그먼트 Base/Limit이 무시됩니다.
| 세그먼트 | Protected Mode | Long Mode (64-bit) |
|---|---|---|
| CS | Base + Limit + DPL + L/D 비트 모두 유효 | DPL, L, D 비트만 유효. Base/Limit 무시. L=1, D=0이어야 64-bit 모드 |
| SS | Base + Limit + DPL 유효 | DPL만 유효 (Ring 전환 시). Base=0, Limit 무시 |
| DS, ES | Base + Limit + 접근 권한 유효 | 완전 무시 — Base=0 강제, Limit 체크 없음 |
| FS, GS | Base + Limit + 접근 권한 유효 | Base만 유효 (MSR로 설정). FS: TLS, GS: per-CPU 데이터 |
/* Long Mode에서 FS/GS Base 설정 (MSR 사용) */
/* 커널 per-CPU 데이터 접근: GS Base */
wrmsrl(MSR_GS_BASE, per_cpu_offset(cpu));
/* 이후 %gs:offset 으로 per-CPU 변수 접근 */
/* 예: movq %gs:current_task, %rax → 현재 task_struct */
/* 유저 TLS(Thread-Local Storage): FS Base */
wrmsrl(MSR_FS_BASE, thread->fsbase);
/* glibc의 __thread 변수는 FS Base 기준으로 접근 */
/* SWAPGS: 시스템 콜 진입 시 GS 교체 */
/* SYSCALL → GS = 유저 값 → SWAPGS → GS = 커널 per-CPU */
/* SYSRET 전 → SWAPGS → GS = 유저 값 복원 */
Identity Mapping의 필요성: 모드 전환(Protected → Long) 직후, CPU는 다음 명령어를 기존 물리 주소에서 실행합니다. 그런데 페이징이 활성화되면 주소 해석이 달라지므로, 전환 코드가 위치한 물리 주소에 대해 가상주소 = 물리주소(identity mapping)를 설정해야 합니다. 이 매핑이 없으면 전환 직후 첫 명령어에서 Page Fault가 발생합니다. 리눅스 커널은 부팅 초기에 임시 identity mapping을 만들고, start_kernel() 이후에 제거합니다.
Long Mode 서브모드
| 서브모드 | CS.L | CS.D | 주소 크기 | 오퍼랜드 크기 | 용도 |
|---|---|---|---|---|---|
| 64-bit Mode | 1 | 0 | 64비트 (기본) | 32비트 (기본), REX.W로 64비트 | 커널 전체, 64비트 유저 프로세스 |
| Compatibility Mode | 0 | 1 | 32비트 | 32비트 | 32비트 유저 프로세스 (ia32 compat) |
| Compatibility Mode (16-bit) | 0 | 0 | 16비트 | 16비트 | 16비트 유저 코드 (vm86 대안, 매우 드묾) |
arch/x86/entry/entry_64_compat.S의
entry_SYSENTER_compat와 entry_SYSCALL_compat가 이 전환을 처리합니다.
Protected Mode vs Long Mode 종합 비교
| 항목 | Protected Mode (32-bit) | Long Mode (64-bit) |
|---|---|---|
| 범용 레지스터 | 8개 × 32비트 (EAX~ESP) | 16개 × 64비트 (RAX~R15) |
| 명령어 포인터 | EIP (32비트) | RIP (64비트), RIP-relative 주소 지정 |
| 가상 주소 공간 | 4GB | 256TB (48-bit) / 128PB (57-bit) |
| 세그먼테이션 | 완전 활성 (Base+Limit+Access) | 사실상 비활성 (FS/GS base만 MSR) |
| 페이징 | 선택 (2-level 또는 PAE 3-level) | 필수 (4-level 또는 5-level) |
| NX 비트 | PAE에서만 가능 | 기본 지원 (EFER.NXE) |
| 시스템 콜 | INT 0x80, SYSENTER/SYSEXIT | SYSCALL/SYSRET (고속, MSR 기반) |
| TSS | 스택 포인터 + I/O bitmap | IST(Interrupt Stack Table) 7개 + I/O bitmap |
| IDT 게이트 | 8바이트 | 16바이트 (64-bit 오프셋, IST 인덱스 추가) |
| GDT 시스템 디스크립터 | 8바이트 | 16바이트 (TSS/LDT는 64-bit base 필요) |
| 함수 호출 규약 | cdecl (스택 기반 인자 전달) | System V AMD64 ABI (레지스터 6개 → 스택) |
| Red Zone | 없음 | RSP 아래 128바이트 (리프 함수 최적화) |
CPU 모드 전환 주의사항
- GDT/IDT 준비 — Protected/Long Mode 전환 전에 반드시 유효한 GDT를 설정. 잘못된 GDT는 Triple Fault → 리셋
- A20 게이트 — Real→Protected 전환 전 A20 활성화 필수. 비활성 시 홀수 MB 주소에 접근 불가
- Identity Mapping — 모드 전환 직후 코드가 실행되는 주소에 대해 가상=물리 매핑(identity mapping)이 있어야 함. 없으면 즉시 페이지 폴트
- PAE 선행 — Long Mode 진입에 PAE 필수. PAE 없이 LME 설정 후 PG 활성화하면 #GP
- CR3 유효성 — Long Mode에서 CR3는 PML4/PML5 테이블의 물리 주소. 잘못된 값은 즉시 크래시
- 5-level 페이징 (LA57) — Intel Ice Lake+에서 CR4.LA57=1로 PML5 활성화 시 57비트 가상 주소 (128PB). 커널
CONFIG_X86_5LEVEL필요 - UEFI 부팅 — UEFI는 이미 Protected/Long Mode로 진입한 상태에서 커널을 호출. EFI stub은 모드 전환 없이 직접 커널 초기화 진행
제어 레지스터(CR) 요약
| 레지스터 | 주요 비트 | 기능 |
|---|---|---|
| CR0 | PE, PG, WP, NE, MP, TS | 보호모드(PE), 페이징(PG), 쓰기 보호(WP), FPU 상태(TS/MP) |
| CR2 | (전체) | Page Fault 발생 시 폴트 주소 저장 |
| CR3 | PCD, PWT, PCID | 페이지 테이블 베이스(PML4/PML5), PCID로 TLB 태깅 |
| CR4 | PAE, PSE, PGE, OSFXSR, OSXSAVE, LA57, PCIDE, SMEP, SMAP, PKE | PAE, 큰 페이지(PSE), 전역 페이지(PGE), SIMD(OSFXSR), 보안(SMEP/SMAP) |
| CR8 (TPR) | [3:0] | Task Priority Register — 인터럽트 우선순위 마스킹 (Long Mode 전용) |
| EFER (MSR) | LME, LMA, SCE, NXE | Long Mode 활성화(LME/LMA), SYSCALL(SCE), NX 비트(NXE) |
Descriptor Table 레지스터 구조
GDTR/IDTR 레지스터는 GDT와 IDT의 위치를 CPU에 알려주는 특수 레지스터입니다.
LGDT/LIDT 명령어로 메모리의 의사 디스크립터(Pseudo-Descriptor)를 읽어 적재하며,
Protected Mode에서는 48비트(6바이트), Long Mode에서는 80비트(10바이트) 구조를 사용합니다.
| 레지스터 | 유형 | 내용 | 적재 명령어 | 저장 명령어 |
|---|---|---|---|---|
GDTR |
시스템 레지스터 | GDT Base + Limit (6/10바이트) | LGDT |
SGDT |
IDTR |
시스템 레지스터 | IDT Base + Limit (6/10바이트) | LIDT |
SIDT |
LDTR |
세그먼트 레지스터 | LDT를 가리키는 GDT 셀렉터 + 캐시된 디스크립터 | LLDT |
SLDT |
TR |
세그먼트 레지스터 | TSS를 가리키는 GDT 셀렉터 + 캐시된 디스크립터 | LTR |
STR |
리눅스 커널은 arch/x86/include/asm/desc.h에서 GDT/IDT 적재를 위한
인라인 함수를 제공합니다:
/* arch/x86/include/asm/desc.h */
/* LGDT/LIDT 명령어가 받는 6/10바이트 메모리 구조 */
struct desc_ptr {
u16 size; /* 테이블 바이트 크기 - 1 (Limit) */
u64 address; /* 테이블 선형 주소 (Base) */
} __attribute__((packed));
static __always_inline void native_load_gdt(const struct desc_ptr *dtr)
{
asm volatile("lgdt %0" : : "m"(*dtr));
}
static __always_inline void native_load_idt(const struct desc_ptr *dtr)
{
asm volatile("lidt %0" : : "m"(*dtr));
}
/* 사용 예시: 부팅 시 GDT 설정 */
/* arch/x86/kernel/head64.c */
struct desc_ptr gdt_descr = {
.size = GDT_SIZE - 1,
.address = (unsigned long)get_cpu_gdt_rw(0),
};
load_gdt(&gdt_descr);
LGDT/LIDT는 메모리 피연산자 하나만 받으며,
Protected Mode에서는 6바이트(16+32비트), Long Mode에서는 10바이트(16+64비트) 구조를 읽습니다.
리눅스의 struct desc_ptr는 __attribute__((packed))으로 패딩을 제거하여
이 요구사항을 충족합니다.
LGDT 실행 직후 CS를 제외한 세그먼트 레지스터는 명시적으로 재적재해야 합니다.
세그먼트 디스크립터 비트 필드 상세
GDT/LDT의 각 엔트리는 8바이트(64비트) 세그먼트 디스크립터입니다. 비트 배치가 연속적이지 않아 소프트웨어에서 직접 조립할 때 주의가 필요합니다. 인텔 SDM Vol.3 Section 3.4.5의 "Segment Descriptor"를 기준으로 설명합니다.
| Type 값 | 세그먼트 종류 | 접근 속성 |
|---|---|---|
0000 | 데이터 | 읽기 전용 |
0010 | 데이터 | 읽기/쓰기 |
0100 | 데이터 | 읽기 전용, Expand-Down |
0110 | 데이터 | 읽기/쓰기, Expand-Down |
1000 | 코드 | 실행 전용 |
1010 | 코드 | 실행/읽기 |
1100 | 코드 | 실행 전용, Conforming |
1110 | 코드 | 실행/읽기, Conforming |
/* GDT_ENTRY() 매크로 — arch/x86/include/asm/desc_defs.h */
/* base: 32비트 세그먼트 베이스, limit: 20비트 크기, flags: 접근·세분성 바이트 */
#define GDT_ENTRY(flags, base, limit) \
((((base) & _AC(0xff000000,ULL)) << (56-24)) | \
(((flags) & _AC(0x0000f0ff,ULL)) << 40) | \
(((limit) & _AC(0x000f0000,ULL)) << (48-16)) | \
(((base) & _AC(0x00ffffff,ULL)) << 16) | \
(((limit) & _AC(0x0000ffff,ULL))))
/* 사용 예: Ring 0 64-bit 코드 세그먼트 (CS=0x10, Long Mode) */
/* flags=0xa09b: G=1, L=1, P=1, DPL=0, S=1, Type=0xb (코드/실행/읽기) */
GDT_ENTRY(0xa09b, 0, 0xfffff)
L=1, D/B=0 조합이어야 합니다.
또한 Long Mode에서는 CS를 제외한 세그먼트(DS/ES/SS)의 Base와 Limit이 CPU에 의해 무시됩니다(플랫 메모리 모델).
단, FS/GS의 Base는 MSR(0xC0000100/0xC0000101)로 별도 설정하여 per-CPU 포인터나 TLS에 활용합니다.
시스템 세그먼트 디스크립터 (S=0)
GDT 디스크립터의 S(Descriptor Type) 비트가 0이면 시스템 디스크립터입니다.
코드/데이터 세그먼트(S=1)와 달리 TSS, LDT, 각종 Gate를 기술하며,
Long Mode에서 TSS와 LDT 디스크립터는 128비트(16바이트)로 확장되어
GDT에서 연속된 두 슬롯을 차지합니다.
| Type 값 (4비트, S=0) | 디스크립터 종류 | 설명 |
|---|---|---|
0x1 |
16비트 TSS (Available) | Protected Mode 16비트 태스크 상태 세그먼트 (사용 가능) |
0x2 |
LDT | 지역 디스크립터 테이블 (Long Mode: 128비트 확장) |
0x3 |
16비트 TSS (Busy) | Protected Mode 16비트 태스크 상태 세그먼트 (실행 중) |
0x9 |
64비트 TSS (Available) | Long Mode TSS (사용 가능) — GDT 2슬롯(128비트) |
0xB |
64비트 TSS (Busy) | Long Mode TSS (실행 중) — TR 레지스터가 가리키는 TSS |
0xC |
Call Gate | 특권 레벨 변경을 위한 원거리 호출 게이트 |
0xE |
Interrupt Gate | 인터럽트/예외 핸들러 진입점 (IF 자동 클리어) |
0xF |
Trap Gate | 트랩 핸들러 진입점 (IF 유지) |
리눅스 커널은 arch/x86/include/asm/desc_defs.h에서
시스템 디스크립터를 위한 구조체를 정의합니다:
/* arch/x86/include/asm/desc_defs.h */
/* Long Mode TSS/LDT 디스크립터: GDT에서 연속된 2슬롯(16바이트) 차지 */
struct ldttss_desc {
u16 limit0; /* Limit[15:0] */
u16 base0; /* Base[15:0] */
unsigned base1:8, /* Base[23:16] */
type:5, /* Type (S=0 포함된 5비트) */
dpl:2, /* 디스크립터 권한 레벨 */
p:1; /* Present 비트 */
unsigned limit1:4, /* Limit[19:16] */
zero0:3, /* Reserved = 0 */
g:1, /* Granularity (1=4KB 단위) */
base2:8; /* Base[31:24] */
u32 base3; /* Base[63:32] — 두 번째 QWord */
u32 zero1; /* Reserved = 0 */
} __attribute__((packed));
/* TSS 디스크립터 설정 예시 (arch/x86/kernel/cpu/common.c) */
set_tssldt_descriptor(&get_cpu_entry_area(cpu)->tss_desc,
(unsigned long)&get_cpu_entry_area(cpu)->tss.x86_tss,
DESC_TSS,
__KERNEL_TSS_LIMIT);
IDT 게이트 디스크립터 구조 상세
64비트 Long Mode의 IDT 엔트리는 16바이트(128비트)로, Protected Mode의 8바이트에서 확장되었습니다. 핸들러 오프셋이 64비트로 늘어났으며, IST(Interrupt Stack Table) 필드가 추가되어 중첩 예외 상황에서도 안전한 전용 스택을 사용할 수 있습니다.
| Gate Type | Type 필드 | IF 플래그 | 용도 |
|---|---|---|---|
| Interrupt Gate | 0xE (1110) | 자동 클리어 | 하드웨어 인터럽트, 재진입 방지 |
| Trap Gate | 0xF (1111) | 유지 | 소프트웨어 예외, 디버그 트랩 |
| IST 값 | 할당 스택 | 사용 벡터 (리눅스) |
|---|---|---|
0 | 일반 스택 (RSP0) | 일반 인터럽트/예외 |
1 | IST1 스택 | #DF Double Fault (벡터 8) |
2 | IST2 스택 | NMI (벡터 2) |
3 | IST3 스택 | #MC Machine Check (벡터 18) |
4 | IST4 스택 | #DB, #BP 디버그 관련 |
/* struct gate_struct — arch/x86/include/asm/desc_defs.h */
struct gate_struct {
u16 offset_low; /* 핸들러 주소 [15:0] */
u16 segment; /* 코드 세그먼트 셀렉터 (예: __KERNEL_CS = 0x10) */
struct idt_bits bits; /* IST[2:0] | 0[4:0] | Type[3:0] | 0 | DPL[1:0] | P */
u16 offset_middle; /* 핸들러 주소 [31:16] */
u32 offset_high; /* 핸들러 주소 [63:32] */
u32 reserved; /* 반드시 0 */
} __attribute__((packed));
/* 인터럽트 게이트 설정 예 (arch/x86/kernel/idt.c) */
set_intr_gate(X86_TRAP_DE, asm_exc_divide_error); /* #DE: DPL=0, IF 클리어 */
set_system_intr_gate(X86_TRAP_BP, asm_exc_int3); /* #BP: DPL=3, 사용자→int3 허용 */
int 0x80(시스템 콜)이나 int3(디버그)처럼 사용자 공간에서 소프트웨어 인터럽트를 발생시키려면
해당 IDT 게이트의 DPL=3이어야 합니다. DPL=0인 게이트에 사용자 공간이 접근하면
#GP(General Protection Fault)가 발생합니다.
현대 리눅스는 SYSCALL 명령어를 선호하므로 int 0x80은 호환성 모드(ia32_syscall)에서만 사용됩니다.
CPL/DPL/RPL 특권 레벨 메커니즘
x86은 4개의 특권 레벨(Ring 0~3)을 지원하며, 세그먼트 접근 시 CPU가 자동으로 세 가지 레벨을 비교합니다. 리눅스는 Ring 0(커널)과 Ring 3(사용자)만 사용하며, Ring 1/2는 활용하지 않습니다.
| 레벨 | 위치 | 의미 | 리눅스 값 |
|---|---|---|---|
| CPL | CS 셀렉터 [1:0] | 현재 실행 코드의 특권 수준 | 0 (커널) 또는 3 (유저) |
| DPL | GDT 디스크립터 [46:45] | 세그먼트 접근에 필요한 최소 특권 | 0 (커널 세그먼트), 3 (유저 세그먼트) |
| RPL | 세그먼트 셀렉터 [1:0] | 호출자가 명시한 접근 권한 | 셀렉터 값에 포함 (0x08→0, 0x2B→3) |
; Ring 3 → Ring 0 전환 시 CPU가 자동으로 수행하는 동작
; (예외/인터럽트 발생 시, CALL 게이트 사용 시)
;
; 1. CPL 변경 감지: 새 DPL < 현재 CPL → 스택 전환 수행
; 2. TSS.RSP0에서 Ring 0 커널 스택 포인터 로드
; 3. 현재 컨텍스트를 새 스택에 자동 푸시 (5개 항목):
; [+40]: SS (원래 Ring 3 스택 세그먼트)
; [+32]: RSP (원래 Ring 3 스택 포인터)
; [+24]: RFLAGS
; [+16]: CS (원래 Ring 3 코드 세그먼트)
; [ +8]: RIP (원래 복귀 주소)
; [ 0]: Error Code (예외인 경우에만 푸시)
;
; 복귀: IRET → 저장된 SS/RSP/RFLAGS/CS/RIP 복원
; CPL이 높아지면 자동으로 Ring 3 스택으로 복귀
SYSCALL 명령어는 세그먼트 기반 특권 검사 없이 직접 Ring 0으로 전환하며,
스택 교체를 자동으로 수행하지 않습니다.
따라서 커널 진입 직후 SWAPGS로 GS.base를 교체하여 current_task 등
per-CPU 데이터에 접근합니다.
IRET과 달리 SYSRET은 RFLAGS를 완전히 복원하지 않으므로
보안상 주의가 필요합니다.
SYSCALL/SYSRET 메커니즘 상세
현대 리눅스 커널은 시스템 콜에 INT 0x80/IRET 대신
SYSCALL/SYSRET 명령어를 사용합니다.
이 명령어들은 세그먼트 디스크립터 검사를 생략하고 MSR에서 직접 CS/SS 셀렉터와
진입점 주소를 읽어 Ring 3 ↔ Ring 0 전환을 수행합니다.
| MSR 주소 | 이름 | 내용 |
|---|---|---|
0xC0000081 |
STAR |
[63:48]: SYSRET CS/SS 셀렉터, [47:32]: SYSCALL CS/SS 셀렉터 기준값 |
0xC0000082 |
LSTAR |
64비트 모드 Ring 0 진입점 주소 (entry_SYSCALL_64) |
0xC0000083 |
CSTAR |
Compatibility Mode 진입점 (entry_SYSCALL_compat) |
0xC0000084 |
SFMASK |
SYSCALL 실행 시 RFLAGS에서 클리어할 비트 마스크 |
리눅스 커널의 syscall_init()에서 부팅 시 MSR을 설정합니다:
/* arch/x86/kernel/cpu/common.c */
void syscall_init(void)
{
/*
* STAR: [63:48] = SYSRET용 CS 기준 셀렉터
* [47:32] = SYSCALL용 CS 셀렉터 (__KERNEL_CS)
* SYSRET 시 CPU는 STAR[63:48]을 CS로, +8을 SS로 사용
*/
wrmsrl(MSR_STAR, ((u64)__USER32_CS) << 48 |
((u64)__KERNEL_CS) << 32);
/* LSTAR: 64비트 SYSCALL 진입점 */
wrmsrl(MSR_LSTAR, (u64)entry_SYSCALL_64);
#ifdef CONFIG_IA32_EMULATION
/* CSTAR: 32비트 Compatibility Mode SYSCALL 진입점 */
wrmsrl(MSR_CSTAR, (u64)entry_SYSCALL_compat);
#endif
/*
* SFMASK: SYSCALL 진입 시 RFLAGS에서 클리어할 비트
* TF(트레이스), DF(방향), IF(인터럽트 금지), NT(중첩 태스크),
* AC(정렬 확인), IOPL 등을 클리어하여 보안 강화
*/
wrmsrl(MSR_SYSCALL_MASK,
X86_EFLAGS_TF | X86_EFLAGS_DF | X86_EFLAGS_IF |
X86_EFLAGS_IOPL | X86_EFLAGS_AC | X86_EFLAGS_NT);
}
RCX에 non-canonical 주소(비트 [63:48]이 모두 동일하지 않은 주소)를 설정하고
SYSCALL을 호출한 뒤, 커널이 그대로 SYSRET을 실행하면
Intel CPU에서 #GP(General Protection Fault)가 커널 권한(Ring 0)으로 발생합니다.
이는 심각한 특권 상승 취약점으로 이어집니다.
리눅스는 entry_64.S에서 SYSRETQ 직전에 RCX canonical 여부를 확인하고,
이상 시 IRET 경로로 우회합니다.
자세한 내용은 커널 취약점을 참조하세요.
TSS (Task State Segment) 구조
Long Mode의 TSS는 하드웨어 멀티태스킹 지원 대신 스택 포인터 저장소로 사용됩니다. CPU는 예외/인터럽트 발생 시 TSS에서 스택 포인터를 가져옵니다. 리눅스는 CPU마다 하나의 TSS를 유지하며, 태스크 전환(context switch)마다 RSP0를 갱신합니다.
/* struct x86_hw_tss — arch/x86/include/asm/processor.h (단순화) */
struct x86_hw_tss {
u32 reserved1;
u64 sp0; /* RSP0: Ring 0 진입 시 커널 스택 최상단 */
u64 sp1; /* RSP1: Ring 1 (리눅스 미사용, 항상 0) */
u64 sp2; /* RSP2: Ring 2 (리눅스 미사용, 항상 0) */
u64 reserved2;
u64 ist[7]; /* IST1~7: NMI, #DF, #MC 등 전용 스택 포인터 */
u32 reserved3;
u32 reserved4;
u16 reserved5;
u16 io_bitmap_base; /* IOPB 오프셋 (0x68 = IOPB 없음) */
} __attribute__((packed));
/* RSP0 갱신 — arch/x86/kernel/process_64.c */
/* __switch_to() 호출 시마다 새 태스크의 커널 스택 최상단을 RSP0에 기록 */
/* this_cpu_write(cpu_tss_rw.x86_tss.sp0, task_top_of_stack(next_p)); */
RSP1/RSP2는 항상 0입니다.
RSP0만 실제로 활용되며, 태스크 전환(context switch)마다 새 태스크의
커널 스택 주소로 갱신됩니다.
IST 스택은 cpu_entry_area에 고정 할당되어, 커널 스택 오버플로우 상황에서도
안전하게 예외를 처리할 수 있습니다.
Long Mode 특화 기능 상세
Long Mode는 단순히 64비트 레지스터와 넓은 주소 공간만 제공하는 것이 아닙니다. Canonical 주소 규칙, REX 프리픽스, FS/GS Base MSR, RIP-relative 주소 지정 등 시스템 소프트웨어가 반드시 이해해야 할 고유 메커니즘을 포함합니다.
| MSR 주소 | 이름 | 용도 | 커널 접근 |
|---|---|---|---|
0xC0000100 | FS.Base | FS 세그먼트 베이스 (사용자 TLS) | arch_prctl(ARCH_SET_FS) |
0xC0000101 | GS.Base | GS 세그먼트 베이스 (커널 per-CPU) | wrmsr(MSR_GS_BASE, ...) |
0xC0000102 | KernelGS.Base | SWAPGS로 교체될 GS.Base 저장소 | SWAPGS 명령어 |
; arch/x86/entry/entry_64.S — 시스템 콜 진입 (Ring 3 → Ring 0)
; SYSCALL 실행 후: GS.base = 사용자 GS 값 (그대로 유지됨)
; per-CPU 데이터 접근을 위해 SWAPGS로 커널 GS.base를 복원해야 함
SYM_CODE_START(entry_SYSCALL_64):
swapgs ; GS.base ↔ KernelGS.base 교체
; 이후 %gs:offset = current_task 등 per-CPU 변수 접근 가능
; ... 커널 스택 설정, 레지스터 저장 ...
; Ring 0 → Ring 3 복귀
SYM_CODE_START(syscall_return_via_sysret):
swapgs ; 사용자 GS.base 복원 (KernelGS.base에 커널 값 보관)
sysretq ; Ring 3 복귀 (RCX→RIP, R11→RFLAGS)
; RIP-relative 주소 지정 — PIE/KASLR 호환
; Long Mode에서 [rip + disp32] 형식으로 현재 명령어 기준 ±2GB 내 접근
; 어셈블러가 자동으로 다음 명령어 주소와의 차이를 계산
lea rax, [rip + my_symbol] ; RIP-relative LEA: 심볼 주소를 위치 독립적으로 로드
mov rbx, [rip + global_var] ; 전역 변수 접근: 재배치 없이 동작
; 컴파일러: -fPIC 또는 -fPIE 플래그 → 자동으로 RIP-relative 코드 생성
; 커널 KASLR: 임의 주소에 로드되어도 내부 참조는 상대 오프셋으로 유효
| REX 비트 | 효과 | 예시 |
|---|---|---|
W (bit 3) | 오퍼랜드 크기를 64비트로 강제 | REX.W mov → 64비트 이동 |
R (bit 2) | ModRM.reg 필드에 4번째 비트 추가 | r8~r15 (reg 필드 인코딩) |
X (bit 1) | SIB.index 필드에 4번째 비트 추가 | SIB 인덱스 레지스터 r8~r15 |
B (bit 0) | ModRM.rm, SIB.base, opcode reg 확장 | r8~r15 (rm/base 인코딩) |
0x0000800000000000)에 접근하면 즉시 #GP(0)가 발생합니다.
이를 이용해 사용자 공간에서 의도적으로 #GP를 유발하면, 잘못된 SWAPGS
타이밍과 결합하여 커널 GS.base 노출 취약점(Spectre-variant)으로 이어질 수 있습니다.
관련 방어 기법(SWAPGS 배리어)은 커널 취약점 및 보안 패치를 참조하세요.
아키텍처 분석 플레이북
아키텍처 문서를 읽을 때는 개념 암기보다 "실제 커널 코드에서 어디서 소비되는지"를 함께 확인해야 이해가 빠르게 고정됩니다. 아래 절차는 x86_64, ARM64, RISC-V를 공통 틀로 비교할 때 유용합니다.
| 분석 축 | 핵심 질문 | 확인 위치 |
|---|---|---|
| 권한 전환 | 사용자→커널 진입 경로가 무엇인가? | arch/*/entry/, syscall entry 코드 |
| 메모리 모델 | 페이지 테이블 구성과 주소 변환 흐름은? | arch/*/mm/, page table 매크로 |
| 인터럽트 구조 | 예외/IRQ 디스패치 체인이 어떻게 구성되는가? | arch/*/kernel/irq*, trap/exception 핸들러 |
| 부팅 초기화 | early init에서 공통 init로 넘어가는 경계는? | head*.S, start_kernel() 이전 코드 |
# 아키텍처별 엔트리 코드 빠른 점검
git grep -n "SYSCALL\|el0_svc\|do_trap" -- arch/x86 arch/arm64 arch/riscv
# 페이지 테이블 핵심 경로 확인
git grep -n "pgd\|pud\|pmd\|pte" -- arch/*/include/asm arch/*/mm
# 부팅 초기 코드 진입점 확인
git grep -n "start_kernel\|head_.*\.S" -- arch/* init/main.c
관련 문서
커널 아키텍처와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.