Basic Part

Memory Segmentation in 8086 Microprocessor

REF ….

存储器 、存储单元

存储器 也就是我们平时所说的内存 。 负责为CPU提供指令和数据 , 在一台PC机中内存的作用仅次于CPU,存储器被划分为若干个存储单元, 每个存储单元从0开始顺序编号. 一般微型机的存储器的存储单元可以存储一个Byte,即一个字节(8个二进制位) - 微机存储器容量的最小单位

一般应具有 存储数据 和 读写数据 的功能, 每个单元有一个地址,是一个整数 编码 ,可以表示为 二进制 整数。

磁盘不同于内存,磁盘上的数据或程序不读取到内存中就无法被CPU使用

CPU对存储器的读写

存储单元在存储器中顺序编号,这些编号可以看作存储单元在存储器中的地址。

计算机中 连接 CPU 与其他芯片的导线被称作是 "总线" ,
根据传输信息的不同, 其从逻辑上可分为三类 :

  • 地址总线 (CPU通过地址总线来指定存储器单元)

    地址总线的宽度(位数)决定了CPU寻址的最大空间大小,假设一个CPU有10根地址总线,其寻址最大数为2^10 = 1024个 最小数为0,最大数为1023

    地址总线可寻到的内存单元构成了这个CPU的内存地址空间

  • 控制总线

    控制总线的宽度决定了CPU对外部器件的控制能力。

    CPU通过地址总线来指定存储单元 ,控制总线上能传送信息的多少(控制总线的宽度),决定了CPU可以对多少个存储单元进行寻址。

  • 数据总线

    数据总线的宽度决定了CPU与外界的数据传输速度 , 8根数据总线可以一次传送8位二进制数据 (即1Byte),

    后注: 8086CPU数据总线宽度为16 ,寄存器为16位, 即一次最大可传送一个字的数据大小

    计算机的字长即取决于数据总线的宽度.

内存地址空间

CPU操作存储器(RAM ROM 带有BIOS的ROM)时将他们看作内存来对待,将他们总的看作一个由若干个存储单元组成的逻辑存储器. 这个逻辑存储器就是所说的内存空间地址.

每一个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间,对于CPU来说,在这段地址空间中进行数据读写 , 实际也就是对对应的物理存储器进行读写.

因为内存空间地址与存储单元相关,其大小受到CPU地址总线宽度的限制,例如8086CPU的地址总线为20,则其可以传送2^20个不同的地址信息(0 ~ 2^20-1),也就是可以定位2^20个内存单元,则其内存地址空间大小为 2^20 / 1024^2

在基于一个计算机硬件进行系统编程时,必须知道这个系统中内存地址的分配情况, 在对某类存储器进行数据读写时,必须知道它的第一个单元的地址和最后一个单元的地址,才能保证读写操作是在预期的存储器中进行的.

寄存器

CPU由运算器 \ 寄存器 \ 控制器 等器件组成,这些器件靠内部的总线相连..

内部总线实现CPU内部各个器件之间的联系外部总线实现CPU和主板上其他器件的联系

🚩 字在寄存器中的存储

字(记为word) , 一个字由2个字节组成 这两个字节分别称为这个字的高位字节低位字节.

字单元,即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成, 高地址内存单元中存放字型数据的高位字节 , 低地址内存单元中存放字型数据的低位字节.

由此可见8086CPU采取了小端模式

所谓的小端模式(Little-endian),是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致. 记忆: 地址的增长顺序与值的增长顺序相同

一个十六进制数 = 四位二进制数

几条汇编指令与8086CPU给出物理地址的方式

在进行数据传送或运算时,要注意两个操作对象的位数应当是一致的. 高低位对应

1
2
3
add ax,bx
add al.bl
add ah,bh

8086CPU有20位地址总线,可以传送20位地址,寻址能力 2^20 (bytes) / 1024^2 (bytes) = 1MB

内部采用2个16位地址合成的方法来形成一个20位的物理地址(一个称为段地址 , 一个称为偏移地址)

段地址和偏移地址通过内部总线被送入一个称作地址加法器的部件中,由它将两个16位的地址组合为20位的物理地址

地址加法器通过物理地址 = 段地址 × 16(d) + 偏移地址的方式合成物理地址(5位十六进制数 , 即20位二进制数)


段地址 × 16 常用说法 左移4位(指二进制位) , 相当于 乘 2^4

关于8086CPU的物理地址的五位十六进制数表示方式:

因为其寻址能力为1MB , 一位十六进制数相当于4位二进制数 , 所以五位十六进制数 = 4*5 = 20位 = 2^20 = 1MB 正好符合了8086CPU的寻址能力范围.

🔓 测验题解 - 2.2

有意思的题目…

有一数据存放在内存20000H单元中,现给定段地址为SA,若想用偏移地址寻到此单元。则SA应满足的条件是:最小为 ? 最大为 ?

最大值: 比较好求, 设SA为x, 则 EA为 0000H, x = 20000H / 16 = 2000H

最小值: EA 最大 为FFFF, 则段地址为 2 0000H - FFFF 后右移4位(逆向) 结果 1000H,但是经过验算结果非20000H 而是 1FFFF,初见这我也觉得很奇怪… (我的计算器绝对没有问题.jpg) 但其实1FFFF即段地址1000H的最大寻址范围 , 与所需求内存单元就差 1 , 所以1000H + 1 = 1001H 这才是正确的值.也即是最小满足SA条件的值


Refer CSDN "Assembly Quiz 2.2"

好怪但雀氏

1
2
3
4
5
(20000H-ffffH)/16
=(10001)/16
这里如果不按传统算数先算括号里,而是把括号打开再算。结果就对了
20000H / 16 - FFFFH / 16
2000H - 0FFFH = 1001H

段寄存器

8086CPU有四个段寄存器 : CS \ DS \ SS \ ES

CS:IP 是8086CPU中最关键的俩个寄存器 , 它们指示了 CPU 当前要读取指令的地址

CS为代码段寄存器 (Code Section) IP为指令指针寄存器 (Instruction Pointer)

CS * 16 + IP = 物理地址

关于 IP / CS 的修改

能改变IP / CS 内容的指令被统称为转移指令, 目前可以用一个简单的指令来修改它们 :jmp 指令

指令形如 jmp 段地址:偏移地址, 将CS修改为段地址, IP修改为偏移地址

或者使用 jmp 合法寄存器来修改IP的内容

e.g. jmp ax => mov IP,ax jmp指令的功能类似于这样子

寄存器与内存访问

DS寄存器(数据段寄存器 , data segment register)通常用来存储要访问数据的段地址

🚩 CPU提供的栈机制

栈 – 拥有特殊访问方式的存储空间 – LIFO 后进先出, CPU提供的最基本两个指令是 PUSH(入栈) 和 POP(出栈),它们的操作都是以为单位进行的

SS \ SP 寄存器 – ( Stack Segement ) / ( Stack Pointer ),通过两者定义栈段,任意时刻 SS:SP都指向栈顶元素,CPU将从此处取得栈顶的地址 , 注意 CPU 只记录栈顶,栈空间的大小需要自己管理

pop / push 的执行过程

执行push时.CPU的两步操作是: 先改变SP, 后向SS:SP处传送, 执行 pop时, CPU的两步操作是:先读取SS:SP处的数据,后改变SP

栈综述

将一段内存当作栈段, 这是我们自己在编程中的安排, 想要pop / push 等栈操作指令访问我们定义的栈段,就得将SS:SP指向我们定义的栈段

一个栈段的最大容量为64KB

栈满时继续压栈将导致栈顶环绕

Refer CSDN "ASM 栈顶环绕"

务必注意, 当栈空时, SS:SP将指向栈底的后一个内存单元.

When the stack is empty ,SP Point to the address of the next memory unit at the bottom of the stack , namely SP = At the bottom of the stack +1.

⭐ 关于32位架构中为什么不能以内存到内存的形式传递数据

The answer involves a fuller understanding of RAM. Simply stated, RAM can only be in two states, read mode or write mode. If you wish to copy one byte in ram to another location, you must have a temporary storage area outside of RAM as you switch from read to write.

It is certainly possible for the architecture to have such a RAM to RAM instruction, but it would be a high level instruction that in microcode would translate to copying of data from RAM to a register then back to RAM. Alternatively, it could be possible to extend the RAM controller to have such a temporary register just for this copying of data, but it wouldnt provide much of a benefit for the added complexity of CPU/Hardware interaction.

Refer Stackoverflow "Why IA32 does not allow memory to memory mov?"
### 程序执行过程与跟踪

程序由Shell(操作系统的外壳程序,用户使用Shell来操作计算机系统进行工作 (如DOS中的command.com就是一个Shell) 加载入内存

之后Shell将设置CS:IP来指向程序的入口点, 然后暂停运行, 将程序交给CPU执行.

待任务执行完毕后, 控制返回到Shell , 屏幕显示由当前盘符和当前路径组成的提示符,等待用户输入.(返回到加载者)

📌 实验三 - 编程|编译|连接|跟踪 - - 分析

debug 跟踪以下程序执行过程 , 记录每步执行后相关寄存器中内存和栈顶内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
assume cs:codesg

codesg segment
mov ax,2000H
mov ss,ax ; ss = ax = 2000H
mov sp,0
add sp,10 ; sp = AH
;; 注意pop执行顺序 , 首先将栈顶内容送入对应寄存器 , 再更新SP(栈指针)
pop ax ; 内存追踪 ax = 076cH , SP 新指向下个字开始处 = SP + 2 = C , 新栈顶 2000:C
pop bx ; bx = 01a4H , SP = SP + 2 = E 新栈顶 2000:E

;; 执行顺序与pop相反 , 优先处理SP , 再处理压栈操作
push ax ; sp = sp - 2 = C , 将 ax 入栈 到 2000:C(D) => 01A4H => 076CH(结果)
push bx ; sp = sp -2 = A , bx 入栈 至 2000:A(B) => 01A4H => 01A4H

pop ax ; 出栈 , ax = 01A4H , sp = sp + 2 = C
pop bx ; 出栈 , bx = 076CH , sp = E

mov ax,4c00h ; ax = 4c00H
int 21h ; p
codesg ends

end

Base Register[bx] And Loop instruction

[bx](base, 基址寄存器 , 常用于地址索引 )实际为一个偏移地址EA , 段地址SA默认存放在DS

LOOP 指令格式 : loop 标号 , 通过loop指令实现循环功能 , 通过 CX ( count )寄存器控制循环次数

执行顺序 :

  • (CX) = (CX) -1
  • 判断 若 CX > 0 , 则向前转至 标号 处执行 , 若 CX = 0 , 则继续向下执行

值得注意的是, 这里跳转到 标号 处,相当于高级语言中常见的 for \ while 等 循环中常见的 条件循环 , 汇编实现为 jle,jmp,jne等. 或许之后会详细提到

总体程序框架:

1
2
3
mov cx,循环次数
s: ; 标号:需要循环的程序段
loop s ; loop label

Pseudo code

1
2
3
4
5
CX = CX - 1
if CX <> 0 then
jump to label
else
no jump, continue

Debug和汇编编译器MASM对指令的不同处理

若处理以[]包含的指令时, 型如[idata],它们的处理结果如下

Debug 将[idata]视作一个内存单元, 将内部idata作为内存单元的偏移地址

编译器将[idata]解释作idata

如果要让编译器将其解释为一个 内存单元, 则必须在[]前显式的给出段地址DS

(Concept)段前缀

用于显式指明内存单元的段地址的"DS:","CS:","SS:","ES:", 在汇编语言中被称为 段前缀

📌 实验四 - [bx]与Loop的使用 (3) - - 分析

补全程序 , 将 mov ax,4c00H 之前的指令复制到内存 0:200中

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
assume cs:code
code segment
; quiz3
; cx = 001BH = 27D NOTE
; 23 bytes before mov ah,4cH , i.e. Hexadecimal as 17H

; when a programm was loaded to the memory , cs:ip will initialized as the first address of the program
mov ax,cs ; # 补全1
mov ds,ax
mov ax,0020H ; = 0020:0 = 00200 = es
; init target
mov es,ax ; es extra segment register ( also refers to a segment in the memory which is another data segment in the memory.)
; es stores the segment address of purpose

mov bx,0
mov cx,17H ; 从 0 ~ cs:16H # 补全2

s:mov al,[bx] ; ds:[bx]
mov es:[bx],al ; es:[bx] = 20:bx = (20H*16+bx)
inc bx
loop s

mov ah,4cH
int 21H
code ends
end

; copy the instructions before mov ax,4c00h to 0:200
; 23 bytes , 从debug中得之第一条指令到 mov ah,4ch 之前的指令占据23bytes的空间

通过debug获取 程序的总字节量, 计算于 mov ax,4c00H之前的字节量.( 且注意debug中以十六进制表示的数据)

可以发现需要获取的数据在076C:0017之前,也就是从偏移地址0~16H总共23(17H)个字节

image-20220317111236284

包含多个段的程序

在代码段中使用数据: 可以使用dw(define word)定义字型数据.

利用end的另一个作用:通知编译器程序的入口点在何处,可以使用Start 标号指明程序入口点

🍊 程序分析 6.3

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

code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ; 8 words = 16bytes
; 预期的内存空间 cs:0 ~ cs:F
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ; 16 words = 32bytes

   ; 共 48 bytes ( 即 0~47 共 2F个数据 , 初始为空栈 , SS:SP应指向其后面一个数据 即 2F+1 = 30H)
; REFER : When the stack is empty ,SP Point to the address of the next memory unit at the bottom of the stack , namely SP = At the bottom of the stack +1.

start:mov ax,cs
mov ss,ax
mov sp,30h

mov bx,0
mov cx,8
s:push cs:[bx] ; 数据入栈 sp -= 2
add bx,2
loop s

mov bx,0
mov cx,8
s0:pop cs:[bx]
add bx,2
loop s0

mov ax,4c00h
int 21h

code ends

end start

编写多段程序

利用assume伪指令将定义的段和相关的寄存器联系起来 , 对于不同的段, 要有不同的段名

段名就相当于一个标号,它代表了一个段的段地址 , 编译器会把它处理为一个表示段地址的数值

注意在8086CPU不允许直接将数值传入段寄存器中, 所以要将定义的段程序赋给特定的寄存器时需要一个寄存器作中转.

CPU如何处理所定义的段的内容, 当作指令执行或是数据访问, 亦或是栈空间… 完全取决于程序中具体的汇编指令 , 及汇编指令中对CS:IP SS:SP DS 等寄存器的设置决定

📌 实验五 - 编写与调试多段程序 - - 分析

编译链接并跟踪调试

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
; Experiment 5
assume cs:code,ds:data,ss:stack
; 数据段定义
data segment
dw 0123h,0456h ; ,0789h,0abch,0defh,0fedh,0cbah,0987h ; 16bytes
data ends
; 栈段定义
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,data
mov ds,ax

push ds:[0]
push ds:[2]
pop ds:[2]
pop ds:[0]

mov ah,4cH
int 21H

code ends
end start

;---------------

; Experiment 5 quiz 4
assume cs:code,ds:data,ss:stack
; 数据段定义

code segment
start:
mov ax,stack
mov ss,ax
mov sp,16 ; 指向第四个字型数据

mov ax,data
mov ds,ax

push ds:[0]
push ds:[2]
pop ds:[2]
pop ds:[0]

mov ah,4cH
int 21H

code ends

data segment
dw 0123h,0456h ; ,0789h,0abch,0defh,0fedh,0cbah,0987h ; 16bytes
data ends
; 栈段定义
stack segment
dw 0,0 ; ,0,0,0,0,0,0
stack ends

end start

Experiment 5 Quiz 1

  • 程序返回前, data段中数据为 23 01 56 04… 共16个字节 , 剩下部分用00补全

  • 程序返回前, CS = 076E SS = 076D DS = 076C

  • 程序加载后, 设Code段的段地址为X , 则data 段的段地址为 X-2 , stack段的段地址为 X-1

  • 对于如下定义的段:

1
2
3
name segment
...
name ends

如果段中的数据占N个字节,则程序加载后 , 该段实际占有的空间为 (N / 16 + 1 )N小于16,则向下取整 * 16 个字节 , 若N大于16 , 则向上取整

Refer CSDN "关于 (N / 16 + 10) * 16"
  • 若将伪指令 end start 改为 end (不指明程序的入口点) . 程序均可正常执行 , 只是不再从指定的入口点开始执行,而是从程序开始的程序段处开始执行, 若此程序段为数据段或栈段, 编译器会将其处理为汇编指令并执行.

Experiment 5 Quiz 5

将a段和b段数据依次相加 , 结果存在cg段

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
assume cs:code
a segment
; define byte
db 1,2,3,4,5,6,7,8
; a segment occpied 8 bytes in memory
a ends

b segment
db 1,2,3,4,5,6,7,8
b ends

cg segment
db 0,0,0,0,0,0,0,0
cg ends

; 实现: 两次循环 分别用寄存器指向 a,c 及 b,c即可
code segment
; main
start:
; change ES point target
; DS point to C
mov ax,cg
mov ds,ax
; ES point to A
mov ax,a
mov es,ax
; bx - excursion address
mov bx,0
mov cx,8
s1:
mov al,es:[bx]
add [bx],al
inc bx
loop s1

; ES point to B
mov ax,b
mov es,ax
mov bx,0
mov cx,8
s2:
mov al,es:[bx]
add [bx],al
inc bx
loop s2

mov ah,4c
int 21h
code ends
end start

; outcome : 02 04 06 08 0A 0C 0E 10

Experiment 5 Quiz 6

用push指令将 A 段中的前8个字型数据逆向存储到B段中

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
; E5q6
assume cs:code
a segment
dw 1,2,3,4,5,6,7,8,9,0ah,0bh,0ch,0dh,0eh,0fh,0ffh
a ends

b segment
dw 0,0,0,0,0,0,0,0
b ends

code segment
; main
start:
mov ax,a
mov es,ax

mov ax,b
mov ss,ax ; b段 段地址 赋值给栈段 作栈空间
mov sp,10H

mov cx,8
mov bx,0
s:
push es:[bx]
add bx,2 ; what stored is a word
loop s

mov ah,4cH
int 21h

code ends
end start

运行结果

更灵活的定位内存地址的方法

如果一个问题的解决方案,使我们陷入一种矛盾之中。那么,很可能是我们考虑问题的出发点有了问题,或是说,我们起初运用的规律并不合适。

AND 与 OR 指令

与基本逻辑与 & 或 | 相同 , 只是汇编中皆按位进行运算

AND: 逻辑与指令, 按位进行与运算 , 通过该指令将操作对象相应位设为 0 ,其他位不变

OR: 逻辑或指令, 按位进行或运算 , 通过该指令将操作对象相应位设为 1 ,其他位不变

以字符形式给出的数据

在汇编程序中 , 可以使用形如 '....'的方式指明数据是以字符的形式给出的 , 编译器会将其转换为相应的 ASCII 码

E.G.

1
db 'unIX' ; 相当于 db 75H,6EH,49H,58H 与字符的 ascii码相对应

[ BX + IDATA ]

可以以此标题方式来灵活的表明一个内存单元 , [bx + idata]表示一个内存单元,偏移地址为 (bx) + idata

E.G.

1
2
3
4
5
6
7
mov ax,[bx+200] ; 将一个内存单元存入AX , 这个内存单元占2个字节, 存放一个字,偏移地址为 bx的值 + 200 , 段地址在ds中
; 即 [(ds)*16 + (bx) + 200]

; 也可写作:
mov ax,[200+bx]
mov ax,[bx].200
mov ax,200[bx]

SI (Source Index Register) & DI (Destination Index Register)

SIDI都是变址寄存器(Index Register)

它们主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。它们可作一般的存储器指针使用。在字符串操作指令的执行过程中,对它们有特定的要求,而且还具有特殊的功能。

Refer Baike.Baidu "变址寄存器"

不同寻址方式的灵活应用

不同的寻址方式:

[idata] 用一个常量来表示地址, 可用于直接定位一个内存单元.直接寻址

[bx]用一个变量来表示内存地址,可用于间接定位一个内存单元. 寄存器间接寻址

[bx+idata]用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元.寄存器相对寻址

[bx+si]用两个变量表示地址. 基址变址寻址

[bx+si+idata]用两个变量和一个常量表示地址。相对基址变址寻址

自顶向下逐渐可以用更灵活的寻址方式来定位一个内存单元的地址 。这使得数据在使用者的角度可以以结构化的角度看待。

关于数据的暂存

有时会遇到需要数据暂存的情况 , 如嵌套循环的CX , 作为计数器使用 . 由于寄存器的数量有限, 且每个程序使用的寄存器不一样 , 遂需要更为通用的方案

在不考虑使用寄存器的情况下

  • 可以将要暂存的数据存放到内存空间中,需要的时候再从内存单元中恢复. 但是当数据量多的时候,这样的存储方式需要记住哪个数据放到 了哪个单元。容易引起程序混乱。
  • 将需要暂存的数据存入栈中 , 利用压栈出栈与 栈顶指向 控制栈空间的数据

📌 实验六 - 灵活定位内存地址并修改值 - - 分析调试

将datasg段中的每个单词的前四个字母改为大写字母

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
; Experiment 6
; initialize regiter's point address
assume cs:codesg,ss:stacksg,ds:datasg

stacksg segment
dw 0,0,0,0,0,0,0,0 ; 16 bytes
stacksg ends

datasg segment
db '1. display '
db '2. brows '
db '3. replace '
db '4. modify '
datasg ends

codesg segment
; main
start:
; associate register with segment name
mov ax,stacksg
mov ss,ax
mov ax,datasg
mov ds,ax

mov bx,3 ; start point of string
mov sp,10H

mov cx,4
; external loop statement
s0:
mov si,0
push cx ; store cx(external) to stack temporary

; internal loop statement , refresh cx when external loop each time
mov cx,4
s1:
mov al,[bx+si]
and al,11011111B ; 5th number of Binary set to ZERO => capital letter
mov [bx+si],al
inc si
loop s1

; restore CX value
pop cx
add bx,10H ; point to next string

loop s0

mov ah,4CH
int 21h
codesg ends

end start

数据处理的两个基本问题

  • 要处理的数据在何处
  • 要处理的的数据有多长

于汇编中数据位置的表达

  • 立即数idata,在汇编中直接给出的数据
  • 寄存器 , 汇编指令中需给出相应的寄存器名
  • 段地址(SA) 和 偏移地址(EA) , 汇编中可用[x]形式给出EA , SA存在于某个段寄存器中

指明要处理的数据长度 - x ptr

8086CPU可处理两种长度的数据 Byte 和 Word ,在指令中需要指明需要处理的数据的尺寸.

  • 可以通过寄存器名来指明数据的尺寸.
  • 在没有寄存器名的情况下,可以使用形如X ptr 来指明内存单元的长度 , X 在汇编中可以为word或是byte .
    mov word ptr ds:[0],1 inc word ptr [bx] 这俩指令使用 word ptr 指明了指令访问的内存单元是一个字单元 .

注意有的指令就不要指明内存单元的长度了 , 像是push \ pop,其只能进行字操作.

div Instruction

即除法指令 , 需要注意几个问题

  • 除数 的两种情况 : 8bit & 16bit , 在一个寄存器或是内存单元中

  • 被除数 默认放在AX中 或是 DX 和 AX中 , 情况如下

    • 若除数为 8位 , 被除数则为 16位 , 此时被除数默认存放在 AX
    • 若除数为16位 , 被除数则为 32位 , 此时被除数被存放在 DX和AX中 ,DX中存放高16位 , AX中存放低16位
  • 对于计算结果的存储

    • 若除数为 8位 , 则 AL 将存储 除法操作的 , AH中存储的则是余数
    • 若除数为 16位 , 则 AX 中将存储 除法操作的 , DX 中存储的是余数

一般寄存器的存储主要是关于被除数 和 结果.

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; 1001 / 100 div calculation
; quizzes about div instruction

assume cs:codesg

codesg segment
; cuz the dividend's HEX = 3E9H , a 16bits register is suitable,it stored into AX register. And the divisor needs a 8bits register to store.

mov ax,3E9H
mov bl,64H
div bl ; al = qutient , ah = remainder

; result: AX = 010AH

mov ah,4CH
int 21h
codesg ends

end

#### 伪指令 - dd (define dword)

dd用来定义双字 型数据 , 占用32位 , 需要2个16位寄存器来分别存储其高低16为位

dup Operator

dup是一个操作符 , 和 db \ dw \ dd 一样, 其也是由编译器识别处理的符号 , 与他们(数据定义指令)配合使用 , 用以进行数据的重复.

示例如下:

1
2
3
4
5
6
db 3 dup (0) ; 定义了3个字节 , 他们的值都是 0  , 相当于db 0,0,0
; 或者这样解释 : 定义了字节"0",重复了3次, 相当于 db 0,0,0

db 3 dup (0,2,3) ; 定义了9个字节 , 他们的值都是 0,2,3 , 相当于 db 0,2,3,0,2,3,0,2,3
db 3 dup ('abc','ABC')
;定义了I8个字节,它们是abcABCabcABCabcABC,相当于db'abcABCabcABCabcABC。

可见,dup的使用格式如下:

  • db 重复的次数 dup(重复的字节型数据)
  • dw 重复的次数 dup(重复的字型数据)
  • dd 重复的次数 dup(重复的双字型数据)

📌 实验七 - 寻址方式在结构化数据访问中的应用 - - 分析调试

; 比较综合的一个实验, 融合之前的知识

将 data 段中的数据按照以下指定格式写入 table段

写到发狂.jpg

存储格式

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
80
81
82
83
84
85
86
87
88
89
90
91
assume cs:codesg

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'
;以上是表示21年的21个字符串,每个占4字节
dd 16,22,382,1356,2390,8000,16000,244486,50065,97479,140417,197514
dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
;以上是表示21年公司总收入的21个双字型数据,每个占四字节
dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
dw 11542,14430,15257,17800
;以上是表示21年公司雇员人数的21个字型数据
data ends

table segment
db 21 dup ('year sume ne ?? ') ; 正好十六字节
table ends

stacksg segment
db 10H dup (0)
stacksg ends

; write a year's all data in every loop
codesg segment
start:
; Initilize all data register
; es 指向 data , ds 指向 table
mov ax,data
mov es,ax
mov ax,table
mov ds,ax

mov ax,stacksg
mov ss,ax
mov sp,10H

mov bx,0 ; 索引table中的列(字节单位)
mov di,0 ; year 字节型 一个字符串4个字节
mov si,54H ; point to income in DATA SEGMENT 双字型
mov bp,168 ; point to staff in DATA SEGMENT 字型

mov cx,21
s:
; 同样也是一次处理一行的数据 , ax作中转寄存器
; bx 将每行看作一个结构型数据 , 用.idata 定位每个数据项(年份 \ 收入 \ 员工 等.. )
; NOTE copy year
mov ax,es:[di]
mov [bx].0,ax
add di,2 ; 注意x86架构采用的是小端存储模式(高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中) , 所以此指令执行将指向高位字节(高16位)

mov ax,es:[di]
mov [bx].2,ax ; idata更新 , 在内存中存储 高十六位
add di,2 ; 指向下一年
; 至此年份处理完毕

; 处理收入
mov ax,es:[si] ; si所指向的结构型数据 => 收入 , 此处同样以字的方式进行数据处理
push ax ; 将 ax 存入栈中, 以便后续进行人均收入的计算 , 低16位
mov [bx].5,ax
add si,2
; NOTE 处理高16位
mov ax,es:[si]
push ax ; 将其高16位入栈
mov [bx].7,ax
add si,2 ; 指向下一个收入数据
; 至此 收入部分处理结束

; NOTE 开始处理雇员结构数据 字型数据处理
mov ax,es:[bp]
mov [bx].0AH,ax
; 至此雇员结构数据处理完毕
pop dx
pop ax
div word ptr [bx].0AH
mov [bx].0dH,ax
add bp,2 ; 指向下一个雇员数据

add bx,10H ; 指向下一行

loop s

mov ah,4CH
int 21h

; average of income
; divisor(staff) is a Word(16bits) , therefore the dividend(total income) should be a dword(32bits)
; staff is divisor while total income is dividend
codesg ends

end start

转移指令及其原理

可以修改IP , 或同时修改 CS和IP的指令统称为 转移指令

针对8086CPU来说, 其转移行为有以下几类:

  • 只修改IP时 , 为段内转移
  • 同时修改CS和 IP 时 , 为段间转移

针对转移指令对IP修改范围的不同, 段内转移还分为: 短转移 (IP修改范围: -128 ~ 127)近转移 (IP修改范围: -32768 ~ 32767) (前后八位的区别吗)

操作符 Offset

操作符offset在汇编语言中是由编译器处理的符号 , 它的功能是取得标号的偏移地址

Jmp 指令

Jmp指令为无条件转移指令 , 可以只修改IP , 或同时修改 CS和IP

此指令需要给出两种信息:

  • 转移的距离 ( 段间转移 \ 段内 短转移 \ 段内 近转移 )
  • 转移的目的地址
依据位移进行转移的 JMP 指令 +. 补码

Jmp short 标号 ( 转到标号处执行指令 )

此格式的 JMP指令 实现的是段内短转移 , 对IP的修改范围为 -128 ~ 127 , 即向前转移最多越过 128个字节 , 向后转移最多越过 127个字节 , “Short” 说明了其进行得是短转移 ,间接说明Short类型为8位 , “标号”存在于代码段中 , 是指令转移的目的地

注意 在jmp short 标号指令所对应的机器码中,并不包含转移的 目的地址,而包含的是转移的位移, 此位移是编译器根据汇编指令中的’标号’计算出来的。(实际也就是目标地址起始偏移地址 - 当前指令起始偏移地址)

位移计算方法,请忽略图上标注

jmp short 标号的功能实际为 : (IP) = (IP) + 8位位移

位移本质上是一个偏移量 , 由 jmp 指令后一条指令的第一个字节地址(IP) [实际就是后一条指令的开始字节值] + 位移 = 目标地址偏移

  • 8位位移 = 标号处的地址 - jmp指令后的第一个字节的地址
  • short指明此处的位移为8位位移
  • 8位位移的范围为-128~127,用补码表示 , 8位的最大最小值
  • 8位位移由编译程序在编译时算出。

可得在 jmp short指令的机器指令中 , 包含的是跳转到目标指令的相对位置 偏移量 , 而不是转移的目标地址

补码 Review:

以8位数据表示 有符号数 , 其最高位表符号 , 1 为负 , 0 为正 . 用其他位表示数值

补码的基本思想:先确定用 0000 0000b0111 1111b表示0127,然后再用它们按位取反加1后的数据表示负数。

补码方案特性:

  • 最高位为1表示负数

  • 正数的补码取反加1后,为其对应的负数的补码:负数的补码取反加1后,为其绝对值。

由于一个负数的补码不太容易看出它所表示的数据 , 但是利用补码的特性将其 按位取反再加1 后可得知其 绝对值 , 则负数的值取相反数即可.

顺便一提 , 计算机都是以补码的形式存储数据的

段内近转移: short near ptr 标号

与短转移相似

  • 16位位移 = 标号处的地址 - jmp指令后的第一个字节的地址
  • near指明此处的位移为16位位移
  • 16位位移的范围为-32768~32767,用补码表示 , 8位的最大最小值
  • 16位位移由编译程序在编译时算出。

jmp far ptr 标号 实现的是段间转移 , 又称为转移

far ptr 指明了指令用标号的段地址和偏移地址修改CS和IP , 在机器码中 , 高地址部分存放段地址 , 低地址存放偏移地址 , 顺序遵循小端存储模式

Recommend to refer : 汇编编译器(MASM.exe) 对 jmp 指令的相关处理

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

格式 :

1
2
jmp 16bit reg    ; 格式
(ip) = (16bit reg) ; 功能
🛠️转移地址在内存中的jmp指令

两种格式 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.
jmp word ptr 内存单元地址 (段内转移)
; 从内存单元地址开始处存放着一个字 , 这个字的数据就是转移的 目的偏移地址(IP)

; e.g.
mov ax,0123h
mov ds:[0],ax
jmp word ptr ds:[0] ; 注意以字类数据长度 进行传输
; 执行后 (IP) = 0123H

jmp dword ptr 内存单元地址 (段间转移)
; 从内存开始处存放2个字 , 高地址处的字是转移目的地的 段地址 , 低地址的是 偏移地址


jcxz指令 (Jump if CX equals Zero)

所有的有条件跳转指令都属于[短转移](#依据位移进行转移的 JMP 指令 +. 补码)(段内短转移),其在机器码中包含的是转移的位移,而非目的地址

格式:

1
2
3
4
5
6
jcxz flag    
; 如果 (cx) == 0 , 则跳转到标号处 , 否则(cx!=0)继续向下执行
; flag = 标号

(类C描述)相当于:
if ((cx)==0) jmp short 标号;

loop 指令