사전 동적 링커
개념

동적 링커

gabury1고친 사람 github-actions[bot]

동적 링커는 프로그램을 켤 때 그 프로그램이 쓸 라이브러리 파일을 찾아 메모리에 올려 줍니다. 그리고 라이브러리 함수를 부르는 빈 주소 칸을 올라간 위치로 채웁니다. 이 준비를 끝낸 다음에야 프로그램의 첫 줄이 돕니다.

쉽고 빠른 이해

동적 링커는 프로그램이 시작하기 직전에 심부름을 대신 해 주는 프로그램입니다. 실행 파일에 「글자를 그리는 라이브러리가 필요하다」는 이름만 적혀 있으면, 그 파일을 찾아 메모리에 올리고 함수 주소를 채워 넣습니다.

이게 없으면 라이브러리 코드를 프로그램마다 안에 넣어 두는 수밖에 없습니다. 그러면 같은 코드가 여러 벌 복사됩니다. 라이브러리를 고칠 때마다 그것을 쓰는 프로그램을 전부 다시 빌드해야 합니다.

어떻게 도나:

  1. 운영체제가 프로그램 본문보다 동적 링커를 먼저 띄웁니다
  2. 동적 링커가 실행 파일에 적힌 이름으로 라이브러리 파일을 찾아 메모리에 올립니다
  3. 비워 둔 함수 주소 칸을 채운 다음 프로그램의 첫 줄로 넘깁니다

대가로 시작이 느려집니다. 라이브러리가 없으면 프로그램이 아예 안 뜹니다. 그래서 무엇이 깔려 있을지 모르는 곳에 가져갈 프로그램은 이 방식을 아예 안 쓰기도 합니다.

상세

출장 나온 첫날 아침을 떠올리면 됩니다. 업무 수첩에는 「자재는 창고에 문의」라고만 적혀 있고 창고 내선 번호 칸은 비어 있습니다. 번호는 건물마다 달라서 수첩을 찍을 때 미리 채울 수 없었습니다. 누군가 출근 전에 오늘 건물의 번호를 알아내 그 칸을 메워 두면, 그때부터는 수첩대로 일하면 됩니다.

이 절은 그 심부름꾼이 하는 일을 따라갑니다.

빌드가 남긴 빈칸

빌드의 마지막 단계는 링커입니다. 컴파일러가 소스 파일마다 만들어 놓은 코드 조각을 모아 실행할 수 있는 프로그램 하나로 잇는 도구입니다.

라이브러리를 붙이는 방식은 둘로 갈립니다. 정적 링크는 필요한 라이브러리 코드를 실행 파일 안으로 복사해 넣습니다. 동적 링크를 고르면 링커가 그 복사를 하지 않습니다. 대신 「이런 라이브러리의 이런 함수를 부른다」는 이름만 실행 파일에 적어 둡니다.

이름만 적고 마는 까닭은 주소를 아직 모르기 때문입니다. 라이브러리 파일이 메모리 어느 번지에 올라갈지는 프로그램을 켜 봐야 정해집니다. 실행 파일만 뜯어봐서는 채울 수 없는 값입니다.

그렇다고 프로그램이 자기 손으로 채울 수도 없습니다. 첫 줄이 돌기 전에 이미 채워져 있어야 하니, 그 일을 할 누군가가 프로그램보다 먼저 돌아야 합니다. 그 누군가가 동적 링커입니다. 이것이 없으면 동적 링크라는 방식 자체가 성립하지 않습니다. 라이브러리 파일 하나를 여러 프로그램이 나눠 쓰는 이득도 같이 사라집니다.

flowchart TD
    C["컴파일러가 만든 코드 조각"] --> L["링커"]
    L -->|정적 링크| S["라이브러리 코드를 실행 파일 안에 복사한다"]
    L -->|동적 링크| D["이름만 적어 둔다 · 주소 칸은 빈 채로"]
    S --> R["여기부터 실행할 때"]
    D --> R
    R --> X["정적 쪽 · 준비할 것이 없다"]
    R --> Y["동적 쪽 · 빈 칸을 채워야 한다"]

