相同的功能,不同的代码

1.用一种不同的分段方法,从另一个不同角度理解处理器的分段内存访问机制
2.计算机中,指定的执行并非总是按它们的自然排列顺序进行的,执行流程也会因为各种原因变化。
3.认识几种新指令,包括movsb,movsw,inc,dec,cld,std,div,neg,cbw,cwd,sub,idiv,jcxz,cmp等
4.认识Intel8086标志寄存器FLAGS的各个标志位,了解条件转移指令
5.认识计算机中的负数
6.学习Bochs调试程序的更多技巧,包括察看FLAGS寄存器各标志位的状态

6.1.代码清单

6.2.跳过非指令的数据区

源程序第8到10行,声明了非指令的数据。一般所有处理器指令都应顺序存放,在它们中间不允许夹杂非指令的普通数据,因为它们不能作为指令执行。但,如有办法让处理器执行不到这些非指令的内容,则另当别论。
jmp near start,如上一章讲到的,像jmp near start这种指令,机器指令的操作码是0xE9,操作数是一个16位的相对偏移量,叫做相对近转移。

6.3.在数据声明中使用字面值

专门定义一个存放字符串的数据区,当要显示它们的时候,再用指令取出来,一个一个地传送到显示缓冲区。这样,负责屏幕上显示的指令就要和显示的内容无关了。
源程序第8,9行,两行的目的是声明要显示的内容。在NASM里,"\"是续行符。
用伪指令db声明字符的ASCII码数据时也可使用字面值。编译阶段,编译器将把'L','a'等转换成与它们等价的ASCII代码。

6.4.段地址的初始化

汇编语言源程序的编译符合一种假设,即编译后的代码将从某个内存段中,偏移地址为0的地方开始加载。这样,如果有一个标号"label_a",它在编译时计算的汇编地址是0x05,那么,程序被加载到内存后,它在段内的偏移地址仍然是0x05,任何使用这个标号访问内存的指令不会有问题。
但是,如程序加载时,不是从段内偏移地址为0的地方开始的,而是0x7c00,则label_a的实际偏移地址就是0x7c05。这时,所有访问label_a的指令仍然会访问偏移地址0x05。上一章,因为知道程序将来的加载位置是0x0000:0x7c00,所以才有:
mov [0x7c00+number+0x00], d1
在主引导程序中,访问内存的指令很多,如都要加上0x7c00是很麻烦的。问题的根源是程序在加载时,没有从段内偏移地址为0的地方开始。
Intel处理器的分段策略有灵活性,逻辑地址0x0000:0x7c00对应的物理地址是0x07c00,该地址又是段0x07c0的起始地址。因此,这个物理地址还对应着另一个逻辑地址0x07c0:0000。
我们看把这512字节的区域看成一个单独的段,段的基地址是0x07c0,段长512字节。该段的最大长度可以为64KB,但这里,我们实际上仅用512字节。尽管BIOS将主引导扇区加载到物理地址0x07c00处,但我们却可以认为它是从0x07c0:0x0000处开始加载的。
这种情况下,如果执行指令
mov [0x05], dl
处理器将把数据段寄存器DS的内容【0x07c0】左移4位,加上指令中指定的偏移地址【0x05】,形成物理内存地址0x07c05,并将寄存器DL中的内容传送到该处。
所以,源程序第13,14行,通过传送指令将数据段寄存器DS的内容设置为0x07c0。源程序第16,17行,使附加段寄存器ES的内容指向显示缓冲区所在的段0xb800。

6.5.段之间的批量数据传送

