💯

X68000上でZigコンパイラを動かすことに挑戦2(後日Done)

2024/02/06に公開

X68000向けにビルドしたZig Stage1コンパイラの実験の続編です。
前回記事[1]の公開後、TcbnErik氏にrun68xの情報を教えてもらったので、run68xを使って前回の挑戦の続きに取り組みました。(教えてもらったのはリロケートに関する情報ですが)
若干まとまりのない記事になってしまいましたが、挑戦の内容を報告させていただきます。

https://twitter.com/kg68k/status/1753794137290309936

run68xのメモリ拡張改造

前回同様run68xを改造してメモリを広く使えるようにしてしまいます。
あまりこういう需要はないかもしれませんが、以下にrun68xの改造個所を説明します。
※当然のことながらこの改造でHuman68kとの互換性はなくなります。

まずは、メモリサイズの定義から変更します。今回は29bitメモリバスとして使える改造としているので512MByteに設定します。

run68.h
#define DEFAULT_MAIN_MEMORY_SIZE (512 * 1024 * 1024)

mem_red_chk, mem_wrt_chkのメモリマップドIOのチェックをしているブロックをコメントアウトしてしまいます。今回はこの領域もメモリとして働いてもらいます。

mem.c
  // if (GVRAM <= adr) {
  //   if (ini_info.io_through) return false;
  //   sprintf(message, "I/OポートorROM($%06X)から読み込もうとしました。", adr);
  //   err68(message);
  // }

DOSコールのMallocSetBlockの戻り値でメモリ確保失敗を表すのに戻り値の最上位バイトを0b10000010, 0b10000001と5bit無駄に空けてマークしているところを0b11000000, 0b10100000に詰めてしまいます。この詰めた5bitをありがたくメモリブロックのサイズとして使わせてもらいます。
DOSコールの戻り値なのでこのビットをちゃんと見ているアプリは動かなくなりますが、今回は問題ありません。

dos_memory.c
  if (maxSize > SIZEOF_MEMBLK) return 0xa0000000 + (maxSize - SIZEOF_MEMBLK);
  return 0xc0000000;  // 完全に確保不可

Mallocの引数をクリップしている処理もビット幅を合わせます。

dos_memory.c
static ULong correct_malloc_size(ULong size) {
  return (size > 0x1fffffff) ? 0x1fffffff : size;
}

Mallocの戻り値をマスクしてサイズを取得する際のマスク部分もすべてエラーマークのビットだけを落とすようにビット幅をそろえます。

run86.cなど
  ULong size = Malloc(MALLOC_FROM_LOWER, (ULong)-1, parent) & 0x1fffffff;

アドレスマスクのビット幅もあわせます。

m68k.h
#define ADDRESS_MASK 0x1fffffff

prog_readがメモリサイズの最上位バイトを利用してモードを渡しているところも改変してモード固定にしてしまいます。
本来はExecDOSコールがモードを渡せるようなケアをしてあげるべきですが今回は使わないのでこれでやっつけます。

load.c
  // loadmode = ((*prog_sz2 >> 24) & 0x03);
  // *prog_sz2 &= 0xFFFFFF;
  loadmode = 0;

以上で改造は終わりです。

run68xでの実行結果

改造も終わったので早速run68xでZig Stage1コンパイラを実行していきます。
今回はZigコンパイラが下記のエラーを出力して止まりました。
前回のrun68(run68mac)を利用していた時とはエラーの内容が変わっています。run68xではm68000エミュレーションの不具合修正も入っているようなのでその影響だろうと思います。
今回は一部のソースコードが読み込めていないようです。

/lib/std/math/big/int.zig:4201:17: error: unable to load '/lib/std/math/big/int_test.zig': SystemResources
/lib/std/std.zig:125:26: error: unable to load '/lib/std/meta.zig': SystemResources
/lib/std/std.zig:128:25: error: unable to load '/lib/std/net.zig': SystemResources
/lib/std/std.zig:131:24: error: unable to load '/lib/std/os.zig': SystemResources
/lib/std/std.zig:133:26: error: unable to load '/lib/std/once.zig': SystemResources
/lib/std/std.zig:138:25: error: unable to load '/lib/std/pdb.zig': SystemResources
/lib/std/std.zig:142:29: error: unable to load '/lib/std/process.zig': SystemResources
/lib/std/std.zig:145:26: error: unable to load '/lib/std/rand.zig': SystemResources
/lib/std/std.zig:148:26: error: unable to load '/lib/std/sort.zig': SystemResources
/lib/std/std.zig:151:26: error: unable to load '/lib/std/simd.zig': SystemResources
/lib/std/std.zig:154:27: error: unable to load '/lib/std/ascii.zig': SystemResources
/lib/std/std.zig:157:25: error: unable to load '/lib/std/tar.zig': SystemResources
/lib/std/std.zig:160:29: error: unable to load '/lib/std/testing.zig': SystemResources
/lib/std/std.zig:163:26: error: unable to load '/lib/std/time.zig': SystemResources
/lib/std/std.zig:166:24: error: unable to load '/lib/std/tz.zig': SystemResources
/lib/std/std.zig:169:29: error: unable to load '/lib/std/unicode.zig': SystemResources
/lib/std/std.zig:172:30: error: unable to load '/lib/std/valgrind.zig': SystemResources
/lib/std/std.zig:175:26: error: unable to load '/lib/std/wasm.zig': SystemResources
/lib/std/std.zig:178:25: error: unable to load '/lib/std/zig.zig': SystemResources
/lib/std/std.zig:179:27: error: unable to load '/lib/std/start.zig': SystemResources

調べてみるとまたもmomory.growの失敗に起因するメモリ破壊が原因でした。

  1. memory.grow失敗でマイナスのインデックスが使われる
  2. wasmの使うメモリ領域の前の領域のmallocの管理情報が破損する
  3. reallocでメモリ確保することができなくなりファイルパスの管理情報が更新できずソースコードの読み込みに失敗する

Heap領域を広げてもう一度実行してみると、今回はエラーを出力せずに最後まで実行できたようです。
半信半疑でディレクトリ内を確認してみると、下記のようにhello.zigから変換されたhello.cが見つかりました。

-rw-r--r--  1 icon icon   148691 Feb  4 08:47 hello.c
-rw-r--r--  1 icon icon       97 Jan 29 16:54 hello.zig

生成結果の確認

出力された生成物を確認していきます。
まず、下記のhello.zigがコンパイル前のソースです。

hello.zig
const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, world!\n", .{});
}

コンパイル結果のC言語のソースは長いので一部をピックアップして記載します。

先頭部分では各種構造体が定義されています、コメントから想像するとStage2コンパイラで使うことに特化したコードが出力されているのかもしれないです。

hello.c先頭
#define ZIG_TARGET_MAX_INT_ALIGNMENT 16
#include "zig.h"
struct anon__lazy_52 {
 uint8_t const *ptr;
 uintptr_t len;
};
struct Set__385; // target.Target.Cpu.Feature.Set
struct Set__385 {
 uintptr_t ints[5];
};
typedef struct anon__lazy_52 anon__498_38;
struct Model__378; // target.Target.Cpu.Model
struct Model__378 {
 struct anon__lazy_52 name;
 struct anon__lazy_52 llvm_name;
 struct Set__385 features;
};
struct Cpu__349; // target.Target.Cpu
struct Cpu__349 {
 uint8_t arch;
 struct Model__378 const *model;
 struct Set__385 features;
};

mainは機械的に生成されたコードらしい風貌です。

hello.c main関数
int main(int const a0, char **const a1, char **const a2) {
 uintptr_t t1;
 uintptr_t t0;
 char *t2;
 uint8_t **t4;
 uint8_t **t5;
 uint8_t **t16;
 uint8_t **const *t6;
 anon__136_43 t7;
 anon__136_43 t17;
 unsigned long t8;

元のメインもこんな感じで出力されています。

hello.c hello_main
static void hello_main__148(void) {
 debug_print__anon_3665__3665();
 return;
}

インラインアセンブラのコードも見受けられます。ターゲットがx86_linuxなのでX68000で動かすのは厳しそうです。

hello.c asm
static uintptr_t os_linux_x86_64_syscall3__4730(uint64_t const a0, uintptr_t const a1, uintptr_t const a2, uintptr_t const a3) {
 uintptr_t t0;
 uintptr_t t1;
 t0 = a0;
 register uintptr_t t2 __asm("rax");
 register uintptr_t const t3 __asm("rax") = t0;
 register uintptr_t const t4 __asm("rdi") = a1;
 register uintptr_t const t5 __asm("rsi") = a2;
 register uintptr_t const t6 __asm("rdx") = a3;
 __asm volatile("syscall": [ret]"=r"(t2): [number]"r"(t3), [arg1]"r"(t4), [arg2]"r"(t5), [arg3]"r"(t6): "rcx", "r11", "memory");
 t1 = t2;
 return t1;
}

コードの末尾にはHello worldの文字列があるのが確認できます。

hello.c末尾
static uint8_t const target_x86_cpu_x86_64__anon_507__507[7] = "x86_64";
static uint8_t const target_x86_cpu_x86_64__anon_508__508[7] = "x86-64";
static struct Sigaction__3636 const os_maybeIgnoreSigpipe__anon_3660__3660 = {{ .handler = &os_noopSigHandler__1978 },{UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0),UINT32_C(0)},0u,((void (*)(void))0x0ul)};
static uint8_t const os_maybeIgnoreSigpipe__anon_3662__3662[50] = "failed to install noop SIGPIPE handler with \'{s}\'";
static uint8_t const hello_main__anon_3664__3664[15] = "Hello, world!\n";
static uint8_t const debug_panicExtra__anon_3673__anon_4114__4114[16] = "(msg truncated)";
static uint8_t const fmt_ANY__anon_4408__4408[4] = "any";
static uint8_t const fmt_formatType__anon_4369__anon_4417__4417[8] = "{ ... }";
static uint8_t const debug_panicImpl__anon_4537__4537[18] = "thread {} panic: ";
static uint8_t const debug_panicImpl__anon_4539__4539[5] = "{s}\n";
static uint8_t const debug_panicImpl__anon_4541__4541[36] = "Panicked during a panic. Aborting.\n";
static uint8_t const debug_dumpStackTrace__anon_4608__4608[49] = "Unable to dump stack trace: debug info stripped\n";
static uint8_t const fmt_digits2__anon_4790__4790[201] = "00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899";

最後にWSL上でビルドして動かしてみます。

$ clang hello.c -I ../../../zig/lib/ -o hello
$ ./hello 
Hello, world!

動きましたね。遥かなるHello world!

実行中のHeap内のブロックを観察

今回何度もmemory.grow(realloc)の失敗に苦しめられてきました。
実行中、Heap内のメモリブロックがどのように確保されているのか観察してみたいと思います。
Zig Stage1コンパイラに下記コードを仕込んで、実行中のmallocのブロックをプリントできるようにしてみました。Heap領域の先頭を表す__HSTAからリンクをたどりながら各メモリブロックの情報をプリントするコードになっています。

struct MALLOC_TBL
{
    struct MALLOC_TBL *prev;
    void *size;
    struct MALLOC_TBL *next;
};
extern struct MALLOC_TBL *_HSTA;
static void printMallocTbl()
{
    struct MALLOC_TBL *current = _HSTA;
    while(current != NULL)
    {
        bool isEmpty = *(uint8_t*)&(current->size) == 0xfe;
        fprintf(stderr, "adr: %x; size: %x; raw: %x\n", current, isEmpty? 0 : current->size - (void*)current - 12, (uint32_t)current->size);
        current = current->next;
    }
}

実行中のHeap内の様子

下記は実行開始間もなく、最初のファイルアクセスでディレクトリ情報を格納する配列がreallocで拡張された直後の状態です。
realloc0x4b22fcのブロックが0x362410に移動したようです。
0x4b2404にあるひときわ大きなブロックがwasmのメモリになります。

実行開始直後
adr: 4b1ad4; size: 4; raw: 4b1ae4
adr: 4b1ae4; size: 400; raw: 4b1ef0
adr: 4b1ef0; size: 400; raw: 4b22fc
adr: 4b22fc; size: 0; raw: fe4b2368
adr: 4b2368; size: 2; raw: 4b2376
adr: 4b2376; size: 2; raw: 4b2384
adr: 4b2384; size: 7; raw: 4b2397
adr: 4b2398; size: 5; raw: 4b23a9
adr: 4b23aa; size: 11; raw: 4b23c7
adr: 4b23c8; size: 30; raw: 4b2404
adr: 4b2404; size: 3150000; raw: 3602410
adr: 3602410; size: 78; raw: 3602494

下記は少し時間が経過した後の状態です。今回のZig Stage1コンパイラに含まれるwasiインタフェースのコードはファイル管理情報を作成する際にpathの文字列をこまめにmallocして領域を確保し、実行終了までfreeで解放せずに保持しています。このため細かいサイズの領域が降り積もるように積み重なっている様子がわかります。
wasmのメモリはまだ同じ場所にいることがわかります。

実行後少し経過
adr: 4b1ad4; size: 4; raw: 4b1ae4
adr: 4b1ae4; size: 400; raw: 4b1ef0
adr: 4b1ef0; size: 400; raw: 4b22fc
adr: 4b22fc; size: 9; raw: 4b2311
adr: 4b2312; size: a; raw: 4b2328
adr: 4b2328; size: a; raw: 4b233e
adr: 4b233e; size: c; raw: 4b2356
adr: 4b2368; size: 2; raw: 4b2376
adr: 4b2376; size: 2; raw: 4b2384
adr: 4b2384; size: 7; raw: 4b2397
adr: 4b2398; size: 5; raw: 4b23a9
adr: 4b23aa; size: 11; raw: 4b23c7
adr: 4b23c8; size: 2a; raw: 4b23fe
adr: 4b2404; size: 3150000; raw: 3602410
adr: 3602410; size: 9; raw: 3602425
adr: 3602426; size: 0; raw: fe6024a2
adr: 36024a2; size: 11; raw: 36024bf
adr: 36024c4; size: 9; raw: 36024d9
adr: 36024da; size: 15; raw: 36024fb
adr: 36024fc; size: 36; raw: 360253e
adr: 360253e; size: 1d; raw: 3602567
adr: 3602568; size: 1f; raw: 3602593
adr: 36025e0; size: 400; raw: 36029ec
adr: 36029ec; size: 0; raw: fe602b18
adr: 3602b18; size: 400; raw: 3602f24
adr: 3602f24; size: 78; raw: 3602fa8
adr: 3602fa8; size: 138; raw: 36030ec

memory.grow発生直後の様子を下記に示します。memory.growで使っているreallocは連続した領域を確保するために現在のブロックの後ろに十分な空きがない場合は領域確保できる場所にブロックを移動して中のデータをコピーします。
下まで見ていくとwasmの巨大ブロックが一番後ろに移動しているのがわかります。
また、このブロックが元居た場所に0x4b2f80から0x3602410の区間に巨大な空き領域が出現しているのも確認できます。

memory.grow発生
adr: 4b1ad4; size: 4; raw: 4b1ae4
adr: 4b1ae4; size: 400; raw: 4b1ef0
adr: 4b1ef0; size: 400; raw: 4b22fc
adr: 4b22fc; size: 9; raw: 4b2311
adr: 4b2312; size: a; raw: 4b2328
adr: 4b2328; size: a; raw: 4b233e
adr: 4b233e; size: c; raw: 4b2356
adr: 4b2356; size: 0; raw: fe4b2367
adr: 4b2368; size: 2; raw: 4b2376
adr: 4b2376; size: 2; raw: 4b2384
adr: 4b2384; size: 7; raw: 4b2397
adr: 4b2398; size: 5; raw: 4b23a9
adr: 4b23aa; size: 11; raw: 4b23c7
adr: 4b23c8; size: 2a; raw: 4b23fe
adr: 4b2404; size: b70; raw: 4b2f80
adr: 3602410; size: 9; raw: 3602425
adr: 3602426; size: 2a; raw: 360245c
...170ブロック省略
adr: 3604f6a; size: 0; raw: fe605376
adr: 3605376; size: 3e0; raw: 3605762
adr: 3605d72; size: 0; raw: fe6068d6
adr: 36068d6; size: 4a70000; raw: 80768e2

その後、細かい文字列は先ほどの大穴の領域にブロックを確保します、そのためwasmのメモリブロックより後ろには新たなブロックが積もらない状態になります。
reallocは対象のブロックの後ろに十分な領域がある場合は移動せずにサイズだけを変更するためmemory.growが発生してもwasmのブロックは移動していないことがわかります。

その後のmemory.grow
adr: 4b1ad4; size: 4; raw: 4b1ae4
adr: 4b1ae4; size: 400; raw: 4b1ef0
...360ブロック省略
adr: 4b7164; size: 2a; raw: 4b719a
adr: 4b719a; size: 0; raw: fe4b75a6
adr: 4b7f00; size: 2130; raw: 4ba03c
adr: 3602410; size: 9; raw: 3602425
adr: 3602426; size: 2a; raw: 360245c
...170ブロック省略
adr: 3604f6a; size: 0; raw: fe605376
adr: 3605376; size: b20; raw: 3605ea2
adr: 36068d6; size: 7020000; raw: a6268e2

最後にプログラム終了時のメモリブロックの様子です。
wasmのメモリはかなり成長しましたが同じアドレスにいることがわかります。
細かいメモリブロックが大量に確保されていますが、かつてwasmメモリがいた領域は最後までかなりのサイズが残っていることがわかります。メモリの利用効率はあまりよくないですね。

プログラム終了時
adr: 4b1ad4; size: 4; raw: 4b1ae4
adr: 4b1ae4; size: 400; raw: 4b1ef0
...1445ブロック省略
adr: 4c6cd8; size: 21a8; raw: 4c8e8c
adr: 4c9a80; size: 64c8; raw: 4cff54
adr: 3602410; size: 9; raw: 3602425
adr: 3602426; size: 2a; raw: 360245c
...170ブロック省略
adr: 3604f6a; size: 0; raw: fe605376
adr: 3605376; size: 0; raw: fe6068d2
adr: 36068d6; size: a8b0000; raw: deb68e2

まとめ

改造版run68xを使うことでX68000向けにビルドしたZig Stage1コンパイラで、Zig言語のソースをC言語のソースに変換することができました。
また、mallocの確保したブロックをモニタするコードを埋め込むことでHeap領域の使い方を観察しました。結果、今回のようなケースではあまり効率の良いメモリの使い方はできていないことがわかりました。wasm2cでビルドしたアプリでこのような大量のファイルにアクセスするシナリオではメモリ確保の戦略に工夫が必要そうです。

脚注
  1. X68000上でZigコンパイラを動かすことに挑戦(動かない) ↩︎

Discussion