汇编语言

BASE

机器语言

  • 只有0和1

汇编语言

  • 汇编语言的主体是汇编指令
    • 汇编指令和机器指令的差别在于指令的表示方法上汇编指令是机器指令便于记忆的书写格式
    • 汇编指令是机器的助记符
  • 汇编语言由3类组成
    • 汇编指令(助记符)
    • 伪指令(由编译器执行)
    • 其他符号(如:+、-、*、/,由编译器识别)

指令和数据

  • 在内存或磁盘上,都是二进制信息,无区别

    • 1000100111011000=>89D8H		
      1000100111011000=>mov AX,BX
      

总线

  • 地址总线:CPU通过地址总线来指定存储单元

  • 数据总线:CPU与内存或其他器件之间的数据传送是通过数据总线来进行的

  • 控制总线:CPU对外部器件的控制是通过控制总线来进行的。

    有多少根控制总线,就意味着CPU提供了对外部器件的多少种控制。
    所以,控制总线的宽度决定了CPU对外部器件的控制能力。

各类存储器芯片

存储器芯片按读写属性分为随机存储器(RAM)和只读存储器(ROM),RAM可读可写,但必须带电存储,关机后存储内容消失,ROM只能读取不能写入,关机后内容不丢失。

存储器按功能和连接又可分为

  1. 随机存储器:用于存放供CPU存放的绝大部分程序和数据,一般由装在主 板上的RAM 和装在扩展槽上的RAM组成。
  2. 装有BIOS(Basic Input/Output System,基本输入/输出系统)的ROM:我们可以通过BIOS(一种软件系统)利用硬件设备进行最基本的输入输出,主板上发ROM中存储着主板的BIOS,显卡上的ROM存储着显卡的BIOS。
  3. 接口卡上的RAM:某些接口卡需要对大批量输入/输出数据进行暂时存储,所以装有RAM,如显示卡上的RAM,一般称为显存,我们将需要显示的内容写入显存,就会出现在显示器上。

存储单元

  • 存储器分若干存储单元,从0开始顺序编号

  • 对于大容量的存储器一般还用以下单位来计容

    • 1KB=1024B
      1MB=1024KB
      1GB=1024MB
      1TB=1024GB
      

CPU对存储器的读写

  • 存储单元地址=====>地址总线
    • CPU通过地址总线指定存储单元
    • N位CPU=>CPU地址总 =>CPU有N根地址总线=>最多存放2^N个内存单元
  • 命令(如读写)===>控制总线
    • 对外部器件的控制
    • 控制总线的宽度对外部器件控制能力,线越多控制越多器件
  • 数据内容========>数据总线
    • 数据总数宽度决定了CPU和外界数据传输速度

CPU对外设的控制

  • CPU对外设都不能直接控制,如显示器、音箱、打印机等。直接控制这些设备进行工作的是插在扩展插槽上的接口卡。扩展插槽通过总线和CPU相连,所以接口卡也通过总线同CPU相连。CPU可以直接控制这些接口卡,从而实现CPU对外设的间接控制。

如:CPU无法直接控制显示器,但CPU可以直接控制显卡,从而实现对显示器的间接控制

内存地址空间

CPU将系统中各类存储器看作一个逻辑存储器,这个逻辑存储器就是我们所说的内存地址空间。
举例,一个CPU的地址总线宽度为10,那么可以寻址1024个内存单元,这1024个可寻到的内存单元就构成这个CPU的内存地址空间。

小结

  1. 汇编指令是机器语言的助记符
  2. 每一种CPU都有自己的汇编指令集
  3. CPU只能使用存放在存储器的信息
  4. 存储器中指令和数据毫无区别都是二进制信息
  5. 存储单元从零开始编号,一存储单元存8 bit
  6. 三种总线宽度决定了CPU不同方面的性能

寄存器

1.内部总线

上文所说三大总线,是CPU控制外部设备,用于CPU和外部部件连接的。而CPU内部有寄存器,运算器,控制器等组成,由内部总线相连,内部总线负责连接CPU内部的部件

2.通用寄存器

8086CPU寄存器都是16位的,一共14个,分别是AX,BX,CX,DX,SI,DI,SP,BP,IP,CS,SS,DS,ES,PSW。其中AX,BX,CX,DX四个寄存器通常存放一般性数据,称为通用寄存器