movsb或movsw指令,通常用于把数据从内存中的一个地方批量地传送到另一个地方。处理器把它们看成是数据串,但是,movsb的传送是以字节为单位的,movsw的传送是以字为单位的。
movsb和movsw指令执行时,原始数据串的段地址由DS指定,偏移地址由SI指定,简写为DS:SI;要传送到的目的地址由ES:DI指定;传送的字节数或字数由CS指定。除此之外,还要指定正向传送还是反向传送,正向表示传送操作方向是从内存区低地址端向高地址端。正向传送时,每传送一个字节或一个字,SI和DI加1或2;反向传送时,每传送一个字节或一个字,SI和DI减去1或2。无论正向还是反向传送,不管每次传送的是字节还是字,每传送一次,CX的内容自动减去1.
8086处理器里,有一个特殊的寄存器,叫做标志寄存器FLAGS。作为一个例子,它的第6位是ZF,即零标志。当处理器执行一条算术或逻辑运算指令后,算术逻辑部件送出的结果除了送到指令中指定位置【目的操作数指定的位置外】,还送到一个或非门。或非门的输入全为0时,输出位1。输入不全位0,或全部为1时,输出为0。或非门的输出送到一个触发器,就是标志寄存器的ZF位。就是说,如果计算结果位0,这一位被置为1,表示计算结果为零是"真"的。否则,清除此位。
也允许通过指令设置一些标志,来改变处理器的运行状态。如,第10位是方向标志DF,通过将这一位清零或置1,就能控制movsb和movsw的传送方向。
源程序第19行是方向标志清零指令cld。这是个无操作数指令,与其相反的是置方向标志指令std。cld指令将DF标志清零,以指示传送是正方向的。std,将DF标志置位,此时,传送的方向是从高地址到低地址的。
源程序第20行,设置SI寄存器的内容是源串的首地址,也就是标号mytext处的汇编地址。
源程序第21行,设置目的地址首地址到DI寄存器。屏幕上的第一个字符的位置对应着0xB800段的开始处,所以设置DI的内容为0.
第22行,设置要批量传送的字节数到CX寄存器,因为数据串在两个标号number和mytext之间声明的,且标号代表的是汇编地址,所以,汇编语言允许将它们相减并除以2来得到这个数值。
第23行,是movsw指令,操作码是0xA5,该指令无操作数。用movsw而不是movsb的原因是每次需传送一个字。单纯的movsb和movsw只能执行一次,如希望处理器反复执行,需加上指令前缀rep,意思是CX不为零则重复。rep movsw的操作码是0xF3 0xA5,它将重复执行movsw直到CX的内容为零。

6.6.使用循环分解数位

声明标号number并从此处开始初始化5字节的目的主要是保存数位。为了访问标号number处的数位,需获取它在内存段中的偏移地址。
源程序29行,将AX的内容传送到BX,使BX指向该处的偏移地址。这等效于
mov bx, number
第29~37行依旧做的是分解数位的事。循环依靠loop。
loop指令的功能是重复执行一段相同的代码,处理器在执行它的时候会顺序做两件事:
将寄存器CX的内容减一
如果CX的内容不为零,转移到指定的位置处执行,否则,顺序执行后面的指令。

loop digit,它的机器指令操作码是0xE2,后面跟着一个字节的操作数,且也是相对于标号处的偏移量,是在编译阶段,编译器用标号digit所在位置的汇编地址减去loop指令的汇编地址,再减去loop指令的长度【2】来得到的。
为了使loop正常工作,需要一些准备。
源程序30行,将循环次数送到CX寄存器。
源程序第31行,将除数10传送到寄存器SI。
源程序第33~37行是循环体,每次循环都会执行这些代码,主要是做除法并保存每次得到的余数。每次除法之前要先将DX清零以得到被除数的高16位,这是源程序33行做的。
做完除法后,第35行,将DL中得到的余数传送到BX所指示的内存单元中去。
mov [bx], dl,把DL中的内容传送到以DS的内容为段地址,以BX的内容为偏移地址的内存单元中去。mov bx,dl是把内容传送到BX中。
在8086处理器上,如要用寄存器来提供偏移地址,只能使用BX,SI,DI,BP,不能使用其他寄存器。所以,以下指令都是非法的:
mov [ax], dl
mov [dx], bx
原因是,寄存器BX的最初功能之一就是用来提供数据访问的基地址,所以又叫基地址寄存器。之所以不能用SP,IP,AX,CX,DX,这是一种硬性规定,没特别理由。
且设计8086处理器时,每个寄存器都有自己的特殊用途,如AX是累加器,与它有关的指令还会做指令长度上的优化【较短】;CX是计数器;DX是数据寄存器,除了作为通用寄存器使用外,还专门用于和外设之间进行数据传送操作;
可在任何带有内存操作数的指令中使用BX,SI,或DI提供偏移地址。
做完一次除法,保存了数位后,源程序36行,用于将BX中的内容加一,以指向下一个内存单元。inc是加一指令,操作数可是8位或16位的寄存器,也可是字节或字内存单元。功能上讲,它和:
add bx, 1
是一样的,但前者机器码更短,速度更快。
inc al
inc byte [bx]
inc word [label_a]
第一条指令执行时,处理器将寄存器AL中的内容加1;第二条指令执行时,将寄存器BX所指向的内存单元的内容加一;就是将段寄存器DS内容左移4位,加上寄存器BX的内容,形成20位物理地址,然后,将该地址处的内容【字节】加一。
第三条是将段寄存器DS内容左移4位,加上label_a代表的汇编地址,物理地址处1个字的内容加一。

