Memory

Edit on GitHub
#include <Memory.h>
SReturnNameParameters
Memory (const Process& p = Process())
bool IsValid (void) const
Process GetProcess (void) const
Stats GetStats (void) const
Stats GetStats (bool reset)
Region GetRegion (uintptr address) const
RegionList GetRegions (uintptr start = 0, uintptr stop = -1) const
bool SetAccess (const Region& region, uint32 nativeAccess)
bool SetAccess (const Region& region, bool r, bool w, bool x)
AddressList Find (const char* pattern, uintptr start = 0, uintptr stop = -1, uintptr limit = 0, const char* f = 0)
bool CreateCache (uintptr blockLength, uintptr blockBuffer, uintptr initialSize, uintptr enlargeSize = 0, uintptr maximumSize = 0)
void ClearCache (void)
void DeleteCache (void)
bool IsCaching (void) const
uintptr GetCacheSize (void) const
uintptr GetPtrSize (void) const
uintptr GetMinAddress (void) const
uintptr GetMaxAddress (void) const
uintptr GetPageSize (void) const
uintptr ReadData (uintptr address, void* dataOutput, uintptr length, Flags f = Default)
uintptr WriteData (uintptr address, const void* data, uintptr length, Flags f = Default)

Description

Represents the virtual memory of a running process. All applications running on the system take up memory, and to them, memory appears contiguous and abundant. But for this to work, a memory management technique known as virtual memory must be used.

Virtual Memory

Virtual memory is a memory management technique which maps memory addresses used by the program, called virtual addresses, to physical addresses in computer memory. This is achieved with the help of both hardware and software. The operating system, and more specifically the kernel, handle allocation and mapping of new virtual addresses to physical addresses while the processor, and more specifically the memory management unit (MMU), handle address translation. By using page tables, the processor and MMU are able to do this translation quickly and easily behind the scenes, without any additional software.

Virtual addresses don't always map to RAM, certain operating systems are able to extend the virtual memory system further by using empty hard drive space as additional storage. Through the use of swap files, or page files, the system can pretend to have more memory than it actually has, opening up contingencies in cases where the system runs out of RAM. This technique is orders of magnitude slower, but is necessary. This also happens automatically and transparently to running applications.

Other benefits of virtual memory systems include: freeing applications from having to manage a shared memory space, increased security due to memory isolation, obscuring physical memory fragmentation, and having a dedicated address space for every running process on the system. However, applications wanting to read or inquire about the memory of other applications must go through the operating system, which may be slow or otherwise inaccessible at times. But performance problems can usually be mitigated through the use of various caching algorithms, which the Memory class implements.

Memory Layout

Most platforms, specifically those supported by Robot, share a common memory layout. On the lowest level, memory is divided into equal-sized chunks called pages and pages are the smallest unit of memory which can be allocated by any application. On most systems, pages consist of 4096 bytes and when a specific address is referenced, the address is first aligned to the nearest page boundary before being offset to the desired address. In addition, each page has a list of attributes associated with it. These attributes are, for the most part, platform-dependent but often share some similarities including whether the page is readable, writable or executable.

To save on memory, pages may be shared with one or more processes at the same time. In such cases, the page is marked as shared and is usually read-only. Shared pages may also be marked as copy-on-write in which case a process writing to it will create a copy of that page in the virtual address space of the writing process. From that point forward, the writing process maintains its own copy of the page, which it may write to at any time.

A group of consecutive pages with identical attributes, including page state and access rights, are grouped into regions. These regions have a start and stop address and can either be bound or unbound. Regions which are bound have a physical address assigned to them and regions which are unbound don't. This is often referred to as committed and non-committed memory, respectively. Some platforms denominate these further but for cross-platform purposes, there are only two possibilities.

On the highest level, processes are divided into three main sections. The first section, starting at zero, is the null section and consists of one or more pages of unbound memory. This is used to prevent null pointer access, since almost all null pointers are represented by zero. The next section, starting at the minimum address, is the application section. This section contains all the memory associated with the application which can be accessed and manipulated. Unless the application is a device driver or root-kit running in ring-0, this is the only section that matters. The third and final section, starting at the maximum address, is the kernel section. This section contains all the memory associated with the operating system kernel and is inaccessible to standard user-space applications.

Process Layout

As mentioned previously, processes are divided into three sections, two of which are unused for memory analysis purposes. Like memory layout, most platforms share a common process layout. On the highest level, the application section is divided into three or more parts. Each part consists of one or more regions, not necessarily contiguous, that represent the overall structure of the application. The first part is the main executable which is a copy of the main executable image used to launch this application. The next part consists of any number of shared libraries which the executable depends upon. These libraries are shared amongst all other applications that need them, resulting in less overall memory consumption. The third part includes the stack and the heap. The stack is used for local and automatic variables, as well as function return addresses while the heap is used to store all dynamically allocated memory. Other parts are considered platform-specific and are not discussed here. Moreover, order is not guaranteed, every part may appear in any order, depending on the underlying application loader.

As described in the Module class, the main executable and the shared libraries are considered modules. All modules share a similar internal data structure consisting of multiple segments including the Header, Text, rData, Data and iData segments. The Header segment describes module metadata such as the processor architecture, segment offsets and more. The Text segment contains the actual code being executed. The rData segment contains read-only data. The Data segment contains initialized and uninitialized data. And the iData segments contain library function offsets. The order and the number of segments depends on the application itself and the platform its running on.

Class Functionality

To interact with the virtual memory of a running application, simply pass the associated Process to the Constructor. To check whether the virtual memory of the assigned process can be interacted with, use the IsValid function. To retrieve the assigned process, use the GetProcess function. To retrieve statistics about how the Memory class is being used, use the GetStats function.

After a process has been assigned, the Memory class offers several functions for interacting with its virtual memory. For instance, to retrieve information about a particular memory region at an arbitrary address, use the GetRegion function. To retrieve information about all memory regions within a particular address range, use the GetRegions function. To assign different protection attributes to existing memory regions, use the SetAccess functions. Useful properties such as the pointer size, page size and minimum and maximum accessible addresses can be retrieved through their respective accessor functions. Finally, reading from and writing to the virtual memory of the assigned process can be done through the ReadData and WriteData functions respectively.

In addition to virtual memory interaction, the Memory class also offers several algorithms for speeding up development and improving performance. One such algorithm is a general-purpose signature scanner which can be used to find a particular pattern of bytes within an address range, available through the Find function. Another algorithm is a memory caching system which greatly improves memory reading performance and reduces overall code complexity. To create a cache, use the CreateCache function. When the cache needs to be cleared, use the ClearCache function. When a cache is no longer needed, use the DeleteCache function. To check whether a cache is currently being used, use the IsCaching function. And finally, to get the total allocated size of the cache, use the GetCacheSize function. Once a cache has been created, simply use the ReadData function as always and memory will be cached automatically based on the parameters of a cache.

Types

typedef vector<uintptr>  Robot::AddressList;
typedef vector<Region > Memory:: RegionList;

Constructors

    
Memory (const Process& p = Process())

Constructs a memory object associated with p, without allocation or process interaction.

Functions

    
bool IsValid (void) const

Returns true if the selected process is valid. On Mac, performs an additional check to determine whether a valid mach task port was retrieved during process selection. If this function returns false, the majority of this class will fail.

    
Process GetProcess (void) const

Returns the process associated with this class during construction.

    
Stats GetStats (void) const
Stats GetStats (bool reset)

Returns statistics about how this class is being used. Especially useful for measuring and optimizing the performance of a particular code segment. Set reset to true to also reset the statistics upon return.

    
Region GetRegion (uintptr address) const

Returns the region at address, or an invalid region if the selected process is not valid or address is out of range.

When using this function, the starting address of the returned region will always be aligned to the nearest page boundary encompassing address. That is, if the region spans [0x4000, 0x8000) and address is 0x6020, the starting address of the returned region will be 0x6000. To determine the true starting address of a region, consider getting all regions with the GetRegions function. As for the ending address, it will always be accurate as this function scans subsequent pages following the initial page until it finishes scanning the entire region. In the example above, the ending address will be 0x8000.

Linux: It is strongly recommended to use the GetRegions function when retrieving information on multiple regions as that function is used in the implementation of this function.

    
RegionList GetRegions (uintptr start = 0, uintptr stop = -1) const

Returns a list of all regions between [start, stop), or an empty list if the selected process is not valid or start is greater than or equal to stop. The resulting list is guaranteed to be sorted from start to stop and will include both bound and unbound regions. Both start and stop will be clamped to the minimum and maximum accessible address values.

Like GetRegion, start will always be aligned to the current page boundary while stop will always be aligned to the next page boundary, if it wasn't already aligned. That is, if start is set to 0x6020 then the first region in the list will begin at 0x6000 and if stop is set to either 0x7020 or 0x8000 then the last region in the list will stop at 0x8000. This function will continue to scan regions until either stop is reached or an error occurs.

    
bool SetAccess (const Region& region, uint32 nativeAccess)
bool SetAccess (const Region& region, bool r, bool w, bool x)

Attempts to change the access rights of region and returns true on success. Returns false if the selected process is not valid or region is not valid and bound. In most cases it is recommended to specify new access rights with read, write and exec as this will work on all platforms. For increased flexibility, however, it is also possible to supply the platform-specific access code directly with access. On Mac, access is passed directly to the mach_vm_protect function. On Windows, access is passed directly to the VirtualProtectEx function.

Note: It is possible to specify a custom address range encompassing multiple regions at the same time. To do this, simply create a region with both Valid and Bound set to true. Then set Start and Stop to the desired address range. This function will then modify the access rights accordingly, however, if the access rights of any page in the range cannot be modified, this function will return false without modifying the access rights of any page. Addresses are automatically aligned to page boundaries.

Linux: This function is not available and will always return false.

    
AddressList Find (const char* pattern, uintptr start = 0, uintptr stop = -1, uintptr limit = 0, const char* f = 0)

Searches for pattern in the selected process and returns a list of results. pattern expects a string of case-insensitive hexadecimal bytes with each byte consisting of exactly two characters. Wildcards are supported and denoted by a single question mark. Each byte or wildcard can be optionally separated by one or more spaces. Internally, searching is performed on a region by region basis beginning at start and ending at stop, however, if the number of results reaches limit, and limit is greater than zero, the search will end. An empty list is returned if the selected process is not valid, pattern is incorrect, start and stop return no regions, f is incorrect or a read error occurred.

This function is not multi-threaded and uses search, which will be slow for large address ranges. It is not ideal for atomic pattern searching, such as searching for integers. This function is, however, ideal for quick tests and other general-purpose use.

Results can be filtered through flags defined by f. These flags define which regions will be searched based on their access rights. Flags are defined by a string of up to four characters. Each character represents a different access right property. The first character represents writable, the second executable, the third private and the fourth guarded. Each character can either be a space, a minus or a letter (W, X, P, G respectively). A space denotes any value, a minus denotes a false value and a letter denotes a true value. As an example, if f is "w-", then in order for a region to be searched it must be writable but not executable.

Warning: Caching should not enabled as it will result in a large memory overhead.

    
bool CreateCache (uintptr blockLength, uintptr blockBuffer, uintptr initialSize, uintptr enlargeSize = 0, uintptr maximumSize = 0)

Engages the internal memory caching system, usually resulting in a tremendous improvement in memory reading performance. The algorithm works by optimizing the calls made to native memory reading functions as well as reducing the total number of calls made to these functions. If the cache was successfully created, this function will return true. If any of the specified parameters are incorrect, this function will return false.

Internally, this function allocates a data buffer for storing the memory of the selected process. As more and more memory gets read, the buffer becomes larger and larger. If multiple reads are performed in proximity of one another, the resulting data is copied from the buffer instead of performing a native read. This results in a huge performance boost as native memory reads are notoriously slow, particularly on Windows.

The caching algorithm requires a strict set of parameters in order to work, most of which should not be surprising. First, all parameters must be divisible by PageSize. Second, blockLength, blockBuffer and initialSize cannot be zero. Third, blockLength must be a power of two and greater than or equal to blockBuffer. Fourth, initialSize must be greater than or equal to the sum of blockLength and blockBuffer. Fifth, if enlargeSize is not zero then it must also be greater than or equal to the sum of blockLength and blockBuffer. And last, if maximumSize is not zero then it must be greater than or equal to initialSize.

As for the parameters themselves, the sum of blockLength and blockBuffer defines the amount of data which will be read during a single native read. blockLength defines the amount of accessible data from that read while blockBuffer defines the largest amount of data which could be read at any given time. Reading more than blockBuffer at a time results in a native read. initialSize defines the initial size of the data buffer, allocated immediately upon return. enlargeSize defines the size to grow the data buffer by upon running out of space. If set to zero, a native read will be performed upon running out of space. maximumSize defines the maximum possible size of the data buffer. If set to zero, the size of the data buffer is limitless, however, in the case that the size cannot be extended, a native read will be performed upon running out of space. All parameters should be represented in bytes.


    
void ClearCache (void)

