一、背景
在之前的博客 跟踪jemalloc 5.3.0的第一次malloc的源头原因及jemalloc相关初始化细节拓展-CSDN博客 里,我们分析了在preload jemalloc的库之后,main之前的一次malloc分配(分配72704字节)的源头原因并做了jemalloc的初始化流程的分析,这篇博客里,我们针对之前博客里 2.4 一节里提到的“最终执行malloc@plt时,是如何找到我们jemalloc的malloc函数”的问题做展开,主要涉及到GOT和PLT和glibc里的_dl_runtime_resolve汇编逻辑和_dl_fixup逻辑。
所以,这篇博客,我们追踪逻辑用的程序和之前的博客 跟踪jemalloc 5.3.0的第一次malloc的源头原因及jemalloc相关初始化细节拓展-CSDN博客 里的一样,分析的是preload jemalloc库后,main之前的malloc 72704的这次调用逻辑里的细节,只是之前的博客里是直接跳过了分析malloc@plt去找jemalloc的malloc函数这个细节逻辑,在这篇博客里展开分析。
我们在下面的第二章里从之前的博客 跟踪jemalloc 5.3.0的第一次malloc的源头原因及jemalloc相关初始化细节拓展-CSDN博客 里 2.4 一节里的malloc@plt处开始跟一下相关的调用关系,及全局的一些状态,里面也涉及到了%rip的相关细节及实验。在第三章里,我们展开分析GOT和PLT表相关的glibc里的_dl_runtime_resolve汇编逻辑和_dl_fixup逻辑的细节。
二、从malloc@plt处开始展开分析
这一章是从下图里的call malloc@plt开始分析,但是如下面截图,下图截图是已经走过了call malloc@plt这句了,但是我们需要从call malloc@plt这句开始处来分析,如果要这样分析,那么断点设在jemalloc的malloc函数的入口处就不够了:
我们需要在前面就断点下来,一步步进入执行。
我们在 2.1 里讲如何设置断点并可以单步去调试main之前的这次malloc 72704的逻辑。在 2.2 里我们讲单步到malloc@plt里后看到的%rip寄存器的细节。然后,我们在 2.3 里比较一下没有执行过malloc和执行过一次malloc之后的跳转表的数值变化情况。最后在 2.4 里单步跟踪一下glibc里相关的逻辑的执行情况。
2.1 单步去调试main之前的这次malloc里的malloc@plt等跳转逻辑
我们在dl-init.c里下图的位置增加断点后,程序运行起来之后,点继续(Run)点9次之后(因为下图里的dl_init_t的循环走了9次以后,第10次是执行的libstdc++.so里的异常用的pool的初始化逻辑,相关细节见之前的博客 跟踪jemalloc 5.3.0的第一次malloc的源头原因及jemalloc相关初始化细节拓展-CSDN博客 ):
就到反汇编窗口里单步执行汇编,走到了malloc@plt如下图:
如上图,这句跳转用到了%rip寄存器。上图中的bnd前缀暂且可以忽略,bnd的实际含义我们当前并不需要了解。我在下面 2.2 一节里先阐述一下rip寄存器,另外,我们强调一下我们看到的汇编一般都是用的AT&T格式,也就是src在前,dst在后。
2.2 x86的rip寄存器
如下图,%rip是指向到了运行的指令的地址上:
rip寄存器,主要是方便寻址,因为无论是so库里还是bin里,最终运行到的虚拟地址在编译器编译时并不知道,但是指令与指令的地址差,也就是相对偏移基本上都是能确定的,所以在汇编里经常使用%rip加一个相对偏移来进行寻址。
往下执行一句汇编以后,%rip寄存器跟着变化,指向到了当前新的执行的指令所在地址上:
2.2.1 调试过程中看到的rip寄存器的值有时候并没有按照预期跟着代码执行而变化
在继续调试的过程中,我们发现%rip寄存器的在执行了call xxx之后,没有跟着变,但是,这仅仅是一个显示上的问题,实际%rip寄存器的值是跟着变的。
下面的容易让人产生%rip寄存器值没有跟着变的误导的截图:
下图是执行了call的截图:
%rip寄存器并没有和当前正在执行的指令的地址的值一致:
而是指向到了调用的call xxx的指令的下一条指令上:
2.2.2 事实上,rip寄存器真实数值一值是跟着变的
在调试过程中,上面的观测情况会让人产生误导,事实上,%rip是跟着变的,我们可以通过反汇编的内容来佐证,来看下面的截图:
我们先看第一条movaps %xmm0,0x17d9d2(%rip)指令:
这条指令的右边的提示:是调试器编译器给我们算出的实际的0x17d9d2(%rip)的地址,是0x7ffff7228300,我们用0x7ffff7228300减去0x17d9d2,得到的是:
恰好就是下一条指令的地址0x00007ffff70aa92e。
再看下一条指令movaps %xmm0,0x17d9db(%rip):
一样的,也是下一条指令的地址:
另外,我们写了一个程序来验证%rip是否是跟着变,下图是main里进行了一级调用,即调用call后,打印出%rip的值的程序代码,无论是gdb显示的%rip的值还是打印出来的%rip的值都是跟着变的:
但是要注意的是,gdb断点下来的位置是执行箭头所指的指令前那一刻的状态,如下图,就是在执行“lea 0x0(%rip),%rax”汇编指令去捞%rip寄存器的值的时刻:
所以,这里有一个关于%rip值的观测和实际引用的数值上的偏差,我们要观测到在实际引用时引用到的%rip的数值,得是断点断在或者执行下一条指令前的时刻才能观测到真正引用%rip的%rip的数值。
如下图可以看到%rip确实是跟着变的,指向的是下一条指令的地址:
复现用的程序代码:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstdlib>
#include <cstdio>
#include <time.h>
#include <string.h>int test1111()
{unsigned long rip_value;__asm__ volatile ("lea (%%rip), %0" // 使用 LEA 指令获取 RIP 的值: "=r" (rip_value) // 输出到 rip_value 变量);printf("RIP value: %lx\n", rip_value);printf("sizeof(unsigned)=%d\n", sizeof(unsigned));__asm__ volatile ("lea (%%rip), %0" // 使用 LEA 指令获取 RIP 的值: "=r" (rip_value) // 输出到 rip_value 变量);printf("RIP value: %lx\n", rip_value);
}int main()
{test1111();return 1;
}
2.3 对比没有执行过malloc和执行了一次malloc后的跳转表的数值变化
扫清了bnd jmp *0x18762d(%rip)里的rip寄存器的细节后,我们继续单步跟踪。
我们看一下在执行malloc@plt里的bnd jmp *0x18762d(%rip) # 0x7ffff7226708 <malloc@got.plt>指令之前,跳转的位置相关的数值:
跳转到了这里:
这时候,可能还没有什么感觉,我们来对比一下,执行了一次malloc时之后,是否还跳转到这里,以及相关地址上的数值是否有变化。
看一下下一次malloc时是否还是跳转到这里:
调试发现,下一次malloc就到了main之后了,我们程序里是下图这里:
进入反汇编窗口进行单步执行来跟踪,由下图的malloc@plt的位置里:
直接跳转到了jemalloc的malloc函数里了:
为什么呢?我们来看一下malloc@plt里bnd jmp *0x18762d(%rip) # 0x7ffff7226708这句里,要跳转到的0x7ffff7226708地址所存放的位置上的数值(因为bnd jmp 后面是有一个*的),对于第一次malloc执行时,0x7ffff7226708地址上的值如下,是0x7ffff709ae10:
从上图可以看到跳转到去执行的地址确实是0x7ffff709ae10。
下图是执行到jemalloc的malloc函数里时,0x7ffff7226708地址上的值,红色表示发生变化了,变成了0x7ffff7859815:
而0x7ffff7859815正时jemalloc的malloc的函数入口:
所以,在第一次执行malloc函数时,系统其实也就是glibc变动了跳转的位置,变动成了jemalloc里的malloc的函数首地址。
2.4 跟踪一下glibc里是如何变动这个跳转表的数值的
我们来看一下glibc是如何进行这个变动操作的,来单步执行,看第一次malloc时跳转到的0x7ffff709ae10地址后具体做了什么:
0x7ffff709ae10地址执行了以后,马上就跳转到了0x7ffff709a020地址,
然后又马上跳转到了0x7ffff7226010地址上的数值指向的地址:
0x7ffff7226010地址上的值是0x7ffff7fd8d30:
而0x7ffff7fd8d30地址其实就是_dl_runtime_resolve_xsavec:
关于_dl_runtime_resolve_xsavec我们在第三章里展开。
三、关于_dl_runtime_resolve和_dl_fixup函数
我们继续第二章里的分析。
3.1 第二章最后看到的_dl_runtime_resolve_xsavec函数即源码里的_dl_runtime_resolve
_dl_runtime_resolve_xsavec函数其实就是对应的源码里的_dl_runtime_resolve:
而_dl_runtime_resolve是arch相关的,我们是x86_64平台,所以就是glibc代码里的sysdeps/x86_64/dl-trampoline.h里的_dl_runtime_resolve汇编执行体,我们可以通过搜索jmp *%r11来百分百确定是用sysdeps/x86_64/dl-trampoline.h里的实现:
执行的是如下这段源码里的汇编:
3.2 _dl_runtime_resolve汇编代码里调用了_dl_fixup函数
_dl_runtime_resolve的汇编代码里,去掉cfi开头的语句(指令和函数检测有关,GNU Profiler)后,剩下了逻辑里就是保存寄存器的值到栈中:
然后调用_dl_fixup函数,第一个入参是link_map,第二个入参是reloc_index,这个是GOT表中关于PLT重定位的索引值,即PLT表中的第几项:
3.3 _dl_fixup函数的分析
_dl_fixup函数是在glibc里的elf/dl-runtime.c里的。
进入到_dl_fixup函数以后,从入参的link_map里可以看到,库路径就是libstdc++这个库的路径:
因为调试的这次调用malloc的源头是在glibc里的异常用pool构造函数里的malloc触发的。
3.3.1 从传入的link_map里获取so的符号表symtab和字符串表strtab和相关符号的elf条目的指针reloc
_dl_fixup函数先从link_map里获取so的符号表symtab和字符串表strtab:
然后通过传入的reloc_arg这个plt表的序号找到相关的符号的elf条目的指针reloc:
该PLTREL有两个成员:
3.3.2 通过reloc的r_offset计算得到要修改的函数所要改的地址rel_addr
r_offset加上link_map的l_addr得到的就是要修改的函数所要改的地址,即该函数对应GOT表的地址:
这个rel_addr是在后面会传给elf_machine_fixup_plt进行修正:
我们看一下上面传入给elf_machine_fixup_plt函数的value变量是怎么获取到要修改成的函数目标地址的。
3.3.3 传入给elf_machine_fixup_plt函数的value是怎么计算得到的
这里说的value变量,也就是要修改成的函数目标地址。
回到刚才说的得到的“plt表的序号找到相关的符号的elf条目的指针”reloc变量,通过reloc变量也就是PLTREL里的r_info作为symbol的index去到symtab这个符号表里去找条目,得到我们关心的这条条目即sym变量:
根据sym里的st_other变量:
判断其符号的可见性,是否是STV_DEFAULT也就是0:
如果不是0,则直接用link_map里的l_addr这个装载地址加上sym里的st_value,得到value
(link_map里的l_addr是指elf里的地址和实际模块所装载到的虚拟地址之间的差值)
(sym里的st_value是指符号地址相对于模块基址的偏移值)
上图中的DL_FIXUP_MAKE_VALUE宏即:
如果sym的st_other不是0的话,就表示传入的link_map和sym都已经是目标函数的信息了,因此可以直接计算目标函数的地址。
如果sym的st_other是0的话,一般都是0,则用_dl_lookup_symbol_x来算出lookup_t也就是link_map*类型的result,再用result来用DL_FIXUP_MAKE_VALUE宏来计算得到最终的value:
3.3.4 _dl_lookup_symbol_x函数
_dl_lookup_symbol_x函数遍历所有的scope,通过do_lookup_x函数在每个scope中查找符号,将查找的结果记录在current_value中:
在do_lookup_x函数里,获取scope下的link_map个数r_nlist和数组r_list:
遍历所有的link_map,通过check_match函数比较符号表中的函数名symtab[symidx]和待查找的函数名undef_name是否一致:
如果相等,就找到了该符号并跳转到found_it语句,否则返回null:
如果找到的是弱符号STB_WEAK,则保存第一次找到的结果,然后继续查找,如果后面没有找到可以覆盖该结果的,则返回这第一次保存的结果:
如果找到的是全局符号STB_GLOBAL,则直接返回该结果: