x86 объяснение, количество аргументов функции и локальные переменные - PullRequest
2 голосов
/ 24 апреля 2020

ABI C для системы x86-64 выглядит следующим образом: Регистры rdi, rsi, rdx, rcx, r8, r9 используются для передачи аргументов в этом порядке. Стек используется для 7-го аргумента и далее. Возвращаемое значение использует регистр rax. Регистр rsp содержит указатель стека.

Сколько аргументов функции определено в функции удара bloop?

Я думаю, что есть только один аргумент функции, rdi. это правильно?

Сколько локальных переменных (не аргументов) объявлено в функции ниже bloop?

Я думаю, что локальной переменной нет. Это правильно?

0000000000001139 <bloop>:
    1139:       55                      push   %rbp
    113a:       48 89 e5                mov    %rsp,%rbp
    113d:       48 83 ec 10             sub    $0x10,%rsp
    1141:       48 89 7d f8             mov    %rdi,-0x8(%rbp)
    1145:       48 83 7d f8 29          cmpq   $0x29,-0x8(%rbp)
    114a:       7f 1b                   jg     1167 <bloop+0x2e>
    114c:       48 8b 05 dd 2e 00 00    mov    0x2edd(%rip),%rax
    1153:       48 89 c6                mov    %rax,%rsi
    1156:       48 8d 3d b5 0e 00 00    lea    0xeb5(%rip),%rdi
    115d:       b8 00 00 00 00          mov    $0x0,%eax
    1162:       e8 c9 fe ff ff          callq  1030 <printf@plt>
    1167:       90                      nop
    1168:       c9                      leaveq
    1169:       c3                      retq

Ответы [ 2 ]

2 голосов
/ 24 апреля 2020

Поскольку этот асм, очевидно, является выводом компилятора из антиоптимизированного режима отладки (уровень оптимизации по умолчанию -O0), вы можете предположить, что все аргументы регистров передаются в стек при входе функции . ( Почему clang производит неэффективный asm с -O0 (для этой простой суммы с плавающей запятой)? )

Так что да, это упрощает реверс-инжиниринг и исключает любые неиспользованные Аргументы функции или аргументы, которые передаются в printf в том же регистре, в котором они поступили.

Строковый nop, и использование инструкции leave означает, что это, вероятно, вывод G CC, в отличие от лязгать или я CC. Действительно актуально только для исключения возможности const int foo = 0x29; или чего-то такого, что G CC не оптимизирует на -O0. I CC и clang производят разные ассмы для источника, который получает G CC для создания этого асма . Я не проверял каждую версию компилятора, только последние версии этих компиляторов.

(Кроме того, это похоже на разбор исполняемой или разделяемой библиотеки P IE. Столбец адреса слева будет иметь более высокие адреса в традиционном зависимом от позиции исполняемом файле ELF, и компилятор использовал бы mov $imm32, %edi для помещения в регистр адреса stati c.)


Так что да, есть один 64- бит целое число / указатель arg (который, конечно, поступает в RDI) , и вызов printf передает значение глобальной или статической c 64-битной переменной, загруженной mov 0x2edd(%rip), %rsi, и адрес глобальной / Стати c форматная строка помещается в регистр с LEA.

И да, я не вижу местных жителей, если они не полностью не используются. На -O0, g cc будет оптимизировать int unused;, но не int foo = 123;. При наличии местных жителей даже register const compare = 0x29; получит G CC до subq $24, %rsp вместо 16 (0x10). (См. Ссылку на Godbolt ниже.) И на самом деле он не будет выполнять постоянное распространение.


Я могу получить GCC9.3 -O0 для получения именно этого asm из этого исходного кода:

#include <stdio.h>
long global_var;

void bloop(long x) {
    if (!(x>0x29))
        printf("%ld", global_var);
}

на Godbolt с gcc9.3 -O0 -fpie -fverbose-asm:

# godbolt strips out directives like .section .rodata
.LC0:
        .string "%ld"

bloop:
        pushq   %rbp  #
        movq    %rsp, %rbp      #,
        subq    $16, %rsp       #,
        movq    %rdi, -8(%rbp)  # x, x
        cmpq    $41, -8(%rbp)   #, x
        jg      .L3 #,
        movq    global_var(%rip), %rax  # global_var, global_var.0_1
        movq    %rax, %rsi      # global_var.0_1,
        leaq    .LC0(%rip), %rdi        #,
        movl    $0, %eax        #,
        call    printf@PLT      #
