Skip to content
FreeDOSDeep Dive Published Updated 7 min readViews unavailable

Detecting CPUID and CPU Features in a Real-Mode DOS Program

Gate CPUID safely, enumerate only supported leaves, and translate processor feature bits into conservative DOS execution requirements.

An old DOS executable can run on processors ranging from the 8086 family through modern x86 systems, but code generation does not automatically make that range safe. A binary that executes an instruction unsupported by the current CPU can raise an invalid-opcode exception or behave unpredictably on processors without that exception mechanism. CPUID gives software a structured way to query processor capabilities, but only after the program has established that the instruction itself exists.

Intel documents the ID bit, bit 21 of EFLAGS, as the architectural indicator: if software can set and clear it, the processor supports CPUID. The classic test uses PUSHFD/POPFD, which require a 386-or-later execution baseline. That fact is easy to miss in DOS code intended to support 8086 or 286 machines. A program with that target range must first use a pre-386-safe processor-generation check or provide a separate legacy path before executing 32-bit flag instructions.

Establish the minimum CPU before the probe

On a known 386-or-later baseline, save flags, toggle only the ID bit in the saved image, restore the image, and read flags back. Compare the ID bit; then restore the original flags so the detection code does not leak altered condition or control state. In 16-bit assembly with 386 instructions enabled, the shape is:

        pushfd
        pop     eax
        mov     ecx, eax          ; preserve original EFLAGS image
        xor     eax, 00200000h    ; toggle ID, bit 21
        push    eax
        popfd
        pushfd
        pop     eax               ; read back the candidate flags
        push    ecx
        popfd                     ; restore caller's original flags
        xor     eax, ecx
        test    eax, 00200000h
        jz      no_cpuid

This sketch assumes the code is already executing on a CPU where PUSHFD and POPFD are legal and the operating environment allows the relevant flags to be changed. It is not a universal 8086-safe detector. On a 286, the available flag width and behavior differ; an 8086 does not have the instruction at all. If supporting those processors matters, use a separately verified generation probe that is safe for the earliest CPU in the product matrix, or ship a build with a declared minimum CPU instead of executing a modern probe speculatively.

After detecting CPUID, invoke it only with leaves supported by the processor. EAX=0 returns the maximum basic leaf in EAX and the vendor string split across EBX, EDX, and ECX. Verify the maximum before querying a numbered basic leaf. Extended leaves use their own maximum-leaf query. Do not infer that every processor implements every leaf because one leaf exists; reserved or unsupported leaves are not a feature-negotiation API.

Convert feature bits into decisions, not labels

Feature bits are inputs to a capability decision. A game might need a specific instruction set, while a memory manager or extender may have different constraints. Check the exact feature bit documented for the exact leaf and subleaf, preserve the registers required by the calling convention, and include a conservative fallback when any check fails. Avoid saying “Pentium compatible” when the binary actually requires a particular CPUID feature set; processor marketing families are not a reliable ABI.

The vendor string and family/model/stepping fields are useful for diagnostics, but they should not usually drive correctness. Emulators and virtual machines expose a virtual processor model, and hypervisors may hide, mask, or synthesize capabilities. A guest’s CPUID data describes what the guest may execute in that environment, not necessarily the physical host’s complete feature set. Similarly, a later microcode update or hypervisor configuration can affect visible capabilities; software must follow the architectural enumeration rather than a cached model-name table.

Use feature checks at a deliberate boundary. Detect once during startup, store a compact capability record, and ensure every optimized routine has a fallback. If a routine dispatches to an optimized implementation, keep the dispatch condition next to the documented instruction requirement and test both branches. Avoid patching machine code based only on vendor strings or assuming the CPU won’t change across a virtual-machine migration.

Remember that the basic-leaf maximum and the feature-bit namespace are separate facts. Query leaf zero first, then query leaf one only if the returned maximum is at least one. For structured extended feature leaves, query the documented maximum and the necessary subleaf before interpreting any output bits. Unsupported leaf output can be reserved, undefined, or virtualized; do not treat a zero-looking result from an unguarded query as a universal architectural guarantee. Store the exact leaf/subleaf and bit used by each feature decision so maintenance does not turn a nearby bit into an accidental dependency.

A diagnostic report should separate raw facts from the product’s decision. For example, print “CPUID present,” “maximum basic leaf,” and “required feature bit set” independently, then say which implementation was selected. That makes failures reproducible when a game works on the host but not in a VM. It also helps distinguish a missing CPUID instruction from a missing optional extension: the remedy for the former may be a lower-baseline executable, while the latter may simply be the baseline code path.

Do not cache a feature report as if it were a hardware serial number or permanent machine identity. Hypervisors can present a stable virtual CPU profile across different hosts, and operators can deliberately mask individual features for compatibility. This is useful for migration but means the report describes the process-visible execution contract. If the program saves a capability record to disk, include the environment and build version and revalidate before executing optimized code on a later boot.

Real-mode and extender considerations

Real mode changes the programming environment, not the CPUID register contract. A 16-bit DOS program can execute 32-bit instructions on a suitable processor by using the appropriate assembler encoding, but the assembler’s .386 or equivalent setting is not runtime detection. It tells the assembler what to emit. It does not protect the executable from a 286 that cannot execute the emitted opcodes.

Under a DPMI host, a client may run in protected mode and the host may virtualize or mediate some operations. CPUID is commonly available to applications, but software should still treat the result as the processor view supplied by the execution environment. If an extender has its own documented CPU-detection API, follow that interface; do not assume a real-mode interrupt or unrestricted I/O model applies to protected-mode code.

Keep the probe small and auditable. Save and restore state, document the minimum instruction baseline, and avoid running the test inside a timer or hardware interrupt handler. If the program has a command-line diagnostics mode, print the raw CPUID maximum leaf, vendor string, and relevant feature bits alongside the derived decision so a support report can be reproduced.

Failure cases worth testing

At minimum, test the declared minimum CPU, one CPU with CPUID but without each optional feature, and one virtualized environment that masks features. If the application claims 8086 compatibility, test the pre-386 detection path on an emulator configured for an 8086 or 286; a successful run on a modern CPU says nothing about that path. Test that every unsupported-leaf branch chooses a safe fallback and never consumes stale output registers from an earlier CPUID call.

Also test the assembler output. Disassemble the binary and confirm that no instruction introduced by the probe appears before the minimum-CPU gate. Review compiler-generated startup code and linked libraries too; a hand-written safe probe cannot compensate for an object file that already contains instructions newer than the declared baseline.

A conservative compatibility policy

Document both the minimum processor generation and optional feature requirements. If CPUID is unavailable, use the baseline implementation or refuse to start with a precise message. If a leaf is unavailable, treat its features as absent. Never equate a family/model value with a guarantee about instruction support, and never mistake hypervisor-visible features for a permanent physical-machine identity.

That policy is especially important for archival DOS tools: the program may be launched on hardware very different from the machine on which it was built. CPUID provides a standardized query mechanism; safe use still depends on an instruction-safe entry path, bounded leaf enumeration, and conservative dispatch.

Related:

Sources:

Comments