[2026] C++ 링킹 완벽 가이드 | 정적/동적 링킹과 에러 해결 (undefined reference)

[2026] C++ 링킹 완벽 가이드 | 정적/동적 링킹과 에러 해결 (undefined reference)

이 글의 핵심

C++ Linking의 C++, Linking, 링킹, 1.

들어가며

링킹(Linking)은 오브젝트 파일(.o)을 하나의 실행 파일이나 라이브러리로 결합하는 컴파일의 마지막 단계입니다. 컴파일러는 각 .cpp를 독립적으로 기계어로 변환하고, 링커가 이들을 연결하여 최종 실행 파일을 생성합니다.

실무에서 마주한 현실

개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.

1. 링킹 기본

컴파일과 링킹

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 1단계: 컴파일 (소스 → 오브젝트)
# -c: 컴파일만 수행 (링킹 안함)
# -o: 출력 파일 이름 지정
g++ -c main.cpp -o main.o  # main.cpp → main.o (기계어 코드)
g++ -c util.cpp -o util.o  # util.cpp → util.o (기계어 코드)
# 각 .cpp 파일이 독립적으로 .o 파일로 변환됨
# 이 단계에서는 함수 호출이 아직 연결 안됨 (심볼만 기록)
# 2단계: 링킹 (오브젝트 → 실행 파일)
# 여러 .o 파일을 하나의 실행 파일로 결합
# main.o의 함수 호출을 util.o의 함수 정의와 연결
g++ main.o util.o -o myapp
# 링커가 심볼 해석, 재배치, 최종 실행 파일 생성
# 또는 한 번에: 컴파일 + 링킹
# 내부적으로는 위의 2단계를 자동으로 수행
g++ main.cpp util.cpp -o myapp

파일 구조: 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// util.h
#pragma once
int add(int a, int b);
// util.cpp
#include "util.h"
int add(int a, int b) {
    return a + b;
}
// main.cpp
#include <iostream>
#include "util.h"
int main() {
    std::cout << add(10, 20) << std::endl;
    return 0;
}

링킹 과정

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

1. 심볼 해석 (Symbol Resolution)
   - main.cpp의 add() 호출을 util.o의 add() 정의와 연결
2. 재배치 (Relocation)
   - 최종 메모리 배치에 맞게 주소/오프셋 조정
3. 최종 이미지 생성
   - 실행 파일 또는 라이브러리(.so/.dll) 생성

2. 정적 링킹

정적 라이브러리 생성

# 1. 오브젝트 파일 생성
g++ -c lib.cpp -o lib.o
# 2. 정적 라이브러리 생성 (.a)
# ar: 아카이브 도구 (여러 .o 파일을 하나의 .a 파일로 묶음)
# r: 파일 추가/교체
# c: 아카이브 생성 (없으면)
# s: 인덱스 생성 (심볼 테이블)
ar rcs libmylib.a lib.o
# 결과: libmylib.a (정적 라이브러리)
# 3. 사용: 정적 라이브러리를 링크하여 실행 파일 생성
# -L.: 현재 디렉토리에서 라이브러리 검색
# -lmylib: libmylib.a 링크 (lib 접두사, .a 접미사 자동 추가)
g++ main.cpp -L. -lmylib -o myapp
# 링커가 libmylib.a의 코드를 myapp에 포함 (정적 링킹)

예제: 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// lib.h
#pragma once
int multiply(int a, int b);
// lib.cpp
#include "lib.h"
int multiply(int a, int b) {
    return a * b;
}
// main.cpp
#include <iostream>
#include "lib.h"
int main() {
    std::cout << multiply(5, 6) << std::endl;  // 30
    return 0;
}

특징:

  • ✅ 배포 간단 (단일 실행 파일)
  • ✅ 빠름 (런타임 로딩 없음)
  • ❌ 크기 큼 (라이브러리 코드 포함)
  • ❌ 업데이트 시 재링크 필요

3. 동적 링킹

동적 라이브러리 생성

# Linux/macOS
# 1. 위치 독립 코드 (PIC) 컴파일
# -fPIC: Position Independent Code
#   메모리 어디에 로드되든 실행 가능한 코드 생성
#   공유 라이브러리는 여러 프로세스가 다른 주소에 로드하므로 필수
g++ -fPIC -c lib.cpp -o lib.o
# 2. 동적 라이브러리 생성 (.so)
# -shared: 공유 라이브러리로 링크
g++ -shared lib.o -o libmylib.so
# 결과: libmylib.so (Shared Object)
# 3. 사용: 동적 라이브러리를 링크하여 실행 파일 생성
# -L.: 현재 디렉토리에서 라이브러리 검색
# -lmylib: libmylib.so 링크 (심볼 정보만 기록)
# -Wl,-rpath,.: 링커 옵션 전달
#   -rpath: 런타임 라이브러리 검색 경로를 실행 파일에 포함
#   .: 현재 디렉토리를 검색 경로로 추가
g++ main.cpp -L. -lmylib -Wl,-rpath,. -o myapp
# 4. 실행
./myapp
# 런타임에 동적 링커가 libmylib.so를 메모리에 로드
# 또는: 환경 변수로 라이브러리 경로 지정
LD_LIBRARY_PATH=. ./myapp
# LD_LIBRARY_PATH: 런타임 라이브러리 검색 경로
# Windows
# 1. DLL 생성 (Dynamic Link Library)
# -shared: 공유 라이브러리로 컴파일
g++ -shared lib.cpp -o mylib.dll
# 결과: mylib.dll (Windows 동적 라이브러리)
# 2. 사용: DLL을 링크하여 실행 파일 생성
# -L.: 현재 디렉토리에서 라이브러리 검색
# -lmylib: mylib.dll 링크 (lib 접두사 생략, .dll 자동 추가)
g++ main.cpp -L. -lmylib -o myapp.exe
# 실행 시 mylib.dll이 같은 디렉토리에 있어야 함

특징:

  • ✅ 크기 작음 (라이브러리 공유)
  • ✅ 메모리 효율 (여러 프로세스 공유)
  • ✅ 업데이트 쉬움 (재컴파일 불필요)
  • ❌ 런타임 로딩 필요
  • ❌ 버전 관리 복잡

4. 자주 발생하는 문제

문제 1: undefined reference

