X86アセンブラ/GASでの文法

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動

概要[編集]

この記事の例題は、GNU ASで使用されているAT&Tアセンブリ構文を使用して作成されています。この構文を使うことの主な利点は、GCCのインラインアセンブリ構文との互換性です。しかし、x86の操作を表現するのに使われる構文はこれだけではありません。例えば、NASMでは、いくつかの高水準アセンブラと同様に、アセンブリのニーモニック、オペランド、アドレッシングモードを表すのに異なる構文を使用しています。Unix系システムではAT&T構文が標準ですが、一部のアセンブラではインテル構文を使用したり、GAS自身は両方を受け入れることができます。

語順[編集]

GASの命令は一般的に、ニーモニック 転送元 転送先という形式をしています。例えば、次のようなmov命令があります。

 movb $0x05, %al

これは、16進数の数値5をレジスタalにコピーする。

プレフィクス[編集]

GASのアセンブリ命令は、一般的に「b」、「s」、「w」、「l」、「q」、「t」の文字がサフィックスとして付けられ、操作されるオペランドのサイズを決定します。

b=バイト(8ビット)。

  • s = single (32ビット浮動小数点)を表します。
  • w = ワード(16ビット)。
  • l = long (32ビット整数または64ビット浮動小数点)。
  • q = quad (64ビット)。
  • t = ten bytes (80ビット浮動小数点)です。

サフィックスが指定されておらず、その命令のメモリ・オペランドがない場合、GASはデスティネーション・レジスタ・オペランド(最終オペランド)のサイズからオペランド・サイズを推定します。

プリフィックス[編集]

レジスタを参照する場合、レジスタの前に「%」を付ける必要があります。定数の場合は"$"を前に付けます。

アドレス・オペランド構文[編集]

アドレス・オペランドには最大4つのパラメータがあり、segment:displacement(base register, index register, scale factor)のような構文で表示されます。これは、インテルのシンタックスではsegment:[base register + displacement + index register * scale factor]に相当します。

ベース、インデックス、ディスプレイスメントの各コンポーネントは、どのような組み合わせでも使用することができ、どのコンポーネントも省略することができます。省略されたコンポーネントは、上記の計算から除外されます[1][2]

movl    -8(%ebp, %edx, 4), %eax  # 詳しい例: *(ebp + (edx * 4) - 8) を eax にロードする
movl    -4(%ebp), %eax           # 典型的な例:スタック上の変数をeaxにロードする
movl    (%ecx), %edx             # インデックスなし: ポインタのターゲットをレジスタにコピーする
leal    8(,%eax,4), %eax         # 算術:eaxを4倍して8を加える
leal    (%edx,%eax,2), %eax      # 算術:eaxに2を掛けてedxを加える

はじめに[編集]

この章は、GASの簡単な紹介として書かれています。GASはGNUプロジェクトの一部であり、次のような優れた特性を持っています。

  • 多くのオペレーティング・システムで利用可能
  • GNU Cコンパイラ(gcc)やGNUリンカ(ld)など、他のGNUプログラミングツールとのインタフェースが良好

Linuxオペレーティングシステムを搭載したコンピュータをお使いの方は、すでにGASがシステムにインストールされていると思います。Windows オペレーティングシステムのコンピュータを使用している場合は、CygwinまたはMingwをインストールすることで、GAS およびその他の有用なプログラミングユーティリティをインストールすることができます。 ここから先は、GASがインストールされていて、コマンドライン・インターフェースの開き方やファイルの編集方法を知っていることを前提に説明します。

C言語のコードからアセンブリコードを生成する[編集]

アセンブリ言語は、CPUが実行する演算に直接対応しているため、注意深く書かれたアセンブリ・ルーチンは、Cなどの高級言語で書かれた同じルーチンよりもはるかに速く実行できる可能性があります。 その一方で、アセンブリ・ルーチンは、Cで書かれた同等のルーチンよりも多くの労力を要するのが一般的です。 したがって、性能の良いプログラムを素早く書くための典型的な方法は、まず(記述やデバッグが容易な)高級言語でプログラムを書き、次に(性能の良い)アセンブリ言語で選択されたルーチンを書き直すことです。 C言語のルーチンをアセンブリ言語に書き換える最初のステップとしては、Cコンパイラを使ってアセンブリ言語を自動生成するのが良いでしょう。 これにより、正しくコンパイルされたアセンブリファイルが得られるだけでなく、アセンブリルーチンがあなたの意図した通りに動作することが保証されます[3]

ここでは、GAS アセンブリ言語の構文を検証するために、GNU C コンパイラを使用してアセンブリ コードを生成します。

ここでは、C言語で書かれた古典的な「Hello, world」プログラムを紹介します。

#include <stdio.h>

int main(void) {
    printf("Hello, world!\n");
    return 0;
}

これを「hello.c」というファイルに保存して、プロンプトで次のように入力します。

gcc -o hello_c hello.c

これで、Cファイルがコンパイルされ、"hello_c "という実行ファイルが作成されます。エラーが発生した場合は、"hello.c "の内容が正しいことを確認してください。

これで、プロンプトで次のように入力できるようになります。

./hello_c

「hello.c」が正しく入力され、目的の動作をすることがわかったので、それに相当する32ビットx86アセンブリ言語を生成してみましょう。プロンプトで次のように入力します。

gcc -S -m32 hello.c

これで「hello.s」というファイルが作成されるはずです(「.s」はGNUシステムがアセンブリファイルに与えるファイル拡張子です)。最近の64ビット・システムでは、32ビットのソース・ツリーが含まれていない場合があり、「bits/predefs.h fatal error」が発生します。64ビット・アセンブリを生成するには、-m32 gcc指令を-m64指令に置き換えてください。アセンブリファイルを実行ファイルにコンパイルするには、次のように入力します。

gcc -o hello_asm -m32 hello.s

(なお、gccはアセンブラ(as)とリンカ(ld)を呼び出してくれます) 次に、プロンプトで次のように入力します。

 ./hello_asm

このプログラムは、コンソールに「Hello, world!」と同じ様に表示します。驚くことではありませんが、これはコンパイルされたCファイルと同じことをしています。

それでは、「hello.s」の中身を見てみましょう。

        .text
LC0:
        .ascii "Hello, world!\12\0"
.globl _main
_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        andl    $-16, %esp
        movl    $0, %eax
        movl    %eax, -4(%ebp)
        movl    -4(%ebp), %eax
        call    __alloca
        call    ___main
        movl    $LC0, (%esp)
        call    _printf
        movl    $0, %eax
        leave
        ret
        .def    _printf;        .scl    2;      .type   32;     .endef

「hello.s」の内容は、インストールされているGNUツールのバージョンによって異なる場合があります。このバージョンは、Cygwinでgccバージョン3.3.1を使用して生成されました

.file.def.asciiのようにピリオドで始まる行は、アセンブラのディレクティブ(ファイルをどのように組み立てるかをアセンブラに指示するコマンド)です。_main:のように、テキストの後にコロンが続く行は、ラベル、つまりコードの中の名前のある場所です。それ以外の行は、アセンブラの命令です。

.file.defは、デバッグ用の指示です。それらを省くことができます。

        .text
LC0:
        .ascii "Hello, world!\12\0"
.globl _main
_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        andl    $-16, %esp
        movl    $0, %eax
        movl    %eax, -4(%ebp)
        movl    -4(%ebp), %eax
        call    __alloca
        call    ___main
        movl    $LC0, (%esp)
        call    _printf
        movl    $0, %eax
        leave
        ret

hello.sを1行ごとに[編集]

         .text

この行は、コードのセクションの開始を宣言します。このディレクティブを使ってセクションに名前を付けることができます。これにより、実行ファイルの中で結果として得られるマシンコードがどこに行くのかを細かく制御することができ、組み込みシステムのプログラミングなどの場合に便利です。使用方法は .textを使うと、それだけでアセンブラに次のコードがデフォルトのセクションに入ることを伝えますので、ほとんどの目的にはこれで十分です。

 LC0:
         .ascii "Hello, world!\12\0"

このコードでは、ラベルを宣言し、ラベルの位置から始まる生のASCIIテキストをプログラムに配置します。12は改行文字を指定し、0は文字列の最後にヌル文字を指定しています。C言語のルーチンは文字列の終わりをヌル文字でマークしますが、C言語の文字列ルーチンを呼ぼうとしているので、ここにこの文字が必要なのです。(注意!C言語の文字列はデータ型char(char[]の配列)であり、他の形式では存在しませんが、大多数のプログラミング言語では文字列を1つの実体として理解することになるため、このように表現した方がわかりやすいです)。

 .globl _main

この行は、_mainというラベルがグローバル・ラベルであることをアセンブラに伝え、プログラムの他の部分がそれを見ることができるようにします。この場合、プログラムがリンクされているスタートアップ・コードがサブルーチンとして_mainを呼び出すので、リンカーは_mainラベルを見ることができる必要があります。

 _main:

この行は、_mainラベルを宣言し、起動コードから呼び出される場所をマークします。

         pushl   %ebp
         movl    %esp, %ebp
         subl    $8, %esp

これらの行は、EBPの値をスタックに保存し、ESPの値をEBPに移動し、ESPから8を引きます。なお、pushlは自動的に適切な長さだけESPをデクリメントします。各オペコードの末尾のlは、そのオペコードが「ロング」(32ビット)オペランドで動作するバージョンであることを示しています。通常、アセンブラはオペランドから正しいオペコードのバージョンを計算することができますが、念のため、lwbなどのサフィックスを付けておくと良いでしょう。パーセント記号はレジスタ名を、ドル記号はリテラル値を表しています。EBPはローカル変数を参照するためのベース・レジスタとして使用され、ESPから値を引いてスタック上のスペースを確保します(インテルのスタックは上位のメモリ・ロケーションから下位のメモリ・ロケーションへと成長するため)。この例では、スタック上に8バイトが確保されています。このスペースがなぜ必要なのかは後ほど説明します。

         andl    $-16, %esp

このコードはandでESPを0xFFFFFFF0とし、スタックを次の最低16バイト境界に合わせています。Mingwのソースコードを調べてみると、これは_mainルーチンに登場するSIMD命令のためではないかと思われますが、この命令は整合されたアドレスに対してのみ動作します。私たちのルーチンにはSIMD命令は含まれていないので、この行は不要です。

         movl $0, %eax
         movl %eax, -4(%ebp)
         movl -4(%ebp), %eax

このコードでは、0をEAXに移動し、次にEAXをメモリ位置EBP - 4に移動しています。このコードは、明らかに最適化されたコードではありません。 括弧はメモリの位置を示し、括弧の前の数字はそのメモリの位置からのオフセットを示していることに注意してください。

         call    __alloca
         call    ___main

これらの関数は、Cライブラリの設定の一部です。私たちはCライブラリの関数を呼び出しているので、おそらくこれらが必要になるでしょう。これらの関数が行う正確な操作は、プラットフォームやインストールされているGNUツールのバージョンによって異なります。

         movl    $LC0, (%esp)
         call    _printf

このコードは、(やっと!)メッセージを印刷します。まず、ASCII文字列の位置をスタックの一番上に移動しています。これは、Cコンパイラがpopl %eax; pushl $LC0の一連の処理を、スタックの一番上への1回の移動に最適化したようです。そして、Cライブラリの_printfサブルーチンを呼び出して、コンソールにメッセージを表示しています。

         movl $0, %eax

この行では、戻り値である0をEAXに格納しています。C言語の呼び出し規則では、ルーチンを終了するときに戻り値をEAXに格納することになっています。

         leave

この行は、通常サブルーチンの最後に見られ、EBPをESPにコピーした後、EBPの保存された値をEBPに戻すことで、スタック上に保存されたスペースを解放します。

         ret

この行は、保存された命令ポインタをスタックからポップすることにより、制御を呼び出しプロシージャに戻します。

失敗学で学ぶアセンブラ[編集]

正常に動くコードよりも、改造して正常に動かなくしたコードを見て比較したほうが、説明が分かりやすいんで、下記にダメなコードを書きます。

	.file	"test9.c"
	.text
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "hello, wrong world!"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
LFB13:
	.cfi_startproc

	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8

	.cfi_def_cfa_register 5 


	movl	$LC0, (%esp)
	call	_puts


	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
LFE13:
	.ident	"GCC: (MinGW.org GCC-8.2.0-5) 8.2.0"
	.def	_puts;	.scl	2;	.type	32;	.endef


なんと、コレだけでも、WindowsでMinGWとgccで実行すると、

C:\Users\ユーザー名\nnnn>test9.o
hello, wrong world!

と表示されますが、すぐに異常停止します。

なんと、リターンに相当する「ret」を除去しても、hello文を表示します(しかし、すぐに異常停止するが・・・)。

そして、上記コードから、もしも

 	movl	$LC0, (%esp)

を除去すると、

オブジェクトファイルの表示結果は

C:\Users\st\nnnn>notest.o
嘉闢(

という意味不明の文字列を表示します(一例)。



推奨されないかもしれないコード[編集]

Windowsの場合[編集]

Windows7では、なんと下記のようなコードが動いてしまう。しかし、「pushl %ebp」など下記コードではコメントアウトしていない命令をコメントアウトしてコンパイルしても、異常な結果になる。


	.file	"test8.c"
	.text
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "hello, wrong world!"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
LFB13:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5 # komento test
	# andl	$-16, %esp
	# subl	$16, %esp
	# call	___main
	movl	$LC0, (%esp)
	call	_puts
	# movl	$0, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	# ret
	.cfi_endproc
LFE13:
	.ident	"GCC: (MinGW.org GCC-8.2.0-5) 8.2.0"
	.def	_puts;	.scl	2;	.type	32;	.endef

この事から、これらコメントアウトできない命令には、特別な意味がある事が分かる。


Linuxの場合[編集]

Linux の Fedora 31 では、なんと下記のコードは動く。

下記コードには、のちの節で後述する push もなければ pop もなく、スタックフレームの確保も破棄も解放も何もしてないコードだが、実験事実として動き、ターミナル(コマンド端末)で実行すれば hello文を表示する。

リターン命令 ret すらないコードである。でも動く。事実、動くのだ。

	.file	"hello.c"
	.text
	.section	.rodata
.LC0:
	.string	"hello, 1234 world"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc

	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16

	.cfi_def_cfa_register 6
	movl	$.LC0, %edi
	call	puts
	movl	$0, %eax

	.cfi_def_cfa 7, 8

	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (GNU) 9.2.1 20190827 (Red Hat 9.2.1-1)"
	.section	.note.GNU-stack,"",@progbits


実験結果
[ユーザー名@localhost ~]$ ./hello.o
hello, 1234 world


Linuxの場合[編集]

基本[編集]

Linuxの場合、やや用語が違うので、アセンブリコード中の用語が違ってくる。

下記のアセンブリコードは、Fedora31 Workstation で「gcc -S hello.c」実行した場合の生成されるファイル「hello.s」の中身である。もし読者が追試した場合、バージョンによって多少、コードの中身が違うかもしれない。

	.file	"hello.c"
	.text
	.section	.rodata
.LC0:
	.string	"Hello, world!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$.LC0, %edi
	call	puts
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (GNU) 9.2.1 20190827 (Red Hat 9.2.1-1)"
	.section	.note.GNU-stack,"",@progbits


違いはいろいろあるし、バージョンも違うので単純には1対1対応はしないですが、 主にLinux(Fedora)の場合、

相違点

.ascii ではなく.stringである。
esp でなく rsp である。 ebp でなく rbp である。「r」はおそらくレジスタ(register)の頭文字か?
メインラベルが(Windowsの)「_main」 でなく (LinuxのFedora31では)「main」である。
printf ではなくputs である。なお、「call puts」や「call printf」などの「call」は、OSまたはC言語ライブラリの用意する命令を呼び出しています。

といった相違点があります。

いっぽう、ほぼ同じ点もあります。

レジスタの名前の前に「%」パーセント記号がつくのは同じ。
定数値の前に「$」ドル記号がつくのは同じ。
ラベル宣言はコロン記号「:」であるのも同じ。
.globl の使い方は同じ。(ただし、メインラベル(「_main」 か「main」かの問題)は違う。)
メインラベルをどこかで宣言しないといけないっぽい。(「_main」 か「main」かの違いはあるが。)

といった類似点があります。

なお「pushq」や「movq」などの末尾qは64ビットCPUパソコン用のコマンドであるという意味です。

pushやmovなど幾つかのコマンドの語感の部分もWindowsとLinuxで同じです。


なお、ささいな相違点だが、グローバルでないラベル(つまりローカルなラベル)の冒頭にドット記号を付ける[4]かどうかの違いがある。「.LC0:」みたいに、ローカルなラベルの冒頭には、Fedora 31 などではドット記号がある。

ローカルラベルの冒頭にドット記号を付けるほうがLinux流。


いろいろな実例[編集]

実例1[編集]

ところで、printf関数を増やしてみよう。

#include <stdio.h>

int main(void) {
    printf("Hello1, world!\n");
    printf("Hello2, world!\n");
    printf("Hello3, world!\n");
    return 0;
}


これをアセンブリコードにすると

	.file	"hello3.c"
	.text
	.section	.rodata
.LC0:
	.string	"Hello1, world!"
.LC1:
	.string	"Hello2, world!"
.LC2:
	.string	"Hello3, world!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$.LC0, %edi
	call	puts
	movl	$.LC1, %edi
	call	puts
	movl	$.LC2, %edi
	call	puts
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (GNU) 9.2.1 20190827 (Red Hat 9.2.1-1)"
	.section	.note.GNU-stack,"",@progbits

となります。


もう、これを見ただけで、どこがどう対応してるか、よく分かりますね。

たとえば、「Hello3, world!」につづけて、さらに「good moring!」と言わせたければ、そのぶんラベルを増やして、そして、そういうput命令を追加すればいいのです。

下記のようになります。

	.file	"hellogood.c"
	.text
	.section	.rodata
.LC0:
	.string	"Hello1, world!"
.LC1:
	.string	"Hello2, world!"
.LC2:
	.string	"Hello3, world!"
.LC5:
	.string	"good moring!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$.LC0, %edi
	call	puts
	movl	$.LC1, %edi
	call	puts
	movl	$.LC2, %edi
	call	puts
	movl	$.LC5, %edi
	call	puts
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (GNU) 9.2.1 20190827 (Red Hat 9.2.1-1)"
	.section	.note.GNU-stack,"",@progbits

これを実際にコンパイルしてみましょう。コマンドは下記のようになるでしょう。

[ユーザー名@localhost ~]$ gcc -o hellok_asm.exe hellogood.s
[ユーザー名@localhost ~]$ ./hellok_asm.exe

実行結果

[ユーザー名@localhost ~]$ ./hellok_asm.exe
Hello1, world!
Hello2, world!
Hello3, world!
good moring!


このような実験事実により、どうやら

call puts は、%edi の中身の値を読み取るっぽいこと.
ラベルの番号は連続してなくてもいいようである。上コード例では「.LC2:」から「.LC5:」へと番号が飛んでいる。

など、いろいろと分かります。

なお、 call puts の代わりに :call printfとすると、Fedoraでは出力文字が改行されないので、見づらくなります。


実例2[編集]

puts を2回連続したら?

もし

	movl	$.LC0, %edi
	call	puts
	call	puts

みたいに、call puts をつづけて2回使ったら、どうなるのでしょうか?

コード
	.file	"helloGood.c"
	.text
	.section	.rodata
.LC0:
	.string	"Hello1, world!"
.LC1:
	.string	"Hello2, world!"
.LC2:
	.string	"Hello3, world!"
.LC5:
	.string	"good moring!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$.LC0, %edi
	call	puts
	call	puts
	movl	$.LC1, %edi
	call	puts
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (GNU) 9.2.1 20190827 (Red Hat 9.2.1-1)"
	.section	.note.GNU-stack,"",@progbits


実行結果

[ユーザー名@localhost ~]$ ./hellok_asm.exe
Hello1, world!

Hello2, world!

空行が1行ぶん、増えてますね。

どうやら call puts をすると、使い終わったデータはレジスタ内部からは消えるらしいですね。


あと、使わないラベルがあっても平気なようですね(以前のコードを流用したので「.LC2:」などの未使用のラベルが残っている)。


ラベルを2回連続で使用したら?

では、

	movl	$.LC0, %edi
	call	puts
	movl	$.LC0, %edi
	call	puts

みたいに、ラベル「.LC0」を2回使ったら、どうなるでしょうか?

実験してみましょう。

コード
	.file	"rezokurabel.c"
	.text
	.section	.rodata
.LC0:
	.string	"Hello1, world!"
.LC1:
	.string	"Hello2, world!"
.LC2:
	.string	"Hello3, world!"
.LC5:
	.string	"good moring!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$.LC0, %edi
	call	puts
	movl	$.LC0, %edi
	call	puts
	movl	$.LC1, %edi
	call	puts
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (GNU) 9.2.1 20190827 (Red Hat 9.2.1-1)"
	.section	.note.GNU-stack,"",@progbits

実行結果

[ユーザー名@localhost ~]$ ./hellok_asm.exe
Hello1, world!
Hello1, world!
Hello2, world!

どうやらラベル自体は残るようです。


つまり、

putsを使うと、レジスタ %edi の内部自体からはデータは消えますが、しかし参照元のラベルなどはそのまま残ります。

コメントつきの説明[編集]

        .text
LC0:
        .ascii "Hello, world!\12\0" # ラベルの定義
.globl _main  # 「_main」がグローバルラベルであると宣言
_main: 
        pushl   %ebp           # ebpの値をスタックメモリに保存。
        movl    %esp, %ebp     # 現時点のespの値を現時点のebpに書き込んでいる。
        subl    $8, %esp       # スタックポインタから8引いている
        andl    $-16, %esp
        movl    $0, %eax
        movl    %eax, -4(%ebp)
        movl    -4(%ebp), %eax
        call    __alloca
        call    ___main
        movl    $LC0, (%esp)
        call    _printf
        movl    $0, %eax
        leave
        ret
        .def    _printf;        .scl    2;      .type   32;     .endef

hello.s を一行ずつ読む[編集]

         .text

この行は、コードのセクションの始まりを示している。 このディレクティブを使い、セクションに名前を付け、実行可能ファイル中で機械語のコードがどこへ行くかを精度良く制御することができる。これは、組み込みシステムでのプログラミングのような場合に有用である。


「.text」ディレクティブ自体は、アセンブラに以降のコードがデフォルトのセクションであることを伝える。ほとんどの場合にはこのように使うので十分である。

 LC0:
         .ascii "Hello, world!\12\0"

このコードは、ラベルを名称「LC0」で宣言し、生のASCIIテキストを、プログラム中のラベルの場所から始まる位置に置いている。 「\12」は、ラインフィード文字(LF)を意味しており、「\0」は文字列の最後に置かれるヌル文字である。

ラインフィードとは、改行のこと。

もし「\12」を省いて

 .ascii "Hello, world!\0"

とした場合のコードを3回実行すると、改行が行われないので、行間のスキマなく続けて実行される。

C:\Users\ユーザー名\nnnn> hello_asm.exe
Hello, world!
C:\Users\ユーザー名\nnnn> hello_asm.exe
Hello, world!
C:\Users\ユーザー名\nnnn> hello_asm.exe
Hello, world!

いっぽう、「\12」の残っている

         .ascii "Hello, world!\12\0"

の場合なら、これを3回実行すれば

C:\Users\ユーザー名\nnnn> hello_asm.exe
Hello, world!

C:\Users\ユーザー名\nnnn> hello_asm.exe
Hello, world!

C:\Users\ユーザー名\nnnn> hello_asm.exe
Hello, world!

と、改行が毎回実行される。


Cのルーチンはヌルで終わる文字列があることで分かる。 私たちはCの文字列ルーチンを呼び出そうとしているので、ヌルがここに必要である。

(注意! Cでの文字列は、データ型charの配列(char[])であり他の形式ではない。 しかし、文字列は多くのプログラミング言語では、一つのデータ型であるので、 このように書いておく。)

 .globl _main

「 .globl」命令は、次のラベルが、外部ファイルから呼び出すことを許可する命令である。

アセンブラに「_main」はグローバルなラベルであると伝えている。


また、「_main」という名前のラベルには特別な意味があって、プログラムの開始地点などを表すラベルである。この名前を別のラベルに変えてコンパイルしても、コンパイルがエラーになる(※ Windows7環境でのMinGWとNASMで確認ずみ。2019年12月2日)。(アンダーバー「_」の無い)「main」ラベルでもダメで、アンダーバーのある「_main」ラベルで宣言しないとエラーになる。

また、グローバル宣言をしないで「_main:」ラベルとそれ以降を書いても、エラーになる。(※ 以上、Windows7環境でのMinGWとNASMで確認ずみ。2019年12月2日)


事実上、

.globl _main
_main:

で、コードのプログラム開始地点を表す慣用句のようなものである。


なお「 .globl」命令の無いラベルは、同じオブジェクトファイルからしか呼び出せない。 つづりに注意しよう。「global」ではなく「globl」である。後ろ「bal」から「a」が抜けている。

上述のプログラムの用途の場合、リンカは「_main」ラベルを見ることのできる必要がある。 リンクされたプログラムのスタートアップコードは「_main」をサブルーチンとして呼び出す。


暗黙の前提だが、「 _main」ラベルの内容を記述する前に、まず、「 _main」がグローバルなラベルであることを宣言する必要がある。

一般に機械語やアセンブラなどのコードは、上から順番に実行していくので、まず上のほうのコードで準備や設定をしておく必要があるからだ。

さて、

 _main:

について。この行は、スタートアップコードから呼び出される場所をマークする「_main」ラベルを宣言している。

さらに、

         pushl   %ebp
         movl    %esp, %ebp
         subl    $8, %esp

この3つの命令は、私達のhello worldのプログラムのアセンブリコードだけでなく、ほかの一般のプログラムのアセンブリコードでもよくある、初期設定のような命令です。よく、この順番の3命令をまとめて「スタックフレームの確保」のようにいいます。

何のためにコレを行うかというと、たとえば(C言語でいう)「関数」のような機能を実装するために行います[5]。ある関数を実行している最中でも、別の場所に呼び出し元のアドレスを保存しておく必要があります。

上述の3命令は、関数の呼び出し始めの時のアドレス保存作業に相当します。なお、関数の終了時の(さっき保存したアドレスなどの)使用は(「スタックフレームの破棄」という)は、leave 命令で行います(後述する)。(※ 「破棄」というが、けっしてスタックに保存したデータを捨てるのではなくて、保存したデータを取り出して使ってから、データを上書き可能な状態にすることである。)


sub のあとの引き算する数だけが、他のプログラムのアセンブリでは違っている場合がよくありますが、それ以外のpush → mov → sub の順番は、まず他のプログラムでも、この通りです。

では、このよくある一連の3命令が、なにをしているのか、調べましょう。


なお、EBPとESPとは、スタックポインタのアドレス記憶用のレジスタです。

pushl命令は、次の指定されたアドレス(ebp)にある数値が、現在のスタックポインタ(esp)が指し示すアドレス先に保存され、さらに、自動的にスタックポインタの値が4減ります[6]。その結果、いま格納したデータの先頭を、スタックポインタが指し示します[7]

32ビットCPUパソコンなら4減ります。64ビットCPUパソコンでは違うかもしれませんので、それぞれの環境でご確認ください。

なお、poplなどのpop系コマンドの内容は、push系コマンドの逆です。   EBPが、スタックポインタの基準(base)です。もしスタックポインタについて分からなければ、一般的な大学理系の課程のオペレーティングシステム論の教材に書いてありますので、分からなければ、そこらへんを調べてください。


まず、上コードでは、pushlによってebpの値を退避させています(退避先はメモリなど適切な場所が選ばれる[8])。


スタックとは

「スタック」とは何か、おおまかにいうと、まずパソコンは、何かのデータを蓄えるとき、そのプログラムそのものを保存するためのメモリ領域を(ハードディスク領域とは別に)確保しておく必要がある。だって、画像データとか長文データとか、画素1個ずつとか文字1個ずつとかをハードディスクから毎回読み出してたら、読み込み回数が多すぎて時間が掛かるでしょ?

なので、まずメモリに、ある程度のデータをまとめてメモリにロードしておく必要があるわけです。

で、たとえば、アドレス100個ぶんのメモリ領域を確保したい、としましょう。

このためには、「メモリのアドレスの、ここからアソコまでを、プログラムの保存のために使うぞ!」と、パソコンに宣言して教えてやる必要があるわけです。


100行ぶんのデータを保存するための領域の、メモリの最初の番地と、終わりの番地を指定するわけです。

たとえば、メモリのアドレスの200番から300番まで確保したとしましょう。(植木算っぽいことは本質でないので、考慮しない。1個分の誤差があっても読者が脳内で補正してくれ)

で、保存に使える大きさは、確保した100行ぶんだけですから、1行なにかを保存するたびに、残りの保存可能なプログラム数は1個ぶん減るわけです。(Push操作)

で、慣習的に、確保した領域の終わりの300番から、1個なにかを保存するたびに、スタックポインタというのを1つアドレスを前にして299番にしていくのです。もう1個なにかを保存したら、さらに1つアドレスを前にして、298番にあわせるのです。

で、スタックポインタがもし200番に合わさってしまったら、「もう、これ以上は保存できないよ!」ってなるわけです。こういうのがスタックポインタ。


さて、使い終わったデータは保存の必要が無いし、また、他のデータを保存できるようにするために、データを廃棄する仕組みも必要そうですね。


で、たとえば仮に300番から220番まで保存されてて、220番目のデータを使い終わったとしましょう。すると、220番目のデータはもう使わないので上書きしてもいいので、221番目にスタックポインタを戻すわけです。(POP操作)


このプッシュとポップの関係は、後から保存された新しいデータのほうが、先に使われます。なので、先に保存されたデータは、取り出すのが後回しになります。

つまり、保存したデータの順番と、実行・使用するデータの順番とが逆になります。(このため、プログラムの処理みたいなのには不便。プログラムの実行順の管理には、(スタックポインタではなく)プログラムカウンタという別の仕組みがある。)


さて、プログラム処理でもスタックポインタの使い方はあって、それはサブルーチン(if文やfor文など)が入れ子構造になっている場合の処理である。(コールスタック)

たとえばfor文のなかに、別のfor文があるとか。

この場合、たとえば最初のfor文の開始(仮にこれをforAと予防)と、次のfor文(forB)の開始という順番なら、for文の終わりは、ForBが先に終わってから、あとからforAが終わることになる。

なので、積んだ順番と、取り出し(データ廃棄)の順番がちょうど逆なので、スタックポインタが使えるわけである。

サブルーチンを呼び出した際の帰り先のアドレスが、これらの場合のスタックポインタに保存されている。


(再掲)

         pushl   %ebp
         movl    %esp, %ebp
         subl    $8, %esp

sublは、減らす命令です。この行のあとに8バイト分のデータサイズのデータ1個を使用する予定なので、あらかじめスタックポインタを8減らす必要があるのです。「subl $8, %esp」によってespの値から8引いて、それをespに代入しています。espの値の内容とは、スタックポインタの指し示すアドレスの番号なので、結局「subl $8, %esp」によってスタックポインタの指し示すアドレス番号が8減らされることになります。


これらの行はEBPの値をスタックに保存し、ESPの値をEBPにコピーし、ESPから8を引いている。

「movl」とは、由来は move (ムーブ、移動)ですが、作業内容は(今でいう)コピーです。


「pushl」や「muvl」「subl」などのそれぞれのオペコードの最後の「l」は、「ロング」(32ビット)のオペランドにはたらくバージョンのオペコードを使おうとすることを示している。

また、このようにオペコードの末尾につく、特別な意味を持った1文字ていどの文字のことをオペランドという。

通常、アセンブラはオペランド(末尾の文字)で適切なオペコードを選ぶことができるが、単に安全のためである。 「l」「w」「b」やほかのサフィックス(英語で「添え字」という意味)を含めるのは良い考えである。 パーセント記号は、レジスタの名前を示し、ドル記号はリテラル値(定数)であることを示す。

この一連の命令は、ローカル変数をスタックに保存するサブルーチンの最初として典型的なものである。 EBPはベースレジスタとしてローカル変数を参照するのに使用される。 そして、値がESPから引かれ、スタック上の場所に保持される。 (インテルのスタックは、メモリ上の位置をアドレスの大きい方から小さい方へ使って行く)

この場合、スタックには8バイトが確保されている。 後に、なぜこの場所が必要になるか見ることとになる。

なお、スタックフレームを確保するための命令として、インテルでは enter という専用の命令が容易されており、引数を適切につける事により上述の3コマンドを代替することもできる。スタックフレームに関して enter 命令と leave 命令とは、機能が反対どうしで対(つい)になっている。だが、このエンター命令は、あまりサポート状況がよくないらしく、たとえばgcc -S 系コマンドでソースを自動生成しても enter コマンドは使われないのが普通であったりする。

なので、本書では enter 命令についてのこれ以上の説明を省略する。(※ 市販の文献でも、あまり詳しく書かれていない。カットシステムの日向さんの本(『プログラミングの不思議を解く』には、付録225ページにあるが、説明は少ない(しかも巻末索引にenterの項目は無い)。マイナビ出版の内田さんの本『自作エミュレータで学ぶX86アーキテクチャ』にも、まったく書かれていない。わずかに翔泳社の『低レベルプログラミング』で数行ばかり触れられているだけである[9]。しかも、その『低レベルプログラミング』には巻末索引にはleave命令が書いてない。索引でleave命令で調べたページ先で、ついでにenterが見つかるだけである。)

         andl    $-16, %esp

このコードは、EPSと0xFFFFFFF0を「and」している。 つまり、次の最も低位の16バイトごとの境界にスタックを配置している。 Mingwのソースコードを見てみると、これは適切に配置されたアドレスにのみ作用するSIMD命令が、「_main」ルーチン内に現われるために存在するものである。 私たちのルーチンにはSIMD命令は含まれていないので、この行は不必要である。

         movl    $0, %eax
         movl    %eax, -4(%ebp)
         movl    -4(%ebp), %eax

このコードは、ゼロをEAXにコピーし、EAXをEBP-4の値のメモリの場所にコピーし、それをEAXへ書き戻している。ここで、EBP-4は最初にスタックに格納されていた値を一時的に保存しておくための場所として使われている。 このコードは最適化されていないことが分かる。 丸カッコ ( ) で囲んでいるのは、メモリの場所を示しており、括弧で囲まれた部分の前にある数字は、メモリの場所からのオフセットを示している。

         call    __alloca
         call    ___main

これらの函数はCライブラリのセットアップの一部である。

私たちはCライブラリ中の関数を呼び出しているので、これらは必要なはずである。 これらが厳密にどうはたらくかということは、プラットフォームとインストールされているGNU toolsに依存する。

         movl    $LC0, (%esp)
         call    _printf

最後になるが、このコードは私たちのメッセージを印字する。「call _printf」でCライブラリのprintf関数を呼び出している。


まず、ASCII文字列の場所をスタックの最上部にコピーする。 Cコンパイラが、「popl %eax; pushl $LC0」という命令を、一つのスタックの最上部へのコピーに最適化したようである。 次に、Cライブラリの中の_printfサブルーチンを呼び出し、コンソールにメッセージを印字する。

         movl    $0, %eax

この行は、戻り値であるゼロをEAXに保存している。 Cの呼び出しのきまりでは、ルーチンを終了する時には、EAXに戻り値を保存する。

         leave

この行はサブルーチンの最後に典型的に見られるものである。leave命令はスタック上に確保された領域を、pushの1個ぶんだけ解放するために EBPをESPにコピーし、EBPに保存された値をEBPにpop する命令です。


つまり、leave 命令は、

mov esp, ebp
pop ebp

と同じ働きです[10]。主に64ビットCPUで、R8 〜 R15 が追加されている[11]

pushで確保された領域1個を解放するので、対応するpopを1回だけ行っているのです。

一般に、leave 命令は「スタックフレームの破棄」とよく説明されますが、むしろ、スタック領域の1個ぶんの「解放」と考えたほうが良いでしょう。


さて、

         ret

この行は、スタックから保存された命令ポインタ(eip)をpopすることで制御を呼び出したプロシージャに返している。

その他[編集]

コメントを追加したい場合、Windowsでは「#」を入れる。Windows版GCC(MinGW)だけでなく LinuxでもFedora31のGCCなら同様に「#」でコメント記号になる。

	andl    $-16, %esp # komento test

のように、どこかの行にコメントを入れてみよう。上の例の場合「 komento test」がコメント文。

そして、変更前のプログラムとの区別のため(※ Windows版で説明)

         .ascii "Hello12345, world!\12\0"

とでも、ハロー文を書き換えておこう。

gcc -o hello_asm.exe hello.s
./hello_asm.exe

で 「Hello12345, world!」 と出れば成功である。


なお、Linuxの場合やアセンブリ言語の種類によっては、セミコロン「;」がコメント(正確にはコメントアウト)の記号であることもある。(コメントアウトの記号の後ろにコメント文の本文を書く。)

しかしWindowsの場合、おそらく、すでにセミコロン「;」が別の意味で使われているからか、セミコロンでコメントしようとしようと失敗する(MinGW.org GCC-8.2.0-5)で2019年12月2日にWin7で確認)。

なおFedora 31 でもWindows同様に「;」をコメントのつもりで使ってもエラーになる。なので、Fedoraではアセンブラのコメント記号には「#」を使わなければいけない。


オペレーティングシステムと直接対話する[編集]

注意してほしいのは、私たちは「printf」のようなCライブラリーにあるファンクションコールを必要とする場合のみ、Cライブラリーセットアップルーチンを呼び出さなくてはいけないということである。 オペレーティングシステムと直接コミュニケートするならば、これらのルーチンを呼び出すのを避けることができる。 オペレーティングシステムと直接コミュニケートすることの不利な点は、移植性が失われることである。つまり、コードが特定のオペレーティングシステムにロックされてしまう。 しかし、教える目的のために、これを Windows でどのようにするのか見てみることにしよう。 これは Mingw または Cygwin でコンパイルできるCソースコードである。

 #include <windows.h>
 
 int main(void) {
     LPSTR text = "Hello, world!\n";
     DWORD charsWritten;
     HANDLE hStdout;
 
     hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
     WriteFile(hStdout, text, 14, &charsWritten, NULL);
     return 0;
 }

理想的には、「GetStdHandle」と「WriteFile」の戻り値により正しく働いたか確認したいところだが、これでも私たちの目的には十分である。 以下のようなアセンブリーコードが生成される。

         .file   "hello2.c"
         .def    ___main;        .scl    2;      .type   32;     .endef
         .text
 LC0:
         .ascii "Hello, world!\12\0"
 .globl _main
         .def    _main;  .scl    2;      .type   32;     .endef
 _main:
         pushl   %ebp
         movl    %esp, %ebp
         subl    $4, %esp
         andl    $-16, %esp
         movl    $0, %eax
         movl    %eax, -16(%ebp)
         movl    -16(%ebp), %eax
         call    __alloca
         call    ___main
         movl    $LC0, -4(%ebp)
         movl    $-11, (%esp)
         call    _GetStdHandle@4
         subl    $4, %esp
         movl    %eax, -12(%ebp)
         movl    $0, 16(%esp)
         leal    -8(%ebp), %eax
         movl    %eax, 12(%esp)
         movl    $14, 8(%esp)
         movl    -4(%ebp), %eax
         movl    %eax, 4(%esp)
         movl    -12(%ebp), %eax
         movl    %eax, (%esp)
         call    _WriteFile@20
         subl    $20, %esp
         movl    $0, %eax
         leave
         ret

C標準ライブラリーを一切使っていないにもかかわらず、生成されたコードは、それを初期化している。 また、多数の不必要なスタック操作がされている。単純化すると以下のようになる。

         .text
 LC0:
         .ascii "Hello, world!\12"
 .globl _main
 _main:
         pushl   %ebp
         movl    %esp, %ebp
         subl    $4, %esp
         pushl   $-11
         call    _GetStdHandle@4
         pushl   $0
         leal    -4(%ebp), %ebx
         pushl   %ebx
         pushl   $14
         pushl   $LC0
         pushl   %eax
         call    _WriteFile@20
         movl    $0, %eax
         leave
         ret

順々に解析していこう。

         pushl   %ebp
         movl    %esp, %ebp
         subl    $4, %esp

古いEBPを保ったまま、スタック上に4バイト確保する。これは、WriteFileはどこかに4バイトの値で書き込む文字数を保存しておく必要があるためである。

         pushl   $-11
         call    _GetStdHandle@4

定数STD_OUTPUT_HANDLE (つまり -11)をスタックにプッシュし、GetStdHandleを呼び出す。戻り値はEAXに入る。

         pushl   $0
         leal    -4(%ebp), %ebx
         pushl   %ebx
         pushl   $14
         pushl   $LC0
         pushl   %eax
         call    _WriteFile@20

WriteFileのパラメーターをプッシュし、WriteFileを呼び出す。 注意してほしいのは、Windowsの呼び出し規則では、パラメーターは右から左の順にプッシュされていなくてはいけないということである。 load-effective-address (lea) 命令は、EBPの値に-4を加え、表示された文字の数のためのスタックを保存した場所を与る。それは、EBX に保管され、スタックにプッシュされる。 また、注意してほしいのは、EAXはまだGetStdHandleコールの戻り値を保持しているということである。そのため単に直接それをプッシュした。

         movl    $0, %eax
         leave

ここでプログラムの戻り値をセットし、EBPとESPの値を「leave」命令を使って戻した。

補足説明[編集]

The GAS manual's AT&T Syntax Bugs sectionによると、


UnixWareのアセンブラーとおそらく他のAT&T由来のix86 Unixアセンブラーは、浮動小数点命令でソースと書き込み先レジスターが逆な命令な命令を生成する。 残念なことにgccとおそらく他の多くのプログラムはこの逆な構文を使用する。私たちはこれにはめられている。

例えば、

         fsub %st,%st(3)

では、%st(3)%st - %st(3)にアップデートされる。%st%st(3) - %stにアップデートされるのではない。 これは、2つのレジスターオペランドをとる全ての非可換な算術浮動小数点操作で起きる。つまり、読み込み元レジスターが%stであり、書き込み先レジスターが%st(i)になる。

注意してほしいのは、objdump -d -M intel も逆になったオペコードを使っていることである。よって違った逆アセンブラーをこれを確認するのに使う。 より詳しく知るには、http://bugs.debian.org/372528 を見ると良い。


アドレスオペランドの文法[編集]

アドレスを示すオペランドは、最大4個のパラメータを取ることができる。これはディスプレイスメント(ベースレジスタ, オフセットレジスタ, スケーラ)の形式をとる。 これは、インテル記法での[ベースレジスタ + ディスプレイスメント + オフセットレジスタ * スケーラ]という表記と同じ意味である。 パラメータ注の数値部分のいずれかあるいは両方は省略可能であり、同時に、レジスタ部分の一方は省略可能である。

 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に格納する

なお。「#」はWindows版NASMでのコメントアウトの記号である。その行でのコメントアウト以降の文字列は、アセンブルからは除去される。 Linuxなどウィンドウズ以外の場合では、コメントアウト記号がセミコロン「;」になっている場合もあるので、適宜に応用のこと。


オペレーションサフィックス[編集]

「pushl」とか「movl」とか、プッシュ命令やムーブ命令のうしろにエル「l」がついているが、こういうのがサフィックスである。

データサイズなどを指定するためにサフィックスを指定する必要がある。

GASのアセンブリ命令では、一般にオペランドがどのサイズのデータを扱うか指定するために、b、s、w、l、q、tのいずれかの文字をオペランドの最後に付ける。これをサフィックスという。

b
バイト(8ビット)
s
ショート (16ビット整数)またはsingle(32ビット浮動小数点数)
w
ワード(16ビット)
l
ロング(32ビット整数または64ビット浮動小数点数)
q
クワッド(64ビット)
t
10バイト(80ビット浮動小数点数)

サフィックスが指定されていない場合、GASはメモリをオペランドにとる命令は使用できない。GASは転送先のレジスタを指すオペランドのサイズが暗黙の内に指定されていることとする。

他の GAS についての読み物[編集]

GNU gas ドキュメントのページで、gas についてもっと知ることができる。

http://sourceware.org/binutils/docs-2.17/as/index.html

アセンブリ言語に無い命令[編集]

GASに限った話題ではないが、アセンブリ言語の専用命令には、C言語でいう「関数」に相当する機能は無い[12]。OSから関数を呼び出すことになる。LinuxでのOSでの関数の実装は、割り込みコントローラーや割り込みテーブルの制御をして、アセンブリ言語などをつかって関数の機能を実装している[12]、といわれている。

脚注および参考文献[編集]

  1. ^ segmentが指定されない場合、ほとんどの場合、ベース・レジスタespまたはebpでない限り、dsと仮定されます。この場合、アドレスはss
  2. ^ もしインデックス・レジスタがない場合、無意味なスケール・ファクターも同様に省略しなければなりません。
  3. ^ これは、コンパイラにバグがないことと、さらに重要なこととして、「あなたが書いたコードがあなたの意図を正しく実装していること」を前提としています。また、コンパイラはコードを最適化するために、低レベルの操作の順序を並べ替えることがあります。これにより、コードの全体的なセマンティクスは維持されますが、アセンブリの命令フローがアルゴリズムのステップと正確に一致しない可能性があります。
  4. ^ Igor Zhirkov 著、古川邦夫 監訳『低レベルプログラミング』、翔泳社、2018年01月19日 初版 第1刷 発行、30ページ
  5. ^ 内田公太・上川大介 著『自作エミュレータで学ぶX86アーキテクチャ』、株式会社マイナビ (※出版社名)、2015年8月30日 初版 第1刷 発行、109ページ
  6. ^ 内田公太・上川大介 著『自作エミュレータで学ぶX86アーキテクチャ』、株式会社マイナビ (※出版社名)、2015年8月30日 初版 第1刷 発行、106ページ
  7. ^ Igor Zhirkov 著、古川邦夫 監訳『低レベルプログラミング』、翔泳社、2018年01月19日 初版 第1刷 発行、17ページ
  8. ^ Q&Aで学ぶマイコン講座(30):スタックの役割 (1/3) 2019年12月3日に閲覧
  9. ^ Igor Zhirkov 著、古川邦夫 監訳『低レベルプログラミング』、翔泳社、2018年01月19日 初版 第1刷 発行、335ページ
  10. ^ 日向俊二 著『プログラムの不思議を解く』、カットシステム(※出版社名)、2016年9月10日 初版 第1刷 発行、26ページ
  11. ^ 日向俊二 著『プログラムの不思議を解く』、カットシステム(※出版社名)、2016年9月10日 初版 第1刷 発行、237ページ
  12. ^ 12.0 12.1 平田豊 著『超例解Linuxカーネルプログラミング』、C&R研究所、2019年7月23日 初版 第1刷、76ページ