プログラムが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)なっている。

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

bashとglibcのソース

オープンソースソースコードが誰にでも無料で公開されています. linuxやそのツール郡は, すべてオープンソースです. 普段お世話になっている基本的なソフトウエアは、GNUプロジェクトで公開されています。

http://www.gnu.org/software/software.html


bashのソースとglibcのソースをダウンロードしました。
mainの呼ばれる仕組みを勉強します。

Linux版のjavaで文字が□になる問題

linux版のastahのzip版をダウンロードして、さっそく起動したら、文字が□になる問題が発生しました。
世にいう豆腐問題です。ずっと前にも見たことがありましたが、どうやって解決したか忘れました。
とりあえず、いろいろ調べてみる。

利用しているJavaを調べる

[foo@localhost astah_community]$ which java
/usr/bin/java
[foo@localhost astah_community]$ ls -al /usr/bin/java
lrwxrwxrwx. 1 root root 22 11月 15 06:34 /usr/bin/java -> /etc/alternatives/java
[foo@localhost astah_community]$ alternatives --display java
java -ステータスは自動です。
リンクは現在 /usr/lib/jvm/jre-1.7.0-openjdk/bin/java を指しています。
/usr/lib/jvm/jre-1.7.0-openjdk/bin/java - 優先項目 17009
[foo@localhost astah_community]$ 

これで、/usr/lib/jvm/jre-1.7.0-openjdk/bin/javaが利用されていることが分かりました。

Javaで利用してフォントを調べる

/usr/lib/jvm/jre-1.7.0-openjdk/lib/fontconfig.Fedora.properties.srcを調べる。
何か見るとdialog.bold.japanese-x0208=Sazanami GothicとかなっていてSazanamiってフォントを使うようになっているぽい。
さらに下を見るとfilename.Sazanami_Gothic=/usr/share/fonts/sazanami-fonts-gothic/sazanami-gothic.ttfとなっている。
あるのかどうか調べると

[foo@localhost astah_community]# ls -al /usr/share/fonts/sazanami-fonts-gothic
ls: /usr/share/fonts/sazanami-fonts-gothic にアクセスできません: そのようなファイルやディレクトリはありません
[foo@localhost astah_community]# 

となりそのフォントがない。原因は、このフォントがないためですね。
解決方法は、2つです。

  1. sazanamiフォントを入れる。
  2. 別のフォントを参照するように変更する。

解決方法

今回は、2を選択します。
上のファイルを編集して参照先をかえてもいいですが、面倒なのでgoogleで検索するとjreでは、jre/lib/fonts/fallbackを作成して
そこにフォントを入れるらしい。
ディレクトリを作成

mkdir -p /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/fonts/fallback

フォントを作成(VL-PGothicを使うようにした)

[foo@localhost astah_community]# ls -al /usr/lib/jvm/jre-1.7.0-openjdk/lib/fonts/fallback/
合計 8
drwxr-xr-x. 2 root root 4096 12月  8 21:36 .
drwxr-xr-x. 3 root root 4096 12月  8 21:35 ..
lrwxrwxrwx. 1 root root   47 12月  8 21:36 VL-Gothic-Regular.ttf -> /usr/share/fonts/vlgothic/VL-Gothic-Regular.ttf
lrwxrwxrwx. 1 root root   48 12月  8 21:36 VL-PGothic-Regular.ttf -> /usr/share/fonts/vlgothic/VL-PGothic-Regular.ttf

このあと起動したら、とりあえず、メニューとかは表示された。多分、これで直ったはず。

非同期シグナルを同期的に扱う

sigwaitを利用して、シグナル受信スレッドを作成してしまおうというのがアイディア。

#include <pthread.h>
#include <sys/time.h>
#include <sys/select.h>
#include <signal.h>
#include <stdio.h>

void *wait_for_sighup(void *dmy) {
    int sig;

    //sig_wait関数はSIGHUPを待つ
    sigset_t ss; 
    sigemptyset(&ss);
    sigaddset(&ss, SIGHUP);
    while(1) {
        //SIGHUPを待つ
        if(sigwait(&ss, &sig) == 0) {
            switch(sig) {
                case SIGHUP:
                    printf("Hello async-signal-safe world!\n");
                    break;
            }
        }
    }   
    return NULL;
}
int main(void) {
    sigset_t ss;
    pthread_t pt;
    pthread_attr_t atr;

    //SIGHUPをマスクする(SIGHUPが起こされても保留状態にする)
    sigemptyset(&ss);
    sigaddset(&ss, SIGHUP);
    sigprocmask(SIG_BLOCK, &ss, NULL);

    //SIGHUPを処理する専用スレッドを生成
    pthread_attr_init(&atr);
    pthread_attr_setdetachstate(&atr, PTHREAD_CREATE_DETACHED);
    pthread_create(&pt, &atr, wait_for_sighup, NULL);
    while(1);

    return 0;
}


