사전 링커
개념

링커

gabury1고친 사람 github-actions[bot]

링커는 따로 컴파일한 코드 조각들을 이어 붙여 실행할 수 있는 프로그램 하나로 만드는 도구입니다. 한 파일이 다른 파일의 함수를 부르면 그 호출이 실제 함수를 찾아가도록 주소를 채웁니다. 이 주소를 채우는 도구가 없으면 여러 파일로 나눈 프로그램이 서로를 부르지 못합니다. 빌드 순서로는 컴파일러 다음에 도는 마지막 단계입니다.

쉽고 빠른 이해

링커는 흩어진 코드 조각을 하나로 이어 실행 파일을 만듭니다. main.c 가 math.c 의 add 를 부르면, 링커가 그 호출을 add 의 진짜 위치에 연결합니다.

이게 없으면 파일 하나를 고칠 때마다 프로그램 전체를 다시 컴파일해야 합니다. 남이 미리 컴파일해 둔 라이브러리도 가져다 쓸 수 없습니다.

어떻게 도나:

  1. 컴파일러가 소스 파일마다 조각 파일을 하나씩 만듭니다. 이 조각은 남의 함수를 이름으로만 기억합니다
  2. 링커가 조각들을 모아 그 이름이 어느 조각에 정의돼 있는지 짝을 찾습니다
  3. 조각들을 한 파일 안에 배치합니다. 비워 둔 주소 칸은 실제 주소로 채웁니다

C·C++ 처럼 기계어로 컴파일하는 언어의 빌드에서 돕니다. 자바 빌드에는 이 단계가 따로 없고, 같은 일을 실행 중에 자바 가상 머신(JVM, Java Virtual Machine)이 합니다.

대가도 있습니다. 짝을 못 찾으면 빌드가 실패합니다. 컴파일은 이미 통과한 뒤입니다. 라이브러리 코드를 실행 파일에 복사해 넣는 방식을 고르면 파일이 커집니다. 복사하지 않고 실행할 때 붙이는 방식도 있습니다.

상세

책 여러 권으로 나눠 쓴 백과사전을 떠올리면 됩니다. 1권은 「자세한 것은 '광합성' 항목을 보라」고만 적어 둡니다. 그 항목이 몇 권 몇 쪽에 있는지는 전권을 다 찍고 색인을 만들 때 채웁니다.

이 절은 C 파일 두 개가 실행 파일 하나가 되는 과정을 따라갑니다. 먼저 컴파일러가 무엇을 남기는지 봅니다. 이어서 링커가 거기에 하는 두 가지 일을 차례로 봅니다. 뒤에서는 라이브러리를 붙이는 두 방식과 자바 개발자가 링커를 거의 못 보는 이유를 짚습니다.

컴파일과 링크가 나뉘는 이유

C 나 C++ 컴파일러는 소스 파일을 한 번에 하나씩 번역합니다. 번역 결과는 목적 파일입니다. 오브젝트 파일이라고도 합니다. 유닉스 계열에서는 확장자가 .o 입니다. 목적 파일에는 기계어가 들어 있지만 아직 혼자서는 실행되지 않습니다.

파일마다 따로 번역하는 이유는 빌드 시간입니다. 파일이 천 개인 프로젝트에서 하나만 고쳤다면 그 하나만 다시 번역하면 됩니다. 나머지 목적 파일은 지난 빌드의 것을 다시 씁니다. 그러려면 흩어진 목적 파일을 마지막에 하나로 모으는 단계가 따로 있어야 합니다. 그 단계를 맡는 도구가 링커입니다.

아래 예에서 main.c 는 add 를 부르기만 합니다. add 의 본문은 math.c 에 있습니다.

C
/* math.c */
int add(int a, int b) { return a + b; }

/* main.c */
int add(int a, int b);  // 선언만
int main(void) { return add(2, 3); }

main.c 를 번역할 때 컴파일러는 add 가 어디 있는지 모릅니다. 선언 한 줄로 부르는 모양만 압니다. 그래서 호출 명령을 적어 두되 주소 칸은 비워 둡니다.

