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

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

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

Добро пожаловать в 3-ю часть серии, которую я делаю по учебной программе низкого уровня для C/C++.

Это о стеке, который, возможно, является наиболее важным компонентом основного «движка» C/C++. Если вы когда-либо пытались узнать только об одном аспекте низкоуровневого поведения C/C++, то мой совет — сделать это стеком.

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

Если вы хотите вернуться и прочитать их, вот ссылки на первые две статьи:

  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

Пролог

Если вы программист на C++ и не уверены на 100%, что такое стек или как он работает, то вы не одиноки.

В книге Бьярна Страуструпа «Язык программирования C++ (3-е издание)» — которая в значительной степени является стандартным текстом по C++ (по крайней мере, до публикации обновления для стандарта C++11…) — не обсуждается, что такое стек и как оно работает; хотя это относится к данным или объектам, находящимся «в стеке», как будто читатель знает, что это значит.

Ближе всего к конкретной информации о стеке в Bjible можно найти следующий абзац в разделе Приложения C, озаглавленном «C.9 Управление памятью»…

"Автоматическая память, в которой размещаются аргументы функций и локальные переменные. Каждый вход в функцию или блок получает свою собственную копию. Память такого рода автоматически создается и уничтожается; отсюда и название «автоматическая память». Также говорят, что автоматическая память «должна находиться в стеке». Если вам абсолютно необходимо указать это явно, C++ предоставляет избыточное ключевое слово auto ».

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

На моем курсе по компьютерным наукам концепция стека была освещена на паре слайдов во время обязательного модуля 1-го года обучения под названием «Компьютерные системы и архитектура», но никогда конкретно не касалась языков программирования, которые мы изучали — и это, дорогой читатель, почему я чувствую себя обязанным написать об этом…

Что такое стек?

Неудивительно, что стек — это стековая структура данных. Для ясности я буду использовать стек с большой буквы, чтобы отличить его от любого старого экземпляра структуры данных стека.

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

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

Детальная специфика того, как работает стек, варьируется от ЦП к ЦП, от машины к машине, от компилятора к компилятору и даже от одного и того же компилятора и разных опций компилятора (подробнее об этом в следующем посте!).

Вообще говоря, каждый раз, когда вызывается новая функция:

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

Таким образом, в обобщенном случае стек включает в себя следующую информацию:

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

Область стека, которая содержит локальные переменные, принадлежащие функции (и любые другие данные, которые она может туда поместить), называется фреймом стека этой функции. Это важный термин, не забывайте его!

Ясно, что работа со стеком чрезвычайно важна для выполнения вашего кода, и, надеюсь, теперь очевидно, почему то, что так легко сделать, как запись за пределами массива, объявленного как локальный внутри функции, может привести к такому эпическому сбою — запись вне границ, скорее всего, перезапишет адрес возврата функции или какое-либо другое значение, имеющее решающее значение для правильного запуска программы после возврата текущей функции.

Как стек работает на практике?

Чтобы помочь ответить на эти вопросы, давайте рассмотрим (очень простую) программу C/C++:

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

Я бы определенно посоветовал сделать это самостоятельно после прочтения этой статьи, потому что всегда гораздо поучительнее разобраться в чем-то подобном самостоятельно, чем просто прочитать это…

Как я упоминал ранее, подробные особенности работы со стеком — особенно в отношении передачи параметров функциям — будут зависеть от используемых вами опций компилятора. Различия в основном сводятся к набору стандартов генерации кода, которые называются «соглашениями о вызовах». Каждое соглашение имеет свои собственные правила о том, как параметры передаются функциям и как возвращаются значения. Некоторые, как правило, работают быстрее, некоторые — медленнее, но большинство из них предназначены для удовлетворения конкретных требований к передаче данных, таких как вызовы функций-членов C++ или переменное количество параметров (например, printf() ).

Соглашение о вызовах по умолчанию, используемое VS2010 с функциями в стиле C (т. е. без указателя this), известно как stdcall, и, поскольку мы смотрим на отладочную сборку, дизассемблирование, которое мы рассматриваем, будет использовать полностью неоптимизированный stdcall. Это соглашение о вызовах помещает в стек все, кроме значений, возвращаемых функциями, которые возвращаются через регистр eax.

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

Настройка

Для начала создайте пустой консольный проект win32, создайте в нем новый файл .cpp (на вашем месте я бы назвал его main.cpp), а затем вставьте в него приведенный выше код.

Затем откройте настройки проекта и убедитесь, что для параметра «проверки во время выполнения» установлено значение «по умолчанию». Это не только делает код отладки (намного) быстрее, но и существенно упрощает ассемблер, который он генерирует, особенно в случае нашей очень простой программы. На изображении ниже показано, как должно выглядеть диалоговое окно параметров после внесения изменений.

