From 188d290d02d6f1cadfb4f7aba7e8420613e0bbe8 Mon Sep 17 00:00:00 2001 From: Jeffrey Tan Date: Fri, 31 Oct 2025 17:12:00 -0700 Subject: [PATCH 1/3] Fix lldb-dap non-leaf frame source resolution issue --- lldb/tools/lldb-dap/DAP.cpp | 23 ++++++++++++++++++----- lldb/tools/lldb-dap/ProtocolUtils.cpp | 7 +++---- lldb/tools/lldb-dap/ProtocolUtils.h | 3 ++- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index f009a902f79e7..ebafda95ff583 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -657,18 +657,31 @@ std::optional DAP::ResolveSource(const lldb::SBFrame &frame) { if (!frame.IsValid()) return std::nullopt; - const lldb::SBAddress frame_pc = frame.GetPCAddress(); - if (DisplayAssemblySource(debugger, frame_pc)) + // IMPORTANT: Get line entry from symbol context, NOT from PC address. + // When a frame's PC points to a return address (the instruction + // after a call), that address may have line number 0 for compiler generated + // code. + // + // EXAMPLE: If PC is at 0x1004 (frame return address after the call + // instruction) with no line info, but 0x1003 (in the middle of previous call + // instruction) is at line 42, symbol context returns line 42. + // + // NOTE: This issue is non-deterministic and depends on compiler debug info + // generation, making it difficult to create a reliable automated test. + const lldb::SBLineEntry frame_line_entry = frame.GetLineEntry(); + if (DisplayAssemblySource(debugger, frame_line_entry)) { + const lldb::SBAddress frame_pc = frame.GetPCAddress(); return ResolveAssemblySource(frame_pc); + } - return CreateSource(frame.GetLineEntry().GetFileSpec()); + return CreateSource(frame_line_entry.GetFileSpec()); } std::optional DAP::ResolveSource(lldb::SBAddress address) { - if (DisplayAssemblySource(debugger, address)) + lldb::SBLineEntry line_entry = GetLineEntryForAddress(target, address); + if (DisplayAssemblySource(debugger, line_entry)) return ResolveAssemblySource(address); - lldb::SBLineEntry line_entry = GetLineEntryForAddress(target, address); if (!line_entry.IsValid()) return std::nullopt; diff --git a/lldb/tools/lldb-dap/ProtocolUtils.cpp b/lldb/tools/lldb-dap/ProtocolUtils.cpp index 868c67ca72986..acf31b03f7af0 100644 --- a/lldb/tools/lldb-dap/ProtocolUtils.cpp +++ b/lldb/tools/lldb-dap/ProtocolUtils.cpp @@ -27,7 +27,7 @@ using namespace lldb_dap::protocol; namespace lldb_dap { static bool ShouldDisplayAssemblySource( - lldb::SBAddress address, + lldb::SBLineEntry line_entry, lldb::StopDisassemblyType stop_disassembly_display) { if (stop_disassembly_display == lldb::eStopDisassemblyTypeNever) return false; @@ -37,7 +37,6 @@ static bool ShouldDisplayAssemblySource( // A line entry of 0 indicates the line is compiler generated i.e. no source // file is associated with the frame. - auto line_entry = address.GetLineEntry(); auto file_spec = line_entry.GetFileSpec(); if (!file_spec.IsValid() || line_entry.GetLine() == 0 || line_entry.GetLine() == LLDB_INVALID_LINE_NUMBER) @@ -174,10 +173,10 @@ bool IsAssemblySource(const protocol::Source &source) { } bool DisplayAssemblySource(lldb::SBDebugger &debugger, - lldb::SBAddress address) { + lldb::SBLineEntry line_entry) { const lldb::StopDisassemblyType stop_disassembly_display = GetStopDisassemblyDisplay(debugger); - return ShouldDisplayAssemblySource(address, stop_disassembly_display); + return ShouldDisplayAssemblySource(line_entry, stop_disassembly_display); } std::string GetLoadAddressString(const lldb::addr_t addr) { diff --git a/lldb/tools/lldb-dap/ProtocolUtils.h b/lldb/tools/lldb-dap/ProtocolUtils.h index a1f7ae0661914..f4d576ba9f608 100644 --- a/lldb/tools/lldb-dap/ProtocolUtils.h +++ b/lldb/tools/lldb-dap/ProtocolUtils.h @@ -53,7 +53,8 @@ std::optional CreateSource(const lldb::SBFileSpec &file); /// Checks if the given source is for assembly code. bool IsAssemblySource(const protocol::Source &source); -bool DisplayAssemblySource(lldb::SBDebugger &debugger, lldb::SBAddress address); +bool DisplayAssemblySource(lldb::SBDebugger &debugger, + lldb::SBLineEntry line_entry); /// Get the address as a 16-digit hex string, e.g. "0x0000000000012345" std::string GetLoadAddressString(const lldb::addr_t addr); From 6468d095944ee2c3e91c7a63b1d22eab344d78a0 Mon Sep 17 00:00:00 2001 From: Jeffrey Tan Date: Wed, 5 Nov 2025 12:28:14 -0800 Subject: [PATCH 2/3] Add Test --- .../stackTraceCompilerGeneratedCode/Makefile | 3 + ...TestDAP_stackTraceCompilerGeneratedCode.py | 67 +++++++++++++++++++ .../stackTraceCompilerGeneratedCode/main.c | 17 +++++ lldb/tools/lldb-dap/DAP.cpp | 4 +- 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/Makefile create mode 100644 lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/TestDAP_stackTraceCompilerGeneratedCode.py create mode 100644 lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/main.c diff --git a/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/Makefile b/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/Makefile new file mode 100644 index 0000000000000..10495940055b6 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules diff --git a/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/TestDAP_stackTraceCompilerGeneratedCode.py b/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/TestDAP_stackTraceCompilerGeneratedCode.py new file mode 100644 index 0000000000000..0313ad99bee9c --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/TestDAP_stackTraceCompilerGeneratedCode.py @@ -0,0 +1,67 @@ +""" +Test lldb-dap stackTrace request for compiler generated code +""" + +import os + +import lldbdap_testcase +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * + + +class TestDAP_stackTraceCompilerGeneratedCode(lldbdap_testcase.DAPTestCaseBase): + @skipIfWindows + def test_non_leaf_frame_compiler_generate_code(self): + """ + Test that non-leaf frames with compiler-generated code are properly resolved. + + This test verifies that LLDB correctly handles stack frames containing + compiler-generated code (code without valid source location information). + When a non-leaf frame contains compiler-generated code immediately after a + call instruction, LLDB should resolve the frame's source location to the + call instruction's line, rather than to the compiler-generated code that + follows, which lacks proper symbolication information. + """ + program = self.getBuildArtifact("a.out") + self.build_and_launch(program) + source = "main.c" + + # Set breakpoint inside bar() function + lines = [line_number(source, "// breakpoint here")] + breakpoint_ids = self.set_source_breakpoints(source, lines) + self.assertEqual( + len(breakpoint_ids), len(lines), "expect correct number of breakpoints" + ) + + self.continue_to_breakpoints(breakpoint_ids) + + # Get the stack frames: [0] = bar(), [1] = foo(), [2] = main() + stack_frames = self.get_stackFrames() + self.assertGreater(len(stack_frames), 2, "Expected more than 2 stack frames") + + # Examine the foo() frame (stack_frames[1]) + # This is the critical frame containing compiler-generated code + foo_frame = stack_frames[1] + print(f"foo_frame: {foo_frame}") + + # Verify that the frame's line number points to the bar() call, + # not to the compiler-generated code after it + foo_call_bar_source_line = foo_frame.get("line") + self.assertEqual( + foo_call_bar_source_line, + line_number(source, "foo call bar"), + "Expected foo call bar to be the source line of the frame", + ) + + # Verify the source file name is correctly resolved + foo_source_name = foo_frame.get("source", {}).get("name") + self.assertEqual( + foo_source_name, "main.c", "Expected foo source name to be main.c" + ) + + # When lldb fails to symbolicate a frame it will emit a fake assembly + # source with path of format ` or `
with + # sourceReference to retrieve disassembly source file. + # Verify that this didn't happen - the path should be a real file path. + foo_path = foo_frame.get("source", {}).get("path") + self.assertNotIn("`", foo_path, "Expected foo source path to not contain `") diff --git a/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/main.c b/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/main.c new file mode 100644 index 0000000000000..b431ac818cae7 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/main.c @@ -0,0 +1,17 @@ +void bar() { + int val = 32; // breakpoint here +} + +void at_line_zero() {} + +int foo() { + bar(); // foo call bar +#line 0 "test.cpp" + at_line_zero(); + return 0; +} + +int main(int argc, char const *argv[]) { + foo(); + return 0; +} diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index ebafda95ff583..995c006f1bf55 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -665,9 +665,7 @@ std::optional DAP::ResolveSource(const lldb::SBFrame &frame) { // EXAMPLE: If PC is at 0x1004 (frame return address after the call // instruction) with no line info, but 0x1003 (in the middle of previous call // instruction) is at line 42, symbol context returns line 42. - // - // NOTE: This issue is non-deterministic and depends on compiler debug info - // generation, making it difficult to create a reliable automated test. + const lldb::SBLineEntry frame_line_entry = frame.GetLineEntry(); if (DisplayAssemblySource(debugger, frame_line_entry)) { const lldb::SBAddress frame_pc = frame.GetPCAddress(); From 3b560101e36fe198e0aca4c025b1afe89dd1e07d Mon Sep 17 00:00:00 2001 From: Jeffrey Tan Date: Mon, 10 Nov 2025 14:57:21 -0800 Subject: [PATCH 3/3] Address review feedback --- .../TestDAP_stackTraceCompilerGeneratedCode.py | 3 +-- .../lldb-dap/stackTraceCompilerGeneratedCode/main.c | 12 +++++++----- lldb/tools/lldb-dap/DAP.cpp | 9 --------- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/TestDAP_stackTraceCompilerGeneratedCode.py b/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/TestDAP_stackTraceCompilerGeneratedCode.py index 0313ad99bee9c..4ddf92402ad8a 100644 --- a/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/TestDAP_stackTraceCompilerGeneratedCode.py +++ b/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/TestDAP_stackTraceCompilerGeneratedCode.py @@ -10,7 +10,6 @@ class TestDAP_stackTraceCompilerGeneratedCode(lldbdap_testcase.DAPTestCaseBase): - @skipIfWindows def test_non_leaf_frame_compiler_generate_code(self): """ Test that non-leaf frames with compiler-generated code are properly resolved. @@ -42,7 +41,6 @@ def test_non_leaf_frame_compiler_generate_code(self): # Examine the foo() frame (stack_frames[1]) # This is the critical frame containing compiler-generated code foo_frame = stack_frames[1] - print(f"foo_frame: {foo_frame}") # Verify that the frame's line number points to the bar() call, # not to the compiler-generated code after it @@ -65,3 +63,4 @@ def test_non_leaf_frame_compiler_generate_code(self): # Verify that this didn't happen - the path should be a real file path. foo_path = foo_frame.get("source", {}).get("path") self.assertNotIn("`", foo_path, "Expected foo source path to not contain `") + self.continue_to_exit() diff --git a/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/main.c b/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/main.c index b431ac818cae7..dd3fcc295d492 100644 --- a/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/main.c +++ b/lldb/test/API/tools/lldb-dap/stackTraceCompilerGeneratedCode/main.c @@ -4,14 +4,16 @@ void bar() { void at_line_zero() {} +int foo(); + +int main(int argc, char const *argv[]) { + foo(); + return 0; +} + int foo() { bar(); // foo call bar #line 0 "test.cpp" at_line_zero(); return 0; } - -int main(int argc, char const *argv[]) { - foo(); - return 0; -} diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index 995c006f1bf55..11aed33886edb 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -657,15 +657,6 @@ std::optional DAP::ResolveSource(const lldb::SBFrame &frame) { if (!frame.IsValid()) return std::nullopt; - // IMPORTANT: Get line entry from symbol context, NOT from PC address. - // When a frame's PC points to a return address (the instruction - // after a call), that address may have line number 0 for compiler generated - // code. - // - // EXAMPLE: If PC is at 0x1004 (frame return address after the call - // instruction) with no line info, but 0x1003 (in the middle of previous call - // instruction) is at line 42, symbol context returns line 42. - const lldb::SBLineEntry frame_line_entry = frame.GetLineEntry(); if (DisplayAssemblySource(debugger, frame_line_entry)) { const lldb::SBAddress frame_pc = frame.GetPCAddress();