6.7.计算机中的负数

6.7.1.无符号数和有符号数

用伪指令db声明的数据都只有一个字节的长度。
对-1,其实等于0-1,做一次二进制减法,用二进制数0,减去二进制数1。结果是...1111111。相减过程不断向左边借位。再比如-2,用0-2得到,二进制世界里,是二进制0减去二进制10,结果是....111111111111110,同样,相减的过程要向左借位。
计算机中,数字保存在寄存器里,在16位处理器里,寄存器通常是8位和16位的。因此,以上相减的结果,只能保留最右边8位或16位。
当然数据还可以保存在内存中,或者编译后的二进制文件中。
在二进制文件中,数据是用伪指令db或dw等定义的。但数据的表示形式和它们在寄存器中的形式相同,以下代码片段很清楚说明了这一点。
data0 db -1	;初始化为0xFF
data1 db -2	;初始化为0xFE
data2 dw -1	;初始化为0xFFFF
data3 dw -2	;初始化为0xFFFE
我们知道0xFF等于十进制数255,但现在它又是十进制数-1,哪一个才是正确的。应该以哪一个为准。
一个良好的解决方案是,将计算机中的数分成两大类:无符号数和有符号数。无符号数。在8位的字节运算中,范围是0x00~0xFF,即十进制的0~255。在16位的字运算中,范围是0x0000~0xFFFF,即十进制的0~65535;在将来要讲的32位运算中,无符号数的范围是0x00000000~0xFFFFFFFF,即十进制的0~4294967295。
相反地,有符号数是分正负的,且规定,数的正负通过它的最高位来辨别。如最高位是0,则是正数;如是1,则是负数。如此8位的字节运算环境中,正数的范围0x00~7F,即十进制的0~127;负数的范围是0x80~0xFF,即十进制的-128~-1。
正的有符号数,和与它同值的无符号数相同。负数不同,对0x8000~0xFFFF这些负数,都是用0减去它们相对应的正数得到的。想知道对应的正数?只需用0减去这个负数就行。
0x00-0x80=0x80【十进制数128】
0x00-0xFF=0x01【十进制数1】
所以,0x80~0xFF这个范围内的有符号数,对应着十进制数-128~-1。
在8086处理器中,有一条指令专门做这件事,它就是neg。neg指令中带有一个操作数,可以是8位或16位的寄存器,或者内存单元。如
neg al
neg dx
neg word [label_a]
它的功能很简单,用0减去指令中指定的操作数。如AL中内容是0x08【十进制8】,执行neg al之后,AL中的内容变为0xF8【十进制数-8】;如果AL中的内容为11000100【十进制数-60】,执行neg al后,AL中的内容为0x3C【十进制数60】。
相应地,在16位字运算环境中,正数的范围是0x0000~0x7FFF,即十进制的0~32767,负数的范围是0x8000~0xFFFF,即十进制的-32768~-1。
使用指令时注意数据不要超出可表示范围
db 255			;正确,可看成声明无符号数
db -125		;正确
db -240		;错误,超过字节表示范围,会被截断
dw -30001	;正确
32位有符号数是16位和8位有符号数的超集;16位有符号数是8位有符号数的超级;相互间有重叠部分。
显然,一个8位有符号数,要想用16位形式表示,只需将最高位,也就是表示符号的那位,扩展到高8位。为了方便,处理器专门设计了两条指令来做这件事:cbw【Convert byte to Word】和cwd【Convert Word to Double-word】
cbw没有操作数,操作码为98,它的功能是,将寄存器AL中的有符号数扩展到整个AX。cwd也没有操作数,操作码为99。它的功能是,将寄存器AX中的有符号数扩展到DX:AX。
尽管有符号数的最高位通常称为符号位,但并不意味着它仅仅用来表示正负号。事实上,通过上面的讲述和实例可以看出,它既是数的一部分,又用来判断正负。

