Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
name: CI

# What this workflow asserts about openkal-dos (technical experiment only).
#
# stub stub/stub.S assembles to the bytes the seal writes (tools/src/stub_bytes.h is what
# stub/build.sh generates from it, byte for byte).
# okdos the seal builds on each system of a building machine and refuses a file that is no
# ELF32 i386 executable.
# programs the examples and tests, built for i686-dos-musl through openkal-platform with this
# repository's openkal-dos, run under DOSBox Staging with CWSDPMI beside them (dirun),
# unoptimised and optimised: hello's arguments, exception, environment and status 7;
# files' FAT operations and status 9; a page fault's status 139.

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

env:
XLINGS_VERSION: v2026.10.26.1
XLINGS_NON_INTERACTIVE: '1'
MCPP_VERSION: 2026.11.13.1
LLVM: llvm@23.1.3

jobs:
stub:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Install xlings and mcpp
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s "$XLINGS_VERSION"
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
- name: mcpp and LLVM
run: |
xlings update
xlings install "mcpp@$MCPP_VERSION" -y -g
mcpp self config --mirror GLOBAL
mcpp toolchain install llvm 23.1.3
- name: The stub's bytes are the committed ones
run: |
LLVM="$HOME/.mcpp/registry/data/xpkgs/xim-x-llvm/23.1.3/bin" sh stub/build.sh
git diff --exit-code tools/src/stub_bytes.h

