Intro to Assembly

Intro to Assembly

Published on December 30, 2025

I've always wanted to learn Assembly, the low-level programming language. I taught myself to code when I was fairly young, which made me a bit of a lone wolf when I was a kid. I didn't have any other friends who were into programming or computer science. I had friends who had built their own PCs, and a few friends who got their A+ certification near the end of high school, but that was the extent of their involvement in the computer world.

Sometimes, when I could get my friends to talk about programming or computer science, the assembly language would come up. There was this mystique around it and how difficult it is to learn. I assumed they were right and I never really tried. Then, some years later, after I graduated college, I decided to try. I remember looking for a good book, and all I could find was some academic version of Assembly that some professor had come up with. I didn't want to learn some academic version of Assembly; I wanted to learn the real thing.

Fast forward to 2025, and now you can ask Gemini or any frontier model to teach you real Assembly. So that's what we're going to do here. We'll only get as far as writing a "hello world" program.

For the purpose of this tutorial, you will need to be on Microsoft Windows 11 and you'll need to install Windows Subsystem for Linux (WSL).

Setting up WSL

Follow these steps on Windows 11 to install WSL:

  1. Right-click on the Start menu and select "Windows PowerShell (Admin)" or "Command Prompt (Admin)".
  2. Type the following command and press enter:
wsl --install
  1. Once it is finished, restart your computer.
  2. When the restart finishes, you should see a Linux terminal window appear asking you to create a Unix username and password.

From here on out, when you want to enter the WSL environment, open up a PowerShell or Command Prompt and type wsl and press enter.

Basics

Gemini offered me a quite useful mental model to think about how Assembly works.

Gemini Pro 3
  • The CPU (The Brain): It does the thinking.
  • Registers (The Hands): The CPU cannot work directly on data sitting in RAM (the warehouse). It must first pick that data up and hold it in its "hands." These hands are called Registers.
    • Registers are tiny, incredibly fast storage slots directly inside the CPU.
    • In modern 64-bit processors (x86-64), these have names like rax, rbx, rcx, etc.
  • The Stack: A scratchpad area in memory for temporary notes (variables).
  • Instructions: The verbs. Assembly is a list of imperative commands: "Move this here," "Add these two," "Jump to that line."

In my previous half-hearted attempts to learn Assembly, I read about registers but they seemed kind of redundant and pointless. What I was missing is the fact that the CPU cannot work directly on data sitting in the random access memory (RAM). The CPU must first fetch data from the RAM and store it in a register, and operate on it there.

You'll have to decide how you want to manage your files. Your WSL environment is just a bootable .vhdx image sitting C:\Users<username>\AppData\Local\wsl\{nnnn-nnnn-nnnn-nnnn-nnnn}\ext4.vhdx

This means when you've booted into WSL, you've booted into this image, and there is a virtual hard drive inside that file where your WSL files are stored. You can store your files in that virtual hard drive, or you can reach your Windows filesystem by pathing to the C mount, like so:

cd /mnt/c

Because I like being able to use a full Windows IDE (e.g., VSCode or Google Antigravity), I have chosen to store my files in /mnt/c/Users/<username>/Documents/Dev/

I recommend doing the same so you can use a full Windows IDE. But the choice is yours. Either way, create a directory for your project and navigate to it.

Our First Program

Create a new file called hello.asm and paste the following code into it:

section .data
    ; Define our data.
    ; db = define byte (creates raw bytes in memory)
    ; 0xA is the hex code for a newline character
    message db "Hello, World!", 0xA
    length  equ $-message          ; Calculate string length automatically

section .text
    global _start                  ; Must declare where the code starts for the linker

_start:
    ; --- STEP 1: PRINT THE MESSAGE ---
    ; To do this, we use a "System Call" (syscall).
    ; We put specific numbers in specific registers so the OS knows what to do.

    mov rax, 1          ; The syscall ID for 'sys_write' is 1
    mov rdi, 1          ; File descriptor 1 is 'stdout' (standard output)
    mov rsi, message    ; Put the address of our message into rsi
    mov rdx, length     ; Put the length of the message into rdx
    syscall             ; Trigger the OS to read these registers and act

    ; --- STEP 2: EXIT THE PROGRAM ---
    ; If we don't exit cleanly, the program will crash (segfault)
    ; after running the previous lines.

    mov rax, 60         ; The syscall ID for 'sys_exit' is 60
    mov rdi, 0          ; The exit code (0 means success)
    syscall             ; Trigger the OS

Make sure to save.

To understand the code above, it is useful to know the anatomy of an Assembly instruction:

Gemini Pro 3

Most x86 Assembly instructions follow this syntax: COMMAND [DESTINATION], [SOURCE] Examples:

  • mov rax, 50 -> "Move the number 50 into the rax register."
  • add rax, rbx -> "Add the value in rbx to the value in rax, and store the result in rax."
  • jmp loop_start -> "Jump to the label named loop_start."

Now we need to run the code. We will need three steps:

  1. Assemble the code: turn hello.asm into an object file (hello.l)
  2. Link the object file: turn hello.o into an executable file (hello)
  3. Run the executable

First, we will need to install NASM (Netwide Assembler). Issue the following command inside WSL:

sudo apt update
sudo apt install nasm build-essential

Next, we will need to assemble the code. Issue the following command inside WSL:

nasm -f elf64 hello.asm -o hello.o

Next, link it:

ld hello.o -o hello

Finally, run it:

./hello

You should see it print "Hello, World!" to the terminal.

Hello, World!

Congratulations on building your first Assembly program!

Explaining the Code

So what exactly did we just do? Well, let's step through the code line by line.

First, it is useful to know about the Applicaton Binary Interface (ABI). This is a strict contract that defines how a program should interact with the OS. For the purposes of this tutorial, we are using System V AMD64 ABI, which is the standard ABI for 64-bit Linux.

Now, let's dive into the code.

section .data
    message db "Hello from the registers!", 0xA
    len equ $-message

section .data tells the operating system (OS) to define a piece of memory to read and write from, but not to execute from.

message db "Hello from the registers!", 0xA assigns a string of bytes to a memory location called message. db means "define byte".

0xA is a hex code for a newline character.

len equ $-message is a bit tricky, and I'll admit I don't fully understand it yet, but $ is a special symbol that represents the current position in memory, equ means "equal", 0xA is the hexadecimal code for a newline character, and $-message means "subtract the address of message from the current position in memory". The result of subtracting the address of message from the current position in memory is the length of the string. There is something more interesting happening here that I want to maybe cover in a future post.

section .text
    global _start

section .text tells the OS to define a piece of memory to read or execute from, but not write to.

global _start tells the OS where the code starts. This is equivalent to main() in C++.

_start:
    mov rax, 1

mov rax, 1 puts the number 1 into the rax register. rax is the Accumulator Register, and it is used to store the result of an operation. This tells the Kernel that we want to call the sys_write function. If we had put a 2 in the rax register, we would tell the Kernel that we want to call the sys_open function.

    mov rdi, 1

mov rdi, 1 puts the number 1 into the rdi register. The ABI states that the first argument (Argument 1) for any system call (syscall) must go in rdi. This is important because sys_write requires 3 arguments: sys_write(int file_descriptor, const char *buffer, size_t count)

There's different values we could have put into rdi:

  • 0 = Standard Input (stdin)
  • 1 = Standard Output (stdout)
  • 2 = Standard Error (stderr)

As you can see, it makes sense that we put 1 into rdi because we want to write to stdout.

    mov rsi, message

The ABI states that the second argument (Argument 2) for any system call (syscall) must go in rsi. As we saw above, the second argument for sys_write is an address pointing to the message we want to write.

    mov rdx, len

The ABI states that the third argument (Argument 3) must go in rdx. As we saw above, the third argument for sys_write is the length of the message we want to write.

    syscall

syscall is a special instruction that triggers the OS to read the registers and act. Or in simpler terms, it says "do it, do the thing".

Gemini offered a more detailed explanation of what syscall really does:

Gemini Pro 3
  1. The CPU pauses your program.
  2. It switches from User Mode (Ring 3) to Kernel Mode (Ring 0).
  3. The Kernel wakes up, looks at rax to see what you want (Write).
  4. It looks at rdi, rsi, and rdx to get the details.
  5. It prints to the screen.
  6. It returns control to your program (back to Ring 3).

This whole business about User Mode, Kernel Mode, Ring 0, and Ring 3 requires a bit more explanation, so I'll consider covering that topic in a future post.

    mov rax, 60

This line puts the number 60 into the rax register. Putting a 60 in this register tells the OS that we want to call the sys_exit function.

    mov rdi, 0
    syscall

Next, we move the value 0 into the rdi register to tell the operating system that we want to exit with a code of 0. This means we exited successfully. If we exited with a code of 1, it would mean we exited with an error. Finally, as before, syscall triggers the OS to read the registers and act.

That sums up our line-by-line explanation of the code. I hope this helps you understand some of the basics of Assembly!

And now, I leave you with a lovely image that Gemini created to illustrate a memory address map for the "Hello" portion of the string we used in our program.

Memory Address Map

-Kairos Anamnesis

Comments? Send me an email! kairosanamnesis@gmail.com