동적 링크: 프로그램 실행 시의 마법
우리가 일반적으로 사용하는 개발 환경은 컴파일과 링크를 하나로 묶은 통합 환경이기 때문에, 링크 과정과 같은 저수준의 세부 사항에 대해 깊이 생각해본 적이 없을 수 있습니다. 이 글은 동적 링크의 핵심 개념을 간단히 정리하는 것을 목표로 합니다. 링크 과정의 모든 디테일을 다루지는 않지만, 기본 원리를 이해하는 데 도움이 되기를 바랍니다.
리눅스 환경에서는 두 가지 명령어만으로 프로그램 개발을 완료할 수 있습니다.
gcc main.c
./a.out
이 두 명령어는 사실상 4가지 작업을 수행합니다.
- 전처리 (Preprocessing): 주석 제거, 매크로 확장, `#`으로 시작하는 지시어 처리
- 컴파일 (Compilation): 알고리즘 및 문법 관련 처리
- 어셈블 (Assembly): 어셈블리 코드를 기계어로 변환
- 링크 (Linking): 여러 개의 오브젝트 파일을 하나의 실행 파일로 결합
링크 과정 자체는 `ld` 명령어를 통해 명시적으로 확인할 수 있습니다.
ld xxx.o yyy.o -o program
링크의 핵심 과정
링크 과정은 크게 네 가지 단계로 나눌 수 있습니다.
- 주소 및 공간 할당: 각 심볼과 섹션에 가상 메모리 주소를 할당합니다.
- 심볼 참조 관계 결정 (심볼 해석): 프로그램 내의 각 심볼 참조가 정의된 위치와 연결됩니다.
- 관련 .o 파일 병합 (재배치): 여러 오브젝트 파일의 섹션을 하나로 합칩니다.
- 각 심볼의 주소 결정 (재배치): 심볼의 최종 주소를 계산합니다.
- 명령어에 새 주소 채우기 (재배치): 코드 내에서 사용된 심볼 참조를 계산된 주소로 업데이트합니다.
정적 링크 (Static Linking)
정적 링크는 동적 링크에 비해 상대적으로 간단합니다. 프로그램을 여러 작은 모듈로 나누어 개발할 때, 이 모듈들 간의 참조 관계를 링크 시점에 해결하여 하나의 실행 파일로 만드는 방식입니다.
정적 링크는 두 단계로 진행됩니다.
- 주소 및 공간 할당: 각 오브젝트 파일의 섹션 크기와 속성을 분석하여, 병합 후의 전체 크기와 위치를 계산하고 매핑 테이블을 생성합니다.
- 심볼 해석 및 재배치: 심볼 테이블을 기반으로, 각 심볼 참조가 실제 정의된 위치를 찾아 해당 주소로 교체합니다. 이 과정에서 재배치 테이블을 사용하여 상대 주소를 절대 주소로 변환합니다.
여기서 심볼(Symbol)이란 함수나 변수를 의미하며, 심볼 테이블(Symbol Table)은 이러한 심볼의 이름과 주소를 기록하는 테이블입니다.
동적 링크 (Dynamic Linking)
정적 링크는 모든 코드를 실행 파일에 포함시키기 때문에 메모리 사용량이 큰 단점이 있습니다. 이를 해결하기 위해 동적 링크가 등장했습니다.
동적 링크란? 프로그램을 여러 모듈로 컴파일하는 것은 정적 링크와 동일하지만, 링크 과정을 프로그램 실행 시점으로 미루는 방식입니다. 즉, 실행 파일 자체에는 모든 코드가 포함되지 않고, 실행 중에 필요한 라이브러리를 동적으로 로드하여 사용합니다.
주방(메모리)에 모든 재료(함수)를 미리 넣어두는 정적 링크와 달리, 동적 링크는 재료실(공유 라이브러리)에 재료를 보관하고 필요할 때만 가져오는 방식으로 비유할 수 있습니다. 이는 메모리 효율성을 크게 향상시킵니다.
전역 오프셋 테이블 (GOT)과 프로시저 링크 테이블 (PLT)
동적 링크는 .got (Global Offset Table)와 .plt (Procedure Linkage Table)라는 두 가지 중요한 데이터 구조를 사용합니다.
.got: 전역 변수의 실제 주소를 저장하는 포인터 배열입니다..plt: 함수 호출을 처리하는 점프 테이블입니다. 함수를 처음 호출할 때는.got.plt를 통해 동적 링크러(_dl_runtime_resolve)를 호출하여 실제 함수 주소를 찾아옵니다.
지연 바인딩 (Lazy Binding)
동적 링크는 실행 시점에 함수를 찾아오기 때문에 초기 실행 속도가 정적 링크보다 느릴 수 있습니다. 지연 바인딩은 이 성능 문제를 해결하기 위한 기법입니다.
프로그램이 libc.so의 `show()` 함수를 처음 호출하면, .plt는 .got.plt를 통해 _dl_runtime_resolve 함수를 호출하여 `show()`의 실제 주소를 찾아옵니다. 이 주소는 이후 .plt의 해당 엔트리에 저장되므로, 두 번째 호출부터는 바로 해당 함수로 점프하여 빠르게 실행할 수 있습니다.
동적 링크 관련 보안 취약점
동적 링크 메커니즘은 효율적이지만, 악의적인 공격에도 노출될 수 있습니다.
- ret2libc: 공격자가 libc 라이브러리에 포함된 기존 코드 조각(Gadget)을 이용해 시스템 호출을 실행하는 기법입니다.
- GOT/PLT 테이블 조작:
.got.plt테이블의 주소를 조작하여 원하는 함수를 실행하거나 정보를 유출하는 공격입니다. - ret2_dl_resolve:
_dl_runtime_resolve함수의 동작 방식을 악용하는 고급 공격 기법입니다.