Ассемблер под Windows для чайников. Часть 12

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

Массив — это структурированный тип данных, состоящий из нескольких элементов одного типа. В высокоуровневых языках существуют специальные средства для описания массивов. В ассемблере такие средства отсутствуют, а потому массив обычно обозначается просто как линейная область данных. Например, чтобы обозначить одномерный массив однобайтовых элементов и выделить под него память, обычно достаточно директивы RB (Reserve Bytes — Зарезервировать Байты), за которой следует число резервируемых байт. Если элемент массива должен иметь размер в два байта, будем использовать директиву RW (Reserve Words) и число резервируемых слов. Для четырехбайтовых элементов — соответственно RD и количество двойных слов и так далее. Если мы хотим не только выделить память, но и сразу внести в массив значения элементов, можно описать элементы при помощи стандартных директив описания данных — таких, как DB, DW, DD и т.д. За директивой определения данных должно следовать одно или несколько числовых выражений, разделенных запятыми. Эти выражения определяют значение элементов массива. Если вместо значения указан символ вопроса, то значение считается неопределенным, то есть таким же, как если бы мы использовали директиву резервирования данных. Размер элемента зависит от того, какая директива используется.

Табл. 1. Директивы определения и резервирования данных

Размер 
элемента 
в байтах
Директивы 
определения 
данных
Директивы 
резервирования 
данных
1db
file
rb
2dw
du
rw
4dd 
6dp
df
rprf
8dqrq
10dtrt


Для инициализации элементов массива одним и тем же значением или повторяющейся цепочкой значений можно использовать специальный оператор DUP. Количество дубликатов указывается перед DUP, а дублируемое значение или цепочка значений, разделенных запятыми, указывается после оператора DUP и заключается в скобки. Например, db 5 dup (1,2) означает, что необходимо создать 5 копий указанной последовательности из двух байт. Массивы бывают статические и динамические. Динамические массивы отложим на потом, а пока попробуем разобраться хотя бы со статическими. Размер статического массива не меняется на протяжении времени работы программы. Поэтому с ними работать несколько проще. Достаточно выделить область памяти под статический массив лишь один раз, и потом остается лишь работать с содержимым массива — его элементами.

Перечислим основные операции, которые мы можем производить над массивами:
— ввод данных в массив;
— вывод данных из массива;
— поиск значения в массиве;
— сортировка элементов.

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

format PE GUI 4.0
entry start

include 'win32a.inc'
;константы
MASSIZE=5 ;количество элементов массива
BUFSIZE=3 ;макс.кол-во знаков в элементе
;(для буфера вывода десятичных значений)

section '.data' data readable writeable

_class db 'FASMWIN32',0
_cedit db 'EDIT',0
_title db 'Работа с массивами',0
_error db 'Ошибка запуска.',0
buf rb BUFSIZE+1 ;+1 для нуль-терминатора
mas db 123,23,3,4,5
hmas dd MASSIZE dup (?)

wc WNDCLASS 0,WindowProc,0,0,NULL,NULL,NULL,COLOR_BTNFACE+1,NULL,_class

msg MSG

section '.code' code readable executable

start:
invoke GetModuleHandle,0
mov [wc.hInstance],eax
invoke LoadIcon,0,IDI_APPLICATION
mov [wc.hIcon],eax
invoke LoadCursor,0,IDC_ARROW
mov [wc.hCursor],eax
invoke RegisterClass,wc
test eax,eax
jz error

invoke CreateWindowEx,0,_class,_title,WS_VISIBLE+WS_DLGFRAME+ WS_SYSMENU,128,128,256,192,NULL,NULL,[wc.hInstance],NULL
test eax,eax
jz error

msg_loop:
invoke GetMessage,msg,NULL,0,0
cmp eax,1
jb end_loop
jne msg_loop
invoke TranslateMessage,msg
invoke DispatchMessage,msg
jmp msg_loop

error:
invoke MessageBox,NULL,_error,NULL,MB_ICONERROR+MB_OK

end_loop:
invoke ExitProcess,[msg.wParam]

