Руководство. Отладка консольного приложения .NET с помощью Visual Studio Code
В этом учебнике представлены средства отладки, доступные в Visual Studio Code для работы с приложениями .NET.
Необходимые компоненты
- В этом учебнике используется консольное приложение, созданное в руководстве Создание консольного приложения .NET в Visual Studio Code.
Использование конфигурации отладочной сборки
В .NET используются две встроенные конфигурации сборки — Отладка и Выпуск. Вы воспользуетесь отладочной конфигурацией сборки для отладки и конфигурацией для выпуска для окончательного выпуска программы.
В отладочной конфигурации программы компилируется с полной символической отладочной информацией и без оптимизации. Оптимизация усложняет отладку, поскольку усложняется связь между исходным кодом и сгенерированными инструкциями. Конфигурация для выпуска полностью оптимизирована и не содержит символической отладочной информации.
По умолчанию параметры запуска в Visual Studio Code используют отладочную конфигурацию сборки, поэтому их не нужно изменять перед отладкой.
- Запустите Visual Studio Code.
- Откройте папку проекта, созданную по инструкциям из статьи Создание консольного приложения .NET в Visual Studio Code.
Установка точки останова
Точка останова приостанавливает выполнение приложения на инструкции, предшествующей той строке, в которой установлена точка останова.

- Откройте файл Program.cs.
- Установите точку останова в строке, где отображается имя, дата и время, щелкнув в левом поле окна кода. Левое поле находится слева от номеров строк. Или нажмите клавишу F9 либо выберите в меню пункт Запуск>Перейти к следующей точке останова при выбранной строке кода. Красная точка в левом поле обозначает строку с точкой останова Visual Studio Code.
Настройка входных данных терминала
Точка останова находится после вызова метода Console.ReadLine . Консоль отладки не принимает входные данные терминала для выполняющейся программы. Чтобы использовать выходные данные терминала во время отладки, можно использовать встроенный терминал (одно из окон Visual Studio Code) или внешний терминал. В этом учебнике используется встроенный терминал.
- Папка проекта содержит папку VSCODE . Откройте файл launch.json, который находится в папке VSCODE.
- В файле launch.json измените console параметр с internalConsole integratedTerminal :
"console": "integratedTerminal",
Запуск отладки
- Откройте окно отладки, щелкнув значок «Отладка» в меню слева.

- Щелкните зеленую стрелку в верхней части панели рядом с элементом .NET Core Launch (console) (Запуск .NET Core (консоль)). Еще один способ запуска программы в режиме отладки — нажать клавишу F5 или выбрать в меню пункт Запуск>Начать отладку.

- Перейдите на вкладку «Терминал «, чтобы просмотреть запрос «Что такое ваше имя?», который отображается в программе перед ожиданием ответа.

- Введите строку в окне Терминал в ответ на запрос имени, а затем нажмите клавишу ВВОД . Выполнение программы остановится, когда будет достигнута точка останова, то есть перед выполнением метода Console.WriteLine . В разделе Локальные окна Переменные отображаются значения переменных, которые определены в текущем выполняемом методе.

Использование консоли отладки
Окно Консоль отладки служит для взаимодействия с приложением, которое вы отлаживаете. Вы можете изменить значения переменных и посмотреть, как это повлияет на работу программы.
- Выберите вкладку Консоль отладки.
- Введите name = «Gracie» в запросе в нижней части окна Консоль отладки и нажмите клавишу ВВОД .

- Введите currentDate = DateTime.Parse(«2019-11-16T17:25:00Z»).ToUniversalTime() в нижней части окна Консоль отладки и нажмите клавишу ВВОД . В окне Переменные отображаются новые значения переменных name и currentDate .
- Продолжите выполнение программы, нажав кнопку Продолжить на панели инструментов. Еще один способ продолжить — нажать клавишу F5 .

- Снова выберите вкладку Терминал. Значения, отображаемые в окне консоли, соответствуют изменениям, произведенным в окне Консоль отладки.

- Нажмите любую клавишу, чтобы выйти из приложения и остановить отладку.
Установка условной точки останова
Программа отображает строку, которую вводит пользователь. Что произойдет, если пользователь ничего не введет? Это можно проверить с помощью полезной функции отладки, которая называется условной точкой останова.

- Щелкните правой кнопкой мыши красную точку, обозначающую точку останова (или щелкните ее, удерживая нажатой клавишу CTRL в macOS). В контекстном меню выберите Изменить точку останова, чтобы открыть диалоговое окно, в котором можно ввести условное выражение.
- Выберите Expression в раскрывающемся списке, введите следующее условное выражение и нажмите клавишу ВВОД .
String.IsNullOrEmpty(name)

