본문 바로가기

Research/Linux

[번역] mach-O 와 ELF 의 링킹 과정에 대해



마크오 볼 일이 있어서.. 관련 항목 도장 깨기 하다 읽은 글을 한글로 짜집기 해 보았다.





memprof 를 빌딩하다가 마주친 ELF 와 Mach-O 의 다이나믹 링킹의 유사점과 차이점에 대해 설명한다.

PLT - Procedure Linkage Table
PLT 는 런타임에 함수 본체의 주소를 저장하는 테이블이다. Mach-O 와 ELF 모두 PLT 를 가지며 이 테이블은 컴파일 타임에 생성된다. 테이블 초기값은 다이나믹 링커를 호출하도록 구성되고, 링커는 호출될 함수의 심볼을 찾는다. 이런 작업방식은 ELF 와 Mach-O 의 하이레벨에선 매우 비슷하지만 구현상의 차이가 있고, 이에 대해 이야기해볼 만한 가치가 있다고 생각한다.

Mach-O 에서의 PLT 의 정렬
Mach-O 오브젝트는 PLT 엔트리를 만드는 데 관련한 여러 섹션들을 갖고 있으며, 이 섹션은 여러 세그먼트에 걸쳐 있다(원문 : Mach-O objects have several different sections across different segments that are all involved to create a PLT entry for a specific symbol). - 역 : 아래 글을 보면.. PLT 엔트리 작성을 위한 정보를 여러 세그먼트에 걸쳐 갖고 있다, 정도로 해석하는 게 좋을 듯.

다음의 어셈블리 블럭을 생각해보자. 이 블럭은 malloc 함수의 PLT 엔트리를 호출한다.

# MACH-O calling a PLT entry (ELF is nearly identical)
0x000000010008c504 [str_new+52]:        callq  0x10009ebbc [dyld_stub_malloc]

역 - 테스트 코드는 printf 로 진행했다.
;실제 내 코드
;0x100000f33 <+131>: callq  0x100000f52 ; symbol stub for: printf

$ gobjdump -x plt.check

plt.check:     file format mach-o-x86-64
plt.check
architecture: i386:x86-64, flags 0x00000012:
EXEC_P, HAS_SYMS
start address 0x0000000100000e70

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         000000be  0000000100000e70  0000000100000e70  00000e70  2**4
                  CONTENTS, ALLOC, LOAD, CODE
  1 __TEXT.__stubs 00000024  0000000100000f2e  0000000100000f2e  00000f2e  2**1 << 이 섹션에 해당함.
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 __TEXT.__stub_helper 0000004c  0000000100000f54  0000000100000f54  00000f54  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  3 .cstring      0000000b  0000000100000fa0  0000000100000fa0  00000fa0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .const        00000002  0000000100000fab  0000000100000fab  00000fab  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 __TEXT.__unwind_info 00000048  0000000100000fb0  0000000100000fb0  00000fb0  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  6 __DATA.__nl_symbol_ptr 00000010  0000000100001000  0000000100001000  00001000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
  7 __DATA.__la_symbol_ptr 00000030  0000000100001010  0000000100001010  00001010  2**3
                  CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
0000000100000000 g       0f SECT   01 0010 [.text] __mh_execute_header
0000000100000e70 g       0f SECT   01 0000 [.text] _main
0000000000000000 g       01 UND    00 0100 _free
0000000000000000 g       01 UND    00 0100 _malloc
0000000000000000 g       01 UND    00 0100 _memset
0000000000000000 g       01 UND    00 0100 _printf
0000000000000000 g       01 UND    00 0100 _puts
0000000000000000 g       01 UND    00 0100 _scanf
0000000000000000 g       01 UND    00 0100 dyld_stub_binder

dyld_stub 접두어(symbol stub)는 callq 인스트럭션이 malloc 자체가 아닌 PLT 엔트리를 호출함을 알려주기 위해 추가되었다. 0x10009ebbc 주소는 malloc 의 PLT 엔트리의 첫번째 인스트럭션이 위치한 주소이다. Mach-O 의 용어로, 이런 0x10009ebbc 의 인스트럭션은 symbol stub 라고 불린다. symbol stub 는 __symbol_stub1 섹션의 __TEXT 세그먼트에서 확인할 수 있다. 

역 - 테스트 코드에서는 0x100000f52 주소가 printf PLT 주소이다. 심볼명에 dyld_stub 라는 접두어가 들어가지는 않지만, gobjdump 를 통해 함수호출 주소가 __stubs 섹션에 속함을 확인할 수 있다. 대신 dyld_stub_binder 라는 함수가 있다.


