⚠ 转载请注明出处:作者:ZobinHuang,更新日期:July 9 2021
本作品由 ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。
1. Segment 的理解
通过上一篇文章我们知道了:CPU 在访问内存时,会把内存中存储的数据分为三类:数据、代码和栈,分别用 DS:[], CS:IP, SS:SP 的分段访问方式进行访问。
但是在之前的例子中我们都是随意指定一段内存就当作代码段、数据段和栈段,实际上这样是非常不安全的。下面我们通过一些汇编代码例子来体会如何稍微安全一些地安排这三种不同的数据。
首先说明,这里是第一次展示汇编代码程序,因此必须对汇编代码的结构作出介绍。汇编代码由:指令、数据和伪指令构成。指令是最终被编译器转化为机器码的部分,数据不言自明,伪指令就是指导编译器编译的一些信息,最终不会被转义为机器码。关于各类伪指令的用法,将在下面代码的例子中进行介绍。
首先我们关心代码段,因为代码段是我们用于编写我们指令的地方,请看下面这段代码的说明,理解汇编程序的各种伪指令的意义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21# 将 code segment 的地址和 CS 寄存器关联起来 (MASM 编译器需要)
assume cs:code
# code segment 指明了代码段开始的位置 (CS 寄存器的初始值)
code segment
# start 指明了程序开始的位置 (也即 IP 寄存器的初始值)
# 编译后,编译器会把 start 的位置附在可执行文件的描述信息中,
# 在运行这个程序的时候就会根据这段描述信息去设置 CS 和 IP 寄存器,从而实现了 start 的入口的功能
start: mov bx, 0
mov ax, 0
mov cx, 8
# 程序退出
mov ax, 4C00H
int 21H
code ends
# 代码段结尾
# 程序结尾
end start
然后,通过 "dw" 关键字,我们可以尝试在代码段中安排我们自己定义的数据,比如使用下面的代码,我们能实现从 1 到 8 的累加功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19assume cs:code
code segment
# 在代码段中定义了数据
dw 1,2,3,4,5,6,7,8
start: mov bx, 0
mov ax, 0
mov cx, 8
add_number: add ax, cs:[bx] #这里 cs 实际上指向的就是代码段最开始的 dw 的部分
add bx, 2
loop add_number
mov ax, 4C00H
int 21H
code ends
end start
我们可以观察一下这段代码导致的内存分布如下,可以看见 code segment 的最初 16 个字节 (076A:0000-076A:000F) 是我们在代码段中定义的数据,我们真正的指令是在之后的 076A:0010 才开始的:

我们也可以通过类似的方式在代码段中定义我们的栈,比如是用下面的代码,将 8 个数 "push" 到了位于代码段中的栈中,然后又 "pop" 出来放进了原来的位置中:
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
35assume cs:code
code segment
dw 1,2,3,4,5,6,7,8
# 代码段中的栈
dw 0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0
start: mov bx, 0
mov ax, 0
mov cx, 8
mov ax, cs
mov ss, ax
mov sp, 48
push_data: push cs:[bx]
add bx, 2
loop push_data
mov bx, 0
mov cx, 8
pop_data: pop cs:[bx]
add bx, 2
loop pop_data
# 程序退出
mov ax, 4C00H
int 21H
code ends
end start
我们可以观察一下这段代码导致的内存分布如下,可以看见 code segment 的最初 16 个字节 (076A:0000-076A:000F) 是我们在代码段中定义的数据,之后的 32 字节 (076A:0010-076A:001F) 是我们定义的栈,我们真正的指令是在之后的 076A:0020 才开始的:

