1. 预编译过程主要处理那些源代码文件中的以#开始的预编译指令. 比如#include,#define等, 主要处理规则如下:
"""
- 将所有的#define删除, 并且展开所有的宏定义
- 处理所有条件预编译指令, 比如#if,#ifdef,#elif,#else,#endif
- 处理#include预编译指令, 将被包含的文件插入到该预编译指令的位置. 注意, 这个过程是递归进行的,
也就是说被包含的文件可能还包含其他文件
- 删除所有的注释//和/* */
- 添加行号和文件名标识, 以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号
- 保留所有的#pragma编译器指令, 因为编译器须要使用它们
"""
2. 编译过程就是把预处理完的文件进行一系列词法分析, 语法分析, 语义分析及优化后生产相应的汇编代码文件
"""
- 现在版本的GCC把预编译和编译两个步骤合并成一个步骤, 使用一个叫做cc1的程序来完成这两个步骤
- 对于C语言的代码来说, 这个预编译和编译的程序是cc1; 对于c++来说, 有对应的程序叫做cc1plus; objective-C是cc1
- fortan是f771; Java是jc1. 所以实际上gcc这个命令只是这些后台程序的包装, 它会根据不同的参数要求去
调用预编译程序cc1, 汇编器as, 链接器ld
"""
3. 词法分析
"""
- 首先源代码程序被输入到扫描器, 扫描器的任务很简单, 它只是简单地进行词法分析,
运用一种类似于有限状态机的算法可以很轻松地将源代码的字符序列分割成一系列的记号。
- 词法分析产生的记号一般可以分为如下几类: 关键字, 标识符, 字面量和特殊符号. 在识别记号的同时,
扫描器也完成了其他工作.比如将标识符放到符号表, 将数字, 字符串常量存放到文字表等, 以备后面的步骤使用.
- 有一个叫做lex的程序可以实现词法扫描, 它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号.
"""
4. 语法分析
"""
- 接下来语法分析器将对由扫描器产生的记号进行语法分析,从而产生语法树。整个分析过程采用上下文无关语法的分析手段。
- 正如前面词法分析有lex一样,语法分析也有一个现成的工具叫做yacc。它也像lex一样,可以根据用户给定的语法规则对输入
的记号序列进行解析,从而构建出一棵语法树。
"""
5. 语义分析
"""
- 接下来进行的是语义分析,由语义分析器来完成。语法分析仅仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句
是否真正有意义。比如C语言里面两个指针做乘法运算是没有意义的,但是这个语句在语法上是合法的;
- 编译器所能分析的语义是静态语义,所谓静态语义是指在编译期可以确定的语义,与之对应的动态语义就是只有在运行期才能确定的语义
- 静态语义通常包括声明和类型的匹配,类型的转换。比如当一个浮点型的表达式赋值给一个整数的表达式时,
其中包含了一个浮点型到整型转换的过程,语义分析过程中需要完成这个步骤。比如将一个浮点型赋值给一个指针的时候,
语义分析程序会发现这个类型不匹配,编译器将会报错。
- 动态语义一般指在运行期出现的语义相关的问题,比如将0作为除数是一个运行期语义错误。
"""
6. 中间语言生成
"""
- 现代的编译器有着很多层次的优化,往往在源代码级别会有一个优化过程。我们这里所描述的源码级优化器在不同编译器中
可能会有不同的定义和一些其他的差异。
- 源代码优化器往往将整个语法树转换成中间代码,它是语法树的顺序表示,其实已经非常接近目标代码了。但是它一般跟目标机器
和运行时环境是无关的,比如它不包含数据的尺寸,变量地址和寄存器的名字等。
- 中间代码有很多种类型,在不同的编译器中有着不同的形式,比较常见的有:三地址码和P-代码。
"""
7. 目标代码生成与优化
"""
- 源代码级优化器产生中间代码标志着下面的过程都属于编译器后端。编译器后端主要包括代码生成器和目标代码优化器。
- 代码生成器将中间代码转换成目标机器代码,这个过程十分依赖于目标机器,因为不同的机器有着不同的字长,寄存器,
整数数据类型和浮点数数据类型等。
- 最后目标代码优化器对上述的目标代码进行优化,比如选择合适的寻址方式,使用位移来代替乘法运算,删除多余的指令等。
-
- 中间代码使得编译器可以被分为前端和后端. 编译器负责产生机器无关的中间代码, 编译器后端将中间代码转换成目标机器代码
"""
8. 链接
"""
- 最常见的属于静态语言的C/C++模块之间通信有两种方式,一种是模块间的函数调用,另一种是模块间的变量访问。
函数访问须知道目标函数的地址,变量访问也须知道目标变量的地址,所以这两种方式都可以归结为一种方式,那就是模块间符号的引用
- 模块间依靠符号来通信类似于拼图版,定义符号的模块多出一块区域,引用该符号的模块刚好少了那一块区域,两者一拼接刚好完美组合
- 链接过程主要包括了地址和空间分配,符号决议和重定位这些步骤。
- 最常见的库是运行时库(Runtime Library),它是支持程序运行的基本函数的集合。库其实是一组目标文件的包,
就是一些最常用的代码编译成目标文件后打包存放。
- 地址修正的过程也被叫做重定位,每个要被修正的地方叫一个重定位入口。重定位所做的就是给程序中每个这样的绝对地址引用的
位置打补丁,使它们指向正确的地址。
"""
9. 编译命令
- 预编译:gcc -E hello.c -o hello.i 或者 cpp hello.c > hello.i
- 编译: gcc -S hello.i -o hello.s
- 汇编: as helloc.s -o hello.o 或者 gcc -c hello.s -o hello.c
- 查找: find /usr/lib -name crt1.o
- 依赖: gcc -static --verbose -fno-builtin hello.c
ld -static \
/usr/lib/x86_64-linux-gnu/crt1.o \
/usr/lib/x86_64-linux-gnu/crti.o \
/usr/lib/gcc/x86_64-linux-gnu/11/crtbeginT.o \
hello.o \
-L/usr/lib/gcc/x86_64-linux-gnu/11 \
-L/usr/lib/x86_64-linux-gnu \
-start-group \
-lgcc \
-lgcc_eh \
-lc \
-end-group \
/usr/lib/gcc/x86_64-linux-gnu/11/crtend.o \
/usr/lib/x86_64-linux-gnu/crtn.o
10. 静态库制作
- gcc -c *.c -I ../include
- ar rcs libcalc.a *.o
- gcc main.c -o app -I ./include -lcalc -L ./lib
11. 动态库制作
- gcc -c -fPIC *.c -I ../include/ 得到位置无关的代码
- gcc -shared *.o -o libcalc.so 得到动态库
- gcc main.c -I include/ -L lib -lcalc -o main 链接动态库
- ldd 检查动态库依赖关系
- 当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径。此时就需要系统的动态载入器来获取该绝对路径
-
- 对于elf格式的可执行程序,是由ld-linux.so来完成的,它先后搜索elf文件的
DT_RPATH段 ——> 环境变量 LD_LIBRARY_PATH ——> /etc/ld.so.cache文件列表
- export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/autodl-tmp/calc/lib 临时环境变量
- vi /etc/ld.so.conf 写入依赖库的路径 并 sudo ldconfig
12. GCC常见编译选项
-E: 预处理指定的源文件, 不进行编译
-S: 编译指定的源文件, 但是不进行汇编
-c: 编译, 汇编指定的源文件,但是不进行链接
-o [file1] [file2] / [file2] -o [file1]: 将文件 file2 编译成可执行文件 file1
-I directory: 指定 include 包含文件的搜索目录
-g: 在编译的时候,生成调试信息,该程序可以被调试器调试
-D: 在程序编译的时候,指定一个宏
-w: 不生成任何警告信息
-Wall: 生成所有警告信息
-On: 编译器优化选项的4个级别
-l: 在程序编译的时候,指定使用的库
-L: 指定编译的时候,搜索的库的路径
-fpic/-fPIC: 生成与位置无关的代码
-shared: 生成共享目标文件,通常用在建立共享库时
13. 线程
"""
一个标准的线程由线程ID, 当前指令指针(PC), 寄存器集合和堆栈组成. 通常意义上, 一个进程由一个到多个线程组成,
各个线程之间共享程序的
内存空间(包括代码段, 数据段, 堆等)及一些进程级的资源(如打开文件和信号).
- 线程的访问权限
1) 线程私有: 局部变量, 函数的参数, 线程局部存储(TLS数据)
2) 线程共享: 全局变量, 堆上的数据, 函数里的静态变量, 程序代码, 打开文件
"""
14. 线程安全
"""
- 竞争与原子操作 - 同步与锁 - 可重入与线程安全
- volatile 关键字试图阻止编译器过度优化
1): 阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回.
2): 阻止编译器调整操作volatile变量的指令顺序.
"""
15. Makefile 规则
"""
- 一个 Makefile 文件中可以有一个或者多个规则
目标 ...: 依赖 ...
命令 (shell命令)
...
- 目标:最终要生成的文件(伪目标除外)
- 依赖: 生成目标所需要的文件或是目标
- 命令: 通过执行命令对依赖操作生成目标
- Makefile 中的其他规则一般都是为第一条规则服务的
- 命令在执行之前,需要先检查规则中的依赖是否存在
- 检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间
"""
16. 变量
"""
- 自定义变量: 变量名=变量值 var=hello $(var)
- 预定义变量:
- AR: 归档维护程序的名称,默认值是 ar
- CC: C 编译器的名称,默认值为 cc
- CXX: C++编译器的名称,默认值为 g++
- $@: 目标的完整名称
- $<: 第一个依赖文件的名称
- $^: 所有的依赖文件
"""
17. GDB调试
"""
- gcc -g -Wall program.c -o program
- 启动和退出:
gdb: 可执行程序 quit: 退出
- 给程序设置参数/获取设置参数
set args 10 20
show args
- GDB使用帮助
help
- 查看文件代码
list -> 从默认位置显示 list 行号 -> 从指定的行显示
list 函数名 -> 从指定的函数显示
- 查看非当前文件代码
list 文件名:行号 list 文件名:函数名
- 设置断点
- break 行号 - break 函数名 - break 文件名:行号 - break 文件名:函数
- 查看断点
info break
- 删除断点
delete 断点编号
- 设置断点无效
disable 断点编号
- 设置断点生效
enable 断点编号
- 设置条件断点
break 行号 if i==5
- 运行GDB程序
start -> 程序停在第一行 run -> 遇到断点才停
- 继续执行, 到下一个断点停
continue
- 向下执行一行代码
next
- 变量操作
print 变量名 ptype 变量名
- 向下单步调试
step finish(跳出函数体)
- 自动变量操作
display 变量名(自动打印指定变量的值) info display undisplay 编号
- 其他操作
set var 变量名=变量值 until -> 跳出循环
"""
18. 目标文件
"""
- 目标文件从结构上讲, 它是已经编译后的可执行文件格式, 只是还没有经过链接的过程, 其中可能有些符号或还有些地址没有被调整.
其实它本身就是按照可执行文件格式存储的, 只是跟真正的可执行文件在结构上稍有不同.
- Windows: PE-COFF 文件格式 Linux: ELF 文件
- 可重定位文件(Relocatable File): 这类文件包含了代码和数据, 可以被用来链接成可执行文件或共享目标文件,
静态链接库也可以归为这一类。
- 可执行文件(Executable File): 这类文件包含了可以直接执行的程序, 它的代表就是ELF可执行文件, 它们一般没有扩展名
- 共享目标文件(Shared Object File): 这种文件包含了代码和数据, 可以在两种情况使用. 一种是链接器可以使用这种文件
跟其他的可重定位文件和共享目标文件链接, 产生新的目标文件. 第二种是动态链接器可以将几个这种共享目标文件与可执行文件
结合, 作为进程映像的一部分来运行.
- 核心转储文件: 当进程意外终止时, 系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件
"""
19. 目标文件的结构
"""
- 一般C语言的编译后执行语句都编译成机器代码, 保存在.text段; 已初始化的全局变量和局部静态变量都保存在.data段;
未初始化的全局变量和局部静态变量一般放在一个叫.bss的段.
- 总体来说, 程序源代码被编译以后主要分成两种段: 程序指令和程序数据. 代码段属于程序指令, 数据段和.bss段属于程序数据.
- 一方面程序被装载后, 数据和指令分别被映射到两个虚存区域. 由于数据区域对于进程来说是可读写的, 而指令区域对于
进程来说是只读的, 所以这两个虚存区域的权限可以被设置成可读写和只读. 这样可以防止程序的指令被有意或无意地改写.
- 现代CPU的缓存一般都被设计成数据缓存和指令缓存分离, 所以程序的指令和数据被分开存放对CPU的缓存命中率提高有好处.
- 当系统中运行着多个该程序的副本时, 它们的指令都是一样的, 所以内存中只需要保存一份该程序的指令部分.
"""