getsentry/symbolic

PDB parsing produces overlapping inlinees

mstange opened this issue · 8 comments

The Function produced for a procedure in a pdb file can contain inlinees whose (start, size) ranges overlap among sibling inlinees.

I am unsure if this should be considered a bug or if it is the expected behavior.
The information is still correct, it is just making my life a little harder than necessary.

Here's an example from symbolic-testutils/fixtures/windows/crash.pdb:

public: __thiscall std::basic_string<wchar_t,struct std::char_traits<wchar_t>,class std::allocator<wchar_t> >::~basic_string<wchar_t,struct std::char_traits<wchar_t>,class std::allocator<wchar_t> >(void)
0x1120  push       esi
0x1121  mov        esi, ecx
0x1123  mov        ecx, dword [esi+0x14]
0x1126  cmp        ecx, 0x8
0x1129  jb         loc_401158
0x112b  mov        eax, dword [esi]
0x112d  lea        ecx, dword [0x2+ecx*2]
0x1134  cmp        ecx, 0x1000
0x113a  jb         loc_40114e
0x113c  mov        edx, dword [eax-4]
0x113f  add        ecx, 0x23
0x1142  sub        eax, edx
0x1144  add        eax, 0xfffffffc
0x1147  cmp        eax, 0x1f
0x114a  ja         loc_40116d
0x114c  mov        eax, edx
0x114e  push       ecx
0x114f  push       eax                                                 
0x1150  call       ??3@YAXPAXI@Z                                       
0x1155  add        esp, 0x8
0x1158  mov        dword [esi+0x10], 0x0
0x115f  xor        eax, eax
0x1161  mov        dword [esi+0x14], 0x7
0x1168  mov        word [esi], ax
0x116b  pop        esi
0x116c  ret
0x116d  call       dword [__imp___invalid_parameter_noinfo_noreturn]
0x1173  int3
> 0x1120: std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >::~basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> > (0x54)
  0x1120: xstring:2424 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)
  0x1123: xstring:2425 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)
  0x116c: xstring:2426 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)
  0x116d: xstring:2425 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)

  > 0x1123: std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >::_Tidy_deallocate (0x50)
    0x1123: xstring:3902 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)
    0x112b: xstring:3907 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)
    0x1158: xstring:3910 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)
    0x115f: xstring:3914 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)
    0x1161: xstring:3911 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)
    0x1168: xstring:3914 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)
    0x116d: xstring:3907 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)

    > 0x1123: std::_String_val<std::_Simple_types<wchar_t> >::_Large_string_engaged (0x6)
      0x1123: xstring:1802 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)

    > 0x115f: std::_WChar_traits<wchar_t>::assign (0xc)
      0x115f: iosfwd:341 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)
      0x1168: iosfwd:341 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)

    > 0x112b: std::allocator<wchar_t>::deallocate (0x48)
      0x112b: xmemory0:1030 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)
      0x116d: xmemory0:1030 (c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.13.26128\include)

[...]
 v 0x1120   v 0x1123   v 0x112b   v 0x1158   v 0x115f   v 0x1161   v 0x1168   v 0x116b   v 0x116c   v 0x116d   v 0x1174
 |------------------------------------------------------------------------------------------------------------| outer function (~basic_string)
 |---------||---------------------------------------------------------------------------||---------||---------| lines
  2424       2425                                                                         2426       2425

inline level 0:
            |-------------------------------------------------------------------------------------------------| inline (_Tidy_deallocate)
            |---------||---------||---------||---------||---------||---------|                      |---------| lines
             3902       3907       3910       3914       3911       3914                             3907

inline level 1:
           
            |----------| inline (_Large_string_engaged)
            |----------| lines
             1802
           
                                             |-------------------------------| inline (assign)
                                             |---------|           |---------| lines
                                              341                   341

                        |-------------------------------------------------------------------------------------| inline (deallocate)
                        |--------|                                                                  |---------| lines
                         1030                                                                        1030

inline level 2:

[...]

It is these two inlinees which are overlapping:

                                             |-------------------------------| inline (assign)
                                             |---------|           |---------| lines
                                              341                   341

                        |-------------------------------------------------------------------------------------| inline (deallocate)
                        |--------|                                                                  |---------| lines
                         1030                                                                        1030

But their line ranges are not overlapping, so it is still possible to get the right information, and all consumers within symbolic seem to be able to deal with this representation just fine.

As of #607, the DWARF parsing no longer produces overlapping sibling inlinees, and the inlinees are sorted by start address.

We could make the same change for PDBs. Or we could say "overlaps and non-sortedness are fine, it's really the lines within each inlinee which determine the true address ranges of that inlinee". If we choose the latter, we may also be able to simplify the DWARF parsing by a small amount.

Whichever option we choose, I think we should update the documentation for Function inlinees to set the right expectations.

I'm running into this in the context of mozilla/dump_syms#280: I need to convert a Function into the breakpad-with-inlines representation. In the breakpad-with-inlines representation, each function only emits "lines" for address ranges which are not covered by any of this functions inlinees. For address ranges which are covered by inlinees, the file+line information is stored in that inlinee's "call file" and "call line".

For example, let's look at the outer function in the example above:

 v 0x1120   v 0x1123   v 0x112b   v 0x1158   v 0x115f   v 0x1161   v 0x1168   v 0x116b   v 0x116c   v 0x116d   v 0x1174
 |------------------------------------------------------------------------------------------------------------| outer function (~basic_string)
 |---------||---------------------------------------------------------------------------||---------||---------| lines
  2424       2425                                                                         2426       2425

inline level 0:
            |-------------------------------------------------------------------------------------------------| inline (_Tidy_deallocate)
            |---------||---------||---------||---------||---------||---------|                      |---------| lines
             3902       3907       3910       3914       3911       3914                             3907

For the outer function, I need to emit two line records: 0x1120 at line 2424, and 0x116c at line 2426. Additionally, there is one inlinee at level 0, with call line 2425 and the two address ranges 0x1123..0x116b and 0x116d..0x1174.
(Edit: Actually, there's a third line record I need to output: 0x116b at line 2425. The inlinee doesn't cover the trailing bit of that 2425 line record. Ugh.)

How do I filter [(0x1120, 2424), (0x1123, 2425), (0x116c, 2426), (0x116d, 2425)] down to [(0x1120, 2424), (0x116c, 2426)]? I suppose I could put the lines of each function into a BTreeMap<address, line info>, and then recurse into the inlinees, and for each inlinee's line record, erase any line records at that address in all ancestor functions. And I'd need to determine each inlinee's call file / call line first, by looking up the inlinee's address in the caller function's line BTreeMap.

The part about erasing all line records in all ancestor functions for each address that is covered by a descendant line record is what concerns me.

Or I compute a RangeSet of addresses for each function based on the function's lines. Then I subtract the rangesets from all inlinees out, and whatever's left is the list of ranges for which I need to emit lines from this function. And then I iterate over my function's lines, and check if the line's address range has overlap with that difference. If it does, I compute the intersection, and emit one or more line ranges for each range in the intersection.

In contrast, let's say our representation was the following: No holes, no overlap, no out-of-order.

 v 0x1120   v 0x1123   v 0x112b   v 0x1158   v 0x115f   v 0x1161   v 0x1168   v 0x116b   v 0x116c   v 0x116d   v 0x1174
 |------------------------------------------------------------------------------------------------------------| outer function (~basic_string)
 |---------||---------------------------------------------------------------------------||---------||---------| lines
  2424       2425                                                                         2426       2425

inline level 0:
            |----------------------------------------------------------------| inline (_Tidy_deallocate)
            |---------||---------||---------||---------||---------||---------|
             3902       3907       3910       3914       3911       3914

                                                                                                    |---------| inline (_Tidy_deallocate)
                                                                                                    |---------| lines
                                                                                                     3907

inline level 1:
           
            |----------| inline (_Large_string_engaged)
            |----------| lines
             1802

                        |--------| inline (deallocate)
                        |--------| lines
                         1030

                                             |---------| inline (assign)
                                             |---------| lines
                                              341
                                                                   |---------| inline (assign)
                                                                   |---------| lines
                                                                    341

                                                                                                    |---------| inline (deallocate)
                                                                                                    |---------| lines
                                                                                                     1030

inline level 2:

[...]

With this representation, I can iterate over all lines and inlinees in order, at the same time (by using two manually-advanced iterators). If the next element is an inlinee, then the current line is the inlinee's call line. If the next element is a line, then I emit a line record. And sometimes I may need to emit another line record if an inlinee ends before the end of the current line's address range.
And I can trust that each inlinee covers exactly the address range which it says it covers. I don't need to look at the inlinee's lines to find out the actual address ranges.

Just to clarify, is the difference between the two diagrams that in the second one, the _Tidy_deallocate inlinee is split into two inlinees, which fixes the problem of inlinees not being contiguous?

That's right. With the split, each of the inlinees now represents a contiguous address range. This makes it easier to to exclude those ranges from the caller's line ranges.

After thinking about this some more, I'm less convinced now that this change is worth making. The transformation from "unsorted inlinees with holes" into "sorted list of inlinee address ranges" is fairly straightforward and is something I could do in dump_syms. I'll give that a go, maybe it'll look fine.

Doing the de-gappification and sorting inside dump_syms turned out to be fine. I'll close this.

I think we run into a variant of this issue ourselves (internal ref https://github.com/getsentry/team-stacktrace-private/issues/8)

The symcache writer was built with the assumption that a line record of an inlinee also exists as a line record in all of its parents, see this logic:

match self.ranges.entry(line.address as u32) {
btree_map::Entry::Vacant(entry) => {
if function.inline {
// BUG:
// the abstraction should have defined this line record inside the caller
// function already!
}
entry.insert(source_location);
}

Essentially the symcache writer works like this:

  • The parent fills in all its line infos into the "ranges" structure
  • The inlined child fills in its own line infos and assumes that the parent has already written a line info for this "range".
    • with this assumption, the child can then just say "I was inlined into this source location of the parent"

Now I think we hit a case where the parent has, for example:

  • 0x10 - 0x40: file X.cpp, line 10
    And the child has:
  • 0x10 - 0x20: file Y.cpp, line 3
  • 0x20 - 0x30: file Y.cpp, line 4

For the first line record, we can correctly link these up, and end up with:

  • 0x10: file Y.cpp, line 3; inlined into X.cpp, line 10

But the second record does not find an existing entry in "ranges" and thus does the wrong thing:

  • 0x20: file Y.cpp, line 4

Even worse, the inlinee line record only goes to 0x30, but we don’t work with ranges when writing the symcache, in particular, we don’t split the "parent" range, thus we lose the info that 0x30 - 0x40 should actually be X.cpp, line 10.

So I believe we need to process and sort all these things in the symbolic-debuginfo abstraction. @mstange do you have a suggestion of how to best do this?

That's actually a different issue than what I filed this issue about. The example I gave does not violate the assumption that you describe. So I think it would be better to track this in a new issue, so that we don't get confused.

That said, I've run into the same thing as you describe in pdb-addr2line. Here's the workaround I use there: https://github.com/mstange/pdb-addr2line/blob/0812a738d1315994497752f200974ec61bdd59ea/src/lib.rs#L1307-L1327

I think we were really able to work around this issue in the symcache conversion now, closing this again ;-)