flowchart TD
    A["main.c"] --> CA["컴파일러"]
    B["math.c"] --> CB["컴파일러"]
    CA --> OA["main.o · add 주소 칸이 빔"]
    CB --> OB["math.o · add 본문이 있음"]
    OA --> L["링커"]
    OB --> L
    L --> E["실행 파일"]

그림에서 컴파일러는 파일마다 따로 두 번 돕니다. 링커는 끝에서 한 번 돕니다. 빈칸을 채우는 일은 링커가 두 목적 파일을 한꺼번에 볼 때에야 할 수 있습니다.

심볼 해석

목적 파일은 자기 안의 이름들을 표로 들고 다닙니다. 함수나 전역 변수의 이름을 심볼이라고 합니다. 그 이름들을 담은 표가 심볼 테이블입니다.

표에는 두 종류가 섞여 있습니다. 하나는 이 파일이 정의한 심볼입니다. 다른 하나는 쓰기만 하고 정의는 다른 파일에 기대는 심볼이고, 이것을 미정의 심볼이라고 합니다.

main.o 의 표에는 main 이 정의로, add 가 미정의로 올라 있습니다. math.o 의 표에는 add 가 정의로 올라 있습니다. 링커는 모든 표를 모아 미정의 심볼마다 정의를 하나씩 짝지어 줍니다. 이 짝짓기가 심볼 해석입니다.

짝짓기는 두 방향으로 실패합니다. 정의가 하나도 없으면 링커는 정의되지 않은 참조라는 오류를 냅니다. 같은 이름의 정의가 둘 이상이면 중복 정의 오류를 냅니다. 둘 다 컴파일은 통과한 뒤에 나는 오류라서, 문법이 아니라 파일 구성이 잘못됐다는 신호입니다.

아래 cc 명령은 C 컴파일러입니다. 소스 파일을 컴파일한 뒤 이어서 링커까지 불러 주므로, 한 줄만 쳐도 두 단계가 모두 돕니다.

$ cc main.c        // math.c 를 빠뜨림
undefined reference to `add'

math.c 를 빼고 빌드하면 이런 오류가 납니다. main.c 자체에는 문제가 없어서 컴파일은 끝났습니다. 그다음 링커가 add 의 정의를 못 찾은 것입니다.

재배치

심볼마다 짝을 찾았으면 이제 실제 주소를 정합니다. 링커는 목적 파일들의 기계어 영역을 이어 붙여 한 덩어리로 배치합니다. 배치가 끝나야 add 가 몇 번지에서 시작하는지 정해집니다.

주소가 정해지면 링커는 비워 둔 주소 칸을 실제 주소로 채웁니다. 이 일이 재배치입니다.

링커가 빈칸의 위치를 아는 것은 목적 파일이 메모를 남겨 두기 때문입니다. 메모에는 「이 위치에 add 의 주소를 넣어라」 같은 내용이 적힙니다. 이 메모를 재배치 항목이라고 합니다. 링커는 배치를 마친 뒤 메모를 하나씩 읽고 빈칸에 주소를 써 넣습니다.

flowchart TD
    subgraph BEFORE["링크 전"]
        M1["main.o · 호출 명령 · 빈 주소 칸"]
        R1["메모 · 이 칸에 add 주소"]
        A1["math.o · add 본문"]
    end
    subgraph AFTER["링크 후"]
        A2["add 본문 · 0x1040 번지에 배치 (예시)"]
        M2["호출 명령 · 주소 칸 = 0x1040"]
    end
    A1 --> A2
    A2 --> M2
    M1 --> M2
    R1 --> M2

위 그림은 빈칸 하나가 채워지는 모습입니다. 번지 값은 설명을 위해 고른 예입니다. 실제 프로그램에는 이런 빈칸이 수천 개 있습니다. 링커는 전부를 같은 방식으로 채우고, 다 채우면 결과를 실행 파일로 씁니다.

라이브러리를 붙이는 두 방식

남이 만든 코드를 쓰는 것도 같은 원리입니다. 라이브러리는 미리 컴파일해 둔 목적 파일 묶음입니다. 문제는 그 코드를 실행 파일 안에 넣느냐, 밖에 두느냐입니다.

정적 링크는 필요한 코드를 실행 파일 안에 복사해 넣습니다. 실행 파일 하나만 옮겨도 돌아갑니다. 다만 라이브러리에 보안 패치가 나오면 그 라이브러리를 쓰는 프로그램을 전부 다시 링크해야 합니다.

동적 링크는 라이브러리 코드를 복사하지 않습니다. 빌드할 때 링커는 실행 파일에 「이 라이브러리가 필요하다」는 이름만 적어 둡니다. 라이브러리 함수를 부르는 주소 칸은 이때 채우지 않고 비워 둡니다.

실제 코드는 공유 라이브러리 파일에 남습니다. 프로그램을 실행할 때 동적 링커가 그 파일을 찾아 메모리에 올립니다. 비워 둔 라이브러리 주소 칸도 그때 채웁니다. 여러 프로그램이 라이브러리 하나를 나눠 쓸 수 있지만, 실행하는 컴퓨터에 그 파일이 없거나 판이 맞지 않으면 시작하자마자 실패합니다.

정적 링크 동적 링크
빈칸을 채우는 때 빌드할 때 실행을 시작할 때
라이브러리 코드 위치 실행 파일 안 실행 파일 밖의 공유 라이브러리
실행 파일 크기 크다 작다
라이브러리 교체 다시 링크해야 한다 파일만 바꾸면 된다
흔한 실패 빌드 중 미정의 참조 실행 중 라이브러리를 못 찾음

표에서 보듯 둘은 라이브러리 빈칸을 채우는 시점이 다릅니다. 도구 이름도 이 시점으로 갈립니다. 빌드할 때 도는 링커를 정적 링커라고 하며, 동적 링크를 쓰는 실행 파일도 이 도구가 만듭니다. 실행할 때 도는 쪽은 동적 링커입니다. 그냥 링커라고 하면 대개 정적 링커를 가리킵니다.

자바 개발자가 링커를 잘 못 보는 이유

자바 컴파일러는 소스 파일마다 클래스 파일을 만들고 거기서 멈춥니다. 여러 클래스 파일을 실행 파일 하나로 잇는 단계가 빌드에 없습니다. JAR(Java ARchive, 자바 아카이브) 파일은 클래스 파일을 압축해 모은 것입니다. 호출과 정의를 이어 두지는 않습니다.

잇는 일은 실행 중에 JVM(Java Virtual Machine, 자바 가상 머신)이 합니다. 클래스 파일은 다른 클래스의 메서드를 주소가 아니라 이름으로 가리킵니다. JVM 은 클래스를 불러올 때나 그 이름을 처음 쓸 때 실제 대상을 찾아 연결합니다. 하는 일은 링커와 같습니다. 시점만 실행 중으로 밀렸습니다.

그래서 자바에서는 링크 오류가 빌드가 아니라 실행 중에 납니다. 컴파일할 때 있던 클래스가 실행 환경에 없으면 NoClassDefFoundError 가 납니다. 메서드가 빠진 판의 라이브러리가 올라오면 NoSuchMethodError 가 납니다. 둘 다 C 의 미정의 참조와 같은 뿌리의 실패입니다.

관련 항목

링커 앞뒤에서 도는 빌드 도구

컴파일러 · 어셈블러 · 로더 · 빌드 · 빌드 시스템 · 툴체인 · Make

링커가 입력으로 받고 출력으로 내는 파일

목적 파일 · 실행 파일 · 정적 라이브러리 · 공유 라이브러리 · ELF · Mach-O · PE · 클래스 파일 · JAR

링커가 파일 안에서 읽고 고치는 구성 요소

심볼 · 심볼 테이블 · 재배치 · 섹션 · 위치 독립 코드 · PLT · GOT · 링커 스크립트

링크 시점에 따라 갈리는 링커의 하위 종류

정적 링커 · 동적 링커 · 정적 링크 · 동적 링크 · 지연 바인딩 · 링크 타임 최적화 · 커널 링커

링커로 구현한 도구

GNU ld · gold · lld · mold · ld.so · dyld

링크 단계에서 터지는 오류

미정의 참조 · 중복 정의 · 이름 맹글링 · ABI · NoClassDefFoundError · NoSuchMethodError · 의존성 지옥

링크를 실행 중으로 미루는 실행 환경

런타임 · JVM · 클래스 로더 · 동적 로딩 · 컴파일 타임

다른 이름: linker · link editor · 링크 편집기 · 연결 편집기