Дата и время публикации:
Дата и время модификации:
Использование и применение
1. Использование
1.1 Что такое кадр стека
Используется для организации кадрирования стека, т.е. каждый раз во время вызова функции ей создается, т.е. выделяется, кадр (англ.frame) в программном стеке (англ.program stack)
Созданный кадр вызываемой функции (англ.call frame) содержит передаваемые ей аргументы, некоторую область для хранения переменных, а так же адрес вершины стека, вызывающей функции (англ.caller function), впрочем как и слепок регистров, если в этом существует необходимость. А при использование операторов-выражения стек вызываемой фунции используетс для временного хранения значений их операндов.
Классическая схема выделение кадра стека вызываемой функции показана на рисунке 1.1
Рисунок 1.1
После завершения данной функции происходит «освобождение» выделенного кадра стека.
Компилятор gcc позволяет выделять кадр стек вызываемой функцией двумя широко известными способами:
- опция -fno-omit-frame-pointer предписывает сохранять указатели кадра стека регистров (%rsp) и (%rbp), который потом будет «дном» стека (неоптимизированная схема, показанная на рисунке 1.1),
- опция -fomit-frame-pointer наоборот не делает сохранение указателя кадра стека и неиспользует его, работая со смещением вниз по указателю программному стеку от старшего адреса к младшему (оптимизированный метод).
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.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