사전 공유 라이브러리
개념

공유 라이브러리

gabury1

여러 프로그램이 함께 쓰는 코드를 실행 파일 밖에 따로 떼어 놓은 파일입니다. 프로그램은 실행될 때 이 파일을 찾아 자기 코드에 이어 붙입니다. 같은 파일을 여러 프로그램이 쓰면 그 코드는 메모리에 한 벌만 올라갑니다.

상세

사무실이 여럿 있는 건물에서 사다리를 사무실마다 사 두지 않고 관리실에 한 대만 둡니다. 어느 사무실이든 필요할 때 관리실 것을 씁니다.

공유 라이브러리는 실행 파일과 따로 놓인 코드와 데이터의 묶음입니다. ELF(Executable and Linking Format, 실행·링크 형식) 명세는 이 묶음을 담은 파일을 공유 오브젝트 파일이라고 부릅니다. 이 파일은 두 자리에서 이어 붙습니다. 하나는 링크 편집기가 다른 오브젝트 파일들과 함께 처리해 또 다른 오브젝트 파일을 만들 때입니다. 다른 하나는 동적 링커가 실행 파일 및 다른 공유 오브젝트와 합쳐 프로세스 이미지를 만들 때입니다.

정적으로 링크된 코드는 동적 링커를 거치지 않고 실행 중에 바로 쓸 수 있습니다. 공유 오브젝트는 그렇지 않습니다. 동적 링커가 알맞은 공유 오브젝트 파일을 프로세스 이미지에 붙여야 합니다. 무엇을 붙여야 하는지는 파일 안에 적혀 있습니다. 동적 구조의 DT_NEEDED 항목이 그 프로그램에 필요한 공유 오브젝트를 알려줍니다. 동적 링커는 참조된 공유 오브젝트와 그것들의 의존을 되풀이해 이어 붙여 완전한 프로세스 이미지를 만듭니다. 같은 공유 오브젝트가 의존 목록에 여러 번 나와도 프로세스에는 한 번만 이어 붙습니다.

flowchart TD
    E["실행 파일"] --> D["동적 링커"]
    S1["공유 오브젝트"] --> D
    S2["의존하는 공유 오브젝트"] --> S1
    D --> P["프로세스 이미지"]

이름으로 적힌 참조를 실제 자리로 바꿀 때는 심볼 표를 너비 우선으로 훑습니다. 실행 파일 자신의 심볼 표를 먼저 보고, 그 다음이 DT_NEEDED 항목들의 심볼 표이며, 그 다음이 두 번째 수준의 DT_NEEDED 항목들입니다.

이어 붙는 시점은 둘로 갈립니다. 프로그램이 시작할 때 필요한 묶음을 미리 이어 붙이는 것이 하나입니다. 프로그램이 도는 중에 필요한 순간 이름을 대고 여는 것이 다른 하나입니다. 어느 쪽이든 찾아서 붙이는 일은 동적 링커가 맡습니다. 동적 링커 자신도 보통은 공유 오브젝트로 놓입니다. 이때는 위치 독립으로 적재되어, 붙는 자리가 프로세스마다 다를 수 있습니다.

배경

자주 쓰는 코드를 라이브러리로 모아 두는 일은 오래된 습관입니다. 같은 코드를 다시 짜지 않으므로 개발 시간이 줄어듭니다. 한 번 잡은 오류는 여러 곳에서 되풀이되지 않습니다. 그런데 이 재사용이 링크 시점에서 끝나면 문제의 일부만 풀립니다. 프로세스가 수십 수백 개씩 동시에 도는 시스템에서는 많은 프로세스가 같은 코드 조각을 각자 자기 실행 파일 안으로 들여옵니다.

현대 운영체제의 메모리 관리는 실행 시점에도 코드를 나눠 쓸 수 있게 합니다. 코드를 물리 메모리에 한 번만 올려 둡니다. 여러 프로세스가 가상 메모리를 통해 그 한 벌을 다시 씁니다. 이렇게 쓰이는 라이브러리를 공유 라이브러리라고 부릅니다.

나눠 쓰기에는 조건이 하나 따라붙습니다. 라이브러리를 고칠 때마다 그것을 쓰던 프로그램을 전부 다시 링크해야 한다면, 따로 떼어 놓은 뜻이 사라집니다. 그래서 진입점, 곧 함수와 변수의 주소가 바뀌지 않아야 한다는 요구가 생깁니다.

예시

리눅스의 ld.so

