Программирование на nasm windows

Последнее обновление: 12.10.2023

NASM является кроссплатформенным ассемблером, который доступен в том числе и на Windows. Рассмотрим, как использовать NASM на Windows.

Установка

Для работы с NASM нам надо установить непосредственно сам ассемблер. Для этого на официальном сайте перейдем на страницу https://www.nasm.us/pub/nasm/releasebuilds/2.16.01/win64/,
где находятся файлы ассемблера NASM версии 2.16.01 для 64-разрядной версии Windows:

Установка NASM на Windows

Здесь нам доступен ассемблер в виде двух пакетов. Один пакет установщика (nasm-2.16.01-installer-x64.exe), а второй — архив (nasm-2.16.01-win64.zip).
Загрузим zip-архив.
. Например, загрузим zip-архив и после загрузки распакуем его. В папке распакованного архива мы можем найти два файла

ассемблер NASM на Windows

Это прежде всего сам ассемблер — файл nasm.exe и дизассемблер — файл ndisasm.exe

Чтобы не прописывать весь путь к ассемблеру, занесем его в переменные среды. Для этого можно в окне поиска в Windows ввести «изменение переменных среды текущего пользователя»:

изменение переменных среды текущего пользователя в Windows

Нам откроется окно Переменныех среды:

Добавление GCC в переменные среды на Windows

И добавим путь к ассемблеру. Например, в моем случае архив ассемблера распакован в папку C:\nasm-2.16.01, соответственно я указываю
в переменной Path среды эту папку:

ассемблер NASM на Windows

Если все настроено правильно, то мы можем запустить командную строку и с помощью команды nasm -v узнать текущую версию ассемблера:

C:\Users\eugen>nasm -v
NASM version 2.16.01 compiled on Dec 21 2022

C:\Users\eugen>

Начало работы с NASM

Определим в файловой системе каталог для файлов с исходным кодом и создадим в нем следующий файл hello.asm:

global _start       ; делаем метку метку _start видимой извне

section .text       ; объявление секции кода
_start:             ; метка _start - точка входа в программу
    mov rax, 22     ; произвольный код возврата - 22 
    ret             ; выход из программы

Рассмотрим поэтапно данный код. Вначале идет директива global:

global _start

Данная директива делает видимой извне определенную метку программы. В данном случае метку _start, которая является точкой входа в программу. Благодаря этому компоновщик при компоновке программы в исполняемый файл сможет увидеть данную метку.

Затем идет секция кода программы, которая и определяет выполняемые программой действия. Для определения секции применяется директива
section, после которой указывается имя секции. Причем секция кода программы должна называться .text.

section .text 

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

Метка _start выступает в качестве точки входа в программу. Название подобной метки произвольное, но обычно это или _start или _main

Наша программа не производит какой-то феноменальной работы. Все что она делает — это помещает в регистр rax число 22 и завершается. Для помещения числа в регистр применяется инструкция
mov:

mov rax, 22

Инструкция mov помещает в первый операнд — регистр rax значение из второго операнда — число 22.

Затем идет вызов инструкции ret, которая завершает программу

ret

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

global _start  ; делаем метку метку _start видимой извне - это текст комментария

Компиляция

Для компиляции откроем командную строку, перейдем в ней к папке с исходным кодом (где располагается файл hello.asm) и выполним следующую команду

nasm -f win64 hello.asm -o hello.o

Здесь с помощью опции -f указывается формат файла, в который мы хотим скомпилировать код.
Для 64-разрядной ОС Windows это — win64. После этого указывается файл, который мы хотим скомпилировать — наш файл hello.asm.
Затем опция -o hello.o указывает на имя скомпилированного файла. В результате выполнения этой команды будет создан объектный файл hello.o

Однако файл hello.o — это объектный файл, а не исполняемый. Он содержит машинный код, который понимает компьютер, но чтобы его можно было запускать как обычную
программу, его надо скомпоновать в исполняемый файл. И для этого нужна программа компоновщика (он же линковщик/линкер или linker).
Недостатком NASM является то, что он не предоставляет встроенного компоновщика. И нам надо использовать внешнюю программу компоновки. Где ее взять?
Далее я рассмотрю два варианта — использование компоновщика из пакета GCC и использование компоновщика компилятора Visual C/C++, который идет вместе с Visual Studio. Оба варианта равноценны.

Компоновка с помощью GCC

Вначале нам надо установить пакет GCC. Пакет компиляторов GCC для Windows не имеет какого-то одного единого разработчика, разные организации могут выпускать свои сборки. Для Windows одной из наиболее популярных версий GCC является пакет средств для разработки от
некоммерческого проекта MSYS2. Следует отметить, что для MSYS2 требуется 64-битная версия Windows 7 и выше (то есть Vista, XP и более ранние версии не подходят)

Итак, загрузим программу установки MSYS2 с официального сайта MSYS2:

Установка MSYS для разработки под С

После загрузки запустим программу установки:

Установка пакета mingw-w64 и msys2 на Windows

На первом шаге установки будет предложено установить каталог для установки. По умолчанию это каталог C:\msys64:

Установка компиляторов Си MSYS2 на Windows

Оставим каталог установки по умолчанию (при желании можно изменить). На следующем шаге устанавливаются настройки для ярлыка для меню Пуск, и затем собственно будет произведена установка.
После завершения установки нам отобразить финальное окно, в котором нажмем на кнопку Завершить

Установка компиляторов MSYS2 на Windows

После завершения установки запустится консольное приложение MSYS2.exe. Если по каким-то причинам оно не запустилось,
то в папке установки C:/msys64 надо найти файл usrt_64.exe:

компиляторы MSYS2.exe на Windows

Теперь нам надо установить собственно набор компиляторов GCC. Для этого введем в этом приложении следующую команду:

pacman -S mingw-w64-ucrt-x86_64-gcc

Для управления пакетами MSYS2 использует пакетный менеджер Packman. И данная команда говорит пакетному менелжеру packman установить пакет mingw-w64-ucrt-x86_64-gcc,
который представляет набор компиляторов GCC (название устанавливаемого пакета указывается после параметра -S).

Установка компиляторов MSYS2 на Windows

Если после завершения установки мы откроем каталог установки и зайдем в нем в папку C:\msys64\ucrt64\bin,
то найдем там файл компоновщика ld

GNU ассемблер на Windows

Для упрощения запуска компоновщика мы можем добавить путь к нему в Переменные среды:

Определение пути к компилятору GCC в переменных среды на Windows

Чтобы убедиться, что нам доступен компоновщик GCC — программа ld, введем следующую команду:

ld --version

В этом случае нам должна отобразиться версия компоновщика

c:\asm>ld --version
GNU ld (GNU Binutils) 2.40
Copyright (C) 2023 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or (at your option) a later version.
This program has absolutely no warranty.

c:\asm>

Теперь скомпонуем файл hello.o в исполняемый файл hello.exe с помощью следующей команды:

ld hello.o -o hello.exe

Мы можем запустить этот файл, введя в консоли его название и нажав на Enter. Но наша программа ничего не выводит на консоль, поэтому после запуска программы мы ничего не увидим. Тем не менее
наша программа устанавливает регистр RAX. А значение этого регистра при завершении программы рассматривается в Windows как статусный код возврата, который сигнализирует, как завершилась программа (успешно или не успешно).
И мы можем этот статусный код получить, если после выполнения программы введем команду

echo %ERRORLEVEL%

И нам должно отобразится число 22 — значение регистра RAX. Полный консольный вывод:

c:\asm>nasm -f win64 hello.asm -o hello.o

c:\asm>ld hello.o -o hello.exe

c:\asm>hello.exe

c:\asm>echo %ERRORLEVEL%
22

c:\asm>

Установка компоновщика link.exe

Компоновщик от GCC — не единственный компоновщик, который можно использовать для компоновки программы на Windows. Еще один вариант представляет компоновщик
link.exe из пакета инструментов разработки для C/C++ для Visual Studio. Условным плюсом этого компоновщика может быть то, что его
разработчик — Microsoft, поэтому можно ожидать более лучшей интеграции с Windows. Поэтому также рассмотрим и этот способ.

Сперва нам надо установить для Visual Studio инструменты разработки для C/C++. Установщик для среды Visual Studio можно загрузить по следующему адресу:
Microsoft Visual Studio 2022. После загрузки программы установщика Visual Studio запустим ее и в окне устанавливаемых
опций выберем пункт Разработка классических приложений на C++:

Установка MASM 64 в Windows

И нажмем на кнопку установки.

В зависимости от конкретной подверсии и номера сборки Visual Studio точное расположение файлов может варьироваться. Например, в моем случае это папка
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.37.32822\bin\Hostx64\x64\. И в этой папке можно найти программу компоновщика link.exe.
Причем при обновлениях Visual Studio этот расположение может измениться, так как при обновлении меняется и версия Visual Studio. Поэтому к конкретным путям можно не цепляться. Вместо этого
мы можем перейти к меню Пуск и в списке программ найти пункт Visual Studio и подпункт
x64 Native Tools Command Prompt for VS 2022

Build Tools for Visual Studio 2022 и MASM64 в Windows

Нам должна открыться консоль. Введем в нее link, и нам отобразится версия ассемблера и некоторая дополнительная информация:

**********************************************************************
** Visual Studio 2022 Developer Command Prompt v17.7.4
** Copyright (c) 2022 Microsoft Corporation
**********************************************************************
[vcvarsall.bat] Environment initialized for: 'x64'

C:\Program Files\Microsoft Visual Studio\2022\Community>link
Microsoft (R) Incremental Linker Version 14.37.32824.0
Copyright (C) Microsoft Corporation.  All rights reserved.

 usage: LINK [options] [files] [@commandfile]

   options:

      /ALIGN:#
      /ALLOWBIND[:NO]
      /ALLOWISOLATION[:NO]
      /APPCONTAINER[:NO]
      /ASSEMBLYDEBUG[:DISABLE]
      /ASSEMBLYLINKRESOURCE:filename
      /ASSEMBLYMODULE:filename
      /ASSEMBLYRESOURCE:filename[,[name][,PRIVATE]]
      /BASE:{address[,size]|@filename,key}
      /CLRIMAGETYPE:{IJW|PURE|SAFE|SAFE32BITPREFERRED}
      /CLRLOADEROPTIMIZATION:{MD|MDH|NONE|SD}
      /CLRSUPPORTLASTERROR[:{NO|SYSTEMDLL}]
      /CLRTHREADATTRIBUTE:{MTA|NONE|STA}
      /CLRUNMANAGEDCODECHECK[:NO]
      /DEBUG[:{FASTLINK|FULL|NONE}]
    ............................

В частности, можно увидеть, что версия компоновщика — 14.37.32824.0 и все опции, которые можно передать программе при компоновке. Стоит отметить, что запуск этой этой утилиты фактически представляет запуск файла C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat —
он по сути вызывает другой файл — vcvarsall.bat, который собственно и настраивает окружение для выполнения ассемблера.

Используем этот компоновщик. Для этого откроем программу x64 Native Tools Command Prompt for VS 2022 и перейдем в ней к папке, где располагается объектный файл
hello.o. Затем выполним следующую команду

link hello.o /entry:_start /subsystem:console /out:hello2.exe

В данном случае компоновщику передаем ряд параметров:

  • собственно объектный файл hello.o, который будет компилироваться в исполняемое приложение

  • Параметр /entry:_start указывает компоновщику на точку входа в программу —
    это наша метка «_start».

  • Параметр /subsystem:console указывает компоновщику, что создается консольное (не графическое) приложение.

  • Параметр /out:hello2.exe устанавливает имя генерируемого файла приложение — оно будет называться «hello2.exe».

В результате будет создан файл hello2.exe, который мы также можем запускать:

c:\asm>link hello.o /entry:_start /subsystem:console /out:hello2.exe
Microsoft (R) Incremental Linker Version 14.37.32824.0
Copyright (C) Microsoft Corporation.  All rights reserved.


c:\asm>hello2.exe

c:\asm>echo %ERRORLEVEL%
22

c:\asm>

Создание первой программы на Windows

Теперь создадим более осмысленную программу, которая выводит на экран строку,kernel32.lib и для этого изменим файл hello.asm следующим образом:

global _start       ; делаем метку метку _start видимой извне

extern WriteFile        ; подключем функцию WriteFile
extern GetStdHandle     ; подключем функцию GetStdHandle

section .data   ; секция данных
message: db "Hello METANIT.COM!",10  ; строка для вывода на консоль

section .text       ; объявление секции кода
_start:             ; метка _start - точка входа в программу
    sub  rsp, 40   ; Для параметров функций WriteFile и GetStdHandle резервируем 40 байт (5 параметров по 8 байт)
    mov  rcx, -11  ; Аргумент для GetStdHandle - STD_OUTPUT
    call GetStdHandle ; вызываем функцию GetStdHandle
    mov  rcx, rax     ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консоли
    mov  rdx, message    ; Второй параметр WriteFile - загружаем указатель на строку в регистр RDX
    mov  r8d, 18      ; Третий параметр WriteFile - длина строки для записи в регистре R8D 
    xor  r9, r9       ; Четвертый параметр WriteFile - адрес для получения записанных байтов
    mov  qword [rsp + 32], 0  ; Пятый параметр WriteFile
    call WriteFile ; вызываем функцию WriteFile
    add  rsp, 40
    ret             ; выход из программы

Разберем эту программу. Для вывода строки на консоль нам надо использовать нативные функции GetStdHandle и WriteFile.
И чтобы воспользоваться этими функциями подключаем их с помощью директивы extern

extern WriteFile
extern GetStdHandle

Далее идет определение секции данных — секции .data:

section .data   ; секция данных
message: db "Hello METANIT.COM!",10  ; строка для вывода на консоль

В секции .data определена метка message, на которую проецируется строка. По сути message — это переменная, которая представляет строку.
После метки указывается тип данных. Строка в ассемблере — это просто набор байтов, поэтому имеет тип db. Затем в кавычках определяется собственно выводимая строка — "Hello METANIT.COM!",10. Обратите внимание на 10 после строки — это код символа перевода строки. То есть при выводе мы ожидаем, что будет происходить перевод на другую строку.

Затем идет секция кода — секция .text и метка _start — точка входа в программу

section .text       ; объявление секции кода
_start:             ; метка _start - точка входа в программу

В программе для вызова функций первым делом необходимо настроить стек. В частности, резервируем в стеке 40 байт для параметров функций GetStdHandle и WriteFile и при этом учитываем выравнивание
стека по 16-байтной границе. Указатель на верхушку стека хранится в регистре
rsp. Поэтому вычитаем с помощью инструкции sub из значения в регистре rsp 40 байт

sub rsp, 40

Почему 40? Прежде всего при вызове функций WinAPI (как в данном случае функций GetStdHandle и WriteFile) необходимо зарезервировать в стеке как минимум 32 байта — так называемое
«shadow storage» (теневое хранилище). Далее нам надо учитывать количество параметров функции. Пеовые 4 параметра функций передаются через регистры, а параметры начиная с 5-го передаются через стек.
Соответственно для 5-го и последующих параметров надо выделить в стеке область. Для каждого параметра вне зависимости от его размера выделяется 8 байт. Функция WriteFile как раз принимает 5 параметров, поэтому для нее надо выделить дополнительные 8 байт в стеке.
Поэтому получаем 32 байта + 8 байт (5-й параметр WriteLine) = 40 байт. Количество параметров смотрим по функции с наибольшим количеством параметров. Третий момент — нам надо учитывать, что перед вызовом функций WinAPI стек имел выравнивание по 16 байтовой границе, то есть значение в RSP
должно быть кратно 16. По умолчанию при вызове функции в стек помещается адрес возврата функии размером 8 байт. Поэтому наши 40 байт + 8 байт (адрес возврата из функции) дадут 48 байт — число кратное 16.

Вначале нам надо использовать встроенную функцию GetStdHandle(), которая позволяет получить дескриптор на устройство ввода-вывода. Она имеет следующее определение на C:

HANDLE WINAPI GetStdHandle(
  _In_ DWORD nStdHandle
);

Функция GetStdHandle() получает числовой код устройства, с которым мы хотим взаимодействовать. В нашем случае нам надо получить устройство стандартного вывода (для вывода строки), которым по умолчанию является консоль. Для обращения к консоли надо передать число -11, которое надо поместить в
регистр rcx:

 mov  rcx, -11  ; Аргумент для GetStdHandle - STD_OUTPUT

После установки параметра этой функции вызываем ее с помощью инструкции call:

call GetStdHandle

В результате выполнения функция GetStdHandle возвращает дескриптор — объект, через который мы можем взаимодействовать с консолью. Этот дескриптор помещается в регистр
rax. Получив этот дескриптор, используем его для вывода на консоль строки с помощью функции WriteFile. Для справки ее определение на С++

BOOL WriteFile(
  [in]                HANDLE       hFile,
  [in]                LPCVOID      lpBuffer,
  [in]                DWORD        nNumberOfBytesToWrite,
  [out, optional]     LPDWORD      lpNumberOfBytesWritten,
  [in, out, optional] LPOVERLAPPED lpOverlapped
);

Вызов функции GetStdHandle помещает в регистр rax дескриптор консоли. И для вывода строкии на консоль с помощью функции WriteFile нам надо поместить
этот дескриптор в регистр rcx

mov rcx, rax

Затем также с помощью инструкции mov загружаем в регистр rdx адрес выводимой строки

mov rdx, message

Далее в регистр r8d помещаем длину выводимой строки в байтах — в данном случае это 18 байт:

mov  r8d, 18

Поскольку у нас строка с символами ASCII, и каждый символ эквивалентен 1 байту, то получаем, что в строке message с учетом последнего символа с числовым кодом 10 будет 18 байт.

Затем в регистре r9 устанавливаем адрес четвертого параметра функции WriteFile:

xor  r9, r9

В данном случае нам не нужно количество считанных байтов, и с помощью инструкции xor обнуляем значение регистра r9.

Последний — пятый параметр функции WriteFile должен иметь значение NULL, по сути 0. Поэтому устанавливаем для него значение 0, смещаясь в стеке вперед на 32 байта (4 параметра * 8):

mov qword [rsp + 32], 0

Инструкция mov помещает значение в определенное место. Здесь в качестве значения служит число 0. А место определяется выражением
qword [rsp + 32]. qword указывает, что этот операнд описывает адрес размером в четыре слова, что означает 8 байтов (слово имеет длину 2 байта). То есть число 0 представляет 8-байтное значение и помещается по адресу rsp + 32.

И далее собственно вызываем функцию WriteFile:

call WriteFile

Этот вызов должен привести к выводу строки на консоль. После этого восстанавливаем значение верхушки стека. Для этого с помощью инструкции add прибавляем
к значению в регстре rsp ранее отнятые 40 байт:

add rsp, 40

И с помощью инструкции ret выходим из программы.

Компиляция

Поскольку теперь программа использует внешние функции WinApi — GetStdHandle и WriteFile, которые определены во внешней библиотеке kernel32.lib, то при компоновке
нам надо подключить эту библиотеку. В зависимости от используемого компоновщика/линкера этот процесс может немного отличаться. Например, при использовании компоновщика ld
из комплекта инструментов GCC все подключаемые библиотеки передаются с помощью опции -l:

ld hello.o -o hello.exe -l kernel32

Здесь последний параметр -l kernel32 как раз указывает, какую библиотеку надо подлючить, при чем название библиотеки указывается без расширения файла.

При использовании компоновщика link.exe от Microsoft подключаемая библиотека просто передается вместе с компонуемыми файлами:

link hello.o kernel32.lib /entry:_start /subsystem:console /out:hello2.exe

Итак, повторно скомпилируем файл и скомпонуем одним из компоновщиков. Затем запустим полученный исполняемый файл, и консоль должна вывести нам строку.
Полный процесс при использовании компоновщика ld из комплекта GCC:

c:\asm>nasm -f win64 hello.asm -o hello.o

c:\asm>ld hello.o -o hello.exe -l kernel32

c:\asm>hello.exe
Hello METANIT.COM!
c:\asm>

Полный процесс при использовании компоновщика link от Microsoft (компоновка выполняется в программе x64 Native Tools Command Prompt for VS 2022):

c:\asm>nasm -f win64 hello.asm -o hello.o

c:\asm>link hello.o kernel32.lib /entry:_start /subsystem:console /out:hello2.exe
Microsoft (R) Incremental Linker Version 14.37.32824.0
Copyright (C) Microsoft Corporation.  All rights reserved.


c:\asm>hello2.exe
Hello METANIT.COM!
c:\asm>

Introduction

Before telling you what this post is, let me tell you what this is not:

    • It is not a step-by-step guide to Assembly language programming.
    • It is not a step-by-step guide to Win32 programming.
    • It is not a replacement for the official NASM documentation.
    • It is not a replacement for the official GCC documentation.

Having said that, let me tell you what this post is: this post is a collection of my experiences, the sum total of my mistakes, hard earned, while trying to program in assembly, C and Win32. Hopefully, someone will find it useful.

In order to understand and gain the maximum use out of this series, you will need:

  1. Good experience in C programming
  2. Some experience in calling Win32 API (from within C/C++ or other language)
  3. Some exposure to assembly programming, hopefully in NASM

Assembly programming is hard. Win32 programming is hard. Mixing them both is harder. Add to that the 32-bit/64-bit mix-ups and it’s going to be a veritable nightmare. That is why this blog exists. It will outline where I’ve gone wrong, and why I’ve gone wrong. But most importantly, it will log how I’ve corrected those mistakes.

On we go, then.

Getting the Tools

Editors

I like to use Notepad++ as my primary editor, but when I’m doing multi-file Windows programming it makes much sense to use DevCPP. Keep in mind that DevCPP uses MinGW port of GCC under the hood. You might need that fact later.

Assembler

There are a good lot of assemblers out there. I was strongly tempted to try Microsoft Macro Assembler (MASM), but in the end I decided against it. After all, if I wanted it easy, I’d have gone for C#.NET: the whole purpose of this excursion was to take a deep dive. Reminding myself of that, I chose to go with Netwide Assembler. In all programs in this blog, I will be using version 2.11.05, but you’re free to download the latest version here.

Although I am not a big fan of the argument “Command line tools build character”, I decided not to go with IDE tools for assembly. However, there are good tools out there, and I can mention at least one here: SASM. You can download for Windows here.

C Compiler

GCC or MinGW? You can read all about the difference between GNU, GCC and MinGW in this excellent stackoverflow question (and this as well).

I decided to go with gcc, simply for the sake of trust on the community. I use version 5.1.0 in this document.

Linker

Since I chose the GCC suite, I already get ld for free. But if you want an alternative, I can suggest ALINK.

“Hello, World”–from NASM, the Wrong Way

I’m sure most of you have seen some version of the following code as the “first Hello, World” program.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
section .text
global _start
_start: 
mov ecx,msg 
mov ebx,1 
mov eax,4 
int 0x80 

mov eax,1 
int 0x80 

section .data

msg db 'Hello, Windows!',0xa 
len equ $ - msg 

This will work fine in Linux and pretty much any *nix. But, the sad news is that this won’t work under Windows at all! Why? Because you’re calling the Linux interrupt code (syscall) 0x80, which doesn’t exist in Windows. In Windows (or rather, DOS), the correct interrupt function would be 0x21 (or 21h), but that would mean you’re forced to write in 16-bit. Either way, calling the kernel directly in Windows seems to be not the way forward.

If we cannot directly call the kernel, then what options are available to us? The obvious choice is C runtime libraries. And why not the trusted printf? Let’s see that version.

“Hello, Windows”–Take Two, with printf

This time, we call printf. We’ll have a deep look at the way the arguments are passed in Part 2 of this article.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    global  _main
    extern  _printf

    section .text
_main:
    push    message
    call    _printf
    add     esp, 4
    ret
message:
    db  'Hello, World', 10, 0

You need to compile this with the following line.

C:\work>nasm -f win32 hello.asm

And then link it with gcc like so:

C:\work>gcc -m32 hello.obj -o hello.exe

Remember that both NASM and GCC succeed silently. That means, unless there’s an error, you get no output on console.

Couple of points to note here:

  1. It is important to use the flag -f win32 here. (Both -fwin32 and -f win32 will work.) Unless you do so, NASM will happily try to compile your assembly file into a binary format (*.bin), find that it has an external reference, and fail with “error: binary output does not support external references“.
  2. It is important to use the option -m32 with gcc here. (Unlike nasm, gcc will not let you put a space between -m and 32.) If you do not specify -m32, then gcc will try to build a 64-bit exe, fail, and complain that “i386 architecture of input file hello.obj is incompatible with i386:x86-64 output.” In addition, you’ll get an error saying “undefined reference to WinMain“.
  3. The -o option lets gcc know the name of the output file. Quite inconsistently, here, gcc will not mind the space between -o and hello.exe. If you don’t specify the output file name, you’ll get a file called “a.exe”.

Assuming everything went right, you should get a file called “hello.exe” in your directory, which you can execute like so:

C:\work>hello
Hello, World
C:\work>

Cheers! You just called a C library routine from assembly, and made some basic I/O work happen.

However, note that we’re still using DOS subsystem. Our aim was to program for Win32, not call a C routine. Let’s do so now.

“Hello, World”–Take Three, with _WriteFile@20

This time, we’re going to use Win32 API to directly access the console.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    global _main
    extern  _GetStdHandle@4
    extern  _WriteFile@20
    extern  _ExitProcess@4

    section .text
_main:
    ; DWORD  bytes;    
    mov     ebp, esp
    sub     esp, 4

    ; hStdOut = GetstdHandle( STD_OUTPUT_HANDLE)
    push    -11
    call    _GetStdHandle@4
    mov     ebx, eax    

    ; WriteFile( hstdOut, message, length(message), &bytes, 0);
    push    0
    lea     eax, [ebp-4]
    push    eax
    push    (message_end - message)
    push    message
    push    ebx
    call    _WriteFile@20

    ; ExitProcess(0)
    push    0
    call    _ExitProcess@4

    ; never here
    hlt
message:
    db      'Hello, World', 10
message_end:

Like in the previous examples, you have to assemble, link and run. However, let me introduce another way to do all 3 in one line:

1
C:\work>nasm -fwin32 hellow.asm && gcc -m32 hellow.obj -o hellow.exe && hellow

If all went well, you should see something like this:

1
2
3
C:\work>nasm -fwin32 hellow.asm && gcc -m32 hellow.obj -o hellow.exe && hellow
Hello, World
C:\work>

Again, couple of important points:

  1. Where is GetStdHandle declared in? The answer is, Kernel32.dll. The immediate next question is, how did gcc know to link with Kernel32.lib? Aren’t we supposed to get an error like this? The short answer is,  because we specified the -m32 flag.
  2. Why the funny names, like _GetStdHandle@4? The answer has to do something with the way method names are mangled or decorated in Win32. That’s juicy material for a next article. For now, remember that the calling convention for Win32 is known as __stdcall, which defines the way functions are made available to public after compilation. Specifically, the MSDN article says, that under __stdcall, “an underscore (_) is prefixed to the name. The name is followed by the at sign (@) followed by the number of bytes (in decimal) in the argument list.
  3. Why the constant (-11)? That’s how we tell _GetStdHandle@4 to get us the “Standard Output” (which in this case, is the screen). Consider this the equivalent way of grabbing a handle to stdout in C, and cout in C++.

Again, knowing how a Win32 function is mangled, or decorated after compiling seems like far too much to expect at this level. After all, all C programmers get to happily write “ExitProcess” instead of “_ExitProcess@4”.

In the next step, we will look at how this can be done.

“Hello, World”–Take Four, with WriteFile

I would like to warn you beforehand: we’ll run into a (rather annoying) known issue with NASM here. This will force us to so something out of the ordinary. Also, this will force us to use ALINK instead of gcc/ld as our linker.

Here’s the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[BITS 32]

extern ExitProcess
import ExitProcess kernel32.dll

extern  GetStdHandle
import GetStdHandle kernel32.dll

extern  WriteFile
import WriteFile kernel32.dll

segment .data use32
Text db "Hello, World",0

segment .code use32
..start:
    push    -11
    call    [GetStdHandle]
    mov     ebx, eax  
    
    ; WriteFile( hstdOut, message, length(message), &bytes, 0);
    push    dword 0
    lea     eax, [ebp-4]
    push    eax
    push    (message_end - message)
    push    message
    push    ebx
    call    [WriteFile]

    ; ExitProcess(0)
    push    dword 0
    call    [ExitProcess]

    ; never here
    hlt
message:
    db      'Hello, World', 10
message_end:

Couple of things to note:

  1. Note that this is identical to the previous version, Take #3, except for this section:
    1
    2
    3
    4
    5
    6
    7
    8
    extern ExitProcess
    import ExitProcess kernel32.dll
    
    extern  GetStdHandle
    import GetStdHandle kernel32.dll
    
    extern  WriteFile
    import WriteFile kernel32.dll
    

    What this does is to import function names directly as defined in the DLLs, thereby freeing us from having to mangle the function names ourselves.

  2. Note that we’re using the function names within square brackets, like to: [ExitProcess]

However, if you try to assemble this with NASM in the usual way, using -fwin32, NASM will throw an error:

C:\work>nasm -fwin32 hellow2.asm
hellow2.asm:4: error: parser: instruction expected
hellow2.asm:7: error: symbol `import' redefined
hellow2.asm:7: error: parser: instruction expected
hellow2.asm:10: error: symbol `import' redefined
hellow2.asm:10: error: parser: instruction expected

Unfortunately, there is nothing we can do about this, so we will use a workaround. We are going to use -fobj instead of -fwin32. At least this will give us an obj file.

C:\work>nasm -fobj hellow2.asm

Now, if we use our usual way of gcc to link this, we will get an error.

C:\work>gcc -m32 hellow2.obj -o hellow2.exe
hellow2.obj: file not recognized: File format not recognized
collect2.exe: error: ld returned 1 exit status

This means that ld (which is the linker under the hood of gcc) did not like our obj format. And it’s right. We did indeed supply a wrong file format. What we should now do is to find a less restricting linker that will overlook this fact. Enter alink.

C:\work>alink -subsys console -oPE hellow2.obj
ALINK v1.6 (C) Copyright 1998-9 Anthony A.J. Williams.
All Rights Reserved

Loading file hellow2.obj
matched Externs
matched ComDefs
Generating PE file hellow2.exe

C:\work>

Here, -subsys can have two values: console and gui. Since this is a console application, we will go with -subsys console. Also, similar to -m32 in gcc, we will need to specify that we want a Win32 PE format executable file: hence the flag -oPE.

If all went well, you should get an output like so:

C:\work>hellow2
Hello, World
C:\work>

Good!

Now, we will finally look at how to go to the other mode, the GUI mode. The simplest way of displaying text output in Win32 is MessageBox function, which comes in 2 flavors: MessageBoxA for ANSI strings and MessageBoxW for UNICODE strings. We will go with ANSI version for now.

“Hello, World”–Take Five, with WriteConsoleA

In the previous example, we used the function WriteFile as the method to write to the console. Technically, the console is a logical file, so there is nothing wrong with that. However, there is a dedicated routine to write text to the console, known as WriteConsoleA. Let us use that now.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[BITS 32] 

extern ExitProcess 
import ExitProcess kernel32.dll 

extern GetStdHandle 
import GetStdHandle kernel32.dll 

extern WriteConsoleA 
import WriteConsoleA kernel32.dll 

segment .data use32 
msg db "Hello, World",0 
written dword   ?

segment .code use32 
..start: 
push -11
call [GetStdHandle] 
mov ebx, eax

push 0
lea eax, written
push eax
push 13
push offset msg
push ebx
call [WriteConsoleA]

push 0
call [ExitProcess]

“Hello, World”–Take Six, with MessageBoxA

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[BITS 32]

extern ExitProcess
import ExitProcess kernel32.dll
extern MessageBoxA
import MessageBoxA user32.dll

segment .data use32
Caption db 'From Assembly',0
Text db "Hello, World",0

segment .code use32
..start:
push dword 0
push dword Caption
push dword Text
push dword 0
call [MessageBoxA]

push dword 0
call [ExitProcess]

We assemble, link and execute in one step, like so:

I:\Work\Shellcode\Samples\0>nasm -fobj hellowin.asm && alink -subsys gui -oPE he
llowin.obj && hellowin
ALINK v1.6 (C) Copyright 1998-9 Anthony A.J. Williams.
All Rights Reserved

Loading file hellowin.obj
matched Externs
matched ComDefs
Generating PE file hellowin.exe

I:\Work\Shellcode\Samples\0>

Note the flag -subsys -gui to alink. There will be no console output, but you should see a familiar message box popping up.

This concludes the first part. In the next part, we’ll take a deeper look at interfacing with C library functions, with special attention to what are known as calling conventions.

NASM Tutorial

Yep, it’s a tutorial.

Scope of the Tutorial

This tutorial will show you how to write assembly language programs on the x86-64 architecture.

You will write both (1) standalone programs and (2) programs that integrate with C.

Don’t worry, we won’t get too fancy.

Your First Program

Before learning any details, let’s make sure you can type in and run programs.

Make sure both nasm and gcc are installed. Save one of the following programs as hello.asm, depending on your machine platform. Then run the program according to the given instructions.

If you are on a Linux-based OS:

hello.asm

; ----------------------------------------------------------------------------------------
; Writes "Hello, World" to the console using only system calls. Runs on 64-bit Linux only.
; To assemble and run:
;
;     nasm -felf64 hello.asm && ld hello.o && ./a.out
; ----------------------------------------------------------------------------------------

          global    _start

          section   .text
_start:   mov       rax, 1                  ; system call for write
          mov       rdi, 1                  ; file handle 1 is stdout
          mov       rsi, message            ; address of string to output
          mov       rdx, 13                 ; number of bytes
          syscall                           ; invoke operating system to do the write
          mov       rax, 60                 ; system call for exit
          xor       rdi, rdi                ; exit code 0
          syscall                           ; invoke operating system to exit

          section   .data
message:  db        "Hello, World", 10      ; note the newline at the end
$ nasm -felf64 hello.asm && ld hello.o && ./a.out
Hello, World

If you are on macOS:

hello.asm

; ----------------------------------------------------------------------------------------
; Writes "Hello, World" to the console using only system calls. Runs on 64-bit macOS only.
; To assemble and run:
;
;     nasm -fmacho64 hello.asm && ld hello.o && ./a.out
; ----------------------------------------------------------------------------------------

          global    start

          section   .text
start:    mov       rax, 0x02000004         ; system call for write
          mov       rdi, 1                  ; file handle 1 is stdout
          mov       rsi, message            ; address of string to output
          mov       rdx, 13                 ; number of bytes
          syscall                           ; invoke operating system to do the write
          mov       rax, 0x02000001         ; system call for exit
          xor       rdi, rdi                ; exit code 0
          syscall                           ; invoke operating system to exit

          section   .data
message:  db        "Hello, World", 10      ; note the newline at the end
$ nasm -fmacho64 hello.asm && ld hello.o && ./a.out
Hello, World

Exercise: Identify the differences between the two programs.

Structure of a NASM Program

NASM is line-based. Most programs consist of directives followed by one or more sections. Lines can have an optional label. Most lines have an instruction followed by zero or more operands.

nasmstructure.png

Generally, you put code in a section called .text and your constant data in a section called .data.

Details

NASM is an awesome assembler, but assembly language is complex. You need more than a tutorial. You need details. Lots of details. Be ready to consult:

  • The NASM Manual, which is pretty good!
  • The Intel Processor Manuals

Your First Few Instructions

There are hundreds of instructions. You can’t learn them all at once. Just start with these:

Instruction Description
$\mathtt{mov}\;x,y$ $x \leftarrow y$
$\mathtt{and}\;x,y$ $x \leftarrow x \wedge y$
$\mathtt{or}\;x,y$ $x \leftarrow x \vee y$
$\mathtt{xor}\;x,y$ $x \leftarrow x \bigoplus y$
$\mathtt{add}\;x,y$ $x \leftarrow x + y$
$\mathtt{sub}\;x,y$ $x \leftarrow x — y$
$\mathtt{inc}\;x$ $x \leftarrow x + 1$
$\mathtt{dec}\;x$ $x \leftarrow x — 1$
$\mathtt{syscall}\;n$ Invoke operating system routine $n$
$\mathtt{db}$ A pseudo-instruction
that declares bytes that will be in memory when the program runs

The Three Kinds of Operands

It helps to know these cold. Get ready to memorize them. How can you remember things? Nicky Case can help you with this!

Register Operands

In this tutorial we only care about the integer registers, the flag register, and the xmm registers. (If you are familiar with the x86 architecture, you will know that this means we are skipping the FP, MMX, YMM, segment, control, debug, test, and protected mode registers.) Hopefully you have already been introduced to the x86-84 architecture, in which case, this is a quick review. The 16 integer registers are 64 bits wide and are called:

R0
aka
RAX

R1
aka
RCX

R2
aka
RDX

R3
aka
RBX

R4
aka
RSP

R5
aka
RBP

R6
aka
RSI

R7
aka
RDI

R8

R9

R10

R11

R12

R13

R14

R15

(Note that 8 of the registers have alternate names.) You can treat the lowest 32-bits of each register as a register itself but using these names:

R0D
aka
EAX

R1D
aka
ECX

R2D
aka
EDX

R3D
aka
EBX

R4D
aka
ESP

R5D
aka
EBP

R6D
aka
ESI

R7D
aka
EDI

R8D

R9D

R10D

R11D

R12D

R13D

R14D

R15D

You can treat the lowest 16-bits of each register as a register itself but using these names:

R0W
aka
AX

R1W
aka
CX

R2W
aka
DX

R3W
aka
BX

R4W
aka
SP

R5W
aka
BP

R6W
aka
SI

R7W
aka
DI

R8W

R9W

R10W

R11W

R12W

R13W

R14W

R15W

You can treat the lowest 8-bits of each register as a register itself but using these names:

R0B
aka
AL

R1B
aka
CL

R2B
aka
DL

R3B
aka
BL

R4B
aka
SPL

R5B
aka
BPL

R6B
aka
SIL

R7B
aka
DIL

R8B

R9B

R10B

R11B

R12B

R13B

R14B

R15B

For historical reasons, bits 15 through 8 of R0..R3 are named:

And finally, there are 16 XMM registers, each 128 bits wide, named:

XMM0

XMM1

XMM2

XMM3

XMM4

XMM5

XMM6

XMM7

XMM8

XMM9

XMM10

XMM11

XMM12

XMM13

XMM14

XMM15

Study this picture; hopefully it helps:

rdx.png

Memory Operands

These are the basic forms of addressing:

  • [ number ]
  • [ reg ]
  • [ reg + reg*scale ]      scale is 1, 2, 4, or 8 only
  • [ reg + number ]
  • [ reg + reg*scale + number ]

The number is called the displacement; the plain register is called the base; the register with the scale is called the index.

Examples:

[750]                  ; displacement only
[rbp]                  ; base register only
[rcx + rsi*4]          ; base + index * scale
[rbp + rdx]            ; scale is 1
[rbx - 8]              ; displacement is -8
[rax + rdi*8 + 500]    ; all four components
[rbx + counter]        ; uses the address of the variable 'counter' as the displacement

Immediate Operands

These can be written in many ways. Here are some examples from the official docs.

200          ; decimal
0200         ; still decimal - the leading 0 does not make it octal
0200d        ; explicitly decimal - d suffix
0d200        ; also decimal - 0d prefex
0c8h         ; hex - h suffix, but leading 0 is required because c8h looks like a var
0xc8         ; hex - the classic 0x prefix
0hc8         ; hex - for some reason NASM likes 0h
310q         ; octal - q suffix
0q310        ; octal - 0q prefix
11001000b    ; binary - b suffix
0b1100_1000  ; binary - 0b prefix, and by the way, underscores are allowed

Instructions with two memory operands are extremely rare

In fact, we’ll not see any such instruction in this tutorial. Most of the basic instructions have only
the following forms:

  • $\mathtt{add}\;reg, reg$
  • $\mathtt{add}\;reg, mem$
  • $\mathtt{add}\;reg, imm$
  • $\mathtt{add}\;mem, reg$
  • $\mathtt{add}\;mem, imm$

Defining Data and Reserving Space

