这是之前写的一个总结,现在回过头来看还是有一些收获,可以自己写一个动态库来进行测试,本文是以我自己写的动态库来进行测试的,使用工具readelf。
.so文件是elf格式文件中的一种,它遵循elf格式的相关规则,在对so文件进行内存装载时我们先学习一下elf格式中的相关内容(这里只是做基础的介绍,较为详细的内容可以参考《ELF文件格式分析》,也可以查阅网络资料,不过大多数网络资料都是从这里面提取的)。
一、可执行链接格式
可执行链接格式(Executable and Linking Format)最初是由 UNIX 系统实验室(UNIX System Laboratories,USL)开发并发布的,作为应用程序二进制接口(Application Binary Interface,ABI)的一部分。
目标文件有三种类型:
- 可重定位文件(Relocatable File) .o)包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
- 可执行文件(Executable File) .exe) 包含适合于执行的一个程序,此文件规定了exec() 如何创建一个程序的进程映像。
- 共享目标文件(Shared Object File) .so) 包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理, 生成另外一个目标文件。其次动态链接器(Dynamic Linker)可能将它与某 个可执行文件以及其它共享目标一起组合,创建进程映像。
目标文件全部是程序的二进制表示,目的是直接在某种处理器上直接执行。
1、ELF文件格式
ELF文件格式排布如下图1所示,分为链接视图和执行视图,我们的so共享库文件对应着链接视图来进行分析即可

图1 ELF文件格式
从图1中可以看出链接视图主要分为ELF头部、程序头部表、节区以及节区头部表。这些内容的定义可以在文件目录中的elf.h中看到,这里不加赘述,过多赘述,直接进入主题,来了解动态库在内存中的装载(以libcac.so为例)。
二、确定装载内容
一个so文件中有这么多的内容格式,哪些才是需要被我们装载的内容呢,通过分析dlopen的源码。
https://blog.youkuaiyun.com/SweeNeil/article/details/83744843
https://blog.youkuaiyun.com/SweeNeil/article/details/83749186
在其中发现真正被加载进内存的是Program Header对应的内容,而且该内容在Program Header中对应的type类型还应该为:PT_LOAD
通过对我们编写的程序的运行(elfso.c)可以看到这部分如下图2所示,红色方框中标识的type为1即表示为type类型为PT_LOAD这是需要被加载进内存的。

图2 libcac.so程序头内容
同样的也可以使用readelf工具来进行相关内容的查看,使用readelf查看获得的内容如下图3所示,红色方框中的内容就是需要被加载到内存中的,他们的type类型都为LOAD,可以看到使用readelf工具查看得到的信息和我们自己写的程序得到的信息是一致的。

图3 readelf查看libcac.so中程序头的内容
在标志为PT_LOAD的程序头中,它包含了需要映射的内容在文件中的位置以及映射的相关内容(这些内容可以再elf.h文件中程序头的定义中得到)。
三、动态库在内存中的装载
1、加载所有需要加载到内存的内容
通过上述第二节,知道了需要被加载进内存的内容,那么要如何加载呢,首先分析dlopen中的加载进内存的方式。
部分代码如下:
struct loadcmd
{
ElfW(Addr) mapstart, mapend, dataend, allocend;
off_t mapoff;
int prot;
} loadcmds[l->l_phnum], *c;
size_t nloadcmds = 0;
case PT_LOAD:
c = &loadcmds[nloadcmds++];
c->mapstart = ph->p_vaddr & ~(GLRO(dl_pagesize) - 1);
c->mapend = ((ph->p_vaddr + ph->p_filesz + GLRO(dl_pagesize) - 1) & ~(GLRO(dl_pagesize) - 1));
c->dataend = ph->p_vaddr + ph->p_filesz;
c->allocend = ph->p_vaddr + ph->p_memsz;
c->mapoff = ph->p_offset & ~(GLRO(dl_pagesize) - 1);
if (nloadcmds > 1 && c[-1].mapend != c->mapstart)
has_holes = true;
……
maplength = loadcmds[nloadcmds - 1].allocend - c->mapstart;
l->l_map_start = (ElfW(Addr)) __mmap ((void *) mappref, maplength,
c->prot,
MAP_COPY|MAP_FILE,
fd, c->mapoff);
l->l_map_end = l->l_map_start + maplength;
l->l_addr = l->l_map_start - c->mapstart;
__mprotect ((caddr_t) (l->l_addr + c->mapend),
loadcmds[nloadcmds - 1].mapstart - c->mapend,
PROT_NONE);
l->l_map_start = c->mapstart + l->l_addr;
l->l_map_end = l->l_map_start + maplength;
l->l_contiguous = !has_holes;
结合实验所使用的libcac.so文件,通过第二节我们知道在libcac.so中需要装载进内存的段有两个,他们的信息如下图4所示,包括要映射的内容在文件中的偏移量,虚拟地址,文件大小等。
![]()
图4 要加载进内存的Segment的信息
通过图4的相关内容可以填充上述的loadcmds结构体(蓝色部分代码),由于这里需要装载进内存的Segment有两个,因此这里以C1表示第一个,C2表示第二个来进行计算。
C1->mapstart = 0 & ~(GLRO(dl_pagesize)-1)
C1->mapend = ((0+0x774+GLRO(dl_pagesize)-1)&~(GLRO(dl_pagesize)-1)
C1->dataend = 0+0x774
C1->allocend = 0+0x774
C1->mapoff = 0&~(GLRO(dl_pagesize)-1)
C2->mapstart = 0x200e28 & ~(GLRO(dl_pagesize)-1)
C2->mapend = ((0x200e28+0x1e8+GLRO(dl_pagesize)-1)&~(GLRO(dl_pagesize)-1)
C2->dataend = 0x200e28+0x1e8
C2->allocend = 0x200e28+0x1f8
C2->mapoff = 0xe28&~(GLRO(dl_pagesize)-1)
其中dl_pagesize为4096(根据自己电脑来确定,使用getconf PAGE_SIZE命令查看),如下图5所示,使用getconf PAGE_SIZE获得的pagesize为4096。
![]()
图5 获得pagesize
得到了pagesize之后,计算可知GLRO(dl_pagesize)-1等于0xfff,因此上述代码&上这样一个pagesize其实是为了将后12位清零,达到页对齐的目的(个人理解)。因此,下一步就可以将C1、C2中的具体内容计算出来。
C1->mapstart = 0
C1->mapend = 0x1000
C1->dataend = 0x774
C1->allocend = 0x774
C1->mapoff = 0
C2->mapstart = 0x200000
C2->mapend = 0x202000
C2->dataend = 0x201010
C2->allocend = 0x201020
C2->mapoff = 0
这里最开始一直不能理解,为什么mapoff也为0了,这里要将mapoff与mapstart结合起来看,他们都将不足一页的偏移变为了0,在读的时候,相应位置的实际内容是内有改变的,其实细心点可以发现,他们的偏移是一样的,比如虚拟地址为0x200e28,文件偏移为e28,mapoff变为0了,mapstart变为了0x200000,但是0x200e28这个地址的内容还是从文件偏移e28开始的。(个人理解)
了解到了C1、C2之中的内容,再来计算它的maplength是如何确定的,maplength的计算由绿色部分代码完成,即最后一个Segment的的allocend减去第一个Segment的mapstart(在第一部分映射中就讲所有空间确定,因此这里映射的标志不是MAP_FIXED)。
因此实验中所使用的libcac.so的maplength就为:
maplength = C2->allocend – C1->mapstart = 0x201020-0 = 0x201020 = 2101280
进一步推出(下面用到的C都是C1)
l->l_map_start = __mmap ((void *) mappref, 2101280,C1->prot,MAP_COPY|MAP_FILE,fd, 0);
l->l_map_end = l->l_map_start + 0x201020;
l->l_addr = l->l_map_start - 0;
__mprotect ((l->l_addr +0x1000),C2->mapstart – C1->mapend,PROT_NONE);
=》__mprotect((l->l_map_start+0x1000),0x1ff000,PROT_NONE);
这里的__mprotect把中间部分保护起来,禁止所有访问,就像未分配一样,然后跳转到正常的段映射循环。
2、装载各个Segment
最开始不太理解,为什么已经全部映射了,却还要再映射一次呢,我们通过下面一个图示来进行直观的查看吧,如下图6所示。第一次对所有需要映射的Segment进行映射,它是按照文件的编排,直接映射过去的,同时它也保证了能够分配到这样一段连续的虚拟地址。而第二次映射需要将对应的Segment映射到其头部表示的虚拟地址中,如图6中的C2,它需要被映射到最后,因此才需要进行两次装载,图中Virtual Address中的白色区域为mprotect部分。

图6 映射图示
接着继续分析代码
/* Remember which part of the address space this object uses. */
l->l_map_start = c->mapstart + l->l_addr;
l->l_map_end = l->l_map_start + maplength;
l->l_contiguous = !has_holes;
while (c < &loadcmds[nloadcmds])
{
if (c->mapend > c->mapstart
/* 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,
fd, c->mapoff)
== MAP_FAILED))
goto map_error;
postmap:
if (c->prot & PROT_EXEC)
l->l_text_end = l->l_addr + c->mapend;
if (l->l_phdr == 0 && (ElfW(Off)) c->mapoff <= header->e_phoff &&
((size_t) (c->mapend - c->mapstart + c->mapoff) >= header->e_phoff + header->e_phnum * sizeof (ElfW(Phdr))))
/* Found the program header in this segment. */
l->l_phdr = (void *) (c->mapstart + header->e_phoff - c->mapoff);
if (c->allocend > c->dataend)
{
/* Extra zero pages should appear at the end of this segment,
after the data mapped from the file.
在从文件映射的数据之后,此段的末尾应出现额外的零页。*/
ElfW(Addr) zero, zeroend, zeropage;
zero = l->l_addr + c->dataend;
zeroend = l->l_addr + c->allocend;
zeropage = ((zero + GLRO(dl_pagesize) - 1)
& ~(GLRO(dl_pagesize) - 1));
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 (__builtin_expect ((c->prot & PROT_WRITE) == 0, 0))
{
/* Dag nab it. */
if (__mprotect ((caddr_t) (zero
& ~(GLRO(dl_pagesize) - 1)),
GLRO(dl_pagesize), c->prot|PROT_WRITE) < 0)
{
errstring = N_("cannot change memory protections");
goto call_lose_errno;
}
}
memset ((void *) zero, '\0', zeropage - zero);
if (__builtin_expect ((c->prot & PROT_WRITE) == 0, 0))
__mprotect ((caddr_t) (zero & ~(GLRO(dl_pagesize) - 1)),
GLRO(dl_pagesize), c->prot);
}
if (zeroend > zeropage)
{
/* Map the remaining zero pages in from the zero fill FD.
从零填充FD映射剩余的零页面。*/
caddr_t mapat;
mapat = __mmap ((caddr_t) zeropage, zeroend - zeropage,
c->prot, MAP_ANON|MAP_PRIVATE|MAP_FIXED,
-1, 0);
if (__builtin_expect (mapat == MAP_FAILED, 0))
{
errstring = N_("cannot map zero-fill pages");
goto call_lose_errno;
}
}
}
++c;
}
继续像上面一样根据实验libcac.so一步一步计算相关内容,红色部分内容计算如下:
l->l_map_start =l->l_map_start;(最开始__mmap返回的地址值)
l->l_map_end = l->l_map_start + 0x201020;
l->l_contiguous = 0;
再到绿色部分代码:
判断当前C是否为最后一个如果,当前C为C1,C1->mapend = 0x1000肯定是大于C1->mapstart = 0。因此在这个if判断后会进行__mmap
__mmap ((void *) (l->l_addr + c->mapstart),
c->mapend - c->mapstart, c->prot,
MAP_FIXED|MAP_COPY|MAP_FILE,
fd, c->mapoff)
=》当前为C1:
=》__mmap(l->l_map_start,0x1000,C1->prot,MAP_FIXED|MAP_COPY|MAP_FILE,fd,0),
上述情况是对C1进行了映射,映射地址为第1小节中返回的那个l->l_map_start,同时采用MAP_FIXED的方式进行。
继续往下看存在
if (l->l_phdr == 0 && (ElfW(Off)) c->mapoff <= header->e_phoff &&
((size_t) (c->mapend - c->mapstart + c->mapoff) >= header->e_phoff + header->e_phnum * sizeof (ElfW(Phdr))))
/* Found the program header in this segment. */
l->l_phdr = (void *) (c->mapstart + header->e_phoff - c->mapoff);
上述代码与映射没什么关系先不管
继续往下看:
if (c->allocend > c->dataend)
我们先来看C1,C1->allocend = 0x774,C1->dataend = 0x774所以C1的映射已经结束,然后继续while,来进行C2所对应的Segment的映射。
__mmap ((void *) (l->l_addr + c->mapstart),
c->mapend - c->mapstart, c->prot,
MAP_FIXED|MAP_COPY|MAP_FILE,
fd, c->mapoff)
=》当前为C2
=》__mmap(l->l_map_start+0x200000,0x2000,C2->prot,MAP_FIXED|MAP_COPY|MAP_FILE,fd,0);
这里是C2对应的Segment的映射,继续往下
if (c->allocend > c->dataend)
这里C2->allocend = 0x201020,C2->dataend = 0x201010,所以在C2对应的段在映射时会进入到这个if中,我们进去到这个if里看里面是什么内容。
它定义了如下的三种数据,zero、zeroend、zeropage,冰球对变量进行了赋值。
ElfW(Addr) zero, zeroend, zeropage;
zero = l->l_addr + c->dataend;
zeroend = l->l_addr + c->allocend;
zeropage = ((zero + GLRO(dl_pagesize) - 1)
& ~(GLRO(dl_pagesize) - 1));
跟着这个赋值来计算相应的值,对于C2:
zero = l->l_map_start + 0x201010
zeroend = l->l_map_start + 0x201020
zeropage = (l->l_map_start+0x201010+0xfff)&(0x000)
为了继续计算,不妨我们给l->l_map_start一个初始值,mmap的返回值都是4096的整数倍,因此这个值可以很好地设置,例如设l->l_map_start为0x6a6c8000
因此:
zero = 0x6a6c8000+0x201010 = 0x6a8c9010
zeroend = 0x6a6c8000 + 0x201020 = 0x6a8c9020
zeropage = (0x6a6c8000+0x201010+0xfff)&(0x000) = 0x6a68ca000
if (zeroend < zeropage)
zeropage = zeroend;
if (zeropage > zero)
{
/* Zero the final part of the last page of the segment. */
if (__builtin_expect ((c->prot & PROT_WRITE) == 0, 0))
{
/* Dag nab it. */
if (__mprotect ((caddr_t) (zero & ~(GLRO(dl_pagesize) - 1)),
GLRO(dl_pagesize), c->prot|PROT_WRITE) < 0)
__mprotect(0x6a8c9000,4096,C2->prot|PROT_WRITE)
{
errstring = N_("cannot change memory protections");
goto call_lose_errno;
}
}
memset ((void *) zero, '\0', zeropage - zero);
if (__builtin_expect ((c->prot & PROT_WRITE) == 0, 0))
__mprotect ((caddr_t) (zero & ~(GLRO(dl_pagesize) - 1)),
GLRO(dl_pagesize), c->prot);
}
if (zeroend > zeropage)
{
/* Map the remaining zero pages in from the zero fill FD.
从零填充FD映射剩余的零页面。*/
caddr_t mapat;
mapat = __mmap ((caddr_t) zeropage, zeroend - zeropage,
c->prot, MAP_ANON|MAP_PRIVATE|MAP_FIXED,
-1, 0);
}

本文深入探讨了Linux系统中动态库的内存装载过程,从ELF文件格式出发,详细分析了动态库的装载内容,特别是PT_LOAD类型的Program Header。通过实际例子展示了如何使用readelf工具查看动态库的Program Header,并解释了动态库在内存中装载的步骤,包括如何确定装载内容、装载策略及映射过程。文章最后讨论了内存中额外零页的处理方式。
405

被折叠的 条评论
为什么被折叠?



