Cover image article
Cover image generated via Stable Diffusion web. Prompt: 'delorean, fire trail, doors open, centered, car'

Time Travel Debugging in Rust

A friend linked me a video showcase of some of the tools that Tomorrow Corporation uses to make video games. If you haven't seen it yet, I recommend at least giving it a skim--it's a really cool set of tools, that includes live editing of code, the ability to watch a scene as it renders in piece-by-piece, and time travel debugging.

While the first two are available in spotty, limited ways depending on language, runtime, target, and a million other variables, the thing that stood out to me most was time travel debugging. I'd seen it discussed a lot in programming circles as sort of the holy grail of debugging, but had never looked into it myself. After seeing it in action though, my curiosity was piqued.

And of course, I wondered 'is this available for Rust?'

Wait, what's "time travel debugging"? 🔗

Also sometimes called "reverse debugging", it is, put really simply, the ability to step forward, or backward any number of times while debugging. As you step backward, the universe--the stack, all the local variables, all your watched variables--step back in time as well. I don't know how many times I've had my IDE set up to automatically break on some exception, or error case, but by the time I actually get there, the state of the program is such a mess that it's a real job to figure out where things started going wrong.

If you could just step backward from the point where things all went wrong, there are plenty of scenarios where finding the issue would be a lot easier.

Some Caveats 🔗

Some important notes before we start. Time travel debugging relies on certain hardware monitoring capabilities, which means that availability is pretty limited. So, I want to be really clear: my setup, and the following tutorial will only cover:

  • Non-virtualized Windows 10 (no Parallels, sorry Mac folks)
  • Using WinDbg, Microsoft's debugger for native code and kernel-mode code
  • Rust code compiled with the x86_64-pc-windows-msvc target triple. (i686-pc-windows-msvc probably works too, but I haven't tried it.)

All clear? Good! Let's move on.

Setting Up 🔗

As I alluded to above, we'll need some specific tools. First of all, you'll need a non-virtual Windows install.

Second, you'll need a copy of WinDbg (Preview) from the Windows Store. If, for whatever reason you can't use the store, there are ways to get this version of WinDbg in other ways, but I haven't used them myself, and can't speak to them. Either way, old WinDbg won't do--it doesn't know how to time travel.

Finally, you'll need the Rust tools installed, and be able to compile to the x86_64-pc-windows-msvc target. I'm assuming you have Rustup installed and managing your Rust toolchain. If not... you should.

You can check your installed targets with the following command:

rustup target list --installed

If you see x86_64-pc-windows-msvc, then you're good to go. If not, you should just be able to do

rustup target add x86_64-pc-windows-msvc

...and you'll be in business.

Producing something debuggable 🔗

Okay! We've got everything we need. Let's produce something to debug. Let's start simple.

> cargo new time-travelling-debugging
     Created binary (application) `time-travelling-debugging` package

The generated main.rs is pretty barebones. Not a lot of interesting state to demonstrate time traveling with. Let's add some simple stuff to give us a few things to play with.

Here, why not something like...

use std::{error::Error, io};

fn main() -> Result<(), Box<dyn Error>> {
    let mut buffer = String::new();

    println!("Let's make a pony! Enter a name!");
    io::stdin().read_line(&mut buffer)?;
    let name = String::from(buffer.trim());
    buffer.clear();

    println!("\nEnter an age!");
    io::stdin().read_line(&mut buffer)?;
    let age: u32 = buffer.trim().parse()?;
    buffer.clear();

    println!("\nEnter a wingspan (in inches), or leave blank (or enter an invalid value) for a wingless pony.");
    io::stdin().read_line(&mut buffer)?;
    let wingspan: Option<u32> = buffer.trim().parse::<u32>().map_or(None, |a| Some(a));
    buffer.clear();

    println!(
        "\nEnter a horn length (in inches), or leave blank (or enter an invalud value) for a hornless pony."
    );
    io::stdin().read_line(&mut buffer)?;
    let horn_length: Option<u32> = buffer.trim().parse::<u32>().map_or(None, |h| Some(h));
    buffer.clear();

    let user_pony = Pony::new(&name, age, wingspan, horn_length);
    println!("Your new pony is: {:?}", user_pony);

    Ok(())
}

#[derive(Debug)]
pub struct Pony {
    pub name: String,
    pub age: u32,
    pub wingspan: Option<u32>,
    pub horn_length: Option<u32>,
}

impl Pony {
    fn new(name: &str, age: u32, wingspan: Option<u32>, horn_length: Option<u32>) -> Pony {
        Pony {
            name: String::from(name),
            age,
            wingspan,
            horn_length,
        }
    }
}

This gives us four intermediate variables (name, age, wingspan, and horn_length), as well as a single String buffer that gets reused over the program's lifetime, as well as an external function. Plenty of things to watch or rewind.

Let's compile it, and then we can start setting up WinDbg.

cargo build --target=x86_64-pc-windows-msvc
   Compiling time-travelling-debugging v0.1.0 (C:\Users\username\Desktop\Repositories\time-travelling-debugging)
    Finished dev [unoptimized + debuginfo] target(s) in 0.35s

Note that I explicitly passed --target=x86_64-pc-windows-msvc to cargo. If you installed Rust on Windows with default settings, this probably isn't necessary. Still, if you get an error at this point, that's a sign that something with your configuration needs attention before you can proceed.

WinDbg 🔗

Okay. Let's boot up WinDbg. You'll probably be greeted with something like this:

An image of WinDbg (Preview)'s startup screen.

For this, we want "Launch executable (advanced)".

That'll present a small dialog that asks a few questions. They're mostly self-explanatory, but I'll lay them out:

Executable
The fully-qualified path to the .exe produced by cargo build up above. For me, that was C:\Users\username\Desktop\Repositories\time-travelling-debugging\target\x86_64-pc-windows-msvc\debug\time-travelling-debugging.exe

Arguments
These will be passed directly to the executable. Since our executable doesn't take any args, we can leave this blank.

Start directory
The working directory for our executable. Since it doesn't read or write to disk, this isn't that important. I set mine to the path that the executable was in, e.g. C:\Users\username\Desktop\Repositories\time-travelling-debugging\target\x86_64-pc-windows-msvc\debug\.

Target architecture
Autodetect is probably fine. Since I'm on a 64-bit machine and producing 64-bit executables, I could set this to 64-bit, but autodetect seems to get things correct.

Debug child processes
We don't spawn any, so feel free to leave this unchecked.

Record with Time Travel Debugging
That's the good stuff. That's what we're here for. Check this.

An image of WinDbg (Preview)'s screen when configured for Time Travel Debugging.

Once we click "Configure and Record", it will ask one last question:

Save location
This is where WinDbg will save the trace and index files that it records while our application is running. These trace and index files are the things that allow the actual time travel. They're also quite large, and are allowed to grow without bound!

Put them somewhere you're likely to clean out regularly. I put mine next to the generated executable, as I tend to consider those directories transient: C:\Users\username\Desktop\Repositories\time-travelling-debugging\target\x86_64-pc-windows-msvc\debug

Once you click "Record", WinDbg will launch your executable, and begin recording its execution. Note that it will ask for Admin elevation if you didn't launch WinDbg elevated--Time Travel Debugging requires Admin rights.

A screenshot of WinDbg (Preview) and the debugged Rust executable running side-by-side.

Enter in your various pieces of pony information as normal. Once your program terminates, WinDbg will do some cleanup. Once it has the trace ready, you can start debugging, both forwards and backwards.

Unfortuantely, the default view that WinDbg gives you isn't very friendly. Let's fix that!

First, click on View up at the top. Click on Command. This summons a pane that shows WinDbg's output, and allows you to interact with it using various commands, which are too numerous and complex to get into here.

The other thing you're likely to want, is your actual source code! To view that, click on Source up at the top, then click Open Source File. Then, navigate to our main.rs and open it up. Note: In the resulting file dialog, you'll need to change the filter from "C/C++ files" to "All files", or it will filter out your .rs files.

Now, if we go back to the Home tab, you'll see that we've got buttons for a variety of things: Break, Go, Step Out, Step Into, Step Over.

But! More fun, right next to those, we also have Step Out Back, Step Into Back, and Step Over Back. Neat!

Let's set a breakpoint on Line 4, the first line of the main() function, and hit Go. The debugger should proceed, then halt at that breakpoint.

A screenshot of WinDbg (Preview) debugging a Rust application, halted on a breakpoint.

You're now free to step forward and backward to your heart's content. At the bottom, Locals and the current stack will display as usual. The visualization for Rust data structures in Locals and Watch isn't amazing, but it's also about the same as we get in LLDB.

Caveats and Tweaks 🔗

So obviously, this comes with a great big pile of "but"s, some of them big and stinky. Let's take a look at them.

  • This isn't live debugging. You're essentially debugging a recording of the program's run.
  • As such, you also can't tweak variable values while time travel debugging--no changing history.
  • Breakpoints not working, or command window not showing correct function names? Try going to File -> Settings -> Debugging settings, and adding the folder containing your program's generated .pdb file. By default, it sits next to the executable. For me, it was C:\Users\username\Desktop\Repositories\time-travelling-debugging\target\x86_64-pc-windows-msvc\debug.
  • This does work across multiple source files as well, though you may have to open those source files manually.
  • By default, numeric values are displayed in hexadecimal. To change to another number base, in the Command window, enter the command n base. So, if you wanted to display things in decimal, n 10. Back to hex? n 16.
  • How does this work with threads? Well... it's rough. WinDbg struggles to set breakpoints, and tracing execution gets tricky. It also seems to be impossible to name threads from Rust in a way that WinDbg understands.
  • What about async? No idea! Seriously, try it out and tell me, I'm curious.
  • Those trace and index files get big, fast. Clean 'em out once in a while, unless you want to hold onto specific program runs.

Closing thoughts, alternatives 🔗

So there you have it! Time traveling debugging in Rust. On Windows. With a specific proprietary tool. When targeting a specific triple.

What about other platforms? What about IDE integration?

🤷‍♀️ ¯\_(ツ)_/¯ 🤷‍♂️

As far as I know, LLDB doesn't support time travel debugging at all. Supposedly there is a way to make VSCode's LLDB plugin work with rr, which a dedicated time traveling debugger. rr only works on an actual Linux machine though--even WSL2 won't cut it. So, I wasn't able to try it out.

Apparently, GDB has a time traveling mode. I have zero experience using GDB with Rust code though, so if anyone has insight here, I'd love to hear it.

In any case! This can be a useful tool to keep in your back pocket if you're trying to dig out a tricky logic error, but the point of failure happens well after things have actually gone wrong. Hopefully this little primer helps some folks out when they just really need to get a little time travel done.


Thanks for reading!

As ever, you can leave a comment below, find me on Twitter at @pingzingy, or Mastodon as @PingZing@pony.social.

Creative Commons BY badge The text of this blog post is licensed under a Creative Commons Attribution 4.0 International License.

Comments

点石阅读如何根据汉文名字起英文名蔬菜批发店起名在线免费起名600362股票cctv在线直播观看免费公司起名大全西安公交姓卢的男孩起什么名字语文补习属牛起名字适合用的字起名不能用的字低眉顺眼店铺起名字大全免费铜制品专卖店咋起名支付公司起名朗朗上口泰罗奥特曼全集中文版猪年张姓宝宝起名字2014年属马的女孩起名给合作社起个名字我的世界房子设计图walmart.com高起专报名网址翊字如何起名非主流颓废女生头像机械厂起取名大全谭凯琪免费送公司起名的网站如何起个好公司名字姓隋男孩子起名炫酷的起名大全歼20紧急升空逼退外机英媒称团队夜以继日筹划王妃复出草木蔓发 春山在望成都发生巨响 当地回应60岁老人炒菠菜未焯水致肾病恶化男子涉嫌走私被判11年却一天牢没坐劳斯莱斯右转逼停直行车网传落水者说“没让你救”系谣言广东通报13岁男孩性侵女童不予立案贵州小伙回应在美国卖三蹦子火了淀粉肠小王子日销售额涨超10倍有个姐真把千机伞做出来了近3万元金手镯仅含足金十克呼北高速交通事故已致14人死亡杨洋拄拐现身医院国产伟哥去年销售近13亿男子给前妻转账 现任妻子起诉要回新基金只募集到26元还是员工自购男孩疑遭霸凌 家长讨说法被踢出群充个话费竟沦为间接洗钱工具新的一天从800个哈欠开始单亲妈妈陷入热恋 14岁儿子报警#春分立蛋大挑战#中国投资客涌入日本东京买房两大学生合买彩票中奖一人不认账新加坡主帅:唯一目标击败中国队月嫂回应掌掴婴儿是在赶虫子19岁小伙救下5人后溺亡 多方发声清明节放假3天调休1天张家界的山上“长”满了韩国人?开封王婆为何火了主播靠辱骂母亲走红被批捕封号代拍被何赛飞拿着魔杖追着打阿根廷将发行1万与2万面值的纸币库克现身上海为江西彩礼“减负”的“试婚人”因自嘲式简历走红的教授更新简介殡仪馆花卉高于市场价3倍还重复用网友称在豆瓣酱里吃出老鼠头315晚会后胖东来又人满为患了网友建议重庆地铁不准乘客携带菜筐特朗普谈“凯特王妃P图照”罗斯否认插足凯特王妃婚姻青海通报栏杆断裂小学生跌落住进ICU恒大被罚41.75亿到底怎么缴湖南一县政协主席疑涉刑案被控制茶百道就改标签日期致歉王树国3次鞠躬告别西交大师生张立群任西安交通大学校长杨倩无缘巴黎奥运

点石阅读 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化