Поставьте точку останова в строке 7 (да, я знаю, что эта строка является определением main()), а затем скомпилируйте и запустите отладочную сборку. Когда точка останова сработает, щелкните правой кнопкой мыши исходное окно и выберите «Перейти к дизассемблированию».

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

Ясно, что это значительно сложнее, чем дизассемблирование, которое мы рассматривали ранее, поэтому, прежде чем двигаться дальше, давайте немного расскажем о том, как стек управляется в сгенерированном компилятором 32-битном ассемблере x86 (по крайней мере, компилятором VS2010 C++ с настройки компилятора и компоновщика по умолчанию).

Прежде чем мы начнем…

Первая часть информации, которая начнет понимать это, заключается в том, что два ключевых регистра ЦП обычно участвуют в управлении кадрами стека в 32-битном ассемблере x86:

  • esp — или регистр Указатель стека, который всегда указывает на «верх» стека, и
  • ebp — или регистр Base Pointer, указывающий на начало (или базу) текущего кадра стека.

Локальные переменные обычно представляются как смещения из регистра ebp, в этом случае iResult хранится по адресу [ ebp-4].

Если вы хотите видеть имена локальных переменных, а не смещения от ebp, вы можете щелкнуть правой кнопкой мыши и установить флажок «Показать имена символов», но полезно знать, что смещения от ebp отрицательны.

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

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

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

Толкай и хлопай

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

Неудивительно, что в наборе инструкций x86 есть инструкции push и pop, каждая из которых принимает регистровый операнд:

  • pushуменьшает esp на размер своего операнда, а затем сохраняет этот операнд по адресу, указанному byes (т. е. на вершине стека).
  • Это означает, что после инструкции push значение по адресу esp указывает на то, что было помещено в стек.
  • pop копирует значение из адреса, содержащегося в esp, в свой операнд, а затем увеличивает esp на размер своего операнда, так что его операнд по существу удалены из стека.

Эти поведения являются ключевыми для того, как работает стек.

Как выглядит стек перед выполнением нашего кода

Я уверен, что большинство из вас знает, что есть код, который запускается перед main(). Этот код отвечает за все виды инициализации системы, и по завершении он вызывает main(), передавая аргументы командной строки в качестве параметров.

Давайте посмотрим на расположение стека в точке непосредственно перед выполнением первой инструкции в main() — на диаграмме ниже я назвал функцию, которая вызывает main(), «pre-main()» — вы, вероятно, обнаружите, что имя фактической функции в стеке вызовов вашей программы представляет собой устрашающую комбинацию символов подчеркивания и акронимов с заглавной буквы.

Эта диаграмма будет иметь больше смысла, когда вы вернетесь к ней после прочтения оставшейся части статьи.

[прим. номера строк, используемые в пунктах ниже, относятся к номерам в поле кода выше]

Преамбула функции (или пролог функции)

Еще до того, как дизассемблирование дойдет до присвоения iResult 0, у нас есть изрядное количество ассемблера для того, что выглядит как ничто на уровне C/C++; так что давайте разбираться.

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

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

[прим. номера строк, используемые в пунктах ниже, относятся к номерам в кодовом поле выше, содержащем фрагмент дизассемблирования]

  • строка 3: сохраняет текущее значение ebp в стеке с помощью инструкции push.
  • строка 4: перемещает текущее значение esp в ebp. ebp теперь указывает прямо на старое значение ebp, которое только что было помещено в стек.
  • строка 5:вычитает 44 часа(68 в десятичном формате) из значения esp. Это приводит к выделению 68 байтов в стеке после ebp.
  • строки 6, 7 и 8: хранят значения, содержащиеся в регистрах ebx, esi и edi соответственно, помещая их в стек. Это связано с тем, что ассемблер в main() использует эти регистры и должен быть в состоянии восстановить их в их старое состояние перед возвратом. Обратите внимание, что каждая инструкция push будет уменьшать значение esp на 4 байта.

Н.Б. Если вы следуете этому в отладчике, то я бы посоветовал вам открыть окно «Регистры» в отладчике, чтобы посмотреть, как значения регистров изменяются по мере того, как вы выполняете один шаг дизассемблирования. Вы, вероятно, также преуспели бы, если бы окна памяти были открыты, чтобы вы могли указывать их на esp и ebp, чтобы наблюдать за изменением значений в памяти (чтобы получить окно памяти в VS2010 для для отслеживания регистра вам нужно будет нажать кнопку «Автоматически переоценивать» справа от поля редактирования «Адрес:», а затем ввести имя регистра в поле редактирования).

Еще до того, как дизассемблирование дойдет до присвоения 0 элементу iResult, у нас будет изрядное количество ассемблера для того, что на уровне C/C++ ни на что не похоже; так что давайте разбираться.

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

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

