EFLAGS description and implementation.
Introduction
EFLAGS是一个32位寄存器,由:一组状态标志位,一个控制标志位,一组系统标志位和系统保留位组成。
状态标志位
状态标志位简称为:OSZAPC,它们分别是:
- Carry Flag (bit0):如果算术运算在结果的MSb(most significant bit最高有效)之外产生进位或借位,该标志值1,反之置0。该标志用于表示无符号整数运算的溢出条件;
- Parity Flag (bit2):如果运算结果的LSB中含有偶数个1的时候,该标志置1,反之置0;
- (least significant bit最低有效位,
最低有效位(the least significant bit,lsb)是指一个二进制数字中的第0位(即最低位),具有权值为2^0,可以用它来检测数的奇偶性。与之相反的称之为最高有效位。在大端序中,lsb指最右边的位。
最低有效位代表二进制数中的最小的单位,可以用来指示数字很小的变化
)
- Adjust Flag (bit4):如果运算结果的bit3上产生进位或借位,该标志置1,反之置0。该标志只用于BCD运算中;
- Zero Flag (bit6):如果运算结果为0,该标志置1,反之置0;
- Sign Flag (bit7):永远和运算结果的MSb相同。即1表示负数,0表示正数。所以,其实在CPU内部看来,数字本身是不分正负的,所谓的有符号数和无符号数类型,只不过是是否把SF表示和运算结果一起考虑的问题了。例如:0xFFFFFFFF,如果把这个数和SF一起考虑,这个数就是-1,如果不和SF一起考虑,这个数就是32位能表示的最大值;
- Overflow Flag (bit11):如果运算结果超过了目的操作数可以表示的最大正数或最小负数,则该标志置1,反之置0。该标志只用来表示有符号数的溢出状况。
上面这组状态标志位可以使得CPU的一个算术运算为三种不同的数据类型产生表达不同的结果:
- 如果把算术运算对待成是针对无符号数的:那么CF标志表示溢出;
- 如果把算术运算对待成是针对有符号数的:那么OF标志表示溢出;
- 如果把算术运算对待成是针对BCD数的:那么AF标志表示溢出;
判断OF的方法 由于OF是用来判断有符号数溢出的,因此我们只考虑有符号数的情况。以一字节数据为例,数值范围是-128 ~ 127:
对于加法操作
- 如果操作数的MSb不同,即表示一正一负进行运算,则“最差”结果是-128 + 127 = -1,即FF。此时是不会造成溢出的,因此OF在MSb不同是为0的时候,无论如何不会被置位;
- 如果操作数的MSb相同,如果和与两个加数的MSb不同,则代表发生了溢出的情况;
对于减法操作
- 如果操作数的MSb不同,即表示一正一负进行运算,-128 - 1和127 - (-1)都会带来溢出结果,而判断的方法是差和被减数的MSb不同,在差在被减数的MSb上进行了借位;
- 如果操作数的MSb相同,减法操作是不会带来算术溢出的,最差的结果无非是128 - 128或者-127 - (-127);
在实现上,通过两个宏来判断OF的置位条件:
控制标志位
Direction Flag (bit10):作为唯一的一个控制标志,DF用来控制字符串指令的方向。当置1时,字符串指令从高地址向低地址执行;反之则从低地址向高地址执行;
系统标志位
- Trap Flag (bit8):在调试模式中,启用Single Step则置1,反之置0;
- Interrupt Enable Flag (bit9):置1时,CPU响应可屏蔽中断,反之禁止可屏蔽中断;
- IOPL (bit12-13):指定当前任务可以访问I/O地址的最低权限。CPL必须小于等于IOPL才可以访问I/O地址。该指令只能在CPL等于0的时候,通过POPF或者IRET指令修改;
- Nested Task Flag (bit14):暂时不清楚;
- Resume Flag (bit16):暂时不清楚;
- Virtual-8086 Mode Flag (bit17):置1启用Virtual8086模式,置0返回保护模式;
- Alignment Check Flag (bit18):当该标志和CR0.AM同时为1时,启用内存访问的对齐检查,二者有一个置0,则不进行检查。且该检查只有在CPL=3,且CPU处于保护模式或V8086模式时,才有用;
- Virutal Interrupt Flag (bit19):暂时不清楚;
- Virtual Interrupt Pending Flag (bit20):暂时不清楚;
- Identification Flag (bit21):置1,CPU支持cpuid指令,反之不支持;
Implementation
由于有些标志位的修改会影响到系统的运行模式,因此实现上,分成不同组,只是单纯读写EFLAGS寄存器,不会影响系统运行状态的分成一组,影响系统运行状态的分成另外一组。
不影响系统状态或CPU工作模式的标志位
为了方便对EFLAGS中的每一个标志位进行读写,AVM定义了一组宏。每一个标志位都有5个基本操作,用
#define DECLARE_EFLAGS_ACCESSOR(name, bitnum) \ bit32u get_##name(); \ avm_bool getB_##name(); \ void assert_##name(); \ void clear_##name(); \ void set_##name(avm_bool val);
这个宏在v_cpu类内部使用,每个方法的说明:
方法 | 含义 |
get_##name | 屏蔽掉除了name标志之外的其他标志后,EFLAGS的值。 |
getB_##name | 单纯的判定name标志是否置位。 |
assert_##name | 保证name标志一定置位。 |
clear_##name | 清除name标志。 |
set_##name | 将name标志设定成val。 |
在实现上,读取标志用宏IMPLEMENT_EFLAGS_ACCESSOR,写入标志用宏IMPLEMENT_SET_EFLAGS_ACCESSOR
#define IMPLEMENT_EFLAG_ACCESSOR(name, bitnum) \ bit32u v_cpu::get_##name() { \ return eflags & (1 << bitnum); \ } \ avm_bool getB_##name() { \ return (eflags >> bitnum) & 1; \ } #define IMPLEMENT_SET_EFLAGS_ACCESSOR(name, bitnum) \ void v_cpu::assert_##name() { \ eflags |= (1 << bitnum); \ } \ void v_cpu::clear_##name() { \ eflag &= ~(1 << bitnum); \ } \ void v_cpu::set_##name(avm_bool val) { \ eflags = (eflags & ~(1 << bitnum)) | ((val) << bitnum); \ }
影响系统状态或CPU工作模式的标志位
- IOPL
#define DECLARE_EFLAG_ACCESSOR_IOPL(bitnum) \ void set_IOPL(bit32u val); \ bit32u get_IOPL(); #define IMPLEMENT_SET_EFLAGS_ACCESSOR_IOPL(bitnum) \ void v_cpu::set_IOPL(bit32u val) { \ eflags = (eflags & ~(0x3 << bitnum)) | ((0x03 & val) << bitnum); \ } \ bit32u v_cpu::get_IOPL() { \ return 3 & (eflags >> bitnum); \ }
- TF, IF & RF Unimplement yet
- AC Unimplement yet
- VM Unimplement yet
算数标志位的缓释评估
在v_cpu.h中,用一组特殊的宏ArithmeticalFlag来定义算术标志位的操作。和DECLARE_EFLAGS_ACCESSOR不同的是,多了两个方法:get_##flag##Lazy和force_##flag。
缓释评估的基本原理:
用一个结构来记录算数运算的操作数、结果和具体的算数指令:avm_lf_flags_entry(定义在lazy_flags.h中),定义如下:
struct avm_lf_flags_entry { avm_address op1; avm_address op2; avm_address result; unsigned instr; };
在v_cpu内部,用一个该类型的对象oszapc,来记录虚拟cpu执行的最后一条算数指令的信息。
在v_cpu内部,单独用一个整型变量lf_flags_status来记录算数指令带来的EFLAGS寄存器算数标志位的变化,可以把lf_flags_status看成一个位图,特定的二进制bit对应特定的算数标志位,当特定的算数标志位需要根据实际的算数指令更新的时候,特定标志位对应的bit会被置1,否则被置0。因此每一条算数指令最后,都会使用一个相应的宏,来更新lf_flags_status的值。那么当使用get_##flags或者get_B##flags获取某一个标志位的时候,先判断lf_flags_status中的特定标志是否被置1,如果置1,则调用缓释评估函数get_##flag##Lazy,根据实际的算数指令结果获取相应的标志位,否则,直接读取EFLAGS中的值。
所有的get_##flag##Lazy的基本结构是:
avm_bool v_cpu::get_##flag##Lazy() { unsigned flag; switch(_instruction type_) { case instr1: flag = ; break; case instr2: flag = ; break; ... case instrN: flag = ; break; default: EXCEPTION(); } return flag; }
所有的算数操作类型在lazy_flags.h中通过宏定义,之后用统一的接口进行定义。
avm_lf_flags_entry的统一接口:所有的算数指令使用一组统一的接口来操作CPU内部的oszapc对象,它们是:
只有当ADC,SBB等需要使用算数标志位的指令被执行到时,才会调用相应的get_##flag##Lazy来根据最近一次的算数运算来获取相应的标志位的值。
两个操作数的情况: 通用宏SET_FLAGS_OSZAPC_SIZE(size, lf_op1, lf_op2, lf_result, ins) size分成8, 16, 32
op1操作数和一个结果: SET_FLAGS_OSZAPC_S1_SIZE(size, lf_op1, lf_result, ins) size分成8, 16, 32
op2操作数和一个结果: SET_FLAGS_OSZAPC_S2_SIZE(size, lf_op1, lf_result, ins) size分成8, 16, 32