동적 링크 갈래만 실행할 때 할 일이 남습니다. 그 일을 맡는 것이 동적 링커입니다.

프로그램보다 먼저 도는 프로그램

동적 링커는 운영체제 안에 숨어 있는 기능이 아니라 그 자체로 하나의 프로그램 파일입니다. 실행 파일 안에는 「나를 띄우려면 이 프로그램을 먼저 돌려라」는 경로가 적혀 있습니다.

프로그램을 켜면 운영체제는 그 경로를 읽어 동적 링커를 먼저 메모리에 올립니다. 그리고 프로그램 본문이 아니라 동적 링커부터 돌립니다. 동적 링커는 준비를 마친 뒤 프로그램의 시작 주소로 제어를 넘깁니다.

운영체제가 실행 파일을 읽어 메모리에 올리는 부분이 로더입니다. 동적 링커는 그 뒤를 이어 라이브러리 쪽 준비를 맡는 셈이라 동적 로더라고도 불립니다. 두 이름이 같은 프로그램을 가리키는 때가 많습니다.

동적 링커도 결국 프로그램이라 자기 주소 칸부터 채워야 합니다. 남에게 부탁할 상대가 없으니 그 일만은 스스로 합니다. 자기 준비를 끝낸 다음에 남의 준비를 시작합니다.

sequenceDiagram
    participant 운영체제
    participant 링커 as 동적 링커
    participant 프로그램
    운영체제->>링커: 실행 파일에 적힌 경로로 먼저 띄운다
    Note over 링커: 자기 주소 칸부터 스스로 채운다
    링커->>프로그램: 준비를 마치고 시작 주소로 넘긴다

프로그램은 자기 첫 줄이 돌기 전에 이미 한 차례 일이 끝나 있었다는 것을 모릅니다.

동적 링커가 도는 순서

동적 링커는 넘겨받은 제어권을 들고 아래 순서로 일합니다.

flowchart TD
    A["실행 파일에 적힌 라이브러리 이름 읽기"] --> B["정해진 순서로 파일 찾기"]
    B --> C["찾은 파일을 메모리에 올리기"]
    C --> D{"그 라이브러리가 또 요구하는 파일이 있나"}
    D -->|있다| B
    D -->|없다| E["빈 주소 칸 채우기"]
    E --> F["라이브러리 초기화 코드 돌리기"]
    F --> G["프로그램 첫 줄로 넘기기"]

되돌아가는 화살표가 하나 있습니다. 라이브러리가 또 다른 라이브러리를 요구하는 일이 흔해서, 찾고 올리는 일은 더 요구할 파일이 없을 때까지 되풀이됩니다.

파일을 어디서 찾을지는 미리 정해져 있습니다. 실행 파일 자신에 박아 둔 경로, 환경 변수로 준 경로, 시스템이 관리하는 라이브러리 목록을 정해진 차례로 봅니다. 먼저 걸리는 파일이 채택되므로 순서가 결과를 바꿉니다. 권한이 높은 프로그램을 띄울 때는 환경 변수로 준 경로를 건너뜁니다.

flowchart TD
    P1["실행 파일에 박아 둔 경로"] --> P2["환경 변수로 준 경로"]
    P1 -->|"권한이 높은 프로세스면 건너뛴다"| P3
    P2 --> P3["시스템이 관리하는 라이브러리 목록"]
    P3 --> NF["못 찾으면 오류를 내고 멈춘다"]
    P1 -.->|먼저 걸리면 채택| OK["채택"]
    P2 -.->|먼저 걸리면 채택| OK
    P3 -.->|먼저 걸리면 채택| OK

빈 주소 칸을 채우는 일은 두 걸음입니다. 실행 파일에는 함수와 변수가 이름으로만 적혀 있습니다. 이 이름들을 심볼이라고 합니다.

