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.