X68000上でZigコンパイラを動かすことに挑戦(動かない)
前回記事[1]でX68000上でRustとZigのHello worldが動くところまでできたので今回は気を良くしてZigのStage1コンパイラを動かすことに挑戦していきます。結果としては残念ながら、、ですがX68000にとってはやや大きめのアプリケーションをwasm経由で持ってきたときに何が起こるのかという情報として記事にまとめたいと思います。
Zigのstage1コンパイラとは
まず、今回動かそうとしているZigのstage1コンパイラについて説明します。
前回[2]も簡単に触れましたがZigのstage1コンパイラは、Zig言語で書かれたZigコンパイラのソースコードをC言語のソースコードに変換するために利用されるものです。
今回はこのZigのstage1コンパイラをX68000向けにビルドして、Zigで書かれたHello worldをC言語のソースに変換することに挑戦します。
今回のモチベーションとしてはどうしてもこのコンパイラを動かしたいというよりは、大きめのWasmファイルをX68000に持ってくる過程でどのような課題があるのか洗い出すことが目標になります。
ビルド編
早速、前回用意したwasm2c
を利用して、zig1.wasm
をX68000向けにビルドしていきます。
結果、下記のようにHAS060のエラーが出力されます。
_build/m68000/zig1.s 453523: Error: オーバーフローしました
_build/m68000/zig1.s 453524: Error: オーバーフローしました
_build/m68000/zig1.s 453525: Error: オーバーフローしました
_build/m68000/zig1.s 453526: Error: オーバーフローしました
...
エラーが 282 個ありました.アセンブルを中止します
いきなりハードなスタートです。大量のオーバーフローエラーを出力して止まってしまいました。
switch
文の相対ジャンプテーブル
このオーバーフローエラーは、下記のように16ビット相対アドレスのテーブルを使ってジャンプを行う個所で発生しています。
ラベルの相対アドレスが16ビットで扱える範囲を超えていることがオーバーフローエラーの原因となっています。
add.l d0,d0 * add.l %d0,%d0 | tmp14625
move.w _?L22342(pc,d0.l),d0 * move.w .L22342(%pc,%d0.l),%d0 |, tmp14626
jmp 2(pc,d0.w) * jmp %pc@(2,%d0:w) | tmp14626
.align 2,0x284c * .balignw 2,0x284c
* .swbeg &159
_?L22342: *.L22342:
.dc.w _?L22441-_?L22342 * .word .L22441-.L22342
.dc.w _?L22440-_?L22342 * .word .L22440-.L22342
.dc.w _?L22439-_?L22342 * .word .L22439-.L22342
.dc.w _?L22438-_?L22342 * .word .L22438-.L22342
.dc.w _?L22437-_?L22342 * .word .L22437-.L22342
...
このアセンブリコードは下記のようなswitch
文のコンパイル結果として生成されます。
wasm2c
が機械的に生成したコードには巨大なswitch
文が含まれており、これをコンパイルした時に同じ関数内にあるラベルとの相対アドレスが16ビットの範囲を超えてしまうという現象です。
switch (l205) {
case 0: goto l146;
case 1: goto l139;
case 2: goto l167;
case 3: goto l168;
case 4: goto l164;
...
これを回避するために絶対アドレスでジャンプするjmp命令を1段追加して2段ジャンプを行うように手を加えました。
このコードを生成するために今回はHAS060に小細工を加えました。HAS060に対するハックの詳細は今回の記事では説明を省略させていただきます。
_?L22342: *.L22342:
.dc.w _?L22441_jmp-_?L22342 * .word .L22441-.L22342
.dc.w _?L22440_jmp-_?L22342 * .word .L22440-.L22342
.dc.w _?L22439_jmp-_?L22342 * .word .L22439-.L22342
.dc.w _?L22438_jmp-_?L22342 * .word .L22438-.L22342
.dc.w _?L22437_jmp-_?L22342 * .word .L22437-.L22342
...
_?L22441_jmp
jmp _?L22441
_?L22440_jmp
jmp _?L22440
...
HLKのメモリ不足
ジャンプテーブルのオーバーフローに対処するとビルドはリンカの処理まで進みます。しかし、今度はHLKが下記のエラーを出力して止まります。
$run68 /xdev68k/x68k_bin/hlk301.x -p -i _build/m68000/_lk_list.tmp -o ZIG1.X
Out of memory !! (゚_゚;
今回はrun68
に変更を加え12MByte以上のメモリを使えるようにして対処しました。run68
に対する変更の詳細に関してはこの記事では説明を省略させていただきます。
ビルド結果
テーブルジャンプの対応と、run68のメモリ拡張を行うとビルドが通るようになります。
生成されたファイルのサイズは 5MByteとなっています。やや大きいサイズではありますが、X68000の12MByteのメモリで何とか実行できそうな期待を持たせてくれます。
-rw-r--r-- 1 icon icon 5074282 Feb 2 16:55 ZIG1.X
-rw-r--r-- 1 icon icon 2491454 Jan 9 22:22 zig1.wasm
ヒープサイズ
ビルドも通るようになったので必要となるヒープサイズの目安を確認します。wasm2c
によって生成されたinit_data
関数の冒頭の処理で確保しているメモリサイズを確認します。
static void init_data(void) {
p0 = UINT32_C(521);
c0 = p0;
m0 = calloc(c0, UINT32_C(1) << 16);
static const uint8_t s0[UINT32_C(18)] = {
0xC7, 0x59, 0x01, 0x02, 0x07, 0x00, 0x00, 0x00, 0xC7, 0x59, 0x01, 0x02, 0x07, 0x00, 0x00, 0x00, 0x20, 0x02,
};
memcpy(&m0[UINT32_C(0x2000000)], s0, UINT32_C(18));
static const uint8_t s1[UINT32_C(175945)] = {
calloc
関数で確保しているメモリは521
かける0x10000
なので必要なメモリサイズとしては0x2090000
となります。
このサイズは24bitで表せる範囲を超えています。この時点で24bitアドレスのm68000上での実行はできないことが確定しました。
ただここで諦めるのはつまらないので、これ以降はメモリ空間を拡張したrun68
上で動かすことを新たな目標とします。
ヒープ領域の設定は、リンク時のオプション-d _HEAP_SIZE=5000000
の数値部分を変えることで変更できます。この数値は16進数で値を設定します。
xcのmallocの24bitアドレス制限を外す
32bitアドレスで動かすにはxcのライブラリにも一部手を加える必要があります。
xcライブラリで提供されているmallocなどのメモリ確保を行う関数では、24bitアドレスを超えたメモリ空間上にメモリを確保しないように制限がかけられています。
具体的には管理テーブルの初期化を行う__MAIN.O
の中とrealloc関数の中で24bitアドレスに強制する処理が入っています。
今回の実験ではこの制限が邪魔になるので外してしまいます。
__MAIN.O
では強制のために0x00FFFFFF
でアドレスをマスクする処理が入っていますのでこれをバイナリエディタで全ビットマスクになるよう改修します。
reallocは24bit強制なし版を作成してリンクするようにします。
これらのファイルはFSHARP許諾条件[3]の範囲で扱うことになります。
デバッグ
ここまでで実行するファイルが出来上がったので、run68上で実行していきます。
実行時、コマンドの第一引数にzigコンパイラリポジトリのlib
ディレクトリを指定し、それ以降にzigコンパイラのオプションを指定していきます。下記の実行例のオプションはzigコンパイラリポジトリのCMake.txt
を参考に設定しました。
では実行してみましょう。
$run68 ZIG1.X ../../../zig/lib build-exe hello.zig -ofmt=c -lc -OReleaseSmall -target x86_64-linux
未対応のリロケート情報があります
またも、簡単には通してもらえません。
リロケーションテーブル
未対応のリロケート情報があります
このエラーは実行ファイルのリロケーションテーブルにrun68
では解釈できないものが含まれていることを表しています。
リロケーションテーブルは再配置に伴ってベースアドレスによる補正が必要な個所のリストとなっています。通常このリスト内の情報は16bitの相対アドレスで格納されています。
例えば下記のように6byteごとに絶対アドレスが登場するコードが先頭にあれば、0x0002, 0x0006, 0x0006
のように前の補正個所からの相対アドレスが格納されることになります。
23C0 0049 EA90 move.l d0,$49ea90
23C0 0049 EA94 move.l d0,$49ea94
23C0 0049 EA98 move.l d0,$49ea98
続いて、補正が必要な個所の間隔が16bitで表せる範囲を超えた場合に何が起こるか見ていきます。
今回の場合、wasm2c
の生成した下記の初期値ありのポインタがdata
セクション上に作成されます。この初期値はm0
の絶対アドレスとなるため再配置での補正の対象になります。
uint8_t **const wasm_memory = &m0;
このwasm_memory
のアドレスは、この直前の補正個所からの相対アドレスが16bitでは収まらない状態となっています。
このような場合、HLKはリロケーションテーブルに0x0001
を置いた後、32bitの相対アドレスを格納しているようです。
一方でrun68ではこの0x0001
を見つけた時点で冒頭のエラーを返すようになっています。
したがって今回はrun68のリロケート処理を下記のように書き換えて対応しました。
/*
機能:Xファイルをリロケートする
戻り値: TRUE = 正常終了
FALSE = 異常終了
*/
static int xrelocate( Long reloc_adr, Long reloc_size, Long read_top )
{
Long prog_adr;
Long data;
UShort disp;
prog_adr = read_top;
for(; reloc_size > 0; reloc_size -= 2, reloc_adr += 2 ) {
disp = (UShort)mem_get( read_top + reloc_adr, S_WORD );
if ( disp == 1 ) {
reloc_size -= 2;
reloc_adr += 2;
prog_adr += mem_get( read_top + reloc_adr, S_LONG );
reloc_size -= 2; // for文内の操作と合わせて4byte分進める
reloc_adr += 2;
} else {
prog_adr += disp;
}
data = mem_get( prog_adr, S_LONG ) + read_top;
mem_set( prog_adr, data, S_LONG );
}
return( TRUE );
}
memory.grow
失敗のハンドリング
ここまでの変更を加えると、run68上に実行ファイルが配置されて最初の部分が動き出します。
実行時ログ出力の冒頭はこのような感じになります。
wasi_snapshot_preview1_args_sizes_get()
wasi_snapshot_preview1_args_get()
wasi_snapshot_preview1_fd_prestat_get(3)
wasi_snapshot_preview1_fd_prestat_dir_name(3, "")
wasi_snapshot_preview1_fd_prestat_get(4)
wasi_snapshot_preview1_fd_prestat_dir_name(4, "")
wasi_snapshot_preview1_fd_prestat_get(5)
wasi_snapshot_preview1_fd_prestat_dir_name(5, "")
wasi_snapshot_preview1_fd_prestat_get(6)
wasi_snapshot_preview1_path_filestat_get(3, 0x1, "./build.zig")
wasi_snapshot_preview1_path_create_directory(4, "h")
wasi_snapshot_preview1_path_open(4, 0x1, "h", 0x2, 0xllX, 0xllX, 0x0)
wasi_snapshot_preview1_path_create_directory(4, "o/cb5efcac25e8361855b0a6001f8ada9b")
wasi_snapshot_preview1_path_open(4, 0x1, "o/cb5efcac25e8361855b0a6001f8ada9b", 0x2, 0xllX, 0xllX, 0x0)
wasi_snapshot_preview1_path_open(5, 0x1, "std", 0x2, 0xllX, 0xllX, 0x0)
wasi_snapshot_preview1_path_create_directory(4, "z")
wasi_snapshot_preview1_path_open(4, 0x1, "z", 0x2, 0xllX, 0xllX, 0x0)
wasi_snapshot_preview1_path_create_directory(4, "z")
wasi_snapshot_preview1_path_open(4, 0x1, "z", 0x2, 0xllX, 0xllX, 0x0)
wasi_snapshot_preview1_path_open(3, 0x0, "hello.c", 0x1, 0xllX, 0xllX, 0x0)
wasi_snapshot_preview1_clock_time_get(1, llu)
wasi_snapshot_preview1_path_filestat_get(7, 0x1, "builtin.zig")
wasi_snapshot_preview1_random_get(32)
wasi_snapshot_preview1_path_open(7, 0x0, "ECPq6GK_MkfezbBT", 0xD, 0xllX, 0xllX, 0x0)
wasi_snapshot_preview1_fd_write(12, 0x1FF2340, 1)
wasi_snapshot_preview1_fd_close(12)
wasi_snapshot_preview1_path_rename(7, "ECPq6GK_MkfezbBT", 7, "builtin.zig")
wasi_snapshot_preview1_clock_time_get(1, llu)
wasi_snapshot_preview1_path_open(8, 0x0, "std.zig", 0x0, 0xllX, 0xllX, 0x0)
wasi_snapshot_preview1_fd_filestat_get(13)
wasi_snapshot_preview1_path_open(10, 0x0, "bc4efcbe47a767d0a76afe4767871fe6", 0x1, 0xllX, 0xllX, 0x0)
wasi_snapshot_preview1_fd_read(14, 0x1FF1EB0, 1)
wasi_snapshot_preview1_fd_filestat_set_size(14, llu)
wasi_snapshot_preview1_fd_read(13, 0x1FF1EC0, 1)
wasi_snapshot_preview1_fd_write(14, 0x1FF2148, 5)
wasi_snapshot_preview1_fd_close(14)
wasi_snapshot_preview1_fd_close(13)
wasi_snapshot_preview1_clock_time_get(1, llu)
wasi_snapshot_preview1_path_open(8, 0x0, "array_list.zig", 0x0, 0xllX, 0xllX, 0x0)
lib
ディレクトリのstd
ライブラリのソースを端から順にコンパイルしているようです。
しばらく動かしていると下記のようなエラーを出して停止します。
run68 exec error: アドレスエラーが発生しました PC=33382C20
PC of previous op code: PC=452502
** EXECUTED INSTRUCTION HISTORY **
ADDRESS OPCODE MNEMONIC
-------------------------------------------------------
$452468 0800 000A btst #10,d0
$45246C 667A bne.b $4524E8
$4524E8 FF3F FCALL $3F
$4524EA 2200 move.l d0,d1
$4524EC 6B1C bmi.b $45250A
$4524EE 660C bne.b $4524FC
$4524FC 2001 move.l d1,d0
$4524FE 4FEF 000A lea.l 10(a7),a7
$452502 4E75 rts
$000000 0000 0000 or.b #$00,d0
アドレス:33382C20
Segmentation fault
スタックを破壊したようですね。
このようなエラーが出るたびにrun68の-debug
オプションを使って解析することになるのですが、ここから先はすべてHeap領域の不足によるwasmのmemory.grow
動作の失敗に起因するエラーでした。したがって、この後はエラーが出るたびに解析してはHeapの設定を変更するという作業の繰り返しになりました。
個々のエラー解析の詳細は省略しますが、解析の中でmemory.grow
失敗時に特徴的な値がメモリアクセスのインデックスに使われることが分かったのでそのことについて記載しておきます。
メモリ破壊のエラーが発生するとき、多くの場合wasm
のメモリ内のインデックスとして0xFFFF0000
が使われていました。このインデックスはマイナスの値のインデックスを使っているのと同じになるので結果としてメモリ領域外の値が破壊されることにつながっていました。
この0xFFFF0000
という値が使われてしまうメカニズムについて以下で説明します。
memory.grow
が実行されたときに十分なメモリがない時には結果として-1
を返すことが仕様で定められています。
The memory.size instruction returns the current size of a memory. The memory.grow instruction grows memory by a given delta and returns the previous size, or -1 if enough memory cannot be allocated. Both instructions operate in units of page size.[4]
wasm2cが出力するmemory.grow
の処理もrealloc
失敗の時などには-1
を返すようになっています。(Fの数が合わない気もしますが、、)
static uint32_t memory_grow(uint8_t **m, uint32_t *p, uint32_t *c, uint32_t n) {
uint8_t *new_m = *m;
uint32_t r = *p;
uint32_t new_p = r + n;
if (new_p > UINT32_C(0x10000)) return UINT32_C(0xFFFFFFF);
uint32_t new_c = *c;
if (new_c < new_p) {
do new_c += new_c / 2 + 8; while (new_c < new_p);
if (new_c > UINT32_C(0x10000)) new_c = UINT32_C(0x10000);
new_m = realloc(new_m, new_c << 16);
if (new_m == NULL) return UINT32_C(0xFFFFFFF);
*m = new_m;
*c = new_c;
}
*p = new_p;
memset(&new_m[r << 16], 0, n << 16);
return r;
}
一方でこの関数の呼び出し元では下記のようなハンドリングになっています。
l6 = memory_grow(&m0, &p0, &c0, l8);
l0 = l6;
l8 = UINT32_C(0x10);
l6 <<= l8 & 0x1F;
l7 = UINT32_C(0x0);
l8 = l0;
uint32_t l14 = UINT32_C(0x0);
l8 = (int32_t)l8 > (int32_t)l14;
l6 = l8 ? l6 : l7;
l3 = l6;
goto l4;
l4:;
uint32_t l15 = l3;
return l15;
処理を見るとmomory_grow
から戻った結果を-1
との比較を経ずに16bitシフトして使ってしまっているようです。(なぜか0
との比較は行っていますが)
この結果、0xFFFF0000
が新しく確保された領域の先頭を指すインデックスとして使われてしまうことになります。
Zigのコードをwasm2c
で動かしていてこの値がインデックスとして出てきたらHeap領域不足と考えてよさそうです。
旅の終わり
Heap領域のサイズを192MByteまで増やすと、momory.grow
由来のエラーは出ずに、下記のようなエラーを1,000行程度出して止まるようになります。今回はrun68の出力するエラーではなく、Zig stage1コンパイラが出力しているエラーとなっています。エラー内容から考えると字句解析はある程度進んでシンボルの解決でエラーになっているようです。
/lib/std/Build.zig:1165:27: error: use of undeclared identifier 'UserInputOption'
/lib/std/Build.zig:1458:5: error: use of undeclared identifier 'assert'
/lib/std/Build.zig:1499:17: error: use of undeclared identifier 'allocPrintCmd'
/lib/std/Build.zig:1519:5: error: use of undeclared identifier 'assert'
/lib/std/Build.zig:1621:39: error: use of undeclared identifier 'name'
/lib/std/Build.zig:1732:17: error: use of undeclared identifier 'dumpBadGetPathHelp'
/lib/std/Build.zig:1812:29: error: use of undeclared identifier 'VcpkgRootStatus'
/lib/std/Build.zig:1833:23: error: use of undeclared identifier 'InstallDir'
/lib/std/Build.zig:1843:10: error: use of undeclared identifier 'InstallDir'
/lib/std/Build.zig:1855:32: error: use of undeclared identifier 'Allocator'
/lib/std/Build.zig:1889:39: error: use of undeclared identifier 'fs'
/lib/std/mem.zig:1815:72: error: use of undeclared identifier 'Endian'
/lib/std/mem.zig:1928:17: error: use of undeclared identifier 'std'
/lib/std/mem.zig:1958:5: error: use of undeclared identifier 'byteSwapAllFields'
...
ここから先はもう少しリッチなデバッグ環境がないと解析が難しそうなので今回の挑戦はここまでとさせていただきたいと思います。
まとめ
X68000上でZigのstage1コンパイラを動かすことに挑戦することで、wasm2c
でやや大きめのアプリケーションをビルドする際の課題を見てきました。
最終的に正常動作させることはできませんでしたが、いくつかの課題とその対処方針が得られました。
2020年代の環境で動かすことを前提としたバイナリであることを差し引いて考える必要はありますが、大量のHeap領域が必要になることと、デバッグする際の情報(関数のメモリ配置など)の少なさはこの方式でX68000の実行ファイルを開発する際の大きな課題だと感じました。
次回予告
次回はRustのno_std
で作成したwasm
ファイルをX68000で動かしてみたいと思います
Discussion