プログラムはどう動くのか? 〜 ELFの黒魔術をかいまみる

もう締切日なのにネタがない。というわけで簡単なプログラム "hello, world" がどのように起動され、どのように処理されて動くのかを無意味に詳しく解説してみよう。

#include <stdio.h>
int
main(int argc, char *argv[])
{
	printf("hello, world\n");
	exit(0);
}

この hello.c をコンパイルすると次のようなhelloというバイナリができる

% cc -g -o hello hello.c

この hello というバイナリは

% file hello
hello: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.0, dynamically linked (uses shared libs), not stripped

のように ELFというフォーマットのバイナリである。

dynamically linked なので ldd をつかうと

% ldd hello
        libc.so.6 => /lib/libc.so.6 (0x4001c000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

のようにどの shared libs を使っているのかを見ることができる。

より詳しくは objdump をつかうとELFバイナリの中身をみることができる。 たとえば -x (--all-headers) でこのバイナリの実行コードではない情報、つまりどのアーキテクチャむけのバイナリか、どのshared libsを必要としているか、どういったsymbolがなかで使われているか/使おうとしているかなどといった情報を見ることができる。

% objdump -x hello

hello:     ファイル形式 elf32-i386
hello
アーキテクチャ: i386, フラグ 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
開始アドレス 0x08048340

プログラムヘッダ:
    PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
         filesz 0x000000c0 memsz 0x000000c0 flags r-x
  INTERP off    0x000000f4 vaddr 0x080480f4 paddr 0x080480f4 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x000004b2 memsz 0x000004b2 flags r-x
    LOAD off    0x000004b4 vaddr 0x080494b4 paddr 0x080494b4 align 2**12
         filesz 0x00000110 memsz 0x00000128 flags rw-
 DYNAMIC off    0x000004c8 vaddr 0x080494c8 paddr 0x080494c8 align 2**2
         filesz 0x000000c8 memsz 0x000000c8 flags rw-
    NOTE off    0x00000108 vaddr 0x08048108 paddr 0x08048108 align 2**2
         filesz 0x00000020 memsz 0x00000020 flags r--
(以下略)

また -d (--disassemble) で逆アセンブルが、バイナリが-gオプションでビルドされていれば-S(--source) でソースコードとの対応も見ることができる。

% objdump -d hello

hello:     ファイル形式 elf32-i386

セクション .init の逆アセンブル:
 80482bc:       55                      push   %ebp
 80482bd:       89 e5                   mov    %esp,%ebp
 80482bf:       83 ec 08                sub    $0x8,%esp
 80482c2:       e8 9d 00 00 00          call   8048364 <call_gmon_start>
 80482c7:       90                      nop    
 80482c8:       e8 1b 01 00 00          call   80483e8 <frame_dummy>
 80482cd:       e8 7e 01 00 00          call   8048450 <__do_global_ctors_aux>
 80482d2:       c9                      leave  
 80482d3:       c3                      ret    
セクション .plt の逆アセンブル:
(以下略)

% objdump -S hello
hello:     ファイル形式 elf32-i386

セクション .init の逆アセンブル:

080482bc <_init>:
(略)
08048420 <main>:
#include <stdio.h>
int
main(int argc, char *argv[])
{
 8048420:       55                      push   %ebp
 8048421:       89 e5                   mov    %esp,%ebp
 8048423:       83 ec 08                sub    $0x8,%esp
        printf("hello, world\n");
 8048426:       83 c4 f4                add    $0xfffffff4,%esp
 8048429:       68 a4 84 04 08          push   $0x80484a4
 804842e:       e8 e1 fe ff ff          call   8048314 <_init+0x58>
 8048433:       83 c4 10                add    $0x10,%esp
        exit(0);
 8048436:       83 c4 f4                add    $0xfffffff4,%esp
 8048439:       6a 00                   push   $0x0
 804843b:       e8 e4 fe ff ff          call   8048324 <_init+0x68>
 8048440:       83 c4 10                add    $0x10,%esp
}
 8048443:       c9                      leave  
 8048444:       c3                      ret    
(以下略)

さて、これをshell(例えばbash)から起動する場合をおいかけてみよう。bashhelloプログラムの実行を指示すると、fork(2)してstdin,stdout,stderrなどのリダイレクトの処理などをしてから、exec(2)fork(2)されたbashからプログラム(この場合はhello)におきかわって実行が開始される。これを順をおってみていってみよう。

まず、bashexecute_cmd.cshell_execve()の中でexecve(2)が呼ばれる。このexecve(2)glibcsysdeps/unix/sysv/linux/execve.cであり、そこでの

 return INLINE_SYSCALL (execve, 3, file, argv, envp);

によりexecveというsystem callのよびだしが行なわれる。これはglibcsysdeps/unix/sysv/linux/i386/sysdep.h#defineされており以下のようなかんじのasmを実行することになる。

        movl <envp>,%edi
        movl <argv>,%ecx
        movl <file>,%edx
        movl $11, %eax			# execve
        int $0x80

このように %eaxにsystemcall番号、%edx%ecx,%ediあたりに引数がセットされて、int $0x80が実行される。int命令はソフトウェア割り込みで、この時点でkernel modeに移行する。kernelのブート時の初期化段階にarch/i386//kernel/traps.ctrap_init()

        set_system_gate(SYSCALL_VECTOR,&system_call); # SYSCALL_VECTOR = 0x80

と設定されているので、int $0x80が呼ばれるとsystem_callへやってくる。このsystem_callというのはarch/i386/kernel/entry.Sにある ENTRY(system_call)である。ここでは基本的にレジスタをスタックにつんで%eaxの内容に従ってsys_call_tableに設定されているアドレスをcallすることになる。

        call *SYMBOL_NAME(sys_call_table)(,%eax,4)

sys_call_tableもおなじくentry.Sにあり11番目がsys_execve である

ENTRY(sys_call_table)
	略
        .long SYMBOL_NAME(sys_unlink)           /* 10 */
        .long SYMBOL_NAME(sys_execve)
        .long SYMBOL_NAME(sys_chdir)
	略

sys_execvearch/i386/kernel/process.casmlinkage int sys_execve(struct pt_regs regs)である。

asmlinkage int sys_execve(struct pt_regs regs)
{
        int error;
        char * filename;

        filename = getname((char *) regs.ebx);
        error = PTR_ERR(filename);
        if (IS_ERR(filename))
                goto out;
        error = do_execve(filename, (char **) regs.ecx, (char **) regs.edx, &regs);
        if (error == 0)
                current->ptrace &= ~PT_DTRACE;
        putname(filename);
out:
        return error;
}

このように%ebxに設定されているファイル名をチェックしたあと、do_execve()を読んでいる。system callを読んだ時に%ecxにはargv%edxにはenvpを設定してあるのでここは

	   do_execve(filename, argv, envp, ...)

のようによびだしていることになる。ここまでがarch依存なコードでありあとは非依存なコードに移行する。do_execve()fs/exec.cにある。

do_execve()ではまずfilenameで指定されたファイルをファイルシステムからopen_exec()をつかって読みこんでいる。ここでパーミッションのチェックなども行なっている。openに成功すればstruct linux_binprmの情報を設定していく。struct linux_binprmにはargvenvp,uid,gidなどの情報が保持されている。そしてexecしようとしているファイルの最初のBINPRM_BUF_SIZE (128byte)を読みこむ。

search_binary_handler()でfile headerのmagic numberなどをチェックし、そのbinary formatをあつかうhandlerをstruct linux_binfmt *formatsのなかから探しだしている。ELFに関してはfs/binfmt_elf.celf_formatの中のload_elf_binary()でELFヘッダのチェックがおこなわれている。ここでmagic numberなど必要な情報のチェックにクリアすればELF headerをkernel_read()をつかって読みこみをする。

ここで elf_ppnt->p_type == PT_INTERP.interp sectionをみつけるとこの内容を読みとる。この内容は

% objdump -s -j .interp hello
hello:     ファイル形式 elf32-i386
セクション .interp の内容:
 80480f4 2f6c6962 2f6c642d 6c696e75 782e736f  /lib/ld-linux.so
 8048104 2e3200                               .2.

である。つまり elf_interpreter = "/lib/ld-linux.so.2" というのが読みこまれる。

ここで再び elf_interpreteropen_exec()して BINPRM_BUF_SIZE分だけよみこんでいる。symbolic linkをたどって、これは/lib/ld-2.3.1.soという shared objectを実行することになる。

% file /lib/ld-linux.so.2
/lib/ld-linux.so.2: symbolic link to ld-2.3.1.so
% file /lib/ld-2.3.1.so
/lib/ld-2.3.1.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), 
not stripped

