Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

59. 실행 메모리 (Execmem)

개요

실행 메모리(Execmem)는 리눅스 커널이 동적으로 생성한 코드를 실행하기 위해 할당하는 메모리 영역을 관리하는 서브시스템입니다. 커널 모듈, BPF JIT, kprobes, ftrace 트램폴린 등은 모두 CPU가 직접 실행해야 하는 기계어 코드를 담고 있는데, 이러한 코드를 담을 메모리는 "읽기+실행(RX)" 또는 "읽기+쓰기+실행(RWX)" 보호 속성을 가져야 합니다. 일반적인 vmalloc 할당은 "읽기+쓰기(RW)" 권한으로만 할당하기 때문에 별도의 실행 메모리 할당 메커니즘이 필요합니다.

Linux 7.0에서 execmem은 mm/execmem.c에 구현되어 있으며, 아키텍처별로 다른 제약 조건(주소 범위, 정렬, 페이지 테이블 권한)을 추상화하여 제공합니다. 특히 CONFIG_ARCH_HAS_EXECMEM_ROX를 지원하는 아키텍처(x86_64)에서는 ROX(Read-Only-Execute) 캐시라는 고급 기능을 통해 거대 페이지(huge page) 단위로 할당된 실행 메모리를 재사용하여 성능을 향상시킵니다.

한 줄로 보면, 실행 메모리는 커널이 "메모리 위에 프로그램을 동적으로 올려서 실행하는" 도우미입니다. BPF 프로그램이 JIT 컴파일되어 실행되거나, kprobe가 커널 함수를 가로채거나, ftrace가 동적 트램폴린을 만들 때 모두 이 서브시스템을 거칩니다.

소스 파일 경로:

mm/execmem.c                          // 실행 메모리 핵심 구현
include/linux/execmem.h               // 인터페이스 정의 (enum, struct, API)
arch/x86/mm/init.c:1060-1127         // x86_64 아키텍처별 초기화
arch/arm64/mm/init.c:496-546         // ARM64 아키텍처별 초기화
kernel/bpf/core.c:1064-1072          // BPF JIT 사용 예시
kernel/kprobes.c:112-121             // kprobes 사용 예시
arch/x86/kernel/ftrace.c:269-276     // ftrace 트램폴린 사용 예시

빠른 점검 명령

# execmem 관련 커널 설정 확인
cat /boot/config-$(uname -r) | grep -E "CONFIG_EXECMEM|CONFIG_ARCH_HAS_EXECMEM_ROX|CONFIG_STRICT_MODULE_RWX"

# BPF JIT 실행 메모리 사용량 확인
cat /sys/kernel/debug/bpf_jit_current 2>/dev/null || cat /proc/sys/net/core/bpf_jit_enable

# kprobes 할당 현황 확인
sudo cat /proc/kallsyms | grep -c "kprobe" 2>/dev/null | head -5

# 모듈 로드 시 execmem 사용 확인
dmesg | grep -i "execmem\|module\|alloc" | tail -20

# ftrace 트램폴린 할당 점검
sudo cat /sys/kernel/debug/tracing/available_filter_functions | head -5

# 실행 메모리 관련 vmalloc 영역 확인
sudo cat /proc/vminfo | grep -i vmalloc
cat /proc/vmallocinfo | grep -i "exec\|module\|bpf" | head -20

# 모듈 텍스트 영역 권한 확인
cat /proc/modules | head -5
sudo readelf -l $(which modprobe 2>/dev/null || echo /bin/ls) 2>/dev/null | grep -E "LOAD|GNU_RELRO" | head -10

# 아키텍처별 execmem 초기화 확인
sudo cat /sys/kernel/debug/kernel_page_tables 2>/dev/null | head -40

# BPF 프로그램 할당 통계
sudo cat /sys/fs/bpf/stats/jit_alloc 2>/dev/null || echo "BPF stats 미지원"
cat /proc/sys/kernel/bpf_jit_enable 2>/dev/null

# 실행 메모리 ROX 캐시 동작 확인 (x86_64만)
dmesg | grep -i "rox\|execmem" | tail -10

자료구조 관계도

실행 메모리 자료구조 관계도

호출 흐름도

실행 메모리 호출 흐름

핵심 자료구조

enum execmem_type

include/linux/execmem.h:37-45 — 실행 메모리의 유형을 정의합니다. 각 유형은 사용 주체에 따라 다른 주소 범위와 권한을 가집니다.

enum execmem_type {
    EXECMEM_DEFAULT,               // 기본 매개변수
    EXECMEM_MODULE_TEXT = EXECMEM_DEFAULT, // 모듈 텍스트 섹션
    EXECMEM_KPROBES,               // kprobes용
    EXECMEM_FTRACE,                // ftrace 트램폴린용
    EXECMEM_BPF,                   // BPF JIT용
    EXECMEM_MODULE_DATA,           // 모듈 데이터 섹션
    EXECMEM_TYPE_MAX,
};

struct execmem_range

include/linux/execmem.h:97-105 — 특정 유형의 실행 메모리에 대한 주소 범위와 권한을 정의합니다.

struct execmem_range {
    unsigned long   start;              // 주소 공간 시작
    unsigned long   end;                // 주소 공간 끝 (포함)
    unsigned long   fallback_start;     // 대체 주소 범위 시작
    unsigned long   fallback_end;       // 대체 주소 범위 끝 (포함)
    pgprot_t        pgprot;             // 페이지 권한 (PAGE_KERNEL_ROX 등)
    unsigned int    alignment;          // 정렬 요구사항
    enum execmem_range_flags flags;     // 옵션 플래그
};

struct execmem_info

include/linux/execmem.h:114-116 — 아키텍처 전반의 실행 메모리 매개변수를 담습니다.

struct execmem_info {
    struct execmem_range    ranges[EXECMEM_TYPE_MAX]; // 유형별 범위 배열
};

struct execmem_cache (ROX 캐시)

mm/execmem.c:89-94CONFIG_ARCH_HAS_EXECMEM_ROX 활성화 시 사용되는 ROX 캐시입니다. 해제된 실행 메모리 블록을 maple tree로 관리하여 재사용합니다.

struct execmem_cache {
    struct mutex mutex;                  // 동시성 제어
    struct maple_tree busy_areas;        // 사용 중인 영역
    struct maple_tree free_areas;        // 해제된 영역 (재사용 대기)
    unsigned int pending_free_cnt;       // 비동기 해제 대기 수
};

enum execmem_range_flags

include/linux/execmem.h:52-55 — 실행 메모리 할당 옵션 플래그입니다.

enum execmem_range_flags {
    EXECMEM_KASAN_SHADOW    = (1 << 0), // KASAN 섀도우 메모리 할당
    EXECMEM_ROX_CACHE       = (1 << 1), // ROX 캐시 사용 (거대 페이지 재사용)
};

핵심 함수

execmem_alloc()

mm/execmem.c:461-477 — 실행 메모리를 할당하는 메인 진입점입니다. 유형에 따라 적절한 범위를 선택하고, ROX 캐시를 사용하면 캐시에서 할당하고, 아니면 vmalloc으로 직접 할당합니다.

void *execmem_alloc(enum execmem_type type, size_t size)
{
    struct execmem_range *range = &execmem_info->ranges[type];
    bool use_cache = range->flags & EXECMEM_ROX_CACHE;
    unsigned long vm_flags = VM_FLUSH_RESET_PERMS;
    pgprot_t pgprot = range->pgprot;
    void *p = NULL;

    size = PAGE_ALIGN(size);

    if (use_cache)
        p = execmem_cache_alloc(range, size);   // ROX 캐시 경로
    else
        p = execmem_vmalloc(range, size, pgprot, vm_flags); // 직접 vmalloc

    return kasan_reset_tag(p);
}
  • 동작: type에 해당하는 execmem_range를 찾아 크기를 페이지 정렬한 뒤, ROX 캐시 사용 여부에 따라 분기합니다.
  • 반환: 할당된 실행 메모리 포인터 또는 NULL
  • execmem_cache_alloc()

    mm/execmem.c:323-337 — ROX 캐시에서 메모리를 할당합니다. 캐시에 충분한 블록이 없으면 새 페이지를 확보한 후 다시 시도합니다.

    static void *execmem_cache_alloc(struct execmem_range *range, size_t size)
    {
        void *p;
        int err;
    
        p = __execmem_cache_alloc(range, size);  // free_areas에서 할당 시도
        if (p)
            return p;
    
        err = execmem_cache_populate(range, size); // 새 페이지 확보
        if (err)
            return NULL;
    
        return __execmem_cache_alloc(range, size); // 재시도
    }
  • 흐름: free_areas 맵에서 블록 검색 → 없으면 vmalloc으로 새 페이지 확보 → ROX 권한 설정 → free_areas에서 할당
  • execmem_cache_populate()

    mm/execmem.c:281-321 — ROX 캐시에 새 페이지를 추가합니다. vmalloc으로 메모리를 할당한 뒤, 트래핑 명령으로 채우고 ROX 권한을 설정합니다.

    static int execmem_cache_populate(struct execmem_range *range, size_t size)
    {
        unsigned long vm_flags = VM_ALLOW_HUGE_VMAP;
        struct vm_struct *vm;
        size_t alloc_size;
        int err = -ENOMEM;
        void *p;
    
        alloc_size = round_up(size, PMD_SIZE);  // PMD 크기로 올림
        p = execmem_vmalloc(range, alloc_size, PAGE_KERNEL, vm_flags);
        if (!p) {
            alloc_size = size;                  // 폴백: 정확한 크기로 재시도
            p = execmem_vmalloc(range, alloc_size, PAGE_KERNEL, vm_flags);
        }
    
        if (!p)
            return err;
    
        vm = find_vm_area(p);
        execmem_fill_trapping_insns(p, alloc_size);  // 트래핑 명령 채우기
        err = set_memory_rox((unsigned long)p, vm->nr_pages); // ROX 권한 설정
        // ... 캐시에 추가, 에러 핸들링
    }
  • 중요: PMD 크기로 할당하면 거대 페이지 매핑이 가능하여 TLB 성능이 향상됩니다.
  • 트래핑 명령: 실행되지 않아야 할 영역에 INT3(0xCC) 같은 트래핑 명령을 채워, 실수로 실행되어도 안전하게 트랩됩니다.
  • execmem_free()

    mm/execmem.c:494-504 — 실행 메모리를 해제합니다. ROX 캐시를 사용하면 캐시에 반환하고, 아니면 vfree합니다.

    void execmem_free(void *ptr)
    {
        WARN_ON(in_interrupt()); // 인터럽트 컨텍스트에서 해제 불가
    
        if (!execmem_cache_free(ptr))
            vfree(ptr);
    }
  • 주의: 인터럽트 컨텍스트에서 해제할 수 없습니다. RO 메모리 해제가 IRQ에서 지원되지 않기 때문입니다.
  • execmem_cache_free()

    mm/execmem.c:405-437 — ROX 캐시에서 메모리를 해제합니다. 빠른 경로에서 실패하면 비동기 워크 큐를 통해 지연 해제합니다.

    static bool execmem_cache_free(void *ptr)
    {
        // ... maple tree에서 busy_areas를 검색
        err = __execmem_cache_free(&mas, area, GFP_KERNEL | __GFP_NORETRY);
        if (err) {
            // 빠른 경로 실패 → 비동기 해제로 전환
            area = pending_free_set(area);      // PENDING_FREE_MASK 비트 설정
            execmem_cache.pending_free_cnt++;
            schedule_delayed_work(&execmem_cache_free_work, FREE_DELAY);
            return true;
        }
        schedule_work(&execmem_cache_clean_work); // 정리 워크 큐에 추가
        return true;
    }
  • PENDING_FREE_MASK: maple tree의 주소 비트에 마스크를 설정하여 해제 대기 상태를 표시합니다.
  • execmem_vmalloc()

    mm/execmem.c:28-63 — VM이 있는 시스템에서 vmalloc을 사용한 실행 메모리 할당입니다. 폴백 범위가 있으면 1차 시도 실패 시 재시도합니다.

    static void *execmem_vmalloc(struct execmem_range *range, size_t size,
                                 pgprot_t pgprot, unsigned long vm_flags)
    {
        bool kasan = range->flags & EXECMEM_KASAN_SHADOW;
        gfp_t gfp_flags = GFP_KERNEL | __GFP_NOWARN;
        unsigned int align = range->alignment;
        unsigned long start = range->start;
        unsigned long end = range->end;
        void *p;
    
        p = __vmalloc_node_range(size, align, start, end, gfp_flags,
                                 pgprot, vm_flags, NUMA_NO_NODE,
                                 __builtin_return_address(0));
        if (!p && range->fallback_start) {
            // 폴백 범위로 재시도
            start = range->fallback_start;
            end = range->fallback_end;
            p = __vmalloc_node_range(size, align, start, end, gfp_flags,
                                     pgprot, vm_flags, NUMA_NO_NODE,
                                     __builtin_return_address(0));
        }
        // KASAN 섀도우 할당 처리 ...
        return p;
    }

    execmem_init() / __execmem_init()

    mm/execmem.c:561-592 — 실행 메모리 서브시스템을 초기화합니다. 아키텍처별 설정을 로드하고 검증한 뒤 누락된 유형의 기본값을 채웁니다.

    static void __init __execmem_init(void)
    {
        struct execmem_info *info = execmem_arch_setup(); // 아키텍처 설정
    
        if (!info) {
            // 아키텍처 미구현 시 기본값 사용
            info->ranges[EXECMEM_DEFAULT].start = VMALLOC_START;
            info->ranges[EXECMEM_DEFAULT].end = VMALLOC_END;
            info->ranges[EXECMEM_DEFAULT].pgprot = PAGE_KERNEL_EXEC;
            info->ranges[EXECMEM_DEFAULT].alignment = 1;
        }
    
        execmem_validate(info);    // 매개변수 검증
        execmem_init_missing(info); // 누락 유형 기본값 채우기
        execmem_info = info;
    }

    호출 흐름

    BPF JIT 할당 흐름

    bpf_jit_alloc_exec(size)
      └→ execmem_alloc(EXECMEM_BPF, size)
           ├→ execmem_cache_alloc() [ROX 캐시 사용 시]
           │    ├→ __execmem_cache_alloc()     // free_areas에서 할당
           │    ├→ execmem_cache_populate()    // 없으면 새 페이지 확보
           │    │    ├→ execmem_vmalloc()       // vmalloc으로 할당
           │    │    ├→ execmem_fill_trapping_insns() // 트래핑 명령 채우기
           │    │    └→ set_memory_rox()        // ROX 권한 설정
           │    └→ __execmem_cache_alloc()     // 재시도
           └→ execmem_vmalloc() [일반 경로]
                └→ __vmalloc_node_range()       // vmalloc 영역 할당

    kprobes 할당 흐름

    alloc_insn_page()
      └→ execmem_alloc(EXECMEM_KPROBES, PAGE_SIZE)
           └→ [위와 동일한 흐름]

    ftrace 트램폴린 할당 흐름

    alloc_tramp(size)
      └→ execmem_alloc_rw(EXECMEM_FTRACE, size)
           ├→ execmem_alloc(type, size)     // 기본 할당
           └→ execmem_force_rw(p, size)     // 쓰기 권한 추가

    해제 흐름

    execmem_free(ptr)
      ├→ execmem_cache_free(ptr) [ROX 캐시 사용 시]
      │    ├→ __execmem_cache_free()        // 정상 해제
      │    │    ├→ execmem_force_rw()        // RW 권한으로 변경
      │    │    ├→ execmem_fill_trapping_insns() // 트래핑 명령 채우기
      │    │    ├→ execmem_restore_rox()     // ROX 권한 복원
      │    │    └→ execmem_cache_add_locked() // free_areas에 추가
      │    └→ [실패 시] pending_free_set() → delayed_work로 비동기 해제
      └→ vfree(ptr) [캐시 미사용 시]

    조건별 비교

    유형별 할당자 비교

    유형사용 주체x86_64 주소 범위권한ROX 캐시
    EXECMEM_MODULE_TEXT커널 모듈MODULES_VADDR ~ MODULES_ENDPAGE_KERNEL_ROX사용
    EXECMEM_KPROBESkprobesMODULES_VADDR ~ MODULES_ENDPAGE_KERNEL_ROX사용
    EXECMEM_FTRACEftraceMODULES_VADDR ~ MODULES_ENDPAGE_KERNEL_ROX사용
    EXECMEM_BPFBPF JITMODULES_VADDR ~ MODULES_ENDPAGE_KERNEL미사용
    EXECMEM_MODULE_DATA모듈 데이터MODULES_VADDR ~ MODULES_ENDPAGE_KERNEL미사용

    아키텍처별 execmem_arch_setup 비교

    아키텍처주소 범위ROX 지원폴백 범위특징
    x86_64MODULES_VADDR ~ MODULES_END✓ (PSE 필요)없음KASAN 섀도우, KASLR 오프셋
    ARM64module_direct_base + SZ_128Mmodule_plt_base + SZ_2G직접 분기 범위 우선
    RISC-VVMALLOC 영역없음기본 설정
    ARMVMALLOC 영역없음기본 설정
    s390모듈 영역없음기본 설정

    ROX 캐시 동작 비교

    동작ROX 캐시 사용ROX 캐시 미사용
    할당free_areas maple tree에서 할당vmalloc 직접 할당
    권한ROX (Read-Only-Execute)ARCH에 따라 다름
    해제free_areas로 반환 (재사용)vfree()
    거대 페이지PMD 크기로 할당하여 대형 매핑 가능일반 페이지
    성능캐시 히트 시 빠름매번 vmalloc 호출

    W^X (Write XOR Execute) 보호

    보호 메커니즘설명
    페이지 권한 분리같은 페이지에 쓰기+실행 동시 불가
    ROX 캐시실행 전용 페이지에 트래핑 명령 사전 채우기
    execmem_force_rw쓰기 필요 시 일시적으로 RW로 변경 후 복원
    execmem_restore_rox수정 후 ROX 권한 복원
    트래핑 명령미사용 영역에 INT3 등으로 실수 실행 방지

    관련 문서

  • 00-메모리 관리 개요
  • 06-vmalloc
  • 20-Migration
  • 33-GUP
  • 36-mprotect
  • 37-mremap
  • 52-Linux 6.x → 7.0 변경점