VOGONS


First post, by superfury

User metadata
Rank l33t++
Rank
l33t++

I'm thinking about finishing the basics of my 80286+ protected mode. One problem I'm struggling about is the task switching and it's exception handling mechanism. Currently all exceptions are handled during the current task. As long as no task switch occurs nothing's wrong there. But how should I handle task switches throwing exceptions throwing exceptions (throwing exceptions ...)? What should be the best way to handle this?

x86 protection handling: https://bitbucket.org/superfury/unipcemu/src/ … le-view-default
Multitasking handling (submodule of protected mode system): https://bitbucket.org/superfury/unipcemu/src/ … ing.c?at=master

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

Reply 1 of 32, by crazyc

User metadata
Rank Member
Rank
Member

The manual is pretty clear about the order. Exceptions caused by an invalid new tss should happen in the current task. Otherwise exceptions caused by faults in selectors loaded from the new tss happen in the new task. Exceptions throwing exceptions would result in a double fault which likely result in a switch to an entirely different context and yet another exception would cause shutdown. The code at https://github.com/mamedev/mame/blob/master/s … 6/i286.cpp#L528 is tested and is known working.

Reply 2 of 32, by Jorpho

User metadata
Rank l33t++
Rank
l33t++
crazyc wrote:

Exceptions throwing exceptions would result in a double fault which likely result in a switch to an entirely different context and yet another exception would cause shutdown.

Triple-faulting the CPU is used in some obscure operating system contexts, isn't it?
http://www.os2museum.com/wp/why-os2-is-hard-to-virtualize/

Might be important for all those people dead set on running OS/2 on their PSPs. 😵

Reply 3 of 32, by crazyc

User metadata
Rank Member
Rank
Member

Yep, on AT hardware, the shutdown signal triggers a processor reset. Actually not so obscure though as Windows standard mode does too (and it happens a lot). 80286 only though as (except for the odd exception of IBM OS/2 1.0) on the 386, protected mode is exited on both Windows and OS/2 1.x the proper way by clearing the PM bit in CR0.

Reply 4 of 32, by superfury

User metadata
Rank l33t++
Rank
l33t++

Just implemented basic double/triple fault handling:
https://bitbucket.org/superfury/unipcemu/src/ … ion.c?at=master
https://bitbucket.org/superfury/unipcemu/src/ … ing.c?at=master

Still need to implement the actual task switching though(I'll do this later). First going to implement the Sound Blaster DSP.

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

Reply 5 of 32, by peterferrie

User metadata
Rank Oldbie
Rank
Oldbie
Jorpho wrote:

Triple-faulting the CPU is used in some obscure operating system contexts, isn't it?

Yes, it was a fast way to switch from protected to real mode on the 286, given that there wasn't a clean documented way to do it.
Once in protected mode there, you were expected to stay in protected mode.

Reply 6 of 32, by Scali

User metadata
Rank l33t
Rank
l33t
peterferrie wrote:

Yes, it was a fast way to switch from protected to real mode on the 286, given that there wasn't a clean documented way to do it.
Once in protected mode there, you were expected to stay in protected mode.

It wasn't so much a fast way (I don't think it was particularly fast), as it was the only reasonably reliable way to get out of protected mode while the system remained up and running.
I believe this actually needs some support in the chipset/BIOS as well, so that it correctly continues executing in real mode where you want it.
It could be that some early 286 systems aren't compatible with this technique.

More info here: https://blogs.msdn.microsoft.com/larryosterma … all-trap-redux/
And here, also a link to a routine that generates the triple-fault on 286: https://web.archive.org/web/20000305035245/ht … triplefault.htm
https://web.archive.org/web/20000411021043/ht … fault/reset.asm

http://scalibq.wordpress.com/just-keeping-it- … ro-programming/

Reply 7 of 32, by superfury

User metadata
Rank l33t++
Rank
l33t++

I'm wondering about one thing though: the technique uses LIDT -1, which essentially loads the descriptor with a base of 0xFFFFFF(16-bit) or 0xFFFFFFFF(32-bit) and a limit of 0xFFFF. Thus all interrupt fetches are fetched from address FFFFFF to FFFFFF+FFFE. But this won't result in exceptions, because memory will wrap around 32-bits(or 24-bits on a 286)? Why will the interrupt exception fire? The interrupts are always fetched past available memory, thus wrapping to the start of memory? How does this guarantee an exception during interrupts, instead of wrapped memory being retrieved and used as an entry?

Or should it actually be LIDT 0(up to 7)? Since a limit of 0-6 should always give an exception, because an entry of 8 bytes is always larger than the limit, no matter what, thus always triple faulting on any interrupt?

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

Reply 8 of 32, by Scali

User metadata
Rank l33t
Rank
l33t

Well, the example code I posted, uses LIDT 0 (valid address, but contents at that address are value 0), it seems. It then executes a mov eax, cr0, which is an invalid instruction on 286 (doesn't have eax). Which will fault, then double-fault, then triple-fault, and reset the CPU.

The 'Microsoft way' seems to fail because it does LIDT with an address of -1. This is an invalid address. Apparently the LIDT itself doesn't check for that, so it continues running. They then use an int 1 to access the interrupt table at address -1. This causes a memory protection fault, which by itself will cause another fault (IDT not present), and a third fault (can't access any error handlers either). I guess...

http://scalibq.wordpress.com/just-keeping-it- … ro-programming/

Reply 9 of 32, by superfury

User metadata
Rank l33t++
Rank
l33t++

Isn't address -1 impossible to assemble? It's a 16-bit unsigned limit and a 24/32-bit unsigned offset according to what I can find. So LIDT -1 wouldn't even compile in x86? Or it would result in a LIDT FFFFFFFFFFh instruction, which is at the end of memory, wrapping around 32-bits when looking up an entry?

Edit: after a simple search:
http://stackoverflow.com/questions/5427934/wh … ean-in-assembly

Addresses are unsigned, so loading a negative IDT that fails is impossible. It would just fetch from/over the end of memory and happily use whatever it finds at the start of RAM as an entry(assuming memory wraps), unless it doesn't wrap, which returns an IDT entry containing all ones(0xFFFFFFFFFFFFFFFF)?

Last edited by superfury on 2016-08-09, 21:54. Edited 1 time in total.

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

Reply 10 of 32, by Scali

User metadata
Rank l33t
Rank
l33t
superfury wrote:

Or it would result in a LIDT FFFFFFFFFFh instruction

That's how I understand it. You're in pmode, so accessing memory that doesn't exist will cause exceptions. So you get the exception when you try to access the IDT (in this case by triggering an int 1).

http://scalibq.wordpress.com/just-keeping-it- … ro-programming/

Reply 11 of 32, by superfury

User metadata
Rank l33t++
Rank
l33t++

So the LIDT -1 would result in a valid IDT at the wrong place in memory? Thus the CPU will happily executes whatever IDT entry it finds there and only if it contains data causing an exception, it will result in an exception. Else the triple fault won't happen.

Is that correct?

Edit: looking at http://wiki.osdev.org/Interrupt_Descriptor_Table , it says some fields have to be zeroed. Does this mean that the processor requires the bits to be zeroed and raise an exception if it isn't? So that means that every possible interrupt vector to be called(the interrupt to trigger, as well as the double fault entry) needs to have those fields filled with non-zero values in order to make the triple fault work?

Does memory wrap around 32-bits on x86? Or can the processor map beyond 32-bits addresses(impossible because of 32-bit/24-bit address lines?), causing ones to be read from nonexistant memory, which causes the triple fault? What about when 4 GB of RAM is installed? Will the technique then still work reliably without IDT entry modification?

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

Reply 12 of 32, by Scali

User metadata
Rank l33t
Rank
l33t
superfury wrote:

So the LIDT -1 would result in a valid IDT at the wrong place in memory? Thus the CPU will happily executes whatever IDT entry it finds there and only if it contains data causing an exception, it will result in an exception. Else the triple fault won't happen.

No, you set a pointer to an invalid address. So as soon as it tries to access the IDT, a memory fault occurs. It can never reach the IDT.
Also, note that this ONLY works for 286 pmode. 386 uses another technique.

http://scalibq.wordpress.com/just-keeping-it- … ro-programming/

Reply 13 of 32, by superfury

User metadata
Rank l33t++
Rank
l33t++

So how does it know it's an invalid address? Does it simply check for overflow by adding the base and limit together and checking for carry? How does it know the IDTR is invalid?

