实验答案托管在我的Github

用了一周的时间读完了《深入理解计算机系统》的第三章:程序的机器级表示。对于x86_64汇编及其逆向工程有了一定的了解。于2017年4月16日完成了其第二个Lab - Bomb Lab.

实验简介

Bomb Lab - Defusing a Binary Bomb主要是关于反汇编的实验,对应于书本的第三章:程序的机器级表示。

本实验非常的有趣。实验给定了一个二进制程序Bomb(炸弹)。这个炸弹由若干阶段组成,每个阶段都要求你在标准输入流中输入一个字符串,如果字符串正确,那么该阶段就会被解除并且程序会进入下一个阶段。如果字符串错误,那么炸弹会爆炸,并且退出。当每个阶段都被解除后,整个炸弹将会被解除。

在正式的实验中,每个学生将会从服务器处得到一个独一无二的炸弹,解除每个炸弹所需要的字符串都是不同的。同时,引爆炸弹将会导致扣分。当实验结束后,可以从服务器的计分板上看到每个学生的成绩。
本实验主要要求学生能够理解汇编代码,同时要求学生能学会使用一个Debugger(调试器)。

本实验提供的答案仅针对官网给出的自学者讲义中的炸弹

实验提示

  • 学会使用一个debugger(调试器),在本次实验中,使用GDB - GNU Debugger
  • 使用objdump -t得到bomb的符号表。符号表包含了bomb中所有函数,全局变量,以及调用的函数的名字及地址。
  • 使用objdump -d得到bomb的反汇编结果。但是注意,系统级别的函数调用将会以被加密的形式出现在反汇编代码中,如果需要知道这些信息,需要在gdb中对相应的函数进行反汇编。

实验过程及分析

实验准备

  1. 熟悉GDB的使用,并且使用objdump -t bomb > bomb-symboltable生成bomb的符号表,再使用objdump -d bomb > bomb-disassemble生成bomb的反汇编文件。
  2. 确保即时输入了错误的字符串炸弹也不会爆炸。观察bomb的符号表可以发现函数explode_bomb,推测该函数的作用是引爆炸弹,因此在GDB中使用break explode_bomb为该函数添加断点。这样,我们可以在bomb爆炸前提前截获并处理。
  3. 观察符号表可以看到phase_1到phase_6这几个函数以及其他的相关辅助函数,我们要做的就是给每一个阶段的函数添加断点,反汇编,并且理解这些函数的作用,并推测出答案。

实验过程

主函数 - main

首先在bomb-disassemble中观察main的反汇编代码,如下:

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
0000000000400da0 <main>:
400da0: 53 push %rbx
400da1: 83 ff 01 cmp $0x1,%edi
400da4: 75 10 jne 400db6 <main+0x16>
400da6: 48 8b 05 9b 29 20 00 mov 0x20299b(%rip),%rax # 603748 <stdin@@GLIBC_2.2.5>
400dad: 48 89 05 b4 29 20 00 mov %rax,0x2029b4(%rip) # 603768 <infile>
400db4: eb 63 jmp 400e19 <main+0x79>
400db6: 48 89 f3 mov %rsi,%rbx
400db9: 83 ff 02 cmp $0x2,%edi
400dbc: 75 3a jne 400df8 <main+0x58>
400dbe: 48 8b 7e 08 mov 0x8(%rsi),%rdi
400dc2: be b4 22 40 00 mov $0x4022b4,%esi
400dc7: e8 44 fe ff ff callq 400c10 <fopen@plt>
400dcc: 48 89 05 95 29 20 00 mov %rax,0x202995(%rip) # 603768 <infile>
400dd3: 48 85 c0 test %rax,%rax
400dd6: 75 41 jne 400e19 <main+0x79>
400dd8: 48 8b 4b 08 mov 0x8(%rbx),%rcx
400ddc: 48 8b 13 mov (%rbx),%rdx
400ddf: be b6 22 40 00 mov $0x4022b6,%esi
400de4: bf 01 00 00 00 mov $0x1,%edi
400de9: e8 12 fe ff ff callq 400c00 <__printf_chk@plt>
400dee: bf 08 00 00 00 mov $0x8,%edi
400df3: e8 28 fe ff ff callq 400c20 <exit@plt>
400df8: 48 8b 16 mov (%rsi),%rdx
400dfb: be d3 22 40 00 mov $0x4022d3,%esi
400e00: bf 01 00 00 00 mov $0x1,%edi
400e05: b8 00 00 00 00 mov $0x0,%eax
400e0a: e8 f1 fd ff ff callq 400c00 <__printf_chk@plt>
400e0f: bf 08 00 00 00 mov $0x8,%edi
400e14: e8 07 fe ff ff callq 400c20 <exit@plt>
400e19: e8 84 05 00 00 callq 4013a2 <initialize_bomb>
400e1e: bf 38 23 40 00 mov $0x402338,%edi
400e23: e8 e8 fc ff ff callq 400b10 <puts@plt>
400e28: bf 78 23 40 00 mov $0x402378,%edi
400e2d: e8 de fc ff ff callq 400b10 <puts@plt>
400e32: e8 67 06 00 00 callq 40149e <read_line>
400e37: 48 89 c7 mov %rax,%rdi
400e3a: e8 a1 00 00 00 callq 400ee0 <phase_1>
400e3f: e8 80 07 00 00 callq 4015c4 <phase_defused>
400e44: bf a8 23 40 00 mov $0x4023a8,%edi
400e49: e8 c2 fc ff ff callq 400b10 <puts@plt>
400e4e: e8 4b 06 00 00 callq 40149e <read_line>
400e53: 48 89 c7 mov %rax,%rdi
400e56: e8 a1 00 00 00 callq 400efc <phase_2>
400e5b: e8 64 07 00 00 callq 4015c4 <phase_defused>
400e60: bf ed 22 40 00 mov $0x4022ed,%edi
400e65: e8 a6 fc ff ff callq 400b10 <puts@plt>
400e6a: e8 2f 06 00 00 callq 40149e <read_line>
400e6f: 48 89 c7 mov %rax,%rdi
400e72: e8 cc 00 00 00 callq 400f43 <phase_3>
400e77: e8 48 07 00 00 callq 4015c4 <phase_defused>
400e7c: bf 0b 23 40 00 mov $0x40230b,%edi
400e81: e8 8a fc ff ff callq 400b10 <puts@plt>
400e86: e8 13 06 00 00 callq 40149e <read_line>
400e8b: 48 89 c7 mov %rax,%rdi
400e8e: e8 79 01 00 00 callq 40100c <phase_4>
400e93: e8 2c 07 00 00 callq 4015c4 <phase_defused>
400e98: bf d8 23 40 00 mov $0x4023d8,%edi
400e9d: e8 6e fc ff ff callq 400b10 <puts@plt>
400ea2: e8 f7 05 00 00 callq 40149e <read_line>
400ea7: 48 89 c7 mov %rax,%rdi
400eaa: e8 b3 01 00 00 callq 401062 <phase_5>
400eaf: e8 10 07 00 00 callq 4015c4 <phase_defused>
400eb4: bf 1a 23 40 00 mov $0x40231a,%edi
400eb9: e8 52 fc ff ff callq 400b10 <puts@plt>
400ebe: e8 db 05 00 00 callq 40149e <read_line>
400ec3: 48 89 c7 mov %rax,%rdi
400ec6: e8 29 02 00 00 callq 4010f4 <phase_6>
400ecb: e8 f4 06 00 00 callq 4015c4 <phase_defused>
400ed0: b8 00 00 00 00 mov $0x0,%eax
400ed5: 5b pop %rbx
400ed6: c3 retq

结合讲义中的bomb.c可以得出,main函数每次调用read_line从标准输入流中读入一行字符串,并将其返回值(即字符串的首地址)设置为第一个参数,然后依次调用phase_1到phase_6。

在每一个phase函数内部检查输入的字符串是否正确,如果不正确,则调用explode_bomb函数引爆bomb;否则则返回,返回之后由主函数再继续调用phase_defused解除该阶段。

因此,在每一个阶段中,我们关注的重点应当是phase函数内部以及phase函数所调用的其他函数。

阶段1 - phase_1

在bomb-disassemble中观察phase_1的反汇编代码,如下:

1
2
3
4
5
6
7
8
9
0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi #设置第二个参数为指针
400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal> #调用strings_not_equal函数
400eee: 85 c0 test %eax,%eax
400ef0: 74 05 je 400ef7 <phase_1+0x17>
400ef2: e8 43 05 00 00 callq 40143a <explode_bomb> #若返回值不为0则引爆bomb
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 retq

可以看出,phase_1调用了strings_not_equal这个函数,并为该函数配置了第二个参数0x402400,并判断该函数的返回值是否为0,如果为0则返回,否则引爆bomb。

从函数名可以推测,该函数的作用是判断两个字符串是否不相同,如果不相同,则返回非0值,相同则返回0。

还可以推测,该函数的第一个参数%rdi中存放了我们输入的字符串的首地址,而第二个参数%rsi指向了待比较的字符串的首地址——也就是阶段1的答案。

下面在GDB中验证推测。使用GDB运行bomb,为phase_1和explode_bomb添加断点。然后输入测试字符串”Test”,进入phase_1后用print分别打印输出%rdi和%rsi所指向的字符串。

首先是设置断点的相关命令及结果:

1
2
3
4
(gdb) break explode_bomb
Breakpoint 1 at 0x40143a
(gdb) break phase_1
Breakpoint 2 at 0x400ee0

然后是运行程序及输入测试字符串的相关命令及结果:
1
2
3
4
5
6
7
(gdb) run
Starting program: /home/blackdragon/CSAPPLabs/BombLab/bomb
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Test

Breakpoint 2, 0x0000000000400ee0 in phase_1 ()

然后我们查看%rdi所指向的字符串:
1
2
(gdb) print (char *)($rdi)
$1 = 0x603780 <input_strings> "Test"

发现%rdi果然指向了我们输入的字符串。然后我们单步执行程序至调用strings_not_equal函数之前,查看%rsi所指向的字符串:
1
2
(gdb) print (char *)($rsi)
$2 = 0x402400 "Border relations with Canada have never been better."

我们已经得到了phase_1的答案,就是”Border relations with Canada have never been better.”

下面我们重新执行bomb并测试,输出”Phase 1 defused. How about the next one?”,phase_1解决。

阶段2 - phase_2

在bomb-disassemble中观察phase_2的反汇编代码,如下:

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
0000000000400efc <phase_2>:
400efc: 55 push %rbp
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp
400f02: 48 89 e6 mov %rsp,%rsi
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers> #从输入的字符串中读入6个数字
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp)
400f0e: 74 20 je 400f30 <phase_2+0x34>
400f10: e8 25 05 00 00 callq 40143a <explode_bomb> #如果第一个数字不为1则引爆炸弹
400f15: eb 19 jmp 400f30 <phase_2+0x34>
400f17: 8b 43 fc mov -0x4(%rbx),%eax
400f1a: 01 c0 add %eax,%eax
# 计算前一个元素*2
400f1c: 39 03 cmp %eax,(%rbx)
400f1e: 74 05 je 400f25 <phase_2+0x29>
400f20: e8 15 05 00 00 callq 40143a <explode_bomb> #若当前元素不等于前一个元素*2则引爆bomb
400f25: 48 83 c3 04 add $0x4,%rbx
400f29: 48 39 eb cmp %rbp,%rbx
400f2c: 75 e9 jne 400f17 <phase_2+0x1b>
# 指向下一个元素
400f2e: eb 0c jmp 400f3c <phase_2+0x40>
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp #设置b和bEnd
400f3a: eb db jmp 400f17 <phase_2+0x1b>
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 retq

可以看出,在phase_2的一开始调用了read_six_numbers这个函数,初步推测这是用来从输入字符串中读取六个数字的函数,其将输入字符串的地址作为第一个参数,将栈顶指针%rsp作为第二个参数,下面我们查看read_six_numbers的反汇编代码,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
000000000040145c <read_six_numbers>:
40145c: 48 83 ec 18 sub $0x18,%rsp
401460: 48 89 f2 mov %rsi,%rdx
401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx
401467: 48 8d 46 14 lea 0x14(%rsi),%rax
40146b: 48 89 44 24 08 mov %rax,0x8(%rsp)
401470: 48 8d 46 10 lea 0x10(%rsi),%rax
401474: 48 89 04 24 mov %rax,(%rsp)
401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9
40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8
401480: be c3 25 40 00 mov $0x4025c3,%esi
401485: b8 00 00 00 00 mov $0x0,%eax
# 为sscanf配置参数
40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt>
# 调用sscanf(input, "%d %d %d %d %d %d", &a[0], &a[1], &a[2], &a[3], &a[4], &a[5]);
40148f: 83 f8 05 cmp $0x5,%eax
401492: 7f 05 jg 401499 <read_six_numbers+0x3d>
401494: e8 a1 ff ff ff callq 40143a <explode_bomb> #若读取成功的数字数小于6则引爆bomb
401499: 48 83 c4 18 add $0x18,%rsp
40149d: c3 retq

可以看出,该函数在0x401460-0x401480为函数的调用构造了参数,在0x401485将返回值置0,并在0x40148a处调用了sscanf函数。并且检查sscanf函数的返回值(读取成功的变量个数),如果返回值<=5,则引爆bomb,否则则正确返回。

下面我们重点观察调用函数的参数构造过程。%rdi是第1个参数,指向了输入的字符串,推测第2个参数%rsi应该指向了格式化字符串的首地址,且该字符串为”%d %d %d %d %d %d”。%rdx是第3个参数,为%rsi+0,%rcx是第4个参数,为%rsi+4,%r8是第5个参数,为%rsi+8,%r9是第6个参数,为%rsi+12。(%rsp)是第7个参数,为%rsi+16,(%rsp+8)是第8个参数,为%rsi+20。

可以归纳出read_six_numbers接收2个参数,其中第1个参数为输入字符串的首地址,第2个参数为6元素的int型数组的首地址,该函数从输入字符串中读取6个数字,并且将这6个数字存入第2个参数所指向的int型数组中。但如果读取的数字不足6个,则会引爆bomb。

然后我们回到phase_2函数中,将phase_2函数逆向,可以得到如下的结果:

1
2
3
4
5
6
7
8
9
10
11
12
void phase_2(char * input) {
int a[6];
read_six_numbers(input, a);
if (a[0] == 1) explode_bomb();
int * b = &a[1], * bEnd = &a[6];
do {
int val = (*(b-1)) * 2;
if (*b != val) explode_bomb();
b++;
} while (b != bEnd);
return;
}

从上述的代码可以明显地看出编译器对于定长数组的优化,编译器使用了指针间接引用替代了数组引用。
上述代码依次检查了读入的6个数字,第1个数字必须为1,从第2个数字开始,每一个数字都必须是上一个数字的两倍,否则将会引爆bomb。显然,phase_2的答案为”1 2 4 8 16 32”。

下面我们在GDB中验证得出的结果。我们为explode_bomb、phase_2和read_six_numbers设置断点,在phase_2中输入”1 2 4 8 16 32”,并且在进入read_six_numbers函数后观察%rsi所指向的字符串是否为”%d %d %d %d %d %d”。这里为了方便起见,我们将phase_1的结果写入test.txt并读入。相关命令与结果如下:

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
(gdb) break explode_bomb
Breakpoint 1 at 0x40143a
(gdb) break phase_2
Breakpoint 2 at 0x400efc
(gdb) break read_six_numbers
Breakpoint 3 at 0x40145c
(gdb) run test.txt
Starting program: /home/blackdragon/CSAPPLabs/BombLab/bomb test.txt
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
1 2 4 8 16 32

Breakpoint 2, 0x0000000000400efc in phase_2 ()
(gdb) continue
Continuing.

Breakpoint 3, 0x000000000040145c in read_six_numbers ()
(gdb) stepi 10
0x0000000000401485 in read_six_numbers ()
(gdb) print (char *)($rsi)
$1 = 0x4025c3 "%d %d %d %d %d %d"
(gdb) continue
Continuing.
That's number 2. Keep going!

阶段3 - phase_3

在bomb-disassemble中观察phase_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
33
34
35
36
37
38
0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
400f51: be cf 25 40 00 mov $0x4025cf,%esi
400f56: b8 00 00 00 00 mov $0x0,%eax
400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt>
# 从输入的字符串中读取两个数字 并存入局部变量中
400f60: 83 f8 01 cmp $0x1,%eax
400f63: 7f 05 jg 400f6a <phase_3+0x27>
400f65: e8 d0 04 00 00 callq 40143a <explode_bomb> # 若读取成功的数字不足两个 则引爆bomb
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp)
400f6f: 77 3c ja 400fad <phase_3+0x6a> # 若第一个数字(无符号)大于7 则引爆bomb
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8)
400f7c: b8 cf 00 00 00 mov $0xcf,%eax
400f81: eb 3b jmp 400fbe <phase_3+0x7b>
400f83: b8 c3 02 00 00 mov $0x2c3,%eax
400f88: eb 34 jmp 400fbe <phase_3+0x7b>
400f8a: b8 00 01 00 00 mov $0x100,%eax
400f8f: eb 2d jmp 400fbe <phase_3+0x7b>
400f91: b8 85 01 00 00 mov $0x185,%eax
400f96: eb 26 jmp 400fbe <phase_3+0x7b>
400f98: b8 ce 00 00 00 mov $0xce,%eax
400f9d: eb 1f jmp 400fbe <phase_3+0x7b>
400f9f: b8 aa 02 00 00 mov $0x2aa,%eax
400fa4: eb 18 jmp 400fbe <phase_3+0x7b>
400fa6: b8 47 01 00 00 mov $0x147,%eax
400fab: eb 11 jmp 400fbe <phase_3+0x7b>
400fad: e8 88 04 00 00 callq 40143a <explode_bomb>
400fb2: b8 00 00 00 00 mov $0x0,%eax
400fb7: eb 05 jmp 400fbe <phase_3+0x7b>
400fb9: b8 37 01 00 00 mov $0x137,%eax # switch语句
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax
400fc2: 74 05 je 400fc9 <phase_3+0x86> # 判断a和b是否相等 不想等则引爆炸弹
400fc4: e8 71 04 00 00 callq 40143a <explode_bomb>
400fc9: 48 83 c4 18 add $0x18,%rsp
400fcd: c3 retq

推测phase_3的一开始用sscanf从输入字符串中读入了2个int型数字。

然后观察phase_3中的jmpq ×0x402470(,%rax,8)以及下面的mov与jmp指令,基本上可以肯定这是使用跳转表实现的switch语句。

下面在GDB中验证并记录不同%rax的值对应跳转表的不同跳转位置。具体的命令及结果如下:

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
(gdb) break explode_bomb
Breakpoint 1 at 0x40143a
(gdb) break phase_3
Breakpoint 2 at 0x400f43
(gdb) run test.txt
Starting program: /home/blackdragon/CSAPPLabs/BombLab/bomb test.txt
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
1 1

Breakpoint 2, 0x0000000000400f43 in phase_3 ()
(gdb) stepi 4
0x0000000000400f56 in phase_3 ()
(gdb) print (char *)($rsi)
$1 = 0x4025cf "%d %d"
(gdb) print /x *(long *)(0x402470+0)
$2 = 0x400f7c
(gdb) print /x *(long *)(0x402470+8)
$3 = 0x400fb9
(gdb) print /x *(long *)(0x402470+16)
$4 = 0x400f83
(gdb) print /x *(long *)(0x402470+24)
$5 = 0x400f8a
(gdb) print /x *(long *)(0x402470+32)
$6 = 0x400f91
(gdb) print /x *(long *)(0x402470+40)
$7 = 0x400f98
(gdb) print /x *(long *)(0x402470+48)
$8 = 0x400f9f
(gdb) print /x *(long *)(0x402470+56)
$9 = 0x400fa6

从GDB运行的结果可以看出,sscanf确实读入了两个int *数字,并且也得到了所有的%rax对应的跳转表的不同跳转位置。我们已经有了足够的信息来进行phase_3的逆向。

将phase_3函数逆向,得到如下的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void phase_3(char * input) {
int a, b;
int val = sscanf(input, "%d %d", &a, &b);
if (val <= 1) explode_bomb();
switch (a) {
case 0: a = 207; break;
case 1: a = 311; break;
case 2: a = 707; break;
case 3: a = 256; break;
case 4: a = 389; break;
case 5: a = 206; break;
case 6: a = 682; break;
case 7: a = 327; break;
default: explode_bomb(); a = 0; break;
}
if (a != b) explode_bomb();
return;
}

根据逆向的结果,我们可以得知,本阶段要求输入2个数,且第1个数只能为0到7,且根据第1个数取值的不同,满足条件的第2个数的取值也不同,可能的答案如下:

第1个数 第2个数
0 207
1 311
2 707
3 256
4 389
5 206
6 682
7 327

在GDB中运行bomb并验证结果,结果正确。

阶段4 - phase_4

在bomb-disassemble中观察phase_4的反汇编代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
000000000040100c <phase_4>:
40100c: 48 83 ec 18 sub $0x18,%rsp
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
40101a: be cf 25 40 00 mov $0x4025cf,%esi
40101f: b8 00 00 00 00 mov $0x0,%eax
401024: e8 c7 fb ff ff callq 400bf0 <__isoc99_sscanf@plt> # 从输入的字符串中读取两个数字 并存入局部变量中
401029: 83 f8 02 cmp $0x2,%eax
40102c: 75 07 jne 401035 <phase_4+0x29> # 若数字不为两个 则引爆bomb
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp)
401033: 76 05 jbe 40103a <phase_4+0x2e> # 若第一个数字大于14 则引爆bomb
401035: e8 00 04 00 00 callq 40143a <explode_bomb>
40103a: ba 0e 00 00 00 mov $0xe,%edx
40103f: be 00 00 00 00 mov $0x0,%esi
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi
401048: e8 81 ff ff ff callq 400fce <func4> # 调用func4
40104d: 85 c0 test %eax,%eax
40104f: 75 07 jne 401058 <phase_4+0x4c>
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
401056: 74 05 je 40105d <phase_4+0x51> # 当返回值和b均为0时 解除阶段 否则 引爆bomb
401058: e8 dd 03 00 00 callq 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3

