커널 아키텍처 (Kernel Architecture)

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

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

일상 비유 ② 특권 레벨 관점: 커널 아키텍처는 층별 출입 카드 시스템이 있는 건물과 같습니다. 일반 직원(사용자 애플리케이션)은 자기 층(User Space)만 접근할 수 있고, 건물 관리 시스템(커널)만 전체 층(Kernel Space)과 기계실(하드웨어)에 접근할 수 있습니다. x86_64·ARM64·RISC-V는 각각 이 출입 권한 체계를 다른 방식으로 구현한 것입니다.

핵심 요약

  • 특권 레벨 — 커널(Ring 0 / EL1 / S-mode)과 사용자 앱(Ring 3 / EL0 / U-mode)의 권한 경계를 이해합니다. 이 경계가 OS 보안과 안정성의 뼈대입니다.
  • 주소 공간 분리 — 가상 주소(Virtual Address) 공간이 커널 영역과 사용자 영역으로 나뉘는 구조를 파악합니다. 같은 가상 주소라도 페이지 테이블이 다르면 전혀 다른 물리 주소를 가리킵니다.
  • ISA 설계 철학 차이 — x86_64(CISC·강한 메모리 순서), ARM64(RISC·약한 메모리 순서·TrustZone), RISC-V(오픈 모듈형 ISA)의 핵심 차이를 구분합니다.
  • 부팅 단계 분리 — 펌웨어(Firmware), 부트로더(Bootloader), 커널 초기화 경계를 구분합니다.
  • 하드웨어 기술 정보 — ACPI/DT 등 기술 정보가 어디서 소비되는지 확인합니다.
  • 신뢰 체인(Chain of Trust) — Secure Boot 등 검증 체인을 흐름으로 이해합니다.
  • 실패 지점 식별 — 부팅 로그에서 단계별 실패 단서를 빠르게 찾습니다.

단계별 이해

  1. 커널/사용자 공간 경계 파악
    코드가 어느 공간에서 실행되는지, 특권 레벨 전환(시스템 콜·인터럽트)이 언제 발생하는지 이해합니다.
  2. 대상 아키텍처의 특권 모델 확인
    x86_64(Ring), ARM64(EL), RISC-V(M/S/U-mode) 중 어느 것인지 파악하고, 커널이 어느 레벨에서 실행되는지 확인합니다.
  3. 부팅 단계 식별
    현재 이슈가 펌웨어·부트로더·커널 초기화 중 어느 단계에서 발생하는지 먼저 고정합니다.
  4. 전환 경계 검증
    단계 간 인자 전달과 상태 인계(부팅 파라미터, 페이지 테이블 활성화 시점 등)를 추적합니다.
  5. 플랫폼별 재검증
    메모리 순서 모델, IOMMU, 인터럽트 컨트롤러 등 플랫폼 의존 요소가 다른 하드웨어 조건에서도 올바르게 동작하는지 확인합니다.

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

왜 커널 아키텍처를 알아야 할까요? 같은 C 코드라도 x86_64에서 문제없이 동작하던 코드가 ARM64에서 데이터 경쟁 버그로 이어지거나, RISC-V에서는 존재하지 않는 확장 명령어에 의존해 빌드 오류가 발생할 수 있습니다. 커널 드라이버·서브시스템 코드를 이식하거나, 부팅 실패를 디버깅하거나, 특권 레벨 경계를 넘는 보안 취약점을 분석할 때 아키텍처 지식이 없으면 근본 원인에 도달하기 어렵습니다. 이 문서는 세 아키텍처 각각의 특권 모델·메모리 주소 공간·부팅 경로·레지스터 체계를 커널 코드와 연결하여 설명합니다.

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

리눅스 커널 개요 (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)" 커널이라고도 합니다. 커널 모듈에 대한 자세한 내용은 커널 모듈 문서를 참고하세요.

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

x86_64 아키텍처 (x86_64 Architecture)

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

부팅 과정

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 문서, 부팅 과정 전반은 부팅 과정 문서를 참고하세요.

커널의 x86_64 엔트리 포인트는 어셈블리(Assembly)로 작성되어 있습니다. 다음은 핵심 부팅 코드의 간략화된 예시입니다:

/* 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) Stack grows ↓ Heap grows ↑
x86_64 가상 주소 공간 레이아웃 - 커널 공간(Kernel Space)(상위 128TB)과 사용자 공간(하위 128TB)

아키텍처별 주소 변환(Address Translation) 경로 비교

가상 주소(Virtual Address)를 물리 주소(Physical Address)로 변환하는 핵심 경로는 세 아키텍처 모두 유사하지만, 제어 레지스터(Register)와 페이지 테이블 포맷이 다릅니다. 아래 다이어그램은 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 코드/데이터/테스크 세그먼트 세그먼트 레지스터 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 PUD 512 entries PMD 512 entries PTE 512 entries Page Frame 4KB page 주소 변환 공식 물리주소 = (PTE가 가리키는 4KB 프레임) + (오프셋 12bit) 최대 물리 주소: 52bit (4PB), 페이지 크기: 4KB/2MB/1GB

링 구조 (Protection Rings)

x86_64는 4개의 특권 레벨(Ring 0 ~ Ring 3)을 제공하지만, 리눅스에서는 실제로 Ring 0(커널 모드)Ring 3(사용자 모드) 두 레벨만 사용합니다. Ring 1과 Ring 2는 사용되지 않으며, 가상화(Virtualization) 확장(VT-x)에서는 Ring -1(VMX root mode)이라는 개념이 추가됩니다.

MSR (Model-Specific Register)

MSR은 프로세서 모델별로 정의되는 특수 레지스터로, 성능 카운터, 전원 관리, 터보 부스트, virtualization 기능 등을 제어합니다. 리눅스 커널은rdmsr/wrmsr 명령어로 MSR에 접근합니다.

MSR 주소이름용도커널 활용
0x10TSCTime Stamp Counter고정밀 타이머
0x1A2IA32_MISC_ENABLE여러 Misc 기능turbo boost, fast string
0x1A4IA32_PERF_CTLPerformance ControlP-상태 제어
0xC0000080EFERExtended Feature EnableLong Mode, NX, SYSCALL
0xC0000100STARSYSCALL Targetsyscall CS/SS
0xC0000101LSTARLong Mode SYSCALL64-bit syscall target
0xC0000102CSTARCompat SYSCALL32-bit compat target
0xC0000103FMASKSYSCALL RFLAGS masksyscall flags mask
0xC0000104FS.baseFS Base Addressper-CPU data
0xC0000105GS.baseGS Base Addressper-CPU kernel
0x0000013B~0x0000013DPMC0~2Performance Counterperf_event
/* arch/x86/include/asm/msr.h - MSR 접근 매크로 */

/* MSR 읽기 */
static inline u64 rdmsrl(unsigned int msr)
{
    u64 val;
    asm volatile("rdmsr" : "=A"(val) : "c"(msr));
    return val;
}

/* MSR 쓰기 */
static inline void wrmsrl(unsigned int msr, u64 val)
{
    asm volatile("wrmsr" : : "c"(msr), "A"(val));
}

/* 시스템 콜 진입 MSR 설정 예시 */
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
wrmsrl(MSR_FMASK, 0);
wrmsrl(MSR_STAR, ((u64)__KERNEL_CS << 32) | (__USER_CS << 16));
MSR 접근 주의: 일부 MSR은 특권 레벨(Ring 0)에서만 접근 가능합니다. 유저 공간에서rdmsr을 시도하면 #GP 예외가 발생합니다. 커널 드라이버에서는wrmsr()을 사용하기 전에capable(CAP_SYS_RAWIO)로 권한을 검사해야 합니다.

x86_64 시스템 콜 Internals

x86_64 시스템 콜은SYSCALL 명령어로 진입하며, MSR에 설정된 핸들러 주소로 점프합니다. 복귀는SYSRET 명령어로 수행됩니다.

/* arch/x86/entry/entry_64.S - syscall 진입 흐름 (핵심 부분) */

/* 사용자 공간에서 SYSCALL */
    syscall                    /* RAX=syscall 번호, RDI, RDI, RDX, RSI, R8, R9 = 인자 */
                                /* RIP → MSR_LSTAR에 저장된 주소 */

