Linuxでアセンブラプログラミング

 最近話題にのぼる事の多くなったOSで、Linuxと言う物があります。これはGPLで配布されているフリーのOSです。かくいう私も、Windowsの不安定さに耐えかねて、もっぱらこちらのOSを使用することが多いです。
 さて、ここでは何を血迷ったかLinux上でのアセンブリ言語によるプログラムの作成方法について書かせていただくことにします。しかしこんな事を知っていてもあまり特にはならないので、暇な人以外は読み飛ばした方が無難です。また、x86のアセンブラの基本を知っている方が対象ですので、知らない人は暇でも読まない方が賢明です。
 まずは、プラットフォームを決めたいと思います。アセンブラはその性質上、かなり機種依存をしますので、ある程度は範囲を決め手置かねばなりません。しかし、そう特殊な環境と言うわけではなく、至極一般的な環境を選んであります。
 CPUは無難にx86系にします。PentiumとかAMD K6とか、Athlonとかが動いていればいいです。(CyrixとかWinchipとかCrusoeとかもそうです)まあ、一般的なパソコンだと思います。私みたいに未だに現役で386や486と格闘している人は大歓迎です。ただしMacはダメです。MacはCPUに68000系列のCPUを用いていて、x86とは別物です。Macの皆さんごめんなさい。
 Linuxのカーネルのバージョンは2.0.x以上とします。1.x.xでも動作するかもしれませんが、私は2.0.36以上のカーネルしか触ったことがない為の処置です。動かせる自信のある人は頑張って下さい。ちなみに私が動作テストしたLinuxのバージョンは2.2.16と2.4.0-test9です。
 アセンブラはNASMを使用することにします。最新バージョンは0.98です。古い物しか持っていない人はアップグレードしてください。そんなに大変な作業ではありません。configureしてmakeしてrootになってmake installするだけです。持っていない人もダウンロードしてきて下さい。yahoo等の検索エンジンでNASMを検索すれば結構当たるはずです。
 なぜasやgasでないかというと、asやgasは本来ccやgccのフロントエンドとして作られたアセンブラであり、その文法も人間が記述するにはとても面倒な所があります。それが私がNASMを推奨する理由です。
 今回私がテストに使用した環境を並べておきます。

Machine: PC-9821Np/340W
CPU: i486DX4 75MHz
MEM: 21.6MB
HDD: hda: IDE-HDD 2.5" 3.2GB
     hda1 1.4GB MS-DOS 6.2
     hda2 200MB FreeDOS(98)
     hda3 800MB /
     hda4 750MB /home
     hda5  50MB swap
Distribution: Plamo Linux 2.0
Kernel ver: 2.2.14-002 (Linux/98)
            2.2.16-003 (Linux/98)

Machine: ViP KTN-ST800
CPU: Athlon(SocketA ThunderBird) 850MHz
MEM: 256MB
HDD: hda Ultra ATA/100 3.5" 40GB
     hda1  500MB FreeDOS (D:)
     hda2    7GB Windows (C:)
     hda3  ----- Linux extended
     hda4   19GB Windows (E:)
     hda5   50MB /boot
     hda6  500MB /
     hda7    3GB /usr
     hda8  200MB /var
     hda9  100MB /tmp
     hda10   9GB /home
     hda11 512MB swap
CD-ROM: DVD 8x/40x
        Plextor CD-RW 1210TA
Video: RIVA TNT2 M64 32MB
Sound: Sound Blaster Live!
Distribution: Vine Linux 2.0 (FTP)
Kernel ver: 2.2.14
            2.4.0-test9
 この2台でテストをしましたが、正常に動作する事を確認しました。

Linuxプログラムの雛形

 さて、プログラム講座恒例のHello, worldからスタートです。まずはいきなりソースを掲載します。


;----------
; ここからがソースコードです
; hello.asm: coded by Yuki Mitsui
; アセンブル方法
; $ nasm -f elf hello.asm
; $ ld -s -o hello hello.o
;
segment .text
	global	_start
_start:
	mov	eax, 4
	mov	ebx, 1
	mov	ecx, message
	mov	edx, length
	int	0x80
	
	mov	eax, 1
	int	0x80
