LoongArch is a relatively recent architecture (first release in 2021), so
linkers do not, and likely will not, support generating unwind info for
synthesized PLT stubs on LoongArch, because this feature is primarily
useful for x86 with its stack-adjusting lazy PLT bindings. So, this means
on LoongArch and other architectures without CFI coverage for the PLT,
the unwinder will have to fall back to CFI-less unwinding if unwinding
begins from within PLT or any other region outside CFI coverage,
normally relying on frame pointers.

In reality though, much code is compiled without frame pointers, in
which case the FP will often contain garbage (from an unwinder
perspective) that fails the `fp > sp` invariant and/or stack alignment,
aborting CFI-less unwinding. This means any sample with PC inside PLT or
any other region not covered by CFI for such FP-less code would likely
be lost, due to unwinding starting from outside CFI-covered regions and
having no valid frame information. This loss can be significant if a
particular workload is PLT-heavy.

Considering that PLT calls and tail calls always have valid RA, in case
of seemingly invalid FP and SP we can simply point PC to RA and retry.
The new PC being inside the text section would likely have proper
compiler-generated CFI, so unwinding can continue.  If not, we arrive
again at the EBL unwind callback with the termination condition now
satisfied so the sample is correctly dropped.

With this patch applied, on my Loongson 3B6000 hardware, the
run-stackprof-user test case went from ~5.4% sample loss to zero. The
payload being observed is a tight interpreted shell loop (in my case,
Bash) so frequent PLT calls are expected. They are now properly
recovered by the EBL unwinder.

ChangeLog:

        * NEWS: Announce the change.
        * backends/loongarch_unwind.c (loongarch_unwind): Pass through
          SP, FP and RA for frames with garbage FP, with only PC set to
          the current frame's RA for the frame in construction, and
          retry unwinding from there. Termination condition is moved
          from the end of the function to the RA validity check: abort
          in case an infinite loop would ensue. Check SP alignment as a
          smoke test for frame validity. Document the logic in detail.

Signed-off-by: WANG Xuerui <[email protected]>
---
 NEWS                        |  3 +++
 backends/loongarch_unwind.c | 49 +++++++++++++++++++++++++++++++++----
 2 files changed, 47 insertions(+), 5 deletions(-)

diff --git a/NEWS b/NEWS
index 129f927fb01f..0bf27c5db5c8 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,8 @@
 Version 0.198
 
+backends: Can now unwind through PLT and tail calls on LoongArch for code
+          compiled without frame pointers.
+
 stackprof: Now supports collecting traces on LoongArch.
 
 Version 0.197 "Anti-Corruption"
diff --git a/backends/loongarch_unwind.c b/backends/loongarch_unwind.c
index fb7480835d71..9c8db55f402f 100644
--- a/backends/loongarch_unwind.c
+++ b/backends/loongarch_unwind.c
@@ -44,27 +44,68 @@
 /* There was no CFI. Maybe we happen to have a frame pointer and can unwind 
from that?  */
 
 bool
-EBLHOOK(unwind) (Ebl *ebl __attribute__ ((unused)), Dwarf_Addr pc 
__attribute__ ((unused)),
+EBLHOOK(unwind) (Ebl *ebl __attribute__ ((unused)), Dwarf_Addr pc,
                  ebl_tid_registers_t *setfunc, ebl_tid_registers_get_t 
*getfunc,
                  ebl_pid_memory_read_t *readfunc, void *arg,
                  bool *signal_framep __attribute__ ((unused)))
 {
   Dwarf_Word fp, ra, sp;
 
+  // RA must be available in the current frame.
   if (!getfunc(RA_REG, 1, &ra, arg))
     return false;
 
-  if (ra == 0 || !setfunc(-1, 1, &ra, arg))
+  // RA must be meaningful, and not equal to PC or we would loop infinitely.
+  // `RA == PC + 1` is the same stuck case when the frame is a non-initial
+  // caller frame.
+  //
+  // In case of retries from the forwarding heuristics below, if we arrive
+  // here the previous frame's RA (which is the current PC) does not have
+  // CFI either, so we stop.
+  if (ra == 0 || ra == pc || ra == pc + 1 || !setfunc(-1, 1, &ra, arg))
     return false;
 
+  // FP and SP must be available too.
   if (!getfunc(FP_REG, 1, &fp, arg))
     fp = 0;
 
   if (!getfunc(SP_REG, 1, &sp, arg))
     sp = 0;
 
+  // The LoongArch stack must be 16-byte aligned at procedure entry, and
+  // essentially all compiled code respects the invariant at all times.
+  // So if SP is unaligned, the frame is not ABI-conformant, and not usable.
+  //
+  // Maybe we lose some samples landing right in hand-written asm snippets
+  // that have SP temporarily unaligned, but such code should be rare in
+  // practice.
+  if ((sp & 0xf) != 0)
+    return false;
+
   Dwarf_Word newRa, newFp, newSp;
 
+  // For code compiled without frame pointer, FP can contain arbitrary data,
+  // so do not try to read anything based on this FP if this is the case,
+  // and just forward everything unwind-related except PC and retry.
+  //
+  // This can happen if the current frame is a leaf function call or inside a
+  // PLT stub.  LoongArch PLT stub sequences do not touch SP nor FP, so this
+  // forwarding in effect gives us the parent frame.
+  //
+  // Here we use the stack growth direction and FP alignment as a heuristic,
+  // because at this point we already have SP satisfying alignment, and valid
+  // frames always have `fp > sp`.
+  if (fp <= sp || (fp & 0xf) != 0)
+    {
+      // Retry this frame with PC set to RA (done earlier).  We either land
+      // back in CFI-covered region or fail.
+      setfunc(RA_REG, 1, &ra, arg);
+      setfunc(FP_REG, 1, &fp, arg);
+      setfunc(SP_REG, 1, &sp, arg);
+      return true;
+    }
+
+  // The stack frame seems plausible, try reading from the expected slots.
   if (!readfunc(fp - RA_OFFSET, &newRa, arg))
     newRa = 0;
 
@@ -78,7 +119,5 @@ EBLHOOK(unwind) (Ebl *ebl __attribute__ ((unused)), 
Dwarf_Addr pc __attribute__
   setfunc(FP_REG, 1, &newFp, arg);
   setfunc(SP_REG, 1, &newSp, arg);
 
-  // If the fp is invalid, we might still have a valid ra.
-  // But if the fp is valid, then the stack should be moving in the right 
direction.
-  return fp == 0 || newSp > sp;
+  return true;
 }
-- 
2.56.0

Reply via email to