程序员自我修养笔记:第十一章

第11章 运行库
1.一个典型的程序运行步骤如下:
系统创建进程后,把控制权交给程序入口,而这个入口称为入口函数或入口点,这个入口往往是运行库中的某个入口函数。
入口函数对运行库和程序运行环境进行初始化,包括堆、IO、线程、全局变量构造等等。
入口函数完成初始化后,调用main函数正式执行程序主体部分。
main函数执行完后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁,关闭IO等,然后进行系统调用(__exit)结束进程。

2.glibc入口函数
glibc的程序入口为_start(这个入口是由ld连接其默认的链接脚本所指定的,可以通过相关参数设定自己的入口)。_start由汇编实现,并且平台相关。
然后介绍了i386的_start实现。
Linux下环境变量和程序参数是由装载器压入栈中的。
_start设置ebp为0,获取argc和argv。调用_libc_start_main,其参数为:
main函数地址、
argc、
argv、
init(main调用前的初始化工作)、
fini(main结束后的收尾工作)、
rtld_fini(跟动态加载相关的收尾工作)、
stack_end(即栈底)
_libc_start_main设置了环境变量指针,调用了一系列函数。比如检查操作系统版本。值得关注的是_cxa_atexit函数,用于将参数指定的函数在main结束之后调用,所以参数的fini和rtld_fini均在main结束后调用。最后
result = main(argc,argv,__environ);
exit(result)。
exit函数遍历由__cxa_atexit和atexit函数注册的函数链表(__exit_funcs)。最后调用_exit(汇编实现,平台相关),由它调用exit系统调用。可见不管是程序中显示调用exit还是正常退出,都会进入exit函数。
_start和_exit后的hlt指令。
_exit后的hlt是为了检测艾滋的系统调用是否成功如果失败程序就不会停止,而hlt指令就可以发挥作用,强行把程序给停下来。
_start后的hlt是为了预防没有调用exit这个函数。(比如误删了_libc_start_main末尾的exit)。
3.Windows下运行库的细节,MSVC的CRT默认的入口函数名为mainCRTStartup(vs 2003里的crt0.c)。
流程如下:
1.初始化和OS版本有关的全局变量
2.初始化堆。
3.初始化I/O。
4.获取命令行参数和环境变量。
5.初始化C库的一些数据。
6.调用main并记录返回值。
7.检查错误并将main返回值返回。

4.MSVC的初始化I/O
Linux和Windows中文件操作类似于File的概念分别叫做文件描述符和句柄。
C语言中操纵文件的渠道则是File结构,而File结构中肯定含有fd。
MSVC入口函数初始化包括堆和I/O初始化。
32位编译条件下,MSVC堆初始化仅仅调用HeapCreate这个API。
I/O初始化相对负责很多。
用户空间的打开文件表用ioinfo表示,而且是用指针数组模拟的二维数组(ioinfo __pioinfo[64][32]),总共可以容纳2048个句柄,之所以使用指针数组是因为更节省空间,不用一次性分配2048个ioinfo。
MSVC的FILE的_file就是fd,由它索引打开文件表。
首先初始化一个第二维的打开文件表。
然后根据
GetStartUpInfo API获取继承父进程的句柄
并复制到当前打开文件表中(可能分配多的第二维的文件表)。
然后初始化标准输入输出。
MSVC的初始化工作如下:
建立打开工作表
若继承自父进程,则从父进程的句柄中复制到打开文件表中。
初始化标准输入输出。

5.C语言运行库大致包含如下功能:
启动与退出:包括入口函数即入口函数所依赖的其他函数等。
I/O:I/O初始化
堆:堆初始化
语言实现。
调试。
C语言标准库,其包含的功能有:
标准输入输出、文件操作,制服操作字符串操作数学函数资源管理格式转化等。
变长参数是基于C语言的调用惯例而实现的。

