From def6e02e24f863418f8a76e430c0c281c27b8e10 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 14 Nov 2026 00:54:11 +0900 Subject: [PATCH 1/5] openkal-dos 0.1.0: openkal on DOS through DPMI, the stub that loads an ELF32 executable, and okdos (technical experiment only) --- .gitignore | 2 + README.md | 115 +++++++ examples/hello/mcpp.toml | 13 + examples/hello/src/main.cpp | 17 ++ mcpp.toml | 37 +++ src/dos.h | 59 ++++ src/dpmi.cpp | 50 ++++ src/env.cpp | 81 +++++ src/kal.cpp | 161 ++++++++++ src/start.S | 14 + src/start.cpp | 73 +++++ src/time.cpp | 75 +++++ stub/build.sh | 24 ++ stub/stub.S | 579 ++++++++++++++++++++++++++++++++++++ tools/mcpp.toml | 15 + tools/src/okdos.cpp | 109 +++++++ tools/src/stub_bytes.h | 164 ++++++++++ 17 files changed, 1588 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 examples/hello/mcpp.toml create mode 100644 examples/hello/src/main.cpp create mode 100644 mcpp.toml create mode 100644 src/dos.h create mode 100644 src/dpmi.cpp create mode 100644 src/env.cpp create mode 100644 src/kal.cpp create mode 100644 src/start.S create mode 100644 src/start.cpp create mode 100644 src/time.cpp create mode 100755 stub/build.sh create mode 100644 stub/stub.S create mode 100644 tools/mcpp.toml create mode 100644 tools/src/okdos.cpp create mode 100644 tools/src/stub_bytes.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a8e8e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +*.okdos diff --git a/README.md b/README.md new file mode 100644 index 0000000..74a665a --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# 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 ` (package `openkal-dos-tools`, the platform's tool) writes the +stub before an executable, in place; `okdos check ` 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` | handles 0, 1 and 2 (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 | +| `abort` | int 21h, 4Ch with the status; an abort writes its message to standard error and ends with 134 | + +DOS is reached through DPMI's real-mode simulation (int 31h, 0300h), with every +buffer in the transfer buffer. Files (`openkal.fs`) over int 21h are the next +interface; processes as openkal states them, contexts and the network have no +DOS counterpart (openkal clause 6.2). + +## The row's conventions + +The row compiles for `i386-unknown-none-elf` at `-march=i586` (the processor +the emulators that run DOS programs model), with 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). The link is `ld.lld -static +--image-base=0x1000 -z norelro --gc-sections --eh-frame-hdr`, entered at +`okdos_entry`. + +## 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 diff --git a/examples/hello/mcpp.toml b/examples/hello/mcpp.toml new file mode 100644 index 0000000..18b5c60 --- /dev/null +++ b/examples/hello/mcpp.toml @@ -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" diff --git a/examples/hello/src/main.cpp b/examples/hello/src/main.cpp new file mode 100644 index 0000000..3a6d616 --- /dev/null +++ b/examples/hello/src/main.cpp @@ -0,0 +1,17 @@ +#include +#include +#include +#include +#include +#include +int main(int argc, char** argv) { + std::vector 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; +} diff --git a/mcpp.toml b/mcpp.toml new file mode 100644 index 0000000..782bd52 --- /dev/null +++ b/mcpp.toml @@ -0,0 +1,37 @@ +# openkal on DOS, through DPMI. TECHNICAL EXPERIMENT ONLY (README): no stability, no support. +# +# A DOS program of this arrangement is a stub (stub/stub.S, an MZ program) followed by an ELF32 +# i386 executable: the stub enters 32-bit protected mode through a DPMI host --- CWSDPMI where +# the machine has none --- and loads the executable into one block of memory; this package +# receives it there (src/start.S) and answers openkal's interfaces through DOS (int 21h), which +# DPMI's real-mode simulation (int 31h, 0300h) reaches with buffers in conventional memory. +[package] +namespace = "mcpplibs" +name = "openkal-dos" +version = "0.1.0" +description = "An implementation of openkal on DOS through DPMI (technical experiment only)" +license = "Apache-2.0" +provides = ["mcpp:kernel-abi=openkal"] +authors = ["mcpplibs"] +repo = "https://github.com/mcpplibs/openkal-dos" + +# The interfaces DOS has a counterpart for. Files (openkal.fs) are the next; contexts, processes +# as openkal states them and the network are absent (openkal clause 6.2). +[kernel-abi] +provides-interfaces = ["openkal.abort", "openkal.env", "openkal.memory", "openkal.stream", "openkal.time"] + +[dependencies] +openkal = "^0.25.0" + +# With no C library this package runs the initialisers and calls `main'; `standalone' is the +# arrangement beneath a C library written above openkal (openkal-musl names it so on +# `cfg(os = "dos")'), whose `__libc_start_main' receives the program. +[features] +standalone = { defines = ["OKDOS_STANDALONE"] } + +[build] +sources = ["src/*.cpp", "src/*.S"] +flags = [ + { glob = "src/**", cxxflags = ["-fno-exceptions", "-fno-rtti", "-fno-stack-protector", + "-fno-asynchronous-unwind-tables"] }, +] diff --git a/src/dos.h b/src/dos.h new file mode 100644 index 0000000..6fd8707 --- /dev/null +++ b/src/dos.h @@ -0,0 +1,59 @@ +// What the stub hands over, and the two ways this implementation reaches DOS. +// +// THE HAND-OVER (stub/stub.S writes it, version 1). The program is entered in 32-bit protected +// mode with CS, DS, ES and SS descriptors whose base is the block the stub allocated and whose +// limit spans 4 GiB, so an address of the program is an offset from the block's base and any +// linear address L is reached at L - base. EBX holds the record's offset. +#pragma once + +namespace okdos { + +using u8 = unsigned char; using u16 = unsigned short; using u32 = unsigned int; + +struct handoff { + u32 magic; // "OKDS" + u32 version; // 1 + u32 base; // the block's linear address + u32 size; // its size + u32 block; // its DPMI handle + u16 psp_selector; + u16 psp_segment; + u16 env_segment; // the environment block, in conventional memory + u16 tb_segment; // the transfer buffer: conventional memory DOS reads and writes + u32 tb_size; + u16 code_selector, data_selector, tb_selector; + u16 exe_handle; // the program's own file, opened by the stub + char name[128]; // the program's name as DOS gave it +}; +static_assert(sizeof(handoff) == 168); +constexpr u32 kMagic = 0x53444b4fu; + +// DPMI's real-mode call structure (int 31h, 0300h). +struct rm_regs { + u32 edi, esi, ebp, reserved, ebx, edx, ecx, eax; + u16 flags, es, ds, fs, gs, ip, cs, sp, ss; +} __attribute__((packed)); +static_assert(sizeof(rm_regs) == 50); + +struct state { + const handoff* h = nullptr; + int argc = 0; + char** argv = nullptr; + u32 ticks_base = 0, ticks_last = 0, ticks_days = 0; // the BIOS tick count, made monotonic +}; +state& self(); + +// A real-mode interrupt with these registers (DPMI 0300h); its carry is the result's. +// Interrupts are enabled during it: DOS may wait on them. +bool real_int(u8 vector, rm_regs& r); +// int 21h with registers only, no buffers; false on carry. +bool dos(rm_regs& r); + +// A linear address of conventional memory, as this program reaches it. +inline u8* low(u32 linear) { return reinterpret_cast(static_cast(linear - self().h->base)); } +inline u8* transfer() { return low(static_cast(self().h->tb_segment) * 16u); } +inline u32 transfer_size() { return self().h->tb_size; } + +void copy(void* to, const void* from, u32 n); + +} // namespace okdos diff --git a/src/dpmi.cpp b/src/dpmi.cpp new file mode 100644 index 0000000..4ceca51 --- /dev/null +++ b/src/dpmi.cpp @@ -0,0 +1,50 @@ +// DOS through DPMI: a real-mode interrupt simulated by the host (int 31h, 0300h), with buffers in +// the transfer buffer the stub allocated in conventional memory. +#include "dos.h" + +namespace okdos { + +state& self() { + static state s{}; + return s; +} + +void copy(void* to, const void* from, u32 n) { + auto* d = static_cast(to); + const auto* s = static_cast(from); + while (n--) *d++ = *s++; +} + +bool real_int(u8 vector, rm_regs& r) { + r.flags = 0x0202; // interrupts enabled + r.ss = 0; // the host's stack + r.sp = 0; + int carry; + __asm__ volatile("int $0x31\n\tsbb %0, %0" + : "=r"(carry) + : "a"(0x0300), "b"(static_cast(vector)), "c"(0), "D"(&r) + : "memory", "cc"); + return carry == 0 && (r.flags & 1) == 0; +} + +bool dos(rm_regs& r) { + if (r.ds == 0) r.ds = self().h->tb_segment; + if (r.es == 0) r.es = self().h->tb_segment; + return real_int(0x21, r); +} + +} // namespace okdos + +// A compiler zeroes and copies a structure through these two whether or not a C library is +// linked; without one they are these, and a C library's own replace them (weak). +extern "C" { +__attribute__((weak)) void* memset(void* p, int c, unsigned long n) { + auto* d = static_cast(p); + while (n--) *d++ = static_cast(c); + return p; +} +__attribute__((weak)) void* memcpy(void* to, const void* from, unsigned long n) { + okdos::copy(to, from, static_cast(n)); + return to; +} +} diff --git a/src/env.cpp b/src/env.cpp new file mode 100644 index 0000000..41aea21 --- /dev/null +++ b/src/env.cpp @@ -0,0 +1,81 @@ +// openkal.env on DOS: the arguments are the command tail of the program's segment prefix (PSP), +// split at blanks; position zero is the name DOS started the program by; the variables are the +// environment block's NAME=VALUE strings. +#include + +#include "dos.h" + +namespace { + +using namespace okdos; + +kal_uintptr length(const char* s) { kal_uintptr n = 0; while (s[n]) ++n; return n; } + +kal_intptr give(const char* s, kal_uintptr n, char* out, kal_uintptr cap) { + if (out && cap) { + const kal_uintptr k = n < cap ? n : cap; + for (kal_uintptr i = 0; i < k; ++i) out[i] = s[i]; + } + return static_cast(n); +} + +const char* environment() { return reinterpret_cast(low(static_cast(self().h->env_segment) * 16u)); } + +// The I-th NAME=VALUE string, or null. +const char* variable(kal_uintptr index) { + const char* p = environment(); + for (kal_uintptr i = 0; *p; ++i) { + if (i == index) return p; + p += length(p) + 1; + } + return nullptr; +} + +} // namespace + +extern "C" { + +kal_uintptr kal_env_arg_count(void) { return static_cast(self().argc); } + +kal_intptr kal_env_arg(kal_uintptr index, char* out, kal_uintptr cap) { + const state& s = self(); + if (index >= static_cast(s.argc)) return -kal_err_not_found; + return give(s.argv[index], length(s.argv[index]), out, cap); +} + +kal_uintptr kal_env_var_count(void) { + kal_uintptr n = 0; + while (variable(n)) ++n; + return n; +} + +kal_intptr kal_env_var_at(kal_uintptr index, char* out, kal_uintptr cap) { + const char* v = variable(index); + if (!v) return -kal_err_not_found; + kal_uintptr n = 0; + while (v[n] && v[n] != '=') ++n; + return give(v, n, out, cap); +} + +kal_intptr kal_env_var(const char* name, kal_uintptr name_len, char* out, kal_uintptr cap) { + for (kal_uintptr i = 0;; ++i) { + const char* v = variable(i); + if (!v) return -kal_err_not_found; + kal_uintptr k = 0; + while (k < name_len && v[k] && v[k] == name[k]) ++k; + if (k == name_len && v[k] == '=') return give(v + k + 1, length(v + k + 1), out, cap); + } +} + +kal_intptr kal_env_image(char* out, kal_uintptr cap) { + const char* n = self().h->name; + return give(n, length(n), out, cap); +} + +kal_intptr kal_env_loader(char*, kal_uintptr) { return -kal_err_not_found; } + +kal_intptr kal_env_system(char* out, kal_uintptr cap) { return give("dos", 3, out, cap); } + +int kal_env_resources(void) { return KAL_RESOURCES_ECONOMY; } + +} // extern "C" diff --git a/src/kal.cpp b/src/kal.cpp new file mode 100644 index 0000000..56ac1d9 --- /dev/null +++ b/src/kal.cpp @@ -0,0 +1,161 @@ +// openkal's core interfaces on DOS, through DPMI (openkal-dos; technical experiment only). +// +// DOS divides its services as openkal does for three interfaces: handles 0 to 2 are the standard +// streams (int 21h, 3Fh and 40h), the program ends with a status (4Ch), and memory beyond the +// first megabyte is the DPMI host's (int 31h, 0501h). The interfaces DOS has no counterpart for +// --- contexts, processes as openkal states them, the network --- are absent rather than present +// and failing (openkal clause 6.2). +#include +#include +#include +#include + +#include "dos.h" + +namespace { + +using namespace okdos; + +constexpr kal_uintptr kStdin = 0, kStdout = 1, kStderr = 2; + +// Writes through the transfer buffer, a piece at a time. +kal_intptr write_handle(u16 handle, const u8* p, kal_uintptr n) { + kal_uintptr done = 0; + while (done < n) { + u32 k = static_cast(n - done); + if (k > transfer_size()) k = transfer_size(); + copy(transfer(), p + done, k); + rm_regs r{}; + r.eax = 0x4000; r.ebx = handle; r.ecx = k; r.edx = 0; + if (!dos(r)) return done ? static_cast(done) : -kal_err_io; + const u32 wrote = r.eax & 0xffff; + done += wrote; + if (wrote < k) break; // a full disk, or a device that takes no more + } + return static_cast(done); +} + +kal_intptr read_handle(u16 handle, u8* p, kal_uintptr n) { + u32 k = static_cast(n); + if (k > transfer_size()) k = transfer_size(); + rm_regs r{}; + r.eax = 0x3f00; r.ebx = handle; r.ecx = k; r.edx = 0; + if (!dos(r)) return -kal_err_io; + const u32 got = r.eax & 0xffff; + copy(p, transfer(), got); + return static_cast(got); +} + +// IOCTL 4400h: bit 7 of the device word is "a device, not a file"; bit 0 or 1 the console. +bool is_console(u16 handle) { + rm_regs r{}; + r.eax = 0x4400; r.ebx = handle; + return dos(r) && (r.edx & 0x80) && (r.edx & 0x03); +} + +} // namespace + +extern "C" { + +KAL_NORETURN void kal_exit(int code) { + __asm__ volatile("int $0x21" ::"a"(0x4c00 | (code & 0xff))); + for (;;) {} +} + +KAL_NORETURN void kal_abort(const char* msg, kal_uintptr len) { + if (self().h) { + if (msg && len) write_handle(2, reinterpret_cast(msg), len); + write_handle(2, reinterpret_cast("\r\n"), 2); + } + // 134, as a process ended by SIGABRT reports itself on the systems that have the signal. + kal_exit(134); +} + +// ── openkal.stream ────────────────────────────────────────────────────────── +kal_stream kal_stdin(void) { return kal_stream{kStdin}; } +kal_stream kal_stdout(void) { return kal_stream{kStdout}; } +kal_stream kal_stderr(void) { return kal_stream{kStderr}; } + +kal_intptr kal_stream_write(kal_stream s, const void* buf, kal_uintptr n) { + if (s.h != kStdout && s.h != kStderr) return -kal_err_invalid; + return write_handle(static_cast(s.h), static_cast(buf), n); +} + +kal_intptr kal_stream_read(kal_stream s, void* buf, kal_uintptr n) { + if (s.h != kStdin) return -kal_err_invalid; + return read_handle(0, static_cast(buf), n); +} + +// DOS writes a handle's bytes when the call returns. +int kal_stream_flush(kal_stream s) { + return s.h <= kStderr ? kal_ok : kal_err_invalid; +} + +kal_uintptr kal_stream_props(kal_stream s) { + if (s.h > kStderr) return 0; + return is_console(static_cast(s.h)) ? KAL_STREAM_PROP_INTERACTIVE : 0; +} + +// ── openkal.memory ────────────────────────────────────────────────────────── +// +// Each region is a block of the DPMI host (0501h), large enough to place the region at its +// alignment with a record of the block before it: its handle and its linear address. The host +// allocates in pages of four kilobytes, which is the granularity stated. +kal_uintptr kal_memory_granularity(void) { return 4096; } + +void* kal_alloc(kal_uintptr size, kal_uintptr align) { + if (!self().h) return nullptr; + if (size == 0) size = 1; + if (align < 16) align = 16; + if ((align & (align - 1)) != 0) return nullptr; + const u32 want = static_cast(size + align + 16); + if (want < size) return nullptr; + u32 bx = want >> 16, cx = want & 0xffff, si, di, carry; + __asm__ volatile("int $0x31\n\tsbb %4, %4" + : "+b"(bx), "+c"(cx), "=S"(si), "=D"(di), "=r"(carry) + : "a"(0x0501) + : "memory", "cc"); + if (carry) return nullptr; + const u32 linear = (bx << 16) | (cx & 0xffff); + const u32 handle = (si << 16) | (di & 0xffff); + const u32 base = self().h->base; + u32 at = (linear - base + 16 + static_cast(align) - 1) & ~(static_cast(align) - 1); + auto* record = reinterpret_cast(static_cast(at - 8)); + record[0] = handle; + record[1] = linear; + return reinterpret_cast(static_cast(at)); +} + +void kal_free(void* p, kal_uintptr, kal_uintptr) { + if (!p) return; + const auto* record = reinterpret_cast(static_cast(p) - 8); + const u32 handle = record[0]; + u32 si = handle >> 16, di = handle & 0xffff; + __asm__ volatile("int $0x31" : "+S"(si), "+D"(di) : "a"(0x0502) : "memory", "cc"); +} + +// DPMI 0500h: the largest free block, and, where the host states them, the pages in total and free. +int kal_memory_info(kal_memory_status* out) { + if (out == nullptr || out->self_size < 2 * sizeof(kal_u32)) return kal_err_invalid; + u32 info[12]; + for (auto& w : info) w = ~0u; + int carry; + __asm__ volatile("push %%es\n\tpush %%ds\n\tpop %%es\n\tint $0x31\n\tpop %%es\n\tsbb %0, %0" + : "=r"(carry) : "a"(0x0500), "D"(info) : "memory", "cc"); + out->present = 0; + if (carry) return kal_ok; + if (out->self_size >= sizeof(kal_memory_status)) { + if (info[6] != ~0u) { out->total_bytes = static_cast(info[6]) * 4096; out->present |= KAL_MSTATUS_TOTAL; } + if (info[0] != ~0u) { out->available_bytes = info[0]; out->present |= KAL_MSTATUS_AVAILABLE; } + } + return kal_ok; +} + +// ── version ───────────────────────────────────────────────────────────────── +kal_u64 kal_version(void) { return KAL_VERSION; } + +kal_u64 kal_interfaces(void) { + return KAL_IFACE_ABORT | KAL_IFACE_STREAM | KAL_IFACE_MEMORY | KAL_IFACE_ENV | KAL_IFACE_TIME; +} + +} // extern "C" diff --git a/src/start.S b/src/start.S new file mode 100644 index 0000000..901837c --- /dev/null +++ b/src/start.S @@ -0,0 +1,14 @@ +// The executable's entry: the stub (stub/stub.S) jumps here in 32-bit protected mode with EBX the +// offset of its hand-over record, and a first stack at the end of its block. + .section .text.okdos_entry, "ax" + .globl okdos_entry + .type okdos_entry, @function +okdos_entry: + xorl %ebp, %ebp + andl $-16, %esp + subl $12, %esp + pushl %ebx + call okdos_start + ud2 + .size okdos_entry, . - okdos_entry + .section .note.GNU-stack, "", @progbits diff --git a/src/start.cpp b/src/start.cpp new file mode 100644 index 0000000..4f95e87 --- /dev/null +++ b/src/start.cpp @@ -0,0 +1,73 @@ +// What the stub enters, and what a program receives (openkal clause 7.13). +// +// okdos_entry (start.S) calls okdos_start with the stub's hand-over record. The arguments are the +// command tail of the program's segment prefix, at offset 80h: a count, then the text, ended by +// a carriage return; position zero is the program's name. +#include + +#include "dos.h" + +extern "C" int main(int, char**, char**); + +#ifdef OKDOS_STANDALONE +// THE STANDALONE ARRANGEMENT: a C library above this implementation (openkal-musl names it so +// on `cfg(os = "dos")') receives the program at the name every C library's hand-over has; it +// obtains the arguments through openkal.env and runs the initialisers itself. +extern "C" int __libc_start_main(int (*)(int, char**, char**), int, char**, + void (*)(), void (*)(), void (*)()); +#else +// Without a C library, the initialisers the linker gathered run here, in order. +extern "C" { +using initialiser = void (*)(); +extern initialiser __init_array_start[] __attribute__((weak)); +extern initialiser __init_array_end[] __attribute__((weak)); +} +#endif + +namespace { + +using namespace okdos; + +// Room for the command tail's words: at most 127 bytes, so at most 64 words. +char tail[128]; +char* words[66]; + +void record_arguments(const handoff* h) { + state& s = self(); + const u8* psp = low(static_cast(h->psp_segment) * 16u); + u32 n = psp[0x80]; + if (n > 127) n = 127; + copy(tail, psp + 0x81, n); + tail[n] = 0; + int argc = 0; + words[argc++] = const_cast(h->name); + for (u32 i = 0; i < n;) { + while (i < n && (tail[i] == ' ' || tail[i] == '\t' || tail[i] == '\r')) tail[i++] = 0; + if (i >= n) break; + words[argc++] = &tail[i]; + while (i < n && tail[i] != ' ' && tail[i] != '\t' && tail[i] != '\r') ++i; + } + words[argc] = nullptr; + s.argc = argc; + s.argv = words; +} + +} // namespace + +extern "C" [[noreturn]] void okdos_start(const handoff* h) { + if (h->magic != kMagic || h->version != 1) { + // A stub of another version: nothing here can be trusted to say so but the status. + kal_exit(127); + } + self().h = h; + record_arguments(h); +#ifdef OKDOS_STANDALONE + __libc_start_main(main, 0, nullptr, nullptr, nullptr, nullptr); + kal_exit(127); +#else + if (__init_array_start) + for (initialiser* f = __init_array_start; f < __init_array_end; ++f) (*f)(); + static char* no_environment[] = {nullptr}; + kal_exit(main(self().argc, self().argv, no_environment)); +#endif +} diff --git a/src/time.cpp b/src/time.cpp new file mode 100644 index 0000000..e71826f --- /dev/null +++ b/src/time.cpp @@ -0,0 +1,75 @@ +// openkal.time on DOS. +// +// MONOTONIC: the BIOS's tick count (linear 0x46C), which the timer interrupt advances 1,573,040 +// times a day (about 18.2 a second) and sets back to zero at midnight; each reading that is below +// the last one counts a day. Its granularity is one tick, 54,925,494 nanoseconds. +// WALL: DOS's date and time (int 21h, 2Ah and 2Ch), to the hundredth of a second. DOS keeps local +// time and states no zone; it is reported as UTC, which it is where the machine's clock was set so. +// SLEEP: the BIOS's wait (int 15h, 86h), in microseconds. +#include + +#include "dos.h" + +namespace { + +using namespace okdos; + +constexpr kal_u64 kTicksPerDay = 0x1800B0; +constexpr kal_u64 kTickNs = 54925494; + +long long days_from_civil(long long y, unsigned m, unsigned d) { + y -= m <= 2; + const long long era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + static_cast(doe) - 719468; +} + +} // namespace + +extern "C" { + +kal_duration kal_time_monotonic(void) { + state& s = self(); + const u32 now = *reinterpret_cast(low(0x46c)); + if (now < s.ticks_last) ++s.ticks_days; + s.ticks_last = now; + const kal_u64 ticks = static_cast(s.ticks_days) * kTicksPerDay + now; + return static_cast(ticks * kTickNs); +} + +kal_duration kal_time_monotonic_granularity(void) { return kTickNs; } + +kal_duration kal_time_wall(void) { + rm_regs d{}; + d.eax = 0x2a00; + if (!dos(d)) return 0; + rm_regs t{}; + t.eax = 0x2c00; + if (!dos(t)) return 0; + const long long year = d.ecx & 0xffff; + const unsigned month = (d.edx >> 8) & 0xff, day = d.edx & 0xff; + const unsigned hour = (t.ecx >> 8) & 0xff, minute = t.ecx & 0xff; + const unsigned second = (t.edx >> 8) & 0xff, hundredths = t.edx & 0xff; + const long long secs = days_from_civil(year, month, day) * 86400 + hour * 3600 + minute * 60 + second; + if (secs < 0) return 0; + return static_cast(secs) * 1000000000ull + static_cast(hundredths) * 10000000ull; +} + +void kal_time_sleep(kal_duration ns) { + kal_duration us = (ns + 999) / 1000; + while (us) { + const kal_duration step = us > 1000000 ? 1000000 : us; + rm_regs r{}; + r.eax = 0x8600; + r.ecx = static_cast(step >> 16); + r.edx = static_cast(step & 0xffff); + real_int(0x15, r); + us -= step; + } +} + +kal_uintptr kal_time_props(void) { return KAL_TIME_PROP_WALL_AVAILABLE; } + +} // extern "C" diff --git a/stub/build.sh b/stub/build.sh new file mode 100755 index 0000000..ba6f362 --- /dev/null +++ b/stub/build.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# Assembles the stub and writes tools/src/stub_bytes.h, the bytes the seal writes before an +# executable. Run after changing stub/stub.S; the result is committed, so that the seal builds +# on a machine whose assembler has no 16-bit x86 (an aarch64 host's). +# LLVM= stub/build.sh +set -eu +here=$(cd "$(dirname "$0")" && pwd) +LLVM=${LLVM:-$(dirname "$(command -v clang)")} +work=$(mktemp -d) +trap 'rm -f "$work/stub.o" "$work/stub.bin"; rmdir "$work"' EXIT +"$LLVM/clang" --target=i386-unknown-none-elf -c "$here/stub.S" -o "$work/stub.o" +"$LLVM/ld.lld" -m elf_i386 --oformat binary --image-base=0 -Ttext=0 -e _okdos_stub "$work/stub.o" -o "$work/stub.bin" +python3 - "$work/stub.bin" "$here/../tools/src/stub_bytes.h" <<'PY' +import sys, hashlib +b = open(sys.argv[1], 'rb').read() +lines = ['// Generated by stub/build.sh from stub/stub.S; do not edit.', + f'// {len(b)} bytes, sha256 {hashlib.sha256(b).hexdigest()}', + '#pragma once', 'inline constexpr unsigned char kOkdosStub[] = {'] +for i in range(0, len(b), 16): + lines.append(' ' + ', '.join(f'0x{x:02x}' for x in b[i:i+16]) + ',') +lines.append('};') +open(sys.argv[2], 'w').write('\n'.join(lines) + '\n') +PY +echo "stub: $(wc -c < "$work/stub.bin") bytes" diff --git a/stub/stub.S b/stub/stub.S new file mode 100644 index 0000000..ab8ddb7 --- /dev/null +++ b/stub/stub.S @@ -0,0 +1,579 @@ +// okdos stub: a DOS program that runs the ELF32 i386 executable appended to it in 32-bit +// protected mode through DPMI (openkal-dos, technical experiment only). +// +// THE FILE is this stub (an MZ program) followed by the executable. The stub's offsets are the +// file's: the header states the code segment and the stack segment two paragraphs below the load +// module, so CS:0 is the file's first byte and an offset here is an offset there. +// +// WHAT IT DOES, in order: frees the memory DOS gave it beyond its own; finds the program's name +// at the end of its environment and opens it; reads the executable's ELF header and program +// headers; asks for a DPMI host (int 2Fh, 1687h), and where there is none runs CWSDPMI.EXE from +// the program's directory and asks again; enters protected mode as a 32-bit client; allocates +// one block of memory for the executable's segments, its first stack and the hand-over record; +// makes a code and a data descriptor whose base is the block and whose limit spans 4 GiB; reads +// each loadable segment into the block through a transfer buffer in conventional memory (int 21h +// simulated by DPMI 0300h), zeroes what the file does not hold, and enters the executable at its +// ELF entry with EBX the hand-over record's offset (src/dos.h, okdos::handoff). +// +// Built by stub/build.sh into tools/src/stub_bytes.h, which the seal (tools/) writes before the +// executable. Apache-2.0, as the rest of openkal-dos; nothing of DJGPP's stub is used. + .code16 + .text + .globl _okdos_stub +_okdos_stub: + // ── MZ header (32 bytes) ───────────────────────────────────────────── + .ascii "MZ" + .word (stub_end - _okdos_stub) % 512 + .word (stub_end - _okdos_stub + 511) / 512 + .word 0 // relocations + .word 2 // header paragraphs + .word (STACK + 15) / 16 // extra paragraphs needed: the stack + .word 0xffff // extra paragraphs wanted + .word 0xfffe // SS: two paragraphs below the load module + .word stub_end + STACK // SP + .word 0 // checksum + .word start // IP + .word 0xfffe // CS: two paragraphs below the load module + .word 0x1c // relocation table + .word 0 // overlay + .word 0, 0 + + .set STACK, 0x800 + .set PHMAX, 16 + .set TB_PARAS, 0x400 // the transfer buffer: 16 KiB + .set FIRST_STACK, 0x10000 // the executable's first stack: 64 KiB + +start: + movw %es, %ax + movw %cs, %bx + movw %bx, %ds + movw %ax, psp_seg + // Keep the stub and its stack; give the rest back so a DPMI host can be loaded. + movw %cs, %bx + subw %ax, %bx // paragraphs from the PSP to CS + addw $((stub_end - _okdos_stub + STACK + 15) / 16), %bx + movb $0x4a, %ah + int $0x21 + jc fail_memory + + // The program's name: after the environment's strings, a word, then the name. + movw psp_seg, %es + movw %es:0x2c, %ax + movw %ax, env_seg + movw %ax, %es + xorw %di, %di +1: cmpb $0, %es:(%di) + je 2f +3: incw %di + cmpb $0, %es:(%di) + jne 3b + incw %di + jmp 1b +2: addw $3, %di // the empty string, then the count word + movw %di, name_off + // Copy it, for CWSDPMI's name beside it. + movw $path, %si + xorw %bx, %bx // the last separator's position in path +4: movb %es:(%di), %al + movb %al, (%si) + cmpb $'\\', %al + jne 5f + movw %si, %bx +5: incw %di + incw %si + cmpb $0, %al + jne 4b + movw %bx, path_sep + + // Open the program (read only) and read the executable's headers. + pushw %ds + movw env_seg, %ds + movw %cs:name_off, %dx + movw $0x3d00, %ax + int $0x21 + popw %ds + jc fail_open + movw %ax, handle + movw $stub_end, %dx + xorw %cx, %cx + call seek + movw $ehdr, %dx + movw $52, %cx + call read + jc fail_read + cmpl $0x464c457f, ehdr // \x7fELF + jne fail_format + cmpw $0x0101, ehdr + 4 // 32-bit, little-endian + jne fail_format + cmpw $3, ehdr + 18 // EM_386 + jne fail_format + movw ehdr + 44, %cx // e_phnum + cmpw $PHMAX, %cx + ja fail_format + movw %cx, phnum + shlw $5, %cx // 32 bytes each + movl ehdr + 28, %edx // e_phoff + addl $stub_end, %edx + pushw %cx + movl %edx, %eax + movw %ax, %dx + shrl $16, %eax + movw %ax, %cx + call seek + popw %cx + movw $phdrs, %dx + call read + jc fail_read + + // The span: the highest end of a loadable segment. + xorl %eax, %eax + movw phnum, %cx + movw $phdrs, %si +6: jcxz 7f + cmpl $1, (%si) // PT_LOAD + jne 8f + movl 8(%si), %edx // p_vaddr + addl 20(%si), %edx // p_memsz + cmpl %eax, %edx + jbe 8f + movl %edx, %eax +8: addw $32, %si + decw %cx + jmp 6b +7: addl $0xfff, %eax + andl $0xfffff000, %eax + movl %eax, span + addl $(FIRST_STACK + 0x1000), %eax + movl %eax, block_size + + // A DPMI host, or CWSDPMI from the program's directory. + call ask_dpmi + jnc have_dpmi + call load_cwsdpmi + call ask_dpmi + jc fail_dpmi +have_dpmi: + // The host's private data, in conventional memory. + movw host_paras, %bx + testw %bx, %bx + jz 9f + movb $0x48, %ah + int $0x21 + jc fail_memory + movw %ax, host_seg +9: movw host_seg, %es + movw $1, %ax // a 32-bit client + lcall *switch_entry + jc fail_switch + + // ── Protected mode (16-bit code, selectors for the real-mode segments) ───────────── + movw %es, psp_sel + // The block: its linear address in BX:CX, its handle in SI:DI. + movl block_size, %eax + movw %ax, %cx + shrl $16, %eax + movw %ax, %bx + movw $0x0501, %ax + int $0x31 + jc fail_block + movw %bx, base + 2 + movw %cx, base + movw %si, block_handle + 2 + movw %di, block_handle + // Two descriptors: code, then data. + xorw %ax, %ax + movw $2, %cx + int $0x31 + jc fail_desc + movw %ax, code_sel + movw %ax, %bx + movw $3, %ax + int $0x31 + addw %bx, %ax + movw %ax, data_sel + // Base and limit of each; access: present, the client's privilege, 32-bit, 4 KiB granular. + movw %cs, %ax + andw $3, %ax + shlw $5, %ax + movw %ax, dpl + movw code_sel, %bx + call frame_descriptor + movw dpl, %cx + orw $0xc09a, %cx // code: execute/read + movw $9, %ax + int $0x31 + jc fail_desc + movw data_sel, %bx + call frame_descriptor + movw dpl, %cx + orw $0xc092, %cx // data: read/write + movw $9, %ax + int $0x31 + jc fail_desc + // The transfer buffer. + movw $0x0100, %ax + movw $TB_PARAS, %bx + int $0x31 + jc fail_memory_pm + movw %ax, tb_seg + movw %dx, tb_sel + + // Each loadable segment, read into the block, the rest of it zero. + movw phnum, %cx + movw $phdrs, %si +10: jcxz 11f + pushw %cx + pushw %si + cmpl $1, (%si) + jne 12f + call load_segment + jc fail_read_pm +12: popw %si + popw %cx + addw $32, %si + decw %cx + jmp 10b +11: + + // The hand-over record, at the end of the executable's span. + movw data_sel, %es + movl span, %edi + movl $0x53444b4f, %es:(%edi) // "OKDS" + movl $1, %es:4(%edi) // version + movl base, %eax + movl %eax, %es:8(%edi) + movl block_size, %eax + movl %eax, %es:12(%edi) + movl block_handle, %eax + movl %eax, %es:16(%edi) + movw psp_sel, %ax + movw %ax, %es:20(%edi) + movw psp_seg, %ax + movw %ax, %es:22(%edi) + movw env_seg, %ax + movw %ax, %es:24(%edi) + movw tb_seg, %ax + movw %ax, %es:26(%edi) + movl $(TB_PARAS * 16), %es:28(%edi) + movw code_sel, %ax + movw %ax, %es:32(%edi) + movw data_sel, %ax + movw %ax, %es:34(%edi) + movw tb_sel, %ax + movw %ax, %es:36(%edi) + movw handle, %ax // the program's file, left open: closing it here hangs + movw %ax, %es:38(%edi) // under DOSBox Staging, and DOS closes it at the end + // The name the program was started by, as DOS gave it. + leal 40(%edi), %edi + movw $path, %si +13: movb (%si), %al + movb %al, %es:(%edi) + incw %si + incl %edi + testb %al, %al + jnz 13b + + // Enter: DS, ES and SS the data descriptor, the first stack at the block's end. + movl span, %ebx + movl ehdr + 24, %edx // e_entry + movw code_sel, %cx + movw data_sel, %ax + movw %ax, %ds + movw %ax, %es + movw %ax, %ss + movl %cs:block_size, %esp + subl $16, %esp + pushl %ecx + pushl %edx + lretl + +// ── Real mode helpers ────────────────────────────────────────────────────── +// seek to CX:DX from the file's start +seek: + movw handle, %bx + movw $0x4200, %ax + int $0x21 + ret +// read CX bytes into DS:DX; carry on error or a short read +read: + pushw %cx + movw handle, %bx + movb $0x3f, %ah + int $0x21 + popw %cx + jc 1f + cmpw %cx, %ax + jne 2f + clc + ret +2: stc +1: ret +// int 2Fh 1687h: carry when there is no 32-bit host +ask_dpmi: + movw $0x1687, %ax + int $0x2f + testw %ax, %ax + jnz 1f + testw $1, %bx + jz 1f + movw %si, host_paras + movw %di, switch_entry + movw %es, switch_entry + 2 + clc + ret +1: stc + ret +// CWSDPMI.EXE beside the program, run as a program; it stays resident for this one. +load_cwsdpmi: + movw path_sep, %di + incw %di + movw $cwsdpmi_name, %si +1: movb (%si), %al + movb %al, (%di) + incw %si + incw %di + testb %al, %al + jnz 1b + movw %cs, %ax + movw %ax, exec_tail + 2 + movw %ax, exec_fcb1 + 2 + movw %ax, exec_fcb2 + 2 + movw %ax, %es + movw $exec_block, %bx + movw $path, %dx + movw %sp, saved_sp + movw $0x4b00, %ax + int $0x21 + movw %cs, %ax // DOS 2 kept nothing but CS and IP across EXEC + movw %ax, %ds + cli + movw %ax, %ss + movw saved_sp, %sp + sti + // The program's own name again, for what follows. + movw env_seg, %es + movw name_off, %si + movw $path, %di +2: movb %es:(%si), %al + movb %al, (%di) + incw %si + incw %di + testb %al, %al + jnz 2b + ret + +// ── Protected mode helpers ───────────────────────────────────────────────── +// BX a descriptor: its base the block, its limit 4 GiB +frame_descriptor: + movw base + 2, %cx + movw base, %dx + movw $7, %ax + int $0x31 + movw $0xffff, %cx + movw $0xffff, %dx + movw $8, %ax + int $0x31 + ret +// int 21h in real mode with the registers as they are (no pointers): DPMI 0300h +dos_rm: + pushl %edi + movl %eax, rm_eax + movl %ebx, rm_ebx + movl %ecx, rm_ecx + movl %edx, rm_edx + movw tb_seg, %ax + movw %ax, rm_ds + movw $0x0202, rm_flags // interrupts enabled: DOS may wait on them + movw $0, rm_ss + movw $0, rm_sp + pushw %es + movw %ds, %ax + movw %ax, %es + movl $rm_regs, %edi // a 32-bit client's structure is at ES:EDI, all of it + movw $0x0300, %ax + movw $0x21, %bx + xorw %cx, %cx + int $0x31 + popw %es + popl %edi + jc 1f + testw $1, rm_flags + jnz 1f + movl rm_eax, %eax + clc + ret +1: stc + ret +// one PT_LOAD at SI: its file bytes, then zero to its size in memory +load_segment: + movl 4(%si), %eax // p_offset + addl $stub_end, %eax + movl %eax, %edx + shrl $16, %eax + movw %ax, %cx + movw handle, %bx + movw $0x4200, %ax + call dos_rm + jc 9f + movl 8(%si), %edi // p_vaddr: the destination in the block + movl 16(%si), %ebp // p_filesz +1: testl %ebp, %ebp + jz 3f + movl $(TB_PARAS * 16), %ecx + cmpl %ecx, %ebp + jae 2f + movl %ebp, %ecx +2: pushl %ecx + movw handle, %bx + xorl %edx, %edx + movb $0x3f, %ah + call dos_rm + popl %ecx + jc 9f + cmpl %ecx, %eax + jne 8f + // copy ECX bytes from the transfer buffer to the block at EDI + pushw %ds + pushw %es + pushw %si + movw data_sel, %ax + movw tb_sel, %dx + movw %ax, %es + movw %dx, %ds + xorl %esi, %esi + cld + subl %ecx, %ebp + .byte 0x67, 0xf3, 0xa4 // rep movsb, 32-bit addresses + popw %si + popw %es + popw %ds + jmp 1b +3: movl 20(%si), %ecx // p_memsz - p_filesz, zero + subl 16(%si), %ecx + jz 4f + pushw %es + movw data_sel, %ax + movw %ax, %es + xorb %al, %al + cld + .byte 0x67, 0xf3, 0xaa // rep stosb, 32-bit addresses + popw %es +4: clc + ret +8: stc +9: ret + +// ── Failures: a sentence on standard error, then status 255 ──────────────── +fail_memory: movw $m_memory, %dx; jmp fail_rm +fail_open: movw $m_open, %dx; jmp fail_rm +fail_read: movw $m_read, %dx; jmp fail_rm +fail_format: movw $m_format, %dx; jmp fail_rm +fail_dpmi: movw $m_dpmi, %dx; jmp fail_rm +fail_switch: movw $m_switch, %dx; jmp fail_rm +fail_rm: + movw %cs, %ax + movw %ax, %ds + movw %dx, %si + xorw %cx, %cx +1: cmpb $0, (%si) + je 2f + incw %si + incw %cx + jmp 1b +2: movw $2, %bx + movb $0x40, %ah + int $0x21 + movw $0x4cff, %ax + int $0x21 +fail_block: movw $m_block, %dx; jmp fail_pm +fail_desc: movw $m_desc, %dx; jmp fail_pm +fail_memory_pm: movw $m_memory, %dx; jmp fail_pm +fail_read_pm: movw $m_read, %dx; jmp fail_pm +fail_pm: + // In protected mode int 21h AH=40h takes DS:DX as it is only where the host translates it; + // the message is copied to the transfer buffer and written from real mode instead, where + // the buffer exists; without one, the status alone says it. + movw tb_sel, %ax + testw %ax, %ax + jz 3f + movw %ax, %es + movw %dx, %si + xorw %di, %di + xorw %cx, %cx +1: movb (%si), %al + testb %al, %al + jz 2f + movb %al, %es:(%di) + incw %si + incw %di + incw %cx + jmp 1b +2: movw $2, %bx + xorw %dx, %dx + movb $0x40, %ah + call dos_rm +3: movw $0x4cff, %ax + int $0x21 + +m_memory: .asciz "okdos: not enough memory\r\n" +m_open: .asciz "okdos: the program cannot open itself\r\n" +m_read: .asciz "okdos: the program's executable cannot be read\r\n" +m_format: .asciz "okdos: the appended executable is not ELF32 i386 with at most 16 program headers\r\n" +m_dpmi: .asciz "okdos: no 32-bit DPMI host, and none from CWSDPMI.EXE beside the program\r\n" +m_switch: .asciz "okdos: the DPMI host refused protected mode\r\n" +m_block: .asciz "okdos: the DPMI host has no block of the executable's size\r\n" +m_desc: .asciz "okdos: the DPMI host refused the program's descriptors\r\n" +cwsdpmi_name: .asciz "CWSDPMI.EXE" + + .balign 4 +psp_seg: .word 0 +psp_sel: .word 0 +env_seg: .word 0 +name_off: .word 0 +path_sep: .word 0 +handle: .word 0 +saved_sp: .word 0 +phnum: .word 0 +host_paras: .word 0 +host_seg: .word 0 +switch_entry: .word 0, 0 +code_sel: .word 0 +data_sel: .word 0 +dpl: .word 0 +tb_seg: .word 0 +tb_sel: .word 0 + .balign 4 +span: .long 0 +block_size: .long 0 +base: .long 0 +block_handle: .long 0 +exec_block: .word 0 +exec_tail: .word exec_empty, 0 +exec_fcb1: .word exec_fcb, 0 +exec_fcb2: .word exec_fcb, 0 +exec_empty: .byte 0, 0x0d +exec_fcb: .space 16, 0 +rm_regs: +rm_edi: .long 0 +rm_esi: .long 0 +rm_ebp: .long 0 + .long 0 +rm_ebx: .long 0 +rm_edx: .long 0 +rm_ecx: .long 0 +rm_eax: .long 0 +rm_flags: .word 0 +rm_es: .word 0 +rm_ds: .word 0 +rm_fs: .word 0 +rm_gs: .word 0 +rm_ip: .word 0 +rm_cs: .word 0 +rm_sp: .word 0 +rm_ss: .word 0 +ehdr: .space 52, 0 +path: .space 128, 0 + .balign 4 +phdrs: .space 32 * PHMAX, 0 + .balign 16 +stub_end: diff --git a/tools/mcpp.toml b/tools/mcpp.toml new file mode 100644 index 0000000..a90ece7 --- /dev/null +++ b/tools/mcpp.toml @@ -0,0 +1,15 @@ +# okdos: the seal of a DOS program --- the stub of openkal-dos written before an ELF32 i386 +# executable. A tool of the building machine, which a platform names in `[platform.tools]` and +# runs as its link's seal. Technical experiment only (README). +[package] +namespace = "mcpplibs" +name = "openkal-dos-tools" +version = "0.1.0" +description = "okdos: seals an ELF32 i386 executable as a DOS program behind openkal-dos's DPMI stub (technical experiment only)" +license = "Apache-2.0" +authors = ["mcpplibs"] +repo = "https://github.com/mcpplibs/openkal-dos" + +[targets.okdos] +kind = "bin" +main = "src/okdos.cpp" diff --git a/tools/src/okdos.cpp b/tools/src/okdos.cpp new file mode 100644 index 0000000..23284d5 --- /dev/null +++ b/tools/src/okdos.cpp @@ -0,0 +1,109 @@ +// okdos: the seal of a DOS program (openkal-dos; technical experiment only). +// +// okdos seal the ELF32 i386 executable becomes a DOS program: the stub +// (stub/stub.S) followed by the executable, written in its place +// okdos check whether is a sealed program, and of which stub +// +// The stub loads the executable's loadable segments from its own file into DPMI memory and +// enters it (src/dos.h states the hand-over). Sealing a sealed program is refused, naming it. +#include "stub_bytes.h" +import std; + +namespace { + +int usage() { + std::println(std::cerr, "usage: okdos seal \n okdos check "); + return 2; +} + +std::optional read(const std::filesystem::path& p) { + std::ifstream in(p, std::ios::binary); + if (!in) return std::nullopt; + return std::string(std::istreambuf_iterator(in), {}); +} + +std::uint32_t u32(const std::string& b, std::size_t at) { + std::uint32_t v = 0; + for (int i = 3; i >= 0; --i) v = (v << 8) | static_cast(b[at + i]); + return v; +} +std::uint16_t u16(const std::string& b, std::size_t at) { + return static_cast(static_cast(b[at]) | static_cast(b[at + 1]) << 8); +} + +bool sealed(const std::string& b) { + return b.size() >= sizeof kOkdosStub + && std::equal(std::begin(kOkdosStub), std::end(kOkdosStub), b.begin(), + [](unsigned char s, char c) { return s == static_cast(c); }); +} + +// What the stub requires of the executable: ELF32, little-endian, i386, an executable (not +// position-independent: the stub applies no relocation), at most 16 program headers. +std::optional refuse(const std::string& b) { + if (b.size() < 52 || b.compare(0, 4, "\x7f" "ELF") != 0) return "not ELF"; + if (b[4] != 1 || b[5] != 1) return "not 32-bit little-endian ELF"; + if (u16(b, 18) != 3) return "not i386 (EM_386)"; + if (u16(b, 16) != 2) return "not an executable linked at its address (ET_EXEC): the stub applies no relocation"; + if (u16(b, 44) > 16) return "more than 16 program headers"; + const std::size_t phoff = u32(b, 28); + for (std::size_t i = 0; i < u16(b, 44); ++i) { + const std::size_t at = phoff + i * 32; + if (at + 32 > b.size()) return "program headers beyond the file"; + if (u32(b, at) == 1 && u32(b, at + 4) + static_cast(u32(b, at + 16)) > b.size()) + return "a loadable segment beyond the file"; + } + return std::nullopt; +} + +} // namespace + +int main(int argc, char** argv) { + if (argc != 3) return usage(); + const std::string_view verb = argv[1]; + const std::filesystem::path file = argv[2]; + const auto bytes = read(file); + if (!bytes) { + std::println(std::cerr, "okdos: {} cannot be read", file.string()); + return 1; + } + if (verb == "check") { + if (!sealed(*bytes)) { + std::println(std::cerr, "okdos: {} is not sealed by this stub", file.string()); + return 1; + } + const std::string elf = bytes->substr(sizeof kOkdosStub); + if (auto why = refuse(elf)) { + std::println(std::cerr, "okdos: {}: the executable after the stub is {}", file.string(), *why); + return 1; + } + std::println("{}: a DOS program: the okdos stub ({} bytes) and an ELF32 i386 executable ({} bytes)", + file.string(), sizeof kOkdosStub, elf.size()); + return 0; + } + if (verb != "seal") return usage(); + if (sealed(*bytes)) { + std::println(std::cerr, "okdos: {} is sealed already", file.string()); + return 1; + } + if (auto why = refuse(*bytes)) { + std::println(std::cerr, "okdos: {} cannot be sealed: {}", file.string(), *why); + return 1; + } + const auto temp = std::filesystem::path(file).concat(".okdos"); + { + std::ofstream out(temp, std::ios::binary | std::ios::trunc); + out.write(reinterpret_cast(kOkdosStub), sizeof kOkdosStub); + out.write(bytes->data(), static_cast(bytes->size())); + if (!out) { + std::println(std::cerr, "okdos: {} cannot be written", temp.string()); + return 1; + } + } + std::error_code ec; + std::filesystem::rename(temp, file, ec); + if (ec) { + std::println(std::cerr, "okdos: {} cannot replace {}: {}", temp.string(), file.string(), ec.message()); + return 1; + } + return 0; +} diff --git a/tools/src/stub_bytes.h b/tools/src/stub_bytes.h new file mode 100644 index 0000000..4e869ac --- /dev/null +++ b/tools/src/stub_bytes.h @@ -0,0 +1,164 @@ +// Generated by stub/build.sh from stub/stub.S; do not edit. +// 2544 bytes, sha256 5c53d70f4175825926c74a1403381cbeb4c9164af30ea1b402fd0103bcc6f01b +#pragma once +inline constexpr unsigned char kOkdosStub[] = { + 0x4d, 0x5a, 0xf0, 0x01, 0x05, 0x00, 0x00, 0x00, 0x02, 0x00, 0x80, 0x00, 0xff, 0xff, 0xfe, 0xff, + 0xf0, 0x11, 0x00, 0x00, 0x20, 0x00, 0xfe, 0xff, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x8c, 0xc0, 0x8c, 0xcb, 0x8e, 0xdb, 0xa3, 0xa8, 0x06, 0x8c, 0xcb, 0x29, 0xc3, 0x81, 0xc3, 0x1f, + 0x01, 0xb4, 0x4a, 0xcd, 0x21, 0x0f, 0x82, 0x30, 0x04, 0x8e, 0x06, 0xa8, 0x06, 0x26, 0xa1, 0x2c, + 0x00, 0xa3, 0xac, 0x06, 0x8e, 0xc0, 0x31, 0xff, 0x26, 0x80, 0x3d, 0x00, 0x74, 0x0a, 0x47, 0x26, + 0x80, 0x3d, 0x00, 0x75, 0xf9, 0x47, 0xeb, 0xf0, 0x83, 0xc7, 0x03, 0x89, 0x3e, 0xae, 0x06, 0xbe, + 0x62, 0x07, 0x31, 0xdb, 0x26, 0x8a, 0x05, 0x88, 0x04, 0x3c, 0x5c, 0x75, 0x02, 0x89, 0xf3, 0x47, + 0x46, 0x3c, 0x00, 0x75, 0xef, 0x89, 0x1e, 0xb0, 0x06, 0x1e, 0x8e, 0x1e, 0xac, 0x06, 0x2e, 0x8b, + 0x16, 0xae, 0x06, 0xb8, 0x00, 0x3d, 0xcd, 0x21, 0x1f, 0x0f, 0x82, 0xe1, 0x03, 0xa3, 0xb2, 0x06, + 0xba, 0xf0, 0x09, 0x31, 0xc9, 0xe8, 0x49, 0x02, 0xba, 0x2e, 0x07, 0xb9, 0x34, 0x00, 0xe8, 0x4a, + 0x02, 0x0f, 0x82, 0xce, 0x03, 0x66, 0x81, 0x3e, 0x2e, 0x07, 0x7f, 0x45, 0x4c, 0x46, 0x0f, 0x85, + 0xc6, 0x03, 0x81, 0x3e, 0x32, 0x07, 0x01, 0x01, 0x0f, 0x85, 0xbc, 0x03, 0x83, 0x3e, 0x40, 0x07, + 0x03, 0x0f, 0x85, 0xb3, 0x03, 0x8b, 0x0e, 0x5a, 0x07, 0x83, 0xf9, 0x10, 0x0f, 0x87, 0xa8, 0x03, + 0x89, 0x0e, 0xb6, 0x06, 0xc1, 0xe1, 0x05, 0x66, 0x8b, 0x16, 0x4a, 0x07, 0x66, 0x81, 0xc2, 0xf0, + 0x09, 0x00, 0x00, 0x51, 0x66, 0x89, 0xd0, 0x89, 0xc2, 0x66, 0xc1, 0xe8, 0x10, 0x89, 0xc1, 0xe8, + 0xef, 0x01, 0x59, 0xba, 0xe4, 0x07, 0xe8, 0xf2, 0x01, 0x0f, 0x82, 0x76, 0x03, 0x66, 0x31, 0xc0, + 0x8b, 0x0e, 0xb6, 0x06, 0xbe, 0xe4, 0x07, 0xe3, 0x1c, 0x66, 0x83, 0x3c, 0x01, 0x75, 0x10, 0x66, + 0x8b, 0x54, 0x08, 0x66, 0x03, 0x54, 0x14, 0x66, 0x39, 0xc2, 0x76, 0x03, 0x66, 0x89, 0xd0, 0x83, + 0xc6, 0x20, 0x49, 0xeb, 0xe2, 0x66, 0x05, 0xff, 0x0f, 0x00, 0x00, 0x66, 0x25, 0x00, 0xf0, 0xff, + 0xff, 0x66, 0xa3, 0xcc, 0x06, 0x66, 0x05, 0x00, 0x10, 0x01, 0x00, 0x66, 0xa3, 0xd0, 0x06, 0xe8, + 0xbd, 0x01, 0x73, 0x0a, 0xe8, 0xd7, 0x01, 0xe8, 0xb5, 0x01, 0x0f, 0x82, 0x2f, 0x03, 0x8b, 0x1e, + 0xb8, 0x06, 0x85, 0xdb, 0x74, 0x0b, 0xb4, 0x48, 0xcd, 0x21, 0x0f, 0x82, 0x0b, 0x03, 0xa3, 0xba, + 0x06, 0x8e, 0x06, 0xba, 0x06, 0xb8, 0x01, 0x00, 0xff, 0x1e, 0xbc, 0x06, 0x0f, 0x82, 0x12, 0x03, + 0x8c, 0x06, 0xaa, 0x06, 0x66, 0xa1, 0xd0, 0x06, 0x89, 0xc1, 0x66, 0xc1, 0xe8, 0x10, 0x89, 0xc3, + 0xb8, 0x01, 0x05, 0xcd, 0x31, 0x0f, 0x82, 0x1b, 0x03, 0x89, 0x1e, 0xd6, 0x06, 0x89, 0x0e, 0xd4, + 0x06, 0x89, 0x36, 0xda, 0x06, 0x89, 0x3e, 0xd8, 0x06, 0x31, 0xc0, 0xb9, 0x02, 0x00, 0xcd, 0x31, + 0x0f, 0x82, 0x05, 0x03, 0xa3, 0xc0, 0x06, 0x89, 0xc3, 0xb8, 0x03, 0x00, 0xcd, 0x31, 0x01, 0xd8, + 0xa3, 0xc2, 0x06, 0x8c, 0xc8, 0x83, 0xe0, 0x03, 0xc1, 0xe0, 0x05, 0xa3, 0xc4, 0x06, 0x8b, 0x1e, + 0xc0, 0x06, 0xe8, 0xaa, 0x01, 0x8b, 0x0e, 0xc4, 0x06, 0x81, 0xc9, 0x9a, 0xc0, 0xb8, 0x09, 0x00, + 0xcd, 0x31, 0x0f, 0x82, 0xd3, 0x02, 0x8b, 0x1e, 0xc2, 0x06, 0xe8, 0x92, 0x01, 0x8b, 0x0e, 0xc4, + 0x06, 0x81, 0xc9, 0x92, 0xc0, 0xb8, 0x09, 0x00, 0xcd, 0x31, 0x0f, 0x82, 0xbb, 0x02, 0xb8, 0x00, + 0x01, 0xbb, 0x00, 0x04, 0xcd, 0x31, 0x0f, 0x82, 0xb4, 0x02, 0xa3, 0xc6, 0x06, 0x89, 0x16, 0xc8, + 0x06, 0x8b, 0x0e, 0xb6, 0x06, 0xbe, 0xe4, 0x07, 0xe3, 0x17, 0x51, 0x56, 0x66, 0x83, 0x3c, 0x01, + 0x75, 0x07, 0xe8, 0xca, 0x01, 0x0f, 0x82, 0x9a, 0x02, 0x5e, 0x59, 0x83, 0xc6, 0x20, 0x49, 0xeb, + 0xe7, 0x8e, 0x06, 0xc2, 0x06, 0x66, 0x8b, 0x3e, 0xcc, 0x06, 0x26, 0x67, 0x66, 0xc7, 0x07, 0x4f, + 0x4b, 0x44, 0x53, 0x26, 0x67, 0x66, 0xc7, 0x47, 0x04, 0x01, 0x00, 0x00, 0x00, 0x66, 0xa1, 0xd4, + 0x06, 0x26, 0x67, 0x66, 0x89, 0x47, 0x08, 0x66, 0xa1, 0xd0, 0x06, 0x26, 0x67, 0x66, 0x89, 0x47, + 0x0c, 0x66, 0xa1, 0xd8, 0x06, 0x26, 0x67, 0x66, 0x89, 0x47, 0x10, 0xa1, 0xaa, 0x06, 0x26, 0x67, + 0x89, 0x47, 0x14, 0xa1, 0xa8, 0x06, 0x26, 0x67, 0x89, 0x47, 0x16, 0xa1, 0xac, 0x06, 0x26, 0x67, + 0x89, 0x47, 0x18, 0xa1, 0xc6, 0x06, 0x26, 0x67, 0x89, 0x47, 0x1a, 0x26, 0x67, 0x66, 0xc7, 0x47, + 0x1c, 0x00, 0x40, 0x00, 0x00, 0xa1, 0xc0, 0x06, 0x26, 0x67, 0x89, 0x47, 0x20, 0xa1, 0xc2, 0x06, + 0x26, 0x67, 0x89, 0x47, 0x22, 0xa1, 0xc8, 0x06, 0x26, 0x67, 0x89, 0x47, 0x24, 0xa1, 0xb2, 0x06, + 0x26, 0x67, 0x89, 0x47, 0x26, 0x67, 0x66, 0x8d, 0x7f, 0x28, 0xbe, 0x62, 0x07, 0x8a, 0x04, 0x26, + 0x67, 0x88, 0x07, 0x46, 0x66, 0x47, 0x84, 0xc0, 0x75, 0xf3, 0x66, 0x8b, 0x1e, 0xcc, 0x06, 0x66, + 0x8b, 0x16, 0x46, 0x07, 0x8b, 0x0e, 0xc0, 0x06, 0xa1, 0xc2, 0x06, 0x8e, 0xd8, 0x8e, 0xc0, 0x8e, + 0xd0, 0x2e, 0x66, 0x8b, 0x26, 0xd0, 0x06, 0x66, 0x83, 0xec, 0x10, 0x66, 0x51, 0x66, 0x52, 0x66, + 0xcb, 0x8b, 0x1e, 0xb2, 0x06, 0xb8, 0x00, 0x42, 0xcd, 0x21, 0xc3, 0x51, 0x8b, 0x1e, 0xb2, 0x06, + 0xb4, 0x3f, 0xcd, 0x21, 0x59, 0x72, 0x07, 0x39, 0xc8, 0x75, 0x02, 0xf8, 0xc3, 0xf9, 0xc3, 0xb8, + 0x87, 0x16, 0xcd, 0x2f, 0x85, 0xc0, 0x75, 0x14, 0xf7, 0xc3, 0x01, 0x00, 0x74, 0x0e, 0x89, 0x36, + 0xb8, 0x06, 0x89, 0x3e, 0xbc, 0x06, 0x8c, 0x06, 0xbe, 0x06, 0xf8, 0xc3, 0xf9, 0xc3, 0x8b, 0x3e, + 0xb0, 0x06, 0x47, 0xbe, 0x9a, 0x06, 0x8a, 0x04, 0x88, 0x05, 0x46, 0x47, 0x84, 0xc0, 0x75, 0xf6, + 0x8c, 0xc8, 0xa3, 0xe0, 0x06, 0xa3, 0xe4, 0x06, 0xa3, 0xe8, 0x06, 0x8e, 0xc0, 0xbb, 0xdc, 0x06, + 0xba, 0x62, 0x07, 0x89, 0x26, 0xb4, 0x06, 0xb8, 0x00, 0x4b, 0xcd, 0x21, 0x8c, 0xc8, 0x8e, 0xd8, + 0xfa, 0x8e, 0xd0, 0x8b, 0x26, 0xb4, 0x06, 0xfb, 0x8e, 0x06, 0xac, 0x06, 0x8b, 0x36, 0xae, 0x06, + 0xbf, 0x62, 0x07, 0x26, 0x8a, 0x04, 0x88, 0x05, 0x46, 0x47, 0x84, 0xc0, 0x75, 0xf5, 0xc3, 0x8b, + 0x0e, 0xd6, 0x06, 0x8b, 0x16, 0xd4, 0x06, 0xb8, 0x07, 0x00, 0xcd, 0x31, 0xb9, 0xff, 0xff, 0xba, + 0xff, 0xff, 0xb8, 0x08, 0x00, 0xcd, 0x31, 0xc3, 0x66, 0x57, 0x66, 0xa3, 0x18, 0x07, 0x66, 0x89, + 0x1e, 0x0c, 0x07, 0x66, 0x89, 0x0e, 0x14, 0x07, 0x66, 0x89, 0x16, 0x10, 0x07, 0xa1, 0xc6, 0x06, + 0xa3, 0x20, 0x07, 0xc7, 0x06, 0x1c, 0x07, 0x02, 0x02, 0xc7, 0x06, 0x2c, 0x07, 0x00, 0x00, 0xc7, + 0x06, 0x2a, 0x07, 0x00, 0x00, 0x06, 0x8c, 0xd8, 0x8e, 0xc0, 0x66, 0xbf, 0xfc, 0x06, 0x00, 0x00, + 0xb8, 0x00, 0x03, 0xbb, 0x21, 0x00, 0x31, 0xc9, 0xcd, 0x31, 0x07, 0x66, 0x5f, 0x72, 0x0e, 0xf7, + 0x06, 0x1c, 0x07, 0x01, 0x00, 0x75, 0x06, 0x66, 0xa1, 0x18, 0x07, 0xf8, 0xc3, 0xf9, 0xc3, 0x66, + 0x8b, 0x44, 0x04, 0x66, 0x81, 0xc0, 0xf0, 0x09, 0x00, 0x00, 0x66, 0x89, 0xc2, 0x66, 0xc1, 0xe8, + 0x10, 0x89, 0xc1, 0x8b, 0x1e, 0xb2, 0x06, 0xb8, 0x00, 0x42, 0xe8, 0x8b, 0xff, 0x72, 0x69, 0x66, + 0x8b, 0x7c, 0x08, 0x66, 0x8b, 0x6c, 0x10, 0x66, 0x85, 0xed, 0x74, 0x42, 0x66, 0xb9, 0x00, 0x40, + 0x00, 0x00, 0x66, 0x39, 0xcd, 0x73, 0x03, 0x66, 0x89, 0xe9, 0x66, 0x51, 0x8b, 0x1e, 0xb2, 0x06, + 0x66, 0x31, 0xd2, 0xb4, 0x3f, 0xe8, 0x60, 0xff, 0x66, 0x59, 0x72, 0x3c, 0x66, 0x39, 0xc8, 0x75, + 0x36, 0x1e, 0x06, 0x56, 0xa1, 0xc2, 0x06, 0x8b, 0x16, 0xc8, 0x06, 0x8e, 0xc0, 0x8e, 0xda, 0x66, + 0x31, 0xf6, 0xfc, 0x66, 0x29, 0xcd, 0x67, 0xf3, 0xa4, 0x5e, 0x07, 0x1f, 0xeb, 0xb9, 0x66, 0x8b, + 0x4c, 0x14, 0x66, 0x2b, 0x4c, 0x10, 0x74, 0x0d, 0x06, 0xa1, 0xc2, 0x06, 0x8e, 0xc0, 0x30, 0xc0, + 0xfc, 0x67, 0xf3, 0xaa, 0x07, 0xf8, 0xc3, 0xf9, 0xc3, 0xba, 0xe4, 0x04, 0xeb, 0x19, 0xba, 0xff, + 0x04, 0xeb, 0x14, 0xba, 0x27, 0x05, 0xeb, 0x0f, 0xba, 0x58, 0x05, 0xeb, 0x0a, 0xba, 0xab, 0x05, + 0xeb, 0x05, 0xba, 0xf6, 0x05, 0xeb, 0x00, 0x8c, 0xc8, 0x8e, 0xd8, 0x89, 0xd6, 0x31, 0xc9, 0x80, + 0x3c, 0x00, 0x74, 0x04, 0x46, 0x41, 0xeb, 0xf7, 0xbb, 0x02, 0x00, 0xb4, 0x40, 0xcd, 0x21, 0xb8, + 0xff, 0x4c, 0xcd, 0x21, 0xba, 0x24, 0x06, 0xeb, 0x0f, 0xba, 0x61, 0x06, 0xeb, 0x0a, 0xba, 0xe4, + 0x04, 0xeb, 0x05, 0xba, 0x27, 0x05, 0xeb, 0x00, 0xa1, 0xc8, 0x06, 0x85, 0xc0, 0x74, 0x20, 0x8e, + 0xc0, 0x89, 0xd6, 0x31, 0xff, 0x31, 0xc9, 0x8a, 0x04, 0x84, 0xc0, 0x74, 0x08, 0x26, 0x88, 0x05, + 0x46, 0x47, 0x41, 0xeb, 0xf2, 0xbb, 0x02, 0x00, 0x31, 0xd2, 0xb4, 0x40, 0xe8, 0xa9, 0xfe, 0xb8, + 0xff, 0x4c, 0xcd, 0x21, 0x6f, 0x6b, 0x64, 0x6f, 0x73, 0x3a, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x65, + 0x6e, 0x6f, 0x75, 0x67, 0x68, 0x20, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x0d, 0x0a, 0x00, 0x6f, + 0x6b, 0x64, 0x6f, 0x73, 0x3a, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x61, + 0x6d, 0x20, 0x63, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x20, 0x6f, 0x70, 0x65, 0x6e, 0x20, 0x69, 0x74, + 0x73, 0x65, 0x6c, 0x66, 0x0d, 0x0a, 0x00, 0x6f, 0x6b, 0x64, 0x6f, 0x73, 0x3a, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x27, 0x73, 0x20, 0x65, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x63, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, + 0x20, 0x72, 0x65, 0x61, 0x64, 0x0d, 0x0a, 0x00, 0x6f, 0x6b, 0x64, 0x6f, 0x73, 0x3a, 0x20, 0x74, + 0x68, 0x65, 0x20, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x20, 0x65, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x69, 0x73, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x45, 0x4c, + 0x46, 0x33, 0x32, 0x20, 0x69, 0x33, 0x38, 0x36, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x61, 0x74, + 0x20, 0x6d, 0x6f, 0x73, 0x74, 0x20, 0x31, 0x36, 0x20, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x61, 0x6d, + 0x20, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x0d, 0x0a, 0x00, 0x6f, 0x6b, 0x64, 0x6f, 0x73, + 0x3a, 0x20, 0x6e, 0x6f, 0x20, 0x33, 0x32, 0x2d, 0x62, 0x69, 0x74, 0x20, 0x44, 0x50, 0x4d, 0x49, + 0x20, 0x68, 0x6f, 0x73, 0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x6e, 0x6f, 0x6e, 0x65, 0x20, + 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x43, 0x57, 0x53, 0x44, 0x50, 0x4d, 0x49, 0x2e, 0x45, 0x58, 0x45, + 0x20, 0x62, 0x65, 0x73, 0x69, 0x64, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x67, + 0x72, 0x61, 0x6d, 0x0d, 0x0a, 0x00, 0x6f, 0x6b, 0x64, 0x6f, 0x73, 0x3a, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x44, 0x50, 0x4d, 0x49, 0x20, 0x68, 0x6f, 0x73, 0x74, 0x20, 0x72, 0x65, 0x66, 0x75, 0x73, + 0x65, 0x64, 0x20, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x20, 0x6d, 0x6f, 0x64, + 0x65, 0x0d, 0x0a, 0x00, 0x6f, 0x6b, 0x64, 0x6f, 0x73, 0x3a, 0x20, 0x74, 0x68, 0x65, 0x20, 0x44, + 0x50, 0x4d, 0x49, 0x20, 0x68, 0x6f, 0x73, 0x74, 0x20, 0x68, 0x61, 0x73, 0x20, 0x6e, 0x6f, 0x20, + 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x78, 0x65, + 0x63, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x27, 0x73, 0x20, 0x73, 0x69, 0x7a, 0x65, 0x0d, 0x0a, + 0x00, 0x6f, 0x6b, 0x64, 0x6f, 0x73, 0x3a, 0x20, 0x74, 0x68, 0x65, 0x20, 0x44, 0x50, 0x4d, 0x49, + 0x20, 0x68, 0x6f, 0x73, 0x74, 0x20, 0x72, 0x65, 0x66, 0x75, 0x73, 0x65, 0x64, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x27, 0x73, 0x20, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x73, 0x0d, 0x0a, 0x00, 0x43, 0x57, 0x53, 0x44, 0x50, 0x4d, + 0x49, 0x2e, 0x45, 0x58, 0x45, 0x00, 0x66, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x90, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x06, + 0x00, 0x00, 0xec, 0x06, 0x00, 0x00, 0xec, 0x06, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x66, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x8d, 0xb4, 0x00, 0x00, 0x8d, 0xb4, 0x00, 0x00, 0x8d, 0xb4, 0x00, 0x00, +}; From 197b477e1c03d4760cb12d3f0253246d283c23dc Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 14 Nov 2026 01:18:55 +0900 Subject: [PATCH 2/5] openkal-dos 0.1.0: files (openkal.fs), the console (openkal.terminal) and executable memory (openkal.exec); regions absent, measured --- README.md | 28 +- examples/files/mcpp.toml | 13 + examples/files/src/main.cpp | 23 ++ mcpp.toml | 8 +- src/dos.h | 17 + src/exec.cpp | 68 ++++ src/fs.cpp | 619 ++++++++++++++++++++++++++++++++++++ src/kal.cpp | 50 ++- src/terminal.cpp | 81 +++++ src/time.cpp | 9 - 10 files changed, 883 insertions(+), 33 deletions(-) create mode 100644 examples/files/mcpp.toml create mode 100644 examples/files/src/main.cpp create mode 100644 src/exec.cpp create mode 100644 src/fs.cpp create mode 100644 src/terminal.cpp diff --git a/README.md b/README.md index 74a665a..0cad8b3 100644 --- a/README.md +++ b/README.md @@ -74,16 +74,36 @@ C library states `c-abi = false` for the row and calls openkal directly. | openkal | DOS | |---|---| -| `stream` | handles 0, 1 and 2 (int 21h, 3Fh and 40h) through the transfer buffer; a handle's properties from IOCTL 4400h | +| `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 | DOS is reached through DPMI's real-mode simulation (int 31h, 0300h), with every -buffer in the transfer buffer. Files (`openkal.fs`) over int 21h are the next -interface; processes as openkal states them, contexts and the network have no -DOS counterpart (openkal clause 6.2). +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. + +`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 diff --git a/examples/files/mcpp.toml b/examples/files/mcpp.toml new file mode 100644 index 0000000..6857402 --- /dev/null +++ b/examples/files/mcpp.toml @@ -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" diff --git a/examples/files/src/main.cpp b/examples/files/src/main.cpp new file mode 100644 index 0000000..ed71194 --- /dev/null +++ b/examples/files/src/main.cpp @@ -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 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; +} diff --git a/mcpp.toml b/mcpp.toml index 782bd52..12deaa6 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -15,10 +15,12 @@ provides = ["mcpp:kernel-abi=openkal"] authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-dos" -# The interfaces DOS has a counterpart for. Files (openkal.fs) are the next; contexts, processes -# as openkal states them and the network are absent (openkal clause 6.2). +# The interfaces DOS has a counterpart for. Absent (openkal clause 6.2): contexts, processes as +# openkal states them, the network, and regions --- the DPMI host reserves no address apart from +# backing it (CWSDPMI is DPMI 0.90 and refuses 0504h, measured). [kernel-abi] -provides-interfaces = ["openkal.abort", "openkal.env", "openkal.memory", "openkal.stream", "openkal.time"] +provides-interfaces = ["openkal.abort", "openkal.env", "openkal.exec", "openkal.fs", "openkal.memory", + "openkal.stream", "openkal.terminal", "openkal.time"] [dependencies] openkal = "^0.25.0" diff --git a/src/dos.h b/src/dos.h index 6fd8707..9db362d 100644 --- a/src/dos.h +++ b/src/dos.h @@ -56,4 +56,21 @@ inline u32 transfer_size() { return self().h->tb_size; } void copy(void* to, const void* from, u32 n); +// A handle's transfers through the transfer buffer (int 21h, 3Fh and 40h): a write of the whole +// buffer a piece at a time, a read of at most the transfer buffer's size. +long write_handle(u16 handle, const u8* p, unsigned long n); +long read_handle(u16 handle, u8* p, unsigned long n); +// Whether a file's handle was opened for appending (fs.cpp): each write is placed at the end. +bool append_handle(u16 handle); + +// Days from 1970-01-01 to a civil date (H. Hinnant's algorithm). +inline long long days_from_civil(long long y, unsigned m, unsigned d) { + y -= m <= 2; + const long long era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + static_cast(doe) - 719468; +} + } // namespace okdos diff --git a/src/exec.cpp b/src/exec.cpp new file mode 100644 index 0000000..df13b3e --- /dev/null +++ b/src/exec.cpp @@ -0,0 +1,68 @@ +// openkal.exec on DOS through DPMI: memory a program may execute, which a loader of images needs +// (the ok-image runner, carried as a DOS program, places an image's code here). +// +// The stub's code descriptor has the block's base and a limit of 4 GiB, so every address of the +// program, a DPMI block allocated since among them, is executable through it: a region is a DPMI +// block, zeroed, at the page granularity. DPMI 0.9 has no protection a client may change on its +// own pages, so publishing and sealing a part make no request; they are statements the caller +// makes, as the interface allows an environment without such a protection. +// +// A file is not mapped (DOS has no mapping): kal_exec_map_part answers kal_err_not_supported, and +// the properties claim neither a file's segments nor a placement. +#include +#include +#include + +namespace { + +constexpr kal_uintptr kPage = 4096; + +kal_uintptr pages(kal_uintptr size) { return (size + kPage - 1) / kPage * kPage; } + +bool part(void* region, kal_uintptr offset, kal_uintptr size) { + return region != nullptr && size != 0 && offset % kPage == 0 && size % kPage == 0; +} + +} // namespace + +extern "C" { + +void* kal_exec_alloc(kal_uintptr size) { + if (size == 0) return nullptr; + const kal_uintptr n = pages(size); + if (n < size) return nullptr; + auto* p = static_cast(kal_alloc(n, kPage)); + if (p) + for (kal_uintptr i = 0; i < n; ++i) p[i] = 0; + return p; +} + +// Placement at a chosen address is not offered: DPMI 0.9 gives a block where the host places it. +void* kal_exec_alloc_at(void*, kal_uintptr) { return nullptr; } + +int kal_exec_publish(void* p, kal_uintptr size) { + return p != nullptr && size != 0 ? kal_ok : kal_err_invalid; +} + +int kal_exec_publish_part(void* region, kal_uintptr offset, kal_uintptr size) { + return part(region, offset, size) ? kal_ok : kal_err_invalid; +} + +int kal_exec_seal_part(void* region, kal_uintptr offset, kal_uintptr size, kal_uintptr flags) { + if (flags & ~KAL_EXEC_SEAL_FOREVER) return kal_err_invalid; + return part(region, offset, size) ? kal_ok : kal_err_invalid; +} + +kal_uintptr kal_exec_granularity(void) { return kPage; } + +int kal_exec_map_part(void*, kal_uintptr, kal_uintptr, struct kal_file, kal_u64, kal_uintptr) { + return kal_err_not_supported; +} + +void kal_exec_free(void* p, kal_uintptr size) { + if (p) kal_free(p, pages(size), kPage); +} + +kal_uintptr kal_exec_props(void) { return KAL_EXEC_PROP_AVAILABLE; } + +} // extern "C" diff --git a/src/fs.cpp b/src/fs.cpp new file mode 100644 index 0000000..f1f5a13 --- /dev/null +++ b/src/fs.cpp @@ -0,0 +1,619 @@ +// openkal.fs on DOS (int 21h), through the transfer buffer. +// +// A DIRECTORY IS A NAME. DOS has no handle for a directory, so a kal_dir is a slot holding the +// directory's whole name (`C:\WORK'), and every operation joins it with the name it is given, +// each `/' written as DOS's `\'. The supplied directories are those of a Windows environment: +// the current directory first (`C:\WORK'), the one the program was started in, then each volume +// DOS has (`C:', `Z:'). A C library above openkal finds the longest supplied name that is a prefix +// of an absolute name, and a volume's name compares as DOS compares it. +// +// THE VOLUME'S PROPERTIES ARE FAT'S: no case is distinguished, no link is made, a node has a +// modification time (to two seconds, DOS's local time read as UTC, as openkal.time reads the +// clock) and no identity, and a rename refuses a name that exists, which is the rename that does +// not replace; the rename that replaces removes the name first, and is not atomic. A volume's +// capacity is int 21h, 36h's. A listing gives each entry's size and time (find first and next). +// +// The names are DOS's: eight characters, a dot and three, at most 64 bytes in all with the +// volume; a longer whole name is refused by DOS and reported as not found. +#include +#include +#include + +#include "dos.h" + +namespace { + +using namespace okdos; + +constexpr int kDirs = 32; +constexpr u32 kPath = 128; +constexpr kal_uintptr kMaxName = 64; + +// THE TABLES ARE ZERO BEFORE THE PROGRAM STARTS (no initialiser runs for them): a C library above +// reads the supplied directories during its own start, before the program's initialisers run, and +// an initialiser here would then empty the table it had filled. +struct listing { + bool active; + bool first; + u8 dta[43]; +}; + +struct directory { + bool used; + char path[kPath]; // `C:\', `C:\WORK' + listing list; +}; + +directory g_dirs[kDirs]; +int g_preopens = -1; +int g_preopen_slot[kDirs]; +bool g_append[256]; +bool g_writable[256]; + +u32 length(const char* s) { u32 n = 0; while (s[n]) ++n; return n; } + +directory* dir_of(kal_dir d) { + if (d.h == 0 || d.h > kDirs || !g_dirs[d.h - 1].used) return nullptr; + return &g_dirs[d.h - 1]; +} + +int new_dir(const char* path) { + for (int i = 0; i < kDirs; ++i) + if (!g_dirs[i].used) { + g_dirs[i].used = true; + g_dirs[i].list.active = false; + const u32 n = length(path); + copy(g_dirs[i].path, path, n + 1); + return i; + } + return -1; +} + +int error_of(const rm_regs& r) { + switch (r.eax & 0xffff) { + case 2: case 3: case 0x0f: case 0x12: return kal_err_not_found; + case 4: case 8: return kal_err_no_memory; + case 5: return kal_err_permission; + case 6: return kal_err_invalid; + case 0x10: return kal_err_permission; // the current directory is not removed + case 0x11: return kal_err_invalid; // not the same volume + case 0x50: return kal_err_exists; + default: return kal_err_io; + } +} + +// The whole name of `name' beneath `base', in `out'; `.' is the directory itself. A name that +// ascends or starts at a root is refused, as the interface states. +int join(kal_dir base, const char* name, kal_uintptr len, char* out) { + const directory* d = dir_of(base); + if (!d) return kal_err_invalid; + if (!name || len == 0 || name[0] == '/') return kal_err_invalid; + if (len > kMaxName) return kal_err_invalid; + u32 o = length(d->path); + copy(out, d->path, o); + if (len == 1 && name[0] == '.') { out[o] = 0; return kal_ok; } + if (o && out[o - 1] != '\\') out[o++] = '\\'; + for (kal_uintptr i = 0; i < len;) { + kal_uintptr j = i; + while (j < len && name[j] != '/') ++j; + const kal_uintptr n = j - i; + if (n == 2 && name[i] == '.' && name[i + 1] == '.') return kal_err_invalid; + if (!(n == 1 && name[i] == '.') && n) { + if (o + n + 2 >= kPath) return kal_err_invalid; + if (o && out[o - 1] != '\\') out[o++] = '\\'; + for (kal_uintptr k = 0; k < n; ++k) { + // DOS's own separator and a volume's colon would leave the directory. + if (name[i + k] == '\\' || name[i + k] == ':') return kal_err_invalid; + out[o++] = name[i + k]; + } + } + i = j + 1; + } + if (o > 3 && out[o - 1] == '\\') --o; + out[o] = 0; + return kal_ok; +} + +// A name in the transfer buffer at `at', for DS:DX (or ES:DI). +void put_name(const char* path, u32 at) { + copy(transfer() + at, path, length(path) + 1); +} + +// A volume's root: `C:\'. +bool is_root(const char* path) { return path[1] == ':' && path[2] == '\\' && path[3] == 0; } + +// int 21h, 4300h: the attributes of a name; -1 when it does not exist. +int attributes(const char* path) { + if (is_root(path)) return 0x10; + put_name(path, 0); + rm_regs r{}; + r.eax = 0x4300; r.edx = 0; + if (!dos(r)) return -1; + return static_cast(r.ecx & 0xffff); +} + +kal_u64 dos_time_ns(u32 date, u32 time) { + const long long y = 1980 + ((date >> 9) & 0x7f); + const unsigned m = (date >> 5) & 0x0f, d = date & 0x1f; + if (m == 0 || d == 0) return 0; + const long long s = days_from_civil(y, m, d) * 86400 + ((time >> 11) & 0x1f) * 3600 + + ((time >> 5) & 0x3f) * 60 + (time & 0x1f) * 2; + return s < 0 ? 0 : static_cast(s) * 1000000000ull; +} + +// DOS's date and time words of a wall time; false before 1980 or after 2107. +bool ns_dos_time(kal_u64 ns, u32& date, u32& time) { + long long s = static_cast(ns / 1000000000ull); + long long days = s / 86400; + long long rem = s % 86400; + // civil_from_days (H. Hinnant) + days += 719468; + const long long era = days / 146097; + const unsigned doe = static_cast(days - era * 146097); + const unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + long long y = static_cast(yoe) + era * 400; + const unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + const unsigned mp = (5 * doy + 2) / 153; + const unsigned d = doy - (153 * mp + 2) / 5 + 1; + const unsigned m = mp < 10 ? mp + 3 : mp - 9; + y += m <= 2; + if (y < 1980 || y > 2107) return false; + date = static_cast(((y - 1980) << 9) | (m << 5) | d); + time = static_cast(((rem / 3600) << 11) | (((rem % 3600) / 60) << 5) | ((rem % 60) / 2)); + return true; +} + +void fill(kal_node_info* out, int kind, kal_u32 wanted, bool sized, kal_u64 size, bool timed, kal_u64 ns, + int attr) { + const kal_u32 cap = out->self_size; + kal_node_info v{}; + v.self_size = cap; + v.kind = kind; + v.present = KAL_INFO_KIND; + if (sized && (wanted & KAL_INFO_SIZE)) { v.size = size; v.present |= KAL_INFO_SIZE; } + if (timed && (wanted & KAL_INFO_MODIFIED)) { v.modified_ns = ns; v.present |= KAL_INFO_MODIFIED; } + if (attr >= 0 && (wanted & KAL_INFO_WRITABLE)) { v.writable = (attr & 1) == 0; v.present |= KAL_INFO_WRITABLE; } + const u32 n = cap < sizeof v ? cap : static_cast(sizeof v); + copy(out, &v, n); + out->self_size = cap; +} + +// Find first (4Eh) for `path' into the transfer buffer's DTA at offset 512; true when found. +bool find_first(const char* path, u16 attrs) { + rm_regs set{}; + set.eax = 0x1a00; set.edx = 512; + dos(set); + put_name(path, 0); + rm_regs r{}; + r.eax = 0x4e00; r.ecx = attrs; r.edx = 0; + return dos(r); +} + +u32 dta_u16(const u8* dta, u32 at) { return dta[at] | (dta[at + 1] << 8); } +u32 dta_u32(const u8* dta, u32 at) { return dta_u16(dta, at) | (dta_u16(dta, at + 2) << 16); } + +void preopen_table() { + if (g_preopens >= 0) return; + g_preopens = 0; + rm_regs drive{}; + drive.eax = 0x1900; + dos(drive); + const char letter = static_cast('A' + (drive.eax & 0xff)); + // The current directory: 47h writes it without the volume and the first separator. + rm_regs cwd{}; + cwd.eax = 0x4700; cwd.edx = 0; cwd.esi = 0; + char path[kPath] = {letter, ':', '\\', 0}; + if (dos(cwd)) { + const char* rest = reinterpret_cast(transfer()); + u32 n = 0; + while (rest[n] && n < 64) { path[3 + n] = rest[n]; ++n; } + path[3 + n] = 0; + } + const int s = new_dir(path); + if (s >= 0) g_preopen_slot[g_preopens++] = s; + // The volumes: IOCTL 4409h answers for a drive that exists. + for (int v = 0; v < 26 && g_preopens < kDirs / 2; ++v) { + rm_regs r{}; + r.eax = 0x4409; r.ebx = static_cast(v + 1); + if (!dos(r)) continue; + const char root[4] = {static_cast('A' + v), ':', '\\', 0}; + const int slot = new_dir(root); + if (slot >= 0) g_preopen_slot[g_preopens++] = slot; + } +} + +} // namespace + +namespace okdos { +bool append_handle(u16 handle) { return handle < 256 && g_append[handle]; } +} + +extern "C" { + +kal_uintptr kal_fs_preopen_count(void) { + preopen_table(); + return static_cast(g_preopens); +} + +int kal_fs_preopen(kal_uintptr index, kal_dir* out, char* name_out, kal_uintptr name_cap, + kal_uintptr* name_len) { + preopen_table(); + if (!out || index >= static_cast(g_preopens)) return kal_err_invalid; + const int slot = g_preopen_slot[index]; + const char* path = g_dirs[slot].path; + // A volume is named without its separator (`C:'), so the rest of a name beneath it does not + // begin with one; the current directory by its whole name. + u32 n = length(path); + if (index > 0 && is_root(path)) n = 2; + if (name_len) *name_len = n; + if (name_out && name_cap) { + const u32 k = n < name_cap ? n : static_cast(name_cap); + copy(name_out, path, k); + } + out->h = static_cast(slot + 1); + return kal_ok; +} + +kal_uintptr kal_fs_props(kal_dir d) { + if (!dir_of(d)) return 0; + return KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_CAPACITY | KAL_FS_PROP_RENAME_NO_REPLACE + | KAL_FS_PROP_LIST_INFO; +} + +kal_uintptr kal_fs_max_name(void) { return kMaxName; } + +int kal_fs_open_dir(kal_dir base, const char* name, kal_uintptr len, kal_dir* out) { + if (!out) return kal_err_invalid; + char path[kPath]; + if (int e = join(base, name, len, path)) return e; + const int a = attributes(path); + if (a < 0) return kal_err_not_found; + if (!(a & 0x10)) return kal_err_not_directory; + const int slot = new_dir(path); + if (slot < 0) return kal_err_no_memory; + out->h = static_cast(slot + 1); + return kal_ok; +} + +int kal_fs_open(kal_dir base, const char* name, kal_uintptr len, kal_uintptr flags, kal_file* out) { + if (!out) return kal_err_invalid; + if (flags & ~(KAL_OPEN_READ | KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_EXCLUSIVE + | KAL_OPEN_TRUNCATE | KAL_OPEN_APPEND)) + return kal_err_invalid; + const bool write = flags & (KAL_OPEN_WRITE | KAL_OPEN_APPEND); + const bool read = flags & KAL_OPEN_READ; + if (!read && !write) return kal_err_invalid; + char path[kPath]; + if (int e = join(base, name, len, path)) return e; + const int a = attributes(path); + if (a >= 0 && (a & 0x10)) return kal_err_is_directory; + rm_regs r{}; + r.edx = 0; + put_name(path, 0); + if ((flags & KAL_OPEN_CREATE) && (flags & KAL_OPEN_EXCLUSIVE)) { + r.eax = 0x5b00; r.ecx = 0; // create a new file: refused where one exists + } else if (a < 0 && (flags & KAL_OPEN_CREATE)) { + r.eax = 0x5b00; r.ecx = 0; + } else if (a < 0) { + return kal_err_not_found; + } else if ((flags & KAL_OPEN_TRUNCATE) && write) { + r.eax = 0x3c00; r.ecx = 0; // create or truncate + } else { + r.eax = 0x3d00u | (read && write ? 2u : write ? 1u : 0u); + } + if (!dos(r)) return error_of(r); + u16 handle = static_cast(r.eax & 0xffff); + // 3Ch and 5Bh open for reading and writing whatever was asked; a file meant for reading only + // is opened again so. + if ((r.eax & 0xffff) && (flags & KAL_OPEN_CREATE) && !write) { + rm_regs c{}; + c.eax = 0x3e00; c.ebx = handle; + dos(c); + put_name(path, 0); + rm_regs o{}; + o.eax = 0x3d00; o.edx = 0; + if (!dos(o)) return error_of(o); + handle = static_cast(o.eax & 0xffff); + } + if (handle >= 256) { + rm_regs c{}; + c.eax = 0x3e00; c.ebx = handle; + dos(c); + return kal_err_no_memory; + } + g_append[handle] = flags & KAL_OPEN_APPEND; + g_writable[handle] = write; + out->h = static_cast(handle); + return kal_ok; +} + +void kal_fs_close_dir(kal_dir d) { + if (directory* p = dir_of(d)) { + for (int i = 0; i < g_preopens; ++i) + if (g_preopen_slot[i] == static_cast(d.h - 1)) return; // supplied: kept + p->used = false; + p->list.active = false; + } +} + +void kal_fs_close_file(kal_file f) { + if (f.h < 5 || f.h >= 256) return; + rm_regs r{}; + r.eax = 0x3e00; r.ebx = static_cast(f.h); + dos(r); + g_append[f.h] = false; + g_writable[f.h] = false; +} + +kal_stream kal_fs_stream(kal_file f) { return kal_stream{f.h}; } + +int kal_fs_seek(kal_file f, kal_i64 offset, int whence, kal_u64* result) { + if (whence < 0 || whence > 2 || f.h < 5) return kal_err_invalid; + if (offset > 0x7fffffffll || offset < -0x80000000ll) return kal_err_invalid; + const u32 o = static_cast(offset); + rm_regs r{}; + r.eax = 0x4200u | static_cast(whence); r.ebx = static_cast(f.h); + r.ecx = o >> 16; r.edx = o & 0xffff; + if (!dos(r)) return error_of(r); + if (result) *result = ((r.edx & 0xffff) << 16) | (r.eax & 0xffff); + return kal_ok; +} + +int kal_fs_truncate(kal_file f, kal_u64 size) { + if (size > 0x7fffffffull) return kal_err_invalid; + kal_u64 here = 0, end = 0; + if (int e = kal_fs_seek(f, 0, KAL_SEEK_CURRENT, &here)) return e; + if (int e = kal_fs_seek(f, 0, KAL_SEEK_END, &end)) return e; + int result = kal_ok; + if (size > end) { + // DOS extends a file with whatever its clusters held; openkal extends it with zero. + u8 zero[512]; + for (auto& b : zero) b = 0; + for (kal_u64 left = size - end; left;) { + const kal_uintptr k = left > sizeof zero ? sizeof zero : static_cast(left); + const kal_intptr w = write_handle(static_cast(f.h), zero, k); + if (w <= 0) { result = kal_err_no_space; break; } + left -= static_cast(w); + } + } else { + kal_fs_seek(f, static_cast(size), KAL_SEEK_SET, nullptr); + rm_regs r{}; + r.eax = 0x4000; r.ebx = static_cast(f.h); r.ecx = 0; r.edx = 0; // zero bytes: the end + if (!dos(r)) result = error_of(r); + } + kal_fs_seek(f, static_cast(here), KAL_SEEK_SET, nullptr); + return result; +} + +kal_intptr kal_fs_read_at(kal_file f, void* buf, kal_uintptr len, kal_u64 offset) { + kal_u64 here = 0; + if (int e = kal_fs_seek(f, 0, KAL_SEEK_CURRENT, &here)) return -e; + if (offset > 0x7fffffffull) return -kal_err_invalid; + if (int e = kal_fs_seek(f, static_cast(offset), KAL_SEEK_SET, nullptr)) return -e; + kal_uintptr done = 0; + while (done < len) { + const kal_intptr n = read_handle(static_cast(f.h), static_cast(buf) + done, len - done); + if (n < 0) { if (!done) done = static_cast(n); break; } + if (n == 0) break; + done += static_cast(n); + } + kal_fs_seek(f, static_cast(here), KAL_SEEK_SET, nullptr); + return static_cast(done); +} + +kal_intptr kal_fs_write_at(kal_file f, const void* buf, kal_uintptr len, kal_u64 offset) { + if (f.h >= 256 || !g_writable[f.h]) return -kal_err_permission; + kal_u64 here = 0; + if (int e = kal_fs_seek(f, 0, KAL_SEEK_CURRENT, &here)) return -e; + if (offset > 0x7fffffffull) return -kal_err_invalid; + if (int e = kal_fs_seek(f, static_cast(offset), KAL_SEEK_SET, nullptr)) return -e; + const kal_intptr n = write_handle(static_cast(f.h), static_cast(buf), len); + kal_fs_seek(f, static_cast(here), KAL_SEEK_SET, nullptr); + return n; +} + +int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, kal_uintptr flags, kal_u32 wanted, + kal_node_info* out) { + if (!out || out->self_size < 8) return kal_err_invalid; + if (flags & ~KAL_FS_NO_RESOLVE) return kal_err_invalid; + char path[kPath]; + if (int e = join(base, name, len, path)) return e; + if (is_root(path)) { + fill(out, kal_node_directory, wanted, false, 0, false, 0, -1); + return kal_ok; + } + // Find first with every attribute: the entry's own attributes, size, date and time. + if (!find_first(path, 0x37)) { + fill(out, kal_node_absent, wanted, false, 0, false, 0, -1); + return kal_ok; + } + const u8* dta = transfer() + 512; + const int attr = dta[21]; + const bool dir = attr & 0x10; + fill(out, dir ? kal_node_directory : kal_node_file, wanted, !dir, dta_u32(dta, 26), true, + dos_time_ns(dta_u16(dta, 24), dta_u16(dta, 22)), attr); + return kal_ok; +} + +int kal_fs_mkdir(kal_dir base, const char* name, kal_uintptr len) { + char path[kPath]; + if (int e = join(base, name, len, path)) return e; + if (attributes(path) >= 0) return kal_err_exists; + put_name(path, 0); + rm_regs r{}; + r.eax = 0x3900; r.edx = 0; + return dos(r) ? kal_ok : error_of(r); +} + +int kal_fs_remove(kal_dir base, const char* name, kal_uintptr len) { + char path[kPath]; + if (int e = join(base, name, len, path)) return e; + const int a = attributes(path); + if (a < 0) return kal_err_not_found; + put_name(path, 0); + rm_regs r{}; + r.eax = (a & 0x10) ? 0x3a00 : 0x4100; r.edx = 0; + if (dos(r)) return kal_ok; + // A directory that holds entries is refused as access denied, which is what it means here. + if ((a & 0x10) && (r.eax & 0xffff) == 5) return kal_err_not_empty; + return error_of(r); +} + +int kal_fs_rename_flags(kal_dir from, const char* a, kal_uintptr alen, kal_dir to, const char* b, + kal_uintptr blen, kal_uintptr flags) { + if (flags & ~KAL_RENAME_NO_REPLACE) return kal_err_invalid; + char pa[kPath], pb[kPath]; + if (int e = join(from, a, alen, pa)) return e; + if (int e = join(to, b, blen, pb)) return e; + const int ab = attributes(pb); + if (ab >= 0) { + if (flags & KAL_RENAME_NO_REPLACE) return kal_err_exists; + // The rename that replaces: DOS refuses a name that exists, so it is removed first. + if (ab & 0x10) return kal_err_is_directory; + put_name(pb, 0); + rm_regs del{}; + del.eax = 0x4100; del.edx = 0; + if (!dos(del)) return error_of(del); + } + put_name(pa, 0); + put_name(pb, 256); + rm_regs r{}; + r.eax = 0x5600; r.edx = 0; r.edi = 256; + return dos(r) ? kal_ok : error_of(r); +} + +int kal_fs_rename(kal_dir from, const char* a, kal_uintptr alen, kal_dir to, const char* b, + kal_uintptr blen) { + return kal_fs_rename_flags(from, a, alen, to, b, blen, 0); +} + +int kal_fs_file_info(kal_file f, kal_u32 wanted, kal_node_info* out) { + if (!out || out->self_size < 8 || f.h < 5) return kal_err_invalid; + kal_u64 here = 0, end = 0; + if (int e = kal_fs_seek(f, 0, KAL_SEEK_CURRENT, &here)) return e; + kal_fs_seek(f, 0, KAL_SEEK_END, &end); + kal_fs_seek(f, static_cast(here), KAL_SEEK_SET, nullptr); + rm_regs t{}; + t.eax = 0x5700; t.ebx = static_cast(f.h); + const bool timed = dos(t); + fill(out, kal_node_file, wanted, true, end, timed, timed ? dos_time_ns(t.edx & 0xffff, t.ecx & 0xffff) : 0, -1); + return kal_ok; +} + +int kal_fs_set_modified(kal_file f, kal_u64 modified_ns) { + u32 date = 0, time = 0; + if (!ns_dos_time(modified_ns, date, time)) return kal_err_invalid; + rm_regs r{}; + r.eax = 0x5701; r.ebx = static_cast(f.h); r.ecx = time; r.edx = date; + return dos(r) ? kal_ok : error_of(r); +} + +int kal_fs_set_modified_at(kal_dir base, const char* name, kal_uintptr len, kal_u64 modified_ns) { + kal_file f{}; + if (int e = kal_fs_open(base, name, len, KAL_OPEN_READ, &f)) return e; + const int r = kal_fs_set_modified(f, modified_ns); + kal_fs_close_file(f); + return r; +} + +int kal_fs_set_executable_at(kal_dir, const char*, kal_uintptr, int) { return kal_err_not_supported; } +int kal_fs_lock(kal_file, kal_u64, kal_u64, kal_uintptr) { return kal_err_not_supported; } +int kal_fs_unlock(kal_file, kal_u64, kal_u64) { return kal_err_not_supported; } + +int kal_fs_capacity(kal_dir d, kal_u64* total, kal_u64* available) { + const directory* p = dir_of(d); + if (!p) return kal_err_invalid; + char letter = p->path[0]; + if (letter >= 'a' && letter <= 'z') letter = static_cast(letter - 'a' + 'A'); + rm_regs r{}; + r.eax = 0x3600; r.edx = static_cast(letter - 'A' + 1); + if (!dos(r) || (r.eax & 0xffff) == 0xffff) return kal_err_not_found; + const kal_u64 cluster = static_cast(r.eax & 0xffff) * (r.ecx & 0xffff); + if (total) *total = cluster * (r.edx & 0xffff); + if (available) *available = cluster * (r.ebx & 0xffff); + return kal_ok; +} + +int kal_fs_link_create(kal_dir, const char*, kal_uintptr, const char*, kal_uintptr, kal_uintptr) { + return kal_err_not_supported; +} +kal_intptr kal_fs_link_read(kal_dir, const char*, kal_uintptr, char*, kal_uintptr) { + return -kal_err_not_supported; +} +int kal_fs_hard_link(kal_dir, const char*, kal_uintptr, kal_dir, const char*, kal_uintptr) { + return kal_err_not_supported; +} +kal_intptr kal_fs_resolved_name(kal_dir, const char*, kal_uintptr, char*, kal_uintptr) { + return -kal_err_not_supported; +} +int kal_fs_dir_attenuate(kal_dir, const kal_dir_rights*, kal_dir*) { return kal_err_not_supported; } + +int kal_fs_list_begin(kal_dir d, kal_uintptr* iter) { + directory* p = dir_of(d); + if (!p || !iter) return kal_err_invalid; + p->list.active = true; + p->list.first = true; + *iter = 1; + return kal_ok; +} + +int kal_fs_list_next_info(kal_dir d, kal_uintptr* iter, char* name_out, kal_uintptr name_cap, + kal_uintptr* name_len, kal_u32 wanted, kal_node_info* info) { + directory* p = dir_of(d); + if (!p || !iter) return kal_err_invalid; + if (*iter == 0 || !p->list.active) { + *iter = 0; + if (name_len) *name_len = 0; + return kal_ok; + } + for (;;) { + bool found; + if (p->list.first) { + char pattern[kPath]; + u32 n = length(p->path); + copy(pattern, p->path, n); + if (n && pattern[n - 1] != '\\') pattern[n++] = '\\'; + copy(pattern + n, "*.*", 4); + found = find_first(pattern, 0x37); + p->list.first = false; + } else { + rm_regs set{}; + set.eax = 0x1a00; set.edx = 512; + dos(set); + copy(transfer() + 512, p->list.dta, sizeof p->list.dta); + rm_regs r{}; + r.eax = 0x4f00; + found = dos(r); + } + if (!found) { + p->list.active = false; + *iter = 0; + if (name_len) *name_len = 0; + return kal_ok; + } + copy(p->list.dta, transfer() + 512, sizeof p->list.dta); + const u8* dta = p->list.dta; + const char* name = reinterpret_cast(dta + 30); + const u32 n = length(name); + if ((n == 1 && name[0] == '.') || (n == 2 && name[0] == '.' && name[1] == '.')) continue; + if (dta[21] & 0x08) continue; // a volume label is not an entry of the directory + if (name_len) *name_len = n; + if (name_out && name_cap) copy(name_out, name, n < name_cap ? n : static_cast(name_cap)); + const bool dir = dta[21] & 0x10; + if (info) + fill(info, dir ? kal_node_directory : kal_node_file, wanted, !dir, dta_u32(dta, 26), true, + dos_time_ns(dta_u16(dta, 24), dta_u16(dta, 22)), dta[21]); + return kal_ok; + } +} + +int kal_fs_list_next(kal_dir d, kal_uintptr* iter, char* name_out, kal_uintptr name_cap, + kal_uintptr* name_len, int* kind) { + kal_node_info info{}; + info.self_size = sizeof info; + const int r = kal_fs_list_next_info(d, iter, name_out, name_cap, name_len, KAL_INFO_KIND, &info); + if (r == kal_ok && kind) *kind = *iter ? info.kind : kal_node_absent; + return r; +} + +} // extern "C" diff --git a/src/kal.cpp b/src/kal.cpp index 56ac1d9..58c0bdc 100644 --- a/src/kal.cpp +++ b/src/kal.cpp @@ -12,40 +12,44 @@ #include "dos.h" -namespace { - -using namespace okdos; - -constexpr kal_uintptr kStdin = 0, kStdout = 1, kStderr = 2; +namespace okdos { // Writes through the transfer buffer, a piece at a time. -kal_intptr write_handle(u16 handle, const u8* p, kal_uintptr n) { - kal_uintptr done = 0; +long write_handle(u16 handle, const u8* p, unsigned long n) { + unsigned long done = 0; while (done < n) { u32 k = static_cast(n - done); if (k > transfer_size()) k = transfer_size(); copy(transfer(), p + done, k); rm_regs r{}; r.eax = 0x4000; r.ebx = handle; r.ecx = k; r.edx = 0; - if (!dos(r)) return done ? static_cast(done) : -kal_err_io; + if (!dos(r)) return done ? static_cast(done) : -kal_err_io; const u32 wrote = r.eax & 0xffff; done += wrote; if (wrote < k) break; // a full disk, or a device that takes no more } - return static_cast(done); + return static_cast(done); } -kal_intptr read_handle(u16 handle, u8* p, kal_uintptr n) { +long read_handle(u16 handle, u8* p, unsigned long n) { u32 k = static_cast(n); if (k > transfer_size()) k = transfer_size(); rm_regs r{}; r.eax = 0x3f00; r.ebx = handle; r.ecx = k; r.edx = 0; - if (!dos(r)) return -kal_err_io; + if (!dos(r)) return (r.eax & 0xffff) == 5 ? -kal_err_permission : -kal_err_io; const u32 got = r.eax & 0xffff; copy(p, transfer(), got); - return static_cast(got); + return static_cast(got); } +} // namespace okdos + +namespace { + +using namespace okdos; + +constexpr kal_uintptr kStdin = 0, kStdout = 1, kStderr = 2; + // IOCTL 4400h: bit 7 of the device word is "a device, not a file"; bit 0 or 1 the console. bool is_console(u16 handle) { rm_regs r{}; @@ -76,19 +80,31 @@ kal_stream kal_stdin(void) { return kal_stream{kStdin}; } kal_stream kal_stdout(void) { return kal_stream{kStdout}; } kal_stream kal_stderr(void) { return kal_stream{kStderr}; } +// A stream is a DOS handle: the three standard ones, or a file's (openkal.fs), which DOS numbers +// from 5. A file opened for appending writes at its end each time (fs.cpp). kal_intptr kal_stream_write(kal_stream s, const void* buf, kal_uintptr n) { - if (s.h != kStdout && s.h != kStderr) return -kal_err_invalid; + if (s.h == kStdin || s.h == 3 || s.h == 4 || s.h >= 256) return -kal_err_invalid; + if (append_handle(static_cast(s.h))) { + rm_regs r{}; + r.eax = 0x4202; r.ebx = static_cast(s.h); r.ecx = 0; r.edx = 0; + dos(r); + } return write_handle(static_cast(s.h), static_cast(buf), n); } kal_intptr kal_stream_read(kal_stream s, void* buf, kal_uintptr n) { - if (s.h != kStdin) return -kal_err_invalid; - return read_handle(0, static_cast(buf), n); + if (s.h == kStdout || s.h == kStderr || s.h == 3 || s.h == 4 || s.h >= 256) return -kal_err_invalid; + return read_handle(static_cast(s.h), static_cast(buf), n); } -// DOS writes a handle's bytes when the call returns. +// DOS writes a standard handle's bytes when the call returns; a file's are committed (68h). int kal_stream_flush(kal_stream s) { - return s.h <= kStderr ? kal_ok : kal_err_invalid; + if (s.h <= kStderr) return kal_ok; + if (s.h < 5 || s.h >= 256) return kal_err_invalid; + rm_regs r{}; + r.eax = 0x6800; r.ebx = static_cast(s.h); + dos(r); + return kal_ok; } kal_uintptr kal_stream_props(kal_stream s) { diff --git a/src/terminal.cpp b/src/terminal.cpp new file mode 100644 index 0000000..7556c95 --- /dev/null +++ b/src/terminal.cpp @@ -0,0 +1,81 @@ +// openkal.terminal on DOS: the console device's mode (IOCTL 4400h and 4401h) and the display's +// size from the BIOS. +// +// DOS distinguishes one mode of the console, by bit 5 of its device word: cooked, where DOS +// assembles a line, echoes it and reserves Ctrl+C and Ctrl+S, and raw (binary), where each byte +// typed reaches the program as typed and nothing is reserved. The four input positions of openkal +// are therefore one switch here: cooked is KAL_TERM_LINE_EDIT | KAL_TERM_ECHO, raw is +// KAL_TERM_PASS_CONTROL | KAL_TERM_PASS_INPUT, and a requested mode is raw when it asks for no line +// assembly. KAL_TERM_SEQUENCES is reported where ANSI.SYS answers its installation check (int 2Fh, +// 1A00h). The size is the BIOS's: the columns at 0040:004A and the rows less one at 0040:0084. +#include + +#include "dos.h" + +namespace { + +using namespace okdos; + +constexpr u32 kRaw = 0x20; + +// The console's device word, or false for a handle that is not the console. +bool console_word(kal_stream s, u32& word) { + if (s.h >= 256) return false; + rm_regs r{}; + r.eax = 0x4400; r.ebx = static_cast(s.h); + if (!dos(r)) return false; + word = r.edx & 0xffff; + return (word & 0x80) && (word & 0x03); +} + +bool ansi_installed() { + rm_regs r{}; + r.eax = 0x1a00; + real_int(0x2f, r); + return (r.eax & 0xff) == 0xff; +} + +} // namespace + +extern "C" { + +int kal_terminal_get_mode(kal_stream s, kal_uintptr* mode) { + u32 word = 0; + if (!console_word(s, word)) return kal_err_not_supported; + if (!mode) return kal_err_invalid; + kal_uintptr m = (word & kRaw) ? (KAL_TERM_PASS_CONTROL | KAL_TERM_PASS_INPUT) + : (KAL_TERM_LINE_EDIT | KAL_TERM_ECHO); + if (ansi_installed()) m |= KAL_TERM_SEQUENCES; + *mode = m; + return kal_ok; +} + +int kal_terminal_set_mode(kal_stream s, kal_uintptr mode) { + u32 word = 0; + if (!console_word(s, word)) return kal_err_not_supported; + const bool raw = !(mode & KAL_TERM_LINE_EDIT); + if (raw == ((word & kRaw) != 0)) return kal_ok; // in effect already: not written again + rm_regs r{}; + r.eax = 0x4401; r.ebx = static_cast(s.h); + r.edx = (word & 0xff & ~kRaw) | (raw ? kRaw : 0); + return dos(r) ? kal_ok : kal_err_io; +} + +int kal_terminal_size(kal_stream s, kal_uintptr* cols, kal_uintptr* rows) { + u32 word = 0; + if (!console_word(s, word)) return kal_err_not_supported; + const u32 c = *reinterpret_cast(low(0x44a)); + const u32 r = *reinterpret_cast(low(0x484)); + if (c == 0) return kal_err_not_supported; + if (cols) *cols = c; + if (rows) *rows = r ? r + 1 : 25; + return kal_ok; +} + +kal_uintptr kal_terminal_props(kal_stream s) { + u32 word = 0; + if (!console_word(s, word)) return 0; + return KAL_TERM_PROP_MODE | KAL_TERM_PROP_SIZE; +} + +} // extern "C" diff --git a/src/time.cpp b/src/time.cpp index e71826f..a650784 100644 --- a/src/time.cpp +++ b/src/time.cpp @@ -17,15 +17,6 @@ using namespace okdos; constexpr kal_u64 kTicksPerDay = 0x1800B0; constexpr kal_u64 kTickNs = 54925494; -long long days_from_civil(long long y, unsigned m, unsigned d) { - y -= m <= 2; - const long long era = (y >= 0 ? y : y - 399) / 400; - const unsigned yoe = static_cast(y - era * 400); - const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; - const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; - return era * 146097 + static_cast(doe) - 719468; -} - } // namespace extern "C" { From 38e8cbc19c5f00ec7f1130b38cedd8f1f6d62516 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 14 Nov 2026 02:26:06 +0900 Subject: [PATCH 3/5] A processor fault is reported on standard error and ends with 128 and the signal's number DPMI's exception handlers (int 31h, 0203h) for #DE, #UD, #SS, #GP and #PF record the vector, the error code and the faulting EIP, and resume the program on a stack of its own, where the report names the fault and its address and the program ends with 136, 132 or 139. CWSDPMI's own report is a register dump on the screen and status 1, which a program's output and status do not distinguish from exit(1) (measured). The README states the 486 the row is compiled for (DOSBox Staging executes no CMPXCHG8B, measured) and an i386 ok image run on DOS by the core runner carried as a program of the row. --- README.md | 18 +++++++++-- src/dos.h | 3 ++ src/fault.S | 65 +++++++++++++++++++++++++++++++++++++ src/fault.cpp | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/start.cpp | 1 + 5 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 src/fault.S create mode 100644 src/fault.cpp diff --git a/README.md b/README.md index 0cad8b3..d7703c9 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ C library states `c-abi = false` for the row and calls openkal directly. | `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. @@ -101,18 +102,29 @@ DPMI host CWSDPMI is DPMI 0.90 and refuses 0504h (measured), which is the case 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=i586` (the processor -the emulators that run DOS programs model), with emulated thread-local storage +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 the +Pentium's 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). The link is `ld.lld -static +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`. diff --git a/src/dos.h b/src/dos.h index 9db362d..bb71311 100644 --- a/src/dos.h +++ b/src/dos.h @@ -62,6 +62,9 @@ long write_handle(u16 handle, const u8* p, unsigned long n); long read_handle(u16 handle, u8* p, unsigned long n); // Whether a file's handle was opened for appending (fs.cpp): each write is placed at the end. bool append_handle(u16 handle); +// The handlers of the processor's faults (fault.S, fault.cpp): a fault is reported on standard +// error and ends the program with 128 and the signal's number. +void install_faults(); // Days from 1970-01-01 to a civil date (H. Hinnant's algorithm). inline long long days_from_civil(long long y, unsigned m, unsigned d) { diff --git a/src/fault.S b/src/fault.S new file mode 100644 index 0000000..71170ea --- /dev/null +++ b/src/fault.S @@ -0,0 +1,65 @@ +// A fault of the program, reported (openkal-dos 0.1.0). The DPMI host calls a processor-exception +// handler (int 31h, 0203h) on its own stack, with this frame (DPMI 0.9, a 32-bit client): +// +// [esp+00h] the host's return EIP [esp+04h] its CS [esp+08h] the error code +// [esp+0Ch] the faulting EIP [esp+10h] its CS [esp+14h] EFLAGS +// [esp+18h] the faulting ESP [esp+1Ch] its SS +// +// The handler records the vector, the error code and the faulting EIP, and returns to the host with +// the frame changed so that the program resumes at okdos_fault_entry on a stack of its own: the +// report runs as ordinary code, whatever the fault did to the stack it had. DS at the fault is not +// trusted; the data selector is read through CS, whose descriptor has the same base and is +// readable. + .set OKDOS_FAULT_STACK, 16384 + .text + + .macro okdos_vector n + .globl okdos_fault_\n + .type okdos_fault_\n, @function +okdos_fault_\n: + pushl $\n + jmp okdos_fault_common + .endm + + okdos_vector 0 + okdos_vector 6 + okdos_vector 12 + okdos_vector 13 + okdos_vector 14 + +okdos_fault_common: + pushl %eax + pushl %ds + movw %cs:okdos_fault_ds, %ax + movw %ax, %ds + // [esp] DS, +04h EAX, +08h the vector, +0Ch the host's EIP, +10h its CS, +14h the error code, + // +18h the faulting EIP, +1Ch its CS, +20h EFLAGS, +24h ESP, +28h SS + movl 0x08(%esp), %eax + movl %eax, okdos_fault_vector + movl 0x14(%esp), %eax + movl %eax, okdos_fault_error + movl 0x18(%esp), %eax + movl %eax, okdos_fault_eip + movl $okdos_fault_entry, 0x18(%esp) + movl $okdos_fault_stack + OKDOS_FAULT_STACK, 0x24(%esp) + movzwl okdos_fault_ds, %eax + movl %eax, 0x28(%esp) + popl %ds + popl %eax + addl $4, %esp + lret + +okdos_fault_entry: + movw %cs:okdos_fault_ds, %ax + movw %ax, %ds + movw %ax, %es + andl $-16, %esp + call okdos_fault + ud2 + + .bss + .p2align 4 +okdos_fault_stack: + .space OKDOS_FAULT_STACK + + .section .note.GNU-stack, "", @progbits diff --git a/src/fault.cpp b/src/fault.cpp new file mode 100644 index 0000000..864d600 --- /dev/null +++ b/src/fault.cpp @@ -0,0 +1,89 @@ +// A fault of the program, reported as the other implementations of openkal report one: a sentence +// on standard error naming the fault and the instruction's address, and the status a process ended +// by the corresponding signal reports on the systems that have signals (128 and the signal's +// number). Without it, CWSDPMI writes a register dump to the screen and ends the program with +// status 1, which a program's output and status do not distinguish from `exit(1)` (measured). +// +// fault.S holds the handlers the DPMI host calls (int 31h, 0203h); they resume the program at +// okdos_fault on a stack of its own. +#include "dos.h" + +extern "C" { +okdos::u16 okdos_fault_ds; +okdos::u32 okdos_fault_vector, okdos_fault_eip, okdos_fault_error; +void okdos_fault_0(); +void okdos_fault_6(); +void okdos_fault_12(); +void okdos_fault_13(); +void okdos_fault_14(); +} + +namespace okdos { +namespace { + +struct kind { + u32 vector; + void (*handler)(); + const char* what; + int status; // 128 and the signal's number +}; + +const kind kKinds[] = { + {0, okdos_fault_0, "a division by zero (#DE)", 128 + 8}, + {6, okdos_fault_6, "an instruction the processor does not have (#UD)", 128 + 4}, + {12, okdos_fault_12, "a fault of the stack (#SS)", 128 + 11}, + {13, okdos_fault_13, "a general protection fault (#GP)", 128 + 11}, + {14, okdos_fault_14, "an access to memory that is not there (#PF)", 128 + 11}, +}; + +unsigned put(char* out, const char* s) { + unsigned n = 0; + while (s[n]) { out[n] = s[n]; ++n; } + return n; +} + +unsigned hex(char* out, u32 v) { + const char digits[] = "0123456789abcdef"; + out[0] = '0'; out[1] = 'x'; + for (int i = 0; i < 8; ++i) out[2 + i] = digits[(v >> (28 - 4 * i)) & 15]; + return 10; +} + +} // namespace + +void install_faults() { + u16 cs = 0, ds = 0; + __asm__ volatile("movw %%cs, %0\n\tmovw %%ds, %1" : "=r"(cs), "=r"(ds)); + okdos_fault_ds = ds; + for (const kind& k : kKinds) { + u32 ax = 0x0203; + __asm__ volatile("int $0x31" + : "+a"(ax) + : "b"(k.vector), "c"(static_cast(cs)), + "d"(reinterpret_cast(k.handler)) + : "memory", "cc"); + } +} + +} // namespace okdos + +extern "C" [[noreturn]] void okdos_fault() { + using namespace okdos; + const kind* k = &kKinds[0]; + for (const kind& c : kKinds) + if (c.vector == okdos_fault_vector) k = &c; + char text[160]; + unsigned n = put(text, "openkal-dos: fault: "); + n += put(text + n, k->what); + n += put(text + n, " at "); + n += hex(text + n, okdos_fault_eip); + if (okdos_fault_vector >= 12) { + n += put(text + n, " (error "); + n += hex(text + n, okdos_fault_error); + n += put(text + n, ")"); + } + n += put(text + n, "\r\n"); + write_handle(2, reinterpret_cast(text), n); + __asm__ volatile("int $0x21" ::"a"(0x4c00 | (k->status & 0xff))); + for (;;) {} +} diff --git a/src/start.cpp b/src/start.cpp index 4f95e87..3b96bcb 100644 --- a/src/start.cpp +++ b/src/start.cpp @@ -60,6 +60,7 @@ extern "C" [[noreturn]] void okdos_start(const handoff* h) { kal_exit(127); } self().h = h; + install_faults(); record_arguments(h); #ifdef OKDOS_STANDALONE __libc_start_main(main, 0, nullptr, nullptr, nullptr, nullptr); From 25c1113f0d2625a9d8eb1fc3bbec2213806cb228 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 14 Nov 2026 02:28:10 +0900 Subject: [PATCH 4/5] README: the 486's row without naming the Pentium --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d7703c9..fc0590b 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,8 @@ lists, resizes (the extension reads as zero) and removes them, and returns 9 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 the -Pentium's instruction. The row also uses emulated thread-local storage +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 From 5bd9d6af3378a0f8ef2e90d4a0d73ad1793aa151 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sat, 14 Nov 2026 02:36:29 +0900 Subject: [PATCH 5/5] CI: the stub's bytes, okdos on three systems, and the programs under DOSBox Staging; tests/fault The programs job builds the examples and tests/fault for i686-dos-musl with this repository's openkal-dos and runs them with dirun and DOSBox Staging: hello unoptimised and optimised (status 7), files (9), a page fault (139). Measured locally against the platform's working tree: 7, 7, 9 and 139. --- .github/workflows/ci.yml | 133 +++++++++++++++++++++++++++++++++++++++ README.md | 9 +++ tests/fault/mcpp.toml | 12 ++++ tests/fault/src/main.cpp | 10 +++ 4 files changed, 164 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 tests/fault/mcpp.toml create mode 100644 tests/fault/src/main.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..272712e --- /dev/null +++ b/.github/workflows/ci.yml @@ -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' diff --git a/README.md b/README.md index fc0590b..3a30fe7 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,15 @@ 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 diff --git a/tests/fault/mcpp.toml b/tests/fault/mcpp.toml new file mode 100644 index 0000000..b82f15b --- /dev/null +++ b/tests/fault/mcpp.toml @@ -0,0 +1,12 @@ +# A program that writes where no memory is: openkal-dos reports the page fault on standard error and +# the program ends with 139 (128 and SIGSEGV's number), where CWSDPMI alone ends it with 1. +[package] +name = "fault" +version = "0.1.0" + +[targets.fault] +kind = "bin" +main = "src/main.cpp" + +[target.i686-dos-musl] +platform = "openkal-platform@0.28.0" diff --git a/tests/fault/src/main.cpp b/tests/fault/src/main.cpp new file mode 100644 index 0000000..c9fd2a8 --- /dev/null +++ b/tests/fault/src/main.cpp @@ -0,0 +1,10 @@ +#include + +int main() { + std::printf("before the fault\n"); + std::fflush(stdout); + volatile int* nowhere = reinterpret_cast(0xfffffff0u); + *nowhere = 1; + std::printf("after the fault\n"); + return 3; +}