リンカ基本事項 リロケーションの導入まで

今回シンボルに関する基本事項とリロケーションの導入まで行っていく。

ローカルシンボルが外部ファイルから参照できないことを確認する。そのために以下のsym0.sとsym1.sという2つのアセンブリファイルを用意する。

_start:
    mov $local_symbol, %rax
    mov $global_symbol, %rax
 .global global_symbol

local_symbol:
    nop
global_symbol:
    nop

これらのファイルをコンパイルしてからリンクを試みると以下のようなエラーが発生する。

$ as sym0.s -o sym0.o
$ as sym1.s -o sym1.o
$ ld sym0.o sym1.o
ld: warning: cannot find entry symbol _start; defaulting to 0000000000400078
sym0.o: In function `_start':
(.text+0x3): undefined reference to `local_symbol'

まず一つ目の警告について、エントリーシンボルである_startシンボルがないと言っている。プログラムは基本的に_startというシンボルから始まることが決まっている。つまり最初はプログラムをメモリ上にロードするブートローダが動くようになっており、そのブートローダのプログラムに普通は_startシンボルが記述されている。普通にgccでCソースコードから実行ファイルを作るときは、自動的にブートローダプログラムもリンクしてくれるのだが、今回はldコマンドでブートローダプログラムを指定していないので、_startシンボルが見つかっていない。ブートローダプログラムはcrt.Sというファイルであることが多い。

実際、gcc -v ret.cを実行すると、リンクのフェイズで/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crt1.oというファイルが最初にリンク対象ファイルに持ってこられていて、この中身を以下のコマンドで見てみると、確かに_startシンボルが存在していることがわかる。

$ objdump -d /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crt1.o
Disassembly of section .text:

0000000000000000 <_start>:
   0:   31 ed                   xor    %ebp,%ebp
   2:   49 89 d1                mov    %rdx,%r9
   5:   5e                      pop    %rsi
   6:   48 89 e2                mov    %rsp,%rdx
   9:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
   d:   50                      push   %rax
   e:   54                      push   %rsp
   f:   49 c7 c0 00 00 00 00    mov    $0x0,%r8
  16:   48 c7 c1 00 00 00 00    mov    $0x0,%rcx
  1d:   48 c7 c7 00 00 00 00    mov    $0x0,%rdi
  24:   e8 00 00 00 00          callq  29 <_start+0x29>
  29:   f4                      hlt

もう一つこれはエラーだが、local_symbolというシンボルが定義されていないというエラーを出している。これは今回local_symbol.global ~のように定義しておらずローカルシンボルとなっており、別のファイルからは参照できないからである。つまりこのように外部のシンボル(ラベル)に飛びたい時は、グローバルシンボルとして定義する必要があることがわかる。

ここまででラベルについて述べてきたが、ここからはリロケーションについて述べていく。オブジェクトファイルには「セクションXのシンボルYをオフセットZで埋めてね」というようなリロケーション情報が含まれている。このリロケーション情報をもとにリンカはバイナリパッチを行い、プログラムのメモリ上の配置を決める。具体的にメモリ上のどこに配置するかはリンカによって決まっている。

リロケーションを理解するために以下のreloc.s, reloc_labe.sという二つのアセンブリファイルを用意する。

 .globl    _start
_start:
    movl $ref_32bit, %eax # 32bitラベルを参照
    movw $ref_16bit, %ax    # 16bitラベルを参照
    movb $ref_8bit, %al   # 8bitラベルを参照
    jmp  ref_as_jmp_label # PC 相対アドレスを参照

    movl $ref_32bit + 32, %eax # オフセット付き

    mov  $60, %rax
    syscall
 .text
    .globl ref_32bit
    .globl ref_16bit
    .globl ref_8bit
    .globl ref_as_jmp_label

    nop
ref_32bit:
    nop
ref_16bit:
    nop
ref_8bit:
    nop
ref_as_jmp_label:
    nop

二つの目のファイルであるreloc_label.sに、.textというセクションがあるが、これはプログラムのテキストである命令を表すセクションである。セクションについては、ここによくまとまっっている。

これらのファイルをオブジェクトファイルに変換した後、リロケーション情報について見てみる。