.L3:
        nop     
        leave   
        ret

У nop нет цели; Я не знаю, почему неоптимизированный вывод G CC иногда имеет его.

См. Также Как удалить "шум" из вывода сборки GCC / clang? для получения дополнительной информации о просмотре вывода компилятора .

0 голосов
/ 24 апреля 2020

Обе команды mov и nop являются инструкциями. Команда - это то, что исполняет процессор и составляет машинную программу. Если вы не знакомы с этой концепцией, может быть полезно прочитать учебное пособие по программированию на ассемблере.

Какие инструкции использует функция, в основном не зависит от количества аргументов и локальных переменных, которые она имеет. Наличие nop и некоторых mov инструкций ничего не говорит вам об аргументах и ​​переменных функции.

Что вам говорит, так это то, что операнды имеют эти инструкции. Если вы не знаете, что такое операнды или как инструкции x86 используют свои операнды, я должен еще раз попросить вас обратиться к учебнику, поскольку это выходит за рамки этого вопроса.

Общий подход к идентификации аргументов функции проверяет, какие регистры, сохраненные вызывающим абонентом, используются функцией без предварительного присвоения им значения. Хотя это не является надежным способом, обычно это лучший heuristi c.

В вашей функции используются сохраненные вызывающим регистры регистры rdi, rsi и rax , Из них только оригинальное значение rdi влияет на функцию. Что касается rsi и rax, функция перезаписывает их исходное значение, даже не глядя на него. Таким образом, они вряд ли будут аргументами функции (rax никогда не используется для аргумента функции в соглашении о вызовах SysV). Следовательно, функция, вероятно, имеет один аргумент в rdi. Я не вижу доступа к слотам стека, выделенным вызывающей стороной, поэтому маловероятно, что там будут скрыты какие-либо дополнительные аргументы.

Возможно, функция написана так, чтобы иметь аргументы в rsi или некоторые другие регистры и эти аргументы просто остались неиспользованными. Мы никогда не узнаем наверняка без дополнительной информации (например, символы отладки, дизассемблирование сайта вызова и т. Д. c.).

Что касается локальных переменных: в общем, нет способа восстановить, какие локальные переменные a C функция, используемая, когда она была скомпилирована в сборку, потому что компилятор может оптимизировать локальные переменные до такой степени, что их существование невозможно распознать. Также могут быть добавлены дополнительные локальные переменные для различных целей.

Однако в вашем конкретном случае c вполне вероятно, что функция была скомпилирована с отключенными оптимизациями. В этом случае многие C компиляторы компилируют код C очень простым и предсказуемым образом, где один слот стека выделяется для каждой локальной переменной, и каждый доступ к локальной памяти генерирует одну загрузку или память в этот слот стека.

Однако все еще невозможно с абсолютной уверенностью сказать, какие типы могли иметь эти переменные, или если два соседних слота стека являются двумя отдельными переменными, одной переменной особенно большого типа (например, long double) или переменной структуры или типа массива. Мы снова никогда не узнаем.

В вашем примере два слота стека по 8 байт каждый выделяются инструкцией sub $0x10, %rsp. Поскольку компилятор должен выделять слоты стека с шагом 16 байт для выравнивания, это означает, что исходная функция имеет как минимум одну переменную (64-битного типа), но может иметь целых девять (остальные - типа char) .

Поскольку доступ к одному из слотов стека (-0x8(%rbp)) заканчивается, мы можем только сказать, что функция имеет хотя бы одну переменную. Поскольку доступ осуществляется с шириной 64 бита, вполне вероятно, что указанная переменная имеет тип шириной 64 бита. Функция может иметь дополнительные неиспользуемые локальные переменные или ее переменная может быть структурой с несколькими членами или массивом, каждый из которых доступен только для первого члена. Мы не можем сказать наверняка.

Также возможно, что не существует локальной переменной, и компилятор решил использовать -0x8(%rbp), чтобы пролить какое-то выражение по какой-то причине (ему нравится делать бессмысленные разливы, подобные этой, когда оптимизация выключены), но это маловероятно.

Итак, подведем итог: как правило, невозможно судить по машинному коду точно, как выглядит функция C, но вы часто можете сделать обоснованное предположение, которое продвинет вас довольно далеко.

Следовательно, как правило, более полезно думать о том, «как может выглядеть функция C с этим машинным кодом?» а не «как выглядела функция C, которая генерировала этот машинный код?» как никогда нельзя быть уверенным.

...