24 августа 2025 года состоялся релиз 86Box 5.0. Низкоуровневый эмулятор IBM PC и совместимых с ним компьютеров получил новый динамический рекомпилятор инструкций процессора, расширил поддержку "железа" и улучшил работу множества уже существующих компонентов. Что ещё внутри "коробки"?

Введение

86Box v5.0 вышел ровно на тридцатую годовщину выпуска в розничную продажу Windows 95 — первой 32-битной операционной системы от Microsoft для домашних ПК. Не случайно разработчики эмулятора оформили список изменений картинкой в соответствующем стиле. Бизнес-сектору ещё около года ждать выхода Windows NT 4.0 Workstation, в которой пользователь встретит прорывной пользовательский интерфейс Windows 95 на молодом, но уже полностью самостоятельном ядре NT с многопроцессорностью, жёстким разграничением доступа и повышенной устойчивостью к сбоям.

В каких случаях может понадобиться "аппаратно-точный" эмулятор, а не гипервизор? Некоторым программам высокое быстродействие процессора явно не на пользу, наиболее наглядный пример — игры для IBM PC XT, рассчитанные на частоту 4.77 МГц, или критически важные программы, работоспособность которых зависит от аппаратной конфигурации компьютера.

Чем точнее эмуляция, тем достовернее результат. Иногда одна упущенная мелочь может привести к неправильному поведению эмулируемой системы. В такие моменты можно надолго закопаться в стопках технической документации на воссоздаваемую в коде микросхему, сверяя каждый записанный или считанный регистр. Но не одной документацией давить баги, можно взять в помощники статический анализатор. И вот момент, который вы ждали: мы с гордостью представляем для вашего удовольствия работу PVS-Studio!

Установка PVS-Studio

Загрузить установочный дистрибутив можно с этой страницы. Для запуска анализа понадобится лицензия, но для получения триальной версии не придётся заполнять огромную регистрационную карточку и отсылать её по почте. Установку анализатора не нужно будет ускорять постоянным шевелением мыши, обойдёмся только установкой основных компонентов анализатора и плагина для JetBrains CLion. Актуальная на момент написания статьи версия PVS-Studio — 7.38.

Конфигурация сборки и анализа

Собирать будем под Windows, и здесь уже не поможет нам Visual Studio: балом правит MSYS2/MinGW! Проект завязан на использование специфичных для Unix (в первую очередь GNU/Linux) заголовочных файлов и функций, недоступных в MSVC. Поэтому разработчики решили для этой системы использовать подсистему MSYS2 для обеспечения такой совместимости без нужды адаптировать кодовую базу под Windows родными для неё способами из MSVC. Исходный код проекта заморожен на теге v5.0, бэкпорты в 86Box не практикуются. Собирать будем проект в конфигурации RelWithDebInfo.

Исключать из анализа нечего: 86Box почти не использует сторонние библиотеки. Для надёжности я решил убрать директорию MSYS2 с глаз подальше — в моём случае это D:\msys64.

Настройки компилятора для проекта оставлены по умолчанию. 86Box при сборке опирается на cc.exe и c++.exe, не определяющиеся CompilerCommandsAnalyzer и плагином для CLion в автоматическом режиме на Windows. Поэтому запустим консольный анализатор из папки сборки с параметром --compiler (-C в короткой записи), чтобы подсказать ему, с кем работать.

CompilerCommandsAnalyzer.exe analyze -C cc.exe -C c++.exe -a GA -j

Затем через PlogConverter преобразуем отчёт в JSON, чтобы открыть его в плагине для CLion:

PlogConverter.exe -t json -a GA:1,2,3 PVS-Studio.log

В отчёте будут срабатывания диагностик общего назначения всех трёх уровней предупреждений. Загружаем отчёт анализатора через Tools > PVS-Studio > Open Report, ждём пару мгновений и начинаем разбагпаковку.

Сразу отмечу, что кошмар-кошмар в 86Box найти будет сложно, разработчики очень ответственно подходят к контролю качества кода: у них настроен регулярный статический анализ средствами SonarQube через CodeQL. PVS-Studio поддерживает работу с обоими инструментами: можно как в CodeQL, так и в SonarQube для агрегации результатов.

Что внутри?

Обычно я сортирую срабатывания по номерам диагностик, но сегодняшняя распаковка багов пройдёт по-другому: срабатывания сортируются по классу оборудования!

Чипсеты

Начнём с серии VIA Apollo — лучших чипсетов для Socket 7 и Super Socket 7, которые добрались даже до Socket 370. А раз лучших, то... подождите, что это пишет PVS-Studio?

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;
  }
  ....
}

