커널 아키텍처 (Kernel Architecture)

리눅스 커널 아키텍처 심층 분석. x86_64, ARM64, RISC-V 아키텍처별 부팅 과정, 주소 공간, 특권 레벨, 커널 소스 트리 구조를 다룹니다.

전제 조건: 개발 환경 설정소스 코드 읽기 문서를 먼저 읽으세요. 커널 아키텍처는 전체 구조를 조감하는 입문 문서이므로, 개발 환경이 준비되고 소스를 탐색할 수 있으면 바로 시작할 수 있습니다.
일상 비유: 이 주제는 시스템 시동 절차 점검표와 비슷합니다. 시동 순서가 어긋나면 전체가 멈추듯이, 초기화 단계의 선후관계를 정확히 지켜야 문제를 줄일 수 있습니다.

핵심 요약

  • 단계 분리 — 펌웨어, 부트로더, 커널 초기화 경계를 구분합니다.
  • 하드웨어 기술 — ACPI/DT 등 기술 정보가 어디서 소비되는지 확인합니다.
  • 신뢰 체인 — Secure Boot 등 검증 체인을 흐름으로 이해합니다.
  • 실패 지점 — 부팅 로그에서 단계별 실패 단서를 빠르게 찾습니다.
  • 호환성 관점 — 플랫폼 차이에 따른 초기화 분기를 함께 점검합니다.

단계별 이해

  1. 부팅 단계 식별
    현재 이슈가 어느 단계에서 발생하는지 먼저 고정합니다.
  2. 입력 데이터 확인
    펌웨어/테이블/이미지 메타데이터를 점검합니다.
  3. 전환 경계 검증
    단계 간 인자 전달과 상태 인계를 추적합니다.
  4. 플랫폼별 재검증
    다른 하드웨어 조건에서도 동일하게 동작하는지 확인합니다.

x86_64, ARM64, RISC-V 아키텍처별 커널 구조, 부팅 과정, 주소 공간 레이아웃을 상세히 다룹니다.

관련 표준: Intel SDM (x86/x64 아키텍처), ARM Architecture Reference Manual (AArch64), UEFI 2.10 — 커널이 지원하는 주요 프로세서 아키텍처와 펌웨어 인터페이스 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

리눅스 커널 개요 (Linux Kernel Overview)

리눅스 커널은 1991년 Linus Torvalds가 처음 공개한 이래, 현재 세계에서 가장 널리 사용되는 운영체제 커널입니다. 서버, 데스크탑, 임베디드 기기, 스마트폰(Android), 슈퍼컴퓨터에 이르기까지 거의 모든 컴퓨팅 영역에서 동작합니다. 커널은 하드웨어와 사용자 공간(user space) 사이에서 추상화 계층 역할을 하며, 다음과 같은 핵심 기능을 담당합니다:

참고: 리눅스 커널 소스는 매우 큰 규모의 코드베이스이며, 전 세계 수천 명의 개발자가 참여하는 대표적인 오픈소스 프로젝트입니다. 지원 아키텍처도 x86, ARM, ARM64, RISC-V, MIPS, PowerPC, s390 등 매우 다양합니다.

모놀리식 vs 마이크로커널 (Monolithic vs Microkernel)

운영체제 커널 설계에는 크게 두 가지 접근 방식이 있습니다. 모놀리식 커널(Monolithic Kernel)은 모든 핵심 서비스(프로세스 관리, 메모리 관리, 파일시스템, 드라이버 등)가 하나의 커다란 커널 이미지 안에서 동일한 주소 공간(커널 공간)에서 실행됩니다. 반면 마이크로커널(Microkernel)은 최소한의 기능만 커널에 포함하고 나머지는 사용자 공간 서버로 분리합니다.

리눅스는 모놀리식 커널입니다. 그러나 순수한 모놀리식이 아닌, 동적으로 적재 가능한 커널 모듈(Loadable Kernel Module, LKM)을 지원하여 모듈화의 유연성을 확보합니다. 이를 "모듈형 모놀리식(Modular Monolithic)" 커널이라고도 합니다. 커널 모듈에 대한 자세한 내용은 커널 모듈 (Kernel Module) 문서를 참고하세요.

모놀리식 커널 (Linux) 커널 공간 (Kernel Space) Process Mgmt Memory Mgmt VFS Network Stack Device Drivers 사용자 공간 (User Space) Application 함수 호출 (높은 성능) 마이크로커널 (Minix, QNX) 마이크로커널 (IPC, 스케줄, 기본 메모리) 사용자 공간 서버 FS 서버 | 드라이버 서버 | 네트워크 서버 IPC (프로세스 간 통신) (격리성 우수, 오버헤드 있음) 비교 요약 • 모놀리식: 단일 주소 공간 함수 호출 → 빠름 • 마이크로커널: 최소 커널 IPC → 격리성 좋음
TIP: 모놀리식 커널의 장점은 서브시스템 간 함수 호출이 직접적이어서 성능이 뛰어나다는 것입니다. 마이크로커널은 IPC(프로세스 간 통신)를 통해 서비스 간 통신하므로 오버헤드가 있지만, 격리성과 안정성이 우수합니다. Linus Torvalds와 Andrew Tanenbaum 사이의 유명한 "모놀리식 vs 마이크로커널" 논쟁은 OS 설계 철학에서 중요한 역사적 사건입니다.

x86_64 아키텍처 (x86_64 Architecture)

