크래프톤 정글 (컴퓨터 시스템: CSAPP)/7장 링커

컴퓨터 시스템 : CSAPP 7장 정리 - 7.1 ~ 7.5

고웅 2025. 4. 17. 19:23

7장 : 링커 (Linking)

링킹이란?

링킹(Linking)은 하나 이상의 목적 파일(Object Files)을 결합하여 단일 실행 파일(Executable File)로 만드는 과정을 의미한다. 프로그램이 여러 파일로 나눠져 개발될 경우, 각 파일은 개별적으로 컴파일된 뒤에 링커에 의해 결합된다.

링커는 단순히 파일들을 이어 붙이는 수준을 넘어서 다음과 같은 중요한 일을 한다.

  1. 심볼 결합(Symol Resolution)

함수나 변수 이름과 같은 심볼을 찾아 서로 연결한다. 예를 들어 main.c에서 sum() 함수를 호출하면, 링커는 sum.csum() 정의를 찾아 연결한다.

  1. 재배치(Relocation)

각 목적 파일의 코드와 데이터를 실행 가능한 주소로 조정한다. 각 파일은 자신의 시작 주소가 0인 것처럼 작성되었기 때문에, 실제 실행할 때는 이를 메모리상의 올바른 위치로 바꿔줘야 한다.


링킹을 배우는 이유

  1. 링커 오류 이해 및 해결
    undefined reference, multiple definition과 같은 링크 오류는 종종 초보 개발자에게 혼란을 준다. 링킹 개념을 이해하면 이런 오류의 원인을 쉽게 파악하고 해결할 수 있다.
  2. 라이브러리 사용
    정적/동적 라이브러리를 사용할 때 링커의 동작 방식을 이해하면, 어떤 함수가 어디서 오는지, 어떤 파일을 포함해야 하는지 쉽게 알 수 있다.
  3. 프로그램 구조 최적화
    프로그램의 메모리 사용, 모듈화, 배포 방식을 고려한 설계가 가능해진다.
  4. 보안 및 성능 향상
    보안 취약점(예: 함수 인터셉팅, 주소 변조 등)과 성능 이슈를 예방할 수 있다.

7.1절 : 컴파일러 드라이버 (Compiler Drivers)

컴파일러 드라이버는 전체 빌드 프로세스를 자동으로 수행해 주는 명령어 인터페이스이다. 대표적인 예로 gcc를 들 수 있다.

예시: gcc -Og -o prog main.c sum.c
이 명령 하나로 다음 과정을 자동으로 수행한다:

  1. 전처리 (Preprocessing)
    main.cmain.i
    • 매크로 치환, #include 처리 등
  2. 컴파일 (Compilation)
    main.imain.s
    • C 소스를 어셈블리로 변환
  3. 어셈블 (Assembly)
    main.smain.o
    • 어셈블리 코드를 기계어로 변환
  4. 링킹 (Linking)
    main.o, sum.oprog
    • 실행 가능한 바이너리 생성

이처럼 컴파일러 드라이버는 복잡한 빌드 과정을 한 줄의 명령어로 실행할 수 있게 해 준다.


7.2절 : 정적 링킹(Static Linking)

정적 링킹은 링커가 여러 개의 재배치 가능한 목적 파일(relocatable object files)을 입력으로 받아, 실행 가능한 완전히 연결된 실행 파일(executable object file)을 출력으로 생성하는 과정이다.

정적 링커의 역할

정적 링커는 아래 두 가지 주요 작업을 수행한다.

  1. 심볼 결합(Symbol Resolution)
  • 심볼(symbol) 은 함수, 전역 변수, 또는 static으로 선언된 정적 변수를 의미한다.
  • 목적 파일은 이러한 심볼을 정의하거나 참조한다.
  • 심볼 결합의 목적은 모든 참조가 정확히 하나의 정의로 연결되도록 하는 것이다.
  1. 재배치(Relocation)
  • 컴파일러와 어셈블러는 코드와 데이터를 주소 0에서 시작하는 것으로 가정하고 생성한다.
  • 링커는 실제 실행 주소에 맞춰 각 심볼에 메모리 주소를 할당하고, 모든 참조를 이 주소로 수정해야 한다.
  • 이 작업은 어셈블러가 생성한 재배치 정보(relocation entries)를 기반으로 수행된다.

7.3절 : 객체 파일(Object Files)

객체 파일이란?

객체 파일은 컴파일러와 어셈블러가 생성하는 바이너리 코드와 데이터의 컨테이너다. 이들은 프로그램을 실행 파일로 만들기 위해 중간 단계로 사용되며, 세 가지 주요 형태로 나뉜다.

  1. 재배치 가능 객체 파일(Relocatable Object File)
    • 예: main.o
    • 다른 객체 파일들과 함께 링크되어 실행 파일을 만들 수 있도록 구성된 파일.
    • 주로 정적 링킹 시 사용.
  2. 실행 객체 파일(Executable Object File)
    • 예: a.out, prog
    • 메모리에 바로 로드되어 실행 가능한 완전한 형태의 파일
    • 링커가 최종적으로 생성하는 결과물
  3. 공유 객체 파일(Shared Object File)
    • 예: libc.so, libm.so
    • 실행 시간 또는 로드 시간에 동적으로 링크될 수 있는 특별한 객체 파일
    • 동적 라이브러리의 형태로 사용

객체 파일 포맷

