Breaking on C++ exception seems to discard much of the stacktrace

I want to stop the debugger when a C++ exception is thrown. I start LLDB and run: break set -E c++. Then run and the program. It stops and this is my stack. As shown below this is not not the stack at the point of the C++ throw. Shouldn’t it be?

This is on macOS, so there is a mix of C, Obj-C, and C++ in the stacktrace.

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 6.2
  * frame #0: 0x00007ff804804e88 libc++abi.dylib`__cxa_rethrow
    frame #1: 0x00007ff8044fc90e libobjc.A.dylib`objc_exception_rethrow + 37
    frame #2: 0x00007ff805754962 Foundation`-[NSAppleEventManager dispatchRawAppleEvent:withRawReply:handlerRefCon:] + 482
    frame #3: 0x00007ff805754726 Foundation`_NSAppleEventManagerGenericHandler + 80
    frame #4: 0x00007ff80adfed9d AE`___lldb_unnamed_symbol829 + 1848
    frame #5: 0x00007ff80adfe608 AE`___lldb_unnamed_symbol828 + 34
    frame #6: 0x00007ff80adf7c1d AE`aeProcessAppleEvent + 419
    frame #7: 0x00007ff80e281e3f HIToolbox`AEProcessAppleEvent + 54
    frame #8: 0x00007ff807a73630 AppKit`_DPSNextEvent + 1738
    frame #9: 0x00007ff807a72174 AppKit`-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 1219
    frame #10: 0x00007ff807a647b7 AppKit`-[NSApplication run] + 586
    frame #11: 0x00007ff807a387f7 AppKit`NSApplicationMain + 817
    frame #12: 0x000000010002e149 buttons`UI::Apple::ApplicationMain(argc=1, argv=0x00007ff7bfeff810, delegate=0x000060000000c000) at Main.mm:60:5
    frame #13: 0x0000000100009151 buttons`main(argc=1, argv=0x00007ff7bfeff810) at main.cc:27:9
    frame #14: 0x00007ff804516310 dyld`start + 2432

It looks like system library code is throwing the exception. Eventually I found the line of code that throws the exception. Here it is below. (The buttons binary is my test program.) If I step over that line, the exception gets thrown and I teleport to the shortened (and unhelpful) stacktrace shown above.

(lldb) 
Process 85259 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x000000010007fc8a buttons`_ZN4objcW12AppleInteropW4objc12ClassBuilder9addMethodEPKcPFvvE(this=0x00007ff7bfefdb38, sel="displayLayer:", imp=(buttons`mcv_displayLayer(objc_object*, objc_selector*, void*) at MetalContentView.cc:290)) at objc_help.cc:140:17
   137 	        if (proto) {
   138 	            auto method = protocol_getMethodDescription(proto, selector, true, true);
   139 	            if (!method.name) {
-> 140 	                throw std::runtime_error(std::format("Protocol doesn't have method: {}", sel));
   141 	            }
   142 	            class_addMethod(cls, selector, imp, method.types);
   143 	        } else if (baseClass) {
Target 0: (buttons) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
  * frame #0: 0x000000010007fc8a buttons`_ZN4objcW12AppleInteropW4objc12ClassBuilder9addMethodEPKcPFvvE(this=0x00007ff7bfefdb38, sel="displayLayer:", imp=(buttons`mcv_displayLayer(objc_object*, objc_selector*, void*) at MetalContentView.cc:290)) at objc_help.cc:140:17
    frame #1: 0x000000010004c95f buttons`setupClass() at MetalContentView.cc:846:8
    frame #2: 0x000000010004c73a buttons`MetalContentView::create() at MetalContentView.cc:874:9
    frame #3: 0x000000010005c131 buttons`UI::Window::Window(this=0x0000600000004840, w=400, h=600) at Window.cc:67:28
    frame #4: 0x000000010005c331 buttons`UI::Window::Window(this=0x0000600000004840, w=400, h=600) at Window.cc:55:30
    frame #5: 0x000000010000a4d7 buttons`AppDelegate::applicationDidFinishLaunching(this=0x0000600000008060) at main.cc:129:18
    frame #6: 0x000000010002e082 buttons`-[AppDelegateAdapter applicationDidFinishLaunching:](self=0x0000600000008070, _cmd="applicationDidFinishLaunching:", aNotification="NSApplicationDidFinishLaunchingNotification") at Main.mm:33:10
    frame #7: 0x00007ff80491a3d4 CoreFoundation`__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 137
    frame #8: 0x00007ff8049b441a CoreFoundation`___CFXRegistrationPost_block_invoke + 88
    frame #9: 0x00007ff8049b4369 CoreFoundation`_CFXRegistrationPost + 536
    frame #10: 0x00007ff8048ed8f9 CoreFoundation`_CFXNotificationPost + 735
    frame #11: 0x00007ff805729abc Foundation`-[NSNotificationCenter postNotificationName:object:userInfo:] + 82
    frame #12: 0x00007ff807a7b034 AppKit`-[NSApplication _postDidFinishNotification] + 305
    frame #13: 0x00007ff807a7ad86 AppKit`-[NSApplication _sendFinishLaunchingNotification] + 208
    frame #14: 0x00007ff807a78ab2 AppKit`-[NSApplication(NSAppleEventHandling) _handleAEOpenEvent:] + 541
    frame #15: 0x00007ff807a78708 AppKit`-[NSApplication(NSAppleEventHandling) _handleCoreEvent:withReplyEvent:] + 665
    frame #16: 0x00007ff8057548b4 Foundation`-[NSAppleEventManager dispatchRawAppleEvent:withRawReply:handlerRefCon:] + 308
    frame #17: 0x00007ff805754726 Foundation`_NSAppleEventManagerGenericHandler + 80
    frame #18: 0x00007ff80adfed9d AE`___lldb_unnamed_symbol829 + 1848
    frame #19: 0x00007ff80adfe608 AE`___lldb_unnamed_symbol828 + 34
    frame #20: 0x00007ff80adf7c1d AE`aeProcessAppleEvent + 419
    frame #21: 0x00007ff80e281e3f HIToolbox`AEProcessAppleEvent + 54
    frame #22: 0x00007ff807a73630 AppKit`_DPSNextEvent + 1738
    frame #23: 0x00007ff807a72174 AppKit`-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 1219
    frame #24: 0x00007ff807a647b7 AppKit`-[NSApplication run] + 586
    frame #25: 0x00007ff807a387f7 AppKit`NSApplicationMain + 817
    frame #26: 0x000000010002e149 buttons`UI::Apple::ApplicationMain(argc=1, argv=0x00007ff7bfeff810, delegate=0x0000600000008060) at Main.mm:60:5
    frame #27: 0x0000000100009151 buttons`main(argc=1, argv=0x00007ff7bfeff810) at main.cc:27:9
    frame #28: 0x00007ff804516310 dyld`start + 2432
(lldb) 

As a first check I like to do on a suspected unwind bug, when I’m stopped there I’ll run sample on my (stopped) process from a separate terminal window. This will show a different algorithm’s backtrace of the stacks. It is possible for lldb’s unwinder to miss a stack frame, most often the currently-executing stack frame where lldb will do static instruction analysis to track register spills and restores during the lifetime of the function, most often a problem on hand-written assembly and much less frequently, on complicated optimized functions. But once we’ve unwound past the first stack frame, we will be using the Darwin compact unwind information to walk the stack and I know of no problems with that algorithm.

Is it possible to construct a standalone ObjC++ project that reproduces the backtrace bug?

I just ran sample on the process when it was stopped in the debugger, after the exception breakpoint. It shows the same stacktrace – the one that is too short.

It’s not going to be easy to distill this to a sample program. I may try later, but I probably have too much else to do, so I will try to find some other way to debug my program with lots of logging and catching exceptions.

I’m running Clang 17 but using the LLDB that comes with Apple’s system. I may try building LLDB from source (as I do Clang), to see if that helps.

A debugger is a nice idea but LLDB always gives me problems. Half the time it can’t print symbols from the C++ code, and it just segfaulted trying to TAB complete a symbol!

i’d be surprised if anything “above” the throw is relevant to the issue, fwiw. I’d expect no difference between the currrent lldb sources and a recent Xcode lldb in this area. As for symbols that don’t print, without specifics, I can’t guess what you might have been seeing. I use lldb to debug lldb regularly.

I agree. But was it clear in my post that I can’t see the source location of the throw either? I only found that by other means (catching exceptions in C++, printing their messages). LLDB only shows me the __cxa_rethrow frame, which displays as assembly code.

The fact that you when you stop you are at __cxa_rethrow makes it sound like the exception was thrown, caught and then rethrown later with the stack frame that you showed, but we only caught the rethrow. Or maybe, this is a separate exception, and we messed the one you were expecting to stop at altogether.

Note that at present, lldb assumes all C++ throws will either go through __cxa_rethrow - the one caught here - or __cxa_throw which is how it usually happens.

So first we should make sure that the breakpoint at __cxa_throw was set but not hit by running:

(lldb) break list

when you stop unexpectedly at __cxa_rethrow. There should be a breakpoint on those two symbols listed. If that’s indeed set and has a “location with a reasonable address” for __cxa_throw, but has a hit count of 0 then there must be some other way the C++ runtime is throwing these days other than the two methods lldb knows about.

If you are interested, you can try instruction stepping through the throw code till you see the exception get thrown, that would tell us which symbol we need to add to catch this throw. Or if you can make up an example that shows this behavior, we can do the same.

Jim

On Mar 16, 2023, at 9:10 AM, RobNik via LLVM Discussion Forums notifications@llvm.discoursemail.com wrote:

RobNik
March 16

jasonmolenda:

i’d be surprised if anything “above” the throw is relevant to the issue, fwiw.

I agree. But was it clear in my post that I can’t see the source location of the throw either? I only found that by other means (catching exceptions in C++, printing their messages). LLDB only shows me the __cxa_rethrow frame, which displays as assembly code.


Visit Topic or reply to this email to respond.

To unsubscribe from these emails, click here.

If I run the program as before, it stops in __cxa_rethrow, and I can see that __cxa_throw was not hit:

(lldb) break list
Current breakpoints:
1: Exception breakpoint (catch: off throw: on) using: names = {'__cxa_throw', '__cxa_rethrow'}, modules(2) = libc++abi.dylib, libSystem.B.dylib, locations = 2, resolved = 2, hit count = 1
  1.1: where = libc++abi.dylib`__cxa_throw, address = 0x00007ff804804c58, resolved, hit count = 0 
  1.2: where = libc++abi.dylib`__cxa_rethrow, address = 0x00007ff804804e88, resolved, hit count = 1 

If set the break manually to file:line, and re-run, I can then do stepi and see it go into __cxa_throw, after allocating and constructing the exception.

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
  * frame #0: 0x000000010056ccb1 libc++abi.1.dylib`__cxa_get_globals + 1
    frame #1: 0x000000010056fe4c libc++abi.1.dylib`__cxa_throw + 28
    frame #2: 0x000000010007f2c9 buttons`_ZN4objcW12AppleInteropW4objc12ClassBuilder9addMethodEPKcPFvvE(this=0x00007ff7bfefdb50, sel="displayLayer:", imp=(buttons`mcv_displayLayer(objc_object*, objc_selector*, void*) at MetalContentView.cc:290)) at objc_help.cc:412:9

So the question seems to be, why is that not triggering the breakpoint?

Could this be related to my building Clang and libc++ myself from the git HEAD? I did that so I could use the latest C++ modules. There must be a builtin libc++ somewhere on the system (I don’t know where offhand, it’s not in /usr/lib). But my binary is linking to the one I built, in ~/Dev/llvm-project/build/lib/.

It looks like we set the breakpoint on libc++abi.dylib, but then you seem to stop on an __cxa_throw in libc++abi.1.dylib. Do you have two libc++abi libraries loaded into the same program? You can confirm this by looking at the results of image list.

We restrict the throw breakpoints to a set of libraries since we don’t want to have to search in lots of places where we aren’t going to find it. But that list includes both “libc++abi.dylib” and “libc++abi.1.dylib” so the breakpoint should have gotten set on both of these symbols. But the exception breakpoints are special since you don’t really know what kind of c++ runtime you’re going to find till you have a real process, so it might be that something is causing this breakpoint to not keep searching after the first hit in libc++abi.dylib.

But if you are indeed loading two copies of libc++abi, you probably want to not do that in general, not just because it seems to trip up our C+ exception catching mechanism.

Jim

On Mar 16, 2023, at 10:28 AM, RobNik via LLVM Discussion Forums notifications@llvm.discoursemail.com wrote:

RobNik
March 16

If I run the program as before, it stops in __cxa_rethrow, and I can see that __cxa_throw was not hit:

(lldb) break list
Current breakpoints:
1: Exception breakpoint (catch: off throw: on) using: names = {'__cxa_throw', '__cxa_rethrow'}, modules(2) = libc++abi.dylib, libSystem.B.dylib, locations = 2, resolved = 2, hit count = 1
  1.1: where = libc++abi.dylib`__cxa_throw, address = 0x00007ff804804c58, resolved, hit count = 0 
  1.2: where = libc++abi.dylib`__cxa_rethrow, address = 0x00007ff804804e88, resolved, hit count = 1 

If set the break manually to file:line, and re-run, I can then do stepi and see it go into __cxa_throw, after allocating and constructing the exception.

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
  * frame #0: 0x000000010056ccb1 libc++abi.1.dylib`__cxa_get_globals + 1
    frame #1: 0x000000010056fe4c libc++abi.1.dylib`__cxa_throw + 28
    frame #2: 0x000000010007f2c9 buttons`_ZN4objcW12AppleInteropW4objc12ClassBuilder9addMethodEPKcPFvvE(this=0x00007ff7bfefdb50, sel="displayLayer:", imp=(buttons`mcv_displayLayer(objc_object*, objc_selector*, void*) at [MetalContentView.cc:290](http://metalcontentview.cc:290/))) at [objc_help.cc:412](http://objc_help.cc:412/):9

So the question seems to be, why is that not triggering the breakpoint?

Could this be related to my building Clang and libc++ myself from the git HEAD? I did that so I could use the latest C++ modules. There must be a builtin libc++ somewhere on the system (I don’t know where offhand, it’s not in /usr/lib). But my binary is linking to the one I built, in ~/Dev/llvm-project/build/lib/.


Visit Topic or reply to this email to respond.

To unsubscribe from these emails, click here.

I had trouble getting my program to link and run, after I built Clang from source, so I’m probably doing something wrong there.

If I run image list, yes, I see multiple C++ libs. But some don’t correspond to a file on disk. Maybe the macOS linker somehow finds them somewhere else?

(lldb) image list
... skipping lots of lines ...
[ 23] DA42E8C2-95EE-31B8-9058-5523BA0E326E 0x0000000000000000 /Users/rob/Dev/llvm-project/build/lib/libc++.1.dylib
[ 66] B8339FB2-CBAE-3D84-B080-BD19DDB2981C 0x00007ff80031e000 /usr/lib/libc++.1.dylib 
[ 90] AF1F6DD6-2182-37B5-9E27-0CC62C440696 0x00007ff800377000 /usr/lib/libc++abi.dylib 
[150] 786B9A5A-5E72-3E5F-81BA-DBC69BE60780 0x0000000000000000 /Users/rob/Dev/llvm-project/build/lib/libc++abi.1.dylib 

Those /usr/lib paths don’t exist on my computer.

% ls -l /usr/lib/libc++.1.dylib
ls: /usr/lib/libc++.1.dylib: No such file or directory

The zeros next to the custom-built ones make me wonder. What does that mean? They’re not loaded? I’m successfully using bleeding edge features libc++, but so much of it is header-based, it could be that I’m getting that from headers and then linked to the wrong binary at run time.

Looking at my CMakeLists.txt file, I see:

set(MY_CLANG_DIR /Users/rob/Dev/llvm-project/build)
set(CMAKE_CXX_COMPILER ${MY_CLANG_DIR}/bin/clang++)
...
set(CMAKE_EXE_LINKER_FLAGS "-L ${MY_CLANG_DIR}/lib -rpath ${MY_CLANG_DIR}/lib")

I don’t know how to tell it to stop linking to the system one. Probably I want to force static linking somehow. (?)