entry_SYSCALL_64:
    /* 레지스터 저장 (pt_regs 구조체) */
    push   %rax           /* RAX 저장 (syscall 번호) */
    cld
    push   %r15
    push   %r14
    push   %r13
    push   %r12
    push   %r11           /* RFLAGS 저장 */
    push   %r10
    push   %r9
    push   %r8
    push   %rcx           /* RIP 저장 (복귀 주소) */
    push   %r11          /* RFLAGS 재저장 */
    push   %rdx
    push   %rsi
    push   %rdi
    push   %rbp
    push   %rax           /* syscall 번호 */

    /* GS 기반 per-CPU 접근 시작 */
    SWAPGS                /* GS → 커널 GS로 교체 */

    /* 시스템 콜 테이블에서 핸들러 호출 */
    mov    %rsp, %gs:thread_struct.sp0  /* 커널 스택 설정 */
    mov    %rsp, %rsp
    test   $3, %rsp          /* Ring 3에서 호출? */
    jz     1f
    swapgs
1:
    and    $-16, %rsp
    mov    %rsp, %gs:cpu_tss.x86_tss.sp1
    leaq   -RED_ZONE_SIZE(%rsp), %rsp
    push   %rsp
    push   %gs:cpu_tss.x86_tss.sp0

    /* syscall 테이블 호출 */
    mov    %rax, %rsp
    sub    $SIZEOF(pt_regs), %rsp
    mov    %rax, %rsp        /* pt_regs 빌드 */
    mov    %rax, %rsp, %rdi
    call   do_syscall_64
    /* 복귀 */
    ret

syscall_return:
    SWAPGS
    sysretq              /*Ring 3 복귀, RIP=RCX, RFLAGS=RDX */

x86_64 인터럽트 처리

x86_64의 인터럽트 처리는 IDT(Interrupt Descriptor Table)과 LAPIC(Local APIC)가 담당합니다. 예외, IRQ, NMI 모두 IDT 게이트를 통해 처리됩니다.

게이트 유형선택자용도
Task Gate0x5하드웨어 태스크 전환 (거의 미사용)
Interrupt Gate0x6예외/인터럽트, IF 자동 Clear
Trap Gate0x7예외만 사용, IF 유지
/* arch/x86/include/asm/desc.h - IDT 게이트 구조 */

struct gate_struct {
    u16 offset_low;         /* offset [15:0] */
    u16 selector;           /* 코드 세그먼트 셀렉터 */
    u8  ist;                /* Interrupt Stack Table */
    u8  type_attr;         /* Type (0x6=Interrupt, 0x7=Trap) | DPL | P */
    u16 offset_mid;       /* offset [31:16] */
    u32 offset_high;       /* offset [63:32] */
    u32 reserved;       /* 보존 */
} __attribute__((packed));

/* IRQ 벡터 할당 */
#define FIRST_EXTERNAL_VECTOR    0x20  /* IRQ0 */
#define FIRST_SYSTEM_VECTOR  0xE0  /* Local APIC */
#define GLOBAL_INTR_VECTORS    224    /* IRQ0~15 + IOAPIC */

VMX (Virtualization) Internals

VMX는 Intel의 하드웨어 가상화 확장입니다. VMX root mode(Ring -1)와 VMX non-root mode(게스트)를 지원합니다.

/* arch/x86/kvm/vmx.h - VMX 주요 MSR */

#define MSR_IA32_VMX_BASIC             0x480
#define MSR_IA32_VMX_PINCTRL         0x481
#define MSR_IA32_VMX_PROCCTRL        0x482
#define MSR_IA32_VMX_EXITCTRL       0x483
#define MSR_IA32_VMX_ENTRYCTRL        0x484

/* VMCS 필드 */
#define VMCS_GUEST_RIP               0x00001602
#define VMCS_GUEST_RSP               0x00001603
#define VMCS_GUEST_CR3               0x00001602
#define VMCS_HOST_CR3               0x00001902
#define VMCS_HOST_RSP               0x00001903
#define VMCS_HOST_RIP               0x00001904

/* VMX 실행 상태 (VMCS field: VMCS_GUEST_ACTIVITY_STATE) */
#define VMX_GUEST_STATE_ACTIVE       0
#define VMX_GUEST_STATE_HLT          1
#define VMX_GUEST_STATE_SHUTDOWN      2
#define VMX_GUEST_STATE_WAIT_SIPI     3
VMX VM-Exit 처리 흐름 VM-Exit 발생 (예: INTR, HLT, I/O) VMCS 업데이트 exit reason, qualifiers KVM 핸들러 kvm_handle_exit() VM-Entry VMLAUNCH/VMRESUME 일반 VM-Exit 유형 EXCEPTION_NMI EXTERNAL_INTERRUPT Triple_Fault HLT CPUID MSR_READ MSR_WRITE IO_INSTRUCTION RDTSC
/* 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에서 시스템 콜(System Call)은 SYSCALL 명령어를 사용합니다. 이 명령어는 Ring 3에서 Ring 0으로 전환하며, MSR_LSTAR 레지스터에 저장된 시스템 콜 핸들러(Handler) 주소로 점프합니다. SYSRET으로 사용자 모드로 복귀합니다. 이전의 int 0x80 방식보다 훨씬 빠릅니다. 시스템 콜에 대한 자세한 내용은 시스템 콜 (System Call) 문서를 참고하세요.

ARM64 아키텍처 (ARM64 / AArch64 Architecture)

ARM64(AArch64)는 모바일 기기부터 서버, 슈퍼컴퓨터까지 빠르게 확산되고 있는 아키텍처입니다. Apple Silicon(M1/M2/M3/M4), AWS Graviton4, Ampere Altra/AmpereOne 등 고성능 ARM64 프로세서가 서버 시장에서도 중요한 위치를 차지하고 있습니다.

ARM64는 x86_64와 비교해 세 가지 설계 철학의 차이를 먼저 이해해야 합니다. 첫째, RISC 기반으로 명령어 수가 적고 길이가 고정(4바이트)되어 있어 파이프라인 설계가 단순합니다. 둘째, x86의 Ring 0~3 대신 Exception Level(EL0~EL3)이라는 4단계 모델을 사용하며, TrustZone(EL3)으로 보안 세계와 일반 세계를 하드웨어 수준에서 격리합니다. 셋째, 메모리 순서가 약한 모델(Weak Ordering)이어서 명시적 배리어 없이는 멀티코어 환경에서 쓰기 순서가 보장되지 않습니다. x86에서 포팅한 코드에 배리어가 빠져 있어도 x86 하드웨어의 강한 메모리 순서 덕분에 동작하던 코드가, ARM64에서는 산발적인 데이터 경쟁 버그로 나타나는 것이 가장 흔한 포팅 실수입니다.

Exception Level (EL0 ~ EL3)

ARM64는 x86의 Ring 구조 대신 Exception Level(EL)이라는 4단계 특권 모델을 사용합니다. 각 EL은 명확한 역할을 가지며, 하위 EL에서 상위 EL로의 전환은 exception 발생 시에만 가능합니다.

ARM64 시스템 레지스터

ARM64는 x86의 MSR과 유사하게MSR/MRS 명령어로 접근하는 시스템 레지스터를 사용합니다. 시스템 레지스터는SCTLR_ELx, TTBR0_EL1, TCR_EL1 등 EL별로 나뉩니다.

레지스터EL용도
SCTLR_EL1EL1System Control, MMU, 캐시 활성화
TTBR0_EL1EL1Translation Table Base 0 (User)
TTBR1_EL1EL1Translation Table Base 1 (Kernel)
TCR_EL1EL1TLB 제어, ASID 크기, 메모리 속성
MAIR_EL1EL1Memory Attribute Index Register
SPselEL0/1스택 포인터 선택
CurrentELAll현재 Exception Level (읽기 전용)
DAIFEL0/1인터럽트 마스크 (D/I/F)
TPIDR_EL0EL0사용자 스레드(Thread) ID
TPIDR_EL1EL1커널 TLS base
CNTV_CTL_EL0EL0가상 타이머 제어
CPACR_EL1EL1협프로세서 액세스
VBAR_EL1EL1Vector Base Address
/* ARM64 시스템 레지스터 접근 예시 */

/* MMU 활성화 (SCTLR_EL1.M = 1) */
    mrs    x0, sctlr_el1
    orr    x0, x0, #1        /* M 비트 */
    msr    sctlr_el1, x0

/* 페이지 테이블 베이스 설정 (TTBR0_EL1) */
    msr    ttbr0_el1, x0

/* 인터럽트 허용 (DAIF 클리어) */
    msr    daif, #0

/* 현재 EL 확인 */
    mrs    x0, currentel
    lsr    x0, x0, #2        /* 0=EL0, 1=EL1, 2=EL2, 3=EL3 */

GIC (Generic Interrupt Controller)

ARM64 시스템의 인터럽트 컨트롤러는 GIC(Generic Interrupt Controller)입니다. GICv2, GICv3, GICv4 버전이 있으며, 버전별로 프로그래밍 인터페이스가 다릅니다.

버전주요 특징커널 드라이버
GICv2최대 8 CPUs, legacy PPI/SPIirq-gic-v2.c
GICv3Redistributor, LPI, MSI 지원, 480+ CPUsirq-gic-v3.c
GICv4가상화 직렬화(Serialization), vPE tableirq-gic-v4.c
GICv4.1Extended LPI range, hierarchical cacheirq-gic-v4.1.c
/* drivers/irqchip/irq-gic-v3.c - GICv3 초기화 핵심 */

/* Redistributor베이스 주소 */
#define GICR_CTLR        0x0
#define GICR_TYPER        0x08
#define GICR_PIDR2       0xFE8

/* Interrupt ID Ranges */
#define GIC_SPI_OFFSET     32    /* SPI: 32~1019 */
#define GIC_PPI_OFFSET      16    /* PPI: 16~31 */
#define GIC_SGI_OFFSET      0      /* SGI: 0~15 */

/* GICD_IROUTERn - Interrupt Routing Register */
/* Each SPI routing to target redistributors */

static int gic_set_affinity(struct irq_data *d, const cpumask *mask, bool force)
{
    void __iomem *reg = gic_dist_base(d) + GIC_DIST_TARGET + irq(d);
    u32 target = cpu_logical_map(cpumask_first(mask));
    writel_relaxed(target, reg);
    return IRQ_SET_MASK_OK;
}

ARM64 KVM 가상화

ARM64에서 KVM은 하이퍼바이저로 동작하며, EL2에서 실행됩니다.CONFIG_KVMCONFIG_ARM64_VHE 옵션이 있습니다.

/* arch/arm64/kvm/arm.c - KVM 초기화 */

static int kvm_init(void)
{
    int ret;

    /* Hyp 시스템 레지스터 초기화 */
    kvm_linux_set_id_aa64pfr0(0);
    kvm_linux_set_id_aa64pfr1(0);
    kvm_linux_set_id_aa64zfr0(0);

    /* 2단계 페이지 테이블 활성화 */
    if (has_vhe()) {
        kvm_init_el2_stuff();
    }

    return 0;
}

/* VM 생성 시 2단계 페이지 테이블 설정 */
static int kvm_init_stage2(struct kvm *kvm)
{
    phys_addr_t pgd = __get_free_pages(GFP_KERNEL, VTCR_SIZE_BITS);

    /* Hyp 레지스터에 2단계 테이블 베이스 설정 */
    write_sysreg(pgd, vtcr_el2);
    write_sysreg(pgd, vttbr_el2);
    return 0;
}

ARM64 보안 확장 (PAC, BTI, MTE, GCS)

ARM64는 다양한 하드웨어 기반 보안 확장을 제공합니다.

확장설명커널 지원
PAC (Pointer Authentication)포인터 인증 코드 (PAC), 반환 주소 무결성(Integrity)CONFIG_ARM64_POINTER_AUTH
BTI (Branch Target Identification) indirect branch 보호CONFIG_ARM64_BTI
MTE (Memory Tagging Extension)메모리 태그로 버퍼 오버플로(Buffer Overflow) 감지CONFIG_ARM64_MTE
GCS (Guarded Control Stack)ROP 공격 방어CONFIG_ARM64_GCS
CFI (Control-Flow Integrity)BTI 기반 간접 호출 검증BTI 의존
/* PAC (Pointer Authentication) 예시 */

/* PACIA 키 생성 (A 키 사용) */
    pacia  x0, x1, x2    /* x0 = x0 | PAC(x1, x2) */

/* PACIB (B 키 사용) */
    pacib  x0, x1, x2

/* AUTIA 복호화 검증, 실패 시 BRK */
    autia  x0, x1, x2

/* BTI (Branch Target Identification) */
    /* BTI 테스트 */
    bti    "c"           /* Call-friendly target */
    bti    "j"           /* Jump-friendly target */
    bti    "i"           /* Indirect branch target */
    bti    "c,j"         /* Both */

/* MTE (Memory Tagging) */
    irg    x0, x1           /* Allocate random tag in x0, base in x1 */
    stg    x0, [x1]        /* Store tag with data */
    ldg    x2, [x1]        /* Load tag to x2 */
    cfinv  x0              /* Compare and invalidate if mismatch */
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 부팅 과정

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 문서를 참고하세요.

ARM64 메모리 순서 모델 (Memory Ordering Model)

ARM64는 x86_64와 달리 약한 메모리 순서 모델 (Weak Consistency Model)을 채택합니다. CPU는 메모리 읽기/쓰기 연산의 실행 순서를 하드웨어 레벨에서 보장하지 않으며, 컴파일러도 메모리 접근의 순서를 변경할 수 있습니다. 이 특성이 x86에서만 개발한 코드를 ARM64에 포팅할 때 산발적인 데이터 경쟁 버그(Data Race)로 나타나는 근본 원인입니다.

ARM64의 메모리 재순서화(Reordering)는 세 가지 차원에서 발생할 수 있습니다:

이러한 재순서화를 방지하기 위해 ARM64는 메모리 배리어(Memory Barrier) 명령어를 제공합니다. 배리어는 종류에 따라 범위가 다르며, 각 배리어는 다른 상황에서 사용됩니다:

명령어종류의미커널 매크로
DMBData Memory Barrier데이터 메모리 접근 순서 보장smp_wmb(), smp_rmb(), smp_mb()
DSBData Synchronization Barrier이전 모든 메모리 접근 완료 후 다음 명령 실행mb() (완전 배리어)
ISBInstruction Synchronization Barrier파이프라인 플러시 후 다음 명령 가져옴명령어 변경 후 사용 (예: branch prediction update)
/* ARM64 메모리 배리어 명령어 예시 */

/* DMB ISH — Inner Shareable Domain Data Memory Barrier */
      dmb      ish         /* 공유 메모리 접근 순서 보장 */

/* DMB SY — Full System Barrier (가장 강함) */
      dmb      sy          /* 모든 메모리 접근 순서 보장 */

/* DSB — Data Synchronization Barrier */
      dsb      sy          /* DMB는 접근 순서만, DSB는 이전 모든 접근 완료 보장 */

/* ISB — Instruction Synchronization Barrier */
      isb                  /* 파이프라인 플러시, 명령어 변경 후 사용 */

커널 코드에서 가장 중요한 배리어는 dmbsy()입니다. 이는 SMP 환경에서 모든 CPU 코어 간의 메모리 순서를 보장하는 가장 강한 배리어입니다. arch/arm64/include/asm/barrier.h에서 정의됩니다:

/* arch/arm64/include/asm/barrier.h */

/* ARM64 배리어 매크로 — DMB SY는 가장 강함 */
#define mb()     __asm__ __volatile__ ("dmb sy" : : : "memory")
#define rmb()    __asm__ __volatile__ ("dmb ishld" : : : "memory")
#define wmb()    __asm__ __volatile__ ("dmb ishst" : : : "memory")

/* SMP 배리어 — 공유 메모리 접근 순서만 보장 */
#define smp_mb()   __asm__ __volatile__ ("dmb ish" : : : "memory")
#define smp_rmb() __asm__ __volatile__ ("dmb ishld" : : : "memory")
#define smp_wmb() __asm__ __volatile__ ("dmb ishst" : : : "memory")

/* DSB SY — 완전 동기화 배리어 */
#define dsb_sy() __asm__ __volatile__ ("dsb sy" : : : "memory")

/* ISB — 파이프라인 플러시 */
#define isb()   __asm__ __volatile__ ("isb" : : : "memory")

x86_64는 Strong Ordering 모델이므로 mb()는 컴파일러 배리어만 발생시키지만, ARM64에서는 실제 CPU-level 배리어 명령어로 확장됩니다. 따라서 x86-only로 작성된 코드에서 배리어를 생략한 코드는 ARM64에서 데이터 경쟁(Data Race)으로 연결될 수 있습니다. 반드시 아키텍처 독립 배리어 매크로를 사용해야 합니다:

x86 → ARM64 포팅 시 주의: x86에서 배리어 없이 동작하던 코드는 ARM64에서 재순서화로 인해 버그가 발생할 수 있습니다. atomic_*(), READ_ONCE(), WRITE_ONCE() 매크로를 사용하여 컴파일러 재순서화도 함께 방지합니다. Linux 커널의 include/linux/compiler.hinclude/linux/atomic.h를 참고하세요.
/* include/linux/compiler.h — READ_ONCE / WRITE_ONCE */

/* 컴파일러 재순서화 방지: 단일 atomic 로드/저장 강제 */
#define READ_ONCE(x) \
    (__typeof__(x))__atomic_load_n(&(x), __ATOMIC_SEQ_CST)

#define WRITE_ONCE(x, v) \
    __atomic_store_n(&(x), (v), __ATOMIC_SEQ_CST)

/* 예: flags 변수의 safe access */
static void safe_flag_clear(volatile unsigned long *flags)
{
      WRITE_ONCE(*flags, 0);
      smp_wmb();   /* flags 변경이 다른 메모리 쓰기보다 먼저 완료됨 */
}

static bool safe_flag_test(volatile unsigned long *flags)
{
      bool result = READ_ONCE(*flags);
      smp_rmb();   /* flags 읽기가 이후 메모리 읽기보다 먼저 완료됨 */
      return result;
}

ARM64 메모리 순서 모델은 또한 LoadLoad, LoadStore, StoreLoad, StoreStore 재순서화의 네 가지 조합을 각각 배리어로 제어할 수 있습니다. DMB SH는 모든 타입의 재순서화를 방지하지만, DMB Ishld는 LoadLoad만, Ishst는 StoreLoad만 방지합니다. 각 상황에 맞는 배리어를 선택하면 성능 최적화가 가능합니다.

MTE 커널 지원 (Memory Tagging Extension for Kernel)

ARMv8.5-A부터 도입된 MTE(Memory Tagging Extension)는 128바이트 블록 단위로 4비트 태그를 메모리 메타데이터에 저장하여, 버퍼 오버플로우, use-after-free, heap corrosion 등의 메모리 안전성 오류를 하드웨어 레벨에서 감지하는 기능입니다. Linux 커널 5.15부터 USER MTE를, 6.6부터 KERNEL MTE를 지원하기 시작했습니다.

Kconfig종류버전설명
CONFIG_ARM64_MTEUser MTE5.15+사용자 공간 MTE (MTE-capable ABI, prctl(PR_SET_TSC))
CONFIG_ARM64_KERNEL_MTEKernel MTE6.6+커널 힙 메모리 태그 검증 (SLAB/SLUB)
CONFIG_ARM64_MTE_SMESME MTE6.11+SVE/SME 확장 명령어와 MTE 통합

Kernel MTE가 활성화되면, 커널 힙 할당자(SLAB/SLUB allocator)는 각 객체(Object)에 의사난수 태그를 할당하고, 객체 접근 시 하드웨어가 태그를 검증합니다. 태그 불일치 시 Tag Check Fault가 발생하며, 이를 통해 버퍼 오버플로, use-after-free, heap corruption 등의 메모리 안전성 위반을 사전에 감지할 수 있습니다. MTE는 KASAN(Kernel Address Sanitizer)과 상호 보완적인 진단 수단입니다: KASAN이 더 큰 오버헤드를 가지지만 더 넓은 범위(스택, 전역 변수 포함)를 커버하는 반면, MTE는 하드웨어 기반이므로 런타임 오버헤드가 훨씬 작지만 힙 객체만 커버합니다.

/* arch/arm64/mm/fault.c — MTE Tag Check Fault 처리 */

/* MTE Tag Check Fault ESr bits */
#define ESR_ESC_OOSSRT        1    /* Out-of-range store */
#define ESR_ESC_OOSRLD        2    /* Out-of-range load */
#define ESR_ESC_GRANULE       3    /* Granule tag mismatch */
#define ESR_ESC_MISCON        4    /* Misconfig */

static void do_tag_check_fault(struct pt_regs *regs, unsigned long esr)
{
      unsigned int esc = esr_ec_code(esr);
      const char *type;

      /* Fault type identification */
      switch (esc) {
            case ESR_ESC_OOSSRT:
                  type = "out-of-bounds store";
                  break;
            case ESR_ESC_OOSRLD:
                  type = "out-of-bounds load";
                  break;
            case ESR_ESC_GRANULE:
                  type = "granule tag mismatch";
                  break;
            default:
                  type = "unknown MTE fault";
                  break;
      }

      pr_err("MTE tag check fault at %pX: %s\n",
            regs->pc, type);
      die_kernel_fault(regs, "MTE tag check fault");
}
/* ARM64 MTE 관련 명령어 */

/* IRG — Issue Random Tag: x1(base)의 주소에서 태그 추출 후 x0에 저장 */
      irg    x0, x1

/* GMI — Generate and Move Immediate: immediate 값을 태그로 설정 */
      gmi    x0, #5

/* MSTT — Move and Set Tag: x0의 태그를 [x1] 메모리 태그에 복사 */
      mstt   x0, [x1]

/* LDGT — Load Granule Tag: [x1]의 128바이트 블록 태그를 x0에 로드 */
      ldgt   x0, [x1]

/* STGT — Store Granule Tag: x2의 태그를 [x1]의 128바이트 블록에 저장 */
      stgt   [x1], x2

/* CFGM — Configure MTE: SCTLR_EL1.TG 를 설정하여 MTE 활성화/비활성화 */
      mrs    x0, sctlr_el1
      bfi    x0, x1, #20, #1    /* TG bit (bit 20) 설정 */
      msr     sctlr_el1, x0

/* CFINV — Compare and Invalidate: x0와 [x1]의 태그 비교, 실패 시 무효화 */
      cfinv  x0, [x1]

MTE를 커널 컴파일에 적용하려면 다음 두 가지 방법이 있습니다:

# MTE 커널 빌드 설정

# .config에 MTE 활성화
make menuconfig
# → ARM64 capabilities → [*] Enable KernelMTE support

# 또는 커맨드 라인에서 설정
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \
     CONFIG_ARM64_MTE=y CONFIG_ARM64_KERNEL_MTE=y

# MTE 활성화 부트 파라미터 추가 (GRUB 또는 Device Tree)
# /etc/default/grub — GRUB_CMDLINE_LINUX_DEFAULT 에 추가
mte=on      # 태그 검사+생성 모두 활성화 (기본)
mte=koff    # 커널 MTE 비활성화
mte=ukon    # 사용자 MTE만 활성화

# MTE 상태 확인
cat /sys/kernel/debug/mte/state
cat /proc/cpuinfo | grep "mem_tag"

MTE는 6.12~6.14 릴리스 사이에도 여러 개선이 적용되었습니다. 6.12에서는 MTE + KASAN 통합이 개선되었고, 6.13에서는 SLAB/SLUB 할당자에서 MTE 할당 overhead 최적화가 적용되었으며, 6.14에서는 SME(Scalable Matrix Extension)와의 통합이 강화되었습니다.

MTE vs KASAN 비교: MTE는 하드웨어 기반이므로 KASAN 대비 오버헤드가 훨씬 작지만 (MTE: ≈5-15%, KASAN: ≈30-50%), KASAN은 스택 버퍼 오버플로, 전역 변수, shadow memory 기반 디버깅을 포함해 훨씬 넓은 범위의 메모리 오류를 감지합니다. 둘을 함께 사용하면 가장 포괄적인 메모리 안전성 검사가 가능합니다.

MTE 커널 사용 시 Tag Check Fault가 발생하면 커널은 해당 faults의 종류에 따라 다음 중 하나를 수행합니다:

MTE의 태그는 128바이트(Granule) 단위로 할당되며, 4비트 태그이므로 2^4 = 16개의 서로 다른 태그값을 가질 수 있습니다. 커널 MTE는 SLUB 할당자(SLAB allocator)와 연동되어 각 객체에 대해 의사난수 기반의 고유 태그값을 할당합니다.

RISC-V 아키텍처 (RISC-V Architecture)

RISC-V는 UC Berkeley에서 설계한 오픈 소스 ISA(Instruction Set Architecture)로, 라이센스 비용 없이 누구나 자유롭게 구현할 수 있습니다. 리눅스 커널은 RISC-V를 공식적으로 지원하며, SiFive, StarFive, SpacemiT K1 등의 칩에서 이미 리눅스가 동작합니다.

