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

Эй,

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

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

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

Огромное спасибо! :]


person masterwok    schedule 20.01.2011    source источник
comment
Голосование за закрытие слишком широкое. Пожалуйста, сосредоточьтесь на одном вопросе, в котором вы не уверены.   -  person Ciro Santilli 新疆再教育营六四事件ۍ    schedule 05.10.2015
comment
Я не думаю, что этот пост слишком широкий. Да, у него есть несколько вопросов, но основное внимание уделяется пониманию базовой разборки. Вы предлагаете, чтобы я задавал каждый вопрос индивидуально. Это не имеет для меня смысла, поскольку каждый вопрос будет без контекста. Я не собираюсь спамить кучей вопросов по каждой строке сборки. К тому же этому посту 4 года.   -  person masterwok    schedule 05.10.2015
comment
Не беспокойтесь, слишком широкое очень субъективно, мы оба можем быть правы :-) Ура.   -  person Ciro Santilli 新疆再教育营六四事件ۍ    schedule 05.10.2015
comment
Я действительно не думаю, что закрытие вполне правильно для вопроса с таким количеством действительных и интересных ответов.   -  person 1800 INFORMATION    schedule 06.10.2015
comment
Аналогично, но с большим количеством кода: stackoverflow .com/questions/2988571/   -  person Ciro Santilli 新疆再教育营六四事件ۍ    schedule 16.10.2015


Ответы (4)


Кадры стека

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

push  %ebp
mov   %esp, %ebp

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

После входа в процедуру регистр указателя стека (%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 видны программисту.

person Community    schedule 03.08.2012

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

person ughoavgfhw    schedule 20.01.2011
comment
Спасибо, это было очень полезно! - person masterwok; 20.01.2011

Что касается вашего первого вопроса (где хранятся аргументы командной строки), аргументы функций находятся прямо перед ebp. Я должен сказать, что ваш "настоящий" мэйн начинается с < main + 10 >, где он толкает ebp и перемещает esp в ebp. Я думаю, что gcc все портит со всеми этими leas только для того, чтобы заменить обычные операции (сложения и вычитания) на 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. В любом случае, ваша интерпретация вполне верна.

person BlackBear    schedule 20.01.2011

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

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

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

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

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

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


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

person CharlesK    schedule 13.04.2011