Sidharth Ghoshal
Sidharth Ghoshal

Reputation: 720

Why is LLDB in C++ able to print my entire data structure but not able to print subcomponents?

I'm using Mac OS Sonoma 14.5 with LLDB 1500.0.404.7 installed via x-code tools. I noticed that my LLDB behaves in a rather confusing way. It is able to print out some std:containers in their entirety yet it is not able to print out sub components of those same containers. In particular I am able to execute the following p primary_map command

(lldb) p primary_map
(std::unordered_map<int, std::unordered_set<int> >) size=2 {
  [0] = {
    first = 1
    second = size=2 {
      [0] = 5
      [1] = 4
    }
  }
  [1] = {
    first = 0
    second = size=2 {
      [0] = 2
      [1] = 1
    }
  }
}

And clearly LLDB can show me the entire map. Yet for some reason if I execute p primary_map[0]

(lldb) p primary_map[0]
error: Couldn't lookup symbols:
  __ZNSt3__113unordered_mapIiNS_13unordered_setIiNS_4hashIiEENS_8equal_toIiEENS_9allocatorIiEEEES3_S5_NS6_INS_4pairIKiS8_EEEEEixEOi

LLDB is not able to render a subcomponent of the datastructure.

Question:

I want to know, why is this happening, how to prevent it in the future, and if it can't be prevented what is the best workaround.

Context:

I have a sample piece of code. It basically creates map of integers to sets of integers by reading the contents of a file. In particular the #of rows in the file and the number of integers per row are NOT known at compile time and only discovered at run-time by reading the file itself (somehow I think this is important).

working_driver.cpp

#include <iostream>
#include <fstream>
#include <string>
#include <unordered_set>
#include <unordered_map>

using namespace std;

unordered_map<int, unordered_set<int> > primary_map;

int main() {
    ifstream input("sample_data_working.txt");
    int row_count; int col_count; int holder;
    input >> row_count; input >> col_count; 

    for (int i =0; i < row_count; i++) {
        primary_map[i] = unordered_set<int>{}; 
        for (int j = 0; j < col_count; j++) {
            input >> holder;
            primary_map[i].insert(holder);
        }
    }
    return 0;
}

The file is below, its header is a pair of numbers (# of rows, # of columns) and then lines containing numbers.

sample_data_working.txt

2 2
1 2
4 5

Reproducing the Problem:

I compile my code using the following command: g++ -std=c++14 -g -O0 -fstandalone-debug working_driver.cpp -o working_driver which actually makes a call to clang (because Mac OS) the version is given as: Apple clang version 15.0.0 (clang-1500.3.9.4).

I then execute lldb working_driver to get an LLDB instance started. From here I set a breakpoint on my return statement via b 23 and then execute the r command which goes forward and gets me to my breakpoint. In the terminal this should look something like:

➜  differentiation git:(main) ✗ lldb working_driver
(lldb) target create "working_driver"
Current executable set to '/Users/sidharthghoshal/cp_training/USACO/python3/chapter4/2/stall4/differentiation/working_driver' (x86_64).
(lldb) b 23
Breakpoint 1: where = working_driver`main + 465 at working_driver.cpp:23:5, address = 0x0000000100000b91
(lldb) r
Process 3140 launched: '/Users/sidharthghoshal/cp_training/USACO/python3/chapter4/2/stall4/differentiation/working_driver' (x86_64)
Process 3140 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100000b91 working_driver`main at working_driver.cpp:23:5
   20               primary_map[i].insert(holder);
   21           }
   22       }
-> 23       return 0;
   24   }
Target 0: (working_driver) stopped.

It is at this point that chaos ensues. p primary_map works exactly as expected at the top of this question yet p primary_map[0] does not.

My ideas on the nature of the problem:

  1. I have seen the p primary_map[0] command work on the same structure unordered_map<int, unordered_set<int> > in the past but formed slightly differently (namely hardcoded upper bounds row_count, col_count in the loop). So stackoverflow posts that say primary_map[0] is not well defined are not exactly satisfactory because that command DOES work sometimes, just not on this instance. Somehow the "runtime-discovered loop bounds" appear to be a confounding issue here.

  2. The commonly discussed fix is to write either a custom allocator or custom parser, what I don't understand is why do I have to do this? Clearly the built-in parser that LLDB carries for unordered_map<int, unordered_set<int> > is sufficient in some cases. Just not this case. I would like to know a why. I then would deeply appreciate the how.

Some further research:

The highest rated answer here clarifies a lot. In particular frame var primary_map[0] does behave as expected. It seems that somehow LLDB is clever enough to know to print the frame var for some of my code but other times it is not smart enough to frame var and then just emits the odd symbol error. It's not clear to me what run-time defined loop bounds have do with switching from frame var to expr but the connection is becoming a little clearer now.

Upvotes: 4

Views: 151

Answers (2)

Jim Ingham
Jim Ingham

Reputation: 27110

One thing to note about how lldb prints values might help clear this up:

Every typed value that lldb prints first gets wrapped in an SBValue class, and then the SBValue is asked to print the value. That's true for results of the expression evaluator, for frame variable results and pretty much everywhere values are printed in lldb. Also, when VSCode or Xcode shows a "Locals" view, they are accessing the structured representation of the value provided by the SBValue class.