flush_old_exec()で現在のprogramの情報を消し、新しいprogramのための情報にcurrentの情報をいれかえていく。memory mapが初期化されプログラムヘッダでLOADのところがmapされる。fileszよりmemszがおおきい場合はその分のmemoryがこのあたりで 0 に fillされるように設定される。

最後に elf_interpreter "/lib/ld-2.3.1.so"load_elf_interp()によってmemory上にmapされる。

% objdump -x /lib/ld-2.3.1.so

/lib/ld-2.3.1.so:     ファイル形式 elf32-i386
/lib/ld-2.3.1.so
アーキテクチャ: i386, フラグ 0x00000150:
HAS_SYMS, DYNAMIC, D_PAGED
開始アドレス 0x00000b50

プログラムヘッダ:
    LOAD off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**12
         filesz 0x00010e00 memsz 0x00010e00 flags r-x
    LOAD off    0x00011000 vaddr 0x00011000 paddr 0x00011000 align 2**12
         filesz 0x00000554 memsz 0x00000700 flags rw-
 DYNAMIC off    0x000113f8 vaddr 0x000113f8 paddr 0x000113f8 align 2**2
         filesz 0x000000b0 memsz 0x000000b0 flags rw-

(略)

その後、currentの情報をさらに設定していって

	start_thread(regs, elf_entry, bprm->p);