При каждом достижении точки останова отладчик вызывает метод String.IsNullOrEmpty(name) и останавливается на этой строке только в том случае, если вызов метода возвращает true . Вместо условного выражения можно указать количество обращений (выполнение программы будет прервано, пока инструкция не будет выполнена указанное количество раз) или условие фильтра (выполнение программы будет прервано на основе таких атрибутов, как идентификатор потока, имя процесса или имя потока).
name == String.Empty
Пошаговое выполнение программы
Visual Studio Code позволяет выполнять программу пошагово, отслеживая результат ее выполнения. Для этого обычно задают точку останова и запускают программу в небольшой части ее кода. Поскольку наша программа невелика, давайте выполним ее пошагово.
- Установите точку останова на открывающей фигурной скобке метода Main .
- Нажмите клавишу F5 , чтобы запустить отладку. Visual Studio Code выделит строку точки останова. На этом этапе в окне Переменные показано, что массив args пуст, а name и currentDate имеют значения по умолчанию.
- Выберите Запуск>Шаг с заходом или нажмите клавишу F11 .
Будет выделена следующая строка. - Выберите Запуск>Шаг с заходом или нажмите клавишу F11 . Visual Studio Code выполняет Console.WriteLine для запроса имени и выделяет следующую строку выполнения. Следующая строка — это Console.ReadLine для name . Окно «Переменные » не изменяется, а вкладка «Терминал » отображает запрос «Что такое ваше имя?».
- Выберите Запуск>Шаг с заходом или нажмите клавишу F11 . Visual Studio выделит назначение переменной name . В окне Переменные видно, что name по-прежнему null .
- Ответьте на этот запрос, введя строку на вкладке «Терминал» и нажав клавишу ВВОД . На вкладке Терминал может не отображаться введенная строка, однако метод Console.ReadLine будет записывать входные данные.
- Выберите Запуск>Шаг с заходом или нажмите клавишу F11 . Visual Studio Code выделит назначение переменной currentDate . В окне Переменные отображается значение, полученное в результате вызова метода Console.ReadLine. На вкладке Терминал также отображается строка, указанная в командной строке.
- Выберите Запуск>Шаг с заходом или нажмите клавишу F11 . В окне Переменные отображается значение переменной currentDate , которому было присвоено свойство DateTime.Now.
- Выберите Запуск>Шаг с заходом или нажмите клавишу F11 . Visual Studio Code вызывает метод Console.WriteLine(String, Object, Object). В окне консоли отображается форматированная строка.
- Выберите Запуск>Шаг с выходом или нажмите клавиши SHIFT + F11 .

- Выберите вкладку Терминал. В окне терминала отобразится сообщение «Нажмите любую клавишу, чтобы выйти. «
- Нажмите любую клавишу для выхода из программы.
Использование конфигурации сборки для выпуска
После тестирования отладочной версии приложения следует скомпилировать и протестировать версию в режиме выпуска. Версию для выпуска компилятор собирает с использованием методов оптимизации, которые могут влиять на поведение приложения. Например, оптимизации компилятора для повышения производительности могут привести к состоянию конкуренции в многопоточных приложениях.
Чтобы создать и протестировать версию консольного приложения для выпуска, откройте терминал и выполните следующую команду:
dotnet run --configuration Release
Дополнительные ресурсы
- Debugging in Visual Studio Code (Отладка в Visual Studio Code)
Следующие шаги
В этом учебнике вы использовали средства отладки Visual Studio Code. В следующем руководстве вы опубликуете развертываемую версию приложения.
Ассемблер для Windows используя Visual Studio
Многие из нас изучали ассемблер в университете, но почти всегда это ограничивалось простыми алгоритмами под DOS. При разработке программ для Windows может возникнуть необходимость написать часть кода на ассемблер, в этой статье я хочу рассказать вам, как использовать ассемблер в ваших программах под Visual Studio 2005.

Создание проекта
В статье мы рассмотрим как вызывать ассемблер из С++ кода и обратно, передавать данные, а также использовать отладчик встроенный в Visual Studio 2005 для отладки кода на ассемблер.
Для начала нам нужно создать проект. Включаем Visual Studio, выбираем File > New > Project. В Visual Studio нет языка ассемблер в окне выбора типа проекта, поэтому создаем С++ Win32 проект. В окне настроек нового проекта выбираем «Empty Project».

По умолчанию Visual Studio не распознает файлы с кодом на ассемблер. Для того чтобы включить поддержку ассемблер нам необходимо настроить в проекте условия сборки указав какой программой необходимо компилировать файлы *.asm. Для этого выбираем пункт меню «Custom Build Rules. ».

В открывшемся окне мы можем указать специальные правила компиляции для различных файлов, Visual Studio 2005 уже имеет готовое правило для файлов *.asm, нам необходимо лишь включить его, установив напротив правила «Microsoft Macro Assembler» галочку.

Добавление исходного кода
Перейдем к написанию исходного кода нашего проекта. Начнем с добавления исходного кода на c++. Добавим новый файл в папку Source Files. В качестве Template выбираем C++ File и вводим желаемое имя файла, например main.cpp. Напишем функцию, которая будет считывать имя введенное пользователем, оформив это в виде функции readName() которая будет возвращать ссылку на считанное имя. Мы получим примерно следующее содержимое файла:
#include void main () < printf("Hello, what is your name?\n"); >void* readName()
Теперь, когда мы знаем имя пользователя мы можем вывести приветствие, его будет выводить функция sayHello() которую мы напишем на ассемблер, чтобы использовать эту функцию сначала мы должны указать что она будет определена в другом файле, для этого добавим блок к main.cpp:
extern «C»
Этот блок говорит компилятору, что функция sayHello() будет объявлена в другом файле и будет иметь правила вызова «C». Компилятор C++ искажает имена функций так, что указание правил вызова обязательно. Кроме того мы хотим использовать функцию readName() из функции sayHello(), для этого необходимо добавить extern «C» перед определением функции readName(), это позволит вызывать эту функцию из других файлов используя правила вызова «C».
Пришло время добавить код на ассемблер, для этого добавим в Source Folder новый файл. Выбираем тип Text File (.txt) и в поле название заменяем .txt на .asm, назовем наш файл hello.asm. Объявим функцию sayHello() и укажем внешние функции, которые мы хотим использовать. Получим следующий код:
.686 .MODEL FLAT, C .STACK .DATA helloFormat BYTE "Hello %s!",10,13,0 .CODE readName PROTO C printf PROTO arg1:Ptr Byte, printlist: VARARG sayHello PROC invoke readName invoke printf, ADDR helloFormat, eax ret sayHello ENDP END
Теперь мы можем запустить проект, для этого просто выбираем Debug > Start Without Debugging или нажимаем комбинацию Ctrl-F5. Если все сделано верно, вы увидите окно программы:

Немного усложним задачу, попробуем написать на ассемблер функцию принимающую параметр и возвращающую значение. Для примера напишем функцию calcSumm() которая будет принимать целое число и возвращать сумму его цифр. Изменим наш код на С++ добавив в него информацию о функции calcSumm, ввод числа и собственно вызов функции. Добавим функцию в файл hello.asm, возвращаемое значение помещается в eax, параметры объявляются после ключевого слова PROC. Все параметры можно использовать в коде процедуры, они автоматически извлекутся из стека. Также в процедурах можно использовать локальные переменные. Вы не можете использовать эти переменные вне процедуры. Они сохранены в стеке и удаляются при возврате из процедуры:
.686 .MODEL FLAT, C .STACK .DATA helloFormat BYTE "Hello %s!",10,13,0 .CODE readName PROTO C printf PROTO arg1:Ptr Byte, printlist: VARARG sayHello PROC invoke readName invoke printf, ADDR helloFormat, eax ret sayHello ENDP calcSumm PROC a:DWORD xor esi, esi mov eax, a mov bx, 10 @div: xor edx, edx div bx add esi, edx cmp ax, 0 jne @div mov eax, esi ret calcSumm ENDP END
Запустив проект мы увидим следующий результат выполнения:

Отладка
Конечно в данной задаче нет ничего сложного и она вовсе не требует использования ассемблер. Более интересным будет рассмотреть, а что же нам дает Visual Studio для разработки на ассемблер. Попробуем включить режим отладки и установим точку остановки в hello.asm, запустим проект, мы увидим следующее:

Окно Disassembly (Debug > Windows > Disassembly) показываем команды ассемблер для данного объектного файла. Код который мы написали на С++ показывается черным цветом. Disassembled code показывается серым после соответствующего ему кода на C++/ассемблер. Окно Disassembly позволяет отлаживать код и осуществлять stepping по нему.
Окно регистров (Debug > Windows > Registers) позволяет посмотреть значение регистров.
Окно памяти (Debug > Windows > Memory) позволяет посмотреть дамп памяти, слева мы видим шестнадцатеричные адрес, справа шеснадцатеричные значения соответствующих ячеек памяти, можно перемещаться, вводя адрес в соответствующее поле в верху окна.
Assembler для Windows в Visual Studio
В этой статье я покажу как написать приложение для windows на ассемблере. В качестве IDE будет привычная Visual Studio 2019 со своими плюшками — подсветка кода, отладка и привычный просмотр локальных переменных и регистров. Собирать приложение будет MASM, а значит, у нас будут и чисто масмовские плюшки. Приложением будет игра в пятнашки. С одной стороны это просто, но не так чтобы совсем хелловорлд (впрочем хелловорлд мы тоже увидим во время настройки VS). С другой стороны это будет полноценное оконное приложение с меню, иконкой, отрисовкой, выводом текста и обработкой мыши с клавиатурой. Изначально у меня была мысль сделать что-нибудь поинтереснее пятнашек, но я быстро передумал. Сложность и размер статьи увеличивается значительно, а она и так получилась немаленькая. Новой же информации сильно больше не становится. Статья рассчитана на тех, кто имеет хотя бы начальные знания ассемблера, поэтому здесь не будет всяких мелочей из разряда как поделить одно число на другое или что делает команда mov. Иначе объем пришлось бы увеличить раза в три.
Заранее постараюсь ответить на вопрос — зачем это нужно, если на ассемблере сейчас уже никто не пишет? Есть пара объективных причин и одна субъективная. Из объективного — написание подобных программ позволяет понять внутреннее устройство windows и как в конечном итоге наш код исполняется на процессоре. Далеко не всем это действительно надо, но для некоторых вещей это обязательное знание. Вторая причина это то, что позволяет взглянуть на разработку немного под другим углом. Примерно так же как попробовать функциональное программирование полезно даже если не писать ничего в функциональном стиле. К примеру я слушал лекции Мартина Одерски вовсе не потому что решил перейти с C# на Scala. Полезно посмотреть на привычную разработку под другим углом. Субъективная же причина — для меня это было просто интересно, отойти от коммерческой разработки, этого цикла задач, спринтов, митингов, сроков и заняться тем, что интересно именно тебе.
Так получилось что у меня появилось много свободного времени, часть из которого я потратил на то, что называется пет-проектами. Это не стало какими-то production-ready вещами, скорее какие-то идеи интересные лично мне, что-то на что вечно не хватало времени. Одна из этих идей это ассемблер в современной IDE. Давно хотел этим заняться, но все не было времени. Мне было очень интересно со всем этим разбираться, надеюсь читателям тоже понравится.
Шаг первый — настраиваем VS
Тут я немного схитрил. Точнее так уж получилось, что здесь все уже сделано за нас. Есть пошаговая инструкция и даже готовый пустой проект. Можно воспользоваться пошаговой инструкцией, а я просто скачал пустой проект и переименовал SampleASM в FifteenAsm. Единственное, что надо сделать помимо переименования, это установить SubSystem : Windows в свойствах проекта (properties > Linker > System > SubSystem : Windows). Далее выбираем x86, нажимаем F5 (либо кликаем мышкой) и видим вот такое сообщение:

Теперь о подсветке синтаксиса. Тут есть разные пути, и я решил поискать что есть готового. Готового оказалось немного, я установил Asm-Dude. Также попробовал ChAsm, но внешний вид меня не порадовал. Впрочем внешний вид это дело вкуса, я остановился на Asm-Dude. Тут правда есть такой нюанс — Asm-Dude не поддерживает VS 2022, самая старшая версия VS 2019. Вот так выглядит все в сборе — дебаг, просмотр переменных, в т.ч. нормальное отображение структур, мнемоника для ассемблера.

Update: AsmDude для 2022 существует, но его нет в студийном менеджере расширений (автор не стал публиковать), доступен по ссылке. Вторая версия расширения в процессе разработки, и 1 октября автор расширения ее опубликовал. Во второй версии заявлено много всяких фич, не все они готовы, но пользоваться уже можно. Спасибо @NN1 и @aroman313 за уточнение.
Теперь еще одна вещь, о которой хочется рассказать, прежде чем приступить к основной части. Это MASM SDK. Это совсем необязательная вещь, но очень полезная. Там есть готовые inc файлы для WinAPI, а еще есть много примеров самых разных приложений на ассемблере. Но проект из этой статьи будет работать и без него.
Шаг второй — оконное приложение
Для того чтобы создать окно средствами WinAPI нужно немного. Заполнить специальную структуру с описанием этого окна, зарегистрировать класс окна, потом это окно создать. Вот практически и все. Еще нам нужна так называемая оконная процедура, или процедура обработки сообщений, называют ее по разному. Суть этой процедуры в обработке сообщений которые приходят в наше приложение. Клики мышкой, команды меню, отрисовка и вообще все специфическое поведение нашего приложения будет там. Со всеми подробностями написано здесь.
О вызове функций вообще и WinAPI в частности
Чтобы вызвать функцию, ее надо объявить. Ниже разные способы это сделать.
extern MessageBoxA@16 : PROC MessageBoxA PROTO, hWnd:DWORD, pText:PTR BYTE, pCaption:PTR BYTE, style:DWORD MessageBoxW PROTO, :DWORD,:DWORD,:DWORD,:DWORD
Объявление со списком параметров более понятно. Хотя именовать параметры и необязательно. Объявлением с extern я пользоваться не буду, оставим это для любителей разгадывать ребусы. Что такое A(или W) в имени функции? Это указание на тип строк, A — ANSI, W — Unicode. Для простоты дела я решил не связываться с юникодом и везде использовал ANSI версии. Обычно же применяют дефайн примерно такого вида:
#ifdef UNICODE #define SetWindowText SetWindowTextW #else #define SetWindowText SetWindowTextA #endif
Теперь о вызовах функций, «стандартный» для ассемблера вызов выглядит так
push 0 push offset caption push offset text push 0 call MessageBoxA
Существует мнемоническое правило для порядка аргументов — слева направо — снизу вверх. Иными словами первый аргумент в объявлении функции (здесь это хендл окна hWnd:DWORD) будет в самом нижнем push. К счастью в MASM есть очень удобная вещь — invoke. Вот так выглядит вызов той же самой функции.
invoke MessageBoxA, 0, offset howToText, offset caption, 0
Одна строчка вместо пяти. На мой взгляд invoke удобнее за редкими исключениями типа большого числа аргументов. В дальнейшем практически везде я буду пользоваться invoke.
Сигнатура, описание и примеры использования функций WinAPI легко гуглятся по их названию. На примере MessageBoxA мы увидим вот это
int MessageBoxA( [in, optional] HWND hWnd, [in, optional] LPCSTR lpText, [in, optional] LPCSTR lpCaption, [in] UINT uType );
Осталось перевести все эти HWND и LPCSTR в соответствующие типы для ассемблера. Тип данных LPCSTR будет DWORD, ведь это просто ссылка. Олдфаги с легкостью узнают венгерскую нотацию, а название типа расшифровывается как Long Pointer Const String. HWND тоже будет просто DWORD, ведь HWND, как и LPCSTR по своей сути просто ссылка. Ну а UINT это DWORD просто по определению. В некотором роде сигнатура функций на ассемблере даже проще, ссылка здесь это просто ссылка, нет кучи разных типов.
Отсюда следует важный вывод — нет никаких специальных «ассемблерных» функций, это то же самое WinAPI !. Нам достаточно знать как решается нужная нам задача средствами WinAPI, неважно на каком языке они будут вызываться. Поэтому задача «вывести текст в окно средствами ассемблера» на самом деле будет «вывести текст в окно средствами WinAPI», а уж информации по WinAPI полно. Обратное тоже верно, зная как что-то сделать средствами WinAPI это можно сделать на практически любом языке. А это уже часто бывает полезно при написании скриптов.
Создаем простое окно
Перед созданием окна я сделал три inc-файла. Один с прототипами WinAPI, другой с константами приложения (ширина окна, заголовок, цвет заливки и все в таком же духе) и третий, со структурами WinAPI и целой кучей винапишных констант. Теперь можно писать NULL или TRUE/FALSE. Или MB_OK вместо 0, как в примере выше с MessageBoxA. Никаких специфических действий не нужно, просто Add — New Item — Text File и не забываем include filename. Файлики назвал WinApiProto.inc, WinApiConstants.inc, AppConstants.inc. Пример содержимого показан ниже.

