当前课程知识点:智能车制作:嵌入式系统 > 第三章 MCU基础 > 3.2.1 堆栈的概念 > Video
各位同学大家好
我是清华大学工程物理系的曾鸣老师
欢迎大家继续回到我们
ARM微控制器与嵌入式系统的学习课堂
那么我们现在进入
第三章的第二个单元的学习
在第一个单元
我们初步认识了
ARM的微控制器里头CPU运行的机制
知道了这个程序是怎么运行的
在一个存储有地址的空间这种概念里头
我们在PC指针寄存器
或者叫做程序计数器寄存器的指引下
根据地址一条条的指令取进来
放到我们CPU里运行
那么我们CPU在这些指令的解析下
从我们的存储器
或者从我们当前的CPU内部的寄存器
拿到的数交给逻辑运算单元
进行一步一步的运算
那么指令告诉了这些数 从哪来
以及这些数拿进来了
分别应该做什么运算
我们称为指令的解析和数据的流向
那么上一个单元
最后遗留了一个问题是什么呢
就是我们的PC程序计数器
或者叫PC指针寄存器
它所指引的程序的运行的流程
可能很多时候不是简单的
逐一递增这样一种自动的操作
很多时候它会出现
具有一定偏移量的跳转比如If Else
但是我们刚才提到的最后一个遗留问题是
当我们发生类似于函数调用
这样的使用的时候
在一个程序执行的流程当中
调用了一个子函数
完成一个特定的功能
那么我们以怎样的机制
能够让计算机
让CPU自动的保存一个返回的地址
知道这个CPU能够回到哪去
那么很多同学会说
如果是我来设计这个计算机
我来设计这个CPU
那很简单啊
你不是有一个寄存器
保存这个正在算的数吗
你不是有一种寄存器
保存这个程序执行到哪个地址
下一条指令是哪个地址了吗
那在CPU设计的时候
再多设计几个寄存器不就完了
比如设计一个寄存器
专门叫做返回地址寄存器
那么这个时候我当发生函数调用的时候
我发生调用之前
要回的那个地址我在CPU里头存一下
那么一旦这个函数执行完
我用一条指令说函数要返回
我就读一下这个寄存器
把这个值赋值还给PC寄存器
我程序不就回去了嘛
PC指针寄存器接着刚才程序往下走
那么这些同学是聪明的
在早期的CPU的设计里
一开始就是这么设计的
有一个叫做返回地址寄存器
之后会发现不够用了
为什么呢
因为我们的程序
有可能会发生函数的嵌套调用对不对
我们经常会在写程序
包括我们自己写程序里头
我们会写一个函数
它在里头调用了一个函数
由这个函数再调用一个子函数
那么当我们出现了函数嵌套调用的时候
我前一次调用的返回地址不能丢
我后一次调用的函数地址还得存
那是不是这个能存几次返回地址
就决定了我能嵌套多少个函数调用啊
那么这对于C语言是非常不舒服的
因为C语言它是一个非常standard的语言
我们可能会使用它的库函数
你无法知道库函数里有没有发生函数调用
所以这样一种机制在1974年我们上次讲计算机
八卦计算机史的时候
4004 8008这样的CPU里头
它从最初的片内
有一个返回地址寄存器
到有两个 三个 四个
这样在逐渐的增加
增加到最后
大家觉得那不能没完没了的加下去啊
我们一定要想一种机制
让它变的更加的聪明和更加的自动
那么这种机制是什么呢
就是我这一个单元我们要讲的栈的概念
栈在英文里就是stack
但是用我们中国人的语言
非常不喜欢用一个单字来表达词汇
这是近代汉语的一个特点
所以我们往往会在
计算机交流的时候会说堆栈 堆栈
绝大多数我们专业
或者准专业人士说堆栈的时候
心里想的可能就只是栈
因为堆是堆 栈是栈
我们待会会讲堆是heap
栈是stack
那么栈究竟是一个什么样的东西呢
它就是为了应对
我们刚才所说的这个问题的一种机制
首先看看栈
堆栈 这个栈是一段连续的存储空间
那么我们可以把它就想成一个内存
之前讲过这个概念有很多格子
每个都有个地址
里头会存数
但是栈约定了对这段存储空间
我们按一种特殊的方法来加以使用
为什么呢
我们对它采用后入先出的方式来进行工作
也就是Last In First Out
那么也就是说
我们因为后入先出更形象的一个比喻
就是我们把它看成一个堆叠的存储器
它是一段可以使用的存储空间
有很多很多格子
这个时候
我们不再按地址直接对它进行访问
而是只能从上面顶部
来放一个数据或者拿走一个数据
这件事情大家的感觉如果还不够鲜明的话
对于初学同学我给你们一个更形象的比喻
就是有一个口很小的桶
里头有很多饼干
然后有一只小猴子来吃饼干
它每次只能把手伸进去拿最上面那个饼干
假如这个小猴子很善于储蓄
有了富余的饼干
它也只能拿放进去
放最上面那个位置
所以这样一种筒状的这种后入先出的结构
就决定了你每次拿到的
总是最后放进去的东西
那么这个东西在我们栈的概念里头就是数据
那么大家想想这样一种结构
这样一种方式使用内存
为什么我们要设计这样一种机制
或者叫我们的前辈
为什么要设置这样一种机制
它一定有它的特点
它的特点是什么
这样一种约定的使用方式的特点
就是它能够保持
我们数据进入这个存储器的顺序
而不用额外的方式
来加以保存这种顺序
大家理解这件事吗
如果我们要存储一个数据
并且把这个数据的顺序加以记录
我们是不是至少要两列数据啊
一列是序号
一列是数据的值
而如果我们对于这个数据
约定了以这样一种方式进行存取
其实我们不需要额外的空间
来存储那个序号
他们摆放的位置
他们的地址顺序本身就是顺序
就是数据进入存储器的顺序
所以这样一种结构
导致我们对于存储器的使用
只有两种基本的操作
不再像原来那样
根据那个地址就直接加以访问
我们只有两种操作
一种称为推入
就是push
把一个数据放到了这个栈的顶部
还有一种呢叫做取出
或者我们叫弹出 pull
把一个数据弹出来拿走
只有这两种操作
那么这两种操作的结果就是
我们对于这样一种存储空间
我们会依次把数据放上去
而我们要拿走的时候
一定是倒序把它拿走
当然了这里要补充一句
我们刚才举的例子
都是这个存储器是自下而上的使用
所谓的顶部就是指
这个最后放进数据的位置
少部分CPU是反过来的
或者是可以配置的
那我们在这里不展开讲
那么有了这样一种机制
我们想想它可以用来做什么
最最简单的一个用途
就是我们刚才所讲的
在程序之间的跳转 调用的时候
它可以用来保存
我们所说的嵌套调用时候的返回地址
比如说我们函数发生了一次调用
我约定好了一块存储空间可以这样使用
是不是我把当前PC指针的地址
放在这个存储空间里
然后我们这个函数
如果只有一级调用
调用完了我是不是到
这存储空间取一个值出来
一定是我刚才放进去的
那么我赋回给我的PC指针寄存器
我的程序自动就指向了
下一条指令 继续往下执行
大家注意计算机并没有那么神秘
只是在PC指针寄存器
所指向地址去取指令来顺序执行
只要程序写的正确
我们的CPU就能完成我们要的功能
那么如果我们的函数发生了嵌套调用呢
或者说我们的子程序发生了嵌套调用呢
是不是在第一次调用
第一级的函数的时候保存了
一个地址在这个堆栈里
在这个栈里
在这个函数没有返回的时候
这个返回地址还在这个栈里
又发生一次调用
于是把当前第二次调用时候的PC指针的值
再放到栈里压在了它的上头
然后我们第二级调用的函数返回的时候
要返回值是不是只用
直接从这个栈的顶部
把最后一次放上去值取出来
就是第二次调用之前的返回值
然后第二级的函数返回以后
第一级函数执行完再返回的时候
我再从栈取一个
是不是它保证了函数是顺次调用的
顺次保存返回地址 函数退出的时候
是按顺序逆序返回的
自动的逆序取回返回值
后入先出非常完美的契合在了一起
那么实际上
在我们的汇编语言和C语言里头
栈并不仅仅只发挥这样一个作用
我们很多的时候
大家如果深入的学习C语言的原理会知道
我们的C语言的函数的参数
和函数的返回值的传递是使用栈的
然后我们C语言在一个函数内部的
局部变量也会根据编译器帮你在栈上使用
占用一到两个存储空间来给局部变量用
那么这件事情因为涉及到
C语言很多内部的东西
我们不展开在这门课程里
但是大家会更加隐性深入的理解到一件事是什么呢
一个是这个堆栈会根据
我们C语言程序的执行来慢慢的加以使用消耗对吧
第二呢
就是说为什么我们C语言
说局部变量是有生命周期的
当一个函数返回的时候
这个变量就不再能够使用
因为函数一旦返回
这些存储空间就加以释放弹出就不再使用了
日后我们还会学习对于计算机的硬件
对于CPU的硬件
如果我们要使用一个难点 中断的功能的时候
栈是一个标准的
用来存储上下文关系的寄存器值的一个存储区域
那么栈有很多很多用处
那么讲清了这个概念以后
大家就会慢慢有一种感觉是什么呢
就是刚才讲的那个CPU那个构架是不完整的
我们还缺一个东西
我们缺一段片外的存储器
我们可以说是内存空间
用来给堆栈用
那么我们需要有一个
类似于指针的东西
指向这个存储空间的某一个地址
从这个地址开始把这个内存当堆栈用
刚才那个机制有个非常大的好处就是
我不需要存储很多地址
我只要存储一个地址
每次从这个地址上加1的往上放数据
或者每次从这个地址上减1的往外取数据
就可以完成一个堆栈的功能
那么我们需要一个CPU内部的寄存器
就是一个新的东西
我们称为堆栈指针寄存器
Stack Pointer
来指向我们内存用于堆栈
这样一种方式使用的内存空间的栈底
或者还没有使用的地址
所以呢 有了这么一个概念
我们对于堆栈的使用一定记住一件事情
就是如果你自己要用堆栈进行操作
用汇编语言编程的时候
你Once push must pull
就是你一旦放了东西你一定记得拿走
然后Last in First Out
你最后放进去的东西
一定是你待会取的时候第一个拿到的东西
一定不要把这个次序弄错
那么所幸如果我们学习的是C语言的时候
我们对于堆栈的绝大多数使用
大家是感觉不到的
很多同学都学过C语言
你其实从来没有接触过堆栈
那么为什么呢
因为C语言的函数调用
函数的返回值
参数的传递
局部变量的开销
都是由编译器帮你隐性的使用了堆栈
所以没有堆栈就没有办法使用C语言
那我们在进行开发日后会学习的时候
我们的开发工具
会帮我们写出了代码初始化堆栈
我们的C语言编译器
会帮我们隐性的使用堆栈
那么直到有朝一日大家用汇编
要做中断的编程的时候
你们会接触到这个概念
那么学了这个概念
我们就对CPU的基本构架
有了一个更加完整的认识
那么这个完整的认识呢
就是现在这张图
我们在一个完整的这个CPU构架里
有一个逻辑运算单元可以负责运算
它什么都不管 它只管算
但是它的数可以从寄存器来
也可以从片外的存储器来
那么它数从哪来
做什么运算
是由我们指令解析产生的
逻辑控制单元来控制它有序的产生
而我们的逻辑控制单元的指令
又是在PC指针寄存器的指引下
从程序的那个地址空间一条一条指令
一个一个0和1的序列
拿进来解析控制数据的流向和数据的来源
以及要做的运算的内容
那么这些运算的过程当中
如果发生PC指针寄存器不是逐一累加的时候
我们需要在堆栈指针寄存器的指引下
把当前PC指针寄存器的值
我们要返回的值想办法存到一个特定的内存空间
用堆栈 用压栈 出栈的方式
来使用这段内存
有序的保存这个值来应付程序之间的函数调用
它们的嵌套调用以及跳转
那么有了这个完整的概念
我们多讲一个高阶的对于堆栈的概念
这就是大家会产生一种非常清晰的对于堆栈的思路
它像一个什么呢
我们刚才打了个比喻
像一个筒子里有很多饼干
小猴子拿饼干
这个小猴子如果嘴巴很馋它会不停的拿饼干
拿到没有为止
如果这个小猴子是一个像松鼠一样的动物
它很喜欢存储
它会把饼干不停的往里攒 越攒越高
什么时候冬天来了肚子饿了
拿一块拿一块它往下消耗
那么我们可以想象
这个内存的使用是一个什么东西啊
有点像那个开水瓶里的水你倒走了水位下降了
你填进去水位上升了
我们叫做这个是floating的
它是涨落的
那么这样一个过程当中意味着
对于内存的开销在动态的变化
它是以一个运行时的动态变化
也就是说它是在你的程序运行的过程当中
根据你程序所撰写的
那个方式 函数的调用的形式在动态的使用
那我们会想一件事情是什么
假设我们的内存终究还是有限的
我们假设把堆栈从内存的最底部
能够使用的最下面开始用
它随着使用会慢慢的往上涨
虽然它会动态的涨落
它总会有一个最大值的时候
那么我们另外还有一个非常重要的概念是什么
就是刚才说到堆的概念
那么堆实际上是从系统的内存的顶部
一般从顶部开始使用的一段存储空间
那么这个空间一般是全局的
我们能够使用堆的一个最基本的东西
就是在我们C语言
我们经常会在main函数之外顶部声明一些全局变量
那么这个是在堆的
如果我们有操作系统
我们可能还会用操作系统的内存分配函数
从堆上分配一些静态的内存来使用
我们做微控制器
做一些没有操作性的嵌入式系统的时候
因为没有操作系统存在
我们往往不会去这样子使用堆
所以我们可以认为堆可能相对小
但是仍然因为某些编程的原因
我们很多同学会声明一些全局变量
它在堆上
那么堆在上面使用内存
栈在下面动态涨落
这个时候我们就会产生一个
很有意思的概念或者现象
就是总会有一种风险
就是函数或者程序的调用
使栈的使用逐渐接近堆
在某个时间点如果他们碰上了会出现什么问题
就是格子只有一个
作为堆它认为我是个变量
这个内存属于我
我想要存数的时候我就对它写个值
而对于栈
它认为我已经涨到这儿了
这个格子属于我
我一旦函数调用
我函数在返回值存在这儿
待会函数退出我要把这个值拿出来作为我的返回值
这是很可能发生的对吧
这种错误在计算机里是一种非常容易碰到的
或者非常严重的错误
我们称为堆栈溢出
那么很多同学说这是高级的东西
老师你现在给我讲好像还很遥远
不是的
我们生活中很多时候会碰到这样的错误
包括大家调写单片机程序的时候
你们很可能会遇到
为什么呢
我们后面会讲会遇到我们的编程习惯的影响
比如很多同学声明变量上来就用int 四个字节
建个数组我不知道用多少
我上来这个数组就一千个元
4000个字节就没有了
那么到使用的时候我们就会发现有问题
或者有同学我做个应用做个智能车
我要抓一个图像
这个图像不大
100乘以50个像素
那么就是5000个单元
每个单元存一个字节
5000个字节就没有了
这个时候栈还在底下往上用
那么我们有两个地方
会非常容易的遇到堆栈的溢出
哪两个呢
一个就是我们做
用windows的时候
都会遇到蓝屏
如果你仔细看看蓝屏的时候那个提示信息
有相当一部分是memory overflow
其实就是溢出错误
这在操作上是不允许所以它就蓝屏了
我们还有很大的概率
遇到另外一种蓝屏的错误是什么呢
就是越界访问
这种错误的发生也是刚才这个机制
一旦溢出
堆栈弹出的时候
它认为我应该把这段存储器里头的值
作为我函数的返回值
但是因为堆栈已经溢出了
这段存储器可能被某一个
别的全局变量加以使用
于是这个值被错误的取出来
当成函数的返回值
所以PC指针寄存器指向了
存储器一个莫名其妙的空间
而且试图把它里头值拿出来进行执行
这个时候我们称为程序跑飞了
对于操作系统
它可能认为这是越界访问
不符合安全规定就蓝屏了
在有些时候嵌入式系统
可能认为是非法指令 程序无法解释
于是程序就复位了
那么这些错误
在我们日后的学习调试中都会遇到
那么我们现在给大家讲这个概念
就是你们一定慢慢的学习一个东西
掌握一些他高阶的知识
最后讲一个有趣味性的东西
我们很多同学都遇到
比如说苹果手机的越狱
一些游戏机的破解
其实很多时候都是用程序里头存在的这样的漏洞
比如很著名的
任天堂的Wii游戏机 我印象非常深刻
他有一代游戏机的越狱或者破解
用的方法就是玩一个《塞尔达传说》的游戏
这个游戏的破解方法就是你要加载一个进度
让这个人走到一个特定的位置
然后这个时候呢
这个存档里头把这个人的身上带的
比如说钱或者道具的数目改成一个特定的值
这个时候你再去执行
你会发现整个系统就找到了漏洞
内部函数就被接管了
那它的机制是什么呢
就是在这个地方有堆栈的溢出
通过改这个道具这个全局变量的值
把它改成了一个函数的入口地址
导致这个函数在进行返回的时候
由于变量的互相之间溢出和覆盖
把这样一个特殊的值跳转
跳出了我们原来的用户程序而进入了内核
获得了破解以后的不应该拥有的权限
所以这也是信息安全当中的一个漏洞
那么学习堆栈我们加深了对于CPU的一个理解
回到我们CPU本身就是这样一张完整的图
那么我们如果现在再来看这张图
我们就会有了一个非常清晰的概念
对于它每一个单元如何运行已经认识的非常清楚
而在这个单元里头
我们会发现其实我们作为一个学习者
老师讲的这些东西可能更多的是概念故事
那么我们深入理解的是什么呢
对于编程者
你其实感受不到当中
它的内部结构的这些运行和电路的设计
因为我们不是CPU设计课程
你能感受到的事情是两件事
一件事情是泛泛层面上如果进行汇编语言的编程
进行C语言编程的调试
你会感觉到这若干个寄存器的存在
比如说你会感觉到堆栈指针寄存器
PC的这个程序计数器
或者PC指针寄存器
你会感觉到若干个通用数据寄存器
你会感受到它的运算值往外送
往寄存器送的这个过程
以及刚才说的这些标志位
所以我们往往对于任何一个通用的CPU
一个complete的一个完整的CPU
不管是谁家的
是ARM还是不是ARM
是Power PC还是0812
都会把它里头的寄存器这些东西提炼出来
我们称为寄存器组
英文叫register file
那么从更大的意义上来讲
我们会把这些东西称为我们的programmer model
也就我们的编程模型
作为一个汇编语言的编程人员
你学习CPU的时候你找到它的手册
比如说上完咱们这门课
日后你要学一个CPU
你非常快的看清楚这个CPU有哪些寄存器是编程的对象
也就它的编程模型
你就对它掌握了一半
那另一半呢
另一半就是这个逻辑运算单元所对应的指令集
它有哪些指令分别代表什么含义
所以如果你充分理解了指令集和寄存器组
这些东西组合起来
你就理解了一个CPU
那么触类旁通
万变不离其宗
大家学习这些知识以后
你可以学习任何一种嵌入式CPU的基本构架
那么我们来看一看几种现实中存在的CPU
非常简短的建立一个概念
而在下一个单元我们会进行一些实际的操作
比如这是08和S08的非常著名的八位的微控制器MCU
它是我们之前所讲过的6800和6502系列CPU的直系后代
架构非常的相似
也就说我们刚才所说任天堂这些游戏机的CPU的后代
是世界上历史上销量第一大的CPU 累计卖了十几亿片
那么这个CPU它的结构非常的简单
它有一个8比特的D data寄存器
然后有一个16比特的地址寄存器
数据寄存器只能存数
地址寄存器只能存对外部存储器访问的地址值
那么大家可以理解它是个指针对吧
另外还有两个地址寄存器
一个就是我们刚才所说到的PC指针寄存器
存当前指令执行到存储器的哪个地址了
还有一个堆栈指针寄存器
存当前把存储器的哪一段内存空间当做堆栈来使用
我们压数的时候往哪个地址上放
然后最后一个长长的8个比特的条件码寄存器
这里头有得0得1 进位借位等等
那么还有一些中断的开关在里头
那么这样一个简简单单的寄存器组
就是一个8比特MCU
那么它的指令集当然也非常简单
但是我们这么一看我们就知道
哦 八位08的MCU
它的CPU就是这样的
汇编怎么写就对他们进行编程
那么16位呢
它一脉相承的下一代
从68HC11一直到12 S12这样一脉相承
包括我们大学生智能车比赛
曾经长达10年一直到现在
都大家还有人使用的16位的MCU微控制器12系列
它的内部结构就这样的
一个16比特数据寄存器
我们称为D
根据需要呢
它有两个别名
前八个比特后八个比特
可以分为 分别叫A和B
拆开来使用作为两个8比特
然后呢 它有两个寻址寄存器
X和Y
可以保存两个16比特的外部存储地址 指针对吧
同样它有个PC指针寄存器
一个堆栈指针寄存器和一个完整的标志位
所以大家可以明显感到CPU就是这么进化的
从八位到十六位变得
更强大 更复用
数据和地址寄存器变得更多
那么把它加以抽象
我们就会看到这样一个模型
就是我们的逻辑运算单元
可以对两个暂存数据的A和B
或者合起来一个D的数据寄存器拿数据
也可以在X Y这样的寻址寄存器下
对外部的存储器来读取存储器里的数据
而这些数据的通路
我们抽象成了这里的这些箭头
我们日后会说它是一个总线的概念
我们既可以通过地址从存储器拿数据直接给CPU用
也可以通过地址拿数据
放到我们寄存器里来做中间的运算
运算的结果可以放回寄存器
也可以直接写回存储器
还是我们原来那个概念
那么在下一个单元呢我们将跳出这些模型
看看一段真正的程序是如何在里头给运行起来的
那么大家又会觉得豁然开朗
-1.1 课程概览
--Video
-1.2 进入嵌入式系统的世界
--Video
-1.3 如何学好嵌入式系统
--Video
-2.1 计算机的基本概念、发展历史
--Video
-2.2 从晶体管到CPU
--Video
-2.3 概念CPU、微控制器MCU和嵌入式系统
--Video
-2.4 八卦计算机史
--Video
-2.5 不同领域、不同系列的嵌入式系统
--Video
-2.6 ARM历史与MKL25Z128 MCU
--Video
-3.1 CPU的基本结构和运行机制
--Video
-3.2.1 堆栈的概念
--Video
-3.2.2 堆栈的概念-头脑体操
--Video
-3.3.1 ARM的体系结构
--Video
-3.3.2 ARM的体系结构-头脑体操
--Video
-3.4 中断的概念和机制
--Video
-3.5 中断子程的概念和编程
--Video
-3.6 复位、时钟、存储器和总线
--Video
-3.7 小结:MCU的总体结构和程序运行机制
--Video
-4.1 第一种外设:IO
--Video
-4.2 IO外设的编程实操-点亮LED
--Video
-4.3 IO外设的进阶知识
--Video
-4.4 嵌入式开发的基本概念与工具链
--Video
-4.5 嵌入式开发的进阶知识
--Video
-4.6 嵌入式开发中的C语言(上)
--Video
-4.7 嵌入式开发中的C语言(下)
--Video
-E0.1 实验零 开发板的初步认识与工具链的安装
--Video
-E0.2 实验零 体验一个例程的编译与下载
--Video
-E0.3 实验零 编写第一个程序:点亮核心板LED
--Video
-E1 实验一 点灯秘籍
--Video
-5 智能车视角的嵌入式设计
--Video