커널 심볼 (Kernel Symbols)

커널 심볼은 단순한 함수 이름 목록이 아니라, 소스 선언, ELF 심볼 테이블, export 정책, modpost 수집, Module.symvers, 런타임 재배치, kallsyms 관측, GPL 및 네임스페이스 제약, CRC 기반 호환성 검사를 하나로 묶는 ABI 계약입니다. 이 문서는 커널 심볼을 "정의되는 이름"이 아니라 "외부 모듈과 디버거, 운영 도구가 의존하는 계약"으로 보고, 정의부터 문제 해결까지 한 흐름으로 정리합니다.

전제 조건: 커널 모듈, ELF, 빌드 시스템 문서를 먼저 읽으세요. 특히 재배치, 섹션, 모듈 로딩, modpost의 역할을 모르면 심볼이 "왜 빌드 단계와 런타임 단계에서 따로 검증되는지"를 이해하기 어렵습니다.
일상 비유: 이 개념은 공항 출입 통제와 화물 추적 번호와 비슷합니다. 짐에 이름표만 붙는다고 바로 탑승하는 것이 아니라, 출발지 등록, 보안 검사, 화물 목록 반영, 도착지 대조까지 모두 통과해야 실제로 운송되듯이, 커널 심볼도 선언만으로 끝나지 않고 export, 수집, 검증, 로드, 관측 단계가 모두 이어집니다.

핵심 요약

  • 심볼 — 함수, 전역 변수, 어셈블리 라벨, 링커가 만든 주소 이름까지 포함하는 "이름 붙은 엔티티"입니다.
  • export — 다른 모듈이 참조할 수 있도록 커널이 ABI 계약을 공개하는 행위입니다.
  • modpost — 빌드 마지막에 exported symbol과 undefined symbol을 대조하고 Module.symvers를 생성하는 후처리 단계입니다.
  • kallsyms — 실행 중 커널이 유지하는 심볼 이름 사전으로, Oops 해석과 디버깅에 직접 쓰입니다.
  • CRC/namespace/GPL — 심볼이 존재하더라도 버전 CRC, 네임스페이스 import, GPL 제약을 통과해야 모듈이 로드됩니다.

단계별 이해

  1. 정의
    C 함수, 전역 변수, 어셈블리 라벨, 링커 스크립트 심볼이 ELF 심볼로 먼저 만들어집니다.
  2. 공개 여부 결정
    내부 심볼은 그대로 숨기고, 외부 모듈이 반드시 써야 하는 것만 EXPORT_SYMBOL*로 공개합니다.
  3. 빌드 후처리
    modpost가 undefined symbol과 exported symbol을 연결하고 Module.symvers.mod.c를 만듭니다.
  4. 런타임 해석
    모듈 로더가 재배치 엔트리를 따라 심볼 주소를 채우고, CRC, GPL, namespace 조건을 검증합니다.
  5. 운영/디버깅
    /proc/kallsyms, System.map, nm, readelf, %pS로 심볼을 해석하고 장애를 추적합니다.
문서 범위: 이 문서는 Kconfig 심볼이 아니라, 코드/링커/모듈/디버깅 관점의 커널 심볼을 다룹니다. Kconfig 항목이 필요하면 빌드 시스템, 심볼을 사용하는 API 관점이 필요하면 커널 필수 함수·매크로·심볼 레퍼런스, 모듈 작성 관점이 필요하면 커널 모듈 문서를 함께 보세요.

개요: 커널 심볼을 무엇으로 봐야 하는가

커널 문맥에서 "심볼"이라는 단어는 적어도 네 가지 층위를 가집니다. 첫째, C/어셈블리 소스에 등장하는 이름입니다. 둘째, 오브젝트 파일과 vmlinux/.ko 안에 기록되는 ELF 심볼입니다. 셋째, EXPORT_SYMBOL*로 외부 모듈에 공개된 모듈 ABI 심볼입니다. 넷째, 실행 중 주소를 이름으로 되돌리기 위해 kallsyms가 유지하는 런타임 심볼 사전입니다. 실전에서는 이 네 층위가 섞여 보이기 때문에, 어느 단계에서 생긴 이름인지 먼저 구분해야 합니다.

즉, static 함수도 컴파일러와 링커 관점에서는 심볼일 수 있고, EXPORT_SYMBOL()이 붙지 않은 전역 함수도 ELF 심볼일 수 있습니다. 그러나 모든 ELF 심볼이 곧 모듈이 참조 가능한 커널 API는 아닙니다. 반대로, 디버거가 이름을 복원할 수 있다고 해서 그 심볼이 외부 모듈에서 링크 가능한 것도 아닙니다. 이 차이를 놓치면 "nm에는 보이는데 왜 모듈이 못 쓰지?" 같은 혼란이 바로 생깁니다.

소스 심볼 함수, 변수, 라벨 ELF 심볼 .symtab / .strtab exported 심볼 __ksymtab* 런타임 심볼 사전 kallsyms / System.map 같은 이름이라도 단계별 역할이 다르다 소스 이름 → ELF 이름 → 외부 공개 여부 → 실행 중 주소 역변환 모듈이 쓸 수 있는가, 디버거가 볼 수 있는가, ABI로 유지되는가는 별개 판단이다
커널 심볼은 "이름" 하나가 아니라, 소스/ELF/export/kallsyms 네 단계에서 서로 다른 역할을 갖는 계약입니다.
용어무엇을 가리키는가대표 확인 도구실수 포인트
소스 심볼C 함수, 전역 변수, 어셈블리 라벨소스 코드, ctags, rg정의된 이름이면 모두 export 가능한 것으로 오해
ELF 심볼오브젝트 파일에 기록된 이름/주소/섹션 정보readelf -Ws, nmELF에 보이면 모듈도 쓸 수 있다고 오해
exported 심볼다른 모듈이 링크할 수 있도록 공개된 심볼__ksymtab*, Module.symvers최소 공개 원칙 없이 내부 구현까지 공개
런타임 심볼주소를 이름으로 역해석하기 위한 실행 중 심볼 사전/proc/kallsyms, System.map보인다고 해서 로딩 가능한 ABI라고 오해
Kconfig 심볼CONFIG_FOO 같은 설정 이름.config, menuconfig코드 심볼과 같은 의미로 혼동

심볼 수명주기: 정의에서 Oops 해석까지

커널 심볼의 수명은 소스 정의에서 끝나지 않습니다. 개발자가 함수를 정의하면 컴파일러가 ELF 심볼로 기록하고, 링크 과정이 최종 배치를 결정하며, export 매크로가 붙은 경우에는 별도 테이블에 등록됩니다. 그 뒤 modpost가 심볼 사용 관계를 수집하고, 모듈 로더는 로딩 시 재배치를 통해 실제 주소를 채웁니다. 마지막으로 Oops, ftrace, GDB, %pS 같은 도구가 이 이름을 다시 역으로 사용합니다.

중요한 점은 이 흐름이 순방향과 역방향 모두 존재한다는 것입니다. 순방향으로는 "이름이 주소를 얻는 과정"이고, 역방향으로는 "주소가 다시 이름으로 보이는 과정"입니다. 전자는 모듈 링크와 로딩의 문제이고, 후자는 디버깅과 운영 관측의 문제입니다. Kernel Symbol을 제대로 이해한다는 것은 이 둘을 같이 보는 것입니다.

소스 정의 C / ASM 컴파일 ELF 심볼 생성 export 표기 __ksymtab* modpost 관계 수집 모듈 로더 재배치 / 검증 실행 주소 보유 역방향 해석 경로 Oops 주소 / 함수 포인터 / perf 샘플 / GDB 백트레이스 → kallsyms / System.map / vmlinux 심볼 / DWARF를 통해 이름과 오프셋 복원
정방향은 이름이 주소를 얻는 과정이고, 역방향은 주소가 다시 이름으로 복원되는 과정입니다.
읽는 법: "왜 이 모듈이 로드되지 않지?"는 정방향 흐름에서, "이 Oops가 어느 함수지?"는 역방향 흐름에서 문제를 추적하면 됩니다.

어떤 심볼이 실제로 존재하는가

