From f772957eb1d676a33504fa857ff48b90370b6405 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Thu, 1 Jun 2023 10:17:38 +0200 Subject: [PATCH] Initial version of the python module working --- .github/workflows/CI.yml | 120 +++++++++++++++++++++++++++++++++ .gitignore | 4 +- Cargo.lock | 138 +++++++++++++++++++++++++++++++++++++- Cargo.toml | 13 +++- README.md | 27 ++++++++ build.rs | 4 ++ icicle-emu | 2 +- icicle.pyi | 29 ++++++++ pyproject.toml | 15 +++++ requirements.txt | 1 + src/lib.rs | 141 +++++++++++++++++++++++++++++++++++++++ tests/example.py | 19 ++++++ 12 files changed, 507 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/CI.yml create mode 100644 README.md create mode 100644 build.rs create mode 100644 icicle.pyi create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 src/lib.rs create mode 100644 tests/example.py diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..f5e11f3 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,120 @@ +# This file is autogenerated by maturin v1.0.1 +# To update, run +# +# maturin generate-ci github +# +name: CI + +on: + push: + branches: + - main + - master + tags: + - '*' + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64, x86, aarch64, armv7, s390x, ppc64le] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + manylinux: auto + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + windows: + runs-on: windows-latest + strategy: + matrix: + target: [x64, x86] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + architecture: ${{ matrix.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + macos: + runs-on: macos-latest + strategy: + matrix: + target: [x86_64, aarch64] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + release: + name: Release + runs-on: ubuntu-latest + if: "startsWith(github.ref, 'refs/tags/')" + needs: [linux, windows, macos, sdist] + steps: + - uses: actions/download-artifact@v3 + with: + name: wheels + - name: Publish to PyPI + uses: PyO3/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + with: + command: upload + args: --skip-existing * diff --git a/.gitignore b/.gitignore index 01d51c4..6581da3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .idea/ target/ debug/ -**/*.rs.bk \ No newline at end of file +**/*.rs.bk +venv*/ +.env/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d88a6a4..6dbdaa5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -362,6 +362,9 @@ dependencies = [ "icicle-cpu", "icicle-vm", "pcode", + "pyo3", + "pyo3-build-config", + "target-lexicon", ] [[package]] @@ -392,12 +395,28 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indoc" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" + [[package]] name = "libc" version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -469,6 +488,29 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + [[package]] name = "pcode" version = "0.2.0" @@ -498,6 +540,66 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pyo3" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b1ac5b3731ba34fdaa9785f8d74d17448cd18f30cf19e0c7e7b1fdb5272109" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb946f5ac61bb61a5014924910d936ebd2b23b705f7a4a3c40b05c720b079a3" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd4d7c5337821916ea2a1d21d1092e8443cf34879e53a0ac653fbb98f44ff65c" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d39c55dab3fc5a4b25bbd1ac10a2da452c4aca13bb450f22818a002e29648d" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97daff08a4c48320587b5224cc98d609e3c27b6d437315bd40b605c98eeb5918" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.28" @@ -507,6 +609,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regalloc2" version = "0.6.1" @@ -548,6 +659,12 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "1.0.163" @@ -577,7 +694,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] @@ -626,6 +743,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.18" @@ -660,7 +788,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] @@ -696,6 +824,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +[[package]] +name = "unindent" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 448cfa9..c385fb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,20 @@ +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + [package] name = "icicle-unicorn" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "icicle" +crate-type = ["cdylib"] [dependencies] icicle-cpu = { path = "icicle-emu/icicle-cpu" } icicle-vm = { path = "icicle-emu/icicle-vm" } -pcode = { path = "icicle-emu/sleigh/pcode" } \ No newline at end of file +pcode = { path = "icicle-emu/sleigh/pcode" } +pyo3 = { version = "0.18.3", features = ["extension-module"] } +target-lexicon = "0.12.7" + +[build-dependencies] +pyo3-build-config = "0.18.3" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..810c79c --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# icicle-unicorn + +This project is an attempt to replace unicorn with a wrapper around [icicle-emu](https://github.com/icicle-emu/icicle-emu). + +## Requirements + +- [Python 3.7+](https://www.python.org/downloads/) +- [Rust 1.48](https://rustup.rs/) + +## Building + +Set up a virtual environment: + +```shell +python -m venv venv +# macOS/Linux +source venv/bin/activate +# Windows +venv\Scripts\activate.bat +``` + +Get the dependencies and build: + +```shell +pip install -r requirements.txt +maturin develop +``` \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..d0456a7 --- /dev/null +++ b/build.rs @@ -0,0 +1,4 @@ +// Reference: https://pyo3.rs/v0.18.3/building_and_distribution#macos +fn main() { + pyo3_build_config::add_extension_module_link_args(); +} \ No newline at end of file diff --git a/icicle-emu b/icicle-emu index 9c97c94..10311d3 160000 --- a/icicle-emu +++ b/icicle-emu @@ -1 +1 @@ -Subproject commit 9c97c94c8ceade509877a763f94c6edb441c1351 +Subproject commit 10311d36d4362f5126108705368e9cb89a6b9156 diff --git a/icicle.pyi b/icicle.pyi new file mode 100644 index 0000000..1620904 --- /dev/null +++ b/icicle.pyi @@ -0,0 +1,29 @@ +from typing import List +from enum import Enum + +class MemoryProtection(Enum): + NoAccess = 0 + ReadOnly = 1 + ReadWrite = 2 + ExecuteRead = 3 + ExecuteReadWrite = 4 + +class Icicle: + """ + The icicle virtual machine. + + :param architecture: Name of the architecture to emulate. + """ + def __init__(self, architecture: str) -> None: ... + + def mem_map(self, address: int, size: int, protection: MemoryProtection): ... + + def mem_read(self, address: int, size: int) -> bytes: ... + + def mem_write(self, address: int, data: bytes) -> None: ... + +def architectures() -> List[str]: + """ + List all the available architectures. + """ + ... diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a8ecb6a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[project] +name = "icicle-emu" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] + +[tool.maturin] +features = ["pyo3/extension-module"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a123712 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +maturin==1.0.1 \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..32bd2fe --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,141 @@ +mod rawenv; + +use std::borrow::Cow; +use std::fmt::format; +use icicle_cpu::mem::{Mapping, perm}; +use pyo3::prelude::*; +use icicle_vm; +use icicle_vm::{BuildError, Vm}; +use pyo3::types::PyList; +use target_lexicon; + +// References: +// - https://pyo3.rs/main/conversions/tables +// - https://pyo3.rs/main/class + +#[pyclass(unsendable)] +struct Icicle { + vm: icicle_vm::Vm, +} + +#[pyclass] +#[derive(Clone)] +enum MemoryProtection { + NoAccess, + ReadOnly, + ReadWrite, + ExecuteRead, + ExecuteReadWrite, +} + +#[pymethods] +impl Icicle { + #[new] + fn new(architecture: String) -> PyResult { + if architecture.split("-").count() != 1 { + return Err( + pyo3::exceptions::PyException::new_err(format!("Bad architecture format: {architecture}")) + ); + } + // Setup the CPU state for the target triple + let mut cpu_config = icicle_vm::cpu::Config::from_target_triple( + format!("{architecture}-none").as_str() + ); + if cpu_config.triple.architecture == target_lexicon::Architecture::Unknown { + return Err( + pyo3::exceptions::PyException::new_err(format!("Unknown architecture: {architecture}")) + ); + } + + // TODO: allow these to be customized + cpu_config.enable_jit = true; + cpu_config.enable_recompilation = false; + + let mut vm = icicle_vm::build(&cpu_config) + .map_err(|e| { + pyo3::exceptions::PyException::new_err(format!("VM build error: {e}")) + })?; + + Ok(Icicle { + vm, + }) + } + + fn __str__(&mut self) -> String { + let arch = &self.vm.cpu.arch; + let endianness = if arch.sleigh.big_endian { + "big endian" + } else { + "little endian" + }; + format!("Icicle VM for {0:?} ({endianness})", arch.triple.architecture) + } + + fn mem_map(&mut self, address: u64, size: u64, protection: MemoryProtection) -> PyResult<()> { + let mapping = Mapping { + perm: match protection { + MemoryProtection::ReadOnly => perm::READ, + MemoryProtection::ReadWrite => perm::READ | perm::WRITE, + MemoryProtection::ExecuteRead => perm::EXEC | perm::READ, + MemoryProtection::ExecuteReadWrite => perm::EXEC | perm::READ | perm::WRITE, + }, + value: 0, + }; + if self.vm.cpu.mem.map_memory_len(address, size, mapping) { + Ok(()) + } else { + Err( + pyo3::exceptions::PyException::new_err(format!("Failed to map memory {address:X}[{size:X}]")) + ) + } + } + + fn mem_protect(&mut self, address: u64, size: usize, protection: MemoryProtection) { + + } + + fn mem_read(&mut self, address: u64, size: usize) -> PyResult> { + // Allocate a buffer + let mut buffer = Vec::with_capacity(size); + buffer.resize(size, 0); + + // Read the memory + self.vm.cpu.mem.read_bytes(address, &mut buffer[..], perm::NONE) + .map_err(|e| { + pyo3::exceptions::PyException::new_err( + format!("Failed to read memory {address:X}[{size:X}]: {e}") + ) + })?; + return Ok(Cow::Owned(buffer)); + } + + fn mem_write(&mut self, address: u64, data: Vec) -> PyResult<()> { + let size = data.len(); + self.vm.cpu.mem.write_bytes(address, &data[..], perm::NONE) + .map_err(|e| { + pyo3::exceptions::PyException::new_err( + format!("Failed to write memory {address:X}[{size:X}]: {e}") + ) + }) + } +} + +#[pyfunction] +fn architectures() -> PyResult> { + Ok(vec![ + "i686", + "x86_64", + "aarch64", + ]) +} + +/// A Python module implemented in Rust. The name of this function must match +/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to +/// import the module. +#[pymodule] +fn icicle(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(architectures, m)?)?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} \ No newline at end of file diff --git a/tests/example.py b/tests/example.py new file mode 100644 index 0000000..60d1ce3 --- /dev/null +++ b/tests/example.py @@ -0,0 +1,19 @@ +import icicle + +def main(): + print("Hello world!") + print(dir(icicle)) + print(icicle.MemoryProtection.ReadOnly) + print(f"Architectures: {icicle.architectures()}") + vm = icicle.Icicle("i686") + + addr = 0x10000 + vm.mem_map(addr, 0x1000, icicle.MemoryProtection.ExecuteRead) + vm.mem_write(addr, b"\x90\x90\x90") + data = vm.mem_read(addr, 4) + print(data, type(data)) + + print(vm) + +if __name__ == "__main__": + main()