GNU Binutils 완전 가이드
커널 및 모듈 분석에서 Binutils를 어떻게 조합해 쓰는지 실전 흐름으로 정리합니다. `readelf`/`objdump`로 ELF 구조와 디스어셈블 확인, `nm`/`addr2line`로 심볼 역추적, `objcopy`/`strip`으로 배포용 바이너리 가공, `ar`/`ranlib`로 정적 라이브러리 관리, `elfedit`로 헤더 조정까지 디버깅·성능 분석·릴리스 준비 단계에서 필요한 절차를 상세히 설명합니다.
핵심 요약
- ar — 오브젝트 파일을 묶어 정적 라이브러리(
.a)를 생성·관리합니다. - nm — 오브젝트/라이브러리 파일의 심볼 테이블을 출력합니다.
- objdump — 섹션 헤더, 역어셈블, 재배치 정보 등을 덤프합니다.
- readelf — BFD 라이브러리 독립적으로 ELF 구조를 정밀하게 표시합니다.
- strip/objcopy — 심볼/섹션을 제거하거나 포맷을 변환합니다.
단계별 이해
- 컴파일 결과 확인
nm으로 심볼 타입과 이름을 확인하고,size로 섹션 크기를 파악합니다. - 오브젝트 구조 분석
readelf -a또는objdump -x로 ELF 헤더·섹션·세그먼트를 전부 검토합니다. - 역어셈블 및 디버깅
objdump -dS로 소스와 어셈블리를 함께 보고,addr2line으로 주소를 파일명:라인으로 변환합니다. - 라이브러리 관리
ar로 정적 라이브러리를 생성하고ranlib으로 심볼 인덱스를 갱신합니다. - 릴리스 최적화
strip으로 디버그 심볼을 제거하고,objcopy로 원하는 섹션만 추출합니다.
개요 및 도구 목록
GNU Binutils(GNU Binary Utilities)는 오브젝트 파일과 실행 파일을 생성·분석·변환하는 명령행 도구 모음입니다. 버전 2.46(2026년 2월)을 기준으로 하며, 리눅스 커널 빌드, 크로스 컴파일, 펌웨어 개발, 역공학 분석 모두에 필수적입니다.
| 도구 | 주요 역할 | 대표 옵션 |
|---|---|---|
ar | 정적 라이브러리(.a) 생성·관리 | cr, t, x |
nm | 심볼 테이블 출력 | -C -D -u -S |
objcopy | 오브젝트 파일 변환·섹션 조작 | -O -R --add-section |
objdump | 역어셈블·섹션·재배치 정보 출력 | -d -S -h -r -t |
readelf | ELF 구조 상세 분석(BFD 독립) | -a -s -S -d -w |
strip | 심볼·섹션 제거로 파일 크기 축소 | -g -s -K -N |
addr2line | 주소 → 파일명:라인번호 변환 | -e -f -C -i |
c++filt | C++ 맹글링 심볼 디맹글링 | -p -t |
strings | 파일 내 출력 가능 문자열 추출 | -a -n -t -e |
size | 섹션(text/data/bss) 크기 출력 | -A -B -G |
ranlib | 아카이브 심볼 인덱스 생성 | -D -U |
elfedit | ELF 헤더 필드 직접 편집 | --output-osabi |
ld | GNU 링커 (별도 매뉴얼 참조) | 링커 스크립트, -T |
readelf만 BFD를 거치지 않고 ELF를 직접 파싱하므로 ELF 분석의 1차 도구로 권장됩니다.
ELF 파일 구조
Binutils 전체 도구가 다루는 핵심 파일 형식인 ELF(Executable and Linkable Format)의 내부 구조를 이해하면 각 도구의 동작 원리를 훨씬 명확히 파악할 수 있습니다. ELF 파일 형식의 심화 내용은 ELF 파일 형식 페이지를 참고하세요.
| ELF 구성 요소 | 확인 도구 | 설명 |
|---|---|---|
| ELF 헤더 | readelf -h, elfedit | 파일 클래스(32/64bit), 엔디안, ABI, 엔트리 포인트, 헤더 오프셋 |
| 프로그램 헤더 | readelf -l, objdump -p | 런타임 로드 세그먼트 (LOAD, DYNAMIC, INTERP, NOTE) |
| 섹션 헤더 | readelf -S, objdump -h | 섹션 이름, 타입, 플래그, 주소, 크기 |
| 심볼 테이블 | nm, readelf -s | .symtab(전체) / .dynsym(동적) 심볼 |
| 재배치 테이블 | readelf -r, objdump -r | 링크/로드 시 주소 패치 항목 |
| DWARF 디버그 | readelf -w, addr2line | .debug_info / .debug_line / .debug_frame 등 |
| 동적 섹션 | readelf -d | 공유 라이브러리 의존성, GOT/PLT 정보 |
도구 선택 매트릭스
작업 목적에 따라 최적의 Binutils 도구를 선택하는 기준표입니다.
| 목적 | 1차 도구 | 보조 도구 | 비고 |
|---|---|---|---|
| ELF 파일 구조 파악 | readelf -a | objdump -x | readelf가 더 정확 (BFD 미사용) |
| 역어셈블 / 소스 혼합 | objdump -dS | objdump --disassemble=함수명 | 소스 필요 시 디버그 심볼 필수 |
| 심볼 조회 | nm -C | readelf -s | nm이 간결, readelf가 상세 |
| 커널 패닉 주소 변환 | addr2line -fip | scripts/decode_stacktrace.sh | vmlinux에 디버그 심볼 필요 |
| 정적 라이브러리 관리 | ar crsD | ranlib -D | -D: 재현 가능 빌드 |
| 파일 크기 축소 | strip --strip-debug | objcopy -R .debug_* | 커널 모듈은 --strip-debug만 |
| 포맷 변환 | objcopy -O binary | objcopy -O ihex | 부트로더/펌웨어 이미지 생성 |
| C++ 심볼 해독 | c++filt | nm -C, addr2line -C | -C 옵션은 내부적으로 c++filt 사용 |
| 문자열 추출 | strings -a | readelf -p .rodata | 특정 섹션만 보려면 readelf |
| 메모리 풋프린트 분석 | size -A | nm -S --size-sort | 심볼별 크기는 nm -S |
| 공유 라이브러리 의존성 | readelf -d | nm -D | NEEDED 엔트리 확인 |
| 재배치 분석 | readelf -r | objdump -r | 커널 모듈 링크 문제 진단 |
| ELF 헤더 수정 | elfedit | objcopy | elfedit이 더 안전 (최소 변경) |
| DWARF 디버그 분석 | readelf -wi | objdump -W | pahole도 구조체 레이아웃에 유용 |
ar — 아카이브 관리
ar은 여러 오브젝트 파일을 하나의 아카이브(정적 라이브러리, .a)로 묶거나 추출합니다. 정적 라이브러리는 링크 시 링커가 필요한 오브젝트만 선택해 실행 파일에 포함시킵니다.
기본 문법
ar [-]키[플래그] [relpos] [count] 아카이브 [멤버…]
키(operation)는 반드시 하나를 지정합니다:
| 키 | 동작 |
|---|---|
d | 아카이브에서 멤버 삭제 |
m | 멤버 위치 변경 (a/b/i 플래그와 조합) |
p | 멤버 내용을 표준 출력으로 출력 |
q | 파일 끝에 빠르게 추가 (인덱스 재생성 없음) |
r | 아카이브에 파일 삽입(교체). 없으면 생성. |
s | 심볼 인덱스 생성/갱신 (ranlib과 동일) |
t | 멤버 목록 출력 |
x | 멤버 추출 |
플래그(modifier)는 여러 개를 조합할 수 있습니다:
| 플래그 | 의미 |
|---|---|
a | 기존 멤버 뒤에 삽입 (m/r 키와 사용) |
b / i | 기존 멤버 앞에 삽입 (m/r 키와 사용) |
c | 아카이브 생성 시 경고 없이 생성 |
D | 결정론적(deterministic) 모드 — 타임스탬프·uid·gid를 0으로 고정 |
f | 멤버명을 15글자로 잘라 구식 시스템 호환 |
l | (/tmp 대신) 현재 디렉토리에 임시 파일 생성 |
N | 이름이 같은 멤버가 여러 개일 때 count번째 항목 선택 |
o | 추출 시 원본 타임스탬프 유지 |
P | 멤버명 매칭에 전체 경로 사용 |
s | 심볼 인덱스 기록/갱신 |
S | 심볼 인덱스 기록 안 함 |
T | Thin 아카이브 생성 (파일 복사 없이 경로만 참조) |
u | 디스크 파일보다 오래된 멤버만 교체 |
U | 비결정론적 모드 (실제 타임스탬프 사용) |
v | 자세한 출력 (verbose) |
V | 버전 정보 출력 |
자주 쓰는 예제
# 정적 라이브러리 생성 (c: 경고 없이 생성, r: 삽입, s: 심볼 인덱스)
ar crs libfoo.a foo.o bar.o baz.o
# 멤버 목록 출력
ar t libfoo.a
# 특정 멤버 추출
ar x libfoo.a bar.o
# 결정론적 모드로 재생성 (빌드 재현성 보장)
ar crsD libfoo.a *.o
# Thin 아카이브 (커널 빌드에서 빌드 속도 향상 목적으로 사용)
ar crsT libkernel.a built-in.o
# 특정 멤버 삭제
ar d libfoo.a old.o
# 멤버 내용 보기 (표준 출력)
ar p libfoo.a foo.o | less
built-in.a를 생성하고, 최종적으로 최상위 링커가 이를 묶어 vmlinux를 만듭니다. CONFIG_THIN_ARCHIVES 옵션이 활성화되면 Thin 아카이브를 사용해 빌드 속도를 향상시킵니다.
nm — 심볼 테이블 분석
nm은 오브젝트 파일, 정적 라이브러리, 실행 파일의 심볼 테이블을 출력합니다. 링크 오류 디버깅, 심볼 충돌 분석, 커널 모듈 심볼 확인에 필수적입니다.
출력 형식
# 기본 출력: 값(value) 타입(type) 이름(name)
$ nm vmlinux | head
ffffffff81000000 T startup_64
ffffffff81000060 T secondary_startup_64
ffffffff81001000 T do_early_param
0000000000000000 A _text_offset
U printk
심볼 타입
| 타입 | 의미 | 설명 |
|---|---|---|
A | Absolute | 절대 주소 심볼. 링크 시 값이 변하지 않습니다. |
B / b | BSS | 초기화되지 않은 전역 변수(데이터). 대문자=전역, 소문자=로컬. |
C / c | Common | 초기화되지 않은 공통 심볼. --warn-common으로 경고 가능. |
D / d | Data | 초기화된 데이터 섹션 심볼. |
G / g | Small Data | 초기화된 소형 오브젝트 섹션 심볼. |
i | Indirect Function | GNU 간접 함수 심볼(ifunc). |
I | Indirect Reference | 다른 심볼에 대한 간접 참조. |
N | Debug | 디버깅 심볼. |
p | Stack Unwind | 스택 언와인드 섹션 심볼. |
R / r | Read-only Data | 읽기 전용 데이터 섹션 심볼. |
S / s | Small BSS | 초기화되지 않은 소형 오브젝트 섹션 심볼. |
T / t | Text | 코드(텍스트) 섹션 심볼. 함수 심볼이 여기에 속합니다. |
U | Undefined | 정의되지 않은 외부 심볼 (다른 오브젝트에서 제공 필요). |
u | Unique Global | ELF 고유 전역 심볼 (GNU 확장). |
V / v | Weak | 약한(weak) 심볼. 강한 심볼이 있으면 덮어쓰여집니다. |
W / w | Weak (unspecified) | 태그 없는 약한 심볼. |
- | Stabs Debug | a.out 포맷 디버그 심볼. |
? | Unknown | 알 수 없는 심볼 타입. |
주요 옵션
| 옵션 | 설명 |
|---|---|
-A / --print-file-name | 각 심볼 앞에 파일명 출력 (아카이브 분석 시 유용) |
-B | BSD 출력 포맷 |
-C / --demangle | C++ 심볼 디맹글링 (=style로 스타일 지정) |
--no-demangle | 디맹글링 비활성화 |
-D / --dynamic | 동적 심볼 출력 (공유 라이브러리 분석) |
-f / --format | 출력 포맷: bsd(기본), sysv, posix, just-symbols |
-g / --extern-only | 외부(전역) 심볼만 출력 |
-j / --print-section-name | 섹션 이름 출력 (--format=sysv와 동일) |
-l / --line-numbers | 디버깅 정보로 소스 파일명:라인번호 출력 |
-n / --numeric-sort | 주소 기준으로 정렬 |
-p / --no-sort | 정렬 안 함 (파일 순서 그대로) |
-r / --reverse-sort | 역순 정렬 |
-S / --print-size | 심볼 값과 크기 모두 출력 |
-s / --print-armap | 아카이브 심볼 인덱스 출력 |
-t / --radix | 주소 출력 기수: d(십진), o(8진), x(16진) |
-u / --undefined-only | 정의되지 않은 심볼만 출력 |
--with-symbol-versions | 심볼 버전 정보 출력 |
--special-syms | 컴파일러/링커 내부 심볼 포함 |
--synthetic | 합성 심볼 포함 |
# C++ 디맹글링 + 파일명 출력 + 주소 기준 정렬
nm -C -A -n vmlinux | grep "schedule"
# 정의되지 않은 심볼 확인 (링크 오류 원인 파악)
nm -u mymodule.ko
# 크기 포함 출력 (심볼 크기 확인)
nm -S --size-sort vmlinux | tail -20
# 동적 심볼 확인 (공유 라이브러리)
nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep "malloc"
# 아카이브 내 심볼 인덱스 출력
nm -s libfoo.a
# POSIX 포맷 출력
nm --format=posix vmlinux | head
System.map과 /proc/kallsyms
System.map은 커널 빌드 시 nm -n vmlinux로 생성되는 정적 심볼 주소 파일입니다. 커널 OOPS/패닉 메시지의 주소를 심볼로 변환하거나, 부트로더·디버거가 커널 심볼을 참조할 때 사용됩니다. 런타임에는 /proc/kallsyms가 로드된 모듈 심볼까지 포함한 동적 심볼 정보를 제공합니다.
System.map 포맷
# System.map 예시 — 형식: 주소 타입 심볼명
ffffffff81000000 T startup_64
ffffffff81001000 T do_early_param
ffffffff81a00000 D init_task
ffffffff82000000 B __bss_start
ffffffff82800000 A _end # 링커 스크립트 정의 커널 끝 마커
| 타입 | 의미 | 예시 |
|---|---|---|
T / t | 텍스트(코드) 섹션 — 대문자=전역 | 함수 심볼 |
D / d | 초기화 데이터 섹션 | 전역 변수 |
B / b | 초기화되지 않은 데이터(BSS) | 전역 0 초기화 변수 |
R / r | 읽기 전용 데이터 | 상수, 문자열 리터럴 |
A | 절대 주소 심볼 — 링크 시 고정 | 섹션 경계 마커 |
U | 미정의 심볼 — 외부 참조 필요 | 모듈 .ko에서 커널 심볼 참조 |
# System.map 수동 생성 (커널 빌드 루트에서)
# scripts/mksysmap이 내부적으로 실행하는 방식과 동일
nm -n vmlinux | grep -v ' [aUwVW] ' | sort > System.map
# [aUwVW]: 로컬 절대값·미정의·weak 심볼·weak 오브젝트 제외
# 주소로 심볼 검색 (커널 OOPS 해석)
grep "ffffffff810a1234" /boot/System.map-$(uname -r)
# 주소 직전 심볼 찾기 (함수 내 오프셋 파악)
awk '$1 <= "ffffffff810a1234" {sym=$0} END{print sym}' \
/boot/System.map-$(uname -r)
# /proc/kallsyms — 런타임 심볼 (로드된 모듈 포함)
grep "schedule" /proc/kallsyms | head -5
# ffffffff81a12340 T schedule
# fffff88001234560 t my_func [my_module]
# root 권한 없이 접근하면 주소가 0으로 마스킹됨 (kptr_restrict)
sudo cat /proc/kallsyms | grep " T schedule"
| 항목 | System.map | /proc/kallsyms |
|---|---|---|
| 생성 시점 | 커널 빌드 시 (정적) | 런타임 (동적) |
| 모듈 심볼 | 포함 안 됨 | 로드된 모듈 포함 |
| KASLR 적용 | 빌드 시 주소 고정 | 실제 로드 주소 반영 |
| 필요 CONFIG | 항상 생성 | CONFIG_KALLSYMS=y |
| 비특권 접근 | 파일 권한 의존 | 주소 마스킹 (kptr_restrict) |
CONFIG_KALLSYMS=y이면 심볼 이름이 커널 이미지 내 .kallsyms 섹션에 내장되어 OOPS 메시지에서 주소 대신 심볼명이 자동 출력됩니다. CONFIG_KALLSYMS_ALL=y이면 static 함수·변수까지 모두 포함합니다. 두 옵션이 모두 꺼지면 커널 패닉 콜 트레이스에 주소만 표시되므로 반드시 vmlinux와 addr2line으로 수동 변환해야 합니다.
심볼 가시성 (Symbol Visibility)
ELF는 심볼마다 가시성(visibility) 속성을 갖습니다. 이 속성은 링크 시 심볼이 다른 오브젝트에 어떻게 보이는지를 결정하며, 공유 라이브러리·커널 모듈 설계에 직접 영향을 줍니다.
| 가시성 | 값 | 설명 | 커널 활용 |
|---|---|---|---|
STV_DEFAULT | 0 | 바인딩(전역/약한)에 따라 결정. 일반 공개 심볼. | EXPORT_SYMBOL, EXPORT_SYMBOL_GPL |
STV_HIDDEN | 2 | 이 모듈 내부에서만 사용. 외부 참조·재배치 불가. | __attribute__((visibility("hidden"))) |
STV_PROTECTED | 3 | 외부에서 참조 가능하지만 인터포징(interposing) 불가. | 드물게 사용 |
STV_INTERNAL | 1 | 프로세서별 정의. 플랫폼 내부 전용. | 아키텍처 ABI 전용 |
# nm으로 가시성 확인 (-W: wide 출력, Ndx 컬럼 참고)
nm -W libfoo.so | grep "HIDDEN\|DEFAULT"
# readelf로 심볼 가시성 상세 확인
readelf -sW libfoo.so | awk '$5 == "HIDDEN"'
# objcopy로 심볼 가시성 변경 (전역 → hidden)
objcopy --hide-symbol=internal_func libfoo.so libfoo_hidden.so
# 모든 전역 심볼을 hidden으로 변환 후 원하는 것만 공개
objcopy --localize-hidden \
--globalize-symbol=public_api \
libfoo.so libfoo_api.so
/* GCC 속성으로 가시성 제어 */
#define EXPORT __attribute__((visibility("default")))
#define INTERNAL __attribute__((visibility("hidden")))
EXPORT int public_api(void); /* .so 외부에서 접근 가능 */
INTERNAL int helper(void); /* 이 .so 내부 전용 */
/* 커널 모듈 심볼 내보내기 */
EXPORT_SYMBOL(my_func); /* GPL·비GPL 모두 사용 가능 */
EXPORT_SYMBOL_GPL(my_func_gpl); /* GPL 라이선스 모듈만 사용 가능 */
EXPORT_SYMBOL()로 내보낸 심볼은 __ksymtab 섹션에 기록됩니다. nm mymodule.ko | grep __ksymtab으로 수출 심볼을 확인하고, nm /boot/System.map-$(uname -r) | grep " T my_func"로 커널이 해당 심볼을 제공하는지 검증할 수 있습니다.
objcopy — 오브젝트 파일 변환 및 조작
objcopy는 오브젝트 파일을 복사하면서 포맷 변환, 섹션 추가·제거, 심볼 조작, 주소 재배치 등을 수행합니다. 커널 이미지 변환(ELF → 바이너리), 펌웨어 패키징, 디버그 심볼 분리 등에 광범위하게 활용됩니다.
기본 문법
objcopy [옵션] 입력파일 [출력파일]
포맷 변환 옵션
| 옵션 | 설명 |
|---|---|
-I / --input-target=BFD이름 | 입력 파일 포맷 지정 |
-O / --output-target=BFD이름 | 출력 파일 포맷 지정 |
-F / --target=BFD이름 | 입출력 포맷 동시 지정 |
-B / --binary-architecture=아키텍처 | 바이너리 입력 시 아키텍처 지정 |
섹션 조작 옵션
| 옵션 | 설명 |
|---|---|
-j / --only-section=섹션명 | 지정 섹션만 출력 파일에 포함 |
-R / --remove-section=패턴 | 지정 섹션 제거 (와일드카드 사용 가능) |
--keep-section=패턴 | 지정 섹션 유지 |
--strip-section=패턴 | 섹션 내용을 비우되 헤더 유지 |
--add-section=섹션명=파일 | 파일 내용을 새 섹션으로 추가 |
--copy-section=from:to | 섹션을 다른 이름으로 복사 |
--rename-section=old=new[,플래그] | 섹션 이름 변경 및 플래그 조정 |
--set-section-flags=섹션=플래그 | 섹션 플래그 설정 (alloc, load, readonly 등) |
--set-section-alignment=섹션=정렬값 | 섹션 정렬 설정 |
--update-section=섹션명=파일 | 섹션 내용을 파일로 업데이트 |
--change-section-address=섹션=값 | 섹션 VMA/LMA 변경 |
--change-section-lma=섹션±값 | 섹션 LMA(로드 주소) 변경 |
--change-section-vma=섹션±값 | 섹션 VMA(가상 주소) 변경 |
--dump-section=섹션명=파일 | 섹션 원시 데이터를 파일로 덤프 |
심볼 조작 옵션
| 옵션 | 설명 |
|---|---|
-g / --strip-debug | 디버깅 심볼 제거 |
-S / --strip-all | 재배치 및 모든 심볼 제거 |
-K / --keep-symbol=심볼 | 지정 심볼 유지 |
-N / --strip-symbol=심볼 | 지정 심볼 제거 |
-G / --keep-global-symbol=심볼 | 지정 심볼만 전역으로 유지, 나머지 로컬화 |
-L / --localize-symbol=심볼 | 심볼을 로컬(static)으로 변경 |
-W / --weaken-symbol=심볼 | 심볼을 약한(weak) 심볼로 변경 |
--weaken | 모든 전역 심볼을 weak로 변경 |
--redefine-sym=old=new | 심볼 이름 변경 |
--redefine-syms=파일 | 파일에서 심볼 이름 매핑 읽기 |
--add-symbol=이름=[섹션:]값[,플래그] | 새 심볼 추가 |
--hide-symbol=심볼 | 심볼 가시성을 hidden으로 설정 |
--globalize-symbol=심볼 | 심볼을 전역으로 변경 |
자주 쓰는 예제
# ELF → 순수 바이너리 변환 (부트로더, 펌웨어)
objcopy -O binary vmlinux vmlinux.bin
# 특정 섹션만 추출 (예: .text 코드 영역)
objcopy -j .text -O binary vmlinux text.bin
# 디버그 심볼 분리 (원본 보존 + 심볼 파일 별도 저장)
objcopy --only-keep-debug vmlinux vmlinux.debug # 디버그 정보 추출
objcopy --strip-debug -O elf64-x86-64 vmlinux vmlinux.nodebug # 배포용
objcopy --add-gnu-debuglink=vmlinux.debug vmlinux.nodebug # 링크 등록
# 섹션 추가 (바이너리 파일을 오브젝트에 임베드)
objcopy --add-section .rodata.fw=firmware.bin \
--set-section-flags .rodata.fw=alloc,load,readonly \
input.o output.o
# 바이너리 파일을 링크 가능한 오브젝트로 변환
objcopy -I binary -O elf64-x86-64 \
-B i386:x86-64 firmware.bin firmware.o
# 섹션 제거 (DWARF 디버그 정보 전체 제거)
objcopy -R .debug_info -R .debug_abbrev -R .debug_aranges \
-R .debug_line -R .debug_str input.o output.o
# 심볼 이름 변경 (이름 충돌 해결)
objcopy --redefine-sym old_func=new_func module.o
# 모든 심볼 weak으로 변경 (테스트 오버라이드용)
objcopy --weaken original.o weak.o
### 커널 debuginfo 분리 패키지 구조 (배포판 빌드 방식) ###
# 1단계: 원본 vmlinux에서 디버그 정보만 별도 파일로 추출
objcopy --only-keep-debug vmlinux vmlinux.debug
# 2단계: strip된 배포용 파일 생성 (-o로 원본 보존)
strip --strip-debug -o vmlinux.stripped vmlinux
# strip만 쓰면 vmlinux를 in-place로 파괴하므로 반드시 -o 지정
# 3단계: 배포용 파일에 디버그 파일 링크 등록
objcopy --add-gnu-debuglink=vmlinux.debug vmlinux.stripped
# → vmlinux.stripped: 배포 패키지 / vmlinux.debug: debuginfo 패키지
# gdb/addr2line이 실행 파일의 GNU_DEBUGLINK 섹션을 참조해 자동 탐색
### initramfs를 커널 이미지에 정적 임베드 ###
# initramfs cpio를 링크 가능 오브젝트로 변환
objcopy -B i386 -I binary -O elf64-x86-64 \
initramfs.cpio.gz initramfs.o
# 생성된 심볼: _binary_initramfs_cpio_gz_start/end/size
# 이후 vmlinux 링크 시 initramfs.o를 함께 링크
### bzImage에서 압축 커널 추출 (분석용) ###
# bzImage의 압축 페이로드 오프셋/크기 확인
readelf -S arch/x86/boot/compressed/vmlinux | grep .rodata
# 또는 매직 바이트로 gzip/zstd 오프셋 탐색
od -A x -t x1z /boot/vmlinuz | grep "1f 8b" | head -1
# 해당 오프셋부터 압축 해제하면 vmlinux.bin 복원 가능
objdump — 오브젝트 파일 덤프 및 역어셈블
objdump는 오브젝트 파일의 다양한 정보를 사람이 읽기 좋은 형태로 출력합니다. ELF 헤더부터 역어셈블까지 폭넓게 활용되며, 커널 디버깅 시 crash와 함께 자주 쓰입니다. GDB와 함께 사용하는 소스 레벨 디버깅 방법은 GDB 페이지를 참고하세요.
주요 옵션
| 옵션 | 설명 |
|---|---|
-a / --archive-headers | 아카이브 파일 헤더 출력 |
-d / --disassemble | 실행 가능 섹션 역어셈블 |
-D / --disassemble-all | 모든 섹션 역어셈블 |
--disassemble=심볼명 | 특정 함수(심볼)만 역어셈블 |
-e | 디버깅 정보 출력 (stabs 포맷) |
-f / --file-headers | 파일 헤더 요약 출력 |
-g | 디버깅 정보 출력 (stabs) |
-G / --stabs | stabs 형식 디버깅 정보 출력 |
-h / --section-headers | 섹션 헤더 요약 출력 |
-H / --help | 도움말 출력 |
-i / --info | 지원 오브젝트 포맷·아키텍처 목록 |
-j / --section=섹션명 | 특정 섹션에만 작업 수행 |
-l / --line-numbers | 역어셈블 출력에 소스 줄번호 추가 |
-p / --private-headers | 포맷 전용 헤더 출력 |
-P / --private=옵션 | 포맷별 사설 옵션 전달 |
-r / --reloc | 재배치 항목 출력 |
-R / --dynamic-reloc | 동적 재배치 항목 출력 |
-s / --full-contents | 섹션 전체 내용 16진수/ASCII로 출력 |
-S / --source | 역어셈블에 소스 코드 혼합 출력 |
--source-comment=텍스트 | 소스 라인 앞에 지정 텍스트 접두사 추가 |
-t / --syms | 심볼 테이블 출력 |
-T / --dynamic-syms | 동적 심볼 테이블 출력 |
-U / --unicode=방식 | 유니코드 문자 처리 방식: default/invalid/locale/escape/hex/highlight |
-W / --dwarf[=옵션] | DWARF 디버깅 정보 출력 |
-x / --all-headers | 모든 헤더 정보 출력 (-a -f -h -p -r -t 조합) |
-z / --disassemble-zeroes | 역어셈블 시 0 블록 건너뛰지 않음 |
-C / --demangle | 심볼 디맹글링 |
-M / --disassembler-options=옵션 | 역어셈블러 옵션 (아키텍처별) |
--disassembler-color=on|off|terminal | 역어셈블 출력 색상 설정 |
--no-addresses | 주소 출력 생략 |
--no-show-raw-insn | 명령어 바이트 출력 생략 |
--visualize-jumps | 점프 대상을 ASCII 아트로 시각화 |
--start-address=주소 | 지정 주소부터 출력 |
--stop-address=주소 | 지정 주소까지만 출력 |
--prefix=prefix | 소스 경로 앞에 접두사 추가 |
--prefix-strip=level | 소스 경로에서 앞 레벨 제거 |
자주 쓰는 예제
# 섹션 헤더 요약
objdump -h vmlinux
# 특정 함수 역어셈블 (소스 혼합)
objdump -dS --disassemble=schedule vmlinux | less
# 색상 + 점프 시각화 역어셈블
objdump -d --disassembler-color=terminal --visualize-jumps vmlinux | less
# 재배치 항목 확인
objdump -r mymodule.ko
# 섹션 전체 내용 16진수 덤프
objdump -s -j .rodata mymodule.ko
# DWARF 디버깅 정보 전체 출력
objdump -W vmlinux | less
# x86_64에서 Intel 문법으로 역어셈블
objdump -d -M intel vmlinux | less
# ARM64에서 역어셈블
aarch64-linux-gnu-objdump -dS vmlinux | less
# 주소 범위 지정 역어셈블
objdump -d --start-address=0xffffffff81000000 \
--stop-address=0xffffffff81001000 vmlinux
재배치(Relocation) 메커니즘
재배치(Relocation)는 링커나 동적 로더가 심볼 참조를 실제 주소로 해석하는 과정입니다. 오브젝트 파일에는 재배치 엔트리가 포함되어 링커가 최종 주소를 결정할 때 어떤 위치의 어떤 값을 어떻게 수정해야 하는지 알려줍니다.
재배치 엔트리 구조
ELF 재배치 엔트리에는 두 종류가 있습니다.
| 타입 | 구조체 | 필드 | 설명 |
|---|---|---|---|
SHT_REL | Elf64_Rel | r_offset, r_info | addend 없음 — 수정할 위치의 값 자체를 addend로 사용 |
SHT_RELA | Elf64_Rela | r_offset, r_info, r_addend | 명시적 addend 포함 — x86-64·AArch64에서 주로 사용 |
x86-64 주요 재배치 타입
| 타입 | 값 | 크기 | 계산식 | 용도 |
|---|---|---|---|---|
R_X86_64_NONE | 0 | — | 없음 | 패딩/무시 |
R_X86_64_64 | 1 | 64bit | S + A | 절대 64비트 주소 |
R_X86_64_PC32 | 2 | 32bit | S + A − P | PC 상대 32비트 (call/jmp) |
R_X86_64_GOT32 | 3 | 32bit | G + A | GOT 엔트리 오프셋 |
R_X86_64_PLT32 | 4 | 32bit | L + A − P | PLT 엔트리 상대 주소 |
R_X86_64_GLOB_DAT | 6 | 64bit | S | GOT 슬롯을 심볼 주소로 채움 |
R_X86_64_JUMP_SLOT | 7 | 64bit | S | PLT 슬롯 (지연 바인딩) |
R_X86_64_RELATIVE | 8 | 64bit | B + A | 베이스 상대 — PIE/DSO 재배치 |
R_X86_64_32 | 10 | 32bit | S + A | 절대 32비트 (zero-extend) |
R_X86_64_32S | 11 | 32bit | S + A | 절대 32비트 (sign-extend) |
R_X86_64_PC64 | 24 | 64bit | S + A − P | PC 상대 64비트 |
R_X86_64_GOTPCREL | 9 | 32bit | G + GOT + A − P | GOT 엔트리의 PC 상대 주소 |
R_X86_64_TPOFF32 | 23 | 32bit | S + A − tp | TLS Initial Exec 오프셋 |
R_X86_64_TLSGD | 19 | 32bit | tlsgd + A − P | TLS General Dynamic 모델 |
R_X86_64_IRELATIVE | 37 | 64bit | ifunc(B+A) | GNU IFUNC 간접 함수 재배치 |
- S: 심볼의 실제 주소 (Symbol value)
- A: addend (Elf64_Rela.r_addend 또는 REL 타입에서 수정 위치의 값)
- P: 패치될 위치의 주소 (Place, r_offset + 로드 베이스)
- B: 공유 오브젝트의 로드 베이스 주소 (Base address)
- G: GOT에서 심볼 엔트리까지의 오프셋
- L: PLT 엔트리 주소
- GOT: Global Offset Table 주소
AArch64 주요 재배치 타입
| 타입 | 설명 |
|---|---|
R_AARCH64_ABS64 | 절대 64비트 주소 |
R_AARCH64_CALL26 | BL 명령어용 26비트 PC 상대 분기 (±128MB) |
R_AARCH64_JUMP26 | B 명령어용 26비트 PC 상대 분기 |
R_AARCH64_ADR_PREL_PG_HI21 | ADRP용 페이지 상대 21비트 (상위) |
R_AARCH64_ADD_ABS_LO12_NC | ADD용 하위 12비트 오프셋 |
R_AARCH64_RELATIVE | 베이스 상대 재배치 (PIE) |
R_AARCH64_GLOB_DAT | GOT 슬롯 채우기 |
R_AARCH64_JUMP_SLOT | PLT 지연 바인딩 |
재배치 엔트리 읽기
# REL/RELA 섹션 표시
readelf -r foo.o
# objdump로 재배치 표시
objdump -r foo.o # REL 타입
objdump -R foo.so # 동적(RELA) 재배치
# 실행 파일의 동적 재배치 확인
readelf -r /lib/x86_64-linux-gnu/libc.so.6 | head -40
PLT · GOT 동작 원리
공유 라이브러리의 외부 함수 호출은 PLT(Procedure Linkage Table)와 GOT(Global Offset Table)를 통해 지연 바인딩됩니다.
커널에서의 재배치 — 정적 재배치 테이블
리눅스 커널은 KASLR(Kernel Address Space Layout Randomization) 지원을 위해 빌드 시 생성된 재배치 테이블(.rela.dyn 또는 arch/x86/kernel/relocs)을 통해 부팅 시 자기 자신을 재배치합니다.
# 커널 재배치 섹션 확인
readelf -r vmlinux | grep -c RELATIVE
readelf -r vmlinux | head -20
# 모듈 재배치 확인 (.ko 파일은 SHT_RELA 섹션 포함)
readelf -r my_module.ko
# 재배치 오류 디버깅 (링크 시 오버플로우)
ld -Map=output.map -o vmlinux ...
# "relocation truncated to fit" 오류는 32비트 재배치 범위 초과 의미
arch/x86/tools/relocs와 CONFIG 옵션
arch/x86/tools/relocs 도구는 x86 커널 빌드 과정에서 vmlinux의 재배치 엔트리를 처리하여 부트 시 자기 재배치(self-relocation)에 필요한 테이블을 생성합니다. CONFIG_RELOCATABLE=y이면 커널이 임의 주소에 로드되어도 정상 동작하고, CONFIG_RANDOMIZE_BASE=y(KASLR)이면 부팅마다 로드 주소가 무작위화됩니다.
| CONFIG | 효과 | 재배치 방식 |
|---|---|---|
CONFIG_RELOCATABLE=y | 임의 물리 주소 로드 가능 | 부트 시 재배치 테이블 순회 |
CONFIG_RANDOMIZE_BASE=y | KASLR — 부팅마다 주소 무작위화 | 재배치 테이블 + ASLR 오프셋 |
| x86-64 기본값 (non-PIE) | -mcmodel=kernel로 빌드 — PIE 미지원, 전환 Kconfig 없음 | 32비트 PC-상대 재배치(R_X86_64_PC32) 사용. 참조 거리 ±2GB 제한 |
| ARM64 기본값 | PIE 커널 (-fpie) | R_AARCH64_RELATIVE 64비트 재배치 — 범위 제한 없음 |
# x86-64 커널 재배치 테이블 생성 확인 (빌드 시)
# arch/x86/tools/relocs 실행 로그는 make V=1 로 확인
make V=1 bzImage 2>&1 | grep relocs
# 재배치 오버플로우 디버깅: .text가 2GB 초과하면 R_X86_64_PC32 범위 초과
# 링크 오류: "relocation truncated to fit: R_X86_64_PC32 against symbol"
# 원인: 두 심볼 간 거리가 ±2GB 초과
ld -Map=vmlinux.map -o vmlinux $(KBUILD_VMLINUX_OBJS) -T vmlinux.lds
grep "\.text" vmlinux.map | awk '{print $1, $2}' | head -5
# ARM64 PIE 커널: 64비트 절대 재배치 사용 → 범위 제한 없음
readelf -r vmlinux | grep "RELATIVE" | wc -l
# 재배치 섹션 상세 분석 (모듈 .ko의 재배치 엔트리)
readelf -r my_module.ko | head -30
- x86-64:
-mcmodel=kernel로 빌드 →R_X86_64_PC32(32비트 PC 상대) 재배치 사용. 두 심볼이 2GB 이상 떨어지면 링크 오류 발생. - ARM64: 기본 PIE 커널(
-fpie) →R_AARCH64_RELATIVE(64비트 절대) 재배치. 범위 제한 없으나 재배치 엔트리 수가 많아져 부팅 오버헤드 증가. - RISC-V:
CONFIG_STRICT_KERNEL_RWX와 함께 PIE 지원.R_RISCV_RELATIVE사용.
readelf — ELF 파일 상세 분석
readelf는 ELF(Executable and Linkable Format) 파일의 구조를 상세히 표시합니다. BFD 라이브러리를 거치지 않고 ELF를 직접 파싱하므로 objdump보다 더 정확한 ELF 구조 분석이 가능합니다. ELF 파일만 지원합니다.
주요 옵션
| 옵션 | 설명 |
|---|---|
-a / --all | 모든 정보 출력 (-h -l -S -s -r -d -n -u -V -A -I 조합) |
-h / --file-header | ELF 파일 헤더 출력 (매직 넘버, 아키텍처, 엔트리 포인트 등) |
-l / --program-headers | 프로그램 헤더(세그먼트) 출력 |
-S / --section-headers | 섹션 헤더 목록 출력 |
-g / --section-groups | 섹션 그룹 출력 |
-t / --section-details | 섹션 상세 정보 (-S 확장) |
-e / --headers | 파일·프로그램·섹션 헤더 모두 출력 |
-s / --syms | 심볼 테이블 출력 (.symtab 및 .dynsym) |
--dyn-syms | 동적 심볼 테이블만 출력 |
--lto-syms | LTO 심볼 테이블 출력 |
-n / --notes | Note 섹션 출력 (빌드 ID, GNU 속성 등) |
-r / --relocs | 재배치 섹션 출력 |
-u / --unwind | 언와인드 정보 출력 (.eh_frame) |
-d / --dynamic | 동적 섹션 출력 (공유 라이브러리 의존성 등) |
-V / --version-info | 심볼 버전 정보 출력 |
-A / --arch-specific | 아키텍처별 정보 출력 |
-c / --archive-index | 아카이브 심볼 인덱스 출력 |
-D / --use-dynamic | 심볼·재배치 출력 시 동적 섹션 사용 |
-L / --lint | ELF 구조 경고 출력 |
-x 번호|이름 | 지정 섹션을 16진수로 덤프 |
-p 번호|이름 | 지정 섹션을 문자열로 덤프 |
-R 번호|이름 | 지정 섹션을 재배치 적용 후 16진수 덤프 |
-w[lLiaprmfFsoORtUuTgk] | DWARF 디버깅 정보 출력 (세부 섹션 선택 가능) |
--debug-dump[=세부항목] | DWARF 상세 덤프 (-w의 긴 형식) |
--dwarf-depth=깊이 | DWARF 다이 출력 깊이 제한 |
--dwarf-start=번호 | 특정 다이부터 출력 |
-I / --histogram | 심볼 버킷 히스토그램 출력 |
-C / --demangle | C++ 심볼 디맹글링 |
--sym-base=0|8|10|16 | 심볼 주소 출력 기수 |
-W / --wide | 80컬럼 이상으로 출력 (잘림 없음) |
-T / --silent-truncation | 심볼명 잘림 경고 생략 |
DWARF 서브 옵션 (-w)
| 문자 | 대상 섹션 |
|---|---|
l | .debug_line (소스 줄번호) |
L | .debug_line_str |
i | .debug_info (DWARF 다이 정보) |
a | .debug_abbrev (약어 테이블) |
p | .debug_pubnames |
r | .debug_aranges (주소 범위) |
m | .debug_macro / .debug_macinfo |
f | .debug_frame (프레임 정보) |
F | .eh_frame (예외 처리 프레임) |
s | .debug_str |
o | .debug_loc / .debug_loclists |
O | .debug_str_offsets |
R | .debug_ranges / .debug_rnglists |
t | .debug_pubtypes |
U | .debug_addr |
u | .debug_names (DWARF5) |
T | .debug_types |
g | .gdb_index |
k | 링커 최적화 힌트 |
# 파일 헤더 (아키텍처, 엔트리 포인트, ABI 확인)
readelf -h vmlinux
# 섹션 헤더 목록
readelf -S vmlinux | less
# 프로그램 헤더 (로드 세그먼트)
readelf -l vmlinux
# 심볼 테이블 (C++ 디맹글링 포함)
readelf -s --demangle vmlinux | grep schedule
# 동적 섹션 (공유 라이브러리 의존성)
readelf -d /lib/x86_64-linux-gnu/libc.so.6
# 재배치 항목 출력
readelf -r mymodule.ko
# DWARF 소스 줄번호 정보
readelf -wl vmlinux | less
# DWARF 전체 정보 (매우 많음)
readelf -wi vmlinux | less
# Note 섹션 (빌드 ID 확인)
readelf -n vmlinux
# 특정 섹션 16진수 덤프
readelf -x .rodata vmlinux | less
# 특정 섹션 문자열 덤프
readelf -p .comment vmlinux
# 80컬럼 이상 출력 (긴 심볼명 잘림 방지)
readelf -sW vmlinux | grep "schedule"
# ELF 구조 경고 검사
readelf -L vmlinux
strip — 심볼 및 섹션 제거
strip은 오브젝트 파일에서 심볼 테이블, 디버깅 정보, 재배치 항목 등을 제거해 파일 크기를 줄입니다. 릴리스 빌드, 커널 이미지 최적화, 배포 패키지 생성 시 활용합니다.
주요 옵션
| 옵션 | 설명 |
|---|---|
-g / -S / -d / --strip-debug | 디버깅 심볼만 제거 (재배치 정보 유지) |
-s / --strip-all | 재배치 정보와 심볼 테이블 모두 제거 |
--strip-section-headers | 섹션 헤더 테이블 제거 (실행 가능 파일) |
--strip-unneeded | 재배치에 필요 없는 심볼만 제거 |
-K / --keep-symbol=심볼 | 지정 심볼 유지 (제거 대상에서 제외) |
-N / --strip-symbol=심볼 | 지정 심볼 강제 제거 |
-o 파일명 | 결과를 별도 파일로 출력 (원본 유지) |
-p / --preserve-dates | 원본 파일의 타임스탬프 유지 |
-R / --remove-section=패턴 | 지정 섹션 제거 |
--keep-section=패턴 | 지정 섹션 유지 |
-x / --discard-all | 비전역 심볼 모두 제거 |
-X / --discard-locals | 컴파일러 생성 로컬 심볼 제거 (예: .L 접두사) |
--keep-file-symbols | 파일명 심볼 유지 |
--merge-notes | 동일한 Note 섹션 병합 (크기 축소) |
-I / --input-target=BFD | 입력 포맷 지정 |
-O / --output-target=BFD | 출력 포맷 지정 |
-D / --enable-deterministic-archives | 결정론적 모드 |
-v / --verbose | 자세한 출력 |
-w / --wildcard | 심볼명에 와일드카드 허용 |
# 모든 심볼 제거 (릴리스 실행 파일)
strip --strip-all myapp
# 디버그 심볼만 제거 (커널 모듈)
strip --strip-debug mymodule.ko
# 원본 유지하며 스트립된 버전 생성
strip -o myapp.stripped myapp
# 특정 심볼 유지하며 나머지 제거
strip --strip-all -K important_func myapp
# DWARF 섹션만 선택적 제거
strip -R .debug_info -R .debug_abbrev -R .debug_line myapp
# 디버그 정보 분리 저장 (gdb 분리 디버깅용)
objcopy --only-keep-debug vmlinux vmlinux.debug
strip --strip-debug -o vmlinux.stripped vmlinux
objcopy --add-gnu-debuglink=vmlinux.debug vmlinux.stripped
addr2line — 주소를 소스 위치로 변환
addr2line은 실행 파일의 주소(또는 함수명+오프셋)를 소스 파일명과 라인 번호로 변환합니다. 커널 패닉 콜 트레이스의 주소 분석, 크래시 보고서 해석에 필수입니다. 커널 크래시 전체 분석 절차는 커널 디버깅 및 크래시 분석 페이지를 참고하세요.
주요 옵션
| 옵션 | 설명 |
|---|---|
-a / --addresses | 출력 앞에 주소 표시 |
-b / --target=BFD이름 | 오브젝트 포맷 지정 |
-C / --demangle[=스타일] | C++ 심볼 디맹글링 |
-e / --exe=파일명 | 분석할 실행 파일 지정 (기본: a.out) |
-f / --functions | 파일명:라인 앞에 함수명 출력 |
-s / --basenames | 소스 파일명의 디렉토리 부분 제거 |
-i / --inlines | 인라인 함수 정보도 모두 출력 |
-p / --pretty-print | 사람이 읽기 좋은 단일 라인 포맷 출력 |
-j / --section=섹션명 | 주소를 지정 섹션 내 오프셋으로 해석 |
-r / --no-recurse-limit | 재귀 디맹글링 한도 제거 |
-R / --recurse-limit | 재귀 디맹글링 한도 복원 (기본) |
# 커널 패닉 주소 분석 (vmlinux에 디버그 심볼 필요)
addr2line -e vmlinux ffffffff810a1234
# 함수명 + 파일명:라인 출력
addr2line -f -e vmlinux ffffffff810a1234
# 인라인 함수 포함 + 가독성 좋은 출력
addr2line -f -i -p -e vmlinux ffffffff810a1234
# C++ 프로그램 분석 (디맹글링)
addr2line -C -f -e myapp 0x401234
# 파이프로 여러 주소 한 번에 분석
echo "ffffffff810a1234
ffffffff810b5678" | addr2line -f -e vmlinux
# 커널 패닉 콜 트레이스 자동 변환 스크립트
grep "Call Trace" -A 30 dmesg.log | \
grep -oP '0xffffffff[0-9a-f]+' | \
addr2line -f -i -e vmlinux
scripts/decode_stacktrace.sh가 있어, addr2line을 내부적으로 활용해 커널 스택 트레이스를 자동 변환합니다. dmesg | ./scripts/decode_stacktrace.sh vmlinux 형태로 사용합니다.
decode_stacktrace.sh 자동 분석
scripts/decode_stacktrace.sh는 리눅스 커널 소스에 포함된 셸 스크립트로, addr2line을 내부적으로 호출해 커널 OOPS·패닉의 콜 트레이스를 사람이 읽을 수 있는 형태로 자동 변환합니다. 모듈 주소도 처리하려면 모듈 디렉토리를 지정해야 합니다.
# 기본 사용법 — dmesg 출력을 파이프로 전달
dmesg | ./scripts/decode_stacktrace.sh vmlinux
# vmlinux 경로와 모듈 디렉토리 명시
dmesg | ./scripts/decode_stacktrace.sh \
/path/to/vmlinux \
/lib/modules/$(uname -r)/kernel
# 저장된 크래시 로그 분석
./scripts/decode_stacktrace.sh vmlinux /lib/modules/$(uname -r) \
< crash.log
# 인라인 함수 추적: -i 옵션이 내부적으로 적용됨
# 최적화(-O2)로 인라인된 함수도 정확히 추적됨
# 스크립트가 없는 환경에서 동일한 효과
grep -oP '(?<=\[<)[0-9a-f]+(?=>\])' crash.log | \
addr2line -f -i -p -e vmlinux
| 항목 | 설명 |
|---|---|
-i / --inlines 옵션 | -O2 컴파일로 인라인된 함수도 체인 전체 출력. decode_stacktrace.sh가 내부적으로 사용 |
CONFIG_DEBUG_INFO=y | 필수 — 디버그 정보 없으면 addr2line이 ??:0 출력 |
CONFIG_DEBUG_INFO_DWARF4/5 | DWARF 버전 선택. GDB 9+ / LLVM 기반 도구는 DWARF5 권장 |
| ORC unwinder | CONFIG_UNWINDER_ORC=y — DWARF 대신 ORC 테이블 사용. 더 빠르고 정확하나 addr2line은 여전히 DWARF에 의존 |
CONFIG_DEBUG_INFO=y이 없으면vmlinux에 DWARF 섹션이 없어addr2line이??:0만 반환합니다.- 배포 커널은 대부분
vmlinux를 별도 패키지(linux-image-dbg,kernel-debuginfo)로 제공합니다. - objtool은 ORC 언와인드 테이블을 생성하는 도구로, 런타임 스택 트레이스에는 ORC가 사용됩니다. 하지만
addr2line·decode_stacktrace.sh의 소스 위치 변환은 DWARF(.debug_info)에 의존합니다.
c++filt — C++ 이름 디맹글링
C++ 컴파일러는 함수 오버로딩을 지원하기 위해 함수명을 인코딩(맹글링)합니다. c++filt는 맹글링된 심볼(_ZN3foo3barEv)을 원래 C++ 표현(foo::bar())으로 복원합니다.
주요 옵션
| 옵션 | 설명 |
|---|---|
-_ / --strip-underscore | 심볼 앞의 언더스코어 제거 후 디맹글링 시도 |
-n / --no-strip-underscore | 언더스코어 제거 안 함 (기본값, 플랫폼 의존) |
-p / --no-params | 함수 매개변수 타입 출력 생략 |
-t / --types | 타입 심볼도 디맹글링 시도 |
-i / --no-verbose | 분석에 사용된 반환 타입 등 상세 정보 생략 |
-r / --no-recurse-limit | 재귀 한도 제거 |
-R / --recurse-limit | 재귀 한도 복원 (기본) |
-s 스타일 / --format=스타일 | 맹글링 스타일: auto(기본), gnu, lucid, arm, hp, edg, gnu-v3, java, gnat |
# 직접 심볼 디맹글링
c++filt _ZN3foo3barEv
# 출력: foo::bar()
# 표준 입력에서 읽기
nm myapp.o | c++filt
# 특정 심볼 파이프
echo "_ZNSt6vectorIiSaIiEE9push_backERKi" | c++filt
# 출력: std::vector<int, std::allocator<int>>::push_back(int const&)
# nm 출력과 결합 (C++ 오브젝트 분석)
nm -C myapp.o
# nm -C 옵션이 내부적으로 c++filt와 동일한 디맹글링 수행
# 매개변수 타입 없이 함수명만 출력
c++filt -p _ZN3foo3barERKiPc
# 출력: foo::bar
strings — 문자열 추출
strings는 파일에서 출력 가능한 문자열(기본 4글자 이상)을 추출합니다. 바이너리 파일 분석, 악성 코드 초기 분석, 커널 이미지에 포함된 버전 문자열·설정 정보 확인에 활용합니다.
주요 옵션
| 옵션 | 설명 |
|---|---|
-a / --all | 파일 전체를 스캔 (기본: ELF 오브젝트는 로드 가능 섹션만) |
-d / --data | 초기화된 데이터 섹션만 스캔 |
-f / --print-file-name | 각 문자열 앞에 파일명 출력 |
-min-len / -n min-len | 최소 문자열 길이 지정 (기본: 4) |
-o | 오프셋을 8진수로 출력 |
-t 기수 | 오프셋 출력: d(10진), o(8진), x(16진) |
-w / --include-all-whitespace | 탭·스페이스를 문자열의 일부로 포함 |
-e 인코딩 | 문자 인코딩: s(7-bit, 기본), S(8-bit), b(16-bit big), l(16-bit little), B(32-bit big), L(32-bit little) |
-U 방식 | 유니코드 표시: d(기본 로케일), i(invalid 표시), l(로케일), e(이스케이프), x(16진), h(하이라이트) |
-T BFD이름 | 오브젝트 포맷 지정 |
-p 구분자 | 문자열 구분자 지정 (기본: 개행) |
--output-separator=구분자 | 출력 구분자 지정 (멀티바이트 가능) |
# 커널 이미지에서 버전 문자열 추출
strings vmlinux | grep "Linux version"
# 전체 파일 스캔 (모든 섹션)
strings -a vmlinux | head -50
# 최소 길이 10 이상 문자열만
strings -n 10 /boot/vmlinuz-$(uname -r)
# 오프셋(16진수) 포함 출력
strings -t x myapp | grep "password"
# 파일명 + 문자열 (여러 파일 동시 분석)
strings -f *.ko | grep "MODULE_VERSION"
# UTF-16 LE 문자열 추출 (Windows 실행 파일)
strings -e l windows.exe
# 유니코드 이스케이프 표시
strings -U e vmlinux | grep "©"
size — 섹션 크기 분석
size는 오브젝트 파일 또는 실행 파일의 섹션 크기(text, data, bss)를 표시합니다. 메모리 풋프린트 분석, 커널 모듈 크기 최적화에 활용합니다.
주요 옵션
| 옵션 | 설명 |
|---|---|
-A / --format=sysv | SysV 포맷: 각 섹션별 상세 크기 출력 |
-B / --format=berkeley | Berkeley 포맷: text+data+bss+합계+16진합계 (기본) |
-G / --format=gnu | GNU 포맷: Berkeley와 유사하나 할당된 섹션만 집계 |
--common | 공통(common) 심볼 크기를 bss에 포함 |
-d / --radix=10 | 10진수로 크기 출력 |
-o / --radix=8 | 8진수로 크기 출력 |
-x / --radix=16 | 16진수로 크기 출력 |
-t / --totals | Berkeley 포맷에서 합계 행 추가 출력 |
--target=BFD이름 | 오브젝트 포맷 지정 |
# 기본 출력 (Berkeley 포맷)
$ size vmlinux
text data bss dec hex filename
23068672 3145728 524288 26738688 1980000 vmlinux
# SysV 포맷 (섹션별 상세)
size -A mymodule.ko
# GNU 포맷 (할당 섹션만)
size -G vmlinux
# 여러 파일 비교
size -t *.ko
# 16진수 출력
size -x vmlinux
# 커널 모듈 크기 정렬 비교
size *.ko | sort -k4 -n
ranlib — 아카이브 심볼 인덱스
ranlib은 정적 라이브러리(.a)에 심볼 인덱스를 추가하거나 갱신합니다. 인덱스가 있으면 링커가 라이브러리 검색 시 모든 멤버를 스캔하지 않아도 되므로 링크 속도가 향상됩니다. ar s와 동일한 기능입니다.
# 심볼 인덱스 생성/갱신
ranlib libfoo.a
# 결정론적 모드 (타임스탬프를 0으로 고정)
ranlib -D libfoo.a
# 비결정론적 모드 (실제 타임스탬프 사용)
ranlib -U libfoo.a
# 인덱스 없이 타임스탬프만 갱신
ranlib -t libfoo.a
ar crs libfoo.a *.o처럼 ar에 s 플래그를 주면 ranlib을 별도로 실행할 필요가 없습니다. 단, 멤버를 q(빠른 추가)로 넣었다면 나중에 ranlib을 실행해야 합니다.
elfedit — ELF 헤더 직접 편집
elfedit은 ELF 파일의 헤더 필드를 직접 수정합니다. 재컴파일 없이 OS/ABI, 머신 타입, ELF 파일 유형 등을 변경할 때 사용합니다.
주요 옵션
| 옵션 | 설명 |
|---|---|
--input-mach=머신 | 입력 ELF의 머신 타입 제한 |
--output-mach=머신 | e_machine 필드 변경 |
--input-type=타입 | 입력 ELF의 파일 타입 제한 |
--output-type=타입 | e_type 필드 변경 (ET_EXEC, ET_DYN 등) |
--input-osabi=ABI | 입력 ELF의 OS/ABI 제한 |
--output-osabi=ABI | e_ident[EI_OSABI] 필드 변경 |
--input-abiversion=버전 | 입력 ABI 버전 제한 |
--output-abiversion=버전 | ABI 버전 필드 변경 |
# OS/ABI를 Linux(3)으로 변경
elfedit --output-osabi=Linux myapp
# 머신 타입 변경 (비정상적인 경우 수정)
elfedit --output-mach=x86-64 myapp
# ELF 파일 타입을 공유 오브젝트(ET_DYN)로 변경
elfedit --output-type=ET_DYN myapp
# 변경 후 확인
readelf -h myapp | grep "OS/ABI\|Machine\|Type"
GNU 어셈블러 (as) 기초
as는 GNU 어셈블러로, 어셈블리 소스를 ELF 오브젝트 파일로 변환합니다. GCC 컴파일 파이프라인에서 gcc -S로 생성한 .s 파일을 .o로 변환할 때 내부적으로 호출됩니다. x86-64·AArch64 어셈블리 명령어 집합 참조는 어셈블리 페이지를, GCC 컴파일 옵션은 GCC 페이지를 참고하세요.
기본 사용법
# 어셈블리 파일을 오브젝트로 변환
as -o foo.o foo.s
# 64비트 모드 명시 (x86-64)
as --64 -o foo.o foo.s
# 32비트 모드
as --32 -o foo.o foo.s
# 디버그 정보 포함
as -g -o foo.o foo.s
# 경고를 오류로 처리
as --fatal-warnings -o foo.o foo.s
# 리스팅 파일 생성 (주소, 인코딩, 소스 나란히)
as -a=foo.lst -o foo.o foo.s
# 크로스 어셈블 (타겟 지정)
aarch64-linux-gnu-as -o foo.o foo.s
주요 GAS 지시어(Directives)
| 지시어 | 설명 |
|---|---|
.section .text | 코드 섹션 시작 |
.section .data | 초기화된 데이터 섹션 |
.section .bss | 비초기화 데이터 섹션 |
.global sym | 심볼을 전역으로 공개 (외부 참조 가능) |
.local sym | 심볼을 로컬(STB_LOCAL)로 제한 |
.type sym, @function | 심볼 타입을 함수로 지정 |
.type sym, @object | 심볼 타입을 데이터 오브젝트로 지정 |
.size sym, .-sym | 심볼 크기를 현재 위치 - 심볼 위치로 설정 |
.byte / .word / .long / .quad | 1/2/4/8 바이트 데이터 직접 기재 |
.string "text" | NULL 종료 문자열 삽입 |
.align n | n 바이트 경계 정렬 (또는 2^n, 아키텍처별 상이) |
.balign n | n 바이트 경계 정렬 (바이트 단위, 이식성↑) |
.fill count, size, val | count×size 바이트를 val로 채움 |
.comm sym, size, align | BSS 공통 심볼 선언 |
.weak sym | 약한 심볼 선언 |
.incbin "file" | 이진 파일 내용을 현재 위치에 직접 삽입 |
.rept / .endr | 블록 반복 |
.macro / .endm | 매크로 정의 |
x86-64 어셈블리 예제
/* hello.s — x86-64 AT&T 문법 */
.section .rodata
msg: .string "Hello, Kernel!\n"
msg_len = . - msg
.section .text
.global _start
.type _start, @function
_start:
movq $1, %rax /* sys_write */
movq $1, %rdi /* stdout fd */
leaq msg(%rip), %rsi /* 문자열 주소 (RIP 상대) */
movq $msg_len, %rdx /* 길이 */
syscall
movq $60, %rax /* sys_exit */
xorq %rdi, %rdi
syscall
.size _start, .-_start
# 어셈블 + 링크
as --64 -o hello.o hello.s
ld -o hello hello.o
./hello
인라인 어셈블리와의 관계
/* 커널 코드에서 자주 쓰이는 인라인 어셈블리 패턴 */
static __always_inline void native_cpuid(unsigned int *eax, unsigned int *ebx,
unsigned int *ecx, unsigned int *edx)
{
/* GCC extended asm — clobber list에 "cc" 포함 */
asm volatile("cpuid"
: "=a" (*eax), "=b" (*ebx), "=c" (*ecx), "=d" (*edx)
: "0" (*eax), "2" (*ecx)
: "memory");
}
/* 컴파일 후 GCC가 as를 호출해 임시 .s 파일 어셈블 */
/* gcc -S -O2 -o foo.s foo.c → as -o foo.o foo.s */
GNU 링커 (ld) 완전 가이드
ld는 GNU 링커로, 여러 오브젝트 파일과 라이브러리를 합쳐 최종 실행 파일 또는 공유 라이브러리를 생성합니다. GCC는 내부적으로 collect2를 통해 ld를 호출합니다. 링커 스크립트를 통해 메모리 레이아웃을 완전히 제어할 수 있어 커널 및 임베디드 펌웨어 개발에 필수적입니다.
기본 사용법
# 오브젝트 파일 링크
ld -o myapp foo.o bar.o
# 라이브러리 경로 + 라이브러리 지정
ld -o myapp foo.o -L/usr/lib -lc
# 공유 라이브러리 생성
ld -shared -o libfoo.so foo.o
# PIE 실행 파일 (Position Independent Executable)
ld -pie -o myapp foo.o
# 심볼 맵 파일 생성 (디버깅용) / -M 단축 옵션
ld -Map=output.map -o myapp foo.o
ld -M -o myapp foo.o > myapp.map
# 링커 스크립트 지정
ld -T mykernel.ld -o vmlinux *.o
# 사용되지 않는 섹션 제거
ld --gc-sections -o myapp foo.o
# verbose 모드 (어떤 파일이 링크되는지 + 기본 링커 스크립트 출력)
ld --verbose -o myapp foo.o 2>&1 | head -100
# 아카이브 전체 포함 (정적 초기화 등 모든 심볼 강제 링크)
ld --whole-archive libfoo.a --no-whole-archive -o myapp foo.o
# 부분 링크 (재배치 가능 출력, 추후 추가 링크)
ld -r -o combined.o foo.o bar.o
# 상태 스택으로 옵션 적용 범위 제어
ld --push-state --whole-archive libfoo.a --pop-state -o myapp foo.o
GCC를 통한 ld 옵션 전달 (-Wl)
GCC를 통해 ld를 간접 호출하는 경우 -Wl,옵션 형식으로 링커 옵션을 전달합니다. 콤마(,)로 옵션과 인수를 구분합니다.
# 링크 맵 파일 생성
gcc -Wl,-Map=output.map -o myapp foo.o bar.o
# 사용되지 않는 섹션 제거 (데드 코드 제거)
gcc -Wl,--gc-sections -ffunction-sections -fdata-sections -o myapp foo.o
# 아카이브 순환 참조 해결 (--start-group / --end-group)
gcc -Wl,--start-group -lfoo -lbar -lbaz -Wl,--end-group -o myapp foo.o
# soname 지정하여 공유 라이브러리 생성
gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.2.3 foo.o
# rpath 삽입 (런타임 라이브러리 검색 경로)
gcc -Wl,-rpath,/opt/myapp/lib -o myapp foo.o -L/opt/myapp/lib -lfoo
# 보안 강화 옵션 조합 (Full RELRO + 스택 실행 방지)
gcc -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack -o myapp foo.o
# 링커 스크립트 지정
gcc -Wl,-T,mykernel.ld -o vmlinux *.o
# 동적/정적 라이브러리 혼합 지정
gcc foo.o -Wl,-Bstatic -lfoo -Wl,-Bdynamic -lbar -o myapp
동적 링킹 및 공유 라이브러리 옵션
공유 라이브러리를 생성하거나 동적 링킹 동작을 제어하는 옵션들입니다.
| 옵션 | 설명 |
|---|---|
-Bdynamic | 이후 지정하는 -l 라이브러리를 동적 링크 (기본값) |
-Bstatic | 이후 지정하는 -l 라이브러리를 정적 링크 |
--as-needed | 실제로 사용하는 DSO만 DT_NEEDED에 기록 (링크 최적화) |
--no-as-needed | --as-needed 해제, 명시된 모든 DSO를 DT_NEEDED에 기록 |
-soname=name | 공유 라이브러리의 DT_SONAME 설정 (런타임 이름) |
-rpath=dir | 실행 파일에 DT_RPATH/DT_RUNPATH 삽입 (런타임 검색 경로) |
-rpath-link=dir | 링크 타임 전용 DSO 검색 경로 (출력에 미포함) |
-E / --export-dynamic | 모든 전역 심볼을 동적 심볼 테이블에 추가 (플러그인 등에서 필요) |
--dynamic-linker=file | PT_INTERP 동적 링커 경로 지정 |
--version-script=file | 심볼 버전 스크립트 지정 (ABI 관리) |
# 공유 라이브러리 생성 (soname 포함)
ld -shared -soname libfoo.so.2 -o libfoo.so.2.0.1 foo.o
# DT_RUNPATH 삽입 (DT_RPATH 대신 권장)
ld -o myapp foo.o -rpath /opt/myapp/lib --enable-new-dtags
# 필요한 DSO만 링크 (빌드 최적화)
ld --as-needed -o myapp foo.o -L/usr/lib -lfoo -lbar
보안 강화 링커 옵션 (-z)
-z keyword 형식으로 보안 관련 ELF 출력 속성을 제어합니다. 현대 리눅스 배포판은 이 옵션들을 기본 활성화하여 메모리 안전성을 높입니다.
| 옵션 | 효과 | 설명 |
|---|---|---|
-z relro | Partial RELRO | 재배치 완료 후 .got 등 섹션을 읽기 전용으로 변경 (PT_GNU_RELRO 세그먼트 추가) |
-z now | Full RELRO | 모든 심볼을 시작 시 즉시 바인딩 (DT_BIND_NOW). -z relro와 함께 사용하면 Full RELRO |
-z noexecstack | NX 스택 | 스택 세그먼트에 실행 권한 제거 (PT_GNU_STACK에 RW만 설정) |
-z execstack | 실행 가능 스택 | 스택 실행 허용 (레거시 호환, 보안상 비권장) |
-z stacksize=n | 스택 크기 힌트 | PT_GNU_STACK에 스택 크기 힌트 기록 |
-z nodlopen | dlopen 방지 | 공유 라이브러리를 dlopen()으로 열 수 없게 표시 |
-z nodelete | 언로드 방지 | 공유 라이브러리가 언로드(dlclose)되지 않도록 표시 |
-z defs | 미정의 심볼 오류 | 공유 라이브러리 빌드 시 미정의 심볼을 오류로 처리 |
-z notext | 텍스트 재배치 허용 | 읽기 전용 세그먼트의 재배치 허용 (비권장) |
# 보안 강화 실행 파일 빌드 (Full RELRO + NX 스택)
gcc -o myapp foo.o \
-Wl,-z,relro \
-Wl,-z,now \
-Wl,-z,noexecstack
# RELRO 적용 여부 확인
readelf -l myapp | grep GNU_RELRO
readelf -d myapp | grep BIND_NOW
환경 변수
ld는 동작 제어를 위해 다음 환경 변수를 참조합니다.
| 환경 변수 | 설명 |
|---|---|
GNUTARGET | 기본 입력 파일 포맷 BFD 이름 지정 (예: elf64-x86-64). --format 옵션과 동일 효과 |
LDEMULATION | 기본 에뮬레이션 링커 지정. 기본 링커 스크립트 선택에 영향 (예: elf_x86_64) |
COLLECT_NO_DEMANGLE | 설정 시 링크 맵에서 C++ 심볼 디맹글링 비활성화 |
LD_STATS | 설정 시 링커 리소스 사용 통계(메모리, 시간) 출력 |
LD_LIBRARY_PATH | 런타임 동적 라이브러리 검색 경로 (링크 타임이 아닌 실행 타임에 적용) |
# 사용 가능한 에뮬레이션 목록 확인
ld --verbose 2>&1 | grep -i emulation
# GNUTARGET으로 입력 포맷 명시
GNUTARGET=elf32-i386 ld -o myapp foo.o
# 링커 통계 출력
LD_STATS=1 ld -o myapp foo.o bar.o 2>&1
링커 스크립트 개요 (VMA vs LMA)
링커 스크립트(.ld)는 텍스트 파일로, C 스타일 주석(/* ... */)을 지원하며 명령어는 세미콜론(;)으로 구분합니다. 핵심 개념인 VMA와 LMA를 이해하는 것이 중요합니다.
| 개념 | 설명 | 예시 |
|---|---|---|
| VMA (Virtual Memory Address) | 실행 시 섹션이 위치하는 가상 주소. 코드에서 참조하는 주소 | 커널 코드: 0xffffffff81000000 |
| LMA (Load Memory Address) | 섹션이 실제 저장/로드되는 물리 주소. 부트로더가 이 주소에 로드 | ROM 주소: 0x08000000 |
임베디드 시스템에서는 코드가 ROM(LMA)에 저장되지만 RAM(VMA)으로 복사하여 실행하는 경우가 많습니다. AT(lma) 지시자로 LMA를 VMA와 다르게 지정합니다. --verbose 옵션으로 현재 플랫폼의 기본 링커 스크립트를 확인할 수 있습니다.
# 기본 링커 스크립트 확인
ld --verbose 2>&1 | grep -A 200 "using internal linker script"
링커 스크립트 기본 구조
링커 스크립트(.ld)는 출력 파일의 메모리 레이아웃을 직접 제어합니다. 커널·임베디드 펌웨어에서 필수적입니다.
/* 최소 링커 스크립트 예제 (x86-64 커널 스타일) */
OUTPUT_FORMAT("elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(startup_64) /* 엔트리 포인트 */
PHDRS {
text PT_LOAD FLAGS(5); /* r-x */
data PT_LOAD FLAGS(6); /* rw- */
}
SECTIONS {
. = 0xffffffff81000000; /* 커널 가상 주소 시작 */
.text : AT(ADDR(.text) - PAGE_OFFSET) {
_text = .;
*(.text.head) /* 헤드 코드 먼저 */
*(.text)
*(.text.*)
_etext = .;
} :text
. = ALIGN(4096); /* 페이지 경계 정렬 */
.rodata : {
*(.rodata)
*(.rodata.*)
} :data
.data : {
_data = .;
*(.data)
*(.data.*)
_edata = .;
} :data
.bss : {
_bss = .;
*(.bss)
*(COMMON)
_ebss = .;
} :data
/DISCARD/ : {
*(.comment)
*(.note.*)
}
}
링커 스크립트 핵심 명령어
| 명령어 | 설명 |
|---|---|
ENTRY(symbol) | 실행 파일 엔트리 포인트 심볼 지정 |
MEMORY { name (attr) : ORIGIN=addr, LENGTH=len } | 메모리 영역 정의 (임베디드) |
SECTIONS { ... } | 섹션 배치 규칙 정의 |
. = addr | 위치 카운터(location counter) 설정 |
. = ALIGN(n) | n 바이트 경계로 위치 카운터 정렬 |
KEEP(*(.init.data)) | --gc-sections에서도 해당 섹션 유지 |
PROVIDE(sym = expr) | 심볼이 미정의 시에만 제공 |
PROVIDE_HIDDEN(sym = expr) | PROVIDE + 심볼을 로컬(은닉) 가시성으로 설정 |
AT(lma) | 로드 메모리 주소(LMA) vs VMA 분리 지정 |
/DISCARD/ | 해당 섹션을 출력에서 제거 |
PHDRS { ... } | 프로그램 헤더(세그먼트) 수동 정의 |
ASSERT(cond, msg) | 링크 시 조건 검사, 실패 시 오류 |
INPUT(file ...) | 링크에 파일 명시적 포함 |
GROUP(file ...) | 아카이브 그룹 (순환 참조 해결, 반복 검색) |
INCLUDE filename | 다른 링커 스크립트 파일 포함 |
OUTPUT_FORMAT(bfd) | 출력 파일 BFD 포맷 지정 |
EXTERN(symbol ...) | 미정의 심볼 강제 추가 (해당 아카이브 멤버 링크 강제) |
NOCROSSREFS(sect ...) | 나열된 섹션 간 교차 참조 금지 (위반 시 오류) |
INSERT AFTER section | 기본 링커 스크립트의 특정 섹션 뒤에 삽입 |
SECTIONS 명령어 심화
출력 섹션의 전체 형식은 다음과 같습니다. 대부분의 선택적 항목은 생략 가능합니다.
/* 출력 섹션 완전 형식 */
/* section [address] [(type)] :
[AT(lma)]
[ALIGN(section_align) | ALIGN_WITH_INPUT]
[SUBALIGN(subsection_align)]
{
output-section-commands
} [>region] [AT>lma_region] [:phdr ...] [=fillexp] */
입력 섹션 선택의 주요 패턴들입니다.
SECTIONS {
.text : {
/* 와일드카드 패턴: *, ?, [chars] */
*(.text) /* 모든 .text 섹션 */
*(.text.*) /* .text.foo, .text.bar 등 */
foo?.o(.text) /* foo1.o, foo2.o 등의 .text */
/* 특정 파일 제외 */
EXCLUDE_FILE(*crtbegin*.o *crtend*.o) *(.ctors)
/* 이름 순 정렬 */
SORT_BY_NAME(*(.init_array*))
SORT_BY_ALIGNMENT(*(.data*))
SORT_BY_INIT_PRIORITY(*(.init_array.*))
/* gc-sections에서 보호 */
KEEP(*(.vectors))
KEEP(*(.init))
/* 초기화되지 않은 전역 변수 */
*(COMMON)
}
/* NOLOAD: 메모리 미할당, 주소만 예약 */
.debug (NOLOAD) : { *(.debug) }
/* 섹션 명시적 제거 */
/DISCARD/ : { *(.comment) *(.note.*) }
}
MEMORY 명령어
임베디드 시스템에서 ROM/RAM 영역을 정의하고 섹션을 배정합니다. 속성 문자: R(읽기전용), W(읽기/쓰기), X(실행), A(할당), I(초기화), !(반전).
MEMORY {
rom (rx) : ORIGIN = 0x08000000, LENGTH = 512K
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
ccm (rw) : ORIGIN = 0x10000000, LENGTH = 64K
}
SECTIONS {
/* 코드 → ROM */
.text : {
*(.text*)
*(.rodata*)
} > rom
/* 초기화 데이터: LMA=ROM, VMA=RAM */
.data : {
_sdata = .;
*(.data*)
_edata = .;
} > ram AT > rom /* VMA는 ram, 저장은 rom */
_data_load = LOADADDR(.data); /* 부트 코드가 복사할 LMA */
/* BSS → RAM (파일에 저장 불필요) */
.bss : {
_sbss = .;
*(.bss*)
*(COMMON)
_ebss = .;
} > ram
/* 고속 CCM 메모리 활용 */
.ccm_data (NOLOAD) : {
*(.ccm_data)
} > ccm
}
/* MEMORY 영역 함수: ORIGIN(rom) → 0x08000000, LENGTH(ram) → 131072 */
REGION_ALIAS(alias, region)를 사용하면 아키텍처별 메모리 구성을 추상화하여 동일한 링커 스크립트를 여러 플랫폼에 재사용할 수 있습니다.
PHDRS 명령어 (ELF 프로그램 헤더)
ELF 프로그램 헤더(세그먼트)를 수동으로 정의합니다. PHDRS를 사용하면 ld가 자동 생성하는 헤더가 없어지므로 필요한 헤더를 모두 명시해야 합니다.
/* PHDRS 구문: name type [FILEHDR] [PHDRS] [AT(address)] [FLAGS(flags)] */
/* FLAGS: PF_R=4, PF_W=2, PF_X=1 */
PHDRS {
headers PT_PHDR FILEHDR PHDRS FLAGS(5); /* 헤더 자체 포함 */
interp PT_INTERP FLAGS(4); /* 동적 링커 경로 */
text PT_LOAD FILEHDR PHDRS FLAGS(5); /* r-x (읽기+실행) */
data PT_LOAD FLAGS(6); /* rw- (읽기+쓰기) */
dynamic PT_DYNAMIC FLAGS(6); /* .dynamic 섹션 */
}
SECTIONS {
/* :phdr_name 으로 세그먼트에 배정 */
.interp : { *(.interp) } :text :interp
.text : { *(.text*) } :text
.rodata : { *(.rodata*) } :text
. = DATA_SEGMENT_ALIGN(0x200000, 0x1000);
.data : { *(.data*) } :data
.dynamic: { *(.dynamic) } :data :dynamic
.bss : { *(.bss*) *(COMMON) } :data
/* :NONE 으로 세그먼트 미배정 */
/DISCARD/ : { *(.comment) }
}
| 세그먼트 타입 | 값 | 설명 |
|---|---|---|
PT_NULL | 0 | 미사용 엔트리 |
PT_LOAD | 1 | 로드 가능한 세그먼트 (코드, 데이터) |
PT_DYNAMIC | 2 | 동적 링킹 정보 (.dynamic) |
PT_INTERP | 3 | 동적 링커 경로 (.interp) |
PT_NOTE | 4 | 보조 정보 (.note.*) |
PT_PHDR | 6 | 프로그램 헤더 테이블 자체 |
PT_TLS | 7 | 스레드 로컬 스토리지 |
PT_GNU_RELRO | 0x6474e552 | RELRO 읽기 전용 범위 |
PT_GNU_STACK | 0x6474e551 | 스택 실행 권한 힌트 |
VERSION 명령어 (심볼 버전 스크립트)
공유 라이브러리의 ABI 버전을 관리합니다. global:은 외부에 공개, local:은 은닉(라이브러리 내부 전용)입니다.
/* libfoo.map — 버전 스크립트 */
VERS_1.0 {
global:
foo_init; /* 공개 API */
foo_destroy;
foo_process;
local:
*; /* 나머지 모두 은닉 */
};
/* 버전 노드 상속: VERS_2.0은 VERS_1.0의 심볼을 포함 */
VERS_2.0 {
global:
foo_init_ex; /* 새 API 추가 */
foo_query;
} VERS_1.0; /* VERS_1.0 상속 */
# 버전 스크립트로 공유 라이브러리 빌드
gcc -shared -Wl,--version-script=libfoo.map -o libfoo.so.2 foo.o
# 버전 심볼 확인
nm -D libfoo.so.2 | grep "@@"
objdump -p libfoo.so.2 | grep -A 5 "Version definitions"
빌트인 함수 레퍼런스
링커 스크립트 표현식(SECTIONS 내)에서 사용할 수 있는 내장 함수들입니다.
| 함수 | 설명 |
|---|---|
ADDR(section) | 출력 섹션의 VMA 반환. 섹션이 이미 할당된 후에만 유효 |
LOADADDR(section) | 출력 섹션의 LMA 반환 (ROM 주소 참조 시 사용) |
SIZEOF(section) | 출력 섹션의 크기(바이트) 반환 |
ALIGN(expr) | 현재 위치 카운터(.)를 expr 배수로 올림 정렬 |
ALIGN(align, expr) | align 값을 expr 배수로 올림 정렬 |
NEXT(expr) | 현재 위치 이후 expr의 다음 배수 주소 반환 |
MAX(expr1, expr2) | 두 값 중 큰 값 반환 |
MIN(expr1, expr2) | 두 값 중 작은 값 반환 |
DEFINED(symbol) | 심볼이 정의되어 있으면 1, 아니면 0 |
SIZEOF_HEADERS | 출력 파일 헤더 크기 (첫 섹션 주소 결정에 활용) |
ORIGIN(memory) | MEMORY 영역의 시작 주소 반환 |
LENGTH(memory) | MEMORY 영역의 크기(바이트) 반환 |
SEGMENT_START(seg, default) | 세그먼트 시작 주소 반환 (-T 옵션 값 또는 기본값) |
DATA_SEGMENT_ALIGN(maxpagesize, commonpagesize) | 데이터 세그먼트 시작 정렬 (페이지 낭비 최소화) |
DATA_SEGMENT_END(expr) | 데이터 세그먼트 끝 표시 (BSS 이후) |
SECTIONS {
.text : { *(.text*) }
.rodata : { *(.rodata*) }
/* LMA 복사를 위한 심볼 */
_sdata_lma = LOADADDR(.data);
_sdata_size = SIZEOF(.data);
.data : {
_sdata = .;
*(.data*)
_edata = .;
} > ram AT > rom
/* DEFINED로 조건부 심볼 정의 */
PROVIDE(_stack_start = DEFINED(__stack_start) ?
__stack_start :
ORIGIN(ram) + LENGTH(ram));
/* MAX 활용 */
. = MAX(., ALIGN(16));
}
링크 오류 해석
| 오류 메시지 | 원인 | 해결책 |
|---|---|---|
undefined reference to 'foo' | 심볼 미정의 또는 라이브러리 누락 | -lfoo 추가 또는 순서 변경 |
multiple definition of 'bar' | 여러 파일에 동일 심볼 정의 | static 추가 또는 헤더 가드 |
relocation truncated to fit: R_X86_64_PC32 | 32비트 PC 상대 범위 초과 (±2GB) | 코드 모델 변경(-mcmodel=large) 또는 레이아웃 조정 |
cannot find -lfoo | 라이브러리 파일 미존재 | -L경로 추가 |
section overlap | 링커 스크립트 주소 충돌 | 섹션 주소/크기 재검토 |
cannot open linker script: No such file | -T 옵션에 지정한 스크립트 파일 미존재 | 파일 경로 확인 또는 -L로 검색 경로 추가 |
warning: orphan section | SECTIONS에서 다루지 않는 입력 섹션 발생 | 링커 스크립트에 해당 섹션 패턴 추가 또는 /DISCARD/로 제거 |
region ... overflowed by N bytes | MEMORY 영역 크기 초과 | 코드/데이터 최적화 또는 MEMORY 크기 조정 |
실전 활용 예제
아래 예제들은 커널 개발 현장에서 자주 쓰이는 Binutils 활용 패턴입니다. 커널 디버깅 전반은 커널 디버깅 페이지를, GDB를 이용한 소스 레벨 분석은 GDB 페이지를 함께 참고하세요.
커널 패닉 콜 트레이스 분석
# 1. dmesg에서 콜 트레이스 추출
dmesg | grep -A 40 "Call Trace" > trace.txt
# 2. 각 주소를 addr2line으로 변환
grep -oP 'ffffffff[0-9a-f]+' trace.txt | \
addr2line -f -i -p -e /usr/lib/debug/boot/vmlinux-$(uname -r)
# 또는 커널 스크립트 활용
dmesg | ./scripts/decode_stacktrace.sh vmlinux
정적 라이브러리 빌드 자동화
# Makefile 예제
OBJS = foo.o bar.o baz.o
LIB = libmylib.a
$(LIB): $(OBJS)
ar crsD $@ $^ # D: 결정론적 모드 (재현 가능 빌드)
# 빌드 후 심볼 확인
nm -C libmylib.a
커널 모듈 심볼 분석
# 모듈이 내보내는 심볼 확인
nm mymodule.ko | grep " T "
# 모듈이 참조하는 외부 심볼 (커널이 제공해야 하는 것)
nm mymodule.ko | grep " U "
# 커널 심볼 테이블에서 제공 여부 확인
nm /boot/System.map-$(uname -r) | grep "schedule"
# 모듈 크기 분석
size mymodule.ko
ELF 파일 완전 분석 워크플로
# 1. 기본 정보
readelf -h target.elf
# 2. 섹션 목록
readelf -S target.elf
# 3. 심볼 테이블
readelf -s target.elf | sort -k3 -n
# 4. 동적 의존성 (공유 라이브러리)
readelf -d target.elf | grep "NEEDED"
# 5. 역어셈블 (Intel 문법)
objdump -dS -M intel target.elf | less
# 6. 크기 분석
size -A target.elf
# 7. 문자열 추출
strings -t x target.elf | grep -i "version\|config\|error"
크로스 컴파일 환경에서의 Binutils
# ARM64용 크로스 컴파일 도구 접두사
CROSS = aarch64-linux-gnu-
# 크로스 컴파일된 커널 분석
${CROSS}nm -C vmlinux | grep schedule
${CROSS}readelf -h vmlinux
${CROSS}objdump -d --disassemble=do_fork vmlinux | head -100
${CROSS}size vmlinux
# ARM64 ELF → 바이너리 변환 (U-Boot raw 이미지)
${CROSS}objcopy -O binary vmlinux Image
펌웨어 임베딩
# 바이너리 파일을 ELF 오브젝트로 변환하여 링크
objcopy -I binary -O elf64-x86-64 -B i386:x86-64 \
firmware.bin firmware.o
# 링크 후 C 코드에서 참조
extern const char _binary_firmware_bin_start[];
extern const char _binary_firmware_bin_end[];
extern const size_t _binary_firmware_bin_size;
/* 사용 */
size_t fw_size = (size_t)(&_binary_firmware_bin_end -
&_binary_firmware_bin_start);
지원 BFD 타겟 및 아키텍처
Binutils가 지원하는 오브젝트 포맷과 아키텍처는 빌드 시 설정에 따라 다릅니다.
# 지원 포맷 목록
objdump -i
# 주요 BFD 타겟 이름
elf64-x86-64 # x86_64 ELF64
elf32-i386 # x86 ELF32
elf64-aarch64 # AArch64/ARM64 ELF64
elf32-littlearm # ARM (Little Endian) ELF32
elf64-littleriscv # RISC-V 64 ELF64
elf32-littleriscv # RISC-V 32 ELF32
elf64-powerpc # PowerPC 64-bit ELF
elf64-s390 # IBM S/390 ELF64
binary # 순수 바이너리 (포맷 없음)
ihex # Intel HEX 포맷
srec # Motorola S-Record 포맷
커널 특수 ELF 섹션
리눅스 커널은 일반 사용자 공간 ELF와 달리 커널 전용 특수 섹션을 사용합니다. 이 섹션들은 링커 스크립트(arch/x86/kernel/vmlinux.lds.S)와 include/linux/init.h, include/linux/export.h 등의 매크로로 정의됩니다.
초기화 관련 섹션
| 섹션명 | 용도 | 관련 매크로 |
|---|---|---|
.init.text | 부팅 시에만 실행되는 초기화 코드. 부팅 완료 후 free_initmem()으로 해제 | __init |
.init.data | 초기화 데이터 (cmdline 파싱 테이블 등). 부팅 후 해제 | __initdata |
.init.rodata | 초기화 읽기 전용 데이터 | __initconst |
.exit.text | 모듈 언로드 시 실행 코드 (내장 모듈에서는 버려짐) | __exit |
.exit.data | 모듈 언로드 시 데이터 | __exitdata |
.cpuinit.text | CPU 핫플러그 초기화 코드 (구버전) | __cpuinit (제거됨) |
심볼 내보내기 섹션
| 섹션명 | 용도 |
|---|---|
__ksymtab | EXPORT_SYMBOL()로 내보낸 심볼 테이블. 각 엔트리: {값, 이름, 네임스페이스} |
__ksymtab_gpl | EXPORT_SYMBOL_GPL()로 내보낸 GPL 전용 심볼 |
__kcrctab | 심볼별 CRC 체크섬 (CONFIG_MODVERSIONS 시 생성). 모듈 ABI 검증용 |
__kcrctab_gpl | GPL 심볼의 CRC 테이블 |
__ksymtab_strings | 심볼 이름 문자열 풀 |
섹션 배열 패턴 (linker set)
커널은 링커가 섹션 내 항목들을 자동으로 수집하는 패턴을 광범위하게 사용합니다.
| 섹션명 | 용도 | 관련 매크로 |
|---|---|---|
__param | 모듈 파라미터 (module_param()) | MODULE_PARAM_DESC |
__tracepoints | ftrace/perf 트레이스포인트 디스크립터 | DEFINE_TRACE |
__jump_table | Jump label (static branch) 엔트리 | DEFINE_STATIC_KEY_* |
__bug_table | BUG()/WARN() 위치 정보 테이블 | BUG_ON() |
.altinstructions | 대체 명령어 패치 테이블 (CPU 특성 기반) | ALTERNATIVE() |
__ex_table | 예외 테이블 (fixup 핸들러 주소) | _ASM_EXTABLE |
.smp_locks | LOCK 접두사 패치 위치 목록 (단일 CPU 최적화) | LOCK_PREFIX |
__patchable_function_entries | ftrace mcount 패치 가능 위치 (CONFIG_DYNAMIC_FTRACE) | -mfentry |
특수 섹션 분석 명령
# 커널 내보낸 심볼 수 확인
readelf -S vmlinux | grep ksymtab
nm vmlinux | grep "__ksymtab_" | wc -l
# 초기화 코드 크기 확인 (부팅 후 해제될 메모리)
size vmlinux
readelf -S vmlinux | grep "\.init"
# 예외 테이블 엔트리 확인
readelf -S vmlinux | grep ex_table
objdump -j __ex_table -s vmlinux | head -40
# jump label 엔트리 수
readelf -S vmlinux | grep jump_table
# 대체 명령어 패치 테이블 분석
objdump -j .altinstructions -d vmlinux 2>/dev/null | head -30
# 모듈의 ksymtab 확인
readelf -S my_module.ko | grep ksymtab
nm my_module.ko | grep "__ksymtab\b"
# 모듈 파라미터 섹션
objdump -j __param -s my_module.ko
섹션 크기 비교 예시
# vmlinux 주요 섹션 크기 분석
readelf -S vmlinux | awk '/\[/{name=$2} /PROGBITS|NOBITS/{
cmd = "printf \"%10d %s\n\", 0x"$6", name"
system(cmd)
}' | sort -rn | head -20
# 또는 간단히
size -A vmlinux | sort -k2 -rn | head -20
__init함수를 부팅 후에 호출하면 BUG: unable to handle kernel NULL pointer dereference 또는 general protection fault 발생 — 해제된 페이지 접근CONFIG_DEBUG_SECTION_MISMATCH=y로 빌드하면 잘못된 섹션 참조(예: 일반 코드가.init.text함수를 호출)를 컴파일 시 경고scripts/checkconfig.pl,scripts/mod/modpost.c가 모듈 섹션 불일치 검사 수행nm vmlinux | grep ' T ' | sort로 커널 함수 목록을 주소순으로 확인 가능
커널 이미지 빌드 파이프라인
리눅스 커널 빌드는 vmlinux(ELF 실행 파일) 생성부터 최종 부트 이미지 패키징까지 여러 단계를 거칩니다. 각 단계에서 Binutils 도구가 핵심 역할을 담당합니다.
단계별 상세
# 1단계: 소스 컴파일 → 오브젝트 파일 생성
make -j$(nproc) vmlinux
# 2단계: System.map 자동 생성 (make가 내부적으로 scripts/mksysmap 실행)
nm -n vmlinux | grep -v ' [aUwVW] ' | sort > System.map
# 3단계: ELF → 순수 바이너리 변환
objcopy -O binary -R .note -R .comment -S vmlinux vmlinux.bin
# 4단계: 압축 (arch/x86/boot/compressed/ 에서 수행)
gzip -n -f -9 vmlinux.bin # → vmlinux.bin.gz
# 5단계: bzImage 조합 (x86 전용)
make bzImage
# → arch/x86/boot/bzImage (부트 섹터 + 설정 헤더 + 압축 커널)
아키텍처별 커널 이미지 포맷
| 아키텍처 | 이미지 파일 | 변환 도구 | 특징 |
|---|---|---|---|
| x86 / x86-64 | bzImage | objcopy + arch/x86/boot | 부트 섹터 + 설정 헤더 + 압축 vmlinux |
| ARM64 | Image / Image.gz | objcopy -O binary + gzip | EFI stub 내장, 헤더 64바이트 |
| ARM (32비트) | zImage / uImage | objcopy + mkimage | U-Boot용 uImage는 mkimage로 래핑 |
| RISC-V | Image / Image.gz | objcopy -O binary + gzip | ARM64와 유사한 구조 |
| MIPS | vmlinuz | objcopy + 부트래퍼 | 플랫폼마다 다름 |
bzImage/Image는 심볼이 strip된 바이너리이므로 addr2line·gdb로 주소를 분석할 수 없습니다. 커널 개발·디버깅 환경에서는 반드시 빌드 디렉토리의 vmlinux(DWARF 포함 ELF)를 보관해야 합니다. 배포판은 별도 linux-image-dbg / kernel-debuginfo 패키지로 제공합니다.
커널 모듈 빌드와 심볼 검증
커널 모듈(.ko)은 커널 빌드 시스템과 별도로 빌드되지만, modpost 단계에서 심볼 의존성·라이선스·버전을 엄격히 검증합니다. Binutils 도구는 빌드된 모듈의 심볼 구조를 분석하는 데 필수적입니다.
모듈 빌드 기본 패턴
# 최소 Makefile (외부 모듈 빌드용)
obj-m += my_module.o
# 여러 소스 파일로 구성된 모듈
my_module-objs := main.o helper.o ops.o
# 외부 모듈 빌드
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
# 설치
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules_install
# 빌드 결과물 확인
ls *.ko *.o .*.cmd
modpost 심볼 검증
scripts/mod/modpost.c는 모듈 빌드의 핵심 검증 단계입니다. 미정의 심볼 참조, 섹션 불일치, GPL 전용 심볼 무단 사용 등을 체크합니다.
# 모듈의 미정의 심볼 확인 (커널에서 제공해야 함)
nm -u my_module.ko
# 모듈이 사용하는 커널 심볼이 실제 존재하는지 확인
nm -u my_module.ko | awk '{print $2}' | \
while read sym; do
grep -q " $sym$" /proc/kallsyms || echo "MISSING: $sym"
done
# 모듈이 내보내는 심볼 확인 (__ksymtab 섹션)
nm my_module.ko | grep "__ksymtab\b"
# 모듈 섹션 구조 전체 확인
readelf -S my_module.ko | grep -E "ksymtab|kcrctab|modinfo|param"
CONFIG_MODVERSIONS와 ABI 검증
CONFIG_MODVERSIONS=y이면 커널은 각 내보내기 심볼에 CRC 체크섬을 계산해 __kcrctab 섹션에 저장합니다. 모듈 로드 시 커널 CRC와 모듈 CRC를 비교해 ABI 불일치를 방지합니다.
# 모듈의 버전 CRC 확인
readelf -S my_module.ko | grep kcrctab
nm my_module.ko | grep "__crc_"
# 커널 Module.symvers 파일 확인 (심볼별 CRC 목록)
grep "my_exported_func" Module.symvers
# 출력: CRC 심볼명 vmlinux/모듈경로 라이선스
# 모듈 로드 오류: "disagrees about version of symbol"
# → 모듈이 다른 커널 버전용으로 빌드되었거나 Module.symvers 불일치
dmesg | grep "disagrees about version"
MODULE_LICENSE와 EXPORT_SYMBOL_GPL
/* 모듈 라이선스 선언 — GPL이어야 _GPL 심볼 접근 가능 */
MODULE_LICENSE("GPL v2");
/* 비GPL 모듈도 접근 가능한 일반 심볼 */
EXPORT_SYMBOL(my_public_func);
/* GPL 라이선스 모듈만 접근 가능 */
EXPORT_SYMBOL_GPL(my_gpl_only_func);
# GPL 전용 심볼 사용 여부 확인
nm -u my_module.ko | grep "_gpl"
# .modinfo 섹션에서 라이선스 확인
readelf -p .modinfo my_module.ko
# 또는
modinfo my_module.ko | grep license
모듈 로드 시 심볼 해석 흐름
모듈이 insmod/modprobe로 로드될 때 커널의 kernel/module/core.c가 심볼 해석을 수행합니다.
# 모듈 로드 및 심볼 해석 흐름 추적
# 1. insmod → sys_finit_module() 시스템 콜
# 2. kernel/module/core.c: load_module() → resolve_symbol_wait()
# 3. __ksymtab 섹션 순회 → CRC 검증 (MODVERSIONS) → 주소 패치
# 로드 후 심볼 주소 확인
grep "my_module" /proc/kallsyms
# 모듈의 의존성 그래프 확인
modinfo -F depends my_module.ko
CONFIG_MODULE_SIG — 서명된 모듈 분석
CONFIG_MODULE_SIG=y이면 scripts/sign-file로 모듈에 서명을 추가합니다. 서명은 .ko 파일 끝에 추가되거나 별도 섹션에 저장됩니다.
# 모듈 서명 추가 (빌드 시스템이 자동 수행)
scripts/sign-file sha256 signing_key.pem signing_cert.pem my_module.ko
# 서명 섹션 확인
readelf -S my_module.ko | grep -i sig
# CONFIG_MODULE_SIG_FORMAT=PKCS7이면 .ko 끝에 바이너리 PKCS7 블록 추가
# 서명 존재 여부 확인 (파일 끝의 매직 바이트)
tail -c 28 my_module.ko | od -An -tx1 | grep "7e 4d 6f 64"
# "~Module signature appended~" 매직 확인
# 빌드 → 검증 → 로드 전체 흐름
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
nm my_module.ko | grep -E "T |U " # 심볼 확인
readelf -S my_module.ko | head -30 # 섹션 구조 확인
sudo insmod my_module.ko # 로드
dmesg | tail -5 # 로드 결과 확인
참고 자료
- GNU Binutils 공식 매뉴얼 — GNU Binary Utilities, Roland H. Pesch, Jeffrey M. Osier (버전 2.46, 2026년 2월)
- GNU Linker 매뉴얼 — Using ld, 링커 스크립트·메모리 레이아웃·섹션 배치 상세
- GNU Assembler 매뉴얼 — Using as, GAS 지시어·문법·아키텍처별 확장
- Binutils 소스 저장소 (Sourceware Git) — binutils-gdb 통합 저장소
- 리눅스 커널 소스:
scripts/decode_stacktrace.sh,scripts/kallsyms.c,arch/x86/kernel/vmlinux.lds.S - ELF 명세 — System V Application Binary Interface, ELF 파일 형식 정의 (PDF)
- DWARF 명세 — DWARF Debugging Information Format 버전 5 (dwarfstd.org)
- x86-64 ABI 명세 — System V AMD64 ABI, 재배치 타입·호출 규약·ELF 확장 정의
관련 페이지
- ELF 파일 형식 — ELF 구조 심화, 동적 링킹, 인터프리터
- GCC — 컴파일 파이프라인, 최적화 옵션, 인라인 어셈블리
- GDB — 소스 레벨 디버깅, DWARF 활용, 원격 디버깅
- GNU Assembler (as) — as 커맨드라인 옵션, GAS 지시자 완전 참조, CFI 지시자, 아키텍처별 기능
- 어셈블리 — x86-64·AArch64 명령어 집합, AT&T/Intel 문법
- 커널 디버깅 — printk, KASAN, kprobe, ftrace 통합 디버깅 기법
- 크래시 분석 — 커널 패닉·콜 트레이스 해석, crash 도구
관련 문서
- C 언어 완전 가이드 & 커널 C 관용어 — GNU C 확장과 커널 관용어를 중심으로 container_of·READ_ONCE·barr
- 커널 필수 함수·매크로·심볼 레퍼런스 — 커널 개발에서 반복적으로 쓰는 함수·매크로·심볼을 컨텍스트별로 무제한 확장 정리한 실전 레