CSAPP--第七章:链接 学习

生成可执行文件的过程:
//用linux gcc编译器,编译main.c,sum.c

①预处理(preprocess):生成main.i
cpp main.c -o main.i

②编译(compile):生成main.s
gcc main.i -o main.s
//CSAPP上说的是cc1,但linux找不到这个,只有cc,但是会报错。

③汇编(assembly):生成main.o
as mian.s -o main.o

④链接(linked):生成可执行文件
ld -o prog main.o sum.o




目标文件

  • 三种形式:

    可重定位目标文件(file.o):包含二进制的代码和数据,可在链接时和其他目标文件合并起来,生成可执行目标文件。

共享目标文件:特殊类型的可重定向目标文件,可以再加载、运行时被动态加载进内存并链接。

可执行目标文件:包含二进制的代码和数据,其可被直接复制到内存并执行。

可重定位目标文件
  • 不同系统格式:

    windows:可移植可执行格式(Portable Executable),PE格式。

    Max OS-X:Mach-O格式。

    Linux和Unix:可执行可链接模式(Executable and Linkable Format),ELF格式。

  • linux的可重定位目标文件:

    ELF格式为一个序列:{ELF,... , 节头部表}

    ELF和节头部表中间,包含了很多个节, 如:

    .text .rodate .data等等,如下图:


    ELF格式
  • 符号和符号表

    //linux中,用readelf name -s查看符号表。

    每个目标文件(模块)中都有一个符号表symtab,其包含了本文件中定义、引用的符号的信息。

    三种不同符号:

    1:模块自己定义,并能被其他模块引用的全局符号(函数或者变量,非静态)。

    2:由其他模块定义,被本模块引用的全局符号。

    3:模块自己定义的静态全局符号,仅被自己使用。

    单个符号的表,如下:


    某个符号表

    可以在linux中,用objdump -dx file.o 来得到符号表的反汇编代码:


    符号表反汇编

    //ndx代表节索引

全局符号根据初始化与否,被分到不同的节:

1:分配到 .data:全局变量初始化,且不为0。

2:分配到 .bss :全局变量初始化为0;未初始化的静态变量符号 //(Block Storage Start)

3:分配到 .COMMON :没有初始化的全局变量。

4:分配到 .UND:没有定义的全局函数。

//UND、COMMON是伪节,仅在重定位文件中,执行文件无。

  • 链接器解析多重定义的全局符号

    解析:将全局符号的引用和定义关联起来。

    强符号:已初始化的全局符号。

    弱符号:未初始化的全局符号。

    重定义 有三种情况:

    1:两个文件中,都有同样的强符号出现,会报错。

    2:两个文件有同名符号,但只有一个强符号,则选择强符号。

    3:都是弱符号,则随机选择一个。

    如图:


与静态库的链接
  • 静态库定义:

    所有的编译系统都提供一种机制,将所有相关的目标模块打包成一个单独的文件,称之为静态库。格式为 lib.a

    如图:

    静态库链接过程

    //(.a表示archive 存档,里面包含了很多个.o模块,当某模块引用了库中的模块名时,变会将其复制链接到可执行文件)

    //通常编译器会自动隐式链接libc.a 标准函数库,而自定义的一些库,需要显式链接。创建静态库用AR工具。

    不使用静态库的实现方法及自定义库:CSAPP P475。

链接器使用静态库来解析引用
  • 编译驱动器按照命令行从左到右,进行顺序扫描。

    链接器维护了三个集合:E(将被合并成可执行文件)、U(还未被解析的引用符号)、D(已经解析的有定义的符号集合)

    1:如果为file.o,则将其放入E,并修改U,D。

    2:如果为libx.a,则将其与U中符号进行匹配,如果匹配,则将该模块.o 放入E中。并修改U、D(U中清空已解析引用,并添加模块.o中未解析引用),存档文件扫描完后,将无引用的模块丢弃。

    3:链接器完成对命令行的遍历后,若U非空,则进行报错并终止。否则,会合并E中目标文件,生成可执行文件。

    //所以切记,不要将.o和.a顺序弄错,否则会无法解析引用,链接失败。

重定位
  • 解析引用之后,便是要重定位:

    1:将不同模块的相同节进行拼接:
    如.text,形成一个新的.text节:其包含了所有模块的指令代码。数据代码、符号表等待也一样。

    2:对引用的符号进行重定位:
    对全局变量、全局函数等进行重定位,使其调用时,指向可执行代码中的定义位置。

    在可重定位目标程序中,如果有未定义的引用,则会将其放在.rel.text、.rel.data中(rel=relocation),使编译器知道在生成可执行代码时,对这些符号进行重定位。

  • 每个重定位条目,类似结构体,包含:

    offset :在代码中的偏移信息。如main函数内部调用一个全局函数f()时,则offset = f()相对于main首地址的偏移量。

    type:重定位之后的类型。

    symbol:模块拼接之后,f()在符号表中的偏移量。

    addend:这个值是针对不同情况,如f()的实现在mian()之后时,因为到call时,PC指令已经指到下一个代码位置,需要减去f()偏移量的大小,64位=8,32位=4;

    如下图:


    重定位entry
可执行目标文件

当链接器将所有模块解析及重定位之后,会生成一个可执行文件的ELF表,其与可重定位目标文件表类似,但是没有了两个.rel节,因为不需要重定位了。


可执行目标文件ELF表

增加的节:

init:定义了一个_init函数,程序的初始化代码会调用它。

段头部表:描述了可执行文件中,代码段和数据段对内存的映射关系。


段头部表的反汇编

加载可执行目标文件

在linux中使用 ./prog 可以运行目标文件。

  • 加载流程://无动态链接

    1:将代码复制到内存:
    操作系统常驻在内存的加载器(loader),会将可执行目标文件中的代码和数据从磁盘复制到内存。

    2:将控制交给prog:
    通过跳转到程序的第一条指令或者入口点(entry point)来运行程序。

其在内存中,图示如下:


内存映像,省略了空隙

动态链接共享库

静态库的缺点是太耗磁盘和内存空间,尤其是当同时运行了上百个程序的时候,几乎每个程序中都有标准I/O函数及其他重复函数,占用了很大的内存的资源。

  • 共享库
    (shared library)用.so后缀表示。是第三种目标文件,即共享目标文件。在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来,这个过程称为动态链接(dynamic linking)。

    动态连接器:dynamic linker。执行动态链接过程。

    "共享"说明:

    1:磁盘中,对于一个动态库只有一个.so文件,所有引用该文件的可执行目标文件共享一个库,而非静态库,需要将其内容复制链接到可执行文件中。

    2:在内存中,共享库的.text节的副本,可以被不同的正在运行的进程所共享。大大节约了内存空间。


    动态链接过程

    动态链接时:

    1:和静态库不同,生成prog21时,并没有复制代码和数据。反而是链接器,复制了一些重定位和符号表信息。

    2:在内存中时,链接器根据这些信息,复制动态库中的文本和数据到另一个内存段,并对prog21中,有引用动态库里定义的符号进行重定位。

    3:动态链接器将控制传递给prog21,开始运行。共享库的位置固定不变了。


从应用程序中加载和链接共享库、位置无关代码PIC(Position Independent Code)、库打桩机制没有学,因为书里比较简略。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。