[PATCH] gdb/mips: unwind past post-prologue SP adjustments in syscall stubs

Orgad Shaneh <[email protected]>
Newsgroups gmane.comp.gdb.patches
Message-ID <[email protected]>
On MIPS o32, syscalls taking five or more arguments pass the extra
arguments on the stack, so libc syscall stubs temporarily lower SP
around the actual syscall instruction, after the function prologue.
uClibc's __syscall_ipc is representative (musl and uClibc's syscall.S
have the same shape), and none of them carry CFI:

	addiu	sp,sp,-8	# prologue
	sw	s0,4(sp)
	lw	v0,24(sp)
	lw	s0,28(sp)
	addiu	sp,sp,-32	# stack arguments for the syscall
	...
	syscall			# <- a blocked thread's PC is here
	addiu	sp,sp,32

mips32_scan_prologue permits at most one non-prologue instruction, so
for a thread blocked in such a syscall the scan stops at the two lw's
and never sees the second SP adjustment.  The computed frame base is
32 bytes too low, the caller's saved-ra slot is read from within the
outgoing argument area, and the backtrace degenerates into a garbage
frame right after the libc stub.  On core dumps of multi-threaded
programs from o32 uClibc/musl systems this loses the call chain of
every thread blocked in msgrcv, ppoll, futex via syscall(), etc.

When computing a frame (not when skipping the prologue), continue
scanning from where the prologue-end heuristic stopped up to the PC,
accumulating any further SP adjustments.  Branches are deliberately
not followed: compilers do not move SP mid-function outside prologue
and epilogue (alloca frames use a frame pointer and are excluded), so
post-prologue adjustments occur in practice only around syscall
instructions in libc stubs, where paired temporary adjustments that
were fully crossed cancel out in a linear scan.  The scan does stop at
a "jr $ra", though: past one, the range crosses a complete alternate
return path - e.g. the single-thread fast path of glibc's cancellable
syscall wrappers, which returns before the multithreaded path saves RA
and runs the syscall - and a linear sum of the remainder is no longer
meaningful, so the frame is left unchanged.  If the net result is
positive, rescan with the entry SP moved accordingly - the same
restart mechanism already used for alloca - so that both the frame
base and the register save slots are computed against the true frame.

The post-prologue scan gets a wider address bound than the prologue
scan's 200 bytes: the syscall window can be a few hundred bytes into
the function (ppoll in uClibc has it at +216).

Tested on a synthetic testcase (added) under qemu-mips, and on a real
core dump from a MIPS32r2 uClibc 0.9.33.2 system where garbage frames
across all 58 threads went from 32 to 0.

Signed-off-by: Orgad Shaneh <[email protected]>
---
 gdb/mips-tdep.c                               | 75 ++++++++++++++++++-
 .../gdb.arch/mips-syscall-unwind-stub.S       | 37 +++++++++
 gdb/testsuite/gdb.arch/mips-syscall-unwind.c  | 36 +++++++++
 .../gdb.arch/mips-syscall-unwind.exp          | 43 +++++++++++
 4 files changed, 190 insertions(+), 1 deletion(-)
 create mode 100644 gdb/testsuite/gdb.arch/mips-syscall-unwind-stub.S
 create mode 100644 gdb/testsuite/gdb.arch/mips-syscall-unwind.c
 create mode 100644 gdb/testsuite/gdb.arch/mips-syscall-unwind.exp

diff --git a/gdb/mips-tdep.c b/gdb/mips-tdep.c
index fe0482fe5bf..30c52abe321 100644
--- a/gdb/mips-tdep.c
+++ b/gdb/mips-tdep.c
@@ -3446,6 +3446,7 @@ mips32_scan_prologue (struct gdbarch *gdbarch,
   CORE_ADDR end_prologue_addr;
   int seen_sp_adjust = 0;
   int load_immediate_bytes = 0;
+  long sp_adjust_extra = 0;
   int in_delay_slot;
   int regsize_is_64_bits = (mips_abi_regsize (gdbarch) == 8);
 
@@ -3458,6 +3459,15 @@ mips32_scan_prologue (struct gdbarch *gdbarch,
   else
     sp = 0;
 
+  /* The address up to which the post-prologue scan below may look for
+     further SP adjustments.  Bounded, but much less tightly than the
+     prologue scan: libc syscall stubs adjust SP shortly before the
+     syscall instruction, which can be a few hundred bytes into the
+     function (e.g. ppoll in uClibc).  */
+  CORE_ADDR post_prologue_limit_pc = limit_pc;
+  if (post_prologue_limit_pc > start_pc + 4096)
+    post_prologue_limit_pc = start_pc + 4096;
+
   if (limit_pc > start_pc + 200)
     limit_pc = start_pc + 200;
 
@@ -3636,12 +3646,75 @@ mips32_scan_prologue (struct gdbarch *gdbarch,
       prev_pc = cur_pc;
     }
 
+  /* If the prologue-end heuristic above stopped the scan before the PC
+     while computing a frame, the function may adjust SP again further on,
+     with no CFI to describe it.  This is how o32 libc syscall stubs build
+     the stack argument area around the actual syscall instruction:
+
+	addiu	sp,sp,-8	# prologue
+	...
+	addiu	sp,sp,-32	# stack arguments for the syscall
+	syscall			# <- a blocked thread's PC is here
+	addiu	sp,sp,32
+
+     Continue scanning up to the PC, accumulating any further SP
+     adjustments.  Control flow is deliberately ignored: compilers do not
+     move SP mid-function outside prologue and epilogue (alloca frames use
+     a frame pointer and are excluded below), so post-prologue adjustments
+     occur in practice only around syscall instructions in libc stubs,
+     where paired temporary adjustments that were fully crossed cancel out
+     in a linear scan.  A net non-positive result leaves the frame
+     unchanged.  If SP was lowered further, rescan with the entry SP moved
+     accordingly, so that the frame base and the register save slots
+     recorded above are computed against the true frame.  */
+  if (this_cache != NULL && frame_reg == MIPS_SP_REGNUM
+      && sp_adjust_extra == 0)
+    {
+      long extra = 0;
+      CORE_ADDR extra_pc;
+
+      for (extra_pc = cur_pc; extra_pc < post_prologue_limit_pc;
+	   extra_pc += MIPS_INSN32_SIZE)
+	{
+	  unsigned long inst, high_word;
+	  long offset;
+
+	  inst = (unsigned long) mips_fetch_instruction (gdbarch, ISA_MIPS,
+							 extra_pc, NULL);
+
+	  /* A "jr $ra" before the PC means the scanned range crosses a
+	     complete alternate return path, e.g. the single-thread fast
+	     path of glibc's cancellable syscall wrappers.  A linear sum
+	     is no longer meaningful there, so stop and leave the frame
+	     unchanged.  */
+	  if (inst == 0x03e00008)	/* jr $ra */
+	    break;
+
+	  high_word = (inst >> 16) & 0xffff;
+	  offset = ((inst & 0xffff) ^ 0x8000) - 0x8000;
+
+	  if (high_word == 0x27bd	/* addiu $sp,$sp,i */
+	      || high_word == 0x23bd	/* addi $sp,$sp,i */
+	      || high_word == 0x67bd)	/* daddiu $sp,$sp,i */
+	    extra -= offset;
+	}
+
+      if (extra > 0)
+	{
+	  sp_adjust_extra = extra;
+	  sp += extra;
+	  reset_saved_regs (gdbarch, this_cache);
+	  goto restart;
+	}
+    }
+
   if (this_cache != NULL)
     {
       this_cache->base =
 	(get_frame_register_signed (this_frame,
 				    gdbarch_num_regs (gdbarch) + frame_reg)
-	 + frame_offset);
+	 + frame_offset
+	 + (frame_reg == MIPS_SP_REGNUM ? sp_adjust_extra : 0));
       /* FIXME: brobecker/2004-09-15: We should be able to get rid of
 	 this assignment below, eventually.  But it's still needed
 	 for now.  */
diff --git a/gdb/testsuite/gdb.arch/mips-syscall-unwind-stub.S b/gdb/testsuite/gdb.arch/mips-syscall-unwind-stub.S
new file mode 100644
index 00000000000..e8cd3f3c464
--- /dev/null
+++ b/gdb/testsuite/gdb.arch/mips-syscall-unwind-stub.S
@@ -0,0 +1,37 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2026 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+/* The shape of an o32 libc syscall stub with stack arguments: SP is
+   temporarily lowered again after the prologue, around the syscall
+   instruction, with no CFI describing it.  A thread blocked in the
+   syscall has its PC inside that window.  */
+
+	.text
+	.globl	stub
+	.type	stub, @function
+	.set	noreorder
+stub:
+	addiu	$sp, $sp, -16
+	lw	$v0, 16($sp)	/* Two non-prologue instructions end the */
+	lw	$v0, 16($sp)	/* prologue scan.  */
+	addiu	$sp, $sp, -32	/* Stack argument area for the syscall.  */
+	nop			/* blocked */
+	addiu	$sp, $sp, 32
+	li	$v0, 1
+	jr	$ra
+	addiu	$sp, $sp, 16
+	.size	stub, .-stub
diff --git a/gdb/testsuite/gdb.arch/mips-syscall-unwind.c b/gdb/testsuite/gdb.arch/mips-syscall-unwind.c
new file mode 100644
index 00000000000..1f71c19b444
--- /dev/null
+++ b/gdb/testsuite/gdb.arch/mips-syscall-unwind.c
@@ -0,0 +1,36 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2026 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+extern long stub (void);
+
+long __attribute__ ((noinline))
+f2 (void)
+{
+  return stub () + 1;
+}
+
+long __attribute__ ((noinline))
+f1 (void)
+{
+  return f2 () + 1;
+}
+
+int
+main (void)
+{
+  return f1 () != 2;
+}
diff --git a/gdb/testsuite/gdb.arch/mips-syscall-unwind.exp b/gdb/testsuite/gdb.arch/mips-syscall-unwind.exp
new file mode 100644
index 00000000000..ced21933aae
--- /dev/null
+++ b/gdb/testsuite/gdb.arch/mips-syscall-unwind.exp
@@ -0,0 +1,43 @@
+# Copyright 2026 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test unwinding past a CFI-less function that adjusts SP again after
+# its prologue, the way o32 libc syscall stubs build the stack argument
+# area around a syscall instruction.  The prologue scanner used to miss
+# the second adjustment, computing a frame base 32 bytes too low and
+# breaking the backtrace right past such a function.
+
+require {istarget "mips*-*-*"} is_ilp32_target
+
+standard_testfile .c mips-syscall-unwind-stub.S
+
+if { [prepare_for_testing "failed to prepare" ${testfile} \
+	  [list $srcfile $srcfile2]] } {
+    return
+}
+
+if { ![runto_main] } {
+    return
+}
+
+gdb_breakpoint [gdb_get_line_number "blocked" $srcfile2]
+gdb_continue_to_breakpoint "syscall window" ".*blocked.*"
+
+gdb_test "backtrace" \
+    [multi_line "#0\[ \t\]+stub \\(\\).*" \
+		"#1\[ \t\]+$hex in f2 \\(\\).*" \
+		"#2\[ \t\]+$hex in f1 \\(\\).*" \
+		"#3\[ \t\]+$hex in main \\(\\).*"] \
+    "backtrace through post-prologue SP adjustment"
-- 
2.53.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.