Sitemap

Writing and running your first Assembly Language code

6 min readNov 1, 2024

--

In part 2 we understood the main elements of Assembly language like system calls, directives and sections. Now we’re going to write our first Assembly program!

Hello World

Let’s start with a basic “Hello World” program in assembly. This example is written in x86 Assembly using NASM (Netwide Assembler) syntax for Linux systems. These are the basic steps:

  1. Define the Text to Print: We need to store the message “Hello World!” in memory because assembly directly accesses memory to display text.
  2. Use a System Call to Print: In assembly, we don’t have high-level functions like print. Instead, we use system calls, which allow us to ask the operating system to perform tasks like writing text to the screen.
  3. Exit the Program: We also need to use a system call to properly exit the program.
section .data
hello_msg db "Hello World!", 0xA ; Define message with a newline at the end

section .text
global _start ; Entry point for the program

_start:
; System call to write text to stdout
mov eax, 4 ; System call number for sys_write
mov ebx, 1 ; File descriptor 1 (stdout)
mov ecx, hello_msg ; Message to display
mov edx, 13 ; Length of message (including newline)
int 0x80 ; Call the kernel

; Exit system call
mov eax, 1 ; System call number for sys_exit
xor ebx, ebx ; Exit code 0
int 0x80 ; Call the kernel to exit

Data Section:

  • We declare the message “Hello World!” in the .data section. This message is stored as a string using the db (define byte) instruction.
  • We add 0xA (newline) at the end to move the cursor to a new line after printing.

Text Section:

  • We set up the _start label as the entry point of our program.
  • The word “global” is used when you want the linker to be able to know the address of some a label. The object file generated will contain a
    link to every label declared “global”. In this case, we have to declare
    “_start” as global since it is required for the code to be properly linked.

The start Label:

  • The “_start” label is essential for all Assembly programs.
  • When your program is compiled and later executed, it is executed first at the location of “_start”.
  • If the linker cannot find “_start”, it will throw an error.

Printing the Message:

  • We use the mov instruction to load registers with specific values for a sys_write system call.
  • eax = 4: System call number for sys_write.
  • ebx = 1: File descriptor for standard output (stdout).
  • ecx = hello_msg: The address of our message to display.
  • edx = 13: The length of the message (characters plus newline).

You can find a full list of system calls in part 2

  • int 0x80: This instruction calls the Linux kernel to execute the sys_write command.

Exiting the Program:

  • eax = 1: System call number for sys_exit.
  • ebx = ebx: Exit code 0, indicating successful execution.

The reason why we do ebx, ebx instead of ebx, 0 to set the exit code to 0, is because the former is more efficient.

  • int 0x80: Calls the kernel to exit the program.

Handling User Input

Now let’s modify the above program to take input from the user, store it, and then display it back. To do this:

  1. We will add space in memory to hold the user’s input.
  2. We will display a prompt message
  3. We’ll use a system call to read user input from the keyboard.
  4. Finally, we’ll display the input back using sys_write.
section .data
prompt_msg db "Enter something: ", 0 ; Prompt message to display
output_msg db "You entered: ", 0 ; Output prefix message

section .bss
user_input resb 20 ; Reserve 20 bytes for user input

section .text
global _start ; Entry point for the program

_start:
; Step 1: Display the prompt message
mov eax, 4 ; System call number for sys_write
mov ebx, 1 ; File descriptor 1 (stdout)
mov ecx, prompt_msg ; Address of prompt message
mov edx, 17 ; Length of prompt message
int 0x80 ; Call the kernel to write

; Step 2: Read user input
mov eax, 3 ; System call number for sys_read
mov ebx, 0 ; File descriptor 0 (stdin)
mov ecx, user_input ; Address to store user input
mov edx, 20 ; Max number of bytes to read
int 0x80 ; Call the kernel to read

; Step 3: Display "You entered: " message
mov eax, 4 ; System call number for sys_write
mov ebx, 1 ; File descriptor 1 (stdout)
mov ecx, output_msg ; Address of output message
mov edx, 13 ; Length of output message
int 0x80 ; Call the kernel to write

