Colderleo's blog Colderleo's blog
首页
Linux
C++
Python
前端
工具软件
mysql
索引
关于
GitHub (opens new window)

Colder Leo

热爱代码的打工人
首页
Linux
C++
Python
前端
工具软件
mysql
索引
关于
GitHub (opens new window)
  • 常见程序性能开销cost、latency延迟耗时的经验值
  • 面试常见问题
  • 静态链接-动态链接-elf详解-elfloader
    • ELF Format文档
    • ELF装载过程
    • elf结构,段(segment)和节(section)
    • 静态链接
    • 动态链接
    • so全局变量覆盖(全局符号介入)
    • 编写loadelf加载器
    • 获取elf中symbol的版本,并通过dlvsym加载
    • stdlibc++源码下载和编译安装
    • 其他参考文章
    • 命令
    • double free
    • 修改elf动态符号表dynsym
  • 动态库和静态库的依赖问题
  • glibc和ld的编译调试-为某程序单独设置ld
  • dl_iterate_phdr遍历linkmap头、获取so加载地址
  • shell、bash语法和脚本模板
  • so文件查找路径
  • 逻辑地址-线性地址or虚拟地址-物理地址
  • 通过ELF读取当前进程的符号表并获取符号的版本信息
  • 虚拟内存,cache,TLB,页表
  • 用户内存空间分布和mmap
  • numa网卡绑定
  • 隔核绑核、服务器优化
  • popen底层原理和仿照实现-execl
  • tmux用法
  • ASLR机制
  • 程序后台运行、恢复前台nohup
  • 大页内存huge_page
  • 用perf查看page-fault
  • Bash设置显示全部路径
  • 查看socket fd状态,设置nonblock
  • cout输出到屏幕的过程
  • 多进程写同一文件-write原子性-log日志
  • vim用法
  • epoll用法
  • signal信号、软中断、硬中断、alarm
  • 内核模块
  • 读写锁之pthread_rwlock和内核rwlock自旋读写锁
  • systemtap
  • xargs、awk用法
  • openssl libssl.so.10.so缺失问题
  • netstat用法
  • fork函数
  • tcp延迟确认ack
  • 90.centos7上一次std-string编译错误定位
  • docker用法
  • find用法
  • dmesg
  • gcc编译用法
  • avx-sse切换惩罚
  • Centos7防火墙
  • chmod用法
  • kernel-devel安装版本
  • Linux-Centos7系统安装、网络设置、常见报错
  • linux下g++编译c++文件
  • MegaCli 安装及使用
  • mysql
  • mysql忘记密码修改方法
  • set用法
  • crontab
  • ssh传文件scp
  • ssh连接
  • tcpdump、tshark、tcpreplay用法
  • ubantu root登录以及创建新用户
  • ubuntu安装g++和gdb
  • uClibc编译失败解决方法
  • win10安装WSL open-ssh
  • yum升级git
  • 比较so文件版本-md5sum
  • 查看磁盘信息
  • 合并两个硬盘,挂载到一个文件夹下
  • 软件安装目录usr-local-src
  • 下载centos历史版本
  • sh脚本转可执行文件、加密
  • Linux
gaoliu
2021-10-06
目录

静态链接-动态链接-elf详解-elfloader

# ELF Format文档

https://uclibc.org/docs/elf-64-gen.pdf (opens new window)

关于elf格式的各个细节基本都可以在这个文档中找到,很方便。

# ELF装载过程

https://www.cnblogs.com/fellow1988/p/6166271.html (opens new window)

当我们在linux bash下执行ELF程序时,Linux系统是怎样装载和执行的呢?

1.bash进程fork出子进程

2.在bash的子进程中调用execve系统调用来执行指定的ELF。

3.execve系统调用的入口是sys_execve,在sys_execve会调用do_execve

4.在do_execve中会读取可执行文件的前128个字节。这128个字节用来判断可执行文件是哪种类型。

5.do_execve读取了128个字节的文件头后,调用serch_binary_handle去搜索匹配合适的可执行文件装载处理过程。search_binary_handle会通过判断文件头部的魔数确定文件的格式。ELF的装载处理过程是load_elf_binary.

6.在load_elf_binary中,检查文件格式的有效性后,寻找动态链接的.interp 段。根据ELF可执行文件的program headers,建立可执行文件和虚拟内存的映射。将execev系统调用的返回地址改成ELF可执行文件的入口地址。当execev返回时就可执行ELF文件。

# elf结构,段(segment)和节(section)

  • 参考:https://www.cnblogs.com/jiqingwu/p/elf_format_research_01.html (opens new window)

  • section(节)是编译时产生的节,每个目标文件(.o文件)都有很多小节。很多时候section也被称为段。

  • segment(段)是静态链接时,把各个目标文件中具有类似属性的节放在一起形成段。程序运行时按照段加载进内存。比如把所有代码(.text)的节放在一起形成一个段,这个段是可读可执行的。把所有.data和.bss节放在一起形成一个段,这个段是可读可写的。

  • 常用指令:readelf、objdump、cat /proc//maps

程序头中包含指向程序头表的指针。程序头表即segment表。

通过readelf查看某个可执行elf文件的程序头表:

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x0000000000000268 0x0000000000000268  R      0x8
  INTERP         0x00000000000002a8 0x00000000000002a8 0x00000000000002a8
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000560 0x0000000000000560  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x00000000000001d5 0x00000000000001d5  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x0000000000000110 0x0000000000000110  R      0x1000
  LOAD           0x0000000000002de8 0x0000000000003de8 0x0000000000003de8
                 0x0000000000000248 0x0000000000000250  RW     0x1000
  DYNAMIC        0x0000000000002df8 0x0000000000003df8 0x0000000000003df8
                 0x00000000000001e0 0x00000000000001e0  RW     0x8
  NOTE           0x00000000000002c4 0x00000000000002c4 0x00000000000002c4
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_EH_FRAME   0x0000000000002004 0x0000000000002004 0x0000000000002004
                 0x0000000000000034 0x0000000000000034  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000002de8 0x0000000000003de8 0x0000000000003de8
                 0x0000000000000218 0x0000000000000218  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
   03     .init .plt .text .fini
   04     .rodata .eh_frame_hdr .eh_frame
   05     .init_array .fini_array .dynamic .got .got.plt .data .bss
   06     .dynamic
   07     .note.ABI-tag .note.gnu.build-id
   08     .eh_frame_hdr
   09
   10     .init_array .fini_array .dynamic .got
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  • 如上所示,文件中共有11个segment,每个segment里有很多section,比如03这个segment,里面包含了03 .init .plt .text .fini四个section。它的Flags是 R(只读)E(可执行),.text是代码节(section,但是我们通常称之为代码段)。

  • 再比如04这个段,Type为LOAD,表示需要加载进内存,标志为R(只读),它包含的section有04 .rodata .eh_frame_hdr .eh_frame,其中rodata是read only data,表示只读的数据,比如程序中用到的字符串常量等。

  • 再比如05这个LOAD段,标志RW(可读写),它包含的节是05 .init_array .fini_array .dynamic .got .got.plt .data .bss,可以看到.data和.bss都包含其中,这段是数据段无疑。

  • 只有类型为LOAD的段是运行时真正需要的,会加载进内存里。 DYNAMIC节记录了所需要的动态链接库的信息.

# 静态链接

# 编译出可执行文件的4个步骤

  • 程序源代码经过预处理(Preprocessing, 展开宏定义,纳入头文件)、编译(Compliation)、汇编(Assembly)、链接(Linking) 四个步骤得到可执行文件。

  • 前三个步骤关联性比较强,可以看做一个步骤,得到.o文件(object),俗称目标文件。目标文件和可执行文件都是elf格式。

  • 多个目标文件经过静态链接,可以得到可执行文件(动态链接先不考虑)。静态链接库.a文件(archive)其实就是很多目标文件的合集。

# 静态链接-符号决议

  • 符号:每个目标文件都有符号表,符号主要是变量名和函数名,在程序执行时就是变量和函数的地址。编译的时候为了防止符号冲突,有时候会改变符号名,这就是符号修饰。对于C语言,Windows一般会在前面加上_,Linux以前会,现在默认不加。C++通常会把符号名改的很长。可以通过c++filt对修饰过的符号进行还原。
  • 弱符号和强符号:函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。我们可以通过GCC的__attribute__((wake))来定义任何一个强符号为弱符号。对外部变量的引用如extern int a;不是强符号也不是弱符号。
  • 符号决议:对于多个目标文件中相同名字的符号(比如多个目标文件都定义了变量a),只能选择一个作为其定义(或者说选择它的地址)。选择规则为: 1. 如果有多个同名的强符号,则报错。2. 如果一个符号在某个目标文件中为强符号,其他目标文件中为弱符号,则选择强符号作为其定义。 3. 如果一个符号在多个目标文件中都是弱符号,则选择占用空间最大的那个。
  • 弱引用和强引用:GCC的__attribute__(())可以定义弱引用。如果是强引用,找不到会报错。弱引用找不到不会报错。这种规则有一个好处,比如库中定义的弱符号可以被用户定义的强符号所覆盖。也可以把扩展模块作为弱引用,有没有都可以正常运行。

# 静态链接-重定位

  • 重定位的概念:编译生成的目标文件中各个符号的地址是不确定的。静态链接时,所有的目标文件被组织成一个整体的可执行文件,此时所有符号的地址才可以确定(除了动态链接的符号),找到这些符号的实际地址,将其被引用的地方修正,这个过程就是重定位。

  • 在目标文件(模块)中,代码中引用符号需要确定其地址,有以下几种情况:

    • 如果被引用符号是内部的,相对引用,那么它不管实际地址在哪都能正常工作,不需要重定位;
    • 如果被引用符号是的内部的,绝对引用,需要根据本模块被链接后的起始地址进行重定位;
    • 如果一个符号是外部的,绝对引用,需要根据外部符号被链接的地址进行重定位;
    • 如果一个符号是外部的,相对引用,需要根据外部符号的地址和本模块被链接后的地址进行重定位;
  • 重定位表:链接器怎么知道哪些符号需要被重定位呢?就是目标文件中有一个重定位表,表里面记录着需要重定位的项。重定位表实际上是section,如果.text段需要重定位,它对应的重定位表就存在.rel.text段;(这里的段是指section)如果.data段需要重定位,其对应的重定位表就存在.rel.data段。

    注意这些静态重定位表的段在生成可执行文件后就不存在了

  • 符号表:目标文件中包含.symtab段,即为符号表。通过符号表可以看出哪些符号是外部的。利用readelf命令查看目标文件的.symtab段,其中‘UND’的(undefined),就是该目标文件中没有定义的,重定位表中应该包含它们,静态链接的时候需要在其他目标文件中寻找。

