├── .gitignore ├── LICENSE ├── Makefile ├── README.md └── dcc6502.c /.gitignore: -------------------------------------------------------------------------------- 1 | # Object files 2 | *.o 3 | *.ko 4 | *.obj 5 | *.elf 6 | 7 | # Libraries 8 | *.lib 9 | *.a 10 | 11 | # Shared objects (inc. Windows DLLs) 12 | *.dll 13 | *.so 14 | *.so.* 15 | *.dylib 16 | 17 | # Executables 18 | *.exe 19 | *.out 20 | *.app 21 | *.i*86 22 | *.x86_64 23 | *.hex 24 | /Default/ 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tennessee Carmel-Veilleux 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-O 3 | 4 | dcc6502: dcc6502.c 5 | $(CC) -o $@ $^ $(CFLAGS) 6 | 7 | clean: 8 | rm -f *.o dcc6502 dcc6502.exe 9 | 10 | all: dcc6502 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dcc6502 2 | ======= 3 | 4 | Disassembler for 6502 processors. 5 | 6 | # Features 7 | * Simple command-line interface 8 | * Single file, ANSI C source 9 | * Annotation for addresses of Nintendo Entertainment System (NES) system registers 10 | * Cycle-counting output 11 | * Machine code display inline with the disassembly 12 | 13 | # History tidbit 14 | The original 1.0 version of dcc6502 was written overnight on Christmas eve 15 | 1998. At the time, I (Tennessee Carmel-Veilleux) was a 16-year-old NES 16 | hacker learning 6502 assembly. Of course, as many teenagers are, I was 17 | a bit arrogant and really thought my code was pretty hot back then :) 18 | Fast-forward 15 years and I'm a grown-up engineer who is quite a bit more 19 | humble about his code. Looking back, I think the tool did the job, but 20 | obviously, 15 years of experience later, I would have made it quite a 21 | bit cleaner. The disassembler has floated online on miscalleanous NES 22 | development sites since 1998. I decided to put it on github starting at 23 | version 1.4 and I will be cleaning-up the code over until version 2.0. 24 | 25 | This disassembler has made the rounds and has been used for a lot of 26 | different purposes by many different people over the years. Hopefully 27 | it will continue to be useful going forward. 28 | 29 | -------------------------------------------------------------------------------- /dcc6502.c: -------------------------------------------------------------------------------- 1 | /********************************************************************************** 2 | * dcc6502.c -> Main module of: * 3 | * Disassembler and Cycle Counter for the 6502 microprocessor * 4 | * * 5 | * This code is offered under the MIT License (MIT) * 6 | * * 7 | * Copyright (c) 1998-2014 Tennessee Carmel-Veilleux * 8 | * * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy * 10 | * of this software and associated documentation files (the "Software"), to deal * 11 | * in the Software without restriction, including without limitation the rights * 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * 13 | * copies of the Software, and to permit persons to whom the Software is * 14 | * furnished to do so, subject to the following conditions: * 15 | * * 16 | * The above copyright notice and this permission notice shall be included in all * 17 | * copies or substantial portions of the Software. * 18 | * * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * 25 | * SOFTWARE. * 26 | **********************************************************************************/ 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | #define VERSION_INFO "v2.1" 35 | #define NUMBER_OPCODES 151 36 | 37 | /* Exceptions for cycle counting */ 38 | #define CYCLES_CROSS_PAGE_ADDS_ONE (1 << 0) 39 | #define CYCLES_BRANCH_TAKEN_ADDS_ONE (1 << 1) 40 | 41 | /* The 6502's 13 addressing modes */ 42 | typedef enum { 43 | IMMED = 0, /* Immediate */ 44 | ABSOL, /* Absolute */ 45 | ZEROP, /* Zero Page */ 46 | IMPLI, /* Implied */ 47 | INDIA, /* Indirect Absolute */ 48 | ABSIX, /* Absolute indexed with X */ 49 | ABSIY, /* Absolute indexed with Y */ 50 | ZEPIX, /* Zero page indexed with X */ 51 | ZEPIY, /* Zero page indexed with Y */ 52 | INDIN, /* Indexed indirect (with X) */ 53 | ININD, /* Indirect indexed (with Y) */ 54 | RELAT, /* Relative */ 55 | ACCUM /* Accumulator */ 56 | } addressing_mode_e; 57 | 58 | /** Some compilers don't have EOK in errno.h */ 59 | #ifndef EOK 60 | #define EOK 0 61 | #endif 62 | 63 | typedef struct opcode_s { 64 | uint8_t number; /* Number of the opcode */ 65 | const char *mnemonic; /* Index in the name table */ 66 | addressing_mode_e addressing; /* Addressing mode */ 67 | unsigned int cycles; /* Number of cycles */ 68 | unsigned int cycles_exceptions; /* Mask of cycle-counting exceptions */ 69 | } opcode_t; 70 | 71 | typedef struct options_s { 72 | char *filename; /* Input filename */ 73 | int nes_mode; /* 1 if NES commenting and warnings are enabled */ 74 | int cycle_counting; /* 1 if we want cycle counting */ 75 | int hex_output; /* 1 if hex dump output is desired at beginning of line */ 76 | unsigned long max_num_bytes; 77 | uint16_t org; /* Origin of addresses */ 78 | long offset; /* File offset to start disassembly from */ 79 | } options_t; 80 | 81 | /* Opcode table */ 82 | static opcode_t g_opcode_table[NUMBER_OPCODES] = { 83 | {0x69, "ADC", IMMED, 2, 0}, /* ADC */ 84 | {0x65, "ADC", ZEROP, 3, 0}, 85 | {0x75, "ADC", ZEPIX, 4, 0}, 86 | {0x6D, "ADC", ABSOL, 4, 0}, 87 | {0x7D, "ADC", ABSIX, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 88 | {0x79, "ADC", ABSIY, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 89 | {0x61, "ADC", INDIN, 6, 0}, 90 | {0x71, "ADC", ININD, 5, CYCLES_CROSS_PAGE_ADDS_ONE}, 91 | 92 | {0x29, "AND", IMMED, 2, 0}, /* AND */ 93 | {0x25, "AND", ZEROP, 3, 0}, 94 | {0x35, "AND", ZEPIX, 4, 0}, 95 | {0x2D, "AND", ABSOL, 4, 0}, 96 | {0x3D, "AND", ABSIX, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 97 | {0x39, "AND", ABSIY, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 98 | {0x21, "AND", INDIN, 6, 0}, 99 | {0x31, "AND", ININD, 5, CYCLES_CROSS_PAGE_ADDS_ONE}, 100 | 101 | {0x0A, "ASL", ACCUM, 2, 0}, /* ASL */ 102 | {0x06, "ASL", ZEROP, 5, 0}, 103 | {0x16, "ASL", ZEPIX, 6, 0}, 104 | {0x0E, "ASL", ABSOL, 6, 0}, 105 | {0x1E, "ASL", ABSIX, 7, 0}, 106 | 107 | {0x90, "BCC", RELAT, 2, CYCLES_CROSS_PAGE_ADDS_ONE | CYCLES_BRANCH_TAKEN_ADDS_ONE}, /* BCC */ 108 | 109 | {0xB0, "BCS", RELAT, 2, CYCLES_CROSS_PAGE_ADDS_ONE | CYCLES_BRANCH_TAKEN_ADDS_ONE}, /* BCS */ 110 | 111 | {0xF0, "BEQ", RELAT, 2, CYCLES_CROSS_PAGE_ADDS_ONE | CYCLES_BRANCH_TAKEN_ADDS_ONE}, /* BEQ */ 112 | 113 | {0x24, "BIT", ZEROP, 3, 0}, /* BIT */ 114 | {0x2C, "BIT", ABSOL, 4, 0}, 115 | 116 | {0x30, "BMI", RELAT, 2, CYCLES_CROSS_PAGE_ADDS_ONE | CYCLES_BRANCH_TAKEN_ADDS_ONE}, /* BMI */ 117 | 118 | {0xD0, "BNE", RELAT, 2, CYCLES_CROSS_PAGE_ADDS_ONE | CYCLES_BRANCH_TAKEN_ADDS_ONE}, /* BNE */ 119 | 120 | {0x10, "BPL", RELAT, 2, CYCLES_CROSS_PAGE_ADDS_ONE | CYCLES_BRANCH_TAKEN_ADDS_ONE}, /* BPL */ 121 | 122 | {0x00, "BRK", IMPLI, 7, 0}, /* BRK */ 123 | 124 | {0x50, "BVC", RELAT, 2, CYCLES_CROSS_PAGE_ADDS_ONE | CYCLES_BRANCH_TAKEN_ADDS_ONE}, /* BVC */ 125 | 126 | {0x70, "BVS", RELAT, 2, CYCLES_CROSS_PAGE_ADDS_ONE | CYCLES_BRANCH_TAKEN_ADDS_ONE}, /* BVS */ 127 | 128 | {0x18, "CLC", IMPLI, 2, 0}, /* CLC */ 129 | 130 | {0xD8, "CLD", IMPLI, 2, 0}, /* CLD */ 131 | 132 | {0x58, "CLI", IMPLI, 2, 0}, /* CLI */ 133 | 134 | {0xB8, "CLV", IMPLI, 2, 0}, /* CLV */ 135 | 136 | {0xC9, "CMP", IMMED, 2, 0}, /* CMP */ 137 | {0xC5, "CMP", ZEROP, 3, 0}, 138 | {0xD5, "CMP", ZEPIX, 4, 0}, 139 | {0xCD, "CMP", ABSOL, 4, 0}, 140 | {0xDD, "CMP", ABSIX, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 141 | {0xD9, "CMP", ABSIY, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 142 | {0xC1, "CMP", INDIN, 6, 0}, 143 | {0xD1, "CMP", ININD, 5, CYCLES_CROSS_PAGE_ADDS_ONE}, 144 | 145 | {0xE0, "CPX", IMMED, 2, 0}, /* CPX */ 146 | {0xE4, "CPX", ZEROP, 3, 0}, 147 | {0xEC, "CPX", ABSOL, 4, 0}, 148 | 149 | {0xC0, "CPY", IMMED, 2, 0}, /* CPY */ 150 | {0xC4, "CPY", ZEROP, 3, 0}, 151 | {0xCC, "CPY", ABSOL, 4, 0}, 152 | 153 | {0xC6, "DEC", ZEROP, 5, 0}, /* DEC */ 154 | {0xD6, "DEC", ZEPIX, 6, 0}, 155 | {0xCE, "DEC", ABSOL, 6, 0}, 156 | {0xDE, "DEC", ABSIX, 7, 0}, 157 | 158 | {0xCA, "DEX", IMPLI, 2, 0}, /* DEX */ 159 | 160 | {0x88, "DEY", IMPLI, 2, 0}, /* DEY */ 161 | 162 | {0x49, "EOR", IMMED, 2, 0}, /* EOR */ 163 | {0x45, "EOR", ZEROP, 3, 0}, 164 | {0x55, "EOR", ZEPIX, 4, 0}, 165 | {0x4D, "EOR", ABSOL, 4, 0}, 166 | {0x5D, "EOR", ABSIX, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 167 | {0x59, "EOR", ABSIY, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 168 | {0x41, "EOR", INDIN, 6, 1}, 169 | {0x51, "EOR", ININD, 5, CYCLES_CROSS_PAGE_ADDS_ONE}, 170 | 171 | {0xE6, "INC", ZEROP, 5, 0}, /* INC */ 172 | {0xF6, "INC", ZEPIX, 6, 0}, 173 | {0xEE, "INC", ABSOL, 6, 0}, 174 | {0xFE, "INC", ABSIX, 7, 0}, 175 | 176 | {0xE8, "INX", IMPLI, 2, 0}, /* INX */ 177 | 178 | {0xC8, "INY", IMPLI, 2, 0}, /* INY */ 179 | 180 | {0x4C, "JMP", ABSOL, 3, 0}, /* JMP */ 181 | {0x6C, "JMP", INDIA, 5, 0}, 182 | 183 | {0x20, "JSR", ABSOL, 6, 0}, /* JSR */ 184 | 185 | {0xA9, "LDA", IMMED, 2, 0}, /* LDA */ 186 | {0xA5, "LDA", ZEROP, 3, 0}, 187 | {0xB5, "LDA", ZEPIX, 4, 0}, 188 | {0xAD, "LDA", ABSOL, 4, 0}, 189 | {0xBD, "LDA", ABSIX, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 190 | {0xB9, "LDA", ABSIY, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 191 | {0xA1, "LDA", INDIN, 6, 0}, 192 | {0xB1, "LDA", ININD, 5, CYCLES_CROSS_PAGE_ADDS_ONE}, 193 | 194 | {0xA2, "LDX", IMMED, 2, 0}, /* LDX */ 195 | {0xA6, "LDX", ZEROP, 3, 0}, 196 | {0xB6, "LDX", ZEPIY, 4, 0}, 197 | {0xAE, "LDX", ABSOL, 4, 0}, 198 | {0xBE, "LDX", ABSIY, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 199 | 200 | {0xA0, "LDY", IMMED, 2, 0}, /* LDY */ 201 | {0xA4, "LDY", ZEROP, 3, 0}, 202 | {0xB4, "LDY", ZEPIX, 4, 0}, 203 | {0xAC, "LDY", ABSOL, 4, 0}, 204 | {0xBC, "LDY", ABSIX, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 205 | 206 | {0x4A, "LSR", ACCUM, 2, 0}, /* LSR */ 207 | {0x46, "LSR", ZEROP, 5, 0}, 208 | {0x56, "LSR", ZEPIX, 6, 0}, 209 | {0x4E, "LSR", ABSOL, 6, 0}, 210 | {0x5E, "LSR", ABSIX, 7, 0}, 211 | 212 | {0xEA, "NOP", IMPLI, 2, 0}, /* NOP */ 213 | 214 | {0x09, "ORA", IMMED, 2, 0}, /* ORA */ 215 | {0x05, "ORA", ZEROP, 3, 0}, 216 | {0x15, "ORA", ZEPIX, 4, 0}, 217 | {0x0D, "ORA", ABSOL, 4, 0}, 218 | {0x1D, "ORA", ABSIX, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 219 | {0x19, "ORA", ABSIY, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 220 | {0x01, "ORA", INDIN, 6, 0}, 221 | {0x11, "ORA", ININD, 5, CYCLES_CROSS_PAGE_ADDS_ONE}, 222 | 223 | {0x48, "PHA", IMPLI, 3, 0}, /* PHA */ 224 | 225 | {0x08, "PHP", IMPLI, 3, 0}, /* PHP */ 226 | 227 | {0x68, "PLA", IMPLI, 4, 0}, /* PLA */ 228 | 229 | {0x28, "PLP", IMPLI, 4, 0}, /* PLP */ 230 | 231 | {0x2A, "ROL", ACCUM, 2, 0}, /* ROL */ 232 | {0x26, "ROL", ZEROP, 5, 0}, 233 | {0x36, "ROL", ZEPIX, 6, 0}, 234 | {0x2E, "ROL", ABSOL, 6, 0}, 235 | {0x3E, "ROL", ABSIX, 7, 0}, 236 | 237 | {0x6A, "ROR", ACCUM, 2, 0}, /* ROR */ 238 | {0x66, "ROR", ZEROP, 5, 0}, 239 | {0x76, "ROR", ZEPIX, 6, 0}, 240 | {0x6E, "ROR", ABSOL, 6, 0}, 241 | {0x7E, "ROR", ABSIX, 7, 0}, 242 | 243 | {0x40, "RTI", IMPLI, 6, 0}, /* RTI */ 244 | 245 | {0x60, "RTS", IMPLI, 6, 0}, /* RTS */ 246 | 247 | {0xE9, "SBC", IMMED, 2, 0}, /* SBC */ 248 | {0xE5, "SBC", ZEROP, 3, 0}, 249 | {0xF5, "SBC", ZEPIX, 4, 0}, 250 | {0xED, "SBC", ABSOL, 4, 0}, 251 | {0xFD, "SBC", ABSIX, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 252 | {0xF9, "SBC", ABSIY, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 253 | {0xE1, "SBC", INDIN, 6, 0}, 254 | {0xF1, "SBC", ININD, 5, CYCLES_CROSS_PAGE_ADDS_ONE}, 255 | 256 | {0x38, "SEC", IMPLI, 2, 0}, /* SEC */ 257 | 258 | {0xF8, "SED", IMPLI, 2, 0}, /* SED */ 259 | 260 | {0x78, "SEI", IMPLI, 2, 0}, /* SEI */ 261 | 262 | {0x85, "STA", ZEROP, 3, 0}, /* STA */ 263 | {0x95, "STA", ZEPIX, 4, 0}, 264 | {0x8D, "STA", ABSOL, 4, 0}, 265 | {0x9D, "STA", ABSIX, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 266 | {0x99, "STA", ABSIY, 4, CYCLES_CROSS_PAGE_ADDS_ONE}, 267 | {0x81, "STA", INDIN, 6, 0}, 268 | {0x91, "STA", ININD, 5, CYCLES_CROSS_PAGE_ADDS_ONE}, 269 | 270 | {0x86, "STX", ZEROP, 3, 0}, /* STX */ 271 | {0x96, "STX", ZEPIY, 4, 0}, 272 | {0x8E, "STX", ABSOL, 4, 0}, 273 | 274 | {0x84, "STY", ZEROP, 3, 0}, /* STY */ 275 | {0x94, "STY", ZEPIX, 4, 0}, 276 | {0x8C, "STY", ABSOL, 4, 0}, 277 | 278 | {0xAA, "TAX", IMPLI, 2, 0}, /* TAX */ 279 | 280 | {0xA8, "TAY", IMPLI, 2, 0}, /* TAY */ 281 | 282 | {0xBA, "TSX", IMPLI, 2, 0}, /* TSX */ 283 | 284 | {0x8A, "TXA", IMPLI, 2, 0}, /* TXA */ 285 | 286 | {0x9A, "TXS", IMPLI, 2, 0}, /* TXS */ 287 | 288 | {0x98, "TYA", IMPLI, 2, 0} /* TYA */ 289 | }; 290 | 291 | /* This function emits a comment header with information about the file 292 | being disassembled */ 293 | static void emit_header(options_t *options, int fsize) { 294 | fprintf(stdout, "; Source generated by DCC6502 version %s\n", VERSION_INFO); 295 | fprintf(stdout, "; For more info about DCC6502, see https://github.com/tcarmelveilleux/dcc6502\n"); 296 | fprintf(stdout, "; FILENAME: %s, File Size: %d, ORG: $%04X\n", options->filename, fsize, options->org); 297 | if (options->hex_output) fprintf(stdout, "; -> Hex output enabled\n"); 298 | if (options->cycle_counting) fprintf(stdout, "; -> Cycle counting enabled\n"); 299 | if (options->nes_mode) fprintf(stdout, "; -> NES mode enabled\n"); 300 | fprintf(stdout, ";---------------------------------------------------------------------------\n"); 301 | } 302 | 303 | /* This function appends cycle counting to the comment block. See following 304 | * for methods used: 305 | * "Nick Bensema's Guide to Cycle Counting on the Atari 2600" 306 | * http://www.alienbill.com/2600/cookbook/cycles/nickb.txt 307 | */ 308 | static char *append_cycle(char *input, uint8_t entry, uint16_t pc, uint16_t new_pc) { 309 | char tmpstr[256]; 310 | int cycles = g_opcode_table[entry].cycles; 311 | int exceptions = g_opcode_table[entry].cycles_exceptions; 312 | int crosses_page = ((pc & 0xff00u) != (new_pc & 0xff00u)) ? 1 : 0; 313 | 314 | // On some exceptional conditions, instruction will take an extra cycle, or even two 315 | if (exceptions != 0) { 316 | if ((exceptions & CYCLES_BRANCH_TAKEN_ADDS_ONE) && (exceptions & CYCLES_CROSS_PAGE_ADDS_ONE)) { 317 | /* Branch case: check for page crossing, since it can be determined 318 | * statically from the relative offset and current PC. 319 | */ 320 | if (crosses_page) { 321 | /* Crosses page, always at least 1 extra cycle, two times */ 322 | sprintf(tmpstr, " Cycles: %d/%d", cycles + 1, cycles + 2); 323 | } else { 324 | /* Does not cross page, maybe one extra cycle if branch taken */ 325 | sprintf(tmpstr, " Cycles: %d/%d", cycles, cycles + 1); 326 | } 327 | } else { 328 | /* One exception: two times, can't tell in advance whether page crossing occurs */ 329 | sprintf(tmpstr, " Cycles: %d/%d", cycles, cycles + 1); 330 | } 331 | } else { 332 | /* No exceptions, no extra time */ 333 | sprintf(tmpstr, " Cycles: %d", cycles); 334 | } 335 | 336 | strcat(input, tmpstr); 337 | return (input + strlen(input)); 338 | } 339 | 340 | static void add_nes_str(char *instr, char *instr2) { 341 | strcat(instr, " [NES] "); 342 | strcat(instr, instr2); 343 | } 344 | 345 | /* This function put NES-specific info in the comment block */ 346 | static void append_nes(char *input, uint16_t arg) { 347 | switch(arg) { 348 | case 0x2000: add_nes_str(input, "PPU setup #1"); break; 349 | case 0x2001: add_nes_str(input, "PPU setup #2"); break; 350 | case 0x2002: add_nes_str(input, "PPU status"); break; 351 | case 0x2003: add_nes_str(input, "SPR-RAM address select"); break; 352 | case 0x2004: add_nes_str(input, "SPR-RAM data"); break; 353 | case 0x2005: add_nes_str(input, "PPU scroll"); break; 354 | case 0x2006: add_nes_str(input, "VRAM address select"); break; 355 | case 0x2007: add_nes_str(input, "VRAM data"); break; 356 | case 0x4000: add_nes_str(input, "Audio -> Square 1"); break; 357 | case 0x4001: add_nes_str(input, "Audio -> Square 1"); break; 358 | case 0x4002: add_nes_str(input, "Audio -> Square 1"); break; 359 | case 0x4003: add_nes_str(input, "Audio -> Square 1"); break; 360 | case 0x4004: add_nes_str(input, "Audio -> Square 2"); break; 361 | case 0x4005: add_nes_str(input, "Audio -> Square 2"); break; 362 | case 0x4006: add_nes_str(input, "Audio -> Square 2"); break; 363 | case 0x4007: add_nes_str(input, "Audio -> Square 2"); break; 364 | case 0x4008: add_nes_str(input, "Audio -> Triangle"); break; 365 | case 0x4009: add_nes_str(input, "Audio -> Triangle"); break; 366 | case 0x400a: add_nes_str(input, "Audio -> Triangle"); break; 367 | case 0x400b: add_nes_str(input, "Audio -> Triangle"); break; 368 | case 0x400c: add_nes_str(input, "Audio -> Noise control reg"); break; 369 | case 0x400e: add_nes_str(input, "Audio -> Noise Frequency reg #1"); break; 370 | case 0x400f: add_nes_str(input, "Audio -> Noise Frequency reg #2"); break; 371 | case 0x4010: add_nes_str(input, "Audio -> DPCM control"); break; 372 | case 0x4011: add_nes_str(input, "Audio -> DPCM D/A data"); break; 373 | case 0x4012: add_nes_str(input, "Audio -> DPCM address"); break; 374 | case 0x4013: add_nes_str(input, "Audio -> DPCM data length"); break; 375 | case 0x4014: add_nes_str(input, "Sprite DMA trigger"); break; 376 | case 0x4015: add_nes_str(input, "IRQ status / Sound enable"); break; 377 | case 0x4016: add_nes_str(input, "Joypad & I/O port for port #1"); break; 378 | case 0x4017: add_nes_str(input, "Joypad & I/O port for port #2"); break; 379 | } 380 | } 381 | 382 | /* Helper macros for disassemble() function */ 383 | #define DUMP_FORMAT (options->hex_output ? "%-16s%-16s;" : "%-8s%-16s;") 384 | #define HIGH_PART(val) (((val) >> 8) & 0xFFu) 385 | #define LOW_PART(val) ((val) & 0xFFu) 386 | #define LOAD_WORD(buffer, current_pc) ((uint16_t)buffer[(current_pc) + 1] | (((uint16_t)buffer[(current_pc) + 2]) << 8)) 387 | 388 | /* This function disassembles the opcode at the PC and outputs it in *output */ 389 | static void disassemble(char *output, uint8_t *buffer, options_t *options, uint16_t *pc) { 390 | char opcode_repr[256], hex_dump[256]; 391 | int opcode_idx; 392 | int len = 0; 393 | int entry = 0; 394 | int found = 0; 395 | uint8_t byte_operand; 396 | uint16_t word_operand = 0; 397 | uint16_t current_addr = *pc; 398 | uint8_t opcode = buffer[current_addr]; 399 | const char *mnemonic; 400 | 401 | opcode_repr[0] = '\0'; 402 | hex_dump[0] = '\0'; 403 | 404 | // Linear search for opcode 405 | for (opcode_idx = 0; opcode_idx < NUMBER_OPCODES; opcode_idx++) { 406 | if (opcode == g_opcode_table[opcode_idx].number) { 407 | /* Found the opcode, record its table index */ 408 | found = 1; 409 | entry = opcode_idx; 410 | } 411 | } 412 | 413 | // For opcode not found, terminate early 414 | if (!found) { 415 | sprintf(opcode_repr, ".byte $%02X", opcode); 416 | if (options->hex_output) { 417 | sprintf(hex_dump, "$%04X> %02X:", current_addr, opcode); 418 | sprintf(output, "%-16s%-16s; INVALID OPCODE !!!\n", hex_dump, opcode_repr); 419 | } else { 420 | sprintf(hex_dump, "$%04X", current_addr); 421 | sprintf(output, "%-8s%-16s; INVALID OPCODE !!!\n", hex_dump, opcode_repr); 422 | } 423 | return; 424 | } 425 | 426 | // Opcode found in table: disassemble properly according to addressing mode 427 | mnemonic = g_opcode_table[entry].mnemonic; 428 | 429 | // Set hex dump to default single address format. Will be overwritten 430 | // by more complex output in case of hex dump mode enabled 431 | sprintf(hex_dump, "$%04X", current_addr); 432 | 433 | switch (g_opcode_table[entry].addressing) { 434 | case IMMED: 435 | /* Get immediate value operand */ 436 | byte_operand = buffer[*pc + 1]; 437 | *pc += 1; 438 | 439 | sprintf(opcode_repr, "%s #$%02X", mnemonic, byte_operand); 440 | if (options->hex_output) { 441 | sprintf(hex_dump, "$%04X> %02X %02X:", current_addr, opcode, byte_operand); 442 | } 443 | 444 | break; 445 | case ABSOL: 446 | /* Get absolute address operand */ 447 | word_operand = LOAD_WORD(buffer, *pc); 448 | *pc += 2; 449 | 450 | sprintf(opcode_repr, "%s $%02X%02X", mnemonic, HIGH_PART(word_operand), LOW_PART(word_operand)); 451 | if (options->hex_output) { 452 | sprintf(hex_dump, "$%04X> %02X %02X%02X:", current_addr, opcode, LOW_PART(word_operand), HIGH_PART(word_operand)); 453 | } 454 | 455 | break; 456 | case ZEROP: 457 | /* Get zero page address */ 458 | byte_operand = buffer[*pc + 1]; 459 | *pc += 1; 460 | 461 | sprintf(opcode_repr, "%s $%02X", mnemonic, byte_operand); 462 | if (options->hex_output) { 463 | sprintf(hex_dump, "$%04X> %02X %02X:", current_addr, opcode, byte_operand); 464 | } 465 | 466 | break; 467 | case IMPLI: 468 | sprintf(opcode_repr, "%s", mnemonic); 469 | if (options->hex_output) { 470 | sprintf(hex_dump, "$%04X> %02X:", current_addr, opcode); 471 | } 472 | 473 | break; 474 | case INDIA: 475 | /* Get indirection address */ 476 | word_operand = LOAD_WORD(buffer, *pc); 477 | *pc += 2; 478 | 479 | sprintf(opcode_repr, "%s ($%02X%02X)", mnemonic, HIGH_PART(word_operand), LOW_PART(word_operand)); 480 | if (options->hex_output) { 481 | sprintf(hex_dump, "$%04X> %02X %02X%02X:", current_addr, opcode, LOW_PART(word_operand), HIGH_PART(word_operand)); 482 | } 483 | 484 | break; 485 | case ABSIX: 486 | /* Get base address */ 487 | word_operand = LOAD_WORD(buffer, *pc); 488 | *pc += 2; 489 | 490 | sprintf(opcode_repr, "%s $%02X%02X,X", mnemonic, HIGH_PART(word_operand), LOW_PART(word_operand)); 491 | if (options->hex_output) { 492 | sprintf(hex_dump, "$%04X> %02X %02X%02X:", current_addr, opcode, LOW_PART(word_operand), HIGH_PART(word_operand)); 493 | } 494 | 495 | break; 496 | case ABSIY: 497 | /* Get baser address */ 498 | word_operand = LOAD_WORD(buffer, *pc); 499 | *pc += 2; 500 | 501 | sprintf(opcode_repr, "%s $%02X%02X,Y", mnemonic, HIGH_PART(word_operand), LOW_PART(word_operand)); 502 | if (options->hex_output) { 503 | sprintf(hex_dump, "$%04X> %02X %02X%02X:", current_addr, opcode, LOW_PART(word_operand), HIGH_PART(word_operand)); 504 | } 505 | 506 | break; 507 | case ZEPIX: 508 | /* Get zero-page base address */ 509 | byte_operand = buffer[*pc + 1]; 510 | *pc += 1; 511 | 512 | sprintf(opcode_repr, "%s $%02X,X", mnemonic, byte_operand); 513 | if (options->hex_output) { 514 | sprintf(hex_dump, "$%04X> %02X %02X:", current_addr, opcode, byte_operand); 515 | } 516 | 517 | break; 518 | case ZEPIY: 519 | /* Get zero-page base address */ 520 | byte_operand = buffer[*pc + 1]; 521 | *pc += 1; 522 | 523 | sprintf(opcode_repr, "%s $%02X,Y", mnemonic, byte_operand); 524 | if (options->hex_output) { 525 | sprintf(hex_dump, "$%04X> %02X %02X:", current_addr, opcode, byte_operand); 526 | } 527 | 528 | break; 529 | case INDIN: 530 | /* Get zero-page base address */ 531 | byte_operand = buffer[*pc + 1]; 532 | *pc += 1; 533 | 534 | sprintf(opcode_repr, "%s ($%02X,X)", mnemonic, byte_operand); 535 | if (options->hex_output) { 536 | sprintf(hex_dump, "$%04X> %02X %02X:", current_addr, opcode, byte_operand); 537 | } 538 | 539 | break; 540 | case ININD: 541 | /* Get zero-page base address */ 542 | byte_operand = buffer[*pc + 1]; 543 | *pc += 1; 544 | 545 | sprintf(opcode_repr, "%s ($%02X),Y", mnemonic, byte_operand); 546 | if (options->hex_output) { 547 | sprintf(hex_dump, "$%04X> %02X %02X:", current_addr, opcode, byte_operand); 548 | } 549 | 550 | break; 551 | case RELAT: 552 | /* Get relative modifier */ 553 | byte_operand = buffer[*pc + 1]; 554 | *pc += 1; 555 | 556 | // Compute displacement from first byte after full instruction. 557 | word_operand = current_addr + 2; 558 | if (byte_operand > 0x7Fu) { 559 | word_operand -= ((~byte_operand & 0x7Fu) + 1); 560 | } else { 561 | word_operand += byte_operand & 0x7Fu; 562 | } 563 | 564 | sprintf(opcode_repr, "%s $%04X", mnemonic, word_operand); 565 | if (options->hex_output) { 566 | sprintf(hex_dump, "$%04X> %02X %02X:", current_addr, opcode, byte_operand); 567 | } 568 | 569 | break; 570 | case ACCUM: 571 | sprintf(opcode_repr, "%s A", mnemonic); 572 | if (options->hex_output) { 573 | sprintf(hex_dump, "$%04X> %02X:", current_addr, opcode); 574 | } 575 | 576 | break; 577 | default: 578 | // Will not happen since each entry in opcode_table has address mode set 579 | break; 580 | } 581 | 582 | // Emit disassembly line content, prior to annotation comments 583 | len = sprintf(output, DUMP_FORMAT, hex_dump, opcode_repr); 584 | output += len; 585 | 586 | /* Add cycle count if necessary */ 587 | if (options->cycle_counting) { 588 | output = append_cycle(output, entry, *pc + 1, word_operand); 589 | } 590 | 591 | /* Add NES port info if necessary */ 592 | switch (g_opcode_table[entry].addressing) { 593 | case ABSOL: 594 | case ABSIX: 595 | case ABSIY: 596 | if (options->nes_mode) { 597 | append_nes(output, word_operand); 598 | } 599 | break; 600 | default: 601 | /* Other addressing modes: not enough info to add NES register annotation */ 602 | break; 603 | } 604 | } 605 | 606 | static void version(void) { 607 | fprintf(stderr, "DCC6502 %s (C)1998-2014 Tennessee Carmel-Veilleux \n", VERSION_INFO); 608 | fprintf(stderr, "This software is licensed under the MIT license. See the LICENSE file.\n"); 609 | fprintf(stderr, "See source on github: https://github.com/tcarmelveilleux/dcc6502.\n"); 610 | } 611 | 612 | static void usage(void) { 613 | fprintf(stderr, "\nUsage: dcc6502 [options] FILENAME\n"); 614 | fprintf(stderr, " -?/-h : Show this help message\n"); 615 | fprintf(stderr, " -o ORIGIN : Set the origin (base address of disassembly) [default: 0x8000]\n"); 616 | fprintf(stderr, " -m NUM_BYTES : Only disassemble the first NUM_BYTES bytes\n"); 617 | fprintf(stderr, " -s NUM_BYTES : Disassemble after skipping NUM_BYTES from start of input file\n"); 618 | fprintf(stderr, " -d : Enable hex dump within disassembly\n"); 619 | fprintf(stderr, " -n : Enable NES register annotations\n"); 620 | fprintf(stderr, " -v : Get only version information\n"); 621 | fprintf(stderr, " -c : Enable cycle counting annotations\n"); 622 | fprintf(stderr, "\n"); 623 | } 624 | 625 | static int str_arg_to_ulong(char *str, unsigned long *value) { 626 | uint32_t tmp = 0; 627 | char *endptr; 628 | 629 | errno = EOK; 630 | tmp = strtoul(str, &endptr, 0); 631 | /* In case of conversion error, return error indication */ 632 | if ((EOK != errno) || (*endptr != '\0')) { 633 | return 0; 634 | } else { 635 | *value = tmp; 636 | return 1; 637 | } 638 | } 639 | 640 | static void usage_and_exit(int exit_code, const char *message) { 641 | version(); 642 | usage(); 643 | if (NULL != message) { 644 | fprintf(stderr, "%s\n", message); 645 | } 646 | exit(exit_code); 647 | } 648 | 649 | static void parse_args(int argc, char *argv[], options_t *options) { 650 | int arg_idx = 1; 651 | unsigned long tmp_value; 652 | 653 | options->cycle_counting = 0; 654 | options->hex_output = 0; 655 | options->nes_mode = 0; 656 | options->org = 0x8000; 657 | options->max_num_bytes = 65536; 658 | options->offset = 0; 659 | 660 | while (arg_idx < argc) { 661 | /* First non-dash-starting argument is assumed to be filename */ 662 | if (argv[arg_idx][0] != '-') { 663 | break; 664 | } 665 | 666 | /* Got a switch, process it */ 667 | switch (argv[arg_idx][1]) { 668 | case 'h': 669 | case '?': 670 | usage_and_exit(0, NULL); 671 | break; 672 | case 'n': 673 | options->nes_mode = 1; 674 | break; 675 | case 'c': 676 | options->cycle_counting = 1; 677 | break; 678 | case 'd': 679 | options->hex_output = 1; 680 | break; 681 | case 'v': 682 | version(); 683 | exit(0); 684 | break; 685 | case 'o': 686 | if ((arg_idx == (argc - 1)) || (argv[arg_idx + 1][0] == '-')) { 687 | usage_and_exit(1, "Missing argument to -o switch"); 688 | } 689 | 690 | /* Get argument and parse it */ 691 | arg_idx++; 692 | if (!str_arg_to_ulong(argv[arg_idx], &tmp_value)) { 693 | usage_and_exit(1, "Invalid argument to -o switch"); 694 | } 695 | options->org = (uint16_t)(tmp_value & 0xFFFFu); 696 | break; 697 | case 'm': 698 | if ((arg_idx == (argc - 1)) || (argv[arg_idx + 1][0] == '-')) { 699 | usage_and_exit(1, "Missing argument to -m switch"); 700 | } 701 | 702 | /* Get argument and parse it */ 703 | arg_idx++; 704 | if (!str_arg_to_ulong(argv[arg_idx], &tmp_value)) { 705 | usage_and_exit(1, "Invalid argument to -m switch"); 706 | } 707 | options->max_num_bytes = tmp_value; 708 | break; 709 | case 's': 710 | if ((arg_idx == (argc - 1)) || (argv[arg_idx + 1][0] == '-')) { 711 | usage_and_exit(1, "Missing argument to -s switch"); 712 | } 713 | /* Get argument and parse it */ 714 | arg_idx++; 715 | if (!str_arg_to_ulong(argv[arg_idx], &tmp_value)) { 716 | usage_and_exit(1, "Invalid argument to -s switch"); 717 | } 718 | options->offset = (long)tmp_value; 719 | break; 720 | default: 721 | version(); 722 | usage(); 723 | fprintf(stderr, "Unrecognized switch: %s\n", argv[arg_idx]); 724 | exit(1); 725 | } 726 | arg_idx++; 727 | } 728 | 729 | /* Make sure we have a filename left to take after we stopped parsing switches */ 730 | if (arg_idx >= argc) { 731 | usage_and_exit(1, "Missing filename from command line"); 732 | } 733 | 734 | options->filename = argv[arg_idx]; 735 | } 736 | 737 | int main(int argc, char *argv[]) { 738 | int byte_count = 0; 739 | char tmpstr[512]; 740 | uint8_t *buffer; /* Memory buffer */ 741 | FILE *input_file; /* Input file */ 742 | uint16_t pc; /* Program counter */ 743 | options_t options; /* Command-line options parsing results */ 744 | int result = 0; 745 | 746 | parse_args(argc, argv, &options); 747 | 748 | buffer = calloc(1, 65536); 749 | if (NULL == buffer) { 750 | usage_and_exit(3, "Could not allocate disassembly memory buffer."); 751 | } 752 | 753 | /* Read file into memory buffer */ 754 | input_file = fopen(options.filename, "rb"); 755 | 756 | if (NULL == input_file) { 757 | version(); 758 | fprintf(stderr, "File not found or invalid filename : %s\n", options.filename); 759 | exit(2); 760 | } 761 | 762 | if (options.offset) { 763 | result = fseek(input_file, options.offset, SEEK_SET); 764 | if (result < 0) { 765 | fprintf(stderr, "fseek(%s, %ld, SEEK_SET) failed: %s (%d)\n", options.filename, options.offset, strerror(errno), result); 766 | exit(2); 767 | } 768 | } 769 | 770 | byte_count = 0; 771 | while(!feof(input_file) && ((options.org + byte_count) <= 0xFFFFu) && (byte_count < options.max_num_bytes)) { 772 | size_t bytes_read = fread(&buffer[options.org + byte_count], 1, 1, input_file); 773 | byte_count += bytes_read; 774 | } 775 | 776 | fclose(input_file); 777 | 778 | /* Disassemble contents of buffer */ 779 | emit_header(&options, byte_count); 780 | pc = options.org; 781 | while((pc <= 0xFFFFu) && ((pc - options.org) < byte_count)) { 782 | disassemble(tmpstr, buffer, &options, &pc); 783 | fprintf(stdout, "%s\n", tmpstr); 784 | pc++; 785 | } 786 | 787 | free(buffer); 788 | 789 | return 0; 790 | } 791 | --------------------------------------------------------------------------------