Skip to main content

cranelift_assembler_x64/
fuzz.rs

1//! A fuzz testing oracle for roundtrip assembly-disassembly.
2//!
3//! This contains manual implementations of the `Arbitrary` trait for types
4//! throughout this crate to avoid depending on the `arbitrary` crate
5//! unconditionally (use the `fuzz` feature instead).
6
7use std::string::{String, ToString};
8use std::vec::Vec;
9use std::{format, println};
10
11use crate::{
12    AmodeOffset, AmodeOffsetPlusKnownOffset, AsReg, CodeSink, DeferredTarget, Feature, Features,
13    Fixed, Gpr, Inst, KnownOffset, NonRspGpr, Registers, TrapCode, Xmm,
14};
15use arbitrary::{Arbitrary, Result, Unstructured};
16use capstone::{Capstone, arch::BuildsCapstone, arch::BuildsCapstoneSyntax, arch::x86};
17
18/// Take a random assembly instruction and check its encoding and
19/// pretty-printing against a known-good disassembler.
20///
21/// This uses Capstone as the disassembler oracle; see `roundtrip_with` for the
22/// oracle-agnostic core.
23///
24/// # Panics
25///
26/// This function panics to express failure as expected by the `arbitrary`
27/// fuzzer infrastructure. It may fail during assembly, disassembly, or when
28/// comparing the disassembled strings.
29pub fn roundtrip(inst: &Inst<FuzzRegs>) {
30    // The bundled capstone build does not disassemble AVX-VNNI instructions, so
31    // the roundtrip oracle has no reference to compare against; skip them. Their
32    // encodings are covered by dedicated filetests.
33    if features_mention(inst.features(), Feature::avx_vnni) {
34        return;
35    }
36
37    // Likewise, capstone cannot decode the APX extended-EVEX ("map 4")
38    // encodings at all, returning zero instructions. These are instead checked
39    // against XED by `roundtrip_xed`, which does understand map 4.
40    if features_mention(inst.features(), Feature::apx) {
41        return;
42    }
43
44    roundtrip_with(
45        inst,
46        "capstone",
47        disassemble_capstone,
48        capstone_matches,
49        |i| format!("{i}"),
50    );
51}
52
53/// Like [`roundtrip`], but uses Intel XED as the disassembler oracle instead of
54/// Capstone.
55///
56/// XED understands newer encodings (e.g. APX) that the bundled Capstone does
57/// not, so this is a useful second oracle. It is only available with the
58/// `fuzz-xed` feature (which requires building XED from source).
59///
60/// # Panics
61///
62/// See [`roundtrip`].
63#[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))]
64pub fn roundtrip_xed(inst: &Inst<FuzzRegs>) {
65    roundtrip_with(inst, "xed", disassemble_xed, xed_matches, |i| {
66        format!("{i:#}")
67    });
68}
69
70/// The oracle-agnostic core of [`roundtrip`]: assemble `inst`, disassemble the
71/// resulting bytes with the provided `disassemble` oracle, and check that the
72/// oracle's output matches how `render` prints the instruction, as judged by
73/// the oracle-specific `matches` predicate.
74///
75/// `render` selects the syntax to compare against: `{inst}` is the assembler's
76/// own (what Cranelift's disassembly and the `precise-output` filetests use),
77/// while `{inst:#}` is XED's. Printing directly in the oracle's dialect avoids
78/// having to reconcile the two strings afterwards.
79///
80/// The `oracle` name is only used to label diagnostic output on failure.
81fn roundtrip_with(
82    inst: &Inst<FuzzRegs>,
83    oracle: &str,
84    disassemble: impl Fn(&[u8], &Inst<FuzzRegs>) -> String,
85    matches: impl Fn(&str, &str) -> bool,
86    render: impl Fn(&Inst<FuzzRegs>) -> String,
87) {
88    // Check that we can actually assemble this instruction.
89    let assembled = assemble(inst);
90    let expected = disassemble(&assembled, inst);
91
92    // Check that our pretty-printed output matches the known-good output. Trim
93    // off the instruction offset first.
94    let expected = expected.split_once(' ').unwrap().1;
95    let actual = render(inst);
96    if !matches(expected, &actual) {
97        println!("> {inst}");
98        println!("  debug: {inst:x?}");
99        println!("  assembled: {}", pretty_print_hexadecimal(&assembled));
100        println!("  expected ({oracle}): {expected}");
101        println!("  actual (to_string):  {actual}");
102        assert_eq!(expected, &actual);
103    }
104}
105
106/// Whether an instruction's feature term references `target`; used to skip
107/// instructions the disassembler oracle cannot handle.
108fn features_mention(features: &Features, target: Feature) -> bool {
109    match features {
110        Features::And(a, b) | Features::Or(a, b) => {
111            features_mention(a, target) || features_mention(b, target)
112        }
113        Features::Feature(f) => *f == target,
114    }
115}
116
117/// Comparison predicate for the Capstone oracle: exact match, or match after
118/// applying Capstone-specific normalization ([`fix_up`]) to the assembler
119/// output.
120fn capstone_matches(expected: &str, actual: &str) -> bool {
121    expected == actual || expected.trim() == fix_up(actual)
122}
123
124/// Use this assembler to emit machine code into a byte buffer.
125///
126/// This will skip any traps or label registrations, but this is fine for the
127/// single-instruction disassembly we're doing here.
128fn assemble(inst: &Inst<FuzzRegs>) -> Vec<u8> {
129    let mut sink = TestCodeSink::default();
130    inst.encode(&mut sink);
131    sink.patch_labels_as_if_they_referred_to_end();
132    sink.buf
133}
134
135#[derive(Default)]
136struct TestCodeSink {
137    buf: Vec<u8>,
138    offsets_using_label: Vec<usize>,
139}
140
141impl TestCodeSink {
142    /// References to labels, e.g. RIP-relative addressing, is stored with an
143    /// adjustment that takes into account the distance from the relative offset
144    /// to the end of the instruction, where the offset is relative to. That
145    /// means that to indeed make the offset relative to the end of the
146    /// instruction, which is what we pretend all labels are bound to, it's
147    /// required that this adjustment is taken into account.
148    ///
149    /// This function will iterate over all labels bound to this code sink and
150    /// pretend the label is found at the end of the `buf`. That means that the
151    /// distance from the label to the end of `buf` minus 4, which is the width
152    /// of the offset, is added to what's already present in the encoding buffer.
153    ///
154    /// This is effectively undoing the `bytes_at_end` adjustment that's part of
155    /// `Amode::RipRelative` addressing.
156    fn patch_labels_as_if_they_referred_to_end(&mut self) {
157        let len = i32::try_from(self.buf.len()).unwrap();
158        for offset in self.offsets_using_label.iter() {
159            let range = self.buf[*offset..].first_chunk_mut::<4>().unwrap();
160            let offset = i32::try_from(*offset).unwrap() + 4;
161            let rel_distance = len - offset;
162            *range = (i32::from_le_bytes(*range) + rel_distance).to_le_bytes();
163        }
164    }
165}
166
167impl CodeSink for TestCodeSink {
168    fn put1(&mut self, v: u8) {
169        self.buf.extend_from_slice(&[v]);
170    }
171
172    fn put2(&mut self, v: u16) {
173        self.buf.extend_from_slice(&v.to_le_bytes());
174    }
175
176    fn put4(&mut self, v: u32) {
177        self.buf.extend_from_slice(&v.to_le_bytes());
178    }
179
180    fn put8(&mut self, v: u64) {
181        self.buf.extend_from_slice(&v.to_le_bytes());
182    }
183
184    fn add_trap(&mut self, _: TrapCode) {}
185
186    fn use_target(&mut self, _: DeferredTarget) {
187        let offset = self.buf.len();
188        self.offsets_using_label.push(offset);
189    }
190
191    fn known_offset(&self, target: KnownOffset) -> i32 {
192        panic!("unsupported known target {target:?}")
193    }
194}
195
196/// Disassemble a single instruction with Capstone, returning its AT&T-syntax
197/// string. This is the default [`roundtrip`] oracle.
198///
199/// Building a new `Capstone` each time is suboptimal (TODO).
200fn disassemble_capstone(assembled: &[u8], original: &Inst<FuzzRegs>) -> String {
201    let cs = Capstone::new()
202        .x86()
203        .mode(x86::ArchMode::Mode64)
204        .syntax(x86::ArchSyntax::Att)
205        .detail(true)
206        .build()
207        .expect("failed to create Capstone object");
208    let insts = cs
209        .disasm_all(assembled, 0x0)
210        .expect("failed to disassemble");
211
212    if insts.len() != 1 {
213        println!("> {original}");
214        println!("  debug: {original:x?}");
215        println!("  assembled: {}", pretty_print_hexadecimal(&assembled));
216        assert_eq!(insts.len(), 1, "not a single instruction");
217    }
218
219    let inst = insts.first().expect("at least one instruction");
220    if assembled.len() != inst.len() {
221        println!("> {original}");
222        println!("  debug: {original:x?}");
223        println!("  assembled: {}", pretty_print_hexadecimal(&assembled));
224        println!(
225            "  capstone-assembled: {}",
226            pretty_print_hexadecimal(inst.bytes())
227        );
228        assert_eq!(assembled.len(), inst.len(), "extra bytes not disassembled");
229    }
230
231    inst.to_string()
232}
233
234/// Disassemble a single instruction with Intel XED, returning a string in the
235/// same shape as [`disassemble_capstone`] (a leading offset token, a space,
236/// then the AT&T-syntax instruction) so that [`roundtrip_with`] can compare it
237/// uniformly.
238#[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))]
239fn disassemble_xed(assembled: &[u8], original: &Inst<FuzzRegs>) -> String {
240    use core::ffi::c_void;
241    use std::sync::Once;
242    use xed_sys::*;
243
244    // XED requires a one-time global table initialization before any decode.
245    static INIT: Once = Once::new();
246    // SAFETY: `xed_tables_init` is safe to call; `Once` guarantees it runs
247    // exactly once even across threads.
248    INIT.call_once(|| unsafe { xed_tables_init() });
249
250    // SAFETY: all of the following are standard XED decode/format calls
251    // operating on stack-allocated, properly initialized structures.
252    unsafe {
253        let mut xedd: xed_decoded_inst_t = core::mem::zeroed();
254        xed_decoded_inst_zero(&mut xedd);
255        xed_decoded_inst_set_mode(&mut xedd, XED_MACHINE_MODE_LONG_64, XED_ADDRESS_WIDTH_64b);
256
257        let error = xed_decode(
258            &mut xedd,
259            assembled.as_ptr(),
260            assembled.len() as core::ffi::c_uint,
261        );
262        if error != XED_ERROR_NONE {
263            println!("> {original}");
264            println!("  debug: {original:x?}");
265            println!("  assembled: {}", pretty_print_hexadecimal(assembled));
266            let name = core::ffi::CStr::from_ptr(xed_error_enum_t2str(error));
267            panic!("xed failed to decode: {}", name.to_string_lossy());
268        }
269
270        // XED must consume exactly the bytes we emitted; a shorter length means
271        // trailing bytes were not part of the instruction.
272        let decoded_len = xed_decoded_inst_get_length(&xedd) as usize;
273        if decoded_len != assembled.len() {
274            println!("> {original}");
275            println!("  debug: {original:x?}");
276            println!("  assembled: {}", pretty_print_hexadecimal(assembled));
277            assert_eq!(
278                decoded_len,
279                assembled.len(),
280                "xed did not consume all bytes"
281            );
282        }
283
284        // Format in AT&T syntax to match the assembler's own pretty-printing.
285        let mut buf = [0i8; 256];
286        let ok = xed_format_context(
287            XED_SYNTAX_ATT,
288            &xedd,
289            buf.as_mut_ptr(),
290            buf.len() as core::ffi::c_int,
291            0,
292            core::ptr::null_mut::<c_void>(),
293            None,
294        );
295        assert!(ok != 0, "xed failed to format instruction");
296
297        let disasm = core::ffi::CStr::from_ptr(buf.as_ptr())
298            .to_string_lossy()
299            .into_owned();
300
301        // Prepend a fake offset token so the shape matches Capstone's
302        // `0x0: <inst>` output that `roundtrip_with` expects.
303        format!("0: {disasm}")
304    }
305}
306
307fn pretty_print_hexadecimal(hex: &[u8]) -> String {
308    use core::fmt::Write;
309    let mut s = String::with_capacity(hex.len() * 2);
310    for b in hex {
311        write!(&mut s, "{b:02X}").unwrap();
312    }
313    s
314}
315
316/// See `replace_signed_immediates`.
317macro_rules! hex_print_signed_imm {
318    ($hex:expr, $from:ty => $to:ty) => {{
319        let imm = <$from>::from_str_radix($hex, 16).unwrap() as $to;
320        let mut simm = String::new();
321        if imm < 0 {
322            simm.push_str("-");
323        }
324        let abs = match imm.checked_abs() {
325            Some(i) => i,
326            None => <$to>::MIN,
327        };
328        if imm > -10 && imm < 10 {
329            simm.push_str(&format!("{:x}", abs));
330        } else {
331            simm.push_str(&format!("0x{:x}", abs));
332        }
333        simm
334    }};
335}
336
337/// Replace signed immediates in the disassembly with their unsigned hexadecimal
338/// equivalent. This is only necessary to match `capstone`'s complex
339/// pretty-printing rules; e.g. `capstone` will:
340/// - omit the `0x` prefix when printing `0x0` as `0`.
341/// - omit the `0x` prefix when print small values (less than 10)
342/// - print negative values as `-0x...` (signed hex) instead of `0xff...`
343///   (normal hex)
344/// - print `mov` immediates as base-10 instead of base-16 (?!).
345fn replace_signed_immediates(dis: &str) -> alloc::borrow::Cow<'_, str> {
346    match dis.find('$') {
347        None => dis.into(),
348        Some(idx) => {
349            let (prefix, rest) = dis.split_at(idx + 1); // Skip the '$'.
350            let (_, rest) = chomp("-", rest); // Skip the '-' if it's there.
351            let (_, rest) = chomp("0x", rest); // Skip the '0x' if it's there.
352            let n = rest.chars().take_while(char::is_ascii_hexdigit).count();
353            let (hex, rest) = rest.split_at(n); // Split at next non-hex character.
354            let simm = if dis.starts_with("mov") {
355                u64::from_str_radix(hex, 16).unwrap().to_string()
356            } else {
357                match hex.len() {
358                    1 | 2 => hex_print_signed_imm!(hex, u8 => i8),
359                    4 => hex_print_signed_imm!(hex, u16 => i16),
360                    8 => hex_print_signed_imm!(hex, u32 => i32),
361                    16 => hex_print_signed_imm!(hex, u64 => i64),
362                    _ => panic!("unexpected length for hex: {hex}"),
363                }
364            };
365            format!("{prefix}{simm}{rest}").into()
366        }
367    }
368}
369
370// See `replace_signed_immediates`.
371fn chomp<'a>(pat: &str, s: &'a str) -> (&'a str, &'a str) {
372    if s.starts_with(pat) {
373        s.split_at(pat.len())
374    } else {
375        ("", s)
376    }
377}
378
379#[test]
380fn replace() {
381    assert_eq!(
382        replace_signed_immediates("andl $0xffffff9a, %r11d"),
383        "andl $-0x66, %r11d"
384    );
385    assert_eq!(
386        replace_signed_immediates("xorq $0xffffffffffffffbc, 0x7f139ecc(%r9)"),
387        "xorq $-0x44, 0x7f139ecc(%r9)"
388    );
389    assert_eq!(
390        replace_signed_immediates("subl $0x3ca77a19, -0x1a030f40(%r14)"),
391        "subl $0x3ca77a19, -0x1a030f40(%r14)"
392    );
393    assert_eq!(
394        replace_signed_immediates("movq $0xffffffff864ae103, %rsi"),
395        "movq $18446744071667638531, %rsi"
396    );
397}
398
399/// Remove everything after the first semicolon in the disassembly and trim any
400/// trailing spaces. This is necessary to remove the implicit operands we end up
401/// printing for Cranelift's sake.
402fn remove_after_semicolon(dis: &str) -> &str {
403    match dis.find(';') {
404        None => dis,
405        Some(idx) => {
406            let (prefix, _) = dis.split_at(idx);
407            prefix.trim()
408        }
409    }
410}
411
412#[test]
413fn remove_after_parenthesis_test() {
414    assert_eq!(
415        remove_after_semicolon("imulb 0x7658eddd(%rcx) ;; implicit: %ax"),
416        "imulb 0x7658eddd(%rcx)"
417    );
418}
419
420/// Run some post-processing on the disassembly to make it match Capstone.
421fn fix_up(dis: &str) -> alloc::borrow::Cow<'_, str> {
422    let dis = remove_after_semicolon(dis);
423    replace_signed_immediates(&dis)
424}
425
426/// Comparison predicate for the XED oracle.
427///
428/// The assembler renders the instruction in XED's own dialect (`{inst:#}`), so
429/// the two strings agree exactly apart from the extra padding XED inserts after
430/// the mnemonic.
431#[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))]
432fn xed_matches(expected: &str, actual: &str) -> bool {
433    expected.split_whitespace().eq(actual.split_whitespace())
434}
435
436/// Fuzz-specific registers.
437///
438/// For the fuzzer, we do not need any fancy register types; see [`FuzzReg`].
439#[derive(Clone, Arbitrary, Debug)]
440pub struct FuzzRegs;
441
442impl Registers for FuzzRegs {
443    type ReadGpr = FuzzReg;
444    type ReadWriteGpr = FuzzReg;
445    type WriteGpr = FuzzReg;
446    type ReadXmm = FuzzReg;
447    type ReadWriteXmm = FuzzReg;
448    type WriteXmm = FuzzReg;
449}
450
451/// A simple `u8` register type for fuzzing only.
452#[derive(Clone, Copy, Debug, PartialEq)]
453pub struct FuzzReg(u8);
454
455impl<'a> Arbitrary<'a> for FuzzReg {
456    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
457        Ok(Self(u.int_in_range(0..=15)?))
458    }
459}
460
461impl AsReg for FuzzReg {
462    fn new(enc: u8) -> Self {
463        Self(enc)
464    }
465    fn enc(&self) -> u8 {
466        self.0
467    }
468}
469
470impl Arbitrary<'_> for AmodeOffset {
471    fn arbitrary(u: &mut Unstructured<'_>) -> Result<Self> {
472        // Custom implementation to try to generate some "interesting" offsets.
473        // For example choose either an arbitrary 8-bit or 32-bit number as the
474        // base, and then optionally shift that number to the left to create
475        // multiples of constants. This can help stress some of the more
476        // interesting encodings in EVEX instructions for example.
477        let base = if u.arbitrary()? {
478            i32::from(u.arbitrary::<i8>()?)
479        } else {
480            u.arbitrary::<i32>()?
481        };
482        Ok(match u.int_in_range(0..=5)? {
483            0 => AmodeOffset::ZERO,
484            n => AmodeOffset::new(base << (n - 1)),
485        })
486    }
487}
488
489impl Arbitrary<'_> for AmodeOffsetPlusKnownOffset {
490    fn arbitrary(u: &mut Unstructured<'_>) -> Result<Self> {
491        // For now, we don't generate offsets (TODO).
492        Ok(Self {
493            simm32: AmodeOffset::arbitrary(u)?,
494            offset: None,
495        })
496    }
497}
498
499impl<R: AsReg, const E: u8> Arbitrary<'_> for Fixed<R, E> {
500    fn arbitrary(_: &mut Unstructured<'_>) -> Result<Self> {
501        Ok(Self::new(E))
502    }
503}
504
505impl<R: AsReg> Arbitrary<'_> for NonRspGpr<R> {
506    fn arbitrary(u: &mut Unstructured<'_>) -> Result<Self> {
507        use crate::gpr::enc::*;
508        let gpr = u.choose(&[
509            RAX, RCX, RDX, RBX, RBP, RSI, RDI, R8, R9, R10, R11, R12, R13, R14, R15,
510        ])?;
511        Ok(Self::new(R::new(*gpr)))
512    }
513}
514impl<'a, R: AsReg> Arbitrary<'a> for Gpr<R> {
515    fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
516        Ok(Self(R::new(u.int_in_range(0..=15)?)))
517    }
518}
519impl<'a, R: AsReg> Arbitrary<'a> for Xmm<R> {
520    fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
521        Ok(Self(R::new(u.int_in_range(0..=15)?)))
522    }
523}
524
525/// Helper trait that's used to be the same as `Registers` except with an extra
526/// `for<'a> Arbitrary<'a>` bound on all of the associated types.
527pub trait RegistersArbitrary:
528    Registers<
529        ReadGpr: for<'a> Arbitrary<'a>,
530        ReadWriteGpr: for<'a> Arbitrary<'a>,
531        WriteGpr: for<'a> Arbitrary<'a>,
532        ReadXmm: for<'a> Arbitrary<'a>,
533        ReadWriteXmm: for<'a> Arbitrary<'a>,
534        WriteXmm: for<'a> Arbitrary<'a>,
535    >
536{
537}
538
539impl<R> RegistersArbitrary for R
540where
541    R: Registers,
542    R::ReadGpr: for<'a> Arbitrary<'a>,
543    R::ReadWriteGpr: for<'a> Arbitrary<'a>,
544    R::WriteGpr: for<'a> Arbitrary<'a>,
545    R::ReadXmm: for<'a> Arbitrary<'a>,
546    R::ReadWriteXmm: for<'a> Arbitrary<'a>,
547    R::WriteXmm: for<'a> Arbitrary<'a>,
548{
549}
550
551#[cfg(test)]
552mod test {
553    use super::*;
554    use arbtest::arbtest;
555    use std::sync::atomic::{AtomicUsize, Ordering};
556
557    #[test]
558    fn smoke() {
559        let count = AtomicUsize::new(0);
560        arbtest(|u| {
561            let inst: Inst<FuzzRegs> = u.arbitrary()?;
562            roundtrip(&inst);
563            println!("#{}: {inst}", count.fetch_add(1, Ordering::SeqCst));
564            Ok(())
565        })
566        .budget_ms(1_000);
567
568        // This will run the `roundtrip` fuzzer for one second. To repeatably
569        // test a single input, append `.seed(0x<failing seed>)`.
570    }
571
572    #[test]
573    fn callq() {
574        for i in -500..500 {
575            println!("immediate: {i}");
576            let inst = crate::inst::callq_d::new(i);
577            roundtrip(&inst.into());
578        }
579    }
580
581    /// Same as [`smoke`], but exercises the Intel XED oracle. Only available
582    /// with the `fuzz-xed` feature.
583    ///
584    /// The instruction is printed in XED's dialect (`{inst:#}`) so that the two
585    /// can be compared directly. Run explicitly with
586    /// `cargo test --features fuzz-xed -- smoke_xed`.
587    #[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))]
588    #[test]
589    fn smoke_xed() {
590        let count = AtomicUsize::new(0);
591        arbtest(|u| {
592            let inst: Inst<FuzzRegs> = u.arbitrary()?;
593            roundtrip_xed(&inst);
594            println!("#{}: {inst}", count.fetch_add(1, Ordering::SeqCst));
595            Ok(())
596        })
597        .budget_ms(1_000);
598    }
599
600    /// Byte-level encoding check for the APX NDD (new-data-destination) form of
601    /// `ADD`, promoted into EVEX "map 4" via the extended-EVEX prefix.
602    ///
603    /// We assert the exact bytes rather than round-tripping through Capstone
604    /// because the bundled disassembler does not yet understand APX. With
605    /// `ND = 1` the architectural destination is the `vvvv`-encoded register, so
606    /// the `RVM` operands are `[ModRM.reg source, vvvv destination, ModRM.rm
607    /// source]`. For `addq %rax, %rcx, %rdx` (destination `%rax` = 0 in `vvvv`,
608    /// source `%rcx` = 1 in ModRM.reg, source `%rdx` = 2 in ModRM.rm), `W = 1`,
609    /// `ND = 1`, `NF = 0`, the expected sequence is:
610    ///
611    /// ```text
612    ///   62 F4 FC 18 01 CA
613    ///   ^^ ^^ ^^ ^^ ^^ ^^
614    ///   |  |  |  |  |  └ ModRM: mod=11 reg=rcx r/m=rdx
615    ///   |  |  |  |  └ opcode 0x01 (ADD r/m, reg)
616    ///   |  |  |  └ P2: ND=1 (bit4), V4=1 (bit3, inverted), NF=0
617    ///   |  |  └ P1: W=1, vvvv=~rax=1111, U=1, pp=00
618    ///   |  └ P0: R3 X3 B3 R4 = 1111, B4=0, map=100 (map 4)
619    ///   └ EVEX identifier
620    /// ```
621    #[test]
622    fn apx_addq_rvm_ndd_encoding() {
623        use crate::inst::addq_rvm;
624        // Format is `RVM` = [ModRM.reg source, vvvv destination, ModRM.rm
625        // source], so the constructor arguments are (reg source = %rcx,
626        // destination = %rax, r/m source = %rdx).
627        let inst = addq_rvm::<FuzzRegs>::new(FuzzReg::new(1), FuzzReg::new(0), FuzzReg::new(2));
628        let assembled = assemble(&inst.into());
629        assert_eq!(pretty_print_hexadecimal(&assembled), "62F4FC1801CA");
630    }
631
632    /// Companion to [`apx_addq_rvm_ndd_encoding`] covering a memory operand.
633    ///
634    /// APX map-4 instructions are legacy instructions promoted into EVEX, so
635    /// their `disp8` keeps legacy semantics: a plain byte offset. The
636    /// compressed-displacement scheme that divides `disp8` by a tuple-derived
637    /// factor `N` applies only to the vector EVEX encodings. Encoding
638    /// `0x50(%rdi)` must therefore emit `0x50` and not `0x50 / 16 = 0x05`,
639    /// which would silently address the wrong memory.
640    #[test]
641    fn apx_addq_rvm_ndd_disp8_is_unscaled() {
642        use crate::inst::addq_rvm;
643        use crate::mem::{Amode, AmodeOffset, AmodeOffsetPlusKnownOffset, GprMem};
644
645        let mem: GprMem<FuzzReg, FuzzReg> = GprMem::Mem(Amode::ImmReg {
646            base: FuzzReg::new(7),
647            simm32: AmodeOffsetPlusKnownOffset {
648                simm32: AmodeOffset::new(0x50),
649                offset: None,
650            },
651            trap: None,
652        });
653        let inst = addq_rvm::<FuzzRegs>::new(FuzzReg::new(7), FuzzReg::new(7), mem);
654        let assembled = assemble(&inst.into());
655        assert_eq!(pretty_print_hexadecimal(&assembled), "62F4C418017F50");
656    }
657}