[ Assembly Programming on Linux (NASM) ] 動作確認は Linux(x86) で行っています。 NASM に関しては http://nasm.sourceforge.net/ を参照してください。DLも 同サイトで出来ますが、ちょっと重いので kernel.org ( http://www.kernel.org/pub/software/devel/nasm/ ) からDLしたほうがいいかもしれません。 この記事はC言語はもちろん、スタックやレジスタといった基本的なことは理 解していること前堤で進めます。 >> [ 0x01 ] 環境を整える NASMのソースをDLします。そしてインストールします。 そしてテキストエディタに以下のように書いて実行してみます。 Hello.asm ------------------------------------------------------------------------------ section .text global _start _start: mov eax, 4 ;write mov ebx, 1 ;stdout mov ecx, msg mov edx, msglen int 0x80 ; mov eax, 1 int 0x80 msg db 'hello!', 0x0A msglen equ $ - msg ------------------------------------------------------------------------------ [kenji@localhost asm]$ ../nasm-0.98.38/nasm -f elf Hello.asm [kenji@localhost asm]$ ld -s -o Hello Hello.o [kenji@localhost asm]$ ./Hello hello! [kenji@localhost asm]$ さて、無事実行できたなら毎回こんなこと書くのはメンドクサイのでシェルス クリプトを作ります。 asm.sh ------------------------------------------------------------------------------ #!/bin/sh ../nasm-0.98.38/nasm -f elf $1.asm && ld -s -o $1 $1.o ------------------------------------------------------------------------------ [kenji@localhost asm]$ chmod 755 asm.sh [kenji@localhost asm]$ ./asm.sh Hello [kenji@localhost asm]$ ./Hello hello! [kenji@localhost asm]$ nasm へのパスは適当に変更してください。あと個人的に asm.sh という文字 列はちょっと長いと思うので私は sm としています。1ストロークで書けると いうことでこのファイル名にしました(笑)以後このファイル名で書いていま すが、それは asm.sh と解釈してください。 >> [ 0x02 ] システムコール まずは以下のファイルを見て下さい。もしかしたらファイルの保存場所は違う かもしれませんが多分このへんにあるので探して下さい。 /usr/src/linux-2.4.18/include/asm/unistd.h ------------------------------------------------------------------------------ #ifndef _ASM_I386_UNISTD_H_ #define _ASM_I386_UNISTD_H_ /* * This file contains the system call numbers. */ #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 #define __NR_creat 8 #define __NR_link 9 #define __NR_unlink 10 #define __NR_execve 11 #define __NR_chdir 12 #define __NR_time 13 ................ ........... (続く) ------------------------------------------------------------------------------ このファイルは見ての通りシステムコールの値が示されています。私はシステ ムコールを使用するのに毎回これを見て対応された番号を検索します。かなり 重要ですのでいつでも見れるカタチにしといてください。 それでは Hello.asm をもう一度見てみます。 section .text global _start _start: mov eax, 4 ;write mov ebx, 1 ;stdout mov ecx, msg mov edx, msglen int 0x80 ; mov eax, 1 int 0x80 msg db 'hello!', 0x0A msglen equ $ - msg eax に 4 が入れられています。これはシステムコールの write を使用すると いうことです。システムコールを使う場合、それに対応する値は必ず eax に 入ります。そして ebx は第一引数、ecx は第二引数、edx は...です。ebx は 1 なので stdout です。(writeシステムコールの第一引数は出力先です) ecx には出力すべき文字列のポインタ、そして edx にはその文字列の長さが入り ます。そして最後に int命令で「実行」ということです。 注 システムコールがどのような引数をとるのかはmanpageで調べて下さい。 使いたいシステムコールを unistd.h から調べてきてそのシステムコー ルに関する詳細をmanpageで調べる。そして使用するという流れでプログ ラムを組んでいきます。 そしてつぎには eax に 1 が代入されていますので、システムコール exit で すね。そして再び int 命令を実行して exit しています。 io.asm ------------------------------------------------------------------------------ section .text global _start _start: push eax ; mov eax, 3 ;read mov ebx, 0 ;stdin mov ecx, esp mov edx, 2 int 0x80 ; mov eax, 4 ;write mov ebx, 1 ;stdout mov ecx, esp mov edx, 2 int 0x80 ; pop eax ; mov eax, 1 int 0x80 ------------------------------------------------------------------------------ [kenji@localhost asm]$ ./sm io [kenji@localhost asm]$ ./io f f [kenji@localhost asm]$ さて多少長いですが、問題ないです。まずシステムコール read を使っていま す。もちろん読むべき場所は標準入力の stdin です。だから ebx = 0 としま す。mov ecx, esp となってますが、esp とはスタックのTOPのアドレスを示し ます。よって標準入力から一文字受け取ってスタックに入れるということにな ります。 ソースコードの最初に注目です。push eax とはスタックTOPに eaxのデータを 格納するということになります。eax の中に何がはいってるかは問題では無い です。ポイントは eax が 32bits だということにあります。つまりメモリを 32bits 確保したということになります。そしてその場所に read を使って標 準入力から受け取ったデータを保存します。長さは edx の 2bytes分ですね。 結果的に 2bytes = 16bits しか使わないことになります。よって eax では 無く ax(=16bits分) としても問題ないです。(しかし今や 1bytes にしのぎ を削る時代じゃないので) そしてつぎに write を呼び出して標準出力に書き出していますね。そして最 後に exit で終了ですが、そのまえに pop eax があります。eax分のメモリを 抜き取った、つまりメモリ解放の意味でとらえて問題ないと思います。(本当 はちょっとニュアンスが違いますがそれは後で説明します) >> [ 0x03 ] 引数 [其ノ壱] argc.asm ------------------------------------------------------------------------------ segment .text global _start ; _start: pop ecx add ecx, '0' push ecx mov eax, 4 ;write mov ebx, 1 ;stdout mov ecx, esp mov edx, 1 int 0x80 ; mov eax, 1 int 0x80 ------------------------------------------------------------------------------ [kenji@localhost asm]$ ./sm argc [kenji@localhost asm]$ ./argc 1[kenji@localhost asm]$ ./argc ee tt ee 4[kenji@localhost asm]$ いきなり pop ecx として、スタックからデータを取得しています。このデー タは引数の数(かず)です(つまりCで言うargcです)数値として取得してしまい ますので、ecx に文字としての'0'を加算しています。 すると文字としての数である数字が取得できるのでこれを再びスタックに格納 します。(これは引数が9以下の場合にのみ当てはまる演算です) そしてあとは 同じです。そのまま write をよびだして引数の数である数字を出力していま す。最後には exit で終了です。 >> [ 0x04 ] 条件分岐 アセンブラで条件分岐を実現する場合、フラグという概念が重要になってきま す。フラグというのはつまり旗ですね。んでフラグというのは命令が実行され るごとに常に変化していきます(必ず変化するという意味ではありません。変 化しないということも含めて変化するということです。ちょっと意味不明です が^^;)これを利用して条件分岐をを実現します。 loop.asm ------------------------------------------------------------------------------ segment .text global _start ; _start: pop ecx mov edx, ecx jmp MAIN_LOOP ; Exit: mov eax, 1 int 0x80 ; MAIN_LOOP: add ecx, 1 cmp ecx, '0' jnz MAIN_LOOP add ecx, edx push ecx ; mov eax, 4 ;write mov ebx, 1 ;stdout mov ecx, esp mov edx, 1 int 0x80 jmp Exit ------------------------------------------------------------------------------ [kenji@localhost asm]$ ./sm loop [kenji@localhost asm]$ ./loop 1[kenji@localhost asm]$ ./loop ee ee tt 4[kenji@localhost asm]$ ↑(ちゃんと出力されてますよ^^;) これは動作としては argc.asm と同じですが、条件分岐をふんだんに使用して います(といっても一ヶ所ですけどね) まず jmp 命令です。これは無条件ジャ ンプです。この命令があれば必ず指定された場所へ処理がジャンプします。こ の場合は MAIN_LOOP へジャンプするわけです。 そして MAIN_LOOPへ入ると ecx に 1 加算されます。cmp A,B は A と B を比 較します。同じ値ならば ZF=1 になり A の方が小さければ SF=1 になり、A の方が大きければ SF=0 となる。(ZF,SFはそれぞれフラグである) さてさて、 では jnz はなんなのか?というと、これは ZF が 0 ならば指定した場所にジ ャンプする。ということは、cmp命令により ZF(フラグ)が 1 になったならば 通りすぎるわけなので、つまり ecx = '0' が成り立つ(真)ならば通りすぎる ということになります。 さて、ここまで分かったらもうプログラムの動作は理解できるはずです。 >> [ 0x05 ] 引数 [其ノ弐] いきなりですが、このプログラムを読みましょう。 argv.asm ------------------------------------------------------------------------------ segment .text global _start ; _start: pop ecx ;引数の個数(捨て) jmp Check ; Length_zero: mov eax, 4 mov ebx, 1 xor edx, edx ; CountLength: inc edx cmp byte [ecx+edx], 0 jnz CountLength int 0x80 ; LineEnd: mov eax, 4 mov ebx, 1 mov ecx, Return mov edx, 1 int 0x80 ; Check: pop ecx cmp ecx, 0 jnz Length_zero ; Exit: mov eax, 1 int 0x80 ; segment .data Return db 0x0a ; ------------------------------------------------------------------------------ [kenji@localhost asm]$ ./sm argv [kenji@localhost asm]$ ./argv e tt w3 yy ./argv e tt w3 yy [kenji@localhost asm]$ byte [ecx+edx], 0 とは「 ecx+edx のアドレスにある値 1byte 」ということ です。つまりCでいうポインタと同じようなものです。 xor は排他的論理和で すね。xor は値を 0 にしたい時に主に使用します(同じ値で排他的論理和をと ると必ず 0 になりますよね) inc は インクリメント。つまり 1 加算です。 最終的になにをしてるのかというと、スタックにある引数を取得しています。 最初は個数ですので捨てます。そしてそれに続いて、プログラム名のアドレス、 第一引数のアドレス、第二引数のアドレス、第三.......と続いて、そのあと にプログラム名(の文字列)、第一引数、第二引数、.....とスタックには保存 されているので、(まぁアドレスだけ分かればデータを取得できるのでここま で知らなくともいいのですけど) それらを1つずつ取得しています。 >> [ 0x06 ] 基礎的なこと Pentium 系プロセッサのレジスタには以下のようなレジスタがあります。 レジスタの種類 ----------------------------------------------------------------- 汎用データレジスタ eax, ebx, ecx, edx, esi, edi, esp, ebp セグメントレジスタ cs, ds, ss, es, fs, gs ステータス制御レジスタ eflags, eip ----------------------------------------------------------------- これまでの章を読んでいれば汎用データレジスタについてはだいたいどういう ものか理解できるはずです。こういうものは最初に説明されても意味不明です ので 0x06章 で突然いれさせてもらいました。 汎用レジスタ eax ,ebx, ..... ebp は 32bits です。がこれらは分割された 部分にも名前があります。 <----------eax----------> <-----ax----> <-ah-> <-al-> +-----+-----+-----+-----+ | | | | | +-----+-----+-----+-----+ eax の下位 16bits が ax であり ax の 上位 8bits が ah, 下位 8bits が al です。これは eax だけでなく ebx, bx, bh, bl ecx, cx, ch, cl edx, dx, dh, dl それぞれに当てはまります。 esi, edi, esp, ebp に関しては、 esi, si edi, di esp, sp ebp, bp と 16bits までは当てはまりますが、8bits 単位の指定はありません。 ステータス制御レジスタの eflags には以下の種類のフラグがあります。 キャリーフラグ (CF) ゼロフラグ (ZF) サインフラグ (SF) オーバーフローフラグ(OF) ジャンプ命令のジャンプする条件。cmp命令の後に使用されることが多い。 | on off jz 命令は ZFフラグ が立っている時にジャンプします。jnz ---+-------- 命令は ZFフラグ が立っていないときジャンプします。以下 ZF | jz jnz 同じ。 SF | js jns CF | jc jnc OF | jo jno cmp A,B は A と B を比較します。同じ値ならば ZF=1 になり A の方が小さ ければ SF=1 になり、A の方が大きければ SF=0 となる。(ZF,SFはそれぞれフ ラグである) こちらは(↓)数値で判定してくれるジャンプ命令です。符号の有る無しで命令 が違います。 | 肯定 | 否定 ------+--------------------+------------------- 符号 | 符号あり 符号無し | 符号あり 符号無し ------+--------------------+------------------- > | jg ja | jng jna < | jl jb | jnl jnb >= | jge jae | jnge jnae <= | jle jbe | jnle jnbe == | je je | jne jne とりあえず、このようなものがあるということだけ覚えといてください。使っ てるうちにどんなものかは分かってきます。気になる方は検索してください。 >> [ 0x07 ] ファイル操作 いよいよファイル操作です。といっても特に難しいところは無いでしょう。 file.asm ------------------------------------------------------------------------------ segment .text global _start ; _start: ; push eax ; OPEN: mov eax, 5 ;open mov ebx, msg mov ecx, 0 ;r_only int 0x80 ; mov esi, eax ; READ: mov eax, 3 ;read mov ebx, esi mov ecx, esp mov edx, 1 int 0x80 ; CLOSE: mov eax, 6 mov ebx, esi int 0x80 ; WRITE: mov eax, 4 ;write mov ebx, 1 ;stdout mov ecx, esp mov edx, 1 int 0x80 ; pop eax ; EXIT: mov eax, 1 int 0x80 ; msg db 'test.txt', 0 msglen equ $ - msg ; ------------------------------------------------------------------------------ [kenji@localhost asm]$ cat test.txt AAA BBB CCC [kenji@localhost asm]$ [kenji@localhost asm]$ ./sm file [kenji@localhost asm]$ ./file A[kenji@localhost asm]$ さて、ソースが長くなってきましたが、やってることは基本的に同じです。引 数を設定してシステムコールを利用しているだけです。 jmp 命令などは使ってないのですが、分かりやすいように処理の名前をつけま した。まず open はシステムコール 5 ですね。ebx にファイル名です。 ecx は 0 = 読み込み, 1 = 書き込み, 2 = 両方, です。ecx = 0 なので読み込み です。そして int で実行していますが、戻り値(ファイルポインタ)が eax に 入るので それを受け取って esi に格納しています。(esi はあまり使いませ んでしたが eax などと同じようにレジスタです) そして read でファイルか ら一文字読み込んでいます。保存先はプログラム冒頭で確保しているスタック 領域ですね。そしてシステムコール close でファイルを閉じて write で標準 出力に書き出して終了です。簡単ですね。 では、いままでのまとめも兼ねてファイルをコピーするプログラムを作りまし た。(↓) ちょっとソースが長いのでUPしました。 http://ruffnex.oc.to/kenji/src/copy.asm [kenji@localhost asm]$ cat test.txt AAA BBB CCC [kenji@localhost asm]$ ./sm copy [kenji@localhost asm]$ ./copy test.txt test2.txt [kenji@localhost asm]$ [kenji@localhost asm]$ cat test2.txt AAA BBB CCC [kenji@localhost asm]$ copy.asm -------- pop ecx cmp ecx, 3 jnz ERROR まずは引数の数を取得します。それが 3 では無いならば ERROR に飛びます。 もし 3 ならば、通過します。 pop ecx ;myProgramName(捨て) ; pop esi ;read_File pop edi ;write_File 最初にプログラム名はいらないので捨てます。そしてつぎの2つのファイル名 を貰って来ます。 mov eax, 5 ;open mov ebx, edi mov ecx, 1 ;w_only or ecx, 100q mov edx, 644q int 0x80 読み込みファイルの open 処理は省略します。書き込みファイルの open 処理 で ecx がいろいろといじられてますが、説明します。まず or 命令はC言語の ビット演算子 '|' と同じです。100q のように最後に q がついていると8進数 となります。つまり8進数の 100 となります。さてこれはどういう意味かとい うと、/usr/src/linux-2.4.18/include/asm/fcntl.h をみてください。さらに (↓)を見ると。 [kenji@localhost asm]$ man open ................ The parameter flags is one of O_RDONLY, O_WRONLY or O_RDWR which request opening the file read-only, write-only or read/write, respectively, bitwise-or'd with zero or more of the following: ................ パラメータフラグは 読み込み専用でOPENする O_RDONLY, 書き込み専用でOPEN する O_WRONLY もしくは両用である O_RDWR のどれか1つを指定することがで きる。ビット or を利用することにより 0 もしくそれ以上の値をフラグに指 定することができます。(以下に O_CREAT, O_EXCL, O_EXCL etc... が続きま す) 私は「書き込み専用、ファイルが無い場合は作る、アクセス権限は 644 」と いうファイルを作りたいので /usr/src/linux-2.4.18/include/asm/fcntl.h をみて、ecx = O_WRONLY, O_CREAT, そして edx = 644q ということになりま す。 JUDGE: cmp eax, 0 jz CLOSE jz 命令は jnz 命令の逆です。ZF が 1 ならばジャンプします。つまり eax が 0 ならば(ファイルがEOFなら)ジャンプします。 まぁ説明すべきところはこんな感じでしょう。大して難しいことはやってませ ん。esi, edi にはそれぞれ最初は ファイル名のポインタが入ってるのですが、 ファイルを OPEN した後は そのファイルポインタが入ることになります。 >> [ 0x07 ] メモリの使い方 さてだいぶアセンブラに慣れてきたところだと思われます。それに従ってサン プルソースも長くなっていきます(笑) http://ruffnex.oc.to/kenji/src/print.asm 以前に cp コマンドを真似た copy.asm を作ったので、今回は cat コマンドを 真似た print.asm です。どちらも同じようなプログラムですが前回は1バイト ずつ読み込んで書き込んでをくり返していましたが今回はファイルの容量分の メモリを確保して一気に読み込みそして出力します。 [kenji@localhost asm]$ ./sm print [kenji@localhost asm]$ ./print Hello.asm section .text global _start _start: mov eax, 4 ;write mov ebx, 1 ;stdout mov ecx, msg mov edx, msglen int 0x80 ; mov eax, 1 int 0x80 msg db 'hello!', 0x0A msglen equ $ - msg [kenji@localhost asm]$ print.asm --------- segment .bss ; buf resd 1 size resd 1 まず最初のこれはですね。いわゆる変数です。 resd(4bytes), resw(2bytes), resb(1byte) を確保します。その次にある数字 は「いくつ確保するか?」です。つまりこの場合「bufという名前のメモリ空 間を (4bytes * 1) だけ確保する」ということになります。 segment .bss にあるデータは初期化されません。そしてresd,resw,resbが使 われます。segment .text は今まで見てきたとおり実行されるプログラムその ものです。segment .data は初期化されるデータです。例えばいままでお馴染 みに使っていた e_msg db 'ERROR!', 0x0A msglen equ $ - e_msg などはまさしく初期化済みのデータですよね。つまりはこれらのデータも本当 は segment .data とした上で書かなければならないようですが、何故か書か なくともOKなので書いてません。ちなみに segment .data に置かれるデー タは dd(4bytes), dw(2bytes), db(1byte)となります。じゃあ equ はなんな んだ?と思われるかもしれませんが、それは御自分で調べてください(^^; mov eax, 19 ;lseek mov ebx, esi xor ecx, ecx mov edx, 2 ;SEEK_END int 0x80 ; mov [size], eax ;File size GET!! システムコール lseek を呼び出してます。まぁ簡単ですね。ファイルのサイ ズを戻り値として返しますので、eax に戻り値が入ります。それを size に代 入しています。 角括弧([]←これね)は以前も少しでてきましたが、改めて説明します。 「size は(値への)アドレス」です。「[size] はアドレスが指してる値」です。 いわゆるCのポインタみたいなもんですね。(だたし size にはあくまでアドレ スが入ってるということを忘れないで下さい。size に入ってるアドレスを変 更するのか、入ってるアドレスが指し示している値を変更するのか、というこ とであって size 自体のアドレスは関係無いのです。) size に 1 加算すると、アドレスを1つ進める、つまり、次のメモリ空間のデ ータを指し示すことになります。一方、[size] に 1 加算すると、値そのもの に 1 加算されます。まぁCを理解していればなんてことはないでしょう。要は ポインタですね。segment .bssで定義された変数はすべてポインタと考えとけ ばいいでしょう(ぉぃ この場合はアドレスを変更したいのではなく値を変更したいので、[size] に eax (つまりはファイルのサイズ)を代入しています。 mov eax, 45 ;brk xor ebx, ebx int 0x80 brk については調べて下さい。メモリ領域を確保するシステムコールです。ま ずは引数に 0 を渡して現在のメモリ領域をもらいます。そしてそれにファイ ルサイズを加算したメモリ領域を再び brk に渡すと、加算した分だけメモリ 領域が確保されるということです。そこに読み込みファイルのデータを確保し てやればOK!というわけです。 FREE: mov eax, 45 ;brk mov ebx, [buf] int 0x80 最後にはもちろんメモリを解放しなければなりません。brk に元のサイズの引 数を渡して実行し、メモリの解放を行っています。 END: push ecx push ecx push ecx えー何故、わざわざ意味も無いデータをスタックにいれてるのかというと、で きれば push と pop は同じ数にしたほうが良いそうなのです。Linuxだとプロ セスが終了するとメモリをちゃんと管理して解放してくれるので、全然問題無 いのですが、他のOSだと問題があるようなのです。まぁ念のためです。 >> [ 0x08 ] 子プロセスの生成 えーそのまんまです。forkを使うプログラムを作ってみます。 http://ruffnex.oc.to/kenji/src/fork.asm [kenji@localhost asm]$ ./sm fork [kenji@localhost asm]$ ./fork Children (← 3秒待つ) Parents [kenji@localhost asm]$ まず最初は fork を呼んでいます。 mov eax, 2 ;fork int 0x80 ; cmp eax, 0 jz CHILD 戻り値が 0 ならばそれは子プロセスなので CHILD にジャンプさせます。親プ ロセスはそのまま先に進ませます。 mov eax, 162;nanoSleep mov ebx, time mov ecx, zero int 0x80 まずは子プロセス側の説明をします。これはシステムコール nanoSleep なの ですが、要するに sleep と同じです。指定した秒だけ処理を待ってくれます。 詳細は manpage にて。第一引数に 3 のアドレスを渡していますので 3 秒待っ てくれます。(nanoSleep はナノ秒レベルで秒数を指定できます) 子プロセスの最後には EXIT に無条件でジャンプしています。これは親プロセ スも同じですね。 PARENT: mov esi, eax mov eax, 7 ;waitpid mov ebx, esi mov ecx, zero mov edx, 0 int 0x80 親プロセスは waitpid システムコールで子プロセスが終了するのを待ちます。 forkの戻り値に 子プロセスのプロセスIDがいれられてますのでそれを利用し て待ちます。子プロセス側で 3 秒のウェイトがかかってますのでその間親プ ロセスはずっと待っていることになります。そして終了を受け取ったら自分自 身も write して終了します。 >> [ 0x09 ] ネットワークプログラム えー最後にアセンブラでネットワークプログラムをしてみます。アセンブラの 入門サイトとかでもあんまりネットワークについてはやってないっぽかったの でやってみました。 でも正直、私はあまり乗り気じゃないです(笑)というか何が悲しくてアセン ブラでサーバなんぞ作らにゃならんのだ、と思ってます。いまや21世紀ですよ。 世紀末なんてもう遠い過去の話ですよ。オブジェクト指向やらなんやらでもう アセンブラなんてやってる人いないですよ。システムコールとか言っちゃって る人いないですよ。 とまぁなげやりですけど、やってみました。ちょっと言うと、実はアセンブラ って結構面白いぞ。とか思ってます(^^; ネットワークは不本意ですが、アセ ンブラ自体は結構ハマッテマス! まずはSocket関係のシステムコールを探します。 /usr/src/linux-2.4.18/include/asm/unistd.h すると socketcall なるものが見つかります。 [kenji@localhost asm]$ man socketcall ..................... int socketcall(int call, unsigned long *args); ..................... socketcall is a common kernel entry point for the socket system calls. call determines which socket function to invoke. args points to a block containing the actual arguments, which are passed through to the appropriate call. User programs should call the appropriate functions by their usual names. Only standard library implementors and kernel hackers need to know about socketcall. ..................... 「call はsocket関数を呼ぶ。args は渡すべき引数を設定する」 と訳せます。えらくやる気の無い訳ですが、まぁ書かれてあることはこんなも んでしょう。(すでになげやりモードだ) さらに最後に 「標準ライブラリを作る人(?)とカーネルハッカーだけが socketcall につい て知る必要がある」 と書かれてある。私はどちらでも無いが知りたいぞ。アセンブラでネットワー クプログラムを書く人も追加してくれ!さぁ追加してくれ!今すぐ追加してく れーーーー!!(ちょっと壊れ気味ですな...) ではこちら(↓)を御覧下さい。 /usr/include/linux/net.h ------------------------------------------------------------------------------ ................. #define SYS_SOCKET 1 /* sys_socket(2) */ #define SYS_BIND 2 /* sys_bind(2) */ #define SYS_CONNECT 3 /* sys_connect(2) */ #define SYS_LISTEN 4 /* sys_listen(2) */ #define SYS_ACCEPT 5 /* sys_accept(2) */ #define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */ #define SYS_GETPEERNAME 7 /* sys_getpeername(2) */ #define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */ #define SYS_SEND 9 /* sys_send(2) */ #define SYS_RECV 10 /* sys_recv(2) */ #define SYS_SENDTO 11 /* sys_sendto(2) */ #define SYS_RECVFROM 12 /* sys_recvfrom(2) */ #define SYS_SHUTDOWN 13 /* sys_shutdown(2) */ #define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */ #define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */ #define SYS_SENDMSG 16 /* sys_sendmsg(2) */ #define SYS_RECVMSG 17 /* sys_recvmsg(2) */ ..........(続く) ------------------------------------------------------------------------------ socketcall の第一引数にここに示されている値をいれて、第二引数に渡すべ き引数をいれてやれば良いらしいです。ではソースを御覧下さい。 http://ruffnex.oc.to/kenji/src/tcp_hello.asm [kenji@localhost asm]$ ./sm tcp_hello [kenji@localhost asm]$ ./tcp_hello Start!! Socket OK! Bind OK! Listen OK! (別のWindowで) [kenji@localhost kenji]$ telnet localhost 3333 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Hello!! Connection closed by foreign host. [kenji@localhost kenji]$ Client Connect. [kenji@localhost asm]$ 実行してみて分かるように接続を受けつけたら Hello!! という文字列を返す サーバです。しかもなぜか自分自身も終了してしまいます。気になる方は jmp 命令でもいれて終了しないようにしてください。 ではソースの説明に入ります。 ;----------------- ; tcp_hello.asm ;----------------- %define socketcall 102 %define dup2 63 %define write 4 ; %define socket 1 %define bind 2 %define connect 3 %define listen 4 %define accept 5 %define send 9 %define recv 10 まず、最初にいろいろと定義しています。define は C と同じです。最初が % ですけど機能は同じです。文字列の置き換えですね。 section .data ; port dw 3333 ; zer db 'Start!!', 0x0A zer_len equ $ - zer ここもいろいろと定義しています。port はもちろんポート番号です。 SETDATA: mov [argv], eax mov [argv+4], ebx mov [argv+8], ecx mov [argv+12], edx ; xor ecx, ecx mov [argv+16], ecx ; ret ; ; DEBUG: mov eax, write mov ebx, 1 int 0x80 ; ret SETDATA はメモリ空間に値をセットする関数(?)です。DEBUG はただwriteシス テムコールを呼び出すだけです。 _start: ;---------------------------- mov ecx, zer mov edx, zer_len call DEBUG ;---------------------------- プログラムの最初でさっそく呼び出しています。 call 命令は jmp 命令に似 ていますが、ret があると call された場所に戻るという部分が違います。 call は C で言うところの関数呼び出しであり、jmp命令は goto と言ったと ころです。;------ で挟まれてるところは DEBUG 用の出力です。 SOCKET: mov eax, 2 mov ebx, 1 mov ecx, 6 mov edx, 0 call SETDATA ; mov eax, socketcall mov ebx, socket mov ecx, argv int 0x80 ; mov esi, eax ; SETDATA によって argv に値を代入しています。そして socketcall システム コールを呼びます。引数は socket( = 1 ですね。/usr/include/linux/net.h) と argv (socket に渡すべき引数)です。 socket関数に渡すべき引数は何故 2, 1, 6, なのか?それは socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); test.c ------------------------------------------------------------------------------ #include #include #include int main(void) { printf("%d %d %d\n", AF_INET, SOCK_STREAM, IPPROTO_TCP); return 0; } ------------------------------------------------------------------------------ [kenji@localhost c.c++]$ gcc -Wall test.c [kenji@localhost c.c++]$ ./a.out 2 1 6 [kenji@localhost c.c++]$ だからです(笑)まぁ詳しくは include してるファイルを見て下さい。 BIND: mov cx, 2 mov [addr], cx mov cx, [port] mov al, ch mov ch, cl mov cl, al mov [addr+2], cx mov cx, 0 mov [addr+4], cx ここはちょっとソースがきたないです。 bind(soc,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); の第二引数を作ってるところです。 最初はserv_addr.sin_family=AF_INET;なので 2 をいれます。次のメモリ枠に はポート番号です。[port]を cx(16bits) にいれて 上位 8bits と 下位 8bits を入れ換えています。この意味は分かりますよね。要するに htons関数 ってことですね。そしてアドレスですがサーバなので 0 でOKですね。これで bindに渡すべき第二引数ができました。 mov eax, esi mov ebx, addr mov ecx, 16 mov edx, 0 call SETDATA ; mov eax, socketcall mov ebx, bind mov ecx, argv int 0x80 esi は socket 関数の戻り値です。addr は今作ったやつですね。んで第三引 数は sockaddr のサイズなので 16 をいれましょう。そしてこれを SETDATA を使い argv にセットして socketcall 引数の第三引数として渡します。 LISTEN: mov eax, esi mov ebx, 1 mov ecx, 0 mov edx, 0 call SETDATA ; mov eax, socketcall mov ebx, listen mov ecx, argv int 0x80 同じですね。listen に渡すべき引数を SETDATA を使って argv にセットして あとは socketcallシステムコール で呼びたいSocket関連の関数を呼び出して いるだけです。 ACCEPT: mov eax, esi mov ebx, 0 mov ecx, 0 mov edx, 0 call SETDATA ; mov eax, socketcall mov ebx, accept mov ecx, argv int 0x80 ; mov edi, eax ここも同じですね。 DUP2: mov eax, dup2 mov ebx, edi xor ecx, ecx int 0x80 ; mov edi, eax これを使って複製しています。dup2 について知らないならば検索してくださ い。 SEND: mov eax, edi mov ebx, s_msg mov ecx, s_len mov edx, 0 call SETDATA ; mov eax, socketcall mov ebx, send mov ecx, argv int 0x80 ここで送信の処理を行っています。send は write と似てるので理解は容易だ と思います。Hello!! という文字列を送信しています。このあとすぐに close が呼ばれ、exit で終了しています。 以上で終わりです。多分説明読んだだけではあまり分からないと思いますので ソースをいろいろとイジッてみてください。 アセンブラといっても結構いろいろとできるものです。サーバもどきが出来た のでもちろんクライアントもできるでしょう。HTTPクライアントなども作れる と思います。ただ、HTTPよりはメモリ管理の方に気を配らなければなりません が(笑) >> [ 0x0A ] 最後に 最初はちょっと興味本位でやってやろうとか思って始めたのですが、結構アセ ンブラって面白いかも。とか思いました。GNUはBOF関係でちょっと触ったこと があったのですが、NASMは始めてだったんで、新鮮でした。アセンブラは微妙 にハマリ気味なんで、いずれまたアセンブラ関係の記事を書くかもしれません。 マジで書くかも(^^; >> [ 0x0B ] 参考文献 The Netwide Assembler: NASM http://guriponn.at.infoseek.co.jp/nasmdoc0.htm Linux でアセンブリプログラミング http://www.nk.rim.or.jp/~jun/lxasm/asm_idx.html アセンブラ入門 http://www.d1.dion.ne.jp/~ecb/assembler/assembler00.html End. written by kenji aiko 2003/12/04 Copyright (C) 2003 kenji aiko All Rights Reserved