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

2023-12-04 12:48

本文主要是介绍《深入理解计算机系统》笔记(一)栈(本篇),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

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

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

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

《深入理解计算机系统》笔记(四)虚拟存储器,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指令从栈中弹出地址,并跳转到这个位置。例如下面的代码:

[cpp] view plain copy
print ?
  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,程序继续执行。

—》函数调用实例

[cpp] view plain copy
print ?
  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)

    计算机

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

[objc] view plain copy
print ?
  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>

这篇关于《深入理解计算机系统》笔记(一)栈(本篇)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/453507

相关文章

深入理解Apache Kafka(分布式流处理平台)

《深入理解ApacheKafka(分布式流处理平台)》ApacheKafka作为现代分布式系统中的核心中间件,为构建高吞吐量、低延迟的数据管道提供了强大支持,本文将深入探讨Kafka的核心概念、架构... 目录引言一、Apache Kafka概述1.1 什么是Kafka?1.2 Kafka的核心概念二、Ka

利用Python快速搭建Markdown笔记发布系统

《利用Python快速搭建Markdown笔记发布系统》这篇文章主要为大家详细介绍了使用Python生态的成熟工具,在30分钟内搭建一个支持Markdown渲染、分类标签、全文搜索的私有化知识发布系统... 目录引言:为什么要自建知识博客一、技术选型:极简主义开发栈二、系统架构设计三、核心代码实现(分步解析

Java并发编程必备之Synchronized关键字深入解析

《Java并发编程必备之Synchronized关键字深入解析》本文我们深入探索了Java中的Synchronized关键字,包括其互斥性和可重入性的特性,文章详细介绍了Synchronized的三种... 目录一、前言二、Synchronized关键字2.1 Synchronized的特性1. 互斥2.

一文带你深入了解Python中的GeneratorExit异常处理

《一文带你深入了解Python中的GeneratorExit异常处理》GeneratorExit是Python内置的异常,当生成器或协程被强制关闭时,Python解释器会向其发送这个异常,下面我们来看... 目录GeneratorExit:协程世界的死亡通知书什么是GeneratorExit实际中的问题案例

深入解析Spring TransactionTemplate 高级用法(示例代码)

《深入解析SpringTransactionTemplate高级用法(示例代码)》TransactionTemplate是Spring框架中一个强大的工具,它允许开发者以编程方式控制事务,通过... 目录1. TransactionTemplate 的核心概念2. 核心接口和类3. TransactionT

深入理解Apache Airflow 调度器(最新推荐)

《深入理解ApacheAirflow调度器(最新推荐)》ApacheAirflow调度器是数据管道管理系统的关键组件,负责编排dag中任务的执行,通过理解调度器的角色和工作方式,正确配置调度器,并... 目录什么是Airflow 调度器?Airflow 调度器工作机制配置Airflow调度器调优及优化建议最

一文带你理解Python中import机制与importlib的妙用

《一文带你理解Python中import机制与importlib的妙用》在Python编程的世界里,import语句是开发者最常用的工具之一,它就像一把钥匙,打开了通往各种功能和库的大门,下面就跟随小... 目录一、python import机制概述1.1 import语句的基本用法1.2 模块缓存机制1.

深入理解C语言的void*

《深入理解C语言的void*》本文主要介绍了C语言的void*,包括它的任意性、编译器对void*的类型检查以及需要显式类型转换的规则,具有一定的参考价值,感兴趣的可以了解一下... 目录一、void* 的类型任意性二、编译器对 void* 的类型检查三、需要显式类型转换占用的字节四、总结一、void* 的

深入理解Redis大key的危害及解决方案

《深入理解Redis大key的危害及解决方案》本文主要介绍了深入理解Redis大key的危害及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着... 目录一、背景二、什么是大key三、大key评价标准四、大key 产生的原因与场景五、大key影响与危

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规