diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/memview.c | 696 |
1 files changed, 696 insertions, 0 deletions
diff --git a/src/memview.c b/src/memview.c new file mode 100644 index 0000000..2abf754 --- /dev/null +++ b/src/memview.c @@ -0,0 +1,696 @@ +#include <unistd.h> +#include <ctype.h> +#include <stdio.h> +#include <string.h> +#include <signal.h> +#include <stdlib.h> +#include <stdbool.h> +#include <stdarg.h> +#include <termios.h> +#include <sys/ioctl.h> +#include <sys/select.h> +#include <errno.h> +#include "mem/io.h" +#include "util.h" + +// Some of this based on this nice essay: http://xn--rpa.cc/essays/term + +#define TERM_STREAM stdout +#define TERM_FILENO STDOUT_FILENO +#define WRITE_CONST(raw) fwrite(raw, 1, sizeof(raw), TERM_STREAM) +#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) + +#define PLAIN "0" +#define FG "3" +#define BG "4" +#define BR_FG "9" +#define BR_BG "10" +#define BLACK "0" +#define RED "1" +#define GREEN "2" +#define YELLOW "3" +#define BLUE "4" +#define MAGNETA "5" +#define CYAN "6" +#define WHITE "7" + +#define ALT_BUF "?1049" +#define CURS "?25" +#define TERM_CLEAR "2J" +#define CLEAR_LINE "2K" +#define HIGH "h" +#define LOW "l" +#define JUMP "H" + +#define ESCA "\x1b[" +#define FMT(f) ESCA f "m" + +static void +usage(const char *argv0) +{ + fprintf(stderr, "usage: %s pid [regions]\n" + " regions must be in /proc/<pid>/maps format", argv0); + exit(EXIT_FAILURE); +} + +struct key { + unsigned char seq[6], i; + bool is_csi; +}; + +static struct { + struct named_region { + struct region region; + const char *name; + } *named; + size_t num_regions, allocated_regions, active_region; + + struct { + char *buffer; + const char *fmt; + size_t pointer, size; + } screen; + + struct { + struct { unsigned char *data; size_t mapped; } memory[2]; + size_t scroll, offset; + unsigned char octects_per_group; + } hexview, last_hexview; + + struct { + char err[255]; + struct termios initial, current; + struct { unsigned int w; unsigned int h; } ws; + struct { unsigned int x; unsigned int y; } cur; + } term; + + struct key last_key; + struct mem_io io; +} ctx = { .hexview.octects_per_group = 1 }; + +const char* +hex_for_byte(unsigned char byte) +{ + // optimization to avoid vprintf for the hex view + static const char *hex[256] = { + "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", + "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", + "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f", + "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f", + "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f", + "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f", + "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f", + "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f", + "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f", + "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f", + "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af", + "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf", + "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf", + "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df", + "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef", + "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff" + }; + return hex[byte]; +} + +static bool +offset_is_in_named_region(const size_t offset, const struct named_region *named) +{ + return (named->region.start <= offset && named->region.end >= offset); +} + +static const struct named_region* +named_region_for_offset(const size_t offset, const bool set_active) +{ + if (!ctx.num_regions) + return NULL; + + if (offset_is_in_named_region(offset, &ctx.named[ctx.active_region])) + return &ctx.named[ctx.active_region]; + + for (size_t i = 1 ; ctx.active_region + i < ctx.num_regions || ctx.active_region >= i; ++i) { + if (ctx.active_region + i < ctx.num_regions && + offset_is_in_named_region(offset, &ctx.named[ctx.active_region + i])) { + ctx.active_region += i * set_active; + return &ctx.named[ctx.active_region + i * !set_active]; + } + if (ctx.active_region >= i && + offset_is_in_named_region(offset, &ctx.named[ctx.active_region - i])) { + ctx.active_region -= i * set_active; + return &ctx.named[ctx.active_region - i * !set_active]; + } + } + + return NULL; +} + +static size_t +bytes_fits_row(void) +{ + // 000000000000: 00 00 00 00 00 00 00 00 00 ......... + const size_t preamble = snprintf(NULL, 0, "%.13zx: ", (size_t)0); + if (ctx.term.ws.w <= preamble) return 0; + return (ctx.term.ws.w - preamble) / ((ctx.hexview.octects_per_group * 3 /* 00?? + ws */) + ctx.hexview.octects_per_group /* for chr view */); +} + +static size_t +bytes_fits_screen(void) +{ + if (ctx.term.ws.h <= 3) return 0; + return bytes_fits_row() * (ctx.term.ws.h - 3); // 3 for top and bottom bars +} + +static void +__attribute__((format(printf, 1, 2))) +screen_printf(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + ctx.screen.pointer += vsnprintf(ctx.screen.buffer + ctx.screen.pointer, ctx.screen.size - ctx.screen.pointer, fmt, ap); + ctx.screen.pointer = (ctx.screen.pointer > ctx.screen.size ? ctx.screen.size : ctx.screen.pointer); + va_end(ap); +} + +static void +screen_print(const char *str) +{ + const size_t slen = strlen(str); + const size_t len = (slen < ctx.screen.size - ctx.screen.pointer ? slen : ctx.screen.size - ctx.screen.pointer); + memcpy(ctx.screen.buffer + ctx.screen.pointer, str, len); + ctx.screen.pointer += len; +} + +static void +screen_format(const char *fmt) +{ + // optimization for reducing format changes in hexview + if (ctx.screen.fmt == fmt) + return; + + if (ctx.screen.fmt) + screen_print(FMT(PLAIN)); + + screen_print((ctx.screen.fmt = fmt)); +} + +static void +screen_cursor(const unsigned int x, const unsigned int y) +{ + screen_printf(ESCA "%u;%u" JUMP, (ctx.term.cur.y = y) + 1, (ctx.term.cur.x = x) + 1); +} + +static void +screen_fill(const unsigned int y, const char *str) +{ + screen_cursor(0, y); + for (size_t x = 0; x < ctx.term.ws.w; ++x) + screen_print(str); +} + +static void +screen_flush(void) +{ + fwrite(ctx.screen.buffer, 1, ctx.screen.pointer, TERM_STREAM); + ctx.screen.pointer = 0; + ctx.screen.fmt = FMT(PLAIN); +} + +static size_t +scroll_for_offset(const struct named_region *named, const size_t offset) +{ + if (!named) + return 0; + + const size_t bw = bytes_fits_row(), bs = bytes_fits_screen(); + const size_t active_row = (offset - named->region.start) / bw; + if (active_row * bw >= ctx.hexview.scroll + bs) { + ctx.hexview.scroll = active_row * bw - (bs - bw); + } else if (active_row * bw <= ctx.hexview.scroll) { + ctx.hexview.scroll = active_row * bw; + } + + return ctx.hexview.scroll; +} + +static void +repaint_static_areas(void) +{ + screen_fill(1, "┉"); + screen_fill(ctx.term.ws.h - 1, "┉"); +} + +static void +repaint_top_bar(const struct named_region *named) +{ + screen_cursor(0, 0); + screen_print(ESCA CLEAR_LINE); + if (named) { + screen_printf("%s", named->name); + } else { + screen_print("no region"); + } +} + +static void +draw_error(const char *line, void *data) +{ + (void)data; + const size_t w = snprintf(NULL, 0, "%s", line); + screen_cursor(ctx.term.ws.w / 2 - w / 2, ctx.term.ws.h / 2); + screen_printf("%s", line); +} + +static void +repaint_hexview(const struct named_region *named, const bool update) +{ + if (!named) + return; + + const size_t bs = bytes_fits_screen(), bw = bytes_fits_row(); + const size_t start = named->region.start + scroll_for_offset(named, ctx.hexview.offset), len = named->region.end - start; + const bool scrolled = (ctx.hexview.scroll != ctx.last_hexview.scroll); + + if (update || scrolled) { + memcpy(ctx.hexview.memory[1].data, ctx.hexview.memory[0].data, ctx.hexview.memory[0].mapped * !scrolled); + ctx.hexview.memory[0].mapped = ctx.io.read(&ctx.io, ctx.hexview.memory[0].data, start, (bs > len ? len : bs)); + memcpy(ctx.hexview.memory[1].data, ctx.hexview.memory[0].data, ctx.hexview.memory[0].mapped * scrolled); + } + + screen_format(FMT(PLAIN)); + for (size_t pointer = 0; pointer < ctx.hexview.memory[0].mapped;) { + screen_cursor(0, 2 + (pointer / bw)); + screen_printf(FMT(FG YELLOW) "%.13zx: " FMT(PLAIN), start + pointer); + ctx.screen.fmt = FMT(FG YELLOW); + + const size_t row_start = pointer; + for (size_t x = 0; x < bw && pointer < bs; ++x) { + const bool selected = (start + pointer == ctx.hexview.offset); + + bool changed = false; + for (size_t o = pointer; o < pointer + ctx.hexview.octects_per_group && o < bs; ++o) { + if (ctx.hexview.memory[0].data[o] == ctx.hexview.memory[1].data[o]) + continue; + + changed = true; + break; + } + + if (selected) + screen_format(FMT(BR_BG RED)); + else if (changed) + screen_format(FMT(FG RED)); + else + screen_format(FMT(PLAIN)); + + for (size_t o = 0; o < ctx.hexview.octects_per_group && pointer < bs; ++o) { + if (pointer > ctx.hexview.memory[0].mapped) { + screen_print(" "); + } else { + screen_print(hex_for_byte(ctx.hexview.memory[0].data[pointer++])); + } + } + + if (selected) + screen_format(FMT(PLAIN)); + + screen_print(" "); + } + + for (size_t x = row_start; x < pointer; ++x) { + const bool selected = (start + x == ctx.hexview.offset); + const bool changed = (ctx.hexview.memory[0].data[x] != ctx.hexview.memory[1].data[x]); + + if (selected) + screen_format(FMT(BR_BG RED)); + else if (changed) + screen_format(FMT(FG RED)); + else + screen_format(FMT(FG CYAN)); + + if (x > ctx.hexview.memory[0].mapped) { + screen_print(" "); + } else { + screen_print((char[]){(isprint(ctx.hexview.memory[0].data[x]) ? ctx.hexview.memory[0].data[x] : '.'), 0}); + } + } + } + screen_format(FMT(PLAIN)); + + if (!ctx.hexview.memory[0].mapped) + screen_cursor(0, 1); + + for (size_t i = 0; i < (bs - ctx.hexview.memory[0].mapped) / bw; ++i) { + screen_cursor(0, ctx.term.cur.y + 1); + screen_print(ESCA CLEAR_LINE); + } + + if (!ctx.hexview.memory[0].mapped && ctx.term.ws.h > 3) { + fflush(stderr); + for_each_token_in_str(ctx.term.err, '\n', draw_error, NULL); + memset(ctx.term.err, 0, sizeof(ctx.term.err)); + } +} + +static void +repaint_dynamic_areas(const bool full_repaint) +{ + const struct named_region *last_active = &ctx.named[ctx.active_region]; + const struct named_region *named = named_region_for_offset(ctx.hexview.offset, true); + + if (named != last_active) { + repaint_top_bar(named); + ctx.last_hexview.scroll = (size_t)~0; // avoid diffing + } + + if (named && memcmp(&ctx.hexview, &ctx.last_hexview, sizeof(ctx.hexview))) { + repaint_hexview(named, (full_repaint || named != last_active)); + ctx.last_hexview = ctx.hexview; + } + + screen_cursor(0, ctx.term.ws.h); + screen_print(ESCA CLEAR_LINE); + screen_printf("%zx", ctx.hexview.offset); + + const unsigned char *seq = ctx.last_key.seq + ctx.last_key.is_csi; + const int seq_len = ctx.last_key.i - ctx.last_key.is_csi; + const size_t rstrlen = snprintf(NULL, 0, "%s%.*s %zu/%zu", (ctx.last_key.is_csi ? "^[" : ""), seq_len, seq, ctx.active_region + 1, ctx.num_regions); + screen_cursor(ctx.term.ws.w - rstrlen, ctx.term.ws.h); + screen_printf("%s%.*s %zu/%zu", (ctx.last_key.is_csi ? "^[" : ""), seq_len, seq, ctx.active_region + 1, ctx.num_regions); +} + +static void +repaint(void) +{ + ctx.last_hexview.scroll = (size_t)~0; // avoid diffing + screen_print(ESCA TERM_CLEAR); + repaint_static_areas(); + repaint_top_bar(named_region_for_offset(ctx.hexview.offset, false)); + repaint_dynamic_areas(true); + screen_flush(); +} + +static void +resize(int sig) +{ + signal(sig, SIG_IGN); + + struct winsize ws; + ioctl(TERM_FILENO, TIOCGWINSZ, &ws); + ctx.term.ws.w = (ws.ws_col > 0 ? ws.ws_col : 0); + ctx.term.ws.h = (ws.ws_row > 0 ? ws.ws_row - 1 : 0); + + for (size_t i = 0; i < ARRAY_SIZE(ctx.hexview.memory); ++i) { + free(ctx.hexview.memory[i].data); ctx.hexview.memory[i].data = NULL; + if (!(ctx.hexview.memory[i].data = malloc(bytes_fits_screen()))) + err(EXIT_FAILURE, "malloc"); + } + + ctx.screen.pointer = 0; + ctx.screen.size = (ctx.term.ws.w * ctx.term.ws.h) * 2; // bit extra for formatting + free(ctx.screen.buffer); ctx.screen.buffer = NULL; + if (!(ctx.screen.buffer = malloc(ctx.screen.size))) + err(EXIT_FAILURE, "malloc"); + + repaint(); + signal(sig, resize); +} + +static void +next_region(int arg) +{ + size_t region; + if (arg < 0 && ctx.active_region < (size_t)(arg * -1)) + region = ctx.num_regions - ((arg * -1) - ctx.active_region); + else + region = (ctx.active_region + arg) % ctx.num_regions; + ctx.hexview.offset = ctx.named[region].region.start; +} + +enum { + MOVE_PAGE_UP, + MOVE_PAGE_DOWN, + MOVE_START, + MOVE_END, + MOVE_UP, + MOVE_DOWN, + MOVE_RIGHT, + MOVE_LEFT +}; + +static void +navigate(int arg) +{ + const struct named_region *named; + if (!(named = named_region_for_offset(ctx.hexview.offset, true))) + return; + + switch (arg) { + case MOVE_PAGE_UP: { + const size_t bs = bytes_fits_screen(); + if (bs <= ctx.hexview.offset - named->region.start) { + ctx.hexview.offset -= bs; + ctx.hexview.scroll -= bs; + } + } break; + case MOVE_PAGE_DOWN: { + const size_t bs = bytes_fits_screen(); + if (bs <= named->region.end - ctx.hexview.offset) { + ctx.hexview.offset += bs; + ctx.hexview.scroll += bs; + } + } break; + case MOVE_START: + ctx.hexview.offset = named->region.start; + break; + case MOVE_END: + ctx.hexview.offset = named->region.end; + break; + case MOVE_UP: { + const size_t bw = bytes_fits_row(); + if (bw <= ctx.hexview.offset - named->region.start) + ctx.hexview.offset -= bw; + } break; + case MOVE_DOWN: { + const size_t bw = bytes_fits_row(); + if (bw <= named->region.end - ctx.hexview.offset) + ctx.hexview.offset += bw; + } break; + case MOVE_LEFT: + ctx.hexview.offset -= !!(ctx.hexview.offset - named->region.start) * ctx.hexview.octects_per_group; + break; + case MOVE_RIGHT: + ctx.hexview.offset += !!(named->region.end - ctx.hexview.offset) * ctx.hexview.octects_per_group; + break; + } +} + +static void +key_press(const struct key *key) +{ + const struct { + unsigned char seq[sizeof(key->seq)]; + void (*fun)(int); + int arg; + } keys[] = { + { .seq = { 0x1b, '[', '1', ';', '2', 'C' }, .fun = next_region, .arg = 1 }, + { .seq = { 0x1b, '[', '1', ';', '2', 'D' }, .fun = next_region, .arg = -1 }, + { .seq = { 0x1b, '[', '5', '~' }, .fun = navigate, .arg = MOVE_PAGE_UP }, + { .seq = { 0x1b, '[', '6', '~' }, .fun = navigate, .arg = MOVE_PAGE_DOWN }, + { .seq = { 0x1b, '[', 'H' }, .fun = navigate, .arg = MOVE_START }, + { .seq = { 0x1b, '[', 'F' }, .fun = navigate, .arg = MOVE_END }, + { .seq = { 0x1b, '[', 'A' }, .fun = navigate, .arg = MOVE_UP }, + { .seq = { 0x1b, '[', 'B' }, .fun = navigate, .arg = MOVE_DOWN }, + { .seq = { 0x1b, '[', 'C' }, .fun = navigate, .arg = MOVE_RIGHT }, + { .seq = { 0x1b, '[', 'D' }, .fun = navigate, .arg = MOVE_LEFT }, + }; + + for (size_t i = 0; i < ARRAY_SIZE(keys); ++i) { + if (memcmp(keys[i].seq, key->seq, key->i)) + continue; + + keys[i].fun(keys[i].arg); + break; + } + + ctx.last_key = *key; +} + +static void +quit(void) +{ + for (size_t i = 0; i < ctx.num_regions; ++i) + free((char*)ctx.named[i].name); + + mem_io_release(&ctx.io); + + for (size_t i = 0; i < ARRAY_SIZE(ctx.hexview.memory); ++i) + free(ctx.hexview.memory[i].data); + + free(ctx.screen.buffer); + + if (!memcmp(&ctx.term.initial, &ctx.term.current, sizeof(ctx.term.initial))) + return; + + WRITE_CONST( + ESCA ALT_BUF HIGH + ESCA TERM_CLEAR + ESCA CURS HIGH + ESCA ALT_BUF LOW + ); + + tcsetattr(TERM_FILENO, TCSANOW, &ctx.term.initial); + memset(&ctx, 0, sizeof(ctx)); +} + +static void +init(void) +{ + freopen("/dev/null", "wb", stderr); + setvbuf(stderr, ctx.term.err, _IOFBF, sizeof(ctx.term.err)); + setvbuf(TERM_STREAM, NULL, _IONBF, 0); + tcgetattr(TERM_FILENO, &ctx.term.initial); + ctx.term.current = ctx.term.initial; + + atexit(quit); + signal(SIGTERM, exit); + signal(SIGINT, exit); + signal(SIGSEGV, exit); + signal(SIGABRT, exit); + + ctx.term.current.c_lflag &= (~ECHO & ~ICANON); + tcsetattr(TERM_FILENO, TCSANOW, &ctx.term.current); + + WRITE_CONST( + ESCA ALT_BUF HIGH + ESCA TERM_CLEAR + ESCA CURS LOW + ); +} + +static const char* +basename(const char *path) +{ + const char *base = strrchr(path, '/'); + return (base ? base + 1 : path); +} + +static void +region_cb(const char *line, void *data) +{ + (void)data; + + const size_t step = 1024; + if (ctx.num_regions >= ctx.allocated_regions && + !(ctx.named = realloc(ctx.named, sizeof(*ctx.named) * (ctx.allocated_regions += step)))) + err(EXIT_FAILURE, "realloc"); + + if (!region_parse(&ctx.named[ctx.num_regions].region, line)) + return; + + char path[255] = {0}; + int region_len_without_name = strlen(line); + sscanf(line, "%*s %*s %*s %*s %*s%n %254s", ®ion_len_without_name, path); + const char *base = basename(path); + + char *name; + size_t name_sz = region_len_without_name + 1 + strlen(base) + 1; + if (name_sz > 1 && !(name = malloc(name_sz))) + err(EXIT_FAILURE, "malloc"); + + snprintf(name, name_sz, "%.*s %s", region_len_without_name, line, base); + ctx.active_region = (strstr(name, "[heap]") ? ctx.num_regions : ctx.active_region); + ctx.named[ctx.num_regions++].name = name; +} + +static bool +ignored(char c) +{ + return (c == 0x00 || (c >= 0x07 && c <= 0x0f) || c == 0x7f); +} + +int +main(int argc, char *argv[]) +{ + if (argc < 2) + usage(argv[0]); + + const pid_t pid = strtoull(argv[1], NULL, 10); + + FILE *regions_file = NULL; + if (argc > 2 && !(regions_file = fopen(argv[2], "rb"))) { + err(EXIT_FAILURE, "fopen(%s)", argv[2]); + } else { + char path[128]; + snprintf(path, sizeof(path), "/proc/%u/maps", pid); + if (!(regions_file = fopen(path, "rb"))) + err(EXIT_FAILURE, "fopen(%s)", path); + } + + mem_io_uio_init(&ctx.io, pid); + for_each_token_in_file(regions_file, '\n', region_cb, NULL); + fclose(regions_file); + ctx.hexview.offset = ctx.named[ctx.active_region].region.start; + + init(); + signal(SIGWINCH, resize); + resize(0); + + struct key press = {0}; + while (true) { + fd_set set; + FD_ZERO(&set); + FD_SET(TERM_FILENO, &set); + struct timeval timeout = { .tv_sec = 1 }; + if (select(TERM_FILENO + 1, &set, NULL, NULL, &timeout) < 0) { + if (errno == EINTR) + continue; + + err(EXIT_FAILURE, "select"); + } + + if (!FD_ISSET(TERM_FILENO, &set)) { + // timeout + repaint_hexview(named_region_for_offset(ctx.hexview.offset, false), true); + screen_flush(); + continue; + } + + unsigned char input; + fread(&input, 1, 1, TERM_STREAM); + + if (ignored(input)) + continue; + + switch (input) { + case 0x18: // ^X + case 0x1a: // ^Z + press = (struct key){0}; + break; + case 0x1b: // ^[ + press = (struct key){0}; + fread(&input, 1, 1, TERM_STREAM); + if (input != '[') { + press.i = 0; + break; + } + /* fallthrough */ + case 0x9b: // CSI + press = (struct key){0}; + press.seq[press.i++] = 0x1b; + press.seq[press.i++] = '['; + press.is_csi = true; + break; + default: + press.seq[press.i++] = input; + if (!(press.is_csi && ((input >= '0' && input <= '9') || input == ';'))) { + key_press(&press); + press = (struct key){0}; + repaint_dynamic_areas(false); + screen_flush(); + } + break; + } + } + + return EXIT_SUCCESS; +} |