我参加了一个汇编课程的练习:


编写一个程序,该程序将三个数字x,y,z作为输入并返回:


如果(x + y + z)为偶数,则为0。
如果(x + y + z)为奇数,则为1。

(请注意,这里+表示算术加法)。无需实际
相加即可。提示:使用XOR指令。


此处的完整练习表:GitHub

这里是我编写的代码。请注意我为解释我的想法而添加的注释。

format PE console
entry start

include 'win32a.inc' 

; ===============================================
section '.text' code readable executable

start:

    call    read_hex              ; Provided by the teacher. Reads a hexadecimal number from stdin.
    mov     ebx,    eax
    call    read_hex
    add     ebx,    eax
    call    read_hex
    add     ebx,    eax         

    mov     eax,    ebx           ; Move the sum of the three given numbers into eax.
    xor     eax,    0x1           ; If the number is odd then the lowest bit becomes 0. If the number is even then the lowest bit becomes 1.
    ror     eax,    0x1           ; Rotate the lowest bit out of the register. So that the carry-flag gets a new state.
    jc      evenNumber            ; jc == Jump if the carry-flag has become 1. Means the last bit has been a 1, which in turn means that the number is even.
    mov     eax,    0x1           ; Otherwise just write 0 into eax, which signals an odd number. And prints this to stdout.
    jmp     print_result

evenNumber:
    mov     eax,    0x0

print_result:
    call    print_eax_binary    ; Provided by the teacher. Prints a binary number to stdout.

        ; Exit the process:
    push    0
    call    [ExitProcess]

include 'training.inc'


我不确定我是否正确地掌握了锻炼任务。但是,让我们假设必须对输入的三个数字求和,并且必须以某种方式使用xor。有更好的解决方案吗?

#1 楼

首先,您将获得漂亮,干净,格式合理且易于阅读的代码。您甚至包括注释,这些注释解释了每个指令的目标。我花太多时间查看汇编语言代码,这些都是出错的地方。您已经把它们全部正确了。不错的工作!现在,我不必费劲尝试阅读和理解您的代码。没有遵守规则。作业说您必须“不用实际加数字就可以做。”,但是,马上,您就可以将输入值组合在一起。而且我们开了一个很好的开端……:-(

现在,我个人认为这些类型的任务很愚蠢,所以我不会给他们。如何使用按位运算符,我会找到有用的东西,然后在现实世界中进行分配,这不像我需要非常努力的工作。芯片设计师没有把它们放在只是为了好玩。

那好吧;您必须完成分配给您的任务。因此,请遵循提示,使用ADD。也许您不确切知道这意味着什么,所以我要做的是打开程序员的计算器(所有主要的台式机操作系统的计算器都采用“程序员”模式)并进行尝试。选择正数和负数的随机组合,并比较将它们相加与异或时的结果。尝试了解XOR的功能。然后,查找XOR(异或)的正式定义。如果您像我一样,您的眼睛就会注视着象征性的逻辑知识(当我在大学上这门课程时,这还不是很好)。随意跳过此任务。您真正的目标是找出XOR在位级别上的实际作用。在线上有很多有关按位操作的详细说明,或者您可能也有一本涵盖该内容的教科书。

例如:

    01101101
XOR 11010100
_____________
    10111001


请注意,每列都遵循用于XOR运算的真值表,该运算基本上只是说只要输入不同,输出就为1(真)。那是OR的“排他性”部分。

最终,在进行这项初步研究时,我怀疑您会发现有人谈论XOR操作如何用于实现奇偶校验。桅杆的答案已经把豆子洒了。奇偶校验指示整数是偶数还是奇数,并且可以简单地通过位的XOR之和来计算。从代码中的注释来看,您已经知道对于一个二进制数,最低(最低有效)位决定它是奇数还是偶数。

所以好消息是您的代码几乎是完全正确。如果将XOR更改为ADD,它将遵循分配规则并产生正确的结果。因此,您将:

call    read_hex              ; Provided by the teacher. Reads a hexadecimal number from stdin.
mov     ebx,    eax
call    read_hex
xor     ebx,    eax
call    read_hex
xor     ebx,    eax    


现在,我们对三个输入的所有位进行了XOR求和。剩下的全部工作就是弄清楚结果的奇偶性,即XOR-sum是奇数还是偶数。

您已经实现了一种方法,但是正如Quuxplusone指出的那样是不必要的复杂方式。当您开始接触更高级的内容时,它并不总是成立,但对于简单的算术运算,更少的指令意味着更快的代码。可以说更重要的是,它意味着更简单的代码,而更可能是正确的代码。而且,更少的分支实际上总是意味着更快的代码,当然也可以是其执行流程更容易遵循并因此更易于调试的代码。

我在这里对Quuxplusone略有不同意见,说聪明完全是很好,只要您的聪明有一定的优势。您不必总是以“常规”方式编写代码,因为“常规”方式可能不是最优的。通常,如果我们拒绝编写汇编代码,那是因为我们想编写可能的最佳代码(最快,最短或用于判断“最佳”的任何度量),这意味着“正常”不一定是重要目标。有时,“可读性”甚至不是重要目标。但是,出于同样的原因,我确实同意,如果您的偏差较差,则偏离正常值是没有意义的,这里肯定是这种情况。

您的XOR和在XOR寄存器中。您知道XOR-sum会以最低有效位告诉您奇偶校验。那么,显而易见的事情是什么?屏蔽掉除了最低有效位以外的所有内容,这将是您的答案!我们如何掩盖位?使用逻辑AND运算:

and  ebx, 1   ; mask off all but the low-order bit


哦,最后,由于我们需要EBX中的结果,因此我们将这样做:

mov  eax, ebx


(我们可以像在EAX之前先完成MOV一样容易。这没关系。)

仔细阅读当前代码,并向自己证明这将产生完全相同的结果,而无需使用AND翻转位,无需通过XOR包含进位标志(CF),并且无需进行任何条件分支( ROR)。

将它们放在一起,代码本质上是:

几乎不可能对任何一段代码都这么说,但是我相信这确实是编写此代码的最佳方法。

评论


\ $ \ begingroup \ $
好的答案,但我认为在这种情况下提及“均等”是一种误导。奇偶校验通常是指一个数字的所有位的属性,而不是指其偶数,它仅是最低有效位的属性。
\ $ \ endgroup \ $
– Toby Speight
17年5月15日在11:13

\ $ \ begingroup \ $
通过将xor ebx,eax,mov eax,ebx替换为单一指令xor eax,ebx,您可以进一步紧凑。
\ $ \ endgroup \ $
–爱德华
17年5月15日在12:07

\ $ \ begingroup \ $
如果我们真的在尝试优化功能(显然是毫无意义的),我想说的是使用三个不同的寄存器来缩短依赖链,理论上可能会快一些(无论如何,这些寄存器都是可用的,为什么不使用它们)。
\ $ \ endgroup \ $
– Voo
17年5月15日在20:59

\ $ \ begingroup \ $
@TobySpeight,科迪根据其常见的数学定义使用“奇偶校验”,即数字是偶数还是奇数。这是该术语专门用作给定位集合中1位计数的奇数或奇数的简写的来源和基础。
\ $ \ endgroup \ $
– John Bollinger
17年5月15日在22:11

\ $ \ begingroup \ $
@Voo:额外的寄存器将如何提供帮助?对于三个输入,只有两个异或运算,并且它们必须相互依赖。使用4个或更多输入,您可以从(a ^ b)^(c ^ d)中获得一些ILP。但是,如果d比其他时间晚准备好,那么((a ^ b)^ c)^ d从d到结果的等待时间较短:1 xor而不是2。这是违反直觉的,但实际上这样做会更好天真的串行依赖关系链合并到单个累加器中,如果您要给它提供瓶颈,则每个时钟的结果为1,例如因为只能在1个执行单元上运行。
\ $ \ endgroup \ $
– Peter Cordes
17年5月16日在21:02

#2 楼

程序的开始显然应该在“ HINT”之后,并使用xor而不是add。 />
通常,如果您只是尝试测试寄存器的某些位,则使用test,而不是这个古怪的rorjc技巧。我的意思是这很聪明,但这并不正常-因此它不是很好的样式。只需执行正常的操作,请:

    ror     eax,    0x1           ; Rotate the lowest bit out of the register. So that the carry-flag gets a new state.
    jc      evenNumber            ; jc == Jump if the carry-flag has become 1. Means the last bit has been a 1, which in turn means that the number is even.
    mov     eax,    0x1           ; Otherwise just write 0 into eax, which signals an odd number. And prints this to stdout.
    jmp     print_result

evenNumber:
    mov     eax,    0x0

print_result:


这里,我给您一个聪明的办法,以弥补丢失的ror。 x86具有条件移动指令,并且mov尤其不会清除标志,这意味着您不需要在此处进行任何跳转:
    test    eax, 0x1
    jz      evenNumber


但最好的是:

    test    eax, 0x1
    mov     eax, 0x0   ; set eax to 0...
    cmovz   eax, 0x1   ; ...but immediately set it to 1 *if* the zero flag was set


评论


\ $ \ begingroup \ $
不幸的是,您的CMOV技巧无法按书面要求进行。条件移动指令不采用立即操作数,而仅采用寄存器。您将需要清除另一个已预先初始化为1的寄存器(或者,更好的是,将该预先初始化为0寄存器,以便可以使用xor reg,reg,然后在两者之间移动ax) TEST和CMOVNZ。)
\ $ \ endgroup \ $
–科迪·格雷
17年5月15日在7:39

\ $ \ begingroup \ $
甚至比您的“所有最佳”还好:eax +和eax为1(永远不要说“最佳” :-)
\ $ \ endgroup \ $
–科迪·格雷
17年5月15日在7:41

#3 楼

“但让我们假设必须对输入的三个数字求和,并且必须以某种方式使用异或。”再次阅读说明。明确不允许您实际对数字求和。可以按照建议使用XOR来完成。解决方案的第一部分使用add,从而使您的代码不合格。

实际解决方案的关键字将是检查奇偶校验。添加数字以知道总和的结果是奇数还是偶数: =偶数

是否对2、3或100个数字执行此操作都与结果的实际值无关。