ELF文件——DWARF源码解析

前言

此前LEF文件——栈回溯中只描述了通过exidx进行栈回溯的过程,本文将描述根据eh_frame进行栈回溯的原理及过程。

原理说明

dwarf的核心是一张表格,该表格根据函数的压栈过程获取,以一个函数的汇编代码为例:

0000000000023c80 <_dl_start>:
_dl_start():
/usr/src/debug/glibc/2.31+gitAUTOINC+f84949f1c4-r0/git/csu/init-first.c:96
   23c80:	a9bf7bfd 	stp	x29, x30, [sp, #-16]!
   23c84:	910003fd 	mov	x29, sp
/usr/src/debug/glibc/2.31+gitAUTOINC+f84949f1c4-r0/git/csu/init-first.c:97
   23c88:	94000024 	bl	23d18 <abort>

其对应的表格为(readelf -wF):

00000050 0000000000000014 00000054 FDE cie=00000000 pc=0000000000023c80..0000000000023c8c
   LOC           CFA      x29   ra    
0000000000023c80 sp+0     u     u     
0000000000023c84 sp+16    c-16  c-8 

表格中的原始数据格式为(readelf -wf):

00000050 0000000000000014 00000054 FDE cie=00000000 pc=0000000000023c80..0000000000023c8c
  DW_CFA_advance_loc: 4 to 0000000000023c84		//ip地址
  DW_CFA_def_cfa_offset: 16						//sp偏移16
  DW_CFA_offset: r29 (x29) at cfa-16
  DW_CFA_offset: r30 (x30) at cfa-8

根据汇编对照表格可获得如下解释:

  • ip地址在23c80处时,还没有发生压栈,return addr也不发生改变
  • ip地址在23c84处时,发生了压栈,上一个栈帧的地址为cfa(call frame addr),x29则保存在cfa - 16处,return addr保存在cfa - 8 处
    由上可知,根据目标IP地址和表格,即可逆推出上一级的cfa和相关寄存器的值,依次类推即可得到完整的调用栈。

上述的该表格通过CIE(common information entry)和FDE(frame description entry)来描述,而CIE和FDE存储在eh_frame section中,为了加快FDE和CIE的查找,还存在一个eh_frame_hdr section,其各自描述如下:

  1. eh_frame_hdr结构
    eh_frame_hdr结构在文件中的组织结构如下所示:
Encoding Field
unsigned char version
unsigned char eh_frame_ptr_enc
unsigned char fde_count_enc
unsigned char table_enc
encoded eh_frame_ptr
encoded fde_count
binary search table
各成员说明如下:
  • eh_frame_ptr_enc:对结构体中eh_frame_ptr的编码,eh_frame_ptr是一个不定长的数据,需要根据eh_frame_ptr_enc的编码规则来组成该数据。
  • fde_count_enc:对结构体中fde_count的编码,fde_count同样是一个不定长的数据,需要根据fde_count_enc的编码规则来组成该数据。
  • table_enc:对fde table的编码,这个编码更像是标志位。
  • eh_frame_ptr:指向eh_frame section的首地址。
  • fde_count:搜索表中fde entry的个数
  • binary search table:包含fde count个fde entry的fde table,每个fde entry又包含起始地址和fde地址两个成员。fde table用于快速定位fde的位置,提高查找效率。

eh_frame_hdr在libunwind库中的声明如下:

struct __attribute__((packed)) dwarf_eh_frame_hdr
  {
   
    unsigned char version;
    unsigned char eh_frame_ptr_enc;	//eh_frame_ptr_enc,对内部成员eh_frame的编码格式
    unsigned char fde_count_enc;	//fde_count 编码格式 
    unsigned char table_enc;		//table encode,table entry的编码格式
    Elf_W (Addr) eh_frame;			//指向eh_frame section,
    /* The rest of the header is variable-length and consists of the
       following members:
后面紧跟的为一个变长的结构体,记录table的内容
        encoded_t fde_count;
        struct
          {
            encoded_t start_ip; // first address covered by this FDE,地址是基于eh_frame_hdr地址的偏移,不是映射地址
            encoded_t fde_addr; // address of the FDE
          }
        binary_search_table[fde_count];  */
  };
  1. FDE结构
    FDE结构在文件中的组织如下:
Field Description
Length Required
Extended Length Optional
CIE Pointer Required
PC begin Required
PC Range Required
Augmentation Data Length Optional
Augmentation Data Optional
Call Frame Instructions Required
Padding

FDE中各成员的长度不定,因此不能以固定格式的结构体对其进行描述,而是需要根据编码规则/标志读取每个成员对应的值,各成员的含义说明如下:

  • Length:FDE占据的长度,固定为4个字节,当该值为0xFFFFFFFF时,表示该FDE为64位DWARF格式,FDE长度记录在紧接着的64位数据中。注意,Length本身不包含在占据的长度中。
  • Extended Length:当FDE为64位DWARF格式时,才包含该成员,表示FDE占据的长度。
  • CIE Pointer:指向CIE的指针,该值实际上为一个偏移量,通过当前CIE Pointer所在地址减去该偏移值即可得到CIE的地址。该值占据的字节数根据DWARF的格式为4或8个字节。
  • PC Begin:被该FDE覆盖的起始地址,该值被CIE中的FDE encoding成员进行了编码,其占据的字节数需要根据编码规则确定。
  • PC range:被该FDE覆盖的范围,该值同样被CIE中的FDE encoding成员进行了编码,其占据的字节数需要根据编码规则确定。
  • Augmentation Data Length:FDE扩展数据大小,依赖于CIE中的扩展字符串,当扩展字符串以‘z’开头时才包含该成员。
  • Augmentation Data:FDE扩展数据,包含指向LSDA的指针,由CIE中的LSDA encoding成员进行了编码。
  • Call Frame Instructions:指令码序列,该序列指示如何推演调用栈。
    由于FDE中的成员依赖于CIE的编码,因此当读取FDE中的数据时,首先需要对CIE进行解析,然后对FDE数据成员进行读取。
  1. CIE结构
    CIE结构在文件中的组织架构如下:
Field Description
Length Required
Extended Length Optional
CIE ID Required
version Required
Augmentation String Required
Code Alignment Factor Required
Data Alignment Factor Required
Return Address Register Required
Augmentation Data Length Optional
Augmentation Data Optional
Inital Instructions Optional
Padding
各成员说明如下:
  • Length:CIE占据的长度,固定为4个字节,当该值为0xFFFFFFFF时,表示该FDE为64位DWARF格式,CIE长度记录在紧接着的64位数据中。
  • Extended Length:当CIE为64位DWARF格式时,才包含该成员,表示CIE占据的长度。
  • CIE ID:0。
  • version:固定为1。
  • Augmentation String:扩充参数字符串,以NULL字符结尾。
  • Code Alignment Factor:代码对齐因子。
  • Data Alignment Factor:数据对齐因子。
  • Return Address Register:返回地址寄存器,指明哪一个寄存器中保存了返回地址。
  • Augmentation Data:依赖于Augmentation String中的字符序列,‘R’字符对应fde encoding。
  • Initial Instructions:指令码

注意:
对于没有fde table的elf文件而言,可转向debug_frame去搜索符合条件的fde,只不过此时需要从debug_frame开始逐行对每个fde进行解析,直到找到目标fde。而拥有fde table的elf文件则可以直接根据搜索表定位到目标fde的地址,直接解析该fde即可。

源码解析

/*src/dwarf/Goarser.c*/
/* The function finds the saved locations and applies the register
   state as well. */
HIDDEN int
dwarf_step (struct dwarf_cursor *c)
{
   
  int ret;
  dwarf_state_record_t sr;
  if ((ret = find_reg_state (c, &sr)) < 0)//[1.1]
    return ret;
  return apply_reg_state (c, &sr.rs_current);//[1.2]
}

[1.1] 获取寄存器装态,根据FDE填充寄存器

/* Find the saved locations. */
static int
find_reg_state (struct dwarf_cursor *c, dwarf_state_record_t *sr)
{
   
  dwarf_reg_state_t *rs;
  struct dwarf_rs_cache *cache;
  int ret = 0;
  intrmask_t saved_mask;

  if ((cache = get_rs_cache(c->as, &saved_mask)) &&
      (rs = rs_lookup(cache, c)))
    {
   
      /* update hint; no locking needed: single-word writes are atomic */
      unsigned short index = rs - cache->buckets;
      c->use_prev_instr = ! cache->links[index].signal_frame;
      memcpy (&sr->rs_current, rs, sizeof (*rs));
    }
  else
    {
   
      ret = fetch_proc_info (c, c->ip);				//[1.1.1]
      int next_use_prev_instr = c->use_prev_instr;
      if (ret >= 0)
	{
   
	  /* Update use_prev_instr for the next frame. */
	  assert(c->pi.unwind_info);
	  struct dwarf_cie_info *dci = c->pi.unwind_info;
	  next_use_prev_instr = ! dci->signal_frame;
	  ret = create_state_record_for (c, sr, c->ip);//[1.1.2]
	}
      put_unwind_info (c, &c->pi);
      c->use_prev_instr = next_use_prev_instr;

      if (cache && ret >= 0)
	{
   
	  rs = rs_new (cache, c);
	  cache->links[rs - cache->buckets].hint = 0;
	  memcpy(rs, &sr->rs_current, sizeof(*rs));
	}
    }

  unsigned short index = -1;
  if (cache)
    {
   
      put_rs_cache (c->as, cache, &saved_mask);
      if (rs)
	{
   
	  index = rs - cache->buckets;
	  c->hint = cache->links[index].hint;
	  cache->links[c->prev_rs].hint = index + 1;
	  c->prev_rs = index;
	}
    }
  if (ret < 0)
      return ret;
  if (cache)
    tdep_reuse_frame (c, cache->links[index].signal_frame);
  return 0;
}

[1.1.1] 获取FDE

static int
fetch_proc_info (struct dwarf_cursor *c, unw_word_t ip)
{
   
  int ret, dynamic = 1;

  /* The 'ip' can point either to the previous or next instruction
     depending on what type of frame we have: normal call or a place
     to resume execution (e.g. after signal frame).

     For a normal call frame we need to back up so we point within the
     call itself; this is important because a) the call might be the
     very last instruction of the function and the edge of the FDE,
     and b) so that run_cfi_program() runs locations up to the call
     but not more.

     For signal frame, we need to do the exact opposite and look
     up using the current 'ip' value.  That is where execution will
     continue, and it's important we get this right, as 'ip' could be
     right at the function entry and hence FDE edge, or at instruction
     that manipulates CFA (push/pop). */
  if (c->use_prev_instr)
    --ip;

  memset (&c->pi, 0, sizeof (c->pi));

/*首先在动态库列表中查找,如果动态库列表中不存在则需要遍历进程加载的动态库去查找*/
  /* check dynamic info first --- it overrides everything else */
  ret = unwi_find_dynamic_proc_info (c->as, ip, &c->pi, 1,
                                     c->as_arg);
  if (ret == -UNW_ENOINFO)
    {
   
      dynamic = 0;
      if ((ret = tdep_find_proc_info (c, ip, 1)) < 0)
        return ret;
    }

  if (c->pi.format != UNW_INFO_FORMAT_DYNAMIC
      && c->pi.format != UNW_INFO_FORMAT_TABLE
      && c->pi.format != UNW_INFO_FORMAT_REMOTE_TABLE)
    return -UNW_ENOINFO;

  c->pi_valid = 1;
  c->pi_is_dynamic = dynamic;

  /* Let system/machine-dependent code determine frame-specific attributes. */
  if (ret >= 0)
    tdep_fetch_frame (c, ip, 1);

  return ret;
}

HIDDEN int
unwi_find_dynamic_proc_info (unw_addr_space_t as, unw_word_t ip,
                             unw_proc_info_t *pi, int need_unwind_info,
                             void *arg)
{
   
/*判断是否是当前地址空间,即是查找本进程的porc_info还是查找其他进程(被attach)的proc_info*/
  if (as == unw_local_addr_space)
    return local_find_proc_info (as, ip, pi, need_unwind_info, arg);
  else
    return remote_find_proc_info (as, ip, pi, need_unwind_info, arg);
}
static inline int
local_find_proc_info (unw_addr_space_t as, unw_word_t ip, unw_proc_info_t *pi,
                      int need_unwind_info, void *arg)
{
   
  unw_dyn_info_list_t *list;
  unw_dyn_info_t *di;

/*检查是否存在动态库链表,无则直接返回,有则在链表中查找符合条件的动态库*/
#ifndef UNW_LOCAL_ONLY
# pragma weak _U_dyn_info_list_addr
  if (!_U_dyn_info_list_addr)
    return -UNW_ENOINFO;
#endif

  list = (unw_dyn_info_list_t *) (uintptr_t) _U_dyn_info_list_addr ();
  for (di = list->first; di; di = di->next)
    if (ip >= di->start_ip && ip < di->end_ip)
      return unwi_extract_dynamic_proc_info (as, ip, pi, di, need_unwind_info,
                                             arg);
  return -UNW_ENOINFO;
}
/*下文分析以arm架构、local unwind为例,/include/tdep-arm/libunwind_i.h*/
#ifdef UNW_LOCAL_ONLY
# define tdep_find_proc_info(c,ip,n)                            \
        arm_find_proc_info((c)->as, (ip), &(c)->pi, (n),        
  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
在Python中,要获取ELF文件的DWARF信息,可以使用第三方库pyelftools。pyelftools是一个用于解析ELF文件格式的Python库,其中包含了解析和访问DWARF调试信息的功能。 首先,我们需要安装pyelftools库。可以使用pip命令进行安装: ``` pip install pyelftools ``` 安装完成后,可以使用以下代码来获取ELF文件的DWARF信息: ```python from elftools.elf.elffile import ELFFile from elftools.dwarf.descriptions import describe_form_class # 打开ELF文件 with open('path_to_elf_file', 'rb') as f: # 创建ELF文件对象 elf = ELFFile(f) # 获取DWARF调试信息 dwarf_info = elf.get_dwarf_info() # 遍历所有DWARF编译单元 for compile_unit in dwarf_info.iter_CUs(): # 获取编译单元中的所有DIE(Debugging Information Entry) for die in compile_unit.iter_DIEs(): # 打印DIE的tag和属性信息 print('Tag:', die.tag) print('Attributes:') for attr_name, attr_value in die.attributes.items(): attr_form = describe_form_class(attr_value.form).__name__ print(f'{attr_name}: {attr_value} ({attr_form})') print() ``` 上述代码首先打开ELF文件,然后创建ELF文件对象。接着,使用`get_dwarf_info`方法获取DWARF调试信息。随后,通过迭代编译单元和DIE,可以获取ELF文件中的DWARF信息。使用`tag`属性可以获取DIE的标签信息,使用`attributes`属性可以获取DIE的属性信息。 需要注意的是,`path_to_elf_file`需要替换为实际的ELF文件路径。此外,pyelftools库还提供了其他方法来解析和访问DWARF调试信息,具体可以参考官方文档。 总结而言,通过使用pyelftools库,可以在Python中方便地获取ELF文件的DWARF信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值