# 动态链接

# 动态库so之位置无关代码fPIC

​ 动态库被加载到内存中的地址是随机的,因此其内部的代码必须是位置无关当的。当so中的代码要访问全局符号时,需要找到该符号的地址,不管该符号是so内部的还是外部的。具体寻找方法是通过相对地址找到自身的got表,got表中存放了全局符号在运行时的实际地址。

​ 动态链接器ld在加载so时,会更新got表,因此代码可以正常运行。更新got表的过程就是relocate。

# 动态链接

  • 静态链接的缺点:静态链接是将静态库文件包含到生成的可执行文件中,会浪费磁盘空间,且当某个目标文件改动时,需要重新编译,不方便。另外运行时,可执行文件要加载进内存,多个可执行文件一起执行时,如果它们包含同一个静态库时,内存中会重复加载,浪费内存,且降低cache命中率。

  • 动态链接:上面静态链接,是在链接时重定位,动态链接即在装载时重定位。程序编译时,需要某个库(so动态库),此时不把该库文件编译成自身文件的一部分,而是作为引用。当程序运行装载时,再把需要的so映射到自身的虚拟空间中(此时so可能已经被其他程序加载到内存了)。因为so是运行时映射的,且地址不确定,所以so编译时并不知道自身地址。实际加载该so时,其地址确定了,动态链接器ld会将引用信息更新。这个过程就是装载时重定位(relocate)。

  • 动态链接-重定位表:.rel.dyn/.rela.dyn和.rel.plt/.rela.plt分别对应静态链接的重定位表中的.rel.text和.rel.data,即函数重定位和变量重定位。(静态链接重定位表存在于目标文件中,而动态链接重定位表位于可执行文件中和so中)。

    其中.rel.dyn是对数据引用的修正,它修正的是.got; .rel.plt是对函数引用的修正,它修正的是.got.plt。

  • 动态链接-符号表, .dynsym,是一个结构体数组,结构体为Elf32_Sym。该结构体包括st_name、st_value等成员。st_name保存着动态符号在 .dynstr 表(动态字符串表)中的偏移,st_value,如果这个符号被导出,这个符号保存着对应的虚拟地址。

# elf中各个section的含义(可执行文件和so中的)

section有名字和type,一般名字和type是对应的,但是实际运行过程中起作用的是type,名字只是为了方便人们理解,改了也不影响。

参考:http://www.360doc.com/content/17/1204/19/7377734_709907822.shtml (opens new window)

  • 下面是动态符号表相关的几个section的包含关系(可以在逻辑上这样理解,并不是在文件中包含)

    • symtab > dynsym > rela.dyn 和 rela.plt (大于号)
    • .symtab,动态符号表,包含全局符号和局部符号,以及大量linker、debugger需要的数据,在链接和debug时有用,但并不为runtime必需,可以被strip去掉。类型为SHT_SYMTAB。
    • .dynsym,动态链接符号表,它是.symtab的一个子集,只包含全局符号。记录了符号的导入导出信息。其中需要重定位的符号会记录在.rela.dyn 和 .rela.plt中。类型为SHT_DYNSYM。
    • .rela.dyn 和 .rela.plt ,重定位表,用于对上面的.dynsym中的符号重定位,重定位的结果会刷新got和got.plt。类型为SHT_RELA。
  • .dynmic 包含了该elf需要哪些so,一些初始化函数和结束函数等。(初始化函数要先执行,可能有很多,执行完后再执行elf程序头的ehdr->e_entry入口地址。)

  • .dynstr 用于辅助.dynsym,动态符号表的字符串表。在.dynsym中的字符串实际上都是索引,真正的字符串要在这里取。类型为SHT_STRTAB。

    • .strtab用于辅助.symtab,里面包含了调试等相关的符号信息,非runtime必须。
    • .shstrtab是记录Section名称的字符串;
  • .gnu.version: .dynsym是动态符号表,但是其中不含库版本信息,其对应的符号在.gnu.version中,二者所含的entries数量是一样的,一一对应。.gnu.version中包含的版本信息是数字,比如1、3、5、6、10等,一般都很小,是index索引。通过该索引可以在.gnu.version_r中找到一个offset,再在strtab中通过该offset即可得到符号版本字符串,比如“GLIBCXX_3.4”

    • .gnu.version的type=SHT_GNU_versym;
    • .gnu.version_r是多个hash表组成的list, 每个hash表对应一个库,组合在一起成为一个大的hash表。其type=SHT_GNU_verneed;
    • 一个修改version的例子:https://blog.csdn.net/Mr_HHH/article/details/83346629
  • section header各字段的具体含义:https://www.cnblogs.com/altc/p/8991730.html (opens new window)

# 延迟重定位 PLT和GOT

如果某个共享库中有很多符号,而程序只引用了很少几个,那么就没必要在加载该共享库时就把里面所有符号解析了。延迟重定位就是在用到该符号时才解析。(延迟重定位应该只能是函数重定位,没有变量重定位)

  • .plt段和.got.plt段

    • .plt段保存了解析符号的指令代码,和代码段放在一起,可读可执行。其内容是一个数组,每个符号对应plt数组的一个条目。plt[0]比较特殊,它是jump到动态链接器中。(实际上是jump到got[2],got[2]保存的就是动态链接器的地址。)

    • .got.plt段保存函数的地址,和数据段放在一起,可读写(.got.plt可以理解为另一个.got,只不过.got保存的是变量的地址,.got.plt保存的是函数的引用地址。后面.got.plt就也称为.got); .got的内容也是一个数组,每个符号对应一个条目。got前三条比较特殊,got[0]: addr of .dynmic,got[1]: addr of reloc entries,got[2]: 动态链接器在ld-linux.so模块的入口点。

    • 符号的id或者说index从1开,plt[1]对应got[3],plt[2]对应got[4],以此类推。

  • 具体延迟重定位的逻辑

    • 一句话总结:第一次访问func@plt时会跳到对应的func@got再跳回来,更新func@got的值为func实际地址;第二次访问plt时直接跳到func实际地址。
    • 当访问某个函数时,假设其id为2,访问时会访问plt[2]。plt[2]中有两条指令(每个plt条目都是两条指令)。plt[2]第一条指令把函数符号的id 2压入栈中,然后jump到对应的got[4]保存的地址,got[4]最开始保存了它对应的plt[2]的第二条指令的地址,也就是说plt[2]的第一条指令执行时会跳到plt[2]的第二条指令。
    • 第二条指令跳转到plt[0]继续执行,plt[0]将addr of reloc entries(保存在got[1]中)压入栈中,然后跳转到动态链接器(got[2]保存的地址)。
    • 跳转到动态链接器后,动态链接器根据栈中的两个参数来确定该符号的实际地址,然后用这个地址重写got[4],再跳转到该符号的地址处执行,也就是执行了该函数。
    • 当第二次访问该函数时,仍然访问plt[2],plt[2]仍然jump到got[4]保存的地址,不过此时got[4]保存的地址已经是函数的实际地址了。

如果把环境变量LD_BIND_NOW设置成一个非空值,所有的重定位操作都会在程序启动时进行。也可以在链接器命令行通过使用-z now链接器选项使延迟绑定对某个特定的共享库失效。此时除非重新链接该共享库,否则对该共享库的这种设置会一直有效。

.plt段一般跟代码段合并到一个segment,可读可执行。plt段保存了需要引入的符号,访问plt时会跳转到got中。第一次访问plt某个项(某个符号),会执行一段导入函数,调用_dl_runtime_resolve()将符号的地址解析出来,然后覆盖got表,并返回该符号的地址;第二次访问时plt的该符号时,对应got表项已经被覆盖成了符号地址,直接取出该地址返回。

# VMA,Virtual Memory Area

进程的虚拟内存空间会被分成不同的若干区域,每个区域都有其相关的属性和用途,每一个虚拟内存区域都由一个相关的 struct vm_area_struct 结构来描述,一个进程有多个VMA,每个 VMA 块都会存入mmap链表和mm_rb红黑树中,存两个地方。

# so全局变量覆盖(全局符号介入)

from: 关于动态链接中的全局变量,https://blog.csdn.net/gerryke/article/details/44083581 (opens new window)

全局变量链接规则:

  • a. 共享对象在编译时不能假设自己在进程虚拟地址空间中的位置。

  • b. 共享库在编译时,默认都把定义在模块内部的全局变量,当做定义在其他模块的全局变量。

  • c. 当共享模块被装载时,如果某个全局变量在可执行程序中拥有副本,那么动态链接器(在我的测试环境是/lib64/ld-linux-x86-64.so.2)就会把全局偏移表(GOT Global Offset Table)中的相应地址指向该副本,这样该变量在运行时实际上最终就只有一个实例(第一个例子验证)。如果变量在共享库中被初始化,动态链接器还要将该初始化值复制到程序主模块中的变量副本;如果该全局变量在程序主模块中没有副本,那么GOT的相应地址就指向模块内部的该变量副本。(第二个例子验证)

# 第一个例子:(主函数中的变量覆盖so中的变量)

有三个程序:main.cpp,so1.cpp,so2.cpp

main.cpp

#include <stdio.h>

extern int global_symbol;
//int global_symbol = 300;


extern void testso1();
extern void testso2();

