aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoriximeow <me@iximeow.net>2022-05-01 13:53:04 -0700
committeriximeow <me@iximeow.net>2022-05-01 13:54:39 -0700
commitd58bfcb1ba2ceee1abe368ba81d31240e711d215 (patch)
tree9dd13b80012c31656ef2e15e0eb3ee22d69b9e59
parent073d3b367ee4164fc5c89c4a7869770f0261a00c (diff)
add testing setup for field descriptions
-rw-r--r--Cargo.toml2
-rw-r--r--test/long_mode/descriptions.rs335
-rw-r--r--test/long_mode/mod.rs2
3 files changed, 338 insertions, 1 deletions
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<MatchResult>)> = Vec::new();
+ let mut extra_checks: Vec<AnnotationCheck> = 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 (<non-fmt build>) for {} under decoder <non-fmt build>:\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;