RISC-Vを使用したアセンブリ言語入門 〜2. アセンブリ言語を見てみよう〜


前回の記事 : https://qiita.com/widedream/items/c135df3277599fa8ebba

はじめに

今回は実際にC言語のプログラムからどのようなアセンブリ言語が生成されるかを見ていく.
ついでにC言語のプログラムのコンパイルの流れについても説明していく.

使用するC言語のプログラム

今回は積和演算 (multiply-add) を計算する簡単なプログラムを例にアセンブリ言語を見てみる.
積和演算とは乗算の結果を順次加算する演算である.

関数として記述すると以下のような簡単なプログラムになる.
これをmadd.cとして作成する.

madd.c
int madd(int a, int b, int c)
{
    int result = a + b * c;
    return result;
}

これに合わせて配列a,bの各要素を掛け合わせた値をすべて加算した結果を計算するプログラムをmain.cとして作成した.

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の説明を通して記述していきたいと思う.