$ readelf -r reloc.o
Relocation section '.rela.text' at offset 0x170 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000001  00050000000a R_X86_64_32       0000000000000000 ref_32bit + 0
000000000007  00060000000c R_X86_64_16       0000000000000000 ref_16bit + 0
00000000000a  00070000000e R_X86_64_8        0000000000000000 ref_8bit + 0
000000000011  00050000000a R_X86_64_32       0000000000000000 ref_32bit + 20
00000000000c  000800000002 R_X86_64_PC32     0000000000000000 ref_as_jmp_label - 4
$ readelf -r reloc_label.o

There are no relocations in this file.

このようにmovなどの命令を吐いているreloc.oの方はリロケーションを行う必要があるので、readelf -rコマンドによりリロケーション情報を出力している。

次回以降リロケーションについて深く調査していく。

参照

リンカ基本事項 シンボルの導入

前回リンクについて軽くまとめたが、リンクする際にはリンカが必要なわけだが、リンカはどのような処理を行っているのかをまとめてみた。

まず大きく分けてリンカが行っていることは以下の二つである。

  • オブジェクトファイルをまとめて一つの実行ファイルにする
  • ラベルの参照を解決してアドレスを決定する

まずオブジェクトファイルには機械語とデータ以外に、シンボル情報とリロケーション情報が含まれている。このシンボルは、アセンブリで言うところのラベルであり、アセンブルする際にシンボルへと変換される。シンボルは定義ずみシンボルと未定義シンボルの二つに分けられる。

  • 定義ずみシンボル:アセンブリでラベルで表されるもので、ファイル内に実体があるシンボルのこと。ファイル内のオフセット、追加情報、文字列表現の情報などを持つ。
  • 未定義シンボル:オブジェクトファイルに実体が存在しないシンボルのことで、追加情報や文字列表現の情報は持つが、ファイル内のオフセットに関する情報は持たない。

以下のアセンブリアセンブルしてオブジェクトファイルを作り、中身をobjdumpで確認してみる。

 .globl    _start
    .globl    sym0
_start:
    nop

sym0:
    nop
sym1:
    nop
    nop
    nop
    nop
sym2:
    nop
$ objdump -d hoge.s

hoge.o: file format mach-o 64-bit x86-64

Disassembly of section __TEXT,__text:

0000000000000000 <_start>:
       0: 90                            nop

0000000000000001 <sym0>:
       1: 90                            nop

0000000000000002 <sym1>:
       2: 90                            nop
       3: 90                            nop
       4: 90                            nop
       5: 90                            nop

0000000000000006 <sym2>:
       6: 90                            nop

シンボル値として_start, sym0, sym1, sym2が現れていることが見て取れる。

このオブジェクトファイルのシンボルの種類などをreadelfコマンドで確認しようとしたが、エラーを吐いてしまった。原因は以下のようにMac上で作成されるオブジェクトファイルはELFではなく、Mach-Oという形式であるからだ。この形式については、第12回 Universal Binary【前編】:Undocumented Mac OS X(3/5 ページ) - ITmedia エンタープライズで確認してもらいたい。

$ file hoge.o
hoge.o: Mach-O 64-bit object x86_64

そこでLinux環境で実行してみる。

$ readelf -s hoge.o
Symbol table '.symtab' contains 8 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    2
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000002     0 NOTYPE  LOCAL  DEFAULT    1 sym1
     5: 0000000000000006     0 NOTYPE  LOCAL  DEFAULT    1 sym2
     6: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT    1 _start
     7: 0000000000000001     0 NOTYPE  GLOBAL DEFAULT    1 sym0

Valueというのがファイル内のオフセット、SizeとTypeカラムはデバッグ情報にも使えるカラムを表す。Bindカラムでシンボルが、グローバルシンボル・ローカルシンボル・エクスポートシンボルのどれであるかを表している。ローカルシンボルとはファイル内でのみ参照できるシンボルのことである。逆にいうとグローバルシンボルは外部ファイルからも参照できるシンボルである。エクスポートシンボルとは共有ライブラリを作る時などに用いられるシンボルで、実行時にもファイルを超えて参照される参照される定義ずみシンボルのことである。VisカラムがDEFAULTだとエクスポートシンボルであるらしい。

とりあえず今回はここまでで次回以降、さらに詳しくまとめていくつもりだ。

参照

  • リンカ: 今回かなり参照したサイトで、より詳しい情報を知りたかったらこちらのサイトを見てみることをおすすめする。
  • リンカーとライブラリ: こちらはリンカについて調べていたらたまたま見つけたサイトだが、リンカにについて非常に詳しくまとめられている。

スタティックリンクとダイナミックリンクによって作成される実行バイナリサイズの違い

前回スタティックリンクとダイナミックリンクの違いを述べ、それぞれの方法でリンクした時のファイルのサイズを見ようとしたが、printfのスタティックライブラリが見つからなかったので、途中で断念してしまった。そこで今回は自分で関数を作ってそれをライブラリにして、スタティック・ダイナミックの二つの方法でリンクしてみようと思う。

注意点

私はMacを使っているのだが、Macgccを使おうとするとデフォルトでclangを使おうとするようだ。具体的に以下のコマンドを実行するとわかる。

$ gcc -v 
Apple clang version 13.1.6 (clang-1316.0.21.2.5)
Target: x86_64-apple-darwin21.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

そこで、brewなどを使ってGNUgccを使えるようにすることを推奨する。なぜなら今回はgccを想定したリンクを行うのだが、gccのオプションはclangでは使えないからだ。私は/usr/local/bin/gcc-12を今回使用した。

準備

以下の2つのC言語ソースコードを準備した。add関数の方をライブラリにするつもりだ。

main.c

#include "Add.c"

int main() {
    int a = 1;
    int b = 2;
    int n = 100;
    int c;
    c = add(a, b, n);
    return c;
}
// Add.c

int add(int a, int b, int n) {
    int c = 0;
    for (int i = 0; i < n; i++) {
        c -= a;
        c += b;
    }
    return c;
}

ダイナミックリンク

まずは、ダイナミックライブラリを以下のコマンドで作る。

gcc-12 -shared -fPIC -o libAdd.so Add.c

ここで、-fPICオプションは、共有ライブラリを作るときに使われるオプションで、メインメモリのどこに配置されても絶対アドレスに関わらず正しく実行されることを保証するものである。Position-Independent Codeの略称である。また共有ライブラリを作るため、オプションとして-sharedも忘れずにつける。

最後に以下のコマンでmain.cのコンパイルと共有ライブラリとのダイナミックリンクを行う。

$ gcc-12 -L. -o main.dynamic main.c -lAdd

スタティックリンク

まず、Add.cのオブジェクトファイルを作る。

$ gcc-12 -c Add.c -o Add.o

これを以下のコマンドで静的ライブラリに変換する。

$ ar rcs libAdd.a Add.o

最後に、このライブラリを用いて、スタティックリンクを行う。

$ gcc-12 -o main.static main.o -L. -lAdd

その後、ダイナミックリンクをした実行バイナリとスタティックリンクをした実行バイナリのファイルサイズを比較した結果が以下だ。

16568 11  1 09:44 main.dynamic
49456 11  1 09:49 main.static

スタティックリンクをした実行バイナリの方が3倍程度も大きくなっていることがわかる。ダイナミックリンクの方がメモリ効率が良いことをこれらの実験で確認することができた。

参照

リンク基本事項

コンパイルした後にオブジェクトファイルをリンクして実行ファイルを作成するという作業を一般にリンクと呼ぶが、そのリンクについての理解が曖昧なので今回まとめてみることにした。

まずリンクの種類として、スタティック(静的)リンクとダイナミック(動的)リンクの二つがある。それぞれについて説明する。

スタティックリンク

オブジェクトファイルをライブラリ中のルーチンとリンクする際、そのルーチンも実行ファイルに含めてしまうことである。このため、ファイルサイズはダイナミックリンク時よりも大きくなる。静的ライブラリは基本的には、.aという拡張子であることが多い。

ダイナミックリンク

リンク時には、オブジェクトファイルが実行時にライブラリ中のルーチンと結合できるように設定ファイルに書き込むだけで、実行ファイルに含めるようなことはしない。そのルーチンを使う時になって初めてそれをメモリ上にロードするので、メモリの節約になる。このため、実行ファイルサイズはスタティックリンク時よりも小さい。動的ライブラリは基本的には、.soという拡張子であることが多い。

ファイルサイズの比較

スタティックリンクの方がファイルサイズが大きくなることがわかっているので、それを確かめてみる。まずは以下のような標準ライブラリを呼び出すような簡単なHello, Worldコードを想定する。

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

