2011年03月13日
アセンブリをやってみよう! 0x140 ~システムコール~
また、前回のスタックが少々難しい内容であったため、その復習も
兼ねたいと思います。
スタックは、espとebpという二つのレジスタによって示される、
手軽に扱えるメモリ領域でした。
ebpがスタックの最下点を示し、espがスタックの最上点を示します。
これを利用するためにpush命令とpop命令を使います。
push命令でスタックを伸ばして(espの値を減算)値を入れ、
pop命令で値を取り出してスタックを縮め(espの値を加算)ます。
前回触れていなかったのは、スタックはメモリの低位アドレスに
伸びるということです。
低位というのは、アドレスの値を比べたときにその値が小さい方
ということですね。
たとえば、今esp=0x884c954cだったとき、4バイトのデータを
プッシュすると、esp=0x884c954c - 4 = 0x884c9548になります。
ようするには、スタックは本当に実在するというよりは、
人間が便宜上そう考えているだけのメモリ領域です。
前回の記事を読み返しつつ、続きからどうぞ。
○今回のプログラム
.set EXIT, 1 # exit system call
.set READ, 3 # read system call
.set WRITE, 4 # write system call
.set OPEN, 5 # open system call
.set CLOSE, 6 # close system call
.set O_RDONLY, 2 # read only
.data
CRLF: .ascii "\n"
ENV_ERR: .string "too few argments"
OPEN_ERR: .string "file open error"
.bss
TMP: .skip 1
.text
.global main
main:
push %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
cmpl $2, %eax
jnb L0
push $ENV_ERR
call print
jmp end
L0:
movl 12(%ebp), %eax
pushl 4(%eax)
call file_open
call file_read
call file_close
end:
call exit
###########################
# sub function
###########################
file_open:
movl $OPEN, %eax
movl 4(%esp), %ebx
movl $O_RDONLY, %ecx
int $0x80
movl %eax, %esi # FDを退避
cmpl $-1, %esi
jne open_end
pushl $OPEN_ERR
call print
popl %esi
open_end:
ret
file_read:
movl $READ, %eax
movl %esi, %ebx
movl $TMP, %ecx
movl $1, %edx
int $0x80
movl $TMP, %edx
cmpb $0x00, (%edx)
je read_end
cmpl $0, %eax
je read_end
cmpl $-1, %eax
je read_end
movl $WRITE, %eax
movl $1, %ebx
movl $TMP, %ecx
movl $1, %edx
int $0x80
jmp file_read
read_end:
ret
file_close:
movl $CLOSE, %eax
movl %esi, %ebx
int $0x80
ret
print:
movl 4(%esp) ,%ecx
movl $1, %ebx # 標準出力へ
movl $1, %edx # 1文字出力
write:
cmpb $0x00, (%ecx) # null文字かどうか
je write_end
movl $WRITE, %eax # write
int $0x80
incl %ecx # 次の文字
jmp write
write_end:
movl $WRITE, %eax
movl $CRLF, %ecx
int $0x80
ret
exit:
movl $EXIT, %eax
xorl %ebx, %ebx
int $0x80
これが今回のソースコードです。
どうやら105行のようです(コメント、空行含む)…。
さて、結論から言ってしまうと、これは簡易型catコマンドを
アセンブリで実装したものになります。
勿論オプション処理なんて高等なものはついていませんが。
○システムコール
さて、これまでwriteやexit、read等のシステムコールを使って
きました。一度、システムコールは「これをやるにはOSの力を
借りるしかない機能」という説明をしたことがあります。
実際、確かにそうであることが多いのですが、システムコールに
明確な定義はあまりなされていません。
システムコールとは、OS、正確に言うならばカーネルが提供している
機能全般のことをさします。
一般的には、文字出力やメモリ確保等、必要最低限の機能が
OSで用意されています。
C言語にはprintf関数やmalloc関数など、便利な標準ライブラリ
関数が多く存在しますが、これらは最終的に内部的にはシステム
コールを呼んでいます。
突然、Perlでprint文が使用禁止になったらどうでしょうか。
また、JavaでWriteLineメソッドが使えなくなったらどうしますか。
コンソールアプリケーションで最も基本的な「文字列表示」という
ユーザーへのレスポンスができなくなるのです。
このように、私たちはOSの力を借りてプログラミングしているため、
最終的にはシステムコールに頼らざるを得ません。
ですから、アセンブリを知っておくのは有益なことではないかと
思います。(Cでもシステムコールは使えます)
*********************************************
さて、前にも言いましたが、システムコールは以下のようにして
使用します。(Linuxの場合)
eax = システムコール番号
ebx = 第一引数
ecx = 第二引数
edx = 第三引数
⇒代入してからint $0x80
システムコールは正確には関数ではないため、「引数」という表現は
不適切かもしれません。要するに「与えるデータ」のことです。
また、引数は0個のものから3個のものまであります。
3個に満たない場合はedxやecxは触らなくてよいです。
たとえば、writeシステムコールを見てみます。
$ man 2 write
manコマンドでこのようにマニュアルを見ることができます。
ちなみに、間に2を挿んでいるのは、「システムコールのwriteを
表示しなさい」という意味です。同じ名前でもコマンドにも
あったりするものもあるので、2をつける癖をつけた方がよいです。
【参考】manコマンドの使い方
SYNOPSISのところを見てください。
第一引数はfdで、あとのDESCRIPTIONを見ると、ファイル
ディスクリプタ(後述)であることがわかります。
第二引数bufは出力するデータ、第三引数countは出力する
バイト数などとわかります。
「俺は死んでも英語を読む気はない」という方はこちらを。
でも、プログラミングには英語はつきものですからね^^;
そして、ebx=fd、ecx=buf、edx=countと代入してやるのです。
**********************************************
使い方は非常にシンプルなのですが、どのようなシステムコールが
あるか知らなければ、使うことはできませんね。
これまで使ってきたシステムコールはexit(=1)、read(=3)、
write(=4)でした。これらの番号はどのように知ったのでしょうか。
これは、「unistd.h」というヘッダファイルに定義されています。
ちなみに、「unistd = unix standard」です。
お使いのLinuxによってどこにあるかわかりませんが、僕の環境の
場合、以下のディレクトリにありました。
もし見つからない場合、findコマンドで検索でもしてみてください。
/usr/include/asm/unistd.h
なるほど。僕はx86ですから、恐らく__i386__マクロが定義されて
いるのでしょう。では、unistd_32.hを参照することにします。
とりあえず、利用しやすくするためにシンボリックリンクを
張っておきます。
さて、中身はどうなっているのでしょうか?
ちなみに、これは
$ cat unistd.h
で出力しました。
おお。システムコールってこんなにもあるんですね。
これはC言語ようにマクロが定義されているのですが、C言語は
最終的にアセンブリに翻訳されるので勿論アセンブリでも使えます。
「__NR_~~」の赤字の部分がシステムコールの名前、
その後の数字がシステムコール番号です。
では、試しにwriteシステムコールの番号を見てみましょう。
grepコマンドを使うと、文字列の中から必要な部分を検索して
抽出できます。縦棒|(パイプ)は説明すると少々面倒なので…。
そういえば、このようなコマンドの詳細な解説記事はほとんど
なかったですね。そろそろ書くか、hiroumauma君に投げます(笑)
さて、本題ですが、pwriteとかwritevとかもありますが、
目的のwriteは4番ですね。
**********************************************
これでシステムコールの番号を調べることができるようになりました。
しかし、そもそも必要としている機能を提供するシステムコールの
名前がわからなければどうしようもありませんね。。。
そこで役立つのが此方のサイトです。
システムコール一覧/Linux2.6
たまに説明が書かれていない(実装されていないシステムコールと
書いてある)ものもありますが、メジャー所はしっかり押さえてあります。
○ファイルディスクリプタ
今回は二つの新出システムコールがあります。openとcloseです。
ちょうど機能の調べ方を紹介したところですし、とりあえず
自分で調べてみましょう。
その中でぶち当たるであろう疑問、というより、この連載を書き
始めてからずっとあたかも当然の知識であるかのように扱ってきた
ファイルディスクリプタなるものがあると思います。
折角の機会ですので、説明します。
ファイルディスクリプタとは"file descriptor"、すなわち「ファイル
説明子」のことです。
**********************************************
もともとファイルとは、メモリ上に置かれているデータをOSが管理し、
ユーザーが扱いやすくしたまとまりのことです。これは感覚的に
理解していただけると思います。
そして、OSがファイルを管理するときにはファイル番号が必要です。
このときの、OSによって与えられる、「ファイルとユニーク対応に
なっている数値」のことをファイルディスクリプタといいます。
ユニークとは一意や一対一を意味しています。
また、多くのOSはいくつかのファイルディスクリプタが予約されて
います。
stdin(標準入力)、stdout(標準出力)、stderr(標準エラー出力)が
メジャーでしょう。DOSではstdprnという標準プリンタ出力があって、
「fprintf(stdprn, "Assembly\f\r");」とすれば「Assembly」と
書かれた紙が排出されていたのです。現在は使用不可ですよ。
*********************************************
標準入力…キーボード、マウスなどユーザーからのレスポンスを
受ける外部接続装置
標準出力…ディスプレイ、スピーカなどユーザーへのレスポンスを
提供する外部接続装置
標準エラー出力(知らなくてよい)…標準出力と似ているが、エラー出力用
Linuxでは暗黙的に標準入力に0、標準出力に1、標準エラー出力に
2が割り当てられているのです。そして、新たなファイルディスクリプタを
プログラムに要求される度に3、4、…を与えていくのです。
ちなみに、Windowsではファイルディスクリプタではなく、
ファイルハンドルという形で管理していたりします。
また、Unixではファイルディスクリプタはファイルだけに与えられる
のではなく、イベントやタイマー、パイプやソケットにも与えられ、
同様に管理されます。イベントとかタイマーとかは知らなくていいですよ。
さて、以上のファイルディスクリプタについてのことをまとめると、
以下のような図になります。
○ラベルと関数
ラベルと関数の区別が曖昧になっている気がするので、
少し話をしたいと思います。
実は0x110で嘘をついていました。globlディレクティブについてです。
.globlはプログラムのエントリポイント、すなわち開始地点を示します
本当はリンカというプログラムに「.globl ~~」で指定した
ラベルが存在することを教えるのです。
今から話すことは、実はこの連載で最も伝えたいことに関わる
部分なのですが、現時点は少々難しいので読み流してください。
読まないでいいというわけではないのですが。
**********************************************
コンピュータは勿論ラベルと関数の区別はつきません。
そもそも、どちらもメモリ上に配置されたただの実行コードの羅列です。
ですから、本来はアセンブルした時点でラベルは不必要になります。
ところが、プログラムは一つのソースコードをアセンブルしただけでは
完成することができないというのが実情で、複数のソースコードを
くっつけるのが基本なのです。
今までなんとなしにgccで一瞬で実行ファイルを生成してきましたが、
これはgccが内部でいろいろ頑張ってライブラリとくっつけているのです。
その中にはランタイムライブラリという、そもそもプログラムを
呼び出すために必要なライブラリもあり、これとくっつけなければ
プログラム自体が動かないのです。
こんな面倒な作業を引き受けているのがリンカです。
コンパイラやアセンブラは内部的に機械語に翻訳してリンカを呼びます。
今までずっと「.global main」でした。エントリポイントなら
別の名前でもいいじゃないかと思って変えた方もいるのでは
ないでしょうか。
試してもらえばわかりますが、アセンブルエラーが出ます。
/usr/lib/gcc/i486-linux-gnu/4.4.3/../../../../lib/crt1.o: In function `_start':
(.text+0x18): undefined reference to `main'
main関数を呼び出す記述が書かれていて、それと自作の部分を
くっつけると、生成された実行ファイルは十分に準備をしたうえで
crt1.oの部分を読み込み、それがmain関数を呼び出し、
自分で書いた部分が実行されるのです。
crt1.oはランタイムライブラリです。(C RunTime)
C言語の場合もcrt1.oが使われています。
なぜ「main」なのか?という疑問は誰しも抱くかもしれません。
そこにはこのような事情があるのです。
***************************************
元に戻しますが、.globalによって、アセンブルするときに
mainラベルを消さないでね、と伝えるのです。
そうすることで、リンカがcrt1.oとくっつけるときにきちんとmainを
探してくれます。
結局、ラベルも関数も違いはないのです。
どちらも、ここからひとまとまりの処理であるというだけです。
ラベルの方がアドレスとしての意味合いが強いです。
○プログラムの始まる前は
前回はスタックがテーマでした。今回は復習を兼ねつつ、新しい
内容を加えていきたいと思います。
さて、プログラムが開始するとき、前回の図ではespとebpの位置を
わかりやすく揃えておきましたが、勿論自分の作ったプログラムの
前もOS等別のプログラムが動いていたわけですから、
espとebpの位置がたまたま揃っていたなんてことは稀でしょう。
実際にはebpはもっと後ろ、正確に言うと「espよりメモリの上位部分」
にあるでしょう。
スタックはメモリの低位部分に向かって伸びていきますから、
ebpの"論理的意味"上、espより高位にあるのが自然な状態です。
**********************************************
それでは、プログラムが始まる前のことを考えてみましょう。
とりあえず、下図のような状態から考えることにします。
勿論、ebpは便宜的な位置ですので、もっと高位でも構いません。
ここからプログラムを呼びます。
コマンドライン引数というのを聞いたことがあるでしょうか。
例をあげます。cdコマンドはカレントディレクトリを変更するコマンド
ですね。これも勿論プログラムです。
$ cd ~/Desktop
これでデスクトップに移動します。このとき、cdのmain関数に
"~/Desktop"という引数を与えているのです。
main関数の引数は以下のようになります。
int main(int argc, char **argv, char **env);
第一引数argcはコマンドライン引数の個数です。コマンドライン引数は
スペース毎にひとつと認識されます。第二引数argvはコマンドライン
引数自体の配列です。つまり上の例では
argc = 2
argv[0] = "cd"
argv[1] = "~/Desktop"
なのです。プログラム名「cd」自体もコマンドライン引数である点に
注意してください。
また、C言語では滅多に利用されることはありませんが、第三引数の
envが存在するのです。これは、環境変数を配列にしたものが
入っています。環境変数が何かについてはこちらをご参照ください。
PATHはWindowsでも頻繁に利用され、非常に有名ですね。
今回は環境変数は関係ありません。次回にでも触れようと思います。
つまり、main関数の引数をプッシュするとこのようになります。
さらに、main関数をよぶと、戻りアドレスがプッシュされてジャンプ
します(call)。
○main関数のおまじない??
main関数のはじめにこんな二行があります。
push %ebp
movl %esp, %ebp
これ、どういう意味があるんでしょうか。
esp、ebpをいじっていますから、スタックを考えましょう。
今、スタックはmain関数が呼ばれた直後、こうなっていましたね。
ひとつ前と同じ図です。まずはebpをプッシュしています。
そして、espをebpに代入します。
これには次のような意味があります。
main関数の第一引数argcには、この状態から8(%esp)、
8(%ebp)の二通りでアクセスできますね。ところが、この二行が
ないと8(%ebp)は全く違うものを指します。
main関数を進んでいくうちに、スタックを利用する機会が訪れる
でしょう。プッシュなどを行いますね。
すると、ebpをespに揃えておかなかったときはいちいちプッシュした
データの個数を考えてx(%esp)のxを変えなければargcやargvに
アクセスできません。これは面倒です。
ebpをespに揃えておけば、どれだけプッシュしてespの値が
動こうとも、argcなら常に8(%ebp)、argvなら12(%ebp)で
アクセスできます。便利です。
他の関数でも同様で、関数に与えられた引数にアクセスしやすく
なるのです。
このような理由から(他にもありますが)、この二行を関数を呼んだ
直後に記述しておくことが多いです。勿論、引数を持たず、
必要がないと判断したら書かなくてもよいです。
**********************************************
ソースコード全体の説明は次回に行います。
それまでに、一度自分で考えてみましょう。
print関数とexit関数は前回のものを流用しています。
プログラム中でargcやargvにもアクセスしていますよ。
Tweet
こちらよりAmazonギフト券による支援を募集しております。
受取人のEメールに hirohorse2-suplbl(アットマーク)yahoo.co.jp を設定してください
少額でも非常に助かりますので、お気に召されました記事がありましたら、何卒ご支援の方よろしくおねがいします。
トラックバックURL
この記事へのコメント
アンセブリはとあるアニメと基本技術者の資格をもつおじさんに聞いてしりました
私はC言語を始めたばかりでアンセブリはよくわからないけど
記事を見るだけですごく難しいとわかりました
hiroumaumaさん
過去記事を読んでいて私も暗号プログラムをつくろうと思い作成してみたが
テキスト形式のものしか読み込めず
バイナリ形式のはファイルエラーとなります
hiroumaumaさんが作成された暗号プログラムのソースを配布してもらえたらと思います
過去記事を読んだらダウンロードリンクがありました
過去記事を読まずに投稿して
誠にすいませんでした
手の届かないところにあり
この記事を読んでも
全然理解できないのですが
OSを作成することには
物凄く興味があります。
なので
自分もチョットずつ勉強
しようかと思いました。
Hiroumaumaさんの知識は
一体どこから手に入れたのですか?