GNU Binutils 완전 가이드

커널 및 모듈 분석에서 Binutils를 어떻게 조합해 쓰는지 실전 흐름으로 정리합니다. `readelf`/`objdump`로 ELF 구조와 디스어셈블 확인, `nm`/`addr2line`로 심볼 역추적, `objcopy`/`strip`으로 배포용 바이너리 가공, `ar`/`ranlib`로 정적 라이브러리 관리, `elfedit`로 헤더 조정까지 디버깅·성능 분석·릴리스 준비 단계에서 필요한 절차를 상세히 설명합니다.

관련 표준: GNU Binutils 2.46 공식 매뉴얼, System V ABI(ELF 명세), DWARF Debugging Information Format (v4/v5) — Binutils가 처리하는 ELF 파일 형식과 디버그 정보의 국제 표준입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 개발 환경 설정커널 개발 도구 문서를 먼저 읽으세요. ELF 파일 형식과 링커·어셈블러의 기본 개념을 알고 있으면 이 문서를 훨씬 빠르게 이해할 수 있습니다.
일상 비유: Binutils는 완성된 제품을 분해하고 검사하는 공구 세트와 같습니다. 공장(컴파일러)이 부품(오브젝트 파일)을 만들면, Binutils는 부품의 내용을 열람(nm/objdump)하고, 조립(ar/ranlib)하거나, 불필요한 라벨을 떼어내고(strip), 제품 번호로 원본 도면을 찾아주는(addr2line) 도구들입니다.

핵심 요약

  • ar — 오브젝트 파일을 묶어 정적 라이브러리(.a)를 생성·관리합니다.
  • nm — 오브젝트/라이브러리 파일의 심볼 테이블을 출력합니다.
  • objdump — 섹션 헤더, 역어셈블, 재배치 정보 등을 덤프합니다.
  • readelf — BFD 라이브러리 독립적으로 ELF 구조를 정밀하게 표시합니다.
  • strip/objcopy — 심볼/섹션을 제거하거나 포맷을 변환합니다.

단계별 이해

  1. 컴파일 결과 확인
    nm으로 심볼 타입과 이름을 확인하고, size로 섹션 크기를 파악합니다.
  2. 오브젝트 구조 분석
    readelf -a 또는 objdump -x로 ELF 헤더·섹션·세그먼트를 전부 검토합니다.
  3. 역어셈블 및 디버깅
    objdump -dS로 소스와 어셈블리를 함께 보고, addr2line으로 주소를 파일명:라인으로 변환합니다.
  4. 라이브러리 관리
    ar로 정적 라이브러리를 생성하고 ranlib으로 심볼 인덱스를 갱신합니다.
  5. 릴리스 최적화
    strip으로 디버그 심볼을 제거하고, objcopy로 원하는 섹션만 추출합니다.

개요 및 도구 목록

GNU Binutils(GNU Binary Utilities)는 오브젝트 파일과 실행 파일을 생성·분석·변환하는 명령행 도구 모음입니다. 버전 2.46(2026년 2월)을 기준으로 하며, 리눅스 커널 빌드, 크로스 컴파일, 펌웨어 개발, 역공학 분석 모두에 필수적입니다.

소스 파일 .c / .s as (어셈블러) 오브젝트 파일 .o (ELF) ar (아카이브) 정적 라이브러리 .a ld (링커) 실행 파일 / SO ELF / COFF / … 분석 도구 nm · size objdump · readelf strings · addr2line 변환/편집 도구 objcopy · strip elfedit · ranlib
도구주요 역할대표 옵션
ar정적 라이브러리(.a) 생성·관리cr, t, x
nm심볼 테이블 출력-C -D -u -S
objcopy오브젝트 파일 변환·섹션 조작-O -R --add-section
objdump역어셈블·섹션·재배치 정보 출력-d -S -h -r -t
readelfELF 구조 상세 분석(BFD 독립)-a -s -S -d -w
strip심볼·섹션 제거로 파일 크기 축소-g -s -K -N
addr2line주소 → 파일명:라인번호 변환-e -f -C -i
c++filtC++ 맹글링 심볼 디맹글링-p -t
strings파일 내 출력 가능 문자열 추출-a -n -t -e
size섹션(text/data/bss) 크기 출력-A -B -G
ranlib아카이브 심볼 인덱스 생성-D -U
elfeditELF 헤더 필드 직접 편집--output-osabi
ldGNU 링커 (별도 매뉴얼 참조)링커 스크립트, -T
BFD 라이브러리: Binutils의 대부분 도구는 BFD(Binary File Descriptor) 라이브러리를 통해 다양한 오브젝트 포맷(ELF, COFF, Mach-O 등)을 지원합니다. readelf만 BFD를 거치지 않고 ELF를 직접 파싱하므로 ELF 분석의 1차 도구로 권장됩니다.

