1 Commits

Author SHA1 Message Date
Duncan Ogilvie e62b6a9622 Add exception for hlt instruction 2025-10-26 12:23:37 +01:00
8 changed files with 25 additions and 393 deletions
Generated
+2 -3
View File
@@ -425,7 +425,6 @@ dependencies = [
"object 0.37.3",
"pcode",
"sleigh-runtime",
"smallvec",
"target-lexicon",
"tracing",
]
@@ -897,9 +896,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.15.1"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "stable_deref_trait"
-201
View File
@@ -1,201 +0,0 @@
# 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
@@ -2728,7 +2728,8 @@ 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
:HLT is vexMode=0 & byte=0xf4 { goto inst_start; }
define pcodeop hlt;
:HLT is vexMode=0 & byte=0xf4 { hlt(); }
: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
+3 -12
View File
@@ -96,12 +96,6 @@ 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
@@ -153,15 +147,12 @@ class Icicle:
def architectures() -> List[str]: ...
class MemoryException(Exception):
def __init__(self, message: str, code: MemoryExceptionCode, size: int = 0, data: bytes = b""):
def __init__(self, message: str, code: MemoryExceptionCode):
super().__init__(message)
self.code = code
self.size = size
self.data = data
def __str__(self):
data_hex = self.data.hex() if self.data else "none"
return f"{super().__str__()}: {self.code} (size={self.size}, data={data_hex})"
return f"{super().__str__()} ({self.code})"
def __ghidra_init():
import os
@@ -176,4 +167,4 @@ def __ghidra_init():
__ghidra_init()
# NOTE: This overrides the stubs at runtime with the actual implementation
from .icicle import *
from .icicle import *
-44
View File
@@ -290,50 +290,6 @@ 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()
-131
View File
@@ -1,131 +0,0 @@
#!/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()
+17
View File
@@ -0,0 +1,17 @@
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()