8086汇编语言学习(一) 8086汇编介绍
小熊餐馆 人气:11. 学习汇编的心路历程
进行8086汇编的介绍之前,想先分享一下我学习汇编的心路历程 。
rocketmq的学习
其实我并没有想到这么快的就需要进一步学习汇编语言,因为汇编对于我的当前的工作内容来说太过底层。
但在几个月前,当时我正尝试着阅读rocketmq的源码。和许多流行的java中间件、框架一样,rocketmq底层的网络通信也是通过netty实现的。但由于我对netty并不熟悉,在工作中使用spring-cloud-gateway的时候甚至写出了一些导致netty内存泄漏的代码,却不太明白个中原理 。出于我个人的习惯,在学习源码时,抛开整体的程序架构不论,希望至少能对其中涉及到的底层内容有一个大致的掌握,能让我像黑盒子一样去看待它们。
趁热打铁,我决定先学习netty,这样既能在工作时更好的定位、解决netty相关的问题,又能在研究依赖netty的开源项目时更加得心应手。
netty的学习
随着对netty学习的深入,除了感叹netty统一规整的api接口设计,内部交互灵活可配置、同时又提供了足够丰富的开箱即用组件外;更进一步的,netty或者说java nio涉及到了许多更底层的东西,例如:io多路复用,零拷贝,事件驱动等等。而这些底层技术在redis,nginx,node-js等以高效率io著称的应用中被广泛使用。
扪心自问,自己在多大程度上理解这些技术?为什么io多路复用在io密集型的应用中,效率能够比之传统的同步阻塞io显著提高?一次网络或磁盘的io传输内部到底发生了什么,零拷贝到底快在了哪里?
如果没有很好的弄明白这些问题,那么我的netty学习将是不完整的。
我浅薄的知识告诉我,答案就在操作系统中。操作系统作为软硬件的大管家,对上提供应用程序接口(程序员们通常使用高级语言提供的api间接调用);对下控制硬件(cpu、内存、磁盘网卡等外设);依赖硬件提供控制并发的系统原语;其牵涉的许多模块内容都已经独立发展了(多系统进程间通信->计算机网络、文件系统->数据库)。要想理解现代计算机系统的工作原理,操作系统是绝对绕不开的。
操作系统的学习
虽然也上过操作系统的课,读过几本操作系统的书。但一方面由于缺乏实际场景的应用,另一方面也因为当时水平有限,学习操作系统的方式是通过完成一个个孤立简单的实验,而不是连贯的实现一个完整的demo操作系统。使得对许多关键的知识点的理解依然是模糊不清的,所以也无法很好地回答上述netty学习中碰到的问题。
在重新学习操作系统的过程中,除了捡起当初没有看完的《现代操作系统》外,我惊喜的发现了清华大学的操作系统公开课(自己动手实现ucore操作系统),以及《OrangeOS 一个操作系统的实现》。
但操作系统的学习从一开始我就遇到了大问题,从零开始实现的操作系统,虽然内核主体是C语言实现的,但在CPU加电开机时的引导程序以及在特定平台上操作特定硬件的功能却都需要通过汇编来实现(ucore和OrangeOS都是基于Intel-80386的(32位)),看的我是一头雾水,非常郁闷。
汇编语言的学习
由于在学校里学习的汇编语言是囫囵吞枣的,无法支持继续操作系统的学习实践。我找到了王爽老师编写的《汇编语言》进行学习,虽然《汇编语言》使用的是更早的基于Intel-8086(16位)机器的汇编语言进行讲解,但由于Intel的CPU迭代是向前兼容的(x86体系),因此其知识也能够适用于更先进的Intel-80386。
对于像我这样的汇编语言初学者,学习简单经典的8086汇编能够为理解更复杂的汇编语言打下基础。通过《汇编语言》这本书的学习,加深了我对诸如内存寻址,中断,指令跳转等硬件工作原理的理解,能够让我从更底层的角度去看待上层的一些技术。
这一段时间,我进行了类似递归的,由上层至底层的学习。在初步完成了8086汇编语言的学习后,我准备返回上层继续操作系统的学习。
通过写博客的方式来巩固这段时间汇编语言学习总结的成果,对知识点查漏补缺的同时也能作为汇编语言知识体系的索引让以后在有需要时能更好的进行回顾。如果能帮助到同样感兴趣的人就更好了( ^_^ )。
2.汇编语言基本介绍
汇编语言作为编程语言的一种,虽然贴近机器底层,但和我们熟悉的高级编程语言依然有诸多共通之处。站在更高的角度去看待汇编语言,能更好的去理解汇编。
编程语言通常由两部分组成:编程语言的基础语法以及操纵编程语言所处环境的api。
举个例子:
对于java,其基础语法部分包括变量/方法/类定义、循环/赋值、继承多态等;同时java作为一门面向通用计算机的编程语言,通常直接运行在操作系统之上,其多线程、系统io、网络传输、图形编程等api使得java开发人员能够更简单的使用操作系统。
对于javaScript,基础语法部分由EcmaScript规范构成;而javaScript作为运行在web浏览器环境中的语言,提供了操作BOM、DOM对象的api,使得js的开发人员能控制浏览器的行为,实现所需要的功能。
对于汇编语言,情况又是什么呢?
一方面,汇编语言的语法部分大致包括指令的格式,注释,定义数据、代码段等的伪指令等。
另一方面,汇编语言是面向CPU硬件编程的,其指令与最终的机器码一一对应,但比起以二进制表示的机器码可读性要高很多。汇编语言提供了直接操作CPU寄存器的指令(各种寄存器的取值、赋值)、控制CPU内存寻址的指令(内存单元的取值、赋值)、控制CPU通过端口操作外设的指令以及控制CPU进行程序跳转的指令等等。
8086汇编的语法和硬件指令的内容会在后续的博客中,进行更加详细的说明。
3.8086硬件介绍
汇编语言是用来操作CPU硬件的,汇编语言与其对应的硬件紧密相关。因此,在学习8086汇编语言之前,我们需要先大致了解一下8086硬件的工作原理(以黑盒子的角度来看待,而不是去深入的研究硬件内部复杂的结构)。
3.1 CPU寄存器
CPU通常由运算器,控制器和寄存器组成;运算器和控制器的工作一般无法直接控制,但寄存器却能够通过汇编语言直接与之交互。
8086CPU中有14个寄存器,各自都有着特殊的功能,我们可以通过汇编语言将其协调起来,满足我们的需求
寄存器可以分为三大类,分别是:
通用寄存器 | 段寄存器 | 特殊功能寄存器 |
ax accumulate-register 累加寄存器 | cs code-segment 代码段寄存器 | si source-index 源变址寄存器 |
bx based-register 基地址寄存器 | ds data-egment 数据段寄存器 | di destination-index 目的变址寄存器 |
cx count-register 计数寄存器 | ss stack-segment 栈段寄存器 |
sp stack-point 堆栈指针寄存器 |
dx data-register 数据寄存器 | es extra-segment 附加段寄存器 | bp base-point 基础指针寄存器 |
ip instructor-point 指令指针寄存器 | ||
psw program-state-word 程序状态字寄存器 |
千万别一下子被繁多的寄存器弄糊涂了,后续会在有需要时进行上述寄存器的详细介绍和用法的。
3.2 CPU和存储器的交互
在计算机中,CPU作为处理器通常不能独自进行工作,还需要与外部存储器(内存 RAM、ROM)进行交互来读写所需要执行的代码指令或数据。
8086 CPU通过逻辑上分为三类的地址总线、数据总线和控制总线共同完成与存储器交互的任务,。
总线由一系列的导线组成,通常高电平表示1,低电平表示0,数量为N的总线集合可以表示一个N位的二进制数。
其中地址总线用于确定存储器的地址,数据总线用于在对应存储器地址和寄存器之间传输数据,而控制总线则可以标识当前所进行的控制操作(读或是写或是其它指令)。
地址总线:
存储器在设计时,被划分为多个存储单元,每个存储单元都有独一无二的地址标识。CPU可以通过这些地址标识来定位对应的存储单元,这叫做内存寻址。
CPU的内存寻址范围由地址总线的根数(位数)决定,20位的地址总线能寻址的最大范围为(2 ^ 20)b = 1M;而32位的地址总线能寻址的最大范围为(4 * (2 ^ 30))b = 4G,这也是在32位CPU时代,PC的内存普遍是4G的主要原因。
数据总线:
CPU与内存或其它器件的数据传输是通过数据总线来完成的。数据总线的位数决定了一次数据传输的数据大小,数据总线的位数越多,数据传输的效率就越高。
8086作为一个16位的CPU,内部寄存器是16位的,其数据总线也是16位的,其一次可以传输一个16位的二进制数据。
控制总线:
CPU通过控制总线来对外部设备实施控制。和前两种总线不同的是,控制总线是不同的控制线的总集合,其中的每一根导线通常是单独提供控制的。CPU通过控制总线发送控制信号和时许信号来对外围设备进行控制(读、写信号等)或者从控制总线接收外围设备发出的通知(中断申请信号 等)。
以8086 CPU从指定内存地址中读取数据为例简单说明CPU总线的工作原理:
首先,CPU通过地址总线发送内存地址选取信号。
然后,CPU通过控制总线发送"读"信号通知内存芯片将要读取数据,而具体被选中的内存芯片由地址总线信号指定。
最后,CPU通过数据总线将对应内存单元中的数据送入CPU中。
这里工作原理的解释很模糊,但大致说明了CPU通过总线与外部存储器交互的方式。
(图片源自 《汇编语言》 王爽著)
3.3 内存单元物理地址
前面提到每个存储器单元都有唯一的标识,这个唯一标识被称为物理地址。
8086的地址总线是20位的,拥有1MB的内存寻址能力。但8086的寄存器却只有16位,单次处理的数据最多也是16位,如果简单的将寻址地址送出,那么最多只能寻址2的16次方,也就是64KB的地址空间。
为此,8086内部通过将两个16位的寻址地址叠加为一个20位地址的方式实现物理地址寻址。
其中一个寻址地址称为段地址,另一个寻址地址被称为偏移地址;16位的段地址左移4位(扩大16倍)后将其和偏移地址相加得到最终的物理寻址地址。
举个例子:
段地址 = 1234 (16j进制 0x1024)
偏移地址 = 1000 (16进制 0x1000)
最终物理地址 = (段地址 * 16) + (偏移地址) = 13340 (16进制 0x13340)
这里引入了内存段的概念,"段"这一概念在8086汇编中非常重要,从寄存器中专门存在一类段寄存器可见一斑,这里就不继续展开了。
(图片源自 《汇编语言》 王爽著)
3.4 CPU执行程序的基本过程
相信很多人都多少对CPU执行程序的原理感到好奇。对于平常再熟悉不过的程序中的if、else逻辑判断,for、while循环以及函数的调用(call)、返回(return)机制在以图灵机为模型的机器中是如何实现的呢?在存储器中数据都是以010101的二进制形式存在的,可是CPU是如何区分在程序中通常是泾渭分明的代码和数据的呢?换句话说,CPU是如何知道应该把0101这样的的二进制"数据"当做代码执行还是视作数据处理呢?
想知道答案,需要先了解一下前面提到的8086寄存器中的CS(代码段寄存器)和IP(指令指针寄存器)这两个或许是最重要的寄存器了,CS/IP两个寄存器
1. CPU在每次执行指令时,都会去读取CS:IP所指向内存单元的"数据",将其当做指令来执行。(CS : IP 其中前面的CS代表段地址,后面的IP代表偏移地址)。
2. 在指令执行完毕后,IP值会增加,增加的值取决于之前加载指令的长度(8086的指令一般需要1-3个字节),这样CS:IP就能正确的指向下一条需要执行的指令了。
3. CPU会不断的重复执行(1)、(2)这两个步。CPU能以非常快的速度执行这一运算过程,这一般取决于CPU的主频。
程序通常都不是线性的、自始至终从上至下执行的,而是存在各种分支判断来决定最终执行的程序片段。为此,CPU提供了许多指令让我们能够修改CS和IP寄存器中的值(例如jump、call、ret指令等),这类指令被统称为跳转指令。有了跳转指令,就可以在实现逻辑分支的跳转、循环以及函数子程序的调用,返回等功能。
上述解释依然是很简陋、不完全的,诸如如何实现函数返回时参数的传递、返回后之前变量的恢复等等更细节的问题都还没有给出答案。限于篇幅不会在这里回答这些问题。我们可以带着这些问题进行接下来的学习,随着学习的深入,相信这些问题的答案会慢慢浮出水面。就我个人而言,如果带着问题去学习,会更加兴致高昂,通过努力将感兴趣却还不理解的地方弄懂是一件很有成就感的事情。
总结
作为8086汇编语言学习的第一篇博客,这里仅仅把学习8086汇编所需要的部分基础知识蜻蜓点水的简单介绍了一下,很多知识点只起了个头就没后续了,会在后续的博客里继续分享8086学习的内容。
作为汇编语言的初学者,博客中存在理解有问题的地方还请多多指教。希望对汇编语言或是计算机底层原理感兴趣的小伙伴有所帮助。
加载全部内容