In this post I’ll implement ptrace support in Rust and use it to build the core of a debugger in Rust. We’ll just be getting started but eventually this will debugger will support symbolic breakpoints, watchpoints, multiple threads.

This post parallels a post from last month: Running a process under ptrace in C. These posts are part of my larger series on writing a debugger in C and Rust.

This is my first project in Rust so this will be a learning experience for me. At the end I will compare the C and Rust versions to get a better understanding of what Rust offers me as a system programmer.

ptrace

The first thing we need is access to ptrace from Rust. I see that the Codius team has put together a crate for Rust and written up a post on how to use ptrace in Rust. There is also the nix crate which supports many UNIX library functions though it is currently lacking support for ptrace.

I started with Codius’ ptrace crate using their blog post as a guide but found that the crates are not working with Rust 1.0. ptrace wasn’t so much problem as the posix-ipc crate. I spent a little time trying, unsuccessfully, to fix the problems before finding the nix crate. I decided that nix is the crate to use going forward and switched it. But, this means that I’ll have to implement my own support for ptrace(). This will be a good oppourtunity to learn to use FFI.

nix

If I’m going to modify nix I’ll need to clone the repo and get setup to work with it:

$ git clone https://github.com/carllerche/nix-rust.git
Cloning into 'nix-rust'...
remote: Counting objects: 18269, done.
remote: Total 18269 (delta 0), reused 0 (delta 0), pack-reused 18269
Receiving objects: 100% (18269/18269), 5.00 MiB | 113.00 KiB/s, done.
Resolving deltas: 100% (16563/16563), done.
Checking connectivity... done.

It builds:

