Initial commit: noxis JVM bytecode obfuscator
Workspace of 9 crates (noxis, noxis-core, noxis-classfile, noxis-analysis, noxis-passes, noxis-runtime, noxis-jar, noxis-verify, noxis-structure-check). AGPL-3.0-only license, Rust 2024 edition, unsafe_code denied.
This commit is contained in:
commit
a641e69455
140 changed files with 17197 additions and 0 deletions
112
crates/noxis-classfile/src/format/parse/code/frames.rs
Normal file
112
crates/noxis-classfile/src/format/parse/code/frames.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
//! Кадры `StackMapTable` при чтении: вариант в вариант, дельты как есть.
|
||||
//!
|
||||
//! Backend хранит кадры в номерах инструкций (см. `from_bytes_code_attributes`),
|
||||
//! наш IR — тоже: writer сам пересчитает их в байты при `to_bytes`.
|
||||
|
||||
use ristretto_classfile::ConstantPool;
|
||||
use ristretto_classfile::attributes::{StackFrame, VerificationType as RtType};
|
||||
|
||||
use super::super::pool::class_name;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::format::{StackMapFrame, VerificationType};
|
||||
|
||||
/// Кадры backend'а → наши (индексные дельты — как есть).
|
||||
pub(super) fn to_frames(
|
||||
frames: &[StackFrame],
|
||||
pool: &ConstantPool<'_>,
|
||||
) -> Result<Vec<StackMapFrame>, ClassfileError> {
|
||||
frames.iter().map(|frame| to_frame(frame, pool)).collect()
|
||||
}
|
||||
|
||||
fn to_frame(frame: &StackFrame, pool: &ConstantPool<'_>) -> Result<StackMapFrame, ClassfileError> {
|
||||
Ok(match frame {
|
||||
StackFrame::SameFrame { frame_type } => StackMapFrame::Same {
|
||||
offset_delta: u32::from(*frame_type),
|
||||
},
|
||||
StackFrame::SameFrameExtended { offset_delta, .. } => StackMapFrame::Same {
|
||||
offset_delta: u32::from(*offset_delta),
|
||||
},
|
||||
StackFrame::SameLocals1StackItemFrame { frame_type, stack } => StackMapFrame::SameLocals {
|
||||
offset_delta: u32::from(*frame_type) - 64,
|
||||
local: single(stack, pool)?,
|
||||
},
|
||||
StackFrame::SameLocals1StackItemFrameExtended {
|
||||
offset_delta,
|
||||
stack,
|
||||
..
|
||||
} => StackMapFrame::SameLocals {
|
||||
offset_delta: u32::from(*offset_delta),
|
||||
local: single(stack, pool)?,
|
||||
},
|
||||
StackFrame::ChopFrame {
|
||||
frame_type,
|
||||
offset_delta,
|
||||
} => StackMapFrame::Chop {
|
||||
offset_delta: u32::from(*offset_delta),
|
||||
count: 251u8.wrapping_sub(*frame_type),
|
||||
},
|
||||
StackFrame::AppendFrame {
|
||||
offset_delta,
|
||||
locals,
|
||||
..
|
||||
} => StackMapFrame::Append {
|
||||
offset_delta: u32::from(*offset_delta),
|
||||
locals: types(locals, pool)?,
|
||||
},
|
||||
StackFrame::FullFrame {
|
||||
offset_delta,
|
||||
locals,
|
||||
stack,
|
||||
..
|
||||
} => StackMapFrame::Full {
|
||||
offset_delta: u32::from(*offset_delta),
|
||||
locals: types(locals, pool)?,
|
||||
stack: types(stack, pool)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Единственный элемент стека кадра (`same_locals_1_stack_item`).
|
||||
fn single(stack: &[RtType], pool: &ConstantPool<'_>) -> Result<VerificationType, ClassfileError> {
|
||||
let mut types = types(stack, pool)?.into_iter();
|
||||
match (types.next(), types.next()) {
|
||||
(Some(first), None) => Ok(first),
|
||||
_ => Err(ClassfileError::Parse(
|
||||
"в same_locals-кадре не один элемент стека".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn types(
|
||||
values: &[RtType],
|
||||
pool: &ConstantPool<'_>,
|
||||
) -> Result<Vec<VerificationType>, ClassfileError> {
|
||||
values.iter().map(|value| vtype(value, pool)).collect()
|
||||
}
|
||||
|
||||
/// Тип верификации backend'а → наш тип (имя класса берётся из constant pool).
|
||||
fn vtype(value: &RtType, pool: &ConstantPool<'_>) -> Result<VerificationType, ClassfileError> {
|
||||
Ok(match value {
|
||||
RtType::Top => VerificationType::Top,
|
||||
RtType::Integer => VerificationType::Integer,
|
||||
RtType::Float => VerificationType::Float,
|
||||
RtType::Double => VerificationType::Double,
|
||||
RtType::Long => VerificationType::Long,
|
||||
RtType::Null => VerificationType::Null,
|
||||
RtType::UninitializedThis => VerificationType::UninitializedThis,
|
||||
RtType::Object { cpool_index } => VerificationType::Object(class_name(pool, *cpool_index)?),
|
||||
RtType::Uninitialized { offset } => VerificationType::Uninitialized(*offset),
|
||||
})
|
||||
}
|
||||
|
||||
/// Тип перехвата (`0` — catch-all для `finally`/`any`).
|
||||
pub(super) fn catch_type(
|
||||
pool: &ConstantPool<'_>,
|
||||
index: u16,
|
||||
) -> Result<Option<String>, ClassfileError> {
|
||||
if index == 0 {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(class_name(pool, index)?))
|
||||
}
|
||||
}
|
||||
187
crates/noxis-classfile/src/format/parse/code/labels.rs
Normal file
187
crates/noxis-classfile/src/format/parse/code/labels.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
//! Метки тела метода: где обязаны стоять и как резолвятся.
|
||||
//!
|
||||
//! Backend нумерует инструкции по порядку — метки ставятся прямо на номера,
|
||||
//! без байтовых смещений.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use ristretto_classfile::attributes::Attribute as RtAttr;
|
||||
use ristretto_classfile::attributes::{ExceptionTableEntry, Instruction};
|
||||
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::Label;
|
||||
|
||||
/// Номера инструкций, на которых обязана стоять метка.
|
||||
///
|
||||
/// `extra` — позиции отладочных таблиц (уже номерами): вставки пассов
|
||||
/// не должны оставлять их без меток.
|
||||
pub(super) fn label_indexes(
|
||||
code: &[Instruction],
|
||||
exception_table: &[ExceptionTableEntry],
|
||||
extra: &BTreeSet<u32>,
|
||||
) -> Result<BTreeSet<u32>, ClassfileError> {
|
||||
let mut indexes = BTreeSet::from([0u32]);
|
||||
for (position, instruction) in code.iter().enumerate() {
|
||||
let index = to_index(position)?;
|
||||
for target in branch_indexes(instruction, index) {
|
||||
indexes.insert(to_index_checked(target)?);
|
||||
}
|
||||
}
|
||||
for entry in exception_table {
|
||||
indexes.insert(u32::from(entry.range_pc.start));
|
||||
indexes.insert(u32::from(entry.range_pc.end));
|
||||
indexes.insert(u32::from(entry.handler_pc));
|
||||
}
|
||||
indexes.extend(extra.iter().copied());
|
||||
Ok(indexes)
|
||||
}
|
||||
|
||||
/// Цели ветвлений backend'а: для `if*`/`goto` — абсолютный номер, для `switch` — дельта.
|
||||
fn branch_indexes(instruction: &Instruction, index: u16) -> Vec<i32> {
|
||||
let base = i32::from(index);
|
||||
match instruction {
|
||||
Instruction::Tableswitch(table) => once(base + table.default)
|
||||
.chain(table.offsets.iter().map(|target| base + target))
|
||||
.collect(),
|
||||
Instruction::Lookupswitch(lookup) => once(base + lookup.default)
|
||||
.chain(lookup.pairs.values().map(|target| base + target))
|
||||
.collect(),
|
||||
Instruction::Ifeq(target)
|
||||
| Instruction::Ifne(target)
|
||||
| Instruction::Iflt(target)
|
||||
| Instruction::Ifge(target)
|
||||
| Instruction::Ifgt(target)
|
||||
| Instruction::Ifle(target)
|
||||
| Instruction::If_icmpeq(target)
|
||||
| Instruction::If_icmpne(target)
|
||||
| Instruction::If_icmplt(target)
|
||||
| Instruction::If_icmpge(target)
|
||||
| Instruction::If_icmpgt(target)
|
||||
| Instruction::If_icmple(target)
|
||||
| Instruction::If_acmpeq(target)
|
||||
| Instruction::If_acmpne(target)
|
||||
| Instruction::Ifnull(target)
|
||||
| Instruction::Ifnonnull(target)
|
||||
| Instruction::Goto(target)
|
||||
| Instruction::Jsr(target) => vec![i32::from(*target)],
|
||||
Instruction::Goto_w(target) | Instruction::Jsr_w(target) => vec![*target],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn once(value: i32) -> std::iter::Once<i32> {
|
||||
std::iter::once(value)
|
||||
}
|
||||
|
||||
/// Превратить отсортированный набор номеров в последовательные метки.
|
||||
pub(super) fn assign(indexes: &BTreeSet<u32>) -> BTreeMap<u32, Label> {
|
||||
indexes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(position, index)| (*index, position as u32))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Метка для номера инструкции.
|
||||
pub(super) fn lookup(labels: &BTreeMap<u32, Label>, index: u16) -> Result<Label, ClassfileError> {
|
||||
labels
|
||||
.get(&u32::from(index))
|
||||
.copied()
|
||||
.ok_or_else(|| ClassfileError::Parse(format!("нет метки для инструкции {index}")))
|
||||
}
|
||||
|
||||
/// Позиции отладочных таблиц, которым нужны метки.
|
||||
pub(super) fn collect_debug_labels(
|
||||
attributes: &[RtAttr],
|
||||
starts: &[u16],
|
||||
code_length: u16,
|
||||
extra: &mut BTreeSet<u32>,
|
||||
) -> Result<(), ClassfileError> {
|
||||
for attr in attributes {
|
||||
match attr {
|
||||
RtAttr::LineNumberTable { line_numbers, .. } => {
|
||||
for line in line_numbers {
|
||||
extra.insert(u32::from(line.start_pc));
|
||||
}
|
||||
}
|
||||
RtAttr::LocalVariableTable { variables, .. } => {
|
||||
for var in variables {
|
||||
extra.insert(byte_to_index(starts, var.start_pc)?);
|
||||
extra.insert(end_index(starts, var.start_pc, var.length, code_length)?);
|
||||
}
|
||||
}
|
||||
RtAttr::LocalVariableTypeTable { variable_types, .. } => {
|
||||
for var in variable_types {
|
||||
extra.insert(byte_to_index(starts, var.start_pc)?);
|
||||
extra.insert(end_index(starts, var.start_pc, var.length, code_length)?);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Байт → номер инструкции (только границы инструкций).
|
||||
pub(super) fn byte_to_index(starts: &[u16], byte: u16) -> Result<u32, ClassfileError> {
|
||||
starts
|
||||
.iter()
|
||||
.position(|&start| start == byte)
|
||||
.and_then(|position| u32::try_from(position).ok())
|
||||
.ok_or_else(|| ClassfileError::Parse(format!("смещение {byte} мимо инструкции")))
|
||||
}
|
||||
|
||||
/// Конец диапазона `[start, start+length)`: индекс за концом.
|
||||
fn end_index(
|
||||
starts: &[u16],
|
||||
start: u16,
|
||||
length: u16,
|
||||
code_length: u16,
|
||||
) -> Result<u32, ClassfileError> {
|
||||
let end = start
|
||||
.checked_add(length)
|
||||
.ok_or_else(|| ClassfileError::Parse("диапазон вне метода".into()))?;
|
||||
if end == code_length {
|
||||
return u32::try_from(starts.len())
|
||||
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()));
|
||||
}
|
||||
byte_to_index(starts, end)
|
||||
}
|
||||
|
||||
/// Метка для байтового смещения отладочной таблицы.
|
||||
pub(super) fn resolve_byte(
|
||||
labels: &BTreeMap<u32, Label>,
|
||||
starts: &[u16],
|
||||
byte: u16,
|
||||
) -> Result<Label, ClassfileError> {
|
||||
let index = byte_to_index(starts, byte)?;
|
||||
labels
|
||||
.get(&index)
|
||||
.copied()
|
||||
.ok_or_else(|| ClassfileError::Parse(format!("нет метки для смещения {byte}")))
|
||||
}
|
||||
|
||||
/// Метка конца диапазона отладочной таблицы.
|
||||
pub(super) fn resolve_end(
|
||||
labels: &BTreeMap<u32, Label>,
|
||||
starts: &[u16],
|
||||
start: u16,
|
||||
length: u16,
|
||||
code_length: u16,
|
||||
) -> Result<Label, ClassfileError> {
|
||||
let index = end_index(starts, start, length, code_length)?;
|
||||
labels
|
||||
.get(&index)
|
||||
.copied()
|
||||
.ok_or_else(|| ClassfileError::Parse(format!("нет метки конца {index}")))
|
||||
}
|
||||
|
||||
fn to_index(position: usize) -> Result<u16, ClassfileError> {
|
||||
u16::try_from(position)
|
||||
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()))
|
||||
}
|
||||
|
||||
fn to_index_checked(index: i32) -> Result<u32, ClassfileError> {
|
||||
u32::try_from(index)
|
||||
.map_err(|_| ClassfileError::Parse(format!("номер инструкции {index} вне тела метода")))
|
||||
}
|
||||
169
crates/noxis-classfile/src/format/parse/code/mod.rs
Normal file
169
crates/noxis-classfile/src/format/parse/code/mod.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
//! `Code` backend'а → тело метода нашего IR: метки, исключения, кадры.
|
||||
//!
|
||||
//! Backend уже перевёл байтовые смещения в номера инструкций — конвертеры
|
||||
//! работают прямо с ними: метки ставятся на целях переходов, границах
|
||||
//! try-блоков и позициях отладочных таблиц; кадры переносятся как есть
|
||||
//! (индексные дельты). Позиции `LineNumberTable`/`LocalVariableTable` —
|
||||
//! тоже метки, поэтому вставки пассов их не ломают (байты досчитает writer).
|
||||
|
||||
mod frames;
|
||||
mod labels;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::io::Cursor;
|
||||
|
||||
use ristretto_classfile::ConstantPool;
|
||||
use ristretto_classfile::attributes::Attribute as RtAttr;
|
||||
use ristretto_classfile::attributes::{ExceptionTableEntry, Instruction};
|
||||
|
||||
use super::attrs;
|
||||
use super::insns::convert;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::Insn;
|
||||
use crate::model::{
|
||||
Attribute, Code, ExceptionHandler, LineNumber, LocalVariable, LocalVariableType,
|
||||
};
|
||||
|
||||
use frames::{catch_type, to_frames};
|
||||
use labels::{assign, collect_debug_labels, label_indexes, lookup, resolve_byte, resolve_end};
|
||||
|
||||
/// Тело метода целиком: backend `Code` → наш [`Code`].
|
||||
pub(super) fn convert_code(
|
||||
max_stack: u16,
|
||||
max_locals: u16,
|
||||
code: &[Instruction],
|
||||
exception_table: &[ExceptionTableEntry],
|
||||
attributes: &[RtAttr],
|
||||
pool: &ConstantPool<'_>,
|
||||
) -> Result<Code, ClassfileError> {
|
||||
let (starts, code_length) = layout(code)?;
|
||||
let mut extra = BTreeSet::new();
|
||||
collect_debug_labels(attributes, &starts, code_length, &mut extra)?;
|
||||
let labels = assign(&label_indexes(code, exception_table, &extra)?);
|
||||
let resolve = |index: u16| lookup(&labels, index);
|
||||
|
||||
let mut insns = Vec::with_capacity(code.len() + labels.len());
|
||||
for (position, instruction) in code.iter().enumerate() {
|
||||
let index = u16::try_from(position)
|
||||
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()))?;
|
||||
if let Some(label) = labels.get(&u32::from(index)) {
|
||||
insns.push(Insn::Mark(*label));
|
||||
}
|
||||
insns.push(convert(instruction, index, pool, &resolve)?);
|
||||
}
|
||||
let end = u32::try_from(code.len())
|
||||
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()))?;
|
||||
if let Some(label) = labels.get(&end) {
|
||||
insns.push(Insn::Mark(*label));
|
||||
}
|
||||
|
||||
let exception_table = exception_table
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
Ok(ExceptionHandler {
|
||||
start: lookup(&labels, entry.range_pc.start)?,
|
||||
end: lookup(&labels, entry.range_pc.end)?,
|
||||
handler: lookup(&labels, entry.handler_pc)?,
|
||||
catch_type: catch_type(pool, entry.catch_type)?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, ClassfileError>>()?;
|
||||
|
||||
let mut frames = Vec::new();
|
||||
let mut rest = Vec::new();
|
||||
for attr in attributes {
|
||||
match attr {
|
||||
RtAttr::StackMapTable { frames: found, .. } => {
|
||||
if !frames.is_empty() {
|
||||
return Err(ClassfileError::Parse(
|
||||
"два StackMapTable в одном Code".into(),
|
||||
));
|
||||
}
|
||||
frames = to_frames(found, pool)?;
|
||||
}
|
||||
RtAttr::LineNumberTable { line_numbers, .. } => rest.push(Attribute::LineNumberTable(
|
||||
line_numbers
|
||||
.iter()
|
||||
.map(|line| {
|
||||
Ok(LineNumber {
|
||||
start: lookup(&labels, line.start_pc)?,
|
||||
line: line.line_number,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, ClassfileError>>()?,
|
||||
)),
|
||||
RtAttr::LocalVariableTable { variables, .. } => {
|
||||
rest.push(Attribute::LocalVariableTable(
|
||||
variables
|
||||
.iter()
|
||||
.map(|var| {
|
||||
Ok(LocalVariable {
|
||||
start: resolve_byte(&labels, &starts, var.start_pc)?,
|
||||
end: resolve_end(
|
||||
&labels,
|
||||
&starts,
|
||||
var.start_pc,
|
||||
var.length,
|
||||
code_length,
|
||||
)?,
|
||||
name: super::pool::utf8(pool, var.name_index)?,
|
||||
descriptor: super::pool::utf8(pool, var.descriptor_index)?,
|
||||
index: var.index,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, ClassfileError>>()?,
|
||||
))
|
||||
}
|
||||
RtAttr::LocalVariableTypeTable { variable_types, .. } => {
|
||||
rest.push(Attribute::LocalVariableTypeTable(
|
||||
variable_types
|
||||
.iter()
|
||||
.map(|var| {
|
||||
Ok(LocalVariableType {
|
||||
start: resolve_byte(&labels, &starts, var.start_pc)?,
|
||||
end: resolve_end(
|
||||
&labels,
|
||||
&starts,
|
||||
var.start_pc,
|
||||
var.length,
|
||||
code_length,
|
||||
)?,
|
||||
name: super::pool::utf8(pool, var.name_index)?,
|
||||
signature: super::pool::utf8(pool, var.signature_index)?,
|
||||
index: var.index,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, ClassfileError>>()?,
|
||||
));
|
||||
}
|
||||
other => rest.push(attrs::convert(other, pool)?),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Code {
|
||||
max_stack,
|
||||
max_locals,
|
||||
insns,
|
||||
exception_table,
|
||||
frames,
|
||||
attributes: rest,
|
||||
})
|
||||
}
|
||||
|
||||
/// Байтовые смещения инструкций и длина тела метода.
|
||||
fn layout(code: &[Instruction]) -> Result<(Vec<u16>, u16), ClassfileError> {
|
||||
let mut cursor = Cursor::new(Vec::new());
|
||||
let mut offsets = Vec::with_capacity(code.len());
|
||||
for instruction in code {
|
||||
offsets.push(tell(&cursor)?);
|
||||
instruction
|
||||
.to_bytes(&mut cursor)
|
||||
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
||||
}
|
||||
Ok((offsets, tell(&cursor)?))
|
||||
}
|
||||
|
||||
fn tell(cursor: &Cursor<Vec<u8>>) -> Result<u16, ClassfileError> {
|
||||
u16::try_from(cursor.position())
|
||||
.map_err(|_| ClassfileError::Parse("тело метода длиннее 64 КБ".into()))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue