- 
                Notifications
    You must be signed in to change notification settings 
- Fork 15k
Description
Found using latest LLDB git, GCC 11.5, & GDB 14.2. Behavior appears to differ depending on GCC (or GDB) version, but the basic issue can be seen with newer versions (re-tested with LLDB 20.1.1, GCC 14.2, and GDB 16.2).
Test case is 3 libraries.
testlib provides extern definitions of some variables (testobj1 through testobj4) in and out of a testcase namespace, this library is not built with debug symbols
testlib2 uses 2 of these externs (testcase::testobj1 and testobj3) in a run() function, called by main in testexe, this library is built with debug symbols
testlib3 uses 2 of these externs (testcase::testobj2 and testobj4) in a lib3() function, not called anywhere, this library is built with debug symbols.
This ends up with testlib2 and testlib3 with DWARF info like:
0x00000053:     DW_TAG_variable
                  DW_AT_name    ("testobj1")
                  DW_AT_decl_file       ("/testcase/lib.h")
                  DW_AT_decl_line       (7)
                  DW_AT_decl_column     (0x17)
                  DW_AT_linkage_name    ("_ZN8testcase8testobj1E")
                  DW_AT_type    (0x0000003a "testcase::TestStruct")
                  DW_AT_external        (true)
                  DW_AT_declaration     (true)
And:
0x0000006b:   DW_TAG_variable
                DW_AT_name      ("testobj3")
                DW_AT_decl_file ("/testcase/lib.h")
                DW_AT_decl_line (11)
                DW_AT_decl_column       (0x1d)
                DW_AT_type      (0x0000003a "testcase::TestStruct")
                DW_AT_external  (true)
                DW_AT_declaration       (true)
Where no DW_AT_location is defined, as these are externally defined symbols.
When built with GCC11, attaching with lldb and breaking on lib2.cpp before it returns, we get something like the following:
(lldb) b lib2.cpp:11
Breakpoint 1: where = libtestlib2.so`run() + 52 at lib2.cpp:11:15, address = 0x000000000000112d
(lldb) r
Process 2920018 launched: '/testcase/build/testexe' (x86_64)
Process 2920018 stopped
* thread #1, name = 'testexe', stop reason = breakpoint 1.1
    frame #0: 0x00007ffff7fbb12d libtestlib2.so`run() at lib2.cpp:11:15
   8   	   using namespace testcase;
   9   	   auto a3 = testobj1.a;
   10  	   auto a4 = testobj3.a;
-> 11  	   return a1 + a2 + a3 + a4;
   12  	}
(lldb) p testobj1
         ˄
         ╰─ error: use of undeclared identifier 'testobj1'
(lldb) p testobj2
         ˄
         ╰─ error: use of undeclared identifier 'testobj2'
(lldb) p testobj3
(void *) 0x0000000200000003
(lldb) p testobj4
(void *) 0x0000000300000004
When attaching with GDB (specifically with gcc11 and gdb 14), we get something more useful:
(gdb) b lib2.cpp:11
No symbol table is loaded.  Use the "file" command.
Breakpoint 3 (lib2.cpp:11) pending.
(gdb) r
Starting program: /testcase/build/testexe 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Breakpoint 3, run () at /testcase/lib2.cpp:11
11	   return a1 + a2 + a3 + a4;
Missing separate debuginfos, use: dnf debuginfo-install glibc-2.34-125.el9_5.3.alma.1.x86_64 libgcc-11.5.0-5.el9_5.alma.1.x86_64 libstdc++-11.5.0-5.el9_5.alma.1.x86_64
(gdb) p testobj1
$1 = {a = 1}
(gdb) p testobj2
$2 = {a = 2}
(gdb) p testobj3
$3 = {a = 3}
(gdb) p testobj4
$4 = {a = 4}
This behavior changes with gcc14 and gdb16, but is still providing printing of variables directly used in the current scope:
(gdb) b lib2.cpp:11
No symbol table is loaded.  Use the "file" command.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (lib2.cpp:11) pending.
(gdb) r
Starting program: /testcase/build/testexe
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, run () at /testcase/lib2.cpp:11
11          return a1 + a2 + a3 + a4;
(gdb) p testobj1
$1 = {a = 1}
(gdb) p testobj2
No symbol "testobj2" in current context.
(gdb) p testobj3
$2 = {a = 3}
(gdb) p testobj4
'testobj4' has unknown type; cast it to its declared type
I did not investigate further with the newer versions of GCC/GDB.
I dove into LLDB a bit and found that ManualDWARFIndex::IndexUnitImpl does not report these variables as has_location_or_const_value and is_global_or_static_variable do not get set to true. I tested with adding a new case for DW_AT_external and allowed the global to be inserted if it is external. I could not process the location here as no target symtab is available here.
Eventually I found my way to DIEEval.cpp LookupGlobalIdentifier and added some extra logic to the first return of value_sp which would attempt to use symbol_context.FindBestGlobalDataSymbol to find the global symbol address and re-create value_sp with that address. This kinda works, providing the same display as gdb for testobj1 and testobj3, but does not work for testobj2 and testobj4. I know this is not the correct place to "fix" this, but it was one place that had a target symbol_context available.
I did not locate a better place to post-process the DWARF DIEs while also having access to the target symbol context / symtab data to find the external variable addresses.
This might be intended behavior and not a bug, but being able to debug extern objects where we have the type data but the variable is defined in a unit built without debug symbols seems useful. I could potentially generate a JSON symbol file for these symbols and pass it to LLDB, but the data is all there already... would be convenient if it just worked.
As a note, using clang instead of gcc does not work with this case at all as it does not output any DW_TAG_variable entries for these external variables, which is unfortunate but possibly intended.
testcase files:
CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(testcase)
add_library(testlib SHARED lib.cpp)
add_library(testlib2 SHARED lib2.cpp)
target_link_libraries(testlib2 PRIVATE testlib)
target_compile_options(testlib2 PRIVATE -O0 -g)
add_library(testlib3 SHARED lib3.cpp)
target_link_libraries(testlib3 PRIVATE testlib)
target_compile_options(testlib3 PRIVATE -O0 -g)
add_executable(testexe main.cpp)
target_link_libraries(testexe PRIVATE testlib2 testlib3)
main.cpp:
#include "lib2.h"
int main() {
    return run();
}
lib.h:
#pragma once
namespace testcase {
    struct TestStruct {
        int a;
    };
    extern TestStruct testobj1; // Accessed from lib2
    extern TestStruct testobj2; // accessed from lib3
}
extern testcase::TestStruct testobj3; // accessed from lib2
extern testcase::TestStruct testobj4; // accessed from lib3
lib.cpp:
#include "lib.h"
namespace testcase {
    TestStruct testobj1 { .a = 1 };
    TestStruct testobj2 { .a = 2 };
}
testcase::TestStruct testobj3 { .a = 3 };
testcase::TestStruct testobj4 { .a = 4 };
lib2.h:
#pragma once
int run();
lib2.cpp:
#include "lib2.h"
#include "lib.h"
int run() {
    auto a1 = testcase::testobj1.a;
    auto a2 = testobj3.a;
    using namespace testcase;
    auto a3 = testobj1.a;
    auto a4 = testobj3.a;
    return a1 + a2 + a3 + a4;
}
lib3.cpp:
#include "lib.h"
int lib3()
{
    auto a1 = testcase::testobj2.a;
    auto a2 = testobj4.a;
    using namespace testcase;
    auto a3 = testobj2.a;
    auto a4 = testobj4.a;
    return a1 + a2 + a3 + a4;
}