int main()
{
    global_symbol++;
    printf("main: the value of global symbol is %d\n", global_symbol);
    printf("main: &glbal_symbol=%p\n\n", &global_symbol);

    testso1();
    printf("main: the value of global symbol is %d\n\n", ++global_symbol);

    testso2();
    printf("main: the value of global symbol is %d\n", ++global_symbol);
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

so1.cpp

#include <stdio.h>

int global_symbol = 500;

void testso1()
{
        printf("so1: the value of global symbol is %d\n", ++global_symbol);
        printf("so1: &glbal_symbol=%p\n", &global_symbol);
}
1
2
3
4
5
6
7
8
9

so2.cpp

#include <stdio.h>

int global_symbol = 900;

void testso2()
{
        printf("so2: the value of global symbol is %d\n", ++global_symbol);
        printf("so2: &glbal_symbol=%p\n", &global_symbol);
}
1
2
3
4
5
6
7
8
9

编译 build.sh:

#compile
g++ -c main.cpp -o main.o
g++ -shared -fPIC -o so1.so so1.cpp
g++ -shared -fPIC -o so2.so so2.cpp

# link
g++ -o a.out main.o so2.so so1.so #只要定义了int global_symbol; 那么这里main.o不论在前面还是在后面,结果都是300
1
2
3
4
5
6
7

执行a.out:

$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
$ ./a.out

the value of global symbol is 301  #main中初始化的300起作用了
the value of global symbol in so1 is 302  #so1和so2中的初始化不起作用
the value of global symbol is 303  
the value of global symbol in so2 is 304  
the value of global symbol is 305  
&glbal_symbol=0x601048
1
2
3
4
5
6
7
8
9

总结:

在动态库中定义的全局变量,其初始化的值并未生效,而是被主程序覆盖。

# 第二个例子:主函数中使用so内的变量

将main.cpp中的int global_symbol = 300; 改为extern int global_symbol;(即只做声明)

则输出的结果取决于链接的顺序。

如果链接的命令是

g++ main.o so1.so so2.so  
1

则输出为:

the value of global symbol is 501  # 因为main中只做了声明,所以so1中全局变量的初始化=500起了作用。
the value of global symbol in so1 is 502  # so1链接命令中比so2靠前,导致so1的初始化起作用,so2不起作用
the value of global symbol is 503  
the value of global symbol in so2 is 504  
the value of global symbol is 505  
&glbal_symbol=0x601044 
1
2
3
4
5
6

如果链接的命令是

g++ main.o so2.so so1.so  
1

则输出为:

the value of global symbol is 901  # so2链接时在so1的前面,所以so2起作用。
the value of global symbol in so1 is 902  
the value of global symbol is 903  
the value of global symbol in so2 is 904  
the value of global symbol is 905
&glbal_symbol=0x601044
1
2
3
4
5
6

# 从elf文件分析上述符号relocate的生效逻辑

(注:centos下测试正常,ubuntu下需对ld的链接脚本做一点修改,详见下一小节。)

对于第二个例子(使用so中的全局变量),so2.so在so1.so前面的情况,上面输出的&glbal_symbol=0x601044,这个地址是a.out在编译完成就后确定的,我们可以通过readelf -a a.out查看,其存在于.dynsym段。

$ readelf -a a.out 

Dynamic section at offset 0xd98 contains 29 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [so2.so]   --so2在前面
 0x0000000000000001 (NEEDED)             Shared library: [so1.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]


Symbol table '.dynsym' contains 12 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _Z7testso1v   ---这里是testso1()
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _Z7testso2v   ---这里是testso2()
     6: 0000000000601050     0 NOTYPE  GLOBAL DEFAULT   25 _end
     7: 0000000000601044     0 NOTYPE  GLOBAL DEFAULT   24 _edata
     8: 0000000000601044     4 OBJECT  GLOBAL DEFAULT   25 global_symbol   ---在这里
     9: 0000000000601044     0 NOTYPE  GLOBAL DEFAULT   25 __bss_start
    10: 0000000000400598     0 FUNC    GLOBAL DEFAULT   11 _init
    11: 0000000000400824     0 FUNC    GLOBAL DEFAULT   14 _fini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

.dynsym是动态链接符号的导入导出表,其中UND表示未定义的符号,是外部so中的符号。有定义的、value有值的,说明是输出的符号,在自身中已经定义。ld在加载so1.so或者so2.so时,将so自身got表中的golbal_symbol的地址修正成了a.out中输出的0x601044,并且将自身初始化的值拷贝了过去。

另外注意readelf的 so列表里,so2.so排在so1.so前面。我们通过patchelf工具(可在GitHub上下载)将其做一些调整,让so1排在so前面,然后执行a.out,会发现输出结果又变成了501

$ patchelf --remove-needed so1.so a.out
$ patchelf --add-needed so1.so a.out
$ readelf -d a.out

Dynamic section at offset 0x3000 contains 29 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [so1.so]  ---调整后so1排在前面了。
 0x0000000000000001 (NEEDED)             Shared library: [so2.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

$ ./a.out
the value of global symbol is 501
...
1
2
3
4
5
6
7
8
9
10
11
12
13

我们知道.dynsym中的符号有很多需要做relocate,需要relocate的变量信息存在'.rela.dyn'里,需要relocate的函数信息存在'.rela.plt'里。下面就是当我们定义extern int global_symbol;时这两个表的情况。

Relocation section '.rela.dyn' at offset 0x4f0 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000600ff8  000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000601044  000800000005 R_X86_64_COPY     0000000000601044 global_symbol + 0   ------这一行

Relocation section '.rela.plt' at offset 0x520 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000601018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 _Z7testso1v + 0
000000601020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601028  000300000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0
000000601030  000400000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000601038  000500000007 R_X86_64_JUMP_SLO 0000000000000000 _Z7testso2v + 0
1
2
3
4
5
6
7
8
9
10
11
12

如果是定义int global_symbol; 则上面的'.rela.dyn'中的global_symbol就会没有了,不需要relocate了。

# centos和ubuntu ld默认链接脚本的差异

上一小节的测试在centos下正常,在ubuntu下发现主程序符号表中虽然指定了 global_symbol 的地址为 0x601044, 但是实际运行时其地址是一个比较大的值,并不是 0x601044。 而将centos上生成的程序拷贝到ubuntu下,发现地址仍为0x601044,因而猜测是ld的默认链接脚本导致了该差异。分别在centos和ubuntu上导出ld的默认链接脚本:

ld --verbose
1

对比二者仅在.glt部分有差异:

ubuntu:

  .plt            : { *(.plt) *(.iplt) }
.plt.got        : { *(.plt.got) }
.plt.sec        : { *(.plt.sec) }
1
2
3

centos:

  .plt            : { *(.plt) *(.iplt) }
.plt.got        : { *(.plt.got) }
.plt.bnd        : { *(.plt.bnd) }
1
2
3

经测试在ubuntu上不论使用其自身导出的链接脚本,还是使用centos导出的链接脚本,主程序中动态符号表定义的global_symbol地址都会生效,而不使用链接脚本时,地址不会生效。

# 从glibc源码分析relocate顺序

  • elf的解释器是动态链接器ld-xxx.so,动态链接器通过dl_main()函数加载程序。首先加载各个object,包括主程序和各个动态库。

  • 加载完成后,对各个object进行倒序重定位。 glibc-2.18/elf/rtld.c: line 2162, dl_main():

/* Now we have all the objects loaded.  Relocate them all except for  the dynamic linker itself.  We do this in reverse order so that copy  relocs of earlier objects overwrite the data written by later  objects.  ... */
1

这是dl_main()接近结尾部分的一段注释:在所有object加载后进行倒序relocate,因为这样可以使前面object的copy类型的relocate可以覆盖后面的object。

  • 符号重定位时会在已加载的object中查找,并更新got/plt.got表。在glibc-2.18/elf/dl-lookup.c: do_lookup_x() 这个函数里查找。每个object对应一个link_map, link_map->l_scope 是符号查找域,顺序查找,也就是说先加载的object中的符号优先生效。

  • 如果编译时指定了 -Wl,-Bsymbolic,则会在elf中添加DT_SYMBOLIC标志, glibc-2.18/elf/dl-load.c, line 1570: _dl_map_object_from_fd() 中如果发现定义了此标志,则将该object自身作为l_symbolic_searchlist的第一个,并将其置为l->l_scope[0]。(实测添加了-Wl,-Bsymbolic并没有生成DT_SYMBOLIC标志,也没有本地符号优先生效,可能是我的编译方式有问题。)

# 编写loadelf加载器

# 相关知识:

  • elf文件执行原理:https://blog.csdn.net/ljy1988123/article/details/50404642 (opens new window)
  • linux下实现在程序运行时的函数替换(热补丁):https://www.cnblogs.com/leo0000/p/5632642.html (opens new window)

# 获取elf中symbol的版本,并通过dlvsym加载

# elf中symbol的版本号

  • .dynsym中包含了动态符号表,但是中不含版本信息,其对应的符号在.gnu.version中,二者所含的entries数量是一样的,一一对应。
  • .gnu.version中包含的版本信息是数字类型的index,比如1、3、5、6、10等,一般都很小。.gnu.version_r是一个hash表,通过该index获取一个offset
  • 在strtab中,用该offset得到version_name字符串,比如“GLIBCXX_3.4”

# 参考文章

https://blog.csdn.net/modisir/article/details/117958192 (opens new window)

下面是复制的内容:

Linux 采用 ELF 作为其可链接可执行文件的格式,并提供诸如 nm 之类的工具进行 ELF 符号表的解析。如下例程(vim test.cc):

#include <iostream>
#include <pthread.h>

int main()
{
  pthread_cond_t cond;
  pthread_condattr_t attr;

  pthread_condattr_init(&attr);
  pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);

  int ret = pthread_cond_init(&cond, &attr);

  if (ret != 0) {
    std::cout << "call_pthread_cond_init failed." << std::endl;
    return ret;
  }

  pthread_cond_destroy(&cond);

  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

我们进行编译,并通过 nm 查看可执行文件的符号表:

[Linux] $ g++ test.cc -lpthread
[Linux] $ nm -g a.out
0000000000400b88 R _IO_stdin_used
                 w _Jv_RegisterClasses
                 U _ZNSolsEPFRSoS_E@@GLIBCXX_3.4
                 U _ZNSt8ios_base4InitC1Ev@@GLIBCXX_3.4
                 U _ZNSt8ios_base4InitD1Ev@@GLIBCXX_3.4
00000000006012a0 B _ZSt4cout@@GLIBCXX_3.4
                 U _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@@GLIBCXX_3.4
                 U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@@GLIBCXX_3.4
0000000000601020 D __DTOR_END__
0000000000601284 A __bss_start
                 U __cxa_atexit@@GLIBC_2.2.5
0000000000601280 D __data_start
0000000000400b90 R __dso_handle
                 w __gmon_start__
                 U __gxx_personality_v0@@CXXABI_1.3
0000000000400aa0 T __libc_csu_fini
0000000000400ab0 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
0000000000601284 A _edata
00000000006013c8 A _end
0000000000400b78 T _fini
0000000000400808 T _init
00000000004008f0 T _start
0000000000601280 W data_start
00000000004009d4 T main
                 U pthread_cond_destroy@@GLIBC_2.3.2
                 U pthread_cond_init@@GLIBC_2.3.2
                 U pthread_condattr_init@@GLIBC_2.2.5
                 U pthread_condattr_setclock@@GLIBC_2.3.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

可以看到,glibc 库函数后面都有一个版本信息,这就又牵扯到了 symbol versioning 机制。1995年,Solaris 的 link editor 和 ld.so 引入了 symbol versioning 机制。知乎作者 MaskRay 的这篇文章对 GNU 的 symbol versioning 机制做了比较详细的描述,可以参考:All about symbol versioning - MaskRay的文章 - 知乎 (opens new window)。此外,我们还可以从 ELF 的 man page 中找到对 symbol versioning entry 的描述。

我们以上面例程中引用的一个 glibc 函数为例,编译时,pthread_cond_init 这个函数使用的版本是GLIBC_2.3.2。而实际上,pthread 库中,pthread_cond_init 这个函数存在两个版本:

[Linux] $ nm -g /lib64/libpthread.so.0 | grep pthread_cond_init
000000390280b0b0 T pthread_cond_init@@GLIBC_2.3.2
000000390280c030 T pthread_cond_init@GLIBC_2.2.5
1
2
3

除了引用头文件 #include <pthread.h> 并显式调用库函数 pthread_cond_init 以外,glibc 还提供了 dlsym 函数,可以从 libpthread.so 中取得 pthread_cond_init 的指针:

#include <pthread.h>
#include <dlfcn.h>
#include <iostream>

typedef int (*cond_init_func_t)(pthread_cond_t *cond,
                                const pthread_condattr_t *attr);

extern "C" {

int pthread_cond_init(pthread_cond_t *cond,
                      const pthread_condattr_t *attr)
{
  return 0;
}

}

static int call_pthread_cond_init(pthread_cond_t *cond,
                                  pthread_condattr_t *attr)
{
  cond_init_func_t func =
      (cond_init_func_t) dlsym(RTLD_NEXT, "pthread_cond_init");
  return func(cond, attr);
}

int main()
{
  pthread_cond_t cond;
  pthread_condattr_t attr;

  pthread_condattr_init(&attr);
  pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);

  int ret = call_pthread_cond_init(&cond, &attr);

  if (ret != 0) {
    std::cout << "call_pthread_cond_init failed." << std::endl;
    return ret;
  }

  pthread_cond_destroy(&cond);

  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

如上,我们修改一下之前的例程,一方面,override 库函数 pthread_cond_init(glibc 库函数一般都声明为 weak symbol,用同名函数就可以 override);另一方面,使用 dlsym 获取 glibc 原生的 pthread_cond_init 函数指针,并调用它。

我们的预期是,call_pthread_cond_init 将会获得 pthread_cond_init@@GLIBC_2.3.2 的指针,并正常执行。但实际情况是怎样的呢?我们编译运行一下:

[Linux] $ g++ test.cc -lpthread -ldl
[Linux] $ ./a.out
call_pthread_cond_init failed.
1
2
3

很显然,这并不符合我们的预期。因为,实际上 dlsym 获取到的函数指针并不是 GLIBC_2.3.2 版本,而是 pthread_cond_init@GLIBC_2.2.5(2.2.5 版本的 pthread_cond_init 还不支持使用 CLOCK_MONOTONIC 类型的时钟)。也就是说,dlsym 并没有获取到这个 symbol 的默认版本,这是 glibc 的一个已知问题。

Glibc 自 2.1 版本开始,就引入了一个名为 dlvsym 的库函数,可以在获取函数指针时指定符号的版本。我们再修改一下上面的例程中的 call_pthread_cond_init 函数:

static int call_pthread_cond_init(pthread_cond_t *cond,
                                  pthread_condattr_t *attr)
{
  cond_init_func_t func =
      (cond_init_func_t) dlvsym(RTLD_NEXT,
                                "pthread_cond_init",
                                "GLIBC_2.3.2");
  return func(cond, attr);
}
1
2
3
4
5
6
7
8
9

编译运行一下:

[Linux] $ g++ test.cc -lpthread -ldl
[Linux] $ ./a.out
1
2

可以看到,获取到正确版本的函数指针以后,就不会报错了。

可是,我们如何同时做到既可以 override glibc 函数,又能够获取 glibc 函数的默认版本呢?可以通过动态库来实现。即,在动态库中 override glibc 函数,并利用 LD_PRELOAD 机制加载(类似于 jemalloc 库和 tcmalloc 库的做法);并在加载阶段从当前可执行程序的ELF中读取版本信息。我们把调用 pthread_cond_init 的例程(test.cc)恢复到初始的样子:

#include <iostream>
#include <pthread.h>

int main()
{
  pthread_cond_t cond;
  pthread_condattr_t attr;

  pthread_condattr_init(&attr);
  pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);

  int ret = pthread_cond_init(&cond, &attr);

  if (ret != 0) {
    std::cout << "call_pthread_cond_init failed." << std::endl;
    return ret;
  }

  pthread_cond_destroy(&cond);

  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

然后,我们再实现一个动态库(readelf.cc),从 “/proc/self/exe” 中读取当前可执行文件的路径,并从该文件的ELF中读取符号表;同时,override pthread_cond_init 函数,通过之前获取的版本号,利用 dlvsym 获取 glibc 原生库函数的默认版本,并调用之:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <limits.h>
#include <link.h>
#include <string.h>
#include <pthread.h>

#include <string>
#include <iostream>
#include <algorithm>
#include <tr1/unordered_map>

typedef int (*cond_init_func_t)(pthread_cond_t *cond,
                                const pthread_condattr_t *attr);

using namespace std;

/*
  Symbol version map.
  Key:   symbol name.
  Value: version string.
*/
std::tr1::unordered_map<string, string> sym_versions;

static int call_pthread_cond_init(pthread_cond_t *cond,
                                  const pthread_condattr_t *attr)
{
  cond_init_func_t func =
      (cond_init_func_t) dlvsym(RTLD_NEXT,
                                "pthread_cond_init",
                                sym_versions[string("pthread_cond_init")].c_str());
  return func(cond, attr);
}

extern "C" {

int pthread_cond_init(pthread_cond_t *cond,
                      const pthread_condattr_t *attr)
{
  int ret = call_pthread_cond_init(cond, attr);
  cout << "Override pthread_cond_init (ret = " << ret << ")." << endl;
  return ret;
}

}

__attribute__((constructor))
void readelf()
{
  ElfW(Ehdr) ehdr;           // ELF header
  ElfW(Phdr*) phdrs = NULL;  // Program headers
  ElfW(Shdr*) shdrs = NULL;  // Section headers
  ElfW(Dyn*) dyns = NULL;    // Dynamic entrys

  ElfW(Sym*) syms = NULL;    // Symbol table
  ElfW(Word) sym_cnt = 0;    // Number of symbol entries

  char *strtab = NULL;       // String table
  ElfW(Word) strtab_sz = 0;  // Size in byte of string table
  ElfW(Off) strtab_off = 0;  // File offset of string table

  ElfW(Versym*) versyms = NULL;
  ElfW(Verneed*) verneeds = NULL;

  std::tr1::unordered_map<ElfW(Half), ElfW(Word)> vermap;

  char buffer[PATH_MAX];
  // Get the absolute path of current executable file.
  int res = readlink("/proc/self/exe", buffer, PATH_MAX);

  FILE *fp = fopen(buffer, "r");
  if (!fp) {
    cout << "Failed to open file: " << buffer << endl;
    return;
  }

  // Read the ELF header
  fread(&ehdr, 1, sizeof(ehdr), fp);

  // Check ELF magic numbers
  if (0 != strncmp((char *) ehdr.e_ident, ELFMAG, SELFMAG)) {
    cout << "Failed to check ELF magic numbers." << endl;
    goto out;
  }

  // Read the program headers
  phdrs = new ElfW(Phdr)[ehdr.e_phnum];
  fseek(fp, ehdr.e_phoff, SEEK_SET);
  fread(phdrs, ehdr.e_phnum, sizeof(ElfW(Phdr)), fp);

  cout << "Read " << ehdr.e_phnum << " program headers." << endl;

  for (int phdr_index = 0; phdr_index < ehdr.e_phnum; phdr_index++) {
    ElfW(Phdr*) phdr = &phdrs[phdr_index];

    if (phdr->p_type != PT_DYNAMIC)
      continue;

    cout << "Got the dynamic program header." << endl;

    dyns = (ElfW(Dyn*)) malloc(phdr->p_filesz);
    fseek(fp, phdr->p_offset, SEEK_SET);
    fread(dyns, phdr->p_filesz, sizeof(char), fp);
    for (ElfW(Dyn*) dyn = dyns; dyn->d_tag != DT_NULL; dyn++) {
      switch (dyn->d_tag) {
      case DT_STRSZ:
        strtab_sz = dyn->d_un.d_val;
        cout << "DT_STRSZ value: " << strtab_sz << "." << endl;
        break;
      default:
        break;
      }
    }

    break;
  }

  // Read section headers
  shdrs = new ElfW(Shdr)[ehdr.e_shnum];
  fseek(fp, ehdr.e_shoff, SEEK_SET);
  fread(shdrs, ehdr.e_shnum, sizeof(ElfW(Shdr)), fp);

  cout << "Read " << ehdr.e_shnum << " section headers." << endl;

  // Get the section name string table
  strtab = new char[std::max((ElfW(Word)) shdrs[ehdr.e_shstrndx].sh_size,
                             strtab_sz)];
  fseek(fp, shdrs[ehdr.e_shstrndx].sh_offset, SEEK_SET);
  fread(strtab, shdrs[ehdr.e_shstrndx].sh_size, sizeof(char), fp);

  // Read sections
  for (int s_idx = 0; s_idx < ehdr.e_shnum; s_idx++) {
    ElfW(Shdr*) sh = &shdrs[s_idx];
    //cout << s_idx << " " << strtab + sh->sh_name << endl;
    if (!strcmp(strtab + sh->sh_name, ".dynsym")) {
      sym_cnt = sh->sh_size / sizeof(ElfW(Sym));
      syms = new ElfW(Sym)[sym_cnt];
      fseek(fp, sh->sh_offset, SEEK_SET);
      fread(syms, sh->sh_size, sizeof(char), fp);
      cout << ".dynsym: got " << sym_cnt << " symbols." << endl;
    } else if (!strcmp(strtab + sh->sh_name, ".dynstr")) {
      cout << ".dynstr: offset " << sh->sh_offset
           << " size " << sh->sh_size << "." << endl;
      strtab_off = sh->sh_offset;
    } else if (!strcmp(strtab + sh->sh_name, ".gnu.version_r")) {
      cout << ".gnu.version_r: verneed offset " << sh->sh_offset
           << " size " << sh->sh_size
           << "." << endl;
      verneeds = (ElfW(Verneed*)) malloc(sh->sh_size);
      fseek(fp, sh->sh_offset, SEEK_SET);
      fread(verneeds, sh->sh_size, sizeof(char), fp);
    } else if (!strcmp(strtab + sh->sh_name, ".gnu.version")) {
      cout << ".gnu.version: versym offset " << sh->sh_offset
           << " size " << sh->sh_size
           << "." << endl;
      versyms = (ElfW(Versym*)) malloc(sh->sh_size);
      fseek(fp, sh->sh_offset, SEEK_SET);
      fread(versyms, sh->sh_size, sizeof(char), fp);
    }
  }

  // Get the symbol name string table
  fseek(fp, strtab_off, SEEK_SET);
  fread(strtab, strtab_sz, sizeof(char), fp);

  // Get verneeds
  for (ElfW(Verneed*) vn = verneeds; ; ) {
    cout << "verneed " << ":"
         << " vn_version " << vn->vn_version
         << " vn_cnt " << vn->vn_cnt
         << " vn_file " << strtab + vn->vn_file
         << " vn_aux " << vn->vn_aux
         << " vn_next " << vn->vn_next
         << "." << endl;

    ElfW(Vernaux*) vna = (ElfW(Vernaux*))((char*)vn + vn->vn_aux);
    for (ElfW(Half)i = 0; i < vn->vn_cnt; i++) {
      cout << "    aux " << i << ": "
           << " vna_name " << strtab + vna->vna_name
           << " vna_other " << vna->vna_other
           << "." << endl;

      vermap.insert(std::make_pair<ElfW(Half), ElfW(Word)>
                      (vna->vna_other, vna->vna_name));

      vna = (ElfW(Vernaux*))((char*)vna + vna->vna_next);
    }

    if (vn->vn_next == 0)
      break;

    vn = (ElfW(Verneed*)) ((char*)vn + vn->vn_next);
  }

  // Get versyms
  for (ElfW(Word) sym_index = 0; sym_index < sym_cnt; sym_index++) {
    ElfW(Sym*) sym = &syms[sym_index];
    const char *ver_name = "NONE";
    if (versyms[sym_index])
      ver_name = strtab + vermap[versyms[sym_index]];

    sym_versions.insert(std::make_pair<string, string>
                          (strtab + sym->st_name, ver_name));

    cout << "symbol " << strtab + sym->st_name
         << " version " << ver_name
         << "." << endl;
  }

out:
  fclose(fp);
  if (phdrs)
    delete [] phdrs;
  if (shdrs)
    delete [] shdrs;
  if (dyns)
    free(dyns);
  if (syms)
    delete [] syms;
  if (strtab)
    delete [] strtab;
  if (verneeds)
    free(verneeds);
  if (versyms)
    free(versyms);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227

我们分别编译,并以 LD_PRELOAD 方式运行上述程序:

[Linux] $ g++ test.cc -lpthread
[Linux] $ g++ -shared -fPIC -o readelf.so readelf.cc -ldl
[Linux] $ LD_PRELOAD=./readelf.so ./a.out
Read 8 program headers.
Got the dynamic program header.
DT_STRSZ value: 489.
Read 30 section headers.
.dynsym: got 16 symbols.
.dynstr: offset 1048 size 489.
.gnu.version: versym offset 1538 size 32.
.gnu.version_r: verneed offset 1576 size 144.
verneed : vn_version 1 vn_cnt 1 vn_file libc.so.6 vn_aux 16 vn_next 32.
    aux 0:  vna_name GLIBC_2.2.5 vna_other 4.
verneed : vn_version 1 vn_cnt 2 vn_file libstdc++.so.6 vn_aux 16 vn_next 48.
    aux 0:  vna_name CXXABI_1.3 vna_other 7.
    aux 1:  vna_name GLIBCXX_3.4 vna_other 3.
verneed : vn_version 1 vn_cnt 3 vn_file libpthread.so.0 vn_aux 16 vn_next 0.
    aux 0:  vna_name GLIBC_2.3.3 vna_other 6.
    aux 1:  vna_name GLIBC_2.2.5 vna_other 5.
    aux 2:  vna_name GLIBC_2.3.2 vna_other 2.
symbol  version NONE.
symbol pthread_cond_destroy version GLIBC_2.3.2.
symbol __gmon_start__ version NONE.
symbol _Jv_RegisterClasses version NONE.
symbol _ZNSt8ios_base4InitC1Ev version GLIBCXX_3.4.
symbol __libc_start_main version GLIBC_2.2.5.
symbol __cxa_atexit version GLIBC_2.2.5.
symbol _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc version GLIBCXX_3.4.
symbol pthread_cond_init version GLIBC_2.3.2.
symbol pthread_condattr_init version GLIBC_2.2.5.
symbol pthread_condattr_setclock version GLIBC_2.3.3.
symbol _ZNSolsEPFRSoS_E version GLIBCXX_3.4.
symbol _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ version GLIBCXX_3.4.
symbol _ZNSt8ios_base4InitD1Ev version GLIBCXX_3.4.
symbol _ZSt4cout version GLIBCXX_3.4.
symbol __gxx_personality_v0 version CXXABI_1.3.
Override pthread_cond_init (ret = 0).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

如上,当前进程所依赖的符号版本都可以被正确获取,pthread_cond_init 使用了我们 hook 的版本,并且没有报错。

# 调试glibc源码

-查看系统的libc版本

whereis libc.so
# 得到lic.so位置:/usr/lib/x86_64-linux-gnu/libc.so

/usr/lib/x86_64-linux-gnu/libc.so
# 得到版本信息:GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.4) stable release version 2.27.
#去官网下载glibc2.27的源代码, http://mirrors.nju.edu.cn/gnu/libc/
1
2
3
4
5
6

其中相关的代码:

// elf\dl-runtime.c    
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
    const ElfW(Half) *vernum =
        (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
    ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
    version = &l->l_versions[ndx];
    if (version->hash == 0)
        version = NULL;
}
1
2
3
4
5
6
7
8
9
10
//elf\dl-load.c
  l = _dl_new_object (realname, name, l_type, loader, mode, nsid);

  elf_get_dynamic_info (l, NULL);
1
2
3
4
//elf\get-dynamic-info.h
elf_get_dynamic_info (struct link_map *l, ElfW(Dyn) *temp)

1
2
3

glibc中relocate源码:

# readelf源码:

git clone git://sourceware.org/git/binutils-gdb.git

# stdlibc++源码下载和编译安装

stdlibc++包含在gcc中,搜索gcc源码下载地址:http://ftp.tsukuba.wide.ad.jp/software/gcc/releases/gcc-7.5.0/

这个镜像下载比较快:ftp://ftp.lip6.fr/pub/gcc/releases/gcc-7.3.0/gcc-7.3.0.tar.xz

重点参考gcc官方编译文档:https://gcc.gnu.org/wiki/InstallingGCC

在其中指定只编译stdlibc++,注意不要在下载的源文件根目录下运行./configure,要另外新建一个文件夹。比如这个文件夹建在gcc-7.5.0同一个目录下,叫gccbuild,后面build生成的文件也在这个新文件夹里

首先cd到gcc-7.5.0目录下,下载所需要的依赖项,再cd出来新建一个gccbuild目录,然后在这个目录里configure

$ ./contrib/download_prerequisites
$ cd ..
$ mkdir gccbuild
$ cd gccbuild
$ ../gcc-7.5.0/configure --enable-languages=c,c++ --disable-multilib
$ make
1
2
3
4
5
6

初次build会build整个gdb,即便设置了--enable-languages=c++。这个过程大概需要2小时,build完成后,查找so文件的生成位置,然后在自己项目中包含进去。通过md5sum查看这三个文件,可以发现其中一个与另外两个不同。项目中应该使用的是x86_64-pc-linux-gnu/libstdc++-v3/src/.libs/libstdc++.so。

# in gccbuild
$ find . -name libstdc++.so | xargs md5sum

2a34bce9213c052ad0ec09af2b9fd657  ./prev-x86_64-pc-linux-gnu/libstdc++-v3/src/.libs/libstdc++.so
2a34bce9213c052ad0ec09af2b9fd657  ./stage1-x86_64-pc-linux-gnu/libstdc++-v3/src/.libs/libstdc++.so
4e58a9d35539c51d4c9cfce07016c89a  ./x86_64-pc-linux-gnu/libstdc++-v3/src/.libs/libstdc++.so
1
2
3
4
5
6

# 修改代码重新编译

如果要修改代码,可在gcc-7.5.0这个源文件下修改,初次build时会建立gcc-7.5.0里面源文件的软连接(绝对路径)到build目录下。修改了代码后,在gccbuild/x86_64-pc-linux-gnu/libstdc++-v3目录下编译:

# curent in gccbuild
$ cd x86_64-pc-linux-gnu/libstdc++-v3
$ make clean
$ make
1
2
3
4

# 其他参考文章

gdb在链接器打断点:https://blog.csdn.net/v_ling_v/article/details/42407587/ (opens new window)

动态库中的全局变量被同名覆盖:https://blog.csdn.net/lcalqf/article/details/78129697 (opens new window)

记录一次gcc 4.8.5 的cow string 引发的问题: https://zhuanlan.zhihu.com/p/344115460 (opens new window)

# 命令

# 设置包含修改后的libstdc++
export LD_LIBRARY_PATH=/home/gaoliu/elfloader_test/gccbuild/x86_64-pc-linux-gnu/libstdc++-v3/src/.libs:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/home/gaoliu/elfloader_test/loader/bin:$LD_LIBRARY_PATH

export LD_LIBRARY_PATH=/usr/local/src/gccbuild/x86_64-pc-linux-gnu/libstdc++-v3/src/.libs:$LD_LIBRARY_PATH

# 查看ASLR等级:(默认为2)
$ cat /proc/sys/kernel/randomize_va_space

# 将ASLR等级设为0:
$ sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"
1
2
3
4
5
6
7
8
9
10
11

# double free

# std::string中的Copy-On-Write

string对象中包含引用计数,源码中对其注释如下:

//   3. _M_refcount has three states:
//      -1: leaked, one reference, no ref-copies allowed, non-const.
//       0: one reference, non-const.
//     n>0: n + 1 references, operations require a lock, const.
1
2
3
4
  • 0,初始创建时为0,表示当前可以共享,有一个引用
  • -1: leaked, 是当调用 iterator erase() 、iterator insert() 等函数时,会被设为-1,表示外界取了iterator,该字符串不受控制了,不能再被共享
  • n>0,表示有n+1个引用。

_M_dispose函数表示对当前字符串引用减1。如果减1之后小于0了,就销毁。注意此时的-1跟上面的leaked不是一个意思,因为它马上被销毁了。而上面的leaked表示字符串正常存在,只是不能被共享。

//_M_dispose源码,gcc-7.5.0/libstdc++-v3/include/bits/basic_string.h
void _M_dispose(const _Alloc& __a) _GLIBCXX_NOEXCEPT
	{
#if _GLIBCXX_FULLY_DYNAMIC_STRING == 0 
	  if (__builtin_expect(this != &_S_empty_rep(), false))
#endif
	    {
	      _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&this->_M_refcount);
          if (__gnu_cxx::__exchange_and_add_dispatch(&this->_M_refcount, -1) <= 0)
							 
		{
		  _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&this->_M_refcount);
          _M_destroy(__a);
		}
	    }
	}  // XXX MT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

__exchange_and_add_dispatch(_Atomic_word* __mem, int __val),将__val加到mem上,并返回mem的旧值。与cas有点像。参考:https://blog.csdn.net/kupepoem/article/details/119848082 (opens new window)

# _M_dispose double free 函数调用

当调用了basic_string::_M_dispose()时,就会发生double free:

//gcc-7.5.0/libstdc++-v3/include/bits/basic_string.h
_M_dispose() {
    _M_destroy(__a); 
}

//gcc-7.5.0/libstdc++-v3/include/bits/basic_string.tcc
_M_destroy(const _Alloc& __a) throw ()
{
    const size_type __size = sizeof(_Rep_base) +
        (this->_M_capacity + 1) * sizeof(_CharT);
    _Raw_bytes_alloc(__a).deallocate(reinterpret_cast<char*>(this), __size);
}
1
2
3
4
5
6
7
8
9
10
11
12

stack:

libc.so.6!__GI_raise(int sig) (\home\gaoliu\glibc-2.27\sysdeps\unix\sysv\linux\raise.c:51)
libc.so.6!__GI_abort() (\home\gaoliu\glibc-2.27\stdlib\abort.c:79)
libc.so.6!__libc_message(enum __libc_message_action action, const char * fmt) (\home\gaoliu\glibc-2.27\sysdeps\posix\libc_fatal.c:181)
libc.so.6!malloc_printerr(const char * str) (\home\gaoliu\glibc-2.27\malloc\malloc.c:5342)
libc.so.6!_int_free(int have_lock, mchunkptr p, mstate av) (\home\gaoliu\glibc-2.27\malloc\malloc.c:4308)
libc.so.6!__GI___libc_free(void * mem) (\home\gaoliu\glibc-2.27\malloc\malloc.c:3134)
libstdc++.so.6!std::string::_M_mutate(unsigned long, unsigned long, unsigned long) (Unknown Source:0)
libstdc++.so.6!std::string::_M_replace_safe(unsigned long, unsigned long, char const*, unsigned long) (Unknown Source:0)
[Unknown/Just-In-Time compiled code] (Unknown Source:0)
1
2
3
4
5
6
7
8
9

修改libstdc++源码,加入printf打印,

源码修改:

//gcc-7.5.0/libstdc++-v3/include/bits/basic_string.h

	void _M_dispose(const _Alloc& __a) _GLIBCXX_NOEXCEPT
	{
#if _GLIBCXX_FULLY_DYNAMIC_STRING == 0
      printf("gl2021-09-22 _M_dispose, -8, this=0x%x, &_S_empty_rep()=0x%x\n", this, &_S_empty_rep());
	  if (__builtin_expect(this != &_S_empty_rep(), false))
#endif
	    {
	      _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&this->_M_refcount);
            printf("gl2021-09-22 _M_dispose, -7, this=0x%x, this->_M_refcount=%d\n", this->_M_refcount);
          
            if (__gnu_cxx::__exchange_and_add_dispatch(&this->_M_refcount,
							 -1) <= 0)
		{
		  _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&this->_M_refcount);
              
              printf("gl2021-09-22 _M_dispose, -6 next call _M_destroy, 0x%x\n", this);
              _M_destroy(__a); //gl2021-09-22 avoid double free, no free
		}
	    }
	}  // XXX MT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

然后分别执行tgw和elfload tgw,二者的函数调用情况如下。

关键在这一句发生了不同的情况:this != &_S_empty_rep()

ubuntu@ubuntu-virtual-machine:/home/gaoliu/elfloader_test/loader/bin$ ./elfload tgw
[load_exe]: tgw
[LoadAndSecInfo]: allocate address at: 0x400000, 0x400000, 0x795fa4
[LoadAndSecInfo]: allocate address at: 0xd96000, 0xd96000, 0x30e68
[load_exe]: load_sym
[load_exe]: init point: 0x4205d8
gl2021-09-22 _S_create, new, 0xf9d89500, size=30
gl2021-09-22 _M_dispose, -8, this=0xf9d89500, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=-1330036112    
gl2021-09-22 _S_create, new, 0xf9d89600, size=26
gl2021-09-22 _S_create, new, 0xf9d89630, size=33
gl2021-09-22 _S_create, new, 0xf9d89660, size=28
gl2021-09-22 _M_dispose, -8, this=0xf9d89660, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=-1330036112    
gl2021-09-22 _M_dispose, -8, this=0x26a59d80, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -8, this=0x26a59d80, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _S_create, new, 0xf9d89a80, size=26
gl2021-09-22 _S_create, new, 0xf9d89ab0, size=33
gl2021-09-22 _S_create, new, 0xf9d89600, size=28
gl2021-09-22 _S_create, new, 0xf9d89d30, size=33
gl2021-09-22 _M_dispose, -8, this=0xf9d89d30, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=-1330036896    
gl2021-09-22 _M_dispose, -8, this=0xf9d89d30, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=-1330036896    
gl2021-09-22 _S_create, new, 0xf9d89d60, size=32
gl2021-09-22 _M_dispose, -8, this=0xf9d89d60, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=-1330036896    
gl2021-09-22 _M_dispose, -8, this=0xf9d89d60, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=-1330036896    
gl2021-09-22 _S_create, new, 0xf9d89d90, size=32
gl2021-09-22 _M_dispose, -8, this=0xf9d89d90, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=-1330036896    
gl2021-09-22 _M_dispose, -8, this=0xf9d89d90, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=-1330036896    
gl2021-09-22 _S_create, new, 0xf9d89dc0, size=31
gl2021-09-22 _M_dispose, -8, this=0xf9d89dc0, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=-1330036896    
gl2021-09-22 _M_dispose, -8, this=0xf9d89dc0, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=-1330036896    
gl2021-09-22 _S_create, new, 0xf9d89df0, size=34
gl2021-09-22 _M_dispose, -8, this=0xf9d89df0, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=-1330036896    
gl2021-09-22 _M_dispose, -8, this=0xf9d89df0, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=-1330036896    
gl2021-09-22 _S_create, new, 0xf9d89e20, size=34
gl2021-09-22 _M_dispose, -8, this=0xf9d89e20, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=-1330036896
gl2021-09-22 _M_dispose, -8, this=0xf9d89e20, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=-1330036896
gl2021-09-22 _S_create, new, 0xf9d89e50, size=33
gl2021-09-22 _M_dispose, -8, this=0xf9d89e50, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=-1330036896
gl2021-09-22 _M_dispose, -8, this=0xf9d89e50, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=-1330036896
gl2021-09-22 _S_create, new, 0xf9d897c0, size=47
gl2021-09-22 _S_create, new, 0xf9d8a1f0, size=29
gl2021-09-22 _S_create, new, 0xf9d896f0, size=50
gl2021-09-22 _M_dispose, -8, this=0xf9d896f0, &_S_empty_rep()=0x26a59d80
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=-1330036240
gl2021-09-22 _S_create, new, 0xf9d8a9e0, size=47
gl2021-09-22 _M_replace_safe, -12, 0xb0b945d0
gl2021-09-22 _S_create, new, 0xf9d8aae0, size=43
gl2021-09-22 _M_mutate, -9, 0xb0b945d0
gl2021-09-22 _M_dispose, -8, this=0xdb4dc0, &_S_empty_rep()=0x26a59d80 (注意这里的this=0xdb4dc0)

gl2021-09-22 _M_dispose, -7, this=0x0, this->_M_refcount=-1330039200
gl2021-09-22 _M_dispose, -6 next call _M_destroy, 0xdb4dc0
double free or corruption (out)
Aborted (core dumped)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
[/home/gaoliu/elfloader_test/loader/bin]$./tgw
gl2021-09-22 _S_create, new, 0x24c03d0, size=30
gl2021-09-22 _M_dispose, -8, this=0x24c03d0, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=2067861168  
gl2021-09-22 _S_create, new, 0x24c08e0, size=26
gl2021-09-22 _S_create, new, 0x24c0910, size=33
gl2021-09-22 _S_create, new, 0x24c0940, size=28
gl2021-09-22 _M_dispose, -8, this=0x24c0940, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=2067861168  
gl2021-09-22 _M_dispose, -8, this=0xdb4dc0, &_S_empty_rep()=0xdb4dc0 
gl2021-09-22 _M_dispose, -8, this=0xdb4dc0, &_S_empty_rep()=0xdb4dc0 
gl2021-09-22 _S_create, new, 0x24c0d60, size=26
gl2021-09-22 _S_create, new, 0x24c0d90, size=33
gl2021-09-22 _S_create, new, 0x24c08e0, size=28
gl2021-09-22 _S_create, new, 0x24c11d0, size=33
gl2021-09-22 _M_dispose, -8, this=0x24c11d0, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=2067860384  
gl2021-09-22 _M_dispose, -8, this=0x24c11d0, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=2067860384  
gl2021-09-22 _S_create, new, 0x24c1200, size=32
gl2021-09-22 _M_dispose, -8, this=0x24c1200, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=2067860384  
gl2021-09-22 _M_dispose, -8, this=0x24c1200, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=2067860384  
gl2021-09-22 _S_create, new, 0x24c1230, size=32
gl2021-09-22 _M_dispose, -8, this=0x24c1230, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=2067860384  
gl2021-09-22 _M_dispose, -8, this=0x24c1230, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=2067860384  
gl2021-09-22 _S_create, new, 0x24c1260, size=31
gl2021-09-22 _M_dispose, -8, this=0x24c1260, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=2067860384  
gl2021-09-22 _M_dispose, -8, this=0x24c1260, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=2067860384  
gl2021-09-22 _S_create, new, 0x24c1290, size=34
gl2021-09-22 _M_dispose, -8, this=0x24c1290, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=2067860384  
gl2021-09-22 _M_dispose, -8, this=0x24c1290, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=2067860384  
gl2021-09-22 _S_create, new, 0x24c12c0, size=34
gl2021-09-22 _M_dispose, -8, this=0x24c12c0, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=2067860384  
gl2021-09-22 _M_dispose, -8, this=0x24c12c0, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=2067860384  
gl2021-09-22 _S_create, new, 0x24c12f0, size=33
gl2021-09-22 _M_dispose, -8, this=0x24c12f0, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x2, this->_M_refcount=2067860384
gl2021-09-22 _M_dispose, -8, this=0x24c12f0, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=2067860384
gl2021-09-22 _S_create, new, 0x24c0aa0, size=47
gl2021-09-22 _S_create, new, 0x24c1800, size=29
gl2021-09-22 _S_create, new, 0x24c09d0, size=50
gl2021-09-22 _M_dispose, -8, this=0x24c09d0, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_dispose, -7, this=0x1, this->_M_refcount=2067861040
gl2021-09-22 _S_create, new, 0x24c1ff0, size=47
gl2021-09-22 _M_replace_safe, -12, 0x7b411210
gl2021-09-22 _S_create, new, 0x24c20f0, size=43
gl2021-09-22 _M_mutate, -9, 0x7b411210
gl2021-09-22 _M_dispose, -8, this=0xdb4dc0, &_S_empty_rep()=0xdb4dc0  (注意这里的this=0xdb4dc0)


gl2021-09-22 _M_replace_safe, -12, 0x7b411220
gl2021-09-22 _S_create, new, 0x24c2130, size=48
gl2021-09-22 _M_mutate, -9, 0x7b411220
gl2021-09-22 _M_dispose, -8, this=0xdb4dc0, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_replace_safe, -12, 0x7b411230
gl2021-09-22 _S_create, new, 0x24c2170, size=39
gl2021-09-22 _M_mutate, -9, 0x7b411230
gl2021-09-22 _M_dispose, -8, this=0xdb4dc0, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_replace_safe, -12, 0x7b411240
gl2021-09-22 _S_create, new, 0x24c21a0, size=41
gl2021-09-22 _M_mutate, -9, 0x7b411240
gl2021-09-22 _M_dispose, -8, this=0xdb4dc0, &_S_empty_rep()=0xdb4dc0
gl2021-09-22 _M_replace_safe, -12, 0x7b411250
gl2021-09-22 _S_create, new, 0x24c21e0, size=40
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

可见关键在于二者&_S_empty_rep()的地址不一样。

# _S_empty_rep相关问题

执行readelf tgw,查找_S_empty相关信息:

db4dc0

$ readelf -a -W tgw | grep S_empty
.dynsym段:
Num:    Value          Size Type    Bind   Vis      Ndx Name
877: 0000000000db50c0    32 OBJECT  GLOBAL DEFAULT   28 _ZNSbIwSt11char_traitsIwESaIwEE4_Rep20_S_empty_rep_storageE@GLIBCXX_3.4 (5)
937: 0000000000db4dc0    32 OBJECT  GLOBAL DEFAULT   28 _ZNSs4_Rep20_S_empty_rep_storageE@GLIBCXX_3.4 (5)

