前回の記事 : https://qiita.com/widedream/items/c135df3277599fa8ebba
はじめに
今回は実際にC言語のプログラムからどのようなアセンブリ言語が生成されるかを見ていく.
ついでにC言語のプログラムのコンパイルの流れについても説明していく.
使用するC言語のプログラム
今回は積和演算 (multiply-add) を計算する簡単なプログラムを例にアセンブリ言語を見てみる.
積和演算とは乗算の結果を順次加算する演算である.
関数として記述すると以下のような簡単なプログラムになる.
これをmadd.cとして作成する.
int madd(int a, int b, int c)
{
int result = a + b * c;
return result;
}
これに合わせて配列a,bの各要素を掛け合わせた値をすべて加算した結果を計算するプログラムをmain.cとして作成した.
#include <stdio.h>
int madd(int a, int b, int c);
int main(void)
{
int a[3] = {1, 3, 6};
int b[3] = {2, 5, 8};
int sum = 0;
for(int i=0; i<3; i++) {
sum = madd(sum, a[i], b[i]);
}
printf("sum = %d\n", sum);
return 0;
}
C言語プログラムのコンパイルのプロセス
以下ではC言語のプログラムのコンパイルの流れについて,通常通りgccのコマンドを1回使用する場合とコマンドを複数使用してアセンブリ言語などの中間状態を経由し個別にコンパイルする場合と2通りに分けて説明する.
通常のC言語プログラムのコンパイル
この2つのソースコードを組み合わせたプログラムは以下のコマンドを実行することでSpikeで実行できるはずだ.
$ riscv32-unknown-elf-gcc -o madd main.c madd.c
$ pk
$ spike pk madd
bbl loader
sum = 65
通常はriscv32-unknown-elf-gcc
コマンドを使用すると以下のプロセスが自動で順番に実行される.
- コンパイル : C言語のソースコードをアセンブリ言語に変換する
- アセンブル : アセンブリ言語のソースコードをオブジェクトファイルに変換する (内部では
riscv32-unknown-elf-as
が実行) - リンク : 複数のオブジェクトファイルやライブラリをリンクする (内部では
riscv32-unknown-elf-ld
が実行)
実際のアセンブリ言語を見てみたいので,それぞれのプロセスを個別に実行してみる.
C言語プログラムの個別のコンパイル
まずC言語のソースコードをアセンブリ言語に変換する"コンパイル"を行うには以下のコマンドを実行する.
$ riscv32-unknown-elf-gcc -S madd.c
コンパイルが成功するとmadd.s
が生成される.
madd.s
を見てみると1行ごとに文字列が羅列している.
これがアセンブリ言語で記述された命令列だ.
$ cat madd.s
.file "madd.c"
.option nopic
.text
.align 2
.globl madd
.type madd, @function
madd:
addi sp,sp,-48
sw s0,44(sp)
addi s0,sp,48
sw a0,-36(s0)
sw a1,-40(s0)
sw a2,-44(s0)
lw a4,-40(s0)
lw a5,-44(s0)
mul a5,a4,a5
lw a4,-36(s0)
add a5,a4,a5
sw a5,-20(s0)
lw a5,-20(s0)
mv a0,a5
lw s0,44(sp)
addi sp,sp,48
jr ra
.size madd, .-madd
.ident "GCC: (GNU) 8.1.0"
ちなみにmain.c
の方も同様に"コンパイル"を行うことでmain.s
が生成される.
$ riscv32-unknown-elf-gcc -S main.c
次に生成されたアセンブリ言語のソースコードを使用して以下のコマンドを実行し"アセンブル"を行う.
$ riscv32-unknown-elf-gcc -c madd.s
"アセンブル"が成功するとオブジェクトファイルmadd.o
が生成される.
この段階ではライブラリなどがリンクされていないため,実行可能ファイルではない.
オブジェクトファイルはバイナリであるが,機械語で記述されているので,riscv32-unknown-elf-objdump
コマンドを使ってアセンブリ言語を出力させる"逆アセンブル"を行うことができる.
$ riscv32-unknown-elf-objdump -d madd.o
madd.o: file format elf32-littleriscv
Disassembly of section .text:
00000000 <madd>:
0: fd010113 addi sp,sp,-48
4: 02812623 sw s0,44(sp)
8: 03010413 addi s0,sp,48
c: fca42e23 sw a0,-36(s0)
10: fcb42c23 sw a1,-40(s0)
14: fcc42a23 sw a2,-44(s0)
18: fd842703 lw a4,-40(s0)
1c: fd442783 lw a5,-44(s0)
20: 02f707b3 mul a5,a4,a5
24: fdc42703 lw a4,-36(s0)
28: 00f707b3 add a5,a4,a5
2c: fef42623 sw a5,-20(s0)
30: fec42783 lw a5,-20(s0)
34: 00078513 mv a0,a5
38: 02c12403 lw s0,44(sp)
3c: 03010113 addi sp,sp,48
40: 00008067 ret
先ほどのmadd.s
とほぼ同じアセンブリ言語が出力されており,一番左端に0, 4, 8, ...
のようなメモリアドレスが振られている.
ちなみにmain.s
の方も同様に"アセンブル"を行うことでmain.o
が生成される.
$ riscv32-unknown-elf-gcc -c main.s
最後に生成されたオブジェクトファイルの"リンク"を行う.
オブジェクトファイルのリンクにはriscv32-unknown-elf-ld
コマンドを使用することもできるが,それぞれのオブジェクトファイルのほかにも_start
のシンボルを含むスタートアップルーチンやprintf
などの関数を含むlibc, libgccなどのライブラリのリンクが必要なので,riscv32-unknown-elf-gcc
コマンドを使用する.
$ riscv32-unknown-elf-gcc -o madd main.o madd.o
(riscv32-unknown-elf-ld
コマンドを使用する場合は正しくライブラリやスタートアップルーチンののパスを設定しないと正しくコンパイルできないっぽい)
コマンドの実行が成功するとバイナリであるmadd
が生成される.
C言語の分割したコンパイルについてはこのページなどが詳しい.
最終的なバイナリファイルの逆アセンブル
生成したバイナリファイルであるmadd
もオブジェクトファイルと同様に機械語で記述されているので,riscv32-unknown-elf-objdump
コマンドを使って"逆アセンブル"を行うことができる.
$ riscv32-unknown-elf-objdump -d madd | less
---
madd: file format elf32-littleriscv
Disassembly of section .text:
00010074 <_start>:
10074: 00013197 auipc gp,0x13
10078: 56c18193 addi gp,gp,1388 # 235e0 <__global_pointer$>
1007c: 82c18513 addi a0,gp,-2004 # 22e0c <_edata>
10080: 88818613 addi a2,gp,-1912 # 22e68 <_end>
10084: 40a60633 sub a2,a2,a0
10088: 00000593 li a1,0
1008c: 370000ef jal ra,103fc <memset>
10090: 00000517 auipc a0,0x0
10094: 26850513 addi a0,a0,616 # 102f8 <__libc_fini_array>
10098: 21c000ef jal ra,102b4 <atexit>
1009c: 2b8000ef jal ra,10354 <__libc_init_array>
100a0: 00012503 lw a0,0(sp)
100a4: 00410593 addi a1,sp,4
100a8: 00000613 li a2,0
100ac: 0fc000ef jal ra,101a8 <main>
100b0: 2180006f j 102c8 <exit>
000100b4 <_fini>:
100b4: 00008067 ret
...
リンクされたスタートアップルーチンやライブラリがリンクされているので,madd.o
のときよりも逆アセンブルの結果が大量になっていることが分かる.
スタートアップルーチンなどの説明は次回以降に行う予定.
おわりに
今回は実際のC言語のプログラムを使用してコンパイルの流れと実際に生成されるアセンブリ言語について見ていった.
とりあえずC言語のプログラムが何かの命令列に変換されているということが分かると思う.
次回は実際に生成された命令列がどのようなアセンブリ言語なのかということをRISC-VのISAの説明を通して記述していきたいと思う.