Assembly Tutorial – I/O Bringing it all Together

We’ve seen in previous posts how to handle errors when writing to files and how to read and write arbitrary numbers of bytes to files. It’s time we put this all together! We are going to write a program that will read an arbitrary number of bytes from the command line and write them to a file. If our program encounters any errors it will gracefully exit with code 1. Here’s the code:

.equ BUFFER_SIZE, 20
.equ NEW_LINE, 10

.section .data
filename:
.string "outputfile\.txt"

.section .bss
.lcomm buffer_data, BUFFER_SIZE

.section .text

.globl _start
_start:

movq $2, %rax
movq $filename, %rdi
movq $0x441, %rsi
movq $0666, %rdx
syscall

cmpq $0, %rax
jl exit_with_error

movq %rax, %r9

read_from_buffer:

movq $0, %rax
movq $0, %rdi
movq $buffer_data, %rsi
movq $BUFFER_SIZE, %rdx
syscall

cmpq $0, %rax
jl exit_with_error

movq %rax, %rbx

movq $1, %rax
movq %r9, %rdi
movq $buffer_data, %rsi
movq %rbx, %rdx
syscall

cmpq $0, %rax
jl exit_with_error

decq %rbx
cmpb $NEW_LINE, buffer_data(,%rbx,1)
je exit

jmp read_from_buffer

exit:
movq $60, %rax
movq $0, %rdi
syscall

exit_with_error:
movq $60, %rax
movq $1, %rdi
syscall

Nothing we have done here is really new. We start by defining some constants and a 20 byte buffer. Then, in the text section, we open the file named “outputfile.txt”.

movq $2, %rax
movq $filename, %rdi
movq $0x441, %rsi
movq $0666, %rdx
syscall

When we open the file we use the flag value 0x441. This flag tells the kernel three things: that we want to open the file in write mode, that we want to create a file if it doesn’t exist and that we want to append to the end of a file if it does exist.

After the open system call, we check if the return value of this system call is negative:

cmpq $0, %rax
jl exit_with_error

movq %rax, %rbx

If so, we jump straight to exit_with_error, otherwise we stash the return value in r9. You have to be careful to stash values you want to keep somewhere they won’t get over written during the next system call. We don’t use rsi, rdi or rdx as we are using to pass values to the kernel. The registers rcx and r11 will, in general, have their values overwritten during a system calls and rax will contain return values. So we choose rbx and r9 as our two stash registers.

Now, once we’ve opened our file, we enter a loop. Our loop starts with the label read_from_buffer. As before, at the end of each loop we check if the last character we have read is a new line and if so, jump to the exit, otherwise we jump back to the loop start:

decq %rbx
cmpb $NEW_LINE, buffer_data(,%rbx,1)
je exit

jmp read_from_buffer

Inside this loop we have our read and write system calls:

movq $0, %rax
movq $0, %rdi
movq $buffer_data, %rsi
movq $BUFFER_SIZE, %rdx
syscall

cmpq $0, %rax
jl exit_with_error

movq %rax, %rbx

movq $1, %rax
movq %r9, %rdi
movq $buffer_data, %rsi
movq %rbx, %rdx
syscall

cmpq $0, %rax
jl exit_with_error

We now have a conditional jump statement after each of these calls. If control returns from either with a negative number in rax we jump straight to exit_with_error.

That’s it, we’ve covered everything we will need to handle input and output in x86 assembly! It’s been quite a journey. In our next few posts we’ll be trying something slightly different.

Leave a Reply

Your email address will not be published. Required fields are marked *