On August 24, 2025, 86Box 5.0 was released. The low-level emulator for IBM PCs and compatibles has received a new dynamic processor instruction recompiler, broader hardware support, and significant performance enhancements across many existing components. What else is packed inside the "box"?
86Box v5.0 was released exactly on the 30th anniversary of the retail launch of Windows 95, Microsoft's first 32-bit operating system for home PCs. Not accidentally, the emulator developers illustrated the changelog with a picture in a fitting Windows 95-style theme. At the same time, the business sector had to wait about a year for the release of Windows NT 4.0 Workstation, where users could encounter the groundbreaking Windows 95 UI on an unseasoned yet fully independent NT kernel with multiprocessing, strict access control, and improved fault tolerance.
But why would anyone need a hardware-accurate emulator instead of a hypervisor? Some programs clearly don't benefit from high processor speed. Classic examples are IBM PC XT games, which are designed for the original 4.77 MHz clock rate, or critical programs whose performance depends on the hardware configuration.
The more accurate the emulation, the more reliable the result! Sometimes, a single overlooked thing can lead to incorrect behavior of the whole emulated system. In those cases, we can get buried in heaps of datasheets on the recreated microchip, checking every register that's been written or read. But we don't have to rely solely on documentation to fix bugs; we can also use a static analyzer to help us. And now, the moment you've been waiting for, we proudly present for your viewing pleasure, PVS-Studio static analyzer!
You can download the installer from this page. A license is required to run the analysis, but you won't have to deal with registration headaches or send a request via pigeon post to get a trial version. Yet, no need to accelerate the analyzer installation by hysterically moving the mouse; just install the main components and the plugin for JetBrains CLion, and that's all. At the time of writing, the version of PVS-Studio analyzer is 7.38.
I'll compile the project on Windows, but Visual Studio won't rule me here: I bow my knees to MSYS2/MinGW! The project relies on the use of header files and Unix-specific functions (primarily GNU/Linux) that are unavailable in MSVC. So, developers use the MSYS2 subsystem to ensure such compatibility without adapting the code base for Windows using native MSVC methods. The project's source code is frozen at the v5.0 tag; backports aren't practiced in 86Box. For our build, I'll use the RelWithDebInfo
configuration.
There's nothing to exclude from the analysis—86Box barely uses any third-party libraries. Just in case, I move the MSYS2 directory out of sight—in my case, it's D:\msys64
.
The project uses the default compiler settings. During the build, 86Box relies on cc.exe
and c++.exe
. However, these aren't automatically detected by CompilerCommandsAnalyzer
or the CLion plugin on Windows. Therefore, I run the console analyzer from the build folder with the ‑‑compiler
parameter (or short -C
) to indicate which compilers to work with.
CompilerCommandsAnalyzer.exe analyze -C cc.exe -C c++.exe -a GA -j
Next, I convert the report into JSON format with PlogConverter
and open it in the CLion plugin:
PlogConverter.exe -t json -a GA:1,2,3 PVS-Studio.log
The report includes general-purpose diagnostic rules for all three warning levels. Load the analyzer report via Tools > PVS-Studio > Open Report, wait a couple of moments, and start unboxing bugs.
I should note right away: don't expect to find terrifying bugs in 86Box. The project developers maintain code quality very seriously and already use SonarQube with CodeQL. Luckily, PVS-Studio integrates with both tools: you can aggregate results using either CodeQL or SonarQube.
I usually group warnings by the number of diagnostic rules, but today, let's unpack the bugs another way—I'll group them by hardware class.
Let's start with the VIA Apollo series—the best chipsets for Socket 7 and Super Socket 7, which even stretched to Socket 370. And if it's the best, then... Hold on... what's this? PVS-Studio issued warnings:
V547 Expression 'dev->id == 0x06910600' is always false. via_apollo.c 515
V547 Expression 'dev->id == 0x05950000' is always false. via_apollo.c 517
V547 Expression 'dev->id == 0x05851000' is always false. via_apollo.c 519
#define VIA_585 0x05851000
#define VIA_595 0x05950000
#define VIA_691 0x06910600
#define VIA_693A 0x06914400
#define VIA_8601 0x86010500
static void
via_apollo_host_bridge_write(int func, int addr, uint8_t val, void *priv)
{
via_apollo_t *dev = (via_apollo_t *) priv;
....
switch (addr) {
....
case 0x6b:
if ((dev->id == VIA_693A) || (dev->id < VIA_8601))
dev->pci_conf[0x6b] = val;
else if (dev->id == VIA_691) // <=
dev->pci_conf[0x6b] = (dev->pci_conf[0x6b] & ~0xcf) | (val & 0xcf);
else if (dev->id == VIA_595) // <=
dev->pci_conf[0x6b] = (dev->pci_conf[0x6b] & ~0xc0) | (val & 0xc0);
else if (dev->id == VIA_585) // <=
dev->pci_conf[0x6b] = (dev->pci_conf[0x6b] & ~0xc4) | (val & 0xc4);
else
dev->pci_conf[0x6b] = (dev->pci_conf[0x6b] & ~0xc1) | (val & 0xc1);
break;
}
....
}
There's a mathematical chaos in the north bridge: checks always return a negative result. All north bridge models with identifiers smaller than VIA VT8601 (VIA_8601
, Apollo PM601) won't be configured correctly in their else if
branches. At the 0x6B
offset, the north bridges VT82C691 (VIA_691
, Apollo Pro), VT82C595 (VIA_595
, Apollo VP2), and VT82C585VP (VIA_585
, Apollo VP) are assigned the same values as VT82C693A (VIA_693A
, Apollo Pro133).
How scary is it? Well, let's find out. Time to take the technical docs on these microchips. Huge thanks and my heartfelt appreciation to The Retro Web website and its community for organizing all this information! Let's start with the Apollo Pro, the first of the alternative code execution branches.
Here's a one-byte value, split into three flag sections with useful data. Now let's look at the Apollo Pro133, and suddenly, with the same register assignment (the DRAM arbitration control), this register has completely different parameters:
The positions of bits 0 and 7-6 match. Bits 5-4 also match, but only because they are reserved in the Apollo Pro (1998), and in the Apollo Pro133 (1999), the reserved values become the default values. The remaining three bits are configured from what the documentation specifies. As a result, Apollo PM601 will be configured "as intended" only in the else
branch.
Here's a similar story with the DRAM MA Map Type register (0x58
).
V560 A part of conditional expression is always true: (dev->id < 0x05970100). via_apollo.c 353
#define VIA_597 0x05970100
static void
via_apollo_host_bridge_write(int func, int addr, uint8_t val, void *priv)
{
via_apollo_t *dev = (via_apollo_t *) priv;
....
switch (addr) {
....
case 0x58:
if ( (dev->id >= VIA_585)
|| (dev->id < VIA_597) || (dev->id == VIA_597) // <=
|| ((dev->id >= VIA_693A) || (dev->id < VIA_8601)))
dev->pci_conf[0x58] = (dev->pci_conf[0x58] & ~0xee) | (val & 0xee);
else
dev->pci_conf[0x58] = val;
}
....
}
The code is supposed to configure the memory bank map according to the characteristics of the north bridge: for some models, it's necessary to evaluate using a bit mask, while for others, the value can be used as is. However, for some reason, the mask-based configuration becomes available for every VIA north bridge starting with Apollo VP. Let's "build" the computer and see what happens. Let's start with something on the Apollo Pro133.
Fundamentally, the condition is correct: the identifier is greater than that of Apollo VP. What if I take something from Apollo VP3 (VIA_597
)?
What's all this! The condition is correct again, but it stops before reaching the second check—everything ends with the first identifier check, which is greater than that of Apollo VP!
Therefore, any of the three north bridges in the condition will be configured identically—using a mask. What does the microchip documentation say about this?
The memory bank map for all VIA Apollo chipsets will be configured by evaluating the mask, rather than "as is". But here, either VIA made a mistake and printed it wrong, or VIA deliberately changed the bank order in the Apollo VP3 (as seen on the right).
VIA combine the 0x58
and 0x59
register tables in the Apollo VP3 documentation. This isn't really a problem; on the contrary, it's more convenient, and the second register is designated with bits 15-8. The incident occurs with the order: whereas previously (in Apollo VP) bits 7-5 and 3-1 denote banks in ascending order, in VP3 they unexpectedly become descending. "Is explicit better than implicit?" If an explicit check doesn't affect code execution, it's clearly redundant—a good puzzle for discussion with developers, I like it!
Let's move on to something simpler, like the i286 generation. PVS-Studio found something in the initialization of the VLSI VL82C311L chipset (SCAMP-DT 286).
V769 The 'dev->card_mem' pointer in the expression equals nullptr. The resulting value of arithmetic operations on this pointer is senseless and it should not be used. scamp.c 1190
#define EMS_PGSIZE 16384
typedef struct card_mem_t {
int in_ram;
uint32_t virt_addr;
uint32_t phys_addr;
uint8_t *mem;
} mem_page_t;
typedef struct scamp_t {
....
uint8_t *card_mem;
mem_page_t card_pages[4];
....
} scamp_t;
static void *
scamp_init(UNUSED(const device_t *info))
{
scamp_t *dev = (scamp_t *) calloc(1, sizeof(scamp_t));
....
dev->card_mem = NULL;
for (uint8_t i = 0; i < 4; i++) {
dev->card_pages[i].virt_addr = i * EMS_PGSIZE;
dev->card_pages[i].phys_addr = dev->card_pages[i].virt_addr;
dev->card_pages[i].mem
= dev->card_mem + dev->card_pages[i].phys_addr; // <=
}
....
}
Well, memory has been allocated. If this were done via malloc
, it'd make sense to manually assign NULL
('\0'
) to the device memory pointer.
dev->card_mem = NULL;
When allocating memory, calloc
immediately overwrites it with '\0'
, making it unnecessary to change anything manually. However, the real problem isn't even the superfluous operation. The diagnostic rule indicates that arithmetic is being performed on a null pointer, and even if developers removed the line with forced overwriting to NULL
, the final expression would still be incorrect.
According to the developers' idea, this is supposed to be a "quick" way to convert the number of the start address of the connected device memory into a pointer. However, they could simply cast uint32_t
to uint8_t*
and get the same result!
dev->card_pages[i].mem = (uint8_t*)dev->card_pages[i].phys_addr;
The compiler will complain about this, raising the warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]
. But I have a counterargument: the numbers recorded in the virt_addr
data member don't exceed 65535
(0xFFFF
). Now everything is blooming and fragrant.
Next up is the Intel PIIX3, a trusty friend and companion of Slot 1 and Socket 370 platforms. PVS-Studio noticed something: V547 Expression 'dev->type > 3' is always true. intel_piix.c 1382
static void
piix_reset_hard(piix_t *dev)
{
uint8_t *fregs;
....
/* Function 2: USB */
if (dev->type > 1) {
fregs = (uint8_t *) dev->regs[2];
....
if (dev->type > 4)
fregs[0x60] = (dev->type > 3) ? 0x10 : 0x00; // <=
if (dev->type < 5) {
fregs[0x6a] = (dev->type == 3) ? 0x01 : 0x00;
fregs[0xc1] = 0x20;
fregs[0xff] = (dev->type > 3) ? 0x10 : 0x00;
}
dev->max_func = 2; /* It starts with USB disabled, then enables it. */
}
}
It looks like a copy-paste from filling a (reserved?..) register 0xFF
, unless the documentation is trying to play a prank on us.
86Box is at least aware that USB exists in chipsets, but it doesn't yet support USB disk emulation or passthrough of physical USB devices. This error is not as bad as it may seem: the USB controller will simply always initialize in the "preliminary" USB 1.0 specification mode. Perhaps in the future, this behavior will be configurable.
Let's conclude our exploration of chipsets with a harmless copy-paste in the SiS 5571 CPU-PCI bus reset chain.
V519 The 'dev->pci_conf[0x93]' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 394, 395. sis_5571_h2p.c 395
static void
sis_5571_host_to_pci_reset(void *priv)
{
sis_5571_host_to_pci_t *dev = (sis_5571_host_to_pci_t *) priv;
....
dev->pci_conf[0x90] = 0x00;
dev->pci_conf[0x91] = 0x00;
dev->pci_conf[0x92] = 0x00;
dev->pci_conf[0x93] = 0x00; // <=
dev->pci_conf[0x93] = 0x00; // <=
dev->pci_conf[0x94] = 0x00;
dev->pci_conf[0x95] = 0x00;
....
}
PVS-Studio is always on guard against shaky hands :)
Let's take it easy for a bit and give our brains a break. In the VESA-compatible adapter of the low-level emulator colleague Bochs, Bochs SVGA, developers got a little carried away with the math, as PVS-Studio kindly pointed out:
V1065 Expression can be simplified, check 'mode.hdisplay' and similar operands. vid_bochs_vbe.c 323
void
bochs_vbe_recalctimings(svga_t* svga)
{
bochs_vbe_t *dev = (bochs_vbe_t *) svga->priv;
if (dev->vbe_regs[VBE_DISPI_INDEX_ENABLE] & VBE_DISPI_ENABLED) {
vbe_mode_info_t mode = { 0 };
....
gen_mode_info(dev->vbe_regs[VBE_DISPI_INDEX_XRES],
dev->vbe_regs[VBE_DISPI_INDEX_YRES], 72.f, &mode);
....
svga->hblankend = mode.hdisplay + (mode.htotal - mode.hdisplay - 1);
....
}
....
}
The end of the horizontal blanking interval can be evaluated here without the number of visible lines (mode.hdisplay
) because the variable is reduced.
svga->hblankend = mode.htotal - 1;
The result is the same, and it's easier to calculate in your head :)
Speaking of simplifications: PVS-Studio didn't pass up the chance to examine a well-stitched Matrox "mattress" and give it a little ironing. In the blitter (that's the component that offloads the CPU from copying video memory and controlling pixels on the screen) of the Matrox Mystique family video cards, this ti-i-iny little bug:
V728 An excessive check can be simplified. The '||' operator is surrounded by opposite expressions '!transc' and 'transc'. vid_mga.c 4395
static void
blit_line(mystique_t *mystique, int closed, int autoline)
{
....
bool transc = !!(mystique->dwgreg.dwgctrl_running & DWGCTRL_TRANSC);
switch (mystique->dwgreg.dwgctrl_running & DWGCTRL_ATYPE_MASK) {
case DWGCTRL_ATYPE_RSTR:
case DWGCTRL_ATYPE_RPL:
while (mystique->dwgreg.length >= 0) {
if (....) {
....
if ( !transc // <=
|| (transc && (mystique->dwgreg
.pattern[pattern_y][pattern_x])))
....
}
This little "bed bug" can be shortened as follows:
if (!transc || mystique->dwgreg.pattern[pattern_y][pattern_x])
If transc
isn't equal to false
, it's logical that this variable will be true
in the alternative condition, so the additional check will be redundant. This bug has a few "relatives" hiding a little deeper in the code:
A couple of bugs crept in here that defy categorization, since the code is generic—not in structure, but in that "something strange is going on" way.
The PVS-Studio warning N1:
V768 The enumeration constant 'CPU_Cx6x86' is used as a variable of a Boolean-type. cpu.c 1552
enum {
....
CPU_Cx6x86, // 34
CPU_Cx6x86MX,
CPU_Cx6x86L,
....
}
void
cpu_set(void)
{
cpu_s = (CPU *) &cpu_f->cpus[cpu_effective];
....
switch (cpu_s->cpu_type) {
....
case CPU_Cx6x86:
case CPU_Cx6x86L:
case CPU_CxGX1:
case CPU_Cx6x86MX:
....
if ( (cpu_s->cpu_type == CPU_Cx6x86L)
|| (cpu_s->cpu_type == CPU_Cx6x86MX))
ccr4 = 0x80;
else if (CPU_Cx6x86) // <=
CPUID = 0; /* Disabled on powerup by default */
break;
....
}
....
}
I remembered a random dialogue between characters in a story mission in an MMO:
— Left or right, Murphy?
— Yes!
— *awkward pause, sigh* Oh, Murphy...
if-else
or switch-case
? Yes! In an if-else
condition, instead of comparing two values in the alternative branch, there is a constant from the enumeration. The condition started to look like it was meant for aswitch-case
. What can this lead to?
Cyrix 6x86 processors had compatibility issues with Intel Pentium, so the CPUID
instruction was disabled by default. And now it turns out that it was disabled in all 6x86 series processors, and suddenly, the Cyrix GX1 too, except for the modified 6x86MX (and oddly enough, the equally problematic 6x86L).
The PVS-Studio warning N2: V646 Consider inspecting the application's logic. It's possible that 'else' keyword is missing. isapnp.c 1127
void
isapnp_enable_card(void *priv, uint8_t enable)
{
isapnp_t *dev = (isapnp_t *) device_get_priv(&isapnp_device);
....
/* Look for a matching card. */
isapnp_card_t *card = dev->first_card;
while (card) {
....
/* Invalidate other references if we're disabling this card. */
if ( (card->enable) && (dev->current_ld_card != NULL)
&& (dev->current_ld_card != card)) {
dev->current_ld = NULL;
dev->current_ld_card = NULL;
} if (!card->enable) { // <=
if (dev->isolated_card == card)
dev->isolated_card = NULL;
if ( (dev->current_ld_card == card)
&& (old_enable != ISAPNP_CARD_FORCE_CONFIG)) {
dev->current_ld = NULL;
dev->current_ld_card = NULL;
}
}
break;
}
....
}
This is very similar to the bug situation in the "mysterious mattress" of Matrox Mystique (!transc || transc ....
), but this variety of little pests hosts who slip up when typing keywords in conditional statements.
I checked blame
, found the specific commit—Enter key probably jammed. I suppose developers could use else if
here, but this is more a matter of their preference. As it stands, the code guarantees that either one of the two conditions will be executed, or neither will. In other words, it's more of a cosmetic blemish than a universe-breaking bug.
In my view, 86Box showed stellar results. All thanks to clearly structured development processes and constant quality control over every code change. For projects like this, PVS-Studio can be used completely free—just write to us, and we'll be glad to discuss this opportunity with you.
The emulator developers plan to refactor processor emulation in 86Box v6.0. Many are eagerly waiting for NVIDIA RIVA 128 emulation, and any relevant documentation for the chip is being actively discovered. Over the years, the availability of old PC hardware has been declining, and not all "authentic" hardware has survived to this day: some fall victim to leaking nickel-cadmium batteries (don't forget to carefully remove or desolder them from the motherboard on your Intel i386DX—or even better, modify it for a CR2032!), some lose their airtightness due to the flaws of the old hard drive designs (a typical "ailment" of WD Caviar drives, whose containment is separated from the external environment by adhesive tape). The list of such "age-related problems" could go on forever. The important thing is that accurate hardware emulation lets us experience the "real deal" hardware just as it was during the years of rapid technological progress. I wish the team all the best in their future endeavors and hope they keep pushing forward!
But that's not all I wanted to show in 86Box v5.0. There's one more little bug I've set aside from what we've covered here, and it will get its own story later. Thanks for taking the time to read! It's now safe to turn off your computer.
0