アセンブラに手を出してみる

  • 93
    いいね
  • 0
    コメント

がっつりとやるつもりはありませんが、もしかしたら読む必要が出るかもしれないので少しだけお勉強。
もともと仕組みを知ることが好きなので読み始めると結構面白いです。

(ほんとはarmのほうが知識としては必要なんだけど、それはまた後日調べる)

はじめに

今回まとめているのはx86x86_64アーキテクチャに関するものです。
armなどはまた異なったものとなります。

また、構文もいくつかあるようで、それぞれ AT&T構文Intel構文というようです。 

構文の違い

%raxなどのように、レジスタに%がついていたりするのはAT&T構文です。
ついていないものはIntel構文です。

また、セクションの定義など細かいところで色々と差異があるようです。

ソース・ディスティネーション

上記構文の中で特に覚えておかないと混乱するのがこの「ソース」と「ディスティネーション」の順番です。

例えば、raxレジスタに1を格納するのをそれぞれ見てみると以下のようになります。

; for intel syntax
mov eax, 1
# for AT&T syntax
mov $1, %eax

intel構文では左がディスティネーション、右がソースです。(AT&Tではその逆)
ディスティネーションは計算結果が格納されるレジスタです。
ソースはレジスタに影響を与える値で、ソースにレジスタを指定した場合でも、ソース側のレジスタは変更されません。

コンパイラ

構文の違いはコンパイラの違いでもあります。
gccなどでコンパイルする場合、 GAS (GNU Assembler)、 nasmでは NASM (Netwide Assembler)とそれぞれ異なります。
(このあたりの細かい違いはまだよくわかっていませんが、とりあえずこれを覚えておかないと調べて出てきた構文が微妙に違うので混乱します( ;´Д`))

ちなみにこちらの記事がその比較を行っていて分かりやすいです。

命令(ニーモニック)の例

アセンブラではあまり多くの命令はありません。
また、1行の処理がCPUに対するひとつの命令になります。
そういう意味でも、CPUができることだけを記述できる、というわけですね。

コメント

命令ではないですが、アセンブラではAT&T構文の場合は;、intel構文の場合は#以降がコメントとして扱われます。

mov

movは値をコピーする命令です。

mov rax, rbx

add

addは値を加算します。
ただ、普通の言語のようにa = b + cみたいな感じではなく、add rax, rbxと書くと、raxrbxの値を足す、という動作になります。
なので、例えば5 + 3という簡単な式を実現するには以下のようにします。

mov rax, 5
mov rbx, 3
add rax, rbx

sub

subは引き算です。
基本的な使い方はaddと同じで、結果がレジスタに保存されるのも同じ動作です。

mov rax, 5
mov rbx, 3
sub rax, rbx

mul

mulは掛け算(Multiple)です。
ちなみにどうやらmulの動作は他と微妙に異なっているようです。
具体的には以下。

mov rax, 5
mov rbx, 3
mul rbx

addsubではオペランド(rbxとかのところ)が2つでしたが、mulの場合はひとつしかありません。
これは 掛け算はraxレジスタが使用されるという「暗黙の了解」 があるようです。
(ただ、参考にした記事がeaxと書いていたのでx64のCPUではもしかしたら動作が違うかもしれません)

div

divは割り算です。

mov rdx, 0
mov rax, 5
mov rbx, 3
div rbx

mulと同様に、divはオペランドがひとつだけとなります。
割り算でもraxが暗黙に使われるようです。
また、最初の行でrdx0を格納していますが、これは、割り算のあまりが自動的にrdxに保存されるためのようです。
そのために、予め0で初期化しておく、というわけです。

そしてこれらの動作は インテルプロセッサの仕様 のようです。
なのでそれ以外のCPUではまた違った仕様になっているはずです。

loop

インテルプロセッサは、loopという命令を見つけると指定回数分、ラベルに制御をジャンプさせます。

そしてこの「指定回数」を指示するにはレジスタを利用します。
rcxレジスタがその役割を持ちます。cはカウントのcです。

mov rcx, 5
mov rax, 0
mov rbx, 3

LOOPLABEL:
add rax, rbx

loop LOOPLABEL

などとすると、raxrbx3を5回足す、という処理になります。

DWORD PTR

アセンブラのコードを見ていると見かけるこの DWORD PTR
これは対象の値(やレジスタ)を何バイトとして扱うか、ということを修飾するもののようです。

  • DWORDは4バイト
  • WORDは2バイト
  • BYTEは1バイト

を意味する修飾子となります。

レジスタは何バイト(何ビット)なのかを明示しないとならないようです。
そのため、例えば即値を指定する場合何バイトのデータなのかを明示する必要があります。
こうした、何バイトのデータか、を明示するために使用するのが上記の修飾子です。

mov [AFLAG], BYTE PTR 1

という理解であってるかな?( ;´Д`)

その他

ニーモニック 意味 機能
LD LoaD メモリーからCPUへデータを読み込む
ST STore CPUからメモリーにデータを書き出す
LAD Load ADdress CPUのレジスタに値を設定する
PUSH PUSH スタックにデータを書き出す
POP POP スタックからデータを読み込む
SVC SuperVisor Call I/Oとの入出力を行う

メモ

とりあえず、忘れそうな、そしてあとで使いそうなことをつらつらとメモ。

レジスタ

レジスタはアルファベットで表示されます。(raxとか)
これがなにを意味しているのか、略称になっているので分かりづらいですね。
Wikipediaによると以下の意味のよう。

ビットの拡張

ソフトウェア資産の有効活用を目的として、16ビットプロセッサの命令セットをそのまま動作できる32ビットプロセッサなどがしばしば開発される。
この場合、プロセッサ内部のレジスタのビット長は大きく(たいていの場合2倍に)なっているのだが、互換性を保つために古いCPUの命令コードで動作する場合には下位のビットしか用いない。
インテル社の8086系列のCPUは、このように拡張してきた経緯を持つ代表的なプロセッサである。8086CPUが誕生する前のインテルの8ビットCPU、8080では汎用レジスタを“a”, “b”, “c”…と名付けていた。これを拡張した8086の汎用レジスタは“ax”, “bx”, “cx”…となった。(xはextendの略)ところが、80386で32ビット化したため、レジスタの名前は“eax”, “ebx”", “ecx”…となった。(eもextendの略)さらに、AMD社がAMD64で64ビットに拡張した時には、レジスタ名は“rax”, “rbx”, “rcx”…とになった。

ちなみにraxとかのrは、Yahoo!知恵袋の回答を見ると以下のよう。

昔の A レジスタ(アキュームレータ)、これは8ビット。そこから、AX レジスタとして16ビットになり、EAX で32ビット。そしてx64 では RAX として64ビットと、随分と進化したものですね。それでレジスタの名前が 'Register' に由来する 'R' と。何か感慨深いものがあります。

また、abcとアルファベットの連続になっているようですが、どうやらそれぞれちゃんと意味のある単語の頭文字のよう。

汎用レジスタ

  • AX アキュムレータ
  • BX ベースレジスタ
  • CX カウントレジスタ
  • DX データレジスタ

インデックスレジスタ

  • SI ソースインデックス
  • DI ディスティネーションインデックス

特殊レジスタ

  • BP ベースポインタ
  • SP スタックポインタ
  • IP インストラクションポインタ

セグメントレジスタ

  • CS コードセグメント
  • DS データセグメント
  • ES エクストラセグメント
  • SS スタックセグメント

0x90

どうやらアセンブラでは「空」を意味するものらしい。
.align 4, 0x90とか出てくるところがあるけど、おそらく4バイト境界にアライメントして、空で埋める、的なこと?

関数

引数

x86_64では6個くらいまではレジスタで渡すみたい。
それ以上の場合はスタックに積んで渡すらしい。
(x86の32bitではレジスタが少ないからより多くスタックに積んで引数として渡すらしい)

戻り値

関数の戻り値はraxに入れて返す、ということのよう。

アドレスアクセス

アドレスを指定してデータを移動したりする場合は[]を利用します。

mov al, [esi]

alレジスタにesiレジスタが保持しているアドレスの情報を移動する、という命令です。

アドレス操作

C言語のポインタと似たような感じでアドレスを操作することができるようです。

mov al, [esi]
mov [edi], al
add edi, 1
add esi, 1

alesiが保持しているアドレスのデータを移動し、次にediが保持しているアドレスにalを書き込んでいます。
そしてediesiのアドレスをひとつ進める意味でadd edi, 1というように記述しています。

スタック

スタックは、スタックオーバーフローで有名なスタックですw
アセンブラでは、スタック用に領域が確保され、その先頭アドレスがスタック用レジスタに格納されます。
(スタック用のレジスタはrsp(スタックポインタ)が利用される)

そしてスタックは、各関数の呼び出しごとに決まった容量だけ確保されます。
これが大事なポイントで、「決まった容量」しかないにもかかわらず、CPUはどれだけスタック領域が割り当てられているか知りません。

なので命令されただけスタックに積み上げていき、それが一定値を超えると「スタックオーバーフロー」が発生します。

ちなみにスタックポインタの利用方法は、関数が呼ばれるたびにスタックに引数や、戻り先アドレスなどがどんどんとpushされていきます。
そして関数のリターン時に、スタックに積まれた情報を元に処理する位置を移動する、という処理を行います。

スタックフレーム - 関数に渡される引数を知る | Web/DB プログラミング徹底解説にスタックの状態の話が載っていました。

それを読んでの理解としては以下です。

  1. 関数呼び出し時、引数は右から左に向かってスタックに積まれる
  2. rbpレジスタの内容をスタックにPUSHする(これはなんでかまだ分かりません;)
  3. ローカル変数(自動変数)がある場合はさらにスタックに積む

という流れになります。
例えば以下のようなCコードがあったとして、

int add(int a, int b) {
    int c = a + b;
    return c;
}

int bint aの順にスタックに積まれ、rbpの値がスタックに積まれ、最後にint cの変数がスタックに積まれます。
rbpは関数開始時のスタックポインタの位置)

図にすると以下のようになります。

0x00000h  +----> +           +
                 |           |
                 |           |
                 +-----------+
                 |   int c   |
                 +-----------+
                 |    rbp    |
                 +-----------+
                 |   int a   |
                 +-----------+
                 |   int b   |
                 +-----------+

なお、スタックは大きいアドレスから小さいアドレスに向かって伸びていきます。
なので、rbpよりマイナス方向(つまり小さい方向)にあるスタックはローカル変数、逆にプラス方向(つまり大きい方向)にあるスタックは引数、と見ることができます。

ちなみに、これはx86の仕様で、x64ではレジスタ数が増えたため(?)、引数を無理にスタックに積むのではなく、いくつかのレジスタに直に保持されて関数が呼び出されるようです。

下記に記載している「システムコールに使われるレジスタ」のところで、どのレジスタが使われるのかを書いているのでそちらも参照ください。

呼び出し規約

アセンブラには呼び出し規約というものがあるようです。
つまり、関数の呼び出し時に、呼び出し元と呼び出し先でそれぞれやらなければならない処理が決まっている、ということです。

これを守らないと破壊してはいけないレジスタの内容を破壊したり、といったことになります。(という認識であってるかな?)

(ちなみにどうやらこの規約を ABI(Application Binary Interface) と呼ぶようです)

こちらの記事から引用させてもらいます。

呼び出す側の規約

次に上記規約を満たす関数(func)があったときに,funcをasmから呼び出す手続きについて述べます.asmでは次の規約に従って関数を呼び出してください.

  • funcの引数を右側からpushする.
  • call funcする(Xbyakではcall(int(func));).
  • 引数の数 x 4だけespを大きくする.
  • 関数の返り値はeaxに入っている.void型の場合はeaxの値は不定. たとえば
int func0(int a, char *b, double *c);

の場合は

push(ポインタcの値);
push(ポインタbの値);
push(aの値);
call(int(func0));
add(esp, 3 * 4); /* 引数が3個なので3 * 4 */

とします.引数がなければpushとaddは省略できます.たとえば

void func();

の場合は

call(int(func));

となります


呼び出される側の規約

Cから呼び出される関数は次の規約に従ってください.

  • 返り値はeaxに代入する(返り値がvoidならeaxの値は不定).
  • 汎用レジスタのうちebx, esi, edi, ebp, espの値は関数を抜けるときに呼び出されたときの状態に戻す.ecx, edxの値を保存する必要はない.
  • ret();で関数から戻る. このとき,関数の引数は「esp + 左から引数が何番目にあるか(1オリジン) * 4」のアドレスに格納されています. 関数の引数についてもう少し詳しく説明します.例としてfunc0()を考えましょう.

func0の呼び出し手続きに従ってfunc0が呼ばれたときのスタックの状況を考えます.c, b, aと順にpushされたのでスタックには大きい方から小さい方へc, b, aと並んでいるはずです.ということは

esp + 8 : c
esp + 4 : b
esp + 0 : a

なのでしょうか.実はちょっと違いまして,callを実行したとき,こっそりと実行していた命令の次の命令の先頭アドレスがスタックに格納されています.つまり正解は

esp + 12: c
esp + 8 : b
esp + 4 : a
esp + 0 : 次の命令のアドレス

呼び出し規約2

なお、こちらの記事を引用させてもらうと以下のような規則もあるみたい。
(こっちはx64の規約かな?)

  • RCX、RDX、R8、R9 は、左から右にこの順序で整数およびポインター引数に使用されます。
  • XMM0、1、2、および 3 は浮動小数点引数に使用されます。
  • 追加の引数は、左から右へスタックにプッシュされます。
  • パラメーターの長さが 64 ビット未満の場合、ゼロ拡張されず、上位ビットは不定です。
  • 関数を呼び出す前に、呼び出し元で (必要に応じて RCX、RDX、R8、および R9 を格納するための) 32 バイトの “シャドウ領域” を割り当てなければなりません。
  • 呼び出し後に、呼び出し元でスタックをクリーンアップしなければなりません。
  • (x86 と同じように) 整数型の戻り値は、64 ビット以下の場合は RAX に格納され返されます。
  • 浮動小数点型の戻り値は、XMM0 に格納され返されます。
  • サイズの大きな戻り値 (構造体など) は、呼び出し元によりスタックに領域が割り当てられ、呼び出し先が呼び出されるときにリターン領域へのポインターが RCX に格納されます。そして、整数パラメーターで使用されるレジスターは右に 1 つプッシュされます。RAX はこのアドレスを呼び出し元に返します。
  • スタックは 16 バイトでアライメントされています。”call” 命令は 8 バイトの戻り値をプッシュするため、リーフでない関数はスタック領域を割り当てる際に、値を 16n+8 形式で指定してスタックを調整する必要があります。
  • RAX、RCX、RDX、R8、R9、R10、および R11 レジスターは不定で、関数呼び出し時に破棄されると考えなければなりません。
  • RBX、RBP、RDI、RSI、R12、R13、R14、および R15 は、これらを使用する関数で保存されなければなりません。
  • 浮動小数点 (つまり MMX) レジスターの呼び出し規約はありません。
  • 詳細 (可変個引数、例外処理、スタックの巻き戻しなど) は、Microsoft* のサイトを参照してください。

ディレクティブ

.から始まる命令を「ディレクティブ」と呼びます。
.sectionなど。ただしこれはAT&T構文)

インストラクション

いわゆる命令のことかな?

****q

pushqなどのqは64bitを表しているようです。
pushqpopqmovqなど64bitでの処理をする場合はqを付けます。

****l

movlなどのlが付くのは32bitの処理をする命令です。

その他大事そうな命令

こちらの記事の引用です。

5.1.3.6. call

 call a は関数呼び出しの命令です。ラベルaの関数を呼び出します。具体的な動作はcallの次のアドレスをスタックに積んでラベルaを実行します。

5.1.3.7. leave

 leaveは、関数終了処理の命令です。%rbp を %rspに移動し、スタックから%rbpに値を取り出します。

アドレスオペランドの文法

AT&T記法では以下のように記述します。
X86アセンブラ/GASでの文法を引用させてもらいました)

movl    -4(%ebp, %edx, 4), %eax  # 完全な例: (ebp - 4 + (edx * 4))のアドレスの内容をeaxに転送する
movl    -4(%ebp), %eax           # よくある例: スタックの値をeaxに転送する
movl    (%ecx), %edx             # オフセットのない場合: ポインタの指す内容をレジスタに転送する
leal    8(,%eax,4), %eax         # 算術演算の例: eaxに4を掛け8を足したアドレスをeaxに格納する
leal    (%eax,%eax,2), %eax      # 算術演算の例: eaxの指す値を3倍したアドレスをeaxに格納する

なお、 Intel構文では以下のようにします。

[ebp - 4 + (edx * 4)]

.len: equ $ - msg

なんかたまに見かけるこんな感じのやつ。

NASM Hello World Segmentation Fault - Stack overflowで同じことを質問している人がいた。

そこから引用させてもらうと、

How does the statement .len equ $-msg work? I understand this is the length of the string. I also know that equ is like #define in C. So this variable does not exist in memory, it is put in place by the assembler. (nasm)

つまり、equ#define的な動作をするみたい。
また、

$ represents the address of the current line. So the following:

とあり、$は現在の行のアドレスを保持する特殊変数らしい。
そして- msgは、msgのアドレスを引く、ということのよう。(普通にマイナス記号とは・・)

説明を引用すると以下の意味みたいです。

Means the current address minus the address of msg. That gives the length of the data stored between msg and .len (since the address of .len is represented by $). Thus, the symbol .len represents (equated to) that length value.

つまり、現在のアドレスである$から直前のmsgのアドレスを引いた長さは、つまるところmsgの長さだよね、ってことで解釈あってるかな?

システムコールに使われるレジスタ

こちらを参考にしました。

The number of the syscall has to be passed in register rax.

rdi - used to pass 1st argument to functions
rsi - used to pass 2nd argument to functions
rdx - used to pass 3rd argument to functions
rcx - used to pass 4th argument to functions
r8 - used to pass 5th argument to functions
r9 - used to pass 6th argument to functions

A system-call is done via the syscall instruction. The kernel destroys registers rcx and r11.

0x2000001とかの正体

これはどうやらマスクされる想定の数値を指定しているらしい。
例えばこんな記述。

mov rax, 0x2000004      ; System call write = 4

これは、Unix系の場合は上位ビットが2としてマスクされて、それにマッチした場合に実行される、という感じだろうか。
とにかくこのマジックナンバーの正体はマスクされることを想定した数値みたいです。
(まだちゃんとわかってない)

hello world

ちなみに上記のマスクを適用し、実際に動くhello worldはこんな感じ。

section .data
hello_world     db      "Hello World!", 0x0a

section .text
global start

start:
mov rax, 0x2000004      ; System call write = 4
mov rdi, 1              ; Write to standard out = 1
mov rsi, hello_world    ; The address of hello_world string
mov rdx, 14             ; The size to write
syscall                 ; Invoke the kernel
mov rax, 0x2000001      ; System call number for exit = 1
mov rdi, 0              ; Exit success = 0
syscall                 ; Invoke the kernel

syscallはシステムコールで、raxにシステムコールの番号を、rbxに引数を、という感じで決まっているよう。
こちらにそのへんがまとまってます)

参考にした記事