Writing and running your first Assembly Language code
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:
- Define the Text to Print: We need to store the message “Hello World!” in memory because assembly directly accesses memory to display text.
- 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. - 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 exitData Section:
- We declare the message “Hello World!” in the
.datasection. This message is stored as a string using thedb(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
_startlabel 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
movinstruction to load registers with specific values for asys_writesystem call. eax = 4: System call number forsys_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 thesys_writecommand.
Exiting the Program:
eax = 1: System call number forsys_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:
- We will add space in memory to hold the user’s input.
- We will display a prompt message
- We’ll use a system call to read user input from the keyboard.
- 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 exitThe 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.asmOpen 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.asmHere, -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:
./program3. 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.oHere, -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.x4. 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.
ASMis the assembler (NASM in this case).ASM_FLAGSincludes options for NASM, such as-f elf64for 64-bit object files.TARGETis the name of the executable to produce.OBJECTis the object file name.
Rules:
- all: This is the default target. Running
makewill build the executable and then run it (./program). - $(OBJECT): This target assembles
program.asmintoprogram.o. - $(TARGET): This target links the object file to create the final executable.
- clean: This target removes any generated files (
program.oandprogram) to allow a clean build.
To build and run the Makefile just run:
makeThis 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 🎉










