Skip to content

[kernel] Move INT 15/1F block move to FARPROC, enable A20 gate afterwards#2320

Merged
ghaerr merged 1 commit intomasterfrom
int15far
Apr 24, 2025
Merged

[kernel] Move INT 15/1F block move to FARPROC, enable A20 gate afterwards#2320
ghaerr merged 1 commit intomasterfrom
int15far

Conversation

@ghaerr
Copy link
Copy Markdown
Owner

@ghaerr ghaerr commented Apr 24, 2025

This PR adds the capability of running XMS using INT 15 (IBM PC) or INT 1F (PC-98) block moves while also having the kernel resident in HMA.

Discussed in #2318, #2293 (comment) and Mellvik/TLVC#163.

Almost all BIOSes (except QEMU and DosBox-X) will reset (disable) the A20 address line gate after executing the protected mode BIOS INT 15/1F block move XMS function. This change moves the block_move and enable_a20_gate functions to the .fartext section so the BIOS can return to a non-affected A20 address (i.e. low memory, not HMA) and re-enable the A20 gate before possibly continuing execution in HMA.

Note: This is tested for both IBM PC and PC-98 however both my only emulators don't actually disable A20 on return from INT 15/!F; so this definitely needs testing on real hardware or a more accurate emulator. In addition, the XMS auto-disable feature is now turned off when hma=kernel since INT 15/1F and HMA kernel can now co-exist. If the system doesn't boot, remove the hma=kernel line from /bootopts.

@tyama501 and @drachen6jp, please test this on PC-98 real hardware or Neko 21/W if possible, thank you!

@Mellvik, you should be able to use this code in TLVC to solve the same HMA/A20 issue we've been discussing.

After successful testing, more cleanup will follow: the AUTODISABLE code and comment will be removed, and the lengthy verify_a20 routine called after every enable_a20_gate can be removed when called just after block_move. This is potentially problematic for IBM PC since there are currently two methods for enabling A20, and the first working one needs to be recorded so that both don't have to when A20 is re-enabled.

It turned out to be a bit tricky to get the a20*.inc files working when called from both setup.S, where the setup code needs to be fully relocatable, but also from kernel code. Adding a .global symbol causes the assembler to generate a relocatable reference (which affected setup.S), and the C compiler can't call a near function within .fartext using the auto-generated thunking code. The latter necessitated the need to remove verify_a20() as a C callable function, since that function needed to stay as a near proc to be called from within enable_a20_gate. This works because the assembler can generate the relocatable form of the call instruction rather than have to use LCALL which requires an immediate form that won't also work in setup.S without special macros.... The good news is the final result is pretty clean, with the exception of calling the slow verify_a20 function after every block_move (for now).

@ghaerr ghaerr merged commit fe54cc0 into master Apr 24, 2025
2 checks passed
@ghaerr ghaerr deleted the int15far branch April 24, 2025 06:08
@drachen6jp
Copy link
Copy Markdown
Contributor

from https://github.com/ghaerr/elks/actions/runs/14634256044
I got floppy image.and checked with several machines.
give up. I am not good at writing English.

translated by tool.
It worked fine on NekoProject2 (16bit emulator).

But unfortunately, simultaneous use of Int1F/15 and HMA still seems to be a no-go.
There is a 286 machine emulated by PCem (using real BIOS), CPU check got 6 correctly and used Int15/1F, but it could not be used with HMA at the same time. I cannot get to the login prompt.
The same was true for PC-9801DX2 (80286).
If I use only HMA or only XMS as before, I can log in and edit files with vi.

Unreal mode was OK. I was able to boot from floppies on my 386 486 Pentium PC-98 and PC-98 compatibles, and they all selected 7 for the CPU check, and I was able to edit /bootopts and manipulate files with vi.

However, even on these 32-bit machines, when I select Int1F while using HMA, it becomes very strange. Int1F is still fine without HMA.

Is it possible to show the console the actual location of block_move and enable_a20, which should be placed at a lower address?

Translated with DeepL.com (free version)

@ghaerr
Copy link
Copy Markdown
Owner Author

ghaerr commented Apr 24, 2025

Hello @drachen6jp,

