Skip to content

Commit 5867c8d

Browse files
committed
Step 6-1: virtualize serial port I/O
1 parent ce5c674 commit 5867c8d

10 files changed

Lines changed: 302 additions & 8 deletions

File tree

hypervisor/src/arch/x86_64/uart16550.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ pub fn console_putchar(c: u8) {
8787
COM1.lock().putchar(c);
8888
}
8989

90-
#[allow(dead_code)]
9190
pub fn console_getchar() -> Option<u8> {
9291
COM1.lock().getchar()
9392
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//! Emulated Intel 8259 Programmable Interrupt Controller. (ref: https://wiki.osdev.org/8259_PIC)
2+
3+
use super::PortIoDevice;
4+
use rvm::{RvmError, RvmResult};
5+
6+
pub struct I8259Pic {
7+
port_base: u16,
8+
}
9+
10+
impl PortIoDevice for I8259Pic {
11+
fn port_range(&self) -> core::ops::Range<u16> {
12+
self.port_base..self.port_base + 2
13+
}
14+
15+
fn read(&self, _port: u16, _access_size: u8) -> RvmResult<u32> {
16+
Err(RvmError::Unsupported) // report error for read
17+
}
18+
19+
fn write(&self, _port: u16, _access_size: u8, _value: u32) -> RvmResult {
20+
Ok(()) // ignore write
21+
}
22+
}
23+
24+
impl I8259Pic {
25+
pub const fn new(port_base: u16) -> Self {
26+
Self { port_base }
27+
}
28+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
mod i8259_pic;
2+
mod uart16550;
3+
4+
use alloc::{sync::Arc, vec, vec::Vec};
5+
6+
pub trait PortIoDevice: Send + Sync {
7+
fn port_range(&self) -> core::ops::Range<u16>;
8+
fn read(&self, port: u16, access_size: u8) -> rvm::RvmResult<u32>;
9+
fn write(&self, port: u16, access_size: u8, value: u32) -> rvm::RvmResult;
10+
}
11+
12+
pub struct VirtDeviceList {
13+
port_io_devices: Vec<Arc<dyn PortIoDevice>>,
14+
}
15+
16+
impl VirtDeviceList {
17+
pub fn find_port_io_device(&self, port: u16) -> Option<&Arc<dyn PortIoDevice>> {
18+
self.port_io_devices
19+
.iter()
20+
.find(|dev| dev.port_range().contains(&port))
21+
}
22+
}
23+
24+
lazy_static::lazy_static! {
25+
static ref VIRT_DEVICES : VirtDeviceList = VirtDeviceList {
26+
port_io_devices: vec![
27+
Arc::new(uart16550::Uart16550::new(0x3f8)), // COM1
28+
Arc::new(i8259_pic::I8259Pic::new(0x20)), // PIC1
29+
Arc::new(i8259_pic::I8259Pic::new(0xA0)), // PIC2
30+
],
31+
};
32+
}
33+
34+
pub fn all_virt_devices() -> &'static VirtDeviceList {
35+
&VIRT_DEVICES
36+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
//! Emulated UART 16550. (ref: https://wiki.osdev.org/Serial_Ports)
2+
3+
use super::PortIoDevice;
4+
use crate::arch::uart;
5+
6+
use rvm::{RvmError, RvmResult};
7+
use spin::Mutex;
8+
9+
const DATA_REG: u16 = 0;
10+
const INT_EN_REG: u16 = 1;
11+
const FIFO_CTRL_REG: u16 = 2;
12+
const LINE_CTRL_REG: u16 = 3;
13+
const MODEM_CTRL_REG: u16 = 4;
14+
const LINE_STATUS_REG: u16 = 5;
15+
const MODEM_STATUS_REG: u16 = 6;
16+
const SCRATCH_REG: u16 = 7;
17+
18+
const UART_FIFO_CAPACITY: usize = 16;
19+
20+
bitflags::bitflags! {
21+
/// Line status flags
22+
struct LineStsFlags: u8 {
23+
const INPUT_FULL = 1;
24+
// 1 to 4 unknown
25+
const OUTPUT_EMPTY = 1 << 5;
26+
// 6 and 7 unknown
27+
}
28+
}
29+
30+
/// FIFO queue for caching bytes read.
31+
struct Fifo<const CAP: usize> {
32+
buf: [u8; CAP],
33+
head: usize,
34+
num: usize,
35+
}
36+
37+
impl<const CAP: usize> Fifo<CAP> {
38+
const fn new() -> Self {
39+
Self {
40+
buf: [0; CAP],
41+
head: 0,
42+
num: 0,
43+
}
44+
}
45+
46+
fn is_empty(&self) -> bool {
47+
self.num == 0
48+
}
49+
50+
fn is_full(&self) -> bool {
51+
self.num == CAP
52+
}
53+
54+
fn push(&mut self, value: u8) {
55+
assert!(self.num < CAP);
56+
self.buf[(self.head + self.num) % CAP] = value;
57+
self.num += 1;
58+
}
59+
60+
fn pop(&mut self) -> u8 {
61+
assert!(self.num > 0);
62+
let ret = self.buf[self.head];
63+
self.head += 1;
64+
self.head %= CAP;
65+
self.num -= 1;
66+
ret
67+
}
68+
}
69+
70+
pub struct Uart16550 {
71+
port_base: u16,
72+
fifo: Mutex<Fifo<UART_FIFO_CAPACITY>>,
73+
}
74+
75+
impl PortIoDevice for Uart16550 {
76+
fn port_range(&self) -> core::ops::Range<u16> {
77+
self.port_base..self.port_base + 8
78+
}
79+
80+
fn read(&self, port: u16, access_size: u8) -> RvmResult<u32> {
81+
if access_size != 1 {
82+
error!("Invalid serial port I/O read size: {} != 1", access_size);
83+
return Err(RvmError::InvalidParam);
84+
}
85+
let ret = match port - self.port_base {
86+
DATA_REG => {
87+
// read a byte from FIFO
88+
let mut fifo = self.fifo.lock();
89+
if fifo.is_empty() {
90+
0
91+
} else {
92+
fifo.pop()
93+
}
94+
}
95+
LINE_STATUS_REG => {
96+
// check if the physical serial port has an available byte, and push it to FIFO.
97+
let mut fifo = self.fifo.lock();
98+
if !fifo.is_full() {
99+
if let Some(c) = uart::console_getchar() {
100+
fifo.push(c);
101+
}
102+
}
103+
let mut lsr = LineStsFlags::OUTPUT_EMPTY;
104+
if !fifo.is_empty() {
105+
lsr |= LineStsFlags::INPUT_FULL;
106+
}
107+
lsr.bits()
108+
}
109+
INT_EN_REG | FIFO_CTRL_REG | LINE_CTRL_REG | MODEM_CTRL_REG | MODEM_STATUS_REG
110+
| SCRATCH_REG => {
111+
info!("Unimplemented serial port I/O read: {:#x}", port); // unimplemented
112+
0
113+
}
114+
_ => unreachable!(),
115+
};
116+
Ok(ret as u32)
117+
}
118+
119+
fn write(&self, port: u16, access_size: u8, value: u32) -> RvmResult {
120+
if access_size != 1 {
121+
error!("Invalid serial port I/O write size: {} != 1", access_size);
122+
return Err(RvmError::InvalidParam);
123+
}
124+
match port - self.port_base {
125+
DATA_REG => uart::console_putchar(value as u8),
126+
INT_EN_REG | FIFO_CTRL_REG | LINE_CTRL_REG | MODEM_CTRL_REG | SCRATCH_REG => {
127+
info!("Unimplemented serial port I/O write: {:#x}", port); // unimplemented
128+
}
129+
LINE_STATUS_REG => {} // ignore
130+
_ => unreachable!(),
131+
}
132+
Ok(())
133+
}
134+
}
135+
136+
impl Uart16550 {
137+
pub const fn new(port_base: u16) -> Self {
138+
Self {
139+
port_base,
140+
fifo: Mutex::new(Fifo::new()),
141+
}
142+
}
143+
}

hypervisor/src/hv/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod device_emu;
12
mod gconfig;
23
mod gpm;
34
mod hal;

hypervisor/src/hv/vmexit.rs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
use super::device_emu;
12
use super::hal::RvmHalImpl;
2-
use rvm::arch::VmxExitReason;
3-
use rvm::{RvmResult, RvmVcpu};
3+
use rvm::arch::{VmxExitInfo, VmxExitReason};
4+
use rvm::{RvmError, RvmResult, RvmVcpu};
45

56
type Vcpu = RvmVcpu<RvmHalImpl>;
67

@@ -65,6 +66,58 @@ fn handle_hypercall(vcpu: &mut Vcpu) -> RvmResult {
6566
Ok(())
6667
}
6768

69+
fn handle_io_instruction(vcpu: &mut Vcpu, exit_info: &VmxExitInfo) -> RvmResult {
70+
let io_info = vcpu.io_exit_info()?;
71+
trace!(
72+
"VM exit: I/O instruction @ {:#x}: {:#x?}",
73+
exit_info.guest_rip,
74+
io_info,
75+
);
76+
if io_info.is_string {
77+
error!("INS/OUTS instructions are not supported!");
78+
return Err(RvmError::Unsupported);
79+
}
80+
if io_info.is_repeat {
81+
error!("REP prefixed I/O instructions are not supported!");
82+
return Err(RvmError::Unsupported);
83+
}
84+
85+
if let Some(dev) = device_emu::all_virt_devices().find_port_io_device(io_info.port) {
86+
if io_info.is_in {
87+
let value = dev.read(io_info.port, io_info.access_size)?;
88+
let rax = &mut vcpu.regs_mut().rax;
89+
// SDM Vol. 1, Section 3.4.1.1:
90+
// * 32-bit operands generate a 32-bit result, zero-extended to a 64-bit result in the
91+
// destination general-purpose register.
92+
// * 8-bit and 16-bit operands generate an 8-bit or 16-bit result. The upper 56 bits or
93+
// 48 bits (respectively) of the destination general-purpose register are not modified
94+
// by the operation.
95+
match io_info.access_size {
96+
1 => *rax = (*rax & !0xff) | (value & 0xff) as u64,
97+
2 => *rax = (*rax & !0xffff) | (value & 0xffff) as u64,
98+
4 => *rax = value as u64,
99+
_ => unreachable!(),
100+
}
101+
} else {
102+
let rax = vcpu.regs().rax;
103+
let value = match io_info.access_size {
104+
1 => rax & 0xff,
105+
2 => rax & 0xffff,
106+
4 => rax,
107+
_ => unreachable!(),
108+
} as u32;
109+
dev.write(io_info.port, io_info.access_size, value)?;
110+
}
111+
} else {
112+
panic!(
113+
"Unsupported I/O port {:#x} access: {:#x?}",
114+
io_info.port, io_info
115+
)
116+
}
117+
vcpu.advance_rip(exit_info.exit_instruction_length as _)?;
118+
Ok(())
119+
}
120+
68121
fn handle_ept_violation(vcpu: &Vcpu, guest_rip: usize) -> RvmResult {
69122
let fault_info = vcpu.nested_page_fault_info()?;
70123
panic!(
@@ -84,6 +137,7 @@ pub fn vmexit_handler(vcpu: &mut Vcpu) -> RvmResult {
84137
let res = match exit_info.exit_reason {
85138
VmxExitReason::CPUID => handle_cpuid(vcpu),
86139
VmxExitReason::VMCALL => handle_hypercall(vcpu),
140+
VmxExitReason::IO_INSTRUCTION => handle_io_instruction(vcpu, &exit_info),
87141
VmxExitReason::EPT_VIOLATION => handle_ept_violation(vcpu, exit_info.guest_rip),
88142
_ => panic!(
89143
"Unhandled VM-Exit reason {:?}:\n{:#x?}",

rvm/src/arch/x86_64/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ cfg_if::cfg_if! {
77
if #[cfg(feature = "vmx")] {
88
mod vmx;
99
use vmx as vender;
10-
pub use vmx::{VmxExitInfo, VmxExitReason};
10+
pub use vmx::{VmxExitInfo, VmxExitReason, VmxIoExitInfo};
1111
}
1212
}
1313

rvm/src/arch/x86_64/vmx/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::hal::RvmHal;
1717
pub use self::definitions::VmxExitReason;
1818
pub use self::ept::ExtendedPageTable as NestedPageTable;
1919
pub use self::vcpu::VmxVcpu as RvmVcpu;
20-
pub use self::vmcs::VmxExitInfo;
20+
pub use self::vmcs::{VmxExitInfo, VmxIoExitInfo};
2121
pub use self::VmxPerCpuState as ArchPerCpuState;
2222

2323
pub fn has_hardware_support() -> bool {

rvm/src/arch/x86_64/vmx/vcpu.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ impl<H: RvmHal> VmxVcpu<H> {
5555
vmcs::exit_info()
5656
}
5757

58+
/// Information for VM exits due to I/O instructions.
59+
pub fn io_exit_info(&self) -> RvmResult<vmcs::VmxIoExitInfo> {
60+
vmcs::io_exit_info()
61+
}
62+
5863
/// Information for VM exits due to nested page table faults (EPT violation).
5964
pub fn nested_page_fault_info(&self) -> RvmResult<NestedPageFaultInfo> {
6065
vmcs::ept_violation_info()
@@ -211,14 +216,15 @@ impl<H: RvmHal> VmxVcpu<H> {
211216
0,
212217
)?;
213218

214-
// Use MSR bitmaps, activate secondary controls,
219+
// Intercept all I/O instructions, use MSR bitmaps, activate secondary controls,
215220
// disable CR3 load/store interception.
216221
use PrimaryControls as CpuCtrl;
217222
vmcs::set_control(
218223
VmcsControl32::PRIMARY_PROCBASED_EXEC_CONTROLS,
219224
Msr::IA32_VMX_TRUE_PROCBASED_CTLS,
220225
Msr::IA32_VMX_PROCBASED_CTLS.read() as u32,
221-
(CpuCtrl::USE_MSR_BITMAPS | CpuCtrl::SECONDARY_CONTROLS).bits(),
226+
(CpuCtrl::UNCOND_IO_EXITING | CpuCtrl::USE_MSR_BITMAPS | CpuCtrl::SECONDARY_CONTROLS)
227+
.bits(),
222228
(CpuCtrl::CR3_LOAD_EXITING | CpuCtrl::CR3_STORE_EXITING).bits(),
223229
)?;
224230

@@ -268,7 +274,7 @@ impl<H: RvmHal> VmxVcpu<H> {
268274
VmcsControl32::VMEXIT_MSR_LOAD_COUNT.write(0)?;
269275
VmcsControl32::VMENTRY_MSR_LOAD_COUNT.write(0)?;
270276

271-
// Pass-through exceptions, I/O instructions, set MSR bitmaps.
277+
// Pass-through exceptions, don't use I/O bitmap, set MSR bitmaps.
272278
VmcsControl32::EXCEPTION_BITMAP.write(0)?;
273279
VmcsControl64::IO_BITMAP_A_ADDR.write(0)?;
274280
VmcsControl64::IO_BITMAP_B_ADDR.write(0)?;

rvm/src/arch/x86_64/vmx/vmcs.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,21 @@ pub struct VmxExitInfo {
490490
pub guest_rip: usize,
491491
}
492492

493+
/// Exit Qualification for I/O Instructions. (SDM Vol. 3C, Section 27.2.1, Table 27-5)
494+
#[derive(Debug)]
495+
pub struct VmxIoExitInfo {
496+
/// Size of access.
497+
pub access_size: u8,
498+
/// Direction of the attempted access (0 = OUT, 1 = IN).
499+
pub is_in: bool,
500+
/// String instruction (0 = not string; 1 = string).
501+
pub is_string: bool,
502+
/// REP prefixed (0 = not REP; 1 = REP).
503+
pub is_repeat: bool,
504+
/// Port number. (as specified in DX or in an immediate operand)
505+
pub port: u16,
506+
}
507+
493508
pub mod controls {
494509
pub use x86::vmx::vmcs::control::{EntryControls, ExitControls};
495510
pub use x86::vmx::vmcs::control::{PinbasedControls, PrimaryControls, SecondaryControls};
@@ -564,6 +579,18 @@ pub fn exit_info() -> RvmResult<VmxExitInfo> {
564579
})
565580
}
566581

582+
pub fn io_exit_info() -> RvmResult<VmxIoExitInfo> {
583+
// SDM Vol. 3C, Section 27.2.1, Table 27-5
584+
let qualification = VmcsReadOnlyNW::EXIT_QUALIFICATION.read()?;
585+
Ok(VmxIoExitInfo {
586+
access_size: qualification.get_bits(0..3) as u8 + 1,
587+
is_in: qualification.get_bit(3),
588+
is_string: qualification.get_bit(4),
589+
is_repeat: qualification.get_bit(5),
590+
port: qualification.get_bits(16..32) as u16,
591+
})
592+
}
593+
567594
pub fn ept_violation_info() -> RvmResult<NestedPageFaultInfo> {
568595
// SDM Vol. 3C, Section 27.2.1, Table 27-7
569596
let qualification = VmcsReadOnlyNW::EXIT_QUALIFICATION.read()?;

0 commit comments

Comments
 (0)