ELF 파일 구조

Binutils 전체 도구가 다루는 핵심 파일 형식인 ELF(Executable and Linkable Format)의 내부 구조를 이해하면 각 도구의 동작 원리를 훨씬 명확히 파악할 수 있습니다. ELF 파일 형식의 심화 내용은 ELF 파일 형식 페이지를 참고하세요.

ELF 파일 ELF 헤더 매직, 클래스, 엔디안, ABI 프로그램 헤더 테이블 세그먼트(LOAD, DYNAMIC…) .text (코드) .rodata (읽기 전용 데이터) .data (초기화 데이터) .bss (비초기화 데이터) .symtab / .dynsym (심볼) .debug_* (DWARF 디버그) 섹션 헤더 테이블 분석 도구 readelf -h objdump -f elfedit readelf -l objdump -p 섹션 내용 도구 objdump -d (역어셈블) objdump -s (헥스 덤프) strings (문자열 추출) size (크기 출력) strip / objcopy (변환·수정) nm readelf -s readelf -w / objdump -W addr2line readelf -S / objdump -h
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 -aobjdump -xreadelf가 더 정확 (BFD 미사용)
역어셈블 / 소스 혼합objdump -dSobjdump --disassemble=함수명소스 필요 시 디버그 심볼 필수
심볼 조회nm -Creadelf -snm이 간결, readelf가 상세
커널 패닉 주소 변환addr2line -fipscripts/decode_stacktrace.shvmlinux에 디버그 심볼 필요
정적 라이브러리 관리ar crsDranlib -D-D: 재현 가능 빌드
파일 크기 축소strip --strip-debugobjcopy -R .debug_*커널 모듈은 --strip-debug만
포맷 변환objcopy -O binaryobjcopy -O ihex부트로더/펌웨어 이미지 생성
C++ 심볼 해독c++filtnm -C, addr2line -C-C 옵션은 내부적으로 c++filt 사용
문자열 추출strings -areadelf -p .rodata특정 섹션만 보려면 readelf
메모리 풋프린트 분석size -Anm -S --size-sort심볼별 크기는 nm -S
공유 라이브러리 의존성readelf -dnm -DNEEDED 엔트리 확인
재배치 분석readelf -robjdump -r커널 모듈 링크 문제 진단
ELF 헤더 수정elfeditobjcopyelfedit이 더 안전 (최소 변경)
DWARF 디버그 분석readelf -wiobjdump -Wpahole도 구조체 레이아웃에 유용

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심볼 인덱스 기록 안 함
TThin 아카이브 생성 (파일 복사 없이 경로만 참조)
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
커널 빌드와 ar: 리눅스 커널 빌드 시스템(Kbuild)은 서브디렉토리별로 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

심볼 타입

타입의미설명
AAbsolute절대 주소 심볼. 링크 시 값이 변하지 않습니다.
B / bBSS초기화되지 않은 전역 변수(데이터). 대문자=전역, 소문자=로컬.
C / cCommon초기화되지 않은 공통 심볼. --warn-common으로 경고 가능.
D / dData초기화된 데이터 섹션 심볼.
G / gSmall Data초기화된 소형 오브젝트 섹션 심볼.
iIndirect FunctionGNU 간접 함수 심볼(ifunc).
IIndirect Reference다른 심볼에 대한 간접 참조.
NDebug디버깅 심볼.
pStack Unwind스택 언와인드 섹션 심볼.
R / rRead-only Data읽기 전용 데이터 섹션 심볼.
S / sSmall BSS초기화되지 않은 소형 오브젝트 섹션 심볼.
T / tText코드(텍스트) 섹션 심볼. 함수 심볼이 여기에 속합니다.
UUndefined정의되지 않은 외부 심볼 (다른 오브젝트에서 제공 필요).
uUnique GlobalELF 고유 전역 심볼 (GNU 확장).
V / vWeak약한(weak) 심볼. 강한 심볼이 있으면 덮어쓰여집니다.
W / wWeak (unspecified)태그 없는 약한 심볼.
-Stabs Debuga.out 포맷 디버그 심볼.
?Unknown알 수 없는 심볼 타입.

