| /* | |
| * 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 current translated block */ | |
| uint64_t tb_pc; | |
| /* 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 tb_pc; | |
| 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, gpointer d) | |
| { | |
| 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, gpointer d) | |
| { | |
| 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, gpointer d) | |
| { | |
| 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(void *p) | |
| { | |
| g_autoptr(GString) result = g_string_new("collected "); | |
| GList *data; | |
| GCompareDataFunc 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_with_data(data, sort, NULL); | |
| 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(g_int64_hash, g_int64_equal); | |
| state = qemu_plugin_scoreboard_new(sizeof(VCPUScoreBoard)); | |
| /* score board declarations */ | |
| tb_pc = qemu_plugin_scoreboard_u64_in_struct(state, VCPUScoreBoard, tb_pc); | |
| 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, &addr); | |
| if (!node && create_if_not_found) { | |
| node = create_node(addr); | |
| g_hash_table_insert(nodes, &node->addr, 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 = qemu_plugin_u64_get(tb_pc, cpu_index); | |
| /* 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(struct qemu_plugin_tb *tb, void *userdata) | |
| { | |
| 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. | |
| */ | |
| qemu_plugin_register_vcpu_tb_exec_inline_per_vcpu( | |
| tb, QEMU_PLUGIN_INLINE_STORE_U64, tb_pc, 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, NULL); | |
| /* | |
| * 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, NULL); | |
| qemu_plugin_register_atexit_cb(id, plugin_exit, NULL); | |
| return 0; | |
| } |