可以注意到,phase_4的反汇编代码前半部分与phase_3相似,均是从输入的字符串中读入2个数字。

将phase_4逆向,得到如下的结果:

1
2
3
4
5
6
7
void phase_4(char * input) {
int a, b;
int val = sscanf(input, "%d %d", &a, &b);
if (val != 2 || ((unsigned)a > 14)) explode_bomb();
val = func4(a, 0, 14);
if (val != 0 || b != 0) explode_bomb();
return;

从逆向的结果我们可以看出,phase_4从输入的字符串中读入2个数字a和b,并且a必须小于等于14且大于等于0,然后phase_4调用func4(a, 0, 14),且只有该函数的返回值和b均为0时,该阶段解除。

现在我们将注意转向phase_4调用的func4函数,func4的反汇编代码如下:

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
0000000000400fce <func4>:
400fce: 48 83 ec 08 sub $0x8,%rsp
400fd2: 89 d0 mov %edx,%eax
400fd4: 29 f0 sub %esi,%eax
400fd6: 89 c1 mov %eax,%ecx
400fd8: c1 e9 1f shr $0x1f,%ecx
400fdb: 01 c8 add %ecx,%eax
400fdd: d1 f8 sar %eax
# 实际上是/2过程,为了保证负数时舍入正确才使用了位移
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx
# %rax加上b
400fe2: 39 f9 cmp %edi,%ecx
400fe4: 7e 0c jle 400ff2 <func4+0x24>
# if分支
400fe6: 8d 51 ff lea -0x1(%rcx),%edx
400fe9: e8 e0 ff ff ff callq 400fce <func4>
# 递归过程
400fee: 01 c0 add %eax,%eax
400ff0: eb 15 jmp 401007 <func4+0x39>
400ff2: b8 00 00 00 00 mov $0x0,%eax
400ff7: 39 f9 cmp %edi,%ecx
400ff9: 7d 0c jge 401007 <func4+0x39>
400ffb: 8d 71 01 lea 0x1(%rcx),%esi
400ffe: e8 cb ff ff ff callq 400fce <func4>
# 递归过程
401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401007: 48 83 c4 08 add $0x8,%rsp
40100b: c3 retq

观察反汇编的func4函数,发现func4函数中调用了func4函数自身,由此可以推测func4函数是一个递归过程。

将func4逆向,得到如下的结果:

1
2
3
4
5
6
7
8
9
10
int func4(int a, int b, int c) {
int returnVal = (c - b) / 2;
int val = returnVal + b;
if (val == a) return 0;
if (val < a) {
return 2 * func4(a, val + 1, c) + 1;
} else {
return 2 * func4(a, b, val - 1);
}
}

观察func4的逆向结果,我们可以发现func4非常类似于二分查找的过程。要另func4的返回值为0,则一定不能让func4执行val<a的分支即必须一直保证(b+c)/2 >= a。考虑到0<=a<=14,所以满足条件的a有a=7 a=3 a=1 a=0 共4个。

在GDB中运行bomb并验证结果,结果正确。

阶段5 - phase_5

在bomb-disassemble中观察phase_5的反汇编代码,如下:

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
0000000000401062 <phase_5>:
401062: 53 push %rbx
401063: 48 83 ec 20 sub $0x20,%rsp
401067: 48 89 fb mov %rdi,%rbx
40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
401071: 00 00
# 栈破坏检测机制
401073: 48 89 44 24 18 mov %rax,0x18(%rsp)
401078: 31 c0 xor %eax,%eax
40107a: e8 9c 02 00 00 callq 40131b <string_length>
# 计算输入字符串长度
40107f: 83 f8 06 cmp $0x6,%eax
401082: 74 4e je 4010d2 <phase_5+0x70>
# 若长度不为6 则引爆bomb
401084: e8 b1 03 00 00 callq 40143a <explode_bomb>
401089: eb 47 jmp 4010d2 <phase_5+0x70>
40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx
40108f: 88 0c 24 mov %cl,(%rsp)
401092: 48 8b 14 24 mov (%rsp),%rdx
401096: 83 e2 0f and $0xf,%edx
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1)
实际上是a[i] = 0x4024b0[input[i] & 0xf]
4010a4: 48 83 c0 01 add $0x1,%rax
4010a8: 48 83 f8 06 cmp $0x6,%rax
4010ac: 75 dd jne 40108b <phase_5+0x29>
# 循环以及条件判断
4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp)
# 为构造的字符串尾部加上'\0'
4010b3: be 5e 24 40 00 mov $0x40245e,%esi
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
4010bd: e8 76 02 00 00 callq 401338 <strings_not_equal>
# 调用strings_not_equal函数
4010c2: 85 c0 test %eax,%eax
4010c4: 74 13 je 4010d9 <phase_5+0x77>
# 若返回值不为0 则引爆bomb
4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb>
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
4010d0: eb 07 jmp 4010d9 <phase_5+0x77>
4010d2: b8 00 00 00 00 mov $0x0,%eax
4010d7: eb b2 jmp 40108b <phase_5+0x29>
4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax
4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
4010e5: 00 00
4010e7: 74 05 je 4010ee <phase_5+0x8c>
4010e9: e8 42 fa ff ff callq 400b30 <__stack_chk_fai
l@plt>
4010ee: 48 83 c4 20 add $0x20,%rsp
4010f2: 5b pop %rbx
4010f3: c3 retq

首先,我们注意到phase_5采用了栈破坏检测机制,并且设置了相应的哨兵值防止栈缓冲区溢出。
然后我们将phase_5逆向,得到如下的结果:
1
2
3
4
5
6
7
8
9
void phase_5(char * input) {
char a[7];
if (strlen(input) != 6) explode_bomb();
for (int i = 0 ; i < 6 ; i++)
a[i] = 0x4024b0[input[i] & 0xf];
a[6] = '\0';
int val = strings_not_equal(a, 0x40245e);
if (val != 0) explode_bomb();
return;

在GDB中打印0x4024b0和0x40245e所对应的字符串,命令和结果如下所示:
1
2
3
4
(gdb) print (char *)0x4024b0
$1 = 0x4024b0 <array> "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
(gdb) print (char *)0x40245e
$2 = 0x40245e "flyers"

可以看出,phase_5实际上是phase_1的提高。

phase_5要求输入一个6个字符的字符串,通过将取出输入字符串中每个字符的后四位当做偏移地址,以该偏移地址取出地址为0x4024b0处的字符数组中对应的字符,并依次保存在临时数组a中构造一个新的字符串。
再将这个新的字符串与0x40245e处的字符串相比较,如果两个字符串相等,则该阶段解除。

0x40245e处的字符串为”flyers”,其每个字符在0x4024b0处的字符数组”maduiersnfotvbyl”中的对应的偏移量为9,15,14,5,6,7。因此只要输入的字符串的后四位换算成10进制分别为9,15,14,5,6,7即可。

本phase的答案不唯一。一个可能的答案为”yo~uvw”,在GDB中运行bomb并验证结果,结果正确。

阶段6 - phase_6

本phase是所有phase中最难的一个,但是仍然需要一步一步的分解反汇编代码,并最终得到正确的字符串。
在bomb-disassemble中观察phase_6的反汇编代码,如下:

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
00000000004010f4 <phase_6>:
4010f4: 41 56 push %r14
4010f6: 41 55 push %r13
4010f8: 41 54 push %r12
4010fa: 55 push %rbp
4010fb: 53 push %rbx
4010fc: 48 83 ec 50 sub $0x50,%rsp
401100: 49 89 e5 mov %rsp,%r13
401103: 48 89 e6 mov %rsp,%rsi
401106: e8 51 03 00 00 callq 40145c <read_six_numbers>
# 读入6int型数字
40110b: 49 89 e6 mov %rsp,%r14
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d
401114: 4c 89 ed mov %r13,%rbp
401117: 41 8b 45 00 mov 0x0(%r13),%eax
40111b: 83 e8 01 sub $0x1,%eax
40111e: 83 f8 05 cmp $0x5,%eax
401121: 76 05 jbe 401128 <phase_6+0x34>
401123: e8 12 03 00 00 callq 40143a <explode_bomb>
# 判断数字必须小于等于6
401128: 41 83 c4 01 add $0x1,%r12d
40112c: 41 83 fc 06 cmp $0x6,%r12d
401130: 74 21 je 401153 <phase_6+0x5f>
401132: 44 89 e3 mov %r12d,%ebx
401135: 48 63 c3 movslq %ebx,%rax
401138: 8b 04 84 mov (%rsp,%rax,4),%eax
40113b: 39 45 00 cmp %eax,0x0(%rbp)
40113e: 75 05 jne 401145 <phase_6+0x51>
401140: e8 f5 02 00 00 callq 40143a <explode_bomb>
401145: 83 c3 01 add $0x1,%ebx
401148: 83 fb 05 cmp $0x5,%ebx
40114b: 7e e8 jle 401135 <phase_6+0x41>
40114d: 49 83 c5 04 add $0x4,%r13
401151: eb c1 jmp 401114 <phase_6+0x20>
# 判断这6个数字两两不重复
401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi
401158: 4c 89 f0 mov %r14,%rax
40115b: b9 07 00 00 00 mov $0x7,%ecx
401160: 89 ca mov %ecx,%edx
401162: 2b 10 sub (%rax),%edx
401164: 89 10 mov %edx,(%rax)
401166: 48 83 c0 04 add $0x4,%rax
40116a: 48 39 f0 cmp %rsi,%rax
40116d: 75 f1 jne 401160 <phase_6+0x6c>
# 分别用7减去这6个数
40116f: be 00 00 00 00 mov $0x0,%esi
401174: eb 21 jmp 401197 <phase_6+0xa3>
401176: 48 8b 52 08 mov 0x8(%rdx),%rdx
40117a: 83 c0 01 add $0x1,%eax
40117d: 39 c8 cmp %ecx,%eax
40117f: 75 f5 jne 401176 <phase_6+0x82>
401181: eb 05 jmp 401188 <phase_6+0x94>
401183: ba d0 32 60 00 mov $0x6032d0,%edx
401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2)
40118d: 48 83 c6 04 add $0x4,%rsi
401191: 48 83 fe 18 cmp $0x18,%rsi
401195: 74 14 je 4011ab <phase_6+0xb7>
401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx
40119a: 83 f9 01 cmp $0x1,%ecx
40119d: 7e e4 jle 401183 <phase_6+0x8f>
40119f: b8 01 00 00 00 mov $0x1,%eax
4011a4: ba d0 32 60 00 mov $0x6032d0,%edx
4011a9: eb cb jmp 401176 <phase_6+0x82>
4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx
4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax
4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi
4011ba: 48 89 d9 mov %rbx,%rcx
4011bd: 48 8b 10 mov (%rax),%rdx
4011c0: 48 89 51 08 mov %rdx,0x8(%rcx)
4011c4: 48 83 c0 08 add $0x8,%rax
4011c8: 48 39 f0 cmp %rsi,%rax
4011cb: 74 05 je 4011d2 <phase_6+0xde>
4011cd: 48 89 d1 mov %rdx,%rcx
4011d0: eb eb jmp 4011bd <phase_6+0xc9>
4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx)
4011d9: 00
4011da: bd 05 00 00 00 mov $0x5,%ebp
4011df: 48 8b 43 08 mov 0x8(%rbx),%rax
4011e3: 8b 00 mov (%rax),%eax
4011e5: 39 03 cmp %eax,(%rbx)
4011e7: 7d 05 jge 4011ee <phase_6+0xfa>
4011e9: e8 4c 02 00 00 callq 40143a <explode_bomb>
4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx
4011f2: 83 ed 01 sub $0x1,%ebp
4011f5: 75 e8 jne 4011df <phase_6+0xeb>
# 主要是重排链表 并且判断重排后的链表递减 否则引爆bomb
4011f7: 48 83 c4 50 add $0x50,%rsp
4011fb: 5b pop %rbx
4011fc: 5d pop %rbp
4011fd: 41 5c pop %r12
4011ff: 41 5d pop %r13
401201: 41 5e pop %r14
401203: c3 retq

