Стек - специально выделенная область памяти для передачи или сохранения данных.
Мы всё время наблюдаем такую форму записи (prax03.com):
00000000: B80300 mov ax,00003 00000003: CD10 int 010 00000005: B402 mov ah,002 00000007: 8B167501 mov dx,[0175] 0000000B: CD10 int 010 0000000D: FEC6 inc dh 0000000F: 80C203 add dl,003 00000012: 89167501 mov [0175],dx 00000016: B409 mov ah,009 00000018: BA5001 mov dx,00150 0000001B: CD21 int 021 0000001D: 803E760119 cmp b,[0176],019 00000022: 75E1 jne 000000005 00000024: B410 mov ah,010 00000026: CD16 int 016 00000028: CD20 int 020
Я имею в виду, что адреса растут вниз. 0 - выше всех (как первая строка в книге), 28 - нижняя строчка.
Так вот в этой системе отображения стек растёт вверх.
Дно стека находится по самому старшему адресу, а вершина - по самому младшему адресу.
На вершину стека указывает регистр-указатель ESP - это его назначение (Stack Pointer - указатель стека).
Видите, prax03 в строке 12h сохраняет значение регистра DX, а в строке 07 при следующем проходе цикла обратно восстанавливает это значение. В данном примере стек использовать неудобно, но если в программе нужно много раз сохранять разные регистры, то лучше делать это через стек. Для записи в стек есть команда PUSH.
Пример:
push EDX ; толкнуть в стек (положить в стек) ... pop EDX ; извлечь из стека в EDX (вытолкнуть в EDX)
Другой пример:
push EDX ; положить в стек ... pop EAX ; извлечь из стека в EAX (в EAX будет то, что было в EDX)
Причём значений в стек можно укладывать очень много.
push EAX ; положить в стек push EBX ; положить в стек push ECX ; положить в стек push EDX ; положить в стек push 01234h ; положить в стек ... pop ESI ; извлечь из стека (значение 01234h было сверху, значит, оно и выйдет) pop EDX ; извлечь из стека в EDX pop ECX ; извлечь из стека в ECX pop EBX ; извлечь из стека в EBX pop EAX ; извлечь из стека в EAX
Обратите внимание, что значения извлекаются в обратном порядке. Вершина выходит первой, а дно последним. На практике, слава Богу, мудрить со стеком придётся мало, но если вы не уловите принцип, то писать программы будет трудновато.
Матрос! Ну-ка иди сюда... Что тут у тебя в корзинке? О, завтрак! Так, сверху яйца, подержи, бутерброд, бутылка... подушка? А зачем тебе подушка? Ты что, на вахте спать собрался! А ну, отдай сюда корзину. Яйца-то сырые, чёрт тебя дери! И бутерброды измазал, и подушку испортил. Какого шлейфа ты их не сварил? Иди отсюда, сам знаю, что яйца сверху кладут.
Память под стек выделяет Windows.
Теперь осмысленно вернёмся к нашей первой программе под форточки:
Адреса байты имена операнды комментарии 00401000 6A 00 push 0 ; /Style = MB_OK|MB_APPLMODAL 00401002 68 00304000 push 00403000 ; |Title = "It's the first your program for Win32" 00401007 68 21304000 push 00403021 ; |Text = "Assembler language for Windows is a fable!" 0040100C 6A 00 push 0 ; |hOwner = NULL 0040100E E8 07000000 call < jmp.&user32.MessageBoxA> ; \MessageBoxA 00401013 6A 00 push 0 ; /ExitCode = 0 00401015 E8 06000000 call < jmp.&kernel32.ExitProcess> ; \ExitProcess 0040101A FF25 08204000 jmp dword ptr ds:[< user32.MessageBo> ; user32.MessageBoxA 00401020 FF25 00204000 jmp dword ptr ds:[< kernel32.ExitPro> ; kernel32.ExitProcess
Команда Push кладёт в стек указанное в ней значение. Оно может быть два байта или четыре. В 32-битных программах это будет 32-битное значение, то есть dword (4 байта). Так что для нас теперь одно значение в стеке 4 байта.
Push 0 ; отправляет в стек значение 00000000 Push 00403000 ; отправляет в стек значение 00403000 Push 00403021 ; отправляет в стек значение 00403021 Push 0 ; отправляет в стек значение 00000000
Это, естественно, целое и для "удобства" чтения, младший байт уже справа, старший слева.
Матрос, ты запомнил, что на наших чертежах в целой боевой единице Бинарников байты строятся по старшинству слева направо?
Каждый младший - самый левый, каждый старший - самый правый (на фиг такие чертёжи :).
У них там в главном штабе есть ещё запутка - стек.
Он в чертежах идёт от дна наверх. Целые (word или dword) в нём укладываются именно так: от дна наверх.
Действительно, стек проще представлять в высоту, чем в одну строку. Вот что будет содержаться в стеке перед выполнением строки 40100Eh в нашей программе.
00000000 00403021 00403000 00000000 ........
Каждая строка здесь - одно значение в стеке. Понятие дно очень наглядно объясняет, как устроен стек.
Давайте удалим из исходника эту строку:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
И впишем вместо неё вот такие команды:
push 0AAAAAAAAh push 0BBBBBBBBh push 0CCCCCCCCh push 0DDDDDDDDh pop EAX pop ECX pop EDX pop EBX
Соберите программу ещё раз. Откройте в Olly.
То, что вы делали в отладчике CodeView клавишей F10, называется по-умному:
пошаговая трассировка исполняемого кода без захода в процедуры. В Olly такой "шаг" выполняет клавиша F8.
Адреса Байты Имена Операнды 00401000 68 AAAAAAAA push AAAAAAAA 00401005 68 BBBBBBBB push BBBBBBBB 0040100A 68 CCCCCCCC push CCCCCCCC 0040100F 68 DDDDDDDD push DDDDDDDD 00401014 58 pop eax 00401015 59 pop ecx 00401016 5A pop edx 00401017 5B pop ebx
Когда вы дойдёте до строки 401014h (F8 четыре раза), стек будет выглядеть вот так:
0012FFB4 DDDDDDDD 0012FFB8 CCCCCCCC 0012FFBC BBBBBBBB 0012FFC0 AAAAAAAA ... ...
Посмотрите в отладчике, обязательно! Нижняя правая часть.
Вы должны были понять, что каждое новое значение укладывается сверху. Получается, стек растёт вверх. Получается, регистр ESP с каждым новым значением стека уменьшается. И каждый раз, когда из стека извлекают значение, ESP увеличивается на 4 (или на 2 в 16-битных программах).
А ещё стек нужно выравнивать, иначе программа будет работать неправильно. После того, как ваши данные в стеке отработали или просто больше не нужны, возвращайте вершину в положение, которое она занимала раньше - это и есть выравнивание. Если вы так не сделаете, программа вызовет ошибку.
Стек, пожалуй, самое сложное понятие, которое нужно усвоить для написания программ на Ассемблере. Честно-честно, если вы поймёте это, значит, вы точно сможете освоить всё остальное. Конечно, нужна практика, помню, я три дня потратил на понимание стека. Калашников обманул, сказал, что это просто. Так и было, когда я его примеры использовал, а вот когда свои программки стал писать, стек вызвал наибольшие затруднения.
Три дня - думаю, это немного для самого сложного фокуса.
Чтоб положить что-то в стек, пишите:
push что-то
Происхождение | От англ. слова push - толкать |
Формат | push операнд |
Действие | Толкает значение в стек |
Примечание | Если нужно толкнуть в стек все 8 регистров общего назначения (EAX, EBX, ECX, EDX, EBP, ESP, ESI, EDI), то лучше использовать одну команду PUSHA/PUSHAD |
Чтоб достать число с "верхушки стека" куда-то, пишите:
pop куда-то
Происхождение | От англ. слова pop - извлекать, раскошеливаться |
Формат | POP операнд |
Действие | Извлекает значение из стека |
Примечание | Если нужно достать из стека все РОН, уложенные командой PUSHA, используйте POPA/POPAD |
Следующий пример (prax06.com).
Наберите в Hiew'e эти байты:
B8 CD 20 50-EB FD
Теперь посмотрите на асмовый вид:
Адреса Байты Имена Операнды 00000000: B8CD20 mov ax,020CD 00000003: 50 push ax 00000004: EBFD jmps 000000003
Давайте проанализируем этот пример.
00000000: B8CD20 mov ax,020CD
Эта строка помещает в регистр AX значение 20CD.
00000003: 50 push ax
Эта строка кладёт в стек word (слово) 20CDh из регистра EAX. Причём в памяти слово вы увидите как положено - CD,20h.
00000004: EBFD jmps 000000003
А вот эта строка зацикливает выполнение строки с адресом 03. То есть после выполнения строки 03 в строке 04 будет совершаться безусловный переход к строке 03.
Так почему же этот пример не вешает комп в ДОС и не виснет сам в WinXP? Запустите и вы увидите, что прога выполнится за доли секунды.
Можете попробовать разобрать хитрость в CV сами, но это займёт некоторое время :).
Честно скажу, что отладчик типа CodeView не очень подходит для разбора этого пустячка.
Дело в том, что простые DOS-отладчики используют стек программы в своих целях, что сбивает значение регистра ESP между шагами. Здесь бы лучше всего запустить пример под SoftIce'ом. И тогда вы могли бы "ровно" увидеть, как программа 32637 раз запишет в стек два байта CD20h. Последний раз байты будут вписаны вместо команды "jmp 03".
CD20h - это машинный код команды int 20h (вызов прерывания завершения программ, как вы уже знаете).
Стек в com-программе начинается на дне её сегмента (FFFE - последний адрес, кратный двум).
Получается, что стек всё растёт и растёт вверх и ничего его не остановит, кроме завершающего или ошибочного кода.
И вот он заполняет всё свободное место в сегменте, а затем начинает затирать данные (в этом примере их нет). Далее затирается код программы.
Такая ситуация называется ошибкой переполнения стека. Для DOS-программ данная ошибка - одна из самых распространённых.
В Win32-программах механизм выделения стека более развит (ОСь сама занимается этим вопросом, используя средства защищённого режима). Однако ошибки переполнения стеков - всё ещё настоящий вызов для программистов, особенно в сфере безопасности. Вы только что могли наблюдать, как из-за такой ошибки стек начинает выполняться.
Ведь изначально программа просто отправляла данные CD20h в стек. Но затем стек вырос (адрес в ESP уменьшился!) до значения, равного адресу команды JMP 0103 (в CodeView этого не видно!). И вот, после следующего выполнения команды push AX, происходит чудесное превращение данных в исполняемый код.
В CodeView тоже можно проследить самый интересный момент этого примера.
Измените регистр указателя стека (ESP). Например, установите в нём значение 200h (чтоб долго не трейсить).
После этого - F10 много раз, и прога завершится.
Посмотрите ещё и ещё раз. Может быть, вы придумаете, как такой фокус можно использовать в своих целях ;).
Предлагаю вам ещё один пример Win32.
Учтите, что русские буквы должны быть в кодировке ANSI (стандарт для форточек).
prax07.asm:
.386 .model flat, stdcall option casemap :none ; case sensitive ;######################################################################### include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib ;######################################################################### .data MsgBoxCaption db "It's my first command line for Win32",0 MsgBoxText db "Аргументы командной строки отсутствуют",0 ;######################################################################### .code start: call GetCommandLine ; API. Возвращает в EAX адрес командной строки mov ECX,512d add ECX,EAX unquote: ; Метка цикла нахождения закрывающей кавычки inc EAX cmp EAX,ECX jz NO cmp byte ptr[EAX],22h jnz unquote Arg_search: ; Метка цикла нахождения аргумента командной строки inc EAX cmp byte ptr[EAX],0 jz NO cmp byte ptr[EAX],20h jz Arg_search push 0 push offset MsgBoxCaption push EAX push 0 call MessageBox ; Вызов API-функции вывода сообщения на экран push 0 ; Пустой параметр для функции выхода call ExitProcess ; Вызов API-функции выхода NO: push 0 ; Параметр "MB_OK" push offset MsgBoxCaption ; Параметр "адрес заголовка" push offset MsgBoxText ; Параметр "адрес текста сообщения" push 0 ; Параметр "родительское окно" call MessageBox ; Вызов API-функции вывода сообщения на экран push 0 ; Пустой параметр для функции выхода call ExitProcess ; Вызов API-функции выхода end start
Наберите, пожалуйста, сами секцию кода. Я уверен, что у вас будут опечатки, и это очень ускорит процесс обучения.
Для того, чтоб пример сделал то, что ему положено, запустите его с каким-нибудь ключом типа: prax07.exe qwerty |
Ну что ж, отладчик Olly - ваш лучший друг. При открытии впишите ключ в поле "Arguments". Не торопитесь, подумайте что к чему.
Здесь используются 3 API-функции:
Осталась только одна команда, которую вы пока не знаете в этой программе.
Команда CALL, но это целая тема. И, между прочим, последняя теоретическая тема в наших уроках.
Bitfry