导读
本书将详细描述现在流行的Windows和Linux操作系统下各自的可执行文件、 目标文件格式; 普通C/C++程序代码如何被编译成目标文件及程序在目标文件中如何存储; 目标文件如何被链接器链接到一起, 并且形成可执行文件; 目标文件在链接时符号处理、 重定位和地址分配如何进
行; 可执行文件如何被装载并且执行; 可执行文件与进程的虚拟空间之间如何映射; 什么是动态链接, 为什么要进行动态链接; Windows和Linux如何进行动态链接及动态链接时的相关问题; 什么是堆, 什么是栈; 函数调用惯例; 运行库, Glibc和MSVC CRT的实现分析; 系统调用与API; 最后实现了一个Mini CRT。
第1章 温故而知新
内存
0、中间层是解决一切问题的法宝
1、硬件厂商负责某操作系统下的驱动;
2、分时让cpu的利用率高,虚拟地址让内存可以多进程使用
3、分段(建立虚拟空间和物理内存的映射关系),物理地址的大小是由地址线的根数决定的。缺页中断
4、分页:
一般一个系统的页面大小是固定的,有的4KB,有的4MB;
出于资金原因,地址线可寻址虚拟空间是大的,但实际物理空间只有很少,平常用不到的代码和数据就保存到磁盘里; ---涉及虚拟页、物理页、磁盘页
缺页中断时操作系统接管进程,然后把磁盘页与内存页建立映射关系
线程
1、线程的优先级改变一般有三种方式:
用户指定优先级;
根据进入等待状态的频繁程度提升或降低优先级;
长时间得不到执行而被提升优先级
2、写时复制, 指的是两个任务可以同时自由地读取内存, 但任意一个任务试图对内存进行修改时, 内存就会复制一份提供给修改方单独使用, 以免影响到其他的任务使用。
3、原子操作确实是防止静态访问的利器,但只能用于简单的场景当前
4、寄存器缓存和指令重排,导致加锁也不一定绝对安全。volatile可以解决第一种场景,内存屏障可以解决第二个问题。
5、用户线程和内核线程:一对一、多对一、多对多
第2章 编译和链接
2.1 被隐藏了的过程
1、构建过程为预处理(Prepressing) 、编译(Compilation) 、 汇编(Assembly) 和链接(Linking)
2、所以当我们无法判断宏定义是否正确或头文件包含是否正确时, 可以查看预编译后的文件来确定问题。
3、编译过程就是把预处理完的文件进行一系列词法分析、 语法分析、 语义分析及优化后生产相应的汇编代码文件。
4、为啥汇编后不直接输出可执行文件,还要有链接呢?下文解释。
2.2 编译器做了什么
1、词法分析将源代码的字符序列分割成一系列的记号,记号一般可以分为如下几类: 关键字、 标识符、 字面量(包含数字、 字符串等) 和特殊符号(如加号、 等号)。lex-词法规则
2、语法分析将对由扫描器产生的记号进行语法分析, 从而产生语法树(Syntax Tree)。yacc-语法规则
3、静态语义通常包括声明和类型的匹配, 类型的转换。经过语义分析阶段以后, 整个语法树的表达式都被标识了类型, 如果有些类型需要做隐式转换, 语义分析程序会在语法树中插入相应的转换节点。
4、中间代码优化进一步优化语法树结构,生成中间代码,已经非常接近最终代码了。
5、目标代码生成与优化最后生成机器语言并做一些指令优化
6、index不知去哪取值,所以需要链接:事实上, 定义其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候才能确定。 所以现代的编译器可以将一个源代码文件编译成一个未链接的目标文件, 然后由链接器最终将这些目标文件链接起来形成可执行文件。
7、
2.3 链接器年龄比编译器长
1、最常见的属于静态语言的C/C++模块之间通信有两种方式, 一种是模块间的函数调用, 另外一种是模块间的变量访问。 函数访问须知道目标函数的地址, 变量访问也须知道目标变量的地
址, 所以这两种方式都可以归结为一种方式, 那就是模块间符号的引用。
2、模块间依靠符号来通信类似于拼图版, 定义符号的模块多出一块区域, 引用该符号的模块刚好少了那一块区域, 两者一拼接刚好完美组合(见图2-7) 。 这个模块的拼接过程就是本书的一个主题: 链接(Linking) 。
2.4 模块拼装——静态链接
1、从原理上来讲, 链接的工作无非就是把一些指令对其他符号地址的引用加以修正。 链接过程主要包括了地址和空间分配(Address and Storage Allocation) 、 符号决议(Symbol Resolution) 和重定位(Relocation) 等这些步骤。
2、库其实也是目标文件,所以才能和.o一起打包成可执行文件。
3、如果b文件依赖A文件的变量,比如movl $0x2a, var,那么编译时首先看到的是
链接后把地址修正为正常地址。
第3章 目标文件里有什么(.o/.a./.so)
3.1 目标文件的格式
1、Windows下为PE,Linux下为ELF,都源于coff。目标文件与可执行文件的格式其实几乎是一样的
2、静态链接库当成很多目标文件捆绑在一起构成文件包。
3、elf文件分类:可重定位文件(.o)、静态链接库(.a)、可执行文件、共享库(.so)、核心转储文件(core dump)。可用file命令查看。
3.2 目标文件是什么样的
1、文件头描述了整个文件的文件属性, 包括文件是否可执行、 是静态链接还是动态链接及入口地址(如果是可执行文件) 、 目标硬件、 目标操作系统等信息, 文件头还包括一个段表(Section Table) , 段表其实是一个描述文件中各个段的数组。
2、.text: 执行语句编译为机器代码; .data: 已初始化的全局变量和局部静态变量; .bss:未初始化的全局变量和局部静态变量;.rodata: 存放的是只读数据和字符串常量;
3.3 挖掘SIMPLESECTION.O(objdump)
1、不了解ELF文件的结构细节就像学习了TCP/IP网络没有了解IP包头的结构一样。
2、readelf和objdump都可以用来分析;
3、size命令也可以看各段长度
4、 objdump -s -d 目标文件 看详细信息,还是很震撼的。“-s”参数可以将所有段的内容以十六进制的方式打印出来 , “-d ”参数可以将所有包含指令的段反汇编。
5、存bss中就当肯定初始化为0:
static int x1 = 0; // bss段
static int x2 = 1; // data段
6、gcc提供机制可以指定变量所在的段;__attribute__((section("FOO"))) int global = 42;
3.4 ELF 文件结构描述(readelf)
1、ELF的文件头中定义了ELF魔数、 文件机器字节长度、 数据存储方式、 版本、 运行平台、 ABI版本、 ELF重定位类型、 硬件平台、 硬件平台版本、 入口地址、 程序头入口和长度、 段表
的位置和长度及段的数量等。
2、操作系统在加载可执行文件的时候会确认魔数是否正确, 如果不正确会拒绝加载。
3、段表是ELF文件中除了文件头以外最重要的结构, 它描述了ELF的各个段的信息, 比如每个段的段名、 段的长度、 在文件中的偏移、 读写权限及段的其他属性。
4、段表居然是溶于段里,re.text是最后一段
5、段的类型SHT和段的标志位SHF
6、重定位表.rel.text,它的类型(sh_type ) 为“ SHT_REL ”,就是对text的重定位表。
7、字符串表常见的段名为“.strtab”或“.shstrtab”。 这两个字符串表分别为字符串表(String Table) 和段表字符串表(Section Header String Table) 。字符串表用来保存普通的字符串, 比如符号的名字; 段表字符串表用来保存段表中用到的字符串, 最常见的就是段名。
8、段表字符串表“.shstrtab”的下标存放在文件头的“ e_shstrndx ”成员中。
3.5 链接的接口——符号
1、链接过程的本质就是要把多个不同的目标文件之间相互“粘”到一起
2、在链接中, 我们将函数和变量统称为符号(Symbol) , 函数名或变量名就是符号名(Symbol Name)。定义符号和引用符号。
3、符号管理--每个目标文件都有符号表,每个符号都有一个对应的值,叫符号值,代表符号的地址。
4、本文件定义的全局符号(其他人可用)、引用的全局符号是要关注的。
5、符号表查看可以用readelf、 objdump、 nm。
6、符号表“.symtab”:
st_info表示符号类型和绑定信息,该成员低4位表示符号的类型( Symbol
Type ) , 高28位表示符号绑定信息( Symbol Binding )
符号所在段(st_shndx)描述符号所在段 位于段表的下标,如果符号不是定义在本目标文件中或有些特殊符号,sh_shndx的值有些特殊
符号值( st_value )是函数或变量的地址,即符号位于由st_shndx指定的段, 偏移st_value的位置。
7、特殊符号是指没有在程序中定义,但是可以直接引用,是ld链接器产生的。
8、符号修饰就是为了减少冲突的。
9、extern "C" 用于不让C++编译器使用自己的命名修饰规则。
10、编译器默认函数和初始化了的全局变量为强符号, 未初始化的全局变量为弱符号。--那其他呢?
11、强弱符号都是针对定义来说的,不是针对符号的引用。比如函数可以声明时指定弱符号,没定义时就出事了;变量就是普通定义时指定
规则1: 不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号) ; 如果有多个强符号定义, 则链接器报符号重复定义错误。
规则2: 如果一个符号在某个目标文件中是强符号, 在其他文件中都是弱符号, 那么选择强符号。
规则3: 如果一个符号在所有目标文件中都是弱符号, 那么选择其中占用空间最大的一个。
12、弱引用允许没有找到此处符号。
__attribute__ ((weakref)) void foo();
int main()
{
if(foo) foo();
}
3.6 调试信息
1、gcc -g参数就可以输出调试信息。发布时一般要去掉的。
第4章 静态链接(组合目标文件,包括生成可执行文件或静态库)
0、两步链接:第一步就是统计符号定义和符号引用的信息,第二步是符号解析与重定位
动态链接是l,静态不能用吗??
4.1 空间与地址分配
1、text和data段在文件和虚拟空间都要分配,bss只需要在虚拟空间分配。
3、空间与地址分配主要采用相似段合并方法,最后得到:每段虚拟地址起始地址和符号的段内偏移
4.2 符号解析与重定位(编译后的链接时重定位!核心!)
1、编译器(包括汇编)把外部引用 使用假地址替代,后面连接器再替换成真实的地址。
2、重定位表保存与重定位相关的信息。text的重定位表就是rel.text,data的就是rel.data。
3、指令修正方式 算法 得到引用的真正地址,然后填到指令里。
4.3 COMMON 块
1、主要就是用来决定最后弱符号多大字节的问题
2、链接器:多文件不能同名强符号,但可以同名弱符号或者 一强一弱。
4.4 C++相关问题
1、链接器负责消除不同编译单元的重复代码问题
2、C++的全局对象的构造函数在main之前被执行, C++全局对象的析构函数在main之后被执行。
--.init存放main函数调用前执行的代码,.fini存放main函数退出时执行的代码。
3、一般俩不同编译器是无法链接的,涉及ABI(Application Binary Interface),影响ABI的因素非常多, 硬件、 编程语言、 编译器、 链接器、 操作系统等都会影响ABI。
--即使一个编译器不同版本也可能出问题,要重视!!!!
4.5 静态库链接
1、静态库可以简单地看成一组目标文件的集合, 即很多目标文件经过压缩打包后形成的一个文件。
2、链接器会自动识别用户程序需要链接.a中的哪些.o
4.6 链接过程控制
1、一般用链接脚本控制链接过程
2、链接脚本由一系列语句组成, 语句分两种, 一种是命令语句, 另外一种是赋值语句。
使用ld -verbose查看。使用ld -T link script指定自己的链接脚本
4.7 BFD 库
0、初衷就是统一目标文件格式。
1、BFD把目标文件抽象成一个统一的模型,GCC(更具体地讲是GNU 汇编器GAS, GNU Assembler) 、 链接器ld、 调试器GDB及binutils的其他工具都通过BFD库来处理目标文件,
而不是直接操作目标文件。
第5章 Windows PE/COFF
5.1 WINDOWS 的二进制文件格式PE/COFF
1、跟elf一样都是coff(Common Object File Format)格式发展来的。
2、windows上目标文件还是coff格式,32位cpu 可执行文件是pe格式,64位cpu 可执行文件是PE32+模式。
3、gcc是“__ attribute__((section(“name”)))”把变量或函数放到自定义段,而visual c++是使用#pragma:
#pragma data_seg("FOO")
int global = 1;
#pragma data_seg(".data")
5.2 PE 的前身——COFF
5.3 链接指示信息
5.4 调试信息
5.5 大家都有符号表
5.6 WINDOWS 下的ELF——PE
第6章 可执行文件的装载与进程
6.1 进程虚拟地址空间
1、32位cpu 虚拟空间只有4g,但还是通过PAE方式可以访问到大于4g的物理内存。Linux下就是mmap。
6.2 装载的方式
1、程序执行时指令和数据必须从硬盘装入内存才能正常运行:
1)全部转入 ---静态装入;
2)按需加载 ---动态转入。 方式有覆盖装入和页映射。
2、覆盖转入是程序员自己控制,搞个覆盖管理器。
3、页映射:
1)内存分页;
2)程序分页;
3)用到谁时就加载进内存,当不够时就涉及把谁覆盖掉的问题
6.3 从操作系统角度看可执行文件的装载(重点!!!)
1、页映射装入新页后就需要地址重定位,用到mmu技术。
MMU介绍
2、进程的建立具体过程:
1)创建虚拟地址空间,实际上只是分配一个页目录表(先不页映射,发生缺页自然会)。----建立虚拟空间和物理内存的映射关系
2)读取可执行文件头,建立虚拟空间和可执行文件的映射关系。通过VMA保存。---建立虚拟空间和可执行文件的映射关系
3)设置寄存器把控制权交给进程
3、缺页中断
1)假设程序入口地址是0x080408000,从这个位置开始执行,发现是个空页面,就把控制权还给操作系统
2)通过查找VMA找到了页面在可执行文件的位置,分配一个物理页面,进行该虚拟页和物理页的映射关系。
3)控制权还给进程,从刚才页错误的位置接着执行
6.4 进程虚存空间分布
1、通过segment技术把相同权限属性的段分配在同一空间。比如代码段(可读可执行)、数据段(可读可写)。每个segment映射到一个VMA。通过程序表头来保存Segment的信息:
typedef struct {
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz; // segment在elf文件中所占空间长度
Elf32_Word p_memsz; //segment在进程虚拟地址空间所占空间长度
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
2、堆和栈也是VMA组织的:
进程虚拟地址空间如下:
6.5 LINUX 内核装载ELF 过程简介
6.6 WINDOWS PE 的装载
1、装载过程:
1)fork一个子进程并execve()调用指定的elf文件;
2)读取前128个字节,可以判断文件的格式和类型;
3)根据文件类型搜索和匹配合适的可执行文件装载过程;
第7章 动态链接
7.1 为什么要动态链接
1、静态链接在多进程下每个程序内部都保留着printf()/scanf()/sterlen()等共用库函数:
2、静态链接下一个.o小改,就要重新链接
3、动态链接是运行时再链接,节省内存和不用重新链接;插件也是动态链接
4、程序主模块和动态链接库的链接工作是动态链接其完成的,而不是前面所说的静态链接器ld。
动态链接就是链接过程从程序装载前(指编译、链接)推迟到了装载时候,程序启动变慢,通过延迟绑定解决。
7.2 简单的动态链接例子
1、链接过程:
2、动态链接下 可执行程序和所依赖的共享对象都是模块。
3、涉及符号地址重定位,符号可能是静态符号,也可能是动态符号。静态符号直接链接,动态链接则后面进行符号重定位。如何知道呢?通过解析lib.so中的符号来判断,
4、静态链接只有一个可执行文件需要映射,动态链接则还有依赖的共享文件。
7.3 地址无关代码(涉及装载时重定位!重点!指so的排布方式)
1、主要解决多个共享库的地址划分问题,不能冲突;
2、一种想法:链接时对绝对地址先不重定位,推迟到装载时完成,装载时目标地址确定,然后对主程序的各种绝对地址进行重定位。---进程共享虚拟地址空间的地址。多进程下共享库只有一份,地址也只有一份,所以用不了多进程
3、真正的办法:把指令中那些需要被修改的部分分离出来(地址相关代码), 跟数据部分放在一起, 这样指令部分就可以保持不变, 而数据部分可以在每个进程中拥有一个副本。 这种方案就是目前被称为地址无关代码(留下来的)(PIC,Position-independent Code) 的技术。--fPIC产生地址无关代码
4、共享对象模块中的地址引用方式:
第一种是模块内部的函数调用、 跳转等。--利用相对地址偏移,地址无关的
第二种是模块内部的数据访问, 比如模块中定义的全局变量、 静态变量。--也是相对地址
第三种是模块外部的函数调用、 跳转等。--先找到GOT,然后找GOT中记录的相对偏移
第四种是模块外部的数据访问, 比如其他模块中定义的全局变量。--也是依托got
5、同一模块但不同文件的数据访问,用的也是GOT方式
extern int global;
int foo()
{
global = 1;
}
6、数据段本来就是在每个进程都有副本,不存在地址无关性问题,所以可以用装载时重定位解决。
7.4 延迟绑定(PLT,指函数用到时再链接,加载时机变化)
1、动态链接执行效率较慢:
1)进行GOT定位和间接寻址;
2)程序执行时完成链接工作(符号查找地址重定位)
2、解决办法:函数第一次被用到时才进行绑定(符号查找、 重定位等) , 如果没有用到则不进行绑定。
gcc可以编译生成动态链接库(shared library),这些库可以在运行时加载或者延时绑定。在Linux系统中,动态链接库默认使用延时绑定方式。
3、PLT实现:略
7.5 动态链接相关结构
1、静态链接在操作系统装载完可执行程序后就把控制权交给可执行程序;
动态链接则这时候启动一个动态链接器,控制权给动态链接器,工作做完后交给可执行程序
2、可执行程序的.interp段存储了动态链接器路径
3、.dynamic段就是文件头,保存了许多动态链接器需要的基本信息,比如依赖哪些共享对象。
4、使用ldd命令可以看当前可执行程序或共享库依赖哪些共享库
5、导入函数是对其他目标文件中函数的引用,而导出函数是在本目标文件定义的函数。
6、动态链接符号表中的.dynsym是保存动态链接相关符号,而.symtab保存所有符号。
7、动态链接重定位表。
静态链接是链接时重定位,动态链接是装载时重定位(数据中的可修改部分,代码中可修改部分),延时绑定(地址无关代码、地址无关数据)
静态链接:.rel.text代表代码段的重定位表,.rel.data数据段的重定位表;
动态链接:.rel.dyn(数据引用的修正)和.rel.plt(函数引用的修正)
7.6 动态链接的步骤和实现
1、动态链接器自己链接自己,属于自举;
2、可执行文件的符号表,连接器本身的符号表,共享对象的符号表。
先把可执行文件和连接器本身的符号表都合并到一个符号表中,叫做全局符号表;然后开始遍历依赖的所有共享对象并加入到符号表中。
3、两个so定义同一个符号问题?重点!!!!
加入符号表时,相同符号名已存在,则忽略后加入的。
4、共享对象的.init段和.finit段会被连接器自动执行,但是可执行文件.init段和.finit段不会
5、execve()系统调用被装载到进程地址空间的可执行程序:
静态链接的可执行文件的程序入口在ELF文件头的e_entry字段;动态链接可执行文件则先找到动态链接器地址(.interp段),然后控制权交给动态链接器
6、动态链接器本质也是一个共享对象,路径是/lib/ld-linux.so.2, 这实际上是个软链接, 它指向/lib/ld-x.y.z.so。还是个可执行程序,源代码位于Glibc的elf目录下。
1)动态链接本身是静态链接的,不能依赖其他共享对象。--静态为啥还叫共享库?
2)动态链接器是PIC的。不是PIC,则代码无法共享,造成内存浪费。
7.7 显式运行时链接(运行时调dlopen装载,一般都是用到时装载)
1、运行时加载,涉及dlopen/dlsym/dlerror/dlclose
2、dlopen的filename设置为0则返回的是全局符号表的句柄。第二个参数flag为RTLD_LAZY时为延时绑定,第一次函数用到时才绑定;为RTLD_NOW时则模块被加载时就完成所有函数的绑定工作。
3、dlopen 模块A,而模块A依赖模块B,则需要先手动加载模块B;
4、dlsym查找重复符号时,如果dlopen传入0,则按装载序列优先级;如果dlopen正常打开共享对象,则按依赖序列优先级。
3、dlopen引用计数+1,dlclose引用计数-1,为0时才真正卸载
第8章 Linux共享库的组织(讲怎么生成so)
8.1 共享库版本
1、二进制接口(ABI)
1)c语言的ABI 在添加一个符号或更改函数功能时均还能保持兼容性;
2)C++不要直接作为二进制接口
2、主版本号、次版本号、发布版本号
3、SO-NAME是只带主版本号的名字,linux通过建立以SO-NAME为名字的软链接指向实际共享库。
ldconfig作用是更新所有软连接指向最新版本的共享库。
4、gcc的-l参数可以通过-lxxx链接libxxx.so.2.6.1的共享库
8.2 符号版本
1、构建时静态链接器会把应用程序依赖的所有共享库版本列表都记录到程序二进制输出文件中;运行时动态链接知道依赖的确切版本号,然后在共享库列表中查找:
1)有的是如果能找到大于或等于依赖版本号,则没事;
2)找不到一种是报warning,一种是终止。
2、给符号加上版本指符号有多个版本,比如VERS_1.1/VERS_1.2/VERS_1.3
8.3 共享库系统路径
/lib :系统最关键和基础的运行库;比如动态链接器、c语言运行库
/usr/lib :开发时用到的共享库
/usr/local/lib :第三方应用程序库,比如python解释器
8.4 共享库查找过程
1、依赖的模块路径保存在.dynamic段中,由DT_NEED类型的项表示。如果DT_NEED里保存的是绝对路径,则按照绝对路径;如果是相对路径,则从/lib、/usr/lib、/etc/ld.so.conf查找
2、ldconfig是用来共享库变更时更新共享库,并有一个/etc/ld.so/cache缓存,加快查找过程。许多安装包会自动调用ldconfig。
8.5 环境变量
1、LD_LIBRARY_PATH也可以用来指定共享库路径,正常的顺序是:
1)LD_LIBRARY_PATH指定路径;
2)/etc/ld.so/cache指定的路径;
3)先/usr/lib,再/lib
2、LD_PRELOAD优先级更高比LD_LIBRARY_PATH
3、LD_DEBUG是打开动态链接器的调试功能
8.6 共享库的创建和安装
1、共享模块反向引用主模块的符号时,可能主模块的符号还没有放入动态符号表,此时可以gcc编译时选择-export-dynamic去导出
2、strip工具清除共享库或可执行文件的所有符号和调试信息、ld -s(清除所有符号信息)/-S(清除调试符号信息)可以链接器输出文件时就不产生符号信息
3、gcc提供了__attribute__((constructor))和__attribute__((destructor))指示共享库的构造和析构函数,会在main函数执行前和退出前调用
第9章 Windows下的动态链接
9.1 DLL 简介
9.2 符号导出导入表
9.3 DLL 优化
9.4 C++与动态链接
9.5 DLL HELL
第10章 内存
10.1 程序的内存布局
1、分为内核空间(Linux为高地址的1GB)和用户空间
10.2 栈与调用惯例
1、函数执行过程中压栈使esp减小,ebp是是帧指针,固定不变
2、一般的函数入栈和出栈形式:
3、函数调用方和函数本身需要有一个调用约定,称为调用惯例:
1)函数参数的传递顺序和方式,最常见的是通过栈传递
2)栈的维护方式
3)名字修饰的策略
4、函数返回值4字节通过eax传递,8字节通过eax+edx返回,128字节呢?
通过看汇编,拷贝了两次,一次是b拷贝给tmp,一次是由tmp拷贝给n。
10.3 堆与内存管理
1、malloc一块小内存就去向操作系统申请,增加了系统调用开销,好的做法是申请一块大的,然后用户自己管理这块空间。这就引入内存池。
2、malloc底层调用brk或mmap系统调用:
1)brk是设置进程数据段的结束地址;
2)mmap是申请虚拟地址空间
3、malloc最多申请多大空间?
4、堆分配算法:
1)空闲链表
2)位图
3)对象池
发现malloc也只是glibc提供的函数,里面是系统调用,可以优化的,要是厉害可以自己实现一套管理
10.4 本章小结
第11章 运行库(glibc实现)
11.1 入口函数和程序初始化
1、main函数并不是万物之始,前面还有很多事情
2、glibc的入口函数是_start,内部调用到_libc_start_main
类似于如下代码,咋感觉少了个入参:
3、每个进程都有一个私有的打开文件表,fd就是下标
11.2 C/C++运行库
1、变长参数:va_list 和va_start
2、非局部跳转longjmp和setjmp
3、运行库是c程序和不同操作系统平台间的抽象层,能力有限比如用户权限控制和操作系统线程创建都不属于标准的c语言运行库,故有时需要直调操作系统API或使用其他的库。glibc和MSVCRT是标准c语言运行库的超级,含有线程操作函数。
4、glibc两部分:头文件和二进制部分(含有静态和动态两个部分,/lib/libc.so.6和/usr/lib/libc.a)
5、crt1.o是crt0.o改进过来的。crt0不支持(crt1也只是支持而不是含有)初始化相关的段.init和.finit。crti.o包含的是.init和.finit的开始部分,crtn.o包含的是.init和.finit的结束部分,链接后合并三个.o的段最后只含有一个.init和.finit
6、一些函数可以加上__attribute__((section(".init")))放到init段中
7、glibc不了解c++的全局构造和析构,只有gcc了解。glibc的crti.o和ctrn.o的.init和.finit只是提供一个main前后运行代码的机制,真正的全局构造和析构在其他.o
11.3 运行库与多线程
1、线程私有数据:栈、线程局部存储、寄存器
2、gcc:__thread int num; MSVC: __declspec(thread) int num;
3、windows实现:定义tls时,放到PE文件的.tls段,新起线程时,在进程的堆上申请一块内存并复制tls段中内容到这块空间中。
4、显式TLS是指程序员要手工申请TLS变量,手动释放。phread库就是pthread_key_create()、pthread_getspecific()、phread_setspecific()和pthread_key_delete()
11.4 C++全局构造与析构
1、最终调用到_CTOR_LIST_数组来完成全局构造析构的调用,第一个元素是函数的个数,剩下的都是函数指针(ctor是constructor的缩写)
2、每个编译单元(指一个目标文件)都有一个.ctors段,里面存放一个函数指针负责本编译单元内所有的全局/静态对象的构造和析构(GLOBAL_I_Hw函数)。链接每个目标目标到一起时,.ctors进行合并成一个,也就构成了函数指针数组。
3、通过__attribute__((section(".ctors")))可以添加自己的函数到该段中。
4、析构函数的调用通过GLOBAL_I_Hw中注册函数指针实现,然后坐到先注册后调用就行
11.5 FREAD 实现(IO)
1、主要讲解msvc crt的实现。
第12章 系统调用与API
12.1 系统调用介绍
1、每个系统调用对应内核源代码中的一个函数,以"sys_"开头
2、系统调用可在程序中直接使用,c语言形式定义在"/usr/include/unistd.h"。完全可以绕过glibc的fopen/fread/fclose,直接使用open(),read()和close()来操作文件 --这些是系统库函数
3、系统调用缺点:
1)使用不变,接口太简单,用封装版更好
2)不兼容,换个系统就不行了--运行库统一了接口
12.2 系统调用原理
1、键盘被按下时,键盘上的芯片发送一个信号给cpu --这就是硬件中断
2、中断有中断号和中断处理程序。--此处又用到中断向量表,中断号就是索引
3、指令INT是软件中断,比如int 0x80就是调用第0x80号中断处理程序。---软件中断
4、系统调用有系统调用号和系统调用表。系统调用都是通过0x80中断去触发的。
5、系统调用封装函数fork的调研过程:
1)__NR_fork是宏,代表fork系统调用号;
2)__syscall_return是另一个宏,用于检查系统调用的返回值,C语言会把出错信息存储到一个名为errno的全局变量。
3)切换堆栈:
把当前的ESP、SS保存到内核栈;更新ESP和SS到内核栈的相应值。
iret指令则从内核栈中弹出寄存器的值,使得栈恢复到用户态的状态。
12.3 WINDOWS API
第13章 运行库实现
13.1 C 语言运行库
1、为支持双系统,采用条件编译
2、运行库的入口函数功能:
1)准备程序运行环境及初始化运行库
2)调用main函数执行程序主体
3)清理程序运行后的各种资源
3、模拟入口函数:
1)首先定义函数原型:void mini_crt_entry(void)
2)完善三部分的内容
但这个函数写完后,系统是怎么调用起来呢?
13.2 如何使用MINI CRT
1、Mini CRT以库文件和头文件的形式提供给用户。-e mini_crt_entry用于指定入口函数