2018-09-07 15:00:13 +00:00
|
|
|
/*
|
|
|
|
* 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/>.
|
|
|
|
*/
|
|
|
|
|
2018-06-25 16:22:37 +00:00
|
|
|
#include <cstdio>
|
2018-06-25 16:25:14 +00:00
|
|
|
#include <cstring>
|
2018-06-25 16:22:37 +00:00
|
|
|
#include <sys/stat.h>
|
|
|
|
#include <sys/types.h>
|
2018-06-25 06:42:26 +00:00
|
|
|
#include <switch.h>
|
2019-01-20 00:32:33 +00:00
|
|
|
|
2018-06-25 06:42:26 +00:00
|
|
|
#include "creport_crash_report.hpp"
|
2018-06-25 07:45:25 +00:00
|
|
|
#include "creport_debug_types.hpp"
|
2019-01-20 00:32:33 +00:00
|
|
|
|
2018-06-25 06:42:26 +00:00
|
|
|
void CrashReport::BuildReport(u64 pid, bool has_extra_info) {
|
|
|
|
this->has_extra_info = has_extra_info;
|
|
|
|
if (OpenProcess(pid)) {
|
2018-06-25 07:45:25 +00:00
|
|
|
ProcessExceptions();
|
2018-11-12 03:52:19 +00:00
|
|
|
this->code_list.ReadCodeRegionsFromThreadInfo(this->debug_handle, &this->crashed_thread_info);
|
2018-08-12 02:02:12 +00:00
|
|
|
this->thread_list.ReadThreadsFromProcess(this->debug_handle, Is64Bit());
|
|
|
|
this->crashed_thread_info.SetCodeList(&this->code_list);
|
2018-07-28 03:34:09 +00:00
|
|
|
this->thread_list.SetCodeList(&this->code_list);
|
2018-06-25 09:40:32 +00:00
|
|
|
|
|
|
|
if (IsApplication()) {
|
|
|
|
ProcessDyingMessage();
|
|
|
|
}
|
|
|
|
|
2018-11-12 03:52:19 +00:00
|
|
|
/* Real creport only does this if application, but there's no reason not to do it all the time. */
|
|
|
|
for (u32 i = 0; i < this->thread_list.GetThreadCount(); i++) {
|
|
|
|
this->code_list.ReadCodeRegionsFromThreadInfo(this->debug_handle, this->thread_list.GetThreadInfo(i));
|
|
|
|
}
|
|
|
|
|
2018-06-25 09:40:32 +00:00
|
|
|
/* Real creport builds the report here. We do it later. */
|
2018-06-25 07:45:25 +00:00
|
|
|
|
2018-06-25 06:42:26 +00:00
|
|
|
Close();
|
|
|
|
}
|
2018-06-25 07:45:25 +00:00
|
|
|
}
|
2019-01-20 00:32:33 +00:00
|
|
|
|
2018-11-14 04:22:54 +00:00
|
|
|
FatalContext *CrashReport::GetFatalContext() {
|
|
|
|
FatalContext *ctx = new FatalContext;
|
|
|
|
*ctx = (FatalContext){0};
|
|
|
|
|
|
|
|
ctx->is_aarch32 = false;
|
|
|
|
ctx->type = static_cast<u32>(this->exception_info.type);
|
|
|
|
|
|
|
|
for (size_t i = 0; i < 29; i++) {
|
|
|
|
ctx->aarch64_ctx.x[i] = this->crashed_thread_info.context.cpu_gprs[i].x;
|
|
|
|
}
|
|
|
|
ctx->aarch64_ctx.fp = this->crashed_thread_info.context.fp;
|
|
|
|
ctx->aarch64_ctx.lr = this->crashed_thread_info.context.lr;
|
|
|
|
ctx->aarch64_ctx.pc = this->crashed_thread_info.context.pc.x;
|
|
|
|
|
|
|
|
ctx->aarch64_ctx.stack_trace_size = this->crashed_thread_info.stack_trace_size;
|
|
|
|
for (size_t i = 0; i < ctx->aarch64_ctx.stack_trace_size; i++) {
|
|
|
|
ctx->aarch64_ctx.stack_trace[i] = this->crashed_thread_info.stack_trace[i];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this->code_list.code_count) {
|
|
|
|
ctx->aarch64_ctx.start_address = this->code_list.code_infos[0].start_address;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* For ams fatal... */
|
|
|
|
ctx->aarch64_ctx.afsr0 = this->process_info.title_id;
|
|
|
|
|
|
|
|
return ctx;
|
|
|
|
}
|
2019-01-20 00:32:33 +00:00
|
|
|
|
2018-06-25 07:45:25 +00:00
|
|
|
void CrashReport::ProcessExceptions() {
|
|
|
|
if (!IsOpen()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
DebugEventInfo d;
|
|
|
|
while (R_SUCCEEDED(svcGetDebugEvent((u8 *)&d, this->debug_handle))) {
|
|
|
|
switch (d.type) {
|
|
|
|
case DebugEventType::AttachProcess:
|
|
|
|
HandleAttachProcess(d);
|
|
|
|
break;
|
|
|
|
case DebugEventType::Exception:
|
|
|
|
HandleException(d);
|
|
|
|
break;
|
|
|
|
case DebugEventType::AttachThread:
|
|
|
|
case DebugEventType::ExitProcess:
|
|
|
|
case DebugEventType::ExitThread:
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-01-20 00:32:33 +00:00
|
|
|
|
2018-06-25 07:45:25 +00:00
|
|
|
void CrashReport::HandleAttachProcess(DebugEventInfo &d) {
|
|
|
|
this->process_info = d.info.attach_process;
|
|
|
|
if (kernelAbove500() && IsApplication()) {
|
|
|
|
/* Parse out user data. */
|
|
|
|
u64 address = this->process_info.user_exception_context_address;
|
|
|
|
u64 userdata_address = 0;
|
|
|
|
u64 userdata_size = 0;
|
|
|
|
|
2018-06-25 08:18:26 +00:00
|
|
|
if (!IsAddressReadable(address, sizeof(userdata_address) + sizeof(userdata_size))) {
|
2018-06-25 07:45:25 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Read userdata address. */
|
|
|
|
if (R_FAILED(svcReadDebugProcessMemory(&userdata_address, this->debug_handle, address, sizeof(userdata_address)))) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Validate userdata address. */
|
|
|
|
if (userdata_address == 0 || userdata_address & 0xFFF) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Read userdata size. */
|
|
|
|
if (R_FAILED(svcReadDebugProcessMemory(&userdata_size, this->debug_handle, address + sizeof(userdata_address), sizeof(userdata_size)))) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Cap userdata size. */
|
2018-06-25 09:40:32 +00:00
|
|
|
if (userdata_size > sizeof(this->dying_message)) {
|
|
|
|
userdata_size = sizeof(this->dying_message);
|
2018-06-25 07:45:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Assign. */
|
2018-06-25 09:40:32 +00:00
|
|
|
this->dying_message_address = userdata_address;
|
|
|
|
this->dying_message_size = userdata_size;
|
2018-06-25 07:45:25 +00:00
|
|
|
}
|
|
|
|
}
|
2019-01-20 00:32:33 +00:00
|
|
|
|
2018-06-25 07:45:25 +00:00
|
|
|
void CrashReport::HandleException(DebugEventInfo &d) {
|
|
|
|
switch (d.info.exception.type) {
|
|
|
|
case DebugExceptionType::UndefinedInstruction:
|
2019-03-28 21:23:34 +00:00
|
|
|
this->result = ResultCreportUndefinedInstruction;
|
2018-06-25 08:18:26 +00:00
|
|
|
break;
|
2018-06-25 07:45:25 +00:00
|
|
|
case DebugExceptionType::InstructionAbort:
|
2019-03-28 21:23:34 +00:00
|
|
|
this->result = ResultCreportInstructionAbort;
|
2018-06-26 06:44:58 +00:00
|
|
|
d.info.exception.specific.raw = 0;
|
2018-06-25 08:18:26 +00:00
|
|
|
break;
|
2018-06-25 07:45:25 +00:00
|
|
|
case DebugExceptionType::DataAbort:
|
2019-03-28 21:23:34 +00:00
|
|
|
this->result = ResultCreportDataAbort;
|
2018-06-25 08:18:26 +00:00
|
|
|
break;
|
2018-06-25 07:45:25 +00:00
|
|
|
case DebugExceptionType::AlignmentFault:
|
2019-03-28 21:23:34 +00:00
|
|
|
this->result = ResultCreportAlignmentFault;
|
2018-06-25 08:18:26 +00:00
|
|
|
break;
|
2018-06-25 07:45:25 +00:00
|
|
|
case DebugExceptionType::UserBreak:
|
2019-03-28 21:23:34 +00:00
|
|
|
this->result = ResultCreportUserBreak;
|
2018-06-25 08:18:26 +00:00
|
|
|
/* Try to parse out the user break result. */
|
2019-01-20 00:23:23 +00:00
|
|
|
if (kernelAbove500()) {
|
2019-01-20 01:28:26 +00:00
|
|
|
Result user_result = 0;
|
2019-01-20 00:23:23 +00:00
|
|
|
if (IsAddressReadable(d.info.exception.specific.user_break.address, sizeof(user_result))) {
|
|
|
|
svcReadDebugProcessMemory(&user_result, this->debug_handle, d.info.exception.specific.user_break.address, sizeof(user_result));
|
|
|
|
}
|
|
|
|
/* Only copy over the user result if it gives us information (as by default nnSdk uses the success code, which is confusing). */
|
|
|
|
if (R_FAILED(user_result)) {
|
|
|
|
this->result = user_result;
|
|
|
|
}
|
2018-06-25 08:18:26 +00:00
|
|
|
}
|
|
|
|
break;
|
2018-06-25 07:45:25 +00:00
|
|
|
case DebugExceptionType::BadSvc:
|
2019-03-28 21:23:34 +00:00
|
|
|
this->result = ResultCreportBadSvc;
|
2018-06-25 08:18:26 +00:00
|
|
|
break;
|
2019-03-28 21:23:34 +00:00
|
|
|
case DebugExceptionType::SystemMemoryError:
|
|
|
|
this->result = ResultCreportSystemMemoryError;
|
2018-06-26 06:44:58 +00:00
|
|
|
d.info.exception.specific.raw = 0;
|
2018-06-25 07:45:25 +00:00
|
|
|
break;
|
|
|
|
case DebugExceptionType::DebuggerAttached:
|
|
|
|
case DebugExceptionType::BreakPoint:
|
|
|
|
case DebugExceptionType::DebuggerBreak:
|
|
|
|
default:
|
2018-06-25 08:18:26 +00:00
|
|
|
return;
|
|
|
|
}
|
2018-06-26 06:44:58 +00:00
|
|
|
this->exception_info = d.info.exception;
|
2018-06-25 09:04:17 +00:00
|
|
|
/* Parse crashing thread info. */
|
|
|
|
this->crashed_thread_info.ReadFromProcess(this->debug_handle, d.thread_id, Is64Bit());
|
2018-06-25 08:18:26 +00:00
|
|
|
}
|
2019-01-20 00:32:33 +00:00
|
|
|
|
2018-06-25 09:40:32 +00:00
|
|
|
void CrashReport::ProcessDyingMessage() {
|
|
|
|
/* Dying message is only stored starting in 5.0.0. */
|
|
|
|
if (!kernelAbove500()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Validate the message address/size. */
|
|
|
|
if (this->dying_message_address == 0 || this->dying_message_address & 0xFFF) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this->dying_message_size > sizeof(this->dying_message)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Validate that the report isn't garbage. */
|
|
|
|
if (!IsOpen() || !WasSuccessful()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!IsAddressReadable(this->dying_message_address, this->dying_message_size)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
svcReadDebugProcessMemory(this->dying_message, this->debug_handle, this->dying_message_address, this->dying_message_size);
|
|
|
|
}
|
2019-01-20 00:32:33 +00:00
|
|
|
|
2018-06-25 08:18:26 +00:00
|
|
|
bool CrashReport::IsAddressReadable(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, this->debug_handle, address))) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Must be read or read-write */
|
|
|
|
if ((o_mi->perm | Perm_W) != Perm_Rw) {
|
|
|
|
return false;
|
2018-06-25 07:45:25 +00:00
|
|
|
}
|
2018-06-25 08:18:26 +00:00
|
|
|
|
|
|
|
/* 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;
|
|
|
|
}
|
2019-01-20 00:35:40 +00:00
|
|
|
|
2018-06-25 08:18:26 +00:00
|
|
|
return true;
|
2018-06-25 16:22:37 +00:00
|
|
|
}
|
2019-01-20 00:32:33 +00:00
|
|
|
|
2018-06-25 16:25:14 +00:00
|
|
|
bool CrashReport::GetCurrentTime(u64 *out) {
|
2018-06-25 16:22:37 +00:00
|
|
|
*out = 0;
|
|
|
|
|
|
|
|
/* Verify that pcv isn't dead. */
|
|
|
|
{
|
|
|
|
Handle dummy;
|
|
|
|
if (R_SUCCEEDED(smRegisterService(&dummy, "time:s", false, 0x20))) {
|
|
|
|
svcCloseHandle(dummy);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Try to get the current time. */
|
|
|
|
bool success = false;
|
|
|
|
if (R_SUCCEEDED(timeInitialize())) {
|
|
|
|
if (R_SUCCEEDED(timeGetCurrentTime(TimeType_LocalSystemClock, out))) {
|
|
|
|
success = true;
|
|
|
|
}
|
|
|
|
timeExit();
|
|
|
|
}
|
|
|
|
return success;
|
|
|
|
}
|
2019-01-20 00:32:33 +00:00
|
|
|
|
2018-06-26 06:44:58 +00:00
|
|
|
void CrashReport::EnsureReportDirectories() {
|
|
|
|
char path[FS_MAX_PATH];
|
|
|
|
strcpy(path, "sdmc:/atmosphere");
|
|
|
|
mkdir(path, S_IRWXU);
|
2018-07-29 23:48:33 +00:00
|
|
|
strcat(path, "/crash_reports");
|
2018-06-26 06:44:58 +00:00
|
|
|
mkdir(path, S_IRWXU);
|
|
|
|
strcat(path, "/dumps");
|
|
|
|
mkdir(path, S_IRWXU);
|
|
|
|
}
|
2019-01-20 00:32:33 +00:00
|
|
|
|
2018-06-26 06:44:58 +00:00
|
|
|
void CrashReport::SaveReport() {
|
2018-11-14 22:13:31 +00:00
|
|
|
/* Save the report to the SD card. */
|
2018-06-26 06:44:58 +00:00
|
|
|
char report_path[FS_MAX_PATH];
|
|
|
|
|
|
|
|
/* Ensure path exists. */
|
|
|
|
EnsureReportDirectories();
|
|
|
|
|
|
|
|
/* Get a timestamp. */
|
|
|
|
u64 timestamp;
|
|
|
|
if (!GetCurrentTime(×tamp)) {
|
|
|
|
timestamp = svcGetSystemTick();
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Open report file. */
|
2018-07-29 23:54:15 +00:00
|
|
|
snprintf(report_path, sizeof(report_path) - 1, "sdmc:/atmosphere/crash_reports/%011lu_%016lx.log", timestamp, process_info.title_id);
|
2018-06-26 06:44:58 +00:00
|
|
|
FILE *f_report = fopen(report_path, "w");
|
|
|
|
if (f_report == NULL) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this->SaveToFile(f_report);
|
|
|
|
fclose(f_report);
|
|
|
|
|
|
|
|
/* Dump threads. */
|
2018-07-29 23:54:15 +00:00
|
|
|
snprintf(report_path, sizeof(report_path) - 1, "sdmc:/atmosphere/crash_reports/dumps/%011lu_%016lx_thread_info.bin", timestamp, process_info.title_id);
|
2018-06-26 06:44:58 +00:00
|
|
|
f_report = fopen(report_path, "wb");
|
|
|
|
this->thread_list.DumpBinary(f_report, this->crashed_thread_info.GetId());
|
|
|
|
fclose(f_report);
|
|
|
|
}
|
2019-01-20 00:32:33 +00:00
|
|
|
|
2018-06-26 06:44:58 +00:00
|
|
|
void CrashReport::SaveToFile(FILE *f_report) {
|
|
|
|
char buf[0x10] = {0};
|
2018-11-12 03:52:19 +00:00
|
|
|
fprintf(f_report, "Atmosphère Crash Report (v1.2):\n");
|
2018-06-26 06:44:58 +00:00
|
|
|
fprintf(f_report, "Result: 0x%X (2%03d-%04d)\n\n", this->result, R_MODULE(this->result), R_DESCRIPTION(this->result));
|
|
|
|
|
|
|
|
/* Process Info. */
|
|
|
|
memcpy(buf, this->process_info.name, sizeof(this->process_info.name));
|
|
|
|
fprintf(f_report, "Process Info:\n");
|
|
|
|
fprintf(f_report, " Process Name: %s\n", buf);
|
|
|
|
fprintf(f_report, " Title ID: %016lx\n", this->process_info.title_id);
|
|
|
|
fprintf(f_report, " Process ID: %016lx\n", this->process_info.process_id);
|
|
|
|
fprintf(f_report, " Process Flags: %08x\n", this->process_info.flags);
|
|
|
|
if (kernelAbove500()) {
|
2018-07-28 03:34:09 +00:00
|
|
|
fprintf(f_report, " User Exception Address: %s\n", this->code_list.GetFormattedAddressString(this->process_info.user_exception_context_address));
|
2018-06-26 06:44:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fprintf(f_report, "Exception Info:\n");
|
|
|
|
fprintf(f_report, " Type: %s\n", GetDebugExceptionTypeStr(this->exception_info.type));
|
2018-07-28 03:34:09 +00:00
|
|
|
fprintf(f_report, " Address: %s\n", this->code_list.GetFormattedAddressString(this->exception_info.address));
|
2018-06-26 06:44:58 +00:00
|
|
|
switch (this->exception_info.type) {
|
|
|
|
case DebugExceptionType::UndefinedInstruction:
|
|
|
|
fprintf(f_report, " Opcode: %08x\n", this->exception_info.specific.undefined_instruction.insn);
|
|
|
|
break;
|
|
|
|
case DebugExceptionType::DataAbort:
|
|
|
|
case DebugExceptionType::AlignmentFault:
|
|
|
|
if (this->exception_info.specific.raw != this->exception_info.address) {
|
2018-07-28 03:34:09 +00:00
|
|
|
fprintf(f_report, " Fault Address: %s\n", this->code_list.GetFormattedAddressString(this->exception_info.specific.raw));
|
2018-06-26 06:44:58 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case DebugExceptionType::BadSvc:
|
|
|
|
fprintf(f_report, " Svc Id: 0x%02x\n", this->exception_info.specific.bad_svc.id);
|
|
|
|
break;
|
2018-08-12 02:16:29 +00:00
|
|
|
case DebugExceptionType::UserBreak:
|
|
|
|
fprintf(f_report, " Break Reason: 0x%lx\n", this->exception_info.specific.user_break.break_reason);
|
|
|
|
fprintf(f_report, " Break Address: %s\n", this->code_list.GetFormattedAddressString(this->exception_info.specific.user_break.address));
|
|
|
|
fprintf(f_report, " Break Size: 0x%lx\n", this->exception_info.specific.user_break.size);
|
|
|
|
break;
|
2018-06-26 06:44:58 +00:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
fprintf(f_report, "Crashed Thread Info:\n");
|
|
|
|
this->crashed_thread_info.SaveToFile(f_report);
|
|
|
|
|
|
|
|
if (kernelAbove500()) {
|
2018-06-26 06:51:53 +00:00
|
|
|
if (this->dying_message_size) {
|
|
|
|
fprintf(f_report, "Dying Message Info:\n");
|
2018-07-28 03:34:09 +00:00
|
|
|
fprintf(f_report, " Address: 0x%s\n", this->code_list.GetFormattedAddressString(this->dying_message_address));
|
2018-06-26 06:51:53 +00:00
|
|
|
fprintf(f_report, " Size: 0x%016lx\n", this->dying_message_size);
|
|
|
|
CrashReport::Memdump(f_report, " Dying Message: ", this->dying_message, this->dying_message_size);
|
|
|
|
}
|
2018-06-26 06:44:58 +00:00
|
|
|
}
|
2018-08-12 02:02:12 +00:00
|
|
|
fprintf(f_report, "Code Region Info:\n");
|
|
|
|
this->code_list.SaveToFile(f_report);
|
|
|
|
fprintf(f_report, "\nThread Report:\n");
|
|
|
|
this->thread_list.SaveToFile(f_report);
|
2018-06-26 06:44:58 +00:00
|
|
|
}
|
2019-01-20 00:32:33 +00:00
|
|
|
|
2018-06-26 06:44:58 +00:00
|
|
|
/* Lifted from hactool. */
|
|
|
|
void CrashReport::Memdump(FILE *f, const char *prefix, const void *data, size_t size) {
|
|
|
|
uint8_t *p = (uint8_t *)data;
|
2019-01-20 00:35:40 +00:00
|
|
|
|
2018-06-26 06:44:58 +00:00
|
|
|
unsigned int prefix_len = strlen(prefix);
|
|
|
|
size_t offset = 0;
|
|
|
|
int first = 1;
|
2019-01-20 00:35:40 +00:00
|
|
|
|
2018-06-26 06:44:58 +00:00
|
|
|
while (size) {
|
|
|
|
unsigned int max = 32;
|
2019-01-20 00:35:40 +00:00
|
|
|
|
2018-06-26 06:44:58 +00:00
|
|
|
if (max > size) {
|
|
|
|
max = size;
|
|
|
|
}
|
2019-01-20 00:35:40 +00:00
|
|
|
|
2018-06-26 06:44:58 +00:00
|
|
|
if (first) {
|
|
|
|
fprintf(f, "%s", prefix);
|
|
|
|
first = 0;
|
|
|
|
} else {
|
|
|
|
fprintf(f, "%*s", prefix_len, "");
|
|
|
|
}
|
2019-01-20 00:35:40 +00:00
|
|
|
|
2018-06-26 06:44:58 +00:00
|
|
|
for (unsigned int i = 0; i < max; i++) {
|
|
|
|
fprintf(f, "%02X", p[offset++]);
|
|
|
|
}
|
2019-01-20 00:35:40 +00:00
|
|
|
|
2018-06-26 06:44:58 +00:00
|
|
|
fprintf(f, "\n");
|
2019-01-20 00:35:40 +00:00
|
|
|
|
2018-06-26 06:44:58 +00:00
|
|
|
size -= max;
|
|
|
|
}
|
|
|
|
}
|