Thank you very much for your quick testing! It seems you have shown that all the fixes and enhancements added in #2318 are working, including the unreal mode on PC-98 w/386, INT 1F/15 on all systems when not using HMA, and CPU check working to automatically configure unreal vs INT 1F when xms=on.

All that is very good news for PC-98. See the end of this comment for bad news about disabling/re-enabling A20 using INT 15/1F & HMA.

Is it possible to show the console the actual location of block_move and enable_a20, which should be placed at a lower address?

To debug a running system, you can look at the elks/arc/i86/boot/system.map file (sample portion follows):

00010010 T _start
00010041 T int3
00010043 T early_putchar
...
00023f1d T enable_a20_gate
00023f32 t verify_a20
00023f64 t bios_set_a20
00023f6e t bios_reset_a20
00023f74 t set_a20
00023faa t empty_8042
00023fb9 T block_move
...
000346a8 b gdt_table
000346e8 b tcpdevq
000346ea b tdout_tail
000346ec b NumConsoles

The entries that start with 0001 are .text, which would be at segment FFFFh w/HMA. The 0002 entries are .fartext, shown below at 0330h, and finally 0003 entries are .data section at 09B1h (shown here happens to be IBM PC build):

ELKS 0.9.0-dev (63648 text, 26640 ftext, 11824 data, 7984 bss, 45726 heap)
Kernel text ffff ftext 330 init 72e data 9b1 end 19b1 top 9fc0 536+10+0K free

To dump a hex display of memory contents, use 'hd seg:offset', or disassembly using 'disasm seg:offset'. So to see the block_move handler (this one on IBM PC):

# disasm 330:3fb9
Disassembly of 3fb9:
0330:3fb9  06                push    %es
0330:3fba  56                push    %si
0330:3fbb  55                push    %bp
0330:3fbc  89 e5             mov     %sp,%bp
0330:3fbe  8b 4e 0c          mov     0x0c(%bp),%cx
0330:3fc1  8b 76 0a          mov     0x0a(%bp),%si
0330:3fc4  1e                push    %ds
0330:3fc5  07                pop     %es
0330:3fc6  b4 87             mov     $0x87,%ah
0330:3fc8  cd 15             int     $0x15
0330:3fca  73 06             jae     3fd2
0330:3fcc  fb                sti
0330:3fcd  b8 ff ff          mov     $0xffff,%ax
0330:3fd0  eb 02             jmp     3fd4
0330:3fd2  31 c0             xor     %ax,%ax
0330:3fd4  5d                pop     %bp
0330:3fd5  5e                pop     %si
0330:3fd6  07                pop     %es
0330:3fd7  0e                push    %cs
0330:3fd8  e8 42 ff          call    3f1d // 3f1d
0330:3fdb  cb                lret

So the INT 15/1F block_move handler is indeed in low memory, outside of HMA. (The reason this works here on QEMU is because QEMU does not disable A20 after INT 15, which is incorrect emulation!!)

But unfortunately, simultaneous use of Int1F/15 and HMA still seems to be a no-go.

However, even on these 32-bit machines, when I select Int1F while using HMA, it becomes very strange. Int1F is still fine without HMA.

I now finally understand what is really going on - while this PR adds support to re-enable A20 after block move, and that is likely working, a full solution would require much more work. And now that unreal mode on 386 w/HMA, and INT 15/1F working on 286 with no HMA, the only systems that won't work are 286 systems w/HMA, as those must use INT 15/1F.

The problem is that a running system cannot have A20 disabled at any time, even within the BIOS and continue to run when in HMA, because hardware interrupts must be handled by the kernel, even in the middle of a BIOS call.

I finally realize that re-enabling A20 at the end of block_move won't work: any hardware or device interrupt will route to the _irqit mainline interrupt handling routine - which is still in HMA and thus not accessible. Even moving the mainline interrupt handler to low memory (.fartext) is not good enough - as all the subsequently called C interrupt handler and kernel routines would also have to be resident in .fartext. To be safe, extensive analysis of exactly which kernel routines are called by interrupt handlers would have to be done, and missing one would result in a system crash, and very hard to automatically determine.

Requiring that all interrupt handlers and all associated routines be in low memory is simply not worth it - we're better off just running the kernel in low memory, and disabling HMA.

It is risky for the ELKS kernel to rely on a BIOS call that may affect the status of A20. Basically, if the hardware can support directly addressing XMS memory via BIOS, then there's a way to perform the same thing within the ELKS kernel. This means that if 286 systems running XMS and kernel HMA are that important, one approach here would be to rewrite INT 15/1F block move directly into the kernel, and not use the BIOS.

ELKS uses a very fast mechanism to access XMS on all 386 systems: enabling A20 once, then using "unreal mode" via setting the segment cache register limits via entering protected mode once, and afterwards only using addr32 instruction prefixes for direct access without any further ado. For 286 systems and HMA, rewriting block_move should be straightforward: enter protected mode with the INT 15/1F GDT, transfer the data while in protected mode, then exit protected mode. The big problem here is that the 80286 can't actually exit protected mode - it requires external hardware to reset the processor. So its a big mess. In general, for what little use ELKS gets on 286 systems using XMS, we're better off just requiring no hma=kernel in order to run INT 15/1F.

So the conclusion here is that INT 15/1F cannot reliably be made to work with HMA kernel, which is only actually required on 80286 systems. If the kernel is HMA and INT 15/1F would otherwise be required, the system should disable XMS, as it did before this PR. I am considering either reverting this PR entirely, or reverting most of it in a subsequent PR.

Thank you!

@drachen6jp
Copy link
Copy Markdown
Contributor

Thanks for your detailed explanations.
I recognized my idea was cheap.
not BIOS call but direct protectmode block_move code is necessary.great.

@ghaerr
Copy link
Copy Markdown
Owner Author

ghaerr commented Apr 25, 2025

Hello @drachen6jp,

It has been nice working with you on this, I am glad to get PC-98 ELKS working very well with hardware, as the result of your help!

but direct protectmode block_move code is necessary

Yes, it could be a potential good idea to rewrite elks/arch/i86/lib/bios1F-pc87.S::block_move() to directly enter protected mode and perform the transfer, as a special version for 80286 PC-98 systems only. You are welcome to do that as a contribution if you like, I would be glad to add this to ELKS.

However, there is the problem that 286 cannot exit protected mode without external hardware. Does PC-98 provide a fast way to do so? If it is very slow, perhaps not a good idea to even bother with special block_move function for PC-98 286 systems, since the BIOS 1F function works well when kernel is not in HMA (although it is probably slow too).

Thank you!

@Mellvik
Copy link
Copy Markdown
Contributor

Mellvik commented Apr 25, 2025

Thank you @ghaerr - regardless of final outcome, this has been a very interesting and educating journey. I'm sure I'm not the only one benefitting from learning by example how to create and use fartext assembly routines and connect the with far and near C code. And how to read the system.map. To mention a couple of examples.

As the the analysis above, I have a slightly different take.

The problem is that a running system cannot have A20 disabled at any time, even within the BIOS and continue to run when in HMA, because hardware interrupts must be handled by the kernel, even in the middle of a BIOS call.

This is true. However, BIOS XMS block moves - to my knowledge, from reading source code and sources on the net - always occur in protected mode, even on the 286(!). IOW, no interrupts, not even NMI, are allowed, and they are consequently blocked. This is guaranteed, otherwise protected mode could not be used.

Is it possible that pending interrupts immediately kicks in after the BIOS call returns and a20 has just been turned off? It is, but my testing so far find it unlikely. I have added NMI blocking and interrupt blocking immediately after the INT15 return, and there is no change. It is still possible that an interrupt could 'sneak' in between, but not every time.

Finally, what I'm observing on TLVC is that using the fartext enable_a20_gate and block move routines - xms=int15 and hma=kernel, the system almost finishes boot - beyond the fork of init, then hanging at various points shortly after that. On real hardware. That's quite a lot of XMS activity.

Further testing is hampered by an elusive memory or stack corruption issue that suddenly appeared, likely triggered by, but not necessarily caused by the recent changes. Until that rabbit has been tracked down, this is on hold. And - admittedly - the results reported above may not be completely reliable, more like an indication.

