AVZ (Agency VirtualiZer) is the type-1 hypervisor that ships inside the
SO3 tree. Built with CONFIG_AVZ, the very same code base runs at EL2 on
ARM64 and hosts guest domains at EL1. AVZ is small and Xen-inspired: it
provides stage-2 memory isolation, a domain scheduler, hypercalls, event
channels, grant tables and a virtual GIC, and nothing more.
AVZ is also the foundation of the :ref:`SO3 capsule <capsules>` model: an agency domain owns the hardware while one or more lightweight capsule (S3C) guests run beside it.
Note
The agency is a Linux kernel: it owns the hardware and hosts the
capsules. The agency, together with the SOO framework, lives in a
:ref:`separate repository <capsules>`. The hypervisor can equally boot a
plain SO3 kernel in the guest slot (built with CONFIG_SOO=n, so no
capsules) — that is the demonstration shipped with this repository, enough
to exercise the hypervisor on its own.
The code lives under avz/ (kernel, memory, scheduler, hypercalls, grant
tables, capsule build/inject) together with the EL2-specific parts of
arch/arm64 (head.S MMU setup, exception.S EL2 vectors,
context.S stage-2 switch, cache.S EL2 TLB ops) and the virtual GIC in
devices/irq/.
The hypervisor entry point is avz_start() (avz/kernel/setup.c). After
early CPU, memory and device initialisation it prints its banner and loads the
guest domain. U-Boot's guest-boot command hands AVZ two FIT images — the
AVZ ITB in x0 and a separate SO3 guest ITB in x1 (see
:ref:`build_system`) — and loadAgency() parses the guest from the x1
ITB. AVZ places the guest's kernel and device tree in RAM,
injects the guest initrd (the ITB ramdisk node) into the guest's
/chosen, builds the guest's stage-2 page tables and sets the guest entry
point. AVZ then erets to EL1, and the guest boots as an ordinary SO3
kernel (kernel_start()). The console trace looks like:
********** Smart Object Oriented technology - AVZ Hypervisor ********** ... Now bootstraping the hypervisor kernel ... ***************** Loading Guest Domain ***************** ... ********** Smart Object Oriented SO3 Operating System **********
Guest memory is organised in memory slots (avz/include/avz/memslot.h):
slot 0 is AVZ itself, slot 1 the agency, and the remaining slots are capsules.
Each slot maps a guest intermediate physical address (IPA) range to real
physical memory; ipa_to_pa() / pa_to_ipa() convert between them.
A domain (struct domain, avz/include/avz/domain.h) is a guest
instance: its virtual CPU state, its event-channel table, a pointer to the
shared info page and its scheduling metadata. Well-known identifiers
(avz/include/avz/uapi/avz.h):
.. flat-table::
:header-rows: 1
:widths: 35 65
* - Identifier
- Meaning
* - ``DOMID_AGENCY`` (0)
- the agency guest — Linux, or a plain SO3 (``CONFIG_SOO=n``); owns the
devices
* - domain id 1
- reserved (the agency occupies memory slot 1 but keeps domain id 0)
* - domain ids 2 …
- capsule domains
* - ``MAX_S3C_DOMAINS`` (5)
- up to five capsules alongside the agency (``MAX_DOMAINS = 2 + 5``)
Each domain shares a page with the hypervisor — the avz_shared structure —
carrying its domain id, event-channel pending bits, the upcall state and the
guest's device-tree address.
Guests call into AVZ with the hvc instruction, which traps to the EL2
synchronous handler (el12_sync_handler in arch/arm64/exception.S) and is
dispatched by avz/kernel/hypercalls.c. Every hypercall is one cmd value
in that single dispatcher. Three are always compiled in
(avz/include/avz/uapi/avz.h):
AVZ_EVENT_CHANNEL_OP— allocate / bind / send / close event channels;AVZ_CONSOLE_IO_OP— console output for guests;AVZ_DOMAIN_CONTROL_OP— domain control (pause / unpause a capsule, …).
The rest are the SOO commands, declared in soo/include/soo/uapi/soo.h and
compiled in only with CONFIG_SOO: the grant-table op
(AVZ_GRANT_TABLE_OP), domain description (AVZ_GET_DOM_DESC), capsule
lifecycle (AVZ_INJECT_CAPSULE, AVZ_START_CAPSULE, AVZ_KILL_S3C,
AVZ_GET_S3C_STATE / AVZ_SET_S3C_STATE), the snapshot primitives
(AVZ_S3C_READ_SNAPSHOT / AVZ_S3C_WRITE_SNAPSHOT), the direct-communication
events (AVZ_DC_EVENT_SET) and the virtual-framebuffer ops
(AVZ_FBDEV_*). They are not layered on top of the three generic ones — see
:ref:`capsules`.
AVZ runs each domain on a CPU according to its role. The agency uses the
sched_agency policy; capsules are scheduled by sched_flip
(avz/kernel/sched_flip.c), a lightweight round-robin over the capsule
domains. A per-CPU current_domain pointer tracks the running guest; switching
domains saves and restores the EL1 register banks and reprograms VTTBR_EL2
through __mmu_switch_vttbr() (arch/arm64/context.S).
Each domain has NR_EVTCHN (128) event-channel ports. A port can be
unbound (waiting for a peer), interdomain (bound to a remote domain's port)
or bound to a virtual IRQ. Sending an event sets a pending bit in the remote
domain's avz_shared page and, if needed, injects a virtual interrupt so the
guest is woken. Event channels are the signalling half of the split-driver
model.
Grant tables (avz/kernel/gnttab.c) let one domain share specific memory
pages with another in a controlled way. A domain reserves a small set of grant
IPA pages; a peer maps a granted page by reference. This is how the shared rings
of the frontend/backend drivers and the capsule framebuffer are set up.
Because guests must not touch the physical interrupt controller directly, AVZ provides a virtual GIC.
HCR_EL2.IMOroutes all Group-1 physical IRQs to EL2. The EL2 IRQ handler (avz_el2_irq_handle()for GICv3,irq_handle()for GICv2) decides what to do with each interrupt.- The hypervisor's own interrupts — the EL2 timer (CNTHP, PPI 26) and the vGIC maintenance interrupt (PPI 25) — are handled locally.
- All other interrupts destined for a guest are injected through the GIC
list registers (
ICH_LR*_EL2on GICv3, the GICH MMIO frame on GICv2). The injected entry is hardware-backed (HW = 1) so that the physical interrupt is deactivated automatically when the guest writes its own end-of-interrupt — keeping hypervisor overhead minimal. - Accesses by a guest to the physical GIC distributor are not mapped in the
guest stage-2 tables; they trap to EL2 and are emulated by the vGIC
(
devices/irq/vgic.c), which forwards most register accesses and translates SGI (software-generated interrupt) requests into AVZ's targeted-IPI helper.
Because the standalone and AVZ builds share the same files, a handful of
low-level operations differ by exception level and are guarded with
#ifdef CONFIG_AVZ:
.. flat-table::
:header-rows: 1
:widths: 30 35 35
* - Operation
- Standalone (EL1)
- AVZ (EL2)
* - TLB maintenance (``cache.S``)
- ``tlbi vmalle1`` / ``vae1is``
- ``tlbi alle2`` / ``vae2is``
* - MMU setup (``head.S``)
- ``ttbr0/1_el1``, ``sctlr_el1``
- ``ttbr0_el2``, ``tcr_el2``, ``sctlr_el2``
* - GIC CPU interface
- ``EOImode = 0``
- ``EOImode = 1`` (split priority-drop / deactivate)
* - sync/IRQ vectors (``exception.S``)
- ``el01_sync`` / ``el01_1_irq``
- ``el12_sync`` / ``el12_2_irq``
Getting these guards right is essential: an EL2-only instruction executed at EL1 (or vice versa) faults immediately at boot. See :ref:`debugging` for how such issues are diagnosed under QEMU/GDB.
