WebAssemblyリンカのGC

GCの基本

  • GC = garbege collection (プログラミング言語のランタイムのGCとは異なる)
  • リンカに渡されたすべてのコード(データ)を愚直にすべて出力すると、サイズが大きくなりすぎてしまう。
  • 出力ファイルは使われるコード(データ)のみを含むようにしたい。
  • 入力ファイルのセクションを最小単位として、サイズを最適化する

(Wasmの場合、ディスアセンブルせずにCodeセクションをそれぞれの関数本体を分割できるため、関数単位でGCが行われるのかと誤解していた...)

アルゴリズム

mark and sweep GCのmarkと同じ原理で使われるセクションを再帰的にマークする。つまり、明らかに使われるシンボルから初めて、関数呼び出しやグローバル変数のアクセスによって辿りつけるセクションをマークしていく。リロケータブルなオブジェクトファイルは関数呼び出しやグローバル変数へのアクセスに対して、リロケーションを持つ。

int ret = 42;

int main() {
    return ret;
}

例えば、上記のコードを-cをつけてコンパイルすると、以下のようなリロケーションが生成される。(__stack_pointerはリンカが生成する特別なシンボル)

  - relocations for section: 4 (Code) [3]
   - R_WASM_GLOBAL_INDEX_LEB offset=0x000006(file=0x00008c) symbol=1 <env.__stack_pointer>
   - R_WASM_MEMORY_ADDR_LEB offset=0x00002b(file=0x0000b1) symbol=2 <ret>
   - R_WASM_FUNCTION_INDEX_LEB offset=0x00003b(file=0x0000c1) symbol=0 <__main_void>

wasmの場合はこの最初のセクションはエントリーポイント、no-stripなシンボル、init functionが所属するセクションとretained segmentsが該当する。(init functionsはグローバル変数のコンストラクタであることを示す関数のフラグである。)

github.com

擬似コードは以下のようになる。

set visited;
stack work_list;
for each section
  if section is a GC root {
    work_list.push_back(section);
    visited.insert(section);
  }
while (work_list.size()) {
  section sec = work_list.back();
  work_list.pop_back();
  for each section referenced by sec
    if (visited.insert(section))
      work_list.push_back(section);
}

from https://maskray.me/blog/2021-02-28-linker-garbage-collection

参考

AArch64 (Arm v8) についてのメモ (レジスタ,関数呼び出し,システムコール)

AArch64→x86-64のバイナリ変換について調べていときのAArch64のメモ

Instruction Set

Arm v8は3つの命令セットをサポートしている。

  • A32 (=ARM)
  • T32 (=Thumb2)
  • A64

A32とT32はどちらも32bitで、これらをまとめてAArch32という。 A32とT32はMOV PC, LDR PCなどの特別な命令でモードをが切り替わる。

A64は64bitの命令セットでAArch32に対してAArch64という。

Registers

GP registers

  • r0~r31: GPR
  • w0~w31: 32bitとして利用する場合の名称
  • x0~x32: 64bitとして利用する場合の名称

Dedicated registers

  • r29: frame pointer(fp)
  • r30: link register (lr): リターンアドレス専用
  • r31: ゼロレジスタ(xzr/wzr)として使われる

Mnemonic

ldrはLoad registerの略。 [w1, 12]w1+12の意味。

ldr w0,[w1,12] 

Calling convention

aapcs64ではこのように定められている。

https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst#general-purpose-registers

parameter passing/return valueにつかわれるレジスタはデータ型で決まる (aapcs64, 6.9節)

以下のCプログラムをコンパイルして眺めてみる。

godbolt.org

struct S {
    char c[3000];
};

static S s;

