Skip to content

os8088 v1.0.20260803

Choose a tag to compare

@jggonz jggonz released this 03 Aug 14:18
· 18 commits to main since this release
a999aef

Packages move out of the kernel's 64KB segment and into segments of their own. A loaded package used to live in a 19,968-byte pool carved out of the kernel, which meant three copies of the fractal renderer would not fit -- and never could have. Format v3 gives each one a paragraph-aligned region from a conventional-memory arena instead, so five run at once where two was the ceiling; the pool's bytes go back to the kernel, which now has 20,756 free where it had 1,579. Alongside it the kernel learns what CPU it is running on, opens the A20 gate, and can reach memory above 1MB as a data store -- and three new emulated AT-class machines run the same 8086 binary on a 286 and two 386s.

Five fractals where two was the ceiling (#41)

One instance of FRACTAL.O88 costs a 7,168-byte region -- 2,774 bytes of image and 4,394 of working storage -- and the package pool held 19,968 bytes at kernel offset 0xB000. The third launch answered Out of memory, and no amount of care inside the fractal was going to change that: the pool was a fixed slice of a 64KB segment the kernel also had to fit in. Under format v3 each loaded package gets a whole-paragraph region from an arena in conventional memory, first-fit, with occupancy derived from the instance table that was already tracking it. The arena is about 233KB on a 640KB machine and about 107KB at 512KB.

Where the arena starts, and why it is not convenient (#41)

The arena begins at linear 0x65800, one paragraph past the end of the optional back buffer's pinned 0x40000..0x657FF. Starting lower would have given packages more room, and would also have let a package load and a Display toggle in the Control Panel fail each other at run time depending on which happened first. A smaller arena with a fixed boundary is the better failure: on the 256KB floor there is no arena at all, and a package load refuses with a message that says so while every built-in runs as it always has.

The API goes far, and callbacks resolve their own segment (#41)

A package in its own segment can no longer near-call the kernel, so all 53 jump table slots become far: the stride goes from 4 bytes to 8 and the sequence is push ds / push cs / pop ds / call / pop ds / retf. Neither pop ds nor retf touches the flags, so the slots that answer in the carry flag keep their contracts unchanged, and slot order is preserved. Window callbacks took the opposite route -- they stay near offsets, and a new per-window stamp records which segment created the window, so the dispatcher resolves far for a package window and near for a kernel one. That keeps the window record the size it was, at the price of one rule: teardown has to sweep every window stamped with a dying package, because a callback resolved this way must not outlive its segment.

Marshalling at the boundary, not segment overrides everywhere (#41)

The kernel can no longer follow a package's pointer through DS, and the tempting fix -- thread a "which segment" answer through font_str, the menu tracker and the filename parsers -- would pile onto an ES already carrying save-unders, directory caches, the FAT snapshot, sound and disk buffers. So the kernel copies at the boundary instead, the way it has always staged directory entries. Strings land in bounded buffers with a stated capacity and a defined truncation; the entire menu set is copied whole at relayout, which retires the nested-pointer traversal in one move. Package data is treated as hostile input, because it is.

Relocation is gone (#41)

At org 0 on a paragraph boundary the segment base does the work the old class 0 relocations did, and far calls with absolute immediates do class 1's. So the relocation table goes away, and with it os88pkg.py's trick of assembling each package twice at two different origins and diffing the results to find the words that needed patching. The build is one assembly per package now, and the tool is validate-and-stamp. The v2 rule that a package author could only relocate whole 16-bit words is retired along with it.

The kernel knows what CPU it is on, and can see above 1MB (#41)

Two new modules. cpudet.inc distinguishes 8086, 286 and 386-or-later, opens the A20 gate through port 0x92 with a keyboard-controller fallback, and never believes it succeeded without a wraparound test. xmem.inc claims the high memory area, sizes extended memory with int 15h AH=88h, and copies through it -- AH=87h block move on a 286, unreal mode on a 386. Real mode addresses only up to 0x10FFEF, so extended memory is a data store and nothing else; package code stays below 1MB, which is why the package arena is in conventional memory. Unreal mode uses FS and GS precisely because no 8086 code in this tree touches them, so the widened limits survive every segment reload, and every 386 instruction sits in a scoped island behind a run-time tier check -- a cpu 386 directive is assembly-time permission, never proof that the chip is there.

Twenty thousand bytes back in the kernel (#41)

The evicted pool's 19,968 bytes return to the kernel segment, and they pay for the far jump table, the marshalling buffers, the copied menu set and an icon cache with a great deal left over. The guard that binds -- image plus far-code blob against the 64KB segment -- goes from 1,579 bytes of slack to 20,756. The kernel image itself grew, from 41,739 bytes to 44,780, and for the first time in a while that is not the interesting number.

The same binary on a 286 and two 386s (#40)

os8088 is 8086 code running in real mode, so a 286 or a 386 executes it verbatim -- and nothing in the tree said so, because every emulated target was an XT. Three 86Box configurations now do: an AMI 286 clone at 12.5MHz with 1MB, a Shuttle 386SX at 16MHz and a Micronics 386DX at 25MHz with 2MB each, all with ISA VGA and a serial mouse, all booting the 1.44MB images. The OS changes not at all, which is the point -- int 12h still answers 640K and the kernel never leaves real mode. What they buy is the other end of the speed range from the 4.77MHz XT, whose repaints you can watch happen.


Notes

  • Packages must be rebuilt. The package format is v3, and the loader rejects v1 and v2 files outright rather than trying to interpret them -- the API call sequence and the stride between jump table slots both changed. Rebuild against the SDK header shipped with this release; the source of a well-behaved package needs no edits, only one reassembly at org 0.
  • On a 256KB machine there is no package arena, so loading a package from the software disk now fails with a message saying so. Every built-in still runs.
  • The extended-memory allocator and both of its transports are written, verified to assemble and to detect correctly, and have no caller yet. Nothing in this release stores anything above 1MB. Treat it as groundwork.
  • Recorded as unexercised: the 8086 and 286 code paths have no automated coverage, because QEMU emulates neither. The A20-less XT path and the 286's block-move copy need a run on 86Box, which is interactive. The three new AT-class machines also want one manual pass through their BIOS setup screen the first time -- they have a CMOS an XT does not, and it waits for EXIT FOR BOOT to be picked once per VM directory.

Build

Kernel image 44,780 bytes
Image + .bss 44,719 of 65,536 (20,817 free)
Source 40,373 lines across 30 kernel modules

Commits since v1.0.20260802

  • a999aef 386 floppy 86box (#42)
  • 44dd285 Packages leave the kernel segment, and the machine finds memory above 1MB (SPEC.md 20/41) (#41)
  • 2558ac0 The same binary on a 286 and two 386s: three AT-class 86Box machines (#40)

Download the images, the browser demo and the full release log at os8088.com/download/.