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 = undefinedStatic 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 reference 와 duplicate symbol 만 만나도 안다.
실용 권고: 작은 프로젝트는 static link 가 간편 (Go 가 default). 큰 시스템은 dynamic link 가 메모리/배포 이점. Docker container 가 이 trade-off 를 많이 완화 — 둘 중 어느 쪽이든 격리됨.