8086汇编语⾔学习(⼀)8086汇编介绍
1. 学习汇编的⼼路历程
进⾏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硬件编程的,其指令与最终的机器码⼀⼀对应,但⽐起以⼆进制表⽰的机器码可读性要⾼很多。
举个例⼦,机器指令:1000100111011000 其对应的汇编语⾔表⽰为:mov ax bx,表⽰将寄存器bx的值送⼊寄存器ax。对⽐⼀下,表⽰同样的内容,汇编语⾔的可读性⽐机器语⾔要⾼很多。⽽机器最终执⾏的是机器码,需要由汇编器将汇编源程序转换成最终的机器码。
汇编语⾔提供了直接操作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中。
零基础学java编程 这⾥⼯作原理的解释很模糊,但⼤致说明了CPU通过总线与外部存储器交互的⽅式。
(图⽚源⾃《汇编语⾔》王爽著)
3.3 内存单元物理地址
前⾯提到每个存储器单元都有唯⼀的标识,这个唯⼀标识被称为物理地址。
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汇编中⾮常重要,从寄存器中专门存在⼀类段寄存器可见⼀斑,这⾥就不继续展开了。
(图⽚源⾃《汇编语⾔》王爽著)
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寄存器中的值(例如jmp、call、ret指令等),这类指令被统称为跳转指令。有了跳转指令,就可以在实现逻辑分⽀的跳转、循环以及函数⼦程序的调⽤,返回等功能。
上述解释依然是很简陋、不完全的,诸如如何实现函数返回时参数的传递、返回后之前变量的恢复等等更细节的问题都还没有给出答案。限于篇幅不会在这⾥回答这些问题。我们可以带着这些问题进⾏接下来的学习,随着学习的深⼊,相信这些问题的答案会慢慢浮出⽔⾯。就我个⼈⽽⾔,如果带着问题去学习,会更加兴致⾼昂,通过努⼒将感兴趣却还不理解的地⽅弄懂是⼀件很有成就感的事情。
总结
作为8086汇编语⾔学习的第⼀篇博客,这⾥仅仅把学习8086汇编所需要的部分基础知识蜻蜓点⽔的简单介绍了⼀下,很多知识点只起了个头就没后续了,会在后续的博客⾥继续分享8086学习的内容。
作为汇编语⾔的初学者,博客中存在理解有问题的地⽅还请多多指教。希望对汇编语⾔或是计算机底层原理感兴趣的⼩伙伴有所帮助。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论