If you’re reverse engineering binaries or messing with low-level exploits, you can’t skip learning assembly. High-level languages sugarcoat everything. Assembly is where you see exactly what’s happening—every register, every call, every push and pop. It’s the bedrock of malware analysis, debugging, and exploitation.

This post is about writing simple x64 assembly on Windows, outputting text to the console using both the C runtime and the Windows API, so you can see how it all actually works under the hood.

NASM vs MASM: Which One Should You Use?

You’ll see both out there. Here’s the quick breakdown:

NASM (Netwide Assembler)

  • Open-source and cross-platform.
  • Great for bare-metal work, OS devs, and anyone outside the Microsoft ecosystem.
  • Syntax is clean and explicit—no surprises.
  • You’ll need to do a lot yourself: linking, calling conventions, you name it.

MASM (Microsoft Macro Assembler)

  • Windows-native and Visual Studio-friendly.
  • Tightly integrated with Microsoft’s toolchain.
  • Comes with macros, language extensions, and better support for structured programming.
  • If you’re reversing on Windows or integrating with C code or Windows API—MASM is your go-to.

Here’s a fast reference if you’re picking a side:

Feature NASM MASM
License BSD Microsoft Proprietary
Platform Cross-platform Windows only
Syntax Bare-bones Macro-rich
Integration Manual Visual Studio native
Best For Bootloaders, x-plat Windows reversing, system-level coding

Now let’s code.

Console Output with MASM: Two Ways to Say Hello

You’re going to print "Hello, Reader!" to the screen—once using the C runtime (printf) and once using the Windows API (WriteConsoleA).

Method 1: C Runtime (printf)

.data
msg db "Hello, Reader!", 0
format db "%s", 10, 0

.code
extrn printf:proc

hello proc public
    lea rax, offset msg
    ret
hello endp

main proc
    sub rsp, 40
    
    call hello
    
    lea rcx, format
    mov rdx, rax
    call printf
    
    add rsp, 40
    xor eax, eax
    ret
main endp

end

What’s Happening?

  • .data: Message string and printf format.
  • hello: Returns the address of the string.
  • main: Sets up stack (Windows x64 ABI requires shadow space), sets up arguments, calls printf, exits cleanly.

Make sure you link:

ml64 /c hello.asm
link hello.obj /subsystem:console /defaultlib:msvcrt.lib /defaultlib:legacy_stdio_definitions.lib

This method is fine if you don’t mind the C runtime. Want to go lower? Read on.


Method 2: Straight Windows API.

.data
msg db "Hello, Reader!", 13, 10, 0

.code
extrn ExitProcess:proc
extrn GetStdHandle:proc  
extrn WriteConsoleA:proc

main proc
    sub rsp, 48

    mov rcx, -11
    call GetStdHandle
    mov r12, rax

    mov rcx, r12
    lea rdx, msg
    mov r8, 15
    lea r9, [rsp+32]
    mov qword ptr [rsp+32], 0
    mov qword ptr [rsp+40], 0
    call WriteConsoleA

    add rsp, 48
    xor rcx, rcx
    call ExitProcess
main endp

end

Why Bother With This?

  • No runtime dependencies.
  • You’re fully in control.
  • You get direct access to Windows internals.

Call Flow:

  • Grab the console handle with GetStdHandle.
  • Use WriteConsoleA to print your message.
  • Clean up and exit via ExitProcess.

Build it:

ml64 /c hello_winapi.asm
link /SUBSYSTEM:CONSOLE /ENTRY:main hello_winapi.obj kernel32.lib

This is as raw as Windows programming gets.


Final Thoughts

Assembly isn’t going away—and honestly, it’s not as scary as people make it sound. It’s just precise. More like this coming soon. Until then, keep it low-level and as always, may your analysis be quick and your code easy to crack!