0%

今天继续学习汇编语言

转移指令的学习

jmp指令

使用jmp指令需要给出两种信息:

  • 转移的目标地址
  • 转移的距离(段间转移,段内转移,段内近转移)

根据位移进行的jmp指令

先对两种jmp进行介绍:

1
jmp short 标号

功能为:(IP)=(IP)+ 八位位移(short指明)

  • 8位指令=标号处的地址-jmp指令后的第一个字节的地址
  • 八位位移的范围是-128~127
  • 位移值是在编译程序的过程中计算出来的
1
jmp near ptr 标号

功能为:(IP)=(IP) + 十六位位移(near ptr)

  • 16位指令=标号处的地址-jmp指令后的第一个字节的地址
  • 十六位位移的范围是-32768~32767
  • 位移值是在编译程序的过程中计算出来的

我们可以通过下面的图片理解位移的计算过程:

image.png

指定转移目的地址的jmp指令

1
jmp far ptr 标号

far ptr指明了指令用标号的段地址和偏移地址修改CS和IP

其机器码表现形式为指定目的地址

转移地址在寄存器中的jmp指令

1
jmp (16位reg)

功能:(IP)= (16位reg)

转移地址在内存中的jmp指令

1
jmp word ptr 内存单元地址(段内转移)

功能:从内存单元地址处开始存放一个字,是转移的目的偏移地址

1
jmp dword ptr 内存单元地址(段间转移)

功能:从内存单元地址处开始存放着两个字,高地址存放的字是转移的目的段地址,低地址是转移的目的偏移地址

  • (CS) = (内存单元地址+2)
  • (IP) = (内存单元地址)

jcxz指令

有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址,对IP修改范围为-128~127

1
jcxz 标号

功能:当(cx)= 0 时,转移到标号处执行

  • 8位位移=标号处的地址-jcxz指令后的第一个字节的地址
  • 八位位移的范围是-128~127
  • 位移值是在编译程序的过程中计算出来的

可以理解为

1
if((cx)==0) jmp short 标号;

loop 指令

所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址

我们在之前学习过,可以理解为

1
2
(cx)--;
if((cx)!=0)jmp short 标号;

根据位移进行转移的意义

因为程序段在不同的机器中内存情况并不一样,如果指定内存地址进行跳转会发生错误

但如果根据位移进行索引,便可以准确的找到位置

地址间的相对关系是不会改变的

当然如果位移距离超出范围,会造成编译错误

offset

我们可以通过

1
offset 标号

取到标号的偏移地址

实验九

参考链接:王爽《汇编语言》(第三版)实验9解析 - nojacky - 博客园 (cnblogs.com)

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
assume cs:code
data segment
db '!!!HelloWorld!!!'
db 2,36,113
data ends
stack segment
db 16 dup (0)
stack ends
code segment
start:
;指向data单元
mov ax,data
mov ds,ax
;指向显存区域
mov ax,0B800H
mov es,ax
;设置栈段
mov ax,stack
mov ss,ax
mov sp,16
;初始化
mov bx,780H;这个是第十二行的位置
mov si,16

mov cx,3
s:
mov ah,ds:[si]
push cx
push si

mov cx,16
mov si,64;这个确保居中显示
mov di,0

s0:
mov al,ds:[di]
mov es:[si+bx],al
mov es:[si+bx+1],ah

add si,2
add di,1

loop s0

pop si
pop cx

add si,16
add bx,0A0H
loop s

mov ax,4c00h
int 21h

code ends
end start

效果图

4ad49d42c593958a5834364a12d92ea8.png

CALL和RET指令

ret与retf

ret指令用栈中的数据,修改IP的内容,从而实现近转移 retf指令用战中的数据,修改CS和IP的内容,从而实现远转移

CPU执行ret:

1
2
(IP) = ((ss)*16 + (sp))
(sp) = (sp) + 2 //pop IP

CPU执行retf:

1
2
3
4
(IP) = ((ss)*16 + (sp))
(sp) = (sp) + 2 //pop IP
(CS) = ((ss)*16) + (sp)
(sp) = (sp) + 2 //pop CS

call指令

总结一下就是:

  • 将当前的IP或CS和IP压入栈中
  • 转移

依据位移进行的call指令

1
call 标号;将当前的IP压入栈中,转到标号处执行指令

执行过程如下:

1
2
3
(sp) = (sp) -2
((ss)*16 + (sp)) = (IP)
(IP) = (IP) + 16位位移

位移的计算同上

转移到目的地址在call指令中

1
call far ptr 标号

相当于进行

1
2
3
push CS
push IP
jmp far ptr 标号

转移地址在寄存器中的call指令

1
call (16位reg)

相当于进行

1
2
push IP
jmp (16位reg)

转移地址在内存中的call指令

(1)单字节索引

1
call word ptr 内存单元地址

相当于进行:

1
2
push IP
jmp word ptr 内存单元地址

(2)双字节索引

1
call dword ptr 内存单元地址

相当于进行

1
2
3
push CS 
push IP
jmp dword ptr 内存单元地址

使用

在学会ret和call的用法之后,我们可以使用下面的框架来模拟函数的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
assume cs:code
code segment
main:
...
call sub1
...
mov ax,4c00h
int 21h
sub1:
...
call sub2
...
ret
sub2:
...
ret
code ends
end main

mul指令

使用mul指令时,我们需要注意以下几点:

  • 两个相乘的数要么都是八位,要么都是16位。如果是八位,则一个放在AL中,另一个在8位的reg或内存字节单元中;如果是16位,则一个在AX中,一个在16位的reg或者内存字单元中
  • 如果是八位乘法,结果放在AX中;如果是十六位乘法,结果高位放在DX中,低位放在AX中
1
mul reg/内存单元

实验十:编写子程序

显示字符串

image.png
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
assume cs:code
data segment
db 'Welcome to masm!',0
data ends

code segment
start:
;指向显存
mov ax,0B800H
mov es,ax

mov dh,8
mov dl,3
mov cl,2
mov ax,data
mov ds,ax
mov si,0
call show_str

mov ax,4c00h
int 21h

show_str:
mov bx,8*160
mov di,3*2
mov ah,cl
mov cx,16
s:
mov al,ds:[si]
mov es:[bx+di],al
mov es:[bx+di+1],ah
inc si
add di,2
loop s
ret

code ends
end start

解决除法溢出问题

tips:

公式 X/N = int(H/N)*65536 + [rem(H/N)*65536+L]/N

X:被除数,范围[0,FFFFFFFF] N:除数,范围[0,FFFF] H:X的高16位,范围[0,FFFF] L:X的低16位,范围[0,FFFF] int():取商 rem():取余

参考链接:汇编语言(王爽第三版)实验10:编写子程序 - 筑基2017 - 博客园 (cnblogs.com)

image.png
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
assume cs:code
stack segment
dw 0,0,0,0,0,0,0,0
stack ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,16

mov ax,4240H
mov dx,000FH
mov cx,0AH
call divdw
mov ax,4c00h
int 21h

divdw:
push ax
mov ax,dx
mov dx,0
div cx
mov bx,ax

pop ax
div cx
mov cx,dx
mov dx,bx
ret

code ends
end start

数值显示

image.png

暂时写不出来

自从大学以来我一直在思考一个问题

是学技术重要还是卷绩点重要?

从学校的角度出发,因为是一个中下游的211,有一定的保研资格但是不多,所以给人一种有希望的感觉。但是僧多肉少,很多人为了保研无所不用其极,或者说是要舍弃很多的东西。对我而言,可能最让我纠结的便是在卷绩点上面所花费的时间。

从我个人的角度出发,我十分喜欢自己正在学习的东西,非常非常喜欢。我等了很久才学到我想学的东西,我很早开始就想学计算机,但是因为教育观念有一定的落后,所以我的家庭和学校并不支持我的想法。到了大学,我有很多时间去做我想做的事情,我把几乎所有的时间投入在技术的学习里面,难以想象的热爱。可是因此,也为我带来了很多烦恼,对我而言,研究生的学历是十分必要的。因为我并不满足于我现在的学历,而且专业需求的是高学历人才,所以要么保研要么考研。就现在来说,肯定是不考虑考研的,因为有保研的名额,而且考研有一定的难度,所以我还是更加倾向于保研。那么问题来了,我并没有时间去学习保研内容相关的,我的绩点岌岌可危。

就目前而言,最为理性的选择无疑是将重心转移到保研上。但是我实在不愿意做出这样的选择,12年以来的应试教育,为什么到了大学就没有选择的权力。我可以做出我想要的选择,可是环境并不会给我想要的结果,这是我内心一直以来的纠结与矛盾。我尝试过两手抓同步走,但是到了最后我才发现自己也是一个很普通的学生。这种心比天高的做法并不适合我。在大学生活的一个学期里,我已经慢慢感受到 了,很多身不由己的瞬间。不能太过于任性,要理性的去判断。

其实当我开始写这段话的时候我的心里已经有了答案,也许我确实应该花更多的时间在学习上面。这是对现实的妥协吧,但是归根到底这也是一种能力不足的体现,也许我更应该锻炼自己。真正优秀的人不会被这些问题困扰,我的心思过于分散,这就是如今局面的原因。反思过后,便是行动。也许人生就是在一次次的妥协里成长。我的高考成绩便是任性的后果,很多事情不想再经历第二遍,不能”只做自己想做”的事情,也许我也应该去做好一些不得不做的事情。

也许以后,慢慢成长,能够放下杂念,也有机会让学习成绩与技术能力并驾齐驱,但是对于现在的我而言却言之过早。不知道向谁倾诉我的烦恼,记录于此,警醒自己。

DEBUG的用法

在 DOSBox 中使用 DEBUG 工具时,可以使用以下命令及其用法,这些命令主要用于调试汇编语言程序、查看和修改寄存器与内存内容、执行代码等。以下是 DEBUG 的所有常用命令及其详细用法:

1. 进入和退出 DEBUG

  • 进入 DEBUG:在 DOSBox 中输入 debug 命令。
  • 退出 DEBUG:使用 q 命令退出 DEBUG 并返回到 DOSBox 命令行。

2. 寄存器操作

  • 查看所有寄存器:输入 r,查看所有寄存器的当前值。
  • 查看和修改单个寄存器:输入 r <寄存器名>,例如 r ax,查看 AX 寄存器的值。输入新值后按回车即可修改。

3. 内存操作

  • 查看内存内容
    • d [起始地址]:从指定地址开始查看内存内容。
    • d [起始地址] [结束地址]:查看指定范围内的内存内容。
  • 修改内存内容
    • e [内存地址]:修改指定地址的内存内容。
    • e [内存地址] '文本':直接输入文本内容。
  • 填充内存内容
    • f [起始地址] [结束地址] [值1] [值2]...:用指定值填充内存区域。

