やってみたことの記録です。
ubuntuの場合
$ sudo apt-get isntall nasm
でインストール。
;hello world
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
セミコロン(;)はコメント行。
segment .text はここは命令ですよってことを表してるっぽい。
globa _start は _start ってラベルを大域的なラベルとして使用することを指示している。
mov eax, 4 は eax に 4 を入れる。
mov ebx, 1 はebx に 1 を入れる
mov ecx, message は message というデータの先頭アドレスを入れてるのかな。
mov edx, length は message の長さ。
int 0x80 でシステムコールを呼ぶということ。
システムコールを呼ぶときにはeaxには呼びたいシステムコールの番号を入れる。
ebxからは順にその引数を入れる。
(これはLinuxだからこうなってるだけで、BSDならスタックに入れたりするらしい)
システムコールの番号はubuntuの場合
/usr/include/asm
というディレクトリのunistd_32.hとかその辺のファイルを見ればわかるようだ。
unistd_32.hの中には
#define __NR_write 4
という記述があるので、eaxに4を入れることはつまりwriteを呼ぶことになる。
必要な引数は
/usr/include/unistd.h
を見ればわかる。(manをみたほうがいいかも)
write (int __fd, __const void *__buf, size_t __n)
なので第一引数に1、第二引数に"Hello, world!"の先頭アドレス、第三引数にサイズを入れてる。
fd(ファイルディスクリプター)が1ってのは標準出力(stdout)に書き込む。
次に
move eax, 1 でeaxに1を入れて
int 0x80 でシステムコール。
システムコール1番はexitなのでこれで終了。
segment .data はこっからデータセグメントってこと。
でもこの segment .data ってのは書かなくてもいいらしい。
実際書かなくても動いた。
(segment .data を書いた実行ファイルと書かなかった実行ファイルを objdump -D で比較してみると、書かなかったほうはデータ部分がtextの方に表示される。でも表示されてるのはexitのシステムコール呼んだあとだから問題ないんだと思うたぶん。)
message db "Hello, world!", 0x0a
dbは1byteの意。dwは2byte、ddは4byte。
そのbyte単位でメモリ確保ってことぽい。
だから
dw 'abc'
だと 0x61 0x62 0x63 0x00
で1文字分余る。
文字の対応はASCIIコード表を見るとよい。
こことか
http://ja.wikipedia.org/wiki/ASCII
最後についてる0x0aは改行だね。
length equ $ - message
equ は定数値を定義するときに使うようだ。
$はその式のある行の頭のアドレスを指す。
messageは文字列が格納されてるメモリの頭のアドレス。
$-message で引き算してmessage番地から始まる文字列の長さを出してるわけね。
ちなみに $-message みたいにスペースはなくてもいいみたい。
とりあえずこれでだいたい理解できた。
nasmについてわかんないことは
のDocumentationみればだいたい解決しそう。
ソースファイルの名前がhello.asmなら
$ asm -f elf hello.asm
で、アセンブル。これでhello.oというオブジェクトファイルができる。
-f elf は elf というフォーマットを指定している。elfはi386のLinuxで指定するフォーマット。
$ ld -s -o hello hello.o
これでhelloという実行可能なファイルができる。
-s オプションはすべてのシンボルの情報を取り除くらしい。よくわからんけどファイルサイズ小さくなるんじゃないかな。
-o オプションは出力するファイル名の指定。
$ ./hello
とすれば実行できるはず。
アセンブルとリンクをまとめてシェルスクリプトにするとしたらこんな感じに。
#!/bin/sh
nasm -f elf $1.asm && ld -s -o $1 $1.o
asm.shって名前にしたなら、asm.shに実行可能属性つけて
$ ./asm.sh hello
でhello.asmの実行ファイルが作成される。
C言語で作ったhelloworldと実行ファイルのサイズを比較してみるとわかるけど、アセンブリで書いたほうはめっちゃファイルサイズ小さい!すごい!なんでかよくわかんないけど、たぶんライブラリとかの関係でしょ!
$ objdump -D hello
みたいにすると逆アセンブルできる。(-d オプションだと命令部分のみ)
結果はgas(GNU Assembler)で表示される。
ぇ、だったらgasやっとけばよかったじゃん・・・。
とりあえずghexとか。
$ sudo apt-get install ghex
起動は
$ ghex2
IA-32の機械語の仕様は
http://www.intel.co.jp/jp/download/index.htm
のIA-32 インテル® アーキテクチャー・ソフトウェア・デベロッパーズ・マニュアル、特に中巻を見れば機械語とアセンブリの対応がわかる。
しかしこれ、上中下巻合計で2千ページ以上もあるのね・・・命令もめちゃめちゃいっぱいあるしよく作ったなぁ。
他のアプリケーションを実行してみる。
今回はシェルで。
新しくなにかを実行するシステムコールであるexecveの番号は11。
引数は
第一引数
ファイル名(のアドレス)
第二引数
引数(のアドレスのアドレス)
第三引数
環境変数(のアドレスのアドレス)
とりあえずはシェルを実行することだけ考えるので第二引数、第三引数はnullにして書いてみる。
segment .text
global _start
_start:
mov eax, 11
mov ebx, path
xor ecx, ecx
xor edx, edx
int 0x80
segment .data
path db "/bin/sh", 0x00
これでシェルが実行できた。
xor で同じレジスターの排他論理和をとることによってゼロクリアすると普通に mov で 0 を入れるより実行ファイルのサイズが小さくなる。試してみたらmovは5byte使うけどxorは2byteしか使わなかった。(実行速度も速くなるのかな?)
execveした場合にはもう制御は戻ってこないらしいのでexitのシステムコールは呼ばなくていいらしい。