본문으로 건너뛰기
yutils

linker 는 어떻게 동작할까?

static vs dynamic linking, ELF symbol table, 'undefined reference to foo' 의 진짜 의미, GOT/PLT 와 libc.so 의 런타임 resolve, main() 보다 먼저 도는 ld.so 프로그램, DLL hell 이 생기는 이유.

약 10분 읽기

gcc foo.c bar.c 끝에 linker 가 조용히 돈다. undefined reference to 'printf' 같은 에러를 본 적 있다면 그게 linker 의 메시지. main() 보다 먼저 실행되는 ld.so 프로그램, DLL hell 의 원인, 그리고 static vs dynamic 의 trade-off — 이 가이드는 정리한다.

Linker 가 푸는 문제

foo.c:
  extern int g_counter;
  void inc() { g_counter++; printf("inc\n"); }

bar.c:
  int g_counter = 0;
  int main() { inc(); return g_counter; }

각 .c → .o (compile):
  foo.o: inc() 정의, g_counter 와 printf 는 "undefined"
  bar.o: main() 와 g_counter 정의, inc 는 "undefined"

linker 의 일:
  - foo.o + bar.o + libc.so 의 symbol 들을 맞춰서
  - undefined reference 해결
  - 최종 실행 파일 또는 라이브러리 만들기

ELF 파일 구조 (Linux)

$ readelf -S foo.o

ELF Sections:
  .text          ← 실행 코드
  .data          ← 초기화된 전역 변수
  .bss           ← 0 으로 초기화될 전역 변수 (디스크에 0 안 박힘, 메타만)
  .rodata        ← read-only 데이터 (string literal 등)
  .symtab        ← symbol table (이 파일이 정의/참조하는 symbol 목록)
  .strtab        ← symbol name 의 문자열들
  .rel.text      ← relocation 정보 (linker 가 채워야 할 곳)

$ nm foo.o
0000000000000000 T inc        ← T = .text 안 정의됨
                 U g_counter  ← U = undefined
                 U printf     ← U = undefined

Static Linking

gcc -static foo.o bar.o -o program

→ libc.a (static library) 안에서 printf 등 사용된 symbol 추출 → program 에 포함
→ 결과 binary 가 self-contained (다른 .so 없이 실행)
→ binary 크기 ↑ (Go binary 가 큰 이유 — 모든 게 static)

장점:
- 배포 단순 (단일 파일)
- 환경 차이 영향 0 (다른 libc 버전 만나도 OK)
- security: dynamic library 변조 위험 없음

단점:
- 크기 (libc 만 ~2 MB)
- libc update 시 재컴파일 필요 (security patch 등)
- 메모리 — 같은 libc 코드가 N 프로세스마다 복사

Dynamic Linking

gcc foo.o bar.o -o program  (default — dynamic)

→ printf 같은 symbol 은 "libc.so 의 것" 으로 기록만 (실제 코드 X)
→ binary 매우 작음
→ 실행 시 ld.so 가 libc.so 로드 + symbol 연결

$ ldd program
  linux-vdso.so.1 (0x...)
  libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x...)
  /lib64/ld-linux-x86-64.so.2 (0x...)

ld.so — main() 보다 먼저 도는 프로그램

실행 시 kernel 동작:
1. exec("./program")
2. ELF header 파싱 → "interpreter = /lib64/ld-linux-x86-64.so.2"
3. ld.so 를 먼저 로드 + 실행 ← 이게 dynamic linker
4. ld.so 가:
   - 의존 .so 들 (libc, libstdc++ 등) 메모리 로드
   - relocation 처리 (각 함수의 실제 주소 채움)
   - 의존성 cascade (libfoo 가 libbar 필요하면 libbar 도)
5. 다 끝나면 program 의 _start → main() 실행

GOT / PLT — runtime 주소 resolve

printf 의 실제 주소는 실행 시점에 정해짐 (ASLR).
컴파일 시점에는 모름. 어떻게 호출?

GOT (Global Offset Table) — runtime 주소 저장소
PLT (Procedure Linkage Table) — 첫 호출 시 resolve trigger

call printf:
  → PLT 의 printf entry 로 jump
  → 첫 호출이면 ld.so 호출 → libc.so 안 printf 주소 → GOT 에 저장
  → 다음부터는 GOT 에서 즉시 jump (한 번만 lookup, "lazy binding")

$ readelf -r program | grep printf
... R_X86_64_JUMP_SLOT  printf  ← runtime 채워질 곳

"undefined reference to foo" 의 진짜 의미

linker 가 모든 .o + 명시된 라이브러리를 다 보고도 foo 의 정의를 못 찾음.

흔한 원인:
1. .c 파일이 빠짐 (compile 안 됨)
2. -lfoo 같은 라이브러리 link 누락
3. C++ 에서 함수 선언만 있고 정의 없음
4. C++ name mangling — extern "C" 없이 C 함수 호출
5. 라이브러리 link 순서 — gcc 는 left-to-right 처리, 사용자 → 라이브러리 순

해결:
  gcc main.c -lfoo   ← main 이 foo 사용한다면 이 순서 (foo 가 뒤)
  gcc -lfoo main.c   ← X (foo 가 먼저면 main 의 사용 못 봄)

DLL Hell — Windows / 일반 dynamic linking 문제

프로그램 A: libfoo.so 1.0 필요
프로그램 B: libfoo.so 2.0 필요 (API 변경)

system 에 libfoo.so 가 한 버전만 있으면 한쪽이 안 됨.

해결 전략:
1. SONAME — libfoo.so.1, libfoo.so.2 처럼 major version 박음 → 공존
2. RPATH — binary 안에 "내 의존 .so 는 ./lib/ 에서 찾아라" 박음
3. static link — 의존 없음
4. container — 각 app 의 dependency 격리 (Docker)
5. snap / flatpak / AppImage — bundle 형태로 배포

symbol visibility — namespace 충돌

두 라이브러리 모두 hash() 함수 정의 → linker 어떻게?

방법 1 — visibility 제한:
  __attribute__((visibility("hidden"))) int hash() { ... }
  → .so 의 외부 export X, 내부 전용

방법 2 — namespace (C++):
  namespace mylib { int hash() { ... } }
  → mangled name 으로 _ZN5mylib4hashEv 같은 unique 이름

방법 3 — extern "C" + prefix:
  extern "C" int mylib_hash() { ... }

흔한 함정

  • -rdynamic 잊음 — dlsym 으로 symbol lookup 하려면 export 필요.
  • strip 후 lookup 실패 — release build 의 strip 이 dynamic symbol 까지 지우면 dlsym 실패.
  • library version mismatch — 빌드 시 libfoo.so.1 link 했는데 배포 시 libfoo.so.2 만 있음 → 실행 불가.
  • LD_PRELOAD 트릭 — 모든 dynamic linked binary 에 임의 라이브러리 앞에 끼움. malloc 추적, network sniffing 등. 보안 측면에서 양날의 칼.
  • static 과 dynamic 혼합의 ODR 위반 — 같은 symbol 이 두 곳에 있으면 UB.

마무리

Linker 는 "compile 이후" 의 거의 모든 문제의 출처. undefined referenceduplicate symbol 만 만나도 안다.

실용 권고: 작은 프로젝트는 static link 가 간편 (Go 가 default). 큰 시스템은 dynamic link 가 메모리/배포 이점. Docker container 가 이 trade-off 를 많이 완화 — 둘 중 어느 쪽이든 격리됨.

가이드 목록으로