x86_64(또는 AMD64, Intel 64)는 데스크탑과 서버 환경에서 가장 널리 사용되는 아키텍처입니다. x86의 32비트 아키텍처를 64비트로 확장한 것으로, 리눅스 커널에서 가장 오랫동안 지원해 온 아키텍처 중 하나입니다.

부팅 과정 (Boot Process)

x86_64 시스템의 부팅 과정은 펌웨어에서 시작하여 커널이 완전히 초기화될 때까지 여러 단계를 거칩니다. 현대 시스템에서는 UEFI가 표준이지만, 레거시 BIOS도 여전히 지원됩니다.

BIOS (레거시) UEFI (현대) 전원 ON / 리셋 벡터 CPU → 0xFFFFFFF0 실행 시작 BIOS POST 하드웨어 자가진단, 부트 디바이스 선택 MBR 로드 (512B) 디스크 첫 섹터, 파티션 테이블 포함 GRUB Stage 1 → Stage 2 grub.cfg 파싱, 파일시스템 인식 커널 + initramfs 로드 Real → Protected → Long Mode 전환 전원 ON / SEC·PEI·DXE UEFI 펌웨어 초기화 단계 UEFI POST + 메모리 맵 하드웨어 초기화, EFI 메모리 맵 생성 ESP 마운트 EFI System Partition (FAT32) GRUB EFI 앱 / EFI Stub grubx64.efi 또는 EFI Stub 직접 부팅 커널 + initramfs 로드 Protected Mode → Long Mode 전환 decompress_kernel() → startup_64() 커널 압축 해제, x86_64 초기화 진입 arch/x86/boot/compressed/misc.c → arch/x86/kernel/head_64.S Real Mode 16-bit / 1MB 주소 공간 CS:IP 세그먼트 기반 주소 BIOS INT 서비스 가용 CR0.PE = 0 리얼 모드 인터럽트 테이블 펌웨어 → 부트로더 단계 최대 20-bit 주소 버스 startup_32() CR0.PE = 1 Protected Mode 32-bit / 4GB 주소 공간 GDT/IDT 기반 디스크립터 CR0.PE = 1 활성화 페이지 테이블 활성화 가능 커널 압축 해제 단계 startup_32() 실행 구간 PAE 활성화 (CR4.PAE) startup_64() EFER.LME = 1 Long Mode 64-bit / 48-bit VA (256TB) RIP 상대 주소 지정 CR4.PAE + EFER.LME = 1 4-level 페이지 테이블 startup_64() 실행 구간 x86_64_start_kernel() 호출 64-bit 레지스터 (RAX, RBX…) 소스 파일 decompress_kernel() 커널 이미지 압축 해제 (zlib / lzma / lz4) arch/x86/boot/compressed/misc.c startup_64() 세그먼트 레지스터 초기화, 초기 페이지 테이블 설정 arch/x86/kernel/head_64.S x86_64_start_kernel() GDT/IDT 설정, CR4/EFER 초기화, 페이지 테이블 최종화 arch/x86/kernel/head64.c start_kernel() CPU/메모리/인터럽트/스케줄러/타이머 초기화 setup_arch() mm_init() sched_init() trap_init() init/main.c rest_init() kernel_init 스레드 생성, idle 루프 진입 (PID 0) init/main.c kernel_init() 드라이버 초기화, rootfs 마운트, init 프로세스 탐색 (PID 1) init/main.c /sbin/init (systemd) 사용자 공간 PID 1 실행, 서비스 및 타겟 시작 (사용자 공간)
주의: UEFI 부팅에서는 EFI Stub 기능을 통해 bootloader 없이 커널을 직접 부팅할 수 있습니다. 이 경우 커널 이미지가 직접 UEFI 애플리케이션으로 동작하며, 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 가상 주소 공간 레이아웃 (4-Level Paging, 48-bit) 0xFFFFFFFF_FFFFFFFF 0xFFFFFFFF_80000000 0xFFFFFFFE_80000000 0xFFFFFC00_00000000 0xFFFF8880_00000000 0xFFFF8000_00000000 0x00007FFF_FFFFFFFF 0x00000000_00000000 Kernel text mapping (512MB) Kernel text (실제 커널 코드, __START_KERNEL_map) Modules space (1.5GB) vmalloc / ioremap 영역 Direct mapping of all physical memory Guard hole / unused 비정규 주소 영역 (Non-canonical hole) Stack (grows downward) mmap 영역 (shared libs, anonymous) Heap (grows upward) BSS / Data / Text (ELF segments) NULL pointer guard page Kernel Space (128 TB) User Space (128 TB) grows grows
x86_64 가상 주소 공간 레이아웃 - 커널 공간(상위 128TB)과 사용자 공간(하위 128TB)

아키텍처별 주소 변환 경로 비교

가상 주소를 물리 주소로 변환하는 핵심 경로는 세 아키텍처 모두 유사하지만, 제어 레지스터와 페이지 테이블 포맷이 다릅니다. 아래 다이어그램은 x86_64, ARM64, RISC-V의 변환 경로를 한 화면에서 비교합니다.

아키텍처별 가상 주소 변환 경로 x86_64 가상 주소 (VA) CR3 기반 페이지 워크 PML4 - PDPT - PD - PT TLB 히트 시 워크 생략 물리 주소 (PA) ARM64 가상 주소 (VA) TTBR0/TTBR1 + TCR L0 - L1 - L2 - L3 테이블 ASID로 TLB flush 최소화 물리 주소 (PA) RISC-V 가상 주소 (VA) satp + Sv39/Sv48 3/4단계 페이지 테이블 sfence.vma로 TLB 동기화 물리 주소 (PA)
x86_64, ARM64, RISC-V의 주소 변환 흐름 비교 - 제어 레지스터와 페이지 테이블 구조의 차이

