vmm/cpu_config/x86_64/cpuid/intel/
normalize.rs

1// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::cpu_config::x86_64::cpuid::normalize::{
5    CheckedAssignError, get_range, set_bit, set_range,
6};
7use crate::cpu_config::x86_64::cpuid::{
8    BRAND_STRING_LENGTH, CpuidKey, CpuidRegisters, CpuidTrait, MissingBrandStringLeaves,
9    host_brand_string,
10};
11
12/// Error type for [`super::IntelCpuid::normalize`].
13#[derive(Debug, thiserror::Error, displaydoc::Display, Eq, PartialEq)]
14pub enum NormalizeCpuidError {
15    /// Failed to set deterministic cache leaf: {0}
16    DeterministicCache(#[from] DeterministicCacheError),
17    /// Leaf 0x6 is missing from CPUID.
18    MissingLeaf6,
19    /// Leaf 0x7 / subleaf 0 is missing from CPUID.
20    MissingLeaf7,
21    /// Leaf 0xA is missing from CPUID.
22    MissingLeafA,
23    /// Failed to get brand string: {0}
24    GetBrandString(DefaultBrandStringError),
25    /// Failed to set brand string: {0}
26    ApplyBrandString(MissingBrandStringLeaves),
27}
28
29/// Error type for setting leaf 4 section of [`super::IntelCpuid::normalize`].
30// `displaydoc::Display` does not support multi-line comments, `rustfmt` will format these comments
31// across multiple lines, so we skip formatting here. This can be removed when
32// https://github.com/yaahc/displaydoc/issues/44 is resolved.
33#[rustfmt::skip]
34#[allow(clippy::enum_variant_names)]
35#[derive(Debug, thiserror::Error, displaydoc::Display, Eq, PartialEq)]
36pub enum DeterministicCacheError {
37    /// Failed to set max addressable core ID in physical package (CPUID.04H:EAX[31:26]): {0}.
38    MaxCorePerPackage(CheckedAssignError),
39    /// Failed to set max addressable core ID in physical package (CPUID.04H:EAX[31:26]) due to underflow in cores.
40    MaxCorePerPackageUnderflow,
41    /// Failed to set max addressable processor ID sharing cache (CPUID.04H:EAX[25:14]): {0}.
42    MaxCpusPerCore(CheckedAssignError),
43    /// Failed to set max addressable processor ID sharing cache (CPUID.04H:EAX[25:14]) due to underflow in cpu count.
44    MaxCpusPerCoreUnderflow,
45}
46
47/// We always use this brand string.
48pub const DEFAULT_BRAND_STRING: &[u8; BRAND_STRING_LENGTH] =
49    b"Intel(R) Xeon(R) Processor\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
50pub const DEFAULT_BRAND_STRING_BASE: &[u8; 28] = b"Intel(R) Xeon(R) Processor @";
51
52// We use this 2nd implementation so we can conveniently define functions only used within
53// `normalize`.
54#[allow(clippy::multiple_inherent_impl)]
55impl super::IntelCpuid {
56    /// Applies required modifications to CPUID respective of a vCPU.
57    ///
58    /// # Errors
59    ///
60    /// When attempting to access missing leaves or set fields within leaves to values that don't
61    /// fit.
62    #[inline]
63    pub fn normalize(
64        &mut self,
65        // The index of the current logical CPU in the range [0..cpu_count].
66        _cpu_index: u8,
67        // The total number of logical CPUs.
68        cpu_count: u8,
69        // The number of logical CPUs per core.
70        cpus_per_core: u8,
71    ) -> Result<(), NormalizeCpuidError> {
72        self.update_deterministic_cache_entry(cpu_count, cpus_per_core)?;
73        self.update_power_management_entry()?;
74        self.update_extended_feature_flags_entry()?;
75        self.update_performance_monitoring_entry()?;
76        self.update_extended_topology_v2_entry();
77        self.update_brand_string_entry()?;
78
79        Ok(())
80    }
81
82    /// Update deterministic cache entry
83    #[allow(clippy::unwrap_in_result)]
84    fn update_deterministic_cache_entry(
85        &mut self,
86        cpu_count: u8,
87        cpus_per_core: u8,
88    ) -> Result<(), DeterministicCacheError> {
89        for i in 0.. {
90            if let Some(subleaf) = self.get_mut(&CpuidKey::subleaf(0x4, i)) {
91                // If ECX contains an invalid subleaf, EAX/EBX/ECX/EDX return 0 and the
92                // normalization should not be applied. Exits when it hits such an invalid subleaf.
93                if subleaf.result.eax == 0
94                    && subleaf.result.ebx == 0
95                    && subleaf.result.ecx == 0
96                    && subleaf.result.edx == 0
97                {
98                    break;
99                }
100
101                // CPUID.04H:EAX[7:5]
102                // Cache Level (Starts at 1)
103                let cache_level = get_range(subleaf.result.eax, 5..=7);
104
105                // CPUID.04H:EAX[25:14]
106                // Maximum number of addressable IDs for logical processors sharing this cache.
107                // - Add one to the return value to get the result.
108                // - The nearest power-of-2 integer that is not smaller than (1 + EAX[25:14]) is the
109                //   number of unique initial APIC IDs reserved for addressing different logical
110                //   processors sharing this cache.
111
112                // We know `cpus_per_core > 0` therefore `cpus_per_core.checked_sub(1).unwrap()` is
113                // always safe.
114                #[allow(clippy::unwrap_used)]
115                match cache_level {
116                    // L1 & L2 Cache
117                    // The L1 & L2 cache is shared by at most 2 hyperthreads
118                    1 | 2 => {
119                        let sub = u32::from(cpus_per_core.checked_sub(1).unwrap());
120                        set_range(&mut subleaf.result.eax, 14..=25, sub)
121                            .map_err(DeterministicCacheError::MaxCpusPerCore)?;
122                    }
123                    // L3 Cache
124                    // The L3 cache is shared among all the logical threads
125                    3 => {
126                        let sub = u32::from(
127                            cpu_count
128                                .checked_sub(1)
129                                .ok_or(DeterministicCacheError::MaxCpusPerCoreUnderflow)?,
130                        );
131                        set_range(&mut subleaf.result.eax, 14..=25, sub)
132                            .map_err(DeterministicCacheError::MaxCpusPerCore)?;
133                    }
134                    _ => (),
135                }
136
137                // We know `cpus_per_core !=0` therefore this is always safe.
138                #[allow(clippy::unwrap_used)]
139                let cores = cpu_count.checked_div(cpus_per_core).unwrap();
140
141                // CPUID.04H:EAX[31:26]
142                // Maximum number of addressable IDs for processor cores in the physical package.
143                // - Add one to the return value to get the result.
144                // - The nearest power-of-2 integer that is not smaller than (1 + EAX[31:26]) is the
145                //   number of unique Core_IDs reserved for addressing different processor cores in
146                //   a physical package. Core ID is a subset of bits of the initial APIC ID.
147                // - The returned value is constant for valid initial values in ECX. Valid ECX
148                //   values start from 0.
149
150                // Put all the cores in the same socket
151                let sub = u32::from(cores)
152                    .checked_sub(1)
153                    .ok_or(DeterministicCacheError::MaxCorePerPackageUnderflow)?;
154                set_range(&mut subleaf.result.eax, 26..=31, sub)
155                    .map_err(DeterministicCacheError::MaxCorePerPackage)?;
156            } else {
157                break;
158            }
159        }
160        Ok(())
161    }
162
163    /// Update power management entry
164    fn update_power_management_entry(&mut self) -> Result<(), NormalizeCpuidError> {
165        let leaf_6 = self
166            .get_mut(&CpuidKey::leaf(0x6))
167            .ok_or(NormalizeCpuidError::MissingLeaf6)?;
168
169        // CPUID.06H:EAX[1]
170        // Intel Turbo Boost Technology available (see description of IA32_MISC_ENABLE[38]).
171        set_bit(&mut leaf_6.result.eax, 1, false);
172
173        // CPUID.06H:ECX[3]
174        // The processor supports performance-energy bias preference if CPUID.06H:ECX.SETBH[bit 3]
175        // is set and it also implies the presence of a new architectural MSR called
176        // IA32_ENERGY_PERF_BIAS (1B0H).
177
178        // Clear X86 EPB feature. No frequency selection in the hypervisor.
179        set_bit(&mut leaf_6.result.ecx, 3, false);
180        Ok(())
181    }
182
183    /// Update structured extended feature flags enumeration leaf
184    fn update_extended_feature_flags_entry(&mut self) -> Result<(), NormalizeCpuidError> {
185        let leaf_7_0 = self
186            .get_mut(&CpuidKey::subleaf(0x7, 0))
187            .ok_or(NormalizeCpuidError::MissingLeaf7)?;
188
189        // Set the following bits as recommended in kernel doc. These bits are reserved in AMD.
190        // - CPUID.07H:EBX[6] (FDP_EXCPTN_ONLY)
191        // - CPUID.07H:EBX[13] (Deprecates FPU CS and FPU DS values)
192        // https://lore.kernel.org/all/20220322110712.222449-3-pbonzini@redhat.com/
193        // https://github.com/torvalds/linux/commit/45016721de3c714902c6f475b705e10ae0bdd801
194        set_bit(&mut leaf_7_0.result.ebx, 6, true);
195        set_bit(&mut leaf_7_0.result.ebx, 13, true);
196
197        // CPUID.(EAX=07H,ECX=0):ECX[5] (Mnemonic: WAITPKG)
198        //
199        // WAITPKG indicates support of user wait instructions (UMONITOR, UMWAIT and TPAUSE).
200        // - UMONITOR arms address monitoring hardware that checks for store operations on the
201        //   specified address range.
202        // - UMWAIT instructs the processor to enter an implementation-dependent optimized state
203        //   (either a light-weight power/performance optimized state (C0.1 idle state) or an
204        //   improved power/performance optimized state (C0.2 idle state)) while monitoring the
205        //   address range specified in UMONITOR. The instruction wakes up when the time-stamp
206        //   counter reaches or exceeds the implicit EDX:EAX 64-bit input value.
207        // - TPAUSE instructs the processor to enter an implementation-dependent optimized state.
208        //   The instruction wakes up when the time-stamp counter reaches or exceeds the implict
209        //   EDX:EAX 64-bit input value.
210        //
211        // These instructions may be executed at any privilege level. Even when UMWAIT/TPAUSE are
212        // executed within a guest, the *physical* processor enters the requested optimized state.
213        // See Intel SDM vol.3 for more details of the behavior of these instructions in VMX
214        // non-root operation.
215        //
216        // MONITOR/MWAIT instructions are the privileged variant of UMONITOR/UMWAIT and are
217        // unconditionally emulated as NOP by KVM.
218        // https://github.com/torvalds/linux/commit/87c00572ba05aa8c9db118da75c608f47eb10b9e
219        //
220        // When UMONITOR/UMWAIT/TPAUSE were initially introduced, KVM clears the WAITPKG CPUID bit
221        // in KVM_GET_SUPPORTED_CPUID by default, and KVM exposed them to guest only when VMM
222        // explicitly set the bit via KVM_SET_CPUID2 API.
223        // https://github.com/torvalds/linux/commit/e69e72faa3a0709dd23df6a4ca060a15e99168a1
224        // However, since v5.8, if the processor supports "enable user wait and pause" in Intel VMX,
225        // KVM_GET_SUPPORTED_CPUID sets the bit to 1 to let VMM know that it is available. So if the
226        // returned value is passed to KVM_SET_CPUID2 API as it is, guests are able to execute them.
227        // https://github.com/torvalds/linux/commit/0abcc8f65cc23b65bc8d1614cc64b02b1641ed7c
228        //
229        // Similar to MONITOR/MWAIT, we disable the guest's WAITPKG in order to prevent a guest from
230        // executing those instructions and putting a physical processor to an idle state which may
231        // lead to an overhead of waking it up when scheduling another guest on it. By clearing the
232        // WAITPKG bit in KVM_SET_CPUID2 API, KVM does not set the "enable user wait and pause" bit
233        // (bit 26) of the secondary processor-based VM-execution control, which makes guests get
234        // #UD when attempting to executing those instructions.
235        //
236        // Note that the WAITPKG bit is reserved on AMD.
237        set_bit(&mut leaf_7_0.result.ecx, 5, false);
238
239        Ok(())
240    }
241
242    /// Update performance monitoring entry
243    fn update_performance_monitoring_entry(&mut self) -> Result<(), NormalizeCpuidError> {
244        let leaf_a = self
245            .get_mut(&CpuidKey::leaf(0xA))
246            .ok_or(NormalizeCpuidError::MissingLeafA)?;
247        leaf_a.result = CpuidRegisters {
248            eax: 0,
249            ebx: 0,
250            ecx: 0,
251            edx: 0,
252        };
253        Ok(())
254    }
255
256    /// Update extended topology v2 entry
257    ///
258    /// CPUID leaf 1FH is a preferred superset to leaf 0xB. Intel recommends using leaf 0x1F when
259    /// available rather than leaf 0xB.
260    ///
261    /// Since we don't use any domains than ones supported in leaf 0xB, we just copy contents of
262    /// leaf 0xB to leaf 0x1F.
263    fn update_extended_topology_v2_entry(&mut self) {
264        // Skip if leaf 0x1F does not exist.
265        if self.get(&CpuidKey::leaf(0x1F)).is_none() {
266            return;
267        }
268
269        for index in 0.. {
270            if let Some(subleaf) = self.get(&CpuidKey::subleaf(0xB, index)) {
271                self.0
272                    .insert(CpuidKey::subleaf(0x1F, index), subleaf.clone());
273            } else {
274                break;
275            }
276        }
277    }
278
279    fn update_brand_string_entry(&mut self) -> Result<(), NormalizeCpuidError> {
280        // Get host brand string.
281        let host_brand_string: [u8; BRAND_STRING_LENGTH] = host_brand_string();
282
283        let default_brand_string =
284            default_brand_string(host_brand_string).unwrap_or(*DEFAULT_BRAND_STRING);
285
286        self.apply_brand_string(&default_brand_string)
287            .map_err(NormalizeCpuidError::ApplyBrandString)?;
288        Ok(())
289    }
290}
291
292/// Error type for [`default_brand_string`].
293#[derive(Debug, Eq, PartialEq, thiserror::Error, displaydoc::Display)]
294pub enum DefaultBrandStringError {
295    /// Missing frequency: {0:?}.
296    MissingFrequency([u8; BRAND_STRING_LENGTH]),
297    /// Missing space: {0:?}.
298    MissingSpace([u8; BRAND_STRING_LENGTH]),
299    /// Insufficient space in brand string.
300    Overflow,
301}
302
303/// Normalize brand string to a generic Xeon(R) processor, with the actual CPU frequency
304///
305/// # Errors
306///
307/// When unable to parse the host brand string.
308/// `brand_string.try_into().unwrap()` cannot panic as we know
309/// `brand_string.len() == BRAND_STRING_LENGTH`
310///
311/// # Panics
312///
313/// Never.
314// As we pass through host frequency, we require CPUID and thus `cfg(cpuid)`.
315// TODO: Use `split_array_ref`
316// (https://github.com/firecracker-microvm/firecracker/issues/3347)
317#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
318#[inline]
319fn default_brand_string(
320    // Host brand string.
321    // This could look like "Intel(R) Xeon(R) Platinum 8275CL CPU @ 3.00GHz".
322    // or this could look like "Intel(R) Xeon(R) Platinum 8275CL CPU\0\0\0\0\0\0\0\0\0\0".
323    host_brand_string: [u8; BRAND_STRING_LENGTH],
324) -> Result<[u8; BRAND_STRING_LENGTH], DefaultBrandStringError> {
325    // The slice of the host string before the frequency suffix
326    // e.g. b"Intel(R) Xeon(R) Processor Platinum 8275CL CPU @ 3.00" and b"GHz"
327    let (before, after) = 'outer: {
328        for i in 0..host_brand_string.len() {
329            // Find position of b"THz" or b"GHz" or b"MHz"
330            if let [b'T' | b'G' | b'M', b'H', b'z', ..] = host_brand_string[i..] {
331                break 'outer Ok(host_brand_string.split_at(i));
332            }
333        }
334        Err(DefaultBrandStringError::MissingFrequency(host_brand_string))
335    }?;
336    debug_assert_eq!(
337        before.len().checked_add(after.len()),
338        Some(BRAND_STRING_LENGTH)
339    );
340
341    // We iterate from the end until hitting a space, getting the frequency number
342    // e.g. b"Intel(R) Xeon(R) Processor Platinum 8275CL CPU @ " and b"3.00"
343    let (_, frequency) = 'outer: {
344        for i in (0..before.len()).rev() {
345            let c = before[i];
346            match c {
347                b' ' => break 'outer Ok(before.split_at(i)),
348                b'0'..=b'9' | b'.' => (),
349                _ => break,
350            }
351        }
352        Err(DefaultBrandStringError::MissingSpace(host_brand_string))
353    }?;
354    debug_assert!(frequency.len() <= before.len());
355
356    debug_assert!(
357        matches!(frequency.len().checked_add(after.len()), Some(x) if x <= BRAND_STRING_LENGTH)
358    );
359    debug_assert!(DEFAULT_BRAND_STRING_BASE.len() <= BRAND_STRING_LENGTH);
360    debug_assert!(BRAND_STRING_LENGTH.checked_mul(2).is_some());
361
362    // As `DEFAULT_BRAND_STRING_BASE.len() + frequency.len() + after.len()` is guaranteed
363    // to be less than or equal to  `2*BRAND_STRING_LENGTH` and we know
364    // `2*BRAND_STRING_LENGTH <= usize::MAX` since `BRAND_STRING_LENGTH==48`, this is always
365    // safe.
366    let len = DEFAULT_BRAND_STRING_BASE.len() + frequency.len() + after.len();
367
368    let brand_string = DEFAULT_BRAND_STRING_BASE
369        .iter()
370        .copied()
371        // Include frequency e.g. "3.00"
372        .chain(frequency.iter().copied())
373        // Include frequency suffix e.g. "GHz"
374        .chain(after.iter().copied())
375        // Pad with 0s to `BRAND_STRING_LENGTH`
376        .chain(std::iter::repeat_n(
377            b'\0',
378            BRAND_STRING_LENGTH
379                .checked_sub(len)
380                .ok_or(DefaultBrandStringError::Overflow)?,
381        ))
382        .collect::<Vec<_>>();
383    debug_assert_eq!(brand_string.len(), BRAND_STRING_LENGTH);
384
385    // Padding ensures `brand_string.len() == BRAND_STRING_LENGTH` thus
386    // `brand_string.try_into().unwrap()` is safe.
387    #[allow(clippy::unwrap_used)]
388    Ok(brand_string.try_into().unwrap())
389}
390
391#[cfg(test)]
392mod tests {
393    #![allow(
394        clippy::undocumented_unsafe_blocks,
395        clippy::unwrap_used,
396        clippy::as_conversions
397    )]
398
399    use std::collections::BTreeMap;
400    use std::ffi::CStr;
401
402    use super::*;
403    use crate::cpu_config::x86_64::cpuid::{CpuidEntry, IntelCpuid, KvmCpuidFlags};
404
405    #[test]
406    fn default_brand_string_test() {
407        let brand_string = b"Intel(R) Xeon(R) Platinum 8275CL CPU @ 3.00GHz\0\0";
408        let ok_result = default_brand_string(*brand_string);
409        let expected = Ok(*b"Intel(R) Xeon(R) Processor @ 3.00GHz\0\0\0\0\0\0\0\0\0\0\0\0");
410        assert_eq!(ok_result, expected);
411    }
412    #[test]
413    fn default_brand_string_test_missing_frequency() {
414        let brand_string = b"Intel(R) Xeon(R) Platinum 8275CL CPU @ \0\0\0\0\0\0\0\0\0";
415        let result = default_brand_string(*brand_string);
416        let expected = Err(DefaultBrandStringError::MissingFrequency(*brand_string));
417        assert_eq!(result, expected);
418    }
419    #[test]
420    fn default_brand_string_test_missing_space() {
421        let brand_string = b"Intel(R) Xeon(R) Platinum 8275CL CPU @3.00GHz\0\0\0";
422        let result = default_brand_string(*brand_string);
423        let expected = Err(DefaultBrandStringError::MissingSpace(*brand_string));
424        assert_eq!(result, expected);
425    }
426    #[test]
427    fn default_brand_string_test_overflow() {
428        let brand_string = b"@ 123456789876543212345678987654321234567898GHz\0";
429        let result = default_brand_string(*brand_string);
430        assert_eq!(
431            result,
432            Err(DefaultBrandStringError::Overflow),
433            "{:?}",
434            result
435                .as_ref()
436                .map(|s| CStr::from_bytes_until_nul(s).unwrap()),
437        );
438    }
439
440    #[test]
441    fn test_update_extended_feature_flags_entry() {
442        let mut cpuid = IntelCpuid(BTreeMap::from([(
443            CpuidKey {
444                leaf: 0x7,
445                subleaf: 0,
446            },
447            CpuidEntry {
448                flags: KvmCpuidFlags::SIGNIFICANT_INDEX,
449                ..Default::default()
450            },
451        )]));
452
453        cpuid.update_extended_feature_flags_entry().unwrap();
454
455        let leaf_7_0 = cpuid
456            .get(&CpuidKey {
457                leaf: 0x7,
458                subleaf: 0,
459            })
460            .unwrap();
461        assert!((leaf_7_0.result.ebx & (1 << 6)) > 0);
462        assert!((leaf_7_0.result.ebx & (1 << 13)) > 0);
463        assert_eq!((leaf_7_0.result.ecx & (1 << 5)), 0);
464    }
465
466    #[test]
467    fn test_update_extended_topology_v2_entry_no_leaf_0x1f() {
468        let mut cpuid = IntelCpuid(BTreeMap::from([(
469            CpuidKey {
470                leaf: 0xB,
471                subleaf: 0,
472            },
473            CpuidEntry {
474                flags: KvmCpuidFlags::SIGNIFICANT_INDEX,
475                ..Default::default()
476            },
477        )]));
478
479        cpuid.update_extended_topology_v2_entry();
480
481        assert!(
482            cpuid
483                .get(&CpuidKey {
484                    leaf: 0x1F,
485                    subleaf: 0,
486                })
487                .is_none()
488        );
489    }
490
491    #[test]
492    fn test_update_extended_topology_v2_entry() {
493        let mut cpuid = IntelCpuid(BTreeMap::from([
494            (
495                CpuidKey {
496                    leaf: 0xB,
497                    subleaf: 0,
498                },
499                CpuidEntry {
500                    flags: KvmCpuidFlags::SIGNIFICANT_INDEX,
501                    result: CpuidRegisters {
502                        eax: 0x1,
503                        ebx: 0x2,
504                        ecx: 0x3,
505                        edx: 0x4,
506                    },
507                },
508            ),
509            (
510                CpuidKey {
511                    leaf: 0xB,
512                    subleaf: 1,
513                },
514                CpuidEntry {
515                    flags: KvmCpuidFlags::SIGNIFICANT_INDEX,
516                    result: CpuidRegisters {
517                        eax: 0xa,
518                        ebx: 0xb,
519                        ecx: 0xc,
520                        edx: 0xd,
521                    },
522                },
523            ),
524            (
525                CpuidKey {
526                    leaf: 0x1F,
527                    subleaf: 0,
528                },
529                CpuidEntry {
530                    flags: KvmCpuidFlags::SIGNIFICANT_INDEX,
531                    result: CpuidRegisters {
532                        eax: 0xFFFFFFFF,
533                        ebx: 0xFFFFFFFF,
534                        ecx: 0xFFFFFFFF,
535                        edx: 0xFFFFFFFF,
536                    },
537                },
538            ),
539        ]));
540
541        cpuid.update_extended_topology_v2_entry();
542
543        // Check leaf 0x1F, subleaf 0 is updated.
544        let leaf_1f_0 = cpuid
545            .get(&CpuidKey {
546                leaf: 0x1F,
547                subleaf: 0,
548            })
549            .unwrap();
550        assert_eq!(leaf_1f_0.result.eax, 0x1);
551        assert_eq!(leaf_1f_0.result.ebx, 0x2);
552        assert_eq!(leaf_1f_0.result.ecx, 0x3);
553        assert_eq!(leaf_1f_0.result.edx, 0x4);
554
555        // Check lefa 0x1F, subleaf 1 is inserted.
556        let leaf_1f_1 = cpuid
557            .get(&CpuidKey {
558                leaf: 0x1F,
559                subleaf: 1,
560            })
561            .unwrap();
562        assert_eq!(leaf_1f_1.result.eax, 0xa);
563        assert_eq!(leaf_1f_1.result.ebx, 0xb);
564        assert_eq!(leaf_1f_1.result.ecx, 0xc);
565        assert_eq!(leaf_1f_1.result.edx, 0xd);
566    }
567}