ELF (Executable and Linkable Format)
ELF(Executable and Linkable Format)는 System V ABI에서 정의한 바이너리 형식으로, Linux에서 실행 파일, 공유 라이브러리(Shared Library), 오브젝트 파일, 코어 덤프(Core Dump)의 표준 형식입니다. 이 문서는 ELF 포맷의 전체 구조(헤더, 세그먼트, 섹션, 심볼 테이블(Symbol Table), 재배치(Relocation), 동적 링킹(Dynamic Linking), GOT/PLT), exec() 실행 흐름, Auxiliary Vector, ASLR/PIE, vDSO, TLS(Thread-Local Storage), 코어 덤프, 커널 모듈(.ko) ELF 구조, binfmt 핸들러(Handler), 심볼 버저닝, 스택 언와인딩(.eh_frame/DWARF/ORC), 링커(Linker) 스크립트, ELF 보안 강화 기법을 다룹니다.
핵심 요약
- 이중 관점 — ELF는 링킹 뷰(섹션)와 실행 뷰(세그먼트)의 두 가지 시각으로 해석됩니다.
- 헤더 체계 — ELF 헤더 → 프로그램 헤더(세그먼트) → 섹션 헤더의 3단 구조를 이해합니다.
- 심볼과 재배치 — 코드가 외부 함수/변수를 참조하는 방법(GOT/PLT)을 파악합니다.
- 동적 링킹 — 공유 라이브러리가 런타임에 로드되고 심볼이 해석되는 과정을 추적합니다.
- 보안 메커니즘 — PIE, ASLR, RELRO, NX 등 ELF 기반 보안 강화 기법을 확인합니다.
단계별 이해
- 파일 구조 파악
readelf -h로 ELF 헤더를 읽고, 파일 타입(ET_EXEC/ET_DYN)과 아키텍처를 확인합니다. - 세그먼트와 섹션 확인
readelf -l(세그먼트)과readelf -S(섹션)으로 메모리 배치와 링킹 구조를 봅니다. - 심볼과 의존성 추적
readelf -s(심볼)과readelf -d(동적 섹션)로 외부 참조와 라이브러리 의존성을 파악합니다. - 런타임 동작 확인
ldd,LD_DEBUG=all,/proc/pid/maps로 실제 로딩과 매핑(Mapping) 상태를 검증합니다.
ELF 포맷 개요 (ELF Format Overview)
ELF는 System V ABI에서 정의한 바이너리 형식으로, 실행 파일, 공유 라이브러리, 오브젝트 파일, 코어 덤프를 모두 표현합니다. ELF 파일은 크게 세 부분으로 구성됩니다:
ELF 파일의 유형은 e_type 필드로 구분됩니다:
| 타입 | 값 | 설명 | 용도 |
|---|---|---|---|
ET_REL | 1 | 재배치 가능 파일 (.o) | 링커 입력, 커널 모듈(.ko) |
ET_EXEC | 2 | 실행 파일 (고정 주소) | 비-PIE 바이너리, 정적 링크 |
ET_DYN | 3 | 공유 오브젝트 | 공유 라이브러리(.so), PIE 실행 파일 |
ET_CORE | 4 | 코어 덤프 | 프로세스 크래시 분석 |
PIE와 ET_DYN: 현대 배포판에서 기본 컴파일 옵션(-fPIE -pie)으로 빌드된 실행 파일은 ET_DYN 타입입니다. 커널은 load_elf_binary()에서 ET_DYN이면서 인터프리터가 있으면 ASLR 적용 대상으로 판단하여 랜덤 로드 주소를 배정합니다. ET_EXEC 바이너리는 ELF 헤더에 지정된 고정 가상 주소(Virtual Address)에 로드됩니다.
ELF 헤더 (ELF Header)
모든 ELF 파일은 파일 오프셋(Offset) 0에서 시작하는 ELF 헤더를 가집니다. 이 헤더는 파일의 전체적인 속성과 나머지 헤더 테이블의 위치를 지정합니다.
/* include/uapi/linux/elf.h */
typedef struct elf64_hdr {
unsigned char e_ident[EI_NIDENT]; /* ELF 식별자 (16바이트) */
Elf64_Half e_type; /* 파일 타입: ET_EXEC, ET_DYN 등 */
Elf64_Half e_machine; /* 아키텍처: EM_X86_64, EM_AARCH64 등 */
Elf64_Word e_version; /* ELF 버전 (항상 EV_CURRENT = 1) */
Elf64_Addr e_entry; /* 엔트리 포인트 가상 주소 */
Elf64_Off e_phoff; /* 프로그램 헤더 테이블의 파일 오프셋 */
Elf64_Off e_shoff; /* 섹션 헤더 테이블의 파일 오프셋 */
Elf64_Word e_flags; /* 프로세서별 플래그 */
Elf64_Half e_ehsize; /* ELF 헤더 크기 (64바이트) */
Elf64_Half e_phentsize; /* 프로그램 헤더 엔트리 크기 */
Elf64_Half e_phnum; /* 프로그램 헤더 엔트리 수 */
Elf64_Half e_shentsize; /* 섹션 헤더 엔트리 크기 */
Elf64_Half e_shnum; /* 섹션 헤더 엔트리 수 */
Elf64_Half e_shstrndx; /* 섹션 이름 문자열 테이블 인덱스 */
} Elf64_Ehdr;
e_ident 배열(16바이트)은 ELF 파일을 식별하는 매직 넘버와 기본 속성을 담고 있습니다:
| 인덱스 | 이름 | 값 | 설명 |
|---|---|---|---|
| 0~3 | EI_MAG0~3 | 0x7f 'E' 'L' 'F' | ELF 매직 넘버 |
| 4 | EI_CLASS | 1=32bit, 2=64bit | 주소 크기 클래스 |
| 5 | EI_DATA | 1=LE, 2=BE | 바이트 순서(엔디안) |
| 6 | EI_VERSION | 1 (EV_CURRENT) | ELF 규격 버전 |
| 7 | EI_OSABI | 0=ELFOSABI_NONE | OS/ABI 식별 (Linux는 보통 0) |
| 8~15 | EI_ABIVERSION~ | 0 | 패딩 (예약) |
# readelf로 ELF 헤더 확인
$ readelf -h /bin/ls
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Entry point address: 0x6b10
Start of program headers: 64 (bytes into file)
Start of section headers: 140224 (bytes into file)
Number of program headers: 13
Number of section headers: 31
프로그램 헤더 — 세그먼트 (Program Headers)
프로그램 헤더 테이블은 커널이 프로세스 이미지를 생성할 때 사용하는 세그먼트(segment) 정보를 담고 있습니다. 커널의 load_elf_binary()는 이 테이블을 순회하면서 각 세그먼트를 처리합니다.
/* include/uapi/linux/elf.h */
typedef struct elf64_phdr {
Elf64_Word p_type; /* 세그먼트 타입 */
Elf64_Word p_flags; /* 세그먼트 권한: PF_R|PF_W|PF_X */
Elf64_Off p_offset; /* 파일 내 세그먼트 시작 오프셋 */
Elf64_Addr p_vaddr; /* 메모리에 매핑될 가상 주소 */
Elf64_Addr p_paddr; /* 물리 주소 (사용되지 않음) */
Elf64_Xword p_filesz; /* 파일 내 세그먼트 크기 */
Elf64_Xword p_memsz; /* 메모리 내 세그먼트 크기 (≥ p_filesz) */
Elf64_Xword p_align; /* 정렬 요구사항 */
} Elf64_Phdr;
커널이 처리하는 주요 세그먼트 타입:
| 타입 | 값 | 커널 처리 | 설명 |
|---|---|---|---|
PT_LOAD | 1 | elf_map()으로 mmap | 메모리에 매핑되는 로드 가능 세그먼트. 코드(.text, R-X)와 데이터(.data/.bss, RW-) 영역 |
PT_INTERP | 3 | 인터프리터 경로 추출 | 동적 링커 경로 (예: /lib64/ld-linux-x86-64.so.2) |
PT_NOTE | 4 | ABI 태그 검증 | 빌드 ID, GNU ABI 태그 등 메타데이터 |
PT_DYNAMIC | 2 | 동적 링커가 사용 | 동적 링킹 정보 (.dynamic 섹션) |
PT_PHDR | 6 | 프로그램 헤더 자체 | 프로그램 헤더 테이블의 위치와 크기 |
PT_GNU_STACK | 0x6474e551 | 스택 실행 권한 결정 | PF_X 없으면 NX 스택 (기본). 커널이 스택 VMA 권한 설정에 사용 |
PT_GNU_RELRO | 0x6474e552 | 동적 링커가 처리 | 재배치 후 읽기 전용(Read-Only)으로 전환될 영역 (.got, .init_array 등) |
p_memsz > p_filesz: .bss 세그먼트(초기화되지 않은 전역 변수)는 파일에 데이터를 저장하지 않으므로 p_filesz가 p_memsz보다 작습니다. 커널은 p_filesz만큼 파일에서 매핑하고, 나머지(p_memsz - p_filesz) 영역은 0으로 초기화된 익명 매핑(anonymous mapping)으로 채웁니다. 이 처리는 load_elf_binary() 내 set_brk()와 padzero()에서 이루어집니다.
섹션 헤더 — 섹션 (Section Headers)
섹션 헤더 테이블은 링커가 오브젝트 파일을 조합할 때 사용하는 정보입니다. 실행 시에는 프로그램 헤더(세그먼트)만 필수이며, 섹션 헤더는 선택 사항입니다. 하지만 디버깅(Debugging)과 심볼 분석에 중요합니다.
/* include/uapi/linux/elf.h */
typedef struct elf64_shdr {
Elf64_Word sh_name; /* 섹션 이름 (.shstrtab 내 오프셋) */
Elf64_Word sh_type; /* 섹션 타입: SHT_PROGBITS, SHT_SYMTAB 등 */
Elf64_Xword sh_flags; /* 플래그: SHF_WRITE, SHF_ALLOC, SHF_EXECINSTR */
Elf64_Addr sh_addr; /* 메모리 내 주소 (로드 가능 시) */
Elf64_Off sh_offset; /* 파일 내 섹션 시작 오프셋 */
Elf64_Xword sh_size; /* 섹션 크기 */
Elf64_Word sh_link; /* 연관된 섹션 인덱스 */
Elf64_Word sh_info; /* 추가 정보 */
Elf64_Xword sh_addralign; /* 정렬 제약 */
Elf64_Xword sh_entsize; /* 테이블 엔트리 크기 (고정 크기 테이블인 경우) */
} Elf64_Shdr;
주요 섹션과 그 역할:
| 섹션 | 타입 | 플래그 | 설명 |
|---|---|---|---|
.text | PROGBITS | AX | 실행 가능 코드 |
.rodata | PROGBITS | A | 읽기 전용 데이터 (문자열 리터럴, const 변수) |
.data | PROGBITS | WA | 초기화된 전역/정적 변수 |
.bss | NOBITS | WA | 0 초기화 전역/정적 변수 (파일 공간 차지 안 함) |
.plt | PROGBITS | AX | Procedure Linkage Table — 지연(Latency) 바인딩 트램펄린 |
.got | PROGBITS | WA | Global Offset Table — 전역 심볼의 런타임 주소 |
.got.plt | PROGBITS | WA | PLT 전용 GOT 엔트리 |
.dynamic | DYNAMIC | WA | 동적 링킹 메타데이터 (DT_NEEDED, DT_SYMTAB 등) |
.symtab | SYMTAB | - | 심볼 테이블 (strip 시 제거됨) |
.dynsym | DYNSYM | A | 동적 심볼 테이블 (런타임 필수, strip 불가) |
.strtab | STRTAB | - | 심볼 이름 문자열 테이블 |
.rela.plt | RELA | A | PLT 엔트리 재배치 정보 |
.rela.dyn | RELA | A | 동적 재배치 정보 |
.init_array | INIT_ARRAY | WA | 초기화 함수 포인터 배열 (main() 전 실행) |
.fini_array | FINI_ARRAY | WA | 종료 함수 포인터 배열 (main() 후 실행) |
.note.gnu.build-id | NOTE | A | 빌드 ID 해시 (디버그 심볼 매칭용) |
세그먼트 vs 섹션: 하나의 세그먼트(PT_LOAD)는 여러 섹션을 포함할 수 있습니다. 예를 들어, 코드 세그먼트(R-X)에는 .text, .rodata, .plt 등이, 데이터 세그먼트(RW-)에는 .data, .bss, .got 등이 포함됩니다. 커널은 세그먼트 단위로 mmap()하므로, 같은 세그먼트에 속한 섹션은 동일한 메모리 보호 속성을 갖습니다.
섹션 플래그 상세 (Section Flags)
sh_flags 필드는 섹션의 메모리 속성과 링킹 동작을 비트마스크로 정의합니다:
| 플래그 | 약어 | 값 | 설명 |
|---|---|---|---|
SHF_WRITE | W | 0x1 | 런타임에 쓰기 가능 — .data, .bss, .got |
SHF_ALLOC | A | 0x2 | 런타임에 메모리에 로드됨 — 비할당 섹션(.symtab, .debug_*)은 이 플래그 없음 |
SHF_EXECINSTR | X | 0x4 | 실행 가능 코드 — .text, .plt, .init |
SHF_MERGE | M | 0x10 | 병합 가능한 동일 요소 — 링커가 중복 문자열/상수를 병합 (.rodata.str1.1) |
SHF_STRINGS | S | 0x20 | NUL 종료 문자열로 구성 — SHF_MERGE와 함께 사용하여 문자열 풀링 |
SHF_INFO_LINK | I | 0x40 | sh_info가 섹션 인덱스를 참조 — .rela.* 섹션에서 대상 섹션 지정 |
SHF_GROUP | G | 0x200 | 섹션 그룹의 멤버 — C++ 템플릿/인라인 함수(Inline Function)의 COMDAT 그룹핑 |
SHF_TLS | T | 0x400 | Thread-Local Storage 데이터 — .tdata, .tbss |
SHF_COMPRESSED | C | 0x800 | 섹션 데이터가 압축됨 (ELF 규격 압축, objcopy --compress-debug-sections) |
SHF_GNU_RETAIN | R | 0x200000 | 가비지 컬렉션 시 제거하지 않음 (--gc-sections에서 보존) |
# 섹션 플래그 확인
$ readelf -S /bin/ls | head -20
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318 000000000000001c 0000000000000000 A 0 0 1
[ 5] .dynsym DYNSYM 00000000000003d8 000003d8 0000000000000ae8 0000000000000018 A 6 1 8
[13] .plt PROGBITS 0000000000004000 00004000 0000000000000460 0000000000000010 AX 0 0 16
[16] .text PROGBITS 0000000000004470 00004470 00000000000130b4 0000000000000000 AX 0 0 16
[18] .rodata PROGBITS 0000000000018000 00018000 0000000000005308 0000000000000000 A 0 0 32
[25] .data PROGBITS 0000000000024060 00024060 0000000000000268 0000000000000000 WA 0 0 32
[26] .bss NOBITS 00000000000242c8 000242c8 0000000000001298 0000000000000000 WA 0 0 8
# 플래그 약어: W=Write, A=Alloc, X=eXecute, M=Merge, S=Strings
# I=Info-link, G=Group, T=TLS, C=Compressed, R=Retain
# 비할당 섹션(A 없음): .symtab, .strtab, .debug_* → strip 시 제거 가능
섹션 타입 상세 (Section Types)
| 타입 | 값 | 설명 | 대표 섹션 |
|---|---|---|---|
SHT_NULL | 0 | 미사용 (인덱스 0의 더미 섹션) | 섹션 0 |
SHT_PROGBITS | 1 | 프로그램 정의 데이터 (범용) | .text, .data, .rodata |
SHT_SYMTAB | 2 | 심볼 테이블 | .symtab |
SHT_STRTAB | 3 | 문자열 테이블 | .strtab, .dynstr, .shstrtab |
SHT_RELA | 4 | 명시적 addend 재배치 | .rela.text, .rela.dyn |
SHT_HASH | 5 | 심볼 해시 테이블 (SYSV) | .hash |
SHT_DYNAMIC | 6 | 동적 링킹 정보 | .dynamic |
SHT_NOTE | 7 | 노트 정보 (빌드 ID, ABI 등) | .note.* |
SHT_NOBITS | 8 | 파일 공간 없는 섹션 (0 초기화) | .bss, .tbss |
SHT_REL | 9 | 암시적 addend 재배치 | .rel.text (32비트) |
SHT_DYNSYM | 11 | 동적 심볼 테이블 | .dynsym |
SHT_INIT_ARRAY | 14 | 초기화 함수 포인터 배열 | .init_array |
SHT_FINI_ARRAY | 15 | 종료 함수 포인터 배열 | .fini_array |
SHT_GROUP | 17 | 섹션 그룹 (COMDAT) | C++ 템플릿 중복 제거 |
SHT_GNU_HASH | 0x6ffffff6 | GNU 해시 테이블 | .gnu.hash |
SHT_GNU_VERSYM | 0x6fffffff | 심볼 버전 인덱스 | .gnu.version |
SHT_GNU_VERNEED | 0x6ffffffe | 필요한 심볼 버전 | .gnu.version_r |
심볼 테이블 (Symbol Table)
심볼 테이블은 프로그램에서 사용되는 함수, 변수, 섹션 등의 이름과 속성을 기록합니다. ELF에는 두 종류의 심볼 테이블이 있습니다: .symtab(전체 심볼, strip 시 제거 가능)과 .dynsym(동적 심볼, 런타임 필수이므로 제거 불가).
/* include/uapi/linux/elf.h */
typedef struct elf64_sym {
Elf64_Word st_name; /* 심볼 이름 (.strtab/.dynstr 내 오프셋) */
unsigned char st_info; /* 바인딩(상위 4비트) + 타입(하위 4비트) */
unsigned char st_other; /* 가시성 (하위 2비트) */
Elf64_Half st_shndx; /* 심볼이 속한 섹션 인덱스 */
Elf64_Addr st_value; /* 심볼 값 (주소 또는 오프셋) */
Elf64_Xword st_size; /* 심볼 크기 (바이트) */
} Elf64_Sym;
/* st_info 매크로 */
#define ELF64_ST_BIND(info) ((info) >> 4)
#define ELF64_ST_TYPE(info) ((info) & 0xf)
#define ELF64_ST_INFO(bind, type) (((bind) << 4) + ((type) & 0xf))
/* st_other 매크로 */
#define ELF64_ST_VISIBILITY(other) ((other) & 0x3)
심볼 바인딩(Binding) — 심볼의 링킹 범위를 결정합니다:
| 바인딩 | 값 | 설명 |
|---|---|---|
STB_LOCAL | 0 | 파일 내부에서만 참조 가능. static 함수/변수. 다른 오브젝트 파일의 동일 이름 심볼과 충돌하지 않음 |
STB_GLOBAL | 1 | 모든 오브젝트 파일에서 참조 가능. 정의가 하나만 존재해야 함 (다중 정의 시 링커 에러) |
STB_WEAK | 2 | 전역 심볼과 유사하나 우선순위(Priority)가 낮음. 같은 이름의 STB_GLOBAL 심볼이 있으면 대체됨. 미해석 시 에러 아님 (0으로 처리) |
심볼 타입(Type) — 심볼이 참조하는 엔티티의 종류:
| 타입 | 값 | 설명 |
|---|---|---|
STT_NOTYPE | 0 | 타입 미지정 |
STT_OBJECT | 1 | 데이터 오브젝트 (변수, 배열 등) |
STT_FUNC | 2 | 함수 또는 실행 가능 코드 |
STT_SECTION | 3 | 섹션 자체를 나타내는 심볼 |
STT_FILE | 4 | 소스 파일 이름 |
STT_COMMON | 5 | 미초기화 공통 블록 (Fortran COMMON, C의 tentative definition) |
STT_TLS | 6 | Thread-Local Storage 변수 |
STT_GNU_IFUNC | 10 | GNU 간접 함수 — 런타임에 CPU 기능에 따라 최적 구현 선택 (예: memcpy의 AVX/SSE 분기) |
심볼 가시성(Visibility) — 동적 링킹에서의 심볼 노출 범위를 제어합니다:
| 가시성 | 값 | 설명 |
|---|---|---|
STV_DEFAULT | 0 | 기본 가시성. 바인딩 규칙에 따라 동적 심볼로 내보내짐 |
STV_INTERNAL | 1 | 프로세서별 숨김 규칙 적용 (거의 사용되지 않음) |
STV_HIDDEN | 2 | 동적 심볼 테이블에 포함되지 않음. -fvisibility=hidden과 동일 효과 |
STV_PROTECTED | 3 | 외부에서 참조 가능하나, 같은 공유 라이브러리 내에서는 심볼 인터포지션 불가 |
특수 섹션 인덱스 (st_shndx):
| 상수 | 값 | 설명 |
|---|---|---|
SHN_UNDEF | 0 | 미정의 심볼 — 다른 오브젝트/라이브러리에서 정의 필요 |
SHN_ABS | 0xfff1 | 절대 주소 — 재배치에 영향받지 않음 |
SHN_COMMON | 0xfff2 | COMMON 블록 — 링커가 .bss에 할당 |
# 심볼 테이블 확인 예시
$ readelf -s /bin/ls | head -20
Symbol table '.dynsym' contains 127 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND abort@GLIBC_2.2.5
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __errno_location@GLIBC_2.2.5
...
60: 000000000001f060 920 FUNC GLOBAL DEFAULT 16 main
61: 0000000000023400 8 OBJECT GLOBAL DEFAULT 26 stdout
# Weak 심볼 예시 — 정의 없어도 링킹 성공
$ nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep ' W '
0000000000044f10 W pthread_cond_signal@@GLIBC_2.3.2
0000000000044a00 W pthread_mutex_lock@@GLIBC_2.2.5
# IFUNC 심볼 — 런타임 최적 구현 선택
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep IFUNC | head -5
132: 00000000001a3700 0 IFUNC GLOBAL DEFAULT 16 memcpy@@GLIBC_2.14
156: 00000000001a37a0 0 IFUNC GLOBAL DEFAULT 16 memset@@GLIBC_2.2.5
173: 00000000001a37f0 0 IFUNC GLOBAL DEFAULT 16 strcmp@@GLIBC_2.2.5
.symtab vs .dynsym: .symtab은 모든 심볼(로컬 포함)을 담는 완전한 심볼 테이블로, 디버깅과 분석에 사용됩니다. strip 명령으로 제거할 수 있습니다. .dynsym은 동적 링킹에 필요한 심볼만 포함하며, SHF_ALLOC 플래그가 설정되어 런타임에 메모리에 로드됩니다. 실행 파일에서 strip해도 .dynsym은 유지됩니다.
문자열 테이블 (String Tables)
ELF의 문자열 테이블은 NUL(\0)로 종료되는 문자열의 연속 배열입니다. 심볼 이름, 섹션 이름 등은 문자열을 직접 저장하지 않고, 해당 문자열 테이블 내의 바이트 오프셋을 참조합니다.
| 문자열 테이블 | 용도 | 참조 위치 |
|---|---|---|
.strtab | 정적 심볼 이름 | .symtab의 st_name 필드 |
.dynstr | 동적 심볼 이름, 라이브러리 이름 | .dynsym의 st_name, .dynamic의 DT_NEEDED 등 |
.shstrtab | 섹션 이름 | 섹션 헤더의 sh_name 필드. ELF 헤더의 e_shstrndx가 이 섹션의 인덱스 |
오프셋: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
내용: \0 m a i n \0 p r i n t f \0 _ s t a r t \0
st_name = 1 → "main"
st_name = 6 → "printf"
st_name = 13 → "_start"
st_name = 0 → "" (빈 문자열, 이름 없는 심볼)
재배치 (Relocation)
재배치(Relocation)는 링커(정적) 또는 동적 링커(런타임)가 심볼 참조를 최종 주소로 해석하는 과정입니다. 오브젝트 파일에서 아직 결정되지 않은 주소(외부 함수, 전역 변수 등)는 재배치 엔트리로 기록되며, 링킹 시 실제 주소로 패치(Patch)됩니다.
/* include/uapi/linux/elf.h */
/* Rel: 암시적 addend (코드 내 기존 값을 addend로 사용) */
typedef struct elf64_rel {
Elf64_Addr r_offset; /* 패치할 위치 (섹션 내 오프셋 또는 가상 주소) */
Elf64_Xword r_info; /* 심볼 인덱스(상위 32비트) + 재배치 타입(하위 32비트) */
} Elf64_Rel;
/* Rela: 명시적 addend 포함 (x86_64에서 표준) */
typedef struct elf64_rela {
Elf64_Addr r_offset; /* 패치할 위치 */
Elf64_Xword r_info; /* 심볼 인덱스 + 재배치 타입 */
Elf64_Sxword r_addend; /* 주소 계산에 더해지는 상수 */
} Elf64_Rela;
#define ELF64_R_SYM(info) ((info) >> 32)
#define ELF64_R_TYPE(info) ((info) & 0xffffffff)
x86_64 주요 재배치 타입:
| 타입 | 값 | 계산식 | 용도 |
|---|---|---|---|
R_X86_64_64 | 1 | S + A | 절대 64비트 주소 (데이터 포인터) |
R_X86_64_PC32 | 2 | S + A - P | PC 상대 32비트 (근거리 call/jmp) |
R_X86_64_PLT32 | 4 | L + A - P | PLT를 통한 함수 호출 |
R_X86_64_COPY | 5 | — | 실행 파일의 .bss에 공유 라이브러리 데이터 심볼 복사 |
R_X86_64_GLOB_DAT | 6 | S | GOT 엔트리에 심볼 절대 주소 저장 |
R_X86_64_JUMP_SLOT | 7 | S | PLT용 GOT 엔트리 (지연 바인딩 대상) |
R_X86_64_RELATIVE | 8 | B + A | PIE/공유 라이브러리의 내부 참조 (base address 보정) |
R_X86_64_TPOFF32 | 23 | S + A - TP | TLS 변수의 TP(Thread Pointer) 상대 오프셋 |
계산식에서: S = 심볼 값, A = addend, P = 패치 위치 주소, B = base address, L = PLT 엔트리 주소, TP = Thread Pointer.
커널 모듈의 재배치: 커널 모듈(.ko)은 ET_REL 타입이므로, 커널의 load_module()이 직접 재배치를 수행합니다. 아키텍처별 apply_relocate_add() 함수가 각 .rela.* 섹션을 처리하여 모듈 코드 내 심볼 참조를 커널 심볼(Kernel Symbol)의 실제 주소로 패치합니다. 이 과정에서 __ksymtab에 등록된 커널 심볼 테이블을 검색합니다.
동적 섹션 (.dynamic)
.dynamic 섹션은 동적 링커가 사용하는 메타데이터 테이블입니다. PT_DYNAMIC 세그먼트가 이 섹션을 가리키며, 런타임에 메모리에 로드되어 동적 링커가 라이브러리 로딩, 심볼 해석, 재배치를 수행하는 데 필요한 모든 정보를 제공합니다.
/* include/uapi/linux/elf.h */
typedef struct elf64_dyn {
Elf64_Sxword d_tag; /* 엔트리 타입 (DT_*) */
union {
Elf64_Xword d_val; /* 정수 값 */
Elf64_Addr d_ptr; /* 주소 값 */
} d_un;
} Elf64_Dyn;
주요 동적 태그:
| 태그 | 값 | d_un | 설명 |
|---|---|---|---|
DT_NEEDED | 1 | d_val | 필요한 공유 라이브러리 이름 (.dynstr 오프셋). 동적 링커가 재귀적으로 로드 |
DT_PLTRELSZ | 2 | d_val | PLT 재배치 (.rela.plt) 전체 크기 |
DT_PLTGOT | 3 | d_ptr | .got.plt 테이블 주소 |
DT_STRTAB | 5 | d_ptr | 동적 문자열 테이블 (.dynstr) 주소 |
DT_SYMTAB | 6 | d_ptr | 동적 심볼 테이블 (.dynsym) 주소 |
DT_RELA | 7 | d_ptr | .rela.dyn 재배치 테이블 주소 |
DT_RELASZ | 8 | d_val | .rela.dyn 테이블 전체 크기 (바이트) |
DT_INIT | 12 | d_ptr | 초기화 함수 주소 (_init) |
DT_FINI | 13 | d_ptr | 종료 함수 주소 (_fini) |
DT_SONAME | 14 | d_val | 공유 라이브러리의 SONAME (.dynstr 오프셋) |
DT_JMPREL | 23 | d_ptr | PLT 재배치 테이블 (.rela.plt) 주소 |
DT_INIT_ARRAY | 25 | d_ptr | 초기화 함수 포인터 배열 (.init_array) 주소 |
DT_FINI_ARRAY | 26 | d_ptr | 종료 함수 포인터 배열 (.fini_array) 주소 |
DT_RUNPATH | 29 | d_val | 라이브러리 검색 경로 (-Wl,-rpath로 설정) |
DT_FLAGS | 30 | d_val | 플래그: DF_ORIGIN, DF_SYMBOLIC, DF_BIND_NOW, DF_STATIC_TLS |
DT_FLAGS_1 | 0x6ffffffb | d_val | 확장 플래그: DF_1_NOW(즉시 바인딩), DF_1_PIE(PIE 표시) |
DT_GNU_HASH | 0x6ffffef5 | d_ptr | GNU 해시 테이블 주소 (심볼 검색 가속) |
DT_VERNEED | 0x6ffffffe | d_ptr | 심볼 버전 필요(version needed) 테이블. 예: GLIBC_2.17 |
DT_NULL | 0 | — | 동적 섹션 종료 표시 |
# 동적 섹션 확인
$ readelf -d /bin/ls
Dynamic section at offset 0x22df0 contains 28 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libselinux.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x4000
0x000000000000000d (FINI) 0x172e4
0x0000000000000019 (INIT_ARRAY) 0x23b90
0x000000006ffffef5 (GNU_HASH) 0x3a0
0x0000000000000005 (STRTAB) 0xec0
0x0000000000000006 (SYMTAB) 0x3d8
0x000000000000001e (FLAGS) BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW PIE
0x0000000000000000 (NULL) 0x0
GNU 해시 테이블: DT_GNU_HASH는 기존 SYSV 해시(DT_HASH)를 대체하는 고속 심볼 검색 메커니즘입니다. Bloom filter와 해시 버킷을 사용하여 평균 검색 시간을 크게 단축합니다. 현대 Linux 바이너리는 거의 모두 GNU 해시를 사용하며, 동적 링커 시작 시간이 약 50% 감소하는 효과가 있습니다.
ELF 해시 테이블 (Hash Tables)
동적 링커가 심볼을 검색할 때 .dynsym을 선형 탐색하면 O(n)의 비용이 발생합니다. 이를 해결하기 위해 ELF는 해시 테이블을 제공합니다. 초기 SYSV 해시(.hash, DT_HASH)와 이를 대체하는 GNU 해시(.gnu.hash, DT_GNU_HASH) 두 가지 형식이 있습니다.
SYSV 해시 테이블 (.hash)
원래 ELF 규격에 정의된 해시 테이블입니다. 단순한 체인 해시(chained hash) 구조를 사용합니다.
/* SYSV 해시 테이블 레이아웃 */
struct elf_hash_table {
uint32_t nbucket; /* 버킷 수 */
uint32_t nchain; /* 체인 수 (= .dynsym의 심볼 수) */
uint32_t bucket[nbucket]; /* 해시 버킷 → 심볼 인덱스 */
uint32_t chain[nchain]; /* 체인 → 다음 심볼 인덱스 (충돌 해결) */
};
/* SYSV 해시 함수 (ELF spec 정의) */
unsigned long elf_hash(const unsigned char *name) {
unsigned long h = 0, g;
while (*name) {
h = (h << 4) + *name++;
g = h & 0xf0000000;
if (g) h ^= g >> 24;
h &= ~g;
}
return h;
}
/* 검색 알고리즘 */
/* idx = bucket[elf_hash(name) % nbucket]; */
/* while (idx != STN_UNDEF) { */
/* if (strcmp(dynsym[idx].name, name) == 0) return idx; */
/* idx = chain[idx]; */
/* } */
GNU 해시 테이블 (.gnu.hash)
GNU 해시는 Bloom filter와 개선된 해시 함수를 사용하여 SYSV 해시 대비 약 50%의 동적 링킹 시작 시간 개선을 달성합니다. 현대 Linux 바이너리는 거의 모두 이 형식을 사용합니다.
/* GNU 해시 테이블 레이아웃 */
struct gnu_hash_table {
uint32_t nbuckets; /* 버킷 수 */
uint32_t symoffset; /* .dynsym에서 해시 대상 시작 인덱스 */
uint32_t bloom_size; /* Bloom filter 워드 수 */
uint32_t bloom_shift; /* Bloom filter 2차 해시 시프트 값 */
uint64_t bloom[bloom_size]; /* Bloom filter 비트 배열 */
uint32_t buckets[nbuckets]; /* 해시 버킷 → 심볼 인덱스 */
uint32_t chains[]; /* 해시 값 배열 (하위 1비트 = 체인 종료 표시) */
};
/* GNU 해시 함수 (djb2 변형) */
uint32_t gnu_hash(const char *name) {
uint32_t h = 5381;
for (; *name; name++)
h = (h << 5) + h + *name; /* h * 33 + c */
return h;
}
# 해시 테이블 확인
$ readelf -I /bin/ls # 히스토그램 출력 (버킷 분포)
Histogram for `.gnu.hash' bucket list length (total of 31 buckets):
Length Number % of total Coverage
0 1 ( 3.2%)
1 5 ( 16.1%) 4.3%
2 7 ( 22.6%) 16.3%
3 10 ( 32.3%) 42.2%
4 6 ( 19.4%) 62.9%
5 2 ( 6.5%) 71.6%
# 평균 체인 길이가 낮을수록 검색이 빠름
# 해시 스타일 확인
$ readelf -S /bin/ls | grep -E 'hash|gnu.hash'
[ 2] .gnu.hash GNU_HASH ...
# .hash 섹션이 없으면 GNU 해시만 사용 (현대 배포판 기본)
# 링크 시 해시 스타일 지정
$ gcc -Wl,--hash-style=gnu -o test test.c # GNU 해시만 (기본)
$ gcc -Wl,--hash-style=both -o test test.c # 호환성: 둘 다 생성
$ gcc -Wl,--hash-style=sysv -o test test.c # 레거시 SYSV만
GNU 해시의 Bloom filter 상세: Bloom filter는 각 심볼 이름의 해시값에서 2개의 비트 위치를 계산합니다. bit1 = hash % ELFCLASS_BITS와 bit2 = (hash >> bloom_shift) % ELFCLASS_BITS입니다 (64비트에서 ELFCLASS_BITS=64). 해시 테이블의 bloom[(hash/64) % bloom_size] 워드에서 두 비트를 검사하여, 하나라도 0이면 해당 심볼은 확실히 존재하지 않습니다. 이 필터 덕분에 라이브러리 로딩 시 수천 개의 미정의 심볼 조회를 거의 비용 없이 건너뛸 수 있습니다.
ELF 노트 섹션 (Note Sections)
ELF 노트는 벤더별 메타데이터를 구조화된 형태로 저장하는 섹션입니다. .note.* 섹션과 PT_NOTE 세그먼트에 포함되며, 각 노트는 이름-타입-설명의 삼중 구조를 가집니다.
/* include/uapi/linux/elf.h — ELF 노트 헤더 (간략화) */
typedef struct elf64_note {
Elf64_Word n_namesz; /* 이름 문자열 길이 (NUL 포함) */
Elf64_Word n_descsz; /* 설명(descriptor) 데이터 크기 */
Elf64_Word n_type; /* 노트 타입 (이름별 의미가 다름) */
/* 이후: 이름(4바이트 정렬) + 설명(4바이트 정렬) */
} Elf64_Nhdr;
주요 GNU 노트 타입:
| 섹션 | 이름 | n_type | 설명 |
|---|---|---|---|
.note.gnu.build-id | "GNU" | NT_GNU_BUILD_ID (3) | 바이너리의 고유 식별 해시 (SHA1 등). 디버그 심볼 매칭, debuginfod에서 사용 |
.note.ABI-tag | "GNU" | NT_GNU_ABI_TAG (1) | 최소 커널 버전 요구사항 (예: Linux 3.2.0). load_elf_binary()에서 검증 |
.note.gnu.property | "GNU" | NT_GNU_PROPERTY_TYPE_0 (5) | CET(IBT/SHSTK), BTI 등 보안 속성 표시. 커널이 arch_setup_elf_property()에서 처리 |
# 노트 섹션 확인
$ readelf -n /bin/ls
Displaying notes found in: .note.gnu.property
Owner Data size Description
GNU 0x00000020 NT_GNU_PROPERTY_TYPE_0
Properties: x86 feature: IBT, SHSTK
x86 ISA needed: x86-64-baseline
Displaying notes found in: .note.gnu.build-id
Owner Data size Description
GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring)
Build ID: 2b0e4e5b0c7c0a4f3e6d...
Displaying notes found in: .note.ABI-tag
Owner Data size Description
GNU 0x00000010 NT_GNU_ABI_TAG (ABI version tag)
OS: Linux, ABI: 3.2.0
CET과 NT_GNU_PROPERTY: Intel CET(Control-flow Enforcement Technology)의 IBT(Indirect Branch Tracking)와 SHSTK(Shadow Stack)이 활성화된 바이너리는 .note.gnu.property에 해당 속성이 기록됩니다. 커널은 arch_setup_elf_property()에서 이 노트를 파싱하여 CR4.CET과 MSR을 설정합니다. 모든 공유 라이브러리에도 CET 속성이 있어야 전체적으로 활성화됩니다.
exec() 처리 흐름 (Execution Flow)
execve() 시스템 콜이 호출되면 커널은 다음 단계를 거쳐 프로세스 이미지를 교체합니다:
- 바이너리 열기:
open_exec()으로 실행 파일을 열고,struct file을 얻습니다. - bprm 구조체(Struct) 준비:
struct linux_binprm에 파일 첫 256바이트(매직 넘버 검사용), credentials, 파일 정보를 채웁니다. - 포맷 핸들러 탐색:
search_binary_handler()가 등록된 바이너리 포맷 핸들러(struct linux_binfmt)를 순회하며, 매직 넘버를 통해 ELF, 스크립트(#!), misc 등 적절한 핸들러를 찾습니다. - ELF 헤더 검증:
load_elf_binary()가 매직 넘버(0x7f ELF), 클래스(32/64bit), 엔디안(Endianness), 아키텍처(e_machine)를 검증합니다. - 인터프리터 로딩:
PT_INTERP세그먼트가 있으면 동적 링커(예:/lib64/ld-linux-x86-64.so.2)의 ELF 파일을 별도로 로드합니다. - 기존 주소 공간 해제:
begin_new_exec()에서 point-of-no-return을 넘기고,exec_mmap()으로 기존mm_struct를 해제합니다. - PT_LOAD 세그먼트 매핑:
elf_map()으로 각PT_LOAD세그먼트를do_mmap()하여 파일 내용을 가상 주소 공간에 매핑합니다. - BSS 설정:
set_brk()로.bss영역(p_memsz > p_filesz 차이분)을 익명 매핑으로 할당합니다. - 스택 설정: 새 스택에 argv, envp, auxiliary vector(AT_*)를 배치합니다.
- 실행 시작:
start_thread()로 레지스터(Register)를 설정하고, 동적 링크 바이너리이면 인터프리터의 엔트리 포인트에서, 아니면 ELF의e_entry에서 실행을 시작합니다.
/* ELF 바이너리 로더 - fs/binfmt_elf.c (간략화) */
static int load_elf_binary(struct linux_binprm *bprm)
{
struct elfhdr *elf_ex = (struct elfhdr *)bprm->buf;
struct elf_phdr *elf_ppnt, *elf_phdata;
struct elfhdr *interp_elf_ex = NULL;
unsigned long load_addr = 0, load_bias = 0;
unsigned long elf_entry;
int executable_stack = EXSTACK_DEFAULT;
/* 1. ELF 매직 넘버 및 타입 검증 */
if (memcmp(elf_ex->e_ident, ELFMAG, SELFMAG) != 0)
return -ENOEXEC;
if (elf_ex->e_type != ET_EXEC && elf_ex->e_type != ET_DYN)
return -ENOEXEC;
/* 2. 프로그램 헤더 테이블 전체 읽기 */
elf_phdata = load_elf_phdrs(elf_ex, bprm->file);
/* 3. 프로그램 헤더 1차 순회: PT_INTERP, PT_GNU_STACK 처리 */
for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) {
if (elf_ppnt->p_type == PT_INTERP) {
/* 동적 링커 경로 읽기 (예: /lib64/ld-linux-x86-64.so.2) */
elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
elf_read(bprm->file, elf_interpreter,
elf_ppnt->p_filesz, elf_ppnt->p_offset);
/* 인터프리터 ELF 파일도 별도로 열어서 헤더 로드 */
interp_elf_ex = ...;
}
if (elf_ppnt->p_type == PT_GNU_STACK) {
/* 스택 실행 권한 결정 */
executable_stack = (elf_ppnt->p_flags & PF_X)
? EXSTACK_ENABLE_X : EXSTACK_DISABLE_X;
}
}
/* 4. Point of no return — 기존 주소 공간 해제 */
begin_new_exec(bprm);
setup_new_exec(bprm);
/* 5. ET_DYN(PIE)이면 ASLR용 load_bias 계산 */
if (elf_ex->e_type == ET_DYN) {
load_bias = ELF_ET_DYN_BASE; /* ASLR randomization 적용 */
if (current->flags & PF_RANDOMIZE)
load_bias += arch_mmap_rnd();
}
/* 6. PT_LOAD 세그먼트 매핑 */
for (i = 0; i < elf_ex->e_phnum; i++) {
if (elf_ppnt->p_type != PT_LOAD)
continue;
/* elf_map() → do_mmap(): 파일을 가상 주소에 매핑 */
error = elf_map(bprm->file,
load_bias + vaddr, /* 매핑 주소 */
elf_ppnt, /* 세그먼트 정보 */
elf_prot, elf_flags, total_size);
}
/* 7. BSS 영역 설정 (p_memsz > p_filesz 부분) */
set_brk(elf_bss, elf_brk, bss_prot);
/* 8. 인터프리터가 있으면 로드 */
if (interp_elf_ex)
elf_entry = load_elf_interp(interp_elf_ex, ...);
else
elf_entry = elf_ex->e_entry + load_bias;
/* 9. auxiliary vector를 스택에 배치 */
create_elf_tables(bprm, elf_ex, interp_load_addr,
e_entry, phdr_addr);
/* 10. 엔트리 포인트에서 실행 시작 */
start_thread(regs, elf_entry, bprm->p);
return 0;
}
Auxiliary Vector (보조 벡터)
커널은 exec() 시 사용자 공간(User Space) 스택에 auxiliary vector(보조 벡터)를 배치합니다. 이 벡터는 동적 링커(ld-linux.so)와 C 라이브러리가 프로세스 환경을 파악하는 데 사용하는 키-값 쌍입니다.
스택 레이아웃 (낮은 주소 → 높은 주소):
주요 auxiliary vector 엔트리 (create_elf_tables()에서 생성):
| 키 | 값 | 설명 |
|---|---|---|
AT_PHDR | 3 | 프로그램 헤더 테이블의 메모리 주소 |
AT_PHENT | 4 | 프로그램 헤더 엔트리 크기 |
AT_PHNUM | 5 | 프로그램 헤더 엔트리 수 |
AT_PAGESZ | 6 | 시스템 페이지 크기 (보통 4096) |
AT_BASE | 7 | 인터프리터(ld.so)의 로드 기본 주소 |
AT_FLAGS | 8 | 프로세서별 플래그 |
AT_ENTRY | 9 | 프로그램 엔트리 포인트 (e_entry) |
AT_UID/GID | 11/12 | 실제 사용자/그룹 ID |
AT_EUID/EGID | 13/14 | 유효 사용자/그룹 ID |
AT_PLATFORM | 15 | 플랫폼 문자열 (예: "x86_64") |
AT_HWCAP | 16 | 하드웨어 능력 비트마스크 (SSE, AVX 등) |
AT_CLKTCK | 17 | sysconf(_SC_CLK_TCK) 값 (보통 100) |
AT_RANDOM | 25 | 16바이트 랜덤 데이터 주소 (스택 canary 시드 등) |
AT_HWCAP2 | 26 | 확장 하드웨어 능력 (CET, TME 등) |
AT_EXECFN | 31 | 실행 파일명 문자열 주소 |
AT_SYSINFO_EHDR | 33 | vDSO ELF 이미지 주소 |
# auxiliary vector 확인 방법
$ LD_SHOW_AUXV=1 /bin/true
AT_SYSINFO_EHDR: 0x7ffd5c9fe000 # vDSO 주소
AT_HWCAP: 178bfbff # CPU 기능 비트맵
AT_PAGESZ: 4096 # 페이지 크기
AT_CLKTCK: 100 # clock ticks/sec
AT_PHDR: 0x55a3c4200040 # 프로그램 헤더 주소
AT_PHENT: 56 # phdr 엔트리 크기
AT_PHNUM: 13 # phdr 엔트리 수
AT_BASE: 0x7f2e8c400000 # ld.so 기본 주소
AT_ENTRY: 0x55a3c4201a90 # _start 주소
AT_RANDOM: 0x7ffd5c9d4a09 # 16바이트 난수 위치
AT_EXECFN: /bin/true
AT_PLATFORM: x86_64
ASLR과 PIE (Address Space Layout Randomization)
ASLR은 프로세스의 주요 메모리 영역(스택, 힙, mmap, 실행 코드)의 기본 주소를 매 실행마다 랜덤화하여 공격자가 코드/데이터 위치를 예측하기 어렵게 만듭니다.
| 영역 | 랜덤화 소스 | 커널 코드 |
|---|---|---|
| 스택 | arch_align_stack() | setup_arg_pages()에서 스택 top에 랜덤 오프셋 추가 |
| mmap base | arch_mmap_rnd() | 공유 라이브러리, vDSO 로드 주소 랜덤화 |
| 힙 (brk) | arch_randomize_brk() | BSS 끝 이후 brk 시작점 랜덤화 |
| PIE 바이너리 | ELF_ET_DYN_BASE + rnd | load_elf_binary()에서 ET_DYN 바이너리에 적용 |
/* fs/binfmt_elf.c — load_elf_binary() PIE(ET_DYN) ASLR 처리 (간략화) */
if (elf_ex->e_type == ET_DYN) {
/*
* ET_DYN 바이너리는 상대 주소로 컴파일되어 임의 위치에 로드 가능.
* ELF_ET_DYN_BASE는 아키텍처별 기본 주소:
* x86_64: (1UL << 47) * 2 / 3 ≈ 0x555555555000 부근
*/
load_bias = ELF_ET_DYN_BASE;
if (current->flags & PF_RANDOMIZE)
load_bias += arch_mmap_rnd();
load_bias = ELF_PAGESTART(load_bias);
}
/* ET_EXEC는 ELF에 지정된 고정 가상 주소에 로드 (ASLR 불가) */
if (elf_ex->e_type == ET_EXEC)
load_bias = 0; /* p_vaddr를 그대로 사용 */
# ASLR 수준 확인 및 설정
$ cat /proc/sys/kernel/randomize_va_space
2 # 0=비활성, 1=스택+mmap+vDSO, 2=1+brk(힙)
# 같은 PIE 바이너리를 두 번 실행하면 주소가 다름
$ cat /proc/self/maps | head -1
55a3c4200000-55a3c4205000 r--p ... # 첫 번째 실행
$ cat /proc/self/maps | head -1
564e8b400000-564e8b405000 r--p ... # 두 번째 실행 (주소 변경됨)
동적 링킹 (Dynamic Linking)
동적 링크 바이너리의 경우, 커널은 실행 파일과 함께 동적 링커(인터프리터)를 메모리에 로드하고, 인터프리터의 엔트리 포인트에서 실행을 시작합니다. 동적 링커(ld-linux.so)는 실제 프로그램의 main()이 실행되기 전에 공유 라이브러리 로드와 심볼 재배치를 수행합니다.
- 동적 링킹 처리 흐름 (커널 + 사용자 공간) */
- [커널] load_elf_binary() */
- PT_INTERP 세그먼트에서 인터프리터 경로 획득 */
- 예: "/lib64/ld-linux-x86-64.so.2" */
- 인터프리터 ELF 파일을 로드 (load_elf_interp) */
- 엔트리 포인트를 인터프리터의 e_entry로 설정 */
- [사용자 공간] ld-linux.so 시작 */
- _dl_start() → 자기 자신(ld.so)을 재배치 */
- _dl_main() → .dynamic 섹션 파싱 */
- → DT_NEEDED 라이브러리를 재귀적으로 로드 */
- → 심볼 해석 및 GOT/PLT 재배치 */
- 초기화 함수 실행 (.init_array) */
- 프로그램 엔트리 포인트(AT_ENTRY)로 점프 */
- → _start → __libc_start_main → main() */
GOT/PLT와 지연 바인딩(Lazy Binding): 외부 함수 호출은 PLT(Procedure Linkage Table)를 거칩니다. PLT 엔트리는 GOT(Global Offset Table)에서 실제 주소를 읽어 점프합니다. 최초 호출 시 GOT 엔트리는 PLT의 리졸버 코드를 가리키며, 리졸버가 심볼을 해석하여 GOT를 갱신합니다. 이후 호출은 GOT에서 직접 점프하여 오버헤드(Overhead)가 없습니다.
; PLT를 통한 printf() 호출 예시 (x86_64)
; 사용자 코드에서 printf 호출
call printf@plt ; → PLT 엔트리로 점프
; .plt 섹션의 printf 엔트리
printf@plt:
jmp *printf@GOTPLT(%rip) ; GOT에서 주소 읽어 간접 점프
; 최초 호출 시 GOT는 아래 push를 가리킴 (리졸버로 이동)
push $0x3 ; 재배치 인덱스
jmp .plt ; _dl_runtime_resolve 호출
; _dl_runtime_resolve()가 printf의 실제 주소를 찾아
; GOT 엔트리를 갱신 → 이후 호출은 직접 printf로 점프
Full RELRO: -Wl,-z,relro,-z,now로 빌드하면 동적 링커가 프로그램 시작 시 모든 GOT 엔트리를 즉시 바인딩(eager binding)한 뒤, PT_GNU_RELRO 영역을 mprotect(PROT_READ)로 읽기 전용으로 전환합니다. 이를 통해 GOT overwrite 공격을 방지할 수 있습니다. 현대 배포판에서는 보안 강화를 위해 기본 활성화되는 추세입니다.
GOT/PLT 상세 구조와 Lazy Binding
GOT(Global Offset Table)와 PLT(Procedure Linkage Table)는 동적 링킹의 핵심 데이터 구조입니다. 외부 함수 호출과 전역 데이터 접근을 위치 독립적으로 구현하며, 지연 바인딩(lazy binding)을 통해 프로그램 시작 시간을 최적화합니다.
GOT 구조
GOT는 두 개의 테이블로 구성됩니다:
| 테이블 | 섹션 | 용도 | 재배치 타입 |
|---|---|---|---|
| .got | 전역 데이터 GOT | 전역 변수의 절대 주소 저장. 프로그램 시작 시 즉시 해석 | R_X86_64_GLOB_DAT |
| .got.plt | PLT용 GOT | 외부 함수의 주소 저장. 지연 바인딩 대상 | R_X86_64_JUMP_SLOT |
.got.plt의 첫 3개 엔트리는 예약되어 있습니다:
| 인덱스 | 내용 | 설명 |
|---|---|---|
| GOT[0] | .dynamic 주소 | _DYNAMIC 심볼 — 동적 섹션의 런타임 주소 |
| GOT[1] | link_map * | 동적 링커가 설정하는 이 오브젝트의 link_map 구조체 포인터 |
| GOT[2] | _dl_runtime_resolve | 지연 바인딩 리졸버 함수 주소 (동적 링커가 설정) |
| GOT[3+] | 외부 함수 주소 | 각 PLT 엔트리에 대응하는 함수의 실제 주소 (또는 PLT stub) |
Lazy Binding 단계별 과정
# .got.plt 엔트리의 초기값 확인 (미해석 상태)
$ objdump -d -j .plt /bin/ls | head -20
Disassembly of section .plt:
0000000000004000 <.plt>:
4000: push QWORD PTR [rip+0x1c002] # GOT[1]
4006: jmp QWORD PTR [rip+0x1c004] # GOT[2]
0000000000004020 <abort@plt>:
4020: jmp QWORD PTR [rip+0x1bff2] # GOT[3] = 0x24018
4026: push 0x0 # reloc_idx = 0
402b: jmp 4000 <.plt> # PLT[0]으로
# GOT 엔트리의 런타임 값 확인 (GDB)
$ gdb -q /bin/ls
(gdb) start
(gdb) x/3gx &_GLOBAL_OFFSET_TABLE_
0x555555578000: 0x0000555555575df0 # GOT[0] = .dynamic
0x555555578008: 0x00007ffff7ffe2e0 # GOT[1] = link_map
0x555555578010: 0x00007ffff7fdd010 # GOT[2] = _dl_runtime_resolve
# printf 호출 전후 GOT 변화 확인
(gdb) x/gx 0x555555578048 # printf의 GOT 엔트리
0x555555578048: 0x0000555555554026 # → PLT stub (미해석)
(gdb) call (int)printf("test\n")
(gdb) x/gx 0x555555578048
0x555555578048: 0x00007ffff7c36e10 # → libc printf (해석됨)
dlopen/dlsym 동적 로딩 (Runtime Dynamic Loading)
dlopen()/dlsym() API는 프로그램 실행 중에 공유 라이브러리를 로드하고 심볼을 검색할 수 있게 합니다. 이는 플러그인 아키텍처, 선택적 기능 로딩, 라이브러리 버전 선택 등에 사용됩니다.
/* dlopen/dlsym 핵심 API — <dlfcn.h> */
/* 공유 라이브러리 로드 */
void *dlopen(const char *filename, int flags);
/* flags:
* RTLD_LAZY — 심볼을 사용 시점에 해석 (지연 바인딩)
* RTLD_NOW — dlopen() 시점에 모든 심볼 즉시 해석
* RTLD_GLOBAL — 이 라이브러리의 심볼을 전역 스코프에 노출
* RTLD_LOCAL — 이 라이브러리의 심볼을 이 핸들에서만 접근 가능 (기본값)
* RTLD_NODELETE — dlclose() 시 언로드하지 않음
* RTLD_NOLOAD — 로드하지 않고 이미 로드된 핸들만 반환
* RTLD_DEEPBIND — 이 라이브러리의 심볼을 전역보다 우선 검색 (인터포지션 방지)
*/
/* 심볼(함수/변수) 주소 획득 */
void *dlsym(void *handle, const char *symbol);
/* handle:
* dlopen() 반환값 — 특정 라이브러리에서 검색
* RTLD_DEFAULT — 기본 심볼 검색 순서 (전역 스코프)
* RTLD_NEXT — 현재 라이브러리 다음에 나오는 심볼 (래퍼 작성용)
*/
/* 라이브러리 언로드 (참조 카운트 기반) */
int dlclose(void *handle);
/* 마지막 dl* 오류 메시지 */
char *dlerror(void);
/* 심볼/라이브러리의 상세 정보 */
int dladdr(const void *addr, Dl_info *info);
/* Dl_info: dli_fname(파일 경로), dli_fbase(로드 주소),
* dli_sname(가장 가까운 심볼 이름), dli_saddr(심볼 주소) */
/* 플러그인 시스템 구현 예시 */
#include <dlfcn.h>
#include <stdio.h>
typedef int (*plugin_init_fn)(void);
typedef void (*plugin_cleanup_fn)(void);
int load_plugin(const char *path) {
/* 1. 라이브러리 로드 (즉시 바인딩) */
void *handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
if (!handle) {
fprintf(stderr, "dlopen: %s\n", dlerror());
return -1;
}
/* 2. 초기화 함수 심볼 검색 */
dlerror(); /* 이전 에러 클리어 */
plugin_init_fn init = (plugin_init_fn)dlsym(handle, "plugin_init");
char *err = dlerror();
if (err) {
fprintf(stderr, "dlsym: %s\n", err);
dlclose(handle);
return -1;
}
/* 3. 플러그인 초기화 실행 */
return init();
}
/* RTLD_NEXT 활용 — 함수 래핑 (인터포지션) */
void *malloc(size_t size) {
static void *(*real_malloc)(size_t) = NULL;
if (!real_malloc)
real_malloc = dlsym(RTLD_NEXT, "malloc"); /* 다음 malloc */
fprintf(stderr, "malloc(%zu)\n", size); /* 로깅 */
return real_malloc(size);
}
# dlopen 디버깅
$ LD_DEBUG=libs ./my_program 2>&1 | head -20
# 라이브러리 검색 과정을 상세히 출력
$ LD_DEBUG=symbols ./my_program 2>&1 | grep 'my_func'
# 특정 심볼의 검색/해석 과정 추적
$ LD_DEBUG=bindings ./my_program 2>&1 | head -30
# 심볼 바인딩 결과 (어디서 어디로) 추적
# 사용 가능한 LD_DEBUG 카테고리
$ LD_DEBUG=help ./any_binary 2>&1
# libs, reloc, files, symbols, bindings, versions, scopes, all, statistics, unused
# dlopen 라이브러리 링크
$ gcc -o plugin_host host.c -ldl # libdl 링크 (glibc 2.34+에서는 불필요)
$ gcc -shared -fPIC -o plugin.so plugin.c # 플러그인 빌드
심볼 인터포지션 (Symbol Interposition)
심볼 인터포지션은 동적 링커의 심볼 검색 순서를 활용하여, 라이브러리 함수를 사용자 정의 구현으로 대체하는 메커니즘입니다. LD_PRELOAD가 가장 일반적인 방법이며, 디버깅, 프로파일링(Profiling), 보안 감사, 테스팅에 활용됩니다.
/* 심볼 인터포지션 예시: malloc 추적기 (preload_malloc.c) */
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
static size_t total_allocated = 0;
static size_t alloc_count = 0;
void *malloc(size_t size) {
/* 원본 malloc 획득 (최초 1회) */
static void *(*real_malloc)(size_t) = NULL;
if (!real_malloc) {
real_malloc = dlsym(RTLD_NEXT, "malloc");
if (!real_malloc) abort();
}
void *ptr = real_malloc(size);
total_allocated += size;
alloc_count++;
/* 주의: fprintf 내부에서 malloc 호출 가능 → 재진입 방지 필요 */
/* 실제 구현에서는 write() 직접 호출이나 TLS 플래그 사용 */
return ptr;
}
/* 프로그램 종료 시 통계 출력 */
__attribute__((destructor))
void report(void) {
fprintf(stderr, "[malloc tracker] %zu allocations, %zu bytes total\n",
alloc_count, total_allocated);
}
# 빌드 및 사용
$ gcc -shared -fPIC -o preload_malloc.so preload_malloc.c -ldl
$ LD_PRELOAD=./preload_malloc.so /bin/ls
# → [malloc tracker] 412 allocations, 98304 bytes total
# 여러 라이브러리 동시 프리로드 (콜론 구분)
$ LD_PRELOAD="./lib1.so:./lib2.so" ./program
# 시스템 전역 프리로드 (주의: 모든 프로그램에 영향)
$ echo /path/to/lib.so >> /etc/ld.so.preload
# DF_1_NOINTERPOSE: 인터포지션 방지 (라이브러리 빌드 시)
$ gcc -shared -Wl,-z,interpose -o lib.so lib.c # 인터포지션 허용 (기본)
# 인터포지션 방지: -fvisibility=hidden + 명시적 export
$ gcc -shared -fPIC -fvisibility=hidden -o lib.so lib.c
# → hidden 심볼은 인터포지션 대상에서 제외
STV_PROTECTED와 인터포지션: STV_PROTECTED 가시성의 심볼은 외부에서 참조할 수 있지만, 같은 공유 라이브러리 내에서는 인터포지션되지 않습니다. 즉, 라이브러리 내부 호출은 항상 자체 정의를 사용합니다. 이는 -Bsymbolic 링커 옵션과 유사한 효과를 가지며, libc 등 성능이 중요한 라이브러리에서 내부 함수 호출의 GOT 우회 오버헤드를 제거하는 데 활용됩니다. 단, 일부 아키텍처에서 copy relocation과의 호환성 문제가 있어 주의가 필요합니다.
GNU IFUNC 리졸버 (Indirect Function)
GNU IFUNC(Indirect Function)은 런타임에 CPU 기능을 감지하여 최적의 함수 구현을 자동 선택하는 ELF 확장입니다. memcpy, strcmp, strlen 등 성능이 중요한 libc 함수에서 SSE/AVX/NEON 등 SIMD 명령어를 활용한 최적화된 구현을 투명하게 선택합니다.
/* IFUNC 리졸버 함수 작성 예시 */
#include <string.h>
/* 일반 구현 */
static void *memcpy_generic(void *dst, const void *src, size_t n) {
/* 바이트 단위 복사 */
...
}
/* AVX2 최적화 구현 */
static void *memcpy_avx2(void *dst, const void *src, size_t n) {
/* 256비트 YMM 레지스터로 대량 복사 */
...
}
/* AVX-512 최적화 구현 */
static void *memcpy_avx512(void *dst, const void *src, size_t n) {
/* 512비트 ZMM 레지스터로 대량 복사 */
...
}
/* IFUNC 리졸버 — 로드 시 한 번 호출되어 최적 구현 선택 */
static void *(*resolve_memcpy(void))
(void *, const void *, size_t)
__attribute__((ifunc("memcpy_resolver")));
static void *memcpy_resolver(void) {
/* AT_HWCAP / cpuid로 CPU 기능 확인 */
__builtin_cpu_init();
if (__builtin_cpu_supports("avx512f"))
return memcpy_avx512;
if (__builtin_cpu_supports("avx2"))
return memcpy_avx2;
return memcpy_generic;
}
/* 호출자 관점: 일반 함수처럼 호출 */
memcpy(dst, src, n); /* 자동으로 최적 구현이 선택됨 */
IFUNC 해석 과정:
- 컴파일:
STT_GNU_IFUNC타입의 심볼이 생성됩니다.st_value는 리졸버 함수의 주소입니다 (실제 구현이 아님). - 링킹: 재배치 엔트리의 타입이
R_X86_64_IRELATIVE(상대) 또는R_X86_64_JUMP_SLOT으로 기록됩니다. - 로딩: 동적 링커가
IRELATIVE재배치를 처리할 때, addend가 가리키는 리졸버 함수를 호출합니다. - 리졸버 실행: CPU 기능을 확인하고, 최적의 구현 함수 포인터를 반환합니다.
- GOT 패치: 반환된 함수 포인터가 GOT 엔트리에 저장됩니다.
- 이후 호출: GOT를 통해 직접 최적 구현으로 점프합니다.
# IFUNC 심볼 확인 (libc)
$ readelf -Ws /lib/x86_64-linux-gnu/libc.so.6 | grep IFUNC | head -10
132: 00000000001a3700 0 IFUNC GLOBAL DEFAULT 16 memcpy@@GLIBC_2.14
156: 00000000001a37a0 0 IFUNC GLOBAL DEFAULT 16 memset@@GLIBC_2.2.5
173: 00000000001a37f0 0 IFUNC GLOBAL DEFAULT 16 strcmp@@GLIBC_2.2.5
201: 00000000001a3810 0 IFUNC GLOBAL DEFAULT 16 strlen@@GLIBC_2.2.5
258: 00000000001a3840 0 IFUNC GLOBAL DEFAULT 16 strncmp@@GLIBC_2.2.5
# IRELATIVE 재배치 확인
$ readelf -r /lib/x86_64-linux-gnu/libc.so.6 | grep IRELATIVE | head -5
000000001f5d50 000000000025 R_X86_64_IRELATIVE 1a3700
000000001f5d58 000000000025 R_X86_64_IRELATIVE 1a37a0
# addend 1a3700 = memcpy의 리졸버 함수 주소
# 실제 선택된 구현 확인 (GDB)
(gdb) info symbol memcpy
__memcpy_avx_unaligned_erms in section .text of /lib/x86_64-linux-gnu/libc.so.6
# → 이 CPU에서는 AVX + ERMS 구현이 선택됨
# glibc의 IFUNC 리졸버 내부 (x86_64)
# sysdeps/x86_64/multiarch/memcpy.c
# __libc_ifunc_impl_list()로 전체 구현 목록 조회 가능
IFUNC과 커널: 리눅스 커널도 유사한 개념의 alternative instructions 메커니즘을 사용합니다. 부팅 시 CPU 기능을 감지하여 .altinstructions 섹션의 지침에 따라 코드를 패치합니다(예: NOP → LFENCE, 일반 락 → 하드웨어 락). 차이점은 IFUNC이 함수 단위로 선택하는 반면, alternative instructions는 명령어 단위로 패치하는 것입니다.
/proc/pid/maps와 ELF 매핑 분석
/proc/pid/maps는 프로세스의 가상 주소 공간 매핑을 보여주며, ELF 세그먼트가 메모리에 어떻게 배치되었는지 확인할 수 있는 핵심 디버깅 도구입니다.
# /proc/self/maps 출력 예시와 각 영역의 의미
$ cat /proc/self/maps
# 시작주소-끝주소 권한 오프셋 장치 inode 경로명
555555554000-555555556000 r--p 00000000 08:01 1234567 /usr/bin/cat # ELF 헤더 + .rodata (R--, PT_LOAD)
555555556000-55555555b000 r-xp 00002000 08:01 1234567 /usr/bin/cat # .text 코드 (R-X, PT_LOAD)
55555555b000-55555555e000 r--p 00007000 08:01 1234567 /usr/bin/cat # .rodata (R--, PT_LOAD)
55555555e000-55555555f000 r--p 00009000 08:01 1234567 /usr/bin/cat # .data.rel.ro, .got (R--, RELRO 적용됨)
55555555f000-555555560000 rw-p 0000a000 08:01 1234567 /usr/bin/cat # .data, .bss 일부 (RW-, PT_LOAD)
555555560000-555555581000 rw-p 00000000 00:00 0 [heap] # 힙 (brk)
7ffff7c00000-7ffff7c28000 r--p 00000000 08:01 2345678 /usr/lib/x86_64-linux-gnu/libc.so.6 # libc 헤더+rodata
7ffff7c28000-7ffff7dbd000 r-xp 00028000 08:01 2345678 /usr/lib/x86_64-linux-gnu/libc.so.6 # libc .text
7ffff7dbd000-7ffff7e15000 r--p 001bd000 08:01 2345678 /usr/lib/x86_64-linux-gnu/libc.so.6 # libc .rodata
7ffff7e15000-7ffff7e19000 r--p 00214000 08:01 2345678 /usr/lib/x86_64-linux-gnu/libc.so.6 # libc RELRO
7ffff7e19000-7ffff7e1b000 rw-p 00218000 08:01 2345678 /usr/lib/x86_64-linux-gnu/libc.so.6 # libc .data
7ffff7e1b000-7ffff7e28000 rw-p 00000000 00:00 0 # libc .bss (익명)
7ffff7fc3000-7ffff7fc7000 r--p 00000000 00:00 0 [vvar] # vDSO 공유 데이터 (커널→유저)
7ffff7fc7000-7ffff7fc9000 r-xp 00000000 00:00 0 [vdso] # vDSO 코드
7ffff7fc9000-7ffff7fca000 r--p 00000000 08:01 3456789 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 # ld.so
7ffff7fca000-7ffff7ff4000 r-xp 00001000 08:01 3456789 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 # ld.so .text
7ffff7ff4000-7ffff7ffe000 r--p 0002b000 08:01 3456789 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 # ld.so .rodata
7ffff7ffe000-7ffff8000000 rw-p 00035000 08:01 3456789 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 # ld.so .data
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack] # 스택
# 실행 중인 프로세스의 ELF 매핑과 세그먼트 비교
$ readelf -l /bin/cat
Program Headers:
Type Offset VirtAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000000000 0x002a58 0x002a58 R 0x1000
LOAD 0x003000 0x0000000000003000 0x004581 0x004581 R E 0x1000
LOAD 0x008000 0x0000000000008000 0x001690 0x001690 R 0x1000
LOAD 0x00a000 0x000000000000a000 0x000258 0x000560 RW 0x1000
# RW 세그먼트: p_filesz=0x258, p_memsz=0x560 → .bss=0x308 추가
# 프로세스별 메모리 사용량 상세 (smaps)
$ cat /proc/$(pidof cat)/smaps | head -20
555555554000-555555557000 r--p 00000000 08:01 1234567 /usr/bin/cat
Size: 12 kB
KernelPageSize: 4 kB
Rss: 12 kB # 실제 물리 메모리 사용량
Pss: 12 kB # 비례 공유 메모리 사용량
Shared_Clean: 0 kB # 다른 프로세스와 공유 (클린)
Private_Clean: 12 kB # 이 프로세스만 사용 (클린)
Private_Dirty: 0 kB # 이 프로세스만 사용 (수정됨)
# 전체 메모리 요약
$ pmap -x $(pidof cat)
Address Kbytes RSS Dirty Mode Mapping
0000555555554000 12 12 0 r---- cat
0000555555557000 20 20 0 r-x-- cat
000055555555c000 8 8 0 r---- cat
000055555555e000 4 4 4 r---- cat # RELRO
000055555555f000 4 4 4 rw--- cat # .data
vDSO (virtual Dynamic Shared Object)
vDSO는 커널이 모든 사용자 공간 프로세스의 주소 공간에 자동으로 매핑하는 특수한 공유 라이브러리입니다. gettimeofday(), clock_gettime(), getcpu() 등 빈번하게 호출되지만 실제 특권이 불필요한 시스템 콜을 커널 진입 없이 사용자 공간에서 처리할 수 있게 합니다.
/* arch/x86/entry/vdso/vma.c — vDSO 매핑 (간략화) */
/* 커널이 exec 시 vDSO를 매핑하는 과정 */
/* load_elf_binary() → arch_setup_additional_pages() */
int arch_setup_additional_pages(struct linux_binprm *bprm, int uses_interp)
{
/* vDSO 이미지를 프로세스 주소 공간에 매핑 */
return map_vdso(&vdso_image_64, 0);
/* AT_SYSINFO_EHDR auxv 엔트리가 이 주소를 가리킴 */
}
/* vDSO 내 함수 예시: 시스템 콜 없이 시간 획득 */
int __vdso_clock_gettime(clockid_t clock, struct timespec *ts)
{
/* 커널이 공유 메모리(vvar)에 갱신해둔 시간 데이터를 직접 읽음 */
/* → syscall 오버헤드(~100ns) 제거 */
...
}
# vDSO 확인
$ cat /proc/self/maps | grep vdso
7ffd5c9fe000-7ffd5ca00000 r-xp 00000000 00:00 0 [vdso]
# vDSO가 export하는 심볼 확인
$ objdump -T /proc/self/root/usr/lib/vdso/vdso64.so 2>/dev/null || \
dd if=/proc/self/mem bs=1 skip=$((0x7ffd5c9fe000)) count=8192 2>/dev/null | \
readelf -Ws /dev/stdin 2>/dev/null
# __vdso_clock_gettime, __vdso_gettimeofday, __vdso_time, __vdso_getcpu
GNU Binutils 도구 (Analysis Tools)
도구 선택 매트릭스
작업 목적에 따라 최적의 Binutils 도구를 선택하는 기준표입니다.
| 목적 | 1차 도구 | 보조 도구 | 비고 |
|---|---|---|---|
| ELF 파일 구조 파악 | readelf -a | objdump -x | readelf가 더 정확 (BFD 미사용) |
| 역어셈블 / 소스 혼합 | objdump -dS | objdump --disassemble=함수명 | 소스 필요 시 디버그 심볼 필수 |
| 심볼 조회 | nm -C | readelf -s | nm이 간결, readelf가 상세 |
| 커널 패닉(Kernel Panic) 주소 변환(Address Translation) | 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 | 부트로더(Bootloader)/펌웨어(Firmware) 이미지 생성 |
| 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이름 | 입출력(I/O) 포맷 동시 지정 |
-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=섹션명=파일 | 섹션 원시 데이터를 파일로 덤프(Dump) |
심볼 조작 옵션
| 옵션 | 설명 |
|---|---|
-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)는 링커나 동적 로더(Loader)가 심볼 참조를 실제 주소로 해석하는 과정입니다. 오브젝트 파일에는 재배치 엔트리가 포함되어 링커가 최종 주소를 결정할 때 어떤 위치의 어떤 값을 어떻게 수정해야 하는지 알려줍니다.
재배치 엔트리 구조
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 | 임의 물리 주소(Physical Address) 로드 가능 | 부트 시 재배치 테이블 순회 |
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"
실전 활용 예제
아래 예제들은 커널 개발 현장에서 자주 쓰이는 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"
크로스 컴파일(Cross Compilation) 환경에서의 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 포맷
TLS — Thread-Local Storage (스레드(Thread) 로컬 저장소)
TLS(Thread-Local Storage)는 각 스레드가 독립적인 복사본을 가지는 변수를 구현하는 메커니즘입니다. C/C++에서 __thread 또는 _Thread_local 키워드로 선언하며, ELF에서는 PT_TLS 세그먼트와 TLS 관련 재배치로 구현됩니다.
/* TLS 변수 선언 예시 */
__thread int tls_var = 42; /* 초기화됨 → .tdata 섹션 */
__thread int tls_bss_var; /* 미초기화 → .tbss 섹션 */
_Thread_local char buf[256]; /* C11 표준 키워드 (동일) */
/* 커널 내 per-CPU 변수도 유사한 개념 */
DEFINE_PER_CPU(int, my_percpu_var); /* 커널에서는 __percpu로 구현 */
TLS 관련 ELF 섹션과 세그먼트:
| 요소 | 타입 | 설명 |
|---|---|---|
.tdata | PROGBITS | 초기화된 TLS 변수. SHF_TLS | SHF_ALLOC | SHF_WRITE 플래그 |
.tbss | NOBITS | 미초기화 TLS 변수 (0 초기화). .bss와 유사하게 파일 공간 불필요 |
PT_TLS | 세그먼트 (7) | TLS 초기화 이미지. 새 스레드 생성 시 이 이미지를 복사하여 스레드별 TLS 영역 생성 |
STT_TLS | 심볼 타입 (6) | TLS 변수 심볼. st_value는 TLS 블록 내 오프셋 |
TLS 접근 모델: 컴파일러는 TLS 변수의 가시성과 링킹 상황에 따라 4가지 접근 모델을 사용합니다:
| 모델 | 플래그 | 적용 상황 | 성능 |
|---|---|---|---|
| Local Exec (LE) | -ftls-model=local-exec | 실행 파일 내부에서 자체 TLS 접근. TP에서 고정 오프셋으로 직접 접근 | 최고 (단일 명령어) |
| Initial Exec (IE) | -ftls-model=initial-exec | 실행 파일에서 공유 라이브러리의 TLS 접근. GOT에서 TP 오프셋을 읽음 | 빠름 (GOT 1회 접근) |
| General Dynamic (GD) | -ftls-model=global-dynamic | dlopen()으로 로드된 라이브러리의 TLS. __tls_get_addr() 호출 필요 | 느림 (함수 호출) |
| Local Dynamic (LD) | -ftls-model=local-dynamic | 같은 DSO 내 여러 TLS 변수 접근 시 GD 최적화. 모듈 base를 1회만 조회 | GD보다 약간 빠름 |
; Local Exec 모델 (x86_64) — 가장 효율적
; 실행 파일 자체의 TLS 변수 접근
mov eax, dword ptr fs:[tls_var@TPOFF] ; FS + 고정 오프셋
; Initial Exec 모델 — GOT에서 오프셋 로드
mov rax, qword ptr [rip + tls_var@GOTTPOFF] ; GOT에서 TP 오프셋
mov eax, dword ptr fs:[rax] ; FS + 오프셋
; General Dynamic 모델 — __tls_get_addr() 호출
lea rdi, [rip + tls_var@TLSGD] ; TLS descriptor 주소
call __tls_get_addr@PLT ; 동적 링커가 주소 반환
mov eax, [rax] ; 반환된 주소로 접근
# TLS 세그먼트 확인
$ readelf -l /usr/bin/python3 | grep TLS
TLS 0x000000000029a890 0x000000000049a890 0x000000000049a890
0x0000000000000010 0x0000000000000088 R 0x8
# TLS 심볼 확인
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep TLS
16: 0000000000000000 4 TLS GLOBAL DEFAULT 32 errno@@GLIBC_PRIVATE
18: 0000000000000008 8 TLS GLOBAL DEFAULT 32 __resp@@GLIBC_PRIVATE
# TLS 관련 재배치
$ readelf -r /usr/bin/python3 | grep TPOFF
00000049c038 001b00000012 R_X86_64_TPOFF64 0000000000000000 _Py_tss_tstate + 0
x86_64 TLS와 FS 레지스터: x86_64에서 FS 세그먼트 레지스터는 TCB(Thread Control Block)를 가리킵니다. 실행 파일의 TLS 블록은 TCB 아래(음수 오프셋)에, dlopen()으로 로드된 모듈의 TLS는 DTV(Dynamic Thread Vector)를 통해 동적으로 할당됩니다. 커널은 arch_prctl(ARCH_SET_FS, addr) 또는 clone() 시 CLONE_SETTLS 플래그로 MSR_FS_BASE를 설정합니다.
코어 덤프 (Core Dump)
프로세스가 치명적 시그널(SIGSEGV, SIGABRT 등)을 받으면 커널은 프로세스의 메모리 상태를 ELF 코어 덤프 파일로 저장합니다. 코어 파일은 ET_CORE 타입의 ELF이며, 크래시 분석에 필수적인 정보를 포함합니다.
코어 덤프 생성 경로:
코어 파일 구조:
/* fs/binfmt_elf.c — 코어 덤프 핵심 함수 (간략화) */
static int elf_core_dump(struct coredump_params *cprm)
{
struct elfhdr elf;
struct elf_phdr *phdr;
/* 1. ELF 헤더 작성 (ET_CORE) */
fill_elf_header(&elf, segs + 1, ELF_ARCH, 0);
elf.e_type = ET_CORE;
/* 2. PT_NOTE 세그먼트 작성 — 레지스터, 시그널 정보 */
fill_note(&psinfo_note, "CORE", NT_PRPSINFO, ...);
fill_note(&siginfo_note, "CORE", NT_SIGINFO, ...);
/* 각 스레드의 레지스터 상태 */
for_each_thread(p, t) {
fill_note(&t->prstatus_note, "CORE", NT_PRSTATUS,
sizeof(struct elf_prstatus), &t->prstatus);
}
/* 3. VMA 순회 — 각 메모리 영역을 PT_LOAD로 기록 */
for_each_vma(cprm->mm, vma) {
/* 코어 덤프 필터에 따라 포함 여부 결정 */
if (!vma_dump_size(vma, cprm->mm_flags))
continue;
phdr->p_type = PT_LOAD;
phdr->p_vaddr = vma->vm_start;
phdr->p_memsz = vma->vm_end - vma->vm_start;
/* 메모리 내용을 파일에 기록 */
dump_user_range(cprm, vma->vm_start, size);
}
}
# 코어 덤프 설정
$ cat /proc/sys/kernel/core_pattern
|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h
# 코어 덤프 필터 (비트마스크로 포함할 VMA 유형 제어)
$ cat /proc/self/coredump_filter
00000033 # bit 0: 익명 private, bit 1: 익명 shared
# bit 4: ELF headers, bit 5: DAX private
# 코어 파일 분석
$ readelf -h core | grep Type
Type: CORE (Core file)
$ readelf -n core | head -20
Displaying notes found at file offset 0x... with length 0x...:
Owner Data size Description
CORE 0x00000150 NT_PRSTATUS (prstatus structure)
CORE 0x00000088 NT_PRPSINFO (prpsinfo structure)
CORE 0x00000080 NT_SIGINFO (siginfo_t data)
CORE 0x00000130 NT_AUXV (auxiliary vector)
CORE 0x00000563 NT_FILE (mapped files)
# GDB로 코어 파일 분석
$ gdb /path/to/binary core
(gdb) bt # 크래시 시점 backtrace
(gdb) info registers # 레지스터 상태
(gdb) x/10i $rip # 크래시 위치 디스어셈블리
# systemd 환경에서 코어 덤프 조회
$ coredumpctl list
$ coredumpctl debug # 최근 코어 덤프로 GDB 실행
커널 모듈 ELF (.ko)
커널 모듈(.ko 파일)은 ET_REL(재배치 가능) 타입의 ELF 오브젝트입니다. 일반 사용자 공간 실행 파일과 달리 프로그램 헤더가 없으며, 커널의 load_module()이 링커 역할을 수행하여 섹션 기반으로 로딩과 재배치를 처리합니다.
모듈 전용 ELF 섹션:
| 섹션 | 설명 |
|---|---|
.modinfo | 모듈 메타데이터: license=, description=, author=, alias=, depends=, vermagic=. modinfo 명령이 읽는 데이터 |
__versions | CRC 기반 심볼 버전 체크섬(Checksum). 커널-모듈 ABI 호환성 검증에 사용 (CONFIG_MODVERSIONS) |
.init.text | 모듈 초기화 코드 (module_init()). 로딩 후 해제 가능한 영역으로 분류 |
.exit.text | 모듈 제거 코드 (module_exit()). 내장 빌드 시 제거됨 |
__ksymtab | EXPORT_SYMBOL()로 내보낸 심볼 테이블 |
__ksymtab_gpl | EXPORT_SYMBOL_GPL()로 내보낸 GPL 전용 심볼 테이블 |
__kcrctab | 내보낸 심볼의 CRC 체크섬 |
.gnu.linkonce.this_module | struct module 인스턴스. 모듈 이름, init/exit 함수 포인터 포함 |
__param | module_param()으로 정의한 모듈 파라미터 기술자 |
.altinstructions | CPU 기능에 따른 대체 명령어 (alternative instructions) 테이블 |
__bug_table | BUG() / WARN() 매크로(Macro)의 위치 정보 |
__ex_table | 예외 테이블 — 사용자 공간 접근(copy_to_user 등)에서 페이지 폴트(Page Fault) 시 복구 주소 |
/* kernel/module/main.c — 모듈 로딩 핵심 흐름 (간략화) */
static int load_module(struct load_info *info, const char *uargs, int flags)
{
/* 1. ELF 헤더 검증 (ET_REL, 아키텍처, 버전) */
err = elf_validity_check(info);
/* 2. 섹션 헤더 테이블 파싱 — 모든 섹션 정보 수집 */
err = setup_load_info(info, flags);
/* 3. .modinfo에서 vermagic 검증 */
err = check_modinfo(info->mod, info, flags);
/* vermagic = "6.1.0 SMP preempt mod_unload" */
/* 4. SHF_ALLOC 섹션을 core/init 영역으로 분류 */
module_frob_arch_sections(info);
err = move_module(info->mod, info);
/* vmalloc으로 메모리 할당, 섹션 데이터 복사 */
/* 5. 심볼 해석 — 커널 심볼 테이블에서 외부 참조 검색 */
err = simplify_symbols(info);
/* find_symbol() → __ksymtab 검색 → 심볼 주소 resolve */
/* 6. 재배치 수행 */
err = apply_relocations(info);
/* 각 .rela.* 섹션에 대해 apply_relocate_add() 호출 */
/* 모듈 코드 내 주소 참조를 실제 커널 심볼 주소로 패치 */
/* 7. __ex_table, __bug_table 등 특수 섹션 등록 */
post_relocation(info->mod, info);
/* 8. 모듈 초기화 함수 실행 */
return do_init_module(info->mod);
/* mod->init() 호출 후 .init.* 섹션 메모리 해제 */
}
# 커널 모듈의 ELF 구조 확인
$ readelf -h drivers/net/ethernet/intel/e1000e/e1000e.ko
ELF Header:
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
# 모듈 전용 섹션 확인
$ readelf -S e1000e.ko | grep -E 'modinfo|ksymtab|init\.text|versions'
[11] .modinfo PROGBITS ...
[13] __versions PROGBITS ...
[21] .init.text PROGBITS ...
[35] __ksymtab PROGBITS ...
[36] __ksymtab_gpl PROGBITS ...
# .modinfo 내용 확인
$ modinfo e1000e.ko
filename: e1000e.ko
license: GPL v2
description: Intel(R) PRO/1000 Network Driver
author: Intel Corporation
alias: pci:v00008086d00001533...
depends:
vermagic: 6.1.0 SMP preempt mod_unload
# 모듈의 재배치 엔트리 — 커널 심볼 참조
$ readelf -r e1000e.ko | head -10
Relocation section '.rela.text' at offset ... contains 1234 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000054 002c00000002 R_X86_64_PC32 0000000000000000 printk - 4
00000000009e 003400000002 R_X86_64_PC32 0000000000000000 __kmalloc - 4
EXPORT_SYMBOL과 심볼 네임스페이스(Namespace): 커널 5.4부터 EXPORT_SYMBOL_NS(sym, ns)로 심볼 네임스페이스를 지정할 수 있습니다. 모듈이 특정 네임스페이스의 심볼을 사용하려면 MODULE_IMPORT_NS(ns)를 선언해야 합니다. __ksymtab 섹션에 네임스페이스 정보가 포함되며, 커널은 로딩 시 이를 검증합니다. 이는 커널 내부 API의 무분별한 사용을 제한하고 서브시스템 간 경계를 명확히 합니다.
__ksymtab, __kcrctab, modpost, namespace 검증이 실제로 어떻게 이어지는지 한 번에 추적할 수 있습니다.
커널 특수 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 핫플러그(Hotplug) 초기화 코드 (구버전) | __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로 커널 함수 목록을 주소순으로 확인 가능
커널 이미지 빌드 파이프라인(Pipeline)
리눅스 커널 빌드는 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 # 로드 결과 확인
binfmt 핸들러 프레임워크 (Binary Format Handlers)
Linux 커널은 다양한 실행 파일 형식을 플러그인 방식으로 지원합니다. exec() 시스템 콜이 호출되면 커널은 등록된 바이너리 포맷 핸들러를 순회하며, 파일의 매직 넘버를 검사하여 적절한 핸들러를 찾습니다. 이 프레임워크의 핵심은 struct linux_binfmt입니다.
/* include/linux/binfmts.h */
struct linux_binfmt {
struct list_head lh; /* 전역 리스트 연결 */
struct module *module; /* 소유 모듈 (참조 카운트) */
int (*load_binary)(struct linux_binprm *); /* 바이너리 로드 */
int (*load_shlib)(struct file *); /* uselib() 지원 (레거시) */
int (*core_dump)(struct coredump_params *cprm); /* 코어 덤프 생성 */
unsigned long min_coredump; /* 최소 코어 덤프 크기 */
};
커널에 내장된 주요 바이너리 포맷 핸들러:
| 핸들러 | 소스 | 매직 / 판별 | 처리 대상 |
|---|---|---|---|
binfmt_elf | fs/binfmt_elf.c | \x7fELF (4바이트) | ELF 실행 파일, 공유 라이브러리 |
binfmt_script | fs/binfmt_script.c | #! (2바이트) | 셸 스크립트, 인터프리터 스크립트 |
binfmt_misc | fs/binfmt_misc.c | 사용자 정의 매직/확장자 | QEMU, Wine, Java, .NET 등 |
binfmt_flat | fs/binfmt_flat.c | BFLT 헤더 | MMU-less 시스템용 플랫 바이너리 |
binfmt_elf_fdpic | fs/binfmt_elf_fdpic.c | \x7fELF + FDPIC ABI | MMU-less ELF (공유 텍스트 세그먼트) |
/* fs/exec.c — 바이너리 포맷 핸들러 탐색 (간략화) */
static int search_binary_handler(struct linux_binprm *bprm)
{
struct linux_binfmt *fmt;
int retval;
/* 등록된 모든 binfmt 핸들러를 순회 */
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
/* 각 핸들러의 load_binary() 호출 */
retval = fmt->load_binary(bprm);
module_put(fmt->module);
if (retval == -ENOEXEC)
continue; /* 이 포맷이 아님 → 다음 핸들러 시도 */
return retval; /* 성공(0) 또는 에러 → 탐색 종료 */
}
return retval; /* -ENOEXEC: 어떤 핸들러도 인식하지 못함 */
}
/* fs/binfmt_elf.c — ELF 핸들러 등록 */
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary, /* ELF 실행 파일 로드 */
.load_shlib = load_elf_library, /* 레거시 uselib() */
.core_dump = elf_core_dump, /* ET_CORE 코어 덤프 */
.min_coredump = ELF_EXEC_PAGESIZE,
};
/* fs/binfmt_script.c — #! 스크립트 핸들러 */
static int load_script(struct linux_binprm *bprm)
{
/* 첫 2바이트가 '#!' 인지 확인 */
if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!'))
return -ENOEXEC;
/* 인터프리터 경로 파싱 (예: /usr/bin/python3) */
i_name = bprm->buf + 2; /* '#!' 이후 */
...
/* bprm의 파일을 인터프리터로 교체 후 재귀 호출 */
bprm->interpreter = open_exec(i_name);
return search_binary_handler(bprm); /* 재귀! */
}
binfmt_misc — 사용자 정의 바이너리 핸들러: binfmt_misc는 사용자 공간에서 /proc/sys/fs/binfmt_misc를 통해 임의의 바이너리 포맷을 등록할 수 있는 강력한 확장 메커니즘입니다. 컨테이너(Container) 환경에서 다른 아키텍처의 바이너리를 투명하게 실행하는 데 특히 유용합니다.
# binfmt_misc 마운트 확인
$ mount | grep binfmt_misc
systemd-1 on /proc/sys/fs/binfmt_misc type binfmt_misc
# QEMU user-mode emulation 등록 (ARM64 바이너리를 x86_64에서 실행)
$ echo ':qemu-aarch64:M::\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7\x00:\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/usr/bin/qemu-aarch64-static:FPC' \
> /proc/sys/fs/binfmt_misc/register
# 등록된 핸들러 확인
$ cat /proc/sys/fs/binfmt_misc/qemu-aarch64
enabled
interpreter /usr/bin/qemu-aarch64-static
flags: FPC
offset 0
magic 7f454c460201010000000000000000000200b700
mask ffffffffffffff00fffffffffffffffffeffffff
# Java JAR 파일 직접 실행 등록
$ echo ':Java:M::\xca\xfe\xba\xbe::/usr/bin/java:' \
> /proc/sys/fs/binfmt_misc/register
# Wine으로 PE (.exe) 바이너리 실행 등록 (매직: MZ)
$ echo ':DOSWin:M::MZ::/usr/bin/wine:' \
> /proc/sys/fs/binfmt_misc/register
# 현재 등록된 모든 핸들러 확인
$ ls /proc/sys/fs/binfmt_misc/
qemu-aarch64 qemu-arm Java DOSWin register status
F(fix-binary) 플래그와 컨테이너: binfmt_misc 등록 시 F 플래그를 사용하면, 등록 시점에 인터프리터 바이너리의 struct file을 미리 열어 둡니다. 이는 Docker/Podman에서 멀티 아키텍처 이미지를 빌드할 때 핵심적인데, 컨테이너 내부에 QEMU 바이너리가 없어도 호스트의 QEMU를 사용할 수 있기 때문입니다. 커널 4.8에서 도입된 이 기능이 없으면, 컨테이너 내에서 인터프리터 경로를 찾을 수 없어 -ENOENT 에러가 발생합니다.
재귀 깊이 제한: #! 스크립트가 또 다른 #! 스크립트를 인터프리터로 지정하는 재귀적 구조를 방지하기 위해, 커널은 linux_binprm.recursion_depth로 최대 재귀 깊이를 4로 제한합니다 (BINPRM_MAX_RECURSION). 이를 초과하면 -ENOEXEC를 반환합니다. 예를 들어, 셸 스크립트(#!/bin/bash) → bash(ELF) 은 깊이 2이므로 정상 동작합니다.
심볼 버저닝 (Symbol Versioning)
심볼 버저닝은 공유 라이브러리가 동일한 심볼의 여러 버전을 동시에 제공할 수 있게 하는 ELF 확장입니다. glibc는 이 메커니즘을 활용하여 하위 호환성을 유지하면서 ABI를 발전시킵니다. 버저닝 정보는 .gnu.version, .gnu.version_r, .gnu.version_d 섹션에 저장됩니다.
/* 심볼 버저닝 관련 ELF 구조체 */
/* .gnu.version — 심볼 버전 인덱스 테이블
* .dynsym의 각 심볼에 1:1 대응하는 16비트 버전 인덱스 */
typedef struct {
Elf64_Half vd_ndx; /* 버전 인덱스 (0=local, 1=global, 2+=정의된 버전) */
} Elf64_Versym;
/* .gnu.version_d — 라이브러리가 정의(제공)하는 버전 */
typedef struct {
Elf64_Half vd_version; /* 구조체 버전 (VER_DEF_CURRENT = 1) */
Elf64_Half vd_flags; /* VER_FLG_BASE: 기본 버전 */
Elf64_Half vd_ndx; /* 버전 인덱스 */
Elf64_Half vd_cnt; /* Verdaux 엔트리 수 */
Elf64_Word vd_hash; /* 버전 이름 ELF 해시 */
Elf64_Word vd_aux; /* Verdaux 오프셋 */
Elf64_Word vd_next; /* 다음 Verdef 오프셋 (0=마지막) */
} Elf64_Verdef;
/* .gnu.version_r — 바이너리가 요구(필요)하는 버전 */
typedef struct {
Elf64_Half vn_version; /* 구조체 버전 (VER_NEED_CURRENT = 1) */
Elf64_Half vn_cnt; /* Vernaux 엔트리 수 */
Elf64_Word vn_file; /* 라이브러리 이름 오프셋 (.dynstr) */
Elf64_Word vn_aux; /* Vernaux 오프셋 */
Elf64_Word vn_next; /* 다음 Verneed 오프셋 (0=마지막) */
} Elf64_Verneed;
/* Vernaux — 요구 버전의 상세 항목 */
typedef struct {
Elf64_Word vna_hash; /* 버전 이름 ELF 해시 */
Elf64_Half vna_flags; /* VER_FLG_WEAK: 약한 버전 참조 */
Elf64_Half vna_other; /* .gnu.version에서 사용하는 인덱스 */
Elf64_Word vna_name; /* 버전 문자열 오프셋 (예: "GLIBC_2.17") */
Elf64_Word vna_next; /* 다음 Vernaux 오프셋 (0=마지막) */
} Elf64_Vernaux;
glibc에서 심볼 버저닝이 동작하는 방식:
# .gnu.version 섹션 — 각 동적 심볼의 버전 인덱스
$ readelf -V /bin/ls
Version symbols section '.gnu.version' contains 120 entries:
Addr: 0x0000000000004ab8 Offset: 0x004ab8 Link: 6 (.dynsym)
000: 0 (*local*) 2 (GLIBC_2.3) 3 (GLIBC_2.14) 1 (*global*)
004: 4 (GLIBC_2.3.4) 2 (GLIBC_2.3) 2 (GLIBC_2.3) 0 (*local*)
# .gnu.version_r — 필요한 버전 목록
Version needs section '.gnu.version_r' contains 1 entry:
Addr: 0x0000000000004c48 Offset: 0x004c48 Link: 7 (.dynstr)
000000: Version: 1 File: libc.so.6 Cnt: 7
0x0010: Name: GLIBC_2.14 Flags: none Version: 3
0x0020: Name: GLIBC_2.3 Flags: none Version: 2
0x0030: Name: GLIBC_2.3.4 Flags: none Version: 4
0x0040: Name: GLIBC_2.17 Flags: none Version: 5
0x0050: Name: GLIBC_2.28 Flags: none Version: 6
0x0060: Name: GLIBC_2.34 Flags: none Version: 7
0x0070: Name: GLIBC_2.2.5 Flags: none Version: 8
# 특정 심볼의 버전 확인
$ objdump -T /bin/ls | grep memcpy
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.14 memcpy
# glibc가 제공하는 memcpy 버전들 확인
$ objdump -T /lib/x86_64-linux-gnu/libc.so.6 | grep ' memcpy'
00000000000a7910 g DF .text 0000000000000039 GLIBC_2.2.5 memcpy
0000000000098fc0 g DF .text 0000000000000244 (GLIBC_2.14) memcpy
# 바이너리가 요구하는 최소 glibc 버전 확인
$ objdump -T /bin/ls | grep -oP 'GLIBC_\d+\.\d+(\.\d+)?' | sort -Vu | tail -1
GLIBC_2.34
ld.so의 버전 해석 과정: 동적 링커(ld-linux.so)는 심볼 해석 시 .gnu.version과 .gnu.version_r을 교차 참조합니다. 바이너리의 .gnu.version에 기록된 인덱스를 .gnu.version_r에서 찾아 버전 문자열(예: GLIBC_2.14)을 얻고, 라이브러리의 .gnu.version_d에서 해당 버전이 제공되는지 확인합니다. 버전이 일치하지 않으면 "version `GLIBC_2.34' not found" 에러를 출력합니다. 이 과정은 _dl_check_map_versions() (elf/dl-version.c)에서 수행됩니다.
커널 모듈의 심볼 버저닝 (CONFIG_MODVERSIONS): 커널 모듈은 glibc와 다른 자체 버저닝 메커니즘을 사용합니다. EXPORT_SYMBOL()로 내보낸 각 심볼에 대해 CRC32 체크섬을 계산하여 __kcrctab 섹션에 저장합니다. 모듈 로딩 시 check_version()이 모듈의 __versions 섹션에 기록된 CRC와 커널의 CRC를 비교하여, 함수 시그니처 변경으로 인한 ABI 불일치를 감지합니다. 이것이 "disagrees about version of symbol"/"Invalid module format" 에러의 원인입니다.
스택 언와인딩과 .eh_frame (Stack Unwinding)
스택 언와인딩(unwinding)은 현재 실행 지점에서 호출 스택을 역추적(Backtrace)하는 과정으로, 예외 처리(C++ throw/catch), 디버거 백트레이스, 시그널 처리, 성능 프로파일링에 필수적입니다. ELF에서는 .eh_frame과 .eh_frame_hdr 섹션에 DWARF CFI(Call Frame Information) 형식으로 언와인딩 정보를 저장합니다.
| 섹션 | 타입 | 설명 |
|---|---|---|
.eh_frame | SHT_PROGBITS | CIE + FDE 레코드: 각 함수의 프레임 복원 규칙 |
.eh_frame_hdr | SHT_PROGBITS | FDE 이진 검색 테이블 — PT_GNU_EH_FRAME 세그먼트로 매핑 |
.debug_frame | SHT_PROGBITS | DWARF 디버그용 CFI (비할당, 스트립 시 제거) |
.gcc_except_table | SHT_PROGBITS | C++ 예외 처리 LSDA (Language Specific Data Area) |
/* DWARF CFI 핵심 구조: CIE (Common Information Entry) + FDE (Frame Description Entry) */
/* CIE — 공통 정보 엔트리: 함수군에 대한 공통 언와인딩 규칙 */
struct cie_record {
uint32_t length; /* CIE 크기 (0xffffffff이면 64비트 확장) */
uint32_t cie_id; /* 항상 0 (CIE 식별자) */
uint8_t version; /* CFI 버전 (1 또는 3) */
char augmentation[]; /* "zR", "zPLR" 등 확장 문자열 */
/* ULEB128 code_alignment_factor; 텍스트 정렬 단위 (x86_64: 1) */
/* SLEB128 data_alignment_factor; 데이터 정렬 단위 (x86_64: -8) */
/* ULEB128 return_address_register; RA 레지스터 번호 (x86_64: 16=RIP) */
/* initial_instructions[]; 초기 CFA 규칙 바이트코드 */
};
/* FDE — 프레임 기술 엔트리: 개별 함수의 언와인딩 규칙 */
struct fde_record {
uint32_t length; /* FDE 크기 */
uint32_t cie_pointer; /* 소속 CIE로의 오프셋 (≠0이면 FDE) */
/* pc_begin; 함수 시작 주소 (인코딩은 CIE augmentation에 따름) */
/* pc_range; 함수 크기 */
/* instructions[]; DW_CFA_* 바이트코드: 주소별 프레임 규칙 변화 */
};
CFI 바이트코드의 핵심 개념: 각 명령어 주소에서 CFA(Canonical Frame Address)와 레지스터 복원 규칙을 정의합니다. CFA는 일반적으로 이전 프레임의 스택 포인터 값입니다.
| 명령어 | 의미 |
|---|---|
| DW_CFA_def_cfa reg, off | CFA = reg + off (프레임 기준 주소 정의) |
| DW_CFA_def_cfa_offset n | CFA의 오프셋을 n으로 변경 |
| DW_CFA_offset reg, off | reg은 CFA+off 위치에 저장됨 |
| DW_CFA_advance_loc n | 코드 위치를 n × code_align만큼 전진 |
| DW_CFA_restore reg | reg 복원 규칙을 CIE 초기 상태로 복원 |
| DW_CFA_remember_state | 현재 규칙 집합을 스택에 push |
| DW_CFA_restore_state | 스택에서 규칙 집합을 pop |
# .eh_frame의 CIE/FDE 레코드 확인
$ readelf --debug-dump=frames /bin/ls | head -40
Contents of the .eh_frame section:
00000000 0000000000000014 00000000 CIE
Version: 1
Augmentation: "zR"
Code alignment factor: 1
Data alignment factor: -8
Return address column: 16
Augmentation data: 1b # DW_EH_PE_pcrel|DW_EH_PE_sdata4
DW_CFA_def_cfa: r7 (rsp) ofs 8 # 초기: CFA = RSP + 8
DW_CFA_offset: r16 (rip) at cfa-8 # RIP는 CFA-8에 저장됨
00000018 0000000000000024 0000001c FDE cie=00000000 pc=0000c040..0000c0a2
DW_CFA_advance_loc: 4
DW_CFA_def_cfa_offset: 16 # push 이후: CFA = RSP + 16
DW_CFA_offset: r6 (rbp) at cfa-16 # RBP가 CFA-16에 저장됨
DW_CFA_advance_loc: 4
DW_CFA_def_cfa_register: r6 (rbp) # mov rbp,rsp 이후: CFA = RBP + 16
# .eh_frame_hdr — FDE 이진 검색 테이블
$ readelf --debug-dump=frames-interp /bin/ls 2>/dev/null | head -5
# PT_GNU_EH_FRAME 세그먼트 확인
$ readelf -l /bin/ls | grep EH_FRAME
GNU_EH_FRAME 0x000000000000e8e4 0x000000000000e8e4 ... R 0x4
# C++ 예외 테이블 확인
$ readelf -S /usr/bin/c++filt | grep except
[15] .gcc_except_table PROGBITS ...
커널 스택 언와인딩 — ORC vs DWARF:
| 특성 | DWARF CFI (.eh_frame) | ORC (Oops Rewind Capability) |
|---|---|---|
| 사용 환경 | 사용자 공간 (glibc, libunwind) | 커널 전용 (x86_64, 커널 4.14+) |
| 데이터 형식 | 가변 길이 바이트코드 (인터프리터 필요) | 고정 크기 6바이트 엔트리 (직접 조회) |
| 정확도 | 높음 (임의 레지스터 복원 가능) | 높음 (SP/BP/IP만 복원) |
| 크기 | 상대적으로 큼 | 작음 (~2-3% 커널 텍스트 크기) |
| 속도 | 상대적으로 느림 (바이트코드 해석) | 빠름 (테이블 직접 조회) |
| 생성 도구 | GCC/Clang (-fasynchronous-unwind-tables) | objtool (컴파일 후 오브젝트 분석) |
| 커널 설정 | CONFIG_UNWINDER_FRAME_POINTER (레거시) | CONFIG_UNWINDER_ORC (기본값) |
/* arch/x86/include/asm/orc_types.h — ORC 엔트리 구조 (6바이트) */
struct orc_entry {
s16 sp_offset; /* SP 복원 오프셋 */
s16 bp_offset; /* BP 복원 오프셋 */
unsigned sp_reg:4; /* SP 기준 레지스터 (SP, BP, SP_INDIRECT 등) */
unsigned bp_type:4; /* BP 상태 (undefined, prev_sp, regs 등) */
unsigned type:2; /* 프레임 타입 (CALL, REGS, REGS_PARTIAL) */
unsigned signal:1; /* 시그널 프레임 여부 */
unsigned end:1; /* 언와인딩 종료 지점 */
} __packed;
/* .orc_unwind + .orc_unwind_ip 섹션에 저장
* objtool이 정적 분석으로 각 명령어 주소의 프레임 상태를 사전 계산 */
/* arch/x86/kernel/unwind_orc.c — ORC 언와인더 핵심 루프 */
void __unwind_start(struct unwind_state *state,
struct task_struct *task, ...)
{
/* IP → ORC 엔트리 테이블 이진 검색 */
orc = orc_find(state->ip);
...
}
bool unwind_next_frame(struct unwind_state *state)
{
struct orc_entry *orc = orc_find(state->ip);
/* ORC 엔트리의 sp_reg/sp_offset으로 이전 SP 계산 */
switch (orc->sp_reg) {
case ORC_REG_SP:
sp = state->sp + orc->sp_offset; break;
case ORC_REG_BP:
sp = state->bp + orc->sp_offset; break;
...
}
/* IP = *(sp - 8): 리턴 주소로 이전 프레임의 IP 복원 */
state->ip = *((unsigned long *)sp - 1);
state->sp = sp;
return true;
}
-fomit-frame-pointer와 언와인딩: 현대 컴파일러는 기본적으로 -fomit-frame-pointer로 RBP 프레임 포인터를 최적화합니다(범용 레지스터로 활용). 이 경우 전통적인 RBP 체이닝 방식의 백트레이스가 불가능하므로, .eh_frame(사용자 공간) 또는 ORC(커널)의 테이블 기반 언와인딩이 필수적입니다. perf record --call-graph dwarf는 .eh_frame을 사용하고, perf record --call-graph fp는 프레임 포인터를 사용합니다. 프레임 포인터가 없는 바이너리에서 fp 모드는 잘못된 백트레이스를 생성합니다.
DWARF 디버그 정보 (Debug Information)
DWARF(Debugging With Attributed Record Formats)는 ELF 바이너리에 포함되는 디버그 정보의 표준 형식입니다. 소스 수준 디버깅(GDB, LLDB), 심볼 해석, 소스-라인 매핑에 사용되며, 컴파일러가 -g 옵션으로 생성합니다. 현재 널리 사용되는 버전은 DWARF 4(-gdwarf-4, GCC 기본)와 DWARF 5(-gdwarf-5, Clang 기본)입니다.
| 섹션 | DWARF 버전 | 설명 |
|---|---|---|
.debug_info | 2+ | 핵심 디버그 정보: DIE(Debugging Information Entry) 트리 — 함수, 변수, 타입, 스코프 |
.debug_abbrev | 2+ | DIE 태그/속성 약어 테이블 — .debug_info의 압축 인코딩을 해석하는 스키마 |
.debug_line | 2+ | 소스 파일명/줄 번호 ↔ 기계어(Machine Code) 주소 매핑 (라인 넘버 프로그램) |
.debug_str | 2+ | 중복 제거된 문자열 풀 (변수명, 파일 경로 등) |
.debug_loc | 2-4 | 변수 위치 목록: PC 범위별로 변수가 레지스터/메모리 어디에 있는지 기술 |
.debug_loclists | 5 | .debug_loc의 DWARF 5 후속 — 더 컴팩트한 인코딩 |
.debug_ranges | 3-4 | 비연속 주소 범위 목록 (인라인 함수, 최적화된 코드용) |
.debug_rnglists | 5 | .debug_ranges의 DWARF 5 후속 |
.debug_aranges | 2+ | 주소 → 컴파일 유닛 빠른 조회 테이블 |
.debug_types | 4 | 타입 정보 전용 섹션 (DWARF 5에서 .debug_info에 통합) |
.debug_str_offsets | 5 | 문자열 오프셋 인덱싱 — 문자열 참조를 인덱스로 압축 |
.debug_line_str | 5 | 라인 테이블 전용 문자열 풀 |
/* DWARF DIE (Debugging Information Entry) 구조 개념
* 각 DIE는 태그(종류)와 속성(키-값) 목록으로 구성됨
* DIE는 트리 구조로 스코프를 표현 */
/* 예: 다음 C 코드의 DWARF DIE 표현 */
int add(int a, int b) { return a + b; }
/*
* DIE 트리:
* DW_TAG_compile_unit ← 컴파일 유닛 (소스 파일)
* DW_AT_name: "math.c"
* DW_AT_comp_dir: "/home/dev/project"
* DW_AT_language: DW_LANG_C11
* DW_AT_producer: "GCC 13.2.0"
* DW_AT_stmt_list: (→ .debug_line 오프셋)
*
* DW_TAG_base_type ← 기본 타입
* DW_AT_name: "int"
* DW_AT_byte_size: 4
* DW_AT_encoding: DW_ATE_signed
*
* DW_TAG_subprogram ← 함수
* DW_AT_name: "add"
* DW_AT_low_pc: 0x401000 ← 함수 시작 주소
* DW_AT_high_pc: 0x401020 ← 함수 끝 주소
* DW_AT_type: → (int) ← 반환 타입 참조
* DW_AT_frame_base: DW_OP_reg6(rbp)
*
* DW_TAG_formal_parameter ← 매개변수
* DW_AT_name: "a"
* DW_AT_type: → (int)
* DW_AT_location: DW_OP_fbreg(-20) ← RBP-20에 저장
*
* DW_TAG_formal_parameter
* DW_AT_name: "b"
* DW_AT_type: → (int)
* DW_AT_location: DW_OP_fbreg(-24)
*/
# DWARF 디버그 섹션 크기 확인 — 디버그 정보가 바이너리보다 큰 경우가 흔함
$ readelf -S /usr/lib/debug/usr/bin/ls.debug | grep debug
[28] .debug_aranges PROGBITS ... 000002d0 ...
[29] .debug_info PROGBITS ... 00032a41 ... # 200KB+
[30] .debug_abbrev PROGBITS ... 00001e25 ...
[31] .debug_line PROGBITS ... 0000d8a2 ...
[32] .debug_str PROGBITS ... 00005e12 ...
# DIE 트리 덤프 — 함수와 변수의 디버그 정보 확인
$ readelf --debug-dump=info a.out | head -50
Contents of the .debug_info section:
Compilation Unit @ offset 0x0:
Length: 0x120
Version: 5 # DWARF 5
Unit Type: DW_UT_compile
Abbrev Offset: 0x0
Pointer Size: 8
<0><c>: Abbrev Number: 1 (DW_TAG_compile_unit)
<d> DW_AT_producer : GNU C17 13.2.0
<22> DW_AT_language : 29 (C11)
<23> DW_AT_name : math.c
<2a> DW_AT_comp_dir : /home/dev/project
<1><3e>: Abbrev Number: 2 (DW_TAG_subprogram)
<3f> DW_AT_name : add
<43> DW_AT_low_pc : 0x401000
<4b> DW_AT_high_pc : 32
<4f> DW_AT_type : <0x70>
# 소스 줄 ↔ 기계어 주소 매핑
$ readelf --debug-dump=decodedline a.out
Decoded dump of debug contents of section .debug_line:
CU: math.c:
File name Line number Starting address View Stmt
math.c 1 0x401000 x
math.c 2 0x401004 x
math.c 2 0x40100a
math.c 2 0x401013 x
# addr2line — 주소를 소스 위치로 변환 (내부적으로 .debug_line 사용)
$ addr2line -e a.out -f 0x401004
add
/home/dev/project/math.c:2
# dwarfdump (libdwarf) — 더 상세한 DWARF 분석
$ dwarfdump --print-all a.out 2>/dev/null | head -20
Split DWARF와 debuginfod — 대규모 프로젝트의 디버그 정보 관리:
| 기술 | 설명 | 장점 |
|---|---|---|
Split DWARF (-gsplit-dwarf) | 디버그 정보를 .dwo 파일로 분리. 바이너리에는 .debug_addr과 스켈레톤 CU만 남김 | 링크 시간 단축(디버그 정보 병합 불필요), 릴리스 바이너리 크기 감소 |
DWARF 패키지 (dwp) | 여러 .dwo 파일을 단일 .dwp 패키지로 병합 | 배포/관리 편의성, GDB가 자동 탐색 |
| debuginfod | HTTP 서버로 .debug 파일을 온디맨드 제공. DEBUGINFOD_URLS 환경 변수 | 로컬 디버그 심볼 설치 불필요, GDB/perf 자동 다운로드 |
| GNU Build ID | .note.gnu.build-id — 바이너리의 고유 SHA1 식별자 | debuginfod에서 바이너리↔디버그 파일 매칭 키로 사용 |
.gnu_debuglink | 별도 디버그 파일(*.debug)의 경로와 CRC32 | 스트립된 바이너리에서 디버그 파일 위치 힌트 |
# Split DWARF 컴파일 — .dwo 파일 생성
$ gcc -g -gsplit-dwarf -c math.c -o math.o
$ ls math.*
math.c math.dwo math.o
# .dwo 파일 병합 (대규모 프로젝트)
$ dwp -e myapp -o myapp.dwp
# GNU Build ID 확인
$ readelf -n /bin/ls | grep 'Build ID'
Build ID: 2f31f68c55e02a7e7a72ad2758e04bd50c4e1e3a
# debuginfod 설정 (Fedora/Ubuntu/Arch 기본 제공)
$ export DEBUGINFOD_URLS="https://debuginfod.elfutils.org/"
$ gdb /bin/ls
Reading symbols from /bin/ls...
Downloading separate debug info for /bin/ls...
# 수동으로 별도 디버그 파일 설치 (Debian/Ubuntu)
$ sudo apt install coreutils-dbgsym
# → /usr/lib/debug/.build-id/2f/31f68c...debug
# 스트립 + debuglink 생성
$ objcopy --only-keep-debug myapp myapp.debug
$ strip --strip-debug myapp
$ objcopy --add-gnu-debuglink=myapp.debug myapp
커널의 디버그 정보 (vmlinux): 커널은 CONFIG_DEBUG_INFO로 DWARF 디버그 정보를 생성합니다. vmlinux에 포함된 디버그 정보는 수백 MB에 달할 수 있으며, crash dump 분석(crash 유틸리티), SystemTap, BPF CO-RE(BTF)의 기반이 됩니다. 커널 5.2+에서는 CONFIG_DEBUG_INFO_BTF로 DWARF에서 추출한 경량 타입 정보인 BTF(BPF Type Format)를 생성하여, BPF 프로그램이 커널 데이터 구조에 안전하게 접근할 수 있게 합니다. BTF는 DWARF의 1/100 이하 크기로 커널 이미지에 포함됩니다 (.BTF, .BTF.ext 섹션).
GNU Build ID와 디버그 정보 분리
GNU Build ID는 빌드 결과물을 고유하게 식별하는 해시값으로, .note.gnu.build-id 섹션에 저장됩니다. 이 ID는 실행 파일과 별도로 저장된 디버그 심볼을 정확하게 매칭하는 데 핵심적인 역할을 합니다.
Build ID 생성과 구조
# Build ID 확인
$ readelf -n /bin/ls | grep 'Build ID'
Build ID: 6d8d16c3fbb1a2c75684c1bdab9a1fc6c60ca2e6
# file 명령으로도 확인 가능
$ file /bin/ls
/bin/ls: ELF 64-bit LSB pie executable, ..., BuildID[sha1]=6d8d16c3..., ...
# Build ID 생성 방법 (링크 시 지정)
$ gcc -Wl,--build-id=sha1 -o test test.c # SHA-1 해시 (기본값)
$ gcc -Wl,--build-id=md5 -o test test.c # MD5 해시
$ gcc -Wl,--build-id=uuid -o test test.c # UUID v4 (랜덤)
$ gcc -Wl,--build-id=0x1234abcd -o test test.c # 수동 지정
# Build ID 기반 디버그 심볼 저장 경로
# Build ID = 6d8d16c3fbb1a2c75684c1bdab9a1fc6c60ca2e6 이면:
# /usr/lib/debug/.build-id/6d/8d16c3fbb1a2c75684c1bdab9a1fc6c60ca2e6.debug
디버그 정보 분리 (debuginfo split)
배포판은 디스크 공간 절약과 보안을 위해 디버그 정보를 별도 패키지(-dbg, -debuginfo)로 분리합니다. GDB 등 디버거는 Build ID를 키로 올바른 디버그 심볼을 자동 매칭합니다.
# 디버그 정보 분리 과정
$ gcc -g -o myapp myapp.c # 디버그 포함 빌드
$ objcopy --only-keep-debug myapp myapp.debug # 디버그 정보만 추출
$ strip --strip-debug myapp # 원본에서 디버그 제거
$ objcopy --add-gnu-debuglink=myapp.debug myapp # 디버그 링크 추가
# .gnu_debuglink 섹션: 디버그 파일명 + CRC32 체크섬
$ readelf -p .gnu_debuglink myapp
[ 0] myapp.debug
# CRC32 체크섬으로 파일 무결성 검증
# GDB의 디버그 심볼 검색 순서
# 1. 실행 파일 내 .debug_info (있으면)
# 2. .gnu_debuglink의 파일명으로 검색:
# - 실행 파일과 같은 디렉토리
# - 같은 디렉토리/.debug/
# - /usr/lib/debug/ + 실행파일 경로
# 3. .note.gnu.build-id로 검색:
# - /usr/lib/debug/.build-id/XX/YYYY...YYYY.debug
# debuginfod — 네트워크 기반 디버그 심볼 서버 (최신 방식)
$ export DEBUGINFOD_URLS="https://debuginfod.ubuntu.com"
$ gdb /bin/ls
# → GDB가 자동으로 Build ID로 서버에서 디버그 심볼 다운로드
# eu-unstrip: 분리된 디버그 정보 재결합
$ eu-unstrip /bin/ls /usr/lib/debug/.build-id/6d/8d16c3...debug -o ls-with-debug
# 코어 덤프에서 Build ID 매칭
$ eu-unstrip -n --core core
# 각 매핑된 라이브러리의 Build ID와 디버그 파일 매칭 상태 출력
debuginfod 서버: debuginfod는 HTTP 기반의 디버그 정보 배포 서비스입니다 (elfutils 0.178+). GDB, Valgrind, systemtap 등이 Build ID를 키로 디버그 심볼, 소스 코드, 실행 파일을 자동으로 요청합니다. 주요 배포판(Ubuntu, Fedora, Debian, Arch)이 공식 서버를 운영하여, 별도 -dbg 패키지 설치 없이도 디버깅이 가능합니다. DEBUGINFOD_URLS 환경변수로 설정합니다.
링커 스크립트 (Linker Script)
링커 스크립트는 링커(ld)에게 입력 섹션을 출력 세그먼트에 어떻게 배치할지 지시하는 설정 파일입니다. 사용자 공간 프로그램은 시스템 기본 링커 스크립트(ld --verbose)를 사용하지만, 커널(vmlinux.lds.S)과 부트로더, 펌웨어, 베어메탈 프로그램은 맞춤 링커 스크립트가 필수적입니다.
/* 기본 링커 스크립트 구조 — 섹션에서 세그먼트로의 매핑 */
/* ENTRY: 프로그램 진입점 (ELF e_entry에 설정) */
ENTRY(_start)
/* PHDRS: 출력 세그먼트(프로그램 헤더) 정의 */
PHDRS
{
headers PT_PHDR PHDRS; /* 프로그램 헤더 자체 */
interp PT_INTERP; /* 인터프리터 경로 */
text PT_LOAD FLAGS(5); /* R-X: 코드 세그먼트 */
rodata PT_LOAD FLAGS(4); /* R--: 읽기 전용 데이터 */
data PT_LOAD FLAGS(6); /* RW-: 데이터 세그먼트 */
dynamic PT_DYNAMIC FLAGS(6); /* 동적 링킹 정보 */
note PT_NOTE FLAGS(4); /* 빌드 ID 등 메타데이터 */
eh_frame PT_GNU_EH_FRAME FLAGS(4); /* .eh_frame_hdr */
stack PT_GNU_STACK FLAGS(6); /* 스택 속성 (NX) */
relro PT_GNU_RELRO FLAGS(4); /* RELRO 영역 */
}
/* SECTIONS: 입력 섹션을 출력 섹션에 배치하고, 세그먼트에 할당 */
SECTIONS
{
/* 코드 영역 — text 세그먼트 */
.text : {
*(.text .text.*) /* 모든 입력의 .text 섹션 */
*(.text.hot .text.hot.*) /* 핫 코드 (PGO) */
} :text /* → text 세그먼트에 배치 */
/* 읽기 전용 데이터 — rodata 세그먼트 */
.rodata : {
*(.rodata .rodata.*)
} :rodata
/* 동적 링킹 — data 세그먼트 */
.dynamic : { *(.dynamic) } :data :dynamic
/* GOT/PLT — RELRO 보호 대상 */
.got : { *(.got) } :data :relro
.got.plt : { *(.got.plt) }
/* 초기화된 데이터 */
.data : { *(.data .data.*) } :data
/* BSS — 파일에 저장되지 않음 (p_memsz > p_filesz) */
.bss : { *(.bss .bss.*) *(COMMON) } :data
/* PROVIDE: 심볼이 정의되지 않았을 때만 제공 */
PROVIDE(_end = .);
PROVIDE(__bss_start = ADDR(.bss));
}
리눅스 커널의 링커 스크립트 (vmlinux.lds.S): 커널은 아키텍처별 링커 스크립트 템플릿(arch/x86/kernel/vmlinux.lds.S)을 C 전처리기로 처리하여 최종 vmlinux.lds를 생성합니다. 커널만의 특수한 섹션 배치가 정의됩니다.
/* arch/x86/kernel/vmlinux.lds.S — 커널 링커 스크립트 (핵심 발췌) */
#include <asm-generic/vmlinux.lds.h>
SECTIONS
{
. = __START_KERNEL; /* x86_64: 0xffffffff81000000 */
.text : AT(ADDR(.text) - LOAD_OFFSET) {
_text = .;
_stext = .;
/* HEAD_TEXT: startup_64 등 부팅 초기 코드 */
HEAD_TEXT
/* TEXT_TEXT: 일반 커널 코드 */
TEXT_TEXT
...
_etext = .;
}
/* .init 영역: 부팅 후 해제되는 코드/데이터 */
.init.text : AT(ADDR(.init.text) - LOAD_OFFSET) {
_sinittext = .;
INIT_TEXT /* __init 매크로 함수들 */
_einittext = .;
}
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {
INIT_DATA /* __initdata 변수들 */
}
/* 커널 특수 섹션 */
.altinstructions : {
__alt_instructions = .;
*(.altinstructions) /* CPU 기능별 대체 명령어 */
__alt_instructions_end = .;
}
__ex_table : {
__start___ex_table = .;
*(__ex_table) /* 예외 테이블 */
__stop___ex_table = .;
}
/* ORC 언와인더 데이터 */
.orc_unwind_ip : {
__start_orc_unwind_ip = .;
*(.orc_unwind_ip)
__stop_orc_unwind_ip = .;
}
.orc_unwind : {
__start_orc_unwind = .;
*(.orc_unwind)
__stop_orc_unwind = .;
}
/* __start/__stop 심볼: 링커가 자동 생성하는 섹션 경계 포인터 */
/* 커널 코드에서 for_each 매크로로 섹션 내용을 순회할 때 사용 */
}
# 시스템 기본 링커 스크립트 확인
$ ld --verbose 2>/dev/null | grep -A3 'SECTIONS'
SECTIONS
{
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000));
. = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
# 커널 빌드에서 생성된 실제 링커 스크립트
$ head -20 /usr/src/linux/vmlinux.lds
# 또는 빌드 디렉토리에서:
$ head -20 /lib/modules/$(uname -r)/build/vmlinux.lds
# 커스텀 링커 스크립트 사용 (임베디드/베어메탈)
$ gcc -T custom.ld -nostdlib -o firmware startup.o main.o
# 섹션→세그먼트 매핑 확인
$ readelf -l /bin/ls | head -30
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R 0x8
INTERP 0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R 0x1
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x003a58 0x003a58 R 0x1000
LOAD 0x004000 0x0000000000004000 0x0000000000004000 0x013581 0x013581 R E 0x1000
LOAD 0x018000 0x0000000000018000 0x0000000000018000 0x007b10 0x007b10 R 0x1000
LOAD 0x020000 0x0000000000020000 0x0000000000020000 0x001258 0x002560 RW 0x1000
...
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss
__start/__stop 매직 심볼과 커널 섹션 순회: GNU ld는 __start_SECTION과 __stop_SECTION 심볼을 자동 생성합니다(섹션 이름이 C 식별자 규칙을 만족하는 경우). 커널은 이 메커니즘을 광범위하게 활용합니다. 예를 들어 __start___ex_table부터 __stop___ex_table까지 순회하면 모든 예외 테이블 엔트리에 접근할 수 있습니다. module_init()/module_exit() 매크로가 함수 포인터를 .initcall*.init 섹션에 배치하고, 커널이 부팅 시 __initcall_start부터 순회하며 드라이버 초기화를 실행하는 것도 같은 원리입니다.
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
ELF 보안 강화 기법 (Security Hardening)
현대 Linux 시스템은 ELF 바이너리에 다양한 보안 메커니즘을 적용하여 메모리 손상 취약점(Vulnerability)의 악용을 방어합니다. 이러한 기법들은 컴파일러, 링커, 커널이 협력하여 구현합니다.
| 기법 | 보호 대상 | 구현 위치 | 컴파일러 플래그 |
|---|---|---|---|
| Stack Canary (SSP) | 스택 버퍼 오버플로(Buffer Overflow) | 컴파일러 (함수 프롤로그/에필로그에 카나리 삽입) | -fstack-protector-strong (기본값) |
| NX (No-Execute) | 코드 주입 | 커널 (PTE NX 비트), 링커 (PT_GNU_STACK) | -z noexecstack (기본값) |
| RELRO | GOT 오버라이트 | 링커 (PT_GNU_RELRO 세그먼트) | -z relro -z now (Full RELRO) |
| PIE/ASLR | ROP/JOP 공격 | 컴파일러 (PIC 코드), 커널 (무작위 주소 배치) | -fPIE -pie (기본값) |
| FORTIFY_SOURCE | 버퍼 오버플로 (libc 함수) | 컴파일러 (strcpy→__strcpy_chk 등 치환) | -D_FORTIFY_SOURCE=2 -O2 |
| CET (Control-flow Enforcement) | ROP/JOP 공격 (하드웨어) | CPU (Shadow Stack, IBT), 컴파일러, 커널 | -fcf-protection=full |
| CFI (Control Flow Integrity) | 간접 호출 하이재킹 | 컴파일러 (간접 호출 대상 검증) | -fsanitize=cfi (Clang) |
# 바이너리의 보안 속성 종합 점검 — checksec 스크립트
$ checksec --file=/bin/ls
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols Yes 5 15 /bin/ls
# 개별 보안 속성 수동 확인
# 1. NX (No-Execute Stack) — PT_GNU_STACK 세그먼트의 플래그 확인
$ readelf -l /bin/ls | grep GNU_STACK
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
# ^^ RW = NX 활성 (E 없음)
# 2. RELRO — PT_GNU_RELRO 세그먼트 존재 + BIND_NOW 확인
$ readelf -l /bin/ls | grep GNU_RELRO
GNU_RELRO 0x01f9c8 0x000000000021f9c8 0x000000000021f9c8 0x000638 0x000638 R 0x1
$ readelf -d /bin/ls | grep BIND_NOW
0x000000000000001e (FLAGS) BIND_NOW
# Full RELRO = PT_GNU_RELRO + BIND_NOW (즉시 바인딩으로 GOT가 읽기 전용)
# 3. PIE — ELF 타입이 DYN (공유 오브젝트)이면 PIE
$ readelf -h /bin/ls | grep Type
Type: DYN (Position-Independent Executable)
# 4. Stack Canary — __stack_chk_fail 심볼 참조 존재
$ readelf -s /bin/ls | grep stack_chk
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4
# 5. FORTIFY — __*_chk 함수 참조 확인
$ readelf -s /bin/ls | grep '_chk@'
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __printf_chk@GLIBC_2.3.4
12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __memcpy_chk@GLIBC_2.3.4
Partial RELRO vs Full RELRO:
| 속성 | Partial RELRO (-z relro) | Full RELRO (-z relro -z now) |
|---|---|---|
| 링커 플래그 | -Wl,-z,relro | -Wl,-z,relro,-z,now |
| PT_GNU_RELRO | 존재 | 존재 |
| GOT (.got) | 읽기 전용 | 읽기 전용 |
| GOT PLT (.got.plt) | 쓰기 가능 (Lazy Binding) | 읽기 전용 (Immediate Binding) |
| DT_BIND_NOW | 없음 | 있음 |
| 시작 시간 | 빠름 (lazy) | 느림 (모든 심볼 즉시 해석) |
| 보안 수준 | GOT PLT 오버라이트 가능 | GOT 전체 보호 |
/* 커널의 ELF 보안 검사 — fs/binfmt_elf.c */
/* NX 스택 처리: PT_GNU_STACK 세그먼트의 PF_X 플래그로 결정 */
static int load_elf_binary(struct linux_binprm *bprm)
{
...
for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) {
if (elf_ppnt->p_type == PT_GNU_STACK) {
if (elf_ppnt->p_flags & PF_X)
executable_stack = EXSTACK_ENABLE_X; /* 실행 가능 스택 */
else
executable_stack = EXSTACK_DISABLE_X; /* NX 스택 (기본) */
}
}
...
/* ASLR: 인터프리터와 바이너리의 로드 주소 무작위화 */
if (elf_ex->e_type == ET_DYN) { /* PIE 바이너리 */
load_bias = ELF_ET_DYN_BASE; /* 2/3 * TASK_SIZE */
if (current->flags & PF_RANDOMIZE)
load_bias += arch_mmap_rnd(); /* ASLR 오프셋 */
load_bias = ELF_PAGESTART(load_bias);
}
...
}
/* Stack Canary (커널 측): 프로세스 생성 시 랜덤 카나리 값 설정 */
/* include/linux/sched.h */
struct task_struct {
...
unsigned long stack_canary; /* copy_process()에서 get_random_canary()로 초기화 */
...
};
/* arch/x86에서는 per-cpu __stack_chk_guard 또는 gs:[0x28]에 저장 */
CET (Control-flow Enforcement Technology): Intel CET는 두 가지 메커니즘을 제공합니다. Shadow Stack은 하드웨어가 관리하는 별도의 리턴 주소 스택으로, CALL/RET 시 자동으로 검증합니다(ROP 방어). IBT(Indirect Branch Tracking)는 간접 점프/호출의 대상이 ENDBR64 명령어로 시작해야 함을 강제합니다(JOP 방어). 커널 6.2+에서는 CONFIG_X86_USER_SHADOW_STACK과 CONFIG_X86_KERNEL_IBT로 사용자/커널 공간(Kernel Space) CET를 지원합니다. arch_prctl(ARCH_SHSTK_ENABLE)으로 사용자 공간 Shadow Stack을 활성화할 수 있습니다.
GNU Property Notes (.note.gnu.property)
.note.gnu.property 섹션은 ELF 바이너리의 하드웨어/보안 기능 요구사항을 선언합니다. PT_GNU_PROPERTY 세그먼트로 로드되어 커널과 동적 링커가 바이너리 실행 시 기능 호환성을 검증하고 보호 메커니즘을 활성화합니다.
| 속성 타입 | 값 | 설명 | 용도 |
|---|---|---|---|
GNU_PROPERTY_X86_FEATURE_1_IBT | 0x1 | Indirect Branch Tracking (CET-IBT) 활성 | 모든 간접 분기 대상이 ENDBR64로 시작해야 함 |
GNU_PROPERTY_X86_FEATURE_1_SHSTK | 0x2 | Shadow Stack (CET-SS) 활성 | 하드웨어 Shadow Stack으로 ROP 공격 방어 |
GNU_PROPERTY_X86_ISA_1_NEEDED | 0xc0008002 | 필요한 ISA 기능 비트마스크 | SSE, SSE2, AVX, AVX2 등 최소 CPU 요구사항 |
GNU_PROPERTY_X86_ISA_1_USED | 0xc0010002 | 사용된 ISA 기능 비트마스크 | 실제 사용된 명령어 집합 (정보용) |
GNU_PROPERTY_AARCH64_FEATURE_1_BTI | 0x1 (aarch64) | Branch Target Identification | ARM64 간접 분기 보호 (CET-IBT의 ARM 대응) |
GNU_PROPERTY_AARCH64_FEATURE_1_PAC | 0x2 (aarch64) | Pointer Authentication | ARM64 포인터 인증 코드로 ROP/JOP 방어 |
# GNU Property 확인
$ readelf -n /bin/ls | grep -A5 'GNU.*property'
Displaying notes found in: .note.gnu.property
Owner Data size Description
GNU 0x00000030 NT_GNU_PROPERTY_TYPE_0
Properties: x86 feature: IBT, SHSTK
x86 ISA needed: x86-64-baseline
# CET 지원 바이너리 빌드
$ gcc -fcf-protection=full -o test test.c
# → .note.gnu.property에 IBT+SHSTK 속성 기록
# → 모든 함수 시작에 ENDBR64 삽입
# ARM64 BTI + PAC 활성화 빌드
$ aarch64-linux-gnu-gcc -mbranch-protection=standard -o test test.c
# → .note.gnu.property에 BTI+PAC 속성 기록
# PT_GNU_PROPERTY 세그먼트 확인
$ readelf -l /bin/ls | grep PROPERTY
GNU_PROPERTY 0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R 0x8
# x86-64 ISA 레벨 (glibc 2.33+)
# x86-64-v1: 기본 (SSE2), v2: +SSE4/POPCNT, v3: +AVX2/FMA, v4: +AVX-512
$ gcc -march=x86-64-v3 -o test test.c
# → ISA_1_NEEDED에 x86-64-v3 비트 설정
# → v3 미지원 CPU에서 실행 시 glibc가 오류 메시지 출력
CET 호환성과 동적 링킹: CET(IBT/SHSTK)는 프로세스 내 모든 ELF 모듈이 해당 속성을 가져야만 활성화됩니다. 동적 링커는 로딩하는 모든 공유 라이브러리의 .note.gnu.property를 검사하여, 하나라도 CET 미지원이면 해당 보호를 비활성화합니다. 이는 레거시 라이브러리와의 호환성을 보장하지만, 보안 수준은 가장 약한 모듈에 의해 결정됩니다. LD_DEBUG=properties로 각 라이브러리의 속성 검사 과정을 추적할 수 있습니다.
ELF 분석 플레이북
ELF 이슈는 컴파일/링크 단계, 로더 단계, 런타임 단계로 나눠서 접근하면 원인 축소가 빠릅니다.
단계별 진단 절차
- 파일 형식 확인:
file,readelf -h로 ELF 타입, 클래스(32/64), 아키텍처, 엔디안 점검 - 세그먼트/섹션 구조 확인:
readelf -l(세그먼트),readelf -S(섹션)으로 매핑과 구조 분석 - 심볼/의존성 확인:
readelf -s(심볼),readelf -d(동적),ldd(의존성)로 미해결 참조 추적 - 재배치 확인:
readelf -r로 재배치 타입과 대상 심볼 점검 - 보안 속성 확인:
checksec또는 수동으로 PIE, RELRO, NX, canary 점검 - 런타임 진단:
LD_DEBUG,/proc/pid/maps,strace로 실제 동작 추적
주요 도구 레퍼런스
# ─── 정적 분석 ───
readelf -h binary # ELF 헤더 (타입, 아키텍처, 엔트리포인트)
readelf -l binary # 프로그램 헤더 (세그먼트, 메모리 매핑)
readelf -S binary # 섹션 헤더 (전체 섹션 목록)
readelf -s binary # 심볼 테이블 (.symtab + .dynsym)
readelf -d binary # 동적 섹션 (DT_NEEDED, SONAME 등)
readelf -r binary # 재배치 엔트리
readelf -n binary # 노트 섹션 (Build ID, GNU Property)
readelf -V binary # 심볼 버전 정보
readelf -I binary # 해시 테이블 히스토그램
readelf -p .comment binary # 컴파일러 버전 문자열
objdump -d binary # 디스어셈블리
objdump -R binary # 동적 재배치
objdump -j .plt -d binary # PLT 엔트리 디스어셈블
nm -D binary # 동적 심볼 (정렬 정의/미정의)
nm -n binary # 주소순 심볼 정렬
nm -C binary # C++ 심볼 디맹글링
ldd binary # 의존 공유 라이브러리 (보안 주의)
checksec --file=binary # 보안 속성 종합 점검
eu-readelf -w binary # 모든 DWARF 정보 출력
# ─── 런타임 진단 ───
LD_DEBUG=all ./binary 2>&1 # 전체 동적 링킹 과정
LD_DEBUG=libs ./binary 2>&1 # 라이브러리 검색 과정
LD_DEBUG=symbols ./binary 2>&1 # 심볼 검색 과정
LD_DEBUG=bindings ./binary 2>&1 # 심볼 바인딩 결과
LD_DEBUG=versions ./binary 2>&1 # 버전 매칭
LD_DEBUG=statistics ./binary 2>&1 # 성능 통계
LD_SHOW_AUXV=1 ./binary # Auxiliary Vector 출력
LD_TRACE_LOADED_OBJECTS=1 ./binary # ldd와 동일 (직접 실행)
strace -e trace=openat ./binary # 파일 접근 추적
ltrace ./binary # 라이브러리 함수 호출 추적
cat /proc/pid/maps # 메모리 매핑
cat /proc/pid/smaps # 상세 메모리 사용량
pmap -x pid # 메모리 매핑 요약
흔한 ELF 오류와 해결
| 증상 / 에러 메시지 | 원인 | 진단 및 해결 |
|---|---|---|
Exec format error (ENOEXEC) |
아키텍처/ABI 불일치, 또는 ELF 매직 손상 | readelf -h로 e_machine/e_class 확인. binfmt_misc QEMU 등록 여부 점검 |
error while loading shared libraries: libfoo.so: cannot open |
공유 라이브러리 경로 미등록 | ldconfig 실행, LD_LIBRARY_PATH 설정, 또는 -Wl,-rpath로 빌드 시 경로 지정 |
undefined symbol: foo |
심볼 미정의 (라이브러리 누락 또는 버전 불일치) | nm -D로 라이브러리에 심볼 존재 확인. readelf -V로 버전 매칭 확인 |
symbol lookup error: ... version 'GLIBC_2.xx' not found |
glibc 버전이 바이너리 요구사항보다 낮음 | readelf -V binary로 요구 버전 확인. glibc 업그레이드 또는 하위 호환 빌드 |
Unknown symbol in module (커널 모듈) |
커널 심볼 export 누락 또는 MODVERSIONS 불일치 | modinfo로 vermagic 확인. nm으로 심볼 존재 확인. 커널 헤더 버전 일치 여부 |
SIGBUS / Bus error |
정렬 오류 (ARM), 매핑 범위 밖 접근, truncated 파일 매핑 | readelf -l로 p_align 확인. /proc/pid/maps로 매핑 범위 점검 |
디버깅 심볼 누락 (no debug info) |
stripped 바이너리, 디버그 패키지 미설치 | file binary로 stripped 확인. debuginfod 설정 또는 -dbg 패키지 설치 |
relocation truncated to fit: R_X86_64_32S |
-fPIC 없이 공유 라이브러리 빌드, 또는 2GB 초과 코드 모델 | -fPIC 추가, 또는 -mcmodel=large 사용 (커널 모듈은 -mcmodel=kernel) |
Segfault in ld.so / 동적 링커 크래시 |
손상된 ELF, GOT/PLT 부정합, 호환 불가 라이브러리 조합 | LD_DEBUG=all로 크래시 전 마지막 심볼/라이브러리 확인. eu-elflint로 ELF 무결성(Integrity) 검증 |
cannot allocate memory in static TLS block |
dlopen()된 라이브러리가 Initial-Exec TLS 사용 | -ftls-model=global-dynamic으로 재빌드하거나, LD_PRELOAD로 미리 로드 |
| GOT overwrite / 제어 흐름 하이재킹 | Partial RELRO 상태에서 GOT 오버라이트 공격 | -Wl,-z,relro,-z,now로 Full RELRO 빌드. checksec으로 검증 |
ELF 무결성 검증 도구
# eu-elflint — ELF 규격 준수 검증 (elfutils)
$ eu-elflint --strict /bin/ls
# 구조적 오류, 정렬 문제, 잘못된 참조 등 검출
# objcopy — 섹션 조작
$ objcopy --remove-section=.comment binary # 특정 섹션 제거
$ objcopy --add-section .custom=data binary # 섹션 추가
$ objcopy --compress-debug-sections=zlib binary # 디버그 섹션 압축
# eu-elfcompress — 디버그 섹션 압축 (ELF SHF_COMPRESSED)
$ eu-elfcompress --type=zstd binary # zstd 압축 (최신)
# 압축 비율: 디버그 정보 ~75% 크기 감소
# 크로스 아키텍처 ELF 분석
$ readelf -h arm_binary
Machine: ARM
Class: ELF32
Data: 2's complement, little endian
# readelf/objdump는 크로스 분석 가능 (실행은 qemu 필요)
관련 문서
ELF와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.