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

家庭教師としてオンラインでプログラミングを教えているのだが、その際fopen_sfprintf_sという関数が出てきて、最初はfopenfprintfの新しいバージョン、もしくは古いバージョンの関数かなくらいに思っていたのだが、どうもMacgccでもclangでも「定義されていない」とエラーを吐き、コンパイラのバージョンは最新でかつ普通後方互換性も担保されているはずなのでおかしいと思って色々調べてみた。するとどうもWindowsでのみ動く関数であることがわかった。意外とそれに関する情報があまりまとめられていなさそうだったので、今回まとめて見ることにした。

基本的にfopen_sやfprintf_sの方がセキュリティに優れているらしい。具体的には、このサイトに非推奨や時代遅れの関数がまとまっている。私たちが普段使っている多くの標準ライブラリ関数のセキュリティレベルが低いとされている。

具体的に説明する。_sがついたCRT(Cランタイム)関数はセキュリティがより強化されたバージョンである。このような関数はセキュリティエラーが防止されたり修正されたりするわけではなく、発生したエラーのキャッチ、それによるエラー状態の追加チェックが行われて、エラーハンドラを呼び出す。

以下のようなセキュリティ機能が付与されている。参照

  • パラメータの検証
  • バッファーの範囲を超えた書き込みが行われないようにバッファーサイズを指定して関数に渡す
  • 文字列を確実にNULLで終了させる
  • より詳細なエラー情報
  • ファイルシステムのセキュリティ
  • Windowsのセキュリティ
  • 書式指定文字列の構文チェック。printfで不適切なフィールド文字を使用しているかどうかなど。

話は少し変わるが、例えばC++でこのようなセキュアなCRT関数を使用したい場合は、_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMESを1として定義すると、例えばstrcpyの呼び出しがバッファーオーバーランを防ぐstrcpy_sの呼び出しに変更される。つまりセキュリティで保護されたテンプレートでオーバーロードすることが可能となる。

fopen_s

プロトタイプ

errno_t fopen_s(FILE * restrict * restrict streamptr, const char * restrict filename, const char * restrict mode);

返り値

正常に終了すると0, そうでないと0でない値を返す。

使い方

/* Program to create backup of a file */

#include <stdio.h>

int main(void)
{
  FILE *in, *out;
  in = new FILE;
  if (fopen_s(&in, "TESTFILE.DAT", "rt")){
    fprintf(stderr, "Cannot open input file.\n");
    return 1;
  }
  if (fopen_s(&out, "TESTFILE.BAK", "wt")){
    fprintf(stderr, "Cannot open output file.\n");
    return 1;
  }
  while (!feof(in)){
    fputc(fgetc(in), out);
  }
  fclose(in);
  fclose(out);
  return 0;
}

fprintf_s

fprintfより優れている点

プロトタイプ

int fprintf_s(FILE * restrict stream, const char * restrict format, [,argument, ...]);

int fwprintf_s(FILE * restrict stream, const wchar_t * restrict format, [,argument, ...]);

返り値

書き込まれた文字数を返す。エラーが発生した場合は負の値を返す。

使い方

#include <stdio.h>
int main(void)
{
  FILE *stream;
  int i = 100;
  char c = 'C';
  float f = 1.234;
  /* Open a file for update */
  if(fopen_s(&stream,"DUMMY.FIL", "w+")){
    printf("Unable to create DUMMY.FIL");
  }
  else{
    /* Write some data to the file */
    fprintf_s(stream, "%d %c %f", i, c, f);
  }
  /* Close the file */
  fclose(stream);
  return 0;
}

参考

クイックソート メモ

