read-only pages: read-write pages: .bss 一、文件的加载 这一过程有点长,但很直观。 首先:启动动态链接器 一个程序启动之后,系统先把文件内容映射到内存中,然后..... 首先,系统通过.interp节找到动态链接器,并将其加载到内存空间,再启动链接器。还要把一个辐助向量传到动态链接器的栈空间里: AT_PHDR, AT_PHENT, and AT_PHNUM:程序头表的地址,每个表项的大小和表项数。 AT_ENTRY: 程序的入口地址 AT_BASE: 动态链接器加载的基地址 然后,动态链接器找到自己的.got节,其中的第一条目指向动态链接器自己的.dynamic节。然后动态链接器先重定位自己的变量和必要函数。使自己可用。(动态链接器ld.so的重要函数都以_dt_开头,一段特殊的代码会直接找到这种字符串开头的符号,并解析它们) 最后,动态链接器初始化一系统的符号表,其中有指针指向动态链接器自己的符号表和程序的符号表。从概念上来讲,一个进行的程序文件和所有的库是共享一个符号表的。但是链接器不没有把所有的符号表混合在一起,而是用一个链表把他们串起来。每个文件都有自己的hash表。链接器查找符号时只计算一次哈希值,然后逐个哈希表去匹配。 然后找到需要的库 动态链表器自己的初始化完成之后,它就开始为程序加载库文件。程序的program header table有一个条目指向程序的DYNAMIC段,也即.dynamic节。它里面包含的是动态链接信息。包含一个DT_STRTAB项,指向.dynstr节,和多个ST_NEEDED项,是需要的库,其名字指向字符号表(.dynstr)中的偏移: 例子: readelf -d test得到的.dynamic节条目表 readefl -S test得到的节头表 找到需要的库名字之后,动态链接器就会去系统里找相同名字的库文件,这是一个相当复杂的过程。比如说在上面的例子中,需要的库文件是libc.so.6(C库,版本6),找它的过程需要去系统的库路径个逐个搜索。可能找到的库名字也不完全一样,比如说可能会找到lib.so.6.2,即后面还带了更小的版本号。 链接器查找库文件的位置包括如下几处:
最终,动态链接器找到了所有的库,并且拥有了一个逻辑上的全局符号表。 初始化共享库 动态链接器重新访问每个库,处理它的重定位条目,添写其GOT(全局偏移表),并对数据区的符号进行重定位。加载阶段的重定义包括:
惰型过程链接(原名是lazy procedure linkage,没找到通用的翻译方法,自己取的名字) 为什么说是惰型的呢?因为这种链接方式很拖拉,不到用时不链接。过程链接是说这种链接是面向函数的。 有些函数,在程序运行过程中很少会调用,比如说错误处理什么的。函数依赖的库还会依赖其他库,但依赖的其它库的函数也很少会用到。如果所有的都加载的话,可能你系统里的所有库都必须加载到内存中去。那机器就没法跑了。 所以对于不常用的函数,我们在不用他的时候,就不加载他所有的库。用时再说。 这样也能加快程序启动时间,很多库都不用加载了嘛。 但那些没有解析的符号怎么办?先不管,留着用到时再绑定。但这些符号可不能放到原来的地方了,我们需要另外的处理方法,和存储方法。 存储方法叫Procedure Linkage Table.PLT。每个动态绑定的程序或库都有一个plt表。里面包含的条目是程序或库调用的非本地例程。当程序或库调用到这个例程的时候,都会跳入到plt的相应条目中去,相应的条目都是几条简单的指令,如果这个函数例程还没有绑定,就是调用动态链接器进行绑定。如是不是第一次调用,那么函数就己经绑定了,就是直接跳转到函数中去。
PLT中的第一个条目叫做PLT0,它的代码与其他PLTn不同。PLT0就是那个调用动态链接器的代码。但第一次调用某个函数时,PLTn都会跳到PLT0上来,然后用PLT0的代码来调用动态链接器。 在程序装载阶段,动态链接器会在GOT中自动加上两个条目:在GOT的第二个字上,和第三个字上,即GOT+4和GOT+8。依次放入的是库鉴定代码(这个代码可以知道当前要调用一个未解析的符号的程序或库是什么)和动态链接器的符号解析函数地址。每个PLTn条目第一个指令都是一个跳转指令,跳转到*GOT+m上去。每个PLTn条目都对应着一个GOT上的条目,那个GOT上的条目初始化时是一个指向这个PLT上跳转指令的下一条指令,是push指令,也就是说开始时是PLTn跳到GOT+m后马上跳回来执行下面的push指令(这块可以看看上面的指令图)。push指令就是push #reloc_offset,它入栈的是重定向表中的一个条目,这个条目的类型为 R_386_JMP_SLOT。这个条目的目地地址是符号表中一个符号,这个符号表条目的value项正指向GOT+m,也就是说现将要解析的符号解析为GOT+m. 这种安排很诡怪。但也很巧妙。当第一次进入PLTn时,第一条指令跳到GOT+m后什么也没做就返回来接着执行push指令了。这个push指令将要解析的符号的重定位表上的条目偏移入栈。它的目的地址是符号表中的条目,条目的目的地址则是GOT+m,下一步我们把解析出来的真正地址填到GOT+m就可以了。 PLTn的第三条指令就是jump PLT0, PLT0: pushl GOT+4 jmp *GOT+8这里将鉴定程序或库的代码入栈,然后调用GOT+8指定的符号解析例程。 这里有一点东西要多说一下: 当我们调用一个call指令的时候,它将eip入栈,然后跳到调用的函数里,函数返回时将eip出栈,接着执行eip所指位置的指令。 在这里也用了这个小技巧,这里先入栈的是#reloc_offset,然后入栈的是GOT+4,然后jump到符号解析例程。 想想:符号解析例程返回时会有什么操作?就是出栈一个数,然后按这个数指定的位置执行指令,也就是说这里GOT+4里的内容会pop eip。 这样,进入符号解析例程之后,符号解析例程在链在一起的那些符号表里找到要解析的符号,然把符号地址存到GOT+m里,返回。然后先pop出库签定例程,确定是哪个程序做的调用,然再pop出重定位表,去重定位表中重新调用那个函数,这时再进入PLTn时,就直接接跳到GOT+m到跳到相应的函数上去了,不会再执行PLTn中的下两条指令了。
|
|
来自: happy123god > 《Linux编程环境》