使用 sub / cmp / setbe 将 asm 逆向工程返回到 C?我的尝试是编译到分支

Reverse-engineering asm using sub / cmp / setbe back to C? My attempt is compiling to branches

这是我要翻译的汇编代码: f1:

subl    , %edi
xorl    %eax, %eax
cmpb    , %dil
setbe   %al
ret

这是我写的我认为等效的 c 代码。

int f1(int y){

  int x = y-97;
  int i = 0;

  if(x<=25){
    x = i;
  }
  return x;
}

下面是我编译 C 代码的结果。

_f1: ## @f1

.cfi_startproc

%bb.0:

pushq   %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq    %rsp, %rbp
.cfi_def_cfa_register %rbp
                  ## kill: def %edi killed %edi def %rdi
leal    -97(%rdi), %ecx
xorl    %eax, %eax
cmpl    3, %edi
cmovgel %ecx, %eax
popq    %rbp
retq
.cfi_endproc

我想知道这是否正确/应该有什么不同,是否有人可以帮助解释 jmps 是如何工作的,因为我也在尝试翻译此汇编代码并被卡住了 f2:

cmpl    , %edi
jle .L6
movl    , %edx
movl    , %eax
jmp .L5

.L8:

movl    %ecx, %edx

.L5:

imull   %edx, %eax
leal    1(%rdx), %ecx
cmpl    %eax, %edi
jg  .L8

.L4:

cmpl    %edi, %eax
sete    %al
movzbl  %al, %eax
ret

.L6:

movl    , %eax
jmp .L4

gcc8.3 -O3 使用无符号比较技巧以这种方式编写范围检查的问题中准确地发出了 asm。

int is_ascii_lowercase_v2(int y){
    unsigned char x = y-'a';
    return x <= (unsigned)('z'-'a');
}

缩小到 8 位 之后 int 减法更精确地匹配 asm,但这不是正确性所必需的,甚至说服编译器使用 32-位sub。对于 unsigned char y,RDI 的高位字节允许存放任意垃圾(x86-64 System V 调用约定),但进位仅通过 sub 和 add 从低到高传播。

结果的低 8 位(即所有 cmp 读取)与 sub $'a', %dilsub $'a', %edi 相同。

将其编写为正常的范围检查也会使 gcc 发出相同的代码,因为编译器知道如何优化范围检查。 (并且 gcc 选择对 sub 使用 32 位操作数大小,这与使用 8 位的 clang 不同。)

int is_ascii_lowercase_v3(char y){
    return (y>='a' && y<='z');
}

On the Godbolt compiler explorer,这与_v2编译如下:

## gcc8.3 -O3
is_ascii_lowercase_v3:    # and _v2 is identical
    subl    , %edi
    xorl    %eax, %eax
    cmpb    , %dil
    setbe   %al
    ret

将比较结果作为整数返回,而不是使用 if,更自然地匹配 asm.

但即使在 C 中编写 "branchlessly" 也不会匹配 asm,除非您启用优化。来自 gcc/clang 的默认代码生成是 -O0:针对一致调试的反优化,storing/reloading 语句之间的所有内容都存储在内存中。 (以及函数入口的函数参数。)你需要优化,因为 -O0 code-gen(故意)大部分是脑残,而且看起来很讨厌。参见

## gcc8.3 -O0
is_ascii_lowercase_v2:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    %edi, -20(%rbp)
    movl    -20(%rbp), %eax
    subl    , %eax
    movb    %al, -1(%rbp)
    cmpb    , -1(%rbp)
    setbe   %al
    movzbl  %al, %eax
    popq    %rbp
    ret

启用优化的 gcc 和 clang 将在有效时将 if 转换为无分支代码。例如

int is_ascii_lowercase_branchy(char y){
    unsigned char x = y-'a';
    if (x < 25U) { 
        return 1;
    }
    return 0;
}

仍然编译为与 GCC8.3 -O3 相同的 asm

is_ascii_lowercase_branchy:
    subl    , %edi
    xorl    %eax, %eax
    cmpb    , %dil
    setbe   %al
    ret

我们可以看出优化级别至少为 gcc -O2。在 -O1,gcc 在 setbe

之前使用效率较低的 setbe / movzx 而不是异或清零 EAX
is_ascii_lowercase_v2:
    subl    , %edi
    cmpb    , %dil
    setbe   %al
    movzbl  %al, %eax
    ret

我永远无法让 clang 重现完全相同的指令序列。它喜欢使用 add $-97, %edi,并且 cmp 与 </code> / <code>setb.

或者它会像这样做非常有趣(但次优)的事情:

# clang7.0 -O3
is_ascii_lowercase_v2:
    addl    9, %edi    # 256-97 = 8-bit version of -97
    andl    4, %edi    # 0xFE; I haven't figured out why it's clearing the low bit as well as the high bits
    xorl    %eax, %eax
    cmpl    , %edi
    setb    %al
    retq

所以这涉及 -(x-97),可能在某处使用 2 的补码标识 (-x = ~x + 1)。

这是程序集的注释版本:

# %edi is the first argument, we denote x
subl , %edi
# x -= 97

# %eax is the return value, we denote y
xorl %eax, %eax
# y = 0

# %dil is the least significant byte (lsb) of x
cmpb , %dil

# %al is lsb(y) which is already zeroed
setbe %al
# if lsb(x) <= 25 then lsb(y) = 1
# setbe is unsigned version, setle would be signed

ret
# return y

所以一个冗长的 C 等价物是:

int f(int x) {
  int y = 0;
  x -= 97;
  x &= 0xFF; // x = lsb(x) using 0xFF as a bitmask
  y = (unsigned)x <= 25; // Section 6.5.8 of C standard: comparisons yield 0 or 1
  return y;
}

我们可以通过意识到 y 是不必要的来缩短它:

int f(int x) {
  x -= 97;
  x &= 0xFF;
  return (unsigned)x <= 25;
}

此程序集与 Godbolt Compiler Explorer (x86-64 gcc8.2 -O2) 完全匹配:https://godbolt.org/z/fQ0LVR