Skip to content

Commit

Permalink
[lldb] Retrieve currently handled Obj-C exception via __cxa_current_e…
Browse files Browse the repository at this point in the history
…xception_type and add GetCurrentExceptionBacktrace SB ABI

This builds on https://reviews.llvm.org/D43884 and https://reviews.llvm.org/D43886 and extends LLDB support of Obj-C exceptions to also look for a "current exception" for a thread in the C++ exception handling runtime metadata (via call to __cxa_current_exception_type). We also construct an actual historical SBThread/ThreadSP that contains frames from the backtrace in the Obj-C exception object.

The high level goal this achieves is that when we're already crashed (because an unhandled exception occurred), we can still access the exception object and retrieve the backtrace from the throw point. In Obj-C, this is particularly useful because a catch+rethrow is very common and in those cases you currently don't have any access to the throw point backtrace.

Differential Revision: https://reviews.llvm.org/D44072

llvm-svn: 349718
  • Loading branch information
kubamracek committed Dec 20, 2018
1 parent 17e7051 commit c9e1190
Show file tree
Hide file tree
Showing 14 changed files with 355 additions and 35 deletions.
3 changes: 1 addition & 2 deletions lldb/include/lldb/API/SBThread.h
Expand Up @@ -200,8 +200,7 @@ class LLDB_API SBThread {

SBValue GetCurrentException();

// TODO(kubamracek): Extract backtrace from SBValue into SBThread
// SBThread GetCurrentExceptionBacktrace();
SBThread GetCurrentExceptionBacktrace();

bool SafeToCallFunctions();

Expand Down
11 changes: 11 additions & 0 deletions lldb/include/lldb/Target/LanguageRuntime.h
Expand Up @@ -119,6 +119,17 @@ class LanguageRuntime : public PluginInterface {
static Breakpoint::BreakpointPreconditionSP
CreateExceptionPrecondition(lldb::LanguageType language, bool catch_bp,
bool throw_bp);

virtual lldb::ValueObjectSP GetExceptionObjectForThread(
lldb::ThreadSP thread_sp) {
return lldb::ValueObjectSP();
}

virtual lldb::ThreadSP GetBacktraceThreadFromException(
lldb::ValueObjectSP thread_sp) {
return lldb::ThreadSP();
}

Process *GetProcess() { return m_process; }

Target &GetTargetRef() { return m_process->GetTarget(); }
Expand Down
3 changes: 1 addition & 2 deletions lldb/include/lldb/Target/Thread.h
Expand Up @@ -1255,8 +1255,7 @@ class Thread : public std::enable_shared_from_this<Thread>,

lldb::ValueObjectSP GetCurrentException();

// TODO(kubamracek): Extract backtrace from ValueObjectSP into ThreadSP
// lldb::ThreadSP GetCurrentExceptionBacktrace();
lldb::ThreadSP GetCurrentExceptionBacktrace();

protected:
friend class ThreadPlan;
Expand Down
@@ -1,6 +1,6 @@
LEVEL = ../../../make

OBJC_SOURCES := main.m
OBJCXX_SOURCES := main.mm

CFLAGS_EXTRAS += -w

Expand Down
Expand Up @@ -17,13 +17,14 @@ class ObjCExceptionsTestCase(TestBase):
mydir = TestBase.compute_mydir(__file__)

@skipUnlessDarwin
def test_objc_exceptions_1(self):
def test_objc_exceptions_at_throw(self):
self.build()

target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
self.assertTrue(target, VALID_TARGET)

lldbutil.run_to_name_breakpoint(self, "objc_exception_throw")
launch_info = lldb.SBLaunchInfo(["a.out", "0"])
lldbutil.run_to_name_breakpoint(self, "objc_exception_throw", launch_info=launch_info)

self.expect("thread list", STOPPED_DUE_TO_BREAKPOINT,
substrs=['stopped', 'stop reason = breakpoint'])
Expand All @@ -33,7 +34,7 @@ def test_objc_exceptions_1(self):
'name: "ThrownException" - reason: "SomeReason"',
])

lldbutil.run_to_source_breakpoint(self, "// Set break point at this line.", lldb.SBFileSpec("main.m"))
lldbutil.run_to_source_breakpoint(self, "// Set break point at this line.", lldb.SBFileSpec("main.mm"), launch_info=launch_info)

self.expect("thread list", STOPPED_DUE_TO_BREAKPOINT,
substrs=['stopped', 'stop reason = breakpoint'])
Expand All @@ -42,27 +43,31 @@ def test_objc_exceptions_1(self):
thread = target.GetProcess().GetSelectedThread()
frame = thread.GetSelectedFrame()

# No exception being currently thrown/caught at this point
self.assertFalse(thread.GetCurrentException().IsValid())
self.assertFalse(thread.GetCurrentExceptionBacktrace().IsValid())

self.expect(
'frame variable e1',
substrs=[
'(NSException *) e1 = ',
'name: @"ExceptionName" - reason: @"SomeReason"'
'name: "ExceptionName" - reason: "SomeReason"'
])

self.expect(
'frame variable --dynamic-type no-run-target *e1',
substrs=[
'(NSException) *e1 = ',
'name = ', '@"ExceptionName"',
'reason = ', '@"SomeReason"',
'name = ', '"ExceptionName"',
'reason = ', '"SomeReason"',
'userInfo = ', '1 key/value pair',
'reserved = ', 'nil',
])

e1 = frame.FindVariable("e1")
self.assertTrue(e1)
self.assertEqual(e1.type.name, "NSException *")
self.assertEqual(e1.GetSummary(), 'name: @"ExceptionName" - reason: @"SomeReason"')
self.assertEqual(e1.GetSummary(), 'name: "ExceptionName" - reason: "SomeReason"')
self.assertEqual(e1.GetChildMemberWithName("name").description, "ExceptionName")
self.assertEqual(e1.GetChildMemberWithName("reason").description, "SomeReason")
userInfo = e1.GetChildMemberWithName("userInfo").dynamic
Expand All @@ -75,23 +80,23 @@ def test_objc_exceptions_1(self):
'frame variable e2',
substrs=[
'(NSException *) e2 = ',
'name: @"ThrownException" - reason: @"SomeReason"'
'name: "ThrownException" - reason: "SomeReason"'
])

self.expect(
'frame variable --dynamic-type no-run-target *e2',
substrs=[
'(NSException) *e2 = ',
'name = ', '@"ThrownException"',
'reason = ', '@"SomeReason"',
'name = ', '"ThrownException"',
'reason = ', '"SomeReason"',
'userInfo = ', '1 key/value pair',
'reserved = ',
])

e2 = frame.FindVariable("e2")
self.assertTrue(e2)
self.assertEqual(e2.type.name, "NSException *")
self.assertEqual(e2.GetSummary(), 'name: @"ThrownException" - reason: @"SomeReason"')
self.assertEqual(e2.GetSummary(), 'name: "ThrownException" - reason: "SomeReason"')
self.assertEqual(e2.GetChildMemberWithName("name").description, "ThrownException")
self.assertEqual(e2.GetChildMemberWithName("reason").description, "SomeReason")
userInfo = e2.GetChildMemberWithName("userInfo").dynamic
Expand All @@ -106,5 +111,84 @@ def test_objc_exceptions_1(self):

pcs = [i.unsigned for i in children]
names = [target.ResolveSymbolContextForAddress(lldb.SBAddress(pc, target), lldb.eSymbolContextSymbol).GetSymbol().name for pc in pcs]
for n in ["objc_exception_throw", "foo", "main"]:
for n in ["objc_exception_throw", "foo(int)", "main"]:
self.assertTrue(n in names, "%s is in the exception backtrace (%s)" % (n, names))

@skipUnlessDarwin
def test_objc_exceptions_at_abort(self):
self.build()

target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
self.assertTrue(target, VALID_TARGET)

self.runCmd("run 0")

# We should be stopped at pthread_kill because of an unhandled exception
self.expect("thread list",
substrs=['stopped', 'stop reason = signal SIGABRT'])

self.expect('thread exception', substrs=[
'(NSException *) exception = ',
'name: "ThrownException" - reason: "SomeReason"',
'libobjc.A.dylib`objc_exception_throw',
'a.out`foo', 'at main.mm:25',
'a.out`rethrow', 'at main.mm:36',
'a.out`main',
])

process = self.dbg.GetSelectedTarget().process
thread = process.GetSelectedThread()

# There is an exception being currently processed at this point
self.assertTrue(thread.GetCurrentException().IsValid())
self.assertTrue(thread.GetCurrentExceptionBacktrace().IsValid())

history_thread = thread.GetCurrentExceptionBacktrace()
self.assertGreaterEqual(history_thread.num_frames, 4)
for n in ["objc_exception_throw", "foo(int)", "rethrow(int)", "main"]:
self.assertEqual(len([f for f in history_thread.frames if f.GetFunctionName() == n]), 1)

self.runCmd("kill")

self.runCmd("run 1")
# We should be stopped at pthread_kill because of an unhandled exception
self.expect("thread list",
substrs=['stopped', 'stop reason = signal SIGABRT'])

self.expect('thread exception', substrs=[
'(MyCustomException *) exception = ',
'libobjc.A.dylib`objc_exception_throw',
'a.out`foo', 'at main.mm:27',
'a.out`rethrow', 'at main.mm:36',
'a.out`main',
])

process = self.dbg.GetSelectedTarget().process
thread = process.GetSelectedThread()

history_thread = thread.GetCurrentExceptionBacktrace()
self.assertGreaterEqual(history_thread.num_frames, 4)
for n in ["objc_exception_throw", "foo(int)", "rethrow(int)", "main"]:
self.assertEqual(len([f for f in history_thread.frames if f.GetFunctionName() == n]), 1)

@skipUnlessDarwin
def test_cxx_exceptions_at_abort(self):
self.build()

target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
self.assertTrue(target, VALID_TARGET)

self.runCmd("run 2")

# We should be stopped at pthread_kill because of an unhandled exception
self.expect("thread list",
substrs=['stopped', 'stop reason = signal SIGABRT'])

self.expect('thread exception', substrs=[])

process = self.dbg.GetSelectedTarget().process
thread = process.GetSelectedThread()

# C++ exceptions are not exposed in the API (yet).
self.assertFalse(thread.GetCurrentException().IsValid())
self.assertFalse(thread.GetCurrentExceptionBacktrace().IsValid())
Expand Up @@ -9,13 +9,37 @@

#import <Foundation/Foundation.h>

void foo()
#import <exception>
#import <stdexcept>

@interface MyCustomException: NSException
@end
@implementation MyCustomException
@end

void foo(int n)
{
NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:@"some_value", @"some_key", nil];
@throw [[NSException alloc] initWithName:@"ThrownException" reason:@"SomeReason" userInfo:info];
switch (n) {
case 0:
@throw [[NSException alloc] initWithName:@"ThrownException" reason:@"SomeReason" userInfo:info];
case 1:
@throw [[MyCustomException alloc] initWithName:@"ThrownException" reason:@"SomeReason" userInfo:info];
case 2:
throw std::runtime_error("C++ exception");
}
}

int main (int argc, const char * argv[])
void rethrow(int n)
{
@try {
foo(n);
} @catch(NSException *e) {
@throw;
}
}

int main(int argc, const char * argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

Expand All @@ -24,12 +48,15 @@ int main (int argc, const char * argv[])
NSException *e2;

@try {
foo();
foo(atoi(argv[1]));
} @catch(NSException *e) {
e2 = e;
}

NSLog(@"1"); // Set break point at this line.

rethrow(atoi(argv[1]));

[pool drain];
return 0;
}
Expand Down
18 changes: 18 additions & 0 deletions lldb/scripts/interface/SBThread.i
Expand Up @@ -397,6 +397,24 @@ public:
") GetExtendedBacktraceOriginatingIndexID;
uint32_t
GetExtendedBacktraceOriginatingIndexID();

%feature("autodoc","
Returns an SBValue object represeting the current exception for the thread,
if there is any. Currently, this works for Obj-C code and returns an SBValue
representing the NSException object at the throw site or that's currently
being processes.
") GetCurrentException;
lldb::SBValue
GetCurrentException();

%feature("autodoc","
Returns a historical (fake) SBThread representing the stack trace of an
exception, if there is one for the thread. Currently, this works for Obj-C
code, and can retrieve the throw-site backtrace of an NSException object
even when the program is no longer at the throw site.
") GetCurrentExceptionBacktrace;
lldb::SBThread
GetCurrentExceptionBacktrace();

%feature("autodoc","
Takes no arguments, returns a bool.
Expand Down
3 changes: 1 addition & 2 deletions lldb/source/API/SBThread.cpp
Expand Up @@ -1491,13 +1491,12 @@ SBValue SBThread::GetCurrentException() {
return SBValue(thread_sp->GetCurrentException());
}

/* TODO(kubamracek)
SBThread SBThread::GetCurrentExceptionBacktrace() {
ThreadSP thread_sp(m_opaque_sp->GetThreadSP());
if (!thread_sp) return SBThread();

return SBThread(thread_sp->GetCurrentExceptionBacktrace());
}*/
}

bool SBThread::SafeToCallFunctions() {
ThreadSP thread_sp(m_opaque_sp->GetThreadSP());
Expand Down
5 changes: 2 additions & 3 deletions lldb/source/Commands/CommandObjectThread.cpp
Expand Up @@ -1552,14 +1552,13 @@ class CommandObjectThreadException : public CommandObjectIterateOverThreads {
exception_object_sp->Dump(strm);
}

/* TODO(kubamracek)
ThreadSP exception_thread_sp = thread_sp->GetCurrentExceptionBacktrace();
if (exception_thread_sp && exception_thread_sp->IsValid()) {
const uint32_t num_frames_with_source = 0;
const bool stop_format = false;
exception_thread_sp->GetStatus(strm, m_options.m_start, m_options.m_count,
exception_thread_sp->GetStatus(strm, 0, UINT32_MAX,
num_frames_with_source, stop_format);
}*/
}

return true;
}
Expand Down

0 comments on commit c9e1190

Please sign in to comment.