x86, Arm, PowerPC のアセンブリコードを簡単に生成・比較する

## この文書の目的

この文書は主に教材など向けに、単純な C 言語のプログラムから Arm や PowerPC のアセンブリコードを生成する作業を記録したものです。同じく、実用でない目的のために手軽に環境を用意したい人のためにまとめてみました。

ただし Docker 環境があることを前提としています。
（一応書いておくと、今回私は Mac 向けの Docker Desktop ver. 2.3.0.5 を使って作業しました。）

## 準備

### アセンブリコードを出してみる

以下のシンプルな C プログラムをコンパイルして、アセンブリコードを出してみます。

```C:sample.c
int main()
{
  int a,b,c;
  a=3;
  b=6;
  c=a+b;
}
```

私の手元にある Macintosh で、cc コマンドを使って試します。

`$ cc -S -O0 -march=x86-64 sample.c `

オプションについて簡単に説明しておきます。

<dl>
<dt>-S</dt>
  <dd>コンパイル作業をアセンブリ言語生成の段階で止めます。拡張子 .s で生成されます。</dd>
<dt>-O</dt>
  <dd>最適化に関する指定です。今回は -O0 つまり最適化を止めています。<br>
  今どきは最適化によってソースコードからは想像もつかない変形が施されてしまい、アーキテクチャによる比較などが難しくなってしまいますから。</dd>
<dt>-march</dt>
  <dd>指定したアーキテクチャに対応したアセンブリ・コードを生成します。<br>
  今回は -march=x86-64 つまり x86 64bit アーキテクチャを指定しています。</dd>
</dl>

参考までに、以下に生成されたアセンブリ・コードのファイル、sample.s を示します。

```:sample.s
        .section        __TEXT,__text,regular,pure_instructions
        .build_version macos, 10, 15    sdk_version 10, 15
        .globl  _main                   ## -- Begin function main
        .p2align        4, 0x90
_main:                                  ## @main
        .cfi_startproc
## %bb.0:
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register %rbp
        xorl    %eax, %eax
        movl    $3, -4(%rbp)
        movl    $6, -8(%rbp)
        movl    -4(%rbp), %ecx
        addl    -8(%rbp), %ecx
        movl    %ecx, -12(%rbp)
        popq    %rbp
        retq
        .cfi_endproc
                                        ## -- End function
.subsections_via_symbols
```

#### 32bit コードが欲しい場合

これは 64bit アセンブリ・コードですが、多くの資料は 32bit の x86 命令に基づくものが多いでしょう。最新の MacOS では 32bit アプリケーションを生成できなくなりましたが、アセンブリ・コードまでならまだ生成してくれるようです。
`$ cc -S -O0 -march=i486 -m32 sample.c `
とすると、sample.s に i486 向けの（当然 32bit）コードを生成してくれます。

#### コンパイル・オプション

ちょっと乱暴ですが、誤ったアーキテクチャ名を指定して他にどのようなバリエーションが指定できるか調べることができます。

```bash
$ cc -O0 -S -march=xxxxx sample.c
error: unknown target CPU 'xxxxx'
note: valid target CPU values are: nocona, core2, penryn, bonnell, atom, silvermont, slm,
      goldmont, goldmont-plus, tremont, nehalem, corei7, westmere, sandybridge,
      corei7-avx, ivybridge, core-avx-i, haswell, core-avx2, broadwell, skylake,
      skylake-avx512, skx, cascadelake, cannonlake, icelake-client, icelake-server, knl,
      knm, k8, athlon64, athlon-fx, opteron, k8-sse3, athlon64-sse3, opteron-sse3,
      amdfam10, barcelona, btver1, btver2, bdver1, bdver2, bdver3, bdver4, znver1, x86-64
$
```

あるいは、以下のようにして（かなり長い）オプション・リストを表示させることもできます。

`cc -v --help`


#### Linux 向けコードが欲しい場合

MacOS ではシステムコールの呼び出し方などが Linux とは異なっており、そのあたりに関わるコードを見比べたい人にとっては、ここで MacOS native な cc コマンド（gccでも同じ）を使ったのは良くなかったかも知れません。
（参考：[初学者向け x86/MacOSX 64bit アセンブリ](https://qiita.com/yyasuda/items/3072f10d45cc4c0930f7) ）

そのような場合は、すぐ次に示す [gcc の Docker イメージ](https://hub.docker.com/_/gcc
)を使うと良いでしょう。

## Docker 環境で動く gcc 

今どきはなんでも Docker にあります。
https://hub.docker.com/_/gcc
に、GNU gcc のイメージがあります。ここの Description 部分に、なかなか魅力的な記述があります。

> Supported architectures: (more info) amd64, arm32v5, arm32v7, arm64v8, ppc64le, s390x

この [more info](https://github.com/docker-library/official-images#architectures-other-than-amd64) のリンクを追うと、Architectures other than amd64? として、各種アーキテクチャ向けイメージへのリンクがあります。素晴らしい。

## Arm アセンブリへのコンパイル

ARMv7 32-bit のアセンブリ・コードを作ってみましょう。
https://hub.docker.com/r/arm32v7/gcc
にあるものをただ動かすだけでOKです。

```bash
$ docker run -it arm32v7/gcc
.....
root@513d27af95d3:/# cat > sample.c
int main()
{
  int a,b,c;
  a=3;
  b=6;
  c=a+b;
}        ## ここで改行して Control-D （入力終了を意味する）
root@513d27af95d3:~# gcc -O0 -S -march=armv7 sample.c
root@513d27af95d3:~# 
```

もちろんこの gcc は Arm アーキテクチャ向けに調整されていますから、-march オプションを付けなくても Arm のコードを吐きます。とりあえず armv7 を明示指定して生成したコードを以下に示します。

```armasm:sample.s
        .arch armv7
        .eabi_attribute 28, 1
        .eabi_attribute 20, 1
        .eabi_attribute 21, 1
        .eabi_attribute 23, 3
        .eabi_attribute 24, 1
        .eabi_attribute 25, 1
        .eabi_attribute 26, 2
        .eabi_attribute 30, 6
        .eabi_attribute 34, 1
        .eabi_attribute 18, 4
        .file   "sample.c"
        .text
        .align  1
        .global main
        .arch armv7
        .syntax unified
        .thumb
        .thumb_func
        .fpu vfpv3-d16
        .type   main, %function
main:
        @ args = 0, pretend = 0, frame = 16
        @ frame_needed = 1, uses_anonymous_args = 0
        @ link register save eliminated.
        push    {r7}
        sub     sp, sp, #20
        add     r7, sp, #0
        movs    r3, #3
        str     r3, [r7, #12]
        movs    r3, #6
        str     r3, [r7, #8]
        ldr     r2, [r7, #12]
        ldr     r3, [r7, #8]
        add     r3, r3, r2
        str     r3, [r7, #4]
        movs    r3, #0
        mov     r0, r3
        adds    r7, r7, #20
        mov     sp, r7
        @ sp needed
        ldr     r7, [sp], #4
        bx      lr
        .size   main, .-main
        .ident  "GCC: (GNU) 10.2.0"
        .section        .note.GNU-stack,"",%progbits
```

## PowerPC アセンブリへのコンパイル

教材としては他のアーキテクチャとの比較が重要になるでしょう。
https://hub.docker.com/r/ppc64le/gcc
に、IBM POWER8 向けのgccがあります。これもただ動かすだけでOKです。

`$ docker run -it ppc64le/gcc`

以下のようにしてコンパイルすると、Power8 向けのコードを出力します。

```bash
root@6fbe652339fd:/# gcc -O0 -S sample.c 
```

```:sample.s
        .file   "sample.c"
        .machine power8
        .abiversion 2
        .section        ".text"
        .align 2
        .globl main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        std 31,-8(1)
        stdu 1,-64(1)
        .cfi_def_cfa_offset 64
        .cfi_offset 31, -8
        mr 31,1
        .cfi_def_cfa_register 31
        li 9,3
        stw 9,32(31)
        li 9,6
        stw 9,36(31)
        lwz 10,32(31)
        lwz 9,36(31)
        add 9,10,9
        stw 9,40(31)
        li 9,0
        mr 3,9
        addi 1,31,64
        .cfi_def_cfa 1, 0
        ld 31,-8(1)
        blr
        .long 0
        .byte 0,0,0,0,128,1,0,1
        .cfi_endproc
.LFE0:
        .size   main,.-main
        .ident  "GCC: (GNU) 10.2.0"
        .section        .note.GNU-stack,"",@progbits
```

この PPC 向けの gcc は -march ではなく -mcpu によってプロセッサタイプを指定することになるようです。

`gcc -S -O0 -mcpu=power8 sample.c `

```bash
root@513d27af95d3:/# gcc -S -O0 -mcpu=xxxxx sample.c 
gcc: error: unrecognized argument in option '-mcpu=xxxxx'
gcc: note: valid arguments to '-mcpu=' are: 401 403 405 405fp 440 440fp 464 464fp 476 476fp 505 601 602 603 603e 604 604e 620 630 740 7400 7450 750 801 821 823 8540 8548 860 970 G3 G4 G5 a2 cell e300c2 e300c3 e500mc e500mc64 e5500 e6500 ec603e native power10 power3 power4 power5 power5+ power6 power6x power7 power8 power9 powerpc powerpc64 powerpc64le rs64 titan
root@513d27af95d3:/# 
```

## 比較

おおっと比較のことを忘れかけていました。私がもともと比較したかったのは以下の部分でした。
つまり二つの値をセットして、加算して結果を残す、という流れです。

```C
  a=3;
  b=6;
  c=a+b;
```

x86 では以下のようになっていました。
（この資料に用がある人には不要な気がしますが、適当にコメントを付けてみました。）

```:x86-sample.s
        movl    $3, -4(%rbp)       # 3 を変数 a に対応するメモリ領域（ -4(%rbp) ) に書き込む
        movl    $6, -8(%rbp)       # 6 を変数 b に対応するメモリ領域（ -8(%rbp) ) に書き込む
        movl    -4(%rbp), %ecx     # 変数 a の領域にある値をレジスタ ecx に値を取り出し
        addl    -8(%rbp), %ecx     # 変数 b の領域にある値をレジスタ exx の値に加算（足し込む）
        movl    %ecx, -12(%rbp)    # ecx にある計算結果を変数 c に対応する領域（ -12(%rbp) ) に書き込む
```

加算処理に注目すると、1) あるメモリ領域にある値をレジスタに入れて、2) そのレジスタに別のメモリ領域の値を足し込み、3) その後、レジスタの値（加算結果）をメモリに書き込む、という流れになっています。
ベースレジスタ(?)となる rbp がどこから来たか、といったことは気にしないことにしましょう。

