150 Views
December 30, 20
スライド概要
OpeLa プロジェクトの進捗報告です。AArch64 対応とテストケースの OpeLa 化が主な話題です。VSCode によるデバッグも紹介しています。
サイボウズ・ラボ株式会社で教育向けのOSやCPU、コンパイラなどの研究開発をしています。
OpeLaの進捗 2020年12月30日 第23回自作OSもくもく会
OpeLaプロジェクトとは OpeLa: Operating and Language processing system OSと言語処理系を全部自作するプロジェクト OS アセンブラ コンパイラ リンカ ライブラリ 特徴:完全なセルフホスト
セルフホストとは(コンパイラ編) 自作コンパイラ ソースコード 入力 第1世代 自作コンパイラ コンパイル 第2世代 自作コンパイラ 自分自身をコンパイルすること GCCやClangはそうなっている GCCをビルドするのに他のコンパイラの助けは 不要 自作言語およびOSでこれをやりたい!
OpeLaの進捗 現在、コンパイラopelacを作成中 OpeLa言語仕様の策定&コンパイラ実装 進捗1:AArch64対応 進捗2:テスト自身をOpeLa言語で実装
進捗1:AArch64対応
Mac mini (M1, 2020)を手に入れ、AArch64対応を進めた
つい先日、今実装してある部分すべてがAArch64に対応した
x86-64で整数をスタックにpushする命令
void Push64(std::ostream& os, uint64_t v) override {
os << "
push " << v << '\n';
}
[email protected] Larry Ewing and The GIMP
AArch64で整数をスタックにpushする命令
void Push64(std::ostream& os, uint64_t v) override {
os << "
mov x8, " << v << '\n';
os << "
str x8, [sp, #-16]!\n";
}
https://www.apple.com/jp/mac/m1/
AArch64対応のやり方 アセンブリ命令を出力する部分を仮想クラスに抽出 各アーキ用のクラスを継承して実装 アセンブリ命令出力用の仮想クラス class Asm { public: enum Register { kRegL, kRegR, kRegNum, }; virtual virtual virtual virtual virtual … }; void void void void void Push64(std::ostream& os, uint64_t v) = 0; Push64(std::ostream& os, Register reg) = 0; Pop64(std::ostream& os, Register reg) = 0; Add64(std::ostream& os, Register lhs, Register rhs) = 0; Sub64(std::ostream& os, Register lhs, Register rhs) = 0;
Asmクラスを利用して命令を出力
ASTを受け取ってアセンブリを出力する関数
Asm* asmgen;
void GenerateAsm(ostream& os, Node* node,
string_view label_break, string_view label_cont,
bool lval = false) {
switch (node->kind) {
case Node::kInt:
asmgen->Push64(os, node->value.i);
return;
void Push64(std::ostream& os, uint64_t v) override {
os << "
push " << v << '\n';
}
void Push64(std::ostream& os, uint64_t v) override {
os << "
mov x8, " << v << '\n';
os << "
str x8, [sp, #-16]!\n";
}
AArch64に対応して分かったこと 下手に抽象化するより、実際に複数アーキに対応する段になって 抽象化する方がうまく行く 余計な抽象化を挟まず、シンプルに保てる 必要な抽象化をもれなく導入できる AArch64には面白い命令がある 意外と複雑なアドレッシングモードがある • x86-64の「base+scale*index」に相当するアドレッシングモードがある • ldr x8, [x9, x10, lsl #3] は「x8←x9+(x10<<3)」 プレインデックス、ポストインデックス str x8, [sp, #-16]! ldr x8, [sp], #16
プレインデックス LDR(メモリ→レジスタ) STR(レジスタ→メモリ) で使える アドレス指定レジスタを事 前に変化させる X1にSPを指定すれば Push/Popが実現できる https://developer.arm.com/architectures/learn-the-architecture/aarch64instruction-set-architecture/loads-and-stores-addressing
AArch64全般に対応したわけではない あくまでM1 Mac(Mach-O形式)のみの対応 同じAArch64でもRaspberry Piには未対応 コンパイラ・トリプルでいうところの<arch>のみの対応 将来的には、少なくともAArch64+ELFに対応させたい OpeLaの開発はLinux(ELFの世界)が主戦場だから https://clang.llvm.org/docs/CrossCompilation.html
進捗2:テスト自身をOpeLa言語で実装 今まではシェルスクリプトで テストを書いていた 各テストケース毎に opelacでコンパイル →ccでリンク →バイナリを実行 を繰り返しており、遅かった テストをOpeLa言語で実装 テスト全部入りのコードを1度 だけopelacでコンパイル めちゃくちゃ高速化! test_exit test_exit test_exit test_exit func func func func 42 11 2 5 'func 'func 'func 'func main() main() main() main() { { { { 42; }' 1+23 - 13; }' 12/2 - 2 * (3 - 1); }' -3 + (+8); }' testIntConstant() { 42; } testAddSub() { 1+23 - 13; } testMulDiv() { 12/2 - 2 * (3 - 1); } testUnaryOp() { -3 + (+8); }
テストをOpeLa言語で書く
テストケースを関数として実装
関数を呼び出し、結果を確認
テストケースを呼び出し、集計する
#define TEST_INT(want, got) testInt(want, got, #got)
var (passed int; failed int;)
テストケースの実行結果を確認する
func testInt(want, got int, gots *byte) {
if want == got {
print_string("[ OK ]: ");
passed += 1;
} else {
print_string("[FAILED]: ");
failed += 1;
}
func main() {
TEST_INT(42, testIntConstant());
TEST_INT(11, testAddSub());
…
TEST_INT(4 , testUserType());
print_int64(passed);
print_string(" passed, ");
print_int64(failed);
print_string(" failed\n");
return failed > 0;
}
print_string(gots);
print_string(" -> ");
print_int64(got);
if want != got {
print_string(", want ");
print_int64(want);
}
print_string("\n");
}
https://github.com/uchan-nos/opela/blob/master/compiler/test_run.opl
テストケース紹介(一部)
func
func
func
func
func
func
func
func
func
func
func
func
func
func
func
func
func
func
func
func
func
func
func
testIntConstant() { 42; }
testAddSub() { 1+23 - 13; }
testMulDiv() { 12/2 - 2 * (3 - 1); }
testUnaryOp() { -3 + (+8); }
testRelOp() { 3 < 1; }
testRelOp2() { 2*3 >= 13/2; }
testEqOp() { 2>2 == 4<=3; }
testBlankBlock() { }
testCompoundBlock() { 42; 3; }
testDefVar() { foo:=5; bar:=3; foo*bar; }
testReturn() { return 3; 42; }
testIf() { 1; if 42 > 10 { 2; } }
testIfVar() { cond := 10 < 1; 1; if cond { 2; } }
testAssign() { foo := 3; foo = 4; foo * 2; }
testAssign2() { foo:=5; bar:=7; foo=(bar=1)=42; foo-4; }
testAssign3() { foo:=5; bar:=7; foo=bar=42; }
testFor() { i:=0; s:=0; for i <= 10 { s=s+i; i=i+1; } s; }
testAssign4() { a:=5; a=b:=3; a*b; }
testFor2() { s:=0; for i:=0; i<=10; i=i+1 { s=s+i; } }
testElse() { if 0 { 3; } else { 4; } }
testElseIf() { if 0 { 3; } else if 1 { 5; } else { 4; } }
testSimpleBlock() { 42; {} }
testNextedFor() { s:=0; for i:=1;i<3;i=i+1{ for j:=1;j<3;j=j+1{ s=s+i*j; } } s; }
テストをOpeLaで書いて分かったこと Cプリプロセッサが便利 特に、引数を文字列化する機能 #define TEST_INT(want, got) testInt(want, got, #got) 気づかないバグをあぶりだせた -1+5が260になるバグ • シェルスクリプトではテスト可能な値が0~255なので気づかなかった • プロセスのexit codeによる制約 ローカル変数とグローバル変数の名前衝突 • グローバル変数と同じ名前のローカル変数を定義できない 意外とOpeLaでテストが書ける!
付録:VSCodeでopelacをデバッグしたい 本格的なデバッグ作業はIDEでやりたい GDBのコマンド操作より変数等が確認しやすいから VSCodeでC++のプログラムをデバッグする方法を模索した Clang + gdb + Ubuntu 18.04 lldbはVSCodeでの使用に対応してないらしい… 2つのJSONファイルの設定が重要 tasks.json: プログラムのビルド手順を設定する launch.json: プロセスの起動手順を設定する 後ほど紹介
VSCodeでopelacを デバッグする様子
tasks.json: プログラムのビルド手順を設定する { "tasks": [ { "type": "shell", "label": "C/C++: build opelac", "command": "make", "args": [ "CXX=clang++" ], "options": { "cwd": "${workspaceFolder}" }, "problemMatcher": [ "$gcc" ], … } ], "version": "2.0.0" } type=cppbuild, shell Cppbuild: Makefileを使わず、 command=clang++として直接 コンパイルする Shell: シェルコマンドなんでも使 える?(未調査) labelはlaunch.jsonから参照す る名前 problemMatcherはビルド時の ログ形式を指定
launch.json: プロセスの起動手順を設定する
{
"version": "0.2.0",
"configurations": [
{
"name": "opelacのビルドとデバッグ",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/opelac",
"args": ["<${workspaceFolder}/hgoe.opl"],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": […],
"preLaunchTask": "C/C++: build opelac",
"miDebuggerPath": "/usr/bin/gdb"
}
]
}
programは起動対象
標準入力にファイルを渡す
にはargsに"<file"
preLaunchTaskにビルドタ
スクを指定
miDebuggerPath
MI=Machine Interface
lldbはMIに対応してないぽい
のでgdbを使おう
まとめ OpeLaプロジェクトの概要 AArch64対応 テストのOpeLa化 VSCodeでのデバッグ
自作言語をセルフホストする道のり 第1世代 自作コンパイラ opelac.cpp 書き直し 入力 入力 入力 コンパイル clang++ 第2世代 自作コンパイラ opelac.opl 第1世代 自作コンパイラ コンパイル 第2世代 自作コンパイラ コンパイル ここまでくれば ぐるぐる回せる (はず) 第3世代 自作コンパイラ
OpeLaで目指す「完全セルフホスト」 自作OSの上で動く自作言語処理系で 自作OSと自作言語処理系をビルドすること 自作OS ソースコード 自作言語処理系 ソースコード 自作言語処理系 自作OS LinuxとGCCの ソースコード GCC,binutils Linux Linuxでいえば