实验1 查看CPU和内存,用机器指令和汇编指令编程
1.预备知识:Debug的使用
我们以后所有的实验中,都将用到debug程序,首先学习一下它的主要用法。
(1)什么是Debug?
Debug是DOS、Windows都提供的实模式(8086方式)程序的调试工具。使用它,可以查看CPU各种寄存器中的内容、内存的情况和在机器码级跟踪程序的运行。
知识小贴士:实模式和保护模式
intel的80286处理器于1982年问世了,它的地址总线位数增加到了24位,因此可以访问到16M的内存空间。更重要的是从此开始引进了一个全新理念--保护模式。这种模式下内存段的访问受到了限制。访问内存时不能直接从段寄存器中获得段的起始地址了,而需要经过额外转换和检查。为了和过去兼容,80286内存寻址可以有两种方式,一种是先进的保护模式,另一种是老式的8086方式,被成为实模式。系统启动时处理器处于实模式,只能访问1M空间,经过处理可进入保护模式,访问空间扩大到16M,但是要想从保护模式返回到实模式,你只有重新启动机器。还有一个致命的缺陷是80286虽然扩大了访问空间,但是每个段的大小还是64k,程序规模仍受到限制。因此这个先天低能儿注定寿命不会很久。很快它就被天资卓越的兄弟--80386代替了。
80386是一个32位的CPU,也就是它的ALU数据总线是32位的,同时它的地址总线与数据总线宽度一致,也是32位,因此,其寻址能力达到4GB。对于内存来说,似乎是足够了。从理论上说,当数据总线与地址总线宽度一致时,其CPU结构应该简洁明了。但是,80386无法做到这一点。作为X86产品系列的一员,80386必须维持那些段寄存器的存在,还必须支持实模式,同时又要能支持保护模式,这给Intel的设计人员带来很大的挑战。Intel选择了在段寄存器的基础上构筑保护模式,并且保留段寄存器16位。在保护模式下,它的段范围不再受限于64K,可以达到4G。这一下真正解放了软件工程师,他们不必再费尽心思去压缩程序规模,软件功能也因此迅速提升。
从8086的16位到80386的32位处理器,这看起来是处理器位数的变化,但实质上是处理器体系结构的变化,从寻址方式上说,就是从“实模式”到“保护模式”的变化。从80386以后,Intel的CPU经历了80486、Pentium、PentiumII、PentiumIII等型号,虽然它们在速度上提高了好几个数量级,功能上也有不少改进,但基本上属于同一种系统结构的改进与加强,而无本质的变化,所以我们把80386以后的处理器统称为IA32(32 Bit Intel Architecture)。
(2)我们用到的Debug功能
R命令查看、改变CPU寄存器的内容;
D命令查看内存中的内容;
E命令改写内存中的内容;
U命令将内存中的机器指令翻译成汇编指令;
T命令执行一条机器指令;
A命令以汇编指令的格式在内存中写入一条机器指令。
P命令 到了 int 21H,我们要用P命令执行;进入循环时用p命令也可以退出循环
G命令g命令调试时能跳到想去的地址,特别是调试循环代码时
Debug的命令比较多(详细请看资料),共有20多个,但这6个命令是和汇编学习密切相关的。在以后的实验中,我们还会用到一个P命令。
(3)进入Debug。
(4)用R命令查看、改变CPU寄存器的内容。由于开发环境的不同,cmd调试结果也是不同的,这里只是将它们的意义说明。
-r
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0B04 ES=0B04 SS=0B04 CS=0B04 IP=0100 NV UP EI PL NZ NA PO NC
0B04:0100 E80400 CALL 0107
看图说明:
【1】ax,bx,cx,dx,cx,ip是我们讲过的6中寄存器,其中ax,bx,cx,dx都是通用寄存器,但是它们也有特定的用途,以后慢慢了解。cs和ip寄存器是专门寄存器。
【2】0B04:0100 E80400 CALL 0107
含义:CS=0B04 IP=0100 也就是CPU将要读取的从0B04:0100开始内存单元的指令,这个指令机器码是:E80400;debug程序将这个机器码翻译成汇编指令是CALL 0107
理解:这个CPU指向的地址cs:ip就是CPU将要读取并执行的指令代码。当然,这个代码对于我们来说是没有任何意义的。
在debug状态下,我们可以使用R命令修改上述寄存器的值。
例如:
r cs 指令含义:修改cs寄存器值
r ip 指令含义:修改ip寄存器值
r ax 指令含义:修改ax寄存器值
(5)使用D命令查看内存中的内容。
直接显示从某单元地址开始内容;
指令格式:d 段地址:偏移地址
例如:
-d 0b04:0
0B04:0000 CD 20 FF 9F 00 9A EE FE-1D F0 4F 03 68 05 8A 03 . ........O.h...
0B04:0010 68 05 17 03 68 05 1C 04-01 01 01 00 02 FF FF FF h...h...........
0B04:0020 FF FF FF FF FF FF FF FF-FF FF FF FF 15 05 4E 01 ..............N.
0B04:0030 28 0A 14 00 18 00 04 0B-FF FF FF FF 00 00 00 00 (...............
0B04:0040 05 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0B04:0050 CD 21 CB 00 00 00 00 00-00 00 00 00 00 20 20 20 .!...........
0B04:0060 20 20 20 20 20 20 20 20-00 00 00 00 00 20 20 20 .....
0B04:0070 20 20 20 20 20 20 20 20-00 00 00 00 00 00 00 00 ........
【1】中间的红色部分是从指定地址开始的内存单元中的内容,每一行为16个字节,用16进制格式输出。内存单元是从低地址向高地址显示的。在红色区域左边的是每行的起始地址(段地址:偏移地址);偏移地址的变化都是16的整数倍,在debug中就是10H。红色区域的右侧是将内存单元存储的值所对应的ASCII码字符,如果不在ASCII码的范围,以“.”形式列出(表示不可识别的ASCII码)。
【2】在debug中,使用D命令显示的内存单元,是为了便于我们调试程序时查看内存的状态。此处注意,真正的内存依然是单列、连续的线性存储空间。而不是像debug中显示的按照表的形式存在。
【3】使用d命令后,我们可以继续使用单个的“d”命令,继续显示连续的内存单元内容。每次显示128个单元(128个字节)内容。
【4】使用 d 段地址:偏移地址(开始) 偏移地址(结束)来显示一段连续内存单元的内容;例如:-d 0b04:0 7 显示的是从0~7共8个内存单元的内容。
(6)用Debug的E命令改写内存中的内容:
【1】可以使用E命令来改写内存中的内容,按照字节单元的方式修改
例如:-e 0b04:0 从内存单元0b04:0开始,改写内存单元的内容。回车键结束;注意使用空格。
【2】可以使用E命令修改内存中内容,直接按照ASCII码的方式修改
例如:
-e 0b04:100 'a','b','c','d','e',1,'a+b' ;修改内存单元,直接写入ASCII码
-d 0b04:100 ;查看内存单元
0B04:0100 61 62 63 64 65 01 61 2B-62 8A F1 80 FC 01 74 09 abcde.a+b.....t.
【3】使用E命令修改代码段中指令的代码,这个太累了。不推荐。
(7)使用U命令查看指定内存单元开始的汇编代码和机器码。
-u 0b04:100
0B04:0100 E80400 CALL 0107
0B04:0103 32C0 XOR AL,AL
0B04:0105 AA STOSB
0B04:0106 C3 RET
0B04:0107 B400 MOV AH,00
……
这里我们看见右侧的是汇编代码,中间的是机器码,左侧的是内存单元地址(物理地址)。
小技巧:如果将某个段内存的段地址赋值给段寄存器,则在执行命令时,使用寄存器名称即可。例如:-u 0b04:100 等价于-u cs:100(因为cs=0b04)
(8)使用t命令执行cs:ip指向的内存单元的指令,单步的执行。注意执行后,每个寄存器值的变化。
(9)使用A命令,以汇编指令的形式在内存中写入机器指令。
上面使用E命令可以在内存中修改单元内容,达到写入机器指令的目的,太累了,写的都是机器码;这个命令就很好,直接按照汇编指令形式写入指令;
注意此指令不接受汇编的伪指令。只接受正确的汇编指令。
使用回车键结束汇编指令的写入。
2. 实验任务
(1)使用debug,将下面的程序段写入内存,逐条执行,观察每条指令执行后CPU中相关寄存器中内容的变化。
机器码 汇编指令
b8 20 4e mov ax,4E20H
05 16 14 add ax,1416H
bb 00 20 mov bx,2000H
01 d8 add ax,bx
89 c3 mov bx,ax
01 d8 add ax,bx
b8 1a 00 mov ax, 001aH
bb 26 00 mov bx,0026H
00 d8 add al,bl
00 dc add ah,bl
00 c7 add bh,al
b4 00 mov ah,0
00 d8 add al,bl
04 9c add al,9cH
提示,可用E命令和A命令以两种方式将指令写入内存。注意用T命令执行时,cs:ip的指向。
打开cmd窗口(命令提示符窗口),键入debug;回车
-r ;查看寄存器的状态
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0B04 ES=0B04 SS=0B04 CS=0B04 IP=0100 NV UP EI PL NZ NA PO NC
0B04:0100 E80400 CALL 0107
我们发现CS=0B04 ; IP=0100,也就是说CPU从这里读取代码并执行。我们也在这开始吧。(注:这个是我的系统debug,你的机器系统不一样,导致debug后寄存器状态不同。)
【1】首先使用E命令
-e cs:100 ;使用e命令在cs:100开始修改内存单元内容
0B04:0100 E8.b8 04.20 00.4e 32.05 C0.16 AA.14 C3.bb B4.00
0B04:0108 00.20 8A.01 F1.d8 ……
使用e命令在cs:100开始修改内存单元内容,将上边的机器码逐个写入内存中。注意红色的是CPU要执行的机器码。回车后修改完毕。
-u cs:100
0B04:0100 B8204E MOV AX,4E20
0B04:0103 051614 ADD AX,1416
0B04:0106 BB0020 MOV BX,2000
使用u命令查看下,看看汇编指令是否正常?
……太累了,此处省略一万字。将所有的机器码都使用上述方式用E命令写入到cs:100开始的内存单元中。
【2】使用a命令,直接写入汇编指令。
-a cs:100
0B04:0100 mov ax, 4e20
0B04:0103 add ax,1416H^ Error
0B04:0103 add ax,1416
0B04:0106 mov bx,2000
0B04:0109 add ax,bx
0B04:010B mov bx,ax
0B04:010D add ax,bx
0B04:010F mov ax,001a
0B04:0112 mov bx,0026
0B04:0115 add al,bl
0B04:0117 add ah,bl
0B04:0119 add bh,al
0B04:011B mov ah,0
0B04:011D add al,bl
0B04:011F add al,9c
在cs:100开始的内存单元处,使用汇编指令写入执行代码。
注意红色的为什么报错?在debug中,一律按照16机制计算(不带H),如果是十进制数值,你需要转换为16进制。
写汇编源程序时,你可以随便使用任何进制,因为有编译器自动给你转换了。
我们发现,使用a命令比较方便,但这二种方式的结果是一样的,都是在cs:100开始的内存单元中写入了这些指令。
我们使用d命令查看下这段内存的内容:
-d cs:100
0B04:0100 B8 20 4E 05 16 14 BB 00-20 01 D8 89 C3 01 D8 B8 . N..... .......
0B04:0110 1A 00 BB 26 00 00 D8 00-DC 00 C7 B4 00 00 D8 04 ...&............
0B04:0120 9C FC 00 74 20 80 FC 01-75 22 3A CE 75 05 80 3C ...t ...u":.u..<
我们发现,内存单元中也都是存储着16进制的数据,跟其他内存空间存储的结构是一样的,但这里存储的是CPU执行的代码。
从而验证了,内存单元存储的数据不区分数值还是指令,在于你在应用层面上的解析。
【3】怎么执行?使用T命令,确保cs存储的就是我们期望的代码段的段地址,ip存储的也是我们期望的开始偏移地址,如果不是,我们可以使用r命令修改cs或ip的值。
-r ;显示寄存器状态值,
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0B04 ES=0B04 SS=0B04 CS=0B04 IP=0100 NV UP EI PL NZ NA PO NC
0B04:0100 B8204E MOV AX,4E20 ;这条指令就是CPU将要读取的指令
-t ;执行MOV AX,4E20指令
AX=4E20 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0B04 ES=0B04 SS=0B04 CS=0B04 IP=0103 NV UP EI PL NZ NA PO NC
0B04:0103 051614 ADD AX,1416
执行完毕后,我们发现ax寄存器的值变化了。
按要求,逐步执行t命令,体会汇编指令导致寄存器变量值的变化。
(2)将下面3条指令写入从2000:0开始的内存单元中,利用这3条指令计算2的8次方。
mov ax,1
add ax,ax
jmp 2000:0003
【1】使用a命令在2000:0处写入这3条代码
-a 2000:0
2000:0000 mov ax, 1
2000:0003 add ax,ax
2000:0005 jmp 2000:0003
使用u命令我们查看下是否写入成功;
-u 2000:0
2000:0000 B80100 MOV AX,0001
2000:0003 01C0 ADD AX,AX
2000:0005 EBFC JMP 0003
【2】使用r命令查看寄存器状态
-r
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0B04 ES=0B04 SS=0B04 CS=0B04 IP=0100 NV UP EI PL NZ NA PO NC
发现CS=0B04 IP=0100不是我们期望的,我们把代码写入了2000:0000H了。希望CPU从2000:0000H开始执行。故将cs和ip修改为2000和0000;
使用r命令直接修改。
-r cs
CS 0B04
:2000
-r ip
IP 0100
:0
【3】使用t命令执行代码,注意观察ax值的变化。由于jmp 2000:0003指令导致这段程序流程为死循环,执行8次循环后,使用q命令直接退出吧。
(3)查看内存中的内容。
PC机主板上的ROM中写有一个生产日期,在内存FFF00H~FFFFFH的某几个单元中,请找到这个生产日期,并试图改变它。
【1】使用d命令查看内存单元的内容,由于物理地址是:FFF00H,我们可以假定偏移地址是0,那么段地址是FFF0H,也就是我们使用D命令查询的是从FFF0:0~FFF0:00FFH的范围。
打开cmd,键入debug
-d fff0:0 ff
……
FFF0:00F0 EA 5B E0 00 F0 31 30 2F-30 35 2F 30 37 00 FC AD .[...10/05/07...
我们发现在00f5开始的内存单元存储着主板的生产日期。看红色标记。
尝试使用e命令修改此段内容,发现成功了!(兴奋!),重新打开cmd,查看内存发现显示依然是原来的结果,为毛?因为只读,不让你改。这里只是模拟模式一个假象。在实模式下,你试试就知道了。
(4)向内存从B800H开始的单元中填写数据,如:
-e b810:0000 01 01 02 02 02 03 03 04 04
观察产生的现象;再改变填写的地址,观察产生的现象。
我们发现,写入的数据在屏幕的第1行第9列开始显示ASCII字符。其中2个字节代表显示一个字符,前一个字节代表字符本身,后一个字符代表字符的属性。
结论:在DOS或命令提示符窗口下,B800:0000开始是显示缓冲区。以后我们多次接触这个。