diff --git a/include/exec/plugin-gen.h b/include/exec/plugin-gen.h
new file mode 100644
index 0000000000..46a167527e
--- /dev/null
+++ b/include/exec/plugin-gen.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017, Emilio G. Cota <cota@braap.org>
+ *
+ * License: GNU GPL, version 2 or later.
+ *   See the COPYING file in the top-level directory.
+ *
+ * plugin-gen.h - TCG-dependent definitions for generating plugin code
+ *
+ * This header should be included only from plugin.c and C files that emit
+ * TCG code.
+ */
+#ifndef QEMU_PLUGIN_GEN_H
+#define QEMU_PLUGIN_GEN_H
+
+#include "qemu/plugin.h"
+#include "tcg/tcg.h"
+
+#ifdef CONFIG_PLUGINS
+
+void qemu_plugin_gen_vcpu_mem_callbacks(struct qemu_plugin_dyn_cb_arr *arr,
+                                        TCGv vaddr, uint8_t info);
+
+void qemu_plugin_gen_vcpu_udata_callbacks(struct qemu_plugin_dyn_cb_arr *arr);
+
+void qemu_plugin_gen_disable_mem_helpers(void);
+
+void
+qemu_plugin_gen_enable_mem_helpers(const struct qemu_plugin_dyn_cb_arr *orig);
+
+#else /* !CONFIG_PLUGINS */
+
+static inline void
+qemu_plugin_gen_vcpu_mem_callbacks(struct qemu_plugin_dyn_cb_arr *arr,
+                                   TCGv vaddr, uint8_t info)
+{ }
+
+static inline void
+qemu_plugin_gen_vcpu_udata_callbacks(struct qemu_plugin_dyn_cb_arr *arr)
+{ }
+
+static inline void qemu_plugin_gen_disable_mem_helpers(void)
+{ }
+
+static inline void
+qemu_plugin_gen_enable_mem_helpers(const struct qemu_plugin_dyn_cb_arr *orig)
+{ }
+
+#endif /* CONFIG_PLUGINS */
+
+#endif /* QEMU_PLUGIN_GEN_H */
+
diff --git a/accel/tcg/plugin-gen.c b/accel/tcg/plugin-gen.c
new file mode 100644
index 0000000000..75f182be37
--- /dev/null
+++ b/accel/tcg/plugin-gen.c
@@ -0,0 +1,230 @@
+#include "qemu/osdep.h"
+#include "cpu.h"
+#include "tcg/tcg.h"
+#include "tcg/tcg-op.h"
+#include "exec/exec-all.h"
+#include "exec/plugin-gen.h"
+
+static void gen_inline_op(struct qemu_plugin_dyn_cb *cb)
+{
+    TCGv_i64 val = tcg_temp_new_i64();
+    TCGv_ptr ptr = tcg_const_ptr(cb->userp);
+
+    tcg_gen_ld_i64(val, ptr, 0);
+    switch (cb->inline_insn.op) {
+    case QEMU_PLUGIN_INLINE_ADD_U64:
+        tcg_gen_addi_i64(val, val, cb->inline_insn.imm);
+        break;
+    default:
+        g_assert_not_reached();
+    }
+    tcg_gen_st_i64(val, ptr, 0);
+
+    tcg_temp_free_ptr(ptr);
+    tcg_temp_free_i64(val);
+}
+
+static void gen_helper_mem_cb(const char *name, unsigned flags,
+                              qemu_plugin_vcpu_mem_cb_t cb, TCGv_i32 cpu_index,
+                              TCGv_i32 meminfo, TCGv_i64 vaddr, TCGv_ptr udata)
+{
+    TCGHelperInfo info = {
+        .func = cb,
+        .name = name,
+        .flags = flags,
+        .sizemask = dh_sizemask(void, 0) |
+                    dh_sizemask(i32, 1) |
+                    dh_sizemask(i32, 2) |
+                    dh_sizemask(i64, 3) |
+                    dh_sizemask(ptr, 4),
+
+    };
+    TCGTemp *args[] = {
+        tcgv_i32_temp(cpu_index),
+        tcgv_i32_temp(meminfo),
+        tcgv_i64_temp(vaddr),
+        tcgv_ptr_temp(udata),
+    };
+
+    tcg_gen_runtime_helper(&info, NULL, ARRAY_SIZE(args), args);
+}
+
+static void gen_helper_mem_haddr_cb(const char *name, unsigned flags,
+                                    qemu_plugin_vcpu_mem_haddr_cb_t cb,
+                                    TCGv_i32 cpu_index, TCGv_i32 meminfo,
+                                    TCGv_i64 vaddr, TCGv_ptr haddr,
+                                    TCGv_ptr udata)
+{
+    TCGHelperInfo info = {
+        .func = cb,
+        .name = name,
+        .flags = flags,
+        .sizemask = dh_sizemask(void, 0) |
+                    dh_sizemask(i32, 1) |
+                    dh_sizemask(i32, 2) |
+                    dh_sizemask(i64, 3) |
+                    dh_sizemask(ptr, 4) |
+                    dh_sizemask(ptr, 5),
+
+    };
+    TCGTemp *args[] = {
+        tcgv_i32_temp(cpu_index),
+        tcgv_i32_temp(meminfo),
+        tcgv_i64_temp(vaddr),
+        tcgv_ptr_temp(haddr),
+        tcgv_ptr_temp(udata),
+    };
+
+    tcg_gen_runtime_helper(&info, NULL, ARRAY_SIZE(args), args);
+}
+
+static void gen_mem_cb(struct qemu_plugin_dyn_cb *cb, TCGv vaddr, uint8_t info)
+{
+    TCGv_i32 cpu_index = tcg_temp_new_i32();
+    TCGv_i32 meminfo = tcg_const_i32(info);
+    TCGv_i64 vaddr64 = tcg_temp_new_i64();
+    TCGv_ptr udata = tcg_const_ptr(cb->userp);
+    TCGv_ptr haddr;
+
+    tcg_gen_ld_i32(cpu_index, cpu_env,
+                   -ENV_OFFSET + offsetof(CPUState, cpu_index));
+    tcg_gen_extu_tl_i64(vaddr64, vaddr);
+
+    if (cb->mem.haddr) {
+#ifdef CONFIG_SOFTMMU
+        haddr = tcg_temp_new_ptr();
+        tcg_gen_ld_ptr(haddr, cpu_env, offsetof(CPUArchState, hostaddr));
+#else
+        haddr = tcg_const_ptr(NULL);
+#endif
+        gen_helper_mem_haddr_cb("helper_plugin_vcpu_mem_haddr_cb",
+                                cb->tcg_flags, cb->f.vcpu_mem_haddr,
+                                cpu_index, meminfo, vaddr64, haddr, udata);
+        tcg_temp_free_ptr(haddr);
+    } else {
+        gen_helper_mem_cb("helper_plugin_vcpu_mem_cb", cb->tcg_flags,
+                          cb->f.vcpu_mem, cpu_index, meminfo, vaddr64,
+                          udata);
+    }
+
+    tcg_temp_free_ptr(udata);
+    tcg_temp_free_i64(vaddr64);
+    tcg_temp_free_i32(meminfo);
+    tcg_temp_free_i32(cpu_index);
+}
+
+void qemu_plugin_gen_vcpu_mem_callbacks(struct qemu_plugin_dyn_cb_arr *arr,
+                                        TCGv vaddr, uint8_t info)
+{
+    size_t i;
+
+    for (i = 0; i < arr->n; i++) {
+        struct qemu_plugin_dyn_cb *cb = &arr->data[i];
+
+        switch (cb->type) {
+        case QEMU_PLUGIN_DYN_CB_TYPE_REGULAR:
+            gen_mem_cb(cb, vaddr, info);
+            break;
+        case QEMU_PLUGIN_DYN_CB_TYPE_INLINE:
+            gen_inline_op(cb);
+            break;
+        default:
+            g_assert_not_reached();
+        }
+    }
+}
+
+static void gen_helper_vcpu_udata_cb(const char *name, unsigned flags,
+                                     qemu_plugin_vcpu_udata_cb_t cb,
+                                     TCGv_i32 cpu_index, TCGv_ptr udata)
+{
+    TCGHelperInfo info = {
+        .func = cb,
+        .name = name,
+        .flags = flags,
+        .sizemask = dh_sizemask(void, 0) |
+                    dh_sizemask(i32, 1) |
+                    dh_sizemask(ptr, 2),
+    };
+    TCGTemp *args[] = {
+        tcgv_i32_temp(cpu_index),
+        tcgv_ptr_temp(udata),
+    };
+
+    tcg_gen_runtime_helper(&info, NULL, ARRAY_SIZE(args), args);
+}
+
+static void gen_vcpu_udata_cb(struct qemu_plugin_dyn_cb *cb)
+{
+    TCGv_i32 cpu_index = tcg_temp_new_i32();
+    TCGv_ptr udata = tcg_const_ptr(cb->userp);
+
+    tcg_gen_ld_i32(cpu_index, cpu_env,
+                   -ENV_OFFSET + offsetof(CPUState, cpu_index));
+
+    gen_helper_vcpu_udata_cb("helper_plugin_vcpu_udata_cb", cb->tcg_flags,
+                             cb->f.vcpu_udata, cpu_index, udata);
+
+    tcg_temp_free_ptr(udata);
+    tcg_temp_free_i32(cpu_index);
+}
+
+void qemu_plugin_gen_vcpu_udata_callbacks(struct qemu_plugin_dyn_cb_arr *arr)
+{
+    size_t i;
+
+    for (i = 0; i < arr->n; i++) {
+        struct qemu_plugin_dyn_cb *cb = &arr->data[i];
+
+        switch (cb->type) {
+        case QEMU_PLUGIN_DYN_CB_TYPE_REGULAR:
+            gen_vcpu_udata_cb(cb);
+            break;
+        case QEMU_PLUGIN_DYN_CB_TYPE_INLINE:
+            gen_inline_op(cb);
+            break;
+        default:
+            g_assert_not_reached();
+        }
+    }
+}
+
+/*
+ * Tracking memory accesses performed from helpers requires extra work.
+ * If an instruction is emulated with helpers, struct qemu_plugin_insn's
+ * .calls_helpers is set. If so, this function is called. Here we do two
+ * things: (1) copy the CB descriptor, and keep track of it so that it can be
+ * freed later on, and (2) point CPUState.plugin_mem_cbs to the descriptor, so
+ * that we can read it at run-time (i.e. when the helper executes).
+ * This run-time access is performed from qemu_plugin_vcpu_mem_cb.
+ *
+ * Note that qemu_plugin_gen_disable_mem_helpers undoes (2).
+ */
+void
+qemu_plugin_gen_enable_mem_helpers(const struct qemu_plugin_dyn_cb_arr *orig)
+{
+    struct qemu_plugin_dyn_cb_arr *arr;
+    TCGv_ptr ptr;
+
+    arr = g_new(struct qemu_plugin_dyn_cb_arr, 1);
+    arr->capacity = orig->n;
+    arr->n = orig->n;
+    arr->data = g_new(struct qemu_plugin_dyn_cb, arr->n);
+    memcpy(arr->data, orig->data, sizeof(*arr->data) * arr->n);
+    qemu_plugin_add_dyn_cb_arr(arr);
+
+    ptr = tcg_const_ptr(arr);
+    tcg_gen_st_ptr(ptr, cpu_env, -ENV_OFFSET + offsetof(CPUState,
+                                                        plugin_mem_cbs));
+    tcg_temp_free_ptr(ptr);
+}
+
+/* Called once we're done instrumenting an instruction that calls helpers */
+void qemu_plugin_gen_disable_mem_helpers(void)
+{
+    TCGv_ptr ptr = tcg_const_ptr(NULL);
+
+    tcg_gen_st_ptr(ptr, cpu_env, -ENV_OFFSET + offsetof(CPUState,
+                                                        plugin_mem_cbs));
+    tcg_temp_free_ptr(ptr);
+}
diff --git a/accel/tcg/Makefile.objs b/accel/tcg/Makefile.objs
index d381a02f34..4f617e4fe5 100644
--- a/accel/tcg/Makefile.objs
+++ b/accel/tcg/Makefile.objs
@@ -6,3 +6,4 @@ obj-y += translator.o
 
 obj-$(CONFIG_USER_ONLY) += user-exec.o
 obj-$(call lnot,$(CONFIG_SOFTMMU)) += user-exec-stub.o
+obj-$(CONFIG_PLUGINS) += plugin-gen.o
