Содержание

Продавай мобильный контент со своего сайта

Учебник по языку Ассемблер в задачах и примерах

Содержание

  • Введение
  • Программирование целочисленных арифметических операций
  • Сложные структуры данных (Часть1)
  • Сложные структуры данных (Часть 2)
  • Процедуры в программах ассемблера
  • Обработка цепочек элементов
  • Работа с консолью в программах на ассемблере
  • Преобразование чисел
  • Работа с файлами в программах на ассемблере (Часть1)
  • Работа с файлами в программах на ассемблере (Часть2)
  • Профайлер
  • Вычисление CRC
  • Программирование ХММ-расширения


Глава 1. Программирование целочисленных арифметических операций

Продавай мобильный контент со своего сайта

Глава 1. Программирование целочисленных арифметических операций

  • Двоичные числа
    • Сложение двоичных чисел
    • Вычитание двоичных чисел
    • Умножение двоичных чисел
    • Деление двоичных чисел
  • Двоично-десятичные числа (BCD-числа)
    • Неупакованные BCD-числа
    • Упакованные BCD-числа
  • Генерация последовательности случайных чисел
    • Конгруэнтный метод генерации последовательности случайных чисел
    • Аддитивный генератор случайных чисел
    • Программа генерации высокослучайных двоичных наборов

 Всякое математическое доказательство, за которым мы можем следить, выразимо конечным числом символов. Эти символы, правда, могут быть связаны с понятием бесконечности, но связь эта такова, что ее можно установить за конечное число шагов. Так, когда в случае математической индукции мы доказываем теорему, зависящую от параметра n, мы доказываем ее сначала для n=0 и затем устанавливаем, что случай, когда параметр имеет значение n+1, вытекает из случая, когда параметр имеет значение n. Тем самым мы убеждаемся в правильности теоремы для всех положительных значений параметра n. Более того, число правил действия в нашем дедуктивном механизме должно быть конечным, даже если оно кажется неограниченным из-за ссылки на понятие бесконечности. Ведь и само понятие бесконечности выразимо в конечных терминах.
Н. Випер, «Кибернетика, или управление и связь в животном и машине»

Микропроцессор Intel имеет средства для обработки целочисленных арифметических данных двух форматов: двоичного и двоично-десятичного (BCD). Данные в двоичном формате рассматриваются как числа со знаком и без знака. При этом необходимо заметить, что не существует отдельного формата для чисел со знаком и без знака. Один и тот же двоичный код может рассматриваться как значение со знаком и без знака. Все зависит от того, как трактуется старший бит операнда. Для чисел без знака он является старшим значащим битом операнда. Для чисел со знаком смысл старшего значащего бита операнда меняется: его нулевое значение соответствует положительному числу, единичное — отрицательному. Остальные разряды операнда — значащие. Но здесь есть один нюанс, смысл которого в том, что остальные разряды не являются модулем числа. Для положительного числа они действительно являются абсолютной величиной числа,
а вот для отрицательного числа они представляют так называемый дополнительный код числа. Идея дополнительного кода состоит в том, что микропроцессору совсем необязательно иметь отдельные устройства для сложения и вычитания. Достаточно только одного — сумматора.
Есть всего лишь две команды в системе команд микропроцессора, которые воспринимают старший бит операнда как знаковый, — это команды IMUL и IDIV. В остальных случаях забота о правильной трактовке старшего бита ложится на программное обеспечение. Программирующий на ассемблере должен сам предусматривать особенности обработки знаковых битов.
Другая характерная ситуация при выполнении арифметических действий — переполнение и антипереполнение. Их причина — ограниченность разрядной сетки операнда. При выполнении операции сложения или умножения возможен выход результата за пределы разрядной сетки. Если результат больше максимально представимого значения для операнда данной размерности, то говорят о ситуации переполнения. Иначе, если результат меньше минимально представимого числа, то говорят о ситуации антипереполнения. При этом результат также верен, но при его соответствующей трактовке. Все эти рассуждения приведены в уроке 8 «Арифметические команды» учебника, и повторять мы их не будем. Сосредоточимся на практическом аспекте этого вопроса. Ситуация переполнения может иметь место при вычислениях, в которых заранее не известен размер операндов.

Двоичные числа

Прежде чем программировать,
запишите программу в псевдокодах.
Д. Ван Тассел

Сложение двоичных чисел

Сложение чисел размером 1 байт без учета знака

---------------------------------------------------------------------
:add_unsign - процедура сложения чисел размером 1 байт без учета_1 знака
;Вход: sumnand_1и summand_2 - слагаемые.
:Выход: sum_b или sum_w - значение суммы с учетом переполнения.
---------------------------------------------------------------------

.data
summand_1db ? значения в summand_1и summand_2
summandj? db ? :нужно внести
sum_w label word
sum_b db 0
carry db 0
.code
add_unsign proc
mov al ,summand_2
add al ,summand_1mov sumji.al
jnc end_p :проверка на переполнение
adc carry,0
end_p: ret
add_unsign endp

Программа учитывает возможное переполнение результата. Сложение двоичных чисел большей размерности (2/4 байта) выполняется аналогично. Для этого необходимо заменить директивы DB на DW/DD и регистр AL на АХ/ЕАХ.

Сложение чисел размером N байт без учета знака

:add_unsign_N - процедура сложения чисел размером N байт без учета знака
:Вход: summand_1 и summand_2 - слагаемые. N - длина в байтах.
:Выход: summand_1или carry+summandj. - значение суммы с учетом переполнения.

.data
summand_1db ? ;первое слагаемое
N=$-surranand_1;длина в байтах значений summand_1и summand_2
carry db 0 :перенос сложения последних байтов
summand_2 db ? :второе слагаемое
.code
add_unsign_N proc
mov cl. N
хог si.si cycl: mov al ,summand_2[si]
adc summand_l[si].al
inc si
loop cycl
jnc end_p ;проверка на переполнение
adc carry. 0
end_p: ret
add_unsign_N endp

Программа учитывает возможное переполнение результата. Сегмент данных может быть задан, например, так:

.data
summand_1db 0.34.56.78.250 ; первое слагаемое
N=$-summand_1:длина в байтах значений summand_1и summand_2
carry db 0 ;перенос сложения последних байт
summand_2 db 0.43.65.230.250 : второе слагаемое

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

Сложение без учета знака чисел размером N байт (макрокоманда)

.data
:summand_ldb ? :первое слагаемое
;N=$-summand_1,:длина в байтах значений summand_1и summand_2
;carry db 0 :перенос сложения последних байтов
;summand_2db ? ; второе слагаемое
.code
.старший байт по младшему адресу
add_unsign_N macro carry.summand_l.summand_2.N
local cycl.end_p
:add_unsign_N carry,summand_1,sunmand_2.N - макрокоманда сложения без учета знака чисел :размером N байт
:Вход: summand_1l и summanct_2 - слагаемые. N - длина в байтах.
;Порядок следования байтов - старший байт по младшему адресу (не Intel).
;Выход: summand_1или carry+summand_1- значение суммы с учетом переполнения.
mov cl.N
mov si.N-1 cycl: moval,summand_2[si]
adc summand_l[si].al
dec si
loop cycl
jnc end_p
adc carry.0
end_p: пор
endm

Сложение чисел размером 1 байт с учетом знака

---------------------------------------------------------------------
;add_sign - процедура сложения чисел размером 1 байт с учетом знака
:Вход: summand_1и summandj? - слагаемые.
:Выход: sum_b или sum_w - значение суммы в зависимости от наличия расширения знака.
---------------------------------------------------------------------

.data
sum_w label word
summandl db ? :значения в summand_1и summand_2 нужно внести
carry db 0 ; расширение знака
summand_2 db ?
.code
add_sign proc
mov al ,summand_2
add summand_l.a1
jc @@cfl_ofl
jo @<acfO_ofl
:cf=0 of=0 -> результат верный :cf"l of=0 -> результат верный r_true: jmp end__p результат -> summand_1@icfl_ofl: jno
@@cfl_of0 :cf=1 of=1 -> результат неверный
mov carry.0ffh расширение знака д.б. -1, результат ->sum_w
jmp end_p
:cf=1 of=0 -> результат верный
@@cfl_of0: jmp r_true результат -> summand_1:cf=0 of=1 -> результат неверный
@@cf0_ofl: mov carry.0 .-расширение знака д.б. =0. результат ->sum_w
jmp end_p end_p: ret add_sign endp

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

  • CF=0F=0 — результат правильный и является положительным числом;
  • CF=1 0F=0 — результат правильный и является отрицательным числом;
  • CF=0F=1 — результат неправильный и является положительным числом, хотя правильный результат должен быть отрицательным (для корректировки необходимо увеличить размер результата в два раза и заполнить это расширение нулевым значением);
  • CF=0 0F=1 — результат неправильный и является отрицательным числом, хотя правильный результат должен быть положительным (для корректировки необходимо увеличить размер результата в два раза и произвести расширение знака).

Сложение чисел со знаком большей размерности (2/4 байта) выполняется аналогично, для этого необходимо внести изменения в соответствующие фрагменты программы. В частности, необходимо заменить директивы DB на DW/DD и регистр AL на АХ/ЕАХ.

Сложение с учетом знака чисел размером N байт

---------------------------------------------------------------------
:add_sign_N - процедура сложения с учетом знака чисел размером N байт
-.Вход: summand_1и summand_2 - слагаемые. N - длина в байтах.
:Выход: summand_1или carry+summand_1- значение суммы с учетом переполнения.
---------------------------------------------------------------------

.data
summand_1db ? :первое слагаемое
N=$-summand_1:длина в байтах значений summand_1и summand_2
carry db 0 расширение знака
summand_2 db ? :второе слагаемое
.code
add_sign_N proc
mov cx.N
mov si .0-1 cycl: incsi
mov al ,summand_2[si]
adc summand_l[si].al
loop cycl
jc @@cfl_ofl
jo @(acfO_ofl
:cf=0 of=0 -> результат верный :cf=1 of=0 -> результат верный r_true:jmp end_p результат -> summand_1@@cfl_ofl:
jno?@cfl_of0 :cf=1 of-1 -> результат неверный
mov carry.0ffh расширение знака д.б. =1. результат ->sum_w
jmp end_p @@cfl_of0: :Cf»l of=0 -> результат верный
jmp r_true результат -> summand_1@0cf0_ofl: :cf=0 of=1 -> результат неверный
mov carry.0расширение знака д.б. =0. результат ->sum_w
jmp end_p end_p: ret add_sign_N endp

Сегмент данных может быть задан, например, так:

.data
summand_1db 32,126,-120 ;первое слагаемое
N=$-summand_1;длина в байтах значений summand_1и sumniand_2
carry db 0 расширение знака
summand_2 db 126,125,-120 ;второе слагаемое

Программа учитывает возможное переполнение результата. Обратите внимание на порядок задания значений слагаемого. Если слагаемое положительное, то проблем нет. Отрицательное слагаемое размером N байт для своего задания требует некоторых допущений. Старший байт отрицательного слагаемого задается со знаком и в процессе трансляции будет преобразован в значение, являющееся двоичным дополнением исходного значения. Остальные байты в своем исходном виде должны быть частью доbxя. Поэтому для работы с числами со знаком удобно иметь программу, которая бы выполняла вычисление значения модуля отрицательного числа и, наоборот, по значению модуля вычисляла его дополнение.

Вычисление дополнения числа размером N байт

---------------------------------------------------------------------
: calc_complement - процедура вычисления дополнения числа размером N байт
;Вход: bx - адрес операнда в памяти; сх - длина операнда.
;Порядок следования байт - младший байт по младшему адресу. ;
Выход: bx - адрес результата в памяти
---------------------------------------------------------------------

.code calc_complement proc
хог si,si
neg byte ptr [bx] дополнение первого байта
cmp byte ptr [bx],0 ;нулевой операнд - особый случай
jne short $+3
stc установить cf, так как есть перенос
dec ex
jcxz @@ml ;для однобайтного операнда
@@cycl: iпс si
not byte ptr [bx][si]
adc byte ptr [bx][si],0
loop @@cycl
@@ml: ret calc_complement endp

Для значений размерностью 1/2/4 байта дополнение можно получать с помощью одной команды NEG:

neg operand

Для значений N байт необходимо реализовывать алгоритм. Дополнение первого байта необходимо вычислять с учетом того, что он может быть нулевым. Попытка получить его дополнение с помощью команды NEG обречена на провал. Флаг CF в этом случае также должен устанавливаться программно. Подумайте, почему?

Вычисление модуля числа размером N байт

---------------------------------------------------------------------
:calc_abs - процедура вычисления модуля числа размером N байт
:Вход: bx - адрес операнда в памяти; сх - длина операнда.
;Порядок следования байтов - младший байт по младшему адресу.
:Выход: bx - адрес результата в памяти.
---------------------------------------------------------------------

.code
calc_abs proc определим знак операнда
mov si.cx
dec si
test byte ptr [bx][si],80h проверяем знак операнда
jz @@exit ;число положительное
call calc_complement @@exit:ret
calc_abs endp

Вычитание двоичных чисел

Вычитание чисел размером 1 байт без учета знака

---------------------------------------------------------------------
;sub_unsign - процедура вычитания чисел размером 1 байт без учета знака
;Вход: minuend и deduction - уменьшаемое и вычитаемое.
:Выход: minuend - результат вычитания
.---------------------------------------------------------------------

.data значения в minuend и deduction нужно задать
minuend db ? уменьшаемое
deduction db ? ;вычитаемое
.code
sub_unsign proc
mov al .deduction
subminuend.al :оценить результат на случай уменьшаемое < вычитаемого
jnc end_p ; нет заема обрабатываем ситуацию заема из старшего разряда - получаем модуль (если нужно)
neg minuend end_p: ret sub_unsign endp

Программа учитывает возможное соотношение: уменьшаемое<вычитаемого. Вычитание чисел большей размерности (2/4 байта) выполняется аналогично. Необходимо заменить директивы DB на DW/DD и регистр AL на АХ/ЕАХ.

Вычитание чисел размером N байт без учета знака

---------------------------------------------------------------------
:sub_unsign_N -.процедура вычитания чисел размером N байт без учета знака
:Вход: minuend и deduction - уменьшаемое и вычитаемое, N - длина в байтах.
;Выход: minuend - значение разности.
---------------------------------------------------------------------

.data значения в minuend и deduction нужно внести
minuenddb ? уменьшаемое
N=$-minuend ;длина в байтах значений minuend и deduction '.
deduction db ? :вычитаемое
.code
sub_unsign_N proc
mov cl.N
xor si,si cycl: moval ,deduction[si]
sbbminuend[si].al
jnc @@ml
negminuendtsi] @@ml: inc si
loop cycl
ret sub_uns1gn_N endp

Программа учитывает возможный заем из старших разрядов. Длина уменьшаемого должна быть не меньше длины вычитаемого, недостающие разряды вычитаемого должны быть нулевыми. В любом случае, результат — абсолютное значение.
Сегмент данных может быть задан, например, так:

.data
N equ5 ;длина в байтах значений minuend и deduction
minuenddb 30.43.65.230,250 уменьшаемое
deduction db 45.34.65.78.250 ;вычитаемое

Вычитание чисел размером 1 байт с учетом знака

---------------------------------------------------------------------
;sub_sign - процедура вычитания чисел размером 1 байт с учетом знака
;Вход: minuend и deduction - уменьшаемое и вычитаемое.
:Выход: minuend - значение разности.
---------------------------------------------------------------------

.data значения в minuend и deduction нужно внести
N equ 2 :длина в байтах результата в ситуации расширения знака для получения его модуля
minuend db ? -.уменьшаемое
carry db 0 расширение знака
deduction db ? :вычитаемое
.code
sub_sign proc
mov al .deduction
subminuend.al ;оценить результат:
jnc no_carry :нет заема обрабатываем ситуацию заема из старшего разряда - получаем модуль (если нужно)
neg minuend
jmp end_p
no_carry: jns no_sign обрабатываем ситуацию получения отрицательного результата - получаем модуль (если нужно)
neg minuend
jmp end_p
no_sign: jno no_overflow обрабатываем ситуацию переполнения - получаем модуль (если нужно).
расширить результат знаком - получаем модуль (если нужно):
mov carry.0ffh
call calc abs no_overflow:
endjr ret sub_sign endp

Программа учитывает возможный заем из старших разрядов. Вычитание чисел большей размерности (2/4 байта) выполняется аналогично. Необходимо заменить директивы DB на DW/DD и регистр AL на АХ/ЕАХ. Подробности зависимости состояния флагов от результата см. в уроке 8 «Арифметические команды» учебника.

Вычитание чисел размером N байт с учетом знака

---------------------------------------------------------------------
:sub_sign_N - процедура вычитания чисел размером Н байт с учетом знака
;Вход: minuend и deduction - уменьшаемое и вычитаемое. N - длина в байтах.
:Выход: minuend - значение разности.
---------------------------------------------------------------------

.data :значения в minuend и deduction нужно внести
minuenddb ? уменьшаемое
lenjninuend=$-minuend ;длина в байтах уменьшаемого и вычитаемого
carry db 0 расширение знака
deduction db ? ;вычитаемое
.code
sub_sign_N proc
mov cx.lenjninuend
mov si.O @@ml: mov al,deduction[si]
sbb minuend[si].al
inc si
loop @@ml оценить результат:
jnc no_carry :нет заема
обрабатываем ситуацию заема из старшего разряда - получаем модуль (если нужно) N=1en_minuend+1
mov carry.0ffh
call calc_abs
jmp end_p no_carry: jns no_sign Обрабатываем ситуацию получения отрицательного результата -
:получаем модуль (если нужно) N=1en_minuend
call calc_abs
jmp end_p
no_sign: jno no_overflow
обрабатываем ситуацию переполнения - получаем модуль (если нужно) расширить результат знаком - получаем модуль (если нужно): N=1en_minuend+1
mov carry,0ffh
call catc_abs no_overflow: end_p: ret sub_sign_N endp

Описанная процедура вычисляет модуль разности и учитывает возможный заем из старших разрядов. Если вычисления модуля разности не требуется, то закомментируйте строки, содержащие команду CALL calc_abs. Подробности зависимости состояния флагов от результата см. в уроке 8 «Арифметические команды» учебника.

Сегмент данных может быть задан, например, так:

.data :значения в minuend и deduction нужно внести
minuend db 25h,0f4h,0eh уменьшаемое
len_minuend=$-minuend ;длина в сайтах уменьшаемого и вычитаемого
carry db 0 ;расширение знака
deduction db 5h,0f4h,0fh :вычитаемое

Далее при рассмотрении программы деления многобайтных двоичных чисел нам понадобится макрокоманда вычитания с учетом знака чисел размером N байт (порядок следования байт не соответствует порядку следования байтов на процессорах Intel, то есть старший байт находится по младшему адресу). Приведем ее.

Вычитание с учетом знака чисел размером N байт (макрокоманда)

sub_sign_N macro minuend.deduction.N
local cycl.ml
---------------------------------------------------------------------
;sub_sign_N minuend.deduction.N - макрокоманда вычитания
;c учетом знака чисел размером N байт
:Вход: minuend и deduction - уменьшаемое и вычитаемое. N - длина в байтах.
:Порядок следования байт - старший байт по младшему адресу (не Intel).
:Выход: minuend - значение разности.
---------------------------------------------------------------------

push si
mov cl,N
mov si.N-1 cycl: moval .deduction[si]
sbbminuend[si],al : jnc ml
: neg minuend[si] ml: dec si
loop cycl
pop si
endm

Умножение двоичных чисел

В отличие от сложения и вычитания операция умножения реализуется двумя типами команд — учитывающими и не учитывающими знаки операндов.

Умножение чисел размером 1 байт без учета знака

---------------------------------------------------------------------
:mul_unsign.asm - программа умножения чисел размером 1 байт без учета знака. ;Вход: multiplier], и multiplied - множители размером 1 байт. ;Выход: product - значение произведения.
---------------------------------------------------------------------
.data
:значения в multiplier], и multiplied нужно внести
product label word
productj label byte
multiplier! db ? :множитель 1 (младшая часть произведения)
product_h db 0 ;старшая часть произведения
multiplied db ? ;множитель 2
.code
mul_unsign proc
mov al .multiplierl
mul multiplier2 :оценить результат:
jnc по_саггу ;нет переполнения - на no_carry обрабатываем ситуацию переполнения
mov product_h.ah :старшая часть результата no_carry: mov product_l.al ;младшая часть результата
ret
mul_unsign endp main:
call mul_unsign
end main

Здесь все достаточно просто и реализуется средствами самого процессора. Проблема состоит лишь в правильном определении размера результата. Произведение чисел большей размерности (2/4 байта) выполняется аналогично. Необходимо заменить директивы DB на DW/DD, регистр AL на АХ/ЕАХ, регистр АН на DX/EDX.

Умножение чисел размером N и М байт без учета знака

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

Умножение N-байтного числа на число размером М байт

ПРОГРАММА mul_unsign_NM
---------------------------------------------------------------------
//mul_unsign_NM - программа на псевдоязыке умножения N-байтного числа
//на число размером М байт
//(порядок - старший байт по младшему адресу (не Intel))
//Вход: U и V - множители размерностью N и М байт соответственно
: Ь=256 - размерность машинного слова.
//Выход: W - произведение размерностью N+M байт.
---------------------------------------------------------------------
ПЕРЕМЕННЫЕ
INT_BYTE u[n]; v[n]; w[n+m]: k=0: INT_WORD b=256: temp_word НАЧ_ПРОГ
ДЛЯ j:=M-l ДО 0 //J изменяется в диапазоне М-1..0
НАЧ_БЛОК_1
//проверка на равенство нулю очередного элемента множителя (не обязательно) ЕСЛИ v[j]==0 TO ПЕРЕЙТИ_НА тб
k:=0: i:=n-l ll\ изменяется в диапазоне N-1..0
ДЛЯ 1:=N-1 ДО О НАЧ_БЛ0К_2
//перемножаем очередные элементы множителей temp_word:=u[i]*v[j]+w[i+j+l]+k
w[i+j+l]:=temp_word MOD b //остаток от деления temp_word\b -> w[i+j+l] k:=temp_word\b //целая часть частного temp_word\b -> k
К0Н_БЛ0К_2 w[j]:=k шб:
КОН БЛОК_1 КОН_ПРОГ
:inul_unsign_NM.asm - программа на ассемблере умножения N-байтного числа на число :размером М байт (порядок - старший байт по младшему адресу (не Intel)).
.data :значения в U и V нужно внести
U db ? ;U-un.i«.UiU() - множитель_1 размерностью N байт
1-S-U :i=N
V db ? ; V"Vm.i_ViV(| - множитель_2 размерностью М байт
j=$-V :j=M
len_product=$-U
;w - результат умножения, длина N+M
W db len_product dup (0) ;1en_product=N+M
k db 0 :перенос 0 < k < 255
b dw lOOh : размер машинного слова
.code
mul_unsign_NM proc
mov bx.j-1 :ml
mov ex, j ;ДЛЯ j:=M-l ДО 0 //J изменяется в диапазоне М-1..0
m2: :НАЧ_БЛОК_1
push ex сложенные циклы
emp v[bx],0 :ЕСЛИ v[j]—0 TO ПЕРЕЙТИ_НА m6
je m6 ;m3
movsi.i-1 :i-0..n-l ;k:=0; 1:41-1 //i изменяется в диапазоне N-1.,0
mov cx.i
movk.O :ДЛЯ i:-N-l ДО О НАЧ_БЛ0К_2 m4: ://перемножаем очередные элементы множителей
mov al,u[s1] :temp_word:-u[i]*v[j]+w[i+j+l]+k
mul byte ptr v[bx]
xor dx.dx
mov dl ,w[bx+si+l]
add ax.dx
xor dx.dx
mov dl , k
add ax.dx :t=(ax) - временная переменная
:w[i+j+l]:=temp_word MOD b //остаток от деления temp_word\b -> w[i+j+l] :k:=temp_word\b //целая часть частного temp_word\b -> k
push dx
xor dx.dx
div b
mov ah.dl
popdx
mov k.al
mov w[bx+si+l].ah :m5 .
dec si
loop m4 ;КОН_БЛ0К_2
moval.k ;w[j]:=k
mov w[bx].al m6: dec bx
pop ex
loop m2 ;КОН_БЛОК_1
ret ;КОН_ПРОГ mul_unsign_NM endp main:
call mul_unsign_NM end main

В отличие от обычного умножения «в столбик» в данном алгоритме сложение частичных произведений выполняется параллельно умножению. Программа производит умножение значений в порядке — старший байт по младшему адресу. Это неестественно для микропроцессоров Intel, поэтому программу необходимо соответствующим образом изменить. Текст измененной процедуры mul_ unsign_NM_I приведен на дискете.
Процедуру умножения чисел без учета знака mul_unsign_NM удобно представить в виде макрокоманды mul_unsign_NM_r u,i ,v, j,w. Это без излишних усложнений сделает ее вызов более универсальным. При последующем рассмотрении программы деления многобайтных двоичных чисел она будет использована нами с большой пользой. Текст макрокоманды приведен на дискете. На дискете также имеется вариант этой макрокоманды mul_unsign_NM u,i ,v, j.WHa случай естественного для микропроцессоров Intel расположения операндов — младший байт по младшему адресу.

Умножение чисел размером 1 байт с учетом знака

;mul_sign.asm - программа умножения чисел размером 1 байт с учетом знака ;Вход: multiplier], и multiplied - множители со знаком размерностью 1 байт. ;Выход: product - значение произведения.
.data значения в multiplier], и multiplied нужно внести
product label word
productj label byte
multiplierl db ? ;множитель 1 (младшая часть произведения)
product_h db 0 :старшая часть произведения
multiplied db ? :множитель 2
.code
mul_sign proc
mov al.multiplierl
imul multiplied :оценить результат:
jnc no_carry :нет переполнения - на no_carry обрабатываем ситуацию переполнения
mov productji.ah :старшая часть результата, знак результата - старший бит product_h no_carry: mov productj,al :младшая часть результата, productji - расширение знвка
ret
mul_sign endp. main:
call mul_sign end main

Аналогично умножению без знака здесь также все достаточно просто и реализуется средствами самого процессора. Проблема та же — правильное определение размера результата. Произведение чисел большей размерности (2/4 байта) выполняется аналогично. Необходимо заменить директивы DB на DW/DD, регистр AL на АХ/ЕАХ, регистр АН на DX/EDX. Более того, в отличие от команды MUL команда IMLJL допускает более гибкое расположение операндов.

Умножение чисел размером N и М байт с учетом знака

Как уже не раз отмечалось, система команд микропроцессора содержит два типа команд умножения — с учетом знаков операнда (IMUL) и без него (MUL). При умножении операндов размером 1/2/4 байта учет знака производится автоматически — по состоянию старших (знаковых) битов. Если умножаются числа размером в большее количество байтов, то для получения правильного результата необходимо учитывать знаковые разряды только старших байтов. В основе программы, реализующей алгоритм умножения чисел размером N и М байт с учетом знака, лежит рассмотренная выше процедура умножения чисел произвольной размерности без учета знака.

Умножение N-байтного числа на число размером М байт с учетом знака

ПРОГРАММА mul_sign_NM
//---------------------------------------------------------
//mul_sign_NM - программа на псевдоязыке умножения N-байтного числа
//на число размером М байт
//(порядок - старший байт по младшему адресу (не Intel)) //
Вход: U и V - множители со знаком размерностью N и М байт соответственно;
//Ь=256 - размерность машинного слова. //Выход: W - модуль (дополнение) произведения размерностью N+M байт.
//---------------------------------------------------------
ПЕРЕМЕННЫЕ
INTJ3YTE u[n]; //множитель 1 размерностью N байт
INT_BYTE v[n]; //:множитель 2 размерностью М байт
INT_BYTE w[n+m]: k=0://перенос 0 < k < 255
INT_BYTE sign=0: //информация о знаке
INT_WORD b=256: temp_word //b - размер машинного слова
НАЧ_ПРОГ
//определим знак результата
ЕСЛИ БИТ_7_БАЙТА((и[0] AND 80h) XOR v[0])==1 TO sign:=1
//результат будет отрицательным //получим модули сомножителей: u:-|u| v:-]v|
w:=mul_unsign_NM() //в этой точке - модуль результата //восстанавливаем знак результата ЕСЛИ sign==0 TO ПЕРЕЙТИ_НА Шт
//для отрицательного результата вычислить дополнение значения w длиной i+j w:=calc_complement_r() //в этой точке - двоичное дополнение результата @йп: КОН_ПРОГ
:mul_sign_NM.asm - программа на ассемблере умножения N-байтного числа :на число размером М байт
;(порядок - старший байт по младшему адресу (не Intel))
.data ;значения в U и V нужно внести
;Помните. что задание отрицательных многобайтных значений в ;сегменте данных должно производиться в дополнительном коде ;(и в порядке байтов - старший байт по младшему адресу)!
U db ? :множитель 1 размерностью N байт
i-$-U
V db ? ;множитель 2 размерностью М байт
J-$-V
len_product=$-U
W db len_product dup (0) результат длиной N+M байт
k db 0 ;перенос О < k < 255
b dw lOOh ;размер машинного слова
sign db 0 информация о знаке
.code
;включить описание процедур calc_complement_r. calc_abs_r. :mul_unsign_NM
mul_sign_NM ргос ;НАЧ_ПРОГ юпределим знак результата
хог ах.ах ; ЕСЛИ БИТ_7_БАЙТА((и[0] AND 80h) XOR v[0])==1 TO sign:=1
mov al.u
and al.80h
xor al.v
bt ax,7
jnc $+7
mov sign.l результат будет отрицательным
lea bx.v ;получим модули сомножителей: u:~|u|; v:=|v|
mov ex.j
call calc_abs_r
1 ea bx, u
mov ex.i
call calc_abs_r ;теперь умножаем
call mul_unsign_NM ;w:=inul_unsign_NM() ;в этой точке - модуль результата ;восстанавливаем знак результата
хог si. si
emp sign.0 :ЕСЛИ sign==0 ТО ПЕРЕЙТИ_НА №т
je @@m ://для отрицательного результата вычислить дополнение значения w длиной i+j
mov ex,i+j ;w:=calc_complement_r(); w[0]:=0-w[0]
lea bx.w
call calc_complement_r ;в этой точке - двоичное дополнение результата @@m: ret ;КОН_ПРОГ
mul_sign_NM endp main:
call mul_sign_NM end main

Процедура mul_sign_NM выполняет умножение с учетом знака исходных значений в порядке байтов — старший байт по младшему адресу. Если вы используете отрицательные значения операндов, тс для правильной работы тестовой программы в сегменте данных их необходимо задать в дополнительном коде.
В данной программе используются две новые процедуры — calc_complement_r и calc_abs_r, вычисляющие соответственно дополнение и модуль числа размером N байт. Подобные процедуры уже были разработаны и введены нами для значений, порядок следования байт которых характерен для микропроцессоров Intel. Чтобы различать эти две пары процедур, процедуры для вычисления дополнения и модуля числа размером байт с порядком следования байтов, отличным от Intel, мы назвали реверсивными.

Вычисление дополнения числа размером N байт (реверсивное)

:calc_complement_r - процедура на ассемблере вычисления дополнения числа размером N байт
:(старший байт по младшему адресу).
;Вход: регистр ВХ - адрес операнда в памяти: регистр СХ - длина операнда. ;Выход: регистр ВХ - адрес дополнения операнда в памяти.
ca1c_complement_r ргос
dec ex
mov si.ex
neg byte ptr [bx][si] дополнение первого байта
cmp byte ptr [bx][si].O :operand=0 - особый случай
jne short $+3
stc установить cf, так как есть перенос
jcxz @@exit_cycl :для однозначного числа
@@cycl:dec si
not byte ptr [bx][si]
adc byte ptr [bx][si],0
loop @@cycl @@exit_cycl: ret calc_complement_r endp
Для значений размерностью 1/2/4 байта дополнение можно получать с помощью одной команды NEG:
neg operand

Дополнение значений N байт вычисляет алгоритм, реализованный в процедуре calc_complement_r. Обратите внимание, что первый байт может быть нулевым, поэтому алгоритм учитывает это обстоятельство. Попытка получить его дополнение с помощью команды NE6 обречена на провал. Флаг CF в этом случае также должен устанавливаться программно. Подумайте, почему?

Вычисление модуля числа размером N байт (реверсивное)

calc_abs_r - процедура на ассемблере вычисления модуля числа размером N байт
:(старший байт по младшему адресу).
:Вход: регистр ВХ - адрес операнда в памяти: регистр СХ - длина операнда. :Выход: регистр ВХ - адрес модуля операнда в памяти.
.code
calc_abs_r proc определим знак операнда
test byte ptr [bx],80h ;проверяем знак operand
jz @@exit :число положительное
call calc_complement_r @@exit: ret calc_abs_r endp

Для вычислений над операндами, порядок следования байтов которых характерен для микропроцессоров Intel, нам придется разработать еще один вариант процедуры умножения значений размерностью в произвольное количество байтов. Псевдокод и соответствующая ему программа на ассемблере mul_sign_NM_I приведены на дискете.

Деление двоичных чисел

Аналогично операции умножения деление реализуется двумя типами команд — учитывающими и не учитывающими знаки операндов. Эти команды накладывают ощутимые ограничения на размерность (а соответственно и на диапазон значений) операндов. Поэтому мы рассмотрим вначале пример стандартного применения команд, после чего займемся реализацией алгоритмов деления чисел произвольной размерности.

Деление без учета знака значения размером 2 байта на значение размером 1 байт

:div_unsign.asm - программа на ассемблере деления без учета знака значения
размером 2 байта на значение размером 1 байт.
:Вход: и - делимое: v - делитель.
;Выход: w - частное, г - остаток.
.data :значения в и и v нужно внести
u dw ? :делимое
v db ? :делитель
w db 0
г db 0
.code
div_unsign proc
mov ax.u
div v сформировать результат:
mov r.ah :остаток
mov w.al :частное
ret
divjjnsign endp , main:
call div_unsign end ma in

Деление чисел большей размерности (4/8 байтов) выполняется аналогично. Необходимо заменить директивы DB на DW/DD, регистр АХ на EAX/EDX: ЕАХ, регистр AL на АХ/ЕАХ, регистр АН на DX/EDX.

Деление с учетом знака значения размером 2 байта на значение размером 1 байт

:div_sign.asm - программа на ассемблере деления с учетом знака значения
;размером 2 байта на значение размером 1 байт.
:Вход: и - делимое; v - делитель.
:Выход: w - частное, г - остаток.
.data значения в и и v нужно внести
и dw ? ;делимое
v db ? ;делитель
w db О
г db О
.code
div_sign proc
mov ax.u
idiv v сформировать результат:
mov г,ah :остаток
mov w.al ;частное
:если нужно получить модуль - уберите знаки комментария : mov cx.l ;длина операнда ; lea bx.w : call calc_abs ;или ; neg w
ret
div_sign endp main:
call div_sign
end main

Деление чисел большей размерности (4/8 байтов) выполняется аналогично. Необходимо заменить директивы DB на DW/DD, регистр АХ на EAX/EDX:EAX, регистр AL на АХ/ЕАХ, регистр АН на DX/EDX.
Деление N-разрядного беззнакового целого на одноразрядное число размером 1 байт

:div_uns1gn_N_1.asm - программа на ассемблере деления без учета знака значения
:размером N байт на значение размером 1 байт.
;Порядок следования байтов - старший байт по младшему адресу (не Intel) :Вход: и - делимое; v - делитель. :Выход: w - частное, г - остаток.
.data значения в и и v нужно внести
u db ? ;делимое
N=$-u :длина в байтах значения и
v db ? :делитель
w db N dup (0)
г dw 0 :остаток
.code
div_unsign_N_lproc
mov г.О
хог si.si ;j»0
mov cx.N
хог dx.dx
хог bx.bx @@ml: movax.256 основание с.с.
mul г результат в dx:ax
mov bl ,u[si]
add ax.bx
div v сформировать результат:
mov w[si].al :частное
shr ax.8
mov г.ах юстаток в г
inc si
1oop @@ml
ret
d i v_uns i gn_N_lendp main:
call div_unsign_N_l
end main
Сегмент данных может быть задан, например, так:
.data значения в и и v нужно внести
u db 5.6.7 :делимое
N=$-u :длина в байтах значения и
v db 15 ;делитель
w db N dup (0)
г dw 0 :остаток

В программе div_unsign_N_l.asm порядок следования байтов делимого неестествен для микропроцессора Intel. Поэтому на дискете приведен вариант программы div_unsign_N_l_I.asm, в котором эта проблема устранена.
Далее при рассмотрении программы деления многобайтных двоичных чисел нам понадобится макрокоманда divunsignN деления N-разрядного беззнакового целого на одноразрядное число размером 1 байт (порядок следования байтов не характерен для процессоров Intel, то есть старший байт находится по младшему адресу). Текст макрокоманды приведен на дискете.

Деление (N+М)-разрядного беззнакового целого на число размером М байт

ПРОГРАММА div_unsign_NM

;----------------------------------------------------------
;div_unsign_NM.asm - программа на ассемблере деления (N+M)-разрядного беззнакового целого на число размером N байт
;(порядок - старший байт по младшему адресу (не Intel))
;Вход: U и V - u=um+n-1..+u1u0 - делимое; v=vn-1…v1v0-
;делитель, m - длина делимого;n - длина делителя; b=256 -
;основание системы счисления.
;Выход: q=qmqm-1…q1q0 - частное, r=rn-1…r1r0 - остаток.
;Ограничения: vn-1№0 OR 0ffh; N>1.
;----------------------------------------------------------
masm
model small,c
.data
;значения в u и v нужно внести
u0 db 0 ;дополнительный старший байт делимого для нормализации
u db 1fh,0c3h,12h,0ffh ;делимое
m=$-u ;длина в байтах значения u
v0 db 0 ;для компенсирующего сложения 0vn-1…v1v0
v db 3fh,50h ;делитель
n=$-v ;длина в байтах значения v
mm=m-n
w db m+1 dup (0) ;для промежуточных вычислений
q db mm dup (0) ;частное
qq dw 0 ;частичное частное ;qq db 0
rr dw 0 ;частичный остаток
r db n dup (0) ;остаток
d db 0
temp dw 0
temp_r db n dup (0)
borrow db 0 ;факт заема на шаге D4
k db 0 ;перенос 0 Ј k < 255
b dw 100h ;размер машинного слова
carry db 0

.stack 256
.486
.code
calc_complement_r proc
dec cx
mov si,cx
neg byte ptr [bx][si] ;дополнение первого байта
cmp byte ptr [bx][si],0 ;operand=0 - особый случай
jne short $+3
stc ;установить cf, так как есть перенос
jcxz @@exit_cycl ;для однозначного числа
@@cycl: dec si
not byte ptr [bx][si]
adc byte ptr [bx][si],0
loop @@cycl
@@exit_cycl: ret
calc_complement_r endp

mul_unsign_NM macro u,i,v,j,w
local m2,m4,m6
push si
;очистим w
push ds
pop es
xor al,al
lea di,w
mov cx,i+j
rep stosb

;m1
mov bx,j-1 ;j=0..m-1
mov cx,j
m2:
push cx ;вложенные циклы
cmp v[bx],0
je m6
;m3
mov si,i-1 ;i=0..n-1
mov cx,i
mov k,0
m4:
mov al,u[si]
mul byte ptr v[bx]
xor dx,dx
mov dl,w[bx+si+1]
add ax,dx
xor dx,dx
mov dl,k
add ax,dx ;t=(ax) ? временная переменная
push dx
xor dx,dx
div b ;t mod b
mov ah,dl
pop dx
mov k,al
mov w[bx+si+1],ah
;m5
dec si
loop m4
mov al,k
mov w[bx],al
m6:
dec bx
pop cx
loop m2
pop si
endm

sub_sign_N macro minuend,deduction,N
local cycl,m1
;старший байт по младшему адресу
push si
mov cl,N
mov si,N-1
cycl: mov al,deduction[si]
sbb minuend[si],al
; jnc m1
; neg minuend[si]
m1: dec si
loop cycl
pop si
endm

add_unsign_N macro carry,summand_1,summand_2,N
local cycl,end_p
mov cl,N
mov si,N-1
cycl: mov al,summand_2[si]
adc summand_1[si],al
dec si
loop cycl
jnc end_p
adc carry,0
end_p: nop
endm

div_sign_N macro u,N,v,w,r
local m1
;старший байт по младшему адресу
mov r,0
lea si,u ;j=0
xor di,di ;j=0
mov cx,N
xor dx,dx
xor bx,bx
m1: mov ax,256 ;основание с.с.
mul word ptr r ;результат в dx:ax
mov bl,[si]
add ax,bx
div v
;сформировать результат:
mov w[di],al ;частное
mov r,ah ;остаток в r
inc si
inc di
loop m1
;если нужно - получим модуль (уберите знаки комментария)
; mov cx,N ;длина операнда
; lea bx,w
; call calc_abs_r
endm

div_unsign_NM proc
;НАЧ_ПРОГ
;//шаг 1 - нормализация:
;D1 - нормализация
;d:=b/(v[n-1]+1)
xor ax,ax
mov dl,v
inc dl ;vn-1+1
mov ax,b
div dl
mov d,al ;d=b/(v1+1)
;u[n+m…0]:=u[n+m-1…0]*d
mul_unsign_NM u,m,d,1,w
cld
push ds
pop es
lea si,w
lea di,u0
mov cx,m+1
rep movsb
;v[n-1…0]:=v[n-1…0]*d
mul_unsign_NM v,n,d,1,w
cld
push ds
pop es
lea si,w+1
lea di,v
mov cx,n
rep movsb
;//шаг 2 - начальная установка j:
;mm:=m-n; j:=mm
;D2:
mov si,0 ;n=0 (? n=n+m)
;D3:
@@m7:
;//шаг 3 - вычислить частичное частное qq :
;qq:=(u[j+n]*b+u[j+n-1]) / v[n-1]
;rr:=(u[j+n]*b+u[j+n-1]) MOD v[n-1]
@@m1: xor ax,ax
mov al,u0[si]
mul b
shl eax,16
shrd eax,edx,16 ;результат умножения в eax
xor edx,edx
mov dl,u0[si+1]
add eax,edx
shld edx,eax,16 ;восстановили пару dx:ax для деления
xor bx,bx
mov bl,v ;v->bx
div bx
mov qq,ax
mov rr,dx
@@m2:
;проверим выполнение неравенства:
;ДЕЛАТЬ ПОКА tf
;НАЧ_БЛОК_1
;ЕСЛИ qq==b OR qq*v[n-2] > b*rr+ u[j+n-1] ТО
;НАЧ_БЛОК_2
;qq:=qq-1
;rr:=rr+v[n-1]
;ЕСЛИ rrіb ТО tf:=FALSE
;КОН_БЛОК_2
;ИНАЧЕ tf:=FALSE
;КОН_БЛОК_1
@@m4:
mov ax,qq
cmp ax,b ;qq<>b
je @@m9 ;на qq=qq-1
;qq*vn-2>b*rr+uj+n-2
mul v+1 ;qq*vn-2
mov temp,ax ;temp=vn-2*qq
xor ax,ax
mov ax,b
mul rr
xor dx,dx
mov dl,u0[si+2]
add ax,dx
cmp temp,ax ;qq*vn-2 > b*rr+uj+n-2
jna @@m8
@@m9:
dec qq
xor ax,ax
mov al,v
add rr,ax
jmp @@m4
@@m8:
@@m3:
;D4
;//шаг 4 - умножить и вычесть:
;u[j+n…j]:=u[j+n…j]-qq*v[n-1…0]
mul_unsign_NM v,n,qq,1,w
mov bx,si
push si
sub_sign_N u0[bx],w,<n+1> ;v<->w
;ЕСЛИ u[j+n…j]<0 ТО ;запоминаем факт заема, получаем дополнение
;НАЧ_БЛОК_3
;borrow:=1
;u[j+n…j]:=calc_complement_r(u[j+n…j])
;КОН_БЛОК_3
jnc @@m5 ;переход, если нет заема (результат положительный)
mov borrow,1 ;запоминаем факт заема, получаем дополнение
pop si
lea bx,u0[si]
mov cx,n+1
call calc_complement_r
;D5
;//шаг 5 - проверка остатка:
;q[j]:=qq
@@m5: mov ax,qq
mov q[si],al
;ЕСЛИ borrow<>1 ТО
cmp borrow,1 ;был заем на шаге D4 ??
jne @@m6
;НАЧ_БЛОК_4
;//шаг 6 - компенсирующее сложение:
;q[j]:= q[j]-1
;u[j+n…j]:=u[j+n…j]+v[n-1…0]
;КОН_БЛОК_4
;D6 - компенсирующее сложение
mov borrow,0 ;сбросим факт заема
dec q[si]
mov bx,si
push si
add_unsign_N carry,u0[bx],v0,<n+1> ;перенос не нужен
;D7
;//шаг 7 - цикл по j:
;j:=j-1
@@m6: pop si
inc si
;ЕСЛИ jі0 ТО ПЕРЕЙТИ_НА @@m7
cmp si,mm
jle @@m7
;D8 - денормализация
;//шаг 8 - денормализация:
;//вычислим остаток:
;r[n-1…0]:=u[n-1…0]/d
mov bx,si
div_sign_N u0[bx],N,d,r,temp_r
ret
;//q[m…0] - частное, r[n-1…0] ? астаток
;КОН_ПРОГ
div_unsign_NM endp

main:
mov dx,@data
mov ds,dx

call div_unsign_NM

mov ax,4c00h
int 21h
end main

Программа не работает, если первый байт делителя равен 0f fh. Сам алгоритм не изменяется, а проблема устраняется просто — необходимо лишь в нужных местах программы поменять разрядность операндов.
Порядок следования байтов делимого неестествен для микропроцессора Intel. Программу деления многобайтных двоичных чисел с порядком следования байтов «младший байт по младшему адресу» оставляем вам в качестве упражнения.

Двоично-десятичные числа (BCD-числа)

Работай постоянно, не почитай работу для себя бедствием
или бременем и не желай себе за это похвалы и участия.
Общее благо - вот чего ты должен желать.
Марк Аврелий

Понятие о BCD-числах и элементарных действиях с ними приведены в уроке 8 «Арифметические команды» учебника. В отличие от действий с двоичными числами работа с BCD-числами в микропроцессоре реализована косвенно. В его системе команд нет инструкций, которые непосредственно выполняют основные арифметические действия над BCD-числами в том виде, как это делается для двоичных операндов. Тем более нет средств, которые каким-то образом учитывали бы знак. Все это должен делать сам программист. Ниже приведены макрокоманды, которые выполняют базовые арифметические операции над BCD-числами различной разрядности.

Неупакованные BCD-числа

Перед тем как приступать к работе с BCD-числами, необходимо договориться о том, как будут представляться BCD-числа в оперативной памяти — старший разряд BCD-числа по старшему адресу или старший разряд BCD-числа по младшему адресу. Изменить фрагмент программы для поддержки того или иного способа представления чисел не представляет особого труда. Поэтому для однозначности рассуждений выберем естественный для микропроцессоров Intel порядок следования байтов — младший байт по младшему адресу.

Сложение неупакованных BCD-чисел (макрокоманда)

add_bcdmacro summand_i.1en_l,summand_2.1 en_2,sum local ml.m2.m3
:add_bcd summand_1.1en_l,summand_2.1en_2.sum - макрокоманда
:сложения неупакованных BCD-чисел размером 1еп_1 и len_2
:байт и помещение результата в sum.
:Вход: summand_i и summand_2 - адреса младших байтов
хлагаемых; 1еп_1 и 1еп__2 - длины слагаемых в байтах.
;Выход: sum - адрес младшего байта поля суммы. Желательно.
:чтобы это поле имело длину на единицу больше, чем длина
:самого длинного слагаемого.
;Порядок следования байт - младший байт по младшему адресу (Intel).
push si
push bx
mov ax.len_l
cmp ax.len_2
jna m2
mov cx,len_l ;длина большего для сложения (см. ниже)
push ex
mov cx,len_2 ;длина меньшего для сложения (см. ниже)
push ex
mov cx.ax
lea bx.summand_l :адрес большего источника для сложения
lea si,summand_2 :адрес меньшего источника для movsb
jmp m3
т2: mov сх.1еп_2 :длина большего для сложения (см. ниже)
push ex
mov cx.len_l ;длина меньшего для сложения (см. ниже)
push ex
mov cx.len_2
lea bx.summand_2 ;адрес большего источника для сложения
lea si.summand_l :адрес меньшего источника для movsb m3: заполняем sum нулями - длина определена выше:
eld
хог al.al
lea di. sum rep stosb ;пересылка меньшего (по длине) BCD-числа в sum:
eld
push ds
pop es
lea di. sum :адрес источника см. выше
pop сх ;длина была определена выше и соотв. меньшему ВСО-числу rep movsb
pop сх ;дикл по большему
хог si,si
ml: mov al.[bx][si]
adc al, sum[si]
aaa
mov sum[si].al
inc si
loop ml
adc sum[si].O
pop bx
pop si
endm

Макрокоманда обрабатывает байты, исходя из предположения, что младший байт слагаемых находится по младшему адресу. Слагаемые необязательно имеют одинаковую длину, но сумма должна иметь размер поля на единицу больше, чем длина самого длинного слагаемого. В начале процесса сложения в поле результата помещается меньшее (по длине) слагаемое.
Нам понадобится и другой вариант этой команды — addbedr, который обрабатывает операнды с порядком следования байтов — старший байт по младшему адресу.

Вычитание неупакованных BCD-чисел (макрокоманда)

sub_bcdmacro minuend.lenjn. deduction.len_d. difference local temp.ml.m2.exit_m
:sub_bcd minuend".len_m.deduction,len_d.difference -макрокоманда вычитания неупакованных BCD-чисел размером ;len_m и len_d байт и помещение результата в difference. ;Вход: minuend и deduction - адреса младших байтов уменьшаемого и вычитаемого: len_m и len_d - длины уменьшаемого и вычитаемого в байтах.
;Выход: difference - адрес младшего байта поля разности. :Длина поля difference должна быть не меньше длины :уменьшаемого.
;Порядок следования байт - младший байт по младшему адресу (Intel).
push si
.¦копируем уменьшаемое в difference:
push ds
pop es
eld
lea si.minuend
lea di.difference
mov cx.lenjn
push ex rep movsb
jmp ml ;копируем вычитаемое во врем, область temp:
temp db len_m dup (0)
ml: lea si .deduction
lea di,cs:temp
mov cx.len_d
push cs
pop es rep movsb
xor si.si
pop ex
m2: mov al,minuend[si]
sbb al,cs:temp[si]
aas
mov difference[si].al
inc si
1oop m2
jc m3 :на обработку заема из старшего разряда
jmp exit_m m3: пор exitjn:
pop si
end

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

Умножение неупакованных BCD-чисел (макрокоманда)

.data
k db 0 :перенос 0 < к < 255
b dw 10 ;основание системы счисления
.code
mul_bcdmacro u.i.v.j.w *
local m2.m4.m6
:mul_bcd u.i.v.j.w - макрокоманда умножения неупакованных
:BCD-чисел u и v размером i и j байт и помещение результата
:в w.
;Вход: и - адрес первого множителя; i - длина u: v - адрес
;второго множителя: j - длина v: w - адрес области
:размерностью i+j байт, куда необходимо поместить
:произведение: Ь=256 - размерность машинного слова.
:Выход: w - произведение размерностью i+j байт.
:Порядок следования байтов - младший байт по младшему адресу
:(Intel).
:сохраним регистры
push si
:очистим w
eld
push ds
pop es
xor al.al
lea di ,w
mov ex,i+j
rep stosb
xor bx.bx ;j=0..m-l
mov CX.j
m2: push ex : вложенные циклы
CiTlp v[bx].O '
je тб
:m3
xor si,si :1=0..n-1
mov cx.i
mov k.O
m4: mov al,u[si]
mul v[bx]
xor dx.dx
mov dl.w[bx+si]
add ax.dx
xor dx.dx
mov dl ,k
add ax.dx ;t-(ax) -- временная переменная
:корректируем результат - (ап)-цифра переноса: ;(а1)=результат
aam
mov k.ah
mov w[bx+si].al
:m5
inc si
loop m4
mov al.k
mov w[bx+si],al
m6: inc bx
pop ex
loop m2
pop si
endm

Нам понадобится и другой вариант этой команды — mul_bcd_r, который обрабатывает операнды с порядком следования байтов — старший байт по младшему адресу.

Деление N-разрядного беззнакового целого BCD-числа на одноразрядное BCD-число размером 1 байт (макрокоманда)

div_bcd_l_r macro u.N.v.w.r local ml
:div_bcd_l_r u.N.v.w.r - деление N-разрядного беззнакового целого BCD-числа
;на одноразрядное BCD-число размером 1 байт. :Вход: и - делимое; N - длина делимого, v - делитель. :Выход: w - частное, г - остаток. :Порядок следования байтов - старший байт по младшему адресу (не Intel) (!).
mov г.О
lea si.u ;j=0
хог di.di :j=0
mov cx.N
xor dx.dx
xor bx.bx
ml: mov ah,г
mov al.[si]
aaJ
div v сформировать результат:
mov w[di].al ;частное
mov r.ah .-остаток в г
inc si
inc di
loop ml
:если нужно - получим модуль (уберите знаки комментария)
: mov cx.N ;длина операнда
: lea bx.w
; call calc_abs_r
endm
Деление неупакованных BCD-чисел
:div_bcd_NM_r.asm - деление неупакованных BCD-чисел и и v размером n+m и п байт.
;Вход: и-и^гиз-.и,^,, - делимое: v=v1v2...vn - делитель. Ь=25б - основание системы счисления.
;Выход: q™q1q2-.qm - частное. r=rir2...rn - остаток,
:Порядок следования байт - старший байт по младшему адресу (не Intel) (!).
.data значения в и и v нужно внести
uO db 0 дополнительный байт для нормализации
u db 1,0.3.5,9,4,6 :делимое
m=$-u :длина в байтах значения и
vO db 0 :для сложения OvjV2..vn
v do 5.9.1 :делитель
n=$-v :длина в байтах значения v
ГПП1=П1-П
w db m+1 ctup (0) :для промежуточных вычислений q db mm dup (0) :частное qq db 0
г db n dup (0) :остаток
b dw 10 юснование системы счисления
d db 0
temp dw 0
temp_r db n dup (0)
borrow db 0 :факт заема на шаге D4
k db 0 ;перенос 0 =< k < 255
carry db 0
.code
:вставить описание макрокоманд calc_complement_r. mul_bcd_r. sub_bcd_r. add_bcd_r. :div_bcd_l_r
div_bcd_NM_r proc ;D1 - нормализация
xor ax.ax
mov dl.v
inc dl :vl+l
mov ax.b
div dl
mov d.al :d=b/(v1+l)
mul_bcd_r u.m.d.l.w
eld
push ds
pop es
lea si.w
lea di.uO
mov cx.m+1 rep movsb
mul_bcd_r v.n.d.l.w
eld
push ds
pop es
lea si.w+1
lea di.v
mov ex,n rep movsb :D2:
mov si.O ;n=0 :D3: @@m7: moval.uO[si]
emp v,al
jne @@ml
mov qq.9 :qq=b-l
jmp @@m2
(<t@ml: xor ax,ax
mov al.uO[si]
mul b
xor dx.dx
mov dl,uO[si+l]
add ax.dx
mov bl.v ;v->bx divbl
mov qq.al
@@m2: :проверим выполнение неравенства:
@@m4: mul v+1
mov temp. ax: taiip=v2*qq
xor ax.ax
mov al ,uO[si]
mul b
xor edx.edx
mov dl.uO[si+l] add dx.ax
mov al.qq
mul v :eax=qq*vl
sub dx,ax
xchg dx.ax
mul b
xor bx.bx
mov bl.uO[si+2]
add ax.bx
cmp temp,ax
jna (a@m3
dec qq
mov al.qq
jmp @@m4
@@m3: : D4
mul_bcd_r v.n.qq.l.w
mov bx.si
push si
sub_bcd_r uO[bx].<n+l>.w,<n+l>,uO[bx] ;v<->w
jnc @@m5 :переход, если нет заема (результат положительный)
mov borrow.1 ;запоминаем факт заема, получаем дополнение
pop si ;D5
@@m5: moval.qq
mov q[si].al
cmp borrow.1 :был заем на шаге D4 ??
jne @@m6 :D6 - компенсирующее сложение
mov borrow.0 :сбросин факт заема
dec q[si]
mov bx.si
push si
add_bcd_r uO[bx].<n+l>.vO,<n+l>,uO ;перенос не нужен :D7
@@m6: pop si
inc si
cmp si.mm
jle @@m7 :D8 - денормализация
mov bx.si
div_bcd_l_r uO[bx],N,d.r.temp_r
ret
div_bcd_NM_r endp *
main:
call div_bcd_NM_r end main

Упакованные BCD-числа

В отличие от неупакованных BCD-чисел, разработчики команд микропроцессора Intel весьма сдержанно отнеслись к проблеме обработки упакованных BCD-чисел. Существуют только две команды — DAA и DAS, которые поддерживают процесс сложения и вычитания упакованных BCD-чисел. Умножение и деление этих чисел не поддерживается вовсе. По этой причине при необходимости выполнения арифметических вычислений с упакованными BCD-числами есть смысл предварительно преобразовывать их в неупакованное представление, выполнять необходимые действия, результат которых конвертировать (если нужно) обратно в упакованный вид. Так как действия по преобразованию не являются основными в процессе вычислений, то желательно, чтобы они были максимально быстрыми. Можно предложить много вариантов подобного преобразования. Рассмотрим один из них.

Преобразование упакованного BCD-числа размером N байт в неупакованное BCD-число (макрокоманда)

BCD_PACK_TO_UNPACK macro PACK.N. UNPACK local cycl
;BCD_PACK_TO_UNPACK PACK,N.UNPACK - макрокоманда преобразования упакованного BCD-числа размером N байт в неупакованное BCD-число размером N*2 байт ;Порядок следования байтов - младший байт по младшему адресу :(Intel).
сохраняем регистры ...
push ds
pop es
mov ecx.N
eld ;порядок обработки BCD-цифр - начиная с младшей
lea edi.UNPACK
lea esi.PACK cycl: xorax.ax
lodsb ;загрузить очередные 2 упакованные BCD-цифры из PACK в al
гог ах.4
гог ah.4
stosw
loop cycl восстанавливаем регистры ...
endm

Преобразование неупакованного BCD-числа размером N байт в упакованное BCD-число (макрокоманда)

ВС D_U N РАС К_ТО_РАСК macro UNPACK.N,PACK local cycl
:BCD_UNPACK_TO_PACK UNPACK,N.PACK - макрокоманда преобразования неупакованного
iBCD-числа"размером N байт в упакованное BCD-число. ;Порядок следования байтов - младший байт по младшему адресу ;(Intel).
сохраняем регистры ... push ds
pop es
mov ecx.N
:определяем N/2 (размерность PACK) - если нечетное, юкругляем в большую сторону
shr ecx.l ;делим на 2
bt есх.О
jc $+4
setc Ы
inc есх добавляем 1 для округления в больщую сторону предыдущие три команды можно заменить одной: adcecx.O :теперь в есх правильное значение сч. цикла в соответствии с размерностью UNPACK
eld шорядок обработки BCD-цифр - начиная с младшей
lea edi.PACK
lea esi.UNPACK cycl: xorax.ax загрузить очередные 2 неупакованные BCD-цифры из UNPACK в ах
lodsw
rol ah,4
rol ax,4
stosb
loop cycl
emp . 0
jne $+7
and byte ptr [edi-1].OfOh восстанавливаем регистры ...
endm

Генерация последовательности случайных чисел

Знание некоторых принципов нередко
возмещает незнание некоторых фактов.
Гельвецкий

Важный класс вычислительных алгоритмов, востребованных на практике, — алгоритмы генерации последовательности случайных величин. По определению, последовательностью случайных чисел является последовательно формируемый массив значений, в котором каждый очередной элемент получен вне всякой связи с другими его элементами и появление этого элемента возможно с вероятностью, подчиненной некоторому закону распределения. К примеру, если появление любого числа в этой последовательности равновероятно, то говорят о равномерном законе распределения случайных чисел.
На практике в большинстве случаев применяются программные методы получения случайных чисел. На самом дел'е случайные последовательности, получаемые по некоторому алгоритму, являются псевдослучайными. Это происходит из-за того, что связь между значениями в последовательности, получаемой программным путем, обычно все-таки существует. Данное обстоятельство приводит к тому, что на каком-то этапе генерируемая последовательность чисел начинает повторяться — «входит в период». Рассмотрим несколько наиболее известных
и пригодных для практического использования программных методов генерации псевдослучайных величин (все же, понимая смысл выражения, будем по привычке и для удобства называть их случайными).

Конгруэнтный метод генерации последовательности случайных чисел

Конгруэнтный метод генерации последовательности случайных чисел получил широкое распространение. Он описан во многих источниках, но конкретных рекомендаций по его использованию на платформе Intel автор не встретил. Попытаемся устранить этот недостаток.
В основе этого метода генерации последовательности случайных чисел лежит понятие конгруэнтности. По определению, два числа А и В конгруэнтны (сравнимы) по модулю М в случае, если существует число К, при котором А-В=КМ, то есть если разность А-В делится на М, и числа А и В дают одинаковые остатки от деления на М. Например, числа 85 и 5 конгруэнтны по модулю 10, так как при делении на 10 дают остаток 5 (при К=1). В соответствии с этим методом каждое число в этой последовательности получается исходя из следующего соотношения:

Хn+1=(аХn+с) mod m, где n > 0. (1)

При задании начального значения Хо, констант а и с данное соотношение однозначно определяет последовательность целых чисел X,, составленную из остатков от деления на m предыдущих членов последовательности, в соответствии с соотношением (1). Величина этих чисел не будет превышать значение т. Если каждое число этой последовательности разделить на т, то получится последовательность случайных чисел из интервала 0.1.1'. Но не спешите подставлять в это соотношение какие-либо значения. Основная трудность при использовании этого метода — подбор компонентов формулы. В зависимости от значения с различают два вида конгруэнтного метода — мультипликативный (с=0) и смешанный (с не равно 0).
Для простоты изложения будем генерировать небольшие по величине случайные числа. Это дает возможность легко отслеживать особенности работы рассматриваемых алгоритмов с использованием стандартной возможности перенаправления ввода-вывода. Для этого необходимо, чтобы текст программы содержал строки:

movdl .ah
mov ah.02
int 21h

Запуск программы производиться командной строкой вида:

prog.exe > p.txt

При этом создается файл p.txt, в который и выводятся результаты работы программы. Если не использовать этой возможности, то вывод будет производиться на экран, что не очень удобно для последующего анализа получающейся последовательности случайных чисел. Более подробно о возможностях работы с файлами и экраном читайте материал в главах 5 и 7, посвященных работе с файлами и консолью из программ на языке ассемблера.
Большинство представленных ниже программ функционируют в бесконечном цикле, с тем, чтобы можно было изучать периодичность последовательности, создаваемой по тому или иному методу генерации случайных чисел. Поэтому для окончания работы программы ее необходимо завершить принудительно (при работе в Windows для этого можно просто закрыть окно DOS). В результате работы программы будет создан файл p.txt, который можно открыть в любом редакторе, допускающем просмотр файлов в шестнадцатеричном виде (например, встроенном редакторе файлового менеджера Windows Commander).
Для увеличения диапазона необходимо внести соответствующие, не принципиальные с точки зрения алгоритма, изменения в тексты программ.

Мультипликативный конгруэнтный метод генерации последовательности случайных чисел

Мультипликативный конгруэнтный метод задает последовательность неотрицательных целых чисел Xj (Xj<m), получаемых по формуле:

Хn+1=аХn(mod m). (2)

На значения накладываются ограничения:

  • Хо — нечетно;
  • а=52р+1 (р=0, 1, 2, ...) или a=2m+3 (m=3, 4, 5, ...) — обе эти записи означают, что младшая цифра а при представлении а в восьмеричной системе счисления должна быть равна 3 или 5 (проще говоря, остаток от деления а/8 должен быть равен 3 или 5);
  • m=2 (1>4).

При соблюдении этих ограничений, длина периода будет равна m/4.

:randjnult_cong_l.asm - датчик линейной (мультипликативной) :конгруэнтной последовательности случайных чисел (с=0).
:Вход: Хо. a, m - в соответствии с указанными выше ограничениями. .-Выход: dl - значение очередного случайного числа.

.data
in db 128
a db 11
х db 3 начальное значение
.code
;первое число в последовательности х-3
cycl: moval.x вычисляем очередное случайное число Х=(а*Х) mod in
mul а :а*х в ah:al
divm ;в ah случайное число
mov x.ah ;вывод в файл - командная строка rand_mu1t_cong.exe > p.txt
mov dl .ah
mov ah.02
nit г»
jmp cyc1
end cycl:

Если m является степенью 2, как в данном случае, то вместо команды DIV мож-, но использовать две команды сдвига.

;----------------------------------------------------------
;rand_mult_cong_2.asm - датчик линейной (мультипликативной) конгруэнтной последовательности
;случайных чисел (c=0).
;Вход: X0, a, m - в соответствие с указанными выше ограничениями.
;Выход: dl - значение очередного случайного числа.
;----------------------------------------------------------
masm
model small
.486
.data
m db 128 ;128=27
a db 11
x db 3 ;начальное значение
.stack 256
.486
.code
main:
mov dx,@data
mov ds,dx
xor dx,dx
mov cl,7 ;значение степени m=27 в cl
;первое число в последовательности x=3
cycl:
;вычисляем очередное случайное число X=(a*X) mod m
mov al,x
mul a ;a*x в ah:al
shrd ax,ax,cl
xor al,al
rol ax,cl ;в al случайное число
;вывод в файл - командная строка rand_mult_cong.exe > p.txt
mov x,al
mov dl,al
mov ah,02
int 21h
jmp cycl
end_cycl:
mov ax,4c00h
int 21h
end main

Используя эти программы, можно получить последовательность случайных чисел, содержащую 32 значения — это ее период. Чтобы увеличить период, необходимо каким-либо способом сгенерировать значения а или х, удовлетворяющие приведенным выше ограничениям. Так, значение а можно вычислить, используя фрагмент:

.data
:.........
divider db 8

.code
вычисляем а исходя из соотношения:
:а mod 8=5
:одним из способов получить значение а (т > а)
удовлетворяем условию a mod 8 = 5
m2: mov al .a
xor ah,ah
div divider
cmp ah,5 :остаток 5?
je ml
cmp ah,3 :остаток 3?
je ml
inc a
jmp m2 ml: ;теперь а найдено до конца

Изменить (увеличить) период можно, корректируя значение т, для чего необходимо будет исправить соответствующие команды в программах rand_mult_ cong_l.asm и rand_mult_cong_2.asm, ориентированные на определенную разрядность регистров. Существует другая возможность увеличения периода — использование смешанного конгруэ}1т)юго метода генерации последовательности случайных чисел.

Смешанный конгруэнтный метод генерации последовательности случайных чисел

Соотношение смешанного конгруэнтного метода выглядит так: Xn+1=(aXn+c) mod m, где n > 0.
При правильном подборе начальных значений элементов кроме увеличения периода последовательности случайных чисел уменьшается корреляция (зависимость) получаемых случайных чисел.

На значения накладываются ограничения:

  • Х0>0;
  • а=21+1, где 1>=2;
  • с>0 взаимно просто с m (это выполнимо, если с — нечетно, а т=2р, где (р>=2)
  • m=2р (р>=2) и т кратно 4.

:rand_mix_cong_l.asm - датчик линейной (смешанной)
:конгруэнтной последовательности случайных чисел (с>0).
:Вход: Хо. а. с. m - в соответствии с указанными выше
ограничениями.
:Выход: dl - значение очередного случайного числа.
.data
m db 128 ; 128=27
a db 9
х db 3 начальное значение
с dw 3
.code
mov cl.7 :значение степени m=27 в cl ;первое число в последовательности х=3 cycl: вычисляем очередное случайное число Х=(а*Х) mod m
mov al.x
mul a :a*x в ah:al
add ax,с
shrd ax.ax.cl
xor al.al
rol ax.cl :b al случайное число :вывод в файл - командная строка rand_mult_cong.exe > p.txt
end_cycl:

Величина периода случайной последовательности, получаемой с помощью данной программы, составляет 128 значений. Сегмент кода программы rand_mix_ cong_1.asm можно оформить в виде процедуры. Начальное значение Хо можно выбирать двумя способами: задавать константой в программе или генерировать случайным образом. В последнем случае можно использовать такты системного таймера, как в следующей макрокоманде:

rCMOS macro
макрокоманда чтения значений CMOS
:на входе: al адрес ячейки, значение которой читаем
:на входе-выходе: al - прочтенное значение
out 70h,al
хог ах,ах :вводим в регистр AL из порта значение ячейки cmos
in al.71h
endm .code
:получить значение секунд из CMOS для x_start mov al.00 rCMOS mov x.al :x=x_start

Таким способом можно получить начальное значение из диапазона 0..59. Для получения большего по величине начального значения можно использовать величину размером 32 бита из области данных BIOS по адресу 0040:006с. Здесь содержится счетчик прерываний от таймера. Извлечь это значение можно, используя следующий программный фрагмент:

push ds
push 40h
pop ds
mov eax.dword ptr ds:006ch
popds

Заменяя команду MOV командами MOV AX,word ptr ds:006ch или MOV AL, byte ptr ds:006ch, можно использовать младшие 8 или 16 бит значения из этой области BIOS. Команда MOV AL, byte ptr ds:006ch позволяет случайным образом получить в регистре AL значение из диапазона 00.. f fh.
Попытки создать программный датчик случайных чисел без опоры на какую-либо теорию обречены на провал. Рассмотрим еще несколько способов генерации случайных чисел.

Аддитивный генератор случайных чисел

Генератор, формирующий очередное случайное число в соответствии с отношением (3), называется аддитивным:

Xn+1=(Xn+Xn-k) mod m. (3)

В трехтомнике Кнута [5] обсуждаются подобные генераторы и рекомендован следующий вариант формулы (3):

Xn+1=(Xn-24+Xn-55) mod m.(4)

Здесь n > 55, m=21, Хо, ..., Х54 — произвольные числа, среди которых есть и нечетные. При этих условиях длина периода последовательности равна 21-1 (255-1).
Для генерации первых 55 случайных чисел можно использовать один из рассмотренных выше генераторов. Возьмем датчик линейной (смешанной) конгруэнтной последовательности случайных чисел (с > 0).

;rand_add.asm - аддитивный генератор случайных чисел.
:Вход: :Х0. а. с. m ;
случайная последовательность длиной 55 значений, получаемая с помощью
программы генерации высокослучайных двоичных наборов (см. rand_mix_cong_1.asm); :N=700 - количество элементов в последовательности + 1; :L=7 - значение степени т=27. ¦.Выход: dl - значение очередного случайного числа.
.data
N=700 количество элементов в последовательности + 1
L=7 :L - значение степени т=2
т db 128 ;128=27
mm dw 256 ; 256=28
a db 9
с dw 3
;массив значений х (начальное значение х=3) длиной N+1
х db 3, N dup (Offh)
;.........
.code
:далее фрагменты из rand_mix_cong_l.asm.
хог si,si
mov ecx.54 :счетчик цикла для формирования начальной последовательности
:первое число в последовательности х=3
mov al ,x
cycl: вычисляем очередное случайное число Х=(а*Х) mod m
mul a ;а*х в ah:al
add ax.с
shrd ax.ax.L:L - значение степени т=27
хог al.al
rol ax.L ;в al случайное число
:вывод в массив х и файл - командная строка rand_mult_cong.exe > p.txt
i nc si
mov x[si].al
mov dl.al
mov ah. 02
int 21h
loop cycl
:далее продолжаем формирование случайной последовательности, но уже аддитивным методом
mov ecx,N-55
cycl 2: incsi
хог dx.dx
mov al.x[si-24]
mov dl.x[si-55]
add ax.dx
хог dx.dx
div mm
mov x[si].dl
mov ah.02
int 21h
loop eye 12
exit:

Судя по результатам, этот метод достаточно хорош. В частности, он неплох тем, что позволяет задавать длину последовательности без оглядки на значение т.
Следующий рассматриваемый нами датчик можно использовать в программах на языке ассемблера для получения случайной последовательности 0 и 1.

Программа генерации высокослучайных двоичных наборов

Для процесса генерации требуются два значения одинаковой размерности — Y и С. Значение Y будет первым в последовательности случайных чисел. Значение С играет роль маски, в соответствии с которой впоследствии будет производиться операция «исключающее ИЛИ». Генерация очередного случайного числа происходит в два этапа. На первом этапе значение Y сдвигается влево на один разряд. При этом нас интересует содержимое выдвинутого бита. Его значением устанавливается флаг eflags.cf. На втором этапе, если cf=1, то корректируем значение Y= =Y XOR С и сохраняем Y; в противном случае сохраняем сдвинутое значение в качестве очередного числа последовательности.

:rand_bin_1.asm - программа генерации высокослучайных двоичных наборов
:(сокращенный вариант).
:Вход: у. с - в соответствии с указанными выше ограничениями.
:Выход: dl - значение очередного случайного числа.
.data
Y db 35h : Obh
С db 33h :03h
. code
cycl: shl Y.I
jnc ml
mov al ,Y
xor al ,C
mov Y.al ml: :вывод на экран (в файл - командная строка rand_bih.exe > p.txt)
jmp cycl end_cycl:

Содержимое файла, в который перенаправлен вывод программы, показывает, что период последовательности достаточно удовлетворительный (при удачном подборе Y и С). Для того чтобы получить случайную последовательность 0 и 1, необходимо на каждой итерации выделять младший или старший бит очередного значения. Доработаем программу rand_bin_1.asm, чтобы продемонстрировать этот прием.

:rand_bin_2.asm - программа генерации высокослучайных двоичных наборов (полный вариант).
:Вход: у. с - в соответствии с указанными выше ограничениями.
;Выход: dl - значение очередного случайного числа.
.data
Y db 35h :0bh
С db 33h ;03h
.code
: получаем очередное значение Y
push ds
push 40h
pop ds
mov eax.dword ptr ds:006ch
pop ds
mov Y.al
xor dl.dl
mov ecx,8 Нормируем случайные 8-ми битовые наборы в регистре DL cyct: shl Y.I
jnc ml
mov a 1. Y
xor al.C
mov Y.al
jmp $+5 ;на shrd
ml: mov al ,Y
shr al.l
rcl dl.l
loop cycl
:вывод на экран (в файл - командная строка rand_bin.exe > p.txt) очередного значения
exit:

Этот датчик хорош, когда его используют для получения одиночных случайных значений, а не всей последовательности сразу. Поэтому в этой программе отсутствует цикл, характерный для предыдущих программных датчиков, и ее удобно оформить в виде процедуры или макрокоманды.
В заключение данной темы — высказывание Джорджа Марсальи, которое приводит Кнут : «Генератор случайных чисел во многом подобен сексу: когда он хорош — это прекрасно, когда он плох, все равно приятно». Возможно, экспериментируя с примерами данного раздела, вы испытали подобное чувство.
Оценка качества последовательности производится по определенным критериям, подробное рассмотрение которых не является предметом данной книги, но все же требует упоминания. В большинстве случаев для быстрой оценки качества работы генератора случайной последовательности достаточно использовать следующие критерии.

  • Длина периода и длина апериодичности. Под длиной периода понимается длина максимальной, не содержащей самой себя, числовой цепочки в последовательности случайных чисел. Длина апериодичности — длина подинтервала последовательности случайных чисел, в пределах которого не встречаются одинаковые значения.
  • Равномерность последовательности псевдослучайных чисел. Этот критерий означает вероятность попадания значений, генерируемых датчиком случайных чисел, в каждый из m подинтервалов, на которые можно разбить весь интервал значений, генерируемых данным датчиком случайных чисел.
  • Стохастичность последовательности псевдослучайных чисел. Этот критерий означает определение вероятности появления единиц (нулей) в определенных п разрядах генерируемых датчиком случайных чисел.
  • Независимость элементов псевдослучайной последовательности. Этот критерий определяет степень корреляции (зависимости) двух случайных величин в сгенерированной последовательности. При проведении оценки по этому критерию можно рассматривать любые, а не только рядом стоящие случайные величины последовательности.

Более подробную информацию о подходах к оценке случайных последовательностей можно получить в литературе.


Основные понятия

Продавай мобильный контент со своего сайта

Сложные структуры данных (Часть 1)

  • Основные понятия
    • Способы распределения памяти
  • Множество
  • Массив
    • Описание массивов
    • Работа с массивами
  • Структура
    • Вложенные структуры
    • Массивы структур — таблицы

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

Основные понятия

Процесс разработки программы на ассемблере традиционно осложняется тем, что в этом языке ограничены средства описания данных, привычные для языков программирования высокого уровня. В уроке 12 «Сложные структуры данных» учебника были рассмотрены средства, которые поддерживает ассемблер для работы с данными. Но это деление весьма условно и не дает представления о том, как реализуется общая концепция понятий «данное», «тип данных» и «структура данных» в контексте программирования на языке ассемблера. Это обстоятельство существенно влияет на эффективность изучения и использования языка ассемблера. Учитывая сложность и практическую важность данного вопроса, есть смысл изложить его более систематично.
Проблема представления и организации эффективной работы с данными возникла одновременно с идеей разработки первой вычислительной машины. Вычислительная машина функционирует согласно некоторому алгоритму. А если есть алгоритм, то должны быть и данные, с которыми он работает. Аналогично извечной дилемме о курице или яйце возникает вопрос о первичности алгоритма и данных. Схожий вопрос неявно заложен в название небезызвестной книги Никлауса Вирта — «Алгоритмы + структуры данных = программы», два издания которой были выпущены издательством «Мир» в 1985 и 1989 годах.
Что же представляют собой понятия «данное», «тип данного», «структура данных»? Попытаемся привести некую классификационную характеристику этих понятий.
Данное — набор байт, рассматриваемый безотносительно к заложенному в них смыслу. Понятие «обработка данных» характерно для процессора как исполнительного устройства. При этом «данное» рассматривается как совокупность двоичных разрядов, которыми манипулирует определенная машинная команда. Для человека подобную интерпретацию вряд ли можно считать удобной. Для него более естественной является логическая интерпретация данных, которая базируется на понятии «типа данных».

Тип данных можно определить множеством значений данного и набором операций над этими значениями. В учебнике с точки зрения типа данные были разделены на две группы — простые и сложные. Данными простого типа считаются элементарные, неструктурированные данные, которые могут быть описаны с помощью одной из директив резервирования и инициализации памяти. Примером таких данных являются целые и вещественные числа различной размерности. В языках высокого уровня в качестве простых данных используются еще и данные символьного, логического, указательного типов. Данные простого типа называют упорядоченными (или скалярными), так как теоретически можно перечислить все значения, которые они могут принять.
Отличительная особенность данных простого типа — их неструктурированность. В контексте конкретной задачи исходя из смысла и логики обработки между некоторыми простыми данными могут существовать определенные отношения И связи, что позволяет рассматривать их как определенным образом организованные совокупности. Обобщенное название таких совокупностей — структуры данных. В общем случае отдельная структура данных может содержать не только простые данные, но и другие структуры данных.
С известной долей условности можно сказать, что тип данного и структура данных — понятия, не зависимые от компьютерной платформы. Вполне можно абстрагироваться от представления в программе данных простого или сложного типа и проводить теоретические рассуждения относительно действий с целыми и вещественными числами, массивами, списками, деревьями и т. д. Поэтому такие структуры данных называют структурами данных уровня представления, абстрактными, или логическими, структурами. Для машинной обработки абстрактные типы и структуры данных необходимо некоторым способом представить в оперативной памяти, то есть в виде физической структуры данных {структуры хранения), которая, в свою очередь, отражает способ физического представления абстрактных данных на конкретной программно-аппаратной платформе. В качестве примера можно привести двумерный массив. Для программиста, пишущего программу для обработки этого массива, он видится в виде матрицы, содержащей определенное количество строк и столбцов. В оперативной памяти данный массив представляется в виде линейной последовательности ячеек. В данном случае логическая структура данных — двумерный массив, а соответствующая ему физическая структура — вектор (см. ниже). Таким образом, в большинстве случаев существует различие между логическими и соответствующими им физическими структурами данных. Задачей программиста фактически является написание кода, осуществляющего отображение логической структуры на физическую и наоборот. Этот код реализует различные операции логического уровня над структурой данных. Так, на логическом уровне доступ к элементу двумерного массива осуществляется указанием номеров столбца и строки в матрице, в которой расположен данный элемент. На физическом уровне эта операция выглядит по-другому. Для доступа^ к определенному элементу массива программный код по известному номеру строки и столбца вычисляет смещение от начального адреса массива в памяти. Исходя из вышеприведенных рассуждений конкретную структуру данных можно характеризовать ее логическим (абстрактным) и физическим (конкретным) представлениями, а также совокупностью операций на этих уровнях.
Язык ассемблера — язык уровня архитектуры конкретного компьютера. Память компьютеров с архитектурой Intel представляет собой упорядоченный набор непосредственно адресуемых машинных ячеек (байтов). Исходя из этого номенклатура структур хранения данных архитектурно ограничена следующим набором: скаляр, вектор, список, сеть.

  • Скаляр — поле, содержащее одиночное двоичное значение, размерностью один или несколько байтов. Количество байтов, составляющих скаляр, определяется допустимыми размерами операндов системы команд конкретного процессора.
  • Вектор — конечное упорядоченное множество расположенных рядом скаляров одного типа, называемых элементами вектора. По сути дела вектор — это одномерный массив. Что у них общего? Геометрически вектор представляет собой состоящий из точек объект в пространстве, имеющий начальную точку, из которой он выходит, и конечную точку, в которую он приходит. Точки, лежащие в пространстве между начальной и конечной точками (элементы вектора), находятся между собой в единственно возможном отношении — отношении непосредственного следования. Такая строгая упорядоченность элементов вектора позволяет произвести их последовательную нумерацию. Аналогично и одномерный массив имеет началом и концом скаляры, расположенные по определенным адресам памяти. Между этими адресами последовательно расположены скаляры, составляющие элементы массива. Определенность с начальным и конечным адресами массива, а также с размерностью его элементов дает возможность однозначно идентифицировать любой его элемент.
  • Список — набор элементов, каждый из которых состоит из двух полей. Одно поле содержит элемент данных или указатель на элемент данных, другое -указатель на следующий элемент списка, который, в свою очередь, тоже может быть начальным или промежуточным элементом другого списка. Наличие явного указания на упорядоченность элементов списка позволяет достаточно легко манипулировать содержимым списка, включая новые и исключая старые элементы списка без их фактического перемещения в памяти. Это свойство позволяет размещать в памяти динамически изменяющиеся структуры данных.
  • Сеть — набор элементов, каждый из которых помимо информационного поля содержит несколько полей-указателей на другие элементы сети. С помощью сети удобно представлять такие структуры данных уровня представления, как деревья, ориентированные графы и т. п.

Как мы увидим ниже, эти четыре структуры хранения дают возможность представить в памяти машины практически все известные структуры уровня представления: массивы, записи, таблицы, стеки, Точереди, деки, списки различной степени связности, деревья, ориентированные и неориентированные графы. Эти структуры можно классифицировать по следующим признакам.

  • Связность, то есть отсутствие или наличие явно заданных связей между элементами структуры. К несвязным структурам уровня представления относятся массивы, символьные строки, стеки, очереди. К связным структурам уровня представления относятся связные списки.
  • Изменчивость, то есть изменение числа элементов и (или) связей между элементами структуры. По этому признаку структуры делятся на статические (массивы, таблицы), полу статические (стеки, очереди, деки) и динамические (одно-, двух- и многосвязные списки).
  • Упорядоченность — линейные (массивы, символьные строки, стеки, очереди, одно- и двусвязные списки) и нелинейные (многосвязные списки, древовидные и графовые структуры).

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

Способы распределения памяти

Прежде чем приступить к рассмотрению того, каким образом поддерживается работа с различными структурами данных на ассемблере, необходимо определиться с тем, как будет решаться проблема распределения памяти. Это важный момент, так как фон-неймановская архитектура современных машин предполагает, что любые данные, обрабатываемые программой, располагаются в памяти. Традиционно в любом языке программирования высокого уровня существуют два способа распределения памяти для переменных и постоянных данных — статический и динамический. Соответственно данные будем называть статическими и динамическими объектами данных. В ассемблере в силу его специфики явно присутствует возможность только статического распределения памяти с помощью директив резервирования и инициализации памяти в сегменте данных. Статические объекты данных занимают отведенную им память на все время работы программы. До сих пор при написании ассемблерных программ мы использовали именно статическое распределение памяти. Недостаток этого типа памяти очевиден — если объект данных занимает большую область памяти и имеет малый период жизни, то выделять память на все время работы программы неразумно. Гораздо эффективнее в таких случаях иметь возможность для временного выделения памяти объекту данных на время его жизни. В этом разделе мы уделим все внимание проблеме динамического распределения памяти в программах на ассемблере.
Динамическими объектами данных называются объекты данных, обладающие двумя особенностями:

  • распределение/освобождение памяти для объекта данных производится по явному запросу программы;
  • доступ к динамическим объектам данных производится не по их именам, а посредством указателей, содержащих ссылку (адрес) на созданный динамический объект.

Существует и третья особенность, о которой, возможно, не подозревают программирующие на языках высокого уровня, — технология выделения памяти для динамических объектов данных. Эта технология напрямую зависит от операционной среды, для которой разрабатывается программа. Так, в MS DOS динамическое выделение памяти во время работы приложения осуществляется с помощью двух функций прерывания int 21h-48h (выделение блока памяти) и 49h (освобождение блока памяти). Единицей выделения памяти при этом является параграф (16-байтный блок памяти). С точки зрения современного программирования — это примитивно и не очень интересно. Гораздо большими возможностями обладает наиболее распространенная в настоящее время операционная система Windows.
В Windows существует несколько механизмов динамического выделения памяти:

  • виртуальная память;
  • кучи;
  • отображаемые в память файлы.

Пример использования последнего из перечисленных механизмов был приведен в уроке 20 «ММХ-технология микропроцессоров Intel» учебника. Также отображаемые в память файлы рассматриваются в главе 7 этой книги, посвященной работе с файлами. Для нашего изложения представляют интерес, хотя и в разной степени, первые два механизма.

Механизм виртуальной памяти Windows

Механизм виртуальной памяти Windows реализуется с помощью функций API Win32 VirtualAlloc и VirtualFree.

LPVOID VirtuaUn ос (LPVOID ipAddress. SIZEJ dwSize. DWORD flAllocationType. DWORD
flProtect) BOOL VirtualFreeCLPVOID IpAddress. SIZEJ dwSize, DWORD dwFreeType)

С помощью функции Virtual All ос приложение запрашивает в свое распоряжение область памяти (регион) в адресном пространстве с размером, указываемым параметром dwSize. Величина dwSize должна быть кратна 64 Кбайт, что, соответственно, является минимальным размером региона. Динамическое выделение памяти такими большими порциями может требоваться лишь для работы с большими массивами данных (структурами данных). Для нашего изложения этот способ выделения данных не подходит. Отметим лишь, что работа функции VirtualAlloc имеет следующую особенность. Имеются три варианта обращения к функции VirtualAlloc: резервирование региона в адресном пространстве процесса; выделение физической памяти в зарезервированном предыдущим вызовом функции VirtualAlloc регионе; резервирование региона с одновременной передачей ему физической памяти. Освобождение региона производится функцией VirtualFree. Более подробную информацию о параметрах функций VirtualAlloc и VirtualFree можно посмотреть в MSDN.

Механизм работы с кучами Windows

Этот механизм наиболее эффективен для поддержки работы с такими структурами данных, как связные списки, деревья и т. п. Как правило, отдельные элементы этих структур имеют небольшой размер, в то время как общее количество памяти, занимаемое этими структурами в разные моменты времени работы приложения, может быть разным. Главное преимущество использования кучи — свобода в определении размера выделяемой памяти. В то же время это самый медленный механизм динамического выделения памяти.
Windows поддерживает работу с двумя видами куч: стандартной и дополнительной.
Во время создания система выделяет процессу стандартную кучу (или кучу по умолчанию), размер которой составляет 1 Мбайт. При желании можно указать компоновщику ключ /HEAP с новой величиной размера стандартной кучи. Создание и уничтожение стандартной кучи производится системой, поэтому в API не существует функций, управляющих этим процессом. Только одна функция должна вызываться перед началом работы со стандартной кучей — GetProcessHeap:

HANDLE GetProcessHeap(VOID)

GetProcessHeap возвращает описатель, используемый далее другими функциями работы с кучей.
Для более эффективного управления памятью и локализации структур хранения в адресном пространстве процесса можно создавать дополнительные кучи. Сделать это можно с использованием функции HeapCreate:

HANDLE HeapCreate(DWORD flOptions. SIZE_T dwInitialSize, SIZE_T dwMaximutnSize)

Размер создаваемой этой функцией кучи определяется параметрами dwInitialSize (начальный размер) и dwMaximumSize (максимальный размер). Возвращаемое функцией HeapCreate значение — описатель кучи, который используется затем другими функциями, работающими с данной кучей. Уничтожение дополнительной кучи осуществляется вызовом функции HeapDestroy, которой в качестве параметра передается описатель уничтожаемой кучи:

BOOL HeapDestroytHANDLE hHeap)

Важно отметить, что на этапе создания как стандартной, так и дополнительных куч реального выделения памяти для них не производится. Главное — получить указатель и сообщить системе характеристики и размер создаваемой кучи.
После получения описателя работа со стандартной и дополнительной кучами осуществляется с помощью функций НеарАПос, HeapReAlloc, HeapSize, HeapFree. Рассмотрим их подробнее.
Выделение физической памяти из кучи производится по запросу НеарАПос:
LPVOID HeapAlloc(HANDLE hHeap. DWORD dwFlags. SIZEJ dwBytes)
Здесь параметр hHeap сообщает НеарАПос, в пространстве какой кучи требуется выделение памяти размером dwBytes байт. Параметр dwFlags представляет собой флаги, с помощью которых можно влиять на особенности выделяемой памяти. В случае успеха функция НеарАПос возвращает адрес, который используется далее для доступа к физической памяти в выделенном блоке.
В ходе работы с выделенным блоком может сложиться ситуация, когда необходимо изменить его размер в большую или меньшую сторону. Для этого предназначена функция HeapReAHoc:

LPVOID HeapReA11oc(HANDLE hHeap. DWORD dwFlags, LPVOID ipMem, SIZE_T dwByt

Параметр hHeap идентифицирует кучу, в которой изменяется размер блока, а параметр lpMem является адресом блока (полученным ранее с помощью НеарАПос), размер которого изменяется. Новый размер блока указывается параметром dwBytes.
Играя размерами блоков, вы можете совсем запутаться. Функция HeapSize поможет вам определить текущий размер блока по адресу 1 рМет в куче hHeap.

DWORD HeapSize(HANDLE hHeap. DWORD dwFlags, LPCVOIO lpMem)

И наконец, когда блок по адресу lpMem в куче hHeap становится ненужным, его можно освободить вызовом функции HeapFree:

BOOL HeapFreeCHANDLE hHeap. DWORD dwFlags. LPVOID lpMem)

Это минимальный набор функций Windows для работы с кучами. Он столь подробно был обсужден нами с целью дальнейшего использования этих функций для разработки приложений этого раздела.

Множество

Соня закрыла глаза и задремала. Но тут Болванщик
ее ущипнул, она взвизгнула и проснулась.
—...начинается на М, — продолжала она. — Они
рисовали мышеловки, математику, множество...
Ты когда-нибудь видела, как рисуют множество?
— Множество чего? - спросила Алиса.
— Ничего, — отвечала Соня. — Просто множество!
— Не знаю, — начала Алиса, — может...
— А не знаешь — молчи, — оборвал ее Болванщик.
Льюис Кэрролл. «Алиса в стране чудес»

Множество как структура уровня представления является совокупностью различных объектов, которые могут либо сами являться множествами, либо представлять собой неделимые элементы, называемые атомами.
Множество как структура уровня хранения реализуется двумя способами:
ш простым — в виде данных перечислимого типа; в языках высокого уровня этот тип данных реализуют с помощью типа «множество» (в Pascal) или констант перечислимого типа (в С);
ш сложным — в виде вектора или связного списка.
Отличие этих двух способов в том, что данные перечислимого типа ограниченно поддерживают понятие множества, представляя собой совокупность объектов, которым сопоставлены некоторые значения. При реализации множеств с помощью вектора или связного списка становится возможным реализовать именно математическое понятие множества, согласно которому, над множествами определен ряд операций: объединение, пересечение, разность, слияние, вставка (удаление) элемента и т. д. С данными перечислимого типа эти операции невозможны.
В языке ассемблер также есть средства, позволяющие реализовать оба способа представления множеств. Так, описание данных перечислимого типа поддерживаются с помощью директивы enum. Как и в языках высокого уровня, данные перечислимого типа, вводимые этой директивой, являются константами, которым соответствуют уникальные символические имена. Директива enum имеет следующий синтаксис:

символ_имя enum значение[. значение[, значение....]]

Здесь значение представляет собой конструкцию символ_имя [=число], а символ_ имя — любое символическое имя, не совпадающее с ключевыми словами ассемблера или другими ранее определенными в программе символическими именами. Следующие примеры описания множеств эквивалентны.

week enum Monday.Tuesday,Wednesday,Thursday,Friday,Saturday.Sunday > week enum Monday=0,Tuesday=l. Wednesday^. Thursday=3. Fri day=4.Saturday=5.Sunday=6 week enum Monday=0,Tuesday.Wednesday,Thursday.Fri day.Saturday.Sunday week enum Sunday=6.Monday=0,Tuesday,Wednesday.Thursday,Fri day.Saturday

Перечисление элементов множества может занимать несколько строк в программе.
Транслятор будет отводить под размещение каждого элемента множества столько байтов, сколько требуется для размещения значения самого большого элемента в этом множестве (байт, слово, двойное слово).
Если при описании очередного элемента множества число в некоторой конструкции символ_имя [=число] не задано, то транслятор присвоит этому элементу множества значение, на единицу большее предыдущего. Если ни одного значения не было задано, то первому элементу множества присваивается 0, второму 1 и т. д.
Работа с элементами множества в программе является завуалированной формой использования непосредственного операнда. В этом несложно убедиться, проанализировав листинг трансляции:

5 :задаем множество:
6 =0000 =0001 =0002 + week enum
Monday.Tuesday,Wednesday.Thursday.Fri day.Saturday.Sunday
7 =0003 =0004 =0005 +
8 =0006
20 0005 B8 0006 mov ax,Sunday

Значение символического имени синвол_имя, стоящего слева от директивы enum, равно количеству битов, необходимому для размещения максимального значения элемента справа от директивы enum:

5 ;задаем множество:
6 =0000 =0001 =0002 + week enum
Monday.Tuesday.Wednesday.Thursday,Fri day.Saturday.Sunday
7 =0003 =0004 =0005 +
8 =0006
21 0005 B8 0007 mov ax.week

Перед использованием в программе необходимо определить и инициализировать экземпляр множества:

F_week week Saturday

Размер этой переменной будет соответствовать размеру максимального элемента множества week. Сказанное иллюстрирует фрагмент листинга:

5 :задаем множество:
6 =0000 =0001 =0002 + week enum
Monday.Tuesday,Wednesday.Thursday.Friday.Saturday.Sunday
7 =0003 =0004 =0005 +
8 =0006
9 0000 05 s week Saturday
21 0005 A2 OOOOr movs.al

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

Массив

Все истинно великое совершается медленным,
незаметным ростом.
Сенека

Описание массивов

Как структура представления массив является упорядоченным множеством элементов определенного типа. Упорядоченность массива определяется набором целых чисел, называемых индексами, которые связываются с каждым элементом массива и однозначно определяют его расположение среди других элементов массива. Локализация конкретного элемента массива — ключевая задача при разработке любых алгоритмов, работающих с массивами. Ее сложность прямо зависит от размерности массива.
Наиболее просто представляются одномерные массивы. Соответствующая им структура хранения — это вектор. Она однозначна и представляет собой просто последовательное расположение элементов в памяти. Чтобы локализовать нужный элемент одномерного массива, достаточно знать его индекс. Так как ассемблер не имеет средств для работы с массивом как структурой данных, то для использования элемента массива необходимо вычислить его адрес. Для вычисления адреса i-ro элемента одномерного массива можно использовать формулу:

Аi=АБ+i*lеn

Здесь АБ — адрес первого элемента массива размерностью n, i — индекс (i=0.. n-1), len — размер элемента массива в байтах. Заметьте, что при таком определении можно не говорить о типе элементов массива. В общем случае они также могут быть структурированными объектами данных.
Представление двумерных массивов немного сложнее. Здесь мы имеем случай, когда структуры хранения и представления различны. О структуре представления говорить излишне — это матрица. Структура хранения остается прежней — вектор. Но теперь его нельзя без специальных оговорок интерпретировать однозначно. Все зависит от того, как решил разработчик программы «вытянуть» массив — по строкам или по столбцам. Наиболее естествен порядок расположения элементов массива — по строкам. При этом наиболее быстро изменяется последний элемент индекса. К примеру, рассмотрим представление на логическом уровне двумерного массива Ац размерностью nxm, где 0 < i < n—I, 0 < j < m-1:

а00 а01 а02 а03
а10 а11 а12 а13
а20 а21 а22 а23
а30 а31 а32 а33

Соответствующее этому массиву физическое представление в памяти — вектор — будет выглядеть так:

а00 а01 а02 а03а10 а11 а12 а13а20 а21 а22 а23а30 а31 а32 а33

Номер конкретного элемента массива в этой, уже как бы ставшей линейной, последовательности определяется адресной функцией, которая устанавливает положение (адрес) в памяти этого элемента исходя из значения его индексов:

aij=n*i + j.

Для получения адреса элемента массива в памяти необходимо полученное значение умножить на размер элемента и сложить с базовым адресом массива.
Аналогично осуществляется локализация элементов в массивах большей размерности. На рис. 2.1 показан трехмерный массив Aijz размерностью n x m x к, где n = 4, m = 4, к = 2.

Рис. 2.1. Пример логической структуры трехмерного массива

Cоответствующий этому массиву вектор памяти будет выглядеть так:а120

а000 а001 а010а011а020 а021 а030 а031а100 а101 а111 а120.. а331

Соответственно номер элемента определяется так:

aijz=n*m*i + m*j + z, где 0 < i < n-1, 0 < j < m-1, 0 < z < k-1.

Для получения адреса осталось умножить полученное значение на размер элемента массива и сложить результат с базовым адресом массива.
Таким образом, преобразование многомерной, в общем случае логической структуры массива в одномерную физическую структуру производится путем ее линеаризации по строкам или столбцам. В первом случае, быстрее всего изменяется последний индекс каждого элемента, во втором — первый индекс. Недостаток описанного способа локализации элемента массива в том, что процесс вычисления адреса подразумевает выполнение операций сложения и умножения. Как известно, они не являются быстрыми. Существует метод Дж. Айлиффа, с помощью которого можно исключить операцию умножения из процесса вычисления индекса. Индексация в массиве производится с помощью иерархии дескрипторов, называемых векторами Айлиффа, так как это показано на рис. 2.2. На этом рисунке j,, )ь j3 обозначают соответственно первый, второй и третий индексы массива.

Рис. 2.2. Представление предыдущего трехмерного массива по методу Айлиффа

Из рисунка видно, что основной дескриптор массива содержит указатель на вектор Айлиффа первого уровня. Вектор первого уровня является единственным и содержит указатели векторов Айлиффа следующего уровня. То есть, иерархия дескрипторов однозначно отражает размерность массива. Локализовать нужный элемент массива можно, руководствуясь его индексом логического уровня. Для этого нужно пройти по цепочке от основного дескриптора через соответствующие элементы векторов Айлиффа, извлекая из них и суммируя значения смещений с базовым адресом массива:
Здесь (D) — значение указателя вектора Айлиффа первого уровня, хранящееся в основном дескрипторе. При необходимости никто не мешает вам разместить в элементах векторов Айлиффа и другую служебную информацию, например диапазон изменения соответствующего индекса. Очевидный недостаток метода Айлиффа — увеличение общего объема памяти для структуры хранения многомерного массива в памяти.
Что касается описания массива в программе на ассемблере, то его варианты были подробно рассмотрены в учебнике. Перечислим их очень конспективно:

  • перечисление элементов массива в поле операндов одной из директив описания данных;
  • резервирование памяти для элементов массива с использованием оператора повторения dup (элементы массива формируются динамически в ходе выполнения программы);
  • использование директив label и rept.

После рассмотрения возможностей по динамическому выделению памяти в операционных средах MS DOS и Windows можно к перечисленным трем вариантам добавить еще один — динамическое распределение памяти. Применительно к программированию для Windows возможности здесь очень широкие и можно использовать наиболее подходящий способ из трех, обсуждавшихся в разделе «Способы распределения памяти».
Массив в разное время работы с ним может изменять свои характеристики — имя массива, адрес в памяти первого его элемента, количество и диапазон изменения индексов, тип элементов, размерность и т. п. В этом случае имеет смысл его физическое представление в памяти предварять специальным заголовком, или дескриптором, в котором указывать характеристики массива, чтобы программа могла правильно обрабатывать элементы массива. Особенно это важно, когда память для массива выделяется динамически.

Работа с массивами

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

Сортировка массивов

Под сортировкой понимается процесс перестановки элементов некоторого множества в порядке убывания или возрастания. Если элементы множества — скаляры, то сортировка ведется относительно их значений. Если элементы множества — структуры, то каждая структура должна иметь поле, относительно которого будет производиться упорядочивание местоположения элементов (то есть сортировка) относительно других элементов-структур множества.
Различают два типа алгоритмов сортировки: сортировку массивов и сортировку файлов. Другое их название — алгоритмы внутренней и внешней сортировки. Разница заключается в местонахождении объектов сортировки: для внутренней сортировки — это оперативная память компьютера, для внешней — файл. В данном разделе будут рассмотрены алгоритмы внутренней сортировки массивов. Алгоритмы внешней сортировки будут рассмотрены в разделе, посвященном работе с файлами.
Существует несколько алгоритмов сортировки массивов, которым следует уделить внимание в контексте проблемы изучения ассемблера. По критерию эффективности алгоритмы сортировки массивов делят на простые и улучшенные. Среди простых методов сортировки, которые также называют сортировками «на том же месте», мы рассмотрим следующие:

  • сортировка прямым включением;
  • сортировка прямым выбором;
  • сортировка прямым обменом.

Улучшенные методы сортировки в нашем изложении будут представлены следующими алгоритмами:

  • сортировка Шелла;
  • сортировка с помощью дерева;
  • быстрая сортировка.

Сортировка прямым включением

Идея сортировки прямым включением (программа prg4_96.asm) заключается в том, что в сортируемой последовательности masi длиной n (i = 0..n - 1) последовательно выбираются элементы начиная со второго (i< 1) и ищутся их местоположения в уже отсортированной левой части последовательности. При этом поиск места включения очередного элемента х в левой части последовательности mas может закончиться двумя ситуациями:

  • найден элемент последовательности mas, для которого masi<x;
  • достигнут левый конец отсортированной слева последовательности.

Первая ситуация разрешается тем, что последовательность mas начиная с masi раздвигается в правую сторону и на место masi перемещается х. Во второй ситуации следует сместить всю последовательность вправо и на место mas0 переместить х.
В этом алгоритме для отслеживания условия окончания просмотра влево отсортированной последовательности используется прием «барьера». Суть его в том, что к исходной последовательности слева добавляется фиктивный элемент X. В начале каждого шага просмотра влево отсортированной части массива элемент X устанавливается в значение того элемента, для которого будет осуществляться поиск места вставки.

ПРОГРАММА рrg4_96
//prg4_96 - программа на псевдоязыке сортировки прямым включением //Вход: mas[n] - неупорядоченная последовательность байтовых двоичных значений. //Выход: mas[n] - упорядоченная последовательность байтовых двоичных значений. //.---------...----...--.........
ПЕРЕМЕННЫЕ
INT_BYTE n=8: //количество элементов в сортируемом массиве
INT_BYTE mas[n]: //сортируемый массив размерностью п (О..п-l)
INT_BYTE X; //барьер
INT_BYTE i=0: j=0 //индексы
НАЧ_ПРОГ
ДЛЯ 1:-1 ДО п-1 /Л изменяется в диапазоне О..п-l
НАЧ_БЛ0К_1
//присвоение исходных значений для очередного шага
X:=mas[i]: mas[0]:=X; j:=i-l
ПОКА (X<mas[j]) ДЕЛАТЬ НАЧ_БЛОК_2
mas[j+l]:=mas[j]; j:=j-l КОН_БЛ0К_2
mas[j+l]:=X
КОН_БЛОК_1 КОН_ПРОГ
;prg4_96.asm - программа на ассемблере сортировки прямым включением.
.data
mas db 44,55.12.42.94.18.06.67 :задаем массив
n=$-mas
X db 0 :барьер
.code
mov ex.п-1 :цикл по 1
movsi.l :i=2 сус13: присвоение исходных значений для очередного шага
mov al ,mas[si]
movx.al :X: = mas[i]: mas[0]:-X: j:=i-l
push si ;временно сохраним i. теперь J-1 :еще один цикл, который перебирает элементы до барьера x=mas[i] сус12: mov al,mas[si-l]
emp x,al ;сравнить х < mas[j-l]
ja ml :если x > mas[j-l] : если x < mas[j-l]. то
mov al,mas[si-l]
irovmas[si],al
dec si
jmpcycl2 ml: mov al ,x
movmas[si].al
pop si
incsi
loop cyc!3

Этот способ сортировки не очень экономен, хотя логически он выглядит очень естественно.

Сортировка прямым выбором

В алгоритме сортировки прямым выбором (программа prg4_99.asm) на каждом шаге просмотру подвергается несортированная правая часть массива. В ней ищется очередной минимальный элемент. Если он найден, то производится его перемещение на место крайнего левого элемента несортированной правой части массива.

ПРОГРАММА prg4_99

//prg4_99 - программа на псевдоязыке сортировки прямым выбором //Вход: mas[n] - неупорядоченная последовательность байтовых двоичных значений. //Выход: mas[n] - упорядоченная последовательность байтовых двоичных значений. // .
ПЕРЕМЕННЫЕ
INT_BYTE n=8; //количество элементов в сортируемом массиве INT_BYTE mas[n]; //сортируемый массив размерностью п (О..п-l) INTJYTE X: i=0: j=0: k=0 // i. j, k - индексы НАЧ_ПРОГ
ДЛЯ i:=l ДО n-1 //i изменяется в диапазоне 1..П-1 НАЧ_6ЛОК_1
//присвоение исходных значений для очередного шага k:=i: Х: = raas[i]
ДЛЯ j:-1+l ДО n //j изменяется в диапазоне 1+1..п ЕСЛИ mas[j]<X TO НАЧ_БЛ0К_2
k:=j: x:= mas[j] К0Н_БЛ0К_2
mas[k]:= mas[i]; mas[i]:=X КОН_БЛОК_1 КОН_ПРОГ
;prg4_99.asm - программа на ассемблере сортировки прямым выбором.
.data
masdb 44.55.12,42,94,18.06,67 ;задаем массив
n-$-mas
X db 0
К dw 0
.code
:внешний цикл - по 1
mov сх.л-1
mov si ,0 cycll: push ex
mov k.si :k:=i
mov al ,mas[si]
movx.al ;x:=mas[i]
push si :временно сохраним i - теперь j=I+l
incsi :j=I+l
сложенный цикл - по j
mov al ,n
sub ax.si
mov ex,ax количество повторений внутреннего цикла по j cycl2: mov al ,mas[si]
cmpal ,x
ja ml
mov k.si ;k:=j
moval ,mas[si]
movx.al :x:=mas[k] ml: inc si :j:=j+l
loop cycl2
pop si
mov al ,mas[s1]
movdi Л
mov mas[di],al :mas[k]:=mas[i]
mov al,x
mov mas[si],al :mas[i]:-x
inc si
pop ex
loop cycll

Первый вариант сортировки прямым обменом

Этот метод основан на сравнении и обмене пар соседних элементов до их полного упорядочивания (программа prg4_101.asm). В народе этот метод называется методом пузырьковой сортировки. Действительно, если упорядочиваемую последовательность расположить не слева направо, а сверху вниз («слева» — это «верх»), то визуально каждый шаг сортировки будет напоминать всплытие легких (меньших по значению) пузырьков вверх.

ПРОГРАММА prg4_101
//..
//prg4_101 - программа на псевдоязыке сортировки прямым обменом 1
//Вход: mas[n] - неупорядоченная последовательность байтовых двоичных значений.
//Выход: mas[n] - упорядоченная последовательность байтовых двоичных значений.
//....-..
ПЕРЕМЕННЫЕ
INTJ3YTE n=8: //количество элементов в сортируемом массиве INT_BYTE mas[n]; //сортируемый массив размерностью п (О..п-l) INTJYTE X: i-0; j-0 // i. j - индексы НАЧ_ПРОГ
ДЛЯ i:=l ДО n-1 //i изменяется в диапазоне L.n-l НАЧ_БЛ0К_1
ДЛЯ j:=n-l ДОВНИЗ i //j изменяется в диапазоне 1+1.,П ЕСЛИ mas[j-l]< mas[j] TO НАЧ_БЛОК_2
x:=mas[j-l]; mas[j-l]:=mas[j]: mas[j]:=X К0Н_БЛ0К_'2 К0Н_БЛ0К_1 КОН_ПРОГ
;prg4_101.asm - программа на ассемблере сортировки прямым выбором 1.
ПОВТОРИТЬ
.data ДЛЯ j:=r ДОВНИЗ 1 //j изменяв!
: задаем массив ЕСЛИ mas[j-l]< mas[j] TO
masdb 44,55.12,42.94.18.06,67 НАЧ^БЛОК_1
n=$-mas x:=mas[j-l]: mas[j-l]:
X db 0 КОН БЛОК 1
ДЛЯ j:-l ДОВНИЗ г //j изменяв"
.code ЕСЛИ mas[j-l]< mas[j] TO
НАЧ_БЛОК_2
;внешний цикл - по 1 x:=mas[j-l]: mas[j-l]:
mov ex, n -1 КОН БЛОК 2
mov si .1 r:=k-l
cycl1: ПОКА (1>г)
push ex КОН_ПРОГ
mov ex n
1 1 1\щ/ V \— Г\ , 1 1 sub ex,si количество повторений внутреннего цикла :prg4 104.asm - программа на аса
push si временно сохраним i - теперь j=n mov si ,n-l

cycl2: :ЕСЛИ mas[j-l]< mas[j] TO .data
mov al ,mas[si-l] :задаем массив
cmpmas[si].al masdb 44.55,12.42.94.18.06.67
ja ml n=$-mas
movx.al :x=mas[j-l] X db 0
mov al ,mas[si] L dw 1
mov mas[si-l],al ;mas[j-l]= mas[j] R dw n
mov al,x k dw n
mov mas[si].al :mas[j]=x
ml: dec si :цикл по j с декрементом n-i раз .code
loop cycl2 ;.... :1:=2: г:=n: k: =n
pop si cycl3: :ДЛЯ j:-r ДОВНИЗ 1
inc si mov si .R : j:=R
pop ex push si
loop cycll sub si.L
;.... mov ex,si количество по
pop si

Второй вариант сортировки прямым обменом

Эту сортировку называют шейкерной (программа prg4_104.asm). Она представляет собой вариант пузырьковой сортировки и предназначена для случая, когда последовательность почти упорядочена. Отличие шейкерной сортировки от пу зырьковой в том, что на каждой итерации меняется направление очередного про хода: слева направо, справа налево.

ПРОГРАММА prg4_104 mov mas[si-l].al :mas[j-
mov al ,x
//prg4_104 - программа на псевдоязыке сортировки прямым обменом 2 (шейкерной) mov mas[si].al ;mas[j]:-x
//Вход: mas[n] - неупорядоченная последовательность байтовых двоичных значений. mov k.si :k:=j
//Выход: mas[n] -упорядоченная последовательность байтовых двоичных значений. ml: dec si :j:=j-l
loop cycll
ПЕРЕМЕННЫЕ mov ax.k
INT BYTE n=8: //количество элементов в сортируемом массиве inc ax
INT BYTE mas[n]: //сортируемый массив размерностью п (О..п-l) mov Lax :L:=k+l
INT BYTE X: 1-0; j=0: г=0: 1=0: k=0 // i. j. г. 1. k - индексы : цикл cycl2 :ДЛЯ j:=l ДОВНИЗ
НАЧ_ПРОГ mov si.L :j:=L
1:=2: r:=n; k:=n mov ax.R
ПОВТОРИТЬ
ДЛЯ j:=r ДОВНИЗ 1 //j изменяется от 1 до г ЕСЛИ mas[j-l]< mas[j] TO НАЧ_БЛОК_1
x:=mas[j-l]: mas[j-l]:=mas[j]: mas[j]:=X: k:=j КОН_БЛОК_1
ДЛЯ j:=l ДОВНИЗ г //j изменяется от г до 1 ЕСЛИ mas[j-l]< mas[j] TO НАЧ_БЛОК_2
x:-mas[j-l]; mas[j-l]:=mas[j]; mas[j]:=X; k:=j К0Н_БЛ0К_2 r:=k-l ПОКА (1>г) КОН_ПРОГ
:prg4_104.asm - программа на ассемблере сортировки прямым выбором 2 (шейкерной).
.data
: задаем массив
masdb 44.55.12.42.94.18.06.67
n=$-mas
X db 0
L dw 1
R dw n
k dw n
.code
;.... :1:=2: r:=n: k:=n
cycl3: :ДЛЯ ДОВНИЗ 1
mov si.R :j:=R
push si
sub si.L
mov ex,si количество повторений цикла cycll
pop si
dec si cycll: :ЕСЛИ mas[j-l]< mas[j] TO
mov al,mas[si-1]
emp al.mas[si]
jna ml
mov al,mas[si-1]
mov x.al ;x:=mas[j-l]
mov al.mas[si]
mov mas[si-l].al :mas[j-l]:=mas[j]
mov a 1, x
mov mas[si].al :mas[j]:=x
mov k.si ;k:=j
ml: dec si :j:=j -1
loop cycll
mov ax.k
inc ax
mov L.ax :L:=k+l : цикл сус12 :ДЛЯ j:-l ДОВНИЗ г
mov si.L :j:=L
mov ax.R
sub ax.L
mov ex.ax количество повторений цикла сус12
cyc12: mov al.mas[si-l] :ЕСЛИ mas[j-l]< mas[j] TO
emp al.mas[si]
jna m2
mov al,mas[si-l]
mov x.al ;x:=mas[j-l]
mov al.mas[si]
mov mas[si-l].al ;mas[j-l]:=mas[j]
mov al .x
mov mas[si].al :mas[j]:=x
mov k.s1 :k:=j
m2: inc si :j:=j+l
loop cycl2
mov ax.k
dec ax
mov R.ax :R:=k-1
emp L.ax ;L>R - ?
jae $+5
jmp cycl3

Улучшение классических методов сортировки

Приведенные выше четыре метода сортировки — базовые. Их обычно рассматривают как основу для последующего обсуждения и совершенствования. Ниже мы рассмотрим несколько усовершенствованных методов сортировки, после чего вы сможете оценить эффективность их всех.

Сортировка Шелла

Приводимый ниже алгоритм сортировки (программа prg4_107.asm) носит имя автора, который предложил его в 1959 году. Суть его состоит в том, что сортировке подвергаются не все подряд элементы последовательности, а только отстоящие друг от друга на определенном расстоянии h. На каждом шаге значение h изменяется, пока не станет (обязательно) равно 1. Важно правильно подобрать значения h для каждого шага. От этого зависит скорость работы алгоритма. В качестве значений h могут быть следующие числовые последовательности: (4, 2, 1), (8, 4, 2, 1) и даже (7, 5, 3, 1). Последовательности чисел можно вычислять аналитически. Так, Кнут предлагает следующие соотношения:

  • Nk-1 = 3Nk+1, в результате получается последовательность смещений: 1, 4, 13,40,121...
  • Nk-1, = 2Nk+l, в результате получается последовательность смещений: 1, 3, 7, 15, 31...

Подобное аналитическое получение последовательности смещений дает возможность разрабатывать программу, которая динамически подстраивается под конкретный сортируемый массив.
Отметим, что если h=1, то алгоритм сортировки Шелла вырождается в сортировку прямыми включениями.
Существует несколько вариантов алгоритма данной сортировки. Вариант, рассмотренный ниже, основывается на алгоритме, предложенном Кнутом.

ПРОГРАММА prg4_107
//prg4_107 - программа на псевдоязыке сортировки Шелла
//Вход: mas_dist=(7,5,3.1) - массив смещений: mas[n] - неупорядоченная
последовательность байтовых двоичных значений. //Выход: mas[n] -упорядоченная последовательность байтовых двоичных значений.
-ПЕРЕМЕННЫЕ
INT_BYTE t=4; //количество элементов в массиве смещений
INT_BYTE mas[n]; //сортируемый массив размерностью п (О..п-1)
INT_BYTE mas_dist[t]=(7.5,3,l): // массив смещений размерностью t (O..t-1)
INT_BYTE h=0 //очередное смещение из mas_dist[]
INT_BYTE X: i=0: j-0; s=0 // i. j. s - индексы
НАЧ_ПРОГ
ДЛЯ s:=0 ДО t-1 НАЧ_БЛОК_1
h:=mas_dist[s] ДЛЯ j:4i ДО n-1 НАЧ_БЛ0К_2
i:=j-h: X:=mas[i]
@@d4: ЕСЛИ Х>= mas[i] TO ПЕРЕЙТИ_НА №66 mas[i+h]:=mas[i]: i:=i-h ЕСЛИ 1>0 ТО ЛЕРЕЙТИ__НА @@d4 Ш6: mas[i+h]:=x К0Н_БЛ0К_2 КОН_БЛОК_1 КОН_ПРОГ
:prg4_107.asm - программа на ассемблере сортировки Шелла
.data
: задаем массив
masdb 44,55.12.42.94.18,06,67
n=$-mas
X db 0
:задаем массив расстояний
mas_dist db 7.5.3.1
t=$-mas_dist ;t - количество элементов в массиве расстояний
.code
xorbx.bx :в bx - очередное смещение из mas_dist[] :dl
movcx.t :цикл по t (внешний)
movsi.O :индекс по mas_dist[] (?@d2: push ex
mov bl,mas_dist[si] :в bx - очередное смещение из mas_dist[]
inc si push si :ДЛЯ j:=h ДО n-1
mov di.bx ' ;di - это j :j:=h+l - это неявно для нумерации массива с нуля @@d3: cmpdi.n-1 ;j=<n ?
ja @@ml :конец итерации при очередном mas_dist[]
mov si ,di
sub si.bx :i:=j-h: si - это i
mov al ,mas[di] ;x:=mas[j]
movx.al :x:=mas[j] @@d4: :ЕСЛИ Х>= mas[i] TO ПЕРЕЙТИ_НА №й6
mov al,x
cmpal ,mas[si]
jae@@d6
:d5 - mas[i+h]:=mas[i]: i:=i-h push di push si popdi
adddi.bx :i+h
mov al, mas[si] :mas[i+h]:=mas[i]
movmas[di],al :mas[i+h]:=mas[i] pop di
subsi.bx ;i:=i-h
cmpsi.O ;ЕСЛИ i>0 TO ПЕРЕЙТИ_НА @@d4
jg Ш4
@@d6: ;mas[i+h]:=x
push di push si pop di
adddi.bx ;i+h
mov al .x
movmas[di].al ;mas[i+h]:=x popdi
incdi :j:=j+l
jmp ШЗ @@ml: pop si pop ex
loop Ш2 @@exit:

Сортировка с помощью дерева

Следующий алгоритм (программа prgl0_229.asm) является улучшением сортировки прямым выбором. Автор алгоритма сортировки с помощью дерева — Дж. У. Дж. Уильяме (1964 год). В литературе этот алгоритм носит название «пирамидальной» сортировки. Если обсужденные выше сортировки интуитивно понятны, то алгоритм данной сортировки необходимо пояснить подробнее. Этот алгоритм предназначен для сортировки последовательности чисел, которые являются отображением в памяти дерева специальной структуры — пирамиды. Пирамида — помеченное корневое двоичное дерево заданной высоты h, обладающее тремя свойствами:

  • каждая конечная вершина имеет высоту h или h-1;
  • каждая конечная вершина высоты h нажщится слева от любой конечной вершины высоты h-1;
  • метка любой вершины больше метки любой следующей за ней вершины.

На рис. 2.3 изображено несколько деревьев, из которых лишь одно Т4 является пирамидой.

Рис. 2.3. Примеры деревьев (Т4 — пирамида)

Такая структура пирамид позволяет компактно располагать их в памяти. Например, пирамида, показанная на рисунке, в памяти будет представлена следующим массивом: 27, 9, 14, 8, 5, 11, 7, 2, 3. Оказывается, что такая последовательность чисел легко подвергается сортировке.
Таким образом, сортировка массива в соответствии с алгоритмом пирамидальной сортировки осуществляется в два этапа: на первом этапе массив преобразуется в отображение пирамиды; на втором — осуществляется собственно сортировка. Соответственно нами должны быть разработаны две процедуры для выполнения задач каждого из этих двух этапов.

ПРОЦЕДУРА insert_item_in_tree (i. mas. N) //
//insert_item_in_tree - процедура на псевдоязыке вставки элемента на свое место
в пирамиду //Вход: mas[n] - сформированная не до конца пирамида: 1 - номер добавляемого элемента
в пирамиду из mas[n] (с конца): n - длина пирамиды //Выход:действие - элемент добавлен в пирамиду.
НАЧ_ПРОГ
j:=i @@ml: k:=2*j: h-k+1
ЕСЛИ (1=<N И (mas[j]<mas[k] ИЛИ mas[j]<mas[l]) TO ПЕРЕЙТИ_НА @йп6
ИНАЧЕ ПЕРЕЙТИ_НА @@m2
КОН_ЕСЛИ @@m6: ЕСЛИ tnas[k]>mas[l] TO ПЕРЕЙТИ_НА @@т4
ИНАЧЕ ПЕРЕЙТИ_НА @@тЗ
КОН_ЕСЛИ @@т4: x:=mas[j]
mas[j]:=mas[k]
j:=k
mas[k]:=x ПЕРЕЙТИ_НА (a0ml (а@тЗ: x:=mas[j]
mas[j]:=mas[l]
mas[l]:=x
ПЕРЕЙТИ_НА @@ml*
@@m2: ЕСЛИ (k==n И mas[j]<mas[k]) TO ПЕРЕЙТИ_НА @@m7
ИНАЧЕ ПЕРЕЙТИ_НА @@m8
КОН_ЕСЛИ @@m7: x:=mas[j]
mas[j]:=mas[n]
;m-n/2 - значение, равное половине числа элементов массива mas
push si push ex
movj.si :j\»1 @@m4:
movsi.j :i->si
movax.j :k:=2*j; l:-k+l
shlax.l :j*2
movk.ax : k :=j*2
inc ax
movl.ax :l:»k+l
cmpax.m :l<m
ja @@m2
moval ,raas[si-l] ;ax:-mas[j]
mov di,k
emp al ,mas[di-l]
jna @@m6
inc di
emp al.mas[di-l]
jna @@m6
jmp @@m2
@@m6: ;условие выполнилось:
;2j+l+<M AND (mas[j]<mas[2j] OR mas[j]<mas[2j+l]) :обмен с mas[j]
mov di,k
mov al ,mas[di-1] ;ax:=mas[k]
inc di
emp al,mas[di-l] :mas[k]>mas[l]
jna ШтЗ
mov al ,raas[si-l]
movx.al ;x:=rnas[j]
dec di
mov al ,mas[di-l]
movmas[si-l].al :mas[j]:=mas[k]
movj.di :j:=k
mov al .x
movmas[di-l],al :mas[k]:=x
jmp @?m4
:@@m3: x:=mas[j] ;ПЕРЕЙТИ_НА @@ml №m3: :mas[k] =< mas[l]
mov al,mas[si-l]
movx.al :x:=mas[j]
mov al,mas[di-l]
movmas[si-l].al ;mas[j]:=mas[l]
movj.di :j:=l
mov al ,x
movmas[di-l].al ;mas[l]:=x
jmp @@m4 Шт2: ; условие не выполнилось: 2j+l+<M AND (mas[j]<mas[2j] OR mas[j]<mas[2j+l])
mov ax.k
emp ax.m

Нахождение медианы

Медиана - элемент последовательности, значение которго не больше значений одной половины этой последовательности. Например, медианой последовательности чисел 38 7 5 90 65 8 74 43 2 является 38.

оответствующая отсортированная последовательность будет выглядеть так: 2 5 7 8 38 43 65 74 90.

Задачу нахождения медианы можно решить просто — предварительно отсортировать исходный массив и выбрать средний элемент. Но К. Хоор предлагает метод, который решает задачу нахождения медианы быстрее и соответственно может рассматриваться как вспомогательный для реализации других задач. Достоинство метода К. Хоора заключается в том, что с его помощью можно эффективно находить не только медиану, но и значение k-го по величине элемента последовательности. Например, третьим по величине элементом приведенной выше последовательности будет 7.
Значение k-го элемента массива определяется по формуле k=n/2, где п — длина исходной последовательности чисел.
Ниже приведена программа prg4_123.asm, которая находит элемент-медиану массива. Аналогичную функцию выполняет и процедура median, но процедура отличается тем, что ее можно вызывать динамически во время работы программы, в которой она используется.

ПРОГРАММА prg4_123
//
//prg4_123 - программа на псевдоязыке нахождения k-го по величине элемента массива mas
длиною п. Для нахождения медианы к=п/2. //Вход: mas[n] - неупорядоченная последовательность двоичных значений: к - номер
искомого по величине элемента mas[n]. //Выход: х - значение к-го по величине элемента последовательности mas[n].
// .....
ПЕРЕМЕННЫЕ
INT_BYTE n: ;длина mas[n]
INT_BYTE mas[n]: //сортируемый массив размерностью n (О..n-l)
INTJYTE X:W;i=0: j=0; 1=0; г=0 // j: 1; г - индексы
НАЧПРОГ
1:=1: г:=п ПОКА 1<г ДЕЛАТЬ НАЧ_БЛОК_1 х:=а[к]: 1:=1: j:=r ПОВТОРИТЬ
ПОКА a[i]<x ДЕЛАТЬ i:=i+l ПОКА х< a[j] ДЕЛАТЬ j:=j-l ЕСЛИ i<j TO НАЧ_БЛ0К_2
w:=a[i]: а[1]:-аШ; a[j]:=w i:=i+l: j:=j-l К0Н_БЛ0К_2 ПОКА (i>j) ЕСЛИ j<k TO 1:=I ЕСЛИ k<i T0 r:=j КОН_БЛОК_1 КОН_ПРОГ
;prg4_123 - программа на ассемблере нахождения k-го по величине элемента массива mas длиною п. Для нахождения медианы к=п/2.
.data
jge $+6
movR.di :R<-J :цикл(1)конец jmp @@m8

Быстрая сортировка

Последний рассматриваемый нами алгоритм (программа prglO_223.asm) является улучшенным вариантом сортировки прямым обменом. Этот метод разработан К. Хоором в 1962 году и назван им «быстрой сортировкой». Эффективность быстрой сортировки зависит от степени упорядоченности исходного массива. Для сильно неупорядоченных массивов — это один из лучших методов сортировки. В худшем случае, когда исходный массив почти упорядочен, его быстрая сортировка по эффективности не намного лучше сортировки прямым обменом.
Идея метода быстрой сортировки состоит в следующем. Первоначально среди элементов сортируемого массива mas[l. .n] выбирается один mas[k], относительно которого выполняется переупорядочивание остальных элементов по принципу — элементы mas[i]<mas[k] (i=0. .n-1) помещаются в левую часть mas; элементы mas[i]>mas[k] (i=0. .n-1) помещаются в правую часть mas. Далее процедура повторяется в полученных левой и правой подпоследовательностях и т. д. В конечном итоге исходный массив будет правильно отсортирован. В идеальном случае элемент mas[k] должен являться медианой последовательности, то есть элементом последовательности, значение которого не больше значений одной части и не меньше значений оставшейся части этой последовательности. Нахождению медианы было посвящено обсуждение программ предыдущего раздела. В следующей программе элементом mas[k] является самый левый элемент подпоследовательности.

ПРОГРАММА prg27_136
//prg27_136 (по Кнуту) - программа на псевдоязыке «быстрой сортировки» массива. //Вход: mas[n] - неупорядоченная последовательность двоичных значений длиной п. //Выход: mas[n] -упорядоченная последовательность двоичных значений длиной n.
ПЕРЕМЕННЫЕ
INTJYTE n: //длина mas[n]
INT_BYTE mas[n]; //сортируемый массив размерностью п (О..п-1)
INTJYTE К; TEMP: i=0; j=0: 1=0: г=0; // i. j. 1. г - индексы
INT_WORD M=l //длина подпоследовательности, для которой производится сортировка методом
прямых включений //для отладки возьмем М=1 НАЧ_ПРОГ
ЕСЛИ M<N TO ПЕРЕЙТИ_НА q9
1 :-1: г:=п
ВКЛЮЧИТЬ_В_СТЕК (Offffh) //Offffh - признак пустого стека q2: i :-l: j:-r+l: k:=mas[l] q3: ЕСЛИ i=<j-l TO ПЕРЕЙТИ_НА qq3
ПЕРЕЙТИ_НА q4 //итерация прекращена qq3: i:=i+l
ЕСЛИ mas[i]<K TO ПЕРЕЙТИ_НА q3 q4: j:-j-l
ЕСЛИ j<1 TO ПЕРЕЙТИ_НА q5
ЕСЛИ K<mas[j] TO ПЕРЕЙТИ_НА q4
ЕСЛИ j>i TO ПЕРЕЙТИ_НА q6
Iq2: ;1:-1: j:-r+l: k:-nas[l] movsi.L ;i(si):=L mov di . R incdi ;j(di):=R+l xor ax.ax mov al ,mas[si] mov k.al
q3: :ЕСЛИ 1-sj-l TO ПЕРЕЙТИ_НА qq3 inc si ;i:=i+l cmp si.di :i=<j jleqq3
dec si :сохраняем i=<j jmp q4 :ПЕРЕЙТИ_НА д4//итерация прекращена qq3: moval.k ;i:=i+l
cmpmas[si].al :ЕСЛИ mas[i]<K TO ПЕРЕЙТИ_НА q3
jb q3
q4: decdi ;j:=j-l cmpdi.si :j>=i-l jb q5 mov al ,k :ЕСЛИ K<mas[j] TO ПЕРЕЙТИ_НА q4
cmp al ,mas[di]
»jb q4 q5: ;ЕСЛИ j>i TO ПЕРЕЙТИ_НА q6 cmpdi.si :j=<i ??? J9 q6 movbx.L ://обмен mas[l]:= mas[j] mov dl ,mas[bx] xchg mas[di].dl xchg mas[bx].dl jmpq7 :ПЕРЕЙТИ_НА q7 q6: mov dl.masCsi] ; mas[i]<-> mas[j] xchg mas[di].dl xchg mas[si].dl jmpq3 ;ПЕРЕЙТИ_НА q3 q7: ;ЕСЛИ r-j>j-l>M TO mov ax,г
subax.di ;r-j->ax mov bx.di
subbx.l :j-l->bx cmpax.bx :r-j>j-l ??? jl q7_m4
cmpbx.M ;j-l>M ??? jleq7_m3 ;1, r-j>j-l>M - в стек (j+l.r); r:=j-l; перейти на шаг q2
mov ax.di inc ax push ax
(push г mov r.di dec г :г:=j -1 q7_m4: ;r-j>M ??? cmp ax.M jg q7_m2 cmp bx. M
jleq8 ;4. j-l>M>r-j - r:=j-l: перейти на шаг q2
mov r.di
dec г
jmpq2 q7_m3: cmpax.M
jleq8 ;3. r-j>M>j-l - l:=j+l: перейти на шаг q2
mov 1,di
inc 1
jmpq2 q7_m2: :2. j-l>r-j>M - в стек (l.j-1); l:=j+l; перейти на шаг q2
push 1
mov ax.di
inc ax
push ax
mov 1 ,di
inc 1
jmpq2 q8: извлекаем из стека очередную пару (l.r)
pop г
cmpr.Offffh :ЕСЛИ r<>0ffffh TO ПЕРЕЙТИ_НА q2
je q9
pop!
jmpq2 q9: ;сортировка методом пряных включений - при М=1 этот шаг можно опустить (что и сделано
для экономии места)

Обратите внимание на каскад сравнений шага q7, целью которых является проверка выполнения следующих неравенств:

  • r-j>=j-1>M — в стек (j+l,r); r:-j-1; перейти на шаг q2;
  • j-1>r-j>M — в стек (1, j-1); I :=j+1; перейти на шаг q2;
  • r-j>M>j-l — 1 :=j+1; перейти на шаг q2;
  • j-1>M>r-j — г:=j-1; перейти на шаг q2.

Проверка этих неравенств реализуется в виде попарных сравнений, последовательность которых выглядит так, как показано на рис. 2.4.

Рис. 2.4. Последовательность сравнений шага q7

За подробностями алгоритма можно обратиться к тому 3 «Сортировка и поиск» книги Кнута .

Сушествует другой вариант алгоритма этой сортировки — у Вирта в книге «Алгоритмы + структуры данных - программы». Но у него используется понятие медианы последовательности. Задачу нахождения медианы мы рассматривали выше. Эта задача интересна еще и тем, что она, по сути, является одной из задач поиска, рассмотрению которых посвящен следующий раздел.

Поиск в массивах

Поиск информации является одной из самых распространенных проблем, с которыми сталкивается программист в процессе написания программы. Правильное решение задачи поиска для каждого конкретного случая позволяет значительно повысить эффективность исполнения программы. Для общего случая обычно предполагается, что поиск ведется среди массива записей определенной структуры. В каждой записи есть уникальное ключевое поле. Абстрагируясь, можно сказать, что массив записей — это массив ключевых полей. В этом случае задачу поиска в массиве записей можно свести к задаче поиска в массиве ключевых слов. В этом разделе мы обсудим проблему поиска в массивах и пути ее решения. В отличие от методов сортировки классификация методов поиска не отличается особым разнообразием. Уверенно можно сказать, что методы поиска будут различными для упорядоченных и неупорядоченных массивов. Неупорядоченными массивами здесь считаются массивы, о порядке следования элементов которых нельзя сделать никаких предположений. Для таких массивов особых способов поиска нет, кроме как последовательно просматривать все их элементы. В теории такой поиск называется линейным. Если элементы массивов каким-то образом отсортированы, то речь идет об упорядоченных массивах и для поиска в них существует определенный набор методов. В следующих разделах мы рассмотрим ассемблерную реализацию наиболее интересных методов поиска, применяемых в тех случаях, когда ключевое поле — некоторое число. Большой интерес представляют методы поиска в строке, которые рассматриваются в главе 4. И наконец, существует третий класс методов поиска, основанный на арифметическом преобразовании исходных ключей — «хэшировании». Эти методы мы рассмотрим в разделе «Поиск в таблице».

Неупорядоченный поиск

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

Первый вариант неупорядоченного поиска

Этот вариант программы реализации линейного поиска предполагает обычный перебор до тех пор, пока не будет найден нужный элемент или не будут перебраны все элементы.

ПРОГРАММА prg27_429
//prg27_429 - программа на псевдоязыке линейного поиска в массиве (вариант 1).
//Вход: mas[n] - неупорядоченная последовательность двоичных значений длиной n: k -
искомое значение
//Выход: 1 - позиция в mas[n] (0<i<n-l), соответствующая найденному символу.
ПЕРЕМЕННЫЕ
INT_BYTE n: //длина mas[n]
INTJSYTE mas[n]: //массив для поиска размерностью п (О..п-l)
INTJYTE к; //искомый элемент
INT_W0RD 1-0 //индекс
НАЧ_ПРОГ
s2: ЕСЛИ (k==mas[i]) TO ПЕРЕЙТИ_НА _exit

ЕСЛИ (i=<n) TO ПЕРЕЙТИ_НА s2 _exit: //вывести значение 1 по месту требования КОН_ПРОГ
:prg27_429.asm - программа на ассемблере линейного поиска в массиве (вариант 1).
.data
: задаем массив
mas db 50h.08h.52h.06h.90h.17h,89h.27h.65h.42h.15h.51h.61h.67h.76h.70h
n=$-mas k db 15h
.code
xorsi.si ;1-(s1):=0
mov al ,k s2: cmpal.mas[si] ;ЕСЛИ k==mas[i] TO ПЕРЕЙТИ_НА _exit
je ok
inc si :i :=i+l
cmpsi,n-l :ЕСЛИ i=<n TO ПЕРЕЙТИ_НА s2
jbes2 :реакция на неудачный результат поиска
jmp exit ok: :реакция на удачный результат поиска
jmpexit

Второй вариант неупорядоченного поиска

Программу первого варианта линейного поиска можно немного усовершенствовать вводом дополнительного элемента — барьера.

ПРОГРАММА prg27_430
//prg27_430 - программа на псевдоязыке линейного поиска в массиве (вариант 2). //Вход: mas[n] - неупорядоченная последовательность двоичных значений длиной ri+1; k -искомое значение
//Выход: i - позиция в mas[n] (0<i<n-l), соответствующая найденному символу.
ПЕРЕМЕННЫЕ

INT_BYTE n: //длина mas[n]
INT_BYTE mas[n+l]: //массив для поиска размерностью п+1 (О..п)
INT BYTE k: //искомый элемент
INT_WORD i=0 //индекс
НАЧ_ПРОГ
i:=0: mas[n]:=k //установить барьер 52: ЕСЛИ (k==mas[i]) TO ПЕРЕЙТИ_НА _exit
1:-1+1: ПЕРЕЙТИ_НА s2 _exit: ЕСЛИ i>n TO ПЕРЕЙТИ_НА exit_error //вывести значение i по месту требования exit_error: //вывести сообщение об отсутствии элемента КОН_ПРОГ
:prg27_430 - программа на ассемблере линейного поиска в массиве (вариант 2).
.data
: задаем массив
mas db 50h. 08h. 52h. 06h. 90h. 17h. 89h. 27h. 65h. 42h. 15h. 51h. 61h .67h.76h.70h
n=$-mas
db Oh дополнительный элемент для барьера k db 15h
.code
xor si.si
mov al ,k
mov mas[n].al :i:=0: mas[n]:=k //установить барьер s2: cmpal.mas[si] ;ЕСЛИ k==mas[i] TO ПЕРЕЙТИ_НА _exit
je ok
inc si
jmps2 ;i:-1+l: ПЕРЕЙТИ_НА s2 ok: emp si,n :ЕСЛИ i>n TO ПЕРЕЙТИ_НА exit_error
je exit_error :вставить реакцию на удачный результат поиска
jmp exit exit_error: ;реакция на неудачный результат поиска

Упорядоченный поиск

На практике массивы элементов (записей) обычно определенным образом упорядочиваются. Это облегчает задачу поиска в них, так как появляется возможность формализации этого процесса. Записи в массиве в зависимости от значений ключевых полей могут быть упорядочены в числовом или лексикографическом поhядке.

Двоичный поиск

Этот вид поиска (программа prg10_242.asm) предназначен для выполнения поиска в упорядоченном массиве (записей), содержащем N числовых ключей: kl < k2 < <-. <kN. Упорядоченный массив чисел 02h, 04h, 06h, 08h, 16h, 24h, 38h, 45h, 47h, 48h, 56h, 57h, 58h, 70h, 76h, 79h можно представить в виде двоичного дерева (Рис. 2.5).

Рис. 2.5. Представление массива в виде двоичного дерева

Для этого необходимо написать программу, которая в цикле выполняет следующие действия. Вначале определяется элемент, расположенный в середине исходного массива, — он будет вершиной двоичного дерева. Далее в стеке запоминаются интервалы ключей подмассивов, расположенных слева и справа от элемента-вершины двоичного дерева. После этого в цикле происходит извлечение интервалов ключей подмассивов из стека, определение серединных элементов в этих подмассивах, разбиение на очередные подмассивы, запоминание их границ в стеке, извлечение и т. д. В результате этих действий будет построено двоичное дерево, подобное изображенному на рисунке.
Абстрактно задачу поиска ключа с определенным значением К можно сравнить с обходом двоичного дерева начиная с его вершины. Если К меньше значения ключа вершины, то идем к узлу дерева по левой ветке, если больше — то по правой. Значение ключа в очередном узле соответствует среднему элементу под-массива, лежащему слева (справа) от элемента, соответствующего вершине дерева. Для среднего элемента подмассива процедура сравнения и принятия решения о переходе повторяется. Процесс заканчивается, когда обнаружен узел, ключ которого равен К, либо очередной узел является листом и идти больше некуда.

:prg10_242.asm - программа на ассемблере двоичного поиска
;Вход: mas[n] - упорядоченная последовательность двоичных значений длиной N
: к - искомое значение
:Выход: 1 - позиция в mas[n] (0<i<n-l), соответствующая найденному символу.
.data
; задаем массив
masdb 02h.04h.06h.08h.16h.24h.38h.45h.47h.48h.57h.56h.58h.70h.76h.79h
n-$-mas
k db 4 ;искомое значение
.code
в si и di индексы первого и последнего элементов последней просматриевой части
последовательности:
movsi.O .
mov di,n-l
хог ах. ах
nraval.k ;искомое значение в ах cont_search: ;получим центральный индекс:
cmpsi.di ;проверка на окончание (неуспешное):si>di
ja exit_bad
mov bx.si
addbx.di
shr bx.l ;делим на 2
cmpmas[bx].al сравниваем с искомым значением
je exit_good -.искомое значение найдено
ja @@ml ;mas[bx]>k
movsi.bx :mas[bx]<k:
inc si
jmpcont_search @@ml: movdi.bx
decdi
jmp cont_search exit_bad: пор :вывод сообщения об ошибке
exit_good: пор ;вывод сообщения об успехе

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

Действия с матрицами

Рассмотренные выше операции над массивами были реализованы исходя из предположения, что массивы имеют одну размерность. Но существует группа задач, в которой работа осуществляется с массивами большей размерности. Прежде всего это действия с матрицами. Кстати, такая структура данных, как сеть (или граф) представляется с помощью матрицы.

Транспонирование прямоугольной матрицы

Приемы работы с массивами размерностью больше 1 удобно рассматривать на типовой задаче, например, такой как транспонирование матрицы.
Суть задачи транспонирования матрицы А = {аij} заключается в замене строк столбцами и столбцов строками в соответствии с формулой а 'ij= аij, где а 'ij — элементы транспонированной матрицы А' = {аij}. Максимальная величина индексов i и j задается константами m (количество строк) и т (количество столбцов), соответственно диапазон их значений составляет: i = 0..m-1, j ~ 0..n-1. Элементы матрицы задаются статически — в сегменте данных.
Выше уже отмечалось то, каким образом производится локализация в памяти элемента многомерного массива исходя из его логического номера при условии, что размерность элементов — 1 байт. Локализация элемента матрицы А относительно базового адреса производится по формуле:

аij = n*i+j. (2.1)

Соответствующий элемент в транспонируемой матрице будет расположен по адресу:

A'ij-m*i+j. (2.2)

Например, рассмотрим матрицу 3x4:

02h 04h 06h 08h
16h 24h 38h 45h
47h 48h 57h 56h

Эта матрица в памяти будет выглядеть так:
02h. 04h, 06h. 08h. 16h. 24h. 38h. 45h. 47h. 48h, 57h. 56h
Транспонированный вариант матрицы:

02h 16h 47h
04h 24h 48h
06h 38h 57h
08h 45h 56h

Транспонированный вариант матрицы в памяти будет выглядеть следующим образом:
02h. 16h. 47h. 04h. 24h. 48h. 06h. 38h, 57h. 08h. 45h. 56h
Для решения задачи «в лоб» по формулам 1 и 2 требуется выделять в памяти область для хранения транспонированной матрицы, совпадающую по размеру с исходной.

;prg29_102.asm - программа на ассемблере транспонирования матрицы.
:Вход: mas[n] - матрица mxn.
:Выход: _mas[n] - транспонированная матрица nxm.
.data
m dw 3 : i =0.. 2
n dw 4 ;j=0..3
:задаем матрицу 3x4 (mxn):
mas db 02h.04h.06h.08h.l6h.24h,38h.45h.47h,48h.57h,56h
s_mas=$-mas
_mas db sjnas dup (Offh)
temp db 0
'code'
mov cx.m
xorsi.si :i:=0 ml: push ex :цикл по i
xordiidi ;J:-0
локализуем masij по формуле: masij=n*i+j m2: mov ax.n
mul si предполагаем, что результат в рамках ах
add ax.di : n*i+j
mov bx.ax
mov al ,mas[bx]
movtemp.al локализуем место-приемник в jnasij по формуле: _masij=masji=m*i+j
mov ax.m
mul di предполагаем, что результат в рамках ах
add ax,si

mov al .temp
mov _mas[bx].al
incdi :j:=j+l
loop rn2
inc si
pop ex восстанавливаем счетчик внешнего цикла
loop ml

Отметим, что для транспонирования прямоугольной матрицы необязательно ее моделировать так, как это сделано в предыдущей программе. Кнут приводит соотношение, которое позволяет транспонировать матрицу в линейном порядке, зная только значения тип. Для этого используется соотношение, при котором значение из ячейки i (для 0<i<N = mn-1) исходной матрицы переводится в ячейку m*x (mod N) транспонированной матрицы.

Структура

Чтобы правильно использовать машину, важно
добиться хорошего понимания структурных отношений,
существующих между данными, способов представления
таких структур в машине и методов работы с ними.
Д. Кнут

Структура (запись) — конечное упорядоченное множество элементов, в общем случае имеющих различный тип. Элементы, входящие в состав структуры, называют полями структуры. Каждое поле структуры имеет имя, по которому производится обращение к содержимому поля. Пример структуры — совокупность сведений о некотором человеке: номер ИНН, фамилия, имя, отчество, дата рождения, место работы, трудовой стаж и т. п. Подробно порядок описания структур и некоторые приемы работы с ними описаны в учебнике. Поэтому мы, чтобы не повторяться, рассмотрим те практически важные моменты, которые были упущены в учебнике: вложенность структур; использование структур для моделирования таблиц и организация работы с ними.

Вложенные структуры

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

Рис. 2.6. Иерархическая структура сложной записи

Листы в этом дереве являются той полезной информацией, к которой необходимо получить доступ. Для этого нужно некоторым образом указать последовательность идентификаторов, в которой первым идентификатором является имя структуры, далее следуют идентификаторы промежуточных узлов и последним идентификатором является идентификатор нужного листа. Но в ассемблере подобная схема реализована очень ограниченно — можно указать только имена первого и последнего идентификаторов (промежуточных попросту нет). Рассмотрим пример. Как обычно, прежде чем работать со структурой, необходимо задать ее шаблон. Далее для работы со структурой в программе необходимо создать ее экземпляр. В программе ниже шаблон структуры называется element, а соответствующий ему экземпляр структуры — si. Отметим лишь, что не стоит искать в приведенных ниже примерах какой-либо смысл, так как они предназначены только для демонстрации механизма вложенности структур (и объединений).

:prg02_01.asm - программа, демонстрирующая описание и использование структуры в программе на ассемблере.
elementstruc
INN dd 0 ;ИНН
name db 30 dup С ') ;Ф.И.О.
у_birthday dw 1962 :год рождения
m birthday db 05 ;месяц рождения
d_birthday db 30 :месяц рождения
nationality db 20 национальность
;и так далее
ends
.data
si element<>
.code
mov al,si.m_birthday

Информацию о дате рождения можно оформить в виде отдельной структуры, вложенной в текущую структуру, так как это сделано в программе ниже:

:prg02_02.asm - программа, демонстрирующая вложение структуры в текущую структуру в программе на ассемблере.
:
elementstruc
INN dd 0 :ИНН
fiodb 30 dup (' ') ;Ф.И.О.
struc
y_birthday dw 1962 ;год рождения m_birthday db 05 ;месяц рождения d_birthday db 30 ;месяц рождения
ends
nationality db 20 национальность ;и так далее
ends
.data
si element<>
.code
mov al ,sl.m_birthday

Синтаксис ассемблера допускает вынести описание вложенной структуры за пределы текущей структуры. При этом можно инициализировать необходимые поля структуры birthday внутри отдельного экземпляра структуры element. Но работает такое описание структуры только в режиме IDEAL, что и продемонстрировано в программе ниже.

:prg02_03.asm - программа, демонстрирующая вложение структуры в другую структуру в программе на ассемблере.
birthday struc
y_bi rthday dw 1962 :год рождения
m_birthday db 05 :месяц рождения
d_birthday db 30 :день рождения
ends
element struc
INN dd 0 ;ИНН
Birthday struc (m_birthday-06. d_birthday=21)
fio db 30 dup С ') ;Ф.И.О.
nationality db 20 национальность
;и так далее
ends
.data
si element <.<»
.code
ideal
mov al,si.m_birthday masm

Кстати, в описание структур можно вкладывать не только описания других структур, но и описание объединений, так как это показано в следующей программе.

:prg02_04.asm - программа, демонстрирующая взаимное вложение объединений и структур в программе на ассемблере.
elementstruc INN del 0 :ИНН fiodb 30 dup С ') :Ф.И.О. union
struc
y_birthday_l dw 1962 :год рождения m_birthday_l db 06 ;месяц рождения d_birthday_l db 03 ;месяц рождения ' ends
struc
d_birthday_2 db ? ;месяц рождения m_birthday_2 db ? :месяц рождения y_birthday_2 dw ? ;год рождения
ends
ends ;конец объединения nationality db 20 -.национальность :и так далее ends .data si element<>
!code"
mov al.si.m_bi rthday_l
mov sl.m_birthday_2,0ffh mov al,sl.m_birthday_2

Массивы структур — таблицы

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

  • включение нового элемента путем расширения таблицы или его вставки на свободное место;
  • поиск элемента для последующей его обработки;
  • исключение элемента из таблицы.

Скорость доступа к элементам таблицы при выполнении этих операций зависит от двух факторов — способа организации поиска нужного элемента и размера таблицы.

Поиск в таблице

Перечисленные выше операции с таблицей предполагают, что для однозначного доступа к ее элементам последние должны содержать один или более признаков, отличающих их от других элементов этой таблицы. С этой целью в логическую структуру элемента включается по крайней мере одно поле — ключ, содержимое которого уникально для любого из элементов, входящих в таблицу. В общем случае работа с таблицами сводится к методам решения задачи поиска нужного элемента таблицы по заданному значению ключа. Результат решения — получение адреса искомого элемента или заключение о его отсутствии. Для локализации элемента таблицы необходимо знать два адресных компонента — адрес таблицы и смещение нужного элемента относительно начала таблицы. Адрес таблицы получить несложно, так как он является адресом ее первого элемента. Что же касается определения второй компоненты адреса, то для этого существует ряд методов, рассмотрению которых и будет посвящено дальнейшее изложение.
В ряде случаев наряду с ключевым полем определяют еще одно служебное поле — поле текущего состояния элемента, которое может принимать три значения:

  • элемент свободен — после инициализации таблицы элемент ни разу не использовался для хранения полезной информации;
  • элемент используется для хранения полезной информации;
  • элемент удален — после инициализации таблицы элемент хотя бы один раз использовался для хранения полезной информации, после чего был освобожден.

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

  • неупорядоченные;
  • древовидные;
  • упорядоченные;
  • с вычисляемыми входами (хэш-таблицы).

Неупорядоченные таблицы

Это простейший вид организации таблиц. Элементы располагаются непосредственно друг за другом, без пропусков. Процесс поиска нужного элемента выполняется очень просто — последовательным перебором элементов таблицы начиная с первого. При этом анализируется ключевое поле и в случае удовлетворения его условию поиска нужный элемент локализуется. Такой способ поиска называется последовательным, или линейным. Для добавления нового элемента необходимо найти первое свободное место, после чего выполнить операцию добавления. Процесс удаления можно реализовать следующим образом. После локализации нужного элемента его поле текущего состояния необходимо установить в состояние «элемент удален». Тогда процесс поиска места для добавления нового элемента будет заключаться в поиске такого элемента, у которого поле текущего состояния элемента имеет значение «элемент удален» или «элемент свободен». Можно еще более оптимизировать работу с неупорядоченной таблицей путем введения в начале таблицы (или перед ней) дескриптора, поля которого содержали бы информацию о размере таблицы, положении первого свободного элемента и т. д.
Обычно неупорядоченные таблицы используют в качестве временных для хранения небольшого количества элементов (до 20). Для таблиц большего размера такая организация поиска неэффективна, поэтому для них следует применять другие способы локализации нужного элемента.

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

  • содержимое файла — идентификаторы и константы, разделенные не более чем одним пробелом, перед первым или последним идентификатором или константой в строке также следуют по одному пробелу;
  • для удобства преобразования предполагаем, что длина и количество строк файла maket.txt находятся в диапазоне 0..99, а общая длина файла — не более 240 байт.

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

  • биты 0 и 1 — состояние элемента: 00 — свободен; 01 — используется; 10 — удален;
  • бит 2 — тип константы: 0 — десятичная константа; 1 — шестнадцатеричная константа;
  • бит 3 — 0 — не последний элемент таблицы; 1 — последний элемент таблицы.

:prg02_05.asm - программа на ассемблере демонстрации работы с неупорядоченной таблицей ;Вход: файл maket.txt с идентификаторами, среди которых присутствуют десятичные
и шестнадцатеричные константы. ;Выход: вывод информации о десятичных константах на экран.
state_tab struc
last_off dw 0 ;адрес первого байта за концом таблицы
elem_free dw 0 ;адрес первого свободного элемента (Offffh - все занято)
ends
constant struc
state db 0 ;поле состояния элемента таблицы
db 02dh форматирование вывода на экран key db 10 dup (' ') :ключ. он же значение константы
db 02dh форматирование вывода на экран
line db 2 dup (' ') :строка файла, в которой встретилась константа endjine db Odh.Oah.'S' :для удобства вывода на экран ends .data
s_tab state_tab <> tab constant 19 dup (<>)
constant <8.> последний элемент таблицы - бит 3=1 end_tab=$-tab
filename db 'maket.txf.0 handle dw 0 :дескриптор файла buf db 240 dup С ') xlat_tab db Odh dup (OO).Odh ;признак конца строки
db 'O'-Oeh dup (0)
db ':'-'0'+l dup CO') ;признак цифры 0..9
db 'H'-':' dup (0). " :признак буквы 'Н'
db 'h'-'H' dup (0). 'h' ;признак буквы 'h' db Offh-'h1 dup (00)
curjine db 0
.code
:открываем файл mov ah. 3dh
movdx,offset filename
int 21h
jc exit :ошибка (cf=l)
mov handle.ax ;читаем файл:
movah.3fh :функция установки указателя
mov bx,handle
mov ex,240 ;читаем максимум 240 байт
lea dx.buf
int 21h
jc exit
mov ex.ax фактическая длина файла в сх инициализируем дескриптор таблицы s_tab
lea si.tab :адрес таблицы в si
mov s_tab.elem_fгее.si ;первый элемент таблицы - свободен
add si .end_tab
mov s_tab.last_off,si :адрес первого байта за концом таблицы
lea bx.xlat_tab
leadi. buf
cканируем до первого пробела: push ds popes
cycll: mov al. ' ' repne scasb -.сканирование до первого пробела
jcxz displ .цепочка отсканирована -> таблица заполнена push сх :классифицируем символ после пробела (команда XLAT):
mov al.[di]
xlat
emp al. '0':первый символ после пробела - 0
je ml
cmpal.Odh :первый символ после пробела - Odh
je m2 :все остальное либо идентификаторы, либо неверно записанные числа
pop сх
jmpcycll ml: :первый символ после пробела - 0..9:
mov si.di ;откуда пересылать
fmov al. ' ' push di repne scasb сканирование до первого пробела mov cx.di dec ex subcx.si ;сколько пересылать lea di.tab emp s__tab.elem_free.0ffffh :есть свободные элементы? je displ : свободных элементов нет
mov di,s_tab.elem_free :адрес первого свободного элемента push di
lea di.[di].key rep movsb ;пересыпаем в элемент таблицы
deed! ;Какого типа это константа?
emp byte ptr [di].'h' popdi
je m4
and [di].state.Ofbh ;десятичная константа
jmp $+5 m4: or [di].state.100b ;шестнадцатеричная константа
mov al ,cur_line :текущий номер строки в al
aam преобразуем в символьное представление
or ah.030h
mov [di].line,ah
or al.030h
mov [di+1].line.al :и в элемент таблицы
or [di].state.lb ;помечаем элемент как используемый
:теперь нужно поместить в поле s_tab.elem_free адрес нового свободного элемента пб: emp di. s_tab. 1 ast_of f
ja displ
add di.type constant :к следующему элементу
test [di].state.lb
jnz m5 ; используется => к следующему элементу
mov s_tab.elem_fгее.d i pop di pop ex
jmpcycU m2: увеличить номер строки
inc cur_line
jmp cycl 1 displ: :отображение на экране элементов таблицы
lea di .tab m6:
test [di].state.100b
jnz m7 :выводим на экран строку
mov ah.9
mov dx.di
int 21h ml: add di. type constant
emp di ,s_tab.last_off
jb m6

В этой программе показаны основные приемы работы с неупорядоченной таблицей. Усложнить данную программу можно, например, дополнив набор сведений о каждой константе информацией о ее длине. Далее можно попрактиковаться в этом вопросе, поставив себе задачу удалить из таблицы данные о нулевых константах (обоих типов — 0 и 0h) и вывести окончательное содержимое таблицы на экран. В качестве ключа для доступа к ячейке таблицы можно использовать значение самой константы (размером 10 байт).
Обратите внимание на еще два момента, отраженные в этой программе.

  • Использование команды XLAT для классификации символов в качестве цифр и остальных символов (букв). Среди других кодов в таблице перекодировки особо выделен байт со значением Odh, который является первым байтом в паре OdOah. Как вы помните, эта пара вставляется редакторами ASCII-текстов для обозначения конца строки и перехода на другую строку.
  • Организация таблицы. Ей предшествует четырехбайтовый префикс, содержащий информацию о конце таблицы и первом свободном элементе таблицы. В целях оптимизации область памяти для буфера buf и таблицы перекодировки лучше выделять динамически и после анализа файла удалять.

Древовидные таблицы

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

Рис. 2.7. Древовидная организация таблицы

Процесс добавления нового элемента в древовидную таблицу производится следующим образом. Значение ключа нового элемента сравнивается со значением ключа первого элемента дерева. По результатам сравнения ключей новый элемент «поступает» в левое или правое поддерево первого элемента дерева. Далее процесс сравнения и поступления в левое или правое поддерево очередного узла продолжается аналогичным образом и до тех пор, пока не будет найден пустой элемент в нужном направлении просмотра. Новая запись помещается в данный пустой элемент, при этом настраивается указатель с адресом узла-предка.
Преимущества древовидной структуры таблицы проявляются на этапе поиска нужного элемента. Сам процесс подобен действиям на этапе вставки нового элемента в таблицу, являясь при этом в отличие от неупорядоченного поиска целенаправленным. Поиск прекращается либо при совпадении ключевого поля, либо при обнаружении пустого указателя, то есть это означает, что нужного элемента в таблице нет.
Самое сложное в использовании древовидных таблиц — реализация процесса удаления элементов, так как при этом необходимо производить их переупорядочивание.
Реализовать таблицу древовидной организации можно как с использованием полей указателей (см. выше), так и без них. Если отказаться от полей указателей, то таблица может представлять собой массив из п структур, которые пронумерованы от 0 до п-1. Тогда структура с ключом, соответствующим корню дерева, должна располагаться на месте структуры с номером m = [(n-1)/2], где скобки [] означают целую часть от деления. Соответственно левое поддерево будет располагаться в левом подмассиве (элементы 0..m-1), а правое — в правом подмассиве (элементы m+1..n-1). Корень левого поддерева — элемент ш,' - [(m-1)/2], корень правого поддерева — элемент mr = [т+1+(п-1)/2] и т. д. Этот способ экономичнее с точки зрения расходования памяти, но сложнее алгоритмически. Предполагается, что соответствующее таблице дерево сбалансировано и максимальное количество элементов в таблице известно.
Наглядный пример организации древовидной таблицы — расстановка слов в лексикографическом порядке. Эта операция выполняется с помощью лексикографических деревьев, понятие о которых приведено ниже — в разделе, посвященном деревьям. Там же приведен соответствующий пример.

Упорядоченные таблицы

Работа с таблицей оказывается более эффективной, если элементы в ней некоторым образом упорядочены. Наиболее часто таблица упорядочивается по значению ключевого поля. Но не исключено и использование другого принципа упорядочивания, к примеру по частоте обращения к элементам таблицы и т. п. Упорядочивание таблицы и последующий поиск в ней производится методами, рассмотренными выше. Порядок выполнения этих и других операций с упорядоченной по значению ключа таблицей рассмотрим на следующем примере.
Пусть необходимо запомнить элементы таблицы информацией о 10 словах, вводимых с клавиатуры. Длина слов — не более 20 символов. Структура элемента таблицы следующая: поле с количеством символов в слове; поле с самим словом. После ввода информации о словах необходимо упорядочить элементы таблицы по признаку длины слов. Затем вывести на экран элемент таблицы, содержащий первое из слов длиной 5 символов, удалить этот элемент из таблицы и вставить в нее новое слово, введенное с клавиатуры.
При такой постановке задачи нам придется решить весь комплекс проблем, возникающих при работе с упорядоченными таблицами.

:prg02_06.asm - программа на ассемблере демонстрации работы с упорядоченной таблицей
;Вход: 10 слов, вводимых с клавиатуры. Длина слов - не более 20 символов.
;Выход: вывод на экран элемента таблицы, содержащего первое из слов длиной 5 символов,
удаление этого элемента из таблицы и вставка в нее нового слова, введенного с
клавиатуры.
element tab struc

lendb 0 -.длина слова
simvjd db 20 dup (20h) :само слово
ends
0длина введенного слова (без учета 0dh) К db 21 dup (20h) -.буфер для ввода (с учетом 0dh)регистры
lea si.in_str -.откуда пересылать
Tea di out_str ;куда пересылать
movcx.lenjnovs -.сколько пересылать repmevsb ; пересылаем строку восстанавливаем регистры
tabelement._tab 10 dup (<>)
len_tab=$-tab
buf buf_0ah<>
key db 5
nrev element tab <> ;предыдущий элемент
Г element tab" <> временная переменная для сортировки
.code
leadi.tab lea si .buf .bufjn mov ex. 10 lea dx. buf movah.Oah push ds
mh ;вводим слова с клавиатуры в буфер buf
-.сохраняем регистры .........
intZlh
move1, buf. lenjn
mov [di].len.cl ;длина слова -> tab.length
adddi ,simv id
repmovsb -.пересылка слова в элемент таблицы :восстанавливаем регистры .........
adddi.type element_tab
;Упорядочиваем таблицу методом пузырьковой сортировки
п-10
mov cx.n-1
mov si .1
@@сус11: :внешний цикл - по 1 push ex
subcx'.S! количество повторений внутреннего цикла push si временно сохраним 1 - теперь о=п
mov si .n-1
@@cyc!2: push si
movax.type element_tab
mul si
mov si.ax
mov di .si
sub di.type element_tab :в di адрес предыдущей записи
mov al.[di].len
cmp [si].len.al сравниваем последующий с предыдущим
ja @@ml ;обмениваем
s_movsbx.[di].<type element_tab> :x=mas[j-l]
s_movsb[di].[si].<type element_tab> ;mas[j-l]»mas[j] ;mas[j]=x push ex
mov ex.type element_tab :сколько пересылать
mov di ,si
lea si.x юткуда пересылать repmovsb : пересылаем строку pop ex @@ml: pop si
dec si :цикл по j с декрементом n-i раз
loop @@cycl2 pop si
inc si pop ex
dec ex
jcxz in2
jmp @@cycl 1
m2: ;ищем элемент путем двоичного поиска :в si и di индексы первого и последнего элементов последней просматриваемой части
последовательности;
mov si.0
mov di.n-1
;получим центральный индекс: cont_search: cmp si .di :проверка на окончание (неуспешное):si>di
ja exit
mov bx.si
add bx.di
shr bx.l ; делим на 2 push bx
movax.type element_tab
mul bx
mov bx.ax
mov al.key :искомое значение в ах
cmp [bx].Ten.al сравниваем с искомым значением
je @@rn4 ; искомое значение найдено
ja ШтЗ :[bx].len>k ;[bx].len<k: popbx
mov si ,bx
inc si
jmpcont_search @@m3: pop bx :1
mov di .bx
dec di
jmp cont_search @@m4: movax.type element_tab
mul si
mov si.ax
: конец поиска - в si адрес элемента таблицы со словом длиной 5 байт :выводим его на экран :преобразуем длину в символьный вид:
mov al, [si]. Ten
хог ex.ex
movcl.al ;длина для movsb
aam
or ах. ;в ах длина в символьном виде
mov buf. 1 en_buf.ah
mov buf .lerMn.al push si :сохраним указатель на эту запись
add si .simvjd
lea di .buf.buf_in rep movsb
mov byte ptr [di].'$' :конец строки для вывода 09h (int 21h) :теперь выводим:
lea dx.buf
mov ah.09h
int 21h
:удаляем запись pop si :-i- восстановим указатель на удаляемую запись
mov di .si
add si .type element_tab
mov cx.len_tab
sub ex.si ;в сх сколько пересылать rep movsb :обнуляем последнюю запись
xor al .al
mov ex.type element_tab rep stosb
:вводим слово с клавиатуры: insert: ;вводим слово с клавиатуры lea dx.buf mov ah.0ah int21h :c помощью линейного поиска ищем место вставки, в котором выполняется условие buf.1еn_
in=<[si].Ten
lea si.tab
mov al .buf.len_in @@m5: emp al,[si].Ten
jbe @@m6
add si .type element_tab
jmp @@m5
@@m6: push si сохраняем место вставки :раздвигаем таблицу, последний элемент теряется
add si.type element_tab
mov cx.len_tab
sub ex.si ;сколько пересылать std
lea si .tab
add si , lentab
mov di.si
sub si.type element_tab rep movsb eld
;формируем и вставляем новый элемент pop di восстанавливаем место вставки :обнуляем место вставки push di
хог al .al
mov ex.type element_tab rep stosb popdi
lea si ,buf .bufjn
mov cl .buf .lenjn
mov [di].len,cl
add di ,simv_id rep movsb

Таблицы с вычисляемыми входами

Ранее мы отмечали, что скорость доступа к элементам таблицы зависит от двух факторов — способа организации поиска нужного элемента и размера таблицы. Для маленьких таблиц любой метод доступа будет работать быстро. С ростом размера таблицы выбор способа организации доступа приходится производить прежде всего исходя из критерия скорости локализации нужного элемента таблицы. Элементы таблицы отличаются друг от друга уникальным значением ключевого поля. При этом ключевыми могут являться не только одно, но и несколько полей элемента таблицы. Ключ, в том числе и символьный, в памяти представляется последовательностью двоичных байт. Исходя из того что ключ уникален, соответствующая двоичная последовательность также будет уникальной. А нельзя ли использовать это уникальное значение ключа для вычисления адреса местоположения элемента в таблице? Оказывается, можно, а в ряде приложений это оказывается очень эффективно, так как в идеальном случае доступ к нужному элементу таблицы осуществляется всего за одно обращение к памяти. С другой стороны, на практике часто возникает необходимость размещения элементов таблицы с достаточно большим диапазоном возможных значений ключевого поля, в то время как программа реально использует лишь небольшое подмножество значений этих ключей. Например, значение ключевого поля может быть в диапазоне 0..3999, но задача постоянно использует не более 50 значений. В этом случае крайне неэффективным было бы резервировать память для таблицы размером в 4000 элементов, а реально использовать чуть больше 1 % отведенной для нее памяти. Гораздо лучше иметь возможность воспользоваться некоторой процедурой, отображающей задействованное пространство ключей на таблицу размером, близким к значению 50. Большинство подобных задач решается с использованием методики, называемой хэшированием. Ее основу составляют различные алгоритмы отображения значения ключа в значение адреса размещения элемента в таблице. Непосредственное преобразование ключа в адрес производится с помощью функций расстановки, или хэш-функций. Адреса, получаемые из ключевых слов с помощью хэш-функций, называются хэш-адресами. Таблицы, для работы с которыми используются методы хеширования, называются таблицами с вычисляемыми входами, хэш-таблицами, или таблицами с прямым доступом.
Основополагающую идею хэширования можно пояснить на следующем примере. Предположим, необходимо подсчитать, сколько раз в тексте встречаются слова, первый символ которых — одна из букв английского алфавита (или русского — это не имеет значения, можно в качестве объекта подсчета использовать любой символ из кодовой таблицы). Для этого в памяти нужно организовать таблицу, количество элементов в которой будет соответствовать количеству букв в алфавите. Далее необходимо составить программу, в которой текст будет анализироваться с помощью цепочечных команд. При этом нас интересуют разделяющие слова пробелы и первые буквы самих слов. Так как символы букв имеют определенное двоичное значение, то на его основе вычисляется адрес в таблице, по которому располагается элемент, в минимальном варианте состоящий из одного поля. В этом поле ведется подсчет количества слов в тексте, начинающихся с данной буквы. В следующей программе с клавиатуры вводится 20 слов (длиной не более 10 символов), производится подсчет английских слов, начинающихся с определенной строчной буквы, и результат подсчета выводится на экран. Хэш-функция (функция расстановки) имеет вид:

A=(C-97)*L,

где А — адрес в таблице, полученный на основе двоичного значения символа С; L — длина элемента таблицы (для нашей задачи L=l); 97 — десятичное смещение в кодовой таблице строчного символа «а» английского алфавита.

:prg02_07.asm - программа на ассемблере для подсчета количества слов, начинающихся
с определенной строчной буквы
:Вход: ввод с клавиатуры 20 слов (длиной не более 10 символов). -.Выход: вывод результата подсчета на экран.
buf_Oahstruc
len_bufdb 11 ; длина bufjn
lenjin db 0 действительная длина введенного слова (без учета Odh) bufjn db 11 dup (20h) -.буфер для ввода (с учетом Cdh) ends 1 .data
tabdb 26 dup (0) buf buf_0ah<>
db Odh.Oah,'$' ;для вывода функцией 09h (int 21h)
.code
-.вводим слова с клавиатуры
mov ex,20
lea dx.buf
movah.Oah ml: int 21h :анализируем первую букву введенного слова - вычисляем хэш-функцию: А=С*1-97
mov Ы , buf .bufjn sub Ы. 97 inc [bx] loop ml
:выводим результат подсчета на экран push ds popes
xor al ,al
lea di ,buf
mov ex.type bufjah rep stosb ; чистим буфер buf
mov ex.26 :синвол в buf.buf_1n
lea dx.buf
mcv Ы,97 m2: push bx
mov buf .bufjn.bi :опять вычисляем хэш-функцию:
А»С*1-97 и преобразуем "количество" в символьный вид
sub Ы. 97
mov al .[bx]
aam
or ax,03030h ;в ах длина в символьном виде
mov buf.len in.al —
mov buf.len_buf.ah ;теперь выводим:
mov ah, 09h
int 21h pop bx
inc Ы
loop m2

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

  • Разработка компиляторов, программ обработки текстов, пользовательских интерфейсов и т. п. В частности, компиляторы значительную часть времени обработки исходного текста программы затрачивают на работу с различными таблицами — операций, идентификаторов, констант и т. д. Правильная организация работы компилятора с информацией в этих таблицах означает значительное увеличение скорости создания объектного модуля, может быть, даже не на один порядок выше. Кстати, другие системные программы — редакторы связей и загрузчики — также активно работают со своими внутренними таблицами.
  • Системы управления базами данных. Здесь особенный интерес представляют алгоритмы выполнения операций поиска по многим ключам, которые также основаны на методе хэширования.
  • Разработка криптографических систем.
  • Поиск по соответствию. Методы хэширования можно применять в системах распознавания образов, когда идентификация элемента в таблице осу ществляется на основе анализа ряда признаков, сопровождающих объект поиска, а не полного соответствия заданному ключу. Если рассматривать эту возможность в контексте задач системного программирования, то ее можно использовать для исправления ошибок операторов при вводе информации в виде ключевых слов. Подробная информация о поиске по соответствию приведена в литературе.

Но на практике не все так гладко и оптимистично. Для эффективной и безотказной работы метода хэширования необходимо очень тщательно подходить как к изучению задачи на этапе ее постановки, так и к возможности использования конкретного алгоритма хэширования в контексте этой задачи. Так, на стадии изучения постановки задачи, в которой для доступа к табличным данным планируется использовать хэширование, требуется проводить тщательные исследования по направлениям: диапазон допустимых ключей, максимальное количество элементов в таблице, вероятность возникновения коллизий и т. д. При этом нужно знать как общие проблемы метода хэширования, так и частные проблемы конкретных алгоритмов хэширования. Одну из таких проблем рассмотрим на примере задачи.
Пусть необходимо подсчитать количество двухсимвольных английских слов в некотором тексте. В качестве хэш-функции для вычисления адреса можно предложить функцию подсчета суммы двух символов, умноженной на длину элемента таблицы: A=(Cl+C2)*L-97, где А — адрес в таблице, полученный на основе суммы двоичных значений символов С1 и С2; L — длина элемента таблицы; 97 — десятичное смещение в кодовой таблице строчного символа «а» английского алфавита. Проведем простые расчеты. Сумма двоичных значений двух символов 'а' равна 97+97=194, сумма двоичных значений двух символов 'г' равна 122+122=244. Если организовать хэш-таблицу, как в предыдущем случае, то получится, что в ней должно быть всего 50 элементов, чего явно недостаточно. Более того, для сочетаний типа ab и Ьа хэш-сумма соответствует одному числовому значению. В случае когда функция хэширования вычисляет одинаковый адрес для двух и более различных объектов, говорят, что произошла коллизия, или конфликт. Исправить положение можно введением допущений и ограничений, вплоть до замены используемой хэш-функции.
Программист может либо применить один из известных алгоритмов хэширования (что, по сути, означает использование определенной хэш-функции), либо изобрести свой алгоритм, наиболее точно отображающий специфику конкретной задачи. При этом необходимо понимать, что разработка хэш-функции происходит в два этапа.

  1. Выбор способа перевода ключевых слов в числовую форму.
  2. Выбор алгоритма преобразования числовых значений в набор хэш-адресов.

Выбор способа перевода ключевых слов в числовую форму

Вся информация, вводимая в компьютер, кодируется в соответствии с одной из систем кодирования (таблиц кодировки). В большинстве таких систем символы (цифры, буквы, служебные знаки) представляются однобайтовыми двоичными числами. В последних версиях Windows (NT, 2000) используется система кодировки Unicode, в которой символы представляются в виде двухбайтовых двоичных величин. Как правило, ключевые поля элементов таблиц — строки символов, Наиболее известные алгоритмы закрытого хэширования основаны на следующих методах :

  • деления;
  • умножения;
  • извлечения битов;
  • квадрата;
  • сегментации;
  • перехода к новому основанию;
  • алгебраического кодирования;
  • вычислении значения CRC (см. соответствующую главу).

Далее мы рассмотрим только первые четыре метода. Остальные методы — сегментации, перехода к новому основанию, алгебраического кодирования — мы рассматривать не будем. Отметим лишь, что их используют либо в случае значительной длины ключевых слов, либо когда ключ состоит из нескольких слов. Информацию об этих методах можно получить в литературе .
Рассмотрение методов хэширования будет произведено на примере одной задачи. Это позволит лучше понять их особенности, преимущества, недостатки и возможные ограничения.
Необходимо разработать программу — фрагмент компилятора, которая собирает информацию об идентификаторах программы. Предположим, что в программе может встретиться не более М различных имен. Длину возможных имен ограничим восьмью символами. В качестве ключа используются символы идентификатора, какие и сколько — будем уточнять для каждого из методов. Элемент таблицы состоит из 10 байт: 1 байт признаков, 1 байт для хранения длины идентификатора и 8 байт для хранения символов самого идентификатора.

Метод деления

Этот простой алгоритм закрытого хэширования основан на использовании остатка деления значения ключа К на число, равное или близкое к числу элементов таблицы М:

А(К) = К mod M

В результате деления образуется целый остаток А(К), который и принимается за индекс блока в таблице. Чтобы получить конечный адрес в памяти, нужно полученный индекс умножить на размер элемента в таблице. Для уменьшения коллизий необходимо соблюдать ряд условий:

  • Значение М выбирается равным простому числу.
  • Значение М не должно являться степенью основания, по которому производится перевод ключей в числовую форму. Так, для алфавита, состоящего из первых пяти английских букв и пробела {a,b,c,d,e,' '} (см. пример выше), основание системы равно 6. Исходя из этого число элементов таблицы М не должно быть степенью 6Р.

Важно отметить случай, когда число элементов таблицы М является степенью основания машинной систем счисления (для микропроцессора Intel — это 2). Тогда операция деления (достаточно медленная) заменяется на несколько операций.

Метод умножения

Для этого метода нет ограничений на длину таблицы, свойственных методу деления. Вычисление хэш-адреса происходит в два этапа:
1. Вычисление нормализованного хэш-адреса в интервале [0..1] по формуле:

F(K) = (С*К) mod 1,

где С — некоторая константа из интервала [0..1], К — результат преобразования ключа в его числовое представление, mod 1 означает, что F(K) является дробной частью произведения С*К.
2. Конечный хэш-адрес А(К) вычисляется по формуле А(К) = [M*F(K)], где М — размер хэш-таблицы, а скобки [] означают целую часть результата умножения.
Удобно рассматривать эти две формулы вместе:

А(К) = М*(С*К) mod 1. (2.4)

Кнут в качестве значения С рекомендует использовать «золотое сечение» — величину, равную ((л/5)-1)/2«0,6180339887. Значение F(K) можно формировать с помощью как команд сопроцессора, так и целочисленных команд. Команды сопроцессора вам хорошо известны и трудностей с реализацией формулы (2.4) не возникает. Интерес представляет реализация вычисления А(К) с помощью целочисленных команд. Правда, в отличие от реализации сопроцессором здесь все же Удобнее ограничиться условием, когда М является степенью 2. Тогда процесс вычисления с использованием целочисленных команд выглядит так:

Выполняем произведение С*К. Для этого величину «золотого сечения» С~0,6180339887 нужно интерпретировать как целочисленное значение,
обходимо стремиться к тому, чтобы появление 0 и 1 в выделяемых позициях было как можно более равновероятным. Здесь трудно дать рекомендации, просто нужно провести анализ как можно большего количества возможных ключей, разделив составляющие их байты на тетрады. Для формирования хэш-адреса нужно будет взять биты из тех тетрад (или полностью тетрады), значения в которых изменялись равномерно.

Метод квадрата

В литературе часто упоминается метод квадрата как один из первых методов генерации последовательностей псевдослучайных чисел. При этом он непременно подвергается критике за плохое качество генерируемых последовательностей. Но, как упомянуто выше, для процесса хэширования это не является недостатком. Более того, в ряде случаев это наиболее предпочтительный алгоритм вычисления значения хэш-функции. Суть метода проста: значение ключа возводится в квадрат, после чего берется необходимое количество средних битов результата. Возможны варианты — при различной длине ключа биты берутся с разных позиций. Для принятия решения об использовании метода квадрата для вычисления хэш-функции необходимо провести статистический анализ возможных значений ключей. Если они часто содержат большое количество нулевых битов, то это означает, что распределение значений битов в средней части квадрата ключа недостаточно равномерно. В этом случае использование метода квадрата неэффективно.
На этом мы закончим знакомство с методами хэширования, так как полное обсуждение этого вопроса не является предметом книги. Информацию об остальных методах (сегментации, перехода к новому основанию, алгебраического кодирования) можно получить из различных источников.
В ходе реализации хэширования с помощью методов деления и умножения возможные коллизии мы лишь обозначали без их обработки. Настало время разобраться с этим вопросом.

Обработка коллизий

Для обработки коллизий используются две группы методов:

  • закрытые — в качестве резервных используются ячейки самой хэш-таблицы;
  • открытые — для хранения элементов с одинаковыми хэш-адресами используется отдельная область памяти.

Видно, что эти группы методов разрешения коллизий соответствуют классификации алгоритмов хэширования — они тоже делятся на открытые и закрытые. Яркий пример открытых методов — метод цепочек, который сам по себе является самостоятельным методом хэширования. Он несложен, и мы рассмотрим его несколько позже.
Закрытые методы разрешения коллизий более сложные. Их основная идея — при возникновении коллизии попытаться отыскать в хэш-таблице свободную ячейку. Процедуру поиска свободной ячейки называют пробитом, или рехэшировани-ем (вторичным хэшированием). При возникновении коллизии к первоначальному хэш-адресу А(К) добавляется некоторое значение р, и вычисляется выражение (2.5). Если новый хэш-адрес А(К) опять вызывает коллизию, то (2.5) вычисляется при р2, и так далее:

А(К) = (A(K)+Pi)mod М (I = 0..М). (2.5)
push ds popes
lea si .buf.len_in
mov cl .buf .lenjn
inccx :длину тоже нужно захватить
add di .lenjd repmovsb
jmp ml displ: :выводим идентификатор, вызвавший коллизию, на экран
рехэширование
;ищем место для идентификатора, вызвавшего коллизию в таблице, путем линейного рехэширования i nc bx mov ax.bx jmp m5

Квадратичное рехэширование

Процедура квадратичного рехэширования предполагает, что процесс поиска резервных ячеек производится с использованием некоторой квадратичной функции, например такой:

Pi = а,2+Ь,+с. (2.6)

Хотя значения а, Ь, с можно задавать любыми, велика вероятность быстрого зацикливания значений р(. Поэтому в качестве рекомендации опишем один из вариантов реализации процедуры квадратичного рехэширования, позволяющий осуществить перебор всех элементов хэш-таблицы [32]. Для этого значения в формуле (2.6) положим равными: а=1,Ь = с = 0. Размер таблицы желательно задавать равным простому числу, которое определяется формулой М = 4п+3, где п — целое число. Для вычисления значений р> используют одно из соотношений:

pi = (K+i2)modM. (2.7)
Pi = [M+2K-(K+i2)modM]modM. (2.8)

где i = 1, 2, ..., (M-l)/2; К — первоначально вычисленный хэш-адрес.

Адреса, формируемые с использованием формулы (2.7), покрывают половину хэш-таблицы, а адреса, формируемые с использованием формулы (2.8), — вторую половину. Практически реализовать данный метод можно следующей процедурой.

  1. Задание I = -М.
  2. Вычисление хэш-адреса К одним из методов хэширования.
  3. Если ячейка свободна или ключ элемента в ней совпадает с искомым ключом, то завершение процесса поиска. Иначе, 1:=1+1.
  4. Вычисление h := (h+|i|)modM.
  5. Если I < М, то переход к шагу 3. Иначе (если I > М), таблица полностью заполнена.

Программа та же, что приведена в методе линейного рехэширования, за исключением добавления одной команды для инициализации процесса рехэширования, самого фрагмента рехэширования и небольших изменений сегмента данных. могут являться методы, основанные на деревьях поиска, и т. п. Наибольший эффект от хеширования — при поиске по заданным идентификаторам или дескрипторам, что характерно для задач баз данных, обработки документов и т. д. Для задач, в которых поиск ведется сравнением или вычислением сложных логических функций, лучше использовать традиционные методы сортировки и поиска.
Для того чтобы совершить плавный переход к рассмотрению следующей структуры данных — спискам, вернемся еще раз к одной проблеме, связанной с массивами. Упоминалось, что среди массивов можно выделить массивы специального вида, которые называют разреженными. В этих массивах большинство элементов равны нулю. Отводить место для хранения всех элементов расточительно. Естественно, возникает желание сэкономить. Что для этого можно предпринять?
Техника обработки массивов предполагает, что все элементы расположены в соседних ячейках памяти. Для ряда приложений это недопустимое ограничение.
Обобщенно можно сказать, что все перечисленные выше структуры имеют общие свойства:

  • постоянство структуры данных на всем протяжении ее существования;
  • память для хранения отводится сразу всем элементам структуры и все элементы находятся в смежных ячейках памяти;
  • отношения между элементами просты настолько, что можно исключить потребность в средствах хранения информации об их отношениях в какой бы то ни было форме.

Исходя из этих свойств данные структуры данных и называют статическими. Снять подобные ограничения можно, используя другой тип данных — списки. Для них подобных ограничений не существует.


Сложные структуры данных (Часть 2)

Продавай мобильный контент со своего сайта

Сложные структуры данных (Часть 2)

  • Список
    • Последовательные списки
    • Связные списки
  • Сеть
    • Создание двусвязного списка состояний, конечного автомата
    • Создание односвязного списка переходов для состояния конечного автомата
  • Дерево
    • Представление дерева в памяти
    • Лексикографическое дерево
  • Элементы компиляции программ
    • Формальное описание языка программирования
    • Описание процесса трансляции программы

Список

Если у веревки есть один конец,
значит, у нее должен быть и другой.
Закон Микша (Прикладная Мерфология)

В общем случае под списком понимается линейно упорядоченная последовательность элементов данных, каждый из которых представляет собой совокупность одних и тех же полей. Упорядоченность элементов данных может быть двоякой:

  • элементы списка располагаются последовательно как в структуре представления, так и в структуре хранения — список такого типа называется последовательным;
  • порядок следования элементов определяется с помощью специальных указателей, которые расположены в самих элементах и определяют их соседей справа и/или слева — подобные списки называются связными.

Последовательные списки

Если количество элементов в списке постоянно, то в зависимости от их типа список вырождается в вектор, массив, структуру или таблицу. Отличие списка от этих структур данных — в длине. Для списков она является переменной. Поэтому программист, разбираясь с самими списками, должен уделить внимание тому, каким образом ему следует выделять память для хранения списка. Обычно языки высокого уровня имеют соответствующие средства в отличие от языка ассемблера. При написании программы на ассемблере программист должен уметь напрямую обратиться к операционной системе для решения проблемы динамического управления памятью. В примерах мы будем использовать соответствующие функции API Win32 как более современные, хотя для общего случая это не принципиально. В MS DOS имеются аналогичные функции (с точки зрения цели использования) — это функции 48h, 49h, 4ah прерывания 21h. Вопрос динамического управления памятью в силу своей важности в контексте настоящего рассмотрения требует особого внимания.
Отличие последовательных списков от связных состоит в том, что добавление и удаление элементов возможно только по концам структуры. В зависимости от алгоритма изменения различают такие виды последовательных списков, как стеки, очереди и деки. Эти виды списков обычно дополняются служебным дескриптором, который может содержать указатели на начало и конец области памяти, выделенной для списка, указатель на текущий элемент.
Зачем нужен, к примеру, стек? Необходимость использования своего стека в программе вполне вероятна, хотя бы исходя из того, что элементы, для хранения которых он создается, могут иметь размер, отличный от 2/4 байта.
Важно понимать, что внутренняя структура элемента практически не зависит от типа последовательного списка. Списки отличает набор операций, которые производятся над ними. Поэтому рассмотрим их отдельно для каждого типа последовательного списка.

Стек

Стек — последовательный список, в котором все включения и исключения элементов производятся на одном его конце — по принципу LIFO (Last In First Out — последним — пришел первым ушел). Для стека определены следующие операции:

  • создание стека;
  • включение элемента в стек;
  • исключение элемента из стека;
  • очистка стека;
  • проверка объема стека (числа элементов в стеке);
  • удаление стека.

Создание стека должно сопровождаться инициализацией специального дескриптора стека, который может содержать следующие поля: имя стека, адреса нижней и верхней границ стека, указатель на вершину стека.
Иллюстрацию организации и работы стека произведем на примере задачи, анализирующей правильность согласования скобок в некотором тексте. Причем условимся, что скобки могут быть нескольких видов: (), {}, [], <>. Программа реализована в виде приложения Win32 с использованием функций API Win32 для работы с кучей (из нее выделяется память для стека).

mov ecx,l_string
lea ebx.string
jmp cycl
e_xit: jmp exit cycl: jcxz e_xit
cmp byte ptr [ebx]."("
je m_push
cmp byte ptr [ebx],"["
je m_push
cmp byte ptr [ebx],"{"
je m_push
cmp byte ptr [ebx]."<"
je m_push
cmp byte ptr [ebx],")"
jneml извлекаем элемент из вершины стека и анализируем его
TestEmptyStk char_stk.mes_error
pop_stkchar_stk.<offset temp>
cmp temp." ("
jne mes_error
jmp r_next ml: cmp byte ptr [ebx],"]"
jnem2 извлекаем элемент из вершины стека и анализируем его
TestEmptyStk char_stk.mes_error
pop_stkchar_stk.<offset temp>
cmp temp," ["
jnemes_error
jmp r_next m2: cmp byte ptr [ebx],"}"
jnem3 ¦.извлекаем элемент из вершины стека и анализируем его
TestEmptyStk char_stk.mes_err6r
pop_stkchar_stk,<offset temp>
cmp temp."{"
jne mes_error
jmp r_next m3: cmp byte ptr [ebx],">"
jne rjiext извлекаем элемент из вершины стека и анализируем его
TestEmptyStk char_stk.mes_error
pop_stkchar_stk.<offset temp>
cmp temp,"<"
jne mes__error
jmp r_next m_push: включение скобки в стек
pushstk char_stk.ebx r_next:add ebx,char_stk.si ze_i tern
dec ecx
jmp cycl mes_error: :вывод на экран сообщения об ошибке mes_e
jmp exitexit
exit:
определяем стек на пустоту
pop_stkchar_stk,<offset temp> jncmes_error :стек не пуст :вывод на экран сообщения mes_ok
exit_exit: :выход из приложения
delete_stk char_stk :удаляем блок памяти со стеком

Код, выполняющий работу со стеком, оформлен в виде макрокоманд. При необходимости код можно сделать еще более гибким. Для этого нужно использовать функцию API Win32 HeapReAl 1 ос, которая изменяет размер выделенного блока в сторону его увеличения или уменьшения. В принципе, полезной может оказаться операция определения объема стека, то есть количества элементов в нем. Попробуйте реализовать ее самостоятельно.

Очередь

Очередь — последовательный список, в котором включение элементов производится на одном конце, а исключение на другом, то есть по принципу FIFO (First In First Out — первым пришел — первым ушел). Сторона очереди, на которой производится включение элементов, называется хвостом. Соответственно, противоположная сторона — голова очереди. Очередь описывается дескриптором, который может содержать поля: имя очереди, адреса верхней и нижней границ области памяти, выделенной для очереди, указатели на голову и хвост. Набор операций для очереди:

  • создание очереди (create_que);
  • включение элемента в очередь (push_que);
  • исключение элемента из очереди (popque);
  • проверка пустоты очереди (TestEmptyQue);
  • очистка очереди без освобождения памяти для нее (initque);
  • удаление очереди (delete_que).

При помощи очереди удобно моделировать некоторые обслуживающие действия, например это может быть некоторая очередь запросов к критическому ресурсу или очереди заданий на обработку. К очереди так же, как и стеку, применимы такие параметры, как емкость очереди и размер элемента очереди.
На практике используются очереди двух типов — простые и кольцевые. Очередь обслуживается двумя указателями — головы (Р,) и хвоста очереди (Р2). Указатель головы Р идентифицирует самый старый элемент очереди, указатель хвоста — первый свободный слот после последнего включенного в очередь элемента. Сами элементы физически по очереди не двигаются. Меняется лишь значение указателей. При включении в очередь новый элемент заносится в ячейку очереди, на которую указывает Р2. Операция исключения подразумевает извлечение элемента из ячейки очереди, указываемой Р,. Кроме извлечения элемента операция исключения производит корректировку указателя Р, так, чтобы он указывал на очередной самый старый элемент очереди. Таким образом, Указатель хвоста простой очереди всегда указывает на свободную ячейку очере-Ди и рано или поздно он выйдет за границы блока, выделенного для очереди. И это случится несмотря на то, что в очереди могут быть свободные ячейки
(ячейки А. Чтобы исключить это явление, очередь организуется по принципу кольца. В обоих способах организации очереди важно правильно определить ее емкость. Недостаточный объем очереди может привести в определенный момент к ее переполнению, что чревато потерей новых элементов, претендующих на включение в очередь.
Для иллюстрации порядка организации и работы с очередью рассмотрим пример. Пусть имеется строка символов, которая следующим образом моделирует некоторую вычислительную ситуацию: символы букв означают запросы на некоторый ресурс и подлежат постановке в очередь (имеющую ограниченный размер). Если среди символов встречаются символы цифр в диапазоне 1-9, то это означает, что необходимо изъять из очереди соответствующее значению цифры количество элементов. Если очередь полна, а символов цифр все нет, то происходит потеря заявок (символов букв). В нашей программе будем считать, что очередь кольцевая и ее переполнение помимо потери новых элементов приводит к выводу соответствующего сообщения. Для кольцевой очереди возможны следующие соотношения значений указателей Р, и Р2: Pj < Р2, Pj = Р2 (пустая очередь), Р, > Р2. Память для очереди в нашей задаче выделяется динамически средствами API Win32.
Заметим, что исходя из потребностей конкретной задачи можно изменить дисциплину обслуживания очереди. Например, установить, что при переполнении очереди потере подлежат старые заявки (в голове очереди) и т. п.

jb ml cmpa1.39h
ja ml
xor есх.есх:удгляем из очереди элементы
mov cl,al
subcl,30h преобразуем символ цифры в двоичный эквивалент ш2: pop_quechar_que.<offset temp>
jc cycl ;если очередь пуста
1 oop m2
jmpcycl ml: добавляем элементы в очередь
mov temp,a 1
push_que char_que. <offset temp>
jnc cycl ;ycnex ;вывод на экран сообщения об ошибке - отсутствие места в очереди
jmp cycl
;выход из приложения exit: :удаляем блок памяти с очередью
delete_que char_que

Как и в случае со стеком, код, выполняющий работу с очередью, оформлен в виде макрокоманд. Программа выдает единственное Сообщение о переполнении очереди. Чтобы наблюдать работу программы в динамике, необходимо выполнить следующие действия.

  1. Загрузить исполняемый модуль программы в отладчик td32.exe.
  2. Отследить адрес начала блока памяти, который будет выделен системой по нашему запросу для размещения очереди. Для этого подвергните пошаговому выполнению в окне CPU содержимое макрокоманды createque.
  3. Выяснив адрес начала блока для размещения очереди, отобразите его в окне Dump и нажмите клавишу F7 или F8. Не отпуская этой клавиши, наблюдайте за тем, как изменяется состояние очереди.

Дека

Дека — очередь с двумя концами. Дека является последовательным списком, в котором включение и исключение элементов может осуществляться с любого из двух концов списка. Логическая и физическая структуры деки аналогичны обычной очереди, но относительно деки следует говорить не о хвосте и голове, а о правом и левом конце очереди. Реализацию операций включения и исключения в деку следует также дополнить признаками левого или правого конца. В литературе можно встретить понятие дека с ограниченным вводом. Это дека, в которой элементы могут вставляться в одном конце, а удаляться с обоих.

Связные списки

Связный список — набор элементов, каждый из которых структурно состоит из двух частей — содержательной и связующей. Содержательная часть представляет собой собственно данные или указатель на область памяти, где эти данные
содержатся. Связующая часть предназначена для связи элементов в списке и определяет очередность их следования. Благодаря связующей части в каждом из элементов становятся возможными «путешествия» по списку путем последовательных переходов от одного элемента к другому.
В отличие от последовательного списка связный список относится к динамическим структурам данных. Для них характерны непредсказуемость в числе элементов, которое может быть нулевым, а может и ограничиваться объемом доступной памяти. Другая характеристика связного списка — необязательная физическая смежность элементов в памяти.
Относительно связующей части различают следующие типы связных списков.

  • Односвязный список — список, начало которого определяется указателем начала, а каждый элемент списка содержит указатель на следующий элемент. Последний элемент должен содержать признак конца списка или указатель на первый элемент списка. В последнем случае мы имеем одно-связный кольцевой список. Преимущество последнего состоит в том, что просмотр всех элементов списка можно начинать с любого элемента, а не только с головы. Более того, в случае кольцевого односвязного списка указателем его начала может быть любой элемент списка. Этот вид списка всегда линейный, так как однозначно задает порядок своего просмотра.
  • Двусвязный список — список, связующая часть которого состоит из двух указателей — на предыдущий и последующий элементы. Начало и конец списка определяются отдельными указателями. Последние элементы дву-связного списка в каждом из направлений просмотра содержат признаки конца. Если замкнуть эти указатели друг на друга, то мы получим кольцевой двусвязный список. В этом случае потребность в указателе конца списка отпадает. Этот вид списка может быть как линейным (иметь одинаковый порядок просмотра в обоих направлениях), так и нелинейным (второй указатель каждого элемента списка задает порядок просмотра, который не является обратным порядку, устанавливаемому первым указателем).
  • Многосвязный список — список, связующая часть элементов которого в общем случае содержит произвольное количество указателей. В этом виде списка каждый элемент входит в такое количество односвязных списков, сколько он имеет в себе полей связи.

В общем случае для связанных списков определены следующие операции:

  • создание связного списка;
  • включение элемента в связный список, в том числе и после (перед) определенным элементом;
  • доступ к определенному элементу связного списка (поиск в списке);
  • исключение элемента из связного списка;
  • упорядочение (перестройка) связного списка;
  • очистка связного списка;
  • проверка объема связного списка (числа элементов в связном списке);
  • объединение нескольких списков в один;
  • разбиение одного списка на несколько;
  • копирование списка;
  • удаление связного списка.

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

Односвязные списки

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

:prg3_10.asm - пример реализации односвязных списков с помощью двух массивов
:Вход: массивы с данными и указателями.
:Выход: нет. В динамике работа программы наблюдается под управлением отладчика.
.data
masdb 0.55.0.12.0.42.94.0.18.0.06.67.0.58.46 :задаем массив
n=$-mas
point db 0 указатель списка - индекс первого ненулевого элемента в mas
masjraint db n dup (0) определим массив указателей
.code
lea si.mas :b si адрес mas_point
xorbx.bx :b bx будут индексы - кандидаты на включение в массив указателей :ищем первый ненулевой элемент
movcx,n-l cycll: cmpbyte ptr [si][bx].O
jneml :если нулевые элементы
inc bx
loop cycll
jmpexit ;если нет ненулевых элементов ml: mov point.Ы :запоминаем индекс первого ненулевого в point
movdi.bx ;в di индекс предыдущего ненулевого ;просматриваем далее (сх тот же)
inc bx сус12: cmpbyte ptr [si][bx].O
je m2 ;нулевой => пропускаем :формируем индекс
movmas_point[di].bl ;индекс следующего ненулевого в элемент mas_point предыдущего
movdi.bx :запоминаем индекс ненулевого
т2: inc bx
loop cycl2
mov mas_point[di].Offh ;индекс следующего ненулевого в элемент mas_point ;выход :а теперь подсчитаем единичные, не просматривая все. - результат в ах
хог ах.ах
cmp point.0
je exit,
inc ax
mov bl .point cycl3: cmp mas_point[bx].Offh
je exit
inc ax
mov Ы ,mas_point[bx]
jmpcycl3 результат подсчета в ах. с ним нужно что-то делать, иначе он будет испорчен

Если количество нулевых элементов велико, то можно сократить объем хранения данного одномерного массива в памяти (см. ниже материал о разреженных массивах).
Кстати, в качестве еще одного реального примера использования односвязных списков вспомните, как реализован учет распределения дискового пространства в файловой системе MS DOS (FAT).
Далее рассмотрим другой, более естественный вариант организации связанного списка. Его элементы содержат как содержательную, так и связующую части.
Рассуждения относительно содержательной части пока опустим — они напрямую зависят от конкретной задачи. Уделим внимание связующей части. Понятно, что это адреса. Но какие? Можно считать, что существуют два типа адресов, которые могут находиться в связующей части: абсолютные и относительные. Абсолютный адрес в конкретном элементе списка — это, по сути, смещение данного элемента относительно начала сегмента данных или блока данных при динамическом выделении памяти и принципиально он лишь логически связан со списком. Относительный адрес формируется исходя из внутренней нумерации элементов в списке и имеет смысл лишь в контексте работы с ним. Пример относительной нумерации рассмотрен выше при организации списка с помощью двух массивов. Поэтому далее этот способ адресации мы рассматривать не будем, а сосредоточимся на абсолютной адресации.
Для начала разработаем код, реализующий применительно к односвязным спискам основные из обозначенных нами выше операций со связанными списками. Односвязные списки часто формируют с головы, то есть включение новых элементов производится в голову списка.
Первая проблема — выделение памяти для списка. Это можно делать либо статическим способом, зарезервировав место в сегменте данных, либо динамически, то есть так, как мы это делали выше при реализации операций со стеками и очередями. Недостатки первого способа очевидны, хотя в реализации он очень прост. Гораздо интереснее и предпочтительнее второй способ, который предполагает использование кучи для хранения связанного списка. Для придания боль-Шей реалистичности изложению рассмотрим простую задачу: ввести с клавиатуры
символьную строку, ограниченную символом "." (точка), и распечатать ее в обратном порядке.

:prgl5_102.asm - реализация программы инвертирования строки с односвязными списками
:Вход: символьная строка с клавиатуры.
:Выход: вывод на экран инвертированной обратной строки.
;.........
itemjist struc :элемент списка
next dd 0 :адрес следующего элемента
info db 0 содержательная часть (в нашем случае - символ)
ends
.data
mas db 80 dup С ') :в эту строку вводим
mas revdb 80 dup (' ') :из этой строки выводим
len mas rev=$-mas rev
mesl db 'Введите строку символов (до 80) для инвертирования (с точкой на конце):'
len_mesl=$-mesl
.code
списание макрокоманд работы со связанным списком;
createjist macro descr:REQ.head:REQ
: создание списка - формирование головы списка и первого элемента
;.........
endm
addjist macro descr:REQ.head:REQ.H_Head:REQ
добавление элемента в связанный список
endm
createjtem macro descr:REQ.H_Head:REQ
;создание элемента в куче для последующего добавления в связанный список
¦.........
endm
start proc near :точка входа в программу:
:вывод строки текста - приглашения на ввод строки для инвертирования !.........
:вводим строку в mas
:.........
;создаем связанный список createjist itemJist.HeadJist :первый элемент обрабатываем отдельно
leaesi.mas
moval.[esi]
cmp al."."
je rev_out
mov [ebx].info.al
:вводим остальные символы строки с клавиатуры до тех пор. пока не встретится "." cycl: incesi
mov al.[esi]
cmp al,"."
je rev_out
addj i st i temj i st. HeadJ i st. Hand_Head
mov [ebx].info.al
jmp cycl
:вывод строки в обратном порядке rev_out: mov esi.offset mas_rev
mov ebx,HeadJist cycl2: mov al .[ebx].info
mov [esil.al
inc esi
mov ebx.[ebx].next
cmpebx.Offffffffh
jne cycl2 ; выводим инвертированную строку на экран

Недостаток такого способа построения списка — в порядке включения элементов. Хорошо видно, что он обратный порядку поступления элементов. Для исправления данной ситуации можно, конечно, ввести еще один указатель на последний элемент списка. Есть и другой вариант — организация двусвязного списка. Уделим теперь внимание другим операциям с односвязным списком.

Включение в список

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

itemjist struc элемент списка
next dd 0 ; адрес следующего элемента
info db 0 содержательная часть (в нашем случае - символ)
предполагаем, что адрес локализованного элемента находится в регистре ЕВХ.
:а адрес нового элемента - в ЕАХ
mov edx.[ebx].next
mov [eax].next.edx mov [ebx].next.eax

Для включения нового элемента после локализованного возникает проблема, I источник которой в однонаправленности односвязного списка, так как, по определению, проход к предшествующему элементу невозможен. Можно, конечно, включить новый элемент, как показано выше, а затем попросту обменять содержимое этих элементов. К примеру, это может выглядеть так:

itemjist struc :элемент списка
next dd 0 :адрес следующего элемента
info db 0 содержательная часть (в нашем случае - символ)
ends

предполагаем, что адрес локализованного элемента находится в регистре ЕВХ.

:а адрес нового элемента - в ЕАХ
mov edx.[ebx].next mov [eax].next.edx mov edx.[ebx].info mov [eax].info.edx mov [ebx].next.eax
:осталось заполнить поле info нового элемента

Но если производится включение в упорядоченный список, то логичнее работать с двумя указателями — на текущий и предыдущий элементы списка, так как это показано ниже. При этом предполагается, что новый элемент создается описанной в предыдущей программе макрокомандой create_item.

описание элемента списка
item_list struc :элемент списка
next dd 0 ; адрес следующего элемента
info db 0 содержательная часть (в нашем случае - символ)
ends
.data
ins_itern itemjist <.15> вставляемый элемент (поле info содержит значение -
:критерий вставки)
Headjist dd Offffffffh указатель на начало списка (Offffffffh - список пуст) Hand_Head dd 0 ;переменная, в которой хранится дескриптор кучи
.code
;здесь мы инициализировали и работали со списком
;список упорядочен по возрастанию
:ищем место вставки
;1 - выбираем первую ячейку
mov ebx.HeadJist .
хогеах.еах ;в еах будет указатель на предыдущий элемент ;2 последняя ячейка? ml: cmpebx.Offffffffh
je noJtern :список пустой :3 - новая ячейка до очередной выбранной? ovd1.CebxJ.info cmpdl.ins_item.info ja nextjtem cmpeax. jne into ;вставить первым
createjtem i temj i st. H_Head :макрос создания элемента в куче mov Headjist.edx :адрес нового в голову списка
mov [edx].next.ebx ;настройка указателей jmpexit :на выход
вставить внутрь списка
into: createjtem item_list.H_Head :макрос создания элемента в куче mov [еах].next.edx :адрес нового в поле next предыдущего mov [edx].next.ebx :в поле next нового адрес текущего jmp exit :на выход
: выбор очередного элемента nextjtem: moveax.ebx:aflpec текущего в еах mov ebx. [ebx].next jmp ml
:4 - список пуст или нет больше элементов no J tern: cmpeax.O
jne no_empty :список непустой :список пуст
mov Headjlist.edx :адрес нового в голову списка
mov [edx].next.Offffffffh:это будет пока единственный элемент в списке
jmpexit :на выход
no_empty: :список не пуст - новая ячейка в конец списка
mov [еах].next.edx
mov [edx].next.Offffffffh:это будет последний элемент в списке
exit: :общий выход

Исключение из списка

Алгоритм предполагает наличие двух указателей — на текущую и предыдущую ячейки. Для разнообразия этот алгоритм предполагает прямой порядок следования элементов в списке. В качестве упражнения читателю предлагается, если нужно, переписать приводимый ниже фрагмент для обратного порядка следования элементов.

:описание элемента списка
itemjist struc -.элемент списка
next dd 0 :адрес следующего элемента
info db 0 содержательная часть (в нашем случае - символ)
ends
.data
search_b db 0 критерий поиска (поле info экземпляра структуры itemjist)
Headjist dd Offffffffh указатель на начало списка (Offffffffh - список пуст)
.code
:здесь мы инициализировали и работали со списком
:ищеи ячейку, подлежащую удалению ;1 - выбираем первую ячейку
mov ebx.HeadJist
хогеах,еах;в еах будет указатель на предыдущую ячейку ;2 последняя ячейка?
cmpebx.Offffffffh
je no J tem cycl: cmp [ebx].info.search_b сравниваем с критерием поиска
jnechjiextjtem : нашли? ;да. нашли!
cmpeax.
jne no_fist:зто не первая ячейка :первая ячейка (?) »> изменяем указатель на начало списка и на выход
mov edx.[ebx].next
mov Headjist.edx
jmpexit no_fist: mov [eax].next.ebx перенастраиваем указатели -> элемент удален
jmpexit ;на выход
:выбор следующего элемента ch_nextjtem; mov eax.ebx : запомним адрес текущей ячейки в указателе на предыдущую
mov ebx.[ebx].next ;адрес следующего элемента в ebx
jmp cycl
:обработка ситуации отсутствия элемента nojtem:

Остальные обозначенные нами выше операции очевидны и не должны вызвать у читателя трудностей в реализации.
Другой интересный и полезный пример использования односвязных списков — работа с разреженными массивами. Разреженные массивы представляют собой массивы, у которых много нулевых элементов. Представить разреженный массив можно двумя способами: с использованием циклических списков с двумя полями связи и с использованием хэш-таблицы.
С разреженными массивами можно работать, используя методы хэширования. Для начала нужно определиться с максимальным размером хэш-таблицы М для хранения разреженного массива. Это значение будет зависеть от максимально возможного количества ненулевых элементов. Ключом для доступа к элементу хэш-таблицы выступает пара индексов (i,j), где i = l..p, j = l..q. Числовое значение ключа вычисляется по одной из следующих формул
Остальные действия зависят от избранного способа вычисления хэш-функции (см. выше соответствующий раздел о методах хэширования).

Двусвязные списки

Двусвязный список представляет собой список, связующая часть которого состоит из двух полей. Одно поле указывает на левого соседа данного элемента списка, другое — на правого. Кроме этого, со списком связаны два указателя — на голову и хвост списка (рис. 2.13, сверху). Более удобным может быть представление списка, когда функции этих указателей выполняет один из элементов списка, одно из полей связи которого указывает на последний элемент списка (рис. 2.13, снизу).

Рис 2.13. Схемы организации двухсвязных списков

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

start proc near :точка входа в программу ,
:вывод строки текста - приглашение на ввод сроки для инвертирования :вводим строку текста для инвертирования
.-создаем связанный список, для чего инициализируем заголовок списка
create_doubly_li st Doubly_Head_l i st :вводим символы строки с клавиатуры до тех пор. пока не встретится "."
lea esi.mas cycl: mov al .[esi]
cmpal.V
je rev_out
add_li st i tem_l i st.Doubly_Head_li st.Hand_Head
mov [ebx].info.al
incesi
jmp cycl rev_out: ;вывод строки в обратном порядке
mov esi.offset mas_rev
mov ebx.DoublyJHeadJ i st. 1 ast cycl2: mova 1 .[ebx].info
mov [esi].al
incesi
mov ebx,[ebx].prev
cmp [ebx].info.Offh :дошли до последнего элемента списка"?
jnecycl2
:выводим инвертированную строку на экран :......

Включение в список

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

itemjist struc ; элемент списка
prev dd 0 :адрес предыдущего элемента
info db 0 содержательная часть (в нашем случае - символ)
next dd 0 :адрес следующего элемента
ends
;предполагаем, что адрес локализованного элемента находится в регистре ЕВХ.
:а адрес нового элемента - в ЕАХ
push [ebx].next
pop [eax].next :[ebx].next->[eax].next mcv [eax].prev.ebx;адрес предыд. эл-та->[еах].ргеу mov [ebx].next.eax:адрес след. эл-та->[еЬх].пех1
:будьте внимательны - меняем указатель предыд. эл-та в следующем за новым элементом mov ebx.[eax].next;адрес след. эл-та-KebxJ.next mov [ebx].prev.eax;адрес предыд. эл-та->[еЬх].ргеу

Включение элемента перед локализованным выполняется аналогично. Простота работы с двусвязными списками в отличие от односвязных достигается за счет отсутствия проблем с передвижением по списку в обе стороны.

Исключение из списка

Аналогично включению, исключение из двусвязного списка не вызывает проблем и достигается перенастройкой указателей. Фрагмент программы, демонстрирующей эту перенастройку, показан ниже.

itemjist struc ;элемент списка
prev dd 0 ;адрес предыдущего элемента
info db 0 содержательная часть (в нашем случае - символ)
next dd 0 -.адрес следующего элемента
ends
предполагаем, что адрес локализованного элемента находится в регистре ЕВХ
mov eax.[ebx].prev;адрес предыд. эл-та->еах
push [ebx].next
pop [eax].next
mov eax.[ebx].next ;адрес следующ. эл-та->еах
push [ebx].prev
pop [eax].prev

В общем случае одно- и двусвязные списки представляют собой линейные связанные структуры. Но в определенных случаях второй указатель двусвязного списка может задавать порядок следования элементов, не являющийся обратным по отношению к первому указателю (рис. 2.14).

Рис. 2.14. Логическая структура нелинейного двусвязного списка

Сеть

Неважно, что кто-то идет неправильно,
возможно, это хорошо выглядит...
Первый закон Скотта (Прикладная Мерфология)

Выше мы уделили достаточно внимания работе с матрицами и списками, и это сделано не случайно. Еще более обобщая понятие списка, мы придем к определению многосвязного списка, для которого характерно наличие произвольного количества указателей на другие элементы списка. Можно говорить, что каждый конкретный элемент входит в такое количество односвязных списков, сколько у него есть указателей. Многосвязный список получается «прошитым» односвяз-ными списками, поэтому такие многосвязные списки называют прошитыми, или плексами. С помощью многосвязных списков можно моделировать такую структуру данных, как сеть или граф. Частный случай сети — дерево. Рассмотрим варианты представления в памяти компьютера этих структур данных.
Сетью называется кортеж G — (V,E), где V — конечное множество вершин, Е -^ конечное множество ребер, соединяющих пары вершин из множества V. Две вершины и и v из множества V называются смежными, если в множестве Е существует ребро (u, v), соединяющее эти вершины. Сеть может быть ориентированной и неориентированной. Это зависит от того, считаются ли ребра (u, v) и (v, u) разными. В практических приложениях часто ребрам приписываются веса, то есть некоторые численные значения. В этом случае сеть называется взвешенной. Для каждой вершины v в сети есть множество смежных вершин, то есть таких вершин Uj (i = l..n), для которых существуют ребра (v, и:). Это далеко не все определения, касающиеся сети, но для нашего изложения их достаточно, так как его цель — иллюстрация средств ассемблера для работы с различными структурами данных. Поэтому рассмотрим варианты представления сетей в памяти компьютера в виде, пригодном для обработки. Какой из этих вариантов лучше, зависит от конкретной задачи. Мы также не будем проводить оценку эффективности того или иного вида представления.

  • Матрица смежности. Сеть, имеющую М вершин, можно представить в виде матрицы размерностью МхМ. При условии, что вершины помечены v,, v2,..., vm, значение матрицы ау = 1 говорит о существовании ребра между вершинами v, и v,. Иначе говоря, эти вершины являются смежными. Для ориентированной сети матрица смежности будет симметричной.
  • Матрица инцидентности. В основе этого представления также лежит матрица, но строки в ней соответствуют вершинам, а столбцы — ребрам (рис. 2.15). Из рисунка видно, что каждый столбец содержит две единицы в строках, причем одинаковых столбцов в матрице нет.

Рис. 2.15. Представление сети матрицей инцидентности

Рис. 2.16. Представление сети векторами смежности

Векторы смежности. Этот вариант представления также матричный (рис. 2.16). Все вершины пронумерованы. Каждой вершине соответствует строка матрицы, в которой перечислены номера вершин, смежных с данной.
В качестве примера рассмотрим фрагмент сканера для распознавания вещественных чисел в директивах ассемблера dd, dq, dt. Правило описания этих чисел в виде синтаксической диаграммы приведено в уроке 19 «Архитектура и программирование сопроцессора» учебника. Ему соответствует показанное ниже регулярное выражение и детерминированный конечный автомат (рис. 2.17):

(+|-)dd*.dd*e(+|-)dd*

Рис. 2.17. Грамматика языка описания вещественных чисел в директиве dd и соответствующий ей детерминированный конечный автомат

Программа будет состоять из двух частей:

  • построение списка — здесь выполняется построение многосвязного списка по заданному регулярному выражению;
  • обработка входной строки на предмет выяснения того, является ли она правильной записью вещественного числа в директивах ассемблера dd, dq, dt.

При выполнении первого пункта возникает проблема — как задавать элемент многосвязного списка, если в общем случае различные элементы списка могут иметь разное количество связей? Как показано на рисунке, различные состояния имеют разное количество связей. Можно предложить разные подходы к выбору программной реализации этих связей. Выберем следующий. Организуем все исходящие связи каждого конкретного элемента в односвязный список. В этом случае многосвязный список будет содержать элементы двух типов:

  • описывающие состояния автомата — они организованы в двусвязный список;
  • описывающие ссылки или переходы от данного состояния к другим состояниям в зависимости от очередного входного символа — эти ссылки организованы в виде односвязных списков для каждого из состояний.

В случае нелинейной организации двусвязного списка его построение таит ряд проблем. Часть из них снимается при статическом способе построения списка, который для конечных автоматов является наилучшим. При необходимости, используя приведенные ниже структуры, читатель может самостоятельно описать такой список в своем сегменте данных. Далее мы займемся динамическим, более сложным вариантом организации нелинейного списка, реализующим конечный автомат. Напомним, что его задачей является распознавание лексемы — описания шестнадцатеричного числа в директивах ассемблера dd, dq, dt. Для построения нелинейного многосвязного списка разработаем ряд макрокоманд. С их помощью построение проводится в два этапа:

  • создание двусвязного списка состояний конечного автомата;
  • создание односвязных списков переходов.

Создание двусвязного списка состояний конечного автомата

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

;но при необходимости можно создать и доп. кучу с помощью HeapCreate :HANDLE GetProcessHeap (VOID);
call GetProcessHeap
mov Hand_Head.eax :сохраняем дескриптор .запрашиваем и инициализируем блоки памяти из кучи:
xorebx.ebx :здесь будут указатели на предыдущ. элементы
xorecx.ecx ;dl - номер элемента состояния (двоичный) rept N_state
push ecx :LPVOID HeapAlloc(HANDLE hHeap. DWORD dwFlags, DWORD dwBytes);
push type descr ;размер структуры
push 0 :флаги не задаем
push HandJHead ;описатель кучи
call HeapAlloc
mov [eax].prev_state.ebx запоминаем адрес предыдущ. (if ebx=0. то это первый)
movebx.eax запоминаем адрес текущего в ebx
raov [eax].current_state,eax (И в descr.current_state
pop ecx
mov [eax].id_state_state,cl
inc cl endm
указатель на последний элемент списка состояний в поле-указатель на начало списка head
mov head.ebx .восстанавливаем регистры
pop ebx
pop eax endm

Создание односвязного списка переходов для состояния конечного автомата

Также опишем структуру элемента списка переходов и макрокоманду для создания этого элемента. Особенность в том, что в отличие от списка состояний макрокоманда строит не весь список, а только один его элемент. Другая особенность в том, что указатель на полученный односвязный список является ссылкой на последний выделенный элемент списка и именно к этому элементу осуществляется привязка элемента состояния к своему списку переходов, что на самом деле особого значения не имеет.

item_l1st_cross struc ;элемент списка переходов
simbol db 0 :входной символ, при котором автомат переходит
:в состояние ниже (поля id_state_cross и nextjtem) id_state_crossdb 0 идентификатор состояния в списке состояний,
:в которое производится переход
point_state dd 0 ;адрес элемента состояния, в которое производится переход next_item dd 0 :адрес следующего элемента в списке переходов для этого состояния
create_item_cross macro sim:REQ.state:REQ.descr:REQ.head:REQ. Hand_Head:REQ
local ml,@@cycl.exit_m
:создание элемента списка переходов для определенного состояния
:вход:
;регистр ЕВХ - адрес предыдущего (для поля descr.nextjtern),
.-Для первого должен быть равен нулю
:sim - символ ASCII, при поступлении которого производится переход в состояние state
:descr - имя структуры-элемента списка переходов
:state - номер состояния, в которое производится переход
;(если двузначное, то в скобках <>)
:head - имя переменной для хранения указателя на конец списка состояний
;Hand_Head - дескриптор кучи процесса по умолчанию
: выход:
регистр ЕВХ - адрес созданного элемента списка переходов
:флаг cf=l - ошибка: нет такого состояния
сохраняем регистры
push eax
:запрашиваем и инициализируем блок памяти из кучи: :LPVOID HeapAlloc(HANDLE hHeap. DWORD dwFlags. DWORD dwBytes); push type descr ;размер структуры push 0 -.флаги не задаем
push Hand_Head ;описатель кучи call HeapAlloc
mov [eax].next_item.ebx :адрес предыдущего movebx.eax запоминаем адрес текущего
mov [eax].simbol,"&sim" инициализируем поле simbol текущего элемента mov [eax].id_state_cross.state ;номер состояния в поле descr.id_state_cross :теперь нужно определить адрес элемента в списке состояний state для выполнения дальнейших переходов и инициализации поля point_state push ebx mov ebx.head clc
(a@cycl :cmp [ebx].id_state_state,state je ml jc exit_m
mov ebx,[ebx].prev_state ;адрес предыдущего состояния в списке состояний cmpebx.O :последний элемент?
jne @@cycl stc
jmp @@cycl ml: :нашли!
mov [eax].poi nt_state,ebx exitjn: восстанавливаем регистры pop ebx pop eax endm
Далее приведена вспомогательная макрокоманда, которая по номеру состоя-ия определяет адрес соответствующего элемента в списке состояний. def_point_item_state macro N_state:REQ,head:REQ local @(acy,@@ml сохраняем регистры :вход:
:N_state - номер состояния
:head - имя ячейки, где хранится указатель на список состояний :выход: регистр ЕВХ - адрес элемента в списке состояний
mov eax.head
@@су: cmp [eax].id_state_state,N_state :ищем ... je @@ml ;нашли?
moveax,[eax].prev_state ;адрес следующего состояния cmp eax. О последний элемент? jne @@cy
stc ;cf=l если состояния с таким номером не существует
jmp @@су
@@ml: :нашли!
endm

Собственно программа prg02_ll.asm, выполняющая построение и инициализацию конечного автомата для распознавания лексемы вещественного числа в директивах dd, dq и dt, достаточно велика.

Дерево

То, что неясно, следует выяснить. То, что трудно творить, следует делать с великой настойчивостью.
Конфуций Деревом называется сетевая структура, обладающая следующими свойствами:

  • среди всех узлов существует один, который не имеет ребер, входящих от других узлов, — этот узел называется корнем;
  • все узлы, за исключением корня, имеют одно и только одно входящее ребро;
  • ко всем узлам дерева имеется путь, начало которого лежит в корне дерева. Графически дерево изображают так, как показано на рис. 2.18.

Рис. 2.18. Изображение дерева и соответствующего ему связного списка

Следуя терминологии дерева, можно ввести некоторые определения, касающиеся его структуры. Ребра, соединяющие смежные узлы дерева, называются ветвями. Оконечные узлы дерева, которые не ссылаются на другие узлы, называются листьями. Другие узлы дерева, за исключением корня, называются узлами ветвления. Две смежные вершины дерева состоят в «родственных» отношениях. Вершина X, находящаяся на более высоком уровне дерева по отношению к вершине Y, называется отцом. Соответственно, вершина Y по отношению к X называется сыном. Если вершина X имеет несколько сыновей, то по отношению друг к другу последних называются братьями. В принципе, вместо этих терминов можно использовать следующие: родитель, потомок и т. п. Классификация деревьев производится по степени исхода ветвей из узлов деревьев. Дерево называется m-арным, если степень исхода его узлов не превышает значения m.
В практических приложениях широко используется специальный класс деревьев — бинарные деревья. Бинарное дерево — m-арное дерево при m = 2. Это означает, что степень исхода его вершин не превышает 2. В случае когда m равно 0 или 2, имеет место полное бинарное дерево. Важно то, что любое m-арное дерево можно преобразовать в эквивалентное ему бинарное дерево. В свою очередь, в силу ограничений по степени исхода вершин бинарное дерево легче представлять в памяти и обрабатывать. По этой причине мы основное внимание уделим работе с бинарными деревьями.
Перечислим операции, которые обычно выполняются при работе с деревьями:

  • представление дерева в памяти;
  • обход или прохождение дерева (поиск в дереве);
  • включение в дерево и исключение из дерева определенного узла;
  • балансировка дерева и операции со сбалансированным деревом.

Представление дерева в памяти

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

  • Связь узлов в дереве осуществляется таким образом (рис. 2.19, а), что каждый узел-сын содержит ссылку на вышестоящий узел (узел-отца). В этом случае корень будет содержать нулевой указатель на месте соответствующего указателя. Имея такой тип связей узлов в дереве, можно подниматься от нижних уровней дерева к его корню, что может быть полезным при реализации определенных видов синтаксического разбора.
  • Каждый узел дерева содержит указатели на свои поддеревья (рис. 2.19, б), то есть каждый отец ссылается на своих сыновей. Этот способ применим для представления деревьев небольшой арности, например бинарных деревьев. Имея указатель на корень дерева, можно достичь любого узла дерева.
  • Каждый узел дерева ссылается на свои поддеревья с помощью списка, при этом каждый узел содержит два указателя — для представления списка поддеревьев и для связывания узлов в этот список (рис. 2.19, е).

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

Бинарное дерево обычно представляется с помощью второго способа (рис. 2.19, б). Минимально для представления узла достаточно трех полей: содержательной части узла и двух указателей — на левого (старшего) и правого (младшего) сыновей. Таким образом, представить бинарное двоичное дерево можно с помощью нелинейного двусвязного списка. Структуру узла в программе можно описать так:

node_tree struc ;узел дерева
simbol db 0 содержательная часть
l_son dd 0 указатель на старшего (левого) сына
r_son dd 0 указатель на младшего (правого) сына
ends

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

Построение двоичного дерева

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

:prg02_12.asm - программа построения и инициализации двоичного дерева.
:Вход: произвольный массив чисел в памяти.
:Выход: двоичное дерево в памяти.
;объявления структур:
node_tree struc :узел дерева
simbol db 0 содержательная часть
l_son dd 0 указатель на старшего (левого) сына
r_son dd 0 указатель на младшего (правого) сына
ends
.data
mas db 37h.12h.8h.65h.4h.54h.llh.02h.32h.5h.4h.87h.7h.21h.65h.45h.22h.
Ilh,77h.51h,26h.73h.35h.l2h.49h.37h.52h l_mas=$-mas
.code
BuildBinTree proc
формируем корень дерева и указатель на дерево HeadTree
:для размещения дерева используем кучу, выделяемую процессу по умолчанию (1 Мбайт).
:но при необходимости можно создать и доп. кучу с помощью HeapCreate
iHANDLE GetProcessHeap (VOID):
call GetProcessHeap
movHand_Head,eax сохраняем дескриптор ;запрашиваем и инициализируем блок памяти из кучи для корня дерева:
xorebx.ebx :здесь будет указатель на предыдущий узел ;LPVOID HeapAllос(HANDLE hHeap, DWORD dwFlags, DWORD dwBytes):
push type node_tree :размер структуры для узла дерева
push 0 ;флаги не задаем
push eax :описатель кучи (он же в Hand_Head)
call НеарАПос
mov HeadTree.eax запоминаем указатель на корень дерева
mov ebx.eax :и в ebx :подчистим выделенную область памяти в куче
push ds
pop es
mov edi .eax
mov ecx.type node_tree
mov al .0 rep stosb
leaesi.mas ;первое число из mas в корень дерева
lodsb ;число в al
mov [ebx].simbol.al
mov ecx.l_mas-l @@cycll: ;далее в цикле работаем с деревом и массивом mas
push ecx ;НеарА11ос портит ecx. возвращая в нем некоторое значение ;запрашиваем блок памяти из кучи для узла дерева: ;LPVOID HeapAllос(HANDLE hHeap. DWORD dwFlags, DWORD dwBytes);
push type node_tree:размер структуры для узла дерева
push 0 :флаги не задаем
push HandJHead :описатель кучи
call НеарАПос
pop ecx
mov ebx.eax запоминаем указатель на узел дерева в ebx
mov NewNode.eax :и во врем, перем. ¦подчистим выделенную область памяти в куче
movedi.eax
push ecx
mov ecx.type node_tree
mov al .0 rep stosb
pop ecx
:читаем очередное число из mas и записываем его в новый узел lodsb :число в al mov [ebx].simbol,al
;ищем место в дереве согласно условиям упорядочивания ;и настраиваем указатели в узлах дерева
mov ebx.HeadTree m_search: cmpal.[ebx].simbol mov edi .ebxпродублируем
jae ml :если al >= [ebxj.simbol :если меньше, то идем по левой ветке
mov ebx.[ebx].l_son
cmp ebx.O
jnem_search ;если этого сына нет. то добавляем его к папе
mov ebx.NewNode
mov [edi].l_son.ebx
jmp end_cycl 1
:если больше или равно, то по правой ml: mov ebx. [ebx]. r_son
cmp ebx.O
jnem_search :если этого сына нет. то добавляем его к папе
mov ebx.NewNode
mov [edi].r_son.ebx end_cycll: loop @@cycll
ret ;двоичное дерево поиска построено BuildBinTree endp start proc near :точка входа в программу:
call BuildBinTree

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

2h.4h.4h,5h.7h,8h.nh.llh,12h.l2h.21h.22h.26h.32h,35h,37h.37h.4*5h.49h.51h.52h. 54h.65h.65h.73h.77h.87h.

бедимся в этом, разработав программу обхода двоичного дерева.

Обход узлов дерева

Возможны три варианта обхода дерева:

  • сверху вниз — корень, левое поддерево, правое поддерево;
  • слева направо — левое поддерево, корень, правое поддерево;
  • снизу вверх — левое поддерево, правое поддерево, корень.

Понятно, что процедура обхода рекурсивна. Под термином «обход дерева» понимается то, что в соответствии с одним из определенных выше вариантов посещается каждый узел дерева и с его содержательной частью выполняются некоторые действия. Для нашей задачи подходит только второй вариант, так как только в этом случае порядок посещения узлов будет соответствовать упорядоченному массиву. В качестве полезной работы будем извлекать значение из узла двоичного дерева и выводить его в массив в памяти. Отметим особенности функции LRBeat, производящей обход дерева. Во-первых, она рекурсивная, во-вторых, в ней для хранения адресов узлов используется свой собственный стек. Стек, поддерживаемый на уровне архитектуры микропроцессора, используется для работы процедурного механизма, и мы со своими проблемами только «путаемся у него под ногами».

:prg02_13.asm - программа обхода двоичного дерева поиска (слева направо). ;Вход: произвольный масиив чисел в памяти. :Выход: двоичное дерево в памяти.
объявления структур: nodejxee struc :узел дерева
ends
: для программного стека
desc_stk struc :дескриптор стека
ends
•.описание макрокоманд работы со стеком:
create_stk macro HandHead:REQ.descr:REQ.Si zeStk:=<256>
¦.создание стека
endm
push_stk macro descr:REQ.rg_item:REQ
добавление элемента в стек
endm
pop_stkmacro descr:REQ. rg_item:REQ
извлечение элемента из стека
endm
.data
исходный массив:
masdb 37h.l2h,8h.65h.4h.54h.llh.02h.32h,5h,4h,87h.7h.21h.65h.45h.22h,llh.77h.
51h.26h.73h.35h.12h.49h.37h.52h l_mas=$-mas
упорядоченный массив (результат см. в отладчике): ordered array db 1 mas dup (0)
doubleWord_stkdesc_stk <> -.дескриптор стека
count call dd 0 :счетчик количества рекурсивного вызова процедуры
.code
BuildBinTree proc ;см. prg02_12.asm
:двоичное дерево поиска построено
ret
BuildBinTree endp LRBeat proc
:рекурсивная процедура обхода дерева - слева направо :(левое поддерево, корень, правое поддерево)
inc count_call ;подсчет количества вызовов процедуры
:(для согласования количества записей и извлечений из стека)
cmp ebx.O
je @@exit_p
mov ebx.[ebx].l_son
cmp ebx, 0
je @@ml
push_stk doubleWord_stk.ebx mini: call LRBeat
pop stkdoubleWord stk.ebx
r r_ —
jnc @@m2
:подчистим за собой стек и на выход raovecx.count_call
dec ecx
pop NewNode:pop "в никуда"
loop $-6
jmp @@exi t_p @@m2: moval,[ebx].simbol
stosb
mov ebx, [ebx]. r_son cmp ebx.O je @@ml push stk doubleWord stk.ebx
c'all LRBeat " @@exit_p: deccount_call
ret
LRBeat endp start proc near ;точка входа в программу:
call BuildBinTree :формируем двоичное дерево поиска ;обходим узлы двоичного дерева слева направо и извлекаем значения ;из содержательной части, нам понадобится свой стек (размер (256 байт) нас устроит. :но макроопределение мы подкорректировали)
create_stk Hand_Head.doubieWord_stk
push ds
pop es
lea edi.ordered_array
mov ebx.HeadTree push_stk doubleWord_stk.ebx указатель на корень в наш стек
call LRBeat

Еще одно замечание о рекурсивной процедуре. Реализация рекурсивное™ в программах ассемблера лишена той комфортности, которая характерна для языков высокого уровня. При реализации рекурсивной процедуры LRBeat возникает несбалансированность стека, в результате после последнего ее выполнения на вершине стека лежит не тот адрес и команда RET отрабатывает неверно. Для устранения подобного эффекта нужно вводить корректировочный код, суть которого заключается в следующем. Процедура LRBeat подсчитывает количество обращений к ней и легальных, то есть через команду RET, выходов из нее. При последнем выполнении анализируется счетчик обращений countcal 1 и производится корректировка стека.
Для полноты изложения осталось только показать, как изменится процедур;!. LRBeat для других вариантов обхода дерева.
Построенное выше бинарное дерево теперь можно использовать для дальнейших операций, в частности поиска. Для достижения максимальной эффективности поиска необходимо, чтобы дерево было сбалансированным. Так, дерево считается идеально сбалансированным, если число вершин в его левых и правых поддеревьях отличается не более чем на 1. Более подробные сведения о сбалансированных деревьях вы можете получить, изучая соответствующую литературу. Здесь же будем считать, что сбалансированное дерево уже построено. Разберемся с тем, как производить включение и исключение узлов в подобном, заранее построенном упорядоченном дереве.

Включение узла в упорядоченное бинарное дерево

Задача включения узла в дерево уже была решена нами при построении дерева. Осталось оформить соответствующий фрагмент программы в виде отдельной процедуры addNodeTree. Чтобы не дублировать код, разработаем рекурсивный вариант процедуры включения — addNodeTree. Он хоть и не так нагляден, как нерекурсивный вариант кода в программе prg_03.asm, но выглядит достаточно профессионально. Текст процедуры addNodeTree вынесен на дискету.
Работу процедуры addNodeTree можно проверить с помощью программы prgO2_ 13.asm (в ПРИМЕРе ей соответствует программа prgO2_14.asm).
В результате проведенных работ мы получили дублирование кода, строящего и дополняющего дерево. В принципе для строительства и включения в дерево достаточно одной процедуры addNodeTree. Но в учебных целях мы ничего изменять не будем, чтобы иметь два варианта строительства дерева — рекурсивный и не рекурсивный. При необходимости читатель сам определит, какой из вариантов более всего отвечает его задаче и в соответствии с этим доработает этот код.
Исключение узла из упорядоченного бинарного дерева
Эта процедура более сложная. Причиной тому то обстоятельство, что существует несколько вариантов расположения исключаемого узла в дереве: узел является листом, узел имеет одного потомка и узел имеет двух потомков. Самый непростой случай — последний. Процедура удаления элемента del NodeTree является рекурсивной и, более того, содержит вложенную процедуру del, которая также является рекурсивной. Текст процедуры del NodeTree вынесен на дискету.
Работу этой процедуры можно проверить с помощью программы prg02_13.asm (в ПРИМЕРе ей соответствует программа prg02_15.asm).
Графически процесс исключения из дерева выглядит так, как показано на рис. 2.20. Исключаемый узел помечен стрелкой.

Рис. 2.20. Исключение из дерева

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

Лексикографическое дерево

Деревья поиска, подобные лексикографическим, являются альтернативой методам поиска, основанным на хэшировании. Структурно лексикографическое дерево представляет собой бинарное дерево поиска. Рассмотрим задачу, которая заключается в анализе некоторого текстового файла и сборе статистики о частоте встречаемости слов в этом тексте. В качестве итога требуется вывести на экран информацию о частоте встречаемости каждого слова. В такой постановке задачи программа будет состоять из следующих частей.

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

Для простоты преобразования положим, что каждое слово может встречаться в тексте не более 9 раз. Длина слова — не более 10 символов (для экономии места контроль количества вводимых символов не производится). Слова в файле разделяются одним пробелом. Также для сокращения текста программы считаем, что имя входного файла фиксировано — in.File.txt.
Программа построения лексикографического дерева prg_12_78.asm имеет достаточно большой размер, в связи с чем ее текст вынесен на дискету.
Бинарные деревья — не единственный вид деревьев, которые могут быть использованы в ваших программах. В литературе можно ознакомиться с представлениями деревьев, отличных от бинарных. Мы не будем развивать далее проблему представления деревьев, хотя, честно говоря, очень хочется. За кадром остались такие важные проблемы, как балансировка деревьев, работа с В-деревь-ями и т. д. После столь обстоятельного введения и наличия соответствующей литературы, вы сможете без особого труда решить эти и другие проблемы. Тем более, что на худой конец любое дерево можно преобразовать в бинарное и работать с ним, используя приемы, описанные в настоящем разделе.
В преддверии рассмотрения следующего материала отметим, что разработанная в этом разделе программа будет очень полезна в процессе построения различных транслирующих программ, частным случаем которых являются компиляторы. Лексикографические деревья можно с успехом использовать в процессе работы сканера, задачей которого, в частности, является выяснение принадлежности некоторого идентификатора к ключевым словам некоторого языка. Введите в файл infile.txt список ключевых слов (разделенных одним пробелом), запустите программу и в памяти вы получите лексикографическое дерево.

Элементы компиляции программ

Большинство людей считают компьютеры математическими машинами,
разработанными для выполнения численных расчетов. В действительности
же компьютеры представляют собой языковые машины: основа их
могущества заключается в способности манипулировать лингвистическими
знаками — символами, которым приписан некоторый смысл.
Терри Виноград

В процессе изложения материала данного раздела мы получили достаточные знания для того, чтобы с пользой применить их на примере достаточно сложной задачи. Одной из таких задач традиционно считается разработка компилятора (интерпретатора). Тем более что у нас есть для этого повод — необходимость создания препроцессора для новых команд микропроцессоров Pentium Pro/II/IH/IV (см. главу 10). Исходя из этого задачу будем решать поэтапно: на первом этапе разберемся с общими вопросами из теории компиляции, которые будут полезны в контексте решения нашей задачи, а затем, на втором этапе, разработаем сам препроцессор. В соответствии с этими этапами весь материал также будет разбит на две части и рассмотрен в разных главах настоящей книги. Но для того, чтобы все наше строение было логически связанным, мы сформулируем для каждого из этапов свои целевые установки. Цель первого этапа — научиться проводить распознавание и синтаксический разбор одиночных предложений, принадлежащих некоторому языку. Цель второго этапа — применить полученные знания для обработки некоторой программы на языке ассемблера, содержащей новые команды микропроцессоров Pentium Pro/II/III, о которых транслятор ассемблера «не знает». В результате этой обработки новые команды будут замещены эмулирующим кодом. Конечно, кто-то возразит — можно попытаться достать соответствующий «patch» к транслятору, что позволит ему непосредственно поддерживать новые команды. Или выбрать другой путь — использовать набор макрокоманд, предоставляемых фирмой Microsoft (защищенный, кстати, ее авторскими правами). Но, как быть тем, кто привык работать с ассемблерами других фирм? Более того, разработка данной задачи и использование ее результатов в своей практической работе ощутимо поднимает уровень профессиональной подготовки программиста. А это уже достижение одной из целей данной книги.
Теория компиляции разработана очень хорошо. В списке литературы вы сможете найти ссылки на некоторые источники, где приведено ее описание. Наша задача — сформировать некий подход, который бы на хорошем профессиональном уровне позволил вам решать определенный круг задач. Конечно же, мало кому придется разрабатывать свой транслятор, но практически всем приходилось или придется разрабатывать языковой интерфейс своей программы с пользователем. Ну и как ваша программа будет его обрабатывать? На основе каких-то символьных заготовок? Изучив предлагаемый материал, вы, например, самостоятельно сможете производить синтаксический разбор предложений, поступающих
на вход вашей программы, оптимальным образом анализировать содержимое нужных файлов и т. п.

Формальное описание языка программирования

Язык программирования является подмножеством естественного языка и предназначен для поддержки процесса общения человека с компьютером. В общем случае язык — это множество предложений, которые можно записать на нем. Отличие языка программирования от естественного — в его законченности или замкнутости. Под этим понимается, что теоретически можно перечислить все предложения, которые можно на нем составить. Для естественного языка это невозможно. В контексте нашего изложения под языком программирования будем понимать не только языки высокого уровня, но и языки командных процессоров и вообще любые наборы предложений, с помощью которых производится управление работой некоторой программы.
Теория компиляции базируется на том, что любой язык может быть описан формально.
Основа любого естественного языка — его алфавит, то есть множество символов букв. Вспомним, что обучение в школе начинается с букваря, то есть со знакомства с набором символов, из которых в дальнейшем будут строиться слова. Приступая к изучению языка программирования, программист также вначале знакомится с набором символов (букв, цифр, разделительных знаков), из которых строятся слова программы и объединяются затем в предложения программы. Для формального описания языка программирования также необходимо знать алфавит, но в этом случае его понятие отличается от того, к которому мы привыкли. К этому мы вернемся чуть позже. Для написания программы недостаточно знать только лишь один алфавит. Так, в школе после изучения алфавита дети начинают изучать предмет «Русский язык». Можно выделить по крайней мере две цели, которые при этом ставятся: во-первых, на основе алфавита и набора правил научить школьника правильно строить слова языка (которые составляют его лексику); во-вторых, научить его правильно составлять предложения из слов, то есть делать это так, чтобы его могли понимать окружающие. Для построения правильных предложений в любом языке существует набор правил, которые описывают синтаксис этого языка. Каждому правильному предложению языка приписывается некоторый смысл. Описание смысла предложений составляет семантику языка.
Естественный язык многозначен, и часто с его помощью одну и ту же мысль можно выразить несколькими синтаксически разными предложениями. Компьютер — не человек, и общение с ним (во всяком случае, пока) должно быть однозначным, то есть не может быть двух разных по написанию предложений, выполняющих одно действие. Применительно к компьютеру семантика языка программирования представляет собой описание того, как следует исполнять на машине конкретное предложение. Различие синтаксиса и семантики лучше всего иллюстрирует следующий классический пример. Имеются два одинаковых с точки зрения синтаксиса предложения:
Здесь I, J, К — целые, а X, Y, Z — вещественные числа. В машинном представлении для вычисления данных выражений будут использоваться не только разные команды, но и алгоритмы. Если вдруг перед нами будет поставлена задача перевода программы на другой язык программирования, то в той или иной степени будет меняться все — алфавит, лексика, синтаксис, но семантика в идеале должна остаться неизменной.
Таким образом, для формального описания языка необходимы по крайней мере два элемента — алфавит и набор правил (синтаксис) — для построения предложений языка. Существует еще несколько элементов формального описания, которые также важны для процесса однозначного построения и распознавания предложений языка. Знакомство с ними целесообразно вести в рамках такого понятия, как грамматика языка.
Грамматика языка представляет собой механизм порождения предложений языка и определяет форму (синтаксис) допустимых предложений языка. Это важное положение, запомните его. В своем изложении мы постараемся по возможности избежать «формализмов», которыми изобилует теория компиляции, хотя полностью сделать это нам не удастся. В частности, без этого трудно ввести понятие грамматики. Формально грамматику языка G можно определить как совокупность четырех объектов: G-{Vt. Vn. P. Z}
Эти объекты можно описать следующим образом.

  • Vt — множество терминальных символов грамматики. Кстати, в этом контексте слово «символ» не означает отдельную литеру. В контексте терминальных и нетерминальных символов символы — это ключевые слова, допустимые имена идентификаторов, знаки операций, разделительные знаки, то есть все отдельные объекты исходного текста программы, имеющие смысл для компилятора. По сути, множество терминальных символов представляет собой набор лексем, которые являются допустимыми словами языка, составляющими его лексику. Таким образом, важно понимать, что исходный текст программы состоит только из терминальных символов.
  • Vn — множество нетерминальных символов. Эти символы являются вспомогательными конструкциями, определенными внутри грамматики. К пояснению того, что представляют собой нетерминальные символы, мы вернемся чуть позже. Важно отметить, что множества Vt и Vn не пересекаются. Объединение множеств Vt и Vn составляет алфавит языка. Отметим, что введенное здесь понятие алфавита грамматики (а значит, и языка) отличается от того, что есть в букваре. Не забывайте об этом важном моменте при проведении дальнейших аналогий.
  • P- набор правил грамматики, определяющих синтаксис языка. Эти правила называются правилами вывода. Эти правила определяют возможность получения любого предложения языка.
  • Z- начальный символ языка. Он входит в множество Vn. Одна из его особенностей состоит в том, что он должен встретиться по крайней мере в левой части одного из синтаксических правил, входящих в множество Р. И именно это правило является первым в процессе вывода любого допустимого предложения языка.

Далее, если не оговорено особо, прописными буквами будем обозначать терминальные символы, а строчными — нетерминальные.
Поясним, как с помощью грамматики задается язык. Начнем с простого примера. Опишем грамматику G1nt языка целых чисел:

Glnt = {Vt=(0.1.2,3.4.5.6.7.8.9). VrH число, цифра). Р. г=(число)}.

Множество правил Р грамматики Gint выглядит так:

число::=цифра
число::=цифра число
цифра::=0
цифра::=1
цифра::=2
цифра::-3
цифра::=4
цифра::=5
цифра::=6
цифра::-7
цифра::-8
цифра::=9

Обычно подобные правила записывают короче:

число::= цифра | цифра число цифра::=0|1|2|3|4|5|6|7|8|9

Здесь символ | означает альтернативу выбора, при этом все элементы, которые он разделяет, равноправны по отношению к нетерминальному символу левой части. Покажем, что эта простая грамматика действительно однозначно определяет язык, с помощью которого можно получить любое целое число. Процесс получения предложения языка, задаваемого грамматикой, называется выводом и определяется правилами вывода, входящими во множество Р. Причем начинать вывод предложения можно не с любого правила, а только с одного из тех, в левой части которых стоит начальный символ языка Z. В нашем случае таким символом является нетерминальный символ число. Видно, что он стоит в левой части двух правил:

число::= цифра (2.9)
число::= цифра число (2.10)

Сам процесс вывода предложения языка итеративный и состоит из одного или нескольких шагов. Например, рассмотрим процесс получения предложения 8745. Согласно сказанному выше, на первом шаге вывода можно использовать правило (2.9) или (2.10). Если использовать правило (2.9), то это означает, что предложение будет состоять из одной цифры. В нашем случае на первом шаге необходимо применить правило (2.10). Производя дальнейшие подстановки, можно получить предложение, состоящее из любого количества цифр. Заметьте, что на последнем шаге вместо правила (2.10) применяется правило (2.9), что в конечном итоге приводит к желаемому результату. Таким образом, предложение 8745 получается использованием правил вывода грамматики в следующей последовательности:

число => цифра число => 8 число => 8 цифра число => 87 число => 87 цифра число => 874 число => 874 цифра => 8745.

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

цифра число, 8 число, 8 цифра число, 87 число, 87 цифра число, 874 число, 874 цифра, 8745.

Предложением языка является только одна из этих сентенциальных форм — строка 8745.
На правила грамматики обычно накладываются определенные ограничения. В зависимости от этих ограничений языки делятся на 4 класса.

  • Класс 0. Грамматики без ограничений. Правила этих грамматик имеют форму: u=>w. При этом не накладывается каких-либо ограничений на строки и и v в левой и правой частях правил вывода. Используя языки этого класса, можно моделировать естественный язык.
  • Класс 1. Контекстно-чувствительные грамматики. Правила этих грамматик имеют форму: AuB=>AwB. Замена и на v возможна лишь в контексте строк А и В (отсюда и название). При этом: ueVn; we(VnuVt)*; A, Be(VnuVt)+. Символы * и + обозначают множество всех строк, выводимых в рамках данной грамматики, включая ("*") и исключая ("+") пустую строку.
  • Класс 2. Контекстно-свободные, или контекст}ю-нечувствительные, грамматики. Их правила имеют форму: u=>w, где ueVn, we(VnuVt)*. Название данного класса грамматик отражает тот факт, что и можно заменить на w, не обращая внимания на контекст. Другая особенность грамматик этого класса в том, что в правой части всех правил грамматики стоит только один нетерминал. Отметим, что языки программирования моделируются с использованием грамматик именно этого класса.
  • Класс 3. Регулярные, или автоматные, грамматики. Исходя из вида правил, которые используются в таких грамматиках, их делят на два подкласса.
  • Грамматика, выравненная вправо. Ее правила имеют форму: u=>Aw или и=>А, где AeVt, u и weVn.
  • Грамматика, выравненная влево. Ее правила имеют форму: u=>\vA или и=>А, где AeVt, u и weVn. Это очень важный класс грамматик, который наряду с грамматикой класса 2 используется для моделирования языков программирования. Заметим, что рассмотренная выше грамматика языка целых чисел как раз и является грамматикой класса 3. Чтобы убедиться в этом, необходимо лишь немного подправить правила:

число: ."¦ цифра
число: :*=0 число |1 число |2 число |3 число |4 число |5 число |б число |7 число
|8 число |9 число иифра::=0|1|2|3|4(5|6|7|8|9

Приведенная выше классификация языков была введена в 1959 году американским ученым-лингвистом Н. Хомским.
Выше, при изложении основ работы с двусвязными списками, мы ввели понятие конечного автомата. Язык, который воспринимает любой конечный автомат, относится к языкам класса 3, то есть является регулярным. Покажем это, сформулировав грамматику для языка вещественных чисел. Напомним соответствующее регулярное выражение: (+| -)dd*.dd*e(+| 0dd*, где d* обозначает цифру 0-9 или пусто.

Grea1-{Vt-(.. + . -. е. 0. 1, 2, 3. 4. 5. 6. 7. 8. 9). VrHreal. s. m, n. k, t). P. Z-(< real >)}.

Множество правил P грамматики Greal:

real=>+s|-s|Ds s=>ds |. m m=>Dn | D n=>Dn | ek k=>+t|-t|Dt|D T=>dT|d

Попробуйте, используя данную грамматику, самостоятельно вывести следующие предложения языка вещественных чисел: 5.5, +0.6е-5. Покажите, что предложение «+.45е4» невыводимо в терминах данной грамматики. При необходимости посмотрите еще раз материал раздела «Сеть» данной главы, где было введено понятие конечного автомата и разработана программа, моделирующая его работу при распознавании строки с вещественным числом.
Анализ правил вывода грамматики Greal показывает, что генерируемый ею язык относится к языкам класса 3, а сама грамматика является грамматикой, выровненной вправо.
Рассмотрим еще одну грамматику для языка идентификаторов Gident.

G1dent-{Vt-(.. +. -. е. 0. 1. 2. 3. 4. 5. 6. 7. 8, 9). VrHreal. S. M. N. К. Т), Р, 2=(< real >)}

Множество правил Р грамматики Gident:

ident=>letter| ident letter | ident figure letter => A|B|C| ... X|Y|Z figure => 0|l|2|...8|9

Видно, что это тоже грамматика языка класса 3.
А теперь приведем пример грамматики, генерирующей язык класса 2. С ее помощью смоделируем псевдоязык, похожий на Pascal, который мы используем в нашей книге для пояснения некоторых алгоритмов:

Vt=(riPOrPAMMA, ПЕРЕМЕННЫЕ, НАЧ_ПРОГ. КОН_ПРОГ, НАЧ_БЛОК. КОН_БЛОК. ".".ID. CHJNT. СН_ REAL. «:».«:». «/». REAL. INTJYTE. INT_WORD. INT_OWORD. «,».«:»». «=».«+».«-».«*». DIV. MOO. «(». «)». «[». «]». «<».«>», «==»,«>=».«=<». ЧИТАТЬ, ПИСАТЬ, ДЛЯ. ДОДЕЛАТЬ. ПОКА. Д08НИЗ, ЕСЛИ. ЕСЛИ. ДО. ТО. ПЕРЕЙТИ_НА. ПОВТОРИТЬ),
Vn»( prog, prog-name, dec-list, stmt-list, dec. id-list, type. var. varjnd. ind. label. go_to. stmt, assign, read, write, until, for. call_func. exp. term, factor, index-exp. body, condition. cond_op). P. Z-(< prog >) }

Множество правил Р грамматики G:

prog => ПРОГРАММА prog-name ПЕРЕМЕННЫЕ dec-list НАЧ_ПРОГ stmt-list КОН_ПРОГ
prog-name => ID
dec-list => dec | dec-list : dec
dec => type id-list
type => INTJYTE | INT_WORD | INT_DWORD | REAL
1d-list=> var | id-list , var
var=> ID | varjnd
var_ind=> ID ind
ind => [ exp ]
stmt-list => stmt | stmt-list ; stmt
stmt => assign | read | write | for | while | until | label | go_to | cond op | call_
func
assign => var :- exp exp => term | exp + term | exp - term
term => factor | term * factor | term DIV factor| term MOD factor factor => var | CH_INT | CH_REAL | ( exp ) read => ЧИТАТЬ (id-list) ~ write => ПИСАТЬ (id-list) for=> ДЛЯ index-exp ДЕЛАТЬ body until => ПОВТОРИТЬ body ПОКА logical_exp call_func => ID (id-list) cond_op=> ЕСЛИ logical_exp TO body while => ПОКА logical_exp ДЕЛАТЬ body label => ID : stmt-list go_to => ПЕРЕЙТИ_НА idjabel idjabel => ID
index-exp => var := exp ДО exp | exp Д0ВНИЗ exp logical_exp => ( condition )
condition => l_exp < l_exp | l_exp <@062> l_exp | l_exp >- l_exp | l_exp =< l_exp | l_exp — l_exp | l_exp ИЛИ l_exp | l_exp И l_exp | l_exp XOR l_exp | HE l_exp l_exp => exp | condition body => stmt | НАЧ_БЛОК stmt-list КОН_БЛОК

Посмотрим внимательно на правило вывода, в левой части которого стоит начальный символ языка prog. Правая часть этого правила представляет собой сентенциальную форму, содержащую все необходимые элементы программы. На примере этой грамматики хорошо видно, что представляет собой алфавит языка программирования. По сути это совокупность лексем, которые программист использует для написания программы и которые в терминах языка являются терминальными символами, а также нетерминалов, которые имеют смысл в рамках грамматики. Более того, к терминальным символам относятся также символы ID (идентификатор), CHINT (целое число) и CHREAL (вещественное число). В программе им соответствуют Совершенно разные сочетания символов букв, цифр и разделительных знаков, например идентификаторы — chl, sab, masl; целые числа — 1, 24, 98584; вещественные числа — +33.5, 0.95е-3. С точки зрения синтаксиса эти разные по написанию объекты являются терминальными символами — идентификатором, целым числом, вещественным числом. Это ключевой момент. Мы
к нему еще вернемся, а пока давайте посмотрим, каким превращениям подвергается исходный текст программы, для того чтобы превратиться в форму, пригодную для машинного исполнения.

Описание процесса трансляции программы

Транслятор представляет собой программу, выполняющую анализ исходного кода на некотором языке программирования и формирующую объектный модуль. Процесс преобразования исходного кода называется трансляцией. Вместо термина «транслятор», часто употребляется слово «компилятор», и соответственно процесс преобразования называется компиляцией. Не вдаваясь в описание лишних подробностей, будем считать эти названия синонимами и в дальнейшем изложении использовать их исходя из своих пристрастий.
Для многих транслятор представляется как некий черный ящик, которому программист много раз на день доверяет выстраданную им программу. При общении программиста с транслятором возможны два варианта исхода: удачный, при котором на выходе транслятора формируется объектный модуль, и неудачный, когда транслятор обнаруживает в программе различные ошибки. Давайте заглянем в черный ящик, именуемый транслятором, и посмотрим, каким образом он работает. Конечно же, нашему взгляду будут доступны только общие принципы его функционирования, но мы их рассмотрим с той степенью детализации, чтобы можно было самим разработать нечто подобное.
Трансляция программы производится в несколько этапов.

  1. Лексический анализ.
  2. Синтаксический анализ.
  3. Генерация кода.

На каждом из этих этапов выполняется вполне определенная работа. В общем случае проблема компиляции заключается в поиске соответствия написанных программистом предложений структурам, определенным грамматикой, и генерации соответствующего кода для каждого предложения.
Итак, файл исходной программы подготовлен, после чего мы некоторым образом передаем его транслятору для обработки. Происходить это может двумя способами: посредством командной строки (возможно, с использованием утилиты make.exe) либо в интегрированной среде. По сути, для транслятора оба эти способа одинаковы, так как ядро транслятора для обработки этих файлов будет одно. Единственное отличие в том, что в первом случае программист явно формирует все необходимые ключи и параметры командной строки, а во втором случае он это делает неявно, путем настройки параметров интегрированной среды.

Лексический анализ

Цель лексического анализа — выделение и классификация лексем в тексте исходной программы. Программа, которая выполняет лексический анализ, называется сканером, или лексическим анализатором. Сканер производит посимвольное чтение файла с исходным текстом программы.
Полный набор лексем языка определен множеством терминальных символов в его грамматике. Среди них можно выделить изменяемые и неизменяемые лексемы. Неизменяемые лексемы в любой программе пишутся одинаково. Для грам матики псевдоязыка это такие лексемы, как: ПРОГРАММА, ПЕРЕМЕННЫЕ, НАЧ_ПРОГ, КОН_ ПРОГ, НАЧ_БЛОК, КОН_БЛОК, «.», «;», «:», «/», REAL, INTBYTE, INT_WORD, INTDWORD, «,», «:=», «=», «+», «-», «*», DIV, MOD, «(», «)», «[», «]», «<», «>», «==», ЧИТАТЬ, ПИСАТЬ, ДЛЯ, ДОДЕЛАТЬ, ПОКА, ДОВНИЗ, ЕСЛИ, ЕСЛИ, ДО, ТО, ПЕРЕЙТИ_НА, ПОВТОРИТЬ. «За бортом» остались три терминальных символа — ID, CH_INT, CH_REAL. Эти терминальные символы соответствуют идентификаторам, целым и вещественным числам. Естественно, что даже в пределах одной программы они будут различны. Задачей сканера как раз и является распознавание изменяемых и неизменяемых терминальных символов. С позиции логики обработки сканером удобно все терминальные символы отнести к одному из следующих классов (применительно к нашей грамматике псевдоязыка):

  • идентификаторы — ID;
  • ключевые слова - ПРОГРАММА, ПЕРЕМЕННЫЕ, НАЧПРОГ, КОН_ПРОГ, НАЧБЛОК, КОН_ БЛОК, REAL, INTJYTE, INTWORD, INT_DWORD, DIV, MOD, ЧИТАТЬ, ПИСАТЬ, ДЛЯ, ДОДЕЛАТЬ, ПОКА, ДОВНИЗ, ЕСЛИ, ЕСЛИ, ДО, ТО, ПЕРЕЙТИ_НА, ПОВТОРИТЬ;
  • целые числа — CHINT;
  • вещественные числа — CH_REAL;
  • однолитерные разделители — «.», «,», «;», «:<@187>, «+»,«-», «*»,«/», «(»,«)», «=», «[», «]», «<», «>»;

В двулитерные разделители — «:=», «=», «>=», «=<».
Задача сканера — построить для каждого терминального символа его внутреннее представление. Делается это для того, чтобы убрать лишнюю информацию, подаваемую на вход синтаксического анализатора. Проведем аналогию. Все знают о твердом порядке слов в английском предложении. При этом не оговариваются конкретные слова, которые должны стоять на месте подлежащего, сказуемого, дополнения. Главное, чтобы слово, которое стоит, например, на месте подлежащего, было существительным или местоимением, то есть относилось к определенному классу. Сканер как раз и выполняет классификацию лексем, подаваемых на его вход. Он распознает, например, что очередная лексема является ключевым словом begin, после чего присваивает ей определенное целое значение и передает его далее. Если же сканер распознал на своем входе некоторый идентификатор, то он производит не просто замещение, но и некоторые другие действия. Чтобы подробнее разобраться со всем этим, необходимо понять состав и назначение структур данных, определенных внутри сканера.
Сканер работает с определенным набором таблиц, среди которых есть входные и выходные.
В общем случае сканер должен иметь две входные таблицы — таблицу лексем языка и таблицу классов литер. Таблица лексем языка содержит перечень всех лексем языка и соответствующих им целочисленных значений. Для нашей грамматики таблица может быть следующей:

Лексема | Внутренний код | Лексема | Внутренний код
ПРОГРАММА | 1 | * | 23
 |  | := | 24
НАЧ БЛОК | 3 | ) | 25
КОН_БЛОК | 4 | НАЧ_ПРОГ | 26
REAL | 5 | КОН_ПРОГ | 27
INT_BYTE | 6 | / | 28
DIV | 7 | INT_WORD | 29
ЧИТАТЬ | 8 | INT_DWORD | 30
ПИСАТЬ | 9 | = | 31
ДЛЯ | 10 | MOD | 32
ДЕЛАТЬ | 11 | [ | 33
( | 12 | ] | 34
ТО | 13 | < | 35
ID | 14 | > | 36
CHJNT | 15 | == | 37
CH_REAL | 16 | >= | 38
 | 17 | =< | 39
> | 18 | до | 40
1 | 19 | ПОКА | 41
• | 20 | довниз | 42
+ | 21 | ЕСЛИ | 43
- | 22 | до | 44
 |  | ПЕРЕЙТИ_НА* |

Таблица классов литер используется только в процессе сканирования и предназначена для выяснения класса литеры, когда она выбирается сканером из входного потока. Лучше всего эту таблицу организовать в виде массива, элементы которого отражены на используемую кодовую таблицу (например, таблицу ASCII). Значение каждого элемента таблицы классов литер определяется классом литеры в кодовой таблице. В общем случае можно определить следующие классы литер:

  • d - цифра;
  • 1 — буква;
  • b — литеры, которые игнорируются, к ним может относится, например, пробел;
  • s1 — одиночные разделители: «.», «:», «(«, «)», «*»;
  • s2 — особые одиночные разделители: «.», «+», «-»,«:», «=», «<», «>».

Последние разделители отличаются тем, что они могут быть как собственно одиночными разделителями, так и входить в состав литер лексем, состоящих из нескольких литер. Например, разделитель «:» является не только одиночным, но и первой литерой двухлитерного разделителя «:=», а литеры «.», «+» и «-» являются составной частью лексемы «вещественное число».
Количество выходных таблиц может быть различным и определяется сложностью входного языка. В минимальном варианте используют две или три таблицы: таблицу лексической свертки, таблицу идентификаторов и, возможно, таблицу констант. Рассмотрим пример программы на псевдоязыке:

ПРОГРАММА progl (1M14, #progl) ПЕРЕМЕННЫЕ (2)
INTBYTE 1 (6) (14. #i)
НАЧ_ПРОГ (26)
ДЛЯ i := О ТО 9 ДЕЛАТЬ (10Н14, #i)(24)(15. 0)(13)(15. 9Н11) ПИСАТЬ (i) (9)(12)(14, #i)(25)
КОНПРОГ (27)

Приведенная программа выводит на экран целые числа от 0 до 9, хотя в контексте нашего обсуждения это и не важно. После обработки сканером исходный текст программы преобразуется во внутреннее представление, которое показано справа для каждой строки. Становится понятным значение термина «лексическая свертка» — программа как бы сворачивается в некоторую унифицированную форму, теряя при этом свою индивидуальность. Каждая лексема замещена своим кодом. Идентификаторы замещены кортежами, первый элемент которых является кодом лексемы-идентификатора, а второй элемент — ссылкой на элемент таблицы идентификаторов, где содержится более подробная информация о данном идентификаторе. Ссылка может быть адресом элемента в таблице, но может быть удобнее, чтобы это было просто число, представляющее собой индекс в этой таблице. Это же касается и лексемы «целое число». Здесь возможны разные варианты: во-первых, можно организовать таблицу констант, подобную таблице идентификаторов; во-вторых, для простых применений константу можно разместить прямо в кортеже. В нашем примере для констант выбран второй вариант.
Можно предложить несколько способов организации таблицы идентификаторов. Самый простой и неэффективный — в виде массива указателей на списки переменной длины, элементы которого содержат символы идентификатора. Имена идентификаторов нужны лишь на этапе лексической свертки для выделения одинаковых идентификаторов. Важно понимать, что после выделения сканером идентификаторов и присвоения одинаковым из них ссылок на определенный элемент массива (таблицы) идентификаторов сами символьные имена уже не нужны. Если, конечно, не будет производиться формирование диагностических сообщений. Впоследствии эти указатели можно переориентировать для ссылок на ячейки с другой информацией, например с адресом области памяти, которая будет выделена для данного идентификатора на этапе генерации кода. Аналогично можно организовать и таблицы констант, если в этом возникнет необходимость. Это самые простые варианты организации подобных таблиц. Но исходя из опыта, полученного при изучении материала данной книги, можно организовать таблицу символов более эффективно — в виде лексикографического дерева или используя методы хэширования. Если используется лексическое дерево, то каждый узел этого дерева может состоять из следующих полей:

  • уникальный номер — номер, который на последующих этапах трансляции
    будет идентифицировать данное символьное имя;
  • указатель на список, содержащий символы идентификатора;
  • указатель на список с номерами строк, в которых встретился данный идентификатор.

Впоследствии, когда имена идентификаторов не будут нужны, можно на основе этого дерева построить хэш-таблицу, доступ к элементам которой будет осуществляться на основе уникальных номеров. Кстати, для повышения эффек-
тивности процесса распознавания стоит все терминальные символы — ключевые слова языка, разделители и т. п. (за исключением id, chint и ch_rea1) — также свести в отдельное лексикографическое дерево или организовать хеш-таблицу.
Можно поступить по-другому. Этот вариант, который можно использовать для анализа ввода в командной строке и т. д., работает в случае, если сканер вызывается из синтаксического анализатора. Суть его в том, что сканер вызывается из синтаксического анализатора, когда последнему нужна очередная лексема. Сканер выделяет ее во входном потоке и выдает синтаксическому анализатору кортеж из двух элементов: кода лексемы и строки, содержащей саму лексему.
А как организовать сам процесс распознавания лексем входной программы? Для этого попробуем сформулировать некий обобщенный алгоритм построения сканера.

  1. Выделить классы лексем.
  2. Определить классы литер.
  3. Определить условия выхода из сканера для каждого класса лексем.
  4. Для каждого класса лексем поставить в соответствие грамматику класса 3.<
  5. Для каждой грамматики, построенной на шаге 4, построить конечный автомат, который будет распознавать лексему данного класса.
  6. Выполнить объединение («склеивание») конечных автоматов для всех классов лексем.
  7. Составить матрицу переходов для «склеенного» конечного автомата.
  8. «Навесить» семантику на дуги «склеенного» конечного автомата.
  9. Выбрать коды лексической свертки для терминалов грамматики и формат таблицы идентификаторов.
  10. Разработать программу сканера.

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

Выделение классов лексем

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

  • идентификаторы — ID;
  • ключевые слова - ПРОГРАММА, ПЕРЕМЕННЫЕ, НАЧ_ПРОГ, КОНПРОГ, НАЧ_БЛОК, КОН БЛОК, REAL, INTJYTE, INT_WORD, INT_DWORD, DIV, MOD, ЧИТАТЬ, ПИСАТЬ, ДЛЯ, ДОДЕЛАТЬ, ПОКА, ДОВНИЗ, ЕСЛИ, ЕСЛИ, ДО, ТО, ПЕРЕЙТИ_НА, ПОВТОРИТЬ;
  • целые числа — CH_INT;

«Навешивание» семантики на дуги «склеенного» конечного автомата
В ходе распознавания (перехода по дугам) требуется выполнять различные действия, например поиск соответствия введенной лексемы ключевому слову в таблице ключевых слов, поиск идентификатора в таблице идентификаторов, преобразования чисел и т. д. Поэтому на этом шаге необходимо определить набор переменных и состав процедур, работающих во время переходов из состояния в состояние (на дугах).
Все... После этого можно программировать сканер. В главе 10, посвященной командам ММХ- и ХММ-расширений, нами будет выполнена практическая работа в этом направлении.

Синтаксический анализ

После того как лексемы входного потока распознаны, транслятору необходимо удостовериться, что вся их совокупность является синтаксически правильной. Вопрос: зачем? Заглянем немного вперед и задумаемся о том, каким образом производится генерация кода. Если рассматривать этот вопрос схематично, то можно утверждать, что для каждого нетерминала грамматики существует некий шаблон кода, который подставляется в определенное место выходного кода. Настройка этого шаблона производится значениями лексем, распознанными сканером. Извлечь эту информацию можно, если соответствующая конструкция в исходном коде синтаксически правильна. В конечном итоге мы должны доказать, что вся программа синтаксически правильна.
Цель синтаксического анализа — доказать существование дерева грамматического разбора в контекстно-свободной грамматике G для входной цепочки s, состоящей из терминальных символов.
Существует много различных методов синтаксического анализа программ. Но главное понимать, что все они неявно стремятся доказать одно и тоже — существование синтаксического дерева для транслируемой программы. Ведь применительно к нашей грамматике не может блок описания переменных следовать где-то в середине или конце программы, за операторами, которые эти переменные используют. Кстати, посмотрите сейчас еще раз на правило грамматики, соответствующее начальному символу языка. В этом правиле определена общая структура программы и в ней видно место блока описаний переменных. Вместо списка стоит нетерминал. Можно нарисовать воображаемую дугу к правилу грамматики, соответствующему нетерминалу блока описания переменных, и т. д. То есть мы действительно начали строить грамматическое дерево. И если нам удастся его построить для данной программы, то последняя действительно синтаксически правильна. Если на каком-то шаге построения этого дерева у нас случится ошибка, допустим в том же блоке описания переменных программы вместо идентификатора будет стоять целое число, то дерево мы построить не сможем. То есть программа синтаксически неправильная. Можно формировать соответствующее диагностическое сообщение.
Таким образом, можно утверждать, что если возможно построение дерева трансляции для данного входного потока, то он синтаксически правилен. Понятно, что для другого текста программы дерево трансляции будет другое, но в лю-
бом случае листьями этого дерева будут лексемы программы, а корнем — начальный символ языка. Причем если совершить обход листьев дерева слева направо, то получим вытянутый в строку текст нашей программы.
Если судить по рисунку, то программа синтаксически правильная. Но каким бразом выясняет этот факт транслятор? Как отмечено выше, существует несколько методов выполнения синтаксического анализа. Все они делятся на два класса в зависимости от того, как они движутся по дереву трансляции во время разбора — сверху вниз или снизу вверх. Алгоритмы «сверху вниз» стремятся построить дерево трансляции начиная вывод от начального символа языка к анализируемой цепочке. И наоборот, алгоритмы «снизу вверх» строят дерево трансляции от анализируемой цепочки терминалов к начальному символу языка. Важно подчеркнуть, что на самом деле никакого дерева в памяти нет и не строится. Как показано выше, его структура заложена в правилах грамматики. Используя алгоритм конкретного метода синтаксического разбора и представленные определенным образом в памяти правила грамматики, мы и производим движение по воображаемому дереву трансляции.
На практике не исключены случаи комбинированного применения восходящих и нисходящих методов синтаксического анализа. Например, арифметические выражения удобно разбирать с помощью восходящих методов, а общий разбор программы делать нисходящими методами. В большинстве случаев достаточно одного универсального метода — метода рекурсивного спуска. Для краткости будем называть синтаксический анализатор распознавателем.

Метод рекурсивного спуска

Главное свойство нисходящих методов — их целенаправленность. Метод рекурсивного спуска — наиболее яркий и простой представитель этих методов. Основ ной его недостаток — сильная зависимость от конкретного языка. Программа распознавателя рекурсивного спуска представляет собой, по сути, набор рекурсивных процедур — по одной для каждого из нетерминалов грамматики. Первой получает управление процедура, соответствующая нетерминалу — начальному символу языка. Эта процедура уже знает, что она должна делать. Если первый символ в этом правиле — терминал, то процедура знает об этом и ждет его. Это ожидание заключается в том, что процедура сравнивает код терминала, который должен быть первым, согласно правилу, с кодом первой лексемы из лексической свертки входной строки. Если они совпадают, то процесс разбора идет дальше. Если они не совпадают, то фиксируется ошибка. Если очередным символом правила должен быть нетерминал, то в этом месте вызывается соответствующая ему процедура. Эта процедура тоже построена исходя из правила грамматики для этого нетерминала, поэтому ее действия также уже предопределены. И так будет продолжаться до тех пор, пока разбор не вернется к первому правилу, то есть управление вернется процедуре, соответствующей правилу грамматики для начального символа языка. В нашем случае последним должен быть проанализирован факт того, что последний символ во входной строке — терминал кон_прог.
При реализации метода рекурсивного спуска в чистом виде возможны сложности. Посмотрим на правила грамматики, подобные следующим:

dec-list => dec | dec-list ; dec
id-list=> ID | id-list . ID
term => factor | term * factor | term div factor
strut-list => stmt | stmt-list ; stmt
Видно, что эти правила леворекурсивны. Например, рассмотрим следующее правило:
id-list => ID | ID-LIST , ID

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

dec-list => dec | dec-list ; dec id-HSts» ID I {. ID}
stmt-list => stmt | {: stmt} exp => term {+ term | - term}
term => factor {* factor | div factor}

Теперь для принятия решения о дальнейшем пути выполнения рекурсивной процедуры необходимо просматривать один символ вперед. Если он совпадает с тем, что в правиле, то процедура рекурсивно вызывается снова. Если этот символ любой другой, то производится возврат из этой процедуры. Таким образом, процесс написания программы неявно представляет собой строительство дерева разбора начиная от его корня — начального символа языка. То есть это действительно нисходящий метод.
Остается заметить еще одно достоинство метода рекурсивного спуска. Если очередная языковая конструкция распознана, то почему бы сразу в соответствующей рекурсивной процедуре не «навесить» семантику, то есть генерацию кода.
Но метод рекурсивного спуска не всегда применим. В сложных случаях, когда имеется скрытая левая рекурсия, необходимо применять более сложные методы для ее устранения или замены правой рекурсией.
После этого обсуждения мы готовы написать распознаватель, но делать этого не будем, так как цель, достижению которой был посвящен материал данного раздела, достигнута. Все остальное — техника программирования. Остается добавить, что разговор о проблемах трансляции мы продолжим в главе Программирование ХММ-расширения.


Процедуры в программах ассемблера

Продавай мобильный контент со своего сайта

Процедуры в программах ассемблера

  • Реализация рекурсивных процедур
  • Реализация вложенных процедур
  • Разработка динамических (DLL) библиотек
    • Шаг 1. Разработка текста DLL-библиотеки
    • Шаг 2. Трансляция и компоновка исходного текста DLL-библиотеки
    • Шаг 3. Создание lib-файла
    • Шаг 4. Сборка приложения с использованием DLL-библиотеки
    • Шаг 5. Проверка работоспособности приложения с использованием DLL-библиотеки
Если бы строители строили здания так же, как
программисты пишут программы, первый же
залетевший дятел разрушил бы цивилизацию.
Второй закон Вейнберга (Прикладная Мерфология)

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

Реализация рекурсивных процедур

Принимаясь за дело, соберись с духом.
Козьма Прутков

В предыдущей главе, посвященной структурам данных, достаточно интенсивно использовались рекурсивные процедуры. Данный вопрос требует дополнительного пояснения.
Процедура называется рекурсивной, если она прямо или косвенно обращается к себе самой. Рекурсия является естественным свойством для большого числа математических и вычислительных алгоритмов. Важно отметить, что любой рекурсивный алгоритм можно сделать итеративным, но это не всегда целесообразно. В теории программирования рекурсия как правило воспринималась неоднозначно. В конечном итоге была выработана следующая рекомендация — рекурсию следует избегать в случаях, когда имеется очевидное итерационное решение. Как мы убедимся из приведенного ниже обсуждения, очевидная рекурсивная задача вычисления факториала не дает никакого выигрыша в попытке программной реализации с помощью рекурсивных процедур. Настоящий эффект возникает в тех задачах, где рекурсия использована в определении обрабатываемых данных. Такими данными могут являться, например, динамические структуры данных — стеки, деревья, списки, очереди и т. п.
Как показал материал, посвященный структурам данных, это действительно верно. При небольшом усилии в программе на ассемблере можно достаточно эф-
фективно организовать рекурсивную процедуру, подобную процедуре обхода дерева LRBeat из главы 2.
В ассемблере нет средств прямой поддержки рекурсивных алгоритмов, но есть косвенные — нужно только немного подыграть этому процессу. По сравнению с языками высокого уровня, имеющими встроенную поддержку рекурсии, в программе ассемблера все действия по ее организации приходится предусматривать самому программисту. А для этого необходимо иметь представление о процессах, происходящих при рекурсивном вызове процедуры.
Планируя использование рекурсивных процедур, необходимо продумывать следующие вопросы:

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

Подробно эти вопросы обсуждались в уроке 14 «Модульное программирование» учебника. Но при организации рекурсии они приобретают особый смысл, так как требуется не просто вызвать процедуру, а вызвать ее из себя несколько раз, обрабатывая при каждом вызове свои локальные данные, и в конечном итоге возвратить управление в точку программы, расположенную следом за первым вызовом данной процедуры.
Как правило, для работы с локальными данными процедуры и параметрами, передаваемыми в процедуру, используется стек. При этом необязательно использовать для этого только системный стек, иногда удобнее создать его модель, работу с которой осуществлять средствами самой программы. Пример организации такого программного стека мы использовали при написании программы обхода дерева с рекурсивной процедурой LRBeat.
Рассмотрим характерные моменты рекурсивного вызова процедур на примере классической рекурсивной задачи — вычисления факториала. Вспомним алгоритм вычисления факториала: F(0)=l: F(i)=ixF(i-1)
Как было отмечено выше, с точки зрения скорости работы кода рекурсивный вариант вычисления факториала неэффективен, его лучше вычислять итеративно в цикле, но в учебных целях этот пример оправдан.

.data
r_fact dw 0
.code
fact proc
push bp nrav bp.sp mov cx.[bp+4] mov ax.ex mul r_fact mov r_fact.ax dec ex jcxz end_p
push ex call fact
end_p: mov sp.bp

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

fact ргос
push bp mov bp.sp mov cx.[bp+4]

Смысл этого фрагмента легче понять, наблюдая поведение программы вычисления факториала в отладчике. Как сказано выше, перед вызовом процедуры в стек помещаются данные (или указатель на них), информация о местонахождении которых должна быть сохранена в интересах как вызывающей, так и вызываемой процедуры. В нашем случае в процедуру fact передается переменная факториала. После этого производится вызов процедуры, в результате чего в стек помещается адрес возврата. В вызванной процедуре к данным переменным необходимо получить доступ. Для этого предназначен регистр ВР. Перед использованием его содержимое должно быть также сохранено в стеке. Для первого вызова его значение несущественно. В этот момент весь предыдущий контекст работы программы сохранен. Команда mov bp.sp загружает в регистр ВР указатель на текущую вершину стека, после чего можно обращаться к данным, переданным в процедуру. По сути, сейчас мы с вами сформировали кадр стека. Следующий рекурсивный вызов этой функции придает действию сохранения регистра ВР особый смысл. Команда push bp сохраняет в стеке адрес кадра стека для предыдущего вызова рекурсивной процедуры. Теперь для выхода из процедуры достаточно выполнить приведенные ниже команды эпилога, позволяющие корректно отработать обратную цепочку возврата в основную программу: будет рассмотрена в этой главе ниже. Далее сравним работу функций DrawPattern i и DrawPattern 1.
Вызов функции DrawPattern_1 из основной программы осуществляется следующим фрагментом кода (полный текст приведен в ПРИМЕРе в каталоге программ для данной главы).

:prg3_1.asm - фрагмент оконного приложения, вызывающего рекурсивную процедуру :DrawPattern_l
объявление пользовательских процедур (из maket_dll.DLL) extrn DrawPattern_l:PROC extrn DrawPattem_2:PR0C
.data
определение констант для фигуры "Узор из окружностей"
р dd 5 ;порядок узора
г dd 60 :радиус окружности
y_Pdd 140 начальная у-координата центра окружности
х_Р dd 200 начальная х-координата центра окружности
.code
обработка сообщений от меню
MenuProc proc
arg (a@hwnd: DWORD. №wparam: DWORD, @(ahdc: DWORD.@@hbrush: DWORD
uses eax.ebx
mov ebx.@@wparam :в Ьх идентификатор меню
onpbx.IDMJ)LL_LACESJ je @@idmdlllaces_l cmpbx.IDM_DLLJ_ACES_2 je @@idmdlllaces_2 jmp@@exit
e@1 chndl 11 aces_l:
;рисуем узор из окружностей, рекурсивная функция для рисования находится
;в DLL-библиотеке:
;DrawPattern_l(hwnd.hdc,x.y.r.p) - функция не работает с локальными переменными:
push p :порядок узора
push г :радиус окружности
push y_P :у-координата центра окружности
push x_P ;х-координата центра окружности
push memdc :контекст устройства
push @@hwnd
call DrawPattern_l
jmp@@exit :.........
Фрагмент файла maket_dll.DLL, содержащий процедуру DrawPattern_l, приведен ниже:
iinaket_dn.DLL - фрагмент DLL-библиотеки, содержащей рекурсивную процедуру DrawPatternJ
объявление процедур DLL-библиотеки общедоступными publicdll WriteCon publicdll DrawPatternJ publicdll DrawPattern_2
.code DrawPatternJ proc
:DrawPattern_l - рекурсивная процедура рисования узора :(без использования локальных переменных)
arg @@hwnd:dword.@@hdc:dword.@@x:dword.@@y:dword.@@r:dword.@@p:dword
:рисуем окружность
:рекурсивно вызываем DrawPattern_l(hwnd.hdc,x.y.r,p)
:BOOL Ellipse(HDC hdc. int nLeftRect. int nTopRect. int nRightRect.int nBottomRect):
:готовим параметры в стеке для вызова Ellipse
call Ellipse:рисуем окружность :и еще четыре меньшего порядка
dec @@p
стр @@р, 0
je @@End_Draw
shr@@r,l -.делим на 2
:готовим параметры в стеке для вызова DrawPatternJ
call DrawPattern_l :готовим параметры в стеке для вызова DrawPattern_l
call DrawPatternJ :готовим параметры в стеке для вызова DrawPatternJ
call DrawPattern_l :готовим параметры в стеке для вызова DrawPatternJ.
call DrawPatternJ
@@End_Draw:
генерация сообщения WM_PAINT для вывода изображения на экран
call InvalidateRect endp DrawPatternJ

Такой вариант процедуры не требует внимания к параметрам, которые пере-, даются в стеке при вызове рекурсивной процедуры, так как после возврата из [ нее они попросту не нужны и удаляются из стека. Но стоит нам в процедуре DrawPattern изменить порядок обращения к процедуре Ellipse, как ситуация резко меняется. Рассмотрим второй вариант организации процедуры DrawPattern.
VOID DrawPattern (HWND hwnd.HDC hdc.INT_DWORD x.INT_DWORD y.INTJMRD r,INT_DWORD p)

//DrawPattern - рекурсивная процедура DrawPatten (вариант 2) вывода на экран узора
//из окружностей на псевдоязыке (фрагмент)
//Вход: х и у - координаты центра окружности; г - радиус окружности:
//р - порядок узора, hwnd - дескриптор окна. HDC - контекст устройства.
ПЕРЕМЕННЫЕ
HWND hwnd: HDC hdc;
INT_DWORD hdc. x. y. r.p
НАЧ_ПРОГ
ЕСЛИ (р) ТО //пока р*0
НАЧ_БЛОК_1
//рисуем еще четыре окружности по с центрами по краям этой DrawPattern (hwnd. hdc. х-г. у. г, р-1) DrawPattern (hwnd. hdc. х. у-г. г. р-1) DrawPattern (hwnd. hdc. х+г. у, г, р-1) DrawPattern (hwnd. hdc. х. у+г. г. р-1)
//Ellipse - функция Win32 API для вывода эллипса (окружности), вписанного //в прямоугольник (квадрат) с координатами правого верхнего угла (x_up. y_up) //и левого нижнего угла (x_low. y_low):
//Ellipse(HDC hdc. INT_DW0RD x_up. INT_DWORD y_up. INTJMJRD x_low. INT_DWORD yjow) //так как для рисования нужны координаты прямоугольника, а не центра окружности, //то преобразуем их при вызове Ellipse: .
Ellipsethdc. x_up-r. y_up-r. x_low+r, y_low+r)
КОН_БЛОК_1 КОН_ПРОГ

Если в первом варианте процедуры DrawPattern — DrawPatternl окружности рисовались перед очередной рекурсивной передачей управления в процедуру DrawPattern, то во втором варианте это делается в последнюю очередь — во время обратного хода по цепочке вызовов процедуры DrawPattern. Это уже требует наличия локальных переменных в процедуре и их сохранения на период пока осуществляются рекурсивные вызовы процедуры DrawPattern. Приведем соответствующие фрагменты основной программы и функции DrawPattern_2 из DLL-библиотеки maket dll.DLL
Из этого фрагмента хорошо видно, в чем разница между размещением параметров, передаваемых в рекурсивную процедуру, и локальными переменными этой процедуры. Для доступа к параметрам используются положительные смещения относительно адреса в ВР (это скрыто от нас с помощью директивы ARG), а для доступа к локальным параметрам — отрицательные смещения.
Разница в изображениях возникла из-за разных мест в программе, где вызывается функция InvalidateRect. Попробуйте самостоятельно исправить этот «дефект».

Реализация вложенных процедур

Понятие вложенной процедуры включает в себя возможность описания процедур внутри друг друга, при этом каждая из процедур может иметь локальные данные, видимые для вложенных в нее процедур, но не видимые для процедур, находящихся на одном уровне вложенности с данной процедурой. Для организации такой вложенности существуют две возможности — организация средствами ассемблера и определенная самим пользователем. Рассмотрим первую из них. Для нее требуются команды ассемблера ENTER и LEAVE. Их формат приведен ниже.
Команда enter loc_size.lexjev — ENTER (setup parameter block for ENTERing procedure) — реализует установку кадра стека для параметров процедуры. Работа команды заключается в следующем.

  1. Размещение текущего значения регистра ЕВР/ВР в стеке.
  2. Сохранение текущего значения ESP/SP в промежуточной переменной FP (имя переменной выбрано случайно).
  3. Если лексический уровень вложенности (операнд lexlev) не равен нулю, то (1ex_1ev-l) сделать следующее:
    1. в зависимости от установленного режима адресации usel6 или use32
    2. выполнить вычитание (ВР-2) или (ЕВР-4) и записать результат обратно в ЕВР/ВР;
    3. сохранить значение ЕВР/ВР в стеке;
    4. сохранить в стеке значение промежуточной переменной fp.
  4. Запись значения промежуточной переменной fp в регистр ЕВР/ВР.
  5. Уменьшение значения регистра ESP/SP на величину, заданную первым операндом, минус размер области локальных переменных locsize: ESP/SP= (ESP/SP)-loc size.

Команда LEAVE (LEAVE from procedure — выход из процедуры) не имеет операндов и выполняет удаление из стека области локальных (динамических) переменных, выделенной командой ENTER. Команда выполняет обратные команде ENTER действия.

  1. Содержимое ebp/bp копируется в ESP/SP, тем самым восстанавливается значение ESP/SP, которое было до вызова данной процедуры. С другой стороны, восстановление старого значения ESP/SP означает освобождение пространства в стеке, отведенного для завершающейся процедуры (локальные переменные процедуры уничтожаются).
  2. Из стека восстанавливается содержимое ЕВР/ВР, которое было до входа в процедуру. После этого действия значение ESP/SP также становится таким, каким оно было до входа в процедуру.

В результате этих двух действий также восстанавливается кадр стека, если он был, вызывающей программы.
Команды ENTER и LEAVE специально введены в систему команд микропроцессора для поддержки вложенных процедур, как это делают для блочно-структури-рованных языков высокого уровня типа Паскаль или С. В этих языках программа разбивается на блоки. В блоках можно описать свои собственные (локальные) идентификаторы, которые не могут быть использованы вне этого блока. К примеру, на рис. 3.1 в виде блоков изображена структура некоторой программы.

Рис. 3.1. Изображение структуры некоторой программы в виде блоков

В правом верхнем углу каждого блока (процедуры) стоит номер лексического уровня вложенности этого блока относительно других блоков программы. Большинство блочно-структурированных языков в качестве основного метода распределения памяти для переменных в блоках используют автоматическое распределение памяти. Это означает, что при входе в блок (вызове процедуры и т. п.) в некотором месте оперативной памяти (или в стеке) выделяется область памяти для переменных этого блока (ее можно назвать областью инициализации). После выхода из этого блока связь программы с этой областью теряется, то есть эти переменные становятся недоступными. Но если, как в нашем примере, в этой Процедуре есть вложенные блоки (процедуры), то для некоторого внутреннего блока (например, С) могут быть доступны области инициализации (переменные) блоков, объемлющих данный блок. В нашем примере для блока С доступны также переменные блоков В и А, но не D. Возникает вопрос: как же программа, находясь в конкретной точке своего выполнения, может отслеживать то, какие области инициализации ей доступны? Это делается с помощью структуры данных называемой дисплеем. Дисплей содержит указатели на самую последнюю область текущего блока и на области инициализации всех блоков, объемлющих данный блок в программе. Например, если в программе А была вызвана сначала процедура В, а затем С, то дисплей содержит указатели на области инициализации А В и С (рис. 3.2).

Рис. 3.2. Соответствие содержимого дисплея области инициализации после вызова процедур В и С

Если после этого вызвать процедуру D (в то время как В и С еще не завершены), то картина изменится (рис. 3.3).

Рис. 3.3. Соответствие содержимого дисплея области инициализации после вызова процедуры D

После того как некоторый блок (процедура) завершает свою работу, его область инициализации удаляется из памяти (стека) и одновременно соответствующим образом корректируется дисплей.
Большинство языков высокого уровня хранит локальные данные блоков в стеке. Эти переменные называют еще автоматическими, или динамическими. Память для них резервируется путем уменьшения значения регистра-указателя стека ESP/SP на величину, равную длине области, занимаемой этими динамическими переменными. Доступ к этим переменным осуществляется посредством регистра ЕВР/ВР. Если один блок вложен в другой, то для его динамических (локальных) переменных также выделяется место (кадр) в стеке, но в этот кадр помещается указатель на кадр стека для включающего его блока. Команды ENTER И LEAVE как раз и позволяют поддержать в языке ассемблера принципы работы с переменными блоков, как в блочно-структурированных языках. Дисплей организуется с помощью второго операнда команды ENTER и стека.
Например, в начале работы главной процедуры А и после вызова процедуры В кадр стека будет выглядеть так, как показано на рис. 3.4.

Рис. 3.4. Кадр стека после вызова процедур А и В

Из рисунков видно, что, используя дисплей, мы фактически имеем адреса областей инициализации, доступных по признаку вложенности объемлющих блоков. Обратный процесс завершения работы с блоками и удаления соответствующих областей инициализации поддерживается командой LEAVE.

Разработка динамических (DLL) библиотек

Стрельба в цель упражняет руку и причиняет верность глазу.
Козьма Прутков

Динамические подключаемые библиотеки (Dynamic Link Libraries, DLLs) являются хранилищем общедоступных процедур. Механизм DLL-библиотек появился вместе с операционной системой Windows и является ее неотъемлемой частью. Суть этого механизма в том, что в процессе компоновки исполняемого модуля с использованием внешних процедур в него помещаются не сами процедуры, а только их названия (номера) вместе с названиями DLL-библиотек, в которых они содержится. В уроке 14 «Модульное программирование» учебника для связи модулей на разных языках рассматривались стандартные соглашения по передаче параметров, которые специфическим образом реализовывались на уровне конкретных компиляторов языков программирования. Этот механизм был, пожалуй, единственным средством связи разноязыковых модулей при программировании для MS DOS. В среде Windows более естественным является механизм DLL-библиотек. Он позволяет, в частности, разработать набор процедур на ассемблере и затем использовать их в программах на языках высокого уровня, поддерживающих механизм динамического связывания.
Как правило, если язык программирования поддерживает разработку Windows-приложений, то он имеет средства для разработки и использования DLL-библиотек. Ассемблер не является исключением. Общие принципы разработки DLL-библиотек для всех языков одинаковы, так как эти библиотеки являются универсальным механизмом, не зависящим от конкретного языка. Поэтому, разрабатывая DLL-библиотеку, необходимо учитывать общие требования к таким библиотекам. Структурно DLL-библиотека представляет собой обычную программу, включающую некоторые специфические элементы. Рассмотрим процесс создания и использования DLL-библиотеки на языке ассемблера. Для этого разработаем консольное приложение, которое выводит некоторую строку на экран 10 раз. На каждой итерации вывода меняются атрибуты этой строки. За основу взята программа prg05_ll.asm из главы 5. Только теперь строка с выводимым сообщением находится в приложении, а сама процедура вывода — в DLL-библиотеке. Для демонстрации передачи и возврата параметров в процедуру передаются длина и адрес строки, а возвращаются значения Offffffffh в четыре регистрах ЕАХ, ЕВХ, ЕСХ, EDX. Обсудим процесс по шагам.

Шаг 1. Разработка текста DLL-библиотеки

Как мы уже отметили, DLL-библиотека представляет собой обычную программу на языке ассемблера. Выбор примера для демонстрации разработки и использования DLL-библиотеки неслучаен. Тем самым мы подтвердим тезис о том, что обычная программа и DLL-библиотека имеют много общего. С точки зрения структуры DLL-библиотека является набором функций, переменных и констант, а также необязательного кода инициализации, которые оформлены в соответствии с требованиями ассемблера. Ниже приведен пример DLL-библиотеки для нашей задачи.

;maket_dll.asm - текст DLL-библиотеки. :Содержит одну функцию - WriteCon
locals
.model flat.STDCALL ;модель памяти flat.
Объявление внешними используемых в данной программе функций Win32 (ASCII):
:обьявление процедуры WriteCon общедоступной publicdll WriteCon
.data
.code
DllMainproc
arg №h I nst: dword. @@event: dword. @<ano_use: dword
@@m: moveax.l
ret
DllMainendp
WriteCon ргос :см. дискету и prg05_ll.asm из главы 5 arg@@adr_str:dword.@@len_str:dword
ret
endp WriteCon endDllMain

Хорошо видно, что DLL-библиотека является действительно обычным файлом ассемблера. Есть все, даже имя точки входа, указываемое в последней директиве END. Но здесь и начинаются странности. На самом деле это не обычная точка входа, которую мы привыкли указывать в любой программе на ассемблере, а адрес команды в DLL-библиотеке, получающей управление в строго определенных случаях. Эта команда является первой в цепочке команд, составляющих так называемый код инициализации DLL-библиотеки. Назначение этого кода — выполнить необходимые действия по инициализации DLL-библиотеки при наступлении определенных событий. Наличие этого кода в DLL-библиотеке необязательно, и при его отсутствии нет необходимости указывать соответствующую метку в заключительной директиве END. Если все же код инициализации присутствует в DLL-библиотеке, то он должен быть разработан с учетом определенных требований.

  • Во-первых, этот код должен быть рассчитан на то, что он получает управление в одном из четырех случаев. О наступлении каждого из этих случаевоперационная система извещает DLL-библиотеку путем передачи ей одного из четырех предопределенных значений — флагов. Значения этих флагов перечислены в файле winnt.h. Рассмотрим эти флаги и возможные действия при их поступлении в DLL-библиотеку.
  • DLLPR0CESSATTACH-1 — передается операционной системой DLL-библиотеке при проецировании последней в адресное пространство процесса. Передача этого флага DLL-библиотеке производится всего один раз, обычно при загрузке приложения, использующего данную DLL-библиотеку. Если позже другой поток процесса попытается загрузить эту же библиотеку, то система попросту увеличит ее счетчик использования без посылки флага DLLPROCESSATTACH. Получив данный флаг, DLL-библиотека должна выполнить действия по созданию необходимой среды функционирования для своих функций. Например, обеспечить их кучей.
  • DLL_THREAD_ATTACH=2 — передается операционной системой DLL-библиотеке при создании нового потока в процессе. Этим DLL-библиотеке предоставляется возможность нужным образом обработать факт создания нового потока. Следует иметь в виду, что этот процесс не является обратимым, то есть если DLL-библиотека загружается в процесс, когда в нем уже функционируют потоки, то ни одному из них не посылается флаг DLL_THREAD_ATTACH.
    # DLL_THREAD_DETACH=3 — передается операционной системой DLL-библиотеке при выгрузке потоком DLL-библиотеки.
    # DLL_PROCESS_DETACH=0 — передается операционной системой DLL-библиотеке при выгрузке DLL-библиотеки из адресного пространства процесса. Логично, что при этом требуется провести завершающие действия по освобождению всех ресурсов, которыми владеет DLL-библиотека. Обычно эти действия являются обратными по отношению к предпринятым при инициализации библиотеки (см. флаг DLLPROCESSATTACH).
    Во-вторых, имя точки входа DLL-библиотеки может быть любым. Главное, чтобы при наличии кода инициализации это имя было указано в директиве END.
    В-третьих, оформление кода инициализации в виде отдельной процедуры необязательно. Главное, выполнить два основных действия кода инициализации DLL-библиотеки (при его наличии):
  • # вернуть единицу в регистре ЕАХ;
    # удалить из стека три параметра, которые передаются DLL-библиотеке при передаче описанных выше флагов: hlnstDLL — дескриптор DLL-библиотеки, назначенный ей системой при ее загрузке в адресное пространство процесса;
  • vent — значение флага, передаваемого в DLL-библиотеку; f ImpLoad — параметр не равен 0, если библиотека загружена неявно (см. ниже), и равен 0 в обратном случае.

Структура полного варианта инициализациониого кода выглядит так:

includeWindowConA.inc;проверьте присутствие значений флагов в этом файле"
DllMain ргос
arg hlnstDLL:dword. event:dword,fImpLoad:dword
cmp [event].DLL_PROCESS_ATTACH
jne m выполняем действия для DLL_PROCESS_ATTACH
cmp [event].DLL_THREAD_ATTACH
jnem :выполняем действия для DLL_THREAD_ATTACH
cmp [event]. DLL_THREAD_DETACH
jnem выполняем действия для DLL_THREAD_DETACH
cmp [event].DLL_PROCESS_DETACH
jnem
выполняем действия для DLL_PROCESS_DETACH m: moveax.l
ret DllMainendp

Минимальный вариант может выглядеть так, как это сделано в нашем примере:

DllMain ргос
arg hlnstDLL:dword. event:dword,fImpLoad:dword
m: mov eax.l
ret DllMainendp

Или так:

DllMain: m: moveax.l ret 12

He забывайте, что директива arg приводит к тому, что в код, генерируемый транслятором, вставляются команды ENTERD и LEAVED (см. выше разделы «Реализация рекурсивных процедур» и «Реализация вложенных процедур»). Кроме этого, команда RET процедуры дополняется значением, равным сумме длин параметров, указанных в директиве ARG . Исполнение такой команды приводит к удалению из стека количества байт, равного этому сформированному значению.
Что касается кода функций (процедур), составляющих DLL-библиотеку, то для их написания используются обычные правила разработки программ. Описание данных также ничем не отличается от обычной программы ассемблера. Ведь в конечном итоге код и данные процедур DLL-библиотеки оказываются в адресном пространстве процесса наравне с его кодом и данными.
Последнее, что необходимо отметить, — все экземпляры данных и имена процедур, которые должны быть видны вне пределов DLL-библиотеки, объявляются общими с использованием одной из директив PUBLIC или PUBLICDLL.

Шаг 2. Трансляция и компоновка исходного текста DLL-библиотеки

После того как подготовлен исходный текст библиотеки, его транслируют обычным для программ ассемблера образом. Что же касается компоновки, то необходимо помнить, что ее целью является получение файла с расширением .dll, а не обычного файла с расширением .ехе. Весь этот процесс удобно обсуждать на примере реального файла makefile, текст которого приведен ниже:

TASM0PT=/m3 /mx /z /q /DWINVER=0400 /D_WIN32_WINNT=0400
!1f $d(DEBUG)
TASMDEBUG=/zi
LINKDEBUG=/v
lelse
TASMDEBUG=/1
LINKDEBUG=
lendif
!if Sd(MAKEDIR)
IMP0RT=import32
lelse
IMP0RT=import32
lendif
${NAME).EXE: $(OBJS) $(DEF)
t1ink32 /Tpd /aa /c $(LINKDEBUG) $(OBJS).$(NAME).. S(IMPORT). $(DEF) .asm.obj:
tasm32 KTASMDEBUG) S(TASMOPT) $&.asm

Запуск данного файла производится командной строкой:

make -DOEBUG -fmakefile_dll.mak >p.txt

В результате формируется несколько файлов, перечень которых определяется тем, насколько успешно отработали программы транслятора tasm32 и компоновщика nk.32. Для быстрой оценки этого результата мы перенаправили весь вывод в файл p.txt Просмотрев этот файл, можно оценить успешность создания DLL-библиотеки, не анализируя другие файлы (например, файл листинга). При наличии синтаксических ошибок необходимо исправить их и повторить запуск make-файла на исполнение.
Для успешной компоновки необходим еще один файл — с расширением .def. Необходимое и достаточное содержимое файла maket_dll.def приведено ниже:

LIBRARY maketjll DESCRIPTION 'Win32 DLL' EXPORTS WriteCon @1

В этом файле следует обратить внимание на директиву EXPORTS, которая содержит имена экспортируемых функций DLL-библиотеки и их ординалы, то есть порядковые номера этих функций в DLL-библиотеке. Последние использовались в 16-разрядных версиях Windows, однако в современных версиях этой операционной системы их использование необязательно, и Microsoft настоятельно рекомендует этого не делать.
О том, что компоновщик должен создать именно DLL-библиотеку, указывают с помощью ключа /Tpd.

Шаг 3. Создание lib-файла

Как указать приложению местонахождение внешних функций, расположенных в DLL-библиотеках? Если бы приложение использовало только одну DLL-библиотеку, то проблем бы не было — указывай нужную и продолжай процесс сборки приложения. Если количество необходимых приложению DLL-библиотек больше одной, а тем более если их десятки, то ситуация требует иного решения, нежели простое перечисление нужных приложению DLL-библиотек. Для централизованного хранения информации о размещении используемых приложением функций в DLL-библиотеках применяют LIB-файлы. Эти файлы представляют собой своеобразный справочник о размещении функций в DLL-библиотеках. При этом не указывается никаких путей, так как при обращении к DLL-библиотеке операционная система ищет ее по следующему алгоритму.

  1. В каталоге, содержащем ехе-файл приложения.
  2. В текущем каталоге процесса.
  3. В системном каталоге Windows.
  4. В основном каталоге Windows.
  5. В каталогах, указанных в переменной окружения PATH.

В пакете TASM для создания LIB-файла предназначена утилита Implib.exe. Для создания LIB-файла в нашем примере необходимо выполнить следующую командную строку:

IMPLIB.EXE maketjll .lib maket_dll.DLL >p.txt

Как видите, мы опять используем перенаправление вывода в файл p.txt для быстрой оценки результата работы программы IMPLIB.EXE. Если выполнение этой утилиты было успешным, то формируется файл maket_dll.lib, который в дальнейшем используется для сборки целевого приложения.

Шаг 4. Сборка приложения с использованием DLL-библиотеки

Приведем содержимое make-файла для сборки целевого приложения:

NAME = maket
OBJS = $(NAME).obj
DEF = $(NAME).def
lif Sd(DEBUG)
TASMDEBUG=/zi
LINKDEBUG=/v
'.else
TASMDEBUG=
LINKDEBUG=
lendif
TASMOPT=/m3 /z /q # /DWINVER=0400 /D_WIN32_WINNT-0400
# /mx
lif Sd(MAKEDIR)
IMPORT=$(MAKEDIR)\import32+maket_dll
lelse
IMPORT=import32+maket_dl1
lendif
$(NAME).EXE: $(OBJS) $(DEF)
tlink32 /Tpe /aa /x /c $(LINKDEBUG) $(OBJS).$(NAME).. $(IMPORT). $(DEF) .asm.obj:
del $(NAME).EXE
tasm32 $(TASMDEBUG) /ml $(TASMOPT) $&.asm...

Теперь, имея два make-файла (для сборки файлов .dll и .ехе ), можно провести сравнительный анализ их содержимого. Отметим два момента:
Ш в макропеременной IMPORT указываются имена (без расширений) LIB-фай-лов, содержащих сведения о нужных приложению функциях в DLL-библиотеках (если LIB-файлов несколько, то они перечисляются с использованием знака +);
ш для сборки ехе-приложения используется ключ компоновщика. Содержимое DEF-файла maket.def приложения:

NAME maket
DESCRIPTION 'Assembly Console Windows Program'
CODE PRELOAD MOVEABLE DISCARDABLE
DATA PRELOAD MOVEABLE MULTIPLE
EXPORTS

И наконец, содержимое самого файла maket.asm, использующего функцию из разработанной нами DLL-библиотеки maket_dll.dll.

: maket.asm - программа, вызывающая функцию WriteCon из файла maket_dll.dll
includelibmaket_dll .lib необязательно
.data
TitleText db "Строка выводится процедурой из DLL"
Lenjitl eText-$ - Ti tl eText
.code
start proc near ;точка входа в программу:
:работаем .........
push Len_TitleText
push offset TitleText
call WriteCon exit: ;выход из приложения

Импортируемую из DLL-библиотеки функцию необходимо объявить внешней директивой extrn WriteCon:PROC.

Шаг 5. Проверка работоспособности приложения с использованием DLL-библиотеки

Для проверки работоспособности полученного на предыдущем шаге приложения можно использовать отладчик TD32.EXE. Кстати, когда вы будете в нем работать, обратите внимание на то, как происходит переход из DLL-библиотеки на код в процедуре. Вы увидите, что помощь в этом оказывает неизвестно откуда появившаяся команда JMP. Причину этого вы можете выяснить, прочитав раздел «Секция описания импортируемых функций РЕ-файла» главы «Форматы исполняемых файлов» книги.
При разработке DLL-библиотек естественным образом возникает вопрос о совместимости с приложениями, разработанными на других языках. Это тем более актуально, если речь идет о продуктах разных фирм-производителей программного обеспечения. Проверить, насколько совместима разработанная нами с помощью средств TASM DLL-библиотека, можно с помощью утилиты DumpBin.exe из пакета Microsoft Visual Studio. Запустите ее командной строкой вида:

DUMPBIN.EXE -exports maketjJll.DLL>p.txt

Тогда в файле p.txt вы получите отчет о содержимом раздела экспорта DLL-библиотеки maket_dll.dll. Проанализировав полученные результаты, вы убедитесь, что проблем с распознаванием нашей DLL-библиотеки у этого программного средства фирмы Microsoft не возникло. Это дает основание полагать, что данную библиотеку при соответствующем наполнении полезными функциями можно использовать при программировании на VisualC/C++, VisualBasic и т. п. При этом необходимо иметь в виду, что может иметь место искажение имен функций при использовании компиляторов различных фирм. Подробнее об этом можно узнать в соответствующей литературе.
Не следует забывать, что на практике возможны три формы загрузки DLL-библиотеки в адресное пространство процесса: неявная, явная и отложенная. Описанный выше способ сборки приложения на самом деле был неявным и предполагал, что загрузка DLL-библиотеки производится при запуске самого приложения. Явный способ загрузки DLL-библиотеки предполагает ее загрузку во время работы приложения. Для этого в Win32 API существуют специальные функции:

HINSTANCE LoadLibraryC LPCTSTR lpLibFileName ):
HMODULE LoadLibraryExtLPCTSTR lpLibFileName,HANDLE hF1le, DWORD dwFlags):

Третий способ загрузки DLL-библиотек — отложенная загрузка. Этот вид загрузки предполагает, что DLL-библиотека не будет загружена в адресное пространство процесса до тех пор, пока приложению не потребуется осуществить доступ к любому экспортируемому из этой DLL-библиотеки объекту (переменной, константе, процедуре). Подробнее об этом и других вопросах разработки и использования DLL-библиотек можно прочитать в литературе.


Обработка цепочек элементов

Продавай мобильный контент со своего сайта

Обработка цепочек элементов

  • Прямой поиск в текстовой строке
  • Поиск с предварительным анализом искомой подстроки

Конструктор знает, что он достиг совершенства
не тогда, когда нечего больше добавить,
а тогда, когда нечего больше убрать.
Антуан де Сент-Экзюпери

Ниже будет приведено несколько программ, демонстрирующих алгоритмы поиска данных в строках символов, то есть цепочках элементов размером 1 байт. Так как все они построены на основе стандартных байтовых цепочечных команд микропроцессора, то при необходимости обработки последовательностей элементов большей размерности (2 и 4 байта) их доработка не составит вам особого труда.
Организацию поиска одиночного символа в программе на ассемблере мы рассматривать не будем, так как это делается самими цепочечными командами без привлечения посторонней алгоритмической поддержки. Информацию об этом можно получить из учебника.
Более подробно мы рассмотрим организацию поиска текстовой подстроки в строке символов, превышающей размер искомой подстроки. При всей кажущейся простоте этого вида поиска его реализация в программах на языке ассемблера сопряжена с рядом проблем.
Введем некоторые обозначения:

  • Р — строка-аргумент поиска, размерность строки Р - М байт, j — индекс символа в строке Р, 0 <j < М-1;
  • S- строка, в которой ведется поиск строки Р, размерность строки S - N байт, i — индекс символа в строке S, 0 < i < N-1.

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

Прямой поиск в текстовой строке

«Корень зла есть незнание истины», — сказал Будда.
Из этого же корня вырастает дерево заблуждения
со своими тысячными плодами страдания.

Цель поиска некоторой строки Р в строке большего размера S — определить первый индекс элемента в строке S, начиная с которого все символы S совпадают с символами строки Р. Для этого алгоритм поиска последовательно просматривает символы строки S, проводя одновременное сравнение ее очередного символа с первым символом строки Р. После возникновения такого совпадения алгоритм производит последовательное сравнение соответствующих элементов строк S и Р до возникновения одного из следующих условий:

  • в процессе поиска соответствия достигнут конец строки Р — это означает,
    что строка Р совпадает с некоторой подстрокой строки S;
  • достигнут конец строки S при незавершенном или неначатом просмотре строки Р — это означает, что строка Р не соответствует ни одна из подстрок S.

Одна из главных проблем, которую приходится решать при написании программы обработки символьной строки, — определение конца строки S. Здесь возможны два варианта:

  • статический — размер строки фиксирован некоторым значением N;
  • динамический (характерен для обработки массивов символьных строк) — длина строки определяется значением, являющимся либо первым элементом очередной строки, либо концевым (служебным) символом, значение которого заранее определено и не может совпадать ни с одним символом строки.

Начнем обсуждение прямого способа поиска с программы поиска в строке с фиксированной длиной. Для экономии места ограничим число вхождений Р в S рдним.

;prg4_67_f.asm - поиск строки Р в строке S, Длина S фиксирована.
;Вход: S и Р - массивы символов размером N и М байт (М=<N.
:Выход: сообщение о количестве вхождений строки Р в строку S.
'.data"
:задаем массив S
s db "Ax. какой был яркий день! Лодка, солнце, блеск и тень, и везде цвела сирень."
Len_S=$-s
Db "$" mes db "Вхождений строки - "
:задаем массив Р - аргумент поиска
р db "ень"
1_еп_Р=$-р
db " - " Count (Jb 0."$" :счетчик вхождений Р a S
.code
eld
movcx.len_s
lea di ,s
moval ,p ;P[0]->al next_search:
lea si,p
incsi ;на следующий символ repne scasb
jcxz exit push ex
mov cx.len_p-l repe empsb
jz eq_substr :строка p <> подстроке в s
mov bx.len_p-l
sub bx.cx pop ex
subcx.bx ;учли пройденное при сравнении empsb
jmp next_search eq_substr:
;далее можно выйти, если поиск однократный, но мы упорные, поэтому продолжаем... рорех
sub сх.1еп_р-1 ;учли пройденное при сравнении empsb
inc count
jmp next_search exit: add count,30h :вывод сообщения mes на экран

Из программы видно, что когда размер строки фиксирован, то проблема конца строки решается просто. Но чаще приходится иметь дело с задачами, выполняющими поиск подстроки в строке, длина которой заранее не известна. Это характерно, в частности, для приложений обработки файлов. Но и с файлами не так все просто. Для текстовых ASCII-файлов особых проблем нет — в них строки заканчиваются символами Odh, Oah. Сложнее дело обстоит с обработкой двоичных файлов, где с равной степенью вероятности могут встретиться любые символы кодовой таблицы. В подобных случаях проблему локализации места в файле, где осуществляется поиск, нужно решать исходя из постановки конкретной задачи. Несмотря на это, сами приемы поиска не сильно отличаются от рассмотренных в этом разделе.
В случае когда поиск осуществляется в строке или в массиве строк в памяти, длина которых заранее не известна, то для обозначения их окончаний нужно ввести некоторый служебный символ. Например, в языке Паскаль существует понятие строки ASCII-Z, представляющей собой обычную символьную строку с завершающим нулевым символом. По этому символу и определяется конец строки. Подобные служебные символы можно использовать и для обработки строк
¦ в памяти. Другая возможность задания границы строки — введение в структуру строки специального байта, обычно первого, содержащего длину этой строки.
Следующая программа демонстрирует возможную организацию поиска в текстовом файле. Для этого содержимое файла читается в динамически выделяемую область памяти. После небольшой модернизации данную программу можно рассматривать как основу для других программ поиска в строках памяти, ограниченных некоторым служебным символом, как это обсуждалось выше. Программа производит поиск слова «шалтай» в строках файла. На экран выводится номер строки, в которой встретилось это слово, и количество повторений этого слова в файле. В такой постановке задачи возникает проблема — необходимо отслеживать наступление одного из двух событий: обнаружение первого символа образца или обнаружение первого из пары символов OdOah, обозначающих конец строки. Вариант использования цепочечных команд из предыдущей программы не подходит, так как сканировать строку можно на предмет наличия только одного символа. Поэтому данную задачу можно реализовать двумя способами. Первый способ заключается в последовательном чтении и проверке каждого символа строки на предмет удовлетворения их одному из обозначенных выше событий. При втором способе каждая строка файла сканируется два раза. На первом проходе определяется размер очередной строки, а затем эта строка сканируется второй раз на предмет наличия в ней искомой подстроки. Достоинство второго способа состоит в том, что его можно реализовать только использованием цепочечных команд. Какой из способов будет работать быстрее, можно определить с помощью профайлера. Мы выберем второй способ по двум причинам: во-первых, в этом разделе нас интересуют варианты использования цепочечных команд; во-вторых, в одной программе мы продемонстрируем приемы работы со строкой, размер которой определяется динамически двумя способами: со служебным символом в конце (им будет Odh) и извлекаемым из байта в начале строки. В нашей программе байт со значением длины очередной строки будет эмулироваться первым проходом.

start proc near :точка входа в программу:
;для размещения файла используем кучу, выделяемую процессу по умолчанию (1 Мбайт)
;HANDLE GetProcessHeap (VOID);
call GetProcessHeap
inov Hand_Head.eax сохраняем дескриптор ;читаем файл в динамически выделяемую область памяти ;открываем файл
:HANDLE CreateFiIeCLPCTSTR ipFileName.DWORD dwDesiredAccess.DWORD ;dwShareMode.LPSECURITY_ATTRIBUTES 1pSecuri tyAttributes.DWORD
:dwCreationDisposition.DWORD :dwFlagsAndAttributes.HANDLE hTemplateFi1e):
call CreateFileA
cmp eax.Offffffffh
je exit :если неуспех
mov hFile.eax :дескриптор файла определим размер файла :DWORD GetFi1eSize(HANDLE hFile.LPDWORD ipFileSizeHigh);
call GetFileSize
cmp eax.O
jz exit :если неуспех
mov FileSize.eax :сохраним размер файла :запрашиваем блок памяти из кучи:
:LPVOID HeapAlloc(HANDLE hHeap. DWORD dwFlags, DWORD dwBytes): ;.........
call HeapAlloc
mov buf_start,eax :адрес блока с текстом программы из общей кучи процесса :читаем файл
:BOOL ReadFile(HANDLE hFile.LPVOID ipBuffer.DWORD nNumberOfBytesToRead. ;LPDWORD lpNumberOfBytesRead.LPOVERLAPPED lpOverlapped):
:.........
call ReadFile
cmp eax.O
jz exit :если неуспех push ds pop es
eld
mov ecx.FileSize
movedi.buf_start :адрес буфера с текстом файла в edi cycll: push ecx push edi
mov ebx.ecx
moval.Odh :0dh ->al repne scasb
jcxz e_exit
jmp $+7 e_exit: jmp exit pop edi
sub ebx.ecx
xchg ebx.ecx
mov al.p :P[0]->al next_search: repne scasb
jcxz end_str:достигнут конец строки проверяем возможное совпадение
push ecx lea esi.p
mov ecx.len_p-l repe empsb
jz eq_substr :строка р <> подстроке в s
mov edx.len_p-l
sub edx.ecx
sub ecx.edx :учли пройденное при сравнении empsb
jmp next_search end_str: incedi
xchg ebx.ecx преобразуем в символьное представление счетчик вхождений count
:вывод на экран строки mes call WriteConsoleA
mov count.0 обнуляем счетчик вхождений в строку pop ecx :1 - восстанавливаем
sub ecx.len_p-учли пройденное при сравнении empsb
inc count
jmp next_search
exit: pop ecx

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

Поиск с предварительным анализом искомой подстроки

Против незнания есть только одно средство — знание.
Истинное же знание может быть достигнуто
только через личное совершенствование.
Л. Н. Толстой

Основу материала этого раздела составляет алгоритм КМП-поиска. Имя «КМП» является выборкой первых букв фамилий его создателей: Д. Кнута, Д. Мориса
и В. Пратта. В основе алгоритма КМП-поиска лежит идея о том, что в процессе просмотра строки S с целью поиска вхождения в нее образца Р становится известной информация о просмотренной части. Работа алгоритма производится в два этапа.

  1. Анализируется строка-образец Р. По результатам анализа заполняется вспомогательный массив смещений D.
  2. Производится поиск в строке S с использованием строки-образца Р и массива смещений D.

Ниже приведена программа, реализующая алгоритм КМП-поиска.

//рrg4_73_КМР - программа на псевдоязыке поиска строки Р в строке S
//по алгоритму КМП-поиска. Длина S фиксирована. ..<. ¦ ¦¦ ¦¦
// Вход: S и Р - массивы символов размером N и М байт (M=<N)
II Выход: сообщение о количестве вхождений строки Р в строку S.

ПЕРЕМЕННЫЕ .
INT_BYTE s[n]://0=<i<N-l
INT_BYTE p[m]://0-<j<M-l .
INT_BYTE d[ml://массив смещений
INT_BYTE k=-l: i=0: j-0 //индексы
НАЧ_ПРОГ .
//этап 1: формирование массива смещений d
' И j:-0; k:-l:'d[0]
ПОКА ДЕЛАТЬ
НАЧ_БЛОК 1 .
nOKA~"((k>=0)M(p[j]<>p[k])) k:-d[k]
j:=j+l: k:-k+l :
ЕСЛИ p[j]-p[k] TO d[j]:-d[k] .
ИНАЧЕ d[j]:-k .
кон_блок_1 .
//этап 2: поиск
i:-0: j:-0: k:=0
ПОКА ((j<M)M(i<N)) ДЕЛАТЬ
НАЧ_БЛОК_1 ' '" '"'
ПОКА ((j>=O)H(s[i]<>p[j])) j:-dtj]
j:-j+l: i:=i+l
КОН_БЛОК_1
ЕСЛИ j=M TO зывод сообщения об удаче поиска
ИНАЧЕ вывод сообщения о неудаче поиска КОН ПРОГ
jmpm3
ml: :ЕСЛИ j=M TO вывод сообщения об удаче поиска :вывод сообщения о результатах поиска
cmp si ,len_p
jneexit_f :ИНАЧЕ вывод сообщения о неудаче поиска
inc count
cmp di ,len_s
jgeexit_f
xor si ,si
jmp m34
exit_f:add count.30h :вывод сообщения mes на экран

Подробно, хотя и не очень удачно, алгоритм КМП-поиска описан у Вирта [4]. Этот алгоритм достаточно сложен для понимания, хотя в конечном итоге его идея проста. Центральное место в алгоритме КМП-поиска играет вспомогательный массив D. Поясним его назначение. Массив D содержит значения, которые нужно добавить к текущему значению j в позиции первого несовпадения символов в строке S и подстроке Р (рис. 4.1).

Рис 4.1. Пример КМП-поиска

Из обсуждения выше можно сделать два вывода — один приятный, другой не очень. Во-первых, явное достоинство этого метода в том, что исключены возвраты
:задаем массив S

s db "fgcabceabcaab"
Len_S=$-s :N=Len_S
db Count db 0 ;счетчик вхождений Р в S
Db " раз(а)$"
d db 255 dup (0) вспомогательный массив ¦' ¦ k db 0 .,.,.,'
.code
:этап 1 - заполнение массива D значением М - размером образца для поиска
mov ex.255 :размер кодовой таблицы ASCII
moval.lenjj :ДЛЯ j=0 ДО 255 ДЕЛАТЬ
lea di .d rep stosb :d[j]:=M
:цикл просмотра образца и замещение некоторых элементов d значениями смещений :(см. пояснение за текстом программы)
xor si .si ; j:=0 '
cycll: :ДЛЯ j>0 ДО М-2 ДЕЛАТЬ ..>;
empsi ,1еп_р-1
jgee_cycll
mov al ,p[si]
movdi.ax
movbl.len_p
decbl
subbx.si
movd[di],bl :d[p[j]]:-MrJ-l:!
inc si ¦, -.r
e_cycll: ://этап 2: поиск
movdi,len_p
cycl 2:,; ПОВТОРИТЬ - цикл пока (j>-0)WW(I<n) . movsi.len_p :j:=M . , ;¦
movbx.di :k:=I . '.
cycl3: :цикл пока (j>-O)MJlH(p[j]~p[k])
decbx :k:-k-l . ' '¦' '
dec si :j:-j-l cmp si.0 jl m2
mov al.p[si] cmps[bx].al jnem2 jmpcycl3 m2: ;i:-i+d[s[i-: push di dec di
mov al,s[di] mov di ,ax moval .dfdi] popdi
add di .ax cmp s i, 0 jl ml cmp di .len_s
jg exi t_f
jmp cycl2 ml: ;вывод сообщения о результатах поиска
inc count
jmpcycl2 exit_f:add count.30h
lea dx.mes
mov ah.09h
int 21h exit:

Идея алгоритма БМ-поиска в том, что сравнению подвергаются не первые, а последние символы образца Р и очередного фрагмента строки S. Если они не равны, то сдвиг в строке S осуществляется сразу на всю длину образца. Если последние символы равны, то сравнению подвергаются предпоследние символы, и т. д. При несовпадении очередных символов величина сдвига извлекается из таблицы D, которая, таким образом, выполняет ключевую роль в этом алгоритме. Заполнение таблицы происходит на основе конкретной строки-образца Р следующим образом. Размер таблицы определяется размером алфавита, то есть количеством кодов символов, которые могут появляться в тексте. В нашем случае мы отвели под таблицу D память длиной, равной длине кодовой таблицы ASCII. Таким образом, строки S и Р могут содержать любые символы. Первоначально все байты кодовой таблицы заполняются значением, равным длине строки-образца для поиска Р. Далее последовательно извлекаются символы строки-образца Р начиная с первого. Для каждого символа определяется позиция его самого правого вхождения в строку-образец Р. Это значение и заносится в таблицу D на место, соответствующее этому символу. Подробнее получить представление о заполнении таблицы можно по фрагменту программы на псевдоязыке:

//этап 1: формирование массива d .ДЛЯ j-О ДО 255 ДЕЛАТЬ НАЧ_БЛ0К_1 d[j]:-M К0Н_БЛ0К_1
ДЛЯ j=0 ДО М-2 ДЕЛАТЬ НАЧ_БЛОК_1
d[p[j]]:-M-j-l КОН_БЛОК_1

Так, для строки abcdabce процесс и результаты формирования таблицы D показаны на рис. 4.2.
Куда в двух последних программах пристроить цепочечные команды? К сожалению, некуда. Честно говоря, когда автор писал текст этих программ, он настолько увлекся, что напрочь забыл как о них, так и о цели настоящего раздела — показать особенности использования этих команд при организации поиска информации. А когда вспомнил, то сделал вывод, что пристроить их вроде бы и некуда. А вы как думаете? Окончательный вывод об эффективности можно сделать по результатам работы профайлера.
Развитие этой темы лежит в плоскости обсуждения проблемы обработки файлов, к которой мы когда-нибудь вернемся.

Рис. 4.2. Формирование массива D в программе БМ-поиска


Работа с консолью в программах на ассемблере

Продавай мобильный контент со своего сайта

Работа с консолью в программах на ассемблере

  • Функции BIOS для работы с консолью
    • Функции BIOS для работы с клавиатурой
    • Функции BIOS для работы с экраном
  • Функции MS DOS для работы с консолью
    • Функции MS DOS для ввода данных с клавиатуры
    • Функции MS DOS для вывода данных на экран
  • Работа с консолью в среде Windows
    • Организация ввода-вывода в консольном приложении Windows

Бросая в воду камешки, смотри на круги, ими образуемые;
иначе бросание будет пустой забавой.
Козьма Прутков

На практике редко возникает необходимость разработки программы как «вещи в себе». В подавляющем большинстве случаев программа должна взаимодействовать с пользователем, получая от него данные посредством клавиатуры и выдавая результаты своей работы на экран. При знакомстве с новым языком программирования одним из первых вопросов, на которые ищет ответа программист, является выяснение средств этого языка для выполнения операций обмена с консолью (консоль — клавиатура и монитор). Что касается языка ассемблера, то собственных средств обмена с консолью у него нет. Чтобы выполнить подобную операцию, программа использует возможности самого компьютера (прерывания BIOS) и операционной системы, в среде которой эта программа работает. Каждый программист самостоятельно ищет решение проблемы обмена с консолью. Так как эта задача актуальна всегда, то есть необходимость на конкретных примерах показать порядок использования средств BIOS и ОС для обмена с консолью в программах на ассемблере. Примеры не очень сложны, и читатель легко сможет быстро встроить их в свои программы.

Функции BIOS для работы с консолью

В контексте нашего изложения ROM BIOS (Read Only Memory Basic Input Output System) представляет собой совокупность программ в энергонезависимой памяти компьютера, одной из задач которых является устранение специфики аппаратных компонент компьютера для функционирующего на нем программного обеспечения, включая операционную систему. Обслуживание клавиатуры и монитора выполняют программы BIOS, называемые драйверами. Структурно драйверы состоят из ряда подпрограмм, называемых функциями, каждая из которых выполняет определенные действия. Обращение к функциям BIOS производится аналогично обращению к функциям MS DOS. Для работы с клавиатурой и экраном BIOS содержит два программных прерывания — 16h и 10h, обращение к которым, исходя из вышесказанного, является обращением к драйверам этих устройств. Для вызова этих прерываний, как обычно, используется команда INT — int 16h или int 10h. Для выполнения определенной операции в регистре АН указывается номер функции. При необходимости в других регистрах может указываться дополнительная (параметрическая) информация. Ниже рассмотрим подробнее возможности BIOS для работы с консолью.

Функции BIOS для работы с клавиатурой

Прерывание 16 BIOS имеет функции для различных типов клавиатур: обычной — 84 клавиши и двух типов расширенной клавиатуры — 101\102 и 122-клавишной. Выяснить функциональные возможности клавиатуры позволяет функция 09п:

Вход: АН - 09h.
Выход: AL = битовое поле, установленные биты которого обозначают поддерживаемые функции: 7 - резерв; 6 — поддержка клавиатуры со 122 клавишами (и функций 20h-22h (int 16h)); 5 — поддержка расширенной клавиатуры со 101-102 клавишами (и функций 10h-12h (int 16h)); 4 — поддержка функции 0Ah (int 16h); 3 — поддержка функции 0З0бп (int 16h); 2 — поддержка функции 0305h (int 16h); 1 — поддержка функции 0304h (int 16h); 0 — поддержка функции 0З00п (int 16h).

Прежде чем вызывать эту функцию, необходимо удостовериться в том, что
она поддерживается данной версией BIOS. Сделать это можно, вызвав функцию
OcOh прерывания int 15h.

Вход: АН = COh получить конфигурацию.
Выход: CF = 1 — BIOS не поддерживает эту функцию; CF - 0 — в случае успеха: ES:BX — адрес конфигурационной таблицы в ROM-памяти; АН = состояние (ООп — успех; 8бп — функция не поддерживается).

Формат конфигурационной ROM-таблицы:

Смещение | Размер | Описание
00h | 2 байта | Число байтов в этой таблице
02h | 1 байт | Модель BIOS
03h | 1 байт | Подмодель BIOS
04h | 1 байт | Издание BIOS: 0 — 1-я редакция, 1 — 2-я редакция и т. д.
05h | 1 байт | 1-й байт свойств
06h | 1 байт | 2-й байт свойств
07h | 1 байт | 3-й байт свойств
08h | 1 байт | 4-й байт свойств
09h | 1 байт | 5-й байт свойств

Если в результате этого вызова бит б второго байта свойств установлен, то BIOS поддерживает функцию 09п прерывания int 16h, с помощью которой определяются функциональные возможности клавиатуры.

Вход: АН = 10h, 20h чтение символа с ожиданием (для 101-102- и 122-клавиш-ных клавиатур соответственно).
Выход: для обычных клавиш (АН = скан-код BIOS; AL = символ ASCII); для клавиш и комбинаций с расширенным кодом (АН = расширенный ASCII-код; AL = 0); для дополнительных клавиш (АН - расширенный ASCII-код; AL = 0Eh).
Для ввода строки символов данные функции необходимо использовать в цикле. На примере показанной ниже программы, используя отладчик, можно исследовать содержимое АХ при нажатии различных клавиш и их комбинаций.

;prg05_02.asm - программа на ассемблере для ввода строки ;с использований функции ввода символа 10h
.data
string db 5 dup (0) len_string =$-string adr_stringdd string .code
mov cx,len_string
les di.adr_string ml: mov ah.O10h
int 16h
stosb
loop ml

Программа вводит 5 символов и сохраняет их в строке str.

Проверка наличия символа (01h, 11h, 21h int 16h)

Вход: АН = Olh проверка наличия символа (для 84-клавишной клавиатуры).
Выход: если ZF=O, то регистры АН и AL содержат: для обычных клавиш (АН = скан-код BIOS; AL = символ ASCII); для клавиш и комбинаций с расширенным ASCII-кодом (АН = расширенный ASCII-код; AL = 0); если ZF=1, то буфер пуст.

Функция 01h получает информацию о символе, не считывая его из буфера клавиатуры. Исключение составляют нажатия дополнительных клавиш на расширенных клавиатурах, не совместимых с 83\84-клавишными клавиатурами. В процессе проверки функцией Olh они удаляются из буфера. Поэтому при работе с расширенными клавиатурами необходимо использовать функции 11h и 21h.

Вход: АН = llh, 21h проверка наличия символа (для 101-102- и 122-клавишных клавиатур соответственно).
Выход: если ZF=O, то регистры АН и AL содержат: для обычных клавиш (АН = BIOS скан-код; AL - символ ASCII); для клавиш и комбинаций с расширенным кодом (АН = расширенный ASCII-код; AL = 0); для дополнительных клавиш (АН = расширенный ASCII-код; AL = 0eh); если ZF=0, то буфер пуст. В большинстве случаев работу с результатами выполнения данной функции логично начинать с анализа флага ZF (командами JZ или JNZ). Что же касается содержимого регистра АХ, то оно аналогично содержим00h int 16h :пересылаем его:
stosb
jmpml

Получение состояния флагов клавиатуры (02h, 12h, 22h int 16h)

BIOS предоставляет функцию 02h для получения состояния световых индикаторов клавиатуры и некоторых управляющих клавиш.
Вход: АН = 02h получить состояние флагов клавиатуры (для 84-клавишной клавиатуры).
Выход: AL = битовое поле, установленные биты которого соответствуют состоянию следующих флагов: 7 — режим вставки активен; 6 — индикатор CapsLock включен; 5 — индикатор NumLock включен; 4 — индикатор ScrollLock включен; 3 — нажата клавиша Alt (любая клавиша Alt на 101-102-клавишной клавиатуре); 2 — нажата клавиша Ctrl (любая клавиша Ctrl на 101-102-клавишной клавиатуре); 1 — нажата левая клавиша Shift; 0 — нажата правая клавиша Shift.
Поддержка расширенных клавиатур осуществляется функциями 12h и 22h BIOS.
Вход: АН = 12h, 22h получить состояние флагов клавиатуры (для 101-102- и 122-клавишных клавиатур).
Выход: AL = первое битовое поле, установленные биты которого соответствуют состоянию флагов, возвращаемых в регистре AL функцией 02п; АН = второе битовое поле, установленные биты которого соответствуют следующему состоянию флагов: 7 — нажата клавиша SysReq (SysRq); 6 - нажата клавиша CapsLock; 5 — нажата клавиша NumLock; 4 — нажата клавиша Scrolllock; 3 — нажата правая клавиша Alt; 2 — нажата правая клавиша Ctrl; 1 — нажата левая клавиша Alt; 0 — нажата левая клавиша Ctrl.
Кроме этого, состояние данных флагов можно прочитать из оперативной памяти по адресам: 0040h:0017h (AL) и O040h:0010h (АН).

Запись символа в буфер клавиатуры (05h int 16h)

Вход: АН = 05h запись символа в буфер клавиатуры: СН = скан-код; CL = символ ASCII.
Выход: AL = состояние: 00h — успешная запись; Olh — ошибка (буфер клавиатуры заполнен).
С помощью этой функции можно осуществлять подыгрыш для программ, которые ожидают ввода с клавиатуры. Сам буфер клавиатуры организован по принципу кольца, имеет размер 16 байт и занимает в памяти диапазон адресов 0040h:001Eh...0040h:003Dh. В ячейке 0040h:001Ah хранится адрес начала буфера, а в ячейке 0040h: OOlCh — адрес конца. Если содержимое этих ячеек равно, то буфер пуст. Одному символу в буфере соответствует слово, в котором первый байт — скан-код клавиши, а второй — символ ASCII. Исследовать данную функцию можно с использованием операции сцепления (|) MS DOS. Для этого оформим фрагмент кода для определения наличия символа в буфере и его ввода в виде отдельной программы.

В командную строку MS DOS необходимо ввести строку: prog_1.exe | prog_2.exe >p.txt
В результате всех этих действий будет создан файл p.txt, который и будет содержать строку str из файла prog_1.asm.

Функции BIOS для работы с экраном

Работа с экраном средствами BIOS производится с помощью набора функций прерывания 10h. С помощью этих функций поддерживаются текстовый и графический режимы работы монитора. В данном разделе будут рассмотрены некоторые функции вывода текста в текстовом режиме.

Установка видеорежима (00h int 10h)

Любой дисплейный адаптер поддерживает несколько текстовых и графических режимов. Переключение между эт000h режимами производится с помощью функции 00h int 10h.
Вход: АН = 00h установить видеорежим: AL - номер видеорежима (если бит 7 регистра AL = 0, то экран очищается, в обратном случае (AL. 7=1) содержимое экрана не изменяется).
Номеров видеорежимов много, нумерация режимов с высоким разрешением (SVGA) зависит от производителя видеоадаптера. Мы не будет приводить никаких сведений по этому поводу, при необходимости информацию о нумерации видеорежимов можно получить из соответствующих источников.

Установка позиции курсора (02h int 10h)

Функция 02h позволяет изменить позицию курсора и сделать ее начальной для последующего вывода. Заметим, что среди функций MS DOS нет подобной функции и функцию 02h int 10h BIOS можно использовать в комбинации с функциями MS DOS для организации форматированного вывода на экран. Вход: АН = 02h — установить позицию курсора: ВН = номер видеостраницы (зависит от используемого видеорежима); DH = строка (00h — верх); DL = ко-" лонка (00h — левая).

Получение позиции курсора (03h int 10h)

Функция 03h позволяет получить текущую позицию курсора. Аналогично сказанному выше, среди функций MS-DOS нет подобной функции и функцию 03h > int 10h BIOS также можно использовать в комбинации с функциями MS-DOS. Вход: АН = 03h — получить позицию курсора; ВН - номер видеостраницы (зависит от используемого видеорежима).
Выход: DH = строка текущей позиции курсора (00h — верх); DL - колонка текущей позиции (00h — левая); СН = номер начальной строки курсора; CL = номер последней строки курсора.

Запись символа и его атрибута в видеопамять (09h int 10h)

Функция 09h предназначена для записи ASCII-кода символа и его атрибута непосредственно в видеопамять, причем сделать это можно с количеством повторений, заданных в регистре СХ.
Вход: АН = 09h — запись символа и его атрибута в текущую позицию курсора: ВН = номер видеостраницы; AL = ASCII-код символа; BL = байт-атрибут; СХ = число повторений.
Для вывода одного символа содержимое регистра СХ должно быть равно 1. В текстовом режиме для СХ>1 вывод осуществляется до конца текущей строки, после чего переходит на другую строку.
Кодировка байта-атрибута в этой и других функциях производится в соответствии со следующими таблицами.

Номера битов | Значение
7 | Мигающий символ
6..4 | Цвет фона
3 | Символ яркого цвета
2..0 | Символ цвета

Биты | Цвет | Яркий цвет
000b | Черный | Темно-серый
001b | Синий | Светло-синий
010b | Зеленый | Светло-зеленый
011b | Голубой | Светло-голубой
100b | Красный | Светло-красный
101b | Пурпурный | Светло-пурпурный
110b | Коричневый | Желтый
111b | Светло-серый | Белый

Чтение символа и его атрибута из видеопамяти (08h int 10h)

В памяти видеоадаптера каждый символ представлен двумя байтами, содержащими ASCII-код символа и его байт-атрибут. Функция 08h BIOS позволяет прочитать код символа и его атрибут непосредственно из видеопамяти.
Вход: АН = 08h — чтение символа и его атрибута в текущей позиции курсора;
ВН = номер видеостраницы. Выход: AL = ASCII-код символа; АН = байт-атрибут.
Ниже приведена программа, которая устанавливает курсор в заданную позицию.

:prg05_04.asm. устанавливающая курсор в заданную позицию.
.code main:
xorbh.bh
mov dh.10
movdl.10
movah.02h
int 10h установили позицию курсора (10.10) записываем символ и атрибут в видеопамять
moval. "a"
mov bl,10001100b :атрибут - ярко-красный мигающий
movcx.5 ;повторить 5 раз
movah.09h
int 10h :прочитаем символ из текущей позиции видеопамяти:
mov ah,08h
int 10h : выясним текущую позицию курсора
хог bh.bh
mov ап.ОЗn
kint 10h установили позицию курсора (10.10)
:все результаты смотрим в отладчике

Важно отметить, что текущая позиция курсора после выполнения функций 08п и 09п осталась неизменной. Отсюда следует важный вывод о том, что при использовании этих функций необходимо также заботиться и о движении курсора функцией 02h. BIOS предоставляет функцию 0Eh, которая выводит символ в режиме телетайпа, предполагающем автоматическую корректировку текущей позиции курсора после вывода символа.

Запись символа в видеопамять (0Ah int 10h)

Функция 0Ah предназначена для записи ASCII-кода символа с текущим значением атрибута в данной позиции непосредственно в видеопамять, причем сделать это можно с количеством повторений, заданных в регистре СХ.
Вход: АН = 0Ah — запись символа в текущую позицию курсора; ВН = номер видеостраницы; AL = ASCII-код символа; СХ = число повторений.
Аналогично функции 09h текущая позиция курсора не изменяется.

Запись символа в режиме телетайпа (0Eh int 10h)

Функция 0Eh выводит символ в текущую позицию курсора с автоматическим ее смещением (в отличие от функций 09h и 0Ah).
Вход: АН = 0Eh — запись символа в текущую позицию курсора; ВН = номер видеостраницы; AL ¦¦ ASCII-код символа; СХ = число повторений.
Запись символа в последнюю позицию строки автоматически переводит кур-Ь cop в первую позицию следующей строки.

Вывод строки (13h int 10h)

Эта функция появилась в BIOS компьютеров архитектуры AT.
Вход: АН = 13h вывод строки (AT); AL = режим записи: бит 0 — после вывода курсор в конец строки; бит 1 — каждый символ в строке представлен двумя байтами: байтом с ASCII-кодом и байтом-атрибутом; бит 2..7 — резерв; ВН = номер видеостраницы; BL = байт атрибут, если строка содержит только символы (AL. 1=0); СХ = число символов в строке; DH, DL = строка и столбец начала вывода строки; ES: ВР — адрес в памяти начала строки.

Обратите внимание, что содержимое строки для вывода может быть двух типов: с байтом-атрибутом, сопровождающим каждый символ строки, и без байта-атрибута. В последнем случае строка состоит из одних кодов символов с единым значением байта-атрибута, указываемым в регистре BL.
Как видно из рассуждения выше, многие функции BIOS работают непосредственно с видеопамятью. Из-за того что для видеопамяти отводится определенный диапазон адресов (для текстового режима — это 0b800h:0000h), доступ к ней можно производить обычными командами работы с памятью микропроцессора, в том числе и цепочечными.

Перемещение в окне вверх (06h int 10h)

Функция 06h позволяет определить на экране окно, в котрром возможно прокрутить определенное количество строк вверх. При такой прокрутке верхние строки исчезают и снизу добавляются пустые строки.
Вход: АН = 06h — перемещение строк в окне вверх; AL = число строк для заполнения снизу; ВН = атрибут символов (цвет) в строке для заполнения; СН и CL = строка и столбец верхнего левого угла окна; DH и DL = строка и столбец нижнего правого угла окна.
Строки для заполнения снизу имеют цвет, определенный в ВН. Если указать AL=0, то окно очистится и заполнится строками с цветом, заданным байтом-атрибутом в ВН.
Ниже приведена программа вывода нескольких строк на экран, после чего она определяет окно на экране и прокручивает его на несколько строк вверх.

:prg05_05.asm - программа для работы с окном на экране.
.data
String db "dfsh3453637869uioraepBBanB"
Ien_str1ng »$-string
adr_stringdd string
. code
..........
movcx,25 ml: mov al ,1 :после вывода - курсор в конец строки
xorbh.bh :номер видеостраницы
movbl.7 : атрибут push ex
mov cx,len_string :длина выводимой строки
les bp.adr_string :адрес строки в пару ES:BP
mov ah,l3h
int l0h
incdh ;строка начала вывода
incdl : столбец начала вывода pop ex
loop ml ¦.определяем и прокручиваем окно вверх
mov al.4 :4 строки
mov bh. 0
mov ch, 5
mov cl .5 . mov dh. 10
mov dl.30
mov ah.06h
int 10h

Заметьте, что функция 06h достаточно гибко работает с курсором.
Перемещение в окне вниз (07h int 10h)
Функция 07h позволяет определить на экране окно, в котором возможно прокрутить определенное количество строк вниз. При такой прокрутке нижние строки исчезают и сверху добавляются пустые строки.
Вход: АН = 07h — перемещение строк в окне вниз; AL = число строк для заполнения сверху; ВН = атрибут символов (цвет) в строке для заполнения; СН и CL - строка и столбец верхнего левого угла окна; DH и DL = строка и столбец нижнего правого угла окна.
Строки для заполнения сверху имеют цвет, определенный в ВН. Если указать А1_=0, то окно очистится и заполнится строками с цветом, заданным в ВН. Структура байта атрибута аналогична описанной выше.

Функции MS DOS для работы с консолью

Ценность программы прямо пропорциональна
весу ее «выдачи».
Прикладная Мерфология

Функции MS DOS для работы с консолью сосредоточены в обработчике прерывания int 21h. Они представляют собой набор средств работы с консолью, занимающий промежуточное положение между программами пользователя и средствами BIOS. Для достижения большей эффективности некоторые из функций BIOS можно комбинировать с функциями MS DOS. Как пример такого полезного взаимодействия можно привести использование возможностей BIOS по работе с курсором. Как будет видно из приведенного ниже материала, среди функций MS DOS подобные средства отсутствуют. При выполнении конкретных практических заданий можно найти и другие полезные примеры взаимодействия.

Функции MS DOS для ввода данных с клавиатуры

Для ввода данных с клавиатуры можно использовать два вида функций: универсальную функцию 3fh (ввод из файла) и группу специализированных функций MS DOS ввода с клавиатуры.
Подробно использование функции 3fh для ввода данных рассматривается в главе 7, а здесь сосредоточимся на второй группе функций, в которую входит семь функций, отличающихся друг от друга следующими характеристиками:

  • ожиданием ввода при отсутствии символа в буфере клавиатуры или только проверкой буфера на наличие символов для ввода;
  • количеством вводимых символов;
    наличием эха при вводе, то есть дублированием вводимого с клавиатуры символа на экране;
  • восприимчивостью к сочетанию клавиш Ctrl+C (код 03h).

Чтение с эхом символа с клавиатуры (10h int 21h)

Функция 01h позволяет ввести один символ с клавиатуры. Если символа нет, то функция ожидает его ввода. Вводимый символ отображается на экране (эхо).
Вход: АН = 01h — чтение символа с эхом. "
Выход: AL = ASCII-код символа или 0.
На выходе функция помещает в регистр AL ASCII-код символа или 0. Наличие нуля в регистре AL говорит о том, что в буфере клавиатуры находится расширенный ASCII-код и необходимо повторить вызов функции с тем, чтобы прочитать его второй байт. Также функция 01h проверяет наличие в буфере символов нажатия комбинации Ctrl+C (Ctrl+Break), при обнаружении которых производится вызов прерывания int 23h.
Для ввода нескольких символов данную функцию необходимо использовать в цикле.

:prg05_06.asm - программа ввода нескольких символов функцией 01h 21h
;.........
.data
string db 5 dup (0)
len_string =$-string
adr_string dd string
.code
......movcx.len_sthng
lesdi.adr_string ml: mov ah.01h
int 21h
cmpal.0 расширенный код???
jnem2
обрабатываем расширенный код
jmp m3
ni2: stosb .формируем строку символов
mЗ: loop ml

Проверяя работу программы, вместо ввода очередного символа введите комбинацию Ctrl+C и посмотрите реакцию программы.

Прямой ввод с эхом символа с клавиатуры (06h int 21h)

Функция 06h также позволяет ввести один символ с клавиатуры. Но в отличие от функции 01h она не ожидает ввода при отсутствии символа в буфере. Вводимый символ отображается на экране (эхо).
Вход: АН = 06h — чтение символа с эхом без ожидания; DL = 0ffn — признак того, что функция 06h используется для ввода; если DL <> 0ffn, то функция используется для вывода символа (см. ниже).
Выход: если ZF=O, то AL = ASCII-код символа; если ZF-1, то символа в буфере нет. Результаты работы этой функции необходимо оценивать прежде всего по значению флага ZF. Если ZF=O, то функция поместила в регистр AL ASCII-код символа или 0. Наличие нуля в регистре AL говорит о том, что в буфере клавиатуры находится расширенный ASCII-код и необходимо повторить вызов функции с тем, чтобы прочитать его второй байт. Функция 06h не проверяет наличие в буфере символов нажатия комбинации Ctrl+C (Ctrl+Break).

Чтение без эха символа с клавиатуры (07h int 21h)

Функция 07h аналогична функции 01h, за исключением того, что вводит символ с клавиатуры без ожидания его ввода, без эха и без проверки нажатия комбинации Ctrl+C (Ctrl+Break). Вход: АН = 07h — чтение символа без эха. Выход: AL = ASCII-код символа или 0 (см. описание функции 01h int 21h).
Наличие нуля в регистре AL говорит о том, что в буфере клавиатуры находится расширенный ASCII-код и необходимо повторить вызов функции с тем, чтобы прочитать его второй байт.

Чтение без эха символа с клавиатуры (08h int 21h)

Функция 08h аналогична функции 01h, за исключением того, что вводит символ
с клавиатуры без отображения его на экране (без эха).
Вход: АН = 08h — чтение символа без эха.
Выход: AL = ASCII-код символа или 0 (см. описание функции 01h int 21h).
Наличие нуля в регистре AL говорит о том, что в буфере клавиатуры находится расширенный ASCII-код и необходимо повторить вызов функции с тем, чтобы прочитать его второй байт. Функция производит проверку нажатия комбинации Ctrl+C (Ctrl+Break), при наличии которого вызывается прерывание int 23h.

Ввод строки символов с клавиатуры (0ah int 21h)

Функция 0ah вводит строку символов в буфер памяти специального формата. Если символов в буфере клавиатуры нет, то функция ожидает их ввода. Конец ввода — нажатие клавиши Enter (0dh). Формат буфера:

  • первый байт буфера содержит количество символов для ввода с учетом символа 0dh, завершающего процесс ввода;
  • второй байт содержит реальное число введенных символов, но уже без учета завершающего символа 0dh;
  • начиная с третьего байта содержится строка введенных символов с завершающим символом 0dh, максимальная длина строки — 254 символа.

Вход: АН = 0ah — ввод строки в буфер (до 254 символов); DS:DX — адрес буфера, первый байт которого должен содержать количество символов для ввода.
Выход: введенная строка начиная с третьего байта буфера по адресу в DS:DX, длина строки во втором байте буфера.
Перед вызовом функции 0ah в первый байт буфера необходимо поместить значение максимальной длины строки. Если первый байт равен нулю, то вызов функции игнорируется и программа продолжает выполнение без ожидания ввода строки. Функция производит проверку нажатия комбинации Ctrl+C (Ctrl+Break), при наличии которого вызывается прерывание int 23h. Вводимая строка отображается на экране. Буфер ввода для данной функции лучше оформлять в виде структуры.

;prg05_07.asm - программа ввода строки функцией OAh int 21h
buf_Oahstruc
len_bufdb 11 :длина buf_0ah
len_in db 0 действительная длина введенного слова (без учета Odh)
buf_in db 11 dup (20h) ;буфер для ввода (с учетом Odh)
ends
.data
buf buf_0ah<>
adr_bufdd buf
.code
..........
:вводим 10 символов с клавиатуры
Ids dx,adr_buf
movah.Oah
int 21h обработка введенной строки

Получить состояние клавиатуры (0Bh int 21h)

Функция 0Bh проверяет наличие в буфере символа для ввода.
Вход: АН = 0Bh — проверка состояния клавиатуры.
Выход: AL = 0ffh — буфер клавиатуры содержит символ для ввода; AL = 0 — буфер клавиатуры пуст.
Данная функция формирует только логический результат — присутствует символ в буфере или буфер пуст, поэтому вызов функции 0Bh необходимо комбинировать с одной из функций извлечения символа из буфера ввода. Использование этой функции удобно для программ, управление которыми производится с клавиатуры, — типа командного процессора. В процессе своей работы они постоянно ожидают ввода пользователем управляющих команд, в связи с чем периодически проверяют входной буфер.
Функция производит проверку нажатия комбинации Ctrl+C (Ctrl+Break), при наличии которого вызывается прерывание int 23h.

Ввод с клавиатуры с предварительной очисткой буфера (ОСh int 21h)

функция 0Ch выполняет ввод, предварительно очищая буфер клавиатуры. Это удобно для предотвращения чтения из буфера оставшихся там символов, возможно, введенных ошибочно или случайно. Функция гарантирует, что программа получит именно те данные, которые ввел оператор. Важно отметить, что функция 0Ch выполняет только очистку буфера, ввод символа осуществляет одна из функций, номер которой указывается в регистре AL при вызове этой функции/ Вход: АН = 0Ch — ввод с клавиатуры с предварительной очисткой; AL = номер
функции (01h, 06h, 07h, 08h, 0ah). Выход: определяется функцией, указанной в AL при вызове функции.
Функция производит проверку нажатия комбинации Ctrl+C (Ctrl+Break), при наличии которого вызывается прерывание int 23h.

Функции MS DOS для вывода данных на экран

Для вывода данных на экран можно использовать два вида функций: универсальную функцию 40h (вывод в файл) и группу специализированных функций MS DOS вывода на экран.
Использование функции 40h уже рассматривалось в разделе, посвященном работе с файлами. Материал, представленный ниже, посвящен второй группе функций — функциям MS DOS для вывода символов на экран. В группу входят три функции. Рассмотрим их.

Вывод символа на экран (02h int 21h)

Функция 02h позволяет вывести один символ на экран. Вход: АН » 02h — вывод символа; DL = символ для вывода.
Функция 02h проверяет наличие в клавиатурном буфере символов нажатия комбинации Ctrl+C (Ctrl+Break), при обнаружении которых производится вызов прерывания int 23h. В процессе вывода функция реагирует на управляющие символы, такие как 0dh (возврат каретки), 0ah (перевод строки), 08h (курсор назад на один символ), 07h (звуковой сигнал) и т. д.
Для того чтобы вывести строку, необходимо использовать цикл.

;выводим строку string на экран
mov ex.len_string :длину строки
lea si.string ;адрес строки
mov ah. 02h ml: mov dl.byte ptr [si]
int 21h
inc si
loop ml

Прямой вывод символа на экран (06h int 21h)

Функция 06h выводит один символ на экран. Эта функция универсальна, так как "используется и для ввода (см. выше), и для вывода символа.
Вход: АН = 06h — вывод символа на экран; DL = символ для вывода (за исключением 0ffn).
Функция 06h не проверяет наличие в буфере символов нажатия комбинации Ctrl+C (Ctrl+Break). Порядок использования данной функции аналогичен порядку использования функции 02h.

Вывод строки на экран (09h int 21h)

Функция 09h выводит строку символов на экран. Строка должна обязательно заканчиваться символом $. Данную функцию удобно использовать для вывода на экран различных диагностических сообщений. Если требуется организовать вывод строк, длина которых формируется динамически, то лучше либо использовать упомянутую выше функцию 40h, либо выводить их в цикле, тело которого содержит одну из функций 02h или 06h.
Вход: АН = 09h — вывод строки на экран; DS: DX — адрес строки для вывода с завершающим символом $.
Функция 09h проверяет наличие в клавиатурном буфере символов нажатия комбинации Ctrl+C (Ctrl+Break), при обнаружении которых производится вызов прерывания int 23h. В процессе вывода функция реагирует на управляющие символы, такие как 0dh (возврат каретки), 0ah (перевод строки), 08h (курсор назад на один символ), 07h (звуковой сигнал) и т. д.
Приведенный ниже фрагмент показывает порядок применения функции 09h.

:prg05_09.asm - программа вывода строки на экран функцией 09h int 21h
.data
string db "Строка для вывода функцией 09h $"
adr_string dd string
.code
:выводим строку string на экран
Ids dx.adr_string ;адрес строки в DS:DX
mov ah.09h

Работа с консолью в среде Windows

Если ничто другое не помогает,
прочтите, наконец, инструкцию.
Прикладная Мерфология

В этом разделе будут рассмотрены средства для работы с консолью в среде операционной системы Windows. Как известно, Windows поддерживает работу двух типов приложений — оконных, в полной мере использующих все достоинства графического интерфейса, и консольных, работающих исключительно в текстовом режиме. Поэтому не следует путать понятие «консоли», используемое выше, с понятием «консольного приложения» Windows. В предшествующем материале под «консолью» подразумевались средства для ввода информации с клавиатуры и вывода ее на экран. Для однозначности изложения далее под термином «консоль» мы будем иметь в виду либо само консольное приложение, либо его видимую часть — окно консольного приложения.
Далее мы рассмотрим порядок ввода-вывода данных в консольное приложение для Windows, написанное на ассемблере. Организация ввода-вывода в оконном приложении Windows здесь рассматриваться не будет, так как в уроках 18 «Создание Windows-приложений на ассемблере» и 19 «Архитектура и программирование сопроцессора» учебника этот вопрос был рассмотрен очень подробно и полно. Что-либо существенное добавить к уже сказанному трудно. Данная книга рассматривается как практическое продолжение учебника, поэтому повторяться просто не имеет смысла. Что же касается организации работы с консольным приложением, то этот вопрос в учебнике был рассмотрен слабо — в контексте одной из задач урока 20 «ММХ-технология микропроцессоров Intel». Поэтому есть смысл рассмотреть его более систематично, попутно осветив проблемы ввода-вывода в консольном приложении Windows. Это тем более актуально, что при программировании на ассемблере необходимость написания консольного приложения возникает более часто, чем оконного. Причина проста — малыми затратами нам становятся доступны многие возможности API Win32.

Организация ввода-вывода в консольном приложении Windows

Язык ассемблера — язык системных программистов, исследователей принципов работы операционных систем, программ и аппаратных средств. Здесь не нужны красивые графические оболочки, а, наоборот, велика потребность в удобных средствах для работы с текстовой информацией. Операционная система Windows обеспечивает встроенную поддержку консолей, которые, по определению, являются интерфейсами ввода-вывода для приложений, работающих в текстовом режиме.
Консоль состоит из одного входного и нескольких экранных буферов. Входной буфер представляет собой очередь, каждая запись которой содержит информацию относительно отдельного входного события консоли. Экранный буфер — h двумерный массив, содержащий символы, выводимые в окно консоли, и данные [ об их цвете.
Очередь входного буфера содержит информацию о следующих событиях:

  • нажатии и отпускании клавиш;
  • манипуляциях мышью — движение, нажатие-отпускание кнопок;
  • изменение размера активного экранного буфера, состояние прокрутки.

Для поддержки работы консольных приложений API Win32 содержит более сорока функций, предназначенных для интеграции в среду Windows программ, работающих в текстовом режиме. Данные функции предоставляют два уровня доступа к консоли — высокий и низкий. Консольные функции ввода высокого уровня позволяют приложению извлечь данные, полученные при вводе с клавиатуры и сохраненные во входном буфере консоли. Консольные функции вывода высокого уровня позволяют приложению записать данные в устройства стандартного вывода или ошибки с тем, чтобы отобразить этот текст в экранном буфере консоли. Функции высокого уровня также поддерживают переназначение стандартных дескрипторов ввода-вывода и управление режимами работы консоли. Консольные функции низкого уровня позволяют приложениям получить детальную информацию о вводе с клавиатуры, событиях нажатия-отпускания кнопок мыши и о манипуляциях пользователя с окном консоли, а также осуществить больший контроль над выводом данных на экран.
Таким образом, API Win32 предоставляет два разных подхода для обеспечения ввода-вывода с консолью, выбор нужного зависит от гибкости и полноты контроля, которыми хочет обладать приложение для обеспечения своей работы с консолью. Функции высокого уровня обеспечивают простоту процесса ввода-вывода путем использования стандартных дескрипторов ввода-вывода, но при этом невозможен доступ к входному и экранным буферам консоли. Функции низкого уровня требуют учета большего количества деталей и объема кода, но это компенсируется большей гибкостью.
Высокоуровневый и низкоуровневый консольный ввод-вывод не являются взаимоисключающими, и приложение может использовать любую комбинацию этих функций.
С каждой консолью связаны две кодовые таблицы — по одной для ввода и вывода. Консоль использует входную кодовую таблицу для трансляции ввода с клавиатуры в соответствующее символьное значение. Аналогичным образом используется кодовая таблица вывода — для трансляции символьных значений, формируемых различными функциями вывода, в символ, отображаемый в окне консоли. Для работы с кодовыми таблицами приложение может использовать пары — функции SetConsoleCP и GetConsoleCP для входных кодовых таблиц и функции SetConsoleOutputCP и GetConsoieOutputCP для выходных кодовых таблиц. Идентификаторы кодовых таблиц, доступные на данном компьютере, сохраняются в системном реестре следующим ключом: <
HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Nls\\CodePage

Минимальная программа консольного приложения

Минимальная программа консольного приложения на ассемблере выглядит так:

.model flat.STDCALL ;модель памяти flat.
i ncludeWindowConA.i nc
:Обьявление внешними используемых в данной программе функций Win32 (ASCII):
extrn AllocConsole:PROC
extrn SetConsoleTitleA:PROC
extrn ExitProcess:PROC
.data
TitleText db 'Минимальное консольное приложение Win32'.0
.code
start proc near :точка входа в программу:
:запрос консоли
call AllocConsole проверить успех запроса консоли
test eax.eax
jz exit :неудача :выведем заголовок окна консоли SetConsoleTitle:
push offset TitleText
call SetConsoleTitleA проверить успех вывода заголовка
test eax.eax
jz exit ;неудача
exit: :выход из приложения
:готовим вызов VOID ExitProcess(UINT uExitCode)
push 0
call ExitProcess start endp end start

Если убрать комментарии, то кода будет совсем немного. В нем представлены вызовы трех функций: AllocConsole, SetConsoleTitle, ExitProcess.
Первой функцией консольного приложения должна быть функция запроса консоли AllocConsole.

B00L AllocConsole(VOID);

Для вызова функции Al I ocConsol e не требуется никаких параметров. В случае успеха функция Al I ocConsol e возвращает ненулевое значение, при неудаче — нуль. Выделенная консоль представляет собой типичное для Windows окно. Процесс в конкретный момент времени может использовать одну консоль. Если ему нужно запустить еще одну консоль, то прежняя должна быть закрыта или освобождена с помощью функции FreeConsole.

B00L FreeConsole(VOID);

В случае успеха функция FreeConsol e возвращает ненулевое значение, при неудаче — нуль.
При завершении процесса выделенная процессу консоль освобождается автоматически. В нашем случае использован именно этот вариант закрытия консоли — функцией ExitProcess.

VOID ExitProcesstUINT uExitCode):

Функции ExitProcess передается код завершения процесса и всех завершаемых цепочек в этом процессе. Проанализировать этот код можно с помощью функ-
ций GetExitCodeProcess и GetExitCodeThread. В общем случае в различных ветвях кода может быть несколько точек выхода с вызовом ExitProcess. Задавая различные значения кода завершения, можно таким образом идентифицировать причину завершения процесса.
Окно консоли может иметь заголовок, для отображения которого предназначена функция SetConsoleTitle.

B00L SetConsolеTitle(LPCTSTR lpConsoleTitle) ;

Функция SetConsoleTitle имеет один параметр — указатель на строку с заголовком консоли, заканчивающуюся нулем.
Организация высокоуровневого консольного ввода-вывода
Для высокоуровневого ввода-вывода приложение может использовать файловые функции ReadFile и WriteFile, а также функции консольного ввода-вывода Read-Console и WriteConsole. Эти функции обеспечивают косвенный доступ к входному и экранным буферам пульта. Физически эти функции фильтруют записи входного буфера консоли так, чтобы возвратить ввод как поток символов, игнорируя все другие записи с расширенной информацией о мыши, клавиатуре и изменении размеров окна консоли. Отфильтрованный поток символов отображается в окне консоли начиная с текущей позиции курсора. Существуют два важных отличия в использовании пар функций ReadFile\WriteFile и ReadConsoleNWrite-Console.

  • Поддержка символов Unicode и ANSI. Консольные функции (ReadConso-le\WriteConsole) поддерживают эти наборы, а файловые (ReadFile\Write-File) — нет.
  • Функции файлового ввода-вывода могут использоваться как для обращения к файлам, так и к именованным каналам, устройствам, присоединенным к последовательному интерфейсу. Консольные функции ввода-вывода можно использовать только с тремя дескрипторами стандартного ввода-вывода (см. ниже описание функций ReadConsole\WriteConsole).

Из вышесказанного следует, что функции высокоуровневого ввода-вывода обеспечивают простой способ обмена (чтения-записи) потоков символов с консолью.

Операция чтения высокого уровня реализуется функцией ReadConsole, которая получает входные символы из буфера ввода консоли и сохраняет их в указанном буфере.

B00L ReadConsoleCHANDLE hConsolelnput. LPVOID ipBuffer. DWORD nNumberOfCharsToRead. LPDWORD lpNumberOfCharsRead. LPVOID lpReserved);

Параметры этой функции означают следующее:

  • hConsolelnput — дескриптор входного потока консоли;
  • IpBuffer — указатель на строку, в которую будет записана вводимая строка символов;
  • nNumberOfCharsToRead — размер буфера, указанного lpBuffer;
  • ipNumberOfCharsRead — количество действительно введенных символов;
  • lpReserved — этот параметр не используется, поэтому должен задаваться
    как NULL.

Операция записи высокого уровня реализуется функцией WriteConsole, которая извлекает символы из указанного буфера и записывает их в экранный буфер, начиная с текущей позиции курсора и продвигая ее по мере записи символов. B00L WriteConsoleCHANDLE hConsoleOutput. CONST VOID *lpBuffer.
DWORD nNumberOfCharsToWrite. LPDWORD ipNumberOfCharsWritten. LPVOID lpReserved);
Параметры этой функции означают следующее:

  • hConsoleOutput — дескриптор выходного потока консоли;
  • lpBuffer — указатель на выводимую строку;
  • nNumberOfCharsToWrite — размер буфера, указанного IpBuffer;
  • IpNumberOfCharsWritten — количество действительно выведенных символов;
  • lpReserved — этот параметр не используется, поэтому должен задаваться
    как NULL.

Для своей работы эти и некоторые другие консольные функции требуют получения стандартных дескрипторов ввода-вывода. Значения этих дескрипторов присваиваются параметрам hConsolelnput и hConsoleOutput. По умолчанию стандартный дескриптор ввода связан с клавиатурой, стандартный дескриптор вывода—с экраном. Получить стандартный дескриптор ввода-вывода можно с помощью функции GetStdHandle.

HANDLE GetStdHand 1 e(DWORD nStdHandle):

На вход функции GetStdHandle должно быть подано одно из следующих значений:

  • STD_INPUT_HANDLE = -10 — дескриптор стандартного входного потока;
  • STD_OUTPUT_HANDLE = -11 — дескриптор стандартного выходного потока;
  • STD_ERROR_HANDLE - -12 — дескриптор стандартного потока ошибок.

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

  • эхо-контроль вводимых символов на экране из активного экранного буфера;
  • ввод строки, окончание операции чтения которой происходит при нажатии клавиши Enter;
  • автоматическая обработка некоторых символов, вводимых с клавиатуры:
    перевода каретки, нажатия клавиш Ctrl+C и т. д.;
  • автоматическая обработка некоторых символов, выводимых на экран: перевода строки и каретки, возврата на один символ и т. д.

Функция SetConsol eCursorPosition предназначена для указания позиции, с которой начинается выполнение операций чтения-записи в окно консоли. B00L SetConsoleCursorPosition(HANDLE hConsoleOutput. COORD dwCursorPosition); Параметрами этой функции являются стандартный дескриптор вывода hCon-[' soleOutput, полученный функцией GetStdHandle, и указатель на структуру COORD с координатами новой позиции курсора:

COORD struc x dw 0 у dw 0 ends

По умолчанию цветовое оформление окна консоли достаточно унылое — черный фон, белый текст. Внести разнообразие во внешний вид окна консоли поможет функция SetConsoleTextAttribute, с помощью которой можно изменить установки цвета по умолчанию для текста и фона.
B00L SetConsoleTextAttributetHANDLE hConsoleOutput. WORD wAttributes):
Первый параметр — без комментариев, второй определяет цвет текста и фона. Второй параметр формируется как логическое ИЛИ следующих значений:

  • FOREGROUND_BLUE=0001h - синий текст;
  • FOREGROUND_GREEN=0002h - зеленый текст;
  • FOREGROUND_RED=0004h — красный текст;
  • FOREGROUND_INTENSITY=0008h — текст повышенной яркости;
  • BACKGROUND_BLUE=0010h - голубой фон;
  • BACKGROUND_GREEN=0020h - зеленый фон;
  • BACKGROUND_RED=0040h - красный фон;
  • BACKGROUND_INTENSITY=0080h — фон повышенной яркости.

Для задания белого цвета складываются три компоненты, для задания черного — компоненты не задаются вовсе.

Пример программы ввода-вывода в консоль

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

:prg05_11.asm - программа ввода-вывода в консоль с изменением атрибутов выводимого текста
!
.data
.code
start proc near -.точка входа в программу:
..........
:получим стандартные дескрипторы ввода-вывода
push STD_OUTPUT_HANDLE
call GetStdHandle
movdOut.eax :dOut-fleCKpnnTop вывода консоли
push STD_INPUT_HANDLE
call GetStdHandle
mov din.eax idln-дескриптор ввода консоли :введем строку .¦установим курсор в позицию (2,6)
mov con.хх.2
mov con.yy,6
push con
push dOut
call SetConsoleCursorPosition cmp eax. 0
jz exit ;если неуспех
push 0
push offset NumWri количество действительно введенных символов
push 80 :размер буфера TitleText для ввода
push offset TitleText
push din
call ReadConsoleA
cmp eax, 0
jz exit :если неуспех
:выведем введенную строку в заголовок окна консоли:
push offset TitleText
call SetConsoleTitleA проверить успех вывода заголовка
test eax.eax
jz exit ;неудача
:выведем строку в окно консоли с различных позиций и с разными цветами установим курсор в позицию (2.5)
mov ecx.10 ;строку выведем 10 раз
mov bl.10000001b начальные атрибуты
ml: push ecx
inc con.xx
inc con.yy
push con
push dOut
call SetConsoleCursorPosition
cmp eax.O
jz exit :если неуспех ;определим атрибуты выводимых символов - будем получать их циклически сдвигом - регистр
BL
хог еах.еах
rol Ы.1
mov al.bl
push eax
push dOut
call SetConsoleTextAttribute
cmp eax.O
jz exit ;если неуспех :вывести строку
push 0
push offset NumWri действительное количество выведенных на экран
push NumWri ;длина строки для вывода на экран
push offset TitleText :адрес строки для вывода на экран
push dOut
call WriteConsoleA
cmp eax.O
jz exit :если неуспех pop ecx
loop ml
exit: :выход из приложения

Каждый консольный процесс имеет свой собственный список функций-обработчиков, которые вызываются системой, когда происходят определенные собы тия, например при активном окне консоли пользователь нажимает комбинации клавиш Ctrl+C, Ctrl+Break или Ctrl+Close. При запуске консольного приложения список функций-обработчиков содержит только заданную по умолчанию функцию-обработчик, которая вызывает функцию ExitProcess. Консольный процесс может добавлять или удалять дополнительные функции-обработчики, вызывая функцию SetConsoleCtrlHandler.
B00L SetConsoleCtrlHandler(PHANDLER_ROUTINE HandlerRoutine. B00L Add): Данная функция имеет два параметра:

  • HandlerRoutine — указатель на определенную приложением функцию HandlerRoutine, которая должна быть добавлена или удалена;
  • Add — логическое значение, которое означает: 1 — функция должна быть
    добавлена, 0 — функцию необходимо удалить.

Функция HandlerRoutine — это определенная приложением функция обратного вызова. Консольный процесс использует эту функцию, чтобы обработать нажатия клавиш управления. На самом деле HandlerRoutine — идентификатор-заполнитель для определенного приложением имени функции. B00L WINAPI HandIerRoutine(DWORD dwCtrlType):
Параметр DwCtrlType определяет тип сигнала управления, получаемого обработчиком. Этот параметр может принимать одно из следующих значений:

  • CTRL_C_EVENT=O — сигнал, имитирующий нажатие клавиш Ctrl+C, может быть получен из двух источников: с клавиатуры или как сигнал, сгенерированный функцией GenerateConsoleCtrl Event;
  • CTRL_BREAK_EVENT=1 — сигнал имитирующий нажатие клавиш Ctrl+Break, может быть получен из двух источников: с клавиатуры или как сигнал, сгенерированный функцией GenerateConsoleCtrl Event;
  • CTRL_CL0SE_EVENT=2 — сигнал, который система посылает всем процессам, подключенным к данному консольному приложению, когда пользователь его закрывает (либо выбирая пункт Close в системном меню окна консоли, либо щелкая на кнопке завершения задачи в диалоговом окне Менеджера задач);
  • CTRL_LOGOFF_EVENT=5 — сигнал, который посылается всем консольным процессам, когда пользователь завершает работу в системе (этот сигнал не указывает, какой именно пользователь завершает работу);
  • CTRL_SHUTD0WN_EVENT=6 — сигнал, который система посылает всем консольным процессам при подготовке к выключению машины. Функция HandlerRoutine должна возвратить логическое значение: 1 — если она обрабатывает конкретный сигнал управления; 0 — для обработки полученного события будет использоваться другая функция-обработчик HandlerRoutine из списка функций-обработчиков для этого процесса (то есть включенная в этот список раньше данной функции).

Как уже было упомянуто, каждый консольный процесс может определить несколько функций HandlerRoutine, которые связываются в цепочку. Первоначально этот список содержит только заданную по умолчанию функцию обработчика, которая вызывает функцию ExitProcess и, как результат, приводит к завершению текущего консольного приложения. Консольный процесс добавляет или удаляет
Работа с консолью в среде Windows 233
дополнительные функции обработчика, вызывая функцию SetConsoleCtrl Handler, которая не затрагивает список функций-обработчиков для других процессов. Когда консольный процесс принимает любой из сигналов управления (см. выше), то вызывается последняя зарегистрированная функция-обработчик, если она не возвращает 1, то управление передается следующему (предыдущему) зарегистрированному обработчику и т. д., до тех пор пока один из обработчиков не возвратит 1. Если ни один из обработчиков этого не сделал, то вызывается обработчик, заданный по умолчанию.
Установка обработчиков для сигналов CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT и
CTRL_SHUTDOWN_EVENT дает процессу возможность выполнить специфичные для него
действия по корректному завершению приложения. Пользовательская функция
HandlerRoutine может быть вызвана для того, чтобы выполнить следующие действия:

  • вызвать функцию ExitProcess для завершения процесса;
  • возвратить 0 (ложь) — это означает, что завершение приложения должен
    выполнить обработчик, заданный по умолчанию;

В возвратить 1 — в этом случае никакие другие функции-обработчики не вызываются, а система отображает всплывающее диалоговое окно с запросом о необходимости завершения процесса; система также отображает диалоговое окно, если процесс не отвечает определенное время (5 секунд для CTRLCLOSEEVENT и 20 секунд для CTRLLOGOFFEVENT и CTRLSHUTDOWNEVENT); процесс может использовать функцию SetProcessShutdownParameters, чтобы запретить системе отображать последнее диалоговое окно, в этом случае система просто заканчивает процесс, когда HandlerRoutine возвращает истину или когда истекает определенный период времени. Ниже приведен пример пользовательского обработчика события — ввода комбинации Ctrl+C или Ctrl+Break. За основу взята предыдущая программа.

:prg05_12.asm - программа, демонстрирующая использование пользовательского обработчика события.
.data
Text_CTRL_C db "Нажаты CTRL+C"
Len_Text_CTRL=$-Text_CTRL_C
TextJREAK db "Нажаты CTRL+BREAK"
Len_BREAK=$-Text_BREAK
.code
CtrlHandler proc
arg @@dwCtrlType:DWORD
uses ebx.edi. esi ;эти регистры обязательно должны сохраняться
:анализируем тип сигнала управления
cmp @@dwCtrlType.CTRL_C_EVENT
je h_CTRL_C_EVENT
cmp (a@dwCtrlType.CTRL_BREAK_EVENT
je h_CTRL_BREAK_EVENT
jmp h_default
h_CTRL_C_EVENT: :при нажатии CTRL+C выводим сообщение: установим курсор

call SetConsoleCursorPosition :вывести строку Text_CTRL_C call WriteConsoleA
; возвращаем признак обработки
mov eax.l
jmp exit_CtrlHandler h_CTRL_BREAK_EVENT:
;при нажатии CTRL+BREAK выводим сообщение:
установим курсор
call SetConsoleCursorPosition : вывести строку
call WriteConsoleA
;возвращаем признак обработки
mov eax.l
jmp exit_CtrlHandler
h_default: mov eax.Offffffffh;возвращаем остальное не обрабатываем exit_CtrlHandler: ret CtrlHandler endp start proc near ;точка входа в программу:
:работаем .........
:получим стандартные дескрипторы ввода-вывода
установим функцию-обработчик сигналов управления
push TRUE
push offset cs: CtrlHandler
call SetConsoleCtrlHandler
onp eax. 0
jz exit :если неуспех ;введем строку в буфер TitleText установим курсор в позицию (2.6)
call SetConsoleCursorPosition call ReadConsoleA
:выведем введенную строку в заголовок окна консоли: push offset TitleText call SetConsoleTitleA
:выведем строку в окно консоли с различных позиций и с разными цветами
mov ecx.10 :строку выведем 10 раз
mov bl.10000001b начальные атрибуты ml: push ecx установим курсор в позицию
call SetConsoleCursorPosition
определим атрибуты выводимых символов - будем получать их циклически сдвигом регистра BL хог еах.еах
rol Ы .1
mov al ,Ы
push eax
push d0ut
call SetConsoleTextAttribute . :вывести строку TitleText
call WriteConsoleA cmp eax.0
jz exit ;если неуспех pop ecx
loop ml

Относительно этой программы можно сделать два замечания. Первое касается функции Handl erRoutine, которая в нашей программе называется Ctrl Handler. Как упоминалось, эта функция является функцией обратного вызова. Ее вызов производится при возникновении определенных событий неявно — из системы Windows. По структуре и алгоритму работы она аналогична оконной функции, которую мы рассматривали в уроке 18 «Создание Windows-приложений на ассемблере» учебника. Поэтому за всеми подробностями отсылаем читателя к этому материалу. Второе замечание касается порядка отладки приложений, содержащих определяемые пользователем функции (процедуры) обратного вызова. Первое, что нужно сделать в процессе пошагового выполнения программы в отладчике, — выяснить адрес процедуры обратного вызова. В программе выше это можно сделать, выяснив, какое значение будет помещено в стек при выполнении команд:

..........
[установим функцию-обработчик сигналов управления
push TRUE
push offset cs: Ctrl Handler
call SetConsoleCtrlHandler
cmp eax. 0
jz exit [если неуспех
.........

После этого, сделав активным окно отладчика CPU (выбрав в меню команду
View CPU), необходимо установить указатель мыши в окно с командами процес-; сора и щелкнуть правой кнопкой мыши. В появившемся контекстном меню вы-
бер*етс пункт Goto... В результате этих действий отладчик отобразит диалоговое ¦ окно, в которое необходимо внести адрес программы-обработчика Ctrl Handler. ; В результате этого в верхней части окна команд отобразится первая команда [' процедуры Ctrl Handler. Установите на нее курсор и нажмите клавишу F4. Все, S программа начнет выполняться по своему алгоритму. При нажатии пользователем
управляющих комбинаций клавиш, допустимых функцией Handl erRoutine, управ-I ление будет передано этой функции, и вы сможете произвести ее отладку.
Организация низкоуровнего консольного ввода-вывода
I Низкий уровень консольного ввода-вывода по сравнению с высоким уровнем И бладает более широкими и гибкими возможностями. Низкоуровневые функции консольного ввода-вывода обеспечивают прямой доступ к входному и экранным буферам консоли, предоставляя приложению доступ к событиям мыши и клавиатуры, а также к информации об изменении размеров окна консоли. Функции низкоуровневого ввода-вывода позволяют приложению иметь доступ по чтению-записи к указанному числу последовательных символьных ячеек в экранном буфере или к прямоугольному блоку символьных ячеек в указанной позиции экранного буфера.
Обсудим возможности низкоуровневого ввода-вывода на примере работы с входным буфером (входной очередью) и буферами экрана. Отметим, для работы с ними существуют разные группы команд. Так, для работы с входным буфером используются функции низкоуровневого ввода-вывода — Wn'teConsolelnputXRead-Consolelnput. Группа функций для работы с буферами экрана будет конспективно рассмотрена в конце этого раздела.

Поддержка работы с мышью в консоли

Большое достоинство консольных приложений — встроенная средствами Windows поддержка мыши. Она реализуется с помощью функции ReadConsolelnput. Важно отметить, что эта функция используется для получения информация о событиях не только мыши, но и о событиях клавиатуры, изменении размера окна и т. д.

B00L ReadConsoleInput(HANDLE hConsolelnput. PINPUT_RECORD lpBuffer, DWORD nLength. LPDWORD lpNumberOfEventsRead);

Параметры этой функции:

  • Consolelnput — стандартный дескриптор ввода, полученный функцией GetStdHandle;
  • lpBuffer — указатель на буфер, в который записывается информация о событии мыши, — эта область памяти имеет структуру, называемую INPUT_ RECORD, ее формат рассмотрен чуть ниже (необходимо заметить, что возможно групповое чтение информации из входного буфера, поэтому указатель ipBuffer может указывать на массив структур; информация о том, сколько событий будет читаться в этот массив структур, определяется параметром nLength);
  • nLength — размер во входных записях буфера, на который указывает указатель lpBuffer;
  • lpNumberOfEventsRead — определяет переменную, в которую записывается
    действительное число прочитанных записей входного буфера.

Запись входного буфера консоли имеет структуру, называемую INPUTRECORD. Ее описание на языке C++ выглядит так:

typedef struct _INPUT_RECORD { WORD EventType; union {
KEYJVENT_RECORD KeyEvent;
MOUSE_EVENT_RECORD MouseEvent;
WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
MENU_EVENT_RECORD MenuEvent:
FOCUSJVENT_RECORD FocusEvent;
} Event: } INPUT_RECORD;

В этой структуре первое поле EventType размером в слово содержит тип события, а второе поле Event является объединением различных структур. Поля какой из структур будут заполнены, определяется типом события, то есть первым полем, которое может принимать значения:

  • KEY_EVENT=0001h - поле Event содержит структуру KEYEVENTRECORD с информацией относительно события клавиатуры;
  • MOUSE_EVENT=0002h — ноле Event содержит структуру MOUSEEVENTRECORD с информацией относительно движения мыши или нажатия кнопки;
  • WINDOW_BUFFER_SIZE_EVENT-O004h - поле Event содержит структуру WINDOW_ BUFFER_SIZE_RECORD с информацией относительно нового размера экранного буфера;
  • MENU_EVENT=OOO8h — поле Event содержит структуру MENUEVENTRECORD (это событие используется внутри Windows и должно игнорироваться);
  • FOCUS_EVENT=0010h - поле Event содержит структуру FOCUSEVENTRECORD (это
    событие используется внутри Windows и должно игнорироваться).

Для обработки события мыши структура MOUSEEVENTRECORD выглядит так:

typedef struct _MOUSE_EVENT_RECORD {
COORD dwMousePosition;
DWORD dwButtonState;
DWORD dwControlKeyState:
DWORD dwEventFlags;
} MOUSE_EVENT_RECORD;

Исходя из вышесказанного структура INPUTRECORD для обработки событий мыши в программе на ассемблере должна выглядеть так:
INPUT_RECORD struc EventType dw 0 dwMousePosition struc x dw 0 у dw 0 ends
dwButtonState dw 0 dwControlKeyState dw 0 DwEventFlags dw 0 ends
Поле EventType для события мыши содержит значение MOUSE_EVENT=0002h, а поля структуры MOUSEEVENTRECORD соответственно означают следующее:
ш dwMousePosition — координаты мыши в окне консоли (в символьных координатах);
м dwButtonState — состояние кнопок мыши в момент возникновения события, при нажатии кнопок устанавливаются следующие биты (при одновременном нажатии устанавливается несколько соответствующих битов):

  • если установлен бит 0 ноля dwButtonState, то в момент наступления события была нажата левая кнопка мыши;
  • если установлен бит 1 поля dwButtonState, то в момент наступления события была нажата правая кнопка мыши;
  • если установлен бит 2 поля dwButtonState, то в момент наступления события была нажата средняя кнопка мыши, если она есть;
  • dwControlKeyState — поле описывает состояние управляющих клавиш клавиатуры в момент наступления события мыши (если одновременно нажато несколько клавиш, то значение в этом поле является результатом операции логического сложения ИЛИ перечисленных ниже значений):
  • • RIGHT_ALT_PRESSED=0001h - нажата правая клавиша Alt;
    • LEFT_ALT_PRESSED=0002h - нажата левая клавиша Alt;
    • RIGHT_CTRL_PRESSED=0004h — нажата правая клавиша Ctrl; LEFT_CTRL_PRESSED=OOO8h — нажата левая клавиша Ctrl;
    • SHIFT_PRESSED=OOlOh - нажата любая клавиша SHIFT;
    • NUMLOCK_ON=0020h - индикатор NumLock включен;
    • SCROLLLOCK_ON=0040h — индикатор ScrollLock включен;
    • CAPSLOCK_ON=0080h — индикатор CapsLock включен;
    ENHANCED_KEY=0100h — нажата клавиша расширенной клавиатуры (101 и 102 клавиши): Ins, Del, Home, End, Page Up, Page Down, «-, t, -», I, / или Enter;
    Ш dwEventFl ags — поле содержит одно из двух значений: » MOUSE_MOVED=0001h — перемещение мыши;
    • DOUBLE_CLICK=0002h — выполнен двойной щелчок мыши.

В ПРИМЕРе приведена демонстрационная программа обработки событий мыши (prg05_13. asm), которые отслеживаются следующим образом: нажатие левой кнопки приводит к выводу сообщения в позиции нажатия, нажатие правой кнопки приводит к завершению работы программы.
В заключение обращу внимание читателя на то, что API Win32 имеет функцию Mouse_Event, которая позволяет генерировать события, соответствующие реальным движениям мыши и щелчкам ее кнопок. Тем самым API Win32 предоставляет механизм для создания обучающих и демо-версий программ. Формат этой функции:
VOID mouse_event(DWORD dwFlags. DWORD dx. DWORD dy, DWORD dwData. DWORD dwExtralnfo)

Расширенная поддержка клавиатуры в консоли

Функции работы с текстом высокого уровня не дают других возможностей работы с клавиатурой, кроме как примитивного ввода текста. При разработке программ текстового режима часто требуется информация о состоянии управляющих клавиш, о факте удержания клавиши, что может свидетельствовать о желании пользователя повторить ввод некоторого символа или просто о желании получить тривиальный скан-код клавиши. Эти и другие события клавиатуры доступны программе посредством описанной выше функции ReadConsolelnput.
События клавиатуры генерируются при нажатии любой клавиши. Процесс их обработки аналогичен обработке событий мыши. В первую очередь заполняется
о нажатии некоторых управляющих клавиш. Для всех остальных клавиш просто фиксируется факт нажатия. При этом необходимо помнить, что однократному нажатию клавиши реально соответствуют два события — нажатие и отпускание клавиши. В связи с этим программа выводит два сообщения. На практике этого можно избежать, анализируя поле bKeyDown: bKeyDown=l, когда клавиша нажата; bKeyDown=0, когда клавиша отпущена. Выход из программы — при выполнении любых действий с мышью.

Окно консоли и экранный буфер

И в заключение обсуждения особенностей работы с консольными приложениями поясним, что представляет собой экранный буфер консоли и какие средства представляет API Win32 для работы с ним.
Для того чтобы читатель мог легко понять соотношение понятий «окно консоли» и «экранный буфер консоли», представьте себе офисный календарь, на котором текущее число отмечается квадратной рамкой, закрепленной на прозрачной целлофановой ленте и перемещающейся вдоль нее. Теперь представим, что содержимое листа календаря вне этой рамки невидимо, то есть доступно только через окошко, которое образует рамка. Для того чтобы увидеть содержимое всего листа календаря, необходимо двигать рамку. В контексте этой ассоциации — лист календаря — это экранный буфер, а площадь внутри рамки — окно консоли, то есть видимая часть экранного буфера.
Возможна поддержка нескольких экранных буферов, связанных с данной консолью, но только один из них может подвергаться отображению в окне консоли — его называют активным экранным буфером. Другие экранные буферы, если они были созданы, являются неактивными. Для создания экранного буфера используется функция CreateConsoleScreenBuffer. К неактивным экранным буферам можно обращаться для чтения и записи, но отображаться в окне консоли будет только активный экранный буфер (или его часть). Для того чтобы сделать экранный буфер активным, используется функция SetConsoleActiveScreenBuffer. Функция CreateConsoleScreenBuffer имеет показанный ниже формат.
HANDLE CreateConsoleScreenBuffer(DWORD dwDesiredAccess,
DWORD dwShareMode, CONST LPSECURITY_ATTRIBUTES ipSecurityAttributes.
DWORD dwFlags. LPVOID lpScreenBufferData):
Параметры функции:

  • dwDesiredAccess — определяет желаемый тип доступа к экранному буферу консоли, этот параметр может быть либо одним из следующих значений либо их комбинацией:
  • GENERIC_READ=80000000h — запрашивается доступ по чтению к экранному буферу консоли для того, чтобы разрешить процессу прочитать данные из буфера;
  • GENERIC_WRITE=40000000h — запрашивается доступ для записи к экранному буферу консоли для того, чтобы разрешить процессу записать данные в буфер;
  • dwShareMode — определяет возможность разделения этого экранного буфера консоли; нулевое значение этого параметра указывает, что буфер не может
    быть разделен, ненулевое состояние этого буфера может быть одним из следующих значений или их комбинацией:
  • FILESHAREREAD — другие операции открытия могут быть выполнены для экранного буфера консоли с доступом для чтения;
    FILESHAREWRITE — другие операции открытия могут быть выполнены для экранного буфера консоли с доступом для записи;
  • IpSecurityAttributes — указатель на структуру SECURITY_ATTRIBUTES, которая определяет, может ли возвращаемый функцией CreateConsoleScreenBuffer дескриптор наследоваться дочерними процессами — если lpSecuri-tyAttributes=NULL, то дескриптор не может быть унаследован;
  • dwFlags — определяет тип создаваемого экранного буфера консоли, в настоящее время поддерживается только один такой тип — CONSOLE_TEXTMODE_ BUFFER=1;
    lpScreenBufferData — зарезервирован и должен быть равен NULL.

Функция CreateConsoleScreenBuffer формирует дескриптор созданного экранного буфера, который затем используется функциями для доступа к этому буферу.
Для того чтобы сделать буфер активным, используют функцию SetConsole-Acti veScreenBuf f er.
B00L SetConsoleActiveScreenBuffertHANDLE hConsoleOutput):
Функция имеет единственный параметр — hConsoleOutput — дескриптор экранного буфера, созданного функцией CreateConsoleScreenBuffer. Как уже было отмечено, консоль может иметь много экранных буферов. Функция SetConsoleActiveScreenBuffer определяет, какой из них будет отображен. Приложение может производить запись в неактивный экранный буфер и затем использовать функцию SetConsoleActiveScreenBuffer для отображения содержимого буфера. Чтение и запись в неактивный (и активный тоже) экранный буфер производится функциями низкоуровневого ввода-вывода — WriteConsoleOutputNWriteConsoleOutput-Character и ReadConsoleOutput\ReadConsoleOutputCharacter, которым при вызове передается дескриптор нужного экранного буфера, полученного предварительно функцией CreateConsol eScreenBuf fer.
Каждый из созданных экранных буферов поддерживает собственный текущий прямоугольник окна, определяемый координатами верхней левой и нижней правой символьных ячеек, которые будут отображены в окне консоли. Для определения видимого в окне консоли прямоугольника экранного буфера используется функция GetConsol eScreenBufferlnfo.
B00L GetConsoleScreenBufferInfo(HANDLE hConsoleOutput.
PCONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferlnfo);
Параметрами этой функции являются:

  • hConsoleOutput — дескриптор экранного буфера, созданного функцией CreateConsoleScreenBuffer; дескриптор должен иметь тип доступа GENERICREAD;
  • lpConsoleScreenBufferlnfo — указатель на структуру CONSOLE_SCREEN_BUFFER_ INFO, в которую помещается информация об экранном буфере.

Структура CONSOLESCREENBUFFERINFO имеет следующий вид:

typedef struct _CONSOLE_SCREEN_BUFFER_INFO
COORD dwSize: :размер экранного буфера в колонках и строках COORD dwCursorPosition: //координаты столбца и строки курсора в экранном буфере
WORD wAttributes: //цвет фона и текста, с которыми записываются
//и отображаются символы в экранном буфере функциями //WriteFile\WriteConsole и ReadFile\ReadConsole
SMALL_RECT srWindow; // определяет структуру
SMALL_RECT. которая содержит координаты // левого верхнего и нижнего правого углов экранного буфера, //видимого в окне консоли на экране дисплея COORD dwMaximumWindowSize; //определяет максимальный размер окна консоли
//с учетом текущего размера экранного буфера и шрифт } CONSOLE_SCREEN_BUFFER_INFO :

Для приложения интерес, в частности, может представлять параметр srWindow с координатами видимой части экранного буфера. Далее, руководствуясь действиями пользователя (выполняющего прокрутку окна или изменение его размера) по отношению к окну консоли, приложение может изменять значения в структуре SMALLRECT и передавать ее на вход функции SetConsoleWindowInfo, которая устанавливает текущий размер и позицию окна консоли относительно экранного буфера.
B00L SetConsoleWindowInf0ChANDLE hConsoleOutput. B00L bAbsolute.
CONST SMALL RECT *lpConsoleWindow);
Параметрами этой функции являются: .

  • hConsoleOutput — дескриптор экранного буфера, созданного функцией Create-Consol eScreenBuf fer; дескриптор должен иметь тип доступа GENERIC_WRITE;
  • bAbsol ute — определяет порядок использования координат в структуре, указанной параметром lpConsoleWindow; если bAbsol ute=l (истина), то координаты определяют новые левую верхнюю и нижнюю правую углы окна; если bAbsol ute=0 (ложь), то координаты — смещения относительно текущих координат углов окна;
  • lpConsoleWindow — указатель на структуру SMALL_RECT, в которую записывается информация о новых координатах экранного буфера.

Структура SMALL_RECT имеет следующий вид:

typedef struct _SMALL_RECT {
SHORT Left; //х-координата верхнего левого угла
SHORT Top: //у-координата верхнего левого угла
SHORT Right: //х-координата нижнего правого угла
SHORT Bottom: //у-координата нижнего правого угла } SMALL_RECT:

При работе с функцией SetConsoleWindowInfo следует иметь в виду, что она возвращает ошибку (нулевое значение), если координаты видимой части экранного буфера указывают за его действительные границы. Максимально допустимый раз мер окна для данной консоли можно получить с помощью функции GetConsole-ScreenBufferlnfo. Таким образом, обе эти функции можно использовать для листания экранного буфера.
Для закрытия экранного буфера используется функция CloseHandle, которой передается дескриптор закрываемого экранного буфера.
B00L CloseHandle(HANDLE hObject);
Работа с консолью в среде Windows 243
Для того чтобы завершить рассмотрение функций, предназначенных для поддержки консольного приложения, перечислим те из них, что остались «за кадром».

функция | Назначение
FiConsoleOutputAttribute | Устанавливает цвет текста и фона для указанного числа символьных ячеек, начинающихся по указанным координатам в экранном буфере
Fi П ConsoleOutputCharacter | Запись символа в экранный буфер указанное число раз по указанным координатам
Fl ushConsolelnputBuffer | Запись на диск входного буфера консоли. Все входные записи во входном буфере консоли до настоящего момента времени удаляются
GenerateConsoleCtrl Event | Посылка сигнала, определенного этой функцией, совместно использующим консоль процессам
GetConsoleCursorlnfo | Предоставление информации о размере и видимости курсора для указанного экранного буфера
GetConsoleMode | Предоставление информации о текущем входном режиме входного буфера консоли или текущем режиме вывода экранного буфера консоли
GetConsoleTitie | Извлечение строки из области заголовка для текущего окна консоли
GetLargestConsoleWi ndowSIze | Возвращает размер самого большого возможного окна консоли, основанного на текущем шрифте и размере изображения
GetNumberOfConsolelnputEvents | Возвращает число непрочитанных записей ввода во входном буфере пульта
GetNumberOfConsoleMouseButtons | Возвращает число кнопок на мыши, используемых текущей консолью
PeekConsolelnput | Чтение данных из входного буфера консоли без их удаления
Scrol1ConsoleScreenBuffer | Перемещение блока данных в экранном буфере. Действие перемещения может быть ограничено путем определения отсекающего прямоугольника. Содержание экранного буфера вне отсекающего прямоугольника будет неизменным
SetConsoleCursorlnfo | Установка размера и видимости курсора для указанного экранного буфера консоли
SetConsoleMode | Установка режима входного буфера консоли или режима вывода экранного буфера консоли
SetConsoleScreenBufferS1ze | Изменение размера указанного экранного буфера консоли
SetStdHandle | Установка некоторого дескриптора как дескриптора стандартного ввода, стандартного вывода или устройства ошибки. Может использоваться при перенаправлении ввода-вывода


Преобразование чисел

Продавай мобильный контент со своего сайта

Преобразование чисел

  • Ввод чисел с консоли
    • Ввод целых десятичных чисел из диапазона 0..99
    • Ввод целых десятичных чисел из диапазона 0..4 294 967 295
    • Ввод целых десятичных чисел из диапазона 0..999 999 999 999 999 999
    • Ввод целых десятичных чисел из диапазона 0..оо
    • Ввод вещественных чисел
  • Вывод чисел на консоль
    • Вывод шестнадцатеричных чисел
    • Вывод целых десятичных чисел из диапазона 0..99
    • Вывод целых десятичных чисел из диапазона 0..оо
    • Вывод целых десятичных чисел из диапазона 0..999 999 999 999 999 999
    • Вывод вещественных чисел

Внутри каждой большой задачи сидит маленькая,
пытающаяся пробиться наружу.
Закон больших чисел Хоара (Прикладная Мерфология)

В предыдущей главе мы рассмотрели решение проблемы обмена данными с консолью. Данные, вводимые с консоли и выводимые на нее, кодируются операционной системой в соответствии с текущей таблицей кодировки. Отдельный предмет обсуждения при этом — процесс ввода-вывода числовой информации. В каждом языке программирования он реализован по-своему. Одна из целей, к которой стремятся разработчики компиляторов, — по возможности сделать этот процесс прозрачным для программиста. Язык ассемблера в отличие от языков высокого уровня не обладает средствами такого прозрачного ввода-вывода числовой информации. Но в этом и состоит его достоинство, так как при определенном опыте и квалификации программиста появляется хорошая возможность повышения эффективности конечного кода.
Для каждой конкретной задачи преобразование чисел между различными представлениями, допустимыми компьютером, может быть выполнено несколькими способами. Для эффективного решения задач обработки числовой информации программист должен знать эти способы и уметь выбрать наиболее подходящий из них для решения конкретной проблемы. Сразу следует отметить, что эта тема далеко не нова. Многие источники, посвященные ассемблеру, с той или иной степенью подробности рассматривают проблему преобразования чисел. И хотя здесь трудно придумать что-то новое, эта тема заслуживает того, чтобы ей было уделено место в книге, посвященной вопросам прикладного программирования па ассемблере. В данной главе сделана попытка систематизированно рассмотреть различные существующие подходы к решению проблемы ввода-вывода числовой информации и ее преобразованию во внутреннее представление в компьютере.
Начнем с того, что вспомним урок 2 «Архитектура персонального компьютера» учебника, где приведена классификация типов данных, допустимых микропроцессором Pentium III. Для нашего изложения важно то, что они делятся на две большие группы — данные целочисленного и вещественного типов. Причем целочисленные данные можно разделить на две подгруппы: двоичные и двоично-десятичные (BCD-числа). Исходя из этого постараемся сформулировать направления преобразований числовой и символьной информации, востребованные на практике.
При обмене с консолью:

  • десятичные целые числа в символьном виде <-» внутреннее двоичное представление;
  • шестнадцатеричные целые числа в символьном виде <-» внутреннее двоичное представление;
  • двоичные целые числа в символьном виде <-> внутреннее двоичное представление;
  • десятичная дробь в символьном виде <-> внутреннее представление в виде
    вещественного числа;
  • дробное шестнадцатеричное число в символьном виде «-> внутреннее представление в виде вещественного числа;
  • дробное двоичное число в символьном виде <-> внутреннее представление в
    виде вещественного числа.
    Взаимное преобразование между внутренними представлениями:
  • двоичное число <-» двоично-десятичное число;
  • двоично-десятичное число <-» вещественное число;
  • двоичное целое число <-> вещественное число.

Рассмотрим основные способы выполнения некоторых из этих видов преобразований. В своих рассуждениях будем предполагать, что числа положительные. Если вы внимательно изучили материал уроков 6 и 8 учебника, а также главы 1 этой книги, посвященной программированию арифметических операций над двоичными и двоично-десятичными числами, то вы легко сможете дополнить приведенные ниже программы возможностью учета знака при выполнении соответствующих преобразований.

Ввод чисел с консоли

В этом разделе разберем способы преобразования десятичных целых и вещественных чисел, вводимых с консоли в символьном виде, в соответствующее им внутреннее двоичное представление. Начнем с преобразования целых десятичных чисел. Заметим, что этот вид преобразования является наиболее востребованным на практике. Далее будет обсуждена проблема преобразования вещественных чисел.
Выбор способа преобразования десятичных целых чисел из символьного во внутреннее двоичное представление и обратно определяется диапазоном возможных исходных значений. Разберем два способа.

Ввод целых десятичных чисел из диапазона 0..99

Для значений из диапазона 0..99 взаимное преобразование между символьной десятичной и двоичной формами может производиться командами умножения и деления двоично-десятичных (BCD-чисел) — ММ и AAD.

:prg06_01.asm - программа ввода с консоли двоичного числа из диапазона 0..99
:в десятичном символьном представлении
;Вход: число в десятичной системе счисления, вводимое в символьном виде с клавиатуры.
;Выход: двоичное число в регистре А1.
buf_Oahstruc
len_bufdb 3 :длина buf_0ah
len_in db 0 действительная длина введенного слова (без учета Odh)
bufjn db 3 dup (20h) :буфер для ввода (с учетом Odh)
ends
.data
bufbuf_0ah<>
adr bufdd buf
.code
;.........
;вводим 2 символа с клавиатуры, контроль на допустимые значения не делаем
Ids dx.adr__buf
nrav ah.Oah
int 21h
xor ax.ax
cmp buf .lenjn.2 ;сколько чисел введено реально?
jneml
mov ah.buf.buf_in ml: mov al,buf.buf_in+l
andax.0f0fh преобразование в неупакованное десятичное представление
aad :в al двоичный эквивалент исходного двузначного десятичного значения

fbld string_pack :помещаем в стек сопроцессора 'fistp string_bin ;и извлекаем эквивалентное двоичное представление в поле string_bin '¦.........
Приведенная программа преобразует любое значение из диапазона 0..1018-!. Интересно отметить количественное значение максимальной двоичной величины, соответствующее верхней границе диапазона, — это +0de0b6b3a763ffffl6. Запомните его, оно пригодится нам при рассмотрении обратного преобразования Для вывода на консоль — из двоичного в десятичное представление. Извлечь значение нужной разрядности можно, если ввести директивой label соответствующие идентификаторы в исходный текст программы (что и сделано в нашем сегменте кода):

Ввод целых десятичных чисел из диапазона 0..4 294 967 295

Если исходное значение выходит за диапазон 0..99, то здесь следует иметь в виду возможность возникновения ситуации, при которой значение вводимого десятичного числа превышает диапазон, допустимый форматами типов целочисленных данных, поддерживаемых, в частности, арифметическими командами микропроцессора. Для Pentium III это 8, 16 и 32 бита. Допустимые диапазоны значений для этих форматов (числа без знака):

  • для операнда размером 8 бит — 0..255;
  • для операнда размером 16 бит — 0..65 535;
  • для операнда размером 32 бита — 0..4 294 967 295.

Как видите, максимальное число не такое уж и большое. Поэтому мы столько внимания уделили работе с числами большой размерности в главе 1, посвященной программированию арифметических операций. В ней данные большой размерности просто описывались в сегменте данных без какого-либо намека на возможность их ввода с консоли или отображения на ней. В этом разделе мы постараемся ликвидировать этот недостаток, что несомненно поднимет привлекательность для читателя того и другого материала. Но вначале мы рассмотрим способы преобразования значений, которые укладываются в указанные выше диапазоны. Для этого можно предложить два способа преобразования в символьном представлении десятичных чисел, вводимых с консоли: с использованием деся-
точного полинома и с использованием возможностей сопроцессора по обработке
данных.
В основе способа с использованием десятичного полинома лежит возможность представления десятичного числа суммой произведений на степени числа 10 составляющих его десятичных цифр, которые соответствуют позициям этих цифр в исходном числе:

А10 = an.,x10nl + an.2x10"-2 + ... + а,х10 + а0х10°.

Вычисление данного полинома лучше производить по схеме Горнера:

А10 - (...(0+an.,)x10+an.2)x10+ ... + а,)х10+а0.

Например, число 3405 по этим формулам может быть представлено так:

3405=Зх103+4х102+Ох101+5х100=(((0+3)х10+4)х10+0)х10+5.

Ниже приведена программа преобразования целого десятичного числа в символьном виде из диапазона 0..4 294 967 295 в эквивалентное двоичное представление. Для ввода числа с клавиатуры используем функцию 3fh MS D0S. Она удобна тем, что возвращает количество действительно введенных символов в регистре AL.

:prg06_02.asm - программа преобразования целого десятичного числа в символьном виде :из диапазона 0..4294967295 в эквивалентное двоичное представление.
:Вход: ввод с клавиатуры числа в десятичной системе счисления (не более 10 цифр).
:Выход: двоичное число-результат преобразования в регистре ЕАХ.
add eax.edx
mul ten
jc exit_e
inc si
loop ml m2: mov dl .[si]
anddl.Ofh преобразуем ASCI I->BCD
add еах^х;результат преобразования в регистре ЕАХ
jncexit результат вышел за границы операнда exit_e: .выводим строку string_e на экран

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

Ввод целых десятичных чисел из диапазона 0..999 999 999 999 999 999

Второй способ преобразования десятичных чисел хотя и выглядит несколько экзотически, вполне работоспособен. Данный способ предполагает использование особенностей некоторых команд сопроцессора. В материале урока 19 «Архитектура и программирование сопроцессора» учебника мы перечисляли форматы данных, которые поддерживает сопроцессор. Перечислим их еще раз:

  • двоичные целые числа в трех форматах — 16, 32 и 64 бита;
  • упакованные целые десятичные (BCD) числа — максимальная длина -18 упакованных десятичных цифр (9 байт);
  • вещественные числа в трех форматах — коротком (32 бита), длинном (64 бита), расширенном (80 бит).

Для нас интерес представляют форматы целых двоичных и упакованных десятичных (BCD) чисел, а также команды обмена этими значениями с вершиной сопроцессора. Процесс преобразования десятичного целого числа, вводимого с клавиатуры, показан в программе ниже. Необходимо отметить, что этот способ преобразования позволяет расширить диапазон значений 0..999 999 999 999 999 999.

:prg06_03.asm - программа ввода целых десятичных чисел из диапазона
:0..999 999 999 999 999 999 и преобразования их в эквивалентное двоичное представление.
;Вход: ввод с клавиатуры числа в десятичной системе счисления
:в диапазоне значений 0..999 999 999 999 999 999.
:Выход: двоичное число-результат преобразования в области памяти stnng_bin.
.data
db 0 :барьер. если введенное количество цифр нечетно string db 20 dup (0) максимальное исходное число состоит из 18 цифр (20 - с учетом
Od0ah)
len_string=$-string adr_string dd string
string_pack dt 0 :сюда упаковывается исходное значение
len_string_pack=$-string_pack adr_string_pack dd string_pack результат - двоичное значение различной разрядности:

string_bin_byte label byte
string_bin_word label word
string_bin_word label word
string_bin_dword label dword

string_bindq 0 ;поле для результата - эквивалентного двоичного представления

--------вводим с экрана----------------............-----
movbx.O стандартный дескриптор - клавиатура
movcx. len_str"ing
lea dx.string :формируем указатель на строку string
movah.3fh :номер функции DOS
int 21h
jc exit ;переход в случае ошибки
;в регистре AL - количество действительно введенных десятичных цифр :преобразуем строку с десятичными числами в ее двоичный эквивалент
mov ex.ax
subcx,2 корректируем счетчик цикла (чтобы не учитывать OdOah, вводимые 3fh)
jeexz exit :число не было введено
Ids si ,adr_string
add si,cx
dec si указатель на последнюю введенную десятичную цифру
les di.adr_string_pack ml: std :флаг df=l - работаем со строкой string, начиная с ее конца
хог ах.ах
lodsb
and al. Of h
shl ax.8
lodsb
shl al .4
add al.ah :в AL две очередные упакованные цифры
eld :флаг df-1 - работаем со строкой string_pack. начиная с ее начала
stosb
sub ex.2
emp ex. 0
J9 ml ;конец преобразования в упакованное представление
fI nit инициализируем сопроцессор ;теперь преобразуем в эквивалентное двоичное представление:
fbld string_pack :помещаем в стек сопроцессора 'fistp string_bin ;и извлекаем эквивалентное двоичное представление в поле string_bin

Приведенная программа преобразует любое значение из диапазона 0..1018-!. Интересно отметить количественное значение максимальной двоичной величины, соответствующее верхней границе диапазона, — это +0de0b6b3a763ffffl6. Запомните его, оно пригодится нам при рассмотрении обратного преобразования Для вывода на консоль — из двоичного в десятичное представление. Извлечь значение нужной разрядности можно, если ввести директивой label соответствующие идентификаторы в исходный текст программы (что и сделано в нашем сегменте кода):

string_bin_byte label byte
string_bin_word label word
string_bin_word label word
string_bin_dword label dword

Ввод целых десятичных чисел из диапазона 0..до оо

Для преобразования десятичного числа произвольной разрядности из символьного представления в двоичное потрудиться придется несколько больше. Основа для этой работы была создана в материале, посвященном арифметическим операциям для чисел произвольной разрядности. Поэтому наши действия при разработке программы преобразования напомнит игру с конструктором, когда из готовых компонент будет создаваться новый продукт.
Исходными компонентами программы преобразования десятичного числа произвольной разрядности из символьного представления в двоичное будут являться макрокоманда умножения N-байтного числа на число размером М байт и программа сложения чисел размером N байт без учета знака. Алгоритм вычисления двоичного эквивалента будет таким же, как рассмотренный выше, — вычисление полинома по схеме Горнера. Ниже приведен вариант реализующей его программы. Расположение байтов результата — по схеме, естественной для микропроцессоров Intel, то есть младший байт располагается по младшему адресу.

:prg06_04.asm - программа ввода целых десятичных чисел из диапазона 0..». ;Вход: ввод с клавиатуры числа в десятичной системе счисления длиной до 20 цифр. ;Выход: двоичное число - результат преобразования в области памяти string_bin.
:см. описание макрокоманд add_unsign_N_l и mul_unsign_NM в главе 1 add_unsIgn_N_lmacro summand_l, summand_2, N
endm mul_unsign_NM macro u.i.v.j.w
endm .data
string db 22 dup (0) максимальное число состоит из 20 цифр (22 - с учетом OdOah) len_string-$-string tendd 10
string_bindb 10 dup (0) максимальная длина двоичного числа - 10 байт 1en_stri ng_bi n-$-stri ng_bi n
carry db 0 :перенос сложения последних байтов
adr_string_bindd string_bin string_bin_w db len_string_bin+l dup (0) результат умножения для макроса
;mul_unsign_NM = len_string_bin+l байт len_string_bin_w =$-string_bin_w adr_string_bin_w dd string_bin_w k db 0 :перенос 0 < k < 255
b dw lOOh ;размер машинного слова
.code
movbx.O стандартный дескриптор - клавиатура
mov cx.len_string
lea dx.string ;формируем указатель на строку string
movah,3fh :номер функции DOS
int 21h
jc exit :переход в случае ошибки :в регистре AL - количество действительно введенных десятичных цифр
mov ecx.eax
sub есх.2 корректируем счетчик цикла (чтобы не учитывать OdOah. вводимые 3fh)
jcxz $+4 :число не было введено
jmp $+5
jmp exit cont_l:dec ecx ;не умножать на 10 последнюю цифру числа
jcxz $+4 юднозначное число
tjmp S+5 jmp m2 lea si.string ;формируем указатель на строку string хог еах.еах :еах:=0 ml: хог edx.edx
mov dl.[si]
and dl.Ofh преобразуем ASCI I->BCD add_unsign_N_lstring_bin.dl. len_string_bin ¦.умножаем на 10
mul_unsign_NM string_bin. len_string_bin.ten,l. string_bin_w :копируем string bin_w в string_bin
eld
push si push ex
Ids si .adr_sthng_bin_w
lesdi.adr_string_bin
mov cx.len_string_bin_w repmovsb pop ex pop si
inc si dec ex jcxm2 ' jmp ml m2: mov dl .[si] and dl.Ofh
¦ add_unsign_N_lstring_bin.dl, len_string_bin
¦ результат преобразования - в строке string_bin

Одно из направлений совершенствования этой программы — динамическое \ выделение памяти для всех чисел с неизвестной длиной. Необходимо заметить, i что способ преобразования длинных чисел универсален — его можно использовать и для преобразования значений, которые укладываются в представимые в микропроцессоре диапазоны типов данных.

Ввод вещественных чисел

Теперь у нас все готово для того, чтобы выполнить ввод с клавиатуры символьного представления вещественного числа и преобразование его в соответствующий двоичный эквивалент. В уроке 19 «Архитектура и программирование сопроцессора» учебника мы обсуждали понятие вещественного числа. Отмечалось, что вещественное число имеет две формы записи — с плавающей точкой (34.89) и научную (3.45е-3=3.45х103). Для преобразования вещественного числа из символьного представления в эквивалентное двоичное можно предложить несколько способов. Самый простой — использовать возможность загрузки в сопроцессор упакованного BCD-числа. В этом случае алгоритм преобразования состоит в следующем. Символьная строка с вещественным числом вводится в память, где она преобразуется в упакованное BCD-число. При вводе указанной символьной строки запоминается положение плавающей точки. Полученное упакованное BCD-число загружается в сопроцессор, после чего оно делится на 10 в степени, соответствующей положению плавающей точки в исходном числе. Для малых чисел (в диапазоне до |1018-1|) этот способ вполне хорош. Его можно расширить, если вводить число в научном формате, при этом процесс перевода мантиссы аналогичен рассмотренному выше, но при подготовке к делению на степень 10 необходимо учесть значение степени, указанное после символа «е». Но все равно, несмотря на расширения диапазона, разряднорть мантиссы ограничена 18 цифрами. Устранить этот недостаток можно, используя операции с числами произвольной разрядности. Этот способ интересен своей универсальностью, поэтому уделим ему основное внимание.
Итак, разработаем программу ввода вещественного числа с клавиатуры в одном из двух возможных форматов — простом формате с плавающей точкой. Доработать программу для использования научного формата для вас не составит труда.
В качестве знаков, разделяющих мантиссу на целую и дробную части, можно использовать как запятую, так и точку. Суть алгоритма преобразования состоит в следующем. Производится ввод с клавиатуры символов вещественного числа. После ввода анализируются символы буфера, куда было помещены символы введенного числа, на предмет выяснения положения плавающей точки. Обнаруженная позиция запоминается. Относительно нее введенные символы делятся на символы цифр целой и дробной частей. Используя алгоритм преобразования символьного представления десятичного числа в двоичный эквивалент, преобразуется целая часть вещественного числа. Дробная часть вещественного числа также преобразуется в двоичный эквивалент. Это преобразование производится с использованием сопроцессора по формуле: ((...(u_m/b+u1_m)/b+...+u_2)/b+u.,)/b, где un — символы десятичных цифр дробной части вещественного числа u_mu1_n,..-u.2u_,, b=10. После того как данное выражение вычислено (его результат находится в вершине стека сопроцессора), производится сложение его результата с преобразованной целой частью вещественного числа. Все, теперь в вершине стека сопроцессора находится вещественное число — эквивалент своего исходного символьного представления. Текст программы преобразования вещественного числа из символьного представления достаточно велик и по этой причине приведен на дискете (prg06_05.asm). Заметьте, что с целью экономии места никаких проверок
на правильность формата вводимого вещественного числа в программе не делается.
Последнее замечание — об ограничениях на размерность исходного числа. Здесь следует различать размерности целой и дробной частей. Что касается дробной части, то здесь вообще ограничений нет, за исключением тех, которые накладывает сам сопроцессор на вводимые в его регистры значения. Для целой части узкое место — максимальная размерность операнда в команде целочисленного сложения FIADD, которая составляет 32 бита.
Ввод шестнадцатеричных и двоичных чисел мы рассматривать не будем, так как общие принципы их ввода аналогичны рассмотренным выше для десятичных чисел. Потребность в вводе с клавиатуры шестнадцатеричных и двоичных чисел возникает значительно реже, чем десятичных.

Вывод чисел на консоль

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

Вывод шестнадцатеричных чисел

Умение работать с шестнадцатеричными числами — необходимое условие успешного программирования на низком уровне. Шестнадцатеричные числа по сравнению с двоичными являются более естественными для анализа внутреннего представления информации в компьютере. Вспомним, что каждый байт — это совокупность двух тетрад, а диапазон значений, представимых одной тетрадой, как раз совпадает с диапазоном значений, которые может принимать однозначное шестнадцатеричное число. Поэтому сам процесс преобразование шестнадцатеричных чисел в символьное представление особого труда не представляет. Например, алгоритм вывода на консоль содержимого одного байта состоит в выделении некоторым способом значений его младшей и старшей тетрад и дальнейшее их преобразование в символьное представление. Если нужно вывести на консоль символьное представление более чем одного байта, то процесс выделения тетрад и их преобразования выполняется последовательно необходимое количество раз.
В качестве полезной иллюстрации алгоритма преобразования шестнадцате-ричной информации в символьное представление рассмотрим макрокоманду SHOW, которая преобразует содержимое одного из четырех регистров — AL, АН, АХ, ЕАХ в символьное шестнадцатеричное представление. Этот макрос является универсальном средством, которое позволяет осуществить «подглядывание» за содержимым регистра или области памяти динамически, во время выполнения программы. С помощью этого макроса можно визуализировать содержимое любого
из доступных регистров или области памяти длиной до 32 бит. Для этого доста точно лишь переслать содержимое нужного объекта (регистра или ячейки памяти} с учетом его размера в один из регистров AL, АН, АХ, ЕАХ. Имя одного из этих регистров указывается затем в качестве фактического аргумента макрокоманды SHOW Второй аргумент этого макроса — позиция на экране. Задавая определенные значения, мы можем судить о том, какая именно макрокоманда SHOW сработала. Еще одна немаловажная особенность данного макроса состоит в его возможности работать как в реальном, так и защищенном режимах. Распознавание текущего режима работы микропроцессора выполняется автоматически. Проверить работу данного макроопределения вы можете с помощью следующей программы.
Ниже приведены фрагменты текста макрокоманды SHOW. Полный текст этой макрокоманды имеется в ПРИМЕРе.

:show.inс
макроопределение для визуализации регистров AL. АН. АХ. ЕАХ
;:на входе:
:;агд_п - имя одного из регистров AL. АН, АХ. ЕАХ
;;п_ро2 - номер позиции на экране, по умолчанию - 1000
Show MACRO a rg_n.n_poz:=<1000>
LOCAL mai n_part.di sp.pause.tempi ate,VideoBuffer.pjnode.ml.m2
:;переход на начало блока команд, чтобы избежать выполнения данных
jmpmain_part ;:некоторые константы и переменные
main_part: :начало блока команд
сохранение в стеке используемых регистров: ЕАХ. ЕВХ. ЕСХ. EDX. EDI. DS. ES
push cs
pop ds :в bx - адрес таблицы-шаблона (для xlat)
lea bx.cs:tempi ate
xor ex.ex :очистка сх
:начало блока определения того, какой регистр был передан макросу IFIDNI <al>.<&arg_n> :если аргумент=а1 или AL. ?reg8bit=TRUE установка флага 8-битового регистра
mov ah.al ENDIF
;передан не al или AL
IFIDNI <ah>.<&arg_n> :если аргумент-ah или АН.
?reg8bit=TRUE -.установка флага 8-битового регистра
ENDIF
;передан не АН или ah
IFIDNI <ax>.<&arg_n> ;если аргумент равен ах или АХ,
?regl6bit=TRUE -.установка флага 16-битового регистра
ENDIF
;передан не ah. АН ,ах или АХ .¦ _ ,;.
обработка содержимого регистров AL. АН, АХ. ЕАХ IF (?reg8bit) -.если передан а! или ah -:"'"" "' push eax -.-'- ¦
and ah. Of Oh; обращение к старшей четверке битоВ'.-ah' shr ax. 12 -.сдвиг битов в начало (16-4=12) xlat трансляция таблицы-шаблона помещение символа из al в edi ¦¦j:\--.i ¦.;¦,.> -«-.iY ¦ ';¦' mov di.ax ¦ ¦ ,k ,-,,
shl di .8 inc ex pop eax and ax.OfOOh shr ax.8 xlat
or di.ax
shl edi. 16 . , ...
inc ex ENDIF ¦ IF (?regl6bit) ;если передан ах или ах
-.начало обработки значения регистра АХ push eax
-.обращение к старшей четверке битов ах .
and ax.OfOOOh .',.
shr ax.12 ;сдвиг битов в начало (16-4=12) xlat трансляция таблицы-шаблона ......
помещение символа из а! в старшую тетраду старшей половины ЕЩ . ",, mov di ,ax *i
shl edi.8
inc ex
pop eax push eax обращение ко второй четверке битов ах
and ax.OfOOh
' shr ax,8 ;сдвиг битов в начало (16-(4+4)=8)
xlat -.трансляция таблицы-шаблона
.помещение очередного символа в младшую тетраду старшей половины EDI
or di.ax
shl edi.8
inc ex ' ' . '' '".¦;;1
pop eax .
push eax
and ax.OfOh;обращение к третьей четверке битов в АХ -. v.'v
shr ax.4 ;сдвиг битов в начало (16-(4+4+4)=4) ¦xlat трансляция таблицы-шаблона
or di.ax -.помещение очередного символа в EDI
! shl edi.8
i nc ex
pop eax
and ax.Ofh обращение к младшей четверке битов АХ
xlat трансляция таблицы-шаблона
or di.ax помещение очередного символа в EDI
inc ex ENDIF
IF (?reg32bit) ;если передан ЕАХ или ЕАХ ;начало обработки значения регистра ЕАХ аналогично АХ
ENDIF
;вывод на экран результата с учетом режима работы микропроцессора результат - в паре EDX:ЕВХ. количество цифр - в СХ
:.........
ENDM

Вывод целых десятичных чисел из диапазона 0..99

Выше упоминалось, что для значений из диапазона 0..99 взаимное преобразование между символьной десятичной и двоичной формами может производиться командами умножения и деления двоично-десятичных (BCD-чисел) — ААМ и AAD.

;prg06_07.asm - ввод с консоли десятичного числа из диапазона 0..99
:и обратный его вывод на консоль.
buf_Oahstruc
len_bufdb 3 ;длина buf_0ah
len_in db 0 .действительная длина введенного слова (без учета Odh)
bufjn db 3 dup (20h) :буфер для ввода (с учетом Odh) ends .data
bufbuf_0ah<> adr_bufdd buf ' .code
:вводим 2 символа с клавиатуры, контроль на допустимые значения не делаем
ldsdx.adr_buf
mov ah.Oah
int 21h
хог ах.ах
emp buf .len_in,2 .-сколько чисел введено реально?
jne ml
mov ah.buf .bufjn ml: mov al.buf,buf_in+l
andax.OfOfh преобразование в неупакованное десятичное представление
add :в AL двоичный эквивалент исходного двузначного десятичного значения ;вывод на консоль содержимого AL
аат
mov dx.ax
or dx.03030h
rol dx.8 :выводим старшую цифру
mov ah,2
int 21h
rol dx.8 :выводим младшую цифру
int 21h

Для преобразования с целью последующего вывода на консоль больших двоичных значений можно использовать два способа: путем деления по модулю 10 (диапазон значений не ограничен) и с помощью сопроцессора (0..1018-1).

Вывод целых десятичных чисел из диапазона от 0 до оо

Для вывода двоичных значений из диапазона от 0 до бесконечности используется способ, в основе которого лежит получение остатков при последовательном делении исходного значения на 10.
В основе алгоритма лежит положение о том, что цифры (...U2U,U0) десятичного представления начиная с младшей получаются последовательным делением исходного двоичного значения и на 10:
U0=u mod 10; U1Lu/10j mod 10; U2 41u/10j /10J mod 10 и т. д., до тех пор пока после очередного деления делимое не окажется равным нулю: L.-iLu/10j/10j..J=0 Здесь символы L и J обозначают целую часть частного, округленного в меньшую
сторону.
Почему в отличие от алгоритмов ввода с консоли для обратного преобразова ния нет такого разнообразия способов? Это объясняется особенностью командь деления DIV микропроцессора, которая используется в описанном выше алгорит ме для получения частного и остатка. Ее требование к соотношению значенш делимого и делителя — размер частного должен быть в два раза меньше делимо го. В противном случае возникает исключение #0Е (ошибка деления) и програм ма аварийно завершается.
Исходя из этих условий нам ничего не остается, кроме как воспользоватьс программой беззнакового деления значения размером N байт на значение разме ром 1 байт. Она была рассмотрена в главе 1, посвященной целочисленным ариф метическим операциям. Для удобства использования эту программу мы офор мим в виде макрокоманды.

:prg06_08.asm - программа вывода целых десятичных чисел из диапазона О..оо.
;Вход: многобайтное двоичное число для преобразование в области памяти bin_dd.
:Выход: вывод десятичного числа из диапазона 0..<ю на экран.
:
div_unsign_N_l_I macro u.N.v.w.r
:div_unsign_N_l_I - макрокоманда деления N-разрядного беззнакового целого
:на одноразрядное число размером 1 байт (порядок следования байтов - младший байт
:по младшему адресу (Intel)). См. главу 1 и дискету
endm .data string db 10 dup (0) ;пусть максимальное десятичное число состоит из 10 цифр
len_string-$-string adr string dd string b1n~dd label BYTE "dd Offffffffh 1 еп_Ы n_dd-$ - bi n_dd ten*db To remainder dw 0 .code
значение для преобразования должно быть в памяти
les di,adr_string строка с десятичными символами
eld обработка в прямом направлении
continue:
di v_unsign_N_l_I bin_dd.1en_bin_dd.ten.Ыn_dd.remainder
mov ax.remainder
or al.30h :преобразуем в символьное представление
stosb сохраняем в string очередную десятичную цифру
inccx {подсчитываем количество цифр
cmpbinjjd.0
jne continue :вывод на консоль с конца строки
mov ah,2
std
mov si .di
dec si ml: "lodsb
mov dl ,al
Int 21h
loop ml

В данной программе преобразованию подвергается значение в памяти. Причем мы в качестве исходного двоичного значения задали максимально возможное беззнаковое число размером в двойное слово. Результат преобразования — 4 294 967 295, что полностью сходится с ожидаемым десятичным значением. Но задавать исходные значения в памяти не всегда удобно, хотелось бы, чтобы можно было подвергать преобразованию значения прямо из регистров процессора. Для такого типа преобразований (значений в регистрах процессора) лучше подойдет способ с использованием сопроцессора. Рассмотрим его.

Вывод целых десятичных чисел из диапазона 0..999 999 999 999 999 999

Этот способ вывода основан на возможности сопроцессора работать с упакованными десятичными числами. Выше мы уже рассматривали преобразование десятичных чисел в двоичное представление с использованием этой возможности. Система команд сопроцессора содержит команду FBSTP, которая сохраняет десятичное число из вершины стека сопроцессора в области памяти с одновременным преобразованием этого числа в формат десятичного числа. Область памяти, в которую происходит сохранение, должна быть описана директивой DT. Важно отметить, что команда FILD, с помощью которой вы будете помещать целое число в сопроцессор для дальнейшего преобразования, трактует целые числа как числа со знаком. Поэтому попытка задать целое число в виде Offffh (с единичным старшим разрядом операнда) приведет к тому что в стек сопроцессора будет помешено значение со всеми вытекающими отсюда последствиями для результата преобразования.

;prg06_09.asm - программа вывода целого десятичного числа
:из диапазона 0..999 999 999 999 999 999 на экран.
¦.Вход: выводимое значение - в поле string_bin_dword.
¦.Выход: вывод десятичного числа из диапазона 0. .999 999 999 999 999 999 на экран.
.data
:поле string_bin_dword содержит выводимое значение - с помощью идентификаторов.
:вводимых директивой label, это значение может трактоваться как значение
различной разрядности:
string_bin_byte label byte
string_bin_word label word
string_bin_dword label dword
string_bin_qword dq 0de0b6b3a763ffffh :зададим максимально возможное
¦.для сопроцессора двоичное целое со знаком
;в string_pack исходное значение из string bin_dword в упакованном десятичном формате string_pack dt О len_string_pack=$-string_pack adr_string_pack dd string_pack string db 20 dup (0) максимальный результат состоит из 18 десятичных цифр
len_string-$-string adr_stringdd string
.code
:.........преобразуем bin->dec
finit
fild string_bin_qword ;заносим в сопроцессор двоичное целое число fbstp string_pack извлекаем упакованное десятичное :.........распакуем........................................
Ids si.adr_string_pack
add si.len_string_pack-2 ;на конец string_pack (18 упак. дес. цифр)
les di.adr_string
mov ex.9 :9 пар упакованных десятичных цифр
cycl: xorax.ax
std :string_pack обрабатываем с конца
lodsb :в al очередные 2 упакованные десятичные цифры
¦»;распаковываем - ah-младшая. al-старшая
shi ax.4
rol al.4
ог ах.З0З0п треобразуем в символьное представление
xchg ah.al iah-младшая, al-старшая
eld ;в string записываем с начала
stosw
loop cycl :.........выводим на консоль...............................
mov bx.l -.стандартный дескриптор - экран
mov cx.len_string
Ids dx.adr_string {формируем указатель на строку string
mov ah.40h ;номер функции DOS
int 21h :выводим
jc exit :переход в случае ошибки

Вывод вещественных чисел

Последнее преобразование, которое мы рассмотрим в этом разделе, — преобразование вещественного значения в вид, пригодный для его визуализации на экране консоли. Ниже приведены только те фрагменты программы, которые относятся непосредственно к преобразованию. Полный текст программы находится в ПРИМЕРе.

;prg06_10.asm - программа вывода вещественного числа короткого формата (32 бита).
;Вход: выводимое значение - в поле float32.
:Выход: вывод вещественного числа короткого формата на экран
.data
dec_bin_mant32dt 0 ;мантисса в двоично-десятичном представлении
dec_bin_har32 dt 0 характеристика в двоично-десятичном представлении
cwr dw 0 переменная для сохранения состояния per. cwr
ten dw 10 ;константа, равная 10
float32dd 1.2345678el2 значение вещ. числа размером в 32 бита для вывода
mant32 dd 0 :мантисса в двоичном представлении .
har32 dd 0 характеристика - вещ. формат в двоичном представлении
int_har32 dd 0 характеристика - целое в двоичном представлении
number db 0
char db 0
cursor_column db 0
cursorjine db 0
number_of_digits db 9
flag db 0
.code
next_cursor_column ргос .-процедура сдвига курсора на одну позицию вправо
обязательно наличие в программе процедур::read_cursor_position, set_cursor_position
next_cursor_column endp
set_cursor_position ргос .'процедура позиционирования курсора
set_cursor_position endp
read_cursor_position ргос процедура определения текущей позиции курсора
read_cursor_position endp
print_charргос процедура вывода символа с учетом цвета
;.........
printjrhar endp
positivejiar ргос
:выделение мантиссы из короткого формата (32бита) и ее преобразование
:в двоично-десятичный формат (для положительной характеристики
результат в st(0))
fimul ten
sub int_har32,6 lab_p_h: fidiv ten
cmp int_har32.0
dec int_har32 jg lab_p_h ret I positivejiar endp
negativejiar ргос
:выделение мантиссы из короткого формата (32 бита) и ее преобразование
;в двоично-десятичный формат (для отрицательной характеристики результат в st(0))
fidiv ten
sub int_har32.7 lab_n_h: fimul ten
cmp int_har32.0
inc int_har32
jl lab_n_h
ret
negative_har endp fprint32 proc :вывод вещественного числа (32 бита) в десятичном виде
pusha
установка размера мантиссы в 24 бита fstcw cwr and cwr.1111000011111111b
or cwr.ldew cwr
fid float32:загрузка 32-битного числа в стек сопроцессора fxtract выделение мантиссьКБШ и характеристики^!) fstp mant32 запоминаем мантиссу fist har32 '.запоминаем характеристику щеревод двоичн. характеристики в дес. характеристику fldlg2 -.загрузка десят. лог. двух fimul har32 -.умножение двоичной хар-ки на 1од10(2) frndint -.округление fistp int_har32 сохранение десетяч. характеристики
fild int_har32
fbstp dec_bin_har32 сохранение двоично-десятичного значения характеристики
:выбор процедуры по выделению мантиссы fid float32 cmp har32.0 jge easel
call negativejiar :вызвать процедуру преобразования мантиссы, -.если хар-ка отрицательная
jmp end_case
I^casel: call positivejiar -.вызвать процедуру преобразования мантиссы, -.если хар-ка положительная end_case-. fbstp dec_bin_mant32 сохранение двоично-десятичного представления мантиссы ;вывод на экран вещественного числа lea si.dec_bin_mant32 add si.9 mov- a "I. [si] : вывод знака числа cmp al .0 je zero mov char."-" call print_char call next_cursor_column
:данный фрагмент пропускает байты с нулевым содержимым до первого байта ;со значащей цифрой zero: dec si
dec number_of_digits mov al.[si] cmp a 1.0
jne first_zero ;найден первый байт со значением, отличным от нуля jmp zero ;байт имеет нулевое значение - продолжаем поиск ; просмотр полубайтов первого найденного байта с ненулевым значением first_zero: and al ,11110000b cmp a 1,0 je second_digit :если старший полубайт байта равен нулю, начинаем вывод
:со второго байта jmp first_digit :если старший полубайт байта не равен нулю, начинаем вывод
:с первого байта :начало цикла вывода мантиссы print_digits: dec si
dec number_of_digits :индекс выводимого байта mov al.[si]
:вывод первого полубайта, содержащего цифру first_digit: andal ,11110000b shr al.4 add al.30h mov char.al
call print_char ,
call next_cursor_column
;если выводимая цифра первая, то выводим после нее точку cmp flag.0 jne second_digit mov char."." call print_char ca11 next_cursor_column inc flag
:вывод второго полубайта, содержащего цифру second_digit: mov al. [si] and al,00001111b add a1,30h mov char.al call print_char call next_cursor_column
.-если выводимая цифра первая, то выводим после нее точку cmp flag,0 jne nonfirst_digit mov char,"." call print_char call next_cursor_column inc flag
nonfi rst_digit: cmp number_of_d1g1ts.O jne print_digits mov flag,0
:вывод характеристики числа mov char,"E" call print_char call next_cursor_column lea si,dec_bin_har32
:вывод знака числа add si.9 mov al,[si] cmp al.O je printjiar mov char,"-" call print_char call next_cursor_column : значения характеристики print_har: sub si .9
mov al.[si] ;вывод первой цифры характеристики
and al.11110000b
shr al.4
add al.30h
mov char.al
call print_char
call next_cursor_col umn : вывод второй цифры характеристики
mov al.[s1]
and al.00001111b
add a1.30h
mov char.al
call print_char
call next_cursor_col umn
mov flag.0
fprint32 endp
main proc
\.........
call fprint32


Работа с файлами в программах на ассемблере (Часть 1)

Продавай мобильный контент со своего сайта

Работа с файлами в программах на ассемблере (Часть 1)

  • Работа с файлами в MS DOS (имена 8.3)
    • Создание, открытие, закрытие и удаление файла
    • Чтение, запись, позиционирование в файле
    • Получение и изменение атрибутов файла
    • Работа с дисками, каталогами и организация поиска файлов
  • Работа с файлами в MS DOS (длинные имена)
    • Создание, открытие, закрытие и удаление файла
    • Получение и изменение атрибутов файла
    • Работа с дисками, каталогами и организация поиска файлов

Очевидно, целесообразно рассматривать прикладную систему как некоторую
совокупность данных и операций, определенных па этих данных, а не просто
как набор программ, взаимодействующих между собой посредством
обмена данными. Программы — это всего лишь рабочие инструменты,
в то время как данные — это своего рода исходное сырье,
из которого вырабатывается конечный продукт. Набор данных вне среды
приспособленной для его использования, не представляет
никакой ценности, однако в рамках такой среды именно
набор данных приобретает основное значение.
Динар Нурмухамедович Бибишев

Язык ассемблера не содержит средств для работы с файлами. Если такая необходимость возникает, то программа должна содержать фрагменты кода, в которых производится обращение к средствам операционной системы, осуществляющим взаимодействие с файловой системой. Это лишний раз подтверждает тот факт, что в области взаимодействия с внешним миром программа на ассемблере оказывается привязанной как к конкретной аппаратной, так и конкретной операционной платформам. В сегодняшней ситуации программисту все еще приходится сталкиваться с необходимостью программирования для MS DOS. Поэтому изучение средств для работы с файлами этой операционной платформы не потеряло своей актуальности и эти средства в плане совместимости поддерживаются различными реализациями Windows. В реализации MS DOS 7.0 введена поддержка длинных имен файлов, используемых системой файлового ввода-вывода Win 32. Таким образом можно выделить три аспекта работы с файлами из программ на ассемблере:

  • работа с системой файлового ввода-вывода MS DOS, использующей короткие имена (по схеме 8.3);
  • работа с системой файлового ввода-вывода MS DOS, использующей длинные имена (длиной до 255 символов);
  • работа с системой файлового ввода-вывода Win 32;
  • использование файлов особого вида, поддерживаемых Win 32 — проецированных на память.

Целью данной главы является предоставить читателю фрагменты кода, реализующего наиболее часто используемые операции по работе с файлами на различных операционных платформах. При этом нам не обойтись без определенной систематизации, но это будет сделано лишь для того, чтобы создать у читателя общее представление об этом вопросе. Если у вас впоследствии возникнет потребность в реализации файловых функций, чье полное практическое описание отсутствует в материале этой главы, то более подробные сведения о них можно будет найти в других справочных руководствах, например MSDN. Далее, используя общие принципы организации ввода-вывода, рассмотренные ниже, вы сможете без труда решить возникшую проблему. Материалы всех разделов подобраны так, чтобы читатель мог познакомиться с общими принципами организации ввода-вывода в каждом случае, начиная от самых простых.

Работа с файлами в MS DOS (имена 8.3)

В основе файловой системы MS DOS лежит древовидная структура каталогов. Корень этой структуры представляет собой совокупность ограниченного числа дескрипторов, описывающих файлы и каталоги (подкаталоги) следующего уровня. Подкаталог представляет собой файл особого типа, который содержит дескрипторы файлов и подкаталогов очередного нижележащего уровня. В отличие от корневого каталога количество дескрипторов в подкаталоге не ограничено и определяется только размером диска. Дескриптор представляет собой экземпляр структуры размером 32 байта. Поля этой структуры содержат различную информацию о файле: идентификатор файла и его характеристики — дата и время создания (модификации), номер начального кластера, длина файла и его атрибуты.
Для использования файла в программе необходимо выполнить следующие операции:

  • создание нового файла;
  • открытие существующего файла;
  • запись/чтение в/из файл(а);
  • закрытие файла.

Операционная система MS DOS поддерживает эти операции с помощью набора функций прерывания 21h. Кроме этих функций данное прерывание содержит функции для работы с каталогами:

  • создать каталог;
  • удалить каталог;
  • сменить каталог.

Существует также ряд других функций для работы с файловой системой, в том числе для поиска файлов и получения информации о них.

Создание, открытие, закрытие и удаление файла

Прежде чем использовать файл в программе, его необходимо открыть с помощью функции 3dh прерывания 21h. Если файл не существует, то перед открытием
его нужно создать. Оба эти действия выполняются одной из следующих функций: 3ch, 5bh, 5ah, 6ch.

Создание файла с усечением существующего до нулевой длины

Вход: АН = 3Ch; CX = атрибуты файла (значения битов: 0=1 — только чтение; 1= 1 — скрытый файл; 2=1 — системный файл; 3=0 — игнорируется; 4=0 — зарезервирован (каталог), должен быть равен 0; 5 — бит архивации; 6= 0 — резерв; 7=1 — общий файл в системе Novell Netware; 8...15=0 — резерв); DS:DX — ASCIZ-имя файла.
Выход: CF=O — АХ = дескриптор файла; CF=1 — АХ = код ошибки (3 — нет такого пути; 4 — нет свободного дескриптора файла; 5 — доступ отказан).

хогсх.сх :атрибуты файла - обычный файл Ids dx,point_fname:
формируем указатель на имя файла movah.5bh :номер функции DOS
int21h открываем файл
jnc ml : обойдем открытие файла
moval,02h ;режим доступа
movah.3dh ;номер функции DOS
int 21h : открываем файл
jc exit :переход, в случае ошибки
mi: :действия при успешном открытии файла:
mov handle.ax :сохраним дескриптор файла

Открытие или создание файла с расширенными возможностями

Функция 6Ch появилась в последних версиях MS DOS (DOS 4.0+). С ее появлением устраняется необходимость отслеживать существование создаваемого файла. Для корректной работы достаточно задать нужные значения в соответствующих регистрах. Анализ возможных значений показывает, что данная функция фактически заменяет существовавшие до этого функции создания и открытия файлов.
Вход: АХ = 6C00h; BL = флаги — режим открытия (значения битов: 7 — наследование; 4-6 — режим разделения; 3=0 — резерв; 0-2 — режим доступа); ВН = флаги (значения битов: 6=0 — использовать стандартную для MS DOS буферизацию; 6=1 — отменить стандартную для MS DOS буферизацию; 5=0 — использовать обычный обработчик ошибок (int 24h); 5=1 — не использовать обычный обработчик ошибок (int 24h), для выяснения причины ошибки использовать функцию 59h int 21h); CX = атрибуты создаваемого (и только) файла; DL = действия, если файл существует или не существует (значения бит: 0-3 — действие, если файл существует (0000 — вернуть ошибку; 0001 — открыть файл; 0002 — открыть файл без сохранения существующего); 4-7 — действие, если файл не существует (0000 — вернуть ошибку; 0001 — открыть файл; 0002 — создать и открыть файл); DH = 00h — резерв; DS:SI — адрес строки с ASCIZ-именем файла.
Выход: CF = 0 — успешное выполнение функции: АХ = дескриптор файла, СХ = состояние (0 — файл открыт; 1 — файл создан и открыт; 2 — файл открыт без сохранения содержимого существующего файла); CF = 1 — АХ = код ошибки.
Следующий фрагмент программы показывает вариант применения функции 6Ch.

:prg07_04.asm - программа демонстрации открытия или создания файла :с расширенными возможностями в текущем каталоге функцией 6Сп.
.data
handle dw 0 :дескриптор файла
filename db 'my_file.txt'.0
point_fname dd filename
.code
хогсх.сх атрибуты файла - обычный файл
movbx.2 :режим доступа обычный - доступ для чтения-записи
movdx.l :если файл существует, то открыть его. в обратной случае вернуть ошибку
(для эксперимента)
Ids si .point_fname-.формируем указатель на имя файла movah.6ch :номер функции DOS int 21h открываем файл jnc ml ;если файл существовал, то переход movdx.lOh -.открыть файл movah.6ch :номер функции DOS int 21h :открываем файл jc exit -.переход в случае ошибки ml: :действия при успешном открытии файла: mov handle.ах :сохраним дескриптор файла

Закрытие файла

В конце работы с файлом его нужно закрыть. Но это действие не является обязательным, так как функция 4сп, которая завершает выполнение программы, в числе прочих действий выполняет и закрытие всех файлов. Вход: АН = 3Eh; BX = дескриптор файла, полученный при его открытии. Выход: CF = 0 — АХ = не определен; CF = 1 — АХ = код ошибки: 6 — недопустимый
дескриптор.
Во время закрытия файла выполняются все незаконченные операции записи на диск в элементе каталога, соответствующего файлу, модифицируются различные поля, в том числе поля времени и даты устанавливаются в текущее время.

;prg07_05.asm - программа демонстрации закрытия файла функцией 3Eh.

:.........
.data
handle dw 0 :дескриптор файла filename db 'my_file.txt',О point fname dd filename
;......:
.code :*......
хогсх.сх :атрибуты файла - обычный файл
Ids dx.point_fnanve :фориируем указатель на имя файла
movah.5bh :номер функции DOS
int 21h :открываем файл
jnc ml ;обойдем открытие файла
moval.02h :режим доступа
1movah.3dh -.номер функции DOS
int 21h :открываем файл
jc exit :переход в случае ошибки
ml: :действия при успешном открытии файла:
mov handle.ах ;сохраним дескриптор файла
т2: ;закрываем файл:

В случае задания имени, как в примере выше, файл будет создан в корневом каталоге текущего диска. Для того чтобы разместить файл в конкретном каталоге, необходимо указать полный путь к нему с завершающим символом ' \' и 13 нулевыми байтами на конце, например:
filename db 'e:\asm_on_a\'.13 dup(0),0

Чтение, запись, позиционирование в файле

При работе с материалом данного раздела помните, что функции чтения и записи можно использовать не только с дескрипторами заранее открытых файлов, но и с дескрипторами стандартных устройств. Эти дескрипторы имеют постоянное значение и доступны в любое время функционирования системы: 0 — клавиатура; 1 и 2 — экран; 3 — последовательный порт СОМ1; 4 — параллельный порт LPT1.

Установка текущей файловой позиции

Чтение-запись в файле производятся с текущей файловой позиции, на которую указывает файловый указатель. Функция 42h MS DOS предоставляет гибкие возможности как для начального, так и для текущего позиционирования файлового указателя для последующей операции ввода-вывода.
Вход: АН = 42h; BX = дескриптор файла, полученный при его открытии; AL = начальное положение в файле, относительно которого производится операция чтения-записи (OOh — смещение (беззнаковое значение в CX:DX) от начала файла; O1h — смещение (значение со знаком в CX:DX) от текущей позиции в файле; 02h — смещение (значение со знаком в CX:DX) от конца файла); CX:DX = смещение новой позиции в файле относительно начальной.
Выход: CF = 0 — DX:AX = значение новой позиции в байтах относительно начала файла; CF = 1 — АХ = код ошибки: 1 — неверное значение в AL; 6 — недопустимый дескриптор файла.
Методы позиционирования, заданные величиной в AL, по-разному трактуют значение в паре регистров CX:DX. Метод al = 00 трактует значение в CX:DX как абсолютное. Два других метода (al = 01 и al = 02 ) трактуют содержимое CX:DX как значение со знаком. Необходимо быть внимательным при выполнении операции позиционирования для избежания последующих ошибок при операции чтения-записи. Так, значение в СХ: DX, позиционирующее указатель, может указывать за пределы файла. При этом выделяются два случая:

  • значение в СХ: DX указывает на позицию перед началом файла — в этом случае последующая операция чтения-записи будет выполнена с ошибкой;
  • значение в СХ:DX указывает на позицию за концом файла — в этом случае последующая операция записи приведет к расширению файла в соответствии со значением в CX:DX.

Примеры использования функции 42h приведем при рассмотрении функций чтения-записи.

Запись в файл или устройство

Запись в файл производится функцией 40h с текущей позиции файлового указателя. Вход: АН = 40 h; ВХ = дескриптор файла; СХ = количество байтов для записи;
DS:DX — указатель на область, из которой записываются данные. Выход: CF = 0 — АХ = число действительно записанных байтов в файл или устройство; CF = 1 — АХ = код ошибки: 5 — в доступе отказано; 6 — недопустимый дескриптор.
Если при вызове функции 40h регистр СХ равен нулю, то данные в файл не записываются и он усекается или расширяется до текущей позиции файлового указателя. Если СХ не равен нулю, то данные в файл записываются начиная с текущей позиции файлового указателя. Операция записи также продвигает файловый указатель на число действительно записанных байтов.
Положение файлового указателя можно изменять явно с помощью функции 42h. Вначале приведем пример программы, выводящей данные на экран.

:prg07_08.asm - программа демонстрации вывода на экран строки функцией 40h.
.data
string db 'строка для вывода на экран функцией 40h'
len_string=$-stnng point_fname dd string
..........
.code
movbx.l -.стандартный дескриптор - экран
mov cx.1en_string
Ids dx.point_fname;формируем указатель на строку string
movah.40h -.номер функции DOS
int 21h ;выводим
jc exit ;переход в случае ошибки
пор -.для тестирования

Далее приведем пример программы, которая заполняет файл my_file.txt данными в виде строк символов, вводимых с клавиатуры. Длина строк — не более 80 символов. Нажатие клавиши Enter после ввода каждой строки означает, что эта строка символов должна являться отдельной строкой файла my_file.txt. Для этого перед выводом каждой строки в файл в конце ее необходимо вставлять символы OdOah. При нажатии клавиши Пробел в начале ввода очередной строки (ASCII-код — 3210 или 2016) направление ввода данных в файл изменяется следующим образом: файл расширяется на величину, равную количеству уже введенных символов, и дальнейший ввод осуществляется с конца файла. Завершение работы программы определяется моментом, когда оба введенных потока Ь в файле встречаются (не перекрываясь).

:prg07_09.asm - программа заполнения файла my_file.txt данными в виде строк символов.
:вводимыми с клавиатуры.
buf_Oahstruc
len_buf db 83 ;длина buf_0ah
len_in db 0 действительная длина введенного слова (без учета 0dh)
buf_in db 82 dup (20h) :буфер для ввода Сс учетом 0dh и позднее добавляем Oah)
ends
.data
handle dw 0 :дескриптор файла
filename db 'my_file.txt',0
point_fname dd filename
buf buf_0ah<>
prev_d label dword ;для сохранения длины предыдущей строки при выводе с конца файла prev dw 0
dw 0
middle dd 0 ;позиция в середине файла, при достижении которой снизу выходим :из программы
.code
:-----открываем файл-----...............................--
хогсх.сх ;атрибуты файла - обычный файл
movbx,2 ;режим доступа - доступ для чтения-записи, режим буферизации MS DOS
movdx,12h ;если файл существует, то открыть его без сохранения прежнего содержимого, :в обратном случае создать его
Ids si ,point_fname:формируем указатель на имя файла
movah.6ch ;номер функции DOS
int 21h открываем (создаем) файл
jc exit :если ошибка, то переход на конец ;действия при успешном открытии файла:
mov handle.ax ,-сохраним дескриптор файла ;—позиционируем файловый указатель с начала файла.......
mov ah.42h
хог al,al
хог ex,ex
хог dx.dx
mov bx, handle
int 21h cycl: ;вводим очередную строку с клавиатуры
lea dx.buf
mov ah,Oah
Int 21h ;для красоты ввода выводим на экран символ Oah
mov dl .Oah
mov ah.2
int 21h
emp buf.buf_in.20h;первый символ введенной строки сравниваем с пробелом
je revers ;переход на изменение ввода - добавляем Oah в конец введенной строки
lea si.buf.buf_in
mov al .buf .lenjn
cbw push si
add si ,ax
incsi учитываем неучтенный в lenjn символ 0dh
mov byte ptr [si],Oah H--......вывод в файл:..........................---........
I popdx указатель на область, откуда будем выводить строку
mov bx.handle
add ax,2 учитываем неучтенный в len_in символ 0dh
movcx.ax :длина выводимых данных
mov ah.40h
int 21h
jmp cycl
revers: ;записываем файл с конца, предварительно расширив его ;узнаем. сколько было уже записано до этого: ;для этого вначале сбрасываем буферы на диск
mov bx.handle
mov ah.68h
int 21h ;теперь можно и узнать - определение длины файла:
mov al ,2
хог сх.сх
хог dx.dx ;CX:DX -0 - нулевое смещение
mov ah,42h
int 21h :в DX:AX возвращается длина файла в байтах
jc exit :если ошибка :формируем полную длину в edx
shl eax,16
shld edx.eax,16
mov middle.edx сохраним как условие выхода из программы при достижении снизу расширение файла с помощью функции 42h int 21h и последующей записи :умножаем длину на 2. при первой операции записи файл расширится:
shl edx.l
shld ecx.edx.16
mov al.O
хог сх.сх
mov ah.42h
int 21h расширяем файл, устанавливая указатель
jc exit :если ошибка расширим файл, выведя последнюю введенную строку с пробелом:
cycl2: lea si,buf,buf_in
mov al .buf .lenjn
cbw ptush si
add si.ax
incsi учитываем неучтенный в lenjn символ 0dh
добавляем Oah в конец введенной строки
mov byte ptr [si],Oah ;выводим в файл:
popdx указатель на область, откуда будем выводить строку
add ах.2 учитываем неучтенный в len_in символ 0dh
movcx.ax :длина выводимых данных
movprev.ax .сохраним длину для корректировки при выводе следующей строки
mov bx.handle movah.40h int 21h jc exit
;сбрасываем буфер, чтобы смотреть изменения в файле при работе в отладчике -:легче запретить (см. обсуждение ниже)
mov bx,handle
mov ah,68h Int 21h :вводим очередную строку с клавиатуры
lea dx.buf
mov ah.Oah
Int 21h :для красоты ввода выводим на экран символ Oah
mov dl .Oah
mov ah,2
int21h
;......использование 42h с отрицательным смещением относительно
:текущего значения файлового указателя:
устанавливаем файловый указатель в позицию вывода следующей строки
;с учетом того, что выводим с конца (текущей позиции) файла:
хог есх.есх
mov al.buf,len_in
cbw
add prev.ax
add prev.2 учитываем наличие OdOah
sub ecx.prev_d :получаем отрицательное смещение - сформируем его в паре СХ:DX
shrd edx,ecx,16
shr edx.16 :довернем edx
shr ecx.16 :и есх устанавливаем файловую позицию для записи очередной строки
mov bx,handle
mov ah.42h
moval.l ;смещение от текущей позиции
int 21h :сравним текущую позицию с middle
shl eax.16
shld edx,eax,16
cmp edx,middle
jl exit
jmp cycl2 exit:

Программа выглядит не очень эстетично, но главная ее цель достигнута — показать работу с файловым указателем при записи в файл. Мы попробовали разные варианты: позиционирование на конец файла (при этом можно узнать длину файла); использование отрицательного смещения (задавая нулевое значение в CX:DX при al = 1 можно получить в DX:AX текущую позицию в файле); расширение файла путем задания в СХ: DX заведомо большего значения, чем длина файла. Как видно из программы выше, все эти и другие эффекты достигаются за счет манипулирования значениями в парах СХ :DX и DX:AX, а также в регистре AL, где задается начальное положение в файле, относительно которого производится операция чтения-записи.
В заключение рассмотрения функции 40h записи в файл отметим то, для чего мы использовали функцию сброса буферов на диск 68h. Для этого коротко необходимо коснуться проблемы буферизации ввода-вывода в MS DOS. MS DOS ис-
пользует буферизацию ввода-вывода для ускорения работы с диском. При этом, р частности, данные, записываемые на диск, не записываются на него сразу, а по-щешаются вначале в буфер. Запись буфера на диск производится при его заполнении- Буферизацию эффективно использовать при интенсивной работе с одними и теми же данными. Тогда при необходимости чтения данных с диска они 6уДУт читаться из буфера. В нашей программе буферизация нам только мешала, так как при работе в отладчике мы не могли своевременно наблюдать за изменениями выходного файла my_file.txt Для этого нам приходилось использовать функцию 68h для принудительного сохранения буферов на диск. Вход: АН = 68h; BX = дескриптор файла. Выход: CF = 0 в случае успеха; CF = 1 — АХ = код ошибки.
В результате работы функции все данные из буферов дисков DOS немедленно записываются на диск, при этом модифицируется соответствующий файлу
элемент каталога.
Для нашей задачи буферизацию лучше вовсе запретить, тогда отпадет необходимость в принудительном сохранении строк в файле для того, чтобы в динамике отслеживать его изменения. Для этого при вызове функции 6ch в регистре ВН необходимо установить бит 6 следующим образом: 6 = 0 — использовать стандартную для MS DOS буферизацию; 6 = 1 — отменить стандартную для MS DOS буферизацию. В нашем примере это можно сделать так:

:......открываем файл-------------------------------------
хогсх.сх атрибуты файла - обычный файл
mov bx.4002h :режим доступа - доступ для чтения-записи, запрет буферизации
movdx,12h :если файл существует, то открыть его без сохранения прежнего
содержимого, в обратном случае создать файл Ids si.point_fname:формируем указатель на имя файла movah.6ch :номер функции DOS int21h открываем (создаем) файл jc exit :если ошибка, то переход на конец
--------------------------------------------------------------------------

Все вызовы функции 68h в приведенной выше программе можно закомментировать.

Чтение из файла или устройства

Чтение из файла в область памяти осуществляется функцией 3Fh.
Вход: АН = 3Fh; BX = дескриптор файла; СХ = количество байтов для чтения; DS-.DX — указатель на область памяти, в которую помещаются прочитанные байты. Выход: CF = 0 — АХ = число действительно прочитанных байтов из файла; CF = 1 — АХ = код ошибки: 5 — в доступе отказано; 6 — недопустимый дескриптор.

Чтение данных производится начиная с текущей позиции в файле, которая после успешного чтения смещается на значение, равное количеству прочитанных байтов. Если в качестве файла используется стандартная консоль (клавиатура), то чтение производится до первого символа CR (carriage return) с кодом 0dh, соответствующего нажатию клавиши Enter. Это, кстати, еще один способ ввода " Данных с клавиатуры в программу. Кроме символов введенной строки в ее конец помещаются символы 0dh и Oah. Это необходимо учитывать при задании размера буфера для ввода. Способ ввода данных с экрана с помощью функции 3Fh . стрирует приведенный ниже пример программы.

:prg07_10.asm - программа демонстрации ввода данных с экрана с помощью функции 3Fh.
.data
string db 80 dup Г ") 1en_string=$-string point_fname dd string
.code
:.........вводим с экрана......-----.....----------------
movbx.O стандартный дескриптор - клавиатура
mov cx.len_string
Ids dx.point_fname:формируем указатель на строку string
movah,3fh ;номер функции DOS
int 21h
jc exit :переход в случае ошибки ;---------выводим на экран---------------------...........
movbx.l стандартный дескриптор - экран :две строки ниже в данном случае можно опустить
mov ex.len_string
Ids dx.point_fname;0opMnpyeM указатель на строку string
movah.40h ;номер функции DOS
int 21h открываем файл
jc exit :переход в случае ошибки

Для демонстрации работы функции с дисковым файлом приведем программу чтения и вывода на экран содержимого файла, имя которого вводится в командной строке. Побочная цель этой программы — научиться обрабатывать в программе командную строку DOS. Поясним последний момент. Содержимое командной строки, следующее за именем программы при ее вызове и называемое хвостом команды, помещается в префикс программного сегмента (PSP) со смещением 80h от его начала и максимально имеет размер 128 байт. Первый байт этой области содержит длину хвоста команды, а первый символ хвоста, при его наличии, располагается со смещением 81h от начала PSP. Последний символ хвоста команды — всегда 0dh. Начало PSP найти очень легко — когда программа загружается в память для исполнения, то загрузчик устанавливает регистры ES и DS равными адресу PSP.

:prg07_ll.asm - программа чтения и вывода на экран содержимого файла. ;имя которого вводится в командной строке.
.data
filejiame db 128 dup (" ") ;буфер, в который будет помещен путь к файлу
point_fname dd file_name
string db 80 dup (" ")
len_stnng=$-string
point_string dd string
>handle dw 0 дескриптор файла
[size_f dd 0 :размер файла
.code
main: :копируем командную строку в filejiame
;вначале уберем (установкой указателя) ведущие пробелы в командной строке
:перед путем к файлу: movdi ,81h mov al," " mov ex. 128 repe scasb
dec di push di pop si
movax.@data -.адрес сегмента данных - в регистр АХ
mov es.ax :ax в es iTOvcl.ds:[80h] deccl
lea di .filejiame rep movsb push es pop ds
:--------открываем файл--......-----------------.........
moval.OOh :режим доступа - только чтение
Ids dx.point_fname:формируем указатель на имя файла
movah.3dh ;номер функции DOS
int 21h открываем файл
jc exit :переход в случае ошибки
mov handle.ax
;--------определяем размер файла-------------------------
raovbx.ax -.дескриптор файла - в bх mov al .2 xor ex. ex
xordx.dx :CX:DX =0 - нулевое смещение mov ah.42h
int 21h ;в DX:AX возвращается длина файла в байтах jc exit
:если ошибка :формируем полную длину в
edx shl eax.16 shld edx.eax.16 mov size_f.edx сохраним как условие выхода из программы при достижении снизу
;--------устанавливаем указатель на начало файла---.......
mov Ьх.handle :дескриптор файла - в Ьх
mov al .0
xor ex.ex
xordx.dx ;CX:DX =0 - нулевое смещение
mov ah,42h
int 21h :текущий указатель в начале файла
jc exit :если ошибка
;.........читаем файл по lenjstring байт................---
cycl: mov bx.handle :дескриптор файла в Ьх mov cx.lenjsthng
Ids dx.pointjstring :формируем указатель на строку string movah.3fh
:номер функции DOS int 21h :открываем файл jc exit ;переход в случае ошибки
;.........выводим на экран целиком.....---------.......----------
movbx.l стандартный дескриптор - экран
mov cx.len_string
Ids dx.point_string .формируем указатель на строку string movah.40h
;номер функции DOS
int 21h открываем файл
jc exit :переход в случае ошибки
cwde расширяем количество выведенных байт
sub size_f.eax cmp size_f.О
jleexit :достигли конца файла
. jmp cycl
exit: :выход из программы
mov al .1 int 21h
.......

He забывайте после определения размера файла возвращать файловый указатель в нужное место файла.

Получение и изменение атрибутов файла

MS DOS позволяет получить для анализа и при необходимости изменить имя файла, байт атрибутов файла, время и дату его последней модификации в элементе каталога, соответствующего этому файлу. Для этого предназначены функции 43h, 56h, 57h. Подфункция 00 функции 43h прерывания 21h предназначена для получения слова атрибутов файла.

Получить атрибуты файла

Вход: АХ = 4300h; DS:DX — ASCIZ-строка с именем (путем) файла.
Выход: CF = 0 — СХ = слово атрибутов файла; CF = 1 — АХ = код ошибки: 1 — неверное значение в AL; 2 — файл не найден; 3 — несуществующий путь; 5 — доступ запрещен.

:prg07_12.asm - программа демонстрации получения атрибутов файла.
.data
fname db "maket.asm".
point_fname dd fname
.code
:----.....получим атрибуты файла-----------------........-
Ids dx.point_fname:формируем указатель на строку string movax.4300h ;номер функции DOS int 21h
jc exit :переход в случае ошибки :в сх атрибуты (см. ниже)

Напомним формат байта атрибутов:

Биты | Описание
7 | Разделяемый в Novell NetWare
6 | Не используется
5 | Архивный
4 | Каталог
3 | Метка тома (только исполнение Novell NetWare)
2 | Системный
1 | Скрытый
0 | Только чтение

Установить атрибуты файла

Подфункция 01 функции 43h прерывания 21h предназначена для установления
слова атрибутов файла.
Вход: АХ = 4301п; СХ = новое слово атрибутов файла; DS:DX — ASCIZ-строка с именем (путем) файла.
Выход: CF = 0 — АХ = не определен; CF = 1 — АХ = код ошибки: 1 — неверное значение в AL; 2 — файл не найден; 3 — несуществующий путь; 5 — доступ запрещен.

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

Для переименования файла используется функция 56h.
Вход: АН = 56h; DS:DX — ASCIZ-имя существующего файла; ES:DI — ASCIZ-имя
нового файла; CL = маска атрибутов.
Выход: CF = 0 — при успешном переименовании; CF = 1 — АХ = код ошибки: 2 — файл не найден; 3 — несуществующий путь; 5 — доступ запрещен; 1 lh — устройства для старого и нового файлов не совпадают.
Функция 56h позволяет произвести перемещение между каталогами, не изменяя устройства.

;prg07_13.asm - программа перемещения между каталогами.
;не изменяя устройства функцией 56h.
¦
.data
fname_s db "maket.asm".О
point_fname_s dd fname_s
fname_d db "e:\maket.asm".0
point_fname_d dd fname_d
.code
;.....переместим файл из текущего в корневой каталог------
Ids dx.point_fname_s :формируем указатель на строку fname_s (исх. файл)
1 es di,point_fname_d ;формируем указатель на строку fname_d (целевой файл)
Imov ah.56h ;номер функции DOS int 21h
jc exit ;переход в случае ошибки

Получить дату и время создания или последней модификации файла
Получить/изменить дату и время создания или модификации файла можно с помощью подфункций функции 57h.
Вход: АХ = 5700h; ВХ = дескриптор файла.
Выход: если CF = 0: СХ = время, DX = дата. Если CF = 1: АХ = код ошибки (CF = п. 1 — недопустимый номер подфункции в А1; 6 — недопустимый дескриптор Время и дата файла получаются в следующих форматах.

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

Установить дату и время создания или последней модификации файла

Вход: АХ = 5701п; ВХ - дескриптор файла; СХ - новое время, DX = новая дата. Выход: если CF = 0: СХ = время, DX = дата. Если CF = 1 — АХ = код ошибки: 1 — недопустимый номер подфункции в А1; 6 — недопустимый дескриптор.

Работа с дисками, каталогами и организация поиска файлов

Задача поиска традиционно является актуальной. При рассмотрении вопроса работы с файлами ее также не обойти. Мы рассмотрим номенклатуру средств, предлагаемых MS DOS для поиска файла и определения его местоположения в древовидной структуре каталогов текущего диска.
Во время работы с предыдущими программами вы заметили, что при задании имен файлов мы практически не указывали имен дисководов и путей к этим файлам. MS DOS имеет средства для установки текущего диска и каталога, в котором выполняются все текущие операции с файлами. При необходимости можно изменить текущий диск и каталог. Ниже приведено несколько функций для работы с текущими диском и каталогом — определение, изменение, получение информации.

Получить номер заданного по умолчанию дисковода

Вход: АН = 19h.
Выход: AL - номер дисковода (00h - A:, 01h - В: и т. д.).

:prg07_14.asm - программа получения номера текущего (по умолчанию) дисковода функцией 19h.

I .code
m ;.....Получить номер текущего (по умолчанию) дисковода ---
movah.l9h :номер функции DOS
1nt2lh
jc exit -.переход в случае ошибки :в al номер текущего диска

Выбрать заданный по умолчанию диск

Вход: АН = OEh; DL = номер нового диска по умолчанию (00h = A:, 01h = В: и т. д.).
Выход: AL = максимально возможный в данной системе номер дисковода (00h =
A:, 01h = В: и т. д.) определяется на основе параметра LASTDRIVE в файле
CONFIG.SYS.

Получить информацию о свободном дисковом пространстве

Вход: АН = 36h; DL = номер диска (00h- текущий,01h = А: и т. д.).
Выход: АХ = FFFFh — неправильный номер устройства в DL,
иначе: АХ = число секторов в одном кластере; ВХ - число свободных кластеров; СХ = размер сектора (в байтах); DX = общее число кластеров на диске.

Используя информацию, возвращаемую функцией 36h, можно подсчитать как свободное пространство на диске — произведение АХ*ВХ*СХ, так и полный объем диска — произведение AX*CX*DX.
MS DOS предоставляет следующие возможности для манипулирования каталогами: создание и удаление каталога, получение информации о текущем каталоге и его смена.

Создание каталога

Вход: АН = 39h; DS:DX — ASCIZ-строка пути к создаваемому каталогу. Выход: АХ = не определен (CF ¦ 0); АХ - код ошибки (CF = 1): 3 — несуществующий путь; 5 — доступ запрещен.
Путь к каталогу должен содержать перечисление всех каталогов начиная от корневого на пути к создаваемому каталогу, при этом они, естественно, должны существовать. Последнее имя каталога — имя создаваемого каталога.

;prg07_15.asm - программа демонстрации создания каталога функцией 39h.
.data
dname db "c:\windows\my_diг".0
point_dname dd dname
.code
;.....создадим каталог в каталоге c:\windows.......-.......
Ids пате;формируем указатель на строку с именем нового каталога
movah,39h :номер функции DOS
int 21h
jc exit ;переход в случае ошибки

Удаление каталога

Вход: АН = 3Ah; DS:DX — ASCIZ-строка пути к удаляемому каталогу. Выход: CF = 0 — АХ = не определен; АХ = код ошибки (CF = 1): 3 — несуществующий путь; 5 — доступ запрещен; 10h — попытка удаления текущего каталога. Удаляемый каталог должен быть пустым.

:prg07_16.asm - программа демонстрации удаления каталога функцией ЗАп.
.data
dname db "c:\windows\my_diг",О point_dname dd dname
.code
:----удалим каталог my_dir в каталоге c:\windows---------
Ids dx.point_dname;формируем указатель на строку с именем нового каталога movah.3ah :номер функции DOS *
int 21h
jc exit ;переход в случае ошибки

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

MS DOS позволяет установить текущий каталог для того, чтобы не указывать полный путь для последующих операций с файлами. При необходимости можно получить полный путь к текущему каталогу в виде ASCIZ-строки. Вход: АН = 3Bh; DS:DX — указатель на буфер, содержащий полный путь от корневого каталога в виде ASCIZ-строки (до 64 байт). Выход: CF = 0 — АХ = не определен; CF = 1 — АХ = код ошибки: 03h — путь не найден.

:prg07_17.asm - программа демонстрации изменения текущего каталога функцией ЗВп.
.data
dname db "c:\windows",0
point_dname dd dname
.code
|.........
:.....изменим текущий каталог на каталог c:\windows--------
Ids dx.point_dname:формируем указатель на строку с именем нового каталога
movah.3bh :номер функции DOS
int21h
jc exit . :переход в случае ошибки
;.........

Получение текущего каталога

Вход: Ан = 47h;
DL = номер устройства (00h= текущее (по умолчанию),01h = А: и т. д.) DS:SI — указатель на 64-байтный буфер для записи полного пути от корневого каталога (ASCIZ-строка).
рыход: АХ = не определен или 0100h(CF=0);
АХ = код ошибки (CF=1): OFh — недопустимый дисковод.

:prg07_18.asm - программа демонстрации получения текущего каталога функцией 47h.
.data
dname db "e:\tools".О
point_dname dd dname
d_cur_name db 64 dup (20h).0
point_d_cur_name dd d_cur_name
.code
:.....изменим текущий каталог на каталог \tools ...........
Ids dx,point_dname;формируем указатель на строку с именем нового каталога
movah,3bh ;номер функции DOS
int 21h
jc exit :переход в случае ошибки
:----получим текущий каталог......-----.....
Ids si,point_d_cur_name ;формируем указатель на строку с именем нового каталога
mov ah.47h ;номер функции DOS
int 21h
jc exit :переход в случае ошибки

Возвращаемый путь не содержит имени диска и первого символа \.
Последняя проблема, на которой мы остановимся в этом разделе, — проблема поиска файлов. Для поиска в каталогах используется пара функций 4eh и 4fh. В имени искомого файла можно указывать символы шаблона * и ?. Совместное использование функций 4eh и 4fh подчинено следующему алгоритму. Первой вызывается функция 4eh. В качестве параметров ей передаются адрес ASCIZ-строки с путем к искомому файлу и комбинация его атрибутов. Имя файла может быть задано в виде шаблона. В случае успеха (cf=0), то есть при обнаружении первого подходящего шаблону файла, данная функция помещает его имя и расширение в область DTA со смещением leh от ее начала (см. таблицу ниже). Далее можно либо открыть файл, либо продолжить поиск, но уже функцией 4fh. При работе с шаблоном функцию 4fh можно вызывать циклически, до тех пор пока в процессе перебора не будут просмотрены имена всех подходящих файлов. Об этом можно узнать по состоянию флага cf, которое должно стать равным 1 в случае, когда файлов, удовлетворяющих шаблону, в данном каталоге больше нет.

Поиск первого соответствующего шаблону файла

Вход: АН = 4Eh; СХ = атрибуты файла (биты 0 и 5 игнорируются); DS:DX — ASCIZ-имя файла (возможно, с путем к нему и символами шаблона * и ?).
Выход: если CF - 0, то в DTA возвращается блок данных для первого найденно го файла (см. ниже). Если CF - 0, то в АХ - код ошибки: 2 — файл не най" ден; 3 — несуществующий путь; 12h — больше файлов в каталоге нет Область DTA (Data Transfer Area) располагается в префиксе программист
сегмента со смещением 80h от его начала и занимает 128 байт. При успешном
окончании поиска функция 4Eh (и 4Fh тоже) помещает блок данных, имеющий
приведенный ниже формат.

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

После анализа данной области в программе принимается решение об окончании или продолжении поиска.
В качестве шаблона можно задать символы *.*, тогда мы сможем получить имена и проанализировать все файлы в текущем каталоге. Это может понадобиться при программировании операции перемещения или копирования каталога.

Найти следующий соответствующий шаблону файл

Вход: АН = 4Fh; в области DTA должен содержаться блок данных, заполненный единственным вызовом 4eh в начале поиска.
Выход: если CF = 0 — успех; если CF = 1 — в АХ = код ошибки: 12h — больше файлов в каталоге нет.
Для работы с DTA в системе MS DOS существуют две функции lah и 2fh. После старта программы текущей DTA является область по адресу PSP:OO8Oh. Мы уже имели с ней дело, когда рассматривали проблему обработки содержимого командной строки.

Получить адрес области DTA

Для выполнения работы, связанной с файлами, MS DOS предоставляет возможность установить свою область DTA.
Вход: АН = 2Fh.
Выход: ES:BX — адрес области, которую впоследствии функцией lah можно сделать текущей областью DTA для последующих операций ввода-вывода.
установить текущую область DTA
Вход: АН - 1Ah; DS:DX — адрес области, которая будет областью DTA для последующих файловых операций.
Понятно, что даже если мы устанавливаем свою область DTA, все смещения и данные, формируемые функциями 4Eh 4Fh, остаются актуальными.

Работа с файлами в MS DOS (длинные имена)

Перечисленные выше функции работают в различных версиях «чистой» системы MS DOS, вплоть до версии 6.22 включительно. Операционные системы Windows 95/98/Mil также поддерживают свою версию MS DOS, которая имеет номер 7.0. Операционная система Windows 95/98/Mil организует для программ MS DOS специальную среду для работы, называемую сеансом DOS. Система MS DOS 7.0, будучи созданной для работы в среде Windows 95/98/Mil, имеет в своем составе средства для работы с файловой системой Windows. Эта файловая система, как известно, отличается тем, что полное имя файла может достигать длины 255 символов. MS DOS 7.0 также умеет работать с длинными именами файлов. В данном разделе мы рассмотрим предназначенные для работы с файловой системой Windows средства среды MS DOS, работающей под управлением
Windows.
Определить факт того, в какой системе работает программа, можно по результатам работы функций 30h — получить версию DOS (прерывания 21п) и 4аЗЗп (прерывания 2fh).
Вход: АН = 30h; AL = определяет значение, возвращаемое в ВН: 00h— OEM-номер (как для DOS 2.0-4.0x);01h — номер версии.
Выход: AL = главный номер версии; АН - младший номер версии; BL:CX = 24-битный серийный номер пользователя (необязательно).

:prg07_19.a$m - программа демонстрации определения факта того. ;в какой системе работает программа.
.code
;.....определим номер версии ОС MS DOS
mov al .00
movah.30h ;номер функции DOS
int21h ;А1-главный номер версии. АН-младший номер версии
]с exit ;переход в случае ошибки

В регистрах AL и АН возвращаются главный и младший номера версии MS DOS. При функционировании под Windows эти номера равны 07h и Oah соответственно. Задание значения AL = 01 дает такой же эффект.

:prg07_20.asm - программа демонстрации определения факта работы в среде MS DOS 7.0.
.code
:-----определить факт работы в среде MS DOS 7.0
movax.4a33h ;номер функции DOS int 2fh
cmp ax, 0
jneexit ;переход, если работа не в среде MS DOS 7.0

Последняя функция возвращает 0 для MS DOS версии 7.0 и выше. Установить факт того, что система поддерживает длинные имена файлов, можно вызовом функции 71a0h прерывания 21п — получить информацию о томе. Если она возвращает ошибку (CF = 1), то текущая файловая система не поддерживает длинных имен файлов. Для вызова этой функции необходимо указать корневой каталог тома, о котором необходимо получить информацию. Вход: АХ = 71A0h; DS:SI — ASCIZ-имя корневого каталога тома, о котором необходимо получить информацию (например, "С:\"); ES:DI — буфер для имени файловой системы; СХ ¦= размер буфера, адрес которого задан в ES: DI (32 байта).
Выход: CF = 0 в случае успеха, следующие регистры установлены: ВХ = флаги файловой системы: 0 — при поиске учитывать регистр букв в именах файлов; 1 — сохранять регистр букв в элементах каталога; 2 — использование символов Unicode в именах каталогов и файлов; 3-13 — резерв (0); 14 — поддержка DOS-функций для длинных имен файлов; 15 — сжатый том; СХ = максимальная длина файловых имен (обычно 255); DX = максимальная длина пути (обычно 260); ES: DI — в буфере по этому адресу ASCIZ-имя файловой системы, например "FAT", "FAT32", "NTFS", "CDFS"; CF = 1 в случае неудачи, при этом АХ = код ошибки или AX=7100h, если функция не поддерживается.
В Windows 95/98 появились дополнительные возможности как самой файловой системы, так и средств по ее управлению. Основное нововведение — поддержка длинных имен файлов. Основа файловой системы та же — таблица разрешения файлов FAT, но любой файл в этой системе имеет два имени — длинное имя и его псевдоним, который соответствует формату 8.3. Данный псевдоним создается системой Windows 95/98 автоматически.
Нужно правильно понимать различие в способах использования длинных имен файлов в приложениях MS DOS и Win32. Приложения MS DOS получают до-ступ к длинным именам файлов с помощью дополнительных функций прерывания 21h. Приложения Windows используют для этого соответствующие функции API.
Длинное имя файла представляет собой ASCIIZ-строку длиной до 255 символов. Система формирует псевдоним для этого имени форматом 8.3 в соответствии со следующим правилом: берутся первые 6 символов длинного имени, после них добавляется символ тильда (-), за тильдой ставится некий порядковый номер. Для первого имени формата 8.3 это 1. Если такой псевдоним уже существует, то порядковый номер очередного псевдонима будет на 1 больше. Расширение псевдонима формируется из первых трех символов расширения длинного имени (если оно существует). Если похожих имен много, то номер в псевдониме может быть двузначным, при этом первая (символьная) часть псевдонима сокращается
до 5 символов, и т. д.
Рассмотренные выше функции MS DOS для работы с файлами и каталогами не поддерживают длинных имен. Для этого система Windows 95/98 предоставляет приложениям MS DOS аналогичные функции, но имеющие другие номера. Впрочем, при внимательном рассмотрении большинства из этих номеров, видно, какой из старых функций они соответствуют. Новые номера состоят из четырех цифр: первые две — 071h, последние две — номер старой функции. Для некоторых функций существуют особенности в их работе. Так, для поиска файлов по-прежнему используются две функции (по новой нумерации — 714eh и 714fh) прерывания 21h. Новые функции теперь возвращают информацию о файлах через специальную структуру WI N32_F INDDATA, адрес которой возвращается в качестве
результата их работы.
При работе с функциями, поддерживающими длинные имена файлов, используются еще две структуры: by handle_f 11 e_information и filetime. Назначение структуры by_hand1e_file_iinformation и работу с ней мы рассмотрим при обсуждении функции MS DOS 71a6h. Структура filetime содержит 64-разрядное значение, которое определяет число 100-наносекундных интервалов, прошедших с 12:00 утра 1 января 1901 года.

FILETIME Struc
DwLowDateTime dd ? :младшие 32 бита значения времени DwHighDateTime dd ? хтаршие 32 бита значения времени FILETIME ends

Теперь приведем перечень функций прерывания 21h, работающих с файлами, которые имеют длинные имена. Для удобства дальнейшего рассмотрения в следующей таблице приведены соответствующие функции API WIN32 и «старые» функции прерывания 21h.

Новая функция int 21h | Старая функция int 21h | Назначение | Функция API Win32
5704h |  | Получить дату и время последнего доступа | GetFileTime
5705h |  | Установить дату и время | SetFileTime
 |  | последнего доступа |
5706h |  | Получить дату и время создания | GetFileTime
5707h |  | Установить дату и время создания | SetFileTime
7139h | 39h | Создать каталог | CreateDirectory
713Ah | 3ah | Удалить каталог | RemoveDirectory
713Bh | 3bh | Изменить текущий каталог | SetCurrentDi rectory
7141h | 41h | Удалить файл | DeleteFile
7143h | 43h | Получить или установить | GetFileAttributes,
 |  | атрибуты файла | SetFileAttributes
7147h | 47h | Получить текущий каталог | GetCurrentDirectory
714Eh | 4Eh | Найти первый файл | FindFirstFile
714Fh | 4Fh | Найти следующий файл | FindNextFile
7156h | 56h | Переименовать файл | MoveFile
7160h |  | Получить полный путь | GetFullPathName
7160h |  | Получить полный путь | GetShortPathName
 |  | с-краткими именами |
7160h |  | Получить полный путь | Отсутствует
 |  | с длинными именами |
716Ch | 3ch, 3dh, 5bh | Создать или открыть файл | CreateFile, OpenFile
71A0h |  | Получить информацию о томе | GetVolumelnfonnati on
71Alh |  | Завершить поиск | FindClose
71A6h |  | Получить информацию о файле | GetFi1 elnformationByHandle
 |  | по описателю |
71A7h |  | Преобразовать время файла | Fi1eTi meToDOSDateTime
 |  | в DOS-время |
71A7h |  | Преобразовать DOS-время во время файла | 00SDateTi meToFi1eTi me
71A8h |  | Создать псевдоним | Отсутствует
71A9h |  | Создать или открыть файл | Отсутствует
 |  | на сервере |
71AAh |  | Провести подмену | Отсутствует
71AAh |  | Отменить подмену | Отсутствует
71AAh |  | Получить информацию | Отсутствует
 |  | о подмене |

Рассмотрим наиболее интересные в контексте нашего изложения функции этой таблицы. Информацию по остальным функциям можно получить, в частности, из MSDN. Порядок рассмотрения будет от простых функций к сложным, так как это было в предыдущем разделе, посвященном функциям работы с файлами ДО DOS, имеющими короткие имена.

Создание, открытие, закрытие и удаление файла

функции MS DOS, поддерживающие длинные имена файлов, имеют номера из четырех цифр — первые две равны 71h, последние две соответствуют номеру аналогичной старой функции MS DOS. В программах старые и новые функции применяются вместе по принципу: там, где функция должна работать непосредственно с длинными именами файлов и каталогов, применяются новые функции; там, где функция работает с дескриптором файла, используются старые функции. Новые функции также используются для реализации новых возможностей по работе с файловой системой.

Открытие или создание файла

Для создания или открытия файла с длинным именем используется функция 716Ch (создать или открыть файл). Эта функция аналогична функции 6ch, которая появилась в последних версиях MS DOS (DOS 4.0+). Мы уже обсуждали ее в разделе, посвященном функциям работы с файлами с короткими именами.
Вход: АХ = 716Ch; BX = режимы доступа и флаги:
режим доступа: 0000h — файл только для чтения; 0001h — файл только для записи; 0002h — файл для чтения и записи; 0003h — резерв; 0004п — открыть файл для чтения без изменения даты последнего доступа к файлу; режим разделения: 0000h — режим эмуляции — файл можно открывать любой программе любое количество раз; 00Wh — файл открыт в монопольном режиме доступа; 0020h — файл открыт в монопольном режиме доступа по записи; 0030h — файл открыт в монопольном режиме доступа по чтению; 0040И — открыть файл, разрешая другим процессам доступ по чтению-записи, но с запретом режима эмуляции; флаги: 0080h — дочерний процесс не наследует дескриптор файла, его при необходимости нужно передавать явно; Ol00h — не использовать буферизацию или кэширование средствами ОС, операции чтения-записи выполняются напрямую с диском в соответствии с текущим положением файлового указателя; 0200h — файл нельзя сжимать; 0400h — содержимое регистра 01 следует использовать как порядковый номер в псевдониме файла; 2000h — не вызвать обработчик критической ошибки (int 24h), MS DOS вернет программе код ошибки; 4000h — после каждой операции записи MS DOS будет отправлять данные на диск без их кэширования; СХ - атрибуты создаваемого (и только) файла: 0000h — файл доступен по записи и чтению; 0001h — файл доступен по чтению; 0002h — скрытый файл; 0004п — системный файл; 0008h — метка тома; 0020h — архивный
файл;
DX ~ действия, если файл существует или не существует, значения битов: 00lOh — вернуть ошибку, если файл существует, иначе создать файл; 0001h — открыть файл, если он существует, иначе вернуть ошибку;

0002h — открыть файл без сохранения существующего, иначе вернуть ошибку (если файл не существует); DS:SI — ASCIZ-имя файла;
DI — порядковый номер, который добавляется к концу имени в псевдониме файла (для этого должен быть задан флаг 0400h в регистре ВХ) Номер будет десятичным письмом, то есть если DI=0010h, то конец псевдонима -----16.
Выход: CF=0 — успешное выполнение функции: АХ = дескриптор файла, СХ = состояние: 1 — файл открыт; 2 — файл создан и открыт; 2 — файл открыт без сохранения содержимого существующего файла; CF=1: AX = код ошибки.
После того как файл открыт или создан функцией 716ch, с ним можно работать, используя старые функции чтения-записи и позиционирования. Следующий фрагмент программы показывает вариант применения функции 716Ch.
Закрытие файла производится функцией 3Eh, которая использовалась для файловых функций MS DOS с короткими именами.

Удаление файла

Удаление файлов, имеющих длинные имена, производится функцией 7141h прерывания 21п. Имя файла может быть задано с использованием символов шаблона * и ?, при этом в результате работы функции будут удалены все файлы, чье имя удовлетворяет заданному шаблону.

Получение и изменение атрибутов файла

Получить дату и время создания файла

Вход: АХ = 5706h; ВХ - дескриптор файла. ; . М
Выход: CF = 0 — успешное выполнение функции: СХ = биты установлены следующим образом: 0..4 - секунды, деленные на 2; 5..10 = минуты 0..59; 11..15 -часы; DX - биты установлены следующим образом: 0..4 = день месяца в диапазоне 1..31; 5..8 = месяц в диапазоне 1..12; 9..15 = число лет начиная с 1980 года; SI - двоичное значение количества 10-миллисекундных интервалов, добавляемых ко времени MS DOS в диапазоне 0..199; CF = 1: АХ = код ошибки.
Данная функция реализована в полном объеме.

Установить дату последней модификации файла

Вход: АХ = 5705И; ВХ = дескриптор файла; СХ = 0000h; DX - биты установлены следующим образом: 0..4 - день месяца в диапазоне 1..31; 5..8 = месяц в диапазоне 1..12; 9..15- число лет, начиная с 1980 года.
Выход: CF = 0 — успешное выполнение функции; CF = 1: АХ код ошибки.

Аналогично функции 5704h данная функция позволяет установить только дату создания файла.

Установить дату и время создания или последней модификации файла

Вход: АХ = 5707h; ВХ = дескриптор файла; СХ - биты установлены следующим образом: 0..4 - секунды, деленные на 2; 5..10 = минуты 0..59; 11..15 - часы; DX " биты установлены следующим образом: 0..4 = день месяца в диапазоне 1..31; 5..8 = месяц в диапазоне 1..12; 9..15 = число лет начиная с
1980 года;
SI - двоичное значение количества 10-миллисекундных интервалов, добавляемых к времени MS DOS в диапазоне 0..199.
Выход: CF = 0 — успешное выполнение функции; CF = 1: АХ - код ошибки.

Данная функция реализована в полном объеме.
Кроме дополнительных функций для работы с различными временными характеристиками файла Windows-версия MS DOS содержит две функции для преобразования форматов времени. Дело в том, что Windows работает со временем в 64 разрядном формате. При этом точкой отсчета является 00 часов 00 минут 1 января 1601 года. Значение времени содержит число 100-наносекундных интервалов, прошедших с этой даты. По расчетам разработчиков этого значения должно хватить на 400 лет. Для того чтобы манипулировать этим 64-разрядным «средневековьем» с целью представления его в виде, воспринимаемом человеком (DOS-время), введена функция 71a7h.
Вход: АХ = 71a7h; BL = 0 — преобразовать 64-разрядное время в DOS-время; DS:SI = указатель на экземпляр структуры FILETIME, содержащей 64-битное значение времени.
Выход: CF = 0 — успешное выполнение функции, при этом регистры устанавливаются следующим образом: ВН = число 10-миллисекундных интервалов, добавляемых к времени MS DOS (значение в диапазоне 0..199); СХ - время в упакованном формате со значением бит: 0..4 — секунды, деленные на 2; 5..10 — минуты в диапазоне 0..59; 0..4 — часы в диапазоне 0..23; DX = дата в упакованном формате со значением бит: 0..4 — день месяца в диапазоне 1.31; 5..8 — месяц в диапазоне 1..12; 9..15 — число лет начиная с 1980 года (для получения истинного значения прибавьте 1980); CF = 1: АХ - код ошибки. (Структура filetime описывается в программе следующим образом:

FILETIME struc
DwLowOateTime dd ? :младшие 32 бита значения времени
DwHighDateTime dd ? :старшие 32 бита значения времени
FILETIME ends

Вход: АХ = 71a7h; BL - 1 — преобразовать DOS-время в 64-разрядное время; ВН = число 10-миллисекундных интервалов, добавляемых ко времени MS DOS (значение в диапазоне 0..199); СХ = время в упакованном формате со значением бит: 0..4 — секунды, деленные на 2; 5..10 — минуты в диапазоне 0..59; 0..4 — часы в диапазоне 0..23; DX - дата в упакованном формате со значением бит: 0..4 — день месяца в диапазоне 1..31; 5..8 — месяц в диа пазоне 1..12; 9..15 — число лет начиная с 1980 года (для получения истинного значения прибавьте 1980) DS:SI = указатель на экземпляр структуры FILETIME, в которой вернется 64-битное значение времени.
Выход: CF=O — успешное выполнение функции, при этом в области памяти, адресуемой DS:SI, возвращается 64-битное значение времени; CF=1: AX - код ошибки.

Получить атрибуты файла

Вход: АХ ¦ 7143h; BX = действие:
0 -- получить атрибуты, на выходе СХ = атрибуты файла: 0000h — файл доступен по записи и чтению; 0001h — файл доступен по чтению; 0002h — скрытый файл; 0004И — системный файл; 0008h — метка тома; 00lOh — каталог; 0020h — архивный файл;
2 — получить размер сжатого файла — на выходе DX:AX = размер сжатого файла в байтах на диске;
4 — получить дату и время последней записи — на выходе; СХ = время в формате: 0..4 = секунды, деленные на 2; 5.. 10 = минуты (0..59); 11..15 = часы(0..23);
DI дата в формате: 0..4 = день месяца (1..31); 5..8 = месяц (1..12); 9..15 = число лет с 1980 года;
6 — получить дату последнего доступа — на выходе: DI = дата в формате (см. ВХ = 4);
8 — получить дату и время создания — на выходе СХ - время в формате, DI - дата в формате (см. ВХ « 4), SI = двоичное значение количества 10-миллисекундных интервалов, добавляемых ко времени MS DOS в диапазоне 0..199;
DS:DX — ASCIZ-строка с именем (путем) файла.
Выход: CF = 0 в случае успеха, информация в регистрах определяется значением ВХ на входе (см. выше):
АХ = код ошибки (CF = 1): 1 — неверное значение в AL; 2 — файл не найден; 3 — несуществующий путь; 5 — доступ запрещен.

Установить атрибуты файла

Вход: АХ = 7143h; ВХ = действие:
1 — установить атрибуты на входе СХ = атрибуты файла: 0000h — файл доступен по записи и чтению; 0001h — файл доступен по чтению; 0002h — скрытый файл; 0004h — системный файл; 0020h — архивный файл; 3 — установить дату и время последней записи: СХ = время в формате: 0..4 " секунды, деленные на 2; 5..10 = минуты (0..59); П..15 - часы (0..23); 01 = дата в формате: 0..4 = день месяца (1..31); 5..8 = месяц (Т.. 12); 9..15 =" число лет с 1980 года;
5 — установить дату последнего доступа (см. ВХ = 3); 7 — установить дату и время создания: СХ = время в формате (см. ВХ - 3), DI = дата в формате (см. ВХ = 3), SI = двоичное значение количества 10-миллисекундных интервалов, добавляемых ко времени MS DOS в диапазоне 0..199; DS:DX — ASCIZ-строка с именем (путем) файла.
Выход: CF = о — СХ = слово атрибутов файла; CF=1 — АХ=код ошибки: 1 — неверное значение в AL; 2 — файл не найден; 3 — несуществующий путь; 5 — до-ступ запрещен.

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

Вход: АН = 7156h; DS:DX — ASCIZ-имя существующего файла; ES:O1
имя нового файла; CL - маска атрибутов. Выход: CF = 0 — при успешном переименовании; CF = 1 — АХ = код ошибки: 2 —
файл не найден; 3 — несуществующий путь; 5 — доступ запрещен;
устройства для старого и нового файлов не совпадают.

Работа с дисками, каталогами и организация поиска файлов

Получить информацию о томе

Вход: АН = 71A0h; DS:DX — адрес ASCIZ-строки с именем корневого каталога диска, о котором необходимо получить информацию (С:\); ES:DI — адрес буфера, в который будет помещена ASCIZ-строка с именем файловой системы; СХ = размер буфера, в который будет помещена ASCIZ-строка с именем файловой системы.
Выход: CF = 0 — успешное выполнение, при этом в буфер по адресу в ES-.DI помешается ASCIZ-строка с именем файловой системы и устанавливаются следующие регистры: ВХ =* флаги файловой системы (комбинация значений: 0001 — при поиске учитывается регистр букв в именах файлов; 0002 — файловая система сохраняет регистр букв в элементах каталога; 0004 — использование символов Unicode в именах каталогов и файлов; 4000 — файловая система поддерживает длинные имена файлов и функции для работы с ними; 8000 — том сжат); СХ = максимально допустимая длина имени файла на данном томе без последнего нулевого символа (до 255); DX = максимально допустимая длина пути для данного тома, включая последний нулевой символ (до 260); ;1. _ .,

Создание каталога

Вход: АН = 7139h; DS:DX — адрес строки с ASCIZ-именем существующего файла. Выход: CF = 0 — при успешном переименовании; CF = 1 — АХ = код ошибки: 3 — несуществующий путь; 5 — доступ запрещен.
Удаление каталога
Удаляемый каталог должен быть пуст.
Вход: АН = 713Ah; DS:DX — ASCIZ-строка пути к удаляемому каталогу. Выход: CF = 0 — АХ = не определен; CF = 1 — АХ = код ошибки: 3 — несуществующий путь; 5 — доступ запрещен; 10h — попытка удаления текущего каталога.

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

Понятие текущего каталога аналогично тому, что приводилось выше при рассмотрении функций для работы с файлами, имеющими короткие имена.
АН = 713Bh; DS:DX — указатель на буфер, содержащий полный путь от кор. невого каталога в виде ASCIZ-строки и в качестве последнего элемента включающий имя нового текущего каталога (естественно, что допустимы длинные имена с ограничениями по максимальной длине (см. функцию 71a0h)). Выход: CF = 0 — АХ - не определен; CF=0 — АХ = код ошибки: 03h — путь не найден

Получение текущего каталога

Вход: АН = 7147h; DL - номер устройства (00h = текущее (заданное по умолчанию), 01h = А: И т. д.); DS:SI — указатель на буфер для записи полного пути от корневого к текущему каталогу (длина буфера должна быть не менее длины, возвращаемой в регистре DX функцией 71a0h). Выход: CF = 0 — успешное выполнение функции, в результате чего полный путь от корневого каталога в виде ASCIZ-строки без имени диска и символа \ записывается в буфер, адрес которого указан в DS:SI; АХ - код ошибки (CF = 1): OFh — недопустимый дисковод. Среди новых функций, работающих в том числе с длинными именами файлов, существует функция 7160h, позволяющая получить полные пути для указанных файлов или относительных путей: получить полный путь (сх = 0), получить полный путь с краткими именами (сх = 1), получить полный путь с длинными именами (сх = 2).

Получить полный путь

Вход: АН = 7160h; CL = 0; СН — содержимое результата (СН = 80 — получить имя диска; СН = 0 — получить полный путь); DS:SI — адрес ASCIIZ-строки с именем файла или каталога, для которых необходимо получить полный путь. Допускаются оба типа имен — длинные и короткие; ES:DI — адрес строки, в которую необходимо записать полный путь. Размер буфера должен быть достаточным для размещения пути максимальной длины (функция 71a0h).
Выход: CF = 0 — успешное выполнение функции, в результате чего полный путь от корневого каталога в виде ASCIZ-строки записывается в буфер, адрес которого указан в ES:DI; CF = 1 — АХ = код ошибки.

;prg07_24.asm - программа демонстрации применения функции 7160h (CL=0) прерывания 21h для получения полного пути.
.data
filename db 'my_file with long name.txt'.O
point_fname dd filename
PathFull db 260 dup (0)
point Path dd PalhFull
.code
Ids si .pointfname:формируем указатель на имя файла
les di,point_Path Нормируем указатель на буфер для полного пути

mcvax.7l60h ;номер функции DOS
movch,80h :CH=80h - получить имя : диска; СН=0 - получить полный путь
movCL.O ;получить полный путь
int 21h
jc exit

Данная функция работает очень принмитивно — при указании имени файла или относительного пути (с символами *«.» и «..») она не проверяет его существование, а лишь добавляет к нему имя тсекущего диска и каталога. Поэтому при использовании этой функции требуютсяя другие средства, позволяющие контролировать реальное наличие файла или шути на диске.

Получить полный путь с краткими именами (в формате 8.3)

Вход: АН = 7160h; CL = 1; СН — содержимоое результата (СН = 80 — получить имя диска; СН = 0 — получить полный путь); DS:SI — адрес ASCIIZ-строки с именем файла или каталога, для ксоторых необходимо получить путь в короткой форме. Допускаются оба . типа имен — длинные и короткие: ES:DI — адрес строки, в которую необходцимо записать полный путь. Размер буфера должен быть достаточным длят размещения пути максимальной длины
(функция 71a0h).
Выход: CF = 0 — успешное выполнение функции, в результате чего полный путь от корневого каталога в виде /ASCIZ-строки записывается в буфер, адрес которого указан в ES: DI; CF ' = 1 — АХ = код ошибки.

:prg07_25.asm - программа демонстрации применения функции 7160h (CL=1) прерывания 21h для получения полного путии с краткими именами (в формате 8.3)
filename db 'my_file with lorg name.:.txt' .0
point_fname dd filename
PathFUll db 260 аир (О)
pointjath dd PathFull
* Ids si .pointjfname формирует указатель на имя файла lesdi.point_Path сформируем учазатель на буфер для полного пути movax.7160h :номер функции DOS
movch.80h ;CH»80h - попучить « имя диска; СН=0 - получить полный путь
movCL.l :получить пспный пууть с краткими именами

На выходе функция формирует стрроку, содержащую полный путь, причем все I Длинные компоненты этого пути замееняются их краткими псевдонимами, удовлетворяющими схеме 8.3. Данный вариант функции (при CL = 1) в отличие от ее Предыдущего варианта, производит прроверку наличия файла или пути.

Получить полный путь с длинными именами

Вход: АН = 7160h; CL = 2; СН — содержимое результата (СН = 80 — получить имя лиска; СН = 0 — получить полный путь); DS:SI — адрес ASCIIZ-строки с именем файла или каталога, для которых необходимо получить путь в длинной форме. Допускаются оба типа имен — длинные и короткие; ES:DI — адрес строки, в которую необходимо записать полный путь. Размер буфера должен быть достаточным для размещения пути максимальной длины (функция 71a0h).
Выход: CF = 0 — успешное выполнение функции, в результате чего полный путь от корневого каталога в виде ASCIZ-строки записывается в буфер, адрес которого указан в ES:DI; CF = 1 — АХ = код ошибки.

Получить информацию о файле по описателю

Вход: АН = 71A6h; флаг cf = 1; ВХ = дескриптор файла; DS:DX — адрес структуры
BYHANDLEFILEINFORMATION.
Выход: CF = 0 — успешное выполнение функции; CF = 1 — АХ = код ошибки. Формат структуры BYHANDLEFILEINFORMATION приведен ниже.

Y_HANDLE_FILE_INFORMATION StuiC
"DwFiieAttributes dd ?
FtCreationTimedd 2 dup(?)
FtLastAccessTime dd 2 dup(?)
FtLastWriteTime dd 2 dup(?)
DwVolumeSeria 1 Number dd ?
NFileSizeHigh dd ?
NfileSizeLow dd ?
NnunberOfLinksdd ?
NFilelndexHighdd ?
NFilelndexLow dd ?
BY_HANDLE_FILE_INFORMATION ends

Поля этой структуры описаны в следующей таблице.

Поле | Описание
dwFileAttributes | Атрибуты файла. Этот элемент может быть комбинацией следующих значений: FI LE_ATTR IBUTENORMAL (00000000H) - файл доступен по чтению и записи; этот атрибут нельзя комбинировать с другими; FILE_ATTRIBUTE_READONLY (00000001Н) - файл только для чтения; FILE_ATTRIBUTE_HIDDEN (00000002H) - скрытый файл; FILE_ATTRIBUTE_SYSTEM (00000004H) - системный файл; FI LE_ATTR IBUTEDI RECTORY (00000010H) - каталог; FILE ATTRIBUTE ARCHIVE (00000020H) - архивный файл
CreationTime | Время создания файла в 64-разрядном формате
ftLastAccessTime | Время последнего доступа к файлу в 64-разрядном формате
ftLastWriteTime | Время последней записи в файл в 64-разрядном формате
dwVolumeSeri alNumber | Серийный помер тома, на котором находится файл
NFileSizeHigh | Старшее слово значения, определяющего размер файла
NFiieSizeLow | Младшее слово значения, определяющего размер файла
NNumberOfLinks | Число связей с данным файлом. В файловых системах FAT и HPFS этот элемент всегда равен 1. В файловой системе NTFS число связей может превышать 1
NFilelndexHigh | Старшее слово уникального дескриптора, связанного с файлом
NFilelndexLow | Младшее слово уникального дескриптора, связанного с файлом. Файл однозначно определяется дескриптором и серийным номером тома

:prg07_26.asm - программа демонстрации применения функции 71A6h прерывания 21п :для получения информации о файле по описателю.
BY_HANDLE_FILE_INFORMATION struc "DwFileAttributes dd ? FtCreationTime dd 2 dup(?) FtLastAccessTime dd 2 dup(?) FtLastWriteTime dd 2 dup(?) DwVolumeSerial Number dd ? NFileSizeHigh dd ? NfileSizeLow dd ? NnumberOfLinks dd ? NFilelndexHigh dd ? NFilelndexLow dd ? BY_HANDLE_F1LE_INFORMATION ends
.data
file_info_ BY_HANDLE_FILE_INFORMATION <>
poir,t_find_ dd file_info_
filename db 'my_file with long name.txt'.O
point_fname dd filename
handle dw 0
:.........
.code

.файл, о котором будем получать информацию

mov bx.0100h+0400h;не использовать буферизацию
содержимое 01 в псевдоним
movdx.l юткрыть файл, если он существует, иначе вернуть ошибку Ids si .point_fname-.формируем указатель на имя файла
mov di.7 repeat:mov ax.716ch
xor ex.ex
int21h
jnc ml
mov dx.10h
jmp repeat ml: mov handle.ax
действия при успешном открытии файла:
добавить в конец псевдонима символ 7 ; номер функции DOS атрибуты файла - обычный файл - доступ для чтения-записи
открываем файл
.если файл существовал, то переход
;создать файл
:переход - повторим открытие файла
:сохраним дескриптор файла
.получаем информацию о файле mov bx. handle
stc ;это обязательно
Idsdx.point_find_;формируем указатель на структуру BY_HANDLE_FILE_INFORMATION

Создать псевдоним

Функция 71A8h предназначена для генерации короткого (в формате 8.3) имени для заданного файла с длинным именем.
Вход: АН = 71A8h; DS:SI — адрес строки (с нулевым символом в конце), содержащей длинное имя нужного файла без указания пути; ES:DI — адрес буфера, в котором возвращается псевдоним; DH — формат псевдонима (0—11 -символьное имя элемента каталога; 1 — имя файла в формате 8.3); DL — набор символов для длинного имени и псевдонима. Это значение — упакованная величина в формате: биты 0..3 — набор символов в prpprg0дном имени файла (0 — Windows ANSI; I — OEM; 2 — Unicode); биты 4..7 — набор символов в создаваемом коротком имени (0 — Windows ANSI; I - OEM; 2 - Unicode).
Выход: CF ¦ 0 — успешное выполнение функции; CF = 1 — АХ = код ошибки.

:prgO7_27.asm - программа демонстрации применения функции 71A8h прерывания 21h ;для создания псевдонима.
.'data'"
fi1enamejong db 'my_file with long name.txt'.0
point_fname_long dd fi1enamejong
filename_short db 11 dup (20h)
point_fname_short dd filename short
handle dw 0
:.........
.code
mov bx.0100h+0400h;He использовать буферизацию + содержимое DI в псевдоним
movdx.l :открыть файл, если он существует, иначе вернуть ошибку
Ids si,point fname_long ;формируем указатель на имя файла
movdi.7 :добавить в конец псевдонима символ 7 repeat:mov ax,716ch ;номер функции DOS
хогсх.сх атрибуты файла - обычный файл - доступ для чтения-записи
int 21h открываем файл
jnc ml :если файл существовал, то переход
movdx.lOh :создать файл
jmp repeat ;переход - повторим открытие файла ml: mov handle.ax :сохранин дескриптор файла создадим псевдоним
Ids si.point_fname_long
les di,point_fname_short
mov dh. 1:prg07_27.asm - программа демонстрации применения функции 71A8h прерывания 21h ;для создания псевдонима.
mov dl. 0
mov ax,7la8h int 21h действия при успешном открытии файла

Действие данной функции несколько отличается от процесса формирования псевдонима файла операционной системой и заключается в том, что длинное имя попросту обрезается по границам 8.3. В этом несложно убедиться, проанализировав работу приведенной выше программы в отладчике.

Поиск файлов и каталогов

В Windows-версии MS DOS процесс поиска несколько отличается от рассмотре-ного выше. Для этого используются три функции и структура WIN32_FIND_DATA в памяти, в которой возвращается информация о файле. Для запуска процесса поиска вызывается функция 714eh — найти первый файл.
Вход: АН = 714eh; CL — атрибуты искомых файлов (0000h — файл доступен по записи и чтению; 0001h — файл доступен по чтению; 0002h — скрытый файл; 0004И — системный файл; 0008И — метка тома; 00lOh — каталог; 0020h — архивный файл); СН — дополнительные атрибуты искомых файлов (0000h — файл доступен по записи и чтению; 0001h — файл доступен по чтению; 0002h — скрытый файл; 0004h — системный файл; 0008h — метка тома; 00lOh — каталог; 0020h — архивный файл); DS:DX — адрес ASCIIZ-строки с именем искомого файла или каталога. Допускаются оба типа имен — длинные и короткие. В именах допустимы символы шаблона * и ?; ES-.DI — адрес структуры WI N32_F I ND_DATA, в которой будет возвращена ин-_ формация о файле; SI — формат, в котором возвращается дата и время
(О — дата и время возвращаются в 64-разрядном формате; 1 — дата и время возвращаются в формате MS DOS).
Выход: CF = 0 — успешное выполнение функции, в результате в регистрах АХ и СХ возвращается следующая информация: АХ = дескриптор, использующийся далее для процесса поиска; СХ = возможные значения: 0000 — все символы структуры WIN32_FIND_DATA, составляющие основное и альтернативное имя файла, успешно преобразованы из Unicode; 0001 — основное имя, возвращенное в структуре WIN32_FIND_DATA, содержит знаки подчеркивания на месте символов, не преобразованных из Unicode; 0002 — альтернативное имя, возвращенное в структуре WIN32_FIND_DATA, содержит знаки подчеркивания на месте символов, не преобразованных из Unicode; CF = 1 — АХ = код ошибки при неудачном выполнении функции. Вызов функции 714eh приводит к заполнению полей структуры WIN32_FIND_ DATA, после чего можно проанализировать ее поля. Основной интерес представляют поля основного (CfileName) и альтернативного (CaHernateFiieName) имен. Их можно анализировать на предмет удовлетворения условиям поиска. Если необходимо продолжить поиск, вызывается функция 714fh — найти следующий файл. Если же поиск считается удачным либо его необходимо прекратить, то вызывается функция 71alh — прекратить поиск. Ниже приведены порядок вызова функций 714fh и 71alh и формат структуры WIN32_FIND_DATA.
Вход: АН = 714fh; ВХ = дескриптор, полученный функцией 714eh; ES:DI — адрес структуры WIN32FINDDATA, в которой будет возвращена информация о файле; SI — формат, в котором возвращается дата и время (0 — дата и время возвращаются в 64-разрядном формате; 1 — дата и время возвращаются в формате MS DOS).
Выход: CF = 0 — успешное выполнение функции, в результате в регистрах АХ и СХ возвращается следующая информация: СХ = возможные значения: 0000 — все символы структуры WIN32FINDDATA, составляющие основное и альтернативное имя файла, успешно преобразованы из Unicode; 0001 — основное имя, возвращенное в структуре WIN32FINDDATA, содержит знаки подчеркивания на месте символов, не преобразованных из Unicode; 0002 — альтернативное имя, возвращенное в структуре WIN32FINDDATA, содержит знаки подчеркивания на месте символов, не преобразованных из Unicode; CF = 1 — АХ = код ошибки при неудачном выполнении функции. Функция 714eh в отличие от аналогичных функций «старой» MS DOS использует не область DTA, а некоторый блок в памяти. Этот блок важно своевременно освобождать, для этого и предназначена функция 71alh.
Вход: АН = 71Alh; ВХ = дескриптор, полученный функцией 714eh. Выход: CF = 0 — успешное выполнение функции; CF = 1 — АХ = код ошибки при неудачном выполнении функции.

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

WIN32_FIND_DATA struc
DwFlleAttributes dd ?
FtCreationTime dd 2 dup(?)
FtLastAccessTime dd 2 dup(?)
FtLastWriteTime dd 2 dup(?)
NFileSizeHigh dd ? .размер файла в байтах (старшее слово)
NFiieSizeLow dd ? ;размер файла в байтах (младшее слово)
DwReservedO dd 0 ;резерв
OwReservedl dd 0 ;резерв
CFileName db MAX_PATH dup(?)
CAlternateFileName db 14 dup(?) WIN32_FIND_DATA ends

Поля этой структуры описаны в следующей таблице.

dwFileAttributes | Атрибуты найденного файла (см. описание аналогичного элемента структуры BY HANDLE FILE INFORMATION)
ftCreationTime | Время создания файла в одном из двух форматов: MS DOS или в 64-разрядном, в зависимости от параметров, указанных при вызове функций 714eh (найти первый файл) и 714fh (найти следующий файл)
ftLastAccessTime | Время последнего доступа к файлу в одном из двух форматов: MS DOS или в 64-разрядном, в зависимости от параметров, указанных при вызове функций 714eh и 714fh
fUastWrUeTime | MS DOS или в 64-разрядном, в зависимости от параметров, указанных при вызове функций 714eh и 714fh
CFileName | ASCIIZ-строка, содержащая имя файла. Размер строки должен быть не менее 256 символов
cAlternateFileName | ASCIIZ-строка, содержащая альтернативное имя файла в стандартном формате 8.3. Если элемент cFileName содержит имя в формате 8.3 или файловая система не поддерживает альтернативные имена в формате 8.3, то элемент cAlternateFileName равен нулю

Приведем пример поиска файла по шаблону. Для этого предварительно создадим несколько файлов в соответствии с шаблоном file*.*. Среди этих файлов должен быть файл file_O5.txt. В отладчике проследим за тем, как изменяется
содержимое области памяти, отведенное для экземпляра структуры WIN32_FIND_ DATA. Выход из программы — при обнаружении файла file_5.txt

:prgO7_28.asm - программа демонстрации поиска файла по шаблону.

WIN32_FIND_DATA -uc
DwFlleAttributes dd ?
FtCreationTime dd 2 dup(?)
FtLastAccessTime dd 2 dup(?)
FtLastWriteTime dd 2 dup(?)
NFileSizeHigh dd ? .размер файла в байтах (старшее слово)
NFileSizeLow dd ? .размер файла в байтах (младшее слово)
DwReservedO dd 0 ;резерв
DwReservedl dd 0 :резерв
CFileName db 260 dup(?)
CAlternateFileName db 14 dup(?)
WIN32_FIND_DATA ends
.data
find_ WIN32_FIND_DATA <>
point_find_ dd find_
f_name_pattern db 'file_*.*'.O
point_f_name_pattern dd f_name_pattern
filename db 'file_05.txt',0 :искомый файл
1e*n_fi1ename=$-fi1ename
handle dw 0
movCL.O ;атрибуты искомого файла
movch.O ;дополнителные атрибуты для поиска
Ids dx,point_f_name_pattern ;формируем указатель на строку с шаблоном
les di.point_find_;формируем указатель на экземпляр структуры WIN32_FIND_DATA
movax.714eh :номер функции DOS
int 21h
jc exit
;в ах был возвращен дескриптор - если нужно, то его необходимо сохранить:

mov handle.ax
:проверяем, тот ли это файл: ml: mov ex.1en_fi1ename
lea di.find_.CfileName
lea si .filename repe empsb
jz exit ;продолжаем поиск - в Ьх дескриптор, полученный от 714eh:
mov bx.handle
les di.point_find_ :формируем указатель на экземпляр структуры WIN32_FIND_DATA
mov ax.714fh :номер функции DOS
хог si.si :формат даты
int 21h
jnc ml exit: завершить поиск
mov ax.71alh
mov bx.handle
int 21h

В отладчике хорошо видно, что выход из данной программы происходит в двух случаях:

  • когда файл найден, выход из программы производится в результате сравнения командой empsb (флаг ZF устанавливается в 1);
  • когда файлов, удовлетворяющих шаблону, нет, функция поиска 714еп и 714fh завершается неудачей (флаг CF устанавливается в 1).

В качестве шаблона можно задать символы *.*, тогда мы сможем получить имена и проанализировать все файлы в текущем каталоге. Это может понадобиться при программировании операции перемещения или копирования каталога.
Остальные функции работы с файлами, предназначенные для записи/чтения/позиционирования, остались прежними.


Работа с файлами в программах на ассемблере (Часть 2)

Продавай мобильный контент со своего сайта

Работа с файлами в программах на ассемблере (Часть 2)

  • Файловый ввод-вывод в Win32
    • Обработка ошибок
    • Создание, открытие, закрытие и удаление файла
    • Чтение, запись, позиционирование в файле
    • Получение и изменение атрибутов файла
    • Работа с дисками, каталогами и организация поиска файлов
    • Файлы, отображаемые в память

Файловый ввод-вывод в Win32

В этом разделе будут приведены минимальные сведения, необходимые для выполнения простых операций с файлами. В отличие от MS DOS среда Win32 способна поддерживать несколько файловых систем. Главные требования к этим системам — иерархичность и соблюдение определенных правил присвоения имен каталогам и файлам.
Перечислим функции API Win32, имеющие отношение к работе с файловой системой. Полное их описание можно получить в MSDN.

Функция | Назначение
AreFileApisANSI | Определение набора символов файла — ANSI или OEM
CancelIo | Отменить все ждущие обработки операции (I/O) ввода и вывода
CloseHandle | Закрыть открытый дескриптор файла
CopyFile, CopyFileEx CopyProgress Routine | Копирование cуществующего файла в новый Определенная приложением функция обратного вызова, используемая с функциями CopyFileEx и MoveFileWithProgress. Она вызывается, когда завершается часть операции копирования или пересылки
CreateDi rectory, CreateDirectoryEx | Создать каталог
CreateFile | Создать файл или объект специального типа
DefineDosDevice | Определить, переопределить или удалить имена устройства MS DOS
DeleteFile | Удалить файл
FindCiose | Закрыть указанный поисковый дескриптор (см. функции FindFirstFile и FindNextFile)
Fi ndCloseChange Noti fi cati on | Закрыть объект-уведомление об изменении файла
FindFi rstChange Noti fication | Создать объект-уведомление об изменении файла
FindFirstFile, FindFirstFileEx, FindNextFile | Поиск файлов
F1ndNextChange Noti fi cati on | Сброс объекта-уведомления в занятое состояние
FlushFileBuffers | Очистка буфера для указанного файла и запись всех буферизированных данных в файл
GetBinaryType | Определить, является ли файл исполняемым, и если это так, то для какой подсистемы — Win32, MS DOS, OS/2, POSIX и т. д.
GetCurrentDirectory | Получить текущий каталог
GetDiskFreeSpace, GetDiskFreeSpaceEx | Информация относительно указанного диска, включая количество свободного пространства на нем
GetDriveType | Определить тип диска — съемный, фиксированный, CD-ROM, электронный или сетевой
GetFileAttributes, GetFileAttributesEx | Получить атрибуты файла или каталога
GetFi1elnformati onByHandle | Найти информацию относительно указанного файла
GetFi1eSi ze, GetFi1eSi zeEx | Получить размер указанного файла
GetFileType | Получить тип указанного файла
GetFullPathName | Получить полный путь и имя для указанного файла
GetLogical Drives, GetLogi calDri veStri ngs | Определить доступные в настоящее время дисководы
GetLongPathName | Преобразовать указанный путь к его длинной форме
GetShortPathName | Получить псевдоним файла
GetTempFileName | Создать имя для временного файла
GetTempPath | Получить путь каталога для временных файлов
LockFile, LockFileEx | Блокировка файла

Далее на примерах конкретных программ разберемся с тем, как использовать в программах на ассемблере наиболее интересные и часто применяемые функции из перечисленных выше для работы с файлами API Win32. В целях экономии места все примеры реализованы в виде консольных приложений. Основное внимание уделено не полноте описания параметров для вызова той или иной функции и результатов ее работы (эту информацию можно найти в справочниках по функциям API), а деталям практической реализации файловых операций в программах на языке ассемблера. Для изучения подробностей работы функций API Win32 необходимо использовать какой-либо отладчик для Windows, напри-MepTD32.EXE.

Обработка ошибок

Прежде чем рассматривать функции API Win32, относящиеся к файловому вводу-выводу, отметим, как можно выяснить причину их ошибочного завершения. Для этого Windows предоставляет функцию GetLastError.

DWORD GetLastError(void):

Для вызова функции GetLastError не нужно передавать никаких параметров. Эту функцию необходимо вызывать сразу после функции API Win32, успешность работы которой мы проверяем.

;.........
push offset info
push hFile
call GetFilelnformationByHandle
call GetLastError ;в регистре ЕАХ возвращается код ошибки

В регистре ЕАХ возвращается код ошибки. Расшифровать его можно с помощью файла Winerror.h, где вместе с кодами ошибок приведены короткие сообщения о причине их возникновения.

Создание, открытие, закрытие и удаление файла

Создание и открытие файла в Win32 производится одной функцией CreateFile. HANDLE CreateFi1eCLPCTSTR ipFileName, DWORD dwDesiredAccess. DWORD dwShareMode. LPSECURITY_ATTRIBUTES ipSecurityAttributes, DWORD dwCreationDistribution, DWORD dwFlagsAndAttributes. HANDLE hTemplateFile):
Параметры данной функции имеют размер двойного слова. Их назначение следующее (параметры описаны в порядке, обратном их записи в стек):

  • lpFileName — указатель на ASCIIZ-строку с именем (путем) открываемого или создаваемого файла;
  • dwDesiredAccess — тип доступа к файлу:
  • GENERICREAD = 80000000b - доступ по чтению;
  • GENERIC_WRITE = 40000000b - доступ по записи;
  • GENERIC_READ+GENERIC_WRITE = 0C0000000h - доступ по чтению-записи;
  • DwShareMode — режим разделения файлов между разными процессами, данный параметр может принимать значения:
    0 — монополизация доступа к файлу;
  • FILE_SHARE_READ = 0000000th — другие процессы могут открыть файл, но только по чтению, запись в файл монополизирована процессом, открывшим файл;
    FILESHAREWRITE = 00000002b — другие процессы могут открыть файл, но только по записи, чтение в файл монополизировано процессом, открывшим файл;
  • FILE_SHARE_READ+FILE_SHARE_WRITE = 00000003b - другие процессы могут
    открывать файл по чтению-записи;
  • IpSecurityAttributes — указатель на структуру SecurityAttributes (файл winbase.h), определяющую защиту связанного с файлом объекта ядра, при отсутствии защиты заносится NULL;
    ш dwCreationDistribution — определяет действия для случаев, когда файл существует или не существует (аналог этого параметра используется при вызове описанных выше функций MS DOS 6ch и 716ch), данный параметр может принимать значения:
  • CREATE_NEW= 1 — создать новый файл, если файл не существует; если файл существует, то функция завершается формированием ошибки;
  • CREATE_ALWAYS=2 — создать новый файл, если файл не существует; если он существует, то заместить новым;
  • 0PEN_EXISTING=3 — открыть файл, если он существует; если файл не существует, то формируется ошибка;
  • 0PEN_ALWAYS=4 — открыть файл при его существовании и создать его если файла нет;
    TRUNCATE_EXISTING=5 — открыть файл с усечением его до нулевой длины; если файл не существует, то формируется ошибка;
    DwFlagsAndAttributes — флаги и атрибуты; этот параметр используется для задания характеристик создаваемого файла:
  • FILE_ATTRIBUTE_READ0NLY=OOOO0OOlh - файл только для чтения;
  • FILE_ATTRIBUTE_HIDDEN=00000002h - скрытый файл;
  • FILE_ATTRIBUTE_SYSTEM=00000004h - системный файл;
  • FILE_ATrRIBUTE_DIRECTORY=OO0000lOh - каталог;
  • FILE ATTRIBUTE ARCHIVE=00000020h - архивный ф'айл;
  • FILE_ATTRIBUTE_N0RMAL=00000080h - обычный файл для чтения-записи (этот атрибут нельзя комбинировать с другими);
  • FILE_ATTRIBUTE_TEMPORARY=0000Ol00h - создается временный файл (преимущество этого файла в том, что система стремится не записывать этот файл на диск, а работать с ним в памяти; этот атрибут выгодно комбинировать с флагом FILE_FLAG_DELETE_ON_CLOSE, тогда после закрытия файла в программе он будет удален, не оставив следов на диске, иначе, как и bMS DOS, программе придется «подчищать» за собой содержимое диска);
  • FILE_FLAG_WRITE_THR0UGH=80000000h - не использовать промежуточное кэширование при записи на диск, а все изменения записывать прямо на диск;
  • FILE_FLAG_NO_BUFFERING=20000000h - не использовать средства буферизации операционной системы;
  • FILE_FLAG_RANDOM_ACCESS=10000000h - прямой доступ к файлу (установка этого флага или флага
  • ILE_FLAG_SEQUENTIAL_SCAN позволяет оптимизировать системе процесс кэширования);
  • FILE_FLAG_SEQUENTIAL_SCAN=08000000h - последовательный доступ к файлу; 0
  • ILE_FLAG_DELETE_0N_CL0SE=04000000h - удалить файл после его закрытия (см. описание атрибута
  • ILEATTRIBUTETEMPORARY);
  • FILE_FLAG_0VERLAPPED=40000000h - асинхронный доступ к файлу (синхронность означает то, что программа, вызвавшая функцию для доступа к файлу, приостанавливается до тех пор, пока не закончит работу функция ввода-вывода);
  • hTemplateFile — параметр используется только при создании нового файла, его значением является дескриптор другого существующего и предварительно открытого файла, а новый файл создается с теми же значениями атрибутов и флагов, что и у файла, дескриптор которого указан в параметре hTemplateFile.

При удачном завершении функция возвращает в регистре ЕАХ дескриптор нового файла. В случае неудачи функция возвращает в регистре ЕАХ значение NULL.

Закрытие файла

Закрытие файла производится функцией Cl oseHandl e:
B00L C1oseHandle( HANDLE hObject );
Функция имеет один параметр размером в двойное слово — дескриптор, полученный при открытии файла функцией CreateFile.
При удачном завершении функция возвращает ненулевое значение в регистре ЕАХ. В случае неудачи функция возвращает в регистре ЕАХ значение NULL.
Win32 поддерживает несколько функций для часто используемых операций над файлами: копирование, перемещение и переименование файлов.

Копирование файла

Для копирования файлов в Win32 используется функция CopyFile:
B00L CopyFile(LPCTSTR lpExistingFileName. LPCTSTR ipNewFileName. B00L bFailIfExists): Параметрами этой функции являются:

  • lpExistingFileName — указатель на ASCIIZ-строку с именем файла-источника;
  • lpNewFileName — указатель на ASCIIZ-строку с именем файла-приемника, который может и не существовать;
  • bFailIfExists — параметр, задаваемый равным 0 или 1, в зависимости от условий копирования:
  • 0 — при наличии файла он удаляется и создается новый с содержимым файла-источника;
  • 1 — при наличии файла копирование не производится, а функция CopyFile
    возвращает ошибку.

При удачном завершении функция возвращает ненулевое значение в регистре ЕАХ. В случае неудачи функция возвращает в регистре ЕАХ значение NULL.

;prg07_29.asm - Win32-nporpaMMa консольного приложения для исследования
;работы функции CopyFile API Win32.
;..........................................................
.data
TitleText db 'Копирование файлов в Win32'.О
s_file db "p".O ;имя входного файла
d_file db "pi".0 ;имя выходного файла
.code
mov eax.l
push eax
push offset d_file
push offset s_file
call CopyFileA
cmp eax.O
jz exit .выход в случае неудачи
:.........

Перемещение файла

Для перемещение файла Win32 содержит две функции MoveFile и MoveFi 1 еЕх:
BOOL MoveFile(LPCTSTR lpExistingFileName, LPCTSTR lpNewFileNam):
BOOL MoveFileEx(LPCTSTR TpExistingFileName. LPCTSTR ipNewFileName. DWORD dwFlags):
Параметрами функции MoveFile являются указатели на ASCIIZ-строки с именами файла-источника и файла-приемника. Функция MoveFi 1 еЕх обладает дополнительными свойствами благодаря наличию третьего параметра, который определяет особенности перемещения:

  • MOVEFILE REPLACE_EXISTING=00000001h - при существовании целевого файла он замещается содержимым файла-источника;
  • MOVEFILE_COPY_AL10WED=00000002h - если не указывать специально, то
  • функция MoveFi 1 еЕх не перемещает файлы на другой диск, а если перемещение требуется, необходимо устанавливать этот флаг; .
  • MOVEFILE_DELAY_UNTIL_REBOOT=00000004h - (только для Windows NT и выше) файл-источник не удаляется до перезагрузки системы;
  • MOVEFILE_WRITE_THROUGH=00000008h — установка флага гарантирует, что возврат из функции не произойдет до фактического перемещения и удаления файла.

Кроме этого, функция MoveFi 1 еЕх допускает указание на месте второго параметра значения NULL, тем самым моделируя вызов функции Del eteFi I e.
При удачном завершении функции MoveFile и MoveFi 1 еЕх возвращают ненулевое значение в регистре ЕАХ. В случае неудачи функции возвращают в регистре ЕАХ значение NULL.

:prg07_30.asm - Win32-nporpaMMa консольного приложения для исследования :работы функции MoveFile(Ex) API Win32.
:
.data
TitleText db 'Перемещение файлов в Win32'.О
s_file db "p",0 ;имя входного файла
d_file db "pi".0 :имя выходного файла
.code
:.........
push offset d_file
push offset s_file
call MoveFileA
cmp eax.O
jz exit :выход в случае неудачи

Переименование файла

Специальной функции для переименования файла нет, так как она и не нужна — перемещение файла в пределах одного каталога по сути и является его переименованием.

Удаление файла

Для удаления файла применяется функция Del eteFi I e:
BOOL DeleteFile(LPCTSTR TpFileName);
У нее единственный параметр — указатель на ASCIIZ-строку с именем (путем) удаляемого файла. Перед удалением файл необходимо закрыть, хотя в некоторых версиях Windows это не является обязательным.
При удачном завершении функция возвращает ненулевое значение в регистре ЕАХ. В случае неудачи функция возвращает в регистре ЕАХ значение NULL.

Чтение, запись, позиционирование в файле

Необходимо сразу отметить, что Win32 допускает два режима доступа к файлу — синхронный и асинхронный. Необходимость введения этих двух режимов в архитектуру Win32 вызвано тем, что файловый ввод-вывод относится к наиболее медленным операциям и способен значительно ухудшить впечатление от компьютера с хорошей центральной частью (процессором и материнской платой) и плохой дисковой подсистемой. Поэтому разработчики Win32 уделяют много внимания моделированию файловых операций с использованием памяти компьютера. Это и упомянутые выше временные файлы и гибкая система кэширования вводимых и выводимых данных, а также файлы, отображенные в память, работу с которыми мы рассмотрим в конце данного раздела. Наше дальнейшее изложение будет посвящено организации синхронного ввода-вывода. Для обычных несложных приложений Win32 его вполне достаточно.

Установка текущей файловой позиции

Доступ к содержимому файла может быть произвольным (прямым) и последовательным. Как обычно, функции ввода-вывода работают с файловым указателем. Но необходимо иметь в виду, что файловый указатель связан только с описателем файла. Его значение равно текущему номеру позиции в файле, с которой будет'производиться чтение-запись данных при очередном вызове функции ввода-вывода. В первый момент после открытия значение указателя равно 0, то есть он указывает на начало файла. Функции, производящие чтение-запись в файле, меняют значение файлового указателя на количество прочитанных или записанных байт. При необходимости, а при организаций прямого доступа к файлу без этого не обойтись, значение файлового указателя можно изменять с помощью функции SetFilePointer:

DWORD SetFilePointer( HANDLE hFile, LONG IDistanceToMove.
PLONG lpDistanceToMoveHigh. DWORD dwMoveMethod );

Параметры этой функции имеют размер двойного слова и следующее назначение:

cmp eax.O
jz exit :если неуспех
eld
mov edi,p_start
mov esi.p_start
mov ecx.FileSize cycl: moval.Odh repne scasb
cmp byte ptr [edi].0ah
jne $-5
inc edi
dec ecx
jeexz exit :весь файл прочитан
mov eax.edi
sub eax.esi :в еах - длина строки для вывода на экран, а в esi - ее адрес ;вывести очередную строку
call WriteConsoleA
cmp eax,0
jz exit :если неуспех
add esi,eax
jmp cycl закрываем файлы exit: :выход из приложения
return: ¦

При небольшой модификации программы можно построчно выводить содержимое любого текстового файла. Для небольших файлов совсем необязательно отслеживать конец каждой строки — можно выводить из буфера сразу весь файл одним вызовом функции WriteConsoleA.

Получение и изменение атрибутов файла

Аналогично группе функций MS DOS для работы с файловой системой файловая подсистема Win32 содержит ряд функций, с помощью которых можно определить характеристики конкретного файла.
Начальные значения атрибутов файла назначаются при создании файла. Впоследствии их можно изменить вызовом функции SetFi I eAttributes.
BOOL SetFileAttributestLPCTSTR ipFileName. DWORD dwFi1eAttributes);
Параметры этой функции означают следующее:
ш ipFileName — указатель на ASCIIZ-строку, содержащую имя файла;
Ш dwFil eAttri butes — двойное слово, определяющее, какие атрибуты файла могут быть установлены.
Планируя использование этой функции, необходимо иметь в виду, что не все возможные атрибуты файлов могут быть установлены с ее помощью. Перечислим те атрибуты, комбинацию которых можно задавать для изменения атрибутов файла, специфицированного параметром lpFileName: FILE_ATTRIBUTE_ARCHIVE, FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTENORMAL,
FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_
SYSTEM, FILEATTRIBUTETEMPORARY.
При удачном завершении функция SetFi 1 eAttri butes возвращает ненулевое значение в регистре ЕАХ. В случае неудачи функция возвращает в регистре ЕАХ значение NULL.
Для получения атрибутов файла используется функция GetFil eAttri butes.
DWORD GetFileAttributes(LPCTSTR lpFileName):
Функция имеет один параметр lpFileName, который является указателем на ASCIIZ-строку, содержащую имя файла.
При удачном завершении функция GetFil eAttri butes возвращает значение в регистре ЕАХ, которое является комбинацией атрибутов файла, специфицированного параметром lpFileName. Выделить эти атрибуты можно, используя логические команды ассемблера или команды обработки битов. В случае неудачи функция возвращает в регистре ЕАХ значение NULL.
В приложениях очень часто требуется определить размер файла. Для этого Win32 содержит отдельную функцию GetFileSize. DWORD GetFileSize
( HANDLE hFile, LPDWORD lpFileSizeHigh ): Параметры функции означают следующее:

  • hFile — дескриптор файла, размер которого требуется определить;
  • lpFileSizeHigh — адрес области памяти, куда помещаются старшие 32 бита значения размера файла, младшие 32 бита возвращаются функцией в регистре ЕАХ.

При удачном завершении функция GetFil eSize возвращает значение младших 32 бит размера файла в регистре ЕАХ. В случае неудачи функция возвращает в регистре ЕАХ значение Offffffffh.
Особого разговора заслуживают возможности получения информации о временных характеристиках файлов. По сравнению с аналогичными средствами MS DOS в Win32 этот вопрос проработан значительно глубже. Хотя если посмотреть номенклатуру и описание функций MS DOS для работы с длинными именами файлов, то видно, что у них уже есть общие идеи, реализованные рассматриваемыми ниже функциями Win32.
Как уже отмечалось выше, в Win32 с файлом связаны три значения времени: время создания, время последнего доступа и время последней модификации. Получить эти значения можно с помощью функции GetFileTime.

BOOL GetFileTime( HANDLE hFile, LPFILETIME ipCreationTime. LPFILETIME ipLastAccessTime,
LPFILETIME ipLastWhteTime);

Перед вызовом данной функции, необходимо открыть файл, о значениях времени которого мы хотим получить информацию. Функции GetFileTime передается дескриптор этого файла и указатели на три экземпляра структуры FILETIME, в которые будут записаны время создания (IpCreationTime), время последнего доступа (lpLastAccessTime) и время последней записи (lpLastWriteTime).
Аналогично функции MS DOS 71a7h Win32 предоставляет две функции для взаимного преобразования DOS-времени файла в 64-битное представление времени:

BOOL FileTimeToDosDateTime(CONST FILETIME *lpFileTime, LPWORD lpFatDate.
LPWORD ipFatTime): BOOL DosDateTimeToFileTimetWORD wFatDate. WORD wFatTime. LPFILETIME ipFiieTime);

Функция FileTimeToDosDateTime в качестве входного параметра принимает указатель *lpFi1eTime на экземпляр структуры FILETIME. Этот указатель содержит представление времени в виде 64-битного значения. На выходе данная функция формирует два значения в переменных размером в слово, адреса которых указаны параметрами lpFatDate и lpFatTime. Формат этих слов совпадает с форматом соответствующих параметров, которыми манипулирует функция 71a7h.
Функция DosDateTimeToFileTime, наоборот, преобразует время в формате DOS представленное в виде двух слов wFatDate, wFatTime (для времени и даты соответственно), в 64-битное значение 1 pFi I eTime.
Установить время создания, последнего доступа или модификации файлов можно с помощью функции SetFileTime.

BOOL SetFileTime( HANDLE hFile. const FILETIME *lpCreationTime.
const FILETIME *lpLastAccessTime, const FILETIME *lpLastWriteTime ):

В качестве входных параметров функция SetFil eTime принимает указатели на три экземпляра структуры FILETIME и дескриптор файла. Экземпляры структур уже заполнены необходимыми значениями времени. Если какое-либо из значений устанавливать не нужно, то вместо указателя на соответствующую структуру передается NULL. В случае успешного завершения функция возвращает ненулевое значение в регистре ЕАХ.
Из вышеизложенного видно, что для получения различных характеристик файла используются множество различных функций. Работа с ними может утомить кого угодно. Нельзя ли чего-нибудь попроще? Можно. Win32 предоставляет функцию GetFi I elnformationByHandl e:

BOOL GetFileInformationByHandle( HANDLE hFile.
LPBY_HANDLE_FILE_INFORMATION lpFilelnformation );

На вход данной функции передается дескриптор файла, о котором необходимо получить Информацию, и указатель на экземпляр структуры BYHANDLEFI LE_ INFORMATION, который заполняется этой функцией. Как видно из названия полей (см. код ниже), в этой структуре сосредоточена вся информация о файле. Ниже приведен пример кода, использующего данную структуру.

;prg07_32.asm - Win32-nporpaMMa консольного приложения для Win32 для исследования работы
функции GetFilelnformationByHandle API Win32.
;описание структур
FILETIME struc
DwLowDateTime dd ? ;младшие 32 бита значения времени
DwHighDateTime dd ? :старшие 32 бита значения времени
FILETIME ends
BY_HANDLE_FILE_INFORMATION struc
DwFileAttributes dd 0 атрибуты файла
struc
FtCreationTime_DwLowDateTime dd ? ;младшие 32 бита значения времени создания файла FtCreationTime_DwHighDateTiirie dd ? :старшие 32 бита значения времени создания файла
ends
struc
FtLastAccessTime_DwLowDateTime dd ? ;младшие 32 бита значения времени поел, доступа FtLastAccessTime_DwHighDateTime dd ? :старшие 32 бита значения времени поел, доступа
ends
struc
^¦LastWriteTime_DwLowOateTime dd ? :младшие 32 бита значения времени поел, записи ^¦LastWnteTimeJDwHighDateTime dd ? :старшие 32 бита значения времени поел, записи ends
. .".-rialNumber dd С ;серийный номер тома, на котором находится файл
| nfTleSizeHigh dd 0 :старшие 32 бита размера файла ¦nFiieSizeLow dd 0 :младшие 32 бита размера файла I nNumberOftinks dd 0 ;число ссылок на файл | nFilelndexHigh dd 0 ;старшие 32 бита идентификатора файла nFilelndexLow dd 0 ; младшие 32 бита идентификатора файла
ends .data
t info BY_HANDLE_FILE_INFORMATION <> TitleText db 'Получение информации о файле
в Win32',О lpBuf db "p",0 I hFile dd 0 .code
¦-------------------------------------CreateFi 1 e----------------------------
Нгкрываем файл
push О
push 0 -.атрибуты (они игнорируются)
push OPEN_EXISTING :открыть существующий файл, если его нет - ошибка
push 0 : защита файла не требуется
push FILE_SHARE_READ разрешено совместное использование файла (по чтению)
push GENERIC_READ разрешено чтение из файла
push offset lpBuf
call CreateFileA
emp eax.Offffffffh
je exit :если неуспех
mov hFile.eax :дескриптор файла №
GetFilelnformationByHandle
push offset info
push hFile
call GetFilelnformationByHandle
emp eax.O
jz exit :выход в случае неудачи ¦"^
результат смотрим в отладчике TD32.exe

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

Работа с дисками, каталогами и организация поиска файлов

Win32 располагает большим набором функций для получения информации о структуре файловой системы конкретного компьютера. Часть этих функций раз-вивает идеи работы с файловой подсистемой, появившиеся в последних версиях WAS DOS. Другие функции являются уникальными для платформы Win32. Рас-Рассмотрим наиболее интересные из них.
Программа, предназначенная для исследования файловой системы, прежде всего должна знать, какие логические диски присутствуют в системе. Среди
нескольких функций, выполняющих эту работу, наиболее удобной для процесса обработки является GetLogicalDrivesString.

DWORD GetLogicalDriveStrings(DWORD nBufferLength, LPTSTR lpBuffer);

Данной функции передаются два параметра: lpBuffer — адрес буфера, в который помешаются имена корневых каталогов логических дисков, установленных в системе; nBufferLength — длина буфера, заданного указателем lpBuffer. В качестве возвращаемого значения функция формирует длину буфера, действительно необходимую для размещения строки с именами корневых каталогов логических дисков. Например, при наличии трех логических дисков структура заполненного буфера будет следующей: А:\0В:\0С:\0. Заметьте, что имена корневых каталогов разделены нулевыми байтами. Более эффективно вызывать эту функцию два раза: первый раз с нулевым значением первого параметра, при этом функция вернет потребное количество байт для размещения буфера; второй раз функцию уже можно вызывать, подставив на место первого параметра значение, возвращенное при первом вызове.

:prg07_33.asm - Win32-консольное приложение для Win32 для исследования работы :функции GetLogicalDriveStrings API Win32.
.data
TitleText db 'Получение информации о дисках в Win32',0 '
infojmf db 10 duo (0)
.code
:.......----GetLogi cal Dri veStri ngs................-........
push offset info_buf
push 0
call GetLogicalDriveStringsA
cmp eax.O
jz exit ;выход в случае неудачи
;вызываем функцию второй раз. когда известно количество байт, потребное :для записи списка корневых каталогов
push offset info_buf
push eax
call GetLogicalDriveStringsA
cmp eax.O
jz exit :выход в случае неудачи результат смотрим в отладчике TD32.exe

Недостаток функции GetLogi cal Dri veStri ngs состоит в том, что она работает не во всех версиях Windows. Альтернативным вариантом получения информации о наличии дисков в системе является функция GetLogi cal Drives.
DWORD GetLogicalDrives(VOID);
Эта функция возвращает в регистре ЕАХ битовую маску, в которой установленные биты указывают на существование логического диска: бит 0 — А, бит 1 — В, бит 2 — С... Таким образом, с помощью функции GetLogi cal Drives можно достичь того же самого результата, что и с помощью функции GetLogi cal Dri veStri ngs, но несколько большими трудами.

;prg07_34.asm - Win32-консольное приложение для исследования
;работы функции GetLogicalDrives API Win32.
.data
TitleText db 'Получение информации о дисках в Win32'.0
info_buf db 10 dup (0)
.code
call GetLogicalDrives cmp eax.O
jz exit ;выход в случае неудачи результат смотрим з отладчике TD32.exe

После того как информация о номенклатуре логических дисков в системе получена, можно получить информацию о каждом из них. Для этого используется функция GetVolumelnformation.

BOOL GetVolumeInformation(LPCTSTR ipRootPathName.
LPTSTR lpvolumeNameBuffer. DWORD nVolumeNameSize,
LPDWORD ipVolumeSerialNumber, LPDWORD ipMaximumComponentLength.
LPDWORD lpFileSystemFlags. LPTSTR ipFileSystemNameBuffer. DWORD nFileSystemNameSize);

На вход функции GetVolumelnformation подаются следующие параметры:

  • IpRootPathName — указатель на строку с именем корневого каталога диска, информацию о котором необходимо получить (если параметр равен NULL, функция формирует информацию о текущем диске). Формат задания имени корневого каталога диска — имя_диска:\. Это единственный параметр, значение которого нужно задавать, остальные параметры — адреса областей памяти, в которые будут помещены значения, формируемые функцией;
  • lpVolumeNameBuffern и nVolumeNameSize — указатель на буфер и размер буфера, в который будет записано имя диска;
  • IpVolumeSerial Number — адрес двойного слова, куда будет записан серийный номер. Если информация о серийном номере диска не нужна, то при вызове функции значение этого параметра необходимо сделать равным NULL;
  • lpMaximumComponentLength — адрес двойного слова, куда будет записано значение максимальной длины пути, возможное в данной файловой системе;
  • lpFileSystemFlags — флаги с дополнительной информацией о файловой системе:
    • FS_CASE_SENSITIVE=FILE_CASE_SENSITIVE_SEARCH=00000001h - поддержка со стороны файловой системы поиска с сохранением регистра букв;
    • FS_CASE_IS_PRESERVED=FILE_CASE_PRESERVED_NAMES=
      00000002h - при записи на диск сохранить регистр букв в имени файла;
    • FS_UNICODE_STORED_ON_DISK=FILE_UNICODE_ON_DISK=
      00000004h - файловая система поддерживает хранение имен файлов в Unicode;
    • FSPERSI STENT_ACLS=FI LEPERS I STENT_ACLS=
      00000008h - файловая система способна оперировать со списками контроля доступа (ACL) — только для NTFS;
      • FS_FILE_COMPRESSION=FILE_FILE_COMPR?SSION=
      00000010h — файловая система поддерживает сжатие файлов;
      • FS_VOL_IS_COMPRESSED=FILE_VOLUME_IS_COMPRESSED=
      00008000h - том, о котором запрашивается информация, был сжат;
      a lpFileSystemNameBuffer и nFileSystemNameSize — указатель и размер буфера в который будет записано имя файловой системы. Если TpFiieSystemName-Buffer=NULL, то в эти параметры ничего не записывается.

Изменить метку диска может вызов функции SetVolumeLabel.
BOOL SetVolumeLabel(LPCTSTR IpRootPathName. LPTSTR lpVolumeName):
Параметр IpRootPathName задает адрес строки с именем корневого каталога диска, метку которого меняем. Второй параметр 1 pVol umeNarae — строка с меткой тома. Для удаления метки тома с диска параметр lpVolumeName нужно задать равным NULL.

Получить информацию о свободном дисковом пространстве

Информацию о свободном дисковом пространстве, а заодно и о разбиении диска на сектора и кластеры позволяет получить функция GetDiskFreeSpace.
BOOL GetDiskFreeSpaceCLPCTSTR IpRootPathName.
LPDWORD ipSectorsPerCluster, LPDWORD lpBytesPerSector,
LPDWORD ipNumDerOfFreeClusters. " LPDWORD ipTotalNumberOfClusters):

На вход функции нужно подать строку с именем корневого каталога диска, о котором необходимо получить информацию (NULL для текущего диска), и адреса буферов, куда будет помещена следующая информация: общее количество кластеров на диске (1 pTotal NumberOfCl usters), общее количество свободных кластеров (TpNumberOfFreeCI usters), количество байт в секторе (lpBytesPerSector), количество секторов в кластере (IpSectorsPerCluster).

Создание и удаление каталога

Создание каталога выполняет функция CreateDi rectory.
BOOL CreateDi rectory (LPCTSTR lpPathName. LPSECURITYJVTTRIBUTES.IpSecurityAttributes ¦:
Первый параметр этой функции lpPathName — указатель на ASCIIZ-строку с путем, последний элемент которого является именем нового каталога. Параметр ipSecurityAttributes — указатель на экземпляр структуры Security_Attributes.

SECURITY_ATTRIBUTES stoic
nLength dd. 0
lpSecurityDeschptor dd 0
blnheritHandle dd
ends

С помощью структуры SecurityAttributes можно ограничить доступ пользователя к каталогу. Параметр IpSecurityAttributes обычно задается равным NULL. Более подробную информацию о параметрах структуры можно получить в MSDN.
Удаление каталога выполняет функция RemoveDi rectory.
BOOL RemoveDirectory(LPCTSTR lpPathName);
единственный параметр этой функции ipPathName — указатель на ASCIIZ-стро-^Вс путем, последний элемент которого является именем удаляемого каталога. ^Удаляемый каталог не должен быть пустым.

Определение и изменение текущего каталога

Аналогично принципам организации файловой системы MS DOS в Win32 также Иуществует понятие текущего каталога, то есть каталога, в котором выполняются ^шеущие операции по работе с файлами. В отличие от MS DOS понятие текуще-^^Италога относится к текущему процессу. При запуске процесса текущим бу-I дет являться каталог, из которого этот процесс был запущен. Определяет текущий налог процесса функция GetCurrentDirectory.
RD GetCurrentDirectory(OWORD nBufferLength, LPTSTR ipBuffer);
it Параметры TpBuffern и BufferLength определяют соответственно адрес и длину I буфера, в который помещается путь с текущим каталогом (строка с завершаю-Нцим нулем). Функция возвращает NULL в случае ошибки и число байтов, необходимо для записи данных в буфер, в случае удачного завершения. Завершающий ^ нуль в возвращаемом функцией числе не учитывается. Если буфер мал, то с помощью возвращаемого значения можно изменить его размер. [ Изменить текущий каталог процесса можно с помощью функции SetCurrent-^Brectory.
¦6001 SetCurrentDi rectory (LPCTSTR IpszPathName): "
j" Параметр IpszPathName — адрес ASCIIZ-строки с путем, последний элемент ко-I торою — новый текущий каталог данного процесса.
[i Платформа Win32 также поддерживает понятие системного и основного ка-^ВДога Windows. Для определения системного каталога существует специальная |; функция GetSystemDi rectory.
[ GetSystemDirectory(LPTSTR ipszBuffer, UINT uSize):
Два параметра этой функции определяют адрес и размер буфера, в который I записывается путь к системному каталогу Windows.
„Для определения основного каталога Windows существует специальная функция
GetWindowsDi rectory.-
UINT GetWindowsDirectorydPTSTR IpBuffer. UINT uSize);
Два параметра этой функции определяют адрес и размер буфера, в которыйзаписывается путь к основному каталогу Windows. Возвращаемое значение количество реально записанных в буфер байтов. Его можно использовать для г корректировки параметра uSize, если он был задан слишком маленьким, и повторного вызова функции GetWindowsDi rectory.
Для демонстрации применения вышеприведенных функций рассмотрим комплексный пример, в ходе которого продемонстрируем порядок вызова и анализа Е возвращаемых значений функциями API Win32 для работы с каталогами. Опре-
делим текущий каталог процесса, создадим новый каталог, сделаем его текущим удалим новый каталог, определим системный каталог и каталог Windows.

:prg07_35.asm - Win32-KOHCoльнoe приложение для исследования работы функций ;работы с каталогами API Win32.
.data
TitleText db 'Работа с каталогами в Win32',0
NewOir db "Новый каталог".0
dirjbuf db 50 dup ("?")
size_dir_buf=$-dir_buf
Parent db "..".0
.code
:определив текущий каталог
push offset dirjwf
push size_dir_buf
call GetCurrentDirectoryA
cmp eax.0
jz exit ;выход в случае неудачи :создадим каталог
push 0
push offset NewDir .
call CreateDirectoryA
cmp eax,0
jz exit :выход в случае неудачи ¦.сделаем новый каталог текущий
push offset NewDir
call SetCurrentDirectoryA
cmp eax.0
jz exit ;выход в случае неудачи
;проверим новый текущий каталог
push offset dir_buf
push size_dir_buf
call GetCurrentDirectoryA
cmp eax.0
jz exit .-выход в случае неудачи
;------.....SetCurrentDi rectory-..................-.........
:вернемся в родительский каталог
push offset Parent
call SetCurrentDirectoryA
cmp eax.0
jz exit ;выход в случае неудачи
;проверил новый текущий каталог
push offset dir_buf
push size_dir_buf
call GetCurrentDirectoryA
cmp eax.O
jz exit :выход в случае неудачи :удалим новый текущий каталог
push offset NewDir
call RemoveDirectoryA
cmp eax.O
jz exit ;выход в случае неудачи I определив системный каталог mov eax.size_dir_buf push eax
push offset dir_buf call GetSystemDirectoryA cmp eax.O
jz exit :выход в случае неудачи ¦.определим основной каталог Windows mov eax.size_d1r_buf push eax
push offset dir_buf call GetWindowsDirectoryA cmp eax.O
jz exit -.выход в случае неудачи результат смотрим в отладчике TD32.exe

Среди функций Win32, работающих с текущим каталогом, существует функция GetFuT I PathName, которая по имени файла формирует его полное имя, состоящее из пути от корневого каталога к текущему. Последний элемент этого имени — имя входного файла.
DWORD GetFulIPathNameCLPCTSTR ipFileName. DWORD
nBufferLength, LPTSTR ipBuffer.
LPTSTR *lpFilePart): I На входе функция принимает имя файла в виде ASCIIZ-строки. На выходе —
три параметра:

  • IpBuffer — адрес буфера, в который помещается полный путь с именем
    файла;
  • nBufferLength — длина буфера, на который указывает параметр IpBuffer,
    в символах;
  • lpFilePart — адрес ячейки размером с двойное слово, в которое помещается указатель на позицию внутри буфера, идентифицированную параметром 1 pBuf fег и соответствующую первому символу имени файла после имен всех каталогов.

Самое интересное в этой функции — механизм ее работы. Суть его в том, что
реально функция GetFul I PathName не ищет файл, ка имя которого указывает параметр IpBuffer. Результат своей работы — полный путь — она формирует из двух
компонент: полного пути к текущему каталогу данного процесса и имени файла,
наличие которого на диске функция GetFul I PathName даже не проверяет. Для подобной работы ей даже не нужно обращаться к диску. С аналогичной функцией
' мы уже имели дело, когда рассматривали функции MS DOS для работы с файлами, имеющими длинные имена.

Поиск файлов

При последовательном изучении материала данного раздела читатель кроме знакомства со средствами по работе с файлами операционных систем фирмы Microsoft поневоле должен был оценить процесс эволюции этих средств. Особенно очевиден этот процесс при поиске файлов.
Платформа Win32 предлагает два способа поиска файлов:

  • с использованием функции SearchPath;
  • с использованием функций FindFirr,tFile, FindNExtFile и структуры WIN32 FIND DATA.

Поиск файлов с помощью функции SearchPath

Функция SearchPath ищет файлы в указанном при ее вызове списке каталогов.
DWORD SearchPathtLPCTSTR lpPath. LPCTSTR ipFileName.
LPCTSTR lpExtension, DWORD nBufferiength. LPTSTR ipBuffer, LPTSTR *lpFilePart):
Первый параметр lpPath определяет список каталогов, в которых будет осуществляться поиск файла. Параметры lpFileName и lpExtension указывают на ASCIIZ-строки с именем и расширением искомого файла. Наличие пары этих параметров позволяет задавать имя и расширение файла двумя способами:

  • одной ASCIIZ-строкой — на нее указывает параметр lpFileName, при этом параметр ipExtension равен NULL;
  • отдельными ASCHZ-строками — в этом случае параметр ipFileName содержит указатель на ASCIIZ-строку с именем файла, а второй парамет lpExtension — содержит указатель на ASCIIZ-строку с расширением файла; строка с расширением должна начинаться с символа . (точка).

Параметр IpBuffer указывает на буфер, куда записывается ASCHZ-строка с полным путем к искомому файлу. Длина этого буфера определяется параметром nBufferLength. Если эта длина слишком мала, то ее можно подкорректировать значением, возвращаемым функцией в регистре ЕАХ. Это значение является личеством символов, действительно необходимых для записи полного имени найденного файла в буфер. Если в ЕАХ возвращается NULL, то это говорит об ошибке вызова функции.
Последний параметр lpFilePart является указателем на символ в буфере, с которого начинается собственно имя файла.
При вызове функции SearchPath параметр lpPath можно задать равным NULL. В этом случае поиск файла будет осуществляться в следующих каталогах (порядок перечисления соответствует порядку просмотра при поиске):

  • каталог, из которого запущено приложение;
  • текущий каталог;
  • системный каталог;
  • основной каталог Windows;
  • каталоги, перечисленные в переменной окружения PATH.

Поиск файлов с помощью функций FindFirstFile и FindNExtFile Предыдущий способ поиска обладает существенным недостатком — ограниченным числом каталогов диска, подвергающихся просмотру в процессе поиска. По этой причине он не может быть использован для поиска в пределах всего диска. Этот недостаток устраняется при втором способе поиска — с использованием функций FindFirstFile, FindNExtFile и структуры WIN32FINDDATA. Этот способ реализует определенный алгоритм поиска. Вначале вызывается функция FindFirstFile, которая имеет два параметра: lpFileName — указатель на ASCII-строку с именем файла; lpFindFileData — указатель на экземпляр структуры WIN32_FIND_OATA.
HANDLE FindFirstFiletLPCTSTR lpFileName. LPWIN32_FIND_DATA lpFindFileData): Имя файла может содержать символы шаблона * и ?. Кроме того, имя может Вырожать путь, с которого нужно начинать поиск. Выше, при знакомстве с функциями MS DOS для работы файлами, имеющими длинные имена, приводилось Иписание структуры WIN32_FIND_DATA и ее полей.
В случае успеха функция FindFirstFile заполняет поля структуры WIN32_FIND_ DATA и возвращает значение дескриптора внутренней структуры в памяти, который впоследствии может быть использован функциями FindNextFile или FindClose. В случае неудачи функция не изменяет содержимое структуры WIN32_FIND_DATA и возвращает значение INVALID_HANDLE_VALUE (EAX=-llo=Offffffffh). 1 Проанализировав результаты поиска, программа может продолжить или прекратить его. Для продолжения поиска необходимо вызвать функцию Fi ndNExtFi I e. I BOOL FindNextFile( HANDLE hFindFile.
LPWIN32_FIND_DATA lpFindFileData ):

В качестве параметров используются дескриптор, полученный в регистре ЕАХ в результате поиска функцией FindFirstFile, и указатель на экземпляр структуры WIN32_FIND_DATA. В случае успеха функция FindNextFile возвращает ненулевое значение в регистре ЕАХ и заполняет структуру WIN32FINDDATA. При неудаче —
I ЕАХ = 0.
Для продолжения поиска при неизменных исходных параметрах поиска функ-
ция FindNextFile вызывается циклически.
Для окончания процесса поиска необходимо вызвать функцию FindClose.
BOOL FindCloset HANDLE hFindFile ):
i Функция FindClose имеет один параметр — дескриптор, полученный функци-Нй FindFirstFile в начале поиска. В случае успеха функция FindClose возвращает Е ненулевое значение в регистре ЕАХ, при неудаче — ЕАХ = 0.

Файлы, отображаемые в память

¦Платформа Win32 позволяет организовать работу с содержимым файла как с ^Властью оперативной памяти, без использования операций файлового ввода-Лывода. Этот механизм отображает (проецирует) содержимое файла на область Императивной памяти. Программе передается адрес этой области, после этого ра-Ибота с содержимым файла осуществляется командами работы с памятью.
Для «проецирования» файла необходимо выполнить следующие действия.

  1. Требуется создать (для несуществующего файла) или открыть (для существующего файла) объект ядра файл. Цель этого шага — сообщить системе, где находится физическое представление файла. Создание или открытие объекта ядра файл производится с помощью функции CreateFile (см. выше). Все параметры этой функции задаются обычным образом. На выходе в случае успеха функция формирует дескриптор (в регистре ЕАХ), в обратном случае - значение INVALID_HANDLE_VALUE (ЕАХ—llo=Offffffffh).
  2. Требуется создать объект ядра проекция файла. Цель этого шага — сообщить системе размер проецируемого файла. Для этого используется функция CreateFi I eMappi ng:
    HANDLE CreateFileMapping(HANDLE hFile. LPSECURITY_ATTRIBUTES
    ipFileMappingAttributes DWORD flProtect. DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow. LPCTSTR ipName);
    Параметр hFile является дескриптором файла, полученным функцией Create-File. Параметр 1 pFi 1 eMappingAttributes — указатель на экземпляр структуры SECURITYATTRIBUTES, которая служит для установки защиты. Присвойте параметру lpFil eMappi ngAttri butes значение NULL. Параметр flProtect предназначен для установки атрибутов защиты страниц физической памяти в адресном пространстве процесса, на которые отображается файл. Используют один из следующих атрибутов:
    PAGE_READONLY=02 — доступ к файлу только по чтению (при использовании этого параметра вызов CreateFile должен был производиться с флагом GENERIC_READ);
    € PAGEREADWRI TE=04 — доступ к файлу только по записи (при использовании этого параметра вызов CreateFile должен был производиться с флагом GENERICREAD | GENERICWRITE);
    a PAGE_WRITECOPY=08 — доступ к файлу по чтению-записи с созданием копии данных из файла, при этом исходный файл не изменяется, изменения касаются лишь модифицированных страниц копии в страничном файле (при использовании этого параметра вызов CreateFile должен был производиться с флагом GENERIC_READ или GENERIt_READ|GENERIC_WRITE).
    Параметры dwMaximumSizeHigh и dwMaximumSizeLow предназначены для того, чтобы сообщить системе максимальный размер файла в байтах. При этом в dwMaximumSizeLow указываются младшие 32 бита этого значения, а в dwMaximumSizeHigh — старшие 32 бита. Если предполагается размер файла, равный текущей его длине, то следует при вызове функции передать dwMaxi ¦ mumSizeLow=dwMaximumSizeHigh=NULL.
    Последний параметр IpName — указатель на ASCIIZ-строку с именем объекта проецируемый файл для обеспечения доступа к нему других процессов. Обычно задают равным NULL.
  3. Требуется выполнить проецирование файла на адресное пространство процесса. В этом шаге две цели. Первая цель — сообщить системе порядок отображения (проецирования) файла на адресное пространство процесса — полный или частичный. Вторая цель — получить адрес этого отображения в памяти. Реализация этих целей достигается функцией MapViewOfFile: LPVOID MapViewOfFi1e(HANDLE hFileMappingObject, DWORD dwDesiredAccess.
  4. DWORD dwFileOffsetHigh. DWORD dwFileOffsetLow. DWORD dwNumberOfBytesToMap);
    Параметр hFil eMappi ngObject — дескриптор, возвращенный функцией Create-Fi 1 eMapping на предыдущем шаге. Параметр dwDesiredAccess определяет вид доступа к данным:
    FILE_MAP_COPY=01 — данные в файле доступны по чтению, хотя отображенные данные доступны по чтению и по записи; операция записи приводит к созданию копии страницы в страничном файле, в которую производится запись, поэтому после первой операции записи теряется соответствие между реальными данными на диске и данными, с кото рыми работает приложение (при использовании этого значения параметра dwDesiredAccess функция CreateFil eMappi ng должна была быть вызвана с одним из атрибутов: PAGE_READONLY, PAGE_READWRITE или PAGEWRITECOPY);
  5. FILE_MAP_WRITE=02 — данные в файле доступны по чтению-записи (при использовании этого значения параметра dwDesiredAccess функция CreateFil eMapping должна была быть вызвана с атрибутом PAGE_READWRITE);
    ш FILEMAPRE AD=04 — данные в файле доступны по чтению (при использовании этого значения параметра dwDesiredAccess функция CreateFile-Mapping должна была быть вызвана с одним из атрибутов: PAGE_READONLY, PAGE_READWRITE или PAGE_WRITECOPY);
    • FILE_MAP_ALL_ACCESS=OOOFO00Oh + OOOOOOOlh + 00000002h + 00000004h + 00000008h + OOOOOOlOh — данные в файле доступны по чтению-записи (при использовании этого значения параметра dwDesiredAccess функция CreateFile-Mapping должна была быть вызвана с атрибутом PAGERE ADWRI ТЕ).
  6. Параметры dwFileOffsetHigh, dwFileOffsetLow и dwNumberOfBytesToMap предназначены для указания позиции в файле, с которой начинать отображение, и количества отображаемых байт (dwNumberOfBytesToMap). Параметр dwFileOffsetHigh — старшие 32 бита этого смещения, а параметр dwFileOffsetLow — младшие 32 бита этого смещения. Таким образом, с файлом можно работать не целиком, а по частям, эффективно используя при этом оперативную память. Заметим, что если задать параметр dwNumberOfBytesToMap равным NULL, то система будет пытаться отобразить содержимое файла с указанной парой dwFileOffsetHigh:dwFileOffsetLow смещения и до конца файла. В случае успеха функция формирует адрес отображения в памяти (регистр ЕАХ), в обратном случае ЕАХ = 0. После получения в ЕАХ адреса начала отображения в памяти приложение может работать с данными файла обычными командами работы с памятью. При необходимости функция MapViewOfFile может быть вызвана повторно с другими параметрами dwDesiredAccess, dwFileOffsetHigh, dwFileOffsetLow и dwNumberOfBytesToMap. При этом (запомните!) резервируется новый регион в памяти.
    После выполнения необходимых действий приложение должно корректно завершить работу с отображением файла.
  7. Требуется выполнить разрыв связи данных в файле и соответствующими данными, отображенными на адресное пространство процесса. Это дейст-
    I вие выполняет функция UnmapViewOfFile. BOOL UnmapViewOfFile( LPCVOID ipBaseAddress);
    Эта функция имеет единственный параметр — IpBaseAddress, который является значением, возвращенным функцией MapViewOfFile. С помощью функции UnmapViewOfFile необходимо разрывать каждое из отображений, созданных последовательностью вызовов MapViewOfFile, сохраняя при этом их соответствия. Также имейте в виду, что если функция MapViewOfFile была вызвана с параметром FILEMAPCOPY, то после вызова UnmapViewOfFile теряются все внесенные в отображенные данные изменения.
  8. Далее нужно закрыть объект ядра проекция файла. В принципе, этот и сле-ivmmuu шяг нр яиляютг.я обязательными, так как система в процессе за-
    вершения работы приложения освободит все ресурсы. Освобождение
    объекта ядра проекция файла производится функцией CloseHandle.
    BOOL CloseHandle( HANDLE hObject):
    Функции CloseHandle передается единственный параметр hObject — де.
    скриптор, полученный как результат вызова функции CreateFileMapping.
  9. Требуется закрыть объект ядра файл. Освобождение объекта ядра файл также производится функцией CloseHandle.
    BOOL CloseHandle( HANDLE hObject );
    Функции CloseHandle передается единственный параметр hObject — дескриптор, полученный как результат вызова функции CreateFile.
    Пример программы (prg07_36.asm), демонстрирующей порядок использования файлов, отображаемых в адресное пространство процесса, достаточно велик и по этой причине вынесен на дискету. Работа программы проста и заключается в следующем: необходимо вывести содержимое некоторого файла на экран — в окно консоли. Имя исходного файла вводится с клавиатуры.


Профайлер

Продавай мобильный контент со своего сайта

Профайлер

  • Расширение традиционной архитектуры Intel
    • Команды RDMSR и WRMSR
    • Команда CPUID — получение информации о текущем процессоре
  • Использование счетчика меток реального времени TSC

Если вы можете измерить и выразить числами то, о чем
говорите, — кое-что вы об этом знаете, если же вы не можете
этого измерить и выразить числами — тание ваше ограничено
и неудовлетворительно: возможно, что это начало знания,
но едва ли вы в мыслях продвинулись до уровня научной теории.
Лорд Кельвин (конец XIX века)
...наука начинается с тех пор, как начинают измерять.
Д. И. Менделеев

В этой главе мы рассмотрим проблему измерения скоросгн работы программ. Интерес к данной теме у программистов всегда повышен и подобен интересу рыболовов к размерам выловленной рыбы. Когда программист разрабатывает алгоритм реализации некоторой задачи, то он обязательно пытается оценить скорость, с которой будет работать программа по этому алгоритму. Б процессе изложения материала мы уже не раз предлагали для решения одной задачи несколько способов, но при этом оставляли открытым вопрос об оценке их эффективности. В этом разделе попытаемся ликвидировать этот пробел, при этом попутно рассмотрим несколько общих вопросов, связанных с микропроцессорами архитектуры Intel (Intel и AMD). Основное внимание мы уделим некоторым средствам микропроцессора Intel, предназначенным для оценки эффективности функционирующих на них программ. В значительной степени рассматриваемый ниже материал также применим и к микропроцессорам AMD, так как эти микропроцессоры имеют подобные средства.

Расширение традиционной архитектуры Intel

С появлением микропроцессоров пятого поколения (Pentium, Pentium MMX...) программисту следует различать два слоя микропроцессоров архитектуры Intel: базовый и модельно-зависимый.
Слой базовой архитектуры включает в себя элементы архитектуры, поддержку неизменность которых производитель (Intel) гарантирует. Это набор и структура cистемных регистров и регистров общего назначения, архитектура памяти и т. д.
Модельно-зависимый слой архитектуры включает в себя средства, поддержка Всоторых зависит от конкретной модели процессора (как правило, снизу вверх). Для того чтобы программа, исполняемая процессором, могла получить сведения о возможностях этого процессора, в систему команд включена команда CPUID Данная команда позволяет программе в любой момент времени получить сведения о классе, модели и архитектурных особенностях текущего процессора. Подробное описание данной команды приведено ниже.
Физически модельно-зависимые средства представляют собой набор модель-но-зависимых регистров — MSR (Model Specific Registers). Наличие и назначение этих регистров по определению зависит от конкретной модели процессора, что, в свою очередь, не гарантирует их поддержку будущими процессорами архитектуры Intel. MSR-регистры обеспечивают управление различными аппаратно-и программно-зависимыми средствами, включая следующие:

  • счетчики мониторинга производительности;
  • расширения отладки;
    И поддержка исключения машинной ошибки и архитектуры машинного контроля (МСА);
  • регистры MTRR для поддержки расширенных средств кэширования.

Чтение и запись в MSR-регистры осуществляются командами RDMSR и WRMSR. Большинство MSR-регистров инициализируются при программной инициализации процессора, многие из них можно впоследствии установить в соответствии с конкретными потребностями программы. Приведем «краткое описание команд RDMSR, WRMSR И CPUID.

Команды RDMSR и WRMSR

Команда RDMSR (ReaD from Model Specific Register) выполняет чтение из MSR-регистра. Действие команды заключается в проверке двух условий: во-первых, проверяется наличие нулевого уровня привилегированности кода, во-вторых, проверяется наличие в регистре ЕСХ значения, адресующего один из MSR-регистров. Если хотя бы одно из этих условий не выполняется, то выполнение команды RDMSR заканчивается. Если выполняются оба условия, то значение MSR-регистра, адресуемого содержимым регистра ЕСХ, помещается в пару 32-битных регистров EDX:EAX.
Команда WRMSR (WRite to Model Specific Register) производит запись значения в один из 64-разрядных MSR-регистров. Действие команды заключается в проверке тех же двух условий: во-первых, проверяется наличие нулевого уровня привилегированности кода, во-вторых, проверяется наличие в регистре ЕСХ значения, адресующего один из MSR-регистров. Если хотя бы одно из этих условий не выполняется, то работа команды заканчивается. Если выполняются оба условия, то значение пары 32-битных регистров EDX: ЕАХ пересылается в 64-битный MSR-регистр, номер которого задан в регистре ЕСХ.

Команда CPUID — получение информации о текущем процессоре

Для получения информации о процессоре необходимо в регистр ЕАХ поместить параметр — одно из значений 0, 1 или 2.
Если ЕАХ = 0, то в регистрах ЕАХ, ЕВХ, EDX, ЕСХ формируется следующая информация:

  • ЕАХ = n, где n — максимально допустимое значение параметра, которое может быть помещено в регистр ЕАХ для задания режима сбора информации;
  • (EBX)+(EDX)+(ECX) — в этих регистрах содержится строка-идентификатор процессора Geninnelntel .

Если ЕАХ = 1, то в регистрах процессора сформируется следующая информация:

  • ЕАХ = n — информация о микропроцессоре (см. табл. 8.1 и 8.2);
  • EDX = n — информация о возможностях процессора (см. табл. 8.3).

Если ЕАХ = 2, то в регистрах ЕАХ, ЕВХ, ЕСХ и EDX формируется информация о кэшпамяти первого уровня и TLB-буферах. Первый байт регистра ЕАХ содержит число, означающее, сколько раз необходимо последовательно выполнить команду CPUID для получения полной информации о кэш-памяти первого уровня и TLB-буферах. Другие байты регистра ЕАХ и все байты регистров ЕВХ, ЕСХ и EDX содержат однобайтовые дескрипторы, характеризующие кэш-память и TLB-буферы (см. табл. 8.4). Старший бит каждого регистра характеризует достоверность информации в регистре. Если он равен нулю, то информация достоверна, иначе — регистр не используется.

Таблица 8.1. Поля регистра ЕАХ после выполнения команды CPUID (при ЕАХ = 1)

Биты ЕАХ | Назначение
0...3 | Версия изменений модели
4...7 | Модель в семействе (см. табл. 8.2)
8...11 | Семейство микропроцессоров (см. табл. 8.2)
12...13 | Тип процессора (00 — обычный процессор; 01 — Overdrive-процессор;
 | 10 — процессор для использования в двухпроцессорных системах)

Таблица 8.2., Значения бит 4...7 и 8...11 регистра ЕАХ

Биты ЕАХ (8...11) | Биты ЕАХ (4...7) | Тип процессора
0100 | 0000 или 0001 | I486DX
0100 | 0010 | I486SX
0101 | 0010 | Pentium 75-200
0101 | 0100 | Pentium MMX 166-200
0110 | 0001 | Pentium Pro
0110 | 0011 | Pentium II, модель 3
0110 | 0101 | Pentium II, модель 5,
 |  | Pentium II Xeon
0110 | 0110 | Celeron, модель 6
0110 | 0111 | Pentium III и Pentium HI
 |  | Xeon
0110 | 0011 | Pentium II OverDrive

Таблица 8.3. Поля регистра EDX после выполнения команды CPUID (при ЕАХ=n)

Биты EDX | Назначение (если биты установлены)
0 | Присутствует сопроцессор с набором команд i387
1 | Поддержка расширенных возможностей обработки прерываний в режиме виртуального процессора i8086
2 | Процессор поддерживает точки прерывания ввода-вывода (точки останова по обращению к портам) для предоставления расширенных возможностей отладки и доступ к регистрам DR4 и DR5. Флаг CR4.DE=1
3 | Процессор поддерживает 4-мегабайтные страницы
4 | Поддержка счетчика меток реального времени TSC
5 | Поддержка команд RDMSR и WRMSR для работы с модельно-зависимыми регистрами
6 | Процессор поддерживает физические адреса, большие, чем 32 бита, расширенный формат элемента таблицы страниц, дополнительный уровень трансляции страничного адреса и 2-мегабайтные страницы
7 | Поддержка исключения 18 — машинного контроля
8 | Поддержка инструкции CMPXCHG8B
9 | Микропроцессор содержит программно-доступный контроллер прерываний APIC
10 | Резерв
11 | Поддержка инструкций SYSENTER и SYSEXIT быстрых системных вызовов
12 | Поддержка регистра управления кэшированием MTRR_CAP (относится к MSR-регистрам)
13 | Поддержка работы с битом G, определяющим глобальность страницы в PTDE и РТЕ. Бит CR4.PGE = 1
14 | Поддержка архитектуры машинного контроля (MSR-регистр MCG_CAP)
15 | Поддержка инструкций CMOV, FCMOVCC, FCOMI условной пересылки
16 | Поддержка инструкций CMOVCC, FMOVCC и FCOMI (если установлен бит 0)
17 | Процессор поддерживает 36-разрядную физическую адресацию с 4-мегабайтными страницами
18 | Процессор поддерживает собственную идентификацию по уникальному 96-битному номеру и эта поддержка активна
19-22 | Резерв
23 | Поддержка целочисленного MMX-расширения
24 | Процессор поддерживает команды FXSAVE и FXRSTOR
25 | Поддержка MMX-расширения с плавающей точкой
24-31 | Резерв

Таблица 8.4. Значения дескрипторов, описывающих кэш-память, и дескрипторов TLB

Содержание дескриптора | Значение
00h Olh | Нулевой дескриптор Буфер TLB-команд: размер страницы — 4 Кбайт, ассоциативный 4-канальный, 32 входа
02h | Буфер TLB-команд: размер страницы — 4 Кбайт, ассоциативный, 2 входа
03h | Буфер TLB-данных: размер страницы — 4 Кбайт, ассоциативный 4-канальный, 64 входа
04h | Буфер TLB-данных: размер страницы — 4 Кбайт, ассоциативный 4-направленный, 8 входов
06h | Кэш команд: размер 8 Кбайт, наборно-ассоциативный 4-канальный, длина строки 32 байта
08h | Кэш команд: размер 16 Кбайт, наборно-ассоциативный 2-канальный, длина строки 32 байта
Oah | Кэш данных: размер 8 Кбайт, ассоциативный 2-направленный, длина строки 32 байта
Och | Кэш данных: размер 16 Кбайт, ассоциативный 2-или 4-направленный, длина строки 32 байта
40h | Кэш-память второго уровня (L2) отсутствует
41h | Объединенный кэш: размер 128 Кбайт, наборно-ассоциативный 4-канальный, длина строки 32 байта
42h | Объединенный кэш: размер 256 Кбайт, наборно-ассоциативный 4-направленный, длина строки 32 байта
43h | Объединенный кэш: размер 512 Кбайт, наборно-ассоциативный 4-направленный, длина строки 32 байта
44h | Объединенный кэш: размер 1 Мбайт, наборно-ассоциативный 4-направленный, длина строки 32 байта
45h | Объединенный кэш: размер 2 Мбайт, наборно-ассоциативный 4-нанравлеиный, длина строки 32 байта

Рассмотрим подробнее средства для мониторинга производительности, которые включают счетчик меток реального времени TSC (Time Stamp Counter) и счетчики событий CTRO, CTRL

Использование счетчика меток реального времени TSC

Счетчик меток реального времени TSC (Time Stamp Counter) — регистр, содержимое которого инкрементируется с каждым тактом процессорного ядра. Каждый раз при аппаратном сбросе (сигналом RESET) отсчет в этом счетчике начинается с нуля. Разрядность регистра обеспечивает счет без переполнения в течение сотен лет. Счетчик продолжает счет как при исполнении инструкции HLT, так и при остановке процессора по сигналу STPCLK# (для энергосбережения). Чтение счетчика обеспечивает инструкция RDTSC, установкой бита CR4.TSD ее можно сделать привилегированной (доступной лишь при CPL = 0). Чтение и запись TSC возможны также по инструкциям обращения к MSR (при CPL ¦ 0), причем запись может выполняться только в младшие 32 бита, а старшие биты при операции записи обнуляются. Присутствие счетчика TSC определяется по инструкции CPUID (ЕАХ = 1), Если в результате ее вызова бит 4 регистра EDX равен 1, то процессор поддерживает счетчик меток реального времени TSC.
Команда RDTSC (ReaD from Time Stamp Counter — чтение 64-разрядного счетчика меток реального времени TSC (Time Stamp Counter)) не имеет операндов. Машинный код этой команды — Of 31. Команда проверяет состояние второго бита регистра CR4.TSD (Time Stamp Disable — отключить счетчик меток реального времени):

  • если CR4.TSD = 0, то выполнение команды RDTSC разрешается на любом уровне привилегий;
  • если CR4. TSD = 1, то выполнение команды RDTSC разрешается только на нулевом уровне привилегий.

Если после данной проверки выполнение команды разрешено на текущем уровне привилегий, то выполняется сохранение значения 64-битного MSR-счетчика TSC в паре 32-битных регистров EDX: ЕАХ. Если выполнение команды запрещено, то работа команды заканчивается.
Разработаем две макрокоманды, использование которых позволит нам получать количественную оценку работы кода, начиная от полной программы и заканчивая отдельной командой. Эти макрокоманды не будут отличаться компактностью, но этого от них и не требуется, так как они являются лишь средством оценки эффективности, используемым на этапе отладки и в конечном коде их наличие не требуется.
На взгляд автора, применение этих макрокоманд должно подчиняться следующему алгоритму. Вызов первой макрокоманды, назовем ее profileMn, должен зафиксировать момент, относительно которого будет производиться отсчет тактов процессора, то есть в начале профилируемого участка программы. Вызов второй макрокоманды prof i I er_out должен зафиксировать момент окончания работы на этом участке программы. Необходимо иметь в виду, что это «грязное» время работы программы, по которому можно производить только приблизительную оценку ее скорости работы. Для этого есть внутренняя и внешние причины. Внутренняя причина заключается в том, что полученная величина включает время, затраченное на работу некоторых команд, составляющих тело самой макрокоманды. Этот недостаток исправить легко. Что касается внешних причин, то они объективны по отношению к программе пользователя и мешают получению истинного времени профилирования. В качестве таких внешних причин могут быть программы операционной системы, которые могут приостанавливать на время программу пользователя. Внешней причиной является и отладчик, так как при работе в нем вообще нет смысла производить оценку скорости.
Ниже приведены макрокоманды profiler_in и profiler_out с тестовым примером для проверки их работы. Данные макрокоманды производят корректировку результата своей работы, с тем чтобы исключить обсужденные выше внутренние причины «грязного» времени работы программы. Заметим, что не всякий транслятор ассемблера «знает» о новых командах микропроцессора, в том числе и о команде RDTSC. По этой причине мы ее моделируем, инициализируя в сегменте кода 2 байта значениями машинного кода этой команды db 0fh,31h.

:prg08_01.asm - программа демонстрации использования макрокоманд profiler_in ;и profiler out для оценки производительности фрагментов кода.
bin_dec_fpu macro string_bin_qword:REQ. string_pack:REQ. adr_string_pack:REQ, len_ ~string_pack:REQ, ad^string^EQ. len_string:REQ
local cycl
макрокоманда вывода на консоль десятичного числа:
;на входе:
;string_bin_qword - адрес 64-битной ячейки (описанной dt) с преобразуемым
:двоичным целым числом
:string_pack - адрес 80-битной ячейки (описанной dt), в которую сохраняется
упакованное 10-е значение
:adr_string_pack - ячейка с адресом string_pack (описанной dd)
:len_string_pack - длина string_pack
:adr_string - ячейка с адресом string (описанной dd). В string помещаются
;символы десятичных цифр для вывода.
:len_string - размер string (18 байт)
:.........преобразуем bin->dec
finit
f 11d string_bin_qword :заносим в сопроцессор двоичное целое число fbstp string_pack извлекаем упакованное десятичное
¦-------------распакуем..........
Ids si.adr_string_pack
add si.len_string_pack-2 :на конец string_pack (18 упак. дес. цифр)
les di.adr_string
movcx.9 ;9 пар упакованных десятичных цифр
xor ax,ax cycl: xor ax,ax
std ;string_pack обрабатываем с конца
lodsb ;в al очередные 2 упакованные десятичные цифры
распаковываем - аИ=младшая, а1=старшая
shi ax.4
rol al .4
or ах.ЗОЗОп :преобразуем в символьное представление
xchg ah .al :ап-младшая. al-старшая
eld :в string записываем с начала
stosw
loop cycl
;.........выводим на консоль...............................
' mov bx.l стандартный дескриптор - экран
mov cx.len_string
Ids dx.adr_string -.формируем указатель на строку string
mov ah.40h :номер функции DOS
int 21h :выводим
jc exit ;переход в случае ошибки
endm
profileMn macro val_l:REQ
;val_l - ячейка памяти 64 бита (2x32) для сохранения момента :начала профилирования ("грязного") pushad сохранение всех регистров общего назначения в стеке
db Ofh.31h;RDTSC mov val_l+4.edx
mov va!_l.eax popad восстановление всех регистров общего назначения из стека
endm
profiler_out macro val_l:REQ. val_2:REQ
:val_l - ячейка памяти 64 бита (2x32). 8 которой при входе в макрос сохранен :момент начала профилирования командой profilerjn. Далее в макросе эта ячейка :содержит результат профилирования - число тактов процессора :val_2 - ячейка памяти 64 бита (2x32). в которой сохраняется момент ("грязный") окончания профилирования
pushad сохранение всех регистров общего назначения в стеке db 0fh,31h :RDTSC - окончание профилирования
mov val_2+4.edx
mov val_2,eax
профилируем pushad и popad с учетом двух shrd pushad popad
db 0fh.31h;RDTSC
:теперь необходимо получить чистое время профилирования, для чего результат необходимо скорректировать (уменьшить) на количество тактов процессора, :потребное для выполнения пар команд PUSHAONPOPAD и M0V
subeax, val_2
jnc $+4 :учет заема из старшего разряда
dec edx
subedx. val_2+4 ;в edx:eax кол-ва тактов для выполнения 2-х команд ;PUSHAD\POPAD и 2-х SHRD
mov eax,val_l
sub val_2.eax
mov eax.val_l+4
jnc $+7 :учет заема из старшего разряда при выполнении предыдущего вычитания
dec val_2+4
sub val_2+4,eax
;в val_2:val_2+4 - чистое количество тактов процессора для выполнения профилируемого участка
popad восстановление всех регистров общего назначения из стека
:выводим
bin_dec_fpu val_2_q. string_pack, adr_string_pack, len_string_pack. adr_string. len_string
endm .data
val_2 label dword val_2_q dq 0 val_l label dword
dq 0
:b string_pack исходное значение из val_2_q в упакованном десятичном формате string_pack dt О
len_string_pack=$-string_pack adr_stringpack dd string_pack
string db 18 dup (0) максимальный результат состоит из 18 десятичных цифр
len_string=$-string adr_string dd string
.code
профилируем выполнение команд работы со стеком profiIer_i n val_l
push eax
pop eax
profiler_out val_l. val_2 exit: :выход из программы

Составьте тестовые примеры и «поиграйтесь» с данной программой. Обратите внимание, что при задании пустой последовательности команд между парой макросов profilerjn и profi1er_out все равно получается некоторая величина профилирования. Она постоянна, ее источник — сами команды RDTSC, которые требуют тактов процессора для своего исполнения. Эту величину можно скорректировать разными способами, но можно и не трогать, а учитывать при подведении окончательных результатов тестирования нужного вам фрагмента кода. На компьютере автора эта величина равна 52.
В этой программе для визуализации результатов профилирования разработана макрокоманда bi n_dec_fpu, производящая перевод значения из двоичной системы в десятичную.


Вычисление CRC

Продавай мобильный контент со своего сайта

Вычисление CRC

  • CRC-арифметика
  • Прямой алгоритм вычисления CRC
  • Табличные адгоритмы вычисления CRC
    • Основы
    • Прямой табличный алгоритм CRC16
    • Прямой табличный алгоритм CRC32
    • "Зеркальный" табличный алгоритм CRC32

Где начало того конца, которым оканчивается начало?
Козьма Прутков

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

  • Посимвольный контроль четности, называемый также поперечным (рис. 9.1), подразумевает передачу с каждым байтом дополнительного бита, принимающего единичное значение по четному или нечетному количеству единичных битов в контролируемом байте. Может использоваться два типа контроля четности — на четность и нечетность. Алгоритм вычисления контрольного бита при контроле на четность предполагает его установку таким образом, чтобы общее количество бит в битовой последовательности (включая и сам бит четности) было четным. И наоборот, контроль на нечетность предполагает установку контрольного бита так, чтобы общее количество битов в битовой последовательности (включая и сам бит четности) было нечетным. Посимвольный контроль четности прост как в программной, так и в аппаратной реализации, но его вряд ли можно назвать эффективным методом обнаружения ошибок, так как искажение более одного бита исходной последовательности резко снижает вероятность обнаружения ошибки передачи. Этот вид контроля обычно реализуется аппаратно в устройствах связи.
  • Поблочный контроль четности, называемый продольным. Схема данного контроля (рис. 9.2) подразумевает, что для источника и приемника информации заранее известно, какое число передаваемых символов будет рассматриваться ими как единый блок данных. В этой схеме контроля для каждой позиции разрядов в символах блока (поперек блока) рассчитыва-
    ются свои биты четности, которые добавляются в виде обычного символа в конец блока. При этом схема выполнения продольного контроля по-прежнему предполагает наличие у каждого из этих символов дополнительного бита четности (см. выше). По сравнению с посимвольным контролем четности поблочный контроль четности обладает большими возможностями по обнаружению и даже корректировке ошибок передачи, но все равно ему не удается обнаруживать определенные типы ошибок. Кроме того, этот вид не реализуется эффективно в аппаратных решениях, в силу чего редко используется. Главное его достоинство в том, что с него можно начинать рассмотрение идеи обнаружения ошибок на уровне блоков передаваемой информации.

Рис. 9.1. Схема выполнения посимвольного контроля четности при передаче информации

Рис. 9.2. Схема выполнения поблочного контроля четности при передаче информации

  • Вычисление контрольных сумм. В отличие от рассмотренных выше методов для метода контрольных сумм нет четкого определения алгоритма. Каждый разработчик трактует понятие контрольной суммы по-своему. В простейшем виде контрольная сумма — это арифметическая сумма двоичных значений контролируемого блока символов. Но этот метод обладает практически теми же недостатками, что и предыдущие, самый главный из которых — нечувствительность контрольной суммы к четному числу ошибок в одной колонке и самому порядку следования символов в блоке.
  • Контроль циклически избыточным кодом — CRC (Cyclical Redundancy Check). Это гораздо более мощный и широко используемый метод обнаружения ошибок передачи информации. Он обеспечивает обнаружение ошибок с вероятностью до 99 %. Кроме того, этот метод обладает рядом других полезных моментов, которые могут найти свое воплощение в практических задачах. Рассмотрению этого метода и будет посвящено дальнейшее изложение.

Cама по себе аббревиатура «CRC» знакома многом пользователям компьютера, особенно тем, кому приходится часто переносить свои архивные файлы посредством дискеты. В один прекрасный день попытка распаковки архивного файла наверняка приводила к выводу на экран сообщения о том, что у этого файла какое-то неправильное значение CRC. Что же представляет собой это загадочное значение CRC, какую пользу можно извлечь из знаний о нем? В данном разделе мы постараемся с этим разобраться, тем более, что данная задача является хорошей возможностью очередной раз продемонстрировать возможности и преимущества ассемблера по обработке данных на уровне битов, строк битов, последовательности байт (в том числе и неопределенной длины).
CRC (Cyclic Redundancy Code) — последовательность бит, полученная по определенному алгоритму на основании другой (исходной) битовой последовательности. Главная особенность (и практическая значимость) значения CRC состоит в том, что оно однозначно идентифицирует исходную битовую последовательность и поэтому используется в различных протоколах связи, таких, как HDLC и ZMODEM, а также для проверки целостности блоков данных, передаваемых различными устройствами. В силу этих свойств алгоритм вычисления CRC часто реализуется на аппаратном уровне. Если взять пример с архиватором, то его работа в общем случае заключается в следующем: архиватор упаковывает файлы в соответствии с некоторым алгоритмом архивации, вычисляя для каждого упаковываемого файла значение CRC. После этого заархивированные файлы могут множество раз копироваться, пересылаться по сети, в том числе с использованием электронной почты, и т. д. В процессе своих путешествий файл может столкнуться с различными неприятными воздействиями внешней среды, например, с неисправным дисководом, искажением его внутреннего содержимого во время передачи по сети и т. п. Эти изменения не обязательно должны быть глобальными, они могут касаться всего одного бита. Когда приходит время, пользователь распаковывает архив, при этом архиватор в первую очередь проверяет целостность файлов в нем. Для этого архиватор опять по содержимому файла вычисляет его CRC и сравнивает полученное значение с тем значением CRC, которое было вычислено при упаковке файла. Если они равны, то считается, что целостность файла не была нарушена, и он распаковывается, в обратном случае, если новое и старое значения CRC не совпадают, то считается, что архивный файл поврежден, и процесс его распаковки завершается. Необходимо отметить, что CRC не обязательно вычислять для больших массивов данных, каким является файл. Его можно вычислять и для отдельных строк текста и даже слов с целью организации простейшего контроля целостности и отождествления символьных (числовых) последовательностей. Более того, из рассмотрения ниже вы увидите, что алгоритм вычисления CRC, по сути, является еще одним методом хэширования, которые мы подробно рассматривали в разделе «Таблицы с вычисляемыми входами» главы 2. Таким образом, алгоритм вычисления CRC имеет много досто-
инств, которые могут найти применение в самых различных практических задачах.
Основная идея вычисления CRC заключается в следующем. Исходная последовательность байтов, которой могут быть и огромный файл, и текст размером несколько слов и даже символов, представляется единой последовательностью битов. Эта последовательность делится на некоторое фиксированное двоичное число. Интерес представляет остаток от этого деления, который и является значением CRC. Все, что теперь требуется, — это некоторым образом запомнить его и передать вместе с исходной последовательностью. Приемник данной информации всегда может таким же образом выполнить деление и сравнить его остаток с исходным значением CRC. Если они равны, то считается, что исходное сообщение не повреждено, и т. д. Но этот лишь общая схема. Реальный алгоритм вычисления CRC использует особые правила арифметики, в соответствии с которыми производятся все вычисления, назовем их правилами CRC-арифметики. В силу принципиальной важности этих правил для нашего изложения коротко рассмотрим их отличия от правил обычной двоичной арифметики.

CRC-арифметика

Расчеты CRC ведутся в двоичной системе счисления. При проведении CRC-вы-числений используется специальная CRC-арифметика, которая, по сути, является полиномиальной арифметикой по модулю 2. Полиномиальная арифметика по модулю 2 — это еще один из видов арифметик, используемых для решения задач в определенной предметной области и отличающихся от привычной двоичной арифметики с циклическим переносом отсутствием переносов и вычислением всех коэффициентов по модулю 2 . В уроке 20 «ММХ-технология микропроцессоров Intel» учебника при рассмотрении ММХ-команд мы знакомились с одной из таких альтернативных арифметик — арифметикой с насыщением. Теперь рассмотрим особенности CRC-арифметики.
Итак, как отмечено выше, в основе CRC-арифметики лежит полиномиальная арифметика. По определению, полином — линейная комбинация (сумма) произведений целых степеней заданного набора переменных с постоянными коэффициентами. Частный случай — полином, содержащий одну переменную:

u(x)=unxn+...u,x1+uoxo.

Здесь un, nu щ — элементы некоторой алгебраической системы S, называемые коэффициентами; х — переменная полинома, которую можно рассматривать как формальный символ без определенного значения. Алгебраическая система S обычно представляет собой множество целых или рациональных чисел в диапазоне 0..m-1 со сложением, вычитанием и умножением, выполняемыми по модулю т. Для нашего рассмотрения особенно важна полиномиальная арифметика по модулю 2, в которой каждый коэффициент полинома равен одному из двух значений — 0 или 1. Например, шестнадцатеричное значение ОеЗп может быть представлено следующим полиномом:

1х27 + 1х26 + 1х25 + 0х24 + 0х23 + 0х22 + 1x21 + 1x2°.

Если ввести в качестве переменной х=2, то получим следующий двоичный полином:

1хx7 + 1xx6 + 1xx5 + 0хх4 + 0xx3 + 0xx2 + 1xx1 + 1хх°.

В этом полиноме, строго говоря, значение х не играет особой роли, так как данное двоичное число можно представить полиномом в другой системе счисления, например, шестнадцатеричной: ехх1 + 2хх°, где х = 16. При этом заметим, что в том и другом случае цифры 0, 1, е, 2 — это просто цифры двоичной и шестнадцатеричной систем счисления.
Так как слагаемые двоичного полинома с нулевыми коэффициентами не дают никакого вклада в конечный результат, то их можно попросту отбросить, оставив только слагаемые, переменные которых имеют единичные коэффициенты:

1xx7 + 1xx6 + 1xx5 + 0xx4 + 0xx3 + 0xx2 + 1xx1 + 1хх° -
= 1xx7 +1xx6 + lxx5 + 1xx1+ 1xx0 = = x7 + x6 + x5 + x1 + x°.

Здесь x = 2. Над полиномами можно выполнять арифметические операции: сложение, умножение и вычитание. Процессы выполнения этих операций для полиномиальной арифметики и обычной арифметики многократной точности (см. главу 1) похожи. Главное отличие в том, что из-за отсутствия связи между коэффициентами полинома понятие переноса в полиномиальной арифметике отсутствует.
Например, для умножения 7h (0111b) на 5h (0101b) выполняются действия:

(х2 + х1 + х°)х(х2 + х°) = (х4 + х3 + х2 + х2 + х1 + х°) -=х4 + х3 + х2 + х2 + х1 + х° + х°= х4 + х3 + 2х2 + х1 + 2х°.

Для того чтобы получить в ответе 35, мы должны х взять равным 2 и привести коэффициенты у членов полинома 2х° и 2х2 к двоичным, сделав перенос соответствующих единиц в старшие разряды: 01010 переносы 11111 результат
100011 = 35)0.
В результате получим двоичный полином х5+х'+х°.
Эти рассуждения призваны сформулировать очередной тезис о том, что переносы, как и в обычной арифметике, можно выполнять в случае, когда известно основание системы счисления. То есть до тех пор, пока мы не знаем х, мы не можем производить и переносы. В приведенном выше примере, мы не знаем, что 2х2 и 2х° на самом деле являются х3 и х1 до тех пор, пока не известно, что х = 2. То есть в полиномиальной арифметике коэффициенты при разных степенях изолированы друг от друга и отношения между ними не определены. Из-за этого возникает первая странность полиномиальной арифметики — операции сложения и вычитания в ней абсолютно идентичны и вместо них можно смело оставлять одну. Например, сложение по правилам полиномиальной арифметики по модулю 2, будем ее далее называть CRC-арифметикой, будет выполнено так:

11111011
+ 11001010
00110001

Из этого примера видны правила сложения двоичных разрядов в арифметике t с отсутствием переносов:

0+0=0; 0+1 = 1; 1+0=1; 1 + 1=0.

Операцию вычитания демонстрирует следующий пример:

 | 11111011
- | 11001010
 | 00110001

Правила выполнения вычитания в арифметике с отсутствием переносов:

0-0=0; 0-1=1; 1-0=1; 1-1=0.

Сравнение примеров для сложения и вычитания полиномов по модулю 2, а также правил, по которым они выполняются, показывает то, что эти две операции CRC-арифметики идентичны и по принципу формирования результата они аналогичны команде ассемблера XOR. Цель, которой достигают всеми этими условностями, — исключить из поля внимания все величины (путем заемов/перено-сов), лежащие за границей старшего разряда.
Умножение в арифметике с отсутствием переносов также выполняется с учетом особенностей CRC-сложения:

 |  |  | 1101
 |  | x | 1011 -----
 |  |  | 1101
 |  | 1101 |
 | 0000 |  |
1101 |  |  |
1111111

Видно, что в самом умножении особенностей нет, а вот сложение промежуточных результатов производится по правилам CRC-сложения.
Для нашего рассмотрения особый интерес представляет операция деления, так как в основе любого алгоритма вычисления CRC лежит представление исходного сообщения в виде огромного двоичного числа, делении его на другое двоичное число и использовании остатка от этого деления в качестве значения CRC. Деление — самая сложная из операций CRC-арифметики. Здесь необходимо ввести так называемое слабое понятие размерности: число X больше или равно числу Y, если оба числа имеют одинаковую размерность и позиции их старших битов единичны, то есть соотношение остальных битов X и Y для операции сравнения не имеет значения. Рассмотрим примеры операции деления, причем для лучшего понимания сделаем это в двух вариантах: вначале вспомним, как выполняется обычное деление (по правилам двоичной арифметики с циклическим переносом), а затем выполним собственно CRC-деление.
Возьмем произвольную двоичную последовательность и разделим ее на некоторое число по правилам двоичной арифметики с циклическим переносом
(рис. 9.3).
По правилам CRC-арифметики деление для приведенных выше исходных данных даст следующий результат (рис. 9.4).

Рис. 9.3. Схема вычисления двоичного деления (с циклическим переносом)

Рис. 9.4. Схема вычисления CRC-деления

Как видите, результаты обычного и CRC-делений отличаются. Главное, чтобы вы подметили особенность выполнения CRC-деления. Особое внимание обратите на порядок формирования промежуточных результатов в процессе деления. Снос двоичных разрядов из делимого продолжается до тех пор, пока размерности промежуточного битового значения и делителя не сравняются, а их старшие разряды не станут равными 1. После этого над двоичными разрядами выполняется побитовое CRC-вычитание. Соотношение разрядов не важно, главное — это одинаковая размерность и единичные старшие биты. В этом случае считается, что вычитаемое больше вычитаемого, что влечет за собой включение 1 в частное и выполнение операции CRC-вычитания. Это как раз и есть проявление слабого
понятия размерности. Корзина для мусора, показанная на рис. 9.4, означает тот факт, что частное для процесса вычисления CRC-битовой последовательности не имеет никакого значения.
Реальные двоичные последовательности являются результатом сцепления порой огромного количества отдельных байтов (символов), образуя одно большое двоичное число, для представления которого нужно использовать двоичные полиномы огромных степеней. При этом каждый бит в подобной последовательности произвольной длины представляется в виде коэффициента длинного полинома. Например, для представления 128-разрядного блока необходим полином, состоящий из 1024 слагаемых, а для 1024-битного блока необходим полином уже с 8192 слагаемыми. В терминах полиномиальной арифметики двоичное число, сформированное в результате подобной сцепки составляющих блока данных, называется полиномом данных {сообщения) и обозначается как D(x) [5, 44]. В алгоритме вычисления CRC вводится еще несколько полиномов и соотношений между ними:

  • порождающий полипом G(x) — предварительно особым образом выбранный полином, на который делится исходный полином сообщения;
  • полином-частное Q(x) — полином, получившийся в качестве частного от деления полиномов D(x)/G(x);
  • полином-остаток R(x) — полином, получившийся в качестве остатка от деления полиномов D(x)/G(x).

Между перечисленными полиномами существуют следующие отношения:
D(x)=Q(x)xG(x)+R(x), Q(x)=(D(x)-R(x))/G(x).
Эти соотношения приводят к следующим основополагающим для дальнейшего рассмотрения тезисам:

  • операция деления двух двоичных полиномов D(x)/G(x), где G(x)*0, дает в качестве результата полином-частное Q(x) и полином-остаток R(x), удовлетворяющие условиям: D(x)=Q(x)xG(x)+R(x);
  • остаток от деления двух полиномов R(x) является двоичным числом, которое после вычитания из D(x) дает в результате еще один полином, делящийся без остатка на G(x); получающееся в результате этого деления частное Q(x) отбрасывается за ненадобностью, а полином-остаток R(x) на-- зывают CRC (Cyclic Redundancy Code).

Из приведенного выше описания общей схемы вычисления CRC возникает ряд вопросов: что представляет собой этот магический делитель G(x), каков его размер? Выбор порождающего полинома G(x) — достаточно сложная задача. Перечислим некоторые важные свойства, которые должны учитываться при этом.

  • Число разрядов (количество членов) в полиноме-остатке R(x) непосредственно определяется длиной самого порождающего полинома G(x). Выбор G(x) длиной п гарантирует, что полином-остаток от деления R(x) будет иметь разрядность не более, чем п-1. Это следует из общего свойства операции деления, которое предполагает, что остаток от деления должен быть меньше делителя.
  • Порождающий полином G(x) должен быть полиномиально простым. Это означает его неделимость нацело на полиномы со значением в диапазоне от 2 до самого себя.
  • Способность порождающего полинома G(x) к выявлению ошибок, специфичных для передачи данных по каналами связи. Это такие ошибки, как ошибки в одном, двух, нечетном количестве битов, а также ошибки блока битов. Выше мы отмечали, что более простые методы обнаружения ошибок передачи не способны обнаружить с достаточной степенью вероятности большинство таких типов ошибок.

Для большинства алгоритмов вычисления CRC значение порождающего полинома известно заранее и, более того, утверждено соответствующими стандартами. Поэтому программисту без особой надобности не стоит терять времени и сил на изобретение нового значения порождающего полинома G(x). Приведем значения некоторых широко известных полиномов, используемых на практике.

  • х'6+х12+х5+х° — полином 1021h, используемый в протоколе XMODEM и производных от него протоколах передачи данных. Он стандартизован в спецификации V.41 МККТТ «Кодонезависимая система контроля ошибок».
    Ш х16+х|5+х2+х° — полином 8005h, используемый в протоколе двоичной синхронной передачи фирмы IBM. Он также стандартизован в приложении А «Процедура коррекции ошибок для оконечного оборудования линии с использованием асинхронно-синхронного преобразования» к стандарту V.42 МККТТ. Этот же полином широко известен как полином, используемый в алгоритме вычисления CRC — CRC16.
  • x32+x26+x23+x22+x16+x12+x"+xlo+xs+x7+x5+x4+x2+x1+x0 - полином 04clldb7h, используемый в алгоритме вычисления CRC — CRC32 и также стандартизованный в стандарте V.42 МККТТ. Этот полином, в частности, используется в технологии локальных вычислительных сетей Ethernet. Необходимо отметить, что вычисление по алгоритму CRC32 зачастую проводят и с другим полиномом: 0edb88320h:
  • x32+x31+x30+x29+x27+x26+x24+x23+x21+x20+ +х19+х15+х9+х8+х5. Последний полином используют различные архиваторы. Необходимо заметить, что полином 0edb88320h — это просто зеркальное отражение полинома 04clldb7h.

В заключение отметим, почему выгодно увеличивать число разрядов CRC. Выше уже было отмечено, что алгоритм вычисления CRC по сути своей является одним из возможных (и неплохих) алгоритмов хэширования. Разрядность порождающего полинома G(x) 16 бит обеспечивает до 65 535 значений хэш-функции. Увеличение разрядности полинома G(x) до 32 битов приводит к расширению набора значений хэш-функции уже до 4 294 967 295.
С полиномом связано еще одно понятие — степени полинома, которое по определению является номером позиции его старшего единичного бита (считая с нуля). Например, для полинома 1011 из приведенного выше примера (см. рис. 9.4) степень равна 3.
В качестве выводов отметим, что CRC-арифметика отличается от двоичной отсутствием переносов/заемов, а CRC-вычитание и сложение выполняются по
тем же правилам, что и команда ассемблера XOR, что и обусловливает ее важную роль при вычислении значений CRC.

Прямой алгоритм вычисления CRC

Даже маленькая практика стоит большой теории.
Закон Букера (Прикладная Мерфология)

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

  1. Выбрать полином Р, в результате автоматически становится известной его степень N.
  2. Добавить к исходной двоичной последовательности N нулевых битов. Это добавление делается для гарантированной обработки всех битов исходной последовательности.
  3. Выполнить деление дополненной N нулями исходной строки S на полином Р по правилам CRC-арифметики. Запомнить остаток, который и будет являться CRC.
  4. Сформировать окончательное сообщение, которое будет состоять из двух частей: собственно сообщения и добавленного в его конец значения CRC.

К примеру, вычисление по этому алгоритму CRC для исходной последовательности 1101001110010110100 (см. рис. 9.4) и сама окончательная последовательность на стороне источника будут выглядеть так, как показано на рис. 9.5.
Из рисунка видно, что в начале вычисления исходная последовательность 1101001110010110100 дополняется нулями в количестве, равном степени полинома (Р=1011 - степень полинома N=3): 1101001110010110100+000. При выполнении CRC-деления эти дополнительные биты гарантируют, что все биты исходной последовательности примут участие в процессе формирования значения CRC. Результирующая последовательность получается равной исходной последовательности, дополненной значением CRC: 1101001110010110100+011. Заметим, что длина присоединяемого к исходной последовательности значения CRC должна быть равна степени полинома, даже если CRC, как в нашем случае, имеет ведущие нули. Это очень важный момент, понимание которого является ключом к пониманию сути процессов, происходящих на стороне приемника при получении и определении целостности исходного сообщения. Действия алгоритма для приемника просты — выполнить деление полученной последовательности на полином. При этом для выполнения деления нет необходимости дополнять исходную последовательность нулями, тем более что на практике соблюдение этого условия крайне неудобно. Приемник попросту выполняет CRC-деление полученной исходной строки (дополненной в конце
исходным значением CRC) на полином и анализирует остаток. Если остаток от этого деления нулевой, то исходная последовательность не была нарушена во время передачи. В обратном случае существует очень большая вероятность нарушения целостности исходной последовательности, и нужно принимать дополнительные меры по выяснению и исправлению ситуации. Одной из таких мер может быть попытка восстановления нужного значения CRC.

Рис. 9.5. Схема формирования выходного сообщения из исходного с использованием CRC-алгоритма

Описанный выше алгоритм вычисления значения CRC называется прямым и чаще всего реализуется аппаратно. Но, тем не менее, для совершенствования навыков программирования на ассемблере составим реализующий его пример программы. Хотя эффективность этой программы не слишком высока, у нее есть две учебные цели:

  • показать в виде программной реализации суть алгоритма вычисления CRC и самого CRC-деления;
  • подготовить себя к пониманию более совершенных алгоритмов расчета CRC, к которым относится, в частности, рассматриваемый ниже табличный алгоритм.

Для компьютерной реализации алгоритмов вычисления CRC удобно выбирать полиномы со степенями, кратными 8 (то есть размерности регистров) — 8, j5, 24, 32 или даже 64. В этом случае можно подобрать команды из системы команд микропроцессора, наиболее оптимально реализующие алгоритмы вычисления CRC. В качестве полинома выберем один из рекомендуемых полиномов (см. ниже) — 4003. И еще одно важное замечание — степень полинома определяет размерность регистра, используемого в алгоритме, при этом считается, что старший (всегда единичный) бит полинома находится сразу за левой границей регистра. В этих условиях программа реализации прямого алгоритма вычисления CRC функционирует следующим образом (для лучшего понимания в процессе разбора алгоритма см. рис. 9.6). В регистр побитно вдвигаются биты исходной строки. Это происходит до тех пор, пока при очередном сдвиге слева появится единичный бит. В этом случае все содержимое регистра подвергается операции XOR со значением полинома без старшего бита. Далее процесс сдвига и анализа выдвигаемого бита продолжается до тех пор, пока не будет выдвинут очередной единичный бит, в результате чего опять между регистром и полиномом выполняется операция XOR, и т. д. После того как последний бит вдвинут в регистр, в него вдвигается количество нулевых битов, равное степени полинома. Этим, как мы не раз уже отмечали, достигается участие всех бит исходной битовой строки в формировании значения CRC. В результате в регистре остается значение CRC, которое необходимо добавить к исходной строке и передать приемнику.

jnc m5 ;старшие разряды не равны - выполняем сдвиг (частное нас не интересует)
;старшие разряды равны - выполняем XOR:
хог eax.ebx;eax(31. .16) XOR pollnom т5: loop m4
В результате вычисления CRC символьной последовательности "6476с8" получим CRC • 35dah.

Рис. 9.6. Схема вычисления значения CRC прямым алгоритмом

Для того чтобы смоделировать действия стороны приемника, можно использовать ту же самую программу со слегка измененными исходными данными — к строке bitstring добавляем вычисленное значение CRC. После этого под отладчиком наблюдаем за процессом CRC-деления, причем контролируем остаток от деления. В определенный момент увидим, что он стал нулевым — это свидетельствует о том, что исходная последовательность не была изменена. Для эксперимента можно изменить значения одного или более битов исходной последовательности и посмотреть, что получится.

;prg09_02.asm - программа демонстрации прямого алгоритма вычисления CRC ;(сторона-приемник).
.data
исходная битовая последовательность в символах
bit_string db "6476c8",35h.0dah
1en_bit_stri ng=$-bi t_stri ng
adr_bit_string dd bit_string
polinomdw 4003h
.code
main:
:см. предыдущую программу
exit: :выход из программы

Очевидный недостаток прямого метода — большое количество операций сдвига, исключающих операций ИЛИ (XOR) и операций условного перехода, которые выполняются для каждого бита исходного сообщения. Поэтому на практике используется другой способ расчета CRC, называемый табличным.

Табличные алгоритмы вычисления CRC

Основы

Для того чтобы лучше понять суть табличного алгоритма вычисления CRC, обратимся опять к прямому методу, точнее к той схеме его вычисления (см. рис. 9.6), которая была реализована в приведенной выше программе.
Из этой схемы видно, что для текущего содержимого старшей половины регистра ЕАХ можно прогнозировать, как будет изменяться содержимое его битов по мере их сдвига. Для этого достаточно подвергнуть анализу его биты начиная с самого старшего. Допустим, что старшие 8 бит ЕАХ равны а7 а6 а5 а4 а3 а2 а, а,,. При следующем сдвиге (см. рис. 9.6) прямой алгоритм определяет, будет ли произведена операция XOR операнда с полиномом b7 b6 b5 b4 Ь3 b2 b, bо в ВХ (а7=1) или нет (а7=0). Если выдвинутый бит был равен 1, то прежнее содержимое старшей половины регистра ЕАХ будет подвергнуто операции XOR с соответствующими битами полинома. В обратном случае, если выдвинутый бит был равен 0, значения битов будут не изменены, а просто сдвинуты влево на одни разряд. В принципе, имея большое желание, можно рассчитать заранее, каким будет содержимое к-го бита в к-й итерации сдвига. К примеру, значение нового старшего бита, определяющего действия алгоритма в следующей итерации, можно рассчитать по содержимому двух старших битов старшего байта исходного операнда — а6 XOR а7 AND b7, где b7 — старший бит полинома (всегда равный единице).
Теперь остановимся для того, чтобы рассмотреть и обсудить очередную схему (рис. 9.7).

Рис. 9.7. Влияние на регистр ЕАХ серии операций XOR при вычислении CRC

Из рассуждений выше следует, что если взять для рассмотрения старший байт операнда, то по его содержимому можно однозначно предположить, сколько операций XOR и когда будет выполнено (см. рисунок). Обозначим старшую половину регистра ЕАХ как переменную А, а операнды со значениями полинома, объединяемые с А при единичном состоянии очередного выдвигаемого слева бита, обозначим соответственно как В, С, D (помним, что В = С = D). Тогда формирование результата Е можно представить формулой:

Е-((А [сдвиги| XOR В) [сдвигиj XOR С) |сдвиги| XOR D

Здесь | сдвиги | представляют собой значение от 0 до 7 и определяются теку щим содержимым старшего байта операнда (регистра ЕАХ). Благодаря ассоциативному свойству операции XOR тот же самый результат можно получить, если предварительно выполнить операцию XOR над полиномами В, С, D с соответствующими значениями сдвигов, а затем результат объединить по XOR с А:

F: = 1 сдвиги| XOR ( В |сдвиги| XOR С |сдвиги| XOR D) Е:= A XOR F

Из этих рассуждений следуют важные выводы:

  • величина F является совершенно точно предсказуемой по содержимому старшего байта операнда;
  • если величина F определяется содержимым старшего байта операнда и собственно значением полинома, то существует всего 256 возможных значений этой величины (по количеству значений, представимых беззнаковым байтом);
  • исходя из первых двух положений величина F не зависит от значения операнда и может быть рассчитана заранее, при этом результаты ее расчетов можно свести в таблицу (!).

Вот мы и выяснили, на чем основано название табличного алгоритма расчета CRC. Теперь со знанием сути дела приведем его общее описание (рис. 9.8). В качестве основы для рассуждений по-прежнему используем программу прямого вычисления значения CRC и соответствующую схему (см. рис. 9.6).

Рис. 9.8. Общая схема табличного алгоритма

На схеме, показанной на рисунке, цифрами обозначена последовательность шагов табличного алгоритма. Шаги 1 и 2 выполняются одновременно и означают, что старший байт из регистра ЕАХ выдвигается в переменную R, а младший байт этого регистра заполняется очередным байтом исходной последовательности. Значение переменной R используется на шаге 3 в качестве индекса в таблице TABL_F для извлечения 16-битного значения, которое на шаге 4 будет объединено
операцией XOR с содержимым старших 16 битов регистра ЕАХ. Таким образом, в отличие от прямого алгоритма процесс преобразования вырастает до уровня байтов и содержит три операции: сдвига, доступа к таблице для извлечения нужного значения и операции XOR извлеченного значения с содержимым старшей половины ЕАХ. По окончании процесса в старшей половине ЕАХ будет содержаться значение CRC. Сообщение по-прежнему должно быть выровненным, то есть дополненным количеством битов, равным степени полинома, или для данного случая — 16. Для практических приложений это крайне неудобно, и решение этой проблемы будет показано чуть ниже. Пока же разработаем программу вычисления содержимого таблицы на основе полинома 1021h степени 16.

:prg09_03.asm - программа вычисления содержимого таблицы :на основе полинома 1021п степени 16.
.data
tabl_16 dw 256 dup (0) :CRC-таблица
len_tabl_16=$-tabl_16
adr_tabl_16 dd tabl_16
polinom dw 1021h
.code
les di,adr_tabl_16
add di.len_tabl_16-2
std :идем назад по таблице
mov ex.255
mov bx.polinom
ml: xor ax.ax
mov ah.cl :индекс в таблице для вычисления CRC
push ex сложенные циклы
mov ex. 8
m2: shl ax.l
jnc m3 :старшие разряды не равны - выполняем сдвиг
: (частное нас не интересует) ;старшие разряды равны - выполняем XOR:
xor ax.bx :ax XOR polinom
m3: loop m2 _^
pop ex
stosw
loop ml

В результате работы этой программы область памяти tabl_16 будет инициализирована таблицей значений, которые могут быть использованы в схеме вычис-- ления значения CRC исходной последовательности (см. рис. 9.8).

Прямой табличный алгоритм CRC16

'Необходимость дополнения исходной строки завершающими нулевыми байтами представляет большое неудобство при реализации общей схемы табличного
алгоритма. Поэтому на практике используют другую схему вычисления CRC, не требующую завершения исходного сообщения нулевыми байтами. В этой схеме очередной байт исходной строки объединяется операцией XOR со старшим байтом регистра, выдвинутым с левой стороны этого регистра. Полученное в результате объединения значение используется в качестве индекса для доступа к CRC-таб-лице. Извлекаемое из CRC-таблицы значение объединяется операцией XOR с содержимым регистра. Описанный алгоритм называют прямым табличным алгоритмом.

Рис. 9.9. Схема вычислений CRC с использованием прямого табличного алгоритма

На схеме вычислений CRC с использованием прямого табличного алгоритма цифрами обозначена последовательность шагов вычисления CRC.

  1. Выдвижение старшего байта регистра АХ в байтовую ячейку.
  2. Выполнение операции XOR над выдвинутым на шаге 1 в байтовую ячейку старшим байтом регистра АХ и очередным байтом исходной строки.
  3. Полученное на шаге 2 значение используется в качестве индекса для доступа к элементу CRC-таблицы.
  4. Извлеченное из CRC-таблицы значение объединяется по XOR со значением в регистре АХ.
  5. Результат выполнения на шаге 4 операции XOR помещается обратно в регистр АХ.

После обработки последнего байта исходной строки регистр АХ содержит значение CRC. Программа вычисления CRC с использованием прямого табличного алгоритма приведена ниже.

:prg09_04.asm - программа вычисления CRC с использованием прямого табличного алгоритма.
.......... »
.data
;исходная битовая последовательность в символах
bit_string db "6476c8"
1en_bi t_string=$-bi t_stri ng
adr_bit_string dd bit_string
tabl_16 dw 256 dup (0) iCRC-таблица
Ien_tab1_16=$-tabl_16 adr_tabl_16 dd tabl_16
polinom dw 1021h
.code
.-------------расчитываем CRC-таблицу---.....-------------
les di.adr_tabl_16
add di.len_tabl_16-2
std :идем назад по таблице
mov ex.255
mov bx.polinom
ml: xor ax,ax
mov ah.cl :индекс в таблице для вычисления CRC
push ex сложенные циклы
mov ex.8
m2: shi ax.l
jnc m3 ;старшие разряды не равны - выполняем сдвиг (частное нас не интересует)
:старшие разряды равны - выполняем XOR:
xor ax.bx :ax XOR polinom
тЗ: loop m2
pop ex
stosw
loop ml
-------------закончили расчет CRC-таблицы.........
хог ах,ах
xor bx.bx
Ids si.adrbitstring
mov cx.len_bit_string
m4: mov bl.ah
shl ax.8
xor bl.[si]
xor ax.tabl_16[bx]
inc si
1oop m4
;.........

Рассмотрением этого алгоритма введение в проблему вычисления CRC можно было бы и закончить. Все существующие алгоритмы вычисления CRC являются, по сути, различными модификациями описанного выше табличного алгоритма. Эти модификации преследуют разные цели, перечислим некоторые из них:

  • переход от цикла по всем битам к циклу по большим порциям данных — байтам, словам и т. д.;
  • повышение разрядности порождающего полинома;
  • отражение особенностей функционирования аппаратуры передачи данных (наличие этой цели обусловлено тем, что микросхемы, поддерживающие работу последовательного порта компьютера, передачу байта начинают с его младших битов, поэтому описанные ниже алгоритмы расчета CRC, учитывающие эту особенность, называются зеркальными).

Алгоритмы вычисления CRC получили свое закрепление в некоторых стандартах. Перечислим отличительные особенности основных алгоритмов вычисле-^ ния CRC. Итак, основные алгоритмы вычисления CRC различаются:

  • по значению и степени порождающего полинома, при этом значение полинома обычно указывается без ведущей единицы в старшем разряде;
  • по начальному содержимому регистра, в котором формируется значение CRC;
    по тому, производится ли обращение битов каждого байта перед его обработкой;
  • по способу прохода байтов через регистр — способ может быть косвенным или прямым;
  • по тому, производится ли обращение конечного результата;
  • по конечному значению, с которым производится объединение по XOR результата вычисления CRC.

После появления 32-разрядных микропроцессоров наибольшей популярностью стали пользоваться 32-разрядные алгоритмы вычисления CRC. В различных источниках рассматривают два типа таких алгоритмов — прямой и зеркальный. Рассмотрим их конкретные реализации, рекомендуемые стандартами.

Прямой табличный алгоритм CRC32

Как и любой табличный алгоритм, табличный алгоритм вычисления CRC32 требует задания CRC-таблицы. Ее можно задать в програм'ме статически, явно прописав значения элементов таблицы в сегменте кода, или динамически, вычислив значения элементов таблицы перед началом расчета CRC. Ниже приведена программа вычисления CRC-таблицы для полинома 04clldb7.
Прямой табличный алгоритм CRC32 удобно рассматривать со стороны участников процесса, как это мы делали выше. Источник выполняет следующие действия.

:prgO9_O5.asm - программа вычисления CRC-таблицы для полинома 04clldb7.
.data
;С1<С32-таблица для прямого табличного алгоритма вычисления CRC32
tabl_32_direct dd 256 dup (0)
len_tabl_32_direct =$-tabl_32_direct
adr_tabl_32_direct dd tabl_32_direct
polinom dd 04clldb7h
.code
les di.adr_tabl_32_direct
add di,len_tabl_32_direct-4
std :идем назад по таблице
mov ex,255
mov ebx.polinom /
ml: xor eax.eax
shrd eax.ecx,8
push ex сложенные циклы
mov ex.8 m2: shl eax.l
jnc m3 :старшие разряды неравны - выполняем сдвиг (частное нас не интересует)
;старшйе разряды равны - выполняем XOR:
xor eax.ebx :ax XOR polinom
шЗ:loop m2
pop ex

  1. Делает начальную установку регистра, в котором будет производиться формирование CRC, значением OFFFFFFFFh.
  2. Вычисляет значение CRC для каждого байта исходной последовательности, принцип которой показан на схеме (см. рис. 9.9). Читатель понимает, что хотя эта схема иллюстрирует принцип вычисления CRC16, но для 32-разрядного алгоритма нужно только увеличить размер элементов таблицы до 32 бит и задействовать весь регистр ЕАХ.
  3. Объединяет по XOR итоговое значение в ЕАХ со значением OFFFFFFFFh.
  4. Добавляет вычисленное значение в конец двоичной исходной последовательности. После этого его можно передать по назначению.

Ниже приведена программа вычисления CRC32 по прямому табличному алгоритму.

;prg09_06.asm - программа вычисления CRC32 по прямому табличному алгоритму.
исходная битовая последовательность в символах
bit_string db "123456789"
len_bit_string=$-bit_string
сгс_32 dd 0 ;сюда мы поместим рассчитанное значение CRC32
adr_bit_string dd bit_string
;СЯС32-таблица для прямого табличного алгоритма вычисления CRC32
tabl_32_direct dd 256 dup (0)
len_tabl_32_direct =$-tabl_32_direct
adr_tabl_32_direct dd tabl_32_direct
polinom dd 04clldb7h
.code
:----.........расчитываем CRC32 таблицу
les di.adr_tabl_32_direct
add di.len_tabl_32_direct-4 * std ;идем назад по таблице
mov ex.255
mov ebx, polinom
ml: xor eax.eax
shrd eax.ecx.8
push ex сложенные циклы
mov ex.8
m2: shl eax.l
jnc m3 :старшие разряды неравны - выполняем сдвиг (частное нас не интересует)
:старшие разряды равны - выполняем XOR:
xor eax.ebx :ax XOR polinom
m3: loop m2
pop ex
stosd
loop ml
:.....--......закончили расчет CRC32 таблицы
mov eax, OFFFFFFFFh
Ids si.adr_bit_string
mov cx.len_bit_string
m4: xor ebx.ebx
shld ebx.eax.8
shl eax.8
xor bl.[si]
xor eax. tabl_32_direct[bx]
inc si
1 oop m4 :запишем crc-32 в конец (или начало, см. обсуждение ниже) последовательности:
xor eax. OFFFFFFFFh
mov crc_32.eax ;добавляем в конец исходной последовательности

Значение CRC32 для строки "123456789" равно 9c970409h.
Если для источника стандарт определяет практически однозначную последовательность действий, то для приемника возможны несколько вариантов поведения. Отметим два из них.
В первом варианте критерием для вывода о целостности полученного приемником сообщения является результат сравнения или нулевой результат.

  1. 1. Выполнить начальную установку регистра, в котором будет производиться формирование значения CRC.
  2. Вычислить значение CRC для каждого байта полученной последовательности, принцип которой показан на схеме (см. рис. 9.9 и замечания выше о различиях CRC16 и CRC32).
  3. Объединить по XOR итоговое значение в ЕАХ со значением OFFFFFFFFh.
  4. Далее можно сделать одно из двух действий:
    1. сравнить значение в ЕАХ со значением CRC, полученным вместе с сообщением, — если они равны, то полученное сообщение идентично исходному;
    2. пропустить байты со значением CRC через процесс получения CRC наравне с другими байтами — об успехе будет свидетельствовать нулевое значение (этот вариант предпочтительнее, так как не предполагает получение предварительных сведений о длине начальной последовательности без учета исходного значения CRC).

Во втором варианте критерием для вывода о целостности полученного приемником сообщения является фиксированная двоичная константа 6b202ca2h.

  1. Выполнить начальную установку регистра, в котором будет производиться формирование CRC, в значение OFFFFFFFFh;
  2. Вычислить значение CRC32 для каждого байта полученной последовательности (вместе со значением CRC32 в конце), принцип которой показан на схеме (см. рис. 9.9 и замечания выше о различиях CRC16 и CRC32). Должно получиться фиксированное двоичное значение — 6b202ca2h. Необ ходимо заметить, что в литературе можно встретить другое значение — 0debb20e3h, но у автора получалось первое.

Основное отличие этих двух вариантов в том, что нужно каким-то образом отделять контролируемую строку и ее значение CRC32. Избежать прямого отслеживания длины принимаемой строки на стороне приемника можно путем формирования источником значения CRC32 в начале исходной строки либо вообще вне строки. Последний случай, например, характерен для программы, которая отслеживает целостность файлов в некотором каталоге. Тогда результаты подсчета CRC для каждого файла хранятся в некотором служебном файле. Если такой вариант вас не устраивает, тогда следует написать код приемника для прямого табличного алгоритма так, как это сделано для приемника в зеркальном табличном алгоритме. Попробуйте самостоятельно определить константу, соответствующую успешному принятию приемником исходной строки.
Учитывая практическую важность обоих вариантов и для демонстрации разнообразия подходов к проблеме вычисления CRC32, работу приемника для прямого табличного алгоритма продемонстрируем на примере первого варианта, приемник «зеркального» табличного алгоритма CRC32 разработаем исходя из положений второго варианта. Но вы должны понимать, что правильное понимание принципов расчета CRC позволяет вам при соответствующей доработке кода поменять варианты работы приемников. В поле сгс_32 должно быть значение CRC32, рассчитанное предыдущей программой. Автор специально не стал объединять эти программы. Пусть это сделает читатель в нужном для его текущей работы контексте.

;prg09_06.asm - программа вычисления CRC32 по прямому табличному алгоритму.
исходная битовая последовательность в символах
bit_string db "123456789"
len_bit_string=$-bit_string
сгс_32 dd 0 ;сюда мы поместим рассчитанное значение CRC32
adr_bit_string dd bit_string
;СЯС32-таблица для прямого табличного алгоритма вычисления CRC32
tabl_32_direct dd 256 dup (0)
len_tabl_32_direct =$-tabl_32_direct
adr_tabl_32_direct dd tabl_32_direct
polinom dd 04clldb7h
.code
:----.........расчитываем CRC32 таблицу
les di.adr_tabl_32_direct
add di.len_tabl_32_direct-4 * std ;идем назад по таблице
mov ex.255
mov ebx, polinom
ml: xor eax.eax
shrd eax.ecx.8
push ex сложенные циклы
mov ex.8
m2: shl eax.l
jnc m3 :старшие разряды неравны - выполняем сдвиг (частное нас не интересует)
:старшие разряды равны - выполняем XOR:
xor eax.ebx :ax XOR polinom
m3: loop m2
pop ex
push ex сложенные циклы
mov ex,8 m2: shl eax.l
jnc m3 ;старшие разряды не равны - выполняем сдвиг (частное нас не интересует) :старшие разряды равны - выполняем XOR:
хог eax.ebx:ax XOR polinom m3: loop m2
pop сх
stosd
loop ml : закончили расчет СРО2-таблицы
mov eax. OFFFFFFFFh
Ids si,adr_bit_string
mov cx,len_bit_string m4: xor ebx.ebx
shld ebx.eax,8
shl eax.8
xor bl.[si]
xor eax. tabl_32_direct[bx]
inc si
1 oop m4
xor eax. OFFFFFFFFh
;если исходная последовательность цела, то должно получиться значение crc32=9c970409h ;его можно сравнить с исходным и на этом закончить работу или продолжить до победного. :то есть до 0:
mov ex.4 : 4 (длина сгс_32) :в si адрес сгс_32
mov ebx.crc_32
bswap ebx
mov crc_32.ebx m5: xor ebx.ebx
shld ebx.eax.8
shl eax.8
xor bl.[si]
xor eax. tabl_32_direct[bx]
inc si
loop m5 :должен быть О

Заметьте, что для получения нулевого результата нам пришлось использовать команду BSWAP, чтобы «перевернуть» "значение в поле сгс_32.

«Зеркальный» табличный алгоритм CRC32

В заключение данного раздела обсудим «зеркальный» вариант табличного алгоритма — алгоритм CRC32 (V.42 МККТТ). Этот вариант вычисления CRC обязан своим возникновением существованию последовательного интерфейса, который посылает биты, начиная с наименее значимого (бит 0) и заканчивая самым старшим (бит 7), то есть в обратном порядке. В «зеркальном» регистре все биты отражены относительно центра. Например, 10111011001 есть отражение значения 10011011101.
Вместо того чтобы менять местами биты перед их обработкой, можно зеркально отразить все значения и действия, участвующие в прямом алгоритме. При этом
направление расчетов поменяется — байты теперь будут сдвигаться вправо, полином 04clldb7h зеркально отразится относительно его центра, в результате получится значение 0edb88320h. С11С32-таблица будет зеркальным отражением аналогичной таблицы для прямого алгоритма (рис. 9.10).

Рис. 9.10. Схема «Зеркального» табличного алгоритма

Для зеркального алгоритма вычисления CRC32 процесс вычисления такой же, за исключением порядка — сдвиги и заполнение регистра осуществляются вправо. Ниже приведена программа вычисления таблицы для зеркального алгоритма CRC32 и полинома 0EDB88320h.

;prg09_08.asm - программа вычисления таблицы для зеркального алгоритма CRC32 :и полинома 0EDB88320h
.data
:СРС32-таблица для зеркального табличного алгоритма вычисления CRC32
tabl_32_mirror dd 256 dup (0)
len_tabl_32jrrirror=$-tabl_32_mirror
adr_tabl_32jTrirror dd tabl_32_mirror
polinom dd 0EDB88320h
.code
les di.adr_tabl_32_mirror
add di,len_tabl_32_mirror-4
std ;идем назад по таблице
mov ex.255
mov ebx.polinom
ml: xor eax.eax
mov al.cl ;индекс в таблице для вычисления CRC
push ex сложенные циклы
mov ex.8
m2: shr eax.l
jnc m3 ;старшие разряды не равны - выполняем сдвиг (частное нас не интересует)
;старшие разряды равны - выполняем XOR:
xor eax.ebx:ax XOR polinom
m3: loop m2
pop ex stosd
loop ml

Для того, кто обладает солидным багажом знаний по проблеме CRC-вычис-лений, написание программы, реализующей зеркальный алгоритм, — дело техники. На стороне источника код будет выглядеть так:

;prgO9_O9.asm - программа вычисления кода CRC32 на стороне источника :для зеркального алгоритма CRC32 и полинома 0EDB88320h.
.data
исходная битовая последовательность в символах bit_stnng db "123456789" 1en_bi t_string=$-bi t_string
crc_32 dd 0 :сюда мы поместим рассчитанное значение CRC32 adr_bit_string dd bit_string
:СРС32-таблица для зеркального табличного алгоритма вычисления CRC32 .. tab1_32_mirror dd 256 dup (0) len_tabl_32_mirror=$-tabl_32_mirror adr_tabl_32_mirror dd tabl_32_mirror polinom dd 0EDB88320h .code
:-------......расчитываем зеркальную СКС32-таблицу.........
les di.adr_tabl_32_mirror
add di,len_tabl_32_mirror-4
std :идем назад по таблице
mov ex.255
mov ebx.polinom
ml: xor eax.eax
mov al.cl ;индекс в таблице для вычисления CRC
push ex ;вложенные циклы
mov ex.8
m2: shr eax.l
jnc m3 ;старшие разряды не равны - выполняем сдвиг (частное нас не интересует) :старшие разряды равны - выполняем XOR:
xor eax.ebx:ax XOR polinom
m3: loop m2
pop сх
stosd ' '
loop ml закончили расчет СРС32-таблицы
xor bx.bx
mov eax. OFFFFFFFFh
Ids si,adr_bit_string
mov cx,len_bit_string
m4: mov Ы .al
shr eax.8
xor bl.[si]
xor eax.tabl_32_mirror[bx]
inc si
1 oop m4
xor eax. OFFFFFFFFh ;запишем сгс-32 в конец последовательности:
mov сгс_32.еах добавляем в конец исходной последовательности

Для исходной строки "123456789" получили CRC=lb948a57h. Теперь осталось приемнику проверить целостность полученной им строки. Приемник работает по второму варианту и выполняет действия, иллюстрируемые следующей программой. Если полученная им строка совпадает с исходной, то результатом работы программы должно быть значение 6b202ca2h.

:prg09_10.asm - программа вычисления кода CRC32 на стороне приемника ;для зеркального алгоритма CRC32 и полинома 0EDB88320h.
1
:исходная битовая последовательность в символах
bit_string db "123456789"
1en_bit_string=$-bit_string
сгс_32 dd Ib948a57h :сюда мы поместили рассчитанное значение CRC32
adr_bit_string dd bit_string
:С(}С32-таблица для зеркального табличного алгоритма вычисления CRC32
tabl_32_mirror dd 256 dup (0)
1 en_tabl_32_mi rror=$-tabl_32_nii rror
adr_tabl_32_mirror dd tabl_32_mirror
polinom dd 0EDB88320h
.code
:.........
;----........расчитываем зеркальную CRC32-Ta6flnuy.........
les di,adr_tabl_32_mirror
add di,len_tabl_32_mirror-4
std ;идем назад по таблице
mov ex.255
mov ebx.poli nom ml: xor eax.eax
mov al.cl :индекс в таблице для вычисления CRC
push сх сложенные циклы
mov сх,8 m2: shr eax.l
jnc m3 ;старшие разряды не равны - выполняем сдвиг (частное нас не интересует) :старшие разряды равны - выполняем XOR: * xor eax,ebx:ax XOR polinom m3: loop m2
pop ex
stosd
loop ml закончили расчет (Ж32-таблицы
xor bx.bx
mov eax. OFFFFFFFFh
Ids si.adr_bit_string
mov cx.len_bit_string+4 ;4 - длина crc_32 m4: mov bl.al
shr eax.8
xor bl.[si]
xor eax.tabl_32_mirror[bx]
inc si loop m4 :сравнить - результат должен быть константой 6b202ca2h

Этот вариант работы алгоритма вычисления CRC32 удобен тем, что не нужно знать длину собственно исходной последовательности (без значения CRC). Достаточно просто обработать весь входной поток, не различая в строке завершающую ее подстроку с CRC. Далее необходимо сравнить содержимое регистра ЕАХ с 6b202ca2h. Если эти значения равны, значит, исходная последовательность нарушена не была. Для получения собственно строки достаточно отбросить последние 4 байта сообщения, полученного приемником. И последнее замечание, которое говорит о том, что проблема вычисления CRC неоднозначна для понимания и предоставляет большое поле для проведения различных экспериментов и совершенствования существующих алгоритмов. Небольшой поправкой в алгоритме работы источника можно сделать так, что успехом целостности при принятии приемником сообщения может быть не указанная выше константа, а нуль. Для этого источнику достаточно не объединять вычисленное значение CRC32 по XOR с Offffffffh, а просто добавить его к исходной последовательности. Оба эти варианта хороши тем, что не нужно заранее знать длину анализируемого сообщения.
На этом, пожалуй, можно и закончить обсуждение проблемы вычисления CRC. На примере этой достаточно сложной задачи были в очередной раз продемонстрированы варианты использования средств ассемблера для обработки информации на различных уровнях, вплоть до битового. За дальнейшими подробностями по проблематике вычисления CRC обратитесь к соответствующим источникам.


Программирование ХММ-расширения

Продавай мобильный контент со своего сайта

Программирование ХММ-расширения

  • Программирование ХММ-расширения
    • Описание упакованных и скалярных данных
    • Примеры использования команд ХММ-расширения
  • Препроцессор команд ХММ-расширения
    • Поддержка ХММ-команд в файле iaxmm.inc

Что бы ученые ни придумали, отвечать приходится студенту.
А. Хлудеев

В этой главе мы рассмотрим практические вопросы программирования ХММ-расширения микропроцессора Pentium III. Программирование целочисленного MMX-расширения рассмотрено в уроке 20 «ММХ-технология микропроцессоров Intel» учебника. Там же рассмотрены архитектура и система команд ХММ-расширения, но остались «за бортом» вопросы организации практической работы с ними. Кроме учебника архитектура и система команд ХММ-расширения рассмотрены в литературе.
С точки зрения структуры материал, изложенный ниже, состоит из двух частей: в первой части рассматриваются практические аспекты применения команд ХММ-расширения для решения некоторых типов задач, а во второй части — порядок разработки программ, использующих команды ХММ-расширения. Предполагается, что для разработки ассемблерных программ используется пакет TASM.

Программирование ХММ-расширения

Новые сапоги всегда жмут.
Козьма Прутков

Необходимо отметить тот факт, что фирма Intel — не единственная из фирм, разрабатывающих микропроцессоры, активно работает над проблемой обработки больших массивов однородной информации. Не секрет, что постоянным соперником фирмы Intel по вопросам архитектуры микропроцессора является фирма AMD. Между ними по различным направлениям идет постоянная борьба за рынок компьютеров архитектуры х86. Мы не будем рассматривать ее хронологию, уделим внимание лишь тому, для чего и в какой форме в архитектуре микропроцессоров этих фирм появились средства для потоковой обработки данных с плавающей точкой или SSE (Streaming SIMD Extensions). Толчком к развитию SIMD-технологий (в том числе и целочисленных) стали задачи с большими объемами однородных исходных данных простой структуры. Основные области, где встречается такая информация, — Интернет и компьютерные игры. Именно здесь возникает множество задач по обработке звука, видео, графики. Если рассматривать современные компьютерные игры, то в них активно используются мощности со процессора для производства расчетов ЗО-объектов в трехмерном пространстве (отсюда, кстати, и название — 3DNow!). Архитектура и производительность стандартного сопроцессора не обеспечивают с нужной эффективностью эти расчеты. Фирмы — производители микропроцессоров активно начали поиск технологий, которые позволили бы увеличить производительность подсистемы для расчетов с плавающей точкой. Известно несколько путей повышения эффективности расчетов подобного рода: увеличение тактовой частоты, уменьшение задержек выполнения команд в сопроцессоре, конвейеризация вычислений с плавающей точкой, реализация SIMD-технологии, использование параллельных конвейерных устройств для расчетов с плавающей точкой. Среди этих путей фирмы Intel и AMD выбрали свои. Тактовую частоту работы микропроцессора постоянно повышают обе фирмы, чему мы являемся заинтересованными свидетелями. Что же касается подсистемы обработки данных с плавающей точкой, то архитектурно они реализованы по-разному. Так, фирма Intel пошла по пути конвейеризации вычислений с плавающей точкой и реализации SIMD-технологии вычислений с плавающей точкой. Фирма AMD работает над уменьшением задержек выполнения команд в сопроцессоре и над реализацией SIMD-технологии вычислений с плавающей точкой. Эти технологии реализованы в микропроцессорах Pentium III фирмы Intel и Atlon фирмы AMD, но они не совсем одинаковы. Архитектура целочисленного MMX-расширения у этих микропроцессоров совпадает (в семействе AMD целочисленное MMX-расширение появилось в микропроцессоре AMD Кб (ММХ)), что же касается архитектуры ХММ-расширения с плавающей точкой, то здесь различия более существенные начиная с их названий. Потоковое расширение с плавающей точкой микропроцессора Atlon называется 3DNo\v! и включает 21+5 команд. 21 команда расширения 3DNow! существовала в предыдущем микропроцессоре AMD K6-2-3DNow!, 5 команд этого расширения были введены дополнительно в расширение 3DNow! микропроцессора AMD Athlon. Потоковое расширение микропроцессора Pentium III фирмы Intel называется Streaming SIMD Extensions и включает 70 команд. Стоит отметить, что не все из этих 70 команд являются командами SSE-расширения: 50 команд относятся непосредственно к блоку SSE-расширения, то есть являются командами SIMD с плавающей точкой, 12 команд дополняют систему команд целочисленного MMX-расширения и 8 команд относятся к системе кэширования.
Велики различия в физической реализации расширений 3DNow! и Streaming SIMD Extensions. SSE-расширение фирмы Intel выполнено в виде отдельного блока, расширение 3DNow процессоров AMD реализовано на базе стандартного сопроцессора. Второе важное отличие — размерность регистров. Их количество совпадает для обоих расширений. Размерность регистров SSE-расширения — 128 бит, то есть 4x32 бита в формате короткого слова с плавающей точкой. Размерность регистров расширения 3DNow — 64 бита, то есть 2x32 бита в формате короткого слова с плавающей точкой. Таким образом, SSE-расширение процессоров Intel имеет вдвое больше регистров, чем 3DNow!-pacurapeHne процессоров AMD. Выводы из этого делайте сами. К примеру, задачи трехмерной графики активно работают с матрицами 4x4 (см. ниже). В процессе написания программы для 3DNow!-pacnrapeHra процессора AMD пропэаммист вынужден постоянно решать проблему нехватки регистров в 3DNow! со всеми вытекающими последствиями, в том числе и для красоты алгоритма. Реализация SSE-расширения
в виде отдельного блока увеличивает параллельность вычислений в процессоре, так как одновременно могут выполняться команды целочисленного устройства, сопроцессора и SSE-расширения. Читатель может возразить, что, несмотря на все эти недостатки, результаты тестов в различных изданиях говорят о том, что зачастую не наблюдается существенного различия в производительности микропроцессоров фирм Intel и AMD одного класса. На взгляд автора, это следствие I того, что фирме AMD удается компенсировать существующие архитектурные различия хорошей проработкой схемотехнических проблем микроархитектурного уровня. Дальнейшее изложение будет основано на базе ХММ-расширения Pentium III — то есть SSE-расширения.
Наибольшую выгоду от использования SSE-расширения Pentium получают задачи трехмерной графики, а также приложения 2D- и 2.50-графики, использующие векторную графику в фоновой части изображения. При желании программист может найти полезные особенности команд ХММ-расширения для использования их при разработке приложений из других предметных областей. Один из основных признаков, на которые стоит обратить внимание при выборе средств реализации, — наличие матричных преобразований, таких как умножение, транспонирование, сложение, вычитание матриц, умножение матрицы на вектор, световые преобразования, подобные преобразованиям между цветовыми моделями RGB и CMYK, и т. п.
В заключение этой части обсуждения отметим, что набор SIMD-инструкций ХММ-расширениия микропроцессора Intel (и AMD тоже) бесполезен без соответствующей программной поддержки. Далее рассмотрим, каким образом использовать ХММ-команды в программах на языке ассемблера. Следует отметить, что данный материал может быть полезен не только для программистов на языке ассемблера, но и для тех, кто пишет программу на языке высокого уровня. Благодаря ассемблерным вставкам или внешним ассемблерным процедурам программист может использовать возможности новых процессоров, не дожидаясь появления новой версии компилятора, поддерживающего эти возможности. Это также имеет место с Pentium III. Оперативно отслеживать процессорные новшества удается только компилятору С/С ++ фирмы Intel и макроассемблеру фирмы Microsoft, который поддерживает новые команды SSE начиная с версии 6.1 Id и выше. Но известно, что далеко не все программисты на С и ассемблере предпочитают эти компиляторы другим аналогичным средствам разработки. Что же делать таким программистам, в частности программирующим на ассемблере с использованием пакета TASM? Ответ один — выкручиваться. Как? Этому и будет посвя-гцено обсуждение ниже. Оно будет логически состоять из двух частей. В первой части мы сделаем вид, что ничего не происходит и транслятор TASM поддерживает любые команды процессора Pentium III. Во второй части мы действительно поможем TASM это сделать.
Вначале разберем порядок описания данных, которыми манипулируют ХММ-команды Pentium III, а затем рассмотрим несколько примеров их использования.

Описание упакованных и скалярных данных

Описание ХММ-данных в приложении обычно производится в одном из двух форматов:

  • в массиве структур;
  • в структуре, элементами которой являются массивы.

Описание точек изображения в трехмерном пространстве принято задавать в виде четырехмерного вектора (x.y.z.w). Это связано с тем, что проективные преобразования, необходимые для показа изображения с различных точек зрения, наиболее просто описываются матрицами 4x4. Используя перечисленные выше форматы задания ХММ-данных, совокупность точек в трехмерном пространстве можно описать двумя способами:

  • первый способ — для каждой точки определить свой экземпляр структуры:
    point 3D struc х del 0.0 у dd 0.0 z dd 0.0 w dd 0.0
    ends .data pi point_3D 4 dup (<>) ;описание пирамиды массивом структур.
    ;каждая из которых описывает одну из 4 вершин
  • В второй способ — все точки описать одной структурой, элементами которой являются массивы координат x,y,z,w:
    pri sm_point_3Dstruc x dd 4 dup (0.0) у dd 4 dup (0.0) z dd 4 dup (0.0) w dd 4 dup (0.0)
    ends .data prism prism_point_3D<> структура, описывающая треугольную пирамиду (4 вершины)

Приведенные выше примеры описания пирамиды иллюстрирует рис. 10.1.

Рис. 10.1. Расположение в памяти описания вершин пирамиды

В большинстве приложений используется первый способ представления ХММ-данных, хотя он считается и менее эффективным. При необходимости можно произвести преобразование представления данных из одного способа в другой. Вариант такого преобразования показан в программе ниже.

;рrg10 01.asm - программа преобразования представления ХММ-данных :из одного способа представления в другой.
prizm struc
union
struc структура, описывающая треугольную пирамиду (1 способ)
xyzwl dd 0.0
xyzw2 dd 0.0
xyzw3 dd 0.0
xyzw4 dd 0.0
ends
struc структура, описывающая треугольную пирамиду (2 способ) х dd 4 dup (0.0) у dd 4 dup (0.0) z dd 4 dup (0.0) w dd 4 dup (0.0)
ends
ends ;конец объединения
ends .data
prizm_l prizm <> :экземпляр объединения .code
преобразование представлений вершин пирамиды (на месте)
lea si.prizm_l
movlps rxmm0.[si] ;rxmm0=? ? уО xO
movhps rxmmO,[si+16] ;rxmm0= yl xl yO xO
movlps rxmm2.[si+32] ;rxmm2« ? ? y2 x2
movhps rxmm2.[si+48] ;rxmm2= уЗ хЗ у2 х2
movaps rxmml.rxmmO :rxmml= yl xl yO xO
shufps rxmmO.rxmm2.88h :rxmm0= x3 x2 xl xO
shufps rxmml.rxmm2.0ddh ;rxmml= уЗ у2 yl yO
movlps rxmm2.[si+8] ;rxmm2=? ? wO zO
movhps rxmm2.[si+24] :rxmm2= wl zl wO zO
movlps rxmm4.[si+40] ;rxmm4= ? ? w2 z2
movhps rxmm4,[si+56] ;rxmm4= w3 z3 w2 z2
movaps rxmm3.rxmm2 ;rxmm3= wl zl wO zO
shufps rxmm2.rxmm4.88h :rxmm2= = z3 z2 zl zO
shufps rxmm3.rxmm4.0ddh ;rxmm3= w3 w2 wl wO . -на выходе получим следующее состояние ХММ-регистров:
;RXMM0= хЗ х2 xl xO. RXMM1= уЗ у2 yl yO. RXMM2= ¦ z3 z2 zl zO. RXMM3= w3 w2 wl wO :теперь их необходимо сохранить в памяти:
movups [si].rxmm0
movups [si+16].rxmml
movups [si+32].rxmm2
movups [si+48].rxmm3

Описание скалярных данных намного проще - это обычные значения с плавающей точкой в коротком формате:

.data
seal real dd 1.0 :пример описания скалярного ХММ-значения;

Примеры использования команд ХММ-расширения

Ниже будут рассмотрены несколько типовых примеров использования команд ХММ-расширения. Основная цель — демонстрация методики работы с основными группами команд ХММ-расширения. Начнем с реализации простейших операций — сложения и умножения.

Сложение и умножение двух упакованных ХММ-значений

Задача — вычислить скалярное произведение двух векторов, каждый из которых состоит из 4 вещественных чисел в коротком формате. Если в качестве таких векторов взять два вектора А и В, то их произведение вычисляется по формуле: АхВ = aoxbo+aixbi+a2xb2+a3xb3- В программной реализации с использованием ХММ-команд это выглядит так, как показано ниже.

:prg10_02.asm - программа вычисления скалярного произведения двух векторов.
.data
xmm_pack_l dd 1.0. 2.0. 3.0. 4.0
xmm_pack_2 dd 5.0. 6.0. 7.0. 8.0
rez_sum dd 0.0 результат сложения .
.code
movaps rxmmO.xrom_pack_l ;RXMM0= 4.0. 3.0, 2.0, 1.0 mulps rxmm0.xinTi_pack_2 :RXMM0= 4.0x8.0. 3.0x7.0. 2.0x6.0, 1.0x5.0 movaps rxmml. rxmmO :RXMM1= 4.0x8.0, 3.0x7.0. 2.0x6.0. 1.0x5.0
shufps rxmml.rxmml.4eh ;RXMM1= 2.0x6.0, 1.0x5.0. 4.0x8.0. 3.0x7.0 addps rxmmO. rxmml :складываем:
;RXMM0= 4.0x8.0. 3.0x7.0. 2.0x6.0, 1.0x5.0
J +
:RXMM1= 2.0x6.0. 1.0x5.0. 4.0x8.0. 3.0x7.0
:RXMM0- 4.0x8.0+2.0x6.0. 3.0x7.0+1.0x5.0, 2.0x6.0+4.0x8.0. 1.0x5.0+3.0x7.0
:или
;RXMM0= 44.0. 26.0, 44.0. 26.0
movaps rxmml, rxmmO :RXMM1= 44.0, 26.0, 44.0, 26.0
shufps rxmml.rxmml.llh :RXMM1= 26,0. 44.0. 26.0. 44.0
addps rxmmO. rxmml :складываем: ;RXMM0= 44.0. 26.0. 44.0, 26.0
; +
;RXMM1= 26.0, 44.0, 26.0, 44.0
:RXMM0= 70.0. 70.0, 70.0, 70.0 сохраняем результат movss rez_sum.rxmm0

Умножение матрицы на вектор

Умножение матрицы на вектор — наиболее характерная операция для вычислений в области машинной графики. Существуют различные способы формирования трехмерного изображения. В наиболее простом случае изображение на экране задается в виде опорных точек. К примеру, рассмотрим случай, когда на экране дисплея находится трехмерное изображение, состоящее из отрезков прямых. Необходимая для его формирования информация хранится в памяти как список опорных точек — концов отрезков. Если изображение дается в трехмерном изображении, то описание каждой точки удобно задать в виде четырехмерного вектора (х, у, z, w). Включение в трехмерный вектор (х, у, z) дополнительной координаты w объясняется тем, что проективные преобразования, необходимые для показа изображения с различных точек зрения, описываются матрицами 4x4. Поэтому для удобства реализации проективных преобразований, зачастую сопровождаемых операциями умножения матриц и векторов, трехмерный вектор (х, у, z) представляют в виде четырехмерного вектора (х, у, г, w), где значение w обычно принимается равным 1. Для выполнения самого преобразования, подготовленная заранее матрица преобразования умножается на этот вектор, в результате чего получается четырехмерный вектор (х1, у', г , w'). Для обратного перехода к требуемому трехмерному вектору необходимо разделить координаты х', у', г на w', после чего удалить четвертую координату w'.
Матрица М преобразования и вектор V имеют следующий вид:

m00 m01 m02 m03 x
mio mu m12 m13 у
m20 m21 m22 m23 z
m30 m31 m32 m33 w=l

Преобразования координат выполняются по формулам:

х' ™ xxm0o+yxni01+zxm02+lxm03
у' » xxm+yxnid+zxm+lxm
г = xxm20+yxm21+zxm22+lxm23
w' = xxm30 +yxm31+zxm32+lxm33

Для получения преобразованных координат в виде трехмерного вектора (x,y,z) делим х', у', z' на w':

х = x'/w' = (ххт00+ухт01+гхт02+1хт0з)/(ххт30+ухт31+гхт32+1хт3з)
У = У'/w' = (xxmlo+yxm))+zxm,2+lxm13)/(xxm3O+yxm31+zxm32+lxm33)
z - z'/w' = (xxm20+yxm2i+zxm22+lxm23)/(xxm30+yxm31+zxm32+lxm33)

Элементы матрицы И векторов представлены числами с плавающей точкой в коротком вещественном формате (4 байта).
Ниже приведены два варианта программы умножения матрицы на вектор — один с использованием стандартного сопроцессора, другой с использованием команд ХММ-расширения (с помощью профайлера можно сравнить скорость преобразования). Результирующий трехмерный вектор замещает исходный.

Умножение матрицы 4x4 на четырехмерный вектор (стандартный сопроцессор)

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

:prgl0_05.asm - программа поворота изображения на месте :с использованием средств стандартного сопроцессора.
t :
:координаты квадрата (необходимо инициализировать) xO.yO.xl.yl.x2.y2.x3.y3 mas_xy dd 8 dup (0.0)
a dd 0.0 :угсл (необходимо инициализировать) .code
lea si.mas_xy
mov ex.4 :цикл 4 раза - по количеству вершин
firm ;вычисляем sin а и cos a;
fid a ;включаем а стек угол
fsin вычисляем sin a
fxch ;меняеи st(0)<->st(l)
fcos ;вычисляем cos a
fxch ;меняем st(0)<->st(l)
fstp a :выталкиваем а :поворот изображения cycl: fild word ptr [si] :включить в стек координату х элемента
fild word ptr [si+2] :включить в стек координату у элемента
fid St(l) ;дублируем их
fid st(l)
fmul st.st(5) вычислить y*sin
fxch :меняем st(0)<->st(l)
fmul St.st(4) вычислить x'cos
fadd :новая координата х
fistp word ptr[si] :передать новую координату х в память
fmul st.st(2) вычислить y*cos
fxch ' ;меняем st(0)<->st(l)
fmul St.StO) .-вычислить x*sin
fsubr .новая координата у
fistp word ptr[si*4] ;передать ее в память
add si.8 ;продвинуть указатель массива mas_xy
loop cycl повторить еще 3 раза :в mas_.ху преобразованные для поворота координаты квадрата
Поворот изображения (ХММ-расширение)
:prgl0_06.asm - программа поворота изображения на месте
:с использованием средств ХММ-расширения.
.data
:ALIGN 16
:координаты квадрата (необходимо инициализировать) хО,уО,х1.у1.х2.у2.х3.уЗ
mas_xy dd 8 dup (0.0)
a dd 0.0 :угол (необходимо инициализировать)
sin_a dd 0.0
cos_a dd 0.0
null dd 0.0
. code
:.........

lea esi.raas_xy
mov ecx.4 :цикл 4 раза - no количеству вершин
finit вычисляем sin а и cos a;
fid a :включаем в стек угол
fsin вычисляем sin a
fxch ;меияем st(Q)<->st(l)
fcos ;вычисляем cos a
fxch ;меняем st(0)<->st(l;
fstp a ;выталкиваем а
fstp cos_a ; выталкиваем cos__a
fstp sin_a ;выталкиваем sin_a
;поворот изображения
;готовим xmm-регистр RXMM2 со значениями углов movlps rxmm2.sin_a
movhps rxmm2,sin_a ;RXMM2= cos_a sin_a cos_a sin__a
movss rxmm2.nul1
siibss rxmm2.sin_a ;RXMM2= cos_a sin_a cos_a -sin_a
cycl: movlps rxmmO.Lesi] :RXMM0= ? ? yi xi movhps rxnwO.Cesi] ;RXMM0= yi xi yi xi
shufps rxmmO.rxmmO.ObOh :RXMM0= xi yi yi xi
mulps rxmmO.rxnm2 ;RXMM0-RXMM0*RXMM2= xi*cos_a yi*sin_a yi* cos_a xi*(-sin_a)
shufps rxmml.rxmmO.31h ;RXMM1=? xi*cos_a ? yi* cos_a
addps rxmmO.rxmml :RXMM0= ? (xi*cos_a+yi*sin_a) ? (yi* cos_a+xi*(-sin_a))
shufps rxmmO.rxmmO.2 ;RXMM0=- ? ? (yi* cos_a+xi*(-sin_a)) (xi*cos_a+yi*sin_a) сохраняем результат: movlps [esi].rxnim0 ;готовимся к вычислению нового положения для следующей координаты
add esi,8 1oop cycl

На этом мы закончим рассмотрение примеров программирования ХММ-расширения. При разработке приведенных выше программ мы считали, что используемый нами транслятор ассемблера поддерживает любые команды микропроцессора Intel, в том числе и ХММ-команды. Реально ситуация далека от этой идеальной картины. Мы уже упоминали, что если транслятор MASM (фирмы Microsoft) пытается поспевать за процессом развития системы команд, то для TASM дело обстоит несколько хуже. Другие фирмы-разработчики трансляторов ассемблера мы не рассматриваем (не потому, что они хуже — просто обсуждение достоинств и недостатков трансляторов ассемблера не является предметом данной книги). Настало время, не меняя любимого транслятора, помочь ему понять неизвестные команды микропроцессора. Для этого в следующей части данного раздела мы выработаем соответствующую методику.

Препроцессор команд ХММ-расширения

Отыщи всему начало, и ты многое поймешь.
Козьма Прутков

Задачу адаптации транслятора TASM к новым командам микропроцессора, и в частности к ХММ-командам, можно решить двумя способами.

  • Можно разработать включаемый файл, в котором дляя каждой ХММ-ком ды реализовать макрокоманду, моделирующую на ббазе существующих к манд нужную ХММ-команду. Кроме этого, фирма Irintel, зная об инерцио ности процесса разработки новых версий трансляторров ассемблера, вмест с подмножеством новых команд разрабатывает соотгветствующий включа мый файл для их поддержки в ассемблерных прогрэаммах. Для подмножества ХММ-команд такой файл называется iaxmm.inac. Он ориентирован на транслятор MASM (фирмы Microsoft) и не пригодеен (требует доработки") для TASM. Однако при доработке TASM необходимую иметь в виду вопрос об авторских правах. Некоторые проблемы использоования файла iaxmm.inc совместно с TASM обсуждены ниже.
  • Можно разработать программу-препроцессор, на ввход которой подавать исходный файл с программой на ассемблере, содеряжащей новые команды процессора, а на выходе получать текст, адаптированнный для компиляции старым транслятором ассемблера. Этот путь имеет' то преимущество, что теперь при появлении новых команд можно, не вноося больших корректив в технологию разработки программ, всего лишь орпределенным образом модифицировать файл-препроцессор, дополнив его i возможностями по обработке новых команд процессора. Более того, дополяяив препроцессор средствами распознавания микропроцессора (Intel или /AMD), можно разрабатывать программы с использованием расширения 31DNo\v!.

Первый способ был реализован в уроке «ММХ-техномюгия микропроцессоров Intel» учебника для ММХ-команд. Там же были привзедены примеры включаемых файлов, полностью пригодных для использованияя как 16-, так и 32-разрядными приложениями на ассемблере. Поэтому основносе внимание мы уделим второму способу организации поддержки ХММ-команд — препроцессорному. Но вначале рассмотрим структуру и содержание включаеемого файла iaxmm.inc. Текст этого файла можно загрузить с официального сайта компании Intel (http:// www.intel.com).

Поддержка ХММ-команд в файле iaxmm.inc

С точки зрения структуры включаемый файл iaxmm.inc представляет собой набор макрокоманд двух типов — основных и вспомогательньях. Названия основных макрокоманд полностью совпадают с названиями ХММ-команд, и эти макрокоманды обеспечивают моделирование определенных XlMM-команд. Вспомогательные макрокоманды расположены в начале файла и предназначены для обеспечения работы основных макрокоманд. В частности, :эти макрокоманды устанавливают тип операндов, указанных при обращении к (основной макрокоманде, причем делают это исходя из режима функционировашия транслятора — 16-или 32-разрядного. Другое важное действие — установление соответствия между названиями ХММ-регистров и регистров общего назначения. Дело в том, что для моделирования ХММ-команд в 16- или 32-разрядном режиме работы ассемблера используются разные регистры общего назначения — 1 6-разрядные регистры в 16-разрядном режиме, и 32-разрядные в 32-разрядном режиме.
Рассмотрим процесс моделирования ХММ-команд. В! качестве основы для моделирования выступает команда основного процессора!. Эта команда должнаудовлетворять определенным требованиям. Каковы они? В поисках ответа посмотрим на машинные коды ХММ-команд в литературе [40, 41]. Видно, что общими у них являются два момента:

  • поле кода операции ХММ-команд состоит из двух или трех байтов, один
    из которых равен Ofh;
  • большинство ХММ-команд использует форматы адресации с байтами modR/M и sib и соответственно допускает сочетание операндов как обычных двух-операндных команд целочисленного устройства — регистр-регистр или память-регистр.

Для моделирования ХММ-команд нужно подобрать такую команду основного процессора, которая удовлетворяет этим двум условиям. Во включаемом файле iaxmm.inc в качестве таких команды присутствуют две — CMPXCHG и ADD. В процессе моделирования на место нужного байта кода операции этих команд помещаются байты со значениями кода операции соответствующей ХММ-команды. Когда микропроцессор «видит», что очередная команда является ХММ-командой, то он начинает трактовать коды регистров в машинной команде как коды ХММ-регистров и ссылки на память, размерностью соответствующей данной команде. В машинном формате команды нет символических названий регистров, которыми мы пользуемся при написании исходного текста программы, например АХ или ВХ. В этом формате они определенным образом кодируются. Например, регистр АХ кодируется в поле REG машинной команды как 000. Если заменить код операции команды, в которой одним из операндов является регистр АХ, на код операции некоторой ХММ-команды, то это же значение в поле reg микропроцессор будет трактовать как регистр RXMMO. Таким образом, в ХММ-командах коды регистров воспринимаются соответственно коду операции. В табл. 10.1 приведены коды регистров общего назначения и соответствующих им ХММ-регистров. В правом столбце этой таблицы содержится условное обозначение ХММ-регистров, принятое в файле iaxmm.inc. Это же соответствие закреплено рядом определений в этом файле, которые иллюстрирует следующая программа.

DefineXMMxRegs Macro IFDEF APPJ.6BIT
rxmmO TEXTEQU<AX>
rxmml TEXTEQU<CX>
rxmm2 TEXTEQU<DX>
rxmm3 TEXTEQU<BX>
rxmm4 TEXTEQU<SP>
rxmm5 TEXTEQU<BP>
гхттб TEXTEQU<SI>
rxmm7 TEXTEQU<DI>
RXMMO TEXTEQU<AX>
RXMM1 TEXTEQU<CX>
RXMM2 TEXTEQU<DX>
RXMM3 TEXTEQU<BX>
RXMM4 TEXTEQU<SP>
RXMM5 TEXTEQU<BP>
P.XMM6 TEXTEQU<SI>
RXMM7 TEXTEQU<DI>
rxmml TEXTEQU<ECX>
rxmm2 TEXTEQU<EDX>
rxmm3 TEXTEQU<EBX>
rxmm4 TEXTEQU<ESP>
rxmm5 TEXTEQU<EBP>
гхттб TEXTEQU<ESI>
ГХШП7 TEXTEQU<EDI>
RXMMO TEXTEQU<EAX>
RXMM1 TEXTEQU<ECX>
NRXMM2 TEXTEQU<EDX>
RXMM3 TEXTEQU<EBX>
RXMM4 TEXTEQU<ESP>
RXMM5 TEXTEQU<EBP>
RXMM6 TEXTEQU<ESI>
RXMM7 TEXTEQU<EDI> ENDIF endm

Таблица 10.1. Кодировка регистров в машинном коде команды

Код в поле reg | Регистр целочисленного устройства | ХММ-регистр
000 | АХ/ЕАХ | RXMM0
001 | СХ/ЕСХ | RXMM1
010 | DX/EDX | RXMM2
Oil | ВХ/ЕВХ | RXMM3
100 | SP/ESP | RXMM4
101 | ВР/ЕВР | RXMM5
110 | SI/ESI | RXMM6
111 | DI/EDI | RXMM7

Теперь в исходном тексте программы можно использовать символические имена ХММ-регистров в качестве аргументов макрокоманд, моделирующих ХММ-команды.
Рассмотрим, как в файле iammx.inc описано макроопределение для моделирования ХММ-команды скалярной пересылки MOVSS.

:F3 OF 10 /г movss xrrnil. xmm2/m32 :F3 OF 11 /r movss xmm2/m32. xnrnl movss macro dst:req. src:req
XMMld_st_f3 opc_Mo«s. dst, src endm

Понимание структуры приведенного макроопределения не должно вызвать у читателя трудностей. Начать следует с того, что данная команда содержит вложенный вызов макрокоманды XMMld_st_f3, у которой две задачи — определить вариант сочетания операндов, после чего сформировать правильный код операции и подставить его на место соответствующих байтов в команде CMPXCHG. В результате этих действий команда CMPXCHG «превращается» в ХММ-команду MOVSS.

1. XMMld_St f3 macro op:req.dst:req, src:req
2. local x. у
3. Defin'eXMMxRegs
4. IF (OPATTR(dst)) AND OOOlOOOOy -.register
5. x: lock cmpxchg src. dst
6. у: org x
7. byte OF3H.0Fh. op& Id
8. org у
9. ELSE
10. x: lock cmpxchgdst. src
11. y: orgx
12. byte 0F3H.0Fh. op&_st
13. orgy
14. ENDIF
15. UnDefineXMMxRegs
16. endm

Центральное место в макроопределении ХММ1 d_st_f3 занимают команда целочисленного устройства (в данном случае — CMPXCHG) и директива ORG. Первое действие данной макрокоманды — выяснить тип операнда приемника (dst) в макрокоманде MOVSS, так как он может быть и регистром, и ячейкой памяти. Это необходимо для правильного определения кода операции, которая будет управлять направлением потока данных. После того как определен приемник данных, с помощью условного перехода осуществляется переход на ветвь программы, где будет выполняться собственно формирование соответствующего ХММ-команде
MOVSS кода операции.
Формирование кода операции ХММ-команды MOVSS производится с помощью директивы org, которая предназначена для изменения значения счетчика адреса. В строках 6 или 11 директива org устанавливает значение счетчика адреса равным адресу метки х. Адрес метки х является адресом первого байта машинного кода команды CMPXCHG. Директива db в следующих строках размещает по этому адресу байтовые значения 0F3H,0Fh, ор&_1 d или 0F3H,0Fh, op&st, в зависимости от того, какое действие производится — загрузка (_ld) или сохранение (_st). Значение opc_Movss, с помощью которого формируются значения op&_st и ор&_1 d, определены в начале файла iaxmm.inc:

opcjtovssjd - 010Н
opc_Movss_st - 011H

Для дотошных читателей заметим еще один характерный момент. Для его полного понимания необходимо хорошо представлять себе формат машинной команды и назначение его полей. Достаточно полная информация об этом приведена в литературе [39, 40]. Обратите внимание на порядок следования операндов в заголовке макрокоманды, который построен по обычной для команд ассемблера схеме: коп назначение, источник. В команде CMPXCHG порядок обратный. Этого требует синтаксис команды. Это хорошо поясняет назначение бита d во втором байте кода операции, который характеризует направление передачи данных в микропроцессор (то есть в регистр) или в память (из микропроцессора (регистра)). Вы можете провести эксперимент. Проанализируйте машинные коды команды MOV:

  • Команды с непосредственным операндом:
    CMPPS RXMM1. RXMM2/ml28, 18 CMPSS RXMM1, RXMM2/m32. i8
  • Однооперандные команды: FXRSTOR m512 FXSAVE m512 LDMXCSR m32 STMXCSR m32

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

метка: код_операции операнд1. операнд2, операндЗ] ;текст комментария

Данная структура почти совпадает со структурой обычных команд ассемблера. В соответствии с общими принципами трансляции препроцессор будет работать с исходной программой в несколько этапов.

  1. Лексический анализ (сканирование) исходного текста.
  2. Синтаксический анализ.
  3. Генерация кода.

Необходимо отметить, что по принципу действия разрабатываемый нами препроцессор относится к интерпретаторам. Читатель наверняка понимает, в чем состоит разница между интерпретатором и компилятором. Объект для работы компилятора — исходный текст программы в полном объеме. Выход компилятора — объектный модуль, то есть машинное представление исходной программы, пригодное для компоновки с другими модулями или получения исполняемого модуля. Интерпретатор работает с отдельными строками исходной программы. Распознав синтаксическую правильность строки, интерпретатор исполняет ее. В частности, интерпретация характерна для обработки входных строк командного процессора. Поэтому на примере данной задачи читатель может научиться достаточно профессионально организовывать языковое взаимодействие с пользователями своих программ.
В главе 2 описаны основные шаги разработки компилятора. Для интерпретатора разница невелика, в чем мы убедимся ниже.
Для распознавания лексем входной программы разработаем сканер, следуя для этого следующему алгоритму.

  1. Выделить классы лексем.
  2. Определить классы литер.
  3. Определить условия выхода из сканера для каждого класса лексем.
  4. Каждому классу лексем поставить в соответствие грамматику класса 3.
  5. Для каждой грамматики, построенной на шаге 4, построить конечный автомат, который будет распознавать лексему данного класса.
  6. Выполнить объединение («склеивание») конечных автоматов для всех классов лексем.
  7. Составить матрицу переходов для «склеенного» конечного автомата.
  8. «Навесить» семантику на дуги «склеенного» конечного автомата.
  9. Выбрать коды лексической свертки для терминалов грамматики и формат таблицы идентификаторов.
  10. Разработать программу сканера.

Язык описания команд ассемблера

Множество команд ассемблера можно описать с помощью следующего языка:

ASM_LENG={
Vt=(+ - AL АН BL BH CL CH DL DH AX EAX BX EBX CX ECX DX EDX BP EBP SP ESP DI EDI SI ESI BYTE SBYTE WORD SWORD DWORD SDWORD FWORD QWORD TBYTE REAL4 REAL8 REAL10 0 12 3 4 56789abcdefABCDEF NEAR16 NEAR32 FAR16 FAR32 AND NOT HIGH LOW HIGHWORO LOWWORD OFFSET SEG LROFFSET TYPE THIS PTR :.{)[] WIDTH MASK SIZE SIZEOF LENGTH LENGTHOF ST SHORT .TYPE OPATTR . название_команды * / MOD NEAR FAR OR XOR " 'hoqt у H 0 Q T Y { } < > :; EQ NE LT LE GT GE CS DS ES FS GS SS SHR SHL CRO CR2 CR3 DRO DR1 DR2 DR3 DR6 DR7 TR3 TR4 TR5 TR6 TR7 А5СП_символ_буква любой_символ_кроме_кавычки). Vn-( addOp asmlnstruction byteRegister constant constExpr dataType decdigit digits distance expr exprtist Expr eOl eO2 eO3 eO4 eO5 eO6 eO7 eO8 eO9 eOlO eOll hexdigit id mnemonic mulOp nearfar radixOverride orOp oldRecordFieldList relOp recordConst recordFieldList register shiftOp sizeArg string type segmentRegister specialRegister stext string stringChar structTag quote type typeld unionTag). P.
Z=(<asmlnstruction>) }

Множество правил Р грамматики ASM_LENG выглядит следующим образом:

smlnstruction => mnemonic [[ exprList ]] AddOp => + | -
byteRegister => AL | AH | BL | BH 1 CL j CH | DL j DH constant => digits [[ radixOverride ]] constExpr => Expr dataType => BYTE | SBYTE | WORD | SWORD | DWORD | SDWORD | FWORD | QWORD |
TBYTE | REAL4 | REAL8 | REAL 10
decdigit => 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 digits => decdigit | digits decdigit | digits hexdigit distance => nearfar | NEAR16 | NEAR32 | FAR16 | FAR32 eOl => eOl orOp eO2 | eO2 eO2 => eO2 AND еОЗ | еОЗ eO3 => NOT eO4 | eO4 eO4 => eO4 relOp eO5 | eO5 eO5 => eO5 addOp eO6 | eO6 eO6 => eO6 mulOp eO7 | eO6 shiftOp eO7 | eQ7 eO7 => eO7 addOp eO8 | eO8
eO8 => HIGH eO9 | LOW eO9 | HIGHWORD eO9 | LOWWORD eO9 | eO9 eO9 => OFFSET elO | SEG elO | LROFFSET elO | TYPE elO ] THIS elO | eO9 PTR
elO | eO9 : elO | elO
elO => elO . ell | elO [[ expr ]] | ell
, ell => ( expr ) | [ expr ] | WIDTH id | MASK id | SIZE SizeArg | SIZEOF
sizeArg | LENGTH id | LENGTHOF id | recordConst | string | constant | type | id | $ | segmentRegister | register | ST | ST ( expr ) expr => SHORT eO5 | .TYPE eOl | OPATTR eOl | eOl exprList => expr | exprList . expr gpRegister => AX | EAX | BX | EBX | CX | ECX | DX 1 EDX | BP | EBP | SP | ESP I
DI | EDI | SI | ESI
hexdigit =>a|b|c|d|e|f|A|B|C|D|E|F
id => А5С11_символ_буква | id А5СП_символ_буква | id decdigit
mnemonic => название_команды
mulOp => * | / | MOD
nearfar => NEAR | FAR
oldRecordFieldList=> [[ constExpr ]] | oldRecordFieldList . [[ constExpr ]]

За основу языка ASMLENG было взято описание языка MASM (из документации на него), ассемблер, поддерживаемый TASM, незначительно отличается от этого описания (в основном это касается некоторых операторов, типа OPATTR). Подчеркнем тот факт, что язык ASMLENG описывает лишь правило построения команд ассемблера, не затрагивая синтаксиса всей программы ассемблера в целом. Все строки, не являющиеся командами, будут просто игнорироваться и включаться в выходной файл в своем изначальном виде.

Выделение классов лексем

Для грамматики языка ASMLENG можно определить следующие классы лексем:

  • идентификатор — id;
  • ключевые слова - AL АН BL ВН CL СН DL DH АХ ЕАХ ВХ ЕВХ СХ ЕСХ DX EDX ВР ЕВР SP ESP DI EDI SI ESI BYTE SBYTE WORD SWORD DWORD SDWORD FWORD QWORD TBYTE REAL4 REAL8 REAL10 NEAR16 NEAR32 FAR16 FAR32 AND NOT HIGH LOW HIGHWORD LOWWORD OFFSET SEG LROFFSET TYPE THIS PTR WIDTH MASK SIZE SIZEOF LENGTH LENGTHOF ST SHORT .TYPE OPATTR MOD NEAR FAR OR XOR EQ NE LT LE GT GE CS DS ES FS GS SS SHR SHL CRO CR2 CR3 DRO DR1 DR2 DR3 DR6 DR7 TR3 TR4 TR5 TR6 TR7 на-звание_команды;
  • целые числа (константы) — 0123456789abcdefABCDEF;
  • однолитерные разделители — +-/: . ()[] ,*" ' {}<>hoqtyHOQ Т Y;
  • двулитерный разделитель — ;;
  • символьные строки — А5СП_символ_буква, любой_символ_кроме_кавычки.

Классы литер

В случае грамматики языка ASMLENG можно определить следующие классы литер:

  • б — цифра;
  • 1 — буква;
  • b — литеры, которые игнорируются (к ним отнесем пробел); ¦ .
  • si - одиночные разделители: + -/:.()[],*"'{}<>;;
  • s2 — двулитерный разделитель: ;;.

Определение условий выхода из сканера для каждого класса лексем
Для каждого класса лексем определим условия, при которых сканер переходит в конечное состояние:

  • для идентификаторов — появление во входном потоке сканера любого символа, отличного от d (цифра) или 1 (буква);
  • ключевые слова — появление пробела и нахождение соответствия введенной лексемы одному из ключевых слов языка;
  • целые числа (константы) — появление любого символа, отличного от d;
  • однолитерные разделители — появление любого символа;
  • двулитерные разделители — появление любого символа; ,;;.
    символьные строки — появление завершающей кавычки. ......г.

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

  • идентификаторы — id, к которым по принципу построения можно отнести и ключевые слова — название_команды:

id=>ASCII_CMMBon_6yKea | id
ASCII_символ_буква | id decdigit :,; decdigit ^ 0|l|2|...8|9

  • целые числа — chint:

digits => decdigit | digits decdigit | digits hexdigit decdigit =>0|l|2|3|4|5|6|7|8|9 ¦

  • Oднолитерные разделители — +-/:.()[]. *"¦'---

SEPiL^. . | . | : |: | + | - | * I 4 ) / I L I ] Г Г I

  • двулитерный разделитель — ;;:

SEP2L=> : Q

«Склеивание» конечных автоматов для всех классов лексем

Мы не будем пытаться получить склеенный автомат, учитывающий все возможные случаи синтаксиса строки с командой ассемблера. Попытаемся получить объединенный конечный автомат для анализа типичной строки программы с ХММ-командой. С учетом этих упрощений «склеенный» конечный автомат может выглядеть так, как показано на рис. 10.2.

Рис. 10.2. Упрощенный вариант «склеенного» конечного автомата

Для представленного на рисунке склеенного конечного автомата таблица (матрица) переходов показана ниже.

L D [ ] пробел
SO SI S2 S4 S4 SO S6
SI SI SI S5 S4
со S2 S2 S4
S3

S4
S5 S3 S3 S3 S3 S3 S3 S4
S6 SG S6 S6 S6 S6 S6 S6 S6

Как обычно, два из этих состояний являются конечными:

  • S3 — состояние ошибки;
  • S4 — конечное состояние.

Состояние S6 — состояние, соответствующее комментарию, то есть допустимы любые символы. Ограничение комментария — конец строки. Строки в таблице переходов соответствуют состояниям склеенного конечного автомата, основа столбцов — классы лексем. Логика работы сканера с использованием таблицы переходов описана в главе 2.
После заполнения таблицы переходов можно навешивать семантику на « дуги» переходов из одного состояния в другое. Основная задача при этом — не брать на себя ничего лишнего. Главное — локализовать поле с названием команды, определить принадлежность ее к группе ХММ-команд. Если это не так, то дальней
ший процесс сканирования строки можно прекращать, копировать ее в выходной поток (пусть транслятор ассемблера разбирается с ней сам) и переходить к анализу очередной строки исходного текста ассемблерной программы.
В самом простом случае нашу задачу можно решить легко — в очередной строке выделить метку, если она есть, затем выделить название команды, и если она является ХММ-командой, то продолжить обработку строки. Если очередная строка не является ХММ-командой, то копируем ее полностью в выходной файл. Если очередная строка — ХММ-команда, то локализуем операнды и определяем их тип. По крайней мере один из операндов должен быть регистром. Если строка синтаксически верна для конкретной ХММ-команды, то формируем ее аналог, понятный для восприятия используемым нами транслятором ассемблера. Этот процесс может быть похожим на первый способ формирования ХММ-команд с помощью включаемого файла iaxmm.inc.

Синтаксический анализ

Если веденная строка исходной программы является строкой с ХММ-командой, то необходимо получить ее лексическую свертку для проведения синтаксической проверки и подготовки к дальнейшему преобразованию. Здесь простор для творческих изысканий весьма велик. Поэтому мы не будем повторять соответствующий материал главы 2, а обратимся к тексту программы, который выполняет такое преобразование. Его описание содержит множество технических подробностей, не относящихся к предмету книги. По этой причине программа-препроцессор с соответствующими пояснениями деталей реализации вынесена на дискету, прилагаемую к книге. Протестировать эту программу вы можете на примерах программ с ХММ-командами, приведенными в первой части данной главы.


Введение

Продавай мобильный контент со своего сайта

Введение

Проявил себя — закрепи...
От Фоменко

Профессия программиста удивительна и уникальна. Давно уже настало время настоящего философского осмысления этой сферы человеческой деятельности, действительно обладающей какими-то особенными, для людей непосвященных чуть ли не магическими, свойствами. Если не брать в рассмотрение коммерческую сторону, то можно сказать, что чужих людей в этой области профессиональной деятельности нет. В чем же ее особенность? Наиболее точно по этому поводу высказался Фредерик Брукс в главе «Пятьдесят лет удивления, восхищения и радости» своей книги «Мифический человеко-месяц, или как создаются программные системы>: «Немногим Бог дает право зарабатывать на жизнь тем, чем они с радостью занимались бы по собственной воле, по увлечению. Я благодарен судьбе». И далее: «Область связанных с компьютерами знаний претерпела взрыв, как и соответстующая технология. Будучи аспирантом в середине 50-х, я мог прочесть все журналы и труды конференций. Я мог оставаться на современном уровне во всей научной дисциплине. Сегодня мне в моей интеллектуальной жизни приходится с сожалением расставаться с интересами то в одной, то в другой подобласти, поскольку количество документов превысило всякую возможность справиться с ними. Масса интересов, масса замечательных возможностей для учебы, исследований и размышлений. Чудесное затруднение! Не только конца не видно, но и шаг не замедляется. В будущем нас ожидают многие радости». Что еще к этому добавить?

Структура книги

Эту книгу можно рассматривать как своеобразную форму программного продукта. Даже беглое ее пролистывание показывает, как много в ней программного кода. Более того, так как ассемблерный код неэк'ономичен с точки зрения использования поверхности листа бумаги для его записи, то в тексте книги приведены лишь значимые для каждого конкретного контекста изложения фрагменты программ. Полные тексты этих программ содержатся на дискете. Некоторые наиболее объемные по размеру исходного текста программы целиком вынесены на дискету без приведения их фрагментов в тексте книги. Для эффективной работы с ними читателю следует внимательно следить за ссылками на них и соответствующими пояснениями. Насколько это возможно, программы были проверены, но было бы опрометчиво утверждать, что вероятность появления ошибок в них равна нулю. Любому программисту, даже имеющему очень скромный опыт практической работы, известно, что вероятность последней ошибки есть всегда. В связи с этим просьба к читателям сообщать о найденных ошибках по указанным ниже адресам электронной почты. Книга состоит из 10 разных по объему глав. Ниже приведены краткие сведения о цели и характере содержимого каждой из этих глав.

  • Глава «Программирование целочисленных арифметических операций». В этой главе приводятся исчерпывающие сведения об алгоритмах реализации четырех основных арифметических операций над числами различной разрядности. На практике нередко возникают ситуации, когда численные значения данных выходят за пределы максимально представимых диапазонов чисел в компьютере. В этом случае нужно использовать алгоритмы для производства вычислении над многобайтными (с разрядностью от 1 до оо) числами. В этой главе приведена реализация этих алгоритмов для двоичных и двоично-десятичных (BCD) чисел. Кроме этого, глава содержит описание алгоритмов генерации псевдослучайных последовательностей, проблема организации которых также возникает достаточно часто.
  • Глава «Сложные структуры данных». Содержимое этой главы значительно дополняет и расширяет содержимое одноименного урока 12 учебника. Достаточно перечислить номенклатуру рассмотренных структур данных, названных «сложными», — это множества, массивы, структуры, таблицы. одно- и двусвязные списки, деревья. Для демонстрации работы с этими «сложными структурами данных» подобраны интересные и востребованные на практике алгоритмы. Так, работа с массивами показана на примерах популярных алгоритмов сортировки и поиска, работы с матрицами. Работа со структурами иллюстрируется на примерах организации массивов структур — таблиц. При этом наряду с обычными таблицами рассмаривается специальный класс таблиц — таблиц с вычисляемыми входами, или хэш-таблиц. Интересные примеры иллюстрируют выполнение основных операций над элементами одно- и двусвязных списков. Работа с сетью показана на примере организации в программе такой структуры, как конечный автомат. Заканчивается глава рассмотрением элементов компиляции программ. Это логичное и оправданное с практической точки зрения завершение главы о сложных структурах данных. Наверняка каждому из вас приходилось организовывать элементарный языковой интерфейс с пользователем и обрабатывать его ввод. О существующих подходах к практической реализации формальных механизмов распознавания ввода пользователя вы узнаете из главы 2.
  • Глава «Процедуры в программах ассемблера». Также достаточно интересная глава, которая является существенным дополнением урока 14 «Модульное программирование» учебника. Большое внимание уделено в ней реализации рекурсивных процедур в программах на ассемблере. Реализация рекурсии в любом языке — предмет дискуссии, причем от полного неприятия до слепого поклонения. Мы не стали принимать участие в этой дискуссии, а просто показали технологию разработки рекурсивных программ на ассемблере. Попутно обсуждению подвергаются проблемы передачи параметров и сохранения локальных параметров процедуры. В несколько более скромном объеме приведены сведения об организации вложенных процедур. В этой главе также содержится очень важный материал для программирующих под Windows — о разработке и об организации работы с DLL-библиотеками в программах на ассемблере.
  • Глава «Обработка цепочек элементов» содержит пример реализации некоторых полезных алгоритмов поиска подстроки в текстовой строке. Материал этой главы представляет собой существенное дополнение (а где-то предлагает и альтернативные решения) урока 11 «Цепочечные команды» учебника.
  • В Глава «Работа с консолью в программах на ассемблере» в полном объеме рассматривает проблему ввода информации с клавиатуры и вывода информации на экран компьютера. Для этого приведено описание соответству-щих средств BIOS, операционных платформ MS DOS и Windows.
  • Глава «Преобразование чисел» предлагает набор алгоритмов для преобразования представлений чисел между различными системами счисления. Этот вид преобразования данных также часто встречается на практике. Из-за того что в ассемблере нет для этого соответствующих средств, каждый из программистов решает эту задачу по-своему, исходя из своего опыта и знаний.
  • Глава «Работа с файлами в программах на ассемблере» содержит систематизированные сведения по работе с файлами из программ на ассемблере. За неимением соответствующих средств языковой поддержки со стороны программистов на практике также наблюдается «свободное творчество». Приведенные сведения относятся к уровню практической реализации задач в MS DOS и Windows с учетом возможности использования как длинных, так и коротких имен.
  • Как оценить эффективность кода, который вы пишете? В этом вам поможет материал главы «Профайлер». В ней содержится описание двух макрокоманд, использующих средства микропроцессора Pentium, которые помогут вам решить эту задачу.
  • Глава «Вычисление CRC» описывает.варианты решения одной интересной практической задачи, которой на практике можно найти достаточно много применений. Обладая этим инструментом, можно проводить быстрые оценочные проверки целостности данных, которыми манипулирует ваша программа. Суть этих алгоритмов с первого взгляда не очень очевидна, поэтому данная глава содержит подробное их объяснение.
  • Глава «Программирование ХММ-расширения» призвана дополнить материал урока 20 «ММХ-технология микропроцесоров Intel» учебника. В ней на примере реализации нескольких характерных алгоритмов приводятся сведения о порядке использования команд ХММ-расширения микропроцессора Intel. Более того, предлагается альтернативный подход к решению проблемы поддержки работы с этими командами в программах на ассемблере. Механизм решения этой проблемы можно использовать и в других случаях, когда требуется обеспечить поддержку новых команд микропроцессора, работая со старыми версиями компилятора (и не только ассемблера), процесс обновления которых по объективным причинам значительно значительно более инертен, чем процесс обновления системы команд микропроцессора.

Необходимо подчеркнуть тот факт, что программы книги реализованы с помощью двух версий ассемблера — 16- и 32-разрядной. Выбор операционной платформы и средств реализации задач книги производился исходя из их конкретной постановки. Главный критерий здесь — подчеркнуть особенности реализации алгоритма. Если для этого достаточно платформы MS DOS, то задача реализовывалась с использованием средств этой ОС. При необходимости вы достаточно легко сможете доработать свою программу так, чтобы она функционировала в среде Windows. Для этого книга содержит достаточно практических примеров. Многие задачи реализованы непосредственно для функционирования в среде Windows. Сделано это в основном на примерах консольных приложений Windows. Тем самым был дополнен материал учебника, в котором процесс разработки оконных приложений для Windows описан достаточно подробно.
Следует отметить, что для пояснения наиболее сложных алгоритмов в некоторых примерах книги введен псевдоязык, синтаксис которого рассмотрен в главе 2. Для экономии места такой вид пояснения используется для текстов программ, вынесенных на дискету. Поэтому имейте в виду, что исходные тексты программ на дискете немного отличаются от соответствующих текстов программ в книге — прежде всего тем, что программы на дискете снабжены более подробными комментариями и псевдокодом. Для общеизвестных алгоритмов приведены ссылки на источники, где с ними можно познакомиться более подробно. Более того, система нумерации программ сделана так, чтобы указать читателю ссылку на источники, которые можно использовать для более глубокого изучения соответствующих алгоритмов. Следует отметить, что псевдоязык использован с двоякой целью. Первую из целей мы уже отметили, что касается второй, то она заключается в том, чтобы подготовить читателя к изучению теории компиляции. Так, пояснение примеров программ на ассеблере производится не только с помощью обычных комментариев, но и с использованием фрагментов программы на псевдокоде. Такое смешанное пояснение ассемблерной реализации алгоритма не только дополнительно иллюстрирует сам алгоритм, но и показывает суть третьего и четвертого этапов .компиляции программы — генерации и оптимизации кода, так как при этом можно видеть, какие ассемблерные (машинные) конструкции соответствуют конструкциям языка высокого уровня.
Завершает книгу достаточно подробный список литературы, который был использован в процессе подготовки книги и который можно рекомендовать как основу для дальнейшего изучения затронутых в ней вопросов.