frames: JDK hierarchy fallback, array-operand rewrite, aaload fixes; LoVisual 131 to 0 FAIL
This commit is contained in:
parent
1c417f596c
commit
63630dd223
14 changed files with 562 additions and 116 deletions
34
crates/noxis-analysis/src/JdkHierarchy.java
Normal file
34
crates/noxis-analysis/src/JdkHierarchy.java
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Answers superclass queries from the obfuscator's frame analysis.
|
||||||
|
*
|
||||||
|
* <p>Protocol: one internal class name per line on stdin
|
||||||
|
* ({@code java/io/BufferedReader}); one line back on stdout - the direct
|
||||||
|
* superclass as an internal name, or an empty line when unknown
|
||||||
|
* (missing dependency, interface, primitive, array). A single process
|
||||||
|
* serves the whole analysis run; EOF on stdin ends it.
|
||||||
|
*/
|
||||||
|
public class JdkHierarchy {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Scanner in = new Scanner(System.in);
|
||||||
|
while (in.hasNextLine()) {
|
||||||
|
String name = in.nextLine().trim().replace('/', '.');
|
||||||
|
String sup = "";
|
||||||
|
if (!name.isEmpty()) {
|
||||||
|
try {
|
||||||
|
Class<?> loaded =
|
||||||
|
Class.forName(name, false, JdkHierarchy.class.getClassLoader());
|
||||||
|
Class<?> parent = loaded.getSuperclass();
|
||||||
|
if (parent != null) {
|
||||||
|
sup = parent.getName().replace('.', '/');
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// Unknown: not on the JDK classpath, an interface with
|
||||||
|
// no superclass, a primitive or an array - empty line.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println(sup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
195
crates/noxis-analysis/src/jdk.rs
Normal file
195
crates/noxis-analysis/src/jdk.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
//! JDK superclass fallback for frame merging.
|
||||||
|
//!
|
||||||
|
//! [`ClassHierarchy`] only knows program classes - deliberately so: rename
|
||||||
|
//! safety must stay conservative outside the program. Frame merging has the
|
||||||
|
//! opposite need: joins over JDK types (`Reader`/`BufferedReader`, a
|
||||||
|
//! multi-catch over `NoSuchMethodException`/`SecurityException`) degrade to
|
||||||
|
//! `Object` without the true superclass chain, and the verifier rejects the
|
||||||
|
//! poisoned frame at the first typed use (`putfield Reader`, a `Throwable`
|
||||||
|
//! parameter). This wrapper answers program classes from the built
|
||||||
|
//! hierarchy and asks the running JDK about the rest: one `java` process
|
||||||
|
//! (single-file launch, no `javac` needed), a line protocol, answers cached.
|
||||||
|
//! Without `java` every answer is `None` - exactly the old behaviour.
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::{BufRead, BufReader, BufWriter, Write};
|
||||||
|
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
|
||||||
|
|
||||||
|
use noxis_classfile::Hierarchy;
|
||||||
|
|
||||||
|
use super::hierarchy::ClassHierarchy;
|
||||||
|
|
||||||
|
const HELPER: &str = include_str!("JdkHierarchy.java");
|
||||||
|
|
||||||
|
/// Frame-time hierarchy: program first, running JDK second.
|
||||||
|
pub struct JdkHierarchy<'a> {
|
||||||
|
inner: &'a ClassHierarchy,
|
||||||
|
shared: RefCell<Shared>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Shared {
|
||||||
|
cache: HashMap<String, Option<String>>,
|
||||||
|
child: Option<Pipes>,
|
||||||
|
dead: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Pipes {
|
||||||
|
rx: BufReader<ChildStdout>,
|
||||||
|
tx: BufWriter<ChildStdin>,
|
||||||
|
proc: Child,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> JdkHierarchy<'a> {
|
||||||
|
/// Wrap a built program hierarchy (spawns nothing yet).
|
||||||
|
pub fn wrap(inner: &'a ClassHierarchy) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
shared: RefCell::new(Shared::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn jdk_superclass(&self, name: &str) -> Option<String> {
|
||||||
|
let mut shared = self.shared.borrow_mut();
|
||||||
|
if let Some(cached) = shared.cache.get(name) {
|
||||||
|
return cached.clone();
|
||||||
|
}
|
||||||
|
if shared.dead {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if shared.child.is_none() {
|
||||||
|
match spawn_helper() {
|
||||||
|
Some(pipes) => shared.child = Some(pipes),
|
||||||
|
None => {
|
||||||
|
shared.dead = true;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let answer = match query(shared.child.as_mut(), name) {
|
||||||
|
// Transport broke: stop spawning, keep the old behaviour.
|
||||||
|
None => {
|
||||||
|
shared.dead = true;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(answer) => answer,
|
||||||
|
};
|
||||||
|
shared.cache.insert(name.to_string(), answer.clone());
|
||||||
|
answer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hierarchy for JdkHierarchy<'_> {
|
||||||
|
fn superclass(&self, class: &str) -> Option<String> {
|
||||||
|
match self.inner.superclass(class) {
|
||||||
|
Some(known) => Some(known),
|
||||||
|
None => self.jdk_superclass(class),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for JdkHierarchy<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Ok(mut shared) = self.shared.try_borrow_mut()
|
||||||
|
&& let Some(mut pipes) = shared.child.take()
|
||||||
|
{
|
||||||
|
let _ = pipes.proc.kill();
|
||||||
|
let _ = pipes.proc.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a `java` runtime exists for the helper.
|
||||||
|
fn java_available() -> bool {
|
||||||
|
Command::new("java")
|
||||||
|
.arg("-version")
|
||||||
|
.output()
|
||||||
|
.is_ok_and(|out| out.status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the helper next to itself atomically-ish (every writer emits
|
||||||
|
/// identical bytes) and launch it; a handshake query proves it runs.
|
||||||
|
fn spawn_helper() -> Option<Pipes> {
|
||||||
|
if !java_available() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let path = std::env::temp_dir().join("noxis-jdk-hierarchy.java");
|
||||||
|
let staging = path.with_extension(format!("java.{}", std::process::id()));
|
||||||
|
std::fs::write(&staging, HELPER).ok()?;
|
||||||
|
std::fs::rename(&staging, &path).ok()?;
|
||||||
|
let mut proc = Command::new("java")
|
||||||
|
.arg(&path)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.ok()?;
|
||||||
|
let rx = BufReader::new(proc.stdout.take()?);
|
||||||
|
let tx = BufWriter::new(proc.stdin.take()?);
|
||||||
|
let mut pipes = Pipes { rx, tx, proc };
|
||||||
|
// Handshake: `Object` has no superclass; any answer at all means a wrong
|
||||||
|
// runtime (or no source-launch support) - stay silent instead.
|
||||||
|
match query(Some(&mut pipes), "java/lang/Object") {
|
||||||
|
// The child must still be alive: EOF also reads as an empty line.
|
||||||
|
Some(None) if pipes.proc.try_wait().is_ok_and(|exit| exit.is_none()) => Some(pipes),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One round trip: `None` - dead transport (child reaped);
|
||||||
|
/// `Some(answer)` - the reply, empty string for unknown superclasses.
|
||||||
|
fn query(pipes: Option<&mut Pipes>, name: &str) -> Option<Option<String>> {
|
||||||
|
let pipes = pipes?;
|
||||||
|
if writeln!(pipes.tx, "{name}").is_err() || pipes.tx.flush().is_err() {
|
||||||
|
kill(pipes);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut line = String::new();
|
||||||
|
if pipes.rx.read_line(&mut line).is_err() {
|
||||||
|
kill(pipes);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return Some(None);
|
||||||
|
}
|
||||||
|
Some(Some(trimmed.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kill(pipes: &mut Pipes) {
|
||||||
|
let _ = pipes.proc.kill();
|
||||||
|
let _ = pipes.proc.wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use noxis_classfile::Program;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn jdk_superclasses_come_from_the_runtime() {
|
||||||
|
if !java_available() {
|
||||||
|
eprintln!("SKIP: no java");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let program = Program::default();
|
||||||
|
let base = ClassHierarchy::build(&program);
|
||||||
|
let jdk = JdkHierarchy::wrap(&base);
|
||||||
|
// Real, stable JDK facts (unchanged for decades).
|
||||||
|
assert_eq!(
|
||||||
|
jdk.superclass("java/io/BufferedReader"),
|
||||||
|
Some("java/io/Reader".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
jdk.superclass("java/lang/NoSuchMethodException"),
|
||||||
|
Some("java/lang/ReflectiveOperationException".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(jdk.superclass("java/lang/Object"), None);
|
||||||
|
// Outside the JDK: honest miss, like before.
|
||||||
|
assert_eq!(jdk.superclass("demo/Nope"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,8 +10,10 @@
|
||||||
|
|
||||||
pub mod entrypoints;
|
pub mod entrypoints;
|
||||||
pub mod hierarchy;
|
pub mod hierarchy;
|
||||||
|
pub mod jdk;
|
||||||
pub mod keep;
|
pub mod keep;
|
||||||
|
|
||||||
pub use entrypoints::Entrypoints;
|
pub use entrypoints::Entrypoints;
|
||||||
pub use hierarchy::{ClassHierarchy, RenameSafety, method_rename_safety};
|
pub use hierarchy::{ClassHierarchy, RenameSafety, method_rename_safety};
|
||||||
|
pub use jdk::JdkHierarchy;
|
||||||
pub use keep::{Analysis, KeepRules, RuleError};
|
pub use keep::{Analysis, KeepRules, RuleError};
|
||||||
|
|
|
||||||
|
|
@ -129,11 +129,22 @@ pub(crate) fn method_slots(descriptor: &str) -> Result<MethodSlots, DescError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Element type from an array descriptor on the stack.
|
/// Element type from an array descriptor on the stack.
|
||||||
|
///
|
||||||
|
/// A `Null` arrayref throws (`NullPointerException`) - no value flows from
|
||||||
|
/// it, so `Null` (bottom) is the precise answer for reference loads.
|
||||||
|
/// Yielding `Object` instead poisons downstream joins permanently: the
|
||||||
|
/// poison arrives through transient worklist states (before upstream slots
|
||||||
|
/// refine) and `lub(Object, X)` can never narrow back. Primitive loads stay
|
||||||
|
/// exact: the opcode determines the result regardless of the arrayref.
|
||||||
pub(crate) fn array_element(
|
pub(crate) fn array_element(
|
||||||
array: &VerificationType,
|
array: &VerificationType,
|
||||||
kind: crate::insn::ArrayKind,
|
kind: crate::insn::ArrayKind,
|
||||||
) -> VerificationType {
|
) -> VerificationType {
|
||||||
|
use crate::format::VerificationType::Null;
|
||||||
use crate::insn::ArrayKind::{Boolean, Byte, Char, Double, Float, Int, Long, Reference, Short};
|
use crate::insn::ArrayKind::{Boolean, Byte, Char, Double, Float, Int, Long, Reference, Short};
|
||||||
|
if *array == Null && kind == Reference {
|
||||||
|
return Null;
|
||||||
|
}
|
||||||
if let VerificationType::Object(descriptor) = array
|
if let VerificationType::Object(descriptor) = array
|
||||||
&& let Some((dims, elem)) = split_array(descriptor)
|
&& let Some((dims, elem)) = split_array(descriptor)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
155
crates/noxis-classfile/src/format/frames/tests/joins/arrays.rs
Normal file
155
crates/noxis-classfile/src/format/frames/tests/joins/arrays.rs
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
//! Join-shape tests: loop heads, wide array loads, nested construction.
|
||||||
|
|
||||||
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use super::super::super::{StackMapFrame, VerificationType, compute_frames};
|
||||||
|
use super::super::{NoHierarchy, code_method, load_ref, store_ref};
|
||||||
|
use crate::insn::{ArrayKind, BranchCond, Cond, Const, Insn};
|
||||||
|
#[test]
|
||||||
|
fn anewarray_of_array_keeps_descriptor() {
|
||||||
|
let method = code_method(
|
||||||
|
"make",
|
||||||
|
"()Ljava/lang/Object;",
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
vec![
|
||||||
|
Insn::Mark(0),
|
||||||
|
Insn::Const(Const::Int(3)),
|
||||||
|
Insn::ANewArray("[B".to_string()),
|
||||||
|
store_ref(0),
|
||||||
|
load_ref(0),
|
||||||
|
Insn::Branch(BranchCond::Null { is_null: false }, 1),
|
||||||
|
Insn::AconstNull,
|
||||||
|
store_ref(0),
|
||||||
|
Insn::Goto(2),
|
||||||
|
Insn::Mark(1),
|
||||||
|
Insn::Mark(2),
|
||||||
|
load_ref(0),
|
||||||
|
Insn::AReturn,
|
||||||
|
],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
let frames = compute_frames("demo/Make", &method, &NoHierarchy).expect("frames");
|
||||||
|
assert_eq!(
|
||||||
|
frames,
|
||||||
|
vec![StackMapFrame::Append {
|
||||||
|
offset_delta: 8,
|
||||||
|
locals: vec![VerificationType::Object("[[B".to_string())],
|
||||||
|
},]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A loop head keeps `Top` for a slot written only inside the loop.
|
||||||
|
///
|
||||||
|
/// Inflows: entry (never wrote it) and back edge (wrote `Integer`). Like
|
||||||
|
/// `javac`, the join declares `Top`: a precise type would reject the inflow
|
||||||
|
/// that never wrote the slot (`Type top is not assignable`).
|
||||||
|
/// `aaload` on a one-dimensional reference array yields the element type.
|
||||||
|
///
|
||||||
|
/// Real-world shape (`V8ValueObject.bind` family): one join inflow carries
|
||||||
|
/// the loaded element (`Method`), the other the array itself (`Method[]`).
|
||||||
|
/// The join must declare their LUB (`Object`); declaring the array rejects
|
||||||
|
/// the element inflow (`Type ... is not assignable to ...`).
|
||||||
|
#[test]
|
||||||
|
fn aaload_on_ref_array_yields_element() {
|
||||||
|
let method = code_method(
|
||||||
|
"pick",
|
||||||
|
"([Ljava/lang/reflect/Method;)Ljava/lang/Object;",
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
vec![
|
||||||
|
Insn::Mark(0),
|
||||||
|
Insn::Const(Const::Int(0)),
|
||||||
|
Insn::Branch(BranchCond::Zero(Cond::Eq), 1),
|
||||||
|
load_ref(0),
|
||||||
|
Insn::Const(Const::Int(0)),
|
||||||
|
Insn::ArrayLoad(ArrayKind::Reference),
|
||||||
|
store_ref(1),
|
||||||
|
Insn::Goto(2),
|
||||||
|
Insn::Mark(1),
|
||||||
|
load_ref(0),
|
||||||
|
store_ref(1),
|
||||||
|
Insn::Mark(2),
|
||||||
|
load_ref(1),
|
||||||
|
Insn::AReturn,
|
||||||
|
],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
let frames = compute_frames("demo/Pick", &method, &NoHierarchy).expect("frames");
|
||||||
|
// The join declares locals[1]; whatever the frame kind, it must be the
|
||||||
|
// LUB (`Object`), never the array the other inflow carried.
|
||||||
|
let mut saw_join = false;
|
||||||
|
for frame in &frames {
|
||||||
|
let locals: Vec<VerificationType> = match frame {
|
||||||
|
StackMapFrame::SameLocals { local, .. } => vec![local.clone()],
|
||||||
|
StackMapFrame::Append { locals, .. } | StackMapFrame::Full { locals, .. } => {
|
||||||
|
locals.clone()
|
||||||
|
}
|
||||||
|
StackMapFrame::Same { .. } | StackMapFrame::Chop { .. } => vec![],
|
||||||
|
};
|
||||||
|
for local in &locals {
|
||||||
|
assert_ne!(
|
||||||
|
local,
|
||||||
|
&VerificationType::Object("[Ljava/lang/reflect/Method;".to_string())
|
||||||
|
);
|
||||||
|
if local == &VerificationType::Object("java/lang/Object".to_string()) {
|
||||||
|
saw_join = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(saw_join, "expected the join LUB in {frames:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `aaload` on a `Null` arrayref yields `Null`, not `Object`.
|
||||||
|
///
|
||||||
|
/// The `Null` path throws (`NullPointerException`) - no value flows from it,
|
||||||
|
/// so `Null` (bottom) is the precise answer. Yielding `Object` poisons the
|
||||||
|
/// join permanently: `lub(Object, String)` stays `Object`, while the true
|
||||||
|
/// merge (`lub(Null, String)`) is `String`. Worse, the poison arrives
|
||||||
|
/// through *transient* worklist states (before upstream slots refine) and
|
||||||
|
/// can never leave - the transfer is non-monotone in the arrayref.
|
||||||
|
#[test]
|
||||||
|
fn aaload_on_null_yields_null() {
|
||||||
|
let method = code_method(
|
||||||
|
"pick",
|
||||||
|
"(Ljava/lang/String;)Ljava/lang/Object;",
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
vec![
|
||||||
|
Insn::Mark(0),
|
||||||
|
Insn::AconstNull,
|
||||||
|
store_ref(1),
|
||||||
|
Insn::Const(Const::Int(0)),
|
||||||
|
Insn::Branch(BranchCond::Zero(Cond::Eq), 1),
|
||||||
|
load_ref(1),
|
||||||
|
Insn::Const(Const::Int(0)),
|
||||||
|
Insn::ArrayLoad(ArrayKind::Reference),
|
||||||
|
Insn::Goto(2),
|
||||||
|
Insn::Mark(1),
|
||||||
|
load_ref(0),
|
||||||
|
Insn::Mark(2),
|
||||||
|
Insn::AReturn,
|
||||||
|
],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
let frames = compute_frames("demo/Pick", &method, &NoHierarchy).expect("frames");
|
||||||
|
let mut saw_string = false;
|
||||||
|
for frame in &frames {
|
||||||
|
let stack: Vec<VerificationType> = match frame {
|
||||||
|
StackMapFrame::SameLocals { local, .. } => vec![local.clone()],
|
||||||
|
StackMapFrame::Full { stack, .. } => stack.clone(),
|
||||||
|
_ => vec![],
|
||||||
|
};
|
||||||
|
for item in &stack {
|
||||||
|
assert_ne!(
|
||||||
|
item,
|
||||||
|
&VerificationType::Object("java/lang/Object".to_string()),
|
||||||
|
"poisoned join in {frames:?}"
|
||||||
|
);
|
||||||
|
if item == &VerificationType::Object("java/lang/String".to_string()) {
|
||||||
|
saw_string = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(saw_string, "expected the String join in {frames:?}");
|
||||||
|
}
|
||||||
|
|
@ -3,51 +3,11 @@
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
use super::super::{StackMapFrame, VerificationType, compute_frames};
|
use super::super::{StackMapFrame, VerificationType, compute_frames};
|
||||||
|
|
||||||
use super::{NoHierarchy, code_method, load, load_ref, store, store_ref};
|
use super::{NoHierarchy, code_method, load, load_ref, store, store_ref};
|
||||||
use crate::insn::{ArrayKind, BranchCond, Cond, Const, Insn, Prim};
|
use crate::insn::{BranchCond, Cond, Const, Insn, Prim};
|
||||||
|
|
||||||
/// `anewarray [B` (a partially bound multi-dim array) yields its descriptor
|
mod arrays;
|
||||||
/// verbatim: wrapping it again (`[L[B;`) is not a class and HotSpot rejects
|
|
||||||
/// the pool entry.
|
|
||||||
#[test]
|
|
||||||
fn anewarray_of_array_keeps_descriptor() {
|
|
||||||
let method = code_method(
|
|
||||||
"make",
|
|
||||||
"()Ljava/lang/Object;",
|
|
||||||
2,
|
|
||||||
1,
|
|
||||||
vec![
|
|
||||||
Insn::Mark(0),
|
|
||||||
Insn::Const(Const::Int(3)),
|
|
||||||
Insn::ANewArray("[B".to_string()),
|
|
||||||
store_ref(0),
|
|
||||||
load_ref(0),
|
|
||||||
Insn::Branch(BranchCond::Null { is_null: false }, 1),
|
|
||||||
Insn::AconstNull,
|
|
||||||
store_ref(0),
|
|
||||||
Insn::Goto(2),
|
|
||||||
Insn::Mark(1),
|
|
||||||
Insn::Mark(2),
|
|
||||||
load_ref(0),
|
|
||||||
Insn::AReturn,
|
|
||||||
],
|
|
||||||
vec![],
|
|
||||||
);
|
|
||||||
let frames = compute_frames("demo/Make", &method, &NoHierarchy).expect("frames");
|
|
||||||
assert_eq!(
|
|
||||||
frames,
|
|
||||||
vec![StackMapFrame::Append {
|
|
||||||
offset_delta: 8,
|
|
||||||
locals: vec![VerificationType::Object("[[B".to_string())],
|
|
||||||
},]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A loop head keeps `Top` for a slot written only inside the loop.
|
|
||||||
///
|
|
||||||
/// Inflows: entry (never wrote it) and back edge (wrote `Integer`). Like
|
|
||||||
/// `javac`, the join declares `Top`: a precise type would reject the inflow
|
|
||||||
/// that never wrote the slot (`Type top is not assignable`).
|
|
||||||
#[test]
|
#[test]
|
||||||
fn loop_head_keeps_top_for_loop_local() {
|
fn loop_head_keeps_top_for_loop_local() {
|
||||||
let method = code_method(
|
let method = code_method(
|
||||||
|
|
@ -189,59 +149,3 @@ fn array_load_wide_pushes_pair() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `aaload` on a one-dimensional reference array yields the element type.
|
|
||||||
///
|
|
||||||
/// Real-world shape (`V8ValueObject.bind` family): one join inflow carries
|
|
||||||
/// the loaded element (`Method`), the other the array itself (`Method[]`).
|
|
||||||
/// The join must declare their LUB (`Object`); declaring the array rejects
|
|
||||||
/// the element inflow (`Type ... is not assignable to ...`).
|
|
||||||
#[test]
|
|
||||||
fn aaload_on_ref_array_yields_element() {
|
|
||||||
let method = code_method(
|
|
||||||
"pick",
|
|
||||||
"([Ljava/lang/reflect/Method;)Ljava/lang/Object;",
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
vec![
|
|
||||||
Insn::Mark(0),
|
|
||||||
Insn::Const(Const::Int(0)),
|
|
||||||
Insn::Branch(BranchCond::Zero(Cond::Eq), 1),
|
|
||||||
load_ref(0),
|
|
||||||
Insn::Const(Const::Int(0)),
|
|
||||||
Insn::ArrayLoad(ArrayKind::Reference),
|
|
||||||
store_ref(1),
|
|
||||||
Insn::Goto(2),
|
|
||||||
Insn::Mark(1),
|
|
||||||
load_ref(0),
|
|
||||||
store_ref(1),
|
|
||||||
Insn::Mark(2),
|
|
||||||
load_ref(1),
|
|
||||||
Insn::AReturn,
|
|
||||||
],
|
|
||||||
vec![],
|
|
||||||
);
|
|
||||||
let frames = compute_frames("demo/Pick", &method, &NoHierarchy).expect("frames");
|
|
||||||
// The join declares locals[1]; whatever the frame kind, it must be the
|
|
||||||
// LUB (`Object`), never the array the other inflow carried.
|
|
||||||
let mut saw_join = false;
|
|
||||||
for frame in &frames {
|
|
||||||
let locals: Vec<VerificationType> = match frame {
|
|
||||||
StackMapFrame::SameLocals { local, .. } => vec![local.clone()],
|
|
||||||
StackMapFrame::Append { locals, .. } | StackMapFrame::Full { locals, .. } => {
|
|
||||||
locals.clone()
|
|
||||||
}
|
|
||||||
StackMapFrame::Same { .. } | StackMapFrame::Chop { .. } => vec![],
|
|
||||||
};
|
|
||||||
for local in &locals {
|
|
||||||
assert_ne!(
|
|
||||||
local,
|
|
||||||
&VerificationType::Object("[Ljava/lang/reflect/Method;".to_string())
|
|
||||||
);
|
|
||||||
if local == &VerificationType::Object("java/lang/Object".to_string()) {
|
|
||||||
saw_join = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(saw_join, "expected the join LUB in {frames:?}");
|
|
||||||
}
|
|
||||||
|
|
@ -9,7 +9,7 @@ mod forms;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use noxis_analysis::ClassHierarchy;
|
use noxis_analysis::{ClassHierarchy, JdkHierarchy};
|
||||||
use noxis_classfile::{Const, Insn, MAX_METHOD_INSNS, Program, compute_frames};
|
use noxis_classfile::{Const, Insn, MAX_METHOD_INSNS, Program, compute_frames};
|
||||||
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
|
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
|
||||||
|
|
||||||
|
|
@ -100,8 +100,9 @@ impl Pass for ConstantsPass {
|
||||||
&mut predicates,
|
&mut predicates,
|
||||||
)?;
|
)?;
|
||||||
let hierarchy = ClassHierarchy::build(program);
|
let hierarchy = ClassHierarchy::build(program);
|
||||||
|
let frames = JdkHierarchy::wrap(&hierarchy);
|
||||||
for (ci, mi) in &mutated {
|
for (ci, mi) in &mutated {
|
||||||
refresh_frames(program, *ci, *mi, &hierarchy)?;
|
refresh_frames(program, *ci, *mi, &frames)?;
|
||||||
}
|
}
|
||||||
Ok(report(replaced, predicates, skipped))
|
Ok(report(replaced, predicates, skipped))
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +188,7 @@ fn refresh_frames(
|
||||||
program: &mut Program,
|
program: &mut Program,
|
||||||
ci: usize,
|
ci: usize,
|
||||||
mi: usize,
|
mi: usize,
|
||||||
hierarchy: &ClassHierarchy,
|
hierarchy: &JdkHierarchy<'_>,
|
||||||
) -> Result<(), CoreError> {
|
) -> Result<(), CoreError> {
|
||||||
let (name, snapshot) = {
|
let (name, snapshot) = {
|
||||||
let class = program
|
let class = program
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ mod rewire;
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use dispatch::{lookup_dispatch, opaque_guard, table_dispatch};
|
use dispatch::{lookup_dispatch, opaque_guard, table_dispatch};
|
||||||
use noxis_analysis::ClassHierarchy;
|
use noxis_analysis::{ClassHierarchy, JdkHierarchy};
|
||||||
use noxis_classfile::{Code, Insn, Label, Program, compute_frames};
|
use noxis_classfile::{Code, Insn, Label, Program, compute_frames};
|
||||||
use noxis_core::{Context, CoreError, Pass, PassId, PassReport, Rng};
|
use noxis_core::{Context, CoreError, Pass, PassId, PassReport, Rng};
|
||||||
use rewire::{dead_catch, invert_branches, split_blocks};
|
use rewire::{dead_catch, invert_branches, split_blocks};
|
||||||
|
|
@ -106,8 +106,9 @@ impl Pass for FlowPass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let hierarchy = ClassHierarchy::build(program);
|
let hierarchy = ClassHierarchy::build(program);
|
||||||
|
let frames = JdkHierarchy::wrap(&hierarchy);
|
||||||
for (ci, mi) in &mutated {
|
for (ci, mi) in &mutated {
|
||||||
refresh_frames(program, *ci, *mi, &hierarchy)?;
|
refresh_frames(program, *ci, *mi, &frames)?;
|
||||||
}
|
}
|
||||||
Ok(report(&per_scheme, mutated.len(), skipped))
|
Ok(report(&per_scheme, mutated.len(), skipped))
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +226,7 @@ fn refresh_frames(
|
||||||
program: &mut Program,
|
program: &mut Program,
|
||||||
ci: usize,
|
ci: usize,
|
||||||
mi: usize,
|
mi: usize,
|
||||||
hierarchy: &ClassHierarchy,
|
hierarchy: &JdkHierarchy<'_>,
|
||||||
) -> Result<(), CoreError> {
|
) -> Result<(), CoreError> {
|
||||||
let (name, snapshot) = {
|
let (name, snapshot) = {
|
||||||
let class = program
|
let class = program
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ mod tests;
|
||||||
use crate::namegen::NameGenerator;
|
use crate::namegen::NameGenerator;
|
||||||
use crate::rename::assign::parent_package;
|
use crate::rename::assign::parent_package;
|
||||||
use body::{entry_body, helper_body};
|
use body::{entry_body, helper_body};
|
||||||
use noxis_analysis::ClassHierarchy;
|
use noxis_analysis::{ClassHierarchy, JdkHierarchy};
|
||||||
use noxis_classfile::{ClassBuilder, Field, Program, Version, compute_frames};
|
use noxis_classfile::{ClassBuilder, Field, Program, Version, compute_frames};
|
||||||
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
|
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
|
||||||
use weave::weave_first_reachable;
|
use weave::weave_first_reachable;
|
||||||
|
|
@ -132,8 +132,9 @@ impl Pass for NoisePass {
|
||||||
|
|
||||||
// Frames for every generated branching method.
|
// Frames for every generated branching method.
|
||||||
let hierarchy = ClassHierarchy::build(program);
|
let hierarchy = ClassHierarchy::build(program);
|
||||||
|
let frames = JdkHierarchy::wrap(&hierarchy);
|
||||||
for (ci, mi) in &fresh_methods {
|
for (ci, mi) in &fresh_methods {
|
||||||
refresh_frames(program, *ci, *mi, &hierarchy)?;
|
refresh_frames(program, *ci, *mi, &frames)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut woven = 0usize;
|
let mut woven = 0usize;
|
||||||
|
|
@ -177,7 +178,7 @@ fn refresh_frames(
|
||||||
program: &mut Program,
|
program: &mut Program,
|
||||||
ci: usize,
|
ci: usize,
|
||||||
mi: usize,
|
mi: usize,
|
||||||
hierarchy: &ClassHierarchy,
|
hierarchy: &JdkHierarchy<'_>,
|
||||||
) -> Result<(), CoreError> {
|
) -> Result<(), CoreError> {
|
||||||
let (name, snapshot) = {
|
let (name, snapshot) = {
|
||||||
let class = program
|
let class = program
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Weaving junk classes into the reachability graph of live methods.
|
//! Weaving junk classes into the reachability graph of live methods.
|
||||||
|
|
||||||
use noxis_analysis::ClassHierarchy;
|
use noxis_analysis::{ClassHierarchy, JdkHierarchy};
|
||||||
use noxis_classfile::{Insn, InvokeKind, Program, compute_frames};
|
use noxis_classfile::{Insn, InvokeKind, Program, compute_frames};
|
||||||
use noxis_core::{CoreError, PassId};
|
use noxis_core::{CoreError, PassId};
|
||||||
|
|
||||||
|
|
@ -68,7 +68,8 @@ pub(super) fn weave_first_reachable(
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(missing)?;
|
.ok_or_else(missing)?;
|
||||||
let hierarchy = ClassHierarchy::build(program);
|
let hierarchy = ClassHierarchy::build(program);
|
||||||
let frames = compute_frames(&class_name, &snapshot, &hierarchy)
|
let frames_hierarchy = JdkHierarchy::wrap(&hierarchy);
|
||||||
|
let frames = compute_frames(&class_name, &snapshot, &frames_hierarchy)
|
||||||
.map_err(|e| CoreError::pass(PassId::Noise, e.to_string()))?;
|
.map_err(|e| CoreError::pass(PassId::Noise, e.to_string()))?;
|
||||||
program
|
program
|
||||||
.classes
|
.classes
|
||||||
|
|
|
||||||
|
|
@ -69,14 +69,25 @@ pub(super) fn rewrite_code(
|
||||||
}
|
}
|
||||||
*descriptor = rewrite_descriptor(descriptor, &maps.classes);
|
*descriptor = rewrite_descriptor(descriptor, &maps.classes);
|
||||||
}
|
}
|
||||||
Insn::New(name)
|
Insn::New(name) => {
|
||||||
| Insn::CheckCast(name)
|
|
||||||
| Insn::InstanceOf(name)
|
|
||||||
| Insn::ANewArray(name) => {
|
|
||||||
if let Some(mapped) = maps.classes.get(name.as_str()) {
|
if let Some(mapped) = maps.classes.get(name.as_str()) {
|
||||||
*name = mapped.clone();
|
*name = mapped.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Type operands may be array descriptors (`[Lpkg/Cls;`, `[B`):
|
||||||
|
// the element class inside follows the mapping, while a plain
|
||||||
|
// name takes the exact entry. A stale descriptor is worse than
|
||||||
|
// useless - later frame merges treat it as a ghost supertype.
|
||||||
|
Insn::CheckCast(name) | Insn::InstanceOf(name) | Insn::ANewArray(name) => {
|
||||||
|
if name.starts_with('[') {
|
||||||
|
*name = rewrite_descriptor(name, &maps.classes);
|
||||||
|
} else if let Some(mapped) = maps.classes.get(name.as_str()) {
|
||||||
|
*name = mapped.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Insn::MultiANewArray { descriptor, .. } => {
|
||||||
|
*descriptor = rewrite_descriptor(descriptor, &maps.classes);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,3 +191,77 @@ fn subclass_gets_alias_for_inherited_member() {
|
||||||
// Чужого члена не выдумываем.
|
// Чужого члена не выдумываем.
|
||||||
assert_eq!(maps.method("p/Sub", "other", "()V"), None);
|
assert_eq!(maps.method("p/Sub", "other", "()V"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Array type operands (`checkcast`, `instanceof`, `anewarray`,
|
||||||
|
/// `multianewarray`) carry descriptors, not plain names: the element class
|
||||||
|
/// inside must follow the mapping, or the reference silently goes stale
|
||||||
|
/// (and the next frame computation merges against a ghost type).
|
||||||
|
#[test]
|
||||||
|
fn array_type_operands_follow_their_element() {
|
||||||
|
let mut program = sample();
|
||||||
|
let main = program
|
||||||
|
.classes
|
||||||
|
.iter_mut()
|
||||||
|
.find(|c| c.this_class == "app/Main")
|
||||||
|
.expect("main");
|
||||||
|
let probe = main
|
||||||
|
.methods
|
||||||
|
.iter_mut()
|
||||||
|
.find(|m| m.name == "main")
|
||||||
|
.expect("main method");
|
||||||
|
let code = probe.code.as_mut().expect("code");
|
||||||
|
code.insns.extend([
|
||||||
|
Insn::CheckCast("[Lapp/Helper;".to_string()),
|
||||||
|
Insn::Pop,
|
||||||
|
Insn::InstanceOf("[Lapp/Helper;".to_string()),
|
||||||
|
Insn::Pop,
|
||||||
|
Insn::ANewArray("app/Helper".to_string()),
|
||||||
|
Insn::Pop,
|
||||||
|
Insn::MultiANewArray {
|
||||||
|
descriptor: "[[Lapp/Helper;".to_string(),
|
||||||
|
dims: 2,
|
||||||
|
},
|
||||||
|
Insn::Pop,
|
||||||
|
]);
|
||||||
|
let ctx = run_rename(&mut program);
|
||||||
|
let helper = ctx.mapping.class("app/Helper").expect("renamed");
|
||||||
|
let main = program.class("app/Main").expect("kept");
|
||||||
|
let method = main
|
||||||
|
.methods
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.name == "main")
|
||||||
|
.expect("main");
|
||||||
|
let insns = &method.code.as_ref().expect("code").insns;
|
||||||
|
let cast = insns
|
||||||
|
.iter()
|
||||||
|
.find_map(|i| match i {
|
||||||
|
Insn::CheckCast(name) => Some(name),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("checkcast");
|
||||||
|
assert_eq!(cast, &format!("[L{helper};"));
|
||||||
|
let inst = insns
|
||||||
|
.iter()
|
||||||
|
.find_map(|i| match i {
|
||||||
|
Insn::InstanceOf(name) => Some(name),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("instanceof");
|
||||||
|
assert_eq!(inst, &format!("[L{helper};"));
|
||||||
|
let anew = insns
|
||||||
|
.iter()
|
||||||
|
.find_map(|i| match i {
|
||||||
|
Insn::ANewArray(name) => Some(name),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("anewarray");
|
||||||
|
assert_eq!(anew, helper);
|
||||||
|
let multi = insns
|
||||||
|
.iter()
|
||||||
|
.find_map(|i| match i {
|
||||||
|
Insn::MultiANewArray { descriptor, .. } => Some(descriptor),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("multianewarray");
|
||||||
|
assert_eq!(multi, &format!("[[L{helper};"));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ mod tests;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use noxis_analysis::ClassHierarchy;
|
use noxis_analysis::{ClassHierarchy, JdkHierarchy};
|
||||||
use noxis_classfile::{Const, Insn, MAX_METHOD_INSNS, Program, compute_frames};
|
use noxis_classfile::{Const, Insn, MAX_METHOD_INSNS, Program, compute_frames};
|
||||||
use noxis_core::{Context, CoreError, NameStyle, Pass, PassId, PassReport, derive_seed};
|
use noxis_core::{Context, CoreError, NameStyle, Pass, PassId, PassReport, derive_seed};
|
||||||
use noxis_runtime::{Scheme, decoder_class, decoder_owner, encrypt};
|
use noxis_runtime::{Scheme, decoder_class, decoder_owner, encrypt};
|
||||||
|
|
@ -128,6 +128,7 @@ impl Pass for StringsPass {
|
||||||
ctx.note(format!("string decoder: {owner}"));
|
ctx.note(format!("string decoder: {owner}"));
|
||||||
// Phase 4: frames of the touched methods.
|
// Phase 4: frames of the touched methods.
|
||||||
let hierarchy = ClassHierarchy::build(program);
|
let hierarchy = ClassHierarchy::build(program);
|
||||||
|
let frames_hierarchy = JdkHierarchy::wrap(&hierarchy);
|
||||||
for (ci, mi) in &mutated {
|
for (ci, mi) in &mutated {
|
||||||
let (name, snapshot) = {
|
let (name, snapshot) = {
|
||||||
let class = program
|
let class = program
|
||||||
|
|
@ -143,7 +144,7 @@ impl Pass for StringsPass {
|
||||||
.ok_or_else(|| pass_error("method vanished"))?,
|
.ok_or_else(|| pass_error("method vanished"))?,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let frames = compute_frames(&name, &snapshot, &hierarchy)
|
let frames = compute_frames(&name, &snapshot, &frames_hierarchy)
|
||||||
.map_err(|e| pass_error(e.to_string()))?;
|
.map_err(|e| pass_error(e.to_string()))?;
|
||||||
program
|
program
|
||||||
.classes
|
.classes
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ struct Args {
|
||||||
seed: u64,
|
seed: u64,
|
||||||
config: Option<String>,
|
config: Option<String>,
|
||||||
bisect: bool,
|
bisect: bool,
|
||||||
|
recompute: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_args() -> Result<Args, String> {
|
fn parse_args() -> Result<Args, String> {
|
||||||
|
|
@ -47,6 +48,7 @@ fn parse_args() -> Result<Args, String> {
|
||||||
seed: 1,
|
seed: 1,
|
||||||
config: None,
|
config: None,
|
||||||
bisect: false,
|
bisect: false,
|
||||||
|
recompute: false,
|
||||||
};
|
};
|
||||||
let mut iter = std::env::args().skip(1);
|
let mut iter = std::env::args().skip(1);
|
||||||
while let Some(flag) = iter.next() {
|
while let Some(flag) = iter.next() {
|
||||||
|
|
@ -62,6 +64,7 @@ fn parse_args() -> Result<Args, String> {
|
||||||
}
|
}
|
||||||
"--config" => args.config = Some(iter.next().ok_or("--config needs a value")?),
|
"--config" => args.config = Some(iter.next().ok_or("--config needs a value")?),
|
||||||
"--bisect" => args.bisect = true,
|
"--bisect" => args.bisect = true,
|
||||||
|
"--recompute" => args.recompute = true,
|
||||||
other => return Err(format!("unknown flag {other}")),
|
other => return Err(format!("unknown flag {other}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -212,9 +215,61 @@ fn bisect(cp: &Path, class: &ClassFile) -> Option<BTreeSet<(String, String)>> {
|
||||||
Some(keep)
|
Some(keep)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recompute frames for a method as written: proves whether the stored
|
||||||
|
/// table is stale (code grew after compute) or compute itself truncates.
|
||||||
|
fn recompute(jar_path: &Path, target: &str) -> Result<bool, String> {
|
||||||
|
use noxis_analysis::{ClassHierarchy, JdkHierarchy};
|
||||||
|
use noxis_classfile::compute_frames;
|
||||||
|
|
||||||
|
let entries = read_file(jar_path).map_err(|e| e.to_string())?;
|
||||||
|
let program = load(entries).map_err(|e| e.to_string())?;
|
||||||
|
let (class_name, method_name) = target
|
||||||
|
.rsplit_once('#')
|
||||||
|
.ok_or("usage: --recompute --class <internal/name>#<method>")?;
|
||||||
|
let class = program
|
||||||
|
.class(class_name)
|
||||||
|
.ok_or_else(|| format!("{class_name} not in jar"))?;
|
||||||
|
let method = class
|
||||||
|
.methods
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.name == method_name)
|
||||||
|
.ok_or_else(|| format!("{method_name} not in {class_name}"))?;
|
||||||
|
let base = ClassHierarchy::build(&program);
|
||||||
|
let hierarchy = JdkHierarchy::wrap(&base);
|
||||||
|
let fresh =
|
||||||
|
compute_frames(class_name, method, &hierarchy).map_err(|e| format!("compute: {e}"))?;
|
||||||
|
let stored = method.code.as_ref().map_or(0, |c| c.frames.len());
|
||||||
|
println!("stored frames: {stored}, recomputed: {}", fresh.len());
|
||||||
|
// Last covered bci of each table.
|
||||||
|
for (label, frames) in [
|
||||||
|
(
|
||||||
|
"stored",
|
||||||
|
method.code.as_ref().map_or(&[][..], |c| &c.frames[..]),
|
||||||
|
),
|
||||||
|
("fresh", &fresh[..]),
|
||||||
|
] {
|
||||||
|
let mut bci: i64 = -1;
|
||||||
|
for frame in frames {
|
||||||
|
let d = match frame {
|
||||||
|
noxis_classfile::StackMapFrame::Same { offset_delta }
|
||||||
|
| noxis_classfile::StackMapFrame::SameLocals { offset_delta, .. }
|
||||||
|
| noxis_classfile::StackMapFrame::Chop { offset_delta, .. }
|
||||||
|
| noxis_classfile::StackMapFrame::Append { offset_delta, .. }
|
||||||
|
| noxis_classfile::StackMapFrame::Full { offset_delta, .. } => *offset_delta as i64,
|
||||||
|
};
|
||||||
|
bci = if bci < 0 { d } else { bci + d + 1 };
|
||||||
|
}
|
||||||
|
println!("{label}: last bci {bci}");
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
fn run() -> Result<bool, String> {
|
fn run() -> Result<bool, String> {
|
||||||
let args = parse_args()?;
|
let args = parse_args()?;
|
||||||
let jar_path = PathBuf::from(&args.jar);
|
let jar_path = PathBuf::from(&args.jar);
|
||||||
|
if args.recompute {
|
||||||
|
return recompute(&jar_path, &args.class);
|
||||||
|
}
|
||||||
let config = load_config(&args)?;
|
let config = load_config(&args)?;
|
||||||
let entries = read_file(&jar_path).map_err(|e| e.to_string())?;
|
let entries = read_file(&jar_path).map_err(|e| e.to_string())?;
|
||||||
if !entries
|
if !entries
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue