diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp --- a/lldb/source/Target/Target.cpp +++ b/lldb/source/Target/Target.cpp @@ -1682,6 +1682,34 @@ m_breakpoint_list.UpdateBreakpoints(module_list, false, delete_locations); m_internal_breakpoint_list.UpdateBreakpoints(module_list, false, delete_locations); + + // If a module was torn down it will have torn down the 'TypeSystemClang's + // that we used as source 'ASTContext's for the persistent variables in + // the current target. Those would now be unsafe to access because the + // 'DeclOrigin' are now possibly stale. Thus clear all persistent + // variables. We only want to flush 'TypeSystem's if the module being + // unloaded was capable of describing a source type. JITted module unloads + // happen frequently for Objective-C utility functions or the REPL and rely + // on the persistent variables to stick around. + const bool should_flush_type_systems = + module_list.AnyOf([](lldb_private::Module &module) { + auto *object_file = module.GetObjectFile(); + + if (!object_file) + return false; + + auto type = object_file->GetType(); + + // eTypeExecutable: when debugged binary was rebuilt + // eTypeSharedLibrary: if dylib was re-loaded + return module.FileHasChanged() && + (type == ObjectFile::eTypeObjectFile || + type == ObjectFile::eTypeExecutable || + type == ObjectFile::eTypeSharedLibrary); + }); + + if (should_flush_type_systems) + m_scratch_type_system_map.Clear(); } } diff --git a/lldb/test/API/functionalities/rerun_and_expr/Makefile b/lldb/test/API/functionalities/rerun_and_expr/Makefile new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/rerun_and_expr/Makefile @@ -0,0 +1 @@ +include Makefile.rules diff --git a/lldb/test/API/functionalities/rerun_and_expr/TestRerunAndExpr.py b/lldb/test/API/functionalities/rerun_and_expr/TestRerunAndExpr.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/rerun_and_expr/TestRerunAndExpr.py @@ -0,0 +1,62 @@ +""" +Test that re-running a process from within the same target +after rebuilding the executable flushes the scratch TypeSystems +tied to that process. +""" + +import lldb +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil + +class TestRerunExpr(TestBase): + def test(self): + """ + Tests whether re-launching a process without destroying + the owning target keeps invalid ASTContexts in the + scratch AST's importer. + + We test this by: + 1. Evaluating an expression to import 'struct Foo' into + the scratch AST + 2. Change the definition of 'struct Foo' and rebuild the executable + 3. Re-launch the process + 4. Evaluate the same expression in (1). We expect to have only + the latest definition of 'struct Foo' in the scratch AST. + """ + self.build(dictionary={'CXX_SOURCES':'main.cpp', 'EXE':'a.out'}) + + exe = self.getBuildArtifact("a.out") + target = self.dbg.CreateTarget(exe) + target.BreakpointCreateBySourceRegex('return', lldb.SBFileSpec('rebuild.cpp')) + target.BreakpointCreateBySourceRegex('return', lldb.SBFileSpec('main.cpp')) + process = target.LaunchSimple(None, None, self.get_process_working_directory()) + + self.expect_expr('foo', result_type='Foo', result_children=[ + ValueCheck(name='m_val', value='42') + ]) + + self.build(dictionary={'CXX_SOURCES':'rebuild.cpp', 'EXE':'a.out'}) + + # Rerun program within the same target + process.Destroy() + process = target.LaunchSimple(None, None, self.get_process_working_directory()) + + self.expect_expr('foo', result_type='Foo', result_children=[ + ValueCheck(name='Base', children=[ + ValueCheck(name='m_base_val', value='42') + ]), + ValueCheck(name='m_derived_val', value='137') + ]) + + self.filecheck("target module dump ast", __file__) + + # The new definition 'struct Foo' is in the scratch AST + # CHECK: |-CXXRecordDecl {{.*}} struct Foo definition + # CHECK: | |-public 'Base' + # CHECK-NEXT: | `-FieldDecl {{.*}} m_derived_val 'int' + # CHECK-NEXT: `-CXXRecordDecl {{.*}} struct Base definition + # CHECK: `-FieldDecl {{.*}} m_base_val 'int' + + # ...but the original definition of 'struct Foo' is not in the scratch AST anymore + # CHECK-NOT: FieldDecl {{.*}} m_val 'int' + diff --git a/lldb/test/API/functionalities/rerun_and_expr/main.cpp b/lldb/test/API/functionalities/rerun_and_expr/main.cpp new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/rerun_and_expr/main.cpp @@ -0,0 +1,8 @@ +struct Foo { + int m_val = 42; +}; + +int main() { + Foo foo; + return 0; +} diff --git a/lldb/test/API/functionalities/rerun_and_expr/rebuild.cpp b/lldb/test/API/functionalities/rerun_and_expr/rebuild.cpp new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/rerun_and_expr/rebuild.cpp @@ -0,0 +1,12 @@ +struct Base { + int m_base_val = 42; +}; + +struct Foo : public Base { + int m_derived_val = 137; +}; + +int main() { + Foo foo; + return 0; +} diff --git a/lldb/test/API/functionalities/rerun_and_expr_dylib/Makefile b/lldb/test/API/functionalities/rerun_and_expr_dylib/Makefile new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/rerun_and_expr_dylib/Makefile @@ -0,0 +1,3 @@ +CXX_SOURCES := main.cpp + +include Makefile.rules diff --git a/lldb/test/API/functionalities/rerun_and_expr_dylib/TestRerunAndExprDylib.py b/lldb/test/API/functionalities/rerun_and_expr_dylib/TestRerunAndExprDylib.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/rerun_and_expr_dylib/TestRerunAndExprDylib.py @@ -0,0 +1,84 @@ +""" +Test that re-running a process from within the same target +after rebuilding the a dynamic library flushes the scratch +TypeSystems tied to that process. +""" + +import lldb +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil +from lldbsuite.test.decorators import * + +class TestRerunExprDylib(TestBase): + @skipIfWindows + def test(self): + """ + Tests whether re-launching a process without destroying + the owning target keeps invalid ASTContexts in the + scratch AST's importer. + + We test this by: + 1. Evaluating an expression to import 'struct Foo' into + the scratch AST + 2. Change the definition of 'struct Foo' and rebuild the dylib + 3. Re-launch the process + 4. Evaluate the same expression in (1). We expect to have only + the latest definition of 'struct Foo' in the scratch AST. + """ + + DYLIB_NAME = 'foo' + FULL_DYLIB_NAME = 'libfoo.dylib' if self.platformIsDarwin() else 'libfoo.so' + + # Build libfoo.dylib + self.build(dictionary={'DYLIB_CXX_SOURCES':'lib.cpp', + 'DYLIB_ONLY':'YES', + 'DYLIB_NAME':DYLIB_NAME, + 'USE_LIBDL':'1', + 'LD_EXTRAS':'-L.'}) + + # Build a.out + self.build(dictionary={'EXE':'a.out', + 'CXX_SOURCES':'main.cpp', + 'USE_LIBDL':'1', + 'CXXFLAGS_EXTRAS':f'-DLIB_NAME=\\"{FULL_DYLIB_NAME}\\"', + 'LD_EXTRAS':'-L.'}) + + exe = self.getBuildArtifact("a.out") + target = self.dbg.CreateTarget(exe) + breakpoint = target.BreakpointCreateBySourceRegex('return', lldb.SBFileSpec('main.cpp')) + process = target.LaunchSimple(None, None, self.get_process_working_directory()) + + self.expect_expr('*foo', result_type='Foo', result_children=[ + ValueCheck(name='m_val', value='42') + ]) + + # Re-build libfoo.dylib + self.build(dictionary={'DYLIB_CXX_SOURCES':'rebuild.cpp', + 'DYLIB_ONLY':'YES', + 'DYLIB_NAME':DYLIB_NAME, + 'USE_LIBDL':'1', + 'LD_EXTRAS':'-L.'}) + + # Rerun program within the same target + process.Destroy() + process = target.LaunchSimple(None, None, self.get_process_working_directory()) + + self.expect_expr('*foo', result_type='Foo', result_children=[ + ValueCheck(name='Base', children=[ + ValueCheck(name='m_base_val', value='42') + ]), + ValueCheck(name='m_derived_val', value='137') + ]) + + self.filecheck("target module dump ast", __file__) + + # The new definition 'struct Foo' is in the scratch AST + # CHECK: |-CXXRecordDecl {{.*}} struct Foo definition + # CHECK: | |-public 'Base' + # CHECK-NEXT: | `-FieldDecl {{.*}} m_derived_val 'int' + # CHECK-NEXT: `-CXXRecordDecl {{.*}} struct Base definition + # CHECK: `-FieldDecl {{.*}} m_base_val 'int' + + # ...but the original definition of 'struct Foo' is not in the scratch AST anymore + # CHECK-NOT: FieldDecl {{.*}} m_val 'int' + diff --git a/lldb/test/API/functionalities/rerun_and_expr_dylib/lib.cpp b/lldb/test/API/functionalities/rerun_and_expr_dylib/lib.cpp new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/rerun_and_expr_dylib/lib.cpp @@ -0,0 +1 @@ +LLDB_DYLIB_EXPORT struct Foo { int m_val = 42; } global_foo; diff --git a/lldb/test/API/functionalities/rerun_and_expr_dylib/main.cpp b/lldb/test/API/functionalities/rerun_and_expr_dylib/main.cpp new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/rerun_and_expr_dylib/main.cpp @@ -0,0 +1,17 @@ +#include +#include +#include + +extern struct Foo imported; + +int main() { + // LIB_NAME defined on commandline + std::string libname{"./"}; + libname += LIB_NAME; + + void *handle = dlopen(libname.c_str(), RTLD_NOW); + struct Foo *foo = (struct Foo *)dlsym(handle, "global_foo"); + assert(foo != nullptr); + + return 0; +} diff --git a/lldb/test/API/functionalities/rerun_and_expr_dylib/rebuild.cpp b/lldb/test/API/functionalities/rerun_and_expr_dylib/rebuild.cpp new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/rerun_and_expr_dylib/rebuild.cpp @@ -0,0 +1,7 @@ +struct Base { + int m_base_val = 42; +}; + +LLDB_DYLIB_EXPORT struct Foo : public Base { + int m_derived_val = 137; +} global_foo;