실행 메모리(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
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,
};
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; // 옵션 플래그
};
include/linux/execmem.h:114-116 — 아키텍처 전반의 실행 메모리 매개변수를 담습니다.
struct execmem_info {
struct execmem_range ranges[EXECMEM_TYPE_MAX]; // 유형별 범위 배열
};
mm/execmem.c:89-94 — CONFIG_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; // 비동기 해제 대기 수
};
include/linux/execmem.h:52-55 — 실행 메모리 할당 옵션 플래그입니다.
enum execmem_range_flags {
EXECMEM_KASAN_SHADOW = (1 << 0), // KASAN 섀도우 메모리 할당
EXECMEM_ROX_CACHE = (1 << 1), // ROX 캐시 사용 (거대 페이지 재사용)
};
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);
}
execmem_range를 찾아 크기를 페이지 정렬한 뒤, ROX 캐시 사용 여부에 따라 분기합니다.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); // 재시도
}
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 권한 설정
// ... 캐시에 추가, 에러 핸들링
}
mm/execmem.c:494-504 — 실행 메모리를 해제합니다. ROX 캐시를 사용하면 캐시에 반환하고, 아니면 vfree합니다.
void execmem_free(void *ptr)
{
WARN_ON(in_interrupt()); // 인터럽트 컨텍스트에서 해제 불가
if (!execmem_cache_free(ptr))
vfree(ptr);
}
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;
}
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;
}
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_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 영역 할당
alloc_insn_page()
└→ execmem_alloc(EXECMEM_KPROBES, PAGE_SIZE)
└→ [위와 동일한 흐름]
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_END | PAGE_KERNEL_ROX | 사용 |
| EXECMEM_KPROBES | kprobes | MODULES_VADDR ~ MODULES_END | PAGE_KERNEL_ROX | 사용 |
| EXECMEM_FTRACE | ftrace | MODULES_VADDR ~ MODULES_END | PAGE_KERNEL_ROX | 사용 |
| EXECMEM_BPF | BPF JIT | MODULES_VADDR ~ MODULES_END | PAGE_KERNEL | 미사용 |
| EXECMEM_MODULE_DATA | 모듈 데이터 | MODULES_VADDR ~ MODULES_END | PAGE_KERNEL | 미사용 |
| 아키텍처 | 주소 범위 | ROX 지원 | 폴백 범위 | 특징 |
|---|---|---|---|---|
| x86_64 | MODULES_VADDR ~ MODULES_END | ✓ (PSE 필요) | 없음 | KASAN 섀도우, KASLR 오프셋 |
| ARM64 | module_direct_base + SZ_128M | ✗ | module_plt_base + SZ_2G | 직접 분기 범위 우선 |
| RISC-V | VMALLOC 영역 | ✗ | 없음 | 기본 설정 |
| ARM | VMALLOC 영역 | ✗ | 없음 | 기본 설정 |
| s390 | 모듈 영역 | ✗ | 없음 | 기본 설정 |
| 동작 | ROX 캐시 사용 | ROX 캐시 미사용 |
|---|---|---|
| 할당 | free_areas maple tree에서 할당 | vmalloc 직접 할당 |
| 권한 | ROX (Read-Only-Execute) | ARCH에 따라 다름 |
| 해제 | free_areas로 반환 (재사용) | vfree() |
| 거대 페이지 | PMD 크기로 할당하여 대형 매핑 가능 | 일반 페이지 |
| 성능 | 캐시 히트 시 빠름 | 매번 vmalloc 호출 |
| 보호 메커니즘 | 설명 |
|---|---|
| 페이지 권한 분리 | 같은 페이지에 쓰기+실행 동시 불가 |
| ROX 캐시 | 실행 전용 페이지에 트래핑 명령 사전 채우기 |
| execmem_force_rw | 쓰기 필요 시 일시적으로 RW로 변경 후 복원 |
| execmem_restore_rox | 수정 후 ROX 권한 복원 |
| 트래핑 명령 | 미사용 영역에 INT3 등으로 실수 실행 방지 |