2013. 4. 5. 23:51

Keyword  : PLT & GOT 심층 분석.  왜사용되나? 그리고 어떠한 상관 관계에 있는가?


우선 아래 몇가지 용어는 확실하게 정의하고 넘어가자.


   .GOT(Global offset Table)

함수 호출 시 가변적이면서도 빠르게 검색을 하기 위해 생성되는 TABLE 이다.

첫 호출시엔 코드 영역의 벡터 주소가 들어있고,

두번째 부터는 그 함수의 실제 주소가 들어가게 된다.

 .PLT(Procedure Linked Table)

 컴파일 타임에 생성되는 테이블로 어떠한 GOT영역의 주소를 참조 할지 정해져 있다.

 _dl_runtime_resolve

 _dl_runtime_resolve 함수는 전달된 인자 값을 사용하여 호출된 함수의 실제 주소를 구한 후 GOT에 저장한 뒤 호출된 함수로 점프한다. (GOT을 두번째 참조 할때부턴 호출되지 않는다.)

 _init  컴파일시 생성되는 함수로 미리 선언 되어 있는 코드 영역이다.


정확하지 않을 수 있지만, 커널은 어떠한 함수가 호출 될때, 자주 호출될 법한, 빈도수 높은 함수들을 GOT 에 집어 넣고,

빠르게 사용하기 위해 위와 같은 구조로 설계 되어 있는 듯하다. 

내가 이제부터 작성할 문서는 사실 GOT 영역을 컴파일시에 결정해 놓으면 PLT 과정이 생략되도 되지 않을까? 하는 생각에서 시작된 분석이다. 결론적으론 GOT 영역에 모든 함수의 실제 주소값을 다 넣어두게 된다면, GOT 자체의 크기도 너무 커져 버리고, 무의미하게 되는 것이다.  


보통 검색을 하다보면 PLT 와 GOT 에 대해서만 설명이 많이 되어 있는데, 내가 생각하기에 결론적으로 중요한 핵심은

_dl_runtime_resolve 가 맡고 있었다. 이놈이 GOT에 함수주소들을 찾아 저장시켜줌으로써 아래와 같은 과정이 이뤄지는 것이다.



 @Fedora_1stFloor TEST]$ readelf -S test


  [ 9] .rel.plt          REL                 08048288 000288 000018 08   A  4  11  4

  [10] .init             PROGBITS        080482a0 0002a0 000017 00  AX  0   0  4

  [11] .plt              PROGBITS        080482b8 0002b8 000040 04  AX  0   0  4

  [12] .text             PROGBITS        080482f8 0002f8 0001d0 00  AX  0   0  4

  [13] .fini             PROGBITS        080484c8 0004c8 00001a 00  AX  0   0  4

  [14] .rodata           PROGBITS        080484e4 0004e4 000011 00   A  0   0  4

  [15] .eh_frame         PROGBITS        080484f8 0004f8 000004 00   A  0   0  4

  [16] .ctors            PROGBITS        080494fc 0004fc 000008 00  WA  0   0  4

  [17] .dtors            PROGBITS        08049504 000504 000008 00  WA  0   0  4

  [18] .jcr              PROGBITS        0804950c 00050c 000004 00  WA  0   0  4

  [19] .dynamic          DYNAMIC         08049510 000510 0000c8 08  WA  5   0  4

  [20] .got              PROGBITS        080495d8 0005d8 000004 04  WA  0   0  4

  [21] .got.plt          PROGBITS        080495dc 0005dc 000018 04  WA  0   0  4

  [22] .data             PROGBITS        080495f4 0005f4 00000c 00  WA  0   0  4

  [23] .bss              NOBITS          08049600 000600 000004 00  WA  0   0  4


[12번] 텍스트 영역에는 컴파일시 생성된 어셈 코드들이 담겨 있다. 그리고 좀더 높은 영역으로 가다보면 main 함수를 거쳐 got.plt 영역까지 보이게 된다.

(gdb) x/50xi 0x080482f8

0x80482f8 <_start>: xor    %ebp,%ebp

0x80482fa <_start+2>: pop    %esi

0x80482fb <_start+3>: mov    %esp,%ecx



           

아래는 printf 를 두번 호출해 코드의 디버깅 모습이다. 빨간색으로 표히산 부분들이 printf를 호출하는 부분인데 이를 트레이싱해보겠다.


 (gdb) disass main

Dump of assembler code for function main:

0x080483b8 <main+0>: push   %ebp

0x080483b9 <main+1>: mov    %esp,%ebp

0x080483bb <main+3>: sub    $0x8,%esp

0x080483be <main+6>: and    $0xfffffff0,%esp

