汇编中文教材下载地址
汇编语言王爽第三版pdf
http://www.downcc.com/soft/284615.html
王爽教材第二版答案pdf版(略有一点不同)
使用win10打开chm文件请不要勾选总询问,否者看不到内容
常见进制对应关系
Binary | Octal | Decimal | Hexadecimal |
---|---|---|---|
0000 | 00 | 0 | 0 |
0001 | 01 | 1 | 1 |
0010 | 02 | 2 | 2 |
0011 | 03 | 3 | 3 |
0100 | 04 | 4 | 4 |
0101 | 05 | 5 | 5 |
0110 | 06 | 6 | 6 |
0111 | 07 | 7 | 7 |
1000 | 10 | 8 | 8 |
1001 | 11 | 9 | 9 |
1010 | 12 | 10 | A |
1011 | 13 | 11 | B |
1100 | 14 | 12 | C |
1101 | 15 | 13 | D |
1110 | 16 | 14 | E |
1111 | 17 | 15 | F |
什么是Intel/英特尔
最大的CPU厂商,个人计算机零件,美国公司,1968年7月16日创立
Intel的全拼是integrated electronics 集成电子
公司大佬都是来自贝尔实验室,天才聚集地
创始人,罗伯特·诺伊斯(1927-1990),戈登·摩尔(1929-..)
数学物理界化学界的大佬,两人都是穷人出身,但是19-20岁就已经很秀了,大奖一堆
20多岁就在肖克利半导体公司拿到了金碗,然后还在这个公司找到一堆道友
30-32岁,离职,找仙童公司老板给他们3600美元开了个仙童半导体公司
41-43岁开了两人终于开了家自己的公司,Intel,做的东西也越来越牛逼了
面向个人的微处理器就在这家公司诞生了,PC从此走进家家户户
没有CPU就没有个人PC,没有个人PC就没有网络
也没有现代编程,CPU和半导体这些东西都是从美国传过来的
所以要想学好编程,英文网站和英文教程就得看的懂
不然你只能看国内看剩的,很多为什么国内根本找不到,因为书上就是这么写的
什么是半导体?
简单来说就是可控开关,不过这种开关非常的小,通电断电0和1变的可以控制了
有了半导体就可以做出非常精密的电路系统
数以亿计的可控开关组成了数字电路,于是电路有了智慧(有了逻辑)
半导体的发展对CPU是非常重要的,半导体做的越小
CPU就可以塞更多的寄存器,计算能力更强,存储更多的数据,当然功耗也会更多
现在的半导体里面的晶体管大小已经接近原子大小了,已经做到了纳米级别,这工艺简直强强强
CPU架构历史
CPU时钟是个什么鬼东西,干啥用的?
首先它和时钟没有关系, 想象一下CPU如何给寄存器赋值
通电赋值,给电容通电放电寄存器变成了0101,寄存器的数量是有限的
如何一下给多个寄存器赋值并控制正确的顺序呢?
由时钟搞定,要是时钟频率越快,那寄存器算的是不是越快
的确,但是电容要放电归0啊,越快功率越高,CPU散热也吃不消啊
还有CPU不一定需要时钟来实现改变电容的值
时钟的单位MHz是什么意思啊?
物质在1秒种完成的周期性变化叫做频率,单位是Hz
1kHz = 1000kHz 一千赫兹
1Mhz = 1000000Hz 一百万赫兹
1Ghz = 1000Mhz = 十亿赫兹
…单位太大了
看下自己当前的CPU,2.2GHz,CPU居然算的这么快,1秒22亿次,还有谁
8086CPU
第一代 x86 CPU
通用计算机系列的标准编号缩写,也标识一套通用的计算机指令集合
定义了芯片的基本使用规则
由Intel
于1978年所设计的16位微处理器芯片,是x86架构的鼻祖。
8086 CPU有20条地址线(2^20寻址最大1M),4个16位的段通用寄存器
16的地址
通过段地址(cs)
左移4位与偏移地址(ip)
之和来表示20个地址
段地址都是16的倍数
1M = 1024kb
1kb = 1204byte
1M = 2<<20 = 1 048 576 = 1024*1024
64kb = 2<<16 = 65 536 = 1024*64
主要分为执行单元(EU)
和总线接口单元(BIU)
两大部分
那么我们常用的32位操作系统和64位操作系统寻址能力有多强呢?
2<<32 = 4 294 967 296 = 41943041024 = 4096 * 1024 * 1024 = 41024^3
32位的寄存器寻址能力只有4GB
2<< 64 = 16 777 216*1024^4
64位寄存器的寻址能力有16777216TB(1TB=1024GB)
总线接口单元(BIU bus interface unit)
CPU在存储器和I/O设备之间的接口部件,主要和主板交换数据使用,
CPU执行指令时,总线接口单元要配合执行单元,
从指定的内存单元或IO端口中取出数据传送给执行单元,
或者把执行单元的处理结果传送到指定的内存单元或IO端口中。
(1)4个段地址寄存器,主要用来存计算数据**
CS(code segment)——16位的代码段寄存器
DS(data segment)——16位的数据段寄存器;
ES(extra segment)——16位的扩展段寄存器;
SS(stack segment)——16位的堆栈段寄存器;
(2)16位的指令指针寄存器IP
(3)20的地址加法器
(4)6字节的指令队列缓冲器
执行单元单元(Execution Unit)
所有指令的解释和执行
8个通用寄存器
4个数据寄存器 AX, BX, CX, DX
2个地址指针寄存器 BP(base pointer),SP(stack pointer)
2个变址指针寄存器 SI(source index),DI(destination index)
标志寄存器 FR(flags register)
算术逻辑单元ALU(arithmetic logic unit)
CS与IP(代码段寄存器和指令寄存器)
假设CPU需要取出一条命令
CS和IP的运行流程
1.CS和IP初始化
2.CS和IP进入地址地址加法器
转换成20个地址
3.地址加法器
将物理地址送上输入输出控制电路
4.输入输出电路
将物理地址送上地址总线
5.地址总线获取命令后送入CPU
6.输入输出电路将指令送入指令缓冲器
7.读取一条指令后,IP+1,把新指令地址传出去执行下一步
jmp 无条件跳转指令
通常用来绕过判断校验
WIN10 使用 DEBUG
安装教程:https://www.cnblogs.com/glorfei/p/5923865.html
DOSBox
模拟一个系统,一个 DOS 模拟程序,由于它采用的是 SDL 库,所以可以很方便的移植到其他的平台
DEBUG
DOS程序,用于检测查看修改内存,追踪执行过程
DOSBox和DEBUG的关系
DOSBox
就相当于一个虚拟系统,DEBUG
就是虚拟系统里面的一个软件
使用DEBUG
修改寄存器的值并不会影响到我们真实使用系统
DEBUG常用命令
debug不区分大小写
寄存器是寄存器,内存是内存
寄存器只能存一个16进制数
命令 | 解释 |
---|---|
R | 观看和修改寄存器的值 |
H | 计算两个十六进制数的和与差 |
D | 显示内存区域的内容 |
E | 改变内存单位的内容 |
F | 使用指定的值填充指定内存区域中的地址 |
M | 将指定内存区域的数据复制到指定的地址去 |
C | 将两块内存的内容进行比较 |
S | 在指定的内存区域中搜索指定的串 |
A | 输入汇编指令 |
G | 执行汇编指令 |
U | 对机器代码反汇编显示 |
N | 设置文件名,为将刚才编写的汇编程序存盘做准备 |
W | 将文件或者特定扇区写入磁盘 |
L | 从磁盘中将文件或扇区内容读入内存 |
T | 执行汇编程序,单步跟踪 |
P | 执行汇编程序,单步跟踪。与T命令不同的是:P命令不会跟踪进入子程序或软中断。 |
I | 从计算机输入端口读取数据并显示 |
O | 向计算机输出端口送出数据 |
Q | Q命令的作用是退出DEBUG,回到DOS状态 |
段存储(DS寄存器)
我们写的代码都是放在内存里面的,放在内存哪里,谁负责去记录呢?
DS寄存器负责记录这个,你要指定某块内存写代码就得改变它的值
在8086里面写代码,是不能直接给寄存器赋值的,要间接通过寄存器修改
DS和CS的区别?
DS记的是你当前的访问地址,而CS记的是代码要从哪里执行
mov ax, [0]是什么意思?
意思是把内存里面偏移地址为0的数据拷贝到ax里面去,
怎么看这个[0]的数据, d DS的值:0
查看这块内存区域的数据
如何看懂内存里面的值?
通过d命令和u命令我们看到不同的结果,一个是16进制的数据,一个对应的命令
也就是说在内存里面的数据同时具有双层含义
它既可以是值
,连起来也可以理解为代码
mov ax, 0
= 076A:0000 B8 00 00
mov ax, 01
= 076A:0000 B8 01 00
mov bx, 01
= 076A:0000 BB 01 00
发现内存里面是这样标识的
字的传送
一个字占2个字节,要用两个地址连续的内存来存放
字的低位字节存在低地址中,高位字节存放在高地址单元中
高字节和底字节
AX = AH<<8 + AL
BX = BH<<8 + BL
CX = CH<<8 + CL
DX = DH<<8 + DL
寄存器从高到低存
内存通常从低到高存储
高位地址对高位数据,低地址对低位数据
内存里面的低地址数据到寄存器就变成高地址了
寄存器里面的高地址到内存里面就变成低地址了
栈
可以理解为一个没有出口的地洞,先进去的后出来,后进去的先出来
栈的设计有什么用?
就是为了函数而设计的
SS与SP/栈Stack和段寄存器
SS(stack segment)存放栈顶的地址,栈的底部位置
SP(stack pointer)存放栈顶的偏移地址,当前位置距离底部差多少
8086压栈是由高地址端(FFFFH)到低地址端(0000H)
栈顶,SS存放的栈最大地址,往里面塞数据,数据的返回地址就应该不断减小
push ax 将寄存器ax中的数据送入栈, 先偏移地址,再给内存赋值
pop ax 从栈顶取出数据送入ax,先取出数据,再偏移地址
重点:栈的出栈里面的值是不会被清零的,入栈会覆盖之前的数据
栈的漏洞
因为CPU没有明确规定控制栈的大小,
栈有可能pop或push或人为修改SP的值导致栈溢出
栈溢出会怎么样呢?数据会从新从栈底部开始入栈,覆盖之前的数据
总结
CPU的寻址能力由寄存器的位数决定,位数越高,寻址越大
CPU分三种寄存器
CS记录你要执行的命令地址
DS记录你要访问的内存地址
SP记录你要访问的栈地址
这三种寄存器可以共同指向同一个地址
将内存里面的数据赋值到寄存器时,
内存里面高位的数据到寄存器的低位,反之
第一个汇编程序
汇编程序下载地址**
http://pan.baidu.com/s/1i5hUFdj
使用win10
注意里面的MASM.EXE
和LINK.EXE
需要在DOSbox
里面打开
不可以使用cmd
打开或者直接执行,包括使用LINK.exe
编译出来的软件也是
强行打开是会提示不兼容的
ASM文件/ Assembly Language Source
asm文件是汇编程序源文件,可以通过debug工具汇编成obj文件,然后用link工具连接成exe 文件
汇编语言是一种功能很强的程序设计语言,是利用计算机所有硬件特性并能直接控制硬件的语言
汇编语言亦称为符号语言
MASM.EXE/Microsoft Macro Assembler
汇编语言的编译软件,仅仅只是翻译成机器指令,生成*.obj
文件
编译器只能发现语法错误而无法发现逻辑错误
LINK.EXE
把机器代码编译成可执行文件
编写步骤
第一步创建*.asm
文件
第二步编写代码
第三步使用MASM.EXE
编译成*.obj
文件
第四步使用LINK.EXE
链接编译成*.exe
文件
第五步使用debug
*.exe
进行调试
快速编译技巧
masm appName
;
link appName
;
代码分析
assume cs:Hello ;假设ip为Hello,cs:ip指向的是程序执行的内存地址
Hello segment ;定义一个段Hello
mov ax,[0] ;
mov bx,1 ;
mov ax,4c00H;
int 21H ;这里的意思是中断dos程序,中断就是告诉cpu,程序到这里CPU不要执行下去了
Hello ends ;段的结束地址
end
mov ax, 4c00H;int 21H代码分析
这两句代码是要连起来理解的
意思是通过给AH寄存器
赋值,然后调用int 21H
指令
int命令
就会根据AH寄存器
中的值执行相应的操作
mov ax, 4c00H
= mov ah, 4c
4c
在DOS功能对照表里面的意思就是带返回码结束
int 21H
调用中断21h的ah里面的命令号
执行到int 21H在debug使用P结束程序
语法分析
1.定义程序执行入口,和对应段名称
assume cs:Hello
2.声明段,在段里面写对应操作,声明段结束
Hello segment
mov ax,[0]
mov bx,1
mov ax,4c00H
int 21H
Hello ends
3.声明编译器编译结束
end
DOS是如何执行*.exe程序的
{% image /img/dos_exe.png '分析' '' %}此时段地址指向075A, 指令地址指向076A,CX寄存器
记录了这个程序的字节数
代码内容存储在076A,中间还空出了10H
书上解释这个10H用来DOS与该程序通信的,被成为PSP
推出加载过程
1.编写的程序代码进入内存
2.段地址记录的是代码真实地址
+10H
3.CS记录的地址就是代码在内存里面的实际位置
4.CS:IP
开始执行代码
5.执行到int 21H
把CPU的控制权交给DOS
为什么在ASM写的[0]在DEBUG调试变成了0?
在ASM文件里面想要拷贝内存你得这么写
mov ax,ds:[0]
或者这样写,设置段的值再设置偏移地址的值
mov bx, 0
mov ds, bx
mov ax,[bx]
在debug你得这么写
mov ax,[0]
为什么我也不知道
分析ds:[0]
= 段地址
+:
+[偏移地址
]
这是一个段地址和偏移地址表示的一块内存
段地址和偏移地址可以写寄存器
mov 寄存器/地址 [寄存器] 与 loop
区分寄存器和地址bx
和[bx]
在我们写的伪指令,汇编代码
8086里面
, [0]
的意思就是内存里面第0个地址的值
我们可以使用 d 0:0
查看这个[0]对应的值是什么,一个[0]
就是一个字
通常情况下,寄存器里面存的可以是一个地址
,也可以是内存里面的一个值
当我们使用寄存器声明它是内存或值
的时候如何区分是值
还是内存
呢?
mov ax, [bx]
[bx]
这样子写就是指内存地址为bx的所对应的值
bx
这样子写就是指bx寄存器的值
loop与cx寄存器
在求一些反复运算的问题的时候,我们需要用到loop标识一段反复运算的指令
ASM代码之计算2的5次方
assume cs:Loop
Loop segment
mov ax, 2;
mov cx, 4;
s: add ax, ax;
loop s;
mov ax, 4c00H;
int 21H
Loop ends
end
仔细发现IP寄存器的值执行完loop
后,又会指向标号的位置
CX
寄存器会减去1
loop循环本身只是一个以cx值为条件,jmp反复指回原来的位置
使用p
命令和g
命令快速执行程序
p 内存地址
命令:一步执行完指定地址的循环、重复
的字符串指令
g 内存地址
命令:行到指定的内存地址对应的函数
loop与[bx]妙用
loop指令本身就是个jmp伪指令
我们可以在s和loop之间写上很多其它操作
例如求0000:0 - 0000:F的字节数值之和存到dx寄存器里面去
assume cs:LoopBx
LoopBx segment
mov ax, ds:[0];
mov bx, 0;
mov dx, 0;
mov cx, 15;
s: add al, ds:[bx];
mov ah,0;
add dx,ax;
inc bx;
loop s;
mov ax, 4c00H;
int 21H
LoopBx ends
end
段地址与偏移地址的加深理解,发现书上就是在考段地址的灵活应用
在mov ax,[0]的时候,[0]是偏移地址,这里隐藏了一个默认地址,DS段地址
我们可以通过寄存器间接赋值来改变段地址
CX寄存器在debug程序的时候一开始会记录程序占了多少个字节
DW,DB,DD一次定义多个段地址
求8个字之和0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
assume cs:HelloDw
HelloDw segment
dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h;定义8个变量
begin: mov bx, 0;start标识程序入口
mov ax, 0;
mov cx, 8;
s:add ax, cs:[bx];
add bx, 2;
loop s;
mov ax, 4c00h;
int 21h;
HelloDw ends
end begin
db(define byte)定义字节类型变量,一个字节数据占1个字节单元,读完一个,偏移量加1
dw(define word)定义字类型变量,一个字数据占2个字节单元,读完一个,偏移量加2
dd(define double word)定义双字类型变量,一个双字数据占4个字节单元,读完一个,偏移量加4
如何定义8个字数据呢,并且在内存里面有自己的地址?
DW 变量1,变量2, 变量3, ...
这时候需要使用DW
来声明,DW
定义的值一个占2个字节
通过上图我们发现
通过d
命令
在076A:0里面变量的确定义的确进去了
在CS记录的段里面记录着我们存储的值
通过u
命令
因为在程序里面的代码至少有三层意思,地址,值,代码
定义的变量被理解为了代码, 而IP肯定会从对应的地方执行
IP按照这样执行下去,肯定不行,它应该从偏移段10开始执行
如何让CS:IP
从偏移地址10开始执行下去呢?
也就是说CS:IP
指向0010我们需要使用标号
我们需要定义两个标号
第一个标号写在代码要执行的地方
标号: 代码
第二个标号写在end的地方
end 标号
DW声明值与栈的妙用
逆向排序8个字数据
assume cs:DwLoop
DwLoop segment
dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h;定义8个变量
dw 0,0,0,0,0,0,0,0;定义存储8个字的栈
begin:mov ax, cs;start标识程序入口,start只是一个普通的标号,任意写
mov ss, ax;
mov sp, 20h;先设置好偏移地址,然后压栈
mov bx, 0;
mov cx, 8;
s1:push cs:[bx];值入栈tt
add bx, 2;
loop s1;0010段记录所有的值
mov bx, 0;
mov cx, 8;
s2:pop cs:[bx];0010的值利用栈的特性倒序输出
add bx, 2;
loop s2;
mov ax, 4c00h;
int 21h;
DwLoop ends
end begin
强化代码意义,定义不同含义的数据段
我们可以在assume假设多个段
注意:这里定义的时候还是要严格区分代码
内存数据
栈
对应的数据请放入指定的寄存器声明
assume cs:DwLoop, ds:Data, ss:Stack
Data segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
Data ends
Stack segment
dw 0,0,0,0,0,0,0,0
Stack ends
DwLoop segment
begin: mov ax, Stack;
mov ss, ax;
mov sp, 20H;
mov ax, Data;
mov ds, ax;间接通过ax给段寄存器赋值
mov bx, 0;
mov cx, 8;
s1:push [bx];
add bx, 2;
loop s1;
mov bx, 0;
mov cx, 8;
s2:pop [bx];
add bx, 2;
loop s2;
mov ax,4c00h
int 21h
DwLoop ends
end begin
那么Stack和Data段的值是什么?
mov ax, Stack
和mov ax,Data
通过u
命令查看Stack和Data的值发现记录的是这个段的首地址
[bx+idata] 及 AND和OR灵活定义偏移地址
利用[bx+数字+idata]
骚写法简化代码
我们需要给一块内存地址赋值,有一块二维内存地址需要这样子赋值
内存地址ax = bx存偏移段前两位+si偏移段存后两位+具体位置10
我们得分三步这样子写,是不是很鸡肋
add ax, bx;
add ax, si;
add ax, 10;
其实还可以这样子写
assume cs:bx_idata
bx_idata segment
add ax, [bx+10H+si];
add ax, [bx].10H+[si];
add ax, [bx+si]+10H;
add ax, [bx.10H].[si]
add ax, 10H+[bx.si];
add ax, 4c00H;
int 21;
bx_idata ends
end
发现这样子写居然也只占3个字节,而且有对应且一样的机器码
怎么花里胡哨的写都会表示一样的机器码
而且编译器喜欢把数字放在最后面
si(源变址寄存器)
和di(目的寄存器)
两个16位且不能拆分
的寄存器
ASM文件如何定义不同的进制
assume cs:and_or
and_or segment
mov al, 11110000B; //mov al, F0
and al, 00001111B; //and al, 0F
mov bl, al; //F0 & 0F = 0 = bl
mov al, 11110000B; //mov al, F0
or al, 00001111B; //or al, 0F
mov dl, al; // F0 | 0F = FF = dl
mov ax, 1111000011110000B; //mov ax, F0F0
and ax, 0000111100001111B; //and ax, 0F0F
mov bx, ax; //F0F0 & 0F0F = 0 = bx
mov ax, 170360O; //mov ax, F0F0
or ax, 7417O; //or ax, 0F0F
mov dx, ax; // F0F0 | 0F0F = FFFF = DX
mov ax, 61680D; //mov ax, F0F0
or ax, 3855D; //or ax, 0F0F
mov dx, ax; // F0F0 | 0F0F = FFFF = DX
mov ax,4c00H;
int 21H;
and_or ends
end
发现在ASM文件里面,可以定义四种不同进制的值
值+H = 16进制数
值+D = 10进制数
值+O = 8进制数
值+B = 2进制数
如果在ASM文件里面不声明是什么类型的数据,编译器会理解成二进制的数据
貌似在debug模式这样写代码会报错
ASCLL表大小写字母的神奇之处
a - A = 61H - 41H = 20H
a - A = 97D - 65D = 32D = 2<<5
所有的字母都相差了20H, 32D
利用and和or进行大小写字母转换
1.一个字母1个字节8个位,使用db
进行定义
2.同一字母,大写字母二进制的第6位为0,而小写字母的第6位为1,其余位相同
3.任何字母二进制第6位与0
就是大写字母,第6位或1
就是小写字母
4.a&11011111 = A
A | 00100000 = a
将两个Hello Asm分别转换成大写和小写
assume cs:A_to_a, ds:Data
Data segment
db 'Hello Asm'
db 'Hello Asm'
Data ends
A_to_a segment
change:mov ax, Data;
mov ds, ax;
mov bx, 0;
mov cx, 9;//字母一共9个字节
s1:mov al, [bx];
and al, 11011111B;//字母转大写
mov [bx], al;
mov al, [9+bx];//发现第二个字符就在第一个字符的后面
or al, 00100000B;//字母转小写
mov [9+bx], al;
inc bx;
loop s1;
mov ax, 4c00h;
int 21h;
A_to_a ends
end change
有规则的字符串的转换与双LOOP妙用
1.执行完loop指令后, CX 寄存器会减 1
2.loop命令就相当于jmp指令
批量将小写字符串转为大写字符串
assume cs:to_cap, ds:str, ss:stack
str segment
db 'life000000000000';//4个16个字节的字符串,最长的为6位
db 'money00000000000';
db 'blood00000000000';
db 'dealth0000000000';
str ends
stack segment
dw 0, 0FFFFH;//因为CX不够用,定义一个栈来存CX的值.后面0FFFFH用来作标记的
stack ends
to_cap segment
begin:mov ax, stack;
mov ss, ax;//标识栈的位置
mov sp, 2;//入栈提前准备好空间
mov ax, str;
mov ds, ax;//准备好字符串的地址
mov bx, 0;
mov cx, 4;//4个字符串循环4次
s1:push cx;//外层的CX入栈,因为里面的loop需要用到CX,保证不会影响外层CX
mov si, 0;
mov cx, 6;//循环6次,因为最长字母也就6
s2:mov al, [bx+si];
and al, 11011111B;//转大写
mov [bx+si], al;//小写变大写
inc si;//一次转一个字母
loop s2;
add bx, 16;//转到下一个字符串的地址
pop cx;//栈里面存的值返回给cx
loop s1;//这里执行loop后, cx=cx-1;
mov ax, 4c00h;
int 21h;
to_cap ends
end begin
无论是定义数据段还是栈,你定义17个字节会用32个字节空间,
用2个字节会用掉16个字节,这里定义数据会自动往16补全,
数据不足16个字节按16个字节给你补全,指令会在下一段开始
补全的字节不写不填默认都是0
数据寄存器 和 字和字节的声明
reg普通寄存器有16个,实际上就8个
ax累加寄存器
,bx基址寄存器
,cx循环寄存器
,dx数据寄存器
8位寄存器
ah,al,bh,bl,ch,cl,dh,dl
sp栈指针
,bp栈辅助指针
,si源变寄存器
,di目的寄存器
sreg段寄存器有4个
ds内存段
,ss栈段地址
,cs代码段
,es辅助段地址偏移
实际上再编写代码过程中我们也发现很多的寄存器都是有特殊的用处的
我们并不能随便修改寄存器,指令与寄存器有着很多隐藏关系
一不小心修改寄存器的值,就有可能导致程序运算出错
哪些寄存器可以当作偏移地址呢?写成[寄存器]
注意:bp寄存器的值是由ss的段地址加上bp偏移地址对应地址表示的,也就是说
mov ax, [bp]的时候, [bp]的值在哪里呢? 是在栈里面的,因为[bp]的段地址用的
是SS栈基地址寄存器的值,此时段地址用的不是DS,非常重要。
debug查看[bp]的值, d:SS寄存器:BP寄存器的值
(ax) = ((ss)*16 + (bp))
mov ax, [bx]
mov ax, [si]
mov ax, [di]
mov ax, [bp]
mov ax, [bx+si]
mov ax, [bx+di]
mov ax, [bp+di]
mov ax, [bp+si]
mov ax, [bx+si+idata]
mov ax, [bx+di+idata]
mov ax, [bp+si+idata]
mov ax, [bp+di+idata]
1.bx
和bp
互斥不能在一起,si
和di
互斥不能在一起
2.它们4个可以单独写
, 组合最多写两个
EA(Effective Address)与 SA(Segment Address)
我们要在内存里面寻址一个地址,需要一个段地址SA
和偏移地址EA
mov ax, [bx]; //这里隐藏了段地址DS
(ax) = ((ds*16)+(bx))
mov ax, [bp];//这里隐藏了段地址SS
(ax) = ((ss*16)+(bp))
ASM区分是字到内存
还是字节到内存
mov ax, [0];//这里拷贝的是一个字, 0010 到001F可以存8个字,这里的0指 [0]+[1],1指[1]+[2],F指的是[F]+[F+1]
mov ah, [0]//这里拷贝的是, 0010到001F可以存16个字节,这里的0指的是[0],1指的是[1],F指[F]
push 和 pop 默认就是一个字单元
这都取决于前面的寄存器是个8位的还是16位的,或者声明
在ASM文件里面的声明格式
当我们需要把一个数字直接拷贝到内存里面去的时候,会遇到这样一个问题?
mov ds:[0], 96;
这个123究竟内存里面该怎么存呢,用一个字存还是一个字节存呢?
在ASM文件里面上面这个写法是错误的,必须严格声明是字还是字节
assume cs:codesg
codesg segment
start: mov byte ptr ds:[0000H], 96;//声明一个字节指向ds
mov word ptr ds:[0010H], 96;//声明一个字指向ds
mov ax, 4c00H;
int 21;
codesg ends
end start
ptr
– pointer 缩写,是用来临时指定类型的。
通常用到ptr命令都是下面这样的
指令码
类型
ptr 数据或地址
ptr
前面声明类型, 后面指向对应的数据或者地址
我们可以发现使用`word`声明的数字自动变成了2个字节数
word的机器码比byte的机器码多了一个字节
DIV除法指令求商和余
(被除数)25 ÷ (除数)3 = (商)8 余 1
要计算25÷3的商和余怎么写呢?
25被除数放在哪个寄存器,而除数3又该放在哪个寄存器呢?
div
指令是这样子规定的
div reg或内单元(reg和内存单元这里指的是除数)
在进行除法运算的时候,AX寄存器
的值会被当作被除数
mov ax, 19H;//19H就是25
mov bx, 3;//准备除数3
div bx;//开始计算,bx是16位的
计算结果放在ax寄存器
里面,商AX =0008
余DX = 0001
当你这样子写的时候,会发现寄存器里面的商和余又换了一个位置
mov ax, 19H;//19H就是25
mov bl, 3;//准备除数3
div bl;//开始计算,bl是8位的
AX = 0108 => 商AL=0008
,余AH=0001
DIV除法指令是这样规定的
1.除数可以用8位的寄存器或者16位的寄存器存
2.被除数可以用AX存储,或者AX+DX联合存储
被除数是怎么确定用AX还是用AX+DX的值呢
1.除数用的是8位寄存器的时候被除数用AX
,除数用的是16位寄存器的时候,被除数用的是DX+AX
2.当被除数大于65535的时候,被除数用的是AX+DX,AX存低16位,DX存高16位, 反之直接用AX
大于65535的被除数请这么存 65536D = 10000H => (DX=0001 , AX = 0000)
商和余怎么看?
当被除数用的是DX+AX的时候,DX存余数,AX存商
当被除数用的是AX的时候,AH存余,AL存商
使用div发现一个问题,当你使用AX求余和商的时候?
例如FFFF FFFF ÷ 1 = FFFF FFFF
发现又好像不是这样看的,余数为0,寄存器AX和DX里面全都是FFFF FFFF
例如FFFF FFFF ÷ 2 = 7FFF FFFF 余 1,寄存器里面越全都是FFFF FFFF
这肯定是错的,解决方案为自己写个函数处理
写一个完整的除法程序
计算65536 ÷3,要求数据在内存段里面存储,第1位放除数,第2位放被除数,第3位放余,第4位放商
65535 = 10000H =>我们需要用AX和DX表示10000H
为了方便,除数和商和余每个用一个字保存
assume cs:division, ds:data
data segment
dw 1, 0;//被除数 01 00 00 00
dw 3; //除数 03 00
dw 0; //商 00 00
dw 0; //余 00 00
data ends
division segment
start:mov ax, data;
mov ds, ax;
mov dx, ds:[0];//65535的高位放在DX里面, mov dx, 0001
mov ax, ds:[2];//准备被除数,65535低位放在AX里面 mov ax, 0000;
mov bx, ds:[4];//准备除数 mov bx, 0003
div bx;
mov ds:[6], ax;//得到商
mov ds:[8], dx;//得到余
mov ax, 4c00h;
int 21h;
division ends
end start
dup(duplicate)快速定义一块内存的值
假设我们需要定义32个字节
用db
至少写17个字符0,dw
写8个0,dd
写5个0
这样子实在是太麻烦了,我们可以使用dup来完成一些有规律数据的批量定义
db 重复的次数 dup(重复的字节数据)
db 17 dup('0')//定义了17个字符0
db 3 dup('123', '456')//定义一个字符串'123456123456123456'
dw 重复的次数 dup(重复的字数据)
dw 8 dup(0)//定义8个字为0
dd 重复的次数 dup(重复的双字数据)
dd 8 dup(0)//定义8个双字为0
offset, jmp, jmpz, loop指令
在一个汇编程序里面有可能会有多个标号
表示声明不同的代码段,
这个不同的代码段的偏移地址怎么找呢,使用offset(编译器符号)
编译器会自动给你找到标号的位置,计算对应的偏移值替换成地址
offset ‘标号’ 表示的是标号对应代码段的首地址
assume cs:codesg
codesg segment
open : mov ax, offset open;//open首地址是0, mov ax, 0
close: mov bx, offset close;//close首地址是3 mov bx, 3
codesg ends
end open
offset可以便捷的得到标号对应段偏移地址(相对程序开始的偏移),省的我们自己去算
对反复调用一些命令,可以简化很多代码,增强代码的阅读性
JMP(Jump Unconditionally)无条件跳转
jmp '段地址CS':'偏移地址IP'
jmp '寄存器=>IP的值'
在ASM文件里面有这么7种写法
assume cs:codesg
codesg segment
start:jmp ax;//修改IP的值为AX
jmp [bx][si];//修改IP的值为[bx+si]
jmp short s;//修改IP的值为S,如果S不符合(-128<S<127),编译会报错
jmp near ptr s;//修改IP的值为S, 如果S不符合(-32768<32767) ,编译会报错
jmp far ptr s;//前面低地址两个字节给IP,后面两个高地址字节给CS
jmp word ptr [bx];//修改IP的值为[bx]
jmp dword ptr [bx+si];//CS = [BX+SI+2],IP = [BX+SI]
jcxz s;
s:mov ax, 4C00H;
int 21H;
codesg ends
end start
90的机器码为 NOP(No Operation)意思是这里什么都不干,IP+1就是了
JMP机器码的详细解析
第一个JMP 0012 对应的机器码为 EB0C
第二个JMP 0012 对应的机器码是 EB0A
第三个JMP 0012 对应的机器码是 EB07
Jmp 标号
中jmp
的机器码是EB,
后面的 0C
,0A
,07
和0012
有什么关系呢?
0012很容易理解那就是需要跳转的代码位置
但是为什么不同的机器码却可以显示成一样的指令呢?
猜想后面的0C,0A应该是一个偏移地址,并不是目的地址
0004+0C = 0010H
0006+0A = 0010H
0009+07 = 0010H
0010-0012 = -2,居然还差了两个字节
为什么差两个字节,想想IP是不是要执行完JMP
指令才能理解这句指令
指令刚好两个字节
为什么JMP 标号的内存地址可以是负数呢?
想想当我们一个程序要跳到上面写的程序怎么办呢,如果标号的只能是正数
那么程序永远都不能调用在段代码之前的指令,函数复用可能就无法实现了。
注意:当我们看到机器码第一位为
8,9,A,B,C,D,E,F
的时候,不用想了,指令会跑到当前指令的上面执行
因为这时候肯定是个负数
仅仅修改IP的命令有,占有的机器码字节数为
jmp ax;//2个字节
jmp [bx+si];//2个字节
jmp short s;//2个字节
jmp near ptr s;//3个字节
jmp word ptr [bx];//2个字节
同时修改CS和IP的命令有
jmp far ptr s;//5个字节
jmp dword ptr [bx+si]//2个字节
发现jmp指令占一个字节,寄存器占一个字节
JCXZ(Jump when CX is zero )
jcxz为有条件转移指令,条件是CX=0,则执行jmp指令
如果CX != 0,CPU读了这条命令这条命令的意思就是啥都不干
jcxz 标号 = jcxz short 标号
jcxz机器码占两个字节,通常命令一个字节,数据一个字节
所以jcxz
的标号偏移地址只能是(-128<标号<127)
牢记:CX寄存器
的值是会影响JCXZ
指令执行的
LOOP循环指令
深入了解jmp
和jcxz
后,不难发现所谓的loop
就是个有条件的jmp
指令
按如下规则进行执行
第一步: ((cx) = (cx) - 1
第二步:if((cx) != 0) jmp short s;
我们在编译的时候发现loop
机器码也是两个字节
loop命令占一个字节,数据占一个字节
所以loop
的标号偏移地址只能是(-128<标号<127)
JMP指令偏移妙用
assume cs:codesg
codesg segment
mov ax,4c00H
int 21h
start:mov ax,0
s: nop
nop
mov di, offset s
mov si, offset s2
mov ax,cs:[si];//这句话
mov cs:[di],ax;//把S2替换S
s0: jmp short s
s1: mov ax,0
int 21h
mov ax,0
s2: jmp short s1
nop
codesg ends
end start
这个程序把jmp指令
拷贝另一个程序段
,
仅仅从字面上我们可能误解程序到S2的时候根本无法结束
牢记JMP指令
它记录的不是目的地址
而是偏移地址
这个程序JMP指令
都在标号的后面,可想执行JMP
时IP
会向前偏移
执行S2指令时,程序变成了,JMP 0000
程序正常结束了
从机器命令分析
EBF0
是 JMP 0008
我们看到F0
就知道它是往上偏移的,偏移多少呢?
此时执行完JMP指令IP应该是0018
0018 - 0008 = 0010 向前偏移10H, 10H是包含JMP命令的
计算下将S2拷贝到S后,执行S段,S2怎么执行呢?
程序执行完S段里面的S2对应的JMP指令
此时IP是0010,向前偏移10H,得出JMP 0000
往DOS面板写入图像
在DOS面板里面B8000H
到BFFFFH
一共32KB的空间
可以显示80
行,每行显示25
个的彩色可闪烁的字符
因为DOS屏幕大小有限,还要分成8页
我们只能看到显示一页,那我们用第一页写点东西
第一页B8000H
到B8F9FH
一共4000字节
怎么显示一个彩色的字呢?
DOS规定,一个彩字两个字节
第一个字节,低地址字节,放字符
第二个字节,高地址字节,放色彩属性
色彩属性这样定义的
1个字节8个位,256种配置,色彩属性有
是否闪烁,背景色,是否高亮,,字体颜色
发现一个颜色3个位,也就是说最多8个颜色
看看8种背景色长啥样子?
00000000 => 00H
00010000 => 10H
00100000 => 20H
00110000 => 30H
01000000 => 40H
01010000 => 50H
01100000 => 60H
01110000 => 70H
assume cs:code,ds:data,ss:stack
data segment
db ' ';
data ends
stack segment
dw 0
stack ends
code segment
start:mov ax,data
mov ds,ax;//设置内存位置
mov ax,0b800h
mov es,ax;//设置要设置的数据的位置
mov ax, stack;
mov ss, ax;//设置栈的位置
mov cx, 16;//打印16次
mov bx, 00A0H;//我们从第2行开始
mov si,160;
mov dh, 0;
s1:push cx;
mov cx, 80;//一行有80字符和色彩配置
s2:mov ax,ds:[0]
mov es:[bx][si], ax;//拷贝空格进去
mov al, dh;//设置颜色
mov es:[bx+1][si],al;//拷贝色彩配置到内存里面
inc bx;//准备下一个字符的位置
inc si;//准备下一个色彩配置
loop s2;
pop cx;
add dh, 16;//准备下一个背景色
loop s1;
mov ax,4c00h
int 21h
code ends
end start
ret, retf, call, mul指令
我们在一个页面写很多函数,也可以说是在一个段里面编写函数,通常一个函数都要return回去
一个大函数里面有很多小函数,小函数调用里面的小函数我们可能就需要用到ret
RET(return)
它会修改IP的值,也就是说会影响一个段内程序的执行顺序
那么它怎么指向IP呢?IP的值存哪里呢?
IP值是存在栈里面的,当执行ret(机器码C3)时, 要执行
(1) (IP) = [SS:SP]
(2) (SP) = (SP)+2
等同于pop IP
多个大函数并级存在,大函数调用大函数,可能这两个代码量非常的大
为了区分功能可能不在一个段里面,这时候我们可能需要使用retf
RETF(return far)
它会修改CS和IP的值,也就是说可以影响别的段里面函数的执行顺序
CS和IP的值也是存在栈里面的
(1) (IP) = [SS:SP]
(2) (SP) = (SP)+2
(3) (CS) = [SS:SP];
(4) (SP) = (SP)+2;
等同于pop IP, pop CS
这里顺序千万不要搞反了,IP先出栈,所以IP入栈要在CS入栈之后
CALL (下一句地址入栈,并且执行相应指令)
assume cs:code
code segment
start:mov ax, 0;
mov ds, ax;
call s;//(1)push IP (2)jmp near ptr s
call far ptr s;//(1)push CS (2)push IP (3) jmp far ptr s
call ax;//(1)push IP (2)jmp ax
call word ptr ds:[0];//(1) push IP (2)jmp word ptr ds:[0]
call dword ptr ds:[0];//(1) push CS (2)push IP (3) jmp dword ptr ds:[0]
s:mov ax,4c00h
int 21h
code ends
end start
使用call命令时候请注意,CPU只有执行完这句代码,才能理解你要干什么!
执行call,请注意入栈的IP和CS的值是指向下一句代码的
使用RET和CALL计算2的2次方
assume cs:code
code segment
start:mov ax,1;
mov cx, 2;
call s;//执行完这句代码,下面这条语句的地址会入栈
mov bx, ax;
mov ax, 4c00H
int 21H
s:add ax, ax;
loop s;
ret;//这里会返回到mov bx, ax处
code ends
end start
这好像一个函数啊,call s
就是说执行函数s
, ret
就是返回
,Amazing!
那我们要写一个子函数调用子函数怎么写呢?
fun A(){
fun B();
return;
}
fun B(){
fun C();
}
fun C(){
return
}
//汇编代码,理解起来还是蛮通畅的
A:add ax, ax;
call B;
mov ax, 4c00H;
int 21h;
B:add ax, ax;
call C;
ret;
C:add ax, ax;
ret;
MUL(multiplication)乘法指令求积
计算 3(因数)*8(因数) = 24(积);
要计算38的积怎么写呢?*
因数3放在哪个寄存器,而因数8又该放在哪个寄存器呢?
mul
指令是这样子规定的
mul reg或内单元(reg和内存单元这里指的是因数)
在进行乘法运算的时候,AX或AL寄存器
的值会被当作隐藏的因数
mov ax, 3;
mov bx, 8;
mul ax; => 0018H => 24
之前学过除法这东西肯定也会根据给出的内存地址判断使用AX
或者AL
要知道FFFF * FFFF = FFFE0001一个寄存器肯定是存不下的积的
mul规定
当mul
后面用的是一个字节单位的时候,使用因数AL
,积放在AX
里面
当mul
后面用的是一个字单元时候,积的前4位放在DX里面,后4位放在AX里面
发现无论使用除法还是乘法还是16位字的好用啊!
使用div
或mul
执行完代码后并不会改变因数或除数的值!
计算2的1,2,3,4,5,6,7,8次方,2的8次方2个字节够存了
assume cs:code
data segment
dw 1,2,3,4,5,6,7,8;//对应的次方
dw 8 dup(0);
dw 8 dup(0);//存一个外层的CX值
data ends
code segment
init:mov ax, data;
mov ds, ax;
mov si, 0;
mov di, 16;
mov cx, 8;
mov ds:[32], cx;
excu:mov bx, [si];//把次方交给bx
mov ds:[32], cx;//把外层循环的值取出来
call cube;
mov [di], ax;
add si, 2;//次方地址偏移
add di, 2;//保留结构地址偏移
mov cx, ds:[32];//保留外层循环的值
loop excu;//循环8次计算2的1-8次方
mov ax, 4c00H;
int 21h;
cube:sub bx, 1;//将CX的值减1,因为计算2的N次方只循环N-1次
mov cx, bx;//循环数
mov ax, 2;//基数 2
ck:add ax, ax;
loop ck;//计算2的N次方
ret
code ends
end init
外层的变量提前用栈存起来,避免冲突
assume cs:code
data segment
db 'jcxz',0
db 'loop',0
db 'call',0
db 'retf',0
data ends;//定义4个小写字符
code segment
init:mov ax, data;
mov ds, ax;
mov bx, 0;//准备段地址和偏移地址
mov cx, 4;//4个字符循环4次,4,3,2,1
s:mov si, bx;//准备要转大写字母的首地址
call and_cap;//执行转大写函数
add bx,5;//一个字符串占5个字节,偏移一个字符串
loop s;
mov ax, 4c00h;
int 21h
and_cap:push cx;
push si;//cx和si的值需要在子函数使用,可能会变动,提前存起来
to_cap:mov cl, [si];//把对应的字母拷贝到cl里面去
mov ch, 0;
jcxz ok;//如果一个字符串转大写完了
and byte ptr [si], 11011111B;//将对应地址的字母转换成大写
inc si;//准备下一个字母
jmp to_cap;
ok:pop si;
pop cx;//把外层需要的值赋予正确的值
ret;
code ends
end init
解决FFFF FFFF ÷ 2 = FFFF FFFF的问题,余存在CX里面
assume cs:code
code segment
fun:mov ax,0FFFFH;
mov dx,0FFFFH;//把FFFF FFFF 放到DX和AX里面
mov cx,2;//把除数放到CX里面
call divdw;
mov ax,4c00h
int 21h
;名称:divdw
;核心想法:一个32位的数,我们拆分成两个16位算,商和余加起来就是正确的商和余了
;功能:进行不会产生溢出的除法运算,被除数为dword型,除数为word型,结果为dword型。
;参数:(ax)=dword型数据的低16位;
; (dx)=dword型数据的高16位;
; (cx)=除数。
;返回:(dx)=结果的高16位;
; (ax)=结果的低16位;
; (cx)=余数。
divdw:
push si
push bx
push ax;//因为下面这个函数会改变si,bx,ax的值,提前存起来
mov ax,dx;//先准备计算高位的值
mov dx,0;//高位都到AX里面了,DX要清零
div cx;//被除数的高位/cx,高位在ax,余数在dx
mov si,ax;//把商存到si里面去
pop ax;//把低位的AX的值取出来,计算低位的商和余
div cx;//(被除数高位的商+低位)/cx,高位在ax,余数在dx
mov cx,dx;//余数入存到DX里面
mov dx,si;//高位的商入dx
pop bx
pop si
ret
code ends
end fun
计算这个函数你可能会想,高位的FFFF ÷ 2的余数不用保存吗?当然要保存
仔细想想,高位的的余数是放在哪里了?计算高位后,DX是不是存了余,我们并没有作任何清零操作
高位的余加上低位的值,经过 div
一计算得出正确的余数,除法就是这么巧妙
将一个16进制的数转换成十进制的字符串
例如求256H,我们要把每位数字都求出来
如何得到每位的值呢,使用除法求余?
256 ÷ 10 = 25 余 6
25 ÷ 10 = 2 余 5
2 ÷ 10 = 0 余 2
这个余数就是每位的值了
注意:只有商为零的时候才标识这个数被除干净了
如何把二进制转换成16进制的ASCII码?
我们可以通过ASCII表发现
十六进制的ASCII码 = 30H + 二进制数(余数) 这样就可以表示2进制对应的字符了
编写一个将16进制转成字符串的函数(h_to_dstr)
assume cs:code,ds:data
data segment
dw 256
db 16 dup (0)
data ends
code segment
start:
mov ax, data;
mov ds, ax;//准备好内存地址
mov ax, ds:[0];//准备好被除数
mov si, 0;//清空计数器
mov bx, 10;//准备除数10
call h_to_bstr;
GG:mov ax, 4c00H;
int 21H;
h_to_bstr:
mov dx, 0;//余数清零
div bx;
add dx, 30h;//dx余数加上30H变成十六进制的字符码
push dx;//入栈是因为余数要反过来显示
inc si;//计数加1
mov cx,ax;//把商存到CX里面,利用JCXZ判断
jcxz setindex;
jmp h_to_bstr;//开始求下一位的ASCII码
setindex:
mov di,0;
mov cx, si;//准备出栈循环的次数
to_ram:
pop dx;//字符出栈
mov ds:[16+di], dx;//将ASCII存到对应的数据区
inc di;
loop to_ram;//准备下一个字符出栈
mov byte ptr ds:[16+di], 0H;//设置结尾符0
jmp GG;
code ends
end start
注意:我在编写除法时忘记给余数DX清零,导致程序死机。发现时执行DIV指令的时候,程序的SP,CS,IP寄存器的值都发生了变化,这时候算的是0036 0019H ÷ 10H = 5 6668H这一定会发生溢出,我的这个版本不会报错,也不知道CPU是怎么实现的除法,除法溢出居然会改变SP,CS,IP的值,百度无果。
如何正确的编写一个函数
在一个CALL命令跳转到某个函数后,函数需要改变寄存器的值,但是又不能破坏外层的值
只有先把值入栈了
通常在执行函数开头的时候都要把我们把会改变寄存器值
1.通常只会用到这几个寄存器,函数执行前,提前保存数据
push_stack:
push ax;
push bx;
push cx;
push dx;
push si;
push di;
2.函数执行完成后,在ret之前还原数据,注意出栈的顺序,先进的后出,顺序倒过来
pop_stack:
pop di;
pop si;
pop dx;
pop cx;
pop bx;
pop ax;
9个标志位-Flag寄存器
在debug模式里面,玩到现在也发现了,使用R命令的时候右下角有8个字符
我们跑程序的时候,有些字符执行命令左下角的字符会发生变化
右下角的8个字符被统称为标志寄存器,还有一个TF隐藏了起来
NV, UP, EI, PL, NZ, NA, PO, NC(这是8个寄存器的状态)
8个标志是放在一个flag 16位寄存器上面的,用特定的为位表示信息
一个标志寄存器有两种状态,而且只占用一个位,它们只能表示0和1
OF(Overflow flag)溢出标志(11)
状态1: OV (Overflow)溢出
状态0: NV (Not Over flow)未溢出
意义: 反映有符号数加减运算是否溢出。如果运算结果超过了8位或者16位有符号数的表示范围,则OF置1,否则置0。
如何定义有符号数呢?
在8086汇编语言中,无符号数和有符号数的定义完全相同,从定义时无法区分,只能通过运算来区分(加减运算不能区分,乘除运算可区分)。比如mul指令表示无符号数相乘,imul表示有符号数相乘。数是否带符号是有你的操作决定的。
已知有符号数做加减法的操作会改变OF的值
8位的有符号数范围就是:(-128 倒 127)
16位的有符号数范围就是:(-32768倒 32767)
那么16位计算32767+32767或者-32768+ (-32768)就会溢出了
mov ax, 7FFF
mov bx, 7FFF
add ax, bx;
通过观察执行完成ADD指令后,NV变成了OV, 还有PL变成了NG, NA变成了AC
DF(Direction flag)方向标志(10)
状态1: DN(Down)减少
状态0: UP增加
在串处理指令中,控制每次操作后si,di的增减
DF=0,每次操作后,si、di添加
DF=1,每次操作后,si、di减小
std//执行std后UP变成了DOWN
cld//执行cld后DOWN变成了UP
这个DF标志主要用于拷贝字符串,控制SI,DI的增长方向
###
IF(Interrupt flag) 中断标志(9)
状态1: EI(Enable Interrupt)允许中断
状态0:DI(Disable Interrupt) 禁止中断
决定CPU是否响应外部可屏蔽中断请求。IF为1时,CPU允许响应外部的可屏蔽中断请求。
mov ax, 4C00;
int 21;
这时候EI变成了DI,禁止中断
在8086里面设置IF的快捷方式为
sti, 设置 IF = 1;(EI)
cli, 设置 IF = 0;(DI)
TF(Trace Flag)跟踪标志(8)
TF被隐藏了,当TF被设置为1时,CPU进入单步模式,所谓单步模式就是CPU在每执行一步指令后都产生一个单步中断。主要用于程序的调试。8086中没有专门用来置位和清零TF的命令,需要用其他办法。
SF(Sign Flag)符号标志(7)
状态1: NG(Negative)负
状态0: PL(Positive) 非负
对于刚才 32767 + 32767发现PL变成了NG
非负变成了负,靠完全不靠谱啊,其实在C语言里面也是这样的
当然数值超越范围是编程人员的锅,没有估计好,这也是我经常程序死机怎么都找不到问题的原因之一
正常来说,我们做有符号数运算
运算结果就需要这个来标识下啦
mov ax, -10;//mov ax, FFF0
mov bx, 1;//mov bx, 0001
mov ax, bx;//FFF1 发现PL变成了NG,这是一个负数,FFF1补码转成原码数字就是
0001 => 1000 + 1 推出算的结果就是-9,正确
ZF(Zero Flag)零标志(6)
状态1: ZR(Zero)等于零
状态0: NZ(Not Zero) 非零
用于判断运算结果是否为0,通常伴随着计算指令
(例如:add, sub, inc, dec,这种会改变寄存器值的指令)改变。
它可以用来保存一个逻辑的真假
mov ax, -1;
mov bx, 1;
add ax, bx;//执行完这句代码后面 NZ 变成了 ZR
inc ax;//执行完这句代码后面 ZR 变成了 NZ
dec ax;执行完这句代码后面 NZ 变成了 ZR
AF(Auxiliary Carry Flag)辅助进位标志(4)
状态1: AC(Ayxiliary Carry)进位
状态0: NA(No Ayxiliary Carry) 没有进位
算数操作结果的低四位, 如果产生了进位或者借位则将其置为1
否则置为0,常在BCD(binary-codedecimal)算术运算中被使用
低4位运算, 也就是F + 1就会进位了
mov al, F;
mov bl, 1;
add al, bl;//这时候发现NA变成了AC,因为 1111 + 0001 发生了进位
PF(Parity Flag)奇偶标志(2)
状态1: PE(Parity Even)偶
状态0: PO(Parity Odd) 奇
用于反映运算结果
中“1”的个数。“1”的个数为偶数(包括0)
则PF置1,否则置0。
用于奇偶校验
,保证传输代码的正确性,检测内存位是传输后是否发生变化
mov ax, 7FFF;//我这里系统默认一开始是PO
mov bx, 7FFF;
sub ax, bx;//7FFF - 7FFF = 0, PO变成了PE
CF(Carry Flag)进位标志(0)
状态1: CY(Carry) 进位
状态0: NC(Not Carry) 无进位
用于反映运算是否产生进位或借位。如果运算结果的最高位
产生一个进位或借位,则CF置1,否则置0。
运算结果的最高位包括字操作的第15位和字节操作的第7位。
mov al, FF;
mov bl, 1;
add al, bl;//NC变成了CY
add al, 1;//CY变成了NC
add ax, 7FFF;
add bx, 1;
add ax, bx;//NC变成了CY
adc利用CF的值计算大数相加
adc => add CF
当我们要计算两个个 32 位数相加的时候, 这时候一个16位的寄存器存一个加数就显得不够用了
所以我们要写个函数解决这个问题
当我们计算两个16位数加16位数的时候,
发现这个数怎么都不会超过17位的最大值,就好比 9 + 9 一定会小于 99
而CF刚好提供了进位的值, 我们可以从低位加起,高位加上进位
计算FFFF + 1 = 10000
mov ax, 0;//设置AX为高位
mov bx, FFFF;//给低位BX赋值
add bx, 1;//低位加1,进位后bx全为0
adc ax, 0;//ax 得到进位的值
计算FFFF FFFF FFFF + 1 = 1 0000 0000 0000
mov ax, 0000;
mov bx, FFFF;
mov cx, FFFF;
mov dx, FFFF;
add dx, 1;//这里DX会变成 0000 发生了进位 00ZF = 1
adc cx, dx;// FFFF + 0000 + 1, CX变成了0000 又发生了进位 ZF = 1
adc bx, cx;// FFFF + 0000 + 1, BX变成了0000 又发生了进位 ZF = 1
adc ax, bx;// 0000 + 0000 + 1 结果 1 0000 0000 0000
编写一个计算内存段相加的函数, 计算076a:0000 + 076a:0010 的函数, 结果保留在0020 和 0030里面
16F + 1 = (1+16个零)
assume cs:code,ds:data
data segment
db 16 dup(255)
db 15 dup(0), 1
db 16 dup(0)
db 16 dup(0)
data ends
code segment
start:
mov ax, data;
mov ds, ax;//准备段地址
addInit:
push ax;
push cx;
push si;
push di;
sub ax, ax;//把CF 设置为0,顺便清零
mov cx, 16;//一个段16个字节
mov si, 000FH;
mov di, 001FH;
addSeg:
mov al, [si];
adc al, [di];//计算每个字节之和
mov [di+0020H], al;//把和存到指定的内存里面
dec si;
dec di;
loop addSeg;
mov ax, 0;
adc [di+0011H], ax;//考虑最大值相加,把进位显示出来
pop di;
pop si;
pop cx;
pop ax;
call GG;
GG:
mov ax, 4c00H;
int 21H;
code ends
end start
sbb(substract with borrow)利用CF计算大数相减
CF还可以计算错位值,sbb不是加错位值,而是减去错位值
sbb ax, bx = ax -bx-1
怎么产生错位呢? 例如 1 - 2就会发生错位, NC会变成CY
错位怎么用呢?通常低位的减法不够,就需要高位来补上这一位
例如: 1 0000 - FFFF = 1
mov ax, 1;//准备高位1
mov bx, 0000;//准备低位
sub bx, FFFF;//低位相减, bx变成了1, 且NC变成了CY
sbb ax, 0;// ax变成了0, CY变成了NC
cmp(compare)利用减法实现比较
cmp 是比较指令, cmp的功能相当于减法指令,但是并不影响运算的寄存器,
只会影响标志寄存器的一些值?
cmp 对象1, 对象2;
cmp本质相当于减法指令,减法会影响哪些标志寄存器呢?
当减法溢出的时候, OF肯定会被影响
1.当减法为0或不为0的时候, ZF肯定也会被影响
2.当减法出现负数的时候, SF肯定也会被影响
3.当减法发生错位或者进位的时候,AF和CF也会被影响
4.当加法的结果为奇数或者偶数的是, PF也会被影响
一个减法居然会改变6个标志寄存器的值
我们怎么通过标志寄存器怎么看出大小呢?
mov ax, 2;
mov bx, 1;
cmp ax, bx;
2 - 1 => OF为 0 => NV(没有发生溢出)
2 - 1 => SF 为 0 => PL (非负数)
2 - 1 => ZF 为 0 => NZ (结果不为零)
2 - 1 => CF 为 0,AF 为0 => NC,NA( 没有进位错位)
PF记录奇偶用不到
mov ax, 1;
mov bx, 2;
cmp ax, bx;
1 - 2 => OF 为 0 => NV(没有发生溢出)
1 - 2 => SF 为 1 => NG (负数)
1 - 2 => ZF 为 0 => NZ (结果不为零)
1 - 2 => CF 为 1, AF 为1 => CY, AC( 发生了错位)
负数和正数,负数和负数不写了,发现起决定性作用有OF和SF和ZF
1.如果ZF = 0, ax = bx
;
2.如果OF = 0(没有溢出), SF = 0(结果为非负),ax > bx
;
3.如果OF = 0(没有溢出), SF = 1(结果为为负),ax < bx
4.如果OF = 1(溢出), SF = 0(结果为非负),溢出的正就是负, ax < bx
5.如果OF = 1(溢出), SF = 1(结果为负),溢出的负就是正,ax > bx
6个CMP后的条件转移命令
指令 | 含义 | 符号 |
---|---|---|
je | jump equal 等于则进行转移 | = |
jne | jump not equal 不等于则进行转移 | != |
jb | jump below 小于则进行转移 | < |
jnb | jump not below 不小于则镜像转移 | >= |
ja | jump above 高于则进行转移 | > |
jna | jump not above 不高于则进行转移 | <= |
只要记住e,,b,a就够了, e是等于,b是小于,a是大于
计算自定义内存段0的个数,个数存在BX里面
大于0的个数存在SI,小于零的存在DI里面
assume cs:code,ds:data
data segment
db 16 dup(0)
db 16 dup(1)
db 16 dup(2)
db 16 dup(0)
data ends
code segment
start:
mov ax, data;
mov ds, ax;//准备段地址
mov ax, 0;
mov bx, 0;
mov cx, 48;
mov bp, 0;
mov si, 0;
mov di, 0;
compare:
mov al, [bx];
cmp al, 1;
je equal;
jb below;
ja above;
equal:
inc bp;
jmp next;
below:
inc si;
jmp next;
above:
inc di;
jmp next;
next:
inc bx;
loop compare;
mov [bx], bp;//将结果存储到段 0030里面
mov [bx+1], si;
mov [bx+2], di;
mov ax, 4c00H;
int 21H;
code ends
end start
串传送指令与DF(有规则地址的拷贝)
当我们需要把一个字符串或者内存拷贝到某块内存的时候
如果我们按照之前语句编写函数,毫无疑问写出的代码非常冗余
所以CPU提供了一个很方便的拷贝内存语句
movsw(mov string word)拷贝字
movsb (mov string byte)拷贝字节
那么源地址和目的地址填在哪里呢?
里面隐藏了一些条件,ds默认为源地址,es默认为目的地址
也就是说使用这个两个命令时,要提前准备好地址
字符串肯定不是一个字,可能有多个,如何设置递增的值呢?
使用si和di寄存器,它们两个用来作增长地址或者偏移地址
它们两个要么一起增长,要么一起减
这里面用到了标志方向寄存器DF
DF = 0, si和di一起增加, 使用cld(clear df)可以把DF设置为0
DF = 1, si和di一起减少,使用std(set df)可以就可以把 DF 置为1
因为movsw
和movsb
仅仅执行串里面的一个命令
通常我们需要使用rep(repeat)和上面两个命令一起使用
rep movsw
;//rep本质是一个循环,所以需要设置CX
控制循环次数
使用串指令拷贝字符串
assume cs:code,ds:data
data segment
db 'Hello world!1234'
db 16 dup(16)
data ends
code segment
start:
mov ax, data;
mov ds, ax;
mov si, 0;
mov es, ax;
mov di, 16;
mov cx, 16;
cld;
rep movsb;
mov ax, 4c00H;
int 21H;
code ends
end start
标志寄存器的入栈(pushf)和出栈(popf)
pushf(push flag)将标志寄存器入栈
popf(pop flag)将标志寄存器出栈
标志寄存器里面的值入栈
,出栈
意味着我们可以直接访问标寄存器
计算ax的值,结果为45H
assume cs:code
code segment
start:
mov ax, 0;
push ax;//ax入栈
popf;//把ax出栈到标志寄存器,相当于清零,初始化
mov ax, 0fff0h;
add ax, 0010h;//AX = FFF0
pushf;//标准寄存器入栈
;想想add ax会改变哪些标准寄存器的值
;0 0 0 0 of df if tf sf zf 0 af 0 pf 0 cf
;of没有溢出0
;df没有方向0
;if没有中断0
;tf默认是1
;sf非负0
;zf进位归零为 1
;af低4位没有进位0
;pf全都是零是偶数 1
;cf有进位 1
;高位0 0 0 0 0 0 0 1
;低位0 1 0 0 0 1 0 1
pop ax;
and al, 11000101B;//结果为45H
and ah, 00001000B;//结果为 0
mov ax, 4c00H;
int 21H;
code ends
end start
内中断
cup中断分为内中断
和外中断
cpu的内部或者外部会产生一种特殊信息,并且立即对收到的信息作处理
cpu收到信息后转而去执行收到的特殊信息
内中断发生的4种原因
(1)除法错误,溢出,我现在也么有出现过,单单是程序的寄存器值都乱了
(2)单步执行,我们使用T命令的时候也是一种单步执行
(3)执行into 指令,它也是调试代码的一种
(4)执行 int 指令, 这个是我们可以自定义的内中断
中断一个程序需要怎么处理呢
有点线程的感觉, 无论程序怎么中断,终归还是要执行代码
代码始终在我们的内存里面,
1.CPU收到中断信息,信息里面会有一个`中断码
2.根据`中断码找到我们中断程序后要执行代码的地址
3CPU规定执行代码的地址放在指定地址
的中断向量表
里面
4CPU里面定义一个中断向量表
,专门存储程序的段地址
和偏移地址
5.CPU定义`中断码是8位的,也就是说有256个对应执行地址
6.存储一个CS:IP就需要16个字节, 规定高地址放段地址,低地址放偏移
例如:
0000:0000 60 10 00 F0 08 00 70 00-08 00 70 00 08 00 70 00
1号中断码地址就是 0070:0008
可以使用 int 1查看CS:IP校验
中断过程
因为CPU在执行中断的时候会保存当前程序的状态值
(1)取到中断码N
(2)pushf 将标志寄存器入栈,TF和IF设置为0(跟踪和中断设置为0)
(3)将代码执行地址入栈,牢记段地址先入栈,然后偏移地址入栈
(4)然后根据N找到执行地址`(CS) = (N4+2) : (IP) =(N4):
(5)执行向量表
(6)执行完后返回原来的程序,通常使用(iret)
iret相当于还原程序的执行位置和标志状态
pop IP
pop CS
popf
自定义一个int 1的中断函数
函数放哪里好呢,我们要找一块系统和程序不会被随便用到的内存
我们就放在向量表吧,程序就放在0010这个地方
要求:执行完int 1后,屏幕打印I am int 1
!,退出dos
1.编写好显示字符的中断程序
2.将编好的程序送入0010去
3.将1号向量的地址改为0000:0010
assume cs:code
code segment
start:
mov ax,cs;
mov ds,ax;//准备自己的段地址
mov si,offset int1;//准备要拷贝程序的出发地址
mov ax,0;
mov es,ax;//准备目标中断向量的段地址
mov di,200h; ;设置es:di指向目标地址
mov cx,offset iend-offset int1 ;设置cx为传输长度
cld;//设置传输方向,si和di一起递增
rep movsb;//拷贝目标程序
mov ax,0
mov es,ax
mov word ptr es:[1*4],200h
mov word ptr es:[1*4+2],0;//改写向量表的1号目的地址
mov ax,4c00h
int 21h
int1:
jmp short int_1_start
db 'i am int 1!'
int_1_start:
mov ax,cs;
mov ds,ax;//准备段地址
mov si,202h;设置ds:si指向字符串,jmp占了两个字节
mov ax,0b800h;
mov es,ax;//准备显存地址
mov di,12*160+34*2;//设置es:di指向显存空间的中间位置
mov cx,11;//设置cx为字符串长度
s:
mov al,[si];//拷贝第一个字符
mov ah,32;//准备颜色
mov es:[di],ax;//第一个字放字符,第二个放颜色
inc si;
add di,2;
loop s;
mov ax,4c00h
int 21h
iend: nop
code ends
end start
响应中断的特殊情况
在CPU运行时,并不是一旦检测到中断信息就响应中断
例如我们在调试SP,SS的时候,DEBUG下都是直接跳过了
这样的原因是SS:SP联合指向栈顶,而对它们的设置应该连续完成
想象下程序中断要干啥,是不是要入栈一些数据,要是在程序入栈前
发生了中断,等中断程序结束,下一个入栈的数据就会发生偏移
所以在执行栈操作的时候, 栈操作都是一下完成的
例如:
mov ax, 1000
mov ss, ax
mov ax, 0;//在Debug模式下你会发现这句代码直接被忽略了,但还是执行了
mov sp, 0
int指令
int 的格式为 int n, n为中断码,它的功能就是引发中断
手写一个int 1替换loop的功能
1.执行int1的时候得提前准备循环次数和偏移地址
2.得到执行代码的CS:IP
3.CX=0的时候程序要执行下一句代码
例如:在屏幕中显示80个A
assume cs:code
code segment
start:
mov ax, 0b800h;
mov es, ax;
mov di, 160*12
mov bx, offset s1 -0ffset s2;//使用BX提前存取S1到s2的偏移地址
mov cx, 80
s1:
mov byte ptr es:[di],'A';
add di, 2;
int 1H;
s2:
nop;
mov ax, 4c00H;
int 21H;
code ends
end start
int 1H的功能应该是CX递减,并且改变IP的值,让它到S1执行代码,直到CX = 0结束
getIP:
push bp;//我们需要用到BP来存IP,先把它存入寄存器
mov bp, sp;//执行int后,此时SP应该的值应该指向原来程序的BP
;//IP就应该在[bp+2]这个地方
dec cx;
jcxz goIP;//如果CX = 0,那么就不修改IP的值了
add [bp+2],bx;//加上BX,此时BX存了个负数,IP被改成了指向S
goIP:
pop bp;//还原BP的值
iret;//还原程序的状态
BIOS和DOS的区别和中断案列
BOIS(基本输入输出系统(Basic Input-Output System)
在系统主板的只读存储器(ROM)里面放着一套程序
功能:
1.硬件系统的检测和初始化
2.中断服务程序(即微机系统中软件与硬件之间的一个可编程接口)
DOS(磁盘操作工具)(Disk Operating System)
DOS主要是一种面向磁盘的系统软件,人性化的操作系统
功能:
1.管理软件和硬件资源
简单来说DOS建立在BIOS上面,相当与DOS给BIOS写了一套升级版程序
DOS使我们更加灵活的操纵BIOS
我们不能通过DOS去修改BIOS,除非有后台
BIOS的int 10H案列
assume cs:code
code segment
start:
mov ah,2;//调用10H的2号子程序显示光标函数
mov bh, 0;//第0页
mov dh, 5;//第5行
mov dl, 12;//第12列
int 10h;
mov ah, 9;//调用10H的9号子程序的显示字符函数
mov al, 'a';//设置显示字符
mov bl,11001010B;//设置样式
mov bh, 0;//设置页码
mov cx, 3;//设置循环次数
int 10h;
mov ax, 4c00h;
int 21h;
code ends
end start
DOS的int 21H案列
assume cs:code,ds:data
data segment
db 'i will blow you away','$'
data ends
code segment
start:
mov ax, data;//准备字符数据
mov ds, ax;
mov dx, 0;
mov ah, 9;//准备调用int 21的第9个函数
;//ah = 9显示字符在光标出现的位置,也就是执行后的上一行
int 21h;
mov ax, 4c00h;
int 21h;
code ends
end start
手写一个int 21H的9号函数安装在int 1;
assume cs:code
code segment
start:
mov ax,cs;
mov ds,ax;
mov si,offset show_str ;设置ds:si指向源地址
mov ax,0 ;
mov es,ax;
mov di,200h; ;设置es:di指向目标地址
mov cx,offset show_strend-offset show_str ;设置cx为传输长度
cld; ;设置传输方向为正
rep movsb;//拷贝函数到目标位置
mov ax,0;
mov es,ax;
mov word ptr es:[1*4],200h
mov word ptr es:[1*4+2],0;设置中断向量表指向我们的函数
mov ax,4c00h;
int 21h;
;名称:show_str
;功能:在指定的位置,用指定的颜色,显示一个用0结束的字符串。
;参数:(dh)=行号(取值范围0~24);
; (dl)=列号(取值范围0~79);
; (cl)=颜色;
; ds:si指向字符串的首地址。
;返回:无。
show_str:
push ax
push bx
push es
push si
mov ax,0b800h
mov es,ax;
mov ax,160
mul dh; ;计算放在第几行
mov bx,ax ;bx=160*dh
mov ax,2
mul dl ;ax=dl*2,计算放在第几列
add bx,ax ;mov bx,(160*dh+dl*2)设置es:bx指向显存首地址
mov al,cl ;把颜色cl赋值al
mov cl,0;//为了不影响CX的值
show0:
mov ch,[si];//将目标字符拷贝到ch里面
jcxz show1;//(ds:si)=0时,转到show1执行
mov es:[bx],ch;// 目标字符到显存里面
mov es:[bx].1,al;// 目标字符的颜色配置到显存里面
inc si;ds:si指向下一个字符地址
add bx,2;es:bx指向下一个显存地址
jmp show0
show1:
pop si
pop es
pop bx
pop ax;//返回原来的配置
iret
mov ax,4c00h
int 21h
show_strend:
nop
code ends
end start
CPU与端口
在PC机系统中,和CPU通过总线相连的芯片除各种存储器外,还有以下3种芯片:
1)各种接口卡(比如:网卡、显卡)上的接口芯片,它们控制接口卡进行工作;
2)主板上的接口芯片,CPU通过它们对部分外设进行访问;
3)其他芯片,用来存储相关的系统信息,或进行相关的输入输出处理。
在这些芯片中,都有一组可以由CPU读写的寄存器。
这些寄存器,它们在物理上可能处于不同的芯片中,但是它们在以下两点上相同:
1)都和CPU的总线相连,当然这种连接是通过它们所在的芯片进行的;
2)CPU对它们进行读或写的时候都通过控制线向它们所在的芯片发出端口读写命令。
CPU可以直接读写三个地方的数据
1)CPU内部的寄存器;
2)内存单元;
3)端口中。(芯片中的寄存器)
所谓的端口只是芯片中的寄存器的抽象概念,
cpu对他们进行统一编址。从而建立起统一的端口地址空间
每一个端口在地址空间都有一个地址
在PC系统中,CPU最多可以定位64KB个不同的端口。
则端口的地址范围为0~65535.
使用in(读取)
和out(写入)
操作端口的值
8086系统汇编对端口的读写只能用in和out指令。
区分下从访问内存和访问端口的步骤
1)访问内存
mov ax, ds:[0];
执行步骤
1.cpu通过地址线将地址信息8发出
2.cpu通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据
3.存储器将0号单元中的数据通过数据线送入CPU的ax寄存器
2)访问端口
in al, 10H;//从16号端口读入一个字节
执行步骤
1..CPU通过地址线将地址信息10H发出
2.CPU通过控制线发出端口命令,告诉芯片将要读取该端口的信息
3.端口所在芯片将10H端口的信息送到al中
注意:在in
和out
命令中,
1.只能使用ax或者al来存放端口数据
2.访问8位端口(0-255)的端口数据时用al
3.访问16位端口(256-65535)端口时ax
mov al, 2;
out 70h, al;//将2送入端口70h
in al, 71h;//将71h单元内容送入al
SHL(shift logical left)和SHR(shift logical right)
shl 相当于C里面的<<
,逻辑左移指令
功能:shl 寄存器, 左移动次数
1)将一个寄存器或内存单元的数据向左移位
2)将最后移出的一位写入CF,也就是一开始在尾部写到CF里面
3)溢出的数据,最低位用0补充
数据左移n位就是 数据*2^n
,前提数据没有溢出
例如:
mov al, 01110000;
shl al, 1;//将al中的数据左移一位
结果:
al = 11100000;
CF = 0;//最后一位是0,所以CF是0
注意当位移次数大于1的时候,请把位移的次数放在CL里面,放在别的寄存器会报错
shr相当于C里面的>>
,逻辑左移指令
功能:shr 寄存器, 左移动次数
1)将一个寄存器或内存单元的数据向右移位
2)将最后移出的一位写入CF,也就是一开始在头部的值写到CF里面
3)溢出的数据,最高位用0补充
数据右移n位就是 数据÷2^n
,前提数据没有溢出
例如:
mov al, 01110000;
shr al, 1;//将al中的数据右移一位
结果:
al = 00111000;
CF = 0;//第一位是0,所以CF为0
计算2的8次方256
mov ax, 1;
mov cl, 8;
shl ax, cl;//结果AX = 100H
计算256的8次开平方
mov ax, 100H;
mov cl, 8;
shr ax, c1;//结果AX = 1
读取CMOS RAM的时间显示到DOS窗口
1.从CMOS RAM的8号单元读取当前月份的BCD码
BCD码就是用2进制的4个位来表示一个十进制数
例如: 0001 0001
1.按照正常理解方式这个就是17
2.按照BCD码理解它就是11
已知时间单元的顺序是
秒: 0 分:2 时:4 日:7 月:8 年:9
2.将对应的年月份BCD码转成对应ASCII码
10进制的值+30H就是对应的ASCLL码了
3.将ASCII码放到显存里面
assume cs:code
code segment
time db 'yy/mm/dd hh:mm:ss','$'
cmos db 9,8,7,4,2,0
start:
mov ax,cs
mov ds,ax
mov bx,0
mov si,0
mov cx,6;//准备6次循环读取6个时间值
getTime:
push cx;//因为CL会用来存偏移值,提前存起来
mov al,cmos[bx];//提取对应的单元数据
out 70h,al ;//将al送入地址端口70h
in al,71h ;//从数据端口71h处读出单元内容
mov ah,al
mov cl,4
shr al,cl ;//右移4位
and ah,0fh ;//al分成两个表示BCD码值的数据
add ax,3030h ;//BCD码+30h=10进制数对应的ASCII码
mov cs:[si],ax ;//ASCII码写入time段
inc bx;//准备提取下一个时间
add si,3;// yy/一共3个字节
pop cx;
loop getTime
;名称:BIOS中断(int 10h)
;功能:(ah)=2置光标到屏幕指定位置、(ah)=9在光标位置显示字符
;参数:(al)=字符、(bh)=页数、(dh)=行号、(dl)=例号
;(bl)=颜色属性、(cx)=字符重复个数
mov ah,2 ;置光标
mov bl,11001010B;//设置样式
mov bh,0 ;第0页
mov dh,13 ;dh中放行号
mov dl,32 ;dl中放例号
int 10h
;名称:DOS中断(int 21h)
;功能:(ah)=9显示用'$'结束的字符串、(ah)=4ch程序返回
;参数:ds:dx指向字符串、(al)=返回值
mov dx,0
mov ah,9
int 21h
;结束
mov ax,4c00h
int 21h
code ends
end start
外中断
简单来说除了CPU和内存,一台正常的PC还有显卡,显示器,电源,键盘等
当这些外接设备请求输入,输出
来处理目的程序就得发生外中断
我们在键盘上面按下一个键,显示在屏幕上面
CPU得接收键盘的信息,然后把键盘的输入存到显存里面
CPU要及时处理外部的输入和输出,肯定要有个实时的监听器
在PC系统中,外中断源分为两类
CPU可以不响应外中断, 可以根据中断标志IF
来设置是否响应中断
IF = 0表示不响应中断(可屏蔽中断)
IF = 1表示在执行完当前指令后响应中断 (不可屏蔽中断)
回忆发生内中断
的时候我们把IF = 0的意思
就是执行内中断程序的时候屏蔽外部中断
模拟键盘产生外中断
按下一个按钮按钮会产生一个扫描码送入主板上面的接口芯片寄存器里面
按下的按钮称为通码,通码的第7位为0
松开的键被称为断码,断码的第7位为1
2^8 = 128 = 80H
断码 = 通码+80H
F0 = 1000 0000
例如 A 的扫描码为1E,那么它的断码就是1E + 80 = 90E
键盘的输入处理过程:
1.键盘产生扫描码
2.扫描码送入60h端口
3.引发9号中断
4.CPU执行int 9中断例程处理键盘输入
给BIOS的int 9扩展一个功能
1)从60H端口读出键盘的输入
in al, 60H
执行原来的int 9函数,为了不破坏原来的Int 9函数
cmp al, 1;//判断是否按下ESC按钮
2)调用int 9中断案例,判断是否符合条件的输入
因为int 9已经被我们新的函数覆盖了,手写一个调用原来int 9的方法
我们把int 9的原地址放到int 0去
int 就是标志寄存器入栈,TF,IF设置为0,CS,IP入栈
pushf;//因为中断已经清零了TF,IF,不用拿出来设置值了
call dword ptr ds:[0];//CS和IP入栈,(IP) = ds:[0], (CS) = ds:[2]
int 9实现功能按下C
键设置颜色
;//任务:安装一个新的int 9中断例程
;//功能:在DOS下,按'C'键后除非不再松开,就把屏幕的背景设置成绿色
assume cs:code
stack segment
db 128 dup (0)
stack ends
code segment
start:
mov ax,stack;
mov ss,ax;
mov sp,128;
push cs;
pop ds;
mov ax,0;
mov es,ax;
mov si,offset int9;//设置ds:si指向源地址
mov di,204h;//设置es:di指向目标地址
mov cx,offset int9end-offset int9;//设置cx为传输长度
cld;//设置传输方向为正
rep movsb;
;//将原来的int 9中断例程的入口地址保存在ds:200、ds:202单元中
push es:[9*4];
pop es:[200h];
push es:[9*4+2];
pop es:[202h];
;//在中断向量表中设置新的int 9中断例程的入口地址
cli;//设置IF=0屏蔽中断
mov word ptr es:[9*4],204h;
mov word ptr es:[9*4+2],0;
sti;//设置IF=1不屏蔽中断
;//结束
mov ax,4c00h;
int 21h;
;//新的int 9中断例程
int9:
push ax;
push bx;
push cx;
push es;
in al,60h;从端口60h读出键盘输入
;//对int指令进行模拟,调用原来的int 9中断例程
pushf;//标志寄存器入栈
call dword ptr cs:[200h];//CS,IP入栈,(IP)=cs:[200h],(CS)=0
;//如果是A断码,改变当前屏幕的显示字符
cmp al,0aeh;//和C的断码(2eh+80h)比较
jne int9ret;//不等于C时转移
mov ax,0b800h;
mov es,ax;
mov bx,0;
mov cx,2000;//80 行 * 25个字符
s:
mov byte ptr es:[bx+1],32;//将绿色写入显存里面
add bx,2;
loop s;
int9ret:
pop es;
pop cx;
pop bx;
pop ax;
iret;
int9end:
nop;
code ends
end start
汇编标号寻址妙用
在ASM文件里面,标号不仅仅表示内存单元的地址还表示了内存单元的长度
例如:计算1到8的内存值之和,结果放在第8位,结果是24H = > 36
assume cs:code
code segment
a db 1,2,3,4,5,6,7,8
b dw 0
start:
mov si, 0;
mov cx, 8;
s:
mov al, a[si];//编译后这里是 mov al, [si+0000]
mov ah, 0;
add b, ax;//编译后这里是 add [0008], ax
inc si;
loop s;
mov ax, 4c00H
int 21h;
code ends
end start
注意在code断中使用的标号a,b后面没有’ : ‘’,我第一次就写错了,
发现在定义 db,dw,dd的时候,在前面加上标号的话
我们在代码段里面这个标号有了两层意思
标号的内存位置
标号的单元大小
db与标号
a db 1//定义第一个字节为1,标号为a 内存里面是 00
mov a, ax;//这里会编译报错 Operand types mush match 操作类型必须匹配
这里的a是一个字节型的,应该
mov a, al;//这样就不会报错了,相当于 mov byte ptr cs:[0], al
dw与标号
a dw 1//定义第一个字为1,标号为a 内存里面是 01 00
mov a, al;//这里会编译报错 Operand types mush match 操作类型必须匹配
这里的a是一个字型的,应该
mov a, ax;//这样就不会报错了,相当于 mov word ptr cs:[0], ax
dd会报 illegal size for operand(操作数大小错误)
a[0]
标号经过MASM编译后变成了 类型 ptr 段地址:[0]
在上面的累计程序中,我们并没有设置段地址,它默认用了CS作为段地址
标号的段地址是和伪指令assume相关的,你用什么段定义的决定用什么段地址
例如用ds:data定义的,data里面声明的标号用段地址就是ds
将数据段的标号定义为数据,段地址,偏移地址
assume cs:code
code segment
a db 1,2,3,4,5,6,7,8
b db 8 dup(0)
c dw a, b, 6 dup(0)
;//dw a, b把a和b视为偏移地址
;//所以有应该是00 00 08 00
d dw offset a, offset b, 6 dup(0)
;//与上面同义
e dd a, b, 2 dup(0)
;//dd a, b把a和b的段地址和偏移地址作为数据
;//所以应该是 00 00 6a 07 08 00 6a 07
f dw offset a, seg a, offset b, seg b, 4 dup(0)
;//与上面同义
start:
mov ax, 4c00H
int 21h;
code ends
end start
利用标号作为偏移地址编写一个多功能程序
(1)清屏;
(2)设置前景色;
(3)设置背景色;
(4)向上滚动一行;
入口参数说明如下。
(1)用ah寄存器传递功能号:0表示清屏,1表示设置前景色,2表示设置背景色,3表示向上滚动一行;
(2)对于2、3号功能,用al传送颜色值,(al)∈{0,1,2,3,4,5,6,7}。
assume cs:code
code segment
start:
;//mov ax, 0;清屏
;mov ax, 102H;//设置字体为绿色
;mov ax, 202H;//设置背景色为绿色
mov ax, 300H;//向上滚动一行
call setscreen
mov ax, 4c00h
int 21h
;==================================================
;将这些功能子程序的入口地址存储在一个表中
;功能号*2 = 对应的功能子程序在地址表中的偏移
;param ah 功能号
;==================================================
setscreen:
jmp short set
table dw sub1, sub2, sub3, sub4;//里面存放4个程序的偏移地址
set:
push bx
cmp ah, 3;
ja sret;//如果ah的值大于3就什么都不干
mov bl, ah;
mov bh, 0;
add bx, bx ;根据ab中的功能号计算对应子程序在table表中的偏移
call word ptr table[bx];//jmp word ptr cs:table[bx]
sret:
pop bx
ret
;=========================
;清屏子程序
;=========================
sub1:
push bx
push cx
push es
mov bx, 0b800h
mov es, bx
mov bx, 0
mov cx, 2000
subls:
mov byte ptr es:[bx], ' '
add bx, 2
loop subls
pop es
pop cx
pop bx
ret
;=========================
;设置前景色
;param al 前景色
;=========================
sub2:
push bx
push cx
push es
mov bx, 0b800h
mov es, bx
mov bx, 1
mov cx, 2000
sub2s:
and byte ptr es:[bx], 11111000b;//把前景色清空
or es:[bx], al;//给前景色赋值
add bx, 2
loop sub2s
pop es
pop cx
pop bx
ret
;=========================
;设置背景色
;param al 背景色
;al 低4位 存放颜色值
; 高4位 0
;=========================
sub3:
push bx
push cx
push es
mov cl, 4
shl al, cl
mov bx, 0b800h
mov es, bx
mov bx, 1
mov cx, 2000
sub3s:
and byte ptr es:[bx], 10001111b;/把背景色清空
or es:[bx], al;//给背景色赋值
add bx, 2
loop sub3s
pop es
pop cx
pop bx
ret
;=========================
;向上滚动一行
;=========================
sub4:
push cx
push si
push di
push es
push ds
mov si, 0b800h
mov es, si
mov ds, si
mov si, 160;//ds:si 指向第n+1行
mov di, 0;//es:di 指向第n行
cld
mov cx, 24;共复制24行
sub4s:;复制
push cx
mov cx, 160
rep movsb;//相当与把第二行的数据拷贝到第一行
pop cx
loop sub4s;//24行数据往上移动
mov cx, 80
mov si, 0
sub4s1:;//最后一行清空
mov byte ptr [160*24+si], ' '
add si, 2
loop sub4s1
pop ds
pop es
pop di
pop si
pop cx
ret
code ends
end start
使用BIOS进行键盘输入和磁盘读写
在BIOS中键盘输入会引发9号中断,从60h端口读取信息,
并且转化成响应的ASCII码.存储到指定的键盘缓冲区
BIOS提供了int 16h中断案例从缓冲区读取一个键盘输入,并从缓冲区删除
int 9负责读键盘写入到缓冲区,int 16负责从缓冲区读出来
int 9是有键按下才向缓冲区写入数据
int 16h是应用程序对其调用才把数据从缓冲区读出来
在执行int 16H的时候,一定设置IF = 1的指令
缓冲总有被清空的时候,如果int 16H不释放执行权(IF=0),
那么int 16H将永远处于等待状态,键盘无法输入响应了
;接收用户的键盘输入, 输入'r',将屏幕上的字符设置为红色
; 输入'g',将屏幕上的字符设置为绿色
; 输入'b',将屏幕上的字符设置为蓝色
assume cs:code
code segment
start:
mov ah, 0
int 16h;//从键盘缓冲区中读取数据[ah:扫描码、al:ASCII]
mov ah, 1;//初始化 颜色 1111 1000 or [ah=0000 0001] 蓝色
cmp al, 'r'
je red
cmp al, 'g'
je green
cmp al, 'b'
je blue
jmp short sret
red:
shl ah, 1;//red 0001 => 0100一共需要左移两次
green:
shl ah, 1;//green 0001 => 0010 一共需要左移一次
blue:
mov bx, 0b800h
mov es, bx
mov bx, 1
mov cx, 2000
s:
and byte ptr es:[bx], 11111000b
or es:[bx], ah
add bx, 2
loop s
sret:
mov ax, 4c00h
int 21h
字符串输入
最基本的字符串输入程序,需要具备下面的功能
1)在输入的同时需要显示这个字符串
调用 int 16H 读入键盘的输入
如果是字符,进入字符栈,显示字符栈中所有字符,继续执行int16 H
2)一般在输入回车符后,字符串输入结束
如果是Enter键,向字符栈压入0,返回
3)能够删除已经输入的字符
如果是推个键,从字符栈中弹出一个字符,显示字符栈中所有的字符
继续执行int 16H
字符串输入函数
assume cs:code
code segment
start:
mov si,0
mov dl,0
mov dh,0
call getstr
mov ax,4c00h
int 21h
;//子程序:接收字符串输入。
getstr:
nop
getstrs:
mov ah,0;
int 16h;
cmp al,20h;
jb nochar;//ASCII码小于20h,说明不是字符
mov ah,0;//准备调用0号功能字符入栈
call charstack;//字符入栈
mov ah,2;//准备调用2号功能显示字符
call charstack;显示栈中字符
jmp getstrs;
nochar:
cmp ah,0eh;//退格键的扫描码
je backspace;
cmp ah,1ch;//Enter键的扫描码
je enter;
jmp getstrs;
backspace:
mov ah,1;
call charstack;//字符出栈
mov ah,2;
call charstack;显示栈中字符
jmp getstrs
enter:
mov al,0;
mov ah,0;
call charstack;0入栈
mov ah,2;
call charstack;显示栈中字符
ret
;子程序:字符栈的入栈、出栈和显示。
;参数说明:(ah)=功能号,0表示入栈,1表示出栈,2表示显示
; ds:si指向字符栈空间;
; 对于0号功能:(al)=入栈字符;
; 对于1号功能:(al)=返回的字符;
; 对于2号功能:(dh)、(dl)=字符串在屏幕上显示的行、列位置。
charstack:
jmp short charstart
table dw charpush,charpop,charshow
top dw 0 ;//栈顶(字符地址、个数记录器)
charstart:
push bx
push dx
push di
push es
cmp ah,2;//判断ah中的功能号是否大于2
ja sret;//功能号>2,结束
mov bl,ah;
mov bh,0;
add bx,bx;//计算对应子程序在table表中的偏移,得到对应的偏移地址
jmp word ptr table[bx];//调用对应的功能子程序
charpush:
mov bx,top;
mov [si][bx],al;
inc top;
jmp sret;
charpop:
cmp top,0;
je sret;//栈顶为0(无字符),结束
dec top;
mov bx,top;//保存数据,其它作用不详
mov al,[si][bx];//保存数据,其它作用不详
jmp sret
charshow:
mov bx,0b800h
mov es,bx
mov al,160
mov ah,0
mul dh;//dh*160
mov di,ax
add dl,dl;//dl*2
mov dh,0
add di,dx;//di=dh*160+dl*2,es:di指向显存
mov bx,0;//ds:[si+bx]指向字符串首地址
charshows:
cmp bx,top;//判断字符栈中字符是否全部显示完毕
jne noempty;//top≠bx,有未显示字符,执行显示
mov byte ptr es:[di],' ';//显示完毕,字符串末加空格
jmp sret
noempty:
mov al,[si][bx];//字符ASCII码赋值al
mov es:[di],al;//显示字符
mov byte ptr es:[di+2],' ';//字符串末加空格
inc bx;//指向下一个字符
add di,2;//指向下一显存单元
jmp charshows
sret:
pop es
pop di
pop dx
pop bx
ret
code ends
end start
将第一面显存数据写入磁盘当中
以3.5英寸软盘为例,3.5英寸分为上下两面
一共两面
每一面80个磁道
每个磁道分为18个扇区
每个扇区的大小为512个字节
2面 * 80磁道 * 18扇区 * 512字节 = 1440kb
BIOS提供int 13h对磁盘进行操作
;读取0面0道1扇区的内容道到):200的程序
mov ax, 0b800H;
mov es, ax;
mov bx, 0;//es:bx 指向扇区的数据
mov al, 8;//准备8个扇区,一面4000字节,一个扇区500字节
mov ch, 0;//磁道号,磁道从0开始, 0 -79
mov cl, 1;//扇区号,注意扇区从1开始, 1-18
mov dl, 0;//驱动号,软驱从0开始
mov dh, 0;//磁头号,对于软盘及面号,一个面用一个磁头来读写
mov ah, 3;//2号表示读取扇区,3 表示写扇区
int 13h;//调用13h
编写一个基本操作系统重写BIOS的int 19H中案例
开机后,CPU自动进入到FFFF:0单元处执行,此处有一条跳转指令。
CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。
初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。
硬件系统检测和初始化完成后,调用int 19h
进行操作系统的引导。
如果设为从软盘启动操作系统,则int 19h将主要完成以下工作。
(1)控制0号软驱,读取软盘0道0面1扇区的内容到0:7c00;
(2)将CS:IP指向0:7c00。
软盘的0道0面1扇区中装有操作系统引导程序。int 19h
将其装到0:7c00处后
设置CPU从0:7c00开始执行此处的引导程序,操作系统被激活,控制计算机。
如果在0号软驱中没有软盘,或发生软盘I/O错误,则int 19h将主要完成以下工作。
(1)读取硬盘C的0道0面1扇区的内容到0:7c00;
(2)将CS:IP指向0:7c00。
该程序的功能如下:
(1)列出功能选项,让用户通过键盘进行选择,界面如下
1) reset pc ;重新启动计算机
2) start system ;引导现有的操作系统
3) clock ;进入时钟程序
4) set clock ;设置时间
(2)用户输入”1”后重新启动计算机(提示:考虑ffff:0单元)
(3)用户输入”2”后引导现有的操作系统(提示:考虑硬盘C的0道0面1扇区)。
(4)用户输入”3”后,执行动态显示当前日期、时间的程序。
显示格式如下:年/月/日 时:分:秒
进入此项功能后,一直动态显示当前的时间,在屏幕上将出现时间按秒变化的效果(提示: 循环读取CMOS)。
当按下F1键后,改变显示颜色;按下Esc键后,返回到主选单(提示:利用键盘中断)。
(5)用户输入”4”后可更改当前的日期、时间,更改后返回到主选单(提示:输入字符串)。
由于代码过长保存在文本文件里面
在虚拟机软盘里面安装我们的系统
1.使用Oracle VM VirtualBox 进行安装, 下载安装Oracle VM VirtualBox
2.下载XP系统,使用迅雷下载,500M的ISO镜像文件,这是一个家庭版
ed2k://|file|sc_winxp_home_with_sp2.iso|611358720|B80F4CCF312420015FFD5740057085B0|/
3.安装XP需要密钥3FKBQ-32TH7-D3TJB-YBWTQ-D26VQ
4.安装好XP后,安装增强功能,然后关机
5.添加软盘
6.与虚拟机共享我们编译好的程序的文件夹,注意勾选自动加载
7.准备启动XP
8.启动XP前把软盘弹出,因为系统优先启动软盘
9.进入XP后,把弹出的软盘加载回来,点击我的电脑
10.先格式化软盘A,然后在XP运行我们共享文件夹里面的sys.exe
11.然后查看软盘A的属性,发现大小和可用都是0了
12.重启XP
13.我在网上好像发现VMware也是可以的,因为视频是用的oracle box
Window 32 下32位汇编语言的设计
罗云彬版本,大致了解下