특권 모드 (Privilege Modes)

RISC-V는 3단계 특권 모드를 정의합니다. 각 모드는 CSR(Control and Status Register) 접근 권한이 다릅니다.

RISC-V CSR (Control and Status Register)

RISC-V는 특권 모드별로 다른 CSR을 사용합니다. M-mode, S-mode, U-mode 각각 접근 가능한 CSR이 정의됩니다.

CSR모드설명
mstatusMMachine Status, MIE, MPIE 등
mieMMachine Interrupt Enable
mtvecMMachine Trap Vector Base
mepcMMachine Exception PC
mcauseMMachine Cause (예외 번호)
mtvalMMachine Trap Value (주소 등)
sstatusSSupervisor Status
sieSSupervisor Interrupt Enable
stvecSSupervisor Trap Vector
sepcSSupervisor Exception PC
scauseSSupervisor Cause
stvalSSupervisor Trap Value
satpSSupervisor Address Translation and Protection
scounterenSSupervisor Counter Enable
/* RISC-V CSR 접근 예시 */

/* sstatus 읽기 */
    csrr   t0, sstatus

/* sstatus의 SIE 비트(Set Interrupt Enable) */
    csrs    sstatus, 0x2

/* sstatus의 SIE 비트 클리어 */
    csrc    sstatus, 0x2

/* sbadir 설정 (Supervisor Binary Interface) */
    csrw    sbadir, t0

/* satp (주소 변환 테이블) 설정 - Sv48의 경우 */
    /* PPN[43:0] = 페이지 테이블 물리 프레임 번호 */
    csrw    satp, t0

RISC-V 페이지 테이블 (Sv39/Sv48/Sv57)

RISC-V는 페이지 테이블 체계를 ISA에서 명시하지 않고 Sv39, Sv48, Sv57 등의 "명명 규약"으로 정의합니다. 리눅스는 Sv48 (4-level)을 주로 사용합니다.

/* arch/riscv/include/asm/pgtable.h - 페이지 테이블 형식 */

#define PTE_SHIFT     10       /* PTE Bits 수 */
#define PTE_PFN_SHIFT 10       /* PFN 시작 비트 */
#define PTE_VALID    _(1 << 0)
#define PTE_READ    _(1 << 1)      /* U=0: Supervisor, U=1: User */
#define PTE_WRITE   _(1 << 2)      /* Write */
#define PTE_EXEC    _(1 << 3)      /* Execute */
#define PTE_USER     _(1 << 4)      /* User accessible */
#define PTE_GLOBAL   _(1 << 5)      /* Global */
#define PTE_ACCESS   _(1 << 6)      /* Accessed */
#define PTE_DIRTY    _(1 << 7)      /* Dirty (Svadu 확장) */

/* Sv48 4-level 페이지 테이블 */
/* VA [47:39] → PML4[511:0] → PDPT[511:0] → PD[511:0] → PT[511:0] → 4KB page */

CLINT와 PLIC (인터럽트 컨트롤러)

RISC-V의 인터럽트는 두 가지 유형으로 나뉩니다:

/* drivers/irqchip/irq-sifive-plic.c - PLIC */

/* PLIC IRQ 소스 */
#define PLIC_ENABLE_OFFSET      0x1000
#define PLIC_THRESHOLD_OFFSET 0x200000
#define PLIC_CLAIM_OFFSET       0x200004

/* IRQ 소스 설정 예시 */
static void plic_toggle(struct irq_data *d, unsigned enable)
{
    u32 mask = 1UL << (irq(d) % 32);
    void __iomem *reg = plic_regs + PLIC_ENABLE_OFFSET +
                       (irq(d) / 32) * 4;
    if (enable)
        writel(readl(reg) | mask, reg);
    else
        writel(readl(reg) & ~mask, reg);
}

/* IRQ 클레임 (CLAIM) - 인터럽트 핸들링 */
static void plic_handle_irq(void)
{
    u32 irq;
    while ((irq = readl(plic_regs + PLIC_CLAIM_OFFSET))) {
        generic_handle_domain_irq(irq_domain, irq);
        writel(irq, plic_regs + PLIC_CLAIM_OFFSET);
    }
}

SBI 확장

SBI는 다양한 확장을 정의합니다. 표준 확장 목록:

확장 ID이름기능
0x10base기본 SBI (getchar, putchar)
0x54494D45time타이머 설정 (gettimeofday)
0x735049ipiInter-Processor Interrupt
0x52464E43rfenceRemote fence (TLB flush)
0x48534DhsmHart State Management (부팅/종료)
0x505343pscPerformance Monitoring Counter
0x444D4EdmDebug Channel (디버그)
0x534353scsGuest Interrupt Controller
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 호출을 통해 타이머(Timer) 설정, 프로세서 간 인터럽트(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 부팅 과정

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() → 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)

운영체제가 여러 프로그램을 동시에 실행할 때, 한 프로그램이 다른 프로그램의 메모리를 덮어쓰거나 하드웨어를 직접 제어하면 시스템 전체가 무너집니다. 이를 막기 위해 CPU는 실행 코드가 "얼마나 많은 권한을 갖느냐"를 하드웨어 수준에서 구분합니다 — 이것이 특권 레벨(Privilege Level)입니다.

특권 레벨이 낮은 코드(사용자 애플리케이션)는 직접 하드웨어에 접근하거나 다른 프로세스의 메모리를 읽으려 하면 CPU가 즉시 예외(Exception)를 발생시켜 커널에 제어권을 넘깁니다. 커널은 특권 레벨이 가장 높은 곳에서 실행되며, 이 경계가 OS의 보안과 안정성의 근본입니다. 세 아키텍처는 이 개념을 각기 다른 이름과 계층 수로 구현합니다:

