【问题标题】:What's the purpose of looping "xorl %edx,%eax; shrl $1,%edx"?循环“xorl %edx,%eax; shrl $1,%edx”的目的是什么?
【发布时间】:2016-12-17 14:37:31
【问题描述】:

我有以下 x86 汇编代码:

  movl   8(%ebp), %edx  //get an argument from the caller
  movl   $0, %eax
  testl  %edx, %edx
  je     .L1            
.L2:                   // what's the purpose of this loop body?
  xorl   %edx, %eax
  shrl   $1, %edx
  jne    .L2
.L1:
  andl   $1, %eax

教科书给出的对应C代码如下

int f1(unsigned x)
{
    int y = 0;
    while(x != 0) {
        __________;
    }
    return __________;
 }

本书要求读者填空并回答“它有什么作用?”的问题

我无法在一个 C 表达式中组合循环体。我可以说出循环体的作用,但我不知道它的用途。教科书上还说 %eax 这里存放的是返回值。那么……这样做的目的是什么

andl  $1, %eax

我也不知道。

【问题讨论】:

  • xorl 是异或 (^) 操作,而 shrl 是右移 (>>)...
  • @ChrisDodd:我认为 OP 知道这一点,但没有将这些部分放在一起,只查看循环更新的寄存器的低位。
  • @PeterCordes 如果原始发布者知道他将能够确定等效的 C 表达式。您实际上不需要知道代码填充空白的目的。
  • @RossRidge:我认为他们一直在尝试使用单个 C 表达式而不是 y ^= x; x>>=1; 一行。或者我是否还遗漏了一些用 C 表达的单一表达式方式(即不是逗号运算符或单独的语句。)当然,我赞成的原因是试图了解函数 的作用,不仅仅是如何解决填空作业反编译问题。

标签: assembly x86 reverse-engineering decompiling att


【解决方案1】:

看起来整个循环的目的是将 32 位 arg 中的所有位异或。即计算parity

从最后一条指令 (and $1,%eax) 开始,我们知道只有结果的低位很重要。

考虑到这一点,xor %edx,%eax 变得更加清晰:将%edx 的当前低位异或到%eax。高垃圾无所谓。

shr 循环直到x 的所有位都被移出。我们总是可以循环 32 次以获取所有位,但这比在 x 为 0 时停止效率要低。(由于 XOR 的工作原理,我们不需要对 0 位进行实际的 XOR;那没有效果。)


一旦我们知道函数的作用,填写 C 就变成了巧妙/紧凑的 C 语法的练习。起初我认为y ^= (x>>=1); 会适合循环,但在第一次使用它之前,x 会发生变化。

我看到在一个 C 语句中执行此操作的唯一方法是使用 , 运算符(它确实引入了 sequence point,因此在左侧阅读 x 并在右侧修改它是安全的,)。所以,y ^= x, x>>=1; 适合。

或者,为了更易读的代码,只是作弊并将两个语句与;放在同一行。

int f1(unsigned x) {
    int y = 0;
    while(x != 0) {
        y ^= x;  x>>=1;      
    }
    return y & 1;
 }

使用gcc5.3 -O3 on the Godbolt compiler explorer,编译为与问题中所示基本相同的asm。问题的代码de-optimizes the xor-zeroing idiom 改为mov $0, %eax,并优化了gcc 对ret 指令的愚蠢重复。 (或者可能使用了没有这样做的早期版本的 gcc。)


循环非常低效:这是一种有效的方式:

我们不需要复杂度为 O(n) 的循环(其中 n 是 x 的位宽度)。相反,我们可以获得 O(log2(n)) 复杂度,并且实际上利用 x86 技巧只执行前 2 步。

对于由寄存器确定的指令,我省略了操作数大小的后缀。 (xorw 使 16 位异或显式除外。)

#untested
parity:
    # no frame-pointer boilerplate

    xor       %eax,%eax        # zero eax (so the upper 24 bits of the int return value are zeroed).  And yes, this is more efficient than mov $0, %eax
                               # so when we set %al later, the whole of %eax will be good.

    movzwl    4(%esp), %edx      # load low 16 bits of `x`.  (zero-extend into the full %edx is for efficiency.  movw 4(%esp), %dx would work too.
    xorw      6(%esp), %dx       # xor the high 16 bits of `x`
    # Two loads instead of a load + copy + shift is probably a win, because cache is fast.
    xor       %dh, %dl           # xor the two 8 bit halves, setting PF according to the result
    setnp      %al               # get the inverse of the CPU's parity flag.  Remember that the rest of %eax is already zero, so the result is already zero-extended to 32-bits (int return value)
    ret

是的,没错,x86 has a parity flag (PF) 是从“根据结果设置标志”的每条指令的结果的低 8 位更新的,例如 xor

我们使用np 条件,因为PF = 1 表示偶校验:所有位的异或 = 0。我们需要逆返回 0 以获得偶校验。

为了利用它,我们通过将高半部分降低到低半部分并组合,重复两次以将 32 位减少到 8 位来进行 SIMD 样式的水平缩减。

在设置标志的指令之前将 eax 归零(使用 xor)比设置标志/setp %al/movzbl %al, %eax 更有效,正如我在What is the best way to set a register to zero in x86 assembly: xor, mov or and? 中解释的那样。


或者,正如@EOF所指出的,如果CPUID为POPCNT feature bit is set,则可以使用popcnt并测试低位以查看设置的位数是偶数还是奇数。 (另一种看待这个问题的方式:xor 是无进位的加法运算,因此无论是对所有位进行异或或将所有位水平相加,低位都是相同的)。

GNU C 也有__builtin_parity__builtin_popcnt,如果你告诉编译器编译目标支持它(使用-march=...-mpopcnt),它们使用硬件指令,但否则编译为一个有效的序列目标机。 Intel 内部函数始终编译为机器指令,而不是回退序列,并且在没有适当的 -mpopcnt 目标选项的情况下使用它们是编译时错误。

不幸的是,gcc 没有将纯 C 循环识别为奇偶校验计算并将其优化为这一点。一些编译器(如 clang 和可能的 gcc)可以识别某些类型的 popcount 惯用语,并将它们优化为 popcnt 指令,但在这种情况下不会发生这种模式识别。 :(

See these on godbolt.

int parity_gnuc(unsigned x) {
    return  __builtin_parity(x);
}
    # with -mpopcnt, compiles the same as below
    # without popcnt, compiles to the same upper/lower half XOR algorithm I used, and a setnp
    # using one load and mov/shift for the 32->16 step, and still %dh, %dl for the 16->8 step.

#ifdef __POPCNT__
#include <immintrin.h>
int parity_popcnt(unsigned x) {
    return  _mm_popcnt_u32(x) & 1;
}
#endif

    # gcc does compile this to the optimal code:
    popcnt    4(%esp), %eax
    and       $1, %eax
    ret

另请参阅 标签 wiki 中的其他链接。

【讨论】:

  • 如果您能够更多地修改代码,可以用更简单的(少 1 个“语句”)C++ 源代码int y = x; while (x&gt;&gt;=1) { y ^= x; } return y&amp;1;(同时坚持原来的愚蠢方式)通过对每一位进行异或计算奇偶校验)......但奇怪的是,根据godbolt,它确实产生了一点点更糟糕的机器代码(你的版本多了一个mov eax,edi)。不想在这里讨论它,但为什么我总是对编译器有更多期望呢? :/ 就像为什么它不通过移动函数开始来对齐循环,而是将 NOP 放在 fn 主体中?我不明白。 :D
  • 但是优化后的方法返回的是相反的值? PF 在位为偶数时设置,而原始函数为偶数位返回 0,如果我没有做错的话。顺便说一句,直到今天我才知道 PF 只受低 8 位的影响......(在十年内编写了超过 1+MB 的 x86 ASM 源之后)......学习过程从未停止......:D跨度>
  • popcount + and.
  • 好吧,既然你提到了程序集等价物,我认为至少对于 GCC 等人来说,还值得一提的是编译器特定的扩展,例如 __builtin_parity 和/或 __builtin_popcount。顺便说一句,答案很好。
  • @Ped7g:感谢优化版本的错误修复。一个好的高效算法比查找 PF 的感觉以避免反转答案等烦人的并发症有趣得多@。在报告了一些未优化的错误之后,我意识到编译器并不“聪明”,它们只是非常复杂但仍然是愚蠢的机器。我同意编译器并不聪明令人失望,但快速编译时间排除的不仅仅是多项式复杂性。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-03-29
  • 1970-01-01
  • 1970-01-01
  • 2014-06-20
  • 2012-12-10
  • 2015-06-29
  • 1970-01-01
相关资源
最近更新 更多