I/O Operations in Assembly
Perform input and output in x86 assembly using raw Linux system calls — reading stdin, writing stdout and stderr, and reading and writing files with NASM and Docker
In a high-level language, input and output hide behind friendly functions: print, input, open, read. In assembly there is no standard library and no printf — every byte that leaves or enters your program travels through a system call, a direct request to the operating system kernel. Understanding I/O in assembly means understanding the exact contract each syscall expects: which register holds the call number, which hold the arguments, and what comes back.
On 32-bit Linux, that contract is simple and consistent. You place the syscall number in eax, the arguments in ebx, ecx, and edx (in that order), and execute int 0x80 to hand control to the kernel. When the call returns, eax holds the result — bytes transferred, a file descriptor, or a negative error code. This tutorial uses the same 32-bit int 0x80 convention as the rest of this series.
Every I/O operation here is built from just five syscalls: sys_write (4), sys_read (3), sys_open (5), sys_close (6), and sys_exit (1). The Hello World page already used sys_write to print a fixed string. Here we go further: printing numbers, reading keyboard input, and creating and reading real files on disk — all without a single library call.
Formatted Output to stdout and stderr
Output is more than printing a constant string. Programs interleave labels, computed numbers, and status messages, and they distinguish normal output (file descriptor 1, stdout) from diagnostics (file descriptor 2, stderr). Because assembly has no built-in way to turn a number into text, we bring back the print_int helper from the functions tutorial: it divides by ten repeatedly, collecting ASCII digits into a buffer.
Create a file named output.asm:
| |
The only difference between writing to the screen and writing to the error stream is the value in ebx: 1 for stdout, 2 for stderr. Both are just file descriptors the kernel opened for you before _start ran. Redirecting them separately (./program 2>errors.log) works precisely because they are distinct descriptors.
Reading Input from stdin
Reading is sys_read (call number 3), the mirror image of sys_write. You give it a file descriptor to read from (0 is stdin), a buffer to fill, and a maximum byte count. Crucially, sys_read returns the number of bytes it actually read in eax — you must use that count, not the buffer size, when you echo the data back, because the user rarely types exactly as many characters as your buffer can hold.
Create a file named input.asm:
| |
Notice mov esi, eax immediately after the read: we preserve the returned length before any other syscall overwrites eax. When the user types Ada and presses Enter, sys_read returns 4 (three letters plus the newline), so echoing esi bytes reproduces the name and its newline exactly.
Writing to a File
Files require one more step than console I/O: you must open the file to obtain a file descriptor before you can write to it, and close it afterward. sys_open (call number 5) takes a null-terminated path in ebx, a set of flags in ecx, and permission bits in edx. It returns a fresh file descriptor in eax, which you then feed to the same sys_write you already know.
Create a file named file_write.asm:
| |
The flag value 0o1101 is octal for three combined constants: O_WRONLY (1, write-only), O_CREAT (0o100, create the file if it does not exist), and O_TRUNC (0o1000, empty an existing file first). The permission argument 0o644 is the familiar Unix mode rw-r--r--, used only when the file is newly created. After this program runs, a real file named greeting.txt appears in your working directory.
Reading from a File
Reading a file combines everything so far: open with O_RDONLY, read into a buffer, then write those bytes to stdout so you can see them. As with stdin, sys_read tells you how many bytes it actually delivered, which you pass straight to sys_write.
Create a file named file_read.asm:
| |
This program reads the file created by file_write.asm, so run that example first. The pattern — open, read the returned count, act on exactly that many bytes, close — is the foundation of all file processing in assembly. Real programs loop on sys_read until it returns 0 (end of file), but a single read suffices for a small file like this one.
Running with Docker
Each example is a complete, standalone program. Assemble and run them with the same NASM image used throughout this series:
| |
Expected Output
output.asm (the last line is written to stderr, which the terminal shows alongside stdout):
=== System Report ===
Answer: 42
note: this line went to stderr
input.asm (with Ada provided on stdin):
Enter your name: Hello, Ada
file_write.asm:
Wrote greeting.txt
file_read.asm:
File contents:
Written by assembly!
Key Concepts
- All I/O is system calls: with no standard library, every read and write is a direct kernel request —
eaxholds the call number,ebx/ecx/edxhold the arguments, andint 0x80makes the call. - File descriptors are just numbers:
0is stdin,1is stdout,2is stderr, andsys_openhands you new descriptors for files.sys_writeandsys_readtreat them all identically. - Always use the returned count:
sys_readandsys_writereturn the number of bytes actually transferred ineax. Echo or process that count, never assume the whole buffer was filled. - Preserve results before the next syscall: a returned length or file descriptor sits in
eax, which the nextint 0x80will overwrite — copy it into a register likeesifirst. - Files need open and close:
sys_openreturns a descriptor;sys_closereleases it. The open flags (O_WRONLY,O_CREAT,O_TRUNC) and permission bits (0o644) are combined numeric values you pass inecxandedx. - Paths must be null-terminated:
sys_openexpects a C-style string, so end filename data with a, 0byte. - stdout vs. stderr is one register apart: writing diagnostics to descriptor
2instead of1lets users redirect errors separately — a convention built entirely from a different value inebx.
Running Today
All examples can be run using Docker:
docker pull esolang/x86asm-nasm:latest
Comments
Loading comments...
Leave a Comment