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

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

В прошлый раз я дал вам весьма трудное задание: самостоятельно запрограммировать удаление элемента из массива. Что ж, сегодня давайте разберемся, чего там было сложного для вас. Начнем, пожалуй, с функции удаления элемента из массива. Для того, чтобы удалить элемент из массива, нам необходимо всего лишь сдвинуть следующий за ним элемент на его место, затем сдвинуть следующий элемент на место предыдущего и т.д. Например, имеем массив из шести букв алфавита: A,B,C,D,E,F. Чтобы удалить из него элемент D, необходимо сдвинуть E на место D, затем сдвинуть F на место E. Получим массив: A,B,C,E,F,F, потому что мы не на самом деле сдвигаем, а лишь копируем (команда mov). Последняя F — лишняя, поэтому для правильного дальнейшего восприятия нашего массива программой необходимо уменьшить переменную, содержащую размер массива, на число удаленных элементов. То есть на единицу. Итак, допустим, что размер массива у нас содержится в переменной [n], а сам массив находится по адресу [mas]. Тогда программа удаления элемента из массива будет выглядеть примерно так:

section '.data' data readable writeable
mas db 'ABCDEF'
n db 6

section '.code' code readable executable

start:
mov esi,4
mov ecx,[n]
sub ecx,esi
add esi,mas
mov edi,esi
dec edi
cld
rep movsb
dec [n]

Смещение первого сдвигаемого элемента мы поместим в регистр ESI. Напомню, что смещение A=0, смещение B=1, смещение E=4, а этот элемент, как вы понимаете, будет помещен на место удаляемого элемента D. Далее мы помещаем в ECX общее количество элементов массива из переменной [n]. ECX будет отсчитывать количество сдвигаемых элементов, поэтому, чтобы он содержал лишь число элементов, подлежащих сдвигу, мы вычитаем ESI из ECX. Теперь в ECX содержится двойка, ведь именно два элемента нам надо сдвинуть влево: E и F. Команда MOVS требует, чтобы в ESI находился адрес источника, а в EDI — адрес приемника. Поэтому мы прибавляем к ESI адрес массива и получаем в ESI адрес элемента E. Копируем этот адрес в EDI и уменьшаем EDI на единицу, чтобы получить адрес символа D. Командой CLD сбрасываем флаг DF в ноль, чтобы определить направление обработки элементов слева направо. Выполняем сдвиг: rep movsb, который будет повторен дважды, ввиду того, что в ECX содержалось число 2. Уменьшаем переменную [n] на единицу. Этот код прост и понятен, но он — лишь скелет функции удаления элемента из массива. В нем не производится никаких проверок и отсутствует возможность выбора удаляемого элемента. Давайте теперь увеличим возможности этой функции и оформим ее соответственно в виде полноценной функции к нашей программе обработки массива из прошлого занятия. Полноценная функция должна осуществлять поиск заданного элемента в массиве и его удаление. Если элемент был найден и удален, функция возвращает в EAX нулевое значение. Если элемент отсутствует, функция выдаст соответствующее сообщение об ошибке и вернет в EAX значение -1 (0FFFFFFFFh). На входе в функцию, то есть непосредственно перед ее вызовом, в AL должно содержаться искомое значение. Вот и все условия. Помещаем в AL значение, которое требуется удалить из массива, вызываем функцию удаления командой CALL, проверяем EAX. Если он не равен -1, значит, элемент был успешно удален, и обновленный массив готов к повторному выводу на экран. Это наши планы. Осталось их реализовать:

proc DeleteElement

cld
mov ecx,[n]
lea edi,[mas]
repne scasb
je .del_el
;Элемент не найден
invoke MessageBox,0,_terror3,0,0
mov eax,-1
ret
;Удаляем элемент
;по адресу EDI-1
.del_el:
mov esi,edi
dec edi
rep movsb
dec [n]
;Удаляем последнее поле
mov ebx,[n]
invoke SendMessage,[hmas+ebx*4],WM_CLOSE,0,0
xor eax,eax
ret
endp

Вот такая вот загогулина! Неужели что-то непонятно? Ну да ладно, сейчас разберемся. Итак, как мы условились, считаем, что на момент начала исполнения функции в AL уже лежит значение от 0 до 255, которое нужно найти и удалить. Поэтому устанавливаем флаг направления поиска в положение "слева направо", то есть сбрасываем DF ноль. Задвигаем в счетчик ECX текущее количество элементов массива. Загружаем в EDI эффективный адрес массива [mas]. В данной ситуации команда lea edi,[mas] дает такой же результат, как и mov edi,mas, но правильнее будет использовать все-таки LEA. Сканируем массив до тех пор, пока ECX не уменьшится до нуля или пока результат сравнения — NE (Not Equal — не равно). По окончании сканирования прыг на удаление элемента (je .del_el), если E (равно). Иначе, если сравнение последнего элемента завершилось результатом NE, значит, заданный элемент найден не был. В таком случае выводим "устрашающий" MessageBox (text error я сократил как t error, и получился террор3=), помещаем в EAX минус единицу — условный код ошибки — и выходим из функции (ret). На метку ".del_el:" мы попадаем лишь в случае, если значение было найдено в массиве. При таком раскладе EDI уже будет содержать адрес следующего за найденным элемента. Чтобы корректно удалить элемент, нам надо, чтобы адрес следующего элемента хранился в ESI, а в EDI был адрес удаляемого элемента. Поэтому мы копируем EDI в ESI, после чего уменьшаем EDI на размер элемента, чтобы он указывал на удаляемый элемент. ECX у нас и так хранит число элементов, оставшихся до конца массива, поэтому смело производим сдвиг: rep movsb. По окончании сдвига уменьшаем на единицу число элементов массива. С этого момента наш массив уже можно считать обновленным. Из него удален один элемент, и переменная, обозначающая количество действительных элементов, тоже обновлена и соответствует действительности. Но ведь полей, в которых мы отображаем значения наших элементов, не убавилось. Если не решить эту проблему, то при выводе обновленный массив будет неверно отображен. Последнее поле будет отображать значение бывшего последнего элемента, который к этому моменту будет уже находиться на бывшем месте предпоследнего. В общем, чтобы этого не допустить, нам надо, удалив элемент из массива, удалить и поле. Будем сдвигать дескрипторы полей в массиве hmas, а затем в цикле подвинем все окошки на место удаленного? Нет, не угадали. Все намного проще. Нам достаточно удалить последнее поле, и все готово. Ведь после того, как мы сдвинули оставшиеся элементы массива на место удаляемого, именно последний элемент остался лишним и перестал числиться в массиве. С учетом того, что переменную n мы уже уменьшили, дескриптор поля, подлежащего удалению, будет находиться по адресу [hmas+[n]*4]. Только значение переменной [n] нам придется передать через регистр, потому что мы не можем записать в вызове функции, что дескриптор окна находится по адресу, значение которого находится по адресу.

