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 .rodata sections 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 .ascii instead of .asciiz or .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 %rbx register. Had we not enough registers, we could also have stored that variable on the stack, but it was not necessary here. Note:: %rbx is 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.

One Comment