아키텍처별 특권 레벨 비교 (Privilege Level Comparison) x86_64 ARM64 (AArch64) RISC-V 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 굵은 테두리 = 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) 문서를, 스케줄러(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(진입/인터페이스) 플랫폼 코어 I/O 드라이버 인터페이스 유틸리티
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
인터럽트 컨트롤러(Interrupt Controller) APIC (LAPIC + I/O APIC) GIC (v2/v3/v4) PLIC / APLIC+IMSIC
가상화 VT-x / AMD-V EL2 + VHE H-extension
하드웨어 보안 CET Shadow Stack, SMEP/SMAP MTE, PAC, BTI, GCS (v6.13) PMP, Svade/Svadu (v6.13)
다음 단계: 커널 아키텍처의 기본 개념을 이해했다면, 다음으로 빌드 시스템 (Build System) 문서에서 실제로 커널을 빌드하는 방법을 학습하거나, 메모리 관리 문서에서 각 아키텍처의 페이지 테이블과 메모리 할당 메커니즘을 심층적으로 살펴볼 수 있습니다. 또한 부팅 과정 문서에서 각 아키텍처별 부팅 흐름을 더 자세히 확인할 수 있습니다.

아키텍처별 성능 최적화 (Architecture-Specific Performance Optimization)

각 아키텍처는 고유한 성능 특성과 최적화 포인트를 가지고 있습니다. 커널 개발자는 타겟 아키텍처의 특성을 이해하고 이에 맞는 최적화를 적용해야 합니다.

x86_64 최적화 포인트

x86_64는 강력한 OoOE(Out-of-Order Execution, 비순서 실행)와 넓은 SIMD 레지스터(AVX-512, AVX2, SSE)를 갖추고 있지만, 그 복잡성만큼 잘못된 사용이 성능을 오히려 깎는 경우도 많습니다. 대표적인 함정은 캐시 라인 경계를 무시한 데이터 배치(False Sharing)와, likely()/unlikely() 없이 작성된 분기 코드입니다. 아래 기법들은 "기본값을 지키면 손해 없다"는 관점에서 핵심만 정리한 것입니다.

캐시 아키텍처와 False Sharing 방지

x86_64 CPU는 일반적으로 L1(32KB~64KB), L2(256KB~1MB), L3(Shared, 여러 MB~수십 MB)의 3단 캐시 계층 구조를 갖습니다. Cache Line(Cache Line) 기본 크기는 64바이트이며, 메모리 접근은 이 64바이트 단위(그레인, Granule)로 L1/L2/L3 캐시에 로드됩니다.

False Sharing은 두 개 이상의 CPU 코어가 서로 다른 변수를 접근하는데, 그 변수들이 같은 64바이트 캐시 라인 내에 배치될 때 발생하는 성능 저하입니다. 한 코어가 변수 A를 수정하면 MESIF/MOSI 일관성 프로토콜에 의해 다른 코어의 해당 캐시 라인이 무효화(Invalid)되어, 다른 코어가 변수 B에 접근할 때도 캐시 미스가 발생하게 됩니다.

/* ❌ 잘못된 예: False Sharing 발생 가능 */

/* hot과 cold가 같은 캐시 라인에 배치될 수 있음 */
struct bad_example {
      atomic_t  hot_counter;     /* 모든 코어에서 자주 접근 */
      spinlock_t hot_lock;       /* 모든 코어에서 자주 경쟁 */
      char        cold_name[64];/* 가끔만 접근하는 이름 */
      struct list_head cold_list;/* 드물게 접근하는 연결 리스트 */
};

/* ✅ 올바른 예: hot/cold 데이터 분리 + 캐시 라인 정렬 */

struct hot_data {
     atomic_t  counter;
     spinlock_t lock;
} __cacheline_aligned;  /* 64바이트 경계 정렬 */

struct cold_data {
     char        name[64];
     struct list_head list;
} __cacheline_aligned;

/* Per-CPU 변수 사용: 각 코어마다 전용 복사본 */
static struct hot_data percpu_hot
     __attribute__((percpu));

/* Per-CPU 변수 접근 (예: network packet counter) */
static void inc_packet_count(void)
{
     atomic_inc(&this_cpu_ptr(&percpu_hot)->counter);
}

커널 내부적으로 __cacheline_aligned는 아키텍처 특정 패딩을 추가하여 캐시 라인 크기에 맞춤합니다. x86_64에서는 64바이트, ARM64에서는 128바이트(일부 새 CPU)에 정렬됩니다.

/* include/linux/cache.h — 캐시 라인 정렬 매크로 */

/* L1 Cacheline-aligned */
#define __cacheline_aligned      __attribute__ ((aligned (L1_CACHE_BYTES)))

/* x86_64: arch/x86/include/asm/cache.h */
#define L1_CACHE_BYTES     64

/* Per-CPU 데이터의 정렬: cacheline_align() */
#define cacheline_aligned(x) __cacheline_aligned_in_smp x

/* struct task_struct 내부 hot/cold 분리 예 */
struct thread_info {
     unsigned long flags;           /* TIF_ flags, hot */
     unsigned long syscall;         /* syscall number, hot */
     struct task_struct *task;     /* task pointer, hot */
     /* ... cacheline boundary here ... */
     void *tp_value;              /* TLS, cold-ish */
     __u8 supervisor_stack[0];   /* supervisor stack pointer */
};

HW/SW Prefetch 기술

x86_64 CPU는 하드웨어 Prefetch를 기본적으로 제공하며, 간단한 순차 접근 패턴이나 stride-2와 같은 규칙적인 패턴을 자동으로 감지하여 데이터를 캐시에 미리 로드합니다. 그러나 커널 코드에서는 하드웨어가 감지하지 못하는 불규칙한 데이터 접근 패턴이 빈번하게 발생합니다. 이런 경우 명시적인 prefetch() 또는 _mm_prefetch() intrinsic 함수를 사용하여 소프트웨어 단에서 Prefetch 힌트를 줄 수 있습니다.

/* include/asm-x86/cpufeature.h — prefetch intrinsic 예시 */

/* Prefetch data into L1 cache (TLB hint) */
#define prefetch(addr)  \
     _mm_prefetch((const char *)addr, _MM_HINT_T0)

/* L2 cache prefetch */
#define prefetchw(addr)   \
     _mm_prefetch((const char *)addr, _MM_HINT_T1)

/* 예: 네트워크 rx ring buffer 처리 */
static int net_rx_poll(struct netdev_rx_queue *rxq, int budget)
{
     struct sk_buff *skb;
     int i;

     for (i = 0; i < budget; i++) {
/* 현재 버퍼 처리 */
         skb = rxq_dequeue(rxq);
         if (!skb)
             break;

/* 다음 2개 버퍼를 Prefetch (HW prefetch가 잡지 못하는 경우) */
         prefetch(skb + 1);
         prefetch(skb + 2);

         netif_receive_skb(skb);
     }
     return i;
}
SW Prefetch 주의사항: prefetch 명령은 "Cache-Line을 캐시에 로드한다"는 힌트일 뿐, 메모리 접근이 아닙니다. TLB 미스(TLB Miss)는 prefetch로 해결되지 않습니다. 또한, prefetch 호출이 너무 가까우면(앞선 접근과 ~32사이클 이내) 오히려 Prefetch 단계를 점유해 성능을 악화할 수 있습니다. 일반적인 가이드라인은 prefetch된 데이터가 실제 접근되기까지 최소 64~128 사이클의 작업이 있는 경우 SW prefetch가 효과적입니다.

분기 예측(Branch Prediction) 최적화

x86_64 CPU는 동적 분기 예측(Dynamic Branch Prediction)을 통해 분기의 실제 결과(진입 or 비진입)를 학습하고 다음 실행에 활용합니다. 분기 예측이 실패하면 파이프라인이 플러시되고 10~20 사이클의 지연(Penalty, x86_64 구현에 따라 다르지만 일반적으로 10~20사이클)이 발생합니다. 커널 코드에서 분기 예측 효율을 높이는 방법은 다음과 같습니다:

/* include/linux/compiler_types.h — likely/unlikely 내부 */

#if defined(__GNUC__) && __GNUC__ >= 4
#  define likely(x)        __builtin_expect (!(!(x)), 1)
#  define unlikely(x)      __builtin_expect (!(!(x)), 0)
#else
#  define unlikely(x) (x)
#endif

/* 사용 예: 네트워크 패킷 처리 */
static int process_packet(struct sk_buff *skb)
{
     /* 정상 경로: 패킷 유효함 (99.9% 케이스) */
     if (likely(skb->len > 0)) {
         process_ip_header(skb);
         process_tcp_header(skb);
         return 0;
     }

     /* 예외 경로: 빈 패킷 (0.1% 케이스) */
     pr_debug("Empty packet received\n");
     kfree_skb(skb);
     return -EINVAL;
}

NUMA Aware 최적화

NUMA(Non-Uniform Memory Access) 아키텍처에서 메모리 접근 지연 시간은 "어느 NUMA 노드에 속한 메모리를 접근하느냐"에 따라 다릅니다. 같은 노드 내 메모리 접근은 약 50-80ns, 다른 노드에서 접근하면 100-200ns(PCIe/ QPI/ UPI 링크를 통과)가 될 수 있습니다.

리눅스 커널은 NUMA 인식 기능을 여러 계층에서 제공합니다:

/* NUMA-인식 메모리 할당 예제 */

/* 현재 CPU가 속한 NUMA 노드에서 kmalloc */
static struct my_device *alloc_device(int node)
{
     struct my_device *dev;

     /* 인자 node가 유효하면 해당 노드, 아니면 현재 노드를 사용 */
     if (node < 0)
         node = numa_node_id();

     dev = kmalloc_node(sizeof(*dev), GFP_KERNEL, node);
     if (!dev)
         return NULL;

     /* RX ring buffer도 같은 NUMA 노드에 배치 */
     dev->rx_buf = alloc_pages_bulk_node(
         node, GFP_KERNEL | __GFP_NOWARN,
         32, &dev->rx_buf_n);
     return dev;
}

/* NUMA-aware per-CPU variable */
static DEFINE_PER_CPU(struct cpu_stats, cpu_stats);

static void update_cpu_stats(int packets)
{
     struct cpu_stats *stats = this_cpu_ptr(&cpu_stats);
     stats->packets += packets;
     stats->bytes += skb->len;
}

SIMD 명령어 활용 (SSE/AVX/AVX-512)

x86_64의 SIMD 명령어(SSE, AVX, AVX-512)는 데이터 병렬처리에 매우 강력합니다. 커널 모드에서는 제한적으로 사용되며, kernel_fpu_begin/end()를 사용하여 FPU/MMX/SIMD 레지스터 컨텍스트를 보호해야 합니다.

커널에서 SIMD를 사용하는 대표적인 예:

/* 커널 모드 SIMD 사용 예제 (AES-NI 암호화) */

/* crypto/aes_crypt.c — AES-NI 기반 암호화 */
static int crypto_aes_crypt_ecb(struct ecb_ctx *ctx,
                                     u8 *dst, const u8 *src,
                                     unsigned int len)
{
     int ret = -EBUSY;

     kernel_fpu_begin();
     do {
         /* aesenc, aesenclast intrinsics (GCC/Clang) */
         _mm_store_si128(&out, _mm_aesenc_si128(in, *round_key));
         _mm_store_si128(&out, _mm_aesenclast_si128(in, *last_round_key));

         src += 16;
         dst += 16;
         len -= 16;
         round_key++;
     } while (len >= 16);

     kernel_fpu_end();
     return 0;
}

/* CRC32c PCLMULQLQDQ 최적화 */
uint32_t crc32c_pclmul(const u8 *data, unsigned int len, uint32_t crc)
{
     return _mm_crc32_u64(crc, *(const uint64_t*)data);
}
SIMD 커널 사용 주의: kernel_fpu_begin() 호출 시 FPU/SIMD 레지스터가 커널에 의해 context-switch되면 컨텍스트 전환 오버헤드가 커집니다. 작은 데이터 블록(< 1KB)에서는 SIMD 오버헤드가 순차 처리보다 더 클 수 있으므로, 데이터 블록이 충분히 큰 경우에 한해 SIMD 루틴을 사용하는 것이 성능적으로 유리합니다.

TLB 최적화

TLB(Translation Lookaside Buffer)는 가상 주소 → 물리 주소 변환 결과를 캐시하는 고속 메모리입니다. TLB miss가 발생하면 Page Table.walk(4-level이면 L1 → L2 → L3 → L4 → Page)가 필요한데, 이 overhead은 100~300사이클에 달합니다.

주요 TLB 최적화 기법:

# Transparent Huge Pages 설정 확인
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never

# HugePages 상태 확인
cat /proc/meminfo | grep -i huge
# HugePages_Total:        128
# HugePages_Free:         96

# TLB miss 측정
perf stat -e dTLB-load-misses,dTLB-store-misses -- ./benchmark

FRED과 GCS (Intel CET)

Intel CET(Control-flow Enforcement Technology)는 두 가지 서브 기술로 구성됩니다. GCS(Guarded Control Stack)는 Shadow Stack을 사용하여 Return Address를 보호하며, FRED(Flexible Return Event Delivery)는 커널 호출/인터럽트 진입-종료를 가속합니다.

FRED은 Linux 6.12(2024 Q3)에 메인 라인 머지되었으며, Intel Alder Lake 이후 CPU에서 지원됩니다. 기존의 SYSCALL/SYSRET 또는 INT 0x80 경로를 대체하여, 시스템 콜 진입 오버헤드를 약 50%까지 낮춥니다.

# FRED 지원 확인
grep -i fred /proc/cpuinfo

# GCS 상태 확인
dmesg | grep -i gcs
# [     1.234567] x86/CET: Enabling Guarded Control Stack

# FRED 활성화 부트 파라미터
fred=on  # forced enable
fred=off # explicit disable
FRED/GCS 설정: FRED은 CPU가 지원해야 활성화되며, CONFIG_X86_FRED=y 빌드 시 cpuidEDX[28] 비트로 지원 여부를 런타임 확인 후 사용 여부가 결정됩니다. GRUB에서 fred=on 파라미터를 전달하면 CPU 지원 여부와 무관하게 강제로 활성화되며, CPU에서 지원하지 않으면 boot-time warning이 발생합니다.

하드웨어 성능 모니터링 (PMC / IntelPT)

x86_64 CPU는 PMU (Performance Monitoring Unit)를 통해 다양한 Hardware Counter를 제공하는데, 이를 통해 명령어 수, Cache Miss, Branch Miss, Cycle 수 등을 정량적으로 측정할 수 있습니다. perf은 이 PMC에 접근하는 최상위 도구이며, IntelPT는 Branch-level trace를 제공합니다.

# CPU cycle, instruction, cache/branch miss를 함께 측정
perf stat -e cycles,instructions,cache-references,cache-misses, \
     branch-instructions,branch-misses -- ./my_benchmark

# 결과를 flame graph로 가시화
perf record -F 99 -- ./my_benchmark
perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg

# Intel PT trace (branch-level) record
perf record -e intel_pt// -- ./my_benchmark
perf inject -j --input perf.data --output perf.pt

# MSR 직접 읽기 (root 필요)
rdmsr -a 0x198  # IA32_PERFCTR0 (core cycles)
rdmsr -a 0x199  # IA32_PERFCTR1 (instructions)

MSR(Model-specific Register)에는 각 CPU 세대마다 다양한 성능 카운터가 위치합니다. perf stat은 PMU의 Counter multiplexing을 자동으로 처리하므로, 일반적으로 raw MSR 접근 없이 perf를 사용하는 것을 권장합니다.

MSR 접근: rdmsr/wrmsr는 raw MSR 값을 읽고 적는 user-space 도구입니다. Kernel module에서는 rdmsrl(msr_id, &val), wrmsrl(msr_id, val) 함수를 사용합니다. 하지만 MSR ID는 CPU 세대별로 다르므로, cpu_has() (cpufeature bit)로 확인한 뒤 접근해야 합니다.

ARM64 최적화 포인트

ARM64(RISC 계열)는 x86와 달리 약한 메모리 순서(Weak Memory Ordering) 모델을 사용합니다. 코드가 작성된 순서와 실제 메모리 접근 순서가 다를 수 있기 때문에, 공유 데이터를 다룰 때는 dmb/dsb 배리어나 smp_wmb()/smp_rmb() 같은 커널 추상화를 명시적으로 사용해야 합니다. 이 점이 x86 경험자가 ARM64 커널 코드를 처음 작성할 때 가장 자주 실수하는 부분입니다.

ARM64 메모리 순서 모델

ARMv8-A Memory Ordering 모델은 REACQUIRE-RELEASE(StoreBuffer 기반) 모델입니다. Load-Load, Load-Store, Store-Load, Store-Store 재배치 모두 허용되므로, DMBDSB 명령의 종류에 따라 재배치 허용 범위 제어합니다.

명령DomainScope커널 매크로
DMB ISHInner ShareableCache coherent domainsmp_mb()
DMB ISHLDInner ShareableLoadLoad barriersmp_rmb()
DMB ISHSTInner ShareableStore-Load barrier (Load-Store는 항상 순서 보장)smp_wmb()
DMB SYFull SystemLoad/Store 모두mb() (strongest)
DSB ISHInner ShareableMemory access completion barrier (DSB ISH)cache/maintains flush
DSB SYFull SystemAll accesses completeContext switch, KASLR
ISB-Pipeline flush after instruction changeisb()

ARM64에서 분기 예측과 명령 Pipeline과 같은 하드웨어 특징은 다음과 같습니다:

/* ARM64 Memory Ordering 예제 */

static void producer(struct shared *d)
{
     WRITE_ONCE(d->data, 42);
     smp_wmb();                   /* DMBS ISHST: store → store 순서 보장    */
     WRITE_ONCE(d->ready, 1);
}

static int consumer(struct shared *d)
{
     int val;

     if (!READ_ONCE(d->ready))
         return 0;
     smp_rmb();                   /* DMB ISHLD: load → load 순서 보장 */
     val = READ_ONCE(d->data);
     return val;
}

LSE (Large System Extensions)

ARMv8.1-A부터 도입된 LSE는 원자성 명령어(Load-Acquire/Store-Release)를 강화하는 확장입니다. LSE 이전에는 Atomic 연산이 LDXR/STXR 루프 + DMB ISH로 구현되었는데, 이는 멀티코어 환경에서 spin-like 재시도를 유발하여 동시성(Spin Count)이 높은 경우 성능 저하를 초래했습니다.

LSE의 LDAR/STLR(Load-Acquire-Exclusive / Store-Release-Exclusive)는 명시적 barrier 없이 Load-Store 순서를 하드웨어가 보장하므로, spin count가 10% 이상 감소하는 경우가 보고되었습니다.

/* LSE Atomic Operations (ARM64 Assembly) */

/* LSE 이전: LDXR/STXR 루프 + DMB */
loop:
       ldxr    w0, [x1]
       add      w0, w0, #1
       stxr    w2, w0, [x1]
       cbnz    w2,    loop         /* store failed → retry */
       dmb     ish

/* LSE 이후: LDAR/STLR (루프 없이) */
       ldar    w0, [x1]
       add      w0, w0, #1
       stlr    w0, [x1]

/* Linux kernel: arch/arm64/include/asm/atomic.h */
/* atomic_add() → ldar/stlr 로 자동 최적화 (CONFIG_ARM64_LSE_ATOMICS) */
ARM64 LSE 확인: lscpu | grep "LSE" 또는 cat /proc/cpuinfo | grep lse 명령으로 LSE 지원 여부를 확인할 수 있습니다. LSE Atomics가 활성화되면 CONFIG_ARM64_LSE_ATOMICS=y 설정과 함께 커널 Atomic 연산이 LSE 명령어로 컴파일됩니다.

SVE (Scalable Vector Extension)

SVE는 ARMv8.2-A부터 도입된 확장 명령어 집합으로, SIMD 작업에 사용되는 SIMD 레지스터의 길이를 고정 길이(AVX-512 = 512-bit)가 아니라 하드웨어에 따라 가변 길이로 정의합니다. ARM64 Server SoC(Graviton2, Fujitsu A64FX)에서 사용되며, 벡터 연산의 처리 단위(LEN)는 하드웨어 설계에 따라 결정됩니다.

SVE의 주요 특징:

/* SVE 명령어 예시 (Scalable Vector Add) */

/* z0, z1, z2 = SVE vector registers */
       ld1     {z0.s}, p0/all, [x0]   /* load vector z0 from address x0. pred p0 */
       ld1     {z1.s}, p0/all, [x1]   /* load vector z1 from address x1 */
       add    z2.s, p0,    z0.s, z1.s  /* z2 = z0 + z1, mask p0 */
       st1     {z2.s}, p0/all, [x2]   /* store z2 to x2 */

/* whilelo: predicate 기반 루프 (loop unroll 없이) */
top:
       ld1     {z0.s}, p0/z, [x0], #16  /* load next vector, post-increment */
       whilelo p0.s, w2, w3            /* p0 = (w2 < w3)? 1:0 per lane */
       b.cond p0.any, top             /* any bit set? → continue */

/* SVE vector length 확인 */
       mov     x0, #0
       svlen   x0                     /* store vector length in bytes to x0 */
/* Kernel SVE 사용 예: crypto/sha256-glue.c */

static void sha256_transform_sve(struct sha256_state *s,
                                     const u8 *data)
{
     kernel_sve_begin();
     /* SVE intrinsics (GCC/Clang) */
     sve_sha256_process(s, data);
     kernel_sve_end();
}

SME (Scalable Matrix Extension)

SME는 ARMv8.2-A의 SVE를 기반으로 확장된 matrix-oriented SIMD 확장입니다. ARMv9.2-A부터 지원되며, Tensor-like 연산에 특화된 ZA 레지스터 집합(128×128-bit matrix register space)를 제공합니다.

SME의 핵심 특징:

/* SME matrix multiply 예시 (ZA register 사용) */

/* 스트리밍 모드 진입 */
       inz     #0                   /* enable streaming mode (set ZT0) */

/* ZA registers loaded from memory */
       ld1w   za0, [x0], #256     /* load matrix A into za0 */
       ld1w   za1, [x1], #256     /* load matrix B into za1 */

/* FMMA: Floating-point Multiply-Accumulate */
       fmaa   za2.s, za0.s, za1.s  /* za2 = za0 × za1 + za2 */

/* Store ZA to memory */
       st1w   za2, [x2], #256

/* 스트리밍 모드 탈출 */
       inv     #0                   /* disable streaming mode */
SME 지원 확인: grep -i sme /proc/cpuinfo 명령으로 SME 지원을 확인할 수 있습니다. Linux 커널에서 SME context switch는 task_structstruct fpsimd_state.sve_state에 SME 컨텍스트를 포함하도록 확장되었습니다.

NEON (128-bit SIMD)

NEON이 ARM64에서 가장 널리 사용되는 SIMD 명령어입니다. AES-NI 같은 전용 명령어는 ARM64에 없으므로, NEON intrinsic을 통해 AES 암호화를 구현해야 합니다. 또한 NEON은 Crypto Cell(ARMv8 Cryptographic Extensions) 와 연동하여 PCLMULQDQ의 ARM 대응인 PMULL/VMULL 명령어로 GHASH, POLY1305 등 Message Authentication Code(MAC) 연산을 가속합니다.

/* NEON + ARM Crypto Extension 예시 */

/* PMULL: Polynomial Multiply (GHASH 핵심 연산) */
       pmull   q0, d0, d8                /* 128-bit × 128-bit → 256-bit (q0, q1) */
       pmull2  q1, d1, d9               /* upper half → q1  (128-bit × 128-bit → 256-bit) */

/* AES round expansion (AES encryption using NEON) */
       aes    q0, q1                   /* SubBytes + ShiftRows */
       aesimc q0, q1                  /* SubBytes + ShiftRows + InvMixColumns (decryption) */
       aesmc  q0, q1                  /* MixColumns only */

/* VMULL: Integer Multiply-Accumulate (128-bit SIMD-wide) */
       vmull   q0, d0, d1                /* 64 × 64 → 128 (saturating) */
/* Kernel NEON usage example: crypto/ghash-neon-arm64.c */

/* GHASH using NEON + ARM Crypto Extensions */
static void ghash_neon_update(struct ghash_ctx *ctx,
                                 const u8 *src, unsigned int len)
{
     uint64_t x[2], h[2];

     kernel_neon_begin();
     /* Load H key into NEON registers */
     vld1q_u64(h, (const uint64_t *)ctx->hash);

     while (len >= 16) {
         /* Load input block */
         vld1q_u64(x, (const uint64_t *)src);

         /* EORB with current state */
         x[0] = veorq_u64(x[0], ctx->x[0]);
         x[1] = veorq_u64(x[1], ctx->x[1]);

         /* Polynomial multiply (PMULL) — core of GHASH */
         /* ... NEON intrinsics for PMULL, EOR, shift ... */

         src += 16;
         len -= 16;
     }
     kernel_neon_end();
}

MTE (Memory Tagging Extension)

MTE가 활성화된 ARM64 시스템에서는 Tag Check Fault의 오버헤드가 발생합니다. 보안 검사와 성능 간의 Trade-off에서, Tag Allocation + Tag Checking을 모두 켰을 때 약 5~15%의 전반적 성능 저하가 관찰되지만, 이는 사용 패턴(Heap 객체 수, Allocation 빈도, Cache Hit Rate)에 따라 달라집니다.

MTE가 성능에 미치는 영향:

MTE 설정이 성능에 미치는 영향을 최소화하려면:

# MTE Tag Storage 확인
cat /proc/cpuinfo | grep -i mte
# Features: ... fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp
#           jpcre32 lrnd lse2 ssbs mte

# MTE 상태 및 설정
grep -i mte /sys/devices/system/cpu/vulnerabilities/spec_store_bypass
prctl PR_GET_TAGGED_ADDR_CTRL  # user-space MTE 상태 확인

ARM64 NUMA 최적화

ARM64 서버(Graviton, Ampere, Apple Silicon)는 NUMA Aware 메모리 할당과 CPU Affinity 정책이 x86_64와 유사하게 동작합니다. 다만 ARM64는 ACPI 대신 Device Tree로 NUMA 토폴로지를 정의하므로, DTB(Deprecated Tree Blob)에서 numa-node, memory, cpus 노드의 numa-node-id 속성으로 NUMA 노드-메모리-CPU 매핑을 정의합니다.

/* ARM64 Device Tree: NUMA 토폴로지 정의 예시 */

/ {
     #address-cells = <2>;
     #size-cells  = <2>;

     /* NUMA 노드 0 */
     numa-map@0 {
         reg = <0x0 0x0 0x0 0x40000000>;  /* 1GB @ 0x80000000 */
         numa-node-id = <0>;
     };

     /* NUMA 노드 1 */
     numa-map@1 {
         reg = <0x0 0x40000000 0x0 0x40000000>; /* 1GB @ 0xC0000000 */
         numa-node-id = <1>;
     };

     cpus {
         cpu0 {
             device_type = "cpu";
             reg = <0>;
             numa-node-id = <0>;
         };

         cpu4 {
             device_type = "cpu";
             reg = <4>;
             numa-node-id = <1>;
         };
     };
};

ARM64 캐시 및 TLB 최적화

ARM64는 캐시 라인 크기가 64바이트(Apple M-series) 또는 128바이트(Graviton4)로 x86_64와 다를 수 있습니다. TLB Entry 수는 아키텍처마다 다르며, TLB 최적화 측면에서 Huge Page, ASID, DSB ISH 명령의 효율적 활용이 중요합니다.