segment	.data
message	db	"Hello, world!", 0x0a
length	equ	$ - message
;----------

 さて、まずはDOS/WindowsとLinuxの基本的な差違について説明します。messageの定義部分を見ると、最後に0x0aと言う物があります。これは改行コードなのですが、Linux(UNIX全般)の改行コードは0x0aになっています。DOS/Windowsでは、改行コードは0x0d,0x0aの2バイトになります。ちなみにMacでは改行コードは0x0dです。
 プログラムは、_startという所から始まります。これは、リンカのld(gccに付属)の仕様というか、機能です。ldは_startからプログラムからスタートするようにヘッダを生成してくれます。また_startはldが参照するために、globalで宣言してあります。
 4行目から8行目までが、このプログラムの中核となる部分です。まずは/usr/include/asm/unistd.h又は/usr/include/asm-i386/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
			:
			:
			:

 このファイルを掲載してしまったがためにこの文書はGPLの元に配布されなければいけないことになるのですね(笑)必要な部分だけ抜粋しましたが、#defineの次の__NR_を取った物がシステムコールの名前で、その次の番号がシステムコールを呼び出すときの番号です。システムコールを使用するときには、eaxにこの番号をいれます。そしてその後、引数をebx, ecx, edx, esi, ediに列挙していくのです。前述したソースファイルを見ると、ebxとecxとedxに引数が列挙されているのが分かります。システムコールの引数を調べる時には、manページが使えます。例えばwriteの引数は、write(2)のmanページを見ると、次のように書かれています。
ssize_t write(int fd, const void *buf, size_t count);
 ちなみに引数の意味もmanページで見ることができます。ここでは簡単に解説させてもらいますと、fdはファイルディスクリプタで、オープンしたファイルの番号を指定します。*bufは書き込むデータへのポインタで、countは書き込むサイズです。このソースファイル中では、ファイルディスクリプタに1をしています。これはstdoutです。ちなみにデフォルトでオープンされているファイルは、0)stdin, 1)stdout, 2)stderrです。ここではstdoutに出力しています。他の引数はそのままの意味なので解説の必要は無いでしょう。ただ例外はedxの文字列の長さですが、これはlengthの宣言時になにやら変なことをしていますが、これはNASMの文法の一つですので、NASMのマニュアルを参照して下さい。
 レジスタだけを引数の受け渡しに使用していたのでは、引数が5つ以上になったときに引数が渡せなくなってしまいます。しかし、心配することはありません。引数が5つ以上の時は、メモリ上に引数を格納します。そしてその引数リストの先頭をebxにいれてシステムコールを呼び出せば良いのです。
 システムコールを呼び出した後、戻り値がシステムコールより渡されて戻ってきます。戻り値はeaxに格納されています。ちなみにwriteの戻り値は、書き込んだデータのバイト数です。失敗したときは-1を返します。0が帰ってくることもありますが、これはエラーではありません。他のプロセスがファイルディスクリプタを排他的オープンしているせいで、自分のプロセスが書き込めなかっただけです。
 この時点でHello, worldという文字列の表示が出来ました。これで仕事は全て終了したので、プログラムを終了させます。プログラムの終了には、exitというシステムコールを用います。システムコールの番号は1です。このシステムコールは引数をとらないので、これだけでint 0x80を実行します。これで処理はシェルに渡ることになります。

Linuxで引数を扱う

 Linuxでも当然のごとく引数を用いることができます。それも、DOSよりもずっと簡単な方法ですませることができます。Linuxでは、引数中に現れた*や?等は、シェルによって展開されます。そこがDOS/Windowsと違うところですので、注意して取り扱って下さい。