주요 옵션

옵션설명
-A / --print-file-name각 심볼 앞에 파일명 출력 (아카이브 분석 시 유용)
-BBSD 출력 포맷
-C / --demangleC++ 심볼 디맹글링 (=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 / CONFIG_KALLSYMS_ALL: CONFIG_KALLSYMS=y이면 심볼 이름이 커널 이미지 내 .kallsyms 섹션에 내장되어 OOPS 메시지에서 주소 대신 심볼명이 자동 출력됩니다. CONFIG_KALLSYMS_ALL=y이면 static 함수·변수까지 모두 포함합니다. 두 옵션이 모두 꺼지면 커널 패닉 콜 트레이스에 주소만 표시되므로 반드시 vmlinuxaddr2line으로 수동 변환해야 합니다.

심볼 가시성 (Symbol Visibility)

ELF는 심볼마다 가시성(visibility) 속성을 갖습니다. 이 속성은 링크 시 심볼이 다른 오브젝트에 어떻게 보이는지를 결정하며, 공유 라이브러리·커널 모듈 설계에 직접 영향을 줍니다.

가시성설명커널 활용
STV_DEFAULT0바인딩(전역/약한)에 따라 결정. 일반 공개 심볼.EXPORT_SYMBOL, EXPORT_SYMBOL_GPL
STV_HIDDEN2이 모듈 내부에서만 사용. 외부 참조·재배치 불가.__attribute__((visibility("hidden")))
STV_PROTECTED3외부에서 참조 가능하지만 인터포징(interposing) 불가.드물게 사용
STV_INTERNAL1프로세서별 정의. 플랫폼 내부 전용.아키텍처 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 / --stabsstabs 형식 디버깅 정보 출력
-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_RELElf64_Relr_offset, r_infoaddend 없음 — 수정할 위치의 값 자체를 addend로 사용
SHT_RELAElf64_Relar_offset, r_info, r_addend명시적 addend 포함 — x86-64·AArch64에서 주로 사용

x86-64 주요 재배치 타입

타입크기계산식용도
R_X86_64_NONE0없음패딩/무시
R_X86_64_64164bitS + A절대 64비트 주소
R_X86_64_PC32232bitS + A − PPC 상대 32비트 (call/jmp)
R_X86_64_GOT32332bitG + AGOT 엔트리 오프셋
R_X86_64_PLT32432bitL + A − PPLT 엔트리 상대 주소
R_X86_64_GLOB_DAT664bitSGOT 슬롯을 심볼 주소로 채움
R_X86_64_JUMP_SLOT764bitSPLT 슬롯 (지연 바인딩)
R_X86_64_RELATIVE864bitB + A베이스 상대 — PIE/DSO 재배치
R_X86_64_321032bitS + A절대 32비트 (zero-extend)
R_X86_64_32S1132bitS + A절대 32비트 (sign-extend)
R_X86_64_PC642464bitS + A − PPC 상대 64비트
R_X86_64_GOTPCREL932bitG + GOT + A − PGOT 엔트리의 PC 상대 주소
R_X86_64_TPOFF322332bitS + A − tpTLS Initial Exec 오프셋
R_X86_64_TLSGD1932bittlsgd + A − PTLS General Dynamic 모델
R_X86_64_IRELATIVE3764bitifunc(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_CALL26BL 명령어용 26비트 PC 상대 분기 (±128MB)
R_AARCH64_JUMP26B 명령어용 26비트 PC 상대 분기
R_AARCH64_ADR_PREL_PG_HI21ADRP용 페이지 상대 21비트 (상위)
R_AARCH64_ADD_ABS_LO12_NCADD용 하위 12비트 오프셋
R_AARCH64_RELATIVE베이스 상대 재배치 (PIE)
R_AARCH64_GLOB_DATGOT 슬롯 채우기
R_AARCH64_JUMP_SLOTPLT 지연 바인딩

재배치 엔트리 읽기

# 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)를 통해 지연 바인딩됩니다.

실행 파일 call printf@plt PLT printf@plt[0] jmp *GOT[n] push index jmp PLT[0] GOT GOT[0] → dynamic GOT[1] → link_map GOT[n] → (초기: PLT+6) ld.so _dl_runtime_resolve libc.so printf() 실제 주소 첫 번째 호출 흐름 ① call printf@plt ② PLT: jmp GOT[n] → PLT+6 (아직 미해석) ③ ld.so가 printf 주소 해석 → GOT[n] 갱신 이후 호출: GOT[n]이 printf 주소를 직접 가리킴 (fast path) 첫 호출 GOT[n] 갱신

커널에서의 재배치 — 정적 재배치 테이블

리눅스 커널은 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=yKASLR — 부팅마다 주소 무작위화재배치 테이블 + 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
PIE 커널(ARM64) vs 비PIE(x86-64) 차이
  • 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-headerELF 파일 헤더 출력 (매직 넘버, 아키텍처, 엔트리 포인트 등)
-l / --program-headers프로그램 헤더(세그먼트) 출력
-S / --section-headers섹션 헤더 목록 출력
-g / --section-groups섹션 그룹 출력
-t / --section-details섹션 상세 정보 (-S 확장)
-e / --headers파일·프로그램·섹션 헤더 모두 출력
-s / --syms심볼 테이블 출력 (.symtab 및 .dynsym)
--dyn-syms동적 심볼 테이블만 출력
--lto-symsLTO 심볼 테이블 출력
-n / --notesNote 섹션 출력 (빌드 ID, GNU 속성 등)
-r / --relocs재배치 섹션 출력
-u / --unwind언와인드 정보 출력 (.eh_frame)
-d / --dynamic동적 섹션 출력 (공유 라이브러리 의존성 등)
-V / --version-info심볼 버전 정보 출력
-A / --arch-specific아키텍처별 정보 출력
-c / --archive-index아카이브 심볼 인덱스 출력
-D / --use-dynamic심볼·재배치 출력 시 동적 섹션 사용
-L / --lintELF 구조 경고 출력
-x 번호|이름지정 섹션을 16진수로 덤프
-p 번호|이름지정 섹션을 문자열로 덤프
-R 번호|이름지정 섹션을 재배치 적용 후 16진수 덤프
-w[lLiaprmfFsoORtUuTgk]DWARF 디버깅 정보 출력 (세부 섹션 선택 가능)
--debug-dump[=세부항목]DWARF 상세 덤프 (-w의 긴 형식)
--dwarf-depth=깊이DWARF 다이 출력 깊이 제한
--dwarf-start=번호특정 다이부터 출력
-I / --histogram심볼 버킷 히스토그램 출력
-C / --demangleC++ 심볼 디맹글링
--sym-base=0|8|10|16심볼 주소 출력 기수
-W / --wide80컬럼 이상으로 출력 (잘림 없음)
-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: 리눅스 커널 소스에는 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/5DWARF 버전 선택. GDB 9+ / LLVM 기반 도구는 DWARF5 권장
ORC unwinderCONFIG_UNWINDER_ORC=y — DWARF 대신 ORC 테이블 사용. 더 빠르고 정확하나 addr2line은 여전히 DWARF에 의존
CONFIG_DEBUG_INFO 필요성
  • 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=sysvSysV 포맷: 각 섹션별 상세 크기 출력
-B / --format=berkeleyBerkeley 포맷: text+data+bss+합계+16진합계 (기본)
-G / --format=gnuGNU 포맷: Berkeley와 유사하나 할당된 섹션만 집계
--common공통(common) 심볼 크기를 bss에 포함
-d / --radix=1010진수로 크기 출력
-o / --radix=88진수로 크기 출력
-x / --radix=1616진수로 크기 출력
-t / --totalsBerkeley 포맷에서 합계 행 추가 출력
--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 s vs ranlib: ar crs libfoo.a *.o처럼 ars 플래그를 주면 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=ABIe_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 페이지를 참고하세요.

상세 참조: 커맨드라인 옵션 19개, GAS 문법 규칙, 어셈블러 지시자 완전 참조, CFI 지시자 25개, x86/AArch64/RISC-V 아키텍처별 기능은 GNU Assembler (as) 완전 가이드를 참고하세요.

기본 사용법

# 어셈블리 파일을 오브젝트로 변환
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 / .quad1/2/4/8 바이트 데이터 직접 기재
.string "text"NULL 종료 문자열 삽입
.align nn 바이트 경계 정렬 (또는 2^n, 아키텍처별 상이)
.balign nn 바이트 경계 정렬 (바이트 단위, 이식성↑)
.fill count, size, valcount×size 바이트를 val로 채움
.comm sym, size, alignBSS 공통 심볼 선언
.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=filePT_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 relroPartial RELRO재배치 완료 후 .got 등 섹션을 읽기 전용으로 변경 (PT_GNU_RELRO 세그먼트 추가)
-z nowFull RELRO모든 심볼을 시작 시 즉시 바인딩 (DT_BIND_NOW). -z relro와 함께 사용하면 Full RELRO
-z noexecstackNX 스택스택 세그먼트에 실행 권한 제거 (PT_GNU_STACKRW만 설정)
-z execstack실행 가능 스택스택 실행 허용 (레거시 호환, 보안상 비권장)
-z stacksize=n스택 크기 힌트PT_GNU_STACK에 스택 크기 힌트 기록
-z nodlopendlopen 방지공유 라이브러리를 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_NULL0미사용 엔트리
PT_LOAD1로드 가능한 세그먼트 (코드, 데이터)
PT_DYNAMIC2동적 링킹 정보 (.dynamic)
PT_INTERP3동적 링커 경로 (.interp)
PT_NOTE4보조 정보 (.note.*)
PT_PHDR6프로그램 헤더 테이블 자체
PT_TLS7스레드 로컬 스토리지
PT_GNU_RELRO0x6474e552RELRO 읽기 전용 범위
PT_GNU_STACK0x6474e551스택 실행 권한 힌트

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_PC3232비트 PC 상대 범위 초과 (±2GB)코드 모델 변경(-mcmodel=large) 또는 레이아웃 조정
cannot find -lfoo라이브러리 파일 미존재-L경로 추가
section overlap링커 스크립트 주소 충돌섹션 주소/크기 재검토
cannot open linker script: No such file-T 옵션에 지정한 스크립트 파일 미존재파일 경로 확인 또는 -L로 검색 경로 추가
warning: orphan sectionSECTIONS에서 다루지 않는 입력 섹션 발생링커 스크립트에 해당 섹션 패턴 추가 또는 /DISCARD/로 제거
region ... overflowed by N bytesMEMORY 영역 크기 초과코드/데이터 최적화 또는 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.textCPU 핫플러그 초기화 코드 (구버전)__cpuinit (제거됨)

심볼 내보내기 섹션

섹션명용도
__ksymtabEXPORT_SYMBOL()로 내보낸 심볼 테이블. 각 엔트리: {값, 이름, 네임스페이스}
__ksymtab_gplEXPORT_SYMBOL_GPL()로 내보낸 GPL 전용 심볼
__kcrctab심볼별 CRC 체크섬 (CONFIG_MODVERSIONS 시 생성). 모듈 ABI 검증용
__kcrctab_gplGPL 심볼의 CRC 테이블
__ksymtab_strings심볼 이름 문자열 풀

섹션 배열 패턴 (linker set)

커널은 링커가 섹션 내 항목들을 자동으로 수집하는 패턴을 광범위하게 사용합니다.

섹션명용도관련 매크로
__param모듈 파라미터 (module_param())MODULE_PARAM_DESC
__tracepointsftrace/perf 트레이스포인트 디스크립터DEFINE_TRACE
__jump_tableJump label (static branch) 엔트리DEFINE_STATIC_KEY_*
__bug_tableBUG()/WARN() 위치 정보 테이블BUG_ON()
.altinstructions대체 명령어 패치 테이블 (CPU 특성 기반)ALTERNATIVE()
__ex_table예외 테이블 (fixup 핸들러 주소)_ASM_EXTABLE
.smp_locksLOCK 접두사 패치 위치 목록 (단일 CPU 최적화)LOCK_PREFIX
__patchable_function_entriesftrace 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
커널 ELF 섹션 디버깅 팁
  • __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 도구가 핵심 역할을 담당합니다.

*.c 컴파일 gcc -c → *.o 링크 ld + vmlinux.lds.S vmlinux (ELF) 심볼·DWARF 포함 System.map nm -n vmlinux | sort vmlinux.bin objcopy -O binary vmlinux.bin.gz gzip / zstd 압축 bzImage arch/x86/boot 조합

단계별 상세

# 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-64bzImageobjcopy + arch/x86/boot부트 섹터 + 설정 헤더 + 압축 vmlinux
ARM64Image / Image.gzobjcopy -O binary + gzipEFI stub 내장, 헤더 64바이트
ARM (32비트)zImage / uImageobjcopy + mkimageU-Boot용 uImage는 mkimage로 래핑
RISC-VImage / Image.gzobjcopy -O binary + gzipARM64와 유사한 구조
MIPSvmlinuzobjcopy + 부트래퍼플랫폼마다 다름
vmlinux 보관 필요성: 배포된 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                      # 로드 결과 확인

참고 자료

관련 페이지