まずは、以下のコマンドでコンパイルとリンクしてみる。オプションをつけずにリンクするとダイナミックリンクとなる。

$ cc -o hello.dynamic hello.c

-vオプションをつけて上のコマンドを実行すると、-L/usr/local/libとなっており、ここのライブラリを参照してリンクしていることがわかる。

次にスタティックリンクをしてみる。-staticオプションを付けるとできるらしいので、やってみた。すると以下のようなエラーが出た。

$ cc -static -o hello.static hello.c
ld: library not found for -lcrt0.o
clang: error: linker command failed with exit code 1 (use -v to see invocation)

どうやら-Lというオプションで、printtfのライブラリが入っているパスを指定する必要があるらしい。しかし、どこにprintfのライブラリがあるのか。。。printfのダイナミックリンクライブラリはあるかもしれないが、もしかしたらprintfのスタティックライブラリはない?

以下にリンクを勉強する上で参考になりそうなサイトを載せておく。これらを参考にまたリンクについて勉強してみようと思う。

参照

RISC-V Vector拡張のアセンブリを読み解く 異なる型同士の演算

以前と同様に、RVVのアセンブリを読み解いてみようと思う。今回は型の大きさが異なる同士の演算を見ていこうと思う。

C言語ソースコード

// Vectorization of Mixed Types

#include <stdio.h>

int foo(int *A, char *B, int n) {
    for (int i = 0; i < n; i++) {
        A[i] += 4 * B[i];
    }
    return 0;
}

int main() {
    int n = 2001;
    int A[n]; char B[n];
    for(int i = 0; i < n; i++) {
        A[i] = i;
        B[i] = i % 256;
    }
    foo(A, B, n);
}

RVVアセンブリ

今回も関数のインライン化が行われているので、main関数の中だけを見れば良い。

 .text
    .attribute    4, 16
    .attribute    5, "rv64i2p0_m2p0_a2p0_f2p0_d2p0_c2p0_v1p0_zvl128b1p0_zvl32b1p0_zvl64b1p0"
    .file "test0.c"
    .globl    foo
    .p2align  1
    .type foo,@function
foo:
    blez a2, .LBB0_9
    slli a3, a2, 32
    li   a4, 8
    srli a6, a3, 32
    bgeu a2, a4, .LBB0_3
    li   a2, 0
    j    .LBB0_7
.LBB0_3:
    slli a2, a6, 2
    add  a2, a2, a0
    add  a3, a1, a6
    sltu a3, a0, a3
    sltu a2, a1, a2
    and  a3, a3, a2
    li   a2, 0
    bnez a3, .LBB0_7
    andi a2, a6, -8
    mv   a4, a2
    mv   a5, a0
    mv   a3, a1
.LBB0_5:
    vsetivli zero, 8, e8, mf4, ta, mu
    vle8.v v8, (a3)
    vsetvli  zero, zero, e32, m1, ta, mu
    vle32.v    v9, (a5)
    vzext.vf4  v10, v8
    vsll.vi    v8, v10, 2
    vadd.vv    v8, v8, v9
    vse32.v    v8, (a5)
    addi a3, a3, 8
    addi a4, a4, -8
    addi a5, a5, 32
    bnez a4, .LBB0_5
    beq  a2, a6, .LBB0_9
.LBB0_7:
    slli a3, a2, 2
    add  a0, a0, a3
    add  a1, a1, a2
    sub  a2, a6, a2
.LBB0_8:
    lbu  a3, 0(a1)
    lw   a4, 0(a0)
    slliw    a3, a3, 2
    addw a3, a3, a4
    sw   a3, 0(a0)
    addi a0, a0, 4
    addi a2, a2, -1
    addi a1, a1, 1
    bnez a2, .LBB0_8
.LBB0_9:
    li   a0, 0
    ret
.Lfunc_end0:
    .size foo, .Lfunc_end0-foo

    .globl    main
    .p2align  1
    .type main,@function
main:
    lui  a0, 2
    addiw    a0, a0, 1824
    sub  sp, sp, a0
    li   a0, 0
    addi a1, sp, 2044
    vsetivli zero, 8, e8, mf4, ta, mu
    vid.v  v8
    vsetvli  zero, zero, e32, m1, ta, mu
    vid.v  v9
    addi a2, sp, 11
    li   a3, 16
    li   a4, 2000