proc WindowProc hwnd,wmsg,wparam,lparam
push ebx esi edi
cmp [wmsg],WM_CREATE
je .wmcreate
cmp [wmsg],WM_DESTROY
je .wmdestroy
.defwndproc:
invoke DefWindowProc,[hwnd],[wmsg],[wparam],[lparam]
jmp .finish
.wmcreate:
;создаем поле под каждый элемент массива mas
;и помещаем дескриптор каждого поля в массив hmas
mov esi,5
mov edi,5
xor ebx,ebx
.create_cycl:
invoke CreateWindowEx,0,_cedit,0,WS_VISIBLE+WS_CHILD+WS_BORDER+ ES_READONLY+ES_RIGHT,esi,edi,40,20,[hwnd],ebx,[wc.hInstance],0
test eax,eax
jz error
mov [hmas+ebx*4],eax
add esi,50
inc ebx
cmp ebx,MASSIZE
jne .create_cycl
.out_mas:
;заполняем поля значениями элементов массива
xor ebx,ebx
.masout_cycl:
mov ah,[mas+ebx]
lea edi,[buf+BUFSIZE]
.dec:
movzx ax,ah
aam
or al,30h
dec edi
mov byte [edi],al
test ah,ah
jnz .dec
invoke SendMessage,[hmas+ebx*4],WM_SETTEXT,0,edi
inc ebx
cmp ebx,MASSIZE
jne .masout_cycl
jmp .finish
.wmdestroy:
invoke PostQuitMessage,0
xor eax,eax
.finish:
pop edi esi ebx
ret
endp

section '.idata' import data readable writeable

library kernel32,'KERNEL32.DLL',\
user32,'USER32.DLL'

include 'api\kernel32.inc'
include 'api\user32.inc'

Для верного отображения значений элементов они должны находиться в диапазоне от 0 до 255 включительно, то есть являться однобайтовыми беззнаковыми целыми. Позже, научимся отображать и числа со знаком, но сейчас нам это только осложнит задачу и отвлечет от понимания принципов работы с массивами. Раз самое длинное число у нас состоит из трех знаков, то и буфер для его вывода (buf) будет иметь размер 3+1 байта. Четвертый байт выделяется специально под нолик, обозначающий конец строки. Значения всех пяти элементов массива (mas) мы на данном этапе определим сразу в секции данных, поскольку функция ввода значений массива пока что отсутствует в нашей программе. Массив hmas (h означает handles — дескрипторы), состоящий из пяти двойных слов (MASSIZE=5), будет содержать дескрипторы полей для вывода значений элементов массива. Так нам на первых порах будет проще: берем значение первого элемента mas, преобразовываем его к символьному виду и выводим в поле, дескриптор которого хранится в первом элементе hmas. Потом выводим значение второго элемента mas в поле, дескриптор которого берем соответственно из второго элемента hmas. Аналогично поступаем с третьим элементом, четвертым, пятым… да хоть миллионным, лишь бы все поля уместились на экране, а массивы значений и дескрипторов — в памяти компьютера. Разумеется, весь этот вывод нам выгоднее оформить в повторяющемся цикле. Этот цикл и будет скелетом функции вывода.

Не будем останавливаться на участках подготовки и создания окна, так как это было подробно описано еще в первых частях данного курса. Обратим внимание сразу на обработчик сообщения WM_CREATE. Здесь мы создаем поле под каждый элемент массива mas и помещаем дескриптор каждого созданного поля в соответствующий элемент hmas. Это еще не сам вывод, но создание полей для вывода значений. Перед циклом мы помещаем в esi и edi соответственно X- и Y-координаты расположения первого поля относительно левого верхнего угла клиентской области нашего основного окна. На каждом шаге цикла мы будем увеличивать esi (координата X) на 50, исходя из того, что ширина самого поля равна 40, а 10 — зазор между соседними полями. Ввиду того, что пять полей прекрасно умещаются в одну строку, содержимое edi мы сейчас изменять не станем. Его на данном этапе можно было бы и не использовать вообще, но это — так сказать, задел на будущее. Подробно разберем, что происходит в цикле ".create_cycl:". Первым делом мы создаем окно стандартного класса "EDIT" с указанными параметрами. Содержимое EBX выступает в качестве идентификатора каждого нового окна, поэтому поля будут иметь идентификаторы 0, 1, 2 и т.д. Это, конечно, не самый красивый вариант, но в данном примере мы не будем обращаться к окнам по их идентификаторам, а потому имеем полное право указать даже один и тот же идентификатор для всех создаваемых полей. Но я решил их все же сделать отличными друг от друга без каких-либо дополнительных действий. Впрочем, это мелочи. Главное назначение регистра EBX в этом цикле — счетчик. Сразу после создания окна мы убеждаемся в отсутствии ошибки и помещаем содержимое EAX (дескриптор созданного окна) в очередной элемент массива hmas. Вам понятно, как это происходит? Элементы считаются в программировании с нулевого (а не с первого) именно потому, что первый элемент никуда не смещен относительно начала строки или массива. Он первый, он находится по адресу начала массива, и его смещение — нулевое. А вот второй элемент уже имеет смещение на размер одного (первого) элемента, третий — на размер двух элементов, четвертый — трех и т.д. Размер дескриптора, как и большинства других элементов в 32-битной системе, равняется 32 битам или 4 байтам. Поэтому мы умножаем 4 на EBX, чтобы получить смещение очередного элемента от начала массива. А затем прибавляем к нему еще смещение самого hmas относительно начала сегмента. Таким образом, для первого элемента смещение получится равным hmas+0, для второго — hmas+4, для третьего — hmas+8 и т.д. Обычно в качестве счетчика используют регистр ECX, который изначально и задуман для этих целей. Однако API-функции (в нашем случае функция CreateWindowEx) используют некоторые регистры в своих целях и могут изменить содержимое этих регистров за время своего исполнения. Гарантированно сохраняется лишь содержимое регистров EBX, EBP, ESP, ESI, EDI. Поэтому содержимое остальных регистров по необходимости следует сохранить командой PUSH, а потом восстановить командой POP. В нашем случае такой необходимости нет, так как в данном цикле мы вполне можем обойтись регистрами EBX, ESI, EDI. Итак, мы сохраняем дескриптор очередного созданного поля EDIT в массив hmas. Увеличиваем ESI на 50, чтобы следующее поле было создано на 50 точек правее. Увеличиваем EBX на единицу. И повторяем цикл при условии, что EBX еще не равен количеству элементов массива. Если EBX=MASSIZE — значит, мы создали поля для всех элементов массива и можем переходить к выводу значений в эти поля. Тут же прописана метка ".out_mas:" для того, чтобы можно было впоследствии осуществлять повторный вывод текущих значений без повторного создания полей. Перед циклом вывода обнуляем наш счетчик EBX. Задвигаем в AH значение текущего элемента (mas+счетчик*размер_элемента). Так как размер элемента у нас 1 байт, то есть единица, пишем просто mas+EBX. Загружаем в EDI эффективный адрес последнего символа буфера — нуль-терминатора. В данном примере буфер у нас состоит из трех символов + нуль-терминатор, потому что константа BUFSIZE у нас равна трем. Адрес первого символа буфера — buf, адрес второго — buf+1, …, адрес нуль-терминатора — buf+3 или buf+BUFSIZE. Адрес нуль-терминатора нам, конечно, не нужен. Но для корректного перевода числа из машинного представления в символьное нам необходимо записывать символы, начиная с конца. Поэтому мы сразу поместим в EDI адрес последнего символа, а потом будем уменьшать EDI на единицу перед записью каждого следующего символа. Таким образом мы запишем сначала число единиц, затем — число десятков, затем — сотен. Для приведения числа к символьному виду можно было бы воспользоваться и API-функцией ОС — например, wsprintf. Но давайте на всякий случай научимся делать такие вещи вручную. Это может сильно помочь вам в понимании низкоуровневых алгоритмов преобразования типов.

Табл. 2. Размещение разрядов в буфере

Смещениеbuf+0buf+1buf+2buf+3
Назначениесотнидесяткиединицынуль-терминатор


Цикл ".masout_cycl:" повторяется для каждого элемента массива, а цикл ".dec:" — для каждого десятичного разряда выводимого элемента. На первом шаге цикла ".dec:" в AH находится значение текущего элемента массива. Чтобы выделить из него число единиц, мы копируем его в AL. Команда AAM, как вы уже знаете, делит содержимое AL на 10 и помещает частное в AH, а остаток — в AL. Остаток от деления числа на 10 и будет числом единиц в нем. Частное на следующем шаге цикла можно будет снова поделить на 10 и получить в остатке уже число десятков, а потом — и сотен. Но сейчас, чтобы привести число единиц в AL к символьному виду, мы выполняем команду or al,30h.

Табл. 3. Символьное представление чисел в ASCII

СимволШестнадцатеричное
значение (ASCII-код)
Двоичное 
значение (ASCII-код)
030h00110000b
131h00110001b
232h00110010b
333h00110011b
939h00111001b


Как видно из третьей таблицы, шестнадцатеричное значение символа отличается от машинного представления числа ровно на 30h. То есть, чтобы число от нуля до девяти преобразовать в значение соответствующего ему символа, достаточно прибавить к числу 30h. Двоичные значения я тоже привел неслучайно. Из них понятно, что 30h — это, по сути, два установленных бита — пятый и шестой. То есть, если мы к двоичному значению числа от 0 до 9 (от 0b до 1001b) применим команду OR со вторым операндом 30h (00110000b), то получим соответствующий этому числу ASCII-код символа. Стало быть, в таких случаях можно на равных правах использовать как команду ADD, так и команду OR. Привели число единиц к символьному виду, уменьшаем EDI (а то он у нас еще на нуль-терминатор указывает, а нам уже единицы в буфер вписать требуется). Копируем байт из AL по адресу, содержащемуся в EDI. Тестируем AH на предмет нулевого содержимого. Если в AH (ведь это частное нашего последнего деления) содержится ноль — значит, десятков в данном элементе нет (или нет сотен на втором шаге цикла), и мы не станем повторять цикл ".dec:", а перейдем к выводу полного значения данного элемента в соответствующее поле. Если же AH нулю не равен — значит, еще остались старшие разряды, которые необходимо обработать аналогичным способом. Функцией SendMessage выводим значение элемента в текущее поле. Дескриптор берем по адресу hmas+ebx*4. Выводимый текст — по адресу, хранящемуся в EDI. Обратите внимание, что не по адресу buf, а именно по адресу в EDI! Буфер целиком у нас содержит неопределенное значение. Если, к примеру, первый элемент состоял из трех десятичных знаков, а второй — из двух, то на момент вывода второго элемента в окно буфер будет содержать в первом байте сотни, оставшиеся от первого элемента, и только со второго байта пойдут десятки текущего элемента. Поэтому верное значение текущего элемента у нас будет только с адреса в EDI. Именно по данному адресу в буфер был прописан последний старший разряд текущего элемента. Увеличиваем EBX на единицу, и, если он еще не равен MASSIZE, переходим в начало цикла ".masout_cycl:" для вывода следующего элемента в следующее поле. Иначе прыгаем на финиш ввиду того, что все элементы выведены на экран, то есть в окно на экране. Ну, а теперь тем, кому "деление на десять" показалось слишком сложным циклом, покажу упрощенный вариант с использованием функции wsprintf. Преобразование значения в символьный вид выполнит за нас эта функция, поэтому мы сможем избавиться от цикла ".dec:". Мы обсудили возможности данной функции еще в четвертой части курса. В первом параметре указывается буфер для результата, во втором — задается формат, а в остальных указываются данные, которые необходимо преобразовать. Так что нам остается лишь добавить в секцию данных строку с образцом формата:
form db '%u',0
Управляющий символ '%u' означает тип "unsigned integer" — беззнаковое целое. И теперь мы можем цикл вывода упростить до следующего вида:

.out_mas:
xor ebx,ebx
.masout_cycl:
movzx eax,[mas+ebx]
invoke wsprintf,buf,form,eax
invoke SendMessage,[hmas+ebx*4],WM_SETTEXT,0,buf
inc ebx
cmp ebx,MASSIZE
jne .masout_cycl
jmp .finish

Функции 32-битных ОС обычно нуждаются в том, чтобы параметры были 32-битными. Поэтому командой movzx мы расширяем 8-битное значение из ячейки [mas+ebx] до 32-битного в регистре eax. Вызываем wsprintf и получаем в буфере искомое значение в символьном виде. Далее все так же, как и в первом варианте программы, но… Помните, друзья мои, что бесплатный сыр бывает только в мышеловке! Используя для такого простого преобразования всю мощь универсальной функции wsprintf, мы совершаем кучу лишних действий и слегка теряем в скорости исполнения. Конечно, на такой простенькой программе вы этого не заметите. На современных мощных компьютерах вы также можете не увидеть разницу невооруженным глазом даже в более прожорливой программе. Но вы же планируете в дальнейшем писать намного более серьезные вещи, не так ли? А значит, всегда следуйте хакерским путем: сперва используйте наиболее простой для понимания метод, а затем оптимизируйте его, преобразуя в более простой для исполнения процессором. Сейчас часто можно услышать, что экономия в несколько тактов или пару байт памяти ничего не дает. Даже и не пытайтесь с этим спорить. Но для себя не забывайте, что вычислительные мощности с каждым днем растут, а большинство программ почему-то становятся все более тормознутыми. Весьма странная закономерность! Да не будем обращать внимание на ошибки окружающих, а вместо этого давайте все же не лениться экономить такты и байты. На сегодня все. В следующий раз продолжим о массивах.

Все приводимые примеры были протестированы на правильность работы под Windows XP и, скорее всего, будут работать под другими версиями Windows, однако я не даю никаких гарантий их правильной работы на вашем компьютере. Исходные тексты программ вы можете найти на форуме: сайт

BarMentaLisk, q@sa-sec.org


Компьютерная газета. Статья была опубликована в номере 34 за 2008 год в рубрике программирование

©1997-2024 Компьютерная газета