From d58bfcb1ba2ceee1abe368ba81d31240e711d215 Mon Sep 17 00:00:00 2001 From: iximeow Date: Sun, 1 May 2022 13:53:04 -0700 Subject: add testing setup for field descriptions --- Cargo.toml | 2 +- test/long_mode/descriptions.rs | 335 +++++++++++++++++++++++++++++++++++++++++ test/long_mode/mod.rs | 2 + 3 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 test/long_mode/descriptions.rs diff --git a/Cargo.toml b/Cargo.toml index a440d12..5de6157 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ lto = true default = ["std", "colors", "use-serde", "fmt"] # opt-in for some apis that are really much nicer with String -std = [] +std = ["yaxpeax-arch/std"] # feature for formatting instructions and their components fmt = [] diff --git a/test/long_mode/descriptions.rs b/test/long_mode/descriptions.rs new file mode 100644 index 0000000..fc53acb --- /dev/null +++ b/test/long_mode/descriptions.rs @@ -0,0 +1,335 @@ +use std::fmt::Write; + +use yaxpeax_arch::{AddressBase, LengthedInstruction}; +use yaxpeax_arch::annotation::FieldDescription; +use yaxpeax_x86::long_mode::Opcode; +use yaxpeax_x86::long_mode::RegSpec; +use yaxpeax_x86::long_mode::InstDecoder; +use yaxpeax_x86::long_mode::Instruction; +use yaxpeax_x86::long_mode::InnerDescription; +use yaxpeax_arch::annotation::AnnotatingDecoder; + +fn test_annotations(data: &[u8], expected: &'static str, checks: &[AnnotationCheck]) { + test_annotations_under(&InstDecoder::default(), data, expected, checks); +} + +// pair up field descriptions and the check that matched them. we'll use this for +// reporting errors if checks don't match up. +#[derive(PartialEq, Eq, Copy, Clone)] +enum CheckResult { + Matched, + Failed, + Ignored, +} + +impl CheckResult { + fn consumed_check(&self) -> bool { + *self != CheckResult::Ignored + } +} + +struct MatchResult { + check: AnnotationCheck, + result: CheckResult, +} + +#[derive(Clone)] +enum AnnotationCheck { + // does not match any description; intended to assert that there should be no extra annotations + // after the last check. + NoExtra, + Exact { + // check the reported annotation matches this description + desc: InnerDescription, + start_bit: u32, + end_bit: u32, + }, + Approximate { + check: fn(&InnerDescription) -> bool, + start_bit: u32, + end_bit: u32, + } +} + +impl std::fmt::Display for AnnotationCheck { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + AnnotationCheck::NoExtra => { + write!(f, "no further field descriptions expected") + } + AnnotationCheck::Exact { desc, start_bit, end_bit } => { + write!(f, "bit {}:{}; {}", start_bit, end_bit, desc) + } + AnnotationCheck::Approximate { + start_bit, end_bit, .. + } => { + write!(f, "bit {}:{}; (fn-based match)", start_bit, end_bit) + } + } + } +} + +impl AnnotationCheck { + fn exact(start: u32, end: u32, desc: InnerDescription) -> AnnotationCheck { + AnnotationCheck::Exact { + desc, + start_bit: start, + end_bit: end, + } + } + + fn approximate(start: u32, end: u32, check: fn(&InnerDescription) -> bool) -> AnnotationCheck { + AnnotationCheck::Approximate { + check, + start_bit: start, + end_bit: end, + } + } + + fn no_extra() -> AnnotationCheck { + AnnotationCheck::NoExtra + } + + fn matches(&self, actual_start: u32, actual_end: u32, actual_desc: InnerDescription) -> CheckResult { + match self { + AnnotationCheck::NoExtra => { + CheckResult::Failed + }, + AnnotationCheck::Exact { start_bit, end_bit, desc } => { + let bits_match = *start_bit == actual_start && *end_bit == actual_end; + let desc_match = desc == &actual_desc; + let fail_anyway = match (desc, &actual_desc) { + // expect that there's only one `Number` field with a given name, so if the + // bits are wrong or the value is wrong, that's a guaranteed fail. + (InnerDescription::Number(expected_name, _), InnerDescription::Number(actual_name, _)) => { + if expected_name == actual_name { + true + } else { + false + } + } + // expect that there's only one opcode field. there won't be a second one that + // we might match on later. + (InnerDescription::Opcode(_), InnerDescription::Opcode(_)) => { + true + } + (_expected, _actual) => false + }; + + if (!bits_match && !desc_match) && !fail_anyway { + return CheckResult::Ignored; + } + + if bits_match && desc_match { + CheckResult::Matched + } else { + CheckResult::Failed + } + }, + AnnotationCheck::Approximate { start_bit, end_bit, check } => { + let bits_match = *start_bit == actual_start && *end_bit == actual_end; + let desc_match = check(&actual_desc); + + if !bits_match && !desc_match { + return CheckResult::Ignored; + } + + if bits_match && desc_match { + CheckResult::Matched + } else { + CheckResult::Failed + } + } + } + } +} + +impl std::fmt::Display for CheckResult { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let s = match self { + CheckResult::Matched => "\x1b[32mmatched\x1b[0m ", + CheckResult::Failed => "\x1b[31mfailed\x1b[0m ", + CheckResult::Ignored => "\x1b[33mignored\x1b[0m", + }; + f.write_str(s) + } +} + +fn test_annotations_under(decoder: &InstDecoder, data: &[u8], expected: &'static str, checks: &[AnnotationCheck]) { + let mut hex = String::new(); + for b in data { + write!(hex, "{:02x}", b).unwrap(); + } + let mut reader = yaxpeax_arch::U8Reader::new(data); + let mut sink = yaxpeax_arch::annotation::VecSink::new(); + let mut inst = Instruction::default(); + match decoder.decode_with_annotation(&mut inst, &mut reader, &mut sink) { + Ok(()) => { + let text = format!("{}", inst); + assert!( + text == expected, + "display error for {}:\n decoded: {:?} under decoder {}\n displayed: {}\n expected: {}\n", + hex, + inst, + decoder, + text, + expected + ); + + let mut matches: Vec<((u32, u32, InnerDescription), Option)> = Vec::new(); + let mut extra_checks: Vec = Vec::new(); + + sink.records.sort_by_key(|x| x.2.id()); + let mut rec_iter = sink.records.iter(); + let mut check_iter = checks.iter(); + + let mut check = check_iter.next(); + + let mut failed = false; + + while let Some((bit_start, bit_end, desc)) = rec_iter.next().cloned() { + if let Some(curr_check) = check { + if let AnnotationCheck::NoExtra = curr_check { + failed = true; + } + + let check_result = curr_check.matches(bit_start, bit_end, desc.desc().clone()); + if check_result == CheckResult::Failed { + failed = true; + } + + matches.push(((bit_start, bit_end, desc.desc().clone()), Some(MatchResult { + check: curr_check.clone(), + result: check_result + }))); + + if check_result.consumed_check() { + check = check_iter.next(); + } + } else { + // no more checks, so we'll have passed the test at least. continue scooping up + // field descriptions into `matches` with no checks. + matches.push(((bit_start, bit_end, desc.desc().clone()), None)); + } + } + + while let Some(missed_check) = check { + check = check_iter.next(); + if let AnnotationCheck::NoExtra = missed_check { + // "no extra" will be "missed" in that nothing matches it above. in the success + // case, it's a leftover check, and should be the only one remaining if the + // test is written correctly. so skip it here, and see if we've exhausted the + // list of checks.. + continue; + } + extra_checks.push(missed_check.clone()); + } + + if extra_checks.len() > 0 { + failed = true; + } + + if failed { + eprintln!("[!] annotation check for {}, `{}`, failed:", hex, inst); + for ((bit_start, bit_end, desc), check) in matches { + let mut desc = format!("bit {}:{}; {}", bit_start, bit_end, desc); + while desc.len() < 60 { + desc.push(' '); + } + desc.push(' '); + let comment = match check { + None => { + "\x1b[34mno check\x1b[0m".to_owned() + } + Some(MatchResult { + result, + check + }) => { + if result == CheckResult::Ignored { + result.to_string() + } else { + format!("{}{}", result, check) + } + } + }; + eprintln!(" - {}{}", desc, comment); + } + for check in extra_checks { + eprintln!(" ! \x1b[31mextra check\x1b[0m: {}", check); + } + } + assert!(!failed); + + // while we're at it, test that the instruction is as long, and no longer, than its + // input + assert_eq!((0u64.wrapping_offset(inst.len()).to_linear()) as usize, data.len(), "instruction length is incorrect, wanted instruction {}", expected); + }, + Err(e) => { + cfg_if::cfg_if! { + if #[cfg(feature="fmt")] { + assert!(false, "decode error ({}) for {} under decoder {}:\n expected: {}\n", e, hex, decoder, expected); + } else { + // avoid the unused `e` warning + let _ = e; + assert!(false, "decode error () for {} under decoder :\n expected: {}\n", hex, expected); + } + } + } + } +} + +#[test] +fn test_modrm_decode() { + test_annotations(&[0xff, 0xc0], "inc eax", &[ + AnnotationCheck::exact(11, 13, InnerDescription::Opcode(Opcode::INC)), + AnnotationCheck::approximate(0, 7, |desc| { desc.to_string().contains("ModRM_0xff_Ev") }), + AnnotationCheck::approximate(14, 15, |desc| { + desc.to_string().contains("mmm") && + desc.to_string().contains("register number") && + desc.to_string().contains("mod bits: 11") + }), +// AnnotationCheck::exact(8, 10, InnerDescription::RegisterNumber("mmm", 0, RegSpec::eax())), +// AnnotationCheck::no_extra(), + ]); + test_annotations(&[0xc1, 0xe0, 0x03], "shl eax, 0x3", &[ + AnnotationCheck::exact(11, 13, InnerDescription::Opcode(Opcode::SHL)), + AnnotationCheck::exact(16, 23, InnerDescription::Number("imm", 3)), + AnnotationCheck::approximate(0, 7, |desc| { desc.to_string().contains("ModRM_0xc1_Ev_Ib") }), + AnnotationCheck::approximate(14, 15, |desc| { + desc.to_string().contains("mmm") && + desc.to_string().contains("register number") && + desc.to_string().contains("mod bits: 11") + }), + AnnotationCheck::exact(8, 10, InnerDescription::RegisterNumber("mmm", 0, RegSpec::eax())), + AnnotationCheck::no_extra(), + ]); + // just modrm + test_annotations(&[0x33, 0x08], "xor ecx, dword [rax]", &[ + AnnotationCheck::exact(0, 7, InnerDescription::Opcode(Opcode::XOR)), + AnnotationCheck::approximate(0, 7, |desc| { desc.to_string() == "operand code `Gv_Ev`" }), + AnnotationCheck::approximate(7, 7, |desc| { desc.to_string().contains("operands begin") }), + AnnotationCheck::approximate(14, 15, |desc| { + desc.to_string().contains("memory operand is [reg]") && + desc.to_string().contains("mod bits: 00") + }), + AnnotationCheck::exact(11, 13, InnerDescription::RegisterNumber("rrr", 1, RegSpec::ecx())), + AnnotationCheck::exact(8, 10, InnerDescription::RegisterNumber("mmm", 0, RegSpec::rax())), + AnnotationCheck::no_extra(), + ]); + + // modrm + rex.w + test_annotations(&[0x48, 0x33, 0x08], "xor rcx, qword [rax]", &[]); + test_annotations(&[0x48, 0x33, 0x20], "xor rsp, qword [rax]", &[]); + test_annotations(&[0x48, 0x33, 0x05, 0x78, 0x56, 0x34, 0x12], "xor rax, qword [rip + 0x12345678]", &[]); + + // specifically sib with base == 0b101 + // mod bits 00 + test_annotations(&[0x42, 0x33, 0x34, 0x25, 0x20, 0x30, 0x40, 0x50], "xor esi, dword [r12 * 1 + 0x50403020]", &[]); + test_annotations(&[0x43, 0x33, 0x34, 0x25, 0x20, 0x30, 0x40, 0x50], "xor esi, dword [r12 * 1 + 0x50403020]", &[]); + // mod bits 01 + test_annotations(&[0x42, 0x33, 0x74, 0x25, 0x20], "xor esi, dword [rbp + r12 * 1 + 0x20]", &[]); + test_annotations(&[0x43, 0x33, 0x74, 0x25, 0x20], "xor esi, dword [r13 + r12 * 1 + 0x20]", &[]); + // mod bits 10 + test_annotations(&[0x42, 0x33, 0xb4, 0x25, 0x20, 0x30, 0x40, 0x50], "xor esi, dword [rbp + r12 * 1 + 0x50403020]", &[]); + test_annotations(&[0x43, 0x33, 0xb4, 0x25, 0x20, 0x30, 0x40, 0x50], "xor esi, dword [r13 + r12 * 1 + 0x50403020]", &[]); +} diff --git a/test/long_mode/mod.rs b/test/long_mode/mod.rs index 2a11b79..eeee155 100644 --- a/test/long_mode/mod.rs +++ b/test/long_mode/mod.rs @@ -4,6 +4,8 @@ mod regspec; mod operand; #[cfg(feature="fmt")] mod display; +#[cfg(feature="std")] +mod descriptions; mod evex_generated; mod reuse_test; -- cgit v1.1