× К оглавлению На главную Об авторе

Дата и время публикации:

Дата и время модификации:

Использование и применение


1. Использование

1.1 Что такое кадр стека

Используется для организации кадрирования стека, т.е. каждый раз во время вызова функции ей создается, т.е. выделяется, кадр (англ.frame) в программном стеке (англ.program stack)

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

Классическая схема выделение кадра стека вызываемой функции показана на рисунке 1.1

Рисунок 1.1

После завершения данной функции происходит «освобождение» выделенного кадра стека.

Компилятор gcc позволяет выделять кадр стек вызываемой функцией двумя широко известными способами:

1.2 Метод выделения с сохранением указателя

Осуществляет сохранение стековых указателей вершины стека, вызывающей функции, регистра (%rbp) и текущего указателя программного стека (%rsp), что достигается путем передачи компилятору GCC (явно или неявно) опции  -fno-omit-frame-pointer , как показано на рисунке 1.2

Рисунок 1.2

Как показано на рисунке 1.2, кадр стека представляет собой "перевернутый стакан", перемещение по которому осуществляется сверху-вниз с помощью регистра %rbp, содержащий значение %rsp, обозначая тем самым "дно стакана". При этом первой пролитой "каплей" будет адрес "дна" кадра стека вызывающей функции, которое кладется инструкцией push . Затем, производится сохранение "дна" вызываемой функции с помощью инструкции mov, которая копирует значение регистра %rsp в %rbp.

После сохранения указателей стека регистров %rbp и %rsp, производится выделение кадра стека путем уменьшения значение регистра %rsp .

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

Освобождение стека происходит после записи результата работы функции в регистр-аккумулятор %rax и до вызова инструкции ret .

.

(п.2.1 настоящего документа)

Примечания. 1. В широко применяемой архитектуре x86-64, первые 6-ть аргументов функции могуг быть переданы в регистре %rdi – 1-й аргумент, %rsi – 2-й аргумент, %rdx – 3-й аргумент, %rcx – 4-й аргумент, %r8 – 5-й аргумент, %r9 – 6-й аргументов. 2. В этой же архитектуре принято возвращать результат работы функции в регистре %rax, размерность которого позволяет не только возвращать значения переменных, но и адрес в памяти, где они могут находится . 3. Регистр %rsp – используется для хранения указателя на текущее положение (адреса) вершины программного стека. Соответственно, уменьшая значения регистра – увеличиваем размер программного стека. 4. Регистр %rbp – указатель базы, задумывался как "опорным" адресом или "дном" кадра стека во время выполнения функции, являлся и является по сей день достаточно удобным средством для собственноручного ассемблирования и отладки. Однако, в настоящее время компилятор GCC использует отладочный формат DWARF, обеспечивающий по средством CFI доступ к кадрам стека отлаживаемых функций без использования указанного регистра. Тем самым, использование регистр %rbp в будущем становится сомнительным.

1.3 Метод выделения кадра без использования указателя

Опция  -fomit-frame-pointer  приводит к пропуску сохранения указателей стека от начала и до конца, а так же во время исполнения вызова функции (англ. function call(. В таком случае выделение кадра стека вызываемой функции будет производится так, как показано на рисунке 1.3 .

Рисунок 1.3

Как показано на рисунке 1.2, во время вызова функции main(), размер кадра выделяется с учетом передаваемых ей трех аргументов: указателя переменных окружения argp (англ. enviroment variables), указателя аргументов командой строки argv (англ. command line arguments) и численного значения argc, содержащее их количество. При этом нужно заметить, что в это случае все булькает от "края стакана", т.е. кадра стека располагается ближе к младщему адресу программного стека или к его вершине, на который указывет %rsp, с похожей компоновкой локальных переменных, аргумент командной строки и переменных окружения.

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

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

(п.2.2 настоящего документа)

2. Применение

2.1 Применение опции  -fno-omit-frame-pointer 

В листинге 2.1 показан результат применение данной опции для исходного кода, приведенного на рисунках 1.2 и 1.3

Листинг 2.1

   ...
093      .globl  main
094 main:
   ...
108      pushq   %rbp
   ...
111      movq    %rsp, %rbp
   ...
113      subq    $48, %rsp
114      movl    %edi, -20(%rbp)
115      movq    %rsi, -32(%rbp)
116      movq    %rdx, -40(%rbp)
   ...
119      movq    $0, -8(%rbp)
   ...
131      call    noautomatic_array_defunc
   ...
133      movq    %rax, -8(%rbp)
   ...
244      leave
   ...
246      ret
   ...

В листинге 2.1 показан фрагмент результата генерации кода ассемблера копилятором GCC,который запускает программа gasrunparts c опцией gnuc_noautomatic_array, доступной с версии 0.11

В строке 108 и 111 производится сохранение значений адресов указателей стека регистров %rbp и %rsp соответственно.

Выделение стека производится в строке 113 путем уменьшения адреса программного стека, а освобождение стека производится c помощью инструкции leave в строке 244, а выход из функции main() – инструкции ret в строке 246

Инструкция leave эквивалентна тем же самым инструкциям в строке 246 и в строке 244. Только с тем отличием, что в строке 245 потребуется заменить инструкцию push на pop , т.к. производится выталкивания сохраненного адреса в регистр %rbp после отката регистра %rsp на вершину кадра стека вызывающей функции с помощью инструкции mov.

В листинге 2.2.1 показан вариант использования указанных инструкций вместо leave.

Листинг 2.2.1

   ...
244      mov   %rbp, %rsp     
245      pop   %rbp
   ...

2.2 Применение опции  -fomit-frame-pointer 

В листинге 2.2 показан результат применение данной опции для исходного кода, приведенного ранее на рисунках 1.2 и 1.3

Листинг 2.1

   ...
101      subq    $56, %rsp
   ...
103      movl    %edi, 28(%rsp)
104      movq    %rsi, 16(%rsp)
   ...
106      movq    %rdx, 8(%rsp)
   ...
110      movq    $0, 40(%rsp)
   ...
115      movq    $0, 32(%rsp)
   ...
124      call    noautomatic_array_defunc
   ...
126      movq    %rax, 40(%rsp)
   ...
234      movl    $0, %eax
   ...
238      addq    $56, %rsp
   ...
240      ret
   ...

В листинге 2.2 показан фрагмент результата генерации кода ассемблера копилятором GCC,который запускает программа gasrunparts c опцией gnuc_noautomatic_array_nofp, доступной с версии 0.11

В строке 101 производится выделение кадра стека путем уменьшением регистра указателя программного стека %rsp на 56 адресов вниз. При этом операция сохранения указателей стека регистров %rbp и %rsp не производится, т.к. на выходе из функции main() производится приращение на тоже число адресов "вверх".

Соответственно, после выделения кадра стека, производится сохранение первых 4-х байт из регистра %rdi согласно размерности 1-го агумента, переменной argc, функции main() на 28 адресов вверх от текущего указателя программного стека в регистре %rsp.

Затем, производится сохранение 2-го аргумент функции main() уже на 16 адресов от так называемого "дна", на который указывает регистр %rsp. А за ним, со смещением на 8-мь адресов – 3-го аргументов функции main().

Сохранение же возвращаемого значения в регистра-аккумуляторе %rax функцией noautomatic_array_defunc() производится в строке 126 со смещением на 40 байт от текущего положения указателя программного стека регистра %rsp .

3. Библиография

3.1 Stackoverflow. Assembly x86 – "leave" Instruction

3.2 [pdf]DWARF Debugging Format Standards, version 4

3.3 What is a call frame

3.4 x86-64 architecture guide

3.5 Initial stack reading-process arguments

3.6 Stack frame layout on x86-64