C++ Linking | "링킹" 가이드

C++ Linking | "링킹" 가이드

이 글의 핵심

C++ Linking에 대한 실전 가이드입니다.

들어가며

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


1. 링킹 기본

컴파일과 링킹

# 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

파일 구조:

// 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;
}

링킹 과정

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에 포함 (정적 링킹)

예제:

// 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)'

# 원인: 함수 구현 누락

해결:

# 1. 오브젝트 파일 추가
g++ main.o missing.o -o myapp

# 2. 라이브러리 추가
g++ main.o -lmissing -o myapp

# 3. 소스 파일 추가
g++ main.cpp missing.cpp -o myapp

문제 2: 라이브러리 순서

# ❌ 순서 잘못 (libA가 libB에 의존)
g++ main.o -lB -lA -o myapp

# ✅ 의존성 순서 (의존하는 쪽이 앞)
g++ main.o -lA -lB -o myapp

문제 3: 라이브러리 경로

# ❌ 경로 없음
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 설정 (동적 라이브러리)

# ❌ 실행 시 라이브러리 못 찾음
$ ./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 명령어 (동적 라이브러리 의존성)

# 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 명령어

# objdump: 오브젝트 파일 분석 도구 (디버깅용)

# 디스어셈블: 기계어 → 어셈블리 변환
# -d: disassemble (코드 영역 디스어셈블)
objdump -d myapp
# 실행 파일의 기계어 코드를 어셈블리로 출력
# 함수별 어셈블리 코드 확인 가능

# 심볼 테이블: 모든 심볼 출력
# -t: symbol table
objdump -t myapp
# nm과 유사하지만 더 자세한 정보 제공

# 동적 심볼: 동적 링킹에 사용되는 심볼만
# -T: dynamic symbol table
objdump -T myapp
# 런타임에 로드되는 공유 라이브러리의 심볼 출력

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

LTO 사용

# 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
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

# 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와 함께 사용
  • 빌드 시간 증가 고려
  • 프로덕션 빌드에만 적용

다음 단계

  • C++ Name Mangling
  • C++ Compilation Process
  • C++ Makefile

관련 글

  • C++ Compilation Process |
  • C++ CRTP 완벽 가이드 | 정적 다형성과 컴파일 타임 최적화
  • C++ Dynamic Initialization |
  • C++ 정적 초기화 순서 |
  • C++ Initialization Order 완벽 가이드 | 초기화 순서의 모든 것