Direct Systemcalls to Linux from Rust Code (x86_64)
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 🙂
0 Comments