elf_entry から実行を開始する。elf_entry/lib/ld-2.3.1.soentry(開始アドレス)からとなる。start_threadinclude/asm-i386/processor.h で次のように #define されている。

#define start_thread(regs, new_eip, new_esp) do {               \
        __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0));       \
        set_fs(USER_DS);                                        \
        regs->xds = __USER_DS;                                  \
        regs->xes = __USER_DS;                                  \
        regs->xss = __USER_DS;                                  \
        regs->xcs = __USER_CS;                                  \
        regs->eip = new_eip;                                    \
        regs->esp = new_esp;                                    \
} while (0)

つまり elf_entry を次の %eip として設定している。これで search_binary_handler()からreturnしてdo_execve()がおわり、sys_execve()からreturnする。

で、arch/i386/kernel/entry.S

        call *SYMBOL_NAME(sys_call_table)(,%eax,4)

から戻ってきて以下が実行される。

        movl %eax,EAX(%esp)             # save the return value
ENTRY(ret_from_sys_call)
        cli                             # need_resched and signals atomic test
        cmpl $0,need_resched(%ebx)
        jne reschedule
        cmpl $0,sigpending(%ebx)
        jne signal_return
restore_all:
        RESTORE_ALL

rescheduleがよびだされて、kernel/sched.casmlinkage void schedule(void)のなかから include/asm-i386/system.hswitch_to()からarch/i386/kernel/process.c__switch_to()にきて %esp, %fs, %gs がいれかえられる。

そうしてexecされたプロセスに制御がまわってくると、このprocessの%eip、つまりelf_interpreterのentry addressから実行を開始することになる。

% objdump -f /lib/ld-2.3.1.so 
/lib/ld-2.3.1.so:     ファイル形式 elf32-i386
アーキテクチャ: i386, フラグ 0x00000150:
HAS_SYMS, DYNAMIC, D_PAGED
開始アドレス 0x00000b50

