x86汇编语法
- 注释
1 | ;我是注释 |
- 变量取值和赋值(传送指令)
1 | ;赋值 |
存放的数据大小根据使用的寄存器而定, 比如ax是16位寄存器,最大只能存放16位数据,也就是4位十六进制数据
十六进制数据不能以字母开头, 前面需加上0 否则编译报错
- 函数声明
结构如下:
1 | 函数名: |
示例:
1 | print: ;函数名 |
- 函数调用
x86架构中使用关键指令call
x86架构汇编示例:
1 | call print ;调用print函数 |
字符串的定义
起因:如果直接将字符串赋值给通用寄存器,会出现以下两个问题:
- 字符顺序是反着的
- 最多只能存放两个字符
- 无法获取到数据地址,不能堆字符串进行修改
为了解决这个问题,需要使用另外一种方式,定义字符串
首先:需要先在内存中申请一块空间,可以使用伪指令db和dw
1
2
3
4
5db-->define byte 定义字节 读写一个数据,偏移量加1
dw-->define word 定义字 读写一个数据,偏移量加2
dd-->define double word 定义双字, 读写一个数据 偏移量加4示例
1
2db 'hello' ;占用五个字节的内存空间
dw 'hello' ;占用六个字节的内存空间 跟偏移量有关系
如果定义数字,使用dw每个数字占用两个字节的空间, 字符串比较特殊,并不是每个字符占用两个字节,而是总长度必须是2的倍数
字符串的获取
获取字符串的数据,首先要获取到数据所对应的内存地址
那怎么获取已经定义好的地址呢?
第一步尝试: 给数据添加别名
1
2
3
4
5
6
7str db 'hello'
start:
mov bx,str ;别名中存放的是偏移地址
end start别名中存放的是偏移地址,但是光有偏移地址还不行,还需要段地址, 段地址+偏移地址=实际物理地址,别名默认从ds寄存器中读取段地址, 但是我们并没有给ds寄存器赋过值, 这就导致我们无法获取正确的数据,因为我们不知道正确的段地址是多少?
那字符串段地址从哪里获取呢?
方法一: 直接从内存中找(仅限于调试,实际开发肯定不行)
方法二:使用段进行包裹, 段能给我们提供一个段地址(正解)
1
2
3
4
5
6
7
8
9
10data segment
str db 'hello'
data ends
;使用段进行包裹, 可以借助段名称获取段地址
start:
mov ax,data
mov ds,ax
mov bx,str
end start
对内存中的数据进行读写
从内存中一次读取数据的多少,取决于寄存器的容器大小
1
2
3
4
5
6
7
8
9
10data segment
str dw 'hello' ;如果定义多个数据 使用逗号进行分隔
data ends
start:
mov ax,data
mov ds,ax
mov ax,str ;如果从内存中读取数据,是根据寄存器大小来读取,16位寄存器则一次性读取16位数据,8位al则一次性读取八位数据
end start
思考:为什么以下写法报错:
1 | ;报错1 |
内存数据的读写是从低往高进行读写
上面使用db或者dw定义数据的方式,定义数据的同时就已经定义好了数据所在的物理地址, 如果我们想要从指定的内存地址中写入或者读取数据的话,需要借助段寄存器来实现 在8086中给我们提供了DS SS CS ES四个寄存器,理论上你使用哪一个都行,但是由于系统默认读取DS寄存器中的数据当做段地址,所以我们一般使用DS进行数据的段地址管理
如何从指定内存中读取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20;假设我们需要从0710:0000这个物理地址中读取数据,然后存放到寄存器中
;错误写法1:
start:
mov ax,0710H:0000H ;没有这种语法
end start
;错误写法2:
start:
mov ds,0710H ;段寄存器不能直接赋值,必须借助通用寄存器
mov ax,ds:[0]
end start
;正确写法:
start:
mov ax,0710H
mov ds,ax
mov ax,ds:[0] ;实际物理地址 段地址+偏移地址 ===>ds:[xxx] 表示从该地址取数据
end start
如何往指定内存中写入数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50;假设我们需要将数据写入0710:0000这个物理地址中
;错误写法1:
start:
mov ax,3333H
mov 0710:0000,ax ;没有这种语法
end start
;错误写法2:
data segment
str dw 'he'
data ends
start:
mov ax,data
mov ds,ax
mov ds:[0],ds:str ;必须借助通用寄存器进行赋值
end start
;正确写法1:
data segment
str dw 'he'
data ends
start:
mov ax,data
mov ds,ax
mov ax,ds:str ;str-==>[xx] ds:[xxx]
mov ds:[0],ax
end start
;正确写法2:
start:
mov ax,0710H
mov ds,ax;指定需要写入数据的段地址
mov ax,3333H ;将3333H当做数据
mov ds:[0],ax
end start
;正确写法3:
start:
mov ax,0710H
mov ds,ax;指定需要写入数据的段地址
mov ds:[0],3333H ;可以直接将数据写入 最多写入十六位的数据
end start
补充:往内存中写入数据是字节宽度还是字型宽度取决于寄存器的宽度也就是
mov ds:[0],ax
或者数据的大小:mov ds:[0] 30H
1 | ; 以下是指定数据占用空间的大小 可以实现8位数据占用16个字节的空间的目的 |
8. 字符串修改和替换
1 | ;需求1 : 将内存中he修改为wo |
分段写法:
1 |
|
Loop循环指令
类似于高级语言中的while循环, 系统默认从cx寄存器中读取数据作为循环的条件,当cx中的值cx-1大于零时循环执行一次代码
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45;需求 : 将内存中wowowo修改为hello
data segment
str dw 'hello '
newstr dw 'wowowo'
data ends
start:
mov ax,data
mov ds,ax
mov bx,0
mov cx,3
replace:
mov ax,ds:[bx]
mov ds:[bx+6],ax
add bx,2
loop replace
end start
;需求 : 将内存中wowowo修改为hello
data segment
dw 'aaa'
str dw 'hello '
newstr dw 'wowowo'
data ends
start:
mov ax,data
mov ds,ax
mov bx,offset str
mov cx,3
replace:
mov ax,ds:[bx]
mov ds:[bx+6],ax
add bx,2
loop replace
end start加减运算指令add和sub
1
2
3
4
5
6
7
8
9
10
11
12add ax,2 ;ax=ax+2
sub ax,2 ;ax=ax-2
sub/add 通用寄存器,数值 ;add/sub ax,2
sub/add 通用寄存器,通用寄存器 ;add/sub ax,bx
sub/add 内存地址,通用寄存器 ;add/sub ds:[0],bx
sub/add 通用寄存器,内存地址 ;add/sub ax,ds:[0]
;错误写法
sub/add 内存地址,内存地址 ;;add/sub ds:[0],ds:[3] 不允许这样写
中断
顾名思义,程序运行到一半暂时断开,官方一点说就是,由于软件或者硬件信号,使得cpu暂停当前任务,转而执行另一段子程序
可以形象理解为游戏中暂时搁置主线任务临时去完成支线任务
中断的分类:
外中断 (硬中断):由外部设备(比如网卡,或者硬盘 键盘 鼠标)引发的中断,比如当网卡收到数据包的时候,就会发出一个中断
内中断(软中断):由执行的中断指令产生的,可以通过程序控制触发
我们接下来要学习的是内中断知识,如果我们想要通过代码发出一个中断,那么需要使用中断指令
int
1
int 21h ;执行中断码为21H的中断指令
cpu接收到中断信号后,暂停当前正在执行的指令,临时去执行中断码对应的内容
中断码不止一个,每个码代表着不同的含义,部分中断码列表如下:
中断 功能 入口参数 出口参数 INT16 键盘输入 AH=0H读键盘 AH=10读扩展键盘 AH=键盘扫描码 AL=字符ascii码 INT20 程序正常退出 CS=PSP段地址 INT21 系统功能调用 AH=功能号 INT22 程序结束处理 INT23 Ctrl-Break处理 AL=0(忽略) INT24 严重错误处理 AL=驱动器号 AL=1(重试)AL=2(通过INT 23H终止)Cy=1出错 INT25 绝对磁盘读 CX=读入扇区数DX=起始逻辑扇区数DS:BX=缓冲区地址AL=驱动器号 Cy=0正确 INT26 绝对磁盘写 CX=写盘扇区数DX=起始逻辑扇区数DS:BX=缓冲区地址 INT27 驻留退出 CS=PSP段地址DX=程序末地址+1
**二、DOS功能调用**
**功能号在AH中,并设好其余的入口参数,向DOS发出INT21H命令,最后获得出口参数。**
| 调用号 | 功能 | 入口参数 | 出口参数 |
| ------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 00H | 程序终止 | CS=PSP段地址 | |
| 01H | 键盘输入字符 | | AL=输入的字符 |
| 02H | 显示输出 | DL=显示的字符 | |
| 03H | 串行设备输入 | | AL=输入的字符 |
| 04H | 串行设备输出 | DL=输出的字符 | |
| 05H | 打印输出 | DL=输出的字符 | |
| 06H | 直接控制台I/O | DL=0FFH(输入请求)DL=字符(输出请求) | AL=输入的字符 |
| 07H | 直接控制台I/O(不显示输入) | | AL=输入的字符 |
| 08H | 键盘输入字符(无回显) | | AL=输入的字符 |
| 09H | 显示字符串 | DS:DX=缓冲区首址 | |
| 0AH | 输入字符串 | DS:DX=缓冲区首址 | |
| 0BH | 检查标准输入状态 | | AL=00无按键AL=0FFH有按键 |
| 0CH | 清除输入缓冲区并执行指定的标准输入功能 | AL=功能号(01/06/07/08/0AH)DS:DX=缓冲区(0AH功能) | AL=输入的数据(功能01/06/07/08) |
| 0DH | 初始化磁盘状态 | | |
| 0EH | 选择缺省的驱动器 | DL=驱动器号(0=A,1=B..) | AL=逻辑驱动器数 |
| 0FH | 打开文件 | DS:DX=未打开的FCB首址 | AL=00成功,0FFH失败 |
| 10H | 关闭文件 | DS:DX=打开的FCB首址 | AL=00成功,0FFH失败 |
| 11H | 查找第一匹配目录 | DS:DX=未打开的FCB首址 | AL=00成功,0FFH失败 |
| 12H | 查找下一匹配目录 | DS:DX=未打开的FCB首址 | AL=00成功,0FFH失败 |
| 13H | 删除文件 | DS:DX=未打开的FCB首址 | AL=00成功,0FFH失败 |
| 14H | 顺序读 | DS:DX=打开的FCB首址 | AL=00成功,01文件结束02缓冲区太小03缓冲区不满 |
| 15H | 顺序写 | DS:DX=打开的FCB首址 | AL=00成功,01盘满02缓冲区太小 |
| 16H | 创建文件 | DS:DX=未打开的FCB首址 | AL=00成功0FFH目录区满 |
| 17H | 文件换名 | DS:DX=被修改的FCB首址 | AL=00成功,0FFH未找到目录项或文件重名 |
| *18H | 保留未用 | | |
| 19H | 取缺省驱动器号 | | AL=驱动器号(0=A,1=B..) |
| 1AH | 设置磁盘缓冲区DTA | DS:DX=磁盘缓冲区首址 | |
| *1BH | 取缺省驱动器的磁盘格式信息 | | AL=每簇的扇区数CX=每扇区的字节数DX=数据区总簇数-1DS:BX=介质描述字节 |
| *1CH | 取指定驱动器的磁盘格式信息 | DL=驱动器号(0=缺省,1=A..) | AL=每簇的扇区数CX=每扇区的字节数DX=数据区总簇数-1DS:BX=介质描述字节 |
| *1DH | 保留未用 | | |
| *1EH | 保留未用 | | |
| *1FH | 取缺省驱动器的DPB | | DS:BX=DPB首址 |
| *20H | 保留未用 | | |
| 21H | 随机读一个记录 | DS:DX=打开的FCB首址 | AL=00成功,01文件结束02缓冲区太小03缓冲区不满 |
| 22H | 随机写一个记录 | DS:DX=打开的FCB首址 | AL=00成功,01盘满02缓冲区太小 |
| 23H | 取文件大小 | DS:DX=未打开的FCB首址 | AL=00成功,0FFH失败 |
| 24H | 设置随机记录号 | DS:DX=打开的FCB首址 | |
| 25H | 设置中断向量 | AL=中断号DS:DX=中断程序入口 | |
| *26H | 创建新的PSP | DS:DX=新的PSP段地址 | |
| 27H | 随机读若干记录 | DS:DX=打开的FCB首址CX=要读入的记录数 | AL=00成功,01文件结束AL=02缓冲区太小AL=03缓冲区不满CX=读入的块数 |
| 28H | 随机写若干记录 | DS:DX=打开的FCB首址CX=要写入的记录数 | AL=00成功,01盘满AL=02缓冲区太小AL=03缓冲区不满CX=已写的块数 |
| 29H | 分析文件名 | AL=分析控制标记DS:SI=要分析的字符串ES:DI=未打开的FCB首址 | AL=00未通配符01有通配符0FFH驱动器字母无效ES:DI=未打开的FCB |
| 2AH | 取系统日期 | | CX=年(1980-2099)DH=月,DL=日,AL=星期(0=星期日) |
| 2BH | 置系统日期 | CX=年,DH=月,DL=日 | AL=00成功,0FFH失败 |
| 2CH | 取系统时间 | | CH=时(0-23),CL=分,DH=秒,DL=百分之几秒 |
| 2DH | 置系统时间 | CX=时,分DX=秒,百分秒 | AL=00成功,0FFH失败 |
| 2EH | 设置/复位校验开关 | AL=0关闭,1打开 | |
| 2FH | 取磁盘传输地址DTA | | ES:BX=DTA首地址 |
| 30H | 取DOS版本 | | AL,AH=DOS主、次版本 |
| 31H | 结束并驻留 | AL=返回码,DX=内存大小 | |
| *32H | 取指定驱动器的DPB | | DS:BX=DPB首址 |
| 33H | 取或置Ctrl-Break标志 | AL=0:取,1:置,DL=标志 | DL=标志(取功能)0:关1:开 |
| *34H | 取DOS中断标志 | | ES:BX=DOS中断标志 |
| 35H | 取中断向量地址 | AL=中断号 | ES:BX=中断程序入口 |
| 36H | 取磁盘的自由空间 | DL=驱动器号(0=缺省,1=A | AX=FF驱动器无效其它每簇扇区数BX=自由簇数CX=每扇区字节数BX=文件区所占簇数 |
| *37H | 取/置参数分隔符取/置设备名许可标记 | AL=0:取分隔符,1:置分隔符,DL=分隔符2:取许可标记3:置许可标记,DL=许可标记 | DL=分隔符(功能0)DL=许可标记(功能2) |
| 38H | 取国家信息 | AL=0,DS:DX=缓冲区首址 | |
| 39H | 创建子目录 | DS:DX=路径字符串 | CF=0成功,1失败,AX=错误码 |
| 3AH | 删除子目录 | DS:DX=路径字符串 | CF=0成功,1失败,AX=错误码 |
| 3BH | 设置子目录 | DS:DX=路径字符串 | CF=0成功,1失败,AX=错误码 |
| 3CH | 创建文件 | DS:DX=带路径的文件名CX=属性1-只读2-隐蔽4-系统 | CF=0成功,AX=文件号CF=1失败,AX=错误码 |
| 3DH | 打开文件 | DS:DX=带路径的文件名AL=方式0-读1-写2-读写 | CF=0成功,AX=文件号CF=1失败,AX=错误码 |
| 3EH | 关闭文件 | BX=文件号 | CF=0成功CF=1失败,AX=错误码 |
| 3FH | 读文件或设备 | BX=文件号CX=字节数 | CF=0成功DX:AX=新的指针位置 |
| 40H | 写文件或设备 | DS:DX=缓冲区首址 | CF=1失败,AX=错误码 |
| 41H | 删除文件 | DS:DX=带路径的文件名 | CF=0成功,1失败,AX=错误码 |
| 42H | 移动文件指针 | AL=方式0-正向1-相对2-反向BX=文件号,CX:DX=移动的位移量 | CF=0成功,DX:AX=新的文件指针CF=1失败,AX=错误码 |
| 43H | 取/置文件属性 | AL=0:取1:置,CX=新属性DS:DX=带路径的文件名 | CX=属性(功能0)1-只读2-隐蔽4-系统20H-归档 |
| 44H | 设备输入/输出控制:设置/取得与打开设备的句柄相关联信息,或发送/接收控制字符串至设备句柄 | AL=0/1取/置设备信息2/3读/写设备控制通道4/5同功能2/36/7取输入/输出状态BX=句柄(功能0-3,6-7)BL=驱动器号(功能4-5)CX=字节数(功能2-5)DS:DX=缓冲区(功能2-5) | CF=0成功DX=设备信息(功能0)AL=状态(功能6/7)0未准备,1准备AX=传送的字节数(功能2-5) |
| 45H | 复制文件号(对于一个打开的文件返回一个新的文件号) | BX=文件号 | CF=0成功,AX=新文件号CF=1失败,AX=错误码 |
| 46H | 强行复制文件号 | BX=现存的文件号,CX=第2文件号 | CF=0成功,1失败AX=错误码 |
| 47H | 取当前目录 | DL=驱动器号DS:SI=缓冲区首址 | CF=0成功,1失败AX=错误码 |
| 48H | 分配内存 | BX=所需的内存节数 | CF=0成功,AX=分配的段数,CF=1失败,AX=错误码BX=最大可用块大小 |
| 49H | 释放内存 | ES=释放块的段值 | CF=1失败,AX=错误码 |
| 4AH | 修改分配内存 | ES=修改块的段值BX=新长度(以节为单位) | CF=1失败,AX=错误码BX=最大可用块大小 |
| 4BH | 装载程序运行程序 | AL=0装载并运行1获得执行信息3装载但不运行DS:DX=带路径的文件名ES:BX=装载用的参数块 | CF=1失败,AX=错误码 |
| 4CH | 带返回码的结束 | AL=进程返回码 | |
| 4DH | 取由31H/4CH带回的返回码 | | AL=进程返回码AH=类型码,0-正常结束1-由Ctrl-Break结束2-由严重设备错误而结束3-由调用31H而结束 |
| 4EH | 查找第一个匹配项 | DS:DX=带路径的文件名CX=属性 | CF=1失败,AX=错误码 |
| 4FH | 查找下一个匹配项 | | CF=1失败,AX=错误码 |
| *50H | 建立当前的PSP段地址 | BX=PSP段地址 | |
| *51H | 读当前的PSP段地址 | | BX=PSP段地址 |
| *52H | 取DOS系统数据区首址 | | ES:BX=DOS数据区首址 |
| *53H | 为块设备建立DPB | DS:SI=BPB,ES:DI=DPB | |
| 54H | 取校验开关设定值 | | AL=标志值(0:关,1:开) |
| *55H | 由当前PSP建立新PSP | DX=PSP段地址 | |
| 56H | 文件换名 | DS:DX=带路径的旧文件名ES:DI=带路径的新文件名 | CF=1失败,AX=错误码 |
| 57H | 取/置文件时间及日期 | AL=0/1取/置,BX=文件号CX=时间,DX=日期 | CF=0成功,CX=时间,DX=日期 |
打印字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20data segment
str db 'hello pangshu!$' ;$ 表示结尾标记
data ends
code segment
start:
mov ax,data
mov ds,ax
;业务逻辑代码
mov dx,offset str;获取别名对应数据的偏移地址
mov ah, 9h ;9h表示调用显存 从dx总读取偏移地址对应的数据
int 21h
;退出程序
mov ah, 4ch
int 21h
code ends
end start
除法指令div
格式:
1
div 寄存器 ;div bx 该寄存器表示除数
公式是被除数÷除数=商……余数
被除数需要预先存放在ax寄存器中,相除后商存放在ax寄存器中,余数存放在dx寄存器中
1 | mov ax,2000 ;定义被除数 |
因为ax和dx需要存放被除数和余数, 因此除数一般放在bx,cx,或者内存中
由于16位寄存器最多只能存放16位数据,假如被除数数值超过十六位,那么被除数低16位存放在ax寄存器中,高十六位存放在dx寄存器中
1 | mov dx,20 ;定义被除数高16位 |
乘法指令
1
2
3
4
5
6mov ax,100
mov bx,1000
mul bx ;相乘
mov ds:[0],ax ;将积低16位存放至内存中
mov ds:[2],dx ;将积高16位存放至内存中
段寄存器
- 数据和指令的区别
内存中存放的数据都是十六进制的数据,如果这些十六进制数据被标记为数据,那么cpu在读取的时候,读到多少就是多少,如果被标记为了指令,那么cpu会将这些十六进制转成指令进行执行
cpu只是个无情的计算机器,它无法自动区分数据和指令,标记需要我们告诉cpu
那么 如何给数据进行标记呢?
- 段寄存器的种类
1 | DS---->data segment |
都是用于存放段地址的
1 | 实际物理地址=段地址*16:偏移地址 10*10=100 |
DS寄存器用于存放数据的段地址,那么数据所对应的偏移地址可以使用bx寄存器进行存放,使用DS寄存操作的内存数据都被当成是纯数据,里面存的是什么,读出来的就是什么
1 | mov ax,3000H ;这行代码会转成16进制的数据存放到内存中 这些数据所对应的段地址默认放在ds寄存器中 ,使用ds进行读取的时候 读取的是该指令对应的16进制数据, 而不是被还原成了指令进行执行 |
CS寄存器用于存放指令所在的段地址,IP寄存器存放的是当前正在执行的指令所对应的偏移地址,所有使用CS:IP进行操作的内存数据都被当成是指令对待,读取的时候会将16进制数据转成对应的指令并执行
1 | mov bx,3333H ;假如这条这条指令数据所对应的物理地址为0710:0000 |
SS寄存器用于存放栈空间对应的段地址,所有被SS操作的内存空间都被当成栈空间进行对待,你想让哪部分内存空间当作栈空间,完全取决于开发者,sp寄存器存放栈空间偏移量,ss和sp配合使用
1 | mov ax,0710H |
ES寄存器一般用于DS的替补,DS被占用无法使用时,临时使用ES替代,用法和DS一致
栈空间的操作
栈段里面存放也是数据和数据段无异,只不过数据排列的方式不一样,正常的排列方式是数据从低地址往高地址进行偏移存放,读取数据也是从低到高,而栈则是写入数据从高到低进行偏移,读取数据从低地址到高地址
由于这个特性,所以我们在定义一块空间作为栈空间使用时,都会先往高地址偏移一段空间
栈存储特点:
一次读写两个字节的数据
数据高地址往低地址逆序偏移存放
栈空间的声明
前面提到过,使用ss寄存器进行标记的空间为栈空间
1
2
3
4
5
6
7
8
9
10
11data segment
db 0,0,0,0,0,0,0,0 ;定义数据相当于是开辟了一块未标记内存空间,这块空间可以当作是数据空间也可以栈或者指令空间,却决于该段地址是由哪个段寄存器进行管理的
data ends
code segment
start:
mov ax,data
mov ss,ax ;该空间被ss指向,因此被当作是栈空间 如果是被CS指向则被当成指令空间,里面存放的数据都会被当成指令进行执行
mov sp,8
code ends
end start往栈空间中写入数据
使用push指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14data segment
db 0,0,0,0,0,0,0,0
data ends
code segment
start:
mov ax,data
mov ss,ax
mov sp,8
mov ax,2000H
push ax ;一次写入两个字节的数据,如果使用al编译报错
code ends
end start从栈空间读取数据
使用pop指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16data segment
db 0,0,0,0,0,0,0,0
data ends
code segment
start:
mov ax,data
mov ss,ax
mov sp,8
mov ax,2000H
push ax
pop bx ;一次读取两个字节的数据,如果使用bl编译报错
code ends
end start思考:栈空间和数据段空间,里面存放的都是数据,那么是否可以使用SS来存放数据段的段地址呢,答案是可以的, 当SS充当数据段的时候,读写操作和DS一样,SS:[N],如果充当栈段的时候,读写操作时候时候pop和push,
由于pop和push默认以SS寄存器中的数据当做栈段地址,因此不能使用其他段寄存器充当栈段寄存器
操控显存输出字符串
前面咱们介绍过使用中断的方式输出字符串, 今天我们学习一种不使用中断的方式实现字符串的打印
在8086的内存地址结构中,B8000H~BFFFFH
这部分的内存区域为显存区域,一旦向这个地址空间写入数据,cpu会从0号偏移地址开始读取数据然后显示输出, (每写入一次数据就从0开始读取一次)
代码尝试:
1 | start: |
在这块区域中,每个字符固定占用两个字节的空间,也就是ds:[0]
和ds:[1]
存放一个字符的信息,前者存放字符具体的内容,后者存放字符对应的颜色
比如:
1 | start: |
字符颜色的设置规则:
1 | 0 0 0 0 0 0 0 0 ;用8个二进制位表示字符属性 |
从高往低数,第一个二进制位表示是否显示闪烁痕迹
1 | start: |
第234个二进制位表示字符背景颜色 分别代表:RGB,即red、green、blue
1 | start: |
第5个二进制位表示字符是否高亮
1 | start: |
第678个二进制位表示字符本身的颜色 分别代表:RGB,即red、green、blue
1 | start: |
由于cpu会从0号偏移地址开始读取数据然后显示输出,因此假如你直接在6号偏移地址写入字符数据, 那么前面三个数据会以占位形式存在
1 | start: |
字符串打印
1 | data segment |
借助字符不断刷新显示的特性,可用让字符动画显示
1 | ;让字符从左往右移动 |
屏幕默认显示80x25个字符,全屏显示106x38个字符,那么可以根据这个特性,让字符上下移动
1 | ;让字符从上往下移动 |
另外, 让字符斜着移动
1 | ;让让字符斜着移动 |
补充: 在8086中系统提供了一个显示服务(Video Service)中断供我们使用,使用10H这个中断码也可以打印带有颜色属性的字符串
1 | ;示例1: |
使用键盘输入控制字符移动
使用16号中断码
1 | ;使用键盘控制字符移动 |
内存读写的另外一种简便方法
直接使用别名+[偏移量]
的形式获取或者修改已经定义好的数据,比如:
1 | ;需求:将第二个字符串最后一个o替换成第一个字符串中的e |
mov ax ,str
中的str
相当于是str[0]
,这也是为什么咱们能够直接通过别名来获取第一个字符的原因
前面我们只介绍了b.str[0],表示读取一个字节的数据,另外一种w.str[0]表示读取一个字的数据
别名str中存放的数据量更db和dw有关, 如果是db那么别名默认取一个字节数据,如果dw 别名默认取两个字节数据, 可以使用b.和w.进行数据量的调节
一维数组的定义
数组本质上就是多个数据的集合,在内存中按照一定顺序排列,角标即为每个数据的偏移值,如果使用db
进行数据的定义那么两个数据之间的偏移值为1个字节,如果使用dw
指令定义数据,则偏移值为2个字节
1 | data segment |
数组的打印
1 | data segment |
外中断
外中断 (硬中断):由外部设备(比如网卡,或者硬盘 键盘 鼠标)引发的中断,比如当网卡收到数据包的时候,就会发出一个中断
中断屏蔽:
cpu监听到外部中断时,第一步需要先判断IF标志位的值,如果为1则执行,如果为0则屏蔽
为什么还有中断屏蔽这么一说? 因为有些重复的外部中断并不需要响应,或者cpu正在在执行非常重要的一段指令,这些指令不能中断,一旦中途调开可能会造成系统崩溃,那么在这种情况下需要先将IF标志位置为0,屏蔽所有可屏蔽的外部信号,等这段指令执行完后,再讲IF还原回1, 这个IF标志位相当于是一个监听外部信号的开关
为了方便IF标志位的修改,8086提供了相应的指令方面我们使用
1 | cli ;相当于IF=0 |
键盘中断
当我们按下键盘中的一个键,那么键盘会向cpu发出一个中断信号,cpu接收到信号后判断标志位IF是否为1,如果为1,则执行这个中断
当松开按键时,同样会发出一个中断信号,也就是说点击一个键实际产生了两个中断信号
以上这一系列过程涉及到几个问题:
1. 键盘是一个外部设备,电信号发出时,cpu怎么知道这个信号是来自键盘而不是鼠标的呢?
2. cpu又如何知道你按下的是哪一个键?
解答1:
cpu除了可以对寄存器 和内存进行数据读写之外,还可以读写端口的数据,电脑上一共有65535个端口,每个端口相当于是一个数据通道,当外部键盘借助USB接口接入电脑被驱动识别后,势必有一个端口与其相连进行数据通信, 在8086中这个端口号为60H
那么如何使用汇编读写端口中的数据呢?
1 | in al,60H ; 使用in指令 从60h这个端口读取一个字节到al寄存器中去 |
从端口读写数据必须使用ax或者al寄存器进行交互
解答2:
键盘每个键位所对应的字符都有与之对应的扫描码一一映射,不同厂商键盘硬件对应的扫描码可能不一致,它们最终都会转成相应的ascii码
键位扫描码参考表如下:
key | mark(Hex)按下 | break(Hex)松开 | 描述 |
---|---|---|---|
NumLock | 45 | c5 | break = mark + 0x80 |
/ | e0 35 | e0 b5 | 由 e0 引导出 extend scan code |
* | 37 | b7 | break = mark + 0x80 |
- | 4a | ca | 同上 |
7/Home | 47 | c7 | 同上 |
8/Up | 48 | c8 | 同上 |
9/PgUp | 49 | c9 | 同上 |
4/Left | 4b | cb | 同上 |
5 | 4c | cc | 同上 |
6/Right | 4d | cd | 同上 |
1/End | 4f | cf | 同上 |
2/Down | 50 | d0 | 同上 |
3/PgDn | 51 | d1 | 同上 |
0/Ins | 52 | d2 | 同上 |
./Del | 53 | d3 | 同上 |
+ | 4e | ce | 同上 |
Enter | e0 1c | e0 9c | |
Scroll Lock | 46 | c6 | 同上 |
Pause/Break | e1 1d 45 e1 9d c5 | * | 同上 |
Insert | e0 52 | e0 d2 | 同上 |
Home | e0 47 | e0 c7 | 同上 |
Page Up | e0 49 | e0 c9 | 同上 |
Delete | e0 53 | e0 d3 | 同上 |
End | e0 4f | e0 cf | 同上 |
Page Down | e0 51 | e0 d1 | 同上 |
left | e0 46 | e0 c6 | 同上 |
right | e0 4d | e0 cd | 同上 |
up | e0 48 | e0 c8 | 同上 |
down | e0 50 | e0 d0 | 同上 |
01 | 81 | scan code | |
F1 | 3b | bb | 同上 |
F2 | 3c | bc | 同上 |
F3 | 3d | bd | 同上 |
F4 | 3e | be | 同上 |
F5 | 3f | bf | 同上 |
F6 | 40 | c0 | 同上 |
F7 | 41 | c1 | 同上 |
F8 | 42 | c2 | 同上 |
F9 | 43 | c3 | 同上 |
F10 | 44 | c4 | 同上 |
F11 | 57 | d7 | 同上 |
F12 | 58 | d8 | 同上 |
~/· | 29 | a9 | |
0f | 8f | ||
3a | ba | ||
2a | aa | ||
!/1 | 02 | 82 | |
q | 10 | 90 | |
a | 1e | 9e | |
z | 2c | ac | |
@/2 | 03 | 83 | |
w | 11 | 91 | |
s | 1f | 9f | |
x | 2d | ad | |
#/3 | 04 | 84 | |
e | 12 | 12 | |
d | 20 | a0 | |
c | 2e | ae | |
$/4 | 05 | 85 | |
r | 13 | 93 | |
f | 21 | a1 | |
v | 2f | af | |
%/5 | 06 | 86 | |
t | 14 | 94 | |
g | 22 | a2 | |
b | 30 | b0 | |
^/6 | 07 | 87 | |
y | 15 | 95 | |
h | 23 | a3 | |
n | 31 | b1 | |
&/7 | 08 | 88 | |
u | 16 | 96 | |
j | 24 | a4 | |
m | 32 | b2 | |
*/8 | 09 | 89 | |
i | 17 | 97 | |
k | 25 | a5 | |
</, | 33 | b3 | |
(/9 | 0a | 8a | |
o | 18 | 98 | |
l | 26 | a6 | |
>/. | 34 | b4 | |
)/0 | 0b | 8b | |
p | 19 | 99 | |
:/; | 27 | a7 | |
?// | 35 | b5 | |
_/- | 0c | 8c | |
{/[ | 1a | 9a | |
“/‘ | 28 | a8 | |
36 | b6 | ||
+/= | 0d | 8d | |
}/] | 1b | 9b | |
1c | 9c | ||
1d | 9d | ||
|/\ | 2b | ab | |
38 | b8 | ||
0e | 8e | ||
39 | b9 |
如果是控制键ctrl shift 则将其转变成状态字节, 记录到0040:0017这个内存空间中,也就是说当我们按下控制键 这个位置的数据会发生相应的改变
磁盘读写
- 概念
磁盘构造:
1 | 一面=80个磁道 |
中断
使用13H号中断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26;读取磁盘中的数据到0:200H这个内存中
mov ax,0
mov es,ax
mov bx,200h
mov al,1 ;读取的扇区数
mov ch,0 ;磁道号
mov cl,1 ;扇区号
mov dl,0 ;驱动器号 软驱a, b
mov dh,2 ;面号
mov ah,2 ;2表示读取 3表示写入
int 13H
;将0:200H这个内存中数据写入软盘
mov ax,0
mov es,ax
mov bx,200h
mov al,1 ;读取的扇区数
mov ch,0 ;磁道号
mov cl,1 ;扇区号
mov dl,0 ;驱动器号 软驱a, b
mov dh,2 ;面号
mov ah,3 ;写入
int 13H
转移指令jmp ,jcxz和retf
如果我们想要实现以下效果:
1 | mov bx,3333H ;假如这条这条指令数据所对应的物理地址为0710:0000 |
修改方法:
调试器直接修改
使用
jmp
指令1
2
3
4
5
6
7
8
9jmp 0100h:8H
mov bx,3333H
mov ax,2000H
;如果在同一个段中 可以直接使用jmp+偏移地址的形式
jmp 8H
mov bx,3333H
mov ax,2000H或者使用标记
1
2
3
4jmp me
mov bx,3333H
me:
mov ax,2000H使用
jcxz
指令jcxz
(jmp cx zero):条件转移指令,功能和jmp一样,只是需要满足条件,也就是当cx寄存器中的值为0时,进行跳转1
2
3
4
5mov cx,0
jcxz me
mov bx,3333H
me:
mov ax,2000H使用
retf
指令retf
需要配合栈进行使用,当程序执行到retf
这条指令时,会连续从栈中pop
两次数据,第一次的数据赋值给CS
,第二次的数据赋值给IP,那么如果我们想要跳转到指定的指令,需要将该指令的段地址和偏移地址分别push
进栈中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18stack segment
db 128 dup(0)
stack ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,128
mov ax,0710H ;指定段地址
push ax
mov ax,0003H ;指定偏移地址
push ax
retf ;程序跳转到0710:0003H这个位置
code ends
end start
Call和Ret进阶
call
在执行时会先将下一条指令所对应的ip
地址入栈,然后修改ip
的值实现跳转, ret
指令执行的时候,将ip
地址pop
出来进行跳转
1 | call s ;标号里面存放的是ip偏移地址 如果写成call 3H 那么意思就是跳转到CS:0003h这个位置 |
call Far ptr
执行时会将下一条指令做对应的cs
和ip
都入栈,retf
指令执行的时候,将ip
和cs
值pop出来进行跳转
ret和call配套使用,retf和call Far ptr 配套使用
可以通过标号(函数名称)之间数值相减计算函数体代码所占用的内存空间大小
1 | code segment |
直接从内存中获取ip地址然后跳转
1 | call word ptr ds:[0] ;ds:[0]存放ip值 |
直接从内存中获取cs和ip地址然后跳转
1 | call dword ptr ds:[0] ;ds:[0]存放的是ip值, ds:[2]存放的是cs值 |
以上两种直接从内存中获取cs:ip的方式对于jmp指令同样有效
call
指令和jmp
指令的区别
jmp
指令仅仅只是修改了cs:ip
的值call
指令除了修改cs:ip
的值之外,还将下一条指令的ip
值入栈,方便ret
指令跳转调用
iret指令:
iret
指令执行,将ip
和cs
值pop出来进行跳转,同时还执行了popf
,相当于执行了以下三步操作
1 | pop ip |
内中断进阶
我们利用中断码段可以调用系统的功能,也就是被系统封装好的子程序
中断既然能够引导cpu
临时去执行子程序,那么势必是更改了cs:ip
的值,也就是在内存中存放了这个子程序的入口指令地址,通过int
关键字找出来并跳转。这里有两个先决条件,一个是子程序必须提前编写好存放在内存中,二是将入口地址存放在内存的某个位置
当程序执行到int
指令时,根据中断码计算出程序入口所在的物理地址,然后然后取出来赋值给cs:ip
那么怎么通过中断码计算呢?
比如 int 0h
会从0000:0000
这个地址开始找出四个字节数据,由高地址往低地址分别为段地址和偏移地址
由于每个中断码需要占用四个字节空间,因此int 2H
从0000:0004
开始找,以此类推
公式为:
1 | IP=中断码*4 |
配合咱们之前学的call
指令 int 9h
可以用以下指令替代:
1 | int 9h |
编写自定义中断
编写子程序
1
2
3child:
mov ax,3322H
retf
将子程序入口地址值存放到中断码对应的内存位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31code segment
start:
call write
mov ah,4cH
int 21h
;=======子程序======
child:
mov ax,3322H
retf
endd: nop
;=======子程序======
;=======将子程序入口地址写入内存======
write:
;子程序所在的段地址
mov ax,cs
;子程序所在的偏移地址
mov bx,child
mov cx,0000H
mov es,cx
mov es:[9h*4],bx
mov es:[9h*4+2],ax
ret
code ends
end start使用中断码调用子程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34code segment
start:
call write
;测试中断
int 9H
mov ah,4cH
int 21h
;=======子程序======
child:
mov ax,3322H
retf
endd: nop
;=======子程序======
;=======将子程序入口地址写入内存======
write:
;子程序所在的段地址
mov ax,cs
;子程序所在的偏移地址
mov bx,child
mov cx,0000H
mov es,cx
mov es:[9h*4],bx
mov es:[9h*4+2],ax
ret
code ends
end start
音乐播放
声音由震动产生,不同频率对应不同的声音,也就是音高
单个音高的声音不能叫音乐,只能叫音频,
而音乐是由不同时长的不同音高组合而成,比如100Hz的声音持续1秒紧接着200Hz的声音持续半秒,如此反复循环,那么在听感上就是一首音乐
在音乐演奏的世界里,为了表示音高,一般采用记谱的方式来代替频率,从而方便音乐家们交流,但是对于计算机而言,计算机不能识别简谱也不能识别五线谱
计算机的发声原理
电信号—–>驱动——->扬声器
示例代码
1 | data segment |
本文为作者原创 转载时请注明出处 谢谢
乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站