FAQ для раздела Assembler, MASM, TASM - Assembler - Ответ 5036671

Что такое стек и для чего он нужен?

Стек — специально отведенная программе область памяти для хранения временных данных, содержимого регистров или адресов команд, следующих за командами

call

(при вызове процедур). На вершину стека (т.е. последнее записанное значение) указывает пара регистров ss:sp (или ss:esp для 32-разрядных программ). При записи слова (или двойного слова) в стек (командами push, call), оно записывается в память по адресу ss:[sp-2] (или ss:[sp-4], в зависимости от разрядности записываемого значения), а затем из регистра

sp/esp

вычитается 2 (или 4), то есть стек "растет" в сторону младших адресов памяти. При извлечении значения из стека (командами

pop

,

ret

) происходит обратная операция. Для чего это нужно? Предположим, Вам необходимо сохранить значение регистра (допустим,

ax

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

push ax

, а в том месте, где это значение понадобится —

pop ax

(или, скажем,

pop dx

, если Вам необходимо записать это значение в регистр

dx

). Но! Важно помнить, что запись в стек производится по принципу

LIFO

(Last In, First Out —

последним зашел, первым вышел

). Нельзя делать так:

push ax ... call ... pushf

, а затем

pop ax ... popf ... ret

, так как в этом случае в регистр

ax

будут занесено значение

регистра флагов

, в регистр флагов — адрес возврата из процедуры, а после выполнения команды

ret

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

ax

. Таким образом, данные записываются последовательно, а не каждый регистр по определенному адресу! В стек может быть записано как 16-битное, так и 32-битное значение, но не 8-битное. Соответственно, при записи 16-битных значений будет записано 2 байта, а при записи 32-битных значений - 4 байта. Но есть одно исключение: при записи в стек сегментных регистров (ds, es, cs, ss, fs или gs) 32-битные программы (например, программы, работающие под Windows) будут заносить не 2, а 4 байта(!), несмотря на то, что размер сегментных регистров составляет 16 бит. При этом на процессорах Pentium и выше старшее записываемое слово будет содержать нулевое значение. Что же касается команд типа

call

и

ret

, то они работают со значениями, размер которых соответствует размеру сегмента кода (то есть 16 бит для DOS-программ, 32 бита — для Win32). Компиляторы ассемблера предлагают несколько модификаций одних и тех же команд, работающих со стеком. Например, команды

pusha/popa

предназначены для сохранения и восстановления всех (восьми) регистров общего назначения (16-битных значений

ax

,

cx

,

dx

,

bx

,

sp

,

bp

,

si

и

di

либо 32-битных

eax

,

ecx

и так далее в зависимости от разрядности сегмента кода), а их модификации

pushaw/popaw

и

pushad/popad

— 16- и 32-битных соответственно независимо от разрядности сегмента. Что же касается команд

ret

и

retf

, то они не имеют модификаций и всегда вынимают из стека столько байт, сколько соответствует текущему размеру сегмента, то есть соответственно 2 и 4 для 16-битных сегментов кода и 4 и 8 для 32-битных. Похожая команда

iret

имеет модификации

iretw

и

iretd

в TASM и только

iretd

в MASM, причем MASM интерпретирует команду

iret

как 16-битную (даже в 32-битных программах), а

iretd

— как 32-битную. Аналогичная ситуация с командами

pushf/popf

и

pushfw/popfw

,

pushfd/popfd

. Записать в стек можно как значение регистра, так и значение из памяти (например,

push es:[bx]

) и число (

push 4000h

)

Что такое процедуры и зачем они нужны?

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

1

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

В процедуре могут использоваться локальные переменные. Это некоторая область в стеке, "выше" начала которой адрес дна стека не может быть увеличен без возврата из процедуры и разрушения локальных переменных. Для адресации локальных переменных используется регистр ЕВР. Так как архитектура NT требует сохранения в процедурах регистров EBX, ESI, EDI и EBP, то вначале процедуры в стеке сохраняется регистр EBP, затем в этот регистр загружается текущее значение ESP, в стеке аллоцируется пространство под локальные переменные. Далее в пределах процедуры до возврата из нее, значение в регистре EBP не изменяется. Таким образом, этот регистр адресует предыдущее значение регистра EBP (предыдущей процедуры), ниже в стеке рассположены локальные переменные и значения регистров, которые процедура сохранила в стеке при входе и восстановит при выходе, выше адрес возврата из процедуры, еще выше — аргументы переданные процедуре, если у функции есть аргументы, либо локальные переменные принадлежащие "материнской" процедуры, которая вызывала "нашу" процедуру. Начало прцедуры выглядит следующим образом:

AssemblerВыделить код

1
2
3
4
5
6
7
8

push ebp ;сохраняем предыдущее значение регистра ЕВР
mov ebp,esp ; загружаем ссылку в ЕВР
sub esp,32 ;выделяем в стеке 32 байта под локальные переменные
...
mov esp,ebp ;восстанавливаем стек. На дне лежит предыдущее значение регистра ЕВР, 
;выше - адрес возврата из процедуры
pop ebp ; восстанавливаем значение регистра EBP
ret 16 ; удаляем из стека четыре параметра типа DWORD и возвращаемся из процедуры

Стек после выполнения инструкции

sub esp,32

ESP ->
...
Locals
локальные переменные
EBP ->
rEbp

предыдущее значение регистра ЕВР, сохраненное

инструкцией push ebp в начале процедуры
lp
адрес возврата, сохраненный инструкцией Call
Arg1
первый параметр процедуры
Arg2

второй параметр процедуры (в stdcall-конвенции

вызова аргументы передаются справа налево)
...

Два слова в стеке всегда будут находится рядом, это адрес возврата из функции и сохраненное значение регистра ЕВР. Эта часть стека называется

базовым стековым фреймом

— Bp-frame

2

и описывается структурой STACK_FRAME

AssemblerВыделить код

1
2
3
4

STACK_FRAME struct
rEbp PVOID ? ; предыдущее значение EBP
rEip  PVOID ? ; адрес возврата из процедуры
STACK_FRAME ends

Эту структуру адресует регистр ЕВР в любой момент в пределах текущей процедуры. Очевидно для иной процедуры, вызываемой из данной, в этой структуре будет сохранена ссылка на текущий фрейм, таким образом формируется цепочка стековых фреймов (SFS). Поэтому изменим имена полей структуры следующим образом

AssemblerВыделить код

1
2
3
4
5

STACK_FRAME struct
Next PVOID ? ; ссылка на фрейм (РSTACK_FRAME) предыдущей процедуры, которая
;вызвала текущую. Это предыдущее значение регистра EBP
lp  PVOID ? ; адрес возврата из текущей процедуры
STACK_FRAME ends

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

lp = STACK_FRAME.Next.lp[ebp]

Такой проход по цепочке стековых фреймов называется

бэктресом

(back trace). Он может выполняться по достижению последнего стекового фрейма, при этом ссылка на следующий фрейм

инвалидна

(указывает за пределы стека

3

)

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

номером стекового фрейма

(

Stack Frame Number — SFN

). Так как в пределах текущей процедуры значение ЕВР не меняется, то можно сказать, что код процедуры исполняется на некотором уровне относительно других процедур, от которого зависит SFN (этот уровень равен ΔSFN двух процедур). Этот уровень называется

уровнем вложенности

процедур (

Nesting Level — NL

)

4

.

Важные свойства процедур:

Теперь можно дать определение процедуры —

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

.

Стековая маршрутизация

Стековый фрейм содержит адрес возврата из текущей процедуры, следующий фрейм содержит адрес возврата из вызывающей процедуры и так далее, то можно заменить во фрейме адрес возврата. Тогда после снижения NL (SFN) до нулевого уровня (относительно текущей процедуры), из стека будет извлечен соответствующий процедуре фрейм и выполнен возврат на адрес, находящийся в этом фрейме. Так как он изменен, то возврат произойдет на новый адрес. Эта манипуляция называется процедурным post-редиректом или стековой маршрутизацией.

____________________________________________

1

Процедурное ветвление в текущем кодовом сегменте осуществляется инструкцией

Call near

. Межсегментное ветвление —

Call far

(селектор описывает кодовый сегмент, а не шлюз вызова) сохраняют в стеке помимо адреса возврата также и селектор кодового сегмента (CS:EIP), куда будет выполнен возврат инструкцией

RETF

.

2

Для формирования и удаления BP-фреймов предназначены инструкции

enter

и

leave

соответственно

3

База и лимит стека находятся в TIB, поля StackBase и StackLimit, смещения которых соответственно +4 и +8 (в ks386 описываются константы с префиксом Tb или Te). В Kernel mode TIB находится в начале PCR, в User mode — в начале TEB. Учитывая, что TEB и PCR адресуются через один и тот же регистр FS, то мод не имеет значения. FS:[+4] в обоих режимах будет адресовать базу стека.

4

По сути это уровень определяющий сколько раз был вызван участок кода через некоторый механизм вызова. В данном случае это процедурное ветвление. Примером не процедурного ветвления может быть ISR — хэндлер прерывания является процедурой, но вызывается через особые механизмы. Непосредственно через инструкции Int, либо косвенно, когда вектор прерывания вызывается при некотором событии (прерывания и исключения). Другим примером могут служить быстрые вызовы сервисных функций ядра посредством инструкций Sysenter. При этом используется совершенно другой механизм (использование MSR), в отличие от прерываний. Важной особенностью процедуры является наличие механизма возврата. Для процедурного ветвления используется инструкция Ret, для прерываний — Iret, для возврата из быстрых сервисных вызовов — инструкция Sysexit и т.д. Механизм вызова и возврата из процедуры не обязательно должен использовать стек (пример — Sysexit). Для ISR NL будет определяться рекурсивным обращением к IDT, например, возникновение #GP из хэндлера #GP увеличит NL

5

Все процедуры Bp-based, то есть формируют стековые фреймы.

Вернуться к обсуждению:

FAQ для раздела Assembler, MASM, TASM Assembler