root/software/fss-parallel-tools/common/backtrace.c @ ed0ec7ca
| 7e8045d8 | David Sorber | #define _GNU_SOURCE
|
|
#include "backtrace.h"
|
|||
#include <execinfo.h> // for backtrace, backtrace_symbols
|
|||
#include <fcntl.h> // for open, O_CLOEXEC, O_RDONLY
|
|||
#include <pthread.h> // for pthread_mutex_lock, pthread_mute...
|
|||
#include <spawn.h> // for posix_spawn_file_actions_t, posi...
|
|||
#include <stddef.h> // for ptrdiff_t
|
|||
#include <stdio.h> // for snprintf, sscanf
|
|||
#include <stdlib.h> // for size_t, NULL, abort, free, strtoull
|
|||
#include <string.h> // for memset, strrchr
|
|||
#include <sys/wait.h> // for waitpid
|
|||
#include <unistd.h> // for close, read, pipe2, ssize_t, rea...
|
|||
#include "simplest-defs.h" // for NELEM, UNUSED
|
|||
#include "sparse.h" // for __force
|
|||
#include "spew.h" // for spew, SPEW_DEBUG, SPEW_INFO, SPE...
|
|||
/* DO NOT USE ASSERT() IN THIS FILE, TO AVOID INFINITE RECURSION */
|
|||
#ifdef ASSERT
|
|||
#error CANNOT_USE_ASSERT_IN_THIS_FILE
|
|||
#endif
|
|||
// I'm using pthread_mutex_t rather than xthread_mutex_t because I cannot
|
|||
// think of a better way to do this than with a static variable. And with a
|
|||
// static variable, I want to use PTHREAD_MUTEX_INITIALIZER, which there
|
|||
// doesn't seem to have a xthread_mutex analogue.
|
|||
static pthread_mutex_t backtrace_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|||
// Backtracer wants to fork an "addr2line" process to be able to translate
|
|||
// code offsets into human-readable strings. Back in the dark ages, we would
|
|||
// do the fork whenever we wanted a backtrace. That turned out to be too
|
|||
// expensive, since fork() would copy huge page table (covering many gigabytes
|
|||
// of memory), and it was silly because the exec() would just throw it away.
|
|||
//
|
|||
// Our next solution used a constructor/destructor. At the beginning of the
|
|||
// program we would start up the backtracer. At that point in time the page
|
|||
// table was small, and we didn't really care if it was expensive since
|
|||
// nothing was really happening yet. However we don't really like the
|
|||
// constructor/destructor stuff, since it's fragile. (And Matteo doesn't like
|
|||
// it because it's a desugared version of C++ :-)
|
|||
//
|
|||
// This solution uses posix_spawnp() to do a fork/exec operation.
|
|||
// posix_spawnp() lets you do some housekeeping (with dup2 and close
|
|||
// operations) between the fork and the spawn. Because the kernel gets all
|
|||
// the information about the fork/dup/close/spawn sequence at once, it can
|
|||
// avoid copying the page tables. The performance of this sequence is about
|
|||
// 200us to 500us.
|
|||
struct backtracer {
|
|||
int broken; // true if something went wrong and the backtracer
|
|||
// is failing. Fall back to not printing
|
|||
// backtraces, and try to keep running.
|
|||
pid_t child; // The addr2line process
|
|||
int pipe_to_child; // The write end of a pipe that goes the
|
|||
// addr2line's stdin.
|
|||
int pipe_from_child; // The read end of a pipe that comes from
|
|||
// addr2line's stdout.
|
|||
size_t va_base; // The offset to be subtracted from addresses to
|
|||
// to ASLR.
|
|||
};
|
|||
static int __attribute__((warn_unused_result)) // NEGATIVE_ON_ERROR
|
|||
sinit(posix_spawn_file_actions_t *file_actions)
|
|||
// Effect: Initialize the file_actions. If anything goes wrong return -1.
|
|||
{
|
|||
int r = posix_spawn_file_actions_init(file_actions);
|
|||
if (r != 0) {
|
|||
spew(SPEW_DEBUG, "Cannot spawn_file_actions_init: r=%d", r);
|
|||
return -1;
|
|||
} else {
|
|||
return 0;
|
|||
}
|
|||
}
|
|||
static int __attribute__((warn_unused_result)) // NEGATIVE_ON_ERROR
|
|||
sdup2(posix_spawn_file_actions_t *file_actions, int from, int to)
|
|||
// Effect: Record a dup2() operation to be done when posix_spawn runs. If
|
|||
// anything goes wrong, spew a diagnostic and return -1;
|
|||
{
|
|||
int r = posix_spawn_file_actions_adddup2(file_actions, from, to);
|
|||
if (r != 0) {
|
|||
spew(SPEW_DEBUG, "Cannot spawn_fileactions_adddup2: r=%d", r);
|
|||
return -1;
|
|||
} else {
|
|||
return 0;
|
|||
}
|
|||
}
|
|||
static int __attribute__((warn_unused_result)) // NEGATIVE_ON_ERROR
|
|||
sclose(posix_spawn_file_actions_t *file_actions, int fd)
|
|||
// Effect: Record a close() operation to be cone when posix_spawn runs. If
|
|||
// anything goes wrong, spew a diagnostic and return -1;
|
|||
{
|
|||
int r = posix_spawn_file_actions_addclose(file_actions, fd);
|
|||
if (r != 0) {
|
|||
spew(SPEW_DEBUG, "Cannot spawn_fileactions_addclose: r=%d", r);
|
|||
return -1;
|
|||
} else {
|
|||
return 0;
|
|||
}
|
|||
}
|
|||
static const struct backtracer errresult = {
|
|||
.pipe_to_child=-1, .pipe_from_child=-1, .child=0, .broken=1, .va_base = 0};
|
|||
static struct backtracer
|
|||
mk_backtracer(void) {
|
|||
// Only one backtracer may exist at a time (to prevent forkbombing if all
|
|||
// threads start backtracing.) This mutex is unlocked when you destroy
|
|||
// the backtracer.
|
|||
pthread_mutex_lock(&backtrace_mutex);
|
|||
// Plumbing
|
|||
int tochildpipe[2];
|
|||
int fromchildpipe[2];
|
|||
{
|
|||
int r = pipe2(tochildpipe, O_CLOEXEC);
|
|||
if (r) {
|
|||
spew(SPEW_ERROR, "pipe2 failed");
|
|||
}
|
|||
}
|
|||
{
|
|||
int r = pipe2(fromchildpipe, O_CLOEXEC);
|
|||
if (r) {
|
|||
spew(SPEW_ERROR, "pipe2 failed");
|
|||
}
|
|||
}
|
|||
// Find the path to the current executable. It's a symlink in
|
|||
// /proc/self/exe.
|
|||
char exec_path[204];
|
|||
// readlink doesn't null terminate. Fill it with zeros and tell it to
|
|||
// only use sizeof(exec_path)-1 to make sure there is a null at the
|
|||
// end.
|
|||
memset(exec_path, 0, sizeof(exec_path));
|
|||
ssize_t rc = readlink("/proc/self/exe", exec_path,
|
|||
sizeof(exec_path)-1);
|
|||
UNUSED(rc);
|
|||
spew(SPEW_INFO, "exec path is %s", exec_path);
|
|||
// Set up to do the plumbing after the spawn.
|
|||
posix_spawn_file_actions_t file_actions;
|
|||
if (0
|
|||
|| sinit(&file_actions)
|
|||
|| sdup2(&file_actions, tochildpipe[0], 0)
|
|||
|| sdup2(&file_actions, fromchildpipe[1], 1)
|
|||
|| sclose(&file_actions, tochildpipe[0])
|
|||
|| sclose(&file_actions, tochildpipe[1])
|
|||
|| sclose(&file_actions, fromchildpipe[0])
|
|||
|| sclose(&file_actions, fromchildpipe[1]))
|
|||
{
|
|||
return errresult;
|
|||
}
|
|||
pid_t child;
|
|||
{
|
|||
// Hack around because posix_spawnp doesn't have const's in quite the right place.
|
|||
char addr2line_s[] = "addr2line";
|
|||
char _e_s[] = "-e";
|
|||
char _f_s[] = "-f";
|
|||
char * const argv[] = {addr2line_s, _e_s, exec_path, _f_s, NULL};
|
|||
int r = posix_spawnp(&child, "addr2line",
|
|||
&file_actions, NULL,
|
|||
argv, environ);
|
|||
if (r != 0) {
|
|||
spew(SPEW_DEBUG, "Could not posix_spawnp r=%d", r);
|
|||
return errresult;
|
|||
}
|
|||
}
|
|||
{
|
|||
int r = posix_spawn_file_actions_destroy(&file_actions);
|
|||
if (r) spew(SPEW_FATAL, "Cannot destroy fail_actions r=%d", r);
|
|||
}
|
|||
// Plumbing back here the main program.
|
|||
struct backtracer result = {.broken = 0,
|
|||
.child = child,
|
|||
.pipe_to_child = tochildpipe[1],
|
|||
.pipe_from_child = fromchildpipe[0],
|
|||
.va_base = 0};
|
|||
close(tochildpipe[0]);
|
|||
close(fromchildpipe[1]);
|
|||
// Find the va_base
|
|||
{
|
|||
int fd = open("/proc/self/maps", O_RDONLY);
|
|||
if (fd >= 0) {
|
|||
char addr_buffer[256];
|
|||
// HACK: we're going to assume that the first line in the
|
|||
// file contains the mapping for our executable. Dynamic
|
|||
// libraries will be mapped in different locations, but we
|
|||
// don't really use any of them so our traceback should be
|
|||
// mostly correct.
|
|||
ssize_t bytes_read = read(fd, &addr_buffer, NELEM(addr_buffer)-1);
|
|||
if (bytes_read > 0) {
|
|||
addr_buffer[bytes_read] = 0;
|
|||
int rval = sscanf(addr_buffer, "%zx-", &result.va_base);
|
|||
if (rval != 1) {
|
|||
result.va_base = 0;
|
|||
}
|
|||
}
|
|||
}
|
|||
close(fd);
|
|||
}
|
|||
spew(SPEW_INFO, "va_base is %zx", result.va_base);
|
|||
if (result.va_base < 0x800000) {
|
|||
// If ASLR is off, subtracting va_base makes offsets that addr2line
|
|||
// doesn't understand. The best heuristic I've (jmp) found is that if
|
|||
// va_base is "low", then ASLR is off. I haven't been able to figure
|
|||
// out if there's a "low" address under which ASLR is disabled, but
|
|||
// I've seen 4 MiB in many systems, so I'm going to claim any va_base
|
|||
// under 8 MiB is in a non-ASLR system.
|
|||
result.va_base = 0;
|
|||
}
|
|||
return result;
|
|||
}
|
|||
static void
|
|||
backtracer_destroy(struct backtracer *bt) {
|
|||
close(bt->pipe_to_child);
|
|||
close(bt->pipe_from_child);
|
|||
spew(SPEW_INFO, "waitpid(%d)", bt->child);
|
|||
int wstatus;
|
|||
waitpid(bt->child, &wstatus, 0);
|
|||
spew(SPEW_INFO, "waitpid(%d) done wstatus=%d", bt->child, wstatus);
|
|||
*bt = errresult;
|
|||
pthread_mutex_unlock(&backtrace_mutex);
|
|||
}
|
|||
static void read_tail_of_line(int fd, char *line, size_t linelen)
|
|||
// Effect: Read a line of data from fd into line. If the line is too long to
|
|||
// fit, then put the last characters in so that if we read
|
|||
// "longfilename/fun.c:25" and linelen is 11 we end up with "e/fun.c:25"
|
|||
// rather than "longfilena".
|
|||
// line will always end NUL-terminated.
|
|||
// Requires: linelen>0. This function will fail unpredictably if linelen==0.
|
|||
{
|
|||
for (size_t i = 0; i+1 < linelen; i++) {
|
|||
char c;
|
|||
ssize_t rr = read(fd, &c, 1);
|
|||
if (0 == rr || '\n' == c) {
|
|||
line[i] = 0;
|
|||
return;
|
|||
} else {
|
|||
line[i] = c;
|
|||
}
|
|||
}
|
|||
while (1) {
|
|||
char c;
|
|||
ssize_t rr = read(fd, &c, 1);
|
|||
if (0 == rr || '\n' == c) {
|
|||
line[linelen-1] = 0;
|
|||
return;
|
|||
} else {
|
|||
line[linelen-1] = c;
|
|||
// Shift the string left by 1.o
|
|||
for (size_t j = 0; j+1 < linelen; j++) {
|
|||
line[j] = line[j+1];
|
|||
}
|
|||
}
|
|||
}
|
|||
}
|
|||
static void spew_backtrace_symbol(struct backtracer *bt,
|
|||
const void *addr,
|
|||
const char *from_backtrace_symbol,
|
|||
size_t spew_level)
|
|||
{
|
|||
void *addr_from_base = (void*)((__force size_t)addr
|
|||
- bt->va_base);
|
|||
char str_of_ptr[20];
|
|||
char fun_line[300] = "";
|
|||
char file_line[300] = "";
|
|||
if (!bt->broken) {
|
|||
int len = snprintf(str_of_ptr, sizeof(str_of_ptr),
|
|||
"%p\n", addr_from_base);
|
|||
ssize_t wr = write(bt->pipe_to_child, str_of_ptr,
|
|||
(__force size_t)(ptrdiff_t)len);
|
|||
int did_write = wr == len;
|
|||
if (!did_write) {
|
|||
bt->broken = 1;
|
|||
} else {
|
|||
read_tail_of_line(bt->pipe_from_child, fun_line, sizeof(fun_line));
|
|||
read_tail_of_line(bt->pipe_from_child, file_line, sizeof(file_line));
|
|||
}
|
|||
}
|
|||
if (!from_backtrace_symbol) {
|
|||
from_backtrace_symbol = "(unknown offset)";
|
|||
}
|
|||
const char* function = &fun_line[0];
|
|||
if (!fun_line[0]) {
|
|||
function = "(unknown function)";
|
|||
}
|
|||
size_t line_number = 0;
|
|||
const char* filename = &file_line[0];
|
|||
if (*filename) {
|
|||
// Find line number by looking for last : in file_line.
|
|||
char* line_number_start = strrchr(file_line, ':');
|
|||
if (line_number_start != NULL) {
|
|||
// Parse the line number
|
|||
line_number = strtoull(line_number_start + 1, NULL, 10);
|
|||
if (line_number > 0) {
|
|||
// Parsed, remove the line number and : from file_line.
|
|||
*line_number_start = 0;
|
|||
}
|
|||
}
|
|||
} else {
|
|||
// No filename, substitute backtrace symbol for filename.
|
|||
filename = from_backtrace_symbol;
|
|||
from_backtrace_symbol = "";
|
|||
}
|
|||
spew_for_backtrace(
|
|||
spew_level, filename, line_number, function, from_backtrace_symbol);
|
|||
}
|
|||
void spew_stored_backtrace(void *const*ptrs, int nptrs, size_t spew_level)
|
|||
{
|
|||
char **strings = backtrace_symbols(ptrs, nptrs);
|
|||
struct backtracer bt = mk_backtracer();
|
|||
for (int j = 0; j < nptrs; j++) {
|
|||
spew_backtrace_symbol(&bt,
|
|||
ptrs[(__force ptrdiff_t)j],
|
|||
strings ? strings[(__force ptrdiff_t)j] : NULL,
|
|||
spew_level);
|
|||
}
|
|||
backtracer_destroy(&bt);
|
|||
free(strings);
|
|||
}
|
|||
void spew_backtrace(size_t spew_level)
|
|||
{
|
|||
if (spew_level < spew_get_level()) return;
|
|||
void *buffer[100];
|
|||
int nptrs = backtrace(buffer, (__force int)NELEM(buffer));
|
|||
/* cannot assert in this file */
|
|||
if (nptrs >= 0 && nptrs <= (__force int)NELEM(buffer)) {
|
|||
spew_stored_backtrace(buffer, nptrs, spew_level);
|
|||
} else {
|
|||
abort(); // can't happen
|
|||
}
|
|||
}
|