Помощь - Поиск - Пользователи - Календарь
Полная версия: Азы (встроенный ассемблер)
Форум «Всё о Паскале» > Современный Паскаль и другие языки > Ассемблер
BlackShadow
От нечего делать опишу основные моменты при программировании на встроенном ассемблере в Паскале.

Есть 2 способа задействовать эту замечательную возможность:
  1. Заключить код на ассемблере между словами asm и end
  2. Написать процедуру с указанием директивы Assembler, что в принципе не сильно отличается от первого варианта, т.к. код по прежнему находится между asm и end smile.gif
Зачем это нужно?
Ну, на самом деле применений целое море, о чём можно убедиться полистав этот раздел форума.

Рассмотрим вопрос адресации в реальном режиме (именно он используется "by def" при компиляции в BP).

Адрес состоит из двух частей: сегментной части и смещения. Обе части являются 16-ти разрядными двоичными числами или, что на практике и применяется, 4-х разрядными шестнадцатиричными.

Рассмотрим пример и на нём разберёмся, что и какая часть значит:

Пусть сегментная часть (далее Seg) = $ABCD, а смещение (далее Ofs) = $1234. Это означает, что эта пара Seg:Ofs хранит следующий адрес Seg * $10 + Ofs = $ABCD * $10 + $1234 = $ABCD0 + $1234 = $ACF04. Как легко заметить, пользуясь таким способом адресации мы можем указать адрес любой ячейки памяти в пределах первого мегабайта ($00000..$FFFFF) и даже чуть-чуть больше, но это не имеет значения, т.к. процессор в реальном режиме даёт доступ только к первому МБ.

Возникает естественный вопрос: а зачем нужен этот геморрой, и почему нельзя просто указывать полный адрес? А вот нельзя. А потому, что процессор при работе с памятью опирается на информацию, которая хранится в его регистрах, а т.к. мы используем 16-битный вариант команд, то, соответственно, в 1 регистр более 16 бит (4 16-ричные цифры) не впихнуть. Поэтому и приходится использовать 2 регистра: сегментный и какой-нибудь, который можно использовать для адресации.

Рассмотрим предназначение регистров процессора:
  • CS - Code Segment, сегмент кода. В этом регистре хранится сегментная часть адреса исполняемой команды.
  • DS - Data Segment, сегмент данных. Тут хранится сегментная часть адреса тех данных программы, которыми она пользуется в текущий момент.
  • SS - Stack Segment, сегмент стека. Хранит -||- области памяти, которая сейчас используется как стек.
  • ES - Extended Segment, дополнительный сегмент. Используется для того, чтобы программа могла использовать данные хотя бы из 2 участков памяти, которые не находятся рядом (например, в DS $1000, а в ES - $F000), а так же для некоторых команд, но об этом не сейчас.
  • AX - Accumulator, регистр общего назначения, т.е. можно его использовать как угодно, но отведён на математические вычисления, т.к. некоторые команды требуют того, чтобы их операнд находился именно в AX, а некоторые команды с использованием AX выглядят на байт короче после компиляции, нежели с каким-нибудь другим регистром.
  • BX - Base, тоже регистр общего назначения, но его конёк в том, что его можно использовать для адресации, т.е. число, хранящееся в нём при необходимости может быть расценено, как часть адреса - смещение.
  • CX - Counter, выделяется от остальных общих регистров тем, что команда реализации циклов (ну ещё пара, но куда реже) предполагает свой аргумент именно в CX. Т.е. при программировании на ассемблере CX выступает в роли Паскалевского i smile.gif
  • DX - Data. Просто регистр общего назначения. Такой на всякий случай - ничего специфического в нём нет.
  • IP - Instruction Pointer. Этот регистр хранит смещение текущей команды, т.е. пара CS:IP указывает на команду в памяти, которая вот-вот будет выполнена процессором.
  • SP - Stack Pointer. Хранит смещение верхушки стека (SS:SP). Очень важное значение, т. к. процессор при обработке команд работы со стеком рассчитывает именно на этот регистр.
  • BP - Base Pointer. Регистр для хранения смещения. Как правило используется в процедурах для адресации переменных через стек. Отличается от SP тем, что процессору его значение не особо интересно, но адресация через этот регистр идёт так же по SS (если не указать другой).
  • SI, DI - Source Index и Destination Index. Два регистра предназначенные для хранения смещений. Особенностью их является то, что некоторые команды рассчитывают, что их операнды хранятся именно тут.
Средствами паскаля мы можем подучить текущие значения регистров DS (функция DSeg), SS (SSeg) и CS (CSeg).

Теперь давайте рассмотри примитивный набор команд.

mov dst, src копирует значение src в dst. Есть один важный момент: командой mov нельзя скопировать значение одной переменной в другую за один приём.
Примеры: (Показать/Скрыть)


inc p увеличивает на 1 значение операнда. После компиляции эта команда занимает меньше места чем команда прибавления единицы.
Примеры: (Показать/Скрыть)


dec p соответственно уменьшает операнд на 1.

add dst, src прибавляет к src значение dst. Результат сохраняется в dst, так что просто число там написать нельзя.
Примеры: (Показать/Скрыть)


sub dst, src вычитает из dst значение src.

mul n умножает значение регистра AL (AX) на n. Если n размером в 1 байт, то происходит следующее: AX = AL * n, если же слово, то старшие 16 бит произведения сохраняются в DX, а младшие в AX, т. е., умножив AX=$1010 на $100 получим в DX $0010 и в AX $1000.
Примеры: (Показать/Скрыть)


div n делит значение в AX (DX:AX, как в команде mul) на n. При этом остаток сохраняется в AH (DX), а целая часть от деления в AL (AX).
Например: (Показать/Скрыть)


cmp a, b - сравнивает значения a и b и устанавливает флаги процессора в соответствии с результатом сравнения.
Например: (Показать/Скрыть)


jmp L - команда безусловного перехода на метку L. То что в BP называется GoTo.
Например: (Показать/Скрыть)


j<cc> L - серия команд условного перехода. Тут <cc> определяет условия перехода:
  • jz, je - переход, если равно
  • jb - переход, если меньше (числа расцениваются как беззнаковые)
  • ja - переход, если больше (числа расцениваются как беззнаковые)
  • jl - переход, если меньше (числа расцениваются как знаковые)
  • jg - переход, если больше (числа расцениваются как знаковые)
Также допускаются комбинации с буквой n (отрицание) и e (равенство).
Например: (Показать/Скрыть)


Рассмотрим ещё команду loop L она сравнивает CX с 0 и, если он отличен, то уменьшает его на 1 и делает переход на указанную метку.
Пример: (Показать/Скрыть)


Теперь для закрепления сказанного рассмотрим реализацию вычисления факториала:

Function Factorial(n: Integer): Integer; Assembler;
Asm
mov CX, [n] {Проверим, может n<0?}
cmp CX, 0 {Сравним с 0}
jl @@1 {Если меньше, то считать не будем}
mov AX, 1 {Начальное произведение}
@@2: {В CX мы уже загрузили кол-вл итераций, так что к циклу готовы}
mul CX {Домножим текущее произведение на значение счётчика}
loop @@2 {И продолжим цикл}
jmp @@3 {Произведение вычислено - можно выходить из функции}
@@1: {А, если попросили вычислить факториал отрицательного числа}
mov AX, 0 {То вернём 0}
End;

Var n: Integer;
Begin
 ReadLn(n);
 WriteLn(Factorial(n))
End.


Стоит объяснить ещё и то, как возвращаются значения функций. Это всё зависит от типа результата:
Byte, Char - через AL
Word, Integer - через AX
LongInt - старшая часть в DX, а младшие 16 бит в AX.
Pointer - сегментная часть в DX, смещение - AX.
Остальные типы возвращаются более извращённым способом...

Так же отмечу, что убрав проверку на <0 можно переписать эту функцию так:
Function Factorial(n: Integer): Integer; Assembler;
Asm
mov CX, [n]
mov AX, 1
@@1:
mul CX
loop @@1
End;

Правда проблема переполнения остаётся, но зато покажите мне компилятор, который стандартное
Function Factorial(n: Integer): Integer;
Var
 i, Res: Integer;
Begin
 Res:=1;
 For i:=2 To n Do
   Res:=Res*n
 Factorial:=Res
End;

скомпилирует вот так вот красиво...
Dark
Цитата
Рассмотрим ещё команду LOOP <L> она сравнивает CX с 0 и, если он отличен, то уменьшает его на 1 и делает переход на указанную метку. Пример:
MOV CX,5
l3:
INC AX
LOOP l3 {Пять раз увеличит AX на 1}


ОШИБКА!!! rolleyes.gif

