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)。
*设置诸如程序参数和环境变量的其他运行时信息。
*开始运行程序。
需要注意的是,这个过程不涉及到动态连接库,动态连接库的加载涉及到符号重定位的问题,会更加复杂
分享到:
相关推荐
《Linkers and Loaders》是一本专注于链接器和加载器技术的英文图书,作者是John R. Levine。本书从历史角度出发,详细阐述了链接器和加载器的功能,以及它们如何处理编译后的代码和数据。在学习程序编译和运行过程...
《链接器与加载器》是理解计算机系统执行过程中的关键环节之一。链接器(Linker)和加载器(Loader)是程序编译和...通过阅读《链接器与加载器》这本书,我们可以系统性地学习这些知识,提升自己在IT领域的专业素养。
**链接器(Linkers)**与**加载器(Loaders)**是计算机科学领域中两个重要的概念,它们在软件开发过程中扮演着关键角色。本书《链接器与加载器》旨在为读者提供关于链接器和加载器的深入理解,特别是对于那些希望...
学生:由于链接过程看起来似乎是微不足道和显而易见的,编译器构建和操作系统 课程通常对链接和加载都缺乏重视。这对于以前讨论Fortan,Pascal,C,和不使 用内存映射或共享库的操作系统而言可能是对的,但是现在就...
链接器和加载器是软件开发...通过阅读《Linkers & Loaders》,学习者可以获得对软件构建过程深刻的洞察力,并理解链接器和加载器是如何将源代码转换为可执行程序的。这对于理解软件的运行机制和优化编译过程至关重要。
《加载器和连接器》是计算机科学领域中关于程序编译和执行过程的重要主题,主要涉及...通过阅读《加载器和连接器》这本书的中文版和英文版,读者可以全面掌握这两个工具的工作原理,从而更好地驾驭软件开发的各个环节。
linkers and loaders,深入理解连接器和加载器的经典书籍。
本书是为任何使用编译代码的程序员编写的,本书对当今的硬件平台进行了概述,并介绍了如何链接和执行代码。
通过阅读《Linkers and Loaders》,我们可以深入理解程序如何从源代码变成可执行文件,以及在内存中如何运行,这对于系统编程、性能优化和安全分析等都具有重要意义。这本书不仅涵盖了理论知识,还提供了实际案例,...
这个压缩包文件"Linkers and Loaders.rar"很可能包含了关于这个主题的详细资料,可能包括书籍、文档或者课件,旨在深入解析这两个关键工具的功能和工作原理。 首先,我们需要了解链接器(Linker)和加载器(Loader...
《Linkers & Loaders》是John R. Levine撰写的一本经典著作,主要探讨了在Linux环境中,链接器和加载器的...通过阅读《Linkers & Loaders》,读者可以深入理解计算机系统的底层运作,从而编写出更高效、更安全的代码。
Whatever your programming language, ... Only now, with the publication of Linkers & Loaders, is there an authoritative book devoted entirely to these deep-seated compile-time and run-time processes.
《链接器与加载器》与《可执行与可链接格式》是两本深入探讨计算机系统底层运作...通过阅读这两本书,读者可以掌握如何从源代码构建可执行文件,以及这些文件在内存中的表现形式,从而更好地驾驭复杂的软件工程任务。
**链接器与加载器(Linkers & Loaders):关键概念与架构问题** 在计算机科学领域,链接器(Linkers)和加载器(Loaders)是编译过程中的两个核心组件,它们负责将多个编译单元连接成一个可执行程序,并在运行时将...
任何一个链接器和加载器的基本工作都非常简单: 将更抽象的名字与更底层的名字绑 定起来,好让程序员使用更抽象的名字编写代码。也就是说,它可以将程序员写的一个诸如 getline 的名字绑定到“iosys 模块内可执行...
链接器和加载器是软件开发过程中的重要组成部分,它们通常在编译器完成源代码编译成机器可执行代码之后工作。链接器负责将一个或多个目标文件链接成单一的可执行文件,而加载器则将可执行文件加载到内存中运行。...