Обратный инжиниринг asm с использованием sub / cmp / setbe обратно в C?Моя попытка компилируется в ветки - PullRequest
0 голосов
/ 04 марта 2019

это код на ассемблере, который я должен перевести: f1:

subl    $97, %edi
xorl    %eax, %eax
cmpb    $25, %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    $123, %edi
cmovgel %ecx, %eax
popq    %rbp
retq
.cfi_endproc

Мне было интересно, если это было правильно /что должно быть по-другому, и если кто-то может помочь объяснить, как работает jmps, я также пытаюсь перевести этот ассемблерный код и застрял f2:

cmpl    $1, %edi
jle .L6
movl    $2, %edx
movl    $1, %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    $1, %eax
jmp .L4

Ответы [ 2 ]

0 голосов
/ 04 марта 2019

Вот аннотированная версия сборки:

# %edi is the first argument, we denote x
subl $97, %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 $25, %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

.
0 голосов
/ 04 марта 2019

gcc8.3 -O3 выдает именно то, что задано в вопросе для этого способа записи проверки диапазона с использованием трюка без знака сравнения.

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 могут содержать произвольный мусор (соглашение о вызовах System V x86-64), но перенос только распространяется от низкого к высокому с помощью sub и add.

Младшие 8 битоврезультат (который читает cmp) будет таким же, как у sub $'a', %dil или sub $'a', %edi.

Запись в качестве обычной проверки диапазона также позволяет gcc выдавать идентичный код, потому что компиляторы знают, как оптимизировать проверку диапазона .(И gcc выбирает 32-битный размер операнда для sub, в отличие от clang, который использует 8-битный.)

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

В проводнике компилятора Godbolt , this и _v2 компилируются следующим образом:

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

Возвращая результат сравнения в виде целого числа, вместо использования if, гораздо более естественно соответствует asm.

Но даже запись этого «без ответвления» в C не будет соответствовать asm, если вы не включите оптимизацию.Код по умолчанию для gcc / clang: -O0: анти-оптимизация для согласованной отладки, хранения / перезагрузки всего в памяти между операторами.(И аргументы функции при входе в функцию.) Вам нужна оптимизация, потому что -O0 code-gen (намеренно) в основном мозговой мертвец , и выглядит неприятно.См. Как удалить «шум» из выходных данных сборки GCC / clang?

## gcc8.3 -O0
is_ascii_lowercase_v2:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    %edi, -20(%rbp)
    movl    -20(%rbp), %eax
    subl    $97, %eax
    movb    %al, -1(%rbp)
    cmpb    $25, -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

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

Мы можем сказать, что уровень оптимизации был по крайней мере gcc -O2-O1, gcc использует менее эффективный setbe / movzx вместо обнуления EAX перед setbe

is_ascii_lowercase_v2:
    subl    $97, %edi
    cmpb    $25, %dil
    setbe   %al
    movzbl  %al, %eax
    ret

Я никогда не смогу заставить clang воспроизвести точно такую ​​же последовательность инструкций.Ему нравится использовать add $-97, %edi и cmp с $26 / setb.

Или он будет делать действительно интересные (но неоптимальные) вещи вроде этого:

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

Так что это что-то, связанное с -(x-97), возможно, где-то там используется идентификатор дополнения 2 (-x = ~x + 1).

...