Skip to content

[翻译] 简谈编译、汇编、链接和加载 – part IV

W. 6: 共享对象的使用方式

要理解一个程序如何使用共享对象,让我们首先检查一下可执行文件的格式以及如何让程序开始运行。

w6.1一些ELF格式的细节
ELF(Executable and Linking Format, 可执行与链接格式)是一个二进制格式,用于SVR4 Unix和Linux系统。它是一种在磁盘上存储程序的格式。ELF不仅简化了创建共享文件的过程,也改进了运行时模块的动态加载。

W6.2 ELF块(sections)

Linux及其它使用ELF格式的系统在可执行程序中定义了一些”块“。这些块用于为二进制文件提供指令,并且可被查看。重要的函数块包括GOT(Global Offset Table,全局偏移表),存储系统函数的地址,和PLT(Procedure Linking Table过程连接表),用于存储指向GOT的非直接链接;.init/.fini,用于内部初始化和关闭;.ctors/.dtors,用于constructors and destructors.

数据相关的块有.rodata,用于只读数据,.data用于已经初始化的数据,和.bss,用于未初始化的数据。
下面是一个ELF块的不完全列表(从低到高):

.init – Startup
.text – String
.fini – Shutdown
.rodata – Read Only
.data – Initialized Data
.tdata – Initialized Thread Data
.tbss – Uninitialized Thread Data
.ctors – Constructors
.dtors – Destructors
.got – Global Offset Table
.bss – Uninitialized Data

你可以使用readelf或objdump程序来查看对象文件或可执行文件的块信息。

在下图中,展示了ELF文件的两种视图: 链接视图和执行视图

别忘了ELF的完整格式包含更多块。像前面解释过的,用于程序或库文件链接过程的链接视图处理对象文件内部的块。

对象文件的大部分信息都位于”块“中: 数据,指令,重定位信息,符号,debug信息等。执行视图用于程序执行时,处理对象是segment(也可以翻译为块)。segment是一种把相关的块组合起来的方式。比如,text segment包含了可执行代码,data segment包含了程序数据,dynamic segment包含了动态加载的相关信息。每个segment由一个或多个块组成。一个进程就是通过加载和分析segments来创建的。

操作系统根据程序的header table表里的信息把程序的各个segment复制到它的虚拟内存的segment。操作系统也可以利用segments来创建一个共享的内存资源。

在链接时,通过把具有相似属性的块合并到segment的方式来生成程序或库文件。一般,所有可执行和只读的数据块会被合并到一个text segment,而.data和.bss块会合并为data segment。通常,这些segment被称为加载segment,因为他们需要在创建进程时被加载到内存中。其它的块,如符号信息和debug信息块会被合并到其它、非加载的segment。

W.7 进程加载

在Linux系统上,从文件系统加载的进程(使用execve()或spwan()系统调用)是ELF格式。如果文件系统是在block设备上(如磁盘),代码和数据会被加载的内存中;如果文件系统是内存映射的(如ROM/Flash镜像),代码不会被加载如内存,可能会就地执行。这种方式让全部内存可用于数据和stack(栈),而把代码留在ROM或Flash上。无论哪种情况,如果一个程序需要多次加载,他们的代码部分将是共享的。

要执行一个可执行文件,我们首先要把它加载到内存中。这个步骤由加载器(loader)来完成。它通常是操作系统的一部分。加载器会做以下事情(以及其它的):

  1. 为程序的执行分配主内存
  2. 把地址空间从第二内存拷贝到主内存
  3. 把程序的.text和.data块拷贝到主内存
  4. 把程序的参数拷贝到stack
  5. 初始化寄存器: 设置ESP指向stack的顶部,清空其它的(寄存器?)
  6. 跳转到程序的start入口。它会从stack上拷贝main()函数的参数,然后跳转到main()

地址空间就是包含程序代码,stack和数据segment,或者说程序运行所需要的其它数据的内存空间。
内存的布局有三部分构成(text,data,stack),可以简化成下图
动态数据segment也被称为heap(堆),是动态分配的内存(如malloc()和new)的来源。这种组织方式让程序主动要求分配的动态内存和程序自身运行需要分配的内存分离开来。这也解释了为什么stack向下增长而heap向上增长。

—待续

 

Avatar

专业Linux/Unix/Windows系统管理员,开源技术爱好者。对操作系统底层技术,TCP/IP协议栈以及信息系统安全有强烈兴趣。电脑技术之外,则喜欢书法,古典诗词,数码摄影和背包行。

Sidebar