S foo() {
    return s;
}
foo():                                // @foo()
        stp     x29, x30, [sp, #-16]!           // 16-byte Folded Spill
        mov     x29, sp
        mov     x0, x8
        mov     x2, #3000                       // =0xbb8
        adrp    x1, _ZL1s
        add     x1, x1, :lo12:_ZL1s
        bl      memcpy
        ldp     x29, x30, [sp], #16             // 16-byte Folded Reload
        ret

関数プロローグ

pc(x30)とfp(x29)の退避が行われる。

もう一つ重要なのは、fooは無引数なのにx8をパラメータとして受け取っていること。 fooはサイズが大きな構造体を返す関数であるため、callerはメモリ領域を確保してx0を介してcalleeにそのメモリアドレスを渡す。 そして、calleeがそのアドレスに結果を書き込む。

aapcs64より:

the caller shall reserve a block of memory of sufficient size and alignment to hold the result. The address of the memory block shall be passed as an additional argument to the function in x8. The callee may modify the result memory block at any point during the execution of the subroutine

本体

aarchにはcall命令はないので、そのかわりにbl (branch and linkの略かな?)を使う。 memcpyの戻り値はアドレス(8byte)なのでr0で受け渡しする。

void* memcpy (void *dstpp, const void *srcpp, size_t len);

関数エピローグ

pcとfpを復元する

System call (Linux)

glibcの実装を見てみる

/* syscall (int nr, ...)

   AArch64 system calls take between 0 and 7 arguments. On entry here nr
   is in w0 and any other system call arguments are in register x1..x7.

   For kernel entry we need to move the system call nr to x8 then
   load the remaining arguments to register. */

ENTRY (syscall)
    uxtw    x8, w0
    mov x0, x1
    mov x1, x2
    mov x2, x3
    mov x3, x4
    mov x4, x5
    mov x5, x6
    mov x6, x7
    svc 0x0
    cmn x0, #4095
    b.cs    1f
    RET
1:
    b   SYSCALL_ERROR
PSEUDO_END (syscall)

https://github.com/bminor/glibc/blob/ad05a42370fa09062ff2b450fb69905d9f407643/sysdeps/unix/sysv/linux/aarch64/syscall.S

aarchではsvc命令でシステムコールを発行する。(x64でいうsyscall) Linuxではx0~x7にシステムコールの引数。x8にシステムコールの番号を入れてからsvc 0で呼び出すことがわかる。 (SVC命令は即値を取れるが、どうやらLinuxでは使われてないっぽい。) Linuxカーネル側でも確かにレジスタがこのように使われていることが確認できる。

Chromium OS Docs - Linux System Call Table b.csはbranch if carry set (carry=1)の意味。

典型的なユーザーモード(EL0)からカーネルモード(E1)のSVC命令直後は以下のように処理が進む。

References

ヨーロッパ最大のOSS会議に参加した

はじめに

2/2~2/3にかけてベルギーのブリュッセル自由大学で開催されたFOSDEM 2024に参加してきた。 現地参加のきっかけは、去年参加したGSoCで知り合ったGCCのメンテナのThomasさんに発表しないかと言われ、プレゼンを聞くだけでなく発表もしてきた。

自分の発表👇

FOSDEM 2024 - Unicode Support for GCC Rust

色んな人と会った

1日目の夜に、ブリュッセル中心部のバーでGSoCの修了生が交流する場所があり、取り組んでいたプロジェクトや大学のことなどを話した。インド人が多く、僕以外のアジア出身者はみんなヨーロッパ留学中でみんな英語が流暢だった。

2日目はGSoCでお世話になったGCCのメンテナの方々やメンターと会って話すことができた。また、僕と同じくGCC devroomで発表したRui Ueyamaさんにも会うことができた。

英語

GSoCがきっかけで英語を書いたり話したりする機会が増えたけど、発表はしたことがなかったので不安だった。質疑応答も含め意外となんとかなって良かった。他の人と話していて、聞き取れなくて聞き返すことが多かったので、もっとリスニングを上手くなりたい。

会場

クッキーを売るキッチンカー:

1日目の開会式の様子:

講義室前の廊下は次のプレゼン待ちの人でいっぱい: Rust devroomの様子:

聞いた発表

好きな低レイヤ関連の発表が多くて面白かった。

Using your Laptop TPM as a Secure Key Store: Are we there yet?

Intelのエンジニアの方の発表。Linux上ではcreate_tpm2_keyでTPMSSH秘密鍵などの色んな鍵を保存することができる。ブート時のTPMの初期化(乱数生成)や実装の説明、TPMに対する攻撃の話もあり面白かった。

elfconv: AOT compiler that translates Linux/AArch64 ELF binary to LLVM bitcode targeting WebAssembly

ELFの実行ファイルをwasmに変換して、ブラウザ上で実行できるようにするツール。remillというツールでx86-64LLVM IRに変換してからwasmにコンパイルするという仕組み。システムコールやjmp命令で工夫が必要だそう。1機械語LLVM IRの1BBに変換されるため、トランスパイルに時間が結構掛かりそう。移植性という点では良いと思った。

The plan for gccrs

GCCのRustフロントエンドの今後の開発についての話。rustcと異なり、borrow checkはHIRを変換したBIR上で行い、Tree codegenはHIRで行うことになったそう。標準ライブラリstd 1.49を目標にしており、そのためにはrustcのunstableな機能や、format_argsを実装する必要がある。よく知らないが、format_argsはトレイト解決の観点でかなり実装が大変らしい。

WASM 101: porting a Sega Game Gear emulator to the browser

SEGAのゲーム機(アーケード用?)のエミュレータをRustで開発して、その後、ブラウザ用にポートしたという話。オーディオやグラフィックのクレートの紹介や、cfgを使ってターゲットにコードを切り替えるテクニックなど。

Embedding Servo in Rust projects

Mozillaが開発していたブラウザServoの今後の話。Mozillaの大規模レイオフの影響で今は別の企業がエンジニアを雇って開発しているとのこと。CSS3のサポートやミニブラウザのデモを通じてEmbedding APIの紹介があった。

Standardizing the generation and signing of boot images

ブートプロセスの標準化の話。U-bootを使った現代のLinuxのブートの解説があったが、難しくて全然わからなかった...😇

旅費

FOSDEMの参加にかかった費用は全額the GNU Toolchain Fundに支援していただいた。 羽田-ベルギー、ロンドン-羽田、ベルギーでのホテル代で、計27万円ほど。 (会議が終わってから、他の国を観光するために、帰りはロンドン発にしてよいという許可をもらった。)

おわりに

初めてのカンファレンス参加で発表し、色んな人と出会うことができ、本当に良い機会になった。 日本ではオープンソースを仕事にする人はとても少ないため、貴重な話を聞けたり、人脈も広がったりして良かった。今後、継続的にGCCにコントリビュートするかはわからないけど、OSSには貢献していって、またこのようなカンファレンスに参加したいと思った。

旅の写真

ロンドン、トラファルガー広場での春節: アムステルダム国立美術館: アンネフランクの隠れ家: ベルリンのビール美味しかったな:

リンカ自作メモ

はじめに

二年前にC++の勉強のついでに作った簡単な静的オブジェクトのリンカを書いた。

github.com

今度はRustで書き直している。最初の目標はlibc.soを動的or静的リンクして、Hello worldを動かすこと。

github.com

現時点での参考文献を挙げる。

mold

github.com

Rui Ueyamaさんの作っているとにかく速いリンカ。 自分は2021年あたりのコミットにcheckoutして読んでいる。

moldの優れた点は

  • コードがわかりやすい
  • 小さなコードでマルチアーキ対応をしている
  • できるだけ多くの箇所で並列化している

例えば、ELFヘッダ、セクションヘッダ、.textセクションなど異なる構造をChunkというクラスで上手く操作・管理している。初めてリンカをつくったときは、ヘッダやセクションの管理に苦労したため参考になった。

ただ、生ポインタの循環参照が多くunsafeなしのRustで同じことをやろうとすると、かえって複雑になる。Rustには循環参のためのポインタ型Weak<T>が用意されているが、これを使わずに、HashMap<Item, ItemId>のようなプールを作り、ItemIdを参照代わりに使うほうが良いと思う。

moldではリンカの各ステージで、tbb::parallel_for_eachを呼び出す構造になっており、クラスの同じフィールドに対して、読み出しと書き込みが同時に発生しないように注意して作られている。Rustではthread safetyが厳しく検査されるため、これを実現するには、構造体の各フィールドをMutexでラップするだけでは不十分で、スピンロックを使う必要がある。

lld

github.com

LLVMツールチェインのリンカ。 初めてリンカを書いたときに参考にした。

Tool Interface Standard (TIS) Executable and Linking Format (ELF)

https://refspecs.linuxfoundation.org/elf/elf.pdf

ELFの仕様書。情報が多いが、実装したい箇所の情報を取り出すのが難しい。というのもELFはリンカが使わない情報(ローダー、デバッグ情報)を含むからだ。 構造体のフィールドの意味を知りたいときに、辞書的に使うと良いと思う。

readelf (binutils)

readelf (GNU Binary Utilities)

リンカ自作の初期は、readelfに認識されるようなファイルを出力することが目標になる。 アラインメントがずれていたり、不正な値が入っているときは、警告を出してくれる。objdumpよりもよく使う。

Linkers and Loaders

https://www.amazon.co.jp/-/en/JohnR-Levine/dp/4274064379

20年以上前(2001年)の本なので古い。リンカについて知らない人が最初に読むにはいいかもしれない。


まだ読んでないものとしては以下の通り。

Linkers ブログシリーズ, Airs – Ian Lance Taylor

Airs – Ian Lance Taylor » Linkers part 1

goldの作者のブログ。

リンカ・ローダ実践開発テクニック

リンカ・ローダ実践開発テクニック

数少ないリンカの和書の一つ。読んだことない。


既存のリンカ

macOS/iOSwindowsはどのリンカを使っているのだろうか?

GCCのGENERICを可視化する

GCCのGENERICをGraphvizで可視化する。

https://gist.github.com/tonyseek/4161012

からコードを拝借する。 以下のように3つファイルを作る。

astviz:

#!/usr/bin/env sh
SCRIPT_PATH=$(dirname $0)
gcc -o $1 $1.c -fdump-tree-original-raw
$SCRIPT_PATH/pre.awk $1.c.* | $SCRIPT_PATH/treeviz.awk > $1.dot
dot -Tpng $1.dot -o $1.png

pre.awk:

#!/usr/bin/env -S gawk -f 
/^[^;]/{
    gsub(/^@/, "~@", $0);
    gsub(/( *):( *)/, ":", $0);
    print;
}

treeviz.awk

#!/usr/bin/env -S gawk -f
#http://alohakun.blog7.fc2.com/?mode=m&no=355
BEGIN {RS = "~@"; printf "digraph G {\n node [shape = record];";}
/^[0-9]/{
s = sprintf("%s [label = \"{%s | {", $1, $1);
for(i = 2; i < NF - 1; i++)
    s = s sprintf("%s | ", $i);
    s = s sprintf("%s}}\"];\n", $i);
    $0 = s;
    while (/([a-zA-Z]+):@([0-9]+)/){
        format = sprintf("\\1 \\3\n %s:\\1 -> \\2;", $1);
        $0 = gensub(/([a-zA-Z]+):@([0-9]+)(.*)$/, format, "g");
    };
    printf " %s\n", $0;
}
END {print "}"}
$ echo "int main() { return 0; }" > a.c
$ ./astviz a

a.png

できた。

ちなみに、 astvizの最後の行を

$SCRIPT_PATH/pre.awk $1.c.* | $SCRIPT_PATH/treeviz.awk | dot -Tx11

にすれば中間ファイルを作らずにウィンドウ上にグラフを表示できて嬉しい。

GCC本体を快適に開発する

はじめに

言語問わず、LLVMと比べGCCの貢献に関するドキュメントが少なく感じるので、メモ程度に残しておきます。GCCのビルドの方法は色んなところに書いてあるので、そちらを参照してください。

対象読者は以下のとおりです。

ビルド

./gcc/configureでビルドコンフィギュレーションを実行します。典型的なオプションは以下の通り。

  • --disable-bootstrap : コンパイラのブートストラップを無効化
  • --disable-multilib : 異なるABIなどをサポートするためのライブラリをビルドしないようにする
  • --enable-languages=rust : ビルドする言語を選択する。この場合はRustを有効化
  • --enable-checking=extra,gimple,tree,types : コード生成時のツリーのチェックを有効化

詳しくはGCCのHPを参照: https://gcc.gnu.org/install/configure.html

makeを実行する時は-j($nproc)を渡しましょう。

テスト

End-to-endテストはgcc/gcc/testsuite/{language}/以下にあります。 単体テスト(gccではselftestsと呼ばれる)はgcc/gcc/{language}内にあります。

End-to-endテストではdejagnuというフレームワークが使われています。 言語処理系で典型的な、期待されるエラーやその内容、標準出力やプロセスの終了値をチェックできます。

以下はCプリプロセッサのテスト例(dejagnu)です。

/* PR preprocess/95183  */

/* { dg-do preprocess } */

#define f(x) x

f( /* { dg-error "-:unterminated" "unterminated macro" } */

LSP

GCCC++で書かれています。 ある程度大きなソースコードなので、LSPはClangdをつかうと速くて快適です。

Clangdはビルド時のコンパイラオプションを収集したcompile_commands.jsonというファイルを認識してLSPとして動作します。 GCCはビルドシステムとしてMakefileを使います。CMakeと異なり、Makefileはcompile_commands.jsonを生成してくれません。そこで、Bearというツール経由でmakeコマンドを呼び出すことでcompile_commands.jsonを生成することができます。

$ ls
gcc
$ mkdir gcc-build && cd gcc-build
# ビルドのコンフィギュレーション
$ ../gcc/configure ...
...
# bear経由でビルドする
$ bear -- make -j($nproc) ...
...
# 生成されたcompile_commands.jsonをコピー
$ cp compile_commands.json ../gcc/compile_commands.json

注意: 自分の環境ではmakeに-j($nproc)を渡すとbearがcompile_commands.jsonを生成してくれませんでした。原因はよくわかりません。

Clangdでシステムヘッダが見つからない場合

clangdはヒューリスティック<string.h>などのヘッダーファイルを探すため、見つからないことがたまにあります。その場合は、.clangdという名前のファイルを作り、以下のように適当にインクルードパスを指定すればよいです。

# .clangd
CompileFlags:
  Add: [
    "-I/usr/lib/gcc/x86_64-linux-gnu/11/include",
    "-I/usr/include/x86_64-linux-gnu",
    "-I/usr/include",
    "-I/usr/include/c++/11",
    "-I/usr/include/x86_64-linux-gnu/c++/11",
    "-I/usr/include/c++/11/backward"
  ]

参考: https://stackoverflow.com/questions/17939930/finding-out-what-the-gcc-include-path-is

コミットログ

GCCのコミットメッセージは定められたフォーマットに従う必要があります。 以下は実際のコミットメッセージです。(名前とメールは伏せました。)

gccrs: Refactor FnType deprecated API

gcc/rust/ChangeLog:

    * backend/rust-compile-expr.cc (CompileExpr::visit): Use new API.
    * typecheck/rust-tyty-call.cc (TypeCheckCallExpr::visit): Use new API.
    * typecheck/rust-tyty-cmp.h: Remove old API.
    * typecheck/rust-tyty.cc (FnPtr::is_equal): Use new API.
    * typecheck/rust-tyty.h: Remove old API.
    * typecheck/rust-unify.cc (UnifyRules::expect_fnptr): Use new API.

Signed-off-by: Name <Email>

このようなChangeLogを自動的に生成するにはgit gcc-commit-mklogコマンドを使います。 gccディレクトリで./contrib/gcc-git-customization.shを実行することでインストールできます。

おわりに

役に立てば幸いです!

GSoC(gcc Rust)の進捗報告

GSoC2023でGCC RustフロントエンドのUnicodeサポートに取り組んでいます。 先週、midterm eveluationが終わり、無事に通過することができました。

この記事では、時系列順に取り組んだことと今後について説明します。

現状

こんな感じで、ソースコード内で色々な文字を使うことができます。 識別子だけでなく、様々な空白や改行にも対応しています。 name mangingは未実装のため、非ASCII文字を名前に含む関数などのリンクには対応していません。

fn Русский() {
  let 日本語 = ();
}

目次の代わりにに進捗を列挙しておきます。

これまでに取り組んだこと

UTF-8識別子のトークナイズ

Rustでは識別子は以下のような正規表現で表されます。

(XID_Start | '_') XID_Continue*

XID_StartとXID_Continueはそれぞれ英語アルファベットと英数字を拡張した属性と思っていただけると良いです。これらの属性はUnicodeのデータベースの一つであるDerivedCoreProperties.txtをルックアップすることで調べることができます。

これまで、gccrsは生文字列リテラル (r#"..."#)はサポートしていたため、部分的にUTF-8デコーダが使われていました。 このデコーダを識別子をトークナイズする際にも使うようにしました。

Unicodeでは、各文字に対して、一意の数であるコードポイントを定義しています。 もともと、コードポイントと属性のテーブルはlibcppというGCCのCプリプロセッサのコード上に存在していました。これを活用するために、libcppにテーブルを二部探索する関数を追加し、gccrsにリンクするようにしました。

lexer/parserのリファクタリング

UTF-8デコーダがlexerの一部でしか使えなかったので、ファイルからソースコードを読み込む時点でデコードを行うように改修しました。 lexer内ではcharを使って1つの文字を表していたため、全てしらみつぶしに置き換えるのが大変でした。

Rustはセルフホストしているため、このような明示的なUTF-8デコーダコンパイラにないのが面白いです。

識別子への位置情報の追加

識別子の位置情報とは、ファイル名や行番号、列番号のことです。 これまでgccrs内部では、識別子と位置情報がデータ構造として一緒に扱われていませんでした。 そのためエラー出力(diagnostics)がユーザーに優しくないという短所がありました。 イメージとしては以下のような感じです。

before:

fn main() { ...
^
error: hoge

after:

fn main() { ...
   ^^^^
   error: hoge

この作業自体はUnicode対応とは関係ありませんが、gccrsの予てからの課題だったので取り組みました。 パーサーやASTまわりのコードだけで数千~数万行あるので大変かと思っていましたが、意外と百行程度の変更で済みました。

UnicodeNFC正規化

UnicodeのUAX#15という仕様書の中で、Unicodeのバイト列の正規化方法が定義されています。 正規化をすることで、同じように見えるがバイト列が異なる文字列を同じバイト列に変換することができ、文字列比較が簡単になります。

以下は正規化の例で、NFC以外にもNFD, NFKC, NFKDの三種類があります。(気になった方は調べてみてください。)

https://unicode.org/reports/tr15/images/UAX15-NormFig5.jpg https://unicode.org/reports/tr15/

Rustはソースコードに現れる識別子をNFCという形式に正規化することを定めています。 (ハングルを除く)文字列の正規化を行うはすでに実装できているため、今後はこれをパーサーに載せる予定です。 ちなみに、正規化においてハングルは音節文字の分解・合成を効率的に処理するため特別扱いされます。

実装には、正規化のリファレンス実装であるW3CCharlintを参考にしました。 10年前からメンテされていなくて最新版のUnicodeに対応していないので、パッチを送ろうかと思っています。

また、Unicodeデータベースを利用するために、PythonC++のヘッダーファイルのジェネレータを作りました。

github.com

これから

今後はナイーブな正規化の実装の最適化や、name manglingに必要なPunycode変換の実装を行っていく予定です。