링커(Linking): 심볼 해석, 재배치, 그리고 로딩

링킹(linking) 은 여러 개의 코드와 데이터를 모아 연결하여, 메모리에 로드되고 실행될 수 있는 하나의 파일로 만드는 작업이다. 링킹은 수행 시점에 따라 나뉜다.

수행 시점 누가 실행 시기
로드 타임 로더 프로그램이 메모리에 로드될 때
실행 시 응용 프로그램 프로그램 실행 중

1. 링커의 역할

링커는 독립적인 컴파일을 가능하게 한다. 큰 응용 프로그램을 하나의 소스 파일로 만드는 대신, 관리하기 쉬운 작은 모듈들로 나눠 별도로 컴파일·수정할 수 있게 해준다.

  • 모듈 하나를 변경하면, 그 파일만 재컴파일한 뒤 다시 링크하면 된다. 나머지 파일을 재컴파일할 필요가 없다.

링킹을 왜 배워야 할까

  • 큰 규모의 프로그램 개발에서 링커가 참조를 어떻게 해결하는지, 라이브러리가 무엇인지 알면 링커 에러를 해결할 수 있다
  • 언어의 변수 영역 규칙(전역변수와 지역변수의 차이 등)이 어떻게 구현되는지 이해하게 된다
  • 공유 라이브러리의 동작을 이해할 수 있다

2. 정적 연결 (Static Linking)

정적 링커는 재배치 가능 목적파일(.o)들명령줄 인자들 을 입력으로 받아, 로드·실행 가능한 완전히 링크된 실행 파일을 출력한다. 이 과정에서 링커는 두 가지 핵심 작업을 한다.

작업 설명 목적
심볼 해석 (Symbol Resolution) 목적파일들이 정의·참조하는 심볼(함수, 전역변수, static 변수)을 처리 각 심볼 참조를 정확히 하나의 심볼 정의에 연결
재배치 (Relocation) 컴파일러·어셈블러가 0번지 기준으로 만든 코드·데이터 섹션을 실제 메모리 위치에 배치 심볼로 가는 모든 참조를 실제 메모리 위치를 가리키도록 수정

3. 목적파일의 세 가지 형태

형태 설명
재배치 가능 목적파일 (Relocatable) 다른 재배치 가능 목적파일들과 결합해 실행 파일을 만들 수 있는 바이너리 코드·데이터. 예: .o 파일
실행 가능 목적파일 (Executable) 메모리에 직접 복사해 바로 실행할 수 있는 형태
공유 목적파일 (Shared) 로드 타임이나 런타임에 동적으로 링크·로드되는 특수한 재배치 가능 목적파일

정리하면, 컴파일러·어셈블러는 재배치 가능 목적파일을 만들고, 링커는 실행 가능 목적파일을 만든다.

4. ELF 재배치 가능 목적파일 구조

ELF(Executable and Linkable Format) 는 리눅스 같은 유닉스 시스템에서 실행 파일·오브젝트 파일에 쓰는 표준 포맷이다(예: .o, a.out).

ELF 재배치 가능 목적파일은 다음 섹션들로 구성된다.

섹션 내용
ELF header 파일 맨 앞의 정보 블록. “ELF 파일임”을 알리는 16바이트 배열로 시작하며, 워드 크기·바이트 순서·목적파일 타입·머신 타입·section header table 위치/크기 등을 담는다
.text 컴파일된 프로그램의 머신 코드
.rodata printf 포맷 스트링, switch 점프 테이블 등 읽기 전용 데이터
.data 초기화된 C 전역·정적 변수. 실제 값이 파일에 저장됨 (디스크 공간 차지)
.bss 초기화되지 않은(또는 0으로 초기화된) 전역·정적 변수. 위치만 표시하고 실제 공간은 차지하지 않음 (디스크 공간 미차지)
.symtab 프로그램에서 정의·참조하는 함수·전역변수 정보를 담는 심볼 테이블
.rel.text 링크 시 수정해야 하는 .text 내 위치 리스트
.rel.data 모듈이 정의·참조하는 전역변수에 대한 재배치 정보
.debug 디버깅 심볼 테이블 (-g 옵션 시 생성)
.line C 소스 라인 번호와 .text 머신 코드의 매핑 (-g 옵션 시 생성)
.strtab 심볼 테이블·섹션 헤더의 이름을 담는 스트링 테이블 (널 종료 스트링 배열)
Section header table 각 섹션의 위치와 크기를 나타내는 테이블

.data vs .bss 짚고 가기

이 둘은 시험에 자주 나오니 예시로 구분해두자.

int a = 10;          // 초기화된 전역변수  → .data (값 10이 파일에 기록됨)
int b;               // 초기화 안 된 전역  → .bss (공간 미차지, 로드 시 0으로 채워짐)
static int c;        // 초기화 안 된 정적  → .bss
int y = 0;           // 0으로 초기화된 전역 → .bss

void func() {
    int local;       // 지역변수 → 스택에 들어감 (.data/.bss 아님)
}

.bss 변수에 쓰레기 값이 들어가지 않을까 걱정할 수 있는데, 운영체제가 프로그램을 메모리에 올릴 때 .bss 영역을 자동으로 0으로 채운다.

5. 실행 가능 목적파일의 로딩

실행 파일을 다음처럼 실행하면,

linux> ./prog

내부에서는 이렇게 동작한다.

  • prog는 내장 Shell 명령어가 아니므로, Shell은 이를 실행 가능 목적파일로 판단하고 Loader(메모리 상주 OS 코드)를 호출한다. 리눅스에서는 execve 함수가 Loader를 호출한다.
  • Loader는 디스크의 실행 파일 코드·데이터를 메모리로 복사하고, 프로그램의 첫 인스트럭션(Entry point)으로 점프해 실행을 시작한다.
  • 이렇게 프로그램을 메모리로 복사하고 실행하는 과정을 로딩(Loading) 이라 한다.

리눅스 런타임 메모리 이미지

런타임 메모리 이미지

낮은 주소부터 높은 주소 순으로 정리하면 다음과 같다.

영역 위치 / 동작
Code segment 주소 0x400000에서 시작
Data segment code segment 뒤에 위치 (사이에 빈 공간 존재)
Run-time heap data segment 다음. malloc 호출로 위로 성장
공유 라이브러리 영역 공유 모듈을 위해 예약된 영역
사용자 스택 가장 큰 합법 주소(2⁴⁸−1) 아래에서 시작해 아래로 성장
커널 영역 스택 위. OS 상주 부분(커널 코드·데이터)을 위해 예약

로더 실행 흐름

로더가 실행될 때의 workflow는 다음 단계를 거친다.

순서 단계 설명
1 로더 실행 실행 파일 덩어리를 가상 메모리의 code/data segment로 복사
2 Entry point로 점프 crt1.o_start 함수 실행
3 _start__libc_start_main 호출 libc.so__libc_start_main 함수 실행
4 __libc_start_main 동작 실행 환경 초기화 → 사용자 main 함수 호출 → return 값 처리 → 필요 시 제어권을 커널로 반환

© 2022 JeongHwan Yun.