11.1 入口函数和程序初始化
11.1.1 程序从main 开始吗
- 程序有一个入口函数 entry point, 实际上是 程序的初始化和结束部分, 他往往是运行库的一个部分
- 一个典型的程序运行步骤:
- 操作系统创建进程, 把控制权交给程序的入口,这个入口, 往往是运行库中的某个入口函数
- 入口函数对运行库和程序运行环境进行初始化, 包括堆, IO, 线程, 全局变量构造 etc
- 入口函数在完成初始化之后, 调用main, 正式开始执行程序的主体部分
- main 执行完毕之后, 返回入口函数, 进行清理资源, 全局变量析构,堆栈销毁, IO 关闭, 然后进行系统调用结束进程
11.1.2 入口函数如何实现
glibc 入口函数
- 默认入口函数: _start
- init : main 调用前的初始化工作
- fini : main 结束之后的收尾工作
- rtld_fini : 和动态加载有关的收尾工作
main 函数所使用的栈布局:
使用 _cxa_atexit 和 atexit 注册的函数链表, 负责在结束的时候对链表中记录的函数进行调用, 释放已有资源
msvc crt 入口函数
- 默认入口函数 mainCRTStartup
- 基本流程:
11.1.3 运行库与IO
- 操作系统层面上, 对文件操作有个类似于 FILE 的概念, 在Linux 中称为是 文件描述符, 而在windows 中称为是 句柄
- 设计这么一个东西的目的在于防止用户随意读写操作系统内核的文件对象, 而内核可以通过句柄计算在内核中文件对象的地址 (ie, 内核中维护了一张打开文件表, 但是用户看不到)
11.1.4 MSVC CRT 的入口函数初始化
系统堆初始化
- 使用 HeapCreate 完成
IO 初始化
11.2 C/C++ 运行库
11.2.1 C 语言运行库
- CRT
11.2.2 C 语言标准库
- 变长参数
使用 va_list, va_start,va_arg, va_end 宏处理 - 他的基本原理是函数参数的默认调用方式是 _cdecl 从右向左进行压栈, 因而函数参数实际上是在栈上连续的, 可以根据栈的方向推导出来
11.2.3 glibc 与 MSVC CRT
glibc
crti.o 和 crtn.o 这两个目标文件中包含的实际上是 _init 和 _finit 函数的开始和结尾部分
通过 crtbeginT.o 和 crtend.o 来配合实现 C++的全局构造和析构工作
MSVC CRT
命名规则:
11.3 运行库和多线程
11.3.1 CRT 多线程的困扰
- 线程访问时非常自由的, (可以访问其他线程的堆栈, 如果知道地址的话), 但是实际中, 线程拥有自己的私有存储空间:
- 栈 (并非完全不能被其他线程访问, 但一般认为是私有的)
- 线程局部存储 (TLS)
- 寄存器
- 很多原有的C++ 运行库在多线程环境中出现各种问题
11.3.2 CRT 改进
- 使用TLS, 如 errno
- 加锁 (修复了 malloc/ new 等操作)
- 改进函数调用方式 xxx_s
11.3.3 线程局部存储实现
- 使用 __declspec(thread)
- 默认是隐式使用
- 在使用 CRT 的时候, 尽量使用 _beginthread/_beginthreadex/_endthread/_endthreadex 来创建线程, 否则 在 非静态链接情况下, 部分数据不会被释放, 导致内存泄露
11.4 C++ 全局构造和析构
glibc
- 通过 CTOR_LIST 存放所有全局对象的构造函数的指针
- 通过 crtbegin.o 和 crtend.o 构建 .ctor
MSVC CRT
与 glibc 类似