.rela.dyn段:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
0000000000da1d58  000003a900000006 R_X86_64_GLOB_DAT      0000000000db4dc0 _ZNSs4_Rep20_S_empty_rep_storageE@GLIBCXX_3.4 + 0
0000000000db4dc0  000003a900000005 R_X86_64_COPY          0000000000db4dc0 _ZNSs4_Rep20_S_empty_rep_storageE@GLIBCXX_3.4 + 0
0000000000da2180  0000036d00000006 R_X86_64_GLOB_DAT      0000000000db50c0 _ZNSbIwSt11char_traitsIwESaIwEE4_Rep20_S_empty_rep_storageE@GLIBCXX_3.4 + 0
0000000000db50c0  0000036d00000005 R_X86_64_COPY          0000000000db50c0 _ZNSbIwSt11char_traitsIwESaIwEE4_Rep20_S_empty_rep_storageE@GLIBCXX_3.4 + 0


(.rel.dyn是对数据引用的修正,它修正的是.got; .rel.plt是对函数引用的修正,它修正的是.got.plt)


$ readelf -a -W elfload | grep _S_empty_rep
#elfload中找不到

$ readelf tgw -a | grep entries
    Dynamic section at offset 0x7a19a8 contains 27 entries:
    Relocation section '.rela.dyn' at offset 0x18e20 contains 645 entries:
    Relocation section '.rela.plt' at offset 0x1ca98 contains 632 entries:
    Symbol table '.dynsym' contains 1273 entries:
    Version symbols section '.gnu.version' contains 1273 entries:
    Version needs section '.gnu.version_r' contains 8 entries:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

注意上面的Name实际上是readelf截断了的,完整的应该是_ZNSs4_Rep20_S_empty_rep_storageE,还有另一个_

_ZNSbIwSt11char_traitsIwESaIwEE4_Rep20_S_empty_rep_storageE
877: 0000000000db50c0    32 OBJECT  GLOBAL DEFAULT   28 _ZNSbIwSt11char_traitsIwE@GLIBCXX_3.4 (5)
1
2

用c++filt查看其原来的名称

$ c++filt _ZNSs4_Rep20_S_empty_rep_storageE
std::basic_string<char, std::char_traits<char>, std::allocator<char> >::_Rep::_S_empty_rep_storage

$ c++filt _ZNSbIwSt11char_traitsIwESaIwEE4_Rep20_S_empty_rep_storageE
std::basic_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t> >::_Rep::_S_empty_rep_storage
1
2
3
4
5

elfload tgw每次运行时&_S_empty_rep()的地址都会改变,而直接执行./tgw,&_S_empty_rep()的地址一直不变,都是0xdb4dc0

上面运行时ASLR默认为2,将其关闭(设为0)后,执行elfload tgw的&_S_empty_rep()不变。重新编译elfload后其地址又会改变。

readelf tgw 读取地址不为0的.dynsym字段:

$ readelf -a tgw -W | grep "OBJECT  GLOBAL DEFAULT"

Num:    Value          Size Type    Bind   Vis      Ndx Name
...  (前面都是0)
606: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  UND _ZTId@CXXABI_1.3 (6)
616: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  UND _ZTVSt7collateIwE@GLIBCXX_3.4 (5)
670: 0000000000db4818     8 OBJECT  GLOBAL DEFAULT   27 _ZN5boost3log12v2s_mt_posix3aux15dump_data_wcharE
740: 0000000000db4ea0   272 OBJECT  GLOBAL DEFAULT   28 _ZSt4clog@GLIBCXX_3.4 (5)
778: 0000000000dc4ef0     8 OBJECT  GLOBAL DEFAULT   28 _ZN5boost6locale9converterIcE2idE
814: 0000000000db5698     8 OBJECT  GLOBAL DEFAULT   28 stderr@GLIBC_2.2.5 (2)
837: 0000000000dc4ef8     8 OBJECT  GLOBAL DEFAULT   28 _ZN5boost6locale19base_message_formatIcE2idE
849: 0000000000dc4ee0     8 OBJECT  GLOBAL DEFAULT   28 _ZN5boost6locale4info2idE
877: 0000000000db50c0    32 OBJECT  GLOBAL DEFAULT   28 _ZNSbIwSt11char_traitsIwESaIwEE4_Rep20_S_empty_rep_storageE@GLIBCXX_3.4 (5)
887: 0000000000db4860  1032 OBJECT  GLOBAL DEFAULT   27 _ZN5boost16re_detail_10600014def_coll_namesE
931: 0000000000db4c80   176 OBJECT  GLOBAL DEFAULT   27 _ZN5boost16re_detail_10600014def_multi_collE
937: 0000000000db4dc0    32 OBJECT  GLOBAL DEFAULT   28 _ZNSs4_Rep20_S_empty_rep_storageE@GLIBCXX_3.4 (5)
999: 0000000000dc4f00     8 OBJECT  GLOBAL DEFAULT   28 _ZN5boost6locale9converterIwE2idE
1003: 0000000000db4810     8 OBJECT  GLOBAL DEFAULT   27 _ZN5boost3log12v2s_mt_posix3aux14dump_data_charE
1006: 0000000000dc4f10     8 OBJECT  GLOBAL DEFAULT   28 _ZN5boost6locale8boundary17boundary_indexingIcE2idE
1026: 0000000000dc4ee8     8 OBJECT  GLOBAL DEFAULT   28 _ZN5boost6locale14calendar_facet2idE
1049: 0000000000dc4f08     8 OBJECT  GLOBAL DEFAULT   28 _ZN5boost6locale19base_message_formatIwE2idE
1076: 0000000000db5238     8 OBJECT  GLOBAL DEFAULT   28 stdin@GLIBC_2.2.5 (2)
1087: 0000000000dc4f18     8 OBJECT  GLOBAL DEFAULT   28 _ZN5boost6locale8boundary17boundary_indexingIwE2idE
1099: 0000000000dc4e60     8 OBJECT  GLOBAL DEFAULT   28 _ZN5boost15program_options3argE
1115: 0000000000db5b60   272 OBJECT  GLOBAL DEFAULT   28 _ZSt4cerr@GLIBCXX_3.4 (5)
1119: 0000000000db4de0     1 OBJECT  GLOBAL DEFAULT   28 _ZSt7nothrow@GLIBCXX_3.4 (5)
1176: 0000000000db54c0   272 OBJECT  GLOBAL DEFAULT   28 _ZSt4cout@GLIBCXX_3.4 (5)
1188: 0000000000dc6ca0    56 OBJECT  GLOBAL DEFAULT   28 _ZN5boost16re_detail_10600011block_cacheE

ndx=27, .data段
ndx=28, .bss段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 问题原因推测

推测应该是tgw的动态符号表dynsym输出了_ZNSs4_Rep20_S_empty_rep_符号且确定了其地址为0x00db4dc0 ,导致glibcxx中的该符号重定位到该地址,且tgw中有对该符号绝对地址的引用。而elfload中此符号是通过dlsym动态加载libstdc++.so得到的,加载该符号时并未输出该符号的绝对地址,因此会得到一个随机地址。

tgw的rela.dyn和.dynsym中均有该符号的信息,说明其定义的是extern类型的变量。

# 修改elf动态符号表dynsym

# 参考文章

  • https://www.cnblogs.com/vo1ad0r/p/11585025.html#autoid-0-2-0 (opens new window) ,该文章中用python的ELF模块来读写elf,看看这个elf模块怎么用的。

  • https://blog.csdn.net/modisir/article/details/118674100 (opens new window) ,运行时遍历elf头,dl_iterate_phdr

# 用elfio工具对elf进行修改

查找tgw中elf文件的.dynsym、rela.dyn、.dynstr entry等信息,找到导出的全局变量;

在elfload中增加相应的entry,从而使ld在加载库的时候对这些全局变量进行重定位

# 通过link脚本lds在segment的section之间加入空白空间

  • section加入新的entry后占用空间变大,会占用后面挨着的section的位置。通过link脚本在section之后增加一些空白,避免冲突。

  • ld -verbose导出默认的链接脚本

SECTIONS
{
  /* Read-only sections, merged into text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x00000)); . = SEGMENT_START("text-segment", 0x00000) + SIZEOF_HEADERS;
  .interp         : { *(.interp) }
  .note.gnu.build-id : { *(.note.gnu.build-id) }
  .hash           : { *(.hash) }
  .gnu.hash       : { *(.gnu.hash) }
  
  . += 0x1000;
  .dynsym         : { *(.dynsym) }
  . += 0x1000;
  .dynstr         : { *(.dynstr) }
  . += 0x1000;
  .gnu.version    : { *(.gnu.version) }
  . += 0x1000;
  .gnu.version_d  : { *(.gnu.version_d) }
  . += 0x1000;
  .gnu.version_r  : { *(.gnu.version_r) }
  . += 0x1000;
  .rela.dyn       :
    {
      *(.rela.init)
      *(.rela.text .rela.text.* .rela.gnu.linkonce.t.*)
      *(.rela.fini)
      ...
    }
  . += 0x1000;
  .rela.plt       :
    {
      *(.rela.plt)
      PROVIDE_HIDDEN (__rela_iplt_start = .);
      *(.rela.iplt)
      PROVIDE_HIDDEN (__rela_iplt_end = .);
    }
  .init           :
  {
    KEEP (*(SORT_NONE(.init)))
  }
  .plt            : { *(.plt) *(.iplt) }
  .plt.got        : { *(.plt.got) }
  ... /* 剩下的省略 */
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

(另外"text-segment", 0x00000, 默认是0x40000,这里改成0,避免占用被load的程序的空间)

# glibc中查找dynsym的方法

上述操作并不能使dlsym获取到的_ZNSs4_Rep20_S_empty_rep_storageE符号地址固定为指定的地址。因为glibc/ld源码中查找dynsym符号时通过.gun.hash段和布隆滤波器等算法来查找,具体算法和参考文章如下。

从实例分析ELF格式的.gnu.hash区与glibc的符号查找: https://www.cnblogs.com/orange-snow/p/14824095.html

开源项目elfio中的查找方法

    bool get_symbol( const std::string& name,
                     Elf64_Addr&        value,
                     Elf_Xword&         size,
                     unsigned char&     bind,
                     unsigned char&     type,
                     Elf_Half&          section_index,
                     unsigned char&     other ) const
    {
        bool ret = false;

        if ( 0 != get_hash_table_index() ) {
            if ( hash_section->get_type() == SHT_HASH ) {
                ret = hash_lookup( name, value, size, bind, type, section_index,
                                   other );
            }
            if ( hash_section->get_type() == SHT_GNU_HASH ||
                 hash_section->get_type() == DT_GNU_HASH ) {
                if ( elf_file.get_class() == ELFCLASS32 ) {
                    ret = gnu_hash_lookup<uint32_t>(
                        name, value, size, bind, type, section_index, other );
                }
                else {
                    ret = gnu_hash_lookup<uint64_t>(
                        name, value, size, bind, type, section_index, other );
                }
            }
        }

        if ( !ret ) {
            for ( Elf_Xword i = 0; !ret && i < get_symbols_num(); i++ ) {
                std::string symbol_name;
                if ( get_symbol( i, symbol_name, value, size, bind, type,
                                 section_index, other ) ) {
                    if ( symbol_name == name ) {
                        ret = true;
                    }
                }
            }
        }

        return ret;
    }


    //------------------------------------------------------------------------------
    bool hash_lookup( const std::string& name,
                      Elf64_Addr&        value,
                      Elf_Xword&         size,
                      unsigned char&     bind,
                      unsigned char&     type,
                      Elf_Half&          section_index,
                      unsigned char&     other ) const
    {
        bool                       ret       = false;
        const endianess_convertor& convertor = elf_file.get_convertor();

        Elf_Word nbucket = *(const Elf_Word*)hash_section->get_data();
        nbucket          = convertor( nbucket );
        Elf_Word nchain =
            *(const Elf_Word*)( hash_section->get_data() + sizeof( Elf_Word ) );
        nchain       = convertor( nchain );
        Elf_Word val = elf_hash( (const unsigned char*)name.c_str() );
        Elf_Word y =
            *(const Elf_Word*)( hash_section->get_data() +
                                ( 2 + val % nbucket ) * sizeof( Elf_Word ) );
        y = convertor( y );
        std::string str;
        get_symbol( y, str, value, size, bind, type, section_index, other );
        while ( str != name && STN_UNDEF != y && y < nchain ) {
            y = *(const Elf_Word*)( hash_section->get_data() +
                                    ( 2 + nbucket + y ) * sizeof( Elf_Word ) );
            y = convertor( y );
            get_symbol( y, str, value, size, bind, type, section_index, other );
        }

        if ( str == name ) {
            ret = true;
        }

        return ret;
    }

    //------------------------------------------------------------------------------
    template <class T>
    bool gnu_hash_lookup( const std::string& name,
                          Elf64_Addr&        value,
                          Elf_Xword&         size,
                          unsigned char&     bind,
                          unsigned char&     type,
                          Elf_Half&          section_index,
                          unsigned char&     other ) const
    {
        bool                       ret       = false;
        const endianess_convertor& convertor = elf_file.get_convertor();

        uint32_t nbuckets    = *( (uint32_t*)hash_section->get_data() + 0 );
        uint32_t symoffset   = *( (uint32_t*)hash_section->get_data() + 1 );
        uint32_t bloom_size  = *( (uint32_t*)hash_section->get_data() + 2 );
        uint32_t bloom_shift = *( (uint32_t*)hash_section->get_data() + 3 );
        nbuckets             = convertor( nbuckets );
        symoffset            = convertor( symoffset );
        bloom_size           = convertor( bloom_size );
        bloom_shift          = convertor( bloom_shift );

        T* bloom_filter =
            (T*)( hash_section->get_data() + 4 * sizeof( uint32_t ) );

        uint32_t hash = elf_gnu_hash( (const unsigned char*)name.c_str() );
        uint32_t bloom_index = ( hash / ( 8 * sizeof( T ) ) ) % bloom_size;
        T        bloom_bits =
            ( (T)1 << ( hash % ( 8 * sizeof( T ) ) ) ) |
            ( (T)1 << ( ( hash >> bloom_shift ) % ( 8 * sizeof( T ) ) ) );

        if ( ( convertor( bloom_filter[bloom_index] ) & bloom_bits ) ==
             bloom_bits ) {
            uint32_t bucket = hash % nbuckets;
            auto*    buckets =
                (uint32_t*)( hash_section->get_data() + 4 * sizeof( uint32_t ) +
                             bloom_size * sizeof( T ) );
            auto* chains =
                (uint32_t*)( hash_section->get_data() + 4 * sizeof( uint32_t ) +
                             bloom_size * sizeof( T ) +
                             nbuckets * sizeof( uint32_t ) );

            if ( convertor( buckets[bucket] ) >= symoffset ) {
                uint32_t chain_index = convertor( buckets[bucket] ) - symoffset;
                uint32_t chain_hash  = convertor( chains[chain_index] );
                std::string symname;
                while ( true ) {
                    if ( ( chain_hash >> 1 ) == ( hash >> 1 ) &&
                         get_symbol( chain_index + symoffset, symname, value,
                                     size, bind, type, section_index, other ) &&
                         name == symname ) {
                        ret = true;
                        break;
                    }

                    if ( chain_hash & 1 )
                        break;
                    chain_hash = convertor( chains[++chain_index] );
                }
            }
        }

        return ret;
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
编辑 (opens new window)
上次更新: 2023/05/07, 17:27:54
面试常见问题
动态库和静态库的依赖问题

← 面试常见问题 动态库和静态库的依赖问题→

最近更新
01
通过模板实现结构体成员拷贝-按成员名匹配
05-07
02
c++17通过模板获取类成员的个数
05-01
03
avx-sse切换惩罚
04-30
更多文章>
Theme by Vdoing | Copyright © 2019-2023 Colder Leo | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×