6.7.2.处理器视角中的数据类型

假如寄存器AX中的内容是0xB23C,它到底是无符号数45628,还是-19908。
答案是,这是你自己的事,取决于你怎么看待它。对处理器的多数指令来说,执行的结果和操作数的类型没关系。如
mov ah, al
mov ah, 0xf0
inc ah
再考虑加法运算,比如
mov ax,0x8c03
add ax,0x05
运算过程,数的视角要统一。
执行后,0x8c08。add指令适用于无符号,有符号数。
对减法,如10-3,可看成10+(-3)
mov ah,10
mov al,-3
add ah,al
因此,很多处理器内部不构造减法电路,使用加法电路做减法。
sub,目的操作数可以是8位或16位通用寄存器,也可以是8位或16位内存单元。源操作数可以是通用寄存器,也可是内存单元或立即数。【不允许两个操作数同时为内存单元】
sub ah,al
sub dx,ax
sub [label_a], ch
因为处理器没减法运算电路,所以,sub ah, al实际等效于
neg al
add ah,al
可以这么说,几乎所有的处理器指令既能操作无符号数,又能操作有符号数。但几条除外,比如除法,乘法。
已经学习过除法div。严格说,它应该叫做无符号除法指令,这条指令只能工作于无符号数,即只能从无符号数的角度解释它的执行结果才说得通。如
mov ax,0x0400
mov bl,0xf0
div bl	;执行后,AL中的内容为0x04,即十进制数4
因为从十进制角度按无符号解释,解释的通。按有符号解释,1024除以-16,理论上结果应该为-64也即0xc0。
为了解决这个问题,处理器专门提供了一个有符号数除法指令idiv。idiv指令格式和div相同,除了它是专门用于计算有符号数的。如你决定要进行有符号数的计算,需采用如下:
mov ax,0x0400
mov bl,0xf0
idiv bl	;执行后,AL中的内容为0xc0,即十进制数-64
用idiv做除法时,需小心。如用0xf0c0除以0x10。你的做法可能这样:
mov ax,0xf0c0
mov bl,0x10
idiv bl	
结果在寄存器AL中,因为除法结果是-244,这样的结果超出了寄存器AL的表示范围,必然溢出。因此可用32位除法来代替以前的除法:
xor dx, dx
mov ax,0xf0c0
mov bx,0x10
idiv bx
但是这样依然错误,因为dx:ax此时成了一个正数。正确应该这样:
mov ax, 0xf0c0
cwd
mov bx,0x10
idiv bx
以上指令全部执行后,寄存器AX中的内容为0xff0c。即十进制数-244。
对于无符号除法,必须用div。对于有符号数除法,必须用idiv。

6.8.数位的显示

mov al, [bx+si]
寄存器BX中的内容是基地址,si被称为索引寄存器,又叫变址寄存器。另一个常用的变址寄存器是DI。
注意,Intel8086处理器只允许以下几种基址寄存器和变址寄存器的组合:
[bx+si]
[bx+di]
[bp+si]
[bp+di]
这些组合可以用于任何带有内存操作数的指令中。其他任何组合,比如[bx+ax],[cx+dx],[ax+cx]等等,都是非法的。
源程序第43行,从指定的内存单元取出一个字节,传送到AL寄存器,偏移地址是BX+SI。但,它们之间的运算并非是在编译阶段进行的,而是在指令实际执行的时候,由处理器完成的。
源程序第44行,将AL中的数字加上0x30,以得到它的ASCII码。
源程序第45行,将数字0x04传送到寄存器AH。0x04是显示属性。到此,AX中是一个完整的字,前8位显示属性值,后8位是字符的ASCII码。
源程序第46行,将AX中的内容传送到由段寄存器ES所指向的显示缓冲区中,偏移地址由DI指定。前面使用movsw传送字符串"Label offset:"到显示缓冲区时,也使用了DI,当时DI是指向显示缓冲区首地址的【0】,且每传送一次就自动加2.传送结束后,DI正好指向字符":"的下一个存储单元。之后,DI一直没用过。
数据传送,寄存器的低字节传送到显示缓冲区的低地址部分,寄存器的高字节传送到显示缓冲区的高地址部分。【字节】
dec是减一指令,和inc指令一样,后面跟一个操作数。可以是8位或16位的通用寄存器或内存单元。
源程序第49行,指令jns show的意思是,如未设置符号位,则转移到标号"show"所在的位置处执行。Intel处理器的标志寄存器里有符号位SF,很多算术逻辑运算会影响到该位。比如这里的,dec。如计算结果的最高位比特是"0",处理器把SF置"0",否则,SF置"1"。
如果运算结果的最高位是"1",它唯一能做的,就是将SF标志置"1"。
jns是条件转移指令,处理器在执行它的时候要参考标志寄存器的SF位。除了只是在符合条件的时候才转移之外,它和jmp指令很相似,也是相对转移指令,编译后的机器指令操作数也是一个相对偏移量,是用标号处的汇编地址减去当前指令的汇编地址,再减去当前指令的长度得到的。

6.9.其他标志位和条件转移指令

在处理器内进行的很多算术逻辑运算,都会影响到标志寄存器的某些位。

6.9.1.奇偶标志位PF

当运算结果出来后,如最低8位中,有偶数个为1的比特,则PF=1;否则,PF=0。例如:
mov ax, 1000100100101110B
xor ax,3
顺序执行以上两条指令后,因为结果是0x892d,低8位0x2d,有偶数个1,所以PF=1。再如:
mov ah,0x26
mov al,0x81
add ah,al
因为最后ah的内容是0xa7,包含奇数个1,故PF=0。

6.9.2.进位标志CF

当处理器进行算术操作时,如最高位有向前进位或借位的情况发生,则CF=1;否则,CF=0。比如:
mov al,0x80
add al,al
这里,寄存器AL自己和自己做加法,并因为最高位是1而产生进位。结果是进位被丢弃,AL中最终结果为0。进位的产生,使得CF=1。同时,ZF=1,PF=1。
下面是因有借位而使得CF为1的例子:
mov ax,0
sub ax,1
CF标志始终忠实记录进位或借位是否发生,但少数指令除外如inc和dec。

6.9.3.溢出标志OF

对于无符号数运算来说,进位标志CF常意味着得到了错误的计算结果,因为目的操作数没能容纳那个进位。这里有一个例子:
mov ah,0xff
add ah,2
执行以上后,CF为1,进位丢失,得到的结果是1,这明显是错的。
但是,如上面进行的是有符号数运算,则实际在计算-1+2【十进制】,AH中最终结果为1,这是正确的。
显然,同样的运算,从无符号数和有符号数视角看,是不同的。但所有情况下,处理器都不能知道你的意图,不知道你进行的是有符号运算,还是无符号数运算。为此,它提供了溢出标志,该标志的意思是,假定你进行的是有符号数运算,如运算结果是正确的,则OF=0,否则OF=1。比如,上面例子,从有符号角度,是-1和2相加,结果为1,是正确的,未溢出。故,OF=0。简单说,OF标志用于指示两个有符号数的运算结果是否错误。
再看一个例子:
mov ah,0x70
add ah.ah
首先,本次相加,用二进制来说是0111 0000 + 0111 0000 = 1110 0000,最高位没有进位,故CF=0。
其次,从无符号数的角度看,即112+112=224,未超出一个字节能容纳的数据上限255,结果是正确的。
但,从有符号数运算的角度看,即112+112=-32。两正数相加,结果未负,明显是错的。这种情况下,OF=1。错误的原因是,两个正数112和112相加,理论上的计算结果224超出了寄存器AH能容纳的有符号数范围,所以破坏了符号位,使得结果变成了负数。
可使用16寄存器AX
mov ax,0x70
add ax,ax
这次,无论它是有符号还是无符号,结果都正确。故CF=0。OF=0。

任何时候,处理器都不可能知道你的意图,不知道你进行的是有符号运算,还是无符号数运算。因此,它所能做的,就是假定进行的是有符号数运算,并根据结果提供OF标志,至于如何处理,是你自己的事。比如,如果你进行的是无符号数运算,则不可不用理会该标志。

6.9.4.现有指令对标志位的影响

add		OF,SF,ZF,AF,CF,PF状态依据计算结果而定
cbw		不影响任何标志位
clb		DF=0,CF,OF,ZF,SF,AF和PF未定义。
cwd		不影响任何标志位
dec		CF标志不受影响,因为该指令常在程序中用于循环计数,而循环体常有依赖于CF标志的指令,故不希望它打扰CF标志;对OF,SF,ZF,AF,PF的影响依计算结果而定。
div/idiv	对CF,OF,SF,ZF,AF和PF的影响未定义
inc		CF标志不受影响,对OF,SF,ZF,AF,PF的影响依计算结果而定
mov/movs	不影响标志位
neg		如操作数未0,则CF为0。否则,CF=1;对OF,SF,ZF,AF和PF的影响依计算结果而定。
std		DF=1,不影响其他标志位
sub		对OF,SF,ZF,AF,PF和CF的影响依计算结果而定
xor		OF=0,CF=0;对SF,ZF,PF依计算结果而定;对AF影响未定义

6.9.5.条件转移指令

"jcc"不是一条指令,而是一个指令族,功能是根据某些条件进行转移。
汇编语言源代码中,条件转移指令的操作数是标号。编译成机器码后,操作数是一个立即数,是相对于目标指令的偏移量。在16位处理器上,偏移量可以是8位或16位。
相似地,jz的意思是ZF标志为1则转移;jnz意思是ZF标志不为1【为0】则转移。
jo的意思是OF标志为1则转移,jno意思是OF标志不为1【为0】则转移。
jc的意思是CF标志为1则转移,jnc意思是CF标志不为1则转移
jp意思是PF为1转移,jnp,PF不为1,则转移
转移指令必须出现在影响标志的指令之后,如
dec si
jns show
多数时候,你会遇到一些和标志位关系不太明显的问题。如,AX寄存器的内容为0x30的时候转移,或当AX寄存器内容小于0xf0时候转移,再或者,当AX寄存器里的内容大于寄存器BX寄存器里的内容时转移。
cmp,需要操作两个数,目的操作数可以是8位或16位通用寄存器,也可以是8位或16位内存单元;源操作数可以是与目的操作数宽度一致的通用寄存器,内存单元或立即数。但两个操作数同时为内存单元的情况除外。比如:
cmp al, 0x80
cmp dx,bx
cmp [label_a], cx
cmp指令在功能上和sub指令相同,唯一不同在于,cmp指令仅仅依据计算的结果设置相应的标志位,而不得保留计算结果,因此,也不会改变两个操作数的原有内容。cmp指令将会影响到CF,OF,SF,ZF,AF和PF标志位。
比较是拿目的操作数和源操作数比,重点关心目的操作数。
cmp ax, bx
// 各种比较结果和相应的条件转移指令
比较结果		指令			相关标志位的状态
等于		je				相减结果为0才成立,故要求ZF=1
不等于		jne				相减结果不为0才成立,故要求ZF-0
大于		jg				适用于有符合数比较
							要求:ZF=0,且SF=OF。要么相减后溢出,结果必须为负数。要么相减后未溢出,结果必须为正数。
大于等于		jge				适用于有符合数比较
							要求:SF=OF
