符号链接
文本:源代码 hello.c
预处理:修改文本 hello.i file
编译器:assembly hello.s file
汇编器:binary code hello.o file
连接器:hello.o + printf.o -> hello
线程级并行:指令执行基本单位
多核心:多个核共享L3缓存,一个核有一个统一的L2缓存,L1缓存分为数据缓存和指令缓存,数据缓存直连CPU寄存器
超线程:可以在单个周期内决定要切换到哪个线程,在一个线程等待数据运过来的过程中,CPU可以去转而执行另一个线程,8核心 16线程,线程并行
指令级并行:指令间并行
- 流水线
- 超标量
单指令,多数据并行:数据流并行 SIMD 一条指令可以产生多个并行执行的操作
抽象:Disk 代表I/O设备,Memory代表主存
- Virtual Machine:OS+CPU+Memory+Disk
- Process: CPU+Memory+Disk
- Virtual Memory:Memory+Disk
- File:Disk
- CPU:Instruction Set architecture
Linker
可重定位目标文件
“目标文件是字节块的集合”这句话通常用来描述目标文件(object file)的本质结构。目标文件是编译器生成的中间文件,包含程序代码和数据,用于后续的链接操作。以下是对这一概念的详细解析:
目标文件的本质:字节块
目标文件是由一系列字节组成的文件,这些字节可以看作是数据块。这些数据块并不是随意排列的,而是按照一定的格式组织起来,通常包括:
- 代码段:存储编译后的机器指令。
- 数据段:存储已初始化的全局或静态变量。
- 未初始化数据段(BSS):存储未初始化的全局或静态变量,实际大小记录在文件中,但不占用物理空间。
- 符号表:记录函数和变量的名字及其在文件中的位置。
- 重定位信息:指示链接器如何修改目标文件以适应最终的内存布局。
这些内容本质上都是字节序列,每一部分由多个字节块组成。
字节块的特点
- 固定大小或可变大小:字节块可以是固定长度的单位,例如4字节对齐的机器指令,也可能是不定长的数据块。
- 有意义的分组:每个块都有明确的意义,比如某个函数的机器代码、某个变量的初始值等。
- 易于解析:链接器、调试器等工具需要能够根据文件的格式解析这些块,因此这些字节块被有序地组织在文件头定义的布局中。
目标文件格式
目标文件的格式是如何定义这些字节块的重要规则。例如:
- ELF(Executable and Linkable Format) 是一种常见的目标文件格式,广泛用于Linux系统。它将字节块分为不同的段(Section),并用段表和符号表记录每个块的起始地址和大小。
- COFF(Common Object File Format) 是Windows等系统常用的格式。
这些格式提供了语法规则,使得目标文件的字节块能够被链接器或加载器正确地解读。
为什么强调“字节块的集合”?
- 通用性:这一描述抽象了目标文件的存储本质。无论是代码段还是数据段,本质上都是按字节存储的数据。
- 便于理解文件结构:将目标文件看作字节块集合,可以帮助程序员理解文件的解析方式,特别是分析目标文件格式时。
- 与内存映射相关:最终,这些字节块会被加载到内存中,按照相同的结构组织成进程的内存布局。
深入理解的关键
为了深入理解,可以通过以下方式进一步探索:
- 查看目标文件结构:使用工具(如
objdump
或readelf
)解析目标文件,观察各部分的字节分布。 - 编写简单的C程序并编译:用编译器生成目标文件,并对比源代码和目标文件中的符号表、代码段。
- 了解链接过程:理解这些字节块如何在链接过程中被组合成最终的可执行文件。
示例
编写简单的程序:
1 | int main() { |
编译生成目标文件后,使用 readelf -a
或 objdump -h
查看其字节块分布。你会发现,虽然程序非常简单,但目标文件已经按照格式组织了各种段和数据块。
符号解析
符号解析是链接过程中一个重要的步骤,用于将目标文件中的符号(如变量名、函数名)解析为具体的内存地址或其他符号的引用地址。它的目的是将代码中使用的符号与其定义进行匹配,以实现正确的程序连接。
以下是详细的解释:
什么是符号?
符号是程序中用来表示变量、函数、常量或其他标识符的名字。它可以包括:
- 全局变量
- 函数名
- 静态变量(如果需要)
- 外部引用的符号(来自其他目标文件或库)
在目标文件中,这些符号并不直接对应内存地址,而是保存在符号表中,等待链接器解析。
符号解析的作用
符号解析的主要目的是:
- 将使用符号的地方(例如函数调用或变量访问)和定义符号的地方联系起来。
- 为符号分配实际的内存地址或引用其他文件中符号的位置。
如果符号在当前目标文件中找不到定义,链接器会尝试在其他目标文件或库中找到它。
符号解析的过程
构建符号表:链接器首先扫描所有目标文件和库文件,读取它们的符号表,记录所有已定义和未定义的符号。
查找符号定义
:对于每个未定义的符号,链接器会在符号表中搜索其定义。
- 如果找到,链接器会将引用符号的位置更新为定义的位置。
- 如果找不到,链接器会报错(如“未定义符号”错误)。
处理重定位信息:对于解析后的符号,链接器会根据其地址调整目标文件中的指令或数据。
常见场景
- 内部符号解析:在同一个目标文件中,一个函数调用另一个函数,链接器只需在这个目标文件内找到符号的定义。
- 外部符号解析:一个目标文件调用另一个目标文件或库中的符号,链接器需要跨文件查找。
- 动态库中的符号解析:如果链接的是动态库(如
.so
或.dll
文件),符号解析可能延迟到程序运行时进行。
符号解析失败的原因
符号解析失败通常会导致编译或链接错误,比如:
- 未定义符号错误:
- 函数或变量的定义缺失。
- 链接时遗漏了需要的库。
- 重复定义错误:
- 同一个符号在多个目标文件中重复定义。
- 符号不可见:
- 符号被声明为
static
或其他方式限制了可见性,导致无法被外部文件引用。
- 符号被声明为
符号解析工具
在实际开发中,可以使用一些工具检查符号:
nm
:列出目标文件或库中的符号表。objdump
:查看符号和段信息。readelf
:分析目标文件的 ELF 格式符号。
例如:
1 | nm myfile.o |
会输出目标文件 myfile.o
中的所有符号及其状态(已定义或未定义)。
示例
假设有以下代码:
**文件 a.c
**:
1 | int foo = 42; |
**文件 b.c
**:
1 | extern int foo; |
编译:
1
2gcc -c a.c -o a.o
gcc -c b.c -o b.oa.o
中定义了符号foo
。b.o
中引用了符号foo
(标记为未定义)。
链接:
1
gcc a.o b.o -o program
- 链接器会解析
b.o
中的符号foo
,在a.o
中找到它的定义,并完成解析。
- 链接器会解析
总结
符号解析是链接器将程序中的符号引用与定义匹配起来的关键步骤。它确保程序在执行时能够正确访问函数、变量等资源。符号解析的成功与否直接影响程序的正确性,开发中需要注意符号的定义与使用是否一致,以及是否正确链接所需的目标文件或库。
switch
性能是否总是比if-else
好?- 一个函数调用的开销有多大?
while
性能是否总是比for
好?- 指针引用比数组索引更加有效吗?
- 为什么将循环求和的结果放到本地变量中,比放到一个通过引用传递过来的参数中更加有效?
- 为什么只是简单将算数表达式中的括号进行重排就能加快运行速度?
链接的错误:
- 静态变量与全局变量的区别
- 不同C文件中定义相同的全局变量
- 静态库和动态库的区别
- 命令行上排列库的顺序有什么影响
- 为什么有些链接错误直到运行时才会出现?