Direct Systemcalls to Linux from Rust Code (x86_64)

Published by Philipp Schuster on

Screenshot of Sourcecode

What I show in this blog post is nothing new or unique, but it is something I wish someone would have shown me in this simplicity in my second or third semester at university.

In this post, I briefly explain how you can talk to Linux without the libc or the Rust standard library (which uses libc behind the scenes most probably). Furthermore, I give you pointers to the corresponding syscall code in Linux, which gives you details about the syscall ABI.

In my example on Github, I show how you can use write(), open(), and read() without any library! Let us take a look at the Linux syscall ABI first:

https://github.com/torvalds/linux/blob/master/arch/x86/entry/entry_64.S#L69

Here we find what role each specific register has during a syscall, i.e. what registers contains what parameter or the syscall number. To find all syscall numbers and the corresponding handler function inside Linux, we can use this link:

https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl

Finally we need to know the definition of each syscall handler, i.e. what argument has what meaning. We can find all the definitions here:

https://github.com/torvalds/linux/blob/master/include/linux/syscalls.h

Note that during the syscall, each argument is just a u64, i.e. the width of a register. write(), open(), and read() have in common that every one takes three arguments. Let’s start by writing this generic syscall function:

unsafe fn syscall_3(num: u64, arg1: u64, arg2: u64, arg3: u64) -> i64 {
    let res;
    asm!(
        // there is no need to write "mov"-instructions, see below
        "syscall",
        // from 'in("rax")' the compiler will
        // generate corresponding 'mov'-instructions
        in("rax") num,
        in("rdi") arg1,
        in("rsi") arg2,
        in("rdx") arg3,
        lateout("rax") res,
    );
    res
}

We use this to construct each syscall.

/// Linux write system call. Works like `write()` in C.
fn sys_write(fd: u64, data: *const u8, len: u64) -> i64 {
    unsafe { syscall_3(LinuxSysCalls::Write as u64, fd, data as u64, len) }
}

/// Opens a file. Works like `open()` in C.
fn sys_open(path: *const u8, flags: u32, umode: u16) -> i64 {
    unsafe {
        syscall_3(
            LinuxSysCalls::Open as u64,
            path as u64,
            flags as u64,
            umode as u64,
        )
    }
}

/// Opens a file. Works like `read()` in C.
fn sys_read(fd: u64, buf: *mut u8, size: u64) -> i64 {
    unsafe {
        syscall_3(
            LinuxSysCalls::Read as u64,
            fd,
            buf as u64,
            size as u64,
        )
    }
}

Now we can use the functions like this in our Rust code:

    const STDOUT_FD: u64 = 1;

    let string = b"hello world\n";
    let res = sys_write(STDOUT_FD, string.as_ptr(), string.len() as u64);

It works, but it’s a little bit unhandy. That’s the reason why standard libraries, like libc or std in Rust exist 🙂


Philipp Schuster

Hi, I'm Philipp and interested in Computer Science. I especially like low level development, making ugly things nice, and de-mystify "low level magic".

0 Comments

Leave a Reply

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