VOGONS


First post, by superfury

User metadata
Rank l33t++
Rank
l33t++

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!
{
Show last 73 lines
				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

Reply 1 of 4, by BloodyCactus

User metadata
Rank Oldbie
Rank
Oldbie

present bit is used to denote if in memory or on disk in a paging file... conforming is for transfer in code segments, you cant transfer from a non-conforming to a conforming segment of the SAME privelege level, and you cant transfer to a high priv level (conforming or not).

you'd need to check conforming on every execution transfer

if segment is not present, you get a segment not present exception. used by the mmu to know when to load pages from disk back into memory.

--/\-[ Stu : Bloody Cactus :: [ https://bloodycactus.com :: http://kråketær.com ]-/\--

Reply 2 of 4, by superfury

User metadata
Rank l33t++
Rank
l33t++

What I mean is, are they(those access byte's bits in the caches) used at all when already loaded into the caches, while accessing memory(e.g. instruction fetches(3 in UniPCemu), reading an offset(e.g. MOV CS:[0000], which performs a normal(1 in UniPCemu) read at CS cached descriptor offset 0&1)? Or are those values that are [b[CACHED[/b] largely unused after loading using mov segreg or control transfers?

I think they might be unused, otherwise they would cause faults on every instruction fetch when CS is reset(CPU reset) or reloaded, which causes it to be loaded as an executable segment(unwritable, while it is supposed to be writable for compatibility, e.g. SMC)?

You say conforming is used when loading? So it does nothing at all when in the cache(e.g. memory accesses using the segment(+descriptor))? What about the accessed bit? Is it only written once to RAM when accessed(and set in the cache afterwards(a sort of one time writethrough), which causes the CPU to not perform a writeback to the accessed bit in RAM every single write(accessed bit set in cache doesn't write through to RAM when the segment is written with e.g. MOV byte [DS:EAX],00h). Otherwise, it would give a huge performance loss?
Does the CPU perform a read-modify-write when updating the accessed bit in the RAM GDT/LDT?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 3 of 4, by BloodyCactus

User metadata
Rank Oldbie
Rank
Oldbie
superfury wrote:

What I mean is, are they(those access byte's bits in the caches) used at all when already loaded into the caches, while accessing memory(e.g. instruction fetches(3 in UniPCemu), reading an offset(e.g. MOV CS:[0000], which performs a normal(1 in UniPCemu) read at CS cached descriptor offset 0&1)? Or are those values that are [b[CACHED[/b] largely unused after loading using mov segreg or control transfers?

you can have a thousand segments. how many do you think it caches???

load GDT, do a far jump, clear gdt memory, access something... see what happens....

I would think the access bits would be used all the time, your always fetching opcodes, reading or writing to memory etc as much as your doing register to register operands...

I dont know the exact inner workings, only what I read in the intel manuals.

superfury wrote:

I think they might be unused, otherwise they would cause faults on every instruction fetch when CS is reset(CPU reset) or reloaded, which causes it to be loaded as an executable segment(unwritable, while it is supposed to be writable for compatibility, e.g. SMC)?

nah remeber, when you switch from rmode to pmode, once in pmode, rmode selectors are still in effect until you do a far jump. so when the system is reset, its in rmode not pmode.. and in rmode GPE dont fire etc.

superfury wrote:

You say conforming is used when loading? So it does nothing at all when in the cache(e.g. memory accesses using the segment(+descriptor))? What about the accessed bit? Is it only written once to RAM when accessed(and set in the cache afterwards(a sort of one time writethrough), which causes the CPU to not perform a writeback to the accessed bit in RAM every single write(accessed bit set in cache doesn't write through to RAM when the segment is written with e.g. MOV byte [DS:EAX],00h). Otherwise, it would give a huge performance loss?
Does the CPU perform a read-modify-write when updating the accessed bit in the RAM GDT/LDT?

conforming is used whenever you transfer CS/TSS etc. whenever you try and execute code in another segment, you cant go from a conforming to non-conforming CS of the same level.

--/\-[ Stu : Bloody Cactus :: [ https://bloodycactus.com :: http://kråketær.com ]-/\--

Reply 4 of 4, by superfury

User metadata
Rank l33t++
Rank
l33t++

When I was talking about CACHED, I meant the effects of the descriptor cache, not the TLB or L1/2/3 caches.

So the conforming bit within the CS cache has no effect on normal execution(it isn't used while accessing memory, only said bit in the GDT itself is used(in the case of the descriptors in memory, before loaded into CS cache, which depends only on the RPL,DPL and CPL. The descriptor cache doesn't affect this afaik)?

It only caches as many as there are segments? So one for CS,DS,ES,FS,GS,SS,LDTR and TR each?

The problem with CS access rights is the executable bit. If it faults(thus is checked when in real mode) because CS has to have it set in the CS descriptor cache, it would immediately #GP fault when executing the first CS load, which essentially loads it as a data segment descriptor(according to the internally loaded access rights byte), which is illegal when loaded in PM(through the GDT/LDT, not using LOADALL)?
Thus it's only affecting the parsing of the lower AR bits(bits 2-0), if at all when combining them. The only bit that's different and should logically have effect in the cache(dependant on code/data toggle) is the topdown segment bit(for data segments) and the readable/writable bit(depending on bit 3)? Afaik data segments which are top-down segments using CS are working according to my latest finds(various sources on Google searches). Thus that must be true.

Thus the only bit without effect(completely no effect) is the conforming bit within the segment descriptor cache?

The system bit(bit 4) should only fault when unset on user segments(all segments but LDTR and TR) and set on TR/LDTR? Since those can't be safely used in such a way? Should be testable with LOADALL?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io