MASM32: Немного основ ассемблера

Эта статья идет прямиком в дополнение к предыдущей. Я не рассказал про самые основы ассемблера. Хотя в интернете полно материала на эту тему, я все равно решил ее немного затронуть. Что же нужно знать для начала, чтобы понимать и писать несложные программы или ассемблерные вставки?

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. Регистр флагов. Об этом регистре поговорим отдельно. В нем хранятся различные биты, позволяющие узнать информацию о текущем состоянии процессора. Напрямую с этим регистром обычно не работают, существует много команд ассемблера, позволяющих работать с отдельными его битами.

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

Изучите этот простой пример и вставьте его в предыдущий исходник в тело обработчика нажатия на кнопку "Test" (сразу после case TEST_BTN). Вы увидите сообщение о том, что в регистре eax число ноль при нажатии кнопки "test".

3. Стек и куча
Теперь немного о стеке и куче. Куча, как вы конечно же сразу подумали, это область памяти, в которой хранится вся программа и ее данные. Все глобальные переменные, которые мы создавали в секции .data и .data? в предыдущем примере будут размещены именно там.
Теперь немного о стеке. Стек - это удобное место для хранения информации. Чаще всего он используется при вызове функций. Стек использует принцип "первым вошел - последним вышел". Приведу пример:

Вставьте этот пример в место обработчика нажатия на TEST_BTN, как и предыдущий, и посмотрите, что произойдет при нажатии на кнопку "Test".

А теперь вернемся к вызову функций. Что происходит, когда мы пишем такую строку?

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

Замените макрос на эту последовательность команд и убедитесь, что так оно и есть!
В предыдущей статье вы сможете еще раз прочитать, что MessageBox - это WinAPI-функция из библиотеки user32.dll, а все WinAPI-функции созданы по соглашению stdcall, то есть, передача аргументов в них производится через стек в обратном порядке. Возвращают эти функции значение в регистре eax. Ну, возвращаемое значение MessageBox нас не сильно интересует, но если бы оно нам было необходимо, то сразу после вызова мы могли бы как-то работать с регистром eax, например, сохранить его. Скажем, мы хотим узнать полную командную строку приложения (с какими аргументами его вызвали). Напишем так:

Выполнив этот код, мы увидим полный путь к приложению и переданные ему аргументы, если такие имеются.
Разумеется, код выше можно упростить с помощью макросов MASM32:

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

Что еще нужно знать о соглашении stdcall? То, что все такие функции сохраняют регистры esi, edi и ebx и могут менять по своему усмотрению регистры ecx, edx. То есть, если мы что-то храним в регистре ecx, потом вызываем какую-то WinAPI-функцию, то после ее вызова уже нельзя использовать ecx - в нем будет храниться совершенно случайное значение. Если же мы все-таки хотим что-то там сохранить, чтобы вызов функции не изменил значения, то следует просто воспользоваться командами push ecx до вызова и pop ecx после - и значение ecx будет восстановлено.

4. Адреса
Теперь я поговорю немного об адресах и указателях. Указатель - это непосредственно адрес какой-то переменной в памяти, будь она в куче или в стеке. Если вы знакомы с указателями, то можете даже не читать этот кусок текста.
Многие функции (например, все WinAPI функции) используют для передачи больших переменных не сами переменные, а указатели на них. Если вы были внимательны, вы могли заметить, что выше я писал

И я использовал не просто buf, а offset buf - адрес буфера. Представьте, что было бы, если бы мы весь 128-байтный буфер решили загрузить в стек (а передача значений в функции, как я уже говорил, производится через стек). Нам бы пришлось заталкивать каким-либо образом в стек 128 байт. Это очень долго и неоптимально, ведь функции MessageBox пришлось бы потом еще и убирать эту строку оттуда. А мы передаем просто адрес этой строки, указатель на нее, который занимает всего 4 байта, и функция MessageBox по этому указателю находит строку в памяти.
Предположим, у нас есть строка

Что во-первых следует знать? То что все строки в Windows, которые используются функциями WinAPI, должны заканчиваться нулевым байтом. По этой причине мы написали 0 в конце инициализации строки. Этот нулевой байт говорит о том, что это последний байт строки, и дальше ничего нет. Unicode-строки (двухбайтовые) должны заканчиваться соответственно двумя нулевыми байтами. Во-вторых, сравните записи:

В первом случае мы загружаем в al первый байт из строки stroka. А во втором случае мы загружаем указатель на строку stroka в регистр eax, и можем работать с самой строкой через указатель. Например, если мы хотим заменить вторую букву строки с "e" на "X", то достаточно просто написать так:

На первый взгляд, это сложная строка, но эта запись по сути эквивалентна stroka[1] = 'X'. Byte ptr говорит о том, что мы записываем один байт по адресу строки + 1. Строка выглядит как массив байтов, и по адресу stroka + 0 лежит первая ее буква, по адресу stroka + 1 - вторая, и так далее.

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

Вставьте этот код в код примера из предыдущей статьи после строки case TEST_BTN, как всегда, и посмотрите, как он работает.

Вы уже испугались, думая, что каждый раз придется ТАК считать длину строк? Не пугайтесь, всё уже сделано за нас! Предыдущий исходник эквивалентен такому:

Мы просто воспользовались WinAPI-функцией lstrlen!

Теперь немного дополнительного материала для интересующихся - а что, если мы хотим оставить свой личный просчет длины строки и вынести его в отдельную процедуру? Тогда напишем так:

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

Этот прототип говорит компилятору о том, что функция STRING_LENGHT принимает единственный аргумент типа "двойное слово", т.е. 4 байта - указатель на нашу строку.
Теперь у нас есть своя функция для просчета длины строки. Мы можем вставить ее в конец кода перед строкой "end start" и вызывать ее вместо lstrlen! Попробуйте сделать это сами. Не забудьте написать прототип функции в начало файла после подключения различных библиотек.

MASM32: Немного основ ассемблера: 10 комментариев

  1. Спасибо за статьи - я только начинаю осваивать ассемблер.
    Подскажите, пожалуйста, что означает выражение в операнде условного перехода:
    jnz $ + 11,
    я так понимаю это адрес метки, что означает знак доллара в этом выражении?

      1. MASM поддерживает удобные переходы по неименованным меткам еще.
        Например, переход до ближайшей метки "@@" ниже инструации перехода:
        jmp @F
        ...
        @@:

        И переход до ближайшей метки "@@" выше инструкции перехода:
        @@:
        ...
        jmp @B

  2. Доброго времени суток;
    Прошу Вашей помощи, пожалуйста;
    Пишу небольшую функцию
    MmASMGetCr3 PROC CR:qword
    movq [CR], 0
    MmASMGetCr3 ENDP
    END
    Но вот ml64 выдаёт ошибку error A2009:syntax error in expression в строке 3;
    Где я ошибся? Вызывается функция из C++ как MmASMGetCr3(cr3), cr3 имеет тип uint64;

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *