Legacy ISA Hardware Pass-Through via Custom QEMU/KVM Proxy
This documentation outlines the complete engineering pipeline required to bridge a legacy CIO-DAS08 ISA Data Acquisition Card into a virtualized Windows 98 SE / MS-DOS environment running on a modern Linux host machine with MBATX-CS620-H310C motherboard from DFI. It details the underlying hardware mapping architecture, the build process for a custom QEMU binary, deployment strategies, and validation protocols.
1. Hardware Architecture & Electrical Blueprint Map
Modern motherboards lack physical ISA slots. To bridge this gap, the target host machine utilizes a PCIe-to-ISA hardware bridge adapter (e.g., using an ITE or Winbond bridge controller).
The bridge maps the legacy 10-bit ISA I/O space into the host’s modern PCIe I/O Base Address Register (BAR) window.
+-----------------------------------------------------------------------+
| WINDOWS 98 GUEST VM |
| Accesses legacy ISA Ports: 0x390 - 0x39F |
+------------------------------------+----------------------------------+
| (Intercepted by QEMU Memory Region)
v
+-----------------------------------------------------------------------+
| CUSTOM QEMU ISA PROXY |
| - Tracks memory region offset: addr (0 to 15) |
| - Dynamically calculates host port: 0x5390 + addr |
| - Executes raw hardware assembly: inb / outb |
+------------------------------------+----------------------------------+
| (Kernel space execution via iopl)
v
+-----------------------------------------------------------------------+
| UBUNTU LINUX HOST KERNEL |
| Directly routes outb/inb commands down the PCIe Bus BAR Window |
+------------------------------------+----------------------------------+
|
v
+-----------------------------------------------------------------------+
| PHYSICAL PCIe-TO-ISA BRIDGE |
| Translates Host Port 0x5390 directly into physical ISA Bus 0x390 |
+------------------------------------+----------------------------------+
|
v
+-----------------------------------------------------------------------+
| CIO-DAS08 PHYSICAL ISA CARD |
| Responds across 8-port register layout block |
+-----------------------------------------------------------------------+
Physical-to-Virtual Address Mapping Layout
- Guest Base Address:
0x390(Configured via the card’s physical DIP switches). - Host Translation Base:
0x5390(Assigned by the host PCIe bus architecture). - Address Space Window: 16 Bytes (
0x390to0x39F).
2. Environment Setup & Dependency Workarounds
Building older or custom versions of QEMU on clean, modern Ubuntu machines requires resolving deprecated dependency packages and handling obsolete git registry submodules.
Step 2.1: Install Core Build Toolchain & Dependencies
Run the following commands to provision the build environment:
sudo apt update
sudo apt install -y git build-essential python3-ninja ninja-build \
libglib2.0-dev libpixman-1-dev libpciaccess-dev libusb-1.0-0-dev \
libsdl2-dev libgtk-3-dev libspice-server-dev libcap-ng-dev \
libattr1-dev libnuma-dev bison flex
Step 2.2: Mitigate “Old Git Registry” / Submodule Errors
Older checkouts of QEMU source code often point to dead or migrated Git URLs for mandatory third-party submodules (like dtc, slirp, or keycodemapdb).
If git submodule update --init --recursive fails due to broken URLs, apply this structural workaround:
- Open the project’s root
.gitmodulesconfiguration file. - Replace any instances of
git://git.qemu.org/with the modern HTTPS mirror:https://gitlab.com/qemu-project/. - Force a sync and update:
git submodule sync
git submodule update --init --recursive
3. Custom QEMU Code Implementation
The custom device model intercepts I/O commands destined for guest ports 0x390–0x39F, dynamically calculates the matching 0x5390 host offset, and safely drops into ring-0 hardware privilege using iopl(3) to execute the instructions via inline x86 assembly.
Locate your custom C device file or implementation block within the QEMU architecture tree (e.g., hw/misc/host-isa-proxy.c) and structure the implementation exactly as follows:
#include "qemu/osdep.h"
#include "hw/sysbus.h"
#include "exec/memory.h"
#include <sys/io.h>
#include <stdbool.h>
/* Dynamic Read Callback */
static uint64_t host_isa_proxy_read(void *opaque, hwaddr addr, unsigned size)
{
static __thread bool iopl_unlocked = false;
if (!iopl_unlocked) {
if (iopl(3) < 0) {
perror("ISA PROXY ERROR: iopl privilege escalation failed");
return 0xFF;
}
iopl_unlocked = true;
}
uint8_t value;
uint16_t host_port = 0x5390 + addr; // Map guest offset to physical host register
// Execute hardware level x86 assembly read instruction
asm volatile("inb %%dx, %0" : "=a"(value) : "d"(host_port));
// Log explicitly to standard error stream
fprintf(stderr, "ISA PROXY: Guest read at offset 0x%lx -> Target Host Port: 0x%x -> Value: 0x%02X\n",
(unsigned long)addr, host_port, value);
fflush(stderr); // Force immediate flush past daemon buffering layers
return value;
}
/* Dynamic Write Callback */
static void host_isa_proxy_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
static __thread bool iopl_unlocked = false;
if (!iopl_unlocked) {
if (iopl(3) < 0) {
perror("ISA PROXY ERROR: iopl privilege escalation failed");
return;
}
iopl_unlocked = true;
}
uint8_t value = (uint8_t)val;
uint16_t host_port = 0x5390 + addr; // Map guest offset to physical host register
// Execute hardware level x86 assembly write instruction
asm volatile("outb %0, %%dx" : : "a"(value), "d"(host_port));
fprintf(stderr, "ISA PROXY: Guest write at offset 0x%lx -> Target Host Port: 0x%x -> Data: 0x%02X\n",
(unsigned long)addr, host_port, value);
fflush(stderr); // Force immediate flush past daemon buffering layers
}
/* Bind Read/Write Callbacks */
static const MemoryRegionOps host_isa_proxy_ops = {
.read = host_isa_proxy_read,
.write = host_isa_proxy_write,
.endianness = DEVICE_LITTLE_ENDIAN,
.impl = {
.min_access_size = 1,
.max_access_size = 1,
},
};
/* Device Initialization & Mapping Window Size Definition */
void host_isa_proxy_init(MemoryRegion *raw_isa)
{
// CRITICAL: Window size must be initialized to 16 bytes to capture full 0x390-0x39F block
memory_region_init_io(raw_isa, NULL, &host_isa_proxy_ops, NULL, "physical-isa-card", 16);
}
4. Compilation & Deployment
Step 4.1: Compile the Custom System Binary
Configure and build only the required x86_64 virtualization targets to speed up compilation time:
mkdir build
cd build
../configure --target-list=x86_64-softmmu --enable-kvm --enable-gtk --enable-sdl
make -j$(nproc)
Step 4.2: Move and Register the Binary on the Target Host
- Transfer the compiled
qemu-system-x86_64binary to the host machine via secure network copy or standard USB media storage. - Store the binary in a distinct local directory to prevent accidental package manager overwrites:
sudo cp qemu-system-x86_64 /usr/local/bin/qemu-system-x86_64-custom
sudo chmod +x /usr/local/bin/qemu-system-x86_64-custom
Step 4.3: Bind Custom Path within Libvirt (Virtual Machine Manager)
If utilizing Libvirt/VMM to manage the Windows 98 guest machine, alter the execution configuration:
sudo virsh edit Win98_VM_Name
Locate the <devices> element block and explicitly update the <emulator> tag to prioritize your custom proxy pipeline:
<devices>
<emulator>/usr/local/bin/qemu-system-x86_64-custom</emulator>
...
</devices>
Verify the active runtime parameters once execution begins:
ps -ef | grep qemu-system-x86_64-custom
5. Verification & Testing Protocol
To ensure full bi-directional pass-through behavior without loading external driver payloads, use the vintage native MS-DOS debug environment.
Step 5.1: Host Side Live Log Monitoring
Before issuing commands inside the guest machine, establish a live trace interface on the Ubuntu Host to bypass systemd tracking blocks:
sudo tail -f /var/log/libvirt/qemu/*.log
Step 5.2: Executing MS-DOS Debug Sweeps
Boot the Windows 98 guest environment into an MS-DOS prompt window. Initialize the low-level utility tool:
C:\> debug
CRITICAL SYNTAX NOTE: Vintage versions of
debug.exeparse spaces strictly. Separate the target I/O port address and data byte payload using a comma (o port,data).
Execute an sequential sweeping check of the card’s assigned registers:
-i 390
-i 391
-i 392
-i 394
-i 396
-i 397
Step 5.3: Interpreting the Diagnostics Data Profile
If the architecture pipeline is fully intact, the hardware registers will match the factory profile shown below:
-i 390 --> Returns 00 (ADC Low Byte: Expected idle condition state)
-i 391 --> Returns 00 (ADC High Byte: Expected idle condition state)
-i 392 --> Returns 77 (Status Flag: Binary 01110111, indicating steady digital inputs)
-i 394 --> Returns FD (Intel 8254 Counter 0: Live fluctuating value proving clock ticks)
-i 396 --> Returns 00 (Intel 8254 Counter 2: Stable, unassigned background counter clock)
-i 397 --> Returns FF (8254 Control Register: Write-only register; reading forces open-bus floating)
Step 5.4: Verifying Complete Hardware Write Execution
To verify full outbound control pathing to the physical card, attempt to cycle the internal Multiplexer (MUX) configuration lines:
-o 392,00 <-- Sets MUX targeting Analog Channel 0
-i 392 <-- Read status line results back
-o 392,07 <-- Sets MUX targeting Analog Channel 7
-i 392 <-- Confirm changes register on the board hardware
Your host log output will capture every single execution step simultaneously, verifying that your software logic and hardware connection are fully operational.