4

まず、これはリアルモード 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 (clangi386アーキテクチャ)、sizeof(Board)は 20 とwhゲーム ボードの寸法です。ここで何が起こっているかをデバッグするために、malloc()出力をそのパラメーターにしました。値 12 ( sizeof(board) + (-1) * sizeof(Slot)?)で呼び出されました。

印刷するwh正しい値が表示されましたが、それでも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からを削除します。CFLAGSlibdos.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おそらくオペコード自体です---しかし、残りはどうですか?

4

1 に答える 1

3

あなたのobjdump出力は偽物です。アドレスとオペランドのサイズが 16 ビットではなく 32 ビットであることを前提に逆アセンブルしているようleaですlds / add。そして、奇跡的に同期に戻り、movzwwゼロが 16b から 16b に広がっていることを確認します... かなり面白いです。

あなたの DOSBOX 逆アセンブル出力を信頼する傾向があります。観察された動作を完全に説明しています (malloc は常に 12 の引数で呼び出されます)。あなたが犯人であるというのは正しい

lea   eax,[000C]   ;  eax = 0x0C = 12.  Intel/MASM/NASM syntax
leal  12, %eax     #or AT&T syntax:

DOSBOXバイナリを組み立てたもののバグのように見えます(clang -m16あなたが言ったと思います) leal 12(,%eax,8), %eax

leal  12(,%eax,8), %eax  # AT&T
lea   eax, [12 + eax*8]  ; Intel/MASM/NASM syntax

おそらく、いくつかの命令エンコーディングテーブル/ドキュメントを掘り下げて、それがどのようにマシンコードにアセンブルされるlea べきかを正確に理解することができました. これは 32 ビット モードのエンコーディングと同じである必要がありますが、67 66プレフィックス (それぞれアドレス サイズとオペランド サイズ) が必要です。(いいえ、これらのプレフィックスの順序は問題ではなく、66 67機能します。)

あなたの DOSBOX と objdump の出力には同じバイナリさえないので、そうです、それらは異なって出力されました。(objdump は、以前の命令でオペランド サイズのプレフィックスを誤って解釈していますが、LEA までは insn の長さに影響しませんでした。)

GNUas .code16バイナリには があり67 66 8D 04 C5、次に 32 ビット0x0000000Cディスプレイスメント (リトル エンディアン) があります。これはLEA両方のプレフィックスを使用しています。これが 16 ビット モードの正しいエンコーディングだと思いleal 12(,%eax,8), %eaxます。

DOSBOX の逆アセンブルには66 8D 06、16 ビットの0x0C絶対アドレスを持つ , だけがあります。(32 ビット アドレス サイズのプレフィックスがなく、別のアドレッシング モードを使用しています。) 私は x86 バイナリの専門家ではありません。逆アセンブラや命令のエンコードで問題が発生したことはありません。(そして、私は通常 64 ビットの asm だけを見ます。) そのため、さまざまなアドレッシング モードのエンコーディングを調べる必要があります。

x86 命令のソースは、Intel の Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 2 (2A, 2B & 2C): Instruction Set Reference, AZです。( https://stackoverflow.com/tags/x86/infoからリンク、ところで。)

それは言う:(セクション2.1.1)

オペランド サイズ オーバーライド プレフィックスを使用すると、プログラムで 16 ビットと 32 ビットのオペランド サイズを切り替えることができます。どちらのサイズもデフォルトにすることができます。プレフィックスを使用すると、デフォルト以外のサイズが選択されます。

簡単です。16 ビットのオペランド サイズがデフォルトであることを除けば、すべてが通常の 32 ビット プロテクト モードとほとんど同じです。

insnのLEA説明には、16、32、および 64 ビット アドレス (67H プレフィックス) とオペランド サイズ (66H プレフィックス) のさまざまな組み合わせで何が起こるかを正確に説明する表があります。いずれの場合も、サイズの不一致がある場合は結果を切り捨てるかゼロ拡張しますが、これは Intel insn ref マニュアルであるため、すべてのケースを個別にレイアウトする必要があります。(これは、より複雑な命令の動作に役立ちます。)

leaはい、アドレス以外のデータで使用することによる「悪用」は、一般的で便利な最適化です。2 つのレジスタの非破壊的な加算を実行して、結果を 3 番目に配置できます。同時に、定数を追加し、入力の 1 つを 2、4、または 8 でスケーリングします。したがって、最大 4 つの他の命令を必要とすることを実行できます。( mov / shl / add r,r / add r,i)。また、フラグには影響しません。これは、別のジャンプや特にcmov.

于 2015-08-07T09:51:25.460 に答える