Add a MappedMemory interface to TaskMemory and use it in

MachOImageSymbolTableReader.

This results in a speed boost for MachOImageSymbolTableReader because
it’s able to read the entire string table in one operation, rather than
reading each string from the remote process individually. Copying is
also reduced. In a debug-mode build on my laptop, util_test
MachOImageReader.* has improved from ~1400ms to ~1000ms.

TEST=util_test TaskMemory.*:MachOImageReader.*
R=rsesek@chromium.org

Review URL: https://codereview.chromium.org/558313002
This commit is contained in:
Mark Mentovai 2014-09-11 15:10:12 -04:00
parent fbf12950fe
commit 0869b3e86d
4 changed files with 323 additions and 21 deletions

View File

@ -104,6 +104,7 @@ class MachOImageSymbolTableReaderInitializer {
return false;
}
scoped_ptr<TaskMemory::MappedMemory> string_table;
for (size_t symbol_index = 0; symbol_index < symbol_count; ++symbol_index) {
const process_types::nlist& symbol = symbols[symbol_index];
std::string symbol_info = base::StringPrintf(", symbol index %zu%s",
@ -121,11 +122,17 @@ class MachOImageSymbolTableReaderInitializer {
return false;
}
if (!string_table) {
string_table = process_reader_->Memory()->ReadMapped(
strtab_address, strtab_size);
if (!string_table) {
LOG(WARNING) << "could not read string table" << module_info_;
return false;
}
}
std::string name;
if (!process_reader_->Memory()->ReadCStringSizeLimited(
strtab_address + symbol.n_strx,
strtab_size - symbol.n_strx,
&name)) {
if (!string_table->ReadCString(symbol.n_strx, &name)) {
LOG(WARNING) << "could not read string" << symbol_info;
return false;
}

View File

@ -21,17 +21,66 @@
#include "base/logging.h"
#include "base/mac/mach_logging.h"
#include "base/mac/scoped_mach_vm.h"
#include "base/strings/stringprintf.h"
#include "util/stdlib/strnlen.h"
namespace crashpad {
TaskMemory::MappedMemory::~MappedMemory() {
}
bool TaskMemory::MappedMemory::ReadCString(
size_t offset, std::string* string) const {
if (offset >= user_size_) {
LOG(WARNING) << "offset out of range";
return false;
}
const char* string_base = reinterpret_cast<const char*>(data_) + offset;
size_t max_length = user_size_ - offset;
size_t string_length = strnlen(string_base, max_length);
if (string_length == max_length) {
LOG(WARNING) << "unterminated string";
return false;
}
string->assign(string_base, string_length);
return true;
}
TaskMemory::MappedMemory::MappedMemory(vm_address_t vm_address,
size_t vm_size,
size_t user_offset,
size_t user_size)
: vm_(vm_address, vm_size),
data_(reinterpret_cast<const void*>(vm_address + user_offset)),
user_size_(user_size) {
vm_address_t vm_end = vm_address + vm_size;
vm_address_t user_address = reinterpret_cast<vm_address_t>(data_);
vm_address_t user_end = user_address + user_size;
DCHECK_GE(user_address, vm_address);
DCHECK_LE(user_address, vm_end);
DCHECK_GE(user_end, vm_address);
DCHECK_LE(user_end, vm_end);
}
TaskMemory::TaskMemory(mach_port_t task) : task_(task) {
}
bool TaskMemory::Read(mach_vm_address_t address, size_t size, void* buffer) {
scoped_ptr<MappedMemory> memory = ReadMapped(address, size);
if (!memory) {
return false;
}
memcpy(buffer, memory->data(), size);
return true;
}
scoped_ptr<TaskMemory::MappedMemory> TaskMemory::ReadMapped(
mach_vm_address_t address, size_t size) {
if (size == 0) {
return true;
return scoped_ptr<MappedMemory>(new MappedMemory(0, 0, 0, 0));
}
mach_vm_address_t region_address = mach_vm_trunc_page(address);
@ -45,16 +94,12 @@ bool TaskMemory::Read(mach_vm_address_t address, size_t size, void* buffer) {
if (kr != KERN_SUCCESS) {
MACH_LOG(WARNING, kr) << base::StringPrintf(
"mach_vm_read(0x%llx, 0x%llx)", region_address, region_size);
return false;
return scoped_ptr<MappedMemory>();
}
DCHECK_EQ(region_count, region_size);
base::mac::ScopedMachVM region_owner(region, region_count);
const char* region_base = reinterpret_cast<const char*>(region);
memcpy(buffer, &region_base[address - region_address], size);
return true;
return scoped_ptr<MappedMemory>(
new MappedMemory(region, region_size, address - region_address, size));
}
bool TaskMemory::ReadCString(mach_vm_address_t address, std::string* string) {
@ -85,16 +130,17 @@ bool TaskMemory::ReadCStringInternal(mach_vm_address_t address,
do {
mach_vm_size_t read_length =
std::min(size, PAGE_SIZE - (read_address % PAGE_SIZE));
std::string read_string(read_length, '\0');
if (!Read(read_address, read_length, &read_string[0])) {
scoped_ptr<MappedMemory> read_region =
ReadMapped(read_address, read_length);
if (!read_region) {
return false;
}
size_t terminator = read_string.find_first_of('\0');
if (terminator == std::string::npos) {
local_string.append(read_string);
} else {
local_string.append(read_string, 0, terminator);
const char* read_region_data =
reinterpret_cast<const char*>(read_region->data());
size_t read_region_data_length = strnlen(read_region_data, read_length);
local_string.append(read_region_data, read_region_data_length);
if (read_region_data_length < read_length) {
string->swap(local_string);
return true;
}

View File

@ -20,19 +20,79 @@
#include <string>
#include "base/basictypes.h"
#include "base/mac/scoped_mach_vm.h"
#include "base/memory/scoped_ptr.h"
namespace crashpad {
//! \brief Accesses the memory of another Mach task.
class TaskMemory {
public:
//! \brief A memory region mapped from another Mach task.
//!
//! The mapping is maintained until this object is destroyed.
class MappedMemory {
public:
~MappedMemory();
//! \brief Returns a pointer to the data requested by the user.
//!
//! This is the value of the \a vm_address + \a user_offset parameters
//! passed to the constructor, casted to `const void*`.
const void* data() const { return data_; }
//! \brief Reads a `NUL`-terminated C string from the mapped region.
//!
//! This method will read contiguous memory until a `NUL` terminator is
//! found.
//!
//! \param[in] offset The offset into data() of the string to be read.
//! \param[out] string The string, whose contents begin at data() and
//! continue up to a `NUL` terminator.
//!
//! \return `true` on success, with \a string set appropriately. If \a
//! offset is greater than or equal to the \a user_size constructor
//! parameter, or if no `NUL` terminator was found in data() after \a
//! offset, returns `false` with an appropriate warning logged.
bool ReadCString(size_t offset, std::string* string) const;
private:
//! \brief Creates an object that owns a memory region mapped from another
//! Mach task.
//!
//! \param[in] vm_address The address in this process address space where
//! the mapping begins. This must be page-aligned.
//! \param[in] vm_size The total size of the mapping that begins at \a
//! vm_address. This must be page-aligned.
//! \param[in] user_offset The offset into the mapped region where the data
//! requested by the user begins. This accounts for the fact that a
//! mapping must be page-aligned but the user data may not be. This
//! parameter must be equal to or less than \a vm_size.
//! \param[in] user_size The size of the data requested by the user. This
//! parameter can be used to compute the end address of user data, which
//! must be within the mapped region.
MappedMemory(vm_address_t vm_address,
size_t vm_size,
size_t user_offset,
size_t user_size);
base::mac::ScopedMachVM vm_;
const void* data_;
size_t user_size_;
// The outer class needs to be able to call this class private constructor.
friend class TaskMemory;
DISALLOW_COPY_AND_ASSIGN(MappedMemory);
};
//! \param[in] task A send right to the target tasks task port. This object
//! does not take ownership of the send right.
explicit TaskMemory(mach_port_t task);
~TaskMemory() {}
//! \brief Copies memory from the target task into a user-provided buffer in
//! \brief Copies memory from the target task into a caller-provided buffer in
//! the current task.
//!
//! \param[in] address The address, in the target tasks address space, of the
@ -45,8 +105,25 @@ class TaskMemory {
//! \return `true` on success, with \a buffer filled appropriately. `false` on
//! failure, with a warning logged. Failures can occur, for example, when
//! encountering unmapped or unreadable pages.
//!
//! \sa ReadMapped()
bool Read(mach_vm_address_t address, size_t size, void* buffer);
//! \brief Maps memory from the target task into the current task.
//!
//! This interface is an alternative to Read() that does not require the
//! caller to provide a buffer to fill. This avoids copying memory, which can
//! offer a performance improvement.
//!
//! \param[in] address The address, in the target tasks address space, of the
//! memory region to map.
//! \param[in] size The size, in bytes, of the memory region to map.
//!
//! \return On success, a MappedMemory object that provides access to the data
//! requested. On faliure, `NULL`, with a warning logged. Failures can
//! occur, for example, when encountering unmapped or unreadable pages.
scoped_ptr<MappedMemory> ReadMapped(mach_vm_address_t address, size_t size);
//! \brief Reads a `NUL`-terminated C string from the target task into a
//! string in the current task.
//!
@ -60,6 +137,8 @@ class TaskMemory {
//! \return `true` on success, with \a string set appropriately. `false` on
//! failure, with a warning logged. Failures can occur, for example, when
//! encountering unmapped or unreadable pages.
//!
//! \sa MappedMemory::ReadCString()
bool ReadCString(mach_vm_address_t address, std::string* string);
//! \brief Reads a `NUL`-terminated C string from the target task into a
@ -75,6 +154,8 @@ class TaskMemory {
//! failure, with a warning logged. Failures can occur, for example, when
//! a `NUL` terminator is not found within \a size bytes, or when
//! encountering unmapped or unreadable pages.
//!
//! \sa MappedMemory::ReadCString()
bool ReadCStringSizeLimited(mach_vm_address_t address,
mach_vm_size_t size,
std::string* string);

View File

@ -15,11 +15,14 @@
#include "util/mach/task_memory.h"
#include <mach/mach.h>
#include <string.h>
#include <algorithm>
#include <string>
#include "base/mac/scoped_mach_port.h"
#include "base/mac/scoped_mach_vm.h"
#include "base/memory/scoped_ptr.h"
#include "gtest/gtest.h"
#include "util/test/mac/mach_errors.h"
@ -42,42 +45,59 @@ TEST(TaskMemory, ReadSelf) {
}
TaskMemory memory(mach_task_self());
// This tests using both the Read() and ReadMapped() interfaces.
std::string result(kSize, '\0');
scoped_ptr<TaskMemory::MappedMemory> mapped;
// Ensure that the entire region can be read.
ASSERT_TRUE(memory.Read(address, kSize, &result[0]));
EXPECT_EQ(0, memcmp(region, &result[0], kSize));
ASSERT_TRUE((mapped = memory.ReadMapped(address, kSize)));
EXPECT_EQ(0, memcmp(region, mapped->data(), kSize));
// Ensure that a read of length 0 succeeds and doesnt touch the result.
result.assign(kSize, '\0');
std::string zeroes = result;
ASSERT_TRUE(memory.Read(address, 0, &result[0]));
EXPECT_EQ(zeroes, result);
ASSERT_TRUE((mapped = memory.ReadMapped(address, 0)));
// Ensure that a read starting at an unaligned address works.
ASSERT_TRUE(memory.Read(address + 1, kSize - 1, &result[0]));
EXPECT_EQ(0, memcmp(region + 1, &result[0], kSize - 1));
ASSERT_TRUE((mapped = memory.ReadMapped(address + 1, kSize - 1)));
EXPECT_EQ(0, memcmp(region + 1, mapped->data(), kSize - 1));
// Ensure that a read ending at an unaligned address works.
ASSERT_TRUE(memory.Read(address, kSize - 1, &result[0]));
EXPECT_EQ(0, memcmp(region, &result[0], kSize - 1));
ASSERT_TRUE((mapped = memory.ReadMapped(address, kSize - 1)));
EXPECT_EQ(0, memcmp(region, mapped->data(), kSize - 1));
// Ensure that a read starting and ending at unaligned addresses works.
ASSERT_TRUE(memory.Read(address + 1, kSize - 2, &result[0]));
EXPECT_EQ(0, memcmp(region + 1, &result[0], kSize - 2));
ASSERT_TRUE((mapped = memory.ReadMapped(address + 1, kSize - 2)));
EXPECT_EQ(0, memcmp(region + 1, mapped->data(), kSize - 2));
// Ensure that a read of exactly one page works.
ASSERT_TRUE(memory.Read(address + PAGE_SIZE, PAGE_SIZE, &result[0]));
EXPECT_EQ(0, memcmp(region + PAGE_SIZE, &result[0], PAGE_SIZE));
ASSERT_TRUE((mapped = memory.ReadMapped(address + PAGE_SIZE, PAGE_SIZE)));
EXPECT_EQ(0, memcmp(region + PAGE_SIZE, mapped->data(), PAGE_SIZE));
// Ensure that a read of a single byte works.
ASSERT_TRUE(memory.Read(address + 2, 1, &result[0]));
EXPECT_EQ(region[2], result[0]);
ASSERT_TRUE((mapped = memory.ReadMapped(address + 2, 1)));
EXPECT_EQ(region[2], reinterpret_cast<const char*>(mapped->data())[0]);
// Ensure that a read of length zero works and doesnt touch the data.
result[0] = 'M';
ASSERT_TRUE(memory.Read(address + 3, 0, &result[0]));
EXPECT_EQ('M', result[0]);
ASSERT_TRUE((mapped = memory.ReadMapped(address + 3, 0)));
}
TEST(TaskMemory, ReadSelfUnmapped) {
@ -109,6 +129,15 @@ TEST(TaskMemory, ReadSelfUnmapped) {
EXPECT_TRUE(memory.Read(address, PAGE_SIZE, &result[0]));
EXPECT_TRUE(memory.Read(address + PAGE_SIZE - 1, 1, &result[0]));
// Do the same thing with the ReadMapped() interface.
scoped_ptr<TaskMemory::MappedMemory> mapped;
EXPECT_FALSE((mapped = memory.ReadMapped(address, kSize)));
EXPECT_FALSE((mapped = memory.ReadMapped(address + 1, kSize - 1)));
EXPECT_FALSE((mapped = memory.ReadMapped(address + PAGE_SIZE, 1)));
EXPECT_FALSE((mapped = memory.ReadMapped(address + PAGE_SIZE - 1, 2)));
EXPECT_TRUE((mapped = memory.ReadMapped(address, PAGE_SIZE)));
EXPECT_TRUE((mapped = memory.ReadMapped(address + PAGE_SIZE - 1, 1)));
// Repeat the test with an unmapped page instead of an unreadable one. This
// portion of the test may be flaky in the presence of other threads, if
// another thread maps something in the region that is deallocated here.
@ -122,6 +151,14 @@ TEST(TaskMemory, ReadSelfUnmapped) {
EXPECT_FALSE(memory.Read(address + PAGE_SIZE - 1, 2, &result[0]));
EXPECT_TRUE(memory.Read(address, PAGE_SIZE, &result[0]));
EXPECT_TRUE(memory.Read(address + PAGE_SIZE - 1, 1, &result[0]));
// Do the same thing with the ReadMapped() interface.
EXPECT_FALSE((mapped = memory.ReadMapped(address, kSize)));
EXPECT_FALSE((mapped = memory.ReadMapped(address + 1, kSize - 1)));
EXPECT_FALSE((mapped = memory.ReadMapped(address + PAGE_SIZE, 1)));
EXPECT_FALSE((mapped = memory.ReadMapped(address + PAGE_SIZE - 1, 2)));
EXPECT_TRUE((mapped = memory.ReadMapped(address, PAGE_SIZE)));
EXPECT_TRUE((mapped = memory.ReadMapped(address + PAGE_SIZE - 1, 1)));
}
// This function consolidates the cast from a char* to mach_vm_address_t in one
@ -383,4 +420,135 @@ TEST(TaskMemory, ReadCStringSizeLimited_StringLong) {
&memory, &string_long[0], string_long.size(), &result));
}
bool IsAddressMapped(vm_address_t address) {
vm_address_t region_address = address;
vm_size_t region_size;
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
vm_region_basic_info_64 info;
mach_port_t object;
kern_return_t kr = vm_region_64(mach_task_self(),
&region_address,
&region_size,
VM_REGION_BASIC_INFO_64,
reinterpret_cast<vm_region_info_t>(&info),
&count,
&object);
if (kr == KERN_SUCCESS) {
// |object| will be MACH_PORT_NULL (10.9.4 xnu-2422.110.17/osfmk/vm/vm_map.c
// vm_map_region()), but the interface acts as if it might carry a send
// right, so treat it as documented.
base::mac::ScopedMachSendRight object_owner(object);
return address >= region_address && address <= region_address + region_size;
}
if (kr == KERN_INVALID_ADDRESS) {
return false;
}
ADD_FAILURE() << MachErrorMessage(kr, "vm_region_64");;
return false;
}
TEST(TaskMemory, MappedMemoryDeallocates) {
// This tests that once a TaskMemory::MappedMemory object is destroyed, it
// releases the mapped memory that it owned. Technically, this test is not
// valid because after the mapping is released, something else (on another
// thread) might wind up mapped in the same address. In the test environment,
// hopefully there are either no other threads or theyre all quiescent, so
// nothing else should wind up mapped in the address.
TaskMemory memory(mach_task_self());
scoped_ptr<TaskMemory::MappedMemory> mapped;
static const char kTestBuffer[] = "hello!";
mach_vm_address_t test_address =
reinterpret_cast<mach_vm_address_t>(&kTestBuffer);
ASSERT_TRUE((mapped = memory.ReadMapped(test_address, sizeof(kTestBuffer))));
EXPECT_EQ(0, memcmp(kTestBuffer, mapped->data(), sizeof(kTestBuffer)));
vm_address_t mapped_address = reinterpret_cast<vm_address_t>(mapped->data());
EXPECT_TRUE(IsAddressMapped(mapped_address));
mapped.reset();
EXPECT_FALSE(IsAddressMapped(mapped_address));
// This is the same but with a big buffer thats definitely larger than a
// single page. This makes sure that the whole mapped region winds up being
// deallocated.
const size_t kBigSize = 4 * PAGE_SIZE;
scoped_ptr<char[]> big_buffer(new char[kBigSize]);
test_address = reinterpret_cast<mach_vm_address_t>(&big_buffer[0]);
ASSERT_TRUE((mapped = memory.ReadMapped(test_address, kBigSize)));
mapped_address = reinterpret_cast<vm_address_t>(mapped->data());
vm_address_t mapped_last_address = mapped_address + kBigSize - 1;
EXPECT_TRUE(IsAddressMapped(mapped_address));
EXPECT_TRUE(IsAddressMapped(mapped_address + PAGE_SIZE));
EXPECT_TRUE(IsAddressMapped(mapped_last_address));
mapped.reset();
EXPECT_FALSE(IsAddressMapped(mapped_address));
EXPECT_FALSE(IsAddressMapped(mapped_address + PAGE_SIZE));
EXPECT_FALSE(IsAddressMapped(mapped_last_address));
}
TEST(TaskMemory, MappedMemoryReadCString) {
// This tests the behavior of TaskMemory::MappedMemory::ReadCString().
TaskMemory memory(mach_task_self());
scoped_ptr<TaskMemory::MappedMemory> mapped;
static const char kTestBuffer[] = "0\0" "2\0" "45\0" "789";
const mach_vm_address_t kTestAddress =
reinterpret_cast<mach_vm_address_t>(&kTestBuffer);
ASSERT_TRUE((mapped = memory.ReadMapped(kTestAddress, 10)));
std::string string;
ASSERT_TRUE(mapped->ReadCString(0, &string));
EXPECT_EQ("0", string);
ASSERT_TRUE(mapped->ReadCString(1, &string));
EXPECT_EQ("", string);
ASSERT_TRUE(mapped->ReadCString(2, &string));
EXPECT_EQ("2", string);
ASSERT_TRUE(mapped->ReadCString(3, &string));
EXPECT_EQ("", string);
ASSERT_TRUE(mapped->ReadCString(4, &string));
EXPECT_EQ("45", string);
ASSERT_TRUE(mapped->ReadCString(5, &string));
EXPECT_EQ("5", string);
ASSERT_TRUE(mapped->ReadCString(6, &string));
EXPECT_EQ("", string);
// kTestBuffers NUL terminator was not read, so these will see an
// unterminated string and fail.
EXPECT_FALSE(mapped->ReadCString(7, &string));
EXPECT_FALSE(mapped->ReadCString(8, &string));
EXPECT_FALSE(mapped->ReadCString(9, &string));
// This is out of the range of what was read, so it will fail.
EXPECT_FALSE(mapped->ReadCString(10, &string));
EXPECT_FALSE(mapped->ReadCString(11, &string));
// Read it again, this time with a length long enough to include the NUL
// terminator.
ASSERT_TRUE((mapped = memory.ReadMapped(kTestAddress, 11)));
ASSERT_TRUE(mapped->ReadCString(6, &string));
EXPECT_EQ("", string);
// These should now succeed.
ASSERT_TRUE(mapped->ReadCString(7, &string));
EXPECT_EQ("789", string);
ASSERT_TRUE(mapped->ReadCString(8, &string));
EXPECT_EQ("89", string);
ASSERT_TRUE(mapped->ReadCString(9, &string));
EXPECT_EQ("9", string);
EXPECT_TRUE(mapped->ReadCString(10, &string));
EXPECT_EQ("", string);
// These are still out of range.
EXPECT_FALSE(mapped->ReadCString(11, &string));
EXPECT_FALSE(mapped->ReadCString(12, &string));
}
} // namespace