]> git.ipfire.org Git - thirdparty/gcc.git/commitdiff
ada: Straigthen Call_Chain and __gnat_backtrace processing
authorOlivier Hainque <hainque@adacore.com>
Thu, 4 Jun 2026 12:02:23 +0000 (12:02 +0000)
committerMarc Poulhiès <dkm@gcc.gnu.org>
Fri, 19 Jun 2026 13:05:28 +0000 (15:05 +0200)
Work on cross-testsuite/ta20_015__exc_traceback exposed
that calls to GNAT.Traceback.Call_Chain, in some configurations,
yield a first entry within Call_Chain rather than at the
call point.

This is supposed to be controlled by a careful computation
of a number of frames to skip, passed down as the SKIP_FRAMES
parameter to __gnat_backtrace in tracebak.c.

Roughly:
```
 subprogramA
  calls subprogramB
   calls GNAT.TB.Call_Chain (Skip_Frames => 1)
    calls System.TB.Chain (Skip_Frames => 2)
     calls __gnat_backtrace (skip_frames => 3)
           Actual backtrace at this point is
             1 __gnat_backtrace
             2 System.TB.Call_Chain
             3 GNAT.TB.Call_Chain
             4 spB
             5 spA
           Skip the first 3 frames -> return
             [spB, spA]
```

Within tracebak.c, the skip operation is influenced
by micro details of the semantics of a BASE_SKIP macro,
which was imprecisely documented.

I first thought the fault was in the value for this macro
in a couple of configurations, while it was entirely elswehere.

There were two issues aside from the BASE_SKIP documentation
(in)accuracy:

1 GNAT.Traceback exposes a Call_Chain procedure and a Call_Chain
  function.

  The function has a Skip_Frames parameter, with a default value,
  and a description of how it operates wrt that parameter.

  The procedure doesn't accept a parameter, the spec was imprecise
  and the behavior was different from that of the function with
  the default parameter value.

2 The way Skip_Frames values propagate down to __gnat_backtrace
  require the internal calls within the runtime to materialze as actual
  calls (and call frames) at run-time. This requires preventing
  inlining and sibling call optimization, which was achieved with
  dedicated compilation switches for s-traceb.adb and overlooked
  for g-traceb.adb.

This patch addresses these combined points together:

gcc/ada/ChangeLog:
* tracebak.c: Improve documentation of BASE_SKIP.
* libgnat/g-traceb.ads (Call_Chain procedure): Clarify the
expected behavior wrt Call_Chain itself.
* libgnat/g-traceb.adb (Call_Chain procedure): Propagate
a Skip_Frames value to System.TB.Call_Chain accordingly.
* libgnat/libgnat.gpr: Use the same special switches for
g-traceb.adb as for s-traceb.adb.
* Makefile.rtl: Likewise.
* libgnat/s-traceb.adb: Reword top comment on the need
to prevent optimizations. No need to specify how to do it
here.

gcc/ada/Makefile.rtl
gcc/ada/libgnat/g-traceb.adb
gcc/ada/libgnat/g-traceb.ads
gcc/ada/libgnat/libgnat.gpr
gcc/ada/libgnat/s-traceb.adb
gcc/ada/tracebak.c

index d4377098a00e5d4cc6284318b973b1f2886645c3..d99c28faec37ded29e48e8530c26d3e120c69d1e 100644 (file)
@@ -3405,15 +3405,20 @@ system.o: system.ads
          $(ADA_TARGET_PROPERTIES) \
          $(OUTPUT_OPTION)
 
-# Force no sibling call optimization on s-traceb.o so the number of stack
-# frames to be skipped when computing a call chain is not modified by
-# optimization. We don't want inlining, either.
+# Force no sibling call optimization on s-traceb.o and g-traceb.o so the
+# number of stack frames to be skipped when computing a call chain is not
+# modified by optimization. We don't want inlining, either.
 
 s-traceb.o: s-traceb.adb s-traceb.ads
        $(ADAC) -c $(ALL_ADAFLAGS) $(FORCE_DEBUG_ADAFLAGS) \
          $(NO_INLINE_ADAFLAGS) $(NO_SIBLING_ADAFLAGS) $(ADA_INCLUDES) $< \
          $(OUTPUT_OPTION)
 
+g-traceb.o: g-traceb.adb g-traceb.ads
+       $(ADAC) -c $(ALL_ADAFLAGS) $(FORCE_DEBUG_ADAFLAGS) \
+         $(NO_INLINE_ADAFLAGS) $(NO_SIBLING_ADAFLAGS) $(ADA_INCLUDES) $< \
+         $(OUTPUT_OPTION)
+
 # Compile s-tasdeb.o without optimization and with debug info so that it is
 # always possible to set conditional breakpoints on tasks.
 
index 12939923f717b225eee6ef915ea959db5d7430fa..7b7d843b8700a740caa83cf23ecbc35ff9a0e37b 100644 (file)
@@ -31,6 +31,9 @@
 
 --  Run-time non-symbolic traceback support
 
+--  As for System.Traceback, inlining and sibling call optimizations
+--  must be prevented within this unit.
+
 with System.Traceback;
 
 package body GNAT.Traceback is
@@ -44,7 +47,9 @@ package body GNAT.Traceback is
       Len       : out Natural)
    is
    begin
-      System.Traceback.Call_Chain (Traceback, Traceback'Length, Len);
+      --  Request skipping this frame + that of our callee
+      System.Traceback.Call_Chain
+        (Traceback, Traceback'Length, Len, Skip_Frames => 2);
    end Call_Chain;
 
    function Call_Chain
index e8d61fdfee9da7ce90a10a775d4fc531fd7d4543..f99c2945b436d66e997c917aef626673b8629999 100644 (file)
@@ -90,11 +90,10 @@ package GNAT.Traceback is
 
    procedure Call_Chain (Traceback : out Tracebacks_Array; Len : out Natural);
    --  Store up to Traceback'Length tracebacks corresponding to the current
-   --  call chain. The first entry stored corresponds to the deepest level
-   --  of subprogram calls. Len shows the number of traceback entries stored.
-   --  It will be equal to Traceback'Length unless the entire traceback is
-   --  shorter, in which case positions in Traceback past the Len position
-   --  are undefined on return.
+   --  call chain. The first entry is for the caller of this subprogram and
+   --  Len conveys the number of entries stored. Len will be Traceback'Length
+   --  unless the entire traceback is shorter. Positions in Traceback beyond
+   --  Len are undefined on return.
 
    function Call_Chain
      (Max_Len     : Positive;
index fd2f225381ba0b8a94c3dc84d4d68afff0043b21..c0c0274886aa99d3981f52907ba51277f4ee7da4 100644 (file)
@@ -26,12 +26,12 @@ library project Libgnat is
       for Switches ("C") use Libgnat_Common.C_Flags;
       for Switches ("Ada") use Libgnat_Common.Ada_Flags;
 
-      for Switches ("s-traceb.adb") use
+      for Switches ("?-traceb.adb") use
         Libgnat_Common.Ada_Flags & Libgnat_Common.Force_Debug &
         Libgnat_Common.No_Inline & Libgnat_Common.No_Sibling;
-      --  Force no sibling call optimization on s-traceb.o so the number of
-      --  stack frames to be skipped when computing a call chain is not
-      --  modified by optimization. We don.t want inlining, either.
+      --  Prevent inlining and sibling call optimization on s-traceb.o and
+      --  g-traceb.o so the number of stack frames to be skipped when computing
+      --  a call chain is not modified by optimization.
 
       for Switches ("a-except.adb") use
         Libgnat_Common.Ada_Flags & ("-O1") &
index 78692582be3bb6af316fd7c82ba12d5dd32c7fc0..cbea261c5d1113810dc632cbf168a4831f4300e5 100644 (file)
 
 --  This is the default version of this package
 
---  Note: this unit must be compiled using -fno-optimize-sibling-calls.
---  See comment below in body of Call_Chain for details on the reason.
+--  Note: To preserve the correctness of "Skip_Frames" processing,
+--  subprogram calls here must materialize as subprogram calls (and
+--  frames) at the machine level, so inlining and sibling call
+--  optimizations must be prevented for this unit.
 
 package body System.Traceback is
 
index 17c6be11f88fdbae0c80357e8fb6075d6354cae6..2c80b2a1610852a02043e7d88bec80a44403914f 100644 (file)
@@ -201,8 +201,8 @@ __gnat_backtrace (void **array,
    o FRAME_OFFSET, the offset, from a given frame address or frame pointer
      value, at which this layout will be found,
 
-   o FRAME_LEVEL, controls how many frames up we get at to start with,
-     from the initial frame pointer we compute by way of the GCC builtin,
+   o FRAME_LEVEL, control how many stack frames up we start from, in
+     builtin_frame_address terms.
 
      0 is most often the appropriate value. 1 may be necessary on targets
      where return addresses are saved by a function in it's caller's frame
@@ -237,19 +237,18 @@ __gnat_backtrace (void **array,
                         |                |
                         +----------------+
 
-   o BASE_SKIP,
+   o BASE_SKIP represents the initial shift incurred by the initial
+     FRAME_LEVEL + the fact that what we find on the stack are return
+     addresses. One way to see it is: starting from
 
-   Since we inherently deal with return addresses, there is an implicit shift
-   by at least one for the initial point we are able to observe in the chain.
+       ptr = (struct layout *)__builtin_frame_address(FRAME_LEVEL);
 
-   On some targets (e.g. sparc-solaris), the first return address we can
-   easily get without special code is even our caller's return address, so
-   there is a initial shift of two.
+     in __gnat_backtrace, where does ptr->return_address land?
 
-   BASE_SKIP represents this initial shift, which is the minimal "skip_frames"
-   value we support. We could add special code for the skip_frames < BASE_SKIP
-   cases. This is not done currently because there is virtually no situation
-   in which this would be useful.
+     If this is within __gnat_backtrace, BASE_SKIP should be 0. If this
+     is within the caller of __gnat_backtrace, BASE_SKIP should be 1. etc.
+
+     BASE_SKIP is the minimal "skip_frames" value we support.
 
    Finally, to account for some ABI specificities, a target may (but does
    not have to) define:
@@ -411,6 +410,8 @@ struct layout
 #define STOP_FRAME(CURRENT, TOP_STACK) \
  ((CURRENT)->next == 0 || ((long)(CURRENT)->next % __alignof__(void*)) != 0)
 
+/* builtin_frame_address(1) gets us the frame pointer of our caller,
+   where we store our own return address.  */
 #define BASE_SKIP 1
 
 /*-------------------------- SPARC Solaris or RTEMS --------------------*/
@@ -497,6 +498,8 @@ struct layout
    || (void *) ((CURRENT)->next) < (TOP_STACK)  \
    || EXTRA_STOP_CONDITION(CURRENT))
 
+/* builtin_frame_address(1) gets us the *frame* pointer of our caller, where
+   we'll find the return address from that caller to its own caller.  */
 #define BASE_SKIP (1+FRAME_LEVEL)
 
 /* On i386 architecture we check that at the call point we really have a call