rust-lang / rust-lang/rust

What kind(s) of breakpoint do we want?

Open
#162,953 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

needs-triage
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

It would be nice to get some form of "breakpoint" operation stabilized in some form. Currently the candidate public API is core::arch::breakpoint. Tracking issue: https://github.com/rust-lang/rust/issues/133724.

However, discussion close to stabilization revealed that different people have subtly but decisively different ideas of what the term "breakpoint" even means.

  • There's the "default-abort" kind of breakpoint, also sometimes called yeet1 in our discussions. This breakpoint can be used in a mylib::assert! kind of macro. In terms of soundness reasoning it is equivalent to an abort_immediate, but in terms of compilation it does its best to generate code that will cause an attached debugger, if any, to halt execution to let the user inspect the state, and it makes it such that in a debugger, one can (at one's own risk) choose to continue beyond the breakpoint.
  • There's the "may-continue" kind of breakpoint, also sometimes called yeet2 in our discussions. In terms of soundness reasoning it is equivalent to "non-deterministically either abort execution or just continue", but in terms of compilation it does its best to generate code that will cause an attached debugger, if any, to halt execution and give the user of the debugger a chance to inspect the current state. It is always safe for the user of the debugger to continue beyond this breakpoint.

These were dubbed yeet1 and yeet2 in this great summary. On most targets, the compiler would do the exact same thing for both of them, the difference is only in who is to blame should there be UB after continuing from a breakpoint: default-abort / yeet1 says that continuing is unsafe and happens at the risk of of whoever controls the debugger, may-continue / yeet2 says that continuing is safe and the programmer who wrote the code is responsible for ensuring that the following code does not cause UB. Accordingly, the default behavior of Miri would differ between yeet1 and yeet2.

The fact that the compiler treats both of them the same most of the time can make it hard to talk about the difference between the two. In my eyes, the best litmus test we found for this is the mylib::assert! test: do you think that an implementation of assert! that uses breakpoint instead of abort/panic! is correct? Yeet2 says "clearly not", yeet1 says "clearly yes". The current docs for our operation say that yes it can be used instead of abort ("in normal execution with no debug tooling involved this will not continue executing"), so the docs represent yeet1 (though it could be made more clear that continuing in a debugger, while possible, is considered to "void the warranty" so to speak).

Another way to frame the difference between these two is to consider what a portable version of the operation would do, in case where an instruction with the semantics of x86 int3 is not available. For instance, on Armv5T, Armv6 the BKPT instruction is not guaranteed to halt execution.

  • yeet1 must abort, so the best it could do is BKPT; call abort.
  • yeet2 could compile to just BKPT.

If we look at the use of the term "breakpoint" and adjacent terms elsewhere, we find:

  • Debuggers themselves provide the option to add breakpoints. It is always safe to remove them again, so this corresponds to yeet2.
  • C++ has a std::breakpoint operation. Its behavior is mostly implementation-defined and it does not say that the program will typically abort, so portable C++ code can arguably not use this as-if it was yeet1. This is reinforced by the phrasing "temporarily stop program execution" (emphasis mine) and by using "infrequent non-critical condition" as the first example. Note however that this text is just in cppref, the standard literally just says "The semantics of this function are implementation-defined", plus a non-normative note that includes the sentence "If there is no debugger present, execution of the program can end abnormally" (emphasis mine).
    Neither GCC nor clang seem to actually define what their implementation does at the moment, so C++ code written specifically for GCC/clang also cannot use std::breakpoint for an assert-like macro in my eyes. (I am talking about a macro with Rust assert! semantics here, i.e. something that would be correct albeit a bad UX for checking user input.) But C++ likes to blur the line between "logic bug" and "UB" so it's hard to be sure about this.
  • Windows docs say that "DebugBreak and __debugbreak have the same effect as setting a breakpoint at that location". I would read this as saying that this is yeet2, not yeet1.
  • LLVM's debugtrap (which is what we use to implement arch::breakpoint at the moment) is not documented precisely enough to tell the difference. This document analyzes the instructions LLVM uses for debugtrap on various architectures. Not all of them are a valid implementation of yeet1. In particular, this means that our current implementation of arch::breakpoint() does not match our docs. I do not know if LLVM intends to make the guarantee we document for debugtrap, but it's not a documented guarantee at the moment so it's clearly fishy for us to make claims about the intrinsic that are not backed by what LLVM documents. (Cc @nikic)
  • On the hardware instruction side, many instruction sets have a "breakpoint" instruction, called EBREAK, BRK, BKPT, BRKPT, BREAK or similar. x86 uses int3 but that is often referred to as a breakpoint as well. Those instructions interrupt execution and put the OS in charge; OSes typically default to aborting the process if no debugger is attached, but that behavior can sometimes be overwritten, e.g. with a signal handler. For bare-metal systems, behavior is up to the interrupt handler. As mentioned above, on older ARM the instruction continues execution (the OS cannot intervene). Old x86 environments (DOS or BIOS mode) choose to continue the program. AVR does have a BREAK instruction that is not meant to be used by application software, and whose behavior depends on the chip (some chips document that it becomes a NOP when no debugger is attached).

In the discussion, most people had a yeet2 understanding of breakpoints, but Josh Triplett understands a "breakpoint" to mean yeet1. We don't have representative data for how many people would implicitly assume "breakpoint" to mean "something that I can use instead of abort" (aka yeet1) or not. That said, it seems clear from the discussion that "breakpoint = what one sets in the debugger; may abort without a debugger but is not guaranteed to abort" (yeet2) is the more common use of the term. I am not aware of any spec/document for a programming language or OS API (apart from our own docs for arch::breakpoint, which were written by Josh) that would say that "breakpoint" means "definitely aborts when no debugger is attached" (yeet1).

So... how do we resolve this? Defining what "breakpoint" means for Rust (and especially the soundness questions) seems like a lang question to me, deciding how to expose that operation and under which name is a libs question.

  • If we provide only a single operation, what should it be called and what semantics should it have? Given the fact that the term has different meaning to different people in the discussion, it might be wise to avoid the use of the unqualified "breakpoint", so e.g. if we decided to stabilize yeet1 we could call it breakpoint_abort or so to make it clear that this is different from e.g. C++ std::breakpoint. If we decided to stabilize yeet2 we could call it resumable_breakpoint to avoid surprises for people that share Josh's understanding of the term. An operation with yeet2 semantics may also be better located in core::hint than core::arch.
  • We could also provide both operations. This could take multiple different shapes, we could either have portable operations that directly correspond to yeet1 and yeet2 (which would compile the same most of the time, but compile different on Armv5T, Armv6 and behave different in Miri), or we could have e.g. a hint::resumable_breakpoint (yeet2) that is always available and something in arch with yeet1 semantics that is only available on targets that actually have an instruction that aborts by default (i.e., not on Armv5T, Armv6, avr), and maybe only available on targets where such a breakpoint supports continuing (i.e., not on wasm).
  • Or we could provide a different operation altogether, maybe along the lines of "whatever instruction the current ISA calls a breakpoint". That would not have precedent in other languages / APIs that I am aware of. Portable code could not rely on this operation aborting, but target-specific code possibly could (assuming we say that e.g. on x86, int3 must abort the process; if the interrupt handler or signal handler does something else they are unsuited to run Rust code).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the current core::arch::breakpoint API and tracking issue #133724, then compare the documented behavior with the yeet1 and yeet2 semantics and the target-specific examples in this discussion. Done means reaching a decision about the operation's semantics, name and placement, with documentation that matches the chosen guarantee.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
backend-api-design, compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.