1. 개요
이문서의 목적은 현대 운영체제가 load-time relocation1(로딩 시 메모리 재배치?) 를 사용한 공유라이브러리2를 어떻게 사용하는 지를 설명하기 위함에 있다. x86 32비트 리눅스 머신에 중점을 두고 있지만, 일반적인 주제이므로 어떤 운영체제나 CPU에도 적용되는 부분이기도 하다.
공유라이브러리를 말할 때 주의 점은 이 용어가 많은 이름으로 불리운다는 것이다 - shared libraries, shared objects, dynamic shared objects (DSOs), dynamically linked libraries (DLLs – 윈도우 환경의 경우).
용어의 일관성을 위해서, 이문서 내에서는 "shared library3"라고 쓰기로 한다.
2. 실행 파일의 로딩
가상 메모리를 지원하는 리눅스 같은 운영체제는 고정 메모리 주소에 실행 파일을 로딩한다. 만약 어떤 랜덤한 실행파일의 ELF 헤더를 실행 한다면,
(이 엔트리 포인트에 대한 정보를 더 원한다면 이 문서의 "Digression – process addresses and entry point" 섹션을 참고 한다.)
우리는 다음과 같은 엔트리 포인트를 보게 될 것이다.
$ readelf -h /usr/bin/uptime
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
[...] some header fields
Entry point address: 0x8048470
[...] some header fields
링커에 의해서 배치되는 이 주소는 운영체제로 하여금 실행 파일의 코드가 어디서 실행을 시작해야 할 지를 가리키는 주소이다. 그리고 이것은 GDB 같은 툴로 실행 파일을 로딩하고 메모리 주소 0x8048470를 살펴 보면 실행 파일의 .text 세그먼트의 첫 명령어가 있을 것이다.
이 의미는 링커가 실행 파일을 링킹할 때, 내부(internal ) 고정시킬 심볼 참조(함수와 데이터에 대한) 와 최종 위치를 전체적으로 분석할 수 있도록 한다. 링커는
링크 시 재배치(Link-time relocation)는 여러 오브젝트 파일을 하나의 실행 파일로 묶는 과정(process)에서 일어난다. 이 의미는 오브젝트 파일들 사이에 심볼 참조를 분석하기 위한 다수의 재배치를 포함하고 있다. 링크 시 재배치는 로딩 시 재배치(load-time relocation) 보다 좀 더 복잡한 주제이므로 이 문서에서는 설명하지 않는다.
링커는 링커 재배치를 수행하지만 결국 생산한 결과물에 부가적인 재배치4를 포함하지는 않는다.
혹 그렇게 하는가? 앞선 문장에서 내부(internal )란 단어를 강조한것에 주목하자.
실행파일이 공유라이브러리를 필요치 않는 한, 재 배치는 필요없다.[3]
이 말은 정적 라이브러리로 현재 당신이 가지고 있는 라이브러리를 모두 컴파일이 가능하다는 것을 뜻하며,(gcc -shared 대신 ar 명령으로 오브젝트 파일을 묶어서) 링킹으로 실행 파일을 만들때 -static 플래그를 사용해서 공유 버전의 libc가 링크 되는 것을 막을수 있다.
그렇지만 공유라이브러리가 필요하다면(리눅스 애플리케이션의 거의 전체를 차지하는), 공유라이브러리로딩되는 방식 때문에 공유라이브러리로 부터 얻어지는 심볼은 재배치 되어야 한다.
3. 공유라이브러리 로딩
실행 파일과 달리, 공유라이브러리가 만들어질때, 링커는 그 코드에 대한 알려진 로딩 주소를 판단 할 수 없다. 그 이유는 단순한데, 각 프로그램은 어떤 수의 공유라이브러리를 사용 할 수 있는, 프로세서의 가상 메모리의 상에 주어진 어떤 공유라이브러리가 로딩 될 위치를 먼저 알 수 있는 간단한 방법이 없다는 것이다. 수년 동안 이 문제를 해결하기 위해서 많은 솔루션이 개발되어있지만, 이 문서에서 리눅스에서 현재 사용되는 방식에만 촛점을 맞출 것이다.
그러나 그러기 앞서 그 문제에 대해서 한 가지 예제를 보도록 한다. 여기 공유라이브러리로 컴파일 할
단순한 C 파일이 있다.
int myglob = 42;
int ml_func(int a, int b)
{
myglob += a;
return b + myglob;
}
ml은 그냥 "my library" 라는 뜻이다. 이 코드는 정말 일반적인 코드가 아니라 데모 목적으로 만들었다
ml_func 함수가 myglob 변수를 몇번 참조 하는 방식에 주목하자. x86 어셈블리로 해석했을때, 이는 mov 명령를 사용해서 메모리상의 myglob 값의 위치를 레지스터로 가져온다. mov 명령은 절대 주소가 요구된다. - 그래서 링커는 그 함수가 있는 위치를 어떻게 알 수 있을까? 답은 - '모른다'이다. 앞서에서도 언급했듯이, 공유라이브러리들은 먼저 정의되어 있는 로딩 주소를 가지고 있지 않다 - 이는 런타임 시에 정해 진다.
리눅스에서 dynamic loader
- "dynamic linker" 라고도 하며,공유 오브젝트 자체이며(비록 실행 파일로 구동이 되지만), /lib/ld-linux.so.2 내에 존재한다(마지막 숫자는 SO 버전이며 다를 수 있다) -
라는 것은 구동하기 위해 준비된 프로그램을 책임 질 코드의 조각 같은 것이다. 그 테스크 중 하나가 디스크에서 메모리로 공유라이브러리 들을 로딩 할 것이며, 새로이 로딩할 위치를 조정해서 정하는 것이다. 이것이 앞선 절에서 소개한 문제를 해결하기 위해서 dynamic loader 가 하는 일이다.
리눅스의 ELF 공유라이브러리들은 이 문제를 해결하기 위한 두가지 주요 접근 방식을 채택했다.
- Load-time relocation
- Position independent code (PIC)
비록 PIC가 요즘 요구되는 솔루션이며 좀 더 일반적인 방식이지만, 이 문서에서 중점을 두는 것은 load-time relocation(로딩 시점 재배치) 이다.
결론적으로 이 두가지 접근 방식에 대해서 설명하기위해서 PIC는 다른 문서에서 다루었으며 load-time relocation(로딩 시점 재배치)에 대한 설명으로 시작하는 것이 나중에 PIC 를 설명하는 데 쉬워 질 것이라고 생각한다.(Update 03.11.2011: PIC 에 대한 기사 )
4. 로딩 시점 재배치를 위한 공유라이브러리 링크
로딩 시점에 재배치 해야하는 공유라이브러리를 생성하기 위해서 -fPIC 플래그(PIC 파일을 생성하는 시작점인) 없이 컴파일 할 것이다:
gcc -g -c ml_main.c -o ml_mainreloc.o
gcc -shared -o libmlreloc.so ml_mainreloc.o
흥미로운 첫 번째는 libmlreloc.so의 시작점(entry point )에 있다:
$ readelf -h libmlreloc.so
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
[...] some header fields
Entry point address: 0x3b0
[...] some header fields
간단하게 하기위해서, 링커는 단지 주소값 0x0 (.text 섹션은 0x3b0에서 시작함)에 대한 공유오브젝트5를 어차피 로더가 주소값을 이동 시킬 것을 알고 있으므로 링크한다. 이 사실을 기억하라 - 이 문서 나중에 유용하게 쓰일 것이다.
그럼 ml_func 함수에 촛점을 맞추면서 공유라이브러리의 디스어셈블한 것을 살펴보자:
$ objdump -d -Mintel libmlreloc.so
libmlreloc.so: file format elf32-i386
[...] skipping stuff
0000046c <ml_func>:
46c: 55 push ebp
46d: 89 e5 mov ebp,esp
46f: a1 00 00 00 00 mov eax,ds:0x0
474: 03 45 08 add eax,DWORD PTR [ebp+0x8]
477: a3 00 00 00 00 mov ds:0x0,eax
47c: a1 00 00 00 00 mov eax,ds:0x0
481: 03 45 0c add eax,DWORD PTR [ebp+0xc]
484: 5d pop ebp
485: c3 ret
[...] skipping stuff
당신이 x86 시스템이 스택 프레임을 구조화 하는 지에 대해서 친숙하지 않으면, 이 문서가 좋은 참고 될 수 있으리라 본다.
objdump 명령어에 -l 플래그를 넣어서 디스어셈블안에 현재 어떤 것이 어떻게 컴파일 되었는지 알 수 있게 C 소스 라인을 집어 넣을 수 있다. 여기서는 출력 결과를 짧게 하기 위해서 뺏다.
myglob 변수의 값은 메모리에서 eax로 옮겨지며, a를 증가시키고(ebp+0x8를 가리킴) 메모리에 다시 옮겨놓는다.
잠시만, mov는 myglob 변수를 가져갔나? 왜? 실제 mov의 오퍼랜드는 단지
objdump 출력 물의 왼쪽 편을 보면, ds:0x0으로 디스어셈블러가 해석한 메모리 메모리 바이트가 a1 00 00 00 00 인데 이는 오퍼랜드 0x00를 eax에 mov 하라는 의미이다.
로만 보여진다. 무슨일이 일어났는가?
바로 이것이 재배치가 동작하는 방식이다. 링커는 임시로 우선 정의한 값(이경우 0x0 )을 명령어 스트림에 저장하고, 이 위치에 대한 특별한 재배치 시작점(entry pointing)을 생성한다. 그럼 공유 라이브러리에 대한 재배치 엔트리를 검토해 보자.
$ readelf -r libmlreloc.so
Relocation section '.rel.dyn' at offset 0x2fc contains 7 entries:
Offset Info Type Sym.Value Sym. Name
00002008 00000008 R_386_RELATIVE
00000470 00000401 R_386_32 0000200C myglob
00000478 00000401 R_386_32 0000200C myglob
0000047d 00000401 R_386_32 0000200C myglob
[...] skipping stuff
ELF의 rel.dyn 섹션은 dynamic loader에 의해서 소비되는 동적(로딩시) 재배치를 예약하는 섹션이다. 디스어셈블에서 본 myglob 변수에 대한 3개의 참조가 있었듯이, 위에서 보듯 myglob 변수에 대한 세개의 재배치 엔트리가 있다.
첫번째를 해석해 보자: 이 오브젝트(공유라이브러리)의 오프셑 0x470로 가서, 재배치 타입 R_386_32를 심볼 myglob에 적용한다. ELF 명세에서 재배치 타입 R_386_32를 찾아보면: 엔트리내의 특정한 오프셑의 값과 그 심볼의 주소를 더해서 오프셑에 다시 저장한다 라는 의미가 된다.
오브젝트 내에 오프셑 0x470은 무엇을 가지고 있을까? ml_func 함수의 디스어셈블 내용으로부터 이 함수를 상기해보면:
46f: a1 00 00 00 00 mov eax,ds:0x0
a1은 mov 명령어로 대체되므로 그 오프랜드는 다음 주소인 0x470에서 시작한다. 이는 디스어셈블해서 본 0x0 위치이다. 그래서 재배치 엔트리로 다시 가서, 해석해 보면: myglob 변수의 주소를 mov 명령어 오퍼랜드에 더한다 이다.
다른 말로 dynamic loader 말한다 - 실제 주소 할당을 수행했다면, 올바른 심볼 값에 의해서 mov의 오퍼랜드를 바꿔서 myglob 변수의 실 주소를 0x470에 넣었다는 것이다.
relocation section에서 myglob에 대해 0x200C값을 가지고 있는 "Sym. value" 컬럼에 주의하자. 이 컬럼은 공유라이브러리의 가상메모리 이미지 내의 myglob의 오프셑 값이다( 링커는 이 라이브러리가 0x0에서 로딩되고 되었다고 가정)
이 값은 라이브러리의 심볼 테이블을 조사하면서 확인할 수 있다. nm 명령어를 예를 들면:
$ nm libmlreloc.so
[...] skipping stuff
0000200c D myglob
이 출력값은 또한 라이브러리 내부의 myglob 변수의 오프셑을 제공한다. D는 초기화 된 data 섹션(.data)의 심볼이라는 의미이다.
5. 로딩 시 재배치 활동
로딩시 재배치 활동을 보기 위해서, 간단한 드라이버 실행 파일에 우리의 공유라이브러리를 사용할 것이다. 이 실행 파일이 실핻 될 때, 운영 체제는 공유라이브러리를 로드하고 알맞게 이 라이브러리를 재배치 할 것이다.
리눅스에서 사용되느는 address space layout randomization feature 때문에, 재배치는 비교적 따라가기 힘든데, 매번 실행파일을 실행 할 때마다, libmlreloc.so인 공유라이브러리는
ldd 명령으로 실행 파일을 출력한다면 각 구동 시점 마다 공유 라이브러리의 로드 되는 주소가 다르게 리포트 될 것이다.
약간 억지스런 방법이긴 하나, 이를 알아 볼수 있는 방법이 있지만 그보다 우선, 우리의 공유라이브러리를 구성하는 세그먼트에 대해서 얘기 해보도록 하자:
$ readelf --segments libmlreloc.so
Elf file type is DYN (Shared object file)
Entry point 0x3b0
There are 6 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x004e8 0x004e8 R E 0x1000
LOAD 0x000f04 0x00001f04 0x00001f04 0x0010c 0x00114 RW 0x1000
DYNAMIC 0x000f18 0x00001f18 0x00001f18 0x000d0 0x000d0 RW 0x4
NOTE 0x0000f4 0x000000f4 0x000000f4 0x00024 0x00024 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x000f04 0x00001f04 0x00001f04 0x000fc 0x000fc R 0x1
Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .eh_frame
01 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
02 .dynamic
03 .note.gnu.build-id
04
05 .ctors .dtors .jcr .dynamic .got
다음 myglob 심볼을 따라가기 위해서, 우리는 여기 리스트되어 있는 두번째 세그먼트에 관심을 가진다. 다음 몇몇 사항을 명심하자:
- 하단의 섹션과 세그먼트 매핑의 경우, 세그먼트 01은 myglob의 홈인 .data 섹션을 포함한다라고 되어있다.
- VirtAddr 컬럼은 두번째 세그먼트가 0x1f04에서 시작하며 크기가 0x10c라고 명시한다. 이는 이 컬럼이 0x2010까지 확장하고 0x200C에 있는 myglob를 포함한다는 의미이다.
자 그럼, 리눅스에서 제공하는 로딩시 링킹 프로세스를 검토할 수 있도록 제공된 함수인 dl_iterate_phdr 함수를 사용해 보자. dl_iterate_phdr 함수는 런타임시 애플리케이션이 로딩 한 공유라이브러리가 어떤것인지 질의하고 좀 더 중요한 부분인 - 프로그램 헤더를 엿볼 수 있도록 해 준다.
그래서 driver.c 파일에 다음 코드를 작성하도록 한다:
#define _GNU_SOURCE
#include <link.h>
#include <stdlib.h>
#include <stdio.h>
static int header_handler(struct dl_phdr_info* info, size_t size, void* data)
{
printf("name=%s (%d segments) address=%p\n",
info->dlpi_name, info->dlpi_phnum, (void*)info->dlpi_addr);
for (int j = 0; j < info->dlpi_phnum; j++) {
printf("\t\t header %2d: address=%10p\n", j,
(void*) (info->dlpi_addr + info->dlpi_phdr[j].p_vaddr));
printf("\t\t\t type=%u, flags=0x%X\n",
info->dlpi_phdr[j].p_type, info->dlpi_phdr[j].p_flags);
}
printf("\n");
return 0;
}
extern int ml_func(int, int);
int main(int argc, const char* argv[])
{
dl_iterate_phdr(header_handler, NULL);
int t = ml_func(argc, argc);
return t;
}
header_handler는 dl_iterate_phdr에 대한 콜백 함수를 구현한다. 이 함수는 모든 함수에 대한 호출을 가져오고 그 이름과 로딩 주소와 함께 모든 세그먼트를 리포팅한다. 또한 ibmlreloc.so 공유라이브러리에서 ml_func 함수를 호출한다:
gcc -g -c driver.c -o driver.o
gcc -o driver driver.o -L. -lmreloc
정보를 얻기위한 드라이버 stand-alone을 구동하면서도 각 구동 주소는 서로 다르다. 그래서 여기서
경험이 많은 독자들은 아마 동적 라이브러리의 로딩 주소를 얻어 내는 것을 공유 해 주는 GDB에게 물어 볼 수 있을 것이라는 것을 알았을 것이다. 하지만, 나는 전체 라이브러리의 로딩 위치에 대한 이야기를 공유 하고 있다.(혹은 좀 더 정확하게 말하면, 이것을 엔트리 포인트) 그리고 다음 세크먼트에 관심이 있다.
GDB 명령을 구동하기로 한다, 그리고 이 의미는 gdb가 프로세서의 메모리 영역의 심도 있는 질의를 위해서 사용되도록 한다는 것이다:
$ gdb -q driver
Reading symbols from driver...done.
(gdb) b driver.c:31
Breakpoint 1 at 0x804869e: file driver.c, line 31.
(gdb) r
Starting program: driver
[...] skipping output
name=./libmlreloc.so (6 segments) address=0x12e000
header 0: address= 0x12e000
type=1, flags=0x5
header 1: address= 0x12ff04
type=1, flags=0x6
header 2: address= 0x12ff18
type=2, flags=0x6
header 3: address= 0x12e0f4
type=4, flags=0x4
header 4: address= 0x12e000
type=1685382481, flags=0x6
header 5: address= 0x12ff04
type=1685382482, flags=0x4
[...] skipping output
Breakpoint 1, main (argc=1, argv=0xbffff3d4) at driver.c:31
31 }
(gdb)
드라이버가 로드한 모든 라이브러리를 리포팅하는 동안,(명시적으로, libc 혹은 dynamic loader 그 자신 같은 ) 출력이 약간 장황하기는 하나 libmlreloc.so 에 대한 리포팅에만 촛점을 맞출 것이다. readelf에 의해서 리포팅된 6개의 세그먼트들은 모두 똑같은 세그먼트이니만 이번은 각각 최종 메모리 위치에 재배치 되었다.
그럼 약간의 수식을 적용해 보자. 해당 출력 부분은 libmlreloc.so이 가상 메모리 0x12e000에 위치되었다는 것을 말해 준다. 두번째 세그먼트가 흥미로운데, readelf에서 본 이 세그먼트의 위치는 오프셑 0x1f04. 정말, 주소 0x12ff04에서 로드된 출력을 우리는 보고 있다. 그리고 myglob이 파일내에서 오프셑 0x200c이고, 우리은 현재 주소 0x13000c이라는 것을 예상 할 수 있다
그래서, GDB에 물어보자:
(gdb) p &myglob
$1 = (int *) 0x13000c
좋아! 그러나 myglob가 참조하는 ml_func의 코드는 어떻까? GDB에 다시 물어보자:
(gdb) set disassembly-flavor intel
(gdb) disas ml_func
Dump of assembler code for function ml_func:
0x0012e46c <+0>: push ebp
0x0012e46d <+1>: mov ebp,esp
0x0012e46f <+3>: mov eax,ds:0x13000c
0x0012e474 <+8>: add eax,DWORD PTR [ebp+0x8]
0x0012e477 <+11>: mov ds:0x13000c,eax
0x0012e47c <+16>: mov eax,ds:0x13000c
0x0012e481 <+21>: add eax,DWORD PTR [ebp+0xc]
0x0012e484 <+24>: pop ebp
0x0012e485 <+25>: ret
End of assembler dump.
예상 했듯이, myglob의 실제 주소는 재배치 엔트리에서 명시했듯이, mov 명령어가 myglob를 참조하는 모든 곳에 있다.
6. 재배치 함수 호출
이 문서에서 데이터 참조에 대한 재배치에 대해서 설명했다 - 전역변수인 myglob 예를 사용해서.
재배치가 필요한 다른 부분은 코드 참조이다 - 다른 말로, 함수 호출. 이 섹션은 어떻게 이 부분이 작동 하는지에 대한 간단한 가이드이다. 이 단계는 이 글을 읽는 독자가 재배치가 어떤것인지 알고 있다고 가정하고 남은 부분은 빠르게 진행 할 것이다.
말할 필요 없이 시작해 보자. 공유라이브러리에 대한 코드를 다음과 같이 수정하였다:
int myglob = 42;
int ml_util_func(int a)
{
return a + 1;
}
int ml_func(int a, int b)
{
int c = b + ml_util_func(a);
myglob += c;
return b + myglob;
}
ml_util_func 함수가 추가되었고 ml_func 함수에서 사용 되어지고 있다. 여기 링크된 공유라이브러리내의 ml_func 함수의 디스어셈블 결과가 있다:
000004a7 <ml_func>:
4a7: 55 push ebp
4a8: 89 e5 mov ebp,esp
4aa: 83 ec 14 sub esp,0x14
4ad: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
4b0: 89 04 24 mov DWORD PTR [esp],eax
4b3: e8 fc ff ff ff call 4b4 <ml_func+0xd>
4b8: 03 45 0c add eax,DWORD PTR [ebp+0xc]
4bb: 89 45 fc mov DWORD PTR [ebp-0x4],eax
4be: a1 00 00 00 00 mov eax,ds:0x0
4c3: 03 45 fc add eax,DWORD PTR [ebp-0x4]
4c6: a3 00 00 00 00 mov ds:0x0,eax
4cb: a1 00 00 00 00 mov eax,ds:0x0
4d0: 03 45 0c add eax,DWORD PTR [ebp+0xc]
4d3: c9 leave
4d4: c3 ret
여기에서 흥미로운 점은 0x4b3 주소에 있는 명령이다 - ml_util_func 함수를 호출하고 있다. 이 함수를 해부해 보자:
e8은 호출의 opcode이다. 이 호출의 아규먼트는 다음 명령어에 연관된 오프셑이다. 위에서 디스어셈블한결과, 이 아규먼트는 0xfffffffc, 이거나 단순히 -4이다. 그래서 현재 호출은 자신을 가리키고 있다. 이는 정확히 잘못된 것이다 - 하지만 재배치에 대한 내용은 잊어먹지 않도록 하자. 여기 공유라이브러리의 재배치 섹션의 지금 어떤가에 대해 있다:
$ readelf -r libmlreloc.so
Relocation section '.rel.dyn' at offset 0x324 contains 8 entries:
Offset Info Type Sym.Value Sym. Name
00002008 00000008 R_386_RELATIVE
000004b4 00000502 R_386_PC32 0000049c ml_util_func
000004bf 00000401 R_386_32 0000200c myglob
000004c7 00000401 R_386_32 0000200c myglob
000004cc 00000401 R_386_32 0000200c myglob
[...] skipping stuff
만약 -r 옵션을 사용한 readelf의 먼저번 호출과 비교 해 본다면, 우리는 ml_util_func에 새로운 엔트리가 첨가 되었다는 것을 발견할 수 있다. 이 엔트리는 호출 명령의 아규먼트인 0x4b4 주소를 가리키고 있으며, 타입은 R_386_PC32이다. 이 재배치 타입은 R_386_32 보다 복잡하지만 그렇게 많이 복잡하지는 않다.
해당 내용은 다음과 같다: 엔트리에 명시되어 있는 오프셑에서 값을 가져와서 그 심볼의 주소에 더하고 오프셑의 주소를 뺀다, 그리고 오프셑을 워드로 저장한다. 이 재배치에 대한 호출은 심볼의 주소들이 최종 로딩되고 재배치된 오프셑이 밝혀졋을 때 정해진다. 결과 연산에 사용되는 최종 주소값은 다음과 같다.
이것은 무엇을 말하는가?, 기본적으로 이것은 그 위치를 고려하고 관련된 주소체계(e8 호출)의 명령에 대한 아규먼트에 적절한 재배치에 관련된 부분이다. 실 숫자가 계산 되었을 때 이부분은 더욱 명확해 진다
지금 드라이버 코드를 빌드하고 GDB 하에서 다시 구동해서, 재배치가 일어나는 것을 보자. 여기 설명에 관련한 GDB 세션이 있다:
$ gdb -q driver
Reading symbols from driver...done.
(gdb) b driver.c:31
Breakpoint 1 at 0x804869e: file driver.c, line 31.
(gdb) r
Starting program: driver
[...] skipping output
name=./libmlreloc.so (6 segments) address=0x12e000
header 0: address= 0x12e000
type=1, flags=0x5
header 1: address= 0x12ff04
type=1, flags=0x6
header 2: address= 0x12ff18
type=2, flags=0x6
header 3: address= 0x12e0f4
type=4, flags=0x4
header 4: address= 0x12e000
type=1685382481, flags=0x6
header 5: address= 0x12ff04
type=1685382482, flags=0x4
[...] skipping output
Breakpoint 1, main (argc=1, argv=0xbffff3d4) at driver.c:31
31 }
(gdb) set disassembly-flavor intel
(gdb) disas ml_util_func
Dump of assembler code for function ml_util_func:
0x0012e49c <+0>: push ebp
0x0012e49d <+1>: mov ebp,esp
0x0012e49f <+3>: mov eax,DWORD PTR [ebp+0x8]
0x0012e4a2 <+6>: add eax,0x1
0x0012e4a5 <+9>: pop ebp
0x0012e4a6 <+10>: ret
End of assembler dump.
(gdb) disas /r ml_func
Dump of assembler code for function ml_func:
0x0012e4a7 <+0>: 55 push ebp
0x0012e4a8 <+1>: 89 e5 mov ebp,esp
0x0012e4aa <+3>: 83 ec 14 sub esp,0x14
0x0012e4ad <+6>: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
0x0012e4b0 <+9>: 89 04 24 mov DWORD PTR [esp],eax
0x0012e4b3 <+12>: e8 e4 ff ff ff call 0x12e49c <ml_util_func>
0x0012e4b8 <+17>: 03 45 0c add eax,DWORD PTR [ebp+0xc]
0x0012e4bb <+20>: 89 45 fc mov DWORD PTR [ebp-0x4],eax
0x0012e4be <+23>: a1 0c 00 13 00 mov eax,ds:0x13000c
0x0012e4c3 <+28>: 03 45 fc add eax,DWORD PTR [ebp-0x4]
0x0012e4c6 <+31>: a3 0c 00 13 00 mov ds:0x13000c,eax
0x0012e4cb <+36>: a1 0c 00 13 00 mov eax,ds:0x13000c
0x0012e4d0 <+41>: 03 45 0c add eax,DWORD PTR [ebp+0xc]
0x0012e4d3 <+44>: c9 leave
0x0012e4d4 <+45>: c3 ret
End of assembler dump.
(gdb)
중요한 부분은 이것이다:
- libmlreloc.so의 첫번째 세그먼트(코드세그먼트)에서 보는 드라이버로 부터 나온 출력은
- What, 0x12e000 again? Didn’t I just talk about load-address randomization? It turns out the dynamic loader can be manipulated to turn this off, for purposes of debugging. This is exactly what GDB is doing.
- 뭐, 또 0x12e000 ? 로딩 주소 랜덤화(load-address randomization)에 대해 얘기하지 않았던가?
- 이 부분은 dynamic loader 가 디버깅 목적으로 이 부분 표출을 끌수 있는 조작이 가능하다는 것이다. 이 부분이 정확히 GDB가 할 수 있는 부분이다
- ml_util_func 함수는 0x0012e49c 주소에 로드 되었다.
- 이 재배치에 대한 오프셑의 주소는 0x0012e4b4이다
- ml_func 에서 ml_util_func 함수 호출은 ml_util_func 함수에 대한 알맞은 오프셑으로 해석된 아규먼트 내의 0xffffffe4의 위치에 패치되었다(ml_func 함수를 /r 플래그로 디스어셈블해서 나온 디스어셈블해서 나온 추가 정보에 대한 핵사값)
분명하게 우리는 어떻게 (4)가 수행되었는가에 대해서 모든 관심을 집중해야 한다. 다시, 수식을 적용해서보면 위에서언급한 R_386_PC32 재배치 엔트리로 해석이 되어진다:
우리는: 엔트리(0xfffffffc)내에 명시된 오프셑의 값을 가지고, 그에 대한 심볼 주소(0x0012e49c)를 더하고, 오프셑의 주소를(0x0012e4b4) 빼서, 오프셑의 값으로 저장한다. 모든 것이 32비트 2의 보수로 물론 계산 되었으며, 결과값은 예상 했듯이, 0xffffffe4이다
#1 추가 질문: 호출 재배치가 필요한 이유는 무엇인가?
리눅스에서 공유라이브러리를 로딩 구현의 몇 몇 특징을 토론해 보는 보너스 섹션이다. 만약 재배치가 어떻게 일어나는지 알고 있다면 이 섹션을 넘어가도 된다.
ml_util_func 함수의 재배치 호출을 이해하려고 한다면, 나는 내 머리를 몇번 긁적거려야 함을 인정한다. 호출 아규먼트에 대한 재호출은 근접 오프셑이다. 분명 호출과 ml_util_func 함수 사이의 오프셑이 라이브러리가 로드 되더가도 바뀌지 않아야 한다 - 그 둘은 같은 한 덩어리로써 움직이는 code segment 내에 있다. 근데 왜 재배치가 필요 한것인가?
여기 이것에 대한 작은 실험을 해 보았다: 공유 라이브러리 코드로 돌아가서, ml_util_func 함수의 선언을 static으로 만들고, 재 컴파일해서 -r 플래그를 다시 사용해서 그 결과를 보자.
끝났나? 어쨋든, 이 결과치에 대해서 파보자 - 재배치는 없다! ml_func 함수의 디스어셈블의 결과를 보자 - 호출의 아규먼트로서 올바른 오프셑이 들어가 있다 - 재배치가 필요치 않은 것이다. 무슨일이 일어난건가?
그에 대한 실 정의의 전역 심볼 참조를 묶을 때, dynamic loader는 공유 라이브러리를 찾는 순서에 대한 어떤 규칙을 가진다. 사용자 또한 LD_PRELOAD 환경 변수를 선언함으로써 이 순서에 영향을 받을 수 있다.
이 영역을 설명하려면 많은 얘기가 필요하다, 그래서 만약 정말 궁금하다면 dynamic loader man 페이지와 구글링으로 ELF 규칙을 쳐다봐야 할 것이다. 근데, 짧게 말해서, ml_util_func 함수가 전역이면, 실행 파일이나 다른 공유 라이브러리가 오버라이딩 될 수 있어
-Bsymbolic 플래그를 전달하지 않는한 말이다. ld의 man 페이지를 읽어 보라.
이것은 dynamic loader가 어떻게 이것들을 분석할 것인가에 대한 판단을 하기 위해서 전역 심볼 재배치에 대한 모든 참조 값을 만들 것이다 - 더이상 전역이거나 엑스포트 되지 않으므로써 링커가 코드상에 오프셑에 하드 코딩 할 수 있다. .
#2 추가 질문 : 실행 파일에서 공유 라이브러리 참조하기
다시 여기는 좀 더 확장된 주제 대한 보너스 섹션이다. 이 정도 읽었으면 됐다고 생각한다면 접어도 된다.
위의 예제에서 myglob 변수는 단순히 공유 라이브러리 내의 내부에서 사용되었다. 만약 프로그램(driver.c)에서 이를 참조 한다면 무슨일이 벌어질까? 결국, myglob는 전역 변수이므로 외부에서도 보인다는 것이다.
driver.c를 다음 코드 처럼 약간 고쳐보자(세그먼트 이터레이션 코드를 없앴다):
#include <stdio.h>
extern int ml_func(int, int);
extern int myglob;
int main(int argc, const char* argv[])
{
printf("addr myglob = %p\n", (void*)&myglob);
int t = ml_func(argc, argc);
return t;
}
addr myglob = 0x804a018
잠시만, 뭔가 계산이 안되었다. myglob 변수는 공유 라이브러리의 주소 공간에 있었던것이 아니었던가? 0x804xxxx 값은 프로그램의 주소 공간 처럼 보인다. 무슨 일이 일어났는가?
프로그램/실행파일이 재배치 되지 않았다는 것을 상기해보자, 그리고 데이터 주소가 링크 시에 합쳐져야만 한다. 그러므로 링커는 프로그램 주소 공간으로 변수의 복사본을 생성해야만 했으며, dynamic loader는 이것을 재배치 주소로 사용했다. 이것은 전 섹션에서 다루어졌던 논의와 비슷한 경우이다 - 어느정도, 메인 프로그램의 myglob 변수는 공유 라이브러내의 변수를 오버라이딩 한것이다, 그리고 전역 심볼 찾기 규칙에 의해서 이 변수가 대신 사용 되었던 것이다. 만약 ml_func 함수를 GDB에서 검사 해본다면 myglob변수로 만들어진 맞는 참조 값을 볼 수 있을 것이다:
0x0012e48e <+23>: a1 18 a0 04 08 mov eax,ds:0x804a018
이는 myglob 변수에 대한 R_386_32 재배치가 libmlreloc.so 에서 일어 나기 때문에 말이 된다, 그리고 dynamic loader는 myglob가 위치한 바른 위치를 가리키게 만든다.
이부분이 바로 훌륭한 부분이지만 뭔가 빠졌다. myglob 변수는 공유 라이브러리내에서 초기화 된다(42) - 어떻게 이 초기화 값이 프로그램의 주소 공간에서 얻어질수 있을까? 이것은 링커가 프로그램 내에 빌드한 특별한 재배치 엔트리 때문이다(여기까지 공유 라이브러리에 대한 재배치 엔트리에 대해서 검토해본 결과이다).
$ readelf -r driver
Relocation section '.rel.dyn' at offset 0x3c0 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
08049ff0 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
0804a018 00000605 R_386_COPY 0804a018 myglob
[...] skipping stuff
myglob 변수에 대한 R_386_COPY 재배치에 대해서 주의하자. 이 의미는: 심볼의 주소로 부터 값을 이 오프셑에 복사한다는 것이다. dynamic loader는 공유 라이브러리가 로드 될 때 이를 수행한다. 어떻게 얼만큼 복사해야 하는지 알까? 심볼 테이블 섹션은 각 심볼의 크기를 가지고 있다; 예를 들어 libmlreloc.so의 .symtab 섹션 안의 myglob의 크기는 4이다.
이 예제는 실행 파일의 대한 링킹과 로딩의 과정이 어떻게 함께 조화를 이루어 나가는 지 보여주는 좋은 예제가 될 것으로 본다. 링커는 특별한 명령어를 dynamic loader가 사용하고 실행 할 수 있도록 결과에 집어넣는다.
7. 결론
로딩시 재배치는 공유 라이브러리가 메모리로 로딩되었을 때, 리눅스(다른 운영체제도)에서 사용하는 내부 데이터와 코드 참조를 찾아가는 방법이다. 요즘은. position independent code (PIC)가 좀 더 일반적인 접근 방식이며, 현대 시스템(x86-64 같은)에서는 더이상 로딩시 재배치를 지원하지 않는다.
여전히, 이 로딩시 재배치에 대해서 이 문서를 쓴 이유가 두가지 있다. 첫번째, 로딩시 재배치는 몇몇 시스템의 PIC보다 잇점이 존재 한다, 특히 성능면에서 더욱이 그렇다. 두번째, 로딩시 재배치는 다음에 PIC의 개념을 설명하는데 선수 지식없이 쉽세 IMHO 를 이해 할 수 있다.
연관된 주제 포스트:
- Position Independent Code (PIC) in shared libraries on x64
- Position Independent Code (PIC) in shared libraries
- Understanding the x64 code models
- How statically linked programs run on Linux
- Shared counter with Python’s multiprocessing
1. 재배치
2. shared library
3. 공유 라이브러리
4. relocation
5. shared object
'프로그래밍' 카테고리의 다른 글
MICROSOFT C Run-Time Win32 1 (2) | 2023.03.11 |
---|---|
[JavaScript] URL 파라메터 얻기 (1) | 2023.03.10 |
윈도우 SDK와 커맨트 프롬프트를 이용한 C++ 프로젝트 컴파일하기 (0) | 2023.02.26 |
[윈32 프로그래밍]이클립스와 Windows SDK 7으로 해보기 (0) | 2023.02.25 |
C# - Thread (0) | 2023.02.24 |