【笔记】8086汇编-包含多个段的程序

前言:本篇仅仅是笔记,整理于2017/8/2,结构可能会略有混乱,见谅。

-我们在第五章找到了相对安全的地址,但是这并不能完全满足我们的需要,如果我们合法的通过操作系统来得到空间,这样得到的空间将会是安全的。

-【本章重点:“封装”的概念】

-程序取得需要的空间有两种方法

–1)加载程序时分配

–2)执行过程中向系统申请【本课程不讨论】

-想象一个场景,我们想要将指定的数据求和,那么我们应当怎么做

–在代码中假如”dw 数据1,数据2,数据n”或”db 数据1,数据2,数据n”,【分别是”define word”、”define byte”】

—一开始,我们将这条代码放在的代码段的第一行,这样在运行时,数据应当放在了cs:0处,但是在运行时,我们会发现,CPU会将数据读成了指令【实际情况比我们预想的要糟糕,CPU会把后面正常指令读取错误】,所以我们可以定义一个start标号:

—-start:

—-最后end start

—-关于此处的解释:end不仅可以通知程序的结束,还可以通知程序的入口,所以只要将一个标号的名字放在end后,计算机便会将标号处的代码当做入口。

-【注意:不可以直接将cs数据传给ss】

-此处有两个重要问题(个人总结)及其解决方案:

–1)正序复制字:使用pop,将sp设置为要复制到的目标地址的最开端,之后将bx在循环中每次+2,使用cx控制loop次数,cx的数值等于复制的字的个数

–2)逆序复制字:使用push,将sp设置为最后一个存放数据的单元的偏移地址+1,,其余同上

–【备注:并不是一定需要使用栈,也可以直接借助寄存器来复制,特别是针对使用栈难以解决的字节型数据传输问题】

-【将数据、代码、栈放入不同的段】

–1)我们将所有东西放到了同一个段中,这样十分乱。2)并且8086CPU一个段只有64KB,如果要存储大量内容,这显然是不够用的。

–所以:

—assume cs:code,ds:data,ss:stack

—这样做完后如何使用段地址呢?其实,一个段的标号就是它的段地址,所以,此处“mov ax,data”就可以将ds的段地址告诉ax,但是要注意,这里我们不可以使用“mov ds,data”

—需要注意的是,即便我们使用了伪指令assume将段寄存器和名称联系起来,但是CPU还是不清楚哪个是入口,因为伪指令只是给编译器看的,所以我们仍然需要使用一个入口标号(就是我们习惯上命名为start的那个)。所以,我们也需要“mov ax,stack””mov ss,ax”将ss设置到stack【一定不要理解为assume后就自动设置了段寄存器,段寄存器的地址仍然需要我们用指令来设置】。

-【提示:要注意ds:[0],虽然[0]也是用数字表示的,但是由于前面显示给出了ds,所以这个[0]会被当做偏移地址,而不是数据】

-实验结论1(源程序中依次是data,stack,code,如果交换顺序,则数量关系也会相应改变,在源程序中靠前的段对应的段地址小):运行时寄存器数量关系:DS+1=SS,SS+1=CS(中间的差值不一定是1)。

-实验结论2:如果一个段中数据占用N个字节,则程序加载后,这个段实际占有的空间为:16×(N/16 + 1)【段一定是16的倍数】

-实验结论3:如果不加程序入口,则程序指令中第一个段会被当做代码段。(注意:不是说assume这样的伪指令中的顺序)

-实验结论4:段不一定必须在assume中声明

【笔记】8086汇编-[bx]与loop指令

前言:本篇仅仅是笔记,整理于2017/7/31,结构可能会略有混乱,见谅。

-编译器会将[0]人为是0,这样便无法进行正确的内存访问,所以我们需要使用[bx]来代表偏移地址。

-loop代表循环

-注释:为了表示方便,我们今后使用()来描述某寄存器或内存单元中的内容,如(ax)、(20000H)。另外,为了表示方便,我们今后使用idata来表示常量,如mov ax,idata

-inc bx与add bx,1等价【inc可能是include的缩写】

-loop指令

–格式:loop 标号

–执行步骤:

—1)(cx) = (cx) – 1

—2)判断cx中的值,如果cx不等于0则转至标号处执行程序,如果为0则向下执行。

–关于标号:标号是由用户命名的,在编译器中,标号相当于一个地址。

—标号使用方法:(此处假设标号是s)

—-s:

—-statements

—-loop s

-【注意:编译器默认是十进制数,所以十六进制数一定要加上H】

-【可以为程序添加一个开始标号,使用方法是:在代码中加上一个标号(推荐是start这样便于理解的单词),之后在最后的end后加上这个标号(用start举例,例如:end start)】

-【提示:特别是在运行乘法运算时要注意,一个寄存器是否可以存放那么大的数】

-实验:计算ffff:0006单元中的数乘3,结果存储在dx中

–十六位寄存器最大会存放65535这个十进制数

–我们所说将8位内容赋值给16位寄存器,指的是让它们树枝上相等,而不是最大位数相等

–提示:使用”;”可以添加注释

–先将数据传给ax,再用ax三次加给dx,这样虽然在步骤上比直接将数值给dx多了一步,但是却可在物理层面上只访问一次内存

–注意:“mov ax,ffffh”是错误的指令,编译器不允许数值的第一个数为字母【汇编源程序不能以字母开头】,所以字母开头要加0,如“mov ax,0ffffh”

-【关于debug命令的补充】

–使用g命令可以一次性执行完到g命令的参数对应的位置的指令

—语法”g 偏移地址(相当于ip)”

–还有一种对loop有特别效果的指令——p指令,当下一条指令是loop指令时,这时如果输入p,计算机会自动执行完循环指令

-思考:在debug与汇编源代码有什么区别?

–1)debug默认数值为十六进制,而汇编源代码默认为十进制,如果想要表示十六进制需要在末尾加上h,并且,数值首位必须是数字,如果以字母开头,则需要在字母前加上0;

–2)[idata]有不同的解释:debug中将这个当做偏移地址,而源代码中将其当做一个数,需要先把偏移地址放到bx中,再使用[bx],不过,我们也可以这样:“mov ax,ds:[0]”,这样[0]便会被当做一个偏移地址。【使用ds:[bx]也是可以的】【注意,这里“:”不是代表冒号,在解读时一定不能理解成ds中存放[0]】 【另外:Left operand(操作数) must have segment.这是如果在源代码中输入“mov cx,3000:[0]”后编译时的提示】

-loop和[bx]的联合应用

–引入问题:如何计算ffff:0~ffff:b中单元中数据的和,结果存储在dx中

–分析:

—1)先考虑是否可以再dx中放下。我们知道,这总共有12个字节的数据,每个数据最大255,所以他们的和显然小于16位上限65535,所以可以放下

—2)但是16位通用寄存器会一次性读取一个字,这显然不是我们想要的,另外,这里我们也不能使用dl来读取(最大255),因为这样会导致可能出现数据放不下的问题。

—3)此处,我们采取找一个寄存器当作中介的方法(以后我们可以有其它方法)

—4)我们发现,bx的变化有规律,可以说,每读取一个数,我们就需要将bx自增1。

–我们可以用数学来表示这一过程:

—sum=∑[在下X=0][在上0bH](ffffh×10h+X)

–在编译时,编译器对”add dx,al”弹出的”Operand types must match”的Warning Errors,不过这不影响编译的结果。

–提示:由于bx自增了12次,它的值由0h变为了ch,也就是说,bx的值总共经历了13个数(因为我们将它的自增放置在循环体的loop语句之前,这么做是考虑到,bx初始值为0对应的偏移地址所指向的物理地址也要进行求和),在这里,前面的求和也进行了12次,只不过对应的是bx为0h~Bh值的情况,而B为ch时循环结束。这里启示我们:1)cx的数是绝对的循环次数;2)如果一个数在循环体内自增1,则其结果一定是初始值加上cx的值。

-【段前缀】

–我们可以在访问内存单元时显式地给出内存单元的段地址所在的段寄存器【隐式默认为ds】,即我们可以

—mov ax,ds:[bx]

–但是我们并不需要总是使用ds,也就是说这里的段前缀“ds:”可以是任意一个段寄存器“cs:”、“ss:”、“ds:”、“es:”

-【一段安全的空间】

–引入:在8086模式,不考虑内存空间是否已经存储了数据时就进行写入是一种不负责任且十分危险的行为。如果碰到了一些系统数据,很可能的结果是:我们的程序运行出错,这显然也不是我们想要的。

–还是引入:大多数情况下,我们想要的是在不影响系统的情况下运行【如果是想要研究被系统保护或封装的指令的话,则是要避开操作系统来操控底层以查看】。另外,如果是纯dos系统(实系统)则可以不用考虑这些,而对于Windows这类写有保护的系统,则需要考虑这一问题。

–一般,在DOS方式下,0:200~0:2ff这256个字节的空间是安全的。【原因以后讨论】

–实验:将ffff:0~b的数据都保存到0020:0~b中

—注意:为了提高效率,我们使用了两个寄存器,因为这样就不用将ds放入循环(每次循环,先赋值ffff,再赋值0020),达到了代码优化

【笔记】8086汇编-第一个程序

前言:本篇仅仅是笔记,整理于2017/7/28,结构可能会略有混乱,见谅。

首先:了解一个程序从编写到运行的流程:

编写源程序→编译→连接成可执行文件→运行

【备注:可执行文件包含两部分内容:1)程序(由翻译得到的机器码)和数据(源程序中定义的数据);2)相关描述信息(如:程序大小,所占空间)】

【.com也是可执行文件,它是dos的可执行文件,在windows下也可以执行,如果一个汇编源程序只有一个段,则如果将生成的可执行程序后缀名由.exe改为.com,这样也是可以执行的】