세그먼테이션과 페이징 (Segmentation & Paging)

x86_64에서 세그먼테이션은 사실상 flat model로 사용됩니다. 모든 세그먼트의 베이스가 0이고 리미트가 최대값으로 설정되어, 세그먼테이션은 사실상 비활성화된 상태입니다. 그러나 GDT(Global Descriptor Table)는 여전히 존재하며, 커널/사용자 모드 전환과 TSS(Task State Segment)를 위해 필수적입니다.

x86_64 세그먼테이션 (Flat Model) GDT (Global Descriptor Table) 코드/데이터/테스크 세그먼트 세그먼트 레지스터 CS, DS, SS, ES, FS, GS Flat Model (실제 동작) • Base = 0x00000000 (모든 세그먼트) • Limit = 0xFFFFFFFF (최대값) • 결과: 세그먼테이션 오버헤드 없이 선형 주소 = 가상 주소로 직접 사용

페이징은 4단계 페이지 테이블을 사용합니다 (5단계 페이징은 CONFIG_X86_5LEVEL=y로 활성화):

x86_64 4-Level Page Table Walk (48-bit VA) PGD [47:39] PUD [38:30] PMD [29:21] PTE [20:12] 9bit 9bit 9bit 9bit 12bit (offset) PGD CR3 → 512 entries PUD 512 entries PMD 512 entries PTE 512 entries Page Frame 4KB page 주소 변환 공식 물리주소 = (PTE가 가리키는 4KB 프레임) + (오프셋 12bit) 최대 물리 주소: 52bit (4PB), 페이지 크기: 4KB/2MB/1GB CR3 레지스터

링 구조 (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/SYSRET: x86_64에서 시스템 콜은 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 발생 시에만 가능합니다.

ARM64 Exception Level 계층 EL3 Secure Monitor ATF/TF-A, TrustZone world 전환, 최상위 특권 EL2 Hypervisor KVM/가상화 실행 계층 EL1 OS Kernel Linux 커널, 시스템 콜/인터럽트 처리 EL0 User 일반 애플리케이션 실행 특권 높음 낮음 전환 명령 EL0→EL1: SVC EL1→EL2: HVC EL2→EL3: SMC 상위→하위: ERET

ARM64 부팅 과정 (Boot Process)

ARM64의 부팅 과정은 x86과 상당히 다릅니다. 대부분의 ARM64 시스템은 Device Tree를 사용하여 하드웨어 구성 정보를 커널에 전달합니다. 부팅 프로토콜은 커널 이미지의 시작점에 명시된 규약을 따릅니다.

ARM64 부팅 시퀀스와 실행 레벨 BootROM SoC 내장 코드 실행 BL1 로드 BL1/BL2 (TF-A) EL3 보안 초기화 BL31/BL33 준비 U-Boot / UEFI 커널 + initramfs 로드 x0 = DTB 주소 전달 Linux Kernel primary_entry() start_kernel() 진입 레벨 전환 EL3 (TF-A) EL2 또는 EL1 (부트 정책) 커널 head.S 에서 MMU 활성화 핵심 레지스터 전달: x0 = DTB 물리 주소, x1~x3 = 플랫폼 의존 파라미터 커널 엔트리 primary_entry() __primary_switch() start_kernel()
ARM64 부팅 단계 - BootROM, TF-A, 부트로더, 커널로 이어지는 흐름과 EL 전환

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 */
    };
};
TIP: Device Tree 소스 파일은 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) 접근 권한이 다릅니다.

RISC-V 특권 모드 계층 M-mode (Machine) OpenSBI/펌웨어, 최고 특권 타이머/IPI/SBI 서비스 제공 하드웨어 직접 제어 S-mode (Supervisor) Linux 커널 실행 satp 기반 페이지 테이블/예외 관리 시스템 콜/인터럽트 처리 U-mode (User) 애플리케이션 실행 `ecall`로 S-mode 진입 특권 높음 낮음 호출/복귀 U→S: `ecall` S→M: `ecall` (SBI) M→S/U: `mret/sret`

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입니다.

RISC-V 부팅 시퀀스와 SBI 경계 ZSBL / BootROM 하드웨어 초기화 M-mode 시작 OpenSBI 타이머/IPI/SBI 서비스 S-mode 진입 준비 U-Boot 커널 + DTB 로드 initramfs 준비 Linux Kernel _start / setup_vm() start_kernel() 진입 SBI 호출 경계 S-mode 커널은 기계 제어 작업을 OpenSBI(M-mode)에 ecall로 요청 예: 타이머 설정, IPI 전송, 전원 상태 제어(HSM) 커널 엔트리 인자 a0 = hartid, a1 = dtb_addr 전달 후 _start 진입 setup_vm() → relocate_enable_mmu() → start_kernel()
RISC-V 부팅 단계 - OpenSBI를 경계로 M-mode와 S-mode 책임이 분리됨
HART: RISC-V에서 CPU 코어에 해당하는 개념을 HART(Hardware Thread)라고 합니다. 멀티코어 시스템에서는 여러 HART가 존재하며, 부팅 시 하나의 HART만 부팅 코드를 실행하고 나머지는 WFI(Wait For Interrupt) 상태에서 대기합니다.

특권 레벨 비교 (Privilege Level Comparison)

세 아키텍처의 특권 레벨 구조를 비교하면 설계 철학의 차이를 명확히 알 수 있습니다. 다음 다이어그램은 x86_64, ARM64, RISC-V의 특권 레벨을 나란히 비교합니다:

