vdso内核与glibc配合的相关逻辑分析
一、背景
在之前的 vdso概念及原理,vdso_fault缺页异常,vdso符号的获取-CSDN博客 博客里,我们分析了vdso的概念及内核部分的原理,并通过讲vdso_fault和vvar_fault两个vdso相关的缺页异常的处理函数来加深vdso实现的细节处的理解,也同时对内存缺页异常处理的逻辑细节进行了一定的拓展,最后也讲解了vdso符号要如何获取,涉及到了vdso代码段还有数据段,还有vsyscall的段这些内容对应的符号的获取。
这篇博客我们继续vdso的原理介绍,侧重于vdso与glibc的配合部分,也会涉及相关的与内核部分配合的逻辑。
二、clock_gettime的实现的调试跟踪
我们在用户态代码里调用clock_gettime函数:
单步+反汇编窗口通过vs2019的gdb调试(具体vs2019的gdb调试见之前的博客 linux上对于so库的调试——包含通过vs2019远程ssh调试so库_vs linux 远程调试so文件-CSDN博客):
调用到的glibc里的sysdeps/unix/sysv/linux/clock_gettime.c里:
然后走到了clock_gettime.c里的42行,如下图:
所以,说明HAVE_CLOCK_GETTIME64_VSYSCALL是有值的,x86下定义的是:
且vdso_time64也是有值的,它是通过GLRO(dl_vdso_clock_gettime64)来得到:
而GLRO(dl_vdso_clock_gettime64)是在dl-vdso-setup.h里的setup_vdso_pointers里进行的赋值:
是通过dl_vdso_vsym函数来拿HAVE_CLOCK_GETTIME64_VSYSCALL也就是__vdso_clock_gettime的符号:
三、auxiliary vector机制与AT_SYSINFO_EHDR
auxiliary vector是用户态和内核态通讯的一种机制,它是一系列键值对的列表,在内核加载应用程序时会将其存储在与用户栈临近的地址空间上。如下图:
这个auxiliary vector数组里有一项与vdso有关,即AT_SYSINFO_EHDR,内核代码里有关于它的注释,如下图:
3.1 内核里与auxiliary vector机制的相关细节
在mm_types.h里定义mm_struct结构体里有如下定义saved_auxv数组:
如上图,saved_auxv数组对应于/proc/PID/auxv。
3.1.1 create_elf_tables函数设置了该saved_auxv数组,用了ARCH_DLINFO宏
在binfmt_elf.c里定义的create_elf_tables函数里有该saved_auxv数组的赋值,如下图:
先是定义了NEW_AUX_ENT宏,然后用ARCH_DLINFO宏,而ARCH_DLINFO宏用了NEW_AUX_ENT宏设置了AT_SYSINFO_EHDR的值。
ARCH_DLINFO宏如下图:
3.1.2 设置AT_SYSINFO_EHDR的值为current->mm->context.vdso
如上图,设置AT_SYSINFO_EHDR的值为current->mm->context.vdso。
而该current->mm->context.vdso的值在之前的博客 vdso概念及原理,vdso_fault缺页异常,vdso符号的获取-CSDN博客 里的 2.6一节里有相关调用链的介绍里的map_vdso函数里做了该current->mm->context.vdso的设置:
exec_binprm->do_execveat_common->bprm_execve->exec_binprm->search_binary_handler->load_elf_binary->ARCH_SETUP_ADDITIONAL_PAGES宏->arch_setup_additional_pages->map_vdso_randomized->map_vdso->_install_special_mapping
map_vdso里设置current->mm->context.vdso的截图:
3.1.3 create_elf_tables函数是在load_elf_binary里的ARCH_SETUP_ADDITIONAL_PAGES宏之后运行
而 3.1.1 里讲的 create_elf_tables 函数在下面的调用链里的下面调用链里的load_elf_binary函数里的ARCH_SETUP_ADDITIONAL_PAGES宏之后运行的。
exec_binprm->do_execveat_common->bprm_execve->exec_binprm->search_binary_handler->load_elf_binary->ARCH_SETUP_ADDITIONAL_PAGES宏->arch_setup_additional_pages->map_vdso_randomized->map_vdso->_install_special_mapping
3.2 execve系统调用的内核逻辑里设置新程序入口和载入解释器逻辑
execve系统调用在load_elf_binary里执行完ARCH_SETUP_ADDITIONAL_PAGES和create_elf_tables之后,还执行了两个重要逻辑,即设置新程序入口,和载入解释器逻辑。
为什么要在分析这两个逻辑,因为这两个逻辑决定了用户态的代码在execve系统调用返回用户态之后从哪里开始执行,如何执行。
3.2.1 在load_elf_binary里执行START_THREAD宏用新的ip和sp
在load_elf_binary里执行完ARCH_SETUP_ADDITIONAL_PAGES和create_elf_tables之后,执行了START_THREAD宏:
在elf.h里有如下定义:
看一下start_thread_common的实现:
如上图,会用new_ip和new_sp来替换当前的ip和sp。这样execve系统调用返回用户态时就能进入新的程序入口了。
而new_ip和new_sp即传入START_THREAD宏里的elf_entry和bprm->p:
3.2.2 load_elf_binary里载入解释器的逻辑
上图里的elf_entry是程序的入口地址,在有用到动态链接库时是解释器的入口地址,如果没有用到动态链接器elf_entry则是程序本身的入口地址。
load_elf_binary里与载入解释器相关的逻辑如下:
即遍历elf里程序头表条目,找到是PT_INTERP类型的,就是解释器段的类型。
如果是解释器段,就读取这个条目的内容到elf_interpreter数组里:
然后调用open_exec(传入解释器文件的路径,解释器文件路径如/lib64/ld-linux-x86-64.so.2),得到打开文件后的file指针。这个open_exec函数里会通过do_open_execat打开,do_open_execat函数实现里会设置一些flags属性再打开,再设置file的deny write access:
通过open_exec(elf_interpreter)得到解释器的file指针赋值给interpreter后,通过load_elf_interp函数通过解释器来加载elf,如果没有用到动态链接库,即解释器file指针为NULL时则直接把elf的entry给到elf_entry:
四、glibc里的dynamic linker的逻辑和_dl_non_dynamic_init
为什么看glibc里的dynamic linker的逻辑和_dl_non_dynamic_init的逻辑是因为vdso的base地址以及如何拿到vdso里的一些函数地址和vdso数据的共享内存地址都是由这一章里介绍的dynamic linker的逻辑和_dl_non_dynamic_init的逻辑调用到的。
4.1 glibc里的dynamic linker逻辑
4.1.1 从_dl_start开始到setup_vdso的完整的调用链
从_dl_start开始到vdso的base地址获取的完整的调用链:
_dl_start->_dl_start_final->_dl_sysdep_start->dl_main->setup_vdso
dynamic linker逻辑由elf/rtld.c里的_dl_start开始:
调用了_dl_start_final函数:
在_dl_start_final函数里调用了_dl_sysdep_start:
在_dl_sysdep_start函数里调用了传入给_dl_sysdep_start函数的cb:dl_main
在dl_main中,会调用与vdso相关的两个函数setup_vdso和setup_vdso_pointers:
我们会在下面 4.1.3 里继续分析这两个vdso相关的函数,setup_vdso和setup_vdso_pointers。在分析setup_vdso和setup_vdso_pointers之前,我们先分析一下这两个函数会用到的GLRO(dl_sysinfo_dso)值的获取:
4.1.2 GLRO(dl_sysinfo_dso)值的设置
GLRO(dl_sysinfo_dso)值在setup_vdso函数里会使用到,这个dl_sysinfo_dso是有关vdso这个so的一些具体信息。这些信息属于so的elf的header里的信息,在内核的工具里有相关信息的设置,这些信息最终都是在编译内核的步骤里完成这个vdso的这个so的完整二进制的组装。相关的如e_phoff和e_phnum如下图,在内核代码arch/x86/tools/relocs.c里进行了设置:
内核逻辑把vdso代码及vvar数据映射到用户态之后(之前的博客 vdso概念及原理,vdso_fault缺页异常,vdso符号的获取_x86架构的vdso-CSDN博客 2.6 一节),通过auxv辅助数组传递给用户态,最终在上面 4.1.1 里介绍的下面的调用链里的_dl_sysdep_start函数里设置的dl_sysinfo_dso:
_dl_start->_dl_start_final->_dl_sysdep_start->dl_main->setup_vdso
dl_sysinfo_dso这个数值其实是一个地址,这个地址是一个基地址,根据这个基地址就可以找到与vdso这个so相关的更具体的信息。
在上图的这个设置之后,在_dl_sysdep_start里的最后,调用了dl_main,在dl_main里调用了setup_vdso。在下一节,我们讲setup_vdso和setup_vdso_pointers两个函数,然后在 4.1.4 一节,我们讲上图里GLRO(dl_sysinfo_dso)的设置所通过的下图里的循环里的GLRO(dl_auxv)是如何获取正确的值的:
4.1.3 setup_vdso和setup_vdso_pointers
setup_vdso函数是根据上面 4.1.2 里说的已经完成初始化了的dl_sysinfo_dso,来进行vdso这个so的link_map的初始化,初始化完成之后,设置到GLRO(dl_sysinfo_map)里去,如下图:
我们看一下这个struct link_map *l是如何创建和初始化的:
如果dl_sysinfo_dso非NULL,则创建一个link_map对象,并设置下图里的__RTLD_VDSO标志位,表示这个so是system_loaded的。
然后把dl_sysinfo_dso这个vdso的base地址根据数据协议找elf header里的信息,填充到刚创建的这个link_map对象里去:
然后遍历这些phnum项,进行link_map里更多信息的设置:
接下来,我们看一下setup_vdso_pointers这个函数:
setup_vdso_pointers函数主要就是用dl_vdso_vsym去根据符号名找函数指针。
我们看dl_vdso_vsym函数的实现:
其实,有了上面的分析,我们其实可以看得懂下面这个dl_vdso_vsym函数的实现的:
就是通过之前完成初始化赋值的这个link_map的对象dl_sysinfo_map,去根据里面的信息,通过dl_lookup_symbol_x函数去根据名字去找里面的符号的函数地址。
4.1.4 START_THREAD宏传入的bprm->p到GLRO(dl_auxv)
在上面 3.2.1 一节里我们介绍了START_THREAD宏,内核里使用START_THREAD宏来设置pt_regs里的ip和sp的调用地方load_elf_binary函数里:
传入的bprm->p的设置的地方是,在create_elf_tables里:
这个传给用户态的这个bprm->p就是arg的地址。
再来看用户态的从_dl_start的入参(void *arg)到到_dl_start_final的入参(void *arg)再到_dl_sysdep_start的入参(void **start_argptr)也是一级级传下来的。
在_dl_sysdep_start函数里,会根据传入的start_argptr,得到计算出_dl_argc,_dl_argv,_environ等与进程上下文相关的一些关键数据:
如上图,这里面也包含了dl_auxv这个包含了vdso的base地址信息的辅助数组:
这里,GLRO宏仅仅是加了一个下划线:
4.2 glibc里的_dl_non_dynamic_init逻辑
上面 4.1 一节,我们其实已经把vdso相关的细节在介绍更常见的带so库运行程序的情景时全部讲到了。
对于不带so库运行程序的场景,vdso的两个相关初始化函数setup_vdso和setup_vdso_pointers也是会被执行到的,如下图: