How main() is executed on Linux

ELF에 대해 공부를 하다가 아래 문서를 발견하고 기록 겸 블로그에 포스팅 합니다. : - ) 

http://www.tldp.org/LDP/LGNET/issue84/hawk.html - How main() is executed on Linux - By Hyouck "Hawk" Kim


Starting

리눅스 환경에서 main() 함수의 동작 원리를 알아 보기 위해 아래 코드를 작성 후 빌드 한다.

1
2
3
4
5
6
7
[root@localhost ~]# vi main.c 
 
main()
{
  return (0);
}
 



Build

1
[root@localhost ~]# gcc -o main main.c


What's in the executable?

objdump 를 이용하여 컴파일된 파일의 정보를 확인할 수 있다. -f 옵션은 해당 파일의 fileheader를 출력해주는 옵션이다.

상기 과정을 통해 생성했던 main 파일의 정보를 확인할 수 있는데 우리가 눈여겨 봐야할 부분은 file format, start address의 정보이다.

첫번쨰로 ELF는 무엇인가? 두번째로 start address는 왜 0x080482a0을 가리키고 있을까?

1
2
3
4
5
6
7
[root@localhost ~]# objdump -f main
 
main:     file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x080482a0
 


What's ELF?

ELF는 Executable and Linking Format의 줄임말이며, 유닉스 시스템에서 사용되는 여러 오브젝트파일/실행파일 형식중 하나이다. 

모든 ELF 실행파일은 다음과 같은 ELF 헤더를 가진다. 구조체에서 "e_entry" 필드는 실행파일의 시작주소이다.

typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;


What's at address "0x080482a0", that is, starting address?

main 파일을 디스어셈블 해보도록 한다. 디스어셈블할 수 있는 도구는 많지만 여기에선 objdump를 이용하여 디스어셈블을 진행한다. 옵션은 -d를 사용한다. 출력 결과가 길지만 여기에선 0x080482a0 부분만 보도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
[root@localhost ~]# objdump -d main
 
main:     file format elf32-i386
 
Disassembly of section .init:
 
08048250 <_init>:
 8048250:    55                       push   %ebp
 8048251:    89 e5                    mov    %esp,%ebp
 8048253:    83 ec 08                 sub    $0x8,%esp
 8048256:    e8 69 00 00 00           call   80482c4 <call_gmon_start>
 804825b:    e8 f0 00 00 00           call   8048350 <frame_dummy>
 8048260:    e8 9b 01 00 00           call   8048400 <__do_global_ctors_aux>
 8048265:    c9                       leave  
 8048266:    c3                       ret    
Disassembly of section .plt:
 
08048268 <__gmon_start__@plt-0x10>:
 8048268:    ff 35 a8 95 04 08        pushl  0x80495a8
 804826e:    ff 25 ac 95 04 08        jmp    *0x80495ac
 8048274:    00 00                    add    %al,(%eax)
    ...
 
08048278 <__gmon_start__@plt>:
 8048278:    ff 25 b0 95 04 08        jmp    *0x80495b0
 804827e:    68 00 00 00 00           push   $0x0
 8048283:    e9 e0 ff ff ff           jmp    8048268 <_init+0x18>
 
08048288 <__libc_start_main@plt>:
 8048288:    ff 25 b4 95 04 08        jmp    *0x80495b4
 804828e:    68 08 00 00 00           push   $0x8
 8048293:    e9 d0 ff ff ff           jmp    8048268 <_init+0x18>
Disassembly of section .text:
 
080482a0 <_start>:
 80482a0:    31 ed                    xor    %ebp,%ebp
 80482a2:    5e                       pop    %esi
 80482a3:    89 e1                    mov    %esp,%ecx
 80482a5:    83 e4 f0                 and    $0xfffffff0,%esp
 80482a8:    50                       push   %eax
 80482a9:    54                       push   %esp
 80482aa:    52                       push   %edx
 80482ab:    68 80 83 04 08           push   $0x8048380
 80482b0:    68 90 83 04 08           push   $0x8048390
 80482b5:    51                       push   %ecx
 80482b6:    56                       push   %esi
 80482b7:    68 74 83 04 08           push   $0x8048374
 80482bc:    e8 c7 ff ff ff           call   8048288 <__libc_start_main@plt>
 80482c1:    f4                       hlt    
 80482c2:    90                       nop    
 80482c3:    90                       nop    
.
.
.
 
cs


"_start"함수의 스택 프레임을 정리하면 아래와 같은 모양의 스택프레임이 생성될 것이다.

Stack Top -------------------
0x8048374
-------------------
esi
-------------------
ecx
-------------------
0x8048390
-------------------
0x8048380
-------------------
edx
-------------------
esp
-------------------
eax
-------------------

이제 이 스택프레임에 대한 궁금증이 더 생겼다.

1. 이 16진수 값들은 무엇인가? 

2. _start가 호출하는 주소 8048288에는 무엇이 있는가? 

3. 어셈블리 명령어는 레지스터를 의미있는 값으로 초기화하지 않는 것 같다. 그러면 누가 레지스터를 초기화하나? 


Question 1> The hexa values.

objdump의 디스어셈블된 출력을 자세히 살펴보면 이 질문에 쉽게 답할 수 있다.

0x8048374 :   이는 우리가 만든 main() 함수의 주소이다.

0x8048390 :   _init 함수.

0x8048380 :   _fini 함수. _init과 _fini는 GCC가 제공하는 초기화(initialization)/종료(finalization) 함수이다.

기본적으로 이 16진수 값들은 함수포인터다.


Question 2> What's at address 08048288?

디스어셈블된 코드에서 80482bc를 확인 시 jmp *0x80495b4 명령을 통해  0x80495b4에 저장된 주소로 건너뛰는 것을 확인할 수 있다.

1
2
3
4
08048288 <__libc_start_main@plt>:
 8048288:    ff 25 b4 95 04 08        jmp    *0x80495b4
 804828e:    68 08 00 00 00           push   $0x8
 8048293:    e9 d0 ff ff ff           jmp    8048268 <_init+0x18>
cs


More about ELF and dynamic linking

ELF를 사용하여 라이브러리에 동적으로 링크되는 실행파일을 만들 수 있다.

여기서 "동적으로 링크된다는" 말은 링크 과정이 실행 시 발생함을 의미한다. 그렇지않으면 호출하는 모든 라이브러리를 포함하는 큰 실행파일을 ("정적으로 링크된" 실행파일) 만들어야 한다. 

1
2
3
4
[root@localhost ~]# ldd main
    linux-gate.so.1 =>  (0x00a07000)
    libc.so.6 => /lib/libc.so.6 (0x0042a000)
    /lib/ld-linux.so.2 (0x0040b000)
cs

ldd 명령어를 이용해 생성한 main 파일의 동적으로 링크되어있는 모든 라이브러리를 확인할 수 있으며, 동적으로 링크되는 자료와 함수는 모두 "동적 재배치 항목"을 가지게 된다.

대략적인 개념은 다음과 같다.

1. 우리는 링크 시 동적 심볼(Dynamic Symbol)의 실제 주소를 모른다. 실행 시 심볼의 실제 주소를 알게 된다.

2. 동적 심볼의 실제 주소를 위해 메모리 공간을 남겨둔다. 로더(loader)가 실행 시 남겨둔 메모리 공간에 심볼의 실제 주소를 쓴다.

3. 

objdump의 -R 옵션을 이용하여 동적 링크된 항목을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
[root@localhost ~]# objdump -R main
 
main:     file format elf32-i386
 
DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE 
080495a0 R_386_GLOB_DAT    __gmon_start__
080495b0 R_386_JUMP_SLOT   __gmon_start__
080495b4 R_386_JUMP_SLOT   __libc_start_main
 
cs


What's __libc_start_main?

__libc_start_main은 libc.so.6에 잇는 함수다. glib 소스 코드에서 __libc_start_main 함수를 찾아보면 함수형은 아래와 같다.

d

extern int BP_SYM (__libc_start_main) (int (*main) (int, char **, char **),
int argc,
char *__unbounded *__unbounded ubp_av,
void (*init) (void),
void (*fini) (void),
void (*rtld_fini) (void),
void *__unbounded stack_end)
__attribute__ ((noreturn));



아래의 스택 프레임을 보면 __libc_start_main 함수가 실행되기 전에  esi, ecs, edx, esp, eax 레지스터에 적당한 값이 채워져아 한다.  그러나 _start 명령어는 이 레지스터를 설정하지 않는다. 그러면 누가 이 레지스터를 설정할까? 한 가지 추측할 수 있는 부분은 커널이다.

세번째 질문으로 돌아가보자.

Stack Top -------------------
0x8048374 main
-------------------
esi argc
-------------------
ecx argv
-------------------
0x8048390 _init
-------------------
0x8048380 _fini
-------------------
edx _rtlf_fini
-------------------
esp stack_end
-------------------
eax this is 0
-------------------



Question 3> What does the kernel do?

우리가 쉘(Shell)에 명령어를 입력 하여 프로그램을 실행시킬 때 리눅스의 경우 다음과 같은 현상이 일어난다.

1. 쉘은 argc/argv를 이용해 커널 시스템 콜인 "execve"를 호출 한다.

2. 커널 시스템 콜 핸들러는 제어와 시스템 콜을 처리하기 시작한다. 커널 코드에서 핸들러는 "sys_execve" 이며 x86 시스템에서 사용자 모드 어플리케이션은 아래 레지스터를 통해 필요한 파라미터를 커널에 넘긴다.

 ○ ebx : 프로그램 문자열의 포인터

 ○ ecx : argv 배열 포인터

 ○ ebx : 환경변수 배열 포인터

3. 일반









+ Recent posts