Skip to content

Commit 659edd1

Browse files
arch: CPU profile generation CLI
This commit introduces a CLI for generating a CPU profile closely matching the CPU of the machine the CLI is executed on. The idea is to have a simple way to add more CPU profiles corresponding to physical CPUs. Note however that with the current setup one still needs a little bit of manual work to integrate the generated CPU profile data into cloud hypervisor itself. Signed-off-by: Oliver Anderson <[email protected]> On-behalf-of: SAP [email protected]
1 parent 1795bfd commit 659edd1

File tree

5 files changed

+327
-0
lines changed

5 files changed

+327
-0
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

arch/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,25 @@ edition.workspace = true
44
name = "arch"
55
version = "0.1.0"
66

7+
# TODO: Consider making this a binary of the main package instead
8+
[[bin]]
9+
name = "generate-cpu-profile"
10+
path = "src/bin/generate-cpu-profile.rs"
11+
required-features = ["cpu_profile_generation"]
12+
713
[features]
814
default = []
915
fw_cfg = []
1016
kvm = ["hypervisor/kvm"]
1117
sev_snp = []
1218
tdx = []
19+
# Currently cpu profiles can only be generated with KVM
20+
cpu_profile_generation = ["dep:clap", "kvm"]
1321

1422
[dependencies]
1523
anyhow = { workspace = true }
1624
byteorder = { workspace = true }
25+
clap = { workspace = true, optional = true }
1726
hypervisor = { path = "../hypervisor" }
1827
libc = { workspace = true }
1928
linux-loader = { workspace = true, features = ["bzimage", "elf", "pe"] }
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright © 2025 Cyberus Technology GmbH
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
#![cfg(all(
6+
target_arch = "x86_64",
7+
feature = "cpu_profile_generation",
8+
feature = "kvm"
9+
))]
10+
use std::io::BufWriter;
11+
12+
use anyhow::Context;
13+
use clap::{Arg, Command};
14+
15+
fn main() -> anyhow::Result<()> {
16+
let cmd_arg = Command::new("generate-cpu-profile")
17+
.version(env!("CARGO_PKG_VERSION"))
18+
.arg_required_else_help(true)
19+
.arg(
20+
Arg::new("name")
21+
.help("The name to give the CPU profile")
22+
.num_args(1)
23+
.required(true),
24+
)
25+
.get_matches();
26+
27+
let profile_name = cmd_arg.get_one::<String>("name").unwrap();
28+
29+
let hypervisor = hypervisor::new().context("Could not obtain hypervisor")?;
30+
// TODO: Consider letting the user provide a file path as a target instead of writing to stdout.
31+
// The way it is now should be sufficient for a PoC however.
32+
let writer = BufWriter::new(std::io::stdout().lock());
33+
arch::x86_64::cpu_profile_generation::generate_profile_data(
34+
writer,
35+
hypervisor.as_ref(),
36+
profile_name,
37+
)
38+
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
// Copyright © 2025 Cyberus Technology GmbH
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
use std::io::Write;
6+
use std::ops::RangeInclusive;
7+
8+
use anyhow::{Context, anyhow};
9+
use hypervisor::arch::x86::CpuIdEntry;
10+
use hypervisor::{CpuVendor, Hypervisor, HypervisorError, HypervisorType};
11+
12+
use crate::x86_64::cpu_profile::CpuProfileData;
13+
#[cfg(feature = "kvm")]
14+
use crate::x86_64::cpuid_definitions::CpuidDefinitions;
15+
use crate::x86_64::cpuid_definitions::intel::INTEL_CPUID_DEFINITIONS;
16+
use crate::x86_64::cpuid_definitions::kvm::KVM_CPUID_DEFINITIONS;
17+
use crate::x86_64::cpuid_definitions::{Parameters, ProfilePolicy};
18+
use crate::x86_64::{CpuidOutputRegisterAdjustments, CpuidReg};
19+
20+
/// Generate CPU profile data and convert it to a string, embeddable as Rust code, which is
21+
/// written to the given `writer` (e.g. a File).
22+
//
23+
// NOTE: The MVP only works with KVM as the hypervisor and Intel CPUs.
24+
#[cfg(feature = "kvm")]
25+
pub fn generate_profile_data(
26+
mut writer: impl Write,
27+
hypervisor: &dyn Hypervisor,
28+
profile_name: &str,
29+
) -> anyhow::Result<()> {
30+
let cpu_vendor = hypervisor.get_cpu_vendor();
31+
if cpu_vendor != CpuVendor::Intel {
32+
unimplemented!("CPU profiles can only be generated for Intel CPUs at this point in time");
33+
}
34+
35+
let hypervisor_type = hypervisor.hypervisor_type();
36+
// This is just a reality check.
37+
if hypervisor_type != HypervisorType::Kvm {
38+
unimplemented!(
39+
"CPU profiles can only be generated when using KVM as the hypervisor at this point in time"
40+
);
41+
}
42+
43+
let brand_string_bytes = cpu_brand_string_bytes(cpu_vendor, profile_name)?;
44+
let cpuid = supported_cpuid(hypervisor)?;
45+
let cpuid = overwrite_brand_string(cpuid, brand_string_bytes);
46+
let supported_cpuid_sorted = sort_entries(cpuid);
47+
48+
generate_cpu_profile_data_with(
49+
hypervisor_type,
50+
cpu_vendor,
51+
supported_cpuid_sorted,
52+
&INTEL_CPUID_DEFINITIONS,
53+
&KVM_CPUID_DEFINITIONS,
54+
&mut writer,
55+
)
56+
}
57+
58+
/// Prepare the bytes which the brand string should consist of
59+
fn cpu_brand_string_bytes(cpu_vendor: CpuVendor, profile_name: &str) -> anyhow::Result<[u8; 48]> {
60+
let cpu_vendor_str: String = serde_json::to_string(&cpu_vendor)
61+
.expect("Should be possible to serialize CPU vendor to a string");
62+
let cpu_vendor_str = cpu_vendor_str.trim_start_matches('"').trim_end_matches('"');
63+
let mut brand_string_bytes = [0_u8; 4 * 3 * 4];
64+
if cpu_vendor_str.len() + 1 + profile_name.len() > brand_string_bytes.len() {
65+
return Err(anyhow!(
66+
"The profile name is too long. Try using a shorter name"
67+
));
68+
}
69+
for (b, brand_byte) in cpu_vendor_str
70+
.as_bytes()
71+
.iter()
72+
.chain(std::iter::once(&b' '))
73+
.chain(profile_name.as_bytes())
74+
.zip(brand_string_bytes.iter_mut())
75+
{
76+
*brand_byte = *b;
77+
}
78+
Ok(brand_string_bytes)
79+
}
80+
/// Computes [`CpuProfileData`] based on the given sorted vector of CPUID entries, hypervisor type, cpu_vendor
81+
/// and cpuid_definitions.
82+
///
83+
/// The computed [`CpuProfileData`] is then converted to a string representation, embeddable as Rust code, which is
84+
/// then written by the given `writer`.
85+
///
86+
// TODO: Consider making a snapshot test or two for this function.
87+
fn generate_cpu_profile_data_with<const N: usize, const M: usize>(
88+
hypervisor_type: HypervisorType,
89+
cpu_vendor: CpuVendor,
90+
supported_cpuid_sorted: Vec<CpuIdEntry>,
91+
processor_cpuid_definitions: &CpuidDefinitions<N>,
92+
hypervisor_cpuid_definitions: &CpuidDefinitions<M>,
93+
mut writer: &mut impl Write,
94+
) -> anyhow::Result<()> {
95+
let mut adjustments: Vec<(Parameters, CpuidOutputRegisterAdjustments)> = Vec::new();
96+
97+
for (parameter, values) in processor_cpuid_definitions
98+
.as_slice()
99+
.iter()
100+
.chain(hypervisor_cpuid_definitions.as_slice().iter())
101+
{
102+
for (sub_leaf_range, maybe_matching_register_output_value) in
103+
extract_parameter_matches(parameter, &supported_cpuid_sorted)
104+
{
105+
// If the compatibility target (current host) has multiple sub-leaves matching the parameter's range
106+
// then we want to specialize:
107+
let mut mask: u32 = 0;
108+
let mut replacements: u32 = 0;
109+
for value in values.as_slice() {
110+
// Reality check on the bit range listed in `value`
111+
{
112+
assert!(value.bits_range.0 <= value.bits_range.1);
113+
assert!(value.bits_range.1 < 32);
114+
}
115+
116+
match value.policy {
117+
ProfilePolicy::Passthrough => {
118+
// The profile should take whatever we get from the host, hence there is no adjustment, but our
119+
// mask needs to retain all bits in the range of bits corresponding to this value
120+
let (first_bit_pos, last_bit_pos) = value.bits_range;
121+
mask |= bit_range_mask(first_bit_pos, last_bit_pos);
122+
}
123+
ProfilePolicy::Static(overwrite_value) => {
124+
replacements |= overwrite_value << value.bits_range.0;
125+
}
126+
ProfilePolicy::Inherit => {
127+
// The value is supposed to be obtained from the compatibility target if it exists
128+
let (first_bit_pos, last_bit_pos) = value.bits_range;
129+
if let Some(matching_register_value) = maybe_matching_register_output_value
130+
{
131+
let extraction_mask = bit_range_mask(first_bit_pos, last_bit_pos);
132+
let value = matching_register_value & extraction_mask;
133+
replacements |= value;
134+
}
135+
}
136+
}
137+
}
138+
adjustments.push((
139+
Parameters {
140+
leaf: parameter.leaf,
141+
sub_leaf: sub_leaf_range,
142+
register: parameter.register,
143+
},
144+
CpuidOutputRegisterAdjustments { mask, replacements },
145+
));
146+
}
147+
}
148+
149+
let profile_data = CpuProfileData {
150+
hypervisor: hypervisor_type,
151+
cpu_vendor,
152+
adjustments,
153+
};
154+
155+
serde_json::to_writer_pretty(&mut writer, &profile_data)
156+
.context("failed to serialize the generated profile data to the given writer")?;
157+
writer
158+
.flush()
159+
.context("CPU profile generation failed: Unable to flush cpu profile data")
160+
}
161+
162+
/// Get as many of the supported CPUID entries from the hypervisor as possible.
163+
fn supported_cpuid(hypervisor: &dyn Hypervisor) -> anyhow::Result<Vec<CpuIdEntry>> {
164+
// Check for AMX compatibility. If this is supported we need to call arch_prctl before requesting the supported
165+
// CPUID entries from the hypervisor. We simply call the enable_amx_state_components method on the hypervisor and
166+
// ignore any AMX not supported error to achieve this.
167+
match hypervisor.enable_amx_state_components() {
168+
Ok(()) => {}
169+
Err(HypervisorError::CouldNotEnableAmxStateComponents(amx_err)) => match amx_err {
170+
// TODO: Explain
171+
err @ hypervisor::arch::x86::AmxGuestSupportError::AmxGuestTileRequest { .. } => {
172+
return Err(err).context("Unable to enable AMX state tiles for guests");
173+
}
174+
_ => {}
175+
},
176+
Err(_) => unreachable!("Unexpected error when checking AMX support"),
177+
}
178+
179+
hypervisor
180+
.get_supported_cpuid()
181+
.context("CPU profile data generation failed")
182+
}
183+
184+
/// Overwrite the Processor brand string with the given `brand_string_bytes`
185+
fn overwrite_brand_string(
186+
mut cpuid: Vec<CpuIdEntry>,
187+
brand_string_bytes: [u8; 48],
188+
) -> Vec<CpuIdEntry> {
189+
let mut iter = brand_string_bytes
190+
.as_chunks::<4>()
191+
.0
192+
.iter()
193+
.map(|c| u32::from_le_bytes(*c));
194+
let mut overwrite = |leaf: u32| CpuIdEntry {
195+
function: leaf,
196+
index: 0,
197+
flags: 0,
198+
eax: iter.next().unwrap_or(0),
199+
ebx: iter.next().unwrap_or(0),
200+
ecx: iter.next().unwrap_or(0),
201+
edx: iter.next().unwrap_or(0),
202+
};
203+
for leaf in [0x80000002, 0x80000003, 0x80000004] {
204+
if let Some(entry) = cpuid
205+
.iter_mut()
206+
.find(|entry| (entry.function == leaf) && (entry.index == 0))
207+
{
208+
*entry = overwrite(leaf);
209+
} else {
210+
cpuid.push(overwrite(leaf));
211+
}
212+
}
213+
cpuid
214+
}
215+
216+
/// Sort the CPUID entries by function and index
217+
fn sort_entries(mut cpuid: Vec<CpuIdEntry>) -> Vec<CpuIdEntry> {
218+
cpuid.sort_unstable_by(|entry, other_entry| {
219+
let fn_cmp = entry.function.cmp(&other_entry.function);
220+
if fn_cmp == core::cmp::Ordering::Equal {
221+
entry.index.cmp(&other_entry.index)
222+
} else {
223+
fn_cmp
224+
}
225+
});
226+
cpuid
227+
}
228+
/// Returns a `u32` where each bit between `first_bit_pos` and `last_bit_pos` is set (including both ends) and all other bits are 0.
229+
fn bit_range_mask(first_bit_pos: u8, last_bit_pos: u8) -> u32 {
230+
(first_bit_pos..=last_bit_pos).fold(0, |acc, next| acc | (1 << next))
231+
}
232+
233+
/// Returns a vector of exact parameter matches ((sub_leaf ..= sub_leaf), register_value) interleaved by
234+
/// the sub_leaf ranges specified by `param` that did not match any cpuid entry.
235+
fn extract_parameter_matches(
236+
param: &Parameters,
237+
supported_cpuid_sorted: &[CpuIdEntry],
238+
) -> Vec<(RangeInclusive<u32>, Option<u32>)> {
239+
let register_value = |entry: &CpuIdEntry| -> u32 {
240+
match param.register {
241+
CpuidReg::EAX => entry.eax,
242+
CpuidReg::EBX => entry.ebx,
243+
CpuidReg::ECX => entry.ecx,
244+
CpuidReg::EDX => entry.edx,
245+
}
246+
};
247+
let mut out = Vec::new();
248+
let param_range = param.sub_leaf.clone();
249+
let mut range_for_consideration = param_range.clone();
250+
let range_end = *range_for_consideration.end();
251+
for sub_leaf_entry in supported_cpuid_sorted
252+
.iter()
253+
.filter(|entry| entry.function == param.leaf && param_range.contains(&entry.index))
254+
{
255+
let matching_subleaf = sub_leaf_entry.index;
256+
257+
// If we are in the middle of the range, it means there is no entry matching the first few sub-leaves within the range
258+
let current_range_start = *range_for_consideration.start();
259+
if current_range_start < matching_subleaf {
260+
let range_not_matching = RangeInclusive::new(current_range_start, matching_subleaf - 1);
261+
out.push((range_not_matching, None));
262+
}
263+
264+
out.push((
265+
RangeInclusive::new(matching_subleaf, matching_subleaf),
266+
Some(register_value(sub_leaf_entry)),
267+
));
268+
if matching_subleaf == range_end {
269+
return out;
270+
}
271+
// Update range_for_consideration: Note that we must have index + 1 <= range_end
272+
range_for_consideration = RangeInclusive::new(matching_subleaf + 1, range_end);
273+
}
274+
// We did not find the last entry within the range hence we push the final range for consideration together with no matching register value
275+
out.push((range_for_consideration, None));
276+
out
277+
}

arch/src/x86_64/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
// Use of this source code is governed by a BSD-style license that can be
88
// found in the LICENSE-BSD-3-Clause file.
99
pub mod cpu_profile;
10+
#[cfg(feature = "cpu_profile_generation")]
11+
pub mod cpu_profile_generation;
1012
pub mod cpuid_definitions;
1113
pub mod interrupts;
1214
pub mod layout;

0 commit comments

Comments
 (0)