Fix frame pointer based unwinder to handle changing stack range

This commit is contained in:
Guangli Dai
2025-03-05 17:58:20 -08:00
committed by Qi Wang
parent ad108d50f1
commit 773b5809f9
3 changed files with 233 additions and 167 deletions

View File

@@ -20,7 +20,7 @@ void prof_fdump_impl(tsd_t *tsd);
void prof_idump_impl(tsd_t *tsd); void prof_idump_impl(tsd_t *tsd);
bool prof_mdump_impl(tsd_t *tsd, const char *filename); bool prof_mdump_impl(tsd_t *tsd, const char *filename);
void prof_gdump_impl(tsd_t *tsd); void prof_gdump_impl(tsd_t *tsd);
uintptr_t prof_thread_stack_start(uintptr_t stack_end); int prof_thread_stack_range(uintptr_t fp, uintptr_t *low, uintptr_t *high);
/* Used in unit tests. */ /* Used in unit tests. */
typedef int (prof_sys_thread_name_read_t)(char *buf, size_t limit); typedef int (prof_sys_thread_name_read_t)(char *buf, size_t limit);

View File

@@ -13,30 +13,60 @@
# include <string.h> # include <string.h>
# include <unistd.h> # include <unistd.h>
static int prof_mapping_containing_addr( /*
uintptr_t addr, * Converts a string representing a hexadecimal number to an unsigned long long
const char* maps_path, * integer. Functionally equivalent to strtoull() (for base 16) but faster for
uintptr_t* mm_start, * that case.
uintptr_t* mm_end) { *
int ret = ENOENT; // not found * @param nptr Pointer to the string to be converted.
* @param endptr Pointer to a pointer to character, which will be set to the
* character in `nptr` where parsing stopped. Can be NULL.
* @return The converted unsigned long long integer value.
*/
static inline unsigned long long int
strtoull_hex(const char *nptr, char **endptr) {
unsigned long long int val = 0;
int ii = 0;
for (; ii < 16; ++ii) {
char c = nptr[ii];
if (c >= '0' && c <= '9') {
val = (val << 4) + (c - '0');
} else if (c >= 'a' && c <= 'f') {
val = (val << 4) + (c - 'a' + 10);
} else {
break;
}
}
if (endptr) {
*endptr = (char *)(nptr + ii);
}
return val;
}
static int
prof_mapping_containing_addr(uintptr_t addr, const char *maps_path,
uintptr_t *mm_start, uintptr_t *mm_end) {
int ret = ENOENT; /* not found */
*mm_start = *mm_end = 0; *mm_start = *mm_end = 0;
// Each line of /proc/<pid>/maps is: /*
// <start>-<end> <perms> <offset> <dev> <inode> <pathname> * Each line of /proc/<pid>/maps is:
// * <start>-<end> <perms> <offset> <dev> <inode> <pathname>
// The fields we care about are always within the first 34 characters so *
// as long as `buf` contains the start of a mapping line it can always be * The fields we care about are always within the first 34 characters so
// parsed. * as long as `buf` contains the start of a mapping line it can always be
* parsed.
*/
static const int kMappingFieldsWidth = 34; static const int kMappingFieldsWidth = 34;
int fd = -1; int fd = -1;
char buf[4096]; char buf[4096];
ssize_t remaining = 0; // actual number of bytes read to buf ssize_t remaining = 0; /* actual number of bytes read to buf */
char *line = NULL; char *line = NULL;
while (1) { while (1) {
if (fd < 0) { if (fd < 0) {
// case 0: initial open of maps file /* case 0: initial open of maps file */
fd = malloc_open(maps_path, O_RDONLY); fd = malloc_open(maps_path, O_RDONLY);
if (fd < 0) { if (fd < 0) {
return errno; return errno;
@@ -44,44 +74,48 @@ static int prof_mapping_containing_addr(
remaining = malloc_read_fd(fd, buf, sizeof(buf)); remaining = malloc_read_fd(fd, buf, sizeof(buf));
if (remaining <= 0) { if (remaining <= 0) {
ret = errno;
break; break;
} }
line = buf; line = buf;
} else if (line == NULL) { } else if (line == NULL) {
// case 1: no newline found in buf /* case 1: no newline found in buf */
remaining = malloc_read_fd(fd, buf, sizeof(buf)); remaining = malloc_read_fd(fd, buf, sizeof(buf));
if (remaining <= 0) { if (remaining <= 0) {
ret = errno;
break; break;
} }
line = memchr(buf, '\n', remaining); line = memchr(buf, '\n', remaining);
if (line != NULL) { if (line != NULL) {
line++; // advance to character after newline line++; /* advance to character after newline */
remaining -= (line - buf); remaining -= (line - buf);
} }
} else if (line != NULL && remaining < kMappingFieldsWidth) { } else if (line != NULL && remaining < kMappingFieldsWidth) {
// case 2: found newline but insufficient characters remaining in buf /*
* case 2: found newline but insufficient characters remaining in
* buf
*/
memcpy(buf, line,
remaining); /* copy remaining characters to start of buf */
line = buf;
// fd currently points to the character immediately after the last size_t count =
// character in buf. Seek fd to the character after the newline. malloc_read_fd(fd, buf + remaining, sizeof(buf) - remaining);
if (malloc_lseek(fd, -remaining, SEEK_CUR) == -1) { if (count <= 0) {
ret = errno; ret = errno;
break; break;
} }
remaining = malloc_read_fd(fd, buf, sizeof(buf)); remaining += count; /* actual number of bytes read to buf */
if (remaining <= 0) {
break;
}
line = buf;
} else { } else {
// case 3: found newline and sufficient characters to parse /* case 3: found newline and sufficient characters to parse */
// parse <start>-<end> /* parse <start>-<end> */
char *tmp = line; char *tmp = line;
uintptr_t start_addr = strtoul(tmp, &tmp, 16); uintptr_t start_addr = (uintptr_t)strtoull_hex(tmp, &tmp);
if (addr >= start_addr) { if (addr >= start_addr) {
tmp++; // advance to character after '-' tmp++; /* advance to character after '-' */
uintptr_t end_addr = strtoul(tmp, &tmp, 16); uintptr_t end_addr = (uintptr_t)strtoull_hex(tmp, NULL);
if (addr < end_addr) { if (addr < end_addr) {
*mm_start = start_addr; *mm_start = start_addr;
*mm_end = end_addr; *mm_end = end_addr;
@@ -90,11 +124,11 @@ static int prof_mapping_containing_addr(
} }
} }
// Advance to character after next newline in the current buf. /* Advance to character after next newline in the current buf. */
char *prev_line = line; char *prev_line = line;
line = memchr(line, '\n', remaining); line = memchr(line, '\n', remaining);
if (line != NULL) { if (line != NULL) {
line++; // advance to character after newline line++; /* advance to character after newline */
remaining -= (line - prev_line); remaining -= (line - prev_line);
} }
} }
@@ -104,58 +138,29 @@ static int prof_mapping_containing_addr(
return ret; return ret;
} }
static uintptr_t prof_main_thread_stack_start(const char* stat_path) { int
uintptr_t stack_start = 0; prof_thread_stack_range(uintptr_t fp, uintptr_t *low, uintptr_t *high) {
/*
int fd = malloc_open(stat_path, O_RDONLY); * NOTE: Prior to kernel 4.5 an entry for every thread stack was included in
if (fd < 0) { * /proc/<pid>/maps as [STACK:<tid>]. Starting with kernel 4.5 only the main
return 0; * thread stack remains as the [stack] mapping. For other thread stacks the
} * mapping is still visible in /proc/<pid>/task/<tid>/maps (though not
* labeled as [STACK:tid]).
char buf[512]; * https://lists.ubuntu.com/archives/kernel-team/2016-March/074681.html
ssize_t n = malloc_read_fd(fd, buf, sizeof(buf) - 1); */
if (n >= 0) {
buf[n] = '\0';
if (sscanf(
buf,
"%*d (%*[^)]) %*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %*u %*u %*d %*d %*d %*d %*d %*d %*u %*u %*d %*u %*u %*u %"FMTuPTR,
&stack_start) != 1) {
}
}
malloc_close(fd);
return stack_start;
}
uintptr_t prof_thread_stack_start(uintptr_t stack_end) {
pid_t pid = getpid();
pid_t tid = gettid();
if (pid == tid) {
char stat_path[32]; // "/proc/<pid>/stat"
malloc_snprintf(stat_path, sizeof(stat_path), "/proc/%d/stat", pid);
return prof_main_thread_stack_start(stat_path);
} else {
// NOTE: Prior to kernel 4.5 an entry for every thread stack was included in
// /proc/<pid>/maps as [STACK:<tid>]. Starting with kernel 4.5 only the main
// thread stack remains as the [stack] mapping. For other thread stacks the
// mapping is still visible in /proc/<pid>/task/<tid>/maps (though not
// labeled as [STACK:tid]).
// https://lists.ubuntu.com/archives/kernel-team/2016-March/074681.html
char maps_path[64]; // "/proc/<pid>/task/<tid>/maps" char maps_path[64]; // "/proc/<pid>/task/<tid>/maps"
malloc_snprintf(maps_path, sizeof(maps_path), "/proc/%d/task/%d/maps", pid, tid); malloc_snprintf(maps_path, sizeof(maps_path), "/proc/%d/task/%d/maps",
getpid(), gettid());
uintptr_t mm_start, mm_end; return prof_mapping_containing_addr(fp, maps_path, low, high);
if (prof_mapping_containing_addr(
stack_end, maps_path, &mm_start, &mm_end) != 0) {
return 0;
}
return mm_end;
}
} }
#else #else
uintptr_t prof_thread_stack_start(UNUSED uintptr_t stack_end) { int
return 0; prof_thread_stack_range(
UNUSED uintptr_t addr, uintptr_t *stack_start, uintptr_t *stack_end) {
*stack_start = *stack_end = 0;
return ENOENT;
} }
#endif // __linux__ #endif // __linux__

View File

@@ -23,6 +23,11 @@
#define _Unwind_Backtrace JEMALLOC_TEST_HOOK(_Unwind_Backtrace, test_hooks_libc_hook) #define _Unwind_Backtrace JEMALLOC_TEST_HOOK(_Unwind_Backtrace, test_hooks_libc_hook)
#endif #endif
#ifdef JEMALLOC_PROF_FRAME_POINTER
// execinfo backtrace() as fallback unwinder
#include <execinfo.h>
#endif
/******************************************************************************/ /******************************************************************************/
malloc_mutex_t prof_dump_filename_mtx; malloc_mutex_t prof_dump_filename_mtx;
@@ -102,41 +107,97 @@ prof_backtrace_impl(void **vec, unsigned *len, unsigned max_len) {
#elif (defined(JEMALLOC_PROF_FRAME_POINTER)) #elif (defined(JEMALLOC_PROF_FRAME_POINTER))
JEMALLOC_DIAGNOSTIC_PUSH JEMALLOC_DIAGNOSTIC_PUSH
JEMALLOC_DIAGNOSTIC_IGNORE_FRAME_ADDRESS JEMALLOC_DIAGNOSTIC_IGNORE_FRAME_ADDRESS
struct stack_range {
uintptr_t start;
uintptr_t end;
};
struct thread_unwind_info {
struct stack_range stack_range;
bool fallback;
};
static __thread struct thread_unwind_info unwind_info = {
.stack_range = {
.start = 0,
.end = 0,
},
.fallback = false,
}; /* thread local */
static void static void
prof_backtrace_impl(void **vec, unsigned *len, unsigned max_len) { prof_backtrace_impl(void **vec, unsigned *len, unsigned max_len) {
// stack_start - highest possible valid stack address (assumption: stacks grow downward) /* fp: current stack frame pointer
// stack_end - current stack frame and lowest possible valid stack address *
// (all earlier frames will be at higher addresses than this) * stack_range: readable stack memory range for the current thread.
* Used to validate frame addresses during stack unwinding.
* For most threads there is a single valid stack range
* that is fixed at thread creation time. This may not be
* the case when folly fibers or boost contexts are used.
* In those cases fall back to using execinfo backtrace()
* (DWARF unwind).
*/
// always safe to get the current stack frame address /* always safe to get the current stack frame address */
void** stack_end = (void**)__builtin_frame_address(0); uintptr_t fp = (uintptr_t)__builtin_frame_address(0);
if (stack_end == NULL) {
*len = 0; /* new thread - get the stack range */
return; if (!unwind_info.fallback &&
unwind_info.stack_range.start == unwind_info.stack_range.end) {
if (prof_thread_stack_range(fp, &unwind_info.stack_range.start,
&unwind_info.stack_range.end) != 0) {
unwind_info.fallback = true;
} else {
assert(fp >= unwind_info.stack_range.start
&& fp < unwind_info.stack_range.end);
}
} }
static __thread void **stack_start = (void **)0; // thread local if (unwind_info.fallback) {
if (stack_start == 0 || stack_end >= stack_start) { goto label_fallback;
stack_start = (void**)prof_thread_stack_start((uintptr_t)stack_end);
}
if (stack_start == 0 || stack_end >= stack_start) {
*len = 0;
return;
} }
unsigned ii = 0; unsigned ii = 0;
void** fp = (void**)stack_end; while (ii < max_len && fp != 0) {
while (fp < stack_start && ii < max_len) { if (fp < unwind_info.stack_range.start ||
vec[ii++] = fp[1]; fp >= unwind_info.stack_range.end) {
void** fp_prev = fp; /*
fp = fp[0]; * Determining the stack range from procfs can be
if (unlikely(fp <= fp_prev)) { // sanity check forward progress * relatively expensive especially for programs with
* many threads / shared libraries. If the stack
* range has changed, it is likely to change again
* in the future (fibers or some other stack
* manipulation). So fall back to backtrace for this
* thread.
*/
unwind_info.fallback = true;
goto label_fallback;
}
void* ip = ((void **)fp)[1];
if (ip == 0) {
break; break;
} }
vec[ii++] = ip;
fp = ((uintptr_t *)fp)[0];
} }
*len = ii; *len = ii;
return;
label_fallback:
/*
* Using the backtrace from execinfo.h here. Note that it may get
* redirected to libunwind when a libunwind not built with build-time
* flag --disable-weak-backtrace is linked.
*/
assert(unwind_info.fallback);
int nframes = backtrace(vec, max_len);
if (nframes > 0) {
*len = nframes;
} else {
*len = 0;
} }
}
JEMALLOC_DIAGNOSTIC_POP JEMALLOC_DIAGNOSTIC_POP
#elif (defined(JEMALLOC_PROF_GCC)) #elif (defined(JEMALLOC_PROF_GCC))
JEMALLOC_DIAGNOSTIC_PUSH JEMALLOC_DIAGNOSTIC_PUSH