커널 개발에서는 흔히 함수 심볼만 떠올리지만, 실제로는 변수 심볼, 어셈블리 진입점, 링커가 만드는 경계 심볼, 모듈이 아직 해결하지 못한 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 도입이 강조되는 이유도, 심볼 수 자체를 줄이면서 의존성 경계를 명확히 하기 위해서입니다.

외부 모듈에 공개할 필요가 있는가? 아니오 static 유지 내부 함수/변수로 숨김 예, GPL 제약이 있는가? 내부 API 성격인지 판단 특정 서브시스템 전용인가? namespace 필요 여부 판단 비공개 유지 가장 유지비가 낮음 EXPORT_SYMBOL 일반 공개 EXPORT_SYMBOL_GPL GPL 전용 공개 NS + import 경계 강화
export 정책은 "공개 여부", "GPL 여부", "namespace 여부"를 순서대로 결정하는 문제입니다.
매크로의미언제 쓰는가대표 실수
EXPORT_SYMBOL일반 공개외부 모듈이 넓게 사용해야 하는 안정 API내부 구현 상세까지 무분별하게 공개
EXPORT_SYMBOL_GPLGPL 모듈에만 공개내부 성격이 강하거나 유지정책을 강하게 통제할 때MODULE_LICENSE 제약을 빼먹고 사용
EXPORT_SYMBOL_NSnamespace 포함 일반 공개서브시스템 경계를 명시하고 싶을 때소비 모듈의 MODULE_IMPORT_NS 누락
EXPORT_SYMBOL_NS_GPLGPL + namespace내부 서브시스템 공용 APInamespace 이름이 자주 바뀌는 경우 유지비 증가
MODULE_IMPORT_NSnamespace 사용 선언namespaced export를 참조하는 모듈오타나 조건부 컴파일로 import가 빠짐
symbol_get선택적 심볼 획득기능이 있으면 쓰고 없으면 우회하는 선택적 연동symbol_put 누락으로 참조 카운트 불균형
설계 원칙: 외부 모듈 하나 때문에 내부 헬퍼 5개를 export하는 대신, 그 모듈이 진짜 필요한 동작만 감싼 얇은 wrapper 1개를 export하는 편이 장기적으로 훨씬 안전합니다.

빌드 단계: __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는 "나중에 로딩해 보면 알겠지"를 막아 주는 첫 번째 방어선입니다.

foo.c / bar.c 정의 + EXPORT_SYMBOL* foo.o / bar.o ELF 심볼 + relocation foo.ko.tmp partial link 결과 modpost 심볼/CRC/namespace 검사 산출물 1: Module.symvers exported symbol + CRC + type 산출물 2: .mod.c 메타데이터 + versions 산출물 3: 경고/오류 Unknown symbol 후보, GPL/NS 검사 핵심: 빌드 성공은 컴파일 성공이 아니라 modpost 성공까지 포함한다 커널 심볼 문제의 상당수는 런타임 전에 modpost가 미리 알려준다
커널 심볼 메타데이터는 export 매크로가 자동으로 만들어 주는 것이 아니라, modpost가 수집하고 정리한 결과입니다.
구성 요소역할무엇을 보면 되는가문제 징후
__ksymtab일반 export 심볼 목록readelf -S, objdump -hexport가 기대와 다르게 누락
__ksymtab_gplGPL 전용 export 목록readelf -S비GPL 모듈 로딩 실패
__kcrctab*심볼 CRC 저장readelf -S, Module.symvers버전 불일치 경고
.mod.c자동 생성 모듈 메타데이터빌드 디렉터리의 *.mod.cversions 배열, vermagic 확인 필요
Module.symversexported 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
관련 문서: modpost 내부 구조와 빌드 파이프라인 자체는 빌드 시스템 — Makefile.modpost가 더 자세하고, __ksymtab 섹션과 ELF 관점은 ELF 문서가 이어서 설명합니다.

런타임 단계: 모듈 로더와 심볼 해석

빌드가 끝나도 심볼 문제는 아직 끝나지 않습니다. 모듈 파일은 여전히 재배치 정보와 undefined symbol 참조를 포함하고 있기 때문에, 실제 로딩 시 커널이 현재 실행 중인 커널 이미지와 이미 로드된 모듈들의 export 테이블을 기준으로 주소를 채워 넣어야 합니다. 이때 심볼 이름이 존재하는지뿐 아니라, 라이선스 제약, namespace 선언, CRC 일치 여부, 아키텍처별 재배치 타입까지 함께 검증합니다.