いつもクイックソートの書き方がわからなくなる(セグフォとかで正確に実行できない)ことが多いので、メモとしてqsortのC言語によるアルゴリズムを書いておく。今回はCの標準ライブラリであるqsort関数とどちらが実行速度が大きいかを比較した。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void myqsort(int left, int right, int A[]) {
    if (left >= right) return;
    int i = left, j = right; // i = 0, j = n-1
    // オーバーフロー対策でこのようにmiddleを求める
    int pivot = (j-i)/2+i;
    int val = A[pivot];
    while(1) {
        // iとjが交わる時は1以下で収まる.交わったらすぐにwhileの条件を満たさなくなるから.
        while(A[i] < val) i++;
        while(A[j] > val) j--;
        if (i >= j) break; 
        int tmp = A[i]; A[i] = A[j]; A[j] = tmp;
        i++; j--;
    }
    // A[i] はvalより大きいが,A[k]は任意のk (<= i-1)でvalより小さい
    myqsort(left, i-1, A);
    // A[i] はvalより小さいが,A[k]は任意のk (>= j+1)でvalより大きい
    myqsort(j+1, right, A);
}

// ライブラリのqsort関数で用いる比較関数
int cmpnum(const void *n1, const void *n2) {
    if (*(int*)n1 > *(int*)n2) {
        return 1;
    } else if (*(int*)n1 < *(int*)n2) {
        return -1;
    } else {
        return 0;
    }
}

int main() {
    int n = 10000;
    int B[n];
    for (int i = 0; i < n; i++) {
        B[i] = rand() % 100000;
    }
    clock_t start = clock();
    // 自作qsort関数
    myqsort(0, n-1, B);
    clock_t end = clock();
    printf("time = %f ", (double)(end - start)/CLOCKS_PER_SEC);

    for (int i = 0; i < n; i++) {
        B[i] = rand() % 100000;
    }
    size_t int_size = sizeof(int);
    clock_t start1 = clock();
    // cの標準ライブラリを使用
    qsort(B, n, int_size, cmpnum);
    clock_t end1 = clock();
    printf("time = %f ", (double)(end1 - start1)/CLOCKS_PER_SEC);
}

実行結果(単位 s)

myqsort qsort
0.079998 0.160001

意外と自分で作ったqsortの方が早かった。

RISC-V アセンブリを読む その2 関数呼び出し

今回は引数や返り値がわかりやすい関数呼び出しが行われているアセンブリを精査していく。

関数呼び出し

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int c = 1;
    int d = 2;
    int e  = add(c, d);
    printf("%d", e);
    return 0;
}
000000008000226c <add>:
    8000226c:   1101                   addi sp,sp,-32
    8000226e:   ec22                 sd   s0,24(sp)
    80002270:  1000                   addi s0,sp,32
// 引数の値を一時レジスタに格納
    80002272:  87aa                    mv   a5,a0
    80002274:  872e                    mv   a4,a1
// とりあえずメモリに格納
    80002276:  fef42623             sw   a5,-20(s0)
    8000227a:   87ba                    mv   a5,a4
    8000227c:   fef42423             sw   a5,-24(s0)
// 結局以下のように使うからメモリに格納する必要はないように思う
    80002280:  fec42783             lw   a5,-20(s0)
    80002284:  873e                    mv   a4,a5
    80002286:  fe842783             lw   a5,-24(s0)
// (a + b)の計算
    8000228a:   9fb9                    addw a5,a5,a4
// signed extension 符号拡張
// 64bitレジスタを考えているから32bit整数を64bitに変更
    8000228c:   2781                   sext.w a5,a5
// 返り値をa0に格納
    8000228e:   853e                    mv   a0,a5
    80002290:  6462                   ld   s0,24(sp)
    80002292:  6105                   addi sp,sp,32
    80002294:  8082                   ret

0000000080002296 <main>:
    80002296:  1101                   addi sp,sp,-32
    80002298:  ec06                 sd   ra,24(sp)
// フレームポインタの退避
    8000229a:   e822                 sd   s0,16(sp)
// 新しいフレームポインタの更新
    8000229c:   1000                   addi s0,sp,32
    8000229e:   4785                   li   a5,1
    800022a0:   fef42623             sw   a5,-20(s0)
    800022a4:   4789                   li   a5,2
    800022a6:   fef42423             sw   a5,-24(s0)
    800022aa:   fe842703             lw   a4,-24(s0)
    800022ae:   fec42783             lw   a5,-20(s0)
// 以下二つは関数のための引数を用意している
// a0が第一引数、a1が第二引数
    800022b2:   85ba                    mv   a1,a4
    800022b4:   853e                    mv   a0,a5
    800022b6:   fb7ff0ef             jal  ra,8000226c <add>