아키텍처별 특권 레벨 비교 (Privilege Level Comparison) x86_64 ARM64 (AArch64) RISC-V High Low Ring -1 (VMX Root) Hypervisor Ring 0 Kernel Mode Ring 1, 2 (미사용) Ring 3 User Mode EL3 Secure Monitor (TrustZone) EL2 Hypervisor (KVM) EL1 OS Kernel (Linux) EL0 User Applications M-mode Machine (OpenSBI) S-mode Supervisor (Linux) U-mode User Applications Legend = Linux 커널 실행 레벨 = 동등 레벨 연결
x86_64, ARM64, RISC-V의 특권 레벨 비교 - 각 아키텍처에서 리눅스 커널이 실행되는 레벨이 굵은 테두리로 표시됨

위 다이어그램에서 주목할 점은 리눅스 커널이 각 아키텍처에서 다른 특권 레벨에서 동작한다는 것입니다:

VHE (Virtualization Host Extensions): ARM64에서 CONFIG_ARM64_VHE=y가 활성화되면, 리눅스 커널이 EL2에서 직접 실행될 수 있습니다. 이를 통해 KVM 호스트 커널이 EL2에서 동작하고, 게스트 OS가 EL1에서 실행되어 가상화 전환 오버헤드가 크게 줄어듭니다.

시스템 콜/예외 진입 경로 비교

사용자 공간에서 커널 공간으로 전환되는 공통 경로는 시스템 콜, 인터럽트, 예외입니다. 아키텍처별 진입 명령과 복귀 명령은 다르지만, 커널이 트랩 프레임을 저장하고 핸들러를 호출한 뒤 사용자 공간으로 복귀한다는 큰 흐름은 동일합니다.

아키텍처별 사용자→커널 진입 경로 x86_64 User (Ring 3) - libc syscall() SYSCALL / INT n / #PF entry_SYSCALL_64 / IDT 핸들러 do_syscall_64() / do_page_fault() SYSRET / IRETQ 로 사용자 복귀 ARM64 User (EL0) - libc syscall() SVC #0 / IRQ / Data Abort EL1 벡터 (VBAR_EL1) 진입 el0_svc / do_mem_abort() ERET 으로 EL0 복귀 RISC-V User (U-mode) - libc syscall() ecall / interrupt / exception stvec 트랩 엔트리 진입 do_trap_ecall_u() / do_page_fault() sret 으로 U-mode 복귀 공통점: 사용자 요청/예외 발생 → 트랩 프레임 저장 → 커널 핸들러 실행 → 상태 복원 후 사용자 복귀
시스템 콜/예외 진입 경로 비교 - 진입 명령은 다르지만 트랩 처리의 핵심 단계는 공통

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)

리눅스 커널 소스 코드는 기능별로 잘 정리된 디렉토리 구조를 가지고 있습니다. 커널 개발을 시작할 때 이 구조를 이해하는 것이 매우 중요합니다.

linux/ (커널 소스 루트) arch/ (아키텍처 의존 코드) - x86/: boot/, kernel/, mm/, include/, entry/ - arm64/: boot/dts/, kernel/, mm/ - riscv/: kernel/, mm/ 아키텍처별 부트/예외/페이지 테이블 구현 kernel/ + mm/ (핵심 서브시스템) - kernel/: sched/, locking/, irq/, time/, rcu/ - mm/: page_alloc.c, slub.c, vmalloc.c, mmap.c 프로세스/스케줄링/동기화/메모리 관리의 중심 fs/ + net/ - fs/: ext4, btrfs, proc, sysfs, namei.c - net/: core, ipv4, ipv6, netfilter, xdp 저장소/네트워크 I/O 경로를 구성 drivers/ (가장 큰 트리) - char/, block/, net/, gpu/, pci/, usb/, of/ 디바이스 모델과 버스 계층 위에서 동작 대부분의 하드웨어 지원 코드가 위치 include/ + init/ - include/: linux/, asm-generic/, uapi/ - init/: main.c (start_kernel) 공용 API 헤더와 부팅 초기화 진입점 기타 핵심 디렉터리 ipc/, security/, crypto/, lib/ scripts/, tools/, Documentation/ Kconfig, Makefile (최상위 빌드/설정) 구조적 포인트: arch(플랫폼 특화) + kernel/mm/fs/net/drivers(공통 코어) + include/init(진입/인터페이스)
TIP: drivers/ 디렉토리가 전체 소스의 약 60% 이상을 차지합니다. 커널의 핵심 로직은 kernel/, mm/, fs/, net/에 집중되어 있으며, 이 디렉토리들의 코드를 이해하면 커널의 핵심 동작 원리를 파악할 수 있습니다. 소스 코드 탐색에는 Bootlin Elixir Cross-referencer가 매우 유용합니다.

arch/ 디렉토리 상세 (Architecture Directory Detail)

arch/ 디렉토리 아래의 각 아키텍처 디렉토리는 비슷한 하위 구조를 가집니다. 이는 커널의 아키텍처 추상화 설계 원칙을 반영합니다:

