Стек
From Wiki.conus.info
Стек - это очень фундаментальная вещь.
Технически, это просто кусок памяти процесса + регистр 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