链接,加载,装载(一)

 

人生若只如初见:弄懂nginx与lua模块的交互。

以下内容是基于对可执行文件有着良好认识的前提下。

 

 

unix> ./p


 

p不是一个内置的命令,所以外壳会认为p是一个可执行目标文件,通过调用某个驻留在存储器中称为加载器(loader)的操作系统代码来运行它。

加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后跳转到程序的第一条指令或入口点(entry point)来运行该程序。

这个将程序拷贝到存储器并运行的过程叫做加载(loading).

 

存储器映像(a run-time memory image )


 

每个unix程序哦独有一个运行时存储器映像。(以下根据方向)

其中分为代码段、数据段、运行时堆。对于32位Linux系统,代码段总是从0x08048000开始,向上数据段是在接下来一个4KB对齐的地址。运行时堆则在接下来第一个4KB处,并通过malloc库往上增长。

用户栈总是从最大的合法用户地址开始。栈的上部开始的段是为操作系统驻留存储器的部分的代码和数据保留的。

根据可执行文件中段头部表,加载器将可执行文件的相关内容拷贝到代码和数据段。

值得一提的是,存储器映射中间的一个段是为共享库保留的。(这里跟初衷可能有很大关系!)

 

加载器是如何工作的?


 

The new code and data segments are initialized to the contents of the executable file by mapping pages in the virtual address space to page-sized chunks of the executable file.

 

 

 

Unix系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当外壳运行一个程序时,父外壳进程生成类似父进程复制品的一个子进程,子进程通过execve系统调用加载器。(以下根据功能、内容分)

加载器删除子进程现有的虚拟存储段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。

新的代码和数据段被初始化为可执行文件的内容,以一种将虚拟地址空间中的映射到可执行文件的页大小的片(chunk)

最后,加载器跳转到_start地址,它最终会调用应用程序的main函数。

值得留意的是,除了一些头部信息,加载过程中没有任何从磁盘到存储器的数据拷贝。CPU引用一个被映射的虚拟页才会进行拷贝,此时,操作系统会利用它的页面调度机制自动将页面从磁盘传送到存储器

 

以上黄色字体是需要了解的概念。

其中以下两个问题待回答:

A.为什么每个C程序都需要一个叫做main的函数?

B.为什么C程序可以通过调用exit或者执行一条return语句来结束。或者程序前两者都不做,而程序仍然可以正确终止?

 

以下内容假定对静态库有良好的认识的前提下

 

共享库


 

有一个问题是几乎每个C程序都是用标准I/O函数,如printf和scanf。在运行时,这些函数的代码会被复制到每个运行进程的文本段中。无论如何,这对于存储器都是极大的浪费现象。

共享库(shared library)是一个目标模块,致力于在运行时可以加载任意的存储器地址,并和一个在存储器中的程序链接起来。以上的过程称为动态链接(dynamic linking),是一个叫做动态链接器(dynamic linker)的程序来执行。

共享库在Unix系统中通常用.so后缀来表示。微软系统大量地利用了共享库,它们称为DLL(动态链接库)。

共享库的“共享”有两种不同方式。所有引用一个so文件的可执行目标文件共享这个.so文件中的代码和数据(这区别于静态库的内容被拷贝和嵌入到引用它们的可执行的文件中。);其次,在存储器中,一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。(chapter 10 in CSAPP)

 

unix>gcc -o p2 main2.c ./libvector.so

需要值得提出的是,libvector.so的代码和数据没有真的被拷贝到可执行文件p2中。反之,链接器拷贝了一些重定位符号表信息,它们使得运行时可以解析对libvector.so中的代码和数据的引用。

留一个问题给自己,什么是.interp?

加载器在这里是加载和运行这个动态链接器(ld-linux.so),其的任务是将共享库的位置确定,并且在程序执行过程中都不会变。

  • 重定位libvector.so的文本和数据到一个存储器段
  • 重定位p2中所有对由libvector.so定义的符号和引用

然后动态链接器将控制传递给应用程序,如这里的可执行文件p2。

 

 

 

 

dlfcn.h


 

目前已经讨论了应用程序执行之前;即应用程序被加载时;动态链接器加载和链接共享库的情况。

然而,应用程序还可能在它运行时要求动态连接器加载和链接任意共享库,而无需编译时链接那些库应用中。

 

必须留意的是,加载器、动态链接器以及共享库是不同的概念。留一个题目给自己,找出它们的英文。

to be continued ... 

 

Linux系统为动态链接器提供了一个简单的接口,允许应用程序在运行时加载和链接共享库。

 

#include <dlfcn.h>

void *dlopen(const char *filename, int flag);

void *dlsym(void *handle, char *symbol);

int dlclose(void *handle);

const char *dlerror(void);

 

 

 

 

其中dlopen函数加载和链接共享库filename。用以前RTLD_GLOBAL选项打开的库解析filename中的外部符号

如果当前可执行文件是带-rdynamic选项编译的,那么对符号解析而言,它的全局符号也是可用的。

flag参数必须包括RTLD_NOW,或者RTLD_LAZY标志,这两个值任一个可以和RTLD_GLOBAL标志取或。其中NOW表示告知链接器立即解析对外部符号的引用,LAZY标志则指示链接器推迟符号解析直到执行来自中的代码。

下面调用GCC的方式:

unix> gcc -rdynamic -02 -o p3 dll.c ldl

 

现在是时候回过头来看看so文件,点.a文件以及可执行目标文件以及可重定位的目标文件了。

--> 链接,加载,装载(二)

 

 

 

 

 

 

 

posted on 2015-06-11 00:13  dotdog  阅读(556)  评论(0编辑  收藏  举报

导航