汇编语言学习笔记-1

0x00 存储器的存储结构

image-20230211201707096

​ 1TB=1024GB,1GB=1024MB,1MB=1024KB,1KB=1024B,1B=1字节(8位)。

​ CPU想要对存储介质上的数据进行操作,就必须通过总线,总线分为地址总线、控制总线、数据总线。例如,CPU在内存中读数据,图示如下:

image-20230211202158567

​ 可以看到,CPU是通过地址总线来指定存储单元的。地址总线上能传送多少个不同的信息, CPU 就可以对多少个存储单元进行寻址。如果一个 CPU 有 N 根地址总线,即地址总线宽度为 N,那么就可以寻址2^N个内存单元。

​ 数据总线宽度关系到了 CPU 和外界的数据传送速度。下面展示了8位和16位数据总线在传输0x89D8时候的不同:

image-20230211202643679

image-20230211202703867

​ 可以看到,8位需要传两次,16位传一次即可。

小结:

  1. 每一种 CPU 都有自己的汇编指令集。
  2. 一个存储单元可以存储 8 个 bits,即 8 位二进制数。

0x01 内存地址空间

什么是内存地址空间?一个 CPU 的地址线宽度为 10 ,那么可以寻址 1024 个内存单元,这 1024 个可寻到的内存单元就构成这个CPU 的内存地址空间。

​ 存储器包括随机存储器(RAM)与只读存储器(ROM)。随机存储器可读可写,但是关机后储存内容就会消失。只读存储器只能读取,关机后储存内容不会消失。例如,BIOS就储存在ROM中。这和磁盘什么的没啥关系哈,RAM与ROM都是主板上的。

​ 可以将各类存储器看作一个逻辑存储器。如下图所示:

image-20230211211247444

0x02 寄存器

​ CPU由运算器 、 控制器 、 寄存器组成,内部总线实现 CPU 内部各个器件之间的联系,外部总线实现 CPU 和主板上其它器件的联系。

​ 8086CPU 所有寄存器都是 16 位 ,可存放两个字节,AX、BX、CX、DX被称为通用寄存器。8086 上一代 CPU 中的寄存器都是 8 位的,为保证兼容性,AX可以分为AH与AL。BX、CX、DX也可以这样分。

​ 字节为8位,字为16位(2个字节)。

​ 以下是简单的汇编指令举例:(注:汇编指令不区分大小写)

image-20230212095143497

​ 再来看两个特殊情况:

image-20230212095304217

image-20230212095407920

​ 可以看到,上述两个图中的红色问号部分都有进位,不同的是第一个图是AX的进位,第二个图是AL的进位。第二张图中,此时CPU 把 al 和 ah 看作 8 位的寄存器上的独立的寄存器,产生的进位不会存储在 ah 中。第一张图也是类似,因此,第一张图中的问号填044CH,第二张图的问号填58H

16位结构的CPU体现在哪里?

  1. 运算器一次最多可以处理 16 位的数据。
  2. 寄存器的最大宽度为 16 位。
  3. 寄存器和运算器之间的通路是 16 位的。

8086CPU是如何找物理地址的?

存在这样一种矛盾,8086 有 20 位地址总线,可传送 20 位地址,寻址能力为 1MB 。而8086 内部为 16 位结构,它只能传送 16 位的地址,表现出的寻址能力却只有 64KB 。为解决这种矛盾,8086CPU 采用一种在内部用两个16 位地址合成的方法来形成一个 20 位的物理地址。具体公式为:

1
物理地址 = 段地址 × 16 + 偏移地址

​ 图像解释如下:

image-20230212101019883

image-20230212101118772

分段

​ 内存并没有分段,段的划分来自于 CPU(就是因为上面的公式)。

  1. 段地址 × 16 必然是 16 的倍数,所以一个段的起始地址也一定是 16 的倍数。
  2. 偏移地址为 16 位, 16 位地址的寻址能力为 64K,所以一个段的长度最大为 64K 。

​ 再给出如下结论:

  • CPU 可以用不同的段地址和偏移地址形成同一个物理地址 。如下所示:

image-20230212101908369

  • 偏移地址 16 位,变化范围为 0-FFFFH ,仅用偏移地址来寻址最多可寻 64K 个内存单元。例如,给定段地址 1000H ,用偏移地址寻址 CPU 的寻址范围为: 10000H-1FFFFH 。

段寄存器

​ 段寄存器就是提供段地址的。8086CPU中有 4 个段寄存器:CS、DS、SS、ES。

CS(代码段寄存器 )和 IP(指令指针寄存器 )是8086CPU 中最关键的寄存器,它们指示了 CPU 当前要读取指令的指令地址。具体可以看《汇编语言第三版》P26-P31。最终,可以总结出8086CPU的工作流程:

  1. 从 CS:IP 指向内存单元读取指令,读取的指令进入指令缓冲器;
  2. IP = IP + 所读取指令的长度 ,从而指向下一条指令;
  3. 执行指令。 转到步骤 1 重复这个过程。

​ 8086CPU刚刚开机时,CS=FFFFH,IP=0000H,所以CPU 从内存 FFFF0H 单元中读取指令执行,这是开机后执行的第一条指令 。内存中指令和数据没有任何区别,都是二进制信息,但是CPU将 CS:IP 指向的内存单元中的内容看作指令

控制CS:IP跳转到目标地址

​ 在CPU中,程序员能够用指令 读写的部件只有寄存器,程序员可以通过改变寄存器中的内容实现对CPU的控制

  • 同时控制CS与IP。

image-20230212104023385

  • 仅仅控制IP。可以看到直接jmp ax控制的其实是偏移地址。

image-20230212104118120

注意:jmp ax等价于mov IP, axjmp 3:01B6等价于mov cs, 3; mov ip, 01B6。你懂得~

Q&A

image-20230212104418722

1
2
3
4
5
6
7
mov ax, 6622H
jmp 1000:3
mov ax, 0000
mov bx, ax
jmp bx
mov ax, 0123H
之后又转到mov ax, 0000 ...

代码段

​ CS:IP指向的内容就是代码段。就类似于上述的Q&A中的示例,就是代码段。代码段举例(123B0H-123B9H):

image-20230212104759149

​ 可以认为上述代码是段地址是123BH,长度为10字节的代码段

0x03 实验 1

​ 见《汇编语言第三版》P35。

补充:

  • Debug是DOS、Windows提供的调试工具,可以查看CPU寄存器中的内容。
  • Debug常用命令。
1
2
3
4
5
6
7
8
R // 查看、改变寄存器中的内容
D // 查看内存
E // 改写内存
U // 将机器指令翻译成汇编指令
T // 执行一条机器指令(步进)
P // 与T不同的是,P不会跟踪进入子程序(步过)
G // 运行到断点
A // 以汇编指令的格式在内存中写入一条机器指令

查看并修改寄存器中的内容:

image-20230212121108518

查看内存0AD40H及其之后的内容:

image-20230212121328140

改写内存10000H的内容:

​ 一块修改

image-20230212121946937

​ 逐位修改

image-20230212122216299

​ 写入字符串

image-20230212122435957

​ 写入机器码并展示

image-20230212122641720

​ 执行刚刚写入的机器码(需要预先修改CS与IP)

image-20230212162049686

​ 直接写入汇编指令

image-20230212162312637

0x04 寄存器(内存访问)

​ 小端序与大端序,如下图所示:

image-20230212163332475

​ 之前我们知道:物理地址 = 段地址 × 16 + 偏移地址。代码段的短地址保存在CS寄存器中,数据段的段地址保存在DS寄存器中。

​ 使用mov指令可以读取内存,其格式为mov 寄存器名 [address],其中address为偏移地址,执行此命令时,8086CPU会自动取DS作为段地址

内存到寄存器。使用mov指令从10000H中读取1个字节放到AL中:

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

​ 注:8086CPU不支持将数据直接送入段寄存器的操作,而物理地址10000H可以看作段地址:偏移地址=1000H:0H

​ 寄存器到内存差不多,举一个例子:

image-20230212165556693

image-20230212165800075

​ 对于8086CPU来说,累加123B0H的前三个单元的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 累加字节数据
mov ax, 123BH
mov ds, ax
mov al, 0
add al, [0]
add al, [1]
add al, [2]

// 累加字数据
mov ax, 123BH
mov ds, ax
mov ax, 0
add ax, [0]
add ax, [2]
add ax, [4]

0x05 栈

​ 8086CPU的入/出栈操作都是以字(两个字节)为单位进行的。栈的增长方向是由高地址往低地址。栈操作举例:

image-20230212171247365

​ 类似于CS:IP,存放着指令的段地址与偏移地址,SS:SP存放着栈顶的段地址与偏移地址。SS:Stack Segment、SP:Stack Pointer。

​ push指令的执行过程,以push ax为例:

  1. SP=SP–2。

  2. 将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶。

image-20230212171757043

​ pop指令的执行过程,以pop ax为例:

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

Q&A

Q1:将10000H-1000FH 这段空间当作栈,初始状态是空的,将 AX、BX、DS中的数据入栈。

A1:

1
2
3
4
5
6
mov cx, 1000H
mov ss, cx
mov sp, 10H
push ax
push bx
push ds

Q2:将10000H-1000FH 这段空间当作栈,初始状态是空的;设置AX=001AH,BX=001BH;将AX、BX中的数据入栈;然后将AX、BX清零;从栈中恢复AX、BX原来的内容。

A2:

1
2
3
4
5
6
7
8
9
10
11
mov cx, 1000H
mov ss, cx
mov sp, 10H
mov ax, 1AH
mov bx, 1BH
push ax
push bx
xor ax, ax
xor ba, bx
pop bx
pop ax

Q3:将10000H-1000FH 这段空间当作栈,初始状态是空的;设置AX=002AH,BX=002BH;利用栈 ,交换 AX 和 BX 中的数据。

A3:

1
2
3
4
5
6
7
8
9
mov cx, 1000H
mov ss, cx
mov sp, 10H
mov ax, 2AH
mov bx, 2BH
push ax
push bx
pop ax
pop bx

Q4:我们如果要在10000H处写入字型数据2266H,可以用以下的代码完成:

1
2
3
4
mov ax, 1000H
mov ds, ax
mov ax, 2266H
mov [0], ax

​ 补全下列代码,完成同样功能。(不能使用mov [内存单元],寄存器

1
2
3
4
5
________
________
________
mov ax, 2266H
push ax
1
2
3
4
5
mov ax, 1000H
mov ss, ax
mov sp, 2H ;sp先减2,然后再写入
mov ax, 2266H
push ax

​ push、pop 实质上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与mov指令不同的是,push和pop指令访问的内存单元的地址不是在指令中给出的,而是由SS:SP指出的。同时,push和pop指令还要改变 SP 中的内容

0x06 实验2

​ Debug的使用技巧:

image-20230212180838625

image-20230212180848292

​ 注:Debug的T命令(运行一条汇编语句)在执行修改寄存器SS的指令时,下一条指令也紧接着被执行。

0x07 第一个程序

流程:

  1. 编写汇编源程序
  2. 对源程序进行编译连接。使用汇编语言编译程序对第一步的源程序进行编译,产生目标文件。再用连接程序对目标文件进行连接,生成可执行文件
  3. 运行可执行文件。

示例程序分析

​ 给出如下程序:

1
2
3
4
5
6
7
8
9
10
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执行,是由编译器来执行,编译器根据伪指令来进行相关的编译工作。

​ 上述程序的伪指令有:

  • 段名 segment ... 段名 ends。代表一个段,这个段用来存放代码。
  • end。汇编程序的结束标记,不要与ends搞混。
  • assume将有特定用途的段和相关的段寄存器关联起来,例如assume cs:codesg就是将codesg段与cs段寄存器关联起来,以说明codesg内存放的是代码。

​ 计算2^3的示例程序:

1
2
3
4
5
6
7
8
9
assume cs:abc
abc segment
mov ax, 2
add ax, ax
add ax, ax
mov ax, 4c00H
int 21H
abc ends
end

​ 在汇编后加入mov ax, 4c00H; int 21H就可以实现程序返回。在DOS中edit并保存为1.asm。接下来使用masm.exe对文件进行编译,生成1.obj。之后使用1.obj进行连接,使用link.exe生成1.exe。

​ 连接的作用:

  • 源程序很大时,将其分为多个源程序来编译,将源程序编译成目标文件后,再用连接程序将其连接到一起,生成一个exe。

  • 将库文件与目标文件连接起来。

image-20230218212118944

​ 下图展示了DOS系统在.EXE文件中的程序加载过程:

image-20230218215152495

  • 程序加载后,ds存放程序内存区的段地址,内存区偏移地址为0,程序所在内存区的地址为ds:0
  • 内存区的前256字节存放PSP(DOS用来和程序来进行通信),从256字节处向后的空间存放的是程序。

​ 调试显示:

image-20230218224403916

DS=0B14可以看到PSP的段地址SA=0B14,PSP的偏移地址为0,因此PSP的物理地址为SA*16+0,程序的物理地址为SA*16+0+256=SA+10H:0=0B24H,这也是CS指向的地址。

​ 再用u查看一下汇编代码,如下:

image-20230218224720512

​ 再p单步执行,执行到INT 21,结束。

0x07 [BX]和内存单元的描述

[BX]是什么?

[0]表示一个内存单元时,0表示单元的偏移地址,段地址默认在DS中,而取出数据的长度由具体指令中的接收值的寄存器指出。[BX]也是同理,BX代表单元的偏移地址,其他与[0]类似。

​ 举例题目如下:

image-20230218230529558

​ 答案:

1
2
3
4
5
6
7
8
| BE |  21000H
| 00 | 21001H
| BE | 21002H
| 00 | 21003H
| BE | 21004H
| BE | 21005H
| BE | 21006H
| 00 | 21007H

一些补充

  • 我们用()来表示一个寄存器中的内容,举例mov ax [2]代表(ax)=((ds)*16+2)add ax, bx代表(ax)=(ax)+(bx)push ax代表(sp)=(sp)-2;((ss)*16+(sp))=(ax)
  • idata表示常量。(不能向DS写常量,例如mov DS, 1

Loop指令

1. `(cx)=(cx)-1`
1. 判断cx中的值是否为0,如果不为0则跳转到标号处执行,否则继续下个下执行。(**cx为循环次数**)

​ 程序示例:

image-20230219102425003

1
2
3
4
5
6
7
8
9
; 任务1
assume cs:task1
task1 segment
mov ax, 2
add ax, ax
mov ax, 4c00H ; 程序返回
int 21H
task1 ends
end
1
2
3
4
5
6
7
8
9
10
; 任务2
assume cs:task2
task1 segment
mov ax, 2
add ax, ax
add ax, ax
mov ax, 4c00H
int 21H
task2 ends
end
1
2
3
4
5
6
7
8
9
10
11
12
; 任务3(loop的使用)
assume cs:task3
task3 segment
mov ax, 2
mov cx, 11
loop1:
add ax, ax
loop loop1
mov ax, 4c00H
int 21H
task1 ends
end

Q&A

  • 加法计算123*256,并将结果保存在A X中。
1
2
3
4
5
6
7
8
9
10
11
assume cs:addFunc
addFunc segment
mov ax, 0
mov cx, 236
loop1:
add ax, 123
loop loop1
mov ax, 4c00H
int 21H
addFunc ends
end
  • 计算ffff:0006单元中的数乘3,结果保存在DX中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
assume cs:func
func segment
mov dx, 0
mov ax, 0ffffH ; 汇编程序中数据不能以字母开头,因此前面要加0
mov ds, ax
mov ax, 0
mov al, [6]
mov cx, 3
loop1:
add dx, ax
loop loop1
mov ax, 4c00H
int 21H
func ends
end

Debug与Masm的不同处理

​ Debug中直接用a写入汇编命令:

1
2
3
4
5
6
mov ax, 2000
mov ds, ax
mov al, [0]
mov bl, [1]
mov cl, [2]
mov dl, [3]

​ 然后在debug中用u命令查看(debug的解释)

image-20230219113718032

​ 和正常一样,但是如果用edit来写汇编程序的话(编译器的解释),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
assume cs:code 
code segment
mov ax,2000h
mov ds,ax
mov al,[0]
mov bl,[1]
mov cl,[2]
mov dl,[3]
mov ax,4c00h
int 21h
code ends
end

​ 结果如下图所示(怎么与程序写的不一样???)

image-20230219113657168

​ 那我们如何避免呢?那就需要这么写:

1
2
3
4
5
6
7
assume cs:code 
code segment
mov ax, 2000h
mov ds, ax
mov al, ds:[0] ;前面加入段寄存器
code ends
end

​ 或者这么写:

1
2
3
4
5
6
7
8
assume cs:code 
code segment
mov ax, 2000h
mov ds, ax
mov bx, 0
mov al, [bx]
code ends
end

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

A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
assume cs:func
func segment
mov ax, 0ffffH
mov ds, ax
mov bx, 0 ; 指针
mov dx, 0 ; 存储结果
mov cx, 0bH ; 循环次数
mov ax, 0 ;中间寄存器
loop1:
mov al, [bx]
add dx, ax
inc bx
loop loop1
mov ax, 4c00H
int 21H
func ends
end

安全空间

  • DOS方式下,0:200-0:2FF空间中没有系统或其他程序的程序,可以放心使用,其他的不确定,有可能存放重要代码。
  • 将内存ffff:0-ffff:b单元中的数据拷贝到0:200-0:20b单元中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
assume cs:code
code segment
mov cx, 0bH
mov bx, 0
loop1:
mov ax, 0ffffH
mov ds, ax
mov dl, [bx]
mov ax, 20H
mov ds, ax
mov [bx], dl
inc bx
loop loop1
mov ax, 4c00H
int 21H
code ends
end

​ 改进如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
assume cs:code
code segment
mov ax, 0ffffH
mov ds, ax
mov ax, 20H
mov es, ax
mov bx, 0
mov cx, 12
loop1:
mov dl, ds:[bx]
mov es:[bx], dl
inc bx
loop loop1
mov ax, 4c00H
int 21H
code ends
end

留言

2023-02-11

© 2024 wd-z711

⬆︎TOP