커널은 이러한 구조를 통해 아키텍처 독립적인 코드(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
다음 단계: 커널 아키텍처의 기본 개념을 이해했다면, 다음으로 빌드 시스템 (Build System) 문서에서 실제로 커널을 빌드하는 방법을 학습하거나, 메모리 관리 (Memory Management) 문서에서 각 아키텍처의 페이지 테이블과 메모리 할당 메커니즘을 심층적으로 살펴볼 수 있습니다. 또한 부팅 과정 (Boot Process) 문서에서 각 아키텍처별 부팅 흐름을 더 자세히 확인할 수 있습니다.

아키텍처별 성능 최적화 (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: 사용 목적에 따라 다릅니다:

Q2. 크로스 컴파일은 어떻게 하나요?

A: ARCHCROSS_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: 가상 주소 공간 크기에 따라 결정됩니다:

레벨이 많을수록 주소 공간은 넓어지지만 페이지 테이블 워킹 오버헤드가 증가합니다. 자세한 내용은 메모리 관리 (Memory Management)메모리 관리 심화 (Advanced Memory Management) 문서를 참고하세요.

Q6. 아키텍처별 시스템 콜 차이는?

A: 시스템 콜 번호와 진입 메커니즘이 다릅니다:

모든 아키텍처는 공통 시스템 콜 인터페이스를 제공하지만, 일부 아키텍처별 시스템 콜이 존재할 수 있습니다. 자세한 내용은 시스템 콜 (System Call) 문서를 참고하세요.

Q7. 가상화 오버헤드는 얼마나 되나요?

A: 하드웨어 가상화 지원 여부에 따라 크게 다릅니다:

현대 시스템에서는 하드웨어 가상화 지원이 표준이므로 오버헤드가 크지 않습니다. 자세한 내용은 가상화 (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! 명령어
Intel SDM과의 차이점: AMD APM Vol. 2는 AMD-V(SVM) 가상화를 상세히 다루며, 이는 Intel VT-x와 구조적으로 다릅니다. KVM 코드에서 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 전송 특성
주의 — IMPLEMENTATION DEFINED: ARM 아키텍처 매뉴얼에는 "IMPLEMENTATION DEFINED"로 표기된 동작이 많습니다. 이는 각 SoC/코어 구현에 따라 달라지므로, 실제 하드웨어에서의 동작은 해당 코어의 TRM(Technical Reference Manual)과 SoC 벤더의 데이터시트를 추가로 확인해야 합니다.

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 인터럽트 체계
RISC-V의 특징: 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-bit, 1MB Protected Mode 32-bit, 4GB PAE Mode 32-bit, 64GB Compatibility 32-bit (in 64) Long Mode 64-bit, 256TB+ PE=1 PAE=1 EFER.LME=1 CS.L=0

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비트 스택 설정
Real Mode → Protected Mode 전환 단계 ① CLI 인터럽트 금지 ② A20 활성 1MB+ 접근 가능 ③ LGDT GDT 적재 ④ CR0.PE=1 PM 진입! ⑤ far JMP CS 갱신 ⑥ DS.. 세그먼트 CR0 레지스터 주요 비트 (Protected Mode 관련) PG bit 31 ... NE bit 5 ET bit 4 TS bit 3 EM bit 2 MP bit 1 PE bit 0 페이징 활성화 (PM에서는 선택) Protection Enable 0→1: PM 진입 WP bit 16 Write Protect Ring 0 쓰기 보호 CR0.PE=1 직후 반드시 far JMP 필수! far JMP 없으면 CPU 명령어 프리페치 큐에 Real Mode 디코딩 결과가 남아 예측 불가능한 동작 발생
Real Mode → Protected Mode 전환은 CLI → A20 → LGDT → CR0.PE=1 → far JMP → 세그먼트 재적재 순서로 진행됩니다.

세그먼테이션 (Segmentation)

Protected Mode의 핵심 메커니즘은 세그먼테이션입니다. 모든 메모리 접근은 세그먼트 셀렉터를 통해 GDT/LDT의 디스크립터를 참조하여 선형 주소(Linear Address)로 변환됩니다.

Protected Mode: 논리 주소 → 선형 주소 → 물리 주소 논리 주소 (Logical) CS:EIP = 0x08:0x00401000 세그먼트 셀렉터 (16비트) Index [15:3] TI RPL TI=0: GDT, TI=1: LDT Index GDT (Global Descriptor Table) [0] NULL (필수) [1] 코드 세그먼트 (0x08) [2] 데이터 세그먼트 (0x10) [3] 유저 코드 (0x1B) [4] TSS (0x20) ... Base 선형 주소 (Linear) = Base + Offset 페이징 (선택) 물리 주소 (Physical) PD → PT → 물리 프레임 Linux의 Flat 세그먼트 모델 Linux는 모든 세그먼트의 Base=0, Limit=0xFFFFF (G=1이면 4GB)로 설정하여 세그먼테이션을 사실상 무력화합니다. 논리 주소 = 선형 주소가 되어 페이징만으로 메모리 보호를 수행합니다. 이유: 세그먼테이션은 x86 전용이므로 아키텍처 이식성을 위해 페이징 기반 보호를 선택합니다.
논리 주소(Segment:Offset)를 세그먼트 디스크립터의 Base와 합산하여 선형 주소를 만들고, 페이징이 활성화되어 있으면 물리 주소로 최종 변환합니다. Linux는 Flat 모델로 세그먼테이션을 사실상 우회합니다.

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단계를 거쳐 물리 주소로 변환됩니다.

Protected Mode 2-Level 페이징 (CR0.PG=1, CR4.PAE=0) 32비트 선형 주소 분해 Directory [31:22] 10비트 = 1024 엔트리 Table [21:12] 10비트 = 1024 엔트리 Offset [11:0] 12비트 = 4KB 페이지 CR3 Page Directory 1024 × 4B = 4KB PDE[0] PDE[1] ... PDE[1023] Page Table 1024 × 4B = 4KB PTE[0] PTE[1] ... PTE[1023] 물리 페이지 4KB (4096 바이트) 물리 주소 = Frame + Offset PDE/PTE 엔트리 형식 (32비트) Page Frame 주소 [31:12] (20비트) AVL G PS D A PCD PWT U/S R/W P 31 12 8 2 1 0 P=Present R/W=Read/Write U/S=User/Supervisor D=Dirty A=Accessed G=Global PS=Page Size (PDE에서 1이면 4MB 대형 페이지, PT 건너뜀)
32비트 선형 주소를 Directory(10bit) + Table(10bit) + Offset(12bit)로 분해하여 2단계 페이지 테이블을 거쳐 물리 주소로 변환합니다. PDE.PS=1이면 4MB 대형 페이지로 PT 단계를 건너뜁니다.
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가 발생합니다.

Protected Mode → Long Mode 전환 상태 머신 Protected Mode CR0: PE=1, PG=0 또는 1 페이징 비활성화 CR0.PG=0 (이미 0이면 생략) PAE 활성화 CR4.PAE = 1 PML4 페이지 테이블 설정 CR3 = PML4 물리 주소 (Identity Map 포함) EFER.LME = 1 MSR 0xC0000080 bit 8 설정 CR0.PG = 1 → Long Mode! EFER.LMA = 1 자동 설정 (CPU가 확인) 64-bit Mode 진입 ljmp $__KERNEL_CS, $startup_64 (CS.L=1) 순서 오류 시: PAE 없이 LME+PG → #GP | PML4 미설정 → Triple Fault | far JMP 없이 32bit 코드 실행 → 비정상 동작 | Identity Map 없음 → #PF UEFI 부팅: 펌웨어가 이미 Long Mode → 커널 EFI stub은 모드 전환 불필요 (직접 startup_64 진입)
Protected Mode에서 Long Mode로의 전환은 페이징 비활성화 → PAE → PML4 설정 → EFER.LME → CR0.PG → far JMP 순서로 진행됩니다. 순서 오류는 #GP 또는 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를 차지합니다.

Long Mode 4-Level 페이징 (48비트 가상 주소) 48비트 가상 주소 필드 분해 PML4 [47:39] PDPT [38:30] PD [29:21] PT [20:12] Offset [11:0] 9비트=512 9비트=512 9비트=512 9비트=512 12비트=4KB CR3 PML4 512 × 8B = 4KB PDPT 512 × 8B = 4KB PD 512 × 8B = 4KB PT 512 × 8B = 4KB 물리 페이지 4KB 대형 페이지 (Huge Pages) 2MB Huge Page PDE.PS=1 → PT 건너뜀 PML4 → PDPT → PD → 2MB 프레임 1GB Huge Page PDPTE.PS=1 → PD+PT 건너뜀 PML4 → PDPT → 1GB 프레임 5-Level (LA57) PML5 → PML4 → PDPT → PD → PT 57비트 VA = 128PB (Ice Lake+) Long Mode PTE 형식 (64비트) NX bit 63 Rsvd/Avail [62:52] 물리 프레임 주소 [51:12] (40비트 = 1TB+) [51:12] AVL G PAT D A PCD PWT U/S R/W P NX(bit 63) = 1이면 해당 페이지에서 코드 실행 금지 → W^X 보호, 스택/힙 실행 방지 (EFER.NXE=1 필요)
48비트 가상 주소를 4단계 (PML4→PDPT→PD→PT)로 분해하여 물리 주소로 변환합니다. 각 테이블은 512 엔트리 × 8바이트 = 4KB. NX 비트(bit 63)로 코드 실행을 페이지 단위로 금지할 수 있습니다.

Protected Mode vs Long Mode 페이징 비교

항목Protected Mode (non-PAE)Protected Mode (PAE)Long Mode (4-level)Long Mode (5-level)
가상 주소 비트32324857
물리 주소 비트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→...)
엔트리/테이블1024512 (PDPT: 4)512512
기본 페이지4KB4KB4KB4KB
대형 페이지4MB (PSE)2MB2MB, 1GB2MB, 1GB
NX 비트없음있음 (bit 63)있음 (bit 63)있음 (bit 63)
CR3 가리킴PDPDPTPML4PML5

레지스터 확장

Long Mode에서는 기존 8개 범용 레지스터가 64비트로 확장되고, R8~R15 8개 레지스터가 추가됩니다. REX 프리픽스가 이 확장 레지스터 접근을 가능하게 합니다.

32-bit (Protected)64-bit (Long)추가 레지스터용도 (System V ABI)
EAX → RAXRAX (64-bit)R85번째 함수 인자
EBX → RBXRBX (callee-saved)R96번째 함수 인자
ECX → RCXRCX (4번째 인자)R10static chain (caller-saved)
EDX → RDXRDX (3번째 인자)R11SYSCALL RFLAGS 저장
ESI → RSIRSI (2번째 인자)R12callee-saved
EDI → RDIRDI (1번째 인자)R13callee-saved
EBP → RBPRBP (frame pointer)R14callee-saved
ESP → RSPRSP (stack pointer)R15callee-saved
EIP → RIPRIP (64-bit)RIP-relative 주소 지정 모드 추가

Long Mode에서의 세그먼테이션 변화

Long Mode는 세그먼테이션을 대부분 비활성화합니다. Protected Mode에서 핵심이었던 세그먼트 Base/Limit이 무시됩니다.

세그먼트Protected ModeLong Mode (64-bit)
CSBase + Limit + DPL + L/D 비트 모두 유효DPL, L, D 비트만 유효. Base/Limit 무시. L=1, D=0이어야 64-bit 모드
SSBase + Limit + DPL 유효DPL만 유효 (Ring 전환 시). Base=0, Limit 무시
DS, ESBase + Limit + 접근 권한 유효완전 무시 — Base=0 강제, Limit 체크 없음
FS, GSBase + 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.LCS.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 대안, 매우 드묾)
Compatibility Mode와 커널: 64비트 리눅스에서 32비트 바이너리를 실행하면 유저 공간은 Compatibility Mode(CS.L=0)로 동작하지만, 시스템 콜로 커널에 진입하면 항상 64-bit Mode로 전환됩니다. arch/x86/entry/entry_64_compat.Sentry_SYSENTER_compatentry_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 주소 지정
가상 주소 공간4GB256TB (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/SYSEXITSYSCALL/SYSRET (고속, MSR 기반)
TSS스택 포인터 + I/O bitmapIST(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바이트) 구조를 사용합니다.

Protected Mode (48비트, 6바이트) Limit [15:0] 16비트 Base (선형 주소) [47:16] 32비트 bit 0 15 16 47 Long Mode (80비트, 10바이트) Limit [15:0] 16비트 Base (선형 주소, 64비트) [79:16] 64비트 bit 0 15 16 79 Limit (테이블 바이트 크기 − 1) Base (테이블 선형 주소)
레지스터 유형 내용 적재 명령어 저장 명령어
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 피연산자 형식: 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"를 기준으로 설명합니다.

비트 [63:32] Base[31:24] G D/B L AVL Lmt[19:16] P DPL S Type[3:0] Base[23:16] 63 56 55 54 53 52 51 48 47 46 45 44 43 40 39 32 비트 [31:0] Base[15:0] Limit[15:0] 31 16 15 0 Base 주소 Segment Limit Access: P/DPL/S/Type Granularity: G/D·B/L/AVL
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)
Long Mode 코드 세그먼트 주의사항: 64비트 Long Mode에서 코드 세그먼트는 반드시 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에서 연속된 두 슬롯을 차지합니다.

상위 64비트 (Quadword 2, GDT 두 번째 슬롯) Base[63:32] [63:32] 32비트 — 상위 기본 주소 Reserved (0으로 설정) [31:0] 하위 64비트 (Quadword 1, GDT 첫 번째 슬롯) Limit[15:0] [15:0] Base[23:0] [39:16] P | DPL | 0 | Type(4) [47:40] G|AVL|Lmt [55:48] Base[31:24] [63:56] Base (기본 주소) Limit (크기) Type / 제어 비트 ※ Long Mode에서 TSS/LDT 디스크립터만 128비트. Call/Interrupt/Trap Gate는 64비트 유지.
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);
Long Mode TSS/LDT 디스크립터 확장: Protected Mode에서는 TSS/LDT 디스크립터가 일반 세그먼트처럼 64비트(8바이트)였지만, Long Mode에서는 64비트 Base 주소를 수용하기 위해 128비트(16바이트)로 확장됩니다. 따라서 GDT에서 두 개의 연속 슬롯을 사용하며, 두 번째 슬롯은 상위 32비트 Base와 예약 영역만 포함합니다. Call Gate, Interrupt Gate, Trap Gate는 Long Mode에서도 여전히 64비트입니다.

