2007年4月20日星期五
《深入理解计算机系统》第三章笔记<七>
--在c中,堆(一个可以用来存放数据结构的存储器池)中的存储分配用的是库函数的malloc和calloc,它们的效果类似于c++和java中的new操作。
--c和c++都要求程序显式调用delete或free函数来释放已分配的空间。在java中,释放是由运行时系统通过一个称为garbage collection(垃圾收集器)的进程自动完成的。
20. 对齐
--linux沿用的对齐策略是2字节数据类型的地址必须是2的倍数,而较大的数据类型的地址必须是4的倍数。
--microsoft windows对对齐的要求更严格--任何k字节(基本)对象的地址都必须是k的倍数。
21. 缓冲区溢出
--存储器的越界引用和缓冲区溢出可能导致程序出现严重的错误,比如非法访问野指针指向的存储区,比如由于缓冲区溢出而修改了栈中保存的寄存器值或返回地址等信息,导致栈引用的错误。
--缓冲区溢出的一个更致命的使用就是让程序执行它本来不愿意执行的函数,这是最常见的通过网络攻击的方法。通常,输入给程序一个字符串,其中包含一些可执行代码的字节编码,称为漏洞入侵代码(exploit code),另外,还有一些字节是指向可执行代码的指针来覆盖掉返回地址。所以,执行ret指令的效果就是跳转到漏洞入侵代码。
22. 蠕虫和病毒
蠕虫和病毒都是试图在计算机中传播它们自己的代码。前者可以自己运行,并能够自我复制传播;后者则是一段代码,能够将自己添加到包括操作系统在内的其它程序中,但不能独立运行。术语“病毒”用来指各种不同的系统间传播攻击代码的策略。所以可能会把本应该称为“蠕虫”的东西叫成了“病毒”。
(第三章完。注:剩余浮点代码,c中嵌汇编代码章节未看。第四章处理器体系结构跳过)
《深入理解计算机系统》第三章笔记<六>
call Label -------- 过程调用
call *Operand ---- 过程调用
leave ------------- 为返回准备栈
ret --------------- 从过程调用中返回
ret指令从栈中弹出地址,要正确使用,就要用leave使栈准备好,leave指令相当于:
movl %ebp, %esp
popl %ebp 得到调用者的帧指针
15. 寄存器使用惯例(P为调用者,Q为被调用者)
(1) 调用者保存(caller save)寄存器:%eax, %edx, %ecx保存在P中,当P调用Q时,Q可以覆盖这些寄存器;
(2) 被调用者保存(callee save)寄存器:%ebx, %esi, %edi,Q在使用之前,先将它们压栈,返回时恢复;
(3) 必须保持寄存器%ebp和%esp;
(4) 通常使用2中的方法,它会尽量减少写和读栈的次数。
17. 为什么要避免使用整数乘法
在较老的IA32模型中,整数乘法指令要花费30个时钟周期,所以尽量避免,以移位和加法代替;但新的模型中只需要3个时钟周期,所以现在不一定进行这样的优化了。
18. 嵌套数组
创建数组的数组时,数组分配和引用的通则也是有效的。
int A[3][4];
等价于以下声明
typedef int row3_t[3];
row3_t A[4];
《深入理解计算机系统》第三章笔记<五>
(1) IA32程序用程序栈来支持过程调用,栈用来传递过程参数,存储返回信息,保存寄存器以供以后恢复之用,以及用于本地存储。
(2) 为单个过程分配的那部分栈称为栈帧(stack frame),栈帧最顶端是以两个指针定界的,
%ebp作为帧指针,%esp作为栈指针。
(3) 程序栈示意图:
设调用帧为P,当前帧为Q,由图中可知:
(1)Q的参数和返回地址是存放在P帧中的;
(2)调用参数是按从右到左顺序压至栈中,最后是返回地址;
(3)一般来说,栈顶指针是可变的,所以一般用帧指针%ebp用作大多数信息访问的基址指针。
以此原理举二个程序实例:
(1)如下语句,判断输出
int i[2] = {1, 2};
int *j = &i[0];
printf("%d %d\n", ++(*j), *(++j));
分析:
直观的判断,一般都会认为输出应该是两个2;
然而运行结果可能会让你琢磨不透,在IA32中,一般来说输出结果应该是3和3。
我们来分析一下原因,按照栈帧结构来看,压栈是先从右边的参数开始,所以先计算*(++j),由于i自增,此时j已经指向i中的第二个元素,作为地址压至栈中,再计算++(*j)时,则将地址内容自增,即2+1=3,所以输出为3,而由于之前压进栈中的是地址,它指向的内容已经变成了3,所以第二个输出也是3。
(2) 给定函数int p(int x, int y), 要求一句话输出x(x+1)...(y-1)y(y-1)...(x+1)x,不允许使用for和if语句。(注:此题我不知道正确答案,但可作如下分析)
分析:既然不能用for,那就只好用递归,然后再以?:语句形式作为判断语句,函数如下
int p(int x, int y)
{
printf("%d", x < y? p(x+1, p(y, x))- 1 : y);
return x;
}
重点是p(x+1, p(y, x))这句,举例x=1,y=3来看
第一遍:p(1+1, p(3, 1)) -> p(3, 1)输出1返回3 -> p(2, 3);
第二遍:p(2+1, p(3, 2)) -> p(3, 2)输出2返回3 -> p(3, 3);
第三遍:因为x==y,所以直接输出y = 3返回3;
此时递归已经中止,但递归栈中还有之前保留的p(2, 3)和p(3, 3),由于return x;所以先返回3,再返回2,将结果减去1就是2和1,所以最后输出为12321。
不知道正确答案是什么,我多用了条return语句,而且使用?:也不知道符不符合题目要求,但是用这个题来作为栈帧结构的例子是合适的。
《深入理解计算机系统》第三章笔记<四>
条件码寄存器描述了最近的算术或逻辑操作的属性,以下是最有用的条件码:
CF: 进位标志
ZF: 零标志
SF: 符号标志
OF: 溢出标志
leal指令不改变任何条件码
10. 循环
(1) c提供好几种循环形式:while, for和do-while;
汇编中将条件测试和跳转指令组合起来实现循环的效果。
一般来说,其他的循环会首先转换成do-while形式,然后编译成机器代码。
(2) while循环写成goto语句形式时,内循环中有两条goto控制语句,而转化成do-while后,只有一条。
(3) for循环可以把初始化条件抽出来,循环条件放到循环体内,就可以转化为while形式,然后在转到do-while形式。
11. switch语句
switch语句通过使用跳转表的数据结构使得实现更加高效。跳转表是一个数组,表项i是一段代码的地址。和if-else相比,跳转表的优点是执行开关语句的时间与开关情况的数量无关。
12. 过程
一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部分;另外,它还必须在进入时为过程的局部变量分配空间,并在退出时释放。
将在下一篇文章中描述过程的栈帧结构。
《深入理解计算机系统》第三章笔记<三>
三种类型操作数
(1) 立即数(常数) 书写方式:$常数
(2)寄存器 表示某个寄存器的内容:Ea表示任意寄存器,R[Ea]表示其值
(3)存储器引用 根据有效地址访问某个存储器位置,用符号Mb[Addr]表示从地址Addr开始的b字节引用。
通用寻址模式 Imm(Eb, Ei, s)
(1)Imm: 立即数偏移
(2)Eb: 基址寄存器
(3)Ei: 变址或索引寄存器
(4)s: 伸缩因子,必须是1,2,4或8
有效地址 = Imm + R[Eb] + R[Ea] * s
6. 栈的增长方向(一般来说,栈是往低地址方向增长)
以下压栈出栈等效语句:
push %ebp<==> subl $4, %esp
movl %ebp, (%esp)
popl %eax <==> movl (%esp), %eax
addl $4, %esp
7. 数据传送
(1) 根据惯例,所有返回整数或指针的函数都是通过将结果放在%eax来达到目的的。
(2) 局部变量通常都是保存在寄存器,而不是存储器中。
8. 算术和逻辑操作
(1) 加载有效地址 leal S, D D<-&S
(2) 一元操作 incl, decl, negl, notl
(3) 二元操作 addl, subl, imull, xorl, orl, andl
(4) 移位操作 sall, shll, sarl, shrl
移位量可以是立即数,也可以放在单字节寄存器%cl中
2007年3月10日星期六
《深入理解计算机系统》第三章笔记<二>
对于不同的数据类型,指令的后缀是不同的,如下:
类型 后缀
(1) char ----------- b
(2) short ---------- w
(3) int、unsigned、long int、unsigned long、char *、double -------- l
(4) float ----------- s
(5) long double ---- t
4. 寄存器访问信息
一个IA32中央处理器(CPU)包含一组八个存储32位值的寄存器,用来存储整数数据和指针。它们的名字以%e开头,大多数情况下,前六个寄存器(%eax、%ebx、%ecx、%edx、%edi和%esi)都可以看成是通用寄存器,使用没有限制。最后两个%ebp和%esp保存指向程序栈中重要位置的指针,分别是帧指针和栈指针。
《深入理解计算机系统》第三章笔记<一>
用c程序作为例子,比如有code.c文件
1.如何生成汇编文件?
编译code.c时只需加上编译选项-S便可,命令如下:gcc -S code.c
这条命令会产生code.s文件,就是汇编文件了,另外也可以使用-o选项来指定文件名,用-O2来告诉编译器采用二级优化,还有其他选项可以参考相关手册。
2.如何反汇编?
还以code.c为例,比如gcc code.c生成a.out可执行文件后,下面命令则将a.out反汇编
objdump -d a.out
这样便会在终端上显示出反汇编代码,同时显示程序的字节编码
