ELF file - stack backtracking

Keywords: Linux


This paper describes the stack backtracking process with libunwind library.

libunwind stack backtracking process

Libunwind contains two sets of user interfaces, each prefixed with unw_ And_ Unwind ID, where_ The unwind prefix interface is a high-level function interface for C + + exception handling, and the unw prefix is a more general-purpose interface. According to the configure.ac file in the libunwind code, exception handling in C + + is not enabled under the arm architecture, so the interfaces used for stack backtracking are function interfaces with the prefix bit unw.

AC_MSG_CHECKING([whether to enable C++ exception support])
AS_HELP_STRING([--enable-cxx-exceptions],[use libunwind to handle C++ exceptions]),,
# C++ exception handling doesn't work too well on x86
case $target_arch in
  x86*) enable_cxx_exceptions=no;;
  aarch64*) enable_cxx_exceptions=no;;
  arm*) enable_cxx_exceptions=no;;
  mips*) enable_cxx_exceptions=no;;
  tile*) enable_cxx_exceptions=no;;
  *) enable_cxx_exceptions=yes;;

Stack backtracking to function unw_backtrace as the starting point:

unw_backtrace (void **buffer, int size)
  unw_cursor_t cursor;
  unw_context_t uc;
  int n = size;

  tdep_getcontext_trace (&uc); //Saves the value of the current register

  if (unlikely (unw_init_local (&cursor, &uc) < 0))  //Initialize cursor and save the value of the current register
    return 0;

  if (unlikely (tdep_trace (&cursor, buffer, &n) < 0)) //Fast search. Search in the cache. If it is not found in the cache, you need to backtrack one by one according to the elf file
      unw_getcontext (&uc);
      return slow_backtrace (buffer, size, &uc); //Backtracking function by function

  return n;
#define tdep_getcontext_trace           unw_getcontext
#define unw_getcontext(uc)		unw_tdep_getcontext(uc)

#ifndef __thumb__ 
#define unw_tdep_getcontext(uc) (({                                     \ 
  unw_tdep_context_t *unw_ctx = (uc);                                   \ 
  register unsigned long *unw_base __asm__ ("r0") = unw_ctx->regs;      \ 
  __asm__ __volatile__ (                                                \ 
    "stmia %[base], {r0-r15}"                                           \ 
    : : [base] "r" (unw_base) : "memory");                              \ 
  }), 0) 
#else /* __thumb__ */ 
#define unw_tdep_getcontext(uc) (({                                     \ 
  unw_tdep_context_t *unw_ctx = (uc);                                   \ 
  register unsigned long *unw_base __asm__ ("r0") = unw_ctx->regs;      \ 
  __asm__ __volatile__ (                                                \ 
    ".align 2\nbx pc\nnop\n.code 32\n"                                  \ 
    "stmia %[base], {r0-r15}\n"                                         \ 
    "orr %[base], pc, #1\nbx %[base]\n"                                 \ 
    ".code 16\n"                                                        \ 
    : [base] "+r" (unw_base) : : "memory", "cc");                       \ 
  }), 0) 
//Save the values of R0-R15 to UC - > reg [0] - UC - > reg [15] successively
int unw_init_local (unw_cursor_t *cursor, unw_context_t *uc)
  return unw_init_local_common(cursor, uc, 1);

static int unw_init_local_common (unw_cursor_t *cursor, unw_context_t *uc, unsigned use_prev_instr)
  struct cursor *c = (struct cursor *) cursor;

  if (!tdep_init_done)
    tdep_init ();

  Debug (1, "(cursor=%p)\n", c);

  c->dwarf.as = unw_local_addr_space;
  c->dwarf.as_arg = uc;

  return common_init (c, use_prev_instr);

static inline int
common_init (struct cursor *c, unsigned use_prev_instr)
  int ret, i;
//Save the address of the previously saved register value to struct cursor
  c->dwarf.loc[UNW_ARM_R0] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R0);  
  c->dwarf.loc[UNW_ARM_R1] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R1);
  c->dwarf.loc[UNW_ARM_R2] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R2);
  c->dwarf.loc[UNW_ARM_R3] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R3);
  c->dwarf.loc[UNW_ARM_R4] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R4);
  c->dwarf.loc[UNW_ARM_R5] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R5);
  c->dwarf.loc[UNW_ARM_R6] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R6);
  c->dwarf.loc[UNW_ARM_R7] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R7);
  c->dwarf.loc[UNW_ARM_R8] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R8);
  c->dwarf.loc[UNW_ARM_R9] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R9);
  c->dwarf.loc[UNW_ARM_R10] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R10);
  c->dwarf.loc[UNW_ARM_R11] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R11);
  c->dwarf.loc[UNW_ARM_R12] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R12);
  c->dwarf.loc[UNW_ARM_R13] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R13);
  c->dwarf.loc[UNW_ARM_R14] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R14);
  c->dwarf.loc[UNW_ARM_R15] = DWARF_REG_LOC (&c->dwarf, UNW_ARM_R15);
  for (i = UNW_ARM_R15 + 1; i < DWARF_NUM_PRESERVED_REGS; ++i)
    c->dwarf.loc[i] = DWARF_NULL_LOC;

  ret = dwarf_get (&c->dwarf, c->dwarf.loc[UNW_ARM_R15], &c->dwarf.ip);
  if (ret < 0)
    return ret;

  /* FIXME: correct for ARM?  */
  ret = dwarf_get (&c->dwarf, DWARF_REG_LOC (&c->dwarf, UNW_ARM_R13),
  if (ret < 0)
    return ret;

  c->sigcontext_format = ARM_SCF_NONE;
  c->sigcontext_addr = 0;
  c->sigcontext_sp = 0;
  c->sigcontext_pc = 0;

  /* FIXME: Initialisation for other registers.  */

  c->dwarf.args_size = 0;
  c->dwarf.stash_frames = 0;
  c->dwarf.use_prev_instr = use_prev_instr;
  c->dwarf.pi_valid = 0;
  c->dwarf.pi_is_dynamic = 0;
  c->dwarf.hint = 0;
  c->dwarf.prev_rs = 0;

  return 0;

#include <linunwind_i.h>   
#define DWARF_REG_LOC(c,r)     (DWARF_LOC((unw_word_t) tdep_uc_addr((c)->as_arg, (r)), 0))                      \
#define DWARF_LOC(r, t)        ((dwarf_loc_t) { .val = (r) }) 
HIDDEN void * 
tdep_uc_addr (unw_tdep_context_t *uc, int reg) 
  return uc_addr (uc, reg); 
uc_addr (unw_tdep_context_t *uc, int reg) 
  if (reg >= UNW_ARM_R0 && reg < UNW_ARM_R0 + 16) 
    return &uc->regs[reg - UNW_ARM_R0];           //The address where the register value is stored. According to the code backtracking, this address should be on the stack, 
    return NULL; 

libunwind will use three backtracking methods to backtrack the stack by default, among which the DWARF method is through eh_frame section is used for backtracking. Only config is configured_ DEBUG_ This section will be compiled only by the frame macro. In configure.ac, the macro is opened by default for the arm architecture. This article only describes the stack backtracking method using exidx for the time being.

int unw_step (unw_cursor_t *cursor)
    if (UNW_TRY_METHOD(UNW_ARM_METHOD_DWARF))  //DWARF mode backtracking
        ret = dwarf_step (&c->dwarf);
    if (UNW_TRY_METHOD (UNW_ARM_METHOD_EXIDX))  //unwind table backtracking
      ret = arm_exidx_step (c);
    if (UNW_TRY_METHOD(UNW_ARM_METHOD_FRAME))   //frame pointer backtracking
//When using the corresponding method for backtracking, you also need to judge whether the corresponding backtracking method is enabled
#define UNW_TRY_METHOD(x)   (unwi_unwind_method & x)

//In Gglobal.c, according to the definition of the global variable, the value of the global variable can be changed to enable the corresponding stack backtracking method
HIDDEN int unwi_unwind_method = UNW_ARM_METHOD_ALL;

#define UNW_ARM_METHOD_ALL          0xFF
#define UNW_ARM_METHOD_DWARF        0x01
#define UNW_ARM_METHOD_FRAME        0x02
#define UNW_ARM_METHOD_EXIDX        0x04

exidx structure

Setions related to stack backtracking in ELF files include exidx and extab section s. The information is as follows:

  [14] .ARM.extab        PROGBITS        000063e0 0063e0 00030c 00   A  0   0  4
  [15] .ARM.exidx        ARM_EXIDX       000066ec 0066ec 0001a0 00  AL 11   0  4

exidx section is a table. Each item in the table is described by the following structure:

struct EHEntry {
  uint32_t Offset; //The function start offset needs to be calculated in combination with the current address
  uint32_t World1; //Bytecode / data to be interpreted

EHEntry.Offset does not directly point to its function address (physical Offset of ELF file), but indirectly uses this value to be calculated according to relocation rules to ensure the universality of exidx section on various platforms. Relocation type is R_ARM_PREL31. In this relocation type, the calculation rule of function address is current address + Offset, where Offset is a 31 bit signed number, and its highest bit is fixed to 0. When calculating the function address, first convert the 31 bit signed number into a 32-bit signed number. The implementation process of the transformation is as follows:

offset = ((long)offset << 1) >> 1;

For example, 0x7FFFFFFF is - 1 in the 31 bit signed number, but 214748364 in the 32-bit signed number. In order to ensure that it still maintains the value of - 1 in the 32-bit signed number, it is necessary to move the 32-bit data to the left first, remove the highest bit not used in the 31 bit number (0x7fffffffff < < 1 = 0xfffffe), and then move it to the right one bit, Keep the highest bit of the 32-bit number consistent with the highest bit of the 31 bit number (the symbol bits are the same), that is (0xfffffffe > > 1 = 0xFFFFFFFF = - 1). At this time, the value parsed from the 32-bit signed number is the real value of the 31 bit signed number. Use the unwind table read by the readelf tool to verify the correctness of the calculation:

Unwind section '.ARM.exidx' at offset 0x66ec contains 52 entries:

0x2178 <deregister_tm_clones>: 0x80b0b0b0
  Compact model index: 0
  0xb0      finish
  0xb0      finish
  0xb0      finish

0x21d8 <__do_global_dtors_aux>: @0x63e0
  Compact model index: 1
  0xb1 0x08 pop {r3}
  0x84 0x00 pop {r14}
  0xb0      finish
  0xb0      finish

Take the second item in the unwind table as an example. The HEX value in the ELF file is 0x66ec(exidx section addr) + 8(entry size) = 0x66f4, as follows:

The value of Offset is 0x7fffbae4 and its address is 0x66f4. The address of the function is:

0x66f4 + ((long)0x7fffbae4 << 1) >> 1 = 0x66f4 + (0xffff75c8 >> 1) = 0x66f4 + 0xffffbae4 = 0x21d8

The calculated function address is consistent with the function address in the unwind table read by the readelf tool.

EHEntry.Word1 records the bytecode of how to perform stack backtracking on the corresponding function. The parsing classification of this element is as follows:

  • EHEntry.Word1 = 1, indicating Cannot Unwind
  • The highest bit of EHEntry.Word1 is 0, and the lower 31 bits form a value of type prel31, pointing to an item in extab (exception handling table), which records the bytecode of how to stack backtrace the function
  • EHEntry.Word1 the highest bit is 1 and the lower 31 bits form a bytecode, which is equivalent to an item in extab

extab entry has two modes, which are distinguished by the highest value:

  • The generic model: the highest bit is 0, and the lower 31 bits form a value of type prel31, pointing to the personality routine
  • The arm defined compact model: the highest bit is fixed to 1, and other bits are composed as follows:
10indexdata for personalityRoutine[index]

index contains three values: 0, 1 and 2, which respectively represent different backtracking paths:

  • 0, su16 - short frame unwinding description followed by descriptors with 16 bit scope__ aeabi_unwind_cpp_pr0
  • 1, lu16 - long frame unwinding description followed by descriptors with 16 bit scope__ aeabi_unwind_cpp_pr1
  • 2. Lu32 - Long frame unwinding description followed by descriptors with 32-bit scope__ aeabi_unwind_cpp_pr2

For short frame unwinding (index is 0), 0-23 bits of 32-bit data contain three instructions, and each instruction occupies 8 bits; For long frame unwinding(index is 1 or 2), 16-23 bits of 32-bit data indicate how many words (4 bytes) are included to store the backtracking instruction of the function in addition to this word, and 0-7 and 8-15 of 32-bit data contain two instructions. Still take the second item in the above unwind table as an example, its EHEntry.Word1 is 0x7ffffce8, and the highest bit is 0, then 0x7ffffce8 is the prel31 value of an extab entry address, which can be obtained through calculation:

0x66f8 + ((long)0x7ffffce8 << 1) >> 1 = 0x66f8 + 0xfffffce8 = 0x63e0

View the contents at 0x63e0 as follows:


The highest bit of the value is 1 and the index is 1. At this time, the data in bits 16-23 indicates that there is another word to save the instruction, that is, 0x8400b0b0, 0-7, 8-15 bits save the instruction 0xb108, extab address 0x63e0 and extab content 0xb108, 0x8400b0b0 are consistent with the unwind table entry read out by readelf. As for whether the two bytes 0xb1 0x08 represent one instruction or two instructions, it needs to be parsed against the bytecode.

In fact, arm unwind table only traces the value of sp according to the value of the function in and out of the stack, and then performs stack backtracking according to the value of lr pressed into the stack. It can only trace back the function, but it can not trace back the value of the register in each stack frame, that is, the context of the register when the function is called.

When performing stack backtracking according to arm unwind table, compare the address of the function in the unwind table with the current PC through dichotomy until the PC value is greater than the function address of one item and less than the function address of the next item. Finally, use the instruction code in this item for backtracking.

exidx stack backtracking process

tatic inline int
arm_exidx_step (struct cursor *c)
  unw_word_t old_ip, old_cfa;
  uint8_t buf[32];
  int ret;

  old_ip = c->dwarf.ip;
  old_cfa = c->dwarf.cfa;

  /* mark PC unsaved */
  c->dwarf.loc[UNW_ARM_R15] = DWARF_NULL_LOC;
  unw_word_t ip = c->dwarf.ip;
  if (c->dwarf.use_prev_instr)

  /* check dynamic info first --- it overrides everything else */
  ret = unwi_find_dynamic_proc_info (c->dwarf.as, ip, &c->dwarf.pi, 1,
                                     c->dwarf.as_arg);   //Find exidx entry corresponding to pc value
  if (ret == -UNW_ENOINFO)
      if ((ret = tdep_find_proc_info (&c->dwarf, ip, 1)) < 0)
        return ret;

  if (c->dwarf.pi.format != UNW_INFO_FORMAT_ARM_EXIDX)
    return -UNW_ENOINFO;

  ret = arm_exidx_extract (&c->dwarf, buf);  //Get the bytecode corresponding to exidx entry
  if (ret == -UNW_ESTOPUNWIND)
    return 0;
  else if (ret < 0)
    return ret;

  ret = arm_exidx_decode (buf, ret, &c->dwarf); //Parse and trace the bytecode
  if (ret < 0)
    return ret;

  if (c->dwarf.ip == old_ip && c->dwarf.cfa == old_cfa)
      Dprintf ("%s: ip and cfa unchanged; stopping here (ip=0x%lx)\n",
               __FUNCTION__, (long) c->dwarf.ip);
      return -UNW_EBADFRAME;

  c->dwarf.pi_valid = 0;

  return (c->dwarf.ip == 0) ? 0 : 1;

Function unwi_find_dynamic_proc_info and function tdep_find_proc_info will eventually be called to arm_search_unwind_table, which finds an item corresponding to PC (ip) value in exidx section by dichotomy as the item of subsequent stack backtracking.

arm_search_unwind_table (unw_addr_space_t as, unw_word_t ip,
			 unw_dyn_info_t *di, unw_proc_info_t *pi,
			 int need_unwind_info, void *arg)
  /* The .ARM.exidx section contains a sorted list of key-value pairs -
     the unwind entries.  The 'key' is a prel31 offset to the start of a
     function.  We binary search this section in order to find the
     appropriate unwind entry.  */
  unw_word_t first = di->u.rti.table_data;
  unw_word_t last = di->u.rti.table_data + di->u.rti.table_len - 8;
  unw_word_t entry, val;

  if (prel31_to_addr (as, arg, first, &val) < 0 || ip < val)
    return -UNW_ENOINFO;

  if (prel31_to_addr (as, arg, last, &val) < 0)
    return -UNW_EINVAL;

  if (ip >= val)
      entry = last;

      if (prel31_to_addr (as, arg, last, &pi->start_ip) < 0)
	return -UNW_EINVAL;

      pi->end_ip = di->end_ip -1;
      while (first < last - 8)
	  entry = first + (((last - first) / 8 + 1) >> 1) * 8;

	  if (prel31_to_addr (as, arg, entry, &val) < 0)
	    return -UNW_EINVAL;

	  if (ip < val)
	    last = entry;
	    first = entry;

      entry = first;

      if (prel31_to_addr (as, arg, entry, &pi->start_ip) < 0)
	return -UNW_EINVAL;

      if (prel31_to_addr (as, arg, entry + 8, &pi->end_ip) < 0)
	return -UNW_EINVAL;


  if (need_unwind_info)
      pi->unwind_info_size = 8;
      pi->unwind_info = (void *) entry;
      pi->format = UNW_INFO_FORMAT_ARM_EXIDX;
  return 0;

For a specific platform, it is best to use the same stack backtracking method when compiling various components, otherwise unexpected errors can be caused. For example, when a call stack contains two stack backtracking methods using exidx and frame pointer, when frame pointer is used to backtrack several levels of functions, the next level function uses exidx stack backtracking method. Exidx stack backtracking method does not save the stack frame address of the upper level. When frame pointer is used for backtracking again, Because the stack frame structure has been broken at this time, the complete call stack cannot be traced back, and the abnormal address may be accessed, resulting in abort.

In addition, when tracking or analyzing programs through tools such as gdb, not only the stack backtracking function will be used, but also the debugging information will be used. The debugging information is stored in. debug_frame,.debug_info and other sections are generated by the compilation parameter - g. these sections provide more specific information for analysis, such as the file and line number of the function. This information is not useful for normal program operation, but for debugging. It is generally not included in the release version, but in the debug version. When calling a function that does not contain debug information in gdb, there may be the following error:

" xxx" has unknown return type; cast the call to its declared return type

This is because gdb does not default that the return parameter of the function that does not contain debug information is void, and the function type of the specified call needs to be displayed. The official website explains as follows:

GDB no longer assumes functions with no debug information return

  This means that GDB now refuses to call such functions unless you
  tell it the function's type, by either casting the call to the
  declared return type, or by casting the function to a function
  pointer of the right type, and calling that:

reference resources

Posted by benutne on Sun, 26 Sep 2021 03:48:07 -0700