TOP / プログラミング / GNUアセンブラ

GNU アセンブラ入門(GAS)

はじめに

2005年2月27日ぐらいにGNUアセンブラ入門という記事を書いた。それから1年以上の月日が立ち、再度GNUアセンブラ入門を書いてみる。

開発環境

とりあえずgccをインストールしてください。Windows上でgccを使いたいならCygwinというアプリケーションがお勧めです。仮想ソフトウェアでKnoppixやLinuxのディストリビューションを入れるのもありです。

Unix/Linux環境での日本語入出力はこのサイトのOSXの記事を参考にしてください。

リンカ知ってる?

gccはC言語の開発環境と教わった人がいると思いますが、gccはグニューコンパイラコレクションでその名のとおり、C言語だろうがJavaだろうがコンパイルできます。それでC言語がどのようにコンパイル作業されるかを知っておく必要がある。

gccの流れ

以下のような流れ

  • C言語のソースコードを書く
  • プリプロセッサの実行
  • コンパイルの実行
  • アセンブラの実行
  • リンカの実行

それでgccというのは、コマンドひとつでこのプリプロセッサからリンカの実行まですべてを行ってくれるのである。

hello.c


#include <stdio.h>
int main(int argc, char *argv[]) {
        printf("hello world\n");
        return 0;
}

コンパイル作業の実行

実際にgccを-vオプションをつけて、プリプロセッサ、コンパイル、アセンブラ、リンカをおこなっているか確認してみる。


satoshiokita@debian:~/develop/as/tutorial$ gcc -v hello.c
Reading specs from /usr/lib/gcc-lib/i486-linux/3.3.5/specs
Configured with: ../src/configure -v --enable-languages=c,
(省略)
Thread model: posix
gcc version 3.3.5 (Debian 1:3.3.5-13)
 /usr/lib/gcc-lib/i486-linux/3.3.5/cc1 -quiet -v
  -D__GNUC__=3 -D__GNUC_MINOR__=3 -D__GNUC_PATCHLEVEL__=5 hello.c 
-quiet -dumpbase hello.c -auxbase hello -version -o /tmp/ccjCs4gN.s
GNU C version 3.3.5 (Debian 1:3.3.5-13) (i486-linux)
        compiled by GNU C version 3.3.5 (Debian 1:3.3.5-13).
GGC heuristics: --param ggc-min-expand=47 --param ggc-min-heapsize=32130
ignoring nonexistent directory "/usr/i486-linux/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/local/include
 /usr/lib/gcc-lib/i486-linux/3.3.5/include
 /usr/include
End of search list.
 as -V -Qy -o /tmp/ccgMI0Jx.o /tmp/ccjCs4gN.s
GNU assembler version 2.15 (i386-linux) using BFD version 2.15
 /usr/lib/gcc-lib/i486-linux/3.3.5/collect2 --eh-frame-hdr -m elf_i386 
 -dynamic-linker /lib/ld-linux.so.2
(省略) 
/tmp/ccgMI0Jx.o -lgcc --as-needed -lgcc_s --no-as-needed
 -lc -lgcc --as-needed -lgcc_s 
(省略) 

コンパイル作業の解説

沢山並んでいるが、注意深く見てみると以下のような処理が読み取れる。

  • /usr/lib/gcc-lib/i486-linux/3.3.5/cc1でプリプロセスからコンパイル。
  • as -V -Qy -o /tmp/ccgMI0Jx.o /tmp/ccjCs4gN.sでアセンブル。
  • /usr/lib/gcc-lib/i486-linux/3.3.5/collect2でリンク

つまり、gccというのは沢山のコマンドを1度に実行するのである。今回はアセンブラなので、プリプロセスとコンパイルという処理は必要ないので、asコマンドによるアセンブルとldコマンドによるリンクを行う。上記ではcollect2でリンクを行っているが、現時点ではldコマンドと変わらないとおもっていてよい。

helloプログラムを実行


satoshiokita@debian:~/develop/as/tutorial$ ./hello
hello world

とりあえず、自分の環境で試してみてください。

アセンブラの実行ファイルを作るまでをまとめると

  • エディタでアセンブラのソースコードファイルを作る
  • asコマンドでリンカに渡すオブジェクトファイルを作成
  • ldコマンドでオブジェクトファイルから実行ファイルを作成

asコマンドとldコマンドの実行の仕方

asコマンドでアセンブルをするとオブジェクトファイルを生成する。

書式:as -o オブジェクトファイル名 アセンブラソースコード

as -o hello.o hello.s

ldコマンドでオブジェクトファイルから実行ファイルを作る。

書式:ld -o 実行ファイル -Ttext 0x0 -e エントリポイント オブジェクトファイル

ld -o hello -Ttext 0x0 -e main hello.o

こんにちは、世界

HelloWorldプログラムを書いてみて基本構造を理解する

コメントとディレクティブ

#または/*を使う事でソースコード内にコメントを加える事が出来る。

ディレクティブとは、ピリオドではじまり、アセンブラ君にここはこのような意味だよ!と伝える。サンプルでは.code32,.text,.dataなどのディレクティブを指定しているので実際に確認してみてください。

01_struct.s


/*
 * GASのファイルのアセンブルの仕方と基本構造の説明です。
 */

/*
 * アセンブルの仕方
 *
 * as -a=リストファイル -o 出力オブジェクト 入力アセンブラファイル
 *
 * as -a=01_struct.lst  -o 01_struct 01_struct.s
 *
 * -aオプションでリストファイルを生成できます。これによりメモリマップの確認などが
 * 出来ます。実際にアセンブルして確認してみましょう。
 * 
 * ヘルプの表示
 * as -h
 */

# 1行のコメントの書き方。
/*
 * 複数行のコメントを書く場合はC言語やJava言語と同じです。
 * 
 */

/*
 * ディレクティブ
 * ディレクティブは、コロンではじめて、これ以降をアセンブラがどのように解釈するかを指示します。例えば以下の場合、
 * .code32ディレクティブで32bitのアセンブラを出力してくださいと指示しています。
 */
.code32
.text

  # .text内にコードを書いていく。

.data

  # データはこちらに書いていく。 

エントリポイントの追加

エントリポイントというのは、ここからプログラムを実行してよという場所の事。以下のように書く。mainラベルが.globalディレクティブで外部からも呼べますよと書いています。


.global main
main:

asコマンドでアセンブルをすると、オブジェクトファイルが作られます。このオブジェクトファイルというのは、ldコマンドの入力ファイルになります。なので先ほど指定したエントリポイントをldコマンドに渡す時に使います。


ld -o hello -Ttext 0x0 -e main hello.o

上記の例は、ldコマンドを実行して、hello実行ファイルをtextディレクティブで指定した部分を実行ファイルの0番目からmainというところから実行が始まるファイルをhello.oという入力ファイルから作ってください。という事になります。

02_hello.s


/*
 * GASでのHelloWorldプログラム
 */

/*
 * アセンブルの仕方
 *
 * as -a=リストファイル -o 出力オブジェクト 入力アセンブラファイル
 *
 * as -a=02_hello.lst  -o 02_hello 02_hello.s
 *
 * -aオプションでリストファイルを生成できます。これによりメモリマップの確認などが
 * 出来ます。実際にアセンブルして確認してみましょう。
 * 
 * ヘルプの表示
 * as -h
 */

# 1行のコメントの書き方。
/*
 * 複数行のコメントを書く場合はC言語やJava言語と同じです。
 * 
 */

/*
 * ディレクティブ
 * ディレクティブは、コロンではじめて、これ以降をアセンブラがどのように解釈するかを指示します。例えば以下の場合、
 * .code32ディレクティブで32bitのアセンブラを出力してくださいと指示しています。
 */
.code32
.text

/* .globalディレクティブでここからはじめますよ!とアセンブラに教えています。
 * .global mainのmain部分はラベルを指定します。つまりこの場合は、mainラベルへ進め!という
 * 意味になります。その後にすぐmainラベルを書いています。
 *
 * ラベル名:
 * main    :
 *
 * 書き方は、ラベル名にコロンです。アセンブラの場合はラベル名を作ってそこにジャンプして
 * 処理を進めていきます。ret命令は、returnの略でその名のとおり処理を戻します。
 * 今はとりあえず処理が終わるのだなと考えておいてください。
 */
.global main
main:
  ret

.data

  # データはこちらに書いていく。 

実際に実行命令を書いてみる

03_hello.s


/*
 * GASでのHelloWorldプログラム
 */

/*
 * アセンブルの仕方
 *
 * as -a=リストファイル -o 出力オブジェクト 入力アセンブラファイル
 *
 * as -a=03_hello.lst  -o 03_hello 03_hello.s
 *
 * -aオプションでリストファイルを生成できます。これによりメモリマップの確認などが
 * 出来ます。実際にアセンブルして確認してみましょう。
 * 
 * ヘルプの表示
 * as -h
 */

# 1行のコメントの書き方。
/*
 * 複数行のコメントを書く場合はC言語やJava言語と同じです。
 * 
 */

/*
 * ディレクティブ
 * ディレクティブは、コロンではじめて、これ以降をアセンブラがどのように解釈するかを指示します。例えば以下の場合、
 * .code32ディレクティブで32bitのアセンブラを出力してくださいと指示しています。
 */
.code32
.text

/* .globalディレクティブでここからはじめますよ!とアセンブラに教えています。
 * .global mainのmain部分はラベルを指定します。つまりこの場合は、mainラベルへ進め!という
 * 意味になります。その後にすぐmainラベルを書いています。
 *
 * ラベル名:
 * main    :
 *
 * 書き方は、ラベル名にコロンです。アセンブラの場合はラベル名を作ってそこにジャンプして
 * 処理を進めていきます。ret命令は、returnの略でその名のとおり処理を戻します。
 * 今はとりあえず処理が終わるのだなと考えておいてください。
 */

.globl main
main:

  /*
   * mov命令によりデータをレジスタに設定する。
   * movl 入力先 出力先
   */
  movl $4, %eax         # AXレジスタに数値4を設定 
  movl $1, %ebx         # BXレジスタに数値1を設定
  movl $msg,%ecx        # CXレジスタにmsgラベルを設定
  movl $5, %edx         # DXレジスタに数値5を指定

  # システムコールの実行
  int  $0x80

  ret

.data

  # データはこちらに書いていく。 
  msg: .ascii "hello"

解説


movl $4, %eax         # AXレジスタに数値4を設定 
movl $1, %ebx         # BXレジスタに数値1を設定
movl $msg,%ecx        # CXレジスタにmsgラベルを設定
movl $5, %edx         # DXレジスタに数値5を指定

mov命令は、データのコピーに利用する。movlやmovbなどがあるがこれは後で説明する。movl $4, %eaxの場合、数値4をAXレジスタにコピーするという意味である。

数値は$ではじまり、レジスタは%ではじめる。$msgはラベルmsg:の場所をCXレジスタにコピーしている。ラベルは人間が読みやすいように文字列にコロンをつけているが、実際にアセンブルする時は、メモリーの何番目という意味になる。

例えば、ラベルmsgがメモリーの5番目に格納されていたとすると、アセンブラ君は、movl $msg, %ecxを見た時に、

「コピーする命令のmovをつかうのか。それでどこの値をどこにコピーすればいいの?」

「そうか、$msgの値をCXレジスタに渡せばいいのか。」

「でも$msgってラベルmsg:のことだよなぁ」

「ってことは、ラベルmsg:のメモリーの場所を示しているのか。これって変数とかとおなじだ。」

$msgのメモリー番地を調べる。。

「あぁ、$msgって書いてるけど、メモリーの5番目ね。なんで人間は、数字で書かないのだろう?読みづらいよ。」

というような感じで解釈する。これで書き込み処理を実行する前の準備が終了した。

最後に、int 0x80で処理を実行する。このintをシステムコールといいます。


