Portfolio / HelloAssembly

Low-Level Programming Project

HelloAssembly

NASM x86-64 Linux and Windows Actual source included below

HelloAssembly is a small low-level programming project built to compare two very direct ways of doing the same thing: printing a line to standard output in Linux and printing a line to the console in Windows. The programs are intentionally small, which makes the assembly readable enough to show directly on the project page.

What this project shows

The Linux version exports a tiny assembly routine that a C driver calls three times. That keeps the assembly focused on register setup and the `sys_write` syscall itself. The Windows version is a native console program that calls `GetStdHandle`, `WriteFile`, and `ExitProcess` directly using the Windows x64 calling convention.

The interesting part is not the output string. It is the contrast in how each operating system expects work to be described: syscall numbers and file descriptors on Linux, versus API functions, argument registers, shadow space, and imported symbols on Windows.

Linux path

Uses `rax`, `rdi`, `rsi`, and `rdx` to set up `sys_write`, then invokes `syscall` directly.

Windows path

Calls Win32 functions with `rcx`, `rdx`, `r8`, and `r9`, plus stack shadow space for `WriteFile`.

Why it works well as a showcase

The source is small enough to read in one sitting, but still exposes real platform-level details.

Repository structure

The repo includes `hello.asm`, `hello_windows.asm`, a tiny `main.c` harness, and generated outputs.

NASM x86-64 Linux syscalls Windows API C interoperability Console I/O

Key takeaways

  • The Linux example is minimal because the kernel interface is exposed directly through `syscall`.
  • The Windows example is longer because console output is handled through imported system functions.
  • RIP-relative addressing keeps the string reference position-independent in both examples.
  • The project makes calling conventions easier to understand because each file has one job and very few moving parts.

Build flow

  • Linux: assemble `hello.asm`, link it with `main.c`, then run the executable from Linux or WSL.
  • Windows: assemble `hello_windows.asm`, link against `kernel32`, then run the resulting `.exe` in PowerShell.
  • Both paths print the same `Hello, World!` message, but they get there through different OS interfaces.

Linux Example

`hello.asm`

This routine is about as direct as it gets: load the write syscall number, point at the message, provide the byte count, and hand control to the kernel.

hello.asm Linux syscall path
; hello.asm - Hand-written NASM x86-64 Linux assembly routine
; This file implements `print_arken` entirely in assembly.
; It demonstrates Linux syscall usage, RIP-relative data access, and
; non-executable stack metadata in a minimal assembly project.

global print_arken       ; export symbol for the C driver
section .data
    ; Define the message to be printed
    msg db "Hello, World!", 0xA

    ; Calculate the length of the message
    len equ $ - msg

section .text
print_arken:
    mov rax, 1          ; tell the kernel we want to write (syscall number for sys_write)
    mov rdi, 1          ; tell the kernel we want to write to stdout (file descriptor 1)
    lea rsi, [rel msg]  ; load the address of the message into rsi
    mov rdx, len        ; tell the kernel the length of the message
    syscall             ; invoke the kernel to perform the write operation
    ret                 ; return to caller

; Tell the linker that this object does not require an executable stack.
; This avoids the `/usr/bin/ld: missing .note.GNU-stack section` warning.
; The GNU stack note is a special empty section used by the linker to infer
; whether the object needs an executable stack.
section .note.GNU-stack
Example output Linux run
Hello, World!
Hello, World!
Hello, World!

Why the file stays small

Linux exposes output as a syscall, so the routine only needs to prepare registers and execute one kernel instruction.

What is useful to notice

Lines 16 through 20 are the whole write operation. `lea rsi, [rel msg]` is the key memory access here, and line 20 is the actual handoff to the kernel.

Windows Example

`hello_windows.asm`

The Windows version shows more ceremony because the program calls into imported API functions instead of issuing a raw write syscall directly from the program body.

hello_windows.asm Windows API path
extern GetStdHandle
extern WriteFile
extern ExitProcess

; This code defines a simple Windows application in x86-64 assembly using NASM syntax.
; It demonstrates how to call Windows API functions to write a message to the console.
section .data
    ; Define the message as bytes.
    ; The string is followed by 0xA
    ; which is the newline character.
    msg db "Hello, World!", 0xA

    ; Calculate the length of the message
    msg_len equ $ - msg

; The .bss section is used for uninitialized data. Here we reserve space for a variable `written`
; that will hold the number of bytes written by WriteFile. We use `resd 1` to reserve space for
; one double word (4 bytes) since WriteFile expects a pointer to a DWORD to store the number of bytes written.
section .bss
    written resd 1

; The .text section contains the executable code. We define the `main` function as the entry point of the program.
section .text
    global main


; The main function is the entry point of the program.
; It will call GetStdHandle to get the handle for standard output,
; then call WriteFile to write the message to standard output,
; and finally call ExitProcess to exit the program.
main:
    sub rsp, 40              ; Reserve shadow space and maintain 16-byte stack alignment

    mov rcx, -11             ; GetStdHandle(STD_OUTPUT_HANDLE) where STD_OUTPUT_HANDLE is -11.
                             ;      Note: -11 is STD_OUTPUT_HANDLE, -10 is STD_INPUT_HANDLE
                             ;            and -12 is STD_ERROR_HANDLE.

    call GetStdHandle        ; Get the handle for standard output, result is in rax

    mov rbx, rax             ; Save the handle in rbx for later use

    mov rcx, rbx             ; Set the handle as the first argument for WriteFile
    lea rdx, [rel msg]       ; Load the address of the message into rdx
    mov r8, msg_len          ; Set the length of the message as the third argument
    lea r9, [rel written]    ; Load the address of the variable to receive the number of bytes written

    mov qword [rsp + 32], 0  ; Set the lpOverlapped parameter to NULL (0) for synchronous operation
    call WriteFile           ; Call WriteFile to write the message to standard output

    mov ecx, 0               ; Set the exit code to 0
    call ExitProcess         ; Call ExitProcess to exit the program
Example output Windows run
Hello, World!

Why the Windows version is longer

Console output is reached through system libraries, so the code has to set up arguments for multiple calls and obey Windows x64 ABI requirements.

What stands out most

Lines 31 through 47 are mostly setup for API calls. `sub rsp, 40` and `mov qword [rsp + 32], 0` are there to satisfy call-frame rules around alignment and shadow space before invoking `WriteFile`.