注重版权,若要转载烦请附上作者和链接
作者:Joshua_yi
链接:https://blog.csdn.net/weixin_44984664/article/details/109006690
文章目录
摘要
编译是编译程序读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再由汇编程序转换为机器语言,并且按照操作系统对可执行文件格式的要求链接生成可执行程序的一个过程。此次实验对本机上的 g++ 编译器编译 c++ 程序的过程作出了较为详细的分析报告。
一、引言
1、实验环境
-
我的平台:
deepin
-
使用的编译器:
g++
2、实验代码
以 g++ 编译器为研究对象,更深入地探究语言处理系统的完整工作过程。以斐波那契程序利用编译器的程序选项获得各阶段的输出,研究它们与源程序的关系。通过实验,了解预处理器、编译器、汇编器、链接器在整个程序处理过程中担当了什么样的角色,具体实现了什么功能。
斐波那契公式:
f n + 2 = f n + 1 + f n f_{n+2} = f_{n+1} + f_n fn+2=fn+1+fn
创建main.cpp
文件
代码如下
#include <iostream>
using namespace std;
int main()
{
int a, b, i, t, n;
a = 0;
b = 1;
i = 1;
cin >> n;
cout << a << endl;
cout << b << endl;
while (i < n)
{
t = b;
b = a + b;
cout << b << endl;
a = t;
i = i + 1;
}
return 0;
}
二、GUN&gcc&g++
1、简介
(1)GNU
GNU 是一个自由的操作系统,其内容软件完全以 GPL 方式发布。GNU是"GNU is Not Unix"的递归缩写. 该系统的基本组成包括GNU 编译器套装(GCC)、GNU 的 C 库(glibc)、以及 GNU 核心工具组(coreutils).
(2)gcc
gcc(GNU Compiler Collection),是 GNU 编译器套装,指一套编程语言编译器,以 GPL 及 LGPL 许可证所发行的自由软件,也是 GNU 计划的关键部分,也是 GNU 工具链的主要组成部分之一。gcc 是可以在多种硬体平台上编译出可执行程序的超级编译器,其执行效率与一般的编译器相比平均效率要高 20% 30%。gcc 编译器能将 C、C++ 语言源程序、汇程式化序和目标程序编译、链接成可执行文件,如果没有给出可执行文件的名字,gcc 将生成一个名为 a.out 的文件。在 Linux 系统中,可执行文件没有统一的后缀,系统从文件的属性来区分可执行文件和不可执行文件,而 gcc 则通过后缀来区别输入文件的类别。
(3)g++
g++也同gcc一样是GNU的编译器的套装 gcc和g++的主要区别
-
对于 *.c和*.cpp文件,gcc分别当做c和cpp文件编译(c和cpp的语法强度是不一样的)
-
对于 *.c和*.cpp文件,g++则统一当做cpp文件编译
-
使用g++编译文件时,g++会自动链接标准库STL,而gcc不会自动链接STL
-
gcc在编译C文件时,可使用的预定义宏是比较少的
-
gcc在编译cpp文件时/g++在编译c文件和cpp文件时(这时候gcc和g++调用的都是cpp文件的编译器),会加入一些额外的宏,这些宏如下:
#define __GXX_WEAK__ 1 #define __cplusplus 1 #define __DEPRECATED 1 #define __GNUG__ 4 #define __EXCEPTIONS 1 #define __private_extern__ extern
-
在用gcc编译c++文件时,为了能够使用STL,需要加参数 --lstdc++ 但这并不代表 gcc --lstdc++ 和 g++等价,它们的区别不仅仅是这个
主要参数
-g - turn on debugging (so GDB gives morefriendly output) -Wall - turns on most warnings -O or -O2 - turn on optimizations -o - name of the output file -c - output an object file (.o) -I - specify an includedirectory -L - specify a libdirectory -l - link with librarylib.a
使用示例:
g++ -ohelloworld -I/homes/me/randomplace/include helloworld.C
2、gcc执行过程
虽然我们称 gcc 是 C 语言的编译器,但使用 gcc 由 C 语言源代码文件生成可执行文件的过程不仅仅是编译的过程,而是要经历四个相互关联的步骤预处理 (也称预编译,Preprocessing)、编译 (Compilation)、汇编 (Assembly) 和连接 (Linking)。命令 gcc 首先调用 cpp 进行预处理,在预处理过程中,对源代码文件中的文件包含 (include)、预编译语句 (如宏定义 define 等) 进行分析。接着调用 cc1 进行编译,这个阶段根据输入文件生成以.o 为后缀的目标文件。汇编过程是针对汇编语言的步骤,调用 as 进行工作,一般来讲,.S为后缀的汇编语言源代码文件和汇编、.s 为后缀的汇编语言文件经过预编译和汇编之后都生成以.o 为后缀的目标文件。当所有的目标文件都生成之后,gcc 就调用 ld 来完成最后的关键性工作,这个阶段就是链接。在链接阶段,所有的目标文件被安排在可执行程序中的恰当的位置,同时,该程序所调用到的库函数也从各自所在的档案库中连到合适的地方。
三、预处理器
预处理器拥有源文件的翻译功能, 主要包含三个功能
-
有条件编译源文件的某些部分(由 #if、#ifdef、#ifndef、#else、#elif 和 #endif 指令控制)。
-
替换文本宏,同时可能对标识符进行拼接或加引号(由 #define 和 #undef 指令与 # 和 ##运算符控制)。
-
包含其他文件(由 #include 指令控制)
在源文件所在位置下,在命令行中输入以下命令得到预处理后的main.i文件:
g++ -E main.cpp>main.i
或者是
g++ -E main.cpp -o main.i
1、预处理器功能分析
包含文件替换在 main.i 的前半部分增加了很长一部分的代码。为了解该段代码的产生,删除了测试程序中引入头文件的操作( #include <iostream>),发现该段代码消失。所以可以得出预处理阶段对头文件进行了以引入替换。
可以简单的看一下main.cpp 和main.i两个文件的大小
也可以从i文件本身可以看出, 此处截取部分作为示例
......
extern wchar_t *wcscpy (wchar_t *__restrict __dest,
const wchar_t *__restrict __src)
throw () __attribute__ ((__nonnull__ (1, 2)));
extern wchar_t *wcsncpy (wchar_t *__restrict __dest,
const wchar_t *__restrict __src, size_t __n)
throw () __attribute__ ((__nonnull__ (1, 2)));
extern wchar_t *wcscat (wchar_t *__restrict __dest,
const wchar_t *__restrict __src)
throw () __attribute__ ((__nonnull__ (1, 2)));
extern wchar_t *wcsncat (wchar_t *__restrict __dest,
const wchar_t *__restrict __src, size_t __n)
throw () __attribute__ ((__nonnull__ (1, 2)));
extern int wcscmp (const wchar_t *__s1, const wchar_t *__s2)
throw () __attribute__ ((__pure__)) __attribute__ ((__nonnull__ (1, 2)));
extern int wcsncmp (const wchar_t *__s1, const wchar_t *__s2, size_t __n)
throw () __attribute__ ((__pure__)) __attribute__ ((__nonnull__ (1, 2)));
extern int wcscasecmp (const wchar_t *__s1, const wchar_t *__s2) throw ();
extern int wcsncasecmp (const wchar_t *__s1, const wchar_t *__s2,
size_t __n) throw ();
......
替换文本宏 在main.cpp添加一句
#define k 100
现在的main2.cpp文件为
#include <iostream>
#define k 100
using namespace std;
int main()
{
cout << k << endl;
int a, b, i, t, n;
a = 0;
b = 1;
i = 1;
cin >> n;
cout << a << endl;
cout << b << endl;
while (i < n)
{
t = b;
b = a + b;
cout << b << endl;
a = t;
i = i + 1;
}
return 0;
}
再次重新预处理生成main2.i 发现其中多了一句
cout << 100 << endl;
即直接对k替换为了100
有条件的编译
为研究预处理器的该项功能 这里选用另外一个更为典型的程序进行实验
#define a 100
#include <iostream>
using namespace std;
int main()
{
#ifdef a
cout << "1: yes" << endl;
#else
cout << "1: no" << endl;
#endif
#ifndef a
cout << "2: no1" << endl;
#elif ABCD == 2
cout << "2: yes" << endl;
#else
cout << "2: no2" << endl;
#endif
if !defined(b) && (a == 100)
cout<< "3: yes" << endl;
return 0;
}
在预处理后生成main3.i 在该文件最后部分变为
# 3 "main3.cpp"
using namespace std;
int main()
{
cout << "1: yes" << endl;
# 17 "main3.cpp"
cout << "2: no2" << endl;
if !defined(b) && (100 == 100)
cout<< "3: yes" << endl;
return 0;
}
由此看来,对于那些不执行的代码,在新的 main3.i 中,被直接略过。#if、#ifdef 和 #ifndef 后的表达式如果为真,则编译其控制的代码块。此时后续的 #else 和 #elif 指令将被忽略。否则,如果表达式为假,则将跳过其所控制的代码块,然后处理后续的 #else 或 #elif 指令
四、编译器
1、简介
编译器(compiler)是一种计算机程序,它会将某种编程语言写成的源代码(原始语言)转换成另一种编程语言(目标语言)。
它主要的目的是将便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序,也就是可执行文件。编译器将原始程序(source program)作为输入,翻译产生使用目标语言(target language)的等价程序。源代码一般为高级语言(High-level language),如Pascal、C、C++、C# 、Java等,而目标语言则是汇编语言或目标机器的目标代码(Object code),有时也称作机器代码(Machine code)。
编译器的主要工作流程分成6个阶段
-
对源文件进行扫描,将字符流拆分为一个个 token => [词法分析]{style=“color: red”}
-
根据规定好的语法规则将这些 token 构造出语法树,形成 token 间的层次关系 => [语法分析]{style=“color: red”}
-
对语法树各节点之间的关系进行检查其是否符合语义规则,并对语法树进行必要的优化 => [语义分析]{style=“color: red”}
-
以特定顺序遍历语法树的节点,将各节点转化为中间代码,并按照特定顺序拼装起来 => [中间代码生成]{style=“color: red”}
-
对中间代码进行优化 => [代码优化]{style=“color: red”}
-
由中间代码,按照相应的规则转化为目标代码–汇编码 => [目标代码生成]{style=“color: red”}
下面对编译器各个阶段做详细的解释说明
2、词法分析
词法分析是计算机中将字符序列转换为token序列的过程。语法分析器读取输入字符流、从中识别出语素、最后生成不同类型的标记。其间一旦发现无效标记,便会报错。其 中token是 一 个 字 串 , 是 构 成 源 代 码 的 最 小 单 位 。 从 输 入 字 符 流 中 生 成token的 过 程 叫作tokenization,并且词法分析器还会对token进行分类,记录token属于的类型。但是,词法分析器一般不会关心token之间的关系(属于语法分析),e.g. 词法分析器可以识别括号是token,但是不能保证括号是匹配的。通常token会用正则表达式进行定义。
词法阶段的实现可以分为两步:
扫 描 器 : 词法分析的第一阶段,通常基于有限状态自动机。扫描器能够识别其所能处理的标记中可能包含的所有字符序列(单个这样的字符序列即前面所说的"语素")。
评 估 器: 词法分析的第二阶段,评估器根据语素中的字符序列生成一个"值",这个"值"和语素的类型便构成了可以送入语法分析器的标记。
3、语法分析
它的作用是进行语法检查、并构建由输入的单词组成的数据结构(一般是语法分析树、抽象语法树等层次化的数据结构)。语法分析器通常使用词法分析器的结果------单词流作为其输入。
主要有两种方法完成:
自顶向下分析: 根据形式语法规则,在语法分析树的自顶向下展开中搜索输入符号串可能的最左推导。单词按从左到右的顺序依次使用。
自底向上分析: 语法分析器从现有的输入符号串开始,尝试将其根据给定的形式语法规则进行改写,最终改写为语法的起始符号。
(1)不同等级优化
g++ -O0 -fdump-tree-all-graph main.cpp
g++ -O1 -fdump-tree-all-graph main.cpp
g++ -O2 -fdump-tree-all-graph main.cpp
g++ -O3 -fdump-tree-all-graph main.cpp
g++ -Os -fdump-tree-all-graph main.cpp
执行上述命令,并在vscode中产生编译阶段的控制流图,同时改变不同优化等级,观察流图之间的差异
4、语义分析
语义分析是编译过程的一个逻辑阶段, 语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。
5、中间代码生成
中间代码生成是产生中间代码的过程。所谓"中间代码"是一种结构简单、含义明确的记号系统,这种记号系统复杂性介于源程序语言和机器语言之间,容易将它翻译成目标代码。
6、代码优化
进行中间代码优化的目的是为了产生更加高效的代码, 他要遵循一定的原则:
等价原则: 经过优化后不应该改变程序运行的结果。
有效原则: 使优化后所产生的目标代码运行时间较短,占用的储存空间较小。
合算原则: 应尽可能以较低的代价取得较好的优化效果。
7、目标代码生成
以中间表示形式作为输入,将其映射到目标语言 使用命令
g++ -S main.cpp
对比不同等级的优化生成出的汇编代码
g++ -S main.cpp -o main.s
g++ -S -O0 main.cpp -o main_O0.s
g++ -S -O1 main.cpp -o main_O1.s
g++ -S -O2 main.cpp -o main_O2.s
g++ -S -O3 main.cpp -o main_O3.s
g++ -S -Os main.cpp -o main_Os.s
之后通过script记录命令的输入输出到typescript 文件中
由diff命令分析main.s, main_O0.s, main_O1.s, main_O2.s, main_O3.s生成文件的不同 直观上来看 main.s和main_O0.s文件, 以及main_O2.s, main_O3.s文件没有差别
由此说明默认情况下是不对代码进行优化的
其他文件之间的不同之处较多,在此截取一部分作为展示
由于笔者本人对汇编语言掌握有限, 很难对这几个.s文件进行深入层次的分析,这也是之后需要学习和改进的地方。
但是可以查找到相关的分析资料, 从中的得出一些不同优化等级的区别与联系。
-
O0选项不进行任何优化,在这种情况下,编译器尽量的缩短编译消耗(时间,空间),此时,debug会产出和程序预期的结果。当程序运行被断点打断,此时程序内的各种声明是独立的,我们可以任意的给变量赋值,或者在函数体内把程序计数器指到其他语句,以及从源程序中 精确地获取你期待的结果。
-
O1优化会消耗少多的编译时间,它主要对代码的分支,常量以及表达式等进行优化。
-
O2会尝试更多的寄存器级的优化以及指令级的优化,它会在编译期间占用更多的内存和编译时间。
-
O3在O2的基础上进行更多的优化,例如使用伪寄存器网络,普通函数的内联,以及针对循环的更多优化。
-
Os主要是对代码大小的优化,我们基本不用做更多的关心。 通常各种优化都会打乱程序的结构,让调试工作变得无从着手。并且会打乱执行顺序,依赖内存操作顺序的程序需要做相关处理才能确保程序的正确性
由于篇幅有限, 更加详细的说明请阅读
https://blog.csdn.net/qq_31108501/article/details/51842166
8、使用不同版本进行编译
在对源程序进行编译时,g++ 给用户提供了两个版本:colorredrelease 和 colorreddebug。
通常 Debug 配置选择较低的优化级别以方便调试 (-O0),包括不会删除无用的代码优化执行效率,可以让代码单步执行时与源代码匹配。
Release 配置会合并代码优化速度,在单步执行的时候与源代码匹配效果较差。
Debug 配置还有一个优点是变量的生存周期会比其作用范围大以方便调试,而 Release 配置生成的代码变量空间会充分复用,这样会导致变量在超出有效范围之后就可能由于被复用而被改变。
实现 debug 和 release 版本的命令:
g++ -c -g -Ddebug main.cpp -o main_d.s
g++ -c -O3 main.cpp -o main_r.s
可以看出 debug 版本下生成的汇编代码量远大于 release
五、汇编器
1、简介
汇编器是将汇编语言翻译成机器语言的程序。汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。 执行该指令
g++ -c main.s -o main.o
生成二进制文件
查看该文件,发现全部为乱码不易阅读
2、nm分析main.o文件
通过 nm命令深度挖掘 main.o文件的结构
使用 nm 产生的是二进制目标文件的符号表,包括符号地址、符号类型、符号名等. 命令
nm main.o
对于 nm 命令列出的每个符号,它们的值使用十六进制来表示,并且在该符号前面加上了一个表示符号类型的编码字符。
类型表示:
-
符号位于非初始化数据区,用于 small object。
-
该符号位于代码区 text section。
-
该符号在当前文件中是未定义的,即该符号的定义在别的文件中。
此外,还通过给 nm 命令加上各种参数 (例如,-a,-u,-g…),查看命令行返回内容,探究 main.o 的组成
3、objdump分析main.o文件
objdump 产生的是目标文件或者可执行的目标文件对应的汇编代码
objdump 命令:是用查看目标文件或者可执行的目标文件的构成的 g++ 工具。打印程序主要段的信息:
objdump -h main.o
打印出结果
main.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000127 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000167 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000001 0000000000000000 0000000000000000 00000167 2**0
ALLOC
3 .rodata 00000001 0000000000000000 0000000000000000 00000167 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .init_array 00000008 0000000000000000 0000000000000000 00000168 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
5 .comment 0000001e 0000000000000000 0000000000000000 00000170 2**0
CONTENTS, READONLY
6 .note.GNU-stack 00000000 0000000000000000 0000000000000000 0000018e 2**0
CONTENTS, READONLY
7 .eh_frame 00000078 0000000000000000 0000000000000000 00000190 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
TEXT:代码段 (存放函数的二进制机器指令) DATA:数据段 (存已初始化的局部/全局静态变量、未初始化的全局静态变量)显示 main.o 文件中的 text 段的内容:
反汇编 main.o 中的 text 段内容,并尽可能用源代码形式表示:
objdump -j .text -S main.o
打印结果
main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 20 sub $0x20,%rsp
8: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
f: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
16: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%rbp)
1d: 48 8d 45 ec lea -0x14(%rbp),%rax
21: 48 89 c6 mov %rax,%rsi
24: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 2b <main+0x2b>
2b: e8 00 00 00 00 callq 30 <main+0x30>
30: 8b 45 fc mov -0x4(%rbp),%eax
33: 89 c6 mov %eax,%esi
35: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 3c <main+0x3c>
3c: e8 00 00 00 00 callq 41 <main+0x41>
41: 48 89 c2 mov %rax,%rdx
44: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # 4b <main+0x4b>
4b: 48 89 c6 mov %rax,%rsi
4e: 48 89 d7 mov %rdx,%rdi
51: e8 00 00 00 00 callq 56 <main+0x56>
56: 8b 45 f8 mov -0x8(%rbp),%eax
59: 89 c6 mov %eax,%esi
5b: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 62 <main+0x62>
62: e8 00 00 00 00 callq 67 <main+0x67>
67: 48 89 c2 mov %rax,%rdx
6a: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # 71 <main+0x71>
71: 48 89 c6 mov %rax,%rsi
74: 48 89 d7 mov %rdx,%rdi
77: e8 00 00 00 00 callq 7c <main+0x7c>
7c: 8b 45 ec mov -0x14(%rbp),%eax
7f: 39 45 f4 cmp %eax,-0xc(%rbp)
82: 7d 3e jge c2 <main+0xc2>
84: 8b 45 f8 mov -0x8(%rbp),%eax
87: 89 45 f0 mov %eax,-0x10(%rbp)
8a: 8b 45 fc mov -0x4(%rbp),%eax
8d: 01 45 f8 add %eax,-0x8(%rbp)
90: 8b 45 f8 mov -0x8(%rbp),%eax
93: 89 c6 mov %eax,%esi
95: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 9c <main+0x9c>
9c: e8 00 00 00 00 callq a1 <main+0xa1>
a1: 48 89 c2 mov %rax,%rdx
a4: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # ab <main+0xab>
ab: 48 89 c6 mov %rax,%rsi
ae: 48 89 d7 mov %rdx,%rdi
b1: e8 00 00 00 00 callq b6 <main+0xb6>
b6: 8b 45 f0 mov -0x10(%rbp),%eax
b9: 89 45 fc mov %eax,-0x4(%rbp)
bc: 83 45 f4 01 addl $0x1,-0xc(%rbp)
c0: eb ba jmp 7c <main+0x7c>
c2: b8 00 00 00 00 mov $0x0,%eax
c7: c9 leaveq
c8: c3 retq
00000000000000c9 <_Z41__static_initialization_and_destruction_0ii>:
c9: 55 push %rbp
ca: 48 89 e5 mov %rsp,%rbp
cd: 48 83 ec 10 sub $0x10,%rsp
d1: 89 7d fc mov %edi,-0x4(%rbp)
d4: 89 75 f8 mov %esi,-0x8(%rbp)
d7: 83 7d fc 01 cmpl $0x1,-0x4(%rbp)
db: 75 32 jne 10f <_Z41__static_initialization_and_destruction_0ii+0x46>
dd: 81 7d f8 ff ff 00 00 cmpl $0xffff,-0x8(%rbp)
e4: 75 29 jne 10f <_Z41__static_initialization_and_destruction_0ii+0x46>
e6: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # ed <_Z41__static_initialization_and_destruction_0ii+0x24>
ed: e8 00 00 00 00 callq f2 <_Z41__static_initialization_and_destruction_0ii+0x29>
f2: 48 8d 15 00 00 00 00 lea 0x0(%rip),%rdx # f9 <_Z41__static_initialization_and_destruction_0ii+0x30>
f9: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 100 <_Z41__static_initialization_and_destruction_0ii+0x37>
100: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # 107 <_Z41__static_initialization_and_destruction_0ii+0x3e>
107: 48 89 c7 mov %rax,%rdi
10a: e8 00 00 00 00 callq 10f <_Z41__static_initialization_and_destruction_0ii+0x46>
10f: 90 nop
110: c9 leaveq
111: c3 retq
0000000000000112 <_GLOBAL__sub_I_main>:
112: 55 push %rbp
113: 48 89 e5 mov %rsp,%rbp
116: be ff ff 00 00 mov $0xffff,%esi
11b: bf 01 00 00 00 mov $0x1,%edi
120: e8 a4 ff ff ff callq c9 <_Z41__static_initialization_and_destruction_0ii>
125: 5d pop %rbp
126: c3 retq
六、链接器, 加载器
链接过程将多个目标文以及所需的库文件 (.so 等) 链接成最终的可执行文件 (executablefile)。
-
程序加载:将程序从辅助存储设备拷贝到主内存中准备运行。在某些情况下,加载仅仅是将数据从磁盘拷入内存;在其他情况下,还包括分配存储空间,设置保护位或通过虚拟内存将虚拟地址映射到磁盘内存页上。
-
重定位:编译器和汇编器通常为每个文件创建程序地址从 0 开始的目标代码,但是几乎没有计算机会允许从地址 0 加载你的程序。如果一个程序是由多个子程序组成的,那么所有的子程序必须被加载到互不重叠的地址上。重定位就是为程序不同部分分配加载地址,调整程序中的数据和代码以反映所分配地址的过程。在很多系统中,重定位不止进行一次。对于链接器的一种普遍情景是由多个子程序来构建一个程序,并生成一个链接好的起始地址为 0 的输出程序,各个子程序通过重定位在大程序中确定位置。当这个程序被加载时,系统会选择一个加载地址,而链接好的程序会作为整体被重定位到加载地址。
-
符号解析:当通过多个子程序来构建一个程序时,子程序间的相互引用是通过符号进行的;主程序可能会调用一个名为 sqrt 的计算平方根例程,并且数学库中定义了 sqrt 例程。链接器通过标明分配给 sqrt 的地址在库中来解析这个符号,并通过修改目标代码使得 call 指令引用该地址。
链接器和加载器共有的一个重要特性就是他们都会修改目标代码,他们也许是唯一比调试程序在这方面应用更为广泛的程序。这是一个独特而强大的特性,而且细节非常依赖于机器的规格,如果做错的话就会引起 bug。使用如下命令获得可执行文件:
使用命令
g++ -g -o main main.cpp
七、自动并行化处理
1、OpenMP框架
OpenMP 是一个跨平台的多线程实现,主线程 (顺序的执行指令) 生成一系列的子线程,并将任务划分给这些子线程进行执行。这些子线程并行的运行,由运行时环境将线程分配给不同的处理器。OpenMp 提供了对并行算法的高层的抽象描述,程序员通过在源代码中加入专用的colorredpragma 来指明自己的意图,由此编译器可以自动将程序进行并行化。 在这里,我们的g++ 编译器支持 OpenMP 框架的使用,实现编译器的自动并行化。
2、OpenMp的使用
先选用一个简单的例子进行测试
#include <iostream>
using namespace std;
int main()
{
#pragma omp parallel
{
cout << "Hello world!" << endl;
}
return 0;
}
之后我们在命令行中进行编译
# 生成main4.out可执行程序
g++ main4.cpp -o main4.out
# 执行main4.out
./main4.out
在terminal中打印出一条
Hello world!
接着使用OpenMP框架
# 生成main4.cout可执行程序
g++ -fopenmp main4.cpp -o main4.out
# 执行main4.out
./main4.out
在terminal中打印出8条Hello world!
Hello world!Hello world!
Hello world!Hello world!
Hello world!Hello world!
Hello world!
Hello world!
可以看出,在运行 a.out 文件时,直接在控制台打印出了 8 行"Hello World!"。
这是因为#pragma omp parallel 仅在指定了 -fopenmp 编译器选项后才会发挥作用。
原理:在编译期间,GCC 会根据硬件和操作系统配置在运行时生成代码,创建尽可能多的线程。 每个线程的起始例程为代码块中位于 #pragma omp parallel 指令之后的代码。 由于本台机器使用的是 Intel® Core™ i7-8550U CPU @ 1.80GHz 处理器
从bash命令中可以看出有8个线程
joshua@joshua-PC:~$ grep 'processor' /proc/cpuinfo | wc -l
8
OpenMP和for循环结合
在 for 循环中使用 parallel for 指令进行并行处理
测试代码如下
#include <omp.h>
#include <math.h>
#include <time.h>
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
int i, nthreads;
clock_t clock_timer;
double wall_timer;
double c[1000000];
for (nthreads = 1; nthreads <= 8; ++nthreads)
{
clock_timer = clock();
wall_timer = omp_get_wtime();
#pragma omp parallel for private(i) num_threads(nthreads)
for (i = 0; i < 1000000; i++)
c[i] = sqrt(i * 4 + i * 2 + i);
cout << "threads: " << nthreads << " time on clock(): " << (double)(clock() - clock_timer) / CLOCKS_PER_SEC
<< " time on wall: " << omp_get_wtime() - wall_timer << "\n";
}
}
可以通过不断增加线程的数量来计算运行内部 for 循环的时间。omp_get_wtime API 从一些任意的但是一致的点返回已用去的时间,以秒为单位。因此,omp_get_wtime() - wall_timer 将返回观察到的所用时间并运行 for 循环。clock() 系统调用用于预估整个程序的处理器使用时间,也就是说,将各个特定于线程的处理器使用时间相加, 然后报告最终的结果。
结果如下图
八、结论
通过研究测试代码的编译流程,并结合资料进行分析整理, 对整个编译系统的执行过程有了大致的了解。也同时发现了其中许多的问题希望可以在之后学习中找到答案。