栈到底是什么玩意

cpu中有栈段SS寄存器和栈指针SP寄存器,它们是用来指定当前使用的栈的物理地址。换句话说,要想让cpu运行,必须得有栈。栈是什么?干吗用的?本节将给大家一个交待。

还记得数据结构中的栈吗?那是逻辑上的数据存取结构,是种如何用这种数据结构来存取数据的描述。在用户进程空间中,堆是堆,栈是栈,但堆栈却是人们常说的栈,和堆没关系,所以,咱们后面为避免混淆,只说栈。

栈是线性表的一种,什么是线性表?如果提出这样的问题,我想您可能不清楚什么是线性。线性就是具有线的性质,就像一条线一样,连续性强,从一个方向到另一个方向。线上没有面积的概念,不管是直线还是曲线,在线上任意位置只能容纳一个数据对象。线性表简而言之就是一个线性存储单元,结构中每个元素都有一个前驱和一个后继元素,且仅各有一个。这就是线性的体现:连续,且任意位置只有一个元素。栈也是这样,不过不同的是,数据的存取都在一端进行,这一端称为栈顶,另一端做为存储单元的基址永远不动,称为栈底。这就是上学时,老师们常常说的后进先出,先放进去的数据要在最后才能取出,后放进去的数据最先被取出。

这里我就不用举汉诺塔这样经典的例子了,毕竟上学时都听得太多了。我说个大家都认同的事实:大家肯定挤过公交车吧(坐过公交车的同学继续看,土豪随意^_^),尤其是早班车和末班车,车厢里人挤人,站都站不稳。先挤上车的乘客其实很倒霉的(有座儿不算^_^),因为他要在最后下车,在拥挤的车厢中折磨的时间最长。后挤上车的乘客在下车的时候还是蛮爽的,因为他会是第一个下车,是率先逃出恶劣环境的人。所以,挤公交车就是典型的后进先出。车厢就相当于栈,乘客就相当于栈中存取的元素,这个例子其实还算生动。

举的例子虽然很常见,但这对于已经理解栈的同学来说,我像是在说废话一样没新意。对于不理解栈的同学来说,可能是依然像说废话一样,说了也意会不到栈是什么。我非常理解这种心情,记得当初我在学网络时,老师说只要在路由器上把三层(网络层)IP协议(不是指令指针寄存器IP)禁用,四层(传输层)上的tcp或udp协议自然就不可用了。老师为了让我们明白这种依赖关系,甚至不惜举出这样的例子:如果不想让某人说话 ,最简单的办法就是给让其睡着,而不是劝他保护安静。这个例子非常浅显易懂,但用例子来理解理论知识,依然让我有点摸不着头脑,这可不是比喻恰不恰当的事,知识是严谨的,不是比喻出来的。如果您现在也有这样的体会,没关系,以后会不断接触栈的,熟了自然就理解了,这只是时间问题。

初次学习数据结构时,不容易理解其本质,我当初在学习这门课时,感觉云里雾里的,似乎明白似乎又不懂,老师让不懂的同学提问,我又不知道该怎样描述问题,不知道哪里不懂。同样的定义,同样的文字描述,每个人理解的都不一样。就像鱼和小鸟,鱼认为自己离开水就会死,水就是生命,小鸟也认为没水会渴死,水也是生命。但鱼和小鸟对水的理解能一样吗?赶紧回来,还是说咱们的正事。栈只是一种抽象概念,是一种虚拟出来的数据存取方法。其实现形式是不限的,只要满足栈的定义就可以:

  • λ首先得是线性结构,并且数据的存取在线性结构的一端进行。如果您愿意,可以用链表来实现,也可以用数组来实现,它们都是线性数据结构。
  • λ其次需要维护一个指针,用它来指向线性结构的一端,数据存取都通过此指针。

前面又比喻又回忆的,说了这么多,栈能够干什么呢?栈是一种很伟大的发明,可以解决很多难题:

  • λ表达式计算,如中缀表达式和后缀表达式的转换
  • λ函数调用,无论是嵌套调用或递归调用,用来维护返回地址。
  • λ深度优先搜索算法

到现在为止,我们说的只是数据结构中的栈,这是逻辑上的,最终我想表达的是内存中的栈,这是物理上的。把数据结构中的栈的概念用物理硬件来实现,这就是我们要说的栈。它同数据段、代码段一样,是个内存中的区域。也就是栈段寄存器SS和栈指针SP所指向的内存区域。我们常听说的栈溢出,指的就是这个内存区域无法容纳数据了。

硬件是如何实现这个栈的呢?还是那句话,首先得满足栈的概念,具备栈的特性,即使是硬件也不能例外,必须满足上面提到的这两个条件:一个是线性结构,一个是在栈顶对数据存取。因为它毕竟造的是栈,不具备这些就不叫栈了。

线性结构这个简单,内存就是,直接用物理内存存取最方便了,咱们要做的,就是给栈指定一片内存区域就成了,区域的起始地址做为栈基址,存入栈基址寄存器SS中,另一端是动态变化的,用栈指针寄存器SP来指定。栈在使用过程中是向下扩展的,所以栈顶地址肯定是小于栈底地址。

栈既然是一片内存区域,访问内存就要用“段基址:段内偏移地址”的形式,所以栈中的内存地址也是用“段基址SS的值*16+栈指针SP(段内偏移地址)形成的20位地址”访问到的。由于是硬件实现的栈,故硬件提供了相应的方法来存取栈,即push和pop指令。push指令负责把数据压入栈,pop指令功能相反,将其从栈中取出。不过我刚才说的不全面,栈的出口和入口都是栈顶,push把数据压向哪里,它得知道栈顶在哪里才行。pop指令也一样,它得知道哪里是栈顶才能从栈中取出正确的数据。这正是栈指针寄存器SP的作用,此寄存器中的值是段内偏移地址,是栈顶相对于栈底的偏移量。

栈顶(SP指针)是栈的出口和入口,它指向的内存中存储的始终是最新的数据。push和pop就是操作这个指针所指向的内存。由于栈是从高地址向低地址发展,所以栈顶、栈指针指向的地址会越来越低。push压入数据的过程是:先将SP减去字长,目的是避免将栈顶的数据破坏,所得的差再存入SP,栈顶在此被更新,这样栈顶就指向了栈中下一个存储单元的位置。再将数据压入SP(新的栈顶)指向的新的内存地址。pop指令相反,既然是在栈中弹出数据,栈指针寄存器SP的值应该是增大一个数据单位。由于要弹出的数据就在当前栈顶,所以在弹出数据后,才将SP加上字长,所得的和再存入SP,从而更新了栈顶。这样SP就指向了上一个存储单元的位置。

上面提到的字长,是指cpu的字长,即一次可处理的数据的长度。在实模式下的字长是16。

物理内存中的栈如图:

注意啦,如图所示,虽然栈是向下发展,但栈也是内存,访问内存依然是从低地址往高地址,假如当前栈顶是0x1233E,栈顶数据占2字节的话,其范围是0x1233E~0x1233F。个人觉得,这个硬件中的栈让人感到神秘,主要有两方面原因:

一方面是栈指针不是自己维护,这不像咱们在高级语言中自己创建的栈那样,指针的一举一动都是自己在操作。不直接受控的东西往往让人心存忧虑和有点小恐慌。其实即使是这里的硬件栈,咱们也可以自己维护指针,如push ax可以这样代替:

mov bp,sp
sub bp, 2
mov [bp],ax

bp默认的段寄存器就是SS,用bp的时候直接操作的便是栈。bp就相当于栈指针啦,自己维护毕竟太麻烦,有直接省事的干吗不用呢^_^。

另一方面,栈就是一片内存区域,只不过“经常”操作这片内存的指令不是mov,而是push、pop,这两条指令无非是自动维护存取数据的位置(SP寄存器的值)而已,大家用mov来操作这片内存,不是也得要给出存取地址吗。这样看来,它和普通的数据段没什么不同,不要觉得它比金字塔还神秘啦。

一定要注意,push和pop操作是要成对出现的,这样才能维护栈平衡。否则,光push,不pop,有进没出,这栈很快就溢出啦。切记,一个push要对应一个pop,每键入一个pop指令,一定要清楚它对应的是哪个push。

栈就先说这么多,不摸索实际东西的话还是不能真正掌握和理解,本书强调实践,纸上谈兵可来不了真知识。

好啦,官人常来玩哦

一步步编写操作系统 13 栈相关推荐

  1. 一步步编写操作系统 71 直接操作显卡,编写自己的打印函数71-74

    一直以来,我们在往屏幕上输出文本时,要么利用bios中断,要么利用系统调用,这些都是依赖别人的方法.咱们还用过一个稍微有点独立的方法,就是直接写显存,但这貌似又没什么含量.如今我们要写一个打印函数了, ...

  2. 一步步编写操作系统 69 汇编语言和c语言共同协作 70

    由于有了上一节的铺垫,本节的内容相对较少,这里给大家准备了两个小文件来实例演示汇编语言和c语言相互调用. 会两种不同语言的人,只是掌握了同一件事物的两种表达方式.人在学习一种新语言时,潜意识里是建立了 ...

  3. 一步步编写操作系统 62 函数调用约定

    由于我们要将c语言和汇编语言结合编程啦,所以一定会存在汇编代码和c代码相互调用的问题,有些事情还是要提前交待给大家的,本节就是要给大家说下函数调用规约中的那些事儿. 函数调用约定是什么? 调用约定,c ...

  4. 一步步编写操作系统 67 系统调用的实现1-2 68

    接上文: 系统调用的子功能要用eax寄存器来指定,所以咱们要看看有哪些系统调用啦,在linux系统中,系统调用是定义在/usr/include/asm/unistd.h文件中,该文件只是个统一的入口, ...

  5. 一步步编写操作系统 30 cpu的分支预测简介

    人在道路的分岔口时要预测哪条路能够到达目的地,面对众多选择时,计算机也一样要抉择,毕竟计算机的运行方式是以人的思路来设计的,计算机中的抉择其实就是人在抉择. cpu中的指令是在流水线上执行.分支预测, ...

  6. 一步步编写操作系统 09 写个mbr

    有点不好意思了,说了好久,才说到实质性的东西,好了,赶紧给客官上菜. 代码2-1(c2/a/boot/mbr.S)1 ;主引导程序2 ;-------------------------------- ...

  7. 一步步编写操作系统 79 在c代码中内联汇编

    基本内联汇编是最简单的内联形式,其格式为: asm [volatile] ("assembly code") 各关键字之间可以用空格或制表符分隔也可以紧凑挨在一起不分隔,各部分意义 ...

  8. 一步步编写操作系统 77 内联汇编与ATT语法简介

    内联汇编 之前和大家介绍过了一种汇编方法,就是C代码和汇编代码分别编译,最后通过链接的方式结合在一起形成可执行文件. 另一种方式就是在C代码中直接嵌入汇编语言,强大的GCC无所不能,咱们本节要学习的就 ...

  9. 一步步编写操作系统 76 用汇编语言编写字符打印函数

    之前咱们介绍显卡上那么多的寄存器终于发挥用处了,我们看看前文中介绍的表CRT Controller Data Registers中索引为0Eh的 Cursor Location High Regist ...

最新文章

  1. 十二 手游开发神器 cocos2d-x editor 之游戏暂停悬浮层
  2. 底部菜单 点击突起_iOS开发之上下文交互菜单(UIContextMenuInteraction)
  3. ubuntu19.10安装remarkable
  4. linux ls 命令排序,如何在Linux中使用ls命令按大小对所有文件进行排序
  5. 电大计算机应用基础试卷号2007,电大计算机应用基础(试卷版)计算机试卷7.doc...
  6. 从月薪2300女工到年薪70万谷歌程序员:人生,永远不要给自己设限
  7. 京东金融回应用户遭盗刷:系用户点击假冒链接 输短信验证码致密码泄露
  8. 通过web的方式动态查看tomcat的catalina.out的日志(web.py)
  9. 主力用计算机吸筹,通达信主力吸筹指标
  10. Java应用性能分析工具:async-profiler
  11. 视频生成动画数据OpenPose+OpenCV
  12. 2023免费电脑录视频软件Camtasia
  13. Matlab用Copula模型进行蒙特卡洛(Monte Carlo)模拟和拟合股票收益数据分析
  14. 巧用右键管家 清除右键菜单【xiame】
  15. 【智能优化算法-飞蛾火焰优化算法】基于动态惯性权值策略的飞蛾火焰优化算法求解单目标问题附matlab代码
  16. Eclipse中如何设置字体大小
  17. [4G+5G专题-145]: 规范-5G NR协议规范快速概览
  18. SEO和SEM的区别是什么,哪个效果更好一些
  19. 深入浅出JavaScript(中文版)__莫里森 初读笔记
  20. C#在聊天室中经常遇到屏蔽脏话功能,完成当用户输入一句话中带有“sb”,则将“sb”替换成“**”

热门文章

  1. 1、管理员登录中间件和注销
  2. 动态规划——矩阵中的最短路径长度
  3. router锚点和html锚点,hash模式下Vue-router页面返回锚点(scroll behavior)实现
  4. python ssh脚本_ssh爆破(python脚本)
  5. 服务器与项目之间的关系,项目 服务器 和数据库的关系
  6. 2021届定远三中高考成绩查询,定远各中学高考喜报!
  7. mysql一个表几亿数据_如何在mysql 造1亿条记录的大容量数据表?
  8. 广工android嵌入式系统试卷_嵌入式系统考试试题A及答案
  9. oracle .dbf文件过大_学习这篇Oracle数据库文件坏块损坏的恢复方法,拓展你的知识面...
  10. 状态模式和策略模式的区别