0.0《深入理解计算机系统》笔记(一)栈【插图】

读后感

        这本书是美国“卡内基-梅隆大学(CMU)”的教科书,逻辑严谨。虽然是教科书,还是有些晦涩难懂啊,不太形象。第二章主要讲整数,浮点数,很是晦涩,全是数学公式。作者的思维数学的思维,动不动就是n、m、k、∑等等,让我们数学很烂的同学如何是好。如果能以普通人的思维把数学知识加进去就好了。

        该书确实系统的介绍了计算机,很完善。它能给你以下几个重要级别的模型和过程:

    1.函数的调用栈模型——第三章(函数不一定都会创建栈帧,本文章将解释此现象)

    2.a.out或者exe可执行文件的结构——第七章点击打开链接

    3.程序加载器和链接——第八章 点击打开链接

    4.malloc和虚拟存储器原理——第九章点击打开链接

    5.线程,在存储器中模型——第12章

    对于处于成长期的程序员来说,真是欣喜若狂!有了这些知识还需要《C专家编程》这本书么?这本书就是《C专家编程》的全覆盖啊,哈哈!

    翻译者很是用心,但是读者不一定领情。比如:可以直接翻译流行的内存、硬盘和固态硬盘,完全没有必要用主存、磁盘和固态存储磁盘。还比如:没有必要把shell翻译成“外壳”多别扭啊。这些翻译者应该像“侯捷”学习。

    这本说内容大而散,感觉没有尽头一样。老外怎么学这种课程,费脑子啊。从计算机结构、二进制表示、到汇编语言函数的调用、然后cpu的结构、再有连接器存储器、还有进程,并发、更有网络编程,基本大学四年也就学了这么多东西。

    这本书中有句话很有意思:存储器的一个有趣的属性是不论系统中有多大的存储器,他总是一种稀缺资源。磁盘空间和垃圾桶同样有这个属性。

    工作2年多的时间里,每每都是在网上搜系统方面的知识、编译、链接和虚拟存储器malloc等等。只有读了这本书才能系统得学到计算机知识。

一、计算机漫游

---》利用直接存储器(DMA)的技术,数据可以不通过cpu而直接从磁盘到达内存。

---》根据机械原理,较大的存储器比较小的存储器运行慢,一个寄存器只能存储几百个Byte,而且内存可以存放GB以上。加快处理器的运行速度比加快内存运行速度更容易。

---》高速缓存至关重要,一个简单的helloworld揭示了一个重要的问题。系统花费大量时间把信息从一个地方挪到另一个地方。helloworld最初放在硬盘上,然后加载到内存,进而进入cpu中。下图说明了一个存储器层次结构:

二、信息的表示和处理

讲的是计算机原理,二进制,补码和浮点数等。因为大学课程已经学习过了,没有细读。

 

---》浮点数,规格化、非规格化和无穷大。

    一般来说我们没把发用小数表示1/3、7/10等这些不能整出的数字,那么如果用二进制表示十进制的小数,更多的表示不出来。二进制甚至不能表示十进制的0.1和0.2

三、程序的机器级表示(其实就是汇编语言)

---》讲的是《汇编语言》,头都大了!个人觉得汇编语言不用花时间了解,即使是本书中的汇编语言也有文字解析。IA32X86-64两种汇编语言。

---》汇编代码不区分有符号和无符号甚至指针类型。

---》下图展示了,汇编代码后缀的含义:

    大多数GCC生成的汇编指令都有一个字符后缀,表示操作数的大小。例如数据传送指令有三个变种:movb(传送字节)、movw(传送字)和movl(传送双字)。注意,汇编代码使用后缀'l'来表示4个字节整数和8个字节双精度浮点数,这不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器。

    操作数指示符,操作数一共有三类:1)立即数(immediate)也就是常数值,立即数的书写方式是$。例如:$0x1F。2)寄存器,3)存储器(memory).由于三种操作数的存在所以寻址方式就有很多种。

---》一个32位cpu中寄存器的结构如下:

    上图是IA32的整数寄存器。所有8个寄存器都可以作为32位和16位使用,例如%eax和%ax。并且前四个寄存器可以访问其两个低字节。如:%ah和%al。

下图是64位cpu的寄存器结构图:

红色框内,是兼容32为cpu的结果。

 

---》寄存器使用惯例:%eax、%edx和%ecx是调用者保存寄存器,%ebx、%esi和%edi是被调用者保存寄存器。那么,一个函数f()可能被别人调用,也可以调用其他函数,所以当f()运行时需要将%ebx、%esi和%edi保存到栈中,并在返回前再恢复它们。(p151)---》64位%rax寄存器用来保存函数的返回值,(p198)

    在x86-64汇编语言,中%rax用来保存函数的返回值,而在结果返回之前,%rax可以重复利用。

---》栈在处理函数调用中起到至关重要的作用。下图栈的示意图,栈顶朝下,由于IA32 的栈竟然是往低地址延伸生长,直让我崩溃。(p115)

图片的上半部分,说明了实际效果,即将%eax的值移动到%edx中,图片的下半部分是栈移动步骤。栈顶的变化最后关键。从0x108 -> 0x104 -> 0x108