■参考文献
Binary Hacks――ハッカー秘伝のテクニック100選

Javaでシグナルをキャッチする

linuxではお馴染み、シグナルをjavaでキャッチする方法です。
SIGHUPをキャッチします。

vim sample.java

import sun.misc.Signal;
import sun.misc.SignalHandler;

class SignalTest {
        static int sig = 0;
        public static void main(String[] args) {
                Signal signal = new Signal("HUP");
                Signal.handle(signal, new SignalHandler() {
                        public void handle(Signal sigInst) {
                                sig = 1;
                        }
                });
                while(sig == 0); 
        }
}                                                                                       

これをコンパイルして起動して別端末から、kill -HUP SinalTestのPID
をしてもプロセスがしなかった。


原因は、whileループで参照しているsigが最適化されて毎回参照していないためだ。
C言語でも最適化によって、この問題がおこるので、sig変数のみ最適化しないようにするための宣言があるそれが、
volatileです。javaでもあったので、使ってみた。

上のソースをsig変数をvolatile宣言する。こんな感じ。

volatile static int sig = 0;

これをコンパイルして起動して別端末から、kill -HUP SinalTestのPID
をするとちゃんと無限ループを抜ける。

LinuxでJava開発

linuxJavaの開発をするためにjavacをインストールしました。
インストール方法のメモです。
rootで下記をインストール

yum install java-1.7.0-openjdk-devel

テストです。
vim sampleTest.java

class Sample {
    public static void main(String[] args) {
    }
}

クラス作成

javac sampleTest.java

実行

java Sample

Canvasを使う

本屋などにいくとhtml5でつくるゲームとかの本が結構あるので、少し触ってみた。ほとんど詳しくないので基礎的なことしかできなかった。とりあえず、キャンバスの背景設定と図形を書いて、定期的に移動するようにして見た。javascriptはまだ全然分からない。

index.hmtl

<!DOCTYPE html>
<html lang="ja" dir="ltr">
	<head>
		<title></title>
		<meta charset="UTF-8"/>
		<script src="./main.js"></script>
	</head>
	<body onload='main();'>
		<canvas id="canvas1" width="400" height="400">
		</canvas>
		<div>
			<p>キャンバス上でクリックでスタート/ストップ</p>
		</div>
	</body>
</html>

main.js

/**
 * 2012/12/05
 * Canvasサンプル
 *
 * */

var canvas, ctx;
var ball
var v
var isStopFlg;

Ball = function(x, y, r) {
        this.x = x;
        this.y = y;
        this.r = r;
}

Velocity = function(x, y) {
        this.x = x;
        this.y = y;
}

function update() {
        ball.x += v.x;
        if( ball.x - ball.r < 0 || ball.x + ball.r > canvas.width ) {
                /* 次回の速度ベクトルを設定 */
                v.x = -v.x;
      }

        ball.y += v.y;
        if( ball.y - ball.r < 0 || ball.y + ball.r > canvas.height ) {
                /* 次回の速度ベクトルを設定 */
                v.y = -v.y;
        }
}

function draw() {
        /* 背景の描画 */
        ctx.fillStyle="#000000";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        /* ボールの描画 */
        ctx.beginPath();
        ctx.fillStyle = 'rgb(192, 80, 77)'; // 赤
        ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI*2, false);
        ctx.fill();
}

function timerHandler() {
        if (!isStopFlg) {
                update()
                draw()
        }
        /* 次回のタイマーハンドラの登録 */
        setTimeout(timerHandler, 100);
}

function addEventListener() {
        canvas.addEventListener('click', clickHandler, false);
}

function clickHandler() {
        isStopFlg = !isStopFlg;
}

function main() {
        canvas = document.getElementById("canvas1");
        ctx = canvas.getContext("2d");

        ball = new Ball(100, 100, 25);
        v    = new Velocity(5, 5); 
        isStopFlg = false;
        draw()
        addEventListener()
        // 10ミリ秒でタイマーハンドラを呼び出す
        setTimeout(timerHandler, 10);
}