fopen_s/fprintf_sなどのセキュアCライブラリ
家庭教師としてオンラインでプログラミングを教えているのだが、その際fopen_s
やfprintf_s
という関数が出てきて、最初はfopen
やfprintf
の新しいバージョン、もしくは古いバージョンの関数かなくらいに思っていたのだが、どうもMacのgccでも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
非常に短く無駄なアセンブリはないスッキリしたものになった。
参考サイト
- コンパイラのコード生成: 繰返し - Qiita:この記事ではフレームサイズの説明などがより詳しく掲載されている。
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でコンパイルしていこうと思う。
TypeScript 基本型
処理の流れ
以下が大まかな処理の流れ。まず1~3はTSにおける処理。4~6はJSにおける処理。
TypeScriptソース→TypeScript AST
ASTが型チェッカーによってチェックされる
TypeScript AST→JavaScriptソース
JavaScriptソース→JavaScript AST
AST→バイトコード
バイトコードがランタイムによって評価される。
ラインタイムに関する理解が曖昧なので、また別の機会でまとめてみようと思う。
型の種類
型の基本的な型を軽く紹介する。
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に沿ってわかりやすく紹介しているので、非常におすすめ。