Калашников.ru - Ассемблер? Это просто!.. (Выпуск № 012)

InterReklama Advertising

Здравствуйте, здравствуйте, дорогие мои!

Выпуск N 012

Кваса. Снова кваса глоток хлебану.

Знаю, что рассылку сейчас получу.

Нужно побыстрее мне в Сеть лишь войти.

Чтоб там новый выпуск найти.

________

Где ж ты моя, долгожданная, где?

Слюни текут уже по бороде.

Вот ты! Ща тебя я прочту.

Что у нас сегодня?

Новость Ваши письма Резидент

Новость

Друзья мои! У нас существенно изменился внешний вид сайта. В этом мне помог BoB, за что ему огромное спасибо, а также море благодарности.

Теперь там есть Гостевая книга, где вы можете лично оставить свое впечатление, пожелание, критику и прочее.

Появился также раздел писем, которые вы можете отправлять сами на сайт (например, решение программы, мысли, заметки и прочее).

Единственная проблема: закачать все это дело на сайт www.Kalashnikoff.ru . Думаю, что на этой неделе я все сделаю...

Если вам интересно, то можете попозже посмотреть одним глазком. Все ваши отклики принимаются тут: assembler@beep.ru?Subject=Сайтик

Ваши письма

Я уже не раз писал, что ко мне очень много приходит писем. Если бы в сутках было 124 часа, то я, возможно, смог бы всем ответить. Я прошу прощения, если кто-то не получил ответ. У меня действительно нет времени отвечать на все письма.

Так как рассылка уже выходит относительно давно, то у меня сложилась определенная система ответа на ваши письма.

Итак, в первую очередь я отвечаю на письма:

с просьбой выслать предыдущие выпуски рассылки;

с просьбой выслать shell.asm, virus.asm, resident.asm и прочие программы на Ассемблере, которые необходимы для работы;

с просьбой выслать программное обеспечение небольшого объема (afd, hiew и пр.).

Во вторую очередь я отвечаю на следующие письма:

"написал программу из выпуска Х, а она не работает. Я делал так-то и так-то, у меня такой-то компьютер (386, Pentium...), такой-то ассемблер (MASM, TASM версии х.хх), такая-то ОС (MS-DOS, Windows 95/98). Набранную мной программу прилагаю. Найдите и исправьте мои ошибки."

"Не работает MASM (TASM, AFD, HIEW). Пишет то-то и то-то. Я запускаю его так-то и так-то. Что делать?"

Я НЕ отвечаю на письма такого плана:

"Я сам тракторист. В глаза компьютер никогда не видел. Неделю назад мне достался в наследство от прабабушки Pentium-III 800. Пожалуйста, напишите подробно, что нужно сделать, чтобы он заработал (какие провода куда повтыкать, какую кнопку нажать, какие программы установить и пр.). Прошу также выслать на e-mail моему другу ВСЁ необходимое программное обеспечение (Windows 2000, MS Office 2000 и прочее). С уважением, Изя Абдулаев. P.S. Где купить еще один винчестер, и - главное - как его вставить в компьютер? У моего деда есть один, но он ходит с ним на охоту каждый день и мне не дает."

"Я подписался на вашу рассылку недавно. Одна беда: никогда не работал с DOS. Напишите подробно, как мне загрузить Norton Commander (DOS Navigator), как в нем создавать и удалять файлы, как их редактировать. И вообще, что такое командная строка? С уважением, ученик 3-го класса, Чайников Т."

"Я пишу программу по управлению спутниками. Пожалуйста, напишите мне процедуру, которая бы выводила спутник на орбиту. А то что-то у меня ничего не получается. С уважением, космонавт Невесомов."

"Здорово, братан! Помоги мне хакнуть банк "Кидалово". Хочу перевести бабки одного мужика на свой счет. Жду ответ! Вован Безбазаров."

Подобные письма приходят пачками. Я просто физически не могу ответь на все. Еще не забывайте, что у меня работа, учеба, сын и куча проблем. Пожалуйста, не обижайтесь, если я кому-нибудь не отвечу.

Единственное, что могу посоветовать: купите книгу по DOS и читайте. Ничего сложного там нет.

Еще один момент. Т.к. я пользуюсь бесплатными ящиками (beep.ru, hotmail.ru), которые не несут ответственность за предоставляемые услуги, то, возможно, что ваше письмо не дойдет до меня. Если я не отвечу в течение 3 дней, то вышлите его повторно, пожалуйста.

______________

ВНИМАНИЕ! У меня довольно-таки много адресов находится в моей адресной книге Outlook'а. Не исключено, что у меня может появиться вирус, который я сразу и не замечу. Пожалуйста, не открывайте письма, которые могут прийти от моего имени с таким содержанием в поле "Тема" (или примерно таким):

"Вышлите мне стольник и получите лимон!"

"Все козлы, а я - Д'Артаньян!"

"Посмотри мои фотки в приложении!"

"Предлагаю крутую работу за сумасшедшие деньги! Подробности в приложении."

и прочее. Я таких писем не отправляю и их не читаю, чего и вам советую. Если же все-таки к вам пришло подобное письмо от моего имени, то, пожалуйста, НЕМЕДЛЕННО сообщите мне об этом. Буду очень признателен.

_____________

Я получил письмо от одного из подписчиков на позапрошлой неделе, который указал на ошибку в выпуске N 009 (когда читали сами себя в память). Эх, какую я допустил досадную логическую ошибку! А заметил-то всего один подписчик, который мне долго объяснял ее суть. Ай-да, молодец, Серега! Итак, вот оно, это письмо:

Здравствуйте, Олег! Простите за назойливость, но....

Боюсь, что я не очень правильно поставил вопрос. Попробую сначала.

Thursday, October 05, 2000, 1:24:51 PM, you wrote:

КО> | Open_File proc

КО> | cmp Handle, 0FFFFh ;Если в Hanlde не 0FFFFh (см. ниже) то снова открывать не надо.

КО> | jne Quit_open

КО> | mov ax, 3D00h      ; открываем на чтение

КО> | int 21h

КО> | mov Handle, ax     ;спасаем handle

КО> Теперь-то Handle у нас не 0FFFFh!!! Поэтому, если мы попытаемся закрыть файл, то прежде проверим, открыт ли он был (равно ли Handle 0FFFFh; если да, то закрывать нечего!!!!)

Согласен. Трудно не согласиться.

КО> Вот здесь проверяем на то, содержит ли Handle 0FFFFh. Т.е. окрыт ли файл на самом деле или нет.

Да, но между вызовами процедуры open_file и close_file мы перечитываем

файл и это-то и меняет handle!

А именно, по-моему, события происходят так

Действие                    Handle (после)

---------------------------------------------------

начало                      FFFF

open_file                   1 (не важно что, главное что не FFFF)

чтение файла              FFFF! Вот про это я и говорю! См. ниже

close_file                   FFFF

Попробуйте заменить процедуру закрытия на следующую (просто добавить вывод 2х строк) - то, что я добавил справа помечено ';///':

Close_File proc

        cmp Handle, 0FFFFh

        je No_Close

        mov ah, 3Eh

        mov bx, Handle

        int 21h

        mov Handle, 0FFFFh

        mov ah, 9h                        ;///

        mov dx, offset Mess_Close_ok      ;///

        int 21h                           ;///

        No_Close:

        mov ah, 9h                        ;///

        mov dx, offset Mess_Close_Nok     ;///

        int 21h                           ;///

        ret

Close_File endp

Если все работает нормально (как Вы говорите) то сначала выводится строка Mess_Close_ok, а затем Mess_Close_Nok. Но реально строка Mess_Close_ok не выводится (а вроде должно, коли Handle не FFFF)!! А выводится сразу Mess_Close_Nok

То есть имеем - команда 'je No_Close' в этой процедуре выполняется и идет на метку No_Close. На всякий случай я приложил asm-файлик (мало ли Вы стерли этот пример, да и чтобы не править руками), где процедура закрытия именно такая и этот эффект можно пронаблюдать.

Именно об этом эффекте я и говорил. А ведь после открытия файла Handle точно не FFFF (по крайней мере в дебаггере). А вот перед закрытием - FFFF. То есть остается предположить, что когда мы перечитываем файл, Handle становится FFFF. Почему так - я пытался описать раньше.

Best regards, Sergey

mailto:s_kessel@mail.ru

______________

Вот еще одно предложение:

Привет, Олег!

Хочу подсказать одну нуууууу очень хорошую книгу: П.Абель "Програмирование на языке Ассемблера". Можешь стянуть ее по адресу www.chat.ru/~rusdoc

По моему она в 2 раза лучше Жордейна. Я в свое время на ней учился.

______________

Ну и еще одно письмецо. Нужна помощь одному из наших подписчиков. Может, кто подскажет чего:

И вот еще - обращаюсь с просьбой о помощи (хотелось бы чтобы оно было опубликовано в рассылке)! Мне срочно нужно описание BIOS (Сетапа) как и что там делать - от этого зависит моя "жизни". Дело в том , что наш системный администратор "слинял" а на свое место некого не поставил! И Все проблемы свалились на меня! solomaxa@rambler.ru

Резидент

Теперь к делу.

Возьмите, пожалуйста, файл здесь: http://www.Kalashnikoff.ru/Assembler/Programs/Lessons/Resid12.rar

Если у вас нет выхода в Сеть, то напишите мне письмо с просьбой выслать. Я также включу ваш адрес в базу данных. Затем перед выходом очередной рассылки вы получите этот файл по почте.

Сегодня рассмотрим один из вариантов применения резидента. Сперва посмотрите файлы resid12.asm и test12.asm.

Что делает программа? Сначала загрузим файл resid12.com в память. Затем запустим файл test12.com. Что мы видим?

Resid12.com оставляет в памяти процедуру, которая выводит на экран строку ASCIZ (строка, заканчивающаяся символом 0). Причем, процедура Int_10h_proc "вешается" на 10h прерывание (это прерывание BIOS (ПЗУ), как вы уже знаете). Нечто похожее мы рассматривали в позапрошлом выпуске. В данном случае мы как бы добавляем еще одну функцию (88h) к прерыванию 10h. Для того, чтобы она отработала, нам нужно вызвать 10h прерывание, а в регистры загрузить необходимые числа (данные). Это:

AH=88h - номер нашей функции;

DS:SI = адрес строки, которую нужно вывести на экран (DS - сегмент, SI - смещение).

Обратите еще раз внимание, что мы в DS (сегмент) ничего не загружаем, только в SI (смещение). Если вы забыли, то напомню: при старте любого *.com-файла, сегментные регистры CS, DS, ES, SS равны нашему единственному сегменту, в который загрузилась программа. Сегмент может быть любым. Все зависит от того, сколько резидентных программ загружено.

Итак, давайте подробно рассмотрим программу resid12.asm.

Сперва переходим на метку инициализации. При инициализации обычно настраиваются необходимые регистры, перехватываются прерывания и пр. Т.е. происходит "подготовительный процесс".

Перво-наперво проверим, загружен ли наш резидент в память или нет. Как это делается? Т.к. мы перехватили прерывание, то можем из него сделать отклик. Вот мы и делаем. Вызываем 10h прерывание, занеся в регистр AX "магическое число": 8899h (можно любое другое. Главное - чтобы не конфликтовало с какой-нибудь функцией данного прерывания). Понятно, что в AH заносится 88h, а в AL - 99h. Функции 88h у 10h прерывания не существует (не дошли пока разработчики до этого числа). Это можно с уверенностью предположить на 99,9%. Т.к. такой функции не существует, то произойдет немедленный выход из 10h прерывания (регистры не меняются!), что легко проверить в отладчике. Следовательно, вызвав 10h прерывание с числом 8899h в AX и получив в ответ 8899h в том же AX, мы уверены, что наш резидент еще не загружен. Сейчас все будет понятно.

Если мы уже находимся в памяти, перехватив 10h прерывание, то прежде всего проверим, вызывают ли его с числом 8899h в AX. Если так, то нужно будет послать какой-нибудь ответ. Какой? В нашем примере мы меняем местами содержимое регистров AH и AL и немедленно выходим из прерывания. Проще говоря, возвращаемся в нашу программу, которая, в свою очередь, проверяет, вернулось ли число 8899h или 9988h (помните, что если нашего резидента нет в памяти, то вернется 8899h в AX. BIOS регистры не изменит, т.к. такой функции просто нет!). Если же вернулось 9988h, то значит мы в памяти! Т.е. кусочек нашей процедуры Int_10h_proc поменял местами регистры (а кто еще может это сделать?). Смотрите первые строки процедуры (часть нашего резидента):

pushf ;сохраним флаги

cmp ax,8899h ;проверим на "магическое число"

jne Next_test ;если не оно, то работаем дальше

xchg ah,al ;иначе меняем местами AH/AL

popf ;восстановим флаги

iret ;и выйдем

Итак, новая инструкция: xchg

Название Перевод Применение Процессор xchg источник, приемник eXCHanGe - обменять Обмен регистров 8086 Пример:

mov ax,10h

mov bx,15h

xchg ax,bx

Теперь AX=15h, BX=10h. Все элементарно!

Мы сейчас рассматриваем одну из самых сложных тем: механизм работы прерываний. Я понимаю, что для многих из вас это очень трудно (особенно для тех, кто не знаком ни с одним языком программирования). Поэтому я постараюсь как можно проще объяснить принцип работы прерываний:

Как уже вам известно, сегментный регистр CS (Code Segemt - сегмент кода) всегда содержит номер сегмента, в котором находится наша программа, а IP (Instruction Pointer - указатель инструкций) - смещение.

Допустим, вектор (адрес) 10h прерывания находится по адресу 0010:0400h, а наша программа загрузилась по адресу 1234:0100h. Тогда сразу после загрузки:

CS:IP = 1234:0100h

1234:0100 mov ax,8899h --- теперь (после выполнения данной инструкции) CS:IP=1234:0103h

1234:0103 int 10h --- теперь CS:IP = сегменту/смещению адреса (вектора) 10h прерывания, т.е. 0010:0400h.

1234:0105 mov bx,10

Остальные регистны не изменятся (кроме SP, т.к. перед вызовом прерывания компьютер кладет в стек адрес следующей команды, чтобы потом на нее вернуться. Примерно, как при вызове процедуры, с той лишь разницей, что при вызове процедуры в стек кладется только смещение (а зачем сегмент, если и так в одном сегменте с программой находится процедура?). При вызове же прерывания меняется как сегмент (CS), так и смещение (IP), которые компьютер и кладет в стек. Поэтому инструкция выхода из подпрограммы и из прерывания разные: ret и iret соответственно. Ret достает со стека только слово (два байта - смещение - IP), а iret достает еще и сегмент (CS+IP)).

Что дальше? А дальше все просто: выполняется 10h прерывание до тех пор, пока не встретится инструкция iret, которая вытащит из стека сегмент/смещение инструкции на которую нужно вернуться, в нашем случае - это 1234h (CS - сегмент) и 0105h (IP - смещение). Проще говоря, в CS:IP загружаются из стека данные значения, при этом SP увеличивается на 4 байта (стек-то снизу вверх растет!).

Очень просто все смотрится на практике. Попробуйте запустить наш резидент (который скачали) в отладчика. Внимательно следите за состоянием регистров CS:IP. Вы заметите, что они постоянно меняются. Обратите внимание, какие значения принимают они при вызове прерываний (для этого нужно зайти внутрь прерываний; в AFD - F1, в CV - F8). Ну что, сложно?

Теперь рассмотрим некоторые новые инструкции, встречающиеся в нашем резиденте: stos, lods, rep. Это очень мощные и быстрые команды Ассемблера. Они предназначены для работы с массивами данных (строки, данные любого типа, числа и пр.).

Инструкция lods загружает в регистр AX/AL число, которое находится по адресу, указанному в регистрах DS:SI, при этом SI увеличивается на 1 или на 2. Почему я написал AX/AL? Дело в том, что эта инструкция имеет две разновидности: lodsb и lodsw. Lodsb (B - byte) загружает в AL, а stosw (W - word - слово) - в AX.

Пример:

...

mov si,offset String ;SI указывает на начало String, т.е. на '1'

lodsb ;теперь в AL - символ '1' (31h); SI=SI+1, т.е. SI теперь указывает не на '1', а на '2'

lodsw ;теперь AX содержит '32' (3332h); SI=SI+2, SI указывает на '45'

...

String db '12345'

Поняли принцип? Если последний символ в инструкции lods 'b' - то загружается один байт в AL, и SI увеличивается на 1. Если же последний символ 'w', то загружается два байта (слово) в AX, и SI увеличивается на два.

Здесь возникает два вопроса:

1. Почему не загружаем ничего в DS?

2. Почему после команды lodsw в AX содержится '32', а не '23', что было бы вполне логичней? Когда указывается число в кавычках ('32'), то это значит, что оно занимает два байта. Т.е. один байт - '3', а второй - '2'.

Превый вопрос уже ясен для многих: если String находится в том же сегменте, в котором наша программа, и он не менялся в процессе работы, то в него ничего загружать не надо (в нем и так находится нужный сегмент. Ведь при запуске *-com программы все сегментные регистры равны нашему единственному сегменту).

Второй вопрос заслуживает подробного рассмотрения.

Дело в том, что компьютере двух- и более байтовые числа хранятся в обратном порядке. Путаницы не возникает, если к двухбайтовому числу (к слову) мы обращаемся как к двухбайтовому, а к четырехбайтовому (к двойному слову) мы обращаемся как к четырехбайтовому. Вот примеры:

Handle dw 1234h --- изначально присваиваем переменной значение 1234h. В памяти это число расположится в таком порядке: 3412h. Проверьте в отладчике...

mov ax,Handle --- AX=1234h

mov al,byte ptr Handle --- AL=34h

mov al,byte ptr Handle+1 --- AL=12h

Byte ptr, как вы уже знаете, обозначает то, что мы хотим загрузить один байт с переменной двухбайтового типа (Handle dw 1234h - DW - Define Word - определить слово (два байта)).

Но повторю еще раз: путаницы, как правило, не возникает. И вы поймете это скоро.

Исходя из вышесказанного, рассмотрим, почему мы при сохранении вектора прерывания 10h заносим вначале смещение, а затем сегмент, хотя логичнее было бы наоборот:

...

mov ax,3510h ;получим адрес (вектор) 10h прерывания.

int 21h ;теперь ES содержит сегмент, а BX - смещение...

mov word ptr Int_10h_vect,bx ;сохраним сперва смещение

mov word ptr Int_10h_vect+2,es ;а затем сегмент, учитывая, что данные хранятся в обратном порядке

...

Int_10h_vect dd ? ;переменная на два слова (четыре байта)

Word ptr указывает на то, что нужно занести слово в переменную Int_10h_vect. Обратите внимание, что данная переменная имеет тип DD - Define Double word - определить двойное слово. Но мы-то заносим одно слово (ES или BX)! Для этого и указываем ассемблеру на то, что заносится только слово, иначе он "не поймет".

Уфф!!! Перекусим и поедем дальше...

Выяснили, что команда lodsb загружает в AL однобайтовое число, находящееся по адресу DS (сегиент):SI (смещение). В принципе, данная команда аналогична следующей паре инструкций:

mov al,ds:[si]

inc si (или add si,1)

только работает она гораздо быстрее, да и занимает меньше байт.

По аналогии: команда lodsw загружает в AX двухбайтовое число, расположенное также по адресу DS:SI. Она эквивалентна паре инструкций:

mov ax,ds:[si]

add si,2

Как правило такие команды (lodsb / lodsw) работают в цикле для чтения значений из строки (или другого массива данных). Массив данных - цепочка символов, расположенных последовательно друг за другом. Например, следующая строка является своего рода массивом данных:

Data_array db 'Это массив данных'

Рассмотрим еще пару подобных команд, которые используются в нашем резиденте: stosb / stosw.

Stosb заносит однобайтовое число из AL по адресу ES:DI, а stosw - двухбайтовое число по тому же адресу.

Пример:

mov si,offset Data_array

mov ax,2030h

stosw

...

Data_array dw ? ;теперь в этой переменной находится число 2030h

что равносильно команде:

mov Data_array,2030h

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

...

mov ax,0B800h

mov es,ax

mov di,0

mov ah,07h ;атрибут символа (белый на черном)

mov al,01h ;(сам символ - "рожица")

stosw ;заносим, что эквивалентно: mov es:[di],ax

Насколько мы знаем, видеобуфер имеет следующую структуру:

символ:атрибут символ:атрибут и т.д.

Если внимательно присмотреться, то можно заметить, что в AH мы загружаем атрибут, а в AL - символ. Получается, что сперва заносится атрибут (т.к. AH - старшая (левая) половинка), а затем символ. Что-то не так...

Стоит вспомнить с сегодняшнего урока, что в компьютере двух и более байтовые данные (а AX - два байта) хранятся в обратном порядке. Получится, что при переносе AX в сегмент видеобуфера, символ и атрибут поменяются местами! Понятно это?

Далее. Рассмотрим т.н. префикс rep. Что он делает?

Допустим, нам надо очистить экран. Это можно сделать, используя команду stosw (просто забьем экран пробелами):

...

mov ax,0B800h

mov es,ax

mov di,0 ;как обычно с верхнего левого угла

mov cx,2000 ;80х25=2000 символов на экране

mov ax,0720h ;07 - атрибут, 20h - пробел

Next_sym: ;заносим посимвольно

stosw

loop Next_sym

;теперь экран чистый

Неуклюже немного. Да и работать будет не так быстро, хотя на глаз и не заметно. Попробуем воспользоваться префиксом rep. Вот, что у нас получится:

...

mov ax,0B800h

mov es,ax

mov di,0

mov cx,2000 ;CX - counter - счетчик

mov ax,0720h ;атрибуты / символ

rep stosw ;выводим пробел столько раз, сколько указано в CX.

...

Все! Экран чистый. Очистка происходит мгновенно. Можете попробовать...

После выполнения последней команды (rep stosw) CX будет содержать 0. REP (REPeat - повтор) от CX неразделимы, как LOOP от CX!

Подведем итог:

одна команда stosw запишет только два символа, находящихся в AX по адресу ES:DI.

команда rep stosw запишет два символа, находящихся в AX по адресу ES:DI столько раз, сколько находится в регистре CX.

Просто? Да элементарно! Пробуйте в отладчике. Там все видно, как на ладони...

Еще покажу вам одну маленькую "фишку". Для того, чтобы обнулить регистр, можно воспользоваться такой командой, которая выполняется быстрее:

xor ax,ax ;равносильно mov ax,0

А можно и так:

sub ax,ax ;вычтем из AX AX.

Например, AX=23. Что будет, если мы сделаем так: AX = AX - AX? Если правильно посчитал мой калькулятор, то будет 0.

Обычно эти команды (xor ax,ax / sub ax,ax) выполняются быстрее mov ax,0. Поэтому не удивляйтесь, если где-то в программе встретите нечто подобное.

XOR - исключающее ИЛИ. Это логическая команда. Подобные инструкции мы рассмотрим позже. Пока что вам нужно уяснить, что XOR'ом и SUB'ом можно аннулировать регистры.

Ну что? Загрузил я вас сегодня? Думаю, что вирус мы рассмотрим на следующей неделе.

Нескучной вам ночи!!!

P.S. Домашнее задание.

Исследовать программы RESID12.ASM и TEST12.ASM в отладчике (лучше в AFD) "от корки до корки".

С уважением,

Автор рассылки: Калашников Олег

E-mail: assembler@beep.ru

URL: http://www.Kalashnikoff.ru

UIN (Тетя Ася): 68951340

[Следующий выпуск] [На главную страницу]

u="u496.71.spylog.com";d=document;nv=navigator;na=nv.appName;p=1; bv=Math.round(parseFloat(nv.appVersion)*100); n=(na.substring(0,2)=="Mi")?0:1;rn=Math.random();z="p="+p+"&rn="+rn;y=""; y+=""; y+=""; y+=""; d.write(y);if(!n) { d.write("

Соседние файлы в папке Выпуски