符号链接

文本:源代码 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等系统常用的格式。

这些格式提供了语法规则,使得目标文件的字节块能够被链接器或加载器正确地解读。


为什么强调“字节块的集合”?

  • 通用性:这一描述抽象了目标文件的存储本质。无论是代码段还是数据段,本质上都是按字节存储的数据。
  • 便于理解文件结构:将目标文件看作字节块集合,可以帮助程序员理解文件的解析方式,特别是分析目标文件格式时。
  • 与内存映射相关:最终,这些字节块会被加载到内存中,按照相同的结构组织成进程的内存布局。

深入理解的关键

为了深入理解,可以通过以下方式进一步探索:

  • 查看目标文件结构:使用工具(如 objdumpreadelf)解析目标文件,观察各部分的字节分布。
  • 编写简单的C程序并编译:用编译器生成目标文件,并对比源代码和目标文件中的符号表、代码段。
  • 了解链接过程:理解这些字节块如何在链接过程中被组合成最终的可执行文件。

示例

编写简单的程序:

1
2
3
int main() {
return 0;
}

编译生成目标文件后,使用 readelf -aobjdump -h 查看其字节块分布。你会发现,虽然程序非常简单,但目标文件已经按照格式组织了各种段和数据块。

符号解析

符号解析是链接过程中一个重要的步骤,用于将目标文件中的符号(如变量名、函数名)解析为具体的内存地址或其他符号的引用地址。它的目的是将代码中使用的符号与其定义进行匹配,以实现正确的程序连接。

以下是详细的解释:


什么是符号?

符号是程序中用来表示变量、函数、常量或其他标识符的名字。它可以包括:

  • 全局变量
  • 函数名
  • 静态变量(如果需要)
  • 外部引用的符号(来自其他目标文件或库)

在目标文件中,这些符号并不直接对应内存地址,而是保存在符号表中,等待链接器解析。


符号解析的作用

符号解析的主要目的是:

  • 使用符号的地方(例如函数调用或变量访问)和定义符号的地方联系起来。
  • 为符号分配实际的内存地址或引用其他文件中符号的位置。

如果符号在当前目标文件中找不到定义,链接器会尝试在其他目标文件或库中找到它。


符号解析的过程

  1. 构建符号表:链接器首先扫描所有目标文件和库文件,读取它们的符号表,记录所有已定义和未定义的符号。

  2. 查找符号定义

    :对于每个未定义的符号,链接器会在符号表中搜索其定义。

    • 如果找到,链接器会将引用符号的位置更新为定义的位置。
    • 如果找不到,链接器会报错(如“未定义符号”错误)。
  3. 处理重定位信息:对于解析后的符号,链接器会根据其地址调整目标文件中的指令或数据。


常见场景

  • 内部符号解析:在同一个目标文件中,一个函数调用另一个函数,链接器只需在这个目标文件内找到符号的定义。
  • 外部符号解析:一个目标文件调用另一个目标文件或库中的符号,链接器需要跨文件查找。
  • 动态库中的符号解析:如果链接的是动态库(如 .so.dll 文件),符号解析可能延迟到程序运行时进行。

符号解析失败的原因

符号解析失败通常会导致编译或链接错误,比如:

  • 未定义符号错误:
    • 函数或变量的定义缺失。
    • 链接时遗漏了需要的库。
  • 重复定义错误:
    • 同一个符号在多个目标文件中重复定义。
  • 符号不可见:
    • 符号被声明为 static 或其他方式限制了可见性,导致无法被外部文件引用。

符号解析工具

在实际开发中,可以使用一些工具检查符号:

  • nm:列出目标文件或库中的符号表。
  • objdump:查看符号和段信息。
  • readelf:分析目标文件的 ELF 格式符号。

例如:

1
nm myfile.o

会输出目标文件 myfile.o 中的所有符号及其状态(已定义或未定义)。


示例

假设有以下代码:

**文件 a.c**:

1
int foo = 42;

**文件 b.c**:

1
2
3
4
extern int foo;
int bar() {
return foo;
}
  1. 编译

    1
    2
    gcc -c a.c -o a.o
    gcc -c b.c -o b.o
    • a.o 中定义了符号 foo
    • b.o 中引用了符号 foo(标记为未定义)。
  2. 链接

    1
    gcc a.o b.o -o program
    • 链接器会解析 b.o 中的符号 foo,在 a.o 中找到它的定义,并完成解析。

总结

符号解析是链接器将程序中的符号引用与定义匹配起来的关键步骤。它确保程序在执行时能够正确访问函数、变量等资源。符号解析的成功与否直接影响程序的正确性,开发中需要注意符号的定义与使用是否一致,以及是否正确链接所需的目标文件或库。

  • switch性能是否总是比if-else好?
  • 一个函数调用的开销有多大?
  • while性能是否总是比for好?
  • 指针引用比数组索引更加有效吗?
  • 为什么将循环求和的结果放到本地变量中,比放到一个通过引用传递过来的参数中更加有效?
  • 为什么只是简单将算数表达式中的括号进行重排就能加快运行速度?

链接的错误:

  • 静态变量与全局变量的区别
  • 不同C文件中定义相同的全局变量
  • 静态库和动态库的区别
  • 命令行上排列库的顺序有什么影响
  • 为什么有些链接错误直到运行时才会出现?