В этой части я расскажу о 64-разрядном ассемблере. Ассемблер будет тот же MASM, IDE будет всё та же Visual Studio. Для тех, кому не хочется использовать VS, будет простой пример с обычным батником в качестве скрипта для сборки. Покажу отличия 64-разрядного MASM и 32-разрядного, и как эти отличия сильно уменьшить макросами из MASM64 SDK. Будет пример работы с юникодом через консоль.

Первая часть про MASM 32 в Visual Studio — ссылка.

Код к статье на Гитхабе. Чтобы выбрать нужный пример для запуска из студии, надо поменять Entry Point в свойствах проекта на нужный, как указано на картинке ниже.

Как выбрать нужный пример для запуска из VS
Как выбрать нужный пример для запуска из VS

Настройка IDE

Здесь практически ничего нового. Есть пошаговая инструкция и готовый проект с хелловорлдом. Можно его скачать и запустить у себя. Если не заработает, тогда вникать в инструкцию. Главная фишка в том, что технически масмовский проект — это разновидность проекта на плюсах. Студия даёт нам возможность пошаговой отладки, просмотра регистров и памяти, хотя просмотр регистров можно было и поудобнее сделать, но зато всё есть сразу «из коробки». Для подсветки синтаксиса и подсказок я использую Asm-Dude. Версия для VS 2022 у меня подглючивала (по крайней мере, так было осенью 2024 года), поэтому я продолжил пользоваться версией для VS 2019. Если нужна только подсветка синтаксиса, то подсветка встречается почти везде, включая notepad++. Есть вариант вообще не пользоваться студией, но там есть нюансы. Подробнее об этом написано в конце статьи.

Что нового в 64-разрядном ассемблере?

Появились новые регистры и были расширены старые, указатели стали 64-битные, изменился ABI, в 64-разрядном masm убрали много директив из 32-разрядного, например invoke. Изменилась работа со стеком. Важный нюанс по ABI: все нововведения нужны только, если ваш код вызывает код из посторонних библиотек (банальный пример — WinAPI) или наоборот, посторонний код вызывает ваш. Внутри своего приложения можно делать всё, что вам нравится, необязательно выравнивать стек, можете не сохранять содержимое nonvolatile регистров, передавать хоть 10 параметров через регистры, благо регистров теперь много. Да и вообще можно делать всё, что вам захочется. Чем ассемблер и хорош. Работы с XYZMM/AVX касаться не буду, это отдельная тема. Дальше подробности по каждому пункту.

Новые регистры

Здесь всё просто. Появилось 8 новых регистров общего назначения — R8-R15. Старые регистры расширены до 64 бит, аналогично тому, как когда-то 16-битные были расширены до 32 бит. RAX — это расширенный EAX, RBP — это расширенный EBP и так далее. В соответствии с такой логикой именования регистр EIP стал RIP, что звучит мрачновато. Регистры разделяются на volatile и nonvolatile. Вот таблица с перечнем регистров, ориентируемся на неё.

  • Volatile (caller-saved) — используется для хранения временных значений, которые не надо сохранять при вызове процедур. Внутри процедур содержимое таких регистров можно менять как угодно. Поэтому любая процедура (неважно чужая или ваша) тоже может изменить их значения, и это надо учитывать. Если вам нужно сохранить значение такого регистра при вызове процедуры, вы это должны делать сами, поэтому они иногда называются caller-saved. Самый простой пример — это RAX, который по умолчанию служит для возврата значения при вызове.

  • Nonvolatile (calee-saved) — значения таких регистров не должны меняться при вызове процедур. Поэтому, если ваша процедура как-то меняет значения такого регистра, ваша задача, чтобы значение было восстановлено при выходе из процедуры. Отсюда и название calee-saved. Можно их не трогать, тогда не надо будет ничего восстанавливать.

Передача параметров

Здесь ещё проще, чем с регистрами. Первые 4 параметра теперь передаются через регистры, остальные через стек. Для целочисленных параметров порядок такой — RCX, RDX, R8, R9, для плавающей точки — XMM0, XMM1, XMM2, XMM3. Два вызова создания файла для наглядности.

