Дневники чайника. Чтива 0, виток0

Стек.
Восьмой день

Стек - специально выделенная область памяти для передачи или сохранения данных.

Мы всё время наблюдаем такую форму записи (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 - толкать
Форматpush операнд
ДействиеТолкает значение в стек
ПримечаниеЕсли нужно толкнуть в стек все 8 регистров общего назначения (EAX, EBX, ECX, EDX, EBP, ESP, ESI, EDI), то лучше использовать одну команду PUSHA/PUSHAD

Чтоб достать число с "верхушки стека" куда-то, пишите:

pop куда-то
Команда 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

<<предыдущая глава     следующая глава>>

Вернуться на главную

Hosted by uCoz