x86汇编语言:从实模式到保护模式(txt+pdf+epub+mobi电子书下载)


发布时间:2020-09-25 22:37:43

点击下载

作者:李忠,王晓波,等

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

x86汇编语言:从实模式到保护模式

x86汇编语言:从实模式到保护模式试读:

内容简介

本书采用开源的NASM 汇编语言编译器和VirtualBox 虚拟机软件,以个人计算机广泛采用的Intel处理器为基础,详细讲解了Intel 处理器的指令系统和工作模式,以大量的代码演示了16/32/64 位软件的开发方法,集中介绍处理器的16 位实模式和32 位保护模式,以及基本的指令系统。

这是一本有趣的书,它没有把篇幅花在计算一些枯燥的数学题上。相反,它教你如何直接控制硬件,在不借助于BIOS、DOS、Windows、LINUX 或任何其他软件支持的情况下来显示字符、读取硬盘数据、控制其他硬件等。本书可作为大专院校相关专业学生和计算机编程爱好者的教程。未经许可,不得以任何方式复制或抄袭本书之部分或全部内容。版权所有,侵权必究。

图书在版编目(CIP)数据

x86 汇编语言:从实模式到保护模式/李忠,王晓波,余洁著. —北京:电子工业出版社,2013.1

ISBN 978-7-121-18799-5

Ⅰ.①x… Ⅱ.①李…②王…③余… Ⅲ.①汇编语言—程序设计 Ⅳ.①TP313

中国版本图书馆CIP 数据核字(2012)第253290 号

责任编辑:董亚峰  特约编辑:王 纲

印  刷:

装  订:

出版发行:电子工业出版社

     北京市海淀区万寿路173信箱 邮编 100036

开  本:787×1092 1/16 印张:24.25 字数:620千字

印  次:2013年1月第1次印刷

定  价:56.00元

凡所购买电子工业出版社图书有缺损问题,请向购买书店调换。若书店售缺,请与本社发行部联系,联系及邮购电话:(010)88254888。

质量投诉请发邮件至zlts@phei.com.cn,盗版侵权举报请发邮件至dbqq@phei.com.cn。

服务热线:(010)88258888。前 言

尽管汇编语言也是一种计算机语言,但却是与众不同的,与它的同类们格格不入。处理器的工作是执行指令并获得结果,而为了驾驭处理器,汇编语言为每一种指令提供了简单好记、易于书写的符号化表示形式。

一直以来,人们对于汇编语言的认识和评价可以分为两种,一种是觉得它非常简单,另一种是觉得它学习起来非常困难。

你认为我会赞同哪一种?说汇编语言难学,这没有道理。学习任何一门计算机语言,都需要一些数制和数制转换的知识,也需要大体上懂得计算机是怎么运作的。在这个前提下,汇编语言是最贴近硬件实体的,也是最自然和最朴素的。最朴素的东西反而最难掌握,这实在说不过去。因此,原因很可能出在我们的教科书上,那些一上来就搞一大堆寻址方式的书,往往以最快的速度打败了本来激情高昂的初学者。

但是,说汇编语言好学,也同样有些荒谬。据我的观察,很多人掌握了若干计算机指令,会编写一个从键盘输入数据,然后进行加减乘除或者归类排序的程序后,就认为自己掌握了汇编语言。还有,直到现在,我还经常在网上看到学生们使用DOS 中断编写程序,他们讨论的也大多是实模式,而非32 位或者64 位保护模式。他们知道如何编译源程序,也知道在命令行输入文件名,程序就能运行了;又或者使用一个中断,就能显示字符。至于这期间发生了什么,程序是如何加载到内存中的,又是怎么重定位的,似乎从来不关汇编语言的事。这样做的结果,就是让人以为汇编语言不过如此,而且非常枯燥。

很难说我已经掌握了汇编语言的要义。但至少我知道,尽管汇编语言不适合用来编写大型程序,但它对于理解计算机原理很有帮助,特别是处理器的工作原理和运行机制。就算是为了这个目的,也应该让汇编语言回归它的本位,那就是访问和控制硬件(包括处理器),而不仅仅是编写程序,输入几个数字,找出正数有几个、负数有几个,大于30 的有几个。

事实上,汇编语言对学习和理解高级语言,比如C 语言,也有极大的帮助。老教授琢磨了好几天,终于想到一个好的比喻来帮助学生理解什么是指针,实际上,这对于懂得汇编语言的学生来说,根本就不算个事儿,并因此能够使老教授省下时间来喝茶。

在这本书之前,我也写过《穿越计算机的迷雾》一书。它们是一个系列,没有基础的读者可以先看那本书,打一点计算机原理的基础再来学习汇编语言。

在计划写这本书的时候,我就给自己画了几条线。首先不能走老路,一上来就讲指令、寻址方式,而是采用任务驱动的方式来写,每一章都要做点事情,最好是比较有趣,有吸引力。在解决问题的过程中,不断地引入新指令,并进行讲解。一句话,我希望是润物细无声式的;其次,汇编语言和硬件并举,完全抛弃BIOS 中断和DOS 中断,直接访问硬件,发挥汇编语言的长处,因为这才是我们学习汇编语言的目的。也只有这样,读者才能深刻体会到汇编语言的妙处。

王晓波和湖北经济学院的余洁共同参与了本书的创作。

本书主要讲述INTEL x86 处理器的16 位实模式、32 位保护模式,至于虚拟8086 模式,则是为了兼容传统的8086 程序,现在看来已经完全过时,不再进行讲述。本书的特色之一是提供了大量典型的源代码,这些代码以及相配套的工具程序可以到书中指定的网站,或者电子工业出版社华信教育资源网搜索下载。

很多读者在读书的时候会遇到这种情况:一开始读得很快,一口气读了好几章。随着内容的深入,学习越来越吃力,不得不频繁回到前面重新学习已经讲过的内容,这就是因为前面的知识没有完全掌握。为此,本书每一章都设有检测点,读者应当在通过检测点之后再继续往后阅读。

本书原来有18 章,后来考虑到实模式的内容过多,而去掉了一章。这一章的标题是《聆听数字的声音》,讲述如何通过直接访问和控制Sound Blaster 16 声卡来播放声音,感兴趣的朋友可以从下载的配书文件包中找到这部分内容。

在本书的写作和出版过程中,长春电视台台长王志强,副台长周武军和技术部主任刘贵先后对本书给予了关心和支持,在此表示衷心的感谢。

好友王南洋、桑国伟、刘维钊、蒋胜友、邱海龙、万利、李文心等负责了本书的一部分校对工作;好友周卫平帮我验证配书代码是否能在他的机器上正常工作,在这里向他们表示感谢,同时也谢谢所有关心和支持本书的朋友们。

感谢我的母亲、我的妻子和我的女儿,她们是我的精神支柱,是我努力创作这本书的动力来源。

在阅读本书的过程中,如果有任何问题,可以往电子邮件地址leechung@126.com 给我写信;要了解其他更多的情况,请访问我的博客:http://blog.163.com/leechung@126。二〇一二年十一月第1部分 预备知识

了解数制的基本知识和数制转换的方法。

了解8086处理器的结构和工作方式,初步认识所谓的针对处理器编程,是针对处理器的哪些部件和哪些方面进行的,理解分段的原理。

了解什么是汇编语言,以及如何书写、编译汇编语言源程序,掌握在虚拟机上运行程序的方法。第1章 十六进制计数法

电子计算机,顾名思义,就是计算的机器。因此,学习汇编语言,就不可避免地要和数字打交道。在这个过程中,我们要用到三种数制:十进制(这是我们再熟悉不过的)、二进制和十六进制。本章的目标是:

1.熟悉后两种数制,了解这两种数制的计数特点。

2.能够在这三种数制之间熟练地进行转换,特别是看到一个二进制数时,能够口算出它对应的十六进制数,反之亦然。

3.对于0~15 之间的任何一个十进制数,能够立即说出它对应的二进制数和十六进制数。1.1 二进制计数法回顾1.1.1 关于二进制计数法

在《穿越计算机的迷雾》那本书里我们已经知道,计算机也是一台机器,唯一不同的地方在于它能计算数学题,且具有逻辑判断能力。

与此同时,我们也已经在那本书里学到,机器在做数学题的时候,也面临着一个如何表示数字的问题,比如你采用什么办法来将加数和被加数送到机器里。

同样是在那本书里,我们揭晓了答案,那就是用高、低两种电平的组合来表示数字。如图1-1所示,参与计算的数字通过电线送往计算机器,高电平被认为是“1”,低电平被认为是“0”,这样就形成了一个序列“11111010”,这就是一个二进制数,在数值上等于我们所熟知的二百五,换句话说,等于十进制数250。图1-1 在计算机里,二进制数字对应着高低电平的组合

从数学的角度来看,二进制计数法是现代主流计算机的基础。一方面,它简化了硬件设计,因为它只有两个符号“0”和“1”,要得到它们,可以用最少的电路元件来接通或者关断电路就行了;另一方面,二进制数与我们熟悉的十进制数之间有着一对一的关系,任何一个十进制数都对应着一个二进制数,不管它有多大。比如,十进制数5,它所对应的二进制数是101,而十进制数5785478965147则对应着一长串“0”和“1”的组合,即1010100001100001001011010110010011110011011。

组成二进制数的每一个数位,称为一个比特(bit),而一个二进制数也可以看成是一个比特串。很明显,它的数值越大,这个比特串就越长,这是二进制计数法不好的一面。1.1.2 二进制到十进制的转换

每一种计数法都有自己的符号(数符)。比如,十进制有0、1、2、3、4、5、6、7、8、9 这十个符号;二进制呢,则只有0、1 这两个符号。这些数字符号的个数称为基数。也就是说,十进制有10 个基数,而二进制只有两个。

二进制和十进制都是进位计数法。进位计数法的一个特点是,符号的值和它在这个数中所处的位置有关。比如十进制数356,数字6 处在个位上,所以是“6 个”;5 处在十位上,所以是“50”;3 处在百位上,所以是“300”。即:210百位3、十位5、个位6=3×10+5×10+6×10=356

这就是说,由于所处的位置不同,每个数位都有一个不同的放大倍数,这称为“权”。每个数位的权是这样计算的(这里仅讨论整数):从右往左开始,以基数为底,指数从0 开始递增的幂。正如上面的公式所清楚表明的那样,“6”在最右边,所以它的权是以10 为0底,指数为0 的幂10;而3 呢,它的权则是以10 为底,指数为2 的2幂10。

上面的算式是把十进制数“翻译”成十进制数。从十进制数又算回到十进制数,这看起来有些可笑,注意这个公式是可以推广的,可以用它来将二进制数转换成十进制数。

比如一个二进制数10110001,它的基数是2,所以要这样来计算与它等值的十进制数:7654321210110001B=1×2+0×2+1×2+1×2+0×2+0×2+0×2+1×0=177D

在上面的公式里,10110001B 里的“B”表示这是一个二进制数,“D”则表示177 是个十进制数。“B”和“D”分别是英语单词Binary 和Decimal 的头一个字母,这两个单词分别表示二进位和十进位的意思。

检测点1.1

将下列二进制数转换成十进制数:

1101、1111、1001110、11111111、10000000、11011011000110111.1.3 十进制到二进制的转换

为了将一个十进制数转换成二进制数,可以采用将它不停地除以二进制的基数2,直到商为0,然后将每一步得到的余数串起来即可。如图1-2 所示,如果要将十进制数26 转换成二进制数11010,那么可采用如下方法:图1-2 将十进制数26 转换成二进制数

第1 步,将26 除以2,商为13,余数为0;

第2 步,用13 除以2,商为6,余数为1;

第3 步,用6 除以2,商为3,余数为0;

第4 步,用3 除以2,商为1,余数为1;

第5 步,用1 除以2,商为0,余数为1,结束。

然后,从下往上,将每一步得到的余数串起来,从左往右书写,就是我们所要转换的二进制数。

检测点1.2

将下列十进制数转换成二进制数:

8、10、12、15、25、64、100、255、1000、65535、10485761.2 十六进制计数法1.2.1 十六进制计数法的原理

二进制数和计算机电路有着近乎直观的联系。电路的状态,可以用二进制数来直观地描述,而一个二进制数,也容易使我们仿佛观察到了每根电线上的电平变化。所以,我们才形象地说,二进制是计算机的官方语言。

即使是在平时的学习和研究中,使用二进制也是必需的。一个数字电路输入什么,输出什么,电路的状态变了,是哪一位发生了变化,研究这些,肯定要精确到每一个比特。这个时候,采用二进制是最直观的。

但是,二进制也有它的缺点。眼下看来,它最主要的缺点就是写起来太长,一点也不方便。为此,人们发明了十六进制计数法。至于为什么要发明另外一套计数方法,而不是依旧采用我们熟悉的十进制,下面就要为大家解释。

一旦知道二进制有两个数符“0”和“1”,十进制有十个数符“0”到“9”,那么我们就会很自然地认为十六进制一定有16 个数符。

一点没错,完全正确。这16 个数符分别是0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F。

你可能会觉得惊讶,字母怎么可以当做数字来用?这样的话,那些熟悉的英语单词,像Face(脸)、Bad(坏的)、Bed(床)就都成了数。

这又有什么奇怪的?你觉得“0”、“5”、“9”是数字,而“A”、“B”不是数字,这是因为你已经从小习惯了这种做法。

对于自然数里的前10 个,十进制和十六进制的表示方法是一致的。但是,9 之后的数,两者的表示方法就大相径庭了,如表1-1 所示。表1-1 部分十进制数和十六进制数对照表续表

很显然,一旦某个数位增加到9 之后,下一次,它将变成A,而不是向前进位,因为这里是逢16 才进位的。进位只发生在某个数位原先是F 的情况下,比如1F,它加一后将会变成20。1.2.2 十六进制到十进制的转换

要把一个十六进制数转换成我们熟悉的十进制数,可以采用和前面一样的方法。只不过,计算各个数位的权时,幂的底数是16。比如将十六进制数125 转换成十进制数的方法如下:210125H=1×16+2×16+5×16=293D

上式里,125 后面的“H”用于表明这是一个十六进制数,它是英语单词Hexadecimal 的头一个字母,这个单词的意思是十六进制。

检测点1.3

将下列十六进制数转换成十进制数:

8、A、B、C、D、E、F、10、1F、6CD、3FE、FFC、FFFF1.2.3 十进制到十六进制的转换

如图1-3 所示,相应地,要把一个十进制数转换成十六进制数,则可以采取不停地除以16 并取其余数的策略。

第1 次,将293 除以16,商为18,余5;

第2 次,用18 除以16,商为1,余2;

第3 次,再用1 除以16,商为0,余1,结束。

然后,从下往上,将每次的余数1、2、5 列出来,得到125,这就是所要的结果。图1-3 将十进制数293 转换成十六进制数

检测点1.4

将下列十进制数转换成十六进制数:

8、10、12、15、25、64、100、255、1000、65535、10485761.2.4 为什么需要十六进制

为什么我们要发明十六进制计数法?为什么我们要学习它?

提出这样的问题,在我看来很有趣,也很有意义,但似乎从来没有人在书上正面回答过。这样一来,可怜的学子们只能在掌握了十六进制若干年之后,在某一天里自己恍然大悟。

为了搞清楚这个问题,我们不妨来列张表(表1-2),看看十进制数、二进制数和十六进制数之间,都有些什么有趣的规律和特点。表1-2 部分十进制数、二进制数和十六进制数对照表

在上面这张表里(表1-2),每一个二进制数在排版的时候,都经过了“艺术加工”,全都以4比特为一组的形式出现。不足4 比特的,前面都额外加了“0”,比如10,被写成0010 的形式。就像十进制数一样,在一个二进制数的前面加多少个零,都不会改变它的值。

注意观察这张表并开动脑子,4 比特的二进制数,可以表示的数是0000 到1111,也就是十进制的0~15,这正好对应于十六进制的0~F。

在这个时候,如果将它们都各自加1,那么,下一个二进制数是0001 0000,与此同时,它对应的十六进制数则是10,你会发现,它们有着如图1-4(左边)所示的奇妙对应关系。图1-4 十六进制的每一位与二进制数每4 比特为一组的对应关系

再比如图1-4(右边)中的二进制数1100 0011,它与等值的十六进制数C3 也有着相同的对应关系。

也就是说,如果将一个二进制数从右往左,分成4 比特为一组的形式,分别将每一组的值转换成十六进制数,就可以得到这个二进制数所对应的十六进制数。

这样一来,如果我们稍加努力,将0~F 这16 个数所对应的二进制数背熟,并能换算自如的话,那么,当我们看到一个十六进制数3F8 时,我们就知道,因为3 对应的二进制数为0011,F对应的二进制数是1111,8 对应的二进制数是1000,所以3F8H=0011 1111 1000B。

同理,如果一个二进制数是1101 0010 0101 0001,那么,将它们按4 比特为一组,分别换算成十六进制数,就得到了D251。

正如前面所说的,从事计算机的学习和研究(包括咱们马上就要进行的汇编语言程序设计),不可避免地要与二进制数打交道,而且有时还必须针对其中某些比特进行特殊处理。这个时候,如果想保留二进制数的直观性,同时还要求写起来简短,十六进制数是最好的选择。

检测点1.5

1.将下列十六进制数转换成二进制数:

3、A、C、F、20、3F、2FE、FFFF、9FC05D、7CCFFEFF

2.快速说出以下十进制数所对应的二进制数和十六进制数:

1、3、5、7、9、11、13、15、0、2、4、6、8、10、12、141.3 使用Windows 计算器方便你的学习过程

和十进制数一样,二进制数和十六进制数也可以进行加、减、乘、除运算。比如,两个十六进制数F 和A 相乘,结果是十六进制数96。从十进制的角度来看这个计算过程,就是两个十进制数15 和10 相乘,结果为150。

在学习汇编语言程序设计的过程中,出于解决实际问题的需要,经常要在编写程序时做一些计算工作。十进制就不说了,我们都很熟悉,计算起来驾轻就熟。但是,如果是几个二进制数进行加减乘除,或者几个十六进制数加减乘除,就很困难了。想想看,为了做十进制乘法,我们要背九九乘法口诀。而十六进制有16 个基数,它的乘法口诀就更多了。

这本书的目的不是教会你十六进制四则运算的方法和步骤,不是这样的。相反,我希望你能借助于一些工具来快速得到计算结果,从而把更多的精力放到学习汇编语言上。

不是所有知识都应当放在脑子里,要善于利用工具!

为了将较大的数转换成不同的数制,或者进行某种数制的四则运算,可以使用Windows 计算器。这是一个小软件,每个版本的Windows 操作系统都有,你应该很熟悉,其界面如图1-5 所示。注意,如果该程序运行后的界面与此不同,则可以通过选择菜单“查看”->“程序员”进行更改。图1-5 Windows 计算器

计算器软件的使用方法并不复杂,只需稍加练习即可掌握。比如,选择单选钮“十六进制”,然后输入一个十六进制数。此时,如果你再选择单选钮“十进制”,则刚才输入的内容就会立即变成十进制的形式,这就是进行数制转换的一个例子。

检测点1.6

1. 用计算器程序将FFCH 转换成十进制数和二进制数;

2. 用计算器程序计算FFCH 乘以27C0H 的结果,并转换成二进制数。本章习题

1. 口算:

5H=___D      12D=___H       0FH=___D=___B

0CH=___D=___B   0AH=___D=___B     8D=___H=___B

0BH=___D=___B   0EH=___D=___B     10H=___D=___B

2. 口算:

10010B=___H  15H=___B  8FH=___B  200H=___B  111111111B=___H第2章 处理器、内存和指令

鉴于汇编语言和处理器之间的紧密关系,学习汇编语言的过程,实际上也是洞悉处理器内部构造和工作方式的过程。在本章中,我们要借助于一款早已淘汰的处理器INTEL8086 来了解处理器、内存和指令这三者之间的关系。不要小看这款处理器,它是整个INTEL x86 处理器家族的起点和基础。本章的目标是:

1.了解INTEL8086 处理器的通用寄存器和段地址加偏移地址的内存访问方式。

2.了解分段机制对程序重定位的好处。

3.理解INTEL8086 处理器内存分段的本质,充分认识到这种分段机制的灵活性。2.1 最早的处理器

1947 年,美国贝尔实验室的肖克利和同事们一起发明了晶体管。1958 年,也许是受够了在一大堆晶体管里连接那些杂乱无章的导线,另一个美国人杰克·基尔比发明了集成电路。接着,1971 年,在为日本人设计计算器芯片的过程中,受到启发的Intel 公司生产了世界上第一个处理器INTEL 4004。

图2-1 所示的是INTEL 4004 处理器和它的设计者弗德里科·法金(Federico Faggin)。图2-1 Intel 第一块处理器4004 和它的设计者弗德里科·法金

我们已经知道,处理器(Processor)是一台电子计算机的核心,它会在振荡器脉冲的激励下,从内存中获取指令,并发起一系列由该指令所定义的操作。当这些操作结束后,它接着再取下一条指令。通常情况下,这个过程是连续不断、循环往复的。2.2 寄存器和算术逻辑部件

为什么处理器能够自动计算,这个问题已经在我的上一本书《穿越计算机的迷雾》里讲过了,不过这些原理讲起来很费劲,花了整整一本书的篇幅。当然,如果你没看过这本书,也没关系,下面就来简单回顾一下。回顾这些知识很有用,因为只有这样你才能知道如何安排处理器做事情。

电子计算机能做很多事情。你能够知道明天出门要穿厚一点才不挨冻,是因为电子计算机算出了天气。除此之外,它还能让你看电影、听音乐、写文章、上网。尽管表面上看来,这些用处和算数学题没什么关系,但实质上,这些功能都是以数学计算为基础的。正是因为如此,人们才会把“计算”这个词挂在嘴边,什么“云计算”、“网络计算”、“64 位计算”,等等。

处理器不是法师手里的仙器,它之所以能计算数学题,是因为其特殊的设计。处理器是一个“器”,即器件,不太大,有的是长方形,有的是正方形,就像饼干。实际上,它是一块集成电路。

如图2-2 所示,在处理器的底部或者四周,有大量的引脚,可以接受从外面来的电信号,或者向外发出电信号。每个引脚都有自己的用处,在往电路板上安装的时候,不能接错。所以,如图中所示,处理器在生产的时候,都会故意缺一个角,这是一个参照标志,可以图2-2 处理器进行数学运算的简单原确保安装的人不会弄错。当然,并不是所有的处理器都会缺一个角,这不是一个固定不变的做法。

处理器的引脚很多,其中有一部分是用来将参与运算的数字送入处理器内部。有些引脚是复用的,假如现在要进行加法运算,那么我们要重复使用这些引脚,来依次将被加数和加数送入。

一旦被加数通过引脚送入处理器,代表这个二进制数字的一组电信号就会出现在与引脚相连的内部线路上。这是一排高低电平的组合,代表着二进制数中的每一位。这时候,必须用一个称为寄存器(Register)的电路锁住。之所以要这样做,是因为相同的引脚和线路马上还要用于输入加数。也正是因为这个原因,这些内部线路称为处理器内部总线。

同样地,加数也要锁进另一个寄存器中。如图2-2 所示,寄存器RA 和RB 将分别锁存参与运算的被加数和加数。此后,RA 和RB 中的内容不再受外部数据线的影响。

寄存器是双向器件,可以在一端接受输入并加以锁存,同时,它也会在另一端产生一模一样的输出。与寄存器RA 和RB 相连的,是算术逻辑单元,或者算术逻辑部件(Arithmetic Logic Unit,ALU),也就是图2-2 中的桶形部分。它是专门负责运算的电路,可以计算加法、减法或者乘法,也可做逻辑运算。在这里,我们要求它做一次加法。

一旦寄存器RA 和RB 锁存了参与运算的两个数,算术逻辑部件就会输出相加的结果,这个结果可以临时用另外一个寄存器RC 锁存,稍后再通过处理器数据总线送到处理器外面,或者再次送入RA 或RB。

处理器内部有一个控制器(图中没有画出),在指令的执行过程中,它负责给各个部件发送控制信号,使各个部件在某个正确的时间点上执行某个动作。同时,它还负责决定在某个时间点上哪个部件有权使用总线,以免彼此发生冲突。

处理器总是很繁忙的,在它操作的过程中,所有数据在寄存器里面都只能是临时存在一会儿,然后再被送往别处,这就是为什么它被叫做“寄存器”的原因。早期的处理器,它的寄存器只能保存4 比特、8 比特或16 比特,分别叫做4 位、8 位和16 位寄存器。现在的处理器,寄存器一般都是32 位、64 位甚至更多。

如图2-3 所示,8 位寄存器可以容纳8 比特(bit),或者说1 字节(Byte),这是因为1 byte = 8 bit

另外,我们还要为这个字节的每一位编上号,编号是从右往左进行的,从0 开始,分别是0、1、2、3、4、5、6、7。在这里,位0(第1 位)是最低位,在最右边;位7(第8 位)是最高位,在最左边。

为了更好地理解上面这些概念,图中假定8 位寄存器里存放的是二进制数10001101,即十六进制的8D。这时,它的最低位和最高位都是1。

16 位寄存器可以存放2 个字节,这称为1 个字(word),各个数位的编号分别是0~15,其中0~7 是低字节,8~15 是高字节。实际上,“字”的概念出现得很早,也并非指16 个比特。只是到了后来,才特指16 个二进制位的长度。

32 位寄存器可以存放4 个字节,这称为1 个双字(double word),各个数位的编号分别是0~31,其中0~15 是低字,16~31 是高字。

尽管图中没有画出,但是64 位寄存器可以容纳更多的比特,也就是8 个字节,或者4 个字。位数越多,寄存器所能保存的数越大,这是显而易见的。图2-3 寄存器数据宽度示意2.3 内 存 储 器

前面已经讲过,处理器的计算过程,实际上是借助于寄存器和算术逻辑部件进行的。那么,参与计算的数是从哪里来的呢?答案是一个可以保存很多数字的电路,叫做存储器(Storage 或 Memory)。

存储器的种类实际上是很多的,包括大家都知道的硬盘和U 盘等。甚至寄存器就是存储器的一种。不过,我们现在所要讲到的存储器,则是另外一种东西。

如图2-4 所示,这是所有个人计算机里都会用到的存储器,我们平时把它叫做内存条。这个概念是这么来的,首先,它是计算机内部最主要的存储器,通常只和处理器相连,所以叫做内存储器或者主存储器,简称内存或主存。其次,它一般被设计成扁平的条状电路板,所以叫内存条。图2-4 个人计算机里使用的内存条

如图2-5 所示,和寄存器不同,内存用于保存更多的比特。对于用得最多的个人计算机来说,内存按字节来组织,单次访问的最小单位是1 字节,这是最基本的存储单元。如图中所示,每个存储单元中,各位的编号分别是0~7。

内存中的每字节都对应着一个地址,如图2-5 所示,第1 个字节的地址是0000H,第2 个字节的地址是0001H,第3 个字节的地址是0002H,其他以此类推。注意,这里采用的是十六进制表示法。作为一个例子,因为这个内存的容量是65536 字节,所以最后一个字节的地址是FFFFH。

为了访问内存,处理器需要给出一个地址。访问包括读和写,为此,处理器还要指明,本次访问是读访问还是写访问。如果是写访问,则还要给出待写入的数据。

8 位处理器包含8 位的寄存器和算术逻辑部件,16 位处理器拥有16 位的寄存器和算术逻辑部件,64 位处理器则包含64 位的寄存器和算术逻辑部件。尽管内存的最小组成单位是字节,但是,经过精心的设计和安排,它能够按字节、字、双字和四字进行访问。换句话说,仅通过单次访问就能处理8 位、16 位、32 位或者64 位的二进制数。注意,我说的是单次访问,而不是一个一个地取出每个字节,然后加以组合。图2-5 内存和内存访问示意图

