答案是否定的,具体请看P343~P344。
程序首先运行的代码不是main而是入口函数或者入口点,它是程序的初始化和结束部分,它往往是运行库的一部分。
一个程序的典型执行步骤见P344。
glibc默认的入口点是_start,它是由连接器脚本指定的,当然这个入口点你也可以自定义,不过一般没人自定义。
在_start中的实现代码中的前三行,主要实现了如下功能:
1、将EBP寄存器清0以表示_start是最外层的函数。
2、把用户的参数和环境变量压栈。一般栈顶是argc,接着是argv,然后是环境变量。
3、把ESI指向argc,把ECX指向argv和环境变量。
接着执行的是_libc_start_main,它的参数里面就包括了main、init、fini、rtld_fini。
init:main前的初始化工作。
fini:main后的收尾工作。
rtld_fini:动态加载的收尾工作。
MSVC的函数入口名为mainCRTStartup,P349~P350的部分显示了该函数代码的内部实现。
在mainCRTStartup中给程序分配内存用的不是malloc而是alloc,这是因为程序在开始时堆还没有初始化,而alloc是唯一不需要堆的动态分配机制。
mainCRTStartup首先分配空间,然后初始化堆,接着各种初始化包括对main的argv、argc和其他C库。
try-except是Windows结构化异常处理机制的一部分。
P351有mainCRTStartup的内部执行步骤总结。
指代一个文件的标识,在Linux下是文件描述符,在Windows下是文件句柄,一般情况下简称为句柄。
句柄存在的意义在于防止用户随便操纵系统内核的文件对象,因为用户是通过句柄才能间接接触到的。
在Linux下每个进程都有一个打开文件表,其实质是一个指针数组,文件描述符就是这个表的下标。
C语言中的FILE结构与文件句柄是一一对应的关系。
主要包括堆初始化和I/O初始化两部分。
在MSVC中堆的初始化工作是由_heap_init完成的,在32位编译环境下,MSVC仅仅是调用HeapAlloc创建了一个堆,可以推出malloc必然是调用了HeapAlloc函数。
句柄是一个32位的数据类型,在MSVC
CRT中已经打开的文件句柄是用ioinfo结构来表示的。
用户的代开文件表是一个二维数组,第二维存储的就是ioinfo结构。
从FILE到文件句柄是通过_osfhnd的宏实现的,
MSVC的I/O初始化函数叫_ioinit,它的工作是初始化一些预定义的打开文件。包括两部分:1、从父进程进程而来的打开文件句柄。2、操作系统提供的标准输入输出。
MSVC中I/O初始化的步骤见P360。
就是C程序运行所需要的代码集合。
C语言运行库英文缩写为CRT(C Run-Time Library)。
C语言运行库的功能可见P360~P361。
这里介绍了变长参数的用法,它的实现依赖于cdecl调用惯例的自右向左压栈的参数传递方式,其实就是利用栈的性质实现的。
va_list是一个指针用来指向某参数。
va_start指向第一个参数的位置。
va_arg获取当前参数的值,并自动移动到下一个参数的位置。
va_end清零指针。
宏也可以是边长的,这种宏是变长参数宏,在gcc中用##表示这种宏,在MSVC中用__VA_ARGS__。
非局部跳转是C语言里面很少用到的语法,最起码我不怎么用。首先在你想跳转到的地方留个记号,就是setjmp(标记)。然后,在你想跳转的地方写上一条跳转语句longjmp(标记,返回值),那么它就会跳转到setjmp所在的地方并且在那个地方留下longjmp中的返回值。
C语言运行库就是个中间层。
Linux下的C语言运行库是glibc,而Windows下的则是MSVCCRT。
C++语言的实现与编译器紧密相关。
MSVC的CRT的版本多了去了,可按照静态和动态、单线程和多线程、调试和发行、托管代码和本地代码分成多个不同的版本。
这些版本之间的组合会出现很多子版本,为此微软推出了一套命名规则。比如:p代表C Plus
Plus——C++标准库,mt代表Multi-Thread,d代表debug。
在MSVC下静态链接就涉及到一个lib,而在动态链接环境下还涉及到lib和dll。
线程可以访问自己进程里面所有的公共资源,也可以访问进程里面其他线程的资源,不过线程也有自己的私有空间,这包括栈、线程局部存储和寄存器。
多线程并不是C/C++标准库的组成部分。
所谓的多线程库都是系统相关的,这包括提供了线程操作接口和能在多线程环境下运行两方面。
原来C/C++存在的问题就是原来单线程环境下,公共资源没人抢,自给自足,运行的非常顺畅。到了多线程阶段,同一个公共资源被多个线程共同使用,某线程自己的数据在无意中被别的县城给改了,这种情况在各线程身上都能发生,结果哪个线程都运行不好,这就是困扰所在。
1、原先的全局变量改为各线程私有,每线程一个。
2、加锁,这个不解释。
3、改进函数调用方式,把所有多线程不安全的函数改成多线程安全的函数。
TLS(Thread Local Storage),线程局部存储,即线程私有的局部变量,每个线程都有一个该变量的副本,这样线程之间的互操作就不会彼此影响了。
在Windows下TLS变量会被存储在PE文件中的tls段中,每个线程的副本都是从进程的堆中把tls段中的内容复制到本线程中的。如果tls中的内容是对象,该过程还需要进行构造和析构操作。
TEB(Thread Environment Block,线程环境块)记录了每个线程的堆栈地址,又因为TLS相对于每个线程的偏移量是固定的,所以可按此法找到相应的TLS。
上面提到的是隐式TLS,即自动的TLS,还有一种,是显式TLS,即程序员手动TLS方式。
显式TLS,即TLS变量的申请、初始化和释放都需要手动操作。它是借助TEB结构中的TLS数组实现的。
WindowsAPI中的线程创建和释放函数是最基本的,其他诸如CRT和MFC线程创建和释放函数都是WindowsAPI的封装。如果你是用WindowsAPI来创建和销毁线程那就不要用运行库中的,反之亦然,不然会出现莫名其妙的错误。
这一工作是由入口函数实现的。
init和finit会生成init()和finit()函数,它们分别在main前后执行。
_start中的init函数指向了__libc_csu_init函数,__libc_csu_init在glibc中的csu\Elf-init.c。
__libc_csu_init调用的是init,即用户所有存放在init中的段都会被执行,init段是由所有输入的目标文件中的init段拼接而成的。
init调用了一个__do_global_ctors_aux的函数,它是由编译器提供的,负责C++中全局对象的构造和语言特性相关的内容。
__do_global_ctors_aux函数里面有个__CTOR_LIST__数组,该数组第一个元素是元素的个数,后面每个元素存储的是函数指针,这些函数指针就是所有全局对象的构造函数的指针。
gcc会遍历每个cpp,然后针对每个编译单元生成一个特殊的构造函数来初始化所有的全局对象,该函数叫__GLOBAL_I_Hw,负责对本编译单元的所有全局和静态对象进行构造和析构。
在编译单元产生的目标文件中有一个ctors段,该段中存放的是指向__GLOBAL_I_Hw的指针,那么所有目标文件组织在一起就会形成一个ctors段,该段中存储着一个__GLOBAL_I_Hw的指针数组,
每个目标文件的前后还要连接上crtbegin.o文件和crtend.o文件。在crtbegin.o中的ctors段中存储的是-1,链接器负责将这个数值改造成为全局变量的个数,然后再将ctors段定义成__CTOR_LIST__,所以实际上__CTOR_LIST__就是所有ctors段合并后的起始地址了。
crtend.o中的ctors段中存储的是0,它定义了一个符号__CTOR_END__指向ctors段的末尾。
gcc可以指定某函数为全局构造函数,方法就是通过在函数声明前添加__attribute__((constructor))。
全局对象当然也有析构的过程,这与构造过程是互逆的。
与gcc类似,首先mainCRTStartup调用_initterm,_initterm的功能类似于__do_global_ctors_aux。__xc_a和__xc_z是_initterm的参数,前者是函数指针数组的其实笛子,后者是结束地址。
全局变量__xc_a和__xc_z在mainCRTStartup之前就被设置好了,这是因为它俩本来就在两个特殊的段里面,最后合并段的的时候被划分到了只读段之中。
各个操作系统在全局构造和析构这一点上的实现机制是相同的。
至于析构,MSVC也是通过把某函数注册为程序退出时的全局析构函数的方式实现的。
它归根结底是调用WindowsAPI中的ReadFile来实现文件读取的。
因为一次写入读出的数据量很小,为此调用系统的API显得较为低效。设置一块缓冲区域,等这块地方被填满再一次性的写入或者读出,即所谓的缓冲。
fread的功能是直接交给fread_s来完成的。
fread_s的主要的功能是检查参数的正确性,保证多线程安全的加锁,然后它直接调用了_fread_nolock_s。
它完成了fread的真正功能,本节讲述的主要是它对缓冲区的各种操作和设置。
_fread_nolock_s调用了_read函数,该函数完成从文件中读取数据和转换文本文件中的回车符两项功能。
_read调用了WindowsAPI ReadFile。
在Windows下回车的存储方式是0x0D(CR)和0x0A(LF)两个字节。但是在Linux/Unix下回车和换行都是\n,在MAC OS下是\r。
在不同的操作系统中都把回车和换行转换为\n。
本节对fread做了一个总结,还是满精辟的,前边讲的都太杂了。