(汇编语言源程序中包含两种指令,汇编指令与伪指令)

第一个程序:

1、伪指令(由编译器执行):

1)定义一个段

XXX segment

XXX ends

segment和ends是一对成对使用的伪指令,segment说明一个段的开始,ends说明一个段的结束(注意与end区分,ends可以用“end segment”来记忆)。

汇编语言体现了段的思想,一个源程序将所有计算机要处理的信息——指令、数据、栈——划分到了不同的段中。

一个汇编程序至少要有一个代码段。

2)end

标志着一个程序的结束,编译器读到这里时就会知道这个程序结束了。

3)assume

将某一个段寄存器与我们在segment…ends定义的段关联起来。

2、【我们为了方便表述,将源程序文件中的所有内容称为源程序,将源程序中最终由计算机执行、处理的指令或数据称为程序】

【将源程序编译连接后会得到可执行文件,描述信息全部用十六进制来表示】

3、标号

就比如我们自己命名的段名称,它被称作标号,它指代了一个地址。

4、程序结构

assume将段寄存器与我们定义的段连接起来

每个段内的内容

结束标识

5、程序返回

DOS是一个单任务操作系统,我们如果想要运行程序P2,我们就必须要有一个程序P1正在运行,并且将控制权转给P2,当P2运行完成后,再将程序返回给P1

所以,在汇编语言中,

我们用:

mov ax,4c00H

int 21H

来实现程序返回

【备注:这与我们前面谈论的程序结束以及段结束不同(它们都是伪指令,由编译器运行),而这里汇编指令最终是由CPU执行的】

【dos系统是中断机制,而windows系统是消息机制(实现了多任务)】

【提示:在dos中,我们可以使用edit指令来编写文件】

【如果在执行masm命令时,将文件最后加上“;”,则会跳过许多提示输入部分】

【编译时一般只会有两种错误使我们无法得到预期文件:1)severe errors;2)找不到文件】

操作系统运行程序的方式:

操作系统是由多个功能模块构成的,任何通用的操作系统,都会提供一个shell(外壳)程序,用户使用这个程序来操作计算机进行工作。

DOS中有一个command.com程序,这个程序在DOS中称为命令解释器,也就是DOS系统的shell。

DOS启动时,会先进行初始化工作,之后便会启动command.com,之后,屏幕上便会显示出由当前路径组成的提示符,用户可以输入指令。

如果用户输入了一个可执行程序的文件名,系统则会运行这个程序,首先,command会找到这个文件,之后将这个文件中的程序载入内存,设置CS:IP指向程序的入口,之后command暂停运行,CPU运行这个程序,运行结束后,又会返回command。大致是如下过程:

编程(edit)→1.asm→编译(masm)→1.obj→连接(link)→1.exe→加载(command)→内存中的程序→运行(CPU)

【跟踪程序运行过程】

我们可以用debug来跟踪一个程序的运行过程,这样我们可以发现一些潜在的较难发现的错误。

使用debug来运行程序,它并不会放弃对CPU的控制,这样,我们就可以使用debug中的相关命令来查看每一条指令的执行结果。

当我们将程序用debug载入后,我们发现,cx中存放了程序的长度。

另外,补充说明DOS对EXE文件的加载过程。

首先,要找到一段起始地址为SA:0000的容量足够的空闲内存区;

其次,DOS会在前256(SA+0~SA+255)字节(2^8)创建一个被称为程序段前缀(PSP)的数据区,DOS要利用PSP来和被加载的程序进行通信。

再之后,DOS将程序装入,且程序的地址被设为SA+10H:0【注意这里是将段地址直接增加,而不是修改偏移地址】。此时,地址安排如下:

空闲内存区:SA:0

PSP区:SA:0

程序区:SA+10H:0

最后,该内存区的段地址被存入ds中(即DS=SA),初始化相关寄存器之后,设置CS:IP指向程序的入口。

【注意:此处忽略了被称为“重定位”的这一步骤,因为这个问题和操作系统关系较大,我们不做讨论】

【注意:debug中默认数据表示为16进制,所以,在debug中十六进制的数并没有用“H”表示】

【注意:使用debug运行程序,文件名不区分大小写,但一定要加上后缀名!!否则提示“File not found”】

【PSP前两个字节是CD 20】

当程序运行到int 21时,要用P命令执行,这样便会弹出提示”program terminated normally”

【使用debug运行程序,command会先打开debug,之后再用debug运行程序,程序正常运行结束后,便会返回debug,如果输入q或quit命令就会退出debug】

【笔记】8086汇编-寄存器(内存访问)

前言:本篇仅仅是笔记,整理于2017/7/26,结构可能会略有混乱,见谅。

【字单元】(与“单元”区分,单元一般是存储单元的简称)

概念:存在一个字型数据(16位)的内存单元,由两个地址连续的内存单元构成。高地址内存单元存放高位字节,低地址内存单元存放低位字节。

(为了方便,之后我们将起始地址为N的字单元简称为N地址字单元)

【DS与[address]】

mov指令功能:

1)直接将数据送入寄存器,指令占据三个单元

2)将一个寄存器内容送入另一个寄存器

3)将一个存储单元内的内容送入一个寄存器:“mov 寄存器,内存单元偏移地址”。

CPU在工作时,会将DS中的数据作为段地址,而[…]中的内容作为段地址,CPU便是从这里寻找要读取数据的内存单元的。

要注意:8086CPU在硬件设计时,不允许直接将数据使用mov送入ds中,只能先将数据送入通用寄存器,在用mov将通用寄存器中的数据送入ds。

如果使用:“mov 内存地址单元偏移地址,通用寄存器”,这样便可以将数据送入内存单元,由于通用寄存器是16位的,所以在存入时,会占据以设定位置的物理地址为低位的字单元。

【字的传输】

由于8086CPU的数据传输是16位的,因此如果执行 mov ax,[0],也会将该地址所在字单元的数据放入通用寄存器ax。(也就是会读取两个内存单元中的数据)

传输的位数会受指令中接受数据的寄存器大小影响,对于8086CPU,它的通用寄存器是十六位的,所以在读取数据时,一次会读取一个字。而如果是如同mov al,[0]这样的指令,由于al是八位寄存器,所以在读取数据时,一次只会读取一个内存单元中的数据。

【mov、add、sub指令】

mov指令使用方法:

mov 通用寄存器,数据

mov 通用寄存器,内存地址单元偏移地址

mov 通用寄存器,段寄存器(既然我们可以通过通用寄存器给段寄存器传数据,那么也就说明物理空间上存在从通用寄存器到段寄存器的通路,那么是否也存在从段寄存器到通用寄存器的通路,确实存在,所以,如“mov ax,ds”这样的指令是合法的)

mov 内存地址单元偏移地址,段寄存器(mov [0],cs这样的传输指令也是合法的)

mov 内存地址单元偏移地址,通用寄存器

需要注意:如“mov [0],23”这样的指令是非法的,也就是说,数据只能传给通用寄存器,而不能直接传送给段寄存器以及内存地址空间。

mov 段寄存器,内存地址单元偏移地址

mov 内存地址单元偏移地址,段寄存器

关于add与sub指令,这两个指令需要注意的是:都无法对段寄存器进行操作,也就是说像add ds,ds或add ax,ds、add cs,ax等这样的指令都是非法的。

 

【数据段】

定义:代码段是人为定义的、一段地址连续的、起始地址为16的倍数的内存单元。

类似于代码段,数据段也是认为定义的一个段,它的长度受限于偏移地址的位数,由于偏移地址是16位的,所以数据段长度也是不可以超过64KiB的。

 

【提示:可以根据自己的推测,在Debug中实验新的指令格式】

【关于物理地址计算,我们可以采用另一个步骤,比如要算0ff0:0100,这时,我们可以将0100H除以10H,得到0010H,之后用0ff0H + 0010H = 1000H,之后再乘10H,计算出物理地址为10000H】

【栈】

备注:这里的栈是一种具有特殊的访问方式的存储空间。(这便是这里我们的研究角度)

特性:最后进入这个空间的数据时最先出去的。(Last in first out. LIFO,后进先出)

【CPU提供的栈机制】

对于8086CPU,可以将一段连续内存空间当做栈来使用。

相关指令:

PUSH 入栈

POP 出栈

【提示:8086CPU出栈、入栈都是以字(16位)为单位的。(如果是32位CPU则是以双字(32位)为单位的)】

CPU是如何定位一个栈呢?

CPU通过段寄存器SS与寄存器SP来定位栈,任意时刻,SS:SP都指向当下的栈顶元素,使用push指令会使SS:SP指向的物理地址的编号减小,使用pop指令会使SS:SP指向的物理地址的编号增加。

执行步骤:

执行push指令:

1)SP=SP-2,SS:SP指向当前栈顶前面的单元(注意:前面我们学习了,单元是两个连续的存储单元,单元存储的数据种类是1个字),以当前栈顶前面的单元为新的栈顶。

2)将寄存器中内容送入SS:SP指向的内存单元处,同样是低8位放在低地址存储空间,高8位放在高地址存储空间。

执行pop指令:

1)将SS:SP指向的单元内数据送入寄存器

2)SP=SP+2。

【注意,执行pop后,数据仍然存放在内存空间中,只是指针位置发生了改变】

【注意,当栈内没有存放东西时,SP指向的单元不是栈顶,确定来说,指向的位置不在栈内部】

【备注,栈是一种逻辑上的东西,对于计算机而言,这仅仅是一种操作方式。栈的大小是人为规定的】

【C语言中,局部变量的实现实际上就是通过了栈,而这个栈是由编译器自动控制生成的。程序员往往需要手动操控的是堆,堆并不是连续的内存,而是非连续的,由线性表组合成的空间,所以要注意及时释放空间。】

 

【栈的超界问题】

对于8086CPU,它并没有被设计拥有检测栈的实际大小的功能,所以,在使用pop和push指令时,都有可能使导致超界,所以在设计时,要计算好入栈和出栈的次数,以防止访问到不属于栈的数据。

(C/C++语言的数据没有对数组进行溢出检查,这样可能会产生漏洞,使得数据被暴露)

 

【push、pop指令】

push与pop都可以操作:寄存器,段寄存器,内存空间

【注意,在使用push时,会先进行减小SP操作】

【实用结论,栈的初始物理地址应当是栈空间的最高地址+1】

【通过异或XOR ax,ax可以将ax寄存器清零(相同为0,不同为1)】

【ss段寄存器只能通过寄存器向其传送数据,而sp则可以直接使用mov将数据传输进入,所以add、sub也都可以操作sp寄存器】

【这时要注意防止混淆,因为8086cpu在设计时,不允许直接使用mov来操作cs与ip】

【受限于sp是十六位的寄存器,所以栈顶的变化范围最大是0~FFFFH】

测试:

(建议先将20000H到20010H存入1H到10H十六个数据)

1)尝试将20000H到20010的数据全部向后移动一个位置;

2)尝试将20000H到20010的数据全部向后移动两个位置;

【提示:对于通用寄存器,可以使用如同mov ax,[bx]这样的指令(mov [ax],bx则在debug中会在编写时报错),这叫做寄存器间接寻址,[bx]指代了内存地址单元(在试验时,不知为何,运行时会报错),不过[address]是不支持使用sp寄存器,即mov ax,[sp]是非法指令】

【提示:pop al是非法指令】

3)将10000H~1000FH中的8各字,逆序复制到20000H~2000FH中(答案不唯一)

【提示:push [address]以及pop [address]是合法指令】

【栈段】

和以前定义代码段和数据段一样,我们用相似的方法定义了栈段。

如果要将10000H~1FFFFH定义为栈段,应当如何操作?

我们可以想到,当栈内存放有1个元素时,我们的SS:SP为1000:FFFE,如果这时再进行pop出栈指令则SP会变为0,也就是说,如果我们初始将SS:SP定为1000:0,则此时便将10000H~1FFFFH定义为栈段。

【栈保存了一个函数调用时所需要的维护信息,这常常被称为堆栈帧或者活动记录,一般包含:1)函数的返回地址和参数;2)临时变量:包括函数的非静态局部变量以及编译器自动生成的其它零时变量】

【我们有时用SA(stack address)表示段地址,而使用OA(Offset Address)或EA(Effective Address)(有效地址)来表示偏移地址】

【关于Debug的补充】

初见端倪——中断机制:

我们发现,在执行:mov ss,ax、mov ss,[0]、pop ss这类指令后,下一条指令会自动的“连带执行”,这与我们以往使用“t”命令只执行一条指令时的现象不同,这边源于debug的中断机制,此处对于中断机制暂不详解,不过由此我们认识到,修改了ss寄存器的指令的下一条指令会被连着执行。

关于限定地址的命令(如d、e、a):

可以使用如:e ds:0这样的指令,也就是说,在表示SA时可以使用现有段寄存器名称来指代该寄存器内的数据。(注意:e ax:0是非法命令)

【笔记】8086汇编-寄存器

前言:本篇仅仅是笔记,整理于2017/7/19,结构可能会略有混乱,见谅。

一个典型的CPU由运算器、控制器、寄存器等器件构成,之间由内部总线相连(与上一章的外部总线区分)

在CPU中:

1)运算器进行信息处理

2)寄存器进行信息存储

3)控制器控制各种器件进行工作

4)内部总线实现内部器件信息传输

对于汇编程序员来说,他们通过改变寄存器的内容来控制CPU工作。

【学习内容:】

1、通用寄存器

2、字在寄存器的存储

3、基础指令

4、物理地址

5、段地址与偏移地址

6、段寄存器

CPU结构:

内部结构:运算器、控制器、寄存器等器件,通过内部总线相连【外部总线实现CPU与主板上其它器件连接,而内部总线负责内部连接】

8086CPU有14各寄存器:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。

【AX、BX、CX、DX成为通用寄存器】

【EAX比AX多了16位】

【AX、BX 、CX、DX可分为高位与低位分别是AL、AH、BL、BH、CL、CH、DL、DH】

【关于指令】

【为了使用方便,我们通常使用十六进制来表示寄存中的数据,因为二进制每四位可对应一个十六进制的位】

汇编语言不区分大小写!!

mov ax,18【AX=18】

mov ah,78【AH=78】

add ax,8【AX+=8】

mov ax,bx【AX=BX】

add ax,bx【AX+=BX】

【关于进位】超过存储范围的数据不会丢失,而会专门存储,但不会存储在原来的寄存器中(提醒:如果操作对象是al,进位不会进到ah,事实上al会被当做一个单独的寄存器)。

【练习】只能使用4条汇编指令,编程计算2的四次方

【思路】2^4是2×2×2×2,乘2可以当做自加

【解答】

mov ax,2

add ax,ax

add ax,ax

add ax,ax

【物理地址】

定义:我们将内存单元在内存空间内的唯一地址(或者说位置)称作它物理地址。

所有的内存单元构成了一个一维线性空间

【16位结构的CPU】

1)运算器一次最多可以处理16位的数据

2)寄存器的最大宽度为16位

3)寄存器和运算器之间的通路是16位的

而8086CPU有20位地址总线,可传送20位地址,寻址能力1M【2^20B=2MB】

内部只有16位结构,只能传送16位地址,表现出的寻址能力只有64K

为了解决位数不同的问题,8086CPU将两个16位的地址(段地址和偏移地址)进过加工变成20位的地址

计算方法:物理地址=段地址×16+偏移地址

【16进制(hex)的数乘16是向左移一位,也就是数据左移4位】

【一个a进制的数左移b位则相当于乘a^b】

【段的概念】

定义:段是人为区分的一段连续的内存空间,它并不实际存在,一个段的长度无法超过偏移地址取值范围。

内存并没有分段,由于我们使用段地址结合偏移地址的方式进行操作,我们人为将若干地址连续的内存单元看作一个段。

【合成物理地址在地址加法器中进行】

【注意】

1)由于段地址×16一定是16的倍数,所以一个段的的起始地址一定也是16的倍数【段是用来描述物理地址的】

2)偏移地址是16位,所以一个段的长度最大是64K(2^16B=64KB)

3)CPU可以通过不同的段地址和偏移地址组合找到同一个物理地址

我们可以用“段地址:偏移地址”来描述一个物理地址,比如:“2000:1F60”即21F60H内存单元;也可以说:数据存在内存的2000段的1F60中

【注意:“段地址×16+偏移地址=物理地址”,实际上是“基础地址+偏移地址=物理地址”的一种实现方式,这里的“段地址×16”便可理解为是一种基础地址】

【段寄存器】

段寄存器提供段地址

8086CPU有四个段寄存器:CS(code segment)、DS(data segment)、SS(stack segmetn)、ES(extra segment)

当计算机要访问内存中的内容时,这时就需要段寄存器。

【CS与IP】

CS是代码段寄存器,IP(Instruction pointer)是指令指针寄存器。

执行过程:CS、IP中的数据→地址加法器→输入输出控制电路→地址总线→找到指令

指令数据→数据总线→输入输出控制电路→指令缓冲器

IP自加,指向下一条指令

指令缓冲器→执行控制器

8086CPU在加电启动时,CS和IP会被设置为CS=FFFFH,IP=0000H,即在8086PC机刚启动时,CPU从内存单元FFFF0H开始执行指令。

【一个存储单元存放1byte,对于指令mov ax,0003H需要3各Byte来存放,即占据三个存储单元;而对于mov ax,bx来说,只需要2各byte来存放,即只占据两个存储单元】

【如果要计算物理地址A到B之间有多少个存储单元,则应当用公式:存储单元数=B-(A-1),注意,这时A与B都算在内】

8086CPU不能用传送指令mov(mov指令被称为传送指令)来控制CS和IP中的内容,而是用转移指令(能够改变CS、IP内容的指令被统称为转移指令)jmp

如:jmp 2AE3:3【意为2AE33H】

jmp 3:2A23【意为0003:2A23即02A53H】

如果只想改变IP寄存器内的信息,则可以使用

jmp 某一合法寄存器

如:

jmp ax【意思是将ax内的数传入IP中(打个比喻,相当于mov ip,ax,注意此处仅是为了方便理解与记忆,并不是说真的存在mov ip,ax)】

【代码段】

将长度为N(N≤64KB)的一组代码存放在连续的内存单元中,我们可以将这段内存定义为代码段(这样方便我们设计)

【实验1-Debug】

【一些Debug的命令】

R命令查看、改变CPU寄存器的内容。【如:“r”或“r ax”】

D查看内存中的内容【假如我们想查看10000H处内存中的内容,我们可以输入“d 1000:0”,这样debug将列出从指定的内存单元开始的128个内存单元内容】【注意:输出时,每行的标号都是16的整数倍,如果输入1000:1,这样在输出时第一行仍然是1000:0000,但是第一个存储单元位置上显示为空(即此时未进行访问的10000H中内数据)】【在输出信息中,“-”前后各有8个数据】【右边是每个的内存单元中数据对应可显示的ASCII码字符,没有对应的则会显示“.”】【在使用一次d后,再次输入d(即便中间已经隔了好几个指令),可查看上面存储单元之后的128各存储单元的内容】【如果想要查看10000H到10010H中的内容,可以输入”d 1000:0 9“,实际上这就是连续‘限定’了两个ip,一个是起始时的ip,一个是终止时的ip】【也可以d 偏移地址,这时,ds中的值被当做段地址】