Математические беспорядки на северном мосте. Из-за них проверки всегда будут возвращать отрицательный ответ. Все модели северных мостов, чьи идентификаторы меньше, чем у VIA VT8601 (VIA_8601, Apollo PM601), не будут настроены как надо в своих else if ветках. По смещению 0x6B северных мостов VT82C691 (VIA_691, Apollo Pro), VT82C595 (VIA_595, Apollo VP2) и VT82C585VP (VIA_585, Apollo VP) будут указаны значения как для VT82C693A (VIA_693A, Apollo Pro133).

Насколько это страшно? Открываем техническую документацию на микросхемы — сайту и сообществу The Retro Web выражаю огромную благодарность за систематизацию всей информации! Начнём с Apollo Pro, он первый из альтернативной ветки исполнения кода.

Значение длиной в один байт, разбитое на три секции флагов с полезными данными. А теперь посмотрим на Apollo Pro133, и, внезапно, при том же назначении регистра (регулировка порядка обращений к оперативной памяти) у этого регистра совершенно другие параметры:

Совпали положения 0 и 7-6 битов. 5-4 тоже совпали только за счёт того, что они были зарезервированными в Apollo Pro (1998 год), и в Apollo Pro133 (1999 год) зарезервированные значения стали значениями по умолчанию. Оставшиеся три бита настраиваются не по документации. Apollo PM601 будет настроен "как надо" только в ветке else.

Похожая история с регистром DRAM MA Map Type (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;
  }
  ....
}

Код должен настроить карту банков памяти с учётом особенностей северного моста: для каких-то моделей нужно вычислить через битовую маску, для других можно использовать значение как есть. Однако конфигурация через битовую маску почему-то становится доступной любому северному мосту VIA, начиная с Apollo VP. Давайте "соберём" компьютер и посмотрим, что происходит. Начнём с чего-нибудь на Apollo Pro133.

Фундаментально условие верно: идентификатор больше, чем у Apollo VP. А если я возьму что-нибудь с Apollo VP3 (VIA_597)?

Да что ж такое, условие опять верным получается, но оно считается выполненным, ещё не дойдя до второй проверки — всё заканчивается на первой проверке идентификатора, он же больше, чем у Apollo VP!

Поэтому любой из трёх северных мостов в условии будет сконфигурирован одинаково — через маску. Что говорит нам документация на микросхемы?

Карта банков памяти у всех чипсетов серии VIA Apollo будет настроена через вычисление маски, а не "как есть". Но тут либо VIA пошутила и опечаталась, либо они что-то замышляли и поменяли порядок банков в Apollo VP3 (справа).

Они объединили таблицу регистров 0x58 и 0x59 в документации на Apollo VP3, это не страшно и, наоборот, удобнее; обозначили второй регистр битами 15-8. Казус произошёл с очерёдностью: если раньше (в Apollo VP) биты 7-5 и 3-1 обозначали банки по нарастающей, в VP3 они неожиданно стали по убывающей. "Явное лучше неявного"? Если явная проверка не влияет никак на исполнение кода, она явно не полезна. Хорошая головоломка для возможного обсуждения с разработчиками, мне понравилось!

Перейдём к чему-нибудь попроще, например, к поколению i286. PVS-Studio кое-что нашёл в инициализации чипсета VLSI VL82C311L (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;      // <=
  }
  ....
}

Итак, у нас есть выделенная память. Было бы это выделение памяти через malloc, тогда был бы смысл ручками присвоить NULL ('\0') указателю на память устройства.

dev->card_mem = NULL;

calloc при выделении памяти сразу затирает её '\0', и руками что-то изменять становится избыточно. Однако проблема даже не в лишней операции. Диагностика говорит, что в деле участвует арифметика с нулевым указателем, и даже если бы строчку с принудительной перезаписью в NULL мы убрали, корректным итоговое выражение никак бы не стало.

По задумке автора, это "быстрое" преобразование числа, обозначающего адрес начала памяти подключённого устройства в указатель. Можно же просто привести uint32_t к uint8_t* и получить такой же результат!

dev->card_pages[i].mem       = (uint8_t*)dev->card_pages[i].phys_addr;

Компилятор скажет, что это нехорошо, и поднимет своё предупреждение cast to pointer from integer of different size [-Wint-to-pointer-cast]. Но у меня есть контраргумент: записанные в поле virt_addr числа не превышают 65535 (0xFFFF). Теперь всё по красоте.

Дальше в очереди Intel PIIX3. Друг и товарищ платформ на Slot 1 и Socket 370. PVS-Studio замечает: 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. */
  }
}

Подозрительно похоже на копипасту с заполнения (зарезервированного?..) регистра 0xFF, если документация не пытается над нами пошутить.

86Box как минимум знает о существовании USB в чипсетах, но пока не реализовывает эмуляцию USB-дисков или проброс физических USB-устройств. Эта ошибка не так ужасна, как может показаться: просто контроллер USB будет всегда инициализироваться в режиме "предварительной" спецификации USB 1.0. Возможно, в будущем это поведение будет настраиваемым.

