この文書の目的
この文書は主に教材など向けに、単純な C 言語のプログラムから Arm や PowerPC のアセンブリコードを生成する作業を記録したものです。同じく、実用でない目的のために手軽に環境を用意したい人のためにまとめてみました。
ただし Docker 環境があることを前提としています。
(一応書いておくと、今回私は Mac 向けの Docker Desktop ver. 2.3.0.5 を使って作業しました。)
準備
アセンブリコードを出してみる
以下のシンプルな C プログラムをコンパイルして、アセンブリコードを出してみます。
int main()
{
int a,b,c;
a=3;
b=6;
c=a+b;
}
私の手元にある Macintosh で、cc コマンドを使って試します。
$ cc -S -O0 -march=x86-64 sample.c
オプションについて簡単に説明しておきます。
- -S
- コンパイル作業をアセンブリ言語生成の段階で止めます。拡張子 .s で生成されます。
- -O
- 最適化に関する指定です。今回は -O0 つまり最適化を止めています。
今どきは最適化によってソースコードからは想像もつかない変形が施されてしまい、アーキテクチャによる比較などが難しくなってしまいますから。 - -march
- 指定したアーキテクチャに対応したアセンブリ・コードを生成します。
今回は -march=x86-64 つまり x86 64bit アーキテクチャを指定しています。
参考までに、以下に生成されたアセンブリ・コードのファイル、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)コードを生成してくれます。
コンパイル・オプション
ちょっと乱暴ですが、誤ったアーキテクチャ名を指定して他にどのようなバリエーションが指定できるか調べることができます。
$ 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 アセンブリ )
そのような場合は、すぐ次に示す gcc の Docker イメージを使うと良いでしょう。
Docker 環境で動く gcc
今どきはなんでも Docker にあります。
https://hub.docker.com/_/gcc
に、GNU gcc のイメージがあります。ここの Description 部分に、なかなか魅力的な記述があります。
Supported architectures: (more info) amd64, arm32v5, arm32v7, arm64v8, ppc64le, s390x
この more info のリンクを追うと、Architectures other than amd64? として、各種アーキテクチャ向けイメージへのリンクがあります。素晴らしい。
Arm アセンブリへのコンパイル
ARMv7 32-bit のアセンブリ・コードを作ってみましょう。
https://hub.docker.com/r/arm32v7/gcc
にあるものをただ動かすだけでOKです。
$ 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 を明示指定して生成したコードを以下に示します。
.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 向けのコードを出力します。
root@6fbe652339fd:/# gcc -O0 -S sample.c
.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
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:/#
比較
おおっと比較のことを忘れかけていました。私がもともと比較したかったのは以下の部分でした。
つまり二つの値をセットして、加算して結果を残す、という流れです。
a=3;
b=6;
c=a+b;
x86 では以下のようになっていました。
(この資料に用がある人には不要な気がしますが、適当にコメントを付けてみました。)
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 ではこうです。
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 はこうでした。
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 で簡単に用意できる事が分かり、ちょっと嬉しくなったのでまとめてみた次第です。