Assembly 101 Your First 'Hello, World!' in NASM

Assembly Advanced 📅 Published: 25/10/2025

A detailed technical article covering various cybersecurity and reverse engineering topics.

Assembly 101: Your First 'Hello, World!' in NASM

This guide introduces assembly language, the low-level language that computers understand directly. Specifically, it focuses on NASM (Netwide Assembler), a popular assembler for x86 architecture.

I cannot generate an image, but I can add a new section that explains the core concepts and components of the NASM code shown, as this is typically the next step in an assembly language tutorial.

Understanding the Code

The hello.asm file is divided into two main sections: .data (or .rodata) and .text, which are standard conventions for assembly programming.

The Data Section (section .data or section .rodata)

This section holds initialized data, such as strings or constants, that the program uses.

Component

Definition

Purpose in hello.asm

msg db 'Hello, World!', 0ah

msg is a label (variable name); db (Define Byte) allocates memory for the string and a newline (0ah or 10).

Stores the message to be printed.

len equ $ - msg

equ (Equate) defines a constant. $ refers to the current address, so len calculates the length of the string msg.

Stores the exact number of bytes the write syscall needs to print.

The Text Section (section .text)

This is where the actual executable instructions (the program logic) reside.

Component

Definition

Purpose in hello.asm

global _start

Declares the _start label as the program's entry point, making it visible to the linker (ld).

Marks where program execution begins.

_start:

The program's entry point.

The first instruction to be executed is immediately following this label.

mov rax, 1

mov (Move) copies the value 1 into the 64-bit register rax.

Sets up the first argument for the syscall: rax=1 corresponds to the write system call.

mov rdi, 1

Copies 1 into the rdi register.

Sets the file descriptor (first argument of write): rdi=1 is Standard Output (stdout).

mov rsi, msg

Copies the address of msg into rsi.

Sets the buffer address (second argument of write).

mov rdx, len

Copies the value of len into rdx.

Sets the size of the buffer (third argument of write).

syscall

Executes the system call defined by the value in rax.

Executes write(1, msg, len).

mov rax, 60

Copies 60 into rax.

Sets up for the exit system call.

mov rdi, 0

Copies 0 into rdi.

Sets the exit code (argument of exit): rdi=0 means success.

syscall

Executes exit(0).

Terminates the program cleanly.

Note on RIP-Relative Addressing (lea rsi, [msg] in PIE code):

The code block for Position-Independent Executables (PIE) uses lea rsi, [msg] instead of mov rsi, msg.

  • mov rsi, msg: Moves the absolute memory address of msg into rsi. This works for non-PIE programs.
  • lea rsi, [msg]: Loads the Effective Address of msg relative to the Instruction Pointer (RIP) into rsi. This is required for PIE executables, as their loading address is not fixed.

The "Hello, World!" example is a classic starting point for learning any programming language, and here it's used to help you:

  • Get comfortable with NASM syntax
  • Understand how instructions translate to machine-level operations
  • Begin interacting with the operating system at a low level

Advanced Topic: PIE vs No-PIE (Security and Modern Development)

The example code provided earlier demonstrated two ways to build your assembly program: one resulting in a standard, fixed-address executable (No-PIE), and one resulting in a Position-Independent Executable (PIE). Understanding this distinction is crucial for modern operating systems and security practices.

Topic

No-PIE (Fixed Address)

PIE (Position-Independent Executable)

Addressing Data

mov rsi, msg (Uses an absolute memory address)

default rel + lea rsi, [msg] (Uses RIP-relative addressing)

Program Base Address

Fixed, consistent location in memory.

Randomized by the operating system loader (ASLR).

Linking Flag

ld -no-pie hello.o -o hello

ld -pie hello.o -o hello

Security Impact

Lower. Predictable addresses make Return-Oriented Programming (ROP) attacks simpler.

Higher. Random addresses force attackers to first leak the program's base address.

Why RIP-relative Addressing Matters for PIE

For a program to be position-independent, its instructions must work correctly regardless of where the program image is loaded in memory.

  • No-PIE (mov rsi, msg): The instruction attempts to load a hardcoded, absolute memory address of msg into the rsi register. If the program is loaded at address 0x400000, the address of msg might be 0x400080. This address is fixed.
  • PIE (lea rsi, [msg]): The LEA (Load Effective Address) instruction, when used with default rel in NASM, calculates the address of msg relative to the current Instruction Pointer (RIP). It effectively says: "The address of msg is X bytes away from the instruction I am currently executing." This displacement (X) is constant, even if the entire program moves.

By using RIP-relative addressing, the code remains functional even when the operating system's loader uses Address Space Layout Randomization (ASLR) to place the executable at a different virtual memory address every time it runs.

Verifying Your Executable Type

You can quickly check whether an executable is PIE or not using standard Linux commands:

Method

Command

PIE Output

No-PIE Output

Quick Check

file ./hello

... PIE executable ...

... executable ...

Detailed Check

`readelf -h ./hello

grep Type`

Type: DYN (Shared object file)

In a modern environment, building binaries as PIE is the recommended default for security, which is why the second, slightly more complex assembly example was included in this guide.

Prerequisites

Before starting, ensure you have the following installed on your system:

  • NASM (Netwide Assembler): This is the assembler that translates your assembly code into an object file.
  • A Linker (like ld on Linux/macOS or a suitable linker on Windows): This links the object file into an executable program.
  • A Text Editor: To write your assembly code.

You can find the NASM installer and documentation at
https://www.nasm.us/docs.html

The 'Hello, World!' Source Code

We will be creating a program for a Linux-based system (using the syscall convention).

Create a new file named hello.asm and enter the following code:


For no-Pie:

run:

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

ld -no-pie hello.o -o hello

hello.asm:

; hello.asm

; An assembly program that prints "Hello, World!" and exits

; ------------------------------------------------------------------

section .data

    ; Our data section. This is where initialized variables are stored.

    msg db 'Hello, World!', 0ah ; 'db' defines bytes; 0ah is the newline character

    len equ $ - msg             ; 'equ' defines a constant: length of the message

section .text

    ; Our code section. All executable instructions go here.

    global _start               ; The linker needs to know the program's entry point

_start:

    ; 1. Write the message to standard output

    mov rax, 1                  ; syscall number 1 is 'write'

    mov rdi, 1                  ; file descriptor 1 is stdout

    mov rsi, msg                ; address of the string to write

    mov rdx, len                ; number of bytes to write

    syscall                     ; execute the system call

    ; 2. Exit the program

    mov rax, 60                 ; syscall number 60 is 'exit'

    mov rdi, 0                  ; exit code 0 (success)

    syscall                     ; execute the system call

With Pie:

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

ld -pie hello.o -o hello

hello.asm:

default rel            ; RIP-relative by default

section .rodata        ; constants go here

msg:    db "Hello, World!", 10

len     equ $ - msg

section .text

global _start

_start:

    ; write(1, msg, len)

    mov     rax, 1

    mov     rdi, 1

    lea     rsi, [msg]    ; RIP-relative address

    mov     rdx, len

    syscall

    ; exit(0)

    mov     rax, 60

    xor     rdi, rdi

    syscall

Compilation and Execution

The process involves two main steps: assembling the source code and linking the object file.

Here is a brief comparison of the command line tools you will use:

Step

Command Line Tool

Output

Assembling

nasm

Object File (.o or .obj)

Linking

ld (Linux)

Executable File

1. Assemble the Code

The NASM assembler translates the human-readable assembly code into machine-readable object code.

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

  • nasm: The NASM executable.
  • -f elf64: Specifies the output format (ELF 64-bit for modern Linux systems).
  • hello.asm: The input source file.
  • -o hello.o: Specifies the name of the output object file.

2. Link the Object File

The linker combines the object file with any necessary system libraries and prepares the final executable program.

ld hello.o -o hello

  • ld: The Linux linker.
  • hello.o: The input object file.
  • -o hello: Specifies the name of the final executable file.

3. Run the Program

Finally, execute your assembly program:

./hello

If successful, you will see the output:

Hello, World!

If you encounter any issues, please refer to the NASM documentation,
Or just be lazy and ask some AI.