Removes all cached data accumulated through reading memory. Call this function when the latest version of the memory needs to be read as the data in the cache may be out of date. This function has no performance implications and does not perform reallocation. This function does nothing if a cache has not yet been created.

    
void DeleteCache (void)

Disables memory caching and deletes any allocated memory. This function does nothing if a cache has not yet been created.

    
bool IsCaching (void) const

Returns true if memory caching has been enabled.

    
uintptr GetCacheSize (void) const

Returns the current size of the allocated memory cache, in bytes. The cache may grow depending on the cache parameters and how much data has already been read. Unless the cache is deleted, it will never shrink.

    
uintptr GetPtrSize (void) const

Returns the size of a single pointer in the selected process, in bytes. That is, if a 32-bit process has been selected, this function will return 4 and if a 64-bit process has been selected, this function will return 8.

    
uintptr GetMinAddress (void) const
uintptr GetMaxAddress (void) const

Returns the minimum and maximum accessible address for the selected process.

    
uintptr GetPageSize (void) const

Returns the size of a single page of memory, in bytes. This value is typically 4096.

    
uintptr ReadData (uintptr address, void* dataOutput, uintptr length, Flags f = Default)

Copies the memory of the selected process into dataOutput. address specifies the base address in the selected process from which the data is read while length specifies the number of bytes to read. Ensure that dataOutput is large enough to store length bytes to avoid a buffer overflow error. On success, this function returns the number of bytes successfully copied into dataOutput. On failure, this function returns zero. This function can fail if the selected process is not valid, address is outside the minimum and maximum accessible address space, dataOutput is null, length is zero or the read fails.

A read can fail due to a variety of reasons, but the most common reason is that address and length intersect an unreadable region of memory. While less of a concern for smaller reads, larger reads spanning multiple regions, such as those performed by the memory caching algorithm, may encounter this error more frequently. For this, consider specifying a non-default flag. See Flags for more information.

Note: For improved performance, consider using a memory cache.

    
uintptr WriteData (uintptr address, const void* data, uintptr length, Flags f = Default)

Copies data into the memory of the selected process. address specifies the base address in the selected process to which the data is written while length specifies the number of bytes to write. Ensure that data holds at least length bytes to avoid a buffer overflow error. On success, this function returns the number of bytes successfully copied into the memory of the selected process. On failure, this function returns zero. This function can fail if the selected process is not valid, address is outside the minimum and maximum accessible address space, data is null, length is zero or the write fails.

A write can fail due to a variety of reasons, but the most common reason is that address and length intersect an unwritable region of memory. While less of a concern for smaller writes, larger writes spanning multiple regions may encounter this error more frequently. For this, consider specifying a non-default flag. See Flags for more information.

Stats

SReturnNameParameters
bool operator == (const Stats& stats) const
bool operator != (const Stats& stats) const
uint32 SystemReads
uint32 CachedReads
uint32 SystemWrites
uint32 AccessWrites
uint32 ReadErrors
uint32 WriteErrors

Description

Represents statistics about how the Memory class is being used. These statistics are quite useful for debugging, especially in the case of measuring and optimizing the performance of particular code segments. To retrieve statistics, use the GetStats function within the Memory class.

After obtaining a copy of the statistics, all information can be retrieved through the class properties. For instance, to get the number of times a native read or write was performed on the process, use the SystemReads or SystemWrites properties. To get the number of times a native read or write failed, use the ReadErrors or WriteErrors properties. See the properties list for all other available properties. Comparison is supported as well.

Operators

    
bool operator == (const Stats& stats) const
bool operator != (const Stats& stats) const

Performs equality comparison to determine whether both structures have identical statistics.

Properties

    
uint32 SystemReads
uint32 SystemWrites

Returns the number of times a native read or write was performed on the process. Regardless or whether or not the operation actually succeeded.

    
uint32 CachedReads

Returns the number of times data was copied from the internal cache, including the number of times a native read was performed to get that data.

    
uint32 AccessWrites

Returns the number of times region access-rights were successfully modified.

    
uint32 ReadErrors
uint32 WriteErrors

Returns the number of times a native read or write on the process failed.

Region

SReturnNameParameters
bool Contains (uintptr address) const
bool operator < (uintptr address) const
bool operator > (uintptr address) const
bool operator <= (uintptr address) const
bool operator >= (uintptr address) const
bool operator < (const Region& region) const
bool operator > (const Region& region) const
bool operator <= (const Region& region) const
bool operator >= (const Region& region) const
bool operator == (const Region& region) const
bool operator != (const Region& region) const
bool Valid
bool Bound
uintptr Start
uintptr Stop
uintptr Size
bool Readable
bool Writable
bool Executable
uint32 Access
bool Private
bool Guarded

Description

Represents a single region of memory. A region is nothing more than a group of consecutive pages which share common attributes including page state and access rights. To retrieve a region or list of regions, use the GetRegion or GetRegions functions within the Memory class. Regions can also be synthesized as all properties are public and mutable, but doing so may lead to various side effects.

After a region has been created, all information can be retrieved through the class properties. For instance, to check whether the region information is valid, use the Valid property. To check whether the region is bound, or has physical storage assigned to it, use the Bound property. To retrieve the start and stop addresses of the region, as well as its size, use the Start, Stop and Size properties. To get the access rights in a cross-platform manner, use the Readable, Writable and Executable properties. To get the native platform-dependent access code, use the Access property. To check whether the region is private to the process, as opposed to shared across multiple processes, use the Private property. And on Windows, to check whether the pages in the region are guard pages, use the Guarded property.

The Region class also offers a wide range of useful functionality. For example, the Contains function can be used to check whether an address is contained within the region. And various relational operators help in cases where a region needs to be compared to other regions, as in the case of sorting. Comparison is supported as well.

Note: Although copy-on-write is not explicitly supported, the native platform-dependent access code can be used to retrieve this property, setting it however, is not recommended.

Functions

    
bool Contains (uintptr address) const

Returns true if address is in the range [Start, Stop) of this region.

Operators

    
bool operator < (uintptr address) const
bool operator > (uintptr address) const
bool operator <= (uintptr address) const
bool operator >= (uintptr address) const

Performs relational comparison using the Start of this region and address.

    
bool operator < (const Region& region) const
bool operator > (const Region& region) const
bool operator <= (const Region& region) const
bool operator >= (const Region& region) const

Performs relational comparison using the Start of this region and region.

    
bool operator == (const Region& region) const
bool operator != (const Region& region) const

Performs equality comparison to determine whether two regions have identical properties.

Properties

    
bool Valid

Gets or sets a value indicating whether the information in this region is valid.

    
bool Bound

Gets or sets a value indicating whether the region is a bound/committed region.

    
uintptr Start
uintptr Stop
uintptr Size

Gets or sets the start and stop address of this region and the size it occupies, in bytes.

    
bool Readable
bool Writable
bool Executable
uint32 Access

Gets or sets a value indicating the various access rights of this region. The Readable, Writable and Executable properties are cross-platform while Access encodes this data in a single platform-specific code.

    
bool Private

Gets or sets a value indicating whether the region is private to the process (i.e. not shared).

    
bool Guarded

On Windows, gets or sets a value indicating whether the pages in the region are guard pages. Any attempt to access a guard page causes the system to raise an exception and turn off the guard page status. Guard pages thus act as a one-time access alarm against spontaneous access. On Linux and Mac, this property is always false.

Flags

Represents flags for modifying the way ReadDate and WriteData behave. Default represents the default behavior which passes all the parameters, as they are, directly to the native system function. SkipErrors splits the read and write request into multiple calls, each aligned to their own region. If an error occurs, that particular region is skipped and the operation continues. Failed reads fill the output buffer with zeros for that particular region. AutoAccess performs the same task as SkipErrors but also attempts to make the region readable or writable prior to performing the operation. Although access rights are restored upon completion, the use of this flag is discouraged as it may destabilize the target application. Flags are exclusive and should not be combined with one another.

enum Flags
{
    Default,
    SkipErrors,
    AutoAccess,
};

Examples

// C++
#include <Robot.h>
ROBOT_NS_USE_ALL;

int main (void)
{
    // Open process and create memory
    Memory memory (Process (4815));

    // Must be true for class to work
    if (!memory.IsValid()) return 0;

    // Results are in the context of the process
    memory.GetPtrSize   (); // Size of a pointer
    memory.GetMinAddress(); // Min accessible address
    memory.GetMaxAddress(); // Max accessible address
    memory.GetPageSize  (); // Size of a single page

    // Retrieve all regions in process
    auto regions = memory.GetRegions();

    // Regions is an STL vector
    for (const auto& r : regions)
    {
        if (r.Valid && r.Bound) // Check if region is valid
        {
            r.Start;      // Starting address of the region
            r.Stop;       // Stopping address of the region
            r.Size;       // Size, in bytes,  of the region

            r.Readable;   // True if region can be read from
            r.Writable;   // True if region can be written to
            r.Executable; // True if region can execute code
        }
    }

    // Read memory directly from the selected process
    int16 v16; memory. ReadData (V16_ADDRESS, &v16, 2);
    int32 v32; memory. ReadData (V32_ADDRESS, &v32, 4);
    int64 v64; memory. ReadData (V64_ADDRESS, &v64, 8);

    // Write memory directly to the selected process
    v16 = 123; memory.WriteData (V16_ADDRESS, &v16, 2);
    v32 = 456; memory.WriteData (V32_ADDRESS, &v32, 4);
    v64 = 789; memory.WriteData (V64_ADDRESS, &v64, 8);

    // Get the stats and then reset them
    auto stats = memory.GetStats (true);
    stats.SystemReads;  // 3
    stats.CachedReads;  // 0
    stats.SystemWrites; // 3

    // Create memory cache for faster reads
    memory.CreateCache (4096, 4096, 16384);
    memory.GetCacheSize(); // 16384

    // Read memory through the memory cache
    memory.ReadData (V16_ADDRESS, &v16, 2);
    memory.ReadData (V32_ADDRESS, &v32, 4);
    memory.ReadData (V64_ADDRESS, &v64, 8);

    // Get the new stats again
    stats = memory.GetStats();
    stats.SystemReads;  // 1
    stats.CachedReads;  // 3
    stats.SystemWrites; // 0

    // Clear the memory cache whenever
    // fresh reads need to be performed
    memory.ClearCache();

    memory.IsCaching  (); // True
    memory.DeleteCache();
    memory.IsCaching  (); // False

    // Perform pattern search
    auto list = memory.Find
        ("04 08 ? 16 23 42");

    // List is an STL vector
    for (const auto& a : list)
        a; // Found address

    return 0;
}
// Node
var robot = require ("robot-js");

// Open process and memory
var memory = robot.Memory
    (robot.Process (4815));

// Check the validity
if (memory.isValid())
{
    // Results are in the context of the process
    memory.getPtrSize   (); // Size of a pointer
    memory.getMinAddress(); // Min accessible address
    memory.getMaxAddress(); // Max accessible address
    memory.getPageSize  (); // Size of a single page

    // Retrieve all regions in process
    var regions = memory.getRegions();

    // Regions is an array
    regions.map (function (r)
    {
        if (r.valid && r.bound) // Check if region is valid
        {
            r.start;      // Starting address of the region
            r.stop;       // Stopping address of the region
            r.size;       // Size, in bytes,  of the region

            r.readable;   // True if region can be read from
            r.writable;   // True if region can be written to
            r.executable; // True if region can execute code
        }
    });

    // Read memory directly from the process
    var v16 = memory.readInt16 (V16_ADDRESS);
    var v32 = memory.readInt32 (V32_ADDRESS);
    var v64 = memory.readInt64 (V64_ADDRESS);

    // Write memory directly to the selected process
    v16 = 123; memory.writeInt16 (V16_ADDRESS, v16);
    v32 = 456; memory.writeInt32 (V32_ADDRESS, v32);
    v64 = 789; memory.writeInt64 (V64_ADDRESS, v64);

    // Get the stats and then reset them
    var stats = memory.getStats (true);
    stats.systemReads;  // 3
    stats.cachedReads;  // 0
    stats.systemWrites; // 3

    // Create memory cache for faster reads
    memory.createCache (4096, 4096, 16384);
    memory.getCacheSize(); // 16384

    // Read memory through memory cache
    v16 = memory.readInt16 (V16_ADDRESS);
    v32 = memory.readInt32 (V32_ADDRESS);
    v64 = memory.readInt64 (V64_ADDRESS);

    // Get the new stats again
    stats = memory.getStats();
    stats.systemReads;  // 1
    stats.cachedReads;  // 3
    stats.systemWrites; // 0

    // Clear the memory cache whenever
    // fresh reads need to be performed
    memory.clearCache();

    memory.isCaching  (); // True
    memory.deleteCache();
    memory.isCaching  (); // False

    // Perform pattern search
    var list = memory.find
        ("04 08 ? 16 23 42");

    // List is an array
    list.map (function (a)
    {
        a; // Found address
    });
}