最近一个月的时间大部分在研究glibc中dlopen的代码,基本上对整个流程建立了一个基本的了解。由于网上相关资料比较少,走了不少弯路,故在此记录一二,希望后人能够站在我这个矮子的肩上做出精彩的成果。

ELF格式简介

dlopen是用来加载ELF文件中的共享对象(shared object,下文简称为so)的。ELF文件有多种类别,通过其header中0x10处的两个字节标识,参考Wikipedia。ELF的header中还包含了一些额外信息如指令集、操作系统信息等等,在本文中不会涉及。

可以把一个ELF文件分为4块:header、program header(phdr) table、section header(shdr) table、sections。下图将其解释地比较清楚了:

其中,最重要的概念就是phdr与shdr,它们分别对应着segment与section这两个在dlopen过程中至关重要的概念,可以使用以下命令查看:

readelf -S lib1.so  #查看section信息
There are 33 section headers, starting at offset 0x20f8: Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.gnu.build-i NOTE 00000000000001c8 000001c8
0000000000000024 0000000000000000 A 0 0 4
[ 2] .gnu.hash GNU_HASH 00000000000001f0 000001f0
0000000000000050 0000000000000000 A 3 0 8
[ 3] .dynsym DYNSYM 0000000000000240 00000240
0000000000000198 0000000000000018 A 4 1 8
[ 4] .dynstr STRTAB 00000000000003d8 000003d8
00000000000000c5 0000000000000000 A 0 0 1
......

每一个section中存放不同用途的数据,以“.”开头,比如我们熟悉的.text,.data,.bss。

readelf -l lib1.so  #查看segment信息
Elf file type is DYN (Shared object file)
Entry point 0x600
There are 7 program headers, starting at offset 64 Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000007cc 0x00000000000007cc R E 0x200000
LOAD 0x0000000000000e00 0x0000000000200e00 0x0000000000200e00
0x0000000000000230 0x0000000000000288 RW 0x200000
DYNAMIC 0x0000000000000e10 0x0000000000200e10 0x0000000000200e10
0x00000000000001d0 0x00000000000001d0 RW 0x8
NOTE 0x00000000000001c8 0x00000000000001c8 0x00000000000001c8
0x0000000000000024 0x0000000000000024 R 0x4
GNU_EH_FRAME 0x000000000000072c 0x000000000000072c 0x000000000000072c
0x0000000000000024 0x0000000000000024 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000000e00 0x0000000000200e00 0x0000000000200e00
0x0000000000000200 0x0000000000000200 R 0x1 Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
01 .init_array .fini_array .dynamic .got .got.plt .data .bss
02 .dynamic
03 .note.gnu.build-id
04 .eh_frame_hdr
05
06 .init_array .fini_array .dynamic .got

详细地显示了每个segment的类型、虚拟地址、物理地址、占文件空间(FileSiz)、占内存空间(MemSiz)、保护模式、对齐信息,以及每一个segment包含哪些section。

一句话概括,不同意义的信息存储在不同的section中,数个section聚合为一个segment。在加载时,我们只关心segment。

dlopen的代码结构

dlopen定义在头文件dlfcn.h中,但其实现横跨了dlfcn/与elf/两个文件夹,且涉及了多个文件与函数,相当复杂。下面简单分析其调用流程:

(in dlfcn/dlopen.c)dlopen -> __dlopen -> dlopen_doit -> (in elf/dl-open.c) _dl_open -> dl_open_worker -> (in dl-load.c) _dl_map_object -> _dl_map_object_from_fd

(in elf/dl-map-segments.h) _dl_map_segments -> __mmap -> 系统调用

这样分配的原因可能是,dlfcn文件夹下的文件被编译为libdl.so,而elf文件夹下的文件部分被编译成ld.so,部分被编译为libc.so。有些接口与成员只能在ld.so内被使用,如下面的例子:

In include/link.h:

struct link_map
{
/* These first few members are part of the protocol with the debugger.
This is the same format used in SVR4. */ ElfW(Addr) l_addr; /* Difference between the address in the ELF
file and the addresses in memory. */
char *l_name; /* Absolute file name object was found in. */
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */ /* All following members are internal to the dynamic linker.
They may change without notice. */ /* This is an element which is only ever different from a pointer to
the very same copy of this type for ld.so when it is used in more
than one namespace. */
struct link_map *l_real;
......

所以,因为在libdl.so中不能访问到某些元素,决定了dlopen不能只在dlfcn/下实现,所以真正的工作需要elf/中的文件进行实现,类似于帮助dlopen干活的工人,即dl_open_worker。而dlfcn/中的部分主要负责配置参数与错误处理。

dlopen实现详解

注:此处只对dlopen的主干进行解释,没有涉及边界条件以及次要部分(如加载一个so的依赖等)

dlopen

void *
dlopen (const char *file, int mode)
{
return __dlopen (file, mode, RETURN_ADDRESS (0));
}

为用户提供调用的接口,调用实际进行工作的函数__dlopen

__dlopen

struct dlopen_args
{
/* The arguments for dlopen_doit. */
const char *file;
int mode;
/* The return value of dlopen_doit. */
void *new; //返回一个地址,即加载完成之后返回handle的地址
/* Address of the caller. */
const void *caller;
}; void *
__dlopen (const char *file, int mode DL_CALLER_DECL)
{
# ifdef SHARED
if (!rtld_active ())
return _dlfcn_hook->dlopen (file, mode, DL_CALLER);
# endif struct dlopen_args args; //准备下一步调用的参数,装在这个struct中
args.file = file;
args.mode = mode;
args.caller = DL_CALLER; # ifdef SHARED
return _dlerror_run (dlopen_doit, &args) ? NULL : args.new; //_dlerror_run是用来错误处理的外层函数,接受一个函数指针与一个dlopen_args
//在这个函数内部,dlopen_doit接受以参数args运行,在其执行结束之后取出args.new
# else
if (_dlerror_run (dlopen_doit, &args))
return NULL; __libc_register_dl_open_hook ((struct link_map *) args.new); //与libc内部调用dlopen有关,非主干内容
__libc_register_dlfcn_hook ((struct link_map *) args.new); return args.new;
# endif
}

dlopen_doit

static void
dlopen_doit (void *a)
{
struct dlopen_args *args = (struct dlopen_args *) a; if (args->mode & ~(RTLD_BINDING_MASK | RTLD_NOLOAD | RTLD_DEEPBIND
| RTLD_GLOBAL | RTLD_LOCAL | RTLD_NODELETE
| __RTLD_SPROF))
_dl_signal_error (0, NULL, NULL, _("invalid mode parameter")); args->new = GLRO(dl_open) (args->file ?: "", args->mode | __RTLD_DLOPEN,
args->caller,
args->file == NULL ? LM_ID_BASE : NS,
__dlfcn_argc, __dlfcn_argv, __environ); //GLRO为预编译命令,此处调用_dl_open
//调用结束之后将args->new配置好
}

_dl_open

struct dl_open_args //同样是承载参数的结构
{
const char *file;
int mode;
/* This is the caller of the dlopen() function. */
const void *caller_dlopen;
struct link_map *map;
/* Namespace ID. */
Lmid_t nsid; /* Original value of _ns_global_scope_pending_adds. Set by
dl_open_worker. Only valid if nsid is a real namespace
(non-negative). */
unsigned int original_global_scope_pending_adds; /* Original parameters to the program and the current environment. */
int argc;
char **argv;
char **env;
}; void *
_dl_open (const char *file, int mode, const void *caller_dlopen, Lmid_t nsid,
int argc, char *argv[], char *env[])
{
...... struct dl_open_args args;
args.file = file;
args.mode = mode;
args.caller_dlopen = caller_dlopen;
args.map = NULL;
args.nsid = nsid;
args.argc = argc;
args.argv = argv;
args.env = env; struct dl_exception exception;
int errcode = _dl_catch_exception (&exception, dl_open_worker, &args); //与上面的_dlerror_run类似,是一个接受参数并处理错误的wrapper

dl_open_worker

static void
dl_open_worker (void *a)
{
struct dl_open_args *args = a; //创建临时变量承载参数
const char *file = args->file;
int mode = args->mode;
struct link_map *call_map = NULL;
......
/* Load the named object. */
struct link_map *new; //创建一个新的link_map,用来存放要加载的so
args->map = new = _dl_map_object (call_map, file, lt_loaded, 0,
mode | __RTLD_CALLMAP, args->nsid); //开始将so映射到内存中去
......
}

_dl_map_object

struct link_map *
_dl_map_object (struct link_map *loader, const char *name,
int type, int trace_mode, int mode, Lmid_t nsid)
{
......
//主要在寻找是否存在已经打开了的so,如果有,直接将对应的link_map返回
return _dl_map_object_from_fd (name, origname, fd, &fb, realname, loader,
type, mode, &stack_end, nsid); //用一个fd开始进行内存映射

_dl_map_object_from_fd

struct link_map *
_dl_map_object_from_fd (const char *name, const char *origname, int fd,
struct filebuf *fbp, char *realname,
struct link_map *loader, int l_type, int mode,
void **stack_endp, Lmid_t nsid)
{
......
{
/* Scan the program header table, collecting its load commands. */
struct loadcmd loadcmds[l->l_phnum]; //loadcmd中每一个元素对应elf中的一个segment,所以它的长度等于elf中phdr的个数
size_t nloadcmds = 0; //并非loadcmd的长度,而是LOAD类segment的个数,见下文
bool has_holes = false; for (ph = phdr; ph < &phdr[l->l_phnum]; ++ph)
switch (ph->p_type)
{
case PT_DYNAMIC: //别的类型的segment,可以无视
......
case PT_PHDR:
......
case PT_LOAD: //最重要的类型,每一个LOAD segment都要被加载进内存
......
struct loadcmd *c = &loadcmds[nloadcmds++]; //只有PT_LOAD类型才会增加nloadcmds
c->mapstart = ALIGN_DOWN (ph->p_vaddr, GLRO(dl_pagesize)); //获得映射的开始地址,由于直接与虚拟内存对应,需要页对齐
c->mapend = ALIGN_UP (ph->p_vaddr + ph->p_filesz, GLRO(dl_pagesize)); //获取结束地址
c->dataend = ph->p_vaddr + ph->p_filesz; //filesz与memsz只在一种情况时不同,见下文。
c->allocend = ph->p_vaddr + ph->p_memsz;
c->mapoff = ALIGN_DOWN (ph->p_offset, GLRO(dl_pagesize)); if (nloadcmds > 1 && c[-1].mapend != c->mapstart) // 当一个LOAD类型的开始地址与上一个LOAD的结束地址不同时,判定为有洞
has_holes = true;
/* Now process the load commands and map segments into memory.
This is responsible for filling in:
l_map_start, l_map_end, l_addr, l_contiguous, l_text_end, l_phdr
*/
errstring = _dl_map_segments (l, fd, header, type, loadcmds, nloadcmds,
maplength, has_holes, loader); //将整理好的loadcmds作为参数,开始进行真正的映射
}
}
......
}

这里的switch与上文中讲的segment的类型相对应,不同的segment对应不同的操作。只有segment类型为PT_LOAD的才会放到loadcmds中,加载到内存中去。loadcmds也是在这里配置完毕的。

_dl_map_segments

static __always_inline const char *
_dl_map_segments (struct link_map *l, int fd,
const ElfW(Ehdr) *header, int type,
const struct loadcmd loadcmds[], size_t nloadcmds,
const size_t maplength, bool has_holes,
struct link_map *loader)
{
......
ElfW(Addr) mappref
= (ELF_PREFERRED_ADDRESS (loader, maplength,
c->mapstart & GLRO(dl_use_load_bias))
- MAP_BASE_ADDR (l)); //mmap的第一个参数接受一个preferred location,一般来说这个值都是0,即由OS决定基地址 l->l_map_start = (ElfW(Addr)) __mmap ((void *) mappref, maplength,
c->prot,
MAP_COPY|MAP_FILE,
fd, c->mapoff); //注意此处MAP_FIXED flag没有打开,不会分配到固定地址
......
if (has_holes)
{
/* Change protection on the excess portion to disallow all access;
the portions we do not remap later will be inaccessible as if
unallocated. Then jump into the normal segment-mapping loop to
handle the portion of the segment past the end of the file
mapping. */
if (__glibc_unlikely
(__mprotect ((caddr_t) (l->l_addr + c->mapend),
loadcmds[nloadcmds - 1].mapstart - c->mapend,
PROT_NONE) < 0)) //使用mprotect改变上文中提到的“洞”的访问权限为不允许任何访问
return DL_MAP_SEGMENTS_ERROR_MPROTECT;
}
while (c < &loadcmds[nloadcmds])
{
if (c->mapend > c->mapstart //mapend > mapstart是expected behavior
/* Map the segment contents from the file. */
&& (__mmap ((void *) (l->l_addr + c->mapstart),
c->mapend - c->mapstart, c->prot,
MAP_FIXED|MAP_COPY|MAP_FILE, //后续的segment被映射到固定的地址,从前一个的结束地址开始
fd, c->mapoff)
== MAP_FAILED)) //当mmap出错时,退出;否则就是正常的mmap loadcmds中下一个segment
return DL_MAP_SEGMENTS_ERROR_MAP_SEGMENT;
......
if (c->allocend > c->dataend) //这个条件用来判断是否进入了最后一个LOAD
{
/* Extra zero pages should appear at the end of this segment,
after the data mapped from the file. */ //在最后一个segment中,没有被用到的部分用0填充
ElfW(Addr) zero, zeroend, zeropage; zero = l->l_addr + c->dataend; //.data section的结束
zeroend = l->l_addr + c->allocend; //.bss section的结束
zeropage = ((zero + GLRO(dl_pagesize) - 1)
& ~(GLRO(dl_pagesize) - 1)); //.data section结束地址的下一页的开始地址
if (zeroend < zeropage)
/* All the extra data is in the last page of the segment.
We can just zero it. */
zeropage = zeroend; if (zeropage > zero)
{
/* Zero the final part of the last page of the segment. */
if (__glibc_unlikely ((c->prot & PROT_WRITE) == 0))
{
/* Dag nab it. */
if (__mprotect ((caddr_t) (zero
& ~(GLRO(dl_pagesize) - 1)),
GLRO(dl_pagesize), c->prot|PROT_WRITE) < 0)
return DL_MAP_SEGMENTS_ERROR_MPROTECT;
}
memset ((void *) zero, '\0', zeropage - zero);
if (__glibc_unlikely ((c->prot & PROT_WRITE) == 0))
__mprotect ((caddr_t) (zero & ~(GLRO(dl_pagesize) - 1)),
GLRO(dl_pagesize), c->prot);
} if (zeroend > zeropage) //当.bss section的长度超过最后一页的剩余长度时,此时需要新增若干页,需要再次调mmap
{
/* Map the remaining zero pages in from the zero fill FD. */
caddr_t mapat;
mapat = __mmap ((caddr_t) zeropage, zeroend - zeropage,
c->prot, MAP_ANON|MAP_PRIVATE|MAP_FIXED, //MAP_ANON打开,因为建立的映射不对应于任何一个fd
-1, 0);
if (__glibc_unlikely (mapat == MAP_FAILED))
return DL_MAP_SEGMENTS_ERROR_MAP_ZERO_FILL;
}
}
++c; //loadcmds中下一条命令
}

这是最重要,最复杂的一个函数,也是dlopen最底层的系统调用。它的工作流程如下:

  1. 没有特殊情况时,mappref为0,由OS自行选择基地址,并将其返回
  2. 后续的segment紧接着这个地址进行映射
  3. 到达最后一个segment时,需要处理allocend和dataend的情况,由.bss section引起

此处结合ELF文件的格式,讲解为什么.bss section有这样的情况:

回顾上文中lib1.so的phdr table:

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000007cc 0x00000000000007cc R E 0x200000
LOAD 0x0000000000000e00 0x0000000000200e00 0x0000000000200e00
0x0000000000000230 0x0000000000000288 RW 0x200000
DYNAMIC 0x0000000000000e10 0x0000000000200e10 0x0000000000200e10
0x00000000000001d0 0x00000000000001d0 RW 0x8
NOTE 0x00000000000001c8 0x00000000000001c8 0x00000000000001c8
0x0000000000000024 0x0000000000000024 R 0x4
GNU_EH_FRAME 0x000000000000072c 0x000000000000072c 0x000000000000072c
0x0000000000000024 0x0000000000000024 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000000e00 0x0000000000200e00 0x0000000000200e00
0x0000000000000200 0x0000000000000200 R 0x1

只有第二个LOAD中出现了FileSiz != MemSiz的情况。这是因为,在ELF中需要存储全局变量的初始值,而由于.bss没有初始值,默认被初始化为0,所以不会在ELF中存储,使得变量在文件中占用的大小(FileSiz)小于运行时占用的内存空间(MemSiz)。在加载到内存中时,使用这个特征判断是否到达了最后一个LOAD segment。

同时,可以注意到两个LOAD之间的虚拟地址(即加载到虚拟内存中时的偏移量,上文中的VirtAddr)差距很大,这是因为想要尽量保证可执行的部分与不可执行的部分相差尽可能大,从而最小化溢出时可能造成的写掉.text的风险,见出处。这也是上文中“洞”的由来。

在笔者所做的实验中,所有so都只有两个LOAD segment,一个是可执行的,另一个是不可执行的,包含的section见上文输出。然而,在某些系统上,可能会有其它的聚合方式,详见这个例子。这与系统产生ELF文件的实现有关。

关于link_map

link_map是用来存储ELF文件的数据结构,其详细定义可以在include/link.h下找到。

dlopen返回的打开的so的handle。这个handle是一个可以被其它libdl函数使用的接口,如dlsym,dlclose。需要注意它与so不存储在一起,也不是so在内存中的基地址。

结语

时间仓促,dlopen的实现只挑了主干研究,其它部分还没空顾及,一些支撑我得到结论的实验也没有放上来。希望能与各路大神深入交流。

dlopen代码详解——从ELF格式到mmap的更多相关文章

  1. ARM Cortex-M底层技术(2)—启动代码详解

    杂谈 工作了一天,脑袋比较乱.一直想把底层的知识写成一个系列,希望可以坚持下去.为什么要写底层的东西呢?首先,工作用到了这部分内容,最近和内部Flash打交道比较多,自然而然会接触到一些底层的东西:第 ...

  2. Log4j使用详解(log4j.properties格式)

    Log4j使用详解(log4j.properties格式) 1.Log4j 的引入 在应用程序中添加日志记录总的来说基于三个目的: ① 监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计 ...

  3. python golang中grpc 使用示例代码详解

    python 1.使用前准备,安装这三个库 pip install grpcio pip install protobuf pip install grpcio_tools 2.建立一个proto文件 ...

  4. BM算法  Boyer-Moore高质量实现代码详解与算法详解

    Boyer-Moore高质量实现代码详解与算法详解 鉴于我见到对算法本身分析非常透彻的文章以及实现的非常精巧的文章,所以就转载了,本文的贡献在于将两者结合起来,方便大家了解代码实现! 算法详解转自:h ...

  5. ASP.NET MVC 5 学习教程:生成的代码详解

    原文 ASP.NET MVC 5 学习教程:生成的代码详解 起飞网 ASP.NET MVC 5 学习教程目录: 添加控制器 添加视图 修改视图和布局页 控制器传递数据给视图 添加模型 创建连接字符串 ...

  6. Github-karpathy/char-rnn代码详解

    Github-karpathy/char-rnn代码详解 zoerywzhou@gmail.com http://www.cnblogs.com/swje/ 作者:Zhouwan  2016-1-10 ...

  7. 代码详解:TensorFlow Core带你探索深度神经网络“黑匣子”

    来源商业新知网,原标题:代码详解:TensorFlow Core带你探索深度神经网络“黑匣子” 想学TensorFlow?先从低阶API开始吧~某种程度而言,它能够帮助我们更好地理解Tensorflo ...

  8. JAVA类与类之间的全部关系简述+代码详解

    本文转自: https://blog.csdn.net/wq6ylg08/article/details/81092056类和类之间关系包括了 is a,has a, use a三种关系(1)is a ...

  9. Java中String的intern方法,javap&cfr.jar反编译,javap反编译后二进制指令代码详解,Java8常量池的位置

    一个例子 public class TestString{ public static void main(String[] args){ String a = "a"; Stri ...

随机推荐

  1. 2020牛客暑假多校训练营 第二场 E Exclusive OR FWT

    LINK:Exclusive OR 没做出 原因前面几篇说过了. 根据线性基的知识容易推出 不超过\(w=log Mx\)个数字即可拼出最大值 其中Mx为值域. 那么考虑w+2个数字显然也为最大值.. ...

  2. CF掉分日记 6.6 6.8

    ---恢复内容开始--- 写的效果依旧不好 还没写完前四题比赛就结束了 而且这些普及组的题目 我大多还是缺少简单算法的灵性 总是把问题搞复杂化. 6.5 A 第一道题非常水 简单分析发现是一个快速幂的 ...

  3. luogu3706 [SDOI2017]硬币游戏

    LINK:硬币游戏 对于40分的暴力 构造出AC自动机 列出转移矩阵 暴力高消.右转上一篇文章. 对于100分 我们不难想到这个矩阵过大 且没有用的节点很多我们最后只要n个节点的答案 其他节点的答案可 ...

  4. JS 常用方法汇总(不定期更新)

    /** * 获取当前日期 * @returns {string} */ Common.currentDate = function () { // 获取当前日期 var date = new Date ...

  5. stat 命令家族(1)- 详解 vmstat

    性能测试必备的 Linux 命令系列,可以看下面链接的文章哦 vmstat 介绍 Virtual Meomory Statistics,报告虚拟内存统计信息 会统计进程信息.内存.交换区.IO.磁盘. ...

  6. “随手记”开发记录day17

    继续开发账单的图形展示这一部分,丰富“随手记”的显示方法,对我们的APP进行添砖加瓦.

  7. Python解决网吧收费系统,远控网吧电脑设备!

    python破解网吧收费系统,远控网吧电脑设备! 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手.很多已经做案例的人,却不知道如何去学习更 ...

  8. 一个Python爬虫工程师学习养成记

    大数据的时代,网络爬虫已经成为了获取数据的一个重要手段. 但要学习好爬虫并没有那么简单.首先知识点和方向实在是太多了,它关系到了计算机网络.编程基础.前端开发.后端开发.App 开发与逆向.网络安全. ...

  9. jenkins集成spring boot持续化构建代码

    我个人使用的是阿里云的云服务器,项目采用的是spring boot为框架,现在要做的功能就是将本地开发的代码提交到github中,通过jenkins自动化集成部署到云服务器.接下来开始步骤. 1 首先 ...

  10. Go之Gorm和BeegoORM简介及配置使用

    简介 ORM Object-Relationl Mapping, 它的作用是映射数据库和对象之间的关系,方便我们在实现数据库操作的时候不用去写复杂的sql语句,把对数据库的操作上升到对于对象的操作 G ...