Самыми популярными на сегодняшний день являются пакеты Turbo Assembler (TASM) фирмы Borland и Macro Assembler (MASM) фирмы Microsoft, предоставляющие весьма широкие возможности для программиста. Какой из них лучше сказать сложно, скорее это дело вкуса, но на мой взгляд, для программирования под DOS лучше подходит TASM v5.00, а для Windows - MASM32 v7.00. Существует также множество других видов ассемблера, число которых постоянно растет. Вот ссылки на некоторые виды ассемблера:
ml !.asm (либо masm !.asm и link !.obj,,nul,,,) del !.obj
Программа в формате DOS COM:
ml !.asm (либо masm !.asm и link /tiny !.obj,,nul,,,) del !.obj
Консольная программа для Windows в формате PE EXE:
ml /c /coff !.asm link /subsystem:console !.obj del !.obj
Оконная программа для Windows в формате PE EXE:
ml /c /coff !.asm link /subsystem:windows !.obj del !.obj
Примечания:
Здесь указаны строки для файлов запуска по расширению оболочек DN (dn.ext), NC (nc.ext), VC (vc.ext) и т.п, и предполагается, что каталоги с компиляторами прописаны в переменной окружения DOS PATH. При создании приложений для Windows с помощью TASM не забудьте скорректировать имя каталога с библиотеками, если он отличается от указанного (c:\tasm\lib\).
Для пакета MASM предполагается, что используется MASM32 v7.0 (с компилятором ML.EXE v6.14). Однако для компиляции программ для DOS (COM, EXE) можно использовать и старый MASM v6.61 (с компиляторами ML.EXE и MASM.EXE v6.11). Варианты, приведенные в скобках можно применять только для "старой" версии MASM, т.к. в новый пакет MASM32 v7.0 не входит программа MASM.EXE.
Имена библиотек для Windows-приложений, создаваемых с помощью MASM32, указываются внутри исходника с помощью директивы includelib (для примера см. Как написать резидентную программу? , примеры для Windows).
Mask = 1 shl n ; Маска
and al,not Mask ; Сбросить бит(ы)
or al,Mask ; Установить бит(ы)
xor al,Mask ; Инвертировать (изменить) бит(ы)
and al,not (1 shl 2 + 1 shl 5) ; Сбросить второй и пятый биты
.386
btr al,n ; Сбросить бит
bts al,n ; Установить бит
btc al,n ; Инвертировать (изменить) бит
Разумеется, эти операции можно применять к операндам любого размера (байт, слово, двойное слово), находящимся как в регистре, так и в памяти.
Определить состояние бита можно с помощью инструкций i8086 test и shr (ror, rcr, sar) и инструкции i386 bt (кстати, с помощью инструкций btr, bts и btc тоже можно определить состояние изменяемого бита):
Mask = 1 shl n ; Маска
test al,Mask ; Проверить состояние бита
jz Reset ; Прыжок, если бит сброшен
jnz Set ; Прыжок, если бит установлен
test al,(1 shl 2 + 1 shl 5) ; Проверить состояние второго и пятого битов
jz Reset ; Прыжок, если ОБА бита сброшены
jnz Set ; Прыжок, если ХОТЯ БЫ ОДИН из битов установлен
shr al,n+1 ; Проверить бит n (регистр AL изменяется!)
jnc Reset ; Прыжок, если бит сброшен
jc Set ; Прыжок, если бит установлен
.386
bt al,n ; Проверить бит n
jnc Reset ; Прыжок, если бит сброшен
jc Set ; Прыжок, если бит установлен
bts al,n ; Проверить бит n и установить его
jnc Reset ; Прыжок, если бит был сброшен
jc Set ; Прыжок, если бит был установлен
; ...и т.д...
Кстати говоря, инструкция test работает так же, как и and, с той разницей, что он не изменяет первый операнд.
В качестве второго операнда для всех вышеперечисленных инструкций (кроме инструкций типа shr) можно использовать не только константу, но и регистр, а для инструкций процессора i8086 еще и память (кроме сочетаний память-память). Например, and dx,[bp+10h] , test es:[di],al или bts ecx,ebx, но не xor [di],[bx+si] . Для инструкций типа shr можно использовать либо константу (больше 1 - только для i286+), либо регистр cl.
Чтение кода комбинации клавиш:
Вход: AH = 0 / 10h
Выход: AH = скэн-код (либо расширенный код, если AL = 0 или AL = 224)
AL = символ ASCII Проверка, была ли нажата клавиша после последнего вызова функций 0 / 10h:
Вход: AH = 1 / 11h
Выход: Флаг ZF = 1, если клавиша была нажата (при этом AH и AL содержат код первой клавиши)
Флаг ZF = 0, если клавиша не была нажата (буфер клавиатуры пуст) Проверка состояния клавиш-аккордов:
Вход: AH = 2 / 12h
Выход: AL = состояние клавиш (значение байта памяти по адресу 40h:17h)
AH = второй байт состояния (только для функции 12h, значение байта по адресу 40h:18h)
P.S. Функция 2 обычно разрушает регистр AH
При нажатии специальных клавиш (например, F1, Alt-Z, но не Enter или Ctrl-A) код клавиши заносится в регистр ah, а в регистр al записывается значение 0 или 224. Функции 0 и 1 отличаются от функций 10h и 11h тем, что первые игнорируют ввод расширенных клавиш (таких, например, как F11, F12, Alt-Enter и др), а последние - нет. Кроме того, последние позволяют анализировать, клавиши какой части клавиатуры были нажаты - левой или правой (Enter, клавиши управления курсором и т.п), в связи с чем у первых есть одно преимущество: они никогда не возвращают в al значения 224 при нажатии специальных (комбинаций) клавиш, что облегчает анализ кода и не создает проблему с русской буквой "р" (код которой как раз 224). Тем не менее, если Вы все-таки решили использовать именно функцию 10h, запомните, что при вводе строчной русской "р" функция возвращает ah = 0, al = 0E0h (т.е. ax = 0E0h).
Приведу несколько примеров использования этих функций (программы в формате DOS COM).
Пример №1. Программа, задающая вопрос и проверяющая нажатие клавиши "Y" или "N":
; tasm /m keycode1.asm
; tlink /t /x keycode1.obj
.MODEL Tiny
.CODE
ORG 100h
Start:
mov ah,9
lea dx,Question
int 21h ; Выводим на экран сообщение
RepeatEnter:
xor ah,ah ; AH = 0
int 16h ; Ожидаем ввода клавиши с клавиатуры
or al,20h ; Преобразуем заглавную английскую букву в строчную
cmp al,'y'
je YesPressed ; Нажата клавиша 'y'
cmp al,'n'
jne RepeatEnter ; Нажата не клавиша 'n' (и не 'y')
lea dx,NoMessage ; Иначе, ясень пень, нажата клавиша 'n'
jmp ShowMessage
YesPressed:
lea dx,YesMessage
ShowMessage:
mov ah,9
int 21h ; Вывод сообщения
int 20h ; Выход
Question db 'Вы хотите этого :) (Y/N)?$'
YesMessage db 13,10,'Выбран ответ "да"$'
NoMessage db 13,10,'Выбран ответ "нет"$'
END Start
Хочу немного также пояснить операцию or al,20h, т.к. вполне вероятно, что она может вызвать у Вас вопрос. Коды заглавных английских букв лежат в диапазоне 41h..5Ah, а коды строчных - в диапазоне 61h..7Ah, поэтому выполняя такую операцию мы приводим заглавные буквы к строчным, а строчные не трогаем (обратная операция - and al,(not 20h)). Но! Не нужно забывать, что данная операция производит преобразование не только английских букв, но и любых других символов, а следовательно многие символы будут испорчены. Например, символ "@" превратится в символ "`", символ "^" - в тильду "~", а символ "_" - в символ с кодом 127. Цифры же и основные знаки препинания будут сохранены, зато управляющие символами (из диапазона 0..1Fh) превратятся в эти самые цифры и знаки препинания. Т.о, данную операцию следует проводить только при проверке английских букв и русских букв от "а" до "п".
Пример №2. Программа выводит точки до тех пор, пока не будет нажата клавиша Esc:
; tasm /m keycode2.asm
; tlink /t /x keycode2.obj
.MODEL Tiny
.CODE
ORG 100h
Start:
Repeat:
mov al,'.'
int 29h ; Выводим точку
mov ah,86h
xor cx,cx
mov dx,50000
int 15h ; Небольшая задержка (CX:DX = 50000 мкс)
mov ah,1
int 16h ; Проверяем нажата ли клавиша
jz Repeat ; Повторяем цикл, если не нажата клавиша
xor ah,ah
int 16h ; Читаем код нажатой клавиши
cmp ax,011Bh ; Это Esc ?
jne Repeat ; Если нет, то повторяем цикл
int 20h ; Выходим из программы
END Start
AH = 13h
AL = режим записи (бит 0 - обновить позицию курсора, бит 1 - строка содержит цвета символов)
BH = номер видеостраницы
BL = цвет текста (только если строка не содержит цветов символов, т.е. бит 1 регистра AL = 0)
CX = количество выводимых символов
DH,DL = номер строки и столбца для вывода сообщения (отсчет начинается с нуля!)
ES:BP = адрес строки (если бит 1 регистра AL = 1, то символы чередуются с их цветами)
Т.о, для вывода сообщения на экран в текущую позицию необходимо получить номер видеостраницы (что можно сделать с помощью функции ah=0Fh / int 10h) и текущую позицию курсора (функция ah=3 / int 10h, в качестве параметра передается номер видеостраницы в регистре bh).
Вот пример работающей программы в формате DOS COM:
; tasm /m colormsg.asm
; tlink /t /x colormsg.obj
.MODEL Tiny
.CODE
ORG 100h
Start:
mov ah,0Fh ; Получить видеостраницу
int 10h
mov ah,3 ; Получить координаты
int 10h
mov ax,1301h ; Код функции (вывести мессагу с обновлением курсора)
mov bl,1Eh ; Желтый цвет на синем фоне
mov cx,lMessage ; Длина строки
lea bp,Message ; ES:BP - адрес строки (ES=CS, т.к. это COM-программа)
int 10h
int 20h ; Выход
Message db 'Привет из глубины души!',10,13
lMessage = $-Message
END Start
Хочу обратить Ваше внимание на строку 'Привет из глубины души',10,13 :) . Обычно строки завершают символами 13,10, если хотят осуществить переход на новую строку, я же поменял эти символы местами и не просто так. Давайте сначала разберемся, что это за символы. Символ 13 - это возврат коретки, т.е. перевод курсора на первую позицию строки, а символ 10 - перевод строки, т.е. переход на новую строку (без изменения позиции относительно начала строки). Т.о, сочетание 13,10 выполнит сначала переход на первую позицию строки, а затем - перевод строки, сочетание же 10,13 - наоборот. На первый взгляд это одно и то же, так в чем же смысл перестановки? Смысл скрывается в работе функций BIOS (и DOS), предназначенных для вывода символов на экран. Дело в том, что если на экране больше не осталось свободных строк (курсор находится на последней строке), то при переводе строки содержимое экрана сдвигается вверх, а новая строка заполняется цветом и фоном символа, под которым стоит курсор. Т.о, если в нашем примере заменить последовательность 10,13 на 13,10, то при переводе строки с последней строки экрана курсор будет находиться под символом "П", выведенным желтым цветом на синем фоне, и вся новая строка будет заполнена этим же цветом и фоном; получится очень некрасиво :) . При переходе же на новую строку с помощью 10,13 курсор будет находиться в конце строки (на следующем символе после восклицательного знака, т.е. на пробеле), цвет и фон которого остались прежними (скорее всего это будет серый цвет на черном фоне), и новая строка будет заполнена этим же цветом и фоном; получится ништяк :) .
; tasm /m showdec.asm
; tlink /t /x showdec.obj
.MODEL Tiny
.CODE
ORG 100h
Number = 12345
Start:
std ; Устанавливаем ОБРАТНЫЙ порядок записи
lea di,StringEnd-1 ; ES:DI = последний символ строки String
mov ax,Number ; Заносим в AX число для перевода
; Начинаем перевод числа AX в строку
mov cx,10 ; Задаемся делителем CX = 10
Repeat:
xor dx,dx ; Обнуляем DX (для деления)
div cx ; Делим DX:AX на CX (10),
; Получаем в AX частное, в DX остаток
xchg ax,dx ; Меняем их местами (нас интересует остаток)
add al,'0' ; Получаем в AL символ десятичной цифры
stosb ; И записываем ее в строку
xchg ax,dx ; Восстанавливаем AX (частное)
or ax,ax ; Сравниваем AX с 0
jne Repeat ; Если не ноль, то повторяем
; Теперь осталось вывести строку на экран
mov ah,9
lea dx,[di+1] ; Заносим в DX адрес начала строки
int 21h ; Выводим ее на экран
int 20h ; Выходим из программы
String db 5 dup (?),'$' ; Резервируем 5 байт для строки
StringEnd = $-1 ; Указывает на символ '$'
END Start
Для вывода 16-ричных значений деление лучше производить командой shr (так проще и быстрее):
; tasm /m showhex.asm
; tlink /t /x showhex.obj
.MODEL Tiny
.CODE
ORG 100h
Number = 5A3Dh
Start:
mov ax,Number ; Заносим в AX число для перевода
; Начинаем перевод числа AX в строку
mov cl,16-4 ; 16-битный регистр, будем выводить по 4 бита (0..F)
xchg dx,ax ; Сохраняем число в DX
Repeat:
mov ax,dx ; Восстанавливаем число в AX
shr ax,cl ; Сдвигаем на CL бит вправо
and al,0Fh ; Получаем в AL цифру 0..15
add al,'0' ; Получаем в AL символ цифры
cmp al,'9' ; Проверяем цифру
jbe Digit09 ; Прыгаем, если это цифра 0..9
add al,'A'-('9'+1) ; Иначе (для A..F) корректируем ее
Digit09: int 29h ; Выводим символ в AL на экран
sub cl,4 ; Уменьшаем CL на 4 для следующей цифры
jnc Repeat ; Если знаковый CL >= 0, то повторяем
int 20h ; Выходим из программы
END Start
Если Вы хотите вывести число в двоичном виде, замените в последней программе цифры 4 (в двух местах, они выделены жирным шрифтов) на 1, инструкцию and al,0Fh на and al,1 и удалите строки, выделенные курсивом (т.к. буквы в качестве цифр уже использоваться не будут).
; Процедура сложения больших чисел
; На входе: в стеке полный адрес X (dword), полный адрес Y (dword), длина чисел (word)
; На выходе: X = X + Y
FastAdd proc pascal
arg X:dword, Y:dword, Len:word
uses ds ; Будем сохранять только регистр DS
les di,X ; Получаем адрес X
lds si,Y ; Получаем адрес Y
mov cx,Len ; Длина чисел
cld ; Направление работы lodsb/stosb
clc ; Изначально переноса не было (для adc)
@@Repeat: lodsb ; Читаем байт из Y
adc al,es:[di] ; Складываем его с байтом из X
stosb ; Записываем в X
dec cx
jnz @@Repeat ; Продолжаем цикл (это быстрее, чем loop)
ret
FastAdd endp
; Процедура сложения больших чисел по 4 байта (гораздо быстрее, чем FastAdd)
; На входе: в стеке полный адрес X (dword), полный адрес Y (dword), длина чисел/4 (word)
; На выходе: X = X + Y
FastAdd4 proc pascal
arg X:dword, Y:dword, Len:word
uses ds ; Будем сохранять только регистр DS
les di,X ; Получаем адрес X
lds si,Y ; Получаем адрес Y
mov cx,Len ; Длина чисел (уменьшенная в 4 раза)
cld ; Направление работы lodsd/stosd
clc ; Изначально переноса не было (для adc)
@@Repeat: lodsd ; Читаем dword из Y
adc eax,es:[di] ; Складываем его с dword'ом из X
stosd ; Записываем в X
dec cx
jnz @@Repeat ; Продолжаем цикл (это быстрее, чем loop)
ret
FastAdd4 endp
; Процедура сложения больших ASCII-чисел
; На входе: в стеке полный адрес X (dword), полный адрес Y (dword), длина чисел (word)
; На выходе: X = X + Y
FastAddASCII proc pascal
arg X:dword, Y:dword, Len:word
uses ds ; Будем сохранять только регистр DS
les di,X ; Получаем адрес X
lds si,Y ; Получаем адрес Y
mov cx,Len ; Длина чисел
cld ; Направление работы lodsb/stosb
clc ; Изначально переноса не было (для adc)
@@Repeat: lodsb ; Читаем байт из Y
adc al,es:[di] ; Складываем его с байтом из X
aaa ; Корректируем для ASCII
stosb ; Записываем в X
dec cx
jnz @@Repeat ; Продолжаем цикл (это быстрее, чем loop)
ret
FastAddASCII endp
; Процедура сложения больших ASCII-чисел
; На входе: в стеке полный адрес X (dword), полный адрес Y (dword), длина чисел (word)
; На выходе: X = X + Y
FastAddBCD proc pascal
arg X:dword, Y:dword, Len:word
uses ds ; Будем сохранять только регистр DS
les di,X ; Получаем адрес X
lds si,Y ; Получаем адрес Y
mov cx,Len ; Длина чисел
cld ; Направление работы lodsb/stosb
clc ; Изначально переноса не было (для adc)
@@Repeat: lodsb ; Читаем байт из Y
adc al,es:[di] ; Складываем его с байтом из X
daa ; Корректируем для BCD
stosb ; Записываем в X
dec cx
jnz @@Repeat ; Продолжаем цикл (это быстрее, чем loop)
ret
FastAddBCD endp
Как мы видим, можно обойтись даже и без инструкции add, т.е. всю работу за нее выполняет инструкция adc (мы же сбрасываем флаг cf для первого цикла с помощью clc).
Данные процедуры, а также процедуры быстрого вычитания Вы найдете в исходниках, которые можно скачать с данного FAQ (см. начало текущей html-страницы).
Тип блока: 'Z' (5Ah) - последний блок, иначе - 'M' (4Dh)
+01h
2
Сегмент PSP владельца блока
+03h
2
Размер блока памяти в 16-байтовых параграфах
+05h
3
Не используется (зарезервировано)
+08h
8
Имя программы (для PSP-блока, только в DOS версий 4.0 и выше!)
Используя эти данные можно определить количество памяти, которую DOS выделила программе при ее загрузке. Для этого в программу необходимо добавить определенный код. Для программ формата DOS COM:
mov ax,cs ; Сегмент PSP (можно также использовать DS, ES или SS)
dec ax ; Сегмент Memory Control Block
mov es,ax
mov ax,es:[3]
Для программы в формате DOS EXE (этот код необходимо добавить в самое начало программы до изменения регистра ds):
mov ax,ds ; Сегмент PSP (можно также использовать регистр ES)
dec ax ; Сегмент Memory Control Block
mov es,ax
mov ax,es:[3]
После выполнения такого кода регистр ax будет содержать объем памяти (в параграфах), выделенной программе. Помните, что эта память также содержит код программы, переменные, стек и т.д. Далее можно либо работать с этой областью памяти, либо уменьшить объем этой памяти (до необходимого количества) и снова выделять ее по кусочкам (любого размера, в т.ч. и более 64Kb) - выбор за Вами. Если вы выберете второй путь, Вам понадобятся следующие функции DOS (int 21h):
Выделение участка памяти / проверка количества свободной памяти DOS:
Вход: AH = 48h
BX = объем запрашиваемого участка памяти (в 16-байтовых параграфах)
Выход: CF = 0 - операция выполнена успешно, AX = сегмент выделенного участка памяти
CF = 1 - не хватает памяти, AX = код ошибки, BX = объем макс. свободного блока памяти Освобождение выделенного участка памяти:
Вход: AH = 49h
ES = сегмент выделенного участка памяти
Выход: CF = 0 - операция выполнена успешно
CF = 1 - ошибка, AX = код ошибки Изменение размера выделенного участка памяти:
Вход: AH = 4Ah
ES = сегмент выделенного участка памяти
BX = новый размер указанного участка памяти (в 16-байтовых параграфах)
Выход: CF = 0 - операция выполнена успешно
CF = 1 - ошибка, AX = код ошибки, BX = объем максимального свободного блока памяти
По правде говоря, при выходе из программы DOS сама освобождает все принадлежащие этой программе участки памяти, но все же лучше это делать самому, хотя бы из этических соображений :) .
ПОМНИТЕ!!! Если Вы пишете программу в формате DOS COM, не забудьте скорректировать регистр sp так, чтобы он не указывал на область свободной памяти!
Я не буду приводить примеры 4-х составленных мною программ для работы с памятью, т.к. их исходники можно скачать с этой страницы. Приведу лишь пример программы, проверяющей и выделяющей 256Kb памяти (программа в формате DOS EXE):
; tasm /m malloc2e.asm
; tlink /x malloc2e.obj
.MODEL Small
.STACK 100h
.DATA
NotEnoughMemMsg db 'Не хватает памяти!',13,10,'$'
.CODE
Start:
; ES = сегмент PSP
mov ah,4Ah
mov bx,UsedMemory/16
int 21h ; Уменьшаем объем памяти, выделенный программе
jc MemoryError ; На выход, если произошла ошибка(?)
mov ah,48h
mov bx,256*(1024/16)
int 21h ; Выделяем 256Kb памяти
jnc MemoryEnough ; Продолжаем, если памяти хватает
MemoryError:
mov ax,@data
mov ds,ax ; DS = сегмент .DATA
mov ah,9
lea dx,NotEnoughMemMsg
int 21h ; Выводим сообщение о нехватке памяти
int 20h ; Выходим из программы
MemoryEnough:
mov es,ax ; ES = сегмент начала выделенной памяти
; . . . . .
mov ax,4C00h
int 21h ; Выходим из программы
; Размер программы
UsedMemory = ((size _text+15)/16+(size _data+15)/16+(size stack+15)/16)*16+100h
END Start
В данном примере программа не определяет количество памяти, которую DOS выделила ей при загрузке. Собственно, а зачем это делать, если программа сразу устанавливает его равным минимальному количеству необходимой памяти?
Есть еще один довольно интересный путь для файлов формата DOS EXE: исправить в MZ-заголовке после компиляции слово по смещению 0Ch, где записано максимальное количество памяти в параграфах, которую DOS может выделить программе при загрузке (это значение компиляторы TASM и MASM устанавливают равным 0FFFFh). На мой взгляд, наилучший вариант - это занести туда слово, хранящееся по смещению 0Ah (минимальное количество памяти, которую DOS может выделить программе). При запуске исправленной таким образом программы DOS будет выделять ей не всю доступную память, а лишь столько, сколько необходимо для ее кода, переменных, стека. Теперь программа может использовать перечисленные выше функции DOS (48h, 49h, 4Ah) без каких-либо предварительных манипуляций (т.е. из приведенного примера можно удалить строки, выделенные курсивом).
Создать и открыть файл:
Вход: AH = 3Ch
СX = атрибуты создаваемого файла (см. Обход всех каталогов диска)
DS:DX = адрес ASCIIZ-строки (строки, оканчивающейся нулевым байтом) с именем файла
Выход: CF = 0 - операция выполнена успешно, AX = handle файла
CF = 1 - ошибка, AX = код ошибки Открыть уже существующий файл:
Вход: AH = 3Dh
AL = режим (0 - для чтения, 1 - для записи, 2 - для чтения/записи)
DS:DX = адрес ASCIIZ-строки с именем файла
Выход: CF = 0 - операция выполнена успешно, AX = handle файла
CF = 1 - ошибка, AX = код ошибки Закрыть открытый файл:
Вход: AH = 3Eh
BX = handle файла
Выход: CF = 0 - операция выполнена успешно
CF = 1 - ошибка, AX = код ошибки (06h - неверный handle) Прочитать из файла:
Вход: AH = 3Fh
BX = handle файла
CX = количество байт
DS:DX = адрес буфера для записи прочитанной информации
Выход: CF = 0 - операция выполнена успешно, AX = количество прочитанных байт
CF = 1 - ошибка, AX = код ошибки Записать в файл:
Вход: AH = 40h
BX = handle файла
CX = количество байт
DS:DX = адрес буфера с информацией для записи
Выход: CF = 0 - операция выполнена успешно, AX = количество записанных байт
CF = 1 - ошибка, AX = код ошибки Изменить позицию в файле:
Вход: AH = 42h
AL = 0 - от начала файла, 1 - от текущей позиции, 2 - от конца файла
BX = handle файла
CX:DX = (знаковое) смещение в файле (начиная с нуля, CX - старшая часть)
Выход: CF = 0 - операция выполнена успешно, DX:AX = новая позиция в файле
CF = 1 - ошибка, AX = код ошибки
P.S. При CX=DX=0 можно определить текущую позицию (AL=1) и размер открытого файла (AL=2) Проверить - достигнут ли конец файла при чтении:
Вход: AX = 4406h
BX = handle файла
Выход: CF = 0 - ok, AL = 0 - достигнут конец файла или AL = 0FFh - конец файла не достигнут
CF = 1 - ошибка, AX = код ошибки
Функция 3Fh может возвратить в регистре ax значение, меньшее, чем задаваемое на входе значение cx, если при чтении достигнут конец файла. Та же ситуация может возникнуть при записи с помощью функции 40h, если на диске не будет хватать места. Думаю, несложно догадаться, что эти функции могут применяться только для открытых файлов. После того, как работа с файлом завершена, открытые файлы желательно закрыть, хоть DOS и делает это автоматически при выходе из программы.
Информацию о других функциях DOS для работы с файлами можно найти в специальных справочниках (например, в списках Ralf'а Brown'а, см. Полезные ссылки)
Пример программы (в формате DOS COM) копирования файла; имена исходного и результирующего файла задаются в командной строке (в данном примере атрибуты файла и дата/время создания не сохраняются):
; tasm /m filecopy.asm
; tlink /t /x filecopy.obj
.MODEL Tiny
.CODE
ORG 100h
BufSize = 32768 ; Размер буфера
Start:
mov si,81h ; Адрес командной строки
lea di,Filename1 ; Сначала будем читать имя первого файла
mov cx,2 ; Нам нужно 2 параметра
cld ; Прямой порядок чтения
ClearSpaces: lodsb ; Читаем символ из командной строки
cmp al,' ' ; Это пробел?
je ClearSpaces ; Да, пропускаем его!
NextChar:
jb CmdStrEnd ; Выходим, предполагаем, что это 0Dh (конец строки)
stosb ; Нет, копируем этот символ
lodsb ; Следующий символ командной строки
cmp al,' ' ; Это пробел?
jne NextChar ; Нет, повторяем цикл
CmdStrEnd: mov al,0 ; Не используем XOR AL,AL, т.к. нужно сохранить флаги
stosb ; Записываем ноль (конец строки)
lea di,Filename2 ; Буфер для копирования второго параметра
loope ClearSpaces ; Повторяем цикл (читаем следующий параметр)
; Цикл будет повторен только в том случае, если флаг ZF=1, т.е. после JE, но не после JB :)
cmp Filename2,0 ; Проверяем - задано ли имя второго файла
lea dx,WrongMsg ; Готовим сообщение об ошибке
je ShowMsg ; Нет (не задано), выходим
; Основная процедура копирования файлов
mov ax,3D00h ; AL = 0, для чтения...
lea dx,Filename1
int 21h ; Открываем исходный файл
jc Error ; Если CF=1, выводим сообщение об ошибке
mov Handle1,ax ; Сохраняем handle файла
mov ah,3Ch
mov cx,20h ; Атрибуты создаваемого файла = Archive
lea dx,Filename2
int 21h ; Создаем получаемый файл
jc Error
mov Handle2,ax ; Сохраняем handle файла
CopyNext:
mov ax,4406h
mov bx,Handle1
int 21h ; Проверяем - достигнут ли конец исходного файла
or al,al ; Аналогично CMP AL,0
je EOF ; Выходим из цикла, если достигнут конец файла
mov ah,3Fh
; BX уже содержит handle исходного файла (от предыдущей функции)
mov cx,BufSize
lea dx,Buffer
int 21h ; Читаем из исходного файла
jc Error
xchg cx,ax ; Перемещаем AX в CX
mov ah,40h
mov bx,Handle2
; DX уже содержит адрес буфера (от предыдущей функции)
int 21h ; Записываем в получаемый файл столько байт,
jnc CopyNext ; сколько мы смогли прочесть из исходного
Error:
lea dx,ErrorMsg ; Готовим сообщение об ошибке
ShowMsg: mov ah,9
int 21h ; Выводим сообщение
int 20h ; Выходим из программы
EOF:
mov ah,3Eh
int 21h ; Закрываем исходный файл (BX=Handle1 от ф-ии 4406h)
mov ah,3Eh
mov bx,Handle2
int 21h ; Закрываем получаемый файл
lea dx,OkMsg ; Готовим сообщение
jmp ShowMsg ; Идем на вывод сообщения
OkMsg db 'Успешное завершение!',13,10,'$'
ErrorMsg db 'Ошибка ввода/вывода!',13,10,'$'
WrongMsg db 'Необходимо задать имена исходного и получаемого файлов!',13,10,'$'
Filename2 db 0,127 dup (?) ; Имя получаемого файла
Filename1 db 128 dup (?) ; Имя исходного файла
Handle1 dw ? ; Handle исходного файла
Handle2 dw ? ; Handle получаемого файла
Buffer label byte ; Буфер для копирования
END Start
Начать поиск файлов/каталогов:
Вход: AH = 4Eh
CX = атрибуты файлов/каталогов, которых нужно искать (см. ниже)
DS:DX = адрес ASCIIZ-строки, содержащей маску поиска
Выход: CF = 0 - операция выполнена успешно, DTA содержит результат поиска (см. ниже)
CF = 1 - ошибка, AX = код ошибки (02h,03h - неверный путь/имя, 12h - файлы не найдены) Продолжить поиск файлов/каталогов:
Вход: AH = 4Fh
DS:DX = адрес DTA (или его копия) с данными, сформированными функцией 4Eh или 4Fh
Выход: CF = 0 - операция выполнена успешно, DTA (не DS:DX(!)) содержит результат поиска
CF = 1 - ошибка, AX = код ошибки (12h - файлы не найдены) Установить адрес буфера DTA:
Вход: AH = 1Ah
DS:DX = адрес DTA
Выход: Нет Получить адрес буфера DTA:
Вход: AH = 2Fh
Выход: ES:BX = адрес DTA
Как видно из таблицы, функции используют 43-байтовый буфер, называемый DTA (Disk Transfer Address). Именно он и является средством получения информации от функций 4Eh и 4Fh. По умолчанию для DTA используется адрес PSP:80h, но этот адрес можно изменить с помощью функции 1Ah. Какие же данные и в каком формате хранятся в этом буфере? Давайте разберемся:
Смещение
Размер
Описание
+00h
21
Данные, используемые функцией 4Fh (для нас не представляют интереса)
+15h
1
Атрибуты найденного файла/каталога (см. ниже)
+16h
2
Время создания/редактирования файла/каталога (в специальном формате)
+18h
2
Дата создания/редактирования файла/каталога (в специальном формате)
+1Ah
4
Размер файла
+1Eh
13
Имя файла/каталога в формате ASCIIZ
Т.о. при каждом вызове функций 4Eh и 4Fh в DTA записывается информация только об одном найденном файле или каталоге. Для того, чтобы определить файл это или каталог необходимо проанализировать байт, содержащий информацию об атрибутах найденного файла/каталога:
№ бита
Маска
Название атрибута
Описание
0
01h
Read only
Только для чтения (не может быть изменен или удален)
1
02h
Hidden
Спрятанный файл
2
04h
System
Системный файл
3
08h
Volume label
Метка тома (диска)
4
10h
Directory entry
Каталог
5
20h
Archive
Файл не архивирован (обычный файл)
При передаче информации об атрибутах функции 4Eh (начать поиск) помните, что будут найдены только те файлы/каталоги, которые имеют комбинацию из одного или нескольких указанных атрибутов и не имеют лишних. Т.е. при задании cx=21h (Read only + Archive) будут найдены файлы, не имеющие атрибутов и файлы с атрибутами Read only, Archive или Read only + Archive, а файлы, имеющие хотя бы один из атрибутов Hidden, System, Volume lablel или Directory entry, найдены не будут!
Перечисленных выше функций вполне достаточно для того, чтобы составить рекурсивную процедуру поиска каталогов. Вот пример программы (в формате DOS COM), выводящей на экран список всех каталогов диска C:
; tasm /m scantree.asm
; tlink /t /x scantree.obj
.MODEL Tiny
.286
.CODE
ORG 100h
LOCALS
PathLen = 100 ; Максимальный путь к файлу/каталогу
; (уменьшать это значение нежелательно)
Start:
push offset MyFunc
call ScanTree
int 20h
MyFunc proc
lea di,ScanPath ; DI = адрес строки ScanPath
push di
mov cx,PathLen
xor al,al
repne scasb ; Ищем конец строки
dec di
mov word ptr [di],0A0Dh ; Записываем туда CR,LF...
mov byte ptr [di+2],'$' ; ...и символ доллара (для ф-ии 9)
mov ah,9
pop dx
int 21h ; Выводим строку на экран
mov byte ptr [di],0; Восстанавливаем строку ScanPath
; (т.к. изменять ее нельзя)
ret
MyFunc endp
; Маски атрибутов файлов/каталогов
faReadOnly = 1
faHidden = 2
faSystem = 4
faVolume = 8
faDirectory = 10h
faArchive = 20h
faAnyFile = faReadOnly+faHidden+faSystem+faArchive
faAnyDir = faAnyFile+faDirectory
faAnithing = faAnyDir+faVolume
; Описание буфера DTA
DTAFileInfo struc
dtaReserved db 21 dup (?)
dtaAttr db ?
dtaTime dw ?
dtaDate dw ?
dtaSize dd ?
dtaName db 13 dup (?)
ends
; Процедура обхода всех подкаталогов заданного каталога.
; Процедура может работать и в программах формата DOS EXE !!!
; На входе:
; - Переменная StartPath (она же ScanPath) содержит имя стартового каталога.
; Имя каталога должно заканчиваться обратным слэшем ('\') !
; - В стеке смещение (word) near-функции, которая будет вызываться для каждого
; найденного каталога. При этом она может использовать ASCIIZ-строку ScanPath,
; которая содержит полное имя найденного каталога со слэшем на конце, но не
; должна изменять эту строку! При необходимости функция должна скопировать
; строку в какой-нибудь буфер, где может делать с ней все, что нужно :)
; Функция также не должна использовать установленный DTA, при необходимости
; нужно создать свой буфер DTA. Адрес прежнего буфера восстанавливать при
; выходе не нужно, как и регистры DS, ES, если они были изменены.
; На выходе:
; - DTA и регистры DS,BP сохранены.
; - Строка StartPath/ScanPath содержит полное имя последнего найденного каталога.
; - ES:DI указывает на конец строки StartPath/ScanPath, которая была при вызове
; функции (т.е. можно сделать xor al,al / stosb и снова вызвать ScanTree).
ScanTree proc pascal
arg Func:word
local OldDTA:dword, NewDTA:DTAFileInfo
; ES всегда равно первоначальному DS !!!
push ds ; Сохраняем DS
mov ah,2Fh
int 21h ; Получаем адрес DTA
mov word ptr OldDTA[0],bx ; Сохраняем его...
mov word ptr OldDTA[2],es ; ...в переменной OldDTA
pop es ; Восстанавливаем DS в регистр ES (ES=DS)
lea di,ScanPath
mov cx,PathLen
xor al,al
cld ; Прямой порядок поиска scasb
repne scasb ; Ищем в имени каталога нулевой байт (конец строки)
dec di
push di ; Сохраняем указатель на ноль
push ds es ; Сохраняем DS и ES
call Func ; Вызываем функцию для стартового каталога
mov ah,1Ah
push ss
pop ds ; DS = SS (сегмент NewDTA)
lea dx,NewDTA
int 21h ; Устанавливаем свой DTA
pop es ds ; Восстанавливаем DS и ES (ES=DS)
mov ah,4Eh ; Готовимся в началу поиска
mov cx,faAnyDir ; Атрибуты (все, кроме Volume label)
lea dx,ScanPath
lea si,ScanMask ; SI = адрес макси поиска
pop di ; Восстанавливаем DI (указатель на конец строки)...
push di ; ...и тут же сохраняем его
cld ; Прямой порядок работы lodsb/stosb
@@CopyMask: lodsb ; Читаем в AL символ из строки [SI]
stosb ; И записываем его в конец ScanPath
or al,al ; Конец строки [SI] (AL=0) ?
jnz @@CopyMask ; Нет, копируем следующий символ
@@FindNext:
int 21h ; Начинаем/продолжаем поиск
jc @@NotFound ; Выходим, если ничего не найдено (CF=1)
push ss
pop ds ; DS = SS (сегмент NewDTA)
test NewDTA.dtaAttr,faDirectory ; Проверяем - каталог ли был найден
jz @@PrepareNext ; Если нет, готовимся продолжать поиск
lea si,NewDTA.dtaName ; Адрес имени найденного файла/каталога
lodsb ; Читаем первый символ
cmp al,'.' ; Это каталоги '.' и '..'? (нам такие не нужны)
je @@PrepareNext ; Если это они, готовимся продолжать поиск
pop di ; Восстанавливаем DI (указатель на конец строки)...
push di ; ...и тут же сохраняем его
@@CopyPath:
stosb ; Записываем символ строки [SI] в конец ScanPath
lodsb ; Читаем в AL символ из строки [SI]
or al,al ; Конец строки [SI] (AL=0) ?
jnz @@CopyPath ; Нет, копируем следующий символ
mov ax,'\' ; AH = 0, AL = '\'
stosw ; Записываем слэш и ноль в конец строки
push es
pop ds ; DS = ES
push Func
call ScanTree ; Вызываем себя рекурсивно
push ss
pop ds ; DS = SS (сегмент NewDTA)
@@PrepareNext:
mov ah,4Fh ; Готовимся к функции 4Fh
lea dx,NewDTA
jmp @@FindNext ; Продолжаем поиск
@@NotFound:
pop di ; Убираем из стека DI
mov ah,1Ah
lds dx,OldDTA
int 21h ; Восстанавливаем DTA
push es
pop ds ; Восстанавливаем первоначальный DS
ret ; Выходим из процедуры
ScanTree endp
.DATA
ScanMask db '*.*',0 ; Маска каталогов для поиска
StartPath db 'C:\',0 ; Стартовый каталог
ORG StartPath ; Совмещаем StartPath и ScanPath
ScanPath db PathLen dup (?) ; ASCIIZ-имя найденного каталога
END Start
Toshiba laptops содержат также 8-байтовый номер продукта по адресу 0F000h:0E000h (например, "T2200SX ") и 8-байтовый номер версии по адресу 0F000h:0E008h (например, "V1.20 ").
Теперь рассмотрим второй способ... Вызовем функцию ah=0C0h / int 15h. Вот ее формат:
Получить конфигурацию системы:
Вход: AH = 0С0h
Выход: CF = 0, AH = 0 - функция поддерживается, ES:BX содержит таблицу ROM
CF = 1 - функция не поддерживается BIOS'ом, AH = код ошибки (80h или 86h)
Слово по смещению 00h таблицы по адресу es:bx содержит размер этой таблицы (без учета самого слова). Байты 02h..09h содержат информацию о модели компьютера и пр. По смещению 0Ah находится информация от производителя BIOS'а. Award BIOS содержит строку с авторскими правами фирмы, Phoenix BIOS - три байта информации о номере версии и 4-байтовую строку (смещение 0Dh) "PTL",0 (Phoenix Technologies Ltd), Quadram Quad386 - только 17-байтовую строку "Quadram Quad386XT", Toshiba (по крайней мере, Satellite Pro 435CDS) - 7-байтовую сигнатуру "TOSHIBA" и через два байта (смещение 13h) - 3-байтовую строку "JPN".
Принцип написания резидентной программы основан на установке обработчиков одного или нескольких прерываний на свои процедуры, которые впоследствии, как правило, вызывают старые обработчики. Такие манипуляции называются перехватом прерываний. Под DOS это сделать проще, чем под Windows. Рассмотрим сначала как перехватываются прерывания под DOS. В первую очередь необходимо получить адрес текущего обработчика интересующего нас прерывания и где-нибудь сохранить его. Затем нужно заменить указатель на процедуру обработки прерывания (вектор прерывания) на указатель на свою процедуру. Процедура обработки прерывания должна сохранять все регистры, которые она изменяет. Некоторые прерывания (например, int 21h) возвращают вызвавшей программе определенные флаги, поэтому в таких случаях нужно учитывать и это. В приведенном ниже примере приводится пример обработки прерываний для обоих случаев: когда прерывание возвращает флаги (int 21h) и когда - нет (int 09h). Для грамотного написания резидентных программ неплохо бы знать, как работают инструкции запуска прерывания (int) и выхода из него (iret). Инструкция int # читает двойное слово (сегмент и смещение процедуры обработки прерывания) по адресу 0000h:#*4, сохраняют в стеке регистр флагов, сегмент и смещение следующей команды (cs и ip), сбрасывает флаги if (разрешение аппаратных прерываний) и tf (трассировка) и передает управление по считанному адресу. Не нужно также забывать, что при входе в процедуру обработки прерывания значения всех регистров (в т.ч. ds, es, ss, но, разумеется, кроме cs и ip) могут принимать любые значения. Кстати говоря, при возникновении аппаратного прерывания (IRQ) процессор будет выполнять такие же действия перед вызовом обработчика соответствующего прерывания. Выход из прерывания осуществляется инструкцией iret, которая вынимает из стека регистры ip и cs (т.е. адрес следующей команды), и регистр флагов. При описании команды int я сказал, что она читает двойное слово по адресу 0000h:#*4. Это происходит потому, что таблица векторов прерываний расположена в памяти по адресу 0000h:0000h и занимает 256*4=1024 байта. Т.о, если изменить какие-либо байты по этому адресу, то будут изменены и адреса обработки соответствующих прерываний. Для облегчения работы с этой таблицей в DOS предусмотрены две функции:
Получить адрес обработчика прерывания:
Вход: AH = 35h
AL = номер прерывания
Выход: ES:BX = адрес обработчика (вектор прерывания) Установить адрес обработчика прерывания:
Вход: AH = 25h
AL = номер прерывания
DS:DX = адрес нового обработчика (вектор прерывания)
Выход: Нет
Для того, чтобы выйти из программы, сохранив ее (всю или часть) в памяти (сделать ее резидентной), существуют следующие функции:
Для программ формата DOS COM (функция int 27h):
Вход: DX = объем сохраняемой памяти, включая PSP (фактически, адрес следующего байта)
Выход: Программа завершена, но сохранена в памяти Для программ формата DOS EXE и COM (функция int 21h):
Вход: AH = 31h
AL = код возврата (exit code), может быть использован как код ERRORLEVEL в BAT-файлах
DX = объем сохраняемой памяти (в 16-байтовых параграфах)
Выход: Нет
Теперь давайте перейдем от теории к практике и рассмотрим пример резидентной программы в формате DOS COM. Программа перехватывает прерывания 09h (аппаратное прерывание от клавиатуры) и 21h (прерывания DOS). Первое выводит вращающийся пропеллер в левом верхнем углу экрана при нажатии или отпускании любой клавиши, а второе - издает звуковой сигнал при открывании файла:
; tasm /m dostsr.asm
; tlink /t /x dostsr.obj
.MODEL Tiny
.286
.CODE
ORG 100h
Start:
jmp SetIntVec ; Прыгаем на установку
INCLUDE .\SOUND.INC ; Подключка для генерации звука
Ventil db '-\|/' ; Вентилятор
Pos dw 0 ; Позиция вентилятора
Handler09 proc ; Начало обработчика int 09h
push bx ds ; Сохраняем регистры BX и DS
push cs
pop ds ; DS = CS
mov bx,Pos ; BX = смещение переменной Pos
inc word ptr [bx] ; Увеличиваем Pos на 1
and word ptr [bx],3; Если больше 4, то 0
mov bx,[bx] ; BX = позиция
mov bl,Ventil[bx] ; BL = символ вентилятора
mov bh,0Eh ; BH = цвет (желтый на черном фоне)
push 0B800h
pop ds ; DS = 0B800h - сегмент видеопамяти
mov ds:[0],bx ; Выводим вентилятор в левый верхний угол
pop ds bx ; Восстанавливаем регистры BX и DS
; Передаем управление старому обработчику
db 0EAh ; jmp large (непосредственный межсегментный переход)
RealAddr09 dd ? ; Адрес перехода
Handler09 endp ; Конец обработчика int 09h
Handler21 proc ; Начало обработчика int 21h
cmp ah,3Dh ; Это функция открытия файла?
jne NotFileOpenFn ; Нет, пропускаем следующие команды
pusha ; Сохраняем все регистры (общего назначения)
mov ax,1000 ; Частота звука 1000 Гц
call Sound ; Включаем звук
popa ; Восстанавливаем все регистры
NotFileOpenFn:
; Вызываем старый обработчик
pushf ; Заносим в стек флаги (*)
push bp ; Сохраняем регистр BP
mov bp,sp ; BP = SP
push ax ; Сохраняем регистр AX
mov ax,[bp+8] ; Флаги, сохраненные при вызове прерывания
mov [bp+2],ax ; Изменяем _в стеке_ сохраненные флаги (*)
pop ax bp ; Восстанавливаем регистры BP и AX
call cs:RealAddr21 ; Вызываем оригинальный обработчик
; Продолжаем обработку прерывания
pushf ; Сохраняем флаги
push ax
call NoSound ; Отключаем звук
pop ax
popf ; Восстанавливаем флаги
retf 2 ; Выходим из прерывания, сохраняя флаги!
Handler21 endp ; Конец обработчика int 21h
RealAddr21 dd ? ; Адрес старого обработчика int 21h
TSREnd = $ ; Конец резидентной части
ORG RealAddr21 ; Небольшой трюк для экономии 4-х байт кода :)
SetIntVec:
mov ax,3509h
int 21h ; Получаем вектор прерывания 09h
mov word ptr RealAddr09[0],bx ; И сохраняем его...
mov word ptr RealAddr09[2],es ; в RealAddr09
mov ah,25h
; AL мы не изменяли
lea dx,Handler09
int 21h ; Устанавливаем новый обработчик int 09h
mov ax,3521h
int 21h ; Получаем вектор прерывания 21h
mov word ptr RealAddr21[0],bx ; И сохраняем его...
mov word ptr RealAddr21[2],es ; в RealAddr21
mov ah,25h
lea dx,Handler21
int 21h ; Устанавливаем новый обработчик int 21h
mov ah,49h
mov es,ds:[2Ch] ; Сегмент, содержащий переменные окружения
int 21h ; Освобождаем его (он нам не понадобится)
lea dx,TSREnd
int 27h ; Выходим, сохраняя резидентную часть
END Start
А вот, собственно, содержимое файла SOUND.INC:
; Подключка для генерации звука
; Процедура Sound: включить звук
; Вход: AX = частота звука (Гц)
Sound proc
mov dx,12h
cmp ax,dx ; Частота <= 18 Гц ?
jbe @@Exit ; Да, на выход, чтобы избежать переполнения
xchg bx,ax ; Сохраняем частоту в BX
in al,61h ; Порт PB
or al,11b ; Устанавливаем биты 0-1
out 61h,al ; Записываем обратно в PB
mov al,10110110b ; Упр. слово таймера: канал 2, режим 3, двоичное слово
out 43h,al ; Выводим в регистр режима
mov ax,34DDh ; DX:AX = 1193181
div bx ; AX = (DX:AX) / BX
out 42h,al ; Записываем младший байт счетчика
mov al,ah
out 42h,al ; Записываем старший байт счетчика
@@Exit: ret ; Выходим из процедуры
Sound endp
; Процедура NoSound: отключить звук
NoSound proc
in al,61h ; Порт PB
and al,not 11b ; Сбрасываем биты 0-1
out 61h,al ; Записываем обратно в PB
ret ; Выходим из процедуры
NoSound endp
При написании резидентной программы возникает несколько нюансов:
Для того, чтобы вызвать старый обработчик прерывания внутри своего, можно выполнить прямой межсегментный переход на этот обработчик, и так как регистры и стек мы не изменяем (или изменяем, но все равно восстанавливаем прежние значения перед вызовом), то старый обработчик будет работать как продолжение нашего обработчика. Но в этом случае уже мы не сможем продолжить обработку прерывания. Этот прием используется в обработчике прерывания 09h приведенной программы. А как же быть, если после вызова старого обработчика нам нужно выполнить еще какие-либо действия? Для этого вызвать старый обработчик так, как это делает команда int, учитывая все особенности ее работы. Вызвать старый обработчик непосредственно командой int мы, разумеется, не можем, т.к. в этом случае вы попадем опять на начало нашего обработчика прерывания, и произойдет зацикливание. Остается только произвести вызов вручную. Т.к. команда int перед передачей управления записывает в стек регистр флаги и регистры cs, ip и сбрасывает флаги if и tf, нам нужно сделать то же самое. Сохранить флаги можно командой pushf, а сбрасывать флаги if и tf не нужно, т.к. они уже и так сброшены. Передать же управление, сохранив в стеке значения cs и ip можно с помощью команды межсегментного вызова call. Этот прием используется в обработчике прерывания 21h вышеприведенной программы. Но если Вы посмотрите на исходник внимательно, то заметите, что между push и call присутствуют еще какие-то непонятные на первый взгляд инструкции (выделенные курсивом). Они нужны для того, чтобы заменить в стеке значение только что занесенного в него (в стек) регистра флагов на то значение, которое было в этом регистре до вызова нашего обработчика (т.е. перед выполнением команды int). Это можно было бы, конечно, и не делать, но тогда вызываемый обработчик будет думать, что перед вызовом прерывания флаги if и tf были сброшены, что может не соответствовать действительности. Для тех прерываний, которые не возвращают значений во флагах это и не имеет значения, но для других (как int 21h) - важно. Т.о, по сути мы восстанавливаем только флаги if и tf. Немного запутанно, конечно, но я надеюсь, что со временем (и с накоплением опыта), все станет понятно :) ... Теперь я хочу привести все шаблоны для написания процедур обработки прерываний.
Шаблон процедур, вызывающих старый обработчик в конце (одинаков для обоих типов прерываний, т.к. флаги записываются командой int основной программы):
. . .
db 0EAh ; jmp large (непосредств. межсегментный переход)
RealAddr dd ? ; Адрес перехода
Шаблоны процедур, вызывающих старый обработчик в середине:
- Для прерываний, не возвращающих флагов (типа int 09h):
pushf ; Заносим в стек флаги
. . .
call cs:RealAddr ; Вызываем оригинальный обработчик
. . .
iret ; Выходим из прерывания, НЕ сохраняя флаги!
- Для прерываний, возвращающих флаги (типа int 21h):
pushf ; Заносим в стек флаги (*)
push bp ; Сохраняем регистр BP
mov bp,sp ; BP = SP
push ax ; Сохраняем регистр AX
mov ax,[bp+8] ; Флаги, сохраненные при вызове прерывания
mov [bp+2],ax ; Изменяем _в стеке_ сохраненные флаги (*)
pop ax bp ; Восстанавливаем регистры BP и AX
. . .
call cs:RealAddr ; Вызываем оригинальный обработчик
pushf ; Сохраняем флаги
. . .
popf ; Восстанавливаем флаги
retf 2 ; Выходим из прерывания, сохраняя флаги!
Как Вы уже, наверное, успели заметить, что в последнем шаблоне (как и в обработчике прерывания 21h вышеприведенной программы) вместо команды iret используется retf 2. Разница между ними в том, что первая восстанавливает флаги из стека, а вторая сохраняет текущие, удаляя сохраненные флаги из стека. Чтобы у Вас не возникало вопросов по этому поводу, поясню, что команда retf # восстанавливает значения регистров cs и ip (адрес следующей команды) и удаляет из стека # байт (не слов!), где # - константа от 0 до 65535. Обычно эта инструкция (как и аналогичная ей инструкция ret #, которая не изменяет регистра cs) используется для выхода из процедур, которым передаются параметры в стеке, но как мы видим, ее можно использовать и для других целей.
Если обработчик изменяет регистры, то он обязан восстановить их перед вызовом старого обработчика и перед выходом, если, кончено, изменение регистров не является целью написания программы. Лучше всего для этого подойдут инструкции процессора i286 pusha и popa (которые соответственно сохраняют и восстанавливают регистры ax, cx, dx, bx, sp, bp, si и di, а для 32-битных программ - eax, ecx и т.д). Здесь очень важно не запутаться в последовательности вызова этих команд и команд pushf / popf, поэтому я приведу последний шаблон еще раз, но уже вместе с этими командами (выделю их курсивом):
pushf ; Заносим в стек флаги (*)
push bp ; Сохраняем регистр BP
mov bp,sp ; BP = SP
push ax ; Сохраняем регистр AX
mov ax,[bp+8] ; Флаги, сохраненные при вызове прерывания
mov [bp+2],ax ; Изменяем _в стеке_ сохраненные флаги (*)
pop ax bp ; Восстанавливаем регистры BP и AX
pusha ; Сохраняем все регистры (кроме сегментных)
. . .
popa ; Восстанавливаем все регистры (кроме сегментных)
call cs:RealAddr ; Вызываем оригинальный обработчик
pushf ; Сохраняем флаги
pusha ; Сохраняем все регистры (кроме сегментных)
. . .
popa ; Восстанавливаем все регистры (кроме сегментных)
popf ; Восстанавливаем флаги
retf 2 ; Выходим из прерывания, сохраняя флаги!
При подключении фалов директивой include, содержащих процедуры, нужно правильно выбрать место в программе, где следует указать эту директиву (и соответственно, куда эти процедуры будут помещены). Т.о, если их используют обработчики прерываний, то, разумеется, нужно поместить ее в резидентный блок (т.е. до метки TSREnd), иначе - в блок установки прерываний. При подключении файлов (не только в резидентных программах) я советую предварять имя файла символами '.\' (как это сделано выше), т.к. в этом случае компилятор будет искаться этот файл только в текущем каталоге, а в противном случае - еще и в каталоге, указанном в переменной окружения DOS INC или в параметре /i при вызове TASM. Причем, если файл будет найден в каталогах, указанных в INC или /i, то будет использоваться именно этот файл, а не тот, что находится в одном каталоге с программой. Это, по моему мнению, большая глупость со стороны разработчиков компилятора, но тем не менее - факт.
Хочу немного рассказать о трюке, связанном с директивой ORG RealAddr21. Переменная RealAddr21 имеет неопределенное значение, поэтому его значение можно заменить байтами нижеследующих инструкций. При запуске программы в RealAddr21 будет записано нужное значение, в результате чего первые байты кода, на которые накладывается эта переменная, будут испорчены. Но для нас это не страшно, так как эта часть кода к тому моменту будет уже выполнена и ее повторного выполнения не потребуется. Используя этот трюк, мы можем сэкономить 4 байта кода. На первый взгляд кажется, что мы могли бы сэкономить 8 байт, переместив строку RealAddr09 dd ? перед TSREnd, но это не так, потому что для этого вместо инструкции непосредственного межсегментного перехода jmp large (занимающую 1 байт 0EAh) пришлось бы использовать команду jmp dword ptr cs:RealAddr09, которая занимает 5 байт, посему мы не сэкономили бы ничего. Если на данном этапе изучения ассемблера Вам трудно понять, как работает этот трюк, можете просто убрать эту строку (строку с директивой) из исходника :) .
; ml /c /coff wintsr.asm
; link /subsystem:windows /section:.text,rwe wintsr.obj
.586P
.MODEL Flat,StdCall
OPTION CASEMAP:NONE
INCLUDE D:\MASM32\INCLUDE\WINDOWS.INC
INCLUDE D:\MASM32\INCLUDE\KERNEL32.INC
INCLUDE D:\MASM32\INCLUDE\USER32.INC
INCLUDELIB D:\MASM32\LIB\KERNEL32.LIB
INCLUDELIB D:\MASM32\LIB\USER32.LIB
MEM_SHARED equ 8000000h ; Память, видимая для всех
.DATA
Text db 'Эта прога не пашет в Windows NT/2000/XP :(',0
Caption db 'Облом, да?',0
.CODE
; Номер перехватываемого прерывания
Intr = 51h ; IRQ от клавиатуры
Start:
; Проверяем платформу (Windows 9X или NT)
invoke GetVersion ; Узнаем номер версии
or eax,eax ; Старший бит установлен?
js @@Continue ; Да, это Windows 9X, ура! :)
invoke MessageBoxA, NULL, OFFSET Text, OFFSET Caption, MB_OK+MB_ICONERROR
invoke ExitProcess, NULL ; Goodbye!
@@Continue:
; Выделяем память, видимую для всех (1 страницу)
invoke VirtualAlloc, 0, 1000h, MEM_COMMIT+MEM_SHARED, PAGE_EXECUTE_READWRITE
or eax,eax
jz @@Exit ; Выходим, если ничего не получается
cli ; Запрещаем аппаратные прерывания
xchg edx,eax ; Сохраняем начальный адрес выделенной памяти в EDX
; Получаем адрес IDT
push eax ; Сохраняем в стеке что-нибудь (*)
sidt [esp-2] ; Записываем в стек предел и адрес (на место (*)) IDT
pop eax ; Записываем в EAX адрес IDT из (*)
; Сохраняем адрес старого обработчика
add eax,Intr*8 ; EAX = адрес дескриптора нужного прерывания
mov ebx,[eax+4] ; Получаем в EBX старшее и...
mov bx,[eax+0] ; младшее слово адреса старого обработчика
mov OldHandler,ebx ; И сохраняем его в OldHandler
; Копируем по выделенному адресу код, который станет новым обработчиком прерывания
mov edi,edx
mov esi,offset Handler
mov ecx,lHandler
rep movsb ; Теперь наш обработчик по адресу EDX !
; Записываем в дескриптор прерывания адрес нашего обработчика
mov [eax+0],dx ; Записываем младшее слово адреса
shr edx,16
mov [eax+6],dx ; Записываем старшее слово адреса
sti ; Разрешаем аппаратные прерывания
; Выходим из программы
@@Exit:
invoke ExitProcess, NULL ; Goodbye!
;----------------------------------------------------------------------------;
; Наш обработчик аппаратного прерывания от клавиатуры! ;
;----------------------------------------------------------------------------;
Handler proc
pusha ; Сохраняем (почти) все регистры
; Выводим звуковой сигнал
in al,61h ; Читаем в AL байт из порта PB
push eax ; Сохраняем AL
mov edx,80h
@@SoundLoop:
; Отключаем звук
and al,not 11b
out 61h,al
mov ecx,60h
loop $ ; Пауза
; Посылаем одиночный сигнал на динамик
or al,10b
out 61h,al
mov cl,60h
loop $ ; Пауза
dec edx
jnz @@SoundLoop ; Повторяем цикл
pop eax ; Восстанавливаем AL
out 61h,al ; Записываем сохраненное значение в порт PB
; Восстанавливаем регистры и передаем управление оригинальному обработчику
popa
db 0EAh ; jmp large (непосредственный межсегментный переход)
OldHandler dd ? ; Адрес старого обработчика
dw 028h ; Селектор
Handler endp
lHandler = $-Handler
END Start
Примечание: опция /section:.text,rwe позволяет создать секцию .text (т.е. секцию, содержащую код программы) с возможностью чтения, записи и исполнения.
; tasm32 /m /mx dll_tmpl.asm
; tlink32 /Tpd /c /x dll_tmpl.obj
.586P
.MODEL Flat,StdCall
PUBLICDLL TestFunc ; Функция на экспорт
.DATA
.CODE
DLLMain proc hInstDLL:dword, fdwReason:dword, lpvReserved:dword
uses ebx, esi, edi ; Сохраняем EBX, ESI, EDI
mov eax,1 ; Инициализация прошла успешно
ret ; Выходим
DLLMain endp
;----------------------------------------------------------------------------;
; Функции на экспорт ;
;----------------------------------------------------------------------------;
TestFunc proc lData:dword
uses ebx, esi, edi ; Сохраняем EBX, ESI, EDI
mov eax,lData
shl eax,1
ret ; Выходим (результат в EAX)
TestFunc endp
END DLLMain
Вариант для MASM32 я приводить не буду, т.к. примеры создания и работы с DLL есть в примерах пакета MASM32 (каталог EXAMPLE1\DLL), тем не менее, я решил прикрепить к данному FAQ (отдельно от остальных исходников) архив, содержащий файлы этого каталога. Архив можно скачать прямо здесь.
Хорошо на данный вопрос ответил CBP, поэтому именно его ответ я здесь и опубликую (надеюсь, он не обидится на меня за то, я не сохранил полностью орфографию и пунктуацию) :) ...
Итак, господа, как же все это работает в Windows? Я полагаю все знакомы с защищенным режимом. Если нет, то весьма рекомендую ознакомиться, чтоб понимать о чем идет речь :) . Допустим мы имеем логический адрес 0137:00456789h. Этот адрес переводится в линейный, т.е. берем селектор 137 находим соответствующий ему дескриптор в таблице дескрипторов и видим: база = 0, граница = 0FFFFFFFFh, следовательно, линейный адрес равен 0 (база) + 00456789h. Однако линейный адрес не является физическим адресом. Для его получения используется третья ступень - страничная адресация. Т.е. 20 старших бит линейного адреса используются для выбора 4Kb памяти из каталога страниц, оставшиеся 12 бит представляют смещение внутри полученной страницы (в качестве упражнения рекомендую написать небольшую программу под Win32, которая будет показывать сегментные регистры, значения дескрипторов для каждого селектора, базу, границу, RPL и т.п). Так вот, к чему я веду: в Win32 и сегмент кода, и сегмент данных и стека приложения имеют одинаковые базу и границу (0 и 0FFFFFFFFh), в чем Вы можете убедиться при помощи той самой вышеозначенной программки :). Это и называется плоской (FLAT) моделью памяти. Хотя cs и ds имеют разные значения и дескрипторы, они указывают на одно и то же линейное адресное пространство 0..0FFFFFFFFh. Следовательно, например, логические адреса cs:12345678 и ds:12345678 совпадают, гм, линейно-физически. Т.е. вполне можно, к примеру, модифицировать свой код при помощи mov byte ptr $+8,21h (секция кода должна быть помечена как writeable). В данном случае в инструкции mov неявно подразумевается ds:, в который можно писать. Однако, если Вы попытаетесь сделать mov cs:xxxxxxxx, то получите исключение. Сегментная защита, однако! :) Т.е. в сегмент кода, строго говоря, писать нельзя, но зато можно писать в сегмент данных, который "совпадает" с сегментом кода, и тем самым модифицировать код. А теперь вспомним про страничную защиту. Именно она используется в Windows, когда Вы задаете атрибуты секций PE-файла (.data, .code и т.д). Собственно, к сегментам памяти они не имеют отношения, посему когда речь идет о Win32, не путайте понятия секций PE-файлов и сегментов памяти! Это разные вещи! Так вот, когда Windows грузит РЕ-файл, она смотрит атрибуты секций и соответственно им устанавливает "защиту" страниц памяти, в которые будет загружена секция. Это и есть типа страничная защита. Как видим, сегменты тут вроде не при делах.
aam 16 ; Делим значение AL на 16, результат в AH и AL
mov cx,2 ; Будем делать 2 цикла (т.е. для AH и AL)
@@NextDigit: xchg al,ah ; Меняем AL и AH (чтобы сначала обработать AH)
add al,'0' ; Переводим в символьный вид
cmp al,'9' ; Получилась цифра 0..9 ?
jbe @@09 ; Да, прыгаем
add al,'A'-('9'+1) ; Нет, корректируем цифры A..F
@@09: int 29h ; Выводим значение AL на экран
loop @@NextDigit ; Следующая цифра!
Рассмотрим теперь обратную операцию: объединение значений регистров ah и al. Для этого нам подойдет инструкция aad (символьная коррекция однобайтового ASCII-чисели перед делением). Данная инструкция умножает содержимое регистра ah на 10 (или другое значение) и прибавляет к содержимому регистра al, при этом значение ah сбрасывается в ноль. Аналогично инструкции aam она может иметь операнд-консанту. Т.о, имея в регистрах ah и al, скажем две десятичные, 16-ричные или какие-нибудь еще цифры, их можно легко объединить с помощью инструкции aad. Сочетание aam + aad фактически аналогично xor ah,ah, т.к. первая инструкция разложит al на 2 части, а вторая - соберет обратно, обнулив содержимое регистра ah :) .
Алгоритм довольно простой. Сначала меняем знак регистра с помощью инструкции neg, которая по окончании своей работы устанавливает флаги в соответствующие значения. Если результат получился отрицательным (т.е. изначально число было положительным, при этом флаг sf=1) необходимо повторить операцию. Но! Если исходное число = -32768 (8000h), т.е. оно не может быть преобразовано в положительное число 32768, т.к. максимальное знаковое целое = 32767 (при этом после выполнения инструкции neg флаг of=1), то произойдет зацикливание. Т.о, инструкция js нам не подходит, и нам необходимо выбрать такую инструкцию, которая осуществляла бы переход только при сочетании флагов sf=1, of=0. Для этих целей как раз подходит инструкция jl. Заметьте, что инструкция jl осуществляет переход также при сочетании флагов sf=0, of=1, но так как после инструкции neg такого сочетания быть не может, то нам это и не грозит.
xchg bx,ax ; Сохраняем AX в BX
mov dx,8000h ; DH = маска с установленным битом, DL = результат
@@NextBit: xor dl,dh ; Устанавливаем в DL очередной бит (с помощью маски)
mov al,dl ; Записываем результат в AL
mul al ; AX = AL*AL (возводим в квадрат)
cmp ax,bx ; Сравниваем результат с исходным числом
jna @@DontReset ; Если результат больше, то...
xor dl,dh ; Сбрасываем установленный ранее бит
@@DontReset: shr dh,1 ; Переходим к следующему биту
jnz @@NextBit ; Если они (биты) не кончились, повторяем
xchg ax,dx ; Иначе записываем результат в AX
3A21:0000 B8 57 3B mov ax,3B57
3A21:0003 8E D8 mov ds,ax
3A21:0005 FF 36 43 01 push word ptr [0143]
3A21:0009 9A 20 01 FE 39 call 39FE:0120
3A21:000E 0B C0 or ax,ax
3A21:0010 74 05 jnz 0017
3A21:0012 EA F0 FF 00 F0 jmp F000:FFF0
3A21:0017 92 xchg dx,ax
Исследуем данный код. Инструкция mov ax,3B57h наверняка переведена с помощью relocation table (изначально здесь был mov ax,@data, где @data - сегмент данных относительно начала кода), в call 39FEh:0120h сегмент 39FEh наверняка переведен тоже, а вот в jmp 0F000h:0FFF0h - нет, т.к. здесь явно осуществляется прыжок на перезагрузчик компьютера (тем более, наша программа не может находиться в сегменте 0F000h). Но все эти заморочки возникают только в программе формата DOS EXE. Хочу еще заменить, что в инструкции jnz 17h значение 17h - это абсолютное значение (переведенное отладчиком), а 05h (см. байты, соответствующие инструкции) - относительное. Это уже, разумеется, касается любой программы... Т.о, в программах формата DOS EXE нельзя искать код по байтам 57h,3Bh (первой инструкции) и 0FEh,39h (инструкции call), т.к. эти значения переведены с помощью relocation table, следовательно мы может осуществить поиск по цепочке байт 8Eh,0D8h, 0FFh,36h,43h,01h, 9Ah,20h,01h или 0Bh,0C0h, 74h,05h, 0EAh,0F0h,0FFh,00h,0F0h, 92h.