Вот так теперь выглядит наш код
.386 .model flat, stdcall .stack 4096 include WinApiConstants.inc include WinApiProto.inc .data include AppConstants.inc .code main PROC ;. more code
Небольшое отступление про строки. Вот пример строковых констант
szClassName db "Fifteen_Class", 0 howToText db "The first line", CR , LF , "The second.", 0
Запятая означает конкатенацию, db это define byte, CR LF определены в WinApiConstants.inc (13 и 10 соответственно), ноль на конце это null-terminated строка. В итоге строки это никакой не специальный тип данных, а просто массивы байт с нулем на конце. В случае с юникодом возни было бы больше, но я решил не усложнять себе жизнь и использовать везде ANSI строки.
Вот мы и добрались до создания окна. Для этого нам надо
- заполнить структуру WNDCLASSEX (объявлена в WinApiConstants)
- зарегистрировать класс окна
- создать процедуру главного окна
- создать окно
Кода вышло уже почти на 200 строк, поэтому я покажу самые интересные куски, целиком можно посмотреть на гитхабе.
Объявление и заполнение WNDCLASSEX, как видим все как в языках высокого уровня. Ну, почти все — автодополнения со списком полей структуры нет.
WNDCLASSEX STRUCT cbSize DWORD ? style DWORD ? lpfnWndProc DWORD ? WNDCLASSEX ENDS mov wc.cbSize, sizeof WNDCLASSEX mov wc.style, CS_BYTEALIGNWINDOW mov wc.lpfnWndProc, offset WndProc
При создании окна весьма важный параметр WS_EX_COMPOSITED. Без него при перерисовке будет мерзкий flickering. Очень хорошо что это работает — реализовывать двойную буферизацию самостоятельно желания не было.
push WS_EX_OVERLAPPEDWINDOW or WS_EX_COMPOSITED call CreateWindowExA
Теперь немного чудесных директив MASM. Вот так вот просто организован цикл обработки сообщений
; Loop until PostQuitMessage is sent .WHILE TRUE invoke GetMessageA, ADDR msg, NULL, 0, 0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessageA, ADDR msg .ENDW
А вот так без них
StartLoop: push 0 push 0 push 0 lea eax, msg push eax call GetMessageA cmp eax, 0 je ExitLoop lea eax, msg push eax call TranslateMessage lea eax, msg push eax call DispatchMessageA jmp StartLoop ExitLoop:
А вот как все просто в оконной процедуре. Никаких тебе cmp uMsg, WM_DESTROY, кучи меток, простой IF
.IF uMsg == WM_DESTROY invoke PostQuitMessage, NULL xor eax, eax ret .ENDIF
Вот как делается подтверждение на закрытие окна
.IF uMsg == WM_CLOSE invoke MessageBoxA, hwin, ADDR exitConfirmationText, ADDR caption, MB_YESNO .IF eax == IDNO xor eax, eax ret .ENDIF .ENDIF
Обещанный хелловорлд готов.

