공유 라이브러리와 스터브 (Shared Library & Stub)

핵심 인사이트 (3줄 요약)

  1. 본질: 공유 라이브러리 (Shared Library)는 메모리에 단 하나만 적재되어 여러 프로세스가 동시에 사용할 수 있도록 설계된 코드 덩어리이며, 스터브 (Stub)는 실행 파일이 이 공유 라이브러리의 **실제 메모리 주소를 런타임에 찾을 수 있도록 돕는 작은 연락처 코드(포인터)**이다.
  2. 가치: 스터브 코드는 무거운 라이브러리 코드를 실행 파일에서 완전히 분리해내면서도 컴파일 에러가 나지 않게 "나중에 운영체제가 찾아줄 것"이라는 약속을 제공하여, 프로세스 초기 로딩 속도와 실행 파일의 크기를 혁신적으로 최적화한다.
  3. 융합: 동적 링킹(Dynamic Linking)의 핵심 메커니즘으로 동작하며, 페이지 테이블의 가상 주소 매핑 기술과 결합하여 프로세스 간 완벽한 코드 공유(Code Sharing)를 하드웨어적으로 지원한다.

Ⅰ. 개요 및 필요성 (Context & Necessity)

  • 개념: 스터브(Stub) 코드는 동적 연결 환경에서 외부 공유 라이브러리(Shared Library)의 함수를 호출할 때, 그 함수의 실제 구현체 대신 자리하고 있는 '임시 대역' 또는 '포인터 테이블'이다.

  • 필요성: 실행 파일을 컴파일할 때 링커(Linker)는 printf 같은 함수가 어디 있는지 모르면 에러를 낸다. 하지만 printf 코드를 통째로 복사해서 넣으면 공유 라이브러리의 의미가 퇴색된다. 따라서 링커에게 "실제 코드는 없지만, 나중에 실행될 때 운영체제가 주소를 채워줄 테니 일단 이 작은 명함(Stub)만 넣어둬라"라고 타협할 매개체가 필수적이었다.

  • 💡 비유: 스터브는 마치 핸드폰에 저장된 **배달 음식점 단축번호(연락처)**와 같다. 내 핸드폰 안에 피자(실제 코드)가 들어있는 것은 아니지만, 1번을 누르면(호출) 통신사(OS)가 진짜 피자집(공유 라이브러리)으로 전화를 연결해주어 피자를 먹을 수 있게 해준다.

  • 등장 배경 및 동작 방식의 변화:

    1. 정적 환경: 모든 코드가 내부에 있으므로 스터브가 필요 없다. 함수를 호출하면 JUMP 명령어 뒤에 그냥 내부 절대/상대 주소가 적힌다.
    2. 공유 라이브러리의 등장: 코드를 밖으로 빼냈더니, 링커가 주소를 결정할 수 없는 "미해결 참조 (Unresolved Reference)" 문제가 발생했다.
    3. PLT와 GOT의 도입: 이를 해결하기 위해 실행 파일 안에 PLT (Procedure Linkage Table)라는 스터브 묶음과 GOT (Global Offset Table)라는 빈 주소록을 만들어, 실행 중(Run-time)에 주소를 채워 넣는 우아한 아키텍처가 탄생했다.
┌───────────────────────────────────────────────────────────────────┐
│     정적 링킹 방식과 스터브(Stub)를 활용한 동적 링킹 비교         │
├───────────────────────────────────────────────────────────────────┤
│                                                                   │
│ [정적 링킹의 실행 파일 내부]                                      │
│  ...                                                              │
│  CALL 0x400500 (내부 printf 코드 주소로 직접 점프)                │
│  ...                                                              │
│  0x400500: [printf 함수의 거대한 기계어 코드 덩어리 존재]         │
│                                                                   │
│ [동적 링킹의 실행 파일 내부 (Stub 존재)]                          │
│  ...                                                              │
│  CALL 0x400100 (내부 Stub 주소로 점프)                            │
│  ...                                                              │
│  0x400100 [printf Stub]:                                          │
│      "진짜 주소 알아? 모르면 OS 불러서 DLL에서 찾아!"             │
│      "찾았으면 그 주소로 점프해!"                                 │
└───────────────────────────────────────────────────────────────────┘