% objdump -d /lib/ld-2.3.1.so
(略)
00000b50 <_start>:
     b50:       89 e0                   mov    %esp,%eax
     b52:       e8 03 01 00 00          call   c5a <_dl_start>

ということで /lib/ld-2.3.1.so_start から実行を開始する。これはglibcsysdeps/i386/dl-machine.h#define RTLD_STARTのコードである。

まず_dl_start()がよばれる。この_dl_start()glibcelf/rtld.c_dl_start()である。まずbootstrap_map.l_infoを0で初期化している。次にsysdeps/i386/dl_machine.hに定義されているelf_machine_load_address()をつかってdynamic linker自体がどのアドレスにあるかを調べている。

sysdeps/generic/dl-sysdep.c_dl_sysdep_start()からelf/rtld.cdl_main()がよばれてくる。まずここでprocess_envvars()がよばれ、環境変数のチェックがおこなわれている。このコードをみると以下の環境変数が /lib/ld-2.3.1.soでつかわれていることがわかる。

 LD_WARN - Warning level, verbose or not
 LD_DEBUG - Debugging of the dynamic linker?
 LD_VERBOSE - Print information about versions.
 LD_PRELOAD - List of objects to be preloaded. 
 LD_PROFILE - Which shared object shall be profiled.
 LD_BIND_NOW - Do we bind early?
 LD_BIND_NOT - ?
 LD_SHOW_AUXV - Test whether we want to see the content of the auxiliary
             array passed up from the kernel.
 LD_HWCAP_MASK - Mask for the important hardware capabilities.
 LD_ORIGIN_PATH - Path where the binary is found. 
 LD_LIBRARY_PATH - The library search path.
 LD_DEBUG_OUTPUT - Where to place the profiling data file.
 LD_DYNAMIC_WEAK - ?
 LD_PROFILE_OUTPUT - Where to place the profiling data file. 
 LD_TRACE_PRELINKING - The mode of the dynamic linker can be set.
 LD_TRACE_LOADED_OBJECTS - The mode of the dynamic linker can be set.

LD_PRELOADLD_LIBRARY_PATHはよく使われるが、他はあまりつかったことがないことだろう。とりあえずいくつかを簡単に紹介しよう。

% LD_DEBUG=help /lib/ld-2.3.1.so
Valid options for the LD_DEBUG environment variable are:

  libs        display library search paths
  reloc       display relocation processing
  files       display progress for input file
  symbols     display symbol table processing
  bindings    display information about symbol binding
  versions    display version dependencies
  all         all previous options combined
  statistics  display relocation statistics
  help        display this help message and exit

To direct the debugging output into a file instead of standard output
a filename can be specified using the LD_DEBUG_OUTPUT environment variable.

たとえば 以下のような結果を得ることができる。

% LD_DEBUG=libs ./hello
04836:  find library=libc.so.6; searching
04836:   search cache=/etc/ld.so.cache
04836:    trying file=/lib/libc.so.6
04836:  
04836:  
04836:  calling init: /lib/libc.so.6
04836:  
04836:  
04836:  initialize program: ./hello
04836:  
04836:  
04836:  transferring control: ./hello
04836:  
hello, world
04836:  
04836:  calling fini: /lib/libc.so.6
04836:  
% LD_DEBUG=reloc ./hello
04837:  
04837:  relocation processing: /lib/libc.so.6 (lazy)
04837:  
04837:  relocation processing: ./hello (lazy)
04837:  
04837:  relocation processing: /lib/ld-linux.so.2
04837:  
04837:  calling init: /lib/libc.so.6
04837:  
04837:  
04837:  initialize program: ./hello
04837:  
04837:  
04837:  transferring control: ./hello
04837:  
hello, world
04837:  
04837:  calling fini: /lib/libc.so.6
04837:  
% LD_DEBUG=files ./hello
04839:  
04839:  file=libc.so.6;  needed by ./hello
04839:  file=libc.so.6;  generating link map
04839:    dynamic: 0x40129618  base: 0x4001c000   size: 0x00112184
04839:      entry: 0x40031a40  phdr: 0x4001c034  phnum:          7
04839:  
04839:  
04839:  calling init: /lib/libc.so.6
04839:  
04839:  
04839:  initialize program: ./hello
04839:  
04839:  
04839:  transferring control: ./hello
04839:  
hello, world
04839:  
04839:  calling fini: /lib/libc.so.6
04839:  
% LD_DEBUG=symbols ./hello
04845:  symbol=_IO_file_close;  lookup in file=./hello
04845:  symbol=_IO_file_close;  lookup in file=/lib/libc.so.6
04845:  symbol=_IO_2_1_stdin_;  lookup in file=./hello
04845:  symbol=_IO_2_1_stdin_;  lookup in file=/lib/libc.so.6
04845:  symbol=_IO_2_1_stdout_;  lookup in file=./hello
04845:  symbol=_IO_2_1_stdout_;  lookup in file=/lib/libc.so.6
04845:  symbol=_IO_2_1_stderr_;  lookup in file=./hello
04845:  symbol=_IO_2_1_stderr_;  lookup in file=/lib/libc.so.6
(略)
% LD_DEBUG=bindings ./hello 
4849:  binding file /lib/libc.so.6 to /lib/libc.so.6: normal symbol `_IO_file_close' [GLIBC_2.0]
04849:  binding file /lib/libc.so.6 to /lib/libc.so.6: normal symbol `_IO_2_1_stdin_' [GLIBC_2.1]
04849:  binding file /lib/libc.so.6 to /lib/libc.so.6: normal symbol `_IO_2_1_stdout_' [GLIBC_2.1]
04849:  binding file /lib/libc.so.6 to /lib/libc.so.6: normal symbol `_IO_2_1_stderr_' [GLIBC_2.1]
(略)
% LD_DEBUG=versions ./hello
04852:  checking for version `GLIBC_2.0' in file /lib/libc.so.6 required by file ./hello
04852:  checking for version `GLIBC_2.1' in file /lib/ld-linux.so.2 required by file /lib/libc.so.6
04852:  checking for version `GLIBC_2.0' in file /lib/ld-linux.so.2 required by file /lib/libc.so.6
04852:  checking for version `GLIBC_PRIVATE' in file /lib/ld-linux.so.2 required by file /lib/libc.so.6
04852:  
(略)
% LD_DEBUG=statistics ./hello
04854:                   number of relocations: 119
04854:        number of relocations from cache: 5
hello, world
04854:  
04854:  runtime linker statistics:
04854:             final number of relocations: 126
04854:  final number of relocations from cache: 5

LD_DEBUG=all とすればすべてが出力される。またLD_DEBUG=helpで書いてある通り、LD_DEBUG_OUTPUTも指定すればそのファイルに出力される。

LD_SHOW_AUXVを使うとおもしろい情報をみることができる。

% LD_SHOW_AUXV=1 /lib/ld-2.3.1.so ./hello
AT_HWCAP:    fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat 
 pse36 mmx fxsr sse
AT_PAGESZ:      4096
AT_CLKTCK:      100
AT_PHDR:        0x80000034
AT_PHENT:       32
AT_PHNUM:       3
AT_BASE:        0x0
AT_FLAGS:       0x0
AT_ENTRY:       0x80000b50
AT_UID:         1000
AT_EUID:        1000
AT_GID:         1000
AT_EGID:        1000
AT_PLATFORM:    i686
hello, world

これは sysdeps/generic/dl-sysdep.c_dl_show_auxv()で処理している。AT_HWCAPに関しては sysdeps/unix/sysv/linux/i386/dl-procinfo.hdl_procinfo()で処理している。どの bit がどの文字列になるかは sysdeps/unix/sysv/linux/i386/dl-procinfo.c_dl_x86_cap_flags[][]である。前から順に 1, 2, 4, ... に対応している。

LD_TRACE_PRELINKINGでどうリンクされるかを確認できる

% LD_TRACE_PRELINKING=1 ./hello
        ./hello => ./hello (0x08048000, 0x00000000)
        libc.so.6 => /lib/libc.so.6 (0x4001c000, 0x4001c000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000, 0x40000000)
lookup 0x4001c000 0x000053a0 -> 0x4001c000 0x00064dfc /0 _IO_file_close
lookup 0x4001c000 0x00005530 -> 0x4001c000 0x0010a1e0 /0 _IO_2_1_stdin_
lookup 0x4001c000 0x00004a50 -> 0x4001c000 0x0010a340 /0 _IO_2_1_stdout_
lookup 0x4001c000 0x0000b7e0 -> 0x4001c000 0x0010a4a0 /0 _IO_2_1_stderr_

LD_TRACE_LOADED_OBJECTSはどのshared libsをloadするかを見ることができる。つまり ldd を同じような動作をする。

% LD_TRACE_LOADED_OBJECTS=1 ./hello
        libc.so.6 => /lib/libc.so.6 (0x4001c000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

process_envvars()の処理の後は /lib/ld-2.3.1.soを直接呼びだした時の処理がされている。

% /lib/ld-2.3.1.so
Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]
You have invoked `ld.so', the helper program for shared library executables.
This program usually lives in the file `/lib/ld.so', and special directives
in executable files using ELF shared libraries tell the system's program
loader to load the helper program from this file.  This helper program loads
the shared libraries needed by the program executable, prepares the program
to run, and runs it.  You may invoke this helper program directly from the
command line to load and run an ELF executable file; this is like executing
that file itself, but always uses this helper program from the file you
specified, instead of the helper program file specified in the executable
file you run.  This is mostly of use for maintainers to test new versions
of this helper program; chances are you did not intend to run this program.

  --list                list all dependencies and how they are resolved
  --verify              verify that given object really is a dynamically linked
                        object we can handle
  --library-path PATH   use given PATH instead of content of the environment
                        variable LD_LIBRARY_PATH
  --inhibit-rpath LIST  ignore RUNPATH and RPATH information in object names
                        in LIST

たとえば --list オプションは ldd のような動作をする。

% /lib/ld-2.3.1.so --list ./hello
        libc.so.6 => /lib/libc.so.6 (0x4000a000)
        /lib/ld-linux.so.2 => /lib/ld-2.3.1.so (0x80000000)

もしくは次のようにすると普通に実行することもできる。

% /lib/ld-2.3.1.so ./hello
hello, world

preloadlist != NULL のチェックをして LD_PRELOADの処理をしている。その次に/etc/ld.so.preloadを見ておなじくpreloadの処理をする。

preloadのを処理をしてからDT_NEEDEDとされているshared libsの処理をおこなう。objdump -pDynamic Secion (動的セクション)で NEEDED になっている shared libs である。

% objdump -p ./hello
(略)

動的セクション:
  NEEDED      libc.so.6
  INIT        0x80482bc
  FINI        0x804847c
(略)

つまりこの場合は libc.so.6 をリンクする。

最終的に dl_mainuser_entry にプログラムの開始アドレスが設定され、それが _dl_sysdep_start()のreturn valueとなって start_addr になり

  start_addr =  _dl_sysdep_start (arg, &dl_main);

これが _dl_start_final()の return value となる。

  ElfW(Addr) entry = _dl_start_final (arg, &info);

この後

  ELF_DYNAMIC_RELOCATE (&bootstrap_map, 0, 0);

_GLOBAL_OFFSET_TABLE_に対して設定をしている。これはelf/dynamic-link.h#defineされており、この中でsysdeps/i386/dl-machine.helf_machine_runtime_setup()がそれをしている。これについては後に説明する。

さて entry はその後

    return ELF_MACHINE_START_ADDRESS (GL(dl_loaded), entry);