Кстати, вы еще не забыли, что в FASM'е квадратные скобки означают? Ну и как вам не стыдно такое забыть? Эдак вы половину материала не понимаете и думаете, что умом не вышли. А на самом деле просто невнимательно читали первые статьи. Второй и последний раз объясняю, но в этот раз запомните уж раз и навсегда! Квадратные скобки указывают на то, что значение, над которым производится действие, находится в памяти по адресу, указанному в квадратных скобках. Например, в нашем случае n — это константа, значением которой является адрес ячейки в памяти, выделенной под хранение значения переменной [n]. Компилятор при сборке программы заменяет повсеместно символ n конкретным значением — адресом, по которому находится наша переменная [n]. Поэтому значение n во время работы программы изменять нельзя. Изменять можно лишь значение, находящееся по адресу n, то есть в ячейке [n]. Регистры — это другое дело. Они могут содержать как непосредственное значение, так и адрес в памяти. Все зависит только от программиста. Командой mov ebx,[n] мы помещаем в ebx значение ячейки по адресу n — количество элементов массива. Если бы мы написали mov ebx,n, то в регистр было бы помещено не значение переменной, а адрес, по которому она хранится. Однако далее при посылке сообщения окну мы указываем в качестве дескриптора окна [hmas+ebx*4]. Это значит, что дескриптор будет прочитан из ячейки по адресу учетверенного количества элементов + адрес массива дескрипторов hmas. По адресу hmas у нас хранится четырехбайтовый дескриптор первого поля, по адресу hmas+4 — дескриптор второго поля и т.д. По адресу hmas+([n]-1)*4 у нас хранится дескриптор последнего поля. Но, так как мы только что уменьшили значение [n] на единицу, то по адресу hmas+[n]*4 у нас находится дескриптор бывшего последнего, все еще не удаленного окошка. И поэтому сейчас самое время его удалить. Посылаем окну сообщение WM_CLOSE, не имеющее параметров. И с этих пор его дескриптор считаем недействительным. Помещаем в EAX ноль как показатель того, что удаление прошло успешно, и возвращаемся из функции. Двигаемся в обратном направлении. Мы условились, что функция, удаляющая элемент, получает его значение через регистр AL. Значит, надо сделать так, чтобы введенное пользователем значение туда как-то попало перед вызовом функции удаления. Для этого создаем поле для ввода удаляемого значения и кнопку удаления. Также сразу привожу то, что нам понадобится добавить в секцию данных в шаблон из предыдущего занятия:

n dd MASSIZE
_terror1 db 'Ошибка запуска.',0
_terror2 db 'Введите число 0-255 !',0
_terror3 db 'Элемент не найден.',0
_tdelbut db 'Удалить элемент',0
hedit dd ?
buf rb BUFSIZE+1

;обработчик кнопки "Удалить элемент"
.de:
call ASCII2BIN
cmp eax,-1
je .finish
call DeleteElement
cmp eax,-1
je .finish
jmp .out_mas

proc ASCII2BIN

invoke SendMessage,[hedit],WM_GETTEXT,BUFSIZE+1,buf
test eax,eax
jz .error
lea ebx,[buf]
xor eax,eax

.dec_check_loop:
cmp byte [ebx],0 ;if end of string
je .finish

mov edx,eax ;умножаем
shl edx,3 ;EAX
shl eax,1 ;на
add eax,edx ;10

xor edx,edx
mov dl,byte [ebx]
sub edx,'0'
cmp edx,9
ja .error
add eax,edx

inc ebx
jmp .dec_check_loop

.error:
invoke MessageBox,0,_terror2,0,0
mov eax,-1
ret
.finish:
cmp eax,255
ja .error

ret
endp

При каких обстоятельствах будет выполнен код обработчика кнопки "Удалить элемент", думаю, объяснять не надо. Для удобства понимания я разделил удаление элемента на 2 процедуры: получение значения из текстового поля и удаление элемента с указанным значением. После каждого вызова производится проверка корректного выполнения функции. Процедуру удаления мы уже рассмотрели. Теперь рассмотрим процедуру получения значения из текстового поля, преобразование его в двоичный вид из символьного. Процедура ASCII2BIN осуществляет именно это. Отправляем сообщение WM_GETTEXT окну, в котором предположительно находится удаляемое значение. BUFSIZE+1 указывает, что максимум нам понадобится получить 3 символа + нуль- терминатор в память по адресу buf. После получения текста в EAX должно находиться число полученных символов. Поэтому, если EAX=0, мы прыгаем на ошибку с текстом "Введите число 0-255 !". Иначе производим преобразование символьного вида в двоичный. Загружаем в EBX адрес буфера, который мы будем преобразовывать, а EAX обнуляем — его мы будем использовать для сохранения результата преобразования. Действуем по тому же принципу, что и при преобразовании двоичного вида в символьный, только, естественно, производим обратные действия в обратном порядке. В цикле ".dec_check_loop" проверяем очередной текущий символ в буфере на равенство нулю. Такое равенство будет означать, что мы дошли до завершающего нуль-терминатора, и преобразование окончено. Понятно, что это произойдет никак не на первом шаге цикла, но проверку придется выполнить один лишний раз. Таков алгоритм. Следующее действие также лишнее на первом шаге цикла, но может понадобиться на остальных повторениях. Это умножение содержимого EAX на 10. Оно необходимо для того, чтобы правильно преобразовать десятки и сотни. Допустим, первый символ числа 123 был единицей. На втором кругу мы умножим 1 на 10 и прибавим к нему 2, и получится уже 12. На третьем кругу 12 умножим на 10 и прибавим 3 — получится требуемое число 123. На первом же кругу мы пока еще не поместили в EAX ничего, кроме нуля, поэтому от умножения на 10 перед преобразованием первого символа нам ни холодно ни жарко. Кстати, вы заметили, как извращенно мы производим умножение EAX на 10 по формуле X*8+X*2=X*10? Такова традиция.

На старых процессорах операция умножения могла занимать 10 и более тактов. Зато мы знаем, что умножить число на степень двойки (2,4,8,16 и т.д.) можно за один такт командой битового сдвига SHL. Сдвигая двоичное число на одну позицию влево, мы получаем результат умножения его на два в первой степени, то есть просто на 2. На две позиции — на два во второй степени, то есть умножение на 4. На три позиции — умножение на 8. Таким образом, за четыре однотактных команды, мы получим в EAX то же самое, что и за одну десятитактную команду. Тем самым сэкономим 6 тактов процессорного времени. Хотя на современных 64-битных процессорах команда умножения занимает уже три такта + один-два такта на помещение множителей в требуемые регистры. Так что, если вы уверены, что ваша программа не будет использоваться на старых компьютерах, то можете пользоваться командой MUL. Но все же на данный момент считается более правильным использовать приведенный мною способ. Итак, умножили на 10, что дальше? Дальше обнуляем EDX и помещаем в него наш очередной преобразуемый символ. Помещаем в DL, так как символ у нас размером в байт. Хотя можно использовать команду movzx edx,byte [ebx]. Результат будет тот же. Вычитаем из преобразуемого символа значение '0', чтобы преобразовать в двоичный вид. Теперь сравниваем уже преобразованное значение с девяткой. Значение больше девяти укажет на то, что символ изначально не являлся символом от 0 до 9, а значит — ошибка ввода. Если ошибки нет, добавляем данное значение к общему результату в EAX и переходим к обработке следующего символа, не забыв увеличить EBX, чтобы он указывал на очередной непреобразованный символ. При возникновении ошибки мы не только выводим окошко с сообщением, но и помещаем в EAX код ошибки (-1). По окончанию процедуры мы должны не забыть сравнить результат с числом 255. Если результат больше, значит, введенное значение не влезает в один байт, и мы прыгаем на ошибку. Иначе в AL лежит подлежащее удалению значение, и мы преспокойно возвращаемся из функции. Чтобы на этапе вывода не возникло проблем, добавляем следующий текст:

.out_mas:
cmp [n],0
je .finish

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


.sv:
cmp [n],2
jb .finish
call Sortirovka
jmp .out_mas


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

;константы
ID_IN=1003

;данные
_tdelbut db 'Удалить',0
_tinsbut db 'Вставить',0
_terror4 db 'Нет свободной ячейки.',0

;код
.wmcommand:

cmp [wparam],ID_IN
je .in

.in:
cmp [n],MASSIZE
jb @f
invoke MessageBox,0,_terror4,0,0
jmp .finish
@@:
call ASCII2BIN
cmp eax,-1
je .finish
push [hwnd]
call InsertElement
cmp eax,-1
je .finish
jmp .out_mas

.wmcreate:

invoke CreateWindowEx,0,_cbutton,_tdelbut,WS_VISIBLE+ WS_CHILD+BS_PUSHBUTTON+WS_TABSTOP,60,120,80,20,[hwnd],ID_DE,[wc.hInstance],0
invoke CreateWindowEx,0,_cbutton,_tinsbut,WS_VISIBLE+ WS_CHILD+BS_PUSHBUTTON+WS_TABSTOP,150,120,80,20,[hwnd],ID_IN,[wc.hInstance],0

proc InsertElement hwnd
;вставляем элемент
mov edi,[n]
mov [mas+edi],al
.sort_cycl:
mov al,[mas+edi-1]
cmp al,[mas+edi]
jbe @f
xchg al,[mas+edi]
dec edi ;двигаемся к началу массива
mov [mas+edi],al
jnz .sort_cycl
@@:
;добавляем поле
mov esi,[n]
mov edi,esi;Умножаем
mov eax,esi;
shl esi,5 ;esi
shl edi,4 ;
shl eax,1 ;на
add esi,edi;
add esi,eax;50
add esi,5
mov edi,5
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 @f
mov ebx,[n]
mov [hmas+ebx*4],eax
inc [n]
ret
@@:
mov eax,-1
ret
endp

Думаю, все, кроме самой процедуры, не вызывает вопросов. Процедуру удаления рассмотрим подробнее. Первое ее отличие от написанных ранее процедур в том, что она имеет параметр hwnd. Его мы передаем в процедуру через стек, выполняя команду push [hwnd] перед вызовом процедуры. Дело в том, что hwnd — это локальная переменная процедуры WindowProc. К ней нельзя обратиться из другого места, кроме как из этой процедуры. Вообще это не совсем корректно — использовать в данном случае переменную hwnd, потому что она не обязательно должна содержать дескриптор главного окна, а содержит дескриптор окна, сообщение которому в данный момент обрабатывается. Но ввиду того, что сообщение WM_COMMAND в нашем случае приходит только главному окну (сообщение от дочерних элементов — кнопок), мы можем не создавать отдельную глобальную переменную под дескриптор главного окна, а передать значение локальной переменной через стек. Что мы и делаем. Дескриптор главного окна нам нужен лишь для указания родительского окна при создании дочернего поля. Локальные переменные процедуры, значения которых содержатся в стеке к моменту ее вызова, должны быть указаны после имени процедуры и разделяются запятыми. Новый элемент мы вставляем по адресу mas+[n]. Ввиду того, что mas — это прямой указатель, а n — указатель на место, где хранится значение, мы передаем n через регистр. Когда мы будем работать с полноправными динамическими массивами, и адрес массива будет храниться в переменной, а не являться константой, как сейчас, нам придется передавать через регистр и адрес массива.

На случай, если массив уже был отсортирован, мы попробуем поставить новое значение сразу же на его место в упорядоченной последовательности. Для этого пишем короткий цикл, в котором текущий элемент сравнивается с предыдущим и меняется с ним местами, если предыдущий оказался большим по значению (jbe — прыг если меньше или равно). Таким образом, в цикле наш элемент будет сдвигаться влево до тех пор, пока очередной предыдущий элемент не окажется меньшим или равным по значению. Здесь все по аналогии с методом сортировки прямым включением, только поиск места производится лишь для единственного элемента. Чтобы добавленное поле было на позицию правее текущего последнего поля или на первой позиции, если поля вообще отсутствуют, нам надо умножить [n] на 50 (50 точек у нас — расстояние между левыми краями соседних полей) и прибавить 5 (отступ для первого поля). Умножаем по тому же принципу, что и на 10, только для умножения на 50 используем формулу: X*50=X*32+X*16+X*2. Если поле было создано, помещаем его дескриптор в следующий за последним текущим элемент массива [hmas], увеличиваем [n] и возвращаемся из функции. Иначе оставляем [n] как есть, помещаем в eax код ошибки, чтобы не выводить повторно неизмененный массив, и возвращаемся.

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

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

BarMentaLisk, SASecurity gr., q@sa-sec.org


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

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