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

Сегодня мы попробуем закончить с азами управления массивами, а также познакомимся поближе с методикой выделения памяти в славном семействе Windows NT/2K/XP/Vista.
В предыдущих примерах наш массив располагался в секции данных исполняемого модуля. Такой подход вполне удобен при работе со статическим массивом. Но статические массивы используются достаточно редко. Чаще возникает необходимость использования динамического массива, количество элементов в котором не определено, а следовательно, размер памяти, занимаемой массивом, может изменяться за время работы программы.


Ранее, на рубеже перехода от семейства 9x к семейству NT, для выделения небольших объемов памяти рекомендовалось пользоваться функциями GlobalAlloc и LocalAlloc. Сейчас это по ряду причин считается нецелесообразным, а для выделения небольших объемов памяти (до нескольких мегабайт) рекомендуется использовать функции работы с кучами. Куча (от англ. Heap) — это область зарезервированного процессом адресного пространства, при помощи которой реализуется динамическое выделение памяти. При создании кучи под нее выделяется лишь виртуальная память, а физическая память выделяется специальным диспетчером куч (heap manager) уже по мере заполнения кучи данными. Физическая память выделяется определенными системой блоками — страницами, а по мере освобождения страниц возвращается системе. У каждого процесса есть стандартная куча, дескриптор которой можно получить вызовом функции GetProcessHeap. Параметры у этой функции отсутствуют, так как каждый процесс имеет лишь одну стандартную кучу. Также можно создавать дополнительные кучи при помощи функции HeapCreate. Ее параметры:

1. опции кучи: HEAP_CREATE_ENABLE_EXECUTE — разрешение на исполнение кода, содержащегося в куче, HEAP_GENERATE_EXCEPTIONS — системные уведомления при переполнении кучи и т.п., HEAP_NO_SERIALIZE — отказ от одновременного доступа к куче несколькими потоками немного
увеличивает производительность, но может вызвать сбой при попытке одновременного доступа.
2. Начальный размер кучи в байтах. Значение округляется в большую сторону до ближайшего значения, кратного размеру страницы, поэтому, если указать ноль, будет зарезервирована одна страница. Размер страницы можно узнать при помощи функции GetSystemInfo.
3. Максимальный размер кучи в байтах. Если при выполнении функций HeapAlloc и HeapReAlloc (выделение памяти в куче) запрашиваемый размер превышает начальный размер кучи, то система выделяет новые страницы физической памяти, но при этом суммарный размер кучи не может превысить значение данного параметра. Если максимальный размер кучи не равен нолю, то полученная куча не является "растущей", и существует ограничение на максимальный размер выделяемого блока, который не должен превышать 0x7FFF8 байт. Функция выделения памяти для такой кучи автоматически вернет ошибку при попытке выделить блок памяти большего размера. Если же в качестве максимального размера кучи указать ноль, то куча считается "растущей". Общий размер такой кучи ограничен лишь доступной системе памятью. Для такой кучи попытка выделения блока памяти размером более 0x7FFF8 байт уже не будет считаться ошибкой, — система сама произведет вызов функции VirtualAlloc для выделения памяти под большой блок данных. Приложения, которым требуется выделять большие блоки памяти, всегда должны устанавливать значение данного параметра в ноль.

При успешном выполнении функция возвращает в EAX-дескриптор созданной кучи. В случае ошибки возвращается ноль. Причем функция работает так, что точный максимальный размер блока в "нерастущей" куче никогда не известен. В официальной документации MSDN сказано лишь, что максимальный размер блока должен быть немного меньше 0x7FFF8 байт. Например, у меня не получилось выделить в "нерастущей" куче блок более 0x7EFF8. Так что старайтесь не использовать кучи фиксированного размера для выделения блоков памяти размером, близким к загадочной цифре 0x7FFF8. Для выделения блоков памяти в куче используется функция HeapAlloc, а для изменения размера выделенного блока — HeapReAlloc. Параметры первой из них:

1. Дескриптор кучи, в которой будет выделен блок (возвращается функциями GetProcessHeap и HeapCreate).

2. Опции блока: HEAP_GENERATE_EXCEPTIONS — системные уведомления при переполнении и т.п. (лучше указывать эту опцию сразу в HeapCreate для всех блоков кучи), HEAP_NO_SERIALIZE (тоже можно указать в HeapCreate для всех блоков кучи, а здесь не указывать), HEAP_ZERO_MEMORY — проинициализировать блок нулевыми значениями (занимает время, использовать по необходимости).

3. Размер выделяемого блока в байтах (если куча не является "растущей", то размер блока не должен превышать 0x7FFF8 байт).

В случае успешного выполнения функция возвращает указатель на выделенный блок памяти. При возникновении ошибки функция возвращает ноль, если не был указан параметр HEAP_GENERATE_EXCEPTIONS. Иначе функция может вернуть значения STATUS_NO_MEMORY (нехватка памяти) или STATUS_ACCESS_VIOLATION (неверные параметры) в зависимости от произошедшей ошибки. В отличие от большинства функций, функции HeapAlloc и HeapReAlloc не вызывают SetLastError в случае ошибки, поэтому обращение к GetLastError не даст более подробных сведений об ошибке. Параметры функции HeapReAlloc:

1. дескриптор кучи, в которой находится перераспределяемый блок (возвращается функциями GetProcessHeap и HeapCreate);
2. опции перераспределения: HEAP_GENERATE_EXCEPTIONS, HEAP_NO_SERIALIZE, HEAP_REALLOC_IN_PLACE_ONLY — запрещает перемещение блока: в случае недостатка свободной памяти для расширения блока по месту его расположения, функция не станет искать подходящее место в памяти, а вернет ошибку, оставив блок без изменений, HEAP_ZERO_MEMORY;
3. указатель на перераспределяемый блок памяти (возвращается функциями HeapAlloc или HeapReAlloc);
4. новый размер блока в байтах (если куча не является "растущей", то размер блока не должен превышать 0x7FFF8 байт).
Возвращаемые значения такие же, как и у HeapAlloc.

Обе вышеописанные функции могут выделить блок памяти как указанного размера, так и чуть больше требуемого. Точный размер выделенного блока можно узнать, обратившись к функции HeapSize. Ее параметры:
1. дескриптор кучи, в которой находится блок;
2. опции: HEAP_NO_SERIALIZE;
3. указатель на заданный блок.

В случае успешного выполнения возвращается размер заданного блока. В случае ошибки — минус единица. Данная функция также не указывает подробности о произошедшей ошибке через SetLastError.

Если выделенный в куче блок памяти вам больше не требуется, его следует удалить функцией HeapFree, чтобы освободить системную память. Параметры этой функции такие же, как и у HeapSize. В случае успешного освобождения памяти от заданного блока возвращается ненулевое значение. В случае ошибки возвращается ноль. Подробности можно узнать при помощи функции GetLastError. Для удаления кучи целиком используется функция HeapDestroy. Она имеет всего один параметр — дескриптор удаляемой кучи, возвращаемый функцией HeapCreate. Только не пытайтесь удалить кучу, дескриптор которой получен при помощи функции GetProcessHeap. Не вы ее создавали, не вам ее удалять. В случае успешного удаления кучи возвращается ненулевое значение. В случае ошибки возвращается ноль. Подробности можно узнать при помощи функции GetLastError.

Ну вот теперь, когда мы изучили функции управления кучами, можем приступать к апгрейду нашего примера управления массивом. Первым делом не забудьте сменить надпись в заголовке окна на гордое словосочетание: "Динамический массив". Пусть все знают о том, что это скромное окошко способно сожрать всю системную память машины, если допустить пару нелепых ошибок в коде обработчиков его событий. Итак, приступим к изучению кода. Чтобы сэкономить газетную площадь, постараюсь приводить лишь новые и измененные участки кода. Массив у нас теперь динамический, поэтому константами задаем начальное количество элементов отдельно от максимального количества, под которое будет выделена память. Переменные [mas] и [hmas] теперь будут хранить лишь адреса массивов в выделенной системой памяти:

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

;секция данных
_title db 'Динамический массив',0
hheap dd ?
mas dd ?
hmas dd ?

Перед попыткой удаления элемента введем проверку количества элементов на ноль. Теперь, если элементы в массиве отсутствуют, нажатие кнопки удаления элемента не приведет ни к каким действиям:
;секция кода

.de:
cmp [n],0
je .finish
call ASCII2BIN
cmp eax,-1
je .finish
call DeleteElement
cmp eax,-1
je .finish
jmp .out_mas

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

.in:
cmp [n],MAXMASSIZE
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

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

.wmcreate:
;выделяем память под массивы
invoke HeapCreate,HEAP_NO_SERIALIZE,0,0
test eax,eax
jz error
mov [hheap],eax
invoke HeapAlloc,[hheap],HEAP_NO_SERIALIZE,MAXMASSIZE
test eax,eax
jz error
mov [mas],eax
invoke HeapAlloc,[hheap],HEAP_NO_SERIALIZE,MAXMASSIZE*4
test eax,eax
jz error
mov [hmas],eax

Здесь начинаются главные отличия от примеров из предыдущих статей. Ранее наш массив располагался по заранее известному адресу, поэтому мы могли с уверенностью утверждать, что по смещению mas хранится первый элемент массива. Теперь по смещению mas у нас хранится адрес первого элемента массива. Заметили разницу? Ранее, например, командой mov [mas+2],al мы бы поместили значение регистра al в третий элемент массива. Теперь для этого нам бы потребовалось что-то вроде mov [[mas]+2],al. Но ввиду того, что синтаксис ассемблера не позволяет использовать двойную адресацию (поместить содержимое al в память по адресу, который находится по адресу…), нам придется сначала поместить значение [mas] в регистр, а потом уже обращаться к элементам, используя адресацию вида [регистр+смещение]. Если здесь вам что-либо неясно, возможно, вы невнимательно читали предыдущую статью.

;создаем поле под каждый элемент массива mas
;и помещаем дескриптор каждого поля в массив hmas
cmp [n],0
je @f
mov esi,5
mov edi,5
xor ebx,ebx
mov edx,[hmas]
.create_cycl:
push edx
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
pop edx
mov [edx+ebx*4],eax
add esi,50
inc ebx
cmp ebx,[n]
jne .create_cycl
@@:
;создаем другие элементы

Здесь у нас добавлена проверка количества элементов массива на ноль. Если элементов нет — незачем создавать поля для их вывода. Адрес, по которому расположен массив hmas, мы копируем в регистр edx перед циклом создания полей. Таким образом, mov [edx],eax приведет к копированию значения eax в память по адресу edx. Ввиду того, что API-функции windows не гарантируют сохранение содержимого регистров EAX, ECX и EDX, нам придется перед вызовом функции сохранить содержимое EDX в стеке (push edx), а после выполнения функции — восстановить его (pop edx). При выводе элементов массива придерживаемся тех же правил. В esi помещаем адрес массива элементов, в edx — массива дескрипторов полей. Перед вызовом API- функции сохраняем edx, после вызова — восстанавливаем:

.out_mas:
cmp [n],0
je .finish
;заполняем поля значениями элементов массива
xor ebx,ebx
mov esi,[mas]
mov edx,[hmas]
.masout_cycl:
mov ah,byte [esi+ebx]
lea edi,[buf+BUFSIZE]
.dec:
mov al,ah
aam
or al,30h ;можно заменить на add al,30h
dec edi
mov byte [edi],al
test ah,ah
jnz .dec
push edx
invoke SendMessage,[edx+ebx*4],WM_SETTEXT,0,edi
pop edx
inc ebx
cmp ebx,[n]
jne .masout_cycl
;test heap size
invoke HeapSize,[hheap],0,[mas]
mov ah,al
lea edi,[buf+BUFSIZE]
.dec1:
mov al,ah
aam
or al,30h
dec edi
mov byte [edi],al
test ah,ah
jnz .dec1
push edx
invoke SendMessage,[hedit],WM_SETTEXT,0,edi
jmp .finish

Для наглядности я добавил вывод текущего размера памяти, выделенного под массив (;test heap size). Чтобы не создавать лишних полей, размер выводится прямо в поле ввода удаляемого/добавляемого элемента. По завершении программы не забываем освободить память от нашей кучи: …
.wmdestroy:
invoke HeapDestroy,[hheap]
invoke PostQuitMessage,0
xor eax,eax
.finish:
pop edi esi ebx
ret
endp

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

proc Sortirovka
mov esi,[mas]
mov ecx,[n]
dec ecx
.cycl1:
xor ebx,ebx
xor edx,edx
.cycl2:
mov ah,[esi+ebx]
cmp ah,[esi+ebx+1]
jna .next
mov al,[esi+ebx+1]
mov [esi+ebx],ax
inc edx
.next:
inc ebx
cmp ebx,ecx
jne .cycl2
test edx,edx
loopnz .cycl1
ret
endp

proc DeleteElement
cld
xor bl,bl
mov ecx,[n]
mov edi,[mas]
.del_next:
repne scasb
je .del_el
test bl,bl
jne .finish
;Элемент не найден
invoke MessageBox,0,_terror3,0,0
mov eax,-1
ret
;Удаляем элемент
;по адресу EDI-1
.del_el:
mov esi,edi
dec edi
push eax ecx edi
rep movsb
dec [n]
;Удаляем последнее поле
mov edx,[n]
shl edx,2
add edx,[hmas]
invoke SendMessage,[edx],WM_CLOSE,0,0
mov bl,1
pop edi ecx eax
test ecx,ecx
jne .del_next
.finish:
xor eax,eax
ret
endp

proc InsertElement hwnd
;вставляем элемент
mov edi,[n]
mov esi,[mas]
mov [esi+edi],al
.sort_cycl:
mov al,[esi+edi-1]
cmp al,[esi+edi]
jbe @f
xchg al,[esi+edi]
dec edi ;двигаемся к началу массива
mov [esi+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
invoke CreateWindowEx,0,_cedit,0,WS_VISIBLE+WS_CHILD+WS_BORDER+ ES_READONLY+ES_RIGHT,esi,5,40,20,[hwnd],ebx,[wc.hInstance],0
test eax,eax
jz @f
mov edx,[n]
shl edx,2
add edx,[hmas]
mov [edx],eax
inc [n]
ret
@@:
mov eax,-1
ret
endp

Процедура удаления элемента теперь удаляет все элементы с указанным значением, а не один из них, как было раньше. Для того, чтобы это корректно реализовать, потребовалось ввести счетчик удаленных элементов. Точнее, даже не счетчик, а своеобразный флаг, который поможет избежать вывода ошибки "Элемент не найден", когда мы уже удалили один или несколько элементов, но при очередном поиске таковых больше не нашли. Если содержимое bl, которое мы обнуляем перед циклом удаления, равно нулю, значит, еще ни один элемент не был удален. В таком случае, когда поиск по массиву завершится (ecx сравняется с нулем), мы будем знать, что в массиве отсутствовали элементы с заданным значением, и после вывода сообщения об ошибке поместим в eax код ошибки (минус единицу) и вернем управление на точку вызова процедуры. Иначе, если bl не равен нулю, мы прыгнем на метку ".finish", потому что bl, отличный от нуля, означает, что удаление заданных элементов было произведено.

Теперь подробнее о цикле удаления. Мы, как и прежде, командой repne scasb сканируем массив в поисках элемента, равного содержимому AL. Сканирование может быть прекращено либо если найдено равенство, либо если ECX сравнялся с нулем. Второй случай мы только что рассмотрели. Если же найден элемент, равный AL, то мы прыгаем на метку ".del_el", чтобы выполнить удаление элемента. В edi к этому моменту находится адрес следующего за удаляемым элементом. А нам для удаления при помощи команды rep movsb необходимо, чтобы в edi был адрес удаляемого элемента, а в esi — следующего за ним. Поэтому мы копируем содержимое edi в esi, а edi уменьшаем на единицу. Теперь мы сохраним регистры eax, ecx и edi, потому что текущие значения ecx и edi нам еще понадобятся для продолжения поиска других кандидатов на удаление с текущей позиции. На текущую позицию будет сдвинут следующий за удаляемым элементом, а он ведь тоже может оказаться подлежащим удалению, хотя пока мы об этом не знаем, а лишь готовимся к удалению текущего элемента, но предусматриваем все варианты. Содержимое EAX мы сохраняем по другой причине: после вызова функции удаления лишнего окошка в EAX вернется результат ее выполнения. А у нас в AL, который является частью EAX, если вы помните, хранится значение искомых элементов. Поэтому обязательно сохраняем.

Элемент удалили, уменьшили текущее количество элементов в переменной [n], удаляем последнее поле. Его дескриптор сейчас находится по адресу [hmas]+[n]*4. Но ввиду невозможности двойной адресации мы поместим значение [n] в edx, умножим на 4 (shl edx,2) и добавим к edx значение [hmas]. Теперь, когда edx хранит адрес дескриптора удаляемого окна, мы смело можем указать [edx] в качестве параметра функции SendMessage. После удаления окошка задвигаем в bl единицу в знак того, что хотя бы один элемент мы уже удалили. Восстанавливаем сохраненные регистры, соответственно, в обратном порядке. На всякий случай проверяем ecx на ноль — вдруг это был последний элемент массива. Если не ноль, то переходим на метку ".del_next", чтобы продолжить сканирование с того элемента, который мы еще не проверяли. Иначе обнуляем eax — успешное выполнение — и возвращаемся из процедуры. В процедуре вставки элемента так же, как и в остальных измененных процедурах, и по тем же причинам адрес массива копируется в регистр. Ну и для доступа к ячейке [hmas]+[n]*4 используется такой же прием, как и в процедуре удаления. Однако и сейчас, несмотря на то, что массив динамический, ему чего-то не хватает. Мы не можем вставить больше элементов, чем MAXMASSIZE. Для того, чтобы обойти это ограничение, заменим данную константу переменной. Например, икс:

x dd MAXMASSIZE

Теперь на метке ".in:" сравнивайте [n] с [x], естественно, через регистр. Если памяти не хватает для вставки очередного элемента — выделите ее функцией HeapReAlloc. Только постарайтесь сделать так, чтобы память выделялась сразу под несколько элементов, а не под один каждый раз — это уменьшит фрагментацию и общее время на выделение памяти. А еще лучше — чтобы память выделялась заранее: например, когда текущей выделенной памяти осталось меньше чем на 10 элементов, блоки увеличиваются на 20 элементов. Попробуйте сделать это сами — ваш уровень это позволяет. Вот, собственно, и все, что я вам хотел рассказать о динамических массивах. Понятно, что существуют массивы, в которых каждый элемент может быть строкой неопределенного размера, существуют двумерные и многомерные массивы, но общие принципы работы с ними остаются такими же. Просто усложняется алгоритм обработки. Однако, если вы разберетесь с простым типом массивов, то и более сложные типы без проблем поймете по мере надобности. Желаю вам успехов в этом, и до новых встреч!

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

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


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

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