Добавляем иконку и меню
Иконка и меню в мире windows относятся к ресурсам. Поэтому добавляем к нашему проекту файл ресурсов — Add — Resource — Menu. Дальше можно воспользоваться встроенным редактором VS, я просто взял и отредактировал свежий файл FifteenAsm.rc в блокноте. Получилось вот так
500 ICON MOVEABLE PURE LOADONCALL DISCARDABLE "FIFTEENICON.ICO" 600 MENUEX MOVEABLE IMPURE LOADONCALL DISCARDABLE BEGIN POPUP "&File", , , 0 BEGIN MENUITEM "&New Game", 1100 MENUITEM "&Exit", 1000 END POPUP "&Help", , , 0 BEGIN MENUITEM "&How to play", 1800 MENUITEM "&About", 1900 END END
Обратите внимание на магические числа 500 и 600. Это идентификаторы ресурсов, совсем скоро мы увидим зачем они нужны. Также обратите внимание на магические числа 1000, 1100, 1800, 1900. Это идентификаторы команд, мы тоже увидим зачем они нужны, но чуть позже. Чуть не забыл про сам файл иконки, нарисовал я ее в каком-то онлайн редакторе. Дизайнер из меня так себе, поэтому что получилось, то получилось. Добавляем в проект под именем Fifteenicon.ico, тут главное назвать точно как в файле ресурсов. Дальше все просто. Иконка добавляется на этапе заполнения структуры WNDCLASSEX, тут у нас магическое число 500
push 500 push hInst call LoadIconA mov wc.hIcon, eax
Меню добавляется после создания окна, здесь магическое число 600
call CreateWindowExA mov hWnd,eax push 600 push hInst call LoadMenuA push eax push hWnd call SetMenu
А вот так обрабатываются команды меню, тут остальные магические числа 1000, 1100, 1800, 1900. Вообще с использованием MASM код не особо отличается от кода на тех же плюсах.
.IF uMsg == WM_COMMAND .IF wParam == 1000 invoke SendMessageA, hwin, WM_SYSCOMMAND, SC_CLOSE, NULL .ENDIF .IF wParam == 1100 invoke MessageBoxA, hwin, ADDR newGameConfirmationText, ADDR caption, MB_YESNO .IF eax == IDYES ;call InitTilesData .ELSEIF eax == IDNO xor eax, eax ret .ENDIF .ENDIF .IF wParam == 1800 invoke MessageBoxA, hwin, ADDR howToText, ADDR caption, MB_OK .ENDIF .IF wParam == 1900 invoke MessageBoxA, hwin, ADDR aboutText, ADDR caption, MB_OK .ENDIF .ENDIF
У приложения появилась иконка и есть меню. Ради интереса посмотрел на размер исполняемого файла, всего 6656 байт.

Шаг третий — игра
Создали окно, пора заняться самой игрой. Здесь я тоже покажу только самые интересные места.
Инициализация данных и начальная перетасовка
Данные о положении тайлов будут храниться в массиве из 16 байт. Ноль будет положением пустого тайла, значения от 1 до 15 соответствующие тайлы. Нумерация индексов тайлов слева направо, сверху вниз. Теперь надо их перетасовать и тут встает вопрос, откуда брать случайные числа? RDRAND и RDSEED появились достаточно поздно, а мне хотелось сделать код в «классическом» стиле. Сгоряча я даже думал реализовать Вихрь Мерсенна, но потом решил что это перебор. Поэтому честно нашел простенький ГПСЧ буквально в десяток команд, для seed использовал системное время. Идея начальной перетасовки простая, сначала заполняем массив по порядку (приводим в конечное состояние), а потом случайным образом двигаем тайлы. Тайлы двигаются по правилам, значит их всегда можно будет собрать в конечное положение. Если заполнять тайлы совсем рандомно, то надо проверять можно ли вообще собрать такую комбинацию. По опыту уже 100 итераций перемешивает тайлы вполне нормально.
local randSeed : DWORD invoke GetTickCount mov randSeed, eax xor eax, eax xor ebx, ebx xor ebx, ebx .WHILE ebx < initialSwapCount mov eax, 4; random numbers count, i.e. from 0 to 3 push edx imul edx, randSeed, prndMagicNumber inc edx mov randSeed, edx mul edx mov eax, edx pop edx add al, VK_LEFT push ebx invoke ProcessArrow, NULL, al; move a tile pop ebx inc ebx .ENDW ret
Отрисовка
Добавляем обработку WM_PAINT в оконной процедуре
LOCAL Ps :PAINTSTRUCT LOCAL hDC :DWORD .IF uMsg == WM_PAINT invoke BeginPaint, hWin, ADDR Ps mov hDC, eax invoke PaintProc, hWin, hDC invoke EndPaint, hWin, ADDR Ps .ENDIF
Отрисовка тайлов. Из интересного здесь организация двойного цикла с использованием директив MASM WHILE и передача указателя на RECT в процедуре CalculateTileRect.
LOCAL Rct : RECT invoke CreateSolidBrush, tileBackgroundColor mov hBrush, eax invoke SelectObject, hDC, hBrush ;fill tiles with background color mov vert, 0 .WHILE vert < 4 mov hor, 0 .WHILE hor < 4 invoke CalculateTileRect, ADDR Rct, hor, vert invoke RoundRect, hDC, Rct.left, Rct.top, Rct.right, Rct.bottom, tileRoundedEllipseSize, tileRoundedEllipseSize inc hor .ENDW inc vert .ENDW invoke DeleteObject, hBrush CalculateTileRect proc rct :DWORD, hor:BYTE, vert:BYTE mov edx, rct invoke CalculateTileRectPos, hor, 0 mov (RECT PTR [edx]).left, eax ret CalculateTileRect endp
Обратите внимание на эту строчку. Структура передана по ссылке, смещение на left вычисляется автоматически.
mov (RECT PTR [edx]).left, eax
А вот как работает IntToStr (почти что честный) на ассемблере. Писать честный IntToStr мне не хотелось, поэтому я тут схитрил. Завел массив из 3 байт под строку, второй и третий байты сразу обнуляются. Числа бывают от 1 до 15, поэтому если число было меньше 10, то к значению прибавляем магическое число 48 (ASCII код для нуля) и получаем нужный первый байт буфера. Получается тоже самое что и на Си, когда пишем c = '0' + i. Поскольку второй байт уже нулевой у нас получается готовая null-terminated строка, неважно что буфер из 3 байт. Если число больше 9, то первая цифра всегда 1, а вторая это остаток от деления на 10. Тут уже третий байт играет роль конца строки.
mov [buffer+1], 0 mov [buffer+2], 0 .IF bl < 10 add bl, asciiShift mov [buffer], bl sub bl, asciiShift .ELSEIF bl >9 mov al, asciiShift inc al mov [buffer], al xor ax, ax mov al, bl mov cl, 10 div cl add ah, asciiShift mov [buffer+1], ah .ENDIF
Вот так выглядит игровое поле

