C++ 编译, 链接, 运行时
参考资料
简介
温故而知新
- Windows API 教科书式的执行进程与线程, 但 Linux 则是把进程和线程统一为任务, 多个任务可以选择共享内存空间
- Linux 有三种方式创建任务, fork (复制当前进程) clone (创建子进程) exec (用新映像替换当前进程映像)
pid_t pid = fork();
if(pid == 0)
{
exec("程序路径", "参数1", "参数2",...);
}
else if(pid > 0)
{
....
}
//省略了其他代码,这是一个简单的子进程创建
//注意 fork() 函数会返回两次以区分父进程与子进程
fork()
非常快, 因为它不需要复制父进程的内存空间, 而是在任务试图修改内存时, 再复制一份 (写时修改)
clone()
函数可以创建一个新的任务, 并指定开始执行的位置, 是否共享内存空间, 因此实际产生一个线程
- 过度优化造成线程不安全, 如寄存器不写回, 调整无关语句顺序, C/C++
volatile
能阻止编译器优化, 但 CPU 还是会调整语句顺序
- 许多 CPU 会提供一个指令, 用来阻止该指令之前的语句和之后的语句顺序交换
- Windows 和 Linux 都支持内核多线程, 但用户线程不一定与内核线程一一对应, 存在一对一, 多对一, 多对多的关系, 其主要差别在于有无对线程数量的限制, 与一个线程会不会堵塞多个线程以及线程的调度时切换上下文的开销
静态链接
编译与链接
- 编译有四个步骤, 预处理, 编译 (前后端), 汇编和链接
- 预处理除了我们老生常谈的展开宏插入文件删除注释之外, 还要添加行号和文件名标识, 以供编译器产生错误信息或者为调试器提供信息, 并且会保留编辑器指令
#pragma
- 编译经词法分析, 语法分析, 语义分析, 优化产生汇编代码,
gcc
中由 cc1
程序负责这两部分,as
负责汇编,ld
负责链接,gcc
只是它们的包装
- 编译过程, 首先扫描器用一种类似有限状态机的算法, 将代码的字符序列分割成一系列的记号 (Token), 记号分为关键字, 标识符, 常量, 运算符等, 其中标识符会存放到符号表, 常量存放到文字表,
lex
是通用的词法分析器, 可以按照用户描述的规则扫描
- 随后用上下文无关语法的手段进行复杂语句的语法分析, 它对重用的符号加以区分, 并建立语法树,
yacc
是通用的语法分析器可以按照用户指定的语法进行分析
- 随后是语义分析, 语句在语法上合法不代表在语义上合法, 例如指针进行乘法, 声明与类型是否匹配, 并且帮助进行自动类型转换, 标识变量常量函数运算符的类型, 但它只能检查静态语义, 而像除零等动态语义错误无法检查
- 接下来是中间代码生成, 生成诸如三地址码 (b 与 c 操作, 结果置于 a),
p-code
等, 它们与平台无关, 便于优化 (可以被解释器执行) 随后基于中间代码优化, 例如编译时可确定语句, 这就是编译器的前端, 不依赖平台
- 最后代码生成器将中间代码编译为目标代码, 随后再由目标代码优化器优化, 诸如位移代替乘除法以及涉及平台的诸多优化
- 但此时变量等的绝对地址以及使用其他模块变量的问题悬而未解, 让 linker 出手, 现代软件开发离不开模块化, 而连接每个模块 (模块间符号的引用) 就是链接
- 链接过程主要包括地址与空间分配, 符号决议 (对应符号与地址), 重定位等, 将目标文件与库 (一组目标文件) 等链接, 最基本的工作就是给跨模块的变量与函数引用添上地址 (修正重定位入口)
- 最常用的库是运行时库, 支持程序运行的基本函数集合
目标文件有什么
- 目标文件本质就是未经链接的可执行文件, 仅在结构上略有不同