まず、これはリアルモード DOS .COM 用のカスタム メモリ アロケータ (フリースタンディング)のフォローアップのようなものです — デバッグ方法は? . しかし、自己完結型にするために、背景は次のとおりです。
clang
(そしてgcc
、あまりにも)には-m16
スイッチがあるため、命令セットの長い命令はi386
「16ビット」リアルモードで実行するためにプレフィックスされます。このブログ投稿.COM
で説明されているように、GNU リンカを使用して DOS 32bit-realmode-executables を作成するためにこれを悪用できます。(もちろん、まだ小さなメモリ モデルに制限されています。1 つの 64KB セグメント内のすべてを意味します) これで遊んでみたくて、非常にうまく動作するように見える最小限のランタイムを作成しました。
次に、最近作成したcurses ベースのゲームをこのランタイムでビルドしようとしましたが、クラッシュしました。私が最初に遭遇したのは、古典的なheisenbug でした。問題のある間違った値を出力すると、正しくなりました。次のクラッシュに直面するためだけに、回避策を見つけました。したがって、私が念頭に置いていた最初の責任は、カスタムmalloc()
実装でした。他の質問を参照してください。しかし、これまでのところ本当に問題があることに誰も気付いていなかったので、ハイゼンバグをもう一度見てみることにしました。次のコード スニペットで明らかになります (他のプラットフォーム用にコンパイルした場合、これは問題なく機能したことに注意してください)。
typedef struct
{
Item it; /* this is an enum value ... */
Food *f; /* and this is an opaque pointer */
} Slot;
typedef struct board
{
Screen *screen;
int w, h;
Slot slots[1]; /* 1 element for C89 compatibility */
} Board;
[... *snip* ...]
size = sizeof(Board) + (size_t)(w*h-1) * sizeof(Slot);
self = malloc(size);
memset(self, 0, size);
sizeof(Slot)
は 8 (clang
とi386
アーキテクチャ)、sizeof(Board)
は 20 とw
はh
ゲーム ボードの寸法です。ここで何が起こっているかをデバッグするために、malloc()
出力をそのパラメーターにしました。値 12 ( sizeof(board) + (-1) * sizeof(Slot)
?)で呼び出されました。
印刷するw
とh
正しい値が表示されましたが、それでもmalloc()
12になりました。印刷するとsize
、正しく計算されたサイズが表示され、今回malloc()
も正しい値が得られました。だから、古典的なハイゼンバグ。
私が見つけた回避策は次のようになります。
size = sizeof(Board);
for (int i = 0; i < w*h-1; ++i) size += sizeof(Slot);
奇妙なことに、これはうまくいきました。次の論理的なステップ: 生成されたアセンブリを比較します。x86
ここで、私はまったく新しい6502
. したがって、次のスニペットでは、私の仮定と考えをコメントとして追加します。ここで修正してください。
最初に「壊れた」元のバージョン ( w
,h
は ,%esi
にあります%edi
):
movl %esi, %eax
imull %edi, %eax # ok, calculate the product w*h
leal 12(,%eax,8), %eax # multiply by 8 (sizeof(Slot)) and add
# 12 as an offset. Looks good because
# 12 = sizeof(Board) - sizeof(Slot)...
movzwl %ax, %ebp # just use 16bit because my size_t for
# realmode is "unsigned short"
movl %ebp, (%esp)
calll malloc
さて、私にはこれで良さそうに見えますが、malloc()
前述のように 12 と表示されます。ループを使用した回避策は、次のアセンブリにコンパイルされます。
movl %edi, %ecx
imull %esi, %ecx # ok, w*h again.
leal -1(%ecx), %edx # edx = ecx-1? loop-end condition?
movw $20, %ax # sizeof(Board)
testl %edx, %edx # I guess that sets just some flags in
# order to check whether (w*h-1) is <= 0?
jle .LBB0_5
leal 65548(,%ecx,8), %eax # This seems to be the loop body
# condensed to a single instruction.
# 65548 = 65536 (0x10000) + 12. So
# there is our offset of 12 again (for
# 16bit). The rest is the same ...
.LBB0_5:
movzwl %ax, %ebp # use bottom 16 bits
movl %ebp, (%esp)
calll malloc
前述のように、この 2 番目のバリアントは期待どおりに機能します。結局のところ、私の質問は、この長いテキストと同じくらい単純です...なぜですか? ここで欠けているリアルモードについて何か特別なことはありますか?
参考までに、このコミットには両方のコード バージョンが含まれています。回避策のあるバージョンを入力するだけmake -f libdos.mk
です (後でクラッシュします)。バグの原因となるコードをコンパイルするには、最初に-DDOSREAL
からを削除します。CFLAGS
libdos.mk
更新:コメントを考慮して、私はこれを自分でもう少し深くデバッグしようとしました。dosboxのデバッガを使うのはちょっと面倒ですが、やっとこのバグの位置で壊れるようになりました。したがって、次のアセンブリ コードは によって意図されていclang
ます。
movl %esi, %eax
imull %edi, %eax
leal 12(,%eax,8), %eax
movzwl %ax, %ebp
movl %ebp, (%esp)
calll malloc
最終的には次のようになります (dosbox の逆アセンブラーで使用される intel 構文に注意してください):
0193:2839 6689F0 mov eax,esi
0193:283C 660FAFC7 imul eax,edi
0193:2840 668D060C00 lea eax,[000C] ds:[000C]=0000F000
0193:2845 660FB7E8 movzx ebp,ax
0193:2849 6766892C24 mov [esp],ebp ss:[FFB2]=00007B5C
0193:284E 66E8401D0000 call 4594 ($+1d40)
この命令は疑わしいと思います。lea
実際、その後、間違った値が に含まれていax
ます。そこで、同じアセンブリ ソースを GNU アセンブラーにフィードしてみまし.code16
た。次の結果が得られました ( objdump
.
00000000 <.text>:
0: 66 89 f0 mov %si,%ax
3: 66 0f af c7 imul %di,%ax
7: 67 66 8d 04 lea (%si),%ax
b: c5 0c 00 lds (%eax,%eax,1),%ecx
e: 00 00 add %al,(%eax)
10: 66 0f b7 e8 movzww %ax,%bp
14: 67 66 89 2c mov %bp,(%si)
唯一の違いは、このlea
指示です。67
ここでは16ビットリアルモードで「アドレスは32ビット」という意味から始まります。私の推測では、アドレスを操作するためのものであり、ここでデータ計算を行うためにオプティマイザによって単に「悪用」されるため、これは実際に必要です。lea
私の仮定は正しいですか?もしそうなら、これはclang
の内部アセンブラのバグ-m16
でしょうか? 多分誰かがこれがどこから668D060C00
放出されたのclang
か、そしてその意味を説明できるでしょうか? 66
「データは32ビット」を意味し、8D
おそらくオペコード自体です---しかし、残りはどうですか?