gbf_core
is a Rust library designed to analyze, disassemble, process, and decompile Graal Script 2 (GS2) bytecode. It provides tools for GS2 bytecode analysis, control flow graph (CFG) generation, and abstract syntax tree (AST) construction, with a focus on modern Rust best practices.
- GS2 Bytecode Analysis:
- Decode and analyze GraalScript bytecode for program analysis.
- Disassembly and Decompilation:
- Disassemble bytecode into human-readable instructions.
- Control Flow Graph Visualization:
- Generate and render CFGs using Graphviz-compatible DOT files.
- Abstract Syntax Tree (AST) Construction:
- Build ASTs from bytecode to enable higher-level program understanding.
- Customizable Graph Rendering:
- Flexible, trait-based rendering for both pre-processed and post-processed CFGs.
- Modular Design:
- Highly extensible and idiomatic Rust library design.
Add gbf_core
as a dependency in your Cargo.toml
:
[dependencies]
gbf_core = "0.0.3"
This project supports Rust 1.70.0 and later.
Decompiling GS2 Bytecode is easy and extremely customizable in gbf_core
. We can do so by loading a GS2 module and invoking the FunctionDecompiler
, like so:
use std::{fs::File, io::Read, path::Path};
use gbf_core::decompiler::{
ast::visitors::emit_context::{EmitContextBuilder, EmitVerbosity, IndentStyle},
function_decompiler::FunctionDecompiler,
};
fn load_bytecode(name: &str) -> Result<impl Read, std::io::Error> {
let path = Path::new("tests").join("gs2bc").join(name);
let file = File::open(path)?;
Ok(file)
}
fn main() {
// Load `simple.gs2bc` bytecode file
let reader = load_bytecode("simple.gs2bc").unwrap();
let module = gbf_core::module::ModuleBuilder::new()
.name("simple.gs2".to_string())
.reader(Box::new(reader))
.build()
.unwrap();
// Get the first function in the module
let function = module.get(0).unwrap();
// Configure the emitter to use specific formatting
let context = EmitContextBuilder::default()
.verbosity(EmitVerbosity::Pretty)
.format_number_hex(true)
.indent_style(IndentStyle::Allman)
.build();
// Invoke the `FunctionDecompiler`
let mut decompiler = FunctionDecompiler::new(function.clone()).unwrap();
let decompiled = decompiler.decompile(context).unwrap();
println!("{}", decompiled);
}
This is the resulting GS2 decompilation for simple.gs2bc
:
player.chat = "Hello, World!";
echo("This is a test");
temp.x = 0x0;
temp.x += 0x2;
echo(temp.x);
This library can generate directed control flow graphs using Graphviz. We can load arbitrary GS2 bytecode using the following Rust code:
use std::{fs::File, io::Read, path::Path};
use gbf_core::{
cfg_dot::{CfgDotConfig, DotRenderableGraph},
};
fn load_bytecode(name: &str) -> Result<impl Read, std::io::Error> {
let path = Path::new("tests").join("gs2bc").join(name);
let file = File::open(path)?;
Ok(file)
}
fn main() {
// Load `switch.gs2bc` bytecode file
let reader = load_bytecode("switch.gs2bc").unwrap();
let module = gbf_core::module::ModuleBuilder::new()
.name("switch.gs2".to_string())
.reader(Box::new(reader))
.build()
.unwrap();
// Get the first function in the module. The "entry"
// function is 0, so this really is the "first"
// function.
let function = module.get(1).unwrap();
println!("{}", function.render_dot(CfgDotConfig::default()));
}
As an example, consider this simple GS2 switch statement.
function switchWithMultipleCasesPerNode() {
temp.server = "classicplus";
switch (temp.server) {
case "classic":
case "classicplus":
this.loginserver = "loginclassic1.graalonline.com:14900";
break;
case "delteria":
case "delteriaplus":
this.loginserver = "logindelteria1.graalonline.com:14900";
break;
case "foo":
this.loginserver = "loginfoo1.graalonline.com:14900";
break;
default:
this.loginserver = "loginserver.graalonline.com:14900";
break;
}
temp.i = this.loginserver.pos(":");
this.loginhost = this.loginserver.substring(0, temp.i);
this.loginport = this.loginserver.substring(temp.i + 1, 255);
}
The resulting Graphviz code that gbf_core
generates will look like this when exported:
To export the resulting Graphviz code, you can use the popular dot
utility like so:
$ dot -Tpng cfg.dot -o cfg.png
The library can also be used to manually build ASTs. Not only can you access the AST from the decompiled output, there are helper functions you can use to define your own AST for testing purposes:
use gbf_core::decompiler::ast::{emit, member_access, new_id, new_str, statement, AstNodeError};
fn build_player_chat() -> Result<String, AstNodeError> {
// player.chat = "Hello, world!";
let stmt = statement(
member_access(new_id("player"), new_id("chat"))?,
new_str("Hello, world!"),
);
Ok(emit(stmt))
}
fn main() -> Result<(), AstNodeError> {
let result = build_player_chat()?;
println!("{}", result);
Ok(())
}
- Rust 1.70.0 or later
- Graphviz (optional, for rendering CFGs)
To build the library:
$ cargo build
To run tests:
$ cargo test
To check formatting:
$ cargo fmt --all -- --check
To lint the code:
$ cargo clippy --workspace --all-targets -- -D warnings
Generate and view the documentation locally
$ cargo doc --no-deps --workspace
Contributions are welcome! If you’d like to contribute:
- Fork the repository.
- Create a new branch (
git checkout -b feature-branch
). - Commit your changes (
git commit -m "Add a new feature"
). - Push the branch (
git push origin feature-branch
). - Open a pull request.
Please ensure your code passes all tests and adheres to the project’s style guidelines.
This project is licensed under the Mozilla Public License v2.0 - see the LICENSE file for details.
- Graphviz is used to make human-readable directed graphs for debugging purposes.
- petgraph is used to implement the directed graph functionality.
If you have any questions or feedback, feel free to reach out via the repository's issues section.
Happy hacking!