機械語
概要[編集]
機械語 (きかいご) とは、コンピューターのプロセッサーが直接解釈・実行できる命令のことです。 プロセッサーが異なると、機械語の体系も異なります。 過去のソフトウェアとの互換性などの理由で、同じプロセッサーでも「命令モード」によって機械語の体系を切り替えることがあります。
ソースコードからどんな機械語が生成されるか[編集]
- フィボナッチ数を返す関数(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つの命令にいくつもプリフィックスを前置する場合まであります。
まとめ[編集]
- 異なるプロセッサーでは、同じ高級言語のコードが全く違う機械語命令列にコンパイルされる。
- 同じプロセッサーでも命令モードによって、同じ高級言語のコードが全く違う機械語命令列にコンパイルされる。
- 再帰などのプログラム構造もコンパイラーによって(意味解析され)等価のより速度的に(あるいはメモリーフットプリント的に)優れたコードに置き換えられる(事がある)。
機械語 = バイナリーデーター? |
機械語はバイナリーデーターですが、全てのバイナリーデーターが機械語ではありません。 例えば、画像データーや映像データーはバイナリーデーターですが機械語ではありません。 実行ファイルもバイナリーデーターですが、オペレーションシステムが「どの様に配置するのか?」「どの位置から実行するのか?」あるいは「初期化済みのデータ領域の値」など機械語以外の付帯的な情報(一般にヘッダーと呼ばれます)を重層的に持っているので、機械語を含んでいますが機械語そのものではありません。 機械語は、アセンブラのニーモニックに一対一で対応するコードと理解するとわかりやすいと思います。 |
脚註[編集]
- ^ ARMアーキテクチャでは、多くの命令でキャリーなどのコンディションコードによって実行する・しないを制御できるのが大きな特徴で、他のアーキテクチャではジャンプ命令以外でコンディションコードによって実行する・しないを制御できるのは稀です(他にはIA-64がプレディケート可能です)。