🎬 GitHub:Vect的代码仓库
文章目录
- 1. C++如何从代码到可执行文件
- 1.0. 一小段代码进行演示
- 1.1. 预处理阶段: `g++ -E`
- 1.2.编译阶段:`g++ -S`
- 1.3. 汇编阶段:`g++ -c`
- 1.4. 链接阶段:`g++ main.o foo.o -o app`
- 1.5. 把模板定义放到`.cpp`会发生什么?
- 2. 动态库和静态库
- 2.1. 动态库
- 2.2. 静态库
- 2.3. 二者对比
- 3. 总结
1. C++如何从代码到可执行文件
1.0. 一小段代码进行演示
// add.h#pragmaoncetemplate<classT>Tadd(T a,T b){returna+b;}// foo.cpp#include"add.h"intfoo(){returnadd(10,30);// add<int>实例化}// main.cpp#include"add.h"#include<iostream>usingnamespacestd;intfoo();intmain(){cout<<add(1,2)<<endl;cout<<foo()<<endl;return0;}1.1. 预处理阶段:g++ -E
预处理阶段编译器只做文本级工作:
- 展开
#include:把头文件的内容拷贝进来 - 宏替换
#define - 条件编译
#if/#ifdef - 去掉注释
注意:模板实例化不在预处理阶段,预处理器不懂C++语义,只做文本级处理
命令演示
[vect@VM-0-11-centos ~]$ g++ -E main.cpp -o main.i[vect@VM-0-11-centos ~]$ g++ -E foo.cpp -o foo.i
-E:只执行预处理操作,预处理结束就停止
-o:指定输出文件名,后面紧跟文件名
filename.i:预处理后的源文件后缀为.i如果不加
-o main.i预处理过后的文件会输出到终端
1.2.编译阶段:g++ -S
把预处理后的.i文件变成汇编文件.s:
词法/语法分析:把字符流变成token流(我们写的代码对于机器来说就是一串字符,编译器会把这串字符组合成有意义的”单词“即token流);判断token流的排列是否符合C++语法并构建AST树(抽象语法树)
写的一行代码:
int a = b + 3;在计算机眼里就是一串字符:i n t a = b + 3 ;,现在把这些字符组成有意义的token,具体包括了关键字、标识符、运算符、字面量、语句结束符,这个阶段只关心”单词的构建“,不关系语法是否正确现在已经形成token流
[int] [a] [=] [b] [+] [3] [;],语法分析的结果不是对与错,而是构建AST树(这句代码真正的结构含义)
= / \ a + / \ b 3然后补充上类型信息:
声明语句 ├──类型:int └── 赋值 ├──变量:a └── 加法 ├──变量:b └──常量:3这里可以类比:词法分析-认识单词 语法分析-分析句子主谓宾 AST-句子的语法树状图
语义分析:类型检查、重载检查、名字查找、访问控制、模板相关规则
模板实例化:当编译器看到”需要用到的模板“时,会生成具体版本的函数体,例如:
add(1,2)--->add<int>(int,int)优化:基于AST树,修改AST树,常量折叠(直接进行运算,不留到运行期
int x = 5 + 3 -> int x = 8)、内联(直接替换函数调用,在这里展开函数,内联只是建议)、死代码删除(删除永远不会执行的代码)、寄存器分配…生成汇编:输出
.s
命令演示:
[vect@VM-0-11-centos ~]$ g++ -S main.i -o main.s[vect@VM-0-11-centos ~]$ g++ -S foo.i -o foo.s可以观察到,此时已经形成了汇编代码
1.3. 汇编阶段:g++ -c
汇编把.s汇编代码变成机器码目标文件(二进制文件).o,二进制文件包含:
.text段:机器指令.rodata:只读常量.data/.bss:全局/静态数据符号表:目标文件中定义了哪些符号、还需要外部提供哪些符号
符号:函数名、全局变量名、静态变量名
符号表:**目标文件里的一张”名字->状态/地址“**的表,回答定义了哪些符号,使用了哪些符号但是还不知道地址,举个例子理解一下:
// show_signal.cppintfoo(){return23;}编译:
[vect@VM-0-11-centos ~]$ g++ -c show_signal.cpp -o foo.o[vect@VM-0-11-centos ~]$ nm -C foo.o# 查看符号表U __cxa_atexit U __dso_handle 0000000000000048 t _GLOBAL__sub_I__Z3foov 0000000000000000 T foo()000000000000000b t __static_initialization_and_destruction_0(int, int)U std::ios_base::Init::Init()U std::ios_base::Init::~Init()0000000000000000 b std::__ioinit0000000000000000 T foo()偏移地址 在.text段中定义 符号名说明了在
foo.o里面,自己定义了foo再看需要外部提供符号的情况:
intfoo();// 声明未定义intmain(){returnfoo();}编译:
[vect@VM-0-11-centos ~]$ g++ -c main_signal.cpp -o main.o[vect@VM-0-11-centos ~]$ nm -C main.o U __cxa_atexit U __dso_handle 0000000000000048 t _GLOBAL__sub_I_main 0000000000000000 T main U foo()000000000000000b t __static_initialization_and_destruction_0(int, int)U std::ios_base::Init::Init()U std::ios_base::Init::~Init()0000000000000000 b std::__ioinit看这段代码:
0000000000000000 T main U foo()U = undefined未定义,这里说明在
mian.o中用到了foo(),但是在mian.o中没有实现,需要外部提供的符号!所以符号表的作用:当链接器拿到
main.o发现需要foo,而foo.o定义了foo,则指向foo.o里的foo地址重定位信息:哪些地址等链接时再决定
一个残酷的事实:在
.o文件中,所有地址都是临时的!!!因为.o不知道将来和谁链接,不知道程序从内存哪里开始,所以编译器只能做到:将来这里要用一个地址,我先占个坑举个例子:
intfoo();// 声明未定义intmain(){returnfoo();}在汇编层面:
call _Z3foov这里foo被改名为_Z3foov,这里可以补充一个知识点:为什么C++支持函数重载?在汇编和链接层面,名字必须唯一,C++函数会进行函数名改编,把函数名编进符号表里_Z 3 foo v │ │ │ │ │ │ │ └── 参数列表:v = void(无参数) │ │ └─────── 函数名 foo │ └─────────── 3 表示 foo 这个名字长度是 3 └─────────────── _Z = C++ 符号前缀voidfoo();// _Z3foovvoidfoo(int);// _Z3fooivoidfoo(double);// _Z3food常见的类型编码:
类型 编码 void v int i double d char c long l 指针 P 这里我们也可以用
nm main_signal.o来查看- 编译器如何解决这个问题?
生成机器码+留一个备注(重定位信息)
- 重定位表是啥:一张需要补地址的清单
总结一下,在汇编阶段编译器的行为:收集所有符号表->给所有符号分配最终地址->处理重定位的信息而符号表表达了谁是谁,重定位信息表达了地址填哪
1.4. 链接阶段:g++ main.o foo.o -o app
编译器把多个.o文件(包括库文件.a/.so)拼成一个可执行文件或共享库:
- 符号解析:把
main.o里未定义的符号去别的.o/.a/.so里找定义 - 地址分配和段合并:把各个
.text/.data合并,给每个符号分配最终地址 - 重定位:把机器码/数据中”占位“的地址改成最终地址
- 处理库依赖:
- 静态库
.a:把需要的目标文件成员抽取进行最终程序 - 动态库
.so:记录依赖关系,运行时由动态装载器加载
- 静态库
命令演示
[vect@VM-0-11-centos link]$ g++ main.o foo.o -o app[vect@VM-0-11-centos link]$ ./app[vect@VM-0-11-centos link]$ ldd ./app# 查看依赖的动态库linux-vdso.so.1=>(0x00007ffd751fe000)libstdc++.so.6=>/home/vect/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6(0x00007ff731b8b000)libm.so.6=>/lib64/libm.so.6(0x00007ff731889000)libgcc_s.so.1=>/lib64/libgcc_s.so.1(0x00007ff731673000)libc.so.6=>/lib64/libc.so.6(0x00007ff7312a5000)/lib64/ld-linux-x86-64.so.2(0x00007ff731f0c000)1.5. 把模板定义放到.cpp会发生什么?
add.h只写声明
// add.h#pragmaoncetemplate<typenameT>Tadd(T a,T b);// 只有声明,没有定义add.cpp定义模板
// add.cpp#include"add.h"template<typenameT>Tadd(T a,T b){returna+b;}// main.cpp#include<iostream>#include"add.h"intmain(){std::cout<<add(1,2)<<std::endl;return0;}编译每个cpp都成功了:
[vect@VM-0-11-centos template]$ g++ -c add.cpp -o add.o[vect@VM-0-11-centos template]$ g++ -c main.cpp -o main.o这里已经埋雷了
链接出错:
[vect@VM-0-11-centos template]$ g++ add.o main.o -o app main.o: Infunction`main': main.cpp:(.text+0xf): undefined reference to`int add<int>(int, int)' collect2: error: ld returned1exitstatus为什么会出错?
编译
main.cpp发生了什么?
add(1,2);
编译器知道这是个模板
需要生成
add<int>函数体但在
add.h里只看到声明,没有定义于是编译器只能假设将来有人实现
add<int>在
mian.o里:符号表是U int add<int>(int, int)编译
add.cpp发生了什么?[vect@VM-0-11-centos template]$ nm -C add.o[vect@VM-0-11-centos template]$符号表是空的!!!
在
add.cpp里没有任何地方用到add<int>,编译器遵循模板哪里使用哪里实例化的原则链接发生了什么?
文件 情况 main.o 我需要 add<int>add.o 我没定义 add<int>现在没人提供这个符号!!!
报错:
undefined reference to int add<int>(int, int)
本质说明了:模板的实例化发生在编译期,而不是链接期
所以,怎么解决?
- 模板定义放在头文件
- 在
add.cpp文件中显式实例化
2. 动态库和静态库
动态库:用的时候,程序只记住去哪里找,真正运行时再加载
静态库:用的时候,把代码直接拷贝到程序里
我们还是用add这份代码,不要模板
2.1. 动态库
Linux下:.so为后缀的文件,本质是独立存在的二进制文件
怎么生成?
生成位置无关代码:
g++ -fPIC add.cpp -o add.o-fPIC告诉编译器这段代码将来被共享生成动态库:
g++ -shared add.o -o libadd.so生成了动态库:libadd.so动态库链接:
g++ main.cpp -L. -ladd -o app_dynamic
-L.:在当前目录找库
-ladd:优先找libadd.so验证依赖:
ldd app_dynamic
完整代码:
[vect@VM-0-11-centos rep]$ g++ -fPIC -c add.cpp -o add.o[vect@VM-0-11-centos rep]$ g++ -shared add.o -o libadd.so[vect@VM-0-11-centos rep]$ g++ main.cpp -L. -ladd -o app_dynamic[vect@VM-0-11-centos rep]$ g++ main.cpp -L. -ladd -o app_dynamic[vect@VM-0-11-centos rep]$ ./app_dynamic3[vect@VM-0-11-centos rep]$ ldd app_dynamic linux-vdso.so.1=>(0x00007ffe6c5bd000)libadd.so(0x00007fdfcef90000)libstdc++.so.6=>/home/vect/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6(0x00007fdfcec0f000)libm.so.6=>/lib64/libm.so.6(0x00007fdfce90d000)libgcc_s.so.1=>/lib64/libgcc_s.so.1(0x00007fdfce6f7000)libc.so.6=>/lib64/libc.so.6(0x00007fdfce329000)/lib64/ld-linux-x86-64.so.2(0x00007fdfcf192000)动态库:编译时只记住地址,运行时再加载代码
2.2. 静态库
Linux下:.a为后缀的文件,本质是一堆.o文件的打包
怎么生成?
编译成目标文件:
g++ -c add.cpp -o add.o此时
add.o里面有add的机器码,还没有生成程序打包成静态库:
ar rcs libadd.a add.o现在有静态库:
libadd.a链接生成可执行程序:
g++ main.cpp libadd.a -o app_static
main.cpp用了add,libadd.a里刚好有add.o,直接把add.o复制进最终程序查看依赖:
ldd app_static会发现没有
libadd.a,其实代码已经拷贝到程序里了
完整代码:
[vect@VM-0-11-centos rep]$ g++ -c add.cpp -o add.o[vect@VM-0-11-centos rep]$ ar rcs libadd.a add.o[vect@VM-0-11-centos rep]$ g++ main.cpp libadd.a -o app_static[vect@VM-0-11-centos rep]$ ./app_static3[vect@VM-0-11-centos rep]$ ldd app_static linux-vdso.so.1=>(0x00007ffd18faa000)libstdc++.so.6=>/home/vect/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6(0x00007f51a6245000)libm.so.6=>/lib64/libm.so.6(0x00007f51a5f43000)libgcc_s.so.1=>/lib64/libgcc_s.so.1(0x00007f51a5d2d000)libc.so.6=>/lib64/libc.so.6(0x00007f51a595f000)/lib64/ld-linux-x86-64.so.2(0x00007f51a65c6000)静态库:编译时把代码拷贝到而可执行文件
2.3. 二者对比
| 对比点 | 静态库 | 动态库 |
|---|---|---|
| add 的代码 | 拷进 app | 在 libadd.so |
| 可执行文件 | 大 | 小 |
| ldd | 看不到 add | 能看到 libadd.so |
| 运行依赖 | 无 | 必须有 so |