~skeeto/public-inbox

5 3

Re: Assertions should be more debugger-oriented

Details
Message ID
<2d3d7662a361ddd049f7dc65b94cecdd@disroot.org>
DKIM signature
missing
Download raw message
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=99299

https://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?

Re: Assertions should be more debugger-oriented

Details
Message ID
<20231216025947.exjlqhinxgblelyl@nullprogram.com>
In-Reply-To
<2d3d7662a361ddd049f7dc65b94cecdd@disroot.org> (view parent)
DKIM signature
missing
Download raw message
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.

Re: Assertions should be more debugger-oriented

Details
Message ID
<94bfb66d-a931-4a6e-aefd-23487a72c6f4@localhost>
In-Reply-To
<20231216025947.exjlqhinxgblelyl@nullprogram.com> (view parent)
DKIM signature
missing
Download raw message
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...

Re: Assertions should be more debugger-oriented

Details
Message ID
<20240107214208.urdfqasxszq2xwbi@nullprogram.com>
In-Reply-To
<94bfb66d-a931-4a6e-aefd-23487a72c6f4@localhost> (view parent)
DKIM signature
missing
Download raw message
> 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

Re: Assertions should be more debugger-oriented

Details
Message ID
<20240112210447.mxhvo7bg4mjp4jyz@nullprogram.com>
In-Reply-To
<abfbidq522enpcyjpbvrqhqycrain44slnpmxjtdueu54ml4zz@kuxizhnu3rei> (view parent)
DKIM signature
missing
Download raw message
> 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.

Re: Assertions should be more debugger-oriented

Details
Message ID
<abfbidq522enpcyjpbvrqhqycrain44slnpmxjtdueu54ml4zz@kuxizhnu3rei>
In-Reply-To
<20240107214208.urdfqasxszq2xwbi@nullprogram.com> (view parent)
DKIM signature
pass
Download raw message
> 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
Reply to thread Export thread (mbox)