라이브러리와 링크
2024년 6월 10일
라이브러리 #
프로그램이 함수나 변수를 공유해서 사용할 수 있도록 묶어서 만든 파일이며, 반복적으로 구현해야 하는 수고를 덜 수 있어서 개발의 효율이 높아진다.
범용적인 함수들은 표준 라이브러리로 구현되어 있어서 리눅스의 경우 /lib/x86_64-linux-gnu/libc.so 위치에 있다.
표준 라이브러리 경로 확인: $ ld --verbose | grep SEARCH_DIR | tr -s ' ;' '\n'
소스코드가 오브젝트 파일로 컴파일되고 심볼은 볼 수 있지만, 호출하는 함수에 대한 선언만 알고 정의된 위치를 몰라 실행할 수 없는 상태인데 링킹과정을 거치면 심볼의 위치를 찾아 연결하게 된다.
1# 아직 연결되지 않은 puts
2$ readelf -s hello-world.o | grep puts
3 11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
4
5# 실행파일에서는 링킹과정을 거쳐 LIBC에서 puts의 정의를 찾아 연결한다.
6$ gcc -o hello-world hello-world.c
7$ readelf -s hello-world | grep puts
8 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
9 46: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
10$ ldd hello-world
11 linux-vdso.so.1 (0x00007ffec3995000)
12 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fee37831000)
13 /lib64/ld-linux-x86-64.so.2 (0x00007fee37e24000)
정적 라이브러리 #
링킹 과정에서 실행 프로그램에 사용하고있는 함수들의 코드를 삽입하는 방식으로 동작하는 라이브러리(win: *.lib, linux: *.a)이다.
1$ gcc -c file.c # 라이브러리로 만들 소스코드를 오브젝트 파일로 컴파일
2$ ar cr libtest.a file.o # 아카이브로 묶는다.
3$ gcc main.c -L. -ltest # 1. 일반적인 라이브러리 컴파일
4$ gcc libtest.a main.c # 2. 이미 컴파일된 소스 파일인것처럼 컴파일
hello 함수는 링킹 후에 실제 주소가 생성되며, 실제 함수 call 명령에서 사용하는 주소이다.
1$ readelf -s main.o | grep hello
2 11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND hello
3$ readelf -s main | grep hello
4 54: 0000000000001162 23 FUNC GLOBAL DEFAULT 16 hello # 실제 주소가 링킹됨
5
6pwndbg> disassemble main
7Dump of assembler code for function main:
8 0x0000000000001149 <+0>: endbr64
9 0x000000000000114d <+4>: push rbp
10 0x000000000000114e <+5>: mov rbp,rsp
11 0x0000000000001151 <+8>: mov eax,0x0
12 0x0000000000001156 <+13>: call 0x1162 <hello> # 링킹 후에 연결된 0x1162 호출
13 0x000000000000115b <+18>: mov eax,0x0
14 0x0000000000001160 <+23>: pop rbp
15 0x0000000000001161 <+24>: ret
라이브러리가 없어도 동작할 수 있고 실행 속도가 빠르다는 장점이 있지만, 파일 용량이 크고, 함수가 변하는 경우 다시 링킹해야 하며, 동일한 코드가 각 프로세스에 포함되게 된다.
동적 라이브러리 #
프로그램 실행 시 메모리에 올라가는 라이브러리이며, 이미 메모리에 올라온 경우 주소만 가져온다.
PIC옵션은 Position-Independent Code 약자로, 재배치가 가능하도록 상대주소로 컴파일 하는 옵션이다.
1$ gcc -c file1.c file2.c
2$ gcc -shared -fPIC -o libtest.so file1.o file2.o
동적 라이브러리를 사용하는 프로그램은 시스템에서 라이브러리를 찾을 수 있어야 하기 때문에 라이브러리 위치를 알려주거나 기존 경로에 옮겨줘야 한다.
1# 1. 프로그램 실행 시 추가로 확인할 라이브러리 경로 추가
2$ export LD_LIBRARY_PATH=/path/to/libtest:$LD_LIBRARY_PATH
3
4# 2. 또는 기본 경로에 라이브러리 복사
5$ cat /etc/ld.so.conf
6include /etc/ld.so.conf.d/*.conf
7$ cat /etc/ld.so.conf.d/*.conf
8/usr/lib/i686-linux-gnu
9/usr/local/lib
10...
11$ mv /path/to/libtest/libtest.so /usr/local/lib
12
13$ ./main
implicit linking (묵시적 링킹) #
컴파일 시 링크할 라이브러리를 지정해서 전달하면 프로그램이 실행될 때 자동으로 라이브러리를 로드하게 된다. (그래서 묵시적)
공유 라이브러리의 방식이며, 코드 섹션을 공유하고 데이터 섹션은 프로세스마다 분리한다. 코드섹션은 p권한으로 보이지만 실제 물리메모리는 같으며, 데이터 섹션은 가상메모리 상에서 동일한것으로 보이지만, 실제로 공유하지는 않는다.
1$ gcc main.c -L. -ltest -o main
2
3$ export LD_LIBRARY_PATH=/home/kdh/worktemp
4$ ldd main
5 linux-vdso.so.1 (0x00007fffe39f5000)
6 libtest.so => /home/kdh/worktemp/lib/libtest.so (0x00007f89b14a1000)
7 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f89b12a5000)
8 /lib64/ld-linux-x86-64.so.2 (0x00007f89b14ad000)
9$ readelf -s main | grep hello
10 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND hello
11 52: 0000000000000000 0 FUNC GLOBAL DEFAULT UND hello
12
13pwndbg> vmmap
14 0x7ffff7fc2000 0x7ffff7fc3000 r--p 1000 0 /home/kdh/worktemp/lib/libtest.so
15 0x7ffff7fc3000 0x7ffff7fc4000 r-xp 1000 1000 /home/kdh/worktemp/lib/libtest.so
16 0x7ffff7fc4000 0x7ffff7fc5000 r--p 1000 2000 /home/kdh/worktemp/lib/libtest.so
17 0x7ffff7fc5000 0x7ffff7fc6000 r--p 1000 2000 /home/kdh/worktemp/lib/libtest.so
18 0x7ffff7fc6000 0x7ffff7fc7000 rw-p 1000 3000 /home/kdh/worktemp/lib/libtest.so
19
20pwndbg> disassemble main
21 0x0000000000001149 <+0>: endbr64
22 0x000000000000114d <+4>: push rbp
23 0x000000000000114e <+5>: mov rbp,rsp
24 0x0000000000001151 <+8>: mov eax,0x0
25 0x0000000000001156 <+13>: call 0x1050 <hello@plt>
26 0x000000000000115b <+18>: mov eax,0x0
27 0x0000000000001160 <+23>: pop rbp
28 0x0000000000001161 <+24>: ret
explicit linking (명시적 링킹) #
프로그램 런타임에 실행되는 코드에 의해 명시적으로 지정한 라이브러리가 로드된다.
코드에서 LoadLibrary, dlopen 과 같은 방법으로 라이브러리를 로드하고, GetProcAddress나 dlsym으로 로드된 함수 주소를 가져와 호출한다.
직접 로드하고 호출하기 때문에 라이브러리가 하나의 시스템 내에서 중복될 수 있다.
1#include <stdio.h>
2#include <dlfcn.h>
3
4int main() {
5 void *handle;
6 void (*my_function)();
7
8 handle = dlopen("./libexample.so", RTLD_LAZY);
9 *(void **)(&my_function) = dlsym(handle, "MyFunction");
10 my_function();
11
12 dlclose(handle);
13 return 0;
14}
이 방법으로 동적 라이브러리를 사용하면, 컴파일 시 explicit 링킹할 라이브러리를 지정할 필요가 없고, 실제로 링킹 과정에서도 따로 dl 라이브러리 이외엔 링킹되지 않는다.
1$ gcc main2.c -ldl -o main2 # dlopen을 위한 dl library 동적 implicit 링킹
2
3$ ldd main2
4 linux-vdso.so.1 (0x00007ffd22ffe000)
5 libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fbe2f90d000)
6 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbe2f71b000)
7 /lib64/ld-linux-x86-64.so.2 (0x00007fbe2f924000)
8
9$ readelf -s main2 | grep dlopen
10 6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND dlopen@GLIBC_2.2.5 (4)
11 58: 0000000000000000 0 FUNC GLOBAL DEFAULT UND dlopen@@GLIBC_2.2.5
12
13$ readelf -s main2 | grep hello
14# 라이브러리를 직접 지정해서 함수주소를 가져왔기 때문에 함수를 링킹할 필요는 없다.
15
16pwndbg> disassemble main
17 0x00000000000011c5 <+28>: mov esi,0x1
18 0x00000000000011ca <+33>: lea rdi,[rip+0xe33] # 0x2004
19 0x00000000000011d1 <+40>: call 0x1090 <dlopen@plt>
20 0x00000000000011d6 <+45>: mov QWORD PTR [rbp-0x20],rax
21 0x00000000000011da <+49>: lea rbx,[rbp-0x28]
22 0x00000000000011de <+53>: mov rax,QWORD PTR [rbp-0x20]
23 0x00000000000011e2 <+57>: lea rsi,[rip+0xe28] # 0x2011
24 0x00000000000011e9 <+64>: mov rdi,rax
25 0x00000000000011ec <+67>: call 0x10b0 <dlsym@plt>
26 0x00000000000011f1 <+72>: mov QWORD PTR [rbx],rax
27 0x00000000000011f4 <+75>: mov rdx,QWORD PTR [rbp-0x28]
28 0x00000000000011f8 <+79>: mov eax,0x0
29 0x00000000000011fd <+84>: call rdx
동적라이브러리 함수 호출 방식 #
PLT(Procedure Linkage Table) GOT(Global Offset Table) #
정적 링킹 방식이나 명시적 링킹 방식은 주소를 직접 호출하기 때문에 해당되지 않지만, 공유라이브러리(묵시적 링킹) 방식은 이미 시스템에 로드된 라이브러리를 사용하게 된다.
시스템이 로드하기 때문에 이미지베이스 충돌 시 재배치 로직이나 ASLR에 의해 어떤 주소에 라이브러리가 로드되었는지 확인하기 어렵다.
그렇기 때문에 함수의 첫 호출때 resolver를 통해 주소를 알아오고 PLT를 호출하는 방식으로 공유 라이브러리의 주소를 런타임에 알아오게 된다
주소를 계속해서 resolver를 사용해서 알아올 수도 있지만, 함수호출에 대한 오버헤드를 줄이기 위해 GOT 영역에 함수 주소를 써넣게된다.
plt는 함수 호출을 위해 call 하고, 실제 코드가 담겨있기 때문에 plt 영역은 코드 영역이고, 함수의 포인터만 저장해두는 got 영역은 데이터 영역이다.
동작 요약 #
- 첫번째 호출 :
call func_plt→jmp *ds:func_got (plt 2nd entry)→jmp dl_resolver - 두번째 호출 :
call func_plt→jmp *ds:func_got (func addr)
두 호출 모두 plt 영역의 코드를 타고 got에 저장된 주소로 jmp 하는것은 동일하지만, 첫번째는 plt의 두번째 엔트리로 이동되며 dl_resolver를 호출하게된다는 점이 다르다.
동작 순서 #
-
프로그램을 실행하면 main에서 hello와 hello2를 두번씩 호출하도록 구현했는데, 외부 함수라서
hello@plt(첫번째 plt entry) 를 호출하고 있다. -
got 명령으로 확인해보면 hello라는 함수의 got entry는
0x404018이며,0x401030을 가리키고 이 주소는 두번째 plt entry이다.
0x401030에서 dl_resolver가 어떤 함수를 찾아야할지 인덱스(0)를 push 하고, 세번째 plt entry로 점프한다.
dl_resolver 호출 전 푸시하는 값은 아직 이해를 못했다.. -
hello 첫 호출 때 plt에서 got entry의 값으로 점프하는데,
[rip + 0x2fbd]가 hello의 got entry가 저장한 값인0x401030이다. -
_dl_runtime_resolve_fxsave 함수에서 hello 함수의 주소가 구해지고 hello의 got entry에 실제 hello의 주소가 저장된다.
-
두번째 hello 호출부터는 동일하게 hello@plt를 호출하지만, got에는 실제 hello의 주소가 이미 구해졌기 때문에 직접 호출하는 것을 볼 수 있다.