如图2-5 所示,处理器发出字长控制信号,以指示本次访问的字长是8、16、32 还是64。如果字长是8,而且给出的地址是0002H,那么,本次访问只会影响到内存的一字节;如果字长是16,给出的地址依然是0002H,那么实际访问的将是地址0002H 处的一个字,低8 位在0002H中,高8 位在0003H 中。

检测点2.1

1. 一个字含有( )个字节和( )比特?一个双字含有( )个字节、( )个字和( )个比特?

2. 二进制数10000000 中,位( )的那个比特是“1”,也就是第( )位。它是最低位还是最高位?

3. 一个存储器的容量是16 个字节,地址范围为( )~( )。用该存储器保存字数据时,可存放( )个字,这些字的地址分别是( ),保存双字呢?2.4 指令和指令集

从一开始,设计处理器的目标之一就是使它成为一种可以自动进行操作的器件。另外,还需要提供一种机制,来允许程序员决定进行何种操作。

处理器何以能够自动进行操作,这不是本书的话题,大学里有这样的课程,《穿越计算机的迷雾》这本书也给出了通俗化的答案。

简单地说, 处理器的设计者用某些数来指示处理器所进行的操作, 这称为指令(Instruction),或者叫机器指令,因为只有处理器才认得它们。前面已经说了,处理器内部有寄存器和负责运算的部件,控制器“分析”一个个指令,然后确定在哪个时间点让哪些部件进行工作。比如,指令F4H 表示让处理器停机,当处理器取到并执行这条指令后,就停止工作。指令是集中存放在内存里的,一条接着一条,处理器的工作是自动按顺序取出并加以执行。

如图2-6 所示,从内存地址0000H 开始(也就是内存地址的最低端)连续存放了一些指令。同时,假定执行这些指令的是一个16 位处理器,拥有两个16 位的寄存器RA 和RB。

一般来说,指令由操作码和操作数构成,但也有小部分指令仅有操作码,而不含操作数。如图2-6 所示,停机指令仅包含1 字节的操作码F4,而没有操作数。指令的长度不定,短的指令仅有1 字节,而长的指令则有可能达到15 字节(对于INTEL x86 处理器来说)。图2-6 处理器指令在内存中的布局

对处理器来说,指令的操作码隐含了如何执行该指令的信息,比如它是做什么的,以及怎么去做。第一条指令的操作码是B8,这表明,该指令是一条传送指令,第一个操作数是寄存器,第二个操作数是直接包含在指令中的,紧跟在操作码之后,可以立即从指令中取得,所以叫做立即数(Immediate Operand)。同时,操作码还直接指出该寄存器是RA。RA 是16 位寄存器,这条指令将按字进行操作。所以,当这条指令执行之后,该指令的操作数(立即数)005DH就被传送到RA 中。

既然操作码中隐含了这么多的信息,那么,处理器就可以“知道”每条指令的长度。这样,当它执行第一条指令B8 5D 00 的时候,就已经知道,这是一个3 字节指令,下一条指令位于3 个字节之后,即内存地址0003H 处。

注意字数据在内存中的存放特点。地址0001H 和0002H 里的内容分别是5D 和00,如果每次读一个字节,则从地址0001H 里读出的是5D,从0002H 里读出的是00。但如果以字的方式来访问地址0001H,读到的就会是005DH。这种差别,跟处理器和内存之间的数据线连接方式有关。对于Intel 处理器来说,如果访问内存中的一个字,那么,它规定高字节位于高地址部分,低字节位于低地址部分,这称为低端字节序(Little Endian)。至于其他公司的处理器,则可能情况正好相反,称为高端字节序。

对于复杂一些的指令来说,1 个字节的操作码可能不会够用。所以,第2 条指令的操作码为8B 1E,它隐含的意思是,这是一条传送指令,第一个操作数是寄存器,而且是RB 寄存器,第二个操作数是内存地址,要传送到RB 寄存器中的数存放在该地址中。同时,这是一个字操作指令,应当从第二个操作数指定的地址中取出一个字。

该指令的操作数部分是3F 00,指定了一个内存地址003FH。它相当于高级语言里的指针,当处理器执行这条指令时,会再次用003FH 作为地址去访问内存,从那里取出一个字(1002H),然后将它传送到寄存器RB。注意,“传送”这个词带有误导性。其实,传送的意思更像是“复制”,传送之后,003FH 单元里的数据还保持原样。

通过这两条指令的比较,很容易分清指令中的“立即数”是什么意思。指令执行和操作的对象是数。如果这个数已经在指令中给出了,不需要再次访问内存,那这个数就是立即数,比如第一条指令中的005DH;相反,如果指令中给出的是地址,真正的数还需要用这个地址访问内存才能得到,那它就不能称为立即数,比如第二条指令中的003FH。

如图2-6 所示,余下的三条指令,旁边都有注解,这里就不再一一解释了。如果一开始内存地址003FH 中存放的是1002H,那么,当所有这些指令执行完后,003FH 里就是最终的结果105FH。

指令和非指令的普通二进制数是一模一样的,在组成内存的电路中,都是一些高低电平的组合。因为处理器是自动按顺序取指令并加以执行的,在指令中混杂了非指令的数据会导致处理器不能正常工作。为此,指令和数据要分开存放,分别位于内存中的不同区域,存放指令的区域叫代码区,存放数据的区域叫数据区。为了让处理器正确识别和执行指令,工程技术人员必须精心安排,并告诉处理器要执行的指令位于内存中的什么位置。

还是那句话,并非每一个二进制数都代表着一条指令。每种处理器在设计的时候,也只能拥有有限的指令,从几十条到几百条不等。一个处理器能够识别的指令的集合,称为该处理器的指令集。

检测点2.2

在内存中,指令和数据一模一样,都是无差别的数。如图2-6 所示,假如处理器访问内存时是按低端字节序的,那么,从地址0008H 处取出一个字时,该字的值为( )。2.5 古老的Intel 8086 处理器

任何时候,一旦提到Intel 公司的处理器,就不能不说8086。8086 是Intel 公司第一款16 位处理器,诞生于1978 年,所以说它很古老。

但是,在Intel 公司的所有处理器中,它占有很重要的地位,是整个Intel 32 位架构处理器(IA-32)的开山鼻祖。首先,最重要的一点是,它是一款非常成功的产品,设计先进,功能很强,卖得很好。

其次,8086 的成功使得市场上出现了大量针对它开发的软件产品。这样,当Intel 公司要设计新的处理器时,它不得不考虑到兼容性的问题。要使得老的软件也能在新的处理器上很好地运行,必须要具备指令集和工作模式上的兼容性和一致性。Intel 公司很清楚,如果新处理器和老处理器不兼容,那么,新处理器越多,它扔掉的拥趸也就越多,要不了多久,这公司就不用再开了。

所以,当我们讲述处理器的时候,必须要从8086 开始;而且,要学习汇编语言,针对8086的汇编技术也是必不可少的。2.5.1 8086 的通用寄存器

8086 处理器内部有8 个16 位的通用寄存器,分别被命名为AX、BX、CX、DX、SI、DI、BP、SP。“通用”的意思是,它们之中的大部分都可以根据需要用于多种目的。

如图2-7 所示,因为这8 个寄存器都是16 位的,所以通常用于进行16 位的操作。比如,可以在这8 个寄存器之间互相传送数据,它们之间也可以进行算术逻辑运算;也可以在它们和内存单元之间进行16 位的数据传送或者算术逻辑运算。

同时,如图2-7 所示,这8 个寄存器中的前4 个,即AX、BX、CX 和DX,又各自可以拆分成两个8 位的寄存器来使用,总共可以提供8 个8 位的寄存器AH、AL、BH、BL、CH、CL、DH 和DL。这样一来,当需要在寄存器和寄存器之间,或者寄存器和内存单元之间进行8 位的数据传送或者算术逻辑运算时,使用它们就很方便。图2-7 8086 的通用寄存器

将一个16 位的寄存器当成两个8 位的寄存器来用时,对其中一个8 位寄存器的操作不会影响到另一个8 位寄存器。举个例子来说,当你操作寄存器AL 时,不会影响到AH 中的内容。2.5.2 程序的重定位难题

我们知道,处理器是自动化的器件,在给出了起始地址之后,它将从这个地址开始,自动地取出每条指令并加以执行。只要每条指令都正确无误,它就能准确地知道下一条指令的地址。这就意味着,完成某个工作的所有指令,必须集中在一起,处于内存的某个位置,形成一个段,叫做代码段。事情是明摆着的,要是指令并没有一条挨着一条存放,中间夹杂了其他非指令的数据,处理器将因为不能识别而出错。

为了做某件事而编写的指令,它们一起形成了我们平时所说的程序。程序总要操作大量的数据,这些数据也应该集中在一起,位于内存中的某个地方,形成一个段,叫做数据段。

注意,我们并没有改变内存的物理性质,并不是真的把它分成几块。段的划分是逻辑上的,从本质上来说,是如何看待和组织内存中的数据。

段在内存中的位置并不重要,因为处理器是可控的,我们可以让它从内存的任何位置开始取指令并加以执行。这里有一个例子,如图2-8 所示,我们有一大堆数字,现在想把它们加起来求出一个总和。图2-8 程序的代码段和数据段示例

假定我们有16 个数要相加,这些数都是16 位的二进制数,分别是0005H、00A0H、00FFH、…。为了让处理器把它们加起来,我们应该先在内存中定义一个数据段,将这些数字写进去。数据段可以起始于内存中的任何位置,既然如此,我们将它定在0100H 处。这样一来,第一个要加的数位于地址0100H,第二个要加的数位于地址0102H,最后一个数的地址是011EH。

一旦定义了数据段,我们就知道了每个数的内存地址。然后,紧挨着数据段,我们从内存地址0120H 处定义代码段。严格地说,数据段和代码段是不需要连续的,但这里把它们挨在一起更自然一些。为了区别数据段和代码段,我们使用了不同的底色。

代码段是从内存地址0120H 处开始的,第一条指令是A1 00 01,其功能是将内存单元0100H里的字传送到AX 寄存器。指令执行后,AX 的内容为0005H。

第二条指令是03 06 02 01,功能是将AX 中的内容和内存单元0102H 里的字相加,结果在AX 中。由于AX 的内容为0005H,而内存地址0102H 里的数是00A0H,这条指令执行后,AX 的内容为00A5H。

第三条指令是03 06 04 01,功能是将AX 中的内容和内存单元0104H 里的字相加,结果在AX 中。此时,由于AX 里的内容是00A5H,内存地址0104H 里的数是00FFH,本指令执行后,AX 的内容为01A4H。

后面的指令没有列出,但和前2 条指令相似,依次用AX 的内容和下一个内存单元里的字相加,一直到最后,在AX 中得到总的累加和。在这个例子中,我们没有考虑AX 寄存器容纳不下结果的情况。当累加的总和超出了AX 所能表示的数的范围(最大为FFFFH,即十进制的65535)时,就会产生进位,但这个进位被丢弃。

在内存中定义了数据段和代码段之后,我们就可以命令处理器从内存地址0120H 处开始执行。当所有的指令执行完后,就能在AX 寄存器中得到最后的结果。

看起来没有什么问题,一切都很完美,不是吗?那本节标题中所说的难题又从何而来呢?

这里确实有一个难题。

在前面的例子中,所有在执行时需要访问内存单元的指令,使用的都是真实的内存地址。比如A1 00 01,这条指令的意思是从地址为0100H 的内存单元里取出一个字,并传送到寄存器AX。在这里,0100H 是一个真实的内存地址,又称物理地址。

整个程序(包括代码段和数据段)在内存中的位置,是由我们自己定的。我们把数据段定在0100H,把代码段定在0120H。

问题是,大多数时候,整个程序(包括代码段和数据段)在内存中的位置并不是我们能够决定的。请想一想你平时是怎么使用计算机的,你所用的程序,包括那些用来调整计算机性能的工具、小游戏、音乐和视频播放器等,都是从网上下载的,位于你的硬盘、U 盘或光盘中。即使有些程序是你自己编写的,那又如何?当你双击它们的图标,使它们在Windows 里启动之前,内存已经被塞了很多东西,就算你是刚刚打开计算机,Windows 自己已经占用了很多内存空间,不然的话,你怎么可能在它上面操作呢?

在这种情况下,你所运行的程序,在内存中被加载的位置完全是随机的,哪里有空闲的地方,它就会被加载到哪里,并从那里开始被处理器执行。所以,前面那段程序不可能恰好如你所愿,被加载到内存地址0100H,它完全可能被加载到另一个不同的位置,比如1000H。但是,同样是那个程序,一旦它在内存中的位置发生了改变,灾难就出现了。

如图2-9 所示,因为程序现在是从内存地址1000H 处被加载的,所以,数据段的起始地址为1000H。这就是说,第一个要加的数,其地址为1000H,图2-9 在指令中使用绝对内存地址的程序是不可重定位的第二个则为1002H,其他以此类推。代码段依然紧挨着数据段之后,起始地址相应地是1020H。

只要所有的指令都是连续存放的,代码段位于内存中的什么地方都可以正常执行。所以,处理器可以按你的要求,从内存地址1020H 处连续执行,但结果完全不是你想要的。

请看第一条指令A1 00 01,它的意思是从内存地址0100 处取得一个字,将其传送到寄存器AX。但是,由于程序刚刚改变了位置,它要取的那个数,现在实际上位于1000H,它取的是别人地盘里的数!

这能怪谁呢?发生这样的事情,是因为我们在指令中使用了绝对内存地址(物理地址),这样的程序是无法重定位的。为了让你写的程序在卖给别人之后,可以在内存中的任何地方正确执行,就只能在编写程序的时候使用相对地址或者逻辑地址了,而不能使用真实的物理地址。当程序加载时,这些相对地址还要根据程序实际被加载的位置重新计算。

