코딩을 하면 #include <stdio.h>를 항상 넣어준다. printf 함수를 사용할 때, printf 함수를 직접 정의하지 않고 libc에 정의된 함수를 가져다가 사용한다. 이를 라이브러리라고 한다. 여러 프로그램이 printf를 사용하는데 그때마다 라이브러리에서 어떻게 가져올지에 따라 static linking과 dynamic linking으로 나뉜다. 라이브러리부터 하나씩 정리해보겠다.
라이브러리
printf, scanf, strlen, memcpy, malloc 등 많은 함수들을 쓸 때, 직접 정의를 할 필요가 없다. 이는 이미 libc(표준 c 라이브러리)에 정의되어 있고 API는 헤더파일(stdio.h, stdint.h, ...)에 선언되어 있다. 그래서 #incldue <stdio.h>를 통해 직접 정의하지 않고 사용할 수 있다.
라이브러리를 사용하면 같은 함수를 반복적으로 정의해야 하는 수고를 덜 수 있어 개발의 효율이 높아진다.
각 언어에서 많이 사용되는 함수들은 표준 라이브러리가 제작되어 있어 쉽게 사용할 수 있다. 대표적으로 c의 표준 라이브러리인 libc에 printf가 정의되어 있다. 리눅스의 경우 /usr/lib/x86_64-linux-gnu/libc-2.31.so를 gdb로 printf 함수를 확인한 내용이다. libc에 printf가 정의되어 있는 것을 알 수 있다.
링크 - 컴파일의 마지막 단계
링크는 컴파일의 마지막 단계로, 호출된 함수와 실제 라이브러리의 함수가 연결되는 과정이다. 이 때, 오브젝트 파일(object file)은 실행 가능한 형식을 가지고 있지만, 라이브러리 함수들의 정의가 어디 있는지 알지 못하므로 실행 불가능하다. printf
함수 선언은 stdio.h에 기록되어 있지만 자세한 내용은 기록되어 있지 않다. 이와 관련한 함수 내용들을 찾아 실행 파일에 기록하는 것이 링크 과정에서 하는 일 중 하나이다.
아래 코드를 컴파일하고 어떤 라이브러리들이 연결되어 있는지 확인해보자
// Compile: gcc -o hello-world hello-world.c
#include <stdio.h>
int main() {
puts("Hello, world!");
return 0;
}
컴파일한 hello-world 바이너리를 readelf -s로 elf 바이너리의 심볼테이블을 확인해보자. puts@GLIBC_2.2.5처럼 libc에서 puts의 심볼을 찾아 연결되어 있는 것을 확인할 수 있다. 또한, ldd
명령어로 바이너리의 라이브러리 의존성을 확인해보면 libc를 같이 컴파일하지 않아도 libc가 연결되어 있는 것을 알 수 있다.
이는 libc가 있는 /lib/x86_64-linux-gnu/가 표준 라이브러리 경로에 포함되어 있어, gcc로 소스 코드를 컴파일할 때 표준 라이브러리 경로에 있는 라이브러리 파일들을 모드 탐색한다. 따라서 링크를 거치고 나면 프로그램에서 puts를 호출할 때, puts의 정의가 있는 libc에서 puts의 코드를 찾고, 해당 코드를 실행한다.
ld 명령을 통해 표준 라이브러리 경로를 확인해보자. ld 명령은 오브젝트 파일, 아카이브, input 파일을 하나의 출력 오브젝트 파일로 결합하고, 외부 참조를 해석한다. --verbose 옵션은 지원되는 링커 에뮬레이션 리스트를 보여준다. 그 결과 다음과 같이 표준 라이브러리 경로가 있는 것을 알 수 있다.
라이브러리는 동적 라이브러리와 정적 라이브러리로 구분되고, 동적 라이브러리를 링크하는 것을 동적 링크(Dynamic Link). 정적 라이브러리를 링크하는 것을 정적 링크(Static Link)라고 한다.
동적 링크(Dynamic Link)
동적 링크는 동적 라이브러리가 프로세스의 메모리에 매핑괸다. 그리고 실행 중에 라이브러리 함수를 호출하면 매핑된 라이브러리에서 호출할 함수의 주소를 찾고, 그 함수를 실행한다.
즉, 공유라이브러리를 사용하여 실행 중에 매핑시키는 것이다. 한번만 메모리에 매핑하고 여러 프로그램이 가져다가 사용하는 것이다. 따라서 실행 파일에 라이브러리 코드가 있지 않아 static. 방식에 비해 크기가 작아진다. 하지만 실행 파일이 라이브러리에 의존해야하기 때문에 라이브러리가 없으면 실행할 수 없다.
정적 링크(Static Link)
정적 링크는 라이브러리를 참조하는 것이 아니라, 자신의 함수를 호출하는 것 처럼 호출한다. 함수 주소를 찾고, 함수를 실행하는 과정은 없지만, 여러 바이너리에서 라이브러리를 사용하면 같은 내용이 여러번 매핑되므로 용량을 낭비하게 된다.
즉, 정적 링크는 파일 생성시 라이브러리 내용을 포함한 실행파일을 만드는 것이다. 따라서 실행 파일 안에 모든 코드가 포함되어 있다. 또한 동일한 라이브러리를 사용하는 모든 프로그램이 다 같은 라이브러리를 여러번 메모리에 매핑 시켜야 한다.
동적 링크 & 정적 링크 비교
앞의 예제코드를 정적, 동적 컴파일 하여 크기와 어셈을 통해 호출 방법을 비교해보자.
$ gcc -o static hello-world.c -static // 정적 컴파일
$ gcc -o dynamic hello-world.c -no-pie // 동적 컴파일
크기
정적 링크의 경우 해당 라이브러리를 복제하여 매핑하므로 더 많은 용량을 차지하는 것을 확인할 수 있다.
호출 방법
정적 링크는 0x411690에서 puts 함수를 직접 호출하고 동적 링크는 puts의 plt주소인 0x401040를 호출한다. plt는 동적 링크된 함수의 주소를 라이브러리에서 찾을 때 사용되는 테이블이다.
'CS > system' 카테고리의 다른 글
[System][Dreamhack] Exploit Tech : Return to Library(RTL) (0) | 2023.02.01 |
---|---|
[System] PLT & GOT (0) | 2022.11.28 |
[System][Dreamhack] 메모리 보호기법 Mitigation: NX & ASLR (0) | 2022.11.25 |
[System][Dreamhack] Exploit tech : Return to Shellcode (0) | 2022.09.06 |
[System][Dreamhack] wargame - ssp_001 스택 순서 (0) | 2022.08.24 |