且为兼容上一代8位寄存器,每一个寄存器可以拆开成两个八位寄存器来使用。

AH,AL,BH,BL,CH,CL,DH,DL

低八位构成L寄存器,高八位构成H寄存器

3.字

8086CPU可以处理以下两种数据

  • 字节byte-8位
  • 字word-16位

4.简单汇编指令

指令 操作 高级语言
mov ax,18 将18存入AX寄存器 AX=18
add ax,8 将AX寄存器中数加8 AX=AX+8
mov ax,bx 将BX中的数据存入AX AX=BX
add ax,bx 将AX,BX中数据相加,结果放入AX AX=AX+BX

注:汇编指令不区分大小写

AX寄存器当做两个8位寄存器al和ah使用的时候,CPU就把他们当做两个8位寄存器使用,而不会看成是一个16未分开,即如果al进行加法运算C5+93=158,即add al,93,al会变成58,ax则是0058而不是0158。

4.8086CPU计算物理地址

8086CPU有20位地址总线,所以寻址能力2^20即1MB

8086CPU又是16位结构,在内部一次性处理、传输、暂时存储的地址为16位。

从8086CPU的内部结构来看,如果将地址从内部简单地发出,那么它只能送出16位的地址,表现出的寻址能力只有64KB。

8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。

  • CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址;
  • 地址加法器将两个16位地址合成为一个20位的物理地址;
  • 地址加法器采用物理地址 = 段地址×16 + 偏移地址的方法用段地址和偏移地址合成物理地址。

例如,8086CPU要访问地址为123C8H的内存单元,1230H左移一位(空出4位)加上00C8H合成123C8H

5.段寄存器与指令指针寄存器

  • 8086CPU有四个段寄存器:
    • CS:代码段
    • DS:数据段
    • SS:栈段
    • ES:其他段

除此之外,IP寄存器称为指令指针寄存器,所以任意时刻可以读取从CSx16+IP单元开始,读取一条指令执行。也就是说,CPU将IP指向的内容当做指令执行。

P26图,CPU执行一段指令。另外,8086CPU开机时CS被置为FFFFH,IP被置为0000H,也就是说刚开机的第一条指令从FFFF0H开始读取执行。

CPU将CS:IP指向的内存中的内容当做指令,一条指令被执行了,那一定被CS:IP指向过。

修改CS,IP

CS和IP寄存器不可以使用传送指令mov来改变,而能改变CS,IP内容的指令是转移指令。

jmp指令用法:

  • jmp 段地址:偏移地址 同时修改CS和IP的值 如jmp 2AE3:3 结果CS=2AE3H IP=0003H
  • jmp 某一合法寄存器 只修改IP的值 如jmp ax,将IP的值置为AX中的值(AX不变)
小结

8086CPU有四个段寄存器,CS是用来存放指令的段地址的段寄存器

IP用来存放指令的偏移地址

CS:IP指向的内容在任意时刻会被当做指令执行

使用转移指令修改CS和IP的内容

Debug命令:

  • R:查看,改变CPU寄存器内容
    • 直接-r查看寄存器内容
    • -r 寄存器名,改变寄存器内容
  • D:查看内存中内容
    • -d直接查看
    • -d 段地址:偏移地址 查看固定地址开始的内容
    • -d 段地址:偏移地址 结尾偏移地址 查看指定范围内存
  • E:改写内存中内容
    • -e 起始地址 数据 数据 数据 …
    • 提问方式修改 -e 段地址:偏移地址 从这个地址开始一个一个改,空格下一个,回车结束
    • 也可以写入字符 ‘a’
  • U:将内存中的机器指令翻译成汇编指令
    • -u 段地址:偏移地址
  • T:执行一条机器指令
    • -t 执行cs:ip指向的命令
  • A:以汇编指令格式在内存中写入一条机器指令
    • -a 段地址:偏移地址 从这个地址开始一行一行的写入汇编语句

  • 栈是一种后进先出的存储空间,从栈顶出栈入栈。LIFO(last in first out)
 入栈指令:push ax ax中的数据送入栈顶
 出栈指令:pop ax 栈顶送入ax
  • 入栈和出栈指令都是以字为单位的。

1.栈寄存器SS,SP与push,pop

CPU通过SS寄存器和SP寄存器来知道栈的范围,段寄存器SS存放的是栈顶的段地址,SP寄存器存放的是栈顶的偏移地址。所以,任意时刻SS:SP指向栈顶元素。

  • 指令push ax执行过程:
    • SP=SP-2,SP指针向前移动两格代表新栈顶
    • AX中的数据送入SS:SP目前指向的内存字单元
    • 所以栈顶在低地址,栈底在高地址。
  • pop ax执行过程相反。
    • 将SS:SP指向的内存单元出的数据送入ax中
    • SP=SP+2,SS:Sp指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶
  • 初始状态下,SP指向栈底的下一个单元。

注:8086CPU并不会自己检测push是否会超栈顶,pop是否会超栈底。

2.push和pop可以加寄存器,段寄存器,内存单元(直接偏移地址[address])

指定栈空间通常通过指定SS来进行,如:

指定10000H~1000FH为栈空间
mov ax,1000
mov ss,ax
mov sp 0010

注:若设定一个栈段为10000H~1FFFFH,栈空的时候SP=0(要知道入栈操作先SP-2,然后再送入栈)

栈顶越界问题

当栈满的时候再使用push指令入栈,栈空的时候再使用pop指令出栈,都将发生栈顶越界问题,也即溢出

栈顶越界是危险的,因为既然安排一段空间为栈,那么在栈空间之外的空间很可能存放了具有其他用途的代码、数据等,这些数据、代码很可能是我们自己的程序的,也有可能是别的程序的。

8086CPU只会知道SS:SP位置,而无法判断SC:SP是否到达栈顶、栈底,是否越界。

3.栈段

与代码段、数据段相似

源程序

assume cs:codesg

codesg segment

start: mov ax,0123H
		mov bx,0456H
		add ax,bx
		add ax,ax
		
		mov ax,4c00H
		int 21h
		
codesg ends
end					

1.伪指令

没有对应的机器码的指令,最终不被CPU执行。

伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作

2.定义一个段

segment和ends是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令。

格式:

段名  segment

...

段名 ends

一个汇编程序是由多个段组成的,这些段被用来存放代码、数据或当作栈空间来使用。

注意:end是程序结束的标志,ends是段结束的标志

3.寄存器与段的关联假设

assume:含义为“假设”。

通过assume假设某一段寄存器和程序中的某一个用segment……ends 定义的段相关联。在需要的情况下,编译程序可以将段寄存器和某一个具体的段相关联

4.程序经编译后变成机器码

5.语法错误和逻辑错误

程序编译时编译器识别不了错误语法

逻辑错误指程序在编译时不能表现出来的、在运行时发生的错误,逻辑错误一般不容易发现

6.编译连接运行源程序

这里从网上找代码

;hello.asm
DATAS  SEGMENT	//定义段,段名叫DATAS
     STRING  DB  'Hello World!',13,10,'$'
DATAS  ENDS	//标志结束

CODES  SEGMENT//定义段,段名叫CODES
     ASSUME    CS:CODES,DS:DATAS//将CODES与CS关联起来,将DATAS与DS数据关联起来
     
START:					//程序入口地址,在最后end后同样标志
     MOV  AX,DATAS
     MOV  DS,AX
     
     LEA  DX,STRING
     
     MOV  AH,9
     INT  21H 
     MOV  AH,4CH
     INT  21H
CODES  ENDS		//代码段结束
    END   START //程序结束,程序入口标识为START

命名 2.asm

使用命令

masm 2.asm
link 2.OBJ

注意可以加“ ;” 快速跳过完成

运行2.exe,成功

问题:2.exe是如何运行的?

1.我们在dos中运行2.exe,是正在运行的command将2.exe导入内存

2.command设置CPU的CS:IP指向程序的第一条指令,即程序的入口,程序得以开始运行

3.运行完毕后,返回到command,CPU继续执行command

连接作用

  • 当源程序很大时,可以将其分为多个源程序文件来编译,每个源程序编译成为目标文件后,再用连接程序把它们连到一起,生成一个可执行文件

  • 程序调用某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接在一起,生成一个可执行文件

汇编程序从写出到执行

7.程序执行过程的跟踪

  • 1.asm