在任何时候,程序的重定位都是非常棘手的事情。当然,也有好几种解决的办法。在8086 处理器上,这个问题特别容易解决,因为该处理器在访问内存时使用了分段机制,我们可以借助该机制。2.5.3 内存分段机制

如图2-10 所示,整个内存空间就像长长的纸条,在内存中分段,就像从长纸条中裁下一小段来。根据需要,段可以开始于内存中的任何位置,比如图中的内存地址A532H 处。

在这个例子中,分段开始于地址为A532H 的内存单元处,这个起始地址就是段地址。

这个分段包含了6 个存储单元。在分段之前,它们在整个内存空间里的物理地址分别是A532H、A533H、A534H、A535H、A536H、A537H。

但是,在分段之后,它们的地址可以只相对于自己所在的段。这样,它们相对于段开始处的距离分别为0、1、2、3、4、5,这叫做偏移地址。

于是,当采用分段策略之后,一个内存单元的地址实际上就可以用“段:偏移”或者 “段地址:偏移地址”来表示,这就是通常所说的逻辑地址。比如,在图2-10 中,段内第1 个存储单元的地址为A532H:0000H,第3 个存储单元的地址为A532H:0002H,而本段最后一个存储单元的地址则是A532H:0005H。图2-10 段地址和偏移地址示意图

为了在硬件一级提供对“段地址:偏移地址”内存访问模式的支持,处理器至少要提供两个段寄存器,分别是代码段寄存器(Code Segment,CS)和数据段寄存器(Data Segment,DS)。

对CS 内容的改变将导致处理器从新的代码段开始执行。同样,在开始访问内存中的数据之前,也必须首先设置好DS 寄存器,使之指向数据段。

除此之外,最重要的是,当处理器访问内存时,它把指令中指定的内存地址看成是段内的偏移地址,而不是物理地址。这样,一旦处理器遇到一条访问内存的指令,它将把DS 中的数据段起始地址和指令中提供的段内偏移相加,来得到访问内存所需要的物理地址。

如图2-11 所示,代码段的段地址为1020H,数据段的段地址为1000H。在代码段中有一条指令A1 02 00,它的功能是将地址0002H 处的一个字传送到寄存器AX。在这里,处理器将0002H 看成是段内的偏移地址,段地址在DS 中,应该在执行这条指令之前就已经用别的指令传送到DS 中了。

当执行指令A1 02 00 时,处理器将把DS 中的内容和指令中指定的偏移地址0002H 相加,得到1002H。这是一个物理地址,处理器用它来访问内存,就可以得到所需要的数00A0H。

如果一下次执行这个程序时,代码段和数据段在内存中的位置发生了变化,只要把它们的段地址分别传送到CS 和DS,它也能够正确执行。图2-11 从逻辑地址到物理地址的转换过程2.5.4 8086 的内存分段机制

前面讲了如何从逻辑地址转换到物理地址,以使得程序的运行和它在内存中的位置无关。这种策略在很多处理器中得到了支持,包括8086 处理器。但是,由于8086 自身的局限性,它的做法还要复杂一些。

如图2-12 所示,8086 内部有8 个16 位的通用寄存器,分别是AX、BX、CX、DX、SI、DI、BP、SP。其中,前四个寄存器中的每一个,都还可以当成两个8 位的寄存器来使用,分别是AH、AL、BH、BL、CH、CL、DH、DL。图2-12 8086 处理器内部组成框图

在进行数据传送或者算术逻辑运算的时候,使用算术逻辑部件(ALU)。比如,将AX 的内容和CX 的内容相加,结果仍在AX 中,那么,在相加的结果返回到AX 之前,需要通过一个叫数据暂存器的寄存器中转。

处理器能够自动运行,这是控制器的功劳。为了加快指令执行速度,8086 内部有一个6 字节的指令预取队列,在处理器忙着执行那些不需要访问内存的指令时,指令预取部件可以趁机访问内存预取指令。这时,多达6 个字节的指令流可以排队等待解码和执行。

8086 内部有4 个段寄存器。其中,CS 是代码段寄存器,DS 是数据段寄存器,ES 是附加段(Extra Segment)寄存器。附加段的意思是,它是额外赠送的礼物,当需要在程序中同时使用两个数据段时,DS 指向一个,ES 指向另一个。可以在指令中指定使用DS 和ES 中的哪一个,如果没有指定,则默认是使用DS。SS 是栈段寄存器,以后会讲到,而且非常重要。

IP 是指令指针(Instruction Pointer)寄存器,它只和CS 一起使用,而且只有处理器才能直接改变它的内容。当一段代码开始执行时,CS 指向代码段的起始地址,IP 则指向段内偏移。这样,由CS 和IP 共同形成逻辑地址,并由总线接口部件变换成物理地址来取得指令。然后,处理器会自动根据当前指令的长度来改变IP 的值,使它指向下一条指令。

当然,如果在指令的执行过程中需要访问内存单元,那么,处理器将用DS 的值和指令中提供的偏移地址相加,来形成访问内存所需的物理地址。

8086 的段寄存器和IP 寄存器都是16 位的,如果按照原先的方式,把段寄存器的内容和偏移地址直接相加来形成物理地址的话,也只能得到16 位的物理地址。麻烦的是,8086 却提供了20根地址线。换句话说,它提供的是20 位的物理地址。

提供20 位地址线的原因很简单,16 位的物理地址只能访问64KB 的内存,地址范围是0000H~FFFFH,共65536 个字节。这样的容量,即使是在那个年代,也显得捉襟见肘。注意,这里提到了一个表示内存容量的单位“KB”。为了方便,我们通常使用更大的单位来描述内存容量,比如千字节(KB)、兆字节(MB)和吉字节(GB),它们之间的换算关系如下:

所以,65536 个字节就是64KB,而20 位的物理地址则可以访问多达1MB 的内存,地址范围从00000H 到FFFFFH。问题是,16 位的段地址和16 位的偏移地址相加,只能形成16 位的物理地址,怎么得到这20 位的物理地址呢?

为了解决这个问题,8086 处理器在形成物理地址时,先将段寄存器的内容左移4 位(相当于乘以十六进制的10,或者十进制的16),形成20 位的段地址,然后再同16 位的偏移地址相加,得到20 位的物理地址。比如,对于逻辑地址F000H:052DH,处理器在形成物理地址时,将段地址F000H 左移4 位,变成F0000H,再加上偏移地址052DH,就形成了20 位的物理地址F052DH。

这样,因为段寄存器是16 位的,在段不重叠的情况下,最多可以将1MB 的内存分成65536个段,段地址分别是0000H、0001H、0002H、0003H,……,一直到FFFFH。在这种情况下,如图2-13 所示,每个段正好16 个字节,偏移地址从0000H 到000FH。图2-13 1MB 内存可以划分为65536 个16 字节的段

同样在不允许段之间重叠的情况下,每个段的最大长度是64KB,因为偏移地址也是16 位的,从0000H 到FFFFH。在这种情况下,1MB 的内存,最多只能划分成16 个段,每段长64KB,段地址分别是0000H、1000H、2000H、3000H,…,一直到F000H。

以上所说的只是两种最典型的情况。通常情况下,段地址的选择取决于内存中哪些区域是空闲的。举个例子来说,假如从物理地址00000H 开始,一直到82251H 处都被其他程序占用着,而后面一直到FFFFFH 的地址空间都是自由的,那么,你可以从物理内存地址82251H 之后的地方加载你的程序。

接着,你的任务是定义段地址并设置处理器的段寄存器,其中最重要的是段地址的选取。因为偏移地址总是要求从0000H 开始,而82260H 是第一个符合该条件的物理地址,因为它恰好对应着逻辑地址8226H:0000H,符合偏移地址的条件,所以完全可以将段地址定为8226H。

但是,举个例子来说,如果你从物理内存地址82255H 处加载程序,由于它根本无法表示成一个偏移地址为0000H 的逻辑地址,所以不符合要求,段不能从这里开始划分。这里面的区别在于,82260H 可以被十进制数16(或者十六进制数10H)整除,而82255H 不能。通过这个例子可以看出,8086 处理器的逻辑分段,起始地址都是16 的倍数,这称为是按16 字节对齐的。

段的划分是自由的,它可以起始于任何16 字节对齐的位置,也可以是任意长度,只要不超过64KB。比如,段地址可以是82260H,段的长度可以是64KB。在这种情况下,该段所对应的逻辑地址范围是8226H:0000H~8226H:FFFFH,其所对应的物理地址范围是82260~9225FH。

同时,正是由于段的划分非常自由,使得8086 的内存访问也非常随意。同一个物理地址,或者同一片内存区域,根据需要,可以随意指定一个段来访问它,前提是那个物理地址位于该段的64KB 范围内。也就是说,同一个物理地址,实际上对应着多个逻辑地址。

检测点2.3

1. INTEL 8086 处理器有( )个16 位通用寄存器,分别是(     )。其中,有些还可以分开来作为两个独立的8 位寄存器来用,这几个8 位寄存器分别是(    )。

2. 选择题(可多选):INTEL 8086 处理器取指令时,使用段寄存器( )和指令指针寄存器( )。方法是,将段寄存器的值( ),加上指令指针寄存器的当前值,形成物理地址访问内存。

A.CS B.DS C.IP D.左移4位 E.右移4位 F.乘以1 G.除以10H

3. 物理地址132FEH 对应的逻辑地址是(可多选):

A.132FH:000EH   B.1300H:02FEH   C.1000H:32FEH   D.1320H:00FEH

E.102FH:03E0H   F.0FE0H:34FEH本章习题

1.在段与段之间互不重叠的前提下,1MB 内存可以完整地划分为多少个16KB 的段?

2.数据段寄存器DS 的值为25BCH 时,计算Intel 8086可以访问的物理地址范围。第3章 汇编语言和汇编软件

处理器依靠机器指令工作,但机器指令从形式上看都是一些没有规律的数字,难以书写、阅读和理解,这样就发明了汇编语言。本章的目标是:

1.了解汇编语言的作用和“汇编”一词的由来。

2.下载NASM 编译器,并学会使用它来编译汇编语言源程序。3.1 汇编语言简介

在前面的章节里,我们讲到了处理器,也讲了处理器是如何进行算术逻辑运算的。为了实现自动计算,处理器必须从内存中取得指令,并执行这些指令。

指令和被指令引用的数据在内存中都是一些或高或低的电平,每一个电平都可以看成是一个二进制位(0 或者1),8 个二进制位形成一字节。

要解读内存中的东西,最好的办法就是将它们按字节转换成数字的形式。比如,下面这些数字就是存放在内存中的INTEL8086 指令,我们用的是十六进制:

对于大多数人来说,他们很难想象上面那一排数字对应着下面几条8086 指令:

即使是很有经验的技术人员,要想用这种方式来编写指令,也是很困难的,而且很容易出错。所以,在第一个处理器诞生之后不久,如何使指令的编写变得更容易,就提上了日程。

为了克服机器指令难以书写和理解的缺点,人们想到可以用一些容易理解和记忆的符号,也就是助记符,来描述指令的功能和操作数的类型,这就产生了汇编语言(Assembly Language)。这样,上面那些指令就可以写成:

对于那些有点英语基础的人来说,理解这些汇编语言指令并不困难。比如这句

首先,mov 是move 的简化形式,意思是“移动”或者“传送”。至于“ax”,很明显,指的就是AX 寄存器。传送指令需要两个操作数,分别是目的操作数和源操作数,它们之间要用逗号隔开。在这里,AX 是目的操作数,源操作数是3FH。汇编语言对指令的大小写没有特别的要求。所以你完全可以这样写:

在很多高级语言中,如果要指示一个数是十六进制数,通常不采用在后面加“H”的做法,而是为它添加一个“0x”前缀。像这样:

你可能想问一下,为什么会是这样,为什么会是“0x”?答案是不知道,不知道在什么时候,为什么就这样用了。这不得不让人怀疑,它肯定是一个非常随意的决定,并在以后形成了惯例。如果你知道确切的答案,不妨写封电子邮件告诉我。注意,为了方便,我们将在本书中采用这种形式。

在汇编语言中,使用十进制数是最自然的。因为3FH 等于十进制数63,所以你可以直接这样写:

当然,如果你喜欢,也可以使用二进制数来这样写:

一定要看清楚,在那串“0”和“1”的组合后面,跟着字母“B”,以表明它是一个二进制数。

至于这句:情况也是一样。add 的意思是把一个数和另一个数相加。在这里,是把BX 寄存器的内容和AX 寄存器的内容相加。相加的结果在BX 中,但AX 的内容并不改变。

像上面那样,用汇编语言提供的符号书写的文本,叫做汇编语言源程序。为此,你需要一个字处理器软件,比如Windows 记事本,来编辑这些内容。如图3-1 所示,相信这些软件的使用都是你已经非常熟悉的。图3-1 用Windows 记事本来书写汇编语言源程序

有了汇编语言所提供的符号,这只是方便了你自己。相反地,对人类来说通俗易懂的东西,处理器是无法识别的。所以,还需要将汇编语言源程序转换成机器指令,这个过程叫做编译(Compile)。在编译的时候,汇编语言编译器的作用是将mov、add、ax、bx 等这些符号组合起来,转换成类似于数值的机器指令,这个过程叫做汇编,这就是汇编语言的由来,也有人称之为组合语言。

编译肯定还需要依靠一个软件,称为编译器,或编译软件。因为如果需要人类自己去做,还费这周折干嘛。另一方面,想想看,一个帮助人类生产软件的工具,自己居然也是一个软件,这很有意思。

从字处理器软件生成的是汇编语言源程序文件。编译软件的任务是读取这些文件,将那些符号转变成二进制形式的机器指令代码。它把这些机器代码存放到另一个文件中,叫做二进制文件或者可执行文件,比如Windows 里以“.exe”为扩展名的文件,就是可执行文件。当需要用处理器执行的时候,再加载到内存里。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载