링커(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컴파일된 프로그램의 머신 코드
.rodataprintf 포맷 스트링, switch 점프 테이블 등 읽기 전용 데이터
.data초기화된 C 전역·정적 변수. 실제 값이 파일에 저장됨 (디스크 공간 차지)
.bss초기화되지 않은(또는 0으로 초기화된) 전역·정적 변수. 위치만 표시하고 실제 공간은 차지하지 않음 (디스크 공간 미차지)
.symtab프로그램에서 정의·참조하는 함수·전역변수 정보를 담는 심볼 테이블
.rel.text링크 시 수정해야 하는 .text 내 위치 리스트
.rel.data모듈이 정의·참조하는 전역변수에 대한 재배치 정보
.debug디버깅 심볼 테이블 (-g 옵션 시 생성)
.lineC 소스 라인 번호와 .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 segmentcode segment 뒤에 위치 (사이에 빈 공간 존재)
Run-time heapdata segment 다음. malloc 호출로 위로 성장
공유 라이브러리 영역공유 모듈을 위해 예약된 영역
사용자 스택가장 큰 합법 주소(2⁴⁸−1) 아래에서 시작해 아래로 성장
커널 영역스택 위. OS 상주 부분(커널 코드·데이터)을 위해 예약

로더 실행 흐름

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

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

© 2022 JeongHwan Yun.