따라서 "빌드되었다"와 "로드된다"는 전혀 다른 단계입니다. 빌드 시점에는 헤더와 심볼 이름이 맞았더라도, 런타임 커널이 다른 설정이나 다른 CRC를 가질 수 있고, GPL/namespace 정책이 달라질 수도 있습니다. DKMS나 외부 모듈에서 이런 문제가 특히 자주 나타납니다.

insmod / modprobe .ko 입력 모듈 ELF 파싱 섹션/심볼/재배치 확인 심볼 검색 vmlinux + loaded modules 검증 GPL / NS / CRC / reloc 성공 재배치 완료, 함수 포인터/변수 주소 확정 실패 Unknown symbol / version mismatch / GPL 오류 같은 이름이 있어도 조건을 모두 통과해야 최종 주소가 채워진다 존재성, 접근 권한, 호환성, 재배치 가능성이 모두 필요하다
모듈 로더는 이름만 찾는 것이 아니라, 그 이름을 현재 커널이 허용하는 방식으로 참조할 수 있는지까지 확인합니다.
# 모듈이 어떤 외부 심볼을 참조하는지 먼저 본다
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 영향이 있고, 후자는 설치된 커널 이미지와 정확히 맞을 때 강력합니다. 모듈 자체를 분석할 때는 nmreadelf가 더 직접적입니다.

알고 싶은 것: 주소 ↔ 이름 ↔ 섹션 ↔ 모듈 /proc/kallsyms 실행 중 커널 심볼 System.map 설치된 커널 맵 nm / readelf 모듈/ELF 구조 분석 GDB / objdump 소스/어셈블리 대응 도구 선택 기준 런타임 주소면 kallsyms, 정적 이미지면 System.map, .ko 구조면 nm/readelf, 코드 위치면 GDB/objdump
같은 심볼 문제라도 "런타임 주소를 보고 있는지" 또는 ".ko 구조를 보고 있는지"에 따라 도구 선택이 달라집니다.
도구주 용도강점제약
/proc/kallsyms실행 중 주소를 이름으로 확인현재 커널 상태 반영kptr_restrict, 권한 제약, KASLR 영향
System.map정적 커널 이미지 주소 확인Oops 해석의 기준점실행 중 커널 이미지와 정확히 맞아야 함
nm심볼 종류와 정의/미정의 확인간단하고 빠름재배치 세부 정보는 부족
readelf -Ws/-r심볼 테이블과 재배치 확인ELF 구조를 정밀하게 볼 수 있음출력이 길고 해석 비용이 큼
objdump -dr기계어와 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);
운영 팁: Oops 주소 해석은 addr2line보다 먼저 %pS, /proc/kallsyms, System.map 정합성을 확인하는 편이 빠릅니다. 특히 KASLR이 켜져 있으면 정적 주소와 런타임 주소를 그대로 비교하면 안 됩니다.

CONFIG_MODVERSIONS와 CRC: 왜 이름이 같아도 거부되는가

CONFIG_MODVERSIONS가 켜져 있으면 커널은 exported symbol마다 시그니처 기반 CRC를 관리합니다. 이 CRC는 단순히 이름이 같은지 보는 것이 아니라, 해당 심볼의 인터페이스가 "같은 계약"인지 확인하기 위한 요약값입니다. 그래서 이름이 그대로여도 인자 타입, 구조체 선언, 일부 전처리 결과가 달라지면 CRC가 바뀌어 모듈 로딩이 거부될 수 있습니다.

이 메커니즘은 안정 ABI를 보장하는 장치가 아닙니다. 오히려 "인터페이스가 바뀌었는데 오래된 모듈이 모르고 로딩되는 것"을 막기 위한 장치입니다. 따라서 외부 모듈 유지보수 관점에서는 CRC mismatch를 해결하기 위해 억지로 강제 로드하는 것이 아니라, 동일한 헤더와 설정으로 재빌드하는 것이 정석입니다.

