程序员的自我修养<读书笔记>

《程序员自我修养--链接器、装载器与库》,书小错误不断,有时叙述很乱,一会windows一会linux,而且中间跳转毫无说明,叙述时大部分时间是windows和linux都会讲,但是到书的后面部分,有时讲完了windows或者linux后就直接跳到别的内容去讲了,对另一个毫无说明或一句带过。这些情况到了书的结束阶段会越加分明,跳来跳去没章法。但是念在是第一版,肯定有很多地方无法校对完全,而且书中静态链接,动态链接,装载等部分确实能学到内容,也就忍了。书总体来说还不错,之前才看完《深入理解计算机系统》中链接那章,这本书是对它的大幅扩充,可以看到很多不知道的东西。

内容就像它名字一样,总共也就分三部分,先是链接,而后装载,最后讲库。还讲了一个自己实现的CRT(C运行库)库,挺好。

第一章 总结了计算机原理,操作系统,进程相关,线程相关

1、感觉本章线程部分不错,总结什么是线程私有的,什么是线程之间共享的。

线程私有线程之间共享(进程所有)
局部变量全局变量
函数的参数堆上的数据
TLS数据函数里的静态变量
 程序代码,任何线程都有权利读取并执行任何代码
 打开的文件,A线程打开的文件可以由B线程读写

2、互斥量与信号量:信号量在整个系统可以被任意线程获取并释放,即同一个信号量可以被系统中的一个线程获取之后,由另一个线程释放。而互斥量则要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁,其他线程去释放这个互斥量是无效的。

3、临界区:临界区的作用范围仅限于本进程,对其他进程无法获取该锁,除此之外,临界区具有和互斥量相同的性质。

4、volatile和barrier是怎么回事。

5、多线程内部情况,即内核线程与线程的对应情况。老实说这段我没有理解,不知道他说的这个内核线程是什么,以前没有遇到过。


第二章 编译和链接

1、介绍一个程序的编译时,编译器都做了什么。讲的很细。预编译,编译,汇编,链接每个过程都做了什么。编译器执行的词法分析、语法分析、语义分析、源码级优化,目标代码生成和优化。这都是我之前不知道的,可以再看看,虽然是大致上介绍,但还是不错。

2、链接器的工作,本质上是把一些指令对其他符号地址的引用加以修正。主要任务为地址和空间分配、符号决议、重定位。(记忆最后连个吧,CSAPP就写两个,也最基本)。

3、库就是一组目标文件(可重定向目标文件)经过压缩组成的包。


第三章  目标文件格式。

挺细的。写了个小程序,编译完后objdump分析其各个数据都存在了哪个段。

1、size命令查看ELF代码段,数据段,bss段长度。

file命令可以查看一个可执行文件是什么类型的。(Relocatable, Executable, shared object, Core dump(最后这个我没见过))

2、.rodata存放只读数据,如const修饰的变量和字符串常量。支持了const关键字。这个实测确实是把变量放到.rodata去了,但当然必须是初始化过全局变量才行。所以用const修饰,并初始化过的全局变量,是无法用强制转换去掉其const属性的。比如:

  1. const int a = 1;  
  2.   
  3. int main(int argc, char *argv[])  
  4. {  
  5.         int *p = (int *)&a;  
  6.         *p = 4;  
  7.         printf("%d,%d\n", *p, a);  
  8.   
  9.         return 0;  
  10. }  

这么写是可以过编译的,但是只要执行,就段错误,因为符号a在.rodata上,只读。而这样:
  1. int main(int argc, char *argv[])  
  2. {  
  3.         const int a = 3;  
  4.         int *p = (int *)&a;  
  5.         *p = 4;  
  6.         printf("%d,%d\n", *p, a);  
  7.   
  8.         return 0;  
  9. }  
是毫无问题的可以把a符号的const属性去掉,成功输出两个4,即改变了a的值,虽然其为const。

3、为什么目标文件要分那么多段(section)?

N1、为了数据和指令映射到虚存的不同区域,讲不同区域设置为不同权限,防止指令被有意或无意的修改。

N2、提高程序局部性,提高Cache命中率。

N3、系统运行着该程序多个副本,其指令都一样的,所以分段后内存中只需要保存一份该程序的指令部分。

4、初始化后的全局变量一般应放在.data段,但是如果初始化值为0,编译器有可能做出优化,将此变量放入.bss,造成不清晰。p66

5、应用程序可以用一些非系统保留的名字作为段名,但自定义段名不能加"."前缀。P68

6、讲解ELF文件结构。包含:

ELF Header
.text
.data
.bss
... other sections
Section header table
String Tables
Symbol Tables
...
每个结构里字节数,都干什么的写的很多。p68 - p86
比较重要的是从p80开始的符号表讲解。符号表头那一堆LOCAL,符号名都没有显示,但是符号名就是那些段名,段名也是符号,这里的LOCAL就是这些段名。看Ndx号可以知道他们各自代表的都是哪些段。

7、编译器对函数符号的修饰。主要是这些修饰影响到符号,所以会影响到链接情况,需要了解。各个编译器对符号的修饰手法不一,导致不同编译器编译出来的目标文件无法相互链接(主要原因之一)。尤其C++,因为其需要支持许多特性,重载,虚函数等等,修饰手法更繁多。C中,GCC以前是自动给函数加下划线,现在不这么干了,但是还是要留意一下。

8、extern "C"的使用。为了让C++能很好的调用C编译出的函数,要使用extern C,如下:
  1. #ifdef __cplusplus  
  2. extern "C" {  
  3. #endif  
  4.   
  5. void *memset(void *, intsize_t);  
  6.   
  7. #ifdef __cplusplus  
  8. }  
  9. #endif  
这个声明方式见的很多,蛮重要的。当然如果把memset换为一个c++函数,就没必要这么写啦。考虑的是,C还是调用不了C++写的函数啊。。p90

9、强弱符号,这里简单介绍了一下强弱符号概念,那个很有用的common块没在这里讲。p92

10、GCC编译时候如果加上-g,会在ELF问件中,加入.debug段,各种调试信息。最终版发布时,用命令 strip func来去掉func ELF文件中的调试信息。p94

第四章 静态链接

1、段的装载地址和空间的对齐单位是页,即4096字节。链接的时候,各个目标文件的相同段合到一起。

2、链接器为目标文件分配地址和空间,这里地址和空间指 1 是分配输出的可执行文件中的空间  2 是分配装载后的虚拟地址中的虚拟地址空间。而对于.bss,其不会真正占用可执行文件中的空间,其在ELF中仅仅为一个符号,包含符号各种信息。直到被装载入虚拟地址后,才会获得空间。

3、链接过程总共分两步:
第一步 空间与地址分配 收集所有符号及其信息放入 全局符号表
第二步 符号解析与重定位  链接过程的核心,特别是重定位 p101

4、重定位 最重要的地方了,p105左右,主讲重定位表(重定位段。与CSAPP一样,这里介绍R_386_32和R_386_PC32。要注意的是.rel.xxx装的是重定位表,不是实际段的信息(.text .data什么的),实际内容还是在.text .data,.rel.xxx中通过r_offset来指示出要重定义的符号在.text或.data的偏移,为重定位提供信息。

5、COMMON块。弱符号(未初始化的全局变量等)编译为可重定向目标文件后,并不是直接放入.bss,而是先标记为COMMON块放入别的地方。等链接后,看别的目标文件中有没有重名的COMMON块,没有的话将此符号放入.bss,有的话要比较各COMMON块大小,将最大的那个放入.bss,其余全舍弃。p111
GCC有个去除COMMON块开关:-fno-common

6、C++链接相关。.init .fini段可以用于实现构造和析构函数。C++的ABI(二进制接口)复杂,很难让各编译器之间兼容。p116

7、静态库。静态库就是一组可重定向目标文件集合,用ar等工具将这些目标文件 压缩到一起,并进行 编号和索引
注意:对于只使用了一个字符串参数的printf,即没有%d %s什么的时候,GCC会自动优化,将此printf改为puts,效率是高了,但是有时候会比较麻烦,改变的办法是:
编译时加入开关 -fno-builtinp           p117

8、静态库中,每个函数独立地放在一个目标文件中,用于减少空间的浪费,没有用到的目标文件(也即是函数),不会链接到最终的可执行目标文件中。p123

9、windows内核在system32\ntoskrnl.exe,不了解,很神奇。

10、链接脚本控制。未仔细读,标记一下吧。这里作者写了个 print及exit函数,之后章节会用到。p123 最后还介绍了BFD,GNU用于统一不同目标文件格式的库。

第五章 windows PE/COFF 未仔细读

第六章 可执行文件的装载与进程         名字起为进程的可执行文件装载是不是更好一点。。。。

1、 程序(或狭义讲的可执行文件)是一个静态的概念,它就是一些预先编译好的指令和数据集合的一个文件; 进程则是一个动态的概念,他是程序运行时的一个过程,很多时候把动态库叫做运行时(Runtime)有一定道理。

2、进程虚存空间。windows虚存空间划分是2G 2G,不同于linux的3G 1G。这块还带了一下PAE。         p151

3、装载方式。动态装载目的就在于将程序最常用部分驻留内存,而不常用数据放在磁盘里。
主要的装载方式有 覆盖装入页映射。覆盖装入就灌个耳音就好了,现在都用页映射。页映射就是,可执行文件,各段都按页对齐放在硬盘上,文件执行时,将各个段映射入物理内存,并且运行到哪块,需要某一页内容时,才会从硬盘将此页内容载入内存,内存满了也会将不用的页换出。p155

4、进程建立过程。
N1创立一个独立的虚拟地址空间。(很快,就创建进程结构,分配页目录。但我在想,页目录有限啊,系统能同时运行多少个进程呢)
N2读取可执行文件头,并且建立虚拟空间与可执行文件的映射。(这块注意可执行文件在硬盘,这里要映射入虚拟内存。Linux将进程虚拟空间中的一个段,叫做 VMA。)
N3将CPU的执行寄存器设置成可执行文件的入口地址,启动运行。p157

5、 链接视图与执行视图。为了节约空间,可执行文件载入内存的时候,并不完全是按.text .init .data等section划分,而是按各个section的读取 执行权限划分为许多的segment。相同权限的section,会载入相同的segment。比如.text和.init都是只读 可执行,就放在同一个segment。装载的时候完全是按照segment来装载。这样.text和.init载入内存后只对应一个VMA,而不是两个,这样的好处在于减少内存碎片。
描述segment信息的结构叫 程序头。打开后可以看到,确实是按权限划分的,且可以看到一个segment里面都有哪些section。
segment和section都是针对同一个ELF文件,只是角度不同。以section角度划分文件就是链接视图,而按segment角度看就是执行视图。p160

6、堆和栈。可以看看堆和栈是怎么按照segment装入内存的。p166

7、 段地址对齐。UNIX系统中,各个段(就是segment,这在讲装载)接壤部分共享一个物理页面(注意,一个segment可能占好几个页,这里单只segment中,接壤的那个页面,别的页面照常映射的),然后将物理页面分别映射两次。这句话看不懂,就去看6-10和6-11两幅图,图仔细看很好懂,看懂图了再看作者话就好明白多了。p169

8、进程栈初始化。这块比较好的是可以看到程序开始的时候栈是什么样子的。一开始给进程的参数和环境变量实际信息都存在栈底。然后后面传给main函数时,是在栈顶压入指向那些实际信息的指针,再传给main。p171

9、把linux装载elf文件全过程, 串讲一遍。挺好的。

10、最后有个PE文件装载介绍,没看。p175。

第七章 动态链接

1、动态链接好处:
N1、减少物理页面的换入换出,增加Cache命中率。因不同进程间数据和指令访问都集中在同一模块上。
N2、当要升级程序库或程序共享的某个模块,理论上只要简单地将旧目标文件覆盖,无需重链接程序所有目标文件链接一遍。
N3、使得开发过程更加模块化,更独立,耦合度低,可进行独立测试。
N4、可实现程序的插件功能,用户自己将要的功能插进程序。

2、动态链接的问题在于,新旧模块接口可能出现不兼容,从而使程序完全无法运行。

3.、运行一个程序之前,控制权首先交给动态链接器,由其完成所有动态链接工作后,控制权再交于程序。

4、上一章中,提到装载信息可以用readelf -l查看装载信息,对于普通链接好的可执行目标文件,装载信息中,虚地址一栏会写入装载后的虚地址,但是打开一个动态库的装载信息后,其虚地址一栏都是000000。说明装载地址在编译时无法确定。p188

5、 地址无关代码。这块已经到了非常好的地方了。笔记这里是无法写下的,看书吧,这块要时常复习才行,很不错,看的时候容易晕,但是讲的还算清楚。p188

6、 共享模块的全局变量问题。同上,这只做标记,内容非常好,必须常复习。p197

7、 延迟绑定。同上。p200

8、动态链接器自举。普通的动态库链接时都依赖于动态链接器,需要其帮助才能完成装载,那么动态库链接器自己又是怎么装载进虚存的呢。其也是个动态链接库,在程序运行后的map信息里可以看他到他的存在,在内存中。
首先,其不依赖于其他人和共享对象;其次,动态链接器本身所需要的全局和静态变量的重定位工作由它本身完成。
自举时,不能使用全局变量或静态变量,也不能调用函数。p214

9、符号优先级和全局符号介入。优先级问题就是,如果一个程序要加载许多动态库,这些动态库中对同一个符号都做了定义时,先加载的动态库中的符号会将后加载的库中的同名符号遮蔽。这也叫全局符号介入。p215
一个普通的可执行目标文件中的函数,去调用本文件中,另一个没有加static的 函数,一般call指令用的都是相对寻址。且两函数都在.text段,没有加static修饰的都是全局函数。但是对于库文件来说,因为链接前,一个库文件无法确定是否会链接另一个库,也无法确定一个函数是否会和另一个库中的函数重名。比如链接可重定向目标文件时,要两个库lib1.so lib2.so。然后cc program ./lib1.so ./lib2.so这么链接,而lib1.so lib2.so中同有个函数,名叫func。那么这时候会出现全局符号介入,所以调用这个函数时的call指令的地址,在链接时必须被重定向,所以func函数不能放入.text。所以任何.so动态库文件中的函数,只要是非static的,都要放到.rel.plt段(如果开启pic的话, 不开pic在.rel.dyn段)以防链接时不只有本库文件,还有别的库,且别的库可能有同名函数符号。所以这么放入.rel.plt段的函数在运行时就会有损失,首次运行要修改.got.plt段,且后续运行时,每次要去.got.plt找函数运行时地址在哪,都要耗费时间,而不是像普通函数在.text段那样,直接有相对地址。p218

上面那段,红线那句话,“只要是非static的,都要放到.rel.plt段(如果开启pic的话,不开pic在.rel.dyn段)”,书上之前不是讲.rel.dyn是修正数据引用.got以及数据段的吗?这里怎么放进去函数了?看p210讲解。

10、显示运行时链接。动态链接另一个神奇的功能。可以在程序都运行起来后,通过调用函数,将某个动态库映射进入程序虚存。相当于链接上了此库。

第八章 Linux共享库的组织。没怎么看。
主要讨论共享库中的兼容性,各个版本命名规则,库默认所在位置,环境变量,怎么安装共享库等。

第九章 windows下动态链接。没怎么看

第十章 内存

1、讲linux3G 1G内存怎么分布的。哪里是栈,哪里是堆。栈的使用方法,汇编指令是什么。在VC中,调试时会将所有分配的栈初始化为0xCC,所以调试时常看到"烫烫烫烫烫"字样。本章开始就开始乱了。此处本来开头在讲Linux内存布局,完全没提到windows,但后面讲栈指令的时候MASM就来了,VC就来了,虽无大碍,但是总感觉编排不好,应该连续才更符合一点么。p284 - p292

2、调用惯例。函数是怎么调用的,谁负责压栈,怎么压。但是不好的地方在于,既然写惯例,也提到保存ebp等,那就该提下由谁来保护eax ecx edx,由谁来保护esi edi ebx 等寄存器啊。没有提到。图很清晰,但不完整。后面提到几种调用惯例还不错,cdecl stdcall fastcall pascal四种调用惯例。可以通过void __fastcall func(void)这样在声明或定义函数时,给定一个函数的调用规则。但声明和定义时调用规则不能不同。函数返回5-8字节对象时,用edx eax联合返回。p293 ~ p305

3、Linux两种堆分配方式,两个系统调用。brk()和mmap()。

4、brk()实际就是设置进程数据段的结束地址,即他可以扩大或缩小数据段。Linux下数据段和BSS合并在一起统称数据段。sbrk()是brk()变种,是Glibc中的函数,不是系统调用。参数是一个增量,增加或减少空间大小,返回值是变化后数据段 结束地址

5、mmap()作用是向系统申请一段虚拟地址空间,这块虚拟地址空间可以映射到某个具体文件(最初此系统调用的作用),当其不映射到某个具体文件时,这块空间就称作 匿名空间,匿名空间可以拿来作为堆空间。从内存分布上也可以看出,只用brk()分配内存的话,会有动态库映射阻碍,不能总用brk(),还需要mmap()。p306

6、glibc下的malloc是这么处理用户空间请求的。对于小于128k的请求,他会在现有堆空间里按照堆分配算法为它分配一块空间并返回;对于大于128k的请求,他会使用mmap()函数为它分配一块匿名空间,然后在这个匿名空间中为用户分配空间。
与windows下的virtualAlloc()类似,malloc申请空间的起始地址和大小,也都必须是系统页的大小的整数倍。所以对字节数很小的请求如果使用mmap()会比较浪费。p307

7、Linux2.6中,动态库映射(动态库装载地址)已经被移到了非常靠近栈的位置,于是,用brk()就可以分配到很大的地址空间。mmap()申请空间时要注意,其申请匿名空间,系统会为他在内存或交换空间预留地址,但 申请的空间大小不能超出空闲内存+空闲交换空间的总和。p308

8、windows进程堆。windows的堆增长方向不同, 不一定向上生长。比如HeapCreate()函数,完全不遵照向上增长这个规律。p311

9、堆分配算法。配合《深入理解计算机系统》看吧,这里也仅大致介绍。glibc中,对于小于64字节的空间申请时采用类似于对象池的方法,对于大于512字节的空间申请采用的是最佳适配算法;对于大于64字节而小于512字节的,他会根据情况采取上述方法中的最佳折中策略;对于大于128kb的申请,他会使用mmap()函数,直接向操作系统申请。p312

第十一章 运行库

1、可以用atexit()函数来注册main()函数结束后,要执行的函数。如:
  1. void foo(void)  
  2. {  
  3.    printf("aaaa\n");  
  4. }  
  5. int main()  
  6. {  
  7.     atexit(&foo);  
  8.     printf("end of main\n");  
  9. }  

2、典型的程序运行过程:
N1 操作系统创建进程后,把控制权交到了程序的入口,这个入口往往是运行库中的某个入口函数。
N2 入口函数对运行库和程序运行环境进行初始化,包括堆、IO、线程、 全局变量构造等等。
N3 入口函数在完成初始化后,调用main()函数,正式执行程序主体部分。
N4 main()函数执行完毕以后,返回到入口函数,入口函数进行清理工作,包括 全局变量析构,堆销毁,关闭IO等,然后进行系统调用结束进程。
注意init fini段进行的构造与析构可都是对全局变量的。

3、glibc在不同情况下差别很大,静态glibc和动态glibc有差别,glibc用于可执行文件和共享库(共享库在一定程度上也就是个可执行文件,非常类似,像动态链接器申请能自己执行)的差别。但差别不是十分的大。
静态、可执行文件的入口函数主要内容如下:
[plain] view plain copy
  1. _start:  
  2.    xorl %ebp,%ebp  
  3.    popl %esi  
  4.    movl %esp, %ecx  
  5.   
  6.    pushl %esp  
  7.    pushl %edx  
  8.    pushl $__libc_csu_fini  
  9.    pushl $__libc_csu_init  
  10.    pushl %ecx  
  11.    pushl %esi  
  12.    pushl main  
  13.    call __libc_start_main  
  14.   
  15.    hlt  
最上面那个xorl %ebp, %ebp最特殊。这个是只有启动函数才有的标识。别的函数都是保存ebp寄存器(上一栈帧开始位置)。进程开始时,进程栈的情况大致如下:
...0env n...env 0arg n...arg 0arg c
从左至右,地址依次递减,也即栈增长方向向右。最初esp,注意是esp寄存器指向argc。上面那个栈字是我自己加的,栈里没有的。看上面汇编可以看出,popl %esi是吧argc即参数个数放入了esi寄存器,然后movl %esp, %ecx是把arg0地址放入ecx,即参数开始位置。
后面一系列pushl就是在给main函数压参数,作为调用main准备。这里的libc_start_main就相当于如下函数:
__libc_start_main(main, argc, argv, __libc_csu_init, __libc_csu_fini, edx, top of stack)。第一个参数main是用户main函数地址。就是进程中的main。最后那个hlt就是打个标记,程序绝不会走到那里,走到那里就出错了。没它的话,万一出意外,程序过了call,那就不好弄了,会一直执行下去。p320

4、上面的__libc_start_main()函数,主要内容如下:
N1、初始化栈、参数和环境变量。就是把他们的各自位置提取出来(用上面传入的ecx,即指向arg0的指针)。
N2、检查操作系统版本信息,估计是为了后面执行做准备的。这个书上也没怎么说。
N3、初始化线程,注册main退出后要执行的函数,比如动态库的收尾函数rtld_fini,还有main结束的收尾函数fini。然后还要执行main之前的初始化函数。相当于:
atexit(rtld_fini);
atexit(fini);
(*init)(argc, argv, 环境参数);
上述代码仅作示意,真正代码书上有p323。
N4、执行main(),和exit()。
exit在main之后执行,里面有个循环链表,装着所有atexit注册的函数指针,在exit函数中会遍历这些指针,依次执行。最后exit会调用_exit,由汇编编写,主要调用系统调用__NR_exit,作为退出。
程序结束就两个途径,一个main正常返回,另一个主动调用exit。两个方式exit都会被调用。

5、windows下入口函数思路更清晰。如下:
N1、初始化和OS版本有关的全局变量
N2、初始化堆。
N3、初始化I/O
N4、获取命令行参数和环境变量
N5、初始化C库的一些数据
N6、调用main并记录返回值
N7、检查错误并将main的返回值返回
可以看到windows这有个专门初始化堆的过程,在初始化堆之前,如果要使用内存动态分配,会使用函数alloca。此函数原理就相当于esp寄存器直接减去要分配的值,即分配内存在栈上,只要栈空间大小允许,并且函数返回的时候会自动释放。

6、在操作系统层面上,文件操作有类似于FILE的概念,在Linux中,叫做文件描述符,在windows中叫做句柄。(后面这俩都叫做句柄)。
用户通过某个函数打开文件以获得句柄,此后用户操作文件皆通过该句柄进行。
在linux中,值为0、1、2的fd分别代表标准输入、标准输出和标准错误输出。在程序中打开文件得到的fd从3开始增长。在内核中,每一个进程都有一个私有的“打开文件表”,这个表是一个指针数组,每一个元素都指向一个内核的打开文件对象。而fd,就是这个表的下标。用户打开一个文件时,内核会在内部生成一个打开文件对象,并在这个表里找到一个空项,让这一项指向生成的打开文件对象,并返回这一项的下标作为fd。由于这个表处于内核,并且用户无法访问到,因此用户即使拥有fd,也无法得到打开文件对象的地址,只能通过系统提供的函数来操作。
在C中的FILE结构必定和fd有一对一的关系,每个FILE结构都会记录自己唯一对应的fd。
Linux中,FILE,fd打开文件表和打开文件对象的关系如下:

图中,内核指针p指向该进程的打开文件表,所以fd要加上p才能得到打开文件表中对应项的地址。stdin stdout stderr都是FILE结构的指针。
windows中,与上图大同小异,不过其句柄不是打开文件表的下标,而是经过某种线性变换后得到的。p327
MSVC CRT里入口函数的初始化过程,在p329,因为看得比较晕,也就没有太细看
7、glibc 启动文件。crt1.o里面包含的就是程序的入口函数_start,由他负责调用__libc_start_main初始化libc并且调用main函数进入真正的函数主体。最开始的时候,它并不叫crt1.o而是crt.o,后来各种原因之下,变成了crt0.o,以凸显其是链接时输入的第一个文件,后来又各种原因之下,编程了crt1.o。crt0.o和crt1.o之间的区别是crt0.o为原始的,不支持.init 和.fini的启动代码,而crt1.o是改进过后,支持.init 和 .fini 段的版本。p342
最开始的时候是没有.init .fini两个段的,后来为了满足在main()函数之前执行构造函数,main()之后执行析构函数,以及类似要求,才加入了.init .fini两个段。

8、对于函数_init() _fini(),crti.o和crtn.o两个目标文件刚好是这两函数的开始和结尾部分,当这两个文件和其他目标文件按顺序连接起来后,刚好能形成完整的_init()和_fini()函数。为了保证.init .fini段的正确性, 必须保证链接时,crti.o必须在用户目标文件和系统库之前,而crtn.o必须在用户目标文件和系统库之后。比如形成的_init函数大概如下:
_init :
pushl %ebx                                                    ----
movl  %esp, %ebp                                        ---| 这俩都是crti.o提供,可以看出,不完整,很特殊的目标文件

......
call XXXX                                                       这三个是用户的各种*.o目标文件,或者是库的目标文件,反正用来初始化的
.......

popl %eax                                                      这四个由crtn.o提供,可以看到,这为结尾,刚好组成了能够使用的_init()函数
popl %ebx
leave
ret

也即 链接器输入文件顺序一般为:
ld crt1.o crti.o [user object]  [system_libraries] crtn.o
嵌入式系统中操作系统内核编译的时候,经常会使用GCC的两个参数,-nostartfile 和 -nostdlib,分别用来取消默认的启动文件和C语言运行库。
用户自己有函数想放到.init段时,可以这样,使用GCC特性 __attribute__((section(".init")))。但是这个放进去的函数 不能有返回,看看上面那个结构就明白为什么不许返回了。

9、GCC使用crtbeginT.o和crtend.o来真正实现C++全局构造和析构的目标文件。.init和.fini就是提供一个在main之前和之后运行代码的机制,C++全局构造和析构由专门目标文件完成。
后面又开始各种MSVC CRT...讲其各种版本,什么线程安全的 不安全的 一大堆。。。 略过。。。

10、栈访问权限。栈可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果他知道其他线程的堆栈地址,然而这是很少间的情况),但实际运用中的线程也拥有自己的私有存储空间。包括:
         (尽管并非完全无法被其他线程访问,但一般情况下仍然认为是私有的数据。
线程局部存储 TLS。     线程局部存储是某些操作系统为线程单独提供的私有空间,但通常只具有很有限的容量。
寄存器   寄存器存放的数据是执行流的基本数据,因此为线程私有。
这说的其实也就是上面那个表。

11、 C/C++标准未对多线程提什么标准。运行库也没有线程操作函数。而MSVC CRT提供_beginthread() _endthread()等函数,用于创建线程和退出。而linux下,glibc也提供了一格可选的线程库pthread(POSIX Thread) ,它提供pthread_create() pthread_exit()等函数。但这些函数都不属于标准库。
C/C++标准库中,errono strtok() malloc/new free/delete 异常处理 printf/fprintf 其他IO函数 信号相关函数等 都布局有多线程安全性。p351
后连还列出了一些带有多线程安全的函数,他们只是多线程情况下本身就是安全的,并不是C/C++库专门加的多线程安全。
对于MSVC中,有多线程版本的运行库,可以比较好的解决这个问题。               p352

12、对CRT进行改进。包括使用TLS、加锁、改进函数调用方式等。包括Glibc,虽然没提供完整的多线程安全库,但是对个别函数有提供多线程安全版本的函数。p352

13、线程局部存储实现。想多线程安全,全局变量是不能用的了,寄存器又少,只好使用TLS。 TLS使用方法简单,如果要定义个全局变量为TLS类型的,只要在它定义前加上相应的关键字即可。对GCC来说,关键字即__thread:
__thread int number; 这么一来这个全局变量就是TLS的了。
对MSVC,相应关键字为__declspec(thread):
__declspec(thread) int number;                                                 p353

作者又提到了windwos下TLS是怎么实现的,偷偷略过LINUX的,也不解释一下。。p354

前面使用__thread 或 __declspec(thread)关键字定义全局变量为TLS变量的方法一般被称为隐式TLS,还有个 显示TLS,但 不推荐使用。这里又是讲windows怎么实现显示TLS,linux上完全不提,就说了用哪个函数。以后有兴趣看看吧。    p355

CreateThread() 和_beginthread()有什么不同。作者写了很多,似乎很有用的样子,但我没细看,至少现在写记录时没细看。 p356

14、比较详细讲 C++的构造和析构过程。Glibc和MSVC都讲了一下。p357 ~ p368

15、缓冲。C支持两种缓冲,即行缓冲(Line Buffer) 和全缓冲(Full Buffer) 。全缓冲是经典的缓冲形式,除了用户手动调用fflush外,仅当缓冲满的时候,缓冲才会被自动flush掉。而行缓冲则比较特殊,这种缓冲 仅用于文本文件,在输入输出遇到一个换行符时,缓冲就会被自动flush,因此叫行缓冲。

int flush(FILE *stream)                       flush指定文件的缓冲,如果参数为NULL,则flush所有打开文件的缓冲。
int setvbuf(FILE *stream, char *buf, int mode, size_t size)             设置文件的缓冲。缓冲类型有:
_IONBF             无缓冲模式
_IOLBF              行缓冲模式      
_IOFBF              全缓冲模式

void setbuf(FILE *stram, char *buf)          设置文件缓冲为全缓冲模式,等价于(void) setvbuf(stream, buf, _IOFBF, BUFSIZE)。注意开头为 void。

16、fread详细调用过程。fread--->fread_s(增加溢出保护和锁)--->_fread_nolock_s(循环读取、缓冲)---->_read(换行符转换)---->ReadFile(系统调用API)
只讲了windows的路线,linux没提。讲的似乎蛮详细,没看过windows具体代码,也无法判断。

第十二章 系统调用与API
这章内容稍微熟一些,作者也仅仅大致介绍,windows linux都有说明。还可以。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值