x86汇编语言之8086语法和指令集

x86汇编语法

  1. 注释
1
;我是注释
  1. 变量取值和赋值(传送指令)
1
2
3
4
5
6
;赋值
mov ax,2000H ;将十六进制2000赋值给十六位寄存器ax 相当于ax=2000H


;取值
mov bx,ax ;将ax中的值取出赋值给bx 相当于bx=ax

存放的数据大小根据使用的寄存器而定, 比如ax是16位寄存器,最大只能存放16位数据,也就是4位十六进制数据

十六进制数据不能以字母开头, 前面需加上0 否则编译报错

  1. 函数声明

结构如下:

1
2
3
函数名:
函数体
ret ;结尾标记

示例:

1
2
3
4
5
6
print:	;函数名
mov dx,offset str
mov ah, 9ch
int 21h

ret ;函数结尾标记
  1. 函数调用

x86架构中使用关键指令call

x86架构汇编示例:

1
2
3
4
5
6
7
8
9
10
11
		call print ;调用print函数

;退出程序
mov ah, 4ch
int 21h

print: ;函数名
mov dx,offset str;获取别名对应数据的偏移地址
mov ah, 9ch ;9h表示调用显存 从dx总读取偏移地址对应的数据
int 21h
ret
  1. 字符串的定义

    起因:如果直接将字符串赋值给通用寄存器,会出现以下两个问题:

    • 字符顺序是反着的
    • 最多只能存放两个字符
    • 无法获取到数据地址,不能堆字符串进行修改

    为了解决这个问题,需要使用另外一种方式,定义字符串

    首先:需要先在内存中申请一块空间,可以使用伪指令dbdw

    1
    2
    3
    4
    5
    db-->define byte  定义字节 读写一个数据,偏移量加1
    dw-->define word 定义字 读写一个数据,偏移量加2


    dd-->define double word 定义双字, 读写一个数据 偏移量加4
    • 示例

      1
      2
      db 'hello'  ;占用五个字节的内存空间
      dw 'hello' ;占用六个字节的内存空间 跟偏移量有关系

    如果定义数字,使用dw每个数字占用两个字节的空间, 字符串比较特殊,并不是每个字符占用两个字节,而是总长度必须是2的倍数

  1. 字符串的获取

    获取字符串的数据,首先要获取到数据所对应的内存地址

    那怎么获取已经定义好的地址呢?

    第一步尝试: 给数据添加别名

    1
    2
    3
    4
    5
    6
    7
    str db 'hello'

    start:
    mov bx,str ;别名中存放的是偏移地址


    end start

    别名中存放的是偏移地址,但是光有偏移地址还不行,还需要段地址, 段地址+偏移地址=实际物理地址,别名默认从ds寄存器中读取段地址, 但是我们并没有给ds寄存器赋过值, 这就导致我们无法获取正确的数据,因为我们不知道正确的段地址是多少?

    那字符串段地址从哪里获取呢?

    • 方法一: 直接从内存中找(仅限于调试,实际开发肯定不行)

    • 方法二:使用段进行包裹, 段能给我们提供一个段地址(正解)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      data segment
      str db 'hello'
      data ends
      ;使用段进行包裹, 可以借助段名称获取段地址
      start:
      mov ax,data
      mov ds,ax
      mov bx,str

      end start
  2. 对内存中的数据进行读写

    从内存中一次读取数据的多少,取决于寄存器的容器大小

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
      data segment
    str dw 'hello' ;如果定义多个数据 使用逗号进行分隔
    data ends

    start:
    mov ax,data
    mov ds,ax
    mov ax,str ;如果从内存中读取数据,是根据寄存器大小来读取,16位寄存器则一次性读取16位数据,8位al则一次性读取八位数据

    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
;报错1
data segment
str db 'hello ' ;改成dw则不报错
data ends
start:
mov ax,data
mov ds,ax
mov ax,str ;

end start

;报错2
data segment
str dw 'hello '
data ends

start:
mov ax,data
mov ds,ax
mov al,str ; ;改成mov ax,str 或者mov al,b.str 则不报错

end start



;报错3
mov ax,bl ;宽度不匹配 以上两个也是同样的问题

内存数据的读写是从低往高进行读写

上面使用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
2
3
4
5
; 以下是指定数据占用空间的大小 可以实现8位数据占用16个字节的空间的目的

mov byte ptr ds:[0],1 ;前面加上byte ptr关键字 表示往指定内存中写入字节型数据1

mov word ptr ds:[0],1 ;往指定内存中写入字型数据1 也就是在内存中占用两个字节空间


8. 字符串修改和替换

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
;需求1 : 将内存中he修改为wo

data segment
str dw 'he'
newstr dw 'wo'
data ends


start:
mov ax,data
mov ds,ax

mov ax,ds:str
mov ds:newstr,ax

end start



;需求2 : 将内存中hello 修改为wowowo

data segment
str dw 'hello '
newstr dw 'wowowo'
data ends


start:
mov ax,data
mov ds,ax

mov ax,ds:str
mov ds:newstr,ax

mov ax,ds:str+2
mov ds:newstr+2,ax

mov ax,ds:str+4
mov ds:newstr+4,ax

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

data segment
str dw 'he'

data ends

newData segment
newstr dw 'wo'
newData ends

code segment
start:
mov ax,data
mov ds,ax
mov ax,newData
mov es,ax

mov ax,ds:str
mov es:newstr,ax

code ends

end start
  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
    12
    add 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] 不允许这样写
  1. 中断

    顾名思义,程序运行到一半暂时断开,官方一点说就是,由于软件或者硬件信号,使得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. 打印字符串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    data 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
  1. 除法指令div

    格式:

    1
    div 寄存器  ;div bx   该寄存器表示除数

    公式是被除数÷除数=商……余数

被除数需要预先存放在ax寄存器中,相除后商存放在ax寄存器中,余数存放在dx寄存器中

1
2
3
4
5
6
mov ax,2000 ;定义被除数
mov bx,300 ;定义除数
div bx ; 进行除法运算

mov ds:[0],ax ;将商存放至内存中
mov ds:[2],dx ;将余数存放至内存中

因为ax和dx需要存放被除数和余数, 因此除数一般放在bx,cx,或者内存中

由于16位寄存器最多只能存放16位数据,假如被除数数值超过十六位,那么被除数低16位存放在ax寄存器中,高十六位存放在dx寄存器中

1
2
3
4
5
6
7
mov dx,20   ;定义被除数高16位
mov ax,2000 ;定义被除数低16位 就是1407D0H
mov bx,300 ;定义除数
div bx ; 进行除法运算

mov ds:[0],ax ;将商存放至内存中
mov ds:[2],dx ;将余数存放至内存中
  1. 乘法指令

    1
    2
    3
    4
    5
    6
    mov ax,100
    mov bx,1000
    mul bx ;相乘

    mov ds:[0],ax ;将积低16位存放至内存中
    mov ds:[2],dx ;将积高16位存放至内存中

段寄存器

  1. 数据和指令的区别

内存中存放的数据都是十六进制的数据,如果这些十六进制数据被标记为数据,那么cpu在读取的时候,读到多少就是多少,如果被标记为了指令,那么cpu会将这些十六进制转成指令进行执行

cpu只是个无情的计算机器,它无法自动区分数据和指令,标记需要我们告诉cpu

那么 如何给数据进行标记呢?

  1. 段寄存器的种类
1
2
3
4
DS---->data segment
SS---->stack segment
CS---->code segement
ES---->extra segement

都是用于存放段地址的

1
2
实际物理地址=段地址*16:偏移地址 10*10=100  
71001 = 0710*16:0001

DS寄存器用于存放数据的段地址,那么数据所对应的偏移地址可以使用bx寄存器进行存放,使用DS寄存操作的内存数据都被当成是纯数据,里面存的是什么,读出来的就是什么

1
mov ax,3000H  ;这行代码会转成16进制的数据存放到内存中 这些数据所对应的段地址默认放在ds寄存器中 ,使用ds进行读取的时候 读取的是该指令对应的16进制数据, 而不是被还原成了指令进行执行

CS寄存器用于存放指令所在的段地址,IP寄存器存放的是当前正在执行的指令所对应的偏移地址,所有使用CS:IP进行操作的内存数据都被当成是指令对待,读取的时候会将16进制数据转成对应的指令并执行

1
2
3
4
5
mov bx,3333H ;假如这条这条指令数据所对应的物理地址为0710:0000
mov ax,2000H ;假如这条这条指令数据所对应的物理地址为0710:0003


;如果我想跳过第一条指令 直接执行第二条指令 那么直接修改CS:IP的值为0710:0003即可

SS寄存器用于存放栈空间对应的段地址,所有被SS操作的内存空间都被当成栈空间进行对待,你想让哪部分内存空间当作栈空间,完全取决于开发者,sp寄存器存放栈空间偏移量,ss和sp配合使用

1
2
3
mov ax,0710H
mov ss,ax
mov sp,20 ;将0710:0000~0710:0014H 这20个字节的内存空间定义为栈空间

ES寄存器一般用于DS的替补,DS被占用无法使用时,临时使用ES替代,用法和DS一致

栈空间的操作

栈段里面存放也是数据和数据段无异,只不过数据排列的方式不一样,正常的排列方式是数据从低地址往高地址进行偏移存放,读取数据也是从低到高,而栈则是写入数据从高到低进行偏移,读取数据从低地址到高地址

由于这个特性,所以我们在定义一块空间作为栈空间使用时,都会先往高地址偏移一段空间

栈存储特点:

  • 一次读写两个字节的数据

  • 数据高地址往低地址逆序偏移存放

  1. 栈空间的声明

    前面提到过,使用ss寄存器进行标记的空间为栈空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    data 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
  2. 往栈空间中写入数据

    使用push指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    data 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
  3. 从栈空间读取数据

    使用pop指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    data 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
2
3
4
5
6
7
8
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl
end start

在这块区域中,每个字符固定占用两个字节的空间,也就是ds:[0]ds:[1]存放一个字符的信息,前者存放字符具体的内容,后者存放字符对应的颜色

比如:

1
2
3
4
5
6
7
8
9
10
11
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl

mov dl,00000100B ;让字符以红色显示
mov ds:[1],dl
end start

字符颜色的设置规则:

1
0 0 0 0 0 0 0 0  ;用8个二进制位表示字符属性

从高往低数,第一个二进制位表示是否显示闪烁痕迹

1
2
3
4
5
6
7
8
9
10
11
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl

mov dl,10000000B ;保留字符闪烁痕迹
mov ds:[1],dl
end start

第234个二进制位表示字符背景颜色 分别代表:RGB,即red、green、blue

1
2
3
4
5
6
7
8
9
10
11
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl

mov dl,01000000B ;背景颜色设为红色
mov ds:[1],dl
end start

第5个二进制位表示字符是否高亮

1
2
3
4
5
6
7
8
9
10
11
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl

mov dl,00001100B ;字符颜色设置为红色 并且高亮显示
mov ds:[1],dl
end start

第678个二进制位表示字符本身的颜色 分别代表:RGB,即red、green、blue

1
2
3
4
5
6
7
8
9
10
11
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl

mov dl,00000111B ;背景颜色设为白色 系统默认颜色是白色
mov ds:[1],dl
end start

由于cpu会从0号偏移地址开始读取数据然后显示输出,因此假如你直接在6号偏移地址写入字符数据, 那么前面三个数据会以占位形式存在

1
2
3
4
5
6
7
8
9
10
11
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl

mov dl,00000111B
mov ds:[6],dl ;输出结果为" a"
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
data segment
str db 'hello pangshu'
endstr db ''
data ends

code segment

start:

mov ax,data
mov ds,ax
mov ax,0B800H
mov es,ax

mov cx ,offset endstr-str
mov bx,0
mov si,0

print:
mov dl,ds:[si]
mov es:[bx],dl

mov dl,00000111B ;背景颜色设为白色 系统默认颜色是白色
mov es:[bx+1],dl
inc si
add bx,2
loop print

code ends
end start

借助字符不断刷新显示的特性,可用让字符动画显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;让字符从左往右移动
code segment

start:
mov ax,0B800H
mov es,ax

mov bx,0
mov cx,30
print:
mov es:[bx],' '
mov dl,'a'
mov es:[bx+2],dl
add bx,2
loop print

code ends
end start

屏幕默认显示80x25个字符,全屏显示106x38个字符,那么可以根据这个特性,让字符上下移动

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
;让字符从上往下移动
code segment

start:
mov ax,0B800H
mov es,ax

mov bx,0
mov cx,25
print:
mov es:[bx],'a'
mov dl,' '
mov es:[bx-160],dl
add bx,160 ;为什么是160而不是80 以内一个字符占两个字节的空间, 80个字符总共偏移了160
loop print

code ends
end start



;让字符从下往上移动
code segment

start:
mov ax,0B800H
mov es,ax

mov bx,160*24
mov cx,25
print:
mov es:[bx],'a'
mov dl,' '
mov es:[bx+160],dl
sub bx,160 ;为什么是160而不是80 以内一个字符占两个字节的空间, 80个字符总共偏移了160
loop print

code ends
end start

另外, 让字符斜着移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;让让字符斜着移动
code segment

start:
mov ax,0B800H
mov es,ax

mov bx,0
mov cx,25
print:
mov es:[bx],'a'
mov dl,' '
mov es:[bx-161],dl
add bx,161 ;向右斜加偏移量 向左斜减偏移量
loop print

code ends
end start

补充: 在8086中系统提供了一个显示服务(Video Service)中断供我们使用,使用10H这个中断码也可以打印带有颜色属性的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;示例1:
mov ah,2 ;放置光标
mov bh,0 ;第0页
mov dh,5 ;行号
mod dl,12 ;列号
int 10H


;示例2:
mov ah,9 ;在光标的位置显示字符
mov al,'a' ;字符
mov bl,11001010B ;颜色
mov bh,0 ;第0页
mov cx,3 ;重复显示3次
int 10H

使用键盘输入控制字符移动

使用16号中断码

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
;使用键盘控制字符移动
code segment

start:
mov ax,0B800H
mov es,ax
mov bx,0
mov cx,30

scan:
mov ah, 00H
int 16H
cmp al,61H ;判断两个值是否相等
jne scan2 ;jmp not equal 如果两者不相等 则跳转到scan2 否则往下执行
call left
jmp scan
scan2:
cmp al,64H
jne scan3
call right
jmp scan
scan3:
cmp al,77H
jne scan4
call top
jmp scan
scan4:

cmp al,73H
jne scan

call down
jmp scan

right:
mov es:[bx],' '
mov dl,'a'
mov es:[bx+2],dl
add bx,2
ret


left:
mov es:[bx],' '
mov dl,'a'
mov es:[bx-2],dl
sub bx,2
ret

top:
mov es:[bx],' '
mov dl,'a'
mov es:[bx-160],dl
sub bx,160
ret



down:
mov es:[bx],' '
mov dl,'a'
mov es:[bx+160],dl
add bx,160
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
;需求:将第二个字符串最后一个o替换成第一个字符串中的e
data segment
str dw 'hello '
newstr dw 'wowowo'
data ends


;以前的写法:
code segment
start:
mov ax,data
mov ds,ax
mov al,ds:b.str+1
mov ds:b.newstr+5,al
code ends
end start

;更加简便的写法:
code segment
start:
mov ax,data
mov ds,ax
mov al ,b.str[1] ;使用b.或者直接定义db数据 实现对单个字符进行控制
mov b.newstr[5],al
code ends
end start

mov ax ,str 中的str相当于是str[0],这也是为什么咱们能够直接通过别名来获取第一个字符的原因

前面我们只介绍了b.str[0],表示读取一个字节的数据,另外一种w.str[0]表示读取一个字的数据

别名str中存放的数据量更db和dw有关, 如果是db那么别名默认取一个字节数据,如果dw 别名默认取两个字节数据, 可以使用b.和w.进行数据量的调节

一维数组的定义

数组本质上就是多个数据的集合,在内存中按照一定顺序排列,角标即为每个数据的偏移值,如果使用db进行数据的定义那么两个数据之间的偏移值为1个字节,如果使用dw指令定义数据,则偏移值为2个字节

1
2
3
4
5
6
7
data segment
db 3 dup(1) ;批量定义三个连续的数据 每个数据的偏移量为1 在内存中的排列为 111
data ends

data segment
db 3 dup(1,2,3) ;批量定义三个连续的数据 每个数据的偏移量为1 在内存中的排列为 123123123
data ends

数组的打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data segment
array db 128 dup(0)
data ends

code segment
start:
mov ax,data
mov ds,ax

mov array[0],1
mov array[1],2
mov array[2],3


code ends
end start

外中断

外中断 (硬中断):由外部设备(比如网卡,或者硬盘 键盘 鼠标)引发的中断,比如当网卡收到数据包的时候,就会发出一个中断

中断屏蔽:

cpu监听到外部中断时,第一步需要先判断IF标志位的值,如果为1则执行,如果为0则屏蔽

为什么还有中断屏蔽这么一说? 因为有些重复的外部中断并不需要响应,或者cpu正在在执行非常重要的一段指令,这些指令不能中断,一旦中途调开可能会造成系统崩溃,那么在这种情况下需要先将IF标志位置为0,屏蔽所有可屏蔽的外部信号,等这段指令执行完后,再讲IF还原回1, 这个IF标志位相当于是一个监听外部信号的开关

为了方便IF标志位的修改,8086提供了相应的指令方面我们使用

1
2
3
cli  ;相当于IF=0

sti ;相当于IF=1

键盘中断

当我们按下键盘中的一个键,那么键盘会向cpu发出一个中断信号,cpu接收到信号后判断标志位IF是否为1,如果为1,则执行这个中断

当松开按键时,同样会发出一个中断信号,也就是说点击一个键实际产生了两个中断信号

以上这一系列过程涉及到几个问题:

​ 1. 键盘是一个外部设备,电信号发出时,cpu怎么知道这个信号是来自键盘而不是鼠标的呢?

​ 2. cpu又如何知道你按下的是哪一个键?

解答1:

​ cpu除了可以对寄存器 和内存进行数据读写之外,还可以读写端口的数据,电脑上一共有65535个端口,每个端口相当于是一个数据通道,当外部键盘借助USB接口接入电脑被驱动识别后,势必有一个端口与其相连进行数据通信, 在8086中这个端口号为60H

那么如何使用汇编读写端口中的数据呢?

1
2
3
in al,60H ; 使用in指令 从60h这个端口读取一个字节到al寄存器中去

out 60h,al ; 使用out指令 al寄存器中的数据写入60H端口

从端口读写数据必须使用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. 概念

磁盘构造:

1
2
3
4
5
一面=80个磁道

一个磁道=18个扇区

一个扇区=512byte
  1. 中断

    使用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
2
3
4
5
mov bx,3333H ;假如这条这条指令数据所对应的物理地址为0710:0000
mov ax,2000H ;假如这条这条指令数据所对应的物理地址为0710:0003


;如果我想跳过第一条指令 直接执行第二条指令 那么直接修改CS:IP的值为0710:0003即可

修改方法:

  • 调试器直接修改

  • 使用jmp指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    jmp 0100h:8H
    mov bx,3333H
    mov ax,2000H


    ;如果在同一个段中 可以直接使用jmp+偏移地址的形式
    jmp 8H
    mov bx,3333H
    mov ax,2000H

    或者使用标记

    1
    2
    3
    4
    jmp me
    mov bx,3333H
    me:
    mov ax,2000H
  • 使用jcxz指令

    jcxz (jmp cx zero):条件转移指令,功能和jmp一样,只是需要满足条件,也就是当cx寄存器中的值为0时,进行跳转

    1
    2
    3
    4
    5
    mov 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
    18
    stack 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 执行时会将下一条指令做对应的csip都入栈,retf指令执行的时候,将ipcs值pop出来进行跳转

ret和call配套使用,retf和call Far ptr 配套使用

可以通过标号(函数名称)之间数值相减计算函数体代码所占用的内存空间大小

1
2
3
4
5
6
7
8
9
10
11
12
code segment          
mov ax,func1
mov bx, func2
sub bx,ax ;计算函数func1占用的内存空间大小

func1:
mov ax,3333H
ret
func2:
mov bx,4444H
ret
code ends

直接从内存中获取ip地址然后跳转

1
call word ptr ds:[0] ;ds:[0]存放ip值

直接从内存中获取cs和ip地址然后跳转

1
2
3
call dword ptr ds:[0] ;ds:[0]存放的是ip值, ds:[2]存放的是cs值

;这种方式同样会将cs和ip入栈 可以配合retf使用

以上两种直接从内存中获取cs:ip的方式对于jmp指令同样有效

call指令和jmp指令的区别

  • jmp指令仅仅只是修改了cs:ip的值
  • call指令除了修改cs:ip的值之外,还将下一条指令的ip值入栈,方便ret指令跳转调用

iret指令:

iret指令执行,将ipcs值pop出来进行跳转,同时还执行了popf,相当于执行了以下三步操作

1
2
3
pop ip
pop cs
popf

内中断进阶

我们利用中断码段可以调用系统的功能,也就是被系统封装好的子程序

中断既然能够引导cpu临时去执行子程序,那么势必是更改了cs:ip的值,也就是在内存中存放了这个子程序的入口指令地址,通过int关键字找出来并跳转。这里有两个先决条件,一个是子程序必须提前编写好存放在内存中,二是将入口地址存放在内存的某个位置

当程序执行到int指令时,根据中断码计算出程序入口所在的物理地址,然后然后取出来赋值给cs:ip

那么怎么通过中断码计算呢?

比如 int 0h 会从0000:0000这个地址开始找出四个字节数据,由高地址往低地址分别为段地址和偏移地址

由于每个中断码需要占用四个字节空间,因此int 2H0000:0004开始找,以此类推

公式为:

1
2
IP=中断码*4
CS=中断码*4+2

配合咱们之前学的call指令 int 9h 可以用以下指令替代:

1
2
3
4
5
6
int 9h 

;相当于一下三行代码
mov ax,0000H
mov ds,ax
call dword ptr ds:[9h*4]

编写自定义中断

  1. 编写子程序

    1
    2
    3
    child:
    mov ax,3322H
    retf
  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
    code 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
  2. 使用中断码调用子程序

    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
    code 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
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
data segment
;音高
freq dw 262,262,262,196
dw 330,330,330,262
dw 262,330,392,392
dw 349,330,294
dw 294,330,349,349
dw 330,294,330,262
dw 262,330,294,196
dw 247,294,262,-1
;时长
time dw 3 dup(12,12,25,25),12,12,50
dw 3 dup(12,12,25,25),12,12,50
data ends

stack segment
db 100H dup(0)
stack ends

code segment
start:
;主程序
mov ax,stack
mov ss,ax
mov sp,100H
mov ax,data
mov ds,ax

lea si,freq
lea di,time

play:
mov dx,[si]
cmp dx,-1
je end_play
call sound
add si,2
add di,2
jmp play

end_play:
mov ah,4cH
int 21H
sound:
push ax
push dx
push cx
;定时器的设置
mov al,0b6h
out 43H,al
mov dx,12H
mov ax,34dcH
div word ptr[si]
out 42h,al
mov al,ah
out 42h,al
;设置8255芯片,控制扬声器开关
in al,61h
mov ah,al
or al ,3
out 61h,al
;延时一定的时长
mov dx,[di]
wait1:
mov cx,28000
delay:
nop
loop delay
dec dx
jnz wait1
;恢复扬声器端口原值
mov al,ah
out 61h,al
pop cx
pop dx
pop ax
ret
code ends
end start

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

0%