항목의미어디서 확인실수
심볼 CRCexported symbol 인터페이스 요약값Module.symvers, __versions이름만 같으면 호환된다고 생각
__versions모듈이 기대하는 외부 심볼 CRC 목록readelf -S, *.mod.c모듈 안의 CRC와 커널 쪽 CRC를 따로 보지 않음
genksyms심볼 서명을 계산하는 빌드 도구빌드 로그, Kbuild 내부헤더 차이를 가볍게 봄
vermagic커널/모듈 빌드 속성 문자열modinfoCRC 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
복구 원칙: CRC mismatch는 우회 옵션을 찾을 문제가 아니라, 모듈을 현재 커널 트리와 같은 헤더/설정으로 다시 빌드해야 하는 문제입니다.

보안과 정책: 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;
}
정책 해석 원칙: "왜 못 쓰게 막아 놨지?"보다 "이 심볼이 어떤 경계를 지키기 위해 공개 범위를 제한하는가"를 먼저 보세요. 대부분의 경우 그 제한은 우연이 아니라 유지보수 비용과 보안 비용을 반영한 설계입니다.

문제 해결: Unknown symbol에서 CRC mismatch까지

커널 심볼 문제는 대부분 증상은 짧고 원인은 길게 퍼져 있습니다. 빌드는 되었지만 로드가 안 되거나, 주소는 보이는데 함수명이 다르거나, namespace 경고가 나오거나, DKMS 모듈이 업데이트 뒤 갑자기 깨지는 식입니다. 이럴 때는 "어느 단계에서 계약이 끊어졌는가"를 기준으로 조사해야 합니다.

가장 실전적인 순서는 다음과 같습니다. 먼저 빌드 로그에서 modpost 경고를 확인하고, 그다음 nm -ureadelf -r로 미해결 참조를 보고, 이어서 /proc/kallsymsModule.symvers에서 공급자 쪽 상태를 확인합니다. 마지막으로 라이선스, namespace, CRC, vermagic 순서로 좁혀가면 됩니다.

증상가장 흔한 원인먼저 볼 것수정 방향
Unknown symbol fooexport 안 됨, 공급 모듈 미로드, 이름 오타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 symbolCRC mismatchModule.symvers, __versions, modinfo현재 커널과 동일 조건으로 재빌드
Invalid module formatvermagic, CRC, 아키텍처, 서명 문제modinfo, dmesg커널/모듈 빌드 환경 맞추기
/proc/kallsyms 주소가 0kptr_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하지 말아야 하는가

Kernel Symbol 문서의 마지막은 "어떻게 쓰는가"보다 "언제 공개하지 않을 것인가"로 끝나야 합니다. export는 재사용성의 출발이 아니라 유지보수 의무의 시작이기 때문입니다. 심볼 하나를 공개하면 다른 모듈이 의존하고, 그 의존은 코드 검색만으로 끝나지 않고 외부 트리, DKMS, 배포판 패키지, 운영 스크립트까지 퍼질 수 있습니다.

따라서 export 전 체크리스트는 기능성보다 경계성을 봐야 합니다. 단순 헬퍼, 락이 노출된 내부 구조, 수명주기가 불안정한 객체 접근자, 디버깅 전용 임시 함수, 직접 필드 접근을 강제하는 구조체 API는 원칙적으로 export 후보가 아닙니다. 대신 wrapper, opaque handle, namespace, 문서화된 수명 규약을 함께 설계해야 합니다.

상황권장 선택이유
한 모듈만 쓰는 내부 헬퍼static 유지ABI 표면을 늘릴 이유가 없음
여러 함수 조합이 필요한 복잡한 내부 절차wrapper 1개만 export내부 순서/락/상태 전이를 숨길 수 있음
서브시스템 내부 공용 APIEXPORT_SYMBOL_NS_GPL경계와 라이선스 정책을 같이 명시 가능
디버깅 전용 함수export 대신 tracepoint/debugfs 검토디버그 훅을 ABI로 굳히지 않음
옵션 기능 연동symbol_get 또는 명시적 의존성강한 링크와 약한 링크를 구분 가능

관련 문서와 커널 소스 길잡이

이 문서는 커널 심볼을 중심축으로 묶은 문서입니다. 세부 축은 아래 문서와 소스 파일로 이어서 들어가면 됩니다.