E改写内存中的内容【可以使用”e 起始地址 数据 数据 数据 数据 ……“来直接修改内存中的数据】【另外也可以用”提问式“的方式输入,方法是”e 起始地址“,之后会输出该起始地址,并在后面输出一个存储空间的原始数据以及一个”.“,光标会停在”.“后这时可输入要修改为的数据,按下回车键(Enter),debug又会输出下一个存储空间的数据,同样将光标停在”.”后等待用户输入。】【第三种方法”e 起始地址 ASCII字符 ASCII字符 数据 数据……“ASCII字符会自动转化为数据】【也可以输入字符串,字符串需要用””来标注】【注意:若采用提问式输入,则无法输入字符或字符串,这点其实很好想,如果这时输入了e,那么debug是该当作字符呢?还是当作十六进制数据呢?所以这里只可以输入十六进制数据。】

U将内存中的机器指令翻译成汇编指令

T执行一条机器指令

A以汇编指令的格式在内存中写入一条机器指令【可在后面加上”初始地址“作为参数,如果不加初始地址,则会从默认地址开始输入】

【mov、add、sub指令】

mov的使用:

mov 段寄存器,通用寄存器

mov 寄存器,段寄存器(既然我们能够从通用寄存器将数据传送到段寄存器中,那么很容易想到,这里应当设置有从通用寄存器到段寄存器的通路,很自然地想到,或许有反向通路,即从段寄存器到通用寄存器,结果是,这样的数据通路确实存在)

mov 通用寄存器,数据

mov 通用寄存器,通用寄存器(即便是mov ax,ax也是合法指令)

mov 通用寄存器,内存地址单元偏移地址

mov 内存地址单元偏移地址,通用寄存器

mov 内存地址单元偏移地址,段寄存器(包括ds)

【在debug中,如果指令涉及到内存地址,则运行到这条指令时,使用r可以在最右端看到提示内存单元中的内容,格式是 “DS:偏移地址=数据”(如果这里访问的是十六位寄存器,右边便会提示出一个字型数据(4位十六进制数),如果是像al这样的八位寄存器,则会提示出该存储单元的数据(2位十六进制数))】

关于add指令,需要注意,add指令的操作参数不可以有段寄存器,即add ds,ax或者add ds,ds、add cs,ax等都是非法的。(sub同理)这里我们得出了结论:add与sub指令都不可对段寄存器进行操作。

【笔记】8086汇编-内存地址空间

前言:本篇仅仅是笔记,整理于2017/7/19,结构可能会略有混乱,见谅。

解释:虽然各种器件物理空间上不连续,但是它们都有两个共性:1)它们都与CPU通过总线相连;2)CPU都可以向它们的内存发出读写命令。

定义:各种器件的内存在逻辑上呈线性排列,可以说,它们组成了一个一维的空间,我们将这个空间称作内存地址空间。

影响:内存地址空间的大小受限于地址总线的宽度。

提醒:不同计算机系统的内存地址空间分配情况是不同的。8086CPU的内存地址空间被这样分配:

00000H~9FFFFH:主存储地址空间

A0000H~BFFFFH:显存地址空间

C0000H~FFFFFH:各类ROM空间(对此空间执行写入命令是无效的)

各类存储芯片从读写上可分为两类:随机存储器(RAM)和只读存储器(ROM)。

1)随机存储器:用于存放供CPU使用的大多数数据,主随机存储器一般由两个位置上的RAM构成,装在主板上的RAM和扩展插槽上的RAM。

2)装有BIOS(Basic Input/Output System,基本输入/输出系统)的ROM:BIOS是由主板和各类接口卡厂商提供的软件系统,存储在各自的ROM中,ROM中的内容断电后不会丢失,无法常规执行写入命令。(相比之下,RAM中的内容如果断电就会丢失,并且RAM支持读写)

3)某些接口卡上的RAM:

它提供给接口卡存储大批量输入、输出数据的功能。最典型的便是显卡上的RAM,即我们称为显存,显卡随时将显存的数据向显示器输出,换句话说,我们将需要显示的内容以数据形式写入显存,显存再将数据输出到显示器上。

【主板】

主板上有核心器件和一些主要器件,这些器件通过总线相连。这些器件有CPU、存储器、外围芯片组、扩展插槽等。扩展插槽上一般插有RAM内存条和各类接口卡。

【笔记】8086汇编-基础

前言:本篇仅仅是笔记,整理于2017/7/19,结构可能会略有混乱,见谅。

【基础知识】

【引入】:汇编语言是直接在硬件之上工作的编程语言,故而为了有效地使用汇编语言进行编程,我们需要对硬件系统的结构有一些了解。

【如果想要深入了解:PC机以及CPU的物理结构和编成结构的全面研究属于《微机原理与接口》的问题;对于计算机一般结构与性能则在《组成原理》之类课程中学习】

CPU(Central Processing Unit,中央处理单元),CPU是一种微处理器,它通过处理二进制信息来工作,而二进制信息是通过控制电平高低来实现的

每一种微处理器,由于硬件设计和内部结构的不同,需要用不同的电平脉冲来控制,所以每一种微处理器都有自己的机器指令集,即<strong>机器语言</strong>。

早期的程序员,是通过在纸带上打孔来“编程”的,这样用“二进制代码”编程的方式十分不方便,且极易出错、同时出错后不易排误,种种问题给整个行业的发展带来了障碍,于是<strong>汇编语言</strong>诞生了。

汇编语言是便于人类书写与记忆的指令形式,它用文字来代替仅仅是顺序发生变化的0与1二进制数。

然而,计算机是无法读懂汇编语句的,所以如果想要用汇编指令来操作计算机,就必须要有一个“翻译官”——编译器。程序员用汇编语言写出源程序,再交给编译器进行编译,最后由计算机执行。

【汇编语言组成】

1)汇编指令:机器码的助记符,有对应机器码

2)伪指令:没有对应机器码,由编译器执行,计算机不执行

3)其他符号:如+、-、*、/,由编译器识别,没有对应机器码

【关于存储器】

磁盘中的文件必须要读到内存中才可被CPU使用,RAM负责提供数据,而ROM也负责提供指令。(注意:提到存储器,不仅仅指代内存,还包括显存等,它们在CPU那里呈现一维线性排列的逻辑结构)

【指令与数据】

指令与数据无本质区别,如89D8H既可以看做数据,也可以当做指令“mov ax,bx”,关键区分它们的在于传输时用的总线。

【存储单元】

一个存储单元可以存放1Byte的信息,即8bit信息【bit是最小信息单位】

【1TB=1024GB、1GB=1024MB、1MB=1024KB】

【实际上应当是GiB、MiB、KiB,这是国际标准化组织(ISO)2008年规范的1024进位的数据单位,它们是二进制单位,而我们平常用的GB实际上是1000进位单位,由于一向的误用,故而我们今日习惯于使用GB而不是GiB】

【K、M、G是国际单位制词头,用10^3换算,而Ki、Mi、Gi是二进制乘数词头】

【在使用上,计算机内存大部分使用二进制表示(如:随机存储器、只读存储器、闪存等,因为连接的地址总线有对应的合法地址,这样使用方便),CD使用二进制,而硬盘、USB闪存卡、DVD、总线使用十进制表示,对于总线使用十进制的原因,或是因为时钟速度】

【以上仅为人为描述信息量时使用,计算机进行处理一定是二进制的】

【在windows系统中,本应标为KiB、GiB等单位被标为了KB、GB等,造成了一些误解】

【这便是买下硬盘、U盘后容量“缩水”的原因】

(KB=kilobyte千字节)

(MB=megabyte兆字节)

(GB=gigabyte吉字节)

(TB=tarebyte太字节)

(PB=petabyte拍字节)

(EB=exabyte艾字节)

(ZB=zettabyte泽字节)

(YB=yottabyte尧字节)

(BB=Brontobyte)

(KiB=kibibyte【kilo binary byte】)

(MiB=Mebibyte【mega binary byte】)

(GiB=gibibyte【giga binary byte】)

【参考资料:

https://www.zhihu.com/question/29680323

https://www.oiegg.com/viewthread.php?tid=1508640】

【IEEE对此也有倡议文件】

【CPU对存储器的读写】

CPU必须与外部器件进行下面三类信息交互:

1)存储单元的地址(地址信息)

2)器件的选择,读或写的命令(控制信息)

3)读或写的数据(数据信息)

以上内容通过所谓<strong>总线</strong>来实现。

在物理层面,总线是一根根导线的集合。

根据传输信息的不同,总线从逻辑上分为3类:地址总线、控制总线、数据总线。这三类总线分别承担了以上三类信息的传输。

【提示:n位二进制数可传送2^n个不同数据,最小值为0,最大值为2^n-1】

【寻址线】

地址总线(AB,address bus)

CPU有N根地址线,我们说这个CPU的地址总线宽度为N,这样的CPU可以寻访2^N各内存单元。

【地址总线的宽度限定了CPU能操作的存储器的大小】

【数据线】

数据总线(data bus)

8088CPU数据总线宽度为8,8086CPU数据总线宽度为16。故对于数据89D8H,8088CPU需要拆两次进行数据传输,第一次传送D8,第二次传送89。(先传送低位,再传送高位,”先低后高“)

【数据总线的宽度限定了CPU与外部器件之间信息的传输速率】

【控制线】

控制总线(control bus)

与前面两种总线不同的是,控制总线是许多不同控制线的总称,如:有一根称为“读信号输出”的控制线负责由CPU向外传送读信号,而“写信号输出”则负责传送写信号。有多少根控制总线,就意味着CPU有多少总操作。

【小结】

1)汇编指令是机器指令的助记符,与机器指令一一对应

2)每一种CPU有自己的汇编指令集

3)只有存储器中存放的信息是CPU可以直接使用的信息

4)在存储其中,数据与指令的存在形式没有区别,都是二进制信息

5)存储单元从零开始

6)每一个CPU都有很多管脚,这些管脚与总线相连,或者说,它们引出总线。