由于phase_6的逆向比较复杂,这里只大致介绍该函数的作用——

  1. 该函数在栈上分配了80个字节,包括了一个有6个int型数字的数组a和一个有6个指针的数组b。
  2. 同phase_2一样,该函数调用了read_six_numbers从输入的字符串中读取了6个数字,并且保存到数组a中。
  3. 判断这6个数字是否都小于等于6并且互相不重复,如果不满足则引爆炸弹。
  4. 分别用7减去这6个数,并且用计算结果覆盖这6个数。既a[i] = 7 - a[i]。
  5. phase_6中引用了来自0x6032d0的结构体数组,该结构体共16个字节,其中前8个字节存放了一个int型的数字(结构体对齐),后8个字节存放了一个指针,该指针在指向结构体数组中的下一个元素。这实际上是一个有6个元素的单向链表。
  6. 程序从i=1开始,将结构体数组中的第a[i]个元素的首地址存放进b[i]中,循环6次,直到i=6。
  7. 程序从b[1]开始,将b[1]对应的元素的后8个字节对应的地址指向b[2],直到b[5]->next = b[6],将b[6]所对应的元素的后8个字节对应的地址指向0。
  8. 然后从b[1]所对应的节点开始,依次判断整个链表是否降序排列,如果不满足降序,则引爆bomb。

phase_6是所有phases中最难的一个,其本质是输入1组数字从而实现对单向链表的重排,使得重排后的单向链表降序。

原链表的情况如下:

节点 1 2 3 4 5 6
322 168 924 691 477 443

因此使得链表降序的输入为:”3 4 5 6 1 2”
注意这是用7减过之后的数字序列,原输入序列也就是答案应为:”4 3 2 1 6 5”

隐藏阶段 - secret_phase

观察bomb-symboltable和bomb-disassemble我们都能发现一个叫做secret_phase的函数,这就是本次实验中被隐藏起来的phase。

经过观察反汇编代码,我们发现call secret_phase位于phase_defused函数中。

在bomb-disassemble中观察phase_defused的反汇编代码,如下:

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
00000000004015c4 <phase_defused>:
4015c4: 48 83 ec 78 sub $0x78,%rsp
4015c8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
4015cf: 00 00
4015d1: 48 89 44 24 68 mov %rax,0x68(%rsp)
4015d6: 31 c0 xor %eax,%eax
4015d8: 83 3d 81 21 20 00 06 cmpl $0x6,0x202181(%rip) # 603760 <num_input_strings>
# 输入过的字符串数目必须为6 意味着必须是phase_6之后
4015df: 75 5e jne 40163f <phase_defused+0x7b>
4015e1: 4c 8d 44 24 10 lea 0x10(%rsp),%r8
4015e6: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
4015eb: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
4015f0: be 19 26 40 00 mov $0x402619,%esi
4015f5: bf 70 38 60 00 mov $0x603870,%edi
4015fa: e8 f1 f5 ff ff callq 400bf0 <__isoc99_sscanf@plt>
4015ff: 83 f8 03 cmp $0x3,%eax
401602: 75 31 jne 401635 <phase_defused+0x71>
401604: be 22 26 40 00 mov $0x402622,%esi
401609: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
40160e: e8 25 fd ff ff callq 401338 <strings_not_equal>
# 第4个输入字符串读入两个数字和一个字符串 且字符串必须与0x402622处的字符串相等
401613: 85 c0 test %eax,%eax
401615: 75 1e jne 401635 <phase_defused+0x71>
401617: bf f8 24 40 00 mov $0x4024f8,%edi
40161c: e8 ef f4 ff ff callq 400b10 <puts@plt>
401621: bf 20 25 40 00 mov $0x402520,%edi
401626: e8 e5 f4 ff ff callq 400b10 <puts@plt>
40162b: b8 00 00 00 00 mov $0x0,%eax
401630: e8 0d fc ff ff callq 401242 <secret_phase>
# 然后才能进入隐藏阶段
401635: bf 58 25 40 00 mov $0x402558,%edi
40163a: e8 d1 f4 ff ff callq 400b10 <puts@plt>
40163f: 48 8b 44 24 68 mov 0x68(%rsp),%rax
401644: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
40164b: 00 00
40164d: 74 05 je 401654 <phase_defused+0x90>
40164f: e8 dc f4 ff ff callq 400b30 <__stack_chk_fail@plt>
401654: 48 83 c4 78 add $0x78,%rsp
401658: c3 retq

当且仅当phase_6解除并且从phase_4的输入字符串依次读取两个数字和一个字符串且这个字符串为”DrEvil”时,可以进入。

在bomb-disassemble中观察secret_phase的反汇编代码,如下:

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
0000000000401242 <secret_phase>:
401242: 53 push %rbx
401243: e8 56 02 00 00 callq 40149e <read_line>
401248: ba 0a 00 00 00 mov $0xa,%edx
40124d: be 00 00 00 00 mov $0x0,%esi
401252: 48 89 c7 mov %rax,%rdi
401255: e8 76 f9 ff ff callq 400bd0 <strtol@plt>
# 调用strtol函数
40125a: 48 89 c3 mov %rax,%rbx
40125d: 8d 40 ff lea -0x1(%rax),%eax
401260: 3d e8 03 00 00 cmp $0x3e8,%eax
401265: 76 05 jbe 40126c <secret_phase+0x2a>
401267: e8 ce 01 00 00 callq 40143a <explode_bomb> # 如果数字减1之后大于1000 则引爆bomb
40126c: 89 de mov %ebx,%esi
40126e: bf f0 30 60 00 mov $0x6030f0,%edi
401273: e8 8c ff ff ff callq 401204 <fun7>
# 调用fun7
401278: 83 f8 02 cmp $0x2,%eax
40127b: 74 05 je 401282 <secret_phase+0x40>
40127d: e8 b8 01 00 00 callq 40143a <explode_bomb> # 如果fun7返回值不为2 则引爆bomb
401282: bf 38 24 40 00 mov $0x402438,%edi
401287: e8 84 f8 ff ff callq 400b10 <puts@plt>
40128c: e8 33 03 00 00 callq 4015c4 <phase_defused>
401291: 5b pop %rbx
401292: c3 retq

根据反汇编的结果,我们可以得知,secret_phase同样需要读入一个字符串,之后其首先调用strtol(input, NULL, 10)将输入的字符串转换成long型数字a。

如果该数字>1001,则引爆bomb,否则则调用fun7(0x6030f0, a)并判断其返回结果,当且仅当结果为2时,解除secret_phase。

在bomb-disassemble中观察fun7的反汇编代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0000000000401204 <fun7>:
401204: 48 83 ec 08 sub $0x8,%rsp
401208: 48 85 ff test %rdi,%rdi
40120b: 74 2b je 401238 <fun7+0x34>
40120d: 8b 17 mov (%rdi),%edx
40120f: 39 f2 cmp %esi,%edx
401211: 7e 0d jle 401220 <fun7+0x1c>
401213: 48 8b 7f 08 mov 0x8(%rdi),%rdi
401217: e8 e8 ff ff ff callq 401204 <fun7>
40121c: 01 c0 add %eax,%eax
40121e: eb 1d jmp 40123d <fun7+0x39>
401220: b8 00 00 00 00 mov $0x0,%eax
401225: 39 f2 cmp %esi,%edx
401227: 74 14 je 40123d <fun7+0x39>
401229: 48 8b 7f 10 mov 0x10(%rdi),%rdi
40122d: e8 d2 ff ff ff callq 401204 <fun7>
401232: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401236: eb 05 jmp 40123d <fun7+0x39>
401238: b8 ff ff ff ff mov $0xffffffff,%eax
40123d: 48 83 c4 08 add $0x8,%rsp
401241: c3 retq
# 同样是递归的二分查找过程 数据结构是BST二叉搜索树

将fun7逆向,结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct tree {
int val;
struct tree * left;
struct tree * right;
};

int fun7(struct tree * ptr, int num) {
if (ptr == NULL) return -1;
if (ptr->val == num) return 0;
if (ptr->val < num) {
ptr = ptr->right;
return 2 * fun7(ptr, num) + 1;
} else {
ptr = ptr->left;
return 2 * fun7(ptr, num);
}
}

其中,ptr所指向的struct tree是一棵二叉搜索树的根节点。对于二叉搜索树的任意一个节点,节点左边孩子的值全部小于该节点的值,节点右边孩子的值全部大于该节点的值。

这棵根节点位于0x6030f0的二叉搜索树的结构如下图所示:

要令fun7返回的结果为2,一种可行的递归返回情况是0->(20+1)=1->(12)=2。在该情况下,递归深度为2,节点的移动路线为左->右,并且在值为22的节点返回0。这表明,fun7的第二个参数,也就是secret_phase的正确答案,为”22”。

在GDB中运行bomb并验证结果,结果正确。

实验答案

注意,有部分答案不唯一,并且Bomb Lab每年都在更新,所以答案可能会有所变化。

1
2
3
4
5
6
7
Border relations with Canada have never been better.
1 2 4 8 16 32
1 311
0 0 DrEvil
yo~uvw
4 3 2 1 6 5
22

评论和共享

实验答案托管在我的GitHub

总共花了五天时间完成了《深入理解计算机系统》(Computer Science A Programmer’s Perspective)的第一个Lab - Data Lab,深感这次实验的效率低下。一方面是由于清明假期自己有所惰怠,另一方面也是由于在做题的时候采取了不正确的方法,在已经明知不会的情况下我还继续投入大量无意义的时间,没有取得很好的学习效果。

实验简介

Data Lab - Manipulating Bits主要是关于位操作的实验,对应于书本的第2章:信息的表示和处理。

在该实验中,学生主要需要在一个严格限制的C子集(使用优先的位运算符以及顺序结构的代码)中实现简单的逻辑,补码以及浮点数相关的函数。本实验的目的是为了让学生熟悉整数和浮点数的位级表示。

本实验的核心在于在各种受限制的条件下完成给出的15个谜题。其中包括5个位操作的谜题,7个补码运算的谜题,以及3个浮点数操作的谜题。其中每完成一个谜题都能得到一定的分数,如果使用的运算符数目小于给定的数目,还能获得加分。本实验的满分是71分。可以通过讲义中的driver.pl脚本直接计算出总分数。

关于本实验具体的介绍详见实验讲义

测试程序btest make失败及解决办法

在完成实验的功能中需要用到make && ./btest命令进行答案正确性的检查,结果系统返回了fetal error: gnu/stubs-32.h:No such file or directory

经过检查发现是系统是Archlinux 64位,而Makefile中包含了-m32的编译参数导致的。

通过启用multilib仓库使得可以在64位的ArchLinux系统上编译和运行32位的程序,问题得以解决。

编辑/etc/pacman.conf,取消下面内容的注释:

1
2
[multilib]
include = /etc/pacman.d/mirrorlist

更新软件包列表并升级系统pacman -Syu

然后安装gcc-multilib并重新执行make && ./btest,问题解决。

实验限制

整数代码规则

整数代码应遵循如下的基本结构:

1
2
3
4
5
6
7
8
9
10
int Funct(arg1, arg2, ...) {
/* brief description of how your implementation works */
int var1 = Expr1;
...
int varM = ExprM;
varJ = ExprJ;
...
varN = ExprN;
return ExprR;
}

每一个表达式只能使用:

  1. 0~255之间的常量(包括0和255)
  2. 函数的参数和局部变量
  3. 单目运算符 ! ~
  4. 双目运算符 & ^ | + << >>

注意,部分题目对于运算符有着更加严格的限制,但是你可以在一行代码中使用多个运算符。

你将会被禁止:

  1. 使用任何的分支或者循环结构
  2. 定义或者使用任何的宏
  3. 在文件中定义任何额外函数
  4. 调用任何函数
  5. 使用任何其他的运算符
  6. 使用任何形式的类型转换
  7. 使用除了int以外的其他数据类型,这意味着你不能使用数组,结构或联合

你可以假设你的机器:

  1. 使用32位补码表示整数
  2. 使用算术右移
  3. 对整数进行的位移运算的位数超越了字长是未定义的行为

浮点代码规则

浮点运算的规则相对较为宽松,你可以使用循环或者是分支结构,你被允许同时使用int和unsigned,此外,你还可以使用任意的常量。

你将会被禁止:

  1. 定义或者使用任何的宏
  2. 在文件中定义任何额外函数
  3. 调用任何函数
  4. 使用任何形式的类型转换
  5. 使用除了int和unsigned以外的任何数据类型,这意味着你不能使用数组,结构或者联合
  6. 使用任何浮点数的数据类型,操作或者是常量

实验答案及分析

第一部分 位操作

  • bitAnd
    问题:要求使用~和|以及不超过8个操作符实现x&y
    分析:使用De Morgan定律即可
    答案:

    1
    2
    3
    4
    int bitAnd(int x, int y) {
    /* x&y = ~(~(x&y)) = ~(~x|~y) */
    return ~(~x | ~y);
    }
  • getByte
    问题:要求使用! ~ & ^ | + << >>以及不超过6个操作符返回x中的第n个字节(0(LSB)<=n<=3(MSB))。
    分析:根据不同的n值将x右移不同的位数,然后使用掩码0xFF取得最低位数的字节即可。先左移再右移会导致算术右移,需要小心。
    答案:

    1
    2
    3
    4
    int getByte(int x, int n) {
    /* right shift in order to avoid boring arithmatic shift and try to get specific byte using bitAnd and mask. */
    return ((x >> (n << 3)) & 0xFF);
    }
  • logicalShift
    问题:要求使用! ~ & ^ | + << >>以及不超过20个操作符实现将int值x的逻辑右移n位。
    分析:通过将一个全1的数通过算术右移的方式构造掩码,然后与算术右移的掩码求按位与即可。
    注意,直接右移32位的结果是未定义的,需要额外处理这种情况。
    答案:

    1
    2
    3
    4
    5
    int logicalShift(int x, int n) {
    /* try to implement logical right shift by mask and logical right shift */
    int mask = ((~0) << (31 + (~n + 1))) << 1;
    return (x >> n) & (~mask);
    }
  • bitCount
    问题:要求使用! ~ & ^ | + << >>以及不超过40个操作符计算一个数字x中有多少为1的位。
    分析:
    本题参考stackoverflow上的回答,并结合题目要求做了一些改动。
    核心的思想是分治法,通过将原数字x中的位按每1,2,4…个位分组,按相邻组进行累加,最后求出结果。
    举例来说,假如现在有一个数395,它的二进制表示是0000000110001011(16位),首先我们将这个数按1位分组,得到:
    0 0 0 0 0 0 0 1 1 0 0 0 1 0 1 1
    然后我们将相邻的组累加,得到:
    0+0 0+0 0+0 0+1 1+0 0+0 1+0 1+1
    00 00 00 01 01 00 01 10
    然后我们继续将相邻的组累加,得到:
    00+00 00+01 01+00 01+10
    0000 0001 0001 0011
    然后我们继续将相邻的组累加,得到:
    0000+0001 0001+0011
    00000001 00000100
    最后,我们最后的两个组累加,得到:
    00000001+0000100=00000101
    结果是5,为正确答案。
    答案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     int bitCount(int x) {
    /* Divide and Conquer */
    int mask = 0x55 + (0x55 << 8) + (0x55 << 16) + (0x55 << 24);
    x = (x & mask) + ((x >> 1) & mask);
    mask = 0x33 + (0x33 << 8) + (0x33 << 16) + (0x33 << 24);
    x = (x & mask) + ((x >> 2) & mask);
    mask = 0xF + (0xF << 8) + (0xF << 16) + (0xF << 24);
    x = (x & mask) + ((x >> 4) & mask);
    return (x + (x >> 8) + (x >> 16) + (x >> 24)) & 0xFF;
    }
  • bang
    问题:要求使用~ & ^ | + << >>以及不超过12个操作数计算x的逻辑非。
    分析:
    逻辑非要求将0变成1,将非0的数变成0。生成0或1可以通过与掩码0x1求按位与实现,重点在于如何构造一个表达式使得0和非0的数产生不同的结果。
    可以考虑对于任意一个非0的数,它的负数都是从最高位到其最低位的1为止(不包括最低位的1)全部取反的结果。
    例如,14是00001110(8位),而-14是11110010(8位),为14从最高位到最低位的1(不包括这个1),也就是000011取反,而最低位的10不变的结果。
    而0的负数永远是0。
    从这里我们可以看出,对于一个非0的数,它与它的负数按位或的结果的最高位一定会是1,而0与非0的按位或的结果的最高位永远是0。我们可以以此实现逻辑非。
    答案:

    1
    2
    3
    4
    int bang(int x) {
    /* for x != 0, the highest bit of x | (-x) will always become 1 while when x == 0, the result is the opposite */
    return (~((x | (~x + 1)) >> 31) & 1);
    }

第二部分 补码运算

  • tmin
    问题:要求使用! ~ & ^ | + << >>以及不超过4个运算符返回以补码表示的最小的整数。
    分析:根据补码到整数的公式,最小的整数的补码表示为10000…
    答案:

    1
    2
    3
    4
    int tmin(void) {
    /* TMin = 10000.... */
    return (1 << 31);
    }
  • fitsBits
    问题:要求使用! ~ & ^ | + << >>以及不超过15个操作符判断x是否能表示成n位的补码,如果是,则返回1,反之则返回0。
    分析:
    这个问题需要分正数和负数两种情况讨论,对于正数来说,如果一个数x能表示成n位的补码,那么它从最高位直到第n位一定全部为0,它的第n位一定不能为1,否则它就会变成一个负数。对于一个负数来说,如果一个数x能表示成n位的补码,那么它只需从最高位到第n位全部为1即可。
    因此,我们可以先将x右移n-1位,对于满足条件的正数和负数,这将产生一个全0的数或是一个全1的数。然后我们可以根据x的符号构造一个全0或者是全1的掩码。通过将这两个数按位异或并且取逻辑非,就能得到正确的结果。
    答案:

    1
    2
    3
    4
    5
    6
    int fitsBits(int x, int n) {
    /* if fitsBits then from highest bit to n bit will all become 1 - negative number or 0 - positive number
    * then can construct a mask to implement fitsBits with the help of ^ and !
    */
    return !((x >> (n + (~1) + 1)) ^ (((1 << 31) & x) >> 31));
    }
  • divpwr2
    问题:要求使用! ~ & ^ | + << >>以及不超过15个操作符求出x/(2^n)的结果,其中0<=n<=30。
    分析:对于正数,直接使用算术右移即可;对于负数,需要加上适当的偏移量以实现向上舍入,这可以用根据符号位生成1个全0或者全1的掩码来控制。
    答案:

    1
    2
    3
    4
    5
    6
    int divpwr2(int x, int n) {
    /* add bias when x is negative, which is controlled by a sign-related mask */
    int mask = x >> 31;
    int add = ((1 << n) + (~1) + 1) & mask;
    return (x + add) >> n;
    }
  • negate
    问题:要求使用! ~ & ^ | + << >>以及不超过5个操作符求出x的负数。
    分析:-x = (~x + 1)
    答案:

    1
    2
    3
    4
    int negate(int x) {
    /* -x = (~x) + 1 */
    return ~x + 1;
    }
  • isPositive
    问题:要求使用! ~ & ^ | + << >>以及不超过8个操作符判断x是否为正数。
    分析:对于正数,它的最高位一定是0,同时还要排除0的影响。
    答案:

    1
    2
    3
    4
    int isPositive(int x) {
    /* ensure the highest bit is 0 and x != 0 */
    return (!(x >> 31)) ^ (!(x ^ 0));
    }
  • isLessOrEqual
    问题:要求使用! ~ & ^ | + << >>以及不超过24个操作符判断x是否小于或等于y。
    分析:
    对于这个问题,我们需要按照是否溢出,以及x与y的符号分别讨论。
    最基本的思路是,做y-x,若结果的符号位为1,则返回0,反之返回1。但这样就忽略了溢出可能导致的问题,现在我们做如下考虑。
    若x,y均为正数,或x,y均为负数,则y-x绝不可能溢出,因此可直接用差的符号位判断大小。
    若x为正数,y为负数,可以直接返回0。
    若x为负数,y为正数,可以直接返回1。
    我们可以生成两种条件变量,一种为x,y同号时返回正确结果的条件变量,另一种为x,y异号时返回正确结果的条件变量。
    对于x,y同号和异号这两种不同的情况,我们可以用!((x ^ y) >> 31)生成掩码使得在任意的情况下,只有正确的条件变量生效。
    答案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int isLessOrEqual(int x, int y) {
    /* different processing ways when x and y have the same signs or different signs */
    int diff = y + (~x) + 1;
    int not_diff_sign = !(diff >> 31);
    int mask = !((x ^ y) >> 31);
    int result_not_overflow = mask & not_diff_sign;
    int result_overflow = (!mask) & (x >> 31);
    return result_overflow | result_not_overflow;
    }
  • ilog2
    问题:要求使用! ~ & ^ | + << >>以及不超过90个操作数求出x以2为底的对数(向下舍入),保证x>0。
    分析:
    这道题参考了网上的答案。核心的思想是二分查找。
    首先将x右移16位,如果x>0,则表明结果的第5位为1,将该位置为1。
    然后根据结果将x右移8位(第5位为0)或者将x右移24位(第5位为1),如果x>0,则表明结果的第4位为1,将该位置为1。
    以此类推,直到得出结果。
    答案:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int ilog2(int x) {
    /* Binary Search */
    int result = (!!(x >> 16) << 4);
    result = result + ((!!(x >> (result + 8))) << 3);
    result = result + ((!!(x >> (result + 4))) << 2);
    result = result + ((!!(x >> (result + 2))) << 1);
    result = result + (!!(x >> (result + 1)));
    return result;
    }

第三部分 浮点数操作

  • float_neg
    问题:返回对于浮点数参数f,-f的等价的位级表示。当参数位NAN时,返回NAN。函数的参数和返回值均为unsigned,但它们实际上都是浮点数的位表示。最多允许使用10个操作符。
    分析:首先要判断参数是否是NAN,如果是则直接返回,否的话直接将参数的最高位取反即可。
    NAN的阶码全为1,尾码不全为0。
    答案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    unsigned float_neg(unsigned uf) {
    /* m_flag - if m != 0 e_flag - if e == 0xff */
    unsigned m_flag = 0x007fffff & uf;
    unsigned e_flag = !(0x7f800000 & (~uf));
    if (e_flag && m_flag) {
    return uf;
    } else {
    return 0x80000000 ^ uf;
    }
    }
  • float_i2f
    问题:返回(float)x的位级的等价表示。结果被作为unsigned int返回,但它实际上是单精度浮点数的位表示。最多允许使用30个操作符。
    分析:
    首先考虑到整数中只有0会被转换为非规格化浮点数,而其他整数会被转化为规格化浮点数。因此对于0需要做额外的处理。
    其次,对于负数,我们需要将其转化为正数再做处理,这就要求我们需要将x的符号保存下来,并且从补码转化为无符号表示。
    然后,我们需要根据转换成无符号数的x算出阶码e和尾码m。
    最后,我们将尾码舍入后再和阶码符号一起,生成最终的结果即可。
    这里我有一个比较大的误区,舍入的时候看的不仅仅是带舍入位的下一位,而是待舍入位之后的所有位。
    答案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    unsigned float_i2f(int x) {
    /* 1.process 0 individually 2.process negative number and store the sign 3.get the e number 4.get the m number and round it 5.construct the result */
    unsigned count = 0;
    unsigned mask = 0x80000000;
    unsigned sign = x & mask;
    unsigned c = 0, absx = x;
    if (x == 0) {
    return 0;
    } else {
    if (x < 0) {
    absx = -x;
    }
    while (!(mask & absx)) {
    count = count + 1;
    mask = mask >> 1;
    }
    absx = absx << (count + 1);
    if (((absx & 0x1ff) > 0x100) || ((absx & 0x3ff) >= 0x300)) {
    c = 1;
    }
    return sign + ((158 - count) << 23) + (absx >> 9)+ c;
    }
    }
  • float_twice
    问题:返回对于浮点数参数f,2*f的等价的位级表示。参数和结果都为unsigned int,但它们实际上是浮点数的位表示。当参数是NAN时,返回NAN。最多允许使用30个操作符。
    分析:
    对于非规格化浮点数,只要在保留符号位的情况下将尾码m右移一位即可,这样同时解决了尾码乘以二和进位的情况。
    对于规格化浮点数,只要将其阶码e加1即可。
    答案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    unsigned float_twice(unsigned uf) {
    /* denormailized number - m = m << 1 normalized number - e = e + 1 */
    unsigned result = uf;
    if ((uf & 0x7f800000) == 0) {
    result = ((uf & 0x007fffff) << 1) | (uf & 0x80000000);
    } else if ((uf & 0x7f800000) != 0x7f800000) {
    result = uf + 0x00800000;
    }
    return result;
    }

评论和共享

开学三周,王爽的《汇编语言》(第三版)总算是基本上看完了,本文是总结的第二部分,包括了书本上的实验

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

  1. 使用Debug,将下面的程序段写入内存,逐条执行,观察每条指令执行后,CPU中相关寄存器的变化。

    机器码汇编指令执行后相关寄存器变化
    b8 20 4emov ax, 4E20HAX=4E20H
    05 16 14add ax, 1416HAX=6236H
    bb 00 20mov bx, 2000HBX=2000H
    01 d8add ax, bxAX=8236H
    89 c3mov bx, axBX=8236H
    01 d8add ax, bxAX=046CH
    b8 1a 00mov ax, 001AHAX=001AH
    bb 26 00mov bx, 0026HBX=0026H
    00 d8add al, blAX=0040H
    00 dcadd ah, blAX=2640H
    00 c7add bh, alBX=4026H
    b4 00mov ah, 0AX=0040H
    00 d8add al, blAX=0066H
    04 9cadd al, 9CHAX=0002H

    提示,可用E命令和A命令以两种方式将指令写入内存。注意用T命令执行时,CS:IP的指向。

  2. 将下面3条指令写入从2000:0开始的内存单元中,利用这3条指令计算2的8次方。

    1
    2
    3
    mov ax, 1
    add ax, ax
    jmp 2000:0003

    每次执行 add ax, ax 相当于将ax乘2,重复执行该条指令8次即可。

  3. 查看内存中的内容。
    PC机主板上的ROM写有一个生产日期,在内存FFF00H~FFFFFH的某几个单元中,请找到这个生产日期并试图改变它。
    提示,如果读者对实验的结果感到疑惑,请仔细阅读第1章中的1.15节。

通过Debug中的D命令,可以观察到生产日期以MM/DD/YY的格式存储在内存的ffff:0005-ffff:000c共计8个字节处。
该生产日期不可被修改,因为其只读。

  1. 向内存从B8100H开始的单元中填写数据,如:
    1
    -e B810:0000 01 01 02 02 03 03 04 04
    请读者先填写不同的数据,观察产生的现象;再改变填写的地址,观察产生的现象。
    提示,如果读者对实验的结果感到疑惑,请仔细阅读第1章中的1.15节。

通过向内存中的显存地址空间写入数据,控制在屏幕上的不同位置显示不同颜色的字符。

实验2 用机器指令和汇编指令编程

  1. 使用Debug,将下面的程序段写入内存,逐条执行,根据指令执行后的实验运行情况填空。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    mov ax, ffff
    mov ds, ax

    mov ax, 2200
    mov ss, ax

    mov sp, 1000

    mov ax,[0] ;ax = _____
    add ax,[2] ;ax = _____
    mov bx,[4] ;bx = _____
    add bx,[6] ;bx = _____

    push ax ;sp = _____ 修改的内存单元的地址是_____内容为_____
    push bx ;sp = _____ 修改的内存单元的地址是_____内容为_____
    pop ax ;sp = _____ ax = _____
    pop bx ;sp = _____ bx = _____

    push [4] ;sp = _____ 修改的内存单元的地址是_____内容为_____
    push [6] ;sp = _____ 修改的内存单元的地址是_____内容为_____

    结果(不唯一):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    D4EA
    25EE
    3002
    5F37
    00FE 220FE 25EE
    00FC 220FC 5F37
    00FE 5F37
    0100 25EE
    00FE 220FE 3002
    00FC 220FC 2F35
  2. 仔细观察图3.19中的实验过程,然后分析:为什么2000:0~2000:f中的内容会发生改变?
    在使用T命令进行单步追踪的时候,产生了中断,为了保护现场,CPU将PSW、CS和IP依此入栈,导致了内存相关位置内容的改变。

实验3 编程、编译、连接、跟踪

  1. 将下面的程序保存为t1.asm文件,将其生成可执行文件t1.exe。

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

    codesg segment
    mov ax, 2000H
    mov ss, ax
    mov sp, 0
    add sp, 10
    pop ax
    pop bx
    push ax
    push bx
    pop ax
    pop bx

    mov ax, 4c00h
    int 21h
    codesg ends

    end

    结果:

    1
    2
    3
    edit t1.asm
    masm.exe t1.asm;
    link.exe t1.obj;
  2. 用Debug跟踪t1.exe的执行过程,写出每一步执行后,相关寄存器中的内容和栈顶的内容。

    汇编指令相关寄存器的内容栈顶的内容
    mov ax, 2000HAX = 2000H00B8H
    mov ss, axSS = 2000H_____
    mov sp, 0SP = 00000H
    add sp, 10SP = 100000H
    pop axAX = 0 SP = 120000H
    pop bxBX = 0 SP = 140000H
    push axSP = 120000H
    push bxSP = 100000H
    pop axAX = 0 SP = 120000H
    pop bxBX = 0 SP = 140000H
    mov ax, 4c00hAX = 4C00H0000H
    int 21h__________

    结果不唯一

  3. PSP的头两个字节是CD 20,用Debug加载t1.exe,查看PSP的内容。

    1
    -d 2119:0

    2119为CS-0010H

实验4 [bx]和loop的使用

  1. 编程,向内存0:200~0:23F依此传送数据0~63(3FH)。

  2. 编程,向内存0:200~0:23F依此传送数据0~63(3FH),程序中只能使用9条指令,9条指令中包括“mov ax, 4c00h”和“int 21h”。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    assume cs:code
    code segment
    mov ax, 0020h
    mov ds, ax
    mov bx, 0
    mov cx, 64

    s: mov [bx], bl
    inc bl
    loop s

    mov ax, 4c00h
    int 21h
    code ends
    end
  3. 下面的程序的功能是将“mov ax, 4c00h”之前的指令复制到内存的0:200处,补全程序,上机调试,跟踪运行结果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    assume cs:code
    code segment
    mov ax, _____
    mov ds, ax
    mov ax, 0020h
    mov es, ax
    mov bx, 0
    mov cx, _____
    s: mov al, [bx]
    mov es:[bx], al
    inc bx
    loop s
    mov ax, 4c00h
    int 21h
    code ends
    end

    结果:

    1
    2
    code
    18H

    提示:
    复制的是什么?从哪里到哪里?
    复制的是代码段中mov ax, 4c00h之前的代码,以数据的形式,从内存中代码段的位置复制到内存中0:200处开始的一段连续的空间。
    复制的是什么?有多少个字节?你如何直到要复制的字节的数量?
    可以用offset计算得出,也可以在Debug中用T命令观察得出。

实验5 编写、调试具有多个段的程序

  1. 将下面的程序编译连接,用Debug加载、跟踪,然后回答问题。

    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
    assume cs:code, ds:data, ss:stack

    data segment
    dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
    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 ax, 4c00h
    int 21h
    code ends

    end start

    ①CPU执行程序,程序返回前,data段中的数据为多少?
    ②CPU执行程序,程序返回前,CS = _____、SS = _____、DS = _____。
    ③设程序加载后,code段的段地址为X,则data段的段地址为_____,stack段的段地址为_____。
    答案:
    ①data段中的数据不变。
    ②212B、212A、2129(答案不唯一)
    ③X-2、X-1

  2. 将下面的程序编译连接,用Debug加载、跟踪,然后回答问题。

    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
    assume cs:code, ds:data, ss:stack

    data segment
    dw 0123h, 0456h
    data ends

    stack segment
    dw 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 ax, 4c00h
    int 21h
    code ends

    end start

    (1)CPU执行程序,程序返回前,data段中的数据为多少?
    (2)CPU执行程序,程序返回前,CS = _____、SS = _____、DS = _____。
    (3)设程序加载后,code段的段地址为X,则data段的段地址为_____,stack段的段地址为_____。
    (4)对于如下定义的段:

    1
    2
    3
    name segment
    ...
    segment ends

    如果段中的数据占N个字节,则程序加载后,该段实际占有的空间为_____。
    答案:
    (1)data段中的数据不变。
    (2)212B、212A、2129(答案不唯一)
    (3)X-2、X-1
    (4)((N-1)/16 + 1)*16 其中除法为整除

  3. 将下面的程序编译连接,用Debug加载、跟踪,然后回答问题。

    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
    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 ax, 4c00h
    int 21h
    code ends

    data segment
    dw 0123h, 0456h
    data ends

    stack segment
    dw 0, 0
    stack ends

    end start

    (1)CPU执行程序,程序返回前,data段中的数据为多少?
    (2)CPU执行程序,程序返回前,CS = _____、SS = _____、DS = _____。
    (3)设程序加载后,code段的段地址为X,则data段的段地址为_____,stack段的段地址为_____。
    答案:
    (1)data段中的数据不变。
    (2)2129、212D、212C(答案不唯一)
    (3)X+3、X+4

  4. 如果将1. 2. 3.题中的最后一条伪指令“end start”改为“end”(也就是说,不指明程序的入口),则哪个程序仍然可以正确执行?请说明原因。
    答案:
    只有程序3可以正确运行,在不指明程序入口的情况下,程序默认按照顺序从头开始执行,而3个程序中只有程序3的code段位于最开始的部分,所以只有程序3可以正确运行。

  5. 程序如下,编写code段中的代码,将a段和b段中的数据依此相加,将结果存到c段中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    assume cs:code
    a segment
    db 1, 2, 3, 4, 5, 6, 7, 8
    a ends

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

    c segment
    db 0, 0, 0, 0, 0, 0, 0, 0
    c ends

    code segment
    start:
    ?
    code ends
    end start

    答案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    start:  mov ax, a
    mov ds, ax
    mov ax, b
    mov es, ax
    mov ax, c
    mov ss, ax
    mov bx, 0
    mov cx, 8
    s: mov al, [bx]
    add al, es:[bx]
    mov ss:[bx], al
    inc bx
    loop s
    mov ax, 4c00h
    int 21h
  6. 程序如下,编写code段中的代码,用push指令将a段中的前8个字型数据,逆序存储到b段中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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
    start:
    ?
    code ends

    end start

    答案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    start:  mov ax, b
    mov ss, ax
    mov sp, 16
    mov ax, a
    mov ds, ax
    mov bx, 0
    mov cx, 8
    s: push [bx]
    add bx, 2
    loop s
    mov ax, 4c00h
    int 21h

实验6 实践课程中的程序

  1. 将课程中所有讲解过的程序上机调试,用Debug跟踪其执行过程,并在过程中进一步理解所讲内容。

  2. 编程,完成问题7.9中的程序。
    程序如下:

    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
    assume cs:codesg, ss:stacksg, ds:datasg
    stacksg segment
    dw 0, 0, 0, 0, 0, 0, 0, 0
    stacksg ends

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

    codesg segment
    start: mov ax, datasg
    mov ds, ax
    mov ax, stacksg
    mov ss, ax
    mov sp, 16
    mov bx, 0
    mov cx, 4
    s: push cx
    mov cx, 4
    mov si, 0
    s0: mov al, [bx + si + 3]
    and al, 11011111b
    mov [bx + si + 3], al
    inc si
    loop s0
    add bx, 16
    pop cx
    loop s
    mov ax, 4c00h
    int 21h
    codesg ends
    end start

实验7 寻址方式在结构化数据访问中的应用

编程,将data段中的数据按如下格式写入到table段中,并计算21年中的人均收入(取整),结果也按照下面的格式保存在table段中。

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
assume cs:codesg, ss:stack

stack segment
dw 8 dup (0)
stack ends

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'

dd 16, 22, 382, 1356, 2390, 8000, 16000, 24486, 50065, 97479, 140417, 197514
dd 345980, 590827, 803530, 1183000, 1843000, 2759000, 3753000, 4649000, 5937000

dw 3, 7, 9, 13, 28, 38, 130, 220, 476, 778, 1001, 1442, 2258, 2793, 4037, 5635, 8226
dw 11542, 14430, 15257, 17800
data ends

table segment
db 21 dup ('year summ ne ?? ')
table ends

codesg segment
start: mov ax, stack
mov ss, ax
mov sp, 16
mov ax, table
mov ds, ax
mov ax, data
mov es, ax
mov bx, 0
mov si, 0
mov cx, 21
year: push cx
mov cx, 4
mov di, 0
char: mov al, es:[si]
mov [bx+di], al
inc si
inc di
loop char
add bx, 16
pop cx
loop year
mov cx, 21
mov bx, 0
income: mov ax, es:[si]
mov [5+bx], ax
add si, 2
mov ax, es:[si]
mov [7+bx], ax
add si, 2
add bx, 16
loop income
mov cx, 21
mov bx, 0
staff: mov ax, es:[si]
mov [10+bx], ax
add si, 2
add bx, 16
loop staff
mov cx, 21
mov bx, 0
average:mov ax, [bx+5]
mov dx, [bx+7]
div word ptr [bx+10]
mov [13+bx], ax
add bx, 16
loop average
mov ax, 4c00h
int 21h
codesg ends
end start

实验8 分析一个奇怪的程序

分析下面的程序,在运行前思考,这个程序可以正确返回吗?
运行后再思考:为什么是这种结果?
通过这个程序加深对相关内容的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
assume cs:codesg
codesg segment
mov ax, 4c00h
int 21h
start: mov ax, 0
s: nop
nop
mov di, offset s
mov si, offset s2
mov ax, cs:[si]
mov cs:[di], ax
s0: jmp short s
s1: mov ax, 0
int 21h
mov ax, 0
s2: jmp short s1
nop
codesg ends
end start

分析:
这个程序可以正确返回,程序的入口为mov ax, 0,注意到指令jmp short s1占2字节,于是指令mov di, offset s将s的偏移地址传送到寄存器DI,mov si, offset s2将s2的偏移地址传送到SI,然后再通过通用寄存器ax做中转将s2处的指令复制到s处,最后再跳转至s处执行复制过来的指令。
注意jmp short s1是相对跳转,其直接修改IP寄存器,从s2到s1共有8个字节的偏移,实际上 jmp short s1等价于(ip)=(ip)-8,通过Debug可知第一个nop指令的偏移地址为8,所以再执行了复制过的指令后,IP将指向0,程序按照顺序执行mov ax, 4c00h和int 21h,正确返回。

实验9 根据材料编程

编程:在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串’welcome to masm!’。
编程所需的只是通过阅读、分析下面的材料获得。

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
assume ds:data, cs:code

data segment
db 'welcome to masm!'
data ends

code segment
start: mov ax, data
mov ds, ax
mov ax, 0B800H
mov es, ax

mov bx, 1664
mov si, 0
mov cx, 16
char1: mov al, [si]
mov ah, 10000010B
mov es:[bx], ax
add bx, 2
inc si
loop char1

mov bx, 1824
mov si, 0
mov cx, 16
char2: mov al, [si]
mov ah, 10100100B
mov es:[bx], ax
add bx, 2
inc si
loop char2

mov bx, 1984
mov si, 0
mov cx, 16
char3: mov al, [si]
mov ah, 11110001B
mov es:[bx], ax
add bx, 2
inc si
loop char3

mov ax, 4C00H
int 21H
code ends
end start

实验10 编写子程序

  1. 显示字符串

    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, ss:stack

    stack segment
    dw 16 dup (0)
    stack ends

    data segment
    db 'Welcome to masm!', 0
    data ends

    code segment
    start: mov dh, 8
    mov dl, 3
    mov cl, 2
    mov ax, data
    mov ds, ax
    mov ax, stack
    mov ss, ax
    mov sp, 32
    mov si, 0
    call show_str

    mov ax, 4c00h
    int 21h

    show_str:
    push cx
    push bx
    push ax
    push si
    push di
    push es
    ;using cx, bx, ax, si, di, es
    mov ax, 0b800h
    mov es, ax
    mov bx, 0
    mov di, 0
    mov al, 160
    mul dh
    add bx, ax
    mov al, 2
    mul dl
    add bx, ax ;bx stores address of start character
    mov al, cl ;al stores the color of character
    char: mov ch, 0
    mov cl, ds:[si]
    jcxz zero
    mov ch, al
    mov es:[bx+di], cx
    add di, 2
    inc si
    jmp char
    zero: pop es
    pop di
    pop si
    pop ax
    pop bx
    pop cx
    ret

    code ends
    end start
  2. 解决除法溢出的问题

    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
    assume cs:code, ss:stack

    stack segment
    dw 16 dup (0)
    stack ends

    code segment
    start: mov ax, stack
    mov ss, ax
    mov sp, 32
    mov ax, 4240h
    mov dx, 000fh
    mov cx, 0ah
    call divdw

    mov ax, 4c00h
    int 21h

    divdw: push bx

    mov bx, ax ; bx stores L
    mov ax, dx ; ax stores H
    mov dx, 0
    div cx ; after div, ax holds int(H/N), dx holds rem(H/N)
    push ax ; push int(H/N) temporarily
    mov ax, bx ; ax stores L
    div cx
    mov cx, dx
    pop dx

    pop bx
    ret

    code ends
    end start
  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
    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
    assume cs:code, ss:stack

    stack segment
    dw 16 dup (0)
    stack ends

    data segment
    db 10 dup (0)
    data ends

    code segment
    start: mov ax, data
    mov ds, ax
    mov ax, stack
    mov ss, ax
    mov sp, 32
    mov ax, 12666
    mov si, 0
    call dtoc

    mov dh, 8
    mov dl, 3
    mov cl, 2
    call show_str

    mov ax, 4c00h
    int 21h

    dtoc: push ax
    push si
    push di
    push dx
    push bx
    push cx
    mov di, 0
    mov dx, 0
    mov bx, 10

    devide: mov cx, ax
    jcxz stop
    div bx
    inc di
    push dx
    mov dx, 0
    jmp devide
    stop: mov cx, di
    string: pop bx
    add bx, 30h
    mov [si], bl
    inc si
    loop string

    pop cx
    pop bx
    pop dx
    pop di
    pop si
    pop ax
    ret

    show_str:
    push cx
    push bx
    push ax
    push si
    push di
    push es
    ;using cx, bx, ax, si, di, es
    mov ax, 0b800h
    mov es, ax
    mov bx, 0
    mov di, 0
    mov al, 160
    mul dh
    add bx, ax
    mov al, 2
    mul dl
    add bx, ax ;bx stores address of start character
    mov al, cl ;al stores the color of character
    char: mov ch, 0
    mov cl, ds:[si]
    jcxz zero
    mov ch, al
    mov es:[bx+di], cx
    add di, 2
    inc si
    jmp char
    zero: pop es
    pop di
    pop si
    pop ax
    pop bx
    pop cx
    ret

    code ends
    end start

实验11 编写子程序

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
assume cs:code

stack segment
dw 8 dup (0)
stack ends

data segment
db "Beginner's All-purpose Symbolic Instruction Code.", 0
data ends

code segment
begin: mov ax, stack
mov ss, ax
mov sp, 16
mov ax, data
mov ds, ax
mov si, 0
call letterc

mov ax, 4c00h
int 21h

letterc:
push cx
push si
pushf

mov ch, 0
start: mov cl, ds:[si]
jcxz zero
cmp cl, 97
jb next
cmp cl, 122
ja next
sub cl, 20h
mov ds:[si], cl
next: inc si
jmp start

zero: popf
pop si
pop cx
ret

code ends
end begin

实验12 编写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
assume cs:code

code segment
start:
mov ax, cs
mov ds, ax
mov si, offset do0

mov ax, 0
mov es, ax
mov di, 200h

mov cx, offset do0end - offset do0

cld

rep movsb

mov ax, 0
mov es, ax
mov word ptr es:[0], 200h
mov word ptr es:[2], 0

mov ax, 4c00h
int 21h

do0:
jmp short do0start
db "overflow!"
do0start:
mov ax, cs
mov ds, ax
mov si, 202h
mov ax, 0b800h
mov es, ax
mov di, 12*160+36*2
mov cx, 9
s:
mov al, [si]
mov es:[di], al
inc si
add di, 2
loop s

mov ax, 4c00h
int 21h
do0end:
nop

code ends
end start

实验13 编写、应用中断例程

  1. 编写并安装int 7ch中断例程,功能为显示一个用0结束的字符串,中断例程安装在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
    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
    assume cs:code

    code segment
    start: mov ax, cs
    mov ds, ax
    mov si, offset print
    mov ax, 0
    mov es, ax
    mov di, 200h
    mov cx, offset printend - offset print
    cld
    rep movsb

    mov ax, 0
    mov es, ax
    mov word ptr es:[7ch * 4], 200h
    mov word ptr es:[7ch * 4 + 2], 0
    mov ax, 4c00h
    int 21h

    print:
    push es
    push ax
    push cx
    push dx
    push si
    push di

    mov ax, 0b800h
    mov es, ax
    mov al, 160
    mov ah, 0
    mul dh
    mov di, ax
    add dl, dl
    mov dh, 0
    add di, dx

    mov al, cl
    printstart:
    mov ch, 0
    mov cl, [si]
    jcxz zero
    mov ch, al
    mov es:[di], cx
    add di, 2
    inc si
    jmp printstart

    zero:
    pop di
    pop si
    pop dx
    pop cx
    pop ax
    pop es
    iret
    printend:
    nop

    code ends

    end start
  2. 编写并安装int 7ch中断例程,功能为完成loop指令功能。

    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
    start: mov ax, cs
    mov ds, ax
    mov si, offset lp
    mov ax, 0
    mov es, ax
    mov di, 200h
    mov cx, offset lpend - offset lp
    cld
    rep movsb

    mov ax, 0
    mov es, ax
    mov word ptr es:[7ch * 4], 200h
    mov word ptr es:[7ch * 4 + 2], 0
    mov ax, 4c00h
    int 21h

    lp: push bp
    mov bp, sp
    dec cx
    jcxz lpret
    add [bp + 2], bx
    lpret: pop bp
    iret
    lpend: nop

    code ends

    end start
  3. 下面的程序,分别在屏幕的第2、4、6、8行显示4句英文诗,补全程序。

    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
    assume cs:code
    code segment
    s1: db 'Good,better,best,', '$'
    s2: db 'Never let it rest,', '$'
    s3: db 'Till good is better,', '$'
    s4: db 'And better,best.', '$'
    s: dw offset s1, offset s2, offset s3, offset s4
    row: db 2, 4, 6, 8

    start: mov ax, cs
    mov ds, ax
    mov bx, offset s
    mov si, offset row
    mov cx, 4
    ok: mov bh, 0
    mov dh, _____
    mov dl, 0
    mov ah, 2
    int 10h

    mov dx, _____
    mov ah, 9
    int 21h
    _________
    _________
    loop ok
    mov ax, 4c00h
    int 21h
    code ends
    end start

    答案:

    1
    2
    3
    4
    [si]
    [bx]
    add bx, 2
    inc si

实验14 访问CMOS RAM

编程,以”年/月/日 时:分:秒”的格式,显示当前的日期,时间。
注意:CMOS RAM中存储着系统的配置信息,出了保存时间信息的单元外,不要向其他的单元中写入内容,否则将引起一些系统错误。

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
assume cs:code, ds:data

data segment
s db 9, 8, 7, 4, 2, 0
data ends
code segment
start: mov ax, 0b800h
mov es, ax
mov di, 160 * 12
mov ax, data
mov ds, ax
mov si, 0
mov cx, 6

print: mov al, s[si]
out 70h, al
in al, 71h
call number
cmp si, 2
jb slash
je space
cmp si, 5
jb colon
next: inc si
loop print

mov ax, 4c00h
int 21h

;al->number, es:di->begin
number: push cx
mov ah, al
mov cl, 4
shr ah, cl
and al, 00001111b
add ah, 30h
add al, 30h
mov byte ptr es:[di], ah
mov byte ptr es:[di + 2], al
add di, 4
pop cx
ret

slash: mov byte ptr es:[di], '\'
add di, 2
jmp next

colon: mov byte ptr es:[di], ':'
add di, 2
jmp next

space: mov byte ptr es:[di], ' '
add di, 2
jmp next

code ends

end start

实验15 安装新的int 9中断例程

安装一个新的int 9中断例程,功能:在DOS下,按下’A’键后,除非不再松开,如果松开,就显示满屏幕的’A’;其他键照常处理。

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
assume cs:code

stack segment
db 128 dup (0)
stack ends

code segment
start: mov ax, stack
mov ss, ax
mov sp, 128

push cs
pop ds

mov ax, 0
mov es, ax

mov si, offset int9
mov di, 204h
mov cx, offset int9end - offset int9
cld
rep movsb

push es: [9 * 4]
pop es: [200h]
push es: [9 * 4 + 2]
pop es: [202h]

cli
mov word ptr es: [9 * 4], 204h
mov word ptr es: [9 * 4 + 2], 0
sti

mov ax, 4c00h
int 21h

int9: push ax
push bx
push cx
push es

in al, 60h

pushf
call word ptr cs:[200h]

cmp al, 9eh
jne int9ret

mov ax, 0b800h
mov es, ax
mov bx, 0
mov cx, 2000
s: mov byte ptr es:[bx], 'A'
add bx, 2
loop s

int9ret:pop es
pop cx
pop bx
pop ax
iret

int9end:nop

code ends
end start

实验16 编写包含多个功能子程序的中断例程

安装一个新的int 7ch中断例程,为显示输出提供如下功能子程序

  1. 清屏
  2. 设置前景色
  3. 设置背景色
  4. 向上滚动一行
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
assume cs:code

code segment
start: mov ax, cs
mov ds, ax
mov si, offset screen
mov ax, 0
mov es, ax
mov di, 200h
mov cx, offset screenend - offset screen
cld
rep movsb

mov ax, 0
mov es, ax
mov word ptr es:[7ch * 4], 200h
mov word ptr es:[7ch * 4 + 2], 0
mov ax, 4c00h
int 21h

screen: jmp short set
;考虑到安装中断例程后偏移地址发生了变化,需要重新计算相关的偏移地址
table dw offset sub1 - offset screen + 200h, offset sub2 - offset screen + 200h, offset sub3 - offset screen + 200h, offset sub4 - offset screen + 200h

set: push bx

cmp ah, 3
ja sret
mov bl, ah
mov bh, 0
add bx, bx

;同上
call word ptr cs:(table - screen + 200h)[bx]

sret: pop bx
iret

sub1: push bx
push cx
push es
mov bx, 0b800h
mov es, bx
mov bx, 0
mov cx, 2000
sub1s: mov byte ptr es:[bx], ' '
add bx, 2
loop sub1s
pop es
pop cx
pop bx
ret

sub2: push bx
push cx
push es

mov bx, 0b800h
mov es, bx
mov bx, 1
mov cx, 2000
sub2s: and byte ptr es:[bx], 11111000B
or es:[bx], al
add bx, 2
loop sub2s

pop es
pop cx
pop bx
ret

sub3: push bx
push cx
push es
mov cl, 4
shl al, cl
mov bx, 0b800h
mov es, bx
mov bx, 1
mov cx, 2000
sub3s: and byte ptr es:[bx], 10001111B
or es:[bx], al
add bx, 2
loop sub3s
pop es
pop cx
pop bx
ret

sub4: push cx
push si
push di
push es
push ds

mov si, 0b800h
mov es, si
mov ds, si
mov si, 160
mov di, 0
cld
mov cx, 24
sub4s: push cx
mov cx, 160
rep movsb
pop cx
loop sub4s

mov cx, 80
mov si, 0
sub4s1: mov byte ptr [160*24+si], ' '
add si, 2
loop sub4s1

pop ds
pop es
pop di
pop si
pop cx
ret

screenend:
nop

code ends
end start

参考王爽《汇编语言》实验16:包含多个功能子程序的中断例程 解答
可以用伪指令org x简化该程序
org x表明接下来的指令从偏移地址x开始
修改后的相关指令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
org 200h
screen: jmp short set

table dw sub1, sub2, sub3, sub4

set: push bx

cmp ah, 3
ja sret
mov bl, ah
mov bh, 0
add bx, bx

call word ptr table[bx]

实验17 编写包含多个功能子程序的中断例程

安装一个新的int 7ch中断例程,实现通过逻辑扇区号对软盘进行读写。

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
assume cs:code

code segment
start: mov ax, cs
mov ds, ax
mov si, offset floppyio
mov ax, 0
mov es, ax
mov di, 200h
mov cx, offset floppyioend - offset floppyio
cld
rep movsb

mov ax, 0
mov es, ax
mov word ptr es:[7ch * 4], 200h
mov word ptr es:[7ch * 4 + 2], 0
mov ax, 4c00h
int 21h

floppyio:
push ax
push cx
push dx

add ah, 2
mov al, 1
push ax ;计算相应的ah和al并压栈
mov ax, dx
mov dx, 0
mov cx, 1440
div cx ;计算逻辑扇区号/1440
push ax ;将商即面号压栈
mov ax, dx
mov dl, 18
div dl ;计算逻辑扇区号/1440的余数/18
inc ah
mov ch, al
mov cl, ah ;设置相应的ch和cl
pop ax ;将相应的面号出栈
mov dh, al
mov dl, 0 ;设置相应的dh和dl
pop ax ;将相应的ah和al出栈
int 13h ;调用13h例程进行实际的读写

pop dx
pop cx
pop ax
iret
floppyioend:
nop

code ends
end start

评论和共享

作者的图片

码龙黑曜

iOS开发者/计算机科学/兽人控


华中科技大学 本科在读


Wuhan, China