// 返り値a0をa5に退避
    800022ba:   87aa                    mv   a5,a0
// 以下の二つの命令は意味がない。無駄なスピル。
    800022bc:   fef42223             sw   a5,-28(s0)
    800022c0:   fe442783             lw   a5,-28(s0)
    800022c4:   85be                    mv   a1,a5
    800022c6:   00000517           auipc    a0,0x0
    800022ca:   1ba50513            addi a0,a0,442 # 80002480 <main+0x1ea>
    800022ce:   a99ff0ef             jal  ra,80001d66 <printf>
// 以下二つの命令はreturn 0を作るため
    800022d2:   4781                   li   a5,0
    800022d4:   853e                    mv   a0,a5
// 退避させていたraの復帰
    800022d6:   60e2                    ld   ra,24(sp)
// 退避させていたフレームポインタの復帰
    800022d8:   6442                   ld   s0,16(sp)
// スタックポインタの値がもとに戻る
    800022da:   6105                   addi sp,sp,32
    800022dc:   8082                   ret

最適化された関数呼び出し

0000000080002378 <add>:
    80002378:  9d2d                    addw a0,a0,a1
    8000237a:   8082                   ret

Disassembly of section .text.startup:

000000008000237c <main-0x18>:
    8000237c:   1141                   addi sp,sp,-16
    8000237e:   00000517           auipc    a0,0x0
    80002382:  05250513           addi a0,a0,82 # 800023d0 <main+0x3c>
    80002386:  e406                 sd   ra,8(sp)
    80002388:  8a3ff0ef            jal  ra,80001c2a <printstr>
    8000238c:   60a2                    ld   ra,8(sp)
    8000238e:   557d                    li   a0,-1
    80002390:  0141                   addi sp,sp,16
    80002392:  8082                   ret

0000000080002394 <main>:
    80002394:  1141                   addi sp,sp,-16
    80002396:  458d                    li   a1,3
    80002398:  00000517           auipc    a0,0x0
    8000239c:   06050513           addi a0,a0,96 # 800023f8 <main+0x64>
    800023a0:   e406                 sd   ra,8(sp)
    800023a2:   c91ff0ef             jal  ra,80002032 <printf>
    800023a6:   60a2                    ld   ra,8(sp)
    800023a8:   4501                   li   a0,0
    800023aa:   0141                   addi sp,sp,16
    800023ac:   8082                   ret

非常に最適化されたいるのはいいのだが、<main-0x18>というラベルが出現している。これはなんなのか?

関数呼び出し(引数が9個以上)

引数が8個以下の時は、a0~a7のレジスタに引数として格納して関数を呼び出す。しかし引数が9個以上となると一時レジスタが足りなくなるので、代わりにスタックポインタを使うようになる。以下が引数が10個の場合。

#include <stdio.h>

int add(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) {
    return a + b + c + d + e + f + g + h + i + j;
}