Добавляем интерактив
Для управления можно пользоваться курсором или кликать мышкой по тайлу, который надо переместить, благо вариант перемещения только один. Перемещение сводится к тому чтобы в массиве тайлов поменять местами перемещаемый и нулевой тайл. Смещение нулевого тайла будет +1/-1 для перемещений вправо/влево и +4/-4 для перемещения вверх/вниз. Путь у тайла только один, поэтому надо только проверить выход за диапазон и поменять местами два элемента в массиве тайлов. Если тайл переместился, то перерисовать окно. Добавим вот такие обработчики в нашу оконную процедуру.
.IF uMsg == WM_KEYDOWN .if wParam == VK_LEFT invoke ProcessArrow, hWin, wParam .elseif wParam == VK_RIGHT invoke ProcessArrow, hWin, wParam .elseif wParam == VK_UP invoke ProcessArrow, hWin, wParam .elseif wParam == VK_DOWN invoke ProcessArrow, hWin, wParam .endif .ENDIF .IF uMsg == WM_LBUTTONUP invoke ProcessClick, hWin, lParam .ENDIF
Сначала посмотрим как реализовано перемещение тайлов курсором. Вот немного укороченная версия процедуры ProcessArrow. FindEmptyTileIndex возвращает в регистре eax индекс пустого тайла . В зависимости от нажатой клавиши проверяем выход за границы диапазона, т.е. можно ли переместить тайл в данной позиции в данном направлении. Если нельзя, уходим на метку pass в конец процедуры, если можно, то вызываем последовательно SwapTiles, RedrawWindow и ProcessPossibleWin.
ProcessArrow proc hWin:DWORD, key:DWORD call FindEmptyTileIndex .IF key == VK_UP cmp eax, 12 ja pass ;when tile goes up, new empty tile index (ETI) will be ETI+4, mov ebx, eax add ebx, 4 .ENDIF .IF key == VK_RIGHT ;empty tile shouldnt be on 0, 4, 8, 12 indexes cmp eax, 0 je pass cmp eax, 4 je pass cmp eax, 8 je pass cmp eax, 12 je pass ;when tile goes right, new empty tile index (ETI) will be ETI-1, mov ebx, eax dec ebx .ENDIF invoke SwapTiles, eax, ebx .IF hWin != NULL ;little trick to simplify initial random data invoke RedrawWindow, hWin, NULL, NULL, RDW_INVALIDATE invoke ProcessPossibleWin, hWin .ENDIF pass: ret ProcessArrow endp
Для перемещения тайла от кликов мышью нужно понять по какому тайлу кликнули и проверить, можно ли его перемещать. Для этого в цикле (двойной цикл организован через директиву MASM .WHILE) вызываем CalculateTileRect и проверяем находится ли курсор мыши внутри прямоугольника. Принцип проверки тот же, что и в ProcessArrow - cmp в ряд, только команды условного перехода другие. Внутри ProcessArrow je (jump equal), а тут ja jb (jump above jump below). Дальше все тоже самое что и с курсором, только наоборот. Смотрим разницу между индексами пустого и кликнутого тайла и вызываем процедуру ProcessArrow (наверное не самое удачное название) с нужными аргументами. Сокращенная версия процедуры.
ProcessClick proc hWin:DWORD, lParam:DWORD local rct : RECT movsx ebx, WORD PTR [ebp+12] ; x coordinate movsx ecx, WORD PTR [ebp+14] ; y coordinate mov vert, 0 .WHILE vert < 4 mov hor, 0 .WHILE hor < 4 invoke CalculateTileRect, ADDR Rct, hor, vert cmp ebx, Rct.left jb next cmp ebx, Rct.right ja next cmp ecx, Rct.top jb next cmp ecx, Rct.bottom ja next ; the idea is that tile can be moved only if there is a particular diff ; between its index and empty tile index ; -1, +1 ,-4, +4 for different directions, similar to ProcessArrow proc call FindEmptyTileIndex .IF index >al sub index, al .IF index == 1 invoke ProcessArrow, hWin, VK_LEFT .ELSEIF index == 4 invoke ProcessArrow, hWin, VK_UP .ENDIF .ENDIF next: inc hor .ENDW inc vert .ENDW ret ProcessClick endp
Вспомогательные процедуры типа проверки на окончание игры, или смены местами значений в массиве я приводить не буду, т.к. они банальны, а статья и так разрослась. Теперь, когда все готово, в итоге получилось 587 строк в Main.asm и 8192 байта исполняемый файл. Размер екзешника меня приятно порадовал - 8 килобайт это и для прежних времен немного, а сейчас и подавно. Полный код приложения можно увидеть в гитхабе.

