从实例分析ELF格式的.gnu.hash区与glibc的符号查询全过程

前言

ELF格式的.gnu.hash节在设计上比较复杂,直接从glibc源码进行分析的难度也比较大。今天静下心来看了这篇精彩的文章,终于将布隆滤波器、算数运算转为位运算等一系列细节搞懂了(值得一提的是,这篇博客十分值得花一些时间读懂,它不仅对总体有一个较好的描述,而且还涉及了许多有益的实现细节)。但本人愚钝异常,没有一个完整的walkthrough就不能觉得自己真的搞懂了一个东西。所以本文从查找一个符号的真实情况出发,把ELF格式是如何组织一个符号,以及动态链接器如何读取并处理这些信息以进行符号查询的全过程详细地讲清楚。
本文假定读者已经读过上文中提到的博客,并理解布隆滤波器,GNU hash采用的单一哈希策略,把取模转为取与这些名词。在后续有时间时我可能会对它们进行简单介绍,但珠玉在前让人确实不想献丑。

本文的实现以及so文件均以glibc 2.31为准。

符号哈希,符号表与字符表

一个符号的相关信息会在ELF文件中dynamic section的三块出现:.gnu.hash对应的符号哈希,.dynsym对应的动态符号表,.dynstr对应的字符表。在查找符号时,动态链接器首先从.gnu.hash中进行查询,得到该符号在动态符号表中的偏移。动态链接器根据这个偏移读出一个符号,并找到这个符号的名字在字符表中的偏移。从字符表中读出符号的名称如果与要查找的符号匹配,则找到了这个符号,再从符号表中读出符号的相关信息并返回。

64位ELF格式的符号定义如下:

// in <elf.h>
typedef struct
{
  // 32 bits
  Elf64_Word	st_name;		/* Symbol name (string tbl index) */
  // 8 bit
  unsigned char	st_info;		/* Symbol type and binding */
  // 8 bit
  unsigned char st_other;		/* Symbol visibility */
  // 16 bits
  Elf64_Section	st_shndx;		/* Section index */
  // 64 bits
  Elf64_Addr	st_value;		/* Symbol value */
  // 64 bits
  Elf64_Xword	st_size;		/* Symbol size */
} Elf64_Sym;

这个数据结构占用内存的大小为24B,这也是合理安排成员顺序以节约文件大小的一个例子。

.gnu.hash的结构

glibc使用如下函数从ELF文件中读取符号哈希相关信息:

// in elf/dl-lookup.c
void
_dl_setup_hash (struct link_map *map)
{
  Elf_Symndx *hash;

  if (__glibc_likely (map->l_info[ELF_MACHINE_GNU_HASH_ADDRIDX] != NULL))
    {
      // 一个指向32位长内存的指针,用来读取哈希相关变量,故名hash32
      Elf32_Word *hash32
	= (void *) D_PTR (map, l_info[ELF_MACHINE_GNU_HASH_ADDRIDX]);
      map->l_nbuckets = *hash32++;
      Elf32_Word symbias = *hash32++;
      Elf32_Word bitmask_nwords = *hash32++;
      /* Must be a power of two.  */
      assert ((bitmask_nwords & (bitmask_nwords - 1)) == 0);
      map->l_gnu_bitmask_idxbits = bitmask_nwords - 1;
      map->l_gnu_shift = *hash32++;

      map->l_gnu_bitmask = (ElfW(Addr) *) hash32;
      hash32 += __ELF_NATIVE_CLASS / 32 * bitmask_nwords;

      map->l_gnu_buckets = hash32;
      hash32 += map->l_nbuckets;
      map->l_gnu_chain_zero = hash32 - symbias;

      /* Initialize MIPS xhash translation table.  */
      ELF_MACHINE_XHASH_SETUP (hash32, symbias, map);

      return;
    }
  // 以下处理古老的DT_HASH项,现已不用
  if (!map->l_info[DT_HASH])
    return;
  hash = (void *) D_PTR (map, l_info[DT_HASH]);//Q: what about some non-GNU ELFs

  map->l_nbuckets = *hash++;
  /* Skip nchain.  */
  hash++;
  map->l_buckets = hash;
  hash += map->l_nbuckets;
  map->l_chain = hash;
}

上述代码读取了关键变量赋值:l_nbuckets,symbias,bitmask_nwords,l_gnu_shift,l_gnu_buckets,l_gnu_chain_zero。其中,以“l”开头的变量存储在ELF文件的link_map中,具体定义见<link.h>。还有不是从文件中读出的变量l_gnu_bitmask_idxbits,它们的具体含义为:

  • l_nbuckets:使用哈希桶的数量
  • symbias:动态符号表中外部不能访问的符号数量,但它们仍然占用了动态符号表项
  • bitmask_nwords:使用bitmask_nwords个字作为布隆滤波器的向量
  • l_gnu_shift:为使用同一哈希函数实现k=2的布隆滤波器,需要右移的位数
  • l_gnu_buckets:哈希桶的开始地址
  • l_gnu_chain_zero:符号哈希值的开始地址
  • l_gnu_bitmask_idxbits:为对bitmask_nwords取模化为取与,由bitmask_nwords-1而来

为了便于理解,将.gnu.hash节中的内容画成示意图:

以libc为例。检查对应字段的值:

$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep .gnu.hash -A 5

Contents of section .gnu.hash:
 38a0 (f3030000)        (0c000000)      (00010000)            (0e000000)     ................
      ->l_nbuckets=1011 ->symbias=12    ->bitmask_nwords=256  ->l_gnu_shift=14
 38b0 (00301044 a0200201) (8803e690 c5458c00)  .0.D. .......E..
      ->第一个bloom word 0x010220a044103000
 38c0 c4005800 07840070 c280010d 8a0c4104  ..X....p......A.
 38d0 10008840 32082a40 88543c2d 200e3248  ...@2.*@.T<- .2H
 38e0 2684c08c 04080002 020ea1ac 1a0666c8  &.............f.

可以看到symbias=12,即有12个内部符号:

$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | head -n 20

Symbol table '.dynsym' contains 2367 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __libpthread_freeres
     2: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  UND _rtld_global@GLIBC_PRIVATE (33)
     3: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  UND __libc_enable_secure@GLIBC_PRIVATE (33)
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __tls_get_addr@GLIBC_2.3 (34)
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _dl_exception_create@GLIBC_PRIVATE (33)
     6: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  UND _rtld_global_ro@GLIBC_PRIVATE (33)
     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __tunable_get_val@GLIBC_PRIVATE (33)
     8: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _dl_find_dso_for_object@GLIBC_PRIVATE (33)
     9: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _dl_starting_up
    10: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __libdl_freeres
    11: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  UND _dl_argv@GLIBC_PRIVATE (33)
    12: 00000000000ab970    33 FUNC    GLOBAL DEFAULT   16 __strspn_c1@GLIBC_2.2.5
    13: 0000000000089260   352 FUNC    GLOBAL DEFAULT   16 putwchar@@GLIBC_2.2.5
    14: 00000000001324f0    20 FUNC    GLOBAL DEFAULT   16 __gethostname_chk@@GLIBC_2.4
    15: 00000000000ab9a0    44 FUNC    GLOBAL DEFAULT   16 __strspn_c2@GLIBC_2.2.5
    16: 000000000014f580   218 FUNC    GLOBAL DEFAULT   16 setrpcent@@GLIBC_2.2.5

可见符号0-11为内部符号。

查找符号

下面以查找符号printf为例,介绍符号查找的过程。

首先使用下面的哈希函数生成符号的32位哈希:

// in elf/dl-lookup.c
static uint_fast32_t
dl_new_hash (const char *s)
{
  uint_fast32_t h = 5381;
  for (unsigned char c = *s; c != '\0'; c = *++s)
    h = h * 33 + c;
  return h & 0xffffffff;
}

得到printf的哈希值为0x156b2bb8。
随后计算布隆滤波器需要的两个hashbit:

unsigned int hashbit1 = new_hash & (__ELF_NATIVE_CLASS - 1);
    unsigned int hashbit2 = ((new_hash >> l->l_gnu_shift) & (__ELF_NATIVE_CLASS - 1));

得到hashbit1 = 56,hashbit2 = 44。
找到该hash对应的bloom word:

  const Elf64_Addr *bitmask = l->l_gnu_bitmask;
    // l->l_gnu_bitmask_idxbits = bitmask_nwords - 1,将取模变为取与
    // (new_hash / __ELF_NATIVE_CLASS) & l->l_gnu_bitmask_idxbits = 174
    Elf64_Addr bitmask_word = bitmask[(new_hash / __ELF_NATIVE_CLASS) & l->l_gnu_bitmask_idxbits];

printf对应的hash在第174个bloom word处,它的值位于bloom word的开始地址0x38b0+174*8=3e20
检查3e20处对应的值:

$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep " 3e20 "

 3e20 d0884a41 c0703429 10ec4303 92003103  ..JA.p4)..C...1.

其bloom word为0x293470c0414a88d0。
将其右移56位:0b0010 1001
将其右移44位:0b10 1001 0011 0100 0111
二者的最后一位均为1,说明布隆滤波器不能拒绝这个哈希值。

这时在对应的哈希桶上进行寻找:

Elf32_Word bucket = l->l_gnu_buckets[new_hash % l->l_nbuckets];

由于0x156b2bb8 % 1011 = 295,需要找到第296个哈希桶。
而哈希桶的起始地址为l_gnu_bitmask + 64 / 32 * bitmask_nwords = 0x40b0,对应哈希桶的地址为0x40b0+295*4=0x454c。
查看0x454c处对应的哈希桶内容:

$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep " 4540 "

 4540 77020000 00000000 7a020000 **7c020000**  w.......z...|...

哈希桶的内容为0x27c。
而l_gnu_chain_zero的地址为:

  l_gnu_chain_zero = l_gnu_buckets + l_nbuckets - symbias;

可计算出l_gnu_chain_zero的地址为0x504c,所以第296个哈希桶包含的真正哈希位于0x504c+27c*4=0x5a3c
查看具体的哈希内容:

$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep " 5a30 " -A 2

 5a30 ade8dbbb 142dcb13 bb86f85f e6952000  .....-....._.. .
 5a40 **b82b6b15** 0a05f1d5 deb6427f 856177fd  .+k.......B..aw.
 5a50 1ae585e7 ec296fa8 1ae585e7 29ce248f  .....)o.....).$.

于0x5a40处找到我们之前计算的哈希0x156b2bb8(注意小端序)。
此时,这个符号在.gnu.hash的下标,就是它在动态符号表中的(下标-symbias)。但由于之前l_gnu_chain_zero已经整体减掉了symbias,所以此处用该符号的地址减掉l_gnu_chain_zero可直接得到符号在符号表中的下标。
0x5a40 - 0x504c = 0x9f4 = 2548,由于一个哈希值为4字节,故下标为2548 / 4 = 637

找到动态符号表的起始地址:

$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep .dynsym -A 1

Contents of section .dynsym:
 07548 00000000 00000000 00000000 00000000  ................

上文中提到,64位ELF文件中一个符号的长度位24字节,故符号在符号表上的起始地址应当为0x7548 + 24*637 = 0xb100
找到动态符号表对应位置的内容:

$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep " 0b0f8 " -A 1

 0b0f8 16000000 00000000 **f3040000** 12001000  ................
 0b108 104e0600 00000000 cc000000 00000000  .N..............

读出符号在字符表上的偏移量为0x4f3。
找到字符表的起始地址:

$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep .dynstr -A 1

Contents of section .dynstr:
 15330 00786472 5f755f6c 6f6e6700 5f5f7763  .xdr_u_long.__wc

起始地址为0x15330,故该符号的地址为0x15330 + 0x4f3 = 0x15823

读出字符表对应位置的值:

$ objdump -s /lib/x86_64-linux-gnu/libc.so.6 | grep " 15820 " -A 1

 15820 494f5f**70** 72696e74 66007265 67697374  IO_printf.regist
 15830 65725f70 72696e74 665f6675 6e637469  er_printf_functi

查找到了符号printf,它是IO_printf的别名,在字符表中为了节省空间将二者合并了。

这样,就完成了一次符号查询的全过程。

以上就是从实例分析ELF格式的.gnu.hash区与glibc的符号查找的详细内容,更多关于ELF格式的.gnu.hash区与glibc的符号查找的资料请关注我们其它相关文章!

(0)

相关推荐

  • 从实例分析ELF格式的.gnu.hash区与glibc的符号查询全过程

    前言 ELF格式的.gnu.hash节在设计上比较复杂,直接从glibc源码进行分析的难度也比较大.今天静下心来看了这篇精彩的文章,终于将布隆滤波器.算数运算转为位运算等一系列细节搞懂了(值得一提的是,这篇博客十分值得花一些时间读懂,它不仅对总体有一个较好的描述,而且还涉及了许多有益的实现细节).但本人愚钝异常,没有一个完整的walkthrough就不能觉得自己真的搞懂了一个东西.所以本文从查找一个符号的真实情况出发,把ELF格式是如何组织一个符号,以及动态链接器如何读取并处理这些信息以进行符号

  • Android 图片的三级缓存机制实例分析

    Android 图片的三级缓存机制实例分析 当我们获取图片的时候,如果不加以协调好图片的缓存,就会造成大流量,费流量应用,用户体验不好,影响后期发展.为此,我特地分享Android图片的三级缓存机制之从网络中获取图片,来优化应用,具体分三步进行: (1)从缓存中获取图片 (2)从本地的缓存目录中获取图片,并且获取到之后,放到缓存中 (3)从网络去下载图片,下载完成之后,保存到本地和放到缓存中 很好的协调这三层图片缓存就可以大幅度提升应用的性能和用户体验. 快速实现三级缓存的工具类ImageCac

  • JSON字符串和对象相互转换实例分析

    本文实例分析了JSON字符串和对象相互转换方法.分享给大家供大家参考,具体如下: 同事问了我一个问题--server端返回了一个json结构的字符串,怎么样去访问json对象里面的值?jquery有没有对返回的JSON数据进行解析? 我自己写了一个小的demo,还有从网上查了一些资料,在这里跟大家分享一下 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/T

  • Jackson的用法实例分析

    通俗的来说,Jackson是一个 Java 用来处理 JSON 格式数据的类库,其性能非常好.本文就来针对Jackson的用法做一个较为详细的实例分析.具体如下: 一.简介 Jackson具有比较高的序列化和反序列化效率,据测试,无论是哪种形式的转换,Jackson > Gson > Json-lib,而且Jackson的处理能力甚至高出Json-lib近10倍左右,且正确性也十分高.相比之下,Json-lib似乎已经停止更新,最新的版本也是基于JDK15,而Jackson的社区则较为活跃.

  • Yii2中Restful API原理实例分析

    本文实例分析了Yii2中Restful API原理.分享给大家供大家参考,具体如下: Yii2 有个很重要的特性是对 Restful API的默认支持, 通过短短的几个配置就可以实现简单的对现有Model的RESTful API 这里通过分析rest部分源码,简单剖析下yii2 实现 restful 的原理,并通过一些定制实现 对 关联模型的RESTful api 操作. ~ 代表 extends from 的关系 | | rest/ | | |-Action.php ~ `\yii\base\

  • PHP文件缓存smarty模板应用实例分析

    本文实例分析了PHP文件缓存smarty模板应用.分享给大家供大家参考,具体如下: 一.使用缓存 要开启smarty的缓存,只需将caching设为true,并指定cache_dir即可. 使用cache_lefetime指定缓存生存时间,单位为秒 要对相同页面生成多个不同的缓存,在display或fetch中加入第二参数cache_id,如: $smarty->display('index.tpl',$my_cache_id); 此特性可用于对不同的$_GET进行不同的缓存   二.清除缓存

  • CodeIgniter配置之routes.php用法实例分析

    本文实例分析了CodeIgniter配置之routes.php用法.分享给大家供大家参考,具体如下: application/config/routes.php中定义了一个名为$route的数组,用来设置默认路由和404页面以及可以设置一些匹配方式. 默认的配置如下: $route['default_controller'] = "welcome"; $route['404_override'] = ''; default_controller指定默认的控制器名称,404_overri

  • C#静态方法与非静态方法实例分析

    本文实例分析了C#静态方法与非静态方法,并对其用法进行了较为全面的分析.分享给大家供大家参考.具体分析如下: 通常来说,C#的类中可以包含两种方法:静态方法和非静态方法. 使用了static 修饰符的方法为静态方法,反之则是非静态方法. 静态方法是一种特殊的成员方法,它不属于类的某一个具体的实例,而是属于类本身.所以对静态方法不需要首先创建一个类的实例,而是采用 类名.静态方法 的格式 . 1)static方法是类中的一个成员方法,属于整个类,即不用创建任何对象也可以直接调用. static内部

  • python开发之str.format()用法实例分析

    本文实例分析了python开发之str.format()用法.分享给大家供大家参考,具体如下: 格式化一个字符串的输出结果,我们在很多地方都可以看到,如:c/c++中都有见过 下面看看python中的字符串格式函数str.format(): #使用str.format()函数 #使用'{}'占位符 print('I\'m {},{}'.format('Hongten','Welcome to my space!')) print('#' * 40) #也可以使用'{0}','{1}'形式的占位符

  • mysql分区功能详解,以及实例分析

    一,什么是数据库分区 前段时间写过一篇关于mysql分表的 的文章,下面来说一下什么是数据库分区,以mysql为例.mysql数据库中的数据是以文件的形势存在磁盘上的,默认放在/mysql/data下面 (可以通过my.cnf中的datadir来查看),一张表主要对应着三个文件,一个是frm存放表结构的,一个是myd存放表数据的,一个是myi存表 索引的.如果一张表的数据量太大的话,那么myd,myi就会变的很大,查找数据就会变的很慢,这个时候我们可以利用mysql的分区功能,在物理上将这 一张

随机推荐