본문 바로가기

개발 개발/Android

objdump


출처 : http://smilk.egloos.com/486882

시작

질문은 간단하다. 리눅스는 어떻게 내가 만든 main()을 실행하나? 
이 글은 아래 간단한 C 프로그램을 예로 삼아 설명한다. 이를 "simple.c"라 하자.

main() {
return(0);
}

컴파일

 

gcc -o simple simple.c

실행파일에는 무엇이 들어있나?

실행파일에 무엇이 있는지 보기위해 "objdump" 도구를 사용하자.

objdump -f simple

simple: file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x080482d0
출력은 실행파일에 대한 귀중한 정보를 알려준다.
 먼저 파일은 "ELF32" 형식이다. 둘째로 시작주소가 "0x080482d0"이다.

ELF란 무엇인가?

ELF는 Executable and Linking Format의 준말로, 유닉스 시스템에서 사용되는 여러 오브젝트파일/실행파일 형식중 하나이다. 
여기서 ELF에 대해 흥미로운 것은 ELF 헤더 형식이다. 모든 ELF 실행파일은 다음과 같은 ELF 헤더를 가진다.

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;
위 구조체에서 "e_entry" 필드는 실행파일의 시작주소이다.

주소 "0x080482d0", 즉 시작주소가 무엇인가?

답을 위해, "simple"을 역어셈블해보자. 실행파일을 역어셈블하는 도구는 많다. 여기서는 objdump를 사용한다.

objdump --disassemble simple
objdump의 출력이 조금 길기때문에 다 적지는 않겠다. 우리의 목적은 주소 0x080482d0에 무엇이 있는지 아는 것이다. 출력은 다음과 같다.
080482d0 <_start>:
80482d0: 31 ed xor %ebp,%ebp
80482d2: 5e pop %esi
80482d3: 89 e1 mov %esp,%ecx
80482d5: 83 e4 f0 and $0xfffffff0,%esp
80482d8: 50 push %eax
80482d9: 54 push %esp
80482da: 52 push %edx
80482db: 68 20 84 04 08 push $0x8048420
80482e0: 68 74 82 04 08 push $0x8048274
80482e5: 51 push %ecx
80482e6: 56 push %esi
80482e7: 68 d0 83 04 08 push $0x80483d0
80482ec: e8 cb ff ff ff call 80482bc <_init+0x48>
80482f1: f4 hlt
80482f2: 89 f6 mov %esi,%esi
시작주소에 "_start"라는 어떤 시작함수가 있는 것 같다. 하는 일은 레지스터를 지우고, 스택에 값을 몇개 푸쉬한(push) 후, 함수를 호출한다. 이 명령을 실행하면 스택플래임(stack frame)은 다음과 같이 된다.
Stack Top -------------------
0x80483d
-------------------
esi
-------------------
ecx
-------------------
0x8048274
-------------------
0x8048420
-------------------
edx
-------------------
esp
-------------------
eax
-------------------

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

  1. 이 16진수 값들은 무엇인가? 
  2. _start가 호출하는 주소 80482bc에는 무엇이 있는가? 
  3. 어셈블리 명령어는 레지스터를 의미있는 값으로 초기화하지 않는 것 같다. 그러면 누가 레지스터를 초기화하나? 

 

하나씩 답을 하겠다.

Q1>16진수 값.

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

답은, 

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

0x8048274 :         _init 함수.

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

당장은 이들을 신경쓰지마라. 기본적으로 이 16진수 값들은 함수포인터다.

Q2>주소 80482bc에 무엇이 있나?

다시 역어셈블된 출력에서 주소 80482bc를 찾자.
찾으면 다음과 같을 것이다.

80482bc: ff 25 48 95 04 08 jmp *0x8049548

여기서 *0x8049548는 포인터 연산이다.
 주소 0x8049548에 저장된 주소로 건너뛴다.


ELF와 동적링크에 대해 더 자세히

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

"ldd simple"

libc.so.6 => /lib/i686/libc.so.6 (0x42000000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

simple과 동적으로 링크된 모든 라이브러리를 볼 수 있다. 동적으로 링크되는 자료와 함수는 모두 "동적 재배치 항목 (dynamic relocation entry)"을 가진다. 

개념은 대략 다음과 같다. 
  1. 우리는 링크시 동적 심볼의 실제 주소를 모른다. 실행할때가 되서야 심볼의 실제 주소를 알게된다.
  2. 동적 심볼의 실제 주소를 위해 메모리 공간을 남겨둔다. 
    로더(loader)가 실행시 이곳에 심볼의 실제 주소를 쓴다. 
  3. 프로그램은 일종의 포인터 연산을 통해 간접적으로 이 메모리 위치에 있는 동적 심볼을 보게된다. 우리의 경우 주소 80482bc에는 달랑 jump 명령어 하나가 있다.
    실행시 로더가 건너뛸 주소를 주소 0x8049548에 저장한다.
    objdump 명령어로 모든 동적 링크 항목을 볼 수 있다.
    objdump -R simple

    simple: file format elf32-i386

    DYNAMIC RELOCATION RECORDS
    OFFSET TYPE VALUE
    0804954c R_386_GLOB_DAT __gmon_start__
    08049540 R_386_JUMP_SLOT __register_frame_info
    08049544 R_386_JUMP_SLOT __deregister_frame_info
    08049548 R_386_JUMP_SLOT __libc_start_main
    여기서 주소 0x8049548은 알맞게 "jump slot"으로 나왔다. 표에 따르면 우리는 실제 __libc_start_main을 호출한다.

__libc_start_main은 무엇인가?

이제 작업은 libc에 달렸다. __libc_start_main은 libc.so.6에 있는 함수다. glibc 소스코드에서 __libc_start_main을 찾으면 함수형이 다음과 같다.

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));
(_start) 어셈블리 명령어는 아규먼트 스택을 만들고 __libc_start_main을 부른다. 
이 함수는 몇몇 자료구조와 환경변수를 만들고 초기화한 후, 우리가 만든 main()을 부른다. 
이 함수형을 가지고 스택플래임을 살펴보자. 

Stack Top     -------------------
                        0x80483d0                               main
                     ------------------- 
                        esi                                            argc
                     ------------------- 
                        ecx                                           argv 
                    ------------------- 
                        0x8048274                             _init
                     ------------------- 
                        0x8048420                             _fini
                     ------------------- 
                        edx                                         _rtlf_fini
                     ------------------- 
                        esp                                         stack_end
                     ------------------- 
                        eax                                         이것은 0이다
                     ------------------- 

이 스택플래임에 따르면, __libc_start_main()이 실행되기 전에 esi, ecx, edx, esp, eax 레지스터에 적당한 값이 채워져야 한다. 그러나 위에서 본 듯이 _start 어셈블리 명령어는 이 레지스터를 설정하지않는다. 그렇다면 누가 이 레지스터를 설정하나? 이제 한 군데가 남았다. 바로 커널이다. 
이제 세번째 질문으로 돌아가자.

Q3>커널은 무엇을 하는가?

쉘에 명령어를 입력하여 프로그램이 실행될때 리눅스에서는 다음과 같은 일이 일어난다.

  1. 쉘은 argc/argv를 가지고 커널 시스템호출 "evecve"를 부른다. 
  2. 커널 시스템호출 핸들러가 제어를 맏아 시스템호출을 처리하기 시작한다. 커널 코드에서 핸들러는 "sys_execve"이다. x86에서 사용자모드 프로그램은 아래 레지스터를 통해 필요한 파라미터를 커널에 넘긴다.
    • ebx : 프로그램명 문자열의 포인터
    • ecx : argv 배열 포인터
    • edx : 환경변수 배열 포인터.
     
  3. 일반적인 커널 시스템호출 핸들러 do_execve가 불린다. 이는 자료구조를 만들고 사용자영역에서 커널영역으로 자료를 복사한 후, 마지막으로 search_binary_handler()를 부른다. 리눅스는 a.out과 ELF와 같은 여러 실행파일 형식을 지원할 수 있다. 이를 위해 각각의 바이너리 형식을 읽어들일 수 있는 함수의 포인터를 담은 "struct linux_binfmt" 자료구조가 있다. search_binary_handler()는 적당한 핸들러를 찾아서 부른다. 이 경우 핸들러는 load_elf_binary()이다. 함수에 대해 자세히 설명하면 너무 길어지고 지루해지기 때문에 생략하겠다. 관심이 있다면 관련 서적을 참고하라. 그림은 천마디 말과 같고, 천줄의 소스코드는 (종종) 만마디 말과 같다. 함수의 요점만 설명하겠다. 함수는 먼저 파일 작업을 위해 커널 자료구조를 만들고 ELF 실행파일을 읽어들인다. 그런후 코드 크기, 자료 세그먼트 시작(data segment start), 스택 세그먼트 시작 (stack segment start) 등 커널 자료구조를 만든다. 그리고 이 프로세스에 대한 사용자모드 페이지를 할당하고, argv와 환경변수를 할당된 페이지 주소로 복사한다. 마지막으로 create_elf_tables()를 사용하여 argc, argv 포인터, 환경변수 배열 포인터를 사용자모드 스택에 푸쉬하고, start_thread()로 프로세스 실행을 시작한다. 


_start 어셈블리 명령어가 시작할때 스택플래임은 다음과 같다. 

Stack Top        -------------
                            argc
                        -------------
                            argv pointer
                        -------------
                            env pointer
                        ------------- 

그러면 어셈블리 명령어는 다음과 같이 스택에서 모든 정보를 얻을 수 있다.

pop %esi <--- argc를 얻는다
move %esp, %ecx <--- argv를 얻는다
실제로 argv 주소는 현재 스택포인터와 동일하다.
이제 실행을 시작할 준비가 끝났다.

다른 레지스터는 어떻게 설정되나?

esp는 프로그램의 스택 끝을 가리키는데 사용된다. 필요한 정보를 팝한(pop) 후, _start 함수는 스택포인터(esp) 레지스터에서 하위 4 비트를 끈다. 우리 프로그램에서 이것이 실제로 스택의 끝이므로 당연하다. edx는 일종의 프로그램 파괴자(destructor)인 rtld_fini에 사용된다. 커널은 다음 매크로로 이 레지스터를 0으로 만든다.

#define ELF_PLAT_INIT(_r) do { \
_r->ebx = 0; _r->ecx = 0; _r->edx = 0; \
_r->esi = 0; _r->edi = 0; _r->ebp = 0; \
_r->eax = 0; \
} while (0)
0은 x86 리눅스에서 그 기능을 사용하지 않음을 뜻한다.

어셈블리 명령어에 대해

이들 코드는 어디서 왔는가? 이 코드들은 GCC 코드의 일부다. 보통 이 코드에 대한 오브젝트파일을
/usr/lib/gcc-lib/i386-redhat-linux/XXX과 (XXX은 gcc 버전)
/usr/lib에서 찾을 수 있다.
파일명은 crtbegin.o,crtend.o, gcrt1.o이다.

 

요약

다음과 같은 일이 일어난다. 

  1. GCC는 crtbegin.o/crtend.o/gcrt1.o을 첨가하여 프로그램을 컴파일한다. 또, 기본적으로 다른 기본 라이브러리들도 동적으로 링크된다. 프로그램의 시작주소는 _start의 주소로 설정된다.
  2. 커널은 실행파일을 읽어들이고, text/data/bss/stack을 만든다. 특히 커널은 아규먼트와 환경변수를 위한 페이지를 할당하고 필요한 정보를 스택에 푸쉬한다.
  3. 이제 _start가 실행된다. _start는 스택에서 커널이 집어넣은 정보를 얻고, __libc_start_main을 위한 아규먼트 스택을 만든 후 이 함수를 부른다. 
  4. __libc_start_main은 필요한 것들을 (특별히 malloc같은 C 라이브러리와 쓰레드 환경) 초기화하고, 우리가 만든 main을 부른다. 
  5. main(argc, argv)로 우리가 만든 main을 부른다. 실제로 흥미로운 점은 main의 함수형이다. __libc_start_main은 main의 함수형이 main(int, char **, char **)라고 생각한다. 의심스러우면 다음 프로그램을 실행해봐라.
    main(int argc, char** argv, char** env)
    {
    int i = 0;
    while(env[i] != 0)
    {
    printf("%s\n", env[i++]);
    }
    return(0);
    }

결론

리눅스에서 우리가 만든 C main() 함수는 GCC, libc, 리눅스 바이너리 로더의 협력으로 실행된다.

참고자료

objdump                         "man objdump" 

ELF header                     /usr/include/elf.h 

__libc_start_main          glibc 소스 
                                       ./sysdeps/generic/libc-start.c 

sys_execve                     리눅스 커널 소스코드 
                                       arch/i386/kernel/process.c 

do_execve                      리눅스 커널 소스코드 
                                       fs/exec.c 

struct linux_binfmt       리눅스 커널 소스코드 
                                       include/linux/binfmts.h 

load_elf_binary             리눅스 커널 소스코드
                                       fs/binfmt_elf.c 

create_elf_tables           리눅스 커널 소스코드 
                                       fs/binfmt_elf.c 

start_thread                   리눅스 커널 소스코드 
                                      include/asm/processor.h