mirror of
https://github.com/pevans/erc-c.git
synced 2024-10-08 07:58:04 +00:00
413 lines
12 KiB
C
413 lines
12 KiB
C
/*
|
|
* mos6502.dis.c
|
|
*
|
|
* Disassembly of the mos6502 machine code into an assembly notation.
|
|
*/
|
|
|
|
#include <stdbool.h>
|
|
|
|
#include "mos6502.h"
|
|
#include "mos6502.dis.h"
|
|
#include "mos6502.enums.h"
|
|
|
|
static vm_8bit jump_table[MOS6502_MEMSIZE];
|
|
|
|
static char *instruction_strings[] = {
|
|
"ADC",
|
|
"AND",
|
|
"ASL",
|
|
"BCC",
|
|
"BCS",
|
|
"BEQ",
|
|
"BIT",
|
|
"BMI",
|
|
"BNE",
|
|
"BPL",
|
|
"BRK",
|
|
"BVC",
|
|
"BVS",
|
|
"CLC",
|
|
"CLD",
|
|
"CLI",
|
|
"CLV",
|
|
"CMP",
|
|
"CPX",
|
|
"CPY",
|
|
"DEC",
|
|
"DEX",
|
|
"DEY",
|
|
"EOR",
|
|
"INC",
|
|
"INX",
|
|
"INY",
|
|
"JMP",
|
|
"JSR",
|
|
"LDA",
|
|
"LDX",
|
|
"LDY",
|
|
"LSR",
|
|
"NOP",
|
|
"ORA",
|
|
"PHA",
|
|
"PHP",
|
|
"PLA",
|
|
"PLP",
|
|
"ROL",
|
|
"ROR",
|
|
"RTI",
|
|
"RTS",
|
|
"SBC",
|
|
"SEC",
|
|
"SED",
|
|
"SEI",
|
|
"STA",
|
|
"STX",
|
|
"STY",
|
|
"TAX",
|
|
"TAY",
|
|
"TSX",
|
|
"TXA",
|
|
"TXS",
|
|
"TYA",
|
|
};
|
|
|
|
/*
|
|
* Given a stream, address mode and 16-bit value, print the value out in
|
|
* the form that is expected given the address mode. The value is not
|
|
* necessarily going to truly be 16-bit; most address modes use one
|
|
* 8-bit operand. But we can contain all possible values with the 16-bit
|
|
* type.
|
|
*/
|
|
void
|
|
mos6502_dis_operand(mos6502 *cpu,
|
|
FILE *stream,
|
|
int address,
|
|
int addr_mode,
|
|
vm_16bit value)
|
|
{
|
|
int rel_address;
|
|
int ind_address;
|
|
|
|
switch (addr_mode) {
|
|
case ACC:
|
|
break;
|
|
case ABS:
|
|
fprintf(stream, "$%04X", value);
|
|
break;
|
|
case ABX:
|
|
fprintf(stream, "$%04X,X", value);
|
|
break;
|
|
case ABY:
|
|
fprintf(stream, "$%04X,Y", value);
|
|
break;
|
|
case IMM:
|
|
fprintf(stream, "#$%02X", value);
|
|
break;
|
|
case IMP:
|
|
break;
|
|
case IND:
|
|
ind_address = vm_segment_get(cpu->memory, value + 1) << 8;
|
|
ind_address |= vm_segment_get(cpu->memory, value);
|
|
if (jump_table[ind_address]) {
|
|
mos6502_dis_label(stream, ind_address);
|
|
} else {
|
|
fprintf(stream, "($%04X)", value);
|
|
}
|
|
break;
|
|
case IDX:
|
|
fprintf(stream, "($%02X,X)", value);
|
|
break;
|
|
case IDY:
|
|
fprintf(stream, "($%02X),Y", value);
|
|
break;
|
|
case REL:
|
|
rel_address = address + value;
|
|
if (value > 127) {
|
|
rel_address -= 256;
|
|
}
|
|
|
|
mos6502_dis_label(stream, rel_address);
|
|
break;
|
|
case ZPG:
|
|
// We add a couple of spaces here to help our output
|
|
// comments line up.
|
|
fprintf(stream, "$%02X ", value);
|
|
break;
|
|
case ZPX:
|
|
fprintf(stream, "$%02X,X", value);
|
|
break;
|
|
case ZPY:
|
|
fprintf(stream, "$%02X,Y", value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This function will write to the stream the instruction that the given
|
|
* opcode maps to.
|
|
*/
|
|
void
|
|
mos6502_dis_instruction(FILE *stream, int inst_code)
|
|
{
|
|
// Arguably this could or should be done as fputs(), which is
|
|
// presumably a simpler output method. But, since we use fprintf()
|
|
// in other places, I think we should continue to do so. Further, we
|
|
// use a simple format string (%s) to avoid the linter's complaints
|
|
// about potential security issues.
|
|
fprintf(stream, "%s", instruction_strings[inst_code]);
|
|
}
|
|
|
|
/*
|
|
* This function returns the number of bytes that the given opcode is
|
|
* expecting to work with. For instance, if the opcode is in absolute
|
|
* address mode, then we will need to read the next two bytes in the
|
|
* stream to compose a full 16-bit address to work with. If our opcode
|
|
* is in immediate mode, then we only need to read one byte. Many
|
|
* opcodes will read no bytes at all from the stream (in which we return
|
|
* zero).
|
|
*/
|
|
int
|
|
mos6502_dis_expected_bytes(int addr_mode)
|
|
{
|
|
switch (addr_mode) {
|
|
// These are 16-bit operands, because they work with absolute
|
|
// addresses in memory.
|
|
case ABS:
|
|
case ABY:
|
|
case ABX:
|
|
case IND:
|
|
return 2;
|
|
|
|
// These are the 8-bit operand address modes.
|
|
case IMM:
|
|
case IDX:
|
|
case IDY:
|
|
case REL:
|
|
case ZPG:
|
|
case ZPX:
|
|
case ZPY:
|
|
return 1;
|
|
|
|
// These two address modes have implied arguments; ACC is
|
|
// the accumulator, and IMP basically means it operates on
|
|
// some specific (presumably obvious) thing and no operand
|
|
// is necessary.
|
|
case ACC:
|
|
case IMP:
|
|
return 0;
|
|
}
|
|
|
|
// I don't know how we got here, outside of foul magicks and cruel
|
|
// trickery. Let's fearfully return zero!
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Scan memory (with a given address) and write the opcode at that
|
|
* point to the given file stream. This function will also write an
|
|
* operand to the file stream if one is warranted. We return the number
|
|
* of bytes consumed by scanning past the opcode and/or operand.
|
|
*/
|
|
int
|
|
mos6502_dis_opcode(mos6502 *cpu, FILE *stream, int address)
|
|
{
|
|
vm_8bit opcode;
|
|
vm_16bit operand;
|
|
int addr_mode;
|
|
int inst_code;
|
|
int expected;
|
|
|
|
// The next byte is assumed to be the opcode we work with.
|
|
opcode = vm_segment_get(cpu->memory, address);
|
|
|
|
// And given that opcode, we need to see how many bytes large our
|
|
// operand will be.
|
|
addr_mode = mos6502_addr_mode(opcode);
|
|
expected = mos6502_dis_expected_bytes(addr_mode);
|
|
|
|
// The specific instruction we mean to execute
|
|
inst_code = mos6502_instruction(opcode);
|
|
|
|
// The operand itself defaults to zero... in cases where this
|
|
// doesn't change, the instruction related to the opcode will
|
|
// probably not even use it.
|
|
operand = 0;
|
|
|
|
// And we need to skip ahead of the opcode.
|
|
address++;
|
|
|
|
switch (expected) {
|
|
case 2:
|
|
// Remember that the 6502 is little-endian, so the operand
|
|
// needs to be retrieved with the LSB first and the MSB
|
|
// second.
|
|
operand |= vm_segment_get(cpu->memory, address++);
|
|
operand |= vm_segment_get(cpu->memory, address++) << 8;
|
|
break;
|
|
|
|
case 1:
|
|
operand |= vm_segment_get(cpu->memory, address++);
|
|
break;
|
|
|
|
// And, in any other case (e.g. 0), we are done; we don't
|
|
// read anything, and we leave the operand as it is.
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// If the stream is NULL, we're doing some kind of lookahead.
|
|
// Furthermore, if this is an instruction that would switch control
|
|
// to a different spot in the program, then let's label this in the
|
|
// jump table.
|
|
if (stream == NULL && mos6502_would_jump(inst_code)) {
|
|
mos6502_dis_jump_label(cpu, operand, address, addr_mode);
|
|
}
|
|
|
|
// It's totally possible that we are not expected to print out the
|
|
// contents of our inspection of the opcode. (For example, we may
|
|
// just want to set the jump table in a lookahead operation.)
|
|
if (stream) {
|
|
// Hey! We might have a label at this position in the code. If
|
|
// so, let's print out the label.
|
|
if (jump_table[address]) {
|
|
// This will print out just the label itself.
|
|
mos6502_dis_label(stream, address);
|
|
|
|
// But to actually define the label, we need a colon to
|
|
// complete the notation. (We don't _need_ a newline, but it
|
|
// looks nicer to my arbitrary sensibilities. Don't @ me!)
|
|
fprintf(stream, ":\n");
|
|
}
|
|
|
|
// Let's print out to the stream what we have so far. First, we
|
|
// indent by four spaces.
|
|
fprintf(stream, " ");
|
|
|
|
// Print out the instruction code that our opcode represents.
|
|
mos6502_dis_instruction(stream, inst_code);
|
|
|
|
// Let's "tab" over; each instruction code is 3 characters, so let's
|
|
// move over 5 spaces (4 spaces indent + 1, just to keep everything
|
|
// aligned by 4-character boundaries).
|
|
fprintf(stream, " ");
|
|
|
|
if (expected) {
|
|
// Print out the operand given the proper address mode.
|
|
mos6502_dis_operand(cpu, stream, address, addr_mode, operand);
|
|
} else {
|
|
// Print out a tab to get a consistent look in our
|
|
// disassembled code (e.g. to take up the space that an
|
|
// operand would otherwise occupy).
|
|
fprintf(stream, "\t");
|
|
}
|
|
|
|
// Here we just want to show a few pieces of information; one,
|
|
// what the PC was at the point of this opcode sequence; two,
|
|
// the opcode;
|
|
fprintf(stream, "\t; pc=$%02x%02x cy=%02d: %02x",
|
|
cpu->PC >> 8, cpu->PC & 0xff,
|
|
mos6502_cycles(cpu, opcode), opcode);
|
|
|
|
// And three, the operand, if any. Remembering that the operand
|
|
// should be shown in little-endian order.
|
|
if (expected == 2) {
|
|
fprintf(stream, " %02x %02x", operand & 0xff, operand >> 8);
|
|
} else if (expected == 1) {
|
|
fprintf(stream, " %02x", operand & 0xff);
|
|
}
|
|
|
|
// And let's terminate the line.
|
|
fprintf(stream, "\n");
|
|
}
|
|
|
|
// The expected number of bytes here is for the operand, but we need
|
|
// to add one for the opcode to return the true number that this
|
|
// opcode sequence would consume.
|
|
return expected + 1;
|
|
}
|
|
|
|
/*
|
|
* Scan the CPU memory, from a given position until a given end, and
|
|
* print the results into a given file stream.
|
|
*/
|
|
void
|
|
mos6502_dis_scan(mos6502 *cpu, FILE *stream, int pos, int end)
|
|
{
|
|
while (pos < end) {
|
|
pos += mos6502_dis_opcode(cpu, stream, pos);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Associate a label with a given address or operand, depending on the
|
|
* address mode. For example, with REL, the jump label will be based on
|
|
* the address but added to or subtracted with the operand. Whereas in
|
|
* IND, the address is wholly dependent on the operand.
|
|
*/
|
|
void
|
|
mos6502_dis_jump_label(mos6502 *cpu,
|
|
vm_16bit operand,
|
|
int address,
|
|
int addr_mode)
|
|
{
|
|
int jump_loc;
|
|
|
|
switch (addr_mode) {
|
|
// With indirect address mode, the address we want to jump to is
|
|
// not the literal operand, but a 16-bit address that is
|
|
// _pointed to_ by the address represented by the operand. Think
|
|
// of the operand as a kind of double pointer, or just re-watch
|
|
// Inception.
|
|
case IND:
|
|
jump_loc = vm_segment_get(cpu->memory, operand) << 8;
|
|
jump_loc |= vm_segment_get(cpu->memory, operand + 1);
|
|
break;
|
|
|
|
// In relative address mode, the jump location will be a
|
|
// number -- well -- relative to the address. If the 8th bit
|
|
// of the operand is 1, then we treat the number as a
|
|
// negative; otherwise, positive or zero.
|
|
case REL:
|
|
jump_loc = address + operand;
|
|
|
|
if (operand > 127) {
|
|
jump_loc -= 256;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
|
|
jump_table[jump_loc] = 1;
|
|
}
|
|
|
|
/*
|
|
* Print out the form of our label to the given file stream. This is
|
|
* fairly dumb; it'll print out whatever address you give to it.
|
|
*/
|
|
inline void
|
|
mos6502_dis_label(FILE *stream, int address)
|
|
{
|
|
fprintf(stream, "ADDR_%x", address);
|
|
}
|
|
|
|
/*
|
|
* Remove the previously-set label in the jump table for a given
|
|
* address.
|
|
*/
|
|
inline void
|
|
mos6502_dis_jump_unlabel(int address)
|
|
{
|
|
jump_table[address] = 0;
|
|
}
|
|
|
|
/*
|
|
* Return true if the given address has a jump label associated with it.
|
|
*/
|
|
inline bool
|
|
mos6502_dis_is_jump_label(int address)
|
|
{
|
|
return jump_table[address] == 1;
|
|
}
|