# 에러 메시지
undefined reference to `add(int, int)'
# 원인: 함수 구현 누락

해결: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 1. 오브젝트 파일 추가
g++ main.o missing.o -o myapp
# 2. 라이브러리 추가
g++ main.o -lmissing -o myapp
# 3. 소스 파일 추가
g++ main.cpp missing.cpp -o myapp

문제 2: 라이브러리 순서

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# ❌ 순서 잘못 (libA가 libB에 의존)
g++ main.o -lB -lA -o myapp
# ✅ 의존성 순서 (의존하는 쪽이 앞)
g++ main.o -lA -lB -o myapp

문제 3: 라이브러리 경로

아래 코드는 bash를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# ❌ 경로 없음
g++ main.o -lmylib -o myapp
# error: cannot find -lmylib
# ✅ 경로 지정
g++ main.o -L./lib -lmylib -o myapp
g++ main.o -L/usr/local/lib -lmylib -o myapp

문제 4: rpath 설정 (동적 라이브러리)

아래 코드는 bash를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# ❌ 실행 시 라이브러리 못 찾음
$ ./myapp
error while loading shared libraries: libmylib.so: cannot open shared object file
# ✅ rpath 설정 (실행 파일에 검색 경로 포함)
g++ main.o -L./lib -lmylib -Wl,-rpath,./lib -o myapp
# ✅ LD_LIBRARY_PATH 설정
export LD_LIBRARY_PATH=./lib:$LD_LIBRARY_PATH
./myapp
# ✅ 시스템 경로에 설치
sudo cp libmylib.so /usr/local/lib/
sudo ldconfig

5. 심볼 확인

nm 명령어

# nm: 심볼 테이블 확인 도구
# 실행 파일이나 오브젝트 파일의 심볼(함수, 변수) 목록 출력
# 심볼 목록: 모든 심볼 출력
nm myapp
# 정의된 심볼 (T: text section)
# -g: 외부 심볼만 (global)
# grep " T ": 코드 영역에 정의된 심볼만 필터링
nm -g myapp | grep " T "
# 미정의 심볼 (U: undefined)
# -u: 링킹이 필요한 외부 심볼만 출력
nm -u myapp
# 예시 출력:
# 0000000000401136 T main
#   0000000000401136: 메모리 주소
#   T: Text section (코드 영역에 정의됨)
#   main: 심볼 이름
# 0000000000401156 T _Z3addii  (mangled name)
#   _Z3addii: C++ 이름 맹글링 (add(int, int))
#                  U printf@@GLIBC_2.2.5
#   U: Undefined (외부 라이브러리에서 제공)
#   printf@@GLIBC_2.2.5: glibc 버전 2.2.5의 printf

ldd 명령어 (동적 라이브러리 의존성)

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# ldd: 동적 라이브러리 의존성 확인 (Linux)
# 실행 파일이 필요로 하는 모든 공유 라이브러리 출력
$ ldd myapp
    linux-vdso.so.1 (0x00007fff...)
    # linux-vdso: 커널 시스템 콜 최적화 (가상 라이브러리)
    
    libmylib.so => ./lib/libmylib.so (0x00007f...)
    # libmylib.so: 우리가 만든 라이브러리
    # => ./lib/libmylib.so: 실제 파일 경로
    # (0x00007f...): 메모리 로드 주소
    
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
    # libc.so.6: C 표준 라이브러리 (printf, malloc 등)
    
    /lib64/ld-linux-x86-64.so.2 (0x00007f...)
    # ld-linux: 동적 링커 (런타임에 라이브러리 로드)

objdump 명령어

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# objdump: 오브젝트 파일 분석 도구 (디버깅용)
# 디스어셈블: 기계어 → 어셈블리 변환
# -d: disassemble (코드 영역 디스어셈블)
objdump -d myapp
# 실행 파일의 기계어 코드를 어셈블리로 출력
# 함수별 어셈블리 코드 확인 가능
# 심볼 테이블: 모든 심볼 출력
# -t: symbol table
objdump -t myapp
# nm과 유사하지만 더 자세한 정보 제공
# 동적 심볼: 동적 링킹에 사용되는 심볼만
# -T: dynamic symbol table
objdump -T myapp
# 런타임에 로드되는 공유 라이브러리의 심볼 출력

6. 링크 타임 최적화 (LTO)

LTO 사용

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# LTO 활성화 (Link Time Optimization)
# -flto: 링크 타임에 전체 프로그램 최적화 수행
g++ -flto main.cpp util.cpp -o myapp
# 동작:
# 1. 컴파일 시 중간 표현(IR, Intermediate Representation) 생성
# 2. 링크 시 모든 파일의 IR을 함께 분석
# 3. 함수 인라이닝, 데드 코드 제거 등 전역 최적화
# 장점: 성능 향상 (10-20%)
# 단점: 빌드 시간 증가
# 최적화 레벨 조합
# -flto: 링크 타임 최적화
# -O3: 최고 수준 컴파일 타임 최적화
g++ -flto -O3 main.cpp util.cpp -o myapp
# -O3와 -flto를 함께 사용하면 최대 성능 달성
# 릴리스 빌드에 권장

효과:

  • 전체 프로그램 최적화
  • 인라인 확장 (파일 경계 넘어)
  • 데드 코드 제거
  • 성능 향상 (5-15%) 단점:
  • 컴파일 시간 증가
  • 메모리 사용 증가
  • 디버깅 어려움

7. 실전 예제: 프로젝트 빌드

Makefile

다음은 makefile를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# Makefile
CXX = g++
CXXFLAGS = -std=c++17 -Wall -g
LDFLAGS = -L./lib
LDLIBS = -lmylib
OBJS = main.o util.o
myapp: $(OBJS)
	$(CXX) $(OBJS) $(LDFLAGS) $(LDLIBS) -o myapp
main.o: main.cpp util.h
	$(CXX) $(CXXFLAGS) -c main.cpp
util.o: util.cpp util.h
	$(CXX) $(CXXFLAGS) -c util.cpp
clean:
	rm -f $(OBJS) myapp
.PHONY: clean

CMake

다음은 cmake를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MyApp)
set(CMAKE_CXX_STANDARD 17)
# 정적 라이브러리
add_library(mylib STATIC lib.cpp)
# 실행 파일
add_executable(myapp main.cpp util.cpp)
# 링크
target_link_libraries(myapp mylib)
# 동적 라이브러리
add_library(mylib_shared SHARED lib.cpp)
set_target_properties(mylib_shared PROPERTIES OUTPUT_NAME mylib)
# rpath 설정
set_target_properties(myapp PROPERTIES
    INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib"
    BUILD_WITH_INSTALL_RPATH TRUE
)

정리

핵심 요약

  1. 링킹: 오브젝트 파일을 실행 파일로 결합
  2. 정적 링킹: 라이브러리 포함 (크기 큼, 빠름)
  3. 동적 링킹: 런타임 로드 (크기 작음, 공유)
  4. 심볼 해석: 함수 호출을 정의와 연결
  5. LTO: 전체 프로그램 최적화

정적 vs 동적 링킹

특징정적 링킹동적 링킹
파일.a (Linux), .lib (Windows).so (Linux), .dll (Windows)
크기큼 (라이브러리 포함)작음 (참조만)
속도빠름 (로딩 없음)약간 느림 (로딩)
메모리중복 가능공유 가능
업데이트재링크 필요라이브러리만 교체
배포간단 (단일 파일)복잡 (의존성)

실전 팁

링킹 전략:

  • 개발: 동적 링킹 (빠른 빌드)
  • 배포: 정적 링킹 (간단한 배포)
  • 공유 라이브러리: 동적 링킹
  • 임베디드: 정적 링킹 에러 해결:
  • undefined reference: 오브젝트/라이브러리 추가
  • cannot find -l: -L 경로 추가
  • cannot open shared object: rpath 또는 LD_LIBRARY_PATH 설정
  • multiple definition: 중복 정의 제거 최적화:
  • LTO로 전체 프로그램 최적화
  • -O3와 함께 사용
  • 빌드 시간 증가 고려
  • 프로덕션 빌드에만 적용

다음 단계


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3