【发布时间】:2020-05-19 20:43:20
【问题描述】:
我正在尝试从 Modern x86 Assembly Language Programming 第 2 版中学习 x64 汇编语言的基础知识。
第 2 章中的第五个示例程序介绍了如何使用各种宽度参数执行简单的整数除法,并提供了一个简单的示例测试例程来展示结果。我遇到的问题是我似乎返回了一个错误的结果,并且我一生都无法理解为什么。
为例程提供的C++代码如下:
void UnsignedIntegerDiv(void)
{
uint8_t a = 12;
uint16_t b = 17;
uint32_t c = 71000000;
uint64_t d = 90000000000;
uint8_t e = 101;
uint16_t f = 37;
uint32_t g = 25;
uint64_t h = 5;
uint64_t quo1, rem1;
uint64_t quo2, rem2;
quo1 = (a + b + c + d) / (e + f + g + h);
rem1 = (a + b + c + d) % (e + f + g + h);
UnsignedIntegerDiv_(a, b, c, d, e, f, g, h, &quo2, &rem2);
cout << "\nResults for UnsignedIntegerDiv\n";
cout << "a = " << (unsigned)a << ", b = " << b << ", c = " << c << ' ';
cout << "d = " << d << ", e = " << (unsigned)e << ", f = " << f << ' ';
cout << "g = " << g << ", h = " << h << '\n';
cout << "quo1 = " << quo1 << ", rem1 = " << rem1 << '\n';
cout << "quo2 = " << quo2 << ", rem2 = " << rem2 << '\n';
}
对应的汇编程序是:
UnsignedIntegerDiv_ proc
; Calculate a + b + c + d
movzx rax,cl ;rax = zero_extend(a)
movzx rdx,dx ;rdx = zero_extend(b)
add rax,rdx ;rax = a + b
mov r8d,r8d ;r8 = zero_extend(c)
add r8,r9 ;r8 = c + d
add rax,r8 ;rax = a + b + c + d
xor rdx,rdx ;rdx:rax = a + b + c + d
; Calculate e + f + g + h
movzx r8,byte ptr [rsp+40] ;r8 = zero_extend(e)
movzx r9,word ptr [rsp+48] ;r9 = zero_extend(f)
add r8,r9 ;r8 = e + f
mov r10d,[rsp+56] ;r10 = zero_extend(g)
add r10,[rsp+64] ;r10 = g + h;
add r8,r10 ;r8 = e + f + g + h
jnz DivOK ;jump if divisor is not zero
xor eax,eax ;set error return code
jmp done
; Calculate (a + b + c + d) / (e + f + g + h)
DivOK: div r8 ;unsigned divide rdx:rax / r8
mov rcx,[rsp+72]
mov [rcx],rax ;save quotient
mov rcx,[rsp+80]
mov [rcx],rdx ;save remainder
mov eax,1 ;set success return code
Done: ret
虽然代码似乎有意义,但与程序集实现相比,我从例程的 C++ 实现中得到了不同的输出(C++ 实现给出了 quo1、rem1 的预期结果 - 它是不正确的 quo2、rem2 ):
Results for UnsignedIntegerDiv
a = 12, b = 17, c = 71000000 d = 90000000000, e = 101, f = 37 g = 25, h = 5
quo1 = 536136904, rem1 = 157
quo2 = 0, rem2 = 90071000029
这让我摸不着头脑。我的猜测是我在某处遗漏了一个错字(尽管我从书籍 GitHub 页面的可下载代码中得到了相同的结果)导致 ASM 部分中某处的溢出或截断。
非常感谢社区提供的任何帮助,因为我知道自己的知识不足以完全解决此问题。
【问题讨论】:
-
UnsignedIntegerDiv_的原型是什么?这决定了编译器对 C args 执行的隐式转换。 -
好地方。 'h' 的原型被设置为 uint32_t 而不是 uint64_t ...我已经盯着代码太久了,并设法完全错过了它。谢谢。
-
一种可行的调试技术是使用调试器单步执行,包括编译器生成的代码。从您自己的代码开始,但要单步执行并观察寄存器值。当您看到
add r10,[rsp+64]在r10的上半部分产生垃圾时,您会检查堆栈内存并看到只有低 4 个字节被正确设置,这将是一个正确方向的重要提示,以检查原因编译器没有清除您想要成为 64 位 arg 的高 4 字节。 -
另外,代码审查:尽可能选择 32 位操作数大小,以使机器代码更小。例如让对 64 位的隐式零扩展使用
movzx eax, cl或xor edx,edx完成其工作,就像您对g所做的那样。也更喜欢mov/movzx到不同的寄存器 so modern CPUs can optimize internally with mov-elimination,为执行端口保存一个 uop,并为该关键路径依赖链添加 0 而不是 1 个周期延迟。例如mov ecx, r8d而不是mov r8d,r8d -
不过,这很小;整体做得很好。没有浪费的指令,并且将 4 个值的总和作为 2 对 -> 1 final 有利于延迟 / OoO exec(指令级并行性)。分支布局:使无错误的快速路径不被采用。
jz到返回零的主要ret之后的 notOk 块。通常,该块根本不会到达,因此如果它不在同一个 i-cache 行中,则 CPU 永远不需要触及它。此外,将寄存器归零比将其设置为 1 便宜,因此您可以使0= 没有问题,因为调用者忽略了返回码...