Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Ассемблер? Это просто! Учимся программировать Выпуск N 016


Служба Рассылок Subscribe.Ru проекта Citycat.Ru

Здравствуйте, уважаемые подписчики!


Выпуск N 016

Ели мясо мужики,

Пивом запива-а-али.

Что рассылка к ним пришла

Они совсем не знали...


Сегодня в рассылке:


Новости

Друзья мои! У меня в почтовом ящике собралось много ваших писем. Извините, у меня просто сейчас нет времени отвечать на них. Очень надеюсь, что, разобравшись с делами, я всем напишу. Извините еще раз...

__________

Старчиков Алексей ( vunder@spb.cityline.ru ) написал программу в помощь программистам на Ассемблерам. Как сообщает сам Алексей, "данная программа признана лучшей в номинации "Программное обеспечение под Windows". Вы можете скачать ее по адресу: http://www.oleg77.newmail.ru/Assembler. Однако Алексей просил пользователей данной программы сообщить ему координаты.


Как работает отладчик

Для дальнейшего изучения Ассемблера, нам нужно рассмотреть, как работает простейший отладчик (таковым является AFD. Было бы хорошо, если бы вы взяли его на нашем сайте и поэкспериментировали с ним).

Интересно, а вы задумывались над тем, как отладчик выполняет программу пошагово? Можно ли его обмануть?

Естественно, дорогие мои! Обмануть отладчик на Ассемблере очень просто. Мы уже рассматривали с вами два способа:

1. Перенести стек в тело программы;

2. Прочитать саму себя заново с диска.

Однако, второй способ мы так и не разобрали. Почему же отладчик работал неверно, если мы читали саму себя в память?

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

Прерывание 03.

Вы уже знаете, что существует ряд прерываний, выполняющих те или иные функции при возникновении некоторых ситуации. Например:

  • при нажатии на клавиши Shift+PrintScreen вызывается 05 прерывание;
  • при нажатии на любую клавишу вызывается 09 прерывание;
  • прерывание же 1Ch вообще вызывается автоматически примерно 18 раз в секунду.

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

Вообще номера прерываний от 0 до 1Fh "обслуживаются" BIOS (ПЗУ). Все остальные доступны программисту или операционной системе. Например, MS-DOS использует номера от 20h до 2Fh (int 20h - выход из программы; int 21h - комплекс процедур и т.д.).

Что значит "обслуживаются ПЗУ"? Это значит, что обработчики этих прерываний находятся в области BIOS (ПЗУ- Постоянное Запоминающее Устройство), в то время, как обработчики 20h - 2Fh и до 0FFh находятся в ОЗУ - Оперативное Запоминающее Устройство (т.е. та область памяти, которая теряется при выключении / перезагрузке компьютера в отличие от ПЗУ). Естественно, мы можем перехватить как прерывания ПЗУ (от 0 до 1Fh), так и все остальные, что мы уже делали.

Прерывание с номером 3 примечательно тем, что:

  • во-первых, используется для отладки программ, в частности, для работы AFD и CV;
  • во-вторых, для его вызова необходим всего один байт - 0CCh, чего не скажешь о вызове других прерываний. Например, int 20h - 0CDh 20h, т.е. два байта;
  • в-третьих, обработчик 03 прерывания изначально содержит всего одну инструкцию: iret, т.е. при вызове данного прерывания происходит моментальный возврат. Можете, кстати, проверить, принудительно вызвав данное прерывание в любом месте вашей программы. Ничего не произойдет.

Пояснение. Принудительно вызвать прерывание 03 - записать в любом месте нашей программы int 3.

При отладке программы отладчик перехватывает 03h прерывание. Проще говоря, устанавливает вектор (адрес) данного прерывания на некую свою процедуру. Выполняя одну команду (когда пользователь нажимает клавиши F1 или F2), отладчик просто сохраняет следующий за текущей командой байт и вместо него вписывает 0CCh, т.е. int 3. Естественно, что на экран отладчик выводит данную инструкцию в ее нормальном, первозданном виде, а не int 3.

Все просто смотрится на примере. Возьмем такую простейшую программу, которая выводит один символ 'Q' на экран в текущую позицию курсора:

_______

cseg segment
assume cs:cseg, ds:cseg, es:cseg, ss:cseg
org 100h

Begin:
mov ah,2
mov dl,'Q'
int 21h

ret

cseg ends
end Begin

_______

Вот как это выглядит в отладчике (только что загрузили ее под AFD):

  Смещение Инструкция Ассемблера Машинный код Что на экране
» 0100h mov ah,2 0B402h MOV AH,02
  0102h mov dl,'Q' 0B251h MOV DL,51
  0104h int 21h 0CD21h INT 21
  0106h ret 0C3h RET

Таблица N 1. Шаг первый: только что загрузились

Примечание. Зеленым цветом (или символом '»') помечена текущая команда, т.е. команда, которая выполнится после нажатия клавиши F1/F2 в AFD.

В колонке "Смещение" указываем, по какому смещению находится в памяти команда. В колонке "Инструкция Ассемблера" - реальные ассемблерные команды, расположенные по соответствующему смещению, а в колонке "Машинный код" - машинный код команды в шестнадцатеричной системе из колонки "Инструкция Ассемблера". Колонка "Что на экране" отражает то, что отладчик показывает нам на экране, а также оригинальный код команд.

________

К слову.

Посмотреть машинные коды соответствующих команд Ассемблера можно в любом отладчике, а лучше - в Hacker's View.

Реальные ассемблерные команды - команды, которые на самом деле находятся в памяти по соответствующему смещению (читайте дальше - все будет понятно).

________

Итак, запускаем нашу программу под отладчиком AFD. Команда mov ah,2 расположится по адресу 100h, а mov dl,'Q' - 102h (см. Таблицу N 1). Естественно, что отладчик сразу не даст программе работать, а просто загрузит ее в память и выведет на экран инструкции Ассемблера.

Внимание! Пользователь нажимает F2. AFD запоминает один байт по адресу 102h (первый байт следующей за mov ah,2 команды. В нашем случае - 0B2h), записывает туда 0CCh, т.е. команду int 3 и выполняет инструкцию mov ah,2. После этого процессор выполнит не команду mov dl,'Q', а int 3 (вызывет прерывание 03, адрес которого указывает на определенную процедуру обработки отладчика AFD (отладчик-то перехватывает это прерывание сразу после загрузки!)).

Вот, что получится, когда пользователь нажмет клавишу F2:

  Смещение Инструкция Ассемблера Машинный код Что на экране
  0100h mov ah,2 0B402h MOV AH,02 0B402h
» 0102h int 3 0CCh MOV DL,51 0B251h
  0103h push cx 51h ---- ----
  0104h int 21h 0CD21h INT 21 0CD21h
  0106h ret 0C3h RET 0C3h

Таблица N 2. Нажали F2 первый раз.

Что делает процедура обработки 03h прерывание? Допустим, текущая команда находится по адресу 0102h (см. Таблицу N 2). Пользователь нажимает F1/F2. Прерывание 03 делает следующее:

  1. Сохраняет все изменяемые отладчиком регистры в памяти;
  2. Восстанавливает сохраненный байт (0B2h) по адресу 102h;
  3. Высчитывает количество байт следующей команды (т.е. mov dl,'Q', т.е. два байта, т.е. 0B251h);
  4. Получив адрес следующей за mov dl,'Q' команды (у нас - 104h), заносит по этому адресу значение 0CCh (т.е. int 3), предварительно сохранив затертый байт (у нас - 0CDh) в своей переменной;
  5. Выполняется инструкция mov dl,'Q' и за ней же сразу int 3, которая и передаст управление отладчику (процедуре обработки 03 прерывания);
  6. Процедура обрабоки 03 изменяет кое-что на экране;
  7. Выполняет некоторые другие действия (все зависит от конкретного отладчика);
  8. И ждет от вас дальнейших указаний (т.е. клавишу!).

Вот, что получится, когда пользователь второй раз нажмет F2:

  Смещение Инструкция Ассемблера Машинный код Что на экране
  0100h mov ah,2 0B402h MOV AH,02 0B402h
  0102h mov dl,'Q' 0B251h MOV DL,51 0B251h
» 0104h int 3 0CCh INT 21 0CD21h
  0105h and bx,ax 21C3h RET 0C3h
  0107h "мусор" "мусор" "мусор"  

Таблица N 3. Нажали F2 второй раз.

Обратите внимание, какой машинный код находится по адресу 0105h - 21C3h. Перед ним, по адресу 0104h, находится код 0CCh (int 3).

Как вы уже поняли, команда Ассемблера может занимать один, два и более байт (например, mov ah,2 - 0B402h - два байта; ret - 0C3h - один байт). Если мы "вручную" в процессе работы рассматриваемой нами программы заменим 0B4h на, скажем, 0C3h, то вместо mov ah,2 мы получим ret (машинный код которой 0C3h). Но команда ret однобайтовая в отличие от mov ah,2. Куда же денется второй байт команды mov ah,2 (02h)? Процессор просто поймет 02 как add dh,[CD51+BP+SI]. Но т.к. данная команда занимает аж 4 байта, то процессор "присоединит" к 02h еще и 0B2h, 51h, 0CDh. Все. Код пошел путаться...

Вот что происходит по смещению 105h в Таблице 3! Процессор распознал 21h как and. Но после and должны идти приемник и источник (например, and ax,11b). Следовательно, процессор возьмет следующий за 21h байт, которым будет 0C3h. 21C3h как раз и есть AND BX,AX! Похоже на кривое зеркало или "испорченный телефон" (игра такая была в моей молодости)...

Отсюда и появляется по смещению 103h какой-то push cx в Таблице N 2.

Вывод: если вы "на лету" меняете код своей же программы (по разным причинам), имейте в виду, что однобайтовую команду нужно менять на однобайтовую, двухбайтовую на двухбайтовую и т.д. Можно, конечно, менять две однобайтовые на одну двухбайтовую и пр. Можно даже, изменив всего один байт, сделать всю программу (или ее часть) другой. Т.е., например, в Hacker's View вы видите один код, а выполняет программа совсем другие действия. Но это вы еще поймете при экспериментах...

Еще вопрос: почему же тогда отладчик работает нормально, "искриввив" код? Неужели вы не знаете? Int 3 ведь восстанавливает код, который был изменен!

Получим так:

  Смещение Инструкция Ассемблера Машинный код Что на экране
  0100h mov ah,2 0B402h MOV AH,02 0B402h
  0102h mov dl,'Q' 0B251h MOV DL,51 0B251h
  0104h int 21h 0CD21h INT 21 0CD21h
» 0105h int 3 0CCh RET 0C3h

Таблица N 4. Нажали F2 третий раз.

Хорошо получилось здесь: ret и int 3 занимают по одному байту.

И последняя таблица:

  Смещение Инструкция Ассемблера Машинный код Что на экране
» 0100h mov ah,2 0B402h MOV AH,02
  0102h mov dl,'Q' 0B251h MOV DL,51
  0104h int 21h 0CD21h INT 21
  0106h ret 0C3h RET

Таблица N 5. Нажали F2 четвертый и последний раз. Пришли к тому, с чего начинали (Таблица N 1). Но не забываем про int 20h!

Отладик пишет: "Program terminated OK".

Всё!


Способы обойти отладку программы

Как же нам сделать так, чтобы AFD не смог производить отладку нашей программы (помимо двух способов, которые мы уже рассмотрели (см. начало настоящего выпуска))?

Очень легко! Рассуждаем --> AFD перехватывает 3 прерывание --> AFD вставляет 0CCh (int 3) после каждой команды --> Процедура обрабоки 03 прерывания AFD постоянно останавливает нашу программу...

Подумайте...

Подсказка: нужно сделать что-то с обработчиком 03 прерывания...

Подумайте, подумайте...

Код команды iret - 0СFh...

Думайте, думайте... Я пока за пивком сгоняю...

...

"Ах, хорошо!" - сказал Олег, глотнув своей любимой "Балтики"...

Надеюсь, что многие нашли не один, а несколько способов. Рассмотрим один.

Нам нужно записать в самое начало обработчика 03 прерывания команду iret. Один из способов получения адреса обработчика того или иного прерывания нам известен: функция 35h прерывания 21h.

Однако, можно получить и поменять адрес не прибегая к функциям 35h и 25h 21h-ого прерывания, хотя Microsoft делать так не советует. Но мы все равно рассмотрим.

Все текущие адреса обработчиков прерываний находятся в так называемой таблице векторов прерываний, которая расположена по адресу 0000:0000h. Как вы знаете, адрес обработчика любого прерывания занимает 4 байта: сегмент (2 байта) и смещение (2 байта), причем первые два байта - это смещение, а вторые - сегмент (мы уже знаем, что все данные в компьютере храняться "задом наперед", включая расположение сегмент: смещение). Вспомните что возвращает функция 35h прерывания 21h. Т.о. адрес нулевого прерывания будет здесь: 0000:0000h, первого - 0000:0004h, второго - 0000:0008h и т.д. Вот пример чтения адреса обработчика 21h-ого прерывания из таблицы векторов прерываний:

_____________

...

xor ax,ax
mov es,ax ;Аннулируем ES

mov bx,es:[21h*4] ;В BX - смещение
mov es,es:[21h*4+2] ;В ES - сегмент

;Сохраним адрес 21h-ого на будущее
mov Int_21h_offset,bx ;Сохраним смещение
mov Int_21h_segment,es ;Сохраним сегмент

mov ah,2
mov dl,'!'
pushf ;Зачем здесь pushf - вы знаете...
call dword ptr [Int_21h_offset] ;Равносильно int 21h

...

Int_21h_offset dw ?
Int_21h_segment dw ?

...

_____________

Думаю, что пример не нуждается в пояснении. Однако, обратите внимание на следующие строки:

mov bx,es:[21h*4]
mov es,es:[21h*4+2]

Ассемблер, в отличие от языков высокого уровня, вычислит выражение в квадратных скобках всего один раз. Т.е. процессор, выполняя приведенные выше строки, не будет постоянно вычислять, сколько будет 21h*4+2. А как такое возможно?

Для начала вычислим сами: 21h*4+2=134 (86h). Подобные выражения, встречающиеся в программе, вычисляются на стадии ассемблирования, т.е. программой-ассемблером (MASM / TASM). Запустив программу под отладчиком, мы увидим, что строка mov es,es:[21h*4+2] будет отображаться как mov es,es:[86h]. Учитывая это, можно смело писать, например, так:

mov ax,(23+5)*3

mov cx,34h+98/2

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

А зачем это нужно? Неужели самому сложно высчитать и записать результат? Ответ прост: для удобства и наглядности в процессе написания программы. В нашем случае мы сразу видим, что в ES заносится сегмент 21h-прерывания:

mov es,es:[21h*4+2]

21h - прерывание; +2 - сегмент.

А если бы так было записано: mov es,es:[86h], то нам пришлось бы тогда постоянно высчитывать, вспоминать, что именно мы загружаем в ES. Можно, конечно, делать пометки в виде примечаний. Но это уж на ваше усмотрение: кому как удобней...

Имейте только в виду, что выражения вида mov ax,[bx+di] остаются как есть! Откуда знать ассемблеру (MASM / TASM), какие значения находятся в регистрах BX и DI в момент ассемблирования? В данном выражении в AX поместится сумма чисел, которые в момент выполнения этой команды находятся в регистрах BX и DI.

А зачем перед [21h*4+2] стоит регистр ES? Конечно, можно опустить ES, но тогда мы получим не адрес 21h-ого прерывания, а что-то совсем другое.

ES указывает на то, что нужно загрузить слово в ES, которое расположено по смещению 21h*4+2, причем из сегмента не какого-нибудь, а именно из того, который указан в ES. Поэтому мы, собственно, и загружали в начале нуль в ES (а по какому сегменту находится таблица векторов прерываний? См. выше):

xor ax,ax
mov es,ax ;Аннулируем ES
...
mov es,es:[21h*4+2] ;В ES - сегмент

_____________

Давайте вернемся к отладчику. Итак, нам нужно занести в первый байт процедуры обработки прерывания 03 команду iret, тем самым "вырубив" отладчик. Прежде получим адрес обработчика прерывания 03:

xor ax,ax
mov es,ax

mov bx,es:[03h*4] ;В BX - смещение
mov es,es:[03h*4+2] ;В ES - сегмент

Теперь занесем iret (машинный код iret - 0CFh) в самое начало обработчика:

mov byte ptr es:[bx],0CFh

Отладчик в нокауте!

_________

Для того, чтобы лучше понять работу отладчика в файле-приложении найдите "!DEBUG.ASM". Что нужно сделать:

  1. Изучите описания в данном файле;
  2. Получите "!DEBUG.COM";
  3. Запустите его из DOS;
  4. Запустите файл под отладчиком AFD или CodeView;
  5. Обратите внимание, что после команды call следует nop;
  6. В AFD нажмите 4 раза F2 (до тех пор, пока не появится надпись "Program terminated OK");
  7. В CodeView нажмите 4 раза F10 (до тех пор, пока не появится надпись "Process XXXX terminated normally");
  8. Выйдите из отладчика;
  9. Запустите снова файл под отладчиком. Все внимание на команду NOP, если таковая имеется!

Вот вам и доказательства...

___________

Усвоив приведенный в данной рассылке материал, надеюсь, что вы без труда поймете, почему при перечитывании самих себя в память, отладчик работает неверно. Причем, что именно он делает?

Вот еще таблица (если вы, конечно, не устали от них):

  Смещение Инструкция Ассемблера Машинный код Что на экране
» 0120h int 21h 0CD21h INT 21
  0122h int 3 0CCh RET

Здесь приведен кусок программы, которая считывает себя в память. Представьте, что полный код такой:

...

mov ah,3Fh

mov bx,Handle

mov cx,offset Finish-100h

mov dx,offset Begin

int 21h ;Эта строка...

ret ;и эта строка приведены в таблице.

...

Внимательно посмотрите на колонки "Инструкция Ассемблера" и "Что на экране". Подумайте, что произойдет, если мы колонку "Что на экране" скопируем в колонку "Инструкция Ассемблера"? В таком случае просто затрется int 3 (0CCh). То же самое происходит, если мы читаем свою программу в память с диска. На диске ведь вместо int 3 будет ret! А как отладчик получит управление, если int 3 затирается?


В приложении к данной рассылке вы найдете файлы типа Dbg16_01.asm. Посмотрите их, почитайте описания. Думаю, что многое станет понятным.

Файл-приложение можно взять здесь: http://www.oleg77.newmail.ru/Assembler/Programs/Lessons/Debug16.rar


С уважением,

Автор рассылки:

Калашников Олег

www.oleg77.newmail.ru

E-mail:

assembler@beep.ru

UIN (Тетя Ася):

68951340

(С) Авторское право. Запрещается использование материала из рассылки в коммерческих целях без письменного согласия автора.


http://subscribe.ru/
E-mail: ask@subscribe.ru
Поиск

В избранное