gdb: fix selecting tail-call frames by name

Message ID 18b032642d5eec6cf22afb3d7f3481069440c17e.1736954422.git.aburgess@redhat.com
State New
Headers
Series gdb: fix selecting tail-call frames by name |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_gdb_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_gdb_check--master-arm success Test passed
linaro-tcwg-bot/tcwg_gdb_check--master-aarch64 success Test passed

Commit Message

Andrew Burgess Jan. 15, 2025, 3:20 p.m. UTC
  I noticed that attempting to select a tail-call frame using 'frame
function NAME' wouldn't work:

  (gdb) bt
  #0  func_that_never_returns () at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.base/frame-selection.c:49
  #1  0x0000000000401183 in func_that_tail_calls () at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.base/frame-selection.c:59
  #2  0x00000000004011a5 in main () at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.base/frame-selection.c:70
  (gdb) frame function func_that_tail_calls
  No frame for function "func_that_tail_calls".
  (gdb) up
  #1  0x0000000000401183 in func_that_tail_calls () at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.base/frame-selection.c:59
  59	  func_that_never_returns ();
  (gdb) disassemble
  Dump of assembler code for function func_that_tail_calls:
     0x000000000040117a <+0>:	push   %rbp
     0x000000000040117b <+1>:	mov    %rsp,%rbp
     0x000000000040117e <+4>:	call   0x40116c <func_that_never_returns>
  End of assembler dump.
  (gdb)

The problem is that the 'function' mechanism uses get_frame_pc() and
then compares the address returned with the bounds of the function
we're looking for.

So in this case, the bounds of func_that_tail_calls are 0x40117a to
0x401183, with 0x401183 being the first address _after_ the function.

However, because func_that_tail_calls ends in a tail call, then the
get_frame_pc() is 0x401183, the first address after the function.  As
a result, GDB fails to realise that frame #1 is inside the function
we're looking for, and the lookup fails.

The fix is to use get_frame_address_in_block, which will return an
adjusted address, in this case, 0x401182, which is within the function
bounds.  Now the lookup works:

  (gdb) frame function func_that_tail_calls
  #1  0x0000000000401183 in func_that_tail_calls () at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.base/frame-selection.c:59
  59	  func_that_never_returns ();
  (gdb)

I've extended the gdb.base/frame-selection.exp test to cover this
case.
---
 gdb/stack.c                                |  6 ++--
 gdb/testsuite/gdb.base/frame-selection.c   | 21 ++++++++++++
 gdb/testsuite/gdb.base/frame-selection.exp | 37 ++++++++++++++++++++++
 3 files changed, 62 insertions(+), 2 deletions(-)


base-commit: ac8f3fc9330da0302ebb491bf2bac8da5e035e35
  

Comments

Andrew Burgess Feb. 10, 2025, 10:09 a.m. UTC | #1
Andrew Burgess <aburgess@redhat.com> writes:

> I noticed that attempting to select a tail-call frame using 'frame
> function NAME' wouldn't work:
>
>   (gdb) bt
>   #0  func_that_never_returns () at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.base/frame-selection.c:49
>   #1  0x0000000000401183 in func_that_tail_calls () at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.base/frame-selection.c:59
>   #2  0x00000000004011a5 in main () at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.base/frame-selection.c:70
>   (gdb) frame function func_that_tail_calls
>   No frame for function "func_that_tail_calls".
>   (gdb) up
>   #1  0x0000000000401183 in func_that_tail_calls () at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.base/frame-selection.c:59
>   59	  func_that_never_returns ();
>   (gdb) disassemble
>   Dump of assembler code for function func_that_tail_calls:
>      0x000000000040117a <+0>:	push   %rbp
>      0x000000000040117b <+1>:	mov    %rsp,%rbp
>      0x000000000040117e <+4>:	call   0x40116c <func_that_never_returns>
>   End of assembler dump.
>   (gdb)
>
> The problem is that the 'function' mechanism uses get_frame_pc() and
> then compares the address returned with the bounds of the function
> we're looking for.
>
> So in this case, the bounds of func_that_tail_calls are 0x40117a to
> 0x401183, with 0x401183 being the first address _after_ the function.
>
> However, because func_that_tail_calls ends in a tail call, then the
> get_frame_pc() is 0x401183, the first address after the function.  As
> a result, GDB fails to realise that frame #1 is inside the function
> we're looking for, and the lookup fails.
>
> The fix is to use get_frame_address_in_block, which will return an
> adjusted address, in this case, 0x401182, which is within the function
> bounds.  Now the lookup works:
>
>   (gdb) frame function func_that_tail_calls
>   #1  0x0000000000401183 in func_that_tail_calls () at /tmp/build/gdb/testsuite/../../../src/gdb/testsuite/gdb.base/frame-selection.c:59
>   59	  func_that_never_returns ();
>   (gdb)
>
> I've extended the gdb.base/frame-selection.exp test to cover this
> case.