IDT 게이트 디스크립터 구조 상세

64비트 Long Mode의 IDT 엔트리는 16바이트(128비트)로, Protected Mode의 8바이트에서 확장되었습니다. 핸들러 오프셋이 64비트로 늘어났으며, IST(Interrupt Stack Table) 필드가 추가되어 중첩 예외 상황에서도 안전한 전용 스택을 사용할 수 있습니다.

상위 8바이트 [127:64] Offset[63:32] Reserved (0) 127 96 95 64 하위 8바이트 [63:0] Offset[31:16] P DPL 0 Type 000 IST Selector Offset[15:0] 63 48 47 46 45 44 43 40 39 35 34 32 31 16 15 0 핸들러 Offset 제어 필드 (P/DPL/Type) IST (스택 선택) 코드 세그먼트 셀렉터
Gate TypeType 필드IF 플래그용도
Interrupt Gate0xE (1110)자동 클리어하드웨어 인터럽트, 재진입 방지
Trap Gate0xF (1111)유지소프트웨어 예외, 디버그 트랩
IST 값할당 스택사용 벡터 (리눅스)
0일반 스택 (RSP0)일반 인터럽트/예외
1IST1 스택#DF Double Fault (벡터 8)
2IST2 스택NMI (벡터 2)
3IST3 스택#MC Machine Check (벡터 18)
4IST4 스택#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 허용 */
DPL 설정 주의: 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는 활용하지 않습니다.