int  $0x80

関数のように書くとたぶんsystem_call(4, 4, $msg, 5)のような感じです。system_callという大きな関数がその名の通りシステム関連んの処理を行います。それで第一引数の部分で書き込み処理とかを選択するのです。

アセンブルと実行

実行命令をこのサンプルで書いたので、実際にasコマンドでアセンブルを行い、ldコマンドでリンクを行う。


satoshiokita@debian:~/develop/as/tutorial$ as -a=03_hello.lst -o 03_hello.o 03_hello.s
satoshiokita@debian:~/develop/as/tutorial$ ld -Ttext 0x0 -e main -o 03_hello 03_hello.o
satoshiokita@debian:~/develop/as/tutorial$ ./03_hello
helloSegmentation fault

./03_helloで実行している。最後にSegmentation faultが発生しているが、helloという文字列が出力されている事が分かる

04_hello.s


/*
 * GASでのHelloWorldプログラム
 */

/*
 * アセンブルの仕方
 *
 * as -a=リストファイル -o 出力オブジェクト 入力アセンブラファイル
 *
 * as -a=04_hello.lst  -o 04_hello 04_hello.s
 *
 * -aオプションでリストファイルを生成できます。これによりメモリマップの確認などが
 * 出来ます。実際にアセンブルして確認してみましょう。
 * 
 * ヘルプの表示
 * as -h
 */

# 1行のコメントの書き方。
/*
 * 複数行のコメントを書く場合はC言語やJava言語と同じです。
 * 
 */

/*
 * ディレクティブ
 * ディレクティブは、コロンではじめて、これ以降をアセンブラがどのように解釈するかを指示します。例えば以下の場合、
 * .code32ディレクティブで32bitのアセンブラを出力してくださいと指示しています。
 */
.code32
.text

/* .globalディレクティブでここからはじめますよ!とアセンブラに教えています。
 * .global mainのmain部分はラベルを指定します。つまりこの場合は、mainラベルへ進め!という
 * 意味になります。その後にすぐmainラベルを書いています。
 *
 * ラベル名:
 * main    :
 *
 * 書き方は、ラベル名にコロンです。アセンブラの場合はラベル名を作ってそこにジャンプして
 * 処理を進めていきます。ret命令は、returnの略でその名のとおり処理を戻します。
 * 今はとりあえず処理が終わるのだなと考えておいてください。
 */

.global main
main:

  /*
   * mov命令によりデータをレジスタに設定する。
   * movl 入力先 出力先
   */
  movl $4, %eax         # AXレジスタにsys_writeシステムコールを指定。
  movl $1, %ebx         # BXレジスタに出力先を標準出力として指定。
  movl $msg,%ecx        # CXレジスタにmsgラベルを設定
  movl $len,%edx        # DXレジスタにlenを指定

  # システムコールの実行
  int  $0x80

  ret

.data

  # データはこちらに書いていく。 
  msg: .ascii "hello"
  msgend: len = msgend - msg

msgendラベルを追加。len = msgend -msgで分かるように、msgendからmsgを引く事で"hello"の文字列数を計算している。これにより"hello"の文字を変更してもプログラムは動作する。実際に試してみてください。

レジスタってなに?

レジスタや数値や文字列が出てきたので、次は簡単なレジスタの説明。

本当は沢山レジスタがあるが、今は以下のような感じ


---- CPU ----------------------------
|                                   |
| [ 演算装置 ] --- [ Register A ]   | 
|                  [ Register B ]   |
|                  [ Register C ]   |
|                  [ Register D ]   |
|                                   |
-------------------------------------

CPUは簡単にすると、足し算や引き算など計算をする演算装置とデータを保存できる場所がある。演算装置はALUと言ってArithmetic and Logic Unitの略です。それでデータを登録するする場所をレジスタという。コンビニのレジとかも同じ意味ですね。それでレジスタは元々A,B,C,Dという順番で アキュムレータ(Accumulator)、ベース(base)、カウンター(counter)、データ(data)を意味します。 どのレジスタを保存に使ってもいいのですが、歴史的な作法として計算処理はAレジスタに入れるって感じになります。accumulateが累算器って意味からも計算に使用するのだろうと分かると思います。

それで、IntelのCPUの歴史を見ると、一番初めに4004というCPUを発売します。4bitCPUです。次に8080というCPUを出します。これが8bitCPUです。では、ちょっと図解でまとめてみましょう。


8080のイメージの一部
---- CPU ----------------------------
|                      8       1    |
| [ ALU      ] --- [ A:0000 0000]   | 
|                  [ B:0000 0000]   |
|                  [ C:0000 0000]   |
|                  [ D:0000 0000]   |
|                                   |
-------------------------------------

はじめの図を少し書き直してみました。ALUという計算する部分と8bitのレジスタがありますね。 さらに時代は流れて16bitCPUが登場します。有名な8086です。この時にレジスタを拡張(eXtend)してレジスタを16bitにします。なのでAX, BX, CX, DXというレジスタの名前になります。


8086のイメージの一部
---- CPU ---------------------------------------
|                      16         8       1    |
| [ ALU      ] --- [ AX:0000 0000 0000 0000]   | 
|                  [ BX:0000 0000 0000 0000]   |
|                  [ CX:0000 0000 0000 0000]   |
|                  [ DX:0000 0000 0000 0000]   |
|                                              |
------------------------------------------------

さらに集積してCPUは32bitの時代に突入します。レジスタ名も拡張(Extend)してEAXレジスタなどと呼ぶようになります。もう名前めちゃくちゃですね。eXtendにExtendですから。。。


i386のイメージの一部
---- CPU -----------------------------------------------------------
|                      32                  16         8       1    |
| [ ALU      ] --- [EAX:0000 0000 0000 0000 0000 0000 0000 0000]   | 
|                  [EBX:0000 0000 0000 0000 0000 0000 0000 0000]   |
|                  [ECX:0000 0000 0000 0000 0000 0000 0000 0000]   |
|                  [EDX:0000 0000 0000 0000 0000 0000 0000 0000]   |
|                                                                  |
--------------------------------------------------------------------

昔は、8bitのレジスタに対して計算をするので非常に簡単でしたが、どんどん進化する過程で32bit計算用の命令とかをどんどん追加していきます。上の図解では、i386のイメージの一部と書いていますが、Intel386 CPUの略です。4004,8080,8086,80186,80286,80386とCPUが進化してきて、80386をi386とか言ったりします。今のCPUは、Pentium 4とかですよね。元々PentiumというCPUもありました。Pentaはアメリカの国防総省のペンタゴンと同じで、数字の5を表すので、80586の事。i586ですね。その後Pentium2,3,4と進化していきます。単純に言ったらi886って感じですね。なぜかi886とは言わないのですが。。ここで重要なのはIntelのCPUはいろいろ新しい命令を加えて進化させても、過去との互換性を保っている事です。(知っている人は、PentiumProが。。とか突っ込み入れないで)

最近は、64bitCPUや64bitのOSが一般家庭にも普及してきています。少なくとも64bitCPUっていったらレジスタが増えるのが分かると思います。

どんな言葉を喋る?

また、IntelのCPUはIntelの機械語を話します。Windows用のアプリケーションは最終的にIntelの機械語を話すようにプログラミングをするのです。ではアップルコンピュータのマシンはどうでしょう?これはPowerPCというCPUを搭載していて、PowerPCの機械語を話します。なのでアップルコンピュータのOSであるOSXを直ぐにWindowsが載っていたPCにインストールする事が出来ないのです。

ちなみに、Intelの機械語はx86とか呼ばれたりするようです。AMD,VIA,Cyrixなどがこの言葉を話します。携帯電話だったら組み込みのARM社などの言葉を話します。プレイステーション3や任天堂の同レベル機種は、アップルコンピュータと同じ言葉を話します。最近はJavaで有名なSunはSpercというCPUを作っているのでSolarisがなかなかIntelのマシンで動かなかったわけですね。

mov命令を見てみる。

051_mov.s


/*
 * mov命令のサンプル
 as -o 051_mov.o 051_mov.s
 ld -Ttext 0x0 -e main -o 051_mov 051_mov.o
 */
.code32
.text
.global main
main:
/*
EAX (Extend Accumurator eXtend) は32bit

32                  16         8       1bit
 0000 0000 0000 0000 0000 0000 0000 0000    %eaxは、32bitの操作をするときに使う
                     0000 0000 0000 0000    %ax は、16bitの操作をするときに使う
                     0000 0000              %ah は、上位8bitの操作に使う
                               0000 0000    %al は、下位8bitの操作に使う

*/

/*
%alレジスタ、つまりEAXレジスタの下位8bitに数値1を入れる
 8bitは1byteであるので、mov命令は、movbと書く。
*/
  movb $0b00000001, %al # 2進数の場合は$0bではじめる。al(Accumurator Low)
  movb $00000001  , %al # 10進数の場合は$ではじめる。
  movb $0x01      , %al # 16進数の場合は$0xではじめる。

# %ahレジスタ、つまりEAXレジスタの上位8bitに数値3を入れる
  movb $0b00000011, %ah
  movb $00000003  , %ah
  movb $0x03      , %ah



/* axレジスタの操作。
 32bitのうち、下位16bitを操作する時は、レジスタにaxと指定して使う。
 この場合、16bitは2bytesなので、mov命令は、movwと書く。
 wはwordの略で、wordとは2bytesを表す。
 */
  movw $0b0001000100010001, %ax

/* eaxレジスタの操作。
  32bitを操作する時は、レジスタにeaxと指定して使う。
  この場合、32bitは4bytesなので、mov命令は、movlと書く。
  lはlongの略で、longとは4bytesを表す。
*/
  # bit  32              16       8      1
  movl $0b11110000111100001111111111111111, %eax

  # 通常は、2進数ではなく16進数で記述する。
  movl $0xf0f0ffff, %eax

/* マイナスの値も使える */
  movb $-0xff, %ah
  movb $-011,  %ah

/* 文字列出力をしようとするとリンクできない。
  movb $0x04,  %al
  movb $0x01,  %bl
  movb $msg,   %cl
  movb $len,   %dl
  int  $0x80
*/

  # exitシステムコール
  movl $0x00000000, %eax
  movl $0x01, %eax
  int  $0x80

.data
  msg: .asciz "calc"
  med: len = med - msg

文字の変更

HelloWorldプログラムでアセンブラで文字列を出力する事が出来た。このデータをプログラムで変更できれば、プログラムの途中でレジスタの値などを出力できたりするようになれる。いままでのプログラムを改造してデータの値を変更して出力してみる。

05_hello.s


/*
 * 文字の変更
 * as -a=05_hello.lst  -o 05_hello.o 05_hello.s
 * ld -Ttext 0x0 -e main -o 05_hello 05_hello.o
 */
.code32
.text

.global main
main:

  movl $4, %eax         # AXレジスタにsys_writeシステムコールを指定。
  movl $1, %ebx         # BXレジスタに出力先を標準出力として指定。
  movl $msg,%ecx        # CXレジスタにmsgラベルを設定
  movl $0x41, msg     # msgのデータを16進の0x41つまり文字列Aに書き換える
  movl $len,%edx        # DXレジスタにlenを指定


  # システムコールの実行
  int  $0x80

#  ret
  mov  $1, %eax
  int  $0x80

.data
  # データはこちらに書いていく。 
  msg: .asciz "Z\n"
  msgend: len = msgend - msg

解説


  movl $0x41, msg

msgラベルを$マークなしに指定すると、直接メモリー番地を指定する事になる。このような直接メモリ参照を使う事でデータを操作する。16進数の0x41はASCIIコードのAを表すのでmsgラベルの先頭にAを上書きする。


  mov  $1, %eax
  int  $0x80

AXレジスタに数値1を指定して、システムコールをしている。数値1はexitを意味するのでこれが実行される事でプログラムが終了する。


  msg: .asciz "Z"

.ascizディレクティブを使用している。これは、.ascizディレクティブは文字の最後にNULコードを意味する0を付加します。上記の場合Zという文字と0の2バイト文メモリを確保している事になります。.ascizディレクティブはC言語との互換性を保つためにあるようです。また.asciz同じ意味の.stringディレクティブもあります。

callによる関数作成

call命令により単純な処理をするサンプル。これで単純な関数の構造が分かると思う。

06_hello.s


/*
 * 文字列の変換を関数にして見る
 * as -a=05_hello.lst  -o 05_hello.o 05_hello.s
 * ld -Ttext 0x0 -e main -o 05_hello 05_hello.o
 */
.code32
.text

.global main
main:

  movl $4, %eax         # AXレジスタにsys_writeシステムコールを指定。
  movl $1, %ebx         # BXレジスタに出力先を標準出力として指定。
  movl $msg,%ecx        # CXレジスタにmsgラベルを設定
  call conv_func        # conv_funcラベルに飛ぶ。
  movl $len,%edx        # DXレジスタにlenを指定


  # システムコールの実行
  int  $0x80

  mov  $1, %eax
  int  $0x80

/*
 * 文字列の変換関数。
 */
conv_func:
  movl $0x42, msg     # msgのデータを16進の0x42つまり文字列Bに書き換える
  ret

.data
  # データはこちらに書いていく。 
  msg: .asciz "Z\n"   # 初期値はZと改行コードにしている。
  msgend: len = msgend - msg

関数の修正

call命令でconv_func関数を呼ぶ前に、push $0x43で値を格納している。また関数内でespレジスタやebpレジスタなどのデータをやり取りしているのが分かると思う。call命令やret命令などの説明をする前に、実際にこのプログラムを打ち込んで動作確認してもらいたい。

07_hello.s


/*
 * 文字列の変換を関数にして見る
 * 関数は、1つ引数をもらって、その値を出力する。
 * as -a=05_hello.lst  -o 05_hello.o 05_hello.s
 * ld -Ttext 0x0 -e main -o 05_hello 05_hello.o
 */
.code32
.text

.global main
main:

  push $0x43            # 0x43つまり文字列Cをスタックに入れる。これが引数になる。
  call conv_func        # conv_funcラベルに飛ぶ。



  mov  $1, %eax
  int  $0x80

/*
 * 文字列の変換関数。
 */
conv_func:

  movl %esp, %ebp
  movl 4(%ebp), %edx
  movl %edx, msg

  movl $4,  %eax        # AXレジスタにsys_writeシステムコールを指定。
  movl $1,  %ebx        # BXレジスタに出力先を標準出力として指定。
  movl $msg,%ecx        # CXレジスタにmsgラベルを設定
  movl $len,%edx        # DXレジスタにlenを指定

  int  $0x80            # システムコールの実行
  ret

.data
  # データはこちらに書いていく。 
  msg: .asciz "Z\n"   # 初期値はZと改行コードにしている。
  msgend: len = msgend - msg

08_stack.s


.code32
.text
.global main
main:
  push $0x01           # pushっていうけどどこにデータを格納している?
  pop  %eax
  # exitシステムコール
  movl $1, %eax
  int $0x80

pushとpop

pushは、スタックという場所にデータを登録します。スタックはSPで表します。SPはスタックポインタの略です。ポインタは、マウスポインタとか言うように矢印を意味します。当然32bitCPUなので、拡張(Extend)しているのでESPと表記します。スタックはダンボール箱に本を平済みしていくような感じです。ダンボール箱に平積みしたら、一番初めに入れた本は後から入れた本より先に取り出せませんよね?そういう構造はFIFO(First In First Out)とも言います。


book1の上にbook2を重ねて入れているので、book2を取り出さないとbook1は取れない。
  |         |
  |---------|
  || book2 ||
  |---------|
  || book1 ||
  -----------

book2をpop命令で取り出す。
  |         |
  |         |  ---------
  |         |  | book2 |
  |---------|  ---------
  || book1 ||
  -----------

