passes: add mba scheme to Constants (and+or+add identity)
(a&b)+(a|b) = a+b, documented honestly: falls to the same symbolic
evaluator as xor/add/shift (every sub-expression is a foldable
LDC,LDC,BINOP triple), but defeats a pattern matcher hardcoded to one
opcode ("ldc,ldc,ixor = split constant"). Added to ConstantsConfig
default schemes.
Tests: mba_form_uses_and_or_add_not_a_single_binop, round-trip coverage
in int_forms_round_trip/long_forms_keep_shape.
This commit is contained in:
parent
550f62e473
commit
2b19ed6096
4 changed files with 133 additions and 10 deletions
|
|
@ -124,7 +124,8 @@ impl Default for FlowConfig {
|
|||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct ConstantsConfig {
|
||||
/// Splitting schemes (`xor`, `add`, `shift`), picked by RNG per constant.
|
||||
/// Splitting schemes (`xor`, `add`, `shift`, `mba`), picked by RNG per
|
||||
/// constant.
|
||||
pub schemes: Vec<String>,
|
||||
/// Insert opaque predicates (at most this many per method).
|
||||
pub max_predicates: usize,
|
||||
|
|
@ -135,7 +136,12 @@ pub struct ConstantsConfig {
|
|||
impl Default for ConstantsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
schemes: vec!["xor".to_string(), "add".to_string(), "shift".to_string()],
|
||||
schemes: vec![
|
||||
"xor".to_string(),
|
||||
"add".to_string(),
|
||||
"shift".to_string(),
|
||||
"mba".to_string(),
|
||||
],
|
||||
max_predicates: 1,
|
||||
skip: Vec::new(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
//! Constant-splitting schemes and opaque predicates.
|
||||
//!
|
||||
//! Integer constants outside the `iconst` range are replaced by triples
|
||||
//! (`x^k`, `a+b`, shift); an opaque predicate is an always-true condition with
|
||||
//! a one-`nop` dead branch (valid, never executed, breaks linear disassembly).
|
||||
//! (`x^k`, `a+b`, shift) or the `mba` form below; an opaque predicate is an
|
||||
//! always-true condition with a one-`nop` dead branch (valid, never
|
||||
//! executed, breaks linear disassembly).
|
||||
//!
|
||||
//! None of this survives a real constant-propagation pass (every operand is
|
||||
//! a literal, so a general evaluator folds the whole expression regardless
|
||||
//! of depth) - that is a known, documented limit, not a claim otherwise.
|
||||
//! `mba` raises the bar against a *pattern* matcher instead: `xor`/`add`
|
||||
//! hardcode one opcode triple as "this is a split constant", `mba` needs the
|
||||
//! `(a&b)+(a|b) = a+b` identity specifically.
|
||||
|
||||
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, Prim};
|
||||
use noxis_core::Rng;
|
||||
|
|
@ -13,6 +21,7 @@ pub(super) enum Scheme {
|
|||
Xor,
|
||||
Add,
|
||||
Shift,
|
||||
Mba,
|
||||
}
|
||||
|
||||
impl Scheme {
|
||||
|
|
@ -21,6 +30,7 @@ impl Scheme {
|
|||
"xor" => Some(Scheme::Xor),
|
||||
"add" => Some(Scheme::Add),
|
||||
"shift" => Some(Scheme::Shift),
|
||||
"mba" => Some(Scheme::Mba),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +46,7 @@ pub(super) fn encrypt_int(value: i32, scheme: Scheme, rng: &mut Rng) -> Vec<Insn
|
|||
}
|
||||
(value ^ key, key)
|
||||
}
|
||||
Scheme::Add => {
|
||||
Scheme::Add | Scheme::Mba => {
|
||||
let part = rng.next_u64() as i32;
|
||||
(part, value.wrapping_sub(part))
|
||||
}
|
||||
|
|
@ -46,6 +56,7 @@ pub(super) fn encrypt_int(value: i32, scheme: Scheme, rng: &mut Rng) -> Vec<Insn
|
|||
Scheme::Xor => binary(first, second, Arith::Xor, Prim::Int),
|
||||
Scheme::Add => binary(first, second, Arith::Add, Prim::Int),
|
||||
Scheme::Shift => shift_form(value),
|
||||
Scheme::Mba => mba_form(first, second),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +70,7 @@ pub(super) fn encrypt_long(value: i64, scheme: Scheme, rng: &mut Rng) -> Vec<Ins
|
|||
}
|
||||
(value ^ key, key)
|
||||
}
|
||||
Scheme::Add => {
|
||||
Scheme::Add | Scheme::Mba => {
|
||||
let part = rng.next_u64() as i64;
|
||||
(part, value.wrapping_sub(part))
|
||||
}
|
||||
|
|
@ -69,6 +80,7 @@ pub(super) fn encrypt_long(value: i64, scheme: Scheme, rng: &mut Rng) -> Vec<Ins
|
|||
Scheme::Xor => binary_long(first, second, Arith::Xor),
|
||||
Scheme::Add => binary_long(first, second, Arith::Add),
|
||||
Scheme::Shift => shift_form_long(value),
|
||||
Scheme::Mba => mba_form_long(first, second),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +104,53 @@ fn binary_long(first: i64, second: i64, op: Arith) -> Vec<Insn> {
|
|||
]
|
||||
}
|
||||
|
||||
/// MBA identity: `a+b == (a&b) + (a|b)` (exact, bit-exact under wrapping).
|
||||
/// `a`/`b` are pushed twice (no stack juggling needed) so each sub-expression
|
||||
/// stays a plain, correct `and`/`or` - the shape, not a single opcode, is
|
||||
/// what a matcher has to recognize.
|
||||
fn mba_form(a: i32, b: i32) -> Vec<Insn> {
|
||||
vec![
|
||||
Insn::Const(Const::Int(a)),
|
||||
Insn::Const(Const::Int(b)),
|
||||
Insn::Binary {
|
||||
prim: Prim::Int,
|
||||
op: Arith::And,
|
||||
},
|
||||
Insn::Const(Const::Int(a)),
|
||||
Insn::Const(Const::Int(b)),
|
||||
Insn::Binary {
|
||||
prim: Prim::Int,
|
||||
op: Arith::Or,
|
||||
},
|
||||
Insn::Binary {
|
||||
prim: Prim::Int,
|
||||
op: Arith::Add,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// The same for `long`.
|
||||
fn mba_form_long(a: i64, b: i64) -> Vec<Insn> {
|
||||
vec![
|
||||
Insn::Const(Const::Long(a)),
|
||||
Insn::Const(Const::Long(b)),
|
||||
Insn::Binary {
|
||||
prim: Prim::Long,
|
||||
op: Arith::And,
|
||||
},
|
||||
Insn::Const(Const::Long(a)),
|
||||
Insn::Const(Const::Long(b)),
|
||||
Insn::Binary {
|
||||
prim: Prim::Long,
|
||||
op: Arith::Or,
|
||||
},
|
||||
Insn::Binary {
|
||||
prim: Prim::Long,
|
||||
op: Arith::Add,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// `v = ((v >> 1) << 1) | (v & 1)` - works for any value, including negatives.
|
||||
fn shift_form(value: i32) -> Vec<Insn> {
|
||||
vec![
|
||||
|
|
|
|||
|
|
@ -30,6 +30,30 @@ fn eval_int(insns: &[Insn]) -> i32 {
|
|||
Insn::Const(Const::Int(b)),
|
||||
Insn::Binary { .. },
|
||||
] => (a << 1) | (b & 1),
|
||||
// MBA: `(a&b) + (a|b) == a+b`. Each sub-expression is still a
|
||||
// foldable `LDC,LDC,BINOP` triple - a generic constant evaluator
|
||||
// still resolves this instantly. What it defeats is a *pattern*
|
||||
// matcher hardcoded to one opcode (`ldc,ldc,ixor` = "this is a
|
||||
// split constant"): it would need to recognize the `and+or+add`
|
||||
// identity specifically, not just fold a literal pair.
|
||||
[
|
||||
Insn::Const(Const::Int(a1)),
|
||||
Insn::Const(Const::Int(b1)),
|
||||
Insn::Binary {
|
||||
op: noxis_classfile::Arith::And,
|
||||
..
|
||||
},
|
||||
Insn::Const(Const::Int(a2)),
|
||||
Insn::Const(Const::Int(b2)),
|
||||
Insn::Binary {
|
||||
op: noxis_classfile::Arith::Or,
|
||||
..
|
||||
},
|
||||
Insn::Binary {
|
||||
op: noxis_classfile::Arith::Add,
|
||||
..
|
||||
},
|
||||
] if a1 == a2 && b1 == b2 => (a1 & b1).wrapping_add(a1 | b1),
|
||||
_ => panic!("not a form: {insns:?}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +62,7 @@ fn eval_int(insns: &[Insn]) -> i32 {
|
|||
fn int_forms_round_trip() {
|
||||
let mut rng = noxis_core::Rng::from_seed(11);
|
||||
for value in [-1000, -7, -6, 6, 7, 42, 65536, i32::MAX, i32::MIN] {
|
||||
for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift] {
|
||||
for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift, Scheme::Mba] {
|
||||
assert_eq!(eval_int(&encrypt_int(value, scheme, &mut rng)), value);
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +71,7 @@ fn int_forms_round_trip() {
|
|||
#[test]
|
||||
fn long_forms_keep_shape() {
|
||||
let mut rng = noxis_core::Rng::from_seed(12);
|
||||
for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift] {
|
||||
for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift, Scheme::Mba] {
|
||||
let insns = encrypt_long(1_000_000_000_000, scheme, &mut rng);
|
||||
assert!(matches!(
|
||||
insns.last(),
|
||||
|
|
@ -59,6 +83,34 @@ fn long_forms_keep_shape() {
|
|||
}
|
||||
}
|
||||
|
||||
/// MBA shape: `and`/`or`/`add` on two reused operands, not `xor`/`add` on a
|
||||
/// single pair - a matcher hardcoded to one opcode triple (`ldc,ldc,ixor`)
|
||||
/// doesn't recognize this as "a split constant" without the identity.
|
||||
#[test]
|
||||
fn mba_form_uses_and_or_add_not_a_single_binop() {
|
||||
let mut rng = noxis_core::Rng::from_seed(13);
|
||||
for value in [-1000, -6, 6, 42, 65536, i32::MAX, i32::MIN] {
|
||||
let insns = encrypt_int(value, Scheme::Mba, &mut rng);
|
||||
let ops: Vec<_> = insns
|
||||
.iter()
|
||||
.filter_map(|i| match i {
|
||||
Insn::Binary { op, .. } => Some(*op),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
ops,
|
||||
[
|
||||
noxis_classfile::Arith::And,
|
||||
noxis_classfile::Arith::Or,
|
||||
noxis_classfile::Arith::Add,
|
||||
],
|
||||
"unexpected mba shape: {insns:?}"
|
||||
);
|
||||
assert_eq!(insns.len(), 7, "unexpected mba insn count: {insns:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn sample() -> Program {
|
||||
let code = Code {
|
||||
max_stack: 1,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue