Как компилятор C++ реализует обработку исключений - Функции и стэк

ОГЛАВЛЕНИЕ

 

Функции и стэк

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

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

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

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

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

Процессор поддерживает две операции для стека: поместить (протолкнуть) и извлечь (вытолкнуть). Рассмотрите:

pop EAX
означает прочитать 4 байта из ячейки, на которую указывает ESP, и увеличить (помните, стек растет сверху вниз в данном случае) ESP на 4 (в 32-битных процессорах). Аналогично,

push EBP
означает уменьшить ESP на 4 и затем записать содержимое регистра EBP в ячейку, на которую указывает ESP.

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

Обычно компилятор генерирует следующую последовательность для пролога:

Push EBP      ; сохраняем текущий указатель кадра в стек
Mov EBP, ESP  ; активируем новый кадр
Sub ESP, 10   ; вычитаем. устанавливаем ESP на конец кадра

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

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

Mov ESP, EBP   
Pop EBP         ; активируем кадр вызывающей функции
Ret             ; возвращаемся в вызывающую функцию

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

Когда процессор сталкивается с командой возврата, он делает следующее: он извлекает адрес возврата из стека и передает управление по этому адресу. Адрес возврата был помещен в стек, когда его вызывающая функция выполнила команду вызова, чтобы вызвать его. Команда вызова сначала помещает в стек адрес следующей команды, к которой должно быть возвращено управление, и затем переходит к началу вызываемой функции. Рисунок 3 показывает более детальный вид стека во время выполнения. Как показывает рисунок, параметры функции – тоже часть кадра стека функции. Вызывающая функция помещает аргументы вызываемой функции в стек. Когда функция возвращает значение, вызывающая функция удаляет аргументы вызываемой функции из стека путем прибавления размера аргументов к ESP, который известен во время компиляции:
Add ESP, args_size

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

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

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