[2/3] libbacktrace: Add OFFLINE flag

Message ID 20260717203854.627989-2-andi@firstfloor.org
State New
Headers
Series [1/3] Add backtrace_decl_line to libbacktrace |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gcc_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_gcc_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_simplebootstrap_build--master-aarch64-bootstrap success Build passed
linaro-tcwg-bot/tcwg_simplebootstrap_build--master-arm-bootstrap success Build passed
linaro-tcwg-bot/tcwg_gcc_check--master-arm success Test passed

Commit Message

Andi Kleen July 17, 2026, 8:38 p.m. UTC
  From: Andi Kleen <ak@gcc.gnu.org>

Some usages of libbacktrace which are merely interested in inlinestacks
run createstate on a binary not in the current process. This works
fine for non PIC/PIE binaries with fixed addresses. But with relocatable
binaries the base address is not known to libbacktrace and cannot be found
with the phdr* calls. That then breaks the pcinfo (or syminfo) resolution.

This patch extends the overloading of the threaded flag to createstate
that was added earlier with another flag called OFFLINE. When OFFLINE is set
libbacktrace makes no attempt to walk the current processes' PHDRs. This
results in PIE/PIC binaries being registered with a zero base address.

There are no address conflicts because only a single binary can be loaded
through createstate for a given state.

The caller of pcinfo/syminfo is then expected to adjust the address by
themselves, e.g. pass relative offsets to the beginning of the binary.
For a autofdo style usage this is fine because the perf metadata
has the base addresses, so it's just subtraction of the known base
address of the mapping.

However there is the problem that for non dynamic ELF binaries the
symbols already contain the base address. To avoid the caller needing
to know what type the binary is, and whether to adjust the addresses,
we instead subtract the base address for ET_EXEC binaries in OFFLINE
mode. This is done by looking at the lowest PHDR.
This then gives an consistent interface for OFFLINE.

[FWIW the alternative would be add a new version of createstate that
has an extra argument to pass the base address for the binary.]

Currently missing an automated in tree test that can find the
right offsets.

libbacktrace/ChangeLog:

	* backtrace.h: Update comments for OFFLINE.
	* elf.c (b_elf_phdr): Add phdr definitions.
	(elf_get_image_base): New function to find lowest PHDR.
	(elf_add): Use elf_get_image_base to adjust ET_EXEC
	addresses in OFFLINE mode.
	(backtrace_initialize): When OFFLINE skip
	walking PHDRs and force ET_DYN to zero.
	* internal.h (struct backtrace_state): Add a moredata flag.
	* state.c (backtrace_create_state): Allow and initialize moredata.
---
 libbacktrace/backtrace.h |  15 ++++-
 libbacktrace/elf.c       | 122 +++++++++++++++++++++++++++++++++++----
 libbacktrace/internal.h  |   2 +
 libbacktrace/state.c     |   5 +-
 4 files changed, 130 insertions(+), 14 deletions(-)
  

Patch

diff --git a/libbacktrace/backtrace.h b/libbacktrace/backtrace.h
index 5b076183ca0..f75ee1a3940 100644
--- a/libbacktrace/backtrace.h
+++ b/libbacktrace/backtrace.h
@@ -94,6 +94,7 @@  typedef void (*backtrace_error_callback) (void *data, const char *msg,
    FLAGS passes flags as bits in an int value:
    1: THREADED
    2: MOREDATA
+   4: OFFLINE
 
    If (FLAGS & 1) != 0 the THREADED flag is set.  If this flag is set,
    the state may be accessed by multiple threads simultaneously, and the
@@ -107,6 +108,12 @@  typedef void (*backtrace_error_callback) (void *data, const char *msg,
    a backward compatible approach to getting more data from the various
    backtrace functions.
 
+   if (FLAGS & 4) != 0 the OFFLINE flag is set.  Don't assume the binary
+   passed as FILENAME is for the current process. pcinfo and syminfo use
+   addresses relative to the lowest page-aligned PT_LOAD virtual address
+   in the ELF file. Shared libraries require opening each shared library
+   individually.
+
    Historical note: in previous versions (before July, 2026) the FLAGS
    argument was named THREADED, and passing non-zero for THREADED was
    documented as doing what setting the THREADED flag does today.  In
@@ -194,7 +201,9 @@  extern void backtrace_print (struct backtrace_state *state, int skip, FILE *);
    the debugging information contains the necessary information, then
    this may call the callback function multiple times.  This will make
    at least one call to either CALLBACK or ERROR_CALLBACK.  This
-   returns the first non-zero value returned by CALLBACK, or 0.  */
+   returns the first non-zero value returned by CALLBACK, or 0.
+   When the state has been opened in OFFLINE mode PC must be relative to
+   the lowest page-aligned PT_LOAD virtual address in the ELF file.  */
 
 extern int backtrace_pcinfo (struct backtrace_state *state, uintptr_t pc,
 			     backtrace_full_callback callback,
@@ -223,7 +232,9 @@  typedef void (*backtrace_syminfo_callback) (void *data, uintptr_t pc,
    the symbol table but does not require the debug info.  Note that if
    the symbol table is present but ADDR could not be found in the
    table, CALLBACK will be called with a NULL SYMNAME argument.
-   Returns 1 on success, 0 on error.  */
+   Returns 1 on success, 0 on error. When STATE was created with OFFLINE,
+   ADDR must be relative to the lowest page-aligned PT_LOAD virtual address
+   in the ELF file.  */
 
 extern int backtrace_syminfo (struct backtrace_state *state, uintptr_t addr,
 			      backtrace_syminfo_callback callback,
diff --git a/libbacktrace/elf.c b/libbacktrace/elf.c
index 60489327024..297fb083b4b 100644
--- a/libbacktrace/elf.c
+++ b/libbacktrace/elf.c
@@ -231,6 +231,32 @@  typedef struct {
   b_elf_half	e_shstrndx;		/* Section header string table index */
 } b_elf_ehdr;  /* Elf_Ehdr.  */
 
+#if BACKTRACE_ELF_SIZE == 32
+typedef struct {
+  b_elf_word p_type;
+  b_elf_off p_offset;
+  b_elf_addr p_vaddr;
+  b_elf_addr p_paddr;
+  b_elf_wxword p_filesz;
+  b_elf_wxword p_memsz;
+  b_elf_word p_flags;
+  b_elf_wxword p_align;
+} b_elf_phdr;
+#else
+typedef struct {
+  b_elf_word p_type;
+  b_elf_word p_flags;
+  b_elf_off p_offset;
+  b_elf_addr p_vaddr;
+  b_elf_addr p_paddr;
+  b_elf_wxword p_filesz;
+  b_elf_wxword p_memsz;
+  b_elf_wxword p_align;
+} b_elf_phdr;
+#endif
+
+#define PT_LOAD 1
+
 #define EI_MAG0 0
 #define EI_MAG1 1
 #define EI_MAG2 2
@@ -458,6 +484,67 @@  elf_release_view (struct backtrace_state *state, struct elf_view *view,
     backtrace_release_view (state, &view->view, error_callback, data);
 }
 
+/* Find the lowest loadable virtual address in an ELF image.  Offline
+   addresses are normalized relative to this image base, including for
+   ET_EXEC files whose link-time addresses are normally nonzero.  */
+static int
+elf_get_image_base (struct backtrace_state *state, int descriptor,
+		    const unsigned char *memory, size_t memory_size,
+		    const b_elf_ehdr *ehdr,
+		    backtrace_error_callback error_callback, void *data,
+		    uintptr_t *image_base)
+{
+  struct elf_view phdrs_view;
+  uint64_t phdrs_size;
+  b_elf_addr min_vaddr = ~(b_elf_addr) 0;
+  b_elf_wxword max_align = 1;
+  unsigned int i;
+
+  if (ehdr->e_phnum == 0
+      || ehdr->e_phentsize < sizeof (b_elf_phdr))
+    {
+      error_callback (data, "ELF has no usable program headers", 0);
+      return 0;
+    }
+
+  phdrs_size = (uint64_t) ehdr->e_phnum * ehdr->e_phentsize;
+  if (phdrs_size / ehdr->e_phentsize != ehdr->e_phnum
+      || !elf_get_view (state, descriptor, memory, memory_size,
+			ehdr->e_phoff, phdrs_size, error_callback, data,
+			&phdrs_view))
+    return 0;
+
+  for (i = 0; i < ehdr->e_phnum; ++i)
+    {
+      b_elf_phdr phdr;
+
+      memcpy (&phdr,
+	      (const unsigned char *) phdrs_view.view.data
+	      + (size_t) i * ehdr->e_phentsize,
+	      sizeof phdr);
+      if (phdr.p_type == PT_LOAD)
+	{
+	  if (phdr.p_vaddr < min_vaddr)
+	    min_vaddr = phdr.p_vaddr;
+	  if (phdr.p_align > max_align)
+	    max_align = phdr.p_align;
+	}
+    }
+
+  elf_release_view (state, &phdrs_view, error_callback, data);
+
+  if (min_vaddr == ~(b_elf_addr) 0)
+    {
+      error_callback (data, "ELF has no loadable segments", 0);
+      return 0;
+    }
+
+  if ((max_align & (max_align - 1)) != 0)
+    max_align = 1;
+  *image_base = (uintptr_t) (min_vaddr & ~(max_align - 1));
+  return 1;
+}
+
 /* Compute the CRC-32 of BUF/LEN.  This uses the CRC used for
    .gnu_debuglink files.  */
 
@@ -6639,6 +6726,7 @@  elf_add (struct backtrace_state *state, const char *filename, int descriptor,
   struct elf_ppc64_opd_data opd_data, *opd;
   int opd_view_valid;
   struct dwarf_sections dwarf_sections;
+  uintptr_t image_base;
 
   if (!debuginfo)
     {
@@ -6707,10 +6795,19 @@  elf_add (struct backtrace_state *state, const char *filename, int descriptor,
       error_callback (data, "executable file has unknown endianness", 0);
       goto fail;
     }
+  if (state->offline && !debuginfo && memory == NULL
+      && !libbacktrace_using_fdpic ())
+    {
+      if (!elf_get_image_base (state, descriptor, memory, memory_size,
+			       &ehdr, error_callback, data, &image_base))
+	goto fail;
+      base_address.m -= image_base;
+    }
 
   /* If the executable is ET_DYN, it is either a PIE, or we are running
      directly a shared library with .interp.  We need to wait for
-     dl_iterate_phdr in that case to determine the actual base_address.  */
+     dl_iterate_phdr in that case to determine the actual base_address.
+     Offline states instead use the normalized image base above.  */
   if (exe && ehdr.e_type == ET_DYN)
     return -1;
 
@@ -7500,21 +7597,24 @@  backtrace_initialize (struct backtrace_state *state, const char *filename,
       memset (&zero_base_address, 0, sizeof zero_base_address);
       ret = elf_add (state, filename, descriptor, NULL, 0, zero_base_address,
 		     NULL, error_callback, data, &elf_fileline_fn, &found_sym,
-		     &found_dwarf, NULL, 1, 0, NULL, 0);
+		     &found_dwarf, NULL, state->offline ? 0 : 1, 0, NULL, 0);
       if (!ret)
 	return 0;
     }
 
-  pd.state = state;
-  pd.error_callback = error_callback;
-  pd.data = data;
-  pd.fileline_fn = &elf_fileline_fn;
-  pd.found_sym = &found_sym;
-  pd.found_dwarf = &found_dwarf;
-  pd.exe_filename = filename;
-  pd.exe_descriptor = ret < 0 ? descriptor : -1;
+  if (!state->offline)
+    {
+      pd.state = state;
+      pd.error_callback = error_callback;
+      pd.data = data;
+      pd.fileline_fn = &elf_fileline_fn;
+      pd.found_sym = &found_sym;
+      pd.found_dwarf = &found_dwarf;
+      pd.exe_filename = filename;
+      pd.exe_descriptor = ret < 0 ? descriptor : -1;
 
-  dl_iterate_phdr (phdr_callback, (void *) &pd);
+      dl_iterate_phdr (phdr_callback, (void *) &pd);
+    }
 
   if (!state->threaded)
     {
diff --git a/libbacktrace/internal.h b/libbacktrace/internal.h
index 26b41fb7f7f..7ff01d0bc4f 100644
--- a/libbacktrace/internal.h
+++ b/libbacktrace/internal.h
@@ -145,6 +145,8 @@  struct backtrace_state
   int threaded;
   /* Non-zero if passing additional data.  */
   int moredata;
+  /* Non-zero if in offline mode for single binary only.  */
+  int offline;
   /* The master lock for fileline_fn, fileline_data, syminfo_fn,
      syminfo_data, fileline_initialization_failed and everything the
      data pointers point to.  */
diff --git a/libbacktrace/state.c b/libbacktrace/state.c
index 491a14dc156..252cff5a8ec 100644
--- a/libbacktrace/state.c
+++ b/libbacktrace/state.c
@@ -49,12 +49,14 @@  backtrace_create_state (const char *filename, int flags,
 {
   int threaded;
   int moredata;
+  int offline;
   struct backtrace_state init_state;
   struct backtrace_state *state;
 
   threaded = (flags & 1) != 0;
   moredata = (flags & 2) != 0;
-  if ((flags & ~3) != 0)
+  offline = (flags & 4) != 0;
+  if ((flags & ~7) != 0)
     {
       error_callback (data, "backtrace_create_state: unsupported flag", 0);
       return NULL;
@@ -72,6 +74,7 @@  backtrace_create_state (const char *filename, int flags,
   init_state.filename = filename;
   init_state.threaded = threaded;
   init_state.moredata = moredata;
+  init_state.offline = offline;
 
   state = ((struct backtrace_state *)
 	   backtrace_alloc (&init_state, sizeof *state, error_callback, data));