На данный момент стек выглядит так:

Несколько замечаний по поводу этих диаграмм снимков стека:

  • Значение Tв левом верхнем углу этих снимков используется для идентификации старых значений ebp, хранящихся в стеке.
  • Различные цвета используются, чтобы показать, какая функция отвечает за помещение данных в стек (и, следовательно, за их удаление…).
  • Различные оттенки одного и того же цвета представляют разные логические типы данных, помещаемых в стек каждой функцией (т. е. базовый указатель, кадр стека (локальные значения), значения сохраненных регистров и параметры/обратные адреса).

Функция Postamble (или Epilogue)

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

Это работа постамбулы, или эпилога. Постамбула просто делает логическую противоположность преамбуле — она выталкивает то, что выталкивает преамбула, и восстанавливает значения, которые esp и >ebp перед выполнением кода преамбулы.

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

Единственная строка в преамбуле, которая не имеет прямой противоположности в постамбуле, — это строка 5 ( sub esp, 44h) — и это потому, что присвоение esp из ebp в строка 14 отменяет строки 4 и 5.

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

А теперь: собственно код, который мы написали!

Теперь, когда мы рассмотрели преамбулу и заключительную часть, мы можем сосредоточиться на теле main() и на том, как он вызывает функцию AddOneTo().

Это первая часть дизассемблирования, которая напрямую связана с кодом, который мы видим на уровне C/C++.

Итак, как мы все должны знать из преодоления нашего страха перед разборкой, строка 2 в приведенном выше коде устанавливает iResult, который, как мы видим, находится в пределах нашего текущего кадр стека по адресу [ ebp-4] в стеке.

Остальные инструкции настраиваются для вызова функции AddOneTo(), вызывают ее и присваивают iResult из возвращаемого значения.

  • строка 4: перемещает значение iResult в регистр eax.
  • строка 5: помещает значение iResult из eax на вершину стека (что также уменьшает esp ). При этом копия значения iResult сохраняется в стеке в качестве параметра функции для AddOneTo().
  • строка 6: звонит по адресу 0131101Eh. Эта инструкция вызывает функцию AddOneTo(). Сначала он помещает в стек адрес 01311299h (который является адресом памяти инструкции в строке 7), а затем переводит выполнение программы на инструкцию в 0131101Eh.
  • строка 7: когда функция, вызванная строкой 6, возвращается, параметр функции, помещенный в стек в строке 5, должен быть удален, чтобы состояние стека так же, как и до вызова AddOneTo(). Для этого мы добавляем 4 к esp — это имеет тот же эффект на esp, что и pop, но нам не важно значение, поэтому имеет смысл настроить esp напрямую. Я предполагаю, что это также более эффективно, но я никогда не изучал это.
  • строка 8: перемещает значение из eax в [ebp-4], где мы знаем, что iResult хранится. Стандартное соглашение «stdcall» для кода win32 x86 указывает, что eax используется для возврата значений из функций, поэтому эта строка присваивает возвращаемое значение AddOneTo() для iРезультат.

Вызов AddOneTo()

Давайте просто подробно рассмотрим инструкции, связанные с вызовом AddOneTo():

  1. Копия значения iResult помещается в стек (через eax) в качестве параметра для AddOneTo().
  2. push перемещает esp на 4 байта (т.е. 32 бита), затем сохраняет свой операнд по этому адресу, после инструкции push значение iResult хранится по адресу [esp].
  3. call помещает адрес следующей инструкции для выполнения после возврата из функции (адрес возврата01311299h) в стек, а затем переходит к выполнению по адресу 0131101Eh.
  4. Копия значения iResult теперь находится по адресу [esp+4], а обратный адрес — по адресу [esp].

На данный момент стек выглядит так:

Код по адресу 0131101Eh, на который был вызван, выглядит следующим образом:

Должен признаться, это сбивает с толку.

Эта инструкция просто заставляет выполнение кода снова перейти, на этот раз к дизассемблированию, представляющему фактическое тело функции AddOneTo(), которое находится по адресу 01311250h. Зачем вызывать инструкцию, которая делает еще один прыжок?

Если вы пройдете через это самостоятельно, вы заметите, что дизассемблирование вокруг этой функции выглядит как набор меток в стиле goto. Это потому, что они есть. Вы также заметите, что инструкции, связанные с каждым ярлыком, перескакивают в другое место. Ясно, что мы смотрим на своего рода «таблицу прыжков».

Причина этого? Поскольку я использовал параметры сборки конфигурации отладки по умолчанию; для параметра «Включить добавочное связывание» установлено значение «Да».