assume cs:codesg

codesg segment
	
linan:mov ax,0123H
	mov bx,0456H
	add ax,bx
	add ax,ax
	
	mov ax,4c00H
	int 21H
	
codesg ends
end linan 

end 后面标号就是代表这个程序入口的地址,可以用其他字符

  • 编译连接

  • 用debug运行1.exe,可以看到cx存放是程序代码长度,意思是:1.exe中程序的机器码共有十五个字节,我们也可以发现当前CS:IP指向的第一条指令就是我们程序入口的第一条指令mov ax,0123H

  • 现在程序已经从1.exe载入内存,接下来我们查看他的内容,有如下问题或者说现象

  • DS和CS相差10H的地址,换成物理地址就是100H,造成原因是存在PSP。DOS利用PSP与被载程序进行通信,所以程序正真开始执行的地方是在下图SA+10H:0而不是SA:0

  • 可以查看psp的内容

    -u 0b6d:0 256
    

小结

程序加载后,ds存放着程序所在内存区的段地址,如果偏移地址为0,那么程序所在的内存区的地址为DS:0;

这个内存区的前256个字节中存放的是PSP,用于DOS和程序通信

从256字节后空间才存放我们的程序代码

  • 我们用t单步运行程序,一切照常,这里要注意的是在执行INT 21指令要用P命令,然后会返回“program terminated normally”,表示程序正常结束,返回debug中

小结

我们在用DOS运行“debug 1.exe”,程序加载顺序是:command加载debug,debug再加载1.exe,返回就倒过来

完成实验三

[BX]和loop指令

1.[BX]

和[0]有些类似,[0]表示内存单元,他的偏移地址是0

mov ax,[0]

mov al,[0]

mov ax,[bx]

mov al,[bx]

编译器不会跟debug一样把[0]识别为偏移地址为0,而是直接认为是数据0,所以mov ax,[0]后,ax就是0,要想实现前者,就必须用[bx]来表示,编译器才会认为是一个偏移地址

  • 为了方便表述,书里增加了描述性符号(),表示某寄存器存放的值

    如:mov ax,[bx]

    代表的意思是是:bx中存放的数据作为一个偏移地址,段地址默认在ds中,将ds:[bx]中的数据送入ax中

    可以用描述性符号表示为(ax)=((ds)*16+(bx))

问题5.1

写程序

assume cs:abd

abd segment

start:

mov ax,2000h
mov ds,ax
mov bx,1000h
mov ax,[bx]
inc bx
inc bx
mov [bx],ax
inc bx
inc bx
mov [bx],ax
inc bx
mov [bx],al
inc bx
mov [bx],al

mov ax,4c00h
int 21h

abd ends

end start

已知21000h~21007h内存单元内容情况如下

BE 21000h
00 21001h
00 21002h
00 21003h
00 21004h
00 21005h
00 21006h
00 21007h

解题:把程序编译链接后debug运行,逐条t指令运行,每步执行后分析如下:

  • mov ax,2000h
    mov ds,ax
    mov bx,1000h

    //设置ds和bx,此时ds=2000,bx=1000

  • mov ax,[bx]

    //将2000:1000的内存地址内容传给ax,此时ax=00be

  • inc bx
    inc bx

    //自增1,两行就加2,此时bx=1002

  • mov [bx],ax

    //把ax的值传给2000:1002,此时21002~21003的值是be 00

  • inc bx
    inc bx

    //自增1,两行就加2,此时bx=1004

  • mov [bx],ax

    //把ax的值传给2000:1004,此时21004~21005的值是be 00

  • inc bx

    //自增1,此时bx=1005

  • mov [bx],al

    //把al的值传给2000:1005,此时21005的值是be

  • inc bx

    //自增1,此时bx=1006

  • mov [bx],al

    //把al的值传给2000:1006,此时21006的值是be

BE 21000h
00 21001h
BE 21002h
00 21003h
BE 21004h
BE 21005h
BE 21006h
00 21007h

2.Loop指令

循环的意思,指令的格式是:loop标号,cpu执行loop指令的时候,要进行两步操作

  1. (cx)=(cx)-1;
  2. 判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行

通常我们用loop指令来实现循环功能,cx中存放循环次数。

