Пролог

Значения в других словарях

  1. Пролог — (греч. prologos, от pro — перед и logos — слово, речь) вступительная часть литературного и театрального произведения, которая предворяет общий смысл, сюжет или основной мотивы произведения или кратко излагает события… Большая советская энциклопедия
  2. пролог — Ч е г о и к ч е м у. Это же пролог новой эры, эры народовластия, социализма! (Соколов). Из всего этого материала может выйти разве пролог к роману!(Гончаров). Управление в русском языке
  3. Пролог — орф. Пролог, -а (церк. книга) Орфографический словарь Лопатина
  4. Пролог — (греч. prologos — вступление, предисловие, от pro — перед и logos — слово, речь) — вступительная сцена спектакля (драм., оперного, балетного). В др.-греч. драм. т-ре в П. излагался миф, положенный в основу произв. Музыкальная энциклопедия
  5. пролог — -а, м. 1. Вступительная часть к литературному или музыкальному произведению, предваряющая общее содержание произведения или раскрывающая общий замысел автора. Пролог к «Руслану и Людмиле» Пушкина. Малый академический словарь
  6. пролог — ПР’ОЛОГ, пролога, мн. пролога, ·муж. (·греч. prologos — предисловие) (лит.). В древней Руси — сборник кратких житий, поучений, назидательных повестей, размещенных в порядке церковного календаря. Рукописный пролог. II. ПРОЛ’ОГ, пролога, мн. и, ·муж. Толковый словарь Ушакова
  7. пролог — 1. пролог м. 1. Вступительная, вводная часть литературного, музыкального произведения или спектакля. 2. перен. Начало чего-либо, вступление к чему-либо. 2. Пролог м. Церковная книга, содержащая жития святых, поучения и т.п., расположенные по дням года. Толковый словарь Ефремовой
  8. ПРОЛОГ — ПРОЛОГ (от греч. prologos — вступление) — вступительная часть литературного, театрального и музыкально-сценического произведения. В литературном (театральном) прологе рассказывается о событиях, предваряющих и мотивирующих основное действие… Большой энциклопедический словарь
  9. пролог — ПРОЛОГ -а; м. 1. Вступительная часть к литературному или музыкальному произведению, предваряющая общее содержание произведения или раскрывающая общий замысел автора. П. симфонии, романа. Толковый словарь Кузнецова
  10. пролог — орф. пролог, -а (вступление) Орфографический словарь Лопатина
  11. Пролог — • Prolŏgus, Πρόλογος см. Comoedia, Комедия, и Tragoedia, Трагедия. Словарь классических древностей
  12. пролог — 1. ПРОЛОГ, а, м. (спец.). Древнерусский, а также южнославянский сборник кратких житий, поучений и назидательных рассказов, расположенных в последовательном порядке по годичным праздникам, по дням богослужений. Славяно-русский П. 2. ПРОЛОГ, а… Толковый словарь Ожегова
  13. пролог — ПРОЛОГ а, м. prologue m. <�гр. prologos < pro впереди + logos речь, слово. 1. Вступительная, вводная часть литературного или музыкального произведения или спектакля. БАС-1. Словарь галлицизмов русского языка
  14. пролог — см. >> начало Словарь синонимов Абрамова
  15. пролог — I. пролога, мн. пролога, м. (лит.). В древней Руси – сборник кратких житий, поучений, назидательных повестей, размещенных в порядке церковного календаря. II. пролога, мн. и, м. (книжн.). Большой словарь иностранных слов
  16. пролог — проло́г род. п. -а. Через нем. Рrоlоg или франц. prologue от лат. prologus из греч. πρόλογος. Напротив, др.-русск. прологъ «сборник кратких житий святых, синаксарий» (Пантел. еванг. 1250 г.; см. Срезн. II, 1542) непосредственно из греч. (Фасмер, Гр.-сл. эт. 160). Этимологический словарь Макса Фасмера
  17. пролог — ПРОЛОГ греч. церковная книга, содержащая краткие слова на годичные праздники, жития святых и отрывки нз писаний св. отцев, для чтения при богослужении. | Пролог или пролог введение, вступление, предисловие к сочинению, особенно драматическому. См. Толковый словарь Даля
  18. пролог — ПРОЛОГ Традиционная велогонка с раздельного старта на короткую дистанцию (около 4-10 км), которой открывается многодневная шоссейная гонка; обычно проводится с целью индивидуального представления гонщиков зрителям и определения лидера для первого этапа. Словарь спортивных терминов
  19. пролог — 1. про́лог/ (церк. книга). 2. про/ло́г/ (вступление) . Морфемно-орфографический словарь
  20. пролог — НАЧАЛО — КОНЕЦ Начальный — конечный (см.) начинать(ся) — кончать(ся) (см.) начинать(ся) — заканчивать(ся) (см.) начало — окончание (см.) начальный — окончательный начинать(ся) — оканчивать(ся) (см.) Начало дороги — конец дороги. Словарь антонимов русского языка
  21. пролог — сущ., кол-во синонимов: 18 введение 40 вводная часть 4 вступление 17 завязка 17 книга 160 начало 92 начальный период 5 парад-пролог 2 первые шаги 20 первый шаг 13 почин 9 преамбула 6 предисловие 9 сборник 61 синаксарь 3 старт 16 экспозиция 12 язык 247 Словарь синонимов русского языка

Значения слов «пролог» и «эпилог» необходимо понимать, готовясь к ЕГЭ по русской и зарубежной литературе. К сожалению, школьникам не всегда удается получить необходимые знания на уроках или из учебника.

Мы постараемся объяснить содержание этих понятий в максимально простой и доступной форме.

Что означают слова «пролог» и «эпилог»?
Что такое пролог в литературе?
Зачем нужен эпилог в литературе?
Для чего применяется пролог в музыке?
Примеры использования эпилога в музыке

Что означают слова «пролог» и «эпилог»?

Пролог – это слово, заимствованное из греческого языка, где «prologos» обозначает «вступление». Так называется вводная часть литературного или музыкального произведения, в том числе театральной пьесы или музыкально-сценической постановки.

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

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

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

Что такое пролог в литературе?

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

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

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

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

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

Зачем нужен эпилог в литературе?

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

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

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

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

Для чего применяется пролог в музыке?

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

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

Примеры использования эпилога в музыке

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

Характерным примером является эпилог оперы «Иван Сусанин» М.Глинки, который представляет собой массовую сцену оплакивания гибели Сусанина, которое затем переходит в мощный величественный хор – знаменитое «Славься».

Одной из основ исследования структуры исполняемых образов программ (реверсивная инженерия), является изучение пролога и эпилога функции. Обычно об этих понятиях мало кто задумывается, до той самой поры, пока не начинают изучать строение исходного кода на уровне машинных (процессорных) команд, иными словами — в дизассемблированном виде. Очевидно, что знакомый с одноименными терминами мира искусства, дальновидный читатель сразу же догадается что пролог это ни что иное как своеобразное «предисловие», некая вводная часть, вступление, а эпилог — это нечто завершающее, «послесловие», заключительная часть. Проводя аналогию, логично было бы предположить, что механизмы эти призваны выполнять некие предваряющие и завершающие действия по отношению к какой-либо сущности, а именно к функциям. И все же не совсем понятно, как именно термины пролога и эпилога относятся к функциям/процедурам из мира вычислительной техники? Довольно нетривиальной задачей является попытка интуитивного сопоставления терминов из мира искусства с областью программирования. К тому же, у данной области имеется множество особенностей, которые мы сегодня и попытаемся понять.

Что такое функция

Для начала давайте немного определимся с терминологией и определим, что же есть функция:

Функция (function) — это подпрограмма, которая может получать параметры и возвращать результат выполнения (значение). Функции позволяют разделить и организовать логику выполнения программы: разграничить содержимое приложения на логические блоки, которые вызываются по мере возникновения необходимости.

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

Применение функции называется вызовом функции (function call).

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

Функция без параметров

Для начала, давайте вернемся в прошлое, в те времена, когда на просторах компьютерной эры безраздельно господствовал реальный режим работы процессора x86 и старые операционные системы реального же режима. В качестве основы возьмем популярную в то время операционную систему MSDOS, а пример приведем на низкоуровневом языке Ассемблера. В общем то, синтаксис Ассемблера x86 не очень то отличается между разными реализациями/операционными системами, поэтому ничего страшного в выборе MSDOS нет, просто в то время удалось поковырять именно её. Давайте посмотрим, как выглядел вызов простой (типовой) функции:

кто пусть даже поверхностно разбирается в языке Ассемблера, может уверенно констатировать, что приведенный пример весьма и весьма прост. Осуществляется вызов некой функции с именем some_function, в момент вызова в стеке (пара регистров SS:SP) сохраняется адрес возврата определенной размерности (длиной в слово, два байта) из процедуры, который указывает на следующую за инструкцией call команду. Потом управление передается на точку входа (по адресу первого байта) функции. Код функции выполняется, затем команда ret извлекает из стека сохраненный (ранее) адрес возврата и передает по этому адресу управление, тем самым осуществляя возврат из функции. Для пущей наглядности проиллюстрируем на конкретном (сюрприз) примере:

Как видно из кода, происходит вызов (инструкция call 000000065), затем в функции осуществляется ряд действий и выполняется возврат (инструкция retn). Тут я намеренно привел достаточно простую функцию, а все ради того, что бы читатель ощутил существенный контраст с публикуемым далее материалом.

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

Функция с параметрами

Как мы уже отметили, в приведенном выше (довольно простом) примере, в функцию не передаются какие-либо аргументы.

Каждый элемент множества входных параметров называется аргументом функции.

Тем не менее, в «дикой природе» компьютерных программ значительно чаще можно встретить более сложные функции, то есть те, при вызове которых требуется передача (в функцию) одного или более входных параметров. К тому же программ, написанных на высокоуровневых языках программирования, значительно больше, нежели тех, что написаны на чистом Ассемблере, поэтому сейчас мы обратимся к современному опыту и приведем более распространенный пример, реализованный на языке MS Visual C++. Давайте набросаем простенький пример функции на C++:

По коду можно определить, что функция f1 (описана в строке 5) имеет ряд входных параметров (три параметра — определенные в самой функции как a, b, c).

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

Функция f1 вызывается (строка 12) с тремя аргументами (значения 1, 2, 3) и осуществляет вывод на консоль (посредством printf) этих значений. А теперь посмотрим, как будет выглядеть вызов функции после компиляции и дизассемблирования (на низком уровне):

1 2 3 4 PUSH 3 ; 3-Й ПАРАМЕТР В СТЕК PUSH 2 ; 2-Й ПАРАМЕТР В СТЕК PUSH 1 ; 1-Й ПАРАМЕТР В СТЕК CALL 00351433 ; ВЫЗОВ ФУНКЦИИ F1

Как интересно, сразу возникает довольно таки много вопросов. Во-первых, почему аргументы передаются через стек (команда push)? Во-вторых, почему аргументы передаются в обратном порядке (начиная с 3-го)? Все эти вопросы, безусловно, требуют пояснения, но на данный момент я не буду давать ответа, Вы его получите позже, в процессе изучения материала статьи. Теперь, неплохо было бы взглянуть на ассемблерный код и самой функции f1:

