summaryrefslogtreecommitdiff
path: root/library/VTableInterpose.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'library/VTableInterpose.cpp')
-rw-r--r--library/VTableInterpose.cpp513
1 files changed, 513 insertions, 0 deletions
diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp
new file mode 100644
index 00000000..48ae6109
--- /dev/null
+++ b/library/VTableInterpose.cpp
@@ -0,0 +1,513 @@
+/*
+https://github.com/peterix/dfhack
+Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com)
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any
+damages arising from the use of this software.
+
+Permission is granted to anyone to use this software for any
+purpose, including commercial applications, and to alter it and
+redistribute it freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must
+not claim that you wrote the original software. If you use this
+software in a product, an acknowledgment in the product documentation
+would be appreciated but is not required.
+
+2. Altered source versions must be plainly marked as such, and
+must not be misrepresented as being the original software.
+
+3. This notice may not be removed or altered from any source
+distribution.
+*/
+
+#include "Internal.h"
+
+#include <string>
+#include <vector>
+#include <map>
+
+#include "MemAccess.h"
+#include "Core.h"
+#include "VersionInfo.h"
+#include "VTableInterpose.h"
+
+#include "MiscUtils.h"
+
+using namespace DFHack;
+
+/*
+ * Code for accessing method pointers directly. Very compiler-specific.
+ */
+
+#if defined(_MSC_VER)
+
+struct MSVC_MPTR {
+ void *method;
+ intptr_t this_shift;
+};
+
+static uint32_t *follow_jmp(void *ptr)
+{
+ uint8_t *p = (uint8_t*)ptr;
+
+ for (;;)
+ {
+ switch (*p)
+ {
+ case 0xE9:
+ p += 5 + *(int32_t*)(p+1);
+ break;
+ case 0xEB:
+ p += 2 + *(int8_t*)(p+1);
+ break;
+ default:
+ return (uint32_t*)p;
+ }
+ }
+}
+
+bool DFHack::is_vmethod_pointer_(void *pptr)
+{
+ auto pobj = (MSVC_MPTR*)pptr;
+ if (!pobj->method) return false;
+
+ // MSVC implements pointers to vmethods via thunks.
+ // This expects that they all follow a very specific pattern.
+ auto pval = follow_jmp(pobj->method);
+ switch (pval[0]) {
+ case 0x20FF018BU: // mov eax, [ecx]; jmp [eax]
+ case 0x60FF018BU: // mov eax, [ecx]; jmp [eax+0x??]
+ case 0xA0FF018BU: // mov eax, [ecx]; jmp [eax+0x????????]
+ return true;
+ default:
+ return false;
+ }
+}
+
+int DFHack::vmethod_pointer_to_idx_(void *pptr)
+{
+ auto pobj = (MSVC_MPTR*)pptr;
+ if (!pobj->method || pobj->this_shift != 0) return -1;
+
+ auto pval = follow_jmp(pobj->method);
+ switch (pval[0]) {
+ case 0x20FF018BU: // mov eax, [ecx]; jmp [eax]
+ return 0;
+ case 0x60FF018BU: // mov eax, [ecx]; jmp [eax+0x??]
+ return ((int8_t)pval[1])/sizeof(void*);
+ case 0xA0FF018BU: // mov eax, [ecx]; jmp [eax+0x????????]
+ return ((int32_t)pval[1])/sizeof(void*);
+ default:
+ return -1;
+ }
+}
+
+void* DFHack::method_pointer_to_addr_(void *pptr)
+{
+ if (is_vmethod_pointer_(pptr)) return NULL;
+ auto pobj = (MSVC_MPTR*)pptr;
+ return pobj->method;
+}
+
+void DFHack::addr_to_method_pointer_(void *pptr, void *addr)
+{
+ auto pobj = (MSVC_MPTR*)pptr;
+ pobj->method = addr;
+ pobj->this_shift = 0;
+}
+
+#elif defined(__GXX_ABI_VERSION)
+
+struct GCC_MPTR {
+ intptr_t method;
+ intptr_t this_shift;
+};
+
+bool DFHack::is_vmethod_pointer_(void *pptr)
+{
+ auto pobj = (GCC_MPTR*)pptr;
+ return (pobj->method & 1) != 0;
+}
+
+int DFHack::vmethod_pointer_to_idx_(void *pptr)
+{
+ auto pobj = (GCC_MPTR*)pptr;
+ if ((pobj->method & 1) == 0 || pobj->this_shift != 0)
+ return -1;
+ return (pobj->method-1)/sizeof(void*);
+}
+
+void* DFHack::method_pointer_to_addr_(void *pptr)
+{
+ auto pobj = (GCC_MPTR*)pptr;
+ if ((pobj->method & 1) != 0 || pobj->this_shift != 0)
+ return NULL;
+ return (void*)pobj->method;
+}
+
+void DFHack::addr_to_method_pointer_(void *pptr, void *addr)
+{
+ auto pobj = (GCC_MPTR*)pptr;
+ pobj->method = (intptr_t)addr;
+ pobj->this_shift = 0;
+}
+
+#else
+#error Unknown compiler type
+#endif
+
+void *virtual_identity::get_vmethod_ptr(int idx)
+{
+ assert(idx >= 0);
+ void **vtable = (void**)vtable_ptr;
+ if (!vtable) return NULL;
+ return vtable[idx];
+}
+
+bool virtual_identity::set_vmethod_ptr(int idx, void *ptr)
+{
+ assert(idx >= 0);
+ void **vtable = (void**)vtable_ptr;
+ if (!vtable) return NULL;
+ return Core::getInstance().p->patchMemory(&vtable[idx], &ptr, sizeof(void*));
+}
+
+/*
+ VMethod interposing data structures.
+
+ In order to properly support adding and removing hooks,
+ it is necessary to track them. This is what this class
+ is for. The task is further complicated by propagating
+ hooks to child classes that use exactly the same original
+ vmethod implementation.
+
+ Every applied link contains in the saved_chain field a
+ pointer to the next vmethod body that should be called
+ by the hook the link represents. This is the actual
+ control flow structure that needs to be maintained.
+
+ There also are connections between link objects themselves,
+ which constitute the bookkeeping for doing that. Finally,
+ every link is associated with a fixed virtual_identity host,
+ which represents the point in the class hierarchy where
+ the hook is applied.
+
+ When there are no subclasses (i.e. only one host), the
+ structures look like this:
+
+ +--------------+ +------------+
+ | link1 |-next------->| link2 |-next=NULL
+ |s_c: original |<-------prev-|s_c: $link1 |<--+
+ +--------------+ +------------+ |
+ |
+ host->interpose_list[vmethod_idx] ------+
+ vtable: $link2
+
+ The original vtable entry is stored in the saved_chain of the
+ first link. The interpose_list map points to the last one.
+ The hooks are called in order: link2 -> link1 -> original.
+
+ When there are subclasses that use the same vmethod, but don't
+ hook it, the topmost link gets a set of the child_hosts, and
+ the hosts have the link added to their interpose_list:
+
+ +--------------+ +----------------+
+ | link0 @host0 |<--+-interpose_list-| host1 |
+ | |-child_hosts-+----->| vtable: $link |
+ +--------------+ | | +----------------+
+ | |
+ | | +----------------+
+ +-interpose_list-| host2 |
+ +----->| vtable: $link |
+ +----------------+
+
+ When a child defines its own hook, the child_hosts link is
+ severed and replaced with a child_next pointer to the new
+ hook. The hook still points back the chain with prev.
+ All child links to subclasses of host2 are migrated from
+ link1 to link2.
+
+ +--------------+-next=NULL +--------------+-next=NULL
+ | link1 @host1 |-child_next------->| link2 @host2 |-child_*--->subclasses
+ | |<-------------prev-|s_c: $link1 |
+ +--------------+<-------+ +--------------+<-------+
+ | |
+ +--------------+ | +--------------+ |
+ | host1 |-i_list-+ | host2 |-i_list-+
+ |vtable: $link1| |vtable: $link2|
+ +--------------+ +--------------+
+
+ */
+
+void VMethodInterposeLinkBase::set_chain(void *chain)
+{
+ saved_chain = chain;
+ addr_to_method_pointer_(chain_mptr, chain);
+}
+
+VMethodInterposeLinkBase::VMethodInterposeLinkBase(virtual_identity *host, int vmethod_idx, void *interpose_method, void *chain_mptr, int priority)
+ : host(host), vmethod_idx(vmethod_idx), interpose_method(interpose_method),
+ chain_mptr(chain_mptr), priority(priority),
+ applied(false), saved_chain(NULL), next(NULL), prev(NULL)
+{
+ if (vmethod_idx < 0 || interpose_method == NULL)
+ {
+ fprintf(stderr, "Bad VMethodInterposeLinkBase arguments: %d %08x\n",
+ vmethod_idx, unsigned(interpose_method));
+ fflush(stderr);
+ abort();
+ }
+}
+
+VMethodInterposeLinkBase::~VMethodInterposeLinkBase()
+{
+ if (is_applied())
+ remove();
+}
+
+VMethodInterposeLinkBase *VMethodInterposeLinkBase::get_first_interpose(virtual_identity *id)
+{
+ auto item = id->interpose_list[vmethod_idx];
+ if (!item)
+ return NULL;
+
+ if (item->host != id)
+ return NULL;
+ while (item->prev && item->prev->host == id)
+ item = item->prev;
+
+ return item;
+}
+
+void VMethodInterposeLinkBase::find_child_hosts(virtual_identity *cur, void *vmptr)
+{
+ auto &children = cur->getChildren();
+
+ for (size_t i = 0; i < children.size(); i++)
+ {
+ auto child = static_cast<virtual_identity*>(children[i]);
+ auto base = get_first_interpose(child);
+
+ if (base)
+ {
+ assert(base->prev == NULL);
+
+ if (base->saved_chain != vmptr)
+ continue;
+
+ child_next.insert(base);
+ }
+ else
+ {
+ void *cptr = child->get_vmethod_ptr(vmethod_idx);
+ if (cptr != vmptr)
+ continue;
+
+ child_hosts.insert(child);
+ find_child_hosts(child, vmptr);
+ }
+ }
+}
+
+void VMethodInterposeLinkBase::on_host_delete(virtual_identity *from)
+{
+ if (from == host)
+ {
+ // When in own host, fully delete
+ remove();
+ }
+ else
+ {
+ // Otherwise, drop the link to that child:
+ assert(child_hosts.count(from) != 0 &&
+ from->interpose_list[vmethod_idx] == this);
+
+ // Find and restore the original vmethod ptr
+ auto last = this;
+ while (last->prev) last = last->prev;
+
+ from->set_vmethod_ptr(vmethod_idx, last->saved_chain);
+
+ // Unlink the chains
+ child_hosts.erase(from);
+ from->interpose_list[vmethod_idx] = NULL;
+ }
+}
+
+bool VMethodInterposeLinkBase::apply(bool enable)
+{
+ if (!enable)
+ {
+ remove();
+ return true;
+ }
+
+ if (is_applied())
+ return true;
+ if (!host->vtable_ptr)
+ return false;
+
+ // Retrieve the current vtable entry
+ VMethodInterposeLinkBase *old_link = host->interpose_list[vmethod_idx];
+ VMethodInterposeLinkBase *next_link = NULL;
+
+ while (old_link && old_link->host == host && old_link->priority > priority)
+ {
+ next_link = old_link;
+ old_link = old_link->prev;
+ }
+
+ void *old_ptr = next_link ? next_link->saved_chain : host->get_vmethod_ptr(vmethod_idx);
+ assert(old_ptr != NULL && (!old_link || old_link->interpose_method == old_ptr));
+
+ // Apply the new method ptr
+ set_chain(old_ptr);
+
+ if (next_link)
+ {
+ next_link->set_chain(interpose_method);
+ }
+ else if (!host->set_vmethod_ptr(vmethod_idx, interpose_method))
+ {
+ set_chain(NULL);
+ return false;
+ }
+
+ // Push the current link into the home host
+ applied = true;
+ prev = old_link;
+ next = next_link;
+
+ if (next_link)
+ next_link->prev = this;
+ else
+ host->interpose_list[vmethod_idx] = this;
+
+ child_hosts.clear();
+ child_next.clear();
+
+ if (old_link && old_link->host == host)
+ {
+ // If the old link is home, just push into the plain chain
+ assert(old_link->next == next_link);
+ old_link->next = this;
+
+ // Child links belong to the topmost local entry
+ child_hosts.swap(old_link->child_hosts);
+ child_next.swap(old_link->child_next);
+ }
+ else if (next_link)
+ {
+ if (old_link)
+ {
+ assert(old_link->child_next.count(next_link));
+ old_link->child_next.erase(next_link);
+ old_link->child_next.insert(this);
+ }
+ }
+ else
+ {
+ // If creating a new local chain, find children with same vmethod
+ find_child_hosts(host, old_ptr);
+
+ if (old_link)
+ {
+ // Enter the child chain set
+ assert(old_link->child_hosts.count(host));
+ old_link->child_hosts.erase(host);
+ old_link->child_next.insert(this);
+
+ // Subtract our own children from the parent's sets
+ for (auto it = child_next.begin(); it != child_next.end(); ++it)
+ old_link->child_next.erase(*it);
+ for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it)
+ old_link->child_hosts.erase(*it);
+ }
+ }
+
+ assert (!next_link || (child_next.empty() && child_hosts.empty()));
+
+ // Chain subclass hooks
+ for (auto it = child_next.begin(); it != child_next.end(); ++it)
+ {
+ auto nlink = *it;
+ assert(nlink->saved_chain == old_ptr && nlink->prev == old_link);
+ nlink->set_chain(interpose_method);
+ nlink->prev = this;
+ }
+
+ // Chain passive subclass hosts
+ for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it)
+ {
+ auto nhost = *it;
+ assert(nhost->interpose_list[vmethod_idx] == old_link);
+ nhost->set_vmethod_ptr(vmethod_idx, interpose_method);
+ nhost->interpose_list[vmethod_idx] = this;
+ }
+
+ return true;
+}
+
+void VMethodInterposeLinkBase::remove()
+{
+ if (!is_applied())
+ return;
+
+ // Remove the link from prev to this
+ if (prev)
+ {
+ if (prev->host == host)
+ prev->next = next;
+ else
+ {
+ prev->child_next.erase(this);
+
+ if (next)
+ prev->child_next.insert(next);
+ else
+ prev->child_hosts.insert(host);
+ }
+ }
+
+ if (next)
+ {
+ next->set_chain(saved_chain);
+ next->prev = prev;
+
+ assert(child_next.empty() && child_hosts.empty());
+ }
+ else
+ {
+ // Remove from the list in the identity and vtable
+ host->interpose_list[vmethod_idx] = prev;
+ host->set_vmethod_ptr(vmethod_idx, saved_chain);
+
+ for (auto it = child_next.begin(); it != child_next.end(); ++it)
+ {
+ auto nlink = *it;
+ assert(nlink->saved_chain == interpose_method && nlink->prev == this);
+ nlink->set_chain(saved_chain);
+ nlink->prev = prev;
+ if (prev)
+ prev->child_next.insert(nlink);
+ }
+
+ for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it)
+ {
+ auto nhost = *it;
+ assert(nhost->interpose_list[vmethod_idx] == this);
+ nhost->interpose_list[vmethod_idx] = prev;
+ nhost->set_vmethod_ptr(vmethod_idx, saved_chain);
+ if (prev)
+ prev->child_hosts.insert(nhost);
+ }
+ }
+
+ applied = false;
+ prev = next = NULL;
+ child_next.clear();
+ child_hosts.clear();
+ set_chain(NULL);
+}