Закончим обзор чипсетов безобидной копипастой в цепочке сброса шины ЦП-PCI SiS 5571.

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 всегда на страже дрожащих рук :)

Видеокарты

Продолжим чем-нибудь попроще, чтобы дать голове отдохнуть. В VESA-совместимом адаптере низкоуровневого эмулятора-коллеги Bochs, Bochs SVGA, чуть-чуть перемудрили с математикой, о чём заботливо сказал PVS-Studio:

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);
    ....
    }
....
}

Конец строчного гасящего импульса здесь вполне реально вычислить без количества видимых строк (mode.hdisplay) — переменная сокращается.

svga->hblankend = mode.htotal - 1;

Результат тот же, и голове посчитать легче :)

В тему сокращений: PVS-Studio не побрезговал изучить качественно сшитый матрац фирмы Matrox и пройтись утюгом по нему. В блиттере (части, отвечающей за разгрузку центрального процессора от задачи копирования видеопамяти и управления пикселями на экране) видеокарт семейства Matrox Mystique нашёлся вот такой ма-а-аленький клоп:

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]))) 

  ....
}

Этого постельного паразита можно вот так сократить:

if (!transc || mystique->dwgreg.pattern[pattern_y][pattern_x])

Если transc не равен false, то вполне логично, что в альтернативном условии эта переменная будет true, и проверять её на истинность избыточно. Сородичи этого клопа спрятались чуть дальше:

  • V728 An excessive check can be simplified. The '||' operator is surrounded by opposite expressions '!transc' and 'transc'. vid_mga.c 4716

  • V728 An excessive check can be simplified. The '||' operator is surrounded by opposite expressions '!transc' and 'transc'. vid_mga.c 4788

Другое

Сюда попала парочка багов, что сортировке не подлежат, потому что код более-менее общий. Общий по загадочности происходящего.

Предупреждение PVS-Studio 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;
    ....
  }
  ....
}

Вспомнил случайный диалог персонажей сюжетной миссии в одной MMO:

— Налево или направо, Мёрфи?

— Да!

— (неловкая пауза, вздох) Ох, Мёрфи...

if-else или switch-case? Да! В условии на основе if-else вместо сравнения двух значений в альтернативной ветке оказалась константа из перечисления. Условие стало выглядеть так, будто оно предназначалось для switch-case. К чему это приведёт?

У процессоров Cyrix 6x86 были проблемы с совместимостью с Intel Pentium, поэтому им отключали инструкцию CPUID по умолчанию. А теперь получается, что она выключена у всех процессоров серии 6x86, и, внезапно, Cyrix GX1, кроме доработанного 6x86MX (и почему-то такого же проблемного 6x86L).

Предупреждение PVS-Studio 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;
    }
    ....
}

Очень похоже на ситуацию с клопами в загадочном матраце Matrox Mystique (!transc || transc ....), но здешняя разновидность паразитов предпочитает переносчиков, которые ошиблись при написании ключевых слов в условных конструкциях.

Проверил blame, нашёл конкретный коммит, а там Enter не прожался. Здесь можно использовать else if, но это, скорее, на усмотрение разработчиков. Код написан так, что будет выполнено одно из двух условий или ни одного, поэтому это просто маленький косметический дефект, не рушащий картину мироздания.

Итоги

86Box показал превосходный, на мой взгляд, результат. Всё благодаря чётко выстроенным процессам разработки и постоянному контролю качества приходящих изменений в код. PVS-Studio можно совершенно бесплатно использовать в таких проектах. Напишите нам, и мы с радостью обсудим с вами такую возможность.

У разработчиков эмулятора планируется рефакторинг эмуляции процессоров в 86Box v6.0. Многие ждут эмуляцию NVIDIA RIVA 128, и на чип активно ищется любая подходящая к нему документация. Доступность старого ПК-железа с годами снижается, и далеко не все "аутентичные" железки доживают до наших дней: кого-то съедает протёкший никель-кадмиевый аккумулятор (не забудьте его выкусить или выпаять из посадочного места на вашей материнской плате с каким-нибудь Intel i386DX, а ещё лучше сразу доработать для установки CR2032!), кто-то теряет герметичность из-за несовершенства тогдашних конструкций жёстких дисков (типичная "болячка" WD Caviar, у которых гермозона от внешней среды отделена клейкой лентой). Список таких "возрастных проблем" можно тянуть до бесконечности. Важно то, что точная аппаратная эмуляция позволяет нам ощутить "то самое" железо таким, каким оно было в годы стремительного развития технологий. Желаю команде не останавливаться на достигнутом!

Однако это ещё не всё, что я хотел бы показать в 86Box v5.0. Есть ещё одна маленькая букашечка, которую я отселил от показанных здесь, и ей будет посвящена отдельная история. Благодарю за уделённое время! Теперь питание компьютера можно отключить.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Taras Shevchenko. Box of bugs (emulated).

Комментарии (0)