и приведем её в очищенном текстовом варианте, для того, что бы проще было объяснять:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 PUSH EBP ; СОХРАНИМ ОРИГИНАЛЬНЫЙ EBP (ДОСТАВШИЙСЯ ОТ ВЫЗЫВАЮЩЕЙ ФУНКЦИИ) MOV EBP,ESP ; ИНИЦИАЛИЗИРУЕМ УКАЗАТЕЛЬ КАДРА SUB ESP,0C0 ; РЕЗЕРВИРОВАНИЕ МЕСТА В СТЕКЕ ПОД ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ ФУНКЦИИ F1 PUSH EBX ; СОХРАНИМ РЕГИСТР ОБЩЕГО НАЗНАЧЕНИЯ, ИСПОЛЬЗУЮЩИЙСЯ ВНУТРИ ФУНКЦИИ PUSH ESI ; СОХРАНИМ РЕГИСТР ОБЩЕГО НАЗНАЧЕНИЯ, ИСПОЛЬЗУЮЩИЙСЯ ВНУТРИ ФУНКЦИИ PUSH EDI ; СОХРАНИМ РЕГИСТР ОБЩЕГО НАЗНАЧЕНИЯ, ИСПОЛЬЗУЮЩИЙСЯ ВНУТРИ ФУНКЦИИ LEA EDI, MOV ECX,30 MOV EAX,CCCCCCCC REP STOS DWORD PTR ES: MOV ECX,OFFSET 0081F027 CALL 0081127B MOV EAX,DWORD PTR SS: ; ПОЛУЧАЕМ АРГУМЕНТ 3, ПЕРЕДАННЫЙ В ФУНКЦИЮ F1 PUSH EAX ; ЗАТАЛКИВАЕМ В СТЕК MOV ECX,DWORD PTR SS: ; ПОЛУЧАЕМ АРГУМЕНТ 2, ПЕРЕДАННЫЙ В ФУНКЦИЮ F1 PUSH ECX ; ЗАТАЛКИВАЕМ В СТЕК MOV EDX,DWORD PTR SS: ; ПОЛУЧАЕМ ПАРАМЕТР 1, ПЕРЕДАННЫЙ В ФУНКЦИЮ F1 PUSH EDX ; ЗАТАЛКИВАЕМ В СТЕК PUSH OFFSET 00819B30 ; ASCII «%d, %d, %d» CALL 0081142E ; ВЫЗЫВАЕМ ФУНКЦИЮ PRINTF ADD ESP,10 ; ОЧИЩАЕМ СТЕК ОТ ПЕРЕМЕННЫХ PRINTF POP EDI ; ВОССТАНАВЛИВАЕМ РЕГИСТР ОБЩЕГО НАЗНАЧЕНИЯ POP ESI ; ВОССТАНАВЛИВАЕМ РЕГИСТР ОБЩЕГО НАЗНАЧЕНИЯ POP EBX ; ВОССТАНАВЛИВАЕМ РЕГИСТР ОБЩЕГО НАЗНАЧЕНИЯ ADD ESP,0C0 ; ВОССТАНАВЛИВАЕМ СТЕК ОТ РЕЗЕРВАЦИИ МЕСТА ПОД ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ ФУНКЦИИ F1 CMP EBP,ESP CALL 00811285 MOV ESP,EBP ; ВОССТАНАВЛИВАЕМ ESP POP EBP ; ВОССТАНАВЛИВАЕМ EBP RETN ; ВОЗВРАТ ИЗ ФУНКЦИИ

Видно невооруженным глазом, что функция f1, написанная на высокоуровневом языке, значительно сложнее функции, приведенной в первом примере выше (написанном на Ассемблере). Конечно, мною выбрана не совсем уж простая функция для первого примера, тем не менее интересующие нас места в коде я выделил цветом. Все дело в том, что все сложные функции, написанные на высокоуровневах языках и имеющие какие-либо аргументы, вне зависимости от операционной системы, на уровне машинного кода стали значительно сложнее. Сразу обращает на себя внимание дополнительная работа со стеком (строки 1-3 и 25, 28, 29), работа с переданными в функцию f1 параметрами (строки 13, 15, 17). Все эти трансформации структуры функций не являются прихотью конкретного компилятора, а объясняются соглашениями о вызовах.