LOOP СНАЧАЛА вычитает 1 а ЗАТЕМ только сравнивает с 0, и если результат не нулевой то делает переход на метку, т.е. такой цикл как

MOV CX,0
MOV AX,0
l3:
INC AX
LOOP l3

НЕ игнорируеться, НИЧЕГО НЕ СДЕЛАВ, а выполнится 65536 раз, и в результате у нас окажется в AX 0.

Кстати, команда REP, тоже используемая для циклов СНАЧАЛА проверяет CX на 0 А ПОТОМ вычитает 1. Но это команда обработки цепочек, и она не обрабатывает кусок программы (хотя кто мешает написать rep call PodProgram??)

т.е.
MOV CX,0
MOV AX,1
rep INC AX

ничего не даст. Т.е. у нас в AX будет 2, так как один раз он команду обработает
Dark
Флаги


В архитектуре компьютера существует флаговый регистр FLAGS, занимающий 16 бит (начиная с процессора 80386 этот регистр расширен до 32-х битного EFLAGS, но его младшее слово по прежнему называется FLAGS для совместимости с 8086/80286), и содержащий значения флагов, которые управляются различными командами для индикации состояния операции. Флаги содержат значение до тех пор, пока другая операция не поменяет их состояния.

Всего из 16 бит используются 9.
     Номер бита:  15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
Флаг: * * * * O D I T S Z * A * P * C


ОПИСАНИЕ(Справа налево) :
  • С: CF (Carry Flag) - флаг переноса.
    Содержит значение "переносов" (0 или 1) из старшего разряда при арифметических операциях и некоторых операциях сдвига и циклического сдвига;
  • P: PF (Parity Flag) - флаг четности.
    Проверяет младшие восемь бит результатов операций над данными.
    Нечетное число бит приводит к установке этого флага в 0, а четное - в 1 (не следует путать флаг четности с битом контроля на четность);
  • A: AF (Auxiliary Carry Flag) - дополнительный флаг переноса.
    Устанавливается в 1, если арифметическая операция приводит к переносу четвертого справа бита (бит номер 3) в регистровой однобайтовой команде.

    Данный флаг имеет отношение к арифметическим операциям над символами кода ASCII и к десятичным упакованным полям (BCD).
  • Z: ZF (Zero Flag) - флаг нуля.
    Устанавливается в качестве результата арифметических команд и команд сравнения. Как это ни странно, ненулевой результат приводит к установке нулевого значения этого флага, а нулевой - к установке единичного значения.

    Кажущееся несоответствие является, однако, логически правильным, так как 0 обозначает "нет" (т.е. результат не равен нулю), а единица обозначает "да" (т.е. результат равен нулю). Команды условного перехода JE (JNE) и JZ (JNZ) проверяют этот флаг.
  • S: SF (SIgn Flag) - знаковый флаг.
    Устанавливается в соответствии со знаком результата (старшего бита) после арифметических операции: положительный результат устанавливает 0, а отрицательный - 1. Команды условного перехода JG (JNG) и JL (JNL) проверяют этот флаг.
  • T: TF (Trap Flag) - флаг пошагового выполнения.
    Если этот флаг установлен в единичное состояние, то процессор переходит в режим пошагового выполнения команд, т.е. в каждый момент выполняется одна команда под пользовательским управлением.
  • I: IF (Interrupt Flag) - флаг прерывания.
    При нулевом состоянии этого флага маскируемые прерывания запрещены, при единичном - разрешены.
  • D: DF (Direction Flag) - флаг направления.
    Управляет строковыми инструкциями (MOVS, CMPS, SCAS, LODS, and STOS). Установка флага DF приводит к тому, что содержимое регистров SI и DI уменьшается (что приводит к обработке строк от старших адресов к младшим, т.е. справа налево).

    Сброс (обнуление) флага DF приводит к тому, что содержимое этих регистров увеличивается, приводя к обработке строки от младших адресов к старшим (слева направо).
  • O: OF (Overflow Flag) - флаг переполнения.
    Фиксирует арифметическое переполнение, т.е. перенос в/из старшего (знакового) бита при знаковых арифметических операциях.

Содержимое расширенного регистра EFLAGS:


Номер бита: 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
I V V A V R N IOPL O D I T S Z A P C
Флаг: 0 0 0 0 0 0 0 0 0 0 D I I C M F 0 T F F F F F F 0 F 0 F 1 F
P F


Дополнительные флаги:
  • IOPL (I/O Privilege Level) - уровень привилегий ввода-вывода.
    Это двухбитовое поле используется в защищенном режиме. Биты IOPL показывают наивысшее значение текущего уровня привилегий (CPL), позволяющее выполнять команды ввода-вывода, не приводя к обработке исключения 13 или обращения к битовой карте
  • NT (Nested Task) - вложенная задача.
    Процессор использует этот флаг для управления последовательностью прерываемых и вызываемых задач. Команда CALL временно передает управление от программы к подпрограмме. При завершении этой подпрограммы управление возобновляется с команды, следующей за командой CALL. Бит NT влияет на выполнение команды IRET.
  • RF (Resume Flag) - флаг возобновления.
    Временно приостанавливает обработку исключений для отладки (т. е. возвращает к нормальному исполнению программы) так, что исполнение команды может быть повторено после обработки исключения для отладки, не вызывая немедленно обработку другого исключения для отладки.
  • VM (Virtual 8086 Mode) - режим виртуального процессора 8086.
    Бит обеспечивает режим виртуального МП 8086 в защищенном режиме. Если он установлен, когда процессор находится в защищенном режиме, то МП 80386 переключается на работу в режиме виртуального МП 8086. Бит VM может быть установлен только двумя способами: 1) в защищенном режиме командой IRET, только если текущий уровень привилегий равен нулю; 2) переключением задачи на любом уровне привилегий.
  • AC (Alignment Check flag)
  • VIF (Virtual Interrupt Flag)
  • VIP (Virtual Interrupt Pending flag)
  • ID (IDentification flag)
Dark
Вот некоторый пример smile.gif
uses crt;

(*
Программа производит операцию с 2мя байтами. Получаем:
fn - first number, первое число
op - operation, операцию - + - * /(div) m(mod)
sn - first number, первое число

СОВЕТ: пройдите каждый шаг при помощи кнопы F7,
для просмотра действий запустите окно Register:
Debug->Register

Замечания :

0) По адресу @result у нас находится возвращаемый результат
функции, который имеет размер, указываемый в описании функции
1) Функция ADC в операции сложения корректирует результат.
Она имеет вид: dest+source+CF, т.о. ADC AH, 0 просто прибавит
к AH CF
2) Функция SBB в операции вычитания тоже корректирует результат
Имеет вид: dest-source-CF, т.о. SBB AH,0 просто вычтет из AH CF
3) В операциях деления имеется доп. проверка на то чтобы делитель
был <> 0,
cmp bl, 0
je @zero
после прыга на @zero мы возвращаем -1,
mov @result, 0FFFFh { с учетом того, что результат - integer -
это отрицательное число }
т.е. по моей спецефикации это будет обозначать деление на 0.
4) Для чего я в операции @div обнуляю AH?
xor ah,ah { вспомните операцию xor, в результате
ксорирования двух одинаковых чисел будет 0.}
Из той информации что дана выше, можно увидеть, что после
деления у нас в AH находится остаток, а в AL находится частное,
которое нам и нужно.
5) Для этого же я в операции @mod обнуляю AL.
xor al, al

Обратите внимание на еще одно действие - shr ax, 8

Этим действием я сдвигаю вправо регистр AX на 8 бит, это
эквивалентно AX = AX div 2^8, т.е. получаю в регистре AL
значение регистра AH. Это важно, т.к. я все время возвращаю
AX, который равен AH*256+AL, т.е. если в AH будет 1 а в AL 0,
то вернется 256)

То же самое можно было достичь другой комбинацией -
mov al, ah
xor ah, ah
Какую из них использовать вам - выбирайте сами.
*)
function oper(fn: byte; op: char; sn: byte): integer;
begin
asm
xor ax, ax
xor bx, bx
xor cx, cx
xor dx, dx
mov al, [fn]
mov bl, [sn]
mov cl, [op]
cmp cl, '+'
je @sum
cmp cl, '-'
je @sub
cmp cl, '*'
je @pow
cmp cl, '/'
je @div
cmp cl, 'm'
je @mod
jmp @exit
@sum:
add al, bl
adc ah, 0
mov @result, ax
jmp @exit
@sub:
sub al, bl
sbb ah, 0
mov @result, ax
jmp @exit
@pow:
mul bl
mov @result, ax
jmp @exit
@div:
cmp bl, 0
je @zero
div bl
xor ah, ah
mov @result, ax
jmp @exit
@mod:
cmp bl, 0
je @zero
div bl
xor al, al
shr ax, 8
mov @result, ax
jmp @exit
@zero:
mov @result, 0FFFFh
@exit:
end;
end;

begin
clrscr;

writeln;writeln('Cложение');
writeln('255+255=',oper(255,'+',255));
writeln('0+0=',oper(0,'+',0));

writeln;writeln('Вычитание');
writeln('0-255=',oper(0,'-',255));
writeln('255-255=',oper(255,'-',255));

writeln;writeln('Умножение');
writeln('255*255=',oper(255,'*',255));
writeln('0*255=',oper(0,'*',255));

writeln;writeln('Деление');
writeln('255/255=',oper(255,'/',255));
writeln('0/255=',oper(0,'/',255));
writeln('255/0=',oper(255,'/',0));
writeln('255/2=',oper(255,'/',2));

writeln;writeln('Остаток');
writeln('255 mod 2=',oper(255,'m',2));
writeln('255 mod 100=',oper(255,'m',100));
writeln('9 mod 10=',oper(9,'m',10));
writeln('99 mod 100=',oper(99,'m',100));
writeln('99 mod 0=',oper(99,'m',0));
end.


Вопросы? Задавайте.

Просьба - протестировать, есть ли вообще глюки.
Учтите, положительный ответ он выдает ДО 32768, а потом отрицательный.
BlackShadow
Продолжим ликбез smile.gif

Стек


Сейчас я вот возьму и расскажу про стек.
Положение стека в памяти определяется парой регистров SS:SP. Причём эта комбинация указывает именно на вершину стека.

Что с ним можно делать?
В него можно чего-то сохранять и чего-то восстанавливать. Особенностью является то, что элементы стека - 16-битные слова. Для работы с ним используются 2 команды:
PUSH <Src>, которая производит следующие действия:
  • Уменьшает SP на 2.
  • По адресу SS:SP записывает <Src>
Примеры:
  PUSH AX
PUSH [WORD PTR a]
PUSH 5 ; только я не уверен, что Pascal даёт возможность такой вот записи.


И естественно POP <Dst>, которая
  • Сохраняет значение из SS:SP в <Dst>
  • Увеличивает SP на 2
Примеры:
  POP AX
POP [WORD PTR b]


Так же есть команды PUSHA и POPA, которые сохраняют регистры AX, BX, CX, DX, SI, DI, BP в стеке (PUSHA) или восстанавливают их оттуда (POPA). По своей сути эти команды аналогичны цепочке команд PUSH или POP. Как правило PUSHA и POPA "обрамляют" код на Assembler'е, в котором происходит недопустимое изменение нескольких регистров.
Например: написали обработчик прерывания. Допустим от таймера. И в этом обработчике мы совсем не позаботились о сохранении регистров. Выполняется какая-нибудь чудная программа, которая занята какими-нибудь чудными вычислениями, а тут происходит плановое прерывание от таймера, вызывается наш обработчик, сбивает все регистры и возвращает управление к программе. В итоге в штате Колорадо банкомат на мостовую выплюнул $700 smile.gif

Стоит вспомнить и про команды PUSHF и POPF, которые сохраняют/восстанавливают регистр флагов в/из стека. Например:
CMP AX, BX
PUSHF
ADD BX, 1234h
POPF
JE TudaTo

Тут сравниваются значения регистров AX и BX, сохраняются флаги, затем производятся какие-то операции, способные изменить состояние флагов, затем они восстанавливаются вместе с результатом сравнения.

Зачем это надо?
Как уже можно понять, стек часто используется для сохранения значения регистров. Например, рассмотрим реализацию вложенного цикла:

MOV CX, 5 ;Заносим в CX кол-во итераций внешнего цикла
Outter:
PUSH CX ;Сохраним значение счётчика в стеке
MOV CX, 3 ;Заносим в CX кол-во итераций внутреннего цикла
Inner:
INC AX ;Чего-нибудь вытворяем}
LOOP Inner ;Продолжаем внутренний цикл
POP CX ;Восстанавливаем значение счётчика внешнего цикла
LOOP Outter ;Продолжаем внешний цикл


А теперь следует отметить, что некоторые команды процессора так же используют стек для сохранения регистров.

А именно команда CALL <Proc> вызывает процедуру <Proc>. И вот как это происходит:
  • В стеке сохраняется значение IP (CS:IP, если используется дальний переход).
  • Происходит JMP <Proc>.
Таким образом управление передаётся подпрограмме. А возврат из неё производится командой RET, которая просто считывает из стека сохранённое там значение IP (CS:IP). Так же работает и команда вызова прерывания INT <n>. Но надо заметить, что при возникновении аппаратного прерывания (от таймера, клавиатуры) процессор перед вызовом прерывания сохраняет в стеке регистр Flags, так что возврат из обработчика аппаратного прерывания следует производить командой IRET.

Есть ещё одно ОЧЕНЬ важное применение стека - это передача параметров в функцию. Возьмём пример:
procedure DrawWindow(x1, y1, width, height: Integer;
Caption: PChar; Flags: Integer; SomethingAlso: Integer;
OneMoreParameter: Integer; NuIEschoChtoNibud: LongInt);
Как передать параметры в такую процедуру? Через регистры, естественно не получится - их просто не хватит. Есть вариант "от MicroSoft'а" написать функцию типа Drawwindow(p: DrawWindowParameters), но это тоже не всегда удобно. В таком случае можно воспользоваться стеком. Так как местоположение стека одинаково для всех функций, то и DrawWindow сможет их оттуда извлечь. Только не командами POP, а при помощи нехитрого трюка. Скопируем в BP значение SP и будем обращаться к параметрам, как[BP+...]. Возникает серия вопросов.
  1. Почему нельзя использовать POP? А потому что в вершине стека после вызова храниться адрес на который нужно будет вернуться. Не очень удачная мысль его сбивать, т.к. при выходе из процедуры команда RET переведёт управление совсем не туда, куда хотелось бы.
  2. Почему бы не использовать сам SP? А потому что стоит только чего-нибудь PUSH и SP меняется и придётся заново пересчитывать адреса параметров.
  3. Откуда пришла такая мысль? Вирту - не знаю, а вообще компилятор BP автоматически дописывает в начале каждой процедуры, следующий код:
    PUSH BP
    MOV BP, SP

    А выражения типа MOV AX, [n] компилируются, допустим, как
    MOV AX, [BP+10]

Как закрепление, перепишем функцию Factorial с использованием рекурсии:
Function Factorial(n: Integer): Integer; Assembler;
Asm
MOV AX, [n]
CMP AX, 0
JE @@0
CMP AX, 1
JE @@1
DEC AX
PUSH AX
CALL Factorial
MOV BX, [n]
MUL BX
JMP @@2
@@0:
@@1:
MOV AX,1
@@2:
End;


Следует отметить, что функция написанная на Паскале автоматически очищает стек от параметров (это достигается путём вставки команды RET n, которая освобождает стек от указанного объёма ненужной инфы). А так же, что, если функция описана типа f(p1, p2, p3), то и при её вызове в стек надо сохранять сначала p1, затем p2 и в последнюю очередь p3.

Зачем я это говорю? А потому что в C это всё совсем наоборот: стек чистит не функция, а тот кто её вызвал и параметры сохраняются в обратном порядке.
BlackShadow
Голова болит, работа достала... Ну как тут не вспомнить про Assembler? smile.gif

Строковые команды


Из названия можно было бы подумать, что в assembler'е тоже есть что-то "стандартное" типа Length или Copy. А вот и нет. Эти команды просто довольно часто используются для работы со строками или просто с цепочками данных.

Но для начала объясню действие ещё 2 команд: LDS/LES <Reg>,<Src>, которые загружают из 4-ёх байтового указателя значения в регистр DS/ES соответственно (сегментную часть указателя) и в указанный регистр вторую часть указателя - смещение. Эти команды довольно удобны, т.к. все указатели Pascal'я имеют именно такую структуру.

Вернёмся к строковым командам. Общий принцип таков:
  • Команды предполагают, что их параметры находятся в регистрах ES:DI, DS:SI или AL/AX (зависит от самой команды).
  • Команда выполняет своё действие (зависит от самой команды)
  • изменяет регистры SI, DI если требуется. Причём, если DF = 0, то происходит увеличение, если DF = 1, то наоборот - уменьшение.
  • величина, на которую изменяются SI, DI зависит от размера параметра: слово - на 2 байта, байт - на 1.

Теперь рассмотрим сами команды.
LODSB/LODSW загружает в AL или AX соответственно (последняя буква в названии команд этого типа и обозначает размер операнда: W - слово, B - байт) значение из DS:SI. Для наглядной иллюстрации напишем функцию StrLen
Function StrLen(s:PChar):Integer;Assembler;
Asm
PUSH DS {Сохраним DS На всякий случай}
LDS SI,[s] {Загрузим в DS:SI адрес источника}
MOV CX,0 {В CX будем считать длину строки}
CLD {Укажем направление - в сторону возрастания адресов}
@@1:
LODSB {Загрузим в AL}
CMP AL,0 {В AL маркер конца строки?}
JE @@2 {Да - закончим}
INC CX {Увеличим длину строки}
JMP @@1 {И перейдём к следующему символу}
@@2:
MOV AX,CX {Результат в AX}
POP DS {Восстановим DS}
End;


Команда STOSB/STOSW сохраняет значение AL/AX по адресу ES:DI. Пример - функция, обнуляющая строку:
Procedure StrNull(s:PChar);Assembler;
Asm
LES DI,[s] {Загрузим адрес приёмника}
MOV AL,0 {Подготовим к сохранению маркер конца строки}
STOSB {Сохраним его на место самого первого символа в строке}
End;


MOBSB/MOVSW копируют байт/слово из DS:SI в ES:DI. Для примера возьмём процедуру копирования строки:
Procedure StrCopy(Dst:PChar;Src:PChar);Assembler;
Asm
LDS SI,[Src] {Загрузим адрес источника}
PUSH DS {Передадим адрес этой строки для вычисления её длины}
PUSH SI {Сначала сегментную часть, затем смещение}
CALL StrLen {Вызов StrLen}
CMP AX,0 {Длина не нулевая ли?}
JNZ @@0 {Если нет, то продолжим}
LDS SI,[Dst] {Иначе загрузим адрес приёмника}
PUSH DS {И запишем в него пустую строку}
PUSH SI
CALL StrNull
JMP @@2 {На чём и выйдем из функции}
MOV CX,AX {Переместим длину строки в счётчик}
LES DI,[Dst] {Подготовим адрес приёмника}
CLD {Укажем направление}
@@1:
MOVSB {Скопируем 1 байт}
LOOP @@1 {И перейдём к следующему}
@@2:
End;


SCASB/CMPSW Сравнивает значение источника с AL/AX. Для примера перепишем StrLen:
Function StrLen(s:PChar):Integer;Assembler;
Asm
PUSH DS
LDS SI,[s]
MOV AL,0 {Символ, который мы ищем}
MOV CX,0 {Длина строки}
CLD {Направление}
@@1:
SCASB {Сравниваем}
JE @@2 {Нашли! -> к выходу}
INC CX {Увеличиваем длину строки}
JMP @@1 {и т.д.}
@@2:
MOV AX,CX {Результат в AX}
POP DS
End;


CMPSB/CMPSW сравнивают два байта/слова из DS:SI и ES:DI и в соответствии с этим устанавливают флаги. Как пример можно привести сравнение строк, но мне, если честно, откровенно влом сейчас заниматься такими извратами.

WARNING!!! Все примеры приведённые тут являются чисто демонстрационными! Никакого практического применения они иметь не должны, т. к. далеко не оптимальны.

А вот намного всё упрощают и улучшают префиксы повторений REPxx. Возможные варианты: REP/REPE/REPNE (ну или REP/REPZ/REPNZ, если так кому больше нравится). Команда с таким префиксом выполняется CX раз, если тому не помешает условие. Следующий пример уже можно использовать в жизни:
Function StrLen(s:PChar):Integer;Assembler;
Asm
PUSH DS
LDS SI,[s]
MOV AL,0
MOV CX,$FFFF
CLD
REPNZ SCASB {Сканировать 65535 раз, пока не наткнёмся на 0}
MOV AX,$FFFF
SUB AX,CX
POP DS
End;

Можно оптимизировать эту функцию и далее, но при помощи команд, которые я ещё не описывал.

Ну и напоследок приведу пример, в котором команда из этого набора используется не для работы со строками:
Procedure VsyoNeboVPopugayah;Assembler;
Asm
MOV AX,$B800 {Адрес видео буфера для текстового режима}
MOV ES,AX {Занесём в ES}
MOV DI,0 {Обнулим DI => ES:DI указывает на видеобуфер}
MOV AX,3042h {30h - код '0' в ASCII, 24h - красным по синему}
{Возможно, надо написать MOV AX,4230h - не помню я}
MOV CX,2000 {80*25 = 2000 слов в видеобуфере}
CLD {Направление}
REP STOSW {Сохраняем 2000 попугайчиков}
End;


Ну вот вроде и всё, что я хотел сказать на эту тему...
BlackShadow
Битовые операции


Начнём с логико-арифметических операций.

NOT <n>: инвертирует <n>. Т. е. было там 11001000, а стало 00110111. Пример:

MOV AL, 0 ; 00000000
NOT AL ; 11111111
INC AL ; 00000000


AND <Dst>,<Src>: выполняет побитовый "and" <Src> и <Dst> и сохраняет результат в <Dst>. Пример:
Function NotOdd(n: Integer): Boolean; Assembler;
Asm
 MOV AX, [n] { Скопируем n в AX }
 AND AX, 1 { Обнулим все биты, кроме нулевого, который, если вдуматься и отвечает }
{ за чётность числа. Если он = 1, то число нечётное. Заметим так же, что 1 }
{ типа boolean, возвращаемая в AL соответствует True}
End;


OR <Dst>,<Src>: выполняет побитовый "and" <Src> и <Dst> и сохраняет результат в <Dst>. Пример:

 OR AX,AX ; Эта операция установит флаг ZF в том случае, если в результате
; получится 0, что возможно только, когда оба операнда равны 0
 JNZ NoZeroInAX
ZeroInAx:
; ...

NoZeroInAX:



XOR <Dst>,<Src>: как ни странно, но вытворяет "xor" с операндами. Пример:

 XOR AX, AX ; Оптимальное обнуление регистра (по комбинации скорость/размер)



И последнее из этого разряда, что приходит мне в голову, это TEST <a>,<b>. Выполняет "and" с операндами, устанавливает флаги, но результат НИКУДА не записывается. Довольно удобная команда при работе с битовыми масками, как например байт атрибутов в DOS.

Теперь поговорим о сдвигах.
SHR/SHL <Dst>,<n>: сдвигает <Dst> вправо/влево на <n> бит. Причём n должно задаваться как конкретное число (в случае использования встроенного ассемблера - при включенном $G+) или регистром CL (можно и при $G-), в котором учитываются только 4 бита (0 .. 15). На освободившееся место записываются 0. Очень удобная команда, т.к. с её помощью очень быстро умножаются/делятся числа на степень двойки, т.к. SHR AX, 3 аналогично делению AX на 16, а SHL AX, 3 равносильно умножению AX на 8.

А ещё есть ROR/ROL <Dst>,<n>, которые отличаются от SHR тем, что "ушедшие" биты не "теряются", а записываются на свободное место. Например:

 MOV AL, $0F ; 00001111
 ROR AL, 3 ; 11100001



Ну, а теперь доведём функцию StrLen (из предыдущей темы) до ума:

Function StrLen(s: PChar): Integer; Assembler;
Asm
 PUSH DS
 LDS SI, [s]
 MOV CX, $FFFF
 XOR AL, AL
 CLD
 REPNE SCASB
 MOV AX, CX
 NOT AX
 POP DS
End;

Так будет получше...
FreeMan
я читаю. кстати, вот пара процедур включения, выключения клавы

procedure keyenable;
begin
asm
in al,21h
and al,not 2
out 21h,al
end;
end;

procedure keydisable;
begin
asm
in al,21h
or al,2
out 21h,al
end;
end;
BlackShadow
По документации, ты конечно прав...
Но вот только не работает это с под WinBlows sad.gif
BlackShadow
Теория это хорошо, но перейдём к
Практика использования


Довольно редко приходится заменять то, что уже написано разработчиками из Borland'а. Всё-таки там тоже не дураки сидят. Но иногда приходится написать что-нибудь своё... А потом прикинуть и понять, что на Assembler'е это куда симпатичнее выходит.

Есть такая команда INT <n>, которая вызывает указанное прерывание. Важно отметить то, что <n> это число и только число.
Прерывания - это набор каких-либо функций, которые предоставляются BIOS'ом, DOS'ом и некоторыми дровами. Откуда процессор знает где храниться обработчик какого-нибудь прерывания? Объясняю. Есть такая таблица прерываний. Она начинается с самого начала оперативки (с адреса 00000h) и занимает 1024 байта. Там для каждого прерывания храниться вектор (адрес обработчика). Таким образом команду
INT n

можно заменить чем-то вроде

XOR AX, AX
MOV ES, AX
MOV DI, 4*n
CALL FAR [ES:DI]

Вот только сбиваются ES и AX, чего как правило делать нельзя.

Что мы можем получить от прерываний
Мы можем получить доступ к функциям BIOS'а. Они доступны через прерывания
  • $10 - работа с монитором. Текстовые и графические режимы. Основные возможности: установка режима, вывод строк, изменение цветов пикселей...
  • $11 - возвращает список периферии. Довольно стрёмный способ сбора информации о железе надо заметить.
  • $12 - возвращает объём памяти без EMA. Т. е. 1 МБ.
  • $13 - работа с дисками на уровне читать/записать сектор, отформатировать дорожку и т. д.
  • $14 - работа с COM'иками. Инициализация, пересылка данных.
  • $15 - "расширенный сервис AT". Тут есть много всякого в том числе и "Delay", только не всегда это всё работает.
  • $16 - клава. Самый минимум, зато стабильно работает. Как показывает практика, это единственное прерывание, которое не заменяется при инициализации Windows. Более того она им ещё и пользуется.
  • $17 - принтер. Тоже примитив: инициализация, печать.
  • $18 - раньше тут хранился встроенный Basic. Что там сейчас, я не знаю.
  • $19 - перезагрузка. Из под Windows не потянет конечно, но, если вызвать его из под чистого DOS'а - результат гарантирован.
  • $1A - работа с CMOS часами, таймером и т. д.
  • $1B - это прерывание вызывается самим BIOS'ом, когда тот обнаруживает нажатие Ctrl-Break. Изначально вектор этого прерывания указывает на команду IRET, но при загрузке DOS ставит на него свой обработчик, который меняет переменные DOS'а и чего-то там ещё.

Ещё вектора прерываний указывают на некоторые таблицы BIOS'а. Например таблицы параметров FDD, HDD, таблицы символов... И т.д.

Есть ещё аппаратные прерывания. Их не вызывают, они сами приходят smile.gif Они генерируются, как реакция на сигналы от периферии. Например $08 генерируется 18.2 раза в секунду по сигналу от таймера, $09 - реакция на нажатие кнопочки на клаве.
Самая крупная и, наверно, многофункциональная, группа прерываний, это прерывания установленные операционной системой. Они располагаются в диапазоне $20-$2F. И вот что они позволяют:
  • $20 - Завершить программу. Есть одна трудность, связанная с использованием этого INT'а. Он предполагает, что CS хранит адрес PSP, т.е. имеет то значение, в который его установила OS при запуске программы. Воспользовавшись этим прерыванием из процедуры с дальним вариантом вызова, в которой CS возможно и не тот, мы можем не получить ожидаемого результата. Рекомендуемый вариант: заменит вызов INT 20h на вызов ф-ции 4Ch от INT 21h.
  • $21 - "сервис DOS". Тут целая куча функций на все случаи жизни. И файловый ввод/вывод, и консольный, и локальные настройки, и много чего другого. В том числе функции, которые ещё и Windows подгружает.
  • $22 - хранит "адрес возврата из программы". Именно сюда передастся управление, когда ваша программа завершится. Не стоит вызывать его непосредственно - DOS и сам умеет.
  • $23 - вот именно его и вызывает DOS запеленговав Ctrl-Break. Сам он в зависимости от установок либо завершит программу либо забьёт на всё. При желании можно изменить этот вектор на свою процедуру и самому решать что делать. Кстати в модуле CRT, есть переменная CheckBreak, вроде, которая определяет реакцию программы на Ctrl-Break (нагло игнорируя всякие там DOS'ы).
  • $24 - вызывается DOS'ом как реакция на что-то критическое. Например монитор отвалился. На практике ни разу не сталкивался smile.gif
  • $25, $26 - прерывания-заподлисты. Они хорошо умеют читать/писать сектора на диски (за исключением FAT'а и Boot'а), но сбивают все регистры и, что самое важное оставляют лишнее слово в стеке. Так что, вызвав эти прерывания не надо забывать вставлять что-то типа POP AX.
  • $27 - то, что в Паскале назвали Keep. Завершает программу оставляя её в памяти (TSR - Terminate And Stay Resident). Яркий пример такого "вандализма" - KeyRus. Есть более гибкая функция для таких целей - 31h от INT 21h
  • $28 - презент от мелко-мягких. Не документированное прерывание, которое вызывается DOS'ом во время ввода/вывода, а точнее во время простоя. Т. е. получив вызов функции ввода строки с клавы, DOS начинает её вводить, а пока юзверь сидит и ищет кнопку, DOS методично вызывает это прерывание.
  • $2E - ещё одна такая же. Но если с той хоть что-то ясно, то с этой вообще ничего. Она выполняет команду DOS'а типа "SET PATH=C:\", но как узнать выполнилась ли она мне не известно. А ещё она сбивает SS и SP, что меня вообще шокирует. Короче, не тронь - не пахнет.
  • $2F - "мультиплексор". Сюда могут вешаться любые программы и предоставлять свой сервис. Таким образом работают и SHARE, и ASSIGN и что-то там ещё.
Вот так.
Осталась последняя группа прерываний - те, которые устанавливаются by ПО. Например INT 33h предоставляет возможность работы с мышью, если запущен *Mouse.Com. Точнее это он перехватывает это прерывание и предоставляет вам какие-то возможности.

Ну вот, хотел перейти к практике, а уже столько теории впаял...
Давайте тогда хотя бы две функции напишем, которые практически ОЧЕНЬ нужны, а создав модуль с ними, мы получаем возможность не подключать CRT только ради них (ну не нравится мне CRT - глючит он, а патчи искать как правило влом):

Function KeyPressed: Boolean; Assembler;
Asm
 MOV AH, 1 { Номер функции прерывания как правило передаётся
через AH, по крайней мере в DOS'е и BIOS'е }
 INT 16h { Работа с клавой }

{ Если в буфере клавы чего-то есть, то это чего-то возвращается в AX,
но из буфера не стирается, а если нету, то устанавливается флаг ZF }
 JNZ @@1
 XOR AL, AL {Не нажата. Надо вернуть False, т. е. 0 в AL}
 JMP @@2
@@1:
 MOV AL, 1 {Нажата. Вернём True - 1 в AL}
@@2:
End;

Function ReadKey:Word;Assembler;
{
Да вот такой вот ReadKey - не Char, а Word.
Отличие в том, что для "обычной" клавиши ReadKey=Word(Key),
а для клавиш типа F1, стрелок и т. п. ReadKey=$??00.
Мне так удобнее...
}
Asm
 XOR AH,AH {Функция №0. Если в буфере клавы что-то есть, то
читает оттуда, иначе ждёт нажатия}
 INT 16h
 OR AL,AL {Если в AL попал 0, то в AH - дополнительный код,
 т. е. результат уже подготовлен}
 JZ @@1
 XOR AH,AH {Иначе обнулим AH на всякий случай, чтобы подогнать
значение из AL до слова}
@@1:
End;

Как видите, пара строк, а CRT из-за этих двух функций подключать уже не надо. А ведь частенько при экспериментах с графикой нужно что-то типа ReadKey, чтоб паузу сделать и посмотреть, что получается.
Dark
Хых, как всегда ты на шаг впереди ;) конеечно, у тебя инет дневной ; )))))

Прерывания. дубль 2


Команда INT прерывает обработку программы, передает управление в DOS или BIOS для определенного действия и затем возвращает управление в прерванную программу для продолжения обработки. Наиболее часто прерывание используется для выполнения операций ввода или вывода. Для выхода из программы на обработку прерывания и для последующего возврата команда INT выполняет следующие действия:
  • уменьшает указатель стека на 2 и заносит в вершину стека содержимое флагового регистра;
  • очищает флаги TF и IF;
  • уменьшает указатель стека на 2 и заносит содержимое регистра CS в стек;
  • уменьшает указатель стека на 2 и заносит в стек значение командного указателя;
  • обеспечивает выполнение необходимых действий;
  • восстанавливает из стека значение регистра и возвращает управление в прерванную программу на команду, следующую после INT.

Этот процесс выполняется полностью автоматически.

Каждое прерывание зафиксировано за серией схожих задач, например прерывание 10h - работа с видео, 21h - набор DOS функций 13h - работа с винчестером, и т.д.

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

ВЫВОД СТРОКИ НА ЭКРАН (DOS прерывание)

Вывод на экран в базовой версии DOS требует определения текстового сообщения в области данных, установки в регистре AH значения 09 (вызов функции вывода на экран) и указания команды DOS INT 21H. В процессе выполнения операции конец сообщения определяется по ограничителю ($), как это показано ниже:
var 
str:string;
begin
str:='Имя покупателя?$';
asm
MOV AH,09 {Запрос вывода на экран}
LEA DX,str {Загрузка адреса сообщ}
INC DX {поскольку Str[0] содержит размер строки, его вывод нам не интересен}
INT 21H {Вызов DOS}
end;
end.


Знак ограничителя "$" можно кодировать непосредственно внутри строки: 'Имя покупателя?$', или прибавлять в процессе обработки.

Используя данную операцию, нельзя вывести на экран символ доллара "$". Кроме того, если знак доллара будет отсутствовать в конце строки, то на экран будут выводиться все последующие символы, пока знак "$" не встретиться в памяти.

Команда LEA загружает адрес области NAMPRMP в регистр DX для передачи в функцию адреса выводимой информации. Адрес поля STR, загружаемый в DX по команде LEA, является относительным, поэтому для вычисления абсолютного адреса данных DOS складывает значения регистров DS и DX (DS:DX), поэтому в тех случаях, когда данные хранятся не в основном сегменте данных необходимо изменить значение сегмента DS на нужный:
Procedure Pr(Str: String);
begin
Str:=Str+'$';
asm
{ !!! в программе относительно ds отсчитываются все данные,
поэтому его при использовании надо сохранять а затем восстанавливать }
push ds {сохраняем DS}
mov ax,ss {вот оно - строка Str лежит в стеке, это мы и указываем}
mov ds,ax
xor ax,ax
mov ah,9
lea dx, Str {загружаем в DX смещение STR относительно SS}
inc dx {тоже что и в прошлом примере}
int 21h {вызываем}
pop ds {восстанавливаем DS}
end;
end;


РАСШИРЕННЫЙ ВВОД ДАННЫХ С КЛАВИАТУРЫ: (DOS прерывание)

Расширенный ввод - значит с ограничением по количеству символов.

Процедура ввода данных с клавиатуры проще, чем вывод на экран. Для ввода, использующего базовую DOS, область ввода требует наличия списка параметров, содержащего поля, которые необходимы при выполнении команды INT. Во-первых, должна быть определена максимальная длина вводимого текста. Это необходимо для предупреждения пользователя звуковым сигналом, если набран слишком длинный текст; символы, превышающие максимальную длину, не принимаются. Во-вторых, в списке параметров должно быть определенное поле, куда команда возвращает действительную длину введенного текста в байтах.

Ниже приведен пример, в котором определен список параметров для области ввода. Первый байт содержит максимальную длину вводимых данных. Так как это однобайтовое поле, то возможное максимальное значение его - шест. FF или 255. Второй байт необходим DOS для занесения в него действительного числа введенных символов. Третьим байтом начинается поле, которое будет содержать введенные символы.
	MAXLEN: byte	{ Максимальная длина }
ACTLEN: byte { Реальная длина }
NAMEFLD: string { Введенные символы}

Для запроса на ввод необходимо поместить в регистр AH номер функции - 10 (шест. 0AH), загрузить адрес списка параметров в регистр DX и выполнить INT 21H:
	MOV AH, 0AH	; Запрос функции ввода
LEA DX, P ; Загрузить адреса списка параметров
INT 21H ; Вызвать DOS

Команда INT ожидает, пока пользователь не введет с клавиатуры текст, проверяя при этом, чтобы число введенных символов не превышало максимального значения, указанного в списке параметров (20 в нашем примере). Для указания конца ввода пользователь нажимает клавишу Return. Код этой клавиши (шест. 0D) также заносится в поле ввода (NAMEFLD в нашем примере). Если, например, пользователь ввел имя BROWN (Return), то список параметров будет содержать информацию:

             дес.:  ¦20¦ 5¦ В¦ R¦ O¦ W¦ N¦ #¦  ¦  ¦  ¦  ¦ ...
шест.: ¦14¦05¦42¦52¦4F¦57¦4E¦0D¦20¦20¦20¦20¦ ...


Во второй байт списка параметров (ACTLEN в нашем примере) команда заносит длину введенного имени - 05. Код Return находится по адресу NAMEFLD + 5. Символ # использован здесь для индикации конца данных, так как шест. 0D не имеет отображаемого символа. Поскольку максимальная длина в 20 символов включает шест. 0D, то действительная длина вводимого текста может быть только 19 символов.


uses crt;
var str: string;

function readstr(maxlen: byte): string;
type
param =
record
MAXLEN,
ACTLEN: byte;
NAMEFLD: array[0 .. 254] of char;
end;

var p: param;
begin
P.MAXLEN := MAXLEN+1;
asm
PUSH DS
MOV AX, ss
MOV DS, AX
MOV AH, 0AH { Запрос функции ввода }
LEA DX, p { Загрузить адреса списка параметров }
INT 21H { Вызвать DOS }
POP DS
end;
READSTR:=P.NAMEFLD;
READSTR[0]:=CHAR(P.ACTLEN);
end;

begin
clrscr;
str:='12';
str:=readstr(5);
clrscr;
write(str);
end.
BlackShadow
Я бы посоветовал
Procedure Input(Var s:String);Assembler;
Var
 Buf:Array[Byte] Of Char;
Asm
 PUSH DS
 PUSH SS
 POP  DS
 LEA  DX,[Buf]
 MOV  SI,DX
 MOV  [BYTE PTR DS:SI],$FF
 MOV  AH,0Ah
 INT  21h
 LES  DI,[s]
 INC  SI
 MOV  CL,[SI]
 INC  CL
 XOR  CH,CH
 REP  MOVSB
 POP  DS
End;
BlackShadow
Текстовый ввод/вывод


Всем прекрасно известны такие незаменимые вещи как Write/WriteLn и Read/ReadLn. Очень удобные хотя бы потому, что умеют работать с неограниченным количеством параметров, а также умеющие вводить/выводить не только строки, но и числа, включая дробные. Довольно удобно, что ни говори. Но вот есть в них недостаток - ну не умеют они красным по белому. А вот захотелось мне флаг Зимбабве в текстовом режиме изобразить. Что делать? Многие, конечно, сразу вспомнят про CRT. А я напомню, что не люблю его... И про RunTime Error 200 тоже напомню... Ну, это не важно. Суть в том, что я собираюсь показать некоторые функции BIOS'а для работы с экраном в текстовом режиме и показать как их подстроить под Pascal так, чтобы не расстаться с Write'ом, забить на CRT, и получить нормальный текстовый ввод/вывод.

Все функции для работы с экраном BIOS предоставляет через INT 10h. Рассмотрим часть функций, которые подходят под тему. Остальные расскажу в другой раз. Итак:
  • Функция 00h - установить видеорежим. Этой функцией можно воспользоваться и для установки графических режимов, но об этом не сейчас. А вот текстовые устанавливаются так: в AL заносится код режима (0 - 40*25 ч/б, 1 - 40*25 цв, 2 - 80*20 ч/б, 3 - 80*25 цв, 7 - 80*25 монохромный), и вызывается INT 10h. По естественным соображениям будем пользоваться 3-им режимом. Вот пример его установки:
    Procedure SetMode; Assembler;
    Asm
     MOV AX,$0003 {00 - номер функции, 03 - режим}
     INT 10h
    End;

    Стоит отметить, что переименовав SetMode в ClrScr мы получим именно ClrScr. Отличие только в том, что фон будет именно чёрным, а не каким-либо ещё.
  • Функция 01h - изменение формы курсора. Ведь он может быть не только скромной мерцающей чёрточкой... Его ведь можно вообще убрать smile.gif Параметры тут таковы: CH - начальная строчка, CL - конечная (0..1F). Таким образом записав в CH 20h мы можем спрятать курсор. Подытожим:

    Procedure CursorHide; Assembler;
    Asm
     MOV AH, 1
     MOV CH, 20h
     INT 10h
    End;

    Procedure CursorShow; Assembler;
    Asm
     MOV CX, $0607
     MOV AL, 1
     INT 10h
    End;

  • Функция 02h - перемещение курсора. Вот оно как GotoXY написать можно... Разница только в том, что в CRT позиция левого верхнего угла - (1,1), а тут - (0,0). Но это ещё не всё. Теперь надо оторваться и рассказать про
    Видеостраницы
    Да, даже в текстовом режиме есть такое понятие! Видеостраница - отрезок памяти, который отображается на экране. По умолчанию работа ведётся со страницей №0 и средства Паскаля не дают этого изменить, но вот BIOS позволяет работать и с другими страницами. В режимах 0 и 1 (см функцию 0) страниц 8, 2 и 3 - 4 страницы, а в режиме №7 - хрен его знает, не пользовался, в теории 4. Страницы располагаются в памяти последовательно начиная с адреса B8000h (B0000h для монохромного режима) и занимают по Width*Height*2 байта. Это связано с тем, что на каждый символ в памяти отводится по 2 байта: 1 хранит код символа, а второй его атрибуты (его цвет, цвет фона, флаг мерцания). Так вот, можно писать на разные страницы разную инфу, а затем просто переключать активную страницу.

    Вернёмся к позиции курсора. Параметры таковы: BH - видеостраница, DL - x, DH - y. Например:
    Procedure GotoXY(x, y: Byte); Assembler;
    Asm
     MOV AH, 2
     XOR BH, BH
     MOV DL, [x]
     MOV DH, [y]
     INT 10h
    End;

  • Функция 03h - получить инфу о курсоре. Входной параметр только 1 - номер страницы, с которой надо считать позицию курсора, а вот выдаёт она в CH, CL - размер, а в DH, DL - позицию.
  • Функция 05h - вот именно она и устанавливает активную видеостраницу. При инициализации режима (см. ф-цию 0) активируется страница №0, а тут можно выбрать и другую какую... Номер активируемой страницы передаётся через AL.
  • Функции 06h и 07h предназначены для прокрутки текста (вот тебе и какой-никакой, а скроллинг!). В CRT эта чудесная возможность задействована только в InsertLine и DeleteLine. А суть их в том, что они смещают несколько строк вверх (06h) или вниз (07h) в указанном "окне" и заполняют освободившееся пространство указанным атрибутом и символами пробела. Т.е. закрашивают. Параметры таковы: (CL, CH) - верхний левый угол "окна", (DL, DH) - нижний правый, AL - кол-во строк, BH - атрибут заполнения. Надо отметить, что эти функции применимы только к текущей видеостранице. Примеры:
    Procedure InsertLine(n: Byte); Assembler;
    Asm
     XOR CL, CL
     MOV CH, [n]
     MOV DX, $184F {(79,24)}
     MOV AX, $0701 {Мотать вниз на одну строку}
     MOV BH, 07h {Обычный цвет - серый по чёрному}
     INT 10h
    End;

    Procedure DeleteLine; Assembler;
    Asm
     XOR CL, CL
     MOV CH, [n]
     MOV DX, $184F {(79,24)}
     MOV AX, $0601 {Мотать вверх на одну строку}
     MOV BH, 07h {Обычный цвет - серый по чёрному}
     INT 10h
    End;

  • Функция 08h даёт возможность, которая в CRT вообще никак не предусмотрена. Эта функция возвращает символ и его атрибут в точке, в которой находится курсор на странице BH. Символ она возвращает в AL, а атрибут в AH.
  • Функция 09h начинает открывать возможность вывода. Она выводит символ AL на страницу BH в текущей позиции курсора используя атрибут BL. Всё это повторяется CX раз. Т. е. можно вывести не "й", а "ййййййййййййй" за один приём.
  • Функция 0Ah отличается от предыдущей только тем, что она не меняет атрибут символа (регистр BL игнорируется). Таким образом, если перед курсором была одна красная буковка, затем одна синяя, а потом три белых, то вывод "ййй" напомнит российский флаг smile.gif
  • Функция 0Fh возвращает информацию о текущем режиме. А точнее: в AL - №режима (см функцию 00h), в AH - кол-во строк, в BH - активная видеостраница.
  • Функции 10h-12h работают с палитрой и шрифтами (вот вам Font8x8), но подробного описания я, к сожалению, привести не могу, т.к. не помню.
  • Функция 13h реализует самый удобный способ вывода информации. Вывод происходит на странице BH, начиная с позиции (DL, DH), выводится CX символов. А вот дальше уже интересно. AL указывает способ вывода на экран:
    0 - вывести CX символов строки ES:BP используя атрибут BL и не шевелить курсор
    1 - сделать всё то же самое, но переместить курсор к концу строки
    2 - расценивает данные в ES:BP не как строку, а как набор записей типа символ/атрибут. Т. е. в буфере должно быть CX*2 байт данных.
    3 - аналогично 2, но перемещает курсор.
    Естественно, функции 2 и 3 игнорируют значение BL.
Ну вот, в общем, и всё, что я хотел рассказать про INT 10h.

Теперь расскажу как это всё дело всунуть в Write.
В модуле DOS описана структура TextRec, экземпляром которой и являются стандартные Input и Output (если кто не знает, то Input и OutPut - это 2 текстовых файла, которые описаны в модуле System и открываются в начале исполнения программы. Функция Write записывает всё в OutPut, если не указать какой другой файл, а Read, соответственно, читает из Input). Этот тип хранит 4 важных для нас поля:
  • OpenFunc - адрес функции типа Function(Var r:TextRec):Integer;FAR. Эта функция вызывается при открытии файла и, в случае успеха, должна вернуть 0.
  • InOutFunc - адрес функции чтения/записи (Function(Var r:TextRec):Integer;Far). Эта функция должна произвести ввод/вывод и вернуть 0 в случае успеха.
  • FlushFunc - адрес функции такого же как и выше типа, которая должна освободить буфер ввода/вывода указанного файла.
  • CloseFunc - функция по типу как и предыдущие, которая закрывает файл.
Итого, как нам сделать красивый вывод?
Берём файл OutPut, настраиваем на свои функции и полетели. А функции должны быть такими: OepnFunc и CloseFunc - пустышки, которые просто возвращают 0. Например:

Function Empty(Var r: TextRec): Integer; Far; Assembler;
Asm
 XOR AX, AX
End;

А адреса InOutFunc и FlushFunc можно настроить на вывод, т. к. они всё равно занимаются одним и тем же. Образно функцию вывода можно описать так:
Function DoOutPut(Var f: TextRec): Integer; Far;
Var i:Integer;
Begin
 For i:=0 To f.BufPos-1 Do
   DoWriteChar(f.BufPtr^[i])
 f.BufPos:=0;
 DoOutPut:=0
End;

Вроде как и не сложно... Вопрос только в реализации DoWriteChar. Тут я пока оставлю полную свободу ваше фантазии, а свою реализацию покажу как-нибудь потом.

Возникает логичный вопрос: тема называется ввод/вывод, так где же ввод?
А вот с вводом дела обстоят крайне туго. BIOS не предоставляет никаких возможностей для ввода. Так что для написания красивого ввода приходится разрабатывать порой довольно хитрые функции и подставлять их в Input. Иногда бывает проще заполнить ту область, где будет осуществляться ввод подходящим атрибутом и воспользоваться стандартными средствами, но только НЕ ПОДКЛЮЧАЯ Crt, т. к. он забьёт на установленные там атрибуты и намалюет по-своему.

Ну вот и всё на этот раз. В следующий раз, я думаю, что напишу как заменить всё оставшееся, от Crt smile.gif
Dark
Продолжим

Секция функций для работы с файлами и дисками.

Работа с файлами.
  1. Создание
  2. Закрытие
  3. Чтение
  4. Запись
  5. Создание
  6. Удаление
  7. Сдвиг указателя
  8. Получение/изменение атрибутов
  9. Переименовать файл
  10. Получить/установить дату и время последней модификации файла

Для работы с файлами используются функции прерывания 21h. Все операции связаны с так называемым файловым хендлом (дескриптором) (file handle), 16 битным числом, при помощи которого DOS идентифицирует файл.

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

Открытие файла: 3Dh

IN:
ah = 3Dh
al = режим открытия:
биты 2-0:
000 (только чтение), 001 (только запись) или 010 (чтение и запись)
DS:DX = указатель на имя файла (указанное в ASCIIZ формате - на конце #0)

OUT:
CF = 1: Ошибка
AX = код ошибки

CF = 0: Все ОК
AX = дескриптор файла, его надо сохранить, иначе дальнейшая работа с файлом невозможна.

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

Закрытие файла: 3Eh

IN:
ah = 3Eh
bx = дескриптор файла

OUT:
CF = 1: Ошибка
AX = код ошибки

CF = 0: Все ОК

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

Чтение файла: 3Fh

IN:
ah = 3Fh
bx = дескриптор файла
cx = количество байт для чтения
DS:DX = куда поместить прочитанные данные

OUT:
CF = 1: Ошибка
AX = код ошибки

CF = 0: Все ОК
AX = количество прочитанных байт, если 0 - пытаемся читать из конца файла.

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

Запись в файл: 40h

IN:
ah = 40h
bx = дескриптор файла
cx = количество записываемых байт
DS:DX = откуда брать данные

RETURN:
CF = 1: Ошибка
AX = код ошибки

CF = 0: Все ОК
AX = количество записанных байт, если не равняется тому, что было в CX - ошибка

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

Создание файла, с усечением существующего до 0 размера: 3Ch

IN:
ah = 3Ch
cl = атрибуты файла
bit 0: только чтение
bit 1: скрытый
bit 2: системный
bit 3: метка тома
bit 4: директория
bit 5: архивный
bit 6&7: резервные биты

DS:DX = указатель на ASCIIZ имя файла

OUT:
CF = 1: Ошибка
AX = код ошибки

CF = 0: Все ОК
AX = дескриптор файла

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

Удаление файла: 41h

IN:
ah = 41h
DS:DX = указатель на ACIIZ имя

OUT:
CF = 1: Ошибка
AX = код ошибки

CF = 0: Все ОК

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

Сдвиг указателя: 42h

IN:
ah = 42h
bx = дескриптор файла
CX:DX = 32 битный указатель на место сдвига
AL: =0 (отсчет от начала файла), =1 (отсчет от текущей позиции), или =2 (отсчет от конца файла)

OUT:
CF = 1: Ошибка
AX = код ошибки

CF = 0: Все ОК
DX:AX = 32 битный указатель на то, где мы сейчас.

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

Получить/Сменить атрибуты файла: 43h

IN:
ah = 43h
DS:DX = указатель на имя файла в ASCIIZ формате
al: =0 (вернуть атрибуты в CX) или l=1 (установить атрибуты из CX)

OUT:
CF = 1: Ошибка
AX = код ошибки

CF = 0: Все идет по плану =)

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

Переименовать файл: 56h

IN:
ah = 42h
DS:DX = указатель на существующее имя
ES:DI = указатель на новое имя файла в ACIIZ
CL = маска атрибутов

OUT:
CF = 1: Ошибка
AX = код ошибки

CF = 0: Все ОК
При помощи этой функции можно переносить файл в другие каталоги.

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

Получить/установить дату и время последней модификации файла: 57h

IN:
ah = 57h
al: =00 (вернуть время в CX дату в DX) или =01 (установить новое время из CX новую дату из DX)
bx = дескриптор файла

OUT:
CF = 1: Ошибка
AX = код ошибки

CF = 0: Все идет по плану =)


Время Дата
биты описание биты описание
15-11 Часы(0-23) 15-9 Год
10-5 Минуты 8-5 Месяц
4-0 Секунды 5-0 День


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


Простейший пример - программка считывает файл, имя которого вводится как параметр программы и выводит его на экран.


jc @OpenError
mov bx, ax ; сохраняю файловый дескриптор

push ds
lds dx, buffer ; загружаю в ds:dx адрес буфера - для считывания файла
mov ah, 3fh
mov cx, 0FFFFh
int 21h ; пытаюсь прочесть 65535 байт в буфер
pop ds
mov FileSize, ax ; сколько мы прочли?
mov ah, 3eh
int 21h ; закрываю файл

cld
push ds
mov cx, FileSize
lds si, buffer

@PrintLoop:
mov ah, 2
lodsb ; al <- [ds:si]
mov dl, al
int 21h ; DOS функция 2h - печатает символ, лежащий в dl
dec cx
jne @PrintLoop
pop ds

jmp @exit
@OpenError:
mov ah, 9
mov dx, offset ErrMsg1+1
int 21h
@exit:
Dark
Оки, тады продолжаем

Работа с диском (DOS функции)
  1. Получить номер текущего диска
  2. Изменить номер текущего диска
  3. Свободное место
  4. Создать каталог
  5. Удалить каталог
  6. Изменить текущий
  7. Получить текущий каталог
  8. Найти первый файл по шаблону
  9. Найти следующий файл по шаблону

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

Получить номер текущего диска: 19h

IN:
ah = 19h

OUT:
al = номер диска (00h -> A; 01h -> B; 00h -> C и т.д.)

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

Изменить номер текущего диска: 0Eh

IN:
ah = 0Eh
dl = новый диск (00h -> A; 01h -> B; 00h -> C и т.д.)

OUT:
al = номер диска, последнего в системе (00h -> A; 01h -> B; 00h -> C и т.д.)

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

Свободное место на диске: 36h

IN:
ah = 36h
dl = номер диска, (00h - текущий, 01h - A 02h - B и т.д.)

OUT:
ax = FFFF - неправильно задано устройство в dl
или
ax = число секторов в кластере
bx = свободные кластеры
cx = размер сектора в байтах
dx = общее число кластеров на диске

Таким образом, свободное пространство на диске - ax*bx*cx, полный объем - ax*cx*dx

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

Создать каталог: 39h

IN:
ah = 39h
ds:dx = указатель на имя создаваемого каталога в ASCIIZ формате (64 символа)

OUT:
CF = 0: Все ОК
AX = не определен

CF = 1: ошибка
AX = код ошибки

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

Удалить каталог: 3Ah

IN:
ah = 3Ah
ds:dx = указатель на путь к удаляемому каталогу в ASCIIZ формате (64 символа)

OUT:
CF = 0: Все ОК
AX = не определен

CF = 1: ошибка
AX = код ошибки

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

Изменить текущий каталог: 3Bh

IN:
ah = 3Bh
ds:dx = указатель на путь к новому каталогу в ASCIIZ формате (64 символа)

OUT:
CF = 0: Все ОК
AX = не определен

CF = 1: ошибка
AX = код ошибки

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

Получить текущий каталог: 47h

IN:
ah = 47h
dl = номер диска
ds:si = указатель на путь к новому каталогу в ASCIIZ формате (64 символа)

OUT:
CF = 0: Все ОК
AX = неопределен (или 100h)

CF = 1: ошибка
AX = код ошибки

Поиск файлов


Поиск происходит следующим образом:
  1. Вызывается функция 4eh
    - Если файл найден, то имя файла и расширение помещается в область DTA (см. ниже) со смещением 1Eh от начала
    - Если файл не найден то устанавливается CF
  2. Если нужно продолжить поиск, то вызывается функция 4fh, которая тоже заполняет DTA область.

Область DTA (Data Transfer Area) располагается в PSP (префикс программного сегмента) со смещением 80h от его начала и занимает 128 байт.

DTA может хранить две структуры:
  • командную строку
  • блок описания найденного файла
Поэтому, если вам необходимо будет работать с командной строкой, то стоит сохранить адрес DTA, создать новую область DTA и в нужный момент восстановить адрес DTA.

Структура DTA:

Смещение Размер Описание
00h 1 Имя диска (Если 7 бит=0, то диск удаленный)
01h 11 Шаблон поиска
0Ch 1 Атрибуты поиска
0Dh 2 Порядковый номер в каталоге
0Fh 6 Резерв
15h 1 Атрибуты найденного файла
16h 2 Время создания файла
18h 2 Дата создания файла
1Ah 4 Размер файла
1Eh 13 ASCIIZ имя файла с расширением


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

Получить адрес DTA: 2Fh

IN:
ah = 2Fh

OUT:
ES:BX = адрес DTA

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

Установить адрес DTA: 1Ah

IN:
ah = 1Ah
ds:dx = новая область DTA

OUT:

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

Найти первый файл по шаблону: 4Eh

IN:
ah = 4Eh
cx = Атрибуты файла
ds:dx = указатель на имя файла в ASCIIZ (можно применять символы ? и *)

OUT:
CF = 0: файл найден, информация в DTA

CF = 1: Ошибка
AX = 2 (файл не найден), 3 (несуществующий путь) или 12h (больше нет файлов)

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

Найти следующий файл по шаблону: 4Fh

IN:
ah = 4Fh
DTA заполнена предыдущим вызовом функции 4Eh

OUT:
CF = 0: файл найден, информация в DTA

CF = 1: Ошибка
AX = 12h - больше нет файлов

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


Пример поиска файла


var
a: string; {шаблон поиска}
 a1: array[0..42] of byte; {инфа} {кстати, я мог бы DTA определить сюда}
 a2: array[0..42] of char; {инфа} {или сюда}
 dta: pointer; {Указатель на DTA}

begin
a:='files.txt'#0;
asm
push ds

mov ah, 2fh
int 21h
mov word ptr dta+2, es
mov word ptr dta, bx {определяем DTA}

mov ax, seg a
mov ds, ax
mov ah, 4Eh {Ищем файл}
mov cx, 7
lea dx, a+1
int 21h
jc @exit {файл не найден}

lds si, dta
mov ax, seg a1
mov es, ax {просмотр DTA}
lea di, a1
mov cx, 128
rep movsb

mov ax, seg a1
mov ds, ax
lea si, a1
mov ax, seg a2 {просмотр DTA}
mov es, ax
lea di, a2
mov cx, 128
rep movsb

@exit:
pop ds
end;
end.


Таким образом несложно написать редактор наподобии EDIT от MICROSOFT =)))
А также, применять для разработки своих вирусов...
Это текстовая версия — только основной контент. Для просмотра полной версии этой страницы, пожалуйста, нажмите сюда.