进行8086汇编的介绍以前,想先分享一下我学习汇编的心路历程 。java
其实我并无想到这么快的就须要进一步学习汇编语言,由于汇编对于个人当前的工做内容来讲太过底层。node
但在几个月前,当时我正尝试着阅读rocketmq的源码。和许多流行的java中间件、框架同样,rocketmq底层的网络通讯也是经过netty实现的。但因为我对netty并不熟悉,在工做中使用spring-cloud-gateway的时候甚至写出了一些致使netty内存泄漏的代码,却不太明白个中原理 。出于我我的的习惯,在学习源码时,抛开总体的程序架构不论,但愿至少能对其中涉及到的底层内容有一个大体的掌握,能让我像黑盒子同样去看待它们。nginx
趁热打铁,我决定先学习netty,这样既能在工做时更好的定位、解决netty相关的问题,又能在研究依赖netty的开源项目时更加驾轻就熟。程序员
随着对netty学习的深刻,除了感叹netty统一规整的api接口设计,内部交互灵活可配置、同时又提供了足够丰富的开箱即用组件外;更进一步的,netty或者说java nio涉及到了许多更底层的东西,例如:io多路复用,零拷贝,事件驱动等等。而这些底层技术在redis,nginx,node-js等以高效率io著称的应用中被普遍使用。web
扪心自问,本身在多大程度上理解这些技术?为何io多路复用在io密集型的应用中,效率可以比之传统的同步阻塞io显著提升?一次网络或磁盘的io传输内部到底发生了什么,零拷贝到底快在了哪里?redis
若是没有很好的弄明白这些问题,那么个人netty学习将是不完整的。spring
我有限的知识告诉我,答案就在操做系统中。操做系统做为软硬件的大管家,对上提供应用程序接口(程序员们一般使用高级语言提供的api间接调用);对下控制硬件(cpu、内存、磁盘网卡等外设);依赖硬件提供控制并发的系统原语;其牵涉的许多模块内容都已经独立发展了(多系统进程间通讯->计算机网络、文件系统->数据库)。要想理解现代计算机系统的工做原理,操做系统是绝对绕不开的。数据库
虽然也上过操做系统的课,读过几本操做系统的书。但一方面因为缺少实际场景的应用,另外一方面也由于当时水平有限,学习操做系统的方式是经过完成一个个孤立简单的实验,而不是连贯的实现一个完整的demo操做系统。使得对许多关键的知识点的理解依然是模糊不清的,因此也没法很好地回答上述netty学习中碰到的问题。编程
在从新学习操做系统的过程当中,除了捡起当初没有看完的《现代操做系统》外,我惊喜的发现了清华大学的操做系统公开课(本身动手实现ucore操做系统),以及《OrangeOS 一个操做系统的实现》。api
但操做系统的学习从一开始我就遇到了大问题,从零开始实现的操做系统,虽然内核主体是C语言实现的,但在CPU加电开机时的引导程序以及在特定平台上操做特定硬件的功能却都须要经过汇编来实现(ucore和OrangeOS都是基于Intel-80386的(32位)),看的我是一头雾水,很是郁闷。
因为在学校里学习的汇编语言是囫囵吞枣的,没法支持继续操做系统的学习实践。我找到了王爽老师编写的《汇编语言》进行学习,虽然《汇编语言》使用的是更早的基于Intel-8086(16位)机器的汇编语言进行讲解,但因为Intel的CPU迭代是向前兼容的(x86体系),所以其知识也可以适用于更先进的Intel-80386。
对于像我这样的汇编语言初学者,学习简单经典的8086汇编可以为理解更复杂的汇编语言打下基础。经过《汇编语言》这本书的学习,加深了我对诸如内存寻址,中断,指令跳转等硬件工做原理的理解,可以让我从更底层的角度去看待上层的一些技术。
这一段时间,我进行了相似递归的,由上层至底层的学习。在初步完成了8086汇编语言的学习后,我准备返回上层继续操做系统的学习。
经过写博客的方式来巩固这段时间汇编语言学习总结的成果,对知识点查漏补缺的同时也能做为汇编语言知识体系的索引让之后在有须要时能更好的进行回顾。若是能帮助到一样感兴趣的人就更好了( ^_^ )。
汇编语言做为编程语言的一种,虽然贴近机器底层,但和咱们熟悉的高级编程语言依然有诸多共通之处。站在更高的角度去看待汇编语言,能更好的去理解汇编。
编程语言一般由两部分组成:编程语言的基础语法以及操纵编程语言所处环境的api。
举个例子:
对于java,其基础语法部分包括变量/方法/类定义、循环/赋值、继承多态等;同时java做为一门面向通用计算机的编程语言,一般直接运行在操做系统之上,其多线程、系统io、网络传输、图形编程等api使得java开发人员可以更简单的使用操做系统。
对于javaScript,基础语法部分由EcmaScript规范构成;而javaScript做为运行在web浏览器环境中的语言,提供了操做BOM、DOM对象的api,使得js的开发人员能控制浏览器的行为,实现所须要的功能。
对于汇编语言,状况又是什么呢?
一方面,汇编语言的语法部分大体包括指令的格式,注释,定义数据、代码段等的伪指令等。
另外一方面,汇编语言是面向CPU硬件编程的,其指令与最终的机器码一一对应,但比起以二进制表示的机器码可读性要高不少。
举个例子,机器指令:1000100111011000 其对应的汇编语言表示为:mov ax bx,表示将寄存器bx的值送入寄存器ax。对比一下,表示一样的内容,汇编语言的可读性比机器语言要高不少。而机器最终执行的是机器码,须要由汇编器将汇编源程序转换成最终的机器码。
汇编语言提供了直接操做CPU寄存器的指令(各类寄存器的取值、赋值)、控制CPU内存寻址的指令(内存单元的取值、赋值)、控制CPU经过端口操做外设的指令以及控制CPU进行程序跳转的指令等等。
8086汇编的语法和硬件指令的内容会在后续的博客中,进行更加详细的说明。
汇编语言是用来操做CPU硬件的,汇编语言与其对应的硬件紧密相关。所以,在学习8086汇编语言以前,咱们须要先大体了解一下8086硬件的工做原理(以黑盒子的角度来看待,而不是去深刻的研究硬件内部复杂的结构)。
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 程序状态字寄存器 |
千万别一会儿被繁多的寄存器弄糊涂了,后续会在有须要时进行上述寄存器的详细介绍和用法的。
在计算机中,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经过总线与外部存储器交互的方式。
(图片源自 《汇编语言》 王爽著)
前面提到每一个存储器单元都有惟一的标识,这个惟一标识被称为物理地址。
8086的地址总线是20位的,拥有1MB的内存寻址能力。但8086的寄存器却只有16位,单次处理的数据最多也是16位,若是简单的将寻址地址送出,那么最多只能寻址2的16次方,也就是64KB的地址空间。
为此,8086内部经过将两个16位的寻址地址叠加为一个20位地址的方式实现物理地址寻址。
其中一个寻址地址称为段地址,另外一个寻址地址被称为偏移地址;16位的段地址左移4位(扩大16倍)后将其和偏移地址相加获得最终的物理寻址地址。
举个例子:
段地址 = 1234 (16进制 0x1024)
偏移地址 = 1000 (16进制 0x1000)
最终物理地址 = (段地址 * 16) + (偏移地址) = 13340 (16进制 0x13340)
这里引入了内存段的概念,"段"这一律念在8086汇编中很是重要,从寄存器中专门存在一类段寄存器可见一斑,这里就不继续展开了。
(图片源自 《汇编语言》 王爽著)
相信不少人都多少对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寄存器中的值(例如jmp、call、ret指令等),这类指令被统称为跳转指令。有了跳转指令,就能够在实现逻辑分支的跳转、循环以及函数子程序的调用,返回等功能。
上述解释依然是很简陋、不彻底的,诸如如何实现函数返回时参数的传递、返回后以前变量的恢复等等更细节的问题都尚未给出答案。限于篇幅不会在这里回答这些问题。咱们能够带着这些问题进行接下来的学习,随着学习的深刻,相信这些问题的答案会慢慢浮出水面。就我我的而言,若是带着问题去学习,会更加兴致高昂,经过努力将感兴趣却还不理解的地方弄懂是一件颇有成就感的事情。
做为8086汇编语言学习的第一篇博客,这里仅仅把学习8086汇编所须要的部分基础知识走马观花的简单介绍了一下,不少知识点只起了个头就没后续了,会在后续的博客里继续分享8086学习的内容。
做为汇编语言的初学者,博客中存在理解有问题的地方还请多多指教。但愿对汇编语言或是计算机底层原理感兴趣的小伙伴有所帮助。