6.运行库是平台相关的C语言的运行库,从某种程度上来讲,是C语言的程序和不同操作系统平台之间的抽象层。
glibc经过时间演变,发展成了Linux下的c标准库。
glibc的发布版本主要由头文件,如stdio.h、stdlib.h,位于/usr/include;河库的二进制文件部分组成,二,进制部分主要就是C语言标准库,它有静态和动态两个版本。动态位于/lib/libc.so.6;静态的位于/usr/lib/libc.a,glibc除了c标准库,还有几个辅助程序运行的运行库。他们就是
/usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o
crt1.o,包含的就是程序的入口函数_start。
为了满足C加加的全局对象的构造和析构。在每个目标文件中引入了.init的和.fini的段。运行库保证这两个段中的代码会先于/后于main函数执行。链接器在进行链接时,会把所有输入目标文件中的.init的和.fini的照顺序收集起来,将它们合并输出成输出文件中的相应的这两个段。但是输出的这两个段所包含的指令还需要一些辅助代码来帮助他们启动,比如计算GOT之类的,于是引入了帮助实现初始化函数的crti.o和crtn.o。
crti.o的这两个段保证在输出文件的相应段的开头运行,而crtn.o则在结尾。因此连接器的输入文件顺序一般是:
ld crt1.o crti.o [user_object] [system_libraries] crtn.o
由于crt1.o并不包含.init和.fini段,因此不会影响最终生成的这两个段的顺序。
C加加的全局对象的构造函数和析构函数并不是直接放在这两个段中的,还是把执行所有构造析构函数的调用放在里面。
除了全局对象构造和析构外,这两个段还有其他作用。比如用户监控程序,性能调试等工具,经常利用它们进行一些初始化和反初始化。我们也可用“attribute((section(“.init”)))”将函数放进.init段。但是普通还是会破坏他们结构,因为其返回指令会是使_init提前返回。必须使用汇编指令。

7.gcc平台相关文件
除了crt1.o crti.o crtn.o,第四章链接hello.c时还有
crtbeginT.o、
libgcc.a、
libgcc_eh.a、
crtend.o
它们都位于gcc的安装目录
/usr/lib/gcc/i486-gnu/4.1.3/

crtbeginT.o、crtend.o是真正用于实现c++全局构造和析构的。因为glibc只是个c语言运行库,它对c++实现并不了解,而gcc是c++的真正实现者。这两个配合glibc实现c++全局构造和析构。
由于gcc支持多平台,而有些32平台不支持64位long long类型运算,因此需要一些辅助例程。libgcc.a就是包含这种函数,还有浮点运算。而它动态链接版本名为libgcc.so。
libgcc_eh.a则包含了c++支持的异常处理平台相关函数。

8.MSVC CRT比glibc看上去更有序。MSVC根据不同属性提供多个子版本,比如静态/动态;单线程/多线程;调试版/发布版:纯c/支持c++。其中有些可以组合,有些不行。
静态版的CRT位于MSVC的安装目录下的/lib,它的命名规则:
libc [p] [mt] [d] .lib
动态链接版包含链接用的.lib和运行时用的.dll。他们命名方式与静态版的相似,但是包含版本号。
默认下,若编译链接时不指定哪个CRT则默认选择libcmt.lib。
MSVC提供额外相应的C++标准库,仅仅包含C++部分。当你程序里包含c++标准库的头文件时,MSVC编译器会在目标文件的.drectve保存相应的c++标准库链接信息。

9.运行库与多线程
对于标准库来说,线程相关部分不属于其内容。但是主流的crt都会有相应多线程的内容。一方面是多线程操作接口,一方面是c运行库本身要能在多线程下正常运行。
1)errno问题
2)printf/fprintf
3)malloc/free
为了解决c标准库在多线程下的问题,许多编译器附带了多线程版本的运行库。MSVC下用/MT和/MTd参数指定使用多线程运行库。

10.CRT改进
使用TLS
对于errno使用tls,不同线程errno返回的地址不一样。
加锁
malloc、printf
改进函数调用方式
提供strtok的线程安全版本strtok_s
但很多时候这种方法不可行。更好的做法是不改变任何标准库原型,只是对其实现方法进行改进。

11.TLS实现。
tls变量声明
MSVC:__declspec(thread) int number;
GCC:__thread int number
Windows下tls变量是放在“.tls”段中,每次启动新线程,会在堆中分配一块内存将“.tls”段中的内容复制到这块空间中。
但对于C++来说,不仅仅是复制这么简单,需要对这些对象进行初始化,线程退出时还要逐个析构。
PE文件的数据目录(DATADIRECTORY)的16个元素中有一个存储的是tls表的地址和长度,其保存了所有tls变量的构造和析构函数地址,Windows就是根据tls表每次线程启动或退出时对tls变量进行构造和析构。tls表往往位于“.rdata”段中。
那么每个线程是怎么访问tls变量的呢?
对于每个Windows线程,会有一个TEB(thread environment block),其保存了线程的堆栈地址、线程ID等。其中有一个域是TLS数组,他在TEB的偏移是0x2C,而对于每个线程FS寄存器所指段就是TEB,于是一个线程的TLS数组可以通过FS:[0x2C]访问。
tls数组一般是64个元素。而第一个元素就是该线程的“.tls”段。再加上变量在“.tls”段的偏移,就是该tls的地址。
显示tls通过Windows API用于tls变量的申请赋值取值摧毁。因为其限制多,现在没怎么用了。
Windows下最好用MSVC CRT提供的_beginthread()和_endthread()用于线程启动退出。用Windows API会因为在堆上经由一些CRT函数如strtok()或_beginthread()本身申请的_tiddata结构在静态链接下无法正常销毁而产生内存泄露(此问题会在动态链接时每个线程启动/退出时必调用的每个dll的DllMain释放掉)。

12.C++全局对象的构造和析构。
glibc的全局构造和析构
在前面讲过glibc的程序的入口_start,其传递给__libc_start_main的一个参数为__libc_csu_init:
它调用了_init()函数,而这个函数是可执行文件的.init段代码,通过反汇编一个可执行文件发现其.init段中,调用了一个叫做__do_global_ctors_aux函数,它并不属于glibc,而是来自gcc提供的一个目标文件crtbegin.o。前面也提到过,链接器最终链接的一部分目标文件来自于gcc,那些是与语言密切相关的函数。查看其源码:
调用了一个名为__CTOR_LIST_的函数指针数组的所有函数。明显其保存的就是全局对象的构造函数指针。
look back,编译器在编译每个编译单元(.cpp)时,生成一个特殊函数,其作用就是对本文件的全局对象进行初始化。
其不仅是调用了每个全局对象的初始化函数,还向atexit注册了一个特殊函数__tcf_1。
编译器在生成这个给当前文件的全局对象初始的函数后,将其指针放在了目标文件的.ctor段。
这样连接器在链接这些.o文件时会把同名段合并起来,.ctor也就保存了所有全局初始化函数的指针。而其前后也链接上了crtbegin.o和crtend.o的.ctors段,crtbegin.o存储了一个四字的值,还将这个值的起始地址定义为符号__CTOR_LIST,连接器负责在链接时填成全局构造函数的数量。crtend.o只是单单保存了null,定义符号__CRT_END__。
析构
在早期的glibc析构,采用的是跟上面所讲的构造差不多相同的方法。但是这必须保证全局对象构造和析构的顺序刚好相反,这增加了连接器的工作量,因此才采用了在__cxa_atexit在exit函数中注册进程退出回调函数来实现析构。
而前面所提到的那个神秘函数__tcf_1就是跟全局构造函数相对立的全局析构函数,其调用析构函数的顺序跟全局构造函数调用构造函数的顺序相反。
由于全局对象的构建和析构由运行库完成,因此构建时不能使用“-nonstartfiles”或“-nostdlib”选项
有些平台的汇编器和链接器不支持.init和.ctor这种机制,为了实现main函数前执行代码,在链接时,collect2程序会收集所以.o文件的特殊符号,这些符号表明他们是全局构造函数或main前执行,collect2讲这些符号地址保存在一个数组,并存储在一个临时的.c文件,编译后与其他.o文件链接成最终输出文件。
这些平台上,gcc编译器会在main前产生一个_main的调用,它负责collect2收集的函数,_main属于gcc提供的.o文件的一部分,使用-nostdlib可能得到_main为定义错误,这时需要-lgcc把它链接上。

13.MSVC CRT的全局构造和析构
在MSVC的入口函数mainCRTStartup里有一个_initterm(__xc_a,__xc_z)的调用,而initterm的内容则是以这两个指针为左右边界调用函数。可见跟__do_global_ctors_aux长的一模一样。
typedef void (__cdel *_PVFV)();
_CRTALLOC(".CRTKaTeX parse error: Expected group after '_' at position 13: XCA") _PVFV _̲_xc_a[]={NULL};…XCA",long,read)
上面的代码表明了,它在目标文件中定义了.CRT段,XCA组,并将__xc_a这个函数指针数组保存在这个段的这个组中。
当编译时,每个编译单元都会生成名为.CRT$XCU的组,并加入自身的全局初始化函数。
链接时,会将该段合并,组按照字符序依次排列。最后往往因为这些段都是只读的被放在.rdata段中,而先前调用_initterm用到的两个指针,一个在开头,一个结尾。因此跟glibc的大相径庭。
析构方面,也都是在全局构造函数里向atexit注册进程退出函数。也跟glibc差不多。
14.文件IO
fread->fread_s->_fread_nolock_s->_read->ReadFile
除了最后一个是Windows API,前面的都是MSVC运行库。
fread 只是调用fread_s
fread_s 增加缓冲溢出保护:加锁
_fread_nolock_s 循环读取、缓冲
_read换行符转换
ReadFile Windows文件读取API

猜你喜欢

转载自blog.csdn.net/weixin_45719581/article/details/123207828
今日推荐