首先,这是实模式 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(clang
和i386
架构),sizeof(Board)
是 20 并且w
是h
游戏板的尺寸,在 DOS 80 和 24 中运行的情况下(因为为标题/状态栏保留了一行)。为了调试这里发生的事情,我将malloc()
输出作为参数,并使用值 12 ( sizeof(board) + (-1) * sizeof(Slot)
?)
打印出来w
并h
显示正确的值,仍然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
内部删除。CFLAGS
libdos.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
它旨在对地址进行操作,并且只是被优化器“滥用”在这里进行数据计算。我的假设正确吗?如果是这样,这可能是clang
s 内部汇编程序中的错误-m16
吗?也许有人可以解释这个668D060C00
发出的clang
来自哪里,可能是什么意思?66
意思是“数据是 32 位的”,8D
可能是操作码本身 --- 但其余的呢?