Подтвердить что ты не робот

Помогите с пониманием очень простой основной() разборки в GDB

Heyo,

Я написал эту основную основную функцию, чтобы поэкспериментировать с разборкой, а также увидеть и, надеюсь, понять, что происходит на более низком уровне:

int main() {
  return 6;
}

Использование gdb для disas main дает следующее:

0x08048374 <main+0>:    lea    0x4(%esp),%ecx
0x08048378 <main+4>:    and    $0xfffffff0,%esp
0x0804837b <main+7>:    pushl  -0x4(%ecx)
0x0804837e <main+10>:   push   %ebp
0x0804837f <main+11>:   mov    %esp,%ebp
0x08048381 <main+13>:   push   %ecx
0x08048382 <main+14>:   mov    $0x6,%eax
0x08048387 <main+19>:   pop    %ecx
0x08048388 <main+20>:   pop    %ebp
0x08048389 <main+21>:   lea    -0x4(%ecx),%esp
0x0804838c <main+24>:   ret  

Вот мое лучшее предположение о том, что я думаю, что происходит, и о том, что мне нужно с помощью строки за строкой:

lea 0x4(%esp),%ecx

Загрузите адрес esp + 4 в ecx. Почему мы добавляем 4 к esp?

Я где-то читал, что это адрес аргументов командной строки. Но когда я сделал x/d $ecx, я получил значение argc. Где хранятся фактические значения аргументов командной строки?

and $0xfffffff0,%esp

Выровнять стек

pushl -0x4(%ecx)

Нажмите адрес, где esp был первоначально указан в стеке. Какова цель этого?

push %ebp

Нажмите базовый указатель на стек

mov %esp,%ebp

Переместите указатель текущего стека в указатель базы

push %ecx

Введите адрес оригинала esp + 4 в стек. Почему?

mov $0x6,%eax

Я хотел вернуть сюда 6, поэтому я предполагаю, что возвращаемое значение сохраняется в eax?

pop %ecx

Восстановить ecx до значения, которое находится в стеке. Почему мы хотим, чтобы ecx был esp + 4, когда мы возвращаемся?

pop %ebp

Восстановить значение ebp для значения, которое находится в стеке

lea -0x4(%ecx),%esp

Восстановить исходное значение esp

ret

Я n00b, когда дело доходит до сборки, поэтому любая помощь будет отличной! Также, если вы видите какие-либо ложные утверждения о том, что я думаю, пожалуйста, исправьте меня.

Спасибо!:]

4b9b3361

Ответ 1

Рамки стека

Код в начале тела функции:

push  %ebp
mov   %esp, %ebp

заключается в создании так называемого фрейма стека, который является "твердой основой" для ссылки на параметры и объекты, локальные для процедуры. Регистр %ebp используется (как указывает его имя) в качестве базового указателя, который указывает на базовую (или нижнюю) локальный стек внутри процедуры.

После ввода процедуры регистр указателя стека (%esp) указывает на адрес возврата, хранящийся в стеке, командой вызова (это адрес инструкции сразу после вызова). Если вы просто вызовете ret, этот адрес будет выведен из стека в %eip (указатель инструкции), и код будет выполняться дальше от этого адреса (следующей инструкции после call). Но мы еще не вернулись, не так ли?; -)

Затем вы нажимаете %ebp register, чтобы сохранить свое предыдущее значение где-то и не потерять его, потому что вы будете использовать его для чего-то в ближайшее время. (BTW, он обычно содержит базовый указатель функции вызывающего абонента, и когда вы заглядываете в это значение, вы найдете ранее сохраненный %ebp, который будет снова базовым указателем функции на один уровень выше, чтобы вы могли отслеживать стек вызовов таким образом.) Когда вы сохраняете %ebp, вы можете сохранить текущий %esp (указатель стека) там, так что %ebp будет указывать на тот же адрес: базу текущего локального стека. %esp будет перемещаться вперед и назад внутри процедуры, когда вы будете толкать и выскакивать значения в стеке или резервировать и освобождать локальные переменные. Но %ebp останется фиксированным, все еще указывая на базу локального фрейма стека.

Доступ к параметрам

Параметры, переданные этой процедуре вызывающим абонентом, являются "заторможенными просто разблокировать землю" (т.е. они имеют положительные смещения относительно базы, поскольку стек растет). У вас есть %ebp адрес базы локального стека, где находится предыдущее значение %ebp. Под ним (т.е. На 4(%ebp) лежит обратный адрес. Таким образом, первый параметр будет в 8(%ebp), второй - в 12(%ebp) и т.д.

Локальные переменные

И локальные переменные могут быть выделены в стек над базой (т.е. они будут иметь отрицательные смещения относительно базы). Просто вычтите N в %esp, и вы просто выделили N байты в стеке для локальных переменных, перемещая верхнюю часть стека выше (или, точно, ниже) этой области:-) Вы можете ссылаться на это область с отрицательными смещениями относительно %ebp, т.е. -4(%ebp) - это первое слово, -8(%ebp) - второе и т.д. Помните, что (%ebp) указывает на базу локального стека, где было сохранено предыдущее значение %ebp, Поэтому не забудьте восстановить стек до предыдущей позиции, прежде чем пытаться восстановить %ebp через pop %ebp в конце процедуры. Вы можете сделать это двумя способами:
1. Вы можете освобождать только локальные переменные, добавляя N к %esp (указателю стека), то есть перемещая верхнюю часть стека, как если бы эти локальные переменные никогда не были там. (Ну, их значения останутся в стеке, но они будут считаться "освобожденными" и могут быть перезаписаны последующими нажатиями, поэтому более безопасно ссылаться на них. Они мертвые тела; -J)
2. Вы можете очистить стек до земли и освободить все локальное пространство, просто восстановив %esp из %ebp, который был исправлен ранее в базе стека. Он восстановит указатель стека до состояния, которое оно только после ввода процедуры, и сохранит %esp в %ebp. Это похоже на загрузку ранее сохраненной игры, когда вы что-то испортили; -)