Заключение
Наша игра готова. Мы увидели как это делается в привычной IDE, узнали откуда брать сигнатуры и как вызывать функции WinAPI, поняли что надо сделать чтобы создать полноценное оконное приложение, использовали директивы MASM для упрощения кода. Хоть я никогда и не использовал ассемблер в коммерческой разработке, но интерес к нему был с юных лет. Начиная с изучения ассемблера для Z80, знаменитого Спектрума и его многочисленных клонов. Писать пусть и очень простую, но полноценную игру на ассемблере мне по-настоящему понравилось. Надеюсь читателям тоже было интересно!
Быстрый поиск ошибок

Мгновенно просматривайте информацию о ваших переменных в редакторе во время отладки. Используйте подсказки по данным, чтобы увидеть имя и текущее значение переменной, развернуть объект и просмотреть его элементы, а также изменить значение переменной.
Любая платформа или место
В локальной, удаленной и рабочей среде.
Где бы ни запускался ваш код, Visual Studio может выполнить его отладку — от запуска приложения для Windows на рабочем столе или в эмуляторе Android до подключения удаленного экземпляра Azure, устройства iOS или игровой приставки, а также любого браузера.
Вы можете выполнять отладку проблем вне сети в рабочей среде, используя такие возможности, как IntelliTrace и глубокий анализ файлов дампа.

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

Гибкая проверка состояния
Это же изображение только в темной теме
Как только вы приостановите выполнение приложения где захотите, Visual Studio предложит множество способов проверить значения переменных для формулирования или проверки гипотезы.
Отслеживайте значение при пошаговом выполнении кода, быстро просматривайте локальные переменные и оценивайте сложные выражения — и все это не выходя из отладчика.Вы даже можете подать интерактивный запрос глубоко в структуру данных.

- Исключения — это хорошо
- Поток стал проще
- На низком уровне…
Получайте оповещения, когда что-то идет не так
Дефекты кода и неожиданные ситуации проявляют себя в виде исключений. Исключения приведут к сбою вашего приложения или просто станут сложными для отслеживания ошибками.
При отладке в Visual Studio вы можете настроить уведомления, которые будут поступать при возникновении исключений, и даже выбрать специальные исключения, которые интересуют вас больше всего. Вы немедленно получаете предупреждение с внутренней информацией об исключении и анализом нулевых ссылок прямо оттуда, так же просто, как если бы вы наткнулись на точку останова.

Упрощение контроля и проверки сложного многопоточного кода
Отладка многопоточного кода — это непростая задача. Visual Studio позволяет управлять выполнением нескольких потоков одновременно и проверять состояние во многих потоках, чтобы получить общую картину.
Просматривайте стеки вызовов всех потоков в одном графическом представлении и оценивайте выражения в нескольких потоках, чтобы сравнить значения. Легко просматривайте информацию о потоках и задачах, помечайте их и замораживайте. Быстро переключайтесь между контекстами выполнения и используйте маркеры потоков, чтобы увидеть, какие строки кода выполняются вашими потоками, в любой момент.

Раскопайте настолько глубоко, насколько этого требует ваш код
Иногда, особенно для кода C++ системного уровня, необходимо подобраться ближе к аппаратному уровню, чтобы диагностировать труднообнаруживаемые ошибки. Visual Studio поддерживает это с помощью окна «Память, регистры и дизассемблирование».