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}