diff options
author | Serhei Makarov <serhei@serhei.io> | 2023-11-16 16:06:03 -0500 |
---|---|---|
committer | Serhei Makarov <serhei@serhei.io> | 2023-11-16 16:06:03 -0500 |
commit | 0f13f2440102d15c4c9ae1e6379c3f5ba76e1d82 (patch) | |
tree | e10da97641fd72f01abd5a7a9e083aa837a0727f | |
parent | fb6fdfe211384968ec42a33a351975d4dbf3ec2c (diff) |
eu-stacktrace WIP: introduce 'naive' unwinder mode, very initial code
Still a number of pieces missing:
- recycle Dwfl structs sensibly
- create and output callchain frame
- compare to baseline sysprof unwinding, improve data quality
-rw-r--r-- | src/stacktrace.c | 404 |
1 files changed, 394 insertions, 10 deletions
diff --git a/src/stacktrace.c b/src/stacktrace.c index cd38c998..8167620d 100644 --- a/src/stacktrace.c +++ b/src/stacktrace.c @@ -76,9 +76,15 @@ #include <stdio.h> #include <string.h> #include <fcntl.h> +/* #include ELFUTILS_HEADER(dwfl) */ +#include "../libdwfl/libdwflP.h" +/* XXX: Private header needed for sysprof_find_procfile. */ #include <system.h> +/* TODO: Enable to show detailed unwinder diagnostics. */ +/* #define DEBUG */ + /* TODO: Make optional through configury. The #ifdefs are included now so we don't miss any code that needs to be controlled with this option. */ @@ -123,7 +129,7 @@ static int input_fd = -1; static char *output_path = NULL; static int output_fd = -1; -#define MODE_OPTS "none/passthru" +#define MODE_OPTS "none/passthru/naive" #define MODE_NONE 0x0 #define MODE_PASSTHRU 0x1 #define MODE_NAIVE 0x2 @@ -425,6 +431,10 @@ parse_opt (int key, char *arg __attribute__ ((unused)), { processing_mode = MODE_PASSTHRU; } + else if (strcmp (arg, "naive") == 0) + { + processing_mode = MODE_NAIVE; + } else { argp_error (state, N_("Unsupported -m '%s', should be " MODE_OPTS "."), arg); @@ -451,6 +461,7 @@ parse_opt (int key, char *arg __attribute__ ((unused)), if (processing_mode == 0) processing_mode = MODE_PASSTHRU; + /* TODO: Change the default to MODE_NAIVE once unwinding works reliably. */ if (input_format == 0) input_format = FORMAT_SYSPROF; @@ -462,14 +473,8 @@ parse_opt (int key, char *arg __attribute__ ((unused)), return 0; } -struct sysprof_passthru_info -{ - int output_fd; - SysprofReader *reader; - int pos; /* TODO for debugging purposes */ -}; - #ifdef HAVE_SYSPROF_4_HEADERS + int sysprof_none_cb (SysprofCaptureFrame *frame __attribute__ ((unused)), void *arg __attribute__ ((unused))) @@ -477,6 +482,13 @@ sysprof_none_cb (SysprofCaptureFrame *frame __attribute__ ((unused)), return SYSPROF_CB_OK; } +struct sysprof_passthru_info +{ + int output_fd; + SysprofReader *reader; + int pos; /* TODO for debugging purposes */ +}; + int sysprof_passthru_cb (SysprofCaptureFrame *frame, void *arg) { @@ -489,6 +501,364 @@ sysprof_passthru_cb (SysprofCaptureFrame *frame, void *arg) error (EXIT_BAD, errno, N_("Write error to file or FIFO '%s'"), output_path); return SYSPROF_CB_OK; } + +struct sysprof_unwind_info +{ + int output_fd; + SysprofReader *reader; + int pos; /* TODO for debugging purposes */ + int nframes; /* TODO for debugging purposes */ +}; + +struct __sample_arg +{ + int tid; + uint64_t base_addr; + uint64_t size; + char *data; + uint64_t pc; + uint64_t sp; +}; + +/* The next few functions Imitate the corefile interface for a single + stack sample, with very restricted access to registers and memory. */ + +/* Just yields the single thread id matching the sample. */ +static pid_t +sample_next_thread (Dwfl *dwfl __attribute__ ((unused)), void *dwfl_arg, + void **thread_argp) +{ + struct __sample_arg *sample_arg = (struct __sample_arg *)dwfl_arg; + if (*thread_argp == NULL) + { + *thread_argp = (void *)0xea7b3375; + return sample_arg->tid; + } + else + return 0; +} + +/* Just checks that the thread id matches the sample. */ +static bool +sample_getthread (Dwfl *dwfl __attribute__ ((unused)), pid_t tid, + void *dwfl_arg, void **thread_argp) +{ + struct __sample_arg *sample_arg = (struct __sample_arg *)dwfl_arg; + *thread_argp = (void *)sample_arg; + if (sample_arg->tid != tid) + { + /* TODO __libdwfl_seterrno (DWFL_E_ERRNO); */ + return false; + } + return true; +} + +static bool +sample_memory_read (Dwfl *dwfl __attribute__ ((unused)), Dwarf_Addr addr, Dwarf_Word *result, void *arg) +{ + struct __sample_arg *sample_arg = (struct __sample_arg *)arg; +#ifdef DEBUG + fprintf(stderr, "DEBUG memory_read base_addr=%lx addr=%lx => sample@%lx of %lx\n", sample_arg->base_addr, addr, addr - sample_arg->base_addr, sample_arg->size); +#endif + /* Imitate read_cached_memory() with the stack sample data as the cache. */ + if (addr < sample_arg->base_addr || addr - sample_arg->base_addr >= sample_arg->size) + return false; + char *d = &sample_arg->data[addr - sample_arg->base_addr]; + if ((((uintptr_t) d) & (sizeof (unsigned long) - 1)) == 0) + *result = *(unsigned long *)d; + else + memcpy (result, d, sizeof (unsigned long)); + return true; +} + +/* TODO: Need to generalize this code across architectures. */ +static bool +sample_set_initial_registers (Dwfl_Thread *thread, void *thread_arg) +{ + struct __sample_arg *sample_arg = (struct __sample_arg *)thread_arg; + dwfl_thread_state_register_pc (thread, sample_arg->pc); + /* TODO for comparison, i386 user_regs sp is 3, pc is 8 */ + dwfl_thread_state_registers (thread, 7 /* x86_64 user_regs sp */, 1, &sample_arg->sp); + dwfl_thread_state_registers (thread, 16 /* x86_64 user_regs pc */, 1, &sample_arg->pc); + return true; +} + +static void +sample_detach (Dwfl *dwfl __attribute__ ((unused)), void *dwfl_arg) +{ + struct __sample_arg *sample_arg = (struct __sample_arg *)dwfl_arg; + free (sample_arg); +} + +static const Dwfl_Thread_Callbacks sample_thread_callbacks = +{ + sample_next_thread, + sample_getthread, + sample_memory_read, + sample_set_initial_registers, + sample_detach, + NULL, /* sample_thread_detach */ +}; + +int +nop_find_debuginfo (Dwfl_Module *mod __attribute__((unused)), + void **userdata __attribute__((unused)), + const char *modname __attribute__((unused)), + GElf_Addr base __attribute__((unused)), + const char *file_name __attribute__((unused)), + const char *debuglink_file __attribute__((unused)), + GElf_Word debuglink_crc __attribute__((unused)), + char **debuginfo_file_name __attribute__((unused))) +{ +#ifdef DEBUG + fprintf(stderr, "DEBUG nop_find_debuginfo modname=%s file_name=%s debuglink_file=%s\n", modname, file_name, debuglink_file); +#endif + return -1; +} + +static const Dwfl_Callbacks sample_callbacks = +{ + .find_elf = dwfl_linux_proc_find_elf, + .find_debuginfo = nop_find_debuginfo, /* work with CFI only */ +}; + +/* TODO: Code mirrors dwfl_linux_proc_attach(); + should probably move this function to libdwfl/linux-pid-attach.c + and switch to including the public dwfl interface? */ +int +sysprof_find_procfile (Dwfl *dwfl, pid_t *pid, Elf **elf, int *elf_fd) +{ + char buffer[36]; + FILE *procfile; + int err = 0; /* The errno to return and set for dwfl->attcherr. */ + + /* Make sure to report the actual PID (thread group leader) to + dwfl_attach_state. */ + snprintf (buffer, sizeof (buffer), "/proc/%ld/status", (long) *pid); + procfile = fopen (buffer, "r"); + if (procfile == NULL) + { + err = errno; + fail: + if (dwfl->process == NULL && dwfl->attacherr == DWFL_E_NOERROR) + { + errno = err; + /* TODO: __libdwfl_canon_error not exported from libdwfl */ + /* dwfl->attacherr = __libdwfl_canon_error (DWFL_E_ERRNO); */ + } + return err; + } + + char *line = NULL; + size_t linelen = 0; + while (getline (&line, &linelen, procfile) >= 0) + if (startswith (line, "Tgid:")) + { + errno = 0; + char *endptr; + long val = strtol (&line[5], &endptr, 10); + if ((errno == ERANGE && val == LONG_MAX) + || *endptr != '\n' || val < 0 || val != (pid_t) val) + *pid = 0; + else + *pid = (pid_t) val; + break; + } + free (line); + fclose (procfile); + + if (*pid == 0) + { + err = ESRCH; + goto fail; + } + + char name[64]; + int i = snprintf (name, sizeof (name), "/proc/%ld/task", (long) *pid); + if (i <= 0 || i >= (ssize_t) sizeof (name) - 1) + { + errno = -ENOMEM; + goto fail; + } + DIR *dir = opendir (name); + if (dir == NULL) + { + err = errno; + goto fail; + } + + i = snprintf (name, sizeof (name), "/proc/%ld/exe", (long) *pid); + assert (i > 0 && i < (ssize_t) sizeof (name) - 1); + *elf_fd = open (name, O_RDONLY); + if (*elf_fd >= 0) + { + *elf = elf_begin (*elf_fd, ELF_C_READ_MMAP, NULL); + if (*elf == NULL) + { + /* Just ignore, dwfl_attach_state will fall back to trying + to associate the Dwfl with one of the existing Dwfl_Module + ELF images (to know the machine/class backend to use). */ +#ifdef DEBUG + fprintf(stderr, "DEBUG sysprof_find_profile pid %lld: elf not found", + (long long)*pid); +#endif + close (*elf_fd); + *elf_fd = -1; + } + } + else + *elf = NULL; + return 0; +} + +Dwfl * +sysprof_init_dwfl (struct sysprof_unwind_info *sui, + SysprofCaptureStackUser *ev, + SysprofCaptureUserRegs *regs) +{ + pid_t pid = ev->frame.pid; + /* TODO: Needs a per-process(?) caching scheme to avoid initializing on every sample. */ + (void)sui; + Dwfl *dwfl = dwfl_begin (&sample_callbacks); + + int err = dwfl_linux_proc_report (dwfl, pid); + if (err < 0) + { +#ifdef DEBUG + fprintf(stderr, "DEBUG dwfl_linux_proc_report pid %lld: %s", + (long long) pid, dwfl_errmsg (-1)); +#endif + return NULL; + } + + /* TODO: Check if elf needs to be freed in sample_detach. */ + Elf *elf = NULL; + int elf_fd; + err = sysprof_find_procfile (dwfl, &pid, &elf, &elf_fd); + if (err < 0) + { +#ifdef DEBUG + fprintf(stderr, "DEBUG sysprof_find_procfile pid %lld: %s", + (long long) pid, dwfl_errmsg (-1)); +#endif + return NULL; + } + + if (regs->n_regs != 2) /* TODO: for now, handling only sp,pc */ + return NULL; + + struct __sample_arg *sample_arg = malloc (sizeof *sample_arg); + if (sample_arg == NULL) + { + if (elf != NULL) { + elf_end (elf); + close (elf_fd); + } + free (sample_arg); + return NULL; + } + sample_arg->tid = ev->tid; + sample_arg->size = ev->size; + sample_arg->data = (char *)&ev->data; /* TODO make unsigned? */ + sample_arg->sp = regs->regs[0]; /* TODO doublecheck */ + sample_arg->pc = regs->regs[1]; /* TODO doublecheck */ + sample_arg->base_addr = sample_arg->sp; /* TODO doublecheck */ +#ifdef DEBUG + fprintf(stderr, "DEBUG sysprof_init_dwfl pid %lld: initial size=%ld sp=%lx pc=%lx\n", (long long) pid, sample_arg->size, sample_arg->sp, sample_arg->pc); +#endif + + if (! dwfl_attach_state (dwfl, elf, pid, &sample_thread_callbacks, sample_arg)) + { +#ifdef DEBUG + fprintf(stderr, "DEBUG dwfl_attach_state pid %lld: %s\n", + (long long)pid, dwfl_errmsg(-1)); +#endif + elf_end (elf); + close (elf_fd); + free (sample_arg); + return NULL; + } + return dwfl; +} + +void +sysprof_recycle_dwfl (struct sysprof_unwind_info *sui __attribute__((unused)), + Dwfl *dwfl) +{ + /* TODO: Needs a per-process(?) caching scheme to avoid de-initializing on every sample. */ + dwfl_end (dwfl); +} + +int +sysprof_unwind_frame_cb (Dwfl_Frame *state, void *arg) +{ + Dwarf_Addr pc; + bool isactivation; + if (! dwfl_frame_pc (state, &pc, &isactivation)) + return DWARF_CB_ABORT; + +#ifdef DEBUG + Dwarf_Addr pc_adjusted = pc - (isactivation ? 0 : 1); + Dwarf_Addr sp; + int rc = dwfl_frame_reg (state, 7 /* TODO make arch-independent */, &sp); + if (rc < 0) + { + fprintf(stderr, "DEBUG dwfl_frame_reg: %s\n", + dwfl_errmsg(-1)); + return DWARF_CB_ABORT; + } + fprintf(stderr, "DEBUG got a frame? pc_adjusted=%lx sp=%lx\n", pc_adjusted, sp); +#endif + + /* TODO add pc to output callchain; for now, just count and report frames */ + struct sysprof_unwind_info *sui = (struct sysprof_unwind_info *)arg; + sui->nframes++; + return DWARF_CB_OK; +} + +int +sysprof_unwind_cb (SysprofCaptureFrame *frame, void *arg) +{ + struct sysprof_unwind_info *sui = (struct sysprof_unwind_info *)arg; + if (frame->type != SYSPROF_CAPTURE_FRAME_STACK_USER) + { + sysprof_reader_bswap_frame (sui->reader, frame); /* reverse the earlier bswap */ + ssize_t n_write = write (sui->output_fd, frame, frame->len); + sui->pos += frame->len; + assert ((sui->pos % SYSPROF_CAPTURE_ALIGN) == 0); + if (n_write < 0) + error (EXIT_BAD, errno, N_("Write error to file or FIFO '%s'"), output_path); + return SYSPROF_CB_OK; + } + SysprofCaptureStackUser *ev = (SysprofCaptureStackUser *)frame; + char *tail_ptr = (char *)ev; + tail_ptr += sizeof(SysprofCaptureStackUser) + ev->size; + SysprofCaptureUserRegs *regs = (SysprofCaptureUserRegs *)tail_ptr; +#ifdef DEBUG + fprintf(stderr, "\n"); /* extra newline for padding */ +#endif + Dwfl *dwfl = sysprof_init_dwfl (sui, ev, regs); + if (dwfl == NULL) + { +#ifdef DEBUG + fprintf(stderr, "DEBUG sysprof_init_dwfl pid %lld (failed)\n", (long long)frame->pid); +#endif + sysprof_recycle_dwfl (sui, dwfl); + return SYSPROF_CB_OK; + } + sui->nframes = 0; + int rc = dwfl_getthread_frames (dwfl, ev->tid, sysprof_unwind_frame_cb, sui); + if (rc < 0) + { +#ifdef DEBUG + fprintf(stderr, "DEBUG dwfl_getthread_frames pid %lld: %s\n", (long long)frame->pid, dwfl_errmsg(-1)); +#endif + } + /* TODO: Assemble and output callchain frame. */ + fprintf(stderr, "sysprof_unwind_cb pid %lld: unwound %d frames\n", (long long)frame->pid, sui->nframes); + sysprof_recycle_dwfl (sui, dwfl); + return SYSPROF_CB_OK; +} #endif int @@ -554,8 +924,22 @@ Utility is a work-in-progress, see README.eu-stacktrace in the source branch.") ssize_t n_write = write (output_fd, &reader->header, sizeof reader->header); if (n_write < 0) error (EXIT_BAD, errno, N_("Write error to file or FIFO '%s'"), output_path); - struct sysprof_passthru_info spi = { output_fd, reader, sizeof reader->header }; - ptrdiff_t offset = sysprof_reader_getframes (reader, &sysprof_passthru_cb, &spi); + ptrdiff_t offset; + if (processing_mode == MODE_NONE) + { + struct sysprof_passthru_info sni = { output_fd, reader, sizeof reader->header }; + offset = sysprof_reader_getframes (reader, &sysprof_none_cb, &sni); + } + else if (processing_mode == MODE_PASSTHRU) + { + struct sysprof_passthru_info spi = { output_fd, reader, sizeof reader->header }; + offset = sysprof_reader_getframes (reader, &sysprof_passthru_cb, &spi); + } + else /* processing_mode == MODE_NAIVE */ + { + struct sysprof_unwind_info sui = { output_fd, reader, sizeof reader->header, 0 }; + offset = sysprof_reader_getframes (reader, &sysprof_unwind_cb, &sui); + } if (offset < 0) error (EXIT_BAD, errno, N_("No frames in file or FIFO '%s'"), input_path); sysprof_reader_end (reader); |