Based on Assembly Edition 4

Basic Part

Memory Segmentation in 8086 Microprocessor

存储器 、存储单元

存储器 也就是我们平时所说的内存 。 负责为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),

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

内存地址空间

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个字节组成 这两个字节分别称为这个字的高位字节低位字节.

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

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

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

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

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

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

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

地址加法器通过物理地址 = 段地址 × 16(d) + 偏移地址的方式合成物理地址


段地址 × 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.

程序执行过程与跟踪

程序由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 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. 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~16总共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]