Btw, the manual ( https://www.google.nl/url?sa=t&source=web&rct … QoK_yQ1kiPPbYbQ ) doesn't say anything about signed data in the IDTR as far as I can see, so it just checks for overflow(undocumented)?

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

Reply 14 of 32, by crazyc

User metadata
Rank Member
Rank
Member

So the LIDT -1 would result in a valid IDT at the wrong place in memory? Thus the CPU will happily executes whatever IDT entry it finds there and only if it contains data causing an exception, it will result in an exception. Else the triple fault won't happen.

Nope, LIDT is a pointer to the new IDTR value so if you try to load an invalid address you'll get a fault immediately which isn't what you want. They key to this working is to set the limit of the IDT to 0 so any interrupt after that will cause a triple fault. mov cr0, eax where bit 0 of eax is clear has the advantage of leaving pmode on a 386 so the code will work on 286's and 386+'s.

Reply 15 of 32, by superfury

User metadata
Rank l33t++
Rank
l33t++

OK. So the code using LIDT -1 is completely wrong in that case.

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

Reply 16 of 32, by Scali

User metadata
Rank l33t
Rank
l33t
crazyc wrote:

So the LIDT -1 would result in a valid IDT at the wrong place in memory? Thus the CPU will happily executes whatever IDT entry it finds there and only if it contains data causing an exception, it will result in an exception. Else the triple fault won't happen.

Nope, LIDT is a pointer to the new IDTR value so if you try to load an invalid address you'll get a fault immediately which isn't what you want. They key to this working is to set the limit of the IDT to 0 so any interrupt after that will cause a triple fault. mov cr0, eax where bit 0 of eax is clear has the advantage of leaving pmode on a 386 so the code will work on 286's and 386+'s.

I think both will work.
Because if you use LIDT -1, it seems the LIDT itself will just load a pointer, and not perform an access check. So the int 1 they fire off after that, will access the IDT, which will cause a fault because it cannot be accessed, causing another fault that the IDT is 'invalid' (because non-existent), and yet another fault because it cannot access a fault handler either.

I think the key to the -1 pointer working is that they have marked that selector as inaccessible in the GDT (remember, you're in pmode).

http://scalibq.wordpress.com/just-keeping-it- … ro-programming/

Reply 17 of 32, by superfury

User metadata
Rank l33t++
Rank
l33t++

Well, logically thinking while keeping in mind that numbers are UNSIGNED:

LIDT -1 will load the IDTR with base 0xFFFFFF and limit 0xFFFF on the 80286.
INT 1 will then check against the limit(0xFFFF) and will be seen as valid (because 7<0xFFFF).
Finally it will retrieve the IDT entry from address 0x0000007([0xFFFFFFFF+(0x01<<3)] to 0x000000E(full 64-bit entry)).
It will then try to jump to the 64-bit entry read from the table, whose exception handling is completely dependant on the contents at memory address 0x8-0xF.

Is that correct?

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

Reply 18 of 32, by Scali

User metadata
Rank l33t
Rank
l33t
superfury wrote:

LIDT -1 will load the IDTR with base 0xFFFFFF and limit 0xFFFF on the 80286.

No, I think you are missing a level of indirection. This is indirect addressing, the -1 is a pointer, not the actual value in memory.
Basically it is LIDT fword ptr [-1] if that makes sense.

http://scalibq.wordpress.com/just-keeping-it- … ro-programming/

Reply 19 of 32, by crazyc

User metadata
Rank Member
Rank
Member
Scali wrote:

I think both will work.
Because if you use LIDT -1, it seems the LIDT itself will just load a pointer, and not perform an access check. So the int 1 they fire off after that, will access the IDT, which will cause a fault because it cannot be accessed, causing another fault that the IDT is 'invalid' (because non-existent), and yet another fault because it cannot access a fault handler either.

I think the key to the -1 pointer working is that they have marked that selector as inaccessible in the GDT (remember, you're in pmode).

Sure, there won't be an access check on the base address value loaded into the idtr because lidt loads a linear address not a selector. It's the address FFFF:FFFF that's the problem and the access check on that will happen when the lidt instruction is executed. Even if the ldt descriptor at FFF8 is a valid user mode data or code segment descriptor and even if the segment limit is FFFF the offset FFFF will cause a GPF due to the pmode segment limit overrun.