int main() {
    int a = 1;int b = 2;int c = 3;int d = 4;
    int e = 5;int f = 6;int g = 7;int h = 8;
    int i = 9;int j =10;
    int fuga  = add(a, b, c, d, e, f, g, h, i, j);
    printf("%d", fuga);
    return 0;
}
000000008000226c <add>:
    8000226c:   7179                   addi sp,sp,-48
    8000226e:   f422                 sd   s0,40(sp)
    80002270:  1800                   addi s0,sp,48
    80002272:  8eaa                    mv   t4,a0
    80002274:  8e2e                    mv   t3,a1
    80002276:  8332                   mv   t1,a2
    80002278:  8536                   mv   a0,a3
    8000227a:   85ba                    mv   a1,a4
    8000227c:   863e                    mv   a2,a5
    8000227e:   86c2                    mv   a3,a6
    80002280:  8746                   mv   a4,a7
    80002282:  87f6                    mv   a5,t4
    80002284:  fef42623             sw   a5,-20(s0)
    80002288:  87f2                    mv   a5,t3
    8000228a:   fef42423             sw   a5,-24(s0)
    8000228e:   879a                    mv   a5,t1
    80002290:  fef42223             sw   a5,-28(s0)
    80002294:  87aa                    mv   a5,a0
    80002296:  fef42023             sw   a5,-32(s0)
    8000229a:   87ae                    mv   a5,a1
    8000229c:   fcf42e23             sw   a5,-36(s0)
    800022a0:   87b2                    mv   a5,a2
    800022a2:   fcf42c23             sw   a5,-40(s0)
    800022a6:   87b6                    mv   a5,a3
    800022a8:   fcf42a23             sw   a5,-44(s0)
    800022ac:   87ba                    mv   a5,a4
    800022ae:   fcf42823             sw   a5,-48(s0)
    800022b2:   fec42783             lw   a5,-20(s0)
    800022b6:   873e                    mv   a4,a5
    800022b8:   fe842783             lw   a5,-24(s0)
    800022bc:   9fb9                    addw a5,a5,a4
    800022be:   2781                   sext.w a5,a5
    800022c0:   fe442703             lw   a4,-28(s0)
    800022c4:   9fb9                    addw a5,a5,a4
    800022c6:   2781                   sext.w a5,a5
    800022c8:   fe042703             lw   a4,-32(s0)
    800022cc:   9fb9                    addw a5,a5,a4
    800022ce:   2781                   sext.w a5,a5
    800022d0:   fdc42703             lw   a4,-36(s0)
    800022d4:   9fb9                    addw a5,a5,a4
    800022d6:   2781                   sext.w a5,a5
    800022d8:   fd842703             lw   a4,-40(s0)
    800022dc:   9fb9                    addw a5,a5,a4
    800022de:   2781                   sext.w a5,a5
    800022e0:   fd442703             lw   a4,-44(s0)
    800022e4:   9fb9                    addw a5,a5,a4
    800022e6:   2781                   sext.w a5,a5
    800022e8:   4018                   lw   a4,0(s0)
    800022ea:   9fb9                    addw a5,a5,a4
    800022ec:   2781                   sext.w a5,a5
    800022ee:   4418                   lw   a4,8(s0)
    800022f0:   9fb9                    addw a5,a5,a4
    800022f2:   2781                   sext.w a5,a5
    800022f4:   853e                    mv   a0,a5
    800022f6:   7422                   ld   s0,40(sp)
    800022f8:   6145                   addi sp,sp,48
    800022fa:   8082                   ret

00000000800022fc <main>:
// 以下四つの命令は基本的に毎回行われる
    800022fc:   715d                    addi sp,sp,-80
    800022fe:   e486                 sd   ra,72(sp)
    80002300:  e0a2                 sd   s0,64(sp)
    80002302:  0880                   addi s0,sp,80

    80002304:  4785                   li   a5,1
    80002306:  fef42623             sw   a5,-20(s0)
    8000230a:   4789                   li   a5,2
    8000230c:   fef42423             sw   a5,-24(s0)
    80002310:  478d                    li   a5,3
    80002312:  fef42223             sw   a5,-28(s0)
    80002316:  4791                   li   a5,4
    80002318:  fef42023             sw   a5,-32(s0)
    8000231c:   4795                   li   a5,5
    8000231e:   fcf42e23             sw   a5,-36(s0)
    80002322:  4799                   li   a5,6
    80002324:  fcf42c23             sw   a5,-40(s0)
    80002328:  479d                    li   a5,7
    8000232a:   fcf42a23             sw   a5,-44(s0)
    8000232e:   47a1                    li   a5,8
    80002330:  fcf42823             sw   a5,-48(s0)
    80002334:  47a5                    li   a5,9
    80002336:  fcf42623             sw   a5,-52(s0)
    8000233a:   47a9                    li   a5,10
    8000233c:   fcf42423             sw   a5,-56(s0)
    80002340:  fd042883             lw   a7,-48(s0)
    80002344:  fd442803             lw   a6,-44(s0)
    80002348:  fd842303             lw   t1,-40(s0)
    8000234c:   fdc42703             lw   a4,-36(s0)
    80002350:  fe042683             lw   a3,-32(s0)
    80002354:  fe442603             lw   a2,-28(s0)
    80002358:  fe842583             lw   a1,-24(s0)
    8000235c:   fec42503             lw   a0,-20(s0)