; Step 4: Display the user's input
mov eax, 4 ; System call number for sys_write
mov ebx, 1 ; File descriptor 1 (stdout)
mov ecx, user_input ; Address of user input
mov edx, 20 ; Length of user input to display
int 0x80 ; Call the kernel to write

; Exit program
mov eax, 1 ; System call number for sys_exit
xor ebx, ebx ; Exit code 0
int 0x80 ; Call the kernel to exit

The only difference in the above code to our previous one is reading the user input. Here we used the .bss section to store the uninitialized user_input value with a limit of 20 bytes using the the resb (reserve byte) instruction. We also used eax = 3 for the sys_read system call and ebx = 0 for the stdin File Descriptor.

Running Assembly code in Linux

Now I think you know how to handle basic input/ouput in Assembly. But how do we run this in a Linux terminal? Here’s a step-by-step guide:

1. Create the .asm (assembly) file

We can store our “Hello World” code in a .asm file. To do this, you can use any code editor (like Nano or Vim) in Linux. We’ll use Nano:

nano program.asm

Open the terminal and run the above command. This will open the Nano editor with a blank file called program.asm. Now you can write your Assembly code in this file.

Get Chamuditha Kekulawala’s stories in your inbox

Join Medium for free to get updates from this writer.

Then, save the file by pressing Ctrl+O, then press Enter to confirm the filename. Exit with Ctrl+X.

2. Create the .o (object) file

The assembler (NASM) has to take your .asm file and translate it into machine code that can be processed by the computer. This translation results in an intermediate .o file, which contains the binary code but is not yet executable. You can generate this file using the command using:

nasm -f elf64 -o program.o program.asm

Here, -f elf64 specifies the format (64-bit ELF format, used in Linux), and -o program.o specifies the output file name. Now you can run the executable using:

./program

3. Create the .x (executable) file

Instead of running the executable as above we can create a exe.x file, by linking one or more .o files and combine them to form a single executable file. This way you can write multiple assembly codes and execute them all at once:

ld -o exe.x program.o

Here, -o exe.x specifies the name of the output executable file. After this step, exe.x can be run directly from the terminal with:

./exe.x

4. Makefile

We can automate the above process using Makefile. This is a configuration file in Linux used by the make build automation tool, to compile and link programs. For assembly programs, a Makefile is particularly useful for organizing and automating the assembly, linking, and running steps, making it easier to manage programs.

You can create the Makefile using nano. Here’s a Makefile that assembles, links, and runs the above program:

# Compiler and flags
ASM = nasm
ASM_FLAGS = -f elf64

# Executable and object names
TARGET = program
OBJECT = program.o

# Default rule to build and run the program
all: $(TARGET)

# Rule to assemble the .asm file into an object file
$(OBJECT): program.asm
$(ASM) $(ASM_FLAGS) -o $(OBJECT) program.asm

# Rule to link the object file into an executable
$(TARGET): $(OBJECT)
ld -o $(TARGET) $(OBJECT)

# Clean rule to remove generated files
clean:
rm -f $(OBJECT) $(TARGET)

Variables: ASM, ASM_FLAGS, TARGET, and OBJECT make the Makefile easier to adjust for different programs or flags.

  • ASM is the assembler (NASM in this case).
  • ASM_FLAGS includes options for NASM, such as -f elf64 for 64-bit object files.
  • TARGET is the name of the executable to produce.
  • OBJECT is the object file name.

Rules:

  • all: This is the default target. Running make will build the executable and then run it (./program).
  • $(OBJECT): This target assembles program.asm into program.o.
  • $(TARGET): This target links the object file to create the final executable.
  • clean: This target removes any generated files (program.o and program) to allow a clean build.

To build and run the Makefile just run:

make

This Makefile automates assembling, linking, and running your assembly code, which is especially useful when you need to modify and re-run code frequently.

That’s it for this article! In the next part we’ll look into more examples including; conditional logic, loops, etc 🎉

--

--

Chamuditha Kekulawala
Chamuditha Kekulawala

Written by Chamuditha Kekulawala

AI/ML | Data Engineering | Computer Architecture | Image Processing