读《程序员的自我修养》
参考:《程序员的自我修养》
下图源自一篇日文博客(不用知道文字的意义,我反正是看不懂 🙃,这张图只是用来表示代码和可执行文件之间的关系),感谢原作者~
Tips
- MMU(memory manager unit),虚拟内存的硬件基础
- 编译器的乱序 && CPU 的乱序
- 编译器的乱序可以使用 volatile/mutex/atomic 等手段禁止
- CPU 的乱序只能使用内存栅栏禁止
- volatile 有两项功能
- 强制编译器写穿透
- 阻止编译器调整对应变量的指令顺序(无法阻止 CPU 的乱序执行)
- 预处理–>编译(汇编代码/cc1)–>汇编(二进制对象/as)–>链接(可执行文件/ld)
过度优化
Double Check 的陷阱
Double Check 缺陷的主要原因是 CPU 的乱序执行(推荐使用 std::call_once
)
volatile T* instance_ptr = nullptr;
T* get_instance() {
if(instance_ptr == nullptr) {
lock();
if(instance_ptr == nullptr) instance_ptr = new T;
unlock();
}
return instance_ptr;
}
第二次 check 与对象创建有风险,主要是因为 new 与赋值有三个过程,其中第二步和第三步有乱序的可能:
- 调用 malloc 等函数分配内存
- 调用 T 的构造函数初始化内存
- 返回内存地址赋值
instance_ptr
如果第二步和第三步乱序,那么其他线程可能看到了一个未初始化完全的对象,程序的行为是未知的
二进制文件
ELF 的大致结构如下图所示:
下图是 COFF 文件变种的内容,包含了 share/static 文件的详细内容:
.data && .bss
初始化的全局变量和局部静态变量会被分配到 .data 段, 未初始化的全局变量和局部静态变量会被分配到 .bss 段
.bss 段并不占用可执行文件的存储空间(具体依赖平台的实现),因为未初始化的全局变量和局部静态变量在程序载入内存的时候会被初始化为 0 ,这个过程可以直接在分配内存的时候完成(此时静态变量的类型是知道的)
.data 的存在主要是为了保存初始化变量的值,这些值不保存在数据段就要保存在代码段,保存在代码段无法修改
因为编译器的优化,初始化为 0 的全局静态变量可能会被保存在 .bss 中
.rodata && .data
只读数据段用于保存运行时常量,比如字符串、const 修饰的变量等等
不同平台下只读数据段的处理方式不同。嵌入式场景下只读数据段可能放在 ROM 中,代码中尝试修改此类变量有可能造成系统崩溃;x86 等场景下只读段可能与 .data 段合并,const 此时只是编译时的约束,通过一定的技巧还是可以修改此类变量的(比如指针);对于比较严格的系统,只读变量所在的虚存块会被设置为只读,此时尝试修改变量会造成进程崩溃
.rel.text 重定位表
编译单元中对外部函数的引用信息常保存在重定位表中
符号表
当前对象中包含的符号
强弱符号(略)
静态链接
链接的两个过程
-
空间地址分配
扫描所有目标文件,确定各个段的长度、属性和位置,为链接后的目标文件提供虚内存分配信息
-
符号解析与重定位
a.c/b.c
// a.c
extern int shared;
int main() {
int a = 100;
swap(&a, &shared);
}
// b.c
int shared = 1;
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
objdump -? a.o
-h:注意 VMA 的起始地址
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000051 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000091 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000091 2**0
ALLOC
......
-d:注意外部符号的地址,这里 swap 的地址被置为 0,因为 a.c 编译时没有 swap 函数的任何信息
0000000000000000 <main>:
......
31: e8 00 00 00 00 callq 36 <main+0x36> # e8 和 b8 是指令码
36: b8 00 00 00 00 mov $0x0,%eax
......
-r,.rel.text :重定位表明确指出了 a.o .text 段第 32 字节处需要重定位,符号名是 swap
,类型是 R_X86_64_PLT32
,表明是一个函数
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000025 R_X86_64_PC32 shared-0x0000000000000004
0000000000000032 R_X86_64_PLT32 swap-0x0000000000000004
000000000000004b R_X86_64_PLT32 __stack_chk_fail-0x0000000000000004
......
objdump -? a.out
-h :可执行文件的 VMA 起始地址和 .o 文件不同,此时已经是确定的虚内存地址位置
Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 0000001c 0000000000000238 0000000000000238 00000238 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.ABI-tag 00000020 0000000000000254 0000000000000254 00000254 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
......
-r:
a.out: file format elf64-x86-64
readelf
readelf -s a.o
注意这里 swap 和 shared 两个符号都是 UND,链接时这些符号如果不存在全局符号表中链接器会报错
Num: Value Size Type Bind Vis Ndx Name
......
8: 0000000000000000 81 FUNC GLOBAL DEFAULT 1 main
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND __stack_chk_fail
......
readelf -s b.o
......
7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 shared
9: 0000000000000000 45 FUNC GLOBAL DEFAULT 1 swap
装载
虚拟内存布局
第七章(待续)
动态链接
Windows 下的动态链接
windows 下的动态链接基于 DLL,windows 下软件和系统的更新很多时候就是对 DLL 的更新
dllexport && dllimport
与 Linux 下动态连接库默认导出所有全局符号不同,DLL 默认不导出任何符号
如果想让外部可见 DLL 中的符号,编译时需告知编译器,示例代码如下:
#if defined(LEVELDB_COMPILE_LIBRARY)
#define LEVELDB_EXPORT __declspec(dllexport)
#else
#define LEVELDB_EXPORT __declspec(dllimport)
#endif // defined(LEVELDB_COMPILE_LIBRARY)
LEVELDB_EXPORT Status DestroyDB(const std::string& name, const Options& options);
__declspec(dllexport)
表示当前符合可以被外部发现
__declspec(dllimport)
表示当前符号由外部 DLL 导入
/LD && /LDd
使用 MSVC 的 cl 编译命令编译代码并提供 /LD 或者 /LDd 参数可以生成 DLL
编译 DLL 文件的时候会生成4个文件:dll、obj、exp 和 lib,其中 lib 文件是其他编译单元使用 DLL 时的编译依赖(import libary),obj 是生成 dll 文件时生成的中间文件