This post is a little follow-up of the Hello World in FreeBSD Assembly tutorial.
At the end of the previous episode, I’ve suggested that you write an assembly program that writes “hello, world!\n” into a file. This is exactly what we’ll do here.
The C Program
This is the program that we’ll convert into assembly language:
/* hello1.c -- write hello world into a file */
#include <fcntl.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
const char *filename = "/var/tmp/hello.dat";
const char *message = "hello, world!\n";
const size_t message_len = 14; /* number of bytes in message, without \0 */
int
main (int argc, char *argv[])
{
int fd;
fd = open(filename, O_CREAT | O_WRONLY, 0600);
if (fd != -1) {
write(fd, message, message_len);
close(fd);
}
return 0;
}
It’s a nice little program. Note that we check for errors of open(2), but not of write(2) or close(2). This is for simplicity only.
The FreeBSD/amd64 version
In the following program, we make use of GAS macros that we didn’t introduce previously. Here is the code:
// hello1_amd64.S -- write hello world into a file. FreeBSD/amd64 version.
/* Some constants */
// The following syscall IDs are from /usr/src/sys/kern/syscalls.master
.equiv SYS_OPEN, 5 /* OPEN syscall */
.equiv SYS_WRITE, 4 /* WRITE syscall */
.equiv SYS_CLOSE, 6 /* CLOSE syscall */
.equiv SYS_EXIT, 1 /* EXIT syscall */
// Flags for the open(2) call are from <fcntl.h>
.equiv O_FLAGS, 0x0200 | 0x0001 /* O_CREAT | O_WRONLY */
.equiv S_FLAGS, 0600 /* S_IRUSR | S_IWUSR (0600) */
/*
* Syscalls implemented as macros for max performance.
*
* All syscall macros use (a fraction of the) x86-64 ABI registers
* %rdi, %rsi, %rdx, %r10, %r8 and %r9,
* and of course %rax for the syscall ID.
*
* The result of the syscall (if any) is then in %rax.
* In case of errors, the CARRY BIT is set, and %rax contains errno.
*
* The syscall instruction clobbers %rcx as it enters the kernel
* (see AMD/Intel manual entry for SYSCALL). You need to save it
* explicitly.
*/
.macro open filename, flags1, flags2
movq \filename, %rdi
movq \flags1, %rsi
movq \flags2, %rdx
movq $SYS_OPEN, %rax
syscall
.endm
.macro write fd, buf, len
movq \fd, %rdi
movq \buf, %rsi
movq \len, %rdx
movq $SYS_WRITE, %rax
syscall
.endm
.macro close fd
movq \fd, %rdi
movq $SYS_CLOSE, %rax
syscall
.endm
.macro exit retcode
movq \retcode, %rdi
movq $SYS_EXIT, %rax
syscall
.endm
/* The .rodata section contains filename and message */
.section .rodata
filename:
.string "/var/tmp/hello.dat"
message:
.ascii "hello, world!\n"
message_len = . - message
/* Code section */
.text
.global _start
_start:
/* set up a stack frame */
pushq %rbp
movq %rsp, %rbp
open $filename, $O_FLAGS, $S_FLAGS
jc bye /* carry bit set: an error occured */
/* %rax contains errno value now! */
/* We silently quit if open() failed. */
movq %rax, %rbx /* open() succeeded: save fd in %rbx */
write %rbx, $message, $message_len
close %rbx
bye:
exit $0
/* NOT REACHED */
popq %rbp
There are a few things to note about this program:
- The constants O_CREAT and O_WRONLY are right out of the C header file. We can take the hex notation from there. The octal notation 0600 for the access rights is also acceptable.
- Every needed syscall is implemented as a macro. We define the following macros: open, write, close and exit.
- The
.rodatasections contains our C string constants. Note that the path filename is \0-terminated, as required by the open(2) syscall, while the message string isn’t (we use.asciiinstead of.asciizor.string). This is okay, because we specify exactly how many bytes to write in message_len. - The main program simply calls the macros open, write, close and exit in order.
- The temporary variable fd in the C program corresponds here to the
%rbxregister. Had we not enough registers, we could also have stored that variable on the stack, but it was not necessary here. Note::%rbxis none of the registers affected by our macros!
The most important lesson to learn in this tutorial is how errors are being reported in assembly language. In C, an error in open(2) is reported by returning -1 and setting the specific error code in the variable errno.
In FreeBSD/amd64 assembly, the kernel reports errors by setting the Carry Flag of the rFLAGS status word, and storing errno in %rax.
So how do we detect whether a regular return value (in %rax) or an error occurred? We simply use a conditional jump jc (“jump if carry bit set”) immediately after the syscall:
open $filename, $O_FLAGS, $S_FLAGS
jc bye /* carry bit set: an error occured */
/* %rax contains errno value now! */
/* We silently quit if open() failed. */
movq %rax, %rbx /* open() succeeded: save fd in %rbx */
We just need to make sure that the carry flag isn’t accidentally reset by instructions following syscall, before testing it with jc (or jnc).
So let’s test the program:
% as --64 -o hello1_amd64.o hello1_amd64.S % ld -o hello1_amd64 hello1_amd64.o % ./hello1_amd64 % cat /var/tmp/hello.dat hello, world! % hd /var/tmp/hello.dat 00000000 68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 0a |hello, world!.| 0000000e
Looks promising, and good. Let’s trace the call with ktrace(1) and kdump(1), as we’ve learned previously:
% ktrace ./hello1_amd64
% kdump
2490 ktrace RET ktrace 0
2490 ktrace CALL execve(0x7fffffffe94f,0x7fffffffe690,0x7fffffffe6a0)
2490 ktrace NAMI "./hello1_amd64"
2490 hello1_amd64 RET execve 0
2490 hello1_amd64 CALL open(0x40010e,O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR)
2490 hello1_amd64 NAMI "/var/tmp/hello.dat"
2490 hello1_amd64 RET open 3
2490 hello1_amd64 CALL write(0x3,0x400121,0xe)
2490 hello1_amd64 GIO fd 3 wrote 14 bytes
"hello, world!
"
2490 hello1_amd64 RET write 14/0xe
2490 hello1_amd64 CALL close(0x3)
2490 hello1_amd64 RET close 0
2490 hello1_amd64 CALL exit(0)
It can’t get shorter than that.
But how can we trigger an error condition on open(2) so we can test that path of execution through our program? We simply set the permissions on the output file /var/tmp/hello.dat to read-only, so the file can’t be opened for writing:
% chmod 400 /var/tmp/hello.dat % ktrace ./hello1_amd64 % kdump 2494 ktrace RET ktrace 0 2494 ktrace CALL execve(0x7fffffffe94f,0x7fffffffe690,0x7fffffffe6a0) 2494 ktrace NAMI "./hello1_amd64" 2494 hello1_amd64 RET execve 0 2494 hello1_amd64 CALL open(0x40010e,O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR) 2494 hello1_amd64 NAMI "/var/tmp/hello.dat" 2494 hello1_amd64 RET open -1 errno 13 Permission denied 2494 hello1_amd64 CALL exit(0)
As you can see, the write and close syscalls were not invoked this time. This is exacly what we wanted.
The FreeBSD/i386 version
Now, let’s move to the 32-bit version of the same program. Here’s the code:
// hello1_i386.S -- write hello world into a file. FreeBSD/i386 version.
/* Some constants */
// The following syscall IDs are from /usr/src/sys/kern/syscalls.master
.equiv SYS_OPEN, 5 /* OPEN syscall */
.equiv SYS_WRITE, 4 /* WRITE syscall */
.equiv SYS_CLOSE, 6 /* CLOSE syscall */
.equiv SYS_EXIT, 1 /* EXIT syscall */
// Flags for the open(2) call are from <fcntl.h>
.equiv O_FLAGS, 0x0200 | 0x0001 /* O_CREAT | O_WRONLY */
.equiv S_FLAGS, 0600 /* S_IRUSR | S_IWUSR (0600) */
/*
* Syscalls implemented as macros for max performance.
*
* All syscall macros use the stack for the arguments of the syscalls,
* and of course %eax for the syscall ID.
*
* The result of the syscall (if any) is then in %eax.
* In case of errors, %eax is negative.
*/
.macro open filename, flags1, flags2
sub $0x10, %esp
movl \filename, (%esp)
movl \flags1, 0x4(%esp)
movl \flags2, 0x8(%esp)
movl $SYS_OPEN, %eax
call do_syscall
add $0x10, %esp
.endm
.macro write fd, buf, len
sub $0x10, %esp
movl \fd, (%esp)
movl \buf, 0x4(%esp)
movl \len, 0x8(%esp)
movl $SYS_WRITE, %eax
call do_syscall
add $0x10, %esp
.endm
.macro close fd
sub $0x10, %esp
movl \fd, (%esp)
movl $SYS_CLOSE, %eax
call do_syscall
add $0x10, %esp
.endm
.macro exit retcode
sub $0x10, %esp
movl \retcode, (%esp)
movl $SYS_EXIT, %eax
call do_syscall
add $0x10, %esp
.endm
/* The .rodata section contains filename and message */
.section .rodata
filename:
.string "/var/tmp/hello.dat"
message:
.ascii "hello, world!\n"
message_len = . - message
/* Code section */
.text
.global _start
_start:
/* set up a stack frame */
pushl %ebp
movl %esp, %ebp
open $filename, $O_FLAGS, $S_FLAGS
cmpl $0, %eax
js bye /* negative %eax: an error occured */
/* %eax contains -errno value now! */
/* We silently quit if open() failed. */
movl %eax, %ebx /* open() succeeded: save fd in %ebx */
write %ebx, $message, $message_len
close %ebx
bye:
exit $0
/* NOT REACHED */
popl %ebp
do_syscall:
int $0x80
jnc ret_from_syscall
neg %eax
ret_from_syscall:
ret
Again, we implement the syscalls as GAS macros. Unlike what we’ve did in the previous tutorial, invoke the kernel in a little subroutine of its own (do_syscall), so we don’t have to push and pop a dummy placeholder on the stack (the return address is that value here).
However, there’s a little problem here: the FreeBSD/i386 kernel signals an error by setting the Carry Flag (CF in eFLAGS), and saving errno in %eax. Because the carry flag is clobbered in a call/ret environment, we resort to a trick: we test the carry flag immediately after the syscall, and invert the value of %eax if it is set:
do_syscall:
int $0x80
jnc ret_from_syscall
neg %eax
ret_from_syscall:
ret
In other words, we store -errno in %eax if an error occured. In the caller code, we test for negative values of %eax with the js instruction (jump if sign negative). Of course, we need to execute an instruction which loads %eax‘s sign into the eFLAGS register before js; cmpl is a good candidate:
open $filename, $O_FLAGS, $S_FLAGS
cmpl $0, %eax
js bye /* negative %eax: an error occured */
/* %eax contains -errno value now! */
/* We silently quit if open() failed. */
movl %eax, %ebx /* open() succeeded: save fd in %ebx */
Okay, let’s test this program too:
$ as --32 -o hello1_i386.o hello1_i386.S
$ ld -o hello1_i386 hello1_i386.o
$ ktrace ./hello1_i386
$ kdump
68429 ktrace RET ktrace 0
68429 ktrace CALL execve(0xbfbfedc7,0xbfbfeca4,0xbfbfecac)
68429 ktrace NAMI "./hello1_i386"
68429 hello1_i386 RET execve 0
68429 hello1_i386 CALL open(0x80480fa,O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR)
68429 hello1_i386 NAMI "/var/tmp/hello.dat"
68429 hello1_i386 RET open 3
68429 hello1_i386 CALL write(0x3,0x804810d,0xe)
68429 hello1_i386 GIO fd 3 wrote 14 bytes
"hello, world!
"
68429 hello1_i386 RET write 14/0xe
68429 hello1_i386 CALL close(0x3)
68429 hello1_i386 RET close 0
68429 hello1_i386 CALL exit(0)
Again, everything looks good here. Of course, we also need to test the error condition of open(2):
$ chmod 400 /var/tmp/hello.dat $ ktrace ./hello1_i386 $ kdump 68432 ktrace RET ktrace 0 68432 ktrace CALL execve(0xbfbfedc7,0xbfbfeca4,0xbfbfecac) 68432 ktrace NAMI "./hello1_i386" 68432 hello1_i386 RET execve 0 68432 hello1_i386 CALL open(0x80480fa,O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR) 68432 hello1_i386 NAMI "/var/tmp/hello.dat" 68432 hello1_i386 RET open -1 errno 13 Permission denied 68432 hello1_i386 CALL exit(0)
Everything looks good: no spurious syscalls between open and exit.
Conclusion
The most important lesson to remember here is how errors are reported. FreeBSD/amd64 and FreeBSD/i386 syscalls save the return value in %rax or %eax, but if an error occurs, both set the Carry Flag and put errno in %rax or %eax. We need to check this carry flag (with jc or jnc) as soon as possible, before it is reset by other instructions.
For the help please use http://www.google.com