笔记:程序员的自我修养——链接、装载、与库

Jul. 22, 2023, edited on Jul. 31, 2023

第一章 温故而知新

CPU、内存和 IO 控制芯片

对于普通的程序开发者:大多时候关心 CPU,不用关心其他硬件细节;Java、Python、脚本语言开发者:CPU 也无需关心,编程语言已经提供了一个通用、无关平台的虚拟机。

计算机的硬件模型

多核、SMP

分层结构

每层之间相互通信,制定的通信协议:接口 ,精心设计,尽量稳定

下层给上层提供服务

操作系统

和硬件打交道(驱动程序),屏蔽硬件细节,为上层的应用程序提供了使用硬件的接口

内存管理

早期,程序直接访问物理内存。进程之间的内存没有进行相互隔离。(进程A可以随意更改进程B的内容(Dos中一个程序可以随意更改任意内存,可以直接破坏操作系统本身的运行))

程序需要的内存大约实际的物理内存时,该怎么办? 将其他程序的数据暂时写入到磁盘里(换入和换出)

直接访问物理内存的缺点:

  1. 地址空间不隔离:随意破坏和改写其他进程
  2. 内存使用效率低:因为程序所使用的内存是连续的,在将空间进程的数据写入硬盘时,可能会导致大量数据的在硬盘中换入换出
  3. 程序的运行地址不确定:程序在载入内存运行时,需要给它找一片空闲的内存。但这个空闲区域是不确定的。但程序在编写时,访问数据和跳转目标地址是固定的(地址在编译成二进制代码的时候已经固定了)——重定位

解决方案:添加中间层。将程序的地址(虚拟地址)映射到真实的物理地址。

好处:

隔离 进程有独立的地址空间(虚拟空间、可以看成一个很大的数组),只能访问到自己的地址空间

分段映射 一个程序进行需要多少内存,就从真实的物理地址里分配出等大小的一片区域,映射到进程的虚拟空间里(?)。物理内存的每一个字节和虚拟地址的每一个字节存在一一映射的关系。通常由硬件来完成

映射按程序为单位,内存不足时换入换出整个应用程序(粒度大)

局部性原理 程序在运行时,大部分时间只会频繁地访问一小部分数据

分页 将地址空间划分成固定等大小的页,每页的大小由硬件决定

Intel 奔腾支持4KB或者4MB的页大小,但操作系统只能选择一种(4KB)

将页作为基本单位,将程序装载到内存、硬盘中:

页错误 进程需要的页不在内存中,硬件会捕获到这个消息,发错页错误,通知操作系统接管进程,从硬盘中载入对应的页。硬件本身就支持按页来存取、交换内存

页面保护 可以给每个页设置权限属性,且只有操作系统能够修改页的属性:保护自己、保护进程

MMU(Memory Management Unit) 实现虚拟存储的硬件,一般集成在 CPU 内部

线程

程序的一条执行路径。

线程=线程 ID+指令指针(PC)+寄存器集合+堆栈

进程共享的资源:程序的内存空间(代码段、数据段、堆)、进程级资源(打开的文件、信号)

进程私有资源:栈(一般认为是私有的,尽管其实可以被其他进程访问)、线程局部存储(Thread Local Storage, TLS)(操作系统提供,容量小)、寄存器(包括 PC 寄存器)

程序员的角度:

私有共享
局部变量全局变量
函数参数堆上的数据
TLS 数据函数里的静态变量
程序代码(代码段)
打开的文件

进程调度:时间片轮转、优先级调度

设置优先级:

操作系统会根据进程的表现自动调整优先级。

IO 密集型的线程更容易得到优先级的提升。

饿死 低优先级的进程总是被高优先级的进程抢占,自己没有办法运行。

如何避免?进程的等待时间过长,优先级提升

抢占

进程被剥夺继续运行的权力(通常是时间片耗尽),进入就绪状态。

在早期的一些操作系统(Windows 3.1),进程是无法被抢占的,除非进程自己主动进入就绪状态

Windows 与 Linux 的多线程支持

clone 可以在复制进程时,可以选择共享内存空间和打开文件,相当于创建了一个新线程 ;fork 写时复制(COW, Copy on Write)

线程安全

可重入

过度优化

寄存器值 编译器为了提高访问速度,将变量优化成一个寄存器值,而寄存器在进程中是私有的

