4

首先,这是实模式 DOS .COM(独立式)自定义内存分配器的一种后续——如何调试?. 但是为了让它自成一体,这里是背景:

clang(而且gcc,也)有一个-m16开关,所以指令集中的指令i386前缀用于在“16位”实模式下执行。这可以被利用来.COM使用 GNU 链接器创建 DOS 32 位实模式可执行文件,如本博文中所述。(当然仍然限于微型内存模型,意味着所有内容都在一个 64KB 段中)想要玩这个,我创建了一个看起来工作得很好的最小运行时。

然后我尝试使用这个运行时构建我最近创建的基于 curses 的游戏,结果它崩溃了。我遇到的第一件事是经典的heisenbug:打印有问题的错误值使其正确。我找到了一个解决方法,只是为了面对下一次崩溃。所以我想到的第一件事是我的自定义malloc()实现,请参阅另一个问题。但由于到目前为止没有人发现它有什么问题,我决定再看看我的heisenbug。它体现在以下代码片段中(请注意,在为其他平台编译时这完美无缺):

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游戏板​​的尺寸,在 DOS 80 和 24 中运行的情况下(因为为标题/状态栏保留了一行)。为了调试这里发生的事情,我将malloc()输出作为参数,并使用值 12 ( sizeof(board) + (-1) * sizeof(Slot)?)

打印出来wh显示正确的值,仍然malloc()得到 12。打印输出size显示正确计算的大小,这一次,malloc()也得到了正确的值。所以,经典的 heisenbug

我发现的解决方法如下所示:

    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

如前所述,第二个变体按预期工作。我的问题毕竟这么长的文字很简单......为什么?我在这里想念的实模式有什么特别之处吗?

供参考:此提交包含两个代码版本。只需键入make -f libdos.mk具有解决方法的版本(稍后崩溃)。要编译导致错误的代码,请先从-DDOSREAL内部删除。CFLAGSlibdos.mk

更新:鉴于评论,我尝试更深入地调试这个自己。使用 dosbox 的调试器有点麻烦,但我终于在这个 bug 的位置上破解了它。因此,以下汇编代码旨在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它旨在对地址进行操作,并且只是被优化器“滥用”在这里进行数据计算。我的假设正确吗?如果是这样,这可能是clangs 内部汇编程序中的错误-m16吗?也许有人可以解释这个668D060C00发出的clang来自哪里,可能是什么意思?66意思是“数据是 32 位的”,8D可能是操作码本身 --- 但其余的呢?

4

1 回答 1

3

你的objdump输出是假的。看起来它是在假设 32 位地址和操作数大小而不是 16 位的情况下进行反汇编。所以它认为lea比它更快结束,并将一些地址字节反汇编为lds / add. 然后奇迹般地恢复同步,并看到一个movzww从 16b 延伸到 16b 的零……非常有趣。

我倾向于相信你的 DOSBOX 反汇编输出。它完美地解释了您观察到的行为(malloc 总是以 12 的 arg 调用)。你是对的,罪魁祸首是

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两个前缀。我认为这是leal 12(,%eax,8), %eax16 位模式的正确编码。

你的 DOSBOX 反汇编只有66 8D 0616 位0x0C绝对地址。(缺少 32 位地址大小前缀,并使用不同的寻址模式。)我不是 x86 二进制专家;我以前没有遇到过反汇编程序/指令编码的问题。(而且我通常只看 64 位 asm。)所以我必须查找不同寻址模式的编码。

x86 指令的首选来源是英特尔的英特尔® 64 和 IA-32 架构软件开发人员手册第 2 卷(2A、2B 和 2C):指令集参考,AZ。(从https://stackoverflow.com/tags/x86/info链接,顺便说一句。)

它说:(第 2.1.1 节)

操作数大小覆盖前缀允许程序在 16 位和 32 位操作数大小之间切换。任何一种尺寸都可以是默认值;使用前缀选择非默认大小。

所以这很简单,一切都与普通的 32 位保护模式几乎相同,除了 16 位操作数大小是默认值。

insn 描述有一个表格,LEA准确描述了 16、32 和 64 位地址(67H 前缀)和操作数大小(66H 前缀)的各种组合会发生什么。在所有情况下,当大小不匹配时,它会截断或零扩展结果,但它是英特尔 insn 参考手册,因此它必须单独列出每个案例。(这有助于更复杂的指令行为。)

lea是的,通过在非地址数据上使用它来“滥用”是一种常见且有用的优化。您可以对 2 个寄存器进行非破坏性相加,将结果放在第 3 个寄存器中。同时添加一个常数,并将其中一个输入缩放 2、4 或 8。因此它可以执行最多需要 4 条其他指令的操作。( mov / shl / add r,r / add r,i)。此外,它不会影响标志,如果您想为另一个跳跃或特别是cmov.

于 2015-08-07T09:51:25.460 回答