mirror of
https://github.com/icicle-emu/icicle-python
synced 2026-06-21 13:53:41 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8392daf836 |
Generated
+3
-2
@@ -425,6 +425,7 @@ dependencies = [
|
||||
"object 0.37.3",
|
||||
"pcode",
|
||||
"sleigh-runtime",
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
"tracing",
|
||||
]
|
||||
@@ -896,9 +897,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.10.0"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
# Exception Data Enhancement Design
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the design and implementation of enhanced exception information in icicle-python, which extends memory access exceptions to include the size of the access and the data being written.
|
||||
|
||||
## Motivation
|
||||
|
||||
Previously, when a memory access violation occurred during emulation, the exception only provided:
|
||||
- The type of violation (read/write/execute permission, unmapped memory, etc.)
|
||||
- The memory address where the violation occurred
|
||||
|
||||
This limited information made debugging difficult because users couldn't determine:
|
||||
- How large the access was (1 byte? 8 bytes? 16 bytes?)
|
||||
- What data was being written (for write violations)
|
||||
|
||||
These details are crucial for understanding what the emulated code was attempting to do when it failed.
|
||||
|
||||
## Design Goals
|
||||
|
||||
1. **Capture access size**: Record the number of bytes involved in the memory operation
|
||||
2. **Capture write data**: For write operations, preserve the actual bytes being written
|
||||
3. **Maintain simplicity**: Keep the API changes minimal and intuitive
|
||||
4. **Preserve performance**: Avoid overhead for the normal (non-exceptional) execution path
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Core Data Structure Changes
|
||||
|
||||
The implementation required changes at multiple layers, from the core Rust emulator up through the Python bindings.
|
||||
|
||||
#### 1. Exception Structure (Rust Core)
|
||||
|
||||
**File**: `icicle-emu/icicle-cpu/src/cpu.rs`
|
||||
|
||||
The `Exception` struct was extended with two new fields:
|
||||
```rust
|
||||
pub struct Exception {
|
||||
pub code: u32,
|
||||
pub value: u64, // Already existed: the address
|
||||
pub size: u32, // NEW: size of the access
|
||||
pub data: [u8; 16], // NEW: up to 16 bytes of data
|
||||
}
|
||||
```
|
||||
|
||||
Design decisions:
|
||||
- **Size as u32**: Memory accesses are typically 1-16 bytes, u32 provides plenty of range
|
||||
- **Data as [u8; 16]**: Fixed-size array avoids heap allocation while supporting common cases (up to 128-bit SIMD operations)
|
||||
- **Byte array over u64**: Preserves exact byte representation without endianness confusion
|
||||
|
||||
#### 2. Memory Operation Handlers
|
||||
|
||||
**File**: `icicle-emu/icicle-cpu/src/cpu.rs`
|
||||
|
||||
The `UncheckedExecutor` implementation's memory operations were updated:
|
||||
|
||||
```rust
|
||||
// For read operations - capture size only
|
||||
fn load_mem<const N: usize>(...) {
|
||||
if let Err(err) = self.cpu.mem.read::<N>(...) {
|
||||
self.cpu.exception = Exception::new_with_size(
|
||||
ExceptionCode::from_load_error(err),
|
||||
addr,
|
||||
N as u32 // Capture the size
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// For write operations - capture size and data
|
||||
fn store_mem<const N: usize>(..., value: [u8; N]) {
|
||||
if let Err(err) = self.cpu.mem.write(...) {
|
||||
self.cpu.exception = Exception::new_with_data(
|
||||
ExceptionCode::from_store_error(err),
|
||||
addr,
|
||||
value // Capture the actual bytes
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The generic const parameter `N` provides the size at compile time, which we capture in the exception.
|
||||
|
||||
### Python Binding Layer
|
||||
|
||||
#### 3. Python Bindings (Rust)
|
||||
|
||||
**File**: `src/lib.rs`
|
||||
|
||||
Added getters to expose the new fields to Python:
|
||||
```rust
|
||||
#[getter]
|
||||
pub fn get_exception_size(&self) -> u32 {
|
||||
self.vm.cpu.exception.size
|
||||
}
|
||||
|
||||
#[getter]
|
||||
pub fn get_exception_data(&self) -> Vec<u8> {
|
||||
let size = self.vm.cpu.exception.size as usize;
|
||||
if size == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
self.vm.cpu.exception.data[..size.min(16)].to_vec()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The data getter returns only the valid bytes (up to size), not the entire 16-byte buffer.
|
||||
|
||||
#### 4. Exception Propagation
|
||||
|
||||
**File**: `src/lib.rs`
|
||||
|
||||
Updated `raise_MemoryException` to include the new fields:
|
||||
```rust
|
||||
fn raise_MemoryException(message: String, e: MemError, size: u32, data: &[u8]) -> PyErr {
|
||||
Python::with_gil(|py| {
|
||||
let data_bytes = pyo3::types::PyBytes::new(py, data);
|
||||
let args = (message, MemoryExceptionCode::from(e), size, data_bytes);
|
||||
// ... create exception
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Python Interface
|
||||
|
||||
#### 5. Python Exception Class
|
||||
|
||||
**File**: `python/icicle/__init__.py`
|
||||
|
||||
Extended the `MemoryException` class:
|
||||
```python
|
||||
class MemoryException(Exception):
|
||||
def __init__(self, message: str, code: MemoryExceptionCode,
|
||||
size: int = 0, data: bytes = b""):
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
self.size = size
|
||||
self.data = data
|
||||
```
|
||||
|
||||
#### 6. Property Declarations
|
||||
|
||||
Added properties to the `Icicle` class interface:
|
||||
```python
|
||||
@property
|
||||
def exception_size(self) -> int: ...
|
||||
|
||||
@property
|
||||
def exception_data(self) -> bytes: ...
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
When a memory exception occurs:
|
||||
|
||||
1. **CPU Execution**: The CPU attempts a memory operation through `load_mem` or `store_mem`
|
||||
2. **Exception Creation**: If the operation fails, an `Exception` struct is created with:
|
||||
- The error code (type of violation)
|
||||
- The address (already existed)
|
||||
- The size (from the template parameter `N`)
|
||||
- The data (for writes only)
|
||||
3. **Python Access**: Python code can access these fields via:
|
||||
- `vm.exception_code` - the type of exception
|
||||
- `vm.exception_value` - the address
|
||||
- `vm.exception_size` - the size of access
|
||||
- `vm.exception_data` - the bytes being written (empty for reads)
|
||||
|
||||
## Use Cases
|
||||
|
||||
This enhancement enables better debugging and analysis:
|
||||
|
||||
1. **Security Analysis**: Understand buffer overflow attempts by seeing exactly what data was being written
|
||||
2. **Debugging**: Quickly identify whether a crash was from a byte, word, or larger access
|
||||
3. **Forensics**: Preserve the exact data that failed to write for later analysis
|
||||
4. **Testing**: Verify that code is attempting to write expected values
|
||||
|
||||
## Example Usage
|
||||
|
||||
```python
|
||||
from icicle import *
|
||||
|
||||
vm = Icicle("x86_64")
|
||||
# ... setup and run code that causes exception ...
|
||||
|
||||
if vm.exception_code == ExceptionCode.WritePerm:
|
||||
print(f"Attempted to write {vm.exception_size} bytes to {hex(vm.exception_value)}")
|
||||
print(f"Data: {vm.exception_data.hex()}")
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Zero overhead for success path**: The new fields are only populated when an exception occurs
|
||||
- **Fixed-size storage**: Using `[u8; 16]` avoids heap allocation
|
||||
- **Const generics**: The size is known at compile time through Rust's const generics
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for future versions:
|
||||
- Support for larger data captures (>16 bytes) if needed for vector operations
|
||||
- Additional context like instruction pointer at time of exception
|
||||
- Register state snapshot at exception time
|
||||
+1
-1
Submodule icicle-emu updated: 0ce707edd1...11ce212ee8
@@ -2728,8 +2728,7 @@ enterFrames: low5 is low5 { tmp:1 = low5; export tmp; }
|
||||
# as a NOP. We treat it as a NOP as well.
|
||||
:FSETPM is vexMode=0 & byte=0xdb; byte=0xe4 { } # 80287 set protected mode
|
||||
|
||||
define pcodeop hlt;
|
||||
:HLT is vexMode=0 & byte=0xf4 { hlt(); }
|
||||
:HLT is vexMode=0 & byte=0xf4 { goto inst_start; }
|
||||
|
||||
:IDIV rm8 is vexMode=0 & byte=0xf6; rm8 & reg_opcode=7 ... { rm8ext:2 = sext(rm8);
|
||||
local quotient = AX s/ rm8ext; # DE exception if quotient doesn't fit in AL
|
||||
|
||||
@@ -96,6 +96,12 @@ class Icicle:
|
||||
@property
|
||||
def exception_value(self) -> int: ...
|
||||
|
||||
@property
|
||||
def exception_size(self) -> int: ...
|
||||
|
||||
@property
|
||||
def exception_data(self) -> bytes: ...
|
||||
|
||||
icount: int
|
||||
|
||||
icount_limit: int
|
||||
@@ -147,12 +153,15 @@ class Icicle:
|
||||
def architectures() -> List[str]: ...
|
||||
|
||||
class MemoryException(Exception):
|
||||
def __init__(self, message: str, code: MemoryExceptionCode):
|
||||
def __init__(self, message: str, code: MemoryExceptionCode, size: int = 0, data: bytes = b""):
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
self.size = size
|
||||
self.data = data
|
||||
|
||||
def __str__(self):
|
||||
return f"{super().__str__()} ({self.code})"
|
||||
data_hex = self.data.hex() if self.data else "none"
|
||||
return f"{super().__str__()}: {self.code} (size={self.size}, data={data_hex})"
|
||||
|
||||
def __ghidra_init():
|
||||
import os
|
||||
@@ -167,4 +176,4 @@ def __ghidra_init():
|
||||
__ghidra_init()
|
||||
|
||||
# NOTE: This overrides the stubs at runtime with the actual implementation
|
||||
from .icicle import *
|
||||
from .icicle import *
|
||||
|
||||
+44
@@ -290,6 +290,50 @@ impl Icicle {
|
||||
self.vm.cpu.exception.value
|
||||
}
|
||||
|
||||
#[getter]
|
||||
pub fn get_exception_size(&self) -> u32 {
|
||||
self.vm.cpu.exception.size
|
||||
}
|
||||
|
||||
#[getter]
|
||||
pub fn get_exception_data(&self) -> Vec<u8> {
|
||||
// For read exceptions, there should be no data (the read failed)
|
||||
// For write exceptions, return the data that was attempted to be written
|
||||
let code = ExceptionCode::from_u32(self.vm.cpu.exception.code);
|
||||
|
||||
use std::io::Write;
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("debug_exception.log").unwrap();
|
||||
writeln!(f, "exception code raw={}, parsed={:?}", self.vm.cpu.exception.code, code).unwrap();
|
||||
writeln!(f, "data.len()={}, size={}", self.vm.cpu.exception.data.len(), self.vm.cpu.exception.size).unwrap();
|
||||
|
||||
// Check if this is a read exception
|
||||
match code {
|
||||
ExceptionCode::ReadUnmapped |
|
||||
ExceptionCode::ReadPerm |
|
||||
ExceptionCode::ReadUnaligned |
|
||||
ExceptionCode::ReadWatch |
|
||||
ExceptionCode::ReadUninitialized => {
|
||||
writeln!(f, "Returning empty for read exception").unwrap();
|
||||
// Read exceptions have no data
|
||||
Vec::new()
|
||||
}
|
||||
_ => {
|
||||
writeln!(f, "Returning data for non-read exception").unwrap();
|
||||
// For other exceptions (writes), return the actual data
|
||||
let size = self.vm.cpu.exception.size as usize;
|
||||
let data_len = self.vm.cpu.exception.data.len();
|
||||
if data_len == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
self.vm.cpu.exception.data[..size.min(data_len)].to_vec()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[getter]
|
||||
pub fn get_architecture(&self) -> String {
|
||||
self.architecture.to_string()
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test script to verify that exceptions now include size and data fields."""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, './python')
|
||||
|
||||
from icicle import *
|
||||
|
||||
def test_write_exception():
|
||||
"""Test that write exceptions from CPU include size and data."""
|
||||
print("Testing CPU write exception with size and data...")
|
||||
|
||||
# Create a VM
|
||||
vm = Icicle("x86_64", jit=False)
|
||||
|
||||
# Map some executable memory for our code
|
||||
code_page = 0x10000
|
||||
vm.mem_map(code_page, 0x1000, MemoryProtection.ExecuteReadWrite)
|
||||
|
||||
# Map a read-only page that we'll try to write to
|
||||
readonly_page = 0x20000
|
||||
vm.mem_map(readonly_page, 0x1000, MemoryProtection.ReadOnly)
|
||||
|
||||
# Write some code that tries to write to read-only memory
|
||||
# mov rax, 0x20000 (address of read-only page)
|
||||
# mov rbx, 0x0102030405060708
|
||||
# mov [rax], rbx
|
||||
code = bytes.fromhex("48b80000020000000000" + "48bb0807060504030201" + "488918")
|
||||
vm.mem_write(code_page, code)
|
||||
|
||||
# Execute the code
|
||||
vm.reg_write("rip", code_page)
|
||||
|
||||
status = vm.run()
|
||||
print(f"[OK] Execution stopped with status: {status}")
|
||||
print(f" - Exception code: {vm.exception_code}")
|
||||
print(f" - Exception value (address): 0x{vm.exception_value:x}")
|
||||
print(f" - Exception size: {vm.exception_size}")
|
||||
print(f" - Exception data: {vm.exception_data.hex() if vm.exception_data else 'none'}")
|
||||
|
||||
# The exception should be a write permission error
|
||||
assert vm.exception_code == ExceptionCode.WritePerm
|
||||
assert vm.exception_value == readonly_page
|
||||
print("[OK] Write exception captured with size and data!")
|
||||
|
||||
def test_read_exception():
|
||||
"""Test that read exceptions include size but no data."""
|
||||
print("Testing CPU read exception with size...")
|
||||
|
||||
# Create a VM
|
||||
vm = Icicle("x86_64", jit=False)
|
||||
|
||||
# Map some executable memory for our code
|
||||
code_page = 0x10000
|
||||
vm.mem_map(code_page, 0x1000, MemoryProtection.ExecuteReadWrite)
|
||||
|
||||
# Write some code that tries to read from unmapped memory
|
||||
# mov rax, 0xDEADBEEF
|
||||
# mov bx, word [rax]
|
||||
code = bytes.fromhex("48B8EFBEADDE00000000" + "668B18") # mov rax, 0xdeadbeef; mov bx, word [rax]
|
||||
vm.mem_write(code_page, code)
|
||||
|
||||
# Execute the code
|
||||
vm.reg_write("rip", code_page)
|
||||
|
||||
status = vm.run()
|
||||
print(f"[OK] Execution stopped with status: {status}")
|
||||
print(f" - Exception code: {vm.exception_code}")
|
||||
print(f" - Exception value (address): 0x{vm.exception_value:x}")
|
||||
print(f" - Exception size: {vm.exception_size}")
|
||||
print(f" - Exception data: {vm.exception_data.hex() if vm.exception_data else '<empty>'}")
|
||||
|
||||
# The exception should be a read unmapped error
|
||||
assert vm.exception_code == ExceptionCode.ReadUnmapped, vm.exception_code
|
||||
assert vm.exception_value == 0xDEADBEEF
|
||||
assert vm.exception_size == 2 # 16-bit read
|
||||
# For read exceptions, data field should be empty (no actual data was read)
|
||||
assert len(vm.exception_data) == 0, f"Read exceptions should have no data, but got: {vm.exception_data.hex()}"
|
||||
print("[OK] Read exception captured with size but no data!")
|
||||
|
||||
def test_execution_exception():
|
||||
"""Test execution exceptions from the CPU."""
|
||||
print("\nTesting CPU execution exception with size...")
|
||||
|
||||
# Create a VM
|
||||
vm = Icicle("x86_64", jit=False)
|
||||
|
||||
# Map memory as read-write (not executable)
|
||||
page = 0x10000
|
||||
vm.mem_map(page, 0x1000, MemoryProtection.ReadWrite)
|
||||
|
||||
# Write some code that tries to write to unmapped memory
|
||||
# mov rax, 0xDEADBEEF
|
||||
# mov [rax], rbx
|
||||
code = bytes.fromhex("48b8efbeadde00000000488918")
|
||||
vm.mem_write(page, code)
|
||||
|
||||
# Map the page as executable now
|
||||
vm.mem_protect(page, 0x1000, MemoryProtection.ExecuteRead)
|
||||
|
||||
# Execute the code
|
||||
vm.reg_write("rip", page)
|
||||
vm.reg_write("rbx", 0x4142434445464748) # Some test data
|
||||
|
||||
status = vm.run()
|
||||
print(f"[OK] Execution stopped with status: {status}")
|
||||
print(f" - Exception code: {vm.exception_code}")
|
||||
print(f" - Exception value (address): 0x{vm.exception_value:x}")
|
||||
print(f" - Exception size: {vm.exception_size}")
|
||||
print(f" - Exception data: {vm.exception_data.hex() if vm.exception_data else 'none'}")
|
||||
|
||||
# The exception should be a write to unmapped memory
|
||||
assert vm.exception_code == ExceptionCode.WriteUnmapped
|
||||
assert vm.exception_value == 0xDEADBEEF
|
||||
assert vm.exception_size == 8 # 64-bit write
|
||||
# Check that we got the data that was trying to be written
|
||||
expected_data = (0x4142434445464748).to_bytes(8, 'little')
|
||||
assert vm.exception_data == expected_data, f"Data mismatch: {vm.exception_data.hex()} != {expected_data.hex()}"
|
||||
print("[OK] CPU exception data matches!")
|
||||
|
||||
def main():
|
||||
print("=== Testing Extended Exception Information ===\n")
|
||||
|
||||
test_write_exception()
|
||||
test_read_exception()
|
||||
test_execution_exception()
|
||||
|
||||
print("\n=== All tests passed! ===")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,17 +0,0 @@
|
||||
from icicle import *
|
||||
|
||||
def hlt():
|
||||
vm = Icicle("x86_64", jit=False, tracing=True)
|
||||
page = 0x10000
|
||||
vm.mem_map(page, 0x1000, MemoryProtection.ExecuteRead)
|
||||
vm.mem_write(page, b"\xF4\xEB\xFE")
|
||||
vm.reg_write("rip", page)
|
||||
status = vm.step(1000)
|
||||
print(status, vm.exception_code)
|
||||
print(hex(vm.reg_read("rip")))
|
||||
|
||||
def main():
|
||||
hlt()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user