| /* |
| * Control Flow plugin |
| * |
| * This plugin will track changes to control flow and detect where |
| * instructions fault. |
| * |
| * Copyright (c) 2024 Linaro Ltd |
| * |
| * SPDX-License-Identifier: GPL-2.0-or-later |
| */ |
| #include <glib.h> |
| #include <inttypes.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <unistd.h> |
| |
| #include <qemu-plugin.h> |
| |
| QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION; |
| |
| typedef enum { |
| SORT_HOTTEST, /* hottest branch insn */ |
| SORT_EXCEPTION, /* most early exits */ |
| SORT_POPDEST, /* most destinations (usually ret's) */ |
| } ReportType; |
| |
| ReportType report = SORT_HOTTEST; |
| int topn = 10; |
| |
| typedef struct { |
| uint64_t daddr; |
| uint64_t dcount; |
| } DestData; |
| |
| /* A node is an address where we can go to multiple places */ |
| typedef struct { |
| GMutex lock; |
| /* address of the branch point */ |
| uint64_t addr; |
| /* array of DestData */ |
| GArray *dests; |
| /* early exit/fault count */ |
| uint64_t early_exit; |
| /* jump destination count */ |
| uint64_t dest_count; |
| /* instruction data */ |
| char *insn_disas; |
| /* symbol? */ |
| const char *symbol; |
| /* times translated as last in block? */ |
| int last_count; |
| /* times translated in the middle of block? */ |
| int mid_count; |
| } NodeData; |
| |
| typedef enum { |
| /* last insn in block, expected flow control */ |
| LAST_INSN = (1 << 0), |
| /* mid-block insn, can only be an exception */ |
| EXCP_INSN = (1 << 1), |
| /* multiple disassembly, may have changed */ |
| MULT_INSN = (1 << 2), |
| } InsnTypes; |
| |
| typedef struct { |
| /* address of the branch point */ |
| uint64_t addr; |
| /* disassembly */ |
| char *insn_disas; |
| /* symbol? */ |
| const char *symbol; |
| /* types */ |
| InsnTypes type_flag; |
| } InsnData; |
| |
| /* We use this to track the current execution state */ |
| typedef struct { |
| /* address of end of block */ |
| uint64_t end_block; |
| /* next pc after end of block */ |
| uint64_t pc_after_block; |
| /* address of last executed PC */ |
| uint64_t last_pc; |
| } VCPUScoreBoard; |
| |
| /* descriptors for accessing the above scoreboard */ |
| static qemu_plugin_u64 end_block; |
| static qemu_plugin_u64 pc_after_block; |
| static qemu_plugin_u64 last_pc; |
| |
| |
| static GMutex node_lock; |
| static GHashTable *nodes; |
| struct qemu_plugin_scoreboard *state; |
| |
| /* SORT_HOTTEST */ |
| static gint hottest(gconstpointer a, gconstpointer b) |
| { |
| NodeData *na = (NodeData *) a; |
| NodeData *nb = (NodeData *) b; |
| |
| return na->dest_count > nb->dest_count ? -1 : |
| na->dest_count == nb->dest_count ? 0 : 1; |
| } |
| |
| static gint exception(gconstpointer a, gconstpointer b) |
| { |
| NodeData *na = (NodeData *) a; |
| NodeData *nb = (NodeData *) b; |
| |
| return na->early_exit > nb->early_exit ? -1 : |
| na->early_exit == nb->early_exit ? 0 : 1; |
| } |
| |
| static gint popular(gconstpointer a, gconstpointer b) |
| { |
| NodeData *na = (NodeData *) a; |
| NodeData *nb = (NodeData *) b; |
| |
| return na->dests->len > nb->dests->len ? -1 : |
| na->dests->len == nb->dests->len ? 0 : 1; |
| } |
| |
| /* Filter out non-branches - returns true to remove entry */ |
| static gboolean filter_non_branches(gpointer key, gpointer value, |
| gpointer user_data) |
| { |
| NodeData *node = (NodeData *) value; |
| |
| return node->dest_count == 0; |
| } |
| |
| static void plugin_exit(qemu_plugin_id_t id, void *p) |
| { |
| g_autoptr(GString) result = g_string_new("collected "); |
| GList *data; |
| GCompareFunc sort = &hottest; |
| int i = 0; |
| |
| g_mutex_lock(&node_lock); |
| g_string_append_printf(result, "%d control flow nodes in the hash table\n", |
| g_hash_table_size(nodes)); |
| |
| /* remove all nodes that didn't branch */ |
| g_hash_table_foreach_remove(nodes, filter_non_branches, NULL); |
| |
| data = g_hash_table_get_values(nodes); |
| |
| switch (report) { |
| case SORT_HOTTEST: |
| sort = &hottest; |
| break; |
| case SORT_EXCEPTION: |
| sort = &exception; |
| break; |
| case SORT_POPDEST: |
| sort = &popular; |
| break; |
| } |
| |
| data = g_list_sort(data, sort); |
| |
| for (GList *l = data; |
| l != NULL && i < topn; |
| l = l->next, i++) { |
| NodeData *n = l->data; |
| const char *type = n->mid_count ? "sync fault" : "branch"; |
| g_string_append_printf(result, " addr: 0x%"PRIx64 " %s: %s (%s)\n", |
| n->addr, n->symbol, n->insn_disas, type); |
| if (n->early_exit) { |
| g_string_append_printf(result, " early exits %"PRId64"\n", |
| n->early_exit); |
| } |
| g_string_append_printf(result, " branches %"PRId64"\n", |
| n->dest_count); |
| for (int j = 0; j < n->dests->len; j++) { |
| DestData *dd = &g_array_index(n->dests, DestData, j); |
| g_string_append_printf(result, " to 0x%"PRIx64" (%"PRId64")\n", |
| dd->daddr, dd->dcount); |
| } |
| } |
| |
| qemu_plugin_outs(result->str); |
| |
| g_mutex_unlock(&node_lock); |
| } |
| |
| static void plugin_init(void) |
| { |
| g_mutex_init(&node_lock); |
| nodes = g_hash_table_new(NULL, g_direct_equal); |
| state = qemu_plugin_scoreboard_new(sizeof(VCPUScoreBoard)); |
| |
| /* score board declarations */ |
| end_block = qemu_plugin_scoreboard_u64_in_struct(state, VCPUScoreBoard, |
| end_block); |
| pc_after_block = qemu_plugin_scoreboard_u64_in_struct(state, VCPUScoreBoard, |
| pc_after_block); |
| last_pc = qemu_plugin_scoreboard_u64_in_struct(state, VCPUScoreBoard, |
| last_pc); |
| } |
| |
| static NodeData *create_node(uint64_t addr) |
| { |
| NodeData *node = g_new0(NodeData, 1); |
| g_mutex_init(&node->lock); |
| node->addr = addr; |
| node->dests = g_array_new(true, true, sizeof(DestData)); |
| return node; |
| } |
| |
| static NodeData *fetch_node(uint64_t addr, bool create_if_not_found) |
| { |
| NodeData *node = NULL; |
| |
| g_mutex_lock(&node_lock); |
| node = (NodeData *) g_hash_table_lookup(nodes, (gconstpointer) addr); |
| if (!node && create_if_not_found) { |
| node = create_node(addr); |
| g_hash_table_insert(nodes, (gpointer) addr, (gpointer) node); |
| } |
| g_mutex_unlock(&node_lock); |
| return node; |
| } |
| |
| /* |
| * Called when we detect a non-linear execution (pc != |
| * pc_after_block). This could be due to a fault causing some sort of |
| * exit exception (if last_pc != block_end) or just a taken branch. |
| */ |
| static void vcpu_tb_branched_exec(unsigned int cpu_index, void *udata) |
| { |
| uint64_t lpc = qemu_plugin_u64_get(last_pc, cpu_index); |
| uint64_t ebpc = qemu_plugin_u64_get(end_block, cpu_index); |
| uint64_t npc = qemu_plugin_u64_get(pc_after_block, cpu_index); |
| uint64_t pc = GPOINTER_TO_UINT(udata); |
| |
| /* return early for address 0 */ |
| if (!lpc) { |
| return; |
| } |
| |
| NodeData *node = fetch_node(lpc, true); |
| DestData *data = NULL; |
| bool early_exit = (lpc != ebpc); |
| GArray *dests; |
| |
| /* the condition should never hit */ |
| g_assert(pc != npc); |
| |
| g_mutex_lock(&node->lock); |
| |
| if (early_exit) { |
| fprintf(stderr, "%s: pc=%"PRIx64", epbc=%"PRIx64 |
| " npc=%"PRIx64", lpc=%"PRIx64"\n", |
| __func__, pc, ebpc, npc, lpc); |
| node->early_exit++; |
| if (!node->mid_count) { |
| /* count now as we've only just allocated */ |
| node->mid_count++; |
| } |
| } |
| |
| dests = node->dests; |
| for (int i = 0; i < dests->len; i++) { |
| if (g_array_index(dests, DestData, i).daddr == pc) { |
| data = &g_array_index(dests, DestData, i); |
| } |
| } |
| |
| /* we've never seen this before, allocate a new entry */ |
| if (!data) { |
| DestData new_entry = { .daddr = pc }; |
| g_array_append_val(dests, new_entry); |
| data = &g_array_index(dests, DestData, dests->len - 1); |
| g_assert(data->daddr == pc); |
| } |
| |
| data->dcount++; |
| node->dest_count++; |
| |
| g_mutex_unlock(&node->lock); |
| } |
| |
| /* |
| * At the start of each block we need to resolve two things: |
| * |
| * - is last_pc == block_end, if not we had an early exit |
| * - is start of block last_pc + insn width, if not we jumped |
| * |
| * Once those are dealt with we can instrument the rest of the |
| * instructions for their execution. |
| * |
| */ |
| static void vcpu_tb_trans(qemu_plugin_id_t id, struct qemu_plugin_tb *tb) |
| { |
| uint64_t pc = qemu_plugin_tb_vaddr(tb); |
| size_t insns = qemu_plugin_tb_n_insns(tb); |
| struct qemu_plugin_insn *first_insn = qemu_plugin_tb_get_insn(tb, 0); |
| struct qemu_plugin_insn *last_insn = qemu_plugin_tb_get_insn(tb, insns - 1); |
| |
| /* |
| * check if we are executing linearly after the last block. We can |
| * handle both early block exits and normal branches in the |
| * callback if we hit it. |
| */ |
| gpointer udata = GUINT_TO_POINTER(pc); |
| qemu_plugin_register_vcpu_tb_exec_cond_cb( |
| tb, vcpu_tb_branched_exec, QEMU_PLUGIN_CB_NO_REGS, |
| QEMU_PLUGIN_COND_NE, pc_after_block, pc, udata); |
| |
| /* |
| * Now we can set start/end for this block so the next block can |
| * check where we are at. Do this on the first instruction and not |
| * the TB so we don't get mixed up with above. |
| */ |
| qemu_plugin_register_vcpu_insn_exec_inline_per_vcpu(first_insn, |
| QEMU_PLUGIN_INLINE_STORE_U64, |
| end_block, qemu_plugin_insn_vaddr(last_insn)); |
| qemu_plugin_register_vcpu_insn_exec_inline_per_vcpu(first_insn, |
| QEMU_PLUGIN_INLINE_STORE_U64, |
| pc_after_block, |
| qemu_plugin_insn_vaddr(last_insn) + |
| qemu_plugin_insn_size(last_insn)); |
| |
| for (int idx = 0; idx < qemu_plugin_tb_n_insns(tb); ++idx) { |
| struct qemu_plugin_insn *insn = qemu_plugin_tb_get_insn(tb, idx); |
| uint64_t ipc = qemu_plugin_insn_vaddr(insn); |
| /* |
| * If this is a potential branch point check if we could grab |
| * the disassembly for it. If it is the last instruction |
| * always create an entry. |
| */ |
| NodeData *node = fetch_node(ipc, last_insn); |
| if (node) { |
| g_mutex_lock(&node->lock); |
| if (!node->insn_disas) { |
| node->insn_disas = qemu_plugin_insn_disas(insn); |
| } |
| if (!node->symbol) { |
| node->symbol = qemu_plugin_insn_symbol(insn); |
| } |
| if (last_insn == insn) { |
| node->last_count++; |
| } else { |
| node->mid_count++; |
| } |
| g_mutex_unlock(&node->lock); |
| } |
| |
| /* Store the PC of what we are about to execute */ |
| qemu_plugin_register_vcpu_insn_exec_inline_per_vcpu(insn, |
| QEMU_PLUGIN_INLINE_STORE_U64, |
| last_pc, ipc); |
| } |
| } |
| |
| QEMU_PLUGIN_EXPORT |
| int qemu_plugin_install(qemu_plugin_id_t id, const qemu_info_t *info, |
| int argc, char **argv) |
| { |
| for (int i = 0; i < argc; i++) { |
| char *opt = argv[i]; |
| g_auto(GStrv) tokens = g_strsplit(opt, "=", 2); |
| if (g_strcmp0(tokens[0], "sort") == 0) { |
| if (g_strcmp0(tokens[1], "hottest") == 0) { |
| report = SORT_HOTTEST; |
| } else if (g_strcmp0(tokens[1], "early") == 0) { |
| report = SORT_EXCEPTION; |
| } else if (g_strcmp0(tokens[1], "exceptions") == 0) { |
| report = SORT_POPDEST; |
| } else { |
| fprintf(stderr, "failed to parse: %s\n", tokens[1]); |
| return -1; |
| } |
| } else { |
| fprintf(stderr, "option parsing failed: %s\n", opt); |
| return -1; |
| } |
| } |
| |
| plugin_init(); |
| |
| qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans); |
| qemu_plugin_register_atexit_cb(id, plugin_exit, NULL); |
| return 0; |
| } |