然而,把数据和栈存储在代码段中显然不是一个好的做法:(1) 这样安排会十分混乱,代码段中混杂着代码、数据和栈;(2) 数据和栈的引入导致了 IP 寄存器的初始值不为 0,使得 IP 寄存器的可变化范围缩小了,减小了可以存储的代码的数量。
实际上,我们应该把程序理解为描述对内存如何安排的文件。我们在编写汇编代码的时候,本质上就是在安排代码段、数据段和栈段中存储的内容,然后编译后交给操作系统运行。一个最简单最古老的操作系统 (i.e. 不考虑虚拟内存等机制) 拿到这个程序后,就会去设置 CS:IP, DS, SS:SP 为相应的值,然后我们的程序就能够在机器上运行起来。
因此,一个更加合理的程序安排应该如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# 将 code segment 的地址和 CS 寄存器关联起来
# 将 data segment 的地址和 DS 寄存器关联起来
# 将 stack segment 的地址和 SS 寄存器关联起来
assume cs:code, ds:data, ss:stack
# 数据段
data segment
data ends
# 栈段
stack segment
stack ends
# 代码段
code segment
start:
code ends
end start
2. 观察内存中的 Segment
下面我们改写我们上面写过的一段针对栈的 "push" 和 "pop" 操作的代码,如下所示:
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
47assume cs:code,ds:data,ss:stack
data segment
dw 1,2,3,4,5,6,7,8
data ends
stack segment
dw 0,0,0,0,0,0,0,0
dw 0,0,0,0,0,0,0,0
stack ends
code segment
start: mov bx, 0
mov cx, 8
# 配置 ds 寄存器,指向数据段
# assume 不会配置 ds 寄存器
# assume 伪指令是用于在编译阶段让编译器知道各个段对应的段基址寄存器是哪个
mov ax, data
mov ds, ax
# 配置 ss:sp 寄存器,指向栈段
# assume 不会配置 ss:sp 寄存器
# assume 伪指令是用于在编译阶段让编译器知道各个段对应的段基址寄存器是哪个
mov ax, stack
mov ss, ax
mov sp, 32
push_data: push ds:[bx]
add bx, 2
loop push_data
mov bx, 0
mov cx, 8
pop_data: pop ds:[bx]
add bx, 2
loop pop_data
; 程序退出
mov ax, 4C00H
int 21H
code ends
end start
下图展示了程序还未开始运行时的内存情况,可以看见我们定义的数据段和栈段已经分别在 [076A:0000-076A:000F] 和 [076A:0010-076A:002F] 就位了,并且我们的代码段也已经位于 [076A:0030-...]。CS:IP 寄存器已经指向了我们的代码段所在的位置,这是由于我们的 start 伪指令将程序入口位置包装在了程序的描述信息中,使得 CPU 的 CS:IP 寄存器组在开始运行这段代码时得以被配置。但是我们发现 DS 和 SS:SP 寄存器并没有被配置,因此我们在上面程序的开始部分也对 DS 和 SS:SP 寄存器做了配置,使得它们能分别瞄准数据段和栈段。

另外值得一提的是,内存中的段是 16-bytes 对齐的。假如我们在 data segment 中声明了 20-bytes 的数据,那么实际编译过后我们的 data segment 会占用 32-bytes 的空间。
另外,在段中声明数据时,我们可以使用 "dup" 伪指令来简化,如下所示,我们可以通过 "db"/"dw"/"dd" 来声明每个单元的长度,一个常数(e.g. 下面的 100)来声明单元的个数,括号内的值来声明各个单元的初始化值:
1
db 100 dup (0)
3. 我们的地址代号如何被编译器和操作系统解读?
通过上文的分析,我们知道了,我们在编写程序的时候,要有意识地把代码分为代码段,栈段和数据段来管理,这样一来会更加地合理和安全。当我们在代码中声明在数据段中存储的数据,在代码段中存储的代码,在栈段中存储的栈时,在这份程序被运行起来的时候,各个段中的内容就必然在内存中被准备好了,这样才能保证我们的程序被正确地运行起来。
这样一来我们不禁好奇:我们程序的各个段的内容是怎么被 load 到内存中的呢?回顾之前我们写过的汇编程序,我们从来没有在代码中写过一个绝对的物理地址,我们从来都是使用代号的方式来表明一个我们想访问的基地址,然后基于这个代号开展我们对段中数据的访问。由于我们是在 CPU 的 实模式 下编写汇编代码,我们使用 段基地址+偏移地址 组合出来的地址就是 8086 CPU 实际送上内存地址线的物理地址。那么,这些代号是怎么被转化为实际的物理地址的呢?

在下面的代码中,我们看到的是由 NASM 编译器编译过后生成的 .lst 文件。在左边的第二列 "汇编地址" 中,我们可以观察到,编译器会为每一条有意义的汇编指令都分配一个汇编地址,就仿佛它们是在真实的内存中存储着一样。基于这个汇编地址,编译器将我们在程序中所使用的所有代号都替换为相应位置的内存地址。并且,只有当程序装入内存后,IP 寄存器同样被设置为从 0x0000 开始增长,才能保证基于汇编地址编译生成的机器码是合法有效的,因为汇编指令也是从 0x0000 开始增长的。如上图所示,当我们的程序被装入 CS:IP = 6000H:0000H 的内存后,由于 IP 寄存器此时是从 0x0000H 开始递增的,因此我们程序中所包含的汇编地址依然有效。
当我们能够预见到我们的程序装入内存后 IP 寄存器不会为 0x0000H 开始增长时,我们就应该在程序中使用代号的地方加上 IP 寄存器的初始值。如下面的代码所示,下面这段代码其实是会被放在硬盘第一个逻辑扇区的代码,会被 ROM-BIOS 拷贝到 0x0000:0x7C00 的地方开始运行。因此,我们的 IP 寄存器一开始并不会为 0x0000。所以,你能看见,我们在 Line 47, 52 等地方在使用代号寻址的时候,我们都加上了 IP 初始偏移量 0x7C00。当然,这是一个比较进阶的写法。我们在之后讨论实模式的文章中,并不会去深究这一点。这里提一嘴主要是为了让读者理解我们所编写的汇编代码中设计的地址信息如何被编译器和操作系统所解读的。
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105# 行号 汇编地址 机器码 汇编代码
# -------------------------------------------------------------------------------------------------
1 # 代码清单5-1
2 # 文件名:c05_mbr.asm
3 # 文件说明:硬盘主引导扇区代码
4 # 创建日期:2011-3-31 21:15
5
6 00000000 B800B8 mov ax,0xb800 # 指向文本模式的显示缓冲区
7 00000003 8EC0 mov es,ax
8
9 # 以下显示字符串"Label offset:"
10 00000005 26C60600004C mov byte [es:0x00],'L'
11 0000000B 26C606010007 mov byte [es:0x01],0x07
12 00000011 26C606020061 mov byte [es:0x02],'a'
13 00000017 26C606030007 mov byte [es:0x03],0x07
14 0000001D 26C606040062 mov byte [es:0x04],'b'
15 00000023 26C606050007 mov byte [es:0x05],0x07
16 00000029 26C606060065 mov byte [es:0x06],'e'
17 0000002F 26C606070007 mov byte [es:0x07],0x07
18 00000035 26C60608006C mov byte [es:0x08],'l'
19 0000003B 26C606090007 mov byte [es:0x09],0x07
20 00000041 26C6060A0020 mov byte [es:0x0a],' '
21 00000047 26C6060B0007 mov byte [es:0x0b],0x07
22 0000004D 26C6060C006F mov byte [es:0x0c],"o"
23 00000053 26C6060D0007 mov byte [es:0x0d],0x07
24 00000059 26C6060E0066 mov byte [es:0x0e],'f'
25 0000005F 26C6060F0007 mov byte [es:0x0f],0x07
26 00000065 26C606100066 mov byte [es:0x10],'f'
27 0000006B 26C606110007 mov byte [es:0x11],0x07
28 00000071 26C606120073 mov byte [es:0x12],'s'
29 00000077 26C606130007 mov byte [es:0x13],0x07
30 0000007D 26C606140065 mov byte [es:0x14],'e'
31 00000083 26C606150007 mov byte [es:0x15],0x07
32 00000089 26C606160074 mov byte [es:0x16],'t'
33 0000008F 26C606170007 mov byte [es:0x17],0x07
34 00000095 26C60618003A mov byte [es:0x18],':'
35 0000009B 26C606190007 mov byte [es:0x19],0x07
36
37 000000A1 B8[2E01] mov ax,number # 取得标号number的偏移地址
38 000000A4 BB0A00 mov bx,10
39
40 # 设置数据段的基地址
41 000000A7 8CC9 mov cx,cs
42 000000A9 8ED9 mov ds,cx
43
44 # 求个位上的数字
45 000000AB BA0000 mov dx,0
46 000000AE F7F3 div bx
47 000000B0 8816[2E7D] mov [0x7c00+number+0x00],dl # 保存个位上的数字
48
49 # 求十位上的数字
50 000000B4 31D2 xor dx,dx
51 000000B6 F7F3 div bx
52 000000B8 8816[2F7D] mov [0x7c00+number+0x01],dl # 保存十位上的数字
53
54 # 求百位上的数字
55 000000BC 31D2 xor dx,dx
56 000000BE F7F3 div bx
57 000000C0 8816[307D] mov [0x7c00+number+0x02],dl # 保存百位上的数字
58
59 # 求千位上的数字
60 000000C4 31D2 xor dx,dx
61 000000C6 F7F3 div bx
62 000000C8 8816[317D] mov [0x7c00+number+0x03],dl # 保存千位上的数字
63
64 # 求万位上的数字
65 000000CC 31D2 xor dx,dx
66 000000CE F7F3 div bx
67 000000D0 8816[327D] mov [0x7c00+number+0x04],dl # 保存万位上的数字
68
69 # 以下用十进制显示标号的偏移地址
70 000000D4 A0[327D] mov al,[0x7c00+number+0x04]
71 000000D7 0430 add al,0x30
72 000000D9 26A21A00 mov [es:0x1a],al
73 000000DD 26C6061B0004 mov byte [es:0x1b],0x04
74
75 000000E3 A0[317D] mov al,[0x7c00+number+0x03]
76 000000E6 0430 add al,0x30
77 000000E8 26A21C00 mov [es:0x1c],al
78 000000EC 26C6061D0004 mov byte [es:0x1d],0x04
79
80 000000F2 A0[307D] mov al,[0x7c00+number+0x02]
81 000000F5 0430 add al,0x30
82 000000F7 26A21E00 mov [es:0x1e],al
83 000000FB 26C6061F0004 mov byte [es:0x1f],0x04
84
85 00000101 A0[2F7D] mov al,[0x7c00+number+0x01]
86 00000104 0430 add al,0x30
87 00000106 26A22000 mov [es:0x20],al
88 0000010A 26C606210004 mov byte [es:0x21],0x04
89
90 00000110 A0[2E7D] mov al,[0x7c00+number+0x00]
91 00000113 0430 add al,0x30
92 00000115 26A22200 mov [es:0x22],al
93 00000119 26C606230004 mov byte [es:0x23],0x04
94
95 0000011F 26C606240044 mov byte [es:0x24],'D'
96 00000125 26C606250007 mov byte [es:0x25],0x07
97
98 0000012B E9FDFF infi: jmp near infi # 无限循环
99
100 0000012E 0000000000 number db 0,0,0,0,0
101
102 00000133 00<rept> times 203 db 0
103 000001FE 55AA db 0x55,0xaa
但是!细心的读者会发现 0x0000:0x7C00 所指示的内存单元,又可以表示为 0x07C0:0x0000。如下图所示:

分析我们上面为什么要在地址代号的地方加上 0x7C00 的原因,是因为我们一直使用 0x0000:0x7C00 的角度去定位我们的程序,但是如果我们将角度切换为 0x07C0:0x0000,则我们既可以访问完全一样的内存位置,又可以避免这种别扭的 "+0x7C00" 的写法。
现在,我们锁定问题的根源在于 Line 41 ~ 42,我们不应该把 DS 寄存器的值设置为 CS 的值 (i.e. 0x0000H),而是应该设置为 0x07C0。这样一来,我们使用 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
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105# 行号 汇编地址 机器码 汇编代码
# -------------------------------------------------------------------------------------------------
1 # 代码清单5-1
2 # 文件名:c05_mbr.asm
3 # 文件说明:硬盘主引导扇区代码
4 # 创建日期:2011-3-31 21:15
5
6 00000000 B800B8 mov ax,0xb800 # 指向文本模式的显示缓冲区
7 00000003 8EC0 mov es,ax
8
9 # 以下显示字符串"Label offset:"
10 00000005 26C60600004C mov byte [es:0x00],'L'
11 0000000B 26C606010007 mov byte [es:0x01],0x07
12 00000011 26C606020061 mov byte [es:0x02],'a'
13 00000017 26C606030007 mov byte [es:0x03],0x07
14 0000001D 26C606040062 mov byte [es:0x04],'b'
15 00000023 26C606050007 mov byte [es:0x05],0x07
16 00000029 26C606060065 mov byte [es:0x06],'e'
17 0000002F 26C606070007 mov byte [es:0x07],0x07
18 00000035 26C60608006C mov byte [es:0x08],'l'
19 0000003B 26C606090007 mov byte [es:0x09],0x07
20 00000041 26C6060A0020 mov byte [es:0x0a],' '
21 00000047 26C6060B0007 mov byte [es:0x0b],0x07
22 0000004D 26C6060C006F mov byte [es:0x0c],"o"
23 00000053 26C6060D0007 mov byte [es:0x0d],0x07
24 00000059 26C6060E0066 mov byte [es:0x0e],'f'
25 0000005F 26C6060F0007 mov byte [es:0x0f],0x07
26 00000065 26C606100066 mov byte [es:0x10],'f'
27 0000006B 26C606110007 mov byte [es:0x11],0x07
28 00000071 26C606120073 mov byte [es:0x12],'s'
29 00000077 26C606130007 mov byte [es:0x13],0x07
30 0000007D 26C606140065 mov byte [es:0x14],'e'
31 00000083 26C606150007 mov byte [es:0x15],0x07
32 00000089 26C606160074 mov byte [es:0x16],'t'
33 0000008F 26C606170007 mov byte [es:0x17],0x07
34 00000095 26C60618003A mov byte [es:0x18],':'
35 0000009B 26C606190007 mov byte [es:0x19],0x07
36
37 000000A1 B8[2E01] mov ax,number # 取得标号number的偏移地址
38 000000A4 BB0A00 mov bx,10
39
40 # 设置数据段的基地址
41 000000A7 8CC9 mov cx,0x07c0
42 000000A9 8ED9 mov ds,cx
43
44 # 求个位上的数字
45 000000AB BA0000 mov dx,0
46 000000AE F7F3 div bx
47 000000B0 8816[2E7D] mov [number+0x00],dl # 保存个位上的数字
48
49 # 求十位上的数字
50 000000B4 31D2 xor dx,dx
51 000000B6 F7F3 div bx
52 000000B8 8816[2F7D] mov [number+0x01],dl # 保存十位上的数字
53
54 # 求百位上的数字
55 000000BC 31D2 xor dx,dx
56 000000BE F7F3 div bx
57 000000C0 8816[307D] mov [number+0x02],dl # 保存百位上的数字
58
59 # 求千位上的数字
60 000000C4 31D2 xor dx,dx
61 000000C6 F7F3 div bx
62 000000C8 8816[317D] mov [number+0x03],dl # 保存千位上的数字
63
64 # 求万位上的数字
65 000000CC 31D2 xor dx,dx
66 000000CE F7F3 div bx
67 000000D0 8816[327D] mov [number+0x04],dl # 保存万位上的数字
68
69 # 以下用十进制显示标号的偏移地址
70 000000D4 A0[327D] mov al,[number+0x04]
71 000000D7 0430 add al,0x30
72 000000D9 26A21A00 mov [es:0x1a],al
73 000000DD 26C6061B0004 mov byte [es:0x1b],0x04
74
75 000000E3 A0[317D] mov al,[number+0x03]
76 000000E6 0430 add al,0x30
77 000000E8 26A21C00 mov [es:0x1c],al
78 000000EC 26C6061D0004 mov byte [es:0x1d],0x04
79
80 000000F2 A0[307D] mov al,[number+0x02]
81 000000F5 0430 add al,0x30
82 000000F7 26A21E00 mov [es:0x1e],al
83 000000FB 26C6061F0004 mov byte [es:0x1f],0x04
84
85 00000101 A0[2F7D] mov al,[number+0x01]
86 00000104 0430 add al,0x30
87 00000106 26A22000 mov [es:0x20],al
88 0000010A 26C606210004 mov byte [es:0x21],0x04
89
90 00000110 A0[2E7D] mov al,[number+0x00]
91 00000113 0430 add al,0x30
92 00000115 26A22200 mov [es:0x22],al
93 00000119 26C606230004 mov byte [es:0x23],0x04
94
95 0000011F 26C606240044 mov byte [es:0x24],'D'
96 00000125 26C606250007 mov byte [es:0x25],0x07
97
98 0000012B E9FDFF infi: jmp near infi # 无限循环
99
100 0000012E 0000000000 number db 0,0,0,0,0
101
102 00000133 00<rept> times 203 db 0
103 000001FE 55AA db 0x55,0xaa