不大于		jng				适用于有符号数比较。要求ZF=1或SF!=OF。如果相减后溢出,则结果必须为正数。如相减后未溢出,结果必须为负数。
不大于等于	jnge			适用于有符合数的比较
							要求:SF!=OF
小于		jl				适用于有符号数的比较,要求:SF!=OF
小于等于		jle				适用于有符号数的比较,要求ZF=1,且SF!=OF
不小于		jnl				适用于有符号数比较,要求SF=OF
不小于等于	jnle			适用于有符号数比较。要求ZF=0且SF=OF
高于		ja				适用于无符号数比较,要求CF=0且ZF-0
高于等于		jae				适用于无符号数比较,要求CF=0
不高于		jna				适用于无符号数比较,要求CF=1或者ZF=1
不高于等于	jnae			适用于无符号数比较,要求CF=1
低于		jb				适用于无符号数比较,要求CF=1
低于等于		jbe				适用于无符号数比较,要求CF=1或ZF=1
不低于等于	jnbe			适用于无符号数比较,要求CF=0且ZF=0
校验为偶		jpe				要求PF=1
校验为奇		jpo				要求PF=0
jcxz,意思是当CX寄存器的内容为0时转移。如
jcxz show

6.10.NASM编译器的 和 和 $标记

NASM编译器提供了一个标记"$",该标记等同于标号,你可以把它看成是一个隐藏在当前行行首的标号。因此,jmp near $意思是,转移到当前指令继续执行。它和:
infi:jmp near infi
一样。

为了得到不多不少,正好512字节的编译结果,同时最后两个字节还必须是0x55和0xAA,需要在所有指令的后面填充一些无用的数据。
源程序55行,用于重复伪指令"db 0"若干次。重复的次数由510-($-$$)得到的。除去0x55和0xAA后,剩余的主引导扇区的内容是510字节;$是当前行的汇编地址;$$是NASM编译器提供的另一个标记,代表当前汇编节【段】的起始汇编地址。当前程序没有定义节或段,就默认地自成一个汇编段,且起始的汇编地址是0【程序起始处】。这样用当前汇编地址减去程序开头汇编地址,就是程序实体大小。再用510-程序实体大小,就是需要填充字节数。
源程序也应按段来组织,划分成独立的代码段,数据段等。

6.12.本章程序的调试

6.12.1.调试命令"n"

Bochs重现时,无标号信息。用关联操作数作为标号。
s单步执行
n对于循环,将一次执行完循环

6.12.2.调试命令"u"

之所以能使用调试命令"n"来越过循环体,是因为Bochs知道控制循环体次数是的寄存器CX,它可以自动监视整个循环过程。
但是,"n"命令对于下面的循环结果无效:
show:
	mov al,[bx+si]
	add al,0x30
	mov ah,0x04
	mov [es:di], ax
	add di, 2
	dec si
	jns show
如何越过,上述。只需知道循环体后面那条指令的物理地址即可,可以使用反汇编命令"u'。
"u"命令可使用两个参数,第一个参数是跟在"/"后面的数字,指定反汇编出多少条指令;第二个参数用于指定一个内存地址,Bochs从这个开始反汇编操作。
使用"u'得到jns后指令的物理地址。为了越过,先使用"b"命令把0x7c61【下条指令物理地址】设为断点,然后执行"c"

6.12.3.用调试命令"info"察看标志位

使用Bochs的命令“info”,可显示多种类型的处理器信息,显示标志寄存器的状态只能其功能之一。
为了显示标志寄存器的状态,可使用"eflags",即"info eflags"。Intel8086的标志寄存器是16位的,称做FLAGS;在32位处理器上,该标志寄存器做了扩展,达到了32位,称做EFLAGS。因此,Bochs,应输入"info eflags"而不是"info flags"。
输入info eflags后,对Bochs输出的解释:
of是溢出标志
df是方向标志
if和tf是和中断有关标志
sf是符号标志
zf是零标志
af是辅助进位标志
pf是奇偶标志
cf是进位标志
如果显示的标志名称是小写的,说明该标志为0;否则,该标志为1。

猜你喜欢

转载自blog.csdn.net/x13262608581/article/details/124770696