Инкрементное связывание делает связывание быстрее (очевидно), но явно вносит небольшие накладные расходы на все вызовы функций. Это то, что вы, возможно, захотите отключить в параметрах сборки, но вы хотели бы профилировать его, чтобы принять обоснованное решение о его влиянии в любом случае, прежде чем делать это.

Инструкция jmp не нарушает работу стека, так что на самом деле никакого вреда не наносится (кроме вероятного промаха кэша инструкций, вызванного таблицей переходов инкрементной ссылки).

Получение параметра, переданного в AddOneTo()

Итак, наконец, мы подошли к разборке тела AddOneTo():

Мы уже знакомы с преамбулой (строки с 3 по 8) и постамбулой (строки с 16 по 20), которые идентичны main().

Строка 10 намного интереснее, так как она извлекает параметр функции iParameter из стека. Обратите внимание на положительное смещение от ebp — это означает, что адрес, из которого перемещает значение в eax, находится вне фрейма стека этой функции.

Как мы установили ранее, когда мы перешли к адресу этой функции, адрес, на который указывал esp, содержал обратный адрес и копию локальной переменной iResult (т.е. хранился по адресу [ esp+4].

Первая инструкция в преамбуле — это push, который изменяет esp еще на 4 байта; поэтому сразу после строки 3 значение iResult — или iParameter, как оно упоминается в этой функции — теперь равно [esp +8].

Следующая инструкция перемещает значение esp в ebp, поэтому значение iReturn передается в качестве параметра в эта функция теперь также находится по адресу [ ebp+8] — откуда к ней обращается строка 10.

Итак, теперь мы знаем, как аргументы передаются функциям. Победить!

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

Возврат результата AddOneTo()

Игнорируя преамбулу функции, мы остаемся с:

  • Строка 2 перемещает значение iParameter из стека в eax.
  • Строки 3 и 4 добавляют единицу к значению параметра в eax, а затем перемещают содержимое eax по адресу [ebp -4], который является адресом локальной переменной iLocal.
  • Строка 6 задает возвращаемое значение функции, перемещая значение iLocal в eax, где соглашение о вызовах stdcall указывает, что возвращаемые значения . Если вы помните, код в main, который обращается к возвращаемому значению, ожидает его в eax.

Если вы обратили внимание, вы должны были заметить, что строки 4 и 6 по существу избыточны, так как возвращаемое значение уже было в eax после строки 3.

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

Итак, последняя часть головоломки во всем этом — фактически возврат от одной функции к другой.

Мы знаем, что постамбула возвращает стек в то же состояние, в котором он находился непосредственно перед выполнением преамбулы функции, ну, мы уже знаем, как это выглядит, потому что у нас есть его биграмма при T=2. :

Последний ret в строке 13 в поле кода выше вызывает адрес возврата, который в настоящее время хранится в верхней части стека с помощью инструкции call в main( ) извлекается (добавляя 4 к esp) и возобновляет выполнение по этому адресу, т. е. с инструкции сразу после вызова в main().

Фу. Вот оно. Видишь ли, это было не так уж и плохо?

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

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

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

Если, например, вы работаете с платформой, которая использует какой-либо вариант процессора IBM Power PC, просто запустите одну из простых демонстраций SDK и выполните дизассемблирование в отладочной сборке. В то время как мнемоника дизассемблера будет (очень) другой, просто потратьте немного времени на руководство по аппаратному обеспечению, и вы обнаружите, что он делает почти то же самое, что и этот код x86, который мы только что рассмотрели.

Вы, вероятно, обнаружите некоторые существенные отличия от ассемблера x86, который мы рассматривали, поскольку платформа, с которой вы работаете, почти наверняка использует другое соглашение о вызове функций — например, ваш компилятор может в основном передавать аргументы через регистры и использовать стек только тогда, когда функция требует большого количества параметров или использует var args (например, printf() ).

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

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

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

Хотите верьте, хотите нет, но мы еще не закончили работу со стеком — нам еще многое предстоит рассказать о нем!

Например:

  • Что происходит при передаче › 1 параметра?
  • Подробнее о том, как локальные переменные используют память стека.
  • Как работает передача по значению и возврат по значению.
  • Как работают некоторые из различных соглашений о вызовах функций x86, в частности __fastcall, который больше похож на соглашения о вызовах, обычно используемые ABI (двоичным интерфейсом приложений) для консольных платформ.

Учитывая, насколько длинной стала эта статья (и сколько времени ушло на ее написание…), я, вероятно, разделю ее на несколько постов.

Эпилог…

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

Я нашел оптимизированный код довольно интересным.

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

Спасибо

Я хотел бы поблагодарить Брюса, Тиффани, Джонатана, Фабиана и Даррена за их отзывы об этой статье. Это определенно намного лучше для него.

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