Assembler FAQ

(последнее обновление 13.11.2002)
v 1.00a




>> Скачать весь FAQ в архиве (вместе с исходниками) <<
>> Скачать только исходники <<

Щелкните по интересующему Вас вопросу... Чего задумался?
Какие бывают ассемблеры и где их можно скачать?
Как правильно выполнить компиляцию программы?
Что такое стек и для чего он нужен?
Почему компилятор ругается на число B800?
Как изменить (установить/сбросить) определенный бит в байте?
Как проверить код нажатой клавиши?
Как вывести на экран цветное сообщение?
Как вывести на экран десятичное и шестнадцатеричное значение регистра?
Как выполнить быстрое сложение больших чисел?
Как вычислить функцию Round(Y+R*Sin(Angle)) с помощью сопроцессора, если все переменные целые?
Можно ли выделить в DOS памяти больше, чем 64Kb?
Как прочитать файл с диска?
Как организовать обход всех каталогов диска?
Можно ди определить тип BIOS'а и его серийный номер?
Как написать резидентную программу?
Как работают инструкции типа movsd под Windows?
Как создать DLL-файл?
Программа под Windows работает с логическими (селектор:смещение) адресами или с линейными?

Разные интересности...
Сокращение длины инструкции на 1-2 байта :)
Как проще разделить регистр al на константу или объединить значения в регистрах ah и al?
Самый простой и короткий способ определения модуля числа в регистре
Самый быстрый способ извлечения квадратного корня без использования сопроцессора
Как правильно искать в файле код, выданный отладчиком?
Полезные ссылки


Ну что, приступим к ответам?

Не мешай!

ВНИМАНИЕ !!!
В данном FAQ приведены примеры преимущественно для пакета Turbo Assembler (TASM) v5.0 фирмы Borland International.



Ась? Какие бывают ассемблеры и где их можно скачать?

Угу!Самыми популярными на сегодняшний день являются пакеты Turbo Assembler (TASM) фирмы
Borland и Macro Assembler (MASM) фирмы Microsoft, предоставляющие весьма широкие возможности для программиста. Какой из них лучше сказать сложно, скорее это дело вкуса, но на мой взгляд, для программирования под DOS лучше подходит TASM v5.00, а для Windows - MASM32 v7.00. Существует также множество других видов ассемблера, число которых постоянно растет. Вот ссылки на некоторые виды ассемблера:

Turbo Assembler (TASM) http://wasm.ru/tools/7/tasm50.zip
Macro Assembler (MASM) http://www.masm32.com/ (MASM32 для Windows)
Netwide Assembler (NASM) http://www.cryogen.com/Nasm/
Flat Assembler (FASM) http://fasm.metro-nt.pl/
NewBasic++ Assembler http://www.cybertrails.com/~fys/newbasic.htm
Pass32 http://www.geocities.com/SiliconValley/Bay/3437/
SpAsm http://betov.free.fr/SpAsm.html

Все эти ассемблеры можно также скачать здесь.



Ась? Как правильно выполнить компиляцию программы?

Угу!Компиляция программ с помощью пакетов TASM и (иногда) MASM производится в два этапа: создание объектного файла и компоновка объектного(ых) файла(ов).

Компиляция с помощью Turbo Assembler'а:
Программа в формате DOS EXE: tasm /m !.asm
tlink /3 /x !.obj
del !.obj
Программа в формате DOS COM: tasm /m !.asm
tlink /t /3 /x !.obj
del !.obj
Консольная программа для Windows
в формате PE EXE:
tasm32 /m /mx !.asm
tlink32 /Tpe /ap /c /x !.obj /o c:\tasm\lib\import32.lib
del !.obj
Оконная программа для Windows
в формате PE EXE:
tasm32 /m /mx !.asm
tlink32 /Tpe /aa /c /x !.obj /o c:\tasm\lib\import32.lib
del !.obj

Компиляция с помощью Macro Assembler'а:
Программа в формате DOS EXE: 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

Примечания:
  1. Здесь указаны строки для файлов запуска по расширению оболочек DN (dn.ext), NC (nc.ext), VC (vc.ext) и т.п, и предполагается, что каталоги с компиляторами прописаны в переменной окружения DOS PATH. При создании приложений для Windows с помощью TASM не забудьте скорректировать имя каталога с библиотеками, если он отличается от указанного (c:\tasm\lib\).

  2. Для пакета 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.

  3. Имена библиотек для Windows-приложений, создаваемых с помощью MASM32, указываются внутри исходника с помощью директивы includelib (для примера см. Как написать резидентную программу? , примеры для Windows).



Ась? Что такое стек и для чего он нужен?

Угу!Стек - это специально отведенная программе область памяти для хранения временных данных, например, регистров или адреса команды, следующей за командой call (при вызове процедуры). На вершину стека (т.е. последнее записанное значение) указывает пара регистров ss:sp (или ss:esp для 32-битных программ). При записи слова (или двойного слова) в стек (командами push, call и т.д), оно записывается в память по адресу ss:[sp-2] (или ss:[sp-4], в зависимости от разрядности записываемого значения), а затем из регистра sp/esp вычитается 2 (или 4), т.е. стек растет в сторону младших адресов памяти. При извлечении значения из стека (командами pop, ret и т.д) происходит обратная операция. Для чего это нужно? Предположим, Вам необходимо сохранить значение регистра (допустим, ax), которое понадобиться через несколько команд. Для этого надо выполнить команду push ax, а в том месте, где это значение понадобится - pop ax (или, скажем, pop dx, если Вам необходимо записать это значение в регистр dx). Но! Важно помнить, что запись в стек производится по принципу LIFO (Last In, First Out - последним зашел, первым вышел). Нельзя делать так: push ax ... call ... pushf, а затем pop ax ... popf ... ret, т.к. в этом случае в регистр ax будут занесено значение регистра флагов, в регистр флагов - адрес возврата из процедуры, а после выполнения команды ret управление перейдет по адресу, который будет взят из сохраненного регистр ax. Т.о, данные записываются последовательно, а не каждый регистр по определенному адресу! В стек может быть записано как 16-битное, так и 32-битное значение, но не 8-битное. Соответственно, при записи 16-битных значений будет записано 2 байта, а при записи 32-битных значений - 4 байта. Но есть одно исключение: при записи в стек сегментных регистров (ds, es, cs, ss, fs или gs) 32-битные программы (например, программы, работающие под Windows) будут заносить не 2, а 4 байта(!), несмотря на то, что размер сегментных регистров составляет 16 бит. При этом на процессорах Pentium и выше старшее записываемое слово будет содержать нулевое значение. Что же касается команд типа call и ret, то они работают со значениями, размер которых соответствует размеру сегмента кода (т.е. 16 бит для DOS-программ, 32 бита - для Win32). Компиляторы ассемблера предлагают несколько модификаций одних и тех же команд, работающих со стеком. Например, команды pusha / popa предназначены для сохранения и восстановления всех (восьми) регистров общего назначения (16-битных значений ax, cx, dx, bx, sp, bp, si и di либо 32-битных eax, ecx и т.д в зависимости от разрядности сегмента кода), а их модификации pushaw / popaw и pushad / popad - 16- и 32-битных соответственно независимо от разрядности сегмента. Что же касается команд ret и retf, то они не имеют модификаций и всегда вынимают из стека столько байт, сколько соответствует текущему размеру сегмента, т.е. соответственно 2 и 4 для 16-битных сегментов кода и 4 и 8 для 32-битных. Однако похожая команда iret имеет модификации iretw и iretd в TASM и только iretd в MASM, причем MASM интерпретирует команду iret как 16-битную (даже в 32-битных программах), а iretd - как 32-битную (маразм, но это факт). Аналогичная ситуация с командами pushf / popf и pushfw / popfw , pushfd / popfd. Кстати, записать в стек можно не только значение регистра, но и значение из памяти (например, push es:[bx])...



Ась? Почему компилятор ругается на число B800?

Угу!При использовании шестнадцатеричных чисел в программе на ассемблере важно помнить, что если число начинается с буквы (A..F), то перед ним необходимо ставить нолик. Т.е. нельзя использовать mov ax,B800h, т.к. в этом случае компилятор будет думать, что B800h - это идентификатор (имя) константы, метки и т.д. Правильным будет написание mov ax,0B800h.



Ась? Как изменить (установить/сбросить) определенный бит в байте?

Угу!Нумеровать биты принято с нуля, причем младший бит (нулевой) имеет маску 1, первый - 2, второй - 4, третий - 8 и т.д, т.е. маска = 2n (или 1 shl n), где n - номер бита. Управлять битами можно с помощью инструкций процессора i8086 and, or и xor, а так же с помощью инструкций процессора i386 btr, bts и btc:
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.



Ась? Как проверить код нажатой клавиши?

Угу!При нажатии или отпускании клавиши клавиатуры генерируется запрос на прерывание №1 (IRQ1), в DOS этому соответствует прерывание 9 (int 9). Это прерывание анализирует код клавиши, который она читает из порта 60h, и записывает соответствующее значение либо в буфер клавиатуры, либо в слово состояния клавиш-аккордов (Ctrl, Alt, Num Lock и т.д). Для чтения кода нажатой комбинации клавиш используется int 16h. Вот некоторые из его функций:
Чтение кода комбинации клавиш:
Вход:  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 / int 10h.
Формат вызова данной функции таков:
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 курсор будет находиться в конце строки (на следующем символе после восклицательного знака, т.е. на пробеле), цвет и фон которого остались прежними (скорее всего это будет серый цвет на черном фоне), и новая строка будет заполнена этим же цветом и фоном; получится ништяк :) .



Ась? Как вывести на экран десятичное и шестнадцатеричное значение регистра?

Угу!Для вывода на экран значения регистра необходимо сначала перевести число в строку, для чего нужно выполнить ряд несложных действий:
  1. Разделить значение регистра на 10 (или 16).
  2. Частное поместить обратно в регистр, а остаток будет значащей цифрой. Полученные таким образом значащие цифры мы будем записывать в обратном порядке.
  3. Если значение регистр стало нулевым, выходим из цикла, иначе переходим к п.1.
При этом необходимо учитывать особенности работы инструкций деления. Если мы делим на двойное слово (например, ecx), то в качестве делимого выступает пара регистров edx:eax (edx - старшая часть, eax - младшая), если на слово, то - пара регистров dx:ax, а если на байт, то - регистр ax. При это частное не должно превышать разрядность делителя (32/16/8 бит, и соответственно, будет записано в eax, ax, al, а остаток - в edx, dx, ah), иначе произойдет вызов прерывания 0. Т.о, для деления регистра ax на 10 лучше очистить значение регистра dx и поместить делитель в 16-битный регистр, например, cx. Если же делить регистр ax на 8-битное значение (например, cl), то при значении ax ≥ 2560 произойдет вызов прерывания 0, т.к. частное (≥ 256) не может быть записано в регистр al. При делении с учетом знака прерывание произойдет, если не будет выполняться условие -1290 < ax < 1280.

Пример программы (в формате DOS COM), выводящей на экран десятичное значение регистра ax:
; 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 и удалите строки, выделенные курсивом (т.к. буквы в качестве цифр уже использоваться не будут).



Ась? Как выполнить быстрое сложение больших чисел?

Угу!Для выполнения быстрого сложения больших чисел (т.е. чисел, хранящихся в памяти, и состоящих из большого количества байт) лучше всего подходят инструкции add и adc (с добавлением инструкций aaa и daa для неупакованных (ASCII) и упакованных (BCD) двоично-десятичных чисел), заключенные в цикл:
; Процедура сложения больших чисел
; На входе: в стеке полный адрес 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-страницы).



Ась? Как вычислить функцию Round(Y+R*Sin(Angle)) с помощью сопроцессора, если все переменные целые?

Угу!Как это ни покажется странным, математический сопроцессор (FPU) тоже имеет стек. Размер этого стека - 8 числовых значений по 80 бит. Обычно элементы стека называют регистрами стека сопроцессора и именуют от st(0) (вершина стека) до st(7) (дно стека). Так как сопроцессор работает с вещественными (дробными) числами, то и стек содержит именно вещественные значения. Для работы со стеком существуют специальные инструкции, например, fld (загрузка в стек вещественного числового значения), fild (загрузка в стек целого числового значения и преобразование его в вещественное), fst и fist (чтение без удаления значения с вершины стека), fstp и fistp (чтение и удаление значения с вершины стека) и др. Причем команды fist и fistp не только читают значение с вершины стека, но и округляют его до ближайшего целого. Обмен значениями из стека может производиться только с памятью размером в 32, 64, 80 бит (для чтения/записи вещественных чисел) или в 16, 32, 64 бита (для обмена целыми числами). При записи/удалении элементов стека сопроцессора другие элементы смещаются, т.е, например, после выполнения команды fld содержимое st(0) окажется в st(1), а st(1), в свою очередь, - в st(2) и т.д. При выполнении какой-либо математической операции (сложение, вычитание, квадратный корень и т.д) из стека вынимается необходимое количество значений (с вершины, если не указано операндов), выполняется операция и результат записывается обратно в стек. Например, при использовании команды fdiv без операндов сопроцессор вынимает из стека значения st(0) и st(1) и записывает в регистр st(0) частное st(0) / st(1), а при использовании fdivr - частное st(1) / st(0). Важно помнить, что при использовании тригонометрических функций необходимо указывать значения не в градусах, а в радианах. Более подробную информацию о математических сопроцессорах и их работе можно прочитать в специальной литературе.

Пример программы (в формате DOS COM), записывающей в ax значение Round(Y+R*Sin(Angle)):
; tasm /m fpu.asm
; tlink /t /x fpu.obj

.MODEL Tiny
.386
.CODE
ORG	100h

Start:

		mov	dx,120         ; Angle = 120
		mov	bx,-10         ; R = -10
		mov	ax,2           ; Y = 2

		finit                  ; Инициализируем сопроцессор (FPU)

		mov	Tmp,dx         ; Сохраняем Angle во временной переменной
		fild	Tmp            ; Загружаем Angle в стек FPU
		fmul	PiDiv180       ; Умножаем: st(0) = Angle * (Pi/180)

		fcos                   ; Косинус: st(0) = Cos(Angle*Pi/180) = -0.5

		mov	Tmp,bx         ; Сохраняем R во временной переменной
		fimul	Tmp            ; Умножаем: st(0) = R * Cos(Angle*Pi/180) = 5

		fistp	Tmp            ; Сохраняем результат во временной переменной
		fwait                  ; Синхронизируем работу CPU и FPU (i8087..i387)
                                       ; (т.к. далее используем Tmp, которое записывает FPU)
		add	ax,Tmp         ; ax = Y + Round(R * Cos(Angle*Pi/180)) = 7

		aam                    ; Делим al на 10 (трюк), частное в ah, остаток - в al
		xchg	al,ah          ; Теперь в al - младшая цифра, а в ah - старшая
		add	ax,'00'        ; Переводим число в десятичное
		int	29h            ; Выводим старшую цифру
		mov	al,ah
		int	29h            ; Выводим младшую цифру

		int	20h            ; Выходим из программы

PiDiv180	dd	0.017453292519943296 ; Pi/180 (думаю, точности dword достаточно)
Tmp		dw	?

END		Start




Ась? Можно ли выделить в DOS памяти больше, чем 64Kb?

Угу!При запуске DOS-программы в формате COM или EXE, написанной на TASM и MASM, ей выделяется вся свободная память. Каждый выделенный участок памяти DOS имеет заголовок, равный 16 байтам (т.е. одному параграфу), который называется блоком управления памятью (Memory Control Block). Этот блок имеет следующий формат:
СмещениеРазмерОписание
+00h1Тип блока: 'Z' (5Ah) - последний блок, иначе - 'M' (4Dh)
+01h2Сегмент PSP владельца блока
+03h2Размер блока памяти в 16-байтовых параграфах
+05h3Не используется (зарезервировано)
+08h8Имя программы (для 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) без каких-либо предварительных манипуляций (т.е. из приведенного примера можно удалить строки, выделенные курсивом).



Ась? Как прочитать файл с диска?

Угу!Для работы с файлами DOS имеет довольно много функций int 21h, вот некоторые из них:
Создать и открыть файл:
Вход:  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




Ась? Как организовать обход всех каталогов диска?

Угу!Для поиска файлов/каталогов на диске существует несколько функций DOS (int 21h):
Начать поиск файлов/каталогов:
Вход:  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. Какие же данные и в каком формате хранятся в этом буфере? Давайте разберемся:
СмещениеРазмерОписание
+00h21Данные, используемые функцией 4Fh (для нас не представляют интереса)
+15h1Атрибуты найденного файла/каталога (см. ниже)
+16h2Время создания/редактирования файла/каталога (в специальном формате)
+18h2Дата создания/редактирования файла/каталога (в специальном формате)
+1Ah4Размер файла
+1Eh13Имя файла/каталога в формате ASCIIZ
Т.о. при каждом вызове функций 4Eh и 4Fh в DTA записывается информация только об одном найденном файле или каталоге. Для того, чтобы определить файл это или каталог необходимо проанализировать байт, содержащий информацию об атрибутах найденного файла/каталога:
№ битаМаскаНазвание атрибутаОписание
001hRead onlyТолько для чтения (не может быть изменен или удален)
102hHiddenСпрятанный файл
204hSystemСистемный файл
308hVolume labelМетка тома (диска)
410hDirectory entryКаталог
520hArchiveФайл не архивирован (обычный файл)
При передаче информации об атрибутах функции 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




Ась? Можно ди определить тип BIOS'а и его серийный номер?

Угу!Я знаю только два способа определения типа BIOS'а: с помощью чтения строк из специальных адресов памяти и с помощью функции ah=0C0h / int 15h. Рассмотрим сначала первый способ... Award BIOS содержит несколько строк по фиксированным адресам памяти. Все эти строки хранятся в формате LString, т.е. первый байт строки содержит ее размер, а остальные - текст. Но у строк Award BIOS есть одна особенность: последний байт строки - нулевой символ. Строка по адресу 0F000h:0E060h содержит имя фирмы-изготовителя и версию BIOS'а (например, "Award Modular BIOS v4.51PG"), строка по адресу 0F000h:0E0C0h - вроде как информацию о материнской плате, для которой он написан ("FOR 6BX67 AGP/PCI/ISA MODE VER:1.1"), а строка по адресу 0F000h:0EC70h - id: дату изготовления и серийный номер ("06/21/1998-i440BX-W978TF-2A69KE99C-01"). Из всего этого можно сделать вывод, что если первая строка содержит слово "Award" (причем, как мне кажется, в первой позиции), то это Award BIOS. Об American Megatrends Inc. (AMI) BIOS'е я могу сказать гораздо меньше: только то, как определить, что это именно AMI BIOS. Как известно, ROM BIOS находится в памяти между адресами 0F000h:0E000h и 0F000h:0FFFFh (8Kb), т.о, если поискать в этой области определенные строки, то наверное, можно найти информацию о любом BIOS'е. Для AMI BIOS'а нужно поискать в этой области строку "American Megatrends" (судя по исходникам, она находится в первых 256 байтах). Хочу добавить, что вся эта информация была взята не из справочников, а из исходников программы HDD Speed v2.0 (Михаила Радченко). Теперь приведу информацию из справочников Ralf'а Brown'а (см.
Полезные ссылки) в виде таблицы:
АдресСтрокаКомпьютер
0F000h:0E076h"DELL" или "Dell"Dell
0F000h:000F8h"HP"Hewlett-Packard
0F000h:0FFEAh"COMPAQ"Compaq
0F000h:0C000h и 0F000h:0FFFEh21h и 0FFh соответственноTandy 1000
0F000h:0C000h"WANG"Wang PC
0F000h:0E010h"TOSHIBA"Toshiba laptop
Через байт после таблицы ROM (см. ниже)"COPYRIGHT AST RESEARCH"Некоторые AST
0F000h:0E010h"TOSHIBA"Phoenix 386 BIOS
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

При написании резидентной программы возникает несколько нюансов:
Теперь перейдем к написанию резидентной программы под Windows. Тут дело несколько усложняется тем, что программы для Windows работают в защищенном режиме, к тому же, необходимо знать немного об устройстве самой Windows. Информацию о защищенном режиме можно найти на сайтах, посвященных данному вопросу (см. Полезные ссылки), а об устройстве Windows нужно знать как минимум то, что она использует плоскую модель памяти, таблица дескрипторов прерываний не защищена от записи только в Windows 9X, а аппаратные прерывания отображаются на векторы от 50h до 5Fh для Windows 9X и от 30h до 3Fh для Windows NT (а не 08h..0Fh и 70h..78h, как в DOS). Т.к. в Windows NT мы не можем редактировать дескрипторы прерываний, описанный ниже алгоритм будет работать только в Windows 9X. Всю прочую интересующую Вас информацию Вы можете получить из соответствующих источников, здесь же я приведу только исходник для MASM32, созданный на основе исходника, который написал Ged (программа издает звуковой сигнал из PC Speaker'а при нажатии и отпускании любых клавиш, исходник должен быть в кодировке Windows):
; 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 (т.е. секцию, содержащую код программы) с возможностью чтения, записи и исполнения.



Ась? Как работают инструкции типа movsd под Windows?

Угу!Разница в работе таких инструкций, как movsd (lodsd, stosd, scasd, cmpsd и их аналогов, обрабатывающих байты и слова: movsb, movsw и т.д) имеют следующие отличии в программах для Windows:
Что же касается инструкций insd (insb, insw) и outsd (outsb, outsw), то они, как в программах для DOS, работают с портом dx (хотя на первый взгляд может показаться, что с edx).

Инструкции loop (loope, loopne) под Windows будут работать с регистром ecx, однако, если Вы хотите, чтобы они работали с регистром cx, используйте loopw (loopwe, loopwne), а если Вы хотите, чтобы эти инструкции работали с регистром ecx в программах для DOS, используйте loopd (loopde, loopdne).

Инструкции для работы со стеком будут записывать и извлекать одиночное слово при записи 16-битных регистров (ax, bx и т.д) и двойное слово - при записи 32-битных и сегментных регистров, а также регистра флагов (более подробно о работе со стеком см.
Что такое стек и для чего он нужен?).

Что же касается инструкций умножения (mul, imul) и деления (div, idiv), то под Windows они работают по тем же правилам, что и под DOS (см. Как вывести на экран десятичное и шестнадцатеричное значение регистра?).

Инструкция xlat тоже изменена и, как Вы уже, наверное, догадались, работает с регистрами ebx и al.



Ась? Как создать DLL-файл?

Угу!Любая функция DLL обязана сохранять сегментные регистры при их изменении и регистры ebx, esi, edi и ebp (последний сохраняется автоматически, т.к. директива .MODEL содержит слово StdCall). Остальное, я думаю, будет понятно из исходника (вариант для TASM):
; 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 (отдельно от остальных исходников) архив, содержащий файлы этого каталога. Архив можно скачать прямо
здесь.



Ась? Программа под Windows работает с логическими (селектор:смещение) адресами или с линейными?

Угу!Хорошо на данный вопрос ответил
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 грузит РЕ-файл, она смотрит атрибуты секций и соответственно им устанавливает "защиту" страниц памяти, в которые будет загружена секция. Это и есть типа страничная защита. Как видим, сегменты тут вроде не при делах.



Разные интересности...

Не мешай!



Сокращение длины инструкции на 1-2 байта :)

Слушай батьку! Сократить длину некоторых инструкций можно за счет использования других, более компактных инструкций, выполняющих почти те же действия. Однако такие замены могут привести к изменению других регистров и флагов, помните об этом! Вот некоторые из таких манипуляций (синими цифрами в квадратных скобках указано количество байт, которое экономится в 16-битных программах):



Как проще разделить регистр al на константу или объединить значения в регистрах ah и al?

Слушай батьку! Самый простой способ деления числа, находящегося в регистре al на константу (кроме использования инструкции shr, которая более предпочтительна, если константа является степенью двойки, т.е. 2,4,8,16 и т.д) - это использование инструкции aam (символьная коррекция результата умножения двух однобайтовых ASCII-чисел). Удивлены? Давайте разберемся, как работает данная инструкция. Инструкция делит значение регистра al на 10, при этом частное записывается в ah, а остаток - в al. Но это еще не все! Эта инструкция может иметь операнд - константу, на которую нужно делить регистр al. По умолчанию это значение равно 10, поэтому инструкция aam без операндов делит число именно на 10. Т.о, можно, например, перевести байт в 16-ричный вид. Для этого нужно просто выполнить aam 16, и мы получим старшую цифру в ah, а младшую в al. Вот пример вывода на экран 16-ричного значения байта al:
		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 :) .



Самый простой и короткий способ определения модуля числа в регистре

Слушай батьку! Обсуждение данного вопроса осуществлялось несколько лет назад в одном из Zin'ов какой-то вирусной команды (информация от
rivitna). Задачей было определение модуля (т.е. абсолютного значения) числа, находящегося в регистре (допустим, ax). Лучший ответ дала девушка-вирмейкер! Вот ее код:
		neg	ax
		jl	$-2
Алгоритм довольно простой. Сначала меняем знак регистра с помощью инструкции neg, которая по окончании своей работы устанавливает флаги в соответствующие значения. Если результат получился отрицательным (т.е. изначально число было положительным, при этом флаг sf=1) необходимо повторить операцию. Но! Если исходное число = -32768 (8000h), т.е. оно не может быть преобразовано в положительное число 32768, т.к. максимальное знаковое целое = 32767 (при этом после выполнения инструкции neg флаг of=1), то произойдет зацикливание. Т.о, инструкция js нам не подходит, и нам необходимо выбрать такую инструкцию, которая осуществляла бы переход только при сочетании флагов sf=1, of=0. Для этих целей как раз подходит инструкция jl. Заметьте, что инструкция jl осуществляет переход также при сочетании флагов sf=0, of=1, но так как после инструкции neg такого сочетания быть не может, то нам это и не грозит.



Самый быстрый способ извлечения квадратного корня без использования сопроцессора

Слушай батьку! Этот метод придумал я сам (хотя он наверняка уже существовал и раньше) и основан он на переборе значений всех битов результата по отдельности!:
  1. Принимаем R = 0, а в качестве первого бита, с которым мы будем работать - старший (с номером B).
  2. Устанавливаем бит с номером B у числа R и возводим полученное в квадрат.
  3. Если результат получился больше исходного значения (из которого извлекаем корень), сбрасываем бит B.
  4. Если B = 0, получаем результат в R, иначе повторяем работу со следующим битом (B = B - 1) с пункта 2.
А вот и реализация данного алгоритма на ассемблере (извлекает корень из значения регистра ax и записываем результат (округлив его в меньшую сторону) обратно в ax):
		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




Как правильно искать в файле код, выданный отладчиком?

Слушай батьку! Иногда при отладке программ (особенно при взломе) бывает необходимо найти определенный код (набор инструкций) именно в файле. Это, конечно, можно сделать вычислив смещение начала нужного кода относительно PSP, а затем - относительно начала файла, но на мой взгляд, проще это сделать с помощью поиска комбинации байт, соответствующих нужному коду. Хотя тут тоже есть некоторые нюансы, но они довольно легко решаются. Главное здесь - помнить о том, что некоторые команды могут кодироваться по-разному (например, add ah,al можно закодировать как 02h,0E0h и как 00h,0C4h), а также об относительной адресации некоторых инструкций и о том, что в программе формата DOS EXE может присутствовать таблица перемещений (relocation table). Первые две проблемы могут возникнуть, если Вы будете искать в файле код по имени инструкции. Т.е, если Вы будете искать с помощью программы hiew инструкцию add ah,al, то найдете только байты 02h,0E0h, хотя в Вашей программе она может быть закодирована как 00h,0C4h. При поиске инструкции jmp 1234h Вы вообще ничего не найдете, т.к. эта инструкция использует косвенную адресацию, а адрес 1234h, выданный отладчиком - это абсолютный адрес :) . Относительная адресация - это такой способ адресации, при котором в код инструкции записывается не абсолютный адрес (смещение относительно начала сегмента), а относительный (смещение относительно начала следующей команды). Отладчики же преобразуют относительный адрес в абсолютный для того, чтобы программисту было понятно, по какому адресу будет производиться переход. Относительная адресация используется в инструкциях условного и безусловного внутрисегментного перехода и вызова процедур при указании абсолютного (как это ни странно) адреса в качестве операнда. Речь идет об инструкциях jmp, je, jne, ja, js, jcxz и т.д, loop, loopz, loopnz и call. Варианты типа jmp 0F000h:0FFF0h, jmp ax или call word ptr [bx] не используют относительной адресации, т.к. первая осуществляет прямой межсегментный переход, а вторая и третья - переход по адресу, записанному в регистре и в ячейке памяти соответственно... Короче говоря, мораль сей басни такова, что лучше всего искать код по байтам, а не по названию инструкций! Т.е, если отладчик выдает add ah,al + push ax + jmp 1234h, то нужно посмотреть, какие байты соответствуют данным инструкциям (например, 02h,0E0h, 50h, 0E9,7Dh,01h), и искать именно эти байты. При поиске кода в файлах формата DOS EXE может возникнуть еще одна проблема, которая связанна с таблицей перемещений. Таблица перемещений (relocation table) - это таблица 4-байтовых адресов (сегмент:смещение) относительно начала кода программы, по которым автоматически при загрузке программы добавляется значение сегмента, по которому загружены первые байты кода программы. Для чего это нужно? Это нужно, например, для получения сегмента данных с помощью пары mov ax,@data + mov ds,ax. При компиляции значение @data заменяется константой, которая определяет сегмент данных относительно сегмента начала кода (т.к. программа может быть загружена по любому адресу, а разница между сегментами данных и начала код всегда постоянно). Теперь, если мы добавим к этому значению сегмент начала кода, то получим сегмент данных. Обычно элементы таблицы перемещений указывают на 2-байтовые константы внутри инструкций mov (например на адрес слова 1 в последовательности mov ax,1 + mov ds,ax) и инструкций межсегментного перехода jmp и call (например, на 2 в call 2:100h). При этом отладчик выдаст на экран уже переведенные с помощью данной таблицы адреса, а не те, что хранятся в файле (т.е, не mov ax,1, а например, mov ax,4F2Ah, если программа загружена по адресу 4F29h, а PSP, соответственно, равно 4F19h), поэтому при обнаружении подобных конструкций следует искать код по байтам рядом находившихся инструкций.

Для более четкого понимания сказанного приведу пример, как нужно искать код, если отладчик выдает строки:
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.



Полезные ссылки

Программирование на ассемблере под различные операционные системы (русский):
Windows Assembly Site (WASM)

Отличные справочники по прерываниям, портам, адресам памяти и пр. в виде текстовых файлов:
Ralf Brown's Interrupt List (HTML)                     Ralf Brown's Interrupt List (FTP)

Огромнейший каталог технической информации от Microsoft (можно заблудиться):
Microsoft Developer Network (MSDN)

Файл помощи по функциям Win32 API на сайте "Мир программирования"

Ведущий источник технической информации о процессорах x86:
Sandpile.org                     Весь сайт в архиве

Smart ASM для 32-разрядных процессоров от 80386 до Pentium-4
(с подробным описанием защищенного режима)

Оптимизация кода для процессоров Pentium:
Help-файл                     Сайт

Файловая поисковая система:
Filesearch.ru




Speed Up Internet Connection!

FAQ составил Красников Е.П. (aka Jin X), © 2002
Форум на Исходниках.Ру, раздел Ассемблера

Благодарю всех, кто отвечает на вопросы форума,
а также тех, кто их задает (иначе было бы скучно).

Оригинал: http://pascal.sources.ru/asm/faq/index.htm



Пока!