These examples come from Chapter 3 of the docs. To place data in memory:

      db    0x55                ; just the byte 0x55
      db    0x55,0x56,0x57      ; three bytes in succession
      db    'a',0x55            ; character constants are OK
      db    'hello',13,10,'$'   ; so are string constants
      dw    0x1234              ; 0x34 0x12
      dw    'a'                 ; 0x61 0x00 (it's just a number)
      dw    'ab'                ; 0x61 0x62 (character constant)
      dw    'abc'               ; 0x61 0x62 0x63 0x00 (string)
      dd    0x12345678          ; 0x78 0x56 0x34 0x12
      dd    1.234567e20         ; floating-point constant
      dq    0x123456789abcdef0  ; eight byte constant
      dq    1.234567e20         ; double-precision float
      dt    1.234567e20         ; extended-precision float

There are other forms; check the NASM docs. Later.

To reserve space (without initializing), you can use the following pseudo instructions. They
should go in a section called .bss (you’ll get an error if you try to use them in
a .text section):

buffer:         resb    64              ; reserve 64 bytes
wordvar:        resw    1               ; reserve a word
realarray:      resq    10              ; array of ten reals

Another Example

Here’s a macOS program to study:

triangle.asm

; ----------------------------------------------------------------------------------------
; This is an macOS console program that writes a little triangle of asterisks to standard
; output. Runs on macOS only.
;
;     nasm -fmacho64 triangle.asm && gcc hola.o && ./a.out
; ----------------------------------------------------------------------------------------

          global    start
          section   .text
start:
          mov       rdx, output             ; rdx holds address of next byte to write
          mov       r8, 1                   ; initial line length
          mov       r9, 0                   ; number of stars written on line so far
line:
          mov       byte [rdx], '*'         ; write single star
          inc       rdx                     ; advance pointer to next cell to write
          inc       r9                      ; "count" number so far on line
          cmp       r9, r8                  ; did we reach the number of stars for this line?
          jne       line                    ; not yet, keep writing on this line
lineDone:
          mov       byte [rdx], 10          ; write a new line char
          inc       rdx                     ; and move pointer to where next char goes
          inc       r8                      ; next line will be one char longer
          mov       r9, 0                   ; reset count of stars written on this line
          cmp       r8, maxlines            ; wait, did we already finish the last line?
          jng       line                    ; if not, begin writing this line
done:
          mov       rax, 0x02000004         ; system call for write
          mov       rdi, 1                  ; file handle 1 is stdout
          mov       rsi, output             ; address of string to output
          mov       rdx, dataSize           ; number of bytes
          syscall                           ; invoke operating system to do the write
          mov       rax, 0x02000001         ; system call for exit
          xor       rdi, rdi                ; exit code 0
          syscall                           ; invoke operating system to exit

          section   .bss
maxlines  equ       8
dataSize  equ       44
output:   resb      dataSize
$ nasm -fmacho64 triangle.asm && ld triangle.o && ./a.out
*
**
***
****
*****
******
*******
********

New things in this example:

  • cmp does a comparison
  • je jumps to a label if the previous comparison was equal. We also have jne (jump if not equal), jl (jump if less), jnl (jump if not less), jg (jump if greater), jng (jump if not greater), jle (jump if less or equal), jnle (jump if not less or equal), jge (jump if greater or equal), jnge (jump if not greater or equal), and many more.
  • equ is actually not a real instruction. It simply defines an abbreviation for the assembler itself to use. (This is a profound idea.)
  • The .bss section is for writable data.

Using a C Library

Writing standalone programs with just system calls is cool, but rare. We would like to use the good stuff in the C library.

Remember how in C execution “starts” at the function main? That’s because the C library actually has the _start label inside itself! The code at _start does some initialization, then it calls main, then it does some clean up, then it issues the system call for exit. So you just have to implement main. We can do that in assembly!

If you have Linux, try this:

hola.asm

; ----------------------------------------------------------------------------------------
; Writes "Hola, mundo" to the console using a C library. Runs on Linux.
;
;     nasm -felf64 hola.asm && gcc hola.o && ./a.out
; ----------------------------------------------------------------------------------------

          global    main
          extern    puts

          section   .text
main:                                       ; This is called by the C library startup code
          mov       rdi, message            ; First integer (or pointer) argument in rdi
          call      puts                    ; puts(message)
          ret                               ; Return from main back into C library wrapper
message:
          db        "Hola, mundo", 0        ; Note strings must be terminated with 0 in C
$ nasm -felf64 hola.asm && gcc hola.o && ./a.out
Hola, mundo

Under macOS, it will look a little different:

hola.asm

; ----------------------------------------------------------------------------------------
; This is an macOS console program that writes "Hola, mundo" on one line and then exits.
; It uses puts from the C library.  To assemble and run:
;
;     nasm -fmacho64 hola.asm && gcc hola.o && ./a.out
; ----------------------------------------------------------------------------------------

          global    _main
          extern    _puts

          section   .text
_main:    push      rbx                     ; Call stack must be aligned
          lea       rdi, [rel message]      ; First argument is address of message
          call      _puts                   ; puts(message)
          pop       rbx                     ; Fix up stack before returning
          ret

          section   .data
message:  db        "Hola, mundo", 0        ; C strings need a zero byte at the end
$ nasm -fmacho64 hola.asm && gcc hola.o && ./a.out
Hola, mundo

In macOS land, C functions (or any function that is exported from one module to another, really) must be prefixed with underscores. The call stack must be aligned on a 16-byte boundary (more on this later). And when accessing named variables, a rel prefix is required.

Understanding Calling Conventions

How did we know the argument to puts was supposed to go in RDI?
Answer: there are a number of conventions that are followed regarding calls.

When writing code for 64-bit Linux that integrates with a C library, you must follow the
calling conventions explained in the
AMD64 ABI Reference.
You can also get this information from
Wikipedia.
The most important points are:

  • From left to right, pass as many parameters as will fit in registers. The order in which
    registers are allocated, are:

    • For integers and pointers, rdi, rsi, rdx,
      rcx, r8, r9.
    • For floating-point (float, double), xmm0, xmm1, xmm2,
      xmm3, xmm4, xmm5, xmm6, xmm7.
  • Additional parameters are pushed on the stack, right to left, and
    are to be removed by the caller after the call.
  • After the parameters are pushed, the call instruction is
    made, so when the called function gets control, the return
    address is at [rsp], the first memory parameter is at
    [rsp+8], etc.
  • The stack pointer rsp must be aligned to a 16-byte boundary
    before making a call
    . Fine, but the process of making a call pushes the return address (8 bytes) on
    the stack, so when a function gets control, rsp is not aligned. You have to make
    that extra space yourself, by pushing something or subtracting 8 from rsp.
  • The only registers that the called function is required to preserve (the calle-save registers)
    are: rbp, rbx, r12, r13, r14, r15.
    All others are free to be changed by the called function.
  • The callee is also supposed to save the control bits of the XMCSR and the x87 control word,
    but x87 instructions are rare in 64-bit code so you probably don’t have to worry about this.
  • Integers are returned in rax or rdx:rax, and floating point values are returned
    in xmm0 or xmm1:xmm0.

Got that? No? What’s need is more examples, and practice.

Here is a program that illustrates how registers have to be saved and restored:

fib.asm

; -----------------------------------------------------------------------------
; A 64-bit Linux application that writes the first 90 Fibonacci numbers. To
; assemble and run:
;
;     nasm -felf64 fib.asm && gcc fib.o && ./a.out
; -----------------------------------------------------------------------------

        global  main
        extern  printf

        section .text
main:
        push    rbx                     ; we have to save this since we use it

        mov     ecx, 90                 ; ecx will countdown to 0
        xor     rax, rax                ; rax will hold the current number
        xor     rbx, rbx                ; rbx will hold the next number
        inc     rbx                     ; rbx is originally 1
print:
        ; We need to call printf, but we are using rax, rbx, and rcx.  printf
        ; may destroy rax and rcx so we will save these before the call and
        ; restore them afterwards.

        push    rax                     ; caller-save register
        push    rcx                     ; caller-save register

        mov     rdi, format             ; set 1st parameter (format)
        mov     rsi, rax                ; set 2nd parameter (current_number)
        xor     rax, rax                ; because printf is varargs

        ; Stack is already aligned because we pushed three 8 byte registers
        call    printf                  ; printf(format, current_number)

        pop     rcx                     ; restore caller-save register
        pop     rax                     ; restore caller-save register

        mov     rdx, rax                ; save the current number
        mov     rax, rbx                ; next number is now current
        add     rbx, rdx                ; get the new next number
        dec     ecx                     ; count down
        jnz     print                   ; if not done counting, do some more

        pop     rbx                     ; restore rbx before returning
        ret
format:
        db  "%20ld", 10, 0
$ nasm -felf64 fib.asm && gcc fib.o && ./a.out
                   0
                   1
                   1
                   2
                   .
                   .
                   .
  679891637638612258
 1100087778366101931
 1779979416004714189

We just saw some new instructions:

Instruction Description
$\mathtt{push}\;x$ Decrement rsp by the size of the operand, then store $x$ in [rsp]
$\mathtt{pop}\;x$ Move [rsp] into $x$, then increment rsp by the size of the operand
$\mathtt{jnz}\;\mathit{label}$ If the processor’s Z (zero) flag is set, jump to the given label
$\mathtt{call}\;\mathit{label}$ Push the address of the next instruction, then jump to the label
$\mathtt{ret}$ Pop into the instruction pointer

Mixing C and Assembly Language

This program is just a simple function that takes in three
integer parameters and returns the maximum value.

maxofthree.asm

; -----------------------------------------------------------------------------
; A 64-bit function that returns the maximum value of its three 64-bit integer
; arguments.  The function has signature:
;
;   int64_t maxofthree(int64_t x, int64_t y, int64_t z)
;
; Note that the parameters have already been passed in rdi, rsi, and rdx.  We
; just have to return the value in rax.
; -----------------------------------------------------------------------------

        global  maxofthree
        section .text
maxofthree:
        mov     rax, rdi                ; result (rax) initially holds x
        cmp     rax, rsi                ; is x less than y?
        cmovl   rax, rsi                ; if so, set result to y
        cmp     rax, rdx                ; is max(x,y) less than z?
        cmovl   rax, rdx                ; if so, set result to z
        ret                             ; the max will be in rax

Here is a C program that calls the assembly language function.

callmaxofthree.c

/*
 * A small program that illustrates how to call the maxofthree function we wrote in
 * assembly language.
 */

#include <stdio.h>
#include <inttypes.h>

int64_t maxofthree(int64_t, int64_t, int64_t);

int main() {
    printf("%ld\n", maxofthree(1, -4, -7));
    printf("%ld\n", maxofthree(2, -6, 1));
    printf("%ld\n", maxofthree(2, 3, 1));
    printf("%ld\n", maxofthree(-2, 4, 3));
    printf("%ld\n", maxofthree(2, -6, 5));
    printf("%ld\n", maxofthree(2, 4, 6));
    return 0;
}
$ nasm -felf64 maxofthree.asm && gcc callmaxofthree.c maxofthree.o && ./a.out
1
2
3
4
5
6

Conditional Instructions

After an arithmetic or logic instruction, or the compare instruction, cmp,
the processor sets or clears bits in its rflags. The most interesting flags
are:

  • s (sign)
  • z (zero)
  • c (carry)
  • o (overflow)

So after doing, say, an addition instruction, we can perform a jump, move, or set,
based on the new flag settings. For example:

Instruction Description
$\mathtt{jz}\;\mathit{L}$ Jump to label $L$ if the result of the operation was zero
$\mathtt{cmovno}\;x,y$ $x \leftarrow y$ if the last operation did not overflow
$\mathtt{setc}\;x$ $x \leftarrow 1$ if the last operation had a carry, but $x \leftarrow 0$ otherwise ($x$ must be a byte-size register or memory location)

The conditional instructions have three base forms: j for conditional jump,
cmov for conditional move, and set for conditional set.
The suffix of the instruction has one of the 30 forms:
s ns z nz c nc o no p np pe po e ne l nl le nle g ng ge nge a na ae nae b nb
be nbe
.

Command Line Arguments

You know that in C, main is just a plain old function, and it has a couple parameters of its own:

int main(int argc, char** argv)

So, you guessed it, argc will end up in rdi, and argv (a pointer) will end up in rsi. Here is a program that uses this fact to simply echo the command line arguments to a program, one per line:

echo.asm

; -----------------------------------------------------------------------------
; A 64-bit program that displays its command line arguments, one per line.
;
; On entry, rdi will contain argc and rsi will contain argv.
; -----------------------------------------------------------------------------

        global  main
        extern  puts
        section .text
main:
        push    rdi                     ; save registers that puts uses
        push    rsi
        sub     rsp, 8                  ; must align stack before call

        mov     rdi, [rsi]              ; the argument string to display
        call    puts                    ; print it

        add     rsp, 8                  ; restore %rsp to pre-aligned value
        pop     rsi                     ; restore registers puts used
        pop     rdi

        add     rsi, 8                  ; point to next argument
        dec     rdi                     ; count down
        jnz     main                    ; if not done counting keep going

        ret

$ nasm -felf64 echo.asm && gcc echo.o && ./a.out dog 22 -zzz "hi there"
./a.out
dog
22
-zzz
hi there

A Longer Example

Note that as far as the C Library is concerned, command line arguments are always strings. If you want to treat them as integers, call atoi. Here’s a neat program to compute $x^y$.

power.asm

; -----------------------------------------------------------------------------
; A 64-bit command line application to compute x^y.
;
; Syntax: power x y
; x and y are (32-bit) integers
; -----------------------------------------------------------------------------

        global  main
        extern  printf
        extern  puts
        extern  atoi

        section .text
main:
        push	r12                     ; save callee-save registers
        push	r13
        push	r14
        ; By pushing 3 registers our stack is already aligned for calls

        cmp     rdi, 3                  ; must have exactly two arguments
        jne     error1

        mov     r12, rsi                ; argv

; We will use ecx to count down form the exponent to zero, esi to hold the
; value of the base, and eax to hold the running product.

        mov     rdi, [r12+16]           ; argv[2]
        call    atoi                    ; y in eax
        cmp     eax, 0                  ; disallow negative exponents
        jl      error2
        mov     r13d, eax               ; y in r13d

        mov     rdi, [r12+8]            ; argv
        call	atoi                    ; x in eax
        mov     r14d, eax               ; x in r14d

        mov     eax, 1                  ; start with answer = 1
check:
        test    r13d, r13d              ; we're counting y downto 0
        jz      gotit                   ; done
        imul    eax, r14d               ; multiply in another x
        dec     r13d
        jmp     check
gotit:                                  ; print report on success
        mov     rdi, answer
        movsxd  rsi, eax
        xor     rax, rax
        call    printf
        jmp     done
error1:                                 ; print error message
        mov     edi, badArgumentCount
        call    puts
        jmp     done
error2:                                 ; print error message
        mov     edi, negativeExponent
        call    puts
done:                                   ; restore saved registers
        pop     r14
        pop     r13
        pop     r12
        ret

answer:
        db      "%d", 10, 0
badArgumentCount:
        db      "Requires exactly two arguments", 10, 0
negativeExponent:
        db      "The exponent may not be negative", 10, 0
$ nasm -felf64 power.asm && gcc -o power power.o
$ ./power 2 19
524288
$ ./power 3 -8
The exponent may not be negative
$ ./power 1 500
1
$ ./power 1
Requires exactly two arguments

Floating Point Instructions

Floating-point arguments go int the xmm registers. Here is a simple function for summing
the values in a double array:

sum.asm

; -----------------------------------------------------------------------------
; A 64-bit function that returns the sum of the elements in a floating-point
; array. The function has prototype:
;
;   double sum(double[] array, uint64_t length)
; -----------------------------------------------------------------------------

        global  sum
        section .text
sum:
        xorpd   xmm0, xmm0              ; initialize the sum to 0
        cmp     rsi, 0                  ; special case for length = 0
        je      done
next:
        addsd   xmm0, [rdi]             ; add in the current array element
        add     rdi, 8                  ; move to next array element
        dec     rsi                     ; count down
        jnz     next                    ; if not done counting, continue
done:
        ret                             ; return value already in xmm0

Note the floating point instructions have an sd suffix; that’s the
most common one, but we’ll see some other ones later. Here is a C program that calls it:

callsum.c

/*
 * Illustrates how to call the sum function we wrote in assembly language.
 */

#include <stdio.h>
#include <inttypes.h>

double sum(double[], uint64_t);

int main() {
    double test[] = {
        40.5, 26.7, 21.9, 1.5, -40.5, -23.4
    };
    printf("%20.7f\n", sum(test, 6));
    printf("%20.7f\n", sum(test, 2));
    printf("%20.7f\n", sum(test, 0));
    printf("%20.7f\n", sum(test, 3));
    return 0;
}
$ nasm -felf64 sum.asm && gcc sum.o callsum.c && ./a.out
          26.7000000
          67.2000000
           0.0000000
          89.1000000

Data Sections

The text section is read-only on most operating systems, so
you might find the need for a data section. On most operating systems,
the data section is only for initialized data, and you have a special
.bss section for uninitialized data. Here is a program that averages
the command line arguments, expected to be integers, and displays the
result as a floating point number.

average.asm

; -----------------------------------------------------------------------------
; 64-bit program that treats all its command line arguments as integers and
; displays their average as a floating point number.  This program uses a data
; section to store intermediate results, not that it has to, but only to
; illustrate how data sections are used.
; -----------------------------------------------------------------------------

        global   main
        extern   atoi
        extern   printf
        default  rel

        section  .text
main:
        dec      rdi                    ; argc-1, since we don't count program name
        jz       nothingToAverage
        mov      [count], rdi           ; save number of real arguments
accumulate:
        push     rdi                    ; save register across call to atoi
        push     rsi
        mov      rdi, [rsi+rdi*8]       ; argv[rdi]
        call     atoi                   ; now rax has the int value of arg
        pop      rsi                    ; restore registers after atoi call
        pop      rdi
        add      [sum], rax             ; accumulate sum as we go
        dec      rdi                    ; count down
        jnz      accumulate             ; more arguments?
average:
        cvtsi2sd xmm0, [sum]
        cvtsi2sd xmm1, [count]
        divsd    xmm0, xmm1             ; xmm0 is sum/count
        mov      rdi, format            ; 1st arg to printf
        mov      rax, 1                 ; printf is varargs, there is 1 non-int argument

        sub      rsp, 8                 ; align stack pointer
        call     printf                 ; printf(format, sum/count)
        add      rsp, 8                 ; restore stack pointer

        ret

nothingToAverage:
        mov      rdi, error
        xor      rax, rax
        call     printf
        ret

        section  .data
count:  dq       0
sum:    dq       0
format: db       "%g", 10, 0
error:  db       "There are no command line arguments to average", 10, 0

$ nasm -felf64 average.asm && gcc average.o && ./a.out 19 8 21 -33
3.75
$ nasm -felf64 average.asm && gcc average.o && ./a.out
There are no command line arguments to average

This program highlighted some processor instructions that convert between integers and floating point values. A few of the most common are:

Instruction Description
$\mathtt{cvtsi2sd}\;\mathit{xmmreg},\mathit{r/m32}$ $\mathit{xmmreg}[63..0] \leftarrow \mathrm{intToDouble}(\mathit{r/m32})$
$\mathtt{cvtsi2ss}\;\mathit{xmmreg},\mathit{r/m32}$ $\mathit{xmmreg}[31..0] \leftarrow \mathrm{intToFloat}(\mathit{r/m32})$
$\mathtt{cvtsd2si}\;\mathit{reg32},\mathit{xmmr/m64}$ $\mathit{reg32} \leftarrow \mathrm{doubleToInt}(\mathit{xmmr/m64})$
$\mathtt{cvtss2si}\;\mathit{reg32},\mathit{xmmr/m32}$ $\mathit{reg32} \leftarrow \mathrm{floatToInt}(\mathit{xmmr/m32})$

Recursion

Perhaps surprisingly, there’s nothing out of the ordinary required
to implement recursive functions. You just have to be careful to save
registers, as usual. Pushing and popping around the recursive call
is a typical strategy.

factorial.asm

; ----------------------------------------------------------------------------
; An implementation of the recursive function:
;
;   uint64_t factorial(uint64_t n) {
;       return (n <= 1) ? 1 : n * factorial(n-1);
;   }
; ----------------------------------------------------------------------------

        global  factorial

        section .text
factorial:
        cmp     rdi, 1                  ; n <= 1?
        jnbe    L1                      ; if not, go do a recursive call
        mov     rax, 1                  ; otherwise return 1
        ret
L1:
        push    rdi                     ; save n on stack (also aligns %rsp!)
        dec     rdi                     ; n-1
        call    factorial               ; factorial(n-1), result goes in %rax
        pop     rdi                     ; restore n
        imul    rax, rdi                ; n * factorial(n-1), stored in %rax
        ret

An example caller:

callfactorial.c

/*
 * An application that illustrates calling the factorial function defined elsewhere.
 */

#include <stdio.h>
#include <inttypes.h>

uint64_t factorial(uint64_t n);

int main() {
    for (uint64_t i = 0; i < 20; i++) {
        printf("factorial(%2lu) = %lu\n", i, factorial(i));
    }
    return 0;
}
$ nasm -felf64 factorial.asm && gcc -std=c99 factorial.o callfactorial.c && ./a.out
factorial( 0) = 1
factorial( 1) = 1
factorial( 2) = 2
factorial( 3) = 6
factorial( 4) = 24
factorial( 5) = 120
factorial( 6) = 720
factorial( 7) = 5040
factorial( 8) = 40320
factorial( 9) = 362880
factorial(10) = 3628800
factorial(11) = 39916800
factorial(12) = 479001600
factorial(13) = 6227020800
factorial(14) = 87178291200
factorial(15) = 1307674368000
factorial(16) = 20922789888000
factorial(17) = 355687428096000
factorial(18) = 6402373705728000
factorial(19) = 121645100408832000

SIMD Parallelism

The XMM registers can do arithmetic on floating point values one operation at a time (scalar) or multiple operations at a time (packed). The operations have the form:

$\mathit{op}\;\mathit{xmmreg\_or\_memory}, \mathit{xmmreg}$

For floating point addition, the instructions are:

Instruction Description
$\mathtt{addpd}$ Do two double-precision additions in parallel (add packed double)
$\mathtt{addsd}$ Do just one double-precision addition, using the low 64-bits of the register (add scalar double)
$\mathtt{addps}$ Do four single-precision additions in parallel (add packed single)
$\mathtt{addss}$ Do just one single-precision addition, using the low 32-bits of the register (add scalar single)

Here’s a function that adds four floats at once:

add_four_floats.asm

; void add_four_floats(float x[4], float y[4])
; x[i] += y[i] for i in range(0..4)

        global   add_four_floats
        section  .text

add_four_floats:
        movdqa   xmm0, [rdi]            ; all four values of x
        movdqa   xmm1, [rsi]            ; all four values of y
        addps    xmm0, xmm1             ; do all four sums in one shot
        movdqa   [rdi], xmm0
        ret

and a caller:

test_add_four_floats.c

#include <stdio.h>
void add_four_floats(float[], float[]);

int main() {
    float x[] = {-29.750, 244.333, 887.29, 48.1E22};
    float y[] = {29.750,  199.333, -8.29,  22.1E23};
    add_four_floats(x, y);
    printf("%f\n%f\n%f\n%f\n", x[0], x[1], x[2], x[3]);
    return 0;
}

Also see this nice little x86 floating-point slide deck from Ray Seyfarth.

Saturated Arithmetic

The XMM registers can also do arithmetic on integers. The instructions have the form:

$\mathit{op}\;\mathit{xmmreg\_or\_memory}, \mathit{xmmreg}$

For integer addition, the instructions are:

Instruction Description
$\mathtt{paddb}$ Do 16 byte-additions
$\mathtt{paddw}$ Do 8 word-additions
$\mathtt{paddd}$ Do 4 dword-additions
$\mathtt{paddq}$ Do 2 qword-additions
$\mathtt{paddsb}$ Do 16 byte-additions with signed saturation (80..7F)
$\mathtt{paddsw}$ Do 8 word-additions with signed saturation (8000..7F)
$\mathtt{paddusb}$ Do 16 byte-additions with unsigned saturation (00..FF)
$\mathtt{paddusw}$ Do 8 word-additions with unsigned saturation (00..FFFF)

Here’s an example. It also illustrates how you load the XMM registers. You can’t load immediate values; you have to use movaps to move from memory. There are other ways, but we’re not covering everything in this tutorial.

satexample.asm


; ----------------------------------------------------------------------------------------
; Example of signed saturated arithmetic.
; ----------------------------------------------------------------------------------------

        global  main
        extern  printf

        section .text
main:
        push    rbp
        movaps  xmm0, [arg1]
        movaps  xmm1, [arg2]
        paddsw  xmm0, xmm1
        movaps  [result], xmm0

        lea     rdi, [format]
        mov     esi, dword [result]
        mov     edx, dword [result+4]
        mov     ecx, dword [result+8]
        mov     r8d, dword [result+12]
        xor     rax, rax
        call    printf
        pop     rbp
        ret
        section .data
        align   16
arg1:   dw      0x3544,0x24FF,0x7654,0x9A77,0xF677,0x9000,0xFFFF,0x0000
arg2:   dw      0x7000,0x1000,0xC000,0x1000,0xB000,0xA000,0x1000,0x0000
result: dd      0, 0, 0, 0
format: db      '%x%x%x%x',10,0

Graphics

Any C program can be “ported” to assembly language. That goes for graphics programs, too.

This program probably does not work.

I last tested this in 2003. Back in the old-school OpenGL days. Used Win32. Pre-GLSL days. Used GLUT. I haven’t had access to a Windows box in a while and I’m not even sure it will work anymore. This is presented here for historical interest only. If you can modify it to work under modern OpenGL, please let me know. I’ll update the program and cite your contribution, of course!

triangle.asm

; ----------------------------------------------------------------------------
; triangle.asm
;
; A very simple *Windows* OpenGL application using the GLUT library.  It
; draws a nicely colored triangle in a top-level application window.  One
; interesting thing is that the Windows GL and GLUT functions do NOT use the
; C calling convention; instead they use the "stdcall" convention which is
; like C except that the callee pops the parameters.
; ----------------------------------------------------------------------------

	global	_main
	extern	_glClear@4
	extern	_glBegin@4
	extern	_glEnd@0
	extern	_glColor3f@12
	extern	_glVertex3f@12
	extern	_glFlush@0
	extern	_glutInit@8
	extern	_glutInitDisplayMode@4
	extern	_glutInitWindowPosition@8
	extern	_glutInitWindowSize@8
	extern	_glutCreateWindow@4
	extern	_glutDisplayFunc@4
	extern	_glutMainLoop@0

	section	.text
title:	db	'A Simple Triangle', 0
zero:	dd	0.0
one:	dd	1.0
half:	dd	0.5
neghalf:dd	-0.5

display:
	push	dword 16384
	call	_glClear@4		; glClear(GL_COLOR_BUFFER_BIT)
	push	dword 9
	call	_glBegin@4		; glBegin(GL_POLYGON)
	push	dword 0
	push	dword 0
	push	dword [one]
	call	_glColor3f@12		; glColor3f(1, 0, 0)
	push	dword 0
	push	dword [neghalf]
	push	dword [neghalf]
	call	_glVertex3f@12		; glVertex(-.5, -.5, 0)
	push	dword 0
	push    dword [one]
	push	dword 0
	call	_glColor3f@12		; glColor3f(0, 1, 0)
	push	dword 0
	push	dword [neghalf]
	push	dword [half]
	call	_glVertex3f@12		; glVertex(.5, -.5, 0)
	push	dword [one]
	push	dword 0
	push	dword 0
	call	_glColor3f@12		; glColor3f(0, 0, 1)
	push	dword 0
	push	dword [half]
	push	dword 0
	call	_glVertex3f@12		; glVertex(0, .5, 0)
	call	_glEnd@0		; glEnd()
	call	_glFlush@0		; glFlush()
	ret

_main:
	push	dword [esp+8]		; push argv
	lea	eax, [esp+8]		; get addr of argc (offset changed :-)
	push	eax
	call	_glutInit@8		; glutInit(&argc, argv)
	push	dword 0
	call	_glutInitDisplayMode@4
	push	dword 80
	push	dword 80
	call	_glutInitWindowPosition@8
	push	dword 300
	push	dword 400
	call	_glutInitWindowSize@8
	push	title
	call	_glutCreateWindow@4
	push	display
	call	_glutDisplayFunc@4
	call	_glutMainLoop@0
	ret

Local Variables and Stack Frames

First, please read Eli
Bendersky’s article. That overview is more complete than my brief notes.

When a function is called the caller will first put the parameters in the correct registers then issue the call instruction. Additional parameters beyond those covered by the registers will be pushed on the stack prior to the call. The call instruction puts the return address on the top of stack. So if you have the function:

int64_t example(int64_t x, int64_t y) {
    int64_t a, b, c;
    b = 7;
    return x * b + y;
}

Then on entry to the function, $x$ will be in edi, $y$ will be in esi, and the return address will be on the top of the stack. Where can we put the local variables? An easy choice is on the stack itself, though if you have enough regsters, use them! Registers tend to be faster anyway.

If you are running on a machine that respect the standard ABI, you can leave rsp where it is and access the “extra parameters” and the local variables directly from rsp for example:

rsp-24

a

rsp-16

b

rsp-8

c

rsp

retaddr

rsp+8

caller’s
stack
frame

So our function looks like this:

        global  example
        section .text
example:
        mov     qword [rsp-16], 7
        mov     rax, rdi
        imul    rax, [rsp+8]
        add     rax, rsi
        ret

If our function were to make another call, you would have to adjust rsp to get out of the way
at that time.

On Windows you can’t use this scheme because if an interrupt were to occur, everything above the stack pointer gets plastered. This doesn’t happen on most other operating systems because there is a red zone of 128 bytes past the stack pointer which is safe from these things. In this case, you can make room on the stack immediately:

example:
        sub rsp, 24

so our stack looks like this:

rsp

a

rsp+8

b

rsp+16

c

rsp+24

retaddr

rsp+32

caller’s
stack
frame

Here’s the function now. Note that we have to remember to replace the stack pointer before
returning!

        global  example
        section .text
example:
        sub     rsp, 24
        mov     qword [rsp+8], 7
        mov     rax, rdi
        imul    rax, [rsp+8]
        add     rax, rsi
        add     rsp, 24
        ret

Using NASM on macOS

Hopefully you’ve gone through the whole tutorial above using a Linux-based operating
system (or perhaps more correctly, and ELF64 system). There are pretty much only five
thing to know to get these examples working under a 64-bit macOS system:

  • This object file format is macho64, not elf64.
  • The system call numbers are totally different.
  • Symbols shared between modules will be prefixed by underscores.
  • It seems that the gcc linker in macOS doesn’t allow absolute addressing unless
    you tweak some settings. So add default rel when you are referencing
    labeled memory locations, and always use lea to get your addresses.
  • Also, it appears that sometimes under Linux, the 16-bit stack alignment requirement
    is not enforced, but it appears to be always enforced under macOS.

So here’s the average program from above, written for macOS.

average.asm

; -----------------------------------------------------------------------------
; 64-bit program that treats all its command line arguments as integers and
; displays their average as a floating point number.  This program uses a data
; section to store intermediate results, not that it has to, but only to
; illustrate how data sections are used.
;
; Designed for OS X.  To assemble and run:
;
;     nasm -fmacho64 average.asm && gcc average.o && ./a.out
; -----------------------------------------------------------------------------

        global   _main
        extern   _atoi
        extern   _printf
        default  rel

        section  .text
_main:
        push     rbx                    ; we don't ever use this, but it is necesary
                                        ; to align the stack so we can call stuff
        dec      rdi                    ; argc-1, since we don't count program name
        jz       nothingToAverage
        mov      [count], rdi           ; save number of real arguments
accumulate:
        push     rdi                    ; save register across call to atoi
        push     rsi
        mov      rdi, [rsi+rdi*8]       ; argv[rdi]
        call     _atoi                  ; now rax has the int value of arg
        pop      rsi                    ; restore registers after atoi call
        pop      rdi
        add      [sum], rax             ; accumulate sum as we go
        dec      rdi                    ; count down
        jnz      accumulate             ; more arguments?
average:
        cvtsi2sd xmm0, [sum]
        cvtsi2sd xmm1, [count]
        divsd    xmm0, xmm1             ; xmm0 is sum/count
        lea      rdi, [format]          ; 1st arg to printf
        mov      rax, 1                 ; printf is varargs, there is 1 non-int argument
        call     _printf                ; printf(format, sum/count)
        jmp      done

nothingToAverage:
        lea      rdi, [error]
        xor      rax, rax
        call     _printf

done:
        pop      rbx                    ; undoes the stupid push at the beginning
        ret

        section  .data
count:  dq       0
sum:    dq       0
format: db       "%g", 10, 0
error:  db       "There are no command line arguments to average", 10, 0
$ nasm -fmacho64 average.asm && gcc average.o && ./a.out
There are no command line arguments to average
$ nasm -fmacho64 average.asm && gcc average.o && ./a.out 54.3
54
$ nasm -fmacho64 average.asm && gcc average.o && ./a.out 54.3 -4 -3 -25 455.1111
95.4

Using NASM on Windows

I’m not sure what the system calls are on Windows, but I do know that if you want to assemble and link with the C library, you have to understand the x64 conventions. Read them. You will learn such things as:

  • The first four integer parameters are passed in RCX, RDX, R8, and R9. The rest are to be pushed on the stack.
  • The callee must preserve RBX, RBP, RDI, RSI, RSP, R12, R13, R14, and R15.
  • The first four floating point parameters are passed in, you guessed it, XMM0, XMM1, XMM2, and XMM3.
  • Return values go in RAX or XMM0.

IMPORTANT: There’s one thing that’s really hard to find in any documentation: the x64 calling convention requires you to allocate 32 bytes of shadow space before each call, and remove it after your call. This means your “hello world” program looks like this:

hello.asm

; ----------------------------------------------------------------------------------------
; This is a Win64 console program that writes "Hello" on one line and then exits.  It
; uses puts from the C library.  To assemble and run:
;
;     nasm -fwin64 hello.asm && gcc hello.obj && a
; ----------------------------------------------------------------------------------------

        global  main
        extern  puts
        section .text
main:
        sub     rsp, 28h                        ; Reserve the shadow space
        mov     rcx, message                    ; First argument is address of message
        call    puts                            ; puts(message)
        add     rsp, 28h                        ; Remove shadow space
        ret
message:
        db      'Hello', 0                      ; C strings need a zero byte at the end

Did you notice we actually reserved 40 bytes? Thirty-two bytes of shadow space is a minimum requirement. In our main function, we are calling another function, so our stack must be aligned on a 16-byte boundary. When main is called, the return address (8 bytes) was pushed, so we have to “add” an extra 8 bytes to the shadow space.

Summary

We’ve covered:

  • How to run a NASM program
  • The structure of a NASM program
  • The most basic instructions
  • Instruction formats
  • Mixing C and assembly language
  • Floating point, saturated arithmetic, and parallel instructions
  • Calls, calling conventions, and recursion
  • Some platform-specific details

Время на прочтение5 мин

Количество просмотров176K

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

MASM

Используется для создания драйверов под Windows.

По ссылке переходим на сайт и скачиваем пакет (masm32v11r.zip). После инсталляции программы на диске создается папка с нашим пакетом C:\masm32. Создадим программу prog11.asm, которая ничего не делает.

.586P
.model flat, stdcall 
_data segment 
_data ends
_text segment
start:
ret   
_text ends
end start

Произведём ассемблирование (трансляцию) файла prog11.asm, используя ассемблер с сайта masm32.

image

Ключ /coff используется здесь для трансляции 32-битных программ.
Линковка производится командой link /subsystem:windows prog11.obj (link /subsystem:console prog11.obj)

Как сказано в Википедии

MASM — один из немногих инструментов разработки Microsoft, для которых не было отдельных 16- и 32-битных версий.

Также ассемблер версии 6. можно взять на сайте Кипа Ирвина kipirvine.com/asm, автора книги «Язык ассемблера для процессоров Intel».

Кстати, вот ссылка на личный сайт Владислава Пирогова, автора книги “Ассемблер для Windows”.

MASM с сайта Microsoft

Далее скачаем MASM (версия 8.0) с сайта Microsoft по ссылке. Загруженный файл носит название «MASMsetup.exe». При запуске этого файла получаем сообщение -«Microsoft Visual C++ Express Edition 2005 required».

Открываем этот файл архиватором (например 7zip). Внутри видим файл setup.exe, извлекаем его, открываем архиватором. Внутри видим два файла vc_masm.msi,vc_masm1.cab. Извлекаем файл vc_masm1.cab, открываем архиватором. Внутри видим файл FL_ml_exe_____X86.3643236F_FC70_11D3_A536_0090278A1BB8. Переименовываем его в файл fl_ml.exe, далее, произведём ассемблирование файла prog11.asm, используя ассемблер fl_ml.exe.

image

MASM в Visual Studio

Также MASM можно найти в папке с Visual Studio (у меня VS 10) вот здесь: C:\Program Files\Microsoft Visual Studio 10.0\VC\bin\ml.exe.

image

Для того, чтобы запустить на 32- или 64-разрядной системе и создавать программы, работающие как под 32-, так и под 64-разрядной Windows, подходит MASM32 (ml.exe, fl_ml.exe). Для того, чтобы работать на 32- и 64-разрядных системах и создавать программы, работающие под 64-разрядной Windows, но неработающие под 32-разрядной нужен ассемблер ml64.exe. Лежит в папке C:\Program Files\Microsoft Visual Studio 10.0\VC\bin\amd64 и вот здесь — C:\Program Files\Microsoft Visual Studio 10.0\VC\bin\x86_amd64.

TASM

Программный пакет компании Borland, предназначенный для разработки программ на языке ассемблера для архитектуры x86. В настоящее время Borland прекратила распространение своего ассемблера.

Скачать можно, например, здесь. Инсталлятора нет, просто извлекаем программу. Вот исходник из книги Питера Абеля (рис. 3.2) «Язык Ассемблера для IBM PC и программирования».

stacksg segment para stack 'stack'
    db 12 dup ('stackseg')
stacksg ends
codesg segment para 'code'
begin proc far
 assume ss:stacksg,cs:codesg,ds:nothing
 push ds
 sub ax,ax
 push ax
 mov ax, 0123h
 add ax, 0025h
 mov bx,ax
 add bx,ax
 mov cx,bx
 sub cx,ax
 sub ax,ax
 nop
 ret
begin endp
codesg ends
 end begin

Выполним ассемблирование (трансляцию) файла abel32.asm.

image

Корректность работы программы можно проверить, произведя линковку (tlink.exe) объектного файла и запустив полученный файл в отладчике.

Как было сказано выше, MASM можно использовать для работы с 16-битными программами. Выполним ассемблирование (трансляцию) программы abel32.asm с помощью ассемблера MASM:

image

Ключ /coff здесь не используется.
Линковка производится файлом link16.exe

Вот здесь приводится порядок действий, которые необходимо выполнить для запуска TASM в DOSbox. Для линковки понадобится файл DPMI16BI.OVL

FASM

В статье Криса Касперски «Сравнение ассемблерных трансляторов» написано, что «FASM — неординарный и весьма самобытный, но увы, игрушечный ассемблер. Пригоден для мелких задач типа „hello, world“, вирусов, демок и прочих произведений хакерского творчества.»

Скачаем FASM с официального сайта. Инсталлятора нет, просто извлекаем программу. Откроем fasm editor — C:\fasm\fasmw.exe. В папке C:\fasm\EXAMPLES\HELLO есть файл HELLO.asm.

include 'win32ax.inc' 
.code
  start:
 invoke    MessageBox,HWND_DESKTOP,"Hi! I'm the example program!",invoke GetCommandLine,MB_OK
    invoke    ExitProcess,0
.end start

Откроем файл HELLO.asm из fasmw.exe. Изменим строку include ‘win32ax.inc’ на строку include ‘c:\fasm\INCLUDE\WIN32AX.INC’. Запускаем из меню Run → Run.

image

Вот ссылки на ресурсы, посвященные FASM:

→ FASM на Cyberforum’е
→ FASM на asmworld .com программы под Dos
→ Цикл статей «Ассемблер под Windows для чайников»
→ Сайт на narod’е

FASM в Linux

Для того, использовать FASM в Linux (у меня Ubuntu), скачаем соответствующий дистрибутив (fasm-1.71.60.tgz), распакуем его, в папке у нас будет бинарный файл fasm, копируем этот файл в /usr/local/bin для того, чтобы можно было запускать его из консоли, как любую другую команду.Выполним ассемблирование программы hello.asm из папки fasm/examples/elfexe/hello.asm.

image

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

Nasm

Nasm успешно конкурирует со стандартным в Linux- и многих других UNIX-системах ассемблером Gas.

Nasm в Linux можно установить с помощью менеджера пакетов или из командной строки: в дистрибутиве Debian (Ubuntu) командой apt-get install nasm, в дистрибутивах Fedora, CentOS, RedHat командой yum install nasm.

Создадим программу, которая 5 раз выводит сообщение “Hello”. Пример взят из книги Андрея Викторовича Столярова “Программирование на языке ассемблера NASM для ОС UNIX”. Учебник, а также библиотека “stud_io.inc” есть на личном сайте автора.

%include "stud_io.inc"
global _start
section .text
_start: mov eax, 0
again:  PRINT "Hello"
PUTCHAR 10
inc eax
cmp eax, 5
jl again
FINISH

Выполним ассемблирование и линковку и запустим файл hello.asm.

$ nasm -f elf hello.asm
$ ld hello.o -o hello
$ ./hello

Для 64bit необходимо использовать команду nasm -f elf64 hello.asm

NASM для Windows

NASM для Windows можно установить, скачав соответствующий дистрибутив с соответствующего сайта.

Ассемблирование:
nasm -f bin имя_файла.asm -o имя_файла.com

Ссылки на ресурсы, посвященные Nasm:

→ Сайт А.В. Столярова
→ Сайт, на котором лежит электронный учебник (в архиве)
→ То же самое

AS

Стандартный ассемблер практически во всех разновидностях UNIX, в том числе Linux и BSD. Свободная версия этого ассемблера называется GAS (GNU assembler). Позволяет транслировать программы с помощью компилятора GCC.

Из учебников удалось найти только книгу на английском «Programming from the ground up». На русском удалось найти только одну главу из книги С. Зубкова «Assembler для DOS, Windows и UNIX».

Возьмем пример программы, которая ничего не делает, с сайта. Создадим программу gas.s

.section .text
   .globl _start
   _start:
      movl  $1, %eax
      movl  $2, %ebx
      int   $0x80

Выполним ассемблирование (трансляцию), линковку и запуск программы:

$ as -o gas.o gas.s
$ ld -o gas gas.o
$ ./gas

Если в данной программе изменить _start на main, то можно выполнить ассемблирование (трансляцию) и линковку компилятором gcc.

.section .text
   .globl main
   main:
      movl  $1, %eax
      movl  $2, %ebx
      int   $0x80

Выполним ассемблирование (трансляцию), линковку и запуск программы:

$ gcc gas.s -o gas
$ ./gas

Выводы: если вы изучаете программирование под Windows, то вы можете остановить свой выбор на Masm; Tasm больше не поддерживается, но для обучения по старым классическим учебникам подойдёт.
Под Linux Gas подойдет тем, кто использует GCC, а тем, кому не нравится синтаксис Gas, подойдёт Nasm.

P.S. Про обработку строк в ассемблере на примере создания транслятора простого «эзотерического» языка можно прочитать здесь.
P.P.S. Упрощенный ограниченный набор ассемблерных инструкций используется в учебной модели компьютера Little Man Computer, которому у меня также посвящено несколько статей ссылка.

12 Sep 2015

In this post, I’ll walk through the steps required to bootstrap your development experience against the Win32 API using the Netwide Assembler.

Prerequisites

Before starting, you’ll need some software. I’ve used the following software set, however you can use any linker and resource compiler that you like.

  • Nasm
  • ALink
  • Gorc
  • Win32 include file

You’ll use Nasm to reduce your assembly source code into COFF object files. Gorc will take your resource scripts and produce linkable object files from these. Finally, ALink will bind all of your object files into a windows executable.

Finally, you’re going to need a copy of the include file for the Win32 API. The API itself is huge; the number of constants and structures is mind boggling. The link above handles all of these for you.

Test program

Probably the easiest thing to accomplish, is showing a message box. You need to show the message box and then return control back to Windows. You do this with calls to MessageBoxA and ExitProcess. The “A” in MessageBoxA as we’re not dealing with the wide-char version of these functions.

Here’s the code.

%include "win32n.inc"

extern MessageBoxA
import MessageBoxA user32.dll

extern ExitProcess
import ExitProcess kernel32.dll

segment .data USE32

	title	db "A message for you", 0
	message db "This is your first message", 0

segment	.bss USE32

segment .code USE32

..start:

	; show the message box
	push MB_OK
	push title
	push message
	push 0
	call [MessageBoxA]

	; return control back to windows
	push 0
	call [ExitProcess]

Functions are imported from the api using import, and are called in a very assembler-traditional fashion here. Taking a look at the definition for the MessageBox function, we can see the order of parameters:

int WINAPI MessageBox(
  HWND    hWnd,
  LPCTSTR lpText,
  LPCTSTR lpCaption,
  UINT    uType
);

Arguments are pushed to the stack in reverse order.

Assembling and linking

Now that you’ve got your source file, hello.asm you can produce an object file with the following:

C:\src> nasm -i c:\nasm\include -f obj hello.asm

You can now link the object file into an executable with the following:

C:\src> alink -c -oPE -subsys gui hello

Ready to go.

Making things a little more high-level

You can make your assembly code a little more high-level by using the nagoa+.inc include file. This include file provides your programs with some really handy constructs (as well as the win32 api bindings), so function invocations now look like this:

call MessageBoxA, 0, message, title, MB_OK
call ExitProcess, 0

Conclusion

This will get you started at least with Nasm in Windows development. Here is a great resource, full of links on assembly development.

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Пропали все шрифты в windows 10
  • Azure active directory для windows powershell
  • Сборник основных программ для windows
  • Есть ли safari для windows
  • Business calendar for windows