多线程的内部情况

线程的并发执行由多处理器和系统调用实现(内核线程),用户使用的线程并不是内核线程,而是用户态的用户线程。

用户线程和内核线程并不总是一一对应

三种线程模型:

第二章 编译和链接

从源代码到可执行文件的过程:预处理(Prepressing)、编译(Compiling)、汇编(Assembly)、链接(Linking)

预编译

gcc -E hello.c -o hello.i
cpp hello.c > hello.i

编译

gcc -S ./hello.i -o ./hello.S

gcc 把预处理和汇编结合到一起,交给 cc1 程序完成:

./libexec/gcc/x86_64-unknown-linux-gnu/12.2.0/cc1 ./hello.c

其他预编译和编译程序:cc1plus(C++)、cc1obj(CObject)、f771(Fortran)、jc1(Java),gcc可以看成是这些后台程序的包装

汇编

将汇编代码文件转变成机器可以执行的机器指令

as ./hello.S -o ./hello.o

链接

生成的中间文件 .o 不能直接运行,因为还缺少了必要的运行库,以及符号,使用链接器 ld 将中间文件和其依赖的运行环境链接起来,得到最终的可执行文件:

编译过程

  1. 源代码经过扫描变成 Token 序列,然后进行词法扫描:lex
  2. 进入词法分析器:语法树——yacc
  3. 语义分析:给词法树的每个节点赋予信息(数据类型,符号类型,隐式转换……)
  4. 生成中间语言(三地址码、P-代码……)
  5. 目标代码生成和优化(生成汇编语言)

连接器

经过汇编器生成的文件,很多符号(变量、函数)的地址是不确定的,甚至有的符号会依赖于其他的模块。连接器负责将这些目标文件链接起来,并重新计算所有符号的地址(重定位),将目标文件拼接成可执行文件

模块间通信:调用函数、访问全局变量

链接的工作过程:地址和空间分配、符号决议、重定位

运行时库

支持程序运行的基本函数的集合,一组目标文件组成的包

第三章 目标文件里有什么

目标文件的格式

COFF(Common File Format)的变种:

目标文件(.o)和可执行文件的格式几乎是一样的:

其他文件格式:Intel/Microsoft 的 OMF(Object Module Format),Unix 的 a.out,MS-DOS 的 COM

ELF文件的类型

file 命令查看可执行文件的格式

目标文件的内容

为什么要分段?

Windows 工具:Process Explorer

objdump 查看目标文件的内容

luo@luo ~/a> objdump -h ./SimpleSection.o
./SimpleSection.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 00000010 0000000000000000 0000000000000000 00000040 2**4
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 00000050 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 00000054 2**2
ALLOC
3 .rodata.str1.1 00000004 0000000000000000 0000000000000000 00000054 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .text.startup 00000018 0000000000000000 0000000000000000 00000060 2**4
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
5 .comment 00000013 0000000000000000 0000000000000000 00000078 2**0
CONTENTS, READONLY
6 .note.GNU-stack 00000000 0000000000000000 0000000000000000 0000008b 2**0
CONTENTS, READONLY
7 .note.gnu.property 00000030 0000000000000000 0000000000000000 00000090 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .eh_frame 00000048 0000000000000000 0000000000000000 000000c0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

代码段

luo@luo ~/a> objdump -sd ./SimpleSection.o
./SimpleSection.o: 文件格式 elf64-x86-64
Contents of section .text:
0000 89fe31c0 488d3d00 000000e9 00000000 ..1.H.=.........
Contents of section .data:
0000 54000000 T...
……
Disassembly of section .text:
0000000000000000 <func1>:
0: 89 fe mov %edi,%esi
2: 31 c0 xor %eax,%eax
4: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # b <func1+0xb>
b: e9 00 00 00 00 jmp 10 <func1+0x10>
……

数据段、只读数据段

BSS 段

// a.c
int a = 0;
static int b = 0;

查看ELF文件:

objdump -h ./b.o
./b.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 00000040 2**2
ALLOC
3 .comment 00000013 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000053 2**0
CONTENTS, READONLY
5 .note.gnu.property 00000030 0000000000000000 0000000000000000 00000058 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA

.data 的大小为0,.bss 大小为4字节

二进制文件 -> 目标文件的一个段

objcopy

