欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[读书笔记]Linkers and Loaders初探

程序员文章站 2024-02-29 19:33:10
...

1.编译
    编译分成3个阶段:
    *预编译阶段(g++ -E选项):这个阶段主要完成预编译指令(#)的处理,包括处理include、define、ifdef等等,譬如如果希望看到宏展开后的结果,可以使用该命令进行预编译处理。
如下将test.cpp预编译后的结果存入test.i

g++ -E -o test.i test.cpp

    像include的文件找不到等错误会在这个阶段发生。
    (有用的编译选项:默认情况下,include的头文件会在当前目录和系统头文件目录中搜索,这个阶段可以通过 -I选项设置需要include的头文件的目录)
    *编译阶段(g++ -S选项):编译代码并生成汇编代码。譬如如下将预编译后的test.i生成汇编代码test.s

g++ -S -o test.s test.i

    如果有语法错误,或者引用了没有声明的变量或函数的错误,会在这个阶段提醒。
    *汇编阶段:将汇编代码汇编成目标文件(.o)。譬如如下将汇编文件test.s汇编成test.o

g++ -c -o test.o test.s

    2.链接
    1)目标文件分析
    编译阶段完成,生成了test2.o目标文件。我们来看看到底生成了一些什么。
    *test.cpp源码:

int add(int first, int second);
int myAdd(int first, int second, int third)
{
    int result = add(first, second);

    result = add(result, third);
    
    return result;
}

    *我们会发现,在如上的程序中,我们仅仅声明了int add(int first, int second)这个函数,实际并没有去实现,用objdump反汇编看一下结果(或者看如上的test2.s)

objdump -d test.o
test.o:     file format elf32-i386
Disassembly of section .text:
00000000 <main>:
   ...
  20:   e8 fc ff ff ff          call   21 <main+0x21>
  ...

      如上,实际发生了对int add(int first, int second)的调用,但在如上中并没有指定 int add(int first, int second)的地址(实际也不可能指出),可以猜测,test.o必定有相关的引用说明,实际就是未解析的符号信息,通过objdump看一下结果:objdump -r test.o

test.o:     file format elf32-i386
RELOCATION RECORDS FOR [.text]:
OFFSET   TYPE              VALUE
00000021 R_386_PC32        _Z3addii
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET   TYPE              VALUE
00000011 R_386_32          __gxx_personality_v0
00000024 R_386_32          .text

    -r选项指出了当前汇编结果中未解析的符号及在本汇编文件相应的占位地址,如上,是00000021
    2)链接过程
    在上面的说明中,我们知道,目标文件并没有包含所有引用到的符号,那么必然有一个过程处理未解析的符号,并最终合并成一个执行文件的过程,也就是链接过程,如下将test.cpp和test1.cpp链接并生成可执行程序test

g++ test.o test1.o -o test

    看看链接后对符号做了什么处理?

 08048434 <main>:
....
 8048454:       e8 13 00 00 00          call   804846c <_Z3addii>
....

0804846c <_Z3addii>:
....

    可以看到,链接过程将实际的地址代入了调用地址中去了。
    3.静态库(.a)
    一般我们会将一些常用的功能打包成库(譬如标准库)。库实际是多个目标文件的聚合。库分成静态库(.a)和动态库(.so),对于静态库,在链接的过程会将相应的代码打包(patch)到相应的可执行程序当中,而动态库,则不会打包到可执行程序中,而是在加载器加载程序的时候才将其链接进来,即加载器的链接。
    我们先来看静态库是怎么回事
    *测试代码
     test2.cpp

int add(int first, int second)
{
    int result = first + second;

    return result;
}

     test3.cpp

int add(int first, int second);

int add2(int first, int second)
{
    int result = add(first, second);

    result++;

    return 0;
}

 

   *先编译成目标文件(.o),然后使用下面的命令打包
    ar -r libtest.a test2.o test3.o
    需要注意,一般打的包名字以lib开头,我们看看到底生成了一些什么东西:objdump -d libtest.a

In archive libtest.a:
test2.o:     file format elf32-i386
Disassembly of section .text:
00000000 <_Z3addii>:
   ...
test3.o:     file format elf32-i386
Disassembly of section .text:
00000000 <_Z4add2ii>:
   ...
  13:   e8 fc ff ff ff          call   14 <_Z4add2ii+0x14>
   ...

    可以看出,虽然test3.cpp中使用的int add(int first, int second),在test2.cpp中定义,但ar并没有对目标文件做什么处理,调用地址处仍然只是保留一个占位符而已
   *使用静态库来链接成可执行程序
   主程序非常简单,调用一下add2函数,执行下面命令产生执行程序
   g++ -o test4 test4.cpp -L. -ltest
   -L.表示需要在当前目录下搜索库,-ltest表示连接需要依赖到libtest.a连接库
   我们看看链接后的结果:objdump -d test4

08048434 <main>:
 ...
 8048454:       e8 13 00 00 00          call   804846c <_Z4add2ii>  (此处已经解析出了add2的地址)
 ...
0804846c <_Z4add2ii>:
 ...
 804847f:       e8 10 00 00 00          call   8048494 <_Z3addii>   (此处的地址在链接的时候被替换掉了)
 ...
08048494 <_Z3addii>:
 ...

   可以看出,在链接的时候,库中的test2.o调用test3.o的add函数部分的地址被替换掉了
   4.再看链接符号解析
   结合静态库,我们再回头看看链接的符号解析处理:
   *连接器从左到右扫描所有指定的目标文件和库文件,在扫描过程当中维持如下三个set:目标文件Set (O Set)、未解析符号Set (U Set)、已解析符号Set (D Set),刚开始的时候,这3个Set都是空的
   *连接器扫描到一个目标文件,则将其加入O Set,同时更新U Set和D Set
   *连接器扫描到一个库文件,则扫描库文件中的各目标文件,如果在其中能够找到U Set中的内容,则将该目标文件加入到O Set,同时更新U Set和D Set中相应符号,如果该目标文件有没有解析的符号,
也相应将其加入到U Set中
   *在扫描完之后,如果U Set不为空,则链接器会报错,如果U Set为空,则连接器将O Set中的内容进行合并处理 
   从如上过程当中,可以注意到
   *各库的顺序是有依赖关系的,如果库a依赖库b,则链接时需要将a依赖放在b的左边
   *不允许在目标文件中定义同一个符号
   *如果在不同的库中定义了同一个符号,则第一次被依赖到地先被扫描进来
   5.加载器
   链接完成,我们开始运行程序,这时候加载器登场了。引用《Linkers and Loaders》的话说:加载是将一个程序放到主存里使其能运行的过程。基本过程大概包括:

<Linkers and Loaders>第8章 写道
*从目标文件中读取足够的头部信息,找出需要多少地址空间。
*分配地址空间,如果目标代码的格式具有独立的段,那么就将地址空间按独立的段划分。
*将程序读入地址空间的段中。
*将程序末尾的bss段空间填充为0,如果虚拟内存系统不自动这么做得话。
*如果体系结构需要的话,创建一个堆栈段(stack segment)。
*设置诸如程序参数和环境变量的其他运行时信息。
*开始运行程序。

   需要注意的是,这个过程不涉及到动态连接库,动态连接库的加载涉及到符号重定位的问题,会更加复杂

相关标签: 读书