32-разрядное

    push NULL
    push FILE_ATTRIBUTE_NORMAL
    push CREATE_ALWAYS
    push NULL
    push FILE_SHARE_READ
    push GENERIC_READ
    push offset fileName

    call CreateFileA

64-разрядное

	lea rcx, fileName
	mov rdx, GENERIC_READ
	mov r8, FILE_SHARE_READ
	mov r9, NULL
	mov qword ptr [rsp + 32], CREATE_ALWAYS
	mov qword ptr [rsp + 40], FILE_ATTRIBUTE_NORMAL
	mov qword ptr [rsp + 48], NULL

	call CreateFileA

Подробности тут.

Shadow space

Перед вызовом любой посторонней функции надо выделить на стеке минимум 32 байта. Эти 32 байта и называются shadow space, home space или spill space. Вызываемая функция может эти 32 байта использовать, может не использовать, но выделить их надо. Даже если параметров меньше 5 и все параметры передаются через регистры. На практике иногда работает и без этого, но гораздо лучше соблюдать соглашения.

Выравнивание стека

Стек должен быть выровнен на 16 байт, или, другими словами, значение RSP должно быть вида xxxxxxx0h. Что будет, если не выравнивать стек? Здесь всё просто, приложение упадёт (скорее всего) при вызове внешней функции, например WinAPI. Вроде бы всё просто, но любой push pop и, что самое неприятное, любой вызов функции будет менять значение RSP и за этим надо следить.

Выравнивать стек можно руками, подсчитывая сколько места будет нужно и добивая до 16 байт. Можно использовать вот такой шаблон, модифицируя при необходимости под свои нужды.

push rbp
mov rbp, rsp
and rsp, not 08h

mov rsp, rbp
pop rbp

В целом работа со стеком сводится к следующему:

В начале процедуры

  • Сохраняются значения non-volatile регистров, если эти регистры используются.

  • Выделяется место на стеке под локальные переменные.

  • Выделяется место на стеке под параметры вызываемых функций. Места должно хватить под параметры для функции с наибольшим числом аргументов. Shadow space (32 байта) должно быть в любом случае, независимо от числа параметров.

  • При необходимости стек выравнивается на 16 байт.

В конце процедуры

  • Восстанавливаются при необходимости значения non-volatile регистров.

  • Восстанавливается выделенное место на стеке.

Изменения в MASM 64

Изменения можно разделить на несколько пунктов:

  • Убраны директивы, специфичные для 32-разрядов, например «.686».

  • Добавлены директивы, специфичные для 64-разрядов, например «.pushframe».

  • Убраны директивы, сильно облегчающие жизнь наподобие invoke и связанные с условиями - .if, .else и прочие conditional control flow. Здесь не надо путать .if (с точкой, генерирует код для проверки условия) и обычный if. Обычный if (и прочие else) как был, так и остался.

В итоге в 64-разрядном MASM вот такой абсолютно рабочий 32-разрядный код не соберётся:

.IF wParam == 1000
   invoke SendMessageA, hwin, WM_SYSCOMMAND, SC_CLOSE, NULL
.ENDIF

Вместо этого надо будет написать примерно так:

cmp wParam, 1000
ja someLabel

mov rcx, hWin
mov rdx, WM_SYSCOMMAND
mov r8, SC_CLOSE
mov r9, NULL

call SendMessageA

someLabel:

Хорошего здесь мало, но мир не без добрых людей, поэтому были написаны макросы для 64-разрядного MASM, которые реализуют функционал для .if, .elseif, .else, .endif. Снова работает invoke и сильно упрощается выравнивание стека. Подробнее об этом будет в разделе MASM 64 SDK.

Unicode

Работа с юникод-строками ничем принципиально не отличается от работы с ANSI-строками, но есть нюансы. Windows использует UTF-16 little-endian, UTF-8 или UTF-32 не подойдёт, хотя технически это тоже юникод. В UTF-16 символ обычно кодируется либо двумя, либо 4 байтами (суррогатная пара), но здесь не всё так просто. Есть комбинирующие метки, канонический пример это и краткое. Для него есть свой код (U+0419), но можно представить в виде комбинации «и» (U+0418) и комбинирующей метки '̆̆' (U+0306). Юникод как стандарт — это тема для отдельного большого разговора, в контексте этой статьи достаточно знать, что символ (в большинстве случаев) будет занимать два байта. Но за этим надо следить самостоятельно. В null-terminated строках null тоже должен быть особенный — 0x0000, размером в два байта.

Именование функций WinAPI

Почти все функции, которые работают со строками, имеют две версии — для работы с юникодом и для работы с ansi. Отличаются они буквой A для ANSI и буквой W (wide) для юникода. Например MessageBoxA и MessageBoxW. Обычно где-то среди объявлений функций присутствует дефайн примерно такого вида, как указано ниже.

#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif // !UNICODE

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

Есть интересная особенность, которая к ассемблеру не имеет отношения. В своё время (вроде бы для Windows XP) ANSI-версии функций WinAPI были переписаны. Они преобразуют ANSI в Юникод и потом вызывают именно Юникод-версию. Для MessageBoxA цепочка вызовов такая:

  • MessageBoxA.

  • MessageBoxExA.

  • MessageBoxTimeoutA — внутри вызовы MBToWCS для конвертации ANSI в Юникод.

  • MessageBoxTimeoutW.

А для MessageBoxW такая:

  • MessageBoxW.

  • MessageBoxExW.

  • MessageBoxTimeoutW — заполнение специальной структуры с данными.

  • MessageBoxWorker — код для показа сообщения.

Поэтому если хотите сделать самый быстрый в мире вызов WinAPI, используйте сразу Юникод-версию.

Как кодировать Юникод-строку

Если нужны латинские буквы, можно немного схитрить и написать вот так:

maxwellCaption              dw "M", "A", "X", "W", "E", "L", "L", 0

Директива dw означает define word, поэтому получается нормальная Юникод-строка.

Можно найти коды для нужных символов и сделать так:

MOP_NABLA                   EQU 2207h
MOP_DOT                     EQU 22c5h
CAP_BETA                    EQU 0392h

maxwellEquationText         dw MOP_NABLA,MOP_DOT,CAP_BETA," ", "=", " ","0",0

Это удобно для специальных символов, особенно если их немного. Но при желании можно закодировать так хоть весь алфавит.

Привет, Максвелл
Привет, Максвелл

Банальный способ кодирования:

russianCaption              dw 0041fh, 00440h, 00438h, 00432h, 00435h, 00442h, 0002ch, 00020h, 0043ch, 00438h, 00440h, 0; Привет, мир

Здесь всё просто. Если нужна пара-другая слов, коды символов можно посмотреть на сайте (например этом, сайтов таких много), в любом hex-редакторе (например, в notepad++ есть плагин «HEX-Editor»). Здесь надо обращать внимание на порядок байт. Сравните одну и ту же строку (слово «код») определённую через dw (define word) и db (define byte).

db 03Ah, 004h,  03Eh, 004h,  034h, 004h,  0, 0; код
dw 0043Ah,      0043Eh,      00434h,      0   ; код

Я написал небольшую утилиту на шарпе, которая выдаёт сразу варианты для define byte и для define word. Запускается на любом dot net fiddle, результат копируете в исходники на ассемблере.

public class Program
{
	public static void Main()
	{
            var wstr = "юникод строка";
            var wsb = new StringBuilder();
            var bytes = Encoding.Unicode.GetBytes(wstr);
            var hexCodes = bytes.Select(b => b.ToString("X2")).ToList();

            wsb.Append("db ");
            hexCodes.ForEach(hex => wsb.Append("0"+hex+"h, "));
            wsb.Append("0, 0");
            wsb.Append("; "+wstr);

            var dbString = wsb.ToString();

            wsb.Clear();

            wsb.Append("dw ");
            for (int i = 0; i < hexCodes.Count / 2; i++)
            {
                wsb.Append("0");
                wsb.Append(hexCodes[i*2+1]);
                wsb.Append(hexCodes[i*2]);
	            wsb.Append("h, ");
            }
            wsb.Append("0");
            wsb.Append("; "+wstr);

            var dwString = wsb.ToString();
            Console.WriteLine(dbString);
            Console.WriteLine(dwString);
	}
}

HelloWorld 64 bit

Теперь 64-разрядный приветмир с учётом вышесказанного — shadow space, выравнивание стека, передача параметров через регистры.


ExitProcess PROTO
MessageBoxW PROTO

.data
myText			dw 0041fh, 00440h, 00438h, 00432h, 00435h, 00442h, 0002ch, 00020h, 0043ch, 00438h, 00440h, 0 ;привет, мир
myCaption		dw 0042eh, 0043dh, 00438h, 0043ah, 0043eh, 00434h, 0 ;юникод

.code
mainHelloWorld PROC

	sub rsp, 28h		; shadow space and align stack to 16

	mov rcx, 0          ; parameters passed in registers
	lea rdx, myText
	lea r8, myCaption
	mov r9, 0

	call MessageBoxW    ; call unicode version, 'invoke' doesnt work in masm 64

	mov rcx, 12345678	; the exit code, means nothing 
	call ExitProcess

	add rsp, 28h        ; restore stack
	ret
	
mainHelloWorld ENDP

END

Для контраста такой же приветмир, но с макросами из MASM 64 SDK.

OPTION DOTNAME                          ; required for masm64 sdk macro files
option casemap:none                     ; required for masm64 sdk macro files

;for the following includes change path to your installation of the masm 64 sdk
include ..\..\..\..\..\..\masm64\include64\win64.inc
include ..\..\..\..\..\..\masm64\include64\kernel32.inc
include ..\..\..\..\..\..\masm64\include64\user32.inc

include ..\..\..\..\..\..\masm64\macros64\vasily.inc
include ..\..\..\..\..\..\masm64\macros64\macros64.inc

.data
myText			dw 0041fh, 00440h, 00438h, 00432h, 00435h, 00442h, 0002ch, 00020h, 0043ch, 00438h, 00440h, 0 ;привет, мир
myCaption		dw 0042eh, 0043dh, 00438h, 0043ah, 0043eh, 00434h, 0 ;юникод

.code

STACKFRAME

mainHelloWorldSdk PROC

	invoke MessageBoxW, NULL, ADDR myText, ADDR myCaption, MB_OK

	invoke ExitProcess, 12345678 ; the exit code, means nothing 

	ret

mainHelloWorldSdk ENDP

END

Всё становится сильно проще, снова работает invoke, а волшебное слово STACKFRAME выравнивает стек за нас.

Макросы MASM 64 SDK

Что такое MASM SDK? Это независимый проект для работы с MASM. В целом это типичный SDK — примеры кода, утилиты и прочие полезные вещи. Есть сайт и форум. В 64-разрядном SDK есть чрезвычайно полезные макросы, которые сильно упрощают работу именно для 64-разрядного ассемблера. Есть макросы для выравнивания стека, для сохранения volatile-регистров, для упрощения вызова функций, для условных операторов. Полный список в справке к SDK, рекомендую ознакомиться.

Устанавливается MASM 64 SDK немного коряво. Если 32-разрядный SDK — это просто архивный файл, то 64-разрядный — это самораспаковывающийся архив, т. е. екзешник. Почему так сделано мне неведомо, я даже задавал этот вопрос на форуме, но внятного ответа не получил. Вот ветка форума с инструкциями для установки. Если весь sdk вам не нужен, то можно обойтись двумя файлами vasiliy.inc и macros64.inc, самое полезное именно там. Как подключить только эти inc-файлы, указано в примере выше.

Как обойтись без Visual Studio

В комментариях к предыдущей статье был вопрос — зачем использовать такую навороченную IDE, как Visual Studio, для сборки простой программы на ассемблере. Вопрос неплохой. Короткий ответ заключается в том, что в студии есть всё для сборки и отладки, ничего не надо устанавливать дополнительно. Дебажить при этом можно как исходный код на ассемблере, так и дизассемблер. Но можно обойтись и без студии.

Чтобы собрать приветмир из примера, нужно выполнить в командной строке буквально две команды

ml64.exe /c HelloWorld.asm
link.exe user32.lib kernel32.lib /SUBSYSTEM:CONSOLE /MACHINE:X64 /ENTRY:main /nologo /LARGEADDRESSAWARE HelloWorld.obj

Чтобы собрать минимальную программу на MASM 64, вам нужны ml64.exe, link.exe, mspdb140.dll, tbbmalloc.dll. Если подключаются библиотеки, то нужны, соответственно, библиотечные файлы. Для приветмира это kernel32.lib и User32.lib. Где все эти файлы взять?

Способ первый. Скачиваете с Гитхаба файл bare.zip, там в папке bare нужные файлы есть. Запускаете makeit.bat из этой папки и проверяете, что HelloWorld.exe собрался. Я проверял на нескольких разных машинах, но навряд ли будет работать везде. Если не собирается, то переходим к следующему способу.

Способ второй. В Visual Studio проект собирается, но всё равно хочется собирать приложение из командной строки. Путь к ml64 (link и dll) в студии можно узнать в свойстве VC++ Directories - Executable Directories.

Путь к ml64 в свойствах проекта
Путь к ml64 в свойствах проекта

Путь к библиотечным файлам можно посмотреть в тех же свойствах проекта, называется Library Directories:

Путь к библиотечным файлам в свойствах проекта
Путь к библиотечным файлам в свойствах проекта

Также можно в командную строку линкера добавить свойство /VERBOSE, запустить билд и посмотреть, откуда именно линкер тянет библиотечные файлы

VERBOSE для линкера
VERBOSE для линкера
Лог линкера в VS
Лог линкера в VS

Способ третий. Если у вас нет (и не было) студии, ну или хотя бы Windows SDK, то навряд ли эти файлы будут на компьютере. Но всё равно попробуйте их поискать просто по имени. Если ничего нет, можно попробовать Windows SDK, которое можно установить без студии. Полезная информация может быть на этом форуме. В целом найти всё это можно, но квест может быть долгим.

Пример консольного приложения REPL

Для примера работы с Юникодом будет простенький консольный REPL (read–eval–print loop). Вычислять он ничего не будет, будет просто повторять введённый текст и завершать работу при вводе команды «quit». Покажу самые основные вещи, чтобы не раздувать объём статьи. Пример написан в двух вариантах — с использованием MASM SDK и без него, Repl.asm и ReplSdk.asm соответственно. Сначала в качестве примера хотел сделать интерпретатор арифметических выражений. Это поинтересней, чем повтор введённой строки, но получалось много по объёму и мало интересного по содержанию. Поэтому решил сделать проще. В примере без MASM SDK я добавил подчёркивание к именам процедур, чтобы названия процедур в Repl.asm и ReplSdk.asm были разные, никакого скрытого смысла здесь нет.

Начинаем с прототипов нужных нам функций WinAPI и констант. Здесь сильно помогает MASM SDK, всё необходимое уже есть, и достаточно подключить пару-тройку inc файлов. Или вообще один.

include ..\..\..\..\..\..\masm64\include64\win64.inc
include ..\..\..\..\..\..\masm64\include64\kernel32.inc
include ..\..\..\..\..\..\masm64\include64\user32.inc

Без sdk надо всё делать самому. Где взять значения констант? Здесь всё просто: гуглите по названию константы либо названию функции, гугл выдаёт вам справку по WinAPI и там всё нужное есть. Строго говоря, пользоваться именованными константами необязательно, всё скомпилируется и будет работать без них. Но лучше так не делать, запутаться можно моментально.

ExitProcess    PROTO
GetStdHandle   PROTO
SetConsoleMode PROTO
ReadConsoleW   PROTO
WriteConsoleW  PROTO

.data

ENABLE_PROCESSED_OUTPUT     equ 1h
ENABLE_WRAP_AT_EOL_OUTPUT   equ 2h
ENABLE_PROCESSED_INPUT      equ 1h

ENABLE_LINE_INPUT           equ 2h
ENABLE_ECHO_INPUT           equ 4h

STD_INPUT_HANDLE            equ -10
STD_OUTPUT_HANDLE           equ -11

NULL                        equ 0

Строковые константы и буфер для текста из консоли одинаковый и там, и там:

inputStringBuffer db 1024 dup (?)
quitCommandString dw 00071h, 00075h, 00069h, 00074h, 0; quit
welcomeString     dw 00077h, 00072h, 00069h, 00074h, 00065h, 0003Eh, 00020h, 0; write>
repeatString      dw 00072h, 00065h, 00070h, 00065h, 00061h, 00074h, 00065h, 00064h, 0003Ah, 00020h, 0; repeated:
newLine           dw 13, 10, 0

Дальше всё просто, настраиваем консоль:

    LOCAL hInput :QWORD
    LOCAL actuallyRead  :QWORD

    sub rsp, 30h

    mov rcx, STD_INPUT_HANDLE
    call GetStdHandle

    mov hInput, rax
    mov rcx, hInput
    mov rdx, ENABLE_LINE_INPUT or ENABLE_ECHO_INPUT or ENABLE_PROCESSED_INPUT
    call SetConsoleMode

Обратите внимание на выравнивание стека в «простом варианте». Мы будем вызывать функции WinAPI с пятью параметрами, поэтому выделяем место на стеке под пять параметров, не забывая про выравнивание (поэтому 30h). В варианте с MASM SDK снова работает invoke, а ещё есть волшебная директива STACKFRAME, поэтому всё выглядит проще.

    LOCAL hInput :QWORD
    LOCAL actuallyRead  :QWORD

    invoke GetStdHandle, STD_INPUT_HANDLE
    mov hInput, rax
    invoke SetConsoleMode, hInput, ENABLE_LINE_INPUT or ENABLE_ECHO_INPUT or ENABLE_PROCESSED_INPUT

Затем основной цикл — читаем консоль, проверяем на «quit», завершаем программу или просто повторяем введённый текст в зависимости от результатов проверки. Вариант без MASM SDK:

    repl:
        lea rcx, welcomeString
        call ConsoleOut_

        mov rcx, hInput
        lea rdx, inputStringBuffer
        mov r8, 100
        lea r9, actuallyRead
        mov qword ptr [rsp + 32], NULL
        call ReadConsoleW

        lea rcx, inputStringBuffer
        mov rdx, actuallyRead
        call RemoveCrLf_

        ;check for quit
        lea rcx, quitCommandString
        lea rdx, inputStringBuffer
        call StartsWith_

        cmp rax, 1
        je endrepl

        ;repeat input
        lea rcx, repeatString
        call ConsoleOut_

        lea rcx, inputStringBuffer
        call ConsoleOut_

        lea rcx, newLine
        call ConsoleOut_

        jmp repl
    
    endrepl:

Посмотрите, насколько проще выглядит вариант с MASM SDK

repl:
        invoke ConsoleOut, ADDR welcomeString
        invoke ReadConsoleW, hInput, ADDR inputStringBuffer, 100, ADDR actuallyRead, NULL

        invoke RemoveCrLf, ADDR inputStringBuffer, actuallyRead

        ;check for quit
        invoke StartsWith, ADDR quitCommandString, ADDR inputStringBuffer

        cmp rax, 1
        je endrepl

        ;repeat input
        invoke ConsoleOut, ADDR repeatString
        invoke ConsoleOut, ADDR inputStringBuffer
        invoke ConsoleOut, ADDR newLine

        jmp repl
    
    endrepl:

Со строками работаем как с цепочками байт, держа в уме, что один символ это два байта (с оговорками, конечно) и что наши строки null-terminated. Надо понимать, что строки не обязательно должны быть null-terminated, сами собой они такими не станут. Для примера простейшая процедура, которая обнуляет символы перевода строки. Ассемблер ничего не «знает» про строки, просто пишем ноль как DWORD (4 байта) по нужному адресу.

; rcx is the string
; rdx is the string length
RemoveCrLf proc

    sub rdx, 2
    mov DWORD PTR [rcx+rdx*2], 0

    ret

RemoveCrLf endp

Для упрощения работы с параметрами можно использовать следующую хитрость, чтобы не путаться во всех этих [rsp + 32], [rsp + 40]

Parameter5 equ qword ptr [rsp + 32]

;теперь можно везде использовать Parameter5

mov Parameter5, NULL

Регистры, в которых передаются параметры процедур, являются volatile и могут меняться при вызове других процедур. Поэтому если ваша процедура вызывает другие процедуры, значения параметров надо сохранять. Можно использовать shadow space (здесь тоже можно упростить написание по аналогии с передачей параметров), а можно выделить на стеке отдельное место с подходящим названием (rcxReg, rdxReg).

StartsWith proc

    LOCAL patternLength : QWORD
    LOCAL sourceLength : QWORD
    LOCAL rcxReg : QWORD
    LOCAL rdxReg : QWORD

    mov rcxReg, rcx
    mov rdxReg, rdx

    invoke GetStringLength, rcxReg
    mov patternLength, rax

    invoke GetStringLength, rdxReg
    mov sourceLength, rax

    mov r10, rcxReg
    mov r11, rdxReg

С одной стороны, выделение лишнее, т. к. всё равно должно быть shadow space, с другой стороны, мне удобнее делать именно так.

Также обращайте внимание на Юникод версии WinAPI (буква W в именах функций)

call ReadConsoleW

call WriteConsoleW

Проверить, что это точно Юникод, и вообще посмотреть, что именно находится в памяти, очень легко. За что мне и нравится Visual Studio, просто ставишь точку останова в нужном месте и запускаешь билд. Срабатывает точка останова, указатель на буфер в регистре rcx, копипастим значение регистра в окно memory и можно смотреть, что именно из консоли попало в память. На скриншоте снизу обратите внимание, что перевод строки тоже в буфере.

Буфер для консоли
Буфер для консоли

Заключение

В 64-разрядном ассемблере и юникоде нет ничего «космического», а написание 64-разрядных приложений на ассемблере вполне посильное дело. Немного огорчает отсутствие многих директив из 32-разрядного MASM и возня с выравниванием стека, но и здесь есть готовые решения. Но директивы это специфика MASM, которым пользоваться не обязательно. Конечно, чтобы в 2025 году писать на ассемблере код в релиз нужны веские причины. Но это хороший способ понять, как всё работает «там внутри». Да и просто интересно. Надеюсь, вам понравилось.

© 2025 ООО «МТ ФИНАНС»