luo@luo ~/a> objcopy -I binary -O elf64-x86-64 ~/Pictures/1.jpg ./picture.o
luo@luo ~/a> objdump -ht ./picture.o
./picture.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .data 00234e69 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
0000000000000000 g .data 0000000000000000 _binary__home_luo_Pictures_1_jpg_start
0000000000234e69 g .data 0000000000000000 _binary__home_luo_Pictures_1_jpg_end
0000000000234e69 g *ABS* 0000000000000000 _binary__home_luo_Pictures_1_jpg_size

自定义段

GCC 拓展:__attribute((section("seg_name")))

__attribute__((section("FOO"))) int var1 = 12;
__attribute__((section("BAR"))) int func1 (int a) { return a; }

ELF 文件里多了两个段:FOO、BAR

objdump -h ./seg_custom.o
./seg_custom.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
……
3 FOO 00000004 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, DATA
4 BAR 0000000c 0000000000000000 0000000000000000 00000044 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
……

ELF 文件结构

查看文件头

readelf -h ./SimpleSection.o
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;

文件头 Magic Number:

// 第 1 个字节 0x7f
#define EI_MAG0 0 /* File identification byte 0 index */
#define ELFMAG0 0x7f /* Magic number byte 0 */
// 第 2, 3, 4 个字节:ELF 的 ASCII
#define EI_MAG1 1 /* File identification byte 1 index */
#define ELFMAG1 'E' /* Magic number byte 1 */
#define EI_MAG2 2 /* File identification byte 2 index */
#define ELFMAG2 'L' /* Magic number byte 2 */
#define EI_MAG3 3 /* File identification byte 3 index */
#define ELFMAG3 'F' /* Magic number byte 3 */
// 第 5 个字节 ELF 文件类
#define EI_CLASS 4 /* File class byte index */
#define ELFCLASSNONE 0 /* Invalid class */
#define ELFCLASS32 1 /* 32-bit objects */
#define ELFCLASS64 2 /* 64-bit objects */
#define ELFCLASSNUM 3
// 第 6 个字节:字节序
#define EI_DATA 5 /* Data encoding byte index */
#define ELFDATANONE 0 /* Invalid data encoding */
#define ELFDATA2LSB 1 /* 2's complement, little endian */
#define ELFDATA2MSB 2 /* 2's complement, big endian */
#define ELFDATANUM 3
// 第 7 个字节:ELF 版本
#define EI_VERSION 6 /* File version byte index */
// 剩下的预留,还没有被定义

随便 hexdump 一个目标文件,看前几个字节:

hexdump --color -C ./cpp_sym_fix.o
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 01 00 3e 00 01 00 00 00 00 00 00 00 00 00 00 00 |..>.............|

操作系统会检查 ELF 的 Magic Number,来决定是否可以载入运行

段表

段描述符

段表 = 段描述符数组,在 elf.h 中,段描述符对应的结构体是 Elf32_Shdr, Elf64_Shdr

typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;

重定位表

字符串表

str-table-content str-ref

符号

特殊符号

符号修饰

目的:防止命名冲突

强符号、弱符号

强引用、弱引用

调试信息

第四章 静态链接

空间和地址分配

符号地址的确定

链接控制脚本

BFD 库

第五章 Windows PE / COFF

Windows 的二进制文件格式:

PE 是 COFF 的拓展,结构和 PE 大致相同,甚至和 ELF 同源;PE+:64 位的 PE,扩大了每个字段的大小

编译链接

COFF 文件结构

COFF = 文件头 + 若干段 + 符号表 + 调试信息等

文件头包含两个部分:

  1. 映像头(ImageHeader):表述文件总体结构和属性
    • IMAGE_FILE_HEADER 结构体(ELF的 Elf32_Ehdr)
  2. 段表(Section Table):描述各个段的属性
    • IMAGE_SECTION_HEADER 数组

映像 PE 在装载是直接映射到进程的虚拟空间中运行,进程的虚拟空间映像

COFF-struct

特有的段:

PE

** PE 数据目录** 用来查找装载需要的数据结构——IMAGE_OPTIONAL_HEADER 结构的 DataDirectory 成员,一个 IMAGE_DATA_DIRECTORY 数组

第六章 可执行文件的装载、进程

进程的虚拟地址空间

装载的方式

进程的建立

页错误

page_err

进程虚拟内存空间分布

ELF 文件的链接视图和执行视图

堆、栈