前言
说起自己读汇编,总有人会在问:读汇编有啥意义?读汇编对我的开发工作有帮助吗?...我觉得读汇编是为了让我们更好地有计算机工作原理方面的知识,不仅仅是一味地高屋建瓴。就拿 IDE 和 Linux gcc 来说,我们为什么要试着用 gcc 去编译链接并生成可执行程序,而不是点一下 Run 程序就能跑起来?原因还是在我们想探究程序运行的本质。
而汇编就是一门帮助我们更好地理解程序如何“跑起来”的语言,它最接近机器语言这门计算机唯一能识别的语言,深入数据在内存单元的存储,直接对内存单元进行操作,自然就涉及到 CPU 相关的运作等等等计算机核心部件,相信读了它,我们对操作系统能有更深入地认识。
CPU与数据
由于最终运行程序的是CPU,所以我们使用汇编语言编程的时候,最好从CPU的角度去考虑问题,这里谈谈存储器、三条总线、内存地址空间的概念。
- 存储器
- 想让CPU工作,就得给CPU提供指令和数据,而这些东西存在哪呢?存在内存器里,也就是我们常说的内存,这些内存被划分成多个存储单元,顺序编号,以方便CPU从里头拿出、放入数据。
- 三条总线
- 三条总线的目的是让CPU知道此时是该从哪儿拿出放入数据,而想要完成这一步操作,就得告诉CPU:在哪儿、干什么。
- 地址总线:将地址传输给CPU,告诉CPU在哪儿操作
- 控制总线:告诉CPU是读出数据还是写入数据
- 数据总线:传输数据
- 内存地址空间
- 什么叫内存地址空间?由于我们的地址总线数量是一定的,那代表了我们可得到的地址数值也是有限的,举个例子:地址总线数量为10,那么从编号为0到1024的内存单元可以被寻址到,而在往后的地址就无可奈何了。这1024个可被寻址的内存单元就构成了内存地址空间。
- 内存地址空间是一个逻辑存储器,因为它把不同的硬件内存都映射在一个空间里,占有不同的地址段,而在实际上硬件的内存位置是离散的。
- 我们在对内存地址空间进行操作的同时,实际上就是对相对应的物理存储器进行操作。
寄存器
寄存器是我们唯一能用指令读写的部件,它用来暂时存储数据、指令、地址,比内存存储要快、方便。规定它的名字即是它的地址。0806中寄存器有如下几种:
- 数据寄存器,用于存储数据,如AX(累加),BX(基址,可用于间接寻址,如[bx]),CX(技术),DX(暂存数据)
- 段地址寄存器,用于存储段地址,如CS(代码段),DS(数据段),SS(堆栈段)
- 指针寄存器,如IP(告诉CPU当前单元读取的是指令)、SP(当前指向的是栈顶)、BP(当前指向的是栈底)
- 标志寄存器,通过标志确定对应事件是否发生,如CF(中断是否发生)
写汇编程序我们需要知道数据存在哪、长度是多少,要对其进行怎样的操作,而这些步骤都要通过寄存器才能实现。
物理地址
编号后的内存地址空间中每一个内存单元都有唯一的地址,我们把这个唯一的地址称为物理地址。然而,8086是16位CPU,也就是说内部只能一次性处理、传输的信息是16位的,但是物理地址地址往往比16位数据要大,这该怎么办?
操作系统对16的CPU给出了计算物理地址的办法:段地址×16+偏移地址=物理地址
这是个很机智的做法,先确定一个起点,然后再以起点开始算它与目的地之间的距离,这么做的好处是只要你定一个规则,比如说:起点×10+偏移距离=目的地,就可以保证起点的位数也是足够小的。由于CPU中地址加法器可以处理进位,所以CPU可以一次性处理起点×10的数据。
段与段寄存器
段的概念来自于CPU中计算物理地址的公式:段地址×16+偏移地址=物理地址,实际上内存是一个个连续的内存单元,并不会分段。作为程序员,我们可以根据自身需要将一组内存定义为一个段。
两个点需要注意:
1.由于计算物理地址公式,段的地址肯定是16的倍数
2.偏移地址是16位,所以一个段的长度最大为 64KB
可能在之前的学习中,我们耳熟能详地就是代码段、数据段与栈段了,但是只知道代码段是存对应的代码、数据段是存数据、栈段用于存储程序中的局部变量,但是又想在内存中存储的是机器码,莫非对应每个机器码都要标记它是不是要执行代码、数据不成?
对于这个问题,读完汇编后才明白,我们可以将代码数据存放在代码段里,然后通过 CS:IP 指向某内存单元,该内存单元的数据将被当成指令读取并送入指令缓冲器,IP指向下一条指令,最后将缓冲器中的指令执行,如此循环,一个程序就跑起来了。
数据也是同理,用“DS:偏移地址 ”表示该单元存储的是数据。从这个角度上看,其实数据与程序本身的存储是没有区别的,判断是否是指令依靠的是IP,IP指向哪哪儿就成了指令。
由于我们需要数据的动态分配,所以引入了栈段,在编写程序时,往往数据段存储全局变量,而栈段存储局部变量。我们通过 ss 表示栈段,ss:sp 指向栈顶单元,ss:bp 表示单元。你可能会有疑问,我们可以通过 ss:0000 就知道栈的底部单元的物理地址了,为什么还需要bp?我的理解是利用 bp sp 可以动态模拟当前函数的内存,当函数返回上一层时,bp sp 又再次变化,指向当前函数的内存。