开学三周,王爽的《汇编语言》(第三版)总算是基本上看完了,本文是总结的第三部分也是最后一部分(大概),主要是书后的综合研究
研究试验1 搭建一个精简的C语言开发环境
TC进行连接需要如下的文件:
- C0S.OBJ
- CS.LIB
- EMU.LIB
- GRAPHICS.LIB
- MATHS.LIB
研究试验2 使用寄存器
- 编一个程序ur1.c
1
2
3
4
5
6
7
8
9main ()
{
_AX = 1;
_BX = 1;
_CX = 2;
_AX = _BX + _CX;
_AH = _BL + _CL;
_AL = _BH + _CH;
} - 用Debug加载ur1.exe,用u命令查看ur1.exe编译后的机器码和汇编代码。
思考:main函数的代码在什么段中?用Debug怎样找到ur1.exe中main函数的代码?
回答:main函数的代码在CS段中,需要知道main函数的偏移地址才能找到main函数的代码。 - 用下面的方法打印出ur1.exe被加载运行时,main函数在代码段中的偏移地址。 思考:为什么这个程序能够打印出main函数在代码段中的偏移地址?
1
2
3
4main ()
{
printf("%x\n", main);
}
回答:由学过的知识可知,C语言中用指针表示地址,这里的printf打印了main函数的入口地址,也即main函数在代码段中的偏移地址。 用Debug加载ur1.exe,根据上面打印出的main函数的偏移地址,用u命令察看main函数的汇编代码。仔细找到ur1.c中每条C语句对应的汇编代码。
地址 汇编语句 C语句 076A:01FA PUSH BP ____ 076A:01FB MOV BP,SP ____ 076A:01FD MOV AX,0001 _AX = 1 076A:0200 MOV BX,0001 _BX = 1 076A:0203 MOV CX,0002 _CX = 2 076A:0206 MOV AX,BX ____ 076A:0208 ADD AX,CX _AX = _BX + _CX 076A:020A MOV AH,BL ____ 076A:020C ADD AH,CL _AH = _BL + _CL 076A:020E MOV AL,BH ____ 076A:0210 ADD AL,CH _AL = _BH + _CH 076A:0212 POP BP ____ 076A:0213 RET ____ 注意:在这里,对于main函数汇编代码开始处的“push bp mov bp,sp”和结尾处的“pop bp”,这里只了解到:这是C编译器安排的为函数中可能使用到bp寄存器而设置的,就可以了。
通过main函数后面有ret指令,我们可以设想:C语言将函数实现为汇编语言中的子程序。研究下面程序的汇编代码,验证我们的设想。
1
2
3
4
5
6
7
8
9
10
11
12
13void 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
14PUSH 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 使用内存空间
编一个程序um1.c:
1
2
3
4
5
6
7
8
9
10
11
12
13main()
{
*(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:01FA PUSH BP ____ ____ 076A:01FB MOV BP,SP ____ ____ 076A:01FD MOV BYTE PTR [2000],61 *(char *)0x2000=’a’ 07C4:2000 -> 61 076A:0202 MOV WORD PTR [2000],000F *(int *)0x2000=0xf 07C4:2000-07C4:2001 -> 0F 00 076A:0208 MOV BX,2000 ____ ____ 076A:020B MOV ES,BX ____ ____ 076A:020D MOV BX,1000 ____ ____ 076A:0210 ES: 076A:0211 MOV BYTE PTR [BX],61 *(char far *)0x20001000=’a’ 2000:1000 -> 61 076A:0214 MOV AX,2000 _AX=0x2000 ____ 076A:0217 MOV BX,AX ____ ____ 076A:0219 MOV BYTE PTR [BX],62 *(char *)_AX=’b’ 07C4:2000 -> 62 076A:021C MOV BX,1000 _BX=0x1000 ____ 076A:021F ADD BX,BX ____ ____ 076A:0221 MOV BYTE PTR [BX],61 *(char *)(_BX + _BX)=’a’ 07C4:2000 -> 61 076A:0224 MOV BX,AX ____ ____ 076A:0226 MOV AL,[BX] ____ ____ 076A:0228 XOR CX,CX ____ ____ 076A:022A ADD BX,1000 ____ ____ 076A:022E ADC CX,2000 ____ ____ 076A:0232 MOV ES,CX ____ ____ 076A:0234 ES: 076A:0235 MOV [BX],AL Ž(char far *)(0x20001000+BX)=*(char *)AX 2000:3000 -> 61 076A:0237 POP BP ____ ____ 076A:0238 RET ____ ____ 编一个程序,用一条C语句实现在屏幕的中间显示一个绿色的字符”a”。
1
2
3
4main()
{
*(int far *)0xb80007d0=0x261
}分析下面程序中所有函数的汇编代码,思考相关的问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14int 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
12PUSH 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
12PUSH 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是为了保护与还原现场。分析下面程序的汇编代码,思考相关的问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15int 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
8PUSH 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
9PUSH BP
MOV BP, SP
MOV AX, [01A6]
ADD AX, [01A8]
MOV [01AA], AX
MOV AX, [01AA]
JMP 021C
POP BP
RET答案:
在该情况下,C语言将返回值存放在通用寄存器AX中。下面的程序向安全的内存空间写入从“a”到“h”8个字符,理解程序的含义,深入理解相关的知识。(注意:请自己学习、研究malloc函数的用法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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函数编程
把程序F.C保存在C:\MINIC下,对其进行编译,连接,思考相关的问题。
问题:
①编译和连接哪个环节会出问题?
②显示出的错误信息是什么?
③这个错误信息可能和哪个文件有关?
答案:
①连接环节出现了错误
②在C0S模块中未定义的标号_main
③和C0S.OBJ这个文件有关用学习汇编语言时使用的LINK.EXE对TC.EXE生成的F.OBJ文件进行连接,生成F.EXE。用DEBUG加载F.EXE,察看整个程序的汇编代码。思考相关的问题。
问题:
①F.EXE的程序代码总共有多少字节?
②F.EXE的程序能正确返回吗?
③F函数的偏移地址是多少?
答案:
①F.EXE的程序代码总共有1DH个字节
②F.EXE的程序不能正确的返回
③F函数的偏移地址为0写一个程序M.C。
1
2
3
4
5main ()
{
*(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函数的汇编代码没有不同用Debug对m.exe进行跟踪:
(1)找到对main函数进行调用的指令的地址
(2)找到整个程序返回的指令
注意:使用g命令和p命令。
答案:
(1)对main函数进行调用的指令的地址为076A:011A CALL 01FA
(2)整个程序返回的指令为076A:0156 INT 21思考如下几个问题:
(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)有用LINK.EXE对C:\MINIC目录下的C0S.OBJ进行连接,生成C0S.EXE。
用Debug分别察看C0S.EXE和M.EXE的汇编代码。注意:从头开始察看,两个文件中的程序代码有和相同之处?
两个程序的代码基本相同,且都是在011A调用了CALL指令,在0156调用了INT 21中断用Debug找到M.EXE中调用main函数的CALL指令的偏移地址,从这个偏移地址开始向后察看10条指令;然后用Debug加载C0S.EXE,从相同的偏移地址开始向后察看10条指令,对两处的指令进行对比。
M.EXE和C0S.EXE在偏移地址011A之后的10条指令除了跳转指令的跳转地址有所不同外几乎完全相同。下面,我们用汇编语言编一个程序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
21cs:code
data
db 128 dup (0)
data ends
code
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在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
21MOV 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
...在新的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
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
60MOV 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完成下面的试验。
写一个程序A.C:
1
2
3
4
5
6
7
8
9
10
11
12
13void 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
11PUSH BP
MOV BP,SP
MOV AX,0002
PUSH AX
MOV AL,61
PUSH AX
CALL 020B ;调用showchar函数
POP CX
POP CX
POP BP
RETshowchar函数的汇编代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16PUSH 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以及寻址在栈中取得对应的参数。写一个程序B.C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18void 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函数以此得知参数的个数。实现一个简单的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
70void 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++;
}
}
}