fatal: automatically collect backtrace for callers.

This commit is contained in:
Michael Scire 2018-11-14 03:23:28 -08:00
parent 9714db14d2
commit 962fa0a690
9 changed files with 481 additions and 19 deletions

View file

@ -12,7 +12,7 @@
"is_64_bit": true,
"address_space_type": 3,
"filesystem_access": {
"permissions": "0x0000000000100000"
"permissions": "0xFFFFFFFFFFFFFFFF"
},
"service_access": ["bpc", "bpc:c", "erpt:c", "fsp-srv", "gpio", "i2c", "lbl", "lm", "nvdrv:s", "pcv", "pl:u", "pm:info", "psm", "set", "set:sys", "spsm", "vi:m", "vi:s"],
"service_host": ["fatal:p", "fatal:u"],
@ -76,7 +76,14 @@
"svcReplyAndReceive": "0x43",
"svcReplyAndReceiveWithUserBuffer": "0x44",
"svcCreateEvent": "0x45",
"svcReadWriteRegister": "0x4E"
"svcReadWriteRegister": "0x4E",
"svcDebugActiveProcess": "0x60",
"svcGetDebugEvent": "0x63",
"svcGetThreadList": "0x66",
"svcGetDebugThreadContext": "0x67",
"svcQueryDebugProcessMemory": "0x69",
"svcReadDebugProcessMemory": "0x6a",
"svcGetDebugThreadParam": "0x6d"
}
}, {
"type": "min_kernel_version",

View file

@ -0,0 +1,256 @@
/*
* Copyright (c) 2018 Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <map>
#include <switch.h>
#include "fatal_debug.hpp"
#include "fatal_config.hpp"
static bool IsAddressReadable(Handle debug_handle, u64 address, u64 size, MemoryInfo *o_mi) {
MemoryInfo mi;
u32 pi;
if (o_mi == NULL) {
o_mi = &mi;
}
if (R_FAILED(svcQueryDebugProcessMemory(o_mi, &pi, debug_handle, address))) {
return false;
}
/* Must be readable */
if ((o_mi->perm & Perm_R) != Perm_R) {
return false;
}
/* Must have space for both userdata address and userdata size. */
if (address < o_mi->addr || o_mi->addr + o_mi->size < address + size) {
return false;
}
return true;
}
static bool CheckThreadIsFatalCaller(FatalThrowContext *ctx, u64 debug_handle, u64 thread_id, u64 thread_tls_addr, ThreadContext *thread_ctx) {
/* Verify that the thread is running or waiting. */
{
u64 _;
u32 thread_state;
if (R_FAILED(svcGetDebugThreadParam(&_, &thread_state, debug_handle, thread_id, DebugThreadParam_State))) {
return false;
}
if (thread_state > 1) {
return false;
}
}
/* Get the thread context. */
if (R_FAILED(svcGetDebugThreadContext(thread_ctx, debug_handle, thread_id, 0xF))) {
return false;
}
/* Check if PC is readable. */
if (!IsAddressReadable(debug_handle, thread_ctx->pc.x, sizeof(u32), NULL)) {
return false;
}
/* Try to read the current instruction. */
u32 insn;
if (R_FAILED(svcReadDebugProcessMemory(&insn, debug_handle, thread_ctx->pc.x, sizeof(insn)))) {
return false;
}
/* If the instruction isn't svcSendSyncRequest, it's not the fatal caller. */
if (insn != 0xD4000421) {
return false;
}
/* The fatal caller will have readable tls. */
if (!IsAddressReadable(debug_handle, thread_tls_addr, 0x100, NULL)) {
return false;
}
/* Read in the fatal caller's tls. */
u8 thread_tls[0x100];
if (R_FAILED(svcReadDebugProcessMemory(thread_tls, debug_handle, thread_tls_addr, sizeof(thread_tls)))) {
return false;
}
/* Replace our tls with the fatal caller's. */
std::memcpy(armGetTls(), thread_tls, sizeof(thread_tls));
/* Parse the command that the thread sent. */
{
IpcParsedCommand r;
if (R_FAILED(ipcParse(&r))) {
return false;
}
/* Fatal command takes in a PID, only one buffer max. */
if (!r.HasPid || r.NumStatics || r.NumStaticsOut || r.NumHandles) {
return false;
}
struct {
u32 magic;
u32 version;
u64 cmd_id;
u32 err_code;
} *raw = (decltype(raw))(r.Raw);
if (raw->magic != SFCI_MAGIC) {
return false;
}
if (raw->cmd_id > 2) {
return false;
}
if (raw->cmd_id != 2 && r.NumBuffers) {
return false;
}
if (raw->err_code != ctx->error_code) {
return false;
}
}
/* We found our caller. */
return true;
}
void TryCollectDebugInformation(FatalThrowContext *ctx, u64 pid) {
Handle debug_handle;
if (R_SUCCEEDED(svcDebugActiveProcess(&debug_handle, pid))) {
/* Ensure we close the debugged process. */
ON_SCOPE_EXIT { svcCloseHandle(debug_handle); };
/* First things first, check if process is 64 bits, and get list of thread infos. */
std::unordered_map<u64, u64> thread_id_to_tls;
{
bool got_attach_process = false;
DebugEventInfo d;
while (R_SUCCEEDED(svcGetDebugEvent((u8 *)&d, debug_handle))) {
if (d.type == DebugEventType::AttachProcess) {
ctx->cpu_ctx.is_aarch32 = (d.info.attach_process.flags & 1) == 0;
got_attach_process = true;
} else if (d.type == DebugEventType::AttachThread) {
thread_id_to_tls[d.info.attach_thread.thread_id] = d.info.attach_thread.tls_address;
}
}
if (!got_attach_process) {
return;
}
}
/* TODO: Try to collect information on 32-bit fatals. This shouldn't really matter for any real use case. */
if (ctx->cpu_ctx.is_aarch32) {
return;
}
/* Welcome to hell. */
bool found_fatal_caller = false;
u64 thread_id = 0;
ThreadContext thread_ctx;
{
/* We start by trying to get a list of threads. */
u32 thread_count;
u64 thread_ids[0x60];
if (R_FAILED(svcGetThreadList(&thread_count, thread_ids, 0x60, debug_handle))) {
return;
}
/* We need to locate the thread that's called fatal. */
for (u32 i = 0; i < thread_count; i++) {
const u64 cur_thread_id = thread_ids[i];
if (thread_id_to_tls.find(cur_thread_id) == thread_id_to_tls.end()) {
continue;
}
if (CheckThreadIsFatalCaller(ctx, debug_handle, cur_thread_id, thread_id_to_tls[cur_thread_id], &thread_ctx)) {
thread_id = cur_thread_id;
found_fatal_caller = true;
break;
}
}
if (!found_fatal_caller) {
return;
}
}
if (R_FAILED(svcGetDebugThreadContext(&thread_ctx, debug_handle, thread_id, 0xF))) {
return;
}
/* So we found our caller. */
for (u32 i = 0; i < 29; i++) {
/* GetDebugThreadContext won't give us any of these registers, because thread is in SVC :( */
ctx->has_gprs[i] = false;
}
for (u32 i = 29; i < NumAarch64Gprs; i++) {
ctx->has_gprs[i] = true;
}
ctx->cpu_ctx.aarch64_ctx.fp = thread_ctx.fp;
ctx->cpu_ctx.aarch64_ctx.lr = thread_ctx.lr;
ctx->cpu_ctx.aarch64_ctx.sp = thread_ctx.sp;
ctx->cpu_ctx.aarch64_ctx.pc = thread_ctx.pc.x;
/* Parse a stack trace. */
u64 cur_fp = thread_ctx.fp;
for (unsigned int i = 0; i < sizeof(ctx->cpu_ctx.aarch64_ctx.stack_trace)/sizeof(u64); i++) {
/* Validate the current frame. */
if (cur_fp == 0 || (cur_fp & 0xF)) {
break;
}
/* Read a new frame. */
StackFrame cur_frame;
if (R_FAILED(svcReadDebugProcessMemory(&cur_frame, debug_handle, cur_fp, sizeof(StackFrame)))) {
break;
}
/* Advance to the next frame. */
ctx->cpu_ctx.aarch64_ctx.stack_trace[ctx->cpu_ctx.aarch64_ctx.stack_trace_size++] = cur_frame.lr;
cur_fp = cur_frame.fp;
}
/* Parse the starting address. */
{
u64 guess = thread_ctx.pc.x;
MemoryInfo mi;
u32 pi;
if (R_FAILED(svcQueryDebugProcessMemory(&mi, &pi, debug_handle, guess)) || mi.perm != Perm_Rx) {
return;
}
/* Iterate backwards until we find the memory before the code region. */
while (mi.addr > 0) {
if (R_FAILED(svcQueryDebugProcessMemory(&mi, &pi, debug_handle, guess))) {
return;
}
if (mi.type == MemType_Unmapped) {
/* Code region will be at the end of the unmapped region preceding it. */
ctx->cpu_ctx.aarch64_ctx.start_address = mi.addr + mi.size;
break;
}
guess -= 4;
}
}
}
}

View file

@ -0,0 +1,149 @@
/*
* Copyright (c) 2018 Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <switch.h>
#include <stratosphere.hpp>
#include "fatal_types.hpp"
void TryCollectDebugInformation(FatalThrowContext *ctx, u64 pid);
struct StackFrame {
u64 fp;
u64 lr;
};
struct AttachProcessInfo {
u64 title_id;
u64 process_id;
char name[0xC];
u32 flags;
u64 user_exception_context_address; /* 5.0.0+ */
};
struct AttachThreadInfo {
u64 thread_id;
u64 tls_address;
u64 entrypoint;
};
/* TODO: ExitProcessInfo */
/* TODO: ExitThreadInfo */
enum class DebugExceptionType : u32 {
UndefinedInstruction = 0,
InstructionAbort = 1,
DataAbort = 2,
AlignmentFault = 3,
DebuggerAttached = 4,
BreakPoint = 5,
UserBreak = 6,
DebuggerBreak = 7,
BadSvc = 8,
UnknownNine = 9,
};
static inline const char *GetDebugExceptionTypeStr(DebugExceptionType type) {
switch (type) {
case DebugExceptionType::UndefinedInstruction:
return "Undefined Instruction";
case DebugExceptionType::InstructionAbort:
return "Instruction Abort";
case DebugExceptionType::DataAbort:
return "Data Abort";
case DebugExceptionType::AlignmentFault:
return "Alignment Fault";
case DebugExceptionType::DebuggerAttached:
return "Debugger Attached";
case DebugExceptionType::BreakPoint:
return "Break Point";
case DebugExceptionType::UserBreak:
return "User Break";
case DebugExceptionType::DebuggerBreak:
return "Debugger Break";
case DebugExceptionType::BadSvc:
return "Bad Svc";
case DebugExceptionType::UnknownNine:
return "Unknown Nine";
default:
return "Unknown";
}
}
struct UndefinedInstructionInfo {
u32 insn;
};
struct DataAbortInfo {
u64 address;
};
struct AlignmentFaultInfo {
u64 address;
};
struct UserBreakInfo {
u64 break_reason;
u64 address;
u64 size;
};
struct BadSvcInfo {
u32 id;
};
union SpecificExceptionInfo {
UndefinedInstructionInfo undefined_instruction;
DataAbortInfo data_abort;
AlignmentFaultInfo alignment_fault;
UserBreakInfo user_break;
BadSvcInfo bad_svc;
u64 raw;
};
struct ExceptionInfo {
DebugExceptionType type;
u64 address;
SpecificExceptionInfo specific;
};
enum class DebugEventType : u32 {
AttachProcess = 0,
AttachThread = 1,
ExitProcess = 2,
ExitThread = 3,
Exception = 4
};
union DebugInfo {
AttachProcessInfo attach_process;
AttachThreadInfo attach_thread;
ExceptionInfo exception;
};
struct DebugEventInfo {
DebugEventType type;
u32 flags;
u64 thread_id;
union {
DebugInfo info;
u64 _[0x40/sizeof(u64)];
};
};
static_assert(sizeof(DebugEventInfo) >= 0x50, "Incorrect DebugEventInfo definition!");

View file

@ -163,6 +163,14 @@ void FontManager::PrintMonospaceU32(u32 x) {
DrawString(char_buf, false, true);
}
void FontManager::PrintMonospaceBlank(u32 width) {
char char_buf[0x400] = {0};
for (size_t i = 0; i < width && i < sizeof(char_buf); i++) {
char_buf[i] = ' ';
}
DrawString(char_buf, false, true);
}
void FontManager::SetFontColor(u16 color) {
g_font_color = color;

View file

@ -41,4 +41,5 @@ class FontManager {
static void PrintFormat(const char *format, ...);
static void PrintMonospaceU64(u64 x);
static void PrintMonospaceU32(u32 x);
static void PrintMonospaceBlank(u32 width);
};

View file

@ -225,21 +225,40 @@ Result ShowFatalTask::ShowFatal() {
/* Print GPRs. */
FontManager::SetFontSize(14.0f);
FontManager::PrintLine("General Purpose Registers");
FontManager::Print("General Purpose Registers ");
{
FontManager::SetPosition(FontManager::GetX() + 2, FontManager::GetY());
u32 x = FontManager::GetX();
FontManager::Print("PC: ");
FontManager::SetPosition(x + 47, FontManager::GetY());
}
if (this->ctx->cpu_ctx.is_aarch32) {
FontManager::PrintMonospaceU32(this->ctx->cpu_ctx.aarch32_ctx.pc);
} else {
FontManager::PrintMonospaceU64(this->ctx->cpu_ctx.aarch64_ctx.pc);
}
FontManager::PrintLine("");
FontManager::SetPosition(32, FontManager::GetY());
FontManager::AddSpacingLines(0.5f);
if (this->ctx->cpu_ctx.is_aarch32) {
for (size_t i = 0; i < (NumAarch32Gprs / 2); i++) {
u32 x = FontManager::GetX();
FontManager::PrintFormat("%s:", Aarch32GprNames[i]);
FontManager::SetPosition(x + 47, FontManager::GetY());
FontManager::Print("0x");
FontManager::PrintMonospaceU32(this->ctx->cpu_ctx.aarch32_ctx.r[i]);
if (this->ctx->has_gprs[i]) {
FontManager::PrintMonospaceU32(this->ctx->cpu_ctx.aarch32_ctx.r[i]);
} else {
FontManager::PrintMonospaceBlank(8);
}
FontManager::Print(" ");
x = FontManager::GetX();
FontManager::PrintFormat("%s:", Aarch32GprNames[i + (NumAarch32Gprs / 2)]);
FontManager::SetPosition(x + 47, FontManager::GetY());
FontManager::Print("0x");
FontManager::PrintMonospaceU32(this->ctx->cpu_ctx.aarch32_ctx.r[i + (NumAarch32Gprs / 2)]);
if (this->ctx->has_gprs[i + (NumAarch32Gprs / 2)]) {
FontManager::PrintMonospaceU32(this->ctx->cpu_ctx.aarch32_ctx.r[i + (NumAarch32Gprs / 2)]);
} else {
FontManager::PrintMonospaceBlank(8);
}
if (i == (NumAarch32Gprs / 2) - 1) {
FontManager::Print(" ");
@ -254,12 +273,20 @@ Result ShowFatalTask::ShowFatal() {
u32 x = FontManager::GetX();
FontManager::PrintFormat("%s:", Aarch64GprNames[i]);
FontManager::SetPosition(x + 47, FontManager::GetY());
FontManager::PrintMonospaceU64(this->ctx->cpu_ctx.aarch64_ctx.x[i]);
if (this->ctx->has_gprs[i]) {
FontManager::PrintMonospaceU64(this->ctx->cpu_ctx.aarch64_ctx.x[i]);
} else {
FontManager::PrintMonospaceBlank(16);
}
FontManager::Print(" ");
x = FontManager::GetX();
FontManager::PrintFormat("%s:", Aarch64GprNames[i + (NumAarch64Gprs / 2)]);
FontManager::SetPosition(x + 47, FontManager::GetY());
FontManager::PrintMonospaceU64(this->ctx->cpu_ctx.aarch64_ctx.x[i + (NumAarch64Gprs / 2)]);
if (this->ctx->has_gprs[i + (NumAarch64Gprs / 2)]) {
FontManager::PrintMonospaceU64(this->ctx->cpu_ctx.aarch64_ctx.x[i + (NumAarch64Gprs / 2)]);
} else {
FontManager::PrintMonospaceBlank(16);
}
if (i == (NumAarch64Gprs / 2) - 1) {
FontManager::Print(" ");

View file

@ -19,6 +19,7 @@
#include "fatal_event_manager.hpp"
#include "fatal_task.hpp"
#include "fatal_config.hpp"
#include "fatal_debug.hpp"
static bool g_thrown = false;
@ -34,17 +35,25 @@ static Result SetThrown() {
Result ThrowFatalForSelf(u32 error) {
u64 pid = 0;
FatalCpuContext ctx = {0};
svcGetProcessId(&pid, CUR_PROCESS_HANDLE);
return ThrowFatalImpl(error, pid, FatalType_ErrorScreen, &ctx);
return ThrowFatalImpl(error, pid, FatalType_ErrorScreen, nullptr);
}
Result ThrowFatalImpl(u32 error, u64 pid, FatalType policy, FatalCpuContext *cpu_ctx) {
Result rc = 0;
FatalThrowContext ctx;
ctx.error_code = error;
ctx.cpu_ctx = *cpu_ctx;
if (cpu_ctx != nullptr) {
ctx.cpu_ctx = *cpu_ctx;
/* Assume if we're provided a context that it's complete. */
for (u32 i = 0; i < NumAarch64Gprs; i++) {
ctx.has_gprs[i] = true;
}
} else {
std::memset(&ctx.cpu_ctx, 0, sizeof(ctx.cpu_ctx));
cpu_ctx = &ctx.cpu_ctx;
}
/* Get config. */
const FatalConfig *config = GetFatalConfig();
@ -59,6 +68,13 @@ Result ThrowFatalImpl(u32 error, u64 pid, FatalType policy, FatalCpuContext *cpu
title_id = cpu_ctx->aarch64_ctx.afsr0;
}
/* Atmosphere extension: automatic debug info collection. */
if (GetRuntimeFirmwareVersion() >= FirmwareVersion_200 && !ctx.is_creport) {
if ((cpu_ctx->is_aarch32 && cpu_ctx->aarch32_ctx.stack_trace_size == 0) || (!cpu_ctx->is_aarch32 && cpu_ctx->aarch32_ctx.stack_trace_size == 0)) {
TryCollectDebugInformation(&ctx, pid);
}
}
switch (policy) {
case FatalType_ErrorReport:
/* TODO: Don't write an error report. */

View file

@ -99,6 +99,7 @@ struct FatalCpuContext {
struct FatalThrowContext {
u32 error_code;
bool is_creport;
bool has_gprs[NumAarch64Gprs];
FatalCpuContext cpu_ctx;
};
@ -129,6 +130,7 @@ static constexpr const char *Aarch64GprNames[NumAarch64Gprs] = {
u8"X18",
u8"X19",
u8"X20",
u8"X21",
u8"X22",
u8"X23",
u8"X24",
@ -139,7 +141,6 @@ static constexpr const char *Aarch64GprNames[NumAarch64Gprs] = {
u8"FP",
u8"LR",
u8"SP",
u8"PC",
};
static constexpr const char *Aarch32GprNames[NumAarch32Gprs] = {

View file

@ -21,19 +21,16 @@
#include "fatal_task.hpp"
Result UserService::ThrowFatal(u32 error, PidDescriptor pid_desc) {
FatalCpuContext ctx = {0};
return ThrowFatalImpl(error, pid_desc.pid, FatalType_ErrorReportAndErrorScreen, &ctx);
return ThrowFatalImpl(error, pid_desc.pid, FatalType_ErrorReportAndErrorScreen, nullptr);
}
Result UserService::ThrowFatalWithPolicy(u32 error, PidDescriptor pid_desc, FatalType policy) {
FatalCpuContext ctx = {0};
return ThrowFatalImpl(error, pid_desc.pid, policy, &ctx);
return ThrowFatalImpl(error, pid_desc.pid, policy, nullptr);
}
Result UserService::ThrowFatalWithCpuContext(u32 error, PidDescriptor pid_desc, FatalType policy, InBuffer<u8> _ctx) {
if (_ctx.num_elements < sizeof(FatalCpuContext)) {
FatalCpuContext ctx = {0};
return ThrowFatalImpl(error, pid_desc.pid, policy, &ctx);
return ThrowFatalImpl(error, pid_desc.pid, policy, nullptr);
} else {
return ThrowFatalImpl(error, pid_desc.pid, policy, reinterpret_cast<FatalCpuContext *>(_ctx.buffer));
}