커널 심볼 (Kernel Symbols)
커널 심볼은 단순한 함수 이름 목록이 아니라, 소스 선언, ELF 심볼 테이블(Symbol Table), export 정책, modpost 수집, Module.symvers, 런타임 재배치(Relocation), kallsyms 관측, GPL 및 네임스페이스(Namespace) 제약, CRC 기반 호환성 검사를 하나로 묶는 ABI 계약입니다. 이 문서는 커널 심볼을 "정의되는 이름"이 아니라 "외부 모듈과 디버거, 운영 도구가 의존하는 계약"으로 보고, 정의부터 문제 해결까지 한 흐름으로 정리합니다.
핵심 요약
- 심볼 — 함수, 전역 변수, 어셈블리(Assembly) 라벨, 링커(Linker)가 만든 주소 이름까지 포함하는 "이름 붙은 엔티티"입니다.
- export — 다른 모듈이 참조할 수 있도록 커널이 ABI 계약을 공개하는 행위입니다.
- modpost — 빌드 마지막에 exported symbol과 undefined symbol을 대조하고
Module.symvers를 생성하는 후처리 단계입니다. - kallsyms — 실행 중 커널이 유지하는 심볼 이름 사전으로, Oops 해석과 디버깅(Debugging)에 직접 쓰입니다.
- CRC/namespace/GPL — 심볼이 존재하더라도 버전 CRC, 네임스페이스 import, GPL 제약을 통과해야 모듈이 로드됩니다.
단계별 이해
- 정의
C 함수, 전역 변수, 어셈블리 라벨, 링커 스크립트 심볼이 ELF 심볼로 먼저 만들어집니다. - 공개 여부 결정
내부 심볼은 그대로 숨기고, 외부 모듈이 반드시 써야 하는 것만EXPORT_SYMBOL*로 공개합니다. - 빌드 후처리
modpost가 undefined symbol과 exported symbol을 연결하고Module.symvers와.mod.c를 만듭니다. - 런타임 해석
모듈 로더(Loader)가 재배치 엔트리를 따라 심볼 주소를 채우고, CRC, GPL, namespace 조건을 검증합니다. - 운영/디버깅
/proc/kallsyms,System.map,nm,readelf,%pS로 심볼을 해석하고 장애를 추적합니다.
개요: 커널 심볼을 무엇으로 봐야 하는가
커널 문맥에서 "심볼"이라는 단어는 적어도 네 가지 층위를 가집니다. 첫째, C/어셈블리 소스에 등장하는 이름입니다. 둘째, 오브젝트 파일과 vmlinux/.ko 안에 기록되는 ELF 심볼입니다. 셋째, EXPORT_SYMBOL*로 외부 모듈에 공개된 모듈 ABI 심볼입니다. 넷째, 실행 중 주소를 이름으로 되돌리기 위해 kallsyms가 유지하는 런타임 심볼 사전입니다. 실전에서는 이 네 층위가 섞여 보이기 때문에, 어느 단계에서 생긴 이름인지 먼저 구분해야 합니다.
즉, static 함수도 컴파일러와 링커 관점에서는 심볼일 수 있고, EXPORT_SYMBOL()이 붙지 않은 전역 함수도 ELF 심볼일 수 있습니다. 그러나 모든 ELF 심볼이 곧 모듈이 참조 가능한 커널 API는 아닙니다. 반대로, 디버거가 이름을 복원할 수 있다고 해서 그 심볼이 외부 모듈에서 링크 가능한 것도 아닙니다. 이 차이를 놓치면 "nm에는 보이는데 왜 모듈이 못 쓰지?" 같은 혼란이 바로 생깁니다.
| 용어 | 무엇을 가리키는가 | 대표 확인 도구 | 실수 포인트 |
|---|---|---|---|
| 소스 심볼 | C 함수, 전역 변수, 어셈블리 라벨 | 소스 코드, ctags, rg | 정의된 이름이면 모두 export 가능한 것으로 오해 |
| ELF 심볼 | 오브젝트 파일에 기록된 이름/주소/섹션 정보 | readelf -Ws, nm | ELF에 보이면 모듈도 쓸 수 있다고 오해 |
| exported 심볼 | 다른 모듈이 링크할 수 있도록 공개된 심볼 | __ksymtab*, Module.symvers | 최소 공개 원칙 없이 내부 구현까지 공개 |
| 런타임 심볼 | 주소를 이름으로 역해석하기 위한 실행 중 심볼 사전 | /proc/kallsyms, System.map | 보인다고 해서 로딩 가능한 ABI라고 오해 |
| Kconfig 심볼 | CONFIG_FOO 같은 설정 이름 | .config, menuconfig | 코드 심볼과 같은 의미로 혼동 |
심볼 수명주기: 정의에서 Oops 해석까지
커널 심볼의 수명은 소스 정의에서 끝나지 않습니다. 개발자가 함수를 정의하면 컴파일러가 ELF 심볼로 기록하고, 링크 과정이 최종 배치를 결정하며, export 매크로가 붙은 경우에는 별도 테이블에 등록됩니다. 그 뒤 modpost가 심볼 사용 관계를 수집하고, 모듈 로더는 로딩 시 재배치를 통해 실제 주소를 채웁니다. 마지막으로 Oops, ftrace, GDB, %pS 같은 도구가 이 이름을 다시 역으로 사용합니다.
중요한 점은 이 흐름이 순방향과 역방향 모두 존재한다는 것입니다. 순방향으로는 "이름이 주소를 얻는 과정"이고, 역방향으로는 "주소가 다시 이름으로 보이는 과정"입니다. 전자는 모듈 링크와 로딩의 문제이고, 후자는 디버깅과 운영 관측의 문제입니다. Kernel Symbol을 제대로 이해한다는 것은 이 둘을 같이 보는 것입니다.
어떤 심볼이 실제로 존재하는가
커널 개발에서는 흔히 함수 심볼만 떠올리지만, 실제로는 변수 심볼, 어셈블리 진입점(Entry Point), 링커가 만드는 경계 심볼, 모듈이 아직 해결하지 못한 undefined 심볼까지 함께 다뤄야 합니다. 특히 모듈은 최종 실행 파일이 아니라 재배치 가능한 오브젝트에 가깝기 때문에, "나중에 해결될 심볼"이라는 범주가 중요합니다.
| 종류 | 예시 | 언제 보이는가 | 주의점 |
|---|---|---|---|
| 로컬 심볼 | static int helper(void) | 컴파일 후 ELF에는 남을 수 있음 | 외부 모듈에서 링크 불가 |
| 전역 비export 심볼 | int core_fn(void) | nm vmlinux에서는 보일 수 있음 | 모듈 로딩에 필요한 ABI 공개와는 별개 |
| exported 함수 심볼 | EXPORT_SYMBOL(foo) | __ksymtab, Module.symvers | 한 번 공개하면 유지 비용이 커짐 |
| GPL 전용 심볼 | EXPORT_SYMBOL_GPL(foo) | __ksymtab_gpl | 비GPL 모듈은 사용 불가 |
| namespaced 심볼 | EXPORT_SYMBOL_NS(foo, USB_STORAGE) | export 테이블 + namespace 메타데이터 | 소비 모듈의 MODULE_IMPORT_NS 필요 |
| 링커 심볼 | __start___ksymtab, _stext | 링크 후 생성 | 소스에 직접 정의하지 않아도 생김 |
| undefined 심볼 | printk, kmalloc | .ko의 재배치 엔트리 | 런타임 또는 modpost가 해결해야 함 |
/* 제공자 모듈: 외부에 최소 API만 공개 */
#include <linux/module.h>
#include <linux/device.h>
static int do_hw_sequence(struct device *dev)
{
/* 내부 구현: export하지 않음 */
return 0;
}
int mysubsys_register_device(struct device *dev)
{
return do_hw_sequence(dev);
}
EXPORT_SYMBOL_NS_GPL(mysubsys_register_device, MYSUBSYS);
MODULE_LICENSE("GPL");
/* 소비 모듈: namespaced exported 심볼만 사용 */
#include <linux/module.h>
#include <linux/device.h>
extern int mysubsys_register_device(struct device *dev);
MODULE_IMPORT_NS(MYSUBSYS);
static int __init consumer_init(void)
{
/* 실제 예제에서는 유효한 device 포인터를 넘겨야 함 */
return 0;
}
module_init(consumer_init);
MODULE_LICENSE("GPL");
EXPORT 매크로와 공개 정책
EXPORT_SYMBOL*는 "이 함수가 편하니 공개한다"는 매크로가 아닙니다. 이 매크로를 붙이는 순간 다른 모듈이 해당 이름에 의존할 수 있고, 이후 시그니처 변경이나 의미 변경이 빌드/런타임 실패로 이어질 수 있습니다. 따라서 export는 구현 편의보다 ABI 비용을 먼저 계산해야 합니다.
가장 중요한 기준은 세 가지입니다. 첫째, 정말 외부 모듈이 필요로 하는가. 둘째, GPL 전용으로 제한할 근거가 있는가. 셋째, 특정 서브시스템 내부 소비만 허용하려면 네임스페이스를 붙일 것인가. 최근 커널 트리에서 namespace 도입이 강조되는 이유도, 심볼 수 자체를 줄이면서 의존성 경계를 명확히 하기 위해서입니다.
| 매크로 | 의미 | 언제 쓰는가 | 대표 실수 |
|---|---|---|---|
EXPORT_SYMBOL | 일반 공개 | 외부 모듈이 넓게 사용해야 하는 안정 API | 내부 구현 상세까지 무분별하게 공개 |
EXPORT_SYMBOL_GPL | GPL 모듈에만 공개 | 내부 성격이 강하거나 유지정책을 강하게 통제할 때 | MODULE_LICENSE 제약을 빼먹고 사용 |
EXPORT_SYMBOL_NS | namespace 포함 일반 공개 | 서브시스템 경계를 명시하고 싶을 때 | 소비 모듈의 MODULE_IMPORT_NS 누락 |
EXPORT_SYMBOL_NS_GPL | GPL + namespace | 내부 서브시스템 공용 API | namespace 이름이 자주 바뀌는 경우 유지비 증가 |
MODULE_IMPORT_NS | namespace 사용 선언 | namespaced export를 참조하는 모듈 | 오타나 조건부 컴파일로 import가 빠짐 |
symbol_get | 선택적 심볼 획득 | 기능이 있으면 쓰고 없으면 우회하는 선택적 연동 | symbol_put 누락으로 참조 카운트(Reference Count) 불균형 |
빌드 단계: __ksymtab, __kcrctab, modpost, Module.symvers
커널 심볼 시스템의 핵심은 "코드에 붙은 export 매크로"가 빌드 후처리에서 실제 메타데이터로 변환된다는 점입니다. 이 변환을 담당하는 것이 scripts/mod/modpost입니다. 모듈 빌드는 컴파일과 링크만으로 끝나지 않고, 마지막에 modpost가 exported symbol과 undefined symbol의 관계를 수집해 Module.symvers, .mod.c, CRC 정보를 만듭니다.
실무에서는 많은 문제가 이 단계에서 먼저 발견됩니다. 예를 들어 undefined symbol, GPL 제약 위반, namespace import 누락, section mismatch, modversions 불일치 가능성 등이 modpost 경고나 오류로 나타납니다. 즉, modpost는 "나중에 로딩해 보면 알겠지"를 막아 주는 첫 번째 방어선입니다.
| 구성 요소 | 역할 | 무엇을 보면 되는가 | 문제 징후 |
|---|---|---|---|
__ksymtab | 일반 export 심볼 목록 | readelf -S, objdump -h | export가 기대와 다르게 누락 |
__ksymtab_gpl | GPL 전용 export 목록 | readelf -S | 비GPL 모듈 로딩 실패 |
__kcrctab* | 심볼 CRC 저장 | readelf -S, Module.symvers | 버전 불일치 경고 |
.mod.c | 자동 생성 모듈 메타데이터 | 빌드 디렉터리의 *.mod.c | versions 배열, vermagic 확인 필요 |
Module.symvers | exported symbol와 CRC 요약표 | head Module.symvers | 기대한 심볼이 목록에 없음 |
modpost | 심볼/라이선스/섹션/namespace 검사 | 빌드 로그 | WARNING:, ERROR: 문구 |
# modpost 이후 생성되는 Module.symvers 예시
0x9f4a1c32 mysubsys_register_device drivers/mysubsys/core EXPORT_SYMBOL_NS_GPL
0x19d4c2c0 mysubsys_unregister_device drivers/mysubsys/core EXPORT_SYMBOL_NS_GPL
0x51eb9c7a mysubsys_get_stats drivers/mysubsys/core EXPORT_SYMBOL_GPL
# 모듈의 섹션과 export 관련 테이블 확인
readelf -S mysubsys.ko | grep -E '__ksymtab|__kcrctab|__versions'
# 심볼 테이블 확인
readelf -Ws mysubsys.ko | less
# modpost 결과물 확인
head -20 Module.symvers
sed -n '1,120p' mysubsys.mod.c
__ksymtab 섹션과 ELF 관점은 ELF 문서가 이어서 설명합니다.
__ksymtab 내부 구조: export 매크로가 실제로 만드는 것
EXPORT_SYMBOL()을 호출하면 단순히 플래그가 켜지는 것이 아닙니다. 전처리기와 컴파일러가 __ksymtab 섹션에 구조체(Struct) 엔트리를 하나 만들고, 심볼 이름 문자열과 네임스페이스 문자열을 별도 섹션에 저장합니다. 커널 5.19 이후로는 struct kernel_symbol이 상대 오프셋(Offset) 기반으로 바뀌어 바이너리 크기가 줄었고, 위치 독립 코드(PIC)와의 호환성이 향상되었습니다.
이 내부 구조를 이해하면 "왜 readelf -S로 __ksymtab 크기를 보면 export 수를 추정할 수 있는지", "왜 __kcrctab이 별도로 존재하는지", "왜 namespace가 있는 심볼과 없는 심볼이 같은 테이블에 들어가는지"를 자연스럽게 파악할 수 있습니다.
/* include/linux/export.h — 커널 6.x 기준 핵심 구조 */
struct kernel_symbol {
int value_offset; /* 심볼 주소까지의 상대 오프셋 */
int name_offset; /* 심볼 이름 문자열까지의 상대 오프셋 */
int namespace_offset; /* 네임스페이스 문자열까지의 상대 오프셋 (없으면 빈 문자열) */
};
/*
* EXPORT_SYMBOL(foo) 확장 결과 (단순화):
*
* extern typeof(foo) foo; ← 타입 검증
* static const char __kstrtab_foo[] = "foo"; ← 이름 문자열
* static const char __kstrtabns_foo[] = ""; ← namespace (빈 문자열)
*
* __attribute__((section("__ksymtab")))
* const struct kernel_symbol __ksymtab_foo = {
* .value_offset = (unsigned long)&foo - (unsigned long)&__ksymtab_foo.value_offset,
* .name_offset = (unsigned long)__kstrtab_foo - ...,
* .namespace_offset = (unsigned long)__kstrtabns_foo - ...,
* };
*/
/* EXPORT_SYMBOL_NS_GPL(foo, MYNS) 가 추가로 하는 일 */
/* 1. 이름 문자열은 동일하게 생성 */
static const char __kstrtab_foo[] = "foo";
/* 2. namespace 문자열이 비어 있지 않음 */
static const char __kstrtabns_foo[] = "MYNS";
/* 3. GPL 전용이면 __ksymtab_gpl 섹션에 배치 */
__attribute__((section("__ksymtab_gpl")))
const struct kernel_symbol __ksymtab_foo = { /* ... */ };
# __ksymtab 섹션 크기로 export 심볼 수 추정
readelf -S vmlinux | grep __ksymtab
# 출력 예: __ksymtab PROGBITS ... 0001e3c0 (크기)
# 크기 / sizeof(struct kernel_symbol) = 대략적인 export 수
# __ksymtab_strings에서 실제 이름 확인
readelf -p __ksymtab_strings vmlinux | head -30
# 특정 심볼의 export 엔트리 확인
nm vmlinux | grep __ksymtab | grep printk
struct kernel_symbol이 절대 주소 포인터(unsigned long)를 직접 저장했습니다. 오래된 문서나 예제를 보고 현재 커널에 적용하면 구조체 크기 계산이 맞지 않아 export 수 추정이 2배 차이 나거나, 커스텀 파싱 도구가 작동하지 않을 수 있습니다. 반드시 대상 커널 버전의 include/linux/export.h를 먼저 확인하세요.
kallsyms 내부: 압축 알고리즘과 주소 역해석 경로
/proc/kallsyms가 수만 개 심볼을 빠르게 보여줄 수 있는 이유는 심볼 이름이 토큰 기반으로 압축되어 있기 때문입니다. 빌드 시 scripts/kallsyms 도구가 전체 심볼 이름에서 가장 빈번한 바이트 쌍을 반복적으로 토큰으로 치환하는 방식을 사용합니다. 이것이 커널 이미지 안의 kallsyms_names, kallsyms_token_table, kallsyms_token_index 배열입니다.
주소에서 이름을 찾는 역해석 경로는 다음과 같습니다. 먼저 kallsyms_addresses (또는 kallsyms_offsets + kallsyms_relative_base) 배열을 이진 검색하여 가장 가까운 심볼 인덱스를 찾고, 그 인덱스로 kallsyms_names에서 압축된 이름을 꺼낸 뒤, 토큰 테이블로 원래 문자열을 복원합니다. 이 과정이 sprint_symbol(), %pS 포맷 스트링, __print_symbol() 등의 핵심 경로입니다.
/* kernel/kallsyms.c — 주소 → 이름 역해석 핵심 경로 (단순화) */
/* 1. 주소로 가장 가까운 심볼 인덱스 찾기 */
static unsigned long get_symbol_pos(unsigned long addr,
unsigned long *symbolsize,
unsigned long *offset)
{
unsigned long i, low, high, mid;
/* 이진 검색으로 addr 이하의 가장 큰 심볼 주소를 찾는다 */
low = 0;
high = kallsyms_num_syms;
while (high - low > 1) {
mid = low + (high - low) / 2;
if (kallsyms_sym_address(mid) <= addr)
low = mid;
else
high = mid;
}
/* low가 해당 심볼의 인덱스 */
return low;
}
/* 2. 인덱스로 압축된 이름을 찾아 토큰 테이블로 복원 */
static unsigned int kallsyms_expand_symbol(
unsigned int off, char *result, size_t maxlen)
{
int len, skipped_first = 0;
const char *tptr;
const u8 *data = &kallsyms_names[off];
len = *data; /* 첫 바이트 = 압축된 이름 길이 */
data++;
while (len) {
/* 각 바이트를 토큰 테이블에서 원래 문자열로 치환 */
tptr = &kallsyms_token_table[kallsyms_token_index[*data]];
data++; len--;
while (*tptr) {
if (skipped_first) {
if (maxlen <= 1) goto tail;
*result = *tptr; result++; maxlen--;
} else
skipped_first = 1;
tptr++;
}
}
tail:
*result = '\0';
return off;
}
| kallsyms 배열 | 역할 | 생성 시점 | 관련 CONFIG |
|---|---|---|---|
kallsyms_addresses | 심볼 주소 배열 (절대 주소 방식) | 빌드 마지막 링크 | CONFIG_KALLSYMS |
kallsyms_offsets + relative_base | 심볼 주소 배열 (상대 오프셋 방식) | 빌드 마지막 링크 | CONFIG_KALLSYMS_BASE_RELATIVE |
kallsyms_names | 압축된 심볼 이름 바이트열 | scripts/kallsyms | CONFIG_KALLSYMS |
kallsyms_num_syms | 전체 심볼 수 | scripts/kallsyms | CONFIG_KALLSYMS |
kallsyms_token_table | 토큰 → 문자열 매핑(Mapping) 테이블 | scripts/kallsyms | CONFIG_KALLSYMS |
kallsyms_token_index | 토큰 ID → token_table 오프셋 | scripts/kallsyms | CONFIG_KALLSYMS |
kallsyms_markers | 256개 단위 점프 테이블 (검색 가속) | scripts/kallsyms | CONFIG_KALLSYMS |
CONFIG_KALLSYMS는 전역 함수 심볼만 포함하지만, CONFIG_KALLSYMS_ALL을 켜면 static 함수와 데이터 심볼까지 포함합니다. 디버깅 정밀도가 크게 올라가지만 커널 이미지 크기도 증가합니다. 배포판 커널은 대부분 이 옵션을 켜 둡니다.
Livepatch와 ftrace의 심볼 활용
커널 심볼은 모듈 로딩과 디버깅뿐 아니라, livepatch와 ftrace에서도 핵심적으로 사용됩니다. ftrace는 함수 진입점에 삽입된 fentry/mcount 호출 지점을 심볼 이름으로 식별하고, livepatch는 패치(Patch) 대상 함수를 심볼 이름과 위치(소스 파일, 심볼 인덱스)로 특정합니다.
특히 livepatch의 klp_func 구조체는 패치할 함수를 .old_name 필드로 지정하며, 커널은 kallsyms를 통해 해당 이름의 주소를 찾습니다. 동일 이름의 static 함수가 여러 개 있을 수 있으므로, .old_sympos 필드로 몇 번째 심볼인지까지 지정할 수 있습니다. 이것이 CONFIG_KALLSYMS_ALL이 livepatch에 필수인 이유입니다.
/* livepatch 모듈에서 패치 대상 지정 */
#include <linux/livepatch.h>
static int patched_cmdline_proc_show(struct seq_file *m, void *v)
{
seq_printf(m, "%s (livepatched)\n", saved_command_line);
return 0;
}
static struct klp_func funcs[] = {
{
.old_name = "cmdline_proc_show", /* kallsyms에서 이 이름으로 검색 */
.new_func = patched_cmdline_proc_show,
.old_sympos = 0, /* 동명 심볼이 여럿이면 순번 지정 */
}, { }
};
static struct klp_object objs[] = {
{
.name = NULL, /* NULL = vmlinux (커널 본체) */
.funcs = funcs,
}, { }
};
static struct klp_patch patch = {
.mod = THIS_MODULE,
.objs = objs,
};
/* ftrace에서 심볼 이름으로 함수 추적 등록 */
#include <linux/ftrace.h>
static struct ftrace_ops my_ops = {
.func = my_trace_handler,
.flags = FTRACE_OPS_FL_SAVE_REGS,
};
/* 심볼 이름으로 필터 설정 → kallsyms로 주소 해석 */
ftrace_set_filter(&my_ops, "do_sys_openat2", 0, 0);
register_ftrace_function(&my_ops);
| 기능 | 심볼 활용 방식 | 필수 CONFIG | 주의점 |
|---|---|---|---|
| ftrace 함수 추적 | 심볼 이름으로 추적 대상 지정 | CONFIG_FUNCTION_TRACER | 인라인된 함수는 추적 불가 |
| kprobe | 심볼 이름 또는 주소로 프로브(Probe) 삽입 | CONFIG_KPROBES | 최적화로 프로브 삽입 불가능한 지점 존재 |
| livepatch | .old_name으로 대상 함수 식별 | CONFIG_LIVEPATCH, CONFIG_KALLSYMS_ALL | 동명 static 함수 구분에 .old_sympos 필요 |
| BPF tracing | bpf_get_func_ip(), kfunc 등에서 심볼 활용 | CONFIG_BPF | kfunc은 별도 BTF 기반 심볼 체계 병행 |
| perf probe | perf probe 심볼명으로 동적 프로브 생성 | CONFIG_KPROBE_EVENTS | DWARF 정보 있으면 지역 변수도 접근 가능 |
static 함수가 서로 다른 파일에 여러 개 있을 때, .old_sympos를 0으로 두면 첫 번째 매칭 심볼을 패치합니다. 잘못된 함수를 패치하면 커널 크래시로 이어질 수 있으므로, 동명 심볼이 있는 경우 반드시 old_sympos를 정확히 지정해야 합니다. grep -c 'T 함수명' /proc/kallsyms로 동명 심볼 수를 미리 확인하세요.
심볼 검색 성능: 해시 테이블(Hash Table)과 이진 검색
모듈 로더가 수천 개의 undefined symbol을 해석할 때, 심볼 검색이 느리면 모듈 로딩 시간이 크게 늘어납니다. 커널은 이를 위해 두 가지 검색 메커니즘을 병행합니다. 첫째, vmlinux의 __ksymtab은 이름순 정렬되어 이진 검색이 가능합니다. 둘째, 이미 로드된 모듈들의 export 심볼은 모듈별 테이블을 순회하되, 최근 커널에서는 해시(Hash) 기반 캐시(Cache)를 사용하여 검색 속도를 높입니다.
/* kernel/module/main.c — 심볼 검색 핵심 경로 (단순화) */
/* vmlinux export 테이블에서 이진 검색 */
static const struct kernel_symbol *
lookup_exported_symbol(const char *name,
const struct kernel_symbol *start,
const struct kernel_symbol *stop)
{
unsigned long lo = 0;
unsigned long hi = stop - start;
while (lo < hi) {
unsigned long mid = lo + (hi - lo) / 2;
int cmp = strcmp(name, kernel_symbol_name(&start[mid]));
if (cmp < 0)
hi = mid;
else if (cmp > 0)
lo = mid + 1;
else
return &start[mid];
}
return NULL;
}
/* 전체 검색 순서: vmlinux → 로드된 모듈들 순회 */
static const struct kernel_symbol *
find_symbol(const char *name, struct module **owner,
const s32 **crc, enum mod_license *license,
bool gplok)
{
/* 1. vmlinux __ksymtab 검색 (이진 검색) */
/* 2. vmlinux __ksymtab_gpl 검색 (gplok이면) */
/* 3. 로드된 모듈 목록 순회 */
}
time insmod으로 로딩 시간을 측정하고, ftrace로 find_symbol 호출 횟수와 소요 시간을 확인해 보세요.
런타임 단계: 모듈 로더와 심볼 해석
빌드가 끝나도 심볼 문제는 아직 끝나지 않습니다. 모듈 파일은 여전히 재배치 정보와 undefined symbol 참조를 포함하고 있기 때문에, 실제 로딩 시 커널이 현재 실행 중인 커널 이미지와 이미 로드된 모듈들의 export 테이블을 기준으로 주소를 채워 넣어야 합니다. 이때 심볼 이름이 존재하는지뿐 아니라, 라이선스 제약, namespace 선언, CRC 일치 여부, 아키텍처별 재배치 타입까지 함께 검증합니다.
따라서 "빌드되었다"와 "로드된다"는 전혀 다른 단계입니다. 빌드 시점에는 헤더와 심볼 이름이 맞았더라도, 런타임 커널이 다른 설정이나 다른 CRC를 가질 수 있고, GPL/namespace 정책이 달라질 수도 있습니다. DKMS나 외부 모듈에서 이런 문제가 특히 자주 나타납니다.
# 모듈이 어떤 외부 심볼을 참조하는지 먼저 본다
readelf -r myconsumer.ko | head -20
# 어떤 심볼이 undefined 상태인지 본다
nm -u myconsumer.ko
# 커널이 현재 제공하는 심볼 목록에서 찾는다
grep mysubsys_register_device /proc/kallsyms
# 로딩 직후 커널 로그를 확인한다
dmesg | tail -50
# 자주 보는 런타임 실패 예시
insmod: ERROR: could not insert module myconsumer.ko: Unknown symbol in module
dmesg:
myconsumer: Unknown symbol mysubsys_register_device (err -2)
myconsumer: module uses symbols from namespace MYSUBSYS, but does not import it
myconsumer: disagrees about version of symbol mysubsys_register_device
/proc/kallsyms에서 이름이 보인다고 해서 항상 로딩 가능한 것은 아닙니다.
동일 이름이 있어도 GPL 전용, namespace 미import, CRC mismatch, vermagic 차이, 아키텍처 재배치 문제 때문에 로딩은 실패할 수 있습니다.
kallsyms, System.map, nm, readelf로 추적하는 법
커널 심볼을 실제로 체감하는 순간은 대개 장애 분석입니다. Oops에 찍힌 주소를 함수명으로 바꾸거나, 함수 포인터를 %pS로 찍거나, 특정 주소가 어느 섹션과 어느 모듈에 속하는지 확인하는 작업은 모두 심볼 정보를 기반으로 합니다. 여기서 가장 많이 쓰이는 도구가 /proc/kallsyms, System.map, nm, readelf, objdump, GDB입니다.
/proc/kallsyms는 현재 실행 중 커널의 심볼 사전이고, System.map은 빌드 또는 설치 시점의 정적 주소-이름 매핑입니다. 전자는 런타임 상태를 반영하지만 권한 제약과 KASLR 영향이 있고, 후자는 설치된 커널 이미지와 정확히 맞을 때 강력합니다. 모듈 자체를 분석할 때는 nm과 readelf가 더 직접적입니다.
| 도구 | 주 용도 | 강점 | 제약 |
|---|---|---|---|
/proc/kallsyms | 실행 중 주소를 이름으로 확인 | 현재 커널 상태 반영 | kptr_restrict, 권한 제약, KASLR 영향 |
System.map | 정적 커널 이미지 주소 확인 | Oops 해석의 기준점 | 실행 중 커널 이미지와 정확히 맞아야 함 |
nm | 심볼 종류와 정의/미정의 확인 | 간단하고 빠름 | 재배치 세부 정보는 부족 |
readelf -Ws/-r | 심볼 테이블과 재배치 확인 | ELF 구조를 정밀하게 볼 수 있음 | 출력이 길고 해석 비용이 큼 |
objdump -dr | 기계어(Machine Code)와 relocation 같이 보기 | 실제 참조 지점을 확인 가능 | 어셈블리 독해 필요 |
GDB | 소스/함수/주소 상호 이동 | 백트레이스와 코드 흐름 분석에 강함 | 적절한 vmlinux 심볼 파일 필요 |
# 실행 중 커널에서 함수 주소 확인
grep ' do_sys_open$' /proc/kallsyms
# 모듈에서 미해결 외부 심볼만 추리기
nm -u myconsumer.ko
# 심볼 테이블과 바인딩/타입까지 보기
readelf -Ws myconsumer.ko | grep mysubsys
# 재배치가 어떤 심볼을 향하는지 보기
readelf -r myconsumer.ko | grep mysubsys
# vmlinux에서 주소를 소스 라인으로 역변환
gdb vmlinux
(gdb) info address do_sys_open
(gdb) list *do_sys_open
# printk에서 함수 포인터를 심볼명으로 출력
pr_info("cb=%pS raw=%px\n", cb, cb);
addr2line보다 먼저 %pS, /proc/kallsyms, System.map 정합성을 확인하는 편이 빠릅니다. 특히 KASLR이 켜져 있으면 정적 주소와 런타임 주소를 그대로 비교하면 안 됩니다.
CONFIG_MODVERSIONS와 CRC: 왜 이름이 같아도 거부되는가
CONFIG_MODVERSIONS가 켜져 있으면 커널은 exported symbol마다 시그니처 기반 CRC를 관리합니다. 이 CRC는 단순히 이름이 같은지 보는 것이 아니라, 해당 심볼의 인터페이스가 "같은 계약"인지 확인하기 위한 요약값입니다. 그래서 이름이 그대로여도 인자 타입, 구조체 선언, 일부 전처리 결과가 달라지면 CRC가 바뀌어 모듈 로딩이 거부될 수 있습니다.
이 메커니즘은 안정 ABI를 보장하는 장치가 아닙니다. 오히려 "인터페이스가 바뀌었는데 오래된 모듈이 모르고 로딩되는 것"을 막기 위한 장치입니다. 따라서 외부 모듈 유지보수 관점에서는 CRC mismatch를 해결하기 위해 억지로 강제 로드하는 것이 아니라, 동일한 헤더와 설정으로 재빌드하는 것이 정석입니다.
| 항목 | 의미 | 어디서 확인 | 실수 |
|---|---|---|---|
| 심볼 CRC | exported symbol 인터페이스 요약값 | Module.symvers, __versions | 이름만 같으면 호환된다고 생각 |
__versions | 모듈이 기대하는 외부 심볼 CRC 목록 | readelf -S, *.mod.c | 모듈 안의 CRC와 커널 쪽 CRC를 따로 보지 않음 |
genksyms | 심볼 서명을 계산하는 빌드 도구 | 빌드 로그, Kbuild 내부 | 헤더 차이를 가볍게 봄 |
vermagic | 커널/모듈 빌드 속성 문자열 | modinfo | CRC mismatch와 vermagic mismatch를 혼동 |
# 모듈의 버전 매직 확인
modinfo myconsumer.ko | grep vermagic
# CRC 테이블이 들어갔는지 확인
readelf -S myconsumer.ko | grep __versions
# 현재 커널용 Module.symvers와 비교
grep mysubsys_register_device Module.symvers
# CRC 불일치 대표 예시
myconsumer: disagrees about version of symbol mysubsys_register_device
myconsumer: Unknown symbol mysubsys_register_device (err -22)
insmod: ERROR: could not insert module myconsumer.ko: Invalid module format
보안과 정책: GPL 전용 심볼, namespace, 주소 노출 제약
커널 심볼 시스템은 단지 편의성 구조가 아니라 정책 집행 수단이기도 합니다. GPL 전용 심볼은 내부 API의 무분별한 외부 사용을 억제하고, namespace는 서브시스템 경계를 강제하며, kptr_restrict와 KASLR은 주소 노출을 제한합니다. 즉, 커널 심볼은 ABI와 디버깅뿐 아니라 보안 경계의 일부이기도 합니다.
특히 운영 환경에서 /proc/kallsyms 주소가 0으로 보이는 현상은 이상이 아니라 정책 적용 결과일 수 있습니다. 반대로 개발 환경에서는 너무 쉽게 주소를 열어 두면, 실제 운영 제약을 놓친 상태로 문제를 재현하게 됩니다. 문서화와 재현 환경에서 이 차이를 의식해야 합니다.
| 정책 | 무엇을 막는가 | 어디서 드러나는가 | 개발 시 주의점 |
|---|---|---|---|
| GPL 전용 export | 비GPL 모듈의 내부 API 사용 | 로딩 실패, taint 상태 | MODULE_LICENSE만 바꾸는 식의 우회는 본질 해결이 아님 |
| symbol namespace | 서브시스템 경계 없는 무차별 참조 | modpost 경고, 로딩 실패 | 공급자와 소비자의 namespace를 같이 관리 |
kptr_restrict | 주소 직접 노출 | /proc/kallsyms 주소 마스킹 | 권한 차이 때문에 재현 결과가 달라질 수 있음 |
| KASLR | 정적 주소 예측 | 디버깅 시 주소 불일치 | 정적 이미지 주소와 런타임 주소를 곧바로 비교하지 않음 |
CONFIG_TRIM_UNUSED_KSYMS | 불필요한 export 남발 | 빌드 결과 export 목록 축소 | 구성에 따라 보이던 심볼이 사라질 수 있음 |
/* 선택적 연동 패턴: 심볼이 있을 때만 기능 사용 */
#include <linux/module.h>
extern int optional_accel_submit(const void *buf, size_t len);
static int (*accel_submit_ptr)(const void *buf, size_t len);
static int try_accel(const void *buf, size_t len)
{
accel_submit_ptr = symbol_get(optional_accel_submit);
if (!accel_submit_ptr)
return -EOPNOTSUPP;
if (accel_submit_ptr(buf, len))
pr_warn("accelerator submit failed\n");
symbol_put(optional_accel_submit);
return 0;
}
실전 Oops 해석 워크스루: 심볼 정보로 장애 추적하기
커널 심볼에 대한 이론적 이해가 실전에서 가장 빛나는 순간은 Oops 분석입니다. 실제 Oops 메시지를 단계별로 해석하며, 심볼 정보가 어떻게 활용되는지 보겠습니다.
# 실제 Oops 출력 예시 (일부 단순화)
BUG: unable to handle page fault at fffffffe00000008
IP: [<ffffffff8127a3c5>] slab_free+0x45/0x2a0
PGD 0 P4D 0
Oops: 0002 [#1] PREEMPT SMP NOPTI
CPU: 3 PID: 1842 Comm: myworker Tainted: G W O 6.8.0-custom
RIP: 0010:slab_free+0x45/0x2a0
...
Call Trace:
<TASK>
kfree+0x62/0x140
my_driver_cleanup+0x1e/0x50 [my_driver]
my_driver_disconnect+0xab/0x120 [my_driver]
usb_unbind_interface+0x7e/0x280
__device_release_driver+0x17a/0x230
...
</TASK>
# Step 1: Oops의 RIP 주소를 심볼로 확인
# Oops 자체가 이미 "slab_free+0x45/0x2a0"으로 해석해 줌
# 만약 심볼 없이 주소만 나왔다면:
grep ffffffff8127a3c5 /proc/kallsyms
# 또는 System.map으로:
awk '$1 ~ /ffffffff8127a/ { print }' /boot/System.map-$(uname -r)
# Step 2: 모듈 내 오프셋을 소스 라인으로 변환
# my_driver_cleanup+0x1e 가 어느 줄인지 확인
# 먼저 모듈의 .text 시작 주소를 알아야 한다
cat /sys/module/my_driver/sections/.text
# 예: 0xffffffffc0820000
# Step 3: addr2line 또는 GDB로 소스 위치 확인
addr2line -e my_driver.ko -fip 0x1e
# 또는
gdb my_driver.ko
(gdb) list *(my_driver_cleanup+0x1e)
# Step 4: objdump로 해당 오프셋의 실제 명령어 확인
objdump -dr my_driver.ko | grep -A5 'my_driver_cleanup>:'
# Step 5: 콜 트레이스에서 모듈(.ko)과 커널 함수 구분
# [my_driver] 표시가 있으면 모듈 함수
# 표시 없으면 vmlinux (커널 본체) 함수
| Oops 정보 | 심볼 역할 | 확인 도구 |
|---|---|---|
RIP: slab_free+0x45/0x2a0 | 충돌 주소를 함수명+오프셋으로 역해석 | kallsyms 자동 해석 |
Call Trace: kfree+0x62 | 호출 스택을 심볼 이름으로 표시 | kallsyms + 스택 언와인더 |
[my_driver] 모듈 표시 | 해당 함수가 어느 모듈에 속하는지 식별 | 모듈 메모리 영역 매핑 |
Tainted: G W O | 오염 플래그 (외부 모듈 로드 여부 등) | G=GPL, O=외부 모듈, W=경고 발생 |
System.map이나 vmlinux와 직접 비교하면 안 됩니다. 대신 오프셋(slab_free+0x45)을 기준으로 분석하거나, /proc/kallsyms에서 해당 부팅 세션의 실제 주소를 확인한 뒤 차이를 계산해야 합니다. cat /proc/kallsyms | head -1의 기본 주소와 System.map의 기본 주소 차이가 KASLR 오프셋입니다.
문제 해결: Unknown symbol에서 CRC mismatch까지
커널 심볼 문제는 대부분 증상은 짧고 원인은 길게 퍼져 있습니다. 빌드는 되었지만 로드가 안 되거나, 주소는 보이는데 함수명이 다르거나, namespace 경고가 나오거나, DKMS 모듈이 업데이트 뒤 갑자기 깨지는 식입니다. 이럴 때는 "어느 단계에서 계약이 끊어졌는가"를 기준으로 조사해야 합니다.
가장 실전적인 순서는 다음과 같습니다. 먼저 빌드 로그에서 modpost 경고를 확인하고, 그다음 nm -u와 readelf -r로 미해결 참조를 보고, 이어서 /proc/kallsyms와 Module.symvers에서 공급자 쪽 상태를 확인합니다. 마지막으로 라이선스, namespace, CRC, vermagic 순서로 좁혀가면 됩니다.
| 증상 | 가장 흔한 원인 | 먼저 볼 것 | 수정 방향 |
|---|---|---|---|
Unknown symbol foo | export 안 됨, 공급 모듈 미로드, 이름 오타 | nm -u, /proc/kallsyms, Module.symvers | 공급자 export 확인, 로드 순서 확인, 이름 정합성 재검토 |
GPL-incompatible module uses GPL-only symbol | 비GPL 모듈이 GPL 심볼 사용 | MODULE_LICENSE, 공급자 export 종류 | 설계 재검토 또는 GPL 호환 정책 정리 |
| namespace 관련 경고/실패 | MODULE_IMPORT_NS 누락 | 빌드 로그, dmesg | 소비 모듈에 namespace import 추가 |
disagrees about version of symbol | CRC mismatch | Module.symvers, __versions, modinfo | 현재 커널과 동일 조건으로 재빌드 |
Invalid module format | vermagic, CRC, 아키텍처, 서명 문제 | modinfo, dmesg | 커널/모듈 빌드 환경 맞추기 |
/proc/kallsyms 주소가 0 | kptr_restrict, 권한 제한 | sysctl 값, root 여부 | 권한/정책 차이를 고려해 확인 |
# 1. 빌드 단계 확인
make V=1
# 2. 미해결 외부 심볼 확인
nm -u myconsumer.ko
# 3. 재배치가 어떤 심볼을 참조하는지 확인
readelf -r myconsumer.ko | less
# 4. 공급자 심볼이 실제로 export됐는지 확인
grep mysubsys_register_device Module.symvers
grep mysubsys_register_device /proc/kallsyms
# 5. 정책 문제 확인
modinfo myconsumer.ko
dmesg | tail -100
dmesg 마지막 한 줄만 보고 끝내지 마세요.
modpost 경고, nm -u, readelf -r, Module.symvers 네 가지를 같이 보면 원인 분류 속도가 크게 빨라집니다.
심볼 진화: 추가, 폐기, 제거의 실무 흐름
커널 심볼은 한 번 export되면 영구적이라는 오해가 있지만, 실제로는 추가, 폐기(deprecation), 제거의 수명주기를 가집니다. 다만 이 과정은 사용자 공간(User Space) ABI와 달리 공식적인 안정성 보장이 없어서(커널 내부 API는 불안정이 원칙), 오히려 더 주의 깊은 관리가 필요합니다.
CONFIG_TRIM_UNUSED_KSYMS는 이 관리를 자동화하는 시도입니다. 트리 내 모듈이 실제로 참조하지 않는 export를 빌드 시점에 제거하여, 불필요한 ABI 표면을 줄입니다. 다만 이 옵션은 트리 외부 모듈을 고려하지 않으므로, DKMS나 외부 모듈에 의존하는 배포판에서는 대부분 비활성화 상태입니다.
/* 심볼 폐기 패턴: 경고 후 제거 */
/* 단계 1: 새 API를 만들고 기존 API에 경고 추가 */
int subsys_new_register(struct device *dev, unsigned int flags)
{
/* 새로운 구현 */
return 0;
}
EXPORT_SYMBOL_GPL(subsys_new_register);
/* 기존 API: 내부적으로 새 API를 호출하면서 경고 출력 */
int subsys_register(struct device *dev)
{
pr_warn_once("subsys_register() is deprecated, use subsys_new_register()\n");
return subsys_new_register(dev, 0);
}
EXPORT_SYMBOL_GPL(subsys_register);
/* 단계 2: 모든 트리 내 사용자를 새 API로 전환 후 export 제거 */
/* 단계 3: 함수 자체를 static으로 전환하거나 삭제 */
| 설정 | 동작 | 사용 시나리오 | 제약 |
|---|---|---|---|
CONFIG_TRIM_UNUSED_KSYMS | 트리 내 모듈이 참조하지 않는 export 제거 | 임베디드 최소 빌드 | 외부 모듈이 사용하는 심볼까지 제거될 수 있음 |
CONFIG_UNUSED_KSYMS_WHITELIST | TRIM에서 제외할 심볼 목록 파일 지정 | 특정 외부 모듈 지원 유지 | 수동 관리 필요, 목록 동기화 비용 |
__deprecated 어노테이션 | 컴파일 경고로 사용 중지 유도 | 점진적 API 전환 | 경고를 무시하는 코드가 많으면 효과 미미 |
TRIM_UNUSED_KSYMS로 잘려 나가면, Unknown symbol으로 로딩이 실패합니다. 이런 경우 CONFIG_UNUSED_KSYMS_WHITELIST에 심볼을 등록하거나, 해당 설정을 비활성화하는 것이 해법입니다. 장기적으로는 외부 모듈을 트리 내로 편입시키는 것이 가장 안정적입니다.
실무 원칙: 무엇을 export하지 말아야 하는가
Kernel Symbol 문서의 마지막은 "어떻게 쓰는가"보다 "언제 공개하지 않을 것인가"로 끝나야 합니다. export는 재사용성의 출발이 아니라 유지보수 의무의 시작이기 때문입니다. 심볼 하나를 공개하면 다른 모듈이 의존하고, 그 의존은 코드 검색만으로 끝나지 않고 외부 트리, DKMS, 배포판 패키지, 운영 스크립트까지 퍼질 수 있습니다.
따라서 export 전 체크리스트는 기능성보다 경계성을 봐야 합니다. 단순 헬퍼, 락이 노출된 내부 구조, 수명주기가 불안정한 객체 접근자, 디버깅 전용 임시 함수, 직접 필드 접근을 강제하는 구조체 API는 원칙적으로 export 후보가 아닙니다. 대신 wrapper, opaque handle, namespace, 문서화된 수명 규약을 함께 설계해야 합니다.
| 상황 | 권장 선택 | 이유 |
|---|---|---|
| 한 모듈만 쓰는 내부 헬퍼 | static 유지 | ABI 표면을 늘릴 이유가 없음 |
| 여러 함수 조합이 필요한 복잡한 내부 절차 | wrapper 1개만 export | 내부 순서/락/상태 전이를 숨길 수 있음 |
| 서브시스템 내부 공용 API | EXPORT_SYMBOL_NS_GPL | 경계와 라이선스 정책을 같이 명시 가능 |
| 디버깅 전용 함수 | export 대신 tracepoint/debugfs 검토 | 디버그 훅을 ABI로 굳히지 않음 |
| 옵션 기능 연동 | symbol_get 또는 명시적 의존성 | 강한 링크와 약한 링크를 구분 가능 |
- 최소 공개: 외부 사용자가 정말 필요한 의미 단위만 export합니다.
- 수명주기 문서화: 호출자가 락, 참조 카운트, sleep 가능 여부를 지켜야 한다면 commit message와 코드 주석에 남깁니다.
- namespace 우선: 서브시스템 내부 공용 API라면 처음부터 namespace를 붙여 경계를 명확히 합니다.
- 정책 일관성: 비슷한 성격의 심볼은 같은 export 정책을 유지해야 소비자가 예측할 수 있습니다.
- 문제 재현성: export를 추가한 커밋에는 어떤 모듈이 왜 필요한지, modpost/로딩 검증을 어떻게 했는지 남깁니다.
심볼 가시성 제어: static에서 EXPORT까지
커널 심볼의 가시성(Visibility)은 C 언어의 static 키워드(Keyword)에서 시작하여, ELF 심볼 바인딩, 그리고 커널 고유의 EXPORT_SYMBOL* 매크로까지 여러 단계에 걸쳐 결정됩니다. 각 단계는 서로 다른 수준의 접근 범위를 제공하며, 이 차이를 정확히 이해해야 "왜 nm에 보이는데 모듈이 못 쓰는가" 같은 질문에 답할 수 있습니다.
MODULE_LICENSE와 GPL 호환성
MODULE_LICENSE() 매크로는 모듈의 라이선스를 선언하며, 이 선언이 GPL 호환인지 여부에 따라 GPL 전용 심볼 접근이 허용됩니다. 커널은 license_is_gpl_compatible() 함수로 이를 확인합니다.
| MODULE_LICENSE 문자열 | GPL 호환 | GPL 심볼 접근 | taint |
|---|---|---|---|
"GPL" | 예 | 가능 | G (정상) |
"GPL v2" | 예 | 가능 | G |
"GPL and additional rights" | 예 | 가능 | G |
"Dual BSD/GPL" | 예 | 가능 | G |
"Dual MIT/GPL" | 예 | 가능 | G |
"Dual MPL/GPL" | 예 | 가능 | G |
"Proprietary" | 아니오 | 불가 | P (taint) |
| (없음 또는 빈 문자열) | 아니오 | 불가 | P (taint) |
/* kernel/module/main.c — 라이선스 호환성 확인 */
static bool license_is_gpl_compatible(const char *license)
{
return (strcmp(license, "GPL") == 0
|| strcmp(license, "GPL v2") == 0
|| strcmp(license, "GPL and additional rights") == 0
|| strcmp(license, "Dual BSD/GPL") == 0
|| strcmp(license, "Dual MIT/GPL") == 0
|| strcmp(license, "Dual MPL/GPL") == 0);
}
/* 비GPL 모듈이 GPL 심볼 사용 시도 시 */
/* → "module X uses GPL-only symbol Y from module Z" 경고 */
/* → 심볼 해석 실패 → 모듈 로딩 거부 */
taint 플래그 상세
커널 taint 상태는 심볼 정책과 밀접하게 연결됩니다. 비GPL 모듈, 외부 모듈, 비서명 모듈이 로드되면 각각 다른 taint 플래그가 설정되며, taint된 커널의 버그 리포트는 커뮤니티에서 우선순위가 낮아질 수 있습니다.
# 현재 taint 상태 확인
cat /proc/sys/kernel/tainted
# 0 = 깨끗한 상태
# 비트 단위로 해석
# 상세 taint 해석
dmesg | grep "Tainted:"
# Tainted: P --- P = 독점 모듈 로드
# Tainted: G --- G = GPL 모듈만 로드 (정상)
# Tainted: G W O --- W = 경고 발생, O = 외부 모듈
# 어떤 모듈이 taint를 발생시켰는지 확인
grep "module.*loaded" /var/log/kern.log | grep -i "taint"
# 모듈별 taint 상태
for m in $(lsmod | awk 'NR>1 {print $1}'); do
license=$(modinfo -F license $m 2>/dev/null)
echo "$m: $license"
done | grep -v GPL
EXPORT_SYMBOL을 GPL로 변경하는 실제 사례
커널 역사에서 EXPORT_SYMBOL이 EXPORT_SYMBOL_GPL로 변경된 사례는 상당히 많습니다. 이러한 변경은 보통 해당 API가 커널 내부 구현에 너무 깊이 결합되어 있어, 외부 독점 모듈이 이를 사용하면 커널 안정성이 위협받거나 라이선스 의도에 반하는 경우에 이루어집니다. 대표적인 예는 다음과 같습니다.
| 심볼/API | 변경 버전 | 변경 이유 | 영향 |
|---|---|---|---|
usb_register_driver | 2.6.x | USB 코어 내부 구조 노출 | 독점 USB 드라이버 호환성 상실 |
security_* 훅 | 5.x | LSM 내부 구조 보호 | 독점 보안 모듈 차단 |
kallsyms_lookup_name | 5.7 | export 자체 제거 (보안) | 모든 모듈에서 사용 불가 |
| DRM 스케줄러 API | 5.4+ | 내부 API + NS 도입 | NS import 없는 모듈 경고 |
MODULE_LICENSE("Proprietary")로 선언하면서 GPL 심볼을 사용하는 것은 기술적으로 불가능할 뿐 아니라 법적 문제도 야기할 수 있습니다. 독점 모듈이 GPL 심볼을 필요로 하는 경우, 해당 기능을 GPL 모듈 계층(shim layer)으로 분리하고 독점 부분은 안정적인 비GPL API만 사용하도록 설계하는 것이 일반적인 해결책입니다.
EXPORT_SYMBOL_NS 네임스페이스
커널 5.4에서 도입된 EXPORT_SYMBOL_NS는 단순히 이름에 태그를 붙이는 것이 아니라, 서브시스템 간 의존 경계를 컴파일 타임과 로딩 타임 모두에서 강제하는 정책 도구입니다. 네임스페이스가 없는 export는 "아무 모듈이나 링크해도 된다"는 암묵적 허가인 반면, 네임스페이스가 있는 export는 소비 모듈이 MODULE_IMPORT_NS()를 선언해야만 사용 가능합니다.
이 구조의 핵심 가치는 두 가지입니다. 첫째, 서브시스템 유지보수자가 "이 심볼은 우리 서브시스템 내부에서만 쓰도록 의도한다"는 의사를 코드에 기록할 수 있습니다. 둘째, 외부 모듈 개발자가 의도하지 않은 내부 API에 실수로 의존하는 것을 방지합니다. 네임스페이스 위반은 modpost 경고와 커널 로딩 시 경고 메시지를 발생시키며, 향후 커널 버전에서는 로딩 거부로 강화될 수 있습니다.
네임스페이스 메타데이터는 Module.symvers의 네 번째 필드에 기록되며, 모듈의 .modinfo 섹션에도 import_ns로 저장됩니다. 모듈 로더는 이 두 정보를 비교하여 네임스페이스 일치 여부를 검증합니다. 이 검증은 심볼 CRC 확인 이후, 실제 재배치 이전에 수행되므로, 네임스페이스 위반은 심볼 해석의 초기 단계에서 감지됩니다.
서브시스템 경계 설정 사례
네임스페이스의 핵심 가치를 이해하기 위해 실제 커널 트리에서 네임스페이스가 어떻게 서브시스템 경계를 형성하는지 살펴봅니다. DRM(Direct Rendering Manager) 서브시스템은 가장 먼저 네임스페이스를 적극 도입한 사례 중 하나입니다.
/* drivers/gpu/drm/drm_drv.c — DRM 핵심 API */
EXPORT_SYMBOL(drm_dev_register); /* 네임스페이스 없음: 범용 */
EXPORT_SYMBOL_GPL(drm_dev_unregister); /* GPL 제한 */
/* drivers/gpu/drm/scheduler/ — GPU 스케줄러 내부 API */
EXPORT_SYMBOL_NS_GPL(drm_sched_init, DRM_GPU_SCHEDULER);
EXPORT_SYMBOL_NS_GPL(drm_sched_fini, DRM_GPU_SCHEDULER);
EXPORT_SYMBOL_NS_GPL(drm_sched_job_init, DRM_GPU_SCHEDULER);
/* drivers/gpu/drm/drm_kms_helper_common.c — KMS 헬퍼 */
EXPORT_SYMBOL_NS(drm_fbdev_generic_setup, DRM_KMS_HELPER);
EXPORT_SYMBOL_NS(drm_helper_mode_fill_fb_struct, DRM_KMS_HELPER);
이 구조에서 drm_dev_register는 모든 DRM 드라이버가 사용해야 하므로 네임스페이스 없이 공개하고, GPU 스케줄러(Scheduler) API는 스케줄러를 직접 사용하는 드라이버(amdgpu, nouveau, panfrost 등)만 접근하도록 DRM_GPU_SCHEDULER 네임스페이스로 격리(Isolation)합니다. 이렇게 하면 간단한 디스플레이 드라이버가 실수로 GPU 스케줄러 내부 API에 의존하는 것을 방지할 수 있습니다.
위반 시 동작: 경고 vs 로딩 거부
현재 커널(6.x 기준)에서 네임스페이스 위반은 두 단계에서 감지됩니다.
| 단계 | 동작 | 심각도 | 우회 가능 여부 |
|---|---|---|---|
빌드 타임 (modpost) | WARNING: module X uses symbol Y from namespace Z, but does not import it | 경고 | 빌드는 계속 진행됨 |
| 런타임 (모듈 로딩) | 커널 로그에 경고 메시지 출력, 심볼은 해결됨 | 경고 | 현재는 로딩 자체를 거부하지 않음 |
| 향후 정책 강화 | CONFIG_MODULE_ALLOW_MISSING_NAMESPACE_IMPORTS=n 시 로딩 거부 | 오류 | 해당 Kconfig 비활성화 시 거부 |
CONFIG_MODULE_ALLOW_MISSING_NAMESPACE_IMPORTS의 기본값은 배포판마다 다릅니다. 개발 환경에서는 이 옵션을 n으로 설정하여 위반을 조기에 발견하는 것이 좋습니다.
EXPORT_SYMBOL_NS_GPL 조합
EXPORT_SYMBOL_NS_GPL(sym, NS)는 GPL 라이선스 제약과 네임스페이스 경계를 동시에 적용합니다. 소비 모듈은 MODULE_LICENSE("GPL")과 MODULE_IMPORT_NS(NS)를 모두 선언해야 합니다. 이 조합은 서브시스템 내부 API 중에서도 특히 커널 내부 구조에 강하게 결합된 심볼에 적합합니다.
네 가지 export 매크로의 조합을 정리하면 다음과 같습니다.
| 매크로 | GPL 제한 | 네임스페이스 | 적합한 사용 사례 |
|---|---|---|---|
EXPORT_SYMBOL(sym) | 없음 | 없음 | 범용 API (printk, kmalloc 등) |
EXPORT_SYMBOL_GPL(sym) | 있음 | 없음 | 커널 내부 구현에 의존하는 API |
EXPORT_SYMBOL_NS(sym, NS) | 없음 | 있음 | 서브시스템 경계가 필요한 범용 API |
EXPORT_SYMBOL_NS_GPL(sym, NS) | 있음 | 있음 | 서브시스템 내부 + GPL 전용 (가장 엄격) |
/* 제공자: DRM 서브시스템 내부 GPU 스케줄러 API */
#include <linux/module.h>
#include <drm/gpu_scheduler.h>
int drm_sched_init(struct drm_gpu_scheduler *sched,
const struct drm_sched_backend_ops *ops,
unsigned hw_submission, unsigned hang_limit,
long timeout, struct workqueue_struct *timeout_wq,
const char *name)
{
/* 구현부 */
return 0;
}
EXPORT_SYMBOL_NS_GPL(drm_sched_init, DRM_GPU_SCHEDULER);
/* 소비자: DRM 드라이버 모듈 */
#include <linux/module.h>
#include <drm/gpu_scheduler.h>
MODULE_IMPORT_NS(DRM_GPU_SCHEDULER);
MODULE_LICENSE("GPL");
static int __init my_drm_init(void)
{
struct drm_gpu_scheduler sched;
return drm_sched_init(&sched, NULL, 16, 0, 500, NULL, "my-sched");
}
module_init(my_drm_init);
nsdeps 스크립트 활용
네임스페이스 위반을 수동으로 수정하는 대신, 커널 소스 트리에 포함된 scripts/nsdeps 스크립트를 사용하면 누락된 MODULE_IMPORT_NS를 자동으로 추가할 수 있습니다. 이 스크립트는 modpost의 경고 메시지를 파싱하여 소스 파일에 필요한 MODULE_IMPORT_NS 선언을 자동 삽입합니다. 대규모 서브시스템 리팩토링이나 네임스페이스 도입 시 수십 개 파일을 수동으로 수정하는 대신 한 번에 처리할 수 있어 매우 유용합니다.
# 전체 커널 트리에서 네임스페이스 위반 수정
make nsdeps
# 특정 디렉토리만 대상으로
make M=drivers/gpu/drm nsdeps
# 변경된 파일 확인
git diff --stat
# nsdeps 실행 결과 확인 (dry-run은 없으므로 git diff로 확인)
# 자동 삽입된 예시:
# +MODULE_IMPORT_NS(USB_STORAGE);
# +MODULE_IMPORT_NS(DRM_KMS_HELPER);
MAINTAINERS 파일이나 Documentation/ 디렉토리에서 해당 서브시스템의 네임스페이스 정책을 확인할 수 있습니다. 일부 서브시스템(예: DRM)은 네임스페이스 도입이 잘 정비되어 있지만, 다른 서브시스템은 아직 네임스페이스를 사용하지 않을 수 있습니다. 새로운 서브시스템을 설계할 때는 처음부터 네임스페이스를 도입하는 것이 나중에 마이그레이션하는 것보다 훨씬 쉽습니다.
다중 네임스페이스 전략
한 모듈이 여러 서브시스템의 네임스페이스 심볼을 사용해야 하는 경우, 각 네임스페이스를 개별적으로 import합니다. 이 패턴은 교차 서브시스템 드라이버(예: DRM + I2C + GPIO를 함께 사용하는 디스플레이 드라이버)에서 흔히 나타납니다. 다중 네임스페이스 import는 해당 모듈이 여러 서브시스템에 의존한다는 사실을 코드에 명시적으로 기록하는 효과가 있으며, 유지보수자가 의존성 범위를 한눈에 파악할 수 있게 합니다.
/* 여러 서브시스템 API를 사용하는 복합 드라이버 */
MODULE_IMPORT_NS(DRM_GPU_SCHEDULER);
MODULE_IMPORT_NS(DRM_KMS_HELPER);
MODULE_IMPORT_NS(I2C_CORE);
MODULE_LICENSE("GPL");
Kconfig 옵션이나 디렉토리 구조와 일치시키는 것이 좋습니다. 예를 들어 drivers/iio/ 하위의 공용 API는 IIO 네임스페이스를, drivers/iio/adc/ 내부 전용 API는 IIO_ADC처럼 세분화할 수 있습니다. 이렇게 하면 빌드 시스템의 디렉토리 구조와 심볼 정책이 자연스럽게 일치합니다.
| 서브시스템 | 네임스페이스 예시 | 적용 커널 버전 | 심볼 수 (대략) |
|---|---|---|---|
| DRM | DRM_GPU_SCHEDULER, DRM_KMS_HELPER | 5.4+ | ~200개 |
| IIO | IIO, IIO_TRIGGERED_BUFFER | 5.10+ | ~80개 |
| USB | USB_STORAGE, USB_COMMON | 5.4+ | ~50개 |
| MCB | MCB | 5.4+ | ~15개 |
| COUNTER | COUNTER | 5.16+ | ~20개 |
BTF 기반 심볼 정보
BTF(BPF Type Format)는 커널의 타입 정보를 컴팩트한 형태로 저장하는 포맷으로, BPF 프로그램이 커널 내부 구조체에 안전하게 접근하는 데 핵심적인 역할을 합니다. CONFIG_DEBUG_INFO_BTF=y를 활성화하면 빌드 시 pahole이 DWARF 디버그 정보를 BTF로 변환하여 vmlinux의 .BTF 섹션에 삽입합니다.
기존의 심볼 시스템(kallsyms, __ksymtab)이 "이름 ↔ 주소" 매핑을 제공한다면, BTF는 "이름 ↔ 타입 구조" 매핑을 제공합니다. 이 둘이 결합되면 BPF 프로그램은 커널 버전이 바뀌어 구조체 레이아웃이 변경되어도, CO-RE(Compile Once – Run Everywhere) 메커니즘을 통해 실행 시점에 올바른 오프셋을 자동 조정할 수 있습니다.
BTF가 특히 중요해진 이유는 BPF 프로그램의 용도가 네트워크 필터링을 넘어 보안 관측(security observability), 스케줄러 확장(sched_ext), 스토리지 계층, 메모리 관리(Memory Management) 등 커널 전반으로 확장되었기 때문입니다. 이 모든 사용 사례에서 BPF 프로그램은 커널 구조체에 안전하게 접근해야 하며, BTF와 CO-RE가 이를 가능하게 합니다.
CO-RE와 심볼의 관계
CO-RE(Compile Once – Run Everywhere)는 BPF 프로그램이 컴파일된 커널과 다른 버전의 커널에서도 실행될 수 있게 해주는 메커니즘입니다. 이것이 가능한 이유는 BTF가 구조체의 필드 오프셋, 크기, enum 값 등을 런타임에 조회할 수 있게 해주기 때문입니다. BPF 로더(libbpf)는 프로그램 로딩 시 대상 커널의 BTF를 읽어 재배치를 수행합니다.
CO-RE 재배치의 핵심은 .BTF.ext 섹션에 기록된 재배치 레코드입니다. BPF 프로그램이 컴파일될 때 Clang은 구조체 필드 접근 위치마다 재배치 레코드를 삽입합니다. 이 레코드는 "이 명령어는 task_struct의 pid 필드에 접근한다"는 의미 정보를 담고 있어, 대상 커널의 BTF에서 해당 필드의 실제 오프셋을 찾아 명령어를 패치할 수 있습니다. 이는 모듈 로더가 __ksymtab으로 심볼 주소를 해결하는 것과 원리적으로 동일하지만, 주소 대신 타입 오프셋을 다룬다는 점이 다릅니다.
/* BPF 프로그램에서 CO-RE를 사용한 커널 구조체 접근 */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
pid_t pid;
/* CO-RE: 커널 버전에 따라 오프셋이 달라도 BTF로 자동 조정 */
pid = BPF_CORE_READ(task, tgid);
if (pid > 0)
bpf_printk("exec: pid=%d", pid);
return 0;
}
pahole과 BTF 생성
pahole은 DWARF 디버그 정보를 BTF로 변환하는 도구로, 커널 빌드 과정에서 자동으로 호출됩니다. BTF는 DWARF에 비해 크기가 수십 배 작으면서도 BPF에 필요한 타입 정보를 모두 포함합니다. DWARF가 디버거를 위한 범용 디버그 정보(변수 위치, 인라인 정보, 매크로 등)를 모두 포함하는 반면, BTF는 타입 정보만 추출하여 최소한의 크기로 압축합니다. 일반적으로 DWARF 300MB 이상의 디버그 정보가 BTF 3–6MB로 압축됩니다.
# 커널 빌드 시 BTF 자동 생성 (CONFIG_DEBUG_INFO_BTF=y)
make -j$(nproc)
# vmlinux에서 BTF 섹션 크기 확인
readelf -S vmlinux | grep BTF
# [N] .BTF PROGBITS ... ~5MB (커널 구성에 따라 다름)
# BTF로 커널 타입 덤프
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 특정 구조체 조회
bpftool btf dump file /sys/kernel/btf/vmlinux | grep -A 20 "'task_struct'"
# 모듈 BTF (CONFIG_DEBUG_INFO_BTF_MODULES=y)
bpftool btf dump file /sys/kernel/btf/e1000e
crash 유틸리티의 타입 인식에도 점차 활용되고 있습니다. pahole 1.24 이상을 권장합니다.
| 항목 | kallsyms | BTF |
|---|---|---|
| 제공 정보 | 이름 ↔ 주소 ↔ 모듈 | 이름 ↔ 타입 ↔ 필드 오프셋 |
| 크기 (일반적) | ~2–4MB | ~3–6MB |
| 주요 소비자 | perf, ftrace, Oops 해석 | BPF, libbpf, bpftool |
| 빌드 의존성 | scripts/kallsyms | pahole (dwarves 패키지) |
| 모듈 지원 | 자동 (로딩 시 등록) | CONFIG_DEBUG_INFO_BTF_MODULES |
| KASLR 영향 | 주소 마스킹 (kptr_restrict) | 영향 없음 (타입 정보만) |
btf_ids.h와 BPF 허용 심볼
BPF 프로그램이 커널 함수에 직접 접근(kfunc)하려면 해당 함수가 BTF ID 세트에 등록되어야 합니다. include/linux/btf_ids.h에 정의된 매크로를 사용하여 허용 심볼을 선언합니다. 이 메커니즘은 BPF 프로그램이 접근 가능한 커널 심볼의 범위를 명시적으로 제한하는 역할을 합니다.
/* include/linux/btf_ids.h — BTF ID 세트 등록 */
BTF_SET8_START(bpf_task_set)
BTF_ID_FLAGS(func, bpf_task_acquire, KF_ACQUIRE | KF_TRUSTED_ARGS)
BTF_ID_FLAGS(func, bpf_task_release, KF_RELEASE)
BTF_ID_FLAGS(func, bpf_task_from_pid, KF_ACQUIRE | KF_RET_NULL)
BTF_SET8_END(bpf_task_set)
/* kfunc 등록 — BPF 프로그램이 호출 가능한 커널 함수 */
static const struct btf_kfunc_id_set bpf_task_kfunc_set = {
.owner = THIS_MODULE,
.set = &bpf_task_set,
};
/* init에서 등록 */
register_btf_kfunc_id_set(BPF_PROG_TYPE_TRACING, &bpf_task_kfunc_set);
이 패턴은 커널 심볼 export(EXPORT_SYMBOL)와 유사하지만, BPF 전용이라는 점에서 차이가 있습니다. EXPORT_SYMBOL이 모듈 간 링크를 허용하는 것처럼, BTF kfunc 등록은 BPF 프로그램이 커널 함수를 호출하는 것을 허용합니다. 두 시스템 모두 "허용 목록" 방식으로 접근을 제어합니다.
bpftool btf dump file /sys/kernel/btf/vmlinux으로 현재 커널에서 사용 가능한 모든 BTF 타입을 확인할 수 있습니다. vmlinux.h를 생성하면 BPF 프로그램에서 커널의 모든 타입을 직접 사용할 수 있어, 별도 헤더 의존성 없이 CO-RE 프로그램을 작성할 수 있습니다.
CONFIG_TRIM_UNUSED_KSYMS
CONFIG_TRIM_UNUSED_KSYMS는 빌드 시점에 실제로 사용되지 않는 exported 심볼을 자동으로 제거하여 커널 이미지 크기를 줄이고 공격 표면을 축소하는 옵션입니다. 이 옵션을 활성화하면 Kbuild가 현재 .config에서 활성화된 모듈들이 실제로 참조하는 심볼만 남기고 나머지 export를 제거합니다. 일반적인 allmodconfig 빌드에서 약 30,000개의 exported 심볼 중 실제로 모듈이 참조하는 것은 12,000~15,000개 수준이므로, TRIM을 적용하면 exported 심볼의 약 50~60%를 제거할 수 있습니다.
동작 원리는 다음과 같습니다. 빌드 시스템은 먼저 모든 모듈의 .mod 파일에서 undefined 심볼 목록을 수집합니다. 그런 다음 이 목록에 포함되지 않은 exported 심볼에 대해 __KSYM_ 매크로를 비활성화하는 방식으로 export를 제거합니다. 이 과정은 CONFIG_MODULES=y일 때만 의미가 있으며, 모듈이 아예 비활성화된 커널에서는 모든 export가 자연스럽게 무시됩니다.
빌드 시간 영향
이 옵션은 빌드 시간을 늘릴 수 있습니다. 사용되지 않는 심볼을 파악하기 위해 빌드가 두 패스로 진행되기 때문입니다. 첫 번째 패스에서 모듈 의존성을 수집하고, 두 번째 패스에서 불필요한 export가 제거된 상태로 최종 빌드가 수행됩니다.
구체적으로, Kbuild는 먼저 모든 모듈을 컴파일한 후 각 모듈의 undefined 심볼 목록을 include/generated/autoksyms.h에 기록합니다. 이 헤더는 __KSYM_ 매크로를 조건부로 정의하여, 사용되는 심볼의 export만 활성화합니다. 그런 다음 이 헤더가 변경된 파일만 재컴파일하는 증분 빌드가 수행됩니다. 초기 전체 빌드는 약 5–15% 느려질 수 있지만, 이후 증분 빌드에서는 영향이 미미합니다.
# .config에서 활성화
CONFIG_TRIM_UNUSED_KSYMS=y
CONFIG_MODULES=y
# 현재 export된 심볼 수 확인 (trim 전후 비교)
cat /proc/kallsyms | grep -c ' [Tt] __ksymtab_'
# 또는 Module.symvers에서 확인
wc -l Module.symvers
외부 모듈과의 충돌
TRIM_UNUSED_KSYMS의 가장 큰 실무 문제는 외부 모듈(out-of-tree module)과의 충돌입니다. 커널 빌드 시점에 외부 모듈의 의존성을 알 수 없으므로, 외부 모듈이 필요로 하는 심볼이 제거될 수 있습니다. 이 경우 외부 모듈 로딩 시 Unknown symbol 오류가 발생합니다.
특히 NVIDIA 드라이버, VirtualBox 커널 모듈, ZFS on Linux 같은 널리 사용되는 외부 모듈은 수백 개의 커널 심볼에 의존합니다. 이 모듈들이 참조하는 심볼이 TRIM에 의해 제거되면, 모듈 로딩이 실패하면서 GPU 가속, 가상화(Virtualization), 파일시스템(Filesystem) 기능이 완전히 중단됩니다. 따라서 외부 모듈을 사용하는 환경에서는 TRIM을 활성화하기 전에 반드시 의존 심볼 목록을 확인해야 합니다.
# 외부 모듈이 필요로 하는 심볼 확인
nm /path/to/external_module.ko | grep ' U ' | awk '{print $2}' > needed_symbols.txt
# TRIM 적용 후 남은 심볼과 비교
awk '{print $2}' Module.symvers | sort > exported_symbols.txt
comm -23 <(sort needed_symbols.txt) exported_symbols.txt
# 출력이 있으면 해당 심볼이 TRIM에 의해 제거됨
CONFIG_TRIM_UNUSED_KSYMS=y가 활성화된 커널에서 외부 모듈(NVIDIA, VirtualBox 등)을 사용하려면 UNUSED_KSYMS_WHITELIST에 해당 심볼을 등록하거나, 이 옵션을 비활성화해야 합니다.
UNUSED_KSYMS_WHITELIST
외부 모듈과의 호환성을 유지하면서도 불필요한 심볼을 제거하려면, 화이트리스트를 사용하여 특정 심볼을 보존할 수 있습니다. 화이트리스트 파일에는 보존할 심볼 이름을 정규식 패턴으로 지정하며, 한 줄에 하나의 패턴을 작성합니다. 이 파일의 경로는 CONFIG_UNUSED_KSYMS_WHITELIST Kconfig 옵션으로 지정합니다.
# Kconfig에서 화이트리스트 파일 지정
CONFIG_UNUSED_KSYMS_WHITELIST="my_whitelist"
# my_whitelist 파일 예시 (정규식 패턴)
# 한 줄에 하나의 심볼 패턴
pci_register_driver
pci_unregister_driver
__alloc_pages
kmalloc_caches
| 시나리오 | TRIM_UNUSED_KSYMS 권장 | 이유 |
|---|---|---|
| 임베디드 전용 커널 | 적극 권장 | 이미지 크기 절감, 외부 모듈 불필요 |
| 배포판 범용 커널 | 비권장 | 외부 모듈 호환성 필수 |
| 개발/테스트 커널 | 선택적 | 테스트 대상에 따라 판단 |
| 보안 강화 커널 | 권장 | 공격 표면 축소 |
Kconfig 의존성 분석
TRIM_UNUSED_KSYMS는 Kconfig 의존성 그래프와 깊이 연결됩니다. 특정 CONFIG_ 옵션이 비활성화되면 해당 서브시스템의 모듈이 빌드되지 않고, 결과적으로 그 모듈이 참조하던 심볼이 "미사용"으로 분류되어 제거됩니다. 이 연쇄 효과를 이해하지 못하면 의도하지 않은 심볼 제거가 발생할 수 있습니다.
예를 들어 CONFIG_USB_STORAGE=n으로 설정하면 USB 스토리지 모듈이 빌드되지 않고, 이 모듈이 유일한 소비자였던 심볼들이 TRIM 대상이 됩니다. 나중에 CONFIG_USB_STORAGE=m으로 변경하고 모듈만 재빌드하면, TRIM으로 제거된 심볼이 커널에 없어 로딩이 실패합니다. 이런 상황을 방지하려면 .config를 변경한 후 커널 전체를 재빌드해야 합니다.
# 특정 CONFIG가 어떤 모듈에 영향을 주는지 확인
grep -r "CONFIG_USB_STORAGE" drivers/usb/storage/Makefile
# obj-$(CONFIG_USB_STORAGE) += usb-storage.o
# TRIM 적용 전후 심볼 수 비교
# 1. TRIM 없이 빌드
make -j$(nproc)
wc -l Module.symvers # 예: 31847
# 2. TRIM 활성화 후 빌드
# .config에 CONFIG_TRIM_UNUSED_KSYMS=y 추가
make -j$(nproc)
wc -l Module.symvers # 예: 12453 (약 60% 감소)
TRIM_UNUSED_KSYMS는 CONFIG_MODULES=y일 때만 활성화할 수 있습니다. CONFIG_MODULES=n인 모놀리식 커널에서는 모든 EXPORT_SYMBOL이 자동으로 무시되므로 별도 설정이 불필요합니다. 커널 모듈 문서에서 모듈 시스템의 전체 구조를 확인하세요.
/proc/kallsyms 성능
/proc/kallsyms는 커널의 모든 심볼(함수, 변수, 모듈 심볼)을 런타임에 조회할 수 있는 인터페이스로, 디버깅과 프로파일링(Profiling)의 핵심 데이터 소스입니다. 이 파일의 내용은 부팅 시 scripts/kallsyms가 생성한 압축 테이블에서 동적으로 생성됩니다.
/proc/kallsyms는 실제 파일이 아닌 proc 파일시스템의 가상 파일입니다. 읽기 요청이 올 때마다 kernel/kallsyms.c의 s_show() 함수가 호출되어, 압축된 심볼 테이블에서 한 줄씩 심볼 정보를 복호화(Decryption)하여 출력합니다. 이 순차 접근 방식은 메모리 효율적이지만, 특정 심볼을 찾기 위해 전체 파일을 순회해야 하므로 대규모 커널에서는 느릴 수 있습니다. 커널 내부에서 심볼을 조회할 때는 kallsyms_lookup_name() 함수를 사용하며, 이 함수는 이진 탐색을 사용하여 훨씬 빠르게 동작합니다.
압축 알고리즘 상세
kallsyms의 심볼 이름은 토큰 기반 압축을 사용합니다. kallsyms_token_table은 가장 빈번하게 나타나는 바이트 쌍(bigram)을 반복적으로 치환하여 심볼 이름을 압축합니다. 예를 들어 "__"가 자주 나타나면 이를 단일 토큰으로 대체합니다. 이 방식은 단순하지만 커널 심볼 이름의 특성(공통 접두사가 많음)에 매우 효과적이어서, 일반적으로 50–60% 압축률을 달성합니다.
압축 과정은 scripts/kallsyms.c에서 수행됩니다. 이 프로그램은 빌드 마지막 단계에서 nm 출력을 입력으로 받아 kallsyms_names, kallsyms_addresses(또는 kallsyms_offsets), kallsyms_token_table 등의 배열을 생성합니다. 생성된 데이터는 어셈블리 형태로 출력되어 최종 vmlinux에 링크됩니다. 커널 6.x에서는 상대 오프셋 방식(kallsyms_relative_base 기준)이 기본으로 사용되어, 64비트 시스템에서도 심볼 주소를 32비트 오프셋으로 저장할 수 있어 메모리 사용량이 추가로 절감됩니다.
/* kernel/kallsyms_internal.h — 압축 데이터 구조 */
extern const unsigned long kallsyms_addresses[] __weak;
extern const int kallsyms_offsets[] __weak;
extern const u8 kallsyms_names[] __weak;
extern const unsigned int kallsyms_num_syms __weak;
extern const unsigned long kallsyms_relative_base __weak;
extern const char kallsyms_token_table[] __weak;
extern const u16 kallsyms_token_index[] __weak;
/* kallsyms_expand_symbol: 토큰을 원래 문자열로 복원 */
KASLR 상호작용과 kptr_restrict
KASLR(Kernel Address Space Layout Randomization)이 활성화되면, /proc/kallsyms의 주소가 무작위 오프셋만큼 이동합니다. 그러나 보안상 비특권 사용자에게 커널 주소를 노출하는 것은 위험하므로, kptr_restrict sysctl로 접근을 제한합니다.
| kptr_restrict 값 | /proc/kallsyms 동작 | %pK 포맷 동작 | 영향받는 도구 |
|---|---|---|---|
0 | 모든 사용자에게 실제 주소 노출 | 실제 주소 출력 | 제한 없음 |
1 | CAP_SYSLOG 필요 (없으면 0x0으로 표시) | 권한 확인 후 출력 | 비특권 perf, 비특권 사용자 |
2 | root 포함 모든 사용자에게 0x0으로 표시 | 항상 0x0 출력 | perf, bpftool, crash |
# 현재 설정 확인
cat /proc/sys/kernel/kptr_restrict
# 디버깅 시 임시 해제 (root 필요)
echo 0 > /proc/sys/kernel/kptr_restrict
# kallsyms 크기 확인
wc -c /proc/kallsyms
# 일반적으로 CONFIG_KALLSYMS_ALL=y 시 수백만 줄
# 심볼 수 비교
wc -l /proc/kallsyms # 전체 심볼
grep -c ' [Tt] ' /proc/kallsyms # 텍스트(함수) 심볼만
CONFIG_KALLSYMS_ALL vs 기본 설정
CONFIG_KALLSYMS=y만 활성화하면 함수 심볼만 포함되지만, CONFIG_KALLSYMS_ALL=y를 추가하면 데이터 심볼(전역 변수)도 포함됩니다. 대부분의 배포판은 후자를 활성화합니다. 데이터 심볼이 포함되면 /proc/kallsyms에서 전역 변수의 주소를 확인할 수 있어, 커널 상태를 조사하거나 메모리 덤프(Dump)를 분석할 때 유용합니다. 예를 들어 jiffies, system_state, init_task 같은 핵심 전역 변수의 주소를 직접 확인할 수 있습니다.
| 설정 | 포함 심볼 | 대략적 크기 증가 | perf 영향 |
|---|---|---|---|
CONFIG_KALLSYMS=y | 함수 심볼 (T, t) | 기준 | 함수 프로파일링 가능 |
+ CONFIG_KALLSYMS_ALL=y | + 데이터 심볼 (D, d, B, b, R, r) | ~30–50% 증가 | 데이터 접근 프로파일링도 가능 |
+ CONFIG_KALLSYMS_ABSOLUTE_PERCPU=y | + per-CPU 변수 절대 주소 | 미미 | per-CPU 심볼 정확도 향상 |
대규모 커널의 kallsyms 크기
allmodconfig나 배포판 범용 커널처럼 많은 기능이 활성화된 커널은 심볼 수가 수십만 개에 달할 수 있습니다. CONFIG_KALLSYMS_ALL=y가 활성화되면 데이터 심볼까지 포함되어 크기가 더 증가합니다.
| 커널 구성 | 대략적 심볼 수 | kallsyms 크기 (압축 후) | 비압축 대비 절감 |
|---|---|---|---|
| 최소 설정 (임베디드) | ~20,000 | ~500KB | ~55% |
| 일반 데스크톱 | ~80,000 | ~2MB | ~55% |
| 배포판 범용 (KALLSYMS_ALL) | ~200,000 | ~5MB | ~55% |
| allmodconfig (개발용) | ~400,000+ | ~10MB+ | ~55% |
kallsyms 크기가 커지면 부팅 시 메모리 소비가 증가하고, /proc/kallsyms를 순차 읽기하는 도구(예: perf의 초기화)의 시작 시간이 늘어납니다. 임베디드 환경에서는 CONFIG_KALLSYMS=n으로 완전히 비활성화하여 수 MB를 절약할 수 있지만, 이 경우 Oops 메시지에서 심볼 이름 대신 원시 주소만 표시됩니다.
perf와 kallsyms 연동
perf는 프로파일링 시 커널 심볼을 해석하기 위해 /proc/kallsyms를 참조합니다. 만약 kptr_restrict가 활성화되어 있으면 perf가 커널 심볼을 해석하지 못하여 주소만 표시됩니다. 프로파일링 세션에서 의미 있는 결과를 얻으려면 적절한 권한 설정이 필요합니다.
# perf에서 커널 심볼이 보이지 않는 경우 확인
perf top
# [kernel.kallsyms] 에 주소만 표시되면 권한 문제
# 해결 방법 1: root로 실행
sudo perf top
# 해결 방법 2: perf_event_paranoid 조정
echo -1 > /proc/sys/kernel/perf_event_paranoid
# 해결 방법 3: CAP_PERFMON 부여 (커널 5.8+)
sudo setcap cap_perfmon+ep $(which perf)
또한 perf는 모듈 심볼도 해석할 수 있습니다. /proc/kallsyms에는 로드된 모듈의 심볼도 포함되므로, 모듈 내 함수의 프로파일링 결과도 함수명으로 표시됩니다. 단, 모듈이 언로드되면 해당 심볼 정보가 사라지므로, 프로파일링 기록 시점에 모듈이 로드되어 있어야 합니다.
# 특정 모듈의 심볼만 필터링하여 프로파일링
perf top -g --sort comm,dso | grep "[my_module]"
# perf.data에 포함된 kallsyms 확인
perf report --header-only | grep "kallsyms"
# 커널 심볼 해석이 올바른지 테스트
perf probe -l # 등록된 프로브 목록 (심볼 해석 확인)
perf record는 기록 시점에 /proc/kallsyms 스냅샷을 perf.data에 포함시킵니다. 따라서 기록 시 권한이 충분했다면, 분석은 비특권 사용자로도 가능합니다. 관련 상세는 perf 프로파일링 문서를 참고하세요.
lockdown 보안 정책 상세
커널 lockdown은 커널 5.4에서 LSM(Linux Security Module)으로 도입된 보안 프레임워크로, 부팅 후 커널 무결성(Integrity)을 보호하기 위해 특정 인터페이스 접근을 제한합니다. lockdown은 심볼 시스템과 밀접한 관계가 있는데, 커널 심볼(주소)에 대한 접근 자체가 공격자에게 KASLR 우회 정보를 제공할 수 있기 때문입니다.
lockdown LSM은 security/lockdown/lockdown.c에 구현되어 있으며, security_locked_down() 함수를 통해 각 보안 민감 동작의 허용 여부를 결정합니다. 이 함수는 현재 lockdown 레벨(none, integrity, confidentiality)과 요청된 동작의 보안 등급을 비교하여, 동작이 현재 레벨에서 허용되는지 판단합니다. 심볼 관련 동작은 주로 LOCKDOWN_KCORE, LOCKDOWN_TRACEFS, LOCKDOWN_PERF 등의 보안 등급으로 분류됩니다.
lockdown과 심볼 노출 제약
lockdown이 활성화되면 심볼과 관련된 여러 인터페이스가 제한됩니다. 특히 confidentiality 모드에서는 커널 주소 자체가 민감 정보로 취급되어, 디버깅 도구 사용이 크게 제한됩니다. 이는 KASLR의 효과를 보장하기 위한 것으로, 공격자가 커널 심볼 주소를 알아내면 KASLR 오프셋을 역산하여 ROP(Return-Oriented Programming) 공격에 사용할 수 있기 때문입니다.
| 인터페이스 | integrity 모드 | confidentiality 모드 | 심볼 관련성 |
|---|---|---|---|
/proc/kallsyms | kptr_restrict 적용 | 주소 완전 마스킹 | 심볼 주소 조회 불가 |
/proc/kcore | 접근 가능 | 접근 차단 | 메모리 기반 심볼 분석 불가 |
/dev/mem | 쓰기 차단 | 읽기/쓰기 차단 | 물리 메모리(Physical Memory) 심볼 접근 불가 |
| kprobes | 제한적 허용 | 차단 | 동적 심볼 훅 불가 |
| BPF (커널 읽기) | 허용 | 차단 | BTF 기반 심볼 접근 제한 |
| 모듈 로딩 | 서명 필수 | 서명 필수 | 비서명 모듈의 심볼 등록 차단 |
| perf 하드웨어 | 허용 | 차단 | 커널 심볼 프로파일링 불가 |
kptr_restrict 단계별 상세
kptr_restrict는 커널 포인터 노출을 제어하는 sysctl로, lockdown과 독립적으로 설정할 수 있지만 lockdown confidentiality 모드에서는 사실상 kptr_restrict=2와 동일한 효과가 강제됩니다.
/* lib/vsprintf.c — %pK 포맷 처리 */
switch (kptr_restrict) {
case 0:
/* 제한 없음: 실제 포인터 값 출력 */
break;
case 1:
/* CAP_SYSLOG 또는 real uid == 0이면 출력,
* 그렇지 않으면 0으로 마스킹 */
if (!has_capability_noaudit(current, CAP_SYSLOG))
ptr = NULL;
break;
case 2:
/* 모든 경우 0으로 마스킹 */
ptr = NULL;
break;
}
참고로 %pK와 %p의 차이도 중요합니다. 커널 4.15부터 %p는 항상 해시된 주소를 출력하여 커널 주소 유출을 방지합니다. 실제 주소가 필요한 경우에만 %px(항상 노출) 또는 %pK(정책 기반)를 사용해야 합니다.
Secure Boot와 lockdown 연결
UEFI Secure Boot가 활성화된 시스템에서 커널이 CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT=y로 빌드되면, lockdown integrity 모드가 자동으로 활성화됩니다. 이는 서명된 부트 체인을 통해 커널 무결성을 보장하는 것과 런타임에 커널 변경을 방지하는 것을 연결합니다.
# 현재 lockdown 상태 확인
cat /sys/kernel/security/lockdown
# [none] integrity confidentiality
# 대괄호가 현재 활성 모드
# Secure Boot 상태 확인
mokutil --sb-state
# 또는
dmesg | grep -i "secure boot"
# 모듈 서명 검증 상태
dmesg | grep "module verification"
lockdown=none 커널 파라미터를 사용하거나, Secure Boot를 비활성화하여 lockdown을 해제할 수 있습니다. 프로덕션 환경에서의 디버깅은 디버깅 가이드의 lockdown 호환 기법을 참고하세요. 보안 모듈 전반에 대해서는 LSM 문서를 참조하세요.
모듈 서명 검증(Signature Verification)과 심볼 등록
lockdown integrity 모드에서는 CONFIG_MODULE_SIG_FORCE=y와 동일한 효과로, 서명이 검증되지 않은 모듈의 로딩이 거부됩니다. 이는 곧 해당 모듈이 export하려는 심볼도 커널 심볼 테이블에 등록되지 않음을 의미합니다. 서명 검증은 모듈 로딩의 가장 초기 단계에서 수행되며, 검증 실패 시 모듈의 init 함수조차 호출되지 않습니다.
# 모듈 서명 확인
modinfo -F sig_hashalgo my_module.ko
# sha256
# 서명 키 확인
modinfo -F signer my_module.ko
# Build time autogenerated kernel key
# lockdown 상태에서 비서명 모듈 로딩 시도
sudo insmod unsigned_module.ko
# insmod: ERROR: could not insert module: Operation not permitted
# dmesg: Lockdown: insmod: unsigned module loading is restricted
# 서명 관련 Kconfig 확인
grep "CONFIG_MODULE_SIG" /boot/config-$(uname -r)
# CONFIG_MODULE_SIG=y
# CONFIG_MODULE_SIG_FORCE=y (lockdown integrity 시 효과적으로 강제됨)
# CONFIG_MODULE_SIG_SHA256=y
모듈 서명은 빌드 시 자동 생성되는 키(certs/signing_key.pem)를 사용하거나, 사전에 등록된 MOK(Machine Owner Key)를 사용할 수 있습니다. 커널 보안 강화 문서에서 서명 인프라의 전체 구조를 다룹니다.
| 서명 방식 | 키 소스 | 적합한 환경 | 관리 난이도 |
|---|---|---|---|
| 빌드 시 자동 서명 | certs/signing_key.pem (자동 생성) | 개발/테스트 | 낮음 (자동) |
| MOK (Machine Owner Key) | mokutil로 등록 | 자체 빌드 커널 + Secure Boot | 중간 |
| 배포판 키 | 배포판 제공 서명 키 | 공식 배포판 모듈 | 해당 없음 (배포판 관리) |
| 커스텀 PKI | 자체 CA 인프라 | 엔터프라이즈 환경 | 높음 |
아키텍처별 차이
커널 심볼의 해석과 재배치는 아키텍처마다 다른 방식으로 수행됩니다. 모듈 로더는 아키텍처별 재배치 핸들러(arch/*/kernel/module.c)를 통해 심볼 주소를 해결하며, 이 과정에서 사용되는 재배치 유형, 거리 제한, 중간 코드(veneer/thunk) 생성 방식이 크게 다릅니다. 이 차이를 이해하는 것은 크로스 플랫폼 모듈 개발이나 모듈 로딩 관련 버그를 분석할 때 필수적입니다.
x86_64 심볼 해석
x86_64에서 모듈은 커널 텍스트 근처의 전용 영역에 로딩됩니다. 이 영역은 커널 이미지로부터 ±2GB 이내에 위치하므로, 대부분의 심볼 참조가 32비트 상대 주소(PC-relative)로 해결됩니다. 따라서 PLT(Procedure Linkage Table)가 필요하지 않으며, 함수 호출이 단일 CALL rel32 명령어로 직접 이루어집니다.
x86_64 모듈 영역은 전통적으로 0xffffffffa0000000 부근에 배치되며, KASLR이 활성화되면 이 영역의 시작 주소도 무작위화됩니다. 모듈 영역의 크기는 기본적으로 1GB(MODULES_LEN)이며, 이 범위 내에서 모듈이 로딩될 수 있습니다. 커널 이미지와 모듈 영역 사이의 거리가 2GB를 초과하지 않도록 설계되어 있으므로, 32비트 상대 주소로 모든 심볼을 참조할 수 있습니다.
/* arch/x86/kernel/module.c — x86_64 재배치 처리 핵심부 */
static int apply_relocate_add(Elf64_Shdr *sechdrs,
const char *strtab,
unsigned int symindex,
unsigned int relsec,
struct module *me)
{
/* R_X86_64_PLT32 → 단순 PC-relative 호출
* R_X86_64_PC32 → PC-relative 데이터 참조
* R_X86_64_32S → 부호 확장 32비트 절대 주소 */
}
ARM64 PLT veneer
ARM64에서 BL 명령어의 분기 범위는 ±128MB로 제한됩니다. 모듈이 커널 텍스트로부터 이 범위를 초과하는 위치에 로딩되면, 모듈 로더가 자동으로 PLT(veneer) 엔트리를 생성합니다. veneer는 먼저 대상 주소를 레지스터(Register)에 로드한 후 간접 분기하는 작은 코드 조각입니다.
KASLR이 활성화된 ARM64 시스템에서는 모듈 로딩 위치의 무작위화로 인해 커널 텍스트와의 거리가 128MB를 쉽게 초과합니다. 따라서 CONFIG_ARM64_MODULE_PLTS=y가 사실상 필수이며, KASLR 활성화 시 자동으로 선택됩니다. PLT 엔트리 수는 모듈이 참조하는 외부 심볼 수에 비례하며, 모듈 로더가 .plt 섹션의 크기를 사전 계산하여 충분한 공간을 할당합니다.
각 veneer 엔트리는 8–12바이트 크기이며, 모듈이 참조하는 외부 심볼 하나당 하나의 veneer가 생성됩니다. 따라서 많은 외부 심볼을 참조하는 모듈은 .plt 섹션이 상당히 커질 수 있습니다. 간접 분기로 인한 성능 오버헤드(Overhead)는 분기 예측(Branch Prediction)기가 학습한 후에는 무시할 수 있는 수준입니다.
/* ARM64 PLT veneer 구조 (개념적) */
veneer_entry:
/* ADRP x16, target@PAGE — 대상 페이지 주소 */
/* ADD x16, x16, target@LO — 페이지 내 오프셋 */
/* BR x16 — 간접 분기 */
/* arch/arm64/kernel/module-plts.c에서 자동 생성
* 모듈 로더가 .plt 섹션 크기를 미리 계산하여 할당 */
RISC-V 모듈 재배치
RISC-V는 auipc + jalr 조합으로 ±2GB 범위의 함수 호출을 수행합니다. 커널 6.x에서는 링커 relaxation을 모듈에도 적용하여, 가까운 심볼에 대해 더 짧은 명령어 시퀀스를 사용하는 최적화가 진행되고 있습니다.
RISC-V의 특징적인 재배치 유형으로 R_RISCV_ADD32/R_RISCV_SUB32 쌍이 있습니다. 이는 두 심볼 간 차이를 계산하는 데 사용되며, 스위치 테이블이나 예외 테이블에서 자주 나타납니다. 모듈 로더는 이 쌍을 함께 처리하여 올바른 오프셋을 계산해야 합니다. 또한 RISC-V는 c.jal 같은 압축 명령어(CONFIG_RISCV_ISA_C)를 지원하며, 이 경우 정렬 요구사항이 2바이트로 완화되어 재배치 처리 시 주의가 필요합니다.
RISC-V의 또 다른 특징은 링커 relaxation입니다. 컴파일러가 보수적으로 긴 명령어 시퀀스를 생성한 후, 링커가 실제 거리를 계산하여 더 짧은 시퀀스로 대체합니다. 예를 들어 auipc+jalr(8바이트)로 생성된 호출이 대상이 가까우면 jal(4바이트) 또는 c.jal(2바이트)로 축소될 수 있습니다. 커널 모듈에서는 이 relaxation이 제한적으로 적용되며, 커널 6.x에서 모듈 relaxation 지원이 점진적으로 확장되고 있습니다.
| 아키텍처 | 직접 분기 범위 | PLT/veneer 필요 | 모듈 영역 배치 | 재배치 소스 |
|---|---|---|---|---|
| x86_64 | ±2GB (rel32) | 불필요 | 커널 근처 고정 영역 | arch/x86/kernel/module.c |
| ARM64 | ±128MB (BL) | 필요 (자동 생성) | vmalloc 영역 | arch/arm64/kernel/module-plts.c |
| RISC-V | ±2GB (auipc+jalr) | 조건부 | vmalloc 영역 | arch/riscv/kernel/module.c |
| ARM (32-bit) | ±32MB (BL) | 필요 (thunk) | vmalloc 영역 | arch/arm/kernel/module-plts.c |
아키텍처별 CONFIG 옵션
심볼 해석과 모듈 로딩에 영향을 주는 아키텍처별 Kconfig 옵션이 있습니다. 이 옵션들은 재배치 전략, 모듈 영역 크기, PLT 생성 여부를 제어합니다. 특히 KASLR 관련 옵션은 모듈 영역의 배치에 직접적인 영향을 미치며, 이는 심볼 재배치가 32비트 상대 주소로 가능한지 여부를 결정합니다.
| CONFIG 옵션 | 아키텍처 | 효과 |
|---|---|---|
CONFIG_RANDOMIZE_BASE | x86_64, ARM64 | KASLR 활성화 — 모듈 영역 오프셋도 무작위화 |
CONFIG_ARM64_MODULE_PLTS | ARM64 | PLT veneer 생성 (기본 y, KASLR에 필수) |
CONFIG_X86_64 | x86_64 | 모듈을 커널 근처 ±2GB 영역에 배치 |
CONFIG_RISCV_ISA_C | RISC-V | 압축 명령어 활성화 — 재배치 정렬 변경 |
CONFIG_ARM64_ERRATUM_843419 | ARM64 | 특정 CPU 에러타 — 추가 veneer/패치 삽입 |
.ko 파일은 대상 아키텍처의 ELF 포맷과 재배치 유형을 포함하므로, readelf -r module.ko로 재배치 엔트리를 확인할 수 있습니다. ELF 문서에서 재배치 유형별 상세 설명을 참고하세요.
# 모듈의 재배치 엔트리 확인 (아키텍처별 유형이 다름)
readelf -r my_module.ko | head -20
# x86_64: R_X86_64_PLT32, R_X86_64_PC32
# ARM64: R_AARCH64_CALL26, R_AARCH64_ADR_PREL_PG_HI21
# RISC-V: R_RISCV_CALL_PLT, R_RISCV_PCREL_HI20
# 모듈 영역 메모리 위치 확인
cat /proc/modules | head -5
# 모듈명 크기 사용횟수 의존모듈 상태 주소
# x86_64 모듈 영역 범위 확인
grep "module" /proc/vmallocinfo | head -5
# ARM64에서 PLT 섹션 크기 확인
readelf -S /path/to/module.ko | grep plt
# [N] .plt PROGBITS ... (PLT veneer 영역)
# 아키텍처 확인
readelf -h /path/to/module.ko | grep Machine
# Machine: Advanced Micro Devices X86-64
# Machine: AArch64
# Machine: RISC-V
커널 6.x 변경사항
커널 6.x 시리즈에서는 심볼 관리 체계에 상당한 리팩토링이 이루어졌습니다. struct kernel_symbol의 내부 구조 변경, modpost 개선, 네임스페이스 확산, 그리고 Rust 모듈의 심볼 export 지원이 핵심 변화입니다. 이러한 변경은 커널의 심볼 시스템을 더 안전하고 효율적으로 만들기 위한 장기적인 노력의 일환이며, 외부 모듈 개발자는 이 변경에 맞춰 코드를 업데이트해야 합니다.
struct kernel_symbol 리팩토링 (6.0+)
커널 6.0 이전에는 struct kernel_symbol이 심볼 이름을 직접 포인터로 참조했습니다. 6.0부터는 상대 오프셋 방식이 기본이 되어 메모리 사용량이 줄었습니다. 이 변경은 특히 심볼 수가 많은 대규모 커널에서 수십 KB의 절약 효과를 가져옵니다.
상대 오프셋 방식에서는 각 필드가 자기 자신의 주소를 기준으로 대상까지의 거리를 32비트 부호 있는 정수로 저장합니다. 이는 64비트 시스템에서 포인터(8바이트)를 오프셋(4바이트)으로 대체하여 엔트리당 12바이트를 절약합니다. 약 30,000개의 exported 심볼이 있는 일반적인 커널 빌드에서 이는 약 360KB의 메모리 절감에 해당합니다. 이 최적화는 include/linux/export.h의 매크로 레벨에서 투명하게 처리되므로, 모듈 개발자가 별도로 신경 쓸 필요는 없습니다.
/* 커널 6.0+ 이후: 상대 오프셋 기반 (기본) */
struct kernel_symbol {
int value_offset; /* 심볼 주소까지의 상대 오프셋 */
int name_offset; /* 심볼 이름까지의 상대 오프셋 */
int namespace_offset; /* 네임스페이스 이름까지의 상대 오프셋 */
};
/* 이전 방식 (포인터 기반)과 비교:
* struct kernel_symbol {
* unsigned long value; // 8바이트 (64비트)
* const char *name; // 8바이트
* const char *namespace; // 8바이트
* };
* → 엔트리당 24바이트 → 12바이트로 50% 절감 */
modpost 개선
modpost(scripts/mod/modpost.c)도 지속적으로 개선되고 있습니다. 6.x에서는 네임스페이스 검증 강화, 더 정확한 섹션 불일치 감지, 그리고 빌드 경고의 구조화된 출력이 추가되었습니다. 특히 섹션 불일치(section mismatch) 감지가 크게 개선되어, __init 섹션의 함수가 일반 .text 섹션에서 참조되는 위험한 패턴을 더 정확하게 잡아냅니다. 이런 패턴은 초기화 후 해제된 메모리에 접근하는 use-after-free 버그로 이어질 수 있습니다.
# modpost 경고 형식 (6.x)
WARNING: modpost: module my_driver uses symbol foo_internal
from namespace FOO_INTERNAL, but does not import it.
# 섹션 불일치 경고 (init에서 non-init 참조)
WARNING: modpost: vmlinux: section mismatch in reference:
init_fn (section: .init.text) -> helper (section: .text)
Rust 모듈 심볼 export
커널 6.1부터 실험적으로 지원되는 Rust 모듈(CONFIG_RUST=y)은 심볼 관리에 새로운 과제를 추가합니다. Rust 함수는 이름 맹글링(name mangling)을 사용하므로, C 심볼과의 호환을 위해 #[no_mangle] 속성이나 extern "C" 선언이 필요합니다.
Rust 커널 모듈은 C 커널 API를 bindgen이 자동 생성한 바인딩을 통해 접근합니다. 이 바인딩은 C 헤더 파일에서 EXPORT_SYMBOL로 공개된 심볼의 Rust 래퍼를 생성합니다. 따라서 Rust 모듈이 사용하는 커널 심볼은 궁극적으로 C 측의 export 정책을 따릅니다. 네임스페이스 심볼도 동일하게 적용되어, Rust 측에서도 MODULE_IMPORT_NS에 해당하는 선언이 필요합니다.
// Rust 커널 모듈에서의 심볼 export (개념적)
#![no_std]
use kernel::prelude::*;
module! {
type: MyModule,
name: "my_rust_module",
license: "GPL",
}
struct MyModule;
impl kernel::Module for MyModule {
fn init(_module: &ThisModule) -> Result<Self> {
pr_info!("Rust module loaded\n");
Ok(MyModule)
}
}
/* Rust 바인딩은 bindgen이 C 헤더에서 자동 생성
* exported 심볼은 C 래퍼를 통해 간접 접근 */
Rust 모듈의 심볼 관련 현재 제약 사항을 정리하면 다음과 같습니다.
| 항목 | C 모듈 | Rust 모듈 (6.x) |
|---|---|---|
| 심볼 export | EXPORT_SYMBOL* 매크로 | 현재 직접 export 미지원 (C 래퍼 경유) |
| 심볼 import | extern 선언 + 링크 | bindgen 생성 바인딩 사용 |
| 네임스페이스 | MODULE_IMPORT_NS | Rust 매크로로 동등한 선언 |
| 이름 맹글링 | 없음 | Rust 기본 맹글링 (C FFI 시 extern "C") |
| modpost 검증 | 완전 지원 | C 래퍼를 통해 간접 검증 |
심볼 테이블 압축 개선 동향
커널 6.8 이후에서는 심볼 테이블 압축 알고리즘의 개선이 논의되고 있습니다. 현재의 토큰 기반 압축은 효과적이지만, 심볼 수가 40만 개를 초과하는 대규모 커널에서는 압축 테이블 자체의 크기와 복호화 성능이 부팅 시간에 영향을 줄 수 있습니다. 제안된 개선 방향에는 LZ4 기반 압축, 정렬된 심볼에 대한 접두사 제거(prefix stripping), 해시 기반 빠른 조회 등이 있습니다.
주요 변경 타임라인
| 커널 버전 | 변경사항 | 영향 |
|---|---|---|
| 6.0 | struct kernel_symbol 상대 오프셋 기본화 | 심볼 테이블 크기 ~50% 감소 |
| 6.1 | Rust 모듈 실험적 지원 시작 | Rust-C 심볼 바인딩 체계 도입 |
| 6.2 | modpost 네임스페이스 검증 강화 | 누락된 MODULE_IMPORT_NS 경고 개선 |
| 6.4 | MODULE_IMPORT_NS 문법 변경 (문자열에서 식별자로) | MODULE_IMPORT_NS(NS) 형태 통일 |
| 6.5 | EXPORT_SYMBOL 내부 구현 정리 | 매크로 확장 단순화 |
| 6.8+ | 심볼 테이블 압축 개선 검토 | 대규모 커널 부팅 속도 향상 목표 |
linux/version.h의 LINUX_VERSION_CODE와 KERNEL_VERSION() 매크로를 사용한 조건부 컴파일이 필수적입니다. 특히 struct kernel_symbol의 내부 레이아웃 변경은 모듈의 심볼 테이블 접근 코드에 직접적인 영향을 줄 수 있으므로, 커널 내부 구조체에 직접 접근하는 코드는 반드시 버전 가드로 보호해야 합니다.
MODULE_IMPORT_NS 문법 변경 (6.4)
커널 6.4에서 MODULE_IMPORT_NS의 인자가 문자열에서 식별자로 변경되었습니다. 이 변경은 외부 모듈의 호환성에 직접적인 영향을 미칩니다.
/* 커널 6.3 이전 */
MODULE_IMPORT_NS(USB_STORAGE); /* 일부 버전에서 문자열로 처리 */
/* 커널 6.4 이후 — 식별자(bare token)로 통일 */
MODULE_IMPORT_NS(USB_STORAGE); /* 따옴표 없음 */
/* 외부 모듈에서 버전 호환 매크로 예시 */
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,4,0)
MODULE_IMPORT_NS(MY_NS);
#else
MODULE_IMPORT_NS(MY_NS); /* 실제로는 동일하지만 내부 처리 차이 */
#endif
struct kernel_symbol 변경과 MODULE_IMPORT_NS 문법 변경에 주의해야 합니다. 커널 모듈 문서에서 버전별 호환성 매크로 사용법을, 빌드 시스템 문서에서 modpost 관련 상세를 참고하세요.
심볼 디버깅 고급 기법
심볼 관련 문제(Unknown symbol, 주소 해석 실패, 모듈 로딩 오류)를 진단할 때는 단순히 dmesg를 확인하는 것 이상의 도구와 기법이 필요합니다. 이 섹션에서는 커널 심볼 디버깅에 특화된 고급 기법을 정리합니다.
심볼 디버깅의 기본 원칙은 "문제가 발생한 계층을 먼저 식별하는 것"입니다. 심볼 문제는 빌드 타임(modpost), 로딩 타임(모듈 로더), 런타임(Oops/패닉) 중 어느 단계에서든 나타날 수 있으며, 각 단계에 적합한 도구가 다릅니다. 빌드 타임 문제는 Module.symvers와 nm으로, 로딩 타임 문제는 dmesg와 modprobe -v로, 런타임 문제는 addr2line/faddr2line과 crash 유틸리티로 분석합니다.
addr2line과 faddr2line
addr2line은 주소를 소스 파일과 줄 번호로 변환하는 도구이고, scripts/faddr2line은 함수+오프셋 형태의 심볼을 해석하는 커널 전용 스크립트입니다. Oops 메시지에서 콜스택을 분석할 때 핵심적으로 사용됩니다.
faddr2line은 addr2line보다 커널 Oops 분석에 특화되어 있습니다. Oops 메시지의 콜 트레이스는 함수명+오프셋/크기 형태로 출력되는데, addr2line은 절대 주소만 받을 수 있어 KASLR이 활성화된 환경에서 직접 사용하기 어렵습니다. 반면 faddr2line은 함수명과 오프셋을 받아 vmlinux에서 해당 위치를 찾으므로, KASLR 오프셋과 무관하게 정확한 소스 위치를 보여줍니다.
# addr2line: 절대 주소 → 소스 위치
addr2line -e vmlinux -f ffffffff81234567
# do_something
# kernel/core.c:142
# faddr2line: 함수+오프셋 → 소스 위치 (Oops 해석에 최적)
./scripts/faddr2line vmlinux "do_something+0x42/0x100"
# do_something+0x42/0x100:
# helper_call at kernel/core.c:158
# decode_stacktrace.sh: Oops 전체를 파이프로 해석
dmesg | ./scripts/decode_stacktrace.sh vmlinux
# 스택 트레이스의 모든 주소가 소스 위치로 변환됨
objdump vs nm: 용도 차이
두 도구 모두 심볼을 조회하지만 목적이 다릅니다. nm은 심볼 목록과 종류를 빠르게 확인하는 데 적합하고, objdump -t는 심볼의 섹션, 크기, 플래그까지 포함한 상세 정보를 제공합니다. 실무에서는 먼저 nm으로 빠르게 심볼의 존재 여부와 종류를 확인한 후, 필요 시 objdump -t나 readelf -Ws로 상세 정보를 조회하는 2단계 접근이 효율적입니다.
| 도구 | 주요 용도 | 출력 정보 | 속도 |
|---|---|---|---|
nm | 심볼 존재 여부, 종류(T/D/B/U) 확인 | 주소, 타입 문자, 이름 | 빠름 |
nm -D | 동적 심볼(모듈 의존성) 확인 | dynamic 심볼만 | 빠름 |
objdump -t | 심볼의 섹션, 크기, 바인딩 상세 | 섹션, 크기, 플래그, 이름 | 보통 |
objdump -T | 동적 심볼 테이블 상세 | dynamic 심볼 + 버전 정보 | 보통 |
readelf -Ws | ELF 심볼 테이블 원본 형태 | 인덱스, 바인딩, 가시성, 섹션 | 보통 |
# 모듈에서 undefined 심볼 확인 (모듈이 필요로 하는 외부 심볼)
nm my_module.ko | grep ' U '
# U printk
# U kmalloc
# U __register_chrdev
# 모듈이 export하는 심볼 확인
nm my_module.ko | grep '__ksymtab_'
# objdump으로 심볼 크기 확인
objdump -t vmlinux | grep ' do_something'
# ffffffff81234500 g F .text 0000000000000100 do_something
# ^^^^^^^^^^ 함수 크기 = 256바이트
# readelf로 exported 심볼의 ELF 섹션 확인
readelf -Ws vmlinux | grep '__ksymtab' | head -5
심볼 충돌 진단
두 모듈이 같은 이름의 심볼을 export하면 충돌이 발생합니다. modpost는 빌드 시 이를 감지하지만, 외부 모듈 간 충돌은 런타임에서야 발견될 수 있습니다. 심볼 충돌은 단순히 이름이 같은 경우뿐만 아니라, CRC가 다른 동일 이름 심볼이 존재하는 경우에도 문제가 됩니다. 이러한 상황은 서로 다른 커널 버전으로 빌드된 모듈을 혼용할 때 자주 발생합니다.
# 빌드 시 심볼 중복 감지 (Module.symvers)
sort Module.symvers | awk '{print $2}' | uniq -d
# 중복 심볼이 있으면 출력됨
# 런타임 심볼 충돌 진단
dmesg | grep "already defined"
# module: exports duplicate symbol foo (owned by other_module)
# 특정 심볼이 어느 모듈에서 제공되는지 확인
grep 'foo' /proc/kallsyms
# ffffffff81234500 T foo [module_a]
모듈 간 심볼 의존성 시각화
복잡한 모듈 스택에서 의존성을 파악하려면 Module.symvers와 lsmod를 조합하여 의존성 그래프를 구성할 수 있습니다. 대규모 시스템에서는 수십 개의 모듈이 상호 의존하는 경우가 흔하며, 순환 의존성이나 불필요한 의존성을 발견하는 것이 모듈 아키텍처 최적화의 첫 단계입니다.
# 현재 로드된 모듈의 의존성 트리
lsmod | awk '{print $1, $4}' | grep -v "^Module"
# modinfo로 특정 모듈의 의존성 확인
modinfo -F depends e1000e
# ptp
# modprobe로 의존성 트리 표시 (설치하지 않고)
modprobe --show-depends e1000e
# depmod 데이터베이스에서 역방향 의존성 (누가 이 모듈을 사용하는가)
grep 'e1000e' /lib/modules/$(uname -r)/modules.dep
# 간단한 Graphviz DOT 형식으로 의존성 그래프 생성
echo "digraph modules {"
lsmod | awk 'NR>1 && $4!="" {split($4,deps,","); for(d in deps) print " \""deps[d]"\" -> \""$1"\";"}'
echo "}"
# 출력을 module_deps.dot으로 저장 후: dot -Tpng module_deps.dot -o deps.png
# 특정 모듈의 전체 심볼 의존성 체인 추적
modprobe --show-depends e1000e 2>/dev/null | awk '{print $2}'
# /lib/modules/.../kernel/drivers/ptp/ptp.ko
# /lib/modules/.../kernel/drivers/net/ethernet/intel/e1000e/e1000e.ko
# 각 의존 모듈이 제공하는 심볼 확인
for m in $(modprobe --show-depends e1000e 2>/dev/null | awk '{print $2}'); do
echo "=== $m ==="
nm "$m" | grep '__ksymtab_' | sed 's/.*__ksymtab_//'
done
decode_stacktrace.sh 활용
커널 Oops나 패닉 메시지의 스택 트레이스를 한 번에 소스 위치로 변환하려면 scripts/decode_stacktrace.sh를 사용합니다. 이 스크립트는 addr2line을 내부적으로 호출하며, 모듈 심볼도 자동으로 처리합니다.
# Oops 메시지를 파이프로 전달
dmesg | ./scripts/decode_stacktrace.sh vmlinux /path/to/modules
# 저장된 로그 파일 처리
cat oops.log | ./scripts/decode_stacktrace.sh vmlinux
# 예시 출력 (변환 전)
# [ 123.456789] Call Trace:
# [] do_something+0x42/0x100
# [] caller_fn+0x30/0x80
# 예시 출력 (변환 후)
# [ 123.456789] Call Trace:
# do_something (kernel/core.c:158) +0x42/0x100
# caller_fn (kernel/entry.c:42) +0x30/0x80
# 모듈 심볼도 지원 (모듈 경로 지정 필요)
dmesg | ./scripts/decode_stacktrace.sh vmlinux /lib/modules/$(uname -r)/
# KASLR 환경에서도 정상 동작
# (함수명+오프셋 기반이므로 KASLR 영향 없음)
decode_stacktrace.sh와 faddr2line 모두 디버그 심볼이 포함된 vmlinux 파일이 필요합니다. CONFIG_DEBUG_INFO=y로 빌드된 커널에서만 사용 가능하며, 스트립된 vmlinux나 bzImage/vmlinuz로는 심볼 해석이 불가능합니다. 배포판에서는 linux-image-*-dbgsym 또는 kernel-debuginfo 패키지에서 디버그 심볼이 포함된 vmlinux를 제공합니다.
심볼 관련 커널 로그 패턴
심볼 문제를 진단할 때 자주 만나는 커널 로그 메시지와 그 원인을 정리합니다.
| 커널 로그 메시지 | 원인 | 해결 방법 |
|---|---|---|
Unknown symbol foo (err -2) | 심볼이 커널에 존재하지 않음 | 제공 모듈 로딩 확인, Module.symvers 동기화 |
foo: disagrees about version of symbol bar | CRC 불일치 (커널 버전 차이) | 현재 커널로 모듈 재빌드 |
module foo: GPL-incompatible module | 비GPL 모듈이 GPL 심볼 사용 시도 | MODULE_LICENSE("GPL") 확인 |
foo: module uses symbol from namespace BAR | 네임스페이스 import 누락 | MODULE_IMPORT_NS(BAR) 추가 |
module foo: exports duplicate symbol bar | 같은 이름의 심볼이 이미 등록됨 | 심볼 이름 변경 또는 충돌 모듈 확인 |
Lockdown: insmod: unsigned module loading | lockdown 모드에서 비서명 모듈 | 모듈 서명 또는 lockdown 해제 |
no symbol version for module_layout | 모듈이 CONFIG_MODVERSIONS 없이 빌드됨 | 같은 CONFIG로 모듈 재빌드 |
module foo: .text is not within module region | ARM64에서 모듈 로딩 주소가 범위 초과 | 커널/모듈 재빌드, KASLR 확인 |
이러한 오류 메시지를 효율적으로 추적하려면 dmesg -T로 타임스탬프를 확인하고, 해당 시점 전후의 모듈 로딩/언로딩 이벤트를 함께 살펴야 합니다. 특히 의존성 모듈이 먼저 로드되지 않아 발생하는 Unknown symbol은 modprobe 대신 insmod를 사용했을 때 자주 나타나며, modprobe는 modules.dep 기반으로 의존 모듈을 자동 로딩합니다.
Unknown symbol→grep symbol /proc/kallsyms로 존재 여부 확인 → 없으면Module.symvers에서 확인disagrees about version→modprobe --dump-modversions module.ko와 커널 CRC 비교- Oops 주소 해석 →
scripts/faddr2line vmlinux "func+offset" - 심볼 종류 확인 →
nm vmlinux | grep symbol(T=텍스트, D=데이터, B=BSS, U=미정의) - 모듈 의존성 순환 →
depmod -n으로 의존성 그래프 사전 확인 - 상세 디버깅 기법은 디버깅 가이드와 GDB 가이드를 참고하세요.
심볼 테이블 아키텍처 종합
커널 심볼 시스템은 단일 테이블이 아니라, 빌드 타임과 런타임에 걸쳐 여러 데이터 구조가 상호작용하는 다층 아키텍처입니다. 이 절에서는 System.map, __ksymtab, kallsyms, /proc/kallsyms, Module.symvers가 어떻게 생성되고, 어떤 시점에 어떤 역할을 하는지 전체 그림을 그립니다.
가장 흔한 혼란은 System.map과 /proc/kallsyms의 차이입니다. System.map은 빌드 시 nm vmlinux의 출력으로 생성되는 정적 파일이고, /proc/kallsyms는 커널 이미지 내부에 압축 저장된 심볼 데이터를 런타임에 동적으로 복원하는 가상 파일입니다. KASLR이 활성화되면 System.map의 주소와 /proc/kallsyms의 주소가 오프셋만큼 다르고, 모듈 심볼은 /proc/kallsyms에만 나타납니다.
System.map 상세 구조
System.map은 nm -n vmlinux의 출력을 정제한 텍스트 파일로, 커널 이미지의 모든 심볼을 주소 순서로 나열합니다. 각 줄은 주소 타입 이름 형태이며, 타입 문자가 심볼의 성격을 나타냅니다. 이 파일은 커널 부팅 후에는 직접 사용되지 않지만, Oops 해석, kexec, 크래시 덤프(Crash Dump) 분석 도구에서 참조합니다.
| 타입 문자 | 의미 | 예시 | 디버깅 관련성 |
|---|---|---|---|
T / t | 텍스트(코드) 심볼 (대문자=전역, 소문자=로컬) | do_sys_open, static_helper | 함수 위치 확인, Oops RIP 매칭 |
D / d | 초기화된 데이터 심볼 | system_state | 전역 변수 주소 확인 |
B / b | BSS(미초기화 데이터) 심볼 | init_task | 제로 초기화 변수 위치 |
R / r | 읽기 전용 데이터 심볼 | linux_banner | 상수 데이터 위치 |
A | 절대 심볼 (재배치 불가) | _etext | 커널 영역 경계 확인 |
W / w | 약한(weak) 심볼 | 아키텍처 폴백 함수 | 오버라이드 가능 함수 |
# System.map 형식 예시
ffffffff81000000 T _stext
ffffffff81000000 T startup_64
ffffffff81001000 T secondary_startup_64
...
ffffffff81234500 T do_sys_open
ffffffff81234600 t do_sys_openat2 # t = static (로컬) 함수
...
ffffffff82a00000 D system_state
ffffffff82b00000 B init_task
ffffffff82c00000 R linux_banner
# System.map과 /proc/kallsyms 주소 차이로 KASLR 오프셋 계산
MAP_BASE=$(awk '/T _stext/ {print "0x"$1}' /boot/System.map-$(uname -r))
KALL_BASE=$(awk '/T _stext/ {print "0x"$1}' /proc/kallsyms)
printf "KASLR offset: 0x%x\n" $((KALL_BASE - MAP_BASE))
# 특정 심볼 양쪽에서 비교
grep ' do_sys_open' /boot/System.map-$(uname -r)
grep ' do_sys_open' /proc/kallsyms
__ksymtab과 모듈 로더의 상호작용
모듈 로더(kernel/module/main.c)가 심볼을 해석할 때, vmlinux의 __ksymtab/__ksymtab_gpl 섹션과 이미 로드된 모듈의 export 테이블을 모두 검색합니다. 검색 순서는 다음과 같습니다.
- 커널 본체(
vmlinux)의__ksymtab검색 (이진 검색, 이름순 정렬) - 커널 본체의
__ksymtab_gpl검색 - 로드된 모듈 목록을 순회하며 각 모듈의 export 테이블 검색 (해시 테이블)
- 모든 검색에서 찾지 못하면
-ENOENT반환 →Unknown symbol
/* kernel/module/main.c — find_symbol() 단순화 */
static const struct kernel_symbol *find_symbol(
const char *name,
struct module **owner,
const s32 **crc,
enum mod_license *license,
bool gplok, bool warn)
{
const struct kernel_symbol *ks;
/* 1. vmlinux의 __ksymtab 검색 */
ks = lookup_exported_symbol(name, __start___ksymtab,
__stop___ksymtab);
if (ks) { *license = MOD_LICENSE_NORMAL; return ks; }
/* 2. vmlinux의 __ksymtab_gpl 검색 */
ks = lookup_exported_symbol(name, __start___ksymtab_gpl,
__stop___ksymtab_gpl);
if (ks) { *license = MOD_LICENSE_GPL; return ks; }
/* 3. 로드된 모듈 검색 */
list_for_each_entry(mod, &modules, list) {
ks = find_symbol_in_module(mod, name);
if (ks) return ks;
}
return NULL; /* Unknown symbol */
}
Module.symvers 파일 형식
Module.symvers는 커널 빌드 후 생성되는 텍스트 파일로, 모든 exported 심볼의 메타데이터를 담고 있습니다. 외부 모듈을 빌드할 때 이 파일을 참조하여 CRC, 타입, 네임스페이스 정보를 얻습니다. 파일 형식은 탭으로 구분된 5개 필드입니다.
# Module.symvers 형식 (커널 6.x)
# CRC 심볼명 모듈 타입 네임스페이스
0x12345678 printk vmlinux EXPORT_SYMBOL
0xabcdef01 kmalloc_caches vmlinux EXPORT_SYMBOL_GPL
0x87654321 drm_sched_init drivers/gpu/drm EXPORT_SYMBOL_NS_GPL DRM_GPU_SCHEDULER
0x11223344 usb_register_driver drivers/usb/core EXPORT_SYMBOL_GPL
# 특정 심볼의 CRC와 타입 확인
grep printk Module.symvers
# 0x12345678 printk vmlinux EXPORT_SYMBOL
# exported 심볼 수 통계
wc -l Module.symvers
awk '{print $4}' Module.symvers | sort | uniq -c | sort -rn
# 18432 EXPORT_SYMBOL_GPL
# 12156 EXPORT_SYMBOL
# 893 EXPORT_SYMBOL_NS_GPL
# 366 EXPORT_SYMBOL_NS
Module.symvers가 커널 빌드 디렉토리에 없으면 외부 모듈의 modpost가 CRC를 확인할 수 없어, CONFIG_MODVERSIONS=y 환경에서 CRC mismatch가 발생합니다. make modules_prepare 또는 make modules 후에 이 파일이 생성됩니다.
CRC 버저닝 메커니즘
CONFIG_MODVERSIONS가 활성화되면 커널은 exported 심볼마다 CRC(Cyclic Redundancy Check) 값을 계산하고 관리합니다. 이 CRC는 심볼의 "인터페이스 서명"으로, 함수 프로토타입(Prototype), 인자 타입, 사용된 구조체 정의 등을 종합하여 하나의 32비트 값으로 요약합니다. CRC가 같으면 인터페이스가 호환되고, 다르면 비호환입니다.
CRC 계산은 빌드 시 scripts/genksyms/genksyms 도구가 수행합니다. 이 도구는 C 전처리기 출력을 파싱하여 exported 심볼의 타입 정보를 추출하고, 그 타입 트리를 문자열로 직렬화한 뒤 CRC를 계산합니다. 따라서 함수 이름이 같아도 인자 타입이 바뀌거나, 사용되는 구조체에 필드가 추가되면 CRC가 바뀝니다.
genksyms 동작 원리
genksyms는 커널 빌드 시스템(Kbuild)에 의해 각 소스 파일 컴파일 시 자동으로 호출됩니다. 컴파일러의 전처리 출력(-E)을 입력으로 받아, exported 심볼의 타입 트리를 구성합니다. 타입 트리에는 함수의 인자 타입, 반환 타입, 사용된 구조체의 멤버(Member) 목록과 타입이 모두 포함됩니다. 이 트리를 문자열로 직렬화한 후 CRC-32를 계산하여 __kcrctab 섹션에 저장합니다.
# genksyms 수동 실행 예시 (일반적으로 Kbuild가 자동 호출)
gcc -E -D__GENKSYMS__ kernel/foo.c | scripts/genksyms/genksyms
# 출력: 심볼별 CRC 값
# __crc_foo_register = 0x12345678;
# 특정 심볼의 타입 서명 확인
gcc -E -D__GENKSYMS__ kernel/foo.c | scripts/genksyms/genksyms -D
# 출력: 심볼의 타입 트리 (디버그 모드)
# foo_register int ( struct device * )
# CRC 불일치 원인 추적 — 두 커널 간 CRC 차이 비교
diff <(grep foo_register Module.symvers.old) \
<(grep foo_register Module.symvers.new)
# CRC 값이 다르면 인터페이스가 변경된 것
__versions 섹션과 모듈 측 CRC
모듈(.ko)이 빌드될 때, modpost가 생성하는 .mod.c 파일에는 __versions 배열이 포함됩니다. 이 배열은 모듈이 참조하는 모든 외부 심볼의 CRC를 기록합니다. 모듈 로더는 이 배열과 커널의 __kcrctab을 비교하여 호환성을 검증합니다.
/* .mod.c — modpost가 자동 생성하는 CRC 배열 */
static const struct modversion_info ____versions[]
__used __section("__versions") = {
{ 0x12345678, "printk" },
{ 0xabcdef01, "kmalloc" },
{ 0x87654321, "module_layout" },
/* ... 모듈이 참조하는 모든 외부 심볼의 CRC */
};
/* module_layout 심볼은 특수:
* struct module의 레이아웃을 대표하는 CRC로,
* 커널 빌드 설정이 바뀌면 거의 항상 변경됩니다.
* 이 CRC가 불일치하면 "Invalid module format" 오류가 발생합니다. */
# 모듈의 __versions 섹션에서 기대 CRC 확인
modprobe --dump-modversions /path/to/my_module.ko
# 0x12345678 printk
# 0xabcdef01 kmalloc
# 0x87654321 module_layout
# readelf로 직접 확인
readelf -x __versions /path/to/my_module.ko | head -20
# 커널 측 CRC와 모듈 측 CRC 비교 (불일치 진단)
MOD_CRC=$(modprobe --dump-modversions my_module.ko | grep printk | awk '{print $1}')
KERN_CRC=$(grep printk Module.symvers | awk '{print $1}')
echo "Module CRC: $MOD_CRC, Kernel CRC: $KERN_CRC"
CONFIG_ 옵션은 #ifdef를 통해 구조체 멤버를 추가하거나 제거합니다. 예를 들어 CONFIG_SMP=y일 때 struct task_struct에 추가되는 필드가 있으면, 이 구조체를 인자로 사용하는 모든 심볼의 CRC가 바뀝니다. 따라서 모듈은 반드시 현재 실행 중인 커널과 동일한 .config로 빌드해야 합니다.
모듈 심볼 의존성과 depmod
커널 모듈은 다른 모듈이 export한 심볼을 참조할 수 있고, 이 참조 관계가 모듈 간 의존성을 형성합니다. depmod는 설치된 모듈을 분석하여 이 의존성 정보를 modules.dep 파일로 생성하며, modprobe는 이 파일을 참조하여 의존 모듈을 자동 로딩합니다.
의존성은 단순히 "모듈 A가 모듈 B를 필요로 한다"는 수준이 아닙니다. 의존성에는 심볼 수준의 정밀한 관계(어떤 심볼을 어느 모듈에서 가져오는가)와, 순서 제약(어떤 모듈이 먼저 로드되어야 하는가)이 포함됩니다. 잘못된 의존성은 부팅 실패, 드라이버(Driver) 미동작, 순환 의존성 데드락(Deadlock) 등의 심각한 문제를 일으킬 수 있습니다.
depmod 내부 동작
depmod는 /lib/modules/$(uname -r)/ 디렉토리의 모든 .ko 파일을 스캔하여 세 가지 정보를 추출합니다. 각 모듈이 export하는 심볼(nm에서 __ksymtab_* 심볼), 각 모듈이 필요로 하는 undefined 심볼(nm에서 U 타입), 그리고 각 모듈의 하드웨어 alias 정보(.modinfo 섹션)입니다.
# depmod 실행 (보통 커널 설치 후 자동 실행)
sudo depmod -a
# 또는 특정 커널 버전
sudo depmod -a 6.8.0-custom
# depmod 출력 파일 확인
ls /lib/modules/$(uname -r)/
# modules.dep — 의존성 목록 (텍스트)
# modules.dep.bin — 의존성 목록 (이진, 빠른 조회)
# modules.symbols — 심볼 → 모듈 매핑
# modules.symbols.bin
# modules.alias — 하드웨어 ID → 모듈
# modules.alias.bin
# modules.softdep — 소프트 의존성
# modules.devname — /dev 노드 매핑
# modules.dep 형식 예시
cat /lib/modules/$(uname -r)/modules.dep | grep e1000e
# kernel/drivers/net/ethernet/intel/e1000e/e1000e.ko: kernel/drivers/ptp/ptp.ko
# modules.symbols 형식 예시
grep ptp_clock_register /lib/modules/$(uname -r)/modules.symbols
# alias symbol:ptp_clock_register ptp
# depmod -n: 실제 쓰지 않고 표준 출력으로 결과 미리보기
depmod -n | head -50
# depmod -e: undefined 심볼 오류만 표시
depmod -e
# 출력이 있으면 해결되지 않은 심볼 의존성이 존재
순환 의존성과 약한 의존성
커널 모듈 간 순환 의존성(A → B → A)은 모듈 시스템이 허용하지 않으며, depmod가 경고를 출력합니다. 순환 의존성이 발생하는 경우에는 모듈 분할, symbol_get() 사용, 또는 공통 심볼을 별도 모듈로 분리하는 방법으로 해결합니다.
약한 의존성(softdep)은 모듈이 동작에 필수적이지는 않지만 있으면 기능이 향상되는 관계를 표현합니다. /etc/modprobe.d/의 설정 파일이나 모듈 자체의 MODULE_SOFTDEP() 매크로로 선언합니다.
/* 모듈 내부에서 소프트 의존성 선언 */
MODULE_SOFTDEP("pre: thermal");
/* thermal 모듈이 있으면 먼저 로딩 시도, 없어도 현재 모듈은 로딩됨 */
/* symbol_get() 패턴으로 순환 의존성 회피 */
static int (*optional_fn)(void);
static int try_optional_feature(void)
{
optional_fn = symbol_get(some_optional_export);
if (!optional_fn)
return -EOPNOTSUPP; /* 기능 없이 계속 동작 */
int ret = optional_fn();
symbol_put(some_optional_export);
return ret;
}
| 의존성 유형 | 선언 방법 | 로딩 실패 시 동작 | 사용 사례 |
|---|---|---|---|
| 강한 의존성 (hard) | undefined 심볼 참조 (자동) | 모듈 로딩 거부 (Unknown symbol) | 필수 API 사용 |
| 약한 의존성 (soft) | MODULE_SOFTDEP() | 경고만, 모듈 로딩은 성공 | 선택적 기능 향상 |
| 선택적 심볼 | symbol_get() / symbol_put() | NULL 반환, 코드에서 분기 | 모듈 있으면 사용, 없으면 우회 |
| 역방향 콜백(Callback) | 함수 포인터(Function Pointer) 등록 | 등록 안 하면 콜백 미호출 | 알림(Notification) 체인, 드라이버 바인딩 |
심볼 해석 과정 상세 흐름
모듈이 로딩될 때 커널 모듈 로더는 .ko 파일의 모든 재배치(Relocation) 엔트리를 순회하며, 각 undefined 심볼을 실제 주소로 해결합니다. 이 과정은 단순한 이름 매칭이 아니라, CRC 검증, GPL 라이선스 확인, 네임스페이스 검증, 주소 바인딩을 포함하는 다단계 파이프라인입니다.
심볼 해시 테이블 구조
모듈 로더는 exported 심볼을 이름으로 빠르게 찾기 위해 해시 테이블을 사용합니다. vmlinux의 __ksymtab은 이름순으로 정렬되어 이진 검색을 사용하지만, 로드된 모듈의 심볼은 해시 테이블에 등록됩니다. 해시 함수는 심볼 이름의 각 문자를 순회하며 다항식(Polynomial) 해싱을 수행합니다.
/* kernel/module/main.c — 심볼 해시 테이블 */
#define MODVERIFY_HASH_BITS 8
#define MODVERIFY_HASH_SIZE (1 << MODVERIFY_HASH_BITS) /* 256 */
static struct hlist_head module_hashtable[MODVERIFY_HASH_SIZE];
/* 이름 → 해시값 계산 */
static unsigned long hash_name(const char *name)
{
unsigned long h = 0;
while (*name)
h = 31 * h + *name++;
return h & (MODVERIFY_HASH_SIZE - 1);
}
/* 모듈 로딩 후 심볼을 해시 테이블에 등록 */
static void mod_update_bounds(struct module *mod)
{
/* 각 exported 심볼을 해시 버킷에 추가 */
for (i = 0; i < mod->num_syms; i++) {
unsigned long h = hash_name(sym->name);
hlist_add_head(&sym->hnode, &module_hashtable[h]);
}
}
주소에서 심볼로 역변환 (크래시 분석)
Oops, 패닉(Panic), ftrace 출력에서 주소를 심볼 이름으로 변환하는 역방향 경로는 sprint_symbol()과 %pS 포맷 스트링이 담당합니다. 이 과정은 kallsyms 테이블의 이진 검색으로 시작하여, 가장 가까운 심볼의 이름과 오프셋을 반환합니다.
/* kernel/kallsyms.c — 주소 → 심볼 역변환 핵심 함수 */
/* sprint_symbol: 주소를 "함수명+오프셋/크기 [모듈]" 형태로 변환 */
int sprint_symbol(char *buffer, unsigned long address)
{
char *modname;
const char *name;
unsigned long offset, size;
int len;
name = kallsyms_lookup(address, &size, &offset, &modname, buffer);
if (!name)
return sprintf(buffer, "0x%lx", address);
if (modname)
len = sprintf(buffer, "%s+%#lx/%#lx [%s]",
name, offset, size, modname);
else
len = sprintf(buffer, "%s+%#lx/%#lx",
name, offset, size);
return len;
}
/* 사용 예: 커널 코드에서 %pS 포맷 */
pr_info("caller: %pS\n", __builtin_return_address(0));
/* 출력: caller: do_sys_open+0x42/0x100 */
/* %pSR: 역방향 참조용 (인라인 함수 포함) */
pr_info("return: %pSR\n", __builtin_return_address(0));
CONFIG_KALLSYMS_ALL 설정에 따라 달라집니다. 기본 설정에서는 함수 심볼만 포함되므로, 주소가 데이터 영역에 있으면 "가장 가까운 함수"의 이름이 부정확하게 출력될 수 있습니다. CONFIG_KALLSYMS_ALL=y를 활성화하면 데이터 심볼까지 포함되어 정확도가 크게 향상됩니다.
KASLR과 심볼 해석
KASLR(Kernel Address Space Layout Randomization)은 커널이 매 부팅마다 다른 기본 주소(Base Address)에 로딩되도록 하여, 공격자가 커널 함수의 주소를 예측하기 어렵게 만드는 보안 기능입니다. 이 기능은 심볼 해석에 직접적인 영향을 미치므로, 디버깅과 프로파일링 시 반드시 고려해야 합니다.
KASLR이 활성화되면 커널 텍스트 영역 전체가 무작위 오프셋만큼 이동합니다. 이 오프셋은 부팅 시 결정되며, 부팅 세션 동안 유지됩니다. 결과적으로 System.map에 기록된 정적 주소와 /proc/kallsyms에 표시되는 런타임 주소 사이에 고정 차이(KASLR 오프셋)가 존재합니다. 이 오프셋을 모르면 Oops 주소를 vmlinux의 심볼과 직접 대조할 수 없습니다.
KASLR 오프셋 계산 방법
디버깅 시 KASLR 오프셋을 알아야 하는 경우, System.map과 /proc/kallsyms의 동일 심볼 주소 차이를 계산합니다. 이 오프셋은 부팅 세션 동안 모든 커널 심볼에 동일하게 적용됩니다.
# KASLR 오프셋 계산
STATIC=$(awk '/ T _stext$/ {print $1}' /boot/System.map-$(uname -r))
RUNTIME=$(awk '/ T _stext$/ {print $1}' /proc/kallsyms)
OFFSET=$((0x$RUNTIME - 0x$STATIC))
printf "KASLR offset: 0x%lx (%ld bytes)\n" $OFFSET $OFFSET
# Oops 주소를 정적 주소로 변환
OOPS_ADDR=0xffffffff9a634567
STATIC_ADDR=$((OOPS_ADDR - OFFSET))
printf "Static address: 0x%lx\n" $STATIC_ADDR
# 이 정적 주소로 addr2line 사용 가능
addr2line -e vmlinux -f $(printf "0x%lx" $STATIC_ADDR)
# KASLR 비활성화 (디버깅 용도)
# 커널 부트 파라미터에 nokaslr 추가
# GRUB: GRUB_CMDLINE_LINUX="... nokaslr"
# 현재 KASLR 상태 확인
dmesg | grep "KASLR"
# 또는
cat /proc/cmdline | grep -o "nokaslr"
# 출력 없으면 KASLR 활성화 상태
KASLR과 모듈 영역
KASLR은 커널 텍스트 영역뿐만 아니라 모듈 로딩 영역도 무작위화합니다. 이는 모듈 심볼의 주소도 매 부팅마다 달라진다는 것을 의미합니다. 모듈 영역의 무작위화는 아키텍처마다 다르게 구현됩니다.
| 구성 요소 | KASLR OFF 주소 (예시) | KASLR ON 주소 (예시) | 무작위화 범위 |
|---|---|---|---|
커널 텍스트 (_stext) | 0xffffffff81000000 | 0xffffffff9a400000 | 최대 1GB |
| 모듈 영역 | 0xffffffffa0000000 | 0xffffffffb2300000 | 모듈 영역 내 랜덤 |
| 물리 매핑 | 고정 | 무작위 | CONFIG_RANDOMIZE_MEMORY |
| vmalloc 영역 | 고정 | 무작위 | CONFIG_RANDOMIZE_MEMORY |
FGKASLR: 함수 수준 무작위화
FGKASLR(Fine-Grained KASLR)은 기존 KASLR을 확장하여 커널 텍스트 영역 전체를 하나의 블록으로 이동하는 대신, 개별 함수 단위로 위치를 무작위화합니다. 이 기능이 활성화되면 같은 커널 이미지 내에서도 함수 간 상대적 위치가 부팅마다 달라집니다.
FGKASLR이 심볼 해석에 미치는 영향은 상당합니다. 기존 KASLR에서는 하나의 오프셋만 알면 모든 심볼의 런타임 주소를 계산할 수 있었지만, FGKASLR에서는 각 함수의 위치가 개별적으로 무작위화되므로, /proc/kallsyms나 vmcore의 심볼 테이블을 반드시 참조해야 합니다.
# FGKASLR 관련 CONFIG 확인
grep "CONFIG_FG_KASLR" /boot/config-$(uname -r)
# FGKASLR 활성화 시 /proc/kallsyms에서 함수 순서가 매 부팅 달라짐
# 기존 KASLR: 함수 순서 유지, 시작 주소만 이동
# FGKASLR: 함수 순서도 셔플됨
# FGKASLR 비활성화 (디버깅 용도)
# 커널 부트 파라미터: nofgkaslr
scripts/faddr2line과 scripts/decode_stacktrace.sh는 함수명+오프셋을 입력으로 받으므로 KASLR과 무관하게 정확한 결과를 제공합니다. crash 유틸리티와 GDB도 vmcore에서 KASLR 오프셋을 자동 감지합니다.
커널 심볼 보안
커널 심볼 정보는 디버깅에 필수적이지만, 동시에 공격자에게 중요한 정보를 제공합니다. 커널 주소를 알면 KASLR 우회, ROP 가젯(Gadget) 찾기, 커널 구조체 레이아웃 파악이 가능해집니다. 따라서 커널은 다양한 메커니즘으로 심볼 정보 접근을 제한합니다. 이 절에서는 kptr_restrict, dmesg_restrict, perf_event_paranoid, Lockdown LSM의 상호작용과 실전 설정 전략을 다룹니다.
dmesg_restrict와 심볼 노출
dmesg_restrict는 커널 로그 접근을 제한하는 sysctl입니다. 커널 로그에는 %pS, %pK 포맷으로 출력된 심볼 주소가 포함될 수 있으므로, 커널 로그 접근 제한은 심볼 보안의 일부입니다. dmesg_restrict=1로 설정하면 CAP_SYSLOG 권한이 없는 사용자는 dmesg 명령어로 커널 로그를 읽을 수 없습니다.
# dmesg_restrict 현재 값 확인
cat /proc/sys/kernel/dmesg_restrict
# 프로덕션 환경에서 활성화
echo 1 | sudo tee /proc/sys/kernel/dmesg_restrict
# 영구 설정 (/etc/sysctl.d/)
echo "kernel.dmesg_restrict = 1" | sudo tee /etc/sysctl.d/99-security.conf
sudo sysctl -p /etc/sysctl.d/99-security.conf
# dmesg_restrict=1 상태에서 비특권 사용자
dmesg
# dmesg: read kernel buffer failed: Operation not permitted
# CAP_SYSLOG 부여로 특정 사용자에게만 허용
sudo setcap cap_syslog+ep /usr/bin/dmesg
%p 포맷 변천사
커널 4.15 이전에는 %p가 실제 포인터 주소를 그대로 출력했습니다. 이것은 커널 주소 유출의 주요 경로였습니다. 4.15부터 %p는 해시된 값을 출력하여 주소 유출을 방지합니다. 디버깅이나 의도적 노출이 필요한 경우에는 목적에 맞는 포맷 스펙(Format Specifier)을 사용해야 합니다.
| 포맷 | 출력 | 보안 수준 | 사용 용도 |
|---|---|---|---|
%p | 해시된 주소 (0000000012345678) | 안전 (유출 방지) | 일반 포인터 표시 (기본 권장) |
%px | 실제 주소 (ffff888012345678) | 위험 (항상 노출) | 디버깅 전용 코드 |
%pK | 정책 기반 (kptr_restrict) | 정책 의존 | 사용자 대면 인터페이스 |
%pS | 심볼+오프셋 (func+0x42) | 중간 (이름 노출) | Oops, 콜 트레이스 |
%pSR | 심볼+오프셋+인라인 | 중간 | 상세 콜 트레이스 |
%pB | 역추적용 심볼 | 중간 | 백트레이스 출력 |
%ps | 심볼 이름만 (오프셋 없음) | 중간 | 간단한 함수명 표시 |
/* 포맷 스트링 사용 예시 */
void *ptr = some_func();
pr_info("hash: %p\n", ptr); /* 00000000a1b2c3d4 (해시) */
pr_info("actual: %px\n", ptr); /* ffffffff81234500 (실제) */
pr_info("policy: %pK\n", ptr); /* kptr_restrict에 따라 다름 */
pr_info("symbol: %pS\n", ptr); /* some_func+0x0/0x100 */
pr_info("name: %ps\n", ptr); /* some_func */
/* WARNING: 새로운 코드에서 %px는 극히 제한적으로만 사용
* checkpatch.pl이 %px 사용 시 경고를 출력합니다 */
보안 설정 종합 체크리스트
심볼 보안과 관련된 모든 sysctl, Kconfig, 부트 파라미터(Boot Parameter)를 한 번에 점검하는 체크리스트입니다.
# === 심볼 보안 종합 점검 스크립트 ===
echo "=== sysctl 설정 ==="
echo "kptr_restrict: $(cat /proc/sys/kernel/kptr_restrict)"
echo "dmesg_restrict: $(cat /proc/sys/kernel/dmesg_restrict)"
echo "perf_event_paranoid: $(cat /proc/sys/kernel/perf_event_paranoid)"
echo ""
echo "=== Lockdown 상태 ==="
cat /sys/kernel/security/lockdown 2>/dev/null || echo "lockdown LSM 미활성화"
echo ""
echo "=== KASLR 상태 ==="
if grep -q "nokaslr" /proc/cmdline; then
echo "KASLR: OFF (nokaslr 파라미터 감지)"
else
echo "KASLR: ON (기본값)"
fi
echo ""
echo "=== Kconfig 보안 옵션 ==="
for opt in CONFIG_KALLSYMS CONFIG_KALLSYMS_ALL \
CONFIG_RANDOMIZE_BASE CONFIG_MODULE_SIG \
CONFIG_MODULE_SIG_FORCE CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY \
CONFIG_SECURITY_LOCKDOWN_LSM; do
val=$(grep "^$opt=" /boot/config-$(uname -r) 2>/dev/null)
echo " $opt: ${val:-not set}"
done
echo ""
echo "=== /proc/kallsyms 접근 테스트 ==="
FIRST=$(head -1 /proc/kallsyms)
if echo "$FIRST" | grep -q "^0000000000000000"; then
echo "주소 마스킹됨 (kptr_restrict 또는 권한 부족)"
else
echo "실제 주소 노출: $FIRST"
fi
echo ""
echo "=== 모듈 서명 상태 ==="
dmesg | grep -i "module.*verif" | tail -3
실전 심볼 분석 워크플로
이 절에서는 실제 업무에서 자주 만나는 심볼 관련 작업을 단계별로 정리합니다. 각 시나리오는 "문제 인식 → 도구 선택 → 분석 → 해결"의 흐름으로 구성됩니다.
시나리오 1: 모듈 빌드는 성공했지만 로딩 실패
# 증상
sudo insmod my_module.ko
# insmod: ERROR: could not insert module: Unknown symbol in module
# Step 1: 어떤 심볼이 없는지 확인
dmesg | tail -20
# my_module: Unknown symbol foo_register (err -2)
# Step 2: 모듈이 필요로 하는 모든 외부 심볼 확인
nm -u my_module.ko
# U foo_register
# U printk
# U kmalloc
# Step 3: 해당 심볼이 커널에 있는지 확인
grep foo_register /proc/kallsyms
# 출력 없음 → 커널에 없음
# Step 4: Module.symvers에 있는지 확인
grep foo_register /usr/src/linux-$(uname -r)/Module.symvers
# 출력 있음 → 빌드 환경에는 있지만 현재 커널에 로드 안 됨
# Step 5: 제공 모듈이 로드되었는지 확인
lsmod | grep foo_provider
# 출력 없음 → modprobe foo_provider 먼저 실행
# 해결: insmod 대신 modprobe 사용 (의존성 자동 해결)
sudo modprobe my_module
시나리오 2: CRC mismatch 진단
# 증상
dmesg | tail
# my_module: disagrees about version of symbol module_layout
# my_module: Unknown symbol module_layout (err -22)
# Step 1: 모듈과 커널의 CRC 비교
modprobe --dump-modversions my_module.ko | grep module_layout
# 0xabcdef01 module_layout
grep module_layout /usr/src/linux-$(uname -r)/Module.symvers
# 0x12345678 module_layout vmlinux EXPORT_SYMBOL
# → CRC 불일치 (0xabcdef01 != 0x12345678)
# Step 2: 원인 진단 — 모듈 빌드 시 사용한 커널 헤더 확인
modinfo my_module.ko | grep vermagic
# vermagic: 6.7.0-custom SMP preempt mod_unload
uname -r
# 6.8.0-custom
# → 다른 커널 버전으로 빌드됨
# 해결: 현재 실행 중인 커널의 소스/헤더로 재빌드
make -C /lib/modules/$(uname -r)/build M=$(pwd) clean
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
시나리오 3: 커널 함수 위치 확인 (개발/분석)
# 특정 함수가 어느 파일에 정의되어 있는지 확인
# 방법 1: /proc/kallsyms에서 주소 확인 후 addr2line
grep ' T do_sys_open' /proc/kallsyms
# ffffffff81234500 T do_sys_open
addr2line -e /usr/lib/debug/boot/vmlinux-$(uname -r) ffffffff81234500
# fs/open.c:1234
# 방법 2: nm + 정렬로 함수 크기 추정
nm -n vmlinux | grep -A1 ' T do_sys_open'
# ffffffff81234500 T do_sys_open
# ffffffff81234600 T do_sys_openat2
# → do_sys_open 크기 ≈ 0x100 (256바이트)
# 방법 3: objdump로 함수 디스어셈블리
objdump -d vmlinux --start-address=0xffffffff81234500 \
--stop-address=0xffffffff81234600 | less
# 방법 4: readelf로 심볼의 정확한 크기와 바인딩 확인
readelf -Ws vmlinux | grep do_sys_open
# 12345: ffffffff81234500 256 FUNC GLOBAL DEFAULT 1 do_sys_open
# ^^^ 크기 ^^^^ 바인딩
시나리오 4: exported 심볼 정책 감사
# 현재 커널의 export 정책 통계
awk '{print $4}' Module.symvers | sort | uniq -c | sort -rn
# 18432 EXPORT_SYMBOL_GPL
# 12156 EXPORT_SYMBOL
# 893 EXPORT_SYMBOL_NS_GPL
# 366 EXPORT_SYMBOL_NS
# 특정 서브시스템의 export 목록
grep "drivers/gpu/drm" Module.symvers | wc -l
grep "drivers/gpu/drm" Module.symvers | awk '{print $5}' | sort | uniq -c
# 네임스페이스별 분포 확인
# 네임스페이스가 없는 GPL 심볼 (네임스페이스 도입 후보)
awk '$4 == "EXPORT_SYMBOL_GPL" && $5 == "" {print $2, $3}' Module.symvers | head -20
# 특정 모듈이 export하는 심볼 수
awk '{print $3}' Module.symvers | sort | uniq -c | sort -rn | head -20
# 어떤 모듈/서브시스템이 가장 많은 심볼을 export하는지 파악
# TRIM_UNUSED_KSYMS 효과 시뮬레이션
# 트리 내 모듈이 실제 참조하는 심볼 수
find /lib/modules/$(uname -r) -name "*.ko" -exec nm -u {} \; 2>/dev/null | \
awk '{print $2}' | sort -u | wc -l
시나리오 5: Oops 심볼 해석 완전 워크플로
# 실제 Oops 메시지에서 원인 추적까지의 완전한 흐름
# 1. Oops 메시지 수집
dmesg > /tmp/oops.log
# 2. decode_stacktrace.sh로 한 번에 소스 위치 변환
cat /tmp/oops.log | ./scripts/decode_stacktrace.sh vmlinux \
/lib/modules/$(uname -r)/ > /tmp/oops_decoded.log
# 3. 모듈 함수의 경우 faddr2line으로 정밀 분석
# Oops에서 "[my_driver] my_driver_cleanup+0x1e/0x50" 발견 시:
./scripts/faddr2line my_driver.ko "my_driver_cleanup+0x1e/0x50"
# my_driver_cleanup+0x1e/0x50:
# kfree at my_driver.c:142 (inlined by) my_driver_cleanup at my_driver.c:138
# 4. objdump로 해당 위치의 실제 명령어 확인
objdump -dS my_driver.ko | grep -B5 -A10 "my_driver_cleanup"
# C 소스와 어셈블리가 인터리브되어 표시 (-S 옵션)
# 5. GDB로 대화형 분석
gdb vmlinux
(gdb) list *(do_something+0x42)
# 해당 오프셋의 소스 코드 표시
# 6. crash 유틸리티로 vmcore 분석 (코어 덤프 있는 경우)
crash vmlinux /var/crash/vmcore
crash> bt
# 크래시 시점의 전체 백트레이스
crash> sym ffffffff81234567
# ffffffff81234567 (T) do_something+0x42 [vmlinux]
crash> dis do_something
# 함수 전체 디스어셈블리
- 빌드 단계 문제 →
nm -u,Module.symvers, modpost 로그 - 로딩 단계 문제 →
dmesg,modprobe -v,/proc/kallsyms - 런타임 Oops →
faddr2line,decode_stacktrace.sh,crash - 성능 분석 →
perf+/proc/kallsyms(kptr_restrict 확인) - 보안 감사 →
Module.symvers분석, export 정책 통계
kallsyms_lookup_name()과 심볼 접근 제한
kallsyms_lookup_name()은 심볼 이름을 주소로 변환하는 커널 내부 함수입니다. 커널 5.7 이전에는 이 함수가 EXPORT_SYMBOL_GPL로 export되어 있어 모듈에서도 사용할 수 있었지만, 5.7부터 export가 제거되었습니다. 이 변경은 외부 모듈이 export되지 않은 내부 심볼의 주소를 임의로 얻어 호출하는 우회 패턴을 방지하기 위한 것입니다.
이 함수의 export 제거는 커널 심볼 보안의 중요한 전환점이었습니다. 이전에는 모듈이 kallsyms_lookup_name("any_internal_function")을 호출하여 export되지 않은 어떤 함수든 주소를 얻고 함수 포인터로 호출할 수 있었습니다. 이는 EXPORT_SYMBOL 정책을 완전히 무력화하는 것이었으므로, 보안상 제거가 불가피했습니다.
/* 커널 5.7 이전: 모듈에서 kallsyms_lookup_name 사용 가능 */
unsigned long addr = kallsyms_lookup_name("internal_fn");
if (addr) {
int (*fn)(void) = (int (*)(void))addr;
fn(); /* export 안 된 함수를 강제 호출 — 위험! */
}
/* 커널 5.7 이후: kallsyms_lookup_name은 더 이상 export 안 됨
* 모듈에서 호출하면 링크 에러 발생
*
* 대안:
* 1. kprobes를 사용하여 함수 주소 획득 (제한적)
* 2. BPF kfunc을 통한 안전한 접근
* 3. 필요한 기능을 정식으로 export 요청 (커널 패치)
*/
/* kprobes를 통한 우회 (커널 내부용, 모듈에서는 제한적) */
#include <linux/kprobes.h>
static struct kprobe kp = {
.symbol_name = "kallsyms_lookup_name",
};
static int __init my_init(void)
{
int ret = register_kprobe(&kp);
if (ret < 0) return ret;
/* kp.addr에 kallsyms_lookup_name의 주소가 들어있음
* 주의: 이 방법도 lockdown 환경에서는 차단될 수 있음 */
unregister_kprobe(&kp);
return 0;
}
kallsyms_lookup_name() export 제거를 우회하는 기법(kprobes 활용, /proc/kallsyms 파싱 등)은 기술적으로 가능하지만, 커널 ABI 정책을 의도적으로 위반하는 행위입니다. 이러한 우회는 커널 업데이트 시 호환성이 보장되지 않으며, lockdown이 활성화된 환경에서는 작동하지 않을 수 있습니다. 장기적으로는 필요한 기능에 대한 정식 export 요청 패치를 제출하거나, BPF kfunc 같은 공식 인터페이스를 사용하는 것이 올바른 접근입니다.
심볼 관련 데이터 소스 비교 총정리
커널 심볼과 관련된 다양한 데이터 소스를 한 눈에 비교합니다. 각 소스는 생성 시점, 포함 정보, 주요 소비자가 다르므로, 상황에 맞는 소스를 선택하는 것이 중요합니다.
| 데이터 소스 | 생성 시점 | 포함 정보 | 모듈 포함 | KASLR 반영 | 주요 소비자 |
|---|---|---|---|---|---|
vmlinux (.symtab) | 링크 시 | 모든 ELF 심볼 + DWARF | vmlinux 심볼만 | 정적 주소 | GDB, addr2line, objdump |
System.map | 빌드 후 | 주소 + 타입 + 이름 | vmlinux 심볼만 | 정적 주소 | kexec, 수동 분석 |
Module.symvers | modpost 후 | CRC + 이름 + 모듈 + 타입 + NS | 모든 export 포함 | 해당 없음 | 외부 모듈 빌드 |
__ksymtab | 링크 시 | exported 심볼 주소/이름 | vmlinux export만 | 런타임 재배치 | 모듈 로더 |
kallsyms (내부) | 최종 링크 | 압축 심볼 이름 + 주소 | vmlinux 심볼 | 런타임 반영 | sprint_symbol, %pS |
/proc/kallsyms | 런타임 | 주소 + 타입 + 이름 [모듈] | 로드된 모듈 포함 | 런타임 반영 | perf, 사용자 도구 |
modules.dep | depmod 실행 | 모듈 → 의존 모듈 | 설치된 모듈 | 해당 없음 | modprobe |
modules.symbols | depmod 실행 | 심볼 → 제공 모듈 | 설치된 모듈 | 해당 없음 | modprobe |
.BTF 섹션 | pahole 실행 | 타입 정보 (이름/오프셋/크기) | CONFIG별 | 해당 없음 | BPF, libbpf |
- Oops 분석 →
/proc/kallsyms(런타임 주소) +vmlinux(소스 위치) - 외부 모듈 빌드 →
Module.symvers(CRC 참조) - 모듈 의존성 →
modules.dep,modprobe --show-depends - BPF 프로그램 →
.BTF섹션 +/proc/kallsyms - perf 프로파일링 →
/proc/kallsyms(kptr_restrict 확인) - vmcore 분석 →
vmlinux+ crash 유틸리티
ELF 심볼 테이블과 커널 심볼의 관계
커널 심볼을 이해하려면 ELF(Executable and Linkable Format) 심볼 테이블의 구조를 알아야 합니다. vmlinux와 .ko 파일은 모두 ELF 형식이며, 각 파일의 .symtab/.strtab 섹션에 심볼 정보가 저장됩니다. 커널의 EXPORT_SYMBOL은 이 ELF 심볼 위에 별도의 export 레이어(__ksymtab)를 추가하는 것입니다.
readelf로 보는 심볼 테이블 실전
readelf -Ws는 ELF 심볼 테이블의 원본 형태를 보여줍니다. 각 필드의 의미를 정확히 이해하면 심볼 문제를 효과적으로 진단할 수 있습니다.
# vmlinux의 심볼 테이블 구조 확인
readelf -S vmlinux | grep -E "symtab|strtab|ksymtab|kcrctab"
# [N] .symtab SYMTAB ... ← ELF 심볼 테이블
# [N] .strtab STRTAB ... ← 심볼 이름 문자열
# [N] __ksymtab PROGBITS ... ← 일반 exported 심볼
# [N] __ksymtab_gpl PROGBITS ... ← GPL exported 심볼
# [N] __kcrctab PROGBITS ... ← CRC 값 배열
# [N] __ksymtab_strings PROGBITS ... ← export 심볼 이름 문자열
# 특정 심볼의 ELF 정보 상세 확인
readelf -Ws vmlinux | grep "printk$"
# Num: Value Size Type Bind Vis Ndx Name
# 12345: ffffffff8112a0 256 FUNC GLOBAL DEFAULT 1 printk
# ^^^^^^^^ ^^ ^^^^ ^^^^^^ ^^^^^^^
# 주소 크기 타입 바인딩 가시성
# .ko 파일의 재배치 엔트리 확인 (undefined 심볼 참조)
readelf -r my_module.ko | head -30
# Offset Info Type Sym. Value Sym. Name + Addend
# 000000000040 000c00000002 R_X86_64_PC32 0000000000000000 printk - 4
# .ko의 __versions 섹션 (모듈이 기대하는 CRC)
readelf -S my_module.ko | grep __versions
# [N] __versions PROGBITS ... ← CRC 기대값 배열
# .ko에서 EXPORT되는 심볼 확인 (__ksymtab)
readelf -S my_module.ko | grep ksymtab
# 출력 있으면 모듈이 심볼을 export하는 것
ELF 바인딩과 커널 export의 차이
ELF 심볼의 바인딩(Binding)은 LOCAL, GLOBAL, WEAK 세 가지가 있습니다. static 함수는 LOCAL, 전역 함수는 GLOBAL로 기록됩니다. 그러나 GLOBAL이라고 해서 외부 모듈이 사용할 수 있는 것은 아닙니다. 커널 모듈 시스템은 ELF 바인딩과 무관하게, __ksymtab에 등록된 심볼만 외부 모듈에 제공합니다.
| ELF 바인딩 | C 선언 | nm 출력 | readelf 출력 | 외부 모듈 사용 |
|---|---|---|---|---|
| LOCAL | static int foo(void) | t foo (소문자) | Bind: LOCAL | 불가 (파일 내부) |
| GLOBAL | int foo(void) | T foo (대문자) | Bind: GLOBAL | EXPORT 없으면 불가 |
| GLOBAL + EXPORT | int foo(void) + EXPORT_SYMBOL(foo) | T foo + D __ksymtab_foo | Bind: GLOBAL | 가능 |
| WEAK | __weak int foo(void) | W foo | Bind: WEAK | 오버라이드 가능 |
stripeed vmlinux와 심볼 손실
배포판이 제공하는 vmlinuz(압축 커널 이미지)와 bzImage는 .symtab과 .strtab이 제거(strip)된 상태입니다. 따라서 이 파일로는 addr2line이나 GDB를 사용할 수 없습니다. 디버그 심볼이 포함된 vmlinux는 별도 패키지로 제공됩니다.
# 배포판별 디버그 심볼 패키지 설치
# Ubuntu/Debian
sudo apt install linux-image-$(uname -r)-dbgsym
# 설치 위치: /usr/lib/debug/boot/vmlinux-$(uname -r)
# Fedora/RHEL/CentOS
sudo debuginfo-install kernel-$(uname -r)
# 설치 위치: /usr/lib/debug/lib/modules/$(uname -r)/vmlinux
# Arch Linux
# AUR에서 linux-debug 패키지
# 직접 빌드 시 vmlinux 보존
make -j$(nproc)
# vmlinux가 소스 디렉토리 최상위에 생성됨
# CONFIG_DEBUG_INFO=y 필수
# vmlinux 파일에 심볼이 있는지 확인
readelf -S vmlinux | grep -c symtab
# 1 이면 심볼 포함, 0 이면 strip됨
# strip된 파일과 비교
ls -lh vmlinux # ~500MB (디버그 포함)
ls -lh /boot/vmlinuz-* # ~10MB (압축 + strip)
.ko)의 디버그 심볼도 배포판에서 별도 패키지로 제공됩니다. CONFIG_DEBUG_INFO=y로 빌드된 모듈에는 DWARF 정보가 포함되어 addr2line으로 모듈 내 소스 위치를 확인할 수 있습니다. 직접 빌드하는 외부 모듈의 경우, Makefile에 CFLAGS_MODULE += -g를 추가하면 디버그 정보가 포함됩니다.
모듈 로딩과 심볼 해석 추적 기법
모듈 로딩 과정에서 심볼 해석이 어떻게 진행되는지 직접 추적하면, 로딩 실패의 원인을 정확히 파악할 수 있습니다. 커널은 ftrace, 로깅, sysfs를 통해 모듈 로딩 과정을 관찰할 수 있는 여러 인터페이스를 제공합니다.
ftrace로 모듈 로딩 추적
ftrace의 함수 추적 기능을 사용하면 모듈 로딩 시 호출되는 심볼 해석 함수의 실행 흐름을 볼 수 있습니다. find_symbol(), simplify_symbols(), apply_relocate_add() 등의 핵심 함수를 추적합니다.
# ftrace로 모듈 로딩 핵심 함수 추적
cd /sys/kernel/debug/tracing
# 추적할 함수 설정
echo "load_module" > set_ftrace_filter
echo "find_symbol" >> set_ftrace_filter
echo "simplify_symbols" >> set_ftrace_filter
echo "check_modstruct_version" >> set_ftrace_filter
echo "resolve_symbol_wait" >> set_ftrace_filter
# function_graph 추적기 활성화
echo function_graph > current_tracer
echo 1 > tracing_on
# 모듈 로딩
modprobe my_module
# 추적 결과 확인
cat trace
# 출력 예시:
# 0) | load_module() {
# 0) | simplify_symbols() {
# 0) 0.123 us | find_symbol();
# 0) 0.098 us | check_modstruct_version();
# 0) 0.087 us | find_symbol();
# 0) ... | }
# 0) | }
# 추적 종료
echo 0 > tracing_on
echo nop > current_tracer
sysfs 모듈 정보 활용
로드된 모듈의 심볼과 섹션 정보는 /sys/module/ 디렉토리에서 확인할 수 있습니다. 특히 모듈의 .text 섹션 시작 주소는 Oops 분석 시 모듈 내 오프셋을 절대 주소로 변환하는 데 필수적입니다.
# 로드된 모듈 목록
ls /sys/module/
# 특정 모듈의 섹션 주소 확인
cat /sys/module/e1000e/sections/.text
# 0xffffffffc0820000
# 모듈의 모든 섹션 주소
for sec in /sys/module/e1000e/sections/.*; do
echo "$(basename $sec): $(cat $sec)"
done
# .text: 0xffffffffc0820000
# .data: 0xffffffffc0835000
# .bss: 0xffffffffc0838000
# .rodata: 0xffffffffc0830000
# 모듈 파라미터와 메타데이터
cat /sys/module/e1000e/version
cat /sys/module/e1000e/srcversion
cat /sys/module/e1000e/refcnt # 참조 카운트
# GDB에서 모듈 심볼 로딩
# 모듈의 .text 주소를 GDB에 전달
TEXT_ADDR=$(cat /sys/module/my_module/sections/.text)
gdb vmlinux
(gdb) add-symbol-file my_module.ko $TEXT_ADDR
# 이제 모듈 내 함수의 소스 디버깅 가능
모듈 로딩 이벤트 모니터링
커널 tracepoint를 사용하면 모듈 로딩/언로딩 이벤트를 실시간으로 모니터링할 수 있습니다. 이는 시스템에서 어떤 모듈이 언제 로드되는지 추적하는 보안 감사에도 유용합니다.
# module tracepoint 활성화
echo 1 > /sys/kernel/debug/tracing/events/module/enable
# 모듈 로딩/언로딩 이벤트 실시간 모니터링
cat /sys/kernel/debug/tracing/trace_pipe &
# 모듈 로딩 시도
modprobe e1000e
# trace_pipe 출력:
# modprobe-1234 [003] module_load: e1000e
# modprobe-1234 [003] module_put: e1000e refcnt=1
# 모듈 언로딩
rmmod e1000e
# modprobe-1235 [001] module_free: e1000e
# 비정상 모듈 로딩 시도 감지 (보안 감사)
# auditd와 연동하여 로깅 가능
auditctl -w /sbin/insmod -p x
auditctl -w /sbin/modprobe -p x
동적 심볼 수 통계
시스템의 심볼 관련 통계를 수집하면, 심볼 테이블 크기 최적화나 export 정책 감사에 유용합니다.
# 현재 시스템의 심볼 통계 종합
echo "=== 심볼 통계 ==="
echo "전체 kallsyms 심볼: $(wc -l < /proc/kallsyms)"
echo "함수 심볼 (T/t): $(grep -c ' [Tt] ' /proc/kallsyms)"
echo "데이터 심볼 (D/d): $(grep -c ' [Dd] ' /proc/kallsyms)"
echo "BSS 심볼 (B/b): $(grep -c ' [Bb] ' /proc/kallsyms)"
echo "모듈 심볼: $(grep -c '\[' /proc/kallsyms)"
echo "로드된 모듈 수: $(lsmod | tail -n +2 | wc -l)"
# 모듈별 심볼 수 상위 10개
echo ""
echo "=== 모듈별 심볼 수 TOP 10 ==="
awk '/\[/ {gsub(/\[|\]/,"",$4); print $4}' /proc/kallsyms | \
sort | uniq -c | sort -rn | head -10
# exported 심볼 수 (Module.symvers 기준)
if [ -f /usr/src/linux-$(uname -r)/Module.symvers ]; then
echo ""
echo "=== Export 정책 분포 ==="
awk '{print $4}' /usr/src/linux-$(uname -r)/Module.symvers | \
sort | uniq -c | sort -rn
fi
/proc/kallsyms의 심볼 수와 로드된 모듈 수를 확인하세요. 심볼 수가 50만 개를 초과하거나 모듈이 300개 이상 로드된 시스템에서는 find_symbol()의 선형 탐색이 병목(Bottleneck)이 될 수 있습니다. 이 경우 불필요한 모듈을 언로드하거나, CONFIG_TRIM_UNUSED_KSYMS를 검토하세요.
커널 심볼 내부 구조 요약 다이어그램
지금까지 다룬 커널 심볼의 모든 구성 요소를 하나의 종합 다이어그램으로 정리합니다. 이 다이어그램은 소스 코드에서 시작하여 빌드, 링크, 런타임 해석, 디버깅까지의 전체 흐름을 보여줍니다.
자주 묻는 질문 (FAQ)
커널 심볼과 관련하여 개발자와 시스템 관리자가 자주 겪는 문제와 그 해답을 정리합니다.
| 질문 | 핵심 답변 | 상세 참조 섹션 |
|---|---|---|
nm vmlinux에 보이는 함수를 모듈에서 왜 못 쓰나요? | ELF 심볼이 있다고 export된 것이 아닙니다. EXPORT_SYMBOL*이 붙어야 __ksymtab에 등록되어 모듈에서 사용 가능합니다. | 심볼 가시성 제어 |
/proc/kallsyms 주소가 전부 0으로 보여요 | kptr_restrict가 1 또는 2로 설정되어 있습니다. root로 확인하거나 echo 0 > /proc/sys/kernel/kptr_restrict로 임시 해제하세요. | 커널 심볼 보안 |
| CRC mismatch로 모듈이 로드 안 됩니다 | 모듈을 현재 실행 중인 커널의 소스/헤더로 재빌드하세요. CONFIG 차이도 CRC를 바꿉니다. | CRC 버저닝 메커니즘 |
System.map과 /proc/kallsyms 주소가 달라요 | KASLR이 활성화되어 있습니다. 두 주소의 차이가 KASLR 오프셋입니다. | KASLR과 심볼 해석 |
| namespace 경고가 나오지만 모듈이 로드됩니다 | 현재 기본 설정에서는 경고만 출력됩니다. CONFIG_MODULE_ALLOW_MISSING_NAMESPACE_IMPORTS=n으로 설정하면 로딩을 거부합니다. | EXPORT_SYMBOL_NS 네임스페이스 |
insmod는 실패하는데 modprobe는 성공해요 | modprobe는 modules.dep을 참조하여 의존 모듈을 자동으로 먼저 로딩합니다. insmod는 의존성을 자동 해결하지 않습니다. | 모듈 심볼 의존성과 depmod |
| 어떤 심볼을 EXPORT_SYMBOL_GPL로 해야 하나요? | 커널 내부 구현에 강하게 결합된 API, 커널 구조체의 내부 필드에 접근하는 API, 보안 관련 API는 GPL로 제한합니다. | 실무 원칙 |
kallsyms_lookup_name()을 모듈에서 쓸 수 없어요 | 커널 5.7에서 export가 제거되었습니다. BPF kfunc이나 kprobes 같은 공식 인터페이스를 사용하세요. | kallsyms_lookup_name() 접근 제한 |
Oops에서 [my_module]만 보이고 소스 위치를 모르겠어요 | scripts/faddr2line my_module.ko "func+offset"로 소스 위치를 확인하세요. KASLR 영향 없이 정확합니다. | 실전 심볼 분석 워크플로 |
TRIM_UNUSED_KSYMS를 켰더니 외부 모듈이 안 돼요 | TRIM은 트리 내 모듈만 고려합니다. 외부 모듈이 필요한 심볼은 화이트리스트에 등록하거나 TRIM을 비활성화하세요. | CONFIG_TRIM_UNUSED_KSYMS |
실전 체크리스트: 모듈 심볼 문제 진단 순서
- 증상 분류: 빌드 실패(modpost 오류)인가, 로딩 실패(
insmod/modprobe오류)인가, 런타임 크래시(Oops)인가? - 빌드 문제:
make V=1로 빌드 → modpost 경고/오류 메시지 확인 →Module.symvers존재 여부 확인 - 로딩 문제:
dmesg | tail -20으로 커널 로그 확인 →nm -u module.ko로 미해결 심볼 확인 →grep symbol /proc/kallsyms로 커널에 심볼 존재 확인 - CRC 문제:
modprobe --dump-modversions module.ko와Module.symversCRC 비교 - GPL 문제:
modinfo module.ko로 라이선스 확인 → 공급자 export 타입 확인 - NS 문제:
MODULE_IMPORT_NS()선언 확인 →make nsdeps로 자동 추가 - 런타임 문제:
scripts/faddr2line vmlinux "func+offset"로 소스 위치 확인 →objdump -dS로 명령어 수준 분석
# 종합 진단 스크립트 — 모듈 심볼 문제 자동 점검
MODULE=$1
if [ -z "$MODULE" ]; then
echo "Usage: $0 module.ko"; exit 1
fi
echo "=== 모듈 기본 정보 ==="
modinfo "$MODULE" 2>/dev/null | grep -E "^(filename|license|vermagic|depends|sig)"
echo ""
echo "=== 미해결 심볼 ==="
nm -u "$MODULE" 2>/dev/null | head -20
UNDEF_COUNT=$(nm -u "$MODULE" 2>/dev/null | wc -l)
echo "(총 ${UNDEF_COUNT}개 undefined 심볼)"
echo ""
echo "=== Export하는 심볼 ==="
nm "$MODULE" 2>/dev/null | grep __ksymtab_ | sed 's/.*__ksymtab_//'
echo ""
echo "=== CRC 기대값 ==="
modprobe --dump-modversions "$MODULE" 2>/dev/null | head -10
echo ""
echo "=== 커널 내 심볼 존재 확인 ==="
for sym in $(nm -u "$MODULE" 2>/dev/null | awk '{print $2}' | head -5); do
result=$(grep -c " $sym$" /proc/kallsyms)
echo " $sym: ${result}개 발견"
done
외부 모듈 개발 모범 사례
외부(out-of-tree) 모듈을 개발하고 유지보수할 때 심볼 관련 문제를 최소화하기 위한 모범 사례입니다.
| 사례 | 권장 방법 | 피해야 할 것 |
|---|---|---|
| 커널 헤더 참조 | 현재 실행 커널의 /lib/modules/$(uname -r)/build 사용 | 임의 경로의 커널 소스 사용 |
| Module.symvers 동기화 | 빌드 시 커널 트리의 Module.symvers 참조 | Module.symvers 없이 빌드 |
| GPL 심볼 사용 | MODULE_LICENSE("GPL") 선언 후 사용 | 라이선스 우회 시도 |
| 내부 심볼 접근 | 공식 export API만 사용, 필요 시 패치 제출 | kallsyms_lookup_name() 우회 패턴 |
| 다중 커널 지원 | KERNEL_VERSION() 매크로로 조건부 컴파일 | 특정 버전만 가정한 코드 |
| 네임스페이스 | MODULE_IMPORT_NS() 명시적 선언 | 경고 무시, 미선언 |
| DKMS 통합 | dkms.conf에 빌드 의존성 명시 | 수동 빌드/설치에만 의존 |
| 심볼 충돌 방지 | 모듈 고유 접두사 사용 (예: mydrv_) | 일반적인 이름 (init, read 등) |
/* 다중 커널 버전 호환 코드 예시 */
#include <linux/version.h>
#include <linux/module.h>
/* 커널 6.4에서 MODULE_IMPORT_NS 문법이 변경됨 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,4,0)
MODULE_IMPORT_NS(MY_SUBSYS);
#else
MODULE_IMPORT_NS(MY_SUBSYS);
#endif
/* 특정 API가 커널 버전에 따라 다를 때 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,2,0)
/* 새로운 API 사용 */
static int my_register(void) { return new_api_register(); }
#else
/* 이전 API 사용 */
static int my_register(void) { return old_api_register(); }
#endif
MODULE_LICENSE("GPL");
# 외부 모듈 빌드 표준 패턴
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
# DKMS 등록 (커널 업데이트 시 자동 재빌드)
sudo dkms add ./
sudo dkms build -m my_module -v 1.0
sudo dkms install -m my_module -v 1.0
# 다중 커널 버전 호환 빌드 테스트
for kver in /lib/modules/*/build; do
echo "Testing: $(basename $(dirname $kver))"
make -C "$kver" M=$(pwd) modules 2>&1 | tail -3
make -C "$kver" M=$(pwd) clean >/dev/null 2>&1
done
관련 문서와 커널 소스 길잡이
이 문서는 커널 심볼을 중심축으로 묶은 문서입니다. 세부 축은 아래 문서와 소스 파일로 이어서 들어가면 됩니다.
- 커널 모듈 — 모듈 작성, 로딩, 의존성, export 실습 맥락
- 빌드 시스템 — Kbuild, modpost, Module.symvers 생성 흐름
- ELF — 심볼 테이블, 재배치,
__ksymtab, 모듈 ELF 구조 - 디버깅 & 트러블슈팅 — Oops 해석, printk, 관측 도구
- GDB 완전 가이드 —
vmlinux,vmcore, 모듈 심볼과의 연결 - 개발 도구 —
nm,readelf,objdump, GDB와의 연결 - 커널 필수 함수·매크로·심볼 레퍼런스 — export 관련 API를 빠르게 찾는 레퍼런스형 문서
- 동기화 기법 — export된 심볼의 동시 접근 보호 설계
- 프로세스(Process) 관리 — 모듈 로딩 컨텍스트와 심볼 해석 시점
- Linked List — 모듈 목록 순회 시 사용되는 리스트 구조
- 보안 모듈 (LSM) — GPL 심볼 정책과 보안 프레임워크의 관계
include/linux/export.h— export 매크로 정의와struct kernel_symbolkernel/module/main.c— 모듈 로딩과 심볼 해석 핵심 경로 (find_symbol,simplify_symbols)kernel/kallsyms.c— kallsyms 생성/조회 로직 (sprint_symbol,kallsyms_lookup_name)scripts/kallsyms.c— 빌드 시 심볼 압축 및 배열 생성 도구scripts/mod/modpost.c— 빌드 후처리와 심볼 검증include/linux/module.h— 모듈 메타데이터, 참조 카운트 관련 선언include/linux/livepatch.h—klp_func,klp_object구조체와 심볼 지정 방식
외부 참고 자료
- 커널 공식 문서 — Symbol Namespaces —
EXPORT_SYMBOL_NS와MODULE_IMPORT_NS의 공식 사용법을 설명합니다. - 커널 공식 문서 — Building External Modules — 외부 모듈 빌드 시
Module.symvers처리와 심볼 의존성 해결 방법을 다룹니다. - 커널 공식 문서 — Kernel Makefiles — Kbuild 시스템에서
EXPORT_SYMBOL과 modpost가 동작하는 빌드 흐름을 설명합니다. - LWN — Rethinking EXPORT_SYMBOL_GPL (2016) — GPL 전용 심볼 내보내기 정책의 논쟁과 커뮤니티 합의 과정을 다룹니다.
- LWN — Symbol namespaces for the kernel (2018) — 심볼 네임스페이스 도입 배경과 설계 목표를 설명합니다.
- LWN — Removing EXPORT_SYMBOL_GPL_FUTURE (2020) — 더 이상 사용되지 않는 심볼 내보내기 매크로의 정리 과정을 다룹니다.
- LWN — Limiting kernel-symbol access (2021) —
TRIM_UNUSED_KSYMS와 심볼 노출 최소화 전략을 설명합니다. - LWN — Kernel address-space layout randomization (KASLR) — KASLR이 심볼 주소 은닉과
/proc/kallsyms접근 정책에 미치는 영향을 다룹니다. - 커널 소스 — include/linux/export.h —
EXPORT_SYMBOL,EXPORT_SYMBOL_GPL,EXPORT_SYMBOL_NS매크로의 최신 구현입니다. - 커널 소스 — kernel/module/main.c — 모듈 로딩 시 심볼 해석(
resolve_symbol,simplify_symbols)의 핵심 코드입니다. - 커널 소스 — kernel/kallsyms.c —
kallsyms_lookup_name,sprint_symbol등 런타임 심볼 조회 구현입니다. - 커널 소스 — scripts/mod/modpost.c — 빌드 후처리 단계에서 심볼 CRC 검증과
Module.symvers생성을 담당하는 도구입니다. - 커널 소스 — scripts/kallsyms.c — 빌드 시
vmlinux의 심볼을 압축·정렬하여 커널 이미지에 내장하는 도구입니다. - 커널 공식 문서 — The Linux Kernel Driver Interface — 커널 내부 심볼이 안정 API가 아닌 이유와 외부 모듈 유지보수 시 유의사항을 설명합니다.