Linuxでの引数の取り扱いは、C言語で取り扱いやすい様なレイアウトになっています。つまりargc, argv, envpの順に取り出せます。(正確にはenvcもありますが)では、具体的な方法を書くことにします。
引数を取り扱うには、まずプログラムが始まってスタックに何も積んでいない状態で、32ビットのデータをpopします。すると、引数の個数を取得することが出来ます。つまりC言語でいうところのargcです。
それからはargcの個数分の引数の要素へのポインタが次々に同じ方法で取得できます。2回目のpopでは最初の引数、3回目のpopでは2番目の引数、というようにしていきます。全ての引数を読み終わった後にpopを実行すると、0が返されます。つまりNULLポインタですね。
さて、これからは本題に反しますが、引数の次に取得出来る環境変数についても説明していきます。これも引数の取得と全く同じ方法で行う事が出来ます。最初のpopで環境変数の要素の数を取得して、次からは実際の要素へのポインタを次々に取得していきます。環境変数の要素は、"環境変数名=値"の形で格納してあります。
では、サンプルを見てください。これは、コマンドラインで指定された引数を全て表示すると言うごく単純な物です。


;----------
; ここからがソースコードです
; argument.asm: coded by Yuki Mitsui
; アセンブル方法
; $ nasm -f elf argument.asm
; $ ld -s -o argument argument.o
;
segment	.text
	global	_start
_start:
	pop	ecx		; 引数の個数(今回は使わない)
	jmp	CheckFinish
MainLoop:
	mov	eax, 4
	mov	ebx, 1
StrLen:
	xor	edx, edx
	jmp	CountFinish
CountLength:
	inc	edx
CountFinish:
	cmp	byte [ecx+edx], 0
	jnz	CountLength
	int	0x80
PrintReturn:
	mov	eax, 4
	mov	ebx, 1
	mov	ecx, Return
	mov	edx, 1
	int	0x80
CheckFinish:
	pop	ecx
	test	ecx, ecx
	jnz	MainLoop
Terminate:
	mov	eax, 1
	int	0x80

segment	.data
Return	db	0x0a
;----------

 アセンブラプログラムのテクニックをいくつか使用していたりしますが、それほど難しいソースファイルでは無いと思います。システムコールもwriteとexitしか使用していません。
 プログラムの全体的な構成は、まず文字列を取得して長さを測る所から始めています。StrLenは、ecxの文字列の長さを解析してedxに格納しています。そうしたら文字列を出力し、改行を出力してもう一度文字列を読みにいきます。文字列が無くなったら、システムコールexitでプログラムを抜けています。頑張ってソースファイルを解析してみて下さい。
 難しい所は、11行目あたりでいきなり無条件ジャンプをしている所でしょうか。ここは何故わざわざ条件判定を最後に持っていっているのかと言いますと、これを使うとジャンプ命令が一つ減らせるからです。自分で普通の組み方をしてみるといいでしょう。
 StrLenからPrintReturnの前の行までの間が文字列の長さを測っている部分です。またPrintReturnからCheckFinishの前の行までが改行をさせるルーチンです。改行は、改行文字1文字だけを出力することによって実現しています。いたって普通の処理です。
 さて、このプログラムを実行してみるとどうなるでしょうか。試しに実行してみましょう。

$ ./arguments foo bar baz quux
./arguments
foo
bar
baz
quux
$ 

 この様に、一番最初にプログラムの名前が出力されています。これはLinuxでは、引数はC言語と同じようにargv[0]から取得される為です。従って、プログラム名はいらなかったら、argcを取得した後に一回から読みをする必要があるでしょう。

解説しなかった物

 ソケットの使い方や、libc,glibc等の共有ライブラリを用いたアセンブリプログラミングについては解説しませんでした。理由は私の勉強不足だけです。これらは独学で頑張って下さい。

Linuxでアセンブリプログラミングを行う際の参考資料

 Linuxでのアセンブリプログラミングを行う際に参考になる資料を次に挙げておきます。この文書とシステムコールの参考書があればLinuxでアセンブリプログラミングを行う事も難しくは無いでしょう。

[Back]

[★高収入が可能!WEBデザインのプロになってみない?! Click Here! 自宅で仕事がしたい人必見! Click Here!]
[ CGIレンタルサービス | 100MBの無料HPスペース | 検索エンジン登録代行サービス ]
[ 初心者でも安心なレンタルサーバー。50MBで250円から。CGI・SSI・PHPが使えます。 ]


FC2 キャッシング 出会い 無料アクセス解析