okdos:
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15, windows-2022]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Install xlings (Linux, macOS)
if: runner.os != 'Windows'
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s "$XLINGS_VERSION"
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
- name: Install xlings (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
irm https://d2learn.org/xlings-install.ps1.txt | iex
"$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: mcpp
run: |
xlings update
xlings install "mcpp@$MCPP_VERSION" -y -g
mcpp self config --mirror GLOBAL
- name: okdos builds and refuses what is no executable of the row
run: |
(cd tools && mcpp build)
okdos=$(find tools/target -path '*bin*' \( -name okdos -o -name okdos.exe \) -type f | head -1)
test -n "$okdos"
printf 'not an executable' > plain.bin
if "$okdos" seal plain.bin; then echo "::error::okdos sealed a file that is no ELF32 executable"; exit 1; fi
if "$okdos" check plain.bin; then echo "::error::okdos check accepted a plain file"; exit 1; fi

programs:
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Install xlings and mcpp
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s "$XLINGS_VERSION"
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
- name: mcpp and LLVM
run: |
xlings update
xlings install "mcpp@$MCPP_VERSION" -y -g
mcpp self config --mirror GLOBAL
mcpp toolchain install llvm 23.1.3
# Each program is built with this repository's openkal-dos in place of the indexed one.
- name: This repository's openkal-dos
run: |
for p in examples/hello examples/files tests/fault; do
printf '\n[target.i686-dos-musl.dependencies]\nmcpplibs.openkal-dos = { path = "%s", override = true }\n' \
"$GITHUB_WORKSPACE" >> "$p/mcpp.toml"
done
- name: hello (status 7), unoptimised and optimised
working-directory: examples/hello
run: |
for profile in "" --release; do
set +e; mcpp run --target i686-dos-musl $profile -- zeta alpha > out.txt; rc=$?; set -e
cat out.txt
[ "$rc" = 7 ] || { echo "::error::hello ($profile) ended with $rc, not 7"; exit 1; }
tr -d '\r' < out.txt | grep -qx ' alpha'
tr -d '\r' < out.txt | grep -qx 'caught: an exception crossed a frame'
tr -d '\r' < out.txt | grep -q '^PATH=.'
done
- name: files (status 9)
working-directory: examples/files
run: |
set +e; mcpp run --target i686-dos-musl > out.txt; rc=$?; set -e
cat out.txt
[ "$rc" = 9 ] || { echo "::error::files ended with $rc, not 9"; exit 1; }
tr -d '\r' < out.txt | grep -qx 'read 3: appended'
tr -d '\r' < out.txt | grep -qx 'resized to 40'
tr -d '\r' < out.txt | grep -qx 'removed: true'
- name: a page fault (status 139)
working-directory: tests/fault
run: |
set +e; mcpp run --target i686-dos-musl > out.txt; rc=$?; set -e
cat out.txt
[ "$rc" = 139 ] || { echo "::error::the faulting program ended with $rc, not 139"; exit 1; }
tr -d '\r' < out.txt | grep -qx 'before the fault'
! tr -d '\r' < out.txt | grep -q 'after the fault'
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
*.okdos
156 changes: 156 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# openkal-dos

An implementation of [openkal][kal] on DOS, through a DPMI host.

**TECHNICAL EXPERIMENT ONLY.** This package exists to test that openkal's
interfaces, its C library and its C++ run-time reach a system as distant from
the others as DOS. It carries no stability guarantee and no support; its
interfaces, its hand-over and its stub may change in any release.

A program names the target and nothing else. The row `i686-dos-musl`, its
compile and link conventions, its seal and its runner are the openkal
platform's ([openkal-platform][plat] 0.28.0): the same C library and C++
run-time as on every other system, openkal-musl and openkal-llvm-runtime, with
this implementation beneath them, so a DOS program is written in C++23 with
`import std`:

```toml
[target.i686-dos-musl]
platform = "openkal-platform@0.28.0"
```

```sh
mcpp build --target i686-dos-musl # hello.exe: the stub and an ELF32 i386 executable
mcpp run --target i686-dos-musl # run by dirun under DOSBox Staging, with CWSDPMI beside it
```

`examples/hello` sorts its arguments, throws and catches an exception across a
frame, reads `PATH` and returns 7.

## The program

A DOS program of this arrangement is two parts in one file:

| part | what it is |
|---|---|
| the stub (`stub/stub.S`, 2544 bytes) | an MZ program in 16-bit code |
| the executable | ELF32, i386, linked at its address (`ET_EXEC`, base 0x1000), at most 16 program headers |

The stub finds a 32-bit DPMI host (int 2Fh, 1687h), and where there is none it
runs `CWSDPMI.EXE` from the program's own directory and asks again. It enters
protected mode as a 32-bit client, allocates one block for the executable's
loadable segments, its first stack (64 KiB) and the hand-over record, makes a
code and a data descriptor whose base is the block and whose limit is 4 GiB,
reads each loadable segment from its own file through a 16 KiB transfer buffer
in conventional memory, and enters the executable at `e_entry` with EBX naming
the record. An address of the program is therefore an offset from the block;
any linear address L is reached at L minus the block's base.

`okdos seal <file>` (package `openkal-dos-tools`, the platform's tool) writes the
stub before an executable, in place; `okdos check <file>` reports whether a file
is such a program. The stub applies no relocation, which is why the executable
is linked at its address.

## The hand-over

The record (`src/dos.h`, version 1, 168 bytes) states the block's linear base,
its size and DPMI handle, the segment prefix's selector and segment, the
environment's segment, the transfer buffer's segment, size and selector, the
code and data selectors, the program's file (left open), and the name DOS
started the program by. `okdos_entry` (`src/start.S`) aligns the stack and calls
`okdos_start`, which refuses a record of another version with status 127.

## Two arrangements

| | entry | initialisers | `main` |
|---|---|---|---|
| beneath a C library (`standalone`) | `okdos_entry` records the hand-over and the arguments | the C library's, through `__libc_start_main` | called by the C library |
| alone (no C library) | `okdos_entry` | `__init_array`, run here | called here |

openkal-musl selects `standalone` on `cfg(os = "dos")`. A program that wants no
C library states `c-abi = false` for the row and calls openkal directly.

## What a program receives

| openkal | DOS |
|---|---|
| `stream` | DOS handles: 0, 1 and 2, and a file's (int 21h, 3Fh and 40h) through the transfer buffer; a handle's properties from IOCTL 4400h |
| `memory` | DPMI blocks (0501h, 0502h), 4 KiB granular; the free memory from 0500h |
| `env` | the arguments from the command tail (offset 80h of the segment prefix), the variables from the environment block, the program's name, the system name `dos` |
| `time` | the BIOS tick count at 0040:006C (54.925494 ms, made monotonic across midnight); the wall clock from int 21h 2Ah and 2Ch, read as UTC; a sleep by int 15h, 86h |
| `fs` | the current directory and each volume as supplied directories (`C:\WORK`, `C:`, `Z:`), a directory held as its name; open, create (5Bh refuses a name that exists), truncate, seek, transfers at an offset, find first and next for enquiry and listing, make and remove a directory, remove, rename, a file's time (5700h, 5701h) and a volume's capacity (36h) |
| `terminal` | the console's mode by bit 5 of its device word (cooked: line assembly and echo; raw: every byte as typed), ANSI.SYS's sequences where it answers int 2Fh 1A00h, the size from the BIOS data area |
| `exec` | a DPMI block, zeroed: the code descriptor spans 4 GiB, so every block is executable through it; publishing makes no request |
| `abort` | int 21h, 4Ch with the status; an abort writes its message to standard error and ends with 134 |
| (faults) | a processor fault (division by zero, an instruction the processor lacks, the stack, general protection, a page) is reported on standard error with the instruction's address, and the program ends with 128 and the corresponding signal's number (136, 132, 139), through DPMI's exception handlers (int 31h, 0203h); CWSDPMI's own report is a register dump on the screen and status 1 (measured) |

DOS is reached through DPMI's real-mode simulation (int 31h, 0300h), with every
buffer in the transfer buffer.

A volume is FAT's: names are eight characters, a dot and three, at most 64
bytes with the volume, and no case is distinguished; a node has a modification
time to two seconds and no identity; no link is made; DOS's rename refuses a
name that exists, which is the rename that does not replace
(`KAL_FS_PROP_RENAME_NO_REPLACE`), and the rename that replaces removes the name
first and is not atomic. A name that holds DOS's `\` or a volume's `:` is
refused, as a name that ascends is.

Absent (openkal clause 6.2): contexts, processes as openkal states them, the
network, and regions --- DOS cannot reserve an address apart from backing it: the
DPMI host CWSDPMI is DPMI 0.90 and refuses 0504h (measured), which is the case
`openkal/region.h` names for leaving the interface out. DOSBox Staging's console
accepts the request for raw mode and stays cooked (measured);
`kal_terminal_get_mode` reports what is in effect.

An i386 ok image (`i686-openkal`) runs on DOS as on firmware: the platform's
runner `hosts` include `dos-i686`, so `mcpp pack --pkg-format single-file
--systems dos-i686` builds openkal-run's core runner as a program of this row,
with the image in its `.oke` section, and the runner places the image through
`exec`. Measured under DOSBox Staging: an image built for `i686-openkal`
(C++23, `import std`, an exception across a frame) sorted its arguments, read
`PATH` and returned 7.

`examples/files` writes, appends and reads a file, renames it into a directory,
lists, resizes (the extension reads as zero) and removes them, and returns 9
(measured under DOSBox Staging).

## The row's conventions

The row compiles for `i386-unknown-none-elf` at `-march=i486`: DOSBox Staging
0.83 executes RDTSC and not CMPXCHG8B, under every `cputype` (measured), so a
sixty-four bit atomic is compiler-rt's call (`atomic.c`) rather than that
instruction. The row also uses emulated thread-local storage
(there is no thread register the toolchain can address: openkal-llvm-runtime
builds compiler-rt's `emutls.c` for the row) and unwind tables for every unit,
C included (clang's default for a triple with no system is none, and the
unwinder's own frames need them), and a stack aligned to sixteen bytes at a
call, as an i386 ok image's is. The link is `ld.lld -static
--image-base=0x1000 -z norelro --gc-sections --eh-frame-hdr`, entered at
`okdos_entry`.

## Checks

CI (`.github/workflows/ci.yml`) asserts that `stub/stub.S` assembles to the
committed `tools/src/stub_bytes.h`; that `okdos` builds on Linux, macOS and
Windows and refuses a file that is no ELF32 i386 executable; and that the
examples and `tests/fault`, built for `i686-dos-musl` with this repository's
openkal-dos and run under DOSBox Staging, unoptimised and optimised, print what
they state and end with 7, 9 and 139.

## CWSDPMI

CWSDPMI is Copyright (C) Charles W. Sandmann. It is not part of this package
and this package contains none of it: the stub runs `CWSDPMI.EXE` when it finds
it beside the program. CWSDPMI is distributed under its own license
(`cwsdpmi.doc` in csdpmi7b.zip, www.delorie.com/pub/djgpp/current/v2misc/),
which permits free redistribution of the unmodified binary with its notice. The
xim package `dosbox-staging`, which the row's runner uses, carries it under
those terms. A program distributed with `CWSDPMI.EXE` beside it carries that
notice too.

## License

Apache-2.0 (LICENSE).

[kal]: https://github.com/mcpplibs/openkal
[plat]: https://github.com/mcpplibs/openkal-platform
13 changes: 13 additions & 0 deletions examples/files/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Files on DOS through openkal.fs (openkal-dos; technical experiment only): writes, appends and
# reads a file, renames it into a directory, lists, resizes and removes them, and returns 9.
# mcpp run --target i686-dos-musl
[package]
name = "files"
version = "0.1.0"

[targets.files]
kind = "bin"
main = "src/main.cpp"

[target.i686-dos-musl]
platform = "openkal-platform@0.28.0"
23 changes: 23 additions & 0 deletions examples/files/src/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import std;
namespace fs = std::filesystem;

int main() {
std::println("cwd {}", fs::current_path().string());
{ std::ofstream("NOTE.TXT") << "first line\nsecond line\n"; }
{ std::ofstream("NOTE.TXT", std::ios::app) << "appended\n"; }
std::ifstream in("NOTE.TXT");
int n = 0;
for (std::string line; std::getline(in, line);) std::println("read {}: {}", ++n, line);
in.close();
std::println("size {}", fs::file_size("NOTE.TXT"));
fs::create_directory("SUB");
fs::rename("NOTE.TXT", "SUB/MOVED.TXT");
std::vector<std::string> names;
for (auto& e : fs::directory_iterator("SUB")) names.push_back(e.path().filename().string());
std::println("SUB holds {}", names);
fs::resize_file("SUB/MOVED.TXT", 40);
std::println("resized to {}", fs::file_size("SUB/MOVED.TXT"));
fs::remove_all("SUB");
std::println("removed: {}", !fs::exists("SUB"));
return 9;
}
13 changes: 13 additions & 0 deletions examples/hello/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# A DOS program in C++23 over openkal-dos (technical experiment only): sorts its arguments, throws
# and catches an exception across a frame, reads PATH and returns 7.
# mcpp run --target i686-dos-musl -- b a
[package]
name = "hello"
version = "0.1.0"

[targets.hello]
kind = "bin"
main = "src/main.cpp"

[target.i686-dos-musl]
platform = "openkal-platform@0.28.0"
17 changes: 17 additions & 0 deletions examples/hello/src/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
#include <stdexcept>
#include <algorithm>
int main(int argc, char** argv) {
std::vector<std::string> v(argv + 1, argv + argc);
std::sort(v.begin(), v.end());
std::printf("hello from openkal-dos: musl and libc++ over DPMI, %d args\n", argc);
for (auto& s : v) std::printf(" %s\n", s.c_str());
try { throw std::runtime_error("an exception crossed a frame"); }
catch (const std::exception& e) { std::printf("caught: %s\n", e.what()); }
const char* p = std::getenv("PATH");
std::printf("PATH=%s\n", p ? p : "(none)");
return 7;
}
Loading
Loading