I've checked this in.

Thanks,
Andrew
  

Patch

diff --git a/gdb/stack.c b/gdb/stack.c
index 2d6712ab16b..20c1f6d6430 100644
--- a/gdb/stack.c
+++ b/gdb/stack.c
@@ -2863,9 +2863,11 @@  find_frame_for_function (const char *function_name)
 
   do
     {
+      CORE_ADDR frame_pc = get_frame_address_in_block (frame);
+
       for (size_t i = 0; (i < sals.size () && !found); i++)
-	found = (get_frame_pc (frame) >= func_bounds[i].low
-		 && get_frame_pc (frame) < func_bounds[i].high);
+	found = (frame_pc >= func_bounds[i].low
+		 && frame_pc < func_bounds[i].high);
       if (!found)
 	{
 	  level = 1;
diff --git a/gdb/testsuite/gdb.base/frame-selection.c b/gdb/testsuite/gdb.base/frame-selection.c
index 237e155b8c5..18a58e44e3f 100644
--- a/gdb/testsuite/gdb.base/frame-selection.c
+++ b/gdb/testsuite/gdb.base/frame-selection.c
@@ -15,6 +15,8 @@ 
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
 
+#include <stdlib.h>
+
 int
 frame_2 (void)
 {
@@ -40,6 +42,23 @@  recursive (int arg)
   return v;
 }
 
+/* A function that never returns.  */
+void __attribute__((noreturn))
+func_that_never_returns (void)
+{
+  exit (0);
+}
+
+/* A function that tail calls.  Calling a 'noreturn' function isn't
+   required for a tail call, but at low optimisation levels, gcc will apply
+   the tail call optimisation only for 'noreturn' calls.  */
+
+void
+func_that_tail_calls (void)
+{
+  func_that_never_returns ();
+}
+
 int
 main (void)
 {
@@ -48,5 +67,7 @@  main (void)
   i = frame_1 ();
   j = recursive (0);
 
+  func_that_tail_calls ();
+
   return i + j;
 }
diff --git a/gdb/testsuite/gdb.base/frame-selection.exp b/gdb/testsuite/gdb.base/frame-selection.exp
index e8d9c87c3a8..32ed92d2439 100644
--- a/gdb/testsuite/gdb.base/frame-selection.exp
+++ b/gdb/testsuite/gdb.base/frame-selection.exp
@@ -186,3 +186,40 @@  with_test_prefix "second frame_2 breakpoint" {
     gdb_test "frame function recursive" "#1  $hex in recursive.*" \
 	"select frame for function recursive, third attempt"
 }
+
+# At one point using the 'function' sub-command (e.g. 'frame function
+# ...') would fail to select a frame if the frame ended with a
+# tail-call, and the address within the frame was outside the bounds
+# of the function.
+with_test_prefix "stack with tail call" {
+    gdb_breakpoint func_that_never_returns
+    gdb_continue_to_breakpoint func_that_never_returns
+
+    gdb_test "bt" \
+	[multi_line \
+	     "#0  func_that_never_returns \\(\\) at \[^\r\n\]+" \
+	     "#1  $hex in func_that_tail_calls \\(\\) at \[^\r\n\]+" \
+	     "#2  $hex in main \\(\\) at \[^\r\n\]+"] \
+	"bt from func_that_never_returns"
+
+    # Reset the frame addresses based on the new stack.
+    gdb_test "frame 0" "#0  func_that_never_returns.*"
+    set frame_0_address [ get_frame_address "frame 0" ]
+    gdb_test "frame 1" "#1  $hex in func_that_tail_calls.*"
+    set frame_1_address [ get_frame_address "frame 1" ]
+    gdb_test "frame 2" "#2  $hex in main.*"
+    set frame_2_address [ get_frame_address "frame 2" ]
+
+    # Test 'select-frame function ...' command.
+    gdb_test_no_output "select-frame function func_that_never_returns"
+    check_frame "0" "${frame_0_address}" "func_that_never_returns"
+    gdb_test_no_output "select-frame function func_that_tail_calls"
+    check_frame "1" "${frame_1_address}" "func_that_tail_calls"
+    gdb_test_no_output "select-frame function main"
+    check_frame "2" "${frame_2_address}" "main"
+
+    # Test 'frame function ...' command.
+    gdb_test "frame function func_that_never_returns" "#0  func_that_never_returns.*"
+    gdb_test "frame function func_that_tail_calls" "#1  $hex in func_that_tail_calls.*"
+    gdb_test "frame function main" "#2  $hex in main.*"
+}