VBoxGuest для KolibriOS: архитектура и устройство драйвера

от автора

Содержание

Введение

VBoxGuest — это драйвер гостевых дополнений (Guest Additions) для KolibriOS, работающей внутри VirtualBox. Драйвер написан целиком на FASM и реализует полноценный стек интеграции: от низкоуровневого общения с виртуальным устройством VMMDev (Virtual Machine Monitor Device) до готовых пользовательских сервисов — мыши, общих папок, буфера обмена, бесшовного режима и синхронизации времени.

Кодовая база — около 11 200 строк в 61 файле (по состоянию на версию 1.0). Архитектура простая: сервисы подключаются через единый диспетчер, HGCM-транспорт обёрнут в унифицированный API с поддержкой PageList-DMA, а низкоуровневая работа с железом (i8042, BGA, CMOS) изолирована внутри соответствующих сервисов (mouse, display, timesync).

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

Текст рассчитан на читателя, который уже знает PCI, прерывания, DMA и виртуализацию x86 в общих чертах. Базовые понятия вроде «что такое IRQ» или «зачем нужен MMIO» я не объясняю — фокус на специфике VMMDev/HGCM и инженерных решениях для KolibriOS.

Почему это пришлось писать с нуля

Готовых гостевых дополнений для KolibriOS до этого не было — ни в виде бинарника, ни в виде портируемых исходников.

В Linux / Windows / FreeBSD гостевые дополнения распределены по нескольким компонентам: kernel-драйвер VBoxGuest (минимальный мост), user-space-демон VBoxService (timesync, properties, balloon) и графический клиент VBoxClient под X11/Wayland (display-resize, seamless, clipboard). Плюс отдельные kernel-модули — vboxsf, vboxvideo/DRM, а на Windows ещё фильтр-драйвер мыши. Каждый компонент опирается на свою инфраструктуру: долгоживущий пользовательский процесс, регистрацию файловой системы, блокирующее ожидание в ядре, X-сервер.

В KolibriOS драйвер выполнен в иной модели — единый PE DLL, без отдельных user-space-сервисов и без отдельных модулей под VFS (виртуальная файловая система), видео и мышь. Ряд возможностей ядра при этом существует и доступен драйверу — события (CreateEvent / WaitEvent / WaitEventTimeout, см. [Kernel_Event/ru]), но в текущей версии драйвер их не использует и обходится spin-loop’ом и периодическим таймером. Это часть осознанного технического долга, к которому ещё вернёмся в разделе «Итог».

Так что весь стек от PCI-пробы до буфера обмена и общих папок написан на FASM. Архитектура построена на внутреннем диспетчере сервисов, compile-time регистрации и тиках одного таймера. Эта статья — про то, как оно устроено и почему именно так.

Структура файлов проекта

vboxguest.asm          точка входа, PE DLL native 0.05config.inc             настройки автозапуска и отладки├── core/              ядро: PCI, MMIO, IRQ, таймер, диспетчер│   ├── pci.inc        обнаружение PCI-устройства, чтение BAR/IRQ│   ├── mmio.inc       отображение MMIO, проверка версии VMMDev│   ├── ports.inc      отправка запросов через I/O-порт│   ├── irq.inc        обработчик прерываний│   ├── timer.inc      периодический тик (100 мс)│   └── dispatcher/    реестр сервисов и маршрутизация событий├── vmmdev/            протокол VMMDev: пакеты, возможности, фильтр событий├── hgcm/              HGCM-транспорт: connect/disconnect/call, PageList├── sys/               точки входа ОС: init, shutdown, ioctl├── services/          8 подключаемых сервисов│   ├── mouse/         абсолютная мышь│   ├── display/       авторесайз экрана│   ├── heartbeat/     сигнал "гость жив"│   ├── timesync/      синхронизация RTC с хостом│   ├── guest_props/   хранилище ключ-значение│   ├── shared_folders/ виртуальная ФС через SHFL│   ├── clipboard/     буфер обмена│   └── seamless/      бесшовный режим├── data/              структуры и константы каждого сервиса└── common/            утилиты (перекодировка UTF-8 <—> CP866)

Если же смотреть не на файловое дерево, а на то, как эти каталоги взаимодействуют в работающем драйвере, получится следующий стек. Сверху — точки входа со стороны приложений KolibriOS, снизу — реальное железо VMMDev на PCI-шине. Стрелки сверху вниз — это запросы и команды (IOCTL от приложений, исходящие пакеты в гипервизор); IRQ-события и DMA-ответы хоста идут в обратную сторону. Каждый блок схемы разбирается отдельно ниже: «Протокол VMMDev», «Инициализация», «Обработка прерываний», «Система сервисов», «HGCM», «Сервисы».

 

Архитектурный стек драйвера

Архитектурный стек драйвера

Как думать об этом драйвере

Всё, что описано в статье ниже, удобно держать в голове как четыре слоя с жёстко заданными границами ответственности — каждый верхний знает только про слой непосредственно под собой.

  1. VMMDev — транспорт. Один порт, один кусок MMIO, одно прерывание. Умеет: «отправь пакет», «прочитай флаги событий», «получи прерывание». Не умеет: имена, сессии, большие буферы. Ничего не знает про «сервисы», для него есть только пакеты разных типов.

  2. HGCM — RPC поверх VMMDev. Добавляет именованные сервисы хоста, идентификаторы соединений, типизированные параметры (включая PageList для DMA), асинхронные вызовы. Граница: всё, что выше HGCM, про VMMDev уже не знает.

  3. Dispatcher — сводит вместе подписки на события и хуки сервисов. Решает, кому из сервисов уходит каждое прерывание и каждый тик таймера. Вычисляет суперсет масок и сообщает его хосту.

  4. Services — собственно фичи: мышь, дисплей, shared clipboard, shared folders и так далее. Каждый сервис живёт в своём каталоге, реализует одинаковый набор хуков (fn_init/fn_enable/fn_on_event/fn_on_tick/…) и не знает ни про PCI, ни про VMMDev-пакеты, ни про таблицы маршрутизации прерываний.

В этой вертикальной структуре (стек слоёв, нарисованный выше) работают два встречных информационных потока:

  • Сверху вниз — управляющие команды: приложение делает IOCTL -> sys/ маршрутизирует -> сервис формирует HGCM-вызов -> HGCM упаковывает в VMMDev-пакет -> out в BAR0.

  • Снизу вверх — события и ответы: прерывание от VMMDev -> core/irq читает маску -> dispatcher_dispatch вызывает обработчики сервисов. Параллельно: хост, обработав HGCM-запрос, DMA-записью выставляет флаг REQ_DONE в страницу пакета. Гость в spin-loop или по тику таймера видит этот флаг и завершает ожидание.

Дальше каждый слой разбирается отдельно — в том же порядке, что и в этом списке.

Три места, где драйвер уходит от upstream-модели

Бо́льшая часть инженерной работы в драйвере — не переписывание известных алгоритмов, а три точки, где была сознательно выбрана не общая для upstream схема. К ним стоит возвращаться по мере чтения статьи:

  1. Shared Folders (структуры SHFL). Хост на любую ошибку в параметрах файловой операции отвечает одинаковым VERR_INVALID_PARAMETER без указания, в каком именно поле проблема. В upstream такой код лечится отладчиком и общими заголовками. Здесь, без эталонных заголовков для FASM, основным методом отладки стало побайтное сравнение собственного пакета с тем, что шлёт работающая реализация.

  2. Clipboard (асинхронный listener). MSG_OLD_GET_WAIT константа протокола Shared Clipboard (старая версия), обозначающая функцию «ждать изменения буфера обмена», может висеть минутами, пока пользователь хоста что-нибудь не скопирует. В upstream это решается wait-queue: поток драйвера спит, его будят по событию. В KolibriOS аналог есть — события ядра (CreateEvent + WaitEventTimeout), — но в текущей версии драйвер их не подключил. Вместо этого выделен отдельный буфер и проверка флага REQ_DONE раз в 100 мс из тика таймера, мимо общего HGCM-слота и мимо spin-loop. Это работает, но является техническим долгом: переход на блокирующее ожидание уберёт зависимость от тика и сократит задержку реакции (сейчас до 100 мс).

  3. Display resize (задержка 500 мс). Драйвер искусственно задерживает применение нового разрешения на 500 мс. Это не связано с обилием событий от хоста (их немного), а вызвано тем, что частое применение SetScreen приводит к крашу taskbar’а из-за ошибки в ядре KolibriOS (окна не обрезаются при уменьшении экрана). Задержка — эмпирический костыль, снижающий вероятность краша при активном перетаскивании окна.

Эти три места обозначены здесь сразу, чтобы при чтении соответствующих разделов было понятно, почему конкретные решения выглядят непривычно с точки зрения upstream.

Протокол VMMDev

VMMDev — единственная точка входа в гипервизор. Всё взаимодействие гостя с хостом проходит через неё: синхронные команды (объявить возможности, получить версию хоста, синхронизовать время), асинхронные события (мышь, дисплей, clipboard, heartbeat), RPC-вызовы поверх HGCM. Понять, как устроен драйвер, нельзя, не разобравшись с этим слоем — все остальные секции статьи стоят на нём.

Физически это виртуальное PCI-устройство с Vendor ID 0x80EE и Device ID 0xCAFE. На него VirtualBox вешает один I/O-порт, окно MMIO и линию прерывания — три ресурса, через которые драйвер делает всё остальное.

VirtualBox предоставляет драйверу три ресурса:

  • BAR0 (I/O-порт) — номер порта в 16-битном I/O-пространстве x86. В этот порт драйвер записывает 32-битное значение — физический адрес пакета в гостевой RAM. Это сигнал гипервизору: «возьми этот пакет и обработай».

  • BAR1 (MMIO, 4 МБ) — окно памяти, из которого драйвер читает флаги событий. Запросы через MMIO не отправляются — только через порт.

  • IRQ — читается из PCI Configuration Space, поле interrupt_line (offset 0x3C в header type 00). Используется для асинхронных уведомлений о событиях и о завершении HGCM-запросов.

Заголовок пакета

Каждый запрос к VMMDev начинается с 24-байтного заголовка (VMMDEV_HEADER) — это формат протокола v1_04, в котором появилось поле f_requestor. В v1_03 заголовок был 20 байт (без f_requestor); подробнее о несовместимости версий — в разделе «Проблемы».

size        dd    - размер всего пакетаversion     dd    - 0x00010001 (VMMDEV_REQUEST_HEADER_VERSION)request     dd    - тип запроса (VMMDEV_REQ_*)rc          dd    - код возврата (заполняет хост)reserved    dd    - 0f_requestor dd    - VMMDEV_REQUESTOR_VBOXGUEST | флаги режима

Заголовок нужен, чтобы гипервизор понял: кто прислал запрос, какого типа, каких размеров и какой версии протокола ожидает. Поле f_requestor кодирует уровень доверия: в этом драйвере туда всегда пишется VMMDEV_REQUESTOR_VBOXGUEST — признак «это ядерный запрос от VBoxGuest-драйвера». Хост по этому полю может различать запросы из разных источников (пользовательское приложение, другой драйвер) и разрешать каждой категории свой набор операций.

Поле rc перед отправкой инициализируется sentinel-значением, по которому гость потом узнает, что ответ действительно записан хостом. Для обычных VMMDev-запросов сюда пишут ноль (так инициализируются capabilities, event_filter, timesync, mouse, display, heartbeat, seamless): ноль (VINF_SUCCESS) — это валидный успешный код, поэтому VMMDev-запросы по rc напрямую не различают «обработано» / «не обработано», а полагаются на синхронность out-инструкции. Для HGCM-запросов в rc ставится VERR_GENERAL_FAILURE (см. hgcm/hgcm.inc и data/hgcm/constants.inc) — здесь важно, чтобы начальное значение не было валидным успехом, иначе ожидающий код не отличит «хост ещё не ответил» от «хост ответил rc=0».

Отправка запросов — синхронно, через порт

Порт BAR0 работает как триггер: сама запись в него не несёт данных — она запускает обработку на стороне гипервизора. Пакет при этом целиком лежит в гостевой RAM, а через порт уходит только 32-битное значение — физический адрес страницы с пакетом. Драйвер заранее (на старте) выделяет страницу под VMMDev-пакеты, при каждом запросе заполняет в ней нужный пакет и пишет физический адрес в BAR0:

; drivers/vboxguest/core/ports.incproc vmmdev_send_request uses edx, phys_addr:dword    mov eax, [phys_addr]          ; физ. адрес страницы с пакетом    mov dx,  [vbox_device.port]   ; BAR0    out dx,  eax                  ; триггер: "забери пакет по этому адресу"    mov ecx, 1000000              ; запас на завершение обработки.wait:    xor eax, eax    loop .wait    retendp

Схематично:

 Отправка запроса через VMMDev

Отправка запроса через VMMDev

Почему spin-loop не проверяет rc. Может показаться, что цикл .wait после out опрашивает ответ — на самом деле он ничего не считывает, и это намеренно:

  • Инструкция out к перехваченному порту вызывает синхронный VMEXIT: процессор передаёт управление гипервизору, тот читает пакет, выполняет запрос, записывает результат в ту же гостевую страницу и возвращает управление гостю. К моменту, когда out завершён с точки зрения гостя, поле rc уже обновлено — проверять флаг готовности нечего.

  • Цикл — страховка на случай нестандартных режимов эмуляции, где запись ответа могла бы задержаться. Для штатного пути избыточен, но стоит дёшево.

