Conversation
|
from https://github.com/ghaerr/elks/actions/runs/14634256044 translated by tool. But unfortunately, simultaneous use of Int1F/15 and HMA still seems to be a no-go. 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) |
|
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.
To debug a running system, you can look at the elks/arc/i86/boot/system.map file (sample portion follows): 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): 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): 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!!)
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! |
|
Thanks for your detailed explanations. |
|
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!
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! |
|
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.
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 - 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'. |
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.
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.
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.
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.
Is that in ELKS, or your fork? I haven't seen any new bugs in ELKS.
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. |
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.
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.
I agree, and it wouldn't surprise me if that ended up being the final conclusion. Curiosity though ...
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.
I haven't tried this on ELKS, I may do that if I hit the wall on this just to narrow down the possibilities.
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. |
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! |
|
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. |
|
It is a very interesting topic and I will get on it. |
|
Hello @drachen6jp,
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.
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: 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 Thank you! |
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! |
|
@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.
[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. Lots to remember and think about, I thought this article very interesting. |
|
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. |
|
thanks.
this article helped my work. I wrote a program.this was good.loadall can access high memory with old 286 system without protect mode. move_block function may use this code.(80:0000 is needed)I will establish it. wait a minute. have a good day. |
|
I wrote that program. Loadall needs this address memory.(80:0000) that is good. 13c980h was used and transfered. place |
|
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: Instead of creating self-modifying code, what do you think of the idea of using something like this instead: I also wonder if
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 Thank you! |
|
Hello @drachen6jp, I have been studying your code. Wow, I find it extremely well done and interesting. The following code: 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?: Thank you, I really enjoy looking at your work!! |
|
@drachen6jp and @ghaerr -- I'm looking forward to really understand what's going on here. |


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).