機械語

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

概要[編集]

機械語 (きかいご) とは、コンピューターのプロセッサーが直接解釈・実行できる命令のことです。 プロセッサーが異なると、機械語の体系も異なります。 過去のソフトウェアとの互換性などの理由で、同じプロセッサーでも「命令モード」によって機械語の体系を切り替えることがあります。

ソースコードからどんな機械語が生成されるか[編集]

フィボナッチ数を返す関数(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つの命令にいくつもプリフィックスを前置する場合まであります。

まとめ[編集]

  • 異なるプロセッサーでは、同じ高級言語のコードが全く違う機械語命令列にコンパイルされる。
  • 同じプロセッサーでも命令モードによって、同じ高級言語のコードが全く違う機械語命令列にコンパイルされる。
  • 再帰などのプログラム構造もコンパイラーによって(意味解析され)等価のより速度的に(あるいはメモリーフットプリント的に)優れたコードに置き換えられる(事がある)。
機械語 = バイナリーデーター?

機械語はバイナリーデーターですが、全てのバイナリーデーターが機械語ではありません。

例えば、画像データーや映像データーはバイナリーデーターですが機械語ではありません。

実行ファイルもバイナリーデーターですが、オペレーションシステムが「どの様に配置するのか?」「どの位置から実行するのか?」あるいは「初期化済みのデータ領域の値」など機械語以外の付帯的な情報(一般にヘッダーと呼ばれます)を重層的に持っているので、機械語を含んでいますが機械語そのものではありません。

機械語は、アセンブラのニーモニックに一対一で対応するコードと理解するとわかりやすいと思います。


脚註[編集]

  1. ^ ARMアーキテクチャでは、多くの命令でキャリーなどのコンディションコードによって実行する・しないを制御できるのが大きな特徴で、他のアーキテクチャではジャンプ命令以外でコンディションコードによって実行する・しないを制御できるのは稀です(他にはIA-64がプレディケート可能です)。