0x080483c1 <main+9>: mov    $0x0,%eax

0x080483c6 <main+14>: add    $0xf,%eax

0x080483c9 <main+17>: add    $0xf,%eax

0x080483cc <main+20>: shr    $0x4,%eax

0x080483cf <main+23>: shl    $0x4,%eax

0x080483d2 <main+26>: sub    %eax,%esp

0x080483d4 <main+28>: sub    $0xc,%esp

0x080483d7 <main+31>: push   $0x80484ee

0x080483dc <main+36>: call   0x80482e8 <_init+72>

0x080483e1 <main+41>: add    $0x10,%esp

0x080483e4 <main+44>: sub    $0xc,%esp

0x080483e7 <main+47>: push   $0x80484f0

0x080483ec <main+52>: call   0x80482e8 <_init+72>

.

.

(중략)

처음 call   0x80482e8 <_init+72> 호출되는 부분이 PLT 이다.

이를 따라가보면 아래와 같다.

(gdb) x/10xi 0x80482e8

0x80482e8 <_init+72>: jmp    *0x80495f0

0x80482ee <_init+78>: push   $0x10

0x80482f3 <_init+83>: jmp    0x80482b8 <_init+24>


처음 호출된 주소를 따라가면 jmp    *0x80495f0 를 만나게 된다. 이를 살펴보면 GOT 영역이란 것을 알 수 있다. 여기까지는 PLT -> GOT을 참조하는 영역으로써 컴파일 시에 이미 결정 되어 있는 것이다.

그값을 보면 GOT영역이라는 것을 확연히 볼 수 있다.


(gdb) x/12x 0x80495f0-20   //테이블을 전체를 보여주기 위해 20만큼 빼 보기좋게 표현했다.

0x80495dc <_GLOBAL_OFFSET_TABLE_>: 0x08049510 0x007194f8 0x0070e9e0 0x080482ce

0x80495ec <_GLOBAL_OFFSET_TABLE_+16>:0x00730d50 0x080482ee 0x00000000 0x00000000


이러한 테이블이 참조하는 영역은 아래와 같다.

(gdb) x/10xi *0x80495f0 

0x80482ee <_init+78>: push   $0x10

0x80482f3 <_init+83>: jmp    0x80482b8 <_init+24>


위에서 보면 이러한 글로벌 테이블은 처음 호출시에는 위와같이 한번더 점프하게 되는데, 그영역을 봐보면 아래와같이 어떠한 인자값을 넣고, dl_runtime_resolve 영역으로 점프하게 된다.

(gdb) x/3i 0x80482b8

0x80482b8 <_init+24>: pushl  0x80495e0

0x80482be <_init+30>: jmp    *0x80495e4


_dl_runtime_resolve 에서는 printf의 코드 영역의 주소를 찾아 GOT 테이블에 삽입하고 호출 후, 다시 복귀하는 루틴으로 구성 되어 있다.

(gdb) x/3i *0x80495e4

0x70e9e0 <_dl_runtime_resolve>: push   %eax

0x70e9e1 <_dl_runtime_resolve+1>: push   %ecx

0x70e9e2 <_dl_runtime_resolve+2>: push   %edx




자 이제 두번째  call   0x80482e8 <_init+72> 호출되는 부분이 PLT 이다. 간단하게 break point 를 걸어놓고 한번 printf 가 호출한 뒤 디버깅을 똑같이 진행해 보겠다.


(gdb) b *main+41

(gdb) c  //함수를 한번 호출시킨뒤 계속 진행


(gdb) x/3xi 0x80482e8

0x80482e8 <_init+72>: jmp    *0x80495f0

0x80482ee <_init+78>: push   $0x10

0x80482f3 <_init+83>: jmp    0x80482b8 <_init+24>


(gdb) x/12x 0x80495f0-20  //테이블을 보기 좋게 20을 빼준뒤 출력시켜 줌

0x80495dc <_GLOBAL_OFFSET_TABLE_>: 0x08049510 0x007194f8 0x0070e9e0 0x080482ce

0x80495ec <_GLOBAL_OFFSET_TABLE_+16>:0x00730d50 0x0075e660 0x00000000 0x00000000


(gdb) x/3xi *0x80495f0

0x75e660 <printf>: push   %ebp

0x75e661 <printf+1>: mov    %esp,%ebp

0x75e663 <printf+3>: lea    0xc(%ebp),%eax


위 과정을 보면 첫번째 호출 했을때의 과정이 사라지고 printf 함수가 바로 호출 되었음을 볼 수 있다.

즉 첫번째 과정에서 _dl_runtime_resolve 함수가 GOT 테이블에 printf 주소를 입력 시켜 놓은 것이다.




Posted by k1rha