// ここで一時レジスタa5を使って引数をメモリ上のスタックに詰めていることがわかる
    80002360:  fc842783             lw   a5,-56(s0)
    80002364:  e43e                 sd   a5,8(sp)
    80002366:  fcc42783             lw   a5,-52(s0)
    8000236a:   e03e                 sd   a5,0(sp)
// 上の方でt1に入れていたのでa5に入れ直す
    8000236c:   879a                    mv   a5,t1
    8000236e:   effff0ef             jal  ra,8000226c <add>
    80002372:  87aa                    mv   a5,a0
    80002374:  fcf42223             sw   a5,-60(s0)
    80002378:  fc442783             lw   a5,-60(s0)
    8000237c:   85be                    mv   a1,a5
    8000237e:   00000517           auipc    a0,0x0
    80002382:  1ba50513            addi a0,a0,442 # 80002538 <main+0x23c>
    80002386:  9e1ff0ef            jal  ra,80001d66 <printf>
    8000238a:   4781                   li   a5,0
    8000238c:   853e                    mv   a0,a5
    8000238e:   60a6                    ld   ra,72(sp)
    80002390:  6406                   ld   s0,64(sp)
    80002392:  6161                   addi sp,sp,80
    80002394:  8082                   ret

これは最適化しなかったアセンブリなので、非常に無駄な命令が多くなってしまった。

次回は分岐命令。

RISC-V アセンブリを読む その1 Hello World

前回の「RISC-V アセンブリを読む その0 - takumi9のブログ」でアセンブリを読むための環境を作ったので、これから実際に様々なRISC-Vアセンブリを読んでいこうと思う。前回の記事の通り、gccを用いてコンパイルした。

Hello, World

まずは一番基本的なコード。

#include <stdio.h>

int main() {
    printf("Hello, World");
    return 0;
}
000000008000226c <main>:
// まずはspを-16して必要な分のフレームサイズを確保
    8000226c:   1141                   addi sp,sp,-16
// 返り値を退避させる
    8000226e:   e406                 sd   ra,8(sp)
// 呼び出し側のフレームポインタを退避
// 呼び出し側に戻った時にフレームポインタを復帰できるように
    80002270:  e022                 sd   s0,0(sp)
// 呼び出され側のフレームポインタをs0に代入
// このフレームポインタがちょうど呼び出し側と呼び出され側の境に位置している
    80002272:  0800                   addi s0,sp,16
// pc相対で即値を作るものの0。mov a0 zeroとかで良くない?
// a0 = 80002274
    80002274:  00000517           auipc    a0,0x0
// a0 = 80002274+444(=1bc) = 80002430。次に呼ぶprintf関数の第一引数。
    80002278:  1bc50513            addi a0,a0,444 # 80002430 <main+0x1c4>
// 次のpcをraレジスタに退避させて戻って来れるようにする
// さらにpcを即値の80001d66に設定してprintfに飛ぶ。
    8000227c:   aebff0ef             jal  ra,80001d66 <printf>
// この後の二つの命令はreturn 0のための準備
    80002280:  4781                   li   a5,0
    80002282:  853e                    mv   a0,a5
// 前に退避させておいたmain関数の返り値を再びロードする。
    80002284:  60a2                    ld   ra,8(sp)
// 前に退避させておいたフレームポインタも再びロードする。
    80002286:  6402                   ld   s0,0(sp)
// spをもとに戻す。
    80002288:  0141                   addi sp,sp,16
    8000228a:   8082                   ret
  • スタックポインタは現在実行中の関数の低位アドレス側を指し、フレームポインタは高位アドレス側(呼び出し側に近い方)を指す。

今回アセンブリを眺めていて、無駄なアセンブリが多いと感じた。これは最適化オプションをつけずに以下のコマンドでコンパイルしていたからだと気づいた。ただ、アセンブリを色々調べるにあたっては冗長でむしろわかりやすくてよかった。次回からも最適化オプションをつけないものと最適化オプション-O3をつけたものの両方を見てみようと思う。

