很抱歉,由于我在写本篇博客的时候比较早期,对汇编指令和计算机原理的理解不到位,导致出现了很多错误。因此我决定重新做一遍这个检测点。这一次我会加入编程实践,使用bochs模拟器在我的内核里执行汇编指令并查看PSW程序状态字,来验证每一步的操作,这样会给大家形象的看出现象,并且还能确保真实性和正确性。这里要感谢评论区指正我之前的错误。
王爽老师的书里使用的是masm+dosbox进行调试汇编语言程序,而我现在的内核已经进入了x86_64的长模式,是nasm/gas/gcc+bochs进行调试,我会在必要的地方进行说明。
我认为这里还是很有难度的,如果觉得这里有点理解不了,可以去看看附录里的对补码的说明,看了之后会对数据的存储更加清晰。
先来复习一下PSW标志寄存器的标志们的作用:
1、ZF标志(Zero Flag零标志):判断执行相关指令后的结果,若结果是0,则把ZF标志位的值设置为1;若结果不是0,则设置为1.
以下是书上的例子代码,我会把它转换成AT&T风格运行。
mov ax,1
sub ax,1
clear_rflags()函数是用于清空flags寄存器的所有位(全部置0)
print_current_rflags()是查看当前flags寄存器情况,它会打印出flags的值,也会打印出哪些标志为1
asm 是一个c语言内嵌汇编语言的关键字,使用gcc默认的AT&T风格的汇编,语法稍微和Intel风格有所区别。
由于我的内核现在处于长模式,因此函数名里出现了rflags,表明是64位的PSW。以上函数是我自己编写的,大家知道是什么功能就好,我们主要看现象。
rflags的第二位是保留位,因此0x10中的1是为Intel保留的,不被使用的,可以忽略。
大家可以看到在运行例子代码之后,PF = 1,ZF = 1
这就说明:
结果是0,则ZF=1
结果中所有bit位为1的个数是0个(偶数),则PF=1
2、PF标志(Parity Flag奇偶标志):判断执行相关指令后的结果,若结果的所有bit位中1的个数是偶数个,则把PF标志位的值设置为1;反之则设置为0.
mov al,1
add al,10
运行结果为 0b00001011 奇数个1
PF=0成立
mov al,1
or al,2
运行结果为 0b00000011 偶数个1
PF=1成立
3、SF标志(Sign Flag符号标志):判断执行相关指令后的结果,若结果是负数,则把SF的值设置为1;如果非负数,则设置为0.
mov al,10000001B
add al,1
执行后的结果为 10000010B 为负数
则SF=1 如果你是当作有符号数来计算,则这个标志对你就有意义,如果你仅仅是当作无符号数来计算,那么可以忽略这个SF=1,不使用它。
并且结果中有偶数个1
PF=1
4、CF标志(Carry Flag进位标志):在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从最高位的借位值。
mov al,98H
add al,al
98H + 98H > FFH
超过了al寄存器能存储的范围,出现了进位,则CF=1
执行后的al=30=0b00110000 其中有偶数个1,则PF=1
大家可以看到其中OF=1
书中已经说明:计算机并不关心你做的加法到底是有符号的加法还是无符号的加法,作为无符号数它进位了,而作为有符号数它溢出了。重要的是程序员怎么使用使用这两个标志位,你认为你做的是有符号加法,那就只看OF,不看CF;你认为你做的是无符号加法,那就只看CF,不看OF.
5、OF标志(Overflow Flag溢出标志):在进行有符号数运算的时候,如果结果超过了机器所能表示的范围时,把OF的值设置为1;如果没有超过则设置为0.
mov al,99
add al,98
这里的情况和上面的例子情况是一样的。
先把答案放一放:
组数 | CF | OF | SF | ZF | PF | |
1 | sub al,al | 0 | 0 | 0 | 1 | 1 |
2 | mov al,10h | 0 | 0 | 0 | 1 | 1 |
3 | add al,90h | 0 | 0 | 1 | 0 | 1 |
4 | mov al,80h | 0 | 0 | 1 | 0 | 1 |
5 | add al,80h | 1 | 1 | 0 | 1 | 1 |
6 | mov al,0FCH | 1 | 1 | 0 | 1 | 1 |
7 | add al,05h | 1 | 0 | 0 | 0 | 0 |
8 | mov al,7Dh | 1 | 0 | 0 | 0 | 0 |
9 | add al,0BH | 0 | 1 | 1 | 0 | 1 |
首先,我们来回忆一下,作者在讲标志寄存器的时候,曾经说过一句话:
在8086的指令集中,一般来说运算指令会影响标志寄存器,比如sub,mul,div,inc,or,and
一般来说传送指令不会影响标志寄存器,比如mov,push,pop
因此遇到传送指令的时候,把上一行的结果抄到当前行就好。
1、sub al,al
结果为0,则ZF=1
有0个1,是偶数个,则PF=1
2、mov al,10H
mov是转移指令,不影响flags
3、add al,90H
10H + 90H = A0H = 0b10100000
有偶数个1,所以PF=1
最高位为1,所以SF=1
OF标志位在确定的时候,CPU会假定你相加的两个数都是有符号数。10H的二进制表示是00010000,可以看见,最高位为0,因此这是一个正数,计算机在存储的时候使用的是补码,正数的原码和补码表示是一样的,因此00010000这个补码的原码还是00010000,转换成十进制是16.而90H则稍微复杂一些,90H=0b10010000,最高位是1,因此这是一个负数,那么我们需要把补码转换成原码01110000,这个原码代表了这个数的数值大小,再加上符号就是-112.
于是,16 - 112 = -96
8位有符号数能表示的范围是-128 ~ 127.那么在范围内,没有溢出,OF=0
结果为负数,所以SF=1
CF标志位在确定的时候,CPU会假定你相加的两个数都是无符号数,16 + 144 = 160,在0 ~ 255的范围内,没有进位。或者也可以看二进制的结果A0H = 0b10100000,没有产生进位。
总结:一个数放在寄存器里面,计算机(CPU)不知道它到底是有符号数还是无符号数,计算机也不想知道。在执行运算的时候,计算机在判断标志位的时候才会关注它。当确定CF标志的时候,把两个运算数都看成是无符号数,在确定OF,SF标志位的时候都看成有符号数。
如果程序员认为这是有符号数的运算,那么在运算结束后去判断OF或SF标志位即可,CF标志位忽略掉就好、CF位是1还是0对咱们来说没有意义;同理,程序员如果认为这是无符号数的运算,那么运算结束后就不需要关注OF或SF位,认为它们没有意义就好。
在手动判断标志位的时候,ZF、PF是比较简单的,就看看结果就好,不用管什么有无符号。
4、mov al,80h
5、add al,80H
80H + 80H = 100H = 0b100000000
结果=0b00000000则ZF=1
最高位的1进位出去了,则CF=1
结果里面有0个1,则PF=1
判断OF的时候当成是有符号数,0b10000000 + 0b10000000
-128 - 128 = -256
不在-128 ~ 127的范围之内,所以OF=1
6、mov al,0FCH
7、add al,05H
FCH + 5H = 101H
结果为1H
发生了进位,CF=1
我们判断一下OF标志位:0b11111100 + 0b00000101
其中0b11111100是负数,0b00000101 是正数,我们转换一下。
-4 + 5 = 1
这个结果在-128 ~ 127 这个范围内,没有溢出。
8、mov al,7DH
9、add al,0BH
7DH + BH = 88H = 0b10001000
其中有2个1,那么PF=1
最高位是1,那么结果为负数,则SF=1
我们再来判断OF位
7DH = 0b01111101 是一个正数 125
BH = 0b00001011 也是一个正数 11
125 + 11 = 136 > 128,那么就是溢出了,OF=1