First post, by superfury
I know that the present bit(known by using with LOADALL) causes a #GP(0) when the segment is used, but what about the other bits? What happens with the undocumented DPL bits when using the segment(which is already loaded in the cache in all cases)? What about the accessed(dirty) bits? Does the CPU not write the dirty bit to memory after it's set due to a write using the segment descriptor(setting it in both the descriptor and memory GDT/LDT entry)? What about the conforming bits in the code segment descriptor cache? Is it used for anything after loading it with a jump(or any other instruction loading CS)?
Currently everything is checked at load-time only(mostly) in UniPCemu, according to the known documentation(although the order in which the checks are performed is unknown, except for the present/valid bit(which should be first checked always?)).
Currently, UniPCemu only checks/uses limits, direction, present etc. in the following order:
1. Present, else #GP(error code 0 on protected mode(no error code on V86/real mode).
2. Protected mode: NULL selector throws #GP(see step 1) for non CS/SS accesses.
3. Forbidden reads/writes according to access rights cause #GP(see step 1).
4. Limit is calculated and compared according to descriptor type(exclude/include for top-down data segments accordingly). Failure to comply causes #GP(see step 1) or #SS(see step 1).
Steps 2/3 are done in protected mode only(unchecked in real/virtual 8086 mode), seeing conflicts with CS segment with (un)real mode usage on access rights?
#NP is never thrown(only when loading segments using normal instructions).
byte CPU_MMU_checkrights(int segment, word segmentval, uint_32 offset, int forreading, SEGMENT_DESCRIPTOR *descriptor, byte addrtest, byte is_offset16)
{
INLINEREGISTER uint_32 limit; //The limit!
INLINEREGISTER byte isvalid;
//First: type checking!
if (unlikely(GENERALSEGMENTPTR_P(descriptor)==0)) //Not present(invalid in the cache)?
{
CPU_MMU_checkrights_cause = 2; //What cause?
return 1; //#GP fault: not present in descriptor cache mean invalid, thus #GP!
}
if (unlikely(getcpumode()==CPU_MODE_PROTECTED)) //Not real mode? Check rights!
{
if (unlikely((segment != CPU_SEGMENT_CS) && (segment != CPU_SEGMENT_SS) && (!getDescriptorIndex(segmentval)) && ((segmentval&4)==0))) //Accessing memory with DS,ES,FS or GS, when they contain a NULL selector?
{
CPU_MMU_checkrights_cause = 1; //What cause?
return 1; //Error!
}
switch ((descriptor->AccessRights&0xE)) //What type of descriptor?
{
case 0: //Data, read-only
case 4: //Data(expand down), read-only
case 10: //Code, execute/read
case 14: //Code, execute/read, conforming
if (unlikely((forreading&~0x10)==0)) //Writing?
{
CPU_MMU_checkrights_cause = 3; //What cause?
return 1; //Error!
}
break; //Allow!
case 2: //Data, read/write
case 6: //Data(expand down), read/write
break; //Allow!
case 8: //Code, execute-only
case 12: //Code, execute-only, conforming
if (unlikely((forreading&~0x10)!=3)) //Writing or reading normally?
{
CPU_MMU_checkrights_cause = 3; //What cause?
return 1; //Error!
}
break; //Allow!
}
}
//Next: limit checking!
if (unlikely(addrtest==0)) return 0; //No address test is to be performed!
//Execute address test?
{
uint_32 limits[2]; //What limit to apply?
limits[0] = limit = ((SEGDESCPTR_NONCALLGATE_LIMIT_HIGH(descriptor) << 16) | descriptor->limit_low); //Base limit!
limits[1] = ((limit << 12) | 0xFFF); //4KB for a limit of 4GB, fill lower 12 bits with 1!
limit = limits[SEGDESCPTR_GRANULARITY(descriptor)]; //Use the appropriate granularity!
/*
if (unlikely((GENERALSEGMENTPTR_S(descriptor) == 1) && (EXECSEGMENTPTR_ISEXEC(descriptor) == 0) && DATASEGMENTPTR_E(descriptor))) //Data segment that's expand-down?
{
if (unlikely(is_offset16)) //16-bits offset? Set the high bits for compatibility!
{
if (unlikely(((SEGDESCPTR_NONCALLGATE_G(descriptor)&CPU[activeCPU].G_Mask) && (EMULATED_CPU>=CPU_80386)))) //Large granularity?
{
offset |= 0xFFFF0000; //Convert to 32-bits for adding correctly in 32-bit cases!
}
}
}
*/ //16-bit access on a wrong granularity isn't special?
isvalid = (offset<=limit); //Valid address range!
isvalid ^= ((descriptor->AccessRights&0x1C)==0x14); //Apply expand-down data segment, if required, which reverses valid!
isvalid &= 1; //Only 1-bit testing!
if (likely(isvalid)) return 0; //OK? We're finished!
//Not valid?
{
CPU_MMU_checkrights_cause = 6; //What cause?
if (segment==CPU_SEGMENT_SS) //Stack fault?
{
return 3; //Error!
}
else //Normal #GP?
{
return 1; //Error!
}
}
}
//Third: privilege levels & Restrict access to data!
//Don't perform rights checks: This is done when loading the segment register only!
//Fifth: Accessing data in Code segments?
return 0; //OK!
}
//Used by the MMU! forreading: 0=Writes, 1=Read normal, 3=Read opcode fetch.
int CPU_MMU_checklimit(int segment, word segmentval, uint_32 offset, int forreading, byte is_offset16) //Determines the limit of the segment, forreading=2 when reading an opcode!
{
byte rights;
//Determine the Limit!
if (likely(EMULATED_CPU >= CPU_80286)) //Handle like a 80286+?
{
if (unlikely(segment==-1))
{
CPU_MMU_checkrights_cause = 0x80; //What cause?
return 0; //Enable: we're an emulator call!
}
//Use segment descriptors, even when in real mode on 286+ processors!
rights = CPU_MMU_checkrights(segment,segmentval, offset, forreading, &CPU[activeCPU].SEG_DESCRIPTOR[segment],1,is_offset16); //What rights resulting? Test the address itself too!
if (unlikely(rights)) //Error?
{
switch (rights)
{
default: //Unknown status? Count #GP by default!
case 1: //#GP(0) or pseudo protection fault(Real/V86 mode(V86 mode only during limit range exceptions, otherwise error code 0))?
if (unlikely((forreading&0x10)==0)) CPU_GP(((getcpumode()==CPU_MODE_PROTECTED) || (!(((CPU_MMU_checkrights_cause==6) && (getcpumode()==CPU_MODE_8086)) || (getcpumode()==CPU_MODE_REAL))))?0:-2); //Throw (pseudo) fault when not prefetching!
return 1; //Error out!
break;
case 2: //#NP?
if (unlikely((forreading&0x10)==0)) THROWDESCNP(segmentval,0,(segmentval&4)?EXCEPTION_TABLE_LDT:EXCEPTION_TABLE_GDT); //Throw error: accessing non-present segment descriptor when not prefetching!
return 1; //Error out!
break;
case 3: //#SS(0) or pseudo protection fault(Real/V86 mode)?
if (unlikely((forreading&0x10)==0)) CPU_StackFault(((getcpumode()==CPU_MODE_PROTECTED) || (!(((CPU_MMU_checkrights_cause==6) && (getcpumode()==CPU_MODE_8086)) || (getcpumode()==CPU_MODE_REAL))))?0:-2); //Throw (pseudo) fault when not prefetching!
return 1; //Error out!
break;
}
}
return 0; //OK!
}
return 0; //Don't give errors: handle like a 80(1)86!
}
Is that behaviour correct and complete(remainder is done and checked against(and faulting( only when loading using MOV/execution transfers)?
For the loading part, see segmentWritten(...) function in UniPCemu/cpu/protection.c in the UniPCemu repository. Is that checking order correct?
Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io