欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《深入理解计算机系统》笔记(一)栈(本篇)

程序员文章站 2023-12-31 16:23:04
...

欢迎查看《深入理解计算机系统》系列博客

《深入理解计算机系统》笔记(一)栈(本篇)

《深入理解计算机系统》笔记(二)内存和高速缓存的原理

《深入理解计算机系统》笔记(三)链接知识

《深入理解计算机系统》笔记(四)虚拟存储器,malloc,垃圾回收

《深入理解计算机系统》笔记(五)并发、多进程和多线程【Final】

——————————————————————————————————————–

读后感

        这本书是美国“卡内基-梅隆大学(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指令从栈中弹出地址,并跳转到这个位置。例如下面的代码:

  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. }  
int accum = 0;
int sum(int x,int y);
int main()
{
    return sum(1.3);
}
int sum(int x,int y)
{
    int t = x + y;
    accum += t;
    return t;
}
经过反汇编后,节选处call部分的代码如下图所示:

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

《深入理解计算机系统》笔记(一)栈(本篇)

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

—》函数调用实例

  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. }  
int swap_add(int* xp,int* yp);
int caller()
{
    int arg1 = 534;
    int arg2 = 1057;
    int sum = swap_add(&arg1 , &arg2);
    int diff = arg1 - arg2;

    retur sum * diff;
}
int swap_add(int* xp,int* yp)
{
    int x = * xp;
    int y = * yp;
    *xp = y;
    *yp = x;
    return x + y;
}
《深入理解计算机系统》笔记(一)栈(本篇)
    (蓝色箭头是“指向”,红色箭头是“偏移量”,绿色箭头是解释说明)

    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)

    计算机

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

  1. int main()  
  2.   
  3. int local;  
  4. printf(”local at %p\n”,&local);  
  5. return 0;  
    int main()
{
    int local;
    printf("local at %p\n",&local);
    return 0;
} 

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

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

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

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

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

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

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

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

    X86-64没有帧寄存器。

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

第二个读书笔记,点击查看

            </div>

欢迎查看《深入理解计算机系统》系列博客

《深入理解计算机系统》笔记(一)栈(本篇)

《深入理解计算机系统》笔记(二)内存和高速缓存的原理

《深入理解计算机系统》笔记(三)链接知识

《深入理解计算机系统》笔记(四)虚拟存储器,malloc,垃圾回收

《深入理解计算机系统》笔记(五)并发、多进程和多线程【Final】

——————————————————————————————————————–

读后感

        这本书是美国“卡内基-梅隆大学(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指令从栈中弹出地址,并跳转到这个位置。例如下面的代码:

  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. }  
int accum = 0;
int sum(int x,int y);
int main()
{
    return sum(1.3);
}
int sum(int x,int y)
{
    int t = x + y;
    accum += t;
    return t;
}
经过反汇编后,节选处call部分的代码如下图所示:

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

《深入理解计算机系统》笔记(一)栈(本篇)

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

—》函数调用实例

  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. }  
int swap_add(int* xp,int* yp);
int caller()
{
    int arg1 = 534;
    int arg2 = 1057;
    int sum = swap_add(&arg1 , &arg2);
    int diff = arg1 - arg2;

    retur sum * diff;
}
int swap_add(int* xp,int* yp)
{
    int x = * xp;
    int y = * yp;
    *xp = y;
    *yp = x;
    return x + y;
}
《深入理解计算机系统》笔记(一)栈(本篇)
    (蓝色箭头是“指向”,红色箭头是“偏移量”,绿色箭头是解释说明)

    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)

    计算机

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

  1. int main()  
  2.   
  3. int local;  
  4. printf(”local at %p\n”,&local);  
  5. return 0;  
    int main()
{
    int local;
    printf("local at %p\n",&local);
    return 0;
} 

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

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

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

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

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

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

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

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

    X86-64没有帧寄存器。

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

第二个读书笔记,点击查看

            </div>
相关标签: 计算机系统

上一篇:

下一篇: