How to Write a Windows App in Assembly Language
If you are learning low-level programming, operating systems, compilers, kernels, or calling conventions, this is a very useful exercise because it forces you to understand what normally happens underneath C or C++.
In this article, we will build a tiny 64-bit Windows GUI application in assembly using MASM and the Win32 API.
Our first application will simply display a Windows message box.
What We Are Building
The program will call the Windows function:
MessageBoxA(...)
from assembly.
Conceptually, the program looks like this:
Assembly Program
|
v
Windows x64 Calling Convention
|
v
Win32 API
|
+---- user32.dll
|
+---- kernel32.dll
|
v
Windows
The final executable will be a normal Windows .exe.
Tools We Need
For this tutorial, I recommend Microsoft's assembler:
MASM
More specifically, for 64-bit Windows:
ml64.exe
Install Visual Studio Build Tools and make sure you include:
- Desktop development with C++
- MSVC x64 build tools
- Windows SDK
After installation, open:
x64 Native Tools Command Prompt for VS
This gives you access to tools such as:
ml64.exe
link.exe
Step 1: Create the Assembly File
Create a file called:
hello.asm
Add the following code:
option casemap:none
EXTERN MessageBoxA:PROC
EXTERN ExitProcess:PROC
.data
message db "Hello from Windows x64 Assembly!", 0
title db "My Assembly App", 0
.code
main PROC
; Reserve shadow space and preserve stack alignment.
sub rsp, 28h
; MessageBoxA(
; HWND hWnd,
; LPCSTR lpText,
; LPCSTR lpCaption,
; UINT uType
; )
xor rcx, rcx
lea rdx, message
lea r8, title
xor r9d, r9d
call MessageBoxA
; ExitProcess(0)
xor ecx, ecx
call ExitProcess
main ENDP
END
This is already enough to create a Windows GUI program.
Understanding the Data Section
This part:
.data
message db "Hello from Windows x64 Assembly!", 0
title db "My Assembly App", 0
creates two null-terminated strings.
The first string contains the message:
Hello from Windows x64 Assembly!
The second contains the title of our message box:
My Assembly App
The trailing 0 terminates each string because MessageBoxA expects C-style strings.
Calling Windows from Assembly
This is where things get interesting.
On 64-bit Windows, the first four integer or pointer arguments are passed through registers.
The Windows x64 calling convention uses:
Argument 1 -> RCX
Argument 2 -> RDX
Argument 3 -> R8
Argument 4 -> R9
So this C function call:
MessageBoxA(
NULL,
"Hello from Windows x64 Assembly!",
"My Assembly App",
0
);
becomes something like:
xor rcx, rcx
lea rdx, message
lea r8, title
xor r9d, r9d
call MessageBoxA
This is one of the most important things to understand when learning assembly.
A normal high-level function call eventually becomes operations involving registers, memory, the stack, and a machine-level call instruction.
Why LEA Is Used
You may notice this:
lea rdx, message
instead of:
mov rdx, message
MessageBoxA needs a pointer to the string.
LEA means:
Load Effective Address
So:
lea rdx, message
places the address of message into RDX.
Conceptually:
RDX -> address of message string
The Windows x64 Stack
Windows x64 also has another important rule.
Before calling a function, the caller reserves 32 bytes of shadow space.
This is sometimes also called:
home space
That is why the example contains:
sub rsp, 28h
The extra space also helps maintain the required stack alignment before making Windows API calls.
This becomes especially important when you begin writing larger assembly programs.
Step 2: Assemble the Program
Run:
ml64 /c hello.asm
MASM translates:
hello.asm
into:
hello.obj
The process looks like this:
hello.asm
|
v
Assembler
|
v
hello.obj
An .obj file is not yet an executable.
It contains machine code and metadata that the linker can combine with other libraries.
Step 3: Link the Program
Now run:
link hello.obj user32.lib kernel32.lib /SUBSYSTEM:WINDOWS /ENTRY:main /OUT:hello.exe
This produces:
hello.exe
We link against:
user32.lib
kernel32.lib
because our program uses functions provided by those Windows libraries.
MessageBoxA comes from:
user32.dll
and ExitProcess comes from:
kernel32.dll
The .lib files allow the linker to construct the necessary import information inside our executable.
Step 4: Run It
Run:
hello.exe
Windows should display a message box containing:
Hello from Windows x64 Assembly!
Congratulations.
You have now created a Windows GUI program without C, C++, C#, Python, or another high-level language.
The application talks directly to Windows through the Win32 API.
From Message Box to a Real Window
A message box is useful for learning, but a real Windows application normally has its own window.
The structure of a traditional Win32 GUI application looks approximately like this:
Program Start
|
v
GetModuleHandle
|
v
Register Window Class
|
v
CreateWindowEx
|
v
ShowWindow
|
v
Message Loop
|
+---- GetMessage
|
+---- TranslateMessage
|
+---- DispatchMessage
|
v
WndProc
|
+---- WM_CREATE
|
+---- WM_PAINT
|
+---- WM_KEYDOWN
|
+---- WM_MOUSEMOVE
|
+---- WM_DESTROY
The central concept is the message loop.
Windows applications are usually event driven.
Your program waits for Windows to send events such as:
mouse movement
keyboard input
window resize
paint request
button click
window close
Each event is represented by a Windows message.
A Typical Message Loop
In C, you might see:
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
In assembly, you manually place those function arguments into the correct registers and call the Win32 functions.
That is where assembly programming becomes extremely educational.
You can see exactly what a high-level language normally hides.
Understanding the Windows ABI
One of the biggest lessons from Windows assembly programming is understanding the ABI.
ABI means:
Application Binary Interface
It defines rules such as:
- where function arguments go
- which registers a function must preserve
- where return values are stored
- how the stack is aligned
- how functions call each other
For Windows x64, remember:
RCX = argument 1
RDX = argument 2
R8 = argument 3
R9 = argument 4
Additional arguments are normally passed using the stack.
The return value is usually placed in:
RAX
So conceptually:
mov rcx, argument1
mov rdx, argument2
mov r8, argument3
mov r9, argument4
call MyFunction
corresponds to something like:
MyFunction(
argument1,
argument2,
argument3,
argument4
);
Once this becomes familiar, reading compiler-generated assembly becomes much easier.
Assembly Is Not Isolated From the Operating System
One misconception about assembly is that everything must be implemented manually.
That is not necessarily true.
Your assembly program can still call operating-system libraries.
For example:
Assembly
|
+---- Windows API
|
+---- DLLs
|
+---- Graphics
|
+---- Files
|
+---- Networking
|
+---- Threads
|
+---- Memory management
Assembly controls how you interact with these interfaces.
You could therefore build:
- graphical applications
- command-line utilities
- DLLs
- debuggers
- emulators
- development tools
- system utilities
- graphics demos
- networking tools
- experimental runtimes
entirely in assembly if you wanted to.
Windows Executables and the PE Format
When the linker creates:
hello.exe
it normally creates a Windows PE executable.
PE means:
Portable Executable
The PE format contains things such as:
PE Header
|
+-- Machine Code
|
+-- Data
|
+-- Import Table
|
+-- Export Table
|
+-- Relocations
|
+-- Resources
Our program's import table tells Windows that the executable needs functions such as:
MessageBoxA
ExitProcess
When Windows loads the program, the loader resolves these imports.
Understanding this process is extremely useful if you are studying operating systems or building your own low-level runtime.
A Good Learning Path
Do not try to build an enormous GUI application immediately.
Build progressively.
A useful progression is:
1. MessageBox
|
v
2. Console output
|
v
3. Basic Win32 window
|
v
4. Window message loop
|
v
5. Keyboard and mouse input
|
v
6. Buttons and controls
|
v
7. GDI graphics
|
v
8. File I/O
|
v
9. Threads
|
v
10. DLLs
|
v
11. Custom runtime
At every step, you learn more about how Windows actually works underneath higher-level programming languages.
Why Learn This?
You probably would not write every modern Windows application entirely in assembly.
But learning to build one is extremely valuable.
It teaches you about:
CPU registers
+
stack frames
+
calling conventions
+
ABI rules
+
assemblers
+
object files
+
linkers
+
DLL loading
+
operating-system APIs
+
executable formats
Those ideas connect directly to areas such as:
- operating-system development
- compiler development
- kernel development
- reverse engineering
- debugging
- emulation
- virtual machines
- runtime development
- systems programming
Once you understand assembly at this level, C stops looking quite so mysterious.
You start seeing what the compiler is actually doing for you.
Final Thoughts
Writing a Windows application in assembly sounds intimidating, but the first step can be surprisingly small.
At its core, our program simply does this:
prepare arguments
|
v
call Windows function
|
v
receive result
Assembly just exposes all of the machinery normally hidden behind a high-level language.
Start with:
MessageBox
Then build a window.
Then build a message loop.
Then add graphics, keyboard input, files, and eventually your own abstractions.
That journey takes you from simply using Windows APIs to understanding how programs actually communicate with an operating system.
And that is where assembly becomes really interesting.
Top comments (0)