编译
知识出自《后台开发核心技术与应用实践》
从一个简单的hello.cpp程序到之后的可执行的文件,中间经历一些列的过程,一般而言通过开发工具,比如Vs一个键就搞定,用g++也是一个指令的事情,中间的过程是编译,那么编译的过程是怎样的?大致可以分为四个部分,分别是预处理、编译、汇编和链接
本文尝试在Ubuntu下,用最简单的Hello.cpp程序来阐述这一过程
1、预处理:预处理过程主要处理那些源代码文件只能够的以“#”开始的预编译指令。比如#define,#include,主要处理规则如下
(1)将所有的#define删除,并且展开所有的宏定义
(2)处理所有的条件预编译指令,比如#ifndef
(3)处理#include预编译指令,将包含的文件插入到该预编译的位置(递归的过程,及包含的文件如果同时包含其它文件,也会展开这个过程)
(4)过滤所有的注释"//"和/**/中的内容
(5)添加行号和文件名标识,比如“#5“hello.cpp””,一边编译时编译器产生的调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号
(6)保留说有的#pragma编译器指令(后期编译器需要)
经过预编译后的.i文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.i文件中
在ubuntu下测试了一下这个过程
源文件:
#include<iostream>
#define a "test_define"
int main(void)
{
int i = 10;
std::cout<<"Hello, World! "<< a <<std::endl;
return 0;
}
经命令:g++ -E hello.cpp -o hello.i (-E编译选项表示只执行到预处理)
之后得到的部分文件:
# 935 "/usr/include/c++/5/istream" 2 3
# 41 "/usr/include/c++/5/iostream" 2 3
namespace std __attribute__ ((__visibility__ ("default")))
{
# 60 "/usr/include/c++/5/iostream" 3
extern istream cin;
extern ostream cout;
extern ostream cerr;
extern ostream clog;
extern wistream wcin;
extern wostream wcout;
extern wostream wcerr;
extern wostream wclog;
static ios_base::Init __ioinit;
}
# 2 "hello.cpp" 2
# 5 "hello.cpp"
int main(void)
{
int i = 10;
std::cout<<"Hello, World! "<< "test_define" <<std::endl;
return 0;
}
可以看到定义的宏已被展开,文件前面插入很多内容(只显示了部分,实际生成的.i文件要大的多)
2、编译: 编译过程就是预处理完的文件进行一系列的词法分析、语法分析、语义分析以及代码优化生成相应的汇编代码文件(核心部分)。编译的过程一般分为六步:扫描(词法分析)-> 语法分析 -> 语义分析 -> 源代码优化 -> 代码生成和目标代码优化
(1)词法分析:将源代码程序输入到扫描器中,扫描器将源代码的字符序列分割成一系列的记号(一般分为:关键字、标识符、字面量和特殊记号)
(2)语法分析:分析器对由扫描器产生的记号进行语法分析,从而产生语法树( 以表达时为节点的树),通过语法树能够确定运算符的优先级和含义
(3)语义分析:语义分析,用我自己的话来讲,就是让句子变得有意义,语法分析只是将表达式进行了拆分,形成了一个语法树,但对于语句具体要做什么一无所知,而语义分析在此基础上为各个节点标识类型,同需要转化的类型进行转化(隐式转化)
(4)中间语言的生成:中间代码使得编译器可以分割为前端和后端。编译器前端负责产生机器无关的中间代码,编译期后端则负责将中间代码转换成目标机器代码。
(5)目标代码的生成和优化:干的活和字面意思一样。介绍一下这里的优化过程,到目前为止还不是一个真正的可执行文件,因为无法确定定义在其他模块中全局变量和函数在运行使得绝对地址,这一步需要等到链接时才能确定。
通过指令生成的.s文件:g++ -S hello.i -o hello.s (-S选项表明只执行到源代码到汇编代码的转换,输出汇编代码)
中间文件:
.file "hello.cpp"
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
.section .rodata
.LC0:
.string "Hello, World! "
.LC1:
.string "test_define"
.text
.globl main
.type main, @function
main:
.LFB1021:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $10, -4(%rbp)
movl $.LC0, %esi
movl $_ZSt4cout, %edi
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movl $.LC1, %esi
movq %rax, %rdi
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esi
movq %rax, %rdi
call _ZNSolsEPFRSoS_E
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1021:
.size main, .-main
.type _Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB1030:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
cmpl $1, -4(%rbp)
jne .L5
cmpl $65535, -8(%rbp)
jne .L5
movl $_ZStL8__ioinit, %edi
call _ZNSt8ios_base4InitC1Ev
movl $__dso_handle, %edx
movl $_ZStL8__ioinit, %esi
movl $_ZNSt8ios_base4InitD1Ev, %edi
call __cxa_atexit
.L5:
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1030:
.size _Z41__static_initialization_and_destruction_0ii, .-_Z41__static_initialization_and_destruction_0ii
.type _GLOBAL__sub_I_main, @function
_GLOBAL__sub_I_main:
.LFB1031:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $65535, %esi
movl $1, %edi
call _Z41__static_initialization_and_destruction_0ii
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1031:
.size _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main
.section .init_array,"aw"
.align 8
.quad _GLOBAL__sub_I_main
.hidden __dso_handle
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
3、链接:把各个模块之间的相互引用的部分都处理好,使得各个模块可以相互正确的衔接,主要包括,地址和空间分配、符号决议和重定位。每个目标文件提供三个表:未解决符号表(在该编译单元里引用但是定义并不是在本编译单元的符号及其出现的地址),导出符号表(提供了本编译单元具有定义,并且提供给其他单元使用的符号及其地址),地址重定向表(提供本编译单元所有对自身地址的引用的记录)
(1)静态链接:放在编译时期完成的函数库的链接
创建动态库:
g++ -c add.cpp
ar cr libmymath.a add.o
g+= -o main.cpp -L. -lmymath
生成.o文件之后使用ar指令创建一个库,在程序中使用时,在编译时将其加上
(2)动态链接:库函数的载入推迟到程序运行时期(.so文件)
具体分为以下几步:
①生成动态链接库文件: g++ -fPIC -shared -o libmymath.so add.cpp
②生成目标文件:g++ -o main main.cpp -L. -lmymath
经过②之后直接运行指令会出现问题,因为系统查找不到动态库文件,所以需要进行下面操作
sudo cp libmymath.so /usr/lib/
export LD_LIBRARY_PATH=/usr/lib:$LD_LIBRARY_PATH
将我们自己做的动态库加入系统库中,同时修改一下环境变量配置即可
使用动态库可以是使生成的程序小,同时方便后期维护升级(由于动态库是在运行时期链接的,所以只要接口不便,升级维护起来很方便),但是缺点就是带来额外的系统额外开销,同时执行起来较静态库慢一点(调入需要时间)。相反,静态库则是每个程序将库函数拷贝到自己的代码段中去了,显然占用的内存资源大了