次に Arm ではこうです。

```armasm:arm-sample.s
        movs    r3, #3
        str     r3, [r7, #12]
        movs    r3, #6
        str     r3, [r7, #8]
        ldr     r2, [r7, #12]
        ldr     r3, [r7, #8]
        add     r3, r3, r2
        str     r3, [r7, #4]
```

メモリの値とレジスタの値を加算するのではなく、加算はレジスタ間で行い（r2 + r3 を r3 に入れる）、その後メモリに書き込んでいます。

次に PowerPC はこうでした。

```:ppc-sample.s
        li 9,3
        stw 9,32(31)
        li 9,6
        stw 9,36(31)
        lwz 10,32(31)
        lwz 9,36(31)
        add 9,10,9
        stw 9,40(31)
```

（ちょっと自信が無いのですが）やはり 31 番レジスタがベースとなっていて、加算はレジスタ間（今度は 9, 10 番レジスタ）で行っている事が分かります。

これでプロセッサ・アーキテクチャごとの命令やレジスタ構成の違い、処理手法の違いを実際に見比べることができたかと思います。
（ただし上で見たアセンブリコードはコンパイラが今回吐いたコードというだけで、これ以外の処理手法があり得ないわけでは無いことに注意して下さい。典型的な各プロセッサ向けの出力を見比べてみた、くらいの気分でどうぞ。）

## おわりに

教養科目向けに CPU ごとの命令セットの違いを示す（互換性、という概念の一例として）ことをしているのですが、そのためのサンプルを取り出す作業を何年かごとに行っています。以前は x86 Linux と PowerPC Mac と、あとは手で書いた 6502 コードくらいで比較していたのですが、最近は PowerPC より断然 Arm の方が身近になった（ex. smartphone) こともあり、Arm に切り替えました。
その際、昔は手間だったクロスコンパイル環境などが、今どきは何でも Docker で簡単に用意できる事が分かり、ちょっと嬉しくなったのでまとめてみた次第です。