]> git.feebdaed.xyz Git - 0xmirror/binutils-gdb.git/commit
gdb: improve line number lookup around inline functions
authorAndrew Burgess <aburgess@redhat.com>
Fri, 26 Jul 2024 15:32:33 +0000 (16:32 +0100)
committerAndrew Burgess <aburgess@redhat.com>
Wed, 17 Dec 2025 14:31:39 +0000 (14:31 +0000)
commitb5160e2ee6a0192389caf7acee1fc32961ed29f4
tree0a7e7a56720383564966680f6f79b893f9bcef17
parentb6ac9fb3e265c36701f5cfae944807641a306b02
gdb: improve line number lookup around inline functions

This commit aims to fix an issue where GDB would report the wrong line
for frames other than #0 if a previous frame had just left an inline
function.

Consider this example which is compiled at -Og:

  volatile int global = 0;

  static inline int bar (void) { asm (""); return 1; }

  static void foo (int count)
  { global += count; }

  int main (void)
  {
    foo (bar ());
    return 0;
  }

Used in this GDB session:

  (gdb) break foo
  Breakpoint 1 at 0x401106: file test.c, line 6.
  (gdb) run
  Starting program: /tmp/inline-bt/test.x

  Breakpoint 1, foo (count=count@entry=1) at test.c:6
  6 { global += count; }
  (gdb) frame 1
  #1  0x0000000000401121 in main () at test.c:3
  3 static inline int bar (void) { asm (""); return 1; }

Notice that GDB incorrectly reports frame #1 as being at line 3 when
it should really be reporting this line:

  foo (bar ());

The cause of this problem is in find_pc_sect_line (symtab.c).  This
function is passed a PC for which GDB must find the symtab_and_line
information.  The function can be called in two modes based on the
NOTCURRENT argument.

When NOTCURRENT is false then we are looking for information about the
current PC, i.e. the PC at which the inferior is currently stopped
at.

When NOTCURRENT is true we are looking for information about a PC that
it not the current PC, but is instead the PC for a previous frame.
The interesting thing in this case is that the PC passed in will be
the address after the address we actually want to lookup information
for, this is because as we unwind the program counter from frame #0
what we get is the return address in frame #1.  The return address is
often (or sometimes) on the line after the calling line, and so in
find_pc_sect_line, when NOTCURRENT is true, we subtract 1 from PC and
then proceed as normal looking for information about this new PC
value.

Now lets look at the x86-64 disassembly for 'main' from the above
example.  The location marker (=>) represents the return address in
'main' after calling 'foo':

  (gdb) run
  Starting program: /tmp/inline-bt/test.x

  Breakpoint 1, foo (count=count@entry=1) at test.c:6
  6 { global += count; }
  #0  foo (count=count@entry=1) at test.c:6
  #1  0x000000000040111f in main () at test.c:3
  (gdb) up
  #1  0x000000000040111f in main () at test.c:3
  3 static inline int bar (void) { asm (""); return 1; }
  (gdb) disassemble
  Dump of assembler code for function main:
     0x0000000000401115 <+0>: mov    $0x1,%edi
     0x000000000040111a <+5>: call   0x401106 <foo>
  => 0x000000000040111f <+10>: mov    $0x0,%eax
     0x0000000000401124 <+15>: ret
  End of assembler dump.

And the corresponding line table:

  (gdb) maintenance info line-table
  objfile: /tmp/inline-bt/test.x ((struct objfile *) 0x59405a0)
  compunit_symtab: test.c ((struct compunit_symtab *) 0x53ad320)
  symtab: /tmp/inline-bt/test.c ((struct symtab *) 0x53ad3a0)
  linetable: ((struct linetable *) 0x53adc90):
  INDEX  LINE   REL-ADDRESS        UNREL-ADDRESS      IS-STMT PROLOGUE-END EPILOGUE-BEGIN
  0      6      0x0000000000401106 0x0000000000401106 Y
  1      6      0x0000000000401106 0x0000000000401106 Y
  2      6      0x0000000000401106 0x0000000000401106
  3      6      0x0000000000401114 0x0000000000401114
  4      9      0x0000000000401115 0x0000000000401115 Y
  5      10     0x0000000000401115 0x0000000000401115 Y
  6      3      0x0000000000401115 0x0000000000401115 Y
  7      3      0x0000000000401115 0x0000000000401115 Y
  8      3      0x0000000000401115 0x0000000000401115 Y
  9      10     0x0000000000401115 0x0000000000401115
  10     11     0x000000000040111f 0x000000000040111f Y
  11     12     0x000000000040111f 0x000000000040111f
  12     END    0x0000000000401125 0x0000000000401125 Y

When looking for the line information of frame #1 we start with the
return address 0x40111f, however, as this is not the current program
counter value we subtract one and look for line information for
0x40111e.

We will find the entry at index 9, this is the last entry with an
address less than the address we're looking for, the next entry has an
address greater than the one we're looking for.  The entry at index 9
is for line 10 which is the correct line, but GDB reports line 3, so
what's going on?

Having found a matching entry GDB checks to see if the entry is marked
as is-stmt (is statement).  In our case index 9 (line 10) is not a
statement, and so GDB looks backwards for entries at the same address,
if any of these are marked is-stmt then GDB will use the last of these
instead.  In our case the previous entry at index 8 is marked is-stmt,
and so GDB uses that.  The entry at index 8 is for line 3, and that is
why GDB reports the wrong line.  So why perform the backward is-stmt
check?

When NOTCURRENT is false (not our case) the backward scan makes
sense.  If the inferior has just stopped at some new location, and we
want to report that location to the user, then it is better (I think)
to select an is-stmt entry.  In this way we will report a line number
for a line which the inferior is just about to start executing, and
non of the side effects of that line have yet taken place.  The line
GDB prints will correspond with the reported line, and if the user
queries the inferior state, the inferior should (assuming the compiler
emitted correct is-stmt markers) correspond to the line in question
having not yet been started.

However, in our case NOTCURRENT is true.  We're looking back to
previous frames that are currently in-progress.  If upon return to the
previous frame we are about to execute the next line then (it seems to
me) that this indicates we must be performing the very last action
from the previous line.  As such, looking back through the line table
in order to report a line that has not yet started is the wrong thing
to do.  We really want to report the very last line table entry for
the previous address as this is (I think) most likely to represent the
previous line that is just about to complete.

Further, in the NOTCURRENT case, we should care less about reporting
an is-stmt line.  When a user looks back to a previous frame I don't
think they expect the line being reported to have not yet started.  In
fact I think the expectation is the reverse ... after all, the
previous line must have executed enough to call the current frame.

So my proposal is that the backward scan of the line table looking for
an is-stmt entry should not be performed when NOTCURRENT is true.  In
the case above this means we will report the entry at index 9, which
is for line 10, which is correct.

For testing this commit I have:

 1. Extended the existing gdb.opt/inline-bt.exp test.  I've extended
 the source code to include a test similar to the example above.  I
 have also extended the script so that the test is compiled at a
 variety of optimisation levels (O0, Og, O1, O2).

 2. Added a new DWARF assembler test which hard codes a line table
 similar to the example given above.  My hope is that even if test
 case (1) changes (due to compiler changes) this test will continue to
 test the specific case I'm interested in.

I have tested the gdb.opt/inline-bt.exp test with gcc versions 8.4.0,
9.3.1, 10.5.0, 11.5.0, 12.2.0, and 14.2.0, in each case the test will
fail (with the expected error) without this patch applied, and will
pass with this patch applied.

I was inspired to write this patch while reviewing these patches:

  https://inbox.sourceware.org/gdb-patches/AS8P193MB1285C58F6F09502252CEC16FE4DF2@AS8P193MB1285.EURP193.PROD.OUTLOOK.COM
  https://inbox.sourceware.org/gdb-patches/AS8P193MB12855708DFF59A5309F5B19EE4DF2@AS8P193MB1285.EURP193.PROD.OUTLOOK.COM

though this patch only covers one of the issues addressed by these
patches, and the approach taken is quite different.  Still, those
patches are worth reading for the history of this fix.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=25987

Approved-By: Tom Tromey <tom@tromey.com>
gdb/symtab.c
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.c [new file with mode: 0644]
gdb/testsuite/gdb.dwarf2/dw2-inline-bt.exp [new file with mode: 0644]
gdb/testsuite/gdb.opt/inline-bt.c
gdb/testsuite/gdb.opt/inline-bt.exp