yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
rom_debug_agent.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <iomanip>
5#include <set>
6#include <sstream>
7
8#include "absl/strings/str_format.h"
9#include "absl/strings/str_join.h"
10#include "absl/strings/str_split.h"
11#include "util/log.h"
12
13namespace yaze {
14namespace cli {
15namespace agent {
16
17using namespace yaze::agent;
18
19namespace {
20
21// Helper to format a 24-bit SNES address
22std::string FormatSnesAddress(uint32_t address) {
23 return absl::StrFormat("$%02X:%04X", (address >> 16) & 0xFF,
24 address & 0xFFFF);
25}
26
27// Helper to check if an opcode is a branch/jump instruction
28bool IsBranchOrJump(uint8_t opcode) {
29 // 65816 branch and jump opcodes
30 static const std::set<uint8_t> branch_jump_opcodes = {
31 0x10, // BPL
32 0x30, // BMI
33 0x50, // BVC
34 0x70, // BVS
35 0x80, // BRA
36 0x82, // BRL
37 0x90, // BCC
38 0xB0, // BCS
39 0xD0, // BNE
40 0xF0, // BEQ
41 0x4C, // JMP abs
42 0x5C, // JML long
43 0x6C, // JMP (abs)
44 0x7C, // JMP (abs,X)
45 0xDC, // JML [abs]
46 0x20, // JSR abs
47 0x22, // JSL long
48 0xFC, // JSR (abs,X)
49 };
50 return branch_jump_opcodes.count(opcode) > 0;
51}
52
53// Helper to check if an opcode is a return instruction
54bool IsReturn(uint8_t opcode) {
55 return opcode == 0x60 || // RTS
56 opcode == 0x6B || // RTL
57 opcode == 0x40; // RTI
58}
59
60// Helper to check if an opcode modifies the stack
61bool ModifiesStack(uint8_t opcode) {
62 static const std::set<uint8_t> stack_opcodes = {
63 0x48, 0x08, // PHA, PHP
64 0x68, 0x28, // PLA, PLP
65 0xDA, 0x5A, // PHX, PHY
66 0xFA, 0x7A, // PLX, PLY
67 0x8B, // PHB
68 0xAB, // PLB
69 0x0B, // PHD
70 0x2B, // PLD
71 0x4B, // PHK
72 0x62, // PER
73 0xD4, // PEI
74 0xF4, // PEA
75 };
76 return stack_opcodes.count(opcode) > 0;
77}
78
79} // namespace
80
82 : emulator_service_(emulator_service),
83 disassembler_(std::make_unique<Disassembler65816>()),
84 symbol_provider_(std::make_unique<yaze::emu::debug::SymbolProvider>()) {
85 // Initialize with default M and X flags (8-bit mode)
86 disassembler_->SetFlags(true, true);
87}
88
89absl::StatusOr<RomDebugAgent::BreakpointAnalysis>
91 const yaze::agent::BreakpointHitResponse& hit) {
92 BreakpointAnalysis analysis;
93
94 // Basic information from the hit
95 if (hit.has_breakpoint()) {
96 analysis.address = hit.breakpoint().address();
97 } else {
98 analysis.address = hit.cpu_state().pc();
99 }
100
101 // Get symbol or format address
102 if (symbol_provider_->HasSymbols()) {
103 analysis.location_description =
104 symbol_provider_->FormatAddress(analysis.address);
105 } else {
106 analysis.location_description = FormatSnesAddress(analysis.address);
107 }
108
109 // Extract registers from the hit response
110 const auto& cpu = hit.cpu_state();
111 analysis.registers["A"] = cpu.a();
112 analysis.registers["X"] = cpu.x();
113 analysis.registers["Y"] = cpu.y();
114 analysis.registers["S"] = cpu.sp();
115 analysis.registers["PC"] = cpu.pc();
116 analysis.registers["P"] = cpu.status();
117 analysis.registers["DB"] = cpu.db();
118 analysis.registers["PB"] = cpu.pb();
119
120 // Disassemble the current instruction
121 grpc::ServerContext context;
122 yaze::agent::DisassemblyRequest disasm_req;
123 disasm_req.set_start_address(analysis.address);
124 disasm_req.set_count(1);
125 yaze::agent::DisassemblyResponse disasm_resp;
126
127 auto status =
128 emulator_service_->GetDisassembly(&context, &disasm_req, &disasm_resp);
129 if (!status.ok()) {
130 return absl::InternalError("Failed to get disassembly");
131 }
132
133 if (disasm_resp.lines_size() > 0) {
134 const auto& inst = disasm_resp.lines(0);
135 analysis.disassembly = inst.mnemonic() + " " + inst.operand_str();
136
137 // Analyze instruction for AI explanation
138 auto explanation = AnalyzeInstruction(
139 analysis.address, nullptr,
140 0); // We don't have raw bytes here easily without reading memory
141 if (explanation.ok()) {
142 analysis.instruction_explanation = *explanation;
143 }
144 }
145
146 // Get context (surrounding lines)
147 analysis.context_lines = GetDisassemblyContext(analysis.address, 5, 5);
148
149 // Build call stack
150 analysis.call_stack = BuildCallStack(cpu.pc());
151
152 // Detect issues
153 auto issue = DetectIssuePattern(analysis.address, nullptr, 0);
154 if (issue) {
155 analysis.suggestions.push_back(issue->suggested_fix);
156 }
157
158 return analysis;
159}
160
161absl::StatusOr<RomDebugAgent::MemoryAnalysis> RomDebugAgent::AnalyzeMemory(
162 uint32_t address, size_t length) {
163 MemoryAnalysis analysis;
164 analysis.address = address;
165 analysis.length = length;
166
167 // Read the memory
168 grpc::ServerContext context;
169 MemoryRequest mem_req;
170 mem_req.set_address(address);
171 mem_req.set_size(length);
172 MemoryResponse mem_resp;
173
174 auto status = emulator_service_->ReadMemory(&context, &mem_req, &mem_resp);
175 if (!status.ok()) {
176 return absl::InternalError("Failed to read memory");
177 }
178
179 analysis.data.assign(mem_resp.data().begin(), mem_resp.data().end());
180
181 // Identify the data type and structure
182 analysis.data_type = IdentifyDataType(address);
183
184 // Get structure information if available
185 auto struct_info = GetStructureInfo(address);
186 if (struct_info.has_value()) {
187 analysis.structure_name = struct_info.value();
188 }
189
190 // Generate description
191 analysis.description = DescribeMemoryLocation(address);
192
193 // Parse fields for known structures
194 if (address >= SPRITE_TABLE_START && address < SPRITE_TABLE_END) {
195 // Parse sprite data
196 uint32_t sprite_index = (address - SPRITE_TABLE_START) / 0x10;
197 analysis.fields["sprite_index"] = sprite_index;
198
199 if (length >= 0x10) {
200 analysis.fields["state"] = analysis.data[0x00];
201 analysis.fields["x_pos_low"] = analysis.data[0x01];
202 analysis.fields["x_pos_high"] = analysis.data[0x02];
203 analysis.fields["y_pos_low"] = analysis.data[0x03];
204 analysis.fields["y_pos_high"] = analysis.data[0x04];
205
206 // Check for anomalies
207 if (analysis.data[0x00] == 0x00) {
208 analysis.anomalies.push_back("Sprite is inactive (state = 0)");
209 }
210 if (analysis.data[0x02] > 0x01) {
211 analysis.anomalies.push_back("Sprite X position exceeds screen bounds");
212 }
213 }
214 } else if (address >= SRAM_START && address <= SRAM_END) {
215 // Parse save data
216 if (address == PLAYER_HEALTH) {
217 analysis.fields["current_health"] = analysis.data[0];
218 if (analysis.data[0] == 0) {
219 analysis.anomalies.push_back("Player health is zero - death state");
220 }
221 } else if (address == PLAYER_MAX_HEALTH) {
222 analysis.fields["max_health"] = analysis.data[0];
223 } else if (address >= INVENTORY_START && address < INVENTORY_START + 0x40) {
224 uint32_t item_slot = address - INVENTORY_START;
225 analysis.fields["inventory_slot"] = item_slot;
226 analysis.fields["item_value"] = analysis.data[0];
227 }
228 } else if (address >= DMA0_CONTROL && address < DMA0_CONTROL + 0x80) {
229 // Parse DMA registers
230 uint32_t dma_channel = (address - DMA0_CONTROL) / 0x10;
231 analysis.fields["dma_channel"] = dma_channel;
232
233 uint32_t offset = (address - DMA0_CONTROL) % 0x10;
234 switch (offset) {
235 case 0x00:
236 analysis.fields["dma_control"] = analysis.data[0];
237 break;
238 case 0x01:
239 analysis.fields["dma_destination"] = analysis.data[0];
240 break;
241 case 0x02:
242 case 0x03:
243 case 0x04:
244 // Source address bytes
245 break;
246 case 0x05:
247 case 0x06:
248 // Transfer size
249 break;
250 }
251
252 // Check for DMA issues
253 if ((analysis.data[0] & 0x80) && address == DMA0_CONTROL) {
254 analysis.anomalies.push_back(
255 "DMA channel enabled during active display - may cause glitches");
256 }
257 }
258
259 // Check for common issues
260 if (analysis.data_type == "sprite" && length >= 16) {
261 // Check sprite corruption patterns
262 bool all_zero =
263 std::all_of(analysis.data.begin(),
264 analysis.data.begin() + std::min(size_t(16), length),
265 [](uint8_t b) { return b == 0; });
266 bool all_ff =
267 std::all_of(analysis.data.begin(),
268 analysis.data.begin() + std::min(size_t(16), length),
269 [](uint8_t b) { return b == 0xFF; });
270
271 if (all_zero) {
272 analysis.anomalies.push_back(
273 "Sprite data is all zeros - likely uninitialized");
274 }
275 if (all_ff) {
276 analysis.anomalies.push_back(
277 "Sprite data is all 0xFF - possible corruption");
278 }
279 }
280
281 return analysis;
282}
283
284absl::StatusOr<std::string> RomDebugAgent::ExplainExecutionTrace(
285 const std::vector<ExecutionTraceBuffer::TraceEntry>& trace) {
286 std::stringstream explanation;
287
288 explanation << "Execution Trace Analysis:\n";
289 explanation << "=========================\n\n";
290
291 // Track subroutine calls and returns
292 std::vector<std::string> call_stack;
293 int indent_level = 0;
294
295 for (size_t i = 0; i < trace.size(); ++i) {
296 const auto& entry = trace[i];
297
298 // Format the entry with indentation for call hierarchy
299 std::string indent(indent_level * 2, ' ');
300
301 // Check if this is a subroutine call
302 if (entry.opcode == 0x20 || entry.opcode == 0x22 || entry.opcode == 0xFC) {
303 // JSR, JSL, JSR (abs,X)
304 uint32_t target_addr = 0;
305 if (entry.operands.size() >= 2) {
306 target_addr = entry.operands[0] | (entry.operands[1] << 8);
307 if (entry.opcode == 0x22 && entry.operands.size() >= 3) {
308 target_addr |= (entry.operands[2] << 16);
309 }
310 }
311
312 std::string target_name =
313 symbol_provider_->HasSymbols()
314 ? symbol_provider_->FormatAddress(target_addr)
315 : FormatSnesAddress(target_addr);
316
317 explanation << indent << "→ CALL " << target_name << " from "
318 << FormatSnesAddress(entry.address) << "\n";
319
320 call_stack.push_back(target_name);
321 indent_level++;
322
323 } else if (IsReturn(entry.opcode)) {
324 // Return instruction
325 if (!call_stack.empty()) {
326 indent_level = std::max(0, indent_level - 1);
327 indent = std::string(indent_level * 2, ' ');
328 explanation << indent << "← RETURN from " << call_stack.back() << "\n";
329 call_stack.pop_back();
330 }
331
332 } else if (IsBranchOrJump(entry.opcode)) {
333 // Branch or jump
334 std::string condition;
335 switch (entry.opcode) {
336 case 0x10:
337 condition = " (if plus)";
338 break;
339 case 0x30:
340 condition = " (if minus)";
341 break;
342 case 0x50:
343 condition = " (if overflow clear)";
344 break;
345 case 0x70:
346 condition = " (if overflow set)";
347 break;
348 case 0x90:
349 condition = " (if carry clear)";
350 break;
351 case 0xB0:
352 condition = " (if carry set)";
353 break;
354 case 0xD0:
355 condition = " (if not zero)";
356 break;
357 case 0xF0:
358 condition = " (if zero)";
359 break;
360 case 0x80:
361 case 0x82:
362 case 0x4C:
363 case 0x5C:
364 condition = " (unconditional)";
365 break;
366 }
367
368 explanation << indent << " " << entry.mnemonic << " "
369 << entry.operand_str << condition << "\n";
370
371 } else {
372 // Regular instruction - only show significant ones
373 bool is_significant = false;
374 std::string description;
375
376 // Memory access instructions
377 if (entry.mnemonic.find("LD") != std::string::npos ||
378 entry.mnemonic.find("ST") != std::string::npos) {
379 is_significant = true;
380
381 // Try to identify what's being accessed
382 if (entry.operands.size() >= 2) {
383 uint32_t addr = entry.operands[0] | (entry.operands[1] << 8);
384 if (entry.operands.size() >= 3) {
385 addr |= (entry.operands[2] << 16);
386 }
387 description = " ; " + DescribeMemoryLocation(addr);
388 }
389 }
390
391 // Stack operations
392 if (ModifiesStack(entry.opcode)) {
393 is_significant = true;
394 description = " ; Stack: ";
395 if (entry.opcode == 0x48 || entry.opcode == 0x08) {
396 description += "pushing";
397 } else if (entry.opcode == 0x68 || entry.opcode == 0x28) {
398 description += "pulling";
399 }
400 }
401
402 // Arithmetic/logic that changes flags significantly
403 if (entry.mnemonic.find("CMP") != std::string::npos ||
404 entry.mnemonic.find("BIT") != std::string::npos ||
405 entry.mnemonic.find("TST") != std::string::npos) {
406 is_significant = true;
407 description = " ; Testing/comparing";
408 }
409
410 if (is_significant) {
411 explanation << indent << " " << FormatSnesAddress(entry.address)
412 << ": " << entry.mnemonic << " " << entry.operand_str
413 << description << "\n";
414 }
415 }
416
417 // Check for patterns
418 if (i > 0) {
419 // Detect infinite loops
420 if (entry.address == trace[i - 1].address &&
421 IsBranchOrJump(entry.opcode)) {
422 explanation << indent << " ⚠️ POSSIBLE INFINITE LOOP DETECTED\n";
423 }
424
425 // Detect rapid DMA
426 if (entry.address >= DMA0_CONTROL &&
427 entry.address < DMA0_CONTROL + 0x80) {
428 if (i > 0 && trace[i - 1].address >= DMA0_CONTROL &&
429 trace[i - 1].address < DMA0_CONTROL + 0x80) {
430 explanation << indent
431 << " ⚠️ RAPID DMA OPERATIONS - CHECK TIMING\n";
432 }
433 }
434 }
435 }
436
437 // Summary
438 explanation << "\nSummary:\n";
439 explanation << "--------\n";
440 explanation << "Total instructions: " << trace.size() << "\n";
441 explanation << "Subroutine depth: " << indent_level << "\n";
442 if (!call_stack.empty()) {
443 explanation << "Unmatched calls: " << absl::StrJoin(call_stack, ", ")
444 << "\n";
445 }
446
447 return explanation.str();
448}
449
450absl::StatusOr<RomDebugAgent::PatchComparisonResult>
451RomDebugAgent::ComparePatch(uint32_t address, size_t length,
452 const std::vector<uint8_t>& original) {
454 result.address = address;
455 result.length = length;
456 result.original_code = original;
457 result.is_safe = true; // Assume safe until proven otherwise
458
459 // Read the patched code from emulator
460 grpc::ServerContext context;
461 MemoryRequest mem_req;
462 mem_req.set_address(address);
463 mem_req.set_size(length);
464 MemoryResponse mem_resp;
465
466 auto status = emulator_service_->ReadMemory(&context, &mem_req, &mem_resp);
467 if (!status.ok()) {
468 return absl::InternalError("Failed to read patched memory");
469 }
470
471 result.patched_code.assign(mem_resp.data().begin(), mem_resp.data().end());
472
473 // Disassemble both versions
474 std::stringstream orig_disasm, patch_disasm;
475 size_t orig_offset = 0, patch_offset = 0;
476
477 while (orig_offset < original.size() &&
478 patch_offset < result.patched_code.size()) {
479 // Disassemble original
480 std::string orig_mnem, orig_operand;
481 std::vector<uint8_t> orig_operands;
482 uint8_t orig_size = disassembler_->DisassembleInstruction(
483 address + orig_offset, original.data() + orig_offset, orig_mnem,
484 orig_operand, orig_operands);
485
486 orig_disasm << FormatSnesAddress(address + orig_offset) << ": " << orig_mnem
487 << " " << orig_operand << "\n";
488
489 // Disassemble patched
490 std::string patch_mnem, patch_operand;
491 std::vector<uint8_t> patch_operands;
492 uint8_t patch_size = disassembler_->DisassembleInstruction(
493 address + patch_offset, result.patched_code.data() + patch_offset,
494 patch_mnem, patch_operand, patch_operands);
495
496 patch_disasm << FormatSnesAddress(address + patch_offset) << ": "
497 << patch_mnem << " " << patch_operand << "\n";
498
499 // Compare instructions
500 if (orig_mnem != patch_mnem || orig_operand != patch_operand) {
501 result.differences.push_back(absl::StrFormat(
502 "At %s: '%s %s' → '%s %s'", FormatSnesAddress(address + orig_offset),
503 orig_mnem, orig_operand, patch_mnem, patch_operand));
504
505 // Check for potential issues
506
507 // Check if jump target is valid
508 if (IsBranchOrJump(result.patched_code[patch_offset])) {
509 uint32_t target = 0;
510 if (patch_operands.size() >= 2) {
511 target = patch_operands[0] | (patch_operands[1] << 8);
512 if (patch_operands.size() >= 3) {
513 target |= (patch_operands[2] << 16);
514 }
515 }
516
517 if (!IsValidJumpTarget(target)) {
518 result.potential_issues.push_back(
519 absl::StrFormat("Invalid jump target at %s: %s",
520 FormatSnesAddress(address + patch_offset),
521 FormatSnesAddress(target)));
522 result.is_safe = false;
523 }
524 }
525
526 // Check for stack imbalance
527 bool orig_modifies_stack = ModifiesStack(original[orig_offset]);
528 bool patch_modifies_stack =
529 ModifiesStack(result.patched_code[patch_offset]);
530
531 if (orig_modifies_stack != patch_modifies_stack) {
532 result.potential_issues.push_back(
533 "Stack modification mismatch - may cause stack imbalance");
534 result.is_safe = false;
535 }
536 }
537
538 orig_offset += orig_size;
539 patch_offset += patch_size;
540 }
541
542 result.original_disassembly = orig_disasm.str();
543 result.patched_disassembly = patch_disasm.str();
544
545 // Additional safety checks
546
547 // Check if patch overwrites critical areas
548 if (IsCriticalMemoryArea(address)) {
549 result.potential_issues.push_back(
550 "Patch modifies critical system area - verify this is intentional");
551 result.is_safe = false;
552 }
553
554 // Check for NOP slides (common in bad patches)
555 int nop_count = 0;
556 for (uint8_t byte : result.patched_code) {
557 if (byte == 0xEA) { // NOP
558 nop_count++;
559 }
560 }
561 if (nop_count > 5) {
562 result.potential_issues.push_back(absl::StrFormat(
563 "Patch contains %d NOPs - possible padding or removed code",
564 nop_count));
565 }
566
567 // Check for BRK instructions (usually indicates problems)
568 for (size_t i = 0; i < result.patched_code.size(); ++i) {
569 if (result.patched_code[i] == 0x00) { // BRK
570 result.potential_issues.push_back(
571 absl::StrFormat("BRK instruction at %s - usually indicates error",
572 FormatSnesAddress(address + i)));
573 result.is_safe = false;
574 }
575 }
576
577 return result;
578}
579
580std::vector<RomDebugAgent::DetectedIssue> RomDebugAgent::ScanForIssues(
581 uint32_t start_address, uint32_t end_address) {
582 std::vector<DetectedIssue> issues;
583
584 // Read the code region
585 grpc::ServerContext context;
586 MemoryRequest mem_req;
587 mem_req.set_address(start_address);
588 mem_req.set_size(end_address - start_address);
589 MemoryResponse mem_resp;
590
591 auto status = emulator_service_->ReadMemory(&context, &mem_req, &mem_resp);
592 if (!status.ok()) {
593 return issues;
594 }
595
596 const uint8_t* code =
597 reinterpret_cast<const uint8_t*>(mem_resp.data().data());
598 size_t code_size = mem_resp.data().size();
599
600 size_t offset = 0;
601 while (offset < code_size) {
602 uint32_t current_addr = start_address + offset;
603
604 // Check for specific patterns
605 auto issue =
606 DetectIssuePattern(current_addr, code + offset, code_size - offset);
607 if (issue.has_value()) {
608 issues.push_back(issue.value());
609 }
610
611 // Get instruction size to advance
612 uint8_t inst_size = disassembler_->GetInstructionSize(code[offset]);
613 if (inst_size == 0) {
614 // Invalid opcode
615 issues.push_back(
616 {IssueType::kInvalidOpcode, current_addr,
617 absl::StrFormat("Invalid opcode $%02X at %s", code[offset],
618 FormatSnesAddress(current_addr)),
619 "Check if this is data being executed as code", 5});
620 offset++;
621 } else {
622 offset += inst_size;
623 }
624 }
625
626 return issues;
627}
628
629bool RomDebugAgent::IsValidJumpTarget(uint32_t address) const {
630 // Check if address is in valid ROM or RAM range
631 if (address < 0x008000) {
632 // Low RAM - generally okay for some routines
633 return true;
634 }
635 if (address >= 0x008000 && address < 0x7E0000) {
636 // ROM space - valid
637 return true;
638 }
639 if (address >= WRAM_START && address <= WRAM_END) {
640 // WRAM - valid but unusual for code
641 return true;
642 }
643 if (address >= 0x800000 && address < 0xC00000) {
644 // Extended ROM banks - valid
645 return true;
646 }
647
648 // Invalid ranges
649 return false;
650}
651
652bool RomDebugAgent::HasStackImbalance(uint32_t routine_start,
653 uint32_t routine_end) {
654 // Read the routine
655 grpc::ServerContext context;
656 MemoryRequest mem_req;
657 mem_req.set_address(routine_start);
658 mem_req.set_size(routine_end - routine_start);
659 MemoryResponse mem_resp;
660
661 auto status = emulator_service_->ReadMemory(&context, &mem_req, &mem_resp);
662 if (!status.ok()) {
663 return false;
664 }
665
666 const uint8_t* code =
667 reinterpret_cast<const uint8_t*>(mem_resp.data().data());
668 size_t code_size = mem_resp.data().size();
669
670 // Track stack depth
671 int stack_depth = 0;
672 size_t offset = 0;
673
674 while (offset < code_size) {
675 uint8_t opcode = code[offset];
676
677 // Track pushes and pulls
678 switch (opcode) {
679 case 0x48: // PHA
680 case 0x08: // PHP
681 case 0xDA: // PHX
682 case 0x5A: // PHY
683 case 0x8B: // PHB
684 case 0x0B: // PHD
685 case 0x4B: // PHK
686 stack_depth++;
687 break;
688
689 case 0x68: // PLA
690 case 0x28: // PLP
691 case 0xFA: // PLX
692 case 0x7A: // PLY
693 case 0xAB: // PLB
694 case 0x2B: // PLD
695 stack_depth--;
696 break;
697
698 case 0x62: // PER (push effective address)
699 case 0xD4: // PEI
700 case 0xF4: // PEA
701 stack_depth += 2; // These push 16-bit values
702 break;
703 }
704
705 // Check for return
706 if (IsReturn(opcode)) {
707 // At return, stack should be balanced
708 return stack_depth != 0;
709 }
710
711 uint8_t inst_size = disassembler_->GetInstructionSize(opcode);
712 offset += (inst_size > 0) ? inst_size : 1;
713 }
714
715 // If we didn't find a return, check final depth
716 return stack_depth != 0;
717}
718
719bool RomDebugAgent::IsMemoryWriteSafe(uint32_t address, size_t length) const {
720 // Check if write touches critical areas
721 uint32_t end_address = address + length;
722
723 // Check system vectors
724 if ((address <= 0x00FFFF && end_address > 0x00FFE0)) {
725 return false; // Writing to interrupt vectors
726 }
727
728 // Check NMI flag
729 if (address <= NMI_FLAG && end_address > NMI_FLAG) {
730 return false; // Modifying NMI flag can break frame timing
731 }
732
733 // Check PPU registers during active display
734 if (address >= 0x002100 && address <= 0x00213F) {
735 // Some PPU registers are unsafe during active display
736 // This would need frame timing info to be accurate
737 return true; // Assume safe for now
738 }
739
740 // Check DMA registers
741 if (address >= DMA0_CONTROL && address < DMA0_CONTROL + 0x80) {
742 // DMA writes during active display can cause issues
743 return true; // Assume safe for now
744 }
745
746 // Check critical WRAM areas
747 if (address >= 0x7E0000 && address < 0x7E2000) {
748 // Low WRAM has critical system variables
749 if (address < 0x7E0100) {
750 return false; // Direct page and stack area
751 }
752 }
753
754 return true;
755}
756
757std::string RomDebugAgent::DescribeMemoryLocation(uint32_t address) const {
758 // Check cache first
759 auto it = address_description_cache_.find(address);
760 if (it != address_description_cache_.end()) {
761 return it->second;
762 }
763
764 std::string description;
765
766 // System areas
767 if (address < 0x100) {
768 description = "Direct Page";
769 } else if (address >= 0x100 && address < 0x200) {
770 description = "Stack";
771 } else if (address >= GAME_MODE && address == GAME_MODE) {
772 description = "Game Mode";
773 } else if (address == SUBMODULE) {
774 description = "Submodule";
775 } else if (address == NMI_FLAG) {
776 description = "NMI Flag";
777 } else if (address == FRAME_COUNTER) {
778 description = "Frame Counter";
779 }
780 // Player/Link
781 else if (address == LINK_X_POS) {
782 description = "Link X Position";
783 } else if (address == LINK_Y_POS) {
784 description = "Link Y Position";
785 } else if (address == LINK_STATE) {
786 description = "Link State";
787 } else if (address == LINK_DIRECTION) {
788 description = "Link Facing Direction";
789 }
790 // Sprites
791 else if (address >= SPRITE_TABLE_START && address < SPRITE_TABLE_END) {
792 uint32_t offset = address - SPRITE_TABLE_START;
793 uint32_t sprite_num = offset / 0x10;
794 description = absl::StrFormat("Sprite %d Data", sprite_num);
795 }
796 // OAM
797 else if (address >= OAM_BUFFER && address <= OAM_BUFFER_END) {
798 description = "OAM Buffer (sprite attributes)";
799 }
800 // DMA
801 else if (address >= DMA0_CONTROL && address < DMA0_CONTROL + 0x80) {
802 uint32_t channel = (address - DMA0_CONTROL) / 0x10;
803 description = absl::StrFormat("DMA Channel %d", channel);
804 } else if (address == DMA_ENABLE) {
805 description = "DMA Enable Register";
806 } else if (address == HDMA_ENABLE) {
807 description = "HDMA Enable Register";
808 }
809 // PPU
810 else if (address == PPU_INIDISP) {
811 description = "Screen Display Register";
812 } else if (address == PPU_BGMODE) {
813 description = "BG Mode Register";
814 } else if (address == PPU_CGADD) {
815 description = "CGRAM Address";
816 } else if (address == PPU_CGDATA) {
817 description = "CGRAM Data";
818 }
819 // Audio
820 else if (address >= APU_PORT0 && address <= APU_PORT3) {
821 description = absl::StrFormat("APU Port %d", address - APU_PORT0);
822 }
823 // Save data
824 else if (address >= SRAM_START && address <= SRAM_END) {
825 if (address >= PLAYER_NAME && address < PLAYER_NAME + 6) {
826 description = "Player Name";
827 } else if (address == PLAYER_HEALTH) {
828 description = "Player Current Health";
829 } else if (address == PLAYER_MAX_HEALTH) {
830 description = "Player Max Health";
831 } else if (address >= INVENTORY_START && address < INVENTORY_START + 0x40) {
833 absl::StrFormat("Inventory Slot %d", address - INVENTORY_START);
834 } else {
835 description = "Save Data";
836 }
837 }
838 // ROM banks
839 else if (address >= 0x008000 && address < 0x00FFFF) {
840 description = absl::StrFormat("ROM Bank $%02X", (address >> 16) & 0xFF);
841 } else if (address >= WRAM_START && address <= WRAM_END) {
842 description = "WRAM";
843 } else if (address >= 0x800000 && address < 0xC00000) {
845 absl::StrFormat("Extended ROM Bank $%02X", (address >> 16) & 0xFF);
846 } else {
847 description = "Unknown";
848 }
849
850 // Cache the result
852 return description;
853}
854
855std::string RomDebugAgent::IdentifyDataType(uint32_t address) const {
856 auto it = data_type_cache_.find(address);
857 if (it != data_type_cache_.end()) {
858 return it->second;
859 }
860
861 std::string type;
862
863 if (address >= SPRITE_TABLE_START && address < SPRITE_TABLE_END) {
864 type = "sprite";
865 } else if (address >= OAM_BUFFER && address <= OAM_BUFFER_END) {
866 type = "oam";
867 } else if (address >= DMA0_CONTROL && address < DMA0_CONTROL + 0x80) {
868 type = "dma";
869 } else if (address >= 0x002100 && address <= 0x00213F) {
870 type = "ppu";
871 } else if (address >= APU_PORT0 && address <= APU_PORT3) {
872 type = "audio";
873 } else if (address >= SRAM_START && address <= SRAM_END) {
874 type = "save";
875 } else if (address >= INVENTORY_START && address < INVENTORY_START + 0x40) {
876 type = "inventory";
877 } else if (address >= 0x008000 && address < 0x7E0000) {
878 type = "code";
879 } else if (address >= WRAM_START && address <= WRAM_END) {
880 type = "ram";
881 } else {
882 type = "unknown";
883 }
884
885 data_type_cache_[address] = type;
886 return type;
887}
888
890 const std::map<std::string, uint16_t>& regs) const {
891 std::stringstream ss;
892 ss << "Registers: ";
893 ss << absl::StrFormat("A=%04X ", regs.at("A"));
894 ss << absl::StrFormat("X=%04X ", regs.at("X"));
895 ss << absl::StrFormat("Y=%04X ", regs.at("Y"));
896 ss << absl::StrFormat("S=%04X ", regs.at("S"));
897 ss << absl::StrFormat("PC=%04X ", regs.at("PC"));
898 ss << absl::StrFormat("P=%02X ", regs.at("P"));
899 ss << absl::StrFormat("DB=%02X ", regs.at("DB"));
900 ss << absl::StrFormat("PB=%02X", regs.at("PB"));
901 return ss.str();
902}
903
904absl::Status RomDebugAgent::LoadSymbols(const std::string& symbol_file) {
905 return symbol_provider_->LoadSymbolFile(symbol_file);
906}
907
908void RomDebugAgent::SetOriginalRom(const std::vector<uint8_t>& rom_data) {
909 original_rom_ = rom_data;
910}
911
912// Private helper methods
913
914absl::StatusOr<std::string> RomDebugAgent::AnalyzeInstruction(
915 uint32_t address, const uint8_t* code, size_t max_length) {
916 if (max_length == 0) {
917 return absl::InvalidArgumentError("No code provided");
918 }
919
920 uint8_t opcode = code[0];
921 std::string explanation;
922
923 // Analyze based on opcode
924 switch (opcode) {
925 // Load instructions
926 case 0xA9:
927 explanation = "Load immediate value into accumulator";
928 break;
929 case 0xA2:
930 explanation = "Load immediate value into X register";
931 break;
932 case 0xA0:
933 explanation = "Load immediate value into Y register";
934 break;
935 case 0xAD:
936 explanation = "Load accumulator from absolute address";
937 break;
938 case 0xAE:
939 explanation = "Load X register from absolute address";
940 break;
941 case 0xAC:
942 explanation = "Load Y register from absolute address";
943 break;
944
945 // Store instructions
946 case 0x8D:
947 explanation = "Store accumulator to absolute address";
948 break;
949 case 0x8E:
950 explanation = "Store X register to absolute address";
951 break;
952 case 0x8C:
953 explanation = "Store Y register to absolute address";
954 break;
955 case 0x9D:
956 explanation = "Store accumulator to address indexed by X";
957 break;
958 case 0x99:
959 explanation = "Store accumulator to address indexed by Y";
960 break;
961
962 // Branches
963 case 0x10:
964 explanation = "Branch if plus (N flag clear)";
965 break;
966 case 0x30:
967 explanation = "Branch if minus (N flag set)";
968 break;
969 case 0x50:
970 explanation = "Branch if overflow clear";
971 break;
972 case 0x70:
973 explanation = "Branch if overflow set";
974 break;
975 case 0x80:
976 explanation = "Branch always";
977 break;
978 case 0x90:
979 explanation = "Branch if carry clear (less than)";
980 break;
981 case 0xB0:
982 explanation = "Branch if carry set (greater than or equal)";
983 break;
984 case 0xD0:
985 explanation = "Branch if not equal (Z flag clear)";
986 break;
987 case 0xF0:
988 explanation = "Branch if equal (Z flag set)";
989 break;
990
991 // Jumps and calls
992 case 0x20:
993 explanation = "Call subroutine";
994 break;
995 case 0x22:
996 explanation = "Call long subroutine (24-bit)";
997 break;
998 case 0x4C:
999 explanation = "Jump to address";
1000 break;
1001 case 0x5C:
1002 explanation = "Jump long (24-bit)";
1003 break;
1004 case 0x60:
1005 explanation = "Return from subroutine";
1006 break;
1007 case 0x6B:
1008 explanation = "Return from long subroutine";
1009 break;
1010
1011 // Stack operations
1012 case 0x48:
1013 explanation = "Push accumulator onto stack";
1014 break;
1015 case 0x68:
1016 explanation = "Pull accumulator from stack";
1017 break;
1018 case 0x08:
1019 explanation = "Push processor status onto stack";
1020 break;
1021 case 0x28:
1022 explanation = "Pull processor status from stack";
1023 break;
1024
1025 // Arithmetic
1026 case 0x69:
1027 explanation = "Add to accumulator with carry";
1028 break;
1029 case 0xE9:
1030 explanation = "Subtract from accumulator with borrow";
1031 break;
1032 case 0xC9:
1033 explanation = "Compare accumulator with value";
1034 break;
1035 case 0xE0:
1036 explanation = "Compare X register with value";
1037 break;
1038 case 0xC0:
1039 explanation = "Compare Y register with value";
1040 break;
1041
1042 // Logical
1043 case 0x29:
1044 explanation = "AND accumulator with value";
1045 break;
1046 case 0x09:
1047 explanation = "OR accumulator with value";
1048 break;
1049 case 0x49:
1050 explanation = "XOR accumulator with value";
1051 break;
1052
1053 // Special
1054 case 0x00:
1055 explanation = "Software interrupt (BRK)";
1056 break;
1057 case 0xEA:
1058 explanation = "No operation (NOP)";
1059 break;
1060 case 0x18:
1061 explanation = "Clear carry flag";
1062 break;
1063 case 0x38:
1064 explanation = "Set carry flag";
1065 break;
1066 case 0xC2:
1067 explanation = "Clear processor flags (REP)";
1068 break;
1069 case 0xE2:
1070 explanation = "Set processor flags (SEP)";
1071 break;
1072
1073 default:
1074 explanation = absl::StrFormat("Execute opcode $%02X", opcode);
1075 }
1076
1077 return explanation;
1078}
1079
1080std::vector<std::string> RomDebugAgent::GetDisassemblyContext(uint32_t address,
1081 int before_lines,
1082 int after_lines) {
1083 std::vector<std::string> context_lines;
1084
1085 // Get disassembly from emulator service
1086 grpc::ServerContext ctx;
1087 DisassemblyRequest req;
1088 req.set_start_address(
1089 address - (before_lines * 3)); // Estimate 3 bytes per instruction
1090 req.set_count(before_lines + after_lines + 1);
1091 DisassemblyResponse resp;
1092
1093 auto status = emulator_service_->GetDisassembly(&ctx, &req, &resp);
1094 if (!status.ok()) {
1095 return context_lines;
1096 }
1097
1098 for (const auto& inst : resp.lines()) {
1099 std::string line =
1100 absl::StrFormat("%s: %s %s", FormatSnesAddress(inst.address()),
1101 inst.mnemonic(), inst.operand_str());
1102 if (inst.address() == address) {
1103 line = ">>> " + line + " <<<"; // Highlight current instruction
1104 }
1105 context_lines.push_back(line);
1106 }
1107
1108 return context_lines;
1109}
1110
1111std::vector<std::string> RomDebugAgent::BuildCallStack(uint32_t current_pc) {
1112 std::vector<std::string> stack;
1113
1114 // Get execution trace to build call stack
1115 grpc::ServerContext ctx;
1116 yaze::agent::TraceRequest req;
1117 req.set_max_entries(100); // Get last 100 instructions
1118 yaze::agent::TraceResponse resp;
1119
1120 auto status = emulator_service_->GetExecutionTrace(&ctx, &req, &resp);
1121 if (!status.ok()) {
1122 return stack;
1123 }
1124
1125 // Walk backwards through trace to find calls
1126 for (int i = resp.entries_size() - 1; i >= 0; --i) {
1127 const auto& entry = resp.entries(i);
1128 uint8_t opcode = entry.opcode();
1129
1130 if (opcode == 0x20 || opcode == 0x22 || opcode == 0xFC) {
1131 // Found a call
1132 std::string caller =
1133 symbol_provider_->HasSymbols()
1134 ? symbol_provider_->FormatAddress(entry.address())
1135 : FormatSnesAddress(entry.address());
1136 stack.push_back(caller);
1137 } else if (IsReturn(opcode) && !stack.empty()) {
1138 // Found a return, pop from our reconstructed stack
1139 stack.pop_back();
1140 }
1141 }
1142
1143 // Reverse to get top-down order
1144 std::reverse(stack.begin(), stack.end());
1145
1146 return stack;
1147}
1148
1149std::optional<RomDebugAgent::DetectedIssue> RomDebugAgent::DetectIssuePattern(
1150 uint32_t address, const uint8_t* code, size_t length) {
1151 if (length < 1) {
1152 return std::nullopt;
1153 }
1154
1155 uint8_t opcode = code[0];
1156
1157 // Check for BRK (usually an error)
1158 if (opcode == 0x00) {
1159 return DetectedIssue{
1161 "BRK instruction found - usually indicates an error or unimplemented "
1162 "code",
1163 "Replace with proper implementation or NOP if intentional padding", 4};
1164 }
1165
1166 // Check for infinite loop (branch to self)
1167 if (opcode == 0x80 && length >= 2 && code[1] == 0xFE) {
1168 // BRA $-2 (branch to self)
1170 "Infinite loop detected (BRA to self)",
1171 "Add proper exit condition or loop counter", 5};
1172 }
1173
1174 // Check for writes to vector table
1175 if (opcode == 0x8D && length >= 3) { // STA abs
1176 uint16_t dest = code[1] | (code[2] << 8);
1177 if (dest >= 0xFFE0) {
1178 return DetectedIssue{
1180 absl::StrFormat("Writing to interrupt vector at $%04X", dest),
1181 "Verify this vector modification is intentional", 5};
1182 }
1183 }
1184
1185 // Check for stack operations without matching pairs
1186 if (ModifiesStack(opcode)) {
1187 // This would need more context to properly detect
1188 // For now, just flag excessive consecutive pushes
1189 int consecutive_pushes = 0;
1190 for (size_t i = 0; i < std::min(size_t(10), length); ++i) {
1191 if (code[i] == 0x48 || code[i] == 0x08 || code[i] == 0xDA ||
1192 code[i] == 0x5A) {
1193 consecutive_pushes++;
1194 } else if (code[i] == 0x68 || code[i] == 0x28 || code[i] == 0xFA ||
1195 code[i] == 0x7A) {
1196 consecutive_pushes--;
1197 }
1198 }
1199
1200 if (consecutive_pushes > 5) {
1201 return DetectedIssue{
1203 "Multiple consecutive pushes without pulls - possible stack overflow",
1204 "Verify stack operations are properly balanced", 3};
1205 }
1206 }
1207
1208 return std::nullopt;
1209}
1210
1211bool RomDebugAgent::IsCriticalMemoryArea(uint32_t address) const {
1212 // Direct page and stack
1213 if (address < 0x200) {
1214 return true;
1215 }
1216
1217 // Interrupt vectors
1218 if (address >= 0xFFE0 && address <= 0xFFFF) {
1219 return true;
1220 }
1221
1222 // NMI handler area
1223 if (address == NMI_FLAG) {
1224 return true;
1225 }
1226
1227 // DMA during critical timing
1228 if (address >= DMA0_CONTROL && address < DMA0_CONTROL + 0x80) {
1229 return true;
1230 }
1231
1232 return false;
1233}
1234
1235std::optional<std::string> RomDebugAgent::GetStructureInfo(
1236 uint32_t address) const {
1237 // Sprite structure
1238 if (address >= SPRITE_TABLE_START && address < SPRITE_TABLE_END) {
1239 uint32_t offset = (address - SPRITE_TABLE_START) % 0x10;
1240 switch (offset) {
1241 case 0x00:
1242 return "Sprite State";
1243 case 0x01:
1244 return "Sprite X Position Low";
1245 case 0x02:
1246 return "Sprite X Position High";
1247 case 0x03:
1248 return "Sprite Y Position Low";
1249 case 0x04:
1250 return "Sprite Y Position High";
1251 case 0x05:
1252 return "Sprite Z Position";
1253 case 0x06:
1254 return "Sprite Velocity X";
1255 case 0x07:
1256 return "Sprite Velocity Y";
1257 case 0x08:
1258 return "Sprite Type";
1259 case 0x09:
1260 return "Sprite Subtype";
1261 case 0x0A:
1262 return "Sprite Graphics";
1263 case 0x0B:
1264 return "Sprite Properties";
1265 case 0x0C:
1266 return "Sprite Health";
1267 case 0x0D:
1268 return "Sprite Damage";
1269 case 0x0E:
1270 return "Sprite Timer";
1271 case 0x0F:
1272 return "Sprite Flags";
1273 }
1274 }
1275
1276 // OAM structure
1277 if (address >= OAM_BUFFER && address <= OAM_BUFFER_END) {
1278 uint32_t offset = (address - OAM_BUFFER) % 4;
1279 switch (offset) {
1280 case 0:
1281 return "OAM X Position";
1282 case 1:
1283 return "OAM Y Position";
1284 case 2:
1285 return "OAM Tile Number";
1286 case 3:
1287 return "OAM Attributes";
1288 }
1289 }
1290
1291 // DMA channel structure
1292 if (address >= DMA0_CONTROL && address < DMA0_CONTROL + 0x80) {
1293 uint32_t offset = (address - DMA0_CONTROL) % 0x10;
1294 switch (offset) {
1295 case 0x00:
1296 return "DMA Control";
1297 case 0x01:
1298 return "DMA Destination";
1299 case 0x02:
1300 return "DMA Source Low";
1301 case 0x03:
1302 return "DMA Source High";
1303 case 0x04:
1304 return "DMA Source Bank";
1305 case 0x05:
1306 return "DMA Size Low";
1307 case 0x06:
1308 return "DMA Size High";
1309 case 0x07:
1310 return "DMA Indirect Bank";
1311 }
1312 }
1313
1314 return std::nullopt;
1315}
1316
1317} // namespace agent
1318} // namespace cli
1319} // namespace yaze
static constexpr uint32_t FRAME_COUNTER
yaze::net::EmulatorServiceImpl * emulator_service_
void SetOriginalRom(const std::vector< uint8_t > &rom_data)
Set the original ROM data for comparison.
static constexpr uint32_t OAM_BUFFER
static constexpr uint32_t PLAYER_HEALTH
static constexpr uint32_t LINK_Y_POS
static constexpr uint32_t OAM_BUFFER_END
static constexpr uint32_t DMA_ENABLE
absl::StatusOr< std::string > ExplainExecutionTrace(const std::vector< ExecutionTraceBuffer::TraceEntry > &trace)
Analyze execution trace and explain program flow.
static constexpr uint32_t WRAM_END
absl::StatusOr< BreakpointAnalysis > AnalyzeBreakpoint(const yaze::agent::BreakpointHitResponse &hit)
Analyze a breakpoint hit with full context.
static constexpr uint32_t APU_PORT0
bool IsMemoryWriteSafe(uint32_t address, size_t length) const
Check if memory write is safe.
static constexpr uint32_t PPU_BGMODE
static constexpr uint32_t LINK_X_POS
std::string FormatRegisterState(const std::map< std::string, uint16_t > &regs) const
Format register state for debugging output.
static constexpr uint32_t SPRITE_TABLE_START
bool IsValidJumpTarget(uint32_t address) const
Check if an address is a valid jump target.
static constexpr uint32_t PPU_CGADD
static constexpr uint32_t INVENTORY_START
static constexpr uint32_t GAME_MODE
static constexpr uint32_t SRAM_END
std::optional< std::string > GetStructureInfo(uint32_t address) const
Get structure information for a memory address.
std::map< uint32_t, std::string > address_description_cache_
std::vector< DetectedIssue > ScanForIssues(uint32_t start_address, uint32_t end_address)
Scan for common ROM hacking issues in a code region.
static constexpr uint32_t SRAM_START
std::vector< std::string > GetDisassemblyContext(uint32_t address, int before_lines, int after_lines)
Get surrounding context for an address.
std::vector< std::string > BuildCallStack(uint32_t current_pc)
Build call stack from execution trace.
bool HasStackImbalance(uint32_t routine_start, uint32_t routine_end)
Detect stack imbalance in a subroutine.
std::vector< uint8_t > original_rom_
std::map< uint32_t, std::string > data_type_cache_
static constexpr uint32_t APU_PORT3
absl::Status LoadSymbols(const std::string &symbol_file)
Load symbol table for better disassembly.
absl::StatusOr< PatchComparisonResult > ComparePatch(uint32_t address, size_t length, const std::vector< uint8_t > &original)
Compare original ROM code with patched code.
std::unique_ptr< yaze::emu::debug::SymbolProvider > symbol_provider_
std::optional< DetectedIssue > DetectIssuePattern(uint32_t address, const uint8_t *code, size_t length)
Detect pattern of common issues.
RomDebugAgent(yaze::net::EmulatorServiceImpl *emulator_service)
static constexpr uint32_t SPRITE_TABLE_END
std::string DescribeMemoryLocation(uint32_t address) const
Get human-readable description of a memory address.
static constexpr uint32_t SUBMODULE
absl::StatusOr< std::string > AnalyzeInstruction(uint32_t address, const uint8_t *code, size_t max_length)
Analyze the instruction at an address.
static constexpr uint32_t PPU_INIDISP
bool IsCriticalMemoryArea(uint32_t address) const
Check if address is in a critical system area.
static constexpr uint32_t NMI_FLAG
absl::StatusOr< MemoryAnalysis > AnalyzeMemory(uint32_t address, size_t length)
Analyze a memory region and identify its purpose.
static constexpr uint32_t LINK_STATE
static constexpr uint32_t PLAYER_NAME
static constexpr uint32_t HDMA_ENABLE
static constexpr uint32_t WRAM_START
static constexpr uint32_t PPU_CGDATA
static constexpr uint32_t PLAYER_MAX_HEALTH
static constexpr uint32_t DMA0_CONTROL
std::unique_ptr< Disassembler65816 > disassembler_
static constexpr uint32_t LINK_DIRECTION
std::string IdentifyDataType(uint32_t address) const
Get the data type at a memory address.
grpc::Status GetDisassembly(grpc::ServerContext *context, const agent::DisassemblyRequest *request, agent::DisassemblyResponse *response) override
grpc::Status ReadMemory(grpc::ServerContext *context, const agent::MemoryRequest *request, agent::MemoryResponse *response) override
grpc::Status GetExecutionTrace(grpc::ServerContext *context, const agent::TraceRequest *request, agent::TraceResponse *response) override
Analysis result for a breakpoint hit.
std::map< std::string, uint32_t > fields