プログラムがmain()にたどりつくまで

この話は、最近読んでいる

Binary Hacks ―ハッカー秘伝のテクニック100選

Binary Hacks ―ハッカー秘伝のテクニック100選

の中にある話です。自分の手元の環境でソースコードを読みながら理解したことをメモして置きます。

環境について

Fedora 17
gcc バージョン 4.7.2 20120921 (Red Hat 4.7.2-2) (GCC)
GNU bash, バージョン 4.2.39(1)-release (i686-redhat-linux-gnu)
です。

対象ソース

これが今回の対象です。いわゆるhello World!です。

[foo@localhost tmp]$ cat hello.c 
#include <stdio.h>
int main(void) {
    printf("Hello World!");
    return 0;
}

最初にしたこと

straceコマンドにより、システムコールを調べた。

[foo@localhost tmp]$ strace ./hello 
execve("./hello", ["./hello"], [/* 45 vars */]) = 0
brk(0)                                  = 0x94ec000
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7783000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=95319, ...}) = 0
mmap2(NULL, 95319, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb776b000
close(3)                                = 0
open("/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\3\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\2207\355F4\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=2011688, ...}) = 0
mmap2(0x46eba000, 1776316, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x46eba000
mprotect(0x47065000, 4096, PROT_NONE)   = 0
mmap2(0x47066000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1ab) = 0x47066000
mmap2(0x47069000, 10940, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x47069000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb776a000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb776a6c0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0x47066000, 8192, PROT_READ)   = 0
mprotect(0x46eb6000, 4096, PROT_READ)   = 0
munmap(0xb776b000, 95319)               = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7782000
write(1, "Hello World!", 12Hello World!)            = 12
exit_group(0)                           = ?
+++ exited with 0 +++

本を読むとbashの場合は、bash_4.2/execute.cmdのshell_execveからexecve(command, args, env);呼ばれている。ここのパラメータは、実行ファイル名, mainのargs, mainの環境変数がそのまま渡ってくるはずです。
execve関数は、glibc/sysdeps/unix/sysv/linux/execv.cで定義されているとあるが、実際にどうか調べてみる。
ファイルの最後にweak_alias(__execve, execve)とあり、
__execve(file, argv, envp)の定義があるのでおそらく、__execveに読み替えられると推測した。
__execveの最後でINLINE_SYSCALL (execve, 3, CHECK_STRING (file), ubp_argv, ubp_envp);としている。

glibc/sysdeps/unix/sysv/linux/i386/sysdep.hの中に定義がある。
>||
#undef INLINE_SYSCALL
#define INLINE_SYSCALL(name, nr, args...) \
  ({                                                                          \
    unsigned int resultvar = INTERNAL_SYSCALL (name, , nr, args);             \
    if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (resultvar, ), 0))         \
      {                                                                       \
        __set_errno (INTERNAL_SYSCALL_ERRNO (resultvar, ));                   \
        resultvar = 0xffffffff;                                               \
      }                                                                       \
    (int) resultvar; })

INTERNAL_SYSCALLの定義も同じファイルにあり、下記のようになっている。いくつも定義があったが、
おそらくソフトウェア割り込み(int $80)をしているものだと思い下記定義を参照した。

# define INTERNAL_SYSCALL(name, err, nr, args...) \
  ({                                                                          \
    register unsigned int resultvar;                                          \
    EXTRAVAR_##nr                                                             \
    asm volatile (                                                            \
    LOADARGS_##nr                                                             \
    "movl %1, %%eax\n\t"                                                      \
    "int $0x80\n\t"                                                           \
    RESTOREARGS_##nr                                                          \
    : "=a" (resultvar)                                                       \
    : "i" (__NR_##name) ASMFMT_##nr(args) : "memory", "cc");                  \
    (int) resultvar; })

nameは、execveだったので、__NR_##nameは、__NR_execveに展開されます。
__NR_execveの値は、/usr/include/asm/unistd.hより11なので、eaxレジスタに11をセットして
ソフトウェア割り込みを発生しています。ここまでユーザ空間での動作です。

カーネル空間へ

SYSCALL_VECTOR(0x80)には、カーネル初期化時にlinux-3.6.9-2.fc17.i686/arch/x86/kernel/traps.cにある
__init trap_init()の中からset_system_trap_gate(SYSCALL_VECTOR, &system_call);を読んで初期化されています。
int $0x80で発生した割り込みで、system_callが呼ばれます。
実装は、linux-3.6.9-2.fc17.i686/arch/x86/kernel/entry_32.Sの中にある。

ENTRY(system_call)
        RING0_INT_FRAME                 # can't unwind into user space anyway
        pushl_cfi %eax                  # save orig_eax
(省略)
syscall_call:
        call *sys_call_table(,%eax,4)
        movl %eax,PT_EAX(%esp)          # store the return value
syscall_exit:
        LOCKDEP_SYS_EXIT
        DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
                                        # setting need_resched or sigpending
                                        # between sampling and the iret
        TRACE_IRQS_OFF

grepしてそれらしいところも見つけました。こんな書き方あるのかい??
このファイルは,linux-3.6.9-2.fc17.i686/arch/x86/kernel/syscall_32.cです。

const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { 
    /*
     * Smells like a compiler bug -- it doesn't work
     * when the & below is removed.
     */
    [0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_32.h>
};

カーネルをビルドすると
linux-3.6.9-2.fc17.i686/arch/x86/include/generated/asm/syscalls_32.hができてその中は、
システムコール一覧がかかれている。(※ビルドしないとできない。)

syscalls_32.hがどうやって生成されるか調べる

linux-3.6.9-2.fc17.i686/arch/x86/syscalls/Makefileを見ると、
先頭にgenerated/asmと記載がある。

out := $(obj)/../include/generated/asm

このMakefileの中に規則がありそうなのでもう少し詳しくみていくと。

syscall32 := $(srctree)/$(src)/syscall_32.tbl
syscall64 := $(srctree)/$(src)/syscall_64.tbl

syshdr := $(srctree)/$(src)/syscallhdr.sh
systbl := $(srctree)/$(src)/syscalltbl.sh

とあり、syscall_32.tblというファイルが怪しそう。
さらに、$(out)/syscalls_32.hのtargetがあるので、ここですね。

$(out)/syscalls_32.h: $(syscall32) $(systbl)
    $(call if_changed,systbl)

syscalltbl.shはwhile read nr abi name entry compactとしていて、ここの引数の数がsyscall_32.tblのフォーマットと同じであり、さらに結合すると__SYSCALL_I386となるため、ここでできる。
以上でシステムコールまわりの生成処理は、終わり。11は、__SYSCALL_I386(11, ptregs_execve, stub32_execve)なっている。

あとは、本のとおりソースを眺めた。ほとんど分からなかった。