Это принципиально отличает VMMDev-вызов от HGCM-вызова: у первого ответ уже лежит в памяти, когда out вернулся, у второго — придётся ждать асинхронной DMA-записи флага DONE (см. раздел [Асинхронное ожидание](#асинхронное-ожидание)).

Для обычных синхронных запросов (получить версию, сообщить возможности, установить маску событий) управление возвращается уже с готовым результатом — этого достаточно для всего слоя VMMDev.

Окно MMIO — только флаги событий

BAR1 — это 4 МБ виртуального окна в физической памяти, куда отображена структура VMMDEV_MEMORY. В VBoxGuest для KolibriOS из неё реально читается одно поле — have_events (offset 0x08): флаг, есть ли для гостя новые события. Больше ничего оттуда драйвер не использует.

Инициализация

Порядок шагов запуска жёстко определён цепочкой зависимостей. Нельзя установить IRQ-обработчик, пока не выделена страница под пакеты (обработчик может прийти в любой момент после AttachIntHandler, и ему уже нужны буферы). Нельзя включать сервисы, пока хост не знает про capabilities (хост не пришлёт нужные события). Нельзя регистрировать драйвер в ОС, пока не настроены и фильтр событий, и capabilities (приложение может тут же вызвать IOCTL и обратиться к ещё не готовому сервису). Поэтому последовательность строго линейна: либо все шаги проходят успешно, либо драйвер не регистрируется в системе.

vmmdev_probe  pci_find_vmmdev          vendor=0x80EE, device=0xCAFE  pci_init_vmmdev_irq      PCI cfg space: interrupt_line @ 0x3C  pci_init_vmmdev_bar      BAR0=I/O-порт, BAR1=физ. адрес MMIOmmio_map_vmmdev            4 МБ виртуального окна  mmio_check_version       проверка magic/version в VMMDEV_MEMORYports_init                 валидация I/O-порта  vmmdev_init_packets        1 страница (4 КБ) под все VMMDev-пакеты:    +0:   GET_HOST_VERSION    +64:  REPORT_GUEST_INFO    +128: REPORT_GUEST_INFO2    +320: SET_GUEST_CAPABILITIES    +384: CTL_GUEST_FILTER_MASK    +448: ACKNOWLEDGE_EVENTSvmmdev_check_versionvmmdev_init_protocol       handshake: REPORT_GUEST_INFO (VBOXOSTYPE=0x90000)hgcm_init                  3 страницы (12 КБ) под HGCM-пакетыtimer_init                 TimerHS(10, 10, timer_cb, 0) -> тик каждые 100 мсvmmdev_irq_install         AttachIntHandler(irq, vbox_irq_handler, 0)dispatcher_init_all        fn_init для каждого сервисаdispatcher_enable_autostart включить сервисы с autostart=1vmmdev_update_event_filter сообщить хосту маску нужных событийvmmdev_update_capabilities сообщить хосту возможности гостяRegService("VBOXGUEST", service_proc)

Первая часть — PCI probing: найти VMMDev по Vendor/Device ID, прочитать BAR’ы и номер прерывания. Затем маппится MMIO-окно и валидируется I/O-порт — это фактически проверка «это действительно VMMDev, а не что-то с таким же идентификатором».

Далее драйвер однократно выделяет одну страницу под VMMDev-пакеты и три — под HGCM. Пакеты предсоздаются «на все случаи жизни» и лежат по фиксированным смещениям. Это важно: под VMMDev нельзя выделять память в обработчике прерывания, а под HGCM — тем более, потому что один HGCM-вызов может требовать и основной пакет, и описатели страниц для DMA.

После инициализации транспорта инсталлируется обработчик прерывания и таймер. Сервисы получают fn_init в порядке регистрации, затем те из них, у которых autostart=1, переходят в состояние enabled.

В самом конце драйвер сообщает хосту две маски — событий и возможностей гостя (об этом подробнее в секции про диспетчер), после чего регистрируется в системе под именем VBOXGUEST и становится доступен пользовательским приложениям через стандартный IOCTL-механизм KolibriOS.

Обработка прерываний

VMMDev — единственный источник асинхронных уведомлений в драйвере. Через одну линию прерывания приходят все события сразу: мышь подвинулась, разрешение изменилось, heartbeat запрошен, HGCM-вызов завершился. Маска в FAST-порту говорит, что именно случилось.

Из этого вытекают два требования, которые задают всю конструкцию обработчика. Во-первых, на «горячем» пути нельзя делать ничего тяжёлого — иначе реакция мыши заметно замедлится, а соседние прерывания (таймер, IDE) начнут пропускать сроки. Во-вторых, реальная обработка событий по сути своей разная: одни сервисы хотят отреагировать мгновенно (мышь — обновить координаты тут же), другим достаточно проснуться раз в 100 мс (display — debounce ресайза, clipboard — проверка флага). Эти две группы естественно лягут в разные контексты выполнения — то, что в Linux называют верхней и нижней половиной. Здесь роль нижней половины играет обычный периодический таймер, что разбираем ниже.

Драйвер при каждом прерывании обязан:

  1. Подтвердить, что прерывание — «его»;

  2. Прочитать маску произошедших событий;

  3. Послать ACK — иначе хост будет генерировать прерывания бесконечно;

  4. Разослать событийные уведомления заинтересованным сервисам.

Верхняя и нижняя половина

Термин — отсылка к Linux: верхняя половина выполняется в контексте прерывания и должна быть максимально короткой; нижняя — отложенная работа в более спокойном контексте (в Linux это tasklet, workqueue, threaded IRQ). В KolibriOS аналога tasklet’ов нет — роль нижней половины играет обычный периодический таймер. Не так выразительно, но для гостевого драйвера хватает: всё, что не требует мгновенной реакции, можно делать раз в 100 мс.

Верхняя половина

; drivers/vboxguest/core/irq.inc; Читаем маску событий через FAST I/O-порт (offset +8 от BAR0)mov dx, [vbox_device.port]add dx, VMMDEV_PORT_OFF_REQUEST_FASTin  eax, dxtest eax, eaxjz  .not_ours              ; не наше прерывание; ACK: подтверждаем принятые событияout dx, eax; Маршрутизация по сервисамstdcall dispatcher_dispatch, eaxmov eax, 1ret

Почему верхняя половина максимально короткая — два соображения, важных конкретно здесь:

  • Пока прерывание обрабатывается, на этом ядре заблокированы все остальные прерывания того же уровня. Длинная обработка увеличивает задержку таймера, IDE, сети.

  • В контексте прерывания нельзя вызывать hgcm_wait_async (он держит CPU миллисекундами) и нельзя выделять память. Тяжёлую работу в верхнюю половину не положить даже при желании.

 Поэтому верхняя делает минимум: читает маску, подтверждает её и вызывает диспетчер. А «сами» сервисы обрабатывают событие уже в своих fn_on_event — но даже там хороший тон оставлять тяжёлое на fn_on_tick.

Схема прохождения прерывания:

Схема прохождения прерывания

Схема прохождения прерывания

Нижняя половина — тик таймера

Тик таймера: нижняя половина

Тик таймера: нижняя половина

Тик используется:

  • display — debounce запросов ресайза;

  • heartbeat — периодическая отправка «гость жив» раз в секунду;

  • timesync — синхронизация времени раз в минуту;

  • clipboard — state machine слушателя буфера обмена;

  • seamless — debounce смены режима.

Система сервисов

Главная задача — собрать модульный драйвер на чистом ассемблере, без runtime-инфраструктуры. Ни VMT, ни классов, ни register_filesystem, ни reflection: то, на чём подобные вещи держатся в C/C++/Rust, в FASM либо отсутствует, либо стоит дороже самой задачи.

Естественный для ассемблера путь — один большой обработчик всех событий, в котором по битам маски ветвление на блоки; аналогичный switch — для тика, для IOCTL, для capabilities. Логика плоская, никаких таблиц. Работает, но плохо масштабируется: каждый новый сервис требует ручной правки трёх-четырёх центральных файлов (irq, timer, ioctl, capabilities), уникального ID для IOCTL, обновления маски событий, добавления записи в список включаемых файлов. Любая забытая правка — пропущенное прерывание или поломанная enable/disable-логика. С восемью сервисами это уже трудно сопровождать, с двадцатью — нереально.

Вместо этого — диспетчер плюс таблица хуков с фиксированной формой SERVICE_ENTRY. Каждый сервис — это запись в таблице с одинаковыми полями (fn_init, fn_enable, fn_disable, fn_on_event, fn_on_tick, маски событий и capabilities). Ядро не знает, что это за сервис — оно просто проходит таблицу и зовёт нужный хук. Сервис в свою очередь не знает про другие сервисы — он отвечает только на свои события.

Специфика в том, что таблица заполняется на этапе компиляции, не в runtime. Каждый файл сервиса в конце вызывает макрос REGISTER_SERVICE, который не генерирует кода, а накапливает строку в compile-time переменной. Финальный BUILD_SERVICE_TABLE в vboxguest.asm разворачивает накопленное в реальные dd-записи. В исполняемом образе регистраций не существует — есть только готовая services_table. Детали реализации — ниже, в подразделе «Авторегистрация через макросы».

В итоге, добавление сервиса — это создать каталог, написать REGISTER_SERVICE в конце файла и добавить include в services/services.inc. Никаких правок в vboxguest.asm, никакой ручной нумерации, никаких «не забудь обновить маску». Это решение определяет всё остальное в драйвере: глобальные маски (рантайм-OR хуков), плагинная файловая структура, разделение IRQ-контекста и тик-контекста — всё подвязано на одну и ту же таблицу.

Дальше разберём, как это устроено по уровням: сначала структура SERVICE_ENTRY и регистрация, потом диспетчеризация события, потом глобальные маски, потом сами макросы. Вся специфика каждого сервиса при этом локализована в собственной директории (services/mouse/, services/clipboard/ и т.д.), а диспетчер ведёт таблицу всех сервисов, их состояния и масок, и по приходу события решает, кого уведомить. 

Структура сервиса

Каждый сервис описывается структурой SERVICE_ENTRY:

id              dd  - порядковый номер (с 1)name_ptr        dd  - указатель на строку имениevent_mask      dd  - маска VMMDev-событийcaps_mask       dd  - маска возможностей гостяenabled         dd  - 0/1autostart       dd  - 1 = включить при стартеfn_init         dd  - инициализацияfn_enable       dd  - включениеfn_disable      dd  - выключениеfn_on_event     dd  - обработчик IRQ-событияfn_on_tick      dd  - обработчик тика таймера

Таблица строится на этапе компиляции макросом BUILD_SERVICE_TABLE (детали ниже, в секции про макросы). Регистрация нового сервиса — одна строка:

REGISTER_SERVICE svc_my_name, MY_EVENT_MASK, MY_CAPS, \    my_init, my_enable, AUTOSTART_MY, \    my_on_event, my_on_tick

Диспетчеризация события

Как сервис узнаёт, что пришедшее событие — для него? Диспетчер идёт по всей таблице и для каждого включённого сервиса делает AND маски:

; drivers/vboxguest/core/dispatcher/dispatcher.incproc dispatcher_dispatch stdcall, event_mask:dword    mov edx, [event_mask]    mov ecx, [services_count]    mov esi, services_table.loop:    cmp dword [esi + SERVICE_ENTRY.enabled], 0    je .next     mov eax, [esi + SERVICE_ENTRY.event_mask]    test eax, edx            ; AND: есть ли пересечение?    jz .next    call [esi + SERVICE_ENTRY.fn_on_event].next:    add esi, sizeof.SERVICE_ENTRY    dec ecx    jnz .loopendp

Сервис получает все биты событий, которые хост поднял в этот раз — отфильтровать «свои» он должен сам. На практике обычно поднимается ровно один бит (событие уникально), так что этот шаг тривиален.

Глобальные маски

Хост по своей инициативе не поднимает произвольные события — он посылает только те, на которые гость подписался через CTL_GUEST_FILTER_MASK. Аналогично, хост ориентируется на возможности гостя, которые тот объявил через SET_GUEST_CAPABILITIES.

Диспетчер хранит два поля:

dispatcher_active_events - OR всех event_mask включённых сервисовdispatcher_active_caps   - OR всех caps_mask включённых сервисов

Они пересчитываются при каждом enable/disable сервиса (при старте или через IOCTL) и отправляются хосту двумя отдельными VMMDev-запросами. Разделение двух масок — потому что это разные вещи: events — это подписка на уведомления, caps — это декларация, что гость поддерживает ту или иную фичу (например, абсолютное позиционирование мыши или shared folders).

После включения/выключения сервиса через IOCTL SVC_ENABLE (2) или SVC_DISABLE (3):

  1.  Пересчитываются dispatcher_active_events и dispatcher_active_caps.

  2. vmmdev_update_event_filter() — хост обновляет фильтр событий и перестаёт посылать ненужные уведомления.

  3. vmmdev_update_capabilities() — хост видит актуальный список фич гостя.

Авторегистрация через макросы

Одна из ключевых задач при проектировании драйвера — как добавить новый сервис, не трогая никакой «центральной» таблицы вручную. В языках высокого уровня это решается reflection или registration-on-include. В FASM пришлось придумать compile-time механизм на основе строковых макросов.

Традиционный подход — вручную вести список в services.inc — ошибкоёмок: добавляя сервис, надо не забыть про структуру, про таблицу, про include. Эта работа нигде не локализована, и пропустить что-то — легко.

Решение на FASM использует два приёма: переопределение констант через equ и сопоставление строк через match.

Этап 1 накопление списка на этапе компиляции. В конце каждого файла сервиса стоит одна строка REGISTER_SERVICE, которая не генерирует никаких байт в коде, только обновляет compile-time строку __SERVICES_LIST__:

macro REGISTER_SERVICE svc_name, event_mask, caps_mask, \                       fn_init, fn_enable, fn_disable, \                       fn_on_event, fn_on_tick, autostart{    ; Если список уже не пуст - добавляем через запятую    match any, __SERVICES_LIST__ \{        __SERVICES_LIST__ equ __SERVICES_LIST__,\            svc_name|event_mask|caps_mask|fn_init|fn_enable|\            fn_disable|fn_on_event|fn_on_tick|autostart    \}    ; Если список пуст - первая запись без запятой    match , __SERVICES_LIST__ \{        __SERVICES_LIST__ equ \            svc_name|event_mask|caps_mask|fn_init|fn_enable|\            fn_disable|fn_on_event|fn_on_tick|autostart\    }}

Этап 2 генерация таблицы. В конце vboxguest.asm стоит BUILD_SERVICE_TABLE, который разворачивает накопленный список в реальные данные:

macro BUILD_SERVICE_TABLE{    align 4    services_table:    match list, __SERVICES_LIST__ \{        __BUILD_ENTRIES__ list    \}    services_table_end:    services_count dd __BUILD_ID__}__BUILD_ID__ = 0macro __BUILD_ENTRIES__ [entry]{    forward        __BUILD_ID__ = __BUILD_ID__ + 1        match name|ev|_cap|_init|_en|_dis|_onevent|_tick|_auto, entry \{            dd __BUILD_ID__     ; id (1, 2, 3, ...)            dd name            ; nameptr            dd ev              ; eventmask            dd cap             ; capsmask            dd 0                ; enabled = 0            dd _auto            ; autostart            dd init            ; fninit            dd en              ; fnenable            dd dis             ; fndisable            dd onevent         ; fnon_event            dd tick            ; fnon_tick        \}}

Ключевой момент — forward в __BUILD_ENTRIES__. Он заставляет FASM обрабатывать каждый элемент вариадического аргумента [entry] слева направо, и __BUILD_ID__ инкрементируется на каждом шаге. Так сервисы получают уникальные ID 1, 2, 3, … строго в порядке включения файлов.

Что это даёт: добавить новый сервис — это

  1. создать директорию с реализацией,

  2. в конце файла написать REGISTER_SERVICE ...,

  3. добавить include в services/services.inc.

Никаких изменений в vboxguest.asm, никакой ручной нумерации ID, никаких «не забудь прописать в таблице».

Схема сборки таблицы сервисов на этапе компиляции:

Сборка таблицы сервисов на этапе компиляции

Сборка таблицы сервисов на этапе компиляции

 Аналогичный приём — накопление через equ — используется для расчёта глобальных масок. В data/core/constants.inc маски начинаются с нуля:

VBOXGUEST_EVENTS_OR_MASK equ 0VBOXGUEST_GUEST_CAPS_OR_MASK equ 0

Каждый data/<svc>/constants.inc дописывает своё:

; data/mouse/constants.incVBOXGUEST_EVENTS_OR_MASK equ (VBOXGUEST_EVENTS_OR_MASK or MOUSE_EVENT_MASK)VBOXGUEST_GUEST_CAPS_OR_MASK equ (VBOXGUEST_GUEST_CAPS_OR_MASK or VMMDEV_GUEST_SUPPORTS_MOUSE)

Порядок включения файлов фиксирован, но на результат он не влияет (OR коммутативен). К моменту компиляции значение VBOXGUEST_EVENTS_OR_MASK (суперсет всех событий, известных драйверу) уже вычислено ассемблером — оно подставляется как начальное значение or_mask в шаблон пакета VMMDEV_CTL_GUEST_FILTER_MASK. Это не путать с runtime-значением dispatcher_active_events, которое пересчитывается на каждом enable/disable и реально отправляется через vmmdev_update_event_filter: compile-time маска — это «всё, что драйвер вообще умеет», а runtime — «что включено прямо сейчас».

HGCM — Host-Guest Communication Manager

По сути HGCM добавляет три вещи поверх VMMDev: именованные RPC-сервисы (имя сервиса, номер функции, типизированные параметры), DMA для больших буферов (вместо копирования передаём список физических страниц), и lifecycle соединения (connect → серия calldisconnect, независимо от других сервисов).

VMMDev — это транспорт без имён: туда уходят пакеты, оттуда приходят флаги. Этого хватает для маленького набора встроенных команд (capabilities, event filter, mouse, display), но не хватает для всего остального — shared folders, clipboard, guest properties. У этих сервисов другие требования: им нужны имена, отдельные жизненные циклы соединений, разные политики (синхронный вызов / долгая подписка / fire-and-forget) и абстракция передачи больших буферов через DMA. HGCM — это слой, который добавляет всё перечисленное поверх VMMDev.

Граница HGCM-обёртки строгая: сервисы выше HGCM ничего не знают про VMMDev. Они оперируют тремя сущностями: client_id (хендл соединения), номером функции и массивом параметров. Как именно пакет попадает на хост, как считывается ответ, как линейный буфер превращается в список физических страниц — всё это закрыто внутри HGCM-обёртки. Тот же shared_folders или clipboard теоретически можно было бы перенести на другой транспорт, не трогая логику сервиса — она про SHFL (Shared Folders) и SHCL (Shared Clipboard), не про PCI и порты.

С точки зрения хоста HGCM-сервисы — это VBoxSharedFolders, VBoxSharedClipboard, VBoxGuestPropSvc и так далее: именованные RPC-эндпоинты, к которым гость подключается по строке и общается дальше через числовой client_id

Жизненный цикл соединения

hgcm_connect("VBoxSharedFolders")      -> client_id (32-битный handle)hgcm_call32_pagelist(client_id, fn, params, ...)    1. Заполнить HGCM_CALL в pre-allocated буфере    2. Для каждого LINADDR-параметра: построить PAGE_LIST       (физические адреса страниц буфера)    3. vmmdev_send_request(phys_addr)    4. hgcm_wait_async()    <- ждём VBOX_HGCM_REQ_DONEhgcm_disconnect(client_id)

Сначала — connect. Драйвер отправляет VMMDev-запрос VMMDEV_REQ_HGCM_CONNECT с именем сервиса в структуре HGCM_SERVICE_LOCATION. Хост ищет сервис по имени, создаёт новый контекст соединения и возвращает client_id — 32-битный handle, который гость будет передавать в каждом последующем вызове как первый аргумент. Такой handle не имеет смысла вне своего соединения: это просто ключ в таблице хоста

Затем — call: собственно RPC-вызов. У него есть номер функции (fn), зависящий от конкретного сервиса (например, для SharedFolders это SHFL_FN_READ, SHFL_FN_WRITE и так далее), и массив параметров.

Наконец — disconnect: освободить контекст на стороне хоста. Важный нюанс: если соединение ждёт асинхронного ответа (как clipboard с MSG_OLD_GET_WAIT), обычный disconnect может заблокироваться на долгое время. Clipboard использует fire-and-forget — об этом ниже.

Общая схема HGCM-вызова: 

HGCM-вызов: последовательность шагов

HGCM-вызов: последовательность шагов

Разделение буферов между сервисами

 Гостевых HGCM-пакетов всего три на весь драйвер (аллоцируются в hgcm_init_packets, hgcm/hgcm.inc):

core:   +-------------------+   | hgcm_connect_virt |  4 КБ  - connect / disconnect   +-------------------+   | hgcm_call_virt    |  4 КБ  - обычные HGCM_CALL   +-------------------+   | hgcm_call_pl_virt |  4 КБ  - HGCM_CALL с PageList   +-------------------+      ^      | используют синхронно:      | shared_folders, guest_props, write-путь clipboard   clipboard (свой пакет, потому что listener работает в асинхронном режиме —              он не блокирует приложение и может ждать изменения буфера обмена минутами):   +---------------------------+   | clip_state.listen_pkt_virt |  4 КБ - MSG_OLD_GET_WAIT   +---------------------------+      ^      | проверяется раз в 100 мс в clipboard_tick

Как сервис понимает, что ответ пришёл именно ему? Корреляция идёт по адресу пакета, а не по IRQ-маске. Shared folders и guest_props делают вызов синхронно: hgcm_wait_async опрашивает флаг DONE конкретно того пакета, который только что отправлен, — и возвращается только по нему. Гонок между сервисами нет, потому что весь путь от отправки до ожидания — однопоточный. Clipboard listener, в отличие от них, не блокируется, а получает свою отдельную страницу — чтобы не занимать общий HGCM-слот десятками секунд, пока хост ждёт изменения буфера обмена.

Прерывание VMMDEV_EVENT_HGCM в этой реализации играет вспомогательную роль: DMA-запись флага DONE в заголовок пакета сама по себе уже достаточна, чтобы spin-loop или тик увидели ответ. Подписка на это событие в маске остаётся — формально хост ожидает её для HGCM-сервисов и без неё может не доставить уведомления в нештатных режимах эмуляции, — но реально на корректность пути «отправил -> дождался DONE» она не влияет.

Заголовок HGCM

[VMMDev header 24B]flags   dd  - VBOX_HGCM_REQ_DONE, VBOX_HGCM_REQ_CANCELLEDresult  dd  - код возврата сервиса

flags — ключевое поле. Когда гость отправляет запрос, flags = 0; когда хост обрабатывает запрос, он DMA-пишет VBOX_HGCM_REQ_DONE. Если запрос был отменён (например, при shutdown’е хоста) — VBOX_HGCM_REQ_CANCELLED. По этому полю гость понимает, что ответ готов.

Типы параметров

Фактически используемые в коде типы:

Тип

Значение

Назначение

HGCM_PARM_TYPE_32BIT

1

скаляр

HGCM_PARM_TYPE_64BIT       

2

64-битный скаляр

HGCM_PARM_TYPE_LINADDR 

4

двунаправленный буфер

HGCM_PARM_TYPE_LINADDR_IN

5

входной буфер (гость -> хост)

HGCM_PARM_TYPE_LINADDR_OUT

6

выходной буфер (хост -> гость)

HGCM_PARM_TYPE_PAGELIST

10

список физических страниц (DMA)

PageList и DMA

Для больших буферов (чтение файла на десятки КБ, передача содержимого буфера обмена) передавать линейный адрес бесполезно: хост не знает отображения гостевой виртуальной памяти в физическую. Решение — PAGE_LIST: дескриптор, в котором перечислены физические адреса всех страниц буфера.

Пример: буфер размером 8 КБ по линейному адресу 0x12345678:

cPages       = 3                        ; буфер покрывает 3 виртуально-смежные                                        ; страницы: 0x12345000..0x12347FFF                                        ; (последний байт буфера - 0x12347677)aPages[0]    = GetPhysAddr(0x12345000)  ; физически эти страницы могут лежатьaPages[1]    = GetPhysAddr(0x12346000)  ; в любых местах гостевой RAM -aPages[2]    = GetPhysAddr(0x12347000)  ; именно поэтому хосту нужен PageList

Хост пишет в страницы DMA напрямую — накладные расходы на промежуточное копирование отсутствуют, а гость не тратит ресурсов на временные буферы.

Асинхронное ожидание

После отправки HGCM-запроса ответ не приходит мгновенно — хост может обрабатывать запрос несколько микросекунд (быстрые сервисы вроде guest properties), а может и неограниченно долго (clipboard listener, ждущий изменений в буфере обмена хоста). Ожидание реализовано простым spin-loop с pause:

; drivers/vboxguest/hgcm/async.incproc hgcm_wait_async uses ebx ecx edx esi, request_ptr:dword    mov ecx, HGCM_TIMEOUT_ITERS   ; = 500000.wait_loop:    mov eax, [esi + HGCM_HEADER.flags]    test eax, VBOX_HGCM_REQ_DONE    jnz .completed    test eax, VBOX_HGCM_REQ_CANCELLED    jnz .cancelled    dec ecx    jz .timeout    ; PAUSE-based delay (внешний счётчик ecx сохраняем)    push ecx    mov ecx, 1000.delay:    pause    loop .delay    pop ecx    jmp .wait_loopendp

Поток ожидания опрашивает флаг VBOX_HGCM_REQ_DONE в памяти, пока хост не выставит его DMA-записью из гипервизора. Плюс — простота и независимость от примитивов синхронизации ядра. Минус — во время ожидания поток потребляет CPU.

Почему spin-loop и таймер, а не блокирующее ожидание?

В KolibriOS есть события ядра — этого достаточно, чтобы переписать hgcm_wait_async как блокирующее ожидание, поднимаемое из IRQ-обработчика. В текущей версии драйвера эта схема не подключена: длинные ожидания в clipboard-listener’е разнесены в собственный буфер с проверкой по тику, а коротким HGCM-вызовам spin-loop’а инженерно хватает. Это осознанный технический долг — переход на блокирующее ожидание уберёт затраты CPU при ожидании и сделает clipboard-listener менее «таймерным».

В upstream та же развилка решается через штатные блокирующие примитивы: wait-queue на Linux, KEVENT на Windows, RTSemEventMulti на FreeBSD (подробнее — в сравнительной секции в конце статьи).

Сценарий: чтение буфера обмена сквозь все слои

Чтобы все четыре слоя сложились в одну картинку, разберём один вызов целиком — CLIP_READ через IOCTL. Это самый показательный путь: задействованы все слои, есть и синхронная (read), и асинхронная (listener) части HGCM.

1. Приложение KolibriOS:   delegate_io(VBOXGUEST, IOCTL_CLIP_READ, in_buf, in_size, ...)                                                                    [Приложение]                                                                          |                                                                          v2. sys/ioctl.inc:                                                       [sys/]   диспетчеризация по коду 11 -> clip_ioctl_read                                                                          |                                                                          v3. services/clipboard/clipboard_io.inc:                          [Сервис clipboard]   - аллоцирует kernel-буфер (max(in_size, CLIP_INITIAL_BUF_SIZE))   - формирует HGCM_CALL: fn = MSG_OLD_DATA_READ                          parm0 = format_id (32BIT)                          parm1 = LINADDR_OUT(buf, size)                                                                          |                                                                          v4. hgcm/call.inc:                                                       [HGCM]   - hgcm_call32_pagelist():     -> для LINADDR-параметра строит PAGE_LIST с физ. адресами     -> заполняет HGCM_CALL header (client_id, fn, parm_count)     -> rc = VERR_GENERAL_FAILURE (sentinel)                                                                          |                                                                          v5. core/ports.inc -> vmmdev_send_request(phys):                       [VMMDev]   - out dx, eax (eax = phys_addr пакета HGCM_CALL)   - ============= VMEXIT =============   - хост: видит VMMDEV_REQ_HGCM_CALL,           разрешает PAGE_LIST в физ. страницы гостя,           вызывает clipboard service на хосте,           тот пишет данные в страницы гостя через DMA,           ставит HGCM_HEADER.flags |= REQ_DONE   - ===========================   - управление возвращается гостю                                                                          |                                                                          v6. hgcm/async.inc -> hgcm_wait_async():                               [HGCM]   - для clip read - короткий вызов, флаг DONE уже стоит     (хост обработал в рамках того же VMEXIT)   - возвращает result из HGCM_HEADER.result                                                                          |                                                                          v7. clip_ioctl_read:                                              [Сервис clipboard]   - если result == ERR_BUF_TOO_SMALL и actual_size > kern_size:       перевыделить буфер, повторить с шага 3   - копирует kernel-буфер в user-буфер приложения                                                                          |                                                                          v8. Приложение получает данные.

Параллельно, начиная с момента clipboard_enable, в драйвере уже висит MSG_OLD_GET_WAIT — отдельный пакет на отдельной странице, отправленный без ожидания. Хост в этот пакет ничего не пишет до тех пор, пока пользователь не скопирует что-нибудь в буфер обмена на стороне хоста. Каждые 100 мс тик таймера зовёт clipboard_tick, который проверяет флаг DONE в этой странице — мимо hgcm_wait_async и мимо общего HGCM-слота. Когда флаг наконец поднят, listener дёргает clip_status (форматы появились), и следующий CLIP_READ от приложения пройдёт по сценарию выше.

То есть в одном сервисе одновременно живут оба паттерна, разобранных в этом разделе: короткий синхронный путь через hgcm_wait_async (read/write по запросу) и долгий асинхронный путь через выделенный пакет + проверка тиком (listener). Именно поэтому общий HGCM-слот не блокируется.

Сервисы

Мышь

Драйвер использует расширенный протокол (пакет типа 223, 48 байт): хост передаёт позицию, состояние кнопок и оба скролла (вертикальный + горизонтальный). PS/2-мышь при этом программно отключается. Современные версии VirtualBox всегда поддерживают расширенный протокол, поэтому этот путь — основной и фактически единственный в работе.

На случай, если хост не объявил VMMDEV_MOUSE_HOST_SUPPORTS_FULL_STATE_PROTOCOL, в коде оставлен фолбэк на стандартный протокол (пакет типа 1, 36 байт) — хост передаёт только координаты (0-65535), а кнопки и скролл читаются с обычного PS/2 через IRQ12 (в неиспользуемом фолбэк-режиме, на практике все современные VirtualBox работают по расширенному протоколу). В современных VirtualBox этот путь не задействуется.

Координаты масштабируются делением на 2, диапазон хоста 0-65535 превращается в диапазон KolibriOS 0-32767. Деление, а не сдвиг, сохраняет центр координат. Данные передаются в ядро через SetMouseData(flags=0xC0000000|buttons, x, y, scroll_z, scroll_w).

Проблема двойного управления. Когда драйвер передаёт абсолютные координаты из VMMDev, ядро одновременно получает движения от PS/2-мыши через IRQ12. Возникает конфликт: хост двигает курсор в абсолютные координаты, а PS/2 тут же сдвигает его на дельту «в никуда». Курсор движется рывками.

Решение — при инициализации расширенного протокола программно отключать IRQ12 сбросом соответствующего бита в командном байте контроллера i8042:

; i8042_mouse_control(enable):;   Ждём готовности: опрос 0x64;   Запрос command byte: 0x20 -> 0x64;   Читаем 0x60 -> текущий command byte;   Сбрасываем бит 1 (IRQ12 enable);   Пишем обратно: 0x60 -> 0x64, затем command byte -> 0x60

Без этой последовательности рывки курсора неизбежны: PS/2-контроллер каждым своим прерыванием будет посылать в ядро дельту, которая немедленно собьёт абсолютную координату, только что установленную VMMDev. Без сброса IRQ12 расширенный протокол на практике непригоден.

Маска событий: VMMDEV_EVENT_MOUSE_POSITION_CHANGED | VMMDEV_EVENT_MOUSE_CAPABILITIES_CHANGED. Возможность: VMMDEV_GUEST_SUPPORTS_MOUSE.

Дисплей — авторесайз

Сервис display/ обрабатывает событие VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST, которое хост генерирует при изменении размера окна VirtualBox пользователем.

Event:  GetTimerTicks() -> vbox_display_event_time  vbox_display_pending = 1Tick (наш TimerHS-callback, каждые 100 мс):  ; «тики» ниже - единицы счётчика GetTimerTicks (шаг 10 мс),  ; не путать с периодом callback'а  if pending && (now - event_time >= 50 ед. = 500 мс):    pending = 0    display_change()

display_change() — обработка события, делает следующее:

  1.  Отправляет VMMDEV_REQ_GET_DISPLAY_CHANGE_REQUEST2 — хост возвращает целевое разрешение и глубину цвета.

  2. Валидирует параметры (мин/макс разрешение, bpp in {8, 16, 24, 32}).

  3. Программирует BGA (Bochs Graphics Adapter) через порты 0x01CE/0x01CF: отключает -> меняет XRES/YRES/BPP -> включает с флагом LFB.

  4. Обновляет структуру DISPLAY в ядре KolibriOS (width, height, bpp, pitch).

  5. Вызывает SetScreen(width-1, height-1)

Зачем задержка 500 мс. При быстром изменении размера окна хоста каждое применение SetScreen с высокой вероятностью вызывает краш taskbar’а из-за того, что ядро не обрезает окна за новыми границами экрана. Задержка в 500 мс — эмпирический костыль: она не устраняет проблему, но снижает вероятность того, что пользователь застанет краш при активном перетаскивании. Полноценное решение требует доработки ядра, а не драйвера.

Краш taskbar при уменьшении окна. При уменьшении окна VirtualBox с открытым taskbar’ом происходит сбой ядра. Причина:

  1. Ядро KolibriOS хранит геометрию каждого окна в глобальной таблице. SetScreen перерисовывает рабочий стол, но не обрезает уже открытые окна, выходящие за новые границы экрана.

  2. Taskbar отрисовывается по всей ширине экрана и кэширует эту ширину в своих внутренних переменных.

  3. Когда ширина экрана уменьшается, фреймбуфер сжимается, но taskbar продолжает рисовать по старому pitch. Запись пикселей выходит за границу выделенной памяти — out-of-bounds — краш.

Было:  ширина 1280 px, pitch = 5120 байтСтало: ширина 800  px, pitch = 3200 байт  (BGA и DISPLAY обновлены)Taskbar рисует 1280 px -> выход за 3200-байтовую строку -> краш
Краш taskbar при уменьшении окна VirtualBox.

Краш taskbar при уменьшении окна VirtualBox.

Это известное поведение ядра: SetScreen должна была бы уведомлять окна об изменении границ и принудительно обрезать те, что вышли за пределы нового разрешения. До соответствующего изменения в ядре уменьшение окна VirtualBox при открытом taskbar остаётся нестабильным. На стороне драйвера это исправить нельзя — драйвер не управляет внутренней геометрией окон.

Маска событий: VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST. Возможность: VMMDEV_GUEST_SUPPORTS_GRAPHICS.

Heartbeat

Простейший сервис: отправляет VMMDEV_REQ_GUEST_HEARTBEAT раз в секунду. Хост использует это для мониторинга «живости» гостя — если heartbeat’ы прекращаются дольше установленного порога, хост может сбросить гостя.

При инициализации драйвер запрашивает у хоста желаемый интервал (VMMDEV_REQ_HEARTBEAT_CONFIGURE) в наносекундах и конвертирует в единицы счётчика GetTimerTicks (шаг 10 мс — это отдельный системный счётчик KolibriOS, не путать с периодом нашего TimerHS-callback’а в 100 мс). Если хост не отвечает — используется дефолтный интервал в 100 единиц GetTimerTicks, то есть 1 секунда.

 Сервис не использует ни события, ни HGCM — только тик таймера и прямые VMMDev-запросы.

Синхронизация времени (timesync)

Сервис синхронизирует часы гостя с часами хоста, работая из пространства ядра, где int 0x40 (системный API для приложений) напрямую не используется.

 Решение — прямая запись в CMOS RTC через порты 0x70/0x71.

timesync_do_sync():  1. VMMDEV_REQ_GET_HOST_TIME -> 64-бит миллисекунды от Unix epoch  2. / 1000 -> unix_timestamp (секунды)  3. timesync_set_cmos():       - Алгоритм civil_from_days (Howard Hinnant) для декомпозиции         timestamp в год/месяц/день       - Ждём окончания цикла обновления CMOS (бит 7 регистра A)       - Устанавливаем бит SET в регистре B (запрещаем обновления)       - Пишем все CMOS-регистры (0x00-0x09, 0x32) в BCD       - Снимаем бит SET -> CMOS возобновляет ход

  BCD-конвертация (binary-coded decimal):

; value 0-99 -> BCDxor edx, edxmov ecx, 10div ecx        ; eax=десятки, edx=единицыshl eax, 4or  eax, edx

Алгоритм civil_from_days — функция из работы Howard Hinnant «chrono-Compatible Low-Level Date Algorithms» (howardhinnant.github.io/date_algorithms.html). Компактная формула перевода «дни от эпохи» в (год, месяц, день) без таблиц високосных лет. Оригинал — на C++, пришлось переписать на ассемблер вручную.

Драйвер пишет CMOS-регистры в BCD-режиме без проверки бита DM (Data Mode) в Status Register B — формально RTC поддерживает и binary-режим, выбираемый через этот бит. На практике гостевой VirtualBox-RTC всегда работает в BCD (это дефолт PC-совместимого RTC), и проблема не наблюдается; но строго по спецификации перед записью стоило бы прочитать DM и переключить путь конвертации. Это потенциальное место для будущей правки на случай нестандартного RTC.

Синхронизация выполняется раз в ~1 минуту. По умолчанию отключена (AUTOSTART_TIMESYNC = 0) — включается пользователем через IOCTL. Причины две. Во-первых, запись в CMOS — операция, которая на время цикла обновления останавливает RTC, и делать её безусловно при каждом старте VM нежелательно. Во-вторых, приложения, уже считавшие время в собственные переменные, изменений не увидят: ядерные функции int 0x40 fn=3 и fn=29 читают CMOS на каждый вызов (kernel/trunk/hid/set_dtc.inc), но приложение должно явно запросить время заново.

Guest Properties

Хранилище ключ-значение для метаданных хоста и гостя (сервис VBoxGuestPropSvc).

При инициализации устанавливаются стандартные свойства:

Используется HGCM с PAGELIST-параметрами для передачи строк имён и значений. Это даёт хосту возможность, например, при помощи VBoxManage guestproperty get <vm> /VirtualBox/GuestInfo/OS/Product узнавать, что внутри VM, без захода в неё.

$ VBoxManage guestproperty get KolibriOS /VirtualBox/GuestInfo/OS/ProductValue: KolibriOS

Общие папки (Shared Folders)

Самый сложный сервис. Монтирует папки, расшаренные хостом, как виртуальную файловую систему, доступную через стандартные системные вызовы KolibriOS.

Последовательность инициализации:

hgcm_connect("VBoxSharedFolders")   -> client_idSHFL_FN_SET_UTF8                    -> включить UTF-8 режимSHFL_FN_GET_LIST                    -> получить список папок хостаfor each folder:    vboxsf_add_folder(root_handle, name)vboxsf_register_disk()    DiskAdd(vboxsf_disk_functions, "vboxsf", ...)    DiskMediaChanged(disk, 1)         -> ядро создаёт раздел    partition.FSUserFunctions = vboxsf_user_functions   <- workaround!
  1. SHFL_FN_SET_UTF8 — запрос к хосту работать в UTF-8 (KolibriOS нативно использует CP866, конвертация идёт на стороне драйвера через утилиты из common/).

  2. SHFL_FN_GET_LIST — запрос списка папок, которые хост пометил как shared. Ответ приходит в PAGELIST-буфере.

  3. Для каждой папки вызывается vboxsf_add_folder, который резервирует ей слот в локальной таблице и запоминает root-handle.

  4. Регистрация «диска» в ядре. Здесь используется обходной манёвр.

Workaround с FSUserFunctions. Штатный путь регистрации файловой системы — FsAdd ядра — здесь не работает: при обходе списка дисков ядро обращается к неинициализированному NumPartitions (например, у CD-ROM без вставленного диска), результат — GPF в чужой памяти. Использовать FsAdd нельзя.

Обходной путь: DiskAdd + DiskMediaChanged(disk, 1) создаёт «raw»-раздел, дальше драйвер напрямую правит указатель FSUserFunctions в структуре раздела ядра — записывает свой набор функций поверх того, что ядро туда положило. Это не использование документированного API: это прямая модификация чужой структуры данных, минуя FsAdd, которая обычно её и заполняет. Опасно (ломает любые предположения ядра о владении этой структурой), но работает и не требует правки самого ядра. Альтернатива — патч FsAdd в ядре KolibriOS — потребовала бы upstream-PR и обновления у пользователей, поэтому здесь сознательный выбор в пользу автономности драйвера ценой одной точки прямого вмешательства.

Список доступных и прикрепленных директорий

Список доступных и прикрепленных директорий
Список файлов прикрепленной директории

Список файлов прикрепленной директории
Лог получения списка файлов и директорий

Лог получения списка файлов и директорий

Файловые операции (Read, ReadFolder, GetFileInfo) транслируются в HGCM-вызовы с PAGELIST-буферами: данные передаются DMA напрямую в страницы гостя без промежуточного копирования.

Отдельная работа была по структурам SHFL. Параметры файловых операций (открытие, чтение, атрибуты) — это вложенные структуры переменной длины, в которых порядок полей и точные размеры критичны до байта. Хост на любую ошибку — неверный размер PageList, перепутанные поля в SHFL_FSOBJINFO, не та выравнивающая дырка — отвечает одинаковым VERR_INVALID_PARAMETER без какой-либо детализации, в каком именно поле проблема. Ни строк, ни смещений, ни имени структуры — только код «параметр невалиден».

В таких условиях единственный способ продвигаться — сравнивать байт-в-байт собственный пакет с тем, что отправляет работающая реализация. Поэтому дизассемблер Windows-версии VBoxGuest стал основным инструментом отладки этого сервиса. Путь от «хост возвращает ошибку» до «файл читается корректно» занял значительное время именно потому, что без этой эталонной реализации ошибки ловились вслепую.

Маска событий: 0 (сервис работает исключительно через HGCM-синхронные вызовы). Возможность: VMMDEV_GUEST_SUPPORTS_SHARED_FOLDERS.

Буфер обмена (Clipboard)

Асинхронная state machine для двунаправленной синхронизации clipboard.

Состояния:

submit MSG_OLD_GET_WAIT            (async HGCM call)     +-----+  ---------->  +-----------+     |IDLE |               | SUBMITTED |     +-----+  <----------  +-----------+       ^      reset state       |       |      after handled     | tick (100 мс):       |                        | flag DONE в пакете?       |                        |       |              no --+    +-- yes       |                   |             |       |                   v             v       |              (ждём     обработать формат       |               ещё)     + submit нового       +----------- обработано <-+ GET_WAIT

MSG_OLD_GET_WAIT — асинхронный HGCM-вызов: хост не возвращает ответ до тех пор, пока в буфере обмена хоста что-то не изменится. Это удобный механизм подписки на изменения без поллинга.

Каждые 100 мс clipboard_tick проверяет флаг VBOX_HGCM_REQ_DONE в пакете. Хост поднимет его DMA-записью, когда в clipboard’е что-то появится. Если флаг поднят — состояние переходит в IDLE, обрабатывается пришедший формат, и отправляет новый GET_WAIT.

Fire-and-forget при отключении. Если отключение сервиса происходит, пока MSG_OLD_GET_WAIT ещё ждёт ответа, обычный hgcm_disconnect заблокировался бы до прихода ответа — а это десятки секунд или минуты, пока пользователь хоста что-нибудь не скопирует. Подвешенный поток драйвера в это время недопустим.

Решение: HGCM_DISCONNECT отправляется, но hgcm_wait_async() сознательно не вызывается. Хост продолжает обработку старого запроса и сам закроет соединение позже. Пакет при этом не освобождается даже после disable: хост ещё может DMA-записать в него, и попытка отдать страницу обратно ядру повредила бы чужую память. Тот же буфер переиспользуется при повторном включении сервиса.

В upstream hgcm_disconnect блокируется на wait-queue до прихода ответа. Здесь, не подключив WaitEventTimeout ядра KolibriOS к HGCM-пути, мы вместо этого оставляем пакет в собственности хоста до выгрузки драйвера. Решение работает, но это технический долг: с переходом на блокирующее ожидание необходимость в fire-and-forget отпадёт.

IOCTL-интерфейс (VBOX_IOCTL_CLIP_READ/WRITE/STATUS) позволяет приложениям читать и писать буфер обмена через драйвер. Протокол используется «старого» поколения: MSG_OLD_GET_WAIT для listener’а, DATA_WRITE с CPARMS_DATA_WRITE_OLD на запись. Чанкинг (разбиение на части, как в MSG_NEW_PEEK_WAIT + MSG_GET + последовательные DATA_READ) не реализован — данные передаются целиком одним HGCM-вызовом.

Чтобы это не ограничивало размер буфера, в clip_ioctl_read встроен retry с увеличением: первый запрос уходит с буфером max(запрошенный размер, CLIP_INITIAL_BUF_SIZE) (CLIP_INITIAL_BUF_SIZE = 65536, 64 КБ), округлённым до страницы. Если хост вернул ошибку и сообщил actual_size > kern_size (значит, данных было больше, чем поместилось), драйвер освобождает буфер, аллоцирует новый размером actual_size, и повторяет DATA_READ. Так обрабатываются произвольно большие буферы, лишь бы хватило непрерывной kernel-памяти.

Маска событий: VMMDEV_EVENT_HGCM. Возможность: VMMDEV_GUEST_SUPPORTS_SHCL.

Один цикл CLIP_READ: out_size=65540 (initial buffer), хост ответил 66 байтами UTF-16 формата 0x01 (CF_TEXT), Iconv сконвертировал в CP866 (32 байта), VBoxCtrl записал результат в буфер обмена гостя. Текст L:\projects\KolibriOS\Статья VBG — путь со скопированного на хосте файла.

Один цикл CLIP_READ: out_size=65540 (initial buffer), хост ответил 66 байтами UTF-16 формата 0x01 (CF_TEXT), Iconv сконвертировал в CP866 (32 байта), VBoxCtrl записал результат в буфер обмена гостя. Текст L:\projects\KolibriOS\Статья VBG — путь со скопированного на хосте файла.

Бесшовный режим (Seamless)

Режим, в котором окна гостя отображаются как обычные окна хостовой ОС без видимой рамки виртуальной машины.

Реализован частично: драйвер умеет включать/выключать режим по запросу хоста и отправлять «видимую область» (visible region). Однако вместо реального списка прямоугольников окон отправляется один прямоугольник весь экран:

; services/seamless/seamless.incvmmdev_visible_region_s VMMDEV_VIDEO_SET_VISIBLE_REGION    <sizeof.VMMDEV_VIDEO_SET_VISIBLE_REGION, ...>,    1,                    ; количество прямоугольников = 1    <0, 0, 0, 0>          ; rect0: заполняется размером всего экрана

Ограничение связано с ядром KolibriOS: оно не предоставляет API для получения позиций открытых окон из пространства драйвера. Ядро знает про окна (таблица окон, используемая WM), но её структура не экспортируется. Без доступа к этой таблице полноценный seamless не собрать.

Как и display, seamless использует debounce 500 мс для фильтрации быстрых переключений режима.

Маска событий: VMMDEV_EVENT_SEAMLESS_MODE_CHANGE. Возможность: VMMDEV_GUEST_SUPPORTS_SEAMLESS.

Бесшовный режим со стороны хоста: окна KolibriOS отрисованы поверх рабочего стола Windows как самостоятельные окна — рамка и заголовок VirtualBox не видны.

Бесшовный режим со стороны хоста: окна KolibriOS отрисованы поверх рабочего стола Windows как самостоятельные окна — рамка и заголовок VirtualBox не видны.

IOCTL-интерфейс 

Драйвер регистрируется в ОС как VBOXGUEST через RegService(name, handler). Приложения вызывают зарегистрированный обработчик через системную функцию SF 68.17 (CallService): передают имя сервиса, код IOCTL и буферы вход/выход. Реализация в коде — sys/ioctl.inc, точка входа service_proc.

Код

Имя

Действие

0

GET_VERSION

Версия API (сейчас 1)

1

GET_SERVICES

Список сервисов с именами и статусами

2

SVC_ENABLE

Включить сервис по ID

3

SVC_DISABLE

Выключить сервис по ID

10

CLIP_STATUS

Опросить форматы буфера обмена

11

CLIP_READ

Прочитать данные из clipboard

12

CLIP_WRITE

Записать данные в clipboard

Утилита VBoxCtrl: клиент IOCTL-интерфейса драйвера. Все статусы и кнопки — это вызовы кодов 1–3, 10–12 из таблицы выше.

Утилита VBoxCtrl: клиент IOCTL-интерфейса драйвера. Все статусы и кнопки — это вызовы кодов 1–3, 10–12 из таблицы выше.

После включения/выключения сервиса через коды 2 и 3 автоматически пересчитываются маски событий и возможностей, и обновляется состояние VMMDev.

Ключевые числа

Параметр

Значение

Vendor ID

0x80EE

Device ID

0xCAFE

MMIO размер

4 МБ

VMMDev пакеты

1 страница (4 КБ)

HGCM буферы

3 страницы (12 КБ)

Период TimerHS-callback

100мс

Debounce display/seamless

500мс

HGCM timeout

500 000 итераций spin (страховочный таймаут), ~5-50 мс в зависимости от частоты CPU

Heartbeat интервал

Timesync интервал

~60с

Макс. общих папок

10

Макс. HGCM-параметров

32

Работа по таймеру 

Прерывания VMMDev хорошо справляются с мгновенной реакцией: мышь двинулась -> событие пришло -> координата обновлена. Но ряд задач по природе своей не событийный:

  • Heartbeat должен стучать с заданным интервалом.

  • Синхронизация времени нужна раз в минуту.

  • Ресайз экрана нельзя применять мгновенно — нужно дождаться паузы в потоке событий.

  • Clipboard должен периодически проверять состояние своего async-запроса.

Для всего этого используется один системный таймер с тиком 100 мс.

Инициализация

proc timer_init    invoke TimerHS, TIMER_DELAY_START, TIMER_INTERVAL, timer_cb, 0    ;              ^10 (100 мс до старта)  ^10 (100 мс интервал)    mov [vbox_timer_handle], eaxendpproc timer_cb stdcall, userdata:dword    stdcall dispatcher_tick_all    retendp

TimerHS — системный вызов KolibriOS, регистрирующий высокоприоритетный таймер. Параметры в единицах 10 мс: значение 10 = 100 мс. Возвращает handle, который сохраняется для последующей отмены через CancelTimerHS при выгрузке драйвера.

Диспетчеризация тиков

dispatcher_tick_all проходит по таблице сервисов и вызывает fn_on_tick у каждого включённого сервиса, у которого он не равен нулю:

proc dispatcher_tick_all uses eax ecx esi    mov ecx, [services_count]    mov esi, services_table.loop:    cmp dword [esi + SERVICE_ENTRY.enabled], 0    je .next    mov eax, [esi + SERVICE_ENTRY.fn_on_tick]    test eax, eax    jz .next    push ecx esi    call eax            ; fn_on_tick()    pop esi ecx.next:    add esi, sizeof.SERVICE_ENTRY    dec ecx    jnz .loopendp

Выключенные сервисы тик не получают — это позволяет безопасно отключать сервис на лету без дополнительных флагов внутри самого сервиса.

Паттерны таймера

Debounce (display, seamless). Откладывать дорогую операцию до тех пор, пока события не «успокоятся». Ключевой момент — сброс флага pending до вызова обработчика: если во время долгой операции придёт новое событие из прерывания, оно поставит pending = 1 снова, и следующий тик его обработает.

Периодическое действие (heartbeat, timesync). Сервис запоминает время последней отправки и на каждом тике проверяет, не пора ли снова:

Event:  GetTimerTicks() -> vbox_display_event_time  vbox_display_pending = 1Tick (наш TimerHS-callback, каждые 100 мс):  ; «тики» ниже — единицы счётчика GetTimerTicks (шаг 10 мс),  ; не путать с периодом callback'а  if pending && (now — event_time >= 50 ед. = 500 мс):    pending = 0    display_change()

State machine (clipboard). Тик используется для продвижения асинхронного автомата: каждый тик — только быстрая проверка флага в памяти. Хост сам поднимет флаг через DMA и (теоретически) разбудит через прерывание.

Сводная таблица использования тика

Сервис

fn_on_tick

Паттерн

Фактическая частота

Display

display_tick

debounce

через 500 мс после последнего события

Seamless

seamless_tick

debounce

через 500 мс после последнего события

Heartbeat

heartbeat_tick

периодическое

раз в ~1с

Timesync

timesync_tick

периодическое

раз в ~60с

Clipboard

clipboard_tick

state machine

по ответу хоста

Проблемы, с которыми пришлось столкнуться

1. Несовместимость структур v1_03 / v1_04

Протокол VMMDev не имеет одного канонического открытого источника для ассемблера — все определения восстанавливались по исходникам VirtualBox и экспериментально. Между версиями заголовков v1_03 и v1_04 изменились смещения и размеры ряда структур: в первую очередь VMMDEV_HEADER (поле f_requestor появилось не сразу), а также структуры HGCM-параметров. Неверное смещение давало либо отказ хоста (VERR_INVALID_PARAMETER), либо молчаливую запись не в то поле — оба сценария крайне неочевидны при отладке без отладчика.

Решение: строгая документация смещений в комментариях к каждой структуре (; +0:, ; +4:, …) и сравнение с дизассемблером Windows-версии VBoxGuest.

2. Двойное управление мышью (PS/2 + VMMDev)

См. раздел «Мышь». Кратко: без программного отключения IRQ12 на i8042 PS/2-источник конкурирует с VMMDev-абсолютными координатами и курсор дёргается.

3. Синхронизация времени видна только при повторном чтении

Драйвер пишет напрямую в CMOS RTC через порты 0x70/0x71. Системные функции KolibriOS int 0x40 fn=3 (GetTime) и fn=29 (GetDate) читают CMOS на каждый вызов (kernel/trunk/hid/set_dtc.inc в репозитории KolibriOS) — поэтому приложения, повторно запрашивающие время, увидят обновление сразу. Однако приложения, считавшие время один раз и хранящие его в собственных переменных, обновления не увидят, пока не сделают новый запрос.

Кроме того, CMOS-запись требует ожидания цикла обновления RTC (бит 7 регистра A), иначе записанные значения будут искажены. Алгоритм перевода Unix timestamp в BCD-дату (Howard Hinnant civil_from_days) пришлось портировать на ассемблер вручную.

По совокупности AUTOSTART_TIMESYNC = 0 — сервис отключён по умолчанию.

4. Сложность работы со структурами Shared Folders

См. раздел «Общие папки». Кратко: вложенные структуры переменной длины, хост даёт только VERR_INVALID_PARAMETER без детализации. Отлаживалось сравнением с дизассемблированной Windows-версией и DEBUGF под флагом __DEBUG_SF__ = 1.

5. Clipboard: старый протокол listener’а

Подписка на изменения буфера обмена реализована через MSG_OLD_GET_WAIT, запись — через DATA_WRITE со «старой» формой параметров (CPARMS_DATA_WRITE_OLD). Это работает, но не использует возможности нового протокола (MSG_NEW_PEEK_WAIT + MSG_GET + последовательные DATA_READ), который, в частности, позволяет передавать данные по частям без удержания всей нагрузки в одном буфере.

В текущей реализации чанкинга нет: данные передаются целиком одним HGCM-вызовом. Чтобы это не ограничивало размер, на read-пути встроен retry с увеличением буфера — если хост сообщил, что данные не поместились, драйвер аллоцирует буфер нужного размера и повторяет запрос. Это покрывает бытовые сценарии, но не освобождает от необходимости держать весь кусок данных в непрерывной kernel-памяти единовременно.

Переход на MSG_NEW_* — запланированная работа.

6. Отладка без живой диагностики

В KolibriOS нет интерактивного отладчика ядра уровня kgdb или WinDbg, поэтому любое зависание драйвера приводило к полной остановке системы и требовало перезагрузки виртуальной машины. Основным окном в работу драйвера оставалась отладочная доска KolibriOS и пишущие в неё DEBUGF сообщения. Цикл: исправил -> собрал -> загрузил -> зависло -> сбросил, был стандартным рабочим процессом. Для самых горячих путей, обработчика прерываний, отладочные сообщения шли через COM-порт с флагами фильтрации, чтобы не утопить вывод в шуме повторяющихся событий.

Вывод отладочных данных в SharedFolder

Вывод отладочных данных в SharedFolder

Отладка SHFL_FN_READ — самый запомнившийся случай. Хост стабильно отвечал VERR_INVALID_PARAMETER, никакой детализации. Перебрал все смещения в SHFL_FSOBJINFO, проверил размеры PageList, переписал упаковку параметров, ничего. Помог только дамп пакета, который отправляет Windows-версия в той же ситуации: расхождение оказалось в одном байте выравнивающего поля, которое в наших заголовках вообще отсутствовало. После добавления заработало.

Сравнение с upstream-реализациями драйвера VBoxGuest

VirtualBox — кроссплатформенный продукт, и исходный код upstream-проекта содержит реализации VBoxGuest под Linux, Windows, FreeBSD, Solaris, OS/2 и другие системы. Описанный здесь драйвер для KolibriOS написан с нуля — портирование upstream-кода не выполнялось. В этом разделе сравниваются именно драйверные реализации: где живёт каждая функциональность, как устроено асинхронное ожидание, через что идёт интеграция с подсистемами ОС.

Где живёт функциональность

В upstream VBoxGuest разделён на несколько компонентов:

  1. Kernel-драйвер VBoxGuest — PCI probing, ISR, общая память, IOCTL-мост между приложениями и хостом.

  2. VBoxService — пользовательский процесс: timesync, guest properties, memory balloon, VMInfo, guest control, автомаунт shared folders.

  3. VBoxClient (X11/Wayland) — display auto-resize, seamless, drag&drop, clipboard.

Плюс отдельные kernel-модули: vboxsf (shared folders), vboxvideo/DRM (видео и resize). На Windows — отдельные VBoxMouse.sys (фильтр-драйвер мыши), VBoxDisplay (WDDM-драйвер), VBoxSF.sys (mini-redirector).

В KolibriOS драйвер выполнен иначе — единый PE DLL vboxguest.sys, всё в одном бинарнике, с внутренним диспетчером сервисов вместо нескольких компонентов. Соответственно по фичам:

Фича

VBoxGuest для KolibriOS

upstream Linux

upstream Windows

upstream FreeBSD

PCI probe + VMMDev

драйвер

vboxguest

VBoxGuest.sys

vboxguest.ko

IRQ / event dispatch

драйвер

vboxguest

VBoxGuest.sys

vboxguest.ko

Mouse integration

в драйвере (программирование i8042)

vboxguest + X-драйвер

отд. VBoxMouse.sys (filter)

vboxguest + X-драйвер

Display auto-resize

в драйвере (программирование BGA)

vboxvideo (DRM) + VBoxClient

отд. VBoxDisplay (WDDM)

vboxvideo + VBoxVideo Xorg

Clipboard

в драйвере + IOCTL

VBoxClient (X11)

user-space service

VBoxClient (X11)

Seamless

частично, в драйвере

VBoxClient

user-space

VBoxClient

Shared folders

в драйвере (DiskAdd + FSUserFunctions)

отд. vboxsf.ko

отд. VBoxSF.sys

отд. vboxvfs.ko

Time sync

в драйвере (запись в CMOS)

VBoxService

VBoxService

VBoxService

Guest Properties

драйвер (минимально)

VBoxService

VBoxService

VBoxService

Heartbeat

драйвер

common-ядро vboxguest

VBoxGuest.sys

vboxguest.ko

Memory balloon

нет

VBoxService + vboxguest

VBoxService + VBoxGuest

VBoxService

Существенное отличие — фичи, которые в upstream вынесены в долгоживущие пользовательские процессы (VBoxService/VBoxClient), здесь живут внутри kernel-драйвера. Часть из них для этого использует прямой доступ к железу: BGA через 0x01CE/0x01CF для display-resize, CMOS через 0x70/0x71 для timesync, контроллер i8042 для отключения IRQ12 при работе с абсолютной мышью. В upstream эти задачи решают штатные DRM-драйвер видео, отдельные фильтры мыши и пользовательский timesync через ядерный API.

HGCM async wait

В upstream HGCM-ожидание построено на блокирующих примитивах ОС: wait-queue (init_waitqueue_head / wake_up_interruptible) на Linux, KEVENT + KeWaitForSingleObject на Windows, RTSemEventMulti из IPRT на FreeBSD/Solaris. Поток вызывающего блокируется, IRQ-обработчик его будит при поступлении ответа.

В описываемом драйвере для KolibriOS используется spin-loop с pause, ограниченный таймаутом в 500 000 итераций. Длинные ожидания (clipboard listener) разнесены в отдельный буфер с проверкой по тику таймера. Подходящие примитивы в ядре KolibriOS есть (события ядра), но к HGCM-пути они в текущей версии драйвера не подключены. Это технический долг; переход на блокирующее ожидание планируется.

Shared folders

В upstream это отдельный kernel-модуль файловой системы: на Linux vboxsf.ko регистрируется через register_filesystem(&g_vboxsf_fs_type) и пользуется VbglR0Sf*-API из common-уровня; на Windows — mini-redirector VBoxSF.sys; на FreeBSD — vboxvfs.

В описываемом драйвере для KolibriOS VFS-провайдер встроен в тот же бинарник, что и транспорт. Регистрация в ядре идёт обходным путём: DiskAdd + DiskMediaChanged создают raw-раздел, после чего драйвер записывает свой набор функций прямо в partition.FSUserFunctions ядерной структуры. Подробнее — в разделе про сервис.

Trade-offs монолитной модели

Что даёт упаковка всего в один драйвер:

  • Один артефакт сборки и поставки. Один файл, одно место, никаких зависимостей между компонентами.

  • Один транспорт, один контекст. Сервисы переиспользуют общие HGCM-буферы, общий диспетчер, общий цикл инициализации.

  • Нет межпроцессной сериализации. В upstream VBoxClient/VBoxService обмениваются с kernel-драйвером через ioctl и сериализуют запросы; здесь все сервисы работают в одном адресном пространстве — параметры передаются по указателю.

Что теряется по сравнению с upstream-моделью:

  • Нет независимой перезагрузки отдельной фичи. Выгрузка драйвера = одновременный отказ всех сервисов. В upstream можно перезапустить VBoxClient без отключения мыши.

  • Нет user-space sandbox для парсинга чужих структур. Парсеры SHFL-структур, форматов clipboard и guest properties работают в kernel mode. Баг в них — не «упал демон», а дестабилизация ядра.

  • Нет независимого upgrade-цикла. Чтобы обновить логику clipboard, надо перевыпускать весь драйвер.

  • Spin-loop / таймерный тик вместо блокирующего ожидания. Технический долг; снимается переходом на WaitEventTimeout ядра.

В upstream-моделях те же trade-offs распределены иначе: kernel-часть тонкая, рискованная логика — в пользовательских процессах, обновление компонентов независимо. Это даёт другие свойства ценой большей сложности сборки и поставки.

Итог

VBoxGuest для KolibriOS — законченный стек гостевых дополнений, написанный целиком на ассемблере и упакованный в один PE DLL. Если оценивать честно, без сглаживаний:

KolibriOS в VirtualBox с включённым VBoxGuest: отладочная доска, файловый менеджер на shared folder, VBoxCtrl со статусами сервисов, синхронизированный буфер обмена

KolibriOS в VirtualBox с включённым VBoxGuest: отладочная доска, файловый менеджер на shared folder, VBoxCtrl со статусами сервисов, синхронизированный буфер обмена

Что сделано полностью. Мышь — расширенный протокол с абсолютными координатами, оба скролла, программное отключение IRQ12 на i8042 для устранения гонки с PS/2. Shared folders — полноценная виртуальная ФС: монтирование папок хоста, операции с файлами через HGCM, передача данных через DMA на основе PageList. Heartbeat — настраиваемый интервал, watchdog со стороны хоста работает штатно. Guest Properties — стандартные ключи установлены, доступ через VBoxManage работает. По функциональности эти четыре фичи сопоставимы с upstream-реализацией.

Что работает с оговорками.

  • Display auto-resize — программирование BGA работает штатно, разрешение применяется. Но при уменьшении окна с открытым taskbar возможен краш — SetScreen обновляет геометрию экрана, но не уведомляет открытые окна об усечении границ, и taskbar пишет за пределы фреймбуфера по старому pitch. Эта часть лежит на стороне ядра, в драйвере её исправить нельзя.

  • Clipboard — двунаправленная синхронизация работает, listener подписывается через MSG_OLD_GET_WAIT, fire-and-forget при отключении не блокирует поток. Но используется протокол «старого» поколения без чанкинга — данные передаются целиком одним HGCM-вызовом. Для бытовых сценариев хватает (есть retry-расширение буфера на чтении), для очень крупных передач буфер живёт в непрерывной kernel-памяти, что ограничивает объём.

  • Timesync — пишет в CMOS RTC напрямую через порты 0x70/0x71. Уже работающие приложения, кэширующие время в собственных переменных, изменения не увидят: int 0x40 возвращает свежее время только при явном запросе. Поэтому сервис отключён по умолчанию.

Что принципиально требует доработок в ядре KolibriOS. Эти пункты в vboxguest.sys починить нельзя — нужны изменения в самом ядре:

  • Полноценный seamless. Драйвер умеет отправлять visible region, но в ядре KolibriOS нет публичного API для получения координат открытых окон из kernel-контекста. Без этого приходится отправлять один прямоугольник на весь экран — что технически отвечает на запрос хоста, но смысла бесшовного режима не даёт.

  • Краш при уменьшении окна VirtualBox с taskbar. Описан выше. Драйвер делает свою часть — переписывает структуру DISPLAY и зовёт SetScreen. Чтобы снять краш, SetScreen должна была бы обрезать окна, выходящие за новые границы.

Что временно. Эти ограничения снимаются в самом драйвере, без правок ядра:

  • Переход на MSG_NEW_PEEK_WAIT + MSG_GET + DATA_READ в clipboard — даст чанкинг и снимет требование непрерывной kernel-памяти на весь объём передачи.

  • Перевод hgcm_wait_async со spin-loop’а на блокирующее ожидание через события ядра KolibriOS. Тот же приём — для clipboard listener’а: вместо тика таймера событие, поднимаемое из IRQ-обработчика.

Часть инженерных решений в этом драйвере выглядит непривычно с точки зрения upstream-моделей VBoxGuest — монолитная упаковка вместо разделения на kernel + user-space, прямой доступ к BGA / CMOS / i8042 вместо отдельных DRM/RTC/filter-драйверов, spin-loop вместо блокирующего ожидания. Часть из этого — осознанный технический долг, снимаемый в самом драйвере (см. список выше). Часть — следствие выбранной архитектуры одного PE DLL: у него другие свойства, чем у трёхкомпонентной upstream-реализации, со своими плюсами и минусами, разобранными в разделе сравнения.

Версия драйвера: 1.0

Лицензия: GPLv2 (как и сама KolibriOS)

Исходники драйвера

ссылка на оригинал статьи https://habr.com/ru/articles/1040248/