C++编译器和连接器原理
本文转载自新浪永远即等待的博客
几个概念:
1、编译:编译器对源文件进行编译,就是把源文件中的文本形式存在的源代码翻译成机器语言形式的目标文件的过程,在这个过程中,编译器会进行一系列的语法检查。如果编译通过,就会把对应的CPP转换成OBJ文件。
2、编译单元:根据C++标准,每一个CPP文件就是一个编译单元。每个编译单元之间是相互独立并且互相不可知。
3、目标文件:由编译所生成的文件,以机器码的形式包含了编译单元里所有的代码和数据,还有一些期他信息,如未解决符号表,导出符号表和地址重定向表等。目标文件是以二进制的形式存在的。
根据C++标准,一个编译单元(Translation Unit)是指一个.cpp文件以及这所include的所有.h文件,.h文件里面的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件,后者拥有PE(Portable Executable,即Windows可执行文件)文件格式,并且本身包含的就是二进制代码,但是不一定能执行,因为并不能保证其中一定有main函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由链接器进行链接成为一个.exe或.dll文件。
下面让我们来分析一下编译器的工作过程:
我们跳过语法分析,直接来到目标文件的生成,假设我们有一个A.cpp文件,如下定义:
int n = 1;
void FunA()
{
++n;
}
它编译出来的目标文件A.obj就会有一个区域(或者说是段),包含以上的数据和函数,其中就有n、FunA,以文件偏移量形式给出可能就是下面这种情况:
偏移量 内容 长度
0x0000 n 4
0x0004 FunA ??
注意:这只是说明,与实际目标文件的布局可能不一样,??表示长度未知,目标文件的各个数据可能不是连续的,也不一定是从0x0000开始。
FunA函数的内容可能如下:
0x0004 inc DWORD PTR[0x0000]
0x00?? ret
这时++n已经被翻译成inc DWORD PTR[0x0000],也就是说把本单元0x0000位置的一个DWORD(4字节)加1。
有另外一个B.cpp文件,定义如下:
extern int n;
void FunB()
{
++n;
}
它对应的B.obj的二进制应该是:
偏移量 内容 长度
0x0000 FunB ??
这里为什么没有n的空间呢,因为n被声明为extern,这个extern关键字就是告诉编译器n已经在别的编译单元里定义了,在这个单元里就不要定义了。由于编译单元之间是互不相关的,所以编译器就不知道n究竟在哪里,所以在函数FunB就没有办法生成n的地址,那么函数FunB中就是这样的:
0x0000 inc DWORD PTR[????]
0x00?? ret
那怎么办呢?这个工作就只能由链接器来完成了。
为了能让链接器知道哪些地方的地址没有填好(也就是还????),那么目标文件中就要有一个表来告诉链接器,这个表就是“未解决符号表”,也就是unresolved symbol table。同样,提供n的目标文件也要提供一个“导出符号表”也就是exprot symbol table,来告诉链接器自己可以提供哪些地址。
好,到这里我们就已经知道,一个目标文件不仅要提供数据和二进制代码外,还至少要提供两个表:未解决符号表和导出符号表,来告诉链接器自己需要什么和自己能提供些什么。那么这两个表是怎么建立对应关系的呢?这里就有一个新的概念:符号。在C/C++中,每一个变量及函数都会有自己的符号,如变量n的符号就是n,函数的符号会更加复杂,假设FunA的符号就是_FunA(根据编译器不同而不同)。
所以,
A.obj的导出符号表为
符号 地址
n 0x0000
_FunA 0x0004
未解决符号为空(因为他没有引用别的编译单元里的东西)。
B.obj的导出符号表为
符号 地址
_FunB 0x0000
未解决符号表为
符号 地址
n 0x0001
这个表告诉链接器,在本编译单元0x0001位置有一个地址,该地址不明,但符号是n。
在链接的时候,链接在B.obj中发现了未解决符号,就会在所有的编译单元中的导出符号表去查找与这个未解决符号相匹配的符号名,如果找到,就把这个符号的地址填到B.obj的未解决符号的地址处。如果没有找到,就会报链接错误。在此例中,在A.obj中会找到符号n,就会把n的地址填到B.obj的0x0001处。
但是,这里还会有一个问题,如果是这样的话,B.obj的函数FunB的内容就会变成inc DWORD PTR[0x000](因为n在A.obj中的地址是0x0000),由于每个编译单元的地址都是从0x0000开始,那么最终多个目标文件链接时就会导致地址重复。所以链接器在链接时就会对每个目标文件的地址进行调整。在这个例子中,假如B.obj的0x0000被定位到可执行文件的0x00001000上,而A.obj的0x0000被定位到可执行文件的0x00002000上,那么实现上对链接器来说,A.obj的导出符号地地址都会加上0x00002000,B.obj所有的符号地址也会加上0x00001000。这样就可以保证地址不会重复。
既然n的地址会加上0x00002000,那么FunA中的inc DWORD PTR[0x0000]就是错误的,所以目标文件还要提供一个表,叫地址重定向表,address redirect table。
总结一下:
目标文件至少要提供三个表:未解决符号表,导出符号表和地址重定向表。
未解决符号表:列出了本单元里有引用但是不在本单元定义的符号及其出现的地址。
导出符号表:提供了本编译单元具有定义,并且可以提供给其他编译单元使用的符号及其在本单元中的地址。
地址重定向表:提供了本编译单元所有对自身地址的引用记录。
链接器的工作顺序:
当链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址。最后把所有的目标文件的内容写在各自的位置上,再作一些另的工作,就生成一个可执行文件。
说明:实现链接的时候会更加复杂,一般实现的目标文件都会把数据,代码分成好向个区,重定向按区进行,但原理都是一样的。
明白了编译器与链接器的工作原理后,对于一些链接错误就容易解决了。
下面再看一看C/C++中提供的一些特性:
extern:这就是告诉编译器,这个变量或函数在别的编译单元里定义了,也就是要把这个符号放到未解决符号表里面去(外部链接)。
static:如果该关键字位于全局函数或者变量的声明前面,表明该编译单元不导出这个函数或变量,因些这个符号不能在别的编译单元中使用(内部链接)。如果是static局部变量,则该变量的存储方式和全局变量一样,但是仍然不导出符号。
默认链接属性:对于函数和变量,默认链接是外部链接,对于const变量,默认内部链接。
外部链接的利弊:外部链接的符号在整个程序范围内都是可以使用的,这就要求其他编译单元不能导出相同的符号(不然就会报duplicated external symbols)。
内部链接的利弊:内部链接的符号不能在别的编译单元中使用。但不同的编译单元可以拥有同样的名称的符号。
为什么头文件里一般只可以有声明不能有定义:头文件可以被多个编译单元包含,如果头文件里面有定义的话,那么每个包含这头文件的编译单元都会对同一个符号进行定义,如果该符号为外部链接,则会导致duplicated external symbols链接错误。
为什么公共使用的内联函数要定义于头文件里:因为编译时编译单元之间互不知道,如果内联被定义于.cpp文件中,编译其他使用该函数的编译单元的时候没有办法找到函数的定义,因些无法对函数进行展开。所以如果内联函数定义于.cpp里,那么就只有这个.cpp文件能使用它。
C++编译器和连接器原理的更多相关文章
- 使用 Edit + MASM 5.0 编译器 + Linker 连接器
其实这种方式是很简单的,只是很麻烦,因为简单而且麻烦, 所以我采用尽可能的将截图传上来,然后稍加注解的方式进行介绍, 软件准备: 需要 MASM 5.0 或者以上的汇编编译器 首先,是要编辑汇编源代码 ...
- C语言编译器 cc 编译原理
生成一个可执行的文件通常需要经过以下几个步骤: 预处理你的源代码,去掉注释,以及其他技巧性的工作就像在 C 中展开宏. 检查代码的语法看你是否遵守了这个语言的规则.如果没有,编译器会给出 警告. 把源 ...
- [WebGL入门]十一,着色器编译器和连接器
注意:文章翻译http://wgld.org/,原作者杉本雅広(doxas).文章中假设有我的额外说明.我会加上[lufy:].另外.鄙人webgl研究还不够深入.一些专业词语,假设翻译有误.欢迎大家 ...
- MMU工作原理
MMU的工作原理就是把虚拟地址转换成物理地址. 虚拟地址:由编译器和连接器在定位程序时分配. 物理地址:用来访问实际的主存硬件模块. 使用虚拟存储器的系统都使用一种称为分页(paging).虚拟地址空 ...
- 深入理解javascript作用域系列第一篇——内部原理
× 目录 [1]编译 [2]执行 [3]查询[4]嵌套[5]异常[6]原理 前面的话 javascript拥有一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则被称为作用域.作用域 ...
- VS编译器优化诱发一个的Bug
VS编译器优化诱发一个的Bug Bug的背景 我正在把某个C++下的驱动程序移植到C下,前几天发生了一个比较诡异的问题. 驱动程序有一个bug,但是这个bug只能 Win32 Release 版本下的 ...
- [转] GCC 中的编译器堆栈保护技术
以堆栈溢出为代表的缓冲区溢出已成为最为普遍的安全漏洞.由此引发的安全问题比比皆是.早在 1988 年,美国康奈尔大学的计算机科学系研究生莫里斯 (Morris) 利用 UNIX fingered 程序 ...
- 深入研究Block捕获外部变量和__block实现原理
Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这个新功能“Blocks”.从那开始,Block就出现在iOS和Mac系统各个API中,并被大 ...
- nvcc编译器选项及配置
nvcc命令选项: 选项命令有长名和短名,通常我们使用是用短名,长名主要用于描述. 1.指定编译阶段 主要指定编译的阶段以及要编译的输入文件. -cuda -cubin -fatbin -ptx ...
随机推荐
- android studio下 library打包文件(.aar)和本地引用
关键点: 利用Gradle发布本地maven库支持android library 打包文件(*.aar) 的本地引用 开发环境: windows7 64位操作系统 android studio0.5. ...
- 用LabVIEW做声源定位系统
前一阵子,研发部举办了为期三天的第一届Innovation Day,让大家用3天时间去完成工作之外的一些创意.有人做微信小程序,有人继续研究一些工作中用到的Tool,有人把一直想解决而没时间解决的老b ...
- Go chan 结构体 写入文件
chan 需要两个进程,一个写,一个读,是分开的, package main import ( "bufio" "fmt" "math/rand&qu ...
- 【Oracle】获取SQL执行计划
一.plsql developer工具F5 在sqldeveloper中选中sql按F5即可查看执行计划
- SpringBoot中使用Interceptor
https://www.cnblogs.com/chao555/p/9573102.html 1.创建自定义的拦截器并实现HandlerInterceptor接口 package com.demo.c ...
- c# 拼接字符串根据逗号切割 后转换成集合或数组
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/qq_27559331/article/d ...
- C语言 - 可变参数再stm32中的应用
参考 C 可变参数 | 菜鸟教程 void func(const char* str,...) { ... } func的最后一个参数写成 ... ,表示可变参数, C语言的printf就是类似这种声 ...
- selenium自学笔记---下拉框定位元素select
下拉框1.先定位select 然后在定位option city = driver.find_element_by_id("selCities_0") city.find_eleme ...
- 全网最easy的better-scroll实现上拉加载和下拉刷新
前言 移动端页面常见的一种效果:下拉刷新(pulldownrefresh)和上拉加载(pullupload),目的都是为了增强用户的体验效果,因此各种移动端滑动插件也是层出不穷,今天小编也在这里给大家 ...
- ajax的五大步骤
什么是Ajax? AJAX 是与服务器交换数据并更新部分网页的艺术,在不重新加载整个页面的情况下. var btn = document.getElementsByTagName('button')[ ...