機械語
概要
編集機械語は、コンピュータが直接実行できるプログラムの形式であり、通常は2進数で表されます。コンピュータのプロセッサは、機械語プログラムの命令を一つずつ読み込み、解釈して実行します。機械語は、高水準のプログラム言語(例えばC言語など)と比較すると、非常に低レベルの言語であり、直接ハードウェアに対するアクセスが可能です。
機械語は、一般的にアセンブリ言語という人間が理解しやすい形式に変換されます。アセンブリ言語は、機械語と1対1に対応する命令を持つため、プログラマにとっては理解しやすいものになっています。
機械語は、コンピュータのプロセッサが理解できる唯一のプログラム形式であるため、コンピュータの動作を理解する上で非常に重要な概念です。また、機械語を直接書くことで、コンピュータの性能を最大限に引き出すことができます。
ソースコードからどんな機械語が生成されるか
編集- フィボナッチ数を返す関数(C言語)
int fibo(int n) { if (n == 0 || n == 1) return n; return fibo(n-1) + fibo(n-2); }
32bitARMプロセッサーの例
編集- コンパイラーによって生成されたコード
fibo.o: file format elf32-littlearm Disassembly of section .text: 00000000 <fibo>: int fibo(int n) { 0: e92d4830 push {r4, r5, fp, lr} 4: e28db008 add fp, sp, #8 if (n == 0 || n == 1) 8: e3500002 cmp r0, #2 c: e1a04000 mov r4, r0 return n; return fibo(n-1) + fibo(n-2); } 10: 31a00004 movcc r0, r4 14: 38bd4830 popcc {r4, r5, fp, lr} 18: 312fff1e bxcc lr return fibo(n-1) + fibo(n-2); 1c: e2440001 sub r0, r4, #1 20: ebfffffe bl 0 <fibo> 24: e1a05000 mov r5, r0 28: e2440002 sub r0, r4, #2 2c: ebfffffe bl 0 <fibo> 30: e0800005 add r0, r0, r5 } 34: e8bd4830 pop {r4, r5, fp, lr} 38: e12fff1e bx lr
このコードは32bitARMプロセッサをターゲットとしたもので、すべての命令が32ビット長なのでアセンブラーの初学者向きです。 また、ARMアーキテクチャは多くのスマートフォンやタブレットで採用されていたり、組込み用途での採用も多いので最も普及しているコンピュータ・アーキテクチャの1つです。
10: 31a00004 movcc r0, r4
10:
がアドレス、31a00004
が機械語命令の16進数表現です。- movcc は Move on キャリークリアーで、キャリーフラッグ(桁上りがあった時にセットされる)かセットされたときだけ r4レジスターの値を r0レジスターに代入します[1]
Thumb命令の例
編集ARMプロセッサはThumbと呼ばれるコード効率の向上を意図した16ビット長のThumb命令モードを持っています。
- コンパイラーによって生成されたコード
fibo.o: file format elf32-littlearm Disassembly of section .text: 00000000 <fibo>: int fibo(int n) { 0: b5b0 push {r4, r5, r7, lr} 2: af02 add r7, sp, #8 4: 4604 mov r4, r0 if (n == 0 || n == 1) 6: 2802 cmp r0, #2 8: d201 bcs.n e <fibo+0xe> return n; return fibo(n-1) + fibo(n-2); } a: 4620 mov r0, r4 c: bdb0 pop {r4, r5, r7, pc} return fibo(n-1) + fibo(n-2); e: 1e60 subs r0, r4, #1 10: f7ff fffe bl 0 <fibo> 14: 4605 mov r5, r0 16: 1ea0 subs r0, r4, #2 18: f7ff fffe bl 0 <fibo> 1c: 4428 add r0, r5 } 1e: bdb0 pop {r4, r5, r7, pc}
10: f7ff fffe bl 0 <fibo>
- bl は Brunch with link で、次の命令のアドレス(ここで言えば 14:)を lrレジスター(リンクレジスター)に保存し、指定されたアドレス(この場合は <fibo>; 関数自身なので再帰です)にジャンプします。多くのプロセッサーでは call と呼ばれスタックに次の命令のアドレスを積んだ後に関数やサプルーチンにジャンプしますが、ARMでは lr が戻り番地を保存される目的に使われ、リーフプロシージャー(それ自身は関数やサブルーチンを呼び出さないプロシージャー)の効率を向上させています。
- Thumb命令は概ね命令長は16ビットで、長いオペランドが必要な命令(この場合は bl)だけが追加のオペランドを持ちます。
64bitARMプロセッサーの例
編集ARMアーキテクチャーは、ARMv8-Aから64ビットモードアーキテクチャーAArch64を採用してます。 AArch64は、32本の64ビットレジスター(うち1本はスタックポインター兼ゼロレジスタ、1本は戻り番地を保持するリンクレジスタ)を持ち、Xnnレジスターは64ビットレジスターのnn本目、WnnはXnnレジスターの下位32ビットです。
- コンパイラーによって生成されたコード
fibo.o: file format elf64-littleaarch64 Disassembly of section .text: 0000000000000000 <fibo>: int fibo(int n) { 0: a9be7bfd stp x29, x30, [sp, #-32]! 4: a9014ff4 stp x20, x19, [sp, #16] 8: 910003fd mov x29, sp c: 2a1f03f3 mov w19, wzr if (n == 0 || n == 1) 10: 71000814 subs w20, w0, #0x2 14: 540000e3 b.cc 30 <fibo+0x30> // b.lo, b.ul, b.last return n; return fibo(n-1) + fibo(n-2); 18: 51000400 sub w0, w0, #0x1 1c: 94000000 bl 0 <fibo> 20: 2a0003e8 mov w8, w0 24: 2a1403e0 mov w0, w20 28: 0b130113 add w19, w8, w19 2c: 17fffff9 b 10 <fibo+0x10> } 30: 0b130000 add w0, w0, w19 34: a9414ff4 ldp x20, x19, [sp, #16] 38: a8c27bfd ldp x29, x30, [sp], #32 3c: d65f03c0 ret
wzrはゼロレジスターを32bitで参照しています、spはスタックポインターでアドレス演算の文脈と左辺値の文脈ではスタックポインター、右辺値の場合はゼロレジスターになりレジスタインデックス(31番)を共有しています。 aarch64 では32bitARMと違って全ての命令に条件フラッグ参照が着くわけではないので、どちらかというと Thumb に似ていますが最小命令サイズは32bitです。
amd64プロセッサーの例
編集- 同じコードをamd64向けにコンパイル
fibo.o: file format elf64-x86-64-freebsd Disassembly of section .text: 0000000000000000 <fibo>: int fibo(int n) { 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 41 56 push %r14 6: 53 push %rbx 7: 89 fb mov %edi,%ebx 9: 45 31 f6 xor %r14d,%r14d if (n == 0 || n == 1) c: 83 ff 02 cmp $0x2,%edi f: 72 16 jb 27 <fibo+0x27> 11: 45 31 f6 xor %r14d,%r14d return n; return fibo(n-1) + fibo(n-2); 14: 8d 7b ff lea -0x1(%rbx),%edi 17: e8 00 00 00 00 call 1c <fibo+0x1c> 1c: 83 c3 fe add $0xfffffffe,%ebx 1f: 41 01 c6 add %eax,%r14d if (n == 0 || n == 1) 22: 83 fb 01 cmp $0x1,%ebx 25: 77 ed ja 14 <fibo+0x14> return fibo(n-1) + fibo(n-2); 27: 41 01 de add %ebx,%r14d } 2a: 44 89 f0 mov %r14d,%eax 2d: 5b pop %rbx 2e: 41 5e pop %r14 30: 5d pop %rbp 31: c3 ret
amd64(X86-64とも)の命令は最小単位は1バイトで、バイト数あたりの操作が多いのが特徴です。 逆アッセンブルされたコードを読む限り不便は感じませんが、ハンドディスアッセンブルする場合は1バイトずれるとまるで違った意味になるのが厄介で、プロセッサーの中でも数命令先の命令を読み込み実行効率を上げる為に命令の切れ目を探すことが性能向上のボトルネックになっています(ARMなら次の命令は4バイト先と決まっているので深い先読みが相対的に容易)。
14: 8d 7b ff lea -0x1(%rbx),%edi
14:
がアドレス、8d 7b ff
が機械語命令の16進数表現です。- leaは Load effective address で、通常は
-0x1(%rbx)
は rbx レジスタの値から1引いたアドレスの値へのアクセスを表しますが、LEA ではその時にアドレスバスに出る値(Effective address)を第二オペランド(この場合は edi レジスタ)にセットします。内部的にはアドレス計算器を数値演算に転用しています。 - またコードにはARMにはあった再帰のコードがなくループに置き換えられています。
x86(32ビット)のコード
編集- x86(32ビット)のコード
fibo.o: file format elf32-i386-freebsd Disassembly of section .text: 00000000 <fibo>: int fibo(int n) { 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 57 push %edi 4: 56 push %esi 5: 8b 7d 08 mov 0x8(%ebp),%edi 8: 31 f6 xor %esi,%esi if (n == 0 || n == 1) a: 83 ff 02 cmp $0x2,%edi d: 72 18 jb 27 <fibo+0x27> f: 31 f6 xor %esi,%esi return n; return fibo(n-1) + fibo(n-2); 11: 8d 47 ff lea -0x1(%edi),%eax 14: 50 push %eax 15: e8 fc ff ff ff call 16 <fibo+0x16> 1a: 83 c4 04 add $0x4,%esp 1d: 83 c7 fe add $0xfffffffe,%edi 20: 01 c6 add %eax,%esi if (n == 0 || n == 1) 22: 83 ff 01 cmp $0x1,%edi 25: 77 ea ja 11 <fibo+0x11> return fibo(n-1) + fibo(n-2); 27: 01 fe add %edi,%esi } 29: 89 f0 mov %esi,%eax 2b: 5e pop %esi 2c: 5f pop %edi 2d: 5d pop %ebp 2e: c3 ret
amd64は、x86のアーキテクチャーを拡張する形で命令セットを設計しているので、両者は似通っていますが相応の差異があります。
- amd64
1: 48 89 e5 mov %rsp,%rbp
- x86
1: 89 e5 mov %esp,%ebp
- amd64では RSPレジスターの内容をRBPレジスタにコピーしており 48 が前置され3バイト。
- x86では ESPレジスターの内容をEBPレジスタにコピーしており 48 がなく、2バイトです。
- RSPとRBPは64ビットレジスター、ESPとEBPは32ビットのレジスターです。
- 48はREXプリフィックスの一種で、「REX.w=オペランドサイズを64ビットにする。」を意味します。
- x86/amd64はこの様なプリフィックスがいくつもあり、1つの命令にいくつもプリフィックスを前置する場合まであります。
まとめ
編集- 異なるプロセッサーでは、同じ高級言語のコードが全く違う機械語命令列にコンパイルされる。
- 同じプロセッサーでも命令モードによって、同じ高級言語のコードが全く違う機械語命令列にコンパイルされる。
- 再帰などのプログラム構造もコンパイラーによって(意味解析され)等価のより速度的に(あるいはメモリーフットプリント的に)優れたコードに置き換えられる(事がある)。
機械語はバイナリーデータですが、全てのバイナリーデータが機械語ではありません。
例えば、画像データや映像データはバイナリーデータですが機械語ではありません。
実行ファイルもバイナリーデータですが、オペレーションシステムが「どの様に配置するのか?」「どの位置から実行するのか?」あるいは「初期化済みのデータ領域の値」など機械語以外の付帯的な情報(一般にヘッダーと呼ばれます)を重層的に持っているので、機械語を含んでいますが機械語そのものではありません。
機械語は、アセンブラのニーモニックに一対一で対応するコードと理解するとわかりやすいと思います。
用語集
編集機械語に関する用語集
- オペコード(Opcode) - 機械語命令の操作コード。特定の動作を実行するための識別子。
- オペランド(Operand) - オペコードによって指定された操作対象。演算の対象となる数値やアドレス。
- レジスタ(Register) - CPU内にある高速なメモリ領域で、演算に使用されるデータやアドレスを格納するために使用される。
- フラグ(Flag) - CPUの状態を示すビットで、演算の結果を表す。フラグは、CPU内部で条件分岐を行うために使用される。
- メモリアドレス(Memory Address) - メモリ内の特定の場所を示す番号。機械語のオペランドとして使用されることが多い。
- メモリマップドI/O(Memory-mapped I/O) - I/Oデバイスを制御するために、メモリアドレスを使用する方法。
- ファイルI/O(File I/O) - ディスクやネットワーク上のファイルを操作するために使用される命令。
- 命令ポインタ(Instruction Pointer) - CPUが次に実行する機械語命令のアドレスを示すレジスタ。
- サブルーチン(Subroutine) - 他の部分から呼び出される、独立した機械語のブロック。
- スタック(Stack) - プログラム内で一時的なデータを格納するためのメモリ領域。
- エンディアン(Endian) - データの並び順を示す方法。リトルエンディアンは、最下位バイトから順にデータを配置する方式。ビッグエンディアンは、最上位バイトから順にデータを配置する方式。
- マシンサイクル(Machine Cycle) - CPUが一つの命令を実行するために必要なサイクル数。
- 命令セット(Instruction Set) - 特定のCPUが実行できる機械語命令の集合。
- アセンブラ(Assembler) - アセンブリ言語を機械語に変換するプログラム。
- リンカ(Linker) - 複数のオブジェクトファイルを結合して、実行可能なプログラムを生成するプログラム。
- デバッガ(Debugger) - プログラムの実行中に、機械語命令やレジスタの値を監視し、プログラムの動作を解析するツール。
- トレース(Trace) - プログラムの実行中に、実行された命令やメモリアクセスなどの履歴を保存すること。
- ブレークポイント(Breakpoint) - プログラムの実行中に、特定の命令の実行を一時停止し、デバッグのために命令の内容やレジスタの値を確認するために使用されるポイント。
- クロスコンパイラ(Cross-compiler) - 特定のCPU向けに、異なるプラットフォームでコンパイルするためのコンパイラ。
- リバースエンジニアリング(Reverse Engineering) - プログラムやハードウェアの動作を解析して、仕様や設計図を作成するプロセス。
- コンパイル(Compile) - 高水準言語で書かれたプログラムを、機械語に変換するプロセス。
- リンク(Link) - コンパイルされた複数のオブジェクトファイルを、実行可能なプログラムに結合するプロセス。
- アセンブル(Assemble) - アセンブリ言語で書かれたプログラムを、機械語に変換するプロセス。
- ローダ(Loader) - プログラムをメモリに読み込み、実行可能な状態にするプログラム。
- バイトコード(Bytecode) - 実行環境に依存しない、仮想マシン上で実行される機械語。
- オーバーフロー(Overflow) - データ型の最大値を超える演算が行われた場合に発生する、予期しない結果のこと。
- セグメンテーション違反(Segmentation Fault) - プログラムがメモリの範囲外をアクセスしようとした場合に発生するエラー。
- プロセス(Process) - プログラムの実行中に割り当てられる、メモリやレジスタ、実行状態などのリソースの集合。
- マルチプロセッシング(Multiprocessing) - 複数のプロセッサを使用して、プログラムを並列に処理する方式。
脚註
編集- ^ ARMアーキテクチャでは、多くの命令でキャリーなどのコンディションコードによって実行する・しないを制御できるのが大きな特徴で、他のアーキテクチャではジャンプ命令以外でコンディションコードによって実行する・しないを制御できるのは稀です(他にはIA-64がプレディケート可能です)。