.LBB1_1:
    vsetvli  zero, zero, e32, m1, ta, mu
    vadd.vi    v10, v9, 8
    // mf4=64bitにすることでintと同様に
    // 8つのデータを扱えるようになる。
    // 8bitをオーバーした分は自動的に切り捨てられる
    vsetvli  zero, zero, e8, mf4, ta, mu
    vadd.vi    v11, v8, 8
    addi a5, a1, -32
    vse32.v    v9, (a5)
    vse32.v    v10, (a1)
    add  a5, a2, a0
    vse8.v v8, (a5)
    addi a5, a5, 8
    vse8.v v11, (a5)
    addi a0, a0, 16
    vsetvli  zero, zero, e32, m1, ta, mu
    vadd.vx    v9, v9, a3 // a3=16
    vsetvli  zero, zero, e8, mf4, ta, mu
    vadd.vx    v8, v8, a3 // a3=16
    addi a1, a1, 64
    bne  a0, a4, .LBB1_1
    li   a0, 0
    lui  a1, 2
    addiw    a2, a1, -192
    addi a1, sp, 2012
    add  a3, a1, a2
    li   a2, 2000
    sw   a2, 0(a3)
    li   a3, 208
    sb   a3, 2011(sp)
    addi a3, sp, 11
.LBB1_3:
    add  a4, a3, a0
    vsetvli  zero, zero, e8, mf4, ta, mu
    vle8.v v8, (a4)
    vsetvli  zero, zero, e32, m1, ta, mu
    vle32.v    v9, (a1)
    // SEW/4のソースオペランドをゼロ拡張してSEW幅化し書き込む
    vzext.vf4  v10, v8
    vsll.vi    v8, v10, 2
    vadd.vv    v8, v8, v9
    vse32.v    v8, (a1)
    addi a0, a0, 8
    addi a1, a1, 32
    bne  a0, a2, .LBB1_3
    lbu  a0, 2011(sp)
    lui  a1, 2
    addiw    a1, a1, -192
    addi a2, sp, 2012
    add  a1, a1, a2
    lw   a2, 0(a1)
    slliw    a0, a0, 2
    addw a0, a0, a2
    sw   a0, 0(a1)
    li   a0, 0
    lui  a1, 2
    addiw    a1, a1, 1824
    add  sp, sp, a1
    ret
.Lfunc_end1:
    .size main, .Lfunc_end1-main

    .ident    "clang version 14.0.0"
    .section  ".note.GNU-stack","",@progbits
    .addrsig

気づいた点

  • 異なる型同士では一度に処理できるデータ数を揃えるために、レジスタ幅を調整している。具体的に、int型は256bitベクトルレジスタで8つのデータを処理することができるので、それに合わせてchar型も8つ処理するように256bitを4つに分けて64bitベクトルレジスタとして使っていた。
  • 8bit扱っていたため、最大値を超えた(オーバーフロー)した時には、データ幅の制約ゆえに自動的に切り捨てられていたから、i % 256のような計算は特に明示的に行われていなかった。

1週間のまとめ 2022/10/29

1回ブログに書いたことも時間が経つとすぐに忘れてしまうので、内容を振り返って、ついでに軽くまとめることにする。普通にまとめるだけだとなんだか簡単なので、英語で書くのもありだと思ったのだが、とりあえず習慣化することが優先なので日本語でまとめる。

実際まとめてみると、そもそ技術記事は要約することは難しいことがわかったので、主に振り返ることで気づいた感想などが中心となっている。詳しい内容は実際に記事を参照されたい。

  • RVV Intrinsicsの一部紹介 その1

takumi9.hatenablog.com

  • TypeScript 基本型

takumi9.hatenablog.com

takumi9.hatenablog.com

アセンブリを眺めていて、わざわざelfファイルをobjdumpする必要はないと感じた。clang -Sでアセンブリを出力して読めばいいかと思った。 takumi9.hatenablog.com

takumi9.hatenablog.com

自分のqsortとライブラリのqsortで性能計測する時、ソート対象の配列を初期化しているが、そこでそれぞれ別の配列を扱っているので、条件が統一されていないと思った。

takumi9.hatenablog.com

  • fopen_s/fprintf_sなどのセキュアCライブラリ

takumi9.hatenablog.com

