mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-11-05 13:20:11 +00:00
feat(libc): print the text that registers point at in Windows crash reports
Unreal titles end in their own libc fatal path with the UTF-16 LowLevelFatalError message in a register, and the crash report shows only the first 64 bytes of it as hex words, so the message is lost (#4238: Demon Slayer PPSA03490, Stray PPSA02100). For every general register that points at readable memory starting with at least four printable ASCII characters, as UTF-16LE or as single bytes, the report now prints the text up to a NUL, the first other character, 512 characters or the end of readable memory (each page is checked with VirtualQuery before it is read). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e9a3343a22
commit
b263cbbb1b
6 files changed
+191
No files matched your search
@@ -281,6 +281,25 @@ if(BUILD_TESTING)
|
||||
PASS_REGULAR_EXPRESSION "C\\+\\+ exception St9bad_alloc raised on thread [0-9]+ cannot be delivered: 0x[0-9a-f]+ undeliverable_host_exception_tests\\.exe\\+0x[0-9a-f]+ has no unwind data")
|
||||
endif()
|
||||
|
||||
add_test_executable(crash_text_tests tests/CrashText.cpp)
|
||||
target_include_directories(crash_text_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
if(MINGW)
|
||||
target_compile_options(crash_text_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
endif()
|
||||
add_test(NAME crash_text COMMAND crash_text_tests)
|
||||
|
||||
if(WIN32)
|
||||
add_test_executable(windows_crash_text_tests tests/WindowsCrashText.cpp)
|
||||
add_dependencies(windows_crash_text_tests libc)
|
||||
if(MINGW)
|
||||
target_compile_options(windows_crash_text_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
endif()
|
||||
add_test(NAME windows_crash_text COMMAND windows_crash_text_tests $<TARGET_FILE:libc>)
|
||||
set_tests_properties(windows_crash_text PROPERTIES TIMEOUT 20
|
||||
PASS_REGULAR_EXPRESSION "faulting[^~]*FATAL: unhandled exception 0xc0000005[^~]* \\[r12\\] \"allocation failed in the heap\"[\r\n]+ \\[r13\\] u\"LowLevelFatalError \\[File:Unknown\\] \\[Line: 42\\] Out of memory\"[\r\n]+ \\[r14\\] \"PAGE END\"[.][.][.]"
|
||||
FAIL_REGULAR_EXPRESSION "\\[rbx\\] u?\"|did not fault")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_voice_port_attr_tests tests/GuestVoicePortAttr.cpp)
|
||||
target_include_directories(guest_voice_port_attr_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_voice_port_attr_tests PRIVATE libSceVoice libc)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CRASHTEXT_HPP
|
||||
#define CORE_LIBS_PRX_LIBC_INCLUDE_CRASHTEXT_HPP
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace crash_text {
|
||||
|
||||
constexpr std::size_t MaximumCharacters = 512;
|
||||
constexpr std::size_t MinimumCharacters = 4;
|
||||
constexpr std::size_t MaximumBytes = 2 * (MaximumCharacters + 1);
|
||||
|
||||
inline std::uint16_t Unit(const std::uint8_t* bytes, std::size_t index, std::size_t width) {
|
||||
if (width == 1) return bytes[index];
|
||||
return static_cast<std::uint16_t>(bytes[2 * index] | bytes[2 * index + 1] << 8);
|
||||
}
|
||||
|
||||
inline bool IsText(std::uint16_t unit) {
|
||||
return (unit >= 0x20 && unit < 0x7f) || unit == '\t' || unit == '\n' || unit == '\r';
|
||||
}
|
||||
|
||||
inline bool StartsWithText(const std::uint8_t* bytes, std::size_t size, std::size_t width) {
|
||||
if (size < MinimumCharacters * width) return false;
|
||||
for (std::size_t index = 0; index < MinimumCharacters; ++index) {
|
||||
if (!IsText(Unit(bytes, index, width))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool Format(const std::uint8_t* bytes, std::size_t size, char* out, std::size_t capacity) {
|
||||
const std::size_t width = StartsWithText(bytes, size, 2) ? 2 : StartsWithText(bytes, size, 1) ? 1 : 0;
|
||||
if (width == 0 || capacity < 8) return false;
|
||||
std::size_t length = 0;
|
||||
if (width == 2) out[length++] = 'u';
|
||||
out[length++] = '"';
|
||||
bool terminated = false;
|
||||
for (std::size_t index = 0; (index + 1) * width <= size; ++index) {
|
||||
const std::uint16_t unit = Unit(bytes, index, width);
|
||||
if (unit == 0) {
|
||||
terminated = true;
|
||||
break;
|
||||
}
|
||||
if (index == MaximumCharacters || !IsText(unit)) break;
|
||||
const char escape = unit == '\n' ? 'n' : unit == '\r' ? 'r' : unit == '\t' ? 't' : unit == '"' || unit == '\\' ? static_cast<char>(unit) : '\0';
|
||||
if (length + (escape != '\0' ? 2 : 1) + 5 > capacity) break;
|
||||
if (escape != '\0') out[length++] = '\\';
|
||||
out[length++] = escape != '\0' ? escape : static_cast<char>(unit);
|
||||
}
|
||||
out[length++] = '"';
|
||||
if (!terminated) {
|
||||
for (int dot = 0; dot < 3; ++dot) out[length++] = '.';
|
||||
}
|
||||
out[length] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "prx/libc/include/FailureReport.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include "prx/libc/include/CrashText.hpp"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -355,6 +356,15 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) {
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
const std::pair<const char*, std::uint64_t> registers[] = {{"rax", context->Rax}, {"rbx", context->Rbx}, {"rcx", context->Rcx}, {"rdx", context->Rdx}, {"rsi", context->Rsi}, {"rdi", context->Rdi}, {"rbp", context->Rbp}, {"r8", context->R8}, {"r9", context->R9}, {"r10", context->R10}, {"r11", context->R11}, {"r12", context->R12}, {"r13", context->R13}, {"r14", context->R14}, {"r15", context->R15}};
|
||||
static std::uint8_t bytes[crash_text::MaximumBytes];
|
||||
static char text[960];
|
||||
for (const auto& [name, value] : registers) {
|
||||
if (value < 0x10000) continue;
|
||||
if (crash_text::Format(bytes, ReadCode(value, bytes, sizeof(bytes)), text, sizeof(text))) Report(" [%s] %s\n", name, text);
|
||||
}
|
||||
}
|
||||
Report(" frame pointer chain:\n");
|
||||
std::uint64_t frame = context->Rbp;
|
||||
for (int depth = 0; depth < 32 && frame != 0 && (frame & 7) == 0 && IsReadable(frame) && IsReadable(frame + 8); ++depth) {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "prx/libc/include/CrashText.hpp"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
std::vector<std::uint8_t> Wide(const std::u16string& text) {
|
||||
std::vector<std::uint8_t> bytes;
|
||||
for (const char16_t unit : text) {
|
||||
bytes.push_back(static_cast<std::uint8_t>(unit & 0xff));
|
||||
bytes.push_back(static_cast<std::uint8_t>(unit >> 8));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> Narrow(const std::string& text) {
|
||||
return std::vector<std::uint8_t>(text.begin(), text.end());
|
||||
}
|
||||
|
||||
void Expect(const char* name, const std::vector<std::uint8_t>& bytes, const std::string& expected, std::size_t capacity = 960) {
|
||||
std::vector<char> out(capacity, '#');
|
||||
const bool formatted = crash_text::Format(bytes.data(), bytes.size(), out.data(), capacity);
|
||||
const std::string actual = formatted ? std::string(out.data()) : std::string("<none>");
|
||||
if (actual == expected && actual.size() < capacity) return;
|
||||
std::fprintf(stderr, "%s: expected %s, got %s\n", name, expected.c_str(), actual.c_str());
|
||||
++failures;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
using namespace std::string_literals;
|
||||
Expect("terminated UTF-16", Wide(u"LowLevelFatalError [File:Unknown]\0tail"s), "u\"LowLevelFatalError [File:Unknown]\"");
|
||||
Expect("terminated ASCII with escapes", Narrow("PS5 Out of Memory:\n\t\"a\\b\"\r\0tail"s), "\"PS5 Out of Memory:\\n\\t\\\"a\\\\b\\\"\\r\"");
|
||||
Expect("UTF-16 up to the end of readable memory", Wide(u"Ran out of memory"), "u\"Ran out of memory\"...");
|
||||
Expect("ASCII up to the end of readable memory", Narrow("PAGE END"), "\"PAGE END\"...");
|
||||
Expect("UTF-16 before a non-ASCII unit", Wide(u"Memory\u00e9 more"), "u\"Memory\"...");
|
||||
Expect("short ASCII", Narrow("abc\0"s), "<none>");
|
||||
Expect("short UTF-16", Wide(u"abc\0"s), "<none>");
|
||||
Expect("three readable bytes", Narrow("abc"), "<none>");
|
||||
Expect("pointer value", std::vector<std::uint8_t>{0x08, 0xd4, 0x8e, 0x5b, 0xf6, 0x7f, 0x00, 0x00}, "<none>");
|
||||
Expect("control byte first", Narrow("\x01text\0"s), "<none>");
|
||||
Expect("character limit", Wide(std::u16string(600, u'A') + u'\0'), "u\"" + std::string(crash_text::MaximumCharacters, 'A') + "\"...");
|
||||
Expect("NUL right at the character limit", Wide(std::u16string(crash_text::MaximumCharacters, u'B') + u'\0'), "u\"" + std::string(crash_text::MaximumCharacters, 'B') + "\"");
|
||||
Expect("output capacity", Narrow(std::string(100, 'x')), "\"" + std::string(10, 'x') + "\"...", 16);
|
||||
Expect("escape at the output capacity", Narrow("abcdefghi\n" + std::string(20, 'z')), "\"abcdefghi\"...", 16);
|
||||
Expect("too small output", Narrow("long enough text"), "<none>", 7);
|
||||
if (failures != 0) return 1;
|
||||
std::puts("crash text: all cases passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <windows.h>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2) {
|
||||
std::fprintf(stderr, "usage: %s <libc.prx>\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
|
||||
SetUnhandledExceptionFilter([](EXCEPTION_POINTERS*) -> LONG {
|
||||
TerminateProcess(GetCurrentProcess(), 3);
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
});
|
||||
if (LoadLibraryExA(argv[1], nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) == nullptr) {
|
||||
std::fprintf(stderr, "%s: LoadLibraryEx failed with error %lu\n", argv[1], GetLastError());
|
||||
return 1;
|
||||
}
|
||||
static const char narrow[] = "allocation failed in the heap";
|
||||
static const char16_t wide[] = u"LowLevelFatalError [File:Unknown] [Line: 42] Out of memory";
|
||||
static const std::uint8_t binary[] = {0x08, 0xd4, 0x8e, 0x5b, 0xf6, 0x7f, 0x00, 0x00};
|
||||
auto* pages = static_cast<char*>(VirtualAlloc(nullptr, 0x2000, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
DWORD previous = 0;
|
||||
if (pages == nullptr || !VirtualProtect(pages + 0x1000, 0x1000, PAGE_NOACCESS, &previous)) {
|
||||
std::fprintf(stderr, "cannot prepare the page edge: error %lu\n", GetLastError());
|
||||
return 1;
|
||||
}
|
||||
char* const edge = pages + 0x1000 - 8;
|
||||
std::memcpy(edge, "PAGE END", 8);
|
||||
std::fputs("faulting\n", stderr);
|
||||
std::fflush(stderr);
|
||||
const std::uintptr_t unmapped = 8;
|
||||
asm volatile(
|
||||
"movq %0, %%r12\n\t"
|
||||
"movq %1, %%r13\n\t"
|
||||
"movq %2, %%r14\n\t"
|
||||
"movq %3, %%rbx\n\t"
|
||||
"movq (%4), %%rax"
|
||||
:
|
||||
: "r"(narrow), "r"(wide), "r"(edge), "r"(binary), "r"(unmapped)
|
||||
: "rax", "rbx", "r12", "r13", "r14", "memory");
|
||||
std::fputs("the read of address 8 did not fault\n", stderr);
|
||||
return 1;
|
||||
}
|
||||
@@ -206,6 +206,8 @@ Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/
|
||||
|
||||
### Windows crash diagnostics
|
||||
|
||||
When a register points at readable memory that starts with at least four printable ASCII characters, either as UTF-16LE or as single bytes, the crash report prints that text after the register memory dump, as `[r13] u"..."` for UTF-16LE or `[rdi] "..."` for single bytes. The text ends at a NUL, at the first character that is not printable ASCII, a tab, a line feed or a carriage return, after 512 characters or at the end of readable memory. Tabs, line breaks, quotes and backslashes are escaped, and `...` after the closing quote means that no NUL ended the text within those limits.
|
||||
|
||||
Set `APS5_CRASH_INDIRECT_MEMORY=1` before launching the executable to include up to 64 bytes from each pointer found in the register memory dump. The report labels each block with its register, pointer offset and target address. Inaccessible, incomplete and executable targets are skipped. This additional dump is disabled when the variable is unset or empty.
|
||||
|
||||
### GPU submission rings and stall dumps
|
||||
|
||||
Reference in new issue
Block a user