C++从代码到可执行程序
编译系统
一个hello.c程序
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
在Linux系统上,由编译器把源文件转换为目标文件
gcc -o hello.c hello
- 预处理(预编译):处理以#开头的预处理命令
- 编译:编译成汇编文件
- 汇编:将汇编文件翻译成可重定位的目标文件
- 链接:将可重定位目标和printf.o等编译好的目标库文件进行合并,得到最终的可执行目标文件
预编译
主要处理以“#”开头的预编译指令
- 删除所有的==#define==,展开所有的宏定义
- 处理所有的条件预编译指令,如==#if、#endif、#ifdef、#elif、#else==
- 处理==#include==预编译指令,将文件内容替换到它的位置,递归进行,文件中包含其他文件
- 删除所有的注释
- 保留所有的==#pragma编译指令,编译器需要用到它们。如pragma once==是为了防止有文件被重复引用
- 添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告时能够显示行号
编译
把预编译生成的==.i或者.ii文件,进行一系列词法分析、语法分析、语义分析、优化后,生成相应的汇编代码文件==
- 词法分析:利用类似有限状态机的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号
- 语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树
- 语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义(在编译期能分析),相对应的动态语义是在运行期间才能确定
- 优化:源代码级别的一个优化过程
- 目标代码生成:由代码生成器将中间代码转换成目标机器代码(汇编语言)
- 目标代码优化:对目标机器代码进行优化,寻找合适的寻址方式、使用位移来代替乘法运算、删除多余的指令
汇编
将汇编代码转换成机器可以执行的指令(机器码文件),汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令对照表翻译过来,经过汇编之后,产生目标文件(与可执行文件几乎一样)
链接
将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序
链接分为静态链接和动态链接:
静态库可能是源代码,动态库可能本身就是一个可执行文件
-
静态链接:静态库 libxxx.o、libxxx.a
-
函数和数据被编译进一个二进制文件
-
使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其他模块组合起来创建最终的可执行文件
-
每当库函数的代码修改了,需要重新进行编译链接形成可执行文件
-
优点:运行速度快,代码装载速度快
-
缺点:
- 编译生成的程序体积会相对大一些
- 如果静态库需要更新,程序需要重新编译
- 如果多个应用程序使用的话,会被装载多次,浪费内存
-
静态链接打包命令
gcc -c test1.c //生成test1.o gcc -c test2.c //生成test2.o ar cr libtest.a test1.o test2.o
首先编译得到test1.o和test2.o两个目标文件,之后通过ar命令将这两个文件打包为.a文件,文件名格式为lib + 静态库名 + .a后缀。在生成可执行文件需要使用到它的时候只需要在编译时加上即可。需要注意的是,使用静态库时加在最后的名字不是libtest.a,而是l + 静态库名
gcc -o main main.c -ltest
-
-
动态链接:动态库 libxxx.so
-
把程序按照模块拆分成各个相对独立部分,在程序链接时才将它们链接在一起形成一个可执行文件
-
共享库:即使每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存多份副本,而是多个程序在执行时共享同一份
-
更新方法:更新的时候只需要替换原来的目标文件,无需将所有程序重新编译链接一遍。当程序下次运行时,新版本的目标文件会被自动加载到内存并链接起来
-
缺点:每次执行程序的时候进行链接,性能会有一定的损失
-
动态链接在形式上倒是和静态链接非常相似,首先也是需要打包,打包成动态库,不过文件格式为libxxx.so。动态库的打包不需要使用ar命令,gcc就可以完成,但要注意在编译时要加上-fPIC选项,打包时加上-shared选项。
gcc -fPIC -c test1.c gcc -fPIC -c test2.c gcc -shared test1.o test2.o -o libtest.so
使用动态链接的用法也和静态链接相同
gcc -o main main.c -ltest
如果仅仅像上面的步骤是没有办法正常使用库的,我们可以通过加-Lpath指定搜索库文件的目录(-L.表示当前目录),默认情况下会到环境变量LD_LIBRARY_PATH指定的目录下搜索库文件,默认情况是/usr/lib,我们可以将库文件拷贝到那个目录下再链接。
-
Makefile编写
对于大的工程通常涉及很多头文件和源文件,编译起来很麻烦,Makefile正是为了自动化编译产生的,Makefile像是编译说明书,指示编译的步骤和条件,之后被make命令解释。
- 基本规则
A:B
(tab)<command>
其中A是语句最后生成的文件,B是生成A所依赖的文件,比如生成test.o依赖于test.h,则写成test.o:test.c test.h。接下来一行的开头必须是tab,再往下就是实际命令了,比如gcc -c test.c -o test.o
- 变更
Makefile的书写非常像shell脚本,可以在文件中定义“变量名 = 变量值”的形式,之后需要使用这个变量时只需要写一个$符号加上变量名即可,当然和shell一样最好用()包裹起语句来