book3をpush命令でespに突っ込む。
  |         |
  |---------|
  || book3 ||
  |---------|
  || book1 ||
  -----------

pushとpop命令がスタックポインタ(ESP)を操作するのは理解できたと思う。それでESPはポインタというだけあって、データがどこまで入っているかを表します。


book1の上にbook2を重ねて入れているので、book2に矢印がある
  |         |
  |---------| <--- %esp 
  || book2 ||
  |---------|
  || book1 ||
  -----------

ここで重要なのが、pushやpopをする事で%espが-4または+4されるという事。


book2をpop命令で取り出す。espは、esp + 4となる。
0 |         |
4 |         |  ---------
  |         |  | book2 |
8 |---------|  ---------  <--- %esp (= esp + 4)
  || book1 ||
  -----------

book3をpush命令でespに突っ込む。 espは、esp -4 となる。
0 |         |
4 |---------| <--- %esp (= esp -4 )
  || book3 ||
8 |---------|
  || book1 ||
  -----------

callとret

先ほど、pushとpopを説明したが、これをちょっと応用したのがcallとretです。呼ぶと戻るに日本語訳できると思います。これを使う事で関数の呼び出しのようにするのです。


call test_func
   ...

test_func:
    # 関数の処理を書く
  ret

call命令は、呼び出し先のラベルが必要になります。この例の場合は、test_funcラベルを渡しています。それでtest_funcというのは、単純に人間がメモリー番地を読みやすくするためのものでした、実際には、メモリー番地が書かれていることになります。


# 例えば、test_funcがメモリー番地0x0012だとしたらこんな感じ。
call 0x0012
   ...

0x0012
    # 関数の処理を書く
  ret

それで、call命令は、2つの命令を1つにまとめたものです。まずスタックに現在の場所を格納します(PUSH 0x0010のような感じ)。そのあとに、0x0012へjmp命令を実行するのです。

ret命令も同じように、call命令でスタックに現在の場所を格納していたので、これを取り出して、(POP) そこにjmp命令を実行します。これで関数から戻れますよね?

つまり、callとretは、pop/pushとjmp命令を1度に出来るのです。

あとbookを出し入れした時にespが+-4移動するといいましたが、これはプログラムに自動にやらせます。例えば本が沢山あって、-20したいとかなった場合は、ebpに一度espの値をコピーして操作します。これで、07_hello.sのソースコードも読み解く事が出来るはずです。

ここまでのまとめ

CPU

ここまでで学んだ部分のCPUイメージ


CPUのイメージの一部
---- CPU -----------------------------------------------------------
|                      32                  16         8       1    |
| [ ALU      ] --- [EAX:0000 0000 0000 0000 0000 0000 0000 0000]   | 
|                  [EBX:0000 0000 0000 0000 0000 0000 0000 0000]   |
|                  [ECX:0000 0000 0000 0000 0000 0000 0000 0000]   |
|                  [EDX:0000 0000 0000 0000 0000 0000 0000 0000]   |
|                                                                  |
|                                                                  |
|                  [ESP:0000 0000 0000 0000 0000 0000 0000 0000]   |
|                  [EBP:0000 0000 0000 0000 0000 0000 0000 0000]   |
|                                                                  |
--------------------------------------------------------------------
名称 説明
ALU 演算をする装置。足し算引き算などの四則演算やAND,ORなどの論理演算などを行う。
EAX Extend Accumulator eXtendの略。計算値の入出力の器として使う。
EBX Baseは、相対アドレスのベースに使われる。
ECX Counter. ループ処理のカウントに使われる。
EDX
ESP Extend Stack Pointer. スタックのどこを示しているか確認する時に使う。自分のアセンブラプログラムから変更する事はしない。変更したい場合は、EDPに値をコピーして行う。
EDP Extend Base Pointer. スタックのどこを指しているかを確認する時に使う。スタックの値変更などに使用する。


イバラキングへのリンク Get Firefox Valid XHTML 1.1 Apple Darwinへのリンク