ld.so 와 ld-linux.so* 는 프로그램에 필요한 공유 오브젝트를 찾아 적재합니다. 프로그램이 돌 준비를 시킨 뒤 실행합니다. 의존 이름에 슬래시가 없으면 다음 순서로 찾습니다.

1  DT_RPATH          DT_RUNPATH 가 없을 때만
2  LD_LIBRARY_PATH
3  DT_RUNPATH
4  /etc/ld.so.cache
5  /lib 그리고 /usr/lib

LD_LIBRARY_PATH 는 실행 시점에 ELF 라이브러리를 찾을 디렉터리 목록입니다. 매뉴얼이 적어 둔 실제 호출은 이렇습니다.

터미널
$ LD_LIBRARY_PATH='$ORIGIN/$LIB' prog

LD_PRELOAD 는 다른 것들보다 먼저 적재할 ELF 공유 오브젝트 목록입니다. 이 기능은 다른 공유 오브젝트 안의 함수를 골라 덮어쓰는 데 쓸 수 있습니다.

ldconfig 는 이름이 lib*.so* 인 파일과 동적 로더 자신인 ld-*.so* 만 봅니다. 심볼릭 링크는 이런 꼴을 기대합니다.

libfoo.so -> libfoo.so.1 -> libfoo.so.1.12

가운데 파일인 libfoo.so.1 이 이 라이브러리의 SONAME 입니다.

실행 중에 여는 쪽은 dlopen(3) 입니다. 파일 이름을 문자열로 넘기면 적재된 오브젝트를 가리키는 불투명한 핸들을 돌려줍니다. RTLD_LAZY 를 주면 지연 바인딩을 합니다. 그 심볼을 참조하는 코드가 실제로 실행될 때만 심볼을 풀고, 끝내 참조되지 않으면 끝내 풀지 않습니다.

윈도우의 DLL

DLL(Dynamic-Link Library, 동적 링크 라이브러리)은 다른 모듈이 쓸 수 있는 함수와 데이터를 담은 모듈입니다. 여기서 다른 모듈은 응용 프로그램일 수도 있고 또 다른 DLL 일 수도 있습니다.

마이크로소프트 공식 문서는 여러 응용 프로그램이 같은 기능을 동시에 쓸 때 DLL 이 메모리 부담을 줄이는 데 도움이 된다고 적습니다. 응용 프로그램마다 DLL 데이터는 자기 복사본을 받지만, DLL 코드는 함께 씁니다. 같은 문서는 윈도우 API(Application Programming Interface, 응용 프로그램 인터페이스) 자체가 DLL 묶음으로 구현돼 있어서, 윈도우 API 를 쓰는 프로세스는 모두 동적 링크를 쓴다고 적습니다.

실행 중에 여는 호출은 LoadLibrary 와 LoadLibraryEx 입니다. 시스템이 DLL 을 찾습니다. 찾으면 그 모듈을 프로세스의 가상 주소 공간에 매핑한 뒤 참조 카운트를 올립니다. 부른 프로세스의 주소 공간에 그 DLL 코드가 이미 매핑돼 있으면 기존 매핑의 핸들을 그대로 돌려줍니다.

macOS 의 dylib

애플 공식 문서는 정적 링커로 링크하면 앱이 쓰는 코드가 생성된 실행 파일로 복사된다고 적습니다. 정적 라이브러리를 많이 링크하면 앱 실행 파일이 커집니다. 그리고 정적 라이브러리가 갱신돼도 그것을 쓰던 앱은 그 개선을 받지 못합니다.

같은 문서가 대안으로 적은 것은 필요한 시점에 코드를 주소 공간으로 적재하는 방식입니다. 시작 시점일 수도 있고 실행 중일 수도 있습니다. 이런 라이브러리를 동적 라이브러리라고 부릅니다. 같은 것을 dynamic shared library, shared object, dynamically linked library 로도 부른다고 문서가 덧붙입니다.

앱이 실행되면 커널이 앱의 코드와 데이터를 새 프로세스의 주소 공간에 올립니다. 커널은 동적 로더인 /usr/lib/dyld 도 그 프로세스에 올립니다. 제어는 그 로더에게 넘어갑니다. 그 다음 동적 로더가 앱의 의존 라이브러리를 적재합니다.

정적 링커는 앱을 링크하는 시점에 의존 라이브러리마다 파일 이름을 기록해 둡니다. 이 이름을 install name 이라고 합니다. 동적 로더는 이 이름으로 파일 시스템에서 라이브러리를 찾습니다.