[다이어그램 해설] 이 단순한 차이가 프로그램의 크기를 수십 MB에서 수십 KB로 줄이는 마법을 부린다. 동적 링킹을 사용하는 프로그램은 자신이 printf를 사용할 줄 안다고 믿지만, 실제로는 printf의 "껍데기(Stub)"만 가지고 있다. 이 껍데기가 런타임에 운영체제의 도움을 받아 진짜 도서관(Shared Library)의 책 위치를 찾아주는 똑똑한 내비게이션 역할을 한다.

  • 📢 섹션 요약 비유: 스터브 코드는 식당의 '키오스크 메뉴판'입니다. 주방(공유 라이브러리)에서 요리를 직접 만들지만, 손님(프로그램)은 키오스크 버튼(스터브)만 누르면 알아서 요리가 연결되어 나오는 구조입니다.

Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)

구성 요소

요소명역할내부 동작관련 기술비유
PLT (Procedure Linkage Table)스터브 코드들의 집합소각 공유 함수마다 하나의 점프(JUMP) 코드를 보유Stub Code아파트 단지 상가 연락처 모음
GOT (Global Offset Table)실제 물리 주소가 적히는 장부처음엔 동적 링커 주소가, 나중엔 실제 함수 주소가 적힘주소 테이블연락처 옆에 적어두는 실제 전화번호
동적 링커 (Dynamic Linker)런타임에 주소를 찾아주는 해결사라이브러리를 메모리에 올리고 GOT 빈칸을 채움ld-linux.so전화번호 안내원 (114)
공유 라이브러리 (Shared Lib)실제 코드가 담긴 파일물리 메모리에 1개만 로드되어 여러 프로세스와 공유됨.dll, .so실제 요리를 만드는 주방

지연 바인딩(Lazy Binding)을 위한 PLT/GOT 아키텍처

스터브 코드는 한 번 찾은 주소를 캐싱(Caching)하여 두 번째 호출부터는 성능 저하를 없애는 지연 바인딩(Lazy Binding) 기법을 위해 아주 정교하게 설계되어 있다. 이 과정은 PLT와 GOT의 상호작용으로 이루어진다.

┌──────────────────────────────────────────────────────────────────────┐
│              공유 라이브러리 호출 시 Stub(PLT/GOT)의 동작 흐름       │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│ [메인 코드]                                                          │
│ CALL printf@PLT ───────┐                                             │
│                        ▼                                             │
│                [ PLT (Stub 코드) ]                                   │
│ printf@PLT: JMP *printf@GOT ────┐ (GOT에 적힌 주소로 점프)           │
│                                 │                                    │
│   ┌─────────────────────────────┘                                    │
│   │                                                                  │
│   ▼ 1. 최초 호출 시 (주소를 모름)                                    │
│ [ GOT (주소록) ] ──▶ 동적 링커로 다시 돌아감!                        │
│                   동적 링커: "아, 아직 주소를 안 찾았구나."          │
│                   → 메모리에서 printf를 찾음                         │
│                   → GOT의 빈칸을 printf 실제 주소로 덮어씀 (갱신!)   │
│                   → 진짜 printf 실행                                 │
│                                                                      │
│   ▼ 2. 두 번째 호출 시 (주소를 앎)                                   │
│ [ GOT (주소록) ] ──▶ 진짜 printf 코드로 다이렉트 점프!               │
│                   (동적 링커 개입 없음, 오버헤드 0)                  │
└──────────────────────────────────────────────────────────────────────┘

[다이어그램 해설] 이 메커니즘은 해커들이 가장 좋아하는 시스템 해킹 기법(GOT Overwrite)의 무대이기도 하다. 프로그램이 실행될 때 수천 개의 라이브러리 함수 주소를 미리 다 찾아놓으면 부팅이 너무 느려진다. 따라서 최초로 함수가 호출될 때만 동적 링커가 개입하여(느림) 주소를 찾고, 그 결과를 변수(GOT)에 저장해 둔다. 두 번째부터는 스터브(PLT)가 바로 GOT 값을 읽어 실제 메모리로 점프하므로 일반 함수 호출과 속도 차이가 거의 없어진다.


공유 라이브러리의 메모리 맵핑 (Virtual Memory)

물리 메모리에 libc.so가 1000번지에 로드되어 있다면, 수백 개의 프로세스는 이 물리 주소를 자신의 페이지 테이블을 통해 공유한다.

  • 프로세스 A의 0x8000 (가상 주소) -> 물리 1000번지 맵핑

  • 프로세스 B의 0x9000 (가상 주소) -> 물리 1000번지 맵핑 이때, 스터브 코드(GOT)는 각 프로세스의 데이터 영역에 개별적으로 존재하므로, 프로세스마다 각기 다른 가상 주소를 GOT에 적어두고 문제없이 작동할 수 있다.

  • 📢 섹션 요약 비유: 처음 식당에 갈 때는 지도 앱을 켜고 길을 찾아가는 수고(동적 링커 개입)가 들지만, 한 번 찾아간 뒤에는 내 스마트폰 즐겨찾기(GOT)에 등록해 두어 다음부터는 눈 감고도 찾아가는 것(지연 바인딩)과 같습니다.


Ⅲ. 융합 비교 및 다각도 분석

비교 1: 스터브 유무에 따른 링킹 방식 차이

비교 항목정적 링킹 (Static Linking)스터브 기반 동적 링킹 (Dynamic Linking)
코드 위치함수 코드가 바이너리 내부에 직접 존재함수 코드는 외부에, 스터브만 내부에 존재
참조 해결 시점컴파일/링킹 시점 (Compile Time)프로그램 실행 및 최초 호출 시점 (Run Time)
디스크/메모리무거움 (중복 낭비)가벼움 (1회 로드 후 공유)
보안 취약점ROP 공격 시 내부 코드 재사용 가능성 있음GOT Overwrite (주소록 조작) 해킹에 취약

비교 2: PLT (Procedure Linkage Table) vs GOT (Global Offset Table)

스터브 코드를 구현하는 핵심 두 요소의 역할을 명확히 구분해야 한다.

항목PLT (코드 영역)GOT (데이터 영역)
성격실행 가능한 기계어 명령어 (Text Segment)주소를 저장하는 단순 변수/배열 (Data Segment)
권한Read & Execute (읽기, 실행)Read & Write (읽기, 쓰기) - 주소가 갱신되어야 하므로
역할JUMP 명령을 수행하는 로직실제 메모리 주소를 담고 있는 그릇
┌──────────┬────────────┬────────────┬───────────────────────────────┐
│ 구성요소   │ 저장 내용   │ 런타임 변경 여부│ 보안적 위협           │
├──────────┼────────────┼────────────┼───────────────────────────────┤
│ PLT        │ JUMP 코드   │ 변경 안 됨 (R-X) │ 낮음 (실행만 됨)     │
│ GOT        │ 주소 데이터 │ 계속 변경됨 (RW-)│ 매우 높음 (조작 타겟)│
└──────────┴────────────┴────────────┴───────────────────────────────┘

[매트릭스 해설] PLT는 "저기로 점프해"라는 명령어 자체이므로 프로그램이 실행되는 동안 변하지 않는다. 반면 GOT는 "저기가 어디냐면..."이라는 주소값이 적힌 장부이므로, 최초 호출 시 동적 링커에 의해 값이 덮어써져야(Write) 한다. 이 '쓰기 권한(Write)' 때문에 해커들이 GOT의 값을 악성 코드 주소로 변조하는 공격(GOT Overwrite)이 성행했고, 이를 막기 위해 현대 OS는 RELRO(Relocation Read-Only) 같은 메모리 보호 기법을 강제하고 있다.

  • 📢 섹션 요약 비유: PLT가 "비상연락망에 적힌 번호로 전화 걸어"라는 행동 지침(코드)이라면, GOT는 진짜 전화번호가 연필로 적혀 있어서 지우고 새로 쓸 수 있는 화이트보드(데이터)입니다.

Ⅳ. 실무 적용 및 기술사적 판단 (Strategy & Decision)

실무 시나리오: LD_PRELOAD를 이용한 라이브러리 후킹 (Hooking)

  1. 상황: 상용 프로그램의 malloc() (메모리 할당) 함수에 메모리 누수가 있는지 추적하고 싶지만, 소스코드가 없어 수정할 수 없다.
  2. 동적 링킹 스터브의 맹점 활용:
    • 리눅스 환경에서 스터브는 공유 라이브러리를 찾을 때 환경 변수에 지정된 경로를 먼저 검색한다.
    • 개발자는 malloc을 가로채어 로그를 남기는 가짜 malloc을 만들어 hook.so로 컴파일한다.
    • LD_PRELOAD=./hook.so ./program 명령으로 실행한다.
  3. 결과:
    • 프로그램 내의 malloc 스터브가 주소를 찾을 때, 시스템의 진짜 libc.so보다 hook.so를 먼저 발견하여 GOT에 가짜 malloc 주소를 적어버린다. (API Hooking)
    • 이 메커니즘을 통해 소스코드 수정 없이 타 프로그램의 동작을 분석하거나 제어할 수 있으며, 이는 백신(Anti-virus)이나 게임 핵(Hack) 프로그램이 널리 사용하는 방식이다.

