Now Reading
A Check Suite for the Intel 8088

A Check Suite for the Intel 8088

2023-09-04 16:49:58

In a earlier article, I revealed one of many secrets and techniques to MartyPC’s accuracy, hardware validation. To summarize, it’s a methodology of utilizing a microcontroller to run an instruction on an actual 8088 CPU concurrently the emulator executes the identical an instruction, after which evaluating the ensuing cycle states (and optionally registers) for consistency.

As soon as once more, credit score goes to phix and his VirtualXT emulator for pioneering this fundamental concept. 

The draw back to this methodology is that it’s fairly sluggish, and requires going out and shopping for a Raspberry Pi or an Arduino, ordering some classic chips off eBay, then both breadboarding up a CPU or ordering a customized PCB and soldering it collectively, to not point out the the trouble of integrating consumer communication and check logic into your emulator.  All this presents a barrier to entry that makes this methodology in all probability solely appropriate for an particularly devoted few. 

Compounding issues, it is probably not sensible to validate directions that ceaselessly. Each time you tweak one thing as basic as BIU logic, you could validate your emulator another time, and validating all the ISA can take many hours.  

In that very same weblog publish I bemoaned the shortage of a JSON check suite for the 8088 CPU. I am blissful to announce that as of now, the 8088 not lacks a check suite! With the assistance of my good friend Folkert van Heusden, we’ve got taken three Arduino8088 {hardware} boards and spent the previous week or so producing the primary complete JSON CPU test suite for the 8088

My two Arduino8088 boards busy producing assessments

JSON CPU Exams

A little bit of a background if you happen to’re not conversant in the idea of a JSON CPU check suite. The thought was initially popularized by Tom Harte, so far as I can inform. JSON is a extremely moveable information format with available parsers in nearly each language, saving emulator builders the difficulty of writing a customized parser for a binary format. JSON is not probably the most environment friendly storage format, so these JSON recordsdata is usually a massive weighty – bigger check units are gzipped to maintain the repository sizes cheap.

Every JSON file usually corresponds to a single opcode, and incorporates an array of randomized assessments that embody the preliminary and ultimate states of reminiscence and CPU registers, and an array of knowledge for every cycle spent executing that opcode.

Tom’s CPU tests have been very properly obtained within the emulation growth group, giving a precious and handy approach to shortly find and squash emulation bugs which may in any other case take weeks of debugging to catch. Ever since growing the Arduino8088, I spotted that utilizing it to generate a set of assessments was theoretically doable if I put within the work.

Since Tom popularized the idea, and these assessments observe the identical fundamental format as his different assessments, it solely appeared honest to contribute the 8088 CPU check suite to Tom’s repository. 

The 8088 Check Suite

The 8088 Check Suite is pretty complete, exercising 10,000 assessments not simply on the opcode degree however on the opcode extension degree as properly, with solely a handful of opcodes omitted resulting from practicality (WAIT, HALT) or unpredictability (largely sure undefined opcode types).

The check suite incorporates 324 opcode types, over 3 million instruction assessments in complete, together with practically 90 million cycle states. Compressed, the gathering weighs in at 677 MB. The uncompressed, pretty-printed JSON totals 9.68GB.

That is a number of check information!

Instance Check

Here is an instance check entry:

