dlopen代码详解——从ELF格式到mmap
最近一个月的时间大部分在研究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最底层的系统调用。它的工作流程如下:
- 没有特殊情况时,mappref为0,由OS自行选择基地址,并将其返回
- 后续的segment紧接着这个地址进行映射
- 到达最后一个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的更多相关文章
- ARM Cortex-M底层技术(2)—启动代码详解
杂谈 工作了一天,脑袋比较乱.一直想把底层的知识写成一个系列,希望可以坚持下去.为什么要写底层的东西呢?首先,工作用到了这部分内容,最近和内部Flash打交道比较多,自然而然会接触到一些底层的东西:第 ...
- Log4j使用详解(log4j.properties格式)
Log4j使用详解(log4j.properties格式) 1.Log4j 的引入 在应用程序中添加日志记录总的来说基于三个目的: ① 监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计 ...
- python golang中grpc 使用示例代码详解
python 1.使用前准备,安装这三个库 pip install grpcio pip install protobuf pip install grpcio_tools 2.建立一个proto文件 ...
- BM算法 Boyer-Moore高质量实现代码详解与算法详解
Boyer-Moore高质量实现代码详解与算法详解 鉴于我见到对算法本身分析非常透彻的文章以及实现的非常精巧的文章,所以就转载了,本文的贡献在于将两者结合起来,方便大家了解代码实现! 算法详解转自:h ...
- ASP.NET MVC 5 学习教程:生成的代码详解
原文 ASP.NET MVC 5 学习教程:生成的代码详解 起飞网 ASP.NET MVC 5 学习教程目录: 添加控制器 添加视图 修改视图和布局页 控制器传递数据给视图 添加模型 创建连接字符串 ...
- Github-karpathy/char-rnn代码详解
Github-karpathy/char-rnn代码详解 zoerywzhou@gmail.com http://www.cnblogs.com/swje/ 作者:Zhouwan 2016-1-10 ...
- 代码详解:TensorFlow Core带你探索深度神经网络“黑匣子”
来源商业新知网,原标题:代码详解:TensorFlow Core带你探索深度神经网络“黑匣子” 想学TensorFlow?先从低阶API开始吧~某种程度而言,它能够帮助我们更好地理解Tensorflo ...
- JAVA类与类之间的全部关系简述+代码详解
本文转自: https://blog.csdn.net/wq6ylg08/article/details/81092056类和类之间关系包括了 is a,has a, use a三种关系(1)is a ...
- Java中String的intern方法,javap&cfr.jar反编译,javap反编译后二进制指令代码详解,Java8常量池的位置
一个例子 public class TestString{ public static void main(String[] args){ String a = "a"; Stri ...
随机推荐
- loj #6039 「雅礼集训 2017 Day5」珠宝 分组背包 决策单调性优化
LINK:珠宝 去年在某个oj上写过这道题 当时懵懂无知wa的不省人事 终于发现这个东西原来是有决策单调性的. 可以发现是一个01背包 但是过不了 冷静分析 01背包的复杂度有下界 如果过不了说明必然 ...
- JAVA设计模式 5【结构型】代理模式的理解与使用
今天要开始我们结构型 设计模式的学习,设计模式源于生活,还是希望能通过生活中的一些小栗子去理解学习它,而不是为了学习而学习这些东西. 结构型设计模式 结构型设计模式又分为 类 结构型 对象 结构型 前 ...
- Python编程初学者指南PDF高清电子书免费下载|百度云盘
百度云盘:Python编程初学者指南PDF高清电子书免费下载 提取码:bftd 内容简介 Python是一种解释型.面向对象.动态数据类型的高级程序设计语言.Python可以用于很多的领域,从科学计算 ...
- 【原创】xenomai与VxWorks实时性对比(资源抢占上下文切换对比)
版权声明:本文为本文为博主原创文章,转载请注明出处.如有问题,欢迎指正.博客地址:https://www.cnblogs.com/wsg1100/ (下面数据,仅供个人参考) 可能大部分人一直好奇Vx ...
- Java 添加、删除、格式化Word中的图片
本文介绍使用Spire.Cloud.SDK for Java提供的ImagesApi接口来操作Word中的图片.具体可通过addImage()方法添加图片.deleteImage()方法删除图片.up ...
- Idea 提交配置说明
Idea 提交配置说明# Auto-update after commit :自动升级后提交 keep files locked :把文件锁上,我想这应该就只能你修改其他开发人不能修改不了的功能 在你 ...
- 用了Dapper之后通篇还是SqlConnection,真的看不下去了
一:背景 1. 讲故事 前几天看公司一个新项目的底层使用了dapper,大家都知道dapper是一个非常强大的半自动化orm,帮程序员解决了繁琐的mapping问题,用起来非常爽,但我还是遇到了一件非 ...
- SonarQube 跳过指定检查
SonarQube 跳过指定检查 如何让 SonarQube 忽略某些检查规则 环境 演示环境参考前边的文章 SonarQube 扫描 Java 代码 步骤 我们已经扫描一个 Java 项目 有 6 ...
- C#LeetCode刷题之#496-下一个更大元素 I(Next Greater Element I)
问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/4026 访问. 给定两个没有重复元素的数组 nums1 和 num ...
- Springboot调用Oracle存储过程的几种方式
因工作需要将公司SSH项目改为Spingboot项目,将项目中部分需要调用存储过程的部分用entityManagerFactory.unwrap(SessionFactory.class).openS ...