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

Настройка 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.

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

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


Способ третий. Если у вас нет (и не было) студии, ну или хотя бы 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)
Emelian
02.09.2025 13:21чтобы в 2025 году писать на ассемблере код в релиз нужны веские причины. Но это хороший способ понять, как всё работает «там внутри». Да и просто интересно.
Да, в свое время, было интересно. Занимался, раньше, декомпиляцией бинарного кода в ассемблерный и, обратно, компиляцией ассемблерного кода в бинарный ( https://erfaren.narod.ru ).
В «народном» дизассемблере IdaPro (автор Ильфак Гильфанов, кстати, у нас с ним был общий научрук на мехмате МГУ) есть даже плагин по переводу дизассемблированного кода, в код на Си. Классная вещь!
Вторая классная программа: «Оля Дебаговая» (Олег Ящук, из Германии, но, видимо, русский), отладчик бинарного кода. Он собирался писать аналогичный вариант программы для x64, не знаю, сделал ли?
Раньше интерес к дизассемблированию заключался в отсутствии желаемого опенсорса на ЯВУ. Теперь, хорошего опенсорса – валом, на любой вкус. Плюс вайбинг, от ИИ («искусственного идиота»). Других причин, у меня лично, к ассемблеру не появилось, поэтому, интерес к нему плавно угас. А ,«просто интересно», достаточно и на С++.
piton_nsk Автор
02.09.2025 13:21Других причин, у меня лично, к ассемблеру не появилось, поэтому, интерес к нему плавно угас. А ,«просто интересно», достаточно и на С++.
Это субъективно, конечно, все. Мне вот плюсы вообще не интересны, отдельные хидер файлы, бррр. А кто-то ковыряется, ему в кайф.
firehacker
02.09.2025 13:21Директива dw означает define word,
А я всегда думал, что data word.
Но вообще, такое впечатление, что ABI под 64-битный режим это плод фантазии очень больных голов.
И SEH тоже испортили, убрав цепочку фреймов с стека. Чем руководствовались — загадка.
piton_nsk Автор
02.09.2025 13:21Но вообще, такое впечатление, что ABI под 64-битный режим это плод фантазии очень больных голов.
Может были какие-то причины чтобы сделать именно так, но странного там много.
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 собирать. Портабелен, без зависимостей, линковщик не нужен, написан на себе самом.
unreal_undead2
В этих случаях ещё неплохо правила для SEH прописать(см. здесь, "Unwind helpers for MASM").
piton_nsk Автор
Решил уж совсем не раздувать объем. И так хотел сделать кратенько, получилось как получилось)