CS:APP示例中使用带有两个操作数的idivq?

12
我正在阅读《计算机系统:程序员的视角》(第三版),了解x86-64(以及汇编语言)。作者与网络上的其他来源一致,指出idivq只有一个操作数 - 就像这个声明的那样。但是,作者在后面的章节中给出了一个例子,使用指令idivq $9,%rcx

两个操作数?我最初认为这是一个错误,但在书中经常出现。

此外,被除数应该从寄存器%rdx(高位64位)和%rax(低位64位)中的数量中给出 - 因此,如果在架构中定义了这一点,则似乎不可能将第二个操作数作为指定的被除数。


这是一个关于IT技术的例子(我有点懒,不想把所有内容都写下来,所以用图片代替文字)。它声称在编译短C函数时,GCC会发出idivq $9, %rcx指令。


1
那是一个错误。只有imul具有多操作数和立即形式,而muldividiv没有。 - Peter Cordes
2
不。从技术上讲,它可能是一个初始化rdx:rax的宏,但不太可能。像这样的书总是有一个大而不完整的勘误表(https://csapp.cs.cmu.edu/3e/errata.html)。 - Hans Passant
3
在书中找到错误是很大的进步! - Weather Vane
2
@PeterCordes 啊是啊 - 没错。我的问题当然还没有解决,除非这在书中是个经常出现的错误。 - That Guy
4
据称,《深入理解计算机系统》中存在许多严重的AT&T语法错误,例如我成功猜出了函数的缺失部分,但gcc生成的汇编代码与答案不符,在这个例子中它表明(%rbx, %rdi, %rsi)(%rsi, %rsi, 9)是有效的寻址模式!(实际上比例因子是2位移位计数,所以这些是完全错误的,是作者严重缺乏理解的标志,而不是打字错误。) - Peter Cordes
显示剩余6条评论
3个回答

13
那是一个错误。只有imul有立即数和2个寄存器的形式。
乘法、除法或整数除法仍然只存在于8086引入的单操作数形式中,使用RDX:RAX作为隐式双宽度输出操作数(以及除法的输入)。
当然,根据操作数大小,也可以使用EDX:EAX、DX:AX或AH:AL。请参考像英特尔手册这样的ISA参考,而不是本书!https://www.felixcloutier.com/x86/idiv 还可以参见何时以及为什么要使用符号扩展并在mul/div中使用cdq?为什么在使用DIV指令之前EDX应该为0?

x86-64唯一的硬件除法指令是idivdiv。64位模式下移除了aam,该指令通过立即数进行8位除法(在Dividing in Assembler x86Displaying Time in Assembly中有使用aam的16位模式示例)。

当然,对于常量除法,idivdiv(以及aam)非常低效。除非你优化的是代码大小而不是性能,否则请使用移位来进行2的幂次方计算,或者使用乘法逆元。


CS:APP 3e Global Edition显然在实践问题中存在多个严重的x86-64指令集错误,声称GCC会发出不可能的指令。这不仅仅是拼写错误或微妙的错误,而是具有误导性的胡言乱语,对熟悉x86-64指令集的人非常明显地是错误的。这不仅仅是语法错误,而是试图使用不能编码的指令(除了扩展为多个指令的宏之外,没有语法可以表达它们。将idivq定义为使用宏的伪指令将非常奇怪)。
例如,我正确猜到了一个函数的缺失部分,但gcc生成的汇编代码与答案不符是另一个例子,其中它建议(%rbx, %rdi, %rsi)(%rsi, %rsi, 9)是有效的寻址模式!比例因子实际上是2位移位计数器,因此这些都是无用的,并且是作者对他们所教授的ISA的认识严重缺乏的迹象,而不是拼写错误。
他们的代码无法与任何AT&T语法汇编器配合使用。
另一个例子是这个x86-64 addq指令是什么意思,只有一个操作数?(来自CSAPP书3版),其中他们有一个荒谬的addq %eax而不是inc %rdx,并且在mov存储中有一个不匹配的操作数大小。
他们似乎只是编造了一些东西,并声称它是由GCC发出的。我不知道他们是否从真实的GCC输出开始,然后将其编辑成他们认为更好的示例,或者是从头开始手写而没有经过测试。
即使在-O0模式下,GCC的实际输出也会使用魔术常数(固定点乘法逆)进行乘法以除以9(但这显然不是调试模式代码)。他们本可以使用 -Os 模式。
想必他们不想谈论为什么GCC在实现整数除法时使用奇怪的数字进行乘法运算?并用自己虚构的指令替换了那个代码块。从上下文中,您可能可以确定他们期望输出结果的位置;也许他们的意思是 rcx /= 9

这些错误来自于全球版的第三方练习问题

来自出版商网站 (https://csapp.cs.cmu.edu/3e/errata.html)

关于全球版的说明: 不幸的是,出版商安排在全球版中生成了一组不同的练习和作业问题负责人做得不太好,因此这些问题及其解决方案存在许多错误。我们没有为这个版本创建勘误表。

所以CS:APP 3e可能是一本不错的教材,只要你获取北美版,或忽略练习/作业问题。这就解释了教科书的声誉和广泛使用与严重和明显的错误(对于熟悉x86-64汇编语言的人来说)之间的巨大脱节,这些错误超出了粗心大意的范畴,进入了不懂语言的领域。


如何设计一个假设的idiv reg, regidiv $imm, reg

此外,被除数应该来自寄存器%rdx(高64位)和%rax(低64位)中的数量 - 因此,如果在架构中定义了这一点,那么似乎不可能将第二个操作数指定为被除数。

如果英特尔或AMD引入了新的方便形式的dividiv他们会设计成使用单宽度被除数,因为编译器总是这样使用。

大多数语言都像C一样,隐式提升+ - * /的两个操作数到相同类型,并产生该宽度的结果。当然,如果已知输入是窄的,则可以进行优化。 (例如,使用一个imul r32实现a *(int64_t)b)。

但是,如果商溢出,dividiv会出错,因此在编译int32_t q =(int64_t)a /(int32_t)b时,不能使用单个32位idiv

编译器始终在DIV之前使用xor edx,edx或在IDIV之前使用cdqcqo来实际进行n / n => n位除法。

对于不仅是零或符号扩展的被除数的实际全宽度除法只能通过使用intrinsic或汇编手动完成(因为gcc / clang和其他编译器不知道何时优化是安全的),或在gcc助手函数中进行,例如在32位代码中执行64位/ 64位除法(或在64位代码中执行128位除法)。

因此,最有帮助的是一个避免使用额外指令来设置RDX的div / idiv,同时最小化隐含寄存器操作数的数量。就像imul r32,r/m32imul r32,r/m32,imm一样:以无隐式寄存器的方式使非扩展乘法的常见情况更加便捷。 这是Intel语法,与手册相同,目标首先。

最简单的方法是使用一个两个操作数的指令进行dst /= src。 或者可能用商和余数替换两个操作数。 使用适用于3个操作数的VEX编码,例如BMI1 andn,您可以使用以下方式:
idivx remainder_dst,dividend,divisor。 第二个操作数也是商的输出。或者您可以将余数写入RDX,并为商使用非破坏性目标。

或者更有可能针对仅需要商的简单情况进行优化,idivx quot, dividend, divisor并且不在任何地方存储余数。当您需要商时,您始终可以使用常规的idivBMI2 mulx使用隐式rdx输入操作数,因为它的目的是允许多个加法链与进位相加以进行扩展精度乘法。因此,它仍然必须产生2个输出。但是,这种假设的新形式的idiv将存在于节省代码大小和uops的正常使用idiv的周围,而这些使用并不是扩大的。因此,386 imul reg,reg / mem是比较的重点,而不是BMI2 mulx
我不知道引入idivx的立即形式是否有意义;出于代码大小的原因,您只会使用它。乘法逆元是除以常数的更有效方法,因此几乎没有实际用例可以使用这样的指令。

3
关于《计算机系统基础》全球版的说明:不幸的是,出版商为全球版生成了一组不同的实践和作业问题。负责此事的人没有做好工作,因此这些问题及其解答存在许多错误。我们没有针对这个版本创建勘误表。我认为很多错误来自这个版本。 - user6573388
@TomášKrál:啊,谢谢,这解释了很多问题。我听说这是一本好教材,而且显然很受欢迎,但与我们在SO问题中看到的疯狂错误的水平和严重性似乎不相容。 - Peter Cordes
顺便提一下,我发现如果你想要理解gcc的代码,通常需要使用-Os - Joshua
@Joshua:是的,根据您想要看到什么,它可能很有用。使用GCC可以避免乘法逆技巧,尽管这不是明智的大小与速度权衡。我曾经在如何从GCC / clang汇编输出中删除“噪音”?上回答过,但显然我之前没有提到-Os-Og。编辑以添加它们。 - Peter Cordes

2
我认为您的书有误。 idivq 只有一个操作数。如果我尝试汇编此代码片段:
idivq $9, %rcx

我收到了这个错误信息:
test.s: Assembler messages:
test.s:1: Error: operand type mismatch for `idiv'

这个有效:

idivq %rcx

但是你可能已经知道了。

它也可能是一个宏(不太可能,但有可能。 感谢@HansPassant)。

也许你应该联系这本书的作者,让他们添加勘误表中的条目。


1
感谢您的输入!关于宏注释,如果有提到,那肯定已经提到了。既然没有提到,那就好,我现在知道这是一个经常出现的错误 :) - That Guy

1
有趣的是,气体似乎允许以下内容:
mov $20, %rax
mov $0, %rdx
mov $5, %rcx
idivq %rcx, %rax
ret

这仍然在底层执行单操作数的除法,但它看起来像是二元形式。只要第一个操作数是寄存器并且第二个操作数特别是%rax,那么这将有效。然而,一般情况下idivq似乎要求单操作数形式。


idiv 的唯一机器编码是 idiv r/m(https://www.felixcloutier.com/x86/idiv),其中 RDX:RAX 操作数是隐含的。有趣(也有点奇怪)的是,GAS / AT&T 语法允许您可选地指定隐含操作数的一部分。它不会被视为可以扩展为 mov-immediate + idiv 的伪指令,因为 AT&T 语法不支持这样做,而且也没有 tmp 寄存器可以用于除数。 - Peter Cordes
与高级语言不同,允许的内容(部分)是由可编码成机器代码的内容决定的。这不像GAS可以决定允许任何其他东西。它不会编译,只是汇编。有一些先例将隐式操作数命名为文档,例如使用stosbstos %al,%es:(%rdi)(AT&T)/ stos BYTE PTR es:[rdi],al(Intel)。https://www.felixcloutier.com/x86/stos:stosb:stosw:stosd:stosq提到,一些汇编器可以用单个操作数表示操作数大小,留下AL / AX / EAX / RAX隐含。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接