세그먼트 셀렉터 (16비트) Index [15:3] GDT/LDT 엔트리 번호 (최대 8192) TI 0=GDT RPL [1:0] 15 3 2 1 0 접근 허용 조건: max(CPL, RPL) ≤ DPL CPL (Current Privilege Level): CS 셀렉터 [1:0] = 현재 실행 코드의 특권 수준 DPL (Descriptor Privilege Level): GDT 디스크립터 [46:45] = 세그먼트가 요구하는 최소 특권 RPL (Requested Privilege Level): 셀렉터 [1:0] = 호출자가 명시적으로 지정한 특권 (0x08→Ring0, 0x0B→Ring3)
레벨위치의미리눅스 값
CPLCS 셀렉터 [1:0]현재 실행 코드의 특권 수준0 (커널) 또는 3 (유저)
DPLGDT 디스크립터 [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/SYSRET과 IRET의 차이: 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 전환을 수행합니다.

Ring 3 (사용자 공간) SYSCALL 실행 시 CPU 동작: • RCX ← RIP (복귀 주소 보존) • R11 ← RFLAGS (플래그 보존) • CS/SS ← STAR[47:32] (Ring 0 전환) • RIP ← LSTAR (커널 진입점으로 점프) Ring 0 (커널 공간) SYSCALL 진입 후 커널 처리: • SWAPGS: GS.base 교체 (per-CPU 접근) • 시스템 콜 번호 기반 디스패치 SYSRET 실행 시 CPU 동작: • RIP←RCX, RFLAGS←R11 & ~SFMASK SYSCALL SYSRET MSR (Model Specific Registers) — syscall_init()에서 부팅 시 설정 LSTAR (0xC0000082): entry_SYSCALL_64 주소 — 64비트 SYSCALL 진입점 STAR (0xC0000081): CS/SS 셀렉터 기준값 — [63:48]=SYSRET용, [47:32]=SYSCALL용 SFMASK(0xC0000084): RFLAGS 클리어 마스크 — SYSCALL 시 TF/DF/IF/IOPL/AC/NT 등 자동 클리어 CSTAR (0xC0000083): entry_SYSCALL_compat 주소 — 32비트 Compatibility Mode 진입점 ⚠ RFLAGS &= ~SFMASK — SYSCALL 명령어가 하드웨어 수준에서 자동 적용 (IF 클리어 → 인터럽트 즉시 차단) ※ Non-canonical RCX로 SYSRET 시 #GP가 Ring 0에서 발생 — 리눅스는 canonical 검사 후 비정상 시 IRET 경로로 우회
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);
}
SYSRET 보안 주의사항 (Non-canonical RIP): 사용자 공간이 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를 갱신합니다.

