Помогите понять основную разборку main () в GDB - PullRequest
12 голосов
/ 20 января 2011

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, когда дело доходит до сборки, поэтому любая помощь будет отличной! Также, если вы видите какие-либо ложные утверждения о том, что, по моему мнению, происходит, исправьте меня.

Спасибо большое! :]

Ответы [ 4 ]

12 голосов
/ 03 августа 2012

Стековые кадры

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

push  %ebp
mov   %esp, %ebp

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

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

Затем вы нажимаете %ebp регистр, чтобы сохранить его предыдущее значение где-нибудь и не потерять его, потому что вы будете использовать его для чего-то в ближайшее время.(Кстати, он обычно содержит базовый указатель функции вызывающей стороны, и когда вы смотрите это значение, вы найдете ранее сохраненный %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 становятся видимыми для программиста.

4 голосов
/ 20 января 2011

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

3 голосов
/ 20 января 2011

Относительно вашего первого вопроса (где хранятся аргументы командной строки), аргументы для функций прямо перед ebp. Я должен сказать, что ваша «настоящая» главная начинается в < 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. Ваша интерпретация в любом случае совершенно правильна.

1 голос
/ 13 апреля 2011

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

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

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

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

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

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


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

...