でさらに _dl_start()のreturn valueとなって sysdeps/i386/dl-machine.hRTLD_STARTのコードに続き、_dl_start_user()にくる。GOTなどを設定し、最終的に

        call _dl_start\n\
_dl_start_user:\n\
        # Save the user entry point address in %edi.\n\
        movl %eax, %edi\n\
	(略)
        # Jump to the user's entry point.\n\
        jmp *%edi\n\

_dl_start()からのreturn valueのアドレスへjumpしユーザのプログラムの実行が開始される。

hello:     ファイル形式 elf32-i386
hello
アーキテクチャ: i386, フラグ 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
開始アドレス 0x08048340

ELFヘッダからわかる通り、開始アドレスが 0x08048340 なので

08048340 g     F .text  00000000              _start

そのアドレスに配置されている_startから実行が開始される。

08048340 <_start>:
 8048340:       31 ed                   xor    %ebp,%ebp
 8048342:       5e                      pop    %esi
 8048343:       89 e1                   mov    %esp,%ecx
 8048345:       83 e4 f0                and    $0xfffffff0,%esp
 8048348:       50                      push   %eax
 8048349:       54                      push   %esp
 804834a:       52                      push   %edx
 804834b:       68 7c 84 04 08          push   $0x804847c
 8048350:       68 bc 82 04 08          push   $0x80482bc
 8048355:       51                      push   %ecx
 8048356:       56                      push   %esi
 8048357:       68 20 84 04 08          push   $0x8048420
 804835c:       e8 a3 ff ff ff          call   8048304 <_init+0x48>
 8048361:       f4                      hlt    
 8048362:       90                      nop    
 8048363:       90                      nop  

stackに引数をpushしていって

 804835c:       e8 a3 ff ff ff          call   8048304 <_init+0x48>

で次のコードをcallする

 8048304:       ff 25 b4 95 04 08       jmp    *0x80495b4

ここで、0x80485a0〜が _GLOBAL_OFFSET_TABLE_ である。

080495a0 <_GLOBAL_OFFSET_TABLE_>:

これを参照してみると 080495b4 には 0a 83 04 08 というバイト列がはいっているので、その32bit little endianの値である 0804830a に jmp することになる

 804830a:       68 10 00 00 00          push   $0x10
 804830f:       e9 c0 ff ff ff          jmp    80482d4 <_init+0x18>

ちなみにこのあたりは gdb をつかってみると

(gdb) disassemble 0x804830a
Dump of assembler code for function __libc_start_main:
0x8048304 <__libc_start_main>:  jmp    *0x80495b4
0x804830a <__libc_start_main+6>:        push   $0x10
0x804830f <__libc_start_main+11>:       jmp    0x80482d4 <_init+24>
End of assembler dump.

__libc_start_main である。ちなみにこのあたりはセクション.pltで、ここを disassemble してみると

0x80482e4 <__register_frame_info>:      jmp    *0x80495ac
0x80482ea <__register_frame_info+6>:    push   $0x0
0x80482ef <__register_frame_info+11>:   jmp    0x80482d4 <_init+24>
0x80482f4 <__deregister_frame_info>:    jmp    *0x80495b0
0x80482fa <__deregister_frame_info+6>:  push   $0x8
0x80482ff <__deregister_frame_info+11>: jmp    0x80482d4 <_init+24>
0x8048304 <__libc_start_main>:  jmp    *0x80495b4
0x804830a <__libc_start_main+6>:        push   $0x10
0x804830f <__libc_start_main+11>:       jmp    0x80482d4 <_init+24>
0x8048314 <printf>:     jmp    *0x80495b8
0x804831a <printf+6>:   push   $0x18
0x804831f <printf+11>:  jmp    0x80482d4 <_init+24>
0x8048324 <exit>:       jmp    *0x80495bc
0x804832a <exit+6>:     push   $0x20
0x804832f <exit+11>:    jmp    0x80482d4 <_init+24>

のようになっている。ここはどうなっているかを簡単に説明しよう。