Long Mode TSS 구조 (최소 104 bytes) 오프셋 크기 필드 설명 0x00 4B Reserved 예약 (0으로 초기화) 0x04 8B RSP0 Ring 0 스택 (커널 스택 최상단) 0x0C 8B RSP1 Ring 1 스택 (리눅스 미사용) 0x14 8B RSP2 Ring 2 스택 (리눅스 미사용) 0x1C 8B Reserved 예약 0x24 ~ 0x5B 7×8B IST1 ~ IST7 (각 8바이트) IST1: #DF Double Fault IST2: NMI IST3: #MC IST4: #DB/#BP 0x5C 10B Reserved 예약 0x66 2B IOPB Offset I/O 권한 비트맵 오프셋 (0x68 = 비사용) 총 크기: 최소 104 (0x68) 바이트 RSP 스택 포인터 IST 포인터 IOPB 오프셋
/* 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)); */
리눅스 TSS 사용 방식: 리눅스는 Ring 1/2를 사용하지 않아 RSP1/RSP2는 항상 0입니다. RSP0만 실제로 활용되며, 태스크 전환(context switch)마다 새 태스크의 커널 스택 주소로 갱신됩니다. IST 스택은 cpu_entry_area에 고정 할당되어, 커널 스택 오버플로우 상황에서도 안전하게 예외를 처리할 수 있습니다.

Long Mode 특화 기능 상세

Long Mode는 단순히 64비트 레지스터와 넓은 주소 공간만 제공하는 것이 아닙니다. Canonical 주소 규칙, REX 프리픽스, FS/GS Base MSR, RIP-relative 주소 지정 등 시스템 소프트웨어가 반드시 이해해야 할 고유 메커니즘을 포함합니다.

Canonical 주소 구조 (4-레벨 페이징, 48비트 VA) Sign Extension [63:48] bit[47]의 복사 (모두 0 또는 모두 1) 유효 가상 주소 [47:0] PML4 → PDPT → PD → PT → 페이지 오프셋 63 48 47 0 Canonical 범위 (4-레벨 페이징): 하위 캐노니컬: 0x0000000000000000 ~ 0x00007FFFFFFFFFFF (유저 공간) 상위 캐노니컬: 0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF (커널 공간) Non-Canonical: 0x0000800000000000 ~ 0xFFFF7FFFFFFFFFFF → #GP(0) 발생 REX 프리픽스 (1바이트: 0x40~0x4F) 0100 고정 (bits 7:4) W bit 3: 64bit 오퍼랜드 R bit 2: ModRM.reg 확장 X bit 1: SIB.index 확장 B bit 0: rm/SIB.base 확장 7 4 3 2 1 0 예: REX.W (0x48) + 0x89 0xC8 = mov rax, rcx | REX.RXB (0x4F) = r8~r15 전체 접근 가능
MSR 주소이름용도커널 접근
0xC0000100FS.BaseFS 세그먼트 베이스 (사용자 TLS)arch_prctl(ARCH_SET_FS)
0xC0000101GS.BaseGS 세그먼트 베이스 (커널 per-CPU)wrmsr(MSR_GS_BASE, ...)
0xC0000102KernelGS.BaseSWAPGS로 교체될 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 인코딩)
Non-canonical 주소와 SWAPGS 보안 이슈: Non-canonical 주소(예: 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
실무 팁: 아키텍처 차이를 비교할 때는 기능 단위(예: syscall, page fault, interrupt)로 파일을 나란히 열어 공통점/차이점을 표로 정리하면 코드 리뷰와 포팅 작업 속도가 크게 올라갑니다.

커널 아키텍처와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.