객체 파일은 시스템마다 포맷이 다르지만, 이 책에서는 주로 ELF(Executable and Linkable Format)를 중심으로 설명한다.

  • ELF 포맷은 리눅스 및 유닉스 시스템에서 표준으로 사용됨
  • 다른 포맷:
    • Windows: PE (Portable Executable)
    • macOS: Mach-O
    • 초기 유닉스: a.out

그림 7.3

ELF 객체 파일의 구조 (그림 7.3 기준)

객체 파일은 여러 섹션(section)으로 구성되며, 대표적인 것들은 다음과 같다.

  • .text: 기계어로 변환된 실행 코드
  • .rodata: 읽기 전용 데이터 (예: 문자열 상수)
  • .data: 초기화된 전역/정적 변수
  • .bss: 초기화되지 않은 전역/정적 변수 (파일 공간은 차지하지 않음)
  • .symtab: 함수와 전역 변수에 대한 심볼 테이블
  • .rel.text, .rel.data: 재배치를 위한 정보
  • .debug: 디버깅 정보를 포함 (옵션)

7.4 재배치 가능 객체 파일(Relocatable Object Files)

이 절에서는 ELF(Executable and Linkable Format) 형식의 재배치 가능 객체 파일의 구조와 역할을 설명한다. 이런 파일은 정적 링커의 입력으로 사용되며, 여러 개의 목적 파일이 실행 파일로 결합되기 전에의 형태이다.

ELF 헤더

ELF 헤더는 다음과 같은 정보를 포함하고 있다.

  • 생성된 시스템의 워드 크기와 바이트 순서 (리틀/빅 엔디안)
  • 파일의 유형 (재배치 가능, 실행 가능, 공유)
  • 사용된 기계 아키텍처 (예: x86-64)
  • 섹션 헤더 테이블의 위치, 크기, 항목 수

이 정보는 링커가 이 파일을 해석하고 결합할 수 있도록 돕는 메타데이터다.

주요 섹션들

ELF 재배치 가능 객체 파일은 여러 섹션(section)으로 나뉘며, 각 섹션은 특정한 목적을 가진다.

섹션 이름 설명
.text 컴파일된 기계어 코드
.rodata 읽기 전용 데이터 (예: 문자열 상수)
.data 초기화된 전역/정적 변수
.bss 초기화되지 않은 전역/정적 변수, 파일 내 공간은 차지하지 않음
.symtab 심볼 테이블, 함수와 전역 변수 정보 포함
.rel.text .text에서 수정이 필요한 위치 정보
.rel.data .data에서 수정이 필요한 위치 정보
.debug 디버깅 정보, -g 옵션 사용 시 포함됨

.bss의 의미

.bss는 "block started by symbol"이라는 오래된 어셈블리 언어 용어에서 유래되었으며, 현재는 초기화되지 않은 데이터를 위한 공간을 의미한다. 이 공간은 디스크 파일 내에서 공간을 차지하지 않기 때문에 저장 공간을 절약할 수 있다.


7.5 심볼과 심볼 테이블 (Symbols and Symbol Tables)

심볼(Symbol)이란?

심볼은 변수나 함수의 이름처럼 컴파일 시점이나 링크 시점에 의미를 가지는 식별자이다. 객체 파일은 어떤 심볼을 정의하거나 참조하고 있고, 이러한 정보를 심볼 테이블(symbol table)에 저장한다.

심볼의 세 가지 종류

링커의 관점에서 심볼은 세 가지로 나눌 수 있다:

  1. 정의된 전역 심볼 (Global Symbols Defined by Module)
    • 이 모듈에서 정의되었고, 다른 모듈에서도 참조 가능한 심볼
    • 예: non-static 함수, 전역 변수
  2. 참조만 하고 다른 모듈에 정의된 전역 심볼 (Global Symbols Referenced by Module)
    • 이 모듈에서는 정의하지 않았지만, 다른 모듈에 의해 정의됨
    • 예: 외부 함수 호출
  3. 로컬 심볼 (Local Symbols)
    • 이 모듈에서만 정의되고 참조되는 심볼
    • 예: static으로 선언된 함수 또는 전역 변수
    • 다른 모듈에서는 참조 불가능

⚠️ 주의: 로컬 변수(local variable)는. symtab 테이블에 포함되지 않는다. 그들은 런타임에 스택(stack)에서 관리되므로 링커는 신경 쓰지 않는다.

static 지역 변수는 예외?

재미있는 예외가 있다. 함수 안의 static 지역 변수는 스택이 아니라 .data 또는 .bss에 저장되고, 링커 심볼로 관리된다.

int f() {
    static int x = 0;
    return x;
}

int g() {
    static int x = 1;
    return x;
}

이 경우, 컴파일러는 두 x를 구분하기 위해 각각 x.1, x.2와 같은 고유한 심볼 이름을 생성한다.

ELF 심볼 테이블 구조

ELF의 심볼 테이블은 .symtab 섹션에 있으며, 각 엔트리는 다음과 같은 정보를 포함한다.

  • 이름(name): 문자열 테이블의 오프셋
  • 값(value): 정의된 객체의 주소 (재배치 파일이면 섹션 기준 오프셋, 실행 파일이면 런타임 주소)
  • 크기(size): 객체의 바이트 크기
  • 종류(type): 함수인지, 데이터인지
  • 바인딩(binding): 전역/로컬 여부
  • 섹션(section): 이 심볼이 속한 ELF 섹션 정보
typedef struct {
    int name;
    char type:4, binding:4;
    char reserved;
    short section;
    long value;
    long size;
} Elf64_Symbol;