まず_GLOBAL_OFFSET_TABLE_ (0x80495a0〜)にsymbolがresolvされたアドレスがはいることになっている。0x80495b4(_GLOBAL_OFFSET_TABLE_の中の20バイト名、つまり_GLOBAL_OFFSET_TABLE_[8]に相当)に格納されるアドレスがさしているのが __libc_start_mainの実体となる。ただし最初はまだマップされていないので、その場合は 0x804830a をさしている。ここにくると push $0x10 して jmp 0x80482d4 である。0x80482d4ではなにをしているかというと

 80482d4:       ff 35 a4 95 04 08       pushl  0x80495a4
 80482da:       ff 25 a8 95 04 08       jmp    *0x80495a8

である。ここで 0x80495a8 は _GLOBAL_OFFSET_TABLE_ が 080495a0 なので _GLOBAL_OFFSET_TABLE_[2] に相当している。この_GLOBAL_OFFSET_TABLE_には実はsysdeps/i386/dl-machine.helf_machine_runtime_setup()により

  got[1] = (Elf32_Addr) l;  /* Identify this shared object.  */
  got[2] = (Elf32_Addr) &_dl_runtime_resolve;

のような値が代入される。つまりjmp *0x80495a8_dl_runtime_resolve()を呼ぶことになる。_dl_runtime_resolvsysdeps/i386/dl-machine.h にあり elf/dl-runtime.cfixup()を呼ぶ。fixup()がshared libsとのsymbolのresolveをおこなっていて、この場合は __libc_start_main()libc.so.6 の中のルーチンを差すようにアドレスを_GLOBAL_OFFSET_TABLE_のエントリ(つまり_GLOBAL_OFFSET_TABLE_[8])に登録する。そうすることでまた同じルーチンを呼ぶときは

  0x8048304 <__libc_start_main>:  jmp    *0x80495b4

というjumpをするわけであるが、一度fixup()を実行すると、0x80495b4(つまり_GLOBAL_OFFSET_TABLE_[8])に書かれているアドレスが本当のルーチンへのアドレスになっているのでresolveする必要もなくそこにjmpするようになる。__libc_start_main()の実体はlibc.so.6で これはsysdeps/generic/libc-start.cの中にある。そしていろいろ処理をした後、最終的に

      result = main (argc, argv, __environ);

main()を呼ぶことになる。これでようやく

int
main(int argc, char *argv[])
{
	printf("hello, world\n");
	exit(0);
}

まできたことになる。あとはこれを実行である。もちろん printfexit などは__libc_start_main()で説明したようなfixupがおこなわれてlibc.so.6とlinkされてそれらが実行されるという処理がおこなわれる。最後に

  exit (result);

exit処理をする。exit()もsystem callであるからexecve()の時のようにglibcsysdeps/unix/sysv/linux/_exit.c

  INLINE_SYSCALL (exit, 1, status);

により system call 1 (__NR_exit) がよびだされ

ENTRY(sys_call_table)
        .long SYMBOL_NAME(sys_ni_syscall)       /* 0  -  old "setup()" system ca
ll*/
        .long SYMBOL_NAME(sys_exit)

から kernel/exit.casmlinkage long sys_exit(int error_code)がよびだされることになる。この関数を実行していくと、processがもっていたresourceの解放をしてreschedule()して他のプロセスに処理をわたしておわりである。あとは親プロセスが waittsk->exit_code にのこっている終了コードをひろってもらえるまで zombie 状態となっているのである。これでプロセスの一生がおわりである。この原稿もここでおしまい。

おまけ:

yaegashi先生からreadelf(1)は使わないのか という指摘があったけれども、もう遅いのでかきなおすのはやめておきます。readelf(1)もつかっていろいろ遊んでみるのは宿題にしておきましょう:-)

これは「でぶあん2002年冬号」の原稿を若干修正してHTML化したものです。(2005/11/03)


Copyright © 2002,2005 Fumitoshi UKAI