Эта статья идет прямиком в дополнение к предыдущей. Я не рассказал про самые основы ассемблера. Хотя в интернете полно материала на эту тему, я все равно решил ее немного затронуть. Что же нужно знать для начала, чтобы понимать и писать несложные программы или ассемблерные вставки?
1. У процессора есть определенный набор регистров.
Представьте, что регистр - это переменная, которая всегда объявлена, и вы можете ее свободно использовать, только переменная эта хранится не в оперативной памяти, а прямиком в процессоре, поэтому работа с регистрами очень быстрая. Существует 8 регистров общего назначения, каждый из них может хранить 4 байта. Чаще всего мы будем использовать следующие регистры: eax, ebx, ecx, edx. Каждый из этих регистров делится еще на несколько следующим образом:
Если поделить EAX на две части по 16 бит (по 2 байта), то младшая часть - это регистр AX. Если AX поделить на две части по 8 бит (по 1 байту), то младшая его часть - AL, а старшая - AH. То же самое справедливо и для остальных перечисленных регистров.
Существуют еще регистры esi и edi (в отличие от предыдущих, у них есть только младшая 16-битная часть si и di). Они используются для выполнения различных операций пересылки данных.
Регистр esp используется для того, чтобы адресовать стек, а ebp - чтобы обращаться к локальным переменным в стеке. Над этими можно пока не задумываться, потому что MASM32 позволяет обойтись без их использования.
В программах мы будем использовать чаще всего только шесть вышеперечисленных регистров. Советую немного прочитать про двоичные формы представления чисел (про перевод десятичных чисел в двоичные и наоборот, а также про дополнительный и прямой код, это пригодится в будущем. Если не всё поняли, опять-таки ничего страшного, про арифметику я еще буду упоминать в дальнейшем.
2. Регистр флагов. Об этом регистре поговорим отдельно. В нем хранятся различные биты, позволяющие узнать информацию о текущем состоянии процессора. Напрямую с этим регистром обычно не работают, существует много команд ассемблера, позволяющих работать с отдельными его битами.
Что ж, небольшой пример, в котором вы уже будете способны разобраться. Предположим, мы хотим сложить два числа и проверить, не равен ли результат нулю.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
mov eax, 123 ;здесь мы записали число 123 в регистр eax. ;Эта операция эквивалентна eax = 123 mov ebx, -123 ;а здесь пишем в ebx число -123. Оно будет представлено в дополнительном коде (см. выше) add eax, ebx; складываем. Эта запись эквивалентна eax = eax + ebx ;В eax у нас будет результат 0. Как же мы можем это проверить? test eax, eax ;Эта команда проверит, есть ли хотя бы одна единичка ;в регистре eax (в бинарном представлении числа) ;если есть, то в регистре флагов будет сброшен флаг ZF (zero flag) ;то есть, его значение будет установлено равным нулю ;можно было проверить и так: ;cmp eax, 0 ;то есть, напрямую сравнить eax и ноль jz zero ;осуществляем переход на метку zero, ;если флаг ZF установлен. Именно это действие производит ;данная команда ;Если в eax будет не 0, то выполнение продолжится дальше invoke MessageBox, hWin, chr$("В eax почему-то не 0!"), chr$("Info"), 0 jmp lexit ;переходим на метку exit (GOTO exit) zero: invoke MessageBox, hWin, chr$("В eax 0!"), chr$("Info"), 0 lexit: |
Изучите этот простой пример и вставьте его в предыдущий исходник в тело обработчика нажатия на кнопку "Test" (сразу после case TEST_BTN). Вы увидите сообщение о том, что в регистре eax число ноль при нажатии кнопки "test".
3. Стек и куча
Теперь немного о стеке и куче. Куча, как вы конечно же сразу подумали, это область памяти, в которой хранится вся программа и ее данные. Все глобальные переменные, которые мы создавали в секции .data и .data? в предыдущем примере будут размещены именно там.
Теперь немного о стеке. Стек - это удобное место для хранения информации. Чаще всего он используется при вызове функций. Стек использует принцип "первым вошел - последним вышел". Приведу пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
mov eax, 100 ;загружаем в eax число 100 mov ecx, 200 ;а в ecx - 200 push eax ;сохраняем в стеке регистр eax push ecx ;сохраняем в стеке регистр ecx ;сейчас мы сохранили оба регистра в стеке, причем первым в стек ;попал eax, а последним - ecx ;здесь мы можем делсть любые действия с этими регистрами mov eax, 555 ;например, такое add ecx, eax ;или такое ;а теперь мы просто восстановим значение из стека pop ecx pop eax ;восстанавливаем в обратном порядке! ;потому что стек организован по принципу "первый вошел - последний вышел", ;как я уже говорил ;выведем значения регистров .data? buffer db 128 dup(?) ;создаем буфер ;в секции неинициализированных данных ;размером 128 байт ;директива dup говорит о том, что все байты будут неопределены ;если бы мы написали dup(123) ;то все байты приняли бы значение 123 ;но у нас - секция НЕинициализированных данных ;и мы не можем так сделать, да и не нужно .code ;опять вернемся к коду ;и вызовем функцию, эквивалентную функции ;printf в языках C/C++ invoke wsprintf, offset buffer, chr$("EAX = %u, ECX = %u"), eax, ecx ;она запишет нам строку EAX = [число], ECX = [число] ;в буфер buf invoke MessageBox, hWin, offset buffer, chr$("Info"), 0 ;а теперь мы просто выведем эту строку |
Вставьте этот пример в место обработчика нажатия на TEST_BTN, как и предыдущий, и посмотрите, что произойдет при нажатии на кнопку "Test".
А теперь вернемся к вызову функций. Что происходит, когда мы пишем такую строку?
1 |
invoke MessageBox, hWin, offset buffer, chr$("Info"), 0 |
Ведь invoke - это всего лишь встроенный макрос для упрощения кода, и при компиляции всё это преобразуется в ассемблерные команды.
Этот код эквивалентен следующему:
1 2 3 4 5 |
push 0 push chr$("Info") push offset buffer push hWin call MessageBox |
Замените макрос на эту последовательность команд и убедитесь, что так оно и есть!
В предыдущей статье вы сможете еще раз прочитать, что MessageBox - это WinAPI-функция из библиотеки user32.dll, а все WinAPI-функции созданы по соглашению stdcall, то есть, передача аргументов в них производится через стек в обратном порядке. Возвращают эти функции значение в регистре eax. Ну, возвращаемое значение MessageBox нас не сильно интересует, но если бы оно нам было необходимо, то сразу после вызова мы могли бы как-то работать с регистром eax, например, сохранить его. Скажем, мы хотим узнать полную командную строку приложения (с какими аргументами его вызвали). Напишем так:
1 2 3 4 5 6 7 8 |
call GetCommandLine push 0 push chr$("Command Line") push eax ;вот оно - в eax будет указатель на строку! ;который нам вернула функция GetCommandLine push hWin call MessageBox |
Выполнив этот код, мы увидим полный путь к приложению и переданные ему аргументы, если такие имеются.
Разумеется, код выше можно упростить с помощью макросов MASM32:
1 |
invoke MessageBox, hWin, FUNC(GetCommandLine), chr$("Command line"), 0 |
Но знать, что в итоге получается, все равно нужно, поэтому я всё и пояснил.
Со стеком можно работать и напрямую, как с кучей, используя адреса и произвольный доступ.
Что еще нужно знать о соглашении stdcall? То, что все такие функции сохраняют регистры esi, edi и ebx и могут менять по своему усмотрению регистры ecx, edx. То есть, если мы что-то храним в регистре ecx, потом вызываем какую-то WinAPI-функцию, то после ее вызова уже нельзя использовать ecx - в нем будет храниться совершенно случайное значение. Если же мы все-таки хотим что-то там сохранить, чтобы вызов функции не изменил значения, то следует просто воспользоваться командами push ecx до вызова и pop ecx после - и значение ecx будет восстановлено.
4. Адреса
Теперь я поговорю немного об адресах и указателях. Указатель - это непосредственно адрес какой-то переменной в памяти, будь она в куче или в стеке. Если вы знакомы с указателями, то можете даже не читать этот кусок текста.
Многие функции (например, все WinAPI функции) используют для передачи больших переменных не сами переменные, а указатели на них. Если вы были внимательны, вы могли заметить, что выше я писал
1 |
invoke MessageBox, hWin, offset buffer, chr$("Info"), 0 |
И я использовал не просто buf, а offset buf - адрес буфера. Представьте, что было бы, если бы мы весь 128-байтный буфер решили загрузить в стек (а передача значений в функции, как я уже говорил, производится через стек). Нам бы пришлось заталкивать каким-либо образом в стек 128 байт. Это очень долго и неоптимально, ведь функции MessageBox пришлось бы потом еще и убирать эту строку оттуда. А мы передаем просто адрес этой строки, указатель на нее, который занимает всего 4 байта, и функция MessageBox по этому указателю находит строку в памяти.
Предположим, у нас есть строка
1 2 |
.data stroka db "Hello, world", 0 |
Что во-первых следует знать? То что все строки в Windows, которые используются функциями WinAPI, должны заканчиваться нулевым байтом. По этой причине мы написали 0 в конце инициализации строки. Этот нулевой байт говорит о том, что это последний байт строки, и дальше ничего нет. Unicode-строки (двухбайтовые) должны заканчиваться соответственно двумя нулевыми байтами. Во-вторых, сравните записи:
1 2 |
mov al, stroka mov eax, offset stroka |
В первом случае мы загружаем в al первый байт из строки stroka. А во втором случае мы загружаем указатель на строку stroka в регистр eax, и можем работать с самой строкой через указатель. Например, если мы хотим заменить вторую букву строки с "e" на "X", то достаточно просто написать так:
1 |
mov byte ptr [eax + 1], 'X' |
На первый взгляд, это сложная строка, но эта запись по сути эквивалентна stroka[1] = 'X'. Byte ptr говорит о том, что мы записываем один байт по адресу строки + 1. Строка выглядит как массив байтов, и по адресу stroka + 0 лежит первая ее буква, по адресу stroka + 1 - вторая, и так далее.
Теперь приведу пример посложнее и закончу на этом. Допустим, мы хотим посчитать длину строки. Мы знаем, что конец строки указывается нулевым байтом. Что же, этого вполне достаточно. Пример я приведу, используя только те команды ассемблера, которые я успел разобрать.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
.data stroka db "Hello, world!", 0 buffer db 128 dup(?) ;буфер, в который мы запишем онформацию о строке .code mov eax, offset stroka mov ecx, 0; это будет наш счетчик длины строки next_sym: mov bl, byte ptr [eax + ecx] ;загружаем очередной символ из строки в регистр bl test bl, bl ;проверяем, а не ноль ли он? jz end_count ;если ноль, идем на вывод длины ;если нет - считаем дальше inc ecx ;прибавляем счетчик на 1 ;(эта команда эквивалентна add ecx, 1) jmp next_sym end_count: ;выводим посчитанную длину: invoke wsprintf, offset buffer, chr$("Длина строки - %u символов"), ecx invoke MessageBox, hWin, offset buffer, chr$("Info"), 0 |
Вставьте этот код в код примера из предыдущей статьи после строки case TEST_BTN, как всегда, и посмотрите, как он работает.
Вы уже испугались, думая, что каждый раз придется ТАК считать длину строк? Не пугайтесь, всё уже сделано за нас! Предыдущий исходник эквивалентен такому:
1 2 3 4 5 6 7 8 |
.data stroka db "Hello, world!", 0 buffer db 128 dup(?) ;буфер, в который мы запишем онформацию о строке .code ;выводим посчитанную длину: invoke wsprintf, offset buffer, chr$("Длина строки - %u символов"), FUNC(lstrlen, offset stroka) invoke MessageBox, hWin, offset buffer, chr$("Info"), 0 |
Мы просто воспользовались WinAPI-функцией lstrlen!
Теперь немного дополнительного материала для интересующихся - а что, если мы хотим оставить свой личный просчет длины строки и вынести его в отдельную процедуру? Тогда напишем так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
STRING_LENGHT PROC pointer_to_string :DWORD push esi ;сохраним в стеке значения регистров esi, edi и ebx push edi ;прямо как настоящая stdcall-функция push ebx mov eax, pointer_to_string mov ecx, 0; это будет наш счетчик длины строки next_sym: mov bl, byte ptr [eax + ecx] ;загружаем очередной символ из строки в регистр bl test bl, bl ;проверяем, а не ноль ли он? jz end_count ;если ноль, идем на вывод длины ;если нет - считаем дальше inc ecx ;прибавляем счетчик на 1 ;(эта команда эквивалентна add ecx, 1) jmp next_sym end_count: mov eax, ecx ;помним, что все stdcall-функции возвращают значение в eax! ;мы ведь пишем, используя этот стандарт pop ebx pop edi ;восстановим значения регистров из стека pop esi ;в обратном порядке (помним принцип работы стека) ret ;выходим из процедуры STRING_LENGHT ENDP |
MASM32 сильно упрощает написание функций. Я даже не буду приводить код, который получается в итоге, чтобы не запутать вас, да это и не нужно.
Перед использованием функции необходимо написать ее прототип прототип (о них я говорил в предыдущих статьях).
Пишем:
1 |
STRING_LENGHT PROTO :DWORD |
Этот прототип говорит компилятору о том, что функция STRING_LENGHT принимает единственный аргумент типа "двойное слово", т.е. 4 байта - указатель на нашу строку.
Теперь у нас есть своя функция для просчета длины строки. Мы можем вставить ее в конец кода перед строкой "end start" и вызывать ее вместо lstrlen! Попробуйте сделать это сами. Не забудьте написать прототип функции в начало файла после подключения различных библиотек.
Привет!
Сделай у себя на блоге "версию для печати",
было бы реально полезно.
Сделал
Спасибо за цикл новых статей. Очень полезно.
Огромное спасибо за статьи!
Спасибо за цикл статей, разжованно как для одаренных)
Спасибо за статьи - я только начинаю осваивать ассемблер.
Подскажите, пожалуйста, что означает выражение в операнде условного перехода:
jnz $ + 11,
я так понимаю это адрес метки, что означает знак доллара в этом выражении?
Означает текущую позицию. Т.е. переход от адреса текущей инструкции вперед на 11 байт.
MASM поддерживает удобные переходы по неименованным меткам еще.
Например, переход до ближайшей метки "@@" ниже инструации перехода:
jmp @F
...
@@:
И переход до ближайшей метки "@@" выше инструкции перехода:
@@:
...
jmp @B
Доброго времени суток;
Прошу Вашей помощи, пожалуйста;
Пишу небольшую функцию
MmASMGetCr3 PROC CR:qword
movq [CR], 0
MmASMGetCr3 ENDP
END
Но вот ml64 выдаёт ошибку error A2009:syntax error in expression в строке 3;
Где я ошибся? Вызывается функция из C++ как MmASMGetCr3(cr3), cr3 имеет тип uint64;
отличная статья!спасибо