1. 「———————————————————— |大地址0xffffffff
|内核虚拟存储器 |
|————————————————————|←0xc0000000
|用户栈(运行时创建) |
|————————————————————|←esp
| ↓栈向下 ↑库向上 |
|————————————————————|
|共享库的存储器映射区域 |
|————————————————————|←0x40000000
| ↑堆向上增张 |
|————————————————————|←brk
|运行时堆(在运行时由malloc创建的) |
|————————————————————|←4K对齐
| 读写数据(.data .bss) |
|————————————————————|←4K对齐
|只读的代码和数据(.init .text .rsdata)|
|————————————————————|←0x08048000
|未用 |
|————————————————————↓小地址0x00000000
2. 由于表示的精度有限,浮点运算是不可结合的。例如在大多数机器上,C表达式(3.14+1e20)-1e20求得的值是0,而3.14+(1e20-1e20)求得的值是3.14。
3. 0x12345678, 小端机低地址在低位:78,56,34,12; 大端机高地址在低位:12,34,56,78。
4. 除以2的幂通常用右移,对于无符号和二进指补码数,分别用逻辑移(补0)和算术移(补符号位),对于负的有符号二进制补码数,直接移有误差,要在移位之前偏置(biasing)下,利用 对于整数x和大于0的y, 「x/y = (x + y - 1)/y」这个性质,对于x<0,x>>K就要让x在移位前先加上2的K次方减1。
5. IEEE浮点标准用V=(-1)^s * M * 2^E的形式来表示一个数。s决定符号,s=1表示负数,s=0表示正数。有效数M是一个二进制小数,范围在1~2-ε之间,或者0~1-ε之间。指数E是2的冥(可以为负数)。C中的float的s占1位,exp占8位,frac占23位,double中s占1位,exp占11位,frac占52位。根据exp的值分成3种情况:规格化值(exp既不全是0,也不全是1,E=e-Bias,float中Bias为127,double中Bias为1023,M=1+f)、非规格化值(exp全为0,E=1-Bias,M=f),特殊数值(exp全为1,frac全为0时,表无穷,否则表NaN)。
6. 从float和的double向int转换时,值会向0截断,对于溢出的值,C标准没有固定的结果,但在大部分机器上,结果将是TMAX或TMIN。
7. leal指令是取源操作数的地址,目的操作数必须是寄存器。
8. [x(n-1), x(n-2), ... ... , x(0)]的运算数,移位量是一个0~n-1的值。移位操作用单个字节编码,只能移0到31位,移位量可以是一个立即数或者放在cl中。
9. jmp指令无条件跳转指令,可以是直接跳转:跳转目标作为指令一部分编码(一般用标号),也可以是间接跳转:跳转目标是从寄存器或存储器位置中读出(* 加上操作数指示符),而其他的条件跳转指令只能是直接跳转。
10. unsigned xi = x -100; if (xi > 6) { //xi不在100~106之间} 可以判断出xi是否在100~106之间,因为x-100的负值会饶会成非常大的无符号数。
11. call next
next: popl %eax
将当前popl指令的的地址放到eax里。
12. 调用者保存栈:eax,edx,ecx;被调用者保存栈:ebx,esi,edi
13. 可执行可链接格式(ELF)文件:
ELF 头 :ELF头大小,目标文件类型,机器类心,节头部表的偏移及表目大小和数量
.text :已编译程序的机器代码
.rodata :只读数据
.data :已初始化全局变量
.bss :未初始化全局变量
.symtab :符号表:定义和引用的函数和全局变量的信息。(包括static型局部变量)
.rel.text :代码要重定义的部分(调用外部函数或者引用全局变量)
.rel.data :重定位数据(值为全局变量或外部定义的函数的地址的已初始化的全局变量)
.debug :调试符号表
.line :源程序中行号和.text节机器指令之间的映射
.strtab :字符串表,包括.symtab和.debug节的符号表以及节头部中的节的名字
节头部表 :描述各个节的信息
14. 静态局部变量/全局变量/静态全局变量,未初始化或初始化为0,放在.bss段;初始化为其他则在.data段。
15. 三种不同的符号:全局(非静态函数,非静态全局变量),外部(外部定义的函数和变量),本地(带static的函数和全局变量,静态局部变量在符号表创建一个有唯一名字的本地链接符号)。
16. 函数(非本地)和已初始化的全局变量(非本地)是强符号,未初始化的全局变量(非本地的)是弱符号:
不允许有多个强符号。
如果有一个强符号和N个弱符号,选强符号。
如果全是弱符号,任意选一个。
17. 在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的相同顺序来扫描可重定位目标文件和存档文件。如果定义一个符号的库出现在引用这个符号的库的目标文件之前,那么引用就不能被解析,链接会失败。所以,关于库的一般准则是将它们放在命令行的末尾,并且如果库是不相互独立的话,必须将它们排序。比如,假设foo.c调用libx.a中的函数,该库又调用liby.a中的函数,而liby.a又调用了libx.a中的函数,那么libx.a必须在命令行上重复出现:gcc foo.c libx.a liby.a libx.a。
18. 编译器和汇编器生成从地址零开始代码和数据节。所以需要需要重定位text节中调用外部函数或引用全局变量的指令,如果已初始化全局变量的初始值为全局变量或外部定义函数的地址的话,也需要重定位。对于R_386_PC32重定位类型,它是重定位一个使用32位PC相关的地址引用,一般都是CALL指令,编译时认为要跳转的地址和当前地址都是0,重定位时将要得到的跳转地址减去当前地址再加上原来的值。对于R_386_32重定位类型,它是重定位一个使用32位绝对地址的应用,编译时认为引用的符号地址是0,重定位时将得到的符号地址加上原来的值即可。
19. 加载器在可执行文件中段头表的指导下,将可执行文件的的相关内容拷贝到代码和数据段后,跳转到程序的入口点,也就是符号 _start的地址。在_start地址处的启动代码是在目标文件ctrl.o中定义的,对所有的C程序都是一样的。
0x080480c <_start>: /* entry point in .text */
call _libc_init_first /* startup code int .text */
call _init /* startupcode in .init */
call atexit /* startupcode in .text */ 附加了一系列在调用exit函数时应调用的程序,exit函数运行aexit注册的函数,然后调用_exit将控制返回操作系统。
call main /* application main routine */
call _exit /* return control to OS */
20. RTLD_NOW和RTLD_LAZY都可以决定链接器对SO中的外部符号的引用进行解析的时机,NOW表示在dlopen后立即解析,而LAZY表示执行到SO中的代码时才解析。
21. PIC(位置无关代码)依赖于:数据段总是分配为紧随在代码段后面,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量。编译器在数据段开始地方创建了一个全局偏移量表(GOT),每个目标模块都有一张自己的GOT。X86中用ebx来存放GOT基址,当访问全局变量时,先通过类似call pop的方法将EIP的值置入ebx中,ebx在根据自己当前在代码段的位置及代码段和数据段的常量偏移来将自己的值修正为GOT基址,这样就能根据在GOT中的偏移GOT来访问全局变量。而使用函数时,可以利用GOT和PLT实现延迟绑定,在GOT中存放PLT中相应函数的入口。
22.不使用PIC表示生成重定位代码,须为每个库都在物理内存中复制一份副本,因为需要修改其中的地址,所以不能达到真正代码段共享的目的。而x86上GCC的-shared似乎应该是包含-fPIC选项的(ubuntu是),但似乎不是所以系统都支持,所以最好显式加上fPIC选项。
23. 重定位时还要注意只读数据段,如printf语句中的格式串和switch语句的跳转表。
24. libc.a和libm.a中包含多少目标文件?
ar t /usr/lib/libc.a | wc -l 1426
ar t /usr/lib/libm.a | wc -l 401
25. setenv/unsetenv只修改本进程及子进程的环境变量,overwrite为0时,不修改已经存在的环境变量,只新增不存在的。
26. UNIX信号处理程序典型地会阻塞当前处理程序正在处理的类型的待处理信号,当然,在处理程序结束后要恢复。
27. 用sigaction函数会将action.sa_mask中的信号集添加blocks中,当然也只是在信号处理函数期间有效,之后要恢复。
28. sigsetjmp、siglongjmp和setjmp、longjmp类似,siglongjmp一般在信号处理程序里面。
29. <signal.h>中的psignal和strsignal函数与perror、strerror类似。
30. 缓冲区溢出见<Experiment>。
31. 处理器流水线:取指、解码(操作数)、执行、访存(读写主存)、写回(寄存器)、更新(PC)。
32. IA32下,pushl %esp相当于将%esp原来的值压入栈中,而popl %esp相当于将栈指针设置为从栈中读出的值。
33. 寄存器访问数据,0个周期,高速缓存要1(L1)~10(L2)个周期,主存访问要50~100个周期,而磁盘则是20 000 000个周期。
34. 逻辑扇区编号方法:逻辑扇区号0对应于物理扇区的0面0道1号。
35. 存储单元映射到高速缓存中的哪个组和缓存的容量大小有关。
36. 全相联高速缓存只适合作小的高速缓存,例如虚拟存储器系统中的翻译备用缓冲器(TLB),当然TLB很多也是用组相联高速缓存。
37. TLB是一个小的、虚拟寻址的缓存,每一行都保存着一个单个PTE组成的块。而高速缓存(L1、L2)是物理地址缓存,所以两者还是有区别。在4K页面大小的奔腾处理器上,TLB和L1、L2都是4路组相联的,所以32位的虚拟地址,前20位用来在TLB中查找页面的物理地址,而后12位则在高速缓存中查找对应的组及相应的数据字,因为L1每块32字节(5位),128个组(7位),加起来正好使用了12位。
38. 分配器设计
1 隐式空闲链表
1.1 头部+载荷
1.2 头部+载荷+脚部
2 显式空闲链表
2.1 双向链表 所有空闲块链一起
2.2 分离存储
2.2.1 简单分离存储 每个大小类的空闲链表中的块大小相等 不切分合并空闲块
2.2.2 分离适配 每个大小类的空闲链表中的块大小不等
2.2.2.1 GNU malloc 切分合并空闲块
2.2.2.2 伙伴系统 每个大小类都是2的幂
39. 只在空闲块中使用头部和脚部,分配块使用头部的方法是要在头部中添加标志位来表示前一个块的分配状态。
40. 保守的垃圾收集器:每个可达区域都被正确地标记可达了,但是对于一些不确定的区域无法判断准确时,只有保守地认为它们也是可达,这样就会使得某些不可达区域也被错误地标记为可达。C/C++中由于没有记录类型信息,无法判断一个数据到底是不是指针类型的,只能使用保守的垃圾收集器。
41.异常分成四类:
a 中断 -来自I/O设备的信号 -异步 -总是返回到下一条指令
b 陷阱 -有意异常(系统调用) -同步 -总是返回到下一条指令
c 故障 -潜在可恢复的错误 -同步 -可能返回到当前指令
d 终止 -不可恢复的错误 -同步 -不会返回
异步异常是由处理器外部I/O设备中的事件产生的,同步异常是执行一条指令的直接产物。
42. 进程的上下文包括:通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构(包括页表、进程表、文件表等);
43. 线程的上下文包括:通用目的寄存器、(如果使用浮点,应该还有浮点寄存器)程序计数器、栈及栈指针、条件代码和线程ID。所以线程切换开销小。每个线程和其他线程一起共享进程上下文的剩余部分,包括整个用户的虚拟地址空间,它是由代码和只读数据、读写数据、堆及所有的共享库的代码和数据区域,也共享打开文件的集合,各自独立的线程栈的位置不一样,但是都在虚拟地址空间内,通常是被相应的线程独立访问,但是如果一个线程得到了指向其他线程栈的指针,那么它就可以读写这个栈的任何部分。寄存器从不共享,虚拟存储器总是共享的。
|内核虚拟存储器 |
|————————————————————|←0xc0000000
|用户栈(运行时创建) |
|————————————————————|←esp
| ↓栈向下 ↑库向上 |
|————————————————————|
|共享库的存储器映射区域 |
|————————————————————|←0x40000000
| ↑堆向上增张 |
|————————————————————|←brk
|运行时堆(在运行时由malloc创建的) |
|————————————————————|←4K对齐
| 读写数据(.data .bss) |
|————————————————————|←4K对齐
|只读的代码和数据(.init .text .rsdata)|
|————————————————————|←0x08048000
|未用 |
|————————————————————↓小地址0x00000000
2. 由于表示的精度有限,浮点运算是不可结合的。例如在大多数机器上,C表达式(3.14+1e20)-1e20求得的值是0,而3.14+(1e20-1e20)求得的值是3.14。
3. 0x12345678, 小端机低地址在低位:78,56,34,12; 大端机高地址在低位:12,34,56,78。
4. 除以2的幂通常用右移,对于无符号和二进指补码数,分别用逻辑移(补0)和算术移(补符号位),对于负的有符号二进制补码数,直接移有误差,要在移位之前偏置(biasing)下,利用 对于整数x和大于0的y, 「x/y = (x + y - 1)/y」这个性质,对于x<0,x>>K就要让x在移位前先加上2的K次方减1。
5. IEEE浮点标准用V=(-1)^s * M * 2^E的形式来表示一个数。s决定符号,s=1表示负数,s=0表示正数。有效数M是一个二进制小数,范围在1~2-ε之间,或者0~1-ε之间。指数E是2的冥(可以为负数)。C中的float的s占1位,exp占8位,frac占23位,double中s占1位,exp占11位,frac占52位。根据exp的值分成3种情况:规格化值(exp既不全是0,也不全是1,E=e-Bias,float中Bias为127,double中Bias为1023,M=1+f)、非规格化值(exp全为0,E=1-Bias,M=f),特殊数值(exp全为1,frac全为0时,表无穷,否则表NaN)。
6. 从float和的double向int转换时,值会向0截断,对于溢出的值,C标准没有固定的结果,但在大部分机器上,结果将是TMAX或TMIN。
7. leal指令是取源操作数的地址,目的操作数必须是寄存器。
8. [x(n-1), x(n-2), ... ... , x(0)]的运算数,移位量是一个0~n-1的值。移位操作用单个字节编码,只能移0到31位,移位量可以是一个立即数或者放在cl中。
9. jmp指令无条件跳转指令,可以是直接跳转:跳转目标作为指令一部分编码(一般用标号),也可以是间接跳转:跳转目标是从寄存器或存储器位置中读出(* 加上操作数指示符),而其他的条件跳转指令只能是直接跳转。
10. unsigned xi = x -100; if (xi > 6) { //xi不在100~106之间} 可以判断出xi是否在100~106之间,因为x-100的负值会饶会成非常大的无符号数。
11. call next
next: popl %eax
将当前popl指令的的地址放到eax里。
12. 调用者保存栈:eax,edx,ecx;被调用者保存栈:ebx,esi,edi
13. 可执行可链接格式(ELF)文件:
ELF 头 :ELF头大小,目标文件类型,机器类心,节头部表的偏移及表目大小和数量
.text :已编译程序的机器代码
.rodata :只读数据
.data :已初始化全局变量
.bss :未初始化全局变量
.symtab :符号表:定义和引用的函数和全局变量的信息。(包括static型局部变量)
.rel.text :代码要重定义的部分(调用外部函数或者引用全局变量)
.rel.data :重定位数据(值为全局变量或外部定义的函数的地址的已初始化的全局变量)
.debug :调试符号表
.line :源程序中行号和.text节机器指令之间的映射
.strtab :字符串表,包括.symtab和.debug节的符号表以及节头部中的节的名字
节头部表 :描述各个节的信息
14. 静态局部变量/全局变量/静态全局变量,未初始化或初始化为0,放在.bss段;初始化为其他则在.data段。
15. 三种不同的符号:全局(非静态函数,非静态全局变量),外部(外部定义的函数和变量),本地(带static的函数和全局变量,静态局部变量在符号表创建一个有唯一名字的本地链接符号)。
16. 函数(非本地)和已初始化的全局变量(非本地)是强符号,未初始化的全局变量(非本地的)是弱符号:
不允许有多个强符号。
如果有一个强符号和N个弱符号,选强符号。
如果全是弱符号,任意选一个。
17. 在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的相同顺序来扫描可重定位目标文件和存档文件。如果定义一个符号的库出现在引用这个符号的库的目标文件之前,那么引用就不能被解析,链接会失败。所以,关于库的一般准则是将它们放在命令行的末尾,并且如果库是不相互独立的话,必须将它们排序。比如,假设foo.c调用libx.a中的函数,该库又调用liby.a中的函数,而liby.a又调用了libx.a中的函数,那么libx.a必须在命令行上重复出现:gcc foo.c libx.a liby.a libx.a。
18. 编译器和汇编器生成从地址零开始代码和数据节。所以需要需要重定位text节中调用外部函数或引用全局变量的指令,如果已初始化全局变量的初始值为全局变量或外部定义函数的地址的话,也需要重定位。对于R_386_PC32重定位类型,它是重定位一个使用32位PC相关的地址引用,一般都是CALL指令,编译时认为要跳转的地址和当前地址都是0,重定位时将要得到的跳转地址减去当前地址再加上原来的值。对于R_386_32重定位类型,它是重定位一个使用32位绝对地址的应用,编译时认为引用的符号地址是0,重定位时将得到的符号地址加上原来的值即可。
19. 加载器在可执行文件中段头表的指导下,将可执行文件的的相关内容拷贝到代码和数据段后,跳转到程序的入口点,也就是符号 _start的地址。在_start地址处的启动代码是在目标文件ctrl.o中定义的,对所有的C程序都是一样的。
0x080480c <_start>: /* entry point in .text */
call _libc_init_first /* startup code int .text */
call _init /* startupcode in .init */
call atexit /* startupcode in .text */ 附加了一系列在调用exit函数时应调用的程序,exit函数运行aexit注册的函数,然后调用_exit将控制返回操作系统。
call main /* application main routine */
call _exit /* return control to OS */
20. RTLD_NOW和RTLD_LAZY都可以决定链接器对SO中的外部符号的引用进行解析的时机,NOW表示在dlopen后立即解析,而LAZY表示执行到SO中的代码时才解析。
21. PIC(位置无关代码)依赖于:数据段总是分配为紧随在代码段后面,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量。编译器在数据段开始地方创建了一个全局偏移量表(GOT),每个目标模块都有一张自己的GOT。X86中用ebx来存放GOT基址,当访问全局变量时,先通过类似call pop的方法将EIP的值置入ebx中,ebx在根据自己当前在代码段的位置及代码段和数据段的常量偏移来将自己的值修正为GOT基址,这样就能根据在GOT中的偏移GOT来访问全局变量。而使用函数时,可以利用GOT和PLT实现延迟绑定,在GOT中存放PLT中相应函数的入口。
22.不使用PIC表示生成重定位代码,须为每个库都在物理内存中复制一份副本,因为需要修改其中的地址,所以不能达到真正代码段共享的目的。而x86上GCC的-shared似乎应该是包含-fPIC选项的(ubuntu是),但似乎不是所以系统都支持,所以最好显式加上fPIC选项。
23. 重定位时还要注意只读数据段,如printf语句中的格式串和switch语句的跳转表。
24. libc.a和libm.a中包含多少目标文件?
ar t /usr/lib/libc.a | wc -l 1426
ar t /usr/lib/libm.a | wc -l 401
25. setenv/unsetenv只修改本进程及子进程的环境变量,overwrite为0时,不修改已经存在的环境变量,只新增不存在的。
26. UNIX信号处理程序典型地会阻塞当前处理程序正在处理的类型的待处理信号,当然,在处理程序结束后要恢复。
27. 用sigaction函数会将action.sa_mask中的信号集添加blocks中,当然也只是在信号处理函数期间有效,之后要恢复。
28. sigsetjmp、siglongjmp和setjmp、longjmp类似,siglongjmp一般在信号处理程序里面。
29. <signal.h>中的psignal和strsignal函数与perror、strerror类似。
30. 缓冲区溢出见<Experiment>。
31. 处理器流水线:取指、解码(操作数)、执行、访存(读写主存)、写回(寄存器)、更新(PC)。
32. IA32下,pushl %esp相当于将%esp原来的值压入栈中,而popl %esp相当于将栈指针设置为从栈中读出的值。
33. 寄存器访问数据,0个周期,高速缓存要1(L1)~10(L2)个周期,主存访问要50~100个周期,而磁盘则是20 000 000个周期。
34. 逻辑扇区编号方法:逻辑扇区号0对应于物理扇区的0面0道1号。
35. 存储单元映射到高速缓存中的哪个组和缓存的容量大小有关。
36. 全相联高速缓存只适合作小的高速缓存,例如虚拟存储器系统中的翻译备用缓冲器(TLB),当然TLB很多也是用组相联高速缓存。
37. TLB是一个小的、虚拟寻址的缓存,每一行都保存着一个单个PTE组成的块。而高速缓存(L1、L2)是物理地址缓存,所以两者还是有区别。在4K页面大小的奔腾处理器上,TLB和L1、L2都是4路组相联的,所以32位的虚拟地址,前20位用来在TLB中查找页面的物理地址,而后12位则在高速缓存中查找对应的组及相应的数据字,因为L1每块32字节(5位),128个组(7位),加起来正好使用了12位。
38. 分配器设计
1 隐式空闲链表
1.1 头部+载荷
1.2 头部+载荷+脚部
2 显式空闲链表
2.1 双向链表 所有空闲块链一起
2.2 分离存储
2.2.1 简单分离存储 每个大小类的空闲链表中的块大小相等 不切分合并空闲块
2.2.2 分离适配 每个大小类的空闲链表中的块大小不等
2.2.2.1 GNU malloc 切分合并空闲块
2.2.2.2 伙伴系统 每个大小类都是2的幂
39. 只在空闲块中使用头部和脚部,分配块使用头部的方法是要在头部中添加标志位来表示前一个块的分配状态。
40. 保守的垃圾收集器:每个可达区域都被正确地标记可达了,但是对于一些不确定的区域无法判断准确时,只有保守地认为它们也是可达,这样就会使得某些不可达区域也被错误地标记为可达。C/C++中由于没有记录类型信息,无法判断一个数据到底是不是指针类型的,只能使用保守的垃圾收集器。
41.异常分成四类:
a 中断 -来自I/O设备的信号 -异步 -总是返回到下一条指令
b 陷阱 -有意异常(系统调用) -同步 -总是返回到下一条指令
c 故障 -潜在可恢复的错误 -同步 -可能返回到当前指令
d 终止 -不可恢复的错误 -同步 -不会返回
异步异常是由处理器外部I/O设备中的事件产生的,同步异常是执行一条指令的直接产物。
42. 进程的上下文包括:通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构(包括页表、进程表、文件表等);
43. 线程的上下文包括:通用目的寄存器、(如果使用浮点,应该还有浮点寄存器)程序计数器、栈及栈指针、条件代码和线程ID。所以线程切换开销小。每个线程和其他线程一起共享进程上下文的剩余部分,包括整个用户的虚拟地址空间,它是由代码和只读数据、读写数据、堆及所有的共享库的代码和数据区域,也共享打开文件的集合,各自独立的线程栈的位置不一样,但是都在虚拟地址空间内,通常是被相应的线程独立访问,但是如果一个线程得到了指向其他线程栈的指针,那么它就可以读写这个栈的任何部分。寄存器从不共享,虚拟存储器总是共享的。