Proven by paranoid runs over lorep (17 classes, fully green), LoVisual (4081 classes) and oc2r (1278 classes) with per-class java -Xverify:all, each fix first reproduced as a red test: - merge: Top absorbs instead of Top-wins (javac declares Top for slots unwritten on some inflow; precise types reject those edges). Regression: loop_head_keeps_top_for_loop_local. - emit: collapse [Long|Double, Top] pairs on operand stacks (javac writes one entry; the pair is rejected). Regression: wide_stack_join_emits_single_entries. - xfer ArrayLoad: push the wide pair, fixing lcmp underflow. Regression: array_load_wide_pushes_pair. - xfer <init>: convert only the receiver's own allocation site instead of every Uninitialized (nested construction clobbered the outer object). Regression: nested_construction_keeps_outer. - xfer ANewArray: array operands gain one dimension ([B] -> [[B) instead of an illegal L-wrap ([L[B;). Covered by the same test. - jar load: duplicate class names (multi-release module-info twins) keep the first occurrence instead of crashing save. Test: duplicate_class_names_keep_first. - strings/constants: skip methods past MAX_METHOD_INSNS (8000) so giants cannot overflow the 64KB writer ceiling. Tests: oversized_methods_are_skipped in both passes. - merge errors now name both types being merged. Also: em-dash removed repo-wide (724 spots, rule added to todo.md), allow-lints only in tests (scratch example deleted), code comments migrated to English in every file this change touches.
129 lines
8.2 KiB
Markdown
129 lines
8.2 KiB
Markdown
# Архитектура
|
||
|
||
## Слои
|
||
|
||
```text
|
||
┌──────────────────────────┐
|
||
CLI (noxis) │ clap → конфиг → отчёт │
|
||
└────────────┬─────────────┘
|
||
│
|
||
┌─────────────────────┼─────────────────────┐
|
||
▼ ▼ ▼
|
||
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
|
||
│ noxis-jar │ │noxis-passes │ │noxis-verify │
|
||
│ ZIP, ресурсы│ │ трансформации│ │ red-паттерны │
|
||
└──────┬──────┘ └──────┬───────┘ └──────┬───────┘
|
||
│ │ │
|
||
│ ┌──────┴───────┐ │
|
||
│ │ noxis-core │ │
|
||
│ │ конфиг, seed │ │
|
||
│ │ маппинги, Pass│ │
|
||
│ └──────┬───────┘ │
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
┌────────────────────────────────────────────────────────┐
|
||
│ noxis-classfile (IR) │
|
||
│ Program → ClassFile → Code → Insn (метки, не смещения)│
|
||
│ model/ format/ insn error │
|
||
└────────────────────────────────────────────────────────┘
|
||
▲
|
||
┌──────┴──────┐ ┌───────────────┐ ┌──────────────────┐
|
||
│noxis- │ │noxis-runtime │ │noxis-structure- │
|
||
│analysis │ │ шифры, ABI │ │check (build-time)│
|
||
└─────────────┘ └───────────────┘ └──────────────────┘
|
||
```
|
||
|
||
Зависимости направлены только вниз. Циклов нет: сборка конвейера живёт в
|
||
`noxis-passes`, потому что `noxis-core` знает только контракт `Pass`.
|
||
|
||
## Крейты
|
||
|
||
| крейт | ответственность |
|
||
|---|---|
|
||
| `noxis-classfile` | собственный IR классовых файлов, чтение/запись, `StackMapTable` |
|
||
| `noxis-core` | конфигурация, детерминированный seed, маппинги, контекст и контракт `Pass` |
|
||
| `noxis-analysis` | иерархия наследования, entrypoint'ы, рефлексия по имени, keep-правила |
|
||
| `noxis-passes` | сами трансформации + сборка стандартного конвейера |
|
||
| `noxis-runtime` | схемы шифрования строк и ABI класса-декодера |
|
||
| `noxis-jar` | ZIP: чтение, перепаковка, вырезание подписей, патч ресурсов |
|
||
| `noxis-verify` | red-паттерны, `java -Xverify:all`, метрики |
|
||
| `noxis` | CLI |
|
||
| `noxis-structure-check` | автогейт структурных правил (см. [`README.md`](README.md)) |
|
||
|
||
## Собственный IR
|
||
|
||
Пассы не работают с API стороннего парсера - они работают с моделью:
|
||
|
||
```text
|
||
Program все классы + ресурсы архива
|
||
├─ ClassFile версия, флаги, this/super, поля, методы, атрибуты
|
||
│ ├─ Field
|
||
│ └─ Method
|
||
│ └─ Code max_stack/max_locals, insns, exception_table, frames
|
||
└─ Resource путь + байты внутри архива
|
||
```
|
||
|
||
Ключевое решение - **`Insn::Mark(Label)` вместо байтовых смещений**:
|
||
|
||
- любой пасс, меняющий порядок или длину инструкций, не пересчитывает
|
||
jump-офсеты;
|
||
- пересчёт делается ровно один раз - в writer'е;
|
||
- в IR нет состояния, в котором «смещение уехало, а метка осталась».
|
||
|
||
Атрибуты, которые мы не понимаем, хранятся как `Attribute::Raw { name, data }`
|
||
и возвращаются на место без изменений - round-trip без потерь.
|
||
|
||
## Контракт пасса
|
||
|
||
```rust
|
||
pub trait Pass: Send + Sync {
|
||
fn id(&self) -> PassId;
|
||
fn apply(&self, ctx: &mut Context, program: &mut Program)
|
||
-> Result<PassReport, CoreError>;
|
||
}
|
||
```
|
||
|
||
- `Context` - всё состояние прогона: конфиг, мастер-сид, маппинги, заметки.
|
||
Из него берётся RNG: `ctx.rng("rename/classes")` - доменное разделение,
|
||
чтобы случайности разных пассов не были статистически коррелированы.
|
||
- `PassReport` - что трансформировано, что сознательно пропущено и почему.
|
||
- `Report` - собирает отчёты всех пассов + счётчики, сериализуется в JSON.
|
||
|
||
Детерминированность - свойство, а не пожелание: одинаковый seed на одинаковой
|
||
версии даёт байт-в-байт одинаковый выход. Это проверяется тестами.
|
||
|
||
## Конвейер
|
||
|
||
Порядок пассов - не деталь:
|
||
|
||
```text
|
||
Rename → Resources → Strings → Constants → Flow → Noise → DebugInfo
|
||
```
|
||
|
||
почему именно так:
|
||
|
||
1. **Rename первым** - все остальные техники порождают новые ссылки
|
||
(`LDC`-шум, вызовы декодера, junk-классы), и они уже должны быть в
|
||
обфусцированных именах.
|
||
2. **Resources сразу после Rename** - патч `fabric.mod.json`, `*.mixins.json`,
|
||
refmap и access widener должен видеть *итоговые* имена классов.
|
||
3. **Strings и Constants** - работают по телам методов, ещё не искажённым
|
||
control-flow'ем.
|
||
4. **Flow** - искажает CFG. Если поставить его раньше, Strings/Constants
|
||
перестанут находить свои шаблоны в инструкциях.
|
||
5. **Noise** - junk-классы и аннотации добавляются в самом конце, когда
|
||
целевой граф уже финален.
|
||
6. **DebugInfo последним** - strip/spoof отладочной информации после всех
|
||
трансформаций, чтобы не породить свежие `LineNumberTable`.
|
||
|
||
## Статус
|
||
|
||
| этап | что |
|
||
|---|---|
|
||
| **готово** | IR, модель, конфиг, seed/PRNG, маппинги, анализ, keep, гейт правил, `format::parse`/`write` поверх ристретто, вывод кадров `StackMapTable` |
|
||
| **готово** | пассы rename/resources/strings/constants/noise/debug, генератор декодера, вывод мастер-секрета в рантайме |
|
||
| **готово** | `noxis-verify` (red-паттерны, `java -Xverify:all`, метрики), CLI (`obfuscate`/`analyze`/`patterns`) |
|
||
| **готово** | пасс `flow` (4 схемы + ротация + мешанина, приёмка настоящим `java`) |
|
||
| **план** | фикстуры и regression-тесты |
|
||
|
||
Детали выбора - [`stack.md`](stack.md), план - [`../todo.md`](../todo.md).
|