Комментарии (8)


  1. unreal_undead2
    02.09.2025 13:21

    все нововведения нужны только, если ваш код вызывает код из посторонних библиотек (банальный пример — WinAPI) или наоборот, посторонний код вызывает ваш

    В этих случаях ещё неплохо правила для SEH прописать(см. здесь, "Unwind helpers for MASM").


    1. piton_nsk Автор
      02.09.2025 13:21

      Решил уж совсем не раздувать объем. И так хотел сделать кратенько, получилось как получилось)


  1. Emelian
    02.09.2025 13:21

    чтобы в 2025 году писать на ассемблере код в релиз нужны веские причины. Но это хороший способ понять, как всё работает «там внутри». Да и просто интересно.

    Да, в свое время, было интересно. Занимался, раньше, декомпиляцией бинарного кода в ассемблерный и, обратно, компиляцией ассемблерного кода в бинарный ( https://erfaren.narod.ru ).

    В «народном» дизассемблере IdaPro (автор Ильфак Гильфанов, кстати, у нас с ним был общий научрук на мехмате МГУ) есть даже плагин по переводу дизассемблированного кода, в код на Си. Классная вещь!

    Вторая классная программа: «Оля Дебаговая» (Олег Ящук, из Германии, но, видимо, русский), отладчик бинарного кода. Он собирался писать аналогичный вариант программы для x64, не знаю, сделал ли?

    Раньше интерес к дизассемблированию заключался в отсутствии желаемого опенсорса на ЯВУ. Теперь, хорошего опенсорса – валом, на любой вкус. Плюс вайбинг, от ИИ («искусственного идиота»). Других причин, у меня лично, к ассемблеру не появилось, поэтому, интерес к нему плавно угас. А ,«просто интересно», достаточно и на С++.


    1. piton_nsk Автор
      02.09.2025 13:21

      Других причин, у меня лично, к ассемблеру не появилось, поэтому, интерес к нему плавно угас. А ,«просто интересно», достаточно и на С++.

      Это субъективно, конечно, все. Мне вот плюсы вообще не интересны, отдельные хидер файлы, бррр. А кто-то ковыряется, ему в кайф.


  1. firehacker
    02.09.2025 13:21

    Директива dw означает define word,

    А я всегда думал, что data word.

    Но вообще, такое впечатление, что ABI под 64-битный режим это плод фантазии очень больных голов.

    И SEH тоже испортили, убрав цепочку фреймов с стека. Чем руководствовались — загадка.


    1. piton_nsk Автор
      02.09.2025 13:21

      Но вообще, такое впечатление, что ABI под 64-битный режим это плод фантазии очень больных голов.

      Может были какие-то причины чтобы сделать именно так, но странного там много.


  1. AndreyDmitriev
    02.09.2025 13:21

    А мне Евро ассемблер очень зашёл для небольших набросков, либо микробенчмаркинга.

    Вот пример эхо, взвращающего тот же текст, и да, юникод:

    EUROASM AutoSegment=Yes, CPU=X64, Unicode=Yes
    habr PROGRAM Format=PE, Width=64, Model=Flat, ListMap=Yes, IconFile=, Entry=Start:
    
    INCLUDE winscon.htm, winabi.htm, cpuext64.htm
    
    Msg1 D "Введите текст:",0
    Msg2 D "Ваш текст: ",0
    Buffer DB 80 * B
        
    Start: nop
    	StdOutput Msg1, Console=Yes
    	StdInput Buffer, Console=Yes
    	StdOutput Msg2, Buffer, Console=Yes
    	TerminateProgram
    ENDPROGRAM habr

    Это надо скопировать в habr.asm

    Ассемблируется

    euroasm.exe habr.asm

    И в общем работает

    >habr.exe
    Введите текст:Prüfung Это мой текст
    Ваш текст: Prüfung Это мой текст

    Вот пример факториала из соседнего топика:

    EUROASM AutoSegment=Yes, CPU=X64
    fact PROGRAM Format=PE, Width=64, Model=Flat, ListMap=Yes, IconFile=, Entry=Start:
    
    INCLUDE winscon.htm, winabi.htm, cpuext64.htm
    
    Msg1 D "Enter Number:",0
    Msg2 D "Factorial is ",0
    Buffer DB 80 * B
        
    Start: nop
    	StdOutput Msg1, Console=Yes
    	StdInput Buffer ; Input Number
    	LodD Buffer ; https://euroassembler.eu/maclib/cpuext64.htm#LodD (rax <- Num)
    
    	mov rbx, 1        ; Initialize result to 1
    .loop:
    	imul rbx, rax     ; Multiply rbx by rax
    	dec rax           ; Decrement rax
    	cmp rax, 1        ; Check if rax <= 1
    	jg .loop          ; If rax > 1, repeat loop
    	mov rax, rbx      ; Move result to rax
    
    	StoD Buffer ; https://euroassembler.eu/maclib/cpuext64.htm#StoD (from rax)
    	StdOutput Msg2, Buffer, Console=Yes
    	TerminateProgram
    
    ENDPROGRAM fact

    Сборка и выполнение

    >euroasm.exe fact.asm
    >fact.exe
    Enter Number:6
    Factorial is 720

    Ну и так далее. В AVX-512 умеет, можно DLL собирать. Портабелен, без зависимостей, линковщик не нужен, написан на себе самом.


    1. piton_nsk Автор
      02.09.2025 13:21

      Евроассемблер тоже когда-то пробовал, штука по своему интересная.