My conclusion though, is 'don't give up on this just yet'.

@ghaerr
Copy link
Copy Markdown
Owner Author

ghaerr commented Apr 25, 2025

BIOS XMS block moves - to my knowledge, from reading source code and sources on the net - always occur in protected mode,

Agreed. But I don't find that surprising - the main parameter to the block move BIOS function is a GDT, after all.

Since "unreal mode" and segment cache limit registers are undocumented behavior by Intel, I would expect that BIOS manufacturers would "go by the book", which means protected mode, not an undocumented functionality (at least undocumented at that point in time).

BTW, I had previously considered the requirement of having to keep all interrupts disabled throughout the block move as pretty problematic, especially when running networking or serial I/O. I'm not sure anyone's tested it, but it would seem to me that dropped serial characters and network packets would be extremely likely during 1K block transfers with interrupts disabled for so long. This alone causes rarer kernel code paths to be executed, with possibly sketchy results.

even on the 286(!)

Can the 80286 even use segment limit cache registers in real mode, even if it has them? It is a known design flaw that the 80286 can't return to real mode after entering protected mode, the Intel designers didn't think there was a need for it. I'm not completely familiar but I think the IBM AT designers added a reset line gate added to the keyboard controller for just this purpose.

Is it possible that pending interrupts immediately kicks in after the BIOS call returns and a20 has just been turned off?

It is

Well, when would the pending interrupts happen, if not immediately after the interrupts were reenabled, either by STI or RETI? The 8259 PIC is managing both edge and level triggered hardware interrupt requests, then queuing them by priority, and holds its interrupt request line active waiting for the CPU to respond to any/all queued interrupts.

IMO, it is that any hardware timer interrupt request that has occurred during the XMS transfer and is pending at the time of BIOS completion is acted upon on immediately after RETI but before the next instruction, and thus crashes the system. It doesn't always happen because the hardware interrupt request doesn't always occur during the XMS transfer.

It is still possible that an interrupt could 'sneak' in between, but not every time.

the system almost finishes boot - beyond the fork of init, then hanging at various points shortly after that. On real hardware. That's quite a lot of XMS activity.

My take isn't that interrupts are "sneaking in", but that you are seeing varying behavior because of exactly when the hardware timer interrupt (the highest 8259 priority) fires: if it fires outside XMS activity it is handled normally, but if it fires during the protected mode XMS transfer with interrupts disabled, it is handled immediately upon return from the BIOS call (assuming the BIOS doesn't enable interrupts earlier). At this point, we're just past the INT 15, with the next instruction not yet executed. The A20 line is off, and even though the CS:IP is in .fartext, the interrupt is processed which vectors to HMA - crash.

Within the BIOS itself, we can't depend on exactly when it re-enables interrupts with respect to its disabling the A20 gate. This varying BIOS behavior would still mean possible crashes even with a FARPROC block_move/enable_a20_gate on some systems, so it's an unreliable solution.

In fact it was this very scenario that pushed me towards the conclusion this couldn't be made to work.

There is a possibility though, somewhat like the MOV SS instructions, where interrupts might be disabled for one instruction past the RETI that you might try playing with: adding a CLI immediately after the INT 15, and STI after the call to re-enable A20. If the CPU delays one instruction before actually enabling interrupts from the RETI, this would change the behavior. If not, well, then the interrupt still occurs immediately after the RETI and before the CLI is executed - crash.

Further testing is hampered by an elusive memory or stack corruption issue that suddenly appeared, likely triggered by, but not necessarily caused by the recent changes.

Is that in ELKS, or your fork? I haven't seen any new bugs in ELKS.

My conclusion though, is 'don't give up on this just yet'.

Your debugging has certainly turned up some very interesting results in the past, so I would have to agree :) I feel pretty confident though, that we entirely understand the segment limit register cache and protected mode issues... what's left is ensuring we fully understand exactly when interrupts are allowed to occur around RETI. Nonetheless, because each BIOS is different, we can't rely on even a one instruction interrupt enable delay after RETI because the BIOS might still enable interrupts earlier, with A20 off.

If one really wants, it seems to me the reliable solution is to write our own 286 block_move function outside the BIOS.

@Mellvik
Copy link
Copy Markdown
Contributor

Mellvik commented Apr 25, 2025

BTW, I had previously considered the requirement of having to keep all interrupts disabled throughout the block move as pretty problematic, especially when running networking or serial I/O. I'm not sure anyone's tested it, but it would seem to me that dropped serial characters and network packets would be extremely likely during 1K block transfers with interrupts disabled for so long. This alone causes rarer kernel code paths to be executed, with possibly sketchy results.

This was my point. The BIOS runs this with all interrupts completely blocked - and I believe the small size of the transfers combined with the relative high speed of the processors (over pre-AT systems) makes this possible. I'm running 57600 and 115200 bps serial regularly on the ALI system and it's fine. I suspect though that 64k block transfers would put us into trouble.

even on the 286(!)

Can the 80286 even use segment limit cache registers in real mode, even if it has them? It is a known design flaw that the 80286 can't return to real mode after entering protected mode, the Intel designers didn't think there was a need for it.

That may well be the reason for the 'remote controlled reset facility'. Anyway, yes, with loadall the segment registers may be set - in effect just like unreal mode but without entering protected mode, but still much more complicated (and the unfortunate use of physical address 0x080:0). That said, what all the 286 BIOSes do, is to use protected mode and exit via the reset line. It's not fast but it works well and XMS transfers are much faster than disk still.

My take isn't that interrupts are "sneaking in", but that you are seeing varying behavior because of exactly when the hardware timer interrupt (the highest 8259 priority) fires: if it fires outside XMS activity it is handled normally, but if it fires during the protected mode XMS transfer with interrupts disabled, it is handled immediately upon return from the BIOS call (assuming the BIOS doesn't enable interrupts earlier). At this point, we're just past the INT 15, with the next instruction not yet executed. The A20 line is off, and even though the CS:IP is in .fartext, the interrupt is processed which vectors to HMA - crash.

Within the BIOS itself, we can't depend on exactly when it re-enables interrupts with respect to its disabling the A20 gate. This varying BIOS behavior would still mean possible crashes even with a FARPROC block_move/enable_a20_gate on some systems, so it's an unreliable solution.

I agree, and it wouldn't surprise me if that ended up being the final conclusion. Curiosity though ...

In fact it was this very scenario that pushed me towards the conclusion this couldn't be made to work.

There is a possibility though, somewhat like the MOV SS instructions, where interrupts might be disabled for one instruction past the RETI that you might try playing with: adding a CLI immediately after the INT 15, and STI after the call to re-enable A20. If the CPU delays one instruction before actually enabling interrupts from the RETI, this would change the behavior. If not, well, then the interrupt still occurs immediately after the RETI and before the CLI is executed - crash.

I did that (plus the NMI blockage) and it changed the behaviour enough to make me even more curious, to be investigated when my newfound rabbit is caught and in the oven.

Further testing is hampered by an elusive memory or stack corruption issue that suddenly appeared, likely triggered by, but not necessarily caused by the recent changes.

Is that in ELKS, or your fork? I haven't seen any new bugs in ELKS.

I haven't tried this on ELKS, I may do that if I hit the wall on this just to narrow down the possibilities.

If one really wants, it seems to me the reliable solution is to write our own 286 block_move function outside the BIOS.

We've mentioned LOADALL briefly in and out for years seemingly, it may be becoming just too tempting. I seem to have at least 4 tabs labeled LOADALL open in the browser just now ...

But first, the rabbit.

@ghaerr
Copy link
Copy Markdown
Owner Author

ghaerr commented Apr 25, 2025

at least 4 tabs labeled LOADALL open in the browser

That got me interested, and I happened to run into this, an absolutely amazing explainer on exactly how and when the segment descriptor caches get set. Robert Collins goes on to explain the how the real operation of the cache is different than that documented, and then talks about how the CS cache is handled differently.

Reading that article (which also explains that there's also a 80386 LOADALL BTW) and his explaining 386 internal cache operation, I started thinking about the difference between my original "unreal mode" code setting up XMS and your version (now standard on both our forks) that enabled the Compaq to run. It occurs to me the difference isn't necessarily that the Compaq 386 CPU is somehow too early a generation nor your version that loads CS to work, but rather something simpler which is that my GDT never defined a CS segment in the new GDT, while yours does. Given Collin's explanation about the CS GDT descriptor being handled differently than documented, this could in fact be the real reason it works. At this point, it doesn't matter, as I'm sure the rabbit hole is deep given the article's explanations of internals and Intel's 386 LOADALL still apparently top secret.

Finally, I didn't realize that the 286 LOADALL allows changing the segment base register caches without even loading a segment register (in fact, it only works until the next segment register is loaded). Anyways, this means that a LOADALL-based 286 scheme would likely run much faster than a full protected-mode entry, although all interrupts would have to remain disabled. The article goes on to talk about how LOADALL was used in DOS 3.3 and RAMDRIVE, and many DOS extenders at the time depended on it since it didn't require a very slow protected mode exit. Fascinating!

@Mellvik
Copy link
Copy Markdown
Contributor

Mellvik commented Apr 25, 2025

Happy to hear that, you're closing in on my reading list. Let's get back to it when this one is out of the way,

BTW, did you encounter STOREALL too? I mentioned it in a different thread the other day?

For later.

@drachen6jp
Copy link
Copy Markdown
Contributor

It is a very interesting topic and I will get on it.
I wanted to rewrite a program I wrote before to make PCM sound with beep using 286 loadall.
How can I make the elks kernel reserve memory from 80:0000 at boot time as unused area for loadall?

@ghaerr
Copy link
Copy Markdown
Owner Author

ghaerr commented Apr 25, 2025

Hello @drachen6jp,

It is a very interesting topic and I will get on it.
I wanted to rewrite a program I wrote before to make PCM sound with beep using 286 loadall.

That would be great! You can put all the code directly in elks/arch/i86/lib/bios1F-pc98.S if you like. However, this function requires a GDT as parameter, which you might have to decode. If you would rather start with real mode segment/offsets, then look at the routine that calls it in elks/arch/i86/mm/xms.c::int15_fmemcpyw() instead.

Since your version will not be disabling A20, there is no reason to have to move block_move to FARPROC.

How can I make the elks kernel reserve memory from 80:0000 at boot time as unused area for loadall?

The easiest way would be to reserve the start of the various ELKS OS low memory segments to start at 90:0000 instead of the current 60:0000. Here is the section of include/linuxmt/config.h that you would modify:

#ifdef CONFIG_ARCH_PC98
#define OPTSEGSZ        0x400       /* max size of /bootopts file (1024 bytes max) */
#define DEF_OPTSEG      0x60        /* 0x400 bytes boot options at lowest usable ram */
#define REL_INITSEG     0xA0        /* 0x200 bytes setup data */
#define DMASEG          0xC0        /* start of floppy sector buffer */
#define REL_SYSSEG      DMASEGEND   /* kernel code segment */
#define SETUP_DATA      REL_INITSEG
#endif

Since LOADALL requires 152 bytes at 80:0, you should be OK with starting DEF_OPTSEG at 0x90. Since DEF_OPTSEG used to be 0x60, you need to add 0x30 to REL_INITSEG and DMASEG values, and should be good. Do a make kclean; make kimage and you can test and verify by looking at the kernel startup banner which shows the Kernel text segment starting 0x30 higher than it used to - I think 0x370. It is probably best to remove hma=kernel at first until you want to test if it is working.

Thank you!

@ghaerr
Copy link
Copy Markdown
Owner Author

ghaerr commented Apr 25, 2025

@Mellvik,

BTW, did you encounter STOREALL too? I mentioned it in a different thread the other day?

Wow, your suggestions are sending me to finding all sorts of interesting articles!

Apart from finding out how STOREALL was apparently used along with LOADALL for entering and exiting ICE chip debuggers, I was also able to find the much-talked-about original Intel 80286 "LOADALL Howto" here. This was apparently only given to systems software developers under an NDA basis back in the day.

The document really gives a lot of information and historical thoughts about what went into the design of the 286 as well as chip errata. Required reading for sure given what we've just been through perfecting our kernel's 80386 "unreal mode" setting the segment limit cache registers. The devil's always in the details, and this and Collins' article help to show that what we might think of as complicated is even more complicated when one considers what's happening inside the chip when the technical discussions turn towards private chip registers and logic few have even heard of, as well as things I've never thought of which must be handled, such how the chip handles NULL selectors in protected mode, and lots more.

Thank you!

@ghaerr
Copy link
Copy Markdown
Owner Author

ghaerr commented Apr 26, 2025

@drachen6jp and @Mellvik,

After reading quite a bit about segment descriptor cache registers, real mode, protected mode and LOADALL, I came across one of the best articles I've seen yet, it's titled The Low-Down on LOADALL. I found the reference from another article on Stack Exchange here.

@Mellvik, this article is most interesting: while giving examples of how the 102 byte (I may have been incorrect saying 152 above) LOADALL structure at 80:0 should be laid out and nicely explained, it also goes on to talk about how code can be executed XMS memory in real mode, although with very serious restrictions. The restriction is that the CS segment cache register is reset with a jump instruction. Sound familiar? The protected mode version of the same is making more sense now.

He then goes on to explain that, at least on 80286, what happens with LOADALL (very similarly to loading the GDT with 80386), the segment cache descriptors are not actually changed until/when the corresponding segment register is changed. We knew that, but he then goes on to say this applies to CS as well. The problem with executing real mode code in XMS is that when the CS register is changed/reloaded, the CPU clears the upper four bits (upper 15 megabytes on 24-bit address 286), and thus XMS execution continues in lower memory - with a crash. Call instructions don't have that effect, and I think he's talking about intrasegment jumps (near jumps) - which clear the prefetch queue. The mechanics sound close to what we've recently seen and you've tested setting up unreal mode.

Apparently a possible difference between the 286 after LOADALL and the 386 in real mode is that on the 286, the segment cache base and limit registers are set by LOADALL but recalculated every time (but only when) the segment register is changed (including CS), whereas on the 386 in real mode, the segment limit cache registers remain unmodified when a segment register is changed (and the segment base registers may also all cleared when returning from PM to real mode, not sure, since our code always pops all registers after exiting PM, including CS).

[EDIT: A better description of how things works comes from Collins: At power-up, the descriptor cache registers are loaded with fixed, default values, the CPU is in real mode, and all segments are marked as read/write data segments, including the code segment (CS). According to Intel, each time the CPU loads a segment register in real mode, the base address is 16 times the segment value, while the access rights and size limit attributes are given fixed, "real-mode compatible" values. This is not true. In fact, only the CS descriptor cache access rights get loaded with fixed values each time the segment register is 1oaded - and even then only when a far jump is encountered. Loading any other segment register in real mode does not change the access rights or the segment size limit attributes stored in the descriptor cache registers. For these segments, the access rights and segment size limit attributes are honored from any previous setting.

Protected mode differs from real mode in this respect each time the CPU loads a segment register, it fully loads the descriptor cache register, no previous values are honored. The CPU loads the descriptor cache directly from the descriptor table. The CPU checks the validity of the segment by testing the access rights in the descriptor table, and illegal values will generate exceptions. Any attempt to load CS with a read/write data segment will generate a protection error. Likewise, any attempt to load a data segment register as an executable segment will also generate an exception. The CPU enforces these protection rules very strictly if the descriptor table entry passes all the tests, then the CPU loads the descriptor cache register.
--- End of quote

Lots to remember and think about, I thought this article very interesting.

@Mellvik
Copy link
Copy Markdown
Contributor

Mellvik commented Apr 27, 2025

Very nice indeed, @ghaerr - thank you, you found a document not in my reading list, and quite possibly the best/most useful one of them all. Appreciated.

I agree- lots to think about and very interesting. There is no doubt we can do this, the questions is whether the combination of usefulness, fun and the good feeling of seeing it working, balances the time and frustration (i.e. bumpy road). My 286 is certainly ready for it.

BTW, my rabbit has been caught and cooked. Too skinny to taste so I gave it to the dog.

And further testing of the fartext block_move implementation leaves no hope that it can work in an interrupt driven world.

So, loadall is the only way. I'll start preparing for the day I or we get the courage to do some serious testing, by adding a CONFIG option and setting aside the 102 (actually 112 to make it a paragraph) bytes at 0x80:0. Then run a few STOREALLs to get familiar with the content in real life as compared to the theory.

Thanks. Sounds like the beginning of a(nother) journey.

@drachen6jp
Copy link
Copy Markdown
Contributor

drachen6jp commented Apr 27, 2025

thanks.

ELKS OS low memory segments to start at 90:0000

this article helped my work.

I wrote a program.this was good.loadall can access high memory with old 286 system without protect mode.
but 286 10Mhz was much slow for playing PCM with interrupt 4000Hz.32kB transfer was heavy.

move_block function may use this code.(80:0000 is needed)I will establish it. wait a minute. have a good day.

@drachen6jp
Copy link
Copy Markdown
Contributor

drachen6jp commented Apr 28, 2025

I wrote that program.
PC-9801DX2 real 80286
hma and xms buffer are both enabled. this picture was shot from serial console. thanks @tyama
Gpn1uukbkAABJb5

Loadall needs this address memory.(80:0000) that is good. 13c980h was used and transfered.
Gpn2QO8bgAA9Asl
I want to port this code. so I will study how to port them.
thank you!

place
https://www7b.biglobe.ne.jp/~drachen6jp/elks_xms_mod.zip

@ghaerr
Copy link
Copy Markdown
Owner Author

ghaerr commented Apr 28, 2025

Hello @drachen6jp,

Wow, this is some fantastic work!!!! You are very quick :) Very well done. The changes to the xms.c and config.c are very minimal, which is quite nice. Your code in bios1F-pc98.S is amazing. I will study it to make sure I fully understand it.

I have a question about:

        mov     %ax,%bx
        sub     $7,%bx
        mov     $0x050f,%ax             #loadall
        mov     %ax,%cs:(%bx)
...
exe:
        nop                     #loadall
        nop

Instead of creating self-modifying code, what do you think of the idea of using something like this instead:

...
exe:
        db 0x0f        #loadall
        db 0x05

I also wonder if call enable_a20_gate at end is necessary? It should work without it?

I want to port this code. so I will study how to port them.

Would you like me to help getting this code in ELKS? If you like I can take this and put together a PR for it to get a work version into ELKS, and then you can fine tune it for your PC9801DX2 80286.

For the problem with meminfo with error "meminfo: No such device", I think this may be because you are running IBM PC version or old version of source code? This should work, but requires either latest version, or "make clean; make" on entire source tree.

Thank you!

@ghaerr
Copy link
Copy Markdown
Owner Author

ghaerr commented Apr 28, 2025

Hello @drachen6jp,

I have been studying your code. Wow, I find it extremely well done and interesting.

The following code:

        push    %cs
        call    modechange2
        mov     %sp,%bp
        mov     -6(%bp),%ax
        add     $2,%ax
        mov     %ax,%es:(0x1a)  #new IP

That code is very tricky, it took me a while to figure out how that works! IP is guaranteed on the stack since interrupts are disabled. Nice :) I assume there is no possibility of NMI interrupt happening.

I did have a question about how (re)setting segment register limits works after LOADALL - I understand how DS and ES base and limits are set from the passed GDT, then the REP MOVSW. But I wonder how the limit for each is reset to zero for normal real mode. That happens for ES with POP ES, right? But I don't see a POP DS, how is the DS segment base reset, in the case where it was set to high XMS address?

I don't understand the need for call enable_a20_gate, this was previously enabled in xms.c and A20 should stay enabled throughout. In particular it should not be required after the block move now?

Finally, is the following code just a remnant and not needed?:

        mov     $0x37,%dx
        mov     $6,%al
        out     %al,%dx
l2:
        jmp     l2

Thank you, I really enjoy looking at your work!!

@Mellvik
Copy link
Copy Markdown
Contributor

Mellvik commented Apr 29, 2025

@drachen6jp and @ghaerr --
without studying the code much I put it as is (necessary adjustment only) into TLVC, just next to the existing block_move routine in the fartext segment, and ran it on the 286 compaq. Works right out of the box! This is what I call a flying start. Great work, @drachen6jp - thanks!

I'm looking forward to really understand what's going on here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants