mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-09-07 09:11:15 -09:00
GP-7021: Add tenet++ pin tracer and docs
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
##VERSION: 2.0
|
||||
Module.manifest||GHIDRA||||END|
|
||||
README.md||GHIDRA||||END|
|
||||
data/TenetPlusPlus_PinTool/Makefile||GHIDRA||||END|
|
||||
data/TenetPlusPlus_PinTool/Readme.md||GHIDRA||||END|
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
CONFIG_ROOT := $(PIN_ROOT)/source/tools/Config
|
||||
include $(CONFIG_ROOT)/makefile.config
|
||||
|
||||
TOOL_ROOTS := tenetplusplus
|
||||
|
||||
$(OBJDIR)tenetplusplus$(PINTOOL_SUFFIX): $(OBJDIR)tenetplusplus$(OBJ_SUFFIX)
|
||||
$(LINKER) $(TOOL_LDFLAGS) $(LINK_EXE)$@ $^ $(TOOL_LPATHS) $(TOOL_LIBS)
|
||||
|
||||
include $(TOOLS_ROOT)/Config/makefile.default.rules
|
||||
|
||||
all: $(OBJDIR)tenetplusplus$(PINTOOL_SUFFIX)
|
||||
@@ -0,0 +1,157 @@
|
||||
# Tenet++
|
||||
|
||||
The `tenetplusplus` pintool is an enhancement of the `pintenet` tracer found in
|
||||
the [Tenet](https://github.com/gaasedelen/tenet) repo under the MIT license. With the Tenet++ tracer comes a new format
|
||||
as well to enable some additional features when imported into Ghidra.
|
||||
|
||||
## New Format
|
||||
|
||||
Tenet++ expands the original tenet format to be better suited for additional features added to Ghidra.
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
Loaded image: 0x55b9ab65b000:0x55b9ab660d57 -> /usr/bin/whoami Bytes: 7f454c4602010100000000000000000003003e000100000020270000000000004000000000000000587600000000000000000000400038000d0040001e001d000600000004000000400000000000000040000000000000004000000000000000d802000000000000d80>
|
||||
Loaded image: 0x7f78bd58d000:0x7f78bd5c38a7 -> /lib64/ld-linux-x86-64.so.2 Bytes: 7f454c4602010103000000000000000003003e000100000030ef0100000000004000000000000000684a0e000000000000000000400038000b004000230022000100000004000000000000000000000000000000000000000000000000000000301b000>
|
||||
tid=0,rdi=0x7ffd3cbe5eb0,rsp=0x7ffd3cbe5eb0,rip=0x7f78bd5abf33
|
||||
tid=0,rsp=0x7ffd3cbe5ea8,rip=0x7f78bd5acca0,mw=0x7ffd3cbe5ea8:38bf5abd787f0000
|
||||
tid=0,rdi=0x6fffffff,rbp=0x7ffd3cbe5ea0,rsp=0x7ffd3cbe5e40,rbx=0x6ffffeff,rdx=0x7f78bd5c5e90,rcx=0x7f78bd5c6a70,rax=0xe,r8=0x70000022,r9=0x32,r10=0x6ffffdff,r11=0x6ffffe35,r12=0x7f78bd58d000,r13=0x6fffff41,r15=0x7ffd3cbe5eb0,rip=0x7f78bd5acd2d,ma=0x7f78bd5c5ac0:5ebb5be2a9520000,ma>
|
||||
tid=0,rip=0x7f78bd5acd4a
|
||||
tid=0,rsi=0x4c,rdx=0x7f78bd5c5eb0,rax=0x4c,r14=0xeffffef5,rip=0x7f78bd5ad089,ma=0x7f78bd5c5ea0:0400000000000000,ma=0x7f78bd5c5eb0:f5feff6f00000000,ma=0x7f78bd5c6a90:a05e5cbd787f0000,ma=0x7f78bd5c6ae0:905e5cbd787f0000
|
||||
tid=0,rip=0x7f78bd5acd39
|
||||
tid=0,rsi=0x29,rdx=0x7f78bd5c5f90,rax=0x7f78bd5c5eb0,rip=0x7f78bd5ad114,ma=0x7f78bd5c5ea8:f0d258bd787f0000,ma=0x7f78bd5c5eb8:38d458bd787f0000050000000000000090d958bd787f00000600000000000000a0d558bd787f00000a00000000000000,ma=0x7f78bd5c5ef0:0b00000000000000,ma=0x7f78bd5c5f00:070000>
|
||||
tid=0,rip=0x7f78bd5acdaa
|
||||
```
|
||||
|
||||
### Breakdown
|
||||
|
||||
A single line is a new snapshot/event and can be one of three types:
|
||||
|
||||
#### Instruction execution
|
||||
|
||||
* __tid=[thread id],[register name]=[value]...,m[r/w/a]=[address]:[bytes]...__
|
||||
* __tid=[thread id]__
|
||||
* Records which thread this event happens in
|
||||
* Required for every instruction event
|
||||
* eg. `tid=0`
|
||||
* __[register name]=[value]__
|
||||
* Records register assignments
|
||||
* 0 or more per line
|
||||
* eg. `rdi=0x7ffd3cbe5eb0`
|
||||
* __m[r/w/a]=[address]:[bytes]__
|
||||
* Records memory reads/writes as well as the coalesced memory from the `-rec_all_memory` option.
|
||||
* 0 or more per line
|
||||
* eg. `mw=0x7ffd3cbe5ea8:38bf5abd787f0000`
|
||||
|
||||
#### Image loading
|
||||
|
||||
* __Loaded image: [start address]:[end address] -> [module name] Bytes: [bytes starting from start address]__
|
||||
* Records when images are loaded into memory as well as the bytes that were loaded
|
||||
* eg. `Loaded image: 0x55b9ab65b000:0x55b9ab660d57 -> /usr/bin/whoami Bytes: 7f454c46020101000...`
|
||||
|
||||
#### Image unloading
|
||||
|
||||
* __Unloaded image: [start address]:[end address] -> [module name]__
|
||||
* Records when images are unloaded from memory
|
||||
* eg. `Unloaded image: 0x55b9ab65b000:0x55b9ab660d57 -> /usr/bin/whoami`
|
||||
|
||||
There can be a variable amount of actual time between events based on the recording granularity.
|
||||
|
||||
## Improvements made by Tenet++ pin tracer:
|
||||
|
||||
1. Trace start and stop addresses
|
||||
- These allow the user to precisely trace only certain points of a program
|
||||
2. Trace granularity
|
||||
- These give the user an option to choose how granular the tracer is: Instruction, Block or Routine level
|
||||
3. Record all memory
|
||||
- Even if using routine level tracing, the user can record all memory events that get coalesced and read prior to
|
||||
the
|
||||
next routine event. This ensures the event has an updated snapshot of modified memory
|
||||
4. Record image bytes at image load time
|
||||
5. Record all threads in process in a single trace
|
||||
|
||||
# Usage
|
||||
|
||||
The pintool can be used to trace simple usermode applications on Windows or Linux. To use it, provide the path for a
|
||||
compiled version of `tenetplusplus` to `pin` via the `-t` argument.
|
||||
|
||||
Example usage:
|
||||
|
||||
```
|
||||
C:\pin\pin -t obj-ia32\tenetplusplus.dll -- "C:\Users\user\Desktop\calc.exe"
|
||||
```
|
||||
|
||||
This pintool will generate a single trace containing trace data from all threads.
|
||||
|
||||
## Additional parameters
|
||||
|
||||
* `-s` Start tracing when an [image name]:[image base offset] is hit
|
||||
* Example: `-s kernel32.dll:1234 -s app:2FFF`
|
||||
* `-e ` Stop tracing when an [image name]:[image base offset] is hit
|
||||
* Example: `-e kernel32.dll:1234 -e app:2FFF`
|
||||
* `-g` Trace granularity level, default is 2
|
||||
* `2` Instruction Level Tracing
|
||||
* `4` Basic Block Level Tracing
|
||||
* `8` Routine Level Tracing
|
||||
* `-o` Prefix of the output .trace file, default is `trace`
|
||||
* `-rec_all_memory` Record all memory r/w events regardless of granularity level until next log write
|
||||
* `-w` Add a module to the whitelist. If none is specified, every module is white-listed
|
||||
* Example: `-w calc.exe`, or `-w calc.exe,kernel32.dll`
|
||||
|
||||
# Compilation
|
||||
|
||||
To compile the pintool, you first will need
|
||||
to [download](https://software.intel.com/en-us/articles/pin-a-binary-instrumentation-tool-downloads) and extract Pin.
|
||||
|
||||
Follow the build instructions below for your respective platform.
|
||||
|
||||
## Building for Linux
|
||||
|
||||
On Linux, one can compile the pintool using the following commands.
|
||||
|
||||
```
|
||||
cd <location where tenetplusplus.cpp is>
|
||||
|
||||
# Location where you extracted Pin
|
||||
export PIN_ROOT=~/pin
|
||||
export PATH=$PATH:$PIN_ROOT
|
||||
make TARGET=intel64
|
||||
make TARGET=ia32
|
||||
```
|
||||
|
||||
## Building for Windows
|
||||
|
||||
Install deps for building Pintools:
|
||||
|
||||
- Install Visual Studio Community 2019 Edition
|
||||
from [https://visualstudio.microsoft.com/downloads/](https://visualstudio.microsoft.com/downloads/)
|
||||
- Make sure to install the Desktop development for C++ workload
|
||||
|
||||
- Install GNU's make, version 4.2.1, using Cygwin's 64-bit installer. Cygwin installer link
|
||||
here: [https://cygwin.com/install.html](https://cygwin.com/install.html)
|
||||
|
||||
### Building 32bit
|
||||
|
||||
1. Launch a new CMD window and paste the EXACT following:
|
||||
```
|
||||
set PIN_ROOT=C:\\pin
|
||||
set PATH=%PATH%;C:\cygwin64\bin
|
||||
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars32.bat"
|
||||
```
|
||||
2. Change to the directory containing the `tenetplusplus` source, build the 32bit pin tool:
|
||||
```
|
||||
make TARGET=ia32
|
||||
```
|
||||
|
||||
### Building 64bit
|
||||
|
||||
1. Launch a new CMD window and paste the EXACT following:
|
||||
```
|
||||
set PIN_ROOT=C:\\pin
|
||||
set PATH=%PATH%;C:\cygwin64\bin
|
||||
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat"
|
||||
```
|
||||
2. Change to the directory containing the `tenetplusplus` source, and build the 64bit pin tool:
|
||||
```
|
||||
make TARGET=intel64
|
||||
```
|
||||
@@ -0,0 +1,711 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
// Code is derived from work done by https://github.com/gaasedelen/tenet under the MIT license
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <regex>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "pin.H"
|
||||
|
||||
using std::ofstream;
|
||||
|
||||
#ifdef __i386__
|
||||
constexpr auto PC = "eip";
|
||||
#else
|
||||
constexpr auto PC = "rip";
|
||||
#endif
|
||||
|
||||
#if defined(TARGET_WINDOWS)
|
||||
constexpr char PATH_SEPARATOR = '\\';
|
||||
#else
|
||||
constexpr char PATH_SEPARATOR = '/';
|
||||
#endif
|
||||
|
||||
static KNOB<std::string> KnobModuleWhitelist(
|
||||
KNOB_MODE_APPEND, "pintool", "w", "",
|
||||
"Add a module to the whitelist. If none is specified, every module is "
|
||||
"white-listed. Example: calc.exe");
|
||||
|
||||
static KNOB<std::string> KnobOutputFilePrefix(
|
||||
KNOB_MODE_WRITEONCE, "pintool", "o", "trace",
|
||||
"Prefix of the output .trace file.");
|
||||
|
||||
static KNOB<std::string> KnobStartAt(
|
||||
KNOB_MODE_APPEND, "pintool", "s", "",
|
||||
"Start tracing when an image:offset is hit.\nExample: -s "
|
||||
"kernel32.dll:1234 -s app:2FFF");
|
||||
|
||||
static KNOB<std::string> KnobStopAt(
|
||||
KNOB_MODE_APPEND, "pintool", "e", "",
|
||||
"Stop tracing when an image:offset is hit.\nExample: -e "
|
||||
"kernel32.dll:1234 -e app:2FFF");
|
||||
|
||||
static KNOB<int> KnobTraceGranularity(
|
||||
KNOB_MODE_WRITEONCE, "pintool", "g", "2",
|
||||
"Trace granularity level:\n2=Instruction Level Tracing\n4=Basic Block "
|
||||
"Level Tracing\n8=Routine Level Tracing");
|
||||
|
||||
static KNOB<BOOL> KnobCollectMem(KNOB_MODE_WRITEONCE, "pintool",
|
||||
"rec_all_memory", "0",
|
||||
"Record all memory r/w events regardless of "
|
||||
"granularity level until next log write");
|
||||
|
||||
|
||||
static std::string base_name(const std::string &path) {
|
||||
const std::string::size_type idx = path.rfind(PATH_SEPARATOR);
|
||||
std::string name = (idx == std::string::npos) ? path : path.substr(idx + 1);
|
||||
return name;
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct Image {
|
||||
std::string name_;
|
||||
ADDRINT low_;
|
||||
ADDRINT high_;
|
||||
|
||||
explicit Image(std::string n = "", const ADDRINT low = 0, const ADDRINT high = 0)
|
||||
: name_(std::move(n)), low_(low), high_(high) {
|
||||
}
|
||||
|
||||
// Overloaded method to implement searches over the loaded images list
|
||||
// and also allow this class to be used on a set like STL container.
|
||||
bool operator<(const Image &rhs) const { return low_ < rhs.low_; }
|
||||
};
|
||||
|
||||
class ImageManager {
|
||||
// Set of module names that are allowed to be traced.
|
||||
std::set<Image> images;
|
||||
PIN_RWMUTEX images_lock{};
|
||||
|
||||
// Here we store the names of the images inside our white list.
|
||||
std::set<std::string, std::less<> > whitelist;
|
||||
|
||||
// Store the last recently matched image so we can use it as a cache.
|
||||
ADDRINT cached_low{};
|
||||
ADDRINT cached_high{};
|
||||
|
||||
public:
|
||||
ImageManager() { PIN_RWMutexInit(&images_lock); }
|
||||
|
||||
virtual ~ImageManager() { PIN_RWMutexFini(&images_lock); }
|
||||
|
||||
VOID addWhiteListedImage(const std::string &image_name) {
|
||||
whitelist.insert(image_name);
|
||||
}
|
||||
|
||||
BOOL isWhiteListed(const std::string &image_name) {
|
||||
return whitelist.find(image_name) != whitelist.end();
|
||||
}
|
||||
|
||||
BOOL isInterestingAddress(ADDRINT addr) {
|
||||
PIN_RWMutexReadLock(&images_lock);
|
||||
{
|
||||
// If there is no white-listed image, everything is white-listed.
|
||||
if ((images.empty() && whitelist.empty()) ||
|
||||
(addr >= cached_low && addr < cached_high)) {
|
||||
PIN_RWMutexUnlock(&images_lock);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto i = images.upper_bound(Image("", addr));
|
||||
if (i == images.begin()) {
|
||||
PIN_RWMutexUnlock(&images_lock);
|
||||
return false;
|
||||
}
|
||||
--i;
|
||||
|
||||
// If the instruction address does not fall inside a valid white listed
|
||||
// image, bail out.
|
||||
if (!(i != images.end() && i->low_ <= addr && addr < i->high_)) {
|
||||
PIN_RWMutexUnlock(&images_lock);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save the matched image.
|
||||
cached_low = i->low_;
|
||||
cached_high = i->high_;
|
||||
}
|
||||
PIN_RWMutexUnlock(&images_lock);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
VOID addImage(std::string image_name, ADDRINT lo_addr, ADDRINT hi_addr) {
|
||||
PIN_RWMutexWriteLock(&images_lock);
|
||||
{
|
||||
images.emplace(std::move(image_name), lo_addr, hi_addr);
|
||||
}
|
||||
PIN_RWMutexUnlock(&images_lock);
|
||||
}
|
||||
|
||||
VOID removeImage(const ADDRINT low) {
|
||||
PIN_RWMutexWriteLock(&images_lock);
|
||||
{
|
||||
if (const auto i = images.find(Image("", low)); i != images.end()) {
|
||||
images.erase(i);
|
||||
}
|
||||
}
|
||||
PIN_RWMutexUnlock(&images_lock);
|
||||
}
|
||||
};
|
||||
|
||||
struct ThreadData {
|
||||
std::array<ADDRINT, REG_GR_LAST + 1> m_cpu{};
|
||||
|
||||
ADDRINT mem_w_addr{};
|
||||
ADDRINT mem_w_size{};
|
||||
ADDRINT mem_r_addr{};
|
||||
ADDRINT mem_r_size{};
|
||||
ADDRINT mem_r2_addr{};
|
||||
ADDRINT mem_r2_size{};
|
||||
|
||||
ADDRINT largestSize = 0;
|
||||
|
||||
struct MemEvent {
|
||||
ADDRINT addr = 0;
|
||||
UINT32 size = 0;
|
||||
uint8_t access_flags = 0;
|
||||
|
||||
ADDRINT end() const { return addr + size; }
|
||||
};
|
||||
|
||||
std::map<ADDRINT, MemEvent> mem_events;
|
||||
};
|
||||
|
||||
enum class TraceGranularity : int {
|
||||
TRACE_INSTRUCTION = 2,
|
||||
TRACE_BASIC_BLOCK = 4,
|
||||
TRACE_ROUTINE = 8,
|
||||
};
|
||||
|
||||
class ToolContext {
|
||||
public:
|
||||
explicit ToolContext(const std::string &log_name) {
|
||||
PIN_InitLock(&write_lock);
|
||||
|
||||
tls_key = PIN_CreateThreadDataKey(nullptr);
|
||||
trace_file = ofstream(log_name.c_str());
|
||||
trace_file << std::hex;
|
||||
}
|
||||
|
||||
~ToolContext() {
|
||||
trace_file.close();
|
||||
}
|
||||
|
||||
ThreadData *GetThreadLocalData(const THREADID tid) const {
|
||||
return static_cast<ThreadData *>(PIN_GetThreadData(tls_key, tid));
|
||||
}
|
||||
|
||||
void setThreadLocalData(const THREADID tid, const ThreadData *data) const {
|
||||
PIN_SetThreadData(tls_key, data, tid);
|
||||
}
|
||||
|
||||
std::ofstream trace_file;
|
||||
std::unique_ptr<ImageManager> image_manager;
|
||||
|
||||
std::vector<std::pair<std::string, ADDRINT> > pending_start_specs;
|
||||
std::vector<ADDRINT> start_addrs;
|
||||
|
||||
std::vector<std::pair<std::string, ADDRINT> > pending_stop_specs;
|
||||
std::vector<ADDRINT> stop_addrs;
|
||||
|
||||
bool wait_for_start = false;
|
||||
bool start_and_end_defined = false;
|
||||
|
||||
PIN_LOCK write_lock{};
|
||||
|
||||
bool tracing_enabled = true;
|
||||
|
||||
TLS_KEY tls_key;
|
||||
};
|
||||
}
|
||||
|
||||
static BOOL should_trace(ToolContext *context, const ADDRINT pc) {
|
||||
const BOOL interestingAddress = context->image_manager->isInterestingAddress(pc);
|
||||
if (context->start_and_end_defined) {
|
||||
BOOL pending_disable_tracing = false;
|
||||
if (context->wait_for_start) {
|
||||
for (const ADDRINT tgt: context->start_addrs) {
|
||||
if (pc == tgt) {
|
||||
context->tracing_enabled = true;
|
||||
context->wait_for_start = false;
|
||||
LOG("Start address hit: 0x" + hexstr(tgt) + ", enabling tracing\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
context->tracing_enabled = interestingAddress;
|
||||
for (const ADDRINT tgt: context->stop_addrs) {
|
||||
if (pc == tgt) {
|
||||
context->tracing_enabled = false;
|
||||
pending_disable_tracing = true;
|
||||
context->wait_for_start = true;
|
||||
LOG("Stop address hit: 0x" + hexstr(tgt) + ", disabling tracing\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pending_disable_tracing) {
|
||||
context->tracing_enabled = false;
|
||||
} else if (!context->tracing_enabled) {
|
||||
return false;
|
||||
}
|
||||
return interestingAddress;
|
||||
}
|
||||
return interestingAddress;
|
||||
}
|
||||
|
||||
static void add_mem_event(ThreadData *data, const ADDRINT addr, const UINT32 size,
|
||||
const uint8_t flags) {
|
||||
if (size == 0) return;
|
||||
|
||||
ADDRINT newEnd = addr + size;
|
||||
ADDRINT newStart = addr;
|
||||
UINT8 newFlags = flags;
|
||||
|
||||
auto it = data->mem_events.lower_bound(newStart);
|
||||
if (it != data->mem_events.begin()) {
|
||||
if (const auto prev = std::prev(it); prev->second.end() >= newStart) {
|
||||
newStart = prev->first;
|
||||
newEnd = std::max(newEnd, prev->second.end());
|
||||
newFlags |= prev->second.access_flags;
|
||||
data->mem_events.erase(prev);
|
||||
}
|
||||
}
|
||||
|
||||
while (it != data->mem_events.end() && it->first <= newEnd) {
|
||||
newFlags |= it->second.access_flags;
|
||||
newEnd = std::max(newEnd, it->second.end());
|
||||
it = data->mem_events.erase(it);
|
||||
}
|
||||
|
||||
const auto newSize = static_cast<UINT32>(newEnd - newStart);
|
||||
if (newSize > data->largestSize) data->largestSize = newSize;
|
||||
data->mem_events[newStart] = {.addr = newStart, .size = newSize, .access_flags = newFlags};
|
||||
}
|
||||
|
||||
static VOID on_thread_start(const THREADID tid, CONTEXT *ctxt, INT32 flags, VOID *v) {
|
||||
const auto &context = *static_cast<ToolContext *>(v);
|
||||
const auto *data = new ThreadData();
|
||||
context.setThreadLocalData(tid, data);
|
||||
}
|
||||
|
||||
static VOID on_image_load(const IMG img, VOID *v) {
|
||||
auto &context = *static_cast<ToolContext *>(v);
|
||||
const std::string img_name = base_name(IMG_Name(img));
|
||||
|
||||
if (IMG_IsVDSO(img)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ADDRINT low = IMG_LowAddress(img);
|
||||
const ADDRINT high = IMG_HighAddress(img);
|
||||
const size_t size = high - low;
|
||||
|
||||
|
||||
std::vector<unsigned char> tmp(size);
|
||||
unsigned char *buf = nullptr;
|
||||
buf = tmp.data();
|
||||
PIN_SafeCopy(buf, reinterpret_cast<const VOID *>(low), size);
|
||||
|
||||
PIN_GetLock(&context.write_lock, 1);
|
||||
{
|
||||
context.trace_file << "Loaded image: 0x" << low << ":0x" << high << " -> "
|
||||
<< IMG_Name(img) << " Bytes: ";
|
||||
LOG("Loaded image: 0x" + hexstr(low) + ":0x" + hexstr(high) + " -> " +
|
||||
IMG_Name(img) + "\n");
|
||||
|
||||
for (UINT32 i = 0; i < size; i++) {
|
||||
context.trace_file << std::hex << std::setw(2) << std::setfill('0') << (buf[i] & 0xff);
|
||||
}
|
||||
|
||||
context.trace_file << std::endl;
|
||||
}
|
||||
PIN_ReleaseLock(&context.write_lock);
|
||||
|
||||
for (const auto &[spec_img, offset]: context.pending_start_specs) {
|
||||
if (spec_img == img_name) {
|
||||
ADDRINT target = low + offset;
|
||||
context.start_addrs.push_back(target);
|
||||
LOG("Resolved start location for " + spec_img + " -> 0x" +
|
||||
hexstr(target) + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &[spec_img, offset]: context.pending_stop_specs) {
|
||||
if (spec_img == img_name) {
|
||||
ADDRINT target = low + offset;
|
||||
context.stop_addrs.push_back(target);
|
||||
LOG("Resolved stop location for " + spec_img + " -> 0x" + hexstr(target) +
|
||||
"\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (context.image_manager->isWhiteListed(img_name)) {
|
||||
context.image_manager->addImage(img_name, low, high);
|
||||
}
|
||||
}
|
||||
|
||||
static VOID on_image_unload(const IMG img, VOID *v) {
|
||||
auto &context = *static_cast<ToolContext *>(v);
|
||||
if (IMG_IsVDSO(img)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.image_manager->removeImage(IMG_LowAddress(img));
|
||||
|
||||
const ADDRINT low = IMG_LowAddress(img);
|
||||
const ADDRINT high = IMG_HighAddress(img);
|
||||
|
||||
context.trace_file << "Unloaded image: 0x" << low << ":0x" << high << " -> "
|
||||
<< IMG_Name(img) << std::endl;
|
||||
LOG("Unloaded image: 0x" + hexstr(low) + ":0x" + hexstr(high) + " -> " +
|
||||
IMG_Name(img) + "\n");
|
||||
}
|
||||
|
||||
static VOID PIN_FAST_ANALYSIS_CALL record_diff(const CONTEXT *cpu, const ADDRINT pc,
|
||||
VOID *v) {
|
||||
auto &context = *static_cast<ToolContext *>(v);
|
||||
const auto tid = PIN_ThreadId();
|
||||
ThreadData *data = context.GetThreadLocalData(tid);
|
||||
|
||||
ADDRINT val;
|
||||
std::ostringstream oss;
|
||||
oss << std::hex;
|
||||
oss << "tid=" << tid << ",";
|
||||
|
||||
for (int reg = REG_GR_BASE; reg <= static_cast<int>(REG_GR_LAST); ++reg) {
|
||||
PIN_GetContextRegval(cpu, static_cast<REG>(reg), reinterpret_cast<UINT8 *>(&val));
|
||||
|
||||
if (val == data->m_cpu[reg]) continue;
|
||||
|
||||
oss << REG_StringShort(static_cast<REG>(reg)) << "=0x" << val << ",";
|
||||
data->m_cpu[reg] = val;
|
||||
}
|
||||
|
||||
oss << PC << "=0x" << pc;
|
||||
|
||||
const ADDRINT largest =
|
||||
std::max(data->largestSize,
|
||||
std::max(data->mem_r_size,
|
||||
std::max(data->mem_r2_size, data->mem_w_size)));
|
||||
std::vector<unsigned char> tmp(largest);
|
||||
unsigned char *buf = nullptr;
|
||||
buf = tmp.data();
|
||||
|
||||
for (auto const &[_, ev]: data->mem_events) {
|
||||
if (ev.size == 0) continue;
|
||||
|
||||
PIN_SafeCopy(buf, reinterpret_cast<const VOID *>(ev.addr), ev.size);
|
||||
|
||||
// It's easier to just call all these events as access instead of an
|
||||
// explicit read or write. Especially since the trace is not keeping track
|
||||
// of where these occurred
|
||||
constexpr auto atype = "ma";
|
||||
|
||||
oss << "," << atype << "=0x" << ev.addr << ":";
|
||||
|
||||
for (UINT32 i = 0; i < ev.size; i++) {
|
||||
oss << std::hex << std::setw(2) << std::setfill('0') << (buf[i] & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
data->mem_events.clear();
|
||||
data->largestSize = 0;
|
||||
|
||||
if (data->mem_r_size) {
|
||||
PIN_SafeCopy(buf, reinterpret_cast<const VOID *>(data->mem_r_addr), data->mem_r_size);
|
||||
oss << ",mr=0x" << data->mem_r_addr << ":";
|
||||
|
||||
for (UINT32 i = 0; i < data->mem_r_size; i++) {
|
||||
oss << std::hex << std::setw(2) << std::setfill('0')
|
||||
<< (buf[i] & 0xff);
|
||||
}
|
||||
|
||||
data->mem_r_size = 0;
|
||||
}
|
||||
|
||||
if (data->mem_r2_size) {
|
||||
PIN_SafeCopy(buf, reinterpret_cast<const VOID *>(data->mem_r2_addr), data->mem_r2_size);
|
||||
oss << ",mr=0x" << data->mem_r2_addr << ":";
|
||||
|
||||
for (UINT32 i = 0; i < data->mem_r2_size; i++) {
|
||||
oss << std::hex << std::setw(2) << std::setfill('0')
|
||||
<< (buf[i] & 0xff);
|
||||
}
|
||||
|
||||
data->mem_r2_size = 0;
|
||||
}
|
||||
|
||||
if (data->mem_w_size) {
|
||||
PIN_SafeCopy(buf, reinterpret_cast<const VOID *>(data->mem_w_addr), data->mem_w_size);
|
||||
oss << ",mw=0x" << data->mem_w_addr << ":";
|
||||
|
||||
for (UINT32 i = 0; i < data->mem_w_size; i++) {
|
||||
oss << std::hex << std::setw(2) << std::setfill('0')
|
||||
<< (buf[i] & 0xff);
|
||||
}
|
||||
|
||||
data->mem_w_size = 0;
|
||||
}
|
||||
|
||||
oss << std::endl;
|
||||
|
||||
PIN_GetLock(&context.write_lock, 1);
|
||||
{
|
||||
context.trace_file << oss.str();
|
||||
}
|
||||
PIN_ReleaseLock(&context.write_lock);
|
||||
}
|
||||
|
||||
static VOID PIN_FAST_ANALYSIS_CALL record_read(const THREADID tid, const ADDRINT access_addr,
|
||||
const UINT32 access_size, VOID *v,
|
||||
const BOOL recordMemOnlyOp) {
|
||||
auto const &context = *static_cast<ToolContext *>(v);
|
||||
ThreadData *data = context.GetThreadLocalData(tid);
|
||||
if (!recordMemOnlyOp) {
|
||||
data->mem_r_addr = access_addr;
|
||||
data->mem_r_size = access_size;
|
||||
} else {
|
||||
add_mem_event(data, access_addr, access_size, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static VOID PIN_FAST_ANALYSIS_CALL record_read2(const THREADID tid, const ADDRINT access_addr,
|
||||
const UINT32 access_size, VOID *v,
|
||||
const BOOL recordMemOnlyOp) {
|
||||
auto const &context = *static_cast<ToolContext *>(v);
|
||||
ThreadData *data = context.GetThreadLocalData(tid);
|
||||
if (!recordMemOnlyOp) {
|
||||
data->mem_r2_addr = access_addr;
|
||||
data->mem_r2_size = access_size;
|
||||
} else {
|
||||
add_mem_event(data, access_addr, access_size, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static VOID PIN_FAST_ANALYSIS_CALL record_write(const THREADID tid, const ADDRINT access_addr,
|
||||
const UINT32 access_size, VOID *v,
|
||||
const BOOL recordMemOnlyOp) {
|
||||
auto const &context = *static_cast<ToolContext *>(v);
|
||||
ThreadData *data = context.GetThreadLocalData(tid);
|
||||
if (!recordMemOnlyOp) {
|
||||
data->mem_w_addr = access_addr;
|
||||
data->mem_w_size = access_size;
|
||||
} else {
|
||||
add_mem_event(data, access_addr, access_size, 2);
|
||||
}
|
||||
}
|
||||
|
||||
static VOID PIN_FAST_ANALYSIS_CALL record_after_instruction(const CONTEXT *cpu,
|
||||
const ADDRINT target, VOID *v) {
|
||||
auto &context = *static_cast<ToolContext *>(v);
|
||||
if (should_trace(&context, target)) {
|
||||
record_diff(cpu, target, v);
|
||||
}
|
||||
}
|
||||
|
||||
static VOID instrument_inst(const INS ins, VOID *v, const BOOL recordNext, const BOOL recordMemOnly) {
|
||||
auto &context = *static_cast<ToolContext *>(v);
|
||||
const BOOL shouldTrace = should_trace(&context, INS_Address(ins));
|
||||
if (KnobCollectMem.Value() == 0 && !shouldTrace) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!recordMemOnly && shouldTrace) {
|
||||
INS_InsertCall(ins, IPOINT_BEFORE, reinterpret_cast<AFUNPTR>(record_diff),
|
||||
IARG_FAST_ANALYSIS_CALL, IARG_CONST_CONTEXT, IARG_INST_PTR,
|
||||
IARG_PTR, v, IARG_END);
|
||||
}
|
||||
|
||||
if (!recordMemOnly && recordNext && INS_IsControlFlow(ins)) {
|
||||
INS_InsertCall(ins, IPOINT_TAKEN_BRANCH, reinterpret_cast<AFUNPTR>(record_after_instruction),
|
||||
IARG_FAST_ANALYSIS_CALL, IARG_CONTEXT,
|
||||
IARG_BRANCH_TARGET_ADDR, IARG_PTR, v, IARG_END);
|
||||
}
|
||||
|
||||
const BOOL memoryRecord = recordMemOnly || !shouldTrace;
|
||||
|
||||
if (INS_IsMemoryRead(ins) || INS_IsMemoryWrite(ins)) {
|
||||
if (INS_IsMemoryRead(ins)) {
|
||||
INS_InsertPredicatedCall(ins, IPOINT_BEFORE, reinterpret_cast<AFUNPTR>(record_read),
|
||||
IARG_FAST_ANALYSIS_CALL, IARG_THREAD_ID,
|
||||
IARG_MEMORYREAD_EA, IARG_MEMORYREAD_SIZE,
|
||||
IARG_PTR, v, IARG_BOOL, memoryRecord, IARG_END);
|
||||
}
|
||||
|
||||
if (INS_HasMemoryRead2(ins)) {
|
||||
INS_InsertPredicatedCall(ins, IPOINT_BEFORE, reinterpret_cast<AFUNPTR>(record_read2),
|
||||
IARG_FAST_ANALYSIS_CALL, IARG_THREAD_ID,
|
||||
IARG_MEMORYREAD2_EA, IARG_MEMORYREAD_SIZE,
|
||||
IARG_PTR, v, IARG_BOOL, memoryRecord, IARG_END);
|
||||
}
|
||||
|
||||
if (INS_IsMemoryWrite(ins)) {
|
||||
INS_InsertPredicatedCall(ins, IPOINT_BEFORE, reinterpret_cast<AFUNPTR>(record_write),
|
||||
IARG_FAST_ANALYSIS_CALL, IARG_THREAD_ID,
|
||||
IARG_MEMORYWRITE_EA, IARG_MEMORYWRITE_SIZE,
|
||||
IARG_PTR, v, IARG_BOOL, memoryRecord, IARG_END);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static VOID instrument_inst_cb(const INS ins, VOID *v) { instrument_inst(ins, v, false, false); }
|
||||
|
||||
static VOID instrument_trace_cb(const TRACE trace, VOID *v) {
|
||||
for (BBL bbl = TRACE_BblHead(trace); BBL_Valid(bbl); bbl = BBL_Next(bbl)) {
|
||||
for (INS ins = BBL_InsHead(bbl); INS_Valid(ins); ins = INS_Next(ins)) {
|
||||
if (ins == BBL_InsHead(bbl) || (ins == BBL_InsTail(bbl) &&
|
||||
BBL_InsHead(bbl) != BBL_InsTail(bbl))) {
|
||||
instrument_inst(ins, v, false, false);
|
||||
} else if (KnobCollectMem.Value() == 1) {
|
||||
instrument_inst(ins, v, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static VOID instrument_routine_cb(const TRACE trace, VOID *v) {
|
||||
static bool recordNext = false;
|
||||
for (BBL bbl = TRACE_BblHead(trace); BBL_Valid(bbl); bbl = BBL_Next(bbl)) {
|
||||
for (INS ins = BBL_InsHead(bbl); INS_Valid(ins); ins = INS_Next(ins)) {
|
||||
if (INS_IsCall(ins) || INS_IsRet(ins) || (INS_IsControlFlow(ins) && !INS_IsCall(ins) &&
|
||||
INS_Category(ins) != XED_CATEGORY_COND_BR && !INS_IsRet(ins) &&
|
||||
!BBL_Valid(BBL_Next(bbl)))) {
|
||||
instrument_inst(ins, v, true, false);
|
||||
} else if (INS_IsSyscall(ins)) {
|
||||
recordNext = true;
|
||||
instrument_inst(ins, v, false, false);
|
||||
} else if (recordNext) {
|
||||
recordNext = false;
|
||||
instrument_inst(ins, v, true, false);
|
||||
} else if (KnobCollectMem.Value() == 1) {
|
||||
instrument_inst(ins, v, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static INT32 usage() {
|
||||
std::cerr << "TenetPlusPlus pintool tracer" << std::endl;
|
||||
std::cerr << KNOB_BASE::StringKnobSummary() << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static EXCEPT_HANDLING_RESULT exception_handler_cb(THREADID tid, EXCEPTION_INFO *pExceptInfo,
|
||||
PHYSICAL_CONTEXT *pPhysCtxt, VOID *v) {
|
||||
std::cout << pExceptInfo->ToString() << std::endl;
|
||||
return EHR_UNHANDLED;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
PIN_InitSymbols();
|
||||
|
||||
if (PIN_Init(argc, argv)) {
|
||||
return usage();
|
||||
}
|
||||
|
||||
PIN_AddInternalExceptionHandler(exception_handler_cb, nullptr);
|
||||
|
||||
auto logFile = KnobOutputFilePrefix.Value() + ".trace";
|
||||
LOG("Trace will be saved in " + logFile + "\n");
|
||||
|
||||
auto context = std::make_unique<ToolContext>(logFile);
|
||||
context->image_manager = std::make_unique<ImageManager>();
|
||||
|
||||
for (unsigned i = 0; i < KnobModuleWhitelist.NumberOfValues(); ++i) {
|
||||
LOG("White-listing image: " + KnobModuleWhitelist.Value(i) + "\n");
|
||||
context->tracing_enabled = false;
|
||||
context->image_manager->addWhiteListedImage(KnobModuleWhitelist.Value(i));
|
||||
}
|
||||
|
||||
if (KnobStartAt.NumberOfValues() > 0) {
|
||||
context->wait_for_start = true;
|
||||
context->tracing_enabled = false;
|
||||
|
||||
std::regex pattern(R"(^(.+):([0-9a-fA-F]+)$)");
|
||||
std::smatch matches;
|
||||
|
||||
for (unsigned i = 0; i < KnobStartAt.NumberOfValues(); ++i) {
|
||||
if (!std::regex_match(KnobStartAt.Value(i), matches, pattern)) {
|
||||
continue;
|
||||
}
|
||||
ADDRINT offset = std::stoi(matches[2], nullptr, 16);
|
||||
if (KnobModuleWhitelist.NumberOfValues() > 0) {
|
||||
context->image_manager->addWhiteListedImage(KnobStartAt.Value(i));
|
||||
}
|
||||
context->pending_start_specs.emplace_back(matches[1], offset);
|
||||
}
|
||||
|
||||
context->start_and_end_defined = true;
|
||||
}
|
||||
|
||||
if (KnobStopAt.NumberOfValues() > 0) {
|
||||
std::regex pattern(R"(^(.+):([0-9a-fA-F]+)$)");
|
||||
std::smatch matches;
|
||||
|
||||
for (unsigned i = 0; i < KnobStopAt.NumberOfValues(); ++i) {
|
||||
if (!std::regex_match(KnobStopAt.Value(i), matches, pattern)) {
|
||||
continue;
|
||||
}
|
||||
ADDRINT offset = std::stoi(matches[2], nullptr, 16);
|
||||
if (KnobModuleWhitelist.NumberOfValues() > 0) {
|
||||
context->image_manager->addWhiteListedImage(KnobStopAt.Value(i));
|
||||
}
|
||||
context->pending_stop_specs.emplace_back(matches[1], offset);
|
||||
}
|
||||
context->start_and_end_defined = true;
|
||||
}
|
||||
|
||||
PIN_AddThreadStartFunction(on_thread_start, std::move(context).get());
|
||||
|
||||
IMG_AddInstrumentFunction(on_image_load, std::move(context).get());
|
||||
IMG_AddUnloadFunction(on_image_unload, std::move(context).get());
|
||||
|
||||
switch (auto inst = static_cast<TraceGranularity>(KnobTraceGranularity.Value())) {
|
||||
case TraceGranularity::TRACE_INSTRUCTION:
|
||||
LOG("Running with instruction level tracing\n");
|
||||
INS_AddInstrumentFunction(instrument_inst_cb, std::move(context).get());
|
||||
break;
|
||||
case TraceGranularity::TRACE_BASIC_BLOCK:
|
||||
LOG("Running with block level tracing\n");
|
||||
TRACE_AddInstrumentFunction(instrument_trace_cb, std::move(context).get());
|
||||
break;
|
||||
case TraceGranularity::TRACE_ROUTINE:
|
||||
LOG("Running with routine level tracing\n");
|
||||
TRACE_AddInstrumentFunction(instrument_routine_cb, std::move(context).get());
|
||||
break;
|
||||
default:
|
||||
std::cerr << "Unknown trace type: " << static_cast<int>(inst) << "\n";
|
||||
abort();
|
||||
}
|
||||
|
||||
PIN_StartProgram();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ import ghidra.util.exception.*;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
public class TenetPlusPlusLoader implements Loader {
|
||||
private record ModuleEvent(String path, String module, AddressRange range) {}
|
||||
private record ModuleEvent(String path, String module, AddressRange range, byte[] bytes) {}
|
||||
|
||||
private static final String TRACE_SUFFIX = ".trace";
|
||||
private static final String TENET_SUFFIX = ".tenet";
|
||||
@@ -73,7 +73,7 @@ public class TenetPlusPlusLoader implements Loader {
|
||||
private static final Pattern MEM_PATTERN =
|
||||
Pattern.compile("(?:^|,)(mr|mw|ma)=0x([^:]+):([0-9a-fA-F]+)");
|
||||
private static final Pattern MODULE_LOAD_PATTERN =
|
||||
Pattern.compile("Loaded image: 0x([0-9a-fA-F]+):0x([0-9a-fA-F]+) -> (.*)$");
|
||||
Pattern.compile("Loaded image: 0x([0-9a-fA-F]+):0x([0-9a-fA-F]+) -> (.*) Bytes: (.*)$");
|
||||
private static final Pattern MODULE_UNLOAD_PATTERN =
|
||||
Pattern.compile("Unloaded image: 0x([0-9a-fA-F]+):0x([0-9a-fA-F]+) -> (.*)$");
|
||||
|
||||
@@ -214,6 +214,8 @@ public class TenetPlusPlusLoader implements Loader {
|
||||
if (!load.isEmpty()) {
|
||||
for (final ModuleEvent event : load) {
|
||||
modMan.addLoadedModule(event.path, event.module, event.range, snap);
|
||||
trace.getMemoryManager()
|
||||
.putBytes(snap, event.range.getMinAddress(), ByteBuffer.wrap(event.bytes));
|
||||
}
|
||||
|
||||
load.clear();
|
||||
@@ -512,7 +514,8 @@ public class TenetPlusPlusLoader implements Loader {
|
||||
|
||||
load.add(new ModuleEvent("Modules[%s]".formatted(mod), mod,
|
||||
rng(Long.parseLong(modLoadMatcher.group(1), 16),
|
||||
Long.parseLong(modLoadMatcher.group(2), 16))));
|
||||
Long.parseLong(modLoadMatcher.group(2), 16)),
|
||||
HexFormat.of().parseHex(modLoadMatcher.group(4))));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -525,7 +528,8 @@ public class TenetPlusPlusLoader implements Loader {
|
||||
|
||||
unload.add(new ModuleEvent("Modules[%s]".formatted(mod), mod,
|
||||
rng(Long.parseLong(modUnloadMatcher.group(1), 16),
|
||||
Long.parseLong(modUnloadMatcher.group(2), 16))));
|
||||
Long.parseLong(modUnloadMatcher.group(2), 16)),
|
||||
null));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -287,15 +287,17 @@ public class BreakpointTimelineProvider extends ComponentProvider {
|
||||
for (final TraceReference reference : currentTrace.getReferenceManager()
|
||||
.getReferencesToRange(Lifespan.ALL, range)) {
|
||||
|
||||
if ((reference.getReferenceType() == RefType.READ) &&
|
||||
(kind == TraceBreakpointKind.READ)) {
|
||||
if ((reference.getReferenceType() == RefType.READ &&
|
||||
kind == TraceBreakpointKind.READ) ||
|
||||
(reference.getReferenceType() == RefType.READ_WRITE)) {
|
||||
breakpointHits.add(
|
||||
new BreakpointHitEvent(reference.getStartSnap(), TraceBreakpointKind.READ,
|
||||
breakpointLocation.getName(reference.getStartSnap())));
|
||||
}
|
||||
|
||||
if ((reference.getReferenceType() == RefType.WRITE) &&
|
||||
(kind == TraceBreakpointKind.WRITE)) {
|
||||
if ((reference.getReferenceType() == RefType.WRITE &&
|
||||
kind == TraceBreakpointKind.WRITE) ||
|
||||
(reference.getReferenceType() == RefType.READ_WRITE)) {
|
||||
breakpointHits.add(
|
||||
new BreakpointHitEvent(reference.getStartSnap(), TraceBreakpointKind.WRITE,
|
||||
breakpointLocation.getName(reference.getStartSnap())));
|
||||
|
||||
@@ -31,8 +31,8 @@ import ghidra.program.model.lang.*;
|
||||
import ghidra.program.model.listing.*;
|
||||
import ghidra.program.model.mem.MemoryAccessException;
|
||||
import ghidra.program.model.pcode.*;
|
||||
import ghidra.util.exception.InvalidInputException;
|
||||
import ghidra.util.exception.NotFoundException;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.exception.*;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
/**
|
||||
@@ -99,6 +99,18 @@ public class SymPcodeExecutor extends PcodeExecutor<Sym> {
|
||||
// TODO: Is there a way to know if a userop affects the stack?
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stepOp(PcodeOp op, PcodeFrame frame, PcodeUseropLibrary<Sym> library) {
|
||||
// TODO: This function can probably be removed after GP-6707 is complete
|
||||
try {
|
||||
monitor.checkCancelled();
|
||||
}
|
||||
catch (CancelledException e) {
|
||||
throw new PcodeExecutionException("Monitor was cancelled", frame, e);
|
||||
}
|
||||
super.stepOp(op, frame, library);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to figure the stack depth change for a given function
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user