ПРИМЕЧАНИЕ. Первоначально это сообщение было опубликовано на сайте AltDevBlogADay.com прибл. 2012
Весь код был написан в Visual Studio 2010 (!!), поэтому ваша текущая версия может иметь другой пользовательский интерфейс или параметры с другими именами.

Другим вариантом, помимо использования реальной IDE, может быть использование Compiler Explorer, созданного замечательным Мэттом Годболтом: https://godbolt.org/z/YEc7h6YaK

(хотя сгенерированный ассемблер будет выглядеть по-другому, он должен быть достаточно похож, чтобы следовать и он интерактивен…)

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

Этот пост предполагает, что вы прочитали предыдущий пост о стеке (или уже знаете, как работает стек на ванильном ассемблере x86).

Если вы пропустили предыдущие посты, вот обратные ссылки:

  1. https://blog.darbotron.com/a-low-level-curriculum-for-c-and-c-part-1-f1df2c73ba14
  2. https://blog.darbotron.com/c-c-low-level-curriculum-part-2-data-types-ef04e9cf4fac
  3. https://blog.darbotron.com/c-c-low-level-curriculum-part-3-the-stack-a3287319384f

Я также дал хорошую ссылку на некоторые ресурсы IBM по PowerPC ABI, которые подробно объясняют (и на уровне ассемблера), как стек используется в процессорах на базе PowerPC, поскольку он на самом деле больше отличается от x86, чем я помнил. Это может оказаться особенно полезным, если вы работаете на консолях текущего поколения и хотите понять, как они используют стек, функции вызова и параметры передачи.

Более одного функционального параметра

Как и прежде, я использую консольное приложение win32, созданное мастером «нового проекта» в VS2010 с параметрами по умолчанию. Дизассемблирование, которое мы рассмотрим, происходит из конфигурации отладочной сборки, которая генерирует ванильный неоптимизированный код stdcall x86.

Единственное изменение, которое я делаю, это отключаю Basic Runtime Checks, чтобы сделать сгенерированный ассемблер более разборчивым (и значительно быстрее…). Подробнее о том, как это сделать, см. в предыдущем посте.

Мы собираемся обновить очень простую программу, использованную в прошлой статье, чтобы для вызываемой ею функции требовалось 3 параметра.

а вот ассемблер, который он генерирует для main() (по-прежнему адреса инструкций почти наверняка будут у вас отличаться):

Вызов суммы()

Как мы видели в предыдущей статье, мы знаем, что можем безопасно игнорировать преамбулу и постамбулу функции (строки 3–8 и строки 28–33; также известные как пролог и эпилог соответственно), которые устанавливают и удаляют фрейм стека функции, поскольку мы знаем, что они не участвуют в передаче параметров в SumOf().

Беглый взгляд на дизассемблирование, инициализирующее локальные переменные, показывает, что iValOne, iValTwo и iValThree хранятся в [ebp-4 ], [ebp-8] и [ebp-0Ch] соответственно.

Дизассемблирование, относящееся к вызову функции и присвоению возвращаемого значения, заключается в этой части:

Как и в случае с одним аргументом, копии значений параметров функции помещаются в стек, но обратите внимание, что они помещаются в в порядке, обратном порядку, в котором ожидается список параметров функции. их в коде C++.

Последнее, что следует отметить, это то, что после инструкции вызова в строке 22 (т. е. непосредственно перед выполнением ассемблера для SumOf()) копия iValOne, помещенная в стек в строке 20, оказывается на [esp+4]. Это связано с тем, что call помещает адрес возврата в стек, а также перенаправляет выполнение на новый адрес.

Просто чтобы убедиться, что у нас есть все это, вот как выглядит стек сразу после выполнения строки 20, но до любого кода в SumOf() выполняется:

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

Вот разборка для SumOf():

Мы видим, что код пролога функции помещает ebp, который перемещает esp еще на 4 байта, а затем перемещает esp в ebp— поэтому после строки 4 копия значения iValOne, переданная перед вызовом SumOf(), теперь находится в [ebp+8].

Вот еще один снимок стека, показывающий состояние после пролога функции (то есть после строки 8):

Глядя на строки 10–12, мы видим, что ассемблер обращается к параметрам функции следующим образом:

  • iParamOne (т. е. копия iValOne) из [ebp+8]
  • iParamTwo (т. е. копия iValTwo) из [ebp+0Ch]
  • iParamThree (т. е. копия iValThree) из [ebp+10h]

Неудивительно, что именно там значения main(), помещенные в стек перед вызовом этой функции, находились после пролога функции: D

Теперь мы можем понять, почему параметры функции помещаются в стек в обратном порядке с помощью main() — потому что функции ожидают, что их параметры будут храниться в стеке в порядке списка параметров, начиная с [ebp +8] и увеличение смещения от ebp для каждого параметра.

Как и раньше, возвращаемое значение (iLocal, хранящееся в [ebp-4]) перемещается в eax перед кодом эпилога функции, чтобы вернуть его в main(), и поскольку мы знаем, как работают эпилог и возврат из предыдущей статьи, мы закончили с ванильным stdcall с несколькими параметрами. Радость!

Краткое содержание

Мы подробно рассмотрели, как стек используется для вызова функций в ванильном неоптимизированном компиляторе, созданном на ассемблере x86 с использованием соглашения stdcall.

Это должно оставить вас в довольно удобном месте, чтобы побродить по окнам дизассемблирования с четким представлением о том, какие части дизассемблирования для каждой функции, скорее всего, будут актуальными - вы определенно должны быть в состоянии сделать обоснованное предположение, в котором параметр был передается как null в аварийном дампе, вызванном сбоем нулевого указателя: D

Для получения дополнительной информации и демонстрации того, насколько различным может быть использование стека (хотя в принципе он остается таким же), вот ссылка на четвертую серию статей на сайте Технической библиотеки IBM, посвященную ассемблеру PowerPC, и в особенно с 64-битным PowerPC ABI:

http://www.ibm.com/developerworks/linux/library/l-powasm4/index.html

По всей вероятности, вам нужно прочитать первые три статьи, чтобы понять смысл четвертой, но именно в четвертой находится большая часть полезной информации :)

В следующий раз

В следующий раз я рассмотрю соглашение о вызовах x86 thiscall, используемое, когда функции-члены C++ (где указатель this передается в ecx ), а также вкратце рассмотрим, как захватывающее соглашение о вызовах fastcall x86 использует стек.

первоначально опубликовано в 2011 году на, к сожалению, несуществующем www.altdevblogaday.com