# MACH-O "symbol stubs" for malloc and other functions
0x10009ebbc [dyld_stub_malloc]:   jmpq   *0x3ae46(%rip)        # 0x1000d9a08
0x10009ebc2 [dyld_stub_realloc]:  jmpq   *0x3ae48(%rip)        # 0x1000d9a10
0x10009ebc8 [dyld_stub_seekdir$INODE64]:        jmpq   *0x3ae4c(%rip)  # 0x1000d9a20

역 - IDA 에서는 심심하게 디스어셈블링 되어서, lldb 를 통해 디스어셈블링 했다. 
(lldb) disas -a 0x100000f52
plt.check`printf:
0x100000f52 <+0>: jmpq   *0xc8(%rip)               ; (void *)0x0000000100000f88

(lldb) disas -s 0x100000f3a -C 24
plt.check`memset:
plt.check[0x100000f3a] <+0>: jmpq   *0xe0(%rip)               ; (void *)0x0000000100000f78
plt.check`printf:
plt.check[0x100000f40] <+0>: jmpq   *0xe2(%rip)               ; (void *)0x0000000100000f82
plt.check`puts:
plt.check[0x100000f46] <+0>: jmpq   *0xe4(%rip)               ; (void *)0x0000000100000f8c
plt.check`scanf:
plt.check[0x100000f4c] <+0>: jmpq   *0xe6(%rip)               ; (void *)0x0000000100000f96

각각의 symbol stub 는 하나의 jmpq 인스트럭션으로만 이루어져 있다.

jmpq 인스트럭션은 아래 두 가지의 역할을 한다.
- 다이나믹 링커를 호출하여 심볼을 찾고 실행한다.
- 실행 흐름을 직접 함수로 옮긴다.
두 행위 모두 테이블 내 엔트리를 통해 이뤄진다.

앞선 예를 통해, malloc 의 테이블 엔트리 주소가 1000d9a08 임을 알 수 있다. 이 테이블 엔트리는 __DATA 세그먼트의 __la_symbol_ptr 이라고 불리는 섹션에 저장된다.
-역) 아래에 나올 dyld_stub_binder 의 정보는 __la_symbol_ptr 이 아닌 __nl_symbol_ptr 에 저장된다. la_symbol_ptr 섹션과 붙어 있음.
malloc 이 정의(resolve, 복구?)되기 전에, 테이블 엔트리의 주소는 helper function 을 가리키고, 이 helper function 은 다이나믹 링커를 호출하여 malloc 을 찾고 그 주소를 테이블 엔트리에 채운다.
아래의 예를 통해 helper function 의 형태를 확인해보자.

# MACH-O stub helpers
0x1000a08d4 [stub helpers+6986]:        pushq  $0x3b73
0x1000a08d9 [stub helpers+6991]:        jmpq   0x10009ed8a [stub helpers]
0x1000a08de [stub helpers+6996]:        pushq  $0x3b88
0x1000a08e3 [stub helpers+7001]:        jmpq   0x10009ed8a [stub helpers]
0x1000a08e8 [stub helpers+7006]:        pushq  $0x3b9e
0x1000a08ed [stub helpers+7011]:        jmpq   0x10009ed8a [stub helpers]
. . . .

역 - 테스트 코드
(lldb) x/10i 0x0000000100000f82
0x100000f82: 68 28 00 00 00  pushq  $0x28
0x100000f87: e9 c8 ff ff ff  jmp    0x100000f54
0x100000f8c: 68 36 00 00 00  pushq  $0x36
0x100000f91: e9 be ff ff ff  jmp    0x100000f54
0x100000f96: 68 42 00 00 00  pushq  $0x42
0x100000f9b: e9 b4 ff ff ff  jmp    0x100000f54

__stub_helper:0000000100000F82                 push    28h
__stub_helper:0000000100000F87                 jmp     loc_100000F54
__stub_helper:0000000100000F8C                 push    36h
__stub_helper:0000000100000F91                 jmp     loc_100000F54
__stub_helper:0000000100000F96                 push    42h
__stub_helper:0000000100000F9B                 jmp     loc_100000F54
__stub_helper:0000000100000F9B __stub_helper   ends

PLT 엔트리가 갖고 있는 각각의 심볼은 pushq+jmpq 두 개의 인스트럭션을 가진다. 이 인스트럭션 쌍은 함수의 ID 를 세팅한 후 다이나믹 링커를 호출한다. 다이나믹 링커는 이 ID 를 통해 찾을 함수를 식별한다.


ELF 에서의 PLT 정렬

ELF 객체는 동일한 메커니즘을 가지고 있지만 각 PLT 항목을 여러 섹션에 걸쳐(across different sections) 연결하는 대신 청크로 구성한다. ELF 객체에서 malloc에 대한 PLT 항목을 살펴보자.

# ELF complete PLT entry for malloc
0x40f3d0 [malloc@plt]:  jmpq   *0x2c91fa(%rip)        # 0x6d85d0
0x40f3d6 [malloc@plt+6]:        pushq  $0x2f
0x40f3db [malloc@plt+11]:       jmpq   0x40f0d0
. . . .

Mach-O 객체와 매우 비슷하게 ELF 객체는 테이블 엔트리를 사용해서 실행 흐름을 돌린다. 동적 링커를 호출하거나, 이미 심볼을 통해 함수 주소값을 알고 있는 경우 원하는 함수를 직접 호출한다.

여기서 확인할 두 가지의 차이점은
- ELF 는 PLT 엔트리를 여러 섹션에 걸쳐넣지 않고(instead of splicing it out across multiple sections), plt 라고 불리는 섹션 하나에 넣는다. 
- 함수 주소를 모를 때(초기화되었을 당시에) jmpq 를 통해 간접적으로 사용된 테이블 엔트리는 got.plt 라고 불리는 섹션에 저장된다. (역 - malloc@plt 에서 호출하게 될 0x6d85d0 는 .got.plt 영역)


둘 다 어셈블리 트램펄린을 호출함.

Mach-O 와 ELF 모두 런타임에서 다이나믹 링커를 호출하도록 한다. 둘 다 어플리케이션과 링커 사이의 갭을 메우기 위해 어셈 트램펄린을 필요로 한다. 64비트 인텔 시스템에서, 두 시스템의 링커는 동일한 Application Binary Interface 를 준수해야 한다.

다만 확인해 볼 점은, 두 링커는 동일한 콜링 컨벤션을 사용함에도 불구하고 어셈블리 트램펄린의 구현이 다소 다르게 되어 있다.

amd64 ABI 콜링 컨벤션을 준수하기 위해서, 두 트램펄린은 프로그램 스택이 16바이트 정렬을 갖도록 한다. 두 트램펄린은 다이나믹 링크를 호출하기 전에 "범용" caller-saved 레지스터를 저장하는 데에 주의를 기울인다. 하지만 리눅스의 트램펄린은 SSE 레지스터를 저장하거나 복원하지 못한다. glibc 가 다이나믹 링커 내에서 해당 레지스터들을 사용하지 않도록 주의하는 한 큰 문제는 아니다(원문 : It turns out that this “shouldn’t” matter, so long as glibc takes care not to use any of those registers in the dynamic linker.). OSX는 보다 보수적으로, 동적 링커를 호출하기 전후에 SSE 레지스터 또한 저장 및 복원한다.

아래에 두 개의 트램펄린 코드 스니핏을 추가했다. 


같은 ABI 에서의 다른 트램펄린 코드

OSX 에서의 트램펄린

dyld_stub_binder:
  pushq   %rbp
  movq    %rsp,%rbp
  subq    $STACK_SIZE,%rsp  # 두 파라미터가 푸시될 장소를 위해, 16바이트의 스택을 정렬한다.
  movq    %rdi,RDI_SAVE(%rsp) # 파라미터로써 사용될지 모르는 레지스터들을 저장한다.
  movq    %rsi,RSI_SAVE(%rsp)
  movq    %rdx,RDX_SAVE(%rsp)
  movq    %rcx,RCX_SAVE(%rsp)
  movq    %r8,R8_SAVE(%rsp)
  movq    %r9,R9_SAVE(%rsp)
  movq    %rax,RAX_SAVE(%rsp)
  movdqa    %xmm0,XMMM0_SAVE(%rsp)
  movdqa    %xmm1,XMMM1_SAVE(%rsp)
  movdqa    %xmm2,XMMM2_SAVE(%rsp)
  movdqa    %xmm3,XMMM3_SAVE(%rsp)
  movdqa    %xmm4,XMMM4_SAVE(%rsp)
  movdqa    %xmm5,XMMM5_SAVE(%rsp)
  movdqa    %xmm6,XMMM6_SAVE(%rsp)
  movdqa    %xmm7,XMMM7_SAVE(%rsp)
  movq    MH_PARAM_BP(%rbp),%rdi  # call fastBindLazySymbol(loadercache, lazyinfo)
  movq    LP_PARAM_BP(%rbp),%rsi
  call    __Z21_dyld_fast_stub_entryPvl


OSX 트램펄린에서는 마지막 call 인스트럭션에서 다이나믹 링커를 불러오기 전에, xmm0-xmm7 레지스터들을 포함한 모든 caller 의 레지스터들을 저장한다. 이 레지스터들은 모두 call 이후 복구되지만, 분량을 위해 삭제했다.

역 - 확인해본 dyld_stub_binder 테스트 코드는 아래와 같다.
libdyld.dylib`dyld_stub_binder:
    0x7fff5045e278 <+0>:   pushq  %rbp

    ...

    0x7fff5045e30c <+148>: jne    0x7fff5045e346            ; <+206>
    0x7fff5045e30e <+150>: subq   $0x80, %rsp
    0x7fff5045e315 <+157>: movdqa %xmm0, (%rsp)
    0x7fff5045e31a <+162>: movdqa %xmm1, 0x10(%rsp)
    0x7fff5045e320 <+168>: movdqa %xmm2, 0x20(%rsp)
    0x7fff5045e326 <+174>: movdqa %xmm3, 0x30(%rsp)
    0x7fff5045e32c <+180>: movdqa %xmm4, 0x40(%rsp)
    0x7fff5045e332 <+186>: movdqa %xmm5, 0x50(%rsp)
    0x7fff5045e338 <+192>: movdqa %xmm6, 0x60(%rsp)
    0x7fff5045e33e <+198>: movdqa %xmm7, 0x70(%rsp)
    0x7fff5045e344 <+204>: jmp    0x7fff5045e385            ; <+269>

    ...

    0x7fff5045e385 <+269>: movq   0x8(%rbp), %rdi
    0x7fff5045e389 <+273>: movq   0x10(%rbp), %rsi
    0x7fff5045e38d <+277>: callq  0x7fff50460014            ; _dyld_fast_stub_entry(void*, long)

    ...

    0x7fff5045e39e <+294>: movdqa (%rsp), %xmm0
    0x7fff5045e3a3 <+299>: movdqa 0x10(%rsp), %xmm1
    0x7fff5045e3a9 <+305>: movdqa 0x20(%rsp), %xmm2
    0x7fff5045e3af <+311>: movdqa 0x30(%rsp), %xmm3
    0x7fff5045e3b5 <+317>: movdqa 0x40(%rsp), %xmm4
    0x7fff5045e3bb <+323>: movdqa 0x50(%rsp), %xmm5
    0x7fff5045e3c1 <+329>: movdqa 0x60(%rsp), %xmm6
    0x7fff5045e3c7 <+335>: movdqa 0x70(%rsp), %xmm7


아래는 리눅스에서의 트램펄린이다.

  subq $56,%rsp
  cfi_adjust_cfa_offset(72) # PLT 편입(Incorporate PLT) (??)
  movq %rax,(%rsp)  # 레지스터들을 다른 방식으로 저장한다.
  movq %rcx, 8(%rsp)
  movq %rdx, 16(%rsp)
  movq %rsi, 24(%rsp)
  movq %rdi, 32(%rsp)
  movq %r8, 40(%rsp)
  movq %r9, 48(%rsp)
  movq 64(%rsp), %rsi # PLT 레지스터에 의해 푸시된 인자를 복사한다.
  movq %rsi, %r11   # 24를 곱한다.(r11*3*2**3)
  addq %r11, %rsi
  addq %r11, %rsi
  shlq $3, %rsi
  movq 56(%rsp), %rdi # %rdi: link_map, %rsi: reloc_offset
  call _dl_fixup    # Call resolver.


리눅스에서의 트램펄린은 SSE 레지스터를 건드리지 않는데, 이는 다이나믹 링커가 SSE 레지스터를 건드리지 않는다 라고 가정하고 있기 때문이다. 그래서 이 과정에 저장/복구 절차가 들어가지 않는다.


정리

  • 함수 호출부에서 다이나믹 링커의 실행 흐름을 추적하는 것은 꽤 흥미로우며 배울 부분이 많다.
  • glibc 는 xmm0-xmm7 레지스터를 저장 및 복구하지 않는다는 게 꽤 불안하다. 해당 레지스터들을 건드리지 않는다는 걸 빌드된 ld.so 를 디스어셈 해서 확인하긴 했다만, 여전히 불안하게 하는 부분이다.
  • Mach-O 와 ELF 사이의 유사점/차이점에 대해 많은 게시물을 기다리고 있다.




역 - 세줄요약

  1. OS X 에서도 PLT 쓴다.
  2. OS X 에서 PLT resolve 는 ELF 와 달리 여러 섹션에 걸쳐 일어난다.
  3. OS X 에서는 PLT resolve 시에 SSE 레지스터도 저장/복구한다.

...; 써놓고 나니 별거 없..는것 같...


'Research > Linux' 카테고리의 다른 글

docker template  (0) 2016.11.05
glibc malloc understanding  (0) 2016.09.08
동적 메모리 관련  (0) 2016.09.06
directly run python code using vim  (0) 2016.06.08
When sidebar(unity) disappeared in Ubuntu  (0) 2016.05.31