실행 중에 여는 쪽은 dlopen(3), 닫는 쪽은 dlclose(3) 입니다. 이 함수는 현재 프로세스가 특정 동적 라이브러리를 dlopen 으로 연 횟수를 참조 카운트로 셉니다. 그 핸들의 참조 카운트가 0 이 되면 라이브러리가 프로세스 주소 공간에서 내려갑니다.

대가

코드를 실행 파일 안에 복사해 넣지 않는 대신 세 가지를 내줍니다.

실행 시점에 치르는 비용

이어 붙이는 일이 실행 시점으로 옮겨 왔으므로 그 비용도 실행 시점에 생깁니다. Ulrich Drepper 의 「How To Write Shared Libraries」는 재배치가 보통 동적 링커가 하는 일 가운데 가장 비싼 부분이라고 적습니다. 그래서 성능을 끌어올리려면 재배치와 심볼의 수를 될 수 있는 대로 줄이는 것이 중요하다고 덧붙입니다.

같은 문서는 심볼 재배치의 범위를 이렇게 적습니다. 실행 시점에 쓰이는 심볼 가운데, 참조와 같은 오브젝트 안에 정의돼 있다고 링크 시점에 알 수 없는 것 전부입니다. 그리고 심볼 재배치는 비싼 과정이며, 참여하는 공유 오브젝트가 많을수록 또 그 안에 정의된 심볼이 많을수록 심볼 조회가 오래 걸린다고 적습니다.

비용을 언제 치를지는 고를 수 있습니다. 기본은 미루는 쪽입니다. ld.so(8) 매뉴얼에 따르면 LD_BIND_NOW 를 비어 있지 않은 문자열로 두면 동적 링커가 프로그램 시작 시점에 모든 심볼을 풉니다. 두지 않으면 함수 호출 해석을 그 심볼이 처음 참조되는 지점까지 미룹니다. 앞으로 당기면 시작할 때 한꺼번에 치르고, 미루면 도는 동안 흩어서 치릅니다.

판이 어긋나는 자리

파일 이름에 판 번호가 붙고, 그 번호를 어떻게 이어 두느냐가 호환에 걸립니다. ldconfig(8) 매뉴얼은 심볼릭 링크를 libfoo.so -> libfoo.so.1 -> libfoo.so.1.12 꼴로 걸기를 기대하며, 이 패턴을 따르지 않으면 업그레이드 뒤 호환성 문제가 생길 수 있다고 적습니다.

윈도우 쪽 공식 문서는 같은 DLL 의 여러 판이 한 운영체제 안 서로 다른 파일 시스템 위치에 존재하는 것이 흔하다고 적습니다. 전체 경로를 지정하면 어느 자리에서 적재할지 정할 수 있습니다. 그러지 않으면 시스템이 정해진 검색 순서로 찾습니다.

대상 기계에 있어야 하는 파일

코드를 실행 파일 안에 넣지 않았으므로, 실행하는 기계에 그 파일이 있어야 합니다. 애플 공식 문서는 실행 시점에 동적 로더가 앱의 의존 라이브러리를 전부 찾지 못하거나 그중 하나라도 앱과 호환되지 않으면 실행 과정이 중단된다고 적습니다.

관련 항목

이것을 이어 붙이는 주체

동적 링커 · 동적 로더 · 링크 편집기 · 링커 · ld.so · dyld

이것을 대신할 수 있는 다른 수단

정적 링크 · 정적 링커 · 정적 라이브러리

이것을 이루는 구성 요소

ELF · 심볼 · 재배치 · 위치 독립 코드 · PLT(Procedure Linkage Table, 프로시저 연결 표) · GOT(Global Offset Table, 전역 오프셋 표) · DT_NEEDED · PT_INTERP · ABI(Application Binary Interface, 응용 이진 인터페이스)

이것의 검색 경로를 정하는 설정

rpath · LD_LIBRARY_PATH · LD_PRELOAD · ldconfig · install name · DT_RPATH · DT_RUNPATH

이것을 실행 중에 여닫는 호출

dlopen · dlclose · LoadLibrary · LoadLibraryEx

이것이 심볼을 푸는 시점을 정하는 방식

지연 바인딩 · LD_BIND_NOW · RTLD_LAZY

이것을 가리키는 다른 이름

공유 오브젝트 · dylib · 동적 라이브러리

이것이 거치는 처리 단계

재배치 가능 파일 · 실행 파일 · 프로세스 이미지

이것을 가능하게 하는 운영체제 장치

운영체제 · 커널 · 가상 메모리

이것의 판이 갈릴 때 쓰는 장치

SONAME · side-by-side assembly · 매니페스트

다른 이름: shared library · shared object · 공유 오브젝트 · 동적 라이브러리 · DLL · dylib