takumi9.hatenablog.com

現在気になっているテーマ

これからもしかしたらまとめるかもしれないテーマを書き出しておく。

RISC-V Vector拡張のアセンブリを読み解く

RISC-V Vector拡張(RVV)のアセンブリを読むのにまだ慣れないので、実際に出力されたコードを読んで読み解いてみようと思う。

C言語ソースコード

void loops(int n, int A[], int B[]) {
    for (int i = 0; i < n; i++) {
        A[i] *= B[i];
    }
}

int main() {
    int n = 1000;
    int A[n], B[n];
    for(int i = 0; i < n; i++) {
        A[i] = i;
        B[i] = i;
    }
    loops(n, A, B);
    return 0;
}

出力されたRVV アセンブリ

    .text
    .attribute    4, 16
    .attribute    5, "rv64i2p0_m2p0_a2p0_f2p0_d2p0_c2p0_v1p0_zvl128b1p0_zvl32b1p0_zvl64b1p0" 
    .file    "test0.c" 
    .globl    loops
    .p2align    1
    .type    loops,@function
loops:
    blez    a0, .LBB0_9
    slli    a3, a0, 32
    li    a4, 16
    srli    a6, a3, 32
    bgeu    a0, a4, .LBB0_3
    li    a7, 0
    j    .LBB0_7
.LBB0_3:
    slli    a0, a6, 2
    add    a3, a1, a0
    add    a0, a0, a2
    sltu    a0, a1, a0
    sltu    a3, a2, a3
    and    a0, a0, a3
    li    a7, 0
    bnez    a0, .LBB0_7
    andi    a7, a6, -16
    addi    a4, a2, 32
    addi    a5, a1, 32
    mv    a0, a7
.LBB0_5:
    addi    a3, a4, -32
    vsetivli    zero, 8, e32, m1, ta, mu
    vle32.v    v8, (a3)
    vle32.v    v9, (a4)
    addi    a3, a5, -32
    vle32.v    v10, (a3)
    vle32.v    v11, (a5)
    vmul.vv    v8, v10, v8
    vmul.vv    v9, v11, v9
    vse32.v    v8, (a3)
    vse32.v    v9, (a5)
    addi    a4, a4, 64
    addi    a0, a0, -16
    addi    a5, a5, 64
    bnez    a0, .LBB0_5
    beq    a7, a6, .LBB0_9
.LBB0_7:
    slli    a3, a7, 2
    add    a0, a1, a3
    add    a1, a2, a3
    sub    a2, a6, a7
.LBB0_8:
    lw    a3, 0(a1)
    lw    a4, 0(a0)
    mulw    a3, a4, a3
    sw    a3, 0(a0)
    addi    a0, a0, 4
    addi    a2, a2, -1
    addi    a1, a1, 4
    bnez    a2, .LBB0_8
.LBB0_9:
    ret
.Lfunc_end0:
    .size    loops, .Lfunc_end0-loops

    .globl    main
    .p2align    1
    .type    main,@function
main:
// 2を12bit左シフト=4096 x 2
// 4バイト要素を1000個持つ配列が2つなのでちょうどこれくらい
    lui    a0, 2
    addiw    a0, a0, -176
// 配列の分だけスタック領域を確保
    sub    sp, sp, a0
    lui    a0, 1
    addiw    a1, a0, -128
    lui    a0, 1
    addiw    a0, a0, -80
// 以下二つの命令でa0にスタック内の配列の開始位置を割り当てる
    add    a0, a0, sp
    add    a0, a0, a1

    addi    a2, sp, 16
    add    a6, a2, a1
    lui    a1, 1
    addiw    a1, a1, -48
// もう一つの配列の開始位置をスタック内に割り当てる
    add    a2, sp, a1
    addi    a3, sp, 48
// 最初のオペランドはzeroだが、本来ここは一回のループで処理される要素数を格納する。
// つまりvlを格納し、いつもはAVLだが最後は余りの数が格納される。
// 二つ目のオペランドは8となっており処理する全ての演算対象要素数を表す。
    vsetivli    zero, 8, e32, m1, ta, mu
// 各要素のインデックスを0, 1, 2, ..., vl-1とv8に書き込む。
    vid.v    v8
    li    a4, 992
    li    a5, 16
.LBB1_1:
// 0, 1, 2, ... vl-1(7)となっているインデックス全てに8を加算
    vadd.vi    v9, v8, 8
    addi    a1, a2, -32
// 配列Aに0, 1, 2, ... 15を書き込んでSTORE
    vse32.v    v8, (a1)
    vse32.v    v9, (a2)
// 配列Bも同じだけSTORE
    addi    a1, a3, -32
    vse32.v    v8, (a1)
    vse32.v    v9, (a3)
// インデックスが入っているv8に16を足して更新
    vadd.vx    v8, v8, a5
// 配列Aのスタック領域を更新
    addi    a2, a2, 64
// 処理すべき配列の要素数の更新。bnezで使う。
    addi    a4, a4, -16
// 配列Bのスタック領域を更新
    addi    a3, a3, 64
    bnez    a4, .LBB1_1
// 以下あまり部分。1000%16 = 8回だけ行う。
    li    a2, 992
    sw    a2, 0(a0)
    sw    a2, 0(a6)
    li    a1, 993
    sw    a1, 4(a0)
    sw    a1, 4(a6)
    li    a1, 994
    sw    a1, 8(a0)
    sw    a1, 8(a6)
    li    a1, 995
    sw    a1, 12(a0)
    sw    a1, 12(a6)
    li    a1, 996
    sw    a1, 16(a0)
    sw    a1, 16(a6)
    li    a1, 997
    sw    a1, 20(a0)
    sw    a1, 20(a6)
    li    a1, 998
    sw    a1, 24(a0)
    sw    a1, 24(a6)
    li    a1, 999
    sw    a1, 28(a0)
    sw    a1, 28(a6)
// 関数のインライン化が行われていてここからloop関数。
    addi    a3, sp, 48
    lui    a1, 1
    addiw    a1, a1, -48
    add    a4, sp, a1
.LBB1_3:
// 配列AのLOAD
    addi    a1, a3, -32
    vle32.v    v8, (a1)
    vle32.v    v9, (a3)
// 配列BのLOAD
    addi    a1, a4, -32
    vle32.v    v10, (a1)
    vle32.v    v11, (a4)
    vmul.vv    v8, v10, v8
    vmul.vv    v9, v11, v9
    vse32.v    v8, (a1)
    vse32.v    v9, (a4)
    addi    a3, a3, 64
    addi    a2, a2, -16 // 最初はa2 = 992
    addi    a4, a4, 64
    bnez    a2, .LBB1_3
// あまりの乗算
    lw    a1, 0(a6)
    lw    a2, 0(a0)
    lw    a3, 4(a6)
    lw    a4, 4(a0)
    mulw    a1, a2, a1
    sw    a1, 0(a0)
    mulw    a1, a4, a3
    lw    a2, 8(a6)
    lw    a3, 8(a0)
    lw    a4, 12(a6)
    lw    a5, 12(a0)
    sw    a1, 4(a0)
    mulw    a1, a3, a2
    sw    a1, 8(a0)
    mulw    a1, a5, a4
    lw    a2, 16(a6)
    lw    a3, 16(a0)
    lw    a4, 20(a6)
    lw    a5, 20(a0)
    sw    a1, 12(a0)
    mulw    a1, a3, a2
    sw    a1, 16(a0)
    mulw    a1, a5, a4
    lw    a2, 24(a6)
    lw    a3, 24(a0)
    lw    a4, 28(a6)
    lw    a5, 28(a0)
    sw    a1, 20(a0)
    mulw    a1, a3, a2
    sw    a1, 24(a0)
    mulw    a1, a5, a4
    sw    a1, 28(a0)
    li    a0, 0
    lui    a1, 2
    addiw    a1, a1, -176
    add    sp, sp, a1
    ret
.Lfunc_end1:
    .size    main, .Lfunc_end1-main

    .ident    "clang version 14.0.0" 
    .section    ".note.GNU-stack","",@progbits
    .addrsig

浮かんできた疑問

  • 今回行っているvsetvli命令は必要?
  • 複数のベクトルレジスタを用いたいけどどうすればいいのか?コンパイルオプションで指定するの?
  • ストリップマイニングとは →大きなループをより小さなセグメントやストリップに分割すること。つまり大きなループ内に並列性を見つけたらそこをひとまとまりにしてベクトル命令で並列計算するというのも含まれる。参照