안티패턴과 방어 (보안 결함)

  • GOT Overwrite: 스터브 메커니즘의 근본적 취약점이다. 포맷 스트링 버그나 버퍼 오버플로우로 GOT 테이블 조작하여 printf 주소를 system("/bin/sh")로 바꿔치기하면 루트 권한을 탈취당한다.

  • 방어(Full RELRO): 보안이 중요한 데몬(Daemon) 프로그램은 지연 바인딩(Lazy Binding)을 포기하고, 프로그램 시작 시점에 모든 주소를 다 찾아 GOT를 채운 뒤, GOT 영역을 읽기 전용(Read-Only)으로 잠가버리는 컴파일 옵션을 사용한다. (초기 로딩 속도와 보안의 트레이드오프)

  • 📢 섹션 요약 비유: 식당 안내원(동적 링커)이 주는 연락처(GOT)를 아무 의심 없이 맹신하다 보니, 사기꾼이 중간에 "내 번호가 피자집 번호야"라고 가로채기(Hooking) 쉬운 구조적 약점이 있습니다.


Ⅴ. 기대효과 및 결론 (Future & Standard)

정량/정성 기대효과

구분내용
프로그램 경량화수십 MB의 라이브러리 코드를 단 몇 바이트의 JUMP 명령어(Stub)로 대체
운영체제 메모리 절약스터브를 통해 수많은 프로세스가 단 1개의 물리적 공유 라이브러리 페이지를 참조 가능
초기 부팅 최적화지연 바인딩(Lazy Binding) 메커니즘으로 안 쓰는 함수의 주소 계산 오버헤드를 제로화

결론 및 미래 전망

공유 라이브러리 (Shared Library)와 스터브 (Stub) 코드는 단순한 소프트웨어 최적화를 넘어, 하드웨어(MMU 가상 메모리)와 운영체제(Loader/Linker)가 완벽한 삼위일체를 이루어 만들어낸 걸작이다. 이 메커니즘 덕분에 우리는 작은 용량의 앱 수백 개를 램 용량 걱정 없이 동시에 띄울 수 있게 되었다. 현대에는 스터브 기반의 동적 링킹이 가진 런타임 오버헤드와 보안 취약점(GOT 변조)을 방어하기 위해 JIT 컴파일러 고도화, ASLR(주소 공간 배치 무작위화), RELRO 같은 보호 기법이 융합되면서 더욱 단단하고 견고한 아키텍처로 진화하고 있다.

  • 📢 섹션 요약 비유: 수백만 명의 사람이 각자 두꺼운 백과사전을 짊어지고 사는 대신, '도서관 검색대(Stub)'라는 가벼운 단말기 하나만 들고 지식을 무한히 공유하는 스마트 시티의 완성입니다.

📌 관련 개념 맵 (Knowledge Graph)

  • 동적 연결 (Dynamic Linking) | 실행 시간에 라이브러리를 연결하는 전체 과정이자 스터브가 존재하는 근본 이유
  • 지연 바인딩 (Lazy Binding) | 프로그램 부팅 속도를 위해 라이브러리 함수가 실제로 호출되는 순간에만 주소를 찾는 최적화 기법
  • PLT & GOT | 스터브 코드를 실제 리눅스/유닉스 환경에서 구현하는 코드(PLT)와 데이터(GOT) 테이블 쌍
  • 위치 독립 코드 (PIC) | 공유 라이브러리가 메모리 어디에 적재되어도 실행되도록 상대 주소를 사용하는 코드 포맷
  • 페이지 테이블 (Page Table) | 물리 메모리의 공유 라이브러리 1개를 여러 프로세스의 가상 주소로 동시에 맵핑해주는 OS 하드웨어 구조

👶 어린이를 위한 3줄 비유 설명

  1. 스터브 코드가 무엇인가요? 진짜 요리 대신 빈 접시 위에 올려놓은 '식당 전화번호표' 같은 거예요.
  2. 왜 이걸 쓰나요? 모든 프로그램이 요리법을 통째로 들고 있으면 너무 무거우니까, 필요할 때 전화를 걸어 공용 주방(공유 라이브러리)에서 요리를 배달받으려고요.
  3. 어떤 효과가 있나요? 한 번 전화해서 알아낸 주방 위치는 수첩(GOT)에 적어두기 때문에, 두 번째부터는 묻지 않고 1초 만에 빠르게 요리를 가져올 수 있답니다.