Hi,
I have switched for a long time to using __builtin_trap() as an assert,
but
recently I noticed I can't step past it in a debugger.
__builtin_trap() generates an instruction "ud2" which effectively seems
to be
noreturn, i.e not a breakpoint if I can't step and continue after.
someone on this discussion comments:
https://stackoverflow.com/questions/173618/is-there-a-portable-equivalent-to-debugbreak-debugbreak> __builtin_trap typically compiles to ud2 (x86) or other illegal > instruction,> not a debug breakpoint, and is also treated noreturn. you can't > continue after> it even with a debugger
Which in my view is a bit different from this claim:
> __builtin_trap inserts a trap instruction -- a built-in breakpoint.
It seems like the real instruction that implements this is "int3". Clang
generates this from __builtin_debugtrap() and this is also what MSVC
generates
for __debugbreak(). GCC appears to lack an intrinsic to generate this
instruction, though __asm__("int3") does of course work.
There is this issue on the bug tracker about it:
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=99299https://godbolt.org/z/7xrqcMWqz
Unforunately, GDB 13.2 seems to be bugged on this point...
for this example code:
#include <stdio.h>
int main(void)
{
#ifdef TRAP
__builtin_trap();
#else
__asm__("int3");
#endif
// give some code to step though
for (int i = 0; i < 10; ++i)
{
printf("%d\n", i);
}
}
compiled with:
gcc break.c -o int3 -O2 -g3
I usually reproduce:
0x56398c3a005b ???
0x56398c0bce24 ???
0x56398bdf0694 ???
0x7f01e6be8ccf ???
0x7f01e6be8d89 ???
0x56398bdf78e4 ???
0xffffffffffffffff ???
---------------------
../../gdb/infrun.c:2640: internal-error: resume_1: Assertion
`pc_in_thread_step_range (pc, tp)' failed.
A problem internal to GDB has been detected,
further debugging may prove unreliable.
Quit this debugging session? (y or n)
GDB being buggy is totally unheard of...
But if I continue debugging it does seem to work as intended, while
__builtin_trap() just ended up ending the whole process. Usually for
assertions
you wouldn't want to continue anyway, but I don't see why it shouldn't
be an
option. Perhaps this is worth mentioning as a footnote?
I don't have much practical experience with it because I rarely want to
continue through assertions, but you can "jump +1" over ud2. Perhaps even
"define skip" to it in your gdbinit to give it a name. It seems to work
fine… usually. GCC treats it code following __builtin_trap() as dead, even
on -O0, so some cases cannot be continued. That makes asm("ud2") a more
attractive option with "j +1" and probably worth more investigation.
I do generally like debugbreak better, but as you noticed, it does not
work well in the GNU toolchain. I don't get the internal error with GDB
13.1, but it doesn't seem to understand it as a kind of assertion. GDB
sees rip on the next line and so displays the next line as the stopping
point, not the assertion, which creates the sort of friction a debugger is
supposed to be eliminating. That's enough not to bother with asm("int3"),
at least for me.
As noted in my last article ("personal coding style"), I've been using
__builtin_unreachable() in my latest assert macro. I can turn assertions
on an off without involving the preprocessor, and in release builds they
turn into optimization hints. Though it's still ultimately ud2 with all
the same limitations of __builtin_trap().
As for a footnote, I'll try out "j +1" more often as I work and see if
it's worth discussing. Thanks for bringing this up!
> GDB being buggy is totally unheard of...
Heh, I'm sure you noticed how I had to roll back the GDB 14.1 upgrade in
w64devkit last week due to problems I experienced.
16 Dec 2023 2:59:51 am Christopher Wellons <wellons@nullprogram.com>:
> I don't have much practical experience with it because I rarely want to > continue through assertions, but you can "jump +1" over ud2. Perhaps > even "define skip" to it in your gdbinit to give it a name. It seems to > work fine… usually. GCC treats it code following __builtin_trap() as > dead, even on -O0, so some cases cannot be continued. That makes > asm("ud2") a more attractive option with "j +1" and probably worth more > investigation.
I agree it's a rather rare need, it's more a desire when I'm dealing
with unknown code where I can't be sure if the asserts actually make any
sense, but I still want to be informed if any get tripped.
Unfortunately, usually these are from assert.h. Not a huge deal either
way, but I don't think there's a compelling reason to use a method which
deliberately prevents it.
> I do generally like debugbreak better, but as you noticed, it does not > work well in the GNU toolchain. I don't get the internal error with GDB > 13.1, but it doesn't seem to understand it as a kind of assertion. GDB > sees rip on the next line and so displays the next line as the stopping > point, not the assertion, which creates the sort of friction a debugger > is supposed to be eliminating. That's enough not to bother with > asm("int3"), at least for me.
Perhaps it's a bug in gdb 13.1? I have no idea, but the following code
does stop on the assert itself, not anything further. I believe int3
does have this property.
#include <stdio.h>
#define assert(e) do { if (!e) __asm__("int3"); } while(0)
int main(void)
{
assert(0);
// give some code to step though
for (int i = 0; i < 10; ++i)
{
printf("%d\n", i);
}
}
$ gcc -g3 -O2 test.c -o int3
$ gdb ./int3
GNU gdb (GDB) 13.2
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./int3...
(gdb) start
Temporary breakpoint 1 at 0x1040: file test.c, line 7.
Starting program: /tmp/int3
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
Temporary breakpoint 1, main () at test.c:7
7 assert(0);
The failing gcc bug assertion only happens when I step to the next line.
> Heh, I'm sure you noticed how I had to roll back the GDB 14.1 upgrade > in w64devkit last week due to problems I experienced.
Yeah, it's a shame. The dap features look really nice, can't wait to
drop vscode-cpptools from my nvim-dap setup. It's somehow even more
fragile and unreliable than GDB itself...
> Perhaps it's a bug in gdb 13.1? I have no idea, but the following code > does stop on the assert itself, not anything further.
Even GDB 14.1 still stops slightly too late in your example program for
me, so I wanted to spend time with it on the back burner. I still think my
speculation about rip being one instruction off is right, and GDB, not
understanding int3, can't deal with it. I've been trying it out a bit more
to see if I could make it work, and I finally had an insight: What if I
put an extra instruction after the int3 so that rip is still on the same
source line? Bingo!
#define breakpoint() asm volatile ("int3; nop")
It works exactly the way I want! A "disas" shows rip on the nop as I had
expected, and it's still inside the asm block, so GDB isn't lost. I was
pushed to solve this after putting a conditional break in a loop. GDB
break conditions are just soooo slow, and it was taking too long to hit
the condition. I really wanted int3 to work, but it kept breaking at the
top of the next iteration. Then the nop idea hit me.
if (i == 1000000) breakpoint(); // break on millionth iteration
Or as a continuable assertion (NRK-style ternary assert):
#define assert(c) ((c) ? 0 : ({ asm volatile ("int3; nop"); }))
A new tool for my toolbelt! I might use this instead of the trap built-in
for trap-based assertions, at least when I know I'm on x86. Looks like ARM
has no equivalent, at least when using GDB and LLDB:
https://github.com/llvm/llvm-project/issues/56268
> the breakpoint "drifts" away due to the edit
I'm surprised there's not more discussion about this online because all
but one of the debugger interfaces I've used handle this poorly. It's
mostly not an issue when debugger and editor are the same program, which
perhaps partially explains why it goes so unnoticed. In that case, the
editor can stick a breakpoint to a line in its internal representation,
and so it updates naturally with most editing.
However, make external edits and it must reload the entire source. With
the one exception, no attempts are made to match the original source line
to the new source line, a la the "patch" utility. It's pure line numbers,
and so edits before a line number breakpoint causes it to drift.
Could a patch-like update to line number breakpoints actually work? Yes!
That one debugger: Visual Studio. On this particular dimension it works
quite well and should be emulated elsewhere. There's no perfect solution,
and guessing will go wrong at times, but that's fine. It's worth it for
reduced friction in the typical case.
Consider GDB: It keeps a list of breakpoints, some of which are target
file+lineno tuples. On attach/start, it goes through its breakpoint list,
converts each to an address, and places breakpoints. To do better, when a
lineno break is set it captures a context snapshot, like a patch, which is
what the user _really_ wants to break on. On debuggee start, it uses the
lineno as a starting point of a context search. If it finds a match, it
uses that for the breakpoint, and updates the lineno used to start future
searches. GDB can display source listings, so it already has access to all
the information it needs to do this.
Fuzzy matching is the real magic, and testing how Visual Studio handles
various external edits, I'm surprised how smart it is. If simultaneously
edit the target line _and_ move it (with its context), the breakpoint
usually survives. It seems to depend more on context than the line itself.
I wonder if the algorithm is documented anywhere. This concept could be
presented as an isolated programming challenge: Given two similar source
files, identify specific lines from the first source in the second.
A GDB front-end could do this without GDB's cooperation the same way the
front-ends with built-in editors handle it for internal edits.
> Having a breakpoint embedded into the source would solve this as well.
Here's another trick for you: name a source line. A label works, though
it's not a global name so (usually) must be qualified function:label. Not
tied to a lineno, it won't drift adversely with edits. More convenient is
a global label:
asm ("b: .globl b");
Drop that anywhere, then "b b" in GDB to create the named breakpoint.
Though if you're taking it that far, "int3; nop" is probably a better
option anyway.
> but it kept breaking at the top of the next iteration.
Just a couple days ago I found myself writing the following macro in
order to solve the exact same issue:
#define BRK() do { int stop_debugger_from_stepping_over_this_line = 5; } while (0)
(And AFAIK visual studio suffers from this problem as well:
https://twitter.com/ryanjfleury/status/1693061763884007643)
> #define assert(c) ((c) ? 0 : ({ asm volatile ("int3; nop"); }))
The int3 solution seems nicer. Another annoyance I face often is when I
set a breakpoint (via vim's official `termdebug` plugin) at a specific
line and then make any changes to the source - the breakpoint "drifts"
away due to the edit. Having a breakpoint embedded into the source would
solve this as well.
However, my recent experience with `debugger.lua` [0] taught me that
embedded breakpoints come with their own sets of annoyances:
a) Need to recompile+restart in order to set a breakpoint. But more annoyingly:
b) Need to recompile+restart just to toggle a breakpoint on/off.
Maybe you can also embed a "switch" onto each breakpoint:
#define brk(...) do {
int brk_##__LINE__ = 1; // set this to 0 in debugger to disable
// ...
} while (0)
This can now be toggled off from the debugger. But each restart will end
up resetting the state back to 1, though. So perhaps not so worthwhile.
Despite the issues, I do think this can be useful, as a way to set
temporary breakpoint in source to avoid drift. So thanks to both of you
for sharing, Peter and Chris.
[0]: https://github.com/slembcke/debugger.lua
- NRK