Updated docs.

This commit is contained in:
Scott Duensing 2026-03-20 20:00:05 -05:00
parent 5a1332d024
commit 26c3d7440d
12 changed files with 3216 additions and 2065 deletions

267
README.md
View file

@ -1,50 +1,64 @@
# DVX -- DOS Visual eXecutive
A Windows 3.x-style desktop shell for DOS, built with DJGPP/DPMI. Combines a
windowed GUI compositor, cooperative task switcher, and DXE3 dynamic loading to
create a multitasking desktop environment where applications are `.app` shared
libraries loaded at runtime.
A windowed GUI compositor and desktop shell for DOS, built with
DJGPP/DPMI. DVX combines a Motif-style window manager, dirty-rectangle
compositor, cooperative task switcher, and DXE3 dynamic application
loading into a multitasking desktop environment where applications are
`.app` shared libraries loaded at runtime.
## Components
Targets real and emulated 486+ hardware with VESA VBE 2.0+ linear
framebuffer. No bank switching -- LFB or fail.
```
dvxgui/
dvx/ GUI compositor library -> lib/libdvx.a
tasks/ Cooperative task switcher -> lib/libtasks.a
dvxshell/ Desktop shell -> bin/dvx.exe
apps/ DXE app modules (.app files) -> bin/apps/*/*.app
rs232/ ISR-driven UART serial driver -> lib/librs232.a
packet/ HDLC framing, CRC, Go-Back-N -> lib/libpacket.a
security/ DH key exchange, XTEA-CTR cipher -> lib/libsecurity.a
seclink/ Secure serial link wrapper -> lib/libseclink.a
proxy/ Linux SecLink-to-telnet proxy -> bin/secproxy
termdemo/ Encrypted ANSI terminal demo -> bin/termdemo.exe
```
## Building
## Features
Requires the DJGPP cross-compiler (`i586-pc-msdosdjgpp-gcc`).
- Motif/GEOS-style beveled window chrome with drag, resize, minimize,
maximize, and restore
- Dirty-rectangle compositor -- only changed regions are flushed to video
memory, critical for acceptable frame rates on 486/Pentium hardware
- 32 widget types: buttons, text inputs, list boxes, tree views, list
views, tab controls, sliders, spinners, progress bars, dropdowns,
combo boxes, splitters, scroll panes, ANSI terminal emulator, and more
- Flexbox-style automatic layout engine (VBox/HBox containers with
weighted space distribution)
- Dropdown menus with cascading submenus, checkbox and radio items,
keyboard accelerators, and context menus
- Modal dialogs: message box (OK/Cancel/Yes/No/Retry) and file
open/save with directory navigation and filter dropdown
- 20-color theme system with live preview and INI-based theme files
- Wallpaper support: stretch, tile, or center with bilinear scaling and
16bpp ordered dithering
- Live video mode switching without restart
- Mouse wheel support (CuteMouse Wheel API)
- Screenshots (full screen or per-window) saved as PNG
- Clipboard (copy/cut/paste within DVX)
- Timer widget for periodic callbacks
- Cooperative task switcher for apps that need their own main loop
- DXE3 dynamic application loading with crash recovery (SIGSEGV,
SIGFPE, SIGILL caught and isolated per-app)
- INI-based preferences system with typed read/write accessors
- Encrypted serial networking stack (DH key exchange, XTEA-CTR cipher)
- Platform abstraction layer -- DOS/DJGPP production target, Linux/SDL2
development target
```
make -C dvx # builds lib/libdvx.a
make -C tasks # builds lib/libtasks.a
make -C dvxshell # builds bin/dvx.exe
make -C apps # builds bin/apps/*/*.app
```
Set `DJGPP_PREFIX` in the Makefiles if your toolchain is installed somewhere
other than `~/djgpp/djgpp`.
## Target Hardware
- **CPU**: 486 baseline, Pentium-optimized paths where significant
- **Video**: VESA VBE 2.0+ with linear framebuffer
- **OS**: DOS with DPMI (CWSDPMI or equivalent)
- **Supported depths**: 8, 15, 16, 24, 32 bpp
- **Test platform**: 86Box with PCI video cards
## Architecture
The shell runs as a single DOS executable (`dvx.exe`) that loads
applications dynamically via DJGPP's DXE3 shared library system. Each app
is a `.app` file exporting an `appDescriptor` and `appMain` entry point.
applications dynamically via DJGPP's DXE3 shared library system.
```
+-------------------------------------------------------------------+
| dvx.exe (Task 0) |
| +-------------+ +-----------+ +----------+ +----------------+ |
| +-------------+ +-----------+ +------------+ |
| | shellMain | | shellApp | | shellExport| |
| | (event loop)| | (lifecycle| | (DXE symbol| |
@ -65,48 +79,189 @@ is a `.app` file exporting an `appDescriptor` and `appMain` entry point.
### App Types
**Callback-only** (`hasMainLoop = false`): `appMain` creates windows, registers
callbacks, and returns. The app lives through event callbacks in the shell's
main loop. No dedicated task or stack needed.
**Callback-only** (`hasMainLoop = false`): `appMain` creates windows,
registers callbacks, and returns. The app lives through event callbacks
in the shell's main loop. No dedicated task or stack needed.
**Main-loop** (`hasMainLoop = true`): A cooperative task is created for the app.
`appMain` runs its own loop calling `tsYield()` to share CPU. Used for apps that
need continuous processing (clocks, terminal emulators, games).
**Main-loop** (`hasMainLoop = true`): A cooperative task is created for
the app. `appMain` runs its own loop calling `tsYield()` to share CPU.
Used for apps that need continuous processing (clocks, terminal
emulators, games).
### Crash Recovery
The shell installs signal handlers for SIGSEGV, SIGFPE, and SIGILL. If an app
crashes, the handler `longjmp`s back to the shell's main loop, the crashed app
is force-killed, and the shell continues running. Diagnostic information
(registers, faulting EIP) is logged to `dvx.log`.
The shell installs signal handlers for SIGSEGV, SIGFPE, and SIGILL. If
an app crashes, the handler `longjmp`s back to the shell's main loop,
the crashed app is force-killed, and the shell continues running.
Diagnostic information (registers, faulting EIP) is logged to `dvx.log`.
## Sample Apps
| App | Type | Description |
|-----|------|-------------|
| `progman.app` | Callback | Program Manager: app launcher grid, Task Manager |
| `notepad.app` | Callback | Text editor with file I/O |
| `clock.app` | Main-loop | Digital clock (multi-instance capable) |
| `dvxdemo.app` | Callback | Widget system showcase |
## Directory Structure
## Target Platform
```
dvxgui/
dvx/ GUI compositor library (libdvx.a)
platform/ Platform abstraction (DOS/DJGPP, Linux/SDL2)
widgets/ Widget system (32 types, one file per type)
thirdparty/ stb_image.h, stb_image_write.h
tasks/ Cooperative task switcher (libtasks.a)
thirdparty/ stb_ds.h
dvxshell/ Desktop shell (dvx.exe)
apps/ DXE app modules (.app files)
progman/ Program Manager -- app launcher grid
notepad/ Text editor with file I/O
clock/ Digital clock (multi-instance, main-loop)
dvxdemo/ Widget system showcase / demo app
cpanel/ Control Panel -- themes, wallpaper, video, mouse
imgview/ Image Viewer -- BMP/PNG/JPEG/GIF display
rs232/ ISR-driven UART serial driver (librs232.a)
packet/ HDLC framing, CRC-16, Go-Back-N (libpacket.a)
security/ DH key exchange, XTEA-CTR cipher (libsecurity.a)
seclink/ Secure serial link wrapper (libseclink.a)
proxy/ Linux SecLink-to-telnet proxy (secproxy)
termdemo/ Encrypted ANSI terminal demo (termdemo.exe)
themes/ Color theme files (.thm)
wpaper/ Bundled wallpaper images
bin/ Build output (dvx.exe, apps/, config/)
lib/ Build output (static libraries)
releases/ Release archives
```
## Building
Requires the DJGPP cross-compiler (`i586-pc-msdosdjgpp-gcc`).
```bash
# Build everything (dvx lib, tasks lib, shell, all apps)
make
# Build individual components
make -C dvx # builds lib/libdvx.a
make -C tasks # builds lib/libtasks.a
make -C dvxshell # builds bin/dvx.exe
make -C apps # builds bin/apps/*/*.app
# Clean all build artifacts
make clean
```
Set `DJGPP_PREFIX` in the component Makefiles if your toolchain is
installed somewhere other than `~/djgpp/djgpp`.
- **CPU**: 486 baseline, Pentium-optimized paths where significant
- **Video**: VESA VBE 2.0+ with linear framebuffer
- **OS**: DOS with DPMI (CWSDPMI or equivalent)
- **Test platform**: 86Box
## Deployment
### CD-ROM ISO (86Box)
The primary deployment method is an ISO image mounted as a CD-ROM in
86Box:
```bash
./mkcd.sh
```
This builds everything, then creates an ISO 9660 image with 8.3
filenames at `~/.var/app/net._86box._86Box/data/86Box/dvx.iso`. Mount
it in 86Box as a CD-ROM drive and run `D:\DVX.EXE` (or whatever drive
letter is assigned).
### Floppy Image
For boot floppy setups with CD-ROM drivers:
```bash
mcopy -o -i <floppy.img> bin/dvx.exe ::DVX.EXE
mcopy -s -o -i <floppy.img> bin/apps ::APPS
mcopy -s -o -i <floppy.img> bin/config ::CONFIG
```
The `apps/` directory structure must be preserved on the target -- Program Manager
recursively scans `apps/` for `.app` files at startup.
The `apps/` and `config/` directory structures must be preserved on the
target -- Program Manager recursively scans `apps/` for `.app` files at
startup.
## Documentation
## Configuration
DVX reads its configuration from `CONFIG\DVX.INI` on the target
filesystem. The INI file uses a standard `[section]` / `key = value`
format. Settings are applied at startup and can be changed live from the
Control Panel app.
```ini
[video]
width = 640
height = 480
bpp = 16
[mouse]
wheel = normal
doubleclick = 500
acceleration = medium
[desktop]
wallpaper = C:\DVX\WPAPER\SWOOP.JPG
wallpaperMode = stretch
theme = C:\DVX\THEMES\WIN31.THM
```
### Video Section
- `width`, `height` -- requested resolution (closest VESA mode is used)
- `bpp` -- preferred color depth (8, 15, 16, 24, or 32)
### Mouse Section
- `wheel` -- `normal` or `reversed`
- `doubleclick` -- double-click speed in milliseconds (200--900)
- `acceleration` -- `off`, `low`, `medium`, or `high`
### Desktop Section
- `wallpaper` -- path to wallpaper image (BMP, PNG, JPEG, GIF)
- `wallpaperMode` -- `stretch`, `tile`, or `center`
- `theme` -- path to color theme file (.thm)
## Bundled Applications
| App | File | Type | Description |
|-----|------|------|-------------|
| Program Manager | `progman.app` | Callback | App launcher grid with icons; also provides the Task Manager (Ctrl+Esc) for switching between running apps |
| Notepad | `notepad.app` | Callback | Text editor with File/Edit menus, open/save dialogs, clipboard, and undo |
| Clock | `clock.app` | Main-loop | Digital clock display; multi-instance capable |
| DVX Demo | `dvxdemo.app` | Callback | Widget system showcase demonstrating all 32 widget types |
| Control Panel | `cpanel.app` | Callback | System settings: color themes with live preview, wallpaper selection, video mode switching, mouse configuration |
| Image Viewer | `imgview.app` | Callback | Displays BMP, PNG, JPEG, and GIF images with file dialog |
## Serial / Networking Stack
A layered encrypted serial communications stack for connecting DVX to
remote systems (BBS, etc.) through 86Box's emulated UART:
| Layer | Library | Description |
|-------|---------|-------------|
| rs232 | `librs232.a` | ISR-driven UART driver with FIFO support, automatic UART type detection (8250 through 16750), configurable baud rate |
| packet | `libpacket.a` | HDLC framing with byte stuffing, CRC-16 integrity checks, Go-Back-N sliding window for reliable delivery, 255-byte max payload |
| security | `libsecurity.a` | 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA-CTR stream cipher, XTEA-CTR DRBG random number generator |
| seclink | `libseclink.a` | Convenience wrapper: multiplexed channels (0--127), per-packet encryption flag, bulk send helper |
| proxy | `secproxy` | Linux-side bridge: 86Box emulated serial port <-> secLink <-> telnet BBS; socket shim replaces rs232 API |
## Third-Party Dependencies
All third-party code is vendored as single-header libraries with no
external dependencies:
| Library | Location | Purpose |
|---------|----------|---------|
| stb_image.h | `dvx/thirdparty/` | Image loading (BMP, PNG, JPEG, GIF) |
| stb_image_write.h | `dvx/thirdparty/` | Image writing (PNG export for screenshots) |
| stb_ds.h | `tasks/thirdparty/` | Dynamic array and hash map (used by task manager) |
## Component Documentation
Each component directory has its own README with detailed API reference:

View file

@ -1,9 +1,51 @@
# DVX Shell Applications
DXE3 shared library applications loaded at runtime by the DVX Shell. Each app
is a `.app` file (DXE3 format) placed under the `apps/` directory tree. The
Program Manager scans this directory recursively and displays all discovered
apps.
DXE3 application modules for the DVX Shell. Each app is a `.app` file (DXE3
shared object format) placed in a subdirectory under `apps/`. The Program
Manager scans this directory recursively at startup and displays all discovered
apps in a launcher grid.
## App Contract
Every DXE app must export two symbols and may optionally export a third:
```c
// Required: app metadata
AppDescriptorT appDescriptor = {
.name = "My App",
.hasMainLoop = false,
.multiInstance = false,
.stackSize = SHELL_STACK_DEFAULT,
.priority = TS_PRIORITY_NORMAL
};
// Required: entry point -- called once by the shell after dlopen
int32_t appMain(DxeAppContextT *ctx);
// Optional: graceful shutdown hook -- called before force-kill
void appShutdown(void);
```
`appMain` receives a `DxeAppContextT` with:
| Field | Type | Description |
|-------|------|-------------|
| `shellCtx` | `AppContextT *` | The shell's GUI context -- pass to all `dvx*`/`wgt*` calls |
| `appId` | `int32_t` | This app's unique ID (1-based slot index; 0 = shell) |
| `appDir` | `char[260]` | Directory containing the `.app` file (for relative resource paths) |
| `configDir` | `char[260]` | Writable config directory (`CONFIG/<apppath>/`) |
Return 0 from `appMain` on success, non-zero on failure (shell will unload).
### AppDescriptorT Fields
| Field | Type | Description |
|-------|------|-------------|
| `name` | `char[64]` | Display name shown in Task Manager and title bars |
| `hasMainLoop` | `bool` | `false` = callback-only (runs in task 0); `true` = gets own cooperative task |
| `multiInstance` | `bool` | `true` = allow multiple instances via temp file copy |
| `stackSize` | `int32_t` | Task stack in bytes; `SHELL_STACK_DEFAULT` (0) for the default |
| `priority` | `int32_t` | `TS_PRIORITY_LOW`, `TS_PRIORITY_NORMAL`, or `TS_PRIORITY_HIGH` |
## Building
@ -12,17 +54,237 @@ make # builds all .app files into ../bin/apps/<name>/
make clean # removes objects and binaries
```
Requires `lib/libdvx.a`, `lib/libtasks.a`, and the DXE3 tools (`dxe3gen`)
from the DJGPP toolchain.
Each app is compiled to an object file with the DJGPP cross-compiler, then
packaged into a `.app` via `dxe3gen`:
## Applications
```makefile
$(BINDIR)/myapp/myapp.app: $(OBJDIR)/myapp.o | $(BINDIR)/myapp
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
```
| App | File | Type | Description |
|-----|------|------|-------------|
| Program Manager | `progman/progman.c` | Callback | Desktop app: app launcher grid, Task Manager (Ctrl+Esc), window management |
| Notepad | `notepad/notepad.c` | Callback | Text editor with file I/O, dirty tracking via hash |
| Clock | `clock/clock.c` | Main-loop | Digital clock, multi-instance capable |
| DVX Demo | `dvxdemo/dvxdemo.c` | Callback | Widget system showcase with all widget types |
- `-E _appDescriptor` and `-E _appMain` export the required symbols (COFF
underscore prefix).
- `-E _appShutdown` is added for apps that export a shutdown hook (e.g.,
Clock).
- `-U` marks all other symbols as unresolved imports to be resolved from the
shell's export table at dlopen time.
Requires `lib/libdvx.a`, `lib/libtasks.a`, and the DXE3 tools from the DJGPP
toolchain.
## Directory Structure
```
apps/
Makefile -- build rules for all apps
README.md -- this file
progman/
progman.c -- Program Manager (desktop shell)
notepad/
notepad.c -- text editor
clock/
clock.c -- digital clock
cpanel/
cpanel.c -- Control Panel (system settings)
imgview/
imgview.c -- image viewer
dvxdemo/
dvxdemo.c -- widget showcase
*.bmp -- toolbar icons and sample images
```
Each app lives in its own subdirectory. The subdirectory name becomes the
output path under `bin/apps/` (e.g., `bin/apps/progman/progman.app`).
## Bundled Applications
### Program Manager (`progman/`)
The desktop shell and default app launcher. Loaded automatically by the shell
at startup as the "desktop app" -- closing it prompts to exit the entire DVX
Shell.
- **App launcher grid**: scans `apps/` recursively for `.app` files (skipping
itself), displays them as a grid of buttons. Click or double-click to
launch.
- **Menu bar**: File (Run..., Exit Shell), Options (Minimize on Run), Window
(Cascade, Tile, Tile H, Tile V), Help (About, System Information, Task
Manager).
- **Minimize on Run**: optional preference -- when enabled, Program Manager
minimizes itself after launching an app, getting out of the way.
- **Status bar**: shows the count of running applications, updated in
real-time via `shellRegisterDesktopUpdate()`.
- **System Information**: opens a read-only text area showing CPU, memory,
drive, and video details gathered by the platform layer.
- **Preferences**: saved to `CONFIG/PROGMAN/progman.ini` via the standard
prefs system. Currently stores the "Minimize on Run" setting.
Type: callback-only. Single instance.
### Notepad (`notepad/`)
A basic text editor with file I/O and dirty-change tracking.
- **TextArea widget**: handles all editing -- keyboard input, cursor movement,
selection, scrolling, word wrap, copy/paste, undo (Ctrl+Z). Notepad only
wires up menus and file I/O around it.
- **File menu**: New, Open, Save, Save As, Exit.
- **Edit menu**: Cut, Copy, Paste, Select All (Ctrl+X/C/V/A).
- **CR/LF handling**: files are opened in binary mode to avoid DJGPP's
translation. `platformStripLineEndings()` normalizes on load;
`platformLineEnding()` writes platform-native line endings on save.
- **Dirty tracking**: uses djb2-xor hash of the text content. Cheap detection
without storing a full shadow buffer. Prompts "Save changes?" on close/new/
open if dirty.
- **32 KB text buffer**: keeps memory bounded on DOS. Larger files are
silently truncated on load.
- **Multi-instance**: each instance gets its own DXE code+data via temp file
copy. Window positions are offset +20px so instances cascade naturally.
Type: callback-only. Multi-instance.
### Clock (`clock/`)
A digital clock displaying 12-hour time and date, centered in a small
non-resizable window.
- **Main-loop app**: polls `time()` each iteration, repaints when the second
changes, then calls `tsYield()`. CPU usage is near zero because the check
is cheap and yields immediately when nothing changes.
- **Raw paint callback**: renders directly into the window's content buffer
using `rectFill` and `drawText` -- no widget tree. Demonstrates the
lower-level alternative to the widget system for custom rendering.
- **Shutdown hook**: exports `appShutdown()` so the shell can signal a clean
exit when force-killing via Task Manager or during shell shutdown.
- **Low priority**: uses `TS_PRIORITY_LOW` since clock updates are cosmetic
and should never preempt interactive apps.
Type: main-loop. Multi-instance.
### Control Panel (`cpanel/`)
System configuration with four tabs, all changes previewing live. OK saves to
`CONFIG/DVX.INI`; Cancel reverts to the state captured when the panel opened.
**Mouse tab:**
- Scroll wheel direction (Normal / Reversed) via dropdown.
- Double-click speed (200-900 ms) via slider with numeric label and test
button.
- Mouse acceleration (Off / Low / Medium / High) via dropdown.
- All changes apply immediately via `dvxSetMouseConfig()`.
**Colors tab:**
- List of all 20 system colors (`ColorCountE` entries from `dvxColorName`).
- RGB sliders (0-255) for the selected color, with numeric labels and a
canvas swatch preview.
- Changes apply live via `dvxSetColor()` -- the entire desktop updates in
real time.
- **Themes**: dropdown of `.thm` files from `CONFIG/THEMES/`, with Apply,
Load..., Save As..., and Reset buttons. Themes are loaded/saved via
`dvxLoadTheme()`/`dvxSaveTheme()`.
- Reset restores the compiled-in default color scheme.
**Desktop tab:**
- Wallpaper list: scans `CONFIG/WPAPER/` for BMP/JPG/PNG files.
- Apply, Browse..., and Clear buttons.
- Mode dropdown: Stretch, Tile, Center. Changes apply live via
`dvxSetWallpaperMode()`.
**Video tab:**
- List of all enumerated VESA modes with human-readable depth names (e.g.,
"800x600 65 thousand colors").
- Apply Mode button or double-click to switch.
- **10-second confirmation dialog**: after switching, a modal "Keep this
mode?" dialog counts down. If the user clicks Yes, the mode is kept.
Clicking No, closing the dialog, or letting the timer expire reverts to
the previous mode. Prevents being stuck in an unsupported mode.
Type: callback-only. Single instance.
### Image Viewer (`imgview/`)
Displays BMP, PNG, JPG, and GIF images loaded via stb_image.
- **Bilinear scaling**: images are scaled to fit the window while preserving
aspect ratio. The scaler converts from RGB to the native pixel format
(8/16/32 bpp) during the scale pass.
- **Deferred resize**: during a window drag-resize, the old scaled image is
shown. The expensive bilinear rescale only runs after the drag ends
(`sAc->stack.resizeWindow < 0`), avoiding per-frame scaling lag.
- **Responsive scaling**: for large images, `dvxUpdate()` is called every 32
scanlines during the scale loop to keep the UI responsive.
- **File menu**: Open (Ctrl+O), Close. Keyboard accelerator table registered.
- **Multi-instance**: multiple viewers can be open simultaneously, each with
its own image.
- **Raw paint callback**: renders directly into the content buffer with a dark
gray background and centered blit of the scaled image.
Type: callback-only. Multi-instance.
### DVX Demo (`dvxdemo/`)
A comprehensive widget showcase and integration test. Opens several windows
demonstrating the full DVX widget system:
- **Main window**: three raw-paint windows -- text rendering with full menu
bar/accelerators/context menu, vertical gradient, and checkerboard pattern
with scrollbars.
- **Widget Demo window**: form pattern with labeled inputs (text, password,
masked phone number), checkboxes, radio groups, single and multi-select
list boxes with context menus and drag reorder.
- **Advanced Widgets window**: nine tab pages covering every widget type --
dropdown, combo box, progress bar (horizontal and vertical), slider,
spinner, tree view (with drag reorder), multi-column list view (with
multi-select and drag reorder), scroll pane, toolbar (with image buttons
and text fallback), image from file, text area, canvas (with mouse
drawing), splitter (nested horizontal+vertical for explorer-style layout),
and a disabled-state comparison of all widget types.
- **ANSI Terminal window**: terminal emulator widget with sample output
demonstrating bold, reverse, blink, all 16 colors, background colors,
CP437 box-drawing characters, and 500-line scrollback.
Type: callback-only. Single instance.
## App Preferences
Apps that need persistent settings use the shell's config directory system:
```c
// In appMain:
shellEnsureConfigDir(ctx); // create CONFIG/<apppath>/ if needed
char path[260];
shellConfigPath(ctx, "settings.ini", path, sizeof(path));
prefsLoad(path);
// Read/write:
int32_t val = prefsGetInt("section", "key", defaultVal);
prefsSetInt("section", "key", newVal);
prefsSave();
```
The preferences system handles INI file format with `[section]` headers and
`key=value` pairs. Missing files or keys silently return defaults.
## Event Model
DVX apps receive events through two mechanisms:
**Widget callbacks** (high-level):
- `onClick`, `onDblClick`, `onChange` on individual widgets.
- The widget system handles focus, tab order, mouse hit testing, keyboard
dispatch, and repainting automatically.
- Used by most apps for standard UI (buttons, inputs, lists, sliders, etc.).
**Window callbacks** (low-level):
- `onPaint(win, dirtyRect)` -- render directly into the window's content
buffer. Used by Clock, Image Viewer, and the DVX Demo paint windows.
- `onClose(win)` -- window close requested (close gadget, Alt+F4).
- `onResize(win, contentW, contentH)` -- window was resized.
- `onMenu(win, menuId)` -- menu item selected or keyboard accelerator fired.
Both mechanisms can be mixed in the same app. For example, DVX Demo uses
widgets in some windows and raw paint callbacks in others.
## Writing a New App
@ -86,6 +348,11 @@ AppDescriptorT appDescriptor = {
static bool sQuit = false;
static void onClose(WindowT *win) {
(void)win;
sQuit = true;
}
void appShutdown(void) {
sQuit = true;
}
@ -111,16 +378,25 @@ int32_t appMain(DxeAppContextT *ctx) {
### Adding to the build
Add your app directory and source to `apps/Makefile`. Each app is compiled to
an object file, then linked into a `.app` via `dxe3gen`:
Add your app directory and source to `apps/Makefile`:
```makefile
$(BIN_DIR)/myapp.app: $(OBJ_DIR)/myapp/myapp.o
APPS = ... myapp
myapp: $(BINDIR)/myapp/myapp.app
$(BINDIR)/myapp/myapp.app: $(OBJDIR)/myapp.o | $(BINDIR)/myapp
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
$(OBJDIR)/myapp.o: myapp/myapp.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(BINDIR)/myapp:
mkdir -p $(BINDIR)/myapp
```
The `-E` flags export the required symbols. `-U` marks unresolved symbols as
imports to be resolved from the shell's export table at load time.
Add `-E _appShutdown` to the `dxe3gen` line if the app exports a shutdown
hook.
## App Guidelines
@ -128,14 +404,22 @@ imports to be resolved from the shell's export table at load time.
`SHELL_STACK_DEFAULT`.
- Use `ctx->shellCtx` (the `AppContextT *`) for all DVX API calls.
- Callback-only apps must destroy their own windows in `onClose` via
`dvxDestroyWindow()`. The shell detects the last window closing and
reaps the app.
`dvxDestroyWindow()`. The shell detects the last window closing and reaps
the app automatically.
- Main-loop apps must call `tsYield()` regularly. A task that never yields
blocks the entire system.
- Use file-scoped `static` variables for app state. Each DXE has its own data
segment, so there is no collision between apps.
- Set `multiInstance = true` in the descriptor if the app can safely run
multiple copies simultaneously.
- Avoid `static inline` functions in shared headers. Code inlined into the DXE
binary cannot be updated without recompiling the app. Use macros for trivial
expressions or regular functions exported through the shell's DXE table.
blocks the entire system (cooperative multitasking -- no preemption).
- Export `appShutdown()` for main-loop apps so the shell can signal a clean
exit on force-kill or shell shutdown.
- Use file-scoped `static` variables for app state. Each DXE has its own
data segment, so there is no collision between apps even with identical
variable names.
- Set `multiInstance = true` only if the app can safely run multiple copies
simultaneously. Each instance gets independent globals and statics.
- Avoid `static inline` functions in shared headers. Code inlined into the
DXE binary cannot be updated without recompiling the app. Use macros for
trivial expressions or regular functions exported through the shell's DXE
table.
- Use `ctx->appDir` for loading app-relative resources (icons, data files).
The working directory is shared by all apps and belongs to the shell.
- Use `shellEnsureConfigDir()` + `shellConfigPath()` for persistent settings.
Never write to the app's own directory -- use `CONFIG/<apppath>/` instead.

View file

@ -878,7 +878,9 @@ static void restoreSnapshot(void) {
dvxChangeVideoMode(sAc, sSavedVideoW, sSavedVideoH, sSavedVideoBpp);
}
// Restore wallpaper mode and image
// Restore wallpaper only if path or mode changed
if (strcmp(sAc->wallpaperPath, sSavedWallpaperPath) != 0 ||
sAc->wallpaperMode != sSavedWpMode) {
sAc->wallpaperMode = sSavedWpMode;
if (sSavedWallpaperPath[0]) {
@ -887,6 +889,7 @@ static void restoreSnapshot(void) {
dvxSetWallpaper(sAc, NULL);
}
}
}
// ============================================================

File diff suppressed because it is too large Load diff

View file

@ -1,140 +1,362 @@
# DVX Shell
Windows 3.x-style desktop shell for DOS. Loads applications as DXE3 shared
libraries and includes crash
recovery so one bad app doesn't take down the system.
## Building
```
make # builds ../bin/dvx.exe
make clean # removes objects and binary
```
Requires `lib/libdvx.a` and `lib/libtasks.a` to be built first.
The DVX Shell (`dvx.exe`) is the host process for the DVX desktop environment.
It initializes the GUI subsystem, loads DXE3 application modules at runtime,
runs the cooperative main loop, and provides crash recovery so a faulting app
does not take down the entire system. Think of it as the Windows 3.1 Program
Manager and kernel combined into one executable.
## Files
| File | Purpose |
|------|---------|
| `shellMain.c` | Entry point, main loop, crash recovery, logging |
| `shellApp.c` | App loading (dlopen), lifecycle, reaping, resource tracking |
| `shellApp.h` | ShellAppT, AppDescriptorT, AppStateE, DxeAppContextT, shell API |
| `shellExport.c` | DXE export table and wrapper functions |
| `Makefile` | Build rules, links `-ldvx -ltasks -ldxe -lm` |
| `shellMain.c` | Entry point, main loop, crash recovery, logging, desktop update callbacks |
| `shellApp.c` | App loading (dlopen/dlsym), lifecycle state machine, reaping, resource tracking, config dirs |
| `shellApp.h` | `ShellAppT`, `AppDescriptorT`, `AppStateE`, `DxeAppContextT`, public shell API |
| `shellExport.c` | DXE export table (400+ symbols), wrapper functions for resource tracking |
| `shellInfo.c` | System information gathering (delegates to platform layer), caches result |
| `shellInfo.h` | `shellInfoInit()`, `shellGetSystemInfo()` |
| `shellTaskMgr.c` | Task Manager window -- list view, Switch To / End Task / Run buttons |
| `shellTaskMgr.h` | `shellTaskMgrOpen()`, `shellTaskMgrRefresh()` |
| `Makefile` | Cross-compile rules, links `-ldvx -ltasks -lm` |
## Shell Main Loop
## Building
Each iteration of the main loop:
1. `dvxUpdate()` -- process input events, dispatch callbacks, composite dirty rects
2. `tsYield()` -- give CPU time to main-loop app tasks
3. `shellReapApps()` -- clean up apps that terminated this frame
4. `desktopUpdate()` -- notify the desktop app if anything changed
An idle callback (`idleYield`) yields to app tasks during quiet periods when
there are no events or dirty rects to process.
## DXE App Contract
Every `.app` file must export these symbols:
```c
// Required: app metadata
AppDescriptorT appDescriptor = {
.name = "My App",
.hasMainLoop = false,
.multiInstance = false,
.stackSize = SHELL_STACK_DEFAULT,
.priority = TS_PRIORITY_NORMAL
};
// Required: entry point
int32_t appMain(DxeAppContextT *ctx);
// Optional: graceful shutdown hook
void appShutdown(void);
```
make # builds ../bin/dvx.exe (also builds libdvx.a, libtasks.a)
make clean # removes objects and binary
```
### AppDescriptorT Fields
Requires the DJGPP cross-compiler toolchain and the DXE3 tools (`dxe3gen`).
| Field | Type | Description |
|-------|------|-------------|
| `name` | `char[64]` | Display name shown in Task Manager |
| `hasMainLoop` | `bool` | `true` = gets its own cooperative task; `false` = callback-only |
| `multiInstance` | `bool` | `true` = allow multiple instances via temp file copy |
| `stackSize` | `int32_t` | Task stack size (`SHELL_STACK_DEFAULT` for 8 KB default) |
| `priority` | `int32_t` | Task priority (`TS_PRIORITY_LOW`/`NORMAL`/`HIGH`) |
## Startup Sequence
### DxeAppContextT
`main()` in `shellMain.c` performs initialization in this order:
Passed to `appMain()`:
1. **Change to exe directory** -- resolve the directory containing `dvx.exe`
via `argv[0]` and `platformChdir()` so that relative paths (`CONFIG/`,
`APPS/`, etc.) work regardless of where the user launched from.
2. **Truncate log** -- open `dvx.log` for write to clear it, then close.
All subsequent writes use append-per-write (the file is never held open).
3. **Load preferences** -- `prefsLoad("CONFIG/DVX.INI")`. Missing file or
keys silently fall back to compiled-in defaults.
4. **dvxInit** -- initialize VESA video (LFB), backbuffer, compositor, window
manager, font, cursor, input subsystems. Reads video width/height/bpp from
preferences (default 640x480x16).
5. **Mouse config** -- read wheel direction, double-click speed, acceleration
from `[mouse]` section and call `dvxSetMouseConfig()`.
6. **Color scheme** -- read `[colors]` section (20 RGB triplets), apply via
`dvxApplyColorScheme()`.
7. **Wallpaper** -- read `[desktop]` section for wallpaper path and mode
(stretch/tile/center), load via `dvxSetWallpaper()`.
8. **Video mode log** -- enumerate all available VESA modes to `dvx.log`.
9. **Task system** -- `tsInit()`, set shell task (task 0) to
`TS_PRIORITY_HIGH` so the UI stays responsive under load.
10. **System info** -- `shellInfoInit()` gathers CPU, memory, drive info via
the platform layer and logs it.
11. **DXE exports** -- `shellExportInit()` calls `dlregsym()` to register the
export table. Must happen before any `dlopen()`.
12. **App slot table** -- `shellAppInit()` zeroes the 32-slot fixed array.
13. **Idle/hotkey callbacks** -- wire up `idleYield`, `ctrlEscHandler`,
`titleChangeHandler` on the `AppContextT`.
14. **Desktop app** -- `shellLoadApp(ctx, "apps/progman/progman.app")`. If
this fails, the shell exits.
15. **Crash handlers** -- `installCrashHandler()` registers signal handlers
for SIGSEGV, SIGFPE, SIGILL. Installed last so initialization crashes
get the default DJGPP abort-with-register-dump instead of our recovery
path.
| Field | Type | Description |
|-------|------|-------------|
| `shellCtx` | `AppContextT *` | The shell's GUI context for creating windows, drawing, etc. |
| `appId` | `int32_t` | This app's unique ID (1-based slot index) |
| `appDir` | `char[260]` | Directory containing the `.app` file for relative resource paths |
## Main Loop
## App Types
The main loop runs until `dvxQuit()` sets `ctx->running = false`:
**Callback-only** (`hasMainLoop = false`):
- `appMain` called in shell's task 0, creates windows, registers callbacks, returns 0
- App lives through event callbacks dispatched by `dvxUpdate()`
- Lifecycle ends when the last window is closed
```
while (ctx->running) {
dvxUpdate(ctx); // (1)
tsYield(); // (2)
shellReapApps(ctx); // (3)
shellDesktopUpdate(); // (4)
}
```
**Main-loop** (`hasMainLoop = true`):
- Shell creates a cooperative task via `tsCreate()`
- `appMain` runs in that task with its own loop calling `tsYield()`
- Lifecycle ends when `appMain` returns
## Multi-Instance Support
DXE3's `dlopen` is reference-counted per path: loading the same `.app` twice
returns the same handle, sharing all global/static state. For apps that support
multiple instances (`multiInstance = true`), the shell copies the `.app` to a
temp file before loading, giving each instance independent code and data. The
temp file is cleaned up when the app terminates.
Apps that don't support multiple instances (`multiInstance = false`, the default)
are blocked from loading a second time with an error message.
Temp file paths use the `TEMP` or `TMP` environment variable if set, falling
back to the current directory.
## Resource Tracking
The shell tracks which app owns which windows via `sCurrentAppId`, a global
set before calling any app code. The shell's `dvxCreateWindow` wrapper stamps
`win->appId` with the current app ID. On termination, the shell destroys all
windows belonging to the app.
1. **dvxUpdate** -- processes mouse/keyboard input, dispatches widget and
window callbacks, composites dirty rects, flushes to the LFB. This is the
shell's primary job. When idle (no events, no dirty rects), dvxUpdate
calls the registered `idleCallback` which yields to app tasks.
2. **tsYield** -- explicit yield to give app tasks CPU time even during busy
frames with many repaints. Without this, a flurry of mouse-move events
could starve app tasks because dvxUpdate would keep finding work.
3. **shellReapApps** -- scans the 32-slot app table for apps in
`AppStateTerminatingE`. Calls the shutdown hook (if present), destroys
all windows owned by the app, kills the task, closes the DXE handle,
and cleans up temp files. Returns true if anything was reaped.
4. **shellDesktopUpdate** -- if apps were reaped, iterates the registered
desktop update callback list so listeners (Program Manager, Task Manager)
can refresh their UI.
## Crash Recovery
Signal handlers for SIGSEGV, SIGFPE, and SIGILL `longjmp` back to the shell's
main loop. The scheduler is fixed via `tsRecoverToMain()`, the crashed app is
force-killed, and a diagnostic message is displayed. Register state and app
identity are logged to `dvx.log`.
The shell provides Windows 3.1-style fault tolerance using `setjmp`/`longjmp`:
1. `installCrashHandler()` registers `crashHandler` for SIGSEGV, SIGFPE,
SIGILL.
2. `setjmp(sCrashJmp)` in `main()` establishes the recovery point.
3. If a signal fires (in any task), `crashHandler` logs the crash details
(signal, app name, full register dump from `__djgpp_exception_state_ptr`),
re-installs the handler (DJGPP uses SysV semantics -- handler resets to
`SIG_DFL` after each delivery), then `longjmp(sCrashJmp, 1)`.
4. `longjmp` restores the main task's stack frame to the `setjmp` point.
This is safe because cooperative switching means the main task's stack
is always intact -- it was cleanly suspended at a yield point.
5. `tsRecoverToMain()` fixes the task scheduler's `currentIdx` so it points
back to task 0 instead of the crashed task.
6. The crashed app is force-killed via `shellForceKillApp()`, and a message
box is displayed: "'AppName' has caused a fault and will be terminated."
7. The desktop update callbacks are notified so the UI refreshes.
The register dump logged includes EIP, CS, all GPRs (EAX-EDI), segment
registers, and EFLAGS -- invaluable for post-mortem debugging.
## App Lifecycle
### DXE3 Loading
DXE3 is DJGPP's dynamic linking mechanism. Each `.app` file is a DXE3 shared
object. The shell resolves symbols from the export table registered via
`dlregsym()`. The load sequence in `shellLoadApp()`:
1. Allocate a slot from the 32-entry fixed array (slot 0 is the shell).
2. Check if this DXE path is already loaded (`findLoadedPath`).
- If the existing app's descriptor says `multiInstance = true`, copy
the `.app` to a unique temp file (using `TEMP`/`TMP` env var) so
`dlopen` gets an independent code+data image.
- If `multiInstance = false`, block with an error message.
3. `dlopen(loadPath, RTLD_GLOBAL)` -- load the DXE.
4. `dlsym(handle, "_appDescriptor")` -- resolve the metadata struct.
5. `dlsym(handle, "_appMain")` -- resolve the entry point.
6. `dlsym(handle, "_appShutdown")` -- resolve the optional shutdown hook.
7. Fill in the `ShellAppT` slot (name, path, handle, state, etc.).
8. Derive `appDir` (directory containing the `.app` file) and `configDir`
(`CONFIG/<apppath>/` -- mirrors the app's directory structure under
`CONFIG/`).
9. Set `sCurrentAppId` to the slot index.
10. Launch:
- **Callback-only** (`hasMainLoop = false`): call `entryFn()` directly
in task 0. The app creates windows, registers callbacks, and returns.
- **Main-loop** (`hasMainLoop = true`): call `tsCreate()` to make a
cooperative task with the descriptor's stack size and priority. The
task wrapper sets `sCurrentAppId`, calls `entryFn()`, and marks the
app `AppStateTerminatingE` when it returns.
11. Reset `sCurrentAppId` to 0. Set state to `AppStateRunningE`. Notify
desktop update callbacks.
### AppDescriptorT
Every DXE app exports a global `AppDescriptorT`:
| Field | Type | Description |
|-------|------|-------------|
| `name` | `char[64]` | Display name (Task Manager, title bars) |
| `hasMainLoop` | `bool` | `true` = dedicated cooperative task; `false` = callback-only |
| `multiInstance` | `bool` | `true` = allow multiple simultaneous instances via temp file copy |
| `stackSize` | `int32_t` | Task stack in bytes (`SHELL_STACK_DEFAULT` = use default) |
| `priority` | `int32_t` | `TS_PRIORITY_LOW` / `TS_PRIORITY_NORMAL` / `TS_PRIORITY_HIGH` |
### Callback vs Main-Loop Apps
**Callback-only** (`hasMainLoop = false`):
- `appMain()` runs in the shell's task 0, creates windows, registers event
callbacks, and returns immediately.
- All subsequent work happens through callbacks dispatched by `dvxUpdate()`.
- Lifecycle ends when the last window is closed (detected by the
`shellWrapDestroyWindow` wrapper).
- No task stack allocated -- simpler and cheaper.
- Examples: Program Manager, Notepad, Control Panel, DVX Demo, Image Viewer.
**Main-loop** (`hasMainLoop = true`):
- A cooperative task is created via `tsCreate()`.
- `appMain()` runs in that task with its own loop calling `tsYield()`.
- Needed when the app has ongoing work that cannot be expressed purely as
event callbacks (polling, computation, animation).
- Lifecycle ends when `appMain()` returns.
- Example: Clock (polls `time()` each second).
### Multi-Instance Support
DXE3's `dlopen` is reference-counted per path -- loading the same `.app`
twice returns the same handle, sharing all globals and statics. For apps
that set `multiInstance = true`, the shell copies the `.app` to a temp file
(e.g., `C:\TEMP\_dvx02.app`) before `dlopen`, giving each instance its own
code and data segment. Temp files are cleaned up on app termination.
### App State Machine
```
Free -> Loaded -> Running -> Terminating -> Free
```
- **Free**: slot available.
- **Loaded**: DXE loaded, entry point not yet called (transient).
- **Running**: entry point called, app is active.
- **Terminating**: app's task returned or last window closed; awaiting reap.
## Resource Tracking
`sCurrentAppId` is a global set before calling any app code (entry, shutdown,
callbacks). The shell's `dvxCreateWindow` wrapper stamps every new window with
`win->appId = sCurrentAppId`. This is how the shell knows which windows belong
to which app, enabling:
- Per-app window cleanup on crash or termination (walk the window stack,
destroy all windows with matching `appId`).
- The last-window-closes-app rule for callback-only apps: when
`shellWrapDestroyWindow` detects that a callback-only app has no remaining
windows, it marks the app `AppStateTerminatingE`.
`sCurrentAppId` is a simple global (not thread-local) because cooperative
multitasking means only one task runs at a time.
## Task Manager
`shellTaskMgr.c` implements a shell-level Task Manager accessible via
**Ctrl+Esc** regardless of which app is focused or whether the desktop app
is running. It is owned by the shell (appId = 0), not by any DXE app.
Features:
- **ListView** with 5 columns: Name, Title, File, Type (Task/Callback), Status.
- **Switch To** -- find the app's topmost window, restore if minimized, raise
and focus it.
- **End Task** -- force-kill the selected app via `shellForceKillApp()`.
- **Run...** -- open a file dialog filtered to `*.app`, load the selected file.
- **Status bar** -- shows running app count and memory usage (total/free MB).
- Registers with `shellRegisterDesktopUpdate` so the list auto-refreshes when
apps load, terminate, or crash.
## Desktop Update Callbacks
`shellRegisterDesktopUpdate(fn)` adds a function pointer to a dynamic array
(managed via stb_ds `arrput`/`arrdel`). `shellDesktopUpdate()` iterates the
array and calls each registered function.
This is the mechanism by which the shell notifies interested parties (Program
Manager status bar, Task Manager list) when app state changes -- without
polling. Multiple listeners are supported. Listeners should call
`shellUnregisterDesktopUpdate()` before they are destroyed.
## App Config Storage
Each app gets a dedicated writable config directory under `CONFIG/`. The path
mirrors the app's location under `APPS/`:
```
App path: APPS/PROGMAN/progman.app
Config dir: CONFIG/PROGMAN/
```
API:
- `shellEnsureConfigDir(ctx)` -- create the directory tree via
`platformMkdirRecursive()`. Returns 0 on success.
- `shellConfigPath(ctx, "settings.ini", buf, sizeof(buf))` -- build a full
path to a file in the config directory.
Apps use the standard preferences system (`prefsLoad`/`prefsSave`) pointed at
their config directory for persistent settings.
## DXE Export Table
The shell registers a symbol export table via `dlregsym()` before loading any
apps. Most symbols (all `dvx*`, `wgt*`, `ts*`, drawing functions, and required
libc functions) are exported directly. `dvxCreateWindow` and `dvxDestroyWindow`
are exported as wrappers that add resource tracking.
`shellExport.c` contains the ABI contract between the shell and apps. Three
categories of exports:
## Shell API
1. **Wrapped functions** (3): `dvxCreateWindow`, `dvxCreateWindowCentered`,
`dvxDestroyWindow`. These are intercepted to stamp `win->appId` for
resource ownership tracking. Apps see them under their original names --
the wrapping is transparent.
2. **Direct exports** (200+): all other `dvx*`, `wgt*`, `wm*`, `ts*`,
drawing, preferences, platform, and shell API functions. Safe to call
without interception.
3. **libc / libm / runtime exports** (200+): DXE3 modules are relocatable
objects, not fully linked executables. Every C library function a DXE
calls must be explicitly listed so the loader can resolve it at dlopen
time. This includes:
- Memory (malloc, calloc, realloc, free)
- String operations (str*, mem*)
- Formatted I/O (printf, snprintf, fprintf, sscanf, etc.)
- File I/O (fopen, fread, fwrite, fclose, etc.)
- Directory operations (opendir, readdir, closedir, mkdir)
- Time (time, localtime, clock, strftime)
- Math (sin, cos, sqrt, pow, floor, ceil, etc.)
- stb_ds internals (arrput/arrfree/hm* macro backends)
- stb_image / stb_image_write
- libgcc 64-bit integer helpers (__divdi3, __moddi3, etc.)
- DJGPP stdio internals (__dj_stdin, __dj_stdout, __dj_stderr)
The table is registered once via `dlregsym()` before any `dlopen()`.
## System Hotkeys
These are always active regardless of which app is focused:
| Hotkey | Action |
|--------|--------|
| Alt+Tab | Cycle windows forward (rotate top to bottom of stack) |
| Shift+Alt+Tab | Cycle windows backward (pull bottom to top) |
| Alt+F4 | Close the focused window (calls its onClose callback) |
| Ctrl+F12 | Full screen screenshot -- prompts for save path (PNG/BMP/JPG) |
| Ctrl+Shift+F12 | Focused window screenshot -- prompts for save path |
| Ctrl+Esc | Open/raise the Task Manager (shell-level, always available) |
| F10 | Activate/toggle the focused window's menu bar |
| Alt+Space | Open/close the system menu on the focused window |
## Screenshot System
Two screenshot functions are available, both accessible from the system menu
(Alt+Space) or via hotkeys:
- `interactiveScreenshot(ctx)` -- captures the full screen (composited
backbuffer), then opens a Save As file dialog filtered to PNG/BMP/JPG/TGA.
Called by Ctrl+F12 or the "Screenshot..." system menu item.
- `interactiveWindowScreenshot(ctx, win)` -- captures just the focused
window's content buffer. Called by Ctrl+Shift+F12 or the "Window Shot..."
system menu item.
The system menu also includes standard window operations: Restore, Move, Size,
Minimize, Maximize, and Close.
## Logging
`shellLog(fmt, ...)` appends a printf-formatted line to `dvx.log`. The file
is opened, written, and closed on each call (append-per-write) so:
- The file is never held open, allowing Notepad to read it while the shell
runs.
- Writes are flushed immediately, important for crash diagnostics.
- The file is truncated once at startup.
Log output includes: startup sequence, preferences applied, video modes
enumerated, system information, app load/reap events, crash details with
full register dumps.
## Shell API Summary
| Function | Description |
|----------|-------------|
| `shellAppInit()` | Initialize the app slot table |
| `shellLoadApp(ctx, path)` | Load and start an app from a `.app` file |
| `shellReapApps(ctx)` | Clean up terminated apps (call each frame) |
| `shellReapApp(ctx, app)` | Gracefully shut down a single app |
| `shellForceKillApp(ctx, app)` | Forcibly kill an app (skip shutdown hook) |
| `shellAppInit()` | Zero the 32-slot app table |
| `shellLoadApp(ctx, path)` | Load and start a DXE app. Returns app ID (>= 1) or -1 |
| `shellReapApps(ctx)` | Clean up terminated apps (call each frame). Returns true if any reaped |
| `shellReapApp(ctx, app)` | Gracefully shut down one app (calls shutdown hook) |
| `shellForceKillApp(ctx, app)` | Forcibly kill an app -- skips shutdown hook |
| `shellTerminateAllApps(ctx)` | Kill all running apps (shell shutdown) |
| `shellGetApp(appId)` | Get app slot by ID |
| `shellRunningAppCount()` | Count running apps |
| `shellLog(fmt, ...)` | Write to `dvx.log` |
| `shellGetApp(appId)` | Get app slot by ID (NULL if invalid/free) |
| `shellRunningAppCount()` | Count running apps (excluding the shell) |
| `shellLog(fmt, ...)` | Append to dvx.log |
| `shellEnsureConfigDir(ctx)` | Create an app's config directory tree |
| `shellConfigPath(ctx, name, buf, size)` | Build path to file in app's config dir |
| `shellExportInit()` | Register DXE symbol export table via dlregsym() |
| `shellRegisterDesktopUpdate(fn)` | Register callback for app state changes |
| `shellExportInit()` | Register DXE symbol export table |
| `shellUnregisterDesktopUpdate(fn)` | Remove a previously registered callback |
| `shellDesktopUpdate()` | Notify all registered listeners |
| `shellTaskMgrOpen(ctx)` | Open or raise the Task Manager window |
| `shellTaskMgrRefresh()` | Refresh the Task Manager list and status |
| `shellInfoInit(ctx)` | Gather and log system hardware information |
| `shellGetSystemInfo()` | Return cached system info text |

View file

@ -1,77 +1,191 @@
# Packet -- Reliable Serial Transport
# Packet -- Reliable Serial Transport with HDLC Framing
Packetized serial transport providing reliable, ordered delivery over an
unreliable serial link. Uses HDLC-style byte-stuffed framing, CRC-16-CCITT
error detection, and a Go-Back-N sliding window protocol for automatic
retransmission.
This layer sits on top of an already-open rs232 COM port. It does not
open or close the serial port itself.
Packetized serial transport with HDLC-style framing, CRC-16 error
detection, and a Go-Back-N sliding window protocol for reliable,
ordered delivery over an unreliable serial link.
## Architecture
```
Application
|
[packet] framing, CRC, retransmit, ordering
| pktSend() queue a packet for reliable delivery
| pktPoll() receive, process ACKs/NAKs, check retransmit timers
|
[rs232] raw byte I/O
[Packet Layer] framing, CRC, sequencing, sliding window ARQ
|
[rs232] raw byte I/O via ISR-driven ring buffers
|
UART
```
The packet layer sits on top of an already-open rs232 COM port. It
does not open or close the serial port itself.
The packet layer adds framing, error detection, and reliability to the
raw byte stream provided by rs232. The caller provides a receive callback
that is invoked synchronously from `pktPoll()` for each complete, CRC-verified,
in-order data packet.
## Frame Format
Before byte stuffing:
Before byte stuffing, each frame has the following layout:
```
[0x7E] [SEQ] [TYPE] [LEN] [PAYLOAD...] [CRC_LO] [CRC_HI]
```
| Field | Size | Description |
|---------|---------|--------------------------------------|
| `0x7E` | 1 byte | Frame delimiter (flag byte) |
| `SEQ` | 1 byte | Sequence number (wrapping uint8) |
| `TYPE` | 1 byte | Frame type (see below) |
|-----------|-----------|----------------------------------------------|
| `0x7E` | 1 byte | Flag byte -- frame delimiter |
| `SEQ` | 1 byte | Sequence number (wrapping uint8_t, 0-255) |
| `TYPE` | 1 byte | Frame type (DATA, ACK, NAK, RST) |
| `LEN` | 1 byte | Payload length (0-255) |
| Payload | 0-255 | Application data |
| `CRC` | 2 bytes | CRC-16-CCITT over SEQ+TYPE+LEN+payload |
| `CRC` | 2 bytes | CRC-16-CCITT, little-endian, over SEQ..payload |
The header is 3 bytes (SEQ + TYPE + LEN), the CRC is 2 bytes, so the
minimum frame size (no payload) is 5 bytes. The maximum frame size (255-byte
payload) is 260 bytes before byte stuffing.
### Frame Types
| Type | Value | Description |
|--------|-------|----------------------------------------------|
| `DATA` | 0x00 | Data frame carrying application payload |
| `ACK` | 0x01 | Cumulative acknowledgment (next expected seq) |
| `NAK` | 0x02 | Negative ack (request retransmit from seq) |
| `RST` | 0x03 | Connection reset |
|--------|-------|----------------------------------------------------|
| `DATA` | 0x00 | Carries application payload |
| `ACK` | 0x01 | Cumulative acknowledgment -- next expected sequence |
| `NAK` | 0x02 | Negative ack -- request retransmit from this seq |
| `RST` | 0x03 | Connection reset -- clear all state |
### Byte Stuffing
The flag byte (`0x7E`) and escape byte (`0x7D`) are escaped within
frame data:
HDLC transparency encoding ensures the flag byte (0x7E) and escape byte
(0x7D) never appear in the frame body. Within the frame data (everything
between flags), these two bytes are escaped by prefixing with 0x7D and
XORing the original byte with 0x20:
- `0x7E` becomes `0x7D 0x5E`
- `0x7D` becomes `0x7D 0x5D`
## Reliability
In the worst case, every byte in the frame is escaped, doubling the wire
size. In practice, these byte values are uncommon in typical data and the
overhead is minimal.
The protocol uses Go-Back-N with a configurable sliding window
(1-8 slots, default 4):
### Why HDLC Framing
HDLC's flag-byte + byte-stuffing scheme is the simplest way to delimit
variable-length frames on a raw byte stream. The 0x7E flag byte
unambiguously marks frame boundaries. This is proven, lightweight, and
requires zero buffering at the framing layer.
The alternative -- length-prefixed framing -- is fragile on noisy links
because a corrupted length field permanently desynchronizes the receiver.
With HDLC framing, the receiver can always resynchronize by hunting for
the next flag byte.
## CRC-16-CCITT
Error detection uses CRC-16-CCITT (polynomial 0x1021, initial value
0xFFFF). The CRC covers the SEQ, TYPE, LEN, and payload fields. It is
stored little-endian in the frame (CRC low byte first, then CRC high byte).
The CRC is computed via a 256-entry lookup table (512 bytes of `.rodata`).
Table-driven CRC is approximately 10x faster than bit-by-bit computation
on a 486 -- a worthwhile trade for a function called on every frame
transmitted and received.
## Go-Back-N Sliding Window Protocol
### Why Go-Back-N
Go-Back-N ARQ is simpler than Selective Repeat -- the receiver does not
need an out-of-order reassembly buffer and only tracks a single expected
sequence number. This works well for the low bandwidth-delay product of a
serial link. On a 115200 bps local connection, the round-trip time is
negligible, so the window rarely fills.
Go-Back-N's retransmit-all-from-NAK behavior wastes bandwidth on lossy
links, but serial links are nearly lossless. The CRC check is primarily a
safety net for electrical noise, not a routine error recovery mechanism.
### Protocol Details
The sliding window is configurable from 1 to 8 slots (default 4).
Sequence numbers are 8-bit unsigned integers that wrap naturally at 256.
The sequence space (256) is much larger than 2x the maximum window (16),
so there is no ambiguity between old and new frames.
**Sender behavior:**
- Assigns a monotonically increasing sequence number to each DATA frame
- Retains a copy of each sent frame in a retransmit slot until it is
acknowledged
- When the window is full (`txCount >= windowSize`), blocks or returns
`PKT_ERR_TX_FULL` depending on the `block` parameter
**Receiver behavior:**
- Accepts frames strictly in order (`seq == rxExpectSeq`)
- On in-order delivery, increments `rxExpectSeq` and sends an ACK
carrying the new expected sequence number (cumulative acknowledgment)
- Out-of-order frames within the window trigger a NAK for the expected
sequence number
- Duplicate and out-of-window frames are silently discarded
**ACK processing:**
- ACKs carry the next expected sequence number (cumulative)
- On receiving an ACK, the sender frees all retransmit slots with
sequence numbers less than the ACK's sequence number
**NAK processing:**
- A NAK requests retransmission from a specific sequence number
- The sender retransmits that frame AND all subsequent unacknowledged
frames (the Go-Back-N property)
- Each retransmitted slot has its timer reset
**RST processing:**
- Resets all sequence numbers and buffers to zero on both sides
- The remote side also sends a RST in response
### Timer-Based Retransmission
Each retransmit slot tracks the time it was last (re)transmitted. If
500ms elapses without an ACK, the slot is retransmitted and the timer
is reset. This handles the case where an ACK or NAK was lost on the
wire -- without this safety net, the connection would stall permanently.
The 500ms timeout is conservative for a local serial link (RTT is under
1ms) but accounts for the remote side being busy processing. On BBS
connections through the Linux proxy, the round-trip includes TCP latency,
making the generous timeout appropriate.
### Receive State Machine
Incoming bytes from the serial port are fed through a three-state HDLC
deframing state machine:
| State | Description |
|----------|------------------------------------------------------|
| `HUNT` | Discarding bytes until a flag (0x7E) is seen |
| `ACTIVE` | Accumulating frame bytes; flag ends frame, ESC escapes |
| `ESCAPE` | Previous byte was 0x7D; XOR this byte with 0x20 |
The flag byte serves double duty: it ends the current frame AND starts
the next one. Back-to-back frames share a single flag byte, saving
bandwidth. A frame is only processed if it meets the minimum size
requirement (5 bytes), so spurious flags produce harmless zero-length
"frames" that are discarded.
- **Sender** assigns sequential numbers to each DATA frame and retains
a copy in the retransmit buffer until acknowledged.
- **Receiver** delivers frames in order. Out-of-order frames trigger a
NAK for the expected sequence number.
- **ACK** carries the next expected sequence number (cumulative).
- **NAK** triggers retransmission of the requested frame and all
subsequent unacknowledged frames.
- **Timer-based retransmit** fires after 500 poll cycles if no ACK or
NAK has been received.
## API Reference
### Types
```c
// Receive callback -- called for each verified, in-order packet
// Receive callback -- called for each verified, in-order data packet
typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len);
// Opaque connection handle
@ -81,8 +195,8 @@ typedef struct PktConnS PktConnT;
### Constants
| Name | Value | Description |
|-----------------------|-------|-------------------------------------|
| `PKT_MAX_PAYLOAD` | 255 | Max payload bytes per packet |
|------------------------|-------|--------------------------------------|
| `PKT_MAX_PAYLOAD` | 255 | Maximum payload bytes per packet |
| `PKT_DEFAULT_WINDOW` | 4 | Default sliding window size |
| `PKT_MAX_WINDOW` | 8 | Maximum sliding window size |
| `PKT_SUCCESS` | 0 | Success |
@ -92,8 +206,9 @@ typedef struct PktConnS PktConnT;
| `PKT_ERR_WOULD_BLOCK` | -4 | Operation would block |
| `PKT_ERR_OVERFLOW` | -5 | Buffer overflow |
| `PKT_ERR_INVALID_PARAM`| -6 | Invalid parameter |
| `PKT_ERR_TX_FULL` | -7 | Transmit window full |
| `PKT_ERR_TX_FULL` | -7 | Transmit window full (non-blocking) |
| `PKT_ERR_NO_DATA` | -8 | No data available |
| `PKT_ERR_DISCONNECTED` | -9 | Serial port disconnected or error |
### Functions
@ -106,12 +221,14 @@ PktConnT *pktOpen(int com, int windowSize,
Creates a packetized connection over an already-open COM port.
- `com` -- RS232 port index (`RS232_COM1`..`RS232_COM4`)
- `windowSize` -- sliding window size (1-8), 0 for default (4)
- `callback` -- called from `pktPoll()` for each received packet
- `callbackCtx` -- user pointer passed to callback
- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`)
- `windowSize` -- sliding window size (1-8), or 0 for the default (4)
- `callback` -- called from `pktPoll()` for each received, verified,
in-order data packet. The `data` pointer is valid only during the
callback.
- `callbackCtx` -- user pointer passed through to the callback
Returns a connection handle, or `NULL` on failure.
Returns a connection handle, or `NULL` on failure (allocation error).
#### pktClose
@ -120,6 +237,7 @@ void pktClose(PktConnT *conn);
```
Frees the connection state. Does **not** close the underlying COM port.
The caller is responsible for calling `rs232Close()` separately.
#### pktSend
@ -127,12 +245,17 @@ Frees the connection state. Does **not** close the underlying COM port.
int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block);
```
Sends a packet. `len` must be 1..`PKT_MAX_PAYLOAD`.
Sends a data packet. `len` must be in the range 1 to `PKT_MAX_PAYLOAD`
(255). The data is copied into a retransmit slot before transmission, so
the caller can reuse its buffer immediately.
- `block = true` -- waits for window space, polling for ACKs internally
- `block = false` -- returns `PKT_ERR_TX_FULL` if the window is full
- `block = true` -- If the transmit window is full, polls internally
(calling `pktPoll()` in a tight loop) until an ACK frees a slot. Returns
`PKT_ERR_DISCONNECTED` if the serial port drops during the wait.
- `block = false` -- Returns `PKT_ERR_TX_FULL` immediately if the window
is full.
The packet is stored in the retransmit buffer until acknowledged.
Returns `PKT_SUCCESS` on success.
#### pktPoll
@ -140,11 +263,21 @@ The packet is stored in the retransmit buffer until acknowledged.
int pktPoll(PktConnT *conn);
```
Reads available serial data, processes received frames, sends ACKs and
NAKs, and checks retransmit timers. Returns the number of DATA packets
delivered to the callback.
The main work function. Must be called frequently (every iteration of
your main loop or event loop). It performs three tasks:
Must be called frequently (e.g. in your main loop).
1. **Drain serial RX** -- reads all available bytes from the rs232 port
and feeds them through the HDLC deframing state machine
2. **Process frames** -- verifies CRC, handles DATA/ACK/NAK/RST frames,
delivers data packets to the callback
3. **Check retransmit timers** -- resends any slots that have timed out
Returns the number of DATA packets delivered to the callback this call,
or `PKT_ERR_DISCONNECTED` if the serial port returned an error, or
`PKT_ERR_INVALID_PARAM` if `conn` is NULL.
The callback is invoked synchronously, so the caller should be prepared
for re-entrant calls to `pktSend()` from within the callback.
#### pktReset
@ -152,8 +285,19 @@ Must be called frequently (e.g. in your main loop).
int pktReset(PktConnT *conn);
```
Resets all sequence numbers and buffers to zero. Sends a RST frame to
the remote side so it resets as well.
Resets all sequence numbers, TX slots, and RX state to zero. Sends a RST
frame to the remote side so it resets as well. Useful for recovering from
a desynchronized state.
#### pktCanSend
```c
bool pktCanSend(PktConnT *conn);
```
Returns `true` if there is room in the transmit window for another
packet. Useful for non-blocking send loops to avoid calling `pktSend()`
when it would return `PKT_ERR_TX_FULL`.
#### pktGetPending
@ -162,32 +306,37 @@ int pktGetPending(PktConnT *conn);
```
Returns the number of unacknowledged packets currently in the transmit
window. Useful for throttling sends in non-blocking mode.
window. Ranges from 0 (all sent packets acknowledged) to `windowSize`
(window full). Useful for throttling sends and monitoring link health.
## Example
## Usage Example
```c
#include "packet.h"
#include "../rs232/rs232.h"
void onPacket(void *ctx, const uint8_t *data, int len) {
// process received packet
// process received packet -- data is valid only during this callback
}
int main(void) {
// Open serial port first
// Open serial port first (packet layer does not manage it)
rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE);
// Create packet connection with default window size
// Create packet connection with default window size (4)
PktConnT *conn = pktOpen(RS232_COM1, 0, onPacket, NULL);
// Send a packet (blocking)
// Send a packet (blocking -- waits for window space if needed)
uint8_t msg[] = "Hello, packets!";
pktSend(conn, msg, sizeof(msg), true);
// Main loop
// Main loop -- must call pktPoll() frequently
while (1) {
int delivered = pktPoll(conn);
if (delivered == PKT_ERR_DISCONNECTED) {
break;
}
// delivered = number of packets received this iteration
}
@ -197,11 +346,55 @@ int main(void) {
}
```
## CRC
### Non-Blocking Send Pattern
```c
// Send as fast as the window allows, doing other work between sends
while (bytesLeft > 0) {
pktPoll(conn); // process ACKs, free window slots
if (pktCanSend(conn)) {
int chunk = bytesLeft;
if (chunk > PKT_MAX_PAYLOAD) {
chunk = PKT_MAX_PAYLOAD;
}
if (pktSend(conn, data + offset, chunk, false) == PKT_SUCCESS) {
offset += chunk;
bytesLeft -= chunk;
}
}
// do other work here (update UI, check for cancel, etc.)
}
// Drain remaining ACKs
while (pktGetPending(conn) > 0) {
pktPoll(conn);
}
```
## Internal Data Structures
### Connection State (PktConnT)
The connection handle contains:
- **COM port index** and **window size** (configuration)
- **Callback** function pointer and context
- **TX state**: next sequence to assign, oldest unacked sequence, array
of retransmit slots, count of slots in use
- **RX state**: next expected sequence, deframing state machine state,
frame accumulation buffer
### Retransmit Slots (TxSlotT)
Each slot holds a copy of the sent payload, its sequence number, payload
length, and a `clock_t` timestamp of when it was last transmitted. The
retransmit timer compares this timestamp against the current time to
detect timeout.
CRC-16-CCITT (polynomial 0x1021, init 0xFFFF) computed via a 256-entry
lookup table (512 bytes). The CRC covers the SEQ, TYPE, LEN, and
payload fields.
## Building
@ -210,6 +403,26 @@ make # builds ../lib/libpacket.a
make clean # removes objects and library
```
Requires `librs232.a` at link time.
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
Target: DJGPP cross-compiler, 486+ CPU.
Objects are placed in `../obj/packet/`, the library in `../lib/`.
Requires `librs232.a` at link time (for `rs232Read()` and `rs232Write()`).
## Files
- `packet.h` -- Public API header (types, constants, function prototypes)
- `packet.c` -- Complete implementation (framing, CRC, ARQ, state machine)
- `Makefile` -- DJGPP cross-compilation build rules
## Dependencies
- `rs232/` -- Serial port I/O (must be linked: `-lrs232`)
## Used By
- `seclink/` -- Secure serial link (adds channel multiplexing and encryption)
- `proxy/` -- Linux serial proxy (uses a socket-based adaptation)

View file

@ -1,16 +1,20 @@
# SecLink Proxy
# SecLink Proxy -- Linux Serial Bridge
Linux-hosted proxy that bridges an 86Box emulated serial port to a
remote telnet BBS. The 86Box side communicates using the secLink
protocol (packet framing, DH key exchange, XTEA encryption). The BBS
side is plain telnet over TCP.
remote telnet BBS. Part of the DVX GUI project.
The 86Box side communicates using the SecLink protocol (HDLC packet
framing, CRC-16, DH key exchange, XTEA-CTR encryption). The BBS side
is plain telnet over TCP. All crypto is transparent to the BBS -- it
sees a normal telnet client.
## Architecture
```
86Box (DOS terminal) Remote BBS
| |
emulated modem telnet:23
emulated modem telnet
| |
TCP:2323 TCP:23
| |
@ -20,10 +24,11 @@ side is plain telnet over TCP.
```
The proxy accepts a single TCP connection from 86Box, performs the
secLink handshake (DH key exchange), then connects to the BBS. All
traffic between 86Box and the proxy is encrypted via XTEA-CTR on
channel 0. Traffic between the proxy and the BBS is unencrypted
telnet.
SecLink handshake (Diffie-Hellman key exchange), then connects to the
BBS. All traffic between 86Box and the proxy is encrypted via XTEA-CTR
on channel 0. Traffic between the proxy and the BBS is unencrypted
telnet with IAC negotiation handling.
## Usage
@ -32,10 +37,12 @@ secproxy [listen_port] [bbs_host] [bbs_port]
```
| Argument | Default | Description |
|---------------|------------------------|---------------------------------|
|---------------|--------------|---------------------------------|
| `listen_port` | 2323 | TCP port for 86Box connection |
| `bbs_host` | bbs.duensing.digital | BBS hostname |
| `bbs_port` | 23 | BBS TCP port |
| `bbs_host` | 10.1.0.244 | BBS hostname or IP |
| `bbs_port` | 2023 | BBS TCP port |
Examples:
```
secproxy # all defaults
@ -44,15 +51,20 @@ secproxy 2323 bbs.example.com 23 # different BBS
secproxy --help # show usage
```
## Startup Sequence
1. Listen on the configured TCP port
2. Wait for 86Box to connect (blocks on accept)
3. Connect to the remote BBS
4. Seed the RNG from `/dev/urandom`
5. Open secLink and perform the DH handshake (blocks until the DOS
side completes its handshake)
6. Enter the proxy loop
1. Listen on the configured TCP port.
2. Wait for 86Box to connect (polls with Ctrl+C support).
3. Map the TCP socket to COM0 via the socket shim.
4. Seed the RNG from `/dev/urandom`.
5. Open SecLink and perform the DH handshake (blocks until the DOS
side completes its handshake).
6. Wait for the terminal to send ENTER (so the user can confirm the
connection is working before BBS output starts).
7. Connect to the remote BBS.
8. Enter the proxy loop.
## Proxy Loop
@ -62,47 +74,84 @@ the two TCP connections:
- **86Box -> BBS**: `secLinkPoll()` reads from the 86Box socket via
the socket shim, decrypts incoming packets, and the receive callback
writes plaintext to the BBS socket.
- **BBS -> 86Box**: `read()` from the BBS socket, then
`secLinkSend()` encrypts and sends to 86Box via the socket shim.
- **Maintenance**: `secLinkPoll()` also handles packet-layer retransmit
timers on every iteration.
- **BBS -> 86Box**: `read()` from the BBS socket, then the telnet
filter strips IAC sequences, then `secLinkSend()` encrypts and sends
to 86Box via the socket shim. If the send window is full, the loop
retries with ACK processing until the data goes through.
- **Maintenance**: `secLinkPoll()` also handles packet-layer
retransmit timers on every iteration.
The proxy exits cleanly on Ctrl+C (SIGINT), SIGTERM, or when either
side disconnects.
## 86Box Configuration
Configure the 86Box serial port to use a telnet connection:
## Telnet Negotiation
The proxy handles RFC 854 telnet IAC sequences from the BBS so they
do not corrupt the terminal display. A state machine parser strips
IAC sequences from the data stream, persisting state across TCP
segment boundaries.
Accepted options:
- ECHO (option 1) -- server echoes characters
- SGA (option 3) -- suppress go-ahead for character-at-a-time mode
- TTYPE (option 24) -- terminal type negotiation
- NAWS (option 31) -- window size negotiation
All other options are refused. Subnegotiations (SB...SE) are consumed
silently.
1. In 86Box settings, set a COM port to "TCP (server)" or
"TCP (client)" mode pointing at the proxy's listen port
2. Enable "No telnet negotiation" to send raw bytes
3. The DOS terminal application running inside 86Box uses secLink
over this serial port
## Socket Shim
The proxy reuses the same packet, security, and secLink source code
as the DOS build. A socket shim (`sockShim.h`/`sockShim.c`) provides
rs232-compatible `rs232Read()`/`rs232Write()` functions backed by TCP
sockets instead of UART hardware:
The proxy reuses the same packet, security, and secLink source code as
the DOS build. A socket shim (`sockShim.h` / `sockShim.c`) provides
rs232-compatible functions backed by TCP sockets instead of UART
hardware:
| rs232 function | Socket shim behavior |
|----------------|-----------------------------------------------|
| `rs232Open()` | No-op (socket already connected) |
| `rs232Close()` | Marks port closed (socket managed by caller) |
| `rs232Open()` | Validates socket assigned; ignores serial params |
| `rs232Close()` | Marks port closed (socket lifecycle is caller's) |
| `rs232Read()` | Non-blocking `recv()` with `MSG_DONTWAIT` |
| `rs232Write()` | Blocking `send()` loop with `MSG_NOSIGNAL` |
The shim maps COM port indices (0-3) to socket file descriptors via
`sockShimSetFd()`, which must be called before opening the secLink
layer.
`sockShimSetFd()`, which must be called before opening the SecLink
layer. Up to 4 ports are supported.
The Makefile uses `-include sockShim.h` when compiling the packet and
secLink layers, which defines `RS232_H` to prevent the real `rs232.h`
from being included.
## DJGPP Stubs
DOS-specific headers required by the security library are replaced by
minimal stubs in `stubs/`:
| Stub | Replaces DJGPP header | Contents |
|-------------------|------------------------|-------------------|
| `stubs/pc.h` | `<pc.h>` | No-op definitions |
| `stubs/go32.h` | `<go32.h>` | No-op definitions |
| `stubs/sys/farptr.h` | `<sys/farptr.h>` | No-op definitions |
The security library's hardware entropy function returns zeros on
Linux, which is harmless since the proxy seeds the RNG from
`/dev/urandom` before the handshake.
## 86Box Configuration
Configure the 86Box serial port to connect to the proxy:
1. In 86Box settings, set a COM port to TCP client mode pointing at
the proxy's listen port (default 2323).
2. Enable "No telnet negotiation" to send raw bytes.
3. The DOS terminal application running inside 86Box uses SecLink
over this serial port.
DOS-specific headers (`<pc.h>`, `<go32.h>`, `<sys/farptr.h>`) are
replaced by minimal stubs in `stubs/` that provide no-op
implementations. The security library's hardware entropy function
returns zeros on Linux, which is harmless since the proxy seeds the
RNG from `/dev/urandom` before the handshake.
## Building
@ -114,7 +163,9 @@ make clean # removes objects and binary
Objects are placed in `../obj/proxy/`, the binary in `../bin/`.
Requires only a standard Linux C toolchain (gcc, libc). No external
dependencies.
dependencies beyond the project's own packet, security, and secLink
source files, which are compiled directly from their source directories.
## Files
@ -123,7 +174,7 @@ proxy/
proxy.c main proxy program
sockShim.h rs232-compatible socket API (header)
sockShim.c socket shim implementation
Makefile Linux build
Makefile Linux native build
stubs/
pc.h stub for DJGPP <pc.h>
go32.h stub for DJGPP <go32.h>

View file

@ -1,63 +1,187 @@
# RS232 -- Serial Port Library for DJGPP
# RS232 -- ISR-Driven Serial Port Library for DJGPP
ISR-driven UART communication library supporting up to 4 simultaneous
COM ports with ring buffers and hardware/software flow control.
Interrupt-driven UART communication library supporting up to 4 simultaneous
COM ports with ring buffers and hardware/software flow control. Targets
486-class DOS hardware running under DJGPP/DPMI.
Ported from the DOS Serial Library 1.4 by Karl Stenerud (MIT License),
stripped to DJGPP-only codepaths and restyled.
stripped to DJGPP-only codepaths and restyled for the DVX project.
## Features
- ISR-driven receive and transmit with 2048-byte ring buffers
- Auto-detected IRQ from BIOS data area
- UART type detection (8250, 16450, 16550, 16550A)
- 16550 FIFO detection and configurable trigger threshold
- XON/XOFF, RTS/CTS, and DTR/DSR flow control
- DPMI memory locking for ISR safety
- Speeds from 50 to 115200 bps
- 5-8 data bits, N/O/E/M/S parity, 1-2 stop bits
## Architecture
The library is built around a single shared ISR (`comGeneralIsr`) that
services all open COM ports. This design is necessary because COM1/COM3
typically share IRQ4 and COM2/COM4 share IRQ3 -- a single handler that
polls all ports avoids the complexity of per-IRQ dispatch.
```
Application
|
| rs232Read() non-blocking drain from RX ring buffer
| rs232Write() blocking polled write directly to UART THR
| rs232WriteBuf() non-blocking write into TX ring buffer
|
[Ring Buffers] 2048-byte RX + TX per port, power-of-2 bitmask indexing
|
[ISR] comGeneralIsr -- shared handler for all open ports
|
[UART] 8250 / 16450 / 16550 / 16550A hardware
```
### ISR Design
The ISR follows a careful protocol to remain safe under DPMI while
keeping the system responsive:
1. **Mask** all COM port IRQs on the PIC to prevent ISR re-entry
2. **STI** to allow higher-priority interrupts (timer tick, keyboard) through
3. **Loop** over all open ports, draining each UART's pending interrupt
conditions (data ready, TX hold empty, modem status, line status)
4. **CLI**, send EOI to the PIC, re-enable COM IRQs, **STI** before IRET
This mask-then-STI pattern is standard for slow device ISRs on PC
hardware. It prevents the same IRQ from re-entering while allowing the
system timer and keyboard to function during UART processing.
### Ring Buffers
Both RX and TX buffers are 2048 bytes, sized as a power of 2 so that
head/tail wraparound is a single AND operation (bitmask indexing) rather
than an expensive modulo -- critical for ISR-speed code on a 486.
The buffers use a one-slot-wasted design to distinguish full from empty:
`head == tail` means empty, `(head + 1) & MASK == tail` means full.
### Flow Control
Flow control operates entirely within the ISR using watermark thresholds.
When the RX buffer crosses 80% full, the ISR signals the remote side to
stop sending; when it drops below 20%, the ISR allows the remote to
resume. This prevents buffer overflow without any application involvement.
Three modes are supported:
| Mode | Stop Signal | Resume Signal |
|------------|-------------------|-------------------|
| XON/XOFF | Send XOFF (0x13) | Send XON (0x11) |
| RTS/CTS | Deassert RTS | Assert RTS |
| DTR/DSR | Deassert DTR | Assert DTR |
On the TX side, the ISR monitors incoming XON/XOFF bytes and the CTS/DSR
modem status lines to pause and resume transmission from the TX ring
buffer.
### DPMI Memory Locking
The ISR code and all per-port state structures (`sComPorts` array) are
locked in physical memory via `__dpmi_lock_linear_region`. This prevents
page faults during interrupt handling -- a hard requirement for any ISR
running under a DPMI host (DOS extender, Windows 3.x, OS/2 VDM, etc.).
An IRET wrapper is allocated by DPMI to handle the real-mode to
protected-mode transition on hardware interrupt entry.
## UART Type Detection
`rs232GetUartType()` probes the UART hardware to identify the chip:
1. **Scratch register test** -- Writes two known values (0xAA, 0x55) to
UART register 7 and reads them back. The 8250 lacks this register, so
readback fails. If both values read back correctly, the UART is at
least a 16450.
2. **FIFO test** -- Enables the FIFO via the FCR (FIFO Control Register),
then reads bits 7:6 of the IIR (Interrupt Identification Register):
- `0b11` = 16550A (working 16-byte FIFO)
- `0b10` = 16550 (broken FIFO -- present in hardware but unusable)
- `0b00` = 16450 (no FIFO at all)
The original FCR value is restored after probing.
| Constant | Value | Description |
|---------------------|-------|----------------------------------------|
| `RS232_UART_UNKNOWN`| 0 | Unknown or undetected |
| `RS232_UART_8250` | 1 | Original IBM PC -- no FIFO, no scratch |
| `RS232_UART_16450` | 2 | Scratch register present, no FIFO |
| `RS232_UART_16550` | 3 | Broken FIFO (rare, unusable) |
| `RS232_UART_16550A` | 4 | Working 16-byte FIFO (most common) |
On 16550A UARTs, the FIFO trigger threshold is configurable via
`rs232SetFifoThreshold()` with levels of 1, 4, 8, or 14 bytes. The
default is 14, which minimizes interrupt overhead at high baud rates.
## IRQ Auto-Detection
When `rs232Open()` is called without a prior `rs232SetIrq()` override,
the library auto-detects the UART's IRQ by:
1. Saving the current PIC interrupt mask registers (IMR)
2. Enabling all IRQ lines on both PICs
3. Generating a TX Hold Empty interrupt on the UART
4. Reading the PIC's Interrupt Request Register (IRR) to see which line
went high
5. Disabling the interrupt, reading IRR again to mask out persistent bits
6. Re-enabling once more to confirm the detection
7. Restoring the original PIC mask
If auto-detection fails (common on virtualized hardware that does not
model the IRR accurately), the library falls back to the default IRQ for
the port (IRQ4 for COM1/COM3, IRQ3 for COM2/COM4).
The BIOS Data Area (at segment 0x0040) is read to determine each port's
I/O base address. Ports not configured in the BDA are unavailable.
## COM Port Support
| Constant | Value | Default IRQ | Default Base |
|--------------|-------|-------------|--------------|
| `RS232_COM1` | 0 | IRQ 4 | 0x3F8 |
| `RS232_COM2` | 1 | IRQ 3 | 0x2F8 |
| `RS232_COM3` | 2 | IRQ 4 | 0x3E8 |
| `RS232_COM4` | 3 | IRQ 3 | 0x2E8 |
Base addresses are read from the BIOS Data Area at runtime. The default
IRQ values are used only as a fallback when auto-detection fails. Both
the base address and IRQ can be overridden before opening with
`rs232SetBase()` and `rs232SetIrq()`.
## Supported Baud Rates
All standard rates from 50 to 115200 bps are supported. The baud rate
divisor is computed from the standard 1.8432 MHz UART crystal:
| Rate | Divisor | Rate | Divisor |
|--------|---------|--------|---------|
| 50 | 2304 | 4800 | 24 |
| 75 | 1536 | 7200 | 16 |
| 110 | 1047 | 9600 | 12 |
| 150 | 768 | 19200 | 6 |
| 300 | 384 | 38400 | 3 |
| 600 | 192 | 57600 | 2 |
| 1200 | 96 | 115200 | 1 |
| 1800 | 64 | | |
| 2400 | 48 | | |
| 3800 | 32 | | |
Data bits (5-8), parity (N/O/E/M/S), and stop bits (1-2) are configured
by writing the appropriate LCR (Line Control Register) bits.
## API Reference
### Types
All functions take a COM port index (`int com`) as their first argument:
| Constant | Value | Description |
|--------------|-------|-------------|
| `RS232_COM1` | 0 | COM1 |
| `RS232_COM2` | 1 | COM2 |
| `RS232_COM3` | 2 | COM3 |
| `RS232_COM4` | 3 | COM4 |
### UART Types
| Constant | Value | Description |
|---------------------|-------|--------------------------------------|
| `RS232_UART_UNKNOWN`| 0 | Unknown or undetected |
| `RS232_UART_8250` | 1 | 8250 -- no FIFO, no scratch register |
| `RS232_UART_16450` | 2 | 16450 -- scratch register, no FIFO |
| `RS232_UART_16550` | 3 | 16550 -- broken FIFO (unusable) |
| `RS232_UART_16550A` | 4 | 16550A -- working 16-byte FIFO |
### Handshaking Modes
| Constant | Value | Description |
|--------------------------|-------|---------------------------|
| `RS232_HANDSHAKE_NONE` | 0 | No flow control |
| `RS232_HANDSHAKE_XONXOFF`| 1 | Software (XON/XOFF) |
| `RS232_HANDSHAKE_RTSCTS` | 2 | Hardware (RTS/CTS) |
| `RS232_HANDSHAKE_DTRDSR` | 3 | Hardware (DTR/DSR) |
### Error Codes
| Constant | Value | Description |
|-------------------------------|-------|---------------------------|
|-------------------------------|-------|--------------------------|
| `RS232_SUCCESS` | 0 | Success |
| `RS232_ERR_UNKNOWN` | -1 | Unknown error |
| `RS232_ERR_NOT_OPEN` | -2 | Port not open |
| `RS232_ERR_ALREADY_OPEN` | -3 | Port already open |
| `RS232_ERR_NO_UART` | -4 | No UART detected |
| `RS232_ERR_NO_UART` | -4 | No UART detected at base |
| `RS232_ERR_INVALID_PORT` | -5 | Bad port index |
| `RS232_ERR_INVALID_BASE` | -6 | Bad I/O base address |
| `RS232_ERR_INVALID_IRQ` | -7 | Bad IRQ number |
@ -71,20 +195,29 @@ All functions take a COM port index (`int com`) as their first argument:
| `RS232_ERR_IRQ_NOT_FOUND` | -15 | Could not detect IRQ |
| `RS232_ERR_LOCK_MEM` | -16 | DPMI memory lock failed |
### Functions
### Handshaking Modes
#### Open / Close
| Constant | Value | Description |
|---------------------------|-------|----------------------|
| `RS232_HANDSHAKE_NONE` | 0 | No flow control |
| `RS232_HANDSHAKE_XONXOFF` | 1 | Software (XON/XOFF) |
| `RS232_HANDSHAKE_RTSCTS` | 2 | Hardware (RTS/CTS) |
| `RS232_HANDSHAKE_DTRDSR` | 3 | Hardware (DTR/DSR) |
### Open / Close
```c
int rs232Open(int com, int32_t bps, int dataBits, char parity,
int stopBits, int handshake);
```
Opens a COM port. Detects the UART base address from the BIOS data
area, auto-detects the IRQ, installs the ISR, and configures the port.
Opens a COM port. Reads the UART base address from the BIOS data area,
auto-detects the IRQ, locks ISR memory via DPMI, installs the ISR, and
configures the UART for the specified parameters. Returns `RS232_SUCCESS`
or an error code.
- `bps` -- baud rate (50, 75, 110, 150, 300, 600, 1200, 1800, 2400,
3800, 4800, 7200, 9600, 19200, 38400, 57600, 115200)
- `com` -- port index (`RS232_COM1` through `RS232_COM4`)
- `bps` -- baud rate (50 through 115200)
- `dataBits` -- 5, 6, 7, or 8
- `parity` -- `'N'` (none), `'O'` (odd), `'E'` (even), `'M'` (mark),
`'S'` (space)
@ -95,42 +228,50 @@ area, auto-detects the IRQ, installs the ISR, and configures the port.
int rs232Close(int com);
```
Closes the port, removes the ISR, and restores the original interrupt
vector.
Closes the port, disables UART interrupts, removes the ISR, restores the
original interrupt vector, and unlocks DPMI memory (when the last port
closes).
#### Read / Write
### Read / Write
```c
int rs232Read(int com, char *data, int len);
```
Reads up to `len` bytes from the receive buffer. Returns the number of
bytes actually read (0 if the buffer is empty).
Non-blocking read. Drains up to `len` bytes from the RX ring buffer.
Returns the number of bytes actually read (0 if the buffer is empty).
If flow control is active and the buffer drops below the low-water mark,
the ISR will re-enable receive from the remote side.
```c
int rs232Write(int com, const char *data, int len);
```
Blocking write. Sends `len` bytes, waiting for transmit buffer space
as needed. Returns `RS232_SUCCESS` or an error code.
Blocking polled write. Sends `len` bytes directly to the UART THR
(Transmit Holding Register), bypassing the TX ring buffer entirely.
Polls LSR for THR empty before each byte. Returns `RS232_SUCCESS` or
an error code.
```c
int rs232WriteBuf(int com, const char *data, int len);
```
Non-blocking write. Copies as many bytes as will fit into the transmit
buffer. Returns the number of bytes actually queued.
Non-blocking buffered write. Copies as many bytes as will fit into the
TX ring buffer. The ISR drains the TX buffer to the UART automatically.
Returns the number of bytes actually queued. If the buffer is full, some
bytes may be dropped.
#### Buffer Management
### Buffer Management
```c
int rs232ClearRxBuffer(int com);
int rs232ClearTxBuffer(int com);
```
Discard all data in the receive or transmit ring buffer.
Discard all data in the receive or transmit ring buffer by resetting
head and tail pointers to zero.
#### Getters
### Getters
```c
int rs232GetBase(int com); // UART I/O base address
@ -141,50 +282,46 @@ int rs232GetDsr(int com); // DSR line state (0 or 1)
int rs232GetDtr(int com); // DTR line state (0 or 1)
int rs232GetHandshake(int com); // Handshaking mode
int rs232GetIrq(int com); // IRQ number
int rs232GetLsr(int com); // Line status register
int rs232GetMcr(int com); // Modem control register
int rs232GetMsr(int com); // Modem status register
int rs232GetLsr(int com); // Line Status Register
int rs232GetMcr(int com); // Modem Control Register
int rs232GetMsr(int com); // Modem Status Register
char rs232GetParity(int com); // Parity setting ('N','O','E','M','S')
int rs232GetRts(int com); // RTS line state (0 or 1)
int rs232GetRxBuffered(int com); // Bytes in receive buffer
int rs232GetRxBuffered(int com); // Bytes waiting in RX buffer
int rs232GetStop(int com); // Stop bits setting
int rs232GetTxBuffered(int com); // Bytes in transmit buffer
int rs232GetTxBuffered(int com); // Bytes waiting in TX buffer
int rs232GetUartType(int com); // UART type (RS232_UART_* constant)
```
`rs232GetUartType` probes the UART hardware to identify the chip:
Most getters return cached register values from the per-port state
structure, avoiding unnecessary I/O port reads. `rs232GetUartType()`
actively probes the hardware (see UART Type Detection above).
1. **Scratch register test** -- writes two values to register 7 and
reads them back. The 8250 lacks this register, so readback fails.
2. **FIFO test** -- enables the FIFO via the FCR, then reads IIR bits
7:6. `0b11` = 16550A (working FIFO), `0b10` = 16550 (broken FIFO),
`0b00` = 16450 (no FIFO). The original FCR value is restored after
probing.
#### Setters
### Setters
```c
int rs232Set(int com, int32_t bps, int dataBits, char parity,
int stopBits, int handshake);
```
Reconfigure all port parameters at once (port must be open).
Reconfigure all port parameters at once. The port must already be open.
```c
int rs232SetBase(int com, int base); // Override I/O base address
int rs232SetBase(int com, int base); // Override I/O base (before open)
int rs232SetBps(int com, int32_t bps); // Change baud rate
int rs232SetData(int com, int dataBits); // Change data bits
int rs232SetDtr(int com, bool dtr); // Assert/deassert DTR
int rs232SetFifoThreshold(int com, int thr); // FIFO trigger level (1,4,8,14)
int rs232SetFifoThreshold(int com, int thr); // FIFO trigger (1, 4, 8, 14)
int rs232SetHandshake(int com, int handshake); // Change flow control mode
int rs232SetIrq(int com, int irq); // Override IRQ (before Open)
int rs232SetMcr(int com, int mcr); // Write modem control register
int rs232SetIrq(int com, int irq); // Override IRQ (before open)
int rs232SetMcr(int com, int mcr); // Write Modem Control Register
int rs232SetParity(int com, char parity); // Change parity
int rs232SetRts(int com, bool rts); // Assert/deassert RTS
int rs232SetStop(int com, int stopBits); // Change stop bits
```
## Example
## Usage Example
```c
#include "rs232.h"
@ -196,14 +333,19 @@ int main(void) {
return 1;
}
// Identify UART chip
// Identify the UART chip
int uartType = rs232GetUartType(RS232_COM1);
// uartType == RS232_UART_16550A on most systems
// uartType == RS232_UART_16550A on most 486+ systems
// Enable 16550A FIFO with trigger at 14 bytes
if (uartType == RS232_UART_16550A) {
rs232SetFifoThreshold(RS232_COM1, 14);
}
// Blocking send
rs232Write(RS232_COM1, "Hello\r\n", 7);
// Non-blocking receive
// Non-blocking receive loop
char buf[128];
int n;
while ((n = rs232Read(RS232_COM1, buf, sizeof(buf))) > 0) {
@ -215,17 +357,38 @@ int main(void) {
}
```
## Implementation Notes
- The ISR handles all four COM ports from a single shared handler.
On entry it disables UART interrupts for all open ports, then
re-enables CPU interrupts so higher-priority devices are serviced.
- The single shared ISR handles all four COM ports. On entry it disables
UART interrupts for all open ports on the PIC, then re-enables CPU
interrupts (STI) so higher-priority devices (timer, keyboard) are
serviced promptly.
- Ring buffers use power-of-2 sizes (2048 bytes) with bitmask indexing
for zero-branch wraparound.
- Flow control watermarks are at 80% (assert) and 20% (deassert) of
buffer capacity.
- DPMI `__dpmi_lock_linear_region` is used to pin the ISR, ring
buffers, and port state in physical memory.
for zero-branch wraparound. Each port uses 4KB total (2KB RX + 2KB TX).
- Flow control watermarks are at 80% (assert stop) and 20% (deassert
stop) of buffer capacity. These percentages are defined as compile-time
constants and apply to both RX and TX directions.
- DPMI `__dpmi_lock_linear_region` is used to pin the ISR code, ring
buffers, and port state in physical memory. The ISR code region is
locked for 2048 bytes starting at the `comGeneralIsr` function address.
- `rs232Write()` is a blocking polled write that bypasses the TX ring
buffer entirely. It writes directly to the UART THR register, polling
LSR for readiness between each byte. `rs232WriteBuf()` is the
non-blocking alternative that queues into the TX ring buffer for ISR
draining.
- Per-port state is stored in a static array of `Rs232StateT` structures
(`sComPorts[4]`). This array is locked in physical memory alongside the
ISR code.
- The BIOS Data Area (real-mode address 0040:0000) is read via DJGPP's
far pointer API (`_farpeekw`) to obtain port base addresses at runtime.
## Building
@ -234,4 +397,21 @@ make # builds ../lib/librs232.a
make clean # removes objects and library
```
Target: DJGPP cross-compiler, 486+ CPU.
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
Objects are placed in `../obj/rs232/`, the library in `../lib/`.
## Files
- `rs232.h` -- Public API header
- `rs232.c` -- Complete implementation (ISR, DPMI, ring buffers, UART I/O)
- `Makefile` -- DJGPP cross-compilation build rules
## Used By
- `packet/` -- Packetized serial transport layer (HDLC framing, CRC, ARQ)
- `seclink/` -- Secure serial link (opens and closes the COM port)
- `proxy/` -- Linux serial proxy (uses a socket-based shim of this API)

View file

@ -1,28 +1,39 @@
# SecLink -- Secure Serial Link Library
SecLink is a convenience wrapper that ties together three lower-level
libraries into a single API for reliable, optionally encrypted serial
communication:
SecLink is the top-level API for the DVX serial/networking stack. It
composes three lower-level libraries into a single interface for reliable,
optionally encrypted, channel-multiplexed serial communication:
- **rs232** -- ISR-driven UART I/O with ring buffers and flow control
- **packet** -- HDLC-style framing with CRC-16 and sliding window reliability
- **security** -- 1024-bit Diffie-Hellman key exchange and XTEA-CTR encryption
- **packet** -- HDLC framing, CRC-16, Go-Back-N sliding window ARQ
- **security** -- 1024-bit Diffie-Hellman key exchange, XTEA-CTR encryption
SecLink adds channel multiplexing and per-packet encryption control on
top of the packet layer's reliable delivery.
## Architecture
```
Application
|
[secLink] channels, optional encryption
| secLinkSend() send data on a channel, optionally encrypted
| secLinkPoll() receive, decrypt, deliver to callback
| secLinkHandshake() DH key exchange (blocking)
|
[packet] framing, CRC, retransmit, ordering
[SecLink] channel header, encrypt/decrypt, key management
|
[rs232] ISR-driven UART, ring buffers, flow control
[Packet] HDLC framing, CRC-16, Go-Back-N ARQ
|
UART
[RS232] ISR-driven UART, 2048-byte ring buffers
|
UART Hardware
```
SecLink adds a one-byte header to every packet:
### Channel Multiplexing
SecLink prepends a one-byte header to every packet's payload before
handing it to the packet layer:
```
Bit 7 Bits 6..0
@ -30,47 +41,113 @@ SecLink adds a one-byte header to every packet:
Encrypt Channel (0-127)
```
This allows mixing encrypted and cleartext traffic on up to 128
independent logical channels over a single serial link.
This allows up to 128 independent logical channels over a single serial
link. Each channel can carry a different type of traffic (terminal data,
file transfer, control messages, etc.) without needing separate framing
or sequencing per stream. The receive callback includes the channel
number so the application can dispatch accordingly.
The encrypt flag (bit 7) tells the receiver whether the payload portion
of this packet is encrypted. The channel header byte itself is always
sent in the clear.
### Mixed Clear and Encrypted Traffic
Unencrypted packets can be sent before or after the DH handshake. This
enables a startup protocol (version negotiation, capability exchange)
before keys are established. Encrypted packets require a completed
handshake -- attempting to send an encrypted packet before the handshake
returns `SECLINK_ERR_NOT_READY`.
On the receive side, encrypted packets arriving before the handshake is
complete are silently dropped. Cleartext packets are delivered regardless
of handshake state.
## Lifecycle
```
secLinkOpen() Open COM port and packet layer
secLinkHandshake() DH key exchange (blocks until complete)
secLinkSend() Send a packet (encrypted or clear)
secLinkHandshake() DH key exchange (blocks until both sides complete)
secLinkSend() Send data on a channel (encrypted or cleartext)
secLinkPoll() Receive and deliver packets to callback
secLinkClose() Tear down everything
secLinkClose() Tear down everything (ciphers, packet, COM port)
```
The handshake is only required if you intend to send encrypted packets.
Cleartext packets can be sent immediately after `secLinkOpen()`.
### Handshake Protocol
The DH key exchange uses the packet layer's reliable delivery, so lost
packets are automatically retransmitted. Both sides can send their public
key simultaneously -- there is no initiator/responder distinction.
1. Both sides generate a DH keypair (256-bit private, 1024-bit public)
2. Both sides send their 128-byte public key as a single packet
3. On receiving the remote's public key, each side immediately computes
the shared secret (`remote^private mod p`)
4. Each side derives separate TX and RX cipher keys from the master key
5. Cipher contexts are created and the link transitions to READY state
6. The DH context (containing the private key) is destroyed immediately
**Directional key derivation:**
The side with the lexicographically lower public key uses
`masterKey XOR 0xAA` for TX and `masterKey XOR 0x55` for RX. The other
side uses the reverse assignment. This is critical for CTR mode security:
if both sides used the same key and counter, they would produce identical
keystreams, and XORing two ciphertexts would reveal the XOR of the
plaintexts. The XOR-derived directional keys ensure each direction has a
unique keystream even though both sides start their counters at zero.
**Forward secrecy:**
The DH context (containing the private key and shared secret) is
destroyed immediately after deriving the session cipher keys. Even if
the application's long-term state is compromised later, past session
keys cannot be recovered from memory.
## Payload Size
The maximum payload per `secLinkSend()` call is `SECLINK_MAX_PAYLOAD`
(254 bytes). This is the packet layer's 255-byte maximum minus the
1-byte channel header that SecLink prepends.
For sending data larger than 254 bytes, use `secLinkSendBuf()` which
automatically splits the data into 254-byte chunks and sends each one
with blocking delivery.
## API Reference
### Types
```c
// Receive callback -- called for each incoming packet with plaintext
typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel);
// Receive callback -- delivers plaintext with channel number
typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len,
uint8_t channel);
// Opaque connection handle
typedef struct SecLinkS SecLinkT;
```
The receive callback is invoked from `secLinkPoll()` for each incoming
packet. Encrypted packets are decrypted before delivery -- the callback
always receives plaintext regardless of whether encryption was used on
the wire. The `data` pointer is valid only during the callback.
### Constants
| Name | Value | Description |
|-------------------------|-------|----------------------------------------|
|-------------------------|-------|---------------------------------------|
| `SECLINK_MAX_PAYLOAD` | 254 | Max bytes per `secLinkSend()` call |
| `SECLINK_MAX_CHANNEL` | 127 | Highest valid channel number |
| `SECLINK_SUCCESS` | 0 | Operation succeeded |
| `SECLINK_ERR_PARAM` | -1 | Invalid parameter |
| `SECLINK_ERR_SERIAL` | -2 | Serial port error |
| `SECLINK_ERR_PARAM` | -1 | Invalid parameter or NULL pointer |
| `SECLINK_ERR_SERIAL` | -2 | Serial port open failed |
| `SECLINK_ERR_ALLOC` | -3 | Memory allocation failed |
| `SECLINK_ERR_HANDSHAKE` | -4 | Key exchange failed |
| `SECLINK_ERR_HANDSHAKE` | -4 | DH key exchange failed |
| `SECLINK_ERR_NOT_READY` | -5 | Encryption requested before handshake |
| `SECLINK_ERR_SEND` | -6 | Packet layer send failed |
| `SECLINK_ERR_SEND` | -6 | Packet layer send failed or window full |
### Functions
@ -82,9 +159,22 @@ SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity,
SecLinkRecvT callback, void *ctx);
```
Opens the COM port via rs232, creates the packet layer, and returns a
link handle. Returns `NULL` on failure. The callback is invoked from
`secLinkPoll()` for each received packet.
Opens the COM port via rs232, creates the packet layer with default
window size (4), and returns a link handle. The callback is invoked from
`secLinkPoll()` for each received packet (decrypted if applicable).
Returns `NULL` on failure (serial port error, packet layer allocation
error, or memory allocation failure). On failure, all partially
initialized resources are cleaned up.
- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`)
- `bps` -- baud rate (50 through 115200)
- `dataBits` -- 5, 6, 7, or 8
- `parity` -- `'N'`, `'O'`, `'E'`, `'M'`, or `'S'`
- `stopBits` -- 1 or 2
- `handshake` -- `RS232_HANDSHAKE_*` constant
- `callback` -- receive callback function
- `ctx` -- user pointer passed through to the callback
#### secLinkClose
@ -92,8 +182,9 @@ link handle. Returns `NULL` on failure. The callback is invoked from
void secLinkClose(SecLinkT *link);
```
Destroys cipher contexts, closes the packet layer and COM port, and
frees all memory.
Full teardown in order: destroys TX and RX cipher contexts (secure zero),
destroys the DH context if still present, closes the packet layer, closes
the COM port, zeroes the link structure, and frees memory.
#### secLinkHandshake
@ -105,41 +196,17 @@ Performs a Diffie-Hellman key exchange. Blocks until both sides have
exchanged public keys and derived cipher keys. The RNG must be seeded
(via `secRngSeed()` or `secRngAddEntropy()`) before calling this.
Each side derives separate TX and RX keys from the shared secret,
using public key ordering to determine directionality. This prevents
CTR counter collisions.
Internally:
1. Creates a DH context and generates keys
2. Sends the 128-byte public key via the packet layer (blocking)
3. Polls the packet layer in a loop until the remote's public key arrives
4. Computes the shared secret and derives directional cipher keys
5. Destroys the DH context (forward secrecy)
6. Transitions the link to READY state
#### secLinkGetPending
```c
int secLinkGetPending(SecLinkT *link);
```
Returns the number of unacknowledged packets in the transmit window.
Useful for non-blocking send loops to determine if there is room to
send more data.
#### secLinkIsReady
```c
bool secLinkIsReady(SecLinkT *link);
```
Returns `true` if the handshake is complete and the link is ready for
encrypted communication.
#### secLinkPoll
```c
int secLinkPoll(SecLinkT *link);
```
Reads available serial data, processes received frames, handles ACKs
and retransmits. Decrypts encrypted packets and delivers plaintext to
the receive callback. Returns the number of packets delivered, or
negative on error.
Must be called frequently (e.g. in your main loop).
Returns `SECLINK_SUCCESS` or `SECLINK_ERR_HANDSHAKE` on failure
(DH key generation failure, send failure, or serial disconnect during
the exchange).
#### secLinkSend
@ -148,14 +215,24 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len,
uint8_t channel, bool encrypt, bool block);
```
Sends up to `SECLINK_MAX_PAYLOAD` (255) bytes on the given channel.
Sends up to `SECLINK_MAX_PAYLOAD` (254) bytes on the given channel.
- `channel` -- logical channel number (0-127)
- `encrypt` -- if `true`, encrypts the payload (requires completed handshake)
- `block` -- if `true`, waits for transmit window space; if `false`,
returns `SECLINK_ERR_SEND` when the window is full
- `encrypt` -- if `true`, encrypts the payload before sending. Requires
a completed handshake; returns `SECLINK_ERR_NOT_READY` otherwise.
- `block` -- if `true`, waits for transmit window space. If `false`,
returns `SECLINK_ERR_SEND` when the packet layer's window is full.
Cleartext packets (`encrypt = false`) can be sent before the handshake.
**Cipher counter safety:** The function checks transmit window space
BEFORE encrypting the payload. If it encrypted first and then the send
failed, the cipher counter would advance without the data being sent,
permanently desynchronizing the TX cipher state from the remote's RX
cipher. This ordering is critical for correctness.
The channel header byte is prepended to the data, and only the payload
portion (not the header) is encrypted.
Returns `SECLINK_SUCCESS` or an error code.
#### secLinkSendBuf
@ -165,12 +242,54 @@ int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len,
```
Sends an arbitrarily large buffer by splitting it into
`SECLINK_MAX_PAYLOAD`-byte chunks. Always blocks until all data is
sent. Returns `SECLINK_SUCCESS` or the first error encountered.
`SECLINK_MAX_PAYLOAD`-byte (254-byte) chunks. Always blocks until all
data is sent. The receiver sees multiple packets on the same channel and
must reassemble if needed.
## Examples
Returns `SECLINK_SUCCESS` or the first error encountered.
### Basic encrypted link
#### secLinkPoll
```c
int secLinkPoll(SecLinkT *link);
```
Delegates to `pktPoll()` to read serial data, process frames, handle
ACKs and retransmits. Received packets are routed through an internal
callback that:
- During handshake: expects a 128-byte DH public key
- When ready: strips the channel header, decrypts the payload if the
encrypt flag is set, and forwards plaintext to the user callback
Returns the number of packets delivered, or a negative error code.
Must be called frequently (every iteration of your main loop).
#### secLinkGetPending
```c
int secLinkGetPending(SecLinkT *link);
```
Returns the number of unacknowledged packets in the transmit window.
Delegates directly to `pktGetPending()`. Useful for non-blocking send
loops to determine when there is room to send more data.
#### secLinkIsReady
```c
bool secLinkIsReady(SecLinkT *link);
```
Returns `true` if the DH handshake is complete and the link is ready for
encrypted communication. Cleartext sends do not require the link to be
ready.
## Usage Examples
### Basic Encrypted Link
```c
#include "secLink.h"
@ -186,13 +305,14 @@ int main(void) {
secRngGatherEntropy(entropy, sizeof(entropy));
secRngSeed(entropy, sizeof(entropy));
// Open link on COM1 at 115200 8N1
SecLinkT *link = secLinkOpen(0, 115200, 8, 'N', 1, 0, onRecv, NULL);
// Open link on COM1 at 115200 8N1, no flow control
SecLinkT *link = secLinkOpen(RS232_COM1, 115200, 8, 'N', 1,
RS232_HANDSHAKE_NONE, onRecv, NULL);
if (!link) {
return 1;
}
// Key exchange (blocks until both sides complete)
// DH key exchange (blocks until both sides complete)
if (secLinkHandshake(link) != SECLINK_SUCCESS) {
secLinkClose(link);
return 1;
@ -212,31 +332,31 @@ int main(void) {
}
```
### Mixed encrypted and cleartext channels
### Mixed Encrypted and Cleartext Channels
```c
#define CHAN_CONTROL 0 // cleartext control channel
#define CHAN_DATA 1 // encrypted data channel
// Send a cleartext status message (no handshake needed)
// Cleartext status message (no handshake needed)
secLinkSend(link, statusMsg, statusLen, CHAN_CONTROL, false, true);
// Send encrypted payload (requires completed handshake)
// Encrypted payload (requires completed handshake)
secLinkSend(link, payload, payloadLen, CHAN_DATA, true, true);
```
### Non-blocking file transfer
### Non-Blocking File Transfer
```c
int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize,
uint8_t channel, bool encrypt, int windowSize) {
uint8_t channel, bool encrypt) {
int offset = 0;
int bytesLeft = fileSize;
while (bytesLeft > 0) {
secLinkPoll(link); // process ACKs, free window slots
if (secLinkGetPending(link) < windowSize) {
if (secLinkGetPending(link) < 4) { // window has room
int chunk = bytesLeft;
if (chunk > SECLINK_MAX_PAYLOAD) {
chunk = SECLINK_MAX_PAYLOAD;
@ -248,7 +368,7 @@ int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize,
offset += chunk;
bytesLeft -= chunk;
}
// SECLINK_ERR_SEND means window full, just retry next iteration
// SECLINK_ERR_SEND means window full, retry next iteration
}
// Application can do other work here:
@ -264,13 +384,34 @@ int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize,
}
```
### Blocking bulk transfer
### Blocking Bulk Transfer
```c
// Send an entire file in one call (blocks until complete)
secLinkSendBuf(link, fileData, fileSize, CHAN_DATA, true);
```
## Internal State Machine
SecLink maintains a three-state internal state machine:
| State | Value | Description |
|--------------|-------|----------------------------------------------|
| `STATE_INIT` | 0 | Link open, no handshake attempted yet |
| `STATE_HANDSHAKE` | 1 | DH key exchange in progress |
| `STATE_READY` | 2 | Handshake complete, ciphers ready |
Transitions:
- `INIT -> HANDSHAKE`: when `secLinkHandshake()` is called
- `HANDSHAKE -> READY`: when the remote's public key is received and
cipher keys are derived
- Any state -> cleanup: when `secLinkClose()` is called
Cleartext packets can be sent and received in any state. Encrypted
packets require `STATE_READY`.
## Building
```
@ -278,18 +419,32 @@ make # builds ../lib/libseclink.a
make clean # removes objects and library
```
Link against all four libraries:
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
Objects are placed in `../obj/seclink/`, the library in `../lib/`.
Link against all four libraries in this order:
```
-lseclink -lpacket -lsecurity -lrs232
```
## Files
- `secLink.h` -- Public API header (types, constants, function prototypes)
- `secLink.c` -- Complete implementation (handshake, send, receive, state
machine)
- `Makefile` -- DJGPP cross-compilation build rules
## Dependencies
SecLink requires these libraries (all in `../lib/`):
SecLink requires these libraries (all built into `../lib/`):
- `librs232.a` -- serial port driver
- `libpacket.a` -- packet framing and reliability
- `libsecurity.a` -- DH key exchange and XTEA cipher
Target: DJGPP cross-compiler, 486+ CPU.
| Library | Purpose |
|------------------|---------------------------------------------|
| `librs232.a` | Serial port driver (ISR, ring buffers) |
| `libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ |
| `libsecurity.a` | DH key exchange, XTEA-CTR cipher, RNG |

View file

@ -1,55 +1,222 @@
# Security -- DH Key Exchange and XTEA-CTR Cipher
# Security -- Diffie-Hellman Key Exchange and XTEA-CTR Cipher
Cryptographic library providing Diffie-Hellman key exchange, XTEA
symmetric encryption in CTR mode, and a DRBG-based pseudo-random number
generator. Optimized for 486-class DOS hardware running under DJGPP/DPMI.
This library has no dependencies on the serial stack and can be used
independently for any application requiring key exchange, encryption,
or random number generation.
Cryptographic library providing Diffie-Hellman key exchange and XTEA
symmetric encryption, optimized for 486-class DOS hardware running
under DJGPP/DPMI.
## Components
### Diffie-Hellman Key Exchange
### 1. XTEA Cipher (CTR Mode)
- 1024-bit MODP group (RFC 2409 Group 2 safe prime)
- 256-bit private exponents for fast computation on 486 CPUs
- Montgomery multiplication (CIOS variant) for modular exponentiation
- Lazy-initialized Montgomery constants (R^2 mod p, -p0^-1 mod 2^32)
XTEA (eXtended Tiny Encryption Algorithm) is a 64-bit block cipher with a
128-bit key and 32 Feistel rounds. In CTR (counter) mode, it operates as
a stream cipher: an incrementing counter is encrypted with the key to
produce a keystream, which is XOR'd with the plaintext. Because XOR is
its own inverse, the same operation encrypts and decrypts.
### XTEA Cipher (CTR Mode)
**Why XTEA instead of AES or DES:**
- 128-bit key, 64-bit block size, 32 rounds
- CTR mode -- encrypt and decrypt are the same XOR operation
- No lookup tables, no key schedule -- just shifts, adds, and XORs
- Ideal for constrained environments with small key setup cost
XTEA requires zero lookup tables, no key schedule, and compiles to
approximately 20 instructions per round (shifts, adds, and XORs only).
This makes it ideal for a 486 where the data cache is tiny (8KB) and
AES's 4KB S-boxes would thrash it. DES is similarly table-heavy and has
a complex key schedule. XTEA has no library dependencies -- the entire
cipher fits in about a dozen lines of C. At 32 rounds, XTEA provides
128-bit security with negligible per-byte overhead even on the slowest
target hardware.
### Pseudo-Random Number Generator
**CTR mode properties:**
- Encrypt and decrypt are the same function (XOR is symmetric)
- No padding required -- operates on arbitrary-length data
- Random access possible (set the counter to any value)
- CRITICAL: the same counter value must never be reused with the same key.
Reuse reveals the XOR of two plaintexts. The secLink layer prevents this
by deriving separate TX/RX cipher keys for each direction.
**XTEA block cipher internals:**
The Feistel network uses the golden-ratio constant (delta = 0x9E3779B9)
as a round key mixer. Each round combines the two 32-bit halves using
shifts, additions, and XORs. The delta ensures each round uses a
different effective subkey, preventing slide attacks. No S-boxes or lookup
tables are involved anywhere in the computation.
### 2. Diffie-Hellman Key Exchange (1024-bit)
Uses the RFC 2409 Group 2 safe prime (1024-bit MODP group) with a
generator of 2. Private exponents are 256 bits for fast computation on
486-class hardware.
**Why 1024-bit DH with 256-bit private exponents:**
RFC 2409 Group 2 provides a well-audited, interoperable safe prime.
256-bit private exponents (versus full 1024-bit) reduce the modular
exponentiation from approximately 1024 squarings+multiplies to approximately
256 squarings + approximately 128 multiplies (half the exponent bits are 1 on
average). This makes key generation feasible on a 486 in under a second
rather than minutes. The security reduction is negligible -- Pollard's
rho on a 256-bit exponent requires approximately 2^128 operations, matching
XTEA's key strength.
**Key validation:**
`secDhComputeSecret()` validates that the remote public key is in the
range [2, p-2] to prevent small-subgroup attacks. Keys of 0, 1, or p-1
would produce trivially guessable shared secrets.
**Key derivation:**
The 128-byte shared secret is reduced to a symmetric key via XOR-folding:
each byte of the secret is XOR'd into the output key at position
`i % keyLen`. For a 16-byte XTEA key, each output byte is the XOR of
8 secret bytes, providing thorough mixing. A proper KDF (HKDF, etc.)
would be more rigorous but adds complexity and code size for marginal
benefit in this use case.
### 3. Pseudo-Random Number Generator
XTEA-CTR based DRBG (Deterministic Random Bit Generator). The RNG
encrypts a monotonically increasing 64-bit counter with a 128-bit XTEA
key, producing 8 bytes of pseudorandom output per block. The counter
never repeats (64-bit space is sufficient for any practical session
length), so the output is a pseudorandom stream as long as the key has
sufficient entropy.
**Hardware entropy sources:**
- PIT (Programmable Interval Timer) -- runs at 1.193182 MHz. Its LSBs
change rapidly and provide approximately 10 bits of entropy per read,
depending on timing jitter. Two readings with intervening code execution
provide additional jitter.
- BIOS tick count -- 18.2 Hz timer at real-mode address 0040:046C. Adds
a few more bits of entropy.
Total from hardware: roughly 20 bits of real entropy per call to
`secRngGatherEntropy()`. This is not enough on its own for
cryptographic use but is sufficient to seed the DRBG when supplemented
by user interaction timing (keyboard, mouse jitter).
**Seeding and mixing:**
The seed function (`secRngSeed()`) XOR-folds the entropy into the XTEA
key, derives the initial counter from the key bits, and then generates and
discards 64 bytes to advance past any weak initial output. This discard
step is standard DRBG practice -- it ensures the first bytes the caller
receives do not leak information about the seed material.
Additional entropy can be stirred in at any time via `secRngAddEntropy()`
without resetting the RNG state. This function XOR-folds new entropy into
the key and then re-mixes by encrypting the key with itself, diffusing
the new entropy across all key bits.
Auto-seeding: if `secRngBytes()` is called before `secRngSeed()`, it
automatically gathers hardware entropy and seeds itself as a safety net.
## BigNum Arithmetic
All modular arithmetic uses a 1024-bit big number type (`BigNumT`)
stored as 32 x `uint32_t` words in little-endian order. Operations:
| Function | Description |
|----------------|------------------------------------------------------|
| `bnAdd` | Add two bignums, return carry |
| `bnSub` | Subtract two bignums, return borrow |
| `bnCmp` | Compare two bignums (-1, 0, +1) |
| `bnBit` | Test a single bit by index |
| `bnBitLength` | Find the highest set bit position |
| `bnShiftLeft1` | Left-shift by 1, return carry |
| `bnClear` | Zero all words |
| `bnSet` | Set to a 32-bit value (clear upper words) |
| `bnCopy` | Copy from source to destination |
| `bnFromBytes` | Convert big-endian byte array to little-endian words |
| `bnToBytes` | Convert little-endian words to big-endian byte array |
| `bnMontMul` | Montgomery multiplication (CIOS variant) |
| `bnModExp` | Modular exponentiation via Montgomery multiply |
## Montgomery Multiplication
The CIOS (Coarsely Integrated Operand Scanning) variant computes
`a * b * R^(-1) mod m` in a single pass without explicit division by the
modulus. This replaces the expensive modular reduction step (division by a
1024-bit number) with cheaper additions and right-shifts.
For each of the 32 outer iterations (one per word of operand `a`):
1. Accumulate `a[i] * b` into the temporary product `t`
2. Compute the Montgomery reduction factor `u = t[0] * m0inv mod 2^32`
3. Add `u * mod` to `t` and shift right by 32 bits (implicit division)
After all iterations, the result is in the range [0, 2m), so a single
conditional subtraction brings it into [0, m).
**Montgomery constants** (computed once, lazily on first DH use):
- `R^2 mod p` -- computed via 2048 iterations of shift-left-1 with
conditional subtraction. This is the Montgomery domain conversion
factor.
- `-p[0]^(-1) mod 2^32` -- computed via Newton's method (5 iterations,
doubling precision each step: 1->2->4->8->16->32 correct bits). This
is the Montgomery reduction constant.
**Modular exponentiation** uses left-to-right binary square-and-multiply
scanning. For a 256-bit private exponent, this requires approximately 256
squarings plus approximately 128 multiplies (half the bits are 1 on average),
where each operation is a Montgomery multiplication on 32-word numbers.
## Secure Zeroing
Key material (private keys, shared secrets, cipher contexts) is erased
using a volatile-pointer loop:
```c
static void secureZero(void *ptr, int len) {
volatile uint8_t *p = (volatile uint8_t *)ptr;
for (int i = 0; i < len; i++) {
p[i] = 0;
}
}
```
The `volatile` qualifier prevents the compiler from optimizing away the
zeroing as a dead store. Without it, the compiler would see that the
buffer is about to be freed and remove the memset entirely. This is
critical for preventing sensitive key material from lingering in freed
memory where a later `malloc` could expose it.
- XTEA-CTR based DRBG (deterministic random bit generator)
- Hardware entropy from PIT counter (~10 bits) and BIOS tick count
- Supports additional entropy injection (keyboard timing, mouse, etc.)
- Auto-seeds from hardware on first use if not explicitly seeded
## Performance
At serial port speeds, encryption overhead is minimal:
At serial port speeds, XTEA-CTR encryption overhead is minimal:
| Speed | Blocks/sec | CPU cycles/sec | % of 33 MHz 486 |
| Speed | Blocks/sec | CPU Cycles/sec | % of 33 MHz 486 |
|----------|------------|----------------|------------------|
| 9600 | 120 | ~240K | < 1% |
| 57600 | 720 | ~1.4M | ~4% |
| 115200 | 1440 | ~2.9M | ~9% |
DH key exchange takes approximately 0.3s at 66 MHz or 0.6s at 33 MHz
(256-bit private exponent, 1024-bit modulus).
DH key exchange takes approximately 0.3 seconds at 66 MHz or 0.6 seconds
at 33 MHz (256-bit private exponent, 1024-bit modulus, Montgomery
multiplication).
## API Reference
### Constants
| Name | Value | Description |
|---------------------|-------|--------------------------------|
| `SEC_DH_KEY_SIZE` | 128 | DH public key size (bytes) |
| `SEC_XTEA_KEY_SIZE` | 16 | XTEA key size (bytes) |
|---------------------|-------|-----------------------------------|
| `SEC_DH_KEY_SIZE` | 128 | DH public key size in bytes |
| `SEC_XTEA_KEY_SIZE` | 16 | XTEA key size in bytes |
| `SEC_SUCCESS` | 0 | Success |
| `SEC_ERR_PARAM` | -1 | Invalid parameter |
| `SEC_ERR_PARAM` | -1 | Invalid parameter or NULL pointer |
| `SEC_ERR_NOT_READY` | -2 | Keys not yet generated/derived |
| `SEC_ERR_ALLOC` | -3 | Memory allocation failed |
@ -66,31 +233,35 @@ typedef struct SecCipherS SecCipherT; // Opaque cipher context
int secRngGatherEntropy(uint8_t *buf, int len);
```
Reads hardware entropy sources (PIT counter, BIOS tick count). Returns
the number of bytes written. Provides roughly 20 bits of true entropy.
Reads hardware entropy from the PIT counter and BIOS tick count. Returns
the number of bytes written (up to 8). Provides roughly 20 bits of true
entropy -- not sufficient alone, but enough to seed the DRBG when
supplemented by user interaction timing.
```c
void secRngSeed(const uint8_t *entropy, int len);
```
Initializes the DRBG with the given entropy. XOR-folds the input into
the XTEA key, derives the counter, and mixes state by generating and
discarding 64 bytes.
the XTEA key, derives the initial counter, and generates and discards 64
bytes to advance past weak initial output.
```c
void secRngAddEntropy(const uint8_t *data, int len);
```
Mixes additional entropy into the running RNG state without resetting
it. Use this to stir in keyboard timing, mouse jitter, or other
runtime entropy.
Mixes additional entropy into the running RNG state without resetting it.
XOR-folds data into the key and re-mixes by encrypting the key with
itself. Use this to stir in keyboard timing, mouse jitter, or other
runtime entropy sources.
```c
void secRngBytes(uint8_t *buf, int len);
```
Generates `len` pseudo-random bytes. Auto-seeds from hardware if not
previously seeded.
Generates `len` pseudorandom bytes. Auto-seeds from hardware entropy if
not previously seeded. Produces 8 bytes per XTEA block encryption of the
internal counter.
### Diffie-Hellman Functions
@ -98,42 +269,48 @@ previously seeded.
SecDhT *secDhCreate(void);
```
Allocates a new DH context. Returns `NULL` on allocation failure.
Allocates a new DH context. Returns `NULL` on allocation failure. The
context must be destroyed with `secDhDestroy()` when no longer needed.
```c
int secDhGenerateKeys(SecDhT *dh);
```
Generates a 256-bit random private key and computes the corresponding
1024-bit public key (g^private mod p). The RNG should be seeded first.
1024-bit public key (`g^private mod p`). Lazily initializes Montgomery
constants on first call. The RNG should be seeded before calling this.
```c
int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
```
Exports the public key into `buf`. On entry, `*len` must be at least
`SEC_DH_KEY_SIZE` (128). On return, `*len` is set to 128.
Exports the public key as a big-endian byte array into `buf`. On entry,
`*len` must be at least `SEC_DH_KEY_SIZE` (128). On return, `*len` is
set to 128.
```c
int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
```
Computes the shared secret from the remote side's public key.
Validates that the remote key is in range [2, p-2] to prevent
small-subgroup attacks.
Computes the shared secret from the remote side's public key
(`remote^private mod p`). Validates the remote key is in range [2, p-2].
Both sides compute this independently and arrive at the same value.
```c
int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen);
```
Derives a symmetric key by XOR-folding the 128-byte shared secret
down to `keyLen` bytes.
Derives a symmetric key by XOR-folding the 128-byte shared secret down
to `keyLen` bytes. Each output byte is the XOR of `128/keyLen` input
bytes.
```c
void secDhDestroy(SecDhT *dh);
```
Securely zeroes and frees the DH context (private key, shared secret).
Securely zeroes the entire DH context (private key, shared secret, public
key) and frees the memory. Must be called to prevent key material from
lingering in the heap.
### Cipher Functions
@ -141,30 +318,37 @@ Securely zeroes and frees the DH context (private key, shared secret).
SecCipherT *secCipherCreate(const uint8_t *key);
```
Creates an XTEA-CTR cipher context with the given 16-byte key. Counter
starts at zero.
Creates an XTEA-CTR cipher context with the given 16-byte key. The
internal counter starts at zero. Returns `NULL` on allocation failure or
NULL key.
```c
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
```
Encrypts or decrypts `data` in place. CTR mode is symmetric -- the
same operation encrypts and decrypts.
Encrypts or decrypts `data` in place. CTR mode is symmetric -- the same
function handles both directions. The internal counter advances by one
for every 8 bytes processed (one XTEA block). The counter must never
repeat with the same key; callers are responsible for ensuring this
(secLink handles it by using separate cipher instances per direction).
```c
void secCipherSetNonce(SecCipherT *c, uint32_t nonceLo, uint32_t nonceHi);
```
Sets the 64-bit nonce/counter. Call before encrypting if you need a
specific starting counter value.
Sets the 64-bit nonce/counter to a specific value. Both the nonce
(baseline) and the running counter are set to the same value. Call this
before encrypting if you need a deterministic starting point.
```c
void secCipherDestroy(SecCipherT *c);
```
Securely zeroes and frees the cipher context.
Securely zeroes the cipher context (key and counter state) and frees the
memory.
## Example
## Usage Examples
### Full Key Exchange
@ -187,12 +371,12 @@ int pubLen = SEC_DH_KEY_SIZE;
secDhGetPublicKey(dh, myPub, &pubLen);
// ... send myPub to remote, receive remotePub ...
// Compute shared secret and derive a 16-byte key
// Compute shared secret and derive a 16-byte XTEA key
secDhComputeSecret(dh, remotePub, SEC_DH_KEY_SIZE);
uint8_t key[SEC_XTEA_KEY_SIZE];
secDhDeriveKey(dh, key, SEC_XTEA_KEY_SIZE);
secDhDestroy(dh);
secDhDestroy(dh); // private key no longer needed
// Create cipher and encrypt
SecCipherT *cipher = secCipherCreate(key);
@ -200,8 +384,7 @@ uint8_t message[] = "Secret message";
secCipherCrypt(cipher, message, sizeof(message));
// message is now encrypted
// Decrypt (same operation -- CTR mode is symmetric)
// Reset counter first if using the same cipher context
// Decrypt (reset counter first, then apply same operation)
secCipherSetNonce(cipher, 0, 0);
secCipherCrypt(cipher, message, sizeof(message));
// message is now plaintext again
@ -209,10 +392,10 @@ secCipherCrypt(cipher, message, sizeof(message));
secCipherDestroy(cipher);
```
### Standalone Encryption
### Standalone Encryption (Without DH)
```c
// XTEA-CTR can be used independently of DH
// XTEA-CTR can be used independently of Diffie-Hellman
uint8_t key[SEC_XTEA_KEY_SIZE] = { /* your key */ };
SecCipherT *c = secCipherCreate(key);
@ -223,31 +406,24 @@ secCipherCrypt(c, data, sizeof(data)); // encrypt in place
secCipherDestroy(c);
```
## Implementation Details
### Random Number Generation
### BigNum Arithmetic
```c
// Seed from hardware
uint8_t hwEntropy[16];
secRngGatherEntropy(hwEntropy, sizeof(hwEntropy));
secRngSeed(hwEntropy, sizeof(hwEntropy));
All modular arithmetic uses a 1024-bit big number type (`BigNumT`)
stored as 32 x `uint32_t` words in little-endian order. Operations:
// Stir in user-derived entropy (keyboard timing, etc.)
uint8_t userEntropy[4];
// ... gather from timing events ...
secRngAddEntropy(userEntropy, sizeof(userEntropy));
- Add, subtract, compare, shift-left-1, bit test
- Montgomery multiplication (CIOS with implicit right-shift)
- Modular exponentiation (left-to-right binary square-and-multiply)
// Generate random bytes
uint8_t randomBuf[32];
secRngBytes(randomBuf, sizeof(randomBuf));
```
### Montgomery Multiplication
The CIOS (Coarsely Integrated Operand Scanning) variant computes
`a * b * R^-1 mod m` in a single pass with implicit division by the
word base. Constants are computed once on first DH use:
- `R^2 mod p` -- via 2048 iterations of shift-and-conditional-subtract
- `-p[0]^-1 mod 2^32` -- via Newton's method (5 iterations)
### Secure Zeroing
Key material is erased using a volatile-pointer loop that the compiler
cannot optimize away, preventing sensitive data from lingering in
memory.
## Building
@ -256,4 +432,23 @@ make # builds ../lib/libsecurity.a
make clean # removes objects and library
```
Target: DJGPP cross-compiler, 486+ CPU.
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
Objects are placed in `../obj/security/`, the library in `../lib/`.
No external dependencies -- the library is self-contained. It uses only
DJGPP's `<pc.h>`, `<sys/farptr.h>`, and `<go32.h>` for hardware entropy
collection (PIT and BIOS tick count access).
## Files
- `security.h` -- Public API header (types, constants, function prototypes)
- `security.c` -- Complete implementation (bignum, Montgomery, DH, XTEA, RNG)
- `Makefile` -- DJGPP cross-compilation build rules
## Used By
- `seclink/` -- Secure serial link (DH handshake, cipher creation, RNG seeding)

View file

@ -1,46 +1,58 @@
# taskswitch -- Cooperative Task Switching Library for DJGPP
# taskswitch -- Cooperative Task Switching Library
A lightweight cooperative multitasking library targeting DJGPP (i386 protected
mode DOS). Tasks voluntarily yield the CPU with `tsYield()`. A credit-based
weighted round-robin scheduler ensures every task runs while giving
higher-priority tasks proportionally more CPU time.
Cooperative (non-preemptive) multitasking library for DJGPP/DPMI (DOS
protected mode). Part of the DVX GUI project.
Tasks voluntarily yield the CPU by calling `tsYield()`. A credit-based
weighted round-robin scheduler gives higher-priority tasks proportionally
more CPU time while guaranteeing that low-priority tasks are never
starved. A priority-10 task gets 11 turns per scheduling round; a
priority-0 task gets 1 -- but it always runs eventually.
The task array is backed by stb_ds and grows dynamically. Terminated
task slots are recycled, so there is no fixed upper limit on the number
of tasks created over the lifetime of the application.
## Why Cooperative?
DOS is single-threaded. DPMI provides no timer-based preemption. The
DVX GUI event model is inherently single-threaded: one compositor, one
input queue, one window stack. Preemptive switching would require
locking around every GUI call for no benefit. Cooperative switching
lets each task yield at safe points, avoiding synchronization entirely.
The task array grows dynamically using stb_ds and terminated task slots are
recycled, so there is no fixed upper limit on the number of tasks created
over the lifetime of the application.
## Files
| File | Description |
|-----------------------|------------------------------------------|
| `taskswitch.h` | Public API -- types, constants, functions |
| `taskswitch.c` | Implementation |
| `demo.c` | Example program exercising every feature |
| `thirdparty/stb_ds.h` | Dynamic array/hashmap library (stb) |
|-----------------------|----------------------------------------------------|
| `taskswitch.h` | Public API -- types, constants, function prototypes |
| `taskswitch.c` | Implementation (scheduler, context switch, slots) |
| `demo.c` | Standalone test harness exercising all features |
| `thirdparty/stb_ds.h` | stb dynamic array/hashmap library (third-party) |
| `Makefile` | DJGPP cross-compilation build rules |
## Building
Cross-compiling from Linux:
Cross-compile from Linux:
```
make
```
Clean:
```
make clean
make # builds ../lib/libtasks.a
make demo # also builds ../bin/tsdemo.exe
make clean # removes objects, library, and demo binary
```
Output:
| Path | Description |
|-------------------|----------------------------|
|---------------------|----------------------|
| `../lib/libtasks.a` | Static library |
| `../obj/tasks/` | Object files |
| `../bin/tsdemo.exe` | Demo executable |
## Quick Start
```c
@ -69,86 +81,91 @@ int main(void) {
}
```
## Lifecycle
1. **`tsInit()`** -- Initialize the task system. The calling context
(typically `main`) becomes task 0 with `TS_PRIORITY_NORMAL`.
1. `tsInit()` -- Initialize the task system. The calling context
(typically `main`) becomes task 0 with `TS_PRIORITY_NORMAL`. No
separate stack is allocated for task 0 -- it uses the process stack.
2. **`tsCreate(...)`** -- Create tasks. Each gets a name, entry function,
argument pointer, stack size (0 for the 8 KB default), and a priority.
Returns the task ID (>= 0) or a negative error code. Terminated task
slots are reused automatically.
2. `tsCreate(...)` -- Create tasks. Each gets a name, entry function,
argument pointer, stack size (0 for the default), and a priority.
Returns the task ID (>= 0) or a negative error code. Terminated
task slots are reused automatically.
3. **`tsYield()`** -- Call from any task (including main) to hand the CPU to
the next eligible task.
3. `tsYield()` -- Call from any task (including main) to hand the CPU
to the next eligible task. This is the sole mechanism for task
switching.
4. **`tsShutdown()`** -- Free all task stacks and the task array.
4. `tsShutdown()` -- Free all task stacks and the task array.
Tasks terminate by returning from their entry function or by calling
`tsExit()`. The main task (id 0) must never call `tsExit()`. When a task
terminates, its stack is freed immediately and its slot becomes available
for reuse by the next `tsCreate()` call.
`tsExit()`. The main task (id 0) must never call `tsExit()`. When a
task terminates, its stack is freed immediately and its slot becomes
available for reuse by the next `tsCreate()` call.
## API Reference
### Initialisation and Teardown
### Initialization and Teardown
| Function | Signature | Description |
|----------------|------------------------------|--------------------------------------------------------------------------------------|
| `tsInit` | `int32_t tsInit(void)` | Initialise the library. Returns `TS_OK` or a negative error code. |
|--------------|-------------------------|--------------------------------------------------------------------|
| `tsInit` | `int32_t tsInit(void)` | Initialize the library. Returns `TS_OK` or a negative error code. |
| `tsShutdown` | `void tsShutdown(void)` | Free all resources. Safe to call even if `tsInit` was never called. |
### Task Creation and Termination
| Function | Signature | Description |
|------------|----------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
| `tsCreate` | `int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t ss, int32_t pri)` | Create a ready task. Returns the task ID (>= 0) or a negative error code. Pass 0 for `ss` to use `TS_DEFAULT_STACK_SIZE` (8 KB). Reuses terminated task slots when available. |
| `tsExit` | `void tsExit(void)` | Terminate the calling task. Must not be called from the main task. |
| `tsKill` | `int32_t tsKill(uint32_t taskId)` | Forcibly terminate another task. Frees its stack and marks the slot for reuse. Cannot kill the main task (id 0) or the calling task (use `tsExit` instead). |
|------------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
| `tsCreate` | `int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t ss, int32_t pri)` | Create a ready task. Returns the task ID (>= 0) or a negative error code. Pass 0 for `ss` to use `TS_DEFAULT_STACK_SIZE`. Reuses terminated slots. |
| `tsExit` | `void tsExit(void)` | Terminate the calling task. Must not be called from the main task. Never returns. |
| `tsKill` | `int32_t tsKill(uint32_t taskId)` | Forcibly terminate another task. Cannot kill main (id 0) or self (use `tsExit` instead). |
### Scheduling
| Function | Signature | Description |
|-----------|-----------------------|--------------------------------------------------------------------------|
|-----------|----------------------|-----------------------------------------------------------------|
| `tsYield` | `void tsYield(void)` | Voluntarily relinquish the CPU to the next eligible ready task. |
### Pausing and Resuming
| Function | Signature | Description |
|------------|-----------------------------------|--------------------------------------------------------------------------------------------------------------------|
| `tsPause` | `int32_t tsPause(uint32_t id)` | Pause a task. The main task (id 0) cannot be paused. If a task pauses itself, an implicit yield occurs. |
| `tsResume` | `int32_t tsResume(uint32_t id)` | Resume a paused task. Its credits are refilled to `priority + 1` so it is not penalised for having been paused. |
|------------|---------------------------------|------------------------------------------------------------------------------------------------------------|
| `tsPause` | `int32_t tsPause(uint32_t id)` | Pause a task. Main task (id 0) cannot be paused. Self-pause triggers an implicit yield. |
| `tsResume` | `int32_t tsResume(uint32_t id)` | Resume a paused task. Credits are refilled so it is not penalized for having been paused. |
### Priority
| Function | Signature | Description |
|-----------------|--------------------------------------------------|---------------------------------------------------------------------------------------------------|
| `tsSetPriority` | `int32_t tsSetPriority(uint32_t id, int32_t pri)`| Change a task's priority. Credits are reset to `pri + 1` so the change takes effect immediately. |
|-----------------|---------------------------------------------------|-----------------------------------------------------------------------------------|
| `tsSetPriority` | `int32_t tsSetPriority(uint32_t id, int32_t pri)` | Change a task's priority. Credits are reset so the change takes effect immediately. |
| `tsGetPriority` | `int32_t tsGetPriority(uint32_t id)` | Return the task's priority, or `TS_ERR_PARAM` on an invalid ID. |
### Crash Recovery
| Function | Signature | Description |
|-------------------|---------------------------------|-----------------------------------------------------------------------------------------------------------------|
| `tsRecoverToMain` | `void tsRecoverToMain(void)` | Reset scheduler state to the main task (id 0) after a `longjmp` from a signal handler. Call before `tsKill` on the crashed task. This fixes the scheduler's bookkeeping when a non-main task crashes and execution is transferred back to main via `longjmp`. |
|-------------------|------------------------------|------------------------------------------------------------------------------------------------------------------|
| `tsRecoverToMain` | `void tsRecoverToMain(void)` | Reset scheduler state to task 0 after a `longjmp` from a signal handler. Call before `tsKill` on the crashed task. The crashed task's slot is NOT freed -- call `tsKill` afterward. |
### Query
| Function | Signature | Description |
|-----------------|-----------------------------------------|---------------------------------------------------------------|
|-----------------|--------------------------------------|--------------------------------------------------------|
| `tsGetState` | `TaskStateE tsGetState(uint32_t id)` | Return the task's state enum value. |
| `tsCurrentId` | `uint32_t tsCurrentId(void)` | Return the ID of the currently running task. |
| `tsGetName` | `const char *tsGetName(uint32_t id)` | Return the task's name string, or `NULL` on invalid ID. |
| `tsActiveCount` | `uint32_t tsActiveCount(void)` | Return the number of non-terminated tasks. |
## Constants
### Error Codes
| Name | Value | Meaning |
|----------------|-------|--------------------------------------|
|----------------|-------|--------------------------------------------------|
| `TS_OK` | 0 | Success |
| `TS_ERR_INIT` | -1 | Library not initialised |
| `TS_ERR_INIT` | -1 | Library not initialized |
| `TS_ERR_PARAM` | -2 | Invalid parameter |
| `TS_ERR_FULL` | -3 | Task table full (unused, kept for compatibility) |
| `TS_ERR_NOMEM` | -4 | Memory allocation failed |
@ -163,18 +180,21 @@ for reuse by the next `tsCreate()` call.
| `TS_PRIORITY_HIGH` | 10 | 11 |
Any non-negative `int32_t` may be used as a priority. The presets are
provided for convenience.
provided for convenience. In the DVX Shell, the main task runs at
`TS_PRIORITY_HIGH` to keep the UI responsive; app tasks default to
`TS_PRIORITY_NORMAL`.
### Defaults
| Name | Value | Description |
|-------------------------|-------|-------------------------|
| `TS_DEFAULT_STACK_SIZE` | 8192 | Default stack per task |
|-------------------------|-------|------------------------|
| `TS_DEFAULT_STACK_SIZE` | 32768 | Default stack per task |
| `TS_NAME_MAX` | 32 | Max task name length |
## Types
### `TaskStateE`
### TaskStateE
```c
typedef enum {
@ -185,64 +205,83 @@ typedef enum {
} TaskStateE;
```
### `TaskEntryT`
Only Ready tasks participate in scheduling. Running is cosmetic (marks
the currently executing task). Paused tasks are skipped until explicitly
resumed. Terminated slots are recycled by `tsCreate`.
### TaskEntryT
```c
typedef void (*TaskEntryT)(void *arg);
```
The signature every task entry function must follow. `arg` is the pointer
passed to `tsCreate`.
The signature every task entry function must follow. The `arg` parameter
is the pointer passed to `tsCreate`.
## Scheduler Details
The scheduler is a **credit-based weighted round-robin**.
The scheduler is a credit-based weighted round-robin, a variant of the
Linux 2.4 goodness() scheduler.
1. Every ready task holds a credit counter initialised to `priority + 1`.
2. When `tsYield()` is called, the scheduler scans tasks starting one past
the current task (wrapping around) looking for a ready task with
1. Every ready task holds a credit counter initialized to `priority + 1`.
2. When `tsYield()` is called, the scheduler scans tasks starting one
past the current task (wrapping around) looking for a ready task with
credits > 0. When found, that task's credits are decremented and it
becomes the running task.
3. When **no** ready task has credits remaining, every ready task is
refilled to `priority + 1` and the scan repeats.
3. When no ready task has credits remaining, every ready task is
refilled to `priority + 1` (one "epoch") and the scan repeats.
This means a priority-10 task receives 11 turns for every 1 turn a
priority-0 task receives, but the low-priority task still runs -- it is
never starved.
priority-0 task receives, but the low-priority task still runs -- it
is never starved.
Credits are also refilled when:
- A task is **created** (`tsCreate`) -- starts with `priority + 1`.
- A task is **resumed** (`tsResume`) -- refilled so it is not penalised.
- A task's **priority changes** (`tsSetPriority`) -- reset to `new + 1`.
- A task is created (`tsCreate`) -- starts with `priority + 1`.
- A task is resumed (`tsResume`) -- refilled so it runs promptly.
- A task's priority changes (`tsSetPriority`) -- reset to `new + 1`.
## Task Slot Management
The task array is a stb_ds dynamic array that grows automatically as needed.
The task array is a stb_ds dynamic array that grows automatically.
Each slot has an `allocated` flag:
- **`tsCreate()`** scans for the first unallocated slot (starting at index 1,
since slot 0 is always the main task). If no free slot exists, the array
is extended with `arrput()`.
- **`tsExit()`** frees the terminated task's stack immediately and marks the
slot as unallocated, making it available for the next `tsCreate()` call.
- Task IDs are stable array indices. Slots are never removed or reordered,
so a task ID remains valid for queries until the slot is recycled.
- `tsCreate()` scans for the first unallocated slot (starting at index
1, since slot 0 is always the main task). If no free slot exists, the
array is extended with `arrput()`.
- `tsExit()` and `tsKill()` free the terminated task's stack immediately
and mark the slot as unallocated, making it available for the next
`tsCreate()` call.
- Task IDs are stable array indices. Slots are never removed or
reordered, so a task ID remains valid for queries until the slot is
recycled.
This supports long-running applications (like the DVX Shell) that
create and destroy many tasks over their lifetime without unbounded
memory growth.
This design supports long-running applications that create and destroy
many tasks over their lifetime without unbounded memory growth.
## Context Switch Internals
Context switching is performed entirely in inline assembly with both i386
and x86_64 code paths.
Context switching uses inline assembly with both i386 and x86_64 code
paths. The `contextSwitch` function is marked `noinline` to preserve
callee-saved register assumptions.
Why inline asm instead of setjmp/longjmp: setjmp/longjmp only save
callee-saved registers and do not give control over the stack pointer
in a portable way. New tasks need a fresh stack with the instruction
pointer set to a trampoline -- setjmp cannot bootstrap that. The asm
approach also avoids ABI differences in jmp_buf layout across DJGPP
versions.
### i386 (DJGPP target)
Six callee-saved values are saved and restored per switch:
| Register | Offset | Purpose |
|----------|--------|-----------------------------------------|
|----------|--------|------------------------------------------|
| EBX | 0 | Callee-saved general purpose |
| ESI | 4 | Callee-saved general purpose |
| EDI | 8 | Callee-saved general purpose |
@ -250,12 +289,12 @@ Six callee-saved values are saved and restored per switch:
| ESP | 16 | Stack pointer |
| EIP | 20 | Resume address (captured as local label) |
### x86_64 (for native testing)
### x86_64 (for native Linux testing)
Eight callee-saved values are saved and restored per switch:
| Register | Offset | Purpose |
|----------|--------|-----------------------------------------|
|----------|--------|------------------------------------------|
| RBX | 0 | Callee-saved general purpose |
| R12 | 8 | Callee-saved general purpose |
| R13 | 16 | Callee-saved general purpose |
@ -265,42 +304,54 @@ Eight callee-saved values are saved and restored per switch:
| RSP | 48 | Stack pointer |
| RIP | 56 | Resume address (RIP-relative lea) |
The save and restore pointers are passed into the assembly block via GCC
register constraints. Segment registers are not saved because DJGPP runs
in a flat protected-mode environment where CS, DS, ES, and SS share the
same base.
Segment registers are not saved because DJGPP runs in a flat
protected-mode environment where CS, DS, ES, and SS share the same
base.
New tasks have their initial stack pointer set to a 16-byte-aligned
region at the top of a malloc'd stack, with the instruction pointer
set to an internal trampoline that calls the user's entry function
and then `tsExit()`.
New tasks have their initial stack pointer set to a 16-byte-aligned region
at the top of a `malloc`'d stack, with the instruction pointer set to an
internal trampoline that calls the user's entry function and then `tsExit()`.
## Limitations
- **Cooperative only** -- tasks must call `tsYield()` (or `tsPause`/`tsExit`)
to allow other tasks to run. A task that never yields blocks everything.
- **Not interrupt-safe** -- the library uses no locking or `volatile` module
state. Do not call library functions from interrupt handlers.
- **Cooperative only** -- tasks must call `tsYield()` (or
`tsPause`/`tsExit`) to allow other tasks to run. A task that never
yields blocks everything.
- **Not interrupt-safe** -- no locking or volatile module state. Do not
call library functions from interrupt handlers.
- **Single-threaded** -- designed for one CPU under DOS protected mode.
- **Stack overflow is not detected** -- size the stack appropriately for each
task's needs.
- **Stack overflow is not detected** -- size the stack appropriately for
each task's needs.
## Demo
`demo.c` exercises five phases:
1. **Priority scheduling** -- creates tasks at low, normal, and high priority.
All tasks run, but the high-priority task gets significantly more turns.
2. **Pause** -- pauses one task mid-run and shows it stops being scheduled.
1. **Priority scheduling** -- creates tasks at low, normal, and high
priority. All tasks run, but the high-priority task gets significantly
more turns.
2. **Pause** -- pauses one task mid-run and shows it stops being
scheduled.
3. **Resume** -- resumes the paused task and shows it picks up where it
left off.
4. **Priority boost** -- raises the low-priority task above all others and
shows it immediately gets more turns.
5. **Slot reuse** -- creates three waves of short-lived tasks that terminate
and shows subsequent waves reuse the same task IDs.
4. **Priority boost** -- raises the low-priority task above all others
and shows it immediately gets more turns.
5. **Slot reuse** -- creates three waves of short-lived tasks that
terminate and shows subsequent waves reuse the same task IDs.
Build and run:
```
make
make demo
tsdemo
```
## Third-Party Dependencies
- **stb_ds.h** (Sean Barrett) -- dynamic array and hashmap library.
Located in `thirdparty/stb_ds.h`. Used for the task control block
array. Public domain / MIT licensed.

View file

@ -1,9 +1,17 @@
# SecLink Terminal Demo
DOS terminal emulator combining the DVX windowed GUI with SecLink
encrypted serial communication. Connects to a remote BBS through the
SecLink proxy, providing a full ANSI terminal in a DVX-style
window with encrypted transport.
Standalone DOS terminal emulator combining the DVX windowed GUI with
SecLink encrypted serial communication. Part of the DVX GUI project.
This is NOT a DXE app -- it is a freestanding program with its own
`main()` that initializes the DVX GUI directly and manages its own
event loop. Unlike the DXE apps (progman, notepad, clock, dvxdemo)
which run inside the DVX Shell, this program demonstrates how to use
the DVX widget system outside the shell framework.
Connects to a remote BBS through the SecLink proxy, providing a full
ANSI terminal in a DVX-style window with encrypted transport.
## Architecture
@ -14,7 +22,7 @@ termdemo (DOS, 86Box)
|
+--- SecLink encrypted serial link
| |
| +--- packet HDLC framing, CRC, retransmit
| +--- packet HDLC framing, CRC-16, Go-Back-N ARQ
| +--- security DH key exchange, XTEA-CTR cipher
| +--- rs232 ISR-driven UART I/O
|
@ -33,6 +41,7 @@ All traffic between the terminal and the proxy is encrypted via
XTEA-CTR on SecLink channel 0. The proxy decrypts and forwards
plaintext to the BBS over telnet.
## Usage
```
@ -40,10 +49,12 @@ termdemo [com_port] [baud_rate]
```
| Argument | Default | Description |
|-------------|---------|------------------------------|
|-------------|---------|--------------------------|
| `com_port` | 1 | COM port number (1-4) |
| `baud_rate` | 115200 | Serial baud rate |
Examples:
```
termdemo # COM1 at 115200
termdemo 2 # COM2 at 115200
@ -51,25 +62,33 @@ termdemo 1 57600 # COM1 at 57600
termdemo -h # show usage
```
## Startup Sequence
1. Seed the RNG from hardware entropy
2. Open SecLink on the specified COM port (8N1, no handshake)
3. Perform DH key exchange (blocks until the proxy completes its side)
4. Initialize the DVX GUI (1024x768, 16bpp VESA)
5. Create a resizable terminal window with menu bar and status bar
6. Enter the main loop
1. Seed the RNG from hardware entropy (PIT-based on DOS).
2. Open SecLink on the specified COM port (8N1, no handshake).
3. Perform DH key exchange (blocks until the proxy completes its side).
4. Initialize the DVX GUI (1024x768, 16bpp VESA).
5. Create a resizable terminal window with menu bar and status bar.
6. Register an idle callback so serial data is polled during GUI idle.
7. Enter the main loop.
The handshake completes in text mode before the GUI starts, so the
DOS console shows progress messages during connection setup.
## Main Loop
Each iteration:
1. `dvxUpdate()` -- process mouse, keyboard, paint, and window events
2. `secLinkPoll()` -- read serial data, decrypt, deliver to ring buffer
3. `wgtAnsiTermPoll()` -- drain ring buffer into the ANSI parser
1. `secLinkPoll()` -- read serial data, decrypt, deliver to ring buffer.
2. `dvxUpdate()` -- process mouse, keyboard, paint, and window events.
During paint, the terminal widget calls `commRead` to drain the ring
buffer and render new data.
An idle callback also calls `secLinkPoll()` so incoming data is
processed even when the user is not interacting with the terminal.
## Data Flow
@ -81,9 +100,13 @@ Keyboard -> widgetAnsiTermOnKey() -> commWrite()
-> secLinkSend() -> serial -> proxy -> BBS
```
A 4KB ring buffer bridges the SecLink receive callback (which fires
during `secLinkPoll()`) and the terminal widget's comm read interface
(which is polled by `wgtAnsiTermPoll()`).
A 4KB ring buffer (`RECV_BUF_SIZE`) bridges the SecLink receive
callback (which fires asynchronously during `secLinkPoll()`) and the
terminal widget's comm read interface (which is polled synchronously
during the widget paint cycle). This decoupling is necessary because
the callback can fire at any time during polling, but the terminal
widget expects to read data synchronously.
## GUI
@ -97,6 +120,7 @@ cursor control, SGR colors (16-color CGA palette), erase, scroll,
insert/delete lines, and DEC private modes (cursor visibility, line
wrap).
## Test Setup
1. Start the SecLink proxy on the Linux host:
@ -106,7 +130,7 @@ wrap).
```
2. Configure 86Box with a COM port pointing at the proxy's listen port
(TCP client mode, port 2323, no telnet negotiation)
(TCP client mode, port 2323, no telnet negotiation).
3. Run the terminal inside 86Box:
@ -115,21 +139,26 @@ wrap).
```
4. The handshake completes, the GUI appears, and BBS output is
displayed in the terminal window
displayed in the terminal window.
## Building
```
make # builds ../bin/termdemo.exe
make # builds ../bin/termdemo.exe (and all dependency libs)
make clean # removes objects and binary
```
The Makefile builds all dependency libraries automatically. Objects
are placed in `../obj/termdemo/`, the binary in `../bin/`.
The Makefile automatically builds all dependency libraries before
linking. Objects are placed in `../obj/termdemo/`, the binary in
`../bin/`.
Target: DJGPP cross-compiler, 486+ CPU, VESA VBE 2.0+ video.
## Dependencies
All libraries are in `../lib/`:
All libraries are built into `../lib/`:
| Library | Purpose |
|------------------|--------------------------------------|
@ -139,7 +168,6 @@ All libraries are in `../lib/`:
| `libsecurity.a` | DH key exchange and XTEA cipher |
| `librs232.a` | ISR-driven UART serial driver |
Target: DJGPP cross-compiler, 486+ CPU, VESA VBE 2.0+ video.
## Files