---》栈帧结构,IA32程序用程序栈来支持函数调用。机器用栈来传递函数参数、返回值、保存寄存器用于以后恢复和本地存储。为单个过程分配的那部分栈成为栈帧(stack frame)。下图说了栈帧的结构。

---》call指令。call指令的效果是将返回值地址入栈,并跳转到被调用过程的起始处。返回地址是在程序中紧跟在call后面的那条指令的地址。这样当被调用函数返回时,执行会从此处继续。ret指令从栈中弹出地址,并跳转到这个位置。例如下面的代码:

 

[cpp] view plain copy
 
  1. int accum = 0;  
  2. int sum(int x,int y);  
  3. int main()  
  4. {  
  5.     return sum(1.3);  
  6. }  
  7. int sum(int x,int y)  
  8. {  
  9.     int t = x + y;  
  10.     accum += t;  
  11.     return t;  
  12. }  

经过反汇编后,节选处call部分的代码如下图所示:

 

第一行call指令的效果就是将0x80483e1压入栈中,同时将%eip(程序计数器)的值设置为sum的第一条指令0x8048394.最后一行的ret指令弹出0x80483e1给%eip,并跳转到这个地址。如图所示:

ret指令的效果就是让0x080483e1弹出,调整栈指针,并且0x080483e1赋给%eip,程序继续执行。

---》函数调用实例

 

[cpp] view plain copy
 
  1. int swap_add(int* xp,int* yp);  
  2. int caller()  
  3. {  
  4.     int arg1 = 534;  
  5.     int arg2 = 1057;  
  6.     int sum = swap_add(&arg1 , &arg2);  
  7.     int diff = arg1 - arg2;  
  8.   
  9.     retur sum * diff;  
  10. }  
  11. int swap_add(int* xp,int* yp)  
  12. {  
  13.     int x = * xp;  
  14.     int y = * yp;  
  15.     *xp = y;  
  16.     *yp = x;  
  17.     return x + y;  
  18. }  


    (蓝色箭头是“指向”,红色箭头是“偏移量”,绿色箭头是解释说明)

 

    arg1和arg2必须存放在栈中,因为我们必须为它们生成地址。swap_add中的变量int x和int y可以存放在寄存器中。

    分配在栈上的24个字节,8个用于局部变量,8个用于参数,8个未使用,这是因为GCC认识所有的栈空间都应该是16的整数倍。这样保证数据放的严格对齐。

    经过调用swap_add之后栈的信息又恢复到最初的状态。

---》许多函数编译后不需要栈帧。如果所有的局部变量都能保存在寄存器中,而且这个函数又不会调用其他函数(叶子过程),那么需要栈的唯一原因就是用来保存返回值。特别是dui'yu所以,虽然C语言中有寄存器变量,但是如果这个函数的变量很少的话,及时不标明这个变量是寄存器,它也会被加载到寄存器中去。(p196)

---》函数需要栈帧的原因有如下几个:

    ●局部变量太多,不能都放在寄存器中。

    ●有些局部变量是数组或者结构。

    ●函数用&来计算一个局部变量的地址。

    ●函数必须将栈上的某些参数传递给另外一个函数

    ●在修改一个被调用着保存寄存器之前,函数需要保存其他状态。

---》栈破坏检测和栈保护(p181)

    在C语言中,没有可靠的方法来防止对数组的越界写操作。数组越界,是栈溢出后发现这个错误然后抛出。

    

echo是一个函数,存放了char buf[8]的一个局部变量。

思想:在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀(canary)值,也成为哨兵值(guard value)是在程序每次运行时随机产生的。因此如果这个哨兵值改变了说明栈溢出了。

---》栈随机化(p180)

    计算机

    比如,多次运行下面的代码,本地变量的地址是不变的。

 

[objc] view plain copy
 
  1. int main()  
  2.   
  3. int local;  
  4. printf("local at %p\n",&local);  
  5. return 0;  

 

    一个现实生活中的例子,但是这个例子说的是每次堆上开辟空间可能是一致的。

    曾经在做Symbian项目的时候,发现一个不是必现的bug,后来发现是野指针。但是问题是为什么不是必现呢?是因为Symbian操作系统每次在上开辟的空间,在短时间内是一个地址。举例:假如,ptr这个指针,现在成为野指针了。但是,之后它指向的内存又被重新malloc了,等同于ptr指向了新的对象。但是,这个巧合并不是每次复现。

---》将IA32扩展到64位。(p183)

    X86-64是AMD提出来,并命名的。现在一般简写X64

    ●通用目的寄存器组从8个扩展到16个。而且名字也变成了%rax,%rbx。其中%rax用来存放返回值。

    ●许多程序状态都保存在寄存器中,而不是栈上。整形和指针类型的参数通过寄存器传递。所以,有些过程根本不需要建立栈。

    ●如果可能,条件操作作用条件传送指令实现,会得到比传统分支代码更好的性能。

    ●浮点操作用面向寄存器的指令集来实现,而不是IA32支持的基于栈的方法来实现。

    ●X86-64没有帧寄存器。

---》函数指针的值是该函数机器代码表示中的第一条指令的地址。(p173)

 

原文:http://blog.csdn.net/hherima/article/details/8916329

posted @ 2017-09-15 10:35  瘋耔  阅读(474)  评论(0编辑  收藏  举报
跳至侧栏