Commit 3f99e4d3 by Jamie Madill Committed by Angle LUCI CQ

Better stack traces on Linux.

Checks the memory information for the process to compute a more accurate address for each function. Bug: angleproject:6070 Change-Id: I57f927f3641298af7921522da0ece683f8fd8faf Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/2971838Reviewed-by: 's avatarShahbaz Youssefi <syoussefi@chromium.org> Reviewed-by: 's avatarTim Van Patten <timvp@google.com> Commit-Queue: Jamie Madill <jmadill@chromium.org>
parent 3d37a374
......@@ -12,8 +12,11 @@
#include "common/FixedVector.h"
#include "common/angleutils.h"
#include "common/string_utils.h"
#include "common/system_utils.h"
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
......@@ -35,11 +38,37 @@
# include <dlfcn.h>
# include <execinfo.h>
# include <libgen.h>
# include <link.h>
# include <signal.h>
# include <string.h>
# endif // defined(ANGLE_PLATFORM_APPLE)
#endif // !defined(ANGLE_PLATFORM_ANDROID) && !defined(ANGLE_PLATFORM_FUCHSIA)
// This code snippet is coped from Chromium's base/posix/eintr_wrapper.h.
#if defined(NDEBUG)
# define HANDLE_EINTR(x) \
({ \
decltype(x) eintr_wrapper_result; \
do \
{ \
eintr_wrapper_result = (x); \
} while (eintr_wrapper_result == -1 && errno == EINTR); \
eintr_wrapper_result; \
})
#else
# define HANDLE_EINTR(x) \
({ \
int eintr_wrapper_counter = 0; \
decltype(x) eintr_wrapper_result; \
do \
{ \
eintr_wrapper_result = (x); \
} while (eintr_wrapper_result == -1 && errno == EINTR && \
eintr_wrapper_counter++ < 100); \
eintr_wrapper_result; \
})
#endif // NDEBUG
namespace angle
{
#if defined(ANGLE_PLATFORM_ANDROID) || defined(ANGLE_PLATFORM_FUCHSIA)
......@@ -115,8 +144,248 @@ static void Handler(int sig)
# if defined(ANGLE_HAS_ADDR2LINE)
namespace
{
// The following code was adapted from Chromium's "stack_trace_posix.cc".
// Describes a region of mapped memory and the path of the file mapped.
struct MappedMemoryRegion
{
enum Permission
{
READ = 1 << 0,
WRITE = 1 << 1,
EXECUTE = 1 << 2,
PRIVATE = 1 << 3, // If set, region is private, otherwise it is shared.
};
// The address range [start,end) of mapped memory.
uintptr_t start;
uintptr_t end;
// Byte offset into |path| of the range mapped into memory.
unsigned long long offset;
// Image base, if this mapping corresponds to an ELF image.
uintptr_t base;
// Bitmask of read/write/execute/private/shared permissions.
uint8_t permissions;
// Name of the file mapped into memory.
//
// NOTE: path names aren't guaranteed to point at valid files. For example,
// "[heap]" and "[stack]" are used to represent the location of the process'
// heap and stack, respectively.
std::string path;
};
using MemoryRegionArray = std::vector<MappedMemoryRegion>;
bool ReadProcMaps(std::string *proc_maps)
{
// seq_file only writes out a page-sized amount on each call. Refer to header
// file for details.
const long kReadSize = sysconf(_SC_PAGESIZE);
int fd(HANDLE_EINTR(open("/proc/self/maps", O_RDONLY)));
if (fd == -1)
{
fprintf(stderr, "Couldn't open /proc/self/maps\n");
return false;
}
proc_maps->clear();
while (true)
{
// To avoid a copy, resize |proc_maps| so read() can write directly into it.
// Compute |buffer| afterwards since resize() may reallocate.
size_t pos = proc_maps->size();
proc_maps->resize(pos + kReadSize);
void *buffer = &(*proc_maps)[pos];
ssize_t bytes_read = HANDLE_EINTR(read(fd, buffer, kReadSize));
if (bytes_read < 0)
{
fprintf(stderr, "Couldn't read /proc/self/maps\n");
proc_maps->clear();
close(fd);
return false;
}
// ... and don't forget to trim off excess bytes.
proc_maps->resize(pos + bytes_read);
if (bytes_read == 0)
break;
}
close(fd);
return true;
}
bool ParseProcMaps(const std::string &input, MemoryRegionArray *regions_out)
{
ASSERT(regions_out);
MemoryRegionArray regions;
// This isn't async safe nor terribly efficient, but it doesn't need to be at
// this point in time.
std::vector<std::string> lines = SplitString(input, "\n", TRIM_WHITESPACE, SPLIT_WANT_ALL);
for (size_t i = 0; i < lines.size(); ++i)
{
// Due to splitting on '\n' the last line should be empty.
if (i == lines.size() - 1)
{
if (!lines[i].empty())
{
fprintf(stderr, "ParseProcMaps: Last line not empty");
return false;
}
break;
}
MappedMemoryRegion region;
const char *line = lines[i].c_str();
char permissions[5] = {'\0'}; // Ensure NUL-terminated string.
uint8_t dev_major = 0;
uint8_t dev_minor = 0;
long inode = 0;
int path_index = 0;
// Sample format from man 5 proc:
//
// address perms offset dev inode pathname
// 08048000-08056000 r-xp 00000000 03:0c 64593 /usr/sbin/gpm
//
// The final %n term captures the offset in the input string, which is used
// to determine the path name. It *does not* increment the return value.
// Refer to man 3 sscanf for details.
if (sscanf(line, "%" SCNxPTR "-%" SCNxPTR " %4c %llx %hhx:%hhx %ld %n", &region.start,
&region.end, permissions, &region.offset, &dev_major, &dev_minor, &inode,
&path_index) < 7)
{
fprintf(stderr, "ParseProcMaps: sscanf failed for line: %s\n", line);
return false;
}
region.permissions = 0;
if (permissions[0] == 'r')
region.permissions |= MappedMemoryRegion::READ;
else if (permissions[0] != '-')
return false;
if (permissions[1] == 'w')
region.permissions |= MappedMemoryRegion::WRITE;
else if (permissions[1] != '-')
return false;
if (permissions[2] == 'x')
region.permissions |= MappedMemoryRegion::EXECUTE;
else if (permissions[2] != '-')
return false;
if (permissions[3] == 'p')
region.permissions |= MappedMemoryRegion::PRIVATE;
else if (permissions[3] != 's' && permissions[3] != 'S') // Shared memory.
return false;
// Pushing then assigning saves us a string copy.
regions.push_back(region);
regions.back().path.assign(line + path_index);
}
regions_out->swap(regions);
return true;
}
// Set the base address for each memory region by reading ELF headers in
// process memory.
void SetBaseAddressesForMemoryRegions(MemoryRegionArray &regions)
{
int mem_fd(HANDLE_EINTR(open("/proc/self/mem", O_RDONLY | O_CLOEXEC)));
if (mem_fd == -1)
return;
auto safe_memcpy = [&mem_fd](void *dst, uintptr_t src, size_t size) {
return HANDLE_EINTR(pread(mem_fd, dst, size, src)) == ssize_t(size);
};
uintptr_t cur_base = 0;
for (MappedMemoryRegion &r : regions)
{
ElfW(Ehdr) ehdr;
static_assert(SELFMAG <= sizeof(ElfW(Ehdr)), "SELFMAG too large");
if ((r.permissions & MappedMemoryRegion::READ) &&
safe_memcpy(&ehdr, r.start, sizeof(ElfW(Ehdr))) &&
memcmp(ehdr.e_ident, ELFMAG, SELFMAG) == 0)
{
switch (ehdr.e_type)
{
case ET_EXEC:
cur_base = 0;
break;
case ET_DYN:
// Find the segment containing file offset 0. This will correspond
// to the ELF header that we just read. Normally this will have
// virtual address 0, but this is not guaranteed. We must subtract
// the virtual address from the address where the ELF header was
// mapped to get the base address.
//
// If we fail to find a segment for file offset 0, use the address
// of the ELF header as the base address.
cur_base = r.start;
for (unsigned i = 0; i != ehdr.e_phnum; ++i)
{
ElfW(Phdr) phdr;
if (safe_memcpy(&phdr, r.start + ehdr.e_phoff + i * sizeof(phdr),
sizeof(phdr)) &&
phdr.p_type == PT_LOAD && phdr.p_offset == 0)
{
cur_base = r.start - phdr.p_vaddr;
break;
}
}
break;
default:
// ET_REL or ET_CORE. These aren't directly executable, so they
// don't affect the base address.
break;
}
}
r.base = cur_base;
}
close(mem_fd);
}
// Parses /proc/self/maps in order to compile a list of all object file names
// for the modules that are loaded in the current process.
// Returns true on success.
bool CacheMemoryRegions(MemoryRegionArray &regions)
{
// Reads /proc/self/maps.
std::string contents;
if (!ReadProcMaps(&contents))
{
fprintf(stderr, "CacheMemoryRegions: Failed to read /proc/self/maps\n");
return false;
}
// Parses /proc/self/maps.
if (!ParseProcMaps(contents, &regions))
{
fprintf(stderr, "CacheMemoryRegions: Failed to parse the contents of /proc/self/maps\n");
return false;
}
SetBaseAddressesForMemoryRegions(regions);
return true;
}
constexpr size_t kAddr2LineMaxParameters = 50;
using Addr2LineCommandLine = angle::FixedVector<const char *, kAddr2LineMaxParameters>;
void CallAddr2Line(const Addr2LineCommandLine &commandLine)
{
pid_t pid = fork();
......@@ -140,6 +409,39 @@ void CallAddr2Line(const Addr2LineCommandLine &commandLine)
_exit(EXIT_FAILURE); // exec never returns
}
}
constexpr size_t kMaxAddressLen = 1024;
using AddressBuffer = angle::FixedVector<char, kMaxAddressLen>;
const char *ResolveAddress(const MemoryRegionArray &regions,
const std::string &resolvedModule,
const char *address,
AddressBuffer &buffer)
{
size_t lastModuleSlash = resolvedModule.rfind('/');
ASSERT(lastModuleSlash != std::string::npos);
std::string baseModule = resolvedModule.substr(lastModuleSlash);
for (const MappedMemoryRegion &region : regions)
{
size_t pathSlashPos = region.path.rfind('/');
if (pathSlashPos != std::string::npos && region.path.substr(pathSlashPos) == baseModule)
{
uintptr_t scannedAddress;
int scanReturn = sscanf(address, "%lX", &scannedAddress);
ASSERT(scanReturn == 1);
scannedAddress -= region.base;
char printBuffer[255] = {};
size_t scannedSize = sprintf(printBuffer, "0x%lX", scannedAddress);
size_t bufferSize = buffer.size();
buffer.resize(bufferSize + scannedSize + 1, 0);
memcpy(&buffer[bufferSize], printBuffer, scannedSize);
return &buffer[bufferSize];
}
}
return address;
}
} // anonymous namespace
# endif // defined(ANGLE_HAS_ADDR2LINE)
......@@ -152,6 +454,10 @@ void PrintStackBacktrace()
char **symbols = backtrace_symbols(stack, count);
# if defined(ANGLE_HAS_ADDR2LINE)
MemoryRegionArray regions;
CacheMemoryRegions(regions);
// Child process executes addr2line
constexpr size_t kAddr2LineFixedParametersCount = 6;
Addr2LineCommandLine commandLineArgs = {
......@@ -164,6 +470,7 @@ void PrintStackBacktrace()
};
const char *currentModule = "";
std::string resolvedModule;
AddressBuffer addressBuffer;
for (int i = 0; i < count; i++)
{
......@@ -171,19 +478,15 @@ void PrintStackBacktrace()
// symbol looks like the following:
//
// path/to/module(+address) [globalAddress]
//
// We are interested in module and address. symbols[i] is modified in place to replace '('
// and ')' with 0, allowing c-strings to point to the module and address. This allows
// accumulating addresses without having to create another storage for them.
// path/to/module(+localAddress) [address]
//
// If module is not an absolute path, it needs to be resolved.
char *module = symbol;
char *address = strchr(symbol, '+') + 1;
char *address = strchr(symbol, '[') + 1;
*strchr(module, '(') = 0;
*strchr(address, ')') = 0;
*strchr(address, ']') = 0;
// If module is the same as last, continue batching addresses. If commandLineArgs has
// reached its capacity however, make the call to addr2line already. Note that there should
......@@ -191,7 +494,8 @@ void PrintStackBacktrace()
if (strcmp(module, currentModule) == 0 &&
commandLineArgs.size() + 1 < commandLineArgs.max_size())
{
commandLineArgs.push_back(address);
commandLineArgs.push_back(
ResolveAddress(regions, resolvedModule, address, addressBuffer));
continue;
}
......@@ -201,6 +505,7 @@ void PrintStackBacktrace()
{
commandLineArgs.push_back(nullptr);
CallAddr2Line(commandLineArgs);
addressBuffer.clear();
}
// Reset the command line and remember this module as the current.
......@@ -247,8 +552,23 @@ void PrintStackBacktrace()
}
}
// Check if this is a symlink. We assume the symlinks are relative to the target.
constexpr size_t kBufSize = 1000;
char linkBuf[kBufSize] = {};
ssize_t readLinkRet = readlink(resolvedModule.c_str(), linkBuf, kBufSize);
if (readLinkRet != -1)
{
ASSERT(strchr(linkBuf, '/') == nullptr);
size_t lastSlash = resolvedModule.rfind('/');
ASSERT(lastSlash != std::string::npos);
resolvedModule = resolvedModule.substr(0, lastSlash + 1) + linkBuf;
}
const char *resolvedAddress =
ResolveAddress(regions, resolvedModule, address, addressBuffer);
commandLineArgs.push_back(resolvedModule.c_str());
commandLineArgs.push_back(address);
commandLineArgs.push_back(resolvedAddress);
}
// Call addr2line for the last batch of addresses.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment