IchigoJam web のマシン語の実行のバグの解析


はじめに

IchigoJamはjig.jpの登録商標です。

去年のアドベントカレンダーで、IchigoJam web でマシン語の実行ができるようになったものの、
なぜかうまく動かないコードがありました。

IchigoJamで換字式暗号 - Qiita

そこで、今回はどのようなバグによってこの誤動作が引き起こされているのかを考えてみました。

バグ

以下のXorshiftのプログラムがうまく動かず、RUNするたびに違う数値列が出力されるという不都合が発生しました。

10 'Xorshift
20 POKE#700,#3F,#A3,#18,#68,#C1,#02,#48,#40
30 POKE#708,#59,#68,#19,#60,#99,#68,#59,#60
40 POKE#710,#D9,#68,#99,#60,#D9,#68,#CA,#0C
50 POKE#718,#51,#40,#02,#0A,#48,#40,#50,#40
60 POKE#720,#D8,#60,#7F,#21,#09,#02,#FF,#31
70 POKE#728,#08,#40,#70,#47
80 POKE#800,#15,#CD,#5B,#07,#E5,#55,#9A,#15
90 POKE#808,#B5,#3B,#12,#1F,#33,#13,#49,#05
100 FOR I=1 TO 20
110 PRINT USR(#700,0)
120 NEXT

そこで、とりあえずマシン語を実行する前後でXorshiftの状態を保存している配列の値がどう変わっているかをチェックしました。

10 'Xorshift
20 POKE#700,#3F,#A3,#18,#68,#C1,#02,#48,#40
30 POKE#708,#59,#68,#19,#60,#99,#68,#59,#60
40 POKE#710,#D9,#68,#99,#60,#D9,#68,#CA,#0C
50 POKE#718,#51,#40,#02,#0A,#48,#40,#50,#40
60 POKE#720,#D8,#60,#7F,#21,#09,#02,#FF,#31
70 POKE#728,#08,#40,#70,#47
80 POKE#800,#15,#CD,#5B,#07,#E5,#55,#9A,#15
90 POKE#808,#B5,#3B,#12,#1F,#33,#13,#49,#05
100 PRINT[0],[1],[2],[3],[4],[5],[6],[7]
110 PRINT USR(#700,0)
120 PRINT[0],[1],[2],[3],[4],[5],[6],[7]

実行結果は例えば以下になりました。

-13035 1883 21989 5530 15285 7954 4915 1353
21989 5530 15285 7954 -24133 -30309 4915 1353

ここから、以下のことがわかります。
ただし、配列の要素2個をペアにして「n番目」とカウントします。

  • 1番目と2番目の値は、正しくシフトされている。
  • 3番目の値は、正しくシフトされず、謎の値が入っている。
  • 4番目の値は、変わっていない。

比較のため、
IchigoJamでXorshift(疑似乱数) マシン語 vs BASIC - Qiita
に載せた(IchigoJam webでも正しく動く)BASIC版でも配列の値を見てみました。

10 'Xorshift (BASIC)
20 LET[0],#CD15,#075B,#55E5,#159A,#3BB5,#1F12,#1333,#0549
30 PRINT[0],[1],[2],[3],[4],[5],[6],[7]
40 T=[0]^([0]<<11):U=[1]^([1]<<11|[0]>>5&#7FF)
50 [0]=[2]:[1]=[3]
60 [2]=[4]:[3]=[5]
70 [4]=[6]:[5]=[7]
80 V=[6]^([7]>>3&#1FFF)^T^(U<<8|T>>8&#FF):W=[7]^U^(U>>8&#FF)
90 [6]=V:[7]=W
100 ?V&#7FFF
110 PRINT[0],[1],[2],[3],[4],[5],[6],[7]

実行結果は以下になりました。

-13035 1883 21989 5530 15285 7954 4915 1353
21989 5530 15285 7954 4915 1353 17898 -9053

1番目~3番目の値が正しくシフトされています。

メモリアクセスの調査

3番目の値を書き込む所までのマシン語のソースコードは、以下のようになっています。

R3 = PC + #40
' t = x ^ (x << 11)
R0 = [R3 + 0]L
R1 = R0 << 11
R0 ^= R1
' x = y
R1 = [R3 + 1]L
[R3 + 0]L = R1
' y = z
R1 = [R3 + 2]L
[R3 + 1]L = R1
' z = w
R1 = [R3 + 3]L
[R3 + 2]L = R1

1番目~3番目の値は、全て同じ Rd = [Rn + u5]L 命令を用いて読み込み、[Rn + u5]L = Rd 命令を用いて書き込んでいます。
とりあえず、読み込みが上手くいっていないのか書き込みが上手くいっていないのか切り分けたいです。
そこで、まずは読み込みをうまくいっている1番目のものにしてみます。
すなわち、

' z = w
R1 = [R3 + 3]L
[R3 + 2]L = R1

' z = w
R1 = [R3 + 1]L
[R3 + 2]L = R1

にします。なお、[R3 + 1]Lは2番目の値の処理で書き換え済みであり、新しい値になるはずです。

10 'Xorshift
20 POKE#700,#3F,#A3,#18,#68,#C1,#02,#48,#40
30 POKE#708,#59,#68,#19,#60,#99,#68,#59,#60
40 POKE#710,#59,#68,#99,#60,#D9,#68,#CA,#0C
50 POKE#718,#51,#40,#02,#0A,#48,#40,#50,#40
60 POKE#720,#D8,#60,#7F,#21,#09,#02,#FF,#31
70 POKE#728,#08,#40,#70,#47
80 POKE#800,#15,#CD,#5B,#07,#E5,#55,#9A,#15
90 POKE#808,#B5,#3B,#12,#1F,#33,#13,#49,#05
100 PRINT[0],[1],[2],[3],[4],[5],[6],[7]
110 PRINT USR(#700,0)
120 PRINT[0],[1],[2],[3],[4],[5],[6],[7]

実行結果は

-13035 1883 21989 5530 15285 7954 4915 1353
21989 5530 21989 5530 28199 -14600 4915 1353

あれ?3番目の値として書き込む値を変えたはずなのに、2番目の値が書き換わっています。
4番目の値が書き換わらないことも考えると…そもそも処理を行う位置全体がずれているかもしれません。

処理を行う位置がずれている…?

このマシン語のプログラムは #700 に配置し、R3 = PC + #40で配列の先頭 #800 のアドレスを取得する仕組みになっています。
きちんと取得できているか、確認してみましょう。
最近のバージョンでは、R1にRAMの#000相当のアドレスが渡されるので、このR3からR1を引けば#800になるはずです。
確かめてみましょう。

R3 = PC + #40
R0 = R3 - R1
RET

これをBASICに組み込みます。

10 POKE#700,#3F,#A3,#58,#1A
20 POKE#704,#70,#47
30 ?HEX$(USR(#700,0))

実行します。

あれれ…?4足りません。

実機でも実行してみます。

予想通り800と出力されました。

したがって、IchigoJam webのRd = PC + u8命令には、
格納される値が本来のものより4少なくなるバグがあることがわかりました。

workaround

バグがわかったので、回避方法を考えます。
PCに頼らずR1からRAMのアドレスを得る方法もありますが、これはIchigoJam 1.0.0では使えません。
そこで、プログラム中のアドレスを取得してメモリを読み込み、正しい値が読み込めているかで判定します。

具体的には、R3 = PC + #40のかわりに以下のプログラムを用います。

' プログラムの先頭から4バイト先のアドレスを取得
R3 = PC + 1
' 取得したアドレス+1のデータを読んでみる
R1 = [R3 + 1]
' IchigoJam webのバグにより、
' (4バイト先ではなく)プログラムの先頭のアドレスになっているかもしれない
R1 - #A3
IF 0 GOTO @BUGGY
' 4バイト先になっていれば、4を引いて先頭のアドレスにする
' (バグの影響を回避してアドレスを統一する)
R3 = R3 - 4
@BUGGY
' 得られたプログラムの先頭のアドレスに #100 を足して、配列の先頭のアドレスを得る
R3 += #80
R3 += #80

BASICに組み込みます。

10 'Xorshift
20 POKE#700,#00,#A3,#59,#78,#A3,#29,#00,#D0
30 POKE#708,#1B,#1F,#80,#33,#80,#33,#18,#68
40 POKE#710,#C1,#02,#48,#40,#59,#68,#19,#60
50 POKE#718,#99,#68,#59,#60,#D9,#68,#99,#60
60 POKE#720,#D9,#68,#CA,#0C,#51,#40,#02,#0A
70 POKE#728,#48,#40,#50,#40,#D8,#60,#7F,#21
80 POKE#730,#09,#02,#FF,#31,#08,#40,#70,#47
90 POKE#800,#15,#CD,#5B,#07,#E5,#55,#9A,#15
100 POKE#808,#B5,#3B,#12,#1F,#33,#13,#49,#05
110 FOR I=1 TO 20
120 PRINT USR(#700,0)
130 NEXT

実行します。

IchigoJam web by jig.jp

無事、IchigoJam webでも正しいXorshiftの値を得ることができました。
また、実行結果は省略しますが、実機でも正しいXorshiftの値を得ることができました。

結論

IchigoJam web でマシン語を使ったXorshiftの値が正しく求まらなかった原因は、
Rd = PC + u8命令のバグにより状態を格納しているとみなすアドレスが意図したものより4バイト手前にずれ、
意図と違う状態に初期化されてしまったことだということがわかりました。

プログラム中のアドレスを指定し、読み取った値を見てアドレスが正しいかを判定することで、
このバグの影響を回避し、実機とIchigoJam webの両方で正しいXorshiftの値を求めることができました。

しかし、今回はこれで正しい値が求まりましたが、IchigoJam webのバグはこれだけとは限りません。
またいつか新しいバグを踏んだら、また解析することになるかもしれないですね…