$ riscv64-unknown-elf-gcc -static -mcmodel=medany -fno-common -fno-builtin-printf -nostdlib -nostartfiles -lm -lgcc -T link.ld syscall.c crt.S test0.c -o test0

最適化したHello, World

0000000080002390 <main>:
    80002390:  1141                   addi sp,sp,-16
    80002392:  00000517           auipc    a0,0x0
    80002396:  05e50513            addi a0,a0,94 # 800023f0 <main+0x60>
    8000239a:   e406                 sd   ra,8(sp)
    8000239c:   c97ff0ef             jal  ra,80002032 <printf>
    800023a0:   60a2                    ld   ra,8(sp)
    800023a2:   4501                   li   a0,0
    800023a4:   0141                   addi sp,sp,16
    800023a6:   8082                   ret

非常に短く無駄なアセンブリはないスッキリしたものになった。

次回以降も別のRISC-Vアセンブリを精査していく。

参考サイト

RISC-V アセンブリを読む その0

RISC-Vのアセンブリの個別の命令に関してはある程度理解しているのですが、高級言語で書かれたものがどのようなアセンブリに変換されるのかの理解が乏しいので、今回はC言語RISC-V向けにコンパイルして出力されたアセンブリを眺めてみようと思う。

以下が使用したmakefile

clang := clang-14
opt := opt
objdump := riscv64-unknown-elf-objdump

# include library file targeting riscv64
IFILE := ~/riscv64_github/riscv64-unknown-elf/include

SRCS := test0.c
OBJS := $(SRCS:.c=.o)
ASMS := $(SRCS:.c=.S)

all: $(ASMS)

$(OBJS): $(SRCS)
  $(clang) -c $< -o $@ -target riscv64 -O3 -I $(IFILE)

$(ASMS): $(OBJS)
  $(objdump) -d $< > $@

clean: 
  rm -f $(OBJS) $(ASMS)

また以下がclang-14のバージョン。

$ clang-14 --version
Ubuntu clang version 14.0.0-1ubuntu1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

以下で、様々なC言語ソースコードから出力されるRISC-Vアセンブリを眺めていこうと思う。

下準備

まずは以下のようなHelloWorldソースコードの出力結果を調べることにする。

#include <stdio.h>

int main() {
    printf("Hello, World");
    return 0;
}
test0.o:     file format elf64-littleriscv


Disassembly of section .text:

0000000000000000 <main>:
   0: 1141          addi sp,sp,-16
   2: e406          sd ra,8(sp)
   4: 00000537      lui a0,0x0
   8: 00050513      mv a0,a0
   c: 00000097      auipc ra,0x0
  10: 000080e7      jalr    ra # c <main+0xc>
  14: 4501          li a0,0
  16: 60a2          ld  ra,8(sp)
  18: 0141          addi sp,sp,16
  1a: 8082          ret

lui命令などで0x0と意味のない即値をしているように見えるのは、test0.oがまだオブジェクトファイルであり、リンク前であるのでどのアドレスに割り当てられるかがわかっていないからである。リロケーション情報を付与して$(obdjump) -d -r $< > $@というコマンドを実行すると以下のようにリロケーション情報も付与されたファイルが出力される。このリロケーション情報をもとにリンカはリンクを実行する。

0000000000000000 <main>:
   0: 1141          addi sp,sp,-16
   2: e406          sd ra,8(sp)
   4: 00000537      lui a0,0x0
            4: R_RISCV_HI20 .L.str
   8: 00050513      mv a0,a0
            8: R_RISCV_LO12_I   .L.str
   c: 00000097      auipc ra,0x0
            c: R_RISCV_CALL   printf
  10: 000080e7      jalr    ra # c <main+0xc>
  14: 4501          li a0,0
  16: 60a2          ld  ra,8(sp)
  18: 0141          addi sp,sp,16
  1a: 8082          ret

具体的にどのアドレスに飛んだかもわかった状態でアセンブリを眺めたいと思い、elfという実行ファイルを作ってそれからobjdumpで出力されるアセンブリを眺めることにする。 作って学ぶコンピュータアーキテクチャという本を参考に以下のようにリンクさせた。

$ riscv64-unknown-elf-gcc -static -mcmodel=medany -fno-common -fno-builtin-printf -nostdlib -nostartfiles -lm -lgcc -T link.ld syscall.c crt.S test0.c -o test0

ここで、syscall.c crt.S link.ldなどはriscv-toolsというリポジトリから持ってきた。これをobjdumpしたら以下のような出力が得られた。

000000008000226c <main>:
    8000226c: 1141          addi    sp,sp,-16
    8000226e: e406          sd    ra,8(sp)
    80002270: e022          sd   s0,0(sp)
    80002272: 0800          addi   s0,sp,16
    80002274: 00000517      auipc  a0,0x0
    80002278: 1bc50513      addi    a0,a0,444 # 80002430 <main+0x1c4>
    8000227c: aebff0ef      jal   ra,80001d66 <printf>
    80002280: 4781          li a5,0
    80002282: 853e          mv  a0,a5
    80002284: 60a2          ld  ra,8(sp)
    80002286: 6402          ld s0,0(sp)
    80002288: 0141          addi   sp,sp,16
    8000228a: 8082          ret

上でclangをコンパイラとして用いようとしたのに、結局gccを使ってコンパイルすることになってしまった。ただ、そこまでclangとgccで大きな違いはないはずなので、gccコンパイルしていこうと思う。

これを使って次回以降RISC-Vアセンブリを詳しく読んでいこうと思う。

TypeScript 基本型

処理の流れ

以下が大まかな処理の流れ。まず1~3はTSにおける処理。4~6はJSにおける処理。

  1. TypeScriptソース→TypeScript AST

  2. ASTが型チェッカーによってチェックされる

  3. TypeScript AST→JavaScriptソース

  4. JavaScriptソース→JavaScript AST

  5. AST→バイトコード

  6. バイトコードがランタイムによって評価される。

ラインタイムに関する理解が曖昧なので、また別の機会でまとめてみようと思う。

型の種類

型の基本的な型を軽く紹介する。

any

どんな型でも取るもの。プログラマが型がわからない時に使う最後の手段の型。できるだけ避けたい。 TSCはデフォルトだとanyに対してエラーを出さないけど、tsconfig.jsonの中でnoImplicitAnyフラグを有効にすることでエラーを出すようになる。TSCフラグのstrictを有効にしている場合も、エラーを出す。

unknown

anyと同様に任意の型を示すが、それが何の型かがわかるまで値の使用を許可しない。以下が具体例。

let a: unknown = 30;        // unknow
let b = a == 50;            // boolean
let c = a + 10;           // エラー TS2571:オブジェクトの型は'unknown'です

boolean

普通はbooleanはTSC型推論させることが多い。また以下のようなリテラル型で型アノテーションすることも可能。

let e: true = true  // true
let f: false = true  // エラー

number

整数、浮動小数点数、正数、負数、Infinity, NaNなど全ての数値を表す。当然この型もリテラル型として宣言することもできる。

bigint

numberの表せる整数は253までだが、bigintはそれよりも大きな整数も表すことができる。まだ全てのJavaScriptエンジンではサポートされていないよう。

string

連結、スライスなど様々なことができる。

symbol

オブジェクトやマップにおいて文字キーの代わりとして既知のキーが適切に使われ、確実に誤った値が設定されたくない場合に用いられる。シンボルは固有であることが保証されている。以下の例を参照。

let Sym1 = Symbol("Sym")
let Sym2 = Symbol("Sym")

console.log(Sym1 === Sym2) // false

object

オブジェクトの形状を指定する。基本的にはobjectと明示的に型付けしない。

let a: {
    b: number
    c?: string
    [key: number]: boolean // [key: T]はインデックスシグネチャ。Tはnumberかstring
}

a = {b: 1, 10: true, 20: false}

終わりに

まずは基本的な型について紹介した。型エイリアス、配列などより応用的な型システムについてはまた紹介します。

参照

O'REILY プログラミング TypeScript

RVV Intrinsicsの一部紹介 その1

RISC-V "V" Vector Extension Intrinsicsについて

risc-vにはベクトル拡張というものがあり、現在公式に批准されるために評価されている段階で、仕様も凍結されている。そのため、ベクトル拡張が正式にrisc-vの拡張命令として認められる日もそう遠くはないだろう。そんなベクトル命令をアセンブリとして書くことができるのはもちろん、高級言語からその命令を使うように指示することができる。つまり、高級言語コンパイル結果としてベクトル命令を吐いてくれるように高級言語から指定できるのだ。これによりコンパイラが最適化を頑張らなくても高級言語プログラマが頑張ることで、より実行性能が高いアセンブリを出力できるようになる。これがIntrinsicsである。これから例を交えながら紹介していく。

rvv_strlen.c

文字列の長さを測るstrlen.cをrvv(risc-v vector exetension) intrinsicを使って書いたコードがgithubに例として上がっているので、それを以下に示す。

#include "common.h"
#include <riscv_vector.h>
#include <string.h>

// reference https://github.com/riscv/riscv-v-spec/blob/master/example/strlen.s
size_t strlen_vec(char *src) {
    // このアーキテクチャが扱える8bitデータの最大の要素数を取得
    // m8としているのは8個のレジスタをグルーピングして一度に使おうとしている
    size_t vlmax = vsetvlmax_e8m8();
    // 文字列があるアドレスに対するポインタ
    char *copy_src = src;
    long first_set_bit = -1;
    
    // 実際に一つのループで扱う要素数
    size_t vl;
    while (first_set_bit < 0) {
        // Unit-stride Fault-Only-First Loads Functions
        /* vlmaxだけ読み込もうとして、アクセス禁止領域へのアクセスのような例外が発生する
       ようなインデックスに来たら、実際には例外は発生させずに読み込みを終了してvlに例外を
       発生させたインデックス(読み込めた要素数)を書き込む。*/
        // 最初の要素が例外を発生させるような場所だったら実際に例外を発生させる。
        // これが最後のループなら"hogefuga($0)"のように文字列の終端記号もvlは含めている
        /* メモリにはデータが連続して格納されている可能性があるので、
       vec_src = "hoge($0)fuga"のようになっている可能性もある。*/
        // 読み取った文字(8bit)がvec_srcに代入されていく。
        vint8m8_t vec_src = vle8ff_v_i8m8(copy_src, &vl, vlmax);
        
        // ベクトル整数比較命令
        // seq = set if equal
        // 文字列の終端記号の検出。
        // vx とあるので、ベクトルとスカラレジスタの値を比較して等しいと1をセット
        // vec_srcの中をvlの数だけ調べる
        // vbool1_tとあるのは、n(1) = SEW(i8) / LMUL(m8)ゆえ
        vbool1_t string_terminate = vmseq_vx_i8m8_b1(vec_src, 0, vl);
        
        // このループで扱ったデータ数だけポインタを進める
        copy_src += vl;
        
        /* string_terminateから1を持つ要素(終端記号がある位置)を探してその中で
       最も低い番号を出力 */
        // 最後のループの場合は、結果的にこのループ内で処理した対象の文字列の文字数を出力
        // 1がない場合は-1を出力
        first_set_bit = vfirst_m_b1(string_terminate, vl);
    }
    copy_src -= vl - first_set_bit;
    return (size_t)(copy_src - src);
}

int main() {
    const uint32_t seed = 0xdeadbeef;
    srand(seed);
    
    int N = rand() % 2000;
    
    // data gen
    char s0[N];
    gen_string(s0, N);
    
    // compute
    size_t golden, actual;
    golden = strlen(s0);
    actual = strlen_vec(s0);
    
    // compare
    pust(golden == actual ? "pass" : "fail");
}

わかったこと

  • vlの更新は基本的に、vsetとfault-only-firstでのみ行われる。大体ループの最初でvsetによってvlをセットして、ループを回っていく中で、どれだけロードできるかでvlの値を更新していくという流れ。
  • 文字列の終端の判定は、char型(8 bit)の0(終端文字列)の検出という形で行っている。
  • セキュリティに配慮してアクセス禁止領域にアクセスするようなロードを行わないようにFault-Only-First Loadsというロード命令が使われている。

その他

  • 上を書いた後に気づいたが、この記事がintrisicの利用例をexampleに沿ってわかりやすく紹介しているので、非常におすすめ。

参考にした記事