The SBValues have a fairly sophisticated type based system (data formatters) that you can teach - for a given complex data type - to read the memory of the value and represent it in some useful structured way.

For instance, there's a data formatter that knows how to present std::unordered_map values as an array of elements. If you had run the command:

(lldb) frame var some_unordered_map

lldb would look up the local variable some_unordered_map, find its type and where it is in memory, then make an SBValue with that location and type. Finally, the SBValue system would be asked to render that SBValue which ends up presenting it as a convenient array using the data formatter for that type (unless, as Enlico said, you pass --raw to turn this feature off.)

If you instead ran:

(lldb) expr some_unordered_map

lldb will compile, JIT, and run an expression that captures the value of the identifier some_unordered_set. The result of that expression will then be made into an SBValue pointing to the memory of that unordered_map.

At that point, the data formatter code in the SBValue system will again kick in, and present to you the full map. The important point being that the separate elements were not produced by the expression evaluator, but rather rendered after the fact by the SBValue system and its knowledge of the layout of this type.

But when you run:

(lldb) expr some_unordered_map[0]

You are now asking the JIT-ed expression to access the 0th element. To do that in code requires actually calling the [] operator, at which point that operator had better be present, or lldb won't be able to run that expression successfully.

However, in the STL, many of these operators come from header files, and are expected to be accessible because the compiler emitted the code for the method directly into the .o file whose .cpp file #included the header. So if the method was never used, it would exist only in the header files.

Note, even if the compiler did have to use that accessor in your code, STL tends to inline these operators if it can. Inlined instances of a method aren't accessible to the debugger, so again this will cause expressions to fail with this sort of symbol lookup error.

There's an experimental feature for C++ that will instruct lldb's expression evaluator to include the STL headers (built as a clang module) into each expression's context. That will allow lldb to create missing STL methods on the fly. That is governed by the target.import-std-module setting in lldb.

Upvotes: 3

Enlico
Enlico

Reputation: 28366

tl;dr

primary_map[0] would require running library code that might have not even been compiled into the binary. That's the same even for the "simple" std::vector v{1,2,3};, where the debugger can't run v[0]. But using only language expressions, you can get there, e.g. in the case of std::vector you could do v._M_impl._M_start[0] to get to the first element.

Full answer

I want to know, why is this happening, how to prevent it in the future, and if it can't be prevented what is the best workaround.

I think the reason is that the debugger can interpret from the console only language expressions and not library expressions. That means, it understands foo[bar] if foo is C-style array, on which the builtin operator[] works (which also implies that bar is an int-like thing), but it doesn't understand it if foo is user defined type.

And the reason is that if foo is C-style array and bar is an int-like thing (I mean, something suitable for an index for a builtin array), then getting to that item in memory does not require to compile any code, but just using the runtime of C/C++, which is known to the debugger regardless of even the existence of your program.

On the other hand, interpreting foo[bar] where foo is a class, like std::vector, would require the debugger to call into some code that is specifically part of your program, because your program #include<vector>s and uses std::vector<T>::operator[]. To call that, the debugger would need to interpret your compiled, hence binary code to go look for the part of the binary that implements operator[] and call that. I'm not even sure that would be a safe thing to do, considering that in general you might be attempting such a thing not for std::vector but for another class that could have a bugged operator[] or who knows what.

Furthemore, your program in general might not even be using the operator[], so that function might not even be anywhere at all in the binary, so how can you run it in the console?

Here's a simpler exmaple. If you compile this code with debug symbols,

#include <vector>
int main() {
    std::vector<int> v{17,2,3};
    return 0;
}

and you put a breakpoint on the return, how can you expect the call to v[0] to succeed, if operator[] is not even in the binary because you haven't used it?

However, if you know how to get to the data you want by just using language expressions, than you can do that. To do so, you can look into the implementation of std::vector, and work out that the actual values are stored in a C-array that is located at v._M_impl._M_start, so if you type v._M_impl._M_start[0]/v._M_impl._M_start[1]/v._M_impl._M_start[2] in the console, that will indeed print 17/2/3 as expected.

Where did I see that the values of v are stored in v._M_impl._M_start? Well, the easiest way is to put a watcher on v, and the debugger will show it like this:

Expression: v
 *- Result: size=3
   *- [0]: 1
   *- [1]: 2
   *- [2]: 3
   *+ [raw]: std::vector<int, std::allocator<int> >

That raw is what you want to look into, so let's expand it:

Expression: v
 *- Result: size=3
   *- [0]: 1
   *- [1]: 2
   *- [2]: 3
   *- [raw]: std::vector<int, std::allocator<int> >
      - std::_Vector_base<int, std::allocator<int> >: {...}
        - _M_impl: {...}
          - std::_Vector_base<int, std::allocator<int> >::_Vector_impl_data: {_M_start:0x000055555556d2b0, _M_finish:0x000055555556d2bc, ...}
            - _M_start: 0x000055555556d2b0
             *- *_M_start: 17
            + _M_finish: 0x000055555556d2bc
            + _M_end_of_storage: 0x000055555556d2bc

Here, searching for 17, you realize that it is equal to *_M_start, so _M_start is the C-array that contains the data. And that is under _M_impl, which is under v.

Upvotes: 1

Related Questions