{
    "identify": "add byte ds:[bx+si+C2h], al",
    "bytes": [0, 64, 194],
    "preliminary": {
        "regs": {
            "ax": 52773,
            "bx": 22214,
            "cx": 16054,
            "dx": 57938,
            "cs": 60492,
            "ss": 17184,
            "ds": 15619,
            "es": 60510,
            "sp": 56738,
            "bp": 13363,
            "si": 58400,
            "di": 31158,
            "ip": 16937,
            "flags": 62535
        },
        "ram": [
            [264920, 71],
            [984809, 0],
            [984810, 64],
            [984811, 194],
            [984812, 144],
            [984813, 144],
            [984814, 144],
            [984815, 144]
        ],
        "queue": []
    },
    "ultimate": {
        "regs": {
            "ax": 52773,
            "bx": 22214,
            "cx": 16054,
            "dx": 57938,
            "cs": 60492,
            "ss": 17184,
            "ds": 15619,
            "es": 60510,
            "sp": 56738,
            "bp": 13363,
            "si": 58400,
            "di": 31158,
            "ip": 16940,
            "flags": 62470
        },
        "ram": [
            [264920, 108],
            [984809, 0],
            [984810, 64],
            [984811, 194],
            [984812, 144],
            [984813, 144],
            [984814, 144],
            [984815, 144]
        ],
        "queue": [144, 144, 144]
    },
    "cycles": [
        ["-", 984810, "CS", "R--", "---", 0, "CODE", "T2", "F", 0],
        ["-", 984810, "CS", "R--", "---", 64, "PASV", "T3", "-", 0],
        ["-", 984810, "CS", "---", "---", 0, "PASV", "T4", "-", 0],
        ["A", 984811, "--", "---", "---", 0, "CODE", "T1", "-", 0],
        ["-", 984811, "CS", "R--", "---", 0, "CODE", "T2", "S", 64],
        ["-", 984811, "CS", "R--", "---", 194, "PASV", "T3", "-", 0],
        ["-", 984811, "CS", "---", "---", 0, "PASV", "T4", "-", 0],
        ["A", 984812, "--", "---", "---", 0, "CODE", "T1", "-", 0],
        ["-", 984812, "CS", "R--", "---", 0, "CODE", "T2", "-", 0],
        ["-", 984812, "CS", "R--", "---", 144, "PASV", "T3", "-", 0],
        ["-", 984812, "CS", "---", "---", 0, "PASV", "T4", "S", 194],
        ["A", 984813, "--", "---", "---", 0, "CODE", "T1", "-", 0],
        ["-", 984813, "CS", "R--", "---", 0, "CODE", "T2", "-", 0],
        ["-", 984813, "CS", "R--", "---", 144, "PASV", "T3", "-", 0],
        ["-", 984813, "CS", "---", "---", 0, "PASV", "T4", "-", 0],
        ["-", 984813, "--", "---", "---", 0, "PASV", "Ti", "-", 0],
        ["-", 984813, "--", "---", "---", 0, "PASV", "Ti", "-", 0],
        ["A", 264920, "--", "---", "---", 0, "MEMR", "T1", "-", 0],
        ["-", 264920, "DS", "R--", "---", 0, "MEMR", "T2", "-", 0],
        ["-", 264920, "DS", "R--", "---", 71, "PASV", "T3", "-", 0],
        ["-", 264920, "DS", "---", "---", 0, "PASV", "T4", "-", 0],
        ["A", 984814, "--", "---", "---", 0, "CODE", "T1", "-", 0],
        ["-", 984814, "CS", "R--", "---", 0, "CODE", "T2", "-", 0],
        ["-", 984814, "CS", "R--", "---", 144, "PASV", "T3", "-", 0],
        ["-", 984814, "CS", "---", "---", 0, "PASV", "T4", "-", 0],
        ["A", 984815, "--", "---", "---", 0, "CODE", "T1", "-", 0],
        ["-", 984815, "CS", "R--", "---", 0, "CODE", "T2", "-", 0],
        ["-", 984815, "CS", "R--", "---", 144, "PASV", "T3", "-", 0],
        ["-", 984815, "CS", "---", "---", 0, "PASV", "T4", "-", 0],
        ["A", 264920, "--", "---", "---", 0, "MEMW", "T1", "-", 0],
        ["-", 264920, "DS", "-A-", "---", 0, "MEMW", "T2", "-", 0],
        ["-", 264920, "DS", "-AW", "---", 108, "PASV", "T3", "-", 0]
    ]
}

There is a full breakdown of this format within the corresponding README.md file, however mainly, the ‘preliminary’ object incorporates the preliminary register and reminiscence state earlier than the instruction has run, and the ‘ultimate’ object incorporates the corresponding post-instruction state. 

To make use of this check, you arrange your emulator, initialize or reset the CPU, set the registers based on the preliminary state, and write the bytes within the preliminary ‘ram’ array to reminiscence.  Then you definitely execute the instruction, and evaluate your emulator’s registers and reminiscence to the values within the ‘ultimate’ state.  This may be very quick – MartyPC can execute 10,000 assessments in a number of seconds, making it sensible to re-run the check suite in its entirety on main CPU logic adjustments. All the advantages of {hardware} validation, however with out spending cash and several other days doing it your self.

A Rust Implementation

Here is a fast overview of how I run these assessments in my emulator, MartyPC.

First, we learn our check file right into a string, then deserialize it utilizing an implementation of the Deserialize trait on a suitable check construction, using the serde-json crate:

        file.read_to_string(&mut file_string).count on("Error studying in JSON file to string!");

        end result = match serde_json::from_str(&file_string) {
            Okay(json_obj) => Some(json_obj),
            Err(e) if e.is_eof() => {
                println!("JSON file {:?} is empty. Creating new vector.", test_path);
                Some(LinkedList::new())
            } 
            Err(e) => {
                eprintln!("Didn't learn json from file: {:?}: {:?}", test_path, e);
                None
            }
        }

We deserialize right into a LinkedList to keep away from vector reallocations. Then, we loop by every check within the record. In the course of the loop we arrange the preliminary register and reminiscence state:

        // Arrange CPU registers to preliminary state.
        println!("Establishing preliminary register state...");
        println!("{}",check.initial_state.regs);

        // Set reset vector to our check instruction ip.
        let cs = check.initial_state.regs.cs;
        let ip = check.initial_state.regs.ip;
        cpu.set_reset_vector(CpuAddress::Segmented(cs, ip));
        cpu.reset();

        cpu.set_register16(Register16::AX, check.initial_state.regs.ax);
        cpu.set_register16(Register16::CX, check.initial_state.regs.cx);
        cpu.set_register16(Register16::DX, check.initial_state.regs.dx);
        cpu.set_register16(Register16::BX, check.initial_state.regs.bx);
        cpu.set_register16(Register16::SP, check.initial_state.regs.sp);
        cpu.set_register16(Register16::BP, check.initial_state.regs.bp);
        cpu.set_register16(Register16::SI, check.initial_state.regs.si);
        cpu.set_register16(Register16::DI, check.initial_state.regs.di);
        cpu.set_register16(Register16::ES, check.initial_state.regs.es);
        cpu.set_register16(Register16::CS, check.initial_state.regs.cs);
        cpu.set_register16(Register16::SS, check.initial_state.regs.ss);
        cpu.set_register16(Register16::DS, check.initial_state.regs.ds);
        cpu.set_register16(Register16::IP, check.initial_state.regs.ip);
        cpu.set_flags(check.initial_state.regs.flags);

        // Arrange reminiscence to preliminary state.
        println!("Establishing preliminary reminiscence state. {} reminiscence states supplied.", check.initial_state.ram.len());
        for mem_entry in &check.initial_state.ram {
            // Validate that mem_entry[1] matches in u8.

            let byte: u8 = mem_entry[1].try_into().count on(&format!("Invalid reminiscence byte worth: {:?}", mem_entry[1]));
            cpu.bus_mut().write_u8(mem_entry[0] as usize, byte, 0).count on("Failed to write down reminiscence");
        }

Then we are able to run the instruction itself:

        // We loop right here to deal with REP string directions, that are damaged up into 1 efficient instruction
        // execution per iteration. The 8088 makes no such distinction.
        loop {
            match cpu.step(false) {
                Okay((step_result, cycles)) => {
                    println!("Instruction reported end result {:?}, {} cycles", step_result, cycles);

                    if rep & cpu.in_rep() {
                        proceed
                    }
                    break;
                },
                Err(err) => {
                    eprintln!("CPU Error: {}n", err);
                    cpu.trace_flush();
                    panic!("CPU Error: {}n", err);
                } 
            }
        }

        // CPU is completed with execution. Test ultimate state.
        println!("CPU accomplished execution.");

        // Get cycle states and registers from CPU.
        let mut cpu_cycles = cpu.get_cycle_states().clone();
        let cpu_regs = cpu.get_vregisters();

The CPU collects cycle states when it’s run with validation enabled, we retrieve them with get_cycle_states() in order that we are able to evaluate with the assessments’ cycle states, if desired. We additionally learn the CPU registers post-instruction. Evaluating the registers is pretty trivial, though it’s possible you’ll need to masks flag values to take away undefined flags from the equation (masks values for this goal are supplied within the metadata file 8088.json).

Then, we simply have to validate the post-memory state:

        // Validate ultimate reminiscence state.
        for mem_entry in &check.final_state.ram {
            
            // Validate that mem_entry[0] < 1MB
            if mem_entry[0] > 0xFFFFF {
                panic!("Check {}: Invalid reminiscence handle worth: {:?}", n, mem_entry[0]);
            }

            let addr: usize = mem_entry[0] as usize;

            // Validate that mem_entry[1] matches in u8.
            let byte: u8 = mem_entry[1].try_into().count on(&format!("Check {}: Invalid reminiscence byte worth: {:?}", n, mem_entry[1]));
            
            let mem_byte = cpu.bus().peek_u8(addr).count on("Didn't learn reminiscence!");

            if byte != mem_byte {
                eprintln!("Check {}: Reminiscence validation error. Handle: {:05X} Check worth: {:02X} Precise worth: {:02X}", n, addr, byte, mem_byte);
                outcomes.cross = false;
            }
        }

That is all you could validate the practical facets of a CPU instruction – if you happen to’re not concerned with emulating a cycle-accurate 8088 CPU, you possibly can cease there.  However in order for you that further accuracy, you possibly can parse the ‘cycles’ array and get a cycle-by-cycle readout of the CPU’s standing and bus traces, evaluating your emulator to how the actual factor carried out. Did your reads and writes happen on the identical cycle as {hardware}? Is your prefetch as much as snuff? Now you possibly can know for positive.

All of those values, save the t-states and queue learn byte, are learn immediately off of the bodily CPU. The t-states and queue learn byte are supplied by MartyPC operating in tandem in your comfort.

See Also

Since these assessments are {hardware} generated, we’ve got fairly robust confidence that they’re correct, though there are doable variations between 8088 steppings.  The assessments had been generated utilizing Harris CD80C88 CPUs. 

The person assessments are double-validated – earlier than a check could be written to disk, MartyPC and the Arduino-controlled 8088 should agree – on a register, reminiscence and cycle-by-cycle foundation.  That is further assurance that the {hardware} interface is not producing inaccurate outcomes – Serial switch errors occur! As soon as an entire check file is produced, MartyPC then runs a separate validation examine on all the file earlier than the file is error-checked, formatted, compressed and dedicated to GitHub.

A New Period of 8088 Emulation

In case you are pondering of constructing an 8088 emulator as we speak, making a cycle correct one is not a ridiculous proposition. It is now a reasonably easy course of. Now you’ve gotten a check set to get rid of trial and error cycle tweaks and prolonged and error inclined post-change check processes reminiscent of operating all the 8088MPH demo and hoping all of it nonetheless works… (we have all been there)

It’s my honest hope that emulator builders discover this check suite helpful and that it encourages extra emulator builders take up the problem of cycle-accurate PC emulation.

The Future – 8088 Check Suite V2

V1 of the 8088 CPU Check Suite shouldn’t be as complete because it might be, in concept.  Every check is run from a clean slate – a freshly reset CPU.  This has the benefit of beginning the instruction with an empty prefetch queue and a identified bus state, but it surely additionally means it’s doable to validate each instruction within the set and nonetheless have accuracy points dealing with the assorted bus and queue states encountered through the pure instruction stream of a 8088 CPU deep in program execution.  

Directions may start with fetch delay cycles lively, they is perhaps absolutely or partially prefetched. The lure flag or interrupt flag may interrupt instruction stream.  These are all issues which might be in all probability price modelling and testing – however complicate the technology of assessments.  

To arrange and skim out the register state for a single instruction, we’ve got to disturb the state of the CPU by operating brief applications to seize that info.  That raises a query of the way you seize the CPU state whereas preserving the register and prefetch queue contents throughout an instruction stream.  An emulator might help with this, a well-validated emulator producing the register state whereas the CPU cycle states are captured from {hardware}. Hybrid check technology, so to talk.  

I’ve some concepts for producing a set that can absolutely train your BIU and prefetch logic by a wide range of instruction transitions, and combine traps and interrupt dealing with.  However that’s work for an additional day.

Give me the assessments, already!

For as much as the minute adjustments, you possibly can take a look at my fork the place I’ll add experimental assessments and such between pull requests:

The present V1 check set is on this department:

https://github.com/dbalsom/ProcessorTests/tree/8088_v1/8088

Joyful Testing!

Source Link

What's Your Reaction?
Excited
0
Happy
0
In Love
0
Not Sure
0
Silly
0
View Comments (0)

Leave a Reply

Your email address will not be published.

2022 Blinking Robots.
WordPress by Doejo

Scroll To Top