Соглашение о вызовах

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

  1. при помощи регистров общего назначения (к примеру, для 32-битного процессора это: eax, ebx, acx, edx, esi, edi и прч.);
  2. посредством записи параметров непосредственно в стек (передача через стек);
  3. посредством использования ячеек памяти (запись/чтение значений по выделенным адресам памяти);

Первый вариант крайне неудобен, поскольку:

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

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

Соглашение о вызовах — стандартизация (определение/описание) способов передачи аргументов в функцию и восстановления системных структур при выходе из функции.

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

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

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

cdecl Аргументы функций передаются через стек, справа налево. Аргументы, размер которых меньше 4х байт, расширяются до 4х байт. Очистку стека выполняет вызывающая программа; pascal Аргументы функций передаются через стек, слева направо. Указатель на вершину стека (значение регистра ESP) в изначальную позицию возвращает вызываемая подпрограмма; stdcall (WinAPI) Аргументы функций передаются через стек, справа налево. Очистку стека производит вызываемая подпрограмма. fastcall Аргументы в функцию передаются через регистры, если для сохранения аргументов (и промежуточных результатов) регистров оказывается не достаточно, используется стек. Включает в себя несколько соглашений, которые действуют по сходим принципам, отличаясь только незначительными деталями. safecall Соглашение о вызовах, используемое для вызова методов интерфейсов COM; thiscall Аргументы функции передаются через стек, справа налево. Очистку стека производит вызываемая функция. Незначительно отличается от stdcall тем, что указатель на объект сохраняется в регистр ECX; Соглашения о вызовах, которым необходимо следовать при выполнении кода приложения, имеет возможность определять автор программы.

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

Формат вызова stdcall стал стандартном де-факто, он имеет поддержку во всех современных компиляторах, и используется в подавляющем большинстве динамических библиотек, представляющих глобальные API-сервисы. В stdcall для передачи аргументов в функцию используется стек, передача происходит справа налево (обратный порядок, от конца к началу: сначала передается последний параметр, затем предпоследний, и так далее до первого). Очистку стека производит вызываемая подпрограмма.

С соглашением о вызовах тесно связано понятие указателя кадра стека, зачастую называемого так же указателем фрейма стека (stack frame), указателем фрейма (frame pointer) или стековым кадром. Теперь давайте плавно перейдем к его изучению.

Стековый фрейм (кадр стека, стековый кадр)

Ну стек то уж вы должны знать что такое? 🙂 Так вот, а стековый фрейм — это часть данных стека, или:

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

Из определения уже начинает проясняться, что это ни что иное как фрагмент стека (часть памяти, выделенной под стек) потока, используемый исключительно для служебных целей: передачи параметров функции из кода вышестоящей (вызывающей) функции и выделения памяти для использования внутри вызываемой функции (как правило, для хранения локальных переменных). Ну, определение то не такое уж и простое, поэтому хотелось бы посмотреть как всё это это используется на практике. В процессе сборки/компоновки (перевод исходного кода в машинный и сборка объектных модулей) программы, написанной на одном из высокоуровневых языков программирования, код всех используемых разработчиком «собственных»/»встраиваемых» функций транслируется таким образом, что для передачи параметров в эти функции и доступа к локальным переменным этих функций, тем или иным образом используются регистры и/или стек. На (самом низком) уровне машинного кода, процессор, выполняющий какой-либо код, встречает в нем инструкцию вызова функции (например call), начинает её исполнять. Исполнение заключается в некоей предварительной подготовке, и непосредственно перед передачей управления на адрес (точку входа) функции, процессор должен положить (разместить, засунуть) аргументы функции в стек в установленном порядке (определяемым соглашением о вызовах), и только после этого произвести вызов функции (передачу ей управления). Когда вызванная таким образом (функция|процедура) получает управление, она наследует и программный стек от вызвавшей функции, на вершине которого располагается адрес возврата из функции, после которого идут входные параметры (аргументы), с которыми данная функция была вызвана. Ну хорошо, стек вызывающей функции мы получили, но можно ли с ним сразу активно работать, ведь мы его тогда «испортим»? Именно так, поэтому для того, чтобы не портить стек вызвавшей функции, вызываемая функция настраивает так называемый собственный стековый кадр, с целью оперирования собственными (локальными) переменными, сохранения произвольных локальных значений и прч. Новый стековый кадр (фрейм) настраивается через инициализацию указателя кадра.

Указатель кадра — регистр общего назначения (обычно EBP), содержащий значение текущей (на момент входа в функцию) вершины стека (ESP). Используется для организации индексируемого доступа (например: SS:) к стековому фрейму (выделенной области стека).

Давайте немного проиллюстрируем сказанное:

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

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

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

Пролог и эпилог функции

В языках высокого уровня имеется два основных типа функций:

  • Кадровая функция (frame function) — функция, которая: выделяет пространство стека, сохраняет содержимое используемых в функции регистров, использует обработку исключений, создает собственный кадровый фрейм в стеке потока, имеет пролог и эпилог. может вызывать другие функции.
  • Простая функция (leaf function) — функция, которая: не создает стековый фрейм в стеке потока, не должна модифицировать указатель стека, не должна вызывать другие функции. Может выходить через инструкцию jmp на стековый кадр другой функции. В самом названии leaf (лист) отражается весь смысл данного типа функции — лист, если рассматривать иерархию вызовов функций в качестве разветвленного дерева, то лист — это «конечная его точка», из которой другие функции вызываться не должны (ничего не произрастает).

Таким образом, любая функция, которая:

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

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

  • для настройки стекового фрейма (сохранение оригинального содержимого регистра ESP, настройку нового и инициализацию указателя кадра EBP);
  • для сохранения значений регистров (через которые в функцию переданы аргументы) в локальный стек для последующей работы с ними в коде функции;
  • для сохранения (в стеке) значений регистров, которые будут использованы внутри подпрограммы, поскольку грамотно написанный код должен заботиться о том, чтобы значения регистров процессора, до и после работы функции оставались неизменными. Это своеобразные правила хорошего тона и просто перестраховка. Однако, если Вы или компилятор уверены в том, что порча содержимого определенных регистров не вызовет деструктивных воздействий на последующее выполнение программы — это условие становится не обязательным;
  • (для пролога вне функции) для записи (в регистры или стек) аргументов функции;
  • для резервирования места в стеке с целью хранения локальных переменных. Достигается это смещением указатель стека в сторону уменьшения адресов, то есть выше, в сторону увеличения стека. После этого мы получаем часть памяти, которая доступна (через указатель фрейма) и не задействована. Это пространство может быть использовано кодом функции по своему усмотрению, чаще всего для хранения локальных переменных функции;

Эпилогом называют код, который предназначается для действий, обратных прологу:

  • восстановления (из стека) значений регистров, сохранённых кодом пролога (в том числе и регистра стекового фрейма ebp);
  • очистка (корректировка указателя) стека (от локальных переменных функции).

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

Пролог

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

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

Частный случай 1:

1 2 3 4 5 push ebp mov ebp, esp push eax push ebx sub esp, X

Рассмотрим подробнее вышеприведенный код:

  1. Код пролога сохраняет текущее значение регистра EBP для последующего восстановления при возврате из функции. Ведь значение EBP в вызывающей функции тоже может использоваться в качестве указателя на стековый фрейм.
  2. Регистр EBP инициализируется значением регистра ESP. Это и есть настройка указателя стекового фрейма, через него мы обеспечим доступ к аргументам функции, переданным из вышестоящей (вызывающей) функции. Одновременное команда имеет смысл с точки зрения сохранения значения регистра ESP, поскольку перед выходом из функции нам надо будет восстановить значение регистра.
  3. Сохраняет в стеке регистры общего назначения, которые будут использованы (испорчены) в коде функции (строки 3-4). Сделано этого с целью предотвращения «порчи» значений регистров по возвращению из функции в вызвавшую ветку кода. Надо учитывать, что сохранение регистров общего назначения может выполняться в разных местах пролога: до/после выделения памяти в стеке, после установления кадра функции, так же регистры могут сохраняться без применения команды push, при помощи прямой записи в память стека.
  4. команда sub esp, X «резервирует» (выделяет) место в стеке для хранения локальных переменных, где X — требуемое количество байт для хранения локальных переменных. Какая сакраментальная цель данного действа? Дело в том, что после выполнения данного «резервирования» мы уже получаем как бы дополнительное место в общем стеке под названием «локальный стек функции», которое может использоваться исключительно на нужды текущей функции, то есть можем смело использовать команды работы со стеком (например: push/pop), не опасаясь испортить стек основной программы. Зачастую резервируется больше места, чем в действительности необходимо коду функции, потому что количество выделяемого места в локальном стеке должно быть выровнено по 16-байтной границе (ссылка: выравнивание данных).

Очевидно, что с этого момента регистр ebp на протяжении работы кода функции начинает использоваться для доступа к локальным переменным и аргументам функции через смещение, задаваемое в явном виде (например: ebp+0Ch). Теоретически, для подобных целей вместо ebp можно использовать любой другой регистр, однако остальные регистры, в том числе и esp, часто меняются, поэтому их не очень удобно использоваться в целях хранения указателя.

Частный случай 2:

1 2 3 4 push ebp mov ebp, esp and esp, fffffff8 sub esp, X

От приведенного в первом варианте, код, показанный в варианте №2, отличается тем, что в нем присутствует инструкция выравнивания указателя стека 3, для того, чтобы адрес был кратен 4 байтам. Существует еще вариант с 0FFFFFFF0h. Происходит выравнивание значения в регистре ESP по 8-байтной границе, делая все впоследствии помещаемые в стек значения также выровненными по этой границе. Зачем? Считается что процессор более эффективно работает с переменными, расположенными в памяти по адресам кратным 4 или 16, то есть скорость выполнения увеличивается или что-то в конвейере?. Таким образом в языках высокого уровня (например С/С++) выравниваются члены классов и прочие переменные.

Частный случай 3:

1 enter

Довольно элегантно, неправда ли? Раз и одной командой все сделал.

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

Эпилог

Эпилог — машинный код в самом конце функции (процедуры, подпрограммы), который:

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

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

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

  • x32: add esp, <константа>;
  • x64: add rsp, <константа>;

..либо коррекции через указатель кадра:

  • x32: lea esp, XXX;
  • x64: lea rsp, ;

Частный случай 1:

1 2 3 mov esp, ebp pop ebp ret X
  1. Код эпилога восстанавливает значение указателя ESP (из регистра EBP, содержимое которого не изменяется на протяжении всей функции), фактически возвращая его в исходное состояние, которые было на момент вызова функции. Таким образом мы восстанавливаем стек, который был ДО входа в функцию и принадлежал коду основной программы/вышестоящей функции. Если бы мы этого не сделали, то после выхода из функции указатель стека указывал бы на другое смещение, из-за чего дальнейшее выполнение с большой вероятностью привело бы к ошибке.
  2. Команда pop ebp восстанавливает сохраненное в стеке значение ebp, поскольку ebp мы использовали как указатель на стековый фрейм функции.
  3. Команда ret выполняет возврат управления в вызывающую функцию.

Частный случай 2:

1 2 leave ret X

как то тут все хитро, не находите? Взяли и заменили команды восстановления указателей ESP и EBP на некую непонятную инструкцию leave, похоже что она содержит в себе некую расширенную логику. И действительно, фактически это аналог хорошо знакомой нам пары команд mov esp, ebp и pop ebp, возвращающей исходное значение ESP (из EBP) и затем восстанавливающей из стека значение EBP. Ну а последняя команда ret X производит сперва выталкивание из стека адреса возврата из подпрограммы (функции), затем выталкивание X байт, и уже потом только выполняет переход на адрес возврата.

Оставить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *