| 1 | /* |
| 2 | * Copyright (C) 2020, Matthias Weckbecker <matthias@weckbecker.name> |
| 3 | * |
| 4 | * License: GNU GPL, version 2 or later. |
| 5 | * See the COPYING file in the top-level directory. |
| 6 | */ |
| 7 | #include <inttypes.h> |
| 8 | #include <assert.h> |
| 9 | #include <stdlib.h> |
| 10 | #include <string.h> |
| 11 | #include <unistd.h> |
| 12 | #include <stdio.h> |
| 13 | #include <glib.h> |
| 14 | |
| 15 | #include <qemu-plugin.h> |
| 16 | |
| 17 | QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION; |
| 18 | |
| 19 | typedef struct { |
| 20 | int64_t num; |
| 21 | int64_t calls; |
| 22 | int64_t errors; |
| 23 | } SyscallStats; |
| 24 | |
| 25 | struct SyscallInfo { |
| 26 | const char *name; |
| 27 | int64_t write_sysno; |
| 28 | }; |
| 29 | |
| 30 | static const struct SyscallInfo arch_syscall_info[] = { |
| 31 | { "aarch64", 64 }, |
| 32 | { "aarch64_be", 64 }, |
| 33 | { "alpha", 4 }, |
| 34 | { "arm", 4 }, |
| 35 | { "armeb", 4 }, |
| 36 | { "avr", -1 }, |
| 37 | { "hexagon", 64 }, |
| 38 | { "hppa", -1 }, |
| 39 | { "i386", 4 }, |
| 40 | { "loongarch64", -1 }, |
| 41 | { "m68k", 4 }, |
| 42 | { "microblaze", 4 }, |
| 43 | { "microblazeel", 4 }, |
| 44 | { "mips", 1 }, |
| 45 | { "mips64", 1 }, |
| 46 | { "mips64el", 1 }, |
| 47 | { "mipsel", 1 }, |
| 48 | { "mipsn32", 1 }, |
| 49 | { "mipsn32el", 1 }, |
| 50 | { "or1k", -1 }, |
| 51 | { "ppc", 4 }, |
| 52 | { "ppc64", 4 }, |
| 53 | { "ppc64le", 4 }, |
| 54 | { "riscv32", 64 }, |
| 55 | { "riscv64", 64 }, |
| 56 | { "rx", -1 }, |
| 57 | { "s390x", -1 }, |
| 58 | { "sh4", -1 }, |
| 59 | { "sh4eb", -1 }, |
| 60 | { "sparc", 4 }, |
| 61 | { "sparc32plus", 4 }, |
| 62 | { "sparc64", 4 }, |
| 63 | { "tricore", -1 }, |
| 64 | { "x86_64", 1 }, |
| 65 | { "xtensa", 13 }, |
| 66 | { "xtensaeb", 13 }, |
| 67 | { NULL, -1 }, |
| 68 | }; |
| 69 | |
| 70 | static GMutex lock; |
| 71 | static GHashTable *statistics; |
| 72 | static GByteArray *memory_buffer; |
| 73 | static bool do_log_writes; |
| 74 | static int64_t write_sysno = -1; |
| 75 | |
| 76 | static SyscallStats *get_or_create_entry(int64_t num) |
| 77 | { |
| 78 | SyscallStats *entry = |
| 79 | (SyscallStats *) g_hash_table_lookup(statistics, &num); |
| 80 | |
| 81 | if (!entry) { |
| 82 | entry = g_new0(SyscallStats, 1); |
| 83 | entry->num = num; |
| 84 | g_hash_table_insert(statistics, &entry->num, entry); |
| 85 | } |
| 86 | |
| 87 | return entry; |
| 88 | } |
| 89 | |
| 90 | /* |
| 91 | * Hex-dump a GByteArray to the QEMU plugin output in the format: |
| 92 | * 61 63 63 65 6c 09 09 20 20 20 66 70 75 09 09 09 | accel.....fpu... |
| 93 | * 20 6d 6f 64 75 6c 65 2d 63 6f 6d 6d 6f 6e 2e 63 | .module-common.c |
| 94 | */ |
| 95 | static void hexdump(const GByteArray *data) |
| 96 | { |
| 97 | g_autoptr(GString) out = g_string_new(""); |
| 98 | |
| 99 | for (guint index = 0; index < data->len; index += 16) { |
| 100 | for (guint col = 0; col < 16; col++) { |
| 101 | if (index + col < data->len) { |
| 102 | g_string_append_printf(out, "%02x ", data->data[index + col]); |
| 103 | } else { |
| 104 | g_string_append(out, " "); |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | g_string_append(out, " | "); |
| 109 | |
| 110 | for (guint col = 0; col < 16; col++) { |
| 111 | if (index + col >= data->len) { |
| 112 | break; |
| 113 | } |
| 114 | |
| 115 | if (g_ascii_isgraph(data->data[index + col])) { |
| 116 | g_string_append_printf(out, "%c", data->data[index + col]); |
| 117 | } else { |
| 118 | g_string_append(out, "."); |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | g_string_append(out, "\n"); |
| 123 | } |
| 124 | |
| 125 | qemu_plugin_outs(out->str); |
| 126 | } |
| 127 | |
| 128 | static void vcpu_syscall(unsigned int vcpu_index, |
| 129 | int64_t num, uint64_t a1, uint64_t a2, |
| 130 | uint64_t a3, uint64_t a4, uint64_t a5, |
| 131 | uint64_t a6, uint64_t a7, uint64_t a8, |
| 132 | void *userdata) |
| 133 | { |
| 134 | if (statistics) { |
| 135 | SyscallStats *entry; |
| 136 | g_mutex_lock(&lock); |
| 137 | entry = get_or_create_entry(num); |
| 138 | entry->calls++; |
| 139 | g_mutex_unlock(&lock); |
| 140 | } else { |
| 141 | g_autofree gchar *out = g_strdup_printf("syscall #%" PRIi64 "\n", num); |
| 142 | qemu_plugin_outs(out); |
| 143 | } |
| 144 | |
| 145 | if (do_log_writes && num == write_sysno) { |
| 146 | if (qemu_plugin_read_memory_vaddr(a2, memory_buffer, a3)) { |
| 147 | hexdump(memory_buffer); |
| 148 | } else { |
| 149 | fprintf(stderr, "Error reading memory from vaddr %"PRIu64"\n", a2); |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | static void vcpu_syscall_ret(unsigned int vcpu_idx, |
| 155 | int64_t num, int64_t ret, |
| 156 | void *userdata) |
| 157 | { |
| 158 | if (statistics) { |
| 159 | SyscallStats *entry; |
| 160 | |
| 161 | g_mutex_lock(&lock); |
| 162 | /* Should always return an existent entry. */ |
| 163 | entry = get_or_create_entry(num); |
| 164 | if (ret < 0) { |
| 165 | entry->errors++; |
| 166 | } |
| 167 | g_mutex_unlock(&lock); |
| 168 | } else { |
| 169 | g_autofree gchar *out = g_strdup_printf( |
| 170 | "syscall #%" PRIi64 " returned -> %" PRIi64 "\n", num, ret); |
| 171 | qemu_plugin_outs(out); |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | static bool vcpu_syscall_filter(unsigned int vcpu_index, |
| 176 | int64_t num, uint64_t a1, uint64_t a2, |
| 177 | uint64_t a3, uint64_t a4, uint64_t a5, |
| 178 | uint64_t a6, uint64_t a7, uint64_t a8, |
| 179 | int64_t *sysret, void *userdata) |
| 180 | { |
| 181 | /* Special syscall to test the filter functionality. */ |
| 182 | if (num == 4096 && a1 == 0x66CCFF) { |
| 183 | *sysret = 0xFFCC66; |
| 184 | |
| 185 | if (!statistics) { |
| 186 | qemu_plugin_outs("magic syscall filtered, set magic return\n"); |
| 187 | } |
| 188 | return true; |
| 189 | } |
| 190 | return false; |
| 191 | } |
| 192 | |
| 193 | static void print_entry(gpointer val, gpointer user_data) |
| 194 | { |
| 195 | SyscallStats *entry = (SyscallStats *) val; |
| 196 | int64_t syscall_num = entry->num; |
| 197 | g_autofree gchar *out = g_strdup_printf( |
| 198 | "%-13" PRIi64 "%-6" PRIi64 " %" PRIi64 "\n", |
| 199 | syscall_num, entry->calls, entry->errors); |
| 200 | qemu_plugin_outs(out); |
| 201 | } |
| 202 | |
| 203 | static gint comp_func(gconstpointer ea, gconstpointer eb, gpointer d) |
| 204 | { |
| 205 | SyscallStats *ent_a = (SyscallStats *) ea; |
| 206 | SyscallStats *ent_b = (SyscallStats *) eb; |
| 207 | |
| 208 | return ent_a->calls > ent_b->calls ? -1 : 1; |
| 209 | } |
| 210 | |
| 211 | /* ************************************************************************* */ |
| 212 | static void plugin_exit(void *p) |
| 213 | { |
| 214 | if (!statistics) { |
| 215 | return; |
| 216 | } |
| 217 | |
| 218 | g_mutex_lock(&lock); |
| 219 | GList *entries = g_hash_table_get_values(statistics); |
| 220 | entries = g_list_sort_with_data(entries, comp_func, NULL); |
| 221 | qemu_plugin_outs("syscall no. calls errors\n"); |
| 222 | |
| 223 | g_list_foreach(entries, print_entry, NULL); |
| 224 | |
| 225 | g_list_free(entries); |
| 226 | g_hash_table_destroy(statistics); |
| 227 | g_mutex_unlock(&lock); |
| 228 | } |
| 229 | |
| 230 | QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id, |
| 231 | const qemu_info_t *info, |
| 232 | int argc, char **argv) |
| 233 | { |
| 234 | bool do_print = false; |
| 235 | |
| 236 | for (int i = 0; i < argc; i++) { |
| 237 | char *opt = argv[i]; |
| 238 | g_auto(GStrv) tokens = g_strsplit(opt, "=", 2); |
| 239 | |
| 240 | if (g_strcmp0(tokens[0], "print") == 0) { |
| 241 | if (!qemu_plugin_bool_parse(tokens[0], tokens[1], &do_print)) { |
| 242 | fprintf(stderr, "boolean argument parsing failed: %s\n", opt); |
| 243 | } |
| 244 | } else if (g_strcmp0(tokens[0], "log_writes") == 0) { |
| 245 | if (!qemu_plugin_bool_parse(tokens[0], tokens[1], &do_log_writes)) { |
| 246 | fprintf(stderr, "boolean argument parsing failed: %s\n", opt); |
| 247 | } |
| 248 | } else { |
| 249 | fprintf(stderr, "unsupported argument: %s\n", argv[i]); |
| 250 | return -1; |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | if (!do_print) { |
| 255 | statistics = g_hash_table_new_full(g_int64_hash, g_int64_equal, NULL, g_free); |
| 256 | } |
| 257 | |
| 258 | if (do_log_writes) { |
| 259 | for (const struct SyscallInfo *syscall_info = arch_syscall_info; |
| 260 | syscall_info->name != NULL; syscall_info++) { |
| 261 | |
| 262 | if (g_strcmp0(syscall_info->name, info->target_name) == 0) { |
| 263 | write_sysno = syscall_info->write_sysno; |
| 264 | break; |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | if (write_sysno == -1) { |
| 269 | fprintf(stderr, "write syscall number not found\n"); |
| 270 | return -1; |
| 271 | } |
| 272 | |
| 273 | memory_buffer = g_byte_array_new(); |
| 274 | } |
| 275 | |
| 276 | qemu_plugin_register_vcpu_syscall_cb(id, vcpu_syscall, NULL); |
| 277 | qemu_plugin_register_vcpu_syscall_ret_cb(id, vcpu_syscall_ret, NULL); |
| 278 | qemu_plugin_register_vcpu_syscall_filter_cb(id, vcpu_syscall_filter, NULL); |
| 279 | qemu_plugin_register_atexit_cb(id, plugin_exit, NULL); |
| 280 | return 0; |
| 281 | } |