Отключение указателей рамки

Возможно иметь менее беспорядочную сборку из gcc -S, добавив переключатель -fomit-frame-pointer. Он указывает GCC не собирать код для установки/сброса кадра стека, пока он действительно не понадобится для чего-то. Просто помните, что это может смущать отладчиков, потому что они обычно зависят от того, какой стек стека находится там, чтобы иметь возможность отслеживать стек вызовов. Но это ничего не сломает, если вам не нужно отлаживать этот двоичный файл. Он отлично подходит для целей релиза и экономит некоторое пространство-время.

Информация о кадре вызова

Иногда вы можете встретить некоторые странные директивы ассемблера, начиная с .cfi, чередующихся с заголовком функции. Это так называемая информация о кадре. Он используется отладчиками для отслеживания вызовов функций. Но он также используется для обработки исключений на языках высокого уровня, для которых требуется разворачивание стека и другие манипуляции на основе вызовов. Вы также можете отключить его в своей сборке, добавив переключатель -fno-dwarf2-cfi-asm. Это говорит GCC использовать простые старые ярлыки вместо этих странных директив .cfi, и он добавляет специальные структуры данных в конце сборки, ссылаясь на эти метки. Это не отключает CFI, просто изменяет формат на более "прозрачный": таблицы CFI затем видны программисту.

Ответ 2

Ты хорошо справился со своей интерпретацией. Когда вызывается функция, адрес возврата автоматически помещается в стек, поэтому аргумент argc, первый аргумент, был отброшен на 4 (% esp). argv начнется с 8 (% esp) с указателем на каждый аргумент, за которым следует нулевой указатель. Эта функция подталкивает старое значение% esp в стек, чтобы оно могло содержать исходное, неизмененное значение после возврата. Значение% ecx при возврате не имеет значения, поэтому оно используется как временное хранилище для ссылки% esp. Кроме этого, вы правы со всем.

Ответ 3

Относительно вашего первого вопроса (где хранятся аргументы командной строки) аргументы для функций находятся прямо перед ebp. Я должен сказать, что ваш "реальный" main начинается с < main + 10 >, где он подталкивает ebp и перемещает esp в ebp. Я думаю, что gcc испортит все это с помощью lea только для замены обычных операций (зависимостей и вычитаний) на esp до и после вызова функций. Обычно процедура выглядит так (простая функция, которую я сделал в качестве примера):

   0x080483b4 <+0>:     push   %ebp     
   0x080483b5 <+1>:     mov    %esp,%ebp
   0x080483b7 <+3>:     sub    $0x10,%esp            # room for local variables
   0x080483ba <+6>:     mov    0xc(%ebp),%eax        # get arg2
   0x080483bd <+9>:     mov    0x8(%ebp),%edx        # and arg1
   0x080483c0 <+12>:    lea    (%edx,%eax,1),%eax    # just add them
   0x080483c3 <+15>:    mov    %eax,-0x4(%ebp)       # store in local var
   0x080483c6 <+18>:    mov    -0x4(%ebp),%eax       # and return the sum
   0x080483c9 <+21>:    leave
   0x080483ca <+22>:    ret 

Возможно, вы включили некоторые оптимизации, которые могут сделать код более сложным. Наконец, да, возвращаемое значение сохраняется в eax. В любом случае, ваша интерпретация вполне верна.

Ответ 4

Единственное, что я считаю непонятным из ваших оригинальных вопросов, - это то, почему в вашем коде существуют следующие утверждения:

0x08048381 <main+13>:   push   %ecx
0x08048382 <main+14>:   mov    $0x6,%eax
0x08048387 <main+19>:   pop    %ecx

Толчок и поп% ecx в <main+13> и <main+19>, похоже, не имеют большого смысла - и они действительно ничего не делают в этом примере, но рассмотрите случай где ваш код вызывает вызовы функций.

Нельзя гарантировать, что система гарантирует, что вызовы других функций, которые будут устанавливать свои собственные кадры активации стека, не будут регистрировать значения reset. На самом деле они, вероятно, будут. Поэтому код устанавливает секцию сохраненного регистра в стеке, где все регистры, используемые кодом (кроме% esp и% ebp, которые уже сохранены с помощью обычной установки стека), хранятся в стеке, прежде чем, возможно, передадут управление вызовам функций в "мясе" текущего кодового блока.

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

Однако в случае вашего кода примера нет вызовов функций - только одна команда в <main+14>, где вы устанавливаете возвращаемое значение, но компилятор не может этого знать и сохраняет свои регистры как обычно.


Было бы интересно посмотреть, что произойдет здесь, если вы добавили операторы С, которые переместили другие значения в стек после <main+14>. Если я прав, если это будет сохраненным регистром раздела стека, вы можете ожидать, что компилятор будет вставлять автоматические операторы pop до <main+19>, чтобы очистить эти значения.