3.1.4. Stack - и его команды
Для комфортной работы, системе необходима некоторая область памяти для производственных нужд. Типичная ситуация, когда нужно временно сохранить значение, а свободных регистров нет. Для таких случаев, ещё в i8086 был выделен сегмент памяти под названием стек, и специальная регистровая пара
В доках Интела такую картину назвали ‘Expand-Down’ – обратное расширение адреса. В предыдущей части я приводил скрин с описанием сегментного дескриптора. В нём есть 4-битное поле ‘Type’, в котором описывается тип и характеристики отдельно взятого сегмента. Географически, эти биты заняли позиции [11-8] в 8-байтном дескрипторе, их описание из третьего тома приводится ниже:
На рисунке видно, что если бит(11) установлен в единицу, перед нами сегмент-кода, иначе данных. Стек – это разновидность сегмента данных, только со-взведённым битом(10) Expand. Судя по докам, стек должен быть неисполняемым Execute, однако на практике выходит совсем иначе, и если запихать в него шелл-код, то он прекрасно отработает. К сожалению INTEL во-время спохватилась и кастрировала эту возможность, введя бит NX с технологией DEP (Data-Execution-Protect).
Для работы со-стеком имеются две команды - PUSH кладёт значение на вершину стека, а POP – снимает его от туда. При этом
Устройство стека можно сравнить с боксом для лазерных дисков. Командой PUSH мы кладём в стек первый
Кроме нашей программы, стек активно используют и системные API. В промышленной их реализации всегда присутствует т.н. ‘Пролог’ и ‘Эпилог’. Пролог копирует
Как видим, FASM добавил в код отсебятину, т.к. мы планировали внутри функции только две инструкции ‘No-Operation’ NOP. Причём в конце стоит не просто RET, а RET 8, чтобы снять со-стека два аргумента по 4-байт, которые мы туда положили при вызове функции. То-есть компилятор сам оптимизирует код, чтобы избавить нас от рутины.
Кроме PUSH есть ещё и инструкция PUSHA, что означает Push-All-Registers. Она без операндов и помещает в стек сразу 8 регистров РОН в следующем порядке:
Программный стек – это кладезь информации, которую оставляют после себя различные функции. Дело в том, что инструкция POP не затирает значения в стеке, а просто сдвигает указатель
Кроме того, на стек опирается и технология SEH для отлова программных исключений, таких-как деление на нуль и прочие. Подмена указателей SEH даёт возможность перехватывать исключения, после чего приложение не отправляется к праотцам, а продолжает работать как ни в чём не бывало. Вообщем стек – это круто!
Для комфортной работы, системе необходима некоторая область памяти для производственных нужд. Типичная ситуация, когда нужно временно сохранить значение, а свободных регистров нет. Для таких случаев, ещё в i8086 был выделен сегмент памяти под названием стек, и специальная регистровая пара
SS:SP – Stack-Segment and Pointer. Устройство данного сегмента отличается от остальных тем, что адреса в нём не увеличиваются, а наоборот уменьшаются. То-есть стек растёт снизу-вверх, а не как обычные сегменты сверху-вниз.В доках Интела такую картину назвали ‘Expand-Down’ – обратное расширение адреса. В предыдущей части я приводил скрин с описанием сегментного дескриптора. В нём есть 4-битное поле ‘Type’, в котором описывается тип и характеристики отдельно взятого сегмента. Географически, эти биты заняли позиции [11-8] в 8-байтном дескрипторе, их описание из третьего тома приводится ниже:
На рисунке видно, что если бит(11) установлен в единицу, перед нами сегмент-кода, иначе данных. Стек – это разновидность сегмента данных, только со-взведённым битом(10) Expand. Судя по докам, стек должен быть неисполняемым Execute, однако на практике выходит совсем иначе, и если запихать в него шелл-код, то он прекрасно отработает. К сожалению INTEL во-время спохватилась и кастрировала эту возможность, введя бит NX с технологией DEP (Data-Execution-Protect).
Для работы со-стеком имеются две команды - PUSH кладёт значение на вершину стека, а POP – снимает его от туда. При этом
ESP автоматически уменьшается или увеличивается на 4, соответственно. Посмотрим на такой рисунок:Устройство стека можно сравнить с боксом для лазерных дисков. Командой PUSH мы кладём в стек первый
dword, и он падает на дно стека. Значение ESP при этом авто\уменьшается на 4. Следующий PUSH бутербродом положит свой dword сверху, и ESP опять уменьшится на 4. Теперь, чтобы изъять из стека первое значение, нужно вытащить все сверху-лежащие. Поэтому программист должен следить за стеком в оба глаза, чтобы стек не оказался не выровненным. То-есть сколько положили в стек, столько нужно и снять с него. Если вспомнить свойства инструкции RET, то станет ясно ‘почему’.Кроме нашей программы, стек активно используют и системные API. В промышленной их реализации всегда присутствует т.н. ‘Пролог’ и ‘Эпилог’. Пролог копирует
ESP в EBP, чтобы через него получить доступ к параметрам функции в стеке. Поскольку (при вызове функции) CALL сохраняет ещё и адрес-возврата, то первый параметр получается по смещению EBP+4, второй EBP+8 и т.д.. Эпилог очищает стек от параметров, тем-самым выравнивая его на выходе из функции. В качестве демонстрации, напишем пустую функцию с параметрами, чтобы посмотреть, как преобразит её FASM.
C-подобный:
include 'win32ax.inc'
;-------
.code
start: stdcall myFunc,1234h,5678h
invoke ExitProcess,0
proc myFunc a,b
nop
nop
ret
endp
.end start
Как видим, FASM добавил в код отсебятину, т.к. мы планировали внутри функции только две инструкции ‘No-Operation’ NOP. Причём в конце стоит не просто RET, а RET 8, чтобы снять со-стека два аргумента по 4-байт, которые мы туда положили при вызове функции. То-есть компилятор сам оптимизирует код, чтобы избавить нас от рутины.
Кроме PUSH есть ещё и инструкция PUSHA, что означает Push-All-Registers. Она без операндов и помещает в стек сразу 8 регистров РОН в следующем порядке:
EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI. Снять их скопом в таком-же порядке позволяет инструкция POPA (хоть и звучит неприлично). Код ниже показывает, как можно вывести на экран значения всех РОН при помощи инструкции PUSHA. Для преобразования числа в строку, задействуем API wsprintf():
C-подобный:
format PE gui
include 'win32ax.inc'
;-------
.data
frmt db 'EDI...: %08X',13,10 ; спецификаторы для wsprintf()
db 'ESI...: %08X',13,10 ;
db 'EBP...: %08X',13,10 ;
db 'ESP...: %08X - ординал',13,10 ;
db 'EBX...: %08X',13,10 ;
db 'EDX...: %08X',13,10 ;
db 'ECX...: %08X',13,10 ;
db 'EAX...: %08X',0 ;
capt db 'RegDump v1.0',0
text db 'Состояние регистров процессора',13,10
db '------------------------------',13,10
buff db 512 dup(?) ; буфер для преобразованных строк
;-------
.code
start: pusha ; запомнить все регистры в стеке
invoke wsprintf, buff, frmt ; куда и в каком формате скинуть результат
add esp,8 ; за wsprintf() нужно очищать стек
popa ; восстановить все регистры
invoke MessageBox,0,text,capt,0 ; вывод результата на экран!
invoke ExitProcess,0 ;
.end start
Программный стек – это кладезь информации, которую оставляют после себя различные функции. Дело в том, что инструкция POP не затирает значения в стеке, а просто сдвигает указатель
ESP. Это приводит к тому, что вся информация остаётся на месте и если прокрутить стек назад, то можно найти в нём много интересного. Кроме того, на стек опирается и технология SEH для отлова программных исключений, таких-как деление на нуль и прочие. Подмена указателей SEH даёт возможность перехватывать исключения, после чего приложение не отправляется к праотцам, а продолжает работать как ни в чём не бывало. Вообщем стек – это круто!
Комментарии
36