414 lines
16 KiB
C++
414 lines
16 KiB
C++
// Copyright 2021 The Chromium Authors
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "base/profiler/chrome_unwinder_android.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "base/check_op.h"
|
|
#include "base/memory/aligned_memory.h"
|
|
#include "base/notreached.h"
|
|
#include "base/numerics/checked_math.h"
|
|
#include "base/profiler/chrome_unwind_info_android.h"
|
|
|
|
namespace base {
|
|
namespace {
|
|
|
|
uintptr_t* GetRegisterPointer(RegisterContext* context,
|
|
uint8_t register_index) {
|
|
DCHECK_LE(register_index, 15);
|
|
static unsigned long RegisterContext::*const registers[16] = {
|
|
&RegisterContext::arm_r0, &RegisterContext::arm_r1,
|
|
&RegisterContext::arm_r2, &RegisterContext::arm_r3,
|
|
&RegisterContext::arm_r4, &RegisterContext::arm_r5,
|
|
&RegisterContext::arm_r6, &RegisterContext::arm_r7,
|
|
&RegisterContext::arm_r8, &RegisterContext::arm_r9,
|
|
&RegisterContext::arm_r10, &RegisterContext::arm_fp,
|
|
&RegisterContext::arm_ip, &RegisterContext::arm_sp,
|
|
&RegisterContext::arm_lr, &RegisterContext::arm_pc,
|
|
};
|
|
return reinterpret_cast<uintptr_t*>(&(context->*registers[register_index]));
|
|
}
|
|
|
|
// Pops the value on the top of stack out and assign it to target register.
|
|
// This is equivalent to arm instruction `Pop r[n]` where n = `register_index`.
|
|
// Returns whether the pop is successful.
|
|
bool PopRegister(RegisterContext* context, uint8_t register_index) {
|
|
const uintptr_t sp = RegisterContextStackPointer(context);
|
|
const uintptr_t stacktop_value = *reinterpret_cast<uintptr_t*>(sp);
|
|
const auto new_sp = CheckedNumeric<uintptr_t>(sp) + sizeof(uintptr_t);
|
|
const bool success =
|
|
new_sp.AssignIfValid(&RegisterContextStackPointer(context));
|
|
if (success)
|
|
*GetRegisterPointer(context, register_index) = stacktop_value;
|
|
return success;
|
|
}
|
|
|
|
// Decodes the given bytes as an ULEB128 format number and advances the bytes
|
|
// pointer by the size of ULEB128.
|
|
//
|
|
// This function assumes the given bytes are in valid ULEB128
|
|
// format and the decoded number should not overflow `uintptr_t` type.
|
|
uintptr_t DecodeULEB128(const uint8_t*& bytes) {
|
|
uintptr_t value = 0;
|
|
unsigned shift = 0;
|
|
do {
|
|
DCHECK_LE(shift, sizeof(uintptr_t) * 8); // ULEB128 must not overflow.
|
|
value += (*bytes & 0x7fu) << shift;
|
|
shift += 7;
|
|
} while (*bytes++ & 0x80);
|
|
return value;
|
|
}
|
|
|
|
uint8_t GetTopBits(uint8_t byte, unsigned bits) {
|
|
DCHECK_LE(bits, 8u);
|
|
return byte >> (8 - bits);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ChromeUnwinderAndroid::ChromeUnwinderAndroid(
|
|
const ChromeUnwindInfoAndroid& unwind_info,
|
|
uintptr_t chrome_module_base_address,
|
|
uintptr_t text_section_start_address)
|
|
: unwind_info_(unwind_info),
|
|
chrome_module_base_address_(chrome_module_base_address),
|
|
text_section_start_address_(text_section_start_address) {
|
|
DCHECK_GT(text_section_start_address_, chrome_module_base_address_);
|
|
}
|
|
|
|
bool ChromeUnwinderAndroid::CanUnwindFrom(const Frame& current_frame) const {
|
|
return current_frame.module &&
|
|
current_frame.module->GetBaseAddress() == chrome_module_base_address_;
|
|
}
|
|
|
|
UnwindResult ChromeUnwinderAndroid::TryUnwind(RegisterContext* thread_context,
|
|
uintptr_t stack_top,
|
|
std::vector<Frame>* stack) {
|
|
DCHECK(CanUnwindFrom(stack->back()));
|
|
uintptr_t frame_initial_sp = RegisterContextStackPointer(thread_context);
|
|
const uintptr_t unwind_initial_pc =
|
|
RegisterContextInstructionPointer(thread_context);
|
|
|
|
do {
|
|
const uintptr_t pc = RegisterContextInstructionPointer(thread_context);
|
|
const uintptr_t instruction_byte_offset_from_text_section_start =
|
|
pc - text_section_start_address_;
|
|
|
|
const absl::optional<FunctionOffsetTableIndex> function_offset_table_index =
|
|
GetFunctionTableIndexFromInstructionOffset(
|
|
unwind_info_.page_table, unwind_info_.function_table,
|
|
instruction_byte_offset_from_text_section_start);
|
|
|
|
if (!function_offset_table_index) {
|
|
return UnwindResult::kAborted;
|
|
}
|
|
|
|
const uint32_t current_unwind_instruction_index =
|
|
GetFirstUnwindInstructionIndexFromFunctionOffsetTableEntry(
|
|
&unwind_info_
|
|
.function_offset_table[function_offset_table_index
|
|
->function_offset_table_byte_index],
|
|
function_offset_table_index
|
|
->instruction_offset_from_function_start);
|
|
|
|
const uint8_t* current_unwind_instruction =
|
|
&unwind_info_
|
|
.unwind_instruction_table[current_unwind_instruction_index];
|
|
|
|
UnwindInstructionResult instruction_result;
|
|
bool pc_was_updated = false;
|
|
|
|
do {
|
|
instruction_result = ExecuteUnwindInstruction(
|
|
current_unwind_instruction, pc_was_updated, thread_context);
|
|
const uintptr_t sp = RegisterContextStackPointer(thread_context);
|
|
if (sp > stack_top || sp < frame_initial_sp ||
|
|
!IsAligned(sp, sizeof(uintptr_t))) {
|
|
return UnwindResult::kAborted;
|
|
}
|
|
} while (instruction_result ==
|
|
UnwindInstructionResult::kInstructionPending);
|
|
|
|
if (instruction_result == UnwindInstructionResult::kAborted) {
|
|
return UnwindResult::kAborted;
|
|
}
|
|
|
|
DCHECK_EQ(instruction_result, UnwindInstructionResult::kCompleted);
|
|
|
|
const uintptr_t new_sp = RegisterContextStackPointer(thread_context);
|
|
// Validate SP is properly aligned across frames.
|
|
// See
|
|
// https://community.arm.com/arm-community-blogs/b/architectures-and-processors-blog/posts/using-the-stack-in-aarch32-and-aarch64
|
|
// for SP alignment rules.
|
|
if (!IsAligned(new_sp, 2 * sizeof(uintptr_t))) {
|
|
return UnwindResult::kAborted;
|
|
}
|
|
// Validate that SP does not decrease across frames.
|
|
const bool is_leaf_frame = stack->size() == 1;
|
|
// Each frame unwind is expected to only pop from stack memory, which will
|
|
// cause sp to increase.
|
|
// Non-Leaf frames are expected to at least pop lr off stack, so sp is
|
|
// expected to strictly increase for non-leaf frames.
|
|
if (new_sp <= (is_leaf_frame ? frame_initial_sp - 1 : frame_initial_sp)) {
|
|
return UnwindResult::kAborted;
|
|
}
|
|
|
|
// For leaf functions, if SP does not change, PC must change, otherwise,
|
|
// the overall execution state will be the same before/after the frame
|
|
// unwind.
|
|
if (is_leaf_frame && new_sp == frame_initial_sp &&
|
|
RegisterContextInstructionPointer(thread_context) ==
|
|
unwind_initial_pc) {
|
|
return UnwindResult::kAborted;
|
|
}
|
|
|
|
frame_initial_sp = new_sp;
|
|
|
|
stack->emplace_back(RegisterContextInstructionPointer(thread_context),
|
|
module_cache()->GetModuleForAddress(
|
|
RegisterContextInstructionPointer(thread_context)));
|
|
} while (CanUnwindFrom(stack->back()));
|
|
return UnwindResult::kUnrecognizedFrame;
|
|
}
|
|
|
|
UnwindInstructionResult ExecuteUnwindInstruction(
|
|
const uint8_t*& instruction,
|
|
bool& pc_was_updated,
|
|
RegisterContext* thread_context) {
|
|
if (GetTopBits(*instruction, 2) == 0b00) {
|
|
// 00xxxxxx
|
|
// vsp = vsp + (xxxxxx << 2) + 4. Covers range 0x04-0x100 inclusive.
|
|
const uintptr_t offset = ((*instruction++ & 0b00111111u) << 2) + 4;
|
|
|
|
const auto new_sp =
|
|
CheckedNumeric<uintptr_t>(RegisterContextStackPointer(thread_context)) +
|
|
offset;
|
|
if (!new_sp.AssignIfValid(&RegisterContextStackPointer(thread_context))) {
|
|
return UnwindInstructionResult::kAborted;
|
|
}
|
|
} else if (GetTopBits(*instruction, 2) == 0b01) {
|
|
// 01xxxxxx
|
|
// vsp = vsp - (xxxxxx << 2) - 4. Covers range 0x04-0x100 inclusive.
|
|
const uintptr_t offset = ((*instruction++ & 0b00111111u) << 2) + 4;
|
|
const auto new_sp =
|
|
CheckedNumeric<uintptr_t>(RegisterContextStackPointer(thread_context)) -
|
|
offset;
|
|
if (!new_sp.AssignIfValid(&RegisterContextStackPointer(thread_context))) {
|
|
return UnwindInstructionResult::kAborted;
|
|
}
|
|
} else if (GetTopBits(*instruction, 4) == 0b1001) {
|
|
// 1001nnnn (nnnn != 13,15)
|
|
// Set vsp = r[nnnn].
|
|
const uint8_t register_index = *instruction++ & 0b00001111;
|
|
DCHECK_NE(register_index, 13); // Must not set sp to sp.
|
|
DCHECK_NE(register_index, 15); // Must not set sp to pc.
|
|
// Note: We shouldn't have cases that are setting caller-saved registers
|
|
// using this instruction.
|
|
DCHECK_GE(register_index, 4);
|
|
|
|
RegisterContextStackPointer(thread_context) =
|
|
*GetRegisterPointer(thread_context, register_index);
|
|
} else if (GetTopBits(*instruction, 5) == 0b10101) {
|
|
// 10101nnn
|
|
// Pop r4-r[4+nnn], r14
|
|
const uint8_t max_register_index = (*instruction++ & 0b00000111u) + 4;
|
|
for (uint8_t n = 4; n <= max_register_index; n++) {
|
|
if (!PopRegister(thread_context, n)) {
|
|
return UnwindInstructionResult::kAborted;
|
|
}
|
|
}
|
|
if (!PopRegister(thread_context, 14)) {
|
|
return UnwindInstructionResult::kAborted;
|
|
}
|
|
} else if (*instruction == 0b10000000 && *(instruction + 1) == 0) {
|
|
// 10000000 00000000
|
|
// Refuse to unwind.
|
|
instruction += 2;
|
|
return UnwindInstructionResult::kAborted;
|
|
} else if (GetTopBits(*instruction, 4) == 0b1000) {
|
|
const uint32_t register_bitmask =
|
|
((*instruction & 0xfu) << 8) + *(instruction + 1);
|
|
instruction += 2;
|
|
// 1000iiii iiiiiiii
|
|
// Pop up to 12 integer registers under masks {r15-r12}, {r11-r4}
|
|
for (uint8_t register_index = 4; register_index < 16; register_index++) {
|
|
if (register_bitmask & (1 << (register_index - 4))) {
|
|
if (!PopRegister(thread_context, register_index)) {
|
|
return UnwindInstructionResult::kAborted;
|
|
}
|
|
}
|
|
}
|
|
// If we set pc (r15) with value on stack, we should no longer copy lr to
|
|
// pc on COMPLETE.
|
|
pc_was_updated |= register_bitmask & (1 << (15 - 4));
|
|
} else if (*instruction == 0b10110000) {
|
|
// Finish
|
|
// Code 0xb0, Finish, copies VRS[r14] to VRS[r15] and also
|
|
// indicates that no further instructions are to be processed for this
|
|
// frame.
|
|
|
|
instruction++;
|
|
// Only copy lr to pc when pc is not updated by other instructions before.
|
|
if (!pc_was_updated)
|
|
thread_context->arm_pc = thread_context->arm_lr;
|
|
|
|
return UnwindInstructionResult::kCompleted;
|
|
} else if (*instruction == 0b10110010) {
|
|
// 10110010 uleb128
|
|
// vsp = vsp + 0x204 + (uleb128 << 2)
|
|
// (for vsp increments of 0x104-0x200, use 00xxxxxx twice)
|
|
instruction++;
|
|
const auto new_sp =
|
|
CheckedNumeric<uintptr_t>(RegisterContextStackPointer(thread_context)) +
|
|
(CheckedNumeric<uintptr_t>(DecodeULEB128(instruction)) << 2) + 0x204;
|
|
|
|
if (!new_sp.AssignIfValid(&RegisterContextStackPointer(thread_context))) {
|
|
return UnwindInstructionResult::kAborted;
|
|
}
|
|
} else {
|
|
NOTREACHED();
|
|
}
|
|
return UnwindInstructionResult::kInstructionPending;
|
|
}
|
|
|
|
uintptr_t GetFirstUnwindInstructionIndexFromFunctionOffsetTableEntry(
|
|
const uint8_t* function_offset_table_entry,
|
|
int instruction_offset_from_function_start) {
|
|
DCHECK_GE(instruction_offset_from_function_start, 0);
|
|
const uint8_t* current_function_offset_table_position =
|
|
function_offset_table_entry;
|
|
|
|
do {
|
|
const uintptr_t function_offset =
|
|
DecodeULEB128(current_function_offset_table_position);
|
|
|
|
const uintptr_t unwind_table_index =
|
|
DecodeULEB128(current_function_offset_table_position);
|
|
|
|
// Each function always ends at 0 offset. It is guaranteed to find an entry
|
|
// as long as the function offset table is well-structured.
|
|
if (function_offset <=
|
|
static_cast<uint32_t>(instruction_offset_from_function_start))
|
|
return unwind_table_index;
|
|
|
|
} while (true);
|
|
|
|
NOTREACHED();
|
|
return 0;
|
|
}
|
|
|
|
const absl::optional<FunctionOffsetTableIndex>
|
|
GetFunctionTableIndexFromInstructionOffset(
|
|
span<const uint32_t> page_start_instructions,
|
|
span<const FunctionTableEntry> function_offset_table_indices,
|
|
uint32_t instruction_byte_offset_from_text_section_start) {
|
|
DCHECK(!page_start_instructions.empty());
|
|
DCHECK(!function_offset_table_indices.empty());
|
|
// First function on first page should always start from 0 offset.
|
|
DCHECK_EQ(function_offset_table_indices.front()
|
|
.function_start_address_page_instruction_offset,
|
|
0ul);
|
|
|
|
const uint16_t page_number =
|
|
instruction_byte_offset_from_text_section_start >> 17;
|
|
const uint16_t page_instruction_offset =
|
|
(instruction_byte_offset_from_text_section_start >> 1) &
|
|
0xffff; // 16 bits.
|
|
|
|
// Invalid instruction_byte_offset_from_text_section_start:
|
|
// instruction_byte_offset_from_text_section_start falls after the last page.
|
|
if (page_number >= page_start_instructions.size()) {
|
|
return absl::nullopt;
|
|
}
|
|
|
|
const span<const FunctionTableEntry>::const_iterator
|
|
function_table_entry_start =
|
|
function_offset_table_indices.begin() +
|
|
checked_cast<ptrdiff_t>(page_start_instructions[page_number]);
|
|
const span<const FunctionTableEntry>::const_iterator
|
|
function_table_entry_end =
|
|
page_number == page_start_instructions.size() - 1
|
|
? function_offset_table_indices.end()
|
|
: function_offset_table_indices.begin() +
|
|
checked_cast<ptrdiff_t>(
|
|
page_start_instructions[page_number + 1]);
|
|
|
|
// `std::upper_bound` finds first element that > target in range
|
|
// [function_table_entry_start, function_table_entry_end).
|
|
const auto first_larger_entry_location = std::upper_bound(
|
|
function_table_entry_start, function_table_entry_end,
|
|
page_instruction_offset,
|
|
[](uint16_t page_instruction_offset, const FunctionTableEntry& entry) {
|
|
return page_instruction_offset <
|
|
entry.function_start_address_page_instruction_offset;
|
|
});
|
|
|
|
// Offsets the element found by 1 to get the biggest element that <= target.
|
|
const auto entry_location = first_larger_entry_location - 1;
|
|
|
|
// When all offsets in current range > page_instruction_offset (including when
|
|
// there is no entry in current range), the `FunctionTableEntry` we are
|
|
// looking for is not within the function_offset_table_indices range we are
|
|
// inspecting, because the function is too long that it spans multiple pages.
|
|
//
|
|
// We need to locate the previous entry on function_offset_table_indices and
|
|
// find its corresponding page_table index.
|
|
//
|
|
// Example:
|
|
// +--------------------+--------------------+
|
|
// | <-----2 byte-----> | <-----2 byte-----> |
|
|
// +--------------------+--------------------+
|
|
// | Page Offset | Offset Table Index |
|
|
// +--------------------+--------------------+-----
|
|
// | 10 | XXX | |
|
|
// +--------------------+--------------------+ |
|
|
// | ... | ... |Page 0x100
|
|
// +--------------------+--------------------+ |
|
|
// | 65500 | ZZZ | |
|
|
// +--------------------+--------------------+----- Page 0x101 is empty
|
|
// | 200 | AAA | |
|
|
// +--------------------+--------------------+ |
|
|
// | ... | ... |Page 0x102
|
|
// +--------------------+--------------------+ |
|
|
// | 65535 | BBB | |
|
|
// +--------------------+--------------------+-----
|
|
//
|
|
// Example:
|
|
// For
|
|
// - page_number = 0x100, page_instruction_offset >= 65535
|
|
// - page_number = 0x101, all page_instruction_offset
|
|
// - page_number = 0x102, page_instruction_offset < 200
|
|
// We should be able to map them all to entry [65500, ZZZ] in page 0x100.
|
|
|
|
// Finds the page_number that corresponds to `entry_location`. The page
|
|
// might not be the page we are inspecting, when the function spans over
|
|
// multiple pages.
|
|
uint16_t function_start_page_number = page_number;
|
|
while (function_offset_table_indices.begin() +
|
|
checked_cast<ptrdiff_t>(
|
|
page_start_instructions[function_start_page_number]) >
|
|
entry_location) {
|
|
// First page in page table must not be empty.
|
|
DCHECK_NE(function_start_page_number, 0);
|
|
function_start_page_number--;
|
|
};
|
|
|
|
const uint32_t function_start_address_instruction_offset =
|
|
(uint32_t{function_start_page_number} << 16) +
|
|
entry_location->function_start_address_page_instruction_offset;
|
|
|
|
const int instruction_offset_from_function_start =
|
|
static_cast<int>((instruction_byte_offset_from_text_section_start >> 1) -
|
|
function_start_address_instruction_offset);
|
|
|
|
DCHECK_GE(instruction_offset_from_function_start, 0);
|
|
return FunctionOffsetTableIndex{
|
|
instruction_offset_from_function_start,
|
|
entry_location->function_offset_table_byte_index,
|
|
};
|
|
}
|
|
|
|
} // namespace base
|