编译

  1. 编译是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再转换为机器代码,生成目标文件(.obj)
  2. 编译分为两个阶段,分别是编译过程和汇编过程。编译过程又分为预处理阶段和编译、优化阶段。由源文件“.cpp/.c”生成“.i”文件,这是在预编译阶段完成的;gcc -E .cpp/.c —>.i
    预处理阶段:
    (1)宏#define
    (2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等
    (3)头文件包含,#include<iostream>
    (4)删除所有的注释“/**/”、”//”等;
    (5)特殊符号:LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换
    缺点:不进行任何安全性及合法性检查
    编译优化阶段:
    (1)针对代码优化,不依赖计算机
    (2)针对计算机优化
  3. 编译过程就是把经过预编译生成的文件进行一系列语法分析、词法分析、语义分析优化后生成相应的汇编代码文件。由“.i”文件生成“.s”文件,这是在编译阶段完成的;gcc -S .i —>.s
    主要功能:
    (1)词法分析:将源代码文件的字符序列划分为一系列的记号,一般词法分析产生的记号有:标识符、关键字、数字、字符串、特殊符号(加号、等号);在识别记号的同时也将标识符放好符号表、将数字、字符放入到文字表等;有一个lex程序可以实现词法扫描,会按照之前定义好的词法规则将输入的字符串分割成记号,所以编译器不需要独立的词法扫描器;
    (2)语法分析:语法分析器将对产生的记号进行语法分析,产生语法树—-就是以表达式为节点的树,一步步判断如何执行表达式操作。如果存在括号不匹配或者表达式错误,编译器就会报告语法分析阶段的错误;相同的存在一个yacc程序可以根据用户输入的语法规则生成语法树;
    (3)语义分析:由语法阶段完成分析的并没有赋予表达式或者其他实际的意义,比如乘法、加法、减法,必须经过语义阶段才能赋予其真正的意义;语义分析主要分为静态语义和动态语义两种;静态语义通常包括声明和类型的匹配、类型的转换。比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含了一个浮点型到整型转换的过程。只要存在类型不匹配编译器会报错。经过语义分析后的语法树的所有表达式都有了类型。动态语义分析只有在运行阶段才能确定;
    (4)优化后生成相应的汇编代码文件
    (5)汇总所有符号
  4. 汇编阶段:把汇编语言代码翻译成目标机器指令,生成目标文件(.o文件、.obj文件)。此过程会依赖机器的硬件和操作系统环境
  5. .o文件至少要提供3张表:
    (1)导出符号表:即该目标文件可以提供的符号及地址
    (2)未解决符号表:即找不到地址的符号的列表,告诉链接器这些符号没找到地址
    (3)地址重定向表:链接的时候,链接器会为目标文件的“未解决符号表”里的符号在其他目标文件中寻找地址,为了区分不同的文件,链接器在链接时就会对每个目标文件的地址进行调整。因为被加上了起始地址,所以符号在自身文件中的实际地址就不对了,需要再用一张地址重定向表记录符号相对自身文件的地址

链接

  1. 链接程序的主要工作就是将有关的目标文件(库文件、.o文件)彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
  2. 当链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址。最后把所有的目标文件的内容写在各自的位置上,再作一些另的工作,就生成一个可执行文件。
  3. 链接方式:
    (1)静态链接:函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。
    (2)动态链接:函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中 记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
  4. 链接阶段主要分为两部分:
    (1)合并所有“.obj”文件的段并调整段偏移和段长度(按照段的属性合并,属性可以是“可读可写”、“只读”、“可读可执行”,合并后将相同属性的组织在一个页面内,比较节省空间),合并符号表,进行符号解析完成后给符号分配地址;其中符号解析的意思是:所有.obj符号表中对符号引用的地方都要找到该符号定义的地方。在编译阶段,有数据的地方都是0地址,有函数的额地方都是下一行指令的偏移量-4(由于指针是4字节);可执行文件以页面对齐。在进行符号解析时要注意只对global符号进行处理,对于local符号不做处理;
    (2)符号的重定位(链接核心):将符号分配的虚拟地址写回原先未分配正确地址的地方对于数据符号会存准确地址,对于函数符号,相对于存下一行指令的偏移量(从PC寄存器取地址,并且PC中下一行指令的地址)

程序的运行

  1. 创建虚拟地址空间到物理空间的映射(创建内核地址映射结构体),创建页目录和页表;
  2. 加载代码段和数据段
  3. 把可执行文件的入口地址写到CPU的PC寄存器里
  4. 目标文件类型,Linux下的ELF文件主要有以下四种:
    (1)可重定位文件.obj,这种文件包括数据和指令,可以被链接成为可执行文件(.exe)或者共享目标文件(.so),静态链接库可以归为这一类;
    (2)可执行文件.exe,这种文件包含了可以直接运行的程序,它的代表就是ELF可执行文件,他们一般都没有扩展名;
    (3)共享目标文件.so,这种文件包含了数据和指令,可以在以下两种情况下使用:一是链接器使用这种文件与其他可重定位文件和共享目标文件链接,二是动态链接器将几个共享目标文件与可执行文件结合,作为进程映像的一部分使用。
    (4)核心转储文件,当进程意外终止时,系统可以将该进程的地址空间的内容及种植的一些信息转储到核心文件中,比如core dump文件。

常见注意事项

  1. extern:这就是告诉编译器,这个变量或函数在别的编译单元里定义了,也就是要把这个符号放到未解决符号表里面去(外部链接)
  2. static:如果该关键字位于全局函数或者变量的声明前面,表明该编译单元不导出这个函数或变量,因些这个符号不能在别的编译单元中使用(内部链接)。如果是static局部变量,则该变量的存储方式和全局变量一样,但是仍然不导出符号。
  3. 默认链接属性:对于函数和变量,默认链接是外部链接,对于const变量,默认内部链接。
  4. 外部链接的利弊:外部链接的符号在整个程序范围内都是可以使用的,这就要求其他编译单元不能导出相同的符号(不然就会报 duplicated external symbols)。
  5. 为什么头文件里一般只可以有声明不能有定义:头文件可以被多个编译单元包含,如果头文件里面有定义的话,那么每个包含这头文件的编译单元都会对同一个符号进行定义,如果该符号为外部链接,则会导致duplicated external symbols链接错误。
  6. 为什么公共使用的内联函数要定义于头文件里:因为编译时编译单元之间互不知道,如果内联被定义于.cpp文件中,编译其他使用该函数的编译单元的时候没有办法找到函数的定义,因些无法对函数进行展开(内联函数不展开,即不采用在使用处标记函数代码再跳转的方式,而是直接将代码嵌入)。所以如果内联函数定义于.cpp里,那么就只有这个.cpp文件能使用它。
  7. .h中的inline 函数可以被多个cpp包含而不造成符号冲突,因为它会被直接嵌入到调用的地方,内部联结不形成外部符号,对外不可见
  8. 为什么类的静态数据成员不可以就地初始化:因为类体一般是放在头文件中的,如果允许其静态成员就地初始化,那就相当于允许在头文件中定义变量了。