Стек

From Wiki.conus.info

Jump to: navigation, search

Стек - это очень фундаментальная вещь.

Технически, это просто кусок памяти процесса + регистр ESP который указывает где-то в пределах этого куска.

Часто используемые инструкции для работы со стеком это PUSH и POP. PUSH уменьшает ESP на 4, затем записывает по адресу на который указывает ESP содержимое своего единственного операнда. POP - сначала достает из ESP значение и кладет его в операнд (который часто является регистром) и затем увеличивает ESP на 4. Конечно, это для 32-битной среды. В x64-среде это будет 8 а не 4.

В самом начале, ESP указывает на конец стека. PUSH уменьшает ESP, а POP - увеличивает. Конец стека находится в начале блока памяти. Это странно, но это так.

Для чего используется стек?

Contents

Сохранение адреса куда должно вернуться управление после CALL

При вызове другой функции через CALL, сначала в стек записывается адрес аккурат после инструкции CALL, затем делается безусловный переход (JMP) на адрес указанный в операнде. CALL это аналог пары инструкций PUSH address_after_call / JMP.

RET вытаскивает из стека значение и передает управление по этому адресу - это аналог пары инструкций POP tmp / JMP tmp

Крайне легко устроить переполнение стека запустив бесконечную рекурсию:

void f()
{
	f();
};

MSVC 2008 предупреждает о проблеме:

c:\tmp6>cl ss.cpp /Fass.asm
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.21022.08 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

ss.cpp
c:\tmp6\ss.cpp(4) : warning C4717: 'f' : recursive on all control paths, function will cause runtime stack overflow

... но тем не менее создает нужный код:

?f@@YAXXZ PROC						; f
; File c:\tmp6\ss.cpp
; Line 2
	push	ebp
	mov	ebp, esp
; Line 3
	call	?f@@YAXXZ				; f
; Line 4
	pop	ebp
	ret	0
?f@@YAXXZ ENDP						; f

... причем, если включить оптимизацию, то будет даже интереснее, без переполнения стека:

?f@@YAXXZ PROC						; f
; File c:\tmp6\ss.cpp
; Line 2
$LL3@f:
; Line 3
	jmp	SHORT $LL3@f
?f@@YAXXZ ENDP						; f

GCC 4.4.1 генерирует точно такой же код в обоих случаях, хотя и не предупреждает о проблеме.

Передача параметров для функции

push arg3
push arg2
push arg1
call f
add esp, 4*3

Вызываемая функция получает свои параметры также через указатель ESP.

См.также: X86 calling conventions

Важно отметить, что, в общем, никто не заставляет программеров передавать параметры именно через стек, это не является требованием к исполняемому коду. Вы можете делать это совершенно иначе, не используя стек. Однако, так традиционно сложилось, что это происходит именно через стек.

Хранение локальных переменных

Функция может выделить для себя некоторое место в стеке для локальных переменных просто отодвинув ESP глубже к концу стека.

Это снова не является необходимым требованием. Вы можете хранить локальные переменные где угодно. Но по традиции всё сложилось так.

Интересен случай с функцией alloca(). Эта функция работает как malloc(), но выделяет память прямо в стеке. Память освобождать через free() не нужно, так как эпилог функции вернет ESP на место и выделенная память просто анулируется. Интересна реализация функции alloca(). Эта функция, если упрощенно, просто сдвигает ESP вглубь стека на столько байт сколько вам нужно и возвращает ESP в качестве указателя на выделенный блок.

Попробуем:

void f()
{
	char *buf=(char*)alloca (600);
	_snprintf (buf, 600, "hi! %d, %d, %d\n", 1, 2, 3);
 
	puts (buf);
};

Компилим (MSVC 2010):

...
 
	mov	eax, 600				; 00000258H
	call	__alloca_probe_16
	mov	esi, esp
 
	push	3
	push	2
	push	1
	push	OFFSET $SG2672
	push	600					; 00000258H
	push	esi
	call	__snprintf
 
	push	esi
	call	_puts
	add	esp, 28					; 0000001cH
 
...

Параметр в alloca() передается через EAX, а не как обычно.

После вызова alloca(), ESP теперь указывает на блок в 600 байт который мы можем использовать под buf.

Реализацию функции можно посмотреть в файлах alloca16.asm и chkstk.asm в C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\crt\src\intel\

А GCC 4.4.1 обходится без вызова других функций:

                public f
f               proc near               ; CODE XREF: main+6�p
 
s               = dword ptr -10h
var_C           = dword ptr -0Ch
 
                push    ebp
                mov     ebp, esp
                sub     esp, 38h
                mov     eax, large gs:14h
                mov     [ebp+var_C], eax
                xor     eax, eax
                sub     esp, 624
                lea     eax, [esp+18h]
                add     eax, 0Fh
                shr     eax, 4           ; выровним указатель
                shl     eax, 4           ; по 16-байтной границе
                mov     [ebp+s], eax
                mov     eax, offset format ; "hi! %d, %d, %d\n"
                mov     dword ptr [esp+14h], 3
                mov     dword ptr [esp+10h], 2
                mov     dword ptr [esp+0Ch], 1
                mov     [esp+8], eax    ; format
                mov     dword ptr [esp+4], 600 ; maxlen
                mov     eax, [ebp+s]
                mov     [esp], eax      ; s
                call    _snprintf
                mov     eax, [ebp+s]
                mov     [esp], eax      ; s
                call    _puts
                mov     eax, [ebp+var_C]
                xor     eax, large gs:14h
                jz      short locret_80484EB
                call    ___stack_chk_fail
 
locret_80484EB:                         ; CODE XREF: f+70�j
                leave
                retn
f               endp

(Windows) SEH

В стеке хранятся записи SEH (Structured Exception Handling) для функции (если имеются).

О SEH: классическая статья Мэтта Питрека: http://www.microsoft.com/msj/0197/Exception/Exception.aspx

Защита от переполнений буфера

http://en.wikipedia.org/wiki/Buffer_overflow_protection

Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox