Show HN: Unbug – Rust macros for programmatically invoking breakpoints

2 months ago (github.com)

This project is inspired by some of the asserts in Unreal engine.

Due to reliance on core_intrinsics it is necessary to develop using nightly Rust, but there are stubs in place so a production build will not require nightly.

I recently released version 0.2 which includes no_std support and adds optional log message arguments to the ensure macro.

You could potentially build on stable Rust by emitting the breakpoint instructions yourself, at least on popular platforms. For instance, `core::arch::asm!("int3")` on x86, or `core::arch::asm!("brk #1")` on ARM.

Also, this is providing motivation to want to stabilize a breakpoint mechanism, perhaps `core::arch::breakpoint()`. I'm going to propose an API Change Proposal (ACP) to the libs-api team to see if we can provide that in stable Rust.

  • Plain int3 is a footgun: the CPU does not keep track of the address of the int3 (at least not until FRED), and it reports the address after int3. It’s impossible to reliably undo that in software, and most debuggers don’t even try, and the result is a failure to identify the location of the breakpoint. It’s problematic if the int3 is the last instruction in a basic block, and even worse if the optimizer thinks that whatever is after the int3 is unreachable.

    If Rust’s standard library does this, please consider using int3;nop instead.

    • Good to know! I've seen the pattern of "int3; nop" before, but I've never seen the explanation for why. I'd always assumed it involved the desire to be able to live-patch a different instruction over it.

      In Rust, we're using the `llvm.debugtrap` intrinsic. Does that DTRT?

    • The "canonical" INT 3 is a single byte opcode (CCh), so the debugger can just subtract 1 from the address pushed on the stack to get the breakpoint location.

      There is another encoding (CD 03), but no assembler should emit it. It used to be possible for adversarial code to confuse debug interrupt handlers with this, but this should be fixed now.

      3 replies →

  • Having a feature like this will significantly increase the demand of better incremental compilation, potentially with the need for patching specific items on existing binaries for speed. At that point you could get very close to an IDE debugging experience/speed with only rustc, a text editor and a debugger. (Inserting a bunch of NOPs on every function and supporting patching of JMPs in their place would likely go a long way for this.)

  • Thanks, yeah I considered using the instructions directly, but I was hoping for a more cross-platform option. For my purposes, developing in the Bevy engine, nightly isn't a huge blocker. Yeah, it would be really great to just have breakpoint support in stable Rust, thanks for doing the proposal! I'll consider stable support in the meantime.

    • On Unix platforms, you could just raise SIGTRAP directly, it will pause the attached debugger and works regardless of architecture.

      This is the macro I use for example:

        #[doc(hidden)]
        pub use libc as __libc;
        
        // This is a macro instead of a function to ensure the debugger shows the breakpoint as being at
        // the caller instead of this file.
        #[cfg(unix)]
        #[macro_export]
        macro_rules! breakpoint {
            () => {
                unsafe {
                    use $crate::__libc as libc;
                    libc::raise(libc::SIGTRAP);
                }
            };
        }

    • Hah, the README says:

      > Additonally, debugging may not land on the macro statements themselves.

      See my comment above, and give int3;nop a try.

      2 replies →

Rusts current pretty printers in lldb and gdb are just not good enough for a fluid step debugging experience. I've had luck with intellij IDE's but it's very sad that the best we can do in a language with such good devex tooling is print debugging.

Theoretically you could generate debug scripts with a macro and embed them with

  #![debugger_visualizer(gdb_script_file = "../foo.py")]

but I haven't seen anyone go through the trouble.

Neat project! Maybe this decision is copied over from unreal engine, but instead of `ensure` and `ensure_always`, having names like `ensure_once` and `ensure` would have been more clear to me.

  • I can understand where you're coming from, but when programming games you generally don't want a breakpoint to be hit more than once since you are running a loop over multiple frames. So in this case the concept of ensure_once is more common, so the shorter inverse is more convenient. Asserts should be enough to get your attention and not to annoy, so orienting it this way is a deliberate choice.

What does nightly mean ? I hate that you could not know a specific version of a nightly.

  • The master branch of the Rust repo is built every night and distributed. That's a nightly build. Most people are on the stable release, which is updated every six weeks.

    A minority use the nightly build for various reasons: a feature that hasn't reached stable yet, or because they want to help test the nightly releases and prevent bugs from reaching stable.

    • Is it a minority? Are there stats posted for this?

      Only recently have I had some projects switching to stable after their required features stabilized.

      2 replies →

  • You can pin versions with the rust-toolchain.toml file you need to be using Rustup afaik. Nightly is just the daily builds.

  • It's a bit unfortunate wording but it basically requires any nightly toolchain version. It uses `std::intrinsics::breakpoint()` which is a compiler intrinstic. This has been available for a long time, but afaik will never be exposed on a stable toolchain.

    Per https://dev-doc.rust-lang.org/nightly/unstable-book/library-...

    >This feature is internal to the Rust compiler and is not intended for general use.

Is there a good newby tutorial on how to use debugger with Rust (and debugger in general?) No videos please.

  • I didn't come across any good ones when creating this library, but if you're using VSCode, I tried my best to make the README as beginner friendly as possible. I'm open to issues and PRs if anything is unclear. I think part of the issue is that debugging is not yet very common in the Rust ecosystem, partially due to the excellent borrow checker and error messages, but partially due to immature tooling, hence I made this to promote the practice of debugging.