123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 | // This file defines some code to print stack traces.
#include <backtrace.h>
#include <dlfcn.h>
#include <err.h>
#include <gc.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <unwind.h>
#include "../config.h"
#include "print.h"
#include "util.h"
extern bool USE_COLOR;
// The raw stack addresses are collected with the compiler's stack unwinder,
// which zig provides on every supported platform (including fully static musl
// binaries, where libc-based alternatives like execinfo.h don't exist).
typedef struct {
void **frames;
int count, max;
} unwind_state_t;
static _Unwind_Reason_Code unwind_callback(struct _Unwind_Context *ctx, void *arg) {
unwind_state_t *state = arg;
if (state->count >= state->max) return _URC_END_OF_STACK;
uintptr_t ip = _Unwind_GetIP(ctx);
if (ip) state->frames[state->count++] = (void *)ip;
return _URC_NO_REASON;
}
static int collect_backtrace(void **buffer, int size) {
unwind_state_t state = {.frames = buffer, .count = 0, .max = size};
_Unwind_Backtrace(unwind_callback, &state);
return state.count;
}
static void fprint_context(FILE *out, const char *filename, int lineno, int context_before, int context_after) {
FILE *f = fopen(filename, "r");
if (!f) return;
char *line = NULL;
size_t size = 0;
ssize_t nread;
int64_t cur_line = 1;
int num_width = 1;
for (int n = lineno + context_after; n >= 10; n /= 10)
num_width += 1;
while ((nread = getline(&line, &size, f)) != -1) {
if (line[nread - 1] == '\n') line[nread - 1] = '\0';
if (cur_line >= lineno - context_before) {
int w = 1;
for (int n = cur_line; n >= 10; n /= 10)
w += 1;
if (USE_COLOR) {
fprint(out, cur_line == lineno ? "\033[91;1m>\033[m " : " ", "\033[2m",
repeated_char(' ', num_width - w), cur_line, "\033(0\x78\033(B",
cur_line == lineno ? "\033[0;91;1m" : "\033[0m", line, "\033[m");
} else {
fprint(out, cur_line == lineno ? "> " : " ", repeated_char(' ', num_width - w), cur_line, "| ", line);
}
}
cur_line += 1;
if (cur_line > lineno + context_after) break;
}
if (line) free(line);
fclose(f);
}
static void _print_stack_frame(FILE *out, const char *cwd, const char *install_dir, const char *function,
const char *filename, int lineno) {
if (function == NULL) {
fprint(out, USE_COLOR ? "\033[2m...unknown function...\033[m" : "...unknown function...");
return;
}
function = String(string_slice(function, strcspn(function, "+")));
if (function[0] == '\0') function = "???";
char *function_display = GC_MALLOC_ATOMIC(strlen(function));
memcpy(function_display, function, strlen(function) + 1);
char *last_dollar = strrchr(function_display, '$');
if (last_dollar) *last_dollar = '\0';
for (char *p = function_display; *p; p++) {
if (*p == '$') *p = '.';
}
if (filename) {
if (strncmp(filename, cwd, strlen(cwd)) == 0) filename += strlen(cwd);
fprint(out, USE_COLOR ? "\033[97;1mIn \033[93m" : "In ", function_display, USE_COLOR ? "()\033[97m" : "()");
if (install_dir[0] && strncmp(filename, install_dir, strlen(install_dir)) == 0) {
fprint_inline(out, USE_COLOR ? " in package \033[95m" : " in package ", filename, ":", lineno);
} else {
fprint(out, USE_COLOR ? "\033[93;4m" : "", filename, USE_COLOR ? "\033[m" : "");
}
fprint_context(out, filename, lineno, 3, 1);
} else {
fprint(out, "LINE: ", function);
}
}
enum { MAX_INLINE_FRAMES = 64 };
typedef struct {
const char *functions[MAX_INLINE_FRAMES], *filenames[MAX_INLINE_FRAMES];
long line_nums[MAX_INLINE_FRAMES];
int count;
} frame_list_t;
static int bt_frame_callback(void *data, uintptr_t pc, const char *filename, int lineno, const char *function) {
(void)pc;
frame_list_t *frames = data;
if (frames->count >= MAX_INLINE_FRAMES) return 1;
// Copy the strings: libbacktrace only guarantees them until the callback returns.
frames->functions[frames->count] = function ? String(function) : NULL;
frames->filenames[frames->count] = filename ? String(filename) : NULL;
frames->line_nums[frames->count] = lineno;
frames->count += 1;
return 0;
}
static void bt_error_callback(void *data, const char *msg, int errnum) {
// Quietly leave the frame unresolved:
(void)data, (void)msg, (void)errnum;
}
__attribute__((noinline)) public
void print_stacktrace(FILE *out, int offset) {
char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd)) == NULL) errx(1, "Path too large!");
size_t cwd_len = strlen(cwd);
if (cwd_len + 2 > sizeof(cwd)) errx(1, "Path too large!");
cwd[cwd_len++] = '/';
cwd[cwd_len] = '\0';
const char *install_dir = String(TOMO_PATH, "/lib/tomo@", TOMO_VERSION, "/");
// Symbolization is done in-process with the vendored libbacktrace (reading
// the executable's own debug info), so no external tools are needed and it
// works in fully static binaries. The state is created once and reused.
static struct backtrace_state *bt_state = NULL;
if (bt_state == NULL) {
const char *exe_path = NULL;
#if !defined(__linux__)
// A NULL filename makes libbacktrace find the executable itself, but it
// has no way to do that on OpenBSD (which offers no API to query the
// running executable's path). These platforms link dynamically, though,
// so dladdr() on one of our own functions yields the executable's path;
// resolve it in case it's relative to the (current) working directory.
Dl_info info;
static char resolved_path[PATH_MAX];
if (dladdr((void *)(uintptr_t)print_stacktrace, &info) && info.dli_fname && info.dli_fname[0]
&& realpath(info.dli_fname, resolved_path) != NULL)
exe_path = resolved_path;
#endif
bt_state = backtrace_create_state(exe_path, 0, bt_error_callback, NULL);
}
static void *stack[1024];
int64_t size = (int64_t)collect_backtrace(stack, sizeof(stack) / sizeof(stack[0]));
bool main_func_onwards = false;
for (int64_t i = size - 1; i > offset; i--) {
uintptr_t call_address = (uintptr_t)stack[i] - 1;
// backtrace_pcinfo() invokes the callback once per (possibly inlined)
// frame at this address, innermost-first. With optimization, several
// source-level calls can collapse into one physical frame, and this
// recovers all of them.
frame_list_t frames = {.count = 0};
if (bt_state != NULL) backtrace_pcinfo(bt_state, call_address, bt_frame_callback, bt_error_callback, &frames);
if (frames.count == 0) {
if (main_func_onwards) {
_print_stack_frame(out, cwd, install_dir, NULL, NULL, 0);
if (i - 1 > offset) fputs("\n", out);
}
continue;
}
// Print outermost-first to match the overall root-to-crash order:
for (int j = frames.count - 1; j >= 0; j--) {
// Start printing at the program's own code, skipping the C
// main()/parse_and_run$$... plumbing above it. When the program
// defines its own main(), the entry function is named
// "main$<file id>", called as a sibling of (not nested inside)
// "$initialize$<file id>" from "parse_and_run$$main$<file id>",
// so "main$" is on the stack but "$initialize$" already returned
// by the time main() runs. Top-level-script programs have no
// "main$" anywhere, since their code runs directly inside
// "$initialize$<file id>", called straight from C's own (always
// bare, never namespaced) main(). Matching either name catches
// both cases while still skipping the plain "main" C wrapper
// frame itself.
if (frames.functions[j]
&& (strstr(frames.functions[j], "main$") != NULL
|| strstr(frames.functions[j], "$initialize$") != NULL))
main_func_onwards = true;
if (main_func_onwards) {
_print_stack_frame(out, cwd, install_dir, frames.functions[j], frames.filenames[j],
frames.line_nums[j]);
if (j > 0 || i - 1 > offset) fputs("\n", out);
}
}
}
}
|