$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index`
   Compiling bitflags v0.1.1
   Compiling libc v0.1.8

And the tests pass

$ cargo test
   Compiling nix-test v0.0.1 (file:///home/joseph/src/rust/nix-rust)
   Compiling rand v0.3.8
   Compiling nix v0.3.7 (file:///home/joseph/src/rust/nix-rust)
     Running target/debug/nix-f265bd1612a6fee4

running 12 tests
...

test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured

     Running target/debug/test-198288d42cd47eaa

running 24 tests
...

test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured

   Doc-tests nix

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

With Rust 1.1.0 there is a warning but I won’t worry about it for now:

test/test_stat.rs:76:5: 76:18 warning: use of deprecated item: replaced with std::os::unix::fs::symlink and std::os::windows::fs::{symlink_file, symlink_dir}, #[warn(deprecated)] on by default
test/test_stat.rs:76     fs::soft_link("bar.txt", str::from_utf8(linkname).unwrap()).unwrap();
                         ^~~~~~~~~~~~~

ptrace in nix

Time to write some Rust code, I need to add ptrace support for Rust.

I’ll create a new file called src/sys/ptrace.rs and I’ll use src/sys/wait.rs and src/sys/mman.rs as a guide to make sure I get the styling to match and all that. I’ll admit, I’m procrastinating by writing up what I’m doing and looking at existing code and tests. I haven’t written any substantial Rust code yet but it’s time to dive in…

I started writing some code in ptrace.rs, any code, junk code just to see cargo build fail. It didn’t fail. I needed to figure out how to add my files to nix. Eventually, I looked through src/lib.rs and saw that it “included” other files with lines like

pub mod fcntl;

I’m guessing “exported other modules” is a more appropriate description than “included”. I specifically noticied the line:

pub mod sys;

So, sys is a module on its own. And my new ptrace module is a part of sys. So, how do I include ptrace in sys? Eventually, I found sys/mod.rs and added

@@ -34,3 +34,5 @@ pub mod mman;
 pub mod uio;

 pub mod time;
+
+pub mod ptrace;

and was able to make the build fail:

$ cargo build
   Compiling nix v0.3.7 (file:///home/joseph/src/rust/nix-rust)
src/sys/ptrace.rs:1:5: 1:18 error: unresolved import `ForcedFailure`. There is no `ForcedFailure` in `???`
src/sys/ptrace.rs:1 use ForcedFailure;
                        ^~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `nix`.

To learn more, run the command again with --verbose.

Now to make the build pass and do something at least slightly useful. I’ve defined all the ptrace() requests:

#[cfg(all(target_os = "linux",
          any(target_arch = "x86",
              target_arch = "x86_64")),
          )]
pub mod ptrace {
  use libc::c_int;

  pub type PtraceRequest = c_int;

  pub const PTRACE_TRACEME:     PtraceRequest = 0;
  pub const PTRACE_PEEKTEXT:    PtraceRequest = 1;
  pub const PTRACE_PEEKDATA:    PtraceRequest = 2;
  pub const PTRACE_PEEKUSER:    PtraceRequest = 3;
  pub const PTRACE_POKETEXT:    PtraceRequest = 4;
  pub const PTRACE_POKEDATA:    PtraceRequest = 5;
  pub const PTRACE_POKEUSER:    PtraceRequest = 6;
  pub const PTRACE_CONT:        PtraceRequest = 7;
  pub const PTRACE_KILL:        PtraceRequest = 8;
  pub const PTRACE_SINGLESTEP:  PtraceRequest = 9;
  pub const PTRACE_GETREGS:     PtraceRequest = 12;
  pub const PTRACE_SETREGS:     PtraceRequest = 13;
  pub const PTRACE_GETFPREGS:   PtraceRequest = 14;
  pub const PTRACE_SETFPREGS:   PtraceRequest = 15;
  pub const PTRACE_ATTACH:      PtraceRequest = 16;
  pub const PTRACE_DETACH:      PtraceRequest = 17;
  pub const PTRACE_GETFPXREGS:  PtraceRequest = 18;
  pub const PTRACE_SETFPXREGS:  PtraceRequest = 19;
  pub const PTRACE_SYSCALL:     PtraceRequest = 24;
  pub const PTRACE_SETOPTIONS:  PtraceRequest = 0x4200;
  pub const PTRACE_GETEVENTMSG: PtraceRequest = 0x4201;
  pub const PTRACE_GETSIGINFO:  PtraceRequest = 0x4202;
  pub const PTRACE_SETSIGINFO:  PtraceRequest = 0x4203;
  pub const PTRACE_GETREGSET:   PtraceRequest = 0x4204;
  pub const PTRACE_SETREGSET:   PtraceRequest = 0x4205;
  pub const PTRACE_SEIZE:       PtraceRequest = 0x4206;
  pub const PTRACE_INTERRUPT:   PtraceRequest = 0x4207;
  pub const PTRACE_LISTEN:      PtraceRequest = 0x4208;
  pub const PTRACE_PEEKSIGINFO: PtraceRequest = 0x4209;
}

Next I add the foreign function ptrace()

mod ffi {
    use libc::{pid_t, c_int, c_long, c_void};

    extern {
        pub fn ptrace(request: c_int, pid: pid_t, addr: * const c_void, data: * const c_void) -> c_long;
    }
}

though this won’t compile on its own because this function is never used and the build settings don’t allow dead code. So I wrote a user of ffi::ptrace - the public ptrace function.

pub fn ptrace(request: ptrace::PtraceRequest, pid: pid_t, addr: *mut c_void, data: *mut c_void) {
    unsafe { ffi::ptrace(request, pid, addr, data); }
}

But I need to figure out what the return type should be for ptrace. Looking at mmap as an guide I should wrap a value in Result as in:

pub unsafe fn mlock(addr: *const c_void, length: size_t) -> Result<()>;

After a lot of trial and error I ended up with this:

pub fn ptrace(request: ptrace::PtraceRequest, pid: pid_t, addr: *mut c_void, data: *mut c_void) -> Result<i64> {
    use self::ptrace::*;

    match request {
        PTRACE_PEEKTEXT | PTRACE_PEEKDATA | PTRACE_PEEKUSER => ptrace_peek(request, pid, addr, data),
        _ => ptrace_other(request, pid, addr, data)
    }
}

fn ptrace_peek(request: ptrace::PtraceRequest, pid: pid_t, addr: *mut c_void, data: *mut c_void) -> Result<i64> {
    let ret = unsafe { ffi::ptrace(request, pid, addr, data) };
    if ret == -1 {
        return Err(Error::Sys(Errno::last()));
    }
    Ok::<i64, Error>(ret)
}

fn ptrace_other(request: ptrace::PtraceRequest, pid: pid_t, addr: *mut c_void, data: *mut c_void) -> Result<i64> {
    match unsafe { ffi::ptrace(request, pid, addr, data) } {
        -1 => Err(Error::Sys(Errno::last())),
        _  => Ok(0)
    }
}

I got a lot of type checking errors and eventually discovered that I didn’t undestand semicolons, ‘;’, in Rust. For example, I had originally written

- let ret = unsafe { ffi::ptrace(request, pid, addr, data) };
+ let ret = unsafe { ffi::ptrace(request, pid, addr, data); };

That semicolon causes the call to ffi::ptrace to be a statement rather than an expression such that ret has type (). Without the semicolon ret has type i64 and the value is the reutrn value of ffi::ptrace which is what I wanted. Once I cleaned this up I was able to implement the functions properly.

Now, the code itself splits ptrace into two cases. The PEEK cases and the other cases. The split lets me handle the errors differently for the two cases so I can handle these more complex error handling conditions for the PEEK requests:

       On  error,  all  requests  return  -1,  and errno is set appropriately.
       Since the value returned by a successful PTRACE_PEEK*  request  may  be
       -1,  the  caller  must  clear  errno before the call, and then check it
       afterward to determine whether or not an error occurred.

In order to fully support this I had to add support to nix’s Errno module to clear errno to 0. Then, I could write this:

fn ptrace_peek(request: ptrace::PtraceRequest, pid: pid_t, addr: *mut c_void, data: *mut c_void) -> Result<i64> {
    let ret = unsafe {
        Errno::clear();
        ffi::ptrace(request, pid, addr, data)
    };
    if ret == -1 && Errno::last() != Errno::UnknownErrno {
        return Err(Error::Sys(Errno::last()));
    }
    Ok::<i64, Error>(ret)
}

In the rest of the post I will update RustyTrap to use my nix::sys::ptrace module. As an asside, after I finished that work I submitted the ptrace code as a pull request to nix:

Hello Cargo!

Following the Cargo section of the The Rust Programming Language I can setup a library for RustyTrap.

cargo new rustyTrap

I’m building a library so I’ve omitted the --bin option.

Now I can run cargo run

Compiling rustyTrap v0.1.0 (file:///home/joseph/src/rust/rustyTrap)

and cargo test

   Compiling rustyTrap v0.1.0 (file:///home/joseph/src/rust/rustyTrap)
     Running target/debug/rustyTrap-2dfebc938e5015f6

running 1 test
test it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

   Doc-tests rustyTrap

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

First, I’ll update rustryTrap’s deps to reference my local git repo.

[dependencies.nix]
path = "/home/joseph/src/rust/nix-rust/"
version = "0.3.7"

And this builds:

$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index`
   Compiling bitflags v0.1.1
   Compiling libc v0.1.8
   Compiling nix v0.3.7 (file:///home/joseph/src/rust/rustyTrap)
   Compiling rustyTrap v0.1.0 (file:///home/joseph/src/rust/rustyTrap)
warning: crate `rustyTrap` should have a snake case name such as `rusty_trap`, #[warn(non_snake_case)] on by default

Looks like I need to change the name of the project (why doesn’t cargo tell me this when I build it?).

Since we basically have nothing so far, I’ll recreate the project with the name rusty_trap.

Now, I need a test and an inferior.

Load an inferior

Here’s my first try at tests/lib.rs:

extern crate rusty_trap;

#[test]
fn it_can_exec () {
    let inferior = rusty_trap::trap_inferior_exec("./inferiors/twelve", { 0 });
    assert_eq!(12, rusty_trap::trap_inferior_continue(inferior));
}

This is a much simpler test than it’s C counterpart. This most significant smiplification is that I’m using an inferior called “twelve” that should exit with an exit code of 12 which simplifies the confirmation that the inferior ran to completion. In C, we had a “Hello World!” inferior and examined stdout in order to confirm execution. This verion of the test lets us focus on rusty_trap’s functionality rather than test infrastructure.

Test driving

We compile the test to drive our feature development:

$ cargo test
   Compiling rusty_trap v0.1.0 (file:///home/joseph/src/rust/rusty_trap)
tests/lib.rs:5:20: 5:38 error: unresolved name `trap_inferior_exec`
tests/lib.rs:5     let inferior = trap_inferior_exec("./inferiors/twelve", { 0 });
                                  ^~~~~~~~~~~~~~~~~~
tests/lib.rs:6:19: 6:41 error: unresolved name `trap_inferior_continue`
tests/lib.rs:6     assert_eq!(12, trap_inferior_continue(inferior));
                                 ^~~~~~~~~~~~~~~~~~~~~~
error: aborting due to 2 previous errors
Could not compile `rusty_trap`.

and the compilation failure tells us to implement trap_inferior_exec and trap_inferior_continue so I wrote the following, empty, functions:

use std::ffi::CString;

pub type TrapInferior = i64;

pub fn trap_inferior_exec(filename: &CString, args: &[CString]) -> TrapInferior {
    return 0;
}

pub fn trap_inferior_continue(inferior: TrapInferior) -> i8 {
    return 0;
}

The signature (is that the right word in rust?) for trap_inferior_exec matches by nix’s execve function (minus the environment). The TrapInferior is i64 for now but may change in the future. This signature required a few changes to the test:

@@ -1,7 +1,10 @@
 extern crate rusty_trap;
+use std::ffi::CString;

 #[test]
 fn it_can_exec () {
-    let inferior = trap_inferior_exec("./inferiors/twelve", { 0 });
+    let inferior = rusty_trap::trap_inferior_exec(
+        &CString::new("./inferiors/twelve").unwrap(),
+        &[]);
     assert_eq!(12, rusty_trap::trap_inferior_continue(inferior));
 }

At this point I’m not that excited about using CStringin my API. It seems inconvinient to use. I think it would be nice for trap_inferior_exec to do this on behalf of the user. So instead the test should look like this:

#[test]
fn it_can_exec () {
    let inferior = trap_inferior_exec("./inferiors/twelve", &[]);
    assert_eq!(12, trap_inferior_continue(inferior));
}

and the API should look like this:

pub fn trap_inferior_exec(filename: &str, args: &[&str]) -> TrapInferior {
    return 0;
}

Now, of course the test fails since we have no real implementation:

$ cargo test
   Compiling rusty_trap v0.1.0 (file:///home/joseph/src/rust/rusty_trap)
     Running target/debug/lib-8caa31b4833c2b69

running 1 test
test it_can_exec ... FAILED

failures:

---- it_can_exec stdout ----
	thread 'it_can_exec' panicked at 'assertion failed: `(left == right)` (left: `12`, right: `0`)', tests/lib.rs:6



failures:
    it_can_exec

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured

thread '<main>' panicked at 'Some tests failed', /home/rustbuild/src/rust-buildbot/slave/stable-dist-rustc-linux/build/src/libtest/lib.rs:255

Now, let’s start with the implementation of trap_inferior_exec. It needs to fork and execve the inferior. I’ll start with the loop to handle fork():

pub fn trap_inferior_exec(filename: &str, args: &[&str]) -> Result<TrapInferior, Error> {
    loop {
        match fork() {
            Ok(Child)                      => exec_inferior(filename, args),
            Ok(Parent(pid))                => return attach_inferior(pid),
            Err(Error::Sys(errno::EAGAIN)) => continue,
            Err(e)                         => return Err(e)
        }
    }
}

One advantage here from Rust is that it makes it a little easier to do proper error handling and to report errors back to the caller (this is someting we haven’t done yet in C). Of course I’ve just added a .unwrap() in the caller to ignore any errors.

Next I need to implement exec_inferior and attach_inferior. But first, there’s a problem we have to look into.

nix::unistd::execve

There’s a bit of a problem here in the way that nix::unistd::execve works. The function is declared as:

pub fn execve(filename: &CString, args: &[CString], env: &[CString]) -> Result<()>

so args and env are fixed size arrays. This makes it hard to build up args orenv and specifically, will make it hard for trap_inferior_exec to convert from one array type to another. It would help if execve accepted Vec types instead of fixed sized arrays. This wouldn’t be a big deal given that the implementation uses Vec internally to conver the data:

#[inline]
pub fn execve(filename: &CString, args: &[CString], env: &[CString]) -> Result<()> {
    let mut args_p: Vec<*const c_char> = args.iter().map(|s| s.as_ptr()).collect();
    args_p.push(ptr::null());

    let mut env_p: Vec<*const c_char> = env.iter().map(|s| s.as_ptr()).collect();
    env_p.push(ptr::null());

    let res = unsafe {
        ffi::execve(filename.as_ptr(), args_p.as_ptr(), env_p.as_ptr())
    };

    if res != 0 {
        return Err(Error::Sys(Errno::last()));
    }

    unreachable!()
}

For our specific test we don’t have any args or an env so we will ignore this problem for the moment and come back to it at another time.

status from nix::sys::wait::waitpid

There are also some limitation with nix::sys::wait::waitpid. It doesn’t return the status which is how waitpid reports the reason why the inferior stopped along withany signals or exit codes. I started out writing up a bunch of the RustyTrap code despite this limitation and then ended up spending a lot of time debugging. Eventually, I realized that I didn’t understand the semantics of what nix::sys::wait::waitpid was returning. For clarity, I’ll present the work I’ve done on nix::sys::wait::waitpid first and then go over the code for RustyTrap.

The function waitpid is defined as:

pub fn waitpid(pid: pid_t, options: Option<WaitPidFlag>) -> Result<WaitStatus>

where WaitStatus is

pub enum WaitStatus {
    Exited(pid_t),
    StillAlive
}

The return value in waitpid is computed like this:

let res = unsafe { ffi::waitpid(pid as pid_t, &mut status as *mut c_int, option_bits) };

if res < 0 {
    Err(Error::Sys(Errno::last()))
} else if res == 0 {
    Ok(StillAlive)
} else {
    Ok(Exited(res))
}

This lines up with what the manpage for waitpid() says:

       waitpid(): on success, returns the process ID of the child whose  state
       has changed; if WNOHANG was specified and one or more child(ren) speci‐
       fied by pid exist, but have not yet changed state, then 0 is  returned.
       On error, -1 is returned.

So, for nix::sys::wait::waitpid we have

  • Err(_) means an error occurred
  • Ok(StillAlive) means NOHANG was specified and onre or more of the children exist.
  • Ok(res) means that process with ID res changed state.

It wasn’t until I wrote this out that I really understood the return value of nix::sys::wait::waitpid and understood that my problem is that the status of the process isn’t being returned.

I can try to improve waitpid to return the status but I don’t know if this will be something I can have comitted into nix given that it will break any current apps using waitpid.

I’ve added the following to waitpid.rs to decode a status from waitpid:

I changed WaitStatus to

pub enum WaitStatus {
    Exited(pid_t, i8),
    Signaled(pid_t, signal::SigNum, bool),
    Stopped(pid_t, signal::SigNum),
    Continued(pid_t),
    StillAlive
}

so that it captures all of the different process statuses. And then wrote a submodule of functions to decode the integer status from waitpid() into my new WaitStatus based on Linux’s bits/waitstatus.h

mod status {
    use libc::pid_t;
    use super::WaitStatus;
    use sys::signal;

    fn exited(status: i32) -> bool {
        (status & 0x7F) == 0
    }

    fn exit_status(status: i32) -> i8 {
        ((status & 0xFF00) >> 8) as i8
    }

    fn signaled(status: i32) -> bool {
        ((((status & 0x7f) + 1) as i8) >> 1) > 0
    }

    fn term_signal(status: i32) -> signal::SigNum {
        (status & 0x7f) as signal::SigNum
    }

    fn dumped_core(status: i32) -> bool {
        (status & 0x80) != 0
    }

    fn stopped(status: i32) -> bool {
        (status & 0xff) == 0x7f
    }

    fn stop_signal(status: i32) -> signal::SigNum {
        ((status & 0xFF00) >> 8) as signal::SigNum
    }

    fn continued(status: i32) -> bool {
        status == 0xFFFF
    }

    pub fn decode(pid : pid_t, status: i32) -> WaitStatus {
        if exited(status) {
            WaitStatus::Exited(pid, exit_status(status))
        } else if signaled(status) {
            WaitStatus::Signaled(pid, term_signal(status), dumped_core(status))
        } else if stopped(status) {
            WaitStatus::Stopped(pid, stop_signal(status))
        } else {
            println!("status is {}", status);
            assert!(continued(status));
            WaitStatus::Continued(pid)
        }
    }
}

Finally, I updated the result for nix::sys::wait::waitpid:

if res < 0 {
      Err(Error::Sys(Errno::last()))
  } else if res == 0 {
      Ok(StillAlive)
  } else {
      Ok(status::decode(res, status))
  }

At this point I was able to get RustyTrap working. I’ll go over it’s code next. But, also I created a pull request for my WaitStatus changes:

Executing the inferior

Before we started looking at waitpid we decided that RustyTrap needed functions exec_inferior and attach_inferior and attach_inferior depends on getting the status out of nix::sys::wait::waitpid. Here are implementations of these functions based on the new WaitStatus code:

pub type TrapInferior = pid_t;

fn exec_inferior(filename: &str, args: &[&str]) -> () {
    let c_filename = &CString::new(filename).unwrap();
    ptrace(PTRACE_TRACEME, 0, ptr::null_mut(), ptr::null_mut())
        .ok()
        .expect("Failed PTRACE_TRACEME");
    execve(c_filename, &[], &[])
        .ok()
        .expect("Failed execve");
    unreachable!();
}

fn attach_inferior(pid: pid_t) -> Result<TrapInferior, Error> {
    match waitpid(pid, None) {
        Ok(WaitStatus::Stopped(pid, signal::SIGTRAP)) => return Ok(pid),
        Ok(_) => panic!("Unexpected stop in attach_inferior"),
        Err(e) => return Err(e)
    }
}

pub fn trap_inferior_exec(filename: &str, args: &[&str]) -> Result<TrapInferior, Error> {
    loop {
        match fork() {
            Ok(Child)                      => exec_inferior(filename, args),
            Ok(Parent(pid))                => return attach_inferior(pid),
            Err(Error::Sys(errno::EAGAIN)) => continue,
            Err(e)                         => return Err(e)
        }
    }
}

exec_inferior makes the PTRACE_TRACEME request and then execs the inferior.

attach_inferior calls waitpid to wait for the inferior to stop. It checks for the new result types from waitpid. It expects the process to be stopped due SIGTRAP. The code panics for any other process status. Perhaps it would be better to run an error in this case.

Next we need to implement trap_inferior_continue it’s just a more general form of attach_inferior (in fact we have some duplicate we could try to refactor later). But, here’s the code:

pub fn trap_inferior_continue(inferior: TrapInferior) -> i8 {
    let pid = inferior;

    ptrace(PTRACE_CONT, pid, ptr::null_mut(), ptr::null_mut())
        .ok()
        .expect("Failed PTRACE_CONTINUE");

    match waitpid(pid, None) {
        Ok(WaitStatus::Exited(_pid, code)) => return code,
        Ok(_) => panic!("Unexpected stop in trap_inferior_continue"),
        Err(_) => panic!("Unhandled error in trap_inferior_continue")
    }
}

This should do it. But, to run the test I need an inferior. Here’s the inferior that should satisfy our test (remember our test just expects the inferior to exit with exit code 12).

tests/inferiors/twelve.rs:

use std::process::exit;

pub fn main() {
    exit(12);
}

This is another program, so I had to get Cargo to build it for me. After some digging around I found I out I could make this change to Cargo.toml:

+
+[lib]
+name = "rusty_trap"
+path = "src/lib.rs"
+
+[[bin]]
+name = "twelve"
+path = "tests/inferiors/twelve.rs"

The lib section replicates the default so I can preserve the build for RustyTrap. The [[bin]] section adds a binary for the twelve inferior.

Next, I need to reference the real path for the inferior:

extern crate rusty_trap;

#[test]
fn it_can_exec () {
    let inferior = rusty_trap::trap_inferior_exec("./target/debug/twelve", &[]).unwrap();
    assert_eq!(12, rusty_trap::trap_inferior_continue(inferior));
}

At this point the test passes and we have the start of our debugger in Rust:

     Running target/debug/lib-8caa31b4833c2b69

running 1 test
test it_can_exec ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

     Running target/debug/rusty_trap-7fb513ce62e94d89

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

     Running target/debug/twelve-24d02d12e160afb8

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

   Doc-tests rusty_trap

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

Wrapping Up

There was a lot of work to get RustyTrap up and running but I learned a lot in the process (and I hope you did too). And, now that I’ve written a couple of hundren lines of Rust code, I am starting to get more comfortable language.

I’d like to compare and contrast the C and Rust versions of Trap to summarize the benefits I’m seeing for Rust over C:

  • It was certainly easier in C to use ptrace() and wait(). So calling some system level functions is difficult in Rust but this is something that will improve over time.
  • I like the pattern matching in Rust. The code I’ve written here only uses a little bit of pattern matching but I’m familiar with the techinique from langagues like Elixir and I have come to appreciate its power.
  • I like the struct-like enums in Rust. The structures I built for the return value from waitpid elegantly capture the different process statuses and should work very well with pattern matching.
  • As a counterpoint, it is worth noting that there is some loss of control in that waitpid always completely categorizes the process status on behalf of the caller. The C version leaves it up to the caller to process status as it sees fit – ignoring status if appropriate.
  • Cargo took all the work out of setup. In C, I spent an entire post setting up the infrastructure for a C project. It invovled setting up a build system (cmake), unit test runner, and documentation. Cargo gives us all of this plus dependency management, doctests, and more. I will still need to setup GitHub and continuous integration for RustyTrap.
  • Placeholders for ptrace unused arguments are not pretty in Rust. We have ptr::null_mut() in Rust vs. 0 in C. I could improve the Rust version by declaring variables at least.
  • As a counterpoint to the previous point, I plan to create a ptrace utility library with a function per ptrace request. Each function can take the appropriate set of arguments for its specific rquest. With Rust I should be able to enforce strict type checking based on what each request expects and add some safety over the loose use of types in standard ptrace.

While it took me some time to get familar with Rust and I spent a lot of time adding features to nix, I am very happy with the result. I have to say that when I started this project I was biased against Rust (I’ve been happily using C for a very long time). But I’m actually very happy with the way the code for RustyTrap is coming along and I’m looking forward to continuing to develop the project in Rust.