4. 汇编指令操作

  • 输入汇编指令
    • a [地址]:从指定地址开始输入汇编指令。输入完成后按回车退出。
  • 反汇编指令
    • u [地址]:从指定地址开始反汇编指令。
    • u [段地址:偏移地址]:指定段地址和偏移地址进行反汇编。

5. 程序执行

  • 单步执行
    • t:执行当前指令并进入下一步。
    • t [地址]:从指定地址开始单步执行。
  • 连续执行
    • g:从当前地址开始执行程序。
    • g=[地址]:从指定地址开始执行程序,并设置断点。
  • 运行程序至结束:使用 p 命令。

6. 其他功能

  • 计算偏移量
    • h value1 value2:计算两个十六进制值的和。
  • 保存程序到文件
    • p [文件名] [地址]:将内存中的程序保存到文件。

汇编程序中的注意点

关于下面四个指令,在汇编程序中有不同的含义:

1
2
3
4
mov al,[0]
mov al,ds:[0]
mov al,[bx]
mov al,ds:[bx]
  1. (al)= 0

  2. (al)= ((ds)*16 + 0)

  3. (al)= ((ds)*16 + bx)

  4. (al)= ((ds)*16 + bx)

所以总结得到:

  • 如果在”[ ]“里用一个常量idata直接给出内存单元的偏移地址,就要在”[ ]“前面显式的给出段地址所在的寄存器,否则会被解释为常量
  • 如果在”[ ]“里面使用寄存器,则默认段地址为ds,可以不用显式的表现

汇编程序的入口

当我们在代码段设置数据时会遇到一个问题,就是程序的入口被设置在代码段

但是这会导致程序无法执行,因为你定义的字节被反编译后可能是未被定义的或者意义不明的指令

所以我们在运行程序时需要手动调节IP值,那么有什么更好的办法呢?

我们可以在汇编代码时事先定义程序的入口:

1
2
3
4
5
6
7
8
9
10
11
assume cs:code
code segment
...
;数据
...
start:
...
;代码
...
code ends
end start

通过这种格式我们可以在start处定义函数的入口,程序将从此开始运行

那么换个角度思考,我们既然可以指定程序的入口,那么我们就可以对程序进行分段

我们将数据,代码,栈分别放入不同的段中

比如下面这个程序

1
2
3
4
5
6
7
8
9
10
11
assume cs:code,ds:data,ss:stack
data segment
;字型数据
data ends
stack segment
;定于栈空间
stack ends
code segment
start:;代码
code ends
end start

注意要分清什么是伪指令,什么是汇编指令。CPU如何处理我们定义的段空间,完全是靠程序中具体的汇编指令,这里的伪指令只是将其进行了抽象,这样的分段定义,实际上是基于段寄存器的使用。并非我们想象的直接对内存进行分段

汇编程序转换大小写

首先我们要知道ASCII字符中大小写字母之间有什么样的联系

  • 大写字母的十六进制数值比小写字母的十六进制数值小 20H
  • 大写字母和小写字母的区别在于小写字母的第五位是1(因为位数从0开始计算)

循环遍历

所以我们可以写出以下程序:

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
assume cs:code,ds:data
data segment
db 'BaSiC'
db 'iNfOrMaTiOn'
data ends

code segment
start:mov ax,data
mov ds,ax
mov bx,0
mov cx,5

s:mov al,[bx]
and al,11011111B ;小写
mov [bx],al
inc bx
loop s

mov bx,5
mov cx,11

s0:mov al,[bx]
or al,00100000B ;大写
mov [bx],al
inc bx
loop s0

mov ax,4c00h
int 21h
code ends
end start

可以看到我们使用了两个循环,分别将其转换为大小写

数组处理

我们可以用’[bx+idata]’的形式来模拟数组的行为

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
assume cs:code,ds:data
data segment
db 'BaSiC'
db 'MinIX'
data ends
code segment
start:mov ax,data
mov ds,ax
mov bx,0

mov cx,5

s: mov al,0[bx]
and al,11011111B
mov 0[bx],al
mov al,5[bx]
or al,00100000B
mov 5[bx],al
inc bx
loop s

mov ax,4c00h
int 21h
code ends
end start

这里面的 0[bx]5[bx]可以分别理解为C语言中的 a[]b[],只不过这里体现的是偏移地址

在这里我们有几种等价的表达方式:

1
[bx + idata] = idata[bx] = [bx].idata

我们可以用数组的思想去理解

汇编内存寻址的进一步理解

我们依次进行深入:

DS,SS,CS

作为一个段地址,往往用来划分一定的内存区域存放特定的数据

我们可以理解成C语言中,申请了一段空间(空间)

[BX]寄存器间接寻址

通过修改寄存器BX中的值,我们可以进一步索引到段中的某一部分内存的起点

我们可以理解成在这一片空间中划分了一部分作为数组(一维数组)

[BX + idata]寄存器相对寻址

我们以BX确定在段空间的位置后,我们可以用常量去查询指定内存的数值

此时我们可以把常量idata理解成数组的下标(二维数组)

我们可以用以下形式表达:

1
[bx + idata] = idata[bx] = [bx].idata

[BX + SI/DI + idata]相对基址变址寻址

我们先用BX确定一部分空间,再用SI/DI中的地址确定在这段空间中的位置,然后用常量去查询指定内存中的数值

此时我们可以把SI/DI中的数值理解成二维数组的首地址,而常量作为数组下标进行索引(三维数组)

当没有常量时我们这样表达:(基址变址寻址)

1
[bx + si/di] = [bx][si/di]

有常量时我们这样表达:

1
[bx + si/di + idata] = idata[bx][si/di] = [bx].idata[si/di] = [bx][si/di].idata

SI/DI

这两个寄存器是8086CPU中与bx功能相近的寄存器,这两个寄存器不能被分成两个八位的寄存器来使用

通过这些各种各样的表达方式,我们可以根据自己的需求进行各种各样的寻址

新的汇编指令

div指令

使用这个指令时我们需要注意以下几点:

  • 除数:有8位和16位两种,在一个reg或者内存单元中
  • 被除数:默认放在AX或DX和AX中,被除数的位数是除数的两倍,如果被除数是32位,那么DX存放高十六位,AX存放低十六位
  • 结果:如果除数为8位,则AL存储除数操作的商,AH存储除数操作的余数;如果为16位,那么AX存储商,DX存储余数

伪指令 dd

db,dw,dd分别代表三种不同的定义类型

1
2
3
db			;定义字节(byte)类型
dw ;定义字(word)类型
dd ;定义double类型

dup

用来重复定义同一类型的数据

1
2
3
db 重复的次数 dup (重复的字节类型)
dw 重复的次数 dup (重复的字类型)
dd 重复的次数 dup (重复的双字类型)

比如定义200个字类型

1
dw 200 dup (0)

实验七

答案如下:

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
assume cs:code
data segment
db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
db '1993','1994','1995'
;0-84 0-54H

dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
;85-168 55H-A9H

dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
dw 11542,14430,15257,17800
;169-210 AAH-D4H

data ends
table segment
db 21 dup ('year summ ne ?? ')
table ends
code segment
start: mov ax,data
mov ds,ax
mov ax,table
mov es,ax

mov bp,0
mov si,0
mov di,0
mov cx,21

s: mov ax,ds:[si]
mov es:[di],ax
mov ax,ds:[si+2]
mov es:[di+2],ax

mov ax,ds:[84+si]
mov es:[di+5],ax
mov ax,ds:[84+si+2]
mov es:[di+5+2],ax

mov ax,ds:[168+bp]
mov es:[di+10],ax

mov ax,ds:[84+si]
mov dx,ds:[84+si+2]
div word ptr ds:[168+bp]
mov es:[di+13],ax

add si,4
add di,16
add bp,2

loop s

mov ax,4c00h
int 21h

code ends
end start



为了进一步的使用对内存单元进行灵活的操作,所以使用[BX]和loop指令对其进行操作

[BX]和loop指令

在开始学习之前,需要对一些符号进行讲解

  1. [bx]和内存单元的描述

    要完整的描述一个内存单元,需要两种信息:

    • 内存单元的地址
    • 内存单元的长度(类型)

    这里[bx]表示一个内存单元,段地址默认为ds,偏移地址存储在bx中

  2. loop

    loop在英文中有循环的含义,所以这个指令肯定和循环有关,我们在后面进行详细的说明

  3. 我们定义的描述性的符号:“( )”

    为了描述上的简洁我们用一个描述性的符号”()“来表示一个寄存器或者一个内存单元中的值

    现在我们可以把[bx]的物理地址表示为((ds)*16+(bx))

    “(X)”所表示的数据有两种类型:1)字节;2)字 数据类型由寄存器名称或者具体的运算决定

  4. 约定符号idata表示常量

    之前我们说在”[…]“里用一个常量0表示内存单元的偏移地址,现在我们可以把所有常量都作为[idata]

[BX]

指令功能:

1
mov ax,[bx]

bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将SA:EA处的数据送入ax中,即(ax) = ((ds)*16+(bx))

1
mov [bx],ax

bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将ax处的数据送入SA:EA处,即((ds)*16+(bx)) = (ax)

Loop指令

loop指令的格式是:loop 标号,CPU执行loop指令的时候,需要进行两步操作:

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

在此我们可以看出cx的值影响了loop指令的执行结果。通常我们用loop来实现循环功能,cx中存放循环次数

这里我们通过一个程序引出关于loop的使用:

1
2
3
4
5
6
7
8
9
10
11
12
assume cs:code
code segment
mov ax,2

mov cx,11
s: add ax,ax
loop s

mov ax,4c00h
int 21h
code ends
end

我们注意到这里使用了标号”s” ,在汇编语言中标号代表一个地址,它实际上标识了一个地址

我突然发现 这样一点一点敲很慢,所以我打算先学完再总结下来

反正这也不是教程 哈哈

今天晚上的学习任务是用汇编写一个可执行程序,接下来开始吧

第一个程序

一个源程序从写出到执行的过程

  1. 编写汇编原程序

    产生一个存储源程序的文本文件

  2. 对源程序进行编译连接

    先使用汇编语言编译程序对源程序进行编译,产生目标文件;再用连接程序对目标程序进行连接,生成可在系统中运行的可执行程序

    可执行程序包含两部分内容:

    • 程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
    • 相关的描述信息(比如,程序有多大,要占用多少内存空间)
  3. 执行可执行文件中的程序

    在操作系统中,执行可执行文件中的程序

在这些步骤中,操作系统可以根据可执行文件中的描述信息,将可执行文件中的机器码和数据加载进入内存,并进行相关的初始化(比如设置CS:IP指向的第一条执行的指令),然后由CPU执行

源程序

我们以一段汇编语言源程序为例

1
2
3
4
5
6
7
8
9
10
11
12
13
assume cs:codesg
codesg segment

mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax

mov ax,4c00H
int 21H

codesg ends
end

伪指令

在汇编语言源程序中包含两种指令。一是汇编指令,二是伪指令。

汇编指令是有对应的机器码的指令,可以被编译为机器指令,最终由CPU执行

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

我们先对这段程序中的三处伪代码进行说明:

1
2
3
XXX segment
...
XXX ends

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

其功能是定义一个段,segment 用来定义一个段的开始,ends用来定义一个断的结束

通常一个源程序是由多个段组成的,一个程序中所有要被处理的信息:指令,数据,栈被划分到了不同的段中

1
end

end是汇编程序结束的标记,编译器在进行编译时,如果碰到了伪指令end就结束对源程序的编译

1
assume cs:codesg

这条伪指令的含义为”假设”。它假设某一段寄存器和程序中的某一个用 segments...ends定义的段相关联

这一段程序的含义便是将 codesg段与 cs段寄存器相关联

程序返回

当一个程序结束后,将CPU的控制权交还给使它得以运行的程序,我们称这个过程为:程序返回

这两条指令实现的功能便是程序返回:

1
2
mov ax,4c00H
int 21H

我们暂时无法理解这两句的含义,不必深究

编辑源程序

我们在edit程序中编辑程序

image.png

我们将其保存至C盘中为1.asm

编译

我们使用masm对其进行编译

image.png

我们一一分析这些信息的作用:

  1. [.ASM]:提示我们默认的文件拓展名是asm,当我们输入名称XXX便在当前目录下调用XXX.asm(如果要用其他拓展名则需输入完全)
  2. [1.OBJ]:提示我们我们生成目标文件为1.obj ,我们可以在后面指定生成的路径,也可以用Enter跳过,使用当前文件夹
  3. [NUL.LST]:提示输入列表名称,这个文件是编译器翻译源程序的过程中的中间结果,使用Enter跳过
  4. [NUL.CRF]:提示输入交叉引用文件的名称,也是中间产物,跳过
  5. 当出现下面的标志后,代表编译成功结束

连接

我们在得到目标文件后,需要对目标文件进行连接,从而得到可执行程序

image.png

我们接着分析这些信息:

  1. [.OBJ]:提示我们默认的文件拓展名是obj,当我们输入名称XXX便在当前目录下调用XXX.obj(如果要用其他拓展名则需输入完全)
  2. [1.EXE]:提示我们我们生成目标文件为1.exe,我们可以在后面指定生成的路径,也可以用Enter跳过,使用当前文件夹
  3. [NUL.MAP]:提示输入映像文件的名称,这个文件是目标文件生成可执行程序的中间结果,使用Enter跳过
  4. [.LIB]:提示输入库文件的名称,库文件里面包含了一些可以调用的子程序,如果调用了某一个库文件的子程序,就需要在连接时,将这个库文件和目标文件连接在一起生成可执行程序,这里我们跳过
  5. 最后显示出现了“没有栈段”的错误,我们直接忽视,此时连接成功

在下面我们简单的介绍以下连接的作用:

  • 当源程序很大时可以分为多个源程序文件生成目标文件,最后再将目标文件连接到一起
  • 程序中调用了某个库文件的子程序,需要将这个库文件和目标文件连接到一起生成可执行程序
  • 一个源程序在编译后,得到了有机器码的目标文件,目标文件中的内容还不能直接生成可执行程序,所以需要连接程序处理

以简化的方式进行编译和连接

image.png

直接在命令后面加一个 ; 可以直接忽略中间产物的生成,实现快速的编译连接

程序执行过程的跟踪

汇编程序从写出到执行的过程:

1
2
 编程 --> 1.asm --> 编译 --> 1.obj --> 连接 --> 1.exe --> 加载 --> 内存中的程序 --> 运行
(Edit) (masm) (Link) (command) (CPU)

我们先展示一下EXE文件中程序加载的过程:

image.png

我们可以根据这副图得到以下信息:

  • 程序加载后ds中存放着程序所在内存区的段地址,这个内存区的偏移地址为0,则程序的内存区的地址为ds:0

  • 这个内存区的前256个字节存放的是PSP,DOS用来用来和程序进行通信。从256个字节之后存放的是程序

    因为PSP占256(100H)个字节,所以程序的物理地址是:

    SA * 16 + 0 + 256 = SA * 16 + 16 * 16 + 0 = (SA + 16) * 16 + 0 = (SA + 10H) + 0

    可以用段地址和偏移地址表示为 SA+10H:0

程序执行过程的跟踪

我们以刚刚的程序1.exe为例:

image.png

我们可以看到图中DS的值为075AH,则PSP的地址为075A:0 ,程序的地址为076A:0(即075A + 10:0)

同时可以看到从076A:0000~076A:000F都是我们的程序的机器码

现在我们开始单步执行跟踪:

image.png

当我们执行到 INT 21时需要用P指令退出程序,最后再使用Q指令返回command

昨天我们学习了简单寄存器的使用DEBUG程序的使用

这一篇我们将学习 寄存器的内存访问

寄存器(内存访问)

从访问内存的角度认识学习寄存器i

我们知道一个字的存储分为高字节和低字节,由于内存地址是自上而下向下递增的,所以高位字节从内存分布上看再地位字节的下面

也就是说当我们从0地址开始存放 数值20000(4E20H)

其在内存空间中的顺序为

1
2
3
4
5
+---------+----------+-----------------------------------
| 0 | 1 | ...
+---------+----------+-----------------------------------
| 20 | 4E | ...
+---------+----------+-----------------------------------

这样的分布特点我们称之为 小端序

综上所述,我们知道任何两个地址连续的内存单元,N号单元和N+1号单元,可以将它看成两个内存单元,也可以看成一个地址为N的字单元中的高位字节单元和低位字节单元

DS和[address]

8086CPU中有一个DS寄存器,通常用来存档要访问的数据的段地址。

我们用下面的例子来展示它的用法,比如读取10000H单元的内容:

1
2
3
mov bx,1000H
mov ds,bx
mov al,[0]

我们通过上面的三个指令,实现读取,接下来一一解释

首先是前两句,为什么不能直接 mov ds,1000H呢?这是8086CPU的硬件问题,我们并不支持此行为,所以我们用一个寄存器来中转

第三句的 […] 又是什么意思呢? […]表示操作对象是一个内存单元,里面的数值代表内存单元的偏移地址

mov,sub,add指令

首先我们需要知道这三个指令的特点:他们都有两个操作对象

MOV指令

我们先看看至今我们所知的mov的用法:

  • mov 寄存器,数据
  • mov 寄存器,寄存器
  • mov 寄存器,内存单元
  • mov 内存单元,寄存器
  • mov 段寄存器,寄存器

根据这些我们可以合理的猜测一些其他的用法,并使用DEBUG程序来验证:

  • mov 内存单元,段寄存器 验证通过
  • mov 寄存器,段寄存器 验证通过
  • mov 段寄存器,内存单元 验证通过

SUB ADD指令

他们也可以有以下用法:

  • add 寄存器,数据
  • add 寄存器,寄存器
  • add 寄存器,内存单元
  • add 内存单元,寄存器
  • sub 寄存器,数据
  • sub 寄存器,寄存器
  • sub 寄存器,内存单元
  • sub 内存单元,寄存器

数据段

在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将一组长度为N(N<=64KB)、地址连续、起始地址为16的倍数的内存单元作为专门存储数据的内存空间。从而定义了一个数据段

将一段内存作为数据段,是我们编程时的一种安排,我们可以在具体操作时,用DS存放数据段的段地址,从而进行访问

比如一段数据段 123B0H~123B9H 的内存单元定义为数据段 ,现在要累加这个数据段的前三个内存单元的值

1
2
3
4
5
6
mov ax,123B
mov ds,ax
mov al,0 ;给ax赋值为0
add al,[0] ;加上数据段的第一个值
add al,[1] ;......
add al,[2]

8086CPU 提供相关的指令来以栈的方式访问内存空间。这意味着,在基于8086CPU编程时,可以将一段内存作为栈来使用

8086CPU提供入栈和出栈指令,分别时POP和PUSH

  • push ax 将ax中的数据送入栈中
  • pop ax 从栈顶取出数据送入ax中

注意: 入栈和出栈的操作都是以字为单位进行的

SS:SP

CPU是怎么知道栈顶的位置呢?

在8086CPU中,有两个寄存器,分别是段寄存器SS 和 寄存器SP,栈顶的段地址存放在SS中,栈顶的偏移地址存放在SP中。任意时刻,SS:SP指向栈顶元素。执行pop和push时,CPU从SS和SP中得到栈顶的地址。

现在我们可以对pop和push进行完整的描述了:

push:

  1. SP = SP - 2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶
  2. 将ax中的内容送入SS:SP指向的内存单元,SS:SP 此时指向新的栈顶

pop:

  1. 将SS:SP 指向的内存单元处的数据送入ax中
  2. SP = SP + 2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶

当栈顶的数据出栈之后,其内存单元所存储的数据仍然存在,但其已经不在栈顶中。当再次进行入栈操作时,直接对其数据进行覆盖

pop push指令

栈空间也是内存空间的一部分,它只是一段可以以一种特殊的方式进行访问的内存空间

push,pop可以是指令格式:

  • push,pop 寄存器
  • push,pop 段寄存器
  • push,pop 内存空间

栈顶超界问题

我们在此讨论一个问题,虽然我们可以通过SS 和SP来确保在进行入栈和出栈时找到栈顶。可是怎么保证栈顶不会超出栈空间呢?

当我们把一个空间容量为16个字节的内存空间当作栈时,向其中压入八个字后就已经达到了栈顶,此若是再使用push操作,其数据便会溢出栈空间,覆盖栈以外的数据。

同理当我们已经达到栈底时,我们再进行一次pop操作,我们会把栈空间以下的数据弹出。

以上这些操作我们都称为 栈顶越界问题

如果CPU中有记录栈顶上限和栈底的寄存器,那么可以检测越界问题。但是,在8086CPU中,并没有这个寄存器,因此其不保证我们对栈的操作不会越界。这一点需要操作者自行考虑

栈段

如果我们设置一段内存,将它当作栈,并以栈的形式进行访问,那么我们可以称之为 栈段

这里我们有一个问题,如果将10000H~1FFFFH这段空间当作栈段,SS = 1000,SP = FFFE。也就是说,此时栈段内还有一个数据,如果我们将这个数据进行出栈操作,那么此时SP = ?,栈顶指向哪里?

由于出栈后,SP = SP + 2,栈顶指向最底部单元下面的单元。所以此时SP = 0

那么我们可以说 SP=0,此时即是空栈也是满栈

实验二:用机器指令和汇编指令编程

正如前文所言,我们使用D命令查看内存单元的命令,那么我们有以下疑问:

  • Debug是靠什么来执行D命令的? 是一段程序

  • 谁来执行这段程序? 用CPU

  • CPU在访问内存单元时从哪里得到内存单元的段地址? 从段寄存器得到

所以我们得出结论 在处理D命令的程序段中,必须有将段地址送入段寄存器的代码

段寄存器有4个:SS,ES,CS DS,那么将段地址送入那个段寄存器呢?

由于CS要用来指向处理D命令的代码,而SS要作为指向栈顶的代码。再因为一般默认段地址再DS中,所以我们将段地址送入DS中

下一条指令执行了嘛?

我们有这样一个程序:

1
2
3
4
5
6
7
mov ax,2000
mov ss,ax
mov sp,10
mov ax,3123
push ax
mov ax,3366
push ax

我们使用T命令单步执行,看一看发生了什么?

image.png

注意看,在执行 mov ss,ax之后本来应该是 mov sp,10但是却直接来到了 mov ss,ax

但通过观察SP的值,我们可以知道 mov sp,10得到了执行,这是为什么呢?

这是因为设计到了之后的一个内容:中断机制

在这里我们只需要知道,T命令在执行修改寄存器 SS 的指令时,下一条指令也被紧接着执行

搞了那么久的博客现在终于可以开始汇编语言的学习了

这里我用的教材是王爽老师的《汇编语言》,使用的环境是基于DOSBOX的模拟DOS环境

接下来开始正式的学习

今天的内容是 寄存器实验一

寄存器

8086CPU共有14个寄存器,每个寄存器都有对应的名称,他们分别是:

AXBXCXDXSIDISPBPIPCSSSDSESPSW

我们不一次性的对其进行研究,我们在后续对这些寄存器的作用进行一一的讲解

通用寄存器

AX,BX,CX,DX 这四个寄存器用来放一般性的数据,所以被称为通用寄存器

8086的所有寄存器都是16位的,可以存放两个字节

为了保证CPU的兼容性,这四个通用寄存器都可以被分为两个可独立使用的八位寄存器:

  • AX = AH + AL
  • BX = BH + BL
  • CX = CH + CL
  • DX = DH + DL

XL 指的是 XX寄存器的低八位(0-7位),XH 指的是 XX 寄存器的高八位(8-15位)

字在寄存器中的存储

出于对兼容性的考虑,8086CPU可以对以下两种尺寸的数据进行操作:

  • 字节(Byte):一个字节由八位组成,可以存储在八位寄存器中
  • 字(Word): 一个字由两个字节组成,这两个字节分别被称为这个字的高位字节和地位字节

一个字可以存在一个16位寄存器中,那么这个字的高位字节和低位字节便存储在这个寄存器的高8位寄存器和低8位寄存器中

基本的汇编指令

介绍 mov 与 add

这里我们用几个例子来展示他们的用法

MOV:

  • mov ax,18 -> 将18送入寄存器ax中
  • mov ah,15 -> 将15送入寄存器ah中
  • mov ax,bx -> 将寄存器bx中的值送入寄存器ax

ADD:

  • add ax,8 -> 将寄存器ax中的值加上8
  • add ax,bx -> 将ax和bx的值相加,并将结果保存在ax中

注意:

  1. 十六位寄存器的溢出:

    当ax = bx = 8226H时,执行 add ax,bx 后ax = ?

    ax本来应该等于 1044CH,但是由于最高位溢出了,所以ax = 044CH

  2. 八位寄存器的溢出:

    当 ax = 00C5H时,执行 add al,93H 后al = ?

    ax本来应该等于 0158H,但是由于最高位溢出了,所以ax = 0058H,al = 58H

8086给出物理地址的方法

所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间都有唯一的地址,我们将这个唯一的地址称为物理地址。在CPU发出物理地址之前,必须要在内部先生成这个地址

8086CPU有20位地址总线,最多可以传20位地址,达到1MB的寻址能力

可是8086是16位机器,按道理只能做到16位的64KB寻址,是怎么做到的呢?

这是因为我们使用了一种通过两个十六位地址合成一个二十位地址的方法

当8086CPU要读写内存时:

  • CPU相关部件提供两个地址:一个是16位段地址,一个是16位偏移地址
  • 这两个地址通过内部总线传输到地址加法器合成为一个20位的物理地址
  • 将这20位的物理地址传输到地址总线上

在这里地址加法器通过 **物理地址 = 段地址*16 + 偏移地址** 生成20位的物理地址

注意:段地址*16 本质上就是将段地址左移四位,将地址XXXXH变为XXXX0H

段的概念

在根据编程需要时我们将若干连续的内存单元看作一个段

先讲解一下的含义,在这里,段并不意味着内存被分为一段一段,而是我们可以通过分段的方式来管理内存

段的划分来源于CPU ,由于我们使用 **物理地址 = 段地址*16 + 偏移地址 ** 的方式给出内存的物理地址

所以我们用 段地址*16 定位段的起始地址(基础地址),用偏移地址定位段中的内存单元

我们需要注意:

  • 段地址*16,即一个段的起始地址一定是16的倍数
  • 偏移地址为16位,即其最大寻址位置为64KB,所以说一个段的最大长度为64KB

段寄存器

8086 有四个段寄存器CS、DS、SS、ES,这里我们先只看CS

其中CS,IP 是8086CPU中最重要的两个寄存器,他们指示了CPU当前要读取的指令的地址

CS 为代码段寄存器,IP为指令指针寄存器,我们可以这样理解,在8086机中任意时刻,CPU将CS:IP指向的内容当作指令执行

我们可以把8086CPU的工作过程表述为:

  1. 从 CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲区
  2. IP = IP + 所读取的指令的长度,从而指向下一条指令
  3. 执行指令,并返回步骤(1),重复这个过程

我们可以这么说,内存中的一段信息被CPU执行过,那么他所在的内存单元一定被CS:IP指向过

修改CS、IP指令

8086CPU大部分的寄存器的值都可以通过mov来改变,但是mov不能用于修改CS:IP 的值,mov被称为传送指令

能够修改CS:IP的值的指令通称为转移指令,如简单的jmp指令

  • 若想同时修改CS、IP的值,我们可以使用指令 jmp 段地址:偏移地址 的指令完成
  • 若想修改仅IP的内容,我们可以使用指令 jmp 某一合法的寄存器 的指令完成,这一步可以抽象理解为 mov IP 该寄存器中的值

第一条指令

在8086CPU加电启动或者复位后(即刚开始工作时),CS和IP被设置为 CS=FFFFH , IP=0000H

即在8086PC机刚启动时,CPU从内存FFFF0H单元中读取指令执行,FFFF0H单元存放着8086PC机开机后执行的第一条命令

实验一:查看CPU和内存,用机器指令和汇编指令编程

使用DEBUG

DEBUG 是 DOS,Windows 都提供的实模式(8086方式)程序的调试工具。可以用它查看CPU的各种寄存器中的内容、内存使用的情况和在机器码级跟踪程序的运行

我们需要用到以下的Debug功能:

  • R:查看、改变CPU寄存器的内容
  • D:查看内存中的内容
  • E:改写内存中的内容
  • U:将内存中的机器指令翻译成汇编指令
  • T:执行一条机器指令
  • A:以汇编指令的格式在内存中写入一条机器指令

R命令

1
debug -r

开启了debug的R模式

image.png

我们可以看到一个用法 -r 寄存器名称 可以修改指定寄存器的值

D命令

1
debug -d

我们可以看出其显示的三部分:

  • 左边部分是每行的起始地址
  • 中间部分是从指定地址开始的128个内存单元的内容,注意每行中间的”-“,这是用来区分每行的前八个字节和后八个字节的标志
  • 右边是每个内存单元中的数据对应可显示的ASCII字符,如果不可显示,则用”.”替代

其有以下三种用法:

  • 一是 d 段地址:偏移地址 指定地址CS:IP,对其进行128字节的查看
  • 二是 d 段地址 :偏移地址 结尾偏移地址 可以指定查看的范围 即从偏移地址到结尾偏移地址之间的内存空间
  • 三是 d 直接查看,将列出Debug预设的地址处的内容

E命令

1
debug -e
image.png

我们可以通过E指令来进行内存空间的改写,其操作如下:

e 起始地址 数据1 数据2 数据3 ……这个操作对起始地址之后的内存空间进行覆盖

其中数据可以是字符,也可以是数值,甚至是字符串

或者我们可以使用提问式的方法来进行修改

image.png

有以下步骤:

  • 首先,输入 e 起始地址 ,按Enter
  • 我们从起始地址的第一个值开始,“.”之前的数值是该单元的原数值,之后则是要修改的数值
  • 我们使用空格跳过当前单元(无论是否修改),按下Enter表示结束修改

U指令

我们使用E指令写入以下的一段汲取嘛,然后用U指令对其内容翻译为汇编指令

1
2
3
b80100		mov ax,0001
b90200 mov cx,0002
01c8 add ax,cx

我们看到D命令的输出可以分为三部分:

  • 左边部分为指令占用的内存单元的起始地址和对应的机器码
  • 右边部分为翻译后的汇编语言

T命令

现在我们尝试执行我们写的汇编语言,使用T命令执行一条或多条汇编指令

首先我们需要修改CS:IP 指向的命令 然后开始执行

image.png

我们可以看到执行结果符合我们呢的期望

A命令

前面我们使用E命令写入机器指令,这样很不方便,所以我们使用A指令直接写入指令

其效果如下:

image.png

我们看到再给出起始地址后可以直接进行编辑

这是我的第一篇博客,这个博客搭建花了很多时间,遇到了很多问题,在这里分享一下

环境配置

首先是环境的配置

你需要安装 node.js 作为包管理器进行 Hexo 网站的搭建

其次你需要将 git 与你的 github 账号建立SSH通道

然后进行Hexo的安装:

1
npm install hexo -g

接着新建一个文件夹作为你的本地博客的文件,进行安装Hexo的依赖

1
npm install --save hexo-deployer-git

搭建博客

初始化

首先我们需要创建一个用于存放博客的文件目录,然后cd进入目录,并对其进行初始化

1
hexo init

因为我遇到一些问题,所以我是直接从Github下载解压到本地

如果你也遇到这个问题从这里

下载

当初始化结束后我们可以看到以下文件出现在我们的目录中

None

测试效果

接着我们生成页面

1
hexo generate

然后我们通过本地网站打开

1
hexo server

然后在http://localhost:4000/查看效果

上传部署

首先创建一个仓库(注意命名:用户名.gitihub.io)比如这里我是Ylin07.github.io

然后我们在本地博客存放的文件中对_config.yml进行以下编辑:

1
2
3
4
deploy:
type: git
repository: git@github.com:Ylin07/Ylin07.github.io.git
branch: main

接下来我们进行Hexo的三部曲

  • hexo clean
  • hexo g
  • hexo d

这里最后的 hexo d我遇到了一个问题,就是始终部署失败

我在网上找到的解决办法是在C:\Windows\System32\drivers\etc下的hosts文件添加github的IP地址,其操作如下:

1
notepad C:/Windows/System32/drivers/etc/hosts

在最下面添加

1
140.82.112.4 github.com

然后保存即可,这样就完成了对网站的部署,接下来让我们看看效果吧