첫 걸음은 짝짓기입니다. 심볼마다 그 정의를 가진 라이브러리를 찾아 이어 줍니다.

둘째 걸음은 번지 써넣기입니다. 짝지은 대상이 메모리 어느 번지에 올라갔는지를 빈칸에 적습니다. 번지를 써 넣는 이 일이 재배치입니다.

칸들은 실행 파일 안에 모여 있습니다. 이 칸들을 모아 둔 것이 주소 표입니다. 프로그램이 라이브러리 함수로 가는 길은 이 표를 지납니다.

flowchart TD
    subgraph 실행파일["프로세스 메모리 · 실행 파일"]
        코드["프로그램 코드"]
        칸A["주소 표 · 첫째 칸 · 번지가 적혀 있다"]
        칸B["주소 표 · 둘째 칸 · 아직 이름뿐(심볼)"]
    end
    subgraph 라이브러리["메모리에 올라간 라이브러리"]
        함수A["글자를 그리는 함수"]
        함수B["글자를 지우는 함수"]
    end
    코드 --> 칸A
    코드 --> 칸B
    칸A -->|"재배치로 써 넣은 번지"| 함수A
    칸B -.->|"아직 못 잇는다"| 함수B

채워진 칸은 라이브러리 함수까지 이어집니다. 빈 칸은 아직 이름만 들고 있습니다.

초기화 코드는 라이브러리가 「나를 쓰기 전에 이 함수를 한 번 돌려 달라」고 등록해 둔 것입니다. 연결을 미리 열어 두거나 내부 자료 구조를 만들어 두는 일을 여기서 합니다.

도는 순서도 동적 링커가 맞춥니다. A 라이브러리가 B 라이브러리를 쓰고 있으면 B 의 초기화 코드를 먼저 돌립니다.

처음 부를 때까지 미루는 지연 바인딩

켤 때 모든 함수의 주소를 채우면 시작이 느려집니다. 실제로는 한 번도 안 불릴 함수까지 찾아 주게 됩니다.

그래서 라이브러리 함수 호출을 곧장 보내지 않습니다. 대신 작은 중계 코드를 한 번 거치게 합니다.

중계 코드는 앞에서 본 주소 표의 칸을 봅니다. 처음 부를 때는 그 칸이 비어 있습니다. 그러면 중계 코드가 동적 링커를 불러 주소를 알아낸 다음 그 칸에 적습니다. 두 번째부터는 칸에 적힌 주소로 바로 갑니다. 이 방식을 지연 바인딩이라고 부릅니다.

sequenceDiagram
    participant 앱 as 프로그램
    participant 중계 as 중계 코드
    participant 링커 as 동적 링커
    participant 함수 as 라이브러리 함수
    앱->>중계: 라이브러리 함수를 부른다
    Note over 중계: 주소 표의 칸을 본다 · 비어 있다
    중계->>링커: 이 함수의 주소를 찾아 달라
    링커->>중계: 찾은 주소를 칸에 적어 준다
    중계->>함수: 그 주소로 넘긴다
    Note over 앱,중계: 두 번째 호출부터는 칸에 적힌 주소로 바로 간다

동적 링커가 끼어드는 것은 첫 호출 한 번뿐입니다. 대신 실패도 그만큼 늦게 드러납니다. 빠진 함수가 있어도 프로그램은 잘 뜹니다. 그 함수를 처음 부르는 때에 죽습니다.

그래서 켤 때 전부 채우도록 강제하는 설정이 함께 마련돼 있습니다. 프로그램을 띄울 때 환경 변수로 켤 수 있습니다. 빌드할 때 아예 그렇게 만들어 둘 수도 있습니다.

돌다가 직접 여는 라이브러리

지금까지는 동적 링커가 프로그램 시작 전에 알아서 도는 경우였습니다. 프로그램이 돌다가 「이 파일을 지금 열어 달라」고 직접 부탁할 수도 있습니다. 이것이 동적 로딩입니다. 유닉스 계열에서 그 부탁을 받는 함수 이름이 dlopen 입니다.

플러그인 구조가 이 방식을 씁니다. 프로그램은 어떤 플러그인이 깔릴지 모르는 채로 빌드됩니다. 돌다가 폴더를 훑어 있는 파일만 엽니다.

이때는 실패를 프로그램이 직접 받습니다. 파일이 없으면 오류 값이 돌아옵니다. 프로그램은 그 기능만 끄고 계속 돌지 정할 수 있습니다. 시작 전에 나는 실패가 프로그램을 통째로 세우는 것과 다른 점입니다.

동적 링커를 쓰는 대가

시작이 느려집니다. 켤 때마다 파일을 찾고 올리고 주소를 채우는 일을 되풀이합니다. 라이브러리와 심볼이 많을수록 그만큼 늘어납니다.

실패가 켜는 때로 밀립니다. 빌드는 통과합니다. 그런데 라이브러리 파일이 없거나 부르려는 함수가 빠져 있으면 프로그램은 한 줄도 못 돌고 끝납니다.

같은 이름의 함수가 여러 라이브러리에 있으면 먼저 찾은 쪽이 이깁니다. 검색 순서가 승자를 정하므로, 올라오는 파일이 달라지면 부르던 것과 다른 함수가 불릴 수 있습니다.

검색 경로를 바꾸면 엉뚱한 파일이 올라오게 만들 수 있습니다. 권한이 높은 프로세스에서는 이것이 침입 통로가 됩니다. 앞에서 본 환경 변수 건너뛰기가 이 통로를 막으려는 것입니다.

대가를 치를 값이 있는지는 결국 언제 도는지에 달렸습니다. 언제 도는지는 실행 파일이 어떻게 만들어졌는지가 정합니다.

이런 때 동적 링커
실행 파일에 라이브러리 이름만 적혀 있다 켤 때 돈다
라이브러리 코드가 실행 파일 안에 복사돼 있다 돌지 않는다
돌던 프로그램이 플러그인 파일을 연다 그때 다시 돈다

표의 가운데 줄이 정적 링크로 만든 실행 파일입니다. 준비할 것이 없으니 운영체제가 올리자마자 첫 줄이 돕니다. 실행할 컴퓨터에 무엇이 깔려 있는지 모를 때 이 쪽을 고릅니다.

관련 항목

동적 링커가 켤 때 붙이는 파일

공유 라이브러리 · 실행 파일 · 정적 라이브러리 · 목적 파일 · ELF · Mach-O · DLL

동적 링커 노릇을 하는 실제 프로그램

ld.so · dyld · 동적 로더 · 클래스 로더 · 런타임

동적 링커가 읽고 고치는 구성 요소

심볼 · 심볼 테이블 · 재배치 · PLT · GOT · 위치 독립 코드 · 섹션 · SONAME

링크 시점으로 갈리는 이웃 방식

동적 링크 · 정적 링크 · 지연 바인딩 · 동적 로딩 · 프리링킹

동적 링커 앞뒤에 서는 도구

링커 · 정적 링커 · 컴파일러 · 로더 · 커널 · 빌드

동적 링커가 찾을 때 보는 경로와 명령

ldd · rpath · LD_LIBRARY_PATH · LD_PRELOAD · ldconfig · dlopen · dlsym

동적 링커에서 자주 나는 오류·장애

의존성 지옥 · 심볼 충돌 · 미정의 참조 · ABI · 버전 스크립트 · 라이브러리 지옥

동적 링커가 기대는 운영체제 기능

가상 메모리 · 메모리 맵 파일 · 프로세스 · 페이지 캐시 · 주소 공간 배치 무작위화

다른 이름: dynamic linker · dynamic loader · 동적 로더 · 런타임 링커