3.在的debug中跟踪用loop指令实现的循环程序

1.用loop指令,求2^12=?

assume cs:a

a segment

start:
	mov ax,2
	mov cx,11
	s:add ax,ax
	loop s
	
	mov ax,4c00h
	int 21h
	
a ends

end start
  • debug跟踪

可以看到,CX原本存放代码字节长度,之后被我们设为loop条件值,就是循环次数

loop 0006中0006就是s在程序的偏移地址,标号就是偏移地址

当执行loop时,程序判断(cx)=(cx)-1是否为零,不为零自动跳到我们得标号s处,继续执行add ax,ax

  • 当执行loop时,程序判断(cx)=(cx)-1为零,不跳到标号,继续下行
  • 程序正常结束,得到ax=1000h,2^12=1000h

2.计算ffff:0006单元中的数据乘以123,结果储存在

assume cs:a
a segment
start:
	mov ax,0ffffh
	mov ds,ax
	mov bx,6
	mov ax,[bx]
	mov dx,0
	mov cx,123
s:	add dx,ax
	loop s
	mov ax,4c00h
	int 21h
a ends
end start

注意:数据如果是字母开头的必须前面加0,因为编译器不认识

如:mov ax,ffffh得改为mov ax,0ffffh

  • 当循环次数过多,可以用G命令或P命令跳出循环

4.debug和汇编编译器Masm对指令的不同的处理

mov ax,[0]

在debug中将ds:0数据送入al中;但在汇编源程序中,masm编译器会把其当作 mov ax,0 处理。当然可以改成mov ax,ds:[0] ,就可以实现。

5.loop和[bx]的联合应用

计算ffff:0~ffff:b单元中的数据和结果储存在dx中

这道题需要考虑几个问题

1.运算结果是否会超出dx的存储范围

​ 由于存储的是字节型数据,范围在0~255之间,12个这样的数据相加,结果不会大于65535,不会超出dx范围

2.能否直接将数据直接累加到dx中

​ 不可以直接累加,因为数据是8位,而dx是16位

3.可否累加到dl中,并置(dh)=0

​ 不可以,这显然会造成进位丢失

综上总结为类型匹配和结果越界问题

目前,可以使用一个16位寄存器作为中介,将内存8位数据赋值到一个16位寄存器ax中,再将ax数据加到dx中,实现数据类型匹配且不越界,源代码如下

assume cs:codesg

codesg segment

start:
mov ax,0ffffh
mov ds,ax
mov bx,0
mov dx,0
mov cx,12

s:
mov al,[bx]
mov ah,0
add dx,ax
inc bx
loop s

mov ax,4c00h
int 21h

codesg ends
end start

注意ffff前一定要加零,不然会报错,0ffff编译器才认识

:happy:

查看ffff:0~b的值

debug运行程序后,结果是476,验算后也是476

6.段前缀

指令"mov ax,[bx]"内存单元的偏移地址由bx给出,而段默认地址在ds中。此时的ds就叫做段前缀。

像出现在访问内存单元的段地址的”ds:“、”cs:“、”ss:”或“es:”,在汇编语言中称为段前缀。

7.一段安全空间

若在8086模式中,随意向一段内存空间写入内容是很危险的,因为这段空间中可能存放着重要的系统数据或代码。

在纯DOS的情况下,可以忽略dos,直接用汇编语言取操作真实的硬件,因为运行在cpu实模式下的dos没有能力对硬件系统进行全面、严格的保护。但在Windows 2000、unix这些运行在cpu保护模式下的操作系统中,不理会操作系统,用汇编语言去操作真实的硬件是不可能的。

所以避免发生触及其他程序内存,我们需要一段相对安全的空间,让我可以直接随意操作

一般情况下,0:200~0:2ff空间中是没有系统和其他数据的代码

8.段前缀使用

把ffff:0~ffff:b数据放到0:200开始处

实现如下:

assume cs:abc
abc segment

start:
mov ax,0ffffh
mov ds,ax
mov bx,0
mov cx,12
mov dx,0
mov ax,0020h
mov es,ax		//这里使用了es,可以减少对ds段前缀的频繁更改,优化效率
s:
mov dl,[bx]
mov es:[bx],dl
inc bx
loop s

mov ax,4c00h
int 21h

abc ends
end start