1.从.c文件到可执行文件,其间经历了几步?
高级语言是偏向人,按照人的思维方式设计的,机器对这些可是莫名奇妙,不知所谓。那从高级语言是如何过渡到机器语言的呢?这可是一个漫长的旅途呀!
其中,得经历这样的历程:C源程序->编译预处理->编译->汇编程序->链接程序->可执行文件
1.预处理 读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。伪指令主要包括以下四个方面:
(1)宏定义指令,如#define Name TokenString,#undef等。对于前一个伪指令,预编译所要作得的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
(2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif,等等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
(3)加载头文件,如#include"FileName"或者#include<FileName>等。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。在程序中#include它们要使用尖括号(<>)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号("")。
预编译是将.c文件转化成 .i文件,
重定向使用的gcc命令是:gcc –E hello.c >hello.i
在预处理阶段是不做语法检查的。
2.编译阶段 :
需要进行三个步骤:词法分析、语法分析和语义分析
在linux环境中,输入命令:gcc–s hello.c 参数c告诉gcc命令只进行编译,不做其他处理。命令运行结束后产生hello.o的目标文件。
3.汇编过程
编译过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。
输入命令:gcc –c hello.c
就会生成hello.o的目标文件。
4.链接过程
链接就是将不同部分的代码和数据收集和组合成为一个单一文件的过程,这个文件可被加载或拷贝到存储器执行.
链接可以执行与编译时(源代码被翻译成机器代码时),也可以执行与加载时(在程序被 加载器加载到存储器并执行时),甚至执行与运行时,由应用程序来执行.在现代系统中, 链接是由链接器自动执行的.
链接器分为:静态链接器和动态链接器两种.
(1).静态链接器
静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出.
静态链接器主要完成两个任务:
1>符号解析:目标文件定义和引用符号.符号解析的目的在于将每个符号引用和一个符号定义联系起来.
2>重定位:编译器和汇编器生成从地址零开始的代码和数据节.链接器通过把每个符号定义和一个存储器位置联系起来,然后修改所有对这些符号的引用,使得他们执行这个存储位置,从而重定位这些节.
(2)动态链接器
共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并在存储器中和一个程序链接起来.这个过程称为动态链接,是由动态链接器完成的.
共享库的共享在两个方面有所不同.首先,在任何给定的文件系统中,对于一个库只有一个.so文件.所有引用该库德可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库的内容那样被拷贝和嵌入到引用它们的可执行的文件中.其次,在存储器中,一个共享库的.text只有一个副本可以被不同的正在运行的进程共享。
2.-O1,-O2,-O3为何方神圣,它们是如何优化编译文件的?
(1)首先,她们的真面目是:
-O1 提供基础级别的优化
-O2提供更加高级的代码优化,会占用更长的编译时间
-O3提供*的代码优化
可以使用-f命令行选项引用每个单独的优化技术。
1, 编译器优化级别1
在优化的第一个级别执行基础代码的优化
这个级别试图执行9种单独的优化功能:
(1).-fdefer-pop: 这种优化技术与汇编语言代码在函数完成时如何进行操作有关。
(2).-fmerge-constans: 使用这种优化技术,编译器试图合并相同的常量.
(3) . -fthread-jumps: 使用这种优化技术与编译器如何处理汇编代码中的条件和非条件分支有关。 在某些情况下,一条跳转指令可能转移到另一条分支语句。 通过一连串跳转, 编译器确定多个跳转之间的最终目标并且把第一个跳转重新定向到最终目标。
(4).-floop-optimize:通过优化如何生成汇编语言中的循环, 编译器可以在很大程序上提高应用程序的性能。通常,程序由很多大型且复杂的循环构成。 通过删除在循环内没有改变值的变量赋值操作, 可以减少循环内执行指令的数量, 在很大程度上提高性能。 此外优化那些确定何时离开循环的条件分支,以便减少分支的影响。
(5).-fif-conversion: if-then语句应该是应用程序中仅次于循环的最消耗时间的部分。简单的if-then语句可能在最终的汇编语言代码中产生众多的条件分支。通过减少或者删除条件分支, 以及使用条件传送 设置标志和使用运算技巧来替换他们,编译器可以减少if-then语句中花费的时间量。
(6)-fif-conversion2: 这种技术结合更加高级的数学特性, 减少实现if-then语句所需的条件分支。
(7)-fdelayed-branch: 这种技术试图根据指令周期时间重新安排指令。 它还试图把尽可能多的指令移动到条件分支前, 以便最充分的利用处理器的治理缓存。
(8) -fguess-branch-probability:就像其名称所暗示的,这种技术试图确定条件分支最可能的结果, 并且相应的移动指令,这和延迟分支技术类似。因为在编译时预测代码的安排,所以使用这一选项两次编译相同的c或者c++代码很可能会产生不同的汇编语言代码,这取决于编译时编译器认为会使用那些分支。
(9)-fcprop-registers: 因为在函数中把寄存器分配给变量,所以编译器执行第二次检查以便减少调度依赖性(两个段要求使用相同的寄存器)并且删除不必要的寄存器复制操作。
2, 编译器优化级别2
结合了第一个级别的所有优化技术,再加上一下一些优化:
(1)-fforce-mem: 这种优化在任何指令使用变量前,强制把存放再内存位置中的所有变量都复制到寄存器中。 对于只涉及单一指令的变量, 这样也许不会有很大的优化效果. 但是对于在很多指令(必须数学操作)中都涉及到的变量来说, 这会时很显著的优化, 因为和访问内存中的值相比 ,处理器访问寄存器中的值要快的多。
(2)-foptimize-sibling-calls: 这种技术处理相关的和/或者递归的函数调用。通常,递归的函数调用可以被展开为一系列一般的指令, 而不是使用分支。
(3)-fstrength-reduce: 这种优化技术对循环执行优化并且删除迭代变量。 迭代变量是捆绑到循环计数器的变量, 比如使用变量, 然后使用循环计数器变量执行数学操作的for-next循环。
(4)-fgcse: 这些优化操作试图分析生成的汇编语言代码并且结合通用片段, 消除冗余的代码段。如果代码使用计算性的goto,gcc指令推荐
(5)-fcse-follow-jumps: 这种特别的通用子表达式消除技术扫描跳转指令, 查找程序中通过任何其他途径都不会到达的目标代码。这种情况最常见的例子就式if-then-else语句的else部分。
(6)-frerun-cse-after-loop: 这种技术在对任何循环已经进行过优化之后重新运行通用子表达式消除例程。这样确保在展开循环代码之后更进一步地优化还编代码。
(7)-fdelete-null-pointer-checks: 这种优化技术扫描生成的汇编语言代码, 查找检查空指针的代码。
(8)-fextensive-optimizations: 这种技术执行从编译时的角度来说代价高昂的各种优化技术,但是它可能对运行时的性能产生负面影响。
(9)-fregmove: 编译器试图重新分配mov指令中使用的寄存器, 并且将其作为其他指令操作数, 以便最大化捆绑的寄存器的数量。
(10)-fschedule-insns: 编译器将试图重新安排指令, 以便消除等待数据的处理器。对于在进行浮点运算时有延迟的处理器来说, 这使处理器在等待浮点结果时可以加载其他指令。
(11)-fsched-interblock: 这种技术使编译器能够跨越指令块调度指令。 这可以非常灵活地移动指令以便等待期间完成的工作最大化。
(12)-fcaller-saves: 这个选项指示编译器对函数调用保存和恢复寄存器, 使函数能够访问寄存器值, 而且不必保存和恢复他们。 如果调用多个函数, 这样能够节省时间, 因为只进行一次寄存器的保存和恢复操作, 而不是在每个函数调用中都进行。
(13)-fpeephole2: 这个选项允许进行任何计算机特定的观察孔优化。
(14)-freorder-blocks: 这种优化技术允许重新安排指令块以便改进分支操作和代码局部性。
(15)-fstrict-aliasing: 这种技术强制实行高级语言的严格变量规则。 对于c和c++程序来说, 它确保不在数据类型之间共享变量. 例如, 整数变量不和单精度浮点变量使用相同的内存位置。
(16)-funit-at-a-time:这种优化技术指示编译器在运行优化例程之前读取整个汇编语言代码。这使编译器可以重新安排不消耗大量时间的代码以便优化指令缓存。
(17)-falign-functions:这个选项用于使函数对准内存中特定边界的开始位置。大多数处理器按照页面读取内存,并且确保全部函数代码位于单一内存页面内,就不需要叫化代码所需的页面。
(18)-fcrossjumping: 这是对跨越跳转的转换代码处理, 以便组合分散在程序各处的相同代码。 这样可以减少代码的长度,但是也许不会对程序性能有直接影响。
3, 编译器优化级别3
它整合了第一和第二级别中的左右优化技巧, 还包括一下优化:
-finline-functions:这种优化技术不为函数创建单独的汇编语言代码,而是把函数代码包含在调度程序的
代码中。 对于多次被调用的函数来说, 为每次函数调用复制函数代码。 虽然这样对于减少代码长度不利, 但是通过最充分的利用指令缓存代码, 而不是在每次函数调用时进行分支操作, 可以提高性能。
-fweb: 构建用于保存变量的伪寄存器网络。 伪寄存器包含数据,就像他们是寄存器一样, 但是可以使用各种其他优化技术进行优化, 比如cse和loop优化技术。
-fgcse-after-reload:这中技术在完全重新加载生成的且优化后的汇编语言代码之后执行第二次gcse优化,帮助消除不同优化方式创建的任何冗余段。