[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