Я продолжаю изучать ассемблер 6502, но для экспериментов мне понадобился дизассемблер, Я пробовал использовать da65 собственно тот что идет вместе с ассемблером и линкером ca65 и ld65. Но заметив в документации коды команд в hex представление. И вдруг понял что если прочитать файл nes то можно просто взять код инструкции, взять ее длину и спарсить аргумент. И мы получим дизассемблированный код в его простом представление.

Первым делом надо сформировать список всех команд и их опкодов. Список я взял из таблиц в документации http://emuverse.ru/wiki/MOS_Technology_6502/Система_команд#ADC немного ручной работы и небольшой скрипт и как результат был сформирован массив команд с которым можно работать.

Массив с hex кодом команды, шаблоном и длиной
<?php

return [
    '69' => [
        'ADC #$argument',
        2
    ], '65' => [
        'ADC $argument',
        2
    ], '75' => [
        'ADC $argument,X',
        2
    ], '6D' => [
        'ADC $argument',
        3
    ], '7D' => [
        'ADC $argument,X',
        3
    ], '79' => [
        'ADC $argument,Y',
        3
    ], '61' => [
        'ADC ($argument,X)',
        2
    ], '71' => [
        'ADC ($argument),Y',
        2
    ], '29' => [
        'AND #$argument',
        2
    ], '25' => [
        'AND $argument',
        2
    ], '35' => [
        'AND $argument,X',
        2
    ], '2D' => [
        'AND $argument',
        3
    ], '3D' => [
        'AND $argument,X',
        3
    ], '39' => [
        'AND $argument,Y',
        3
    ], '21' => [
        'AND ($argument,X)',
        2
    ], '31' => [
        'AND ($argument),Y',
        2
    ], '0A' => [
        'ASLA',
        1
    ], '06' => [
        'ASL $argument',
        2
    ], '16' => [
        'ASL $argument,X',
        2
    ], '0E' => [
        'ASL $argument',
        3
    ], '1E' => [
        'ASL $argument,X',
        3
    ], '90' => [
        'BCC $argument',
        2
    ], 'B0' => [
        'BCS $argument',
        2
    ], 'F0' => [
        'BEQ $argument',
        2
    ], '24' => [
        'BIT $argument',
        2
    ], '2C' => [
        'BIT $argument',
        3
    ], '30' => [
        'BMI $argument',
        2
    ], 'D0' => [
        'BNE $argument',
        2
    ], '10' => [
        'BPL $argument',
        2
    ], '00' => [
        'BRK',
        1
    ], '50' => [
        'BVC $argument',
        2
    ], '70' => [
        'BVS $argument',
        2
    ], '18' => [
        'CLC',
        1
    ], 'D8' => [
        'CLD',
        1
    ], '58' => [
        'CLI',
        1
    ], 'B8' => [
        'CLV',
        1
    ], 'C9' => [
        'CMP #$argument',
        2
    ], 'C5' => [
        'CMP $argument',
        2
    ], 'D5' => [
        'CMP $argument,X',
        2
    ], 'CD' => [
        'CMP $argument',
        3
    ], 'DD' => [
        'CMP $argument,X',
        3
    ], 'D9' => [
        'CMP $argument,Y',
        3
    ], 'C1' => [
        'CMP ($argument,X)',
        2
    ], 'D1' => [
        'CMP ($argument),Y',
        2
    ], 'E0' => [
        'CPX $argument',
        2
    ], 'E4' => [
        'CPX $argument',
        2
    ], 'EC' => [
        'CPX $argument',
        3
    ], 'C0' => [
        'CPY $argument',
        2
    ], 'C4' => [
        'CPY $argument',
        2
    ], 'CC' => [
        'CPY $argument',
        3
    ], 'C6' => [
        'DEC $argument',
        2
    ], 'D6' => [
        'DEC $argument,X',
        2
    ], 'CE' => [
        'DEC $argument',
        3
    ], 'DE' => [
        'DEC $argument,X',
        3
    ], 'CA' => [
        'DEX',
        1
    ], '88' => [
        'DEY',
        1
    ], '49' => [
        'EOR #$argument',
        2
    ], '45' => [
        'EOR $argument',
        2
    ], '55' => [
        'EOR $argument,X',
        2
    ], '4D' => [
        'EOR $argument',
        3
    ], '5D' => [
        'EOR $argument,X',
        3
    ], '59' => [
        'EOR $argument,Y',
        3
    ], '41' => [
        'EOR ($argument,X)',
        2
    ], '51' => [
        'EOR ($argument),Y',
        2
    ], 'E6' => [
        'INC $argument',
        2
    ], 'F6' => [
        'INC $argument,X',
        2
    ], 'EE' => [
        'INC $argument',
        3
    ], 'FE' => [
        'INC $argument,X',
        3
    ], 'E8' => [
        'INX',
        1
    ], 'C8' => [
        'INY',
        1
    ], '4C' => [
        'JMP $argument',
        3
    ], '6C' => [
        'JMP ($argument)',
        3
    ], '20' => [
        'JSR $argument',
        3
    ], 'A9' => [
        'LDA #$argument',
        2
    ], 'A5' => [
        'LDA $argument',
        2
    ], 'B5' => [
        'LDA $argument,X',
        2
    ], 'AD' => [
        'LDA $argument',
        3
    ], 'BD' => [
        'LDA $argument,X',
        3
    ], 'B9' => [
        'LDA $argument,Y',
        3
    ], 'A1' => [
        'LDA ($argument,X)',
        2
    ], 'B1' => [
        'LDA ($argument),Y',
        2
    ], 'A2' => [
        'LDX #$argument',
        2
    ], 'A6' => [
        'LDX $argument',
        2
    ], 'B6' => [
        'LDX $argument,Y',
        2
    ], 'AE' => [
        'LDX $argument',
        3
    ], 'BE' => [
        'LDX $argument,Y',
        3
    ], 'A0' => [
        'LDY #$argument',
        2
    ], 'A4' => [
        'LDY $argument',
        2
    ], 'B4' => [
        'LDY $argument,X',
        2
    ], 'AC' => [
        'LDY $argument',
        3
    ], 'BC' => [
        'LDY $argument,X',
        3
    ], '4A' => [
        'LSRA',
        1
    ], '46' => [
        'LSR $argument',
        2
    ], '56' => [
        'LSR $argument,X',
        2
    ], '4E' => [
        'LSR $argument',
        3
    ], '5E' => [
        'LSR $argument,X',
        3
    ], 'EA' => [
        'NOP',
        1
    ], '09' => [
        'ORA #$argument',
        2
    ], '05' => [
        'ORA $argument',
        2
    ], '15' => [
        'ORA $argument,X',
        2
    ], '0D' => [
        'ORA $argument',
        3
    ], '1D' => [
        'ORA $argument,X',
        3
    ], '19' => [
        'ORA $argument,Y',
        3
    ], '01' => [
        'ORA ($argument,X)',
        2
    ], '11' => [
        'ORA ($argument),Y',
        2
    ], '48' => [
        'PHA',
        1
    ], '08' => [
        'PHP',
        1
    ], '68' => [
        'PLA',
        1
    ], '28' => [
        'PLP',
        1
    ], '2A' => [
        'ROLA',
        1
    ], '26' => [
        'ROL $argument',
        2
    ], '36' => [
        'ROL $argument,X',
        2
    ], '2E' => [
        'ROL $argument',
        3
    ], '3E' => [
        'ROL $argument,X',
        3
    ], '6A' => [
        'RORA',
        1
    ], '66' => [
        'ROR $argument',
        2
    ], '76' => [
        'ROR $argument,X',
        2
    ], '6E' => [
        'ROR $argument',
        3
    ], '7E' => [
        'ROR $argument,X',
        3
    ], '40' => [
        'RTI',
        1
    ], '60' => [
        'RTS',
        1
    ], 'E9' => [
        'SBC #$argument',
        2
    ], 'E5' => [
        'SBC $argument',
        2
    ], 'F5' => [
        'SBC $argument,X',
        2
    ], 'ED' => [
        'SBC $argument',
        3
    ], 'FD' => [
        'SBC $argument,X',
        3
    ], 'F9' => [
        'SBC $argument,Y',
        3
    ], 'E1' => [
        'SBC ($argument,X)',
        2
    ], 'F1' => [
        'SBC ($argument),Y',
        2
    ], '38' => [
        'SEC',
        1
    ], 'F8' => [
        'SED',
        1
    ], '78' => [
        'SEI',
        1
    ], '85' => [
        'STA $argument',
        2
    ], '95' => [
        'STA $argument,X',
        2
    ], '8D' => [
        'STA $argument',
        3
    ], '9D' => [
        'STA $argument,X',
        3
    ], '99' => [
        'STA $argument,Y',
        3
    ], '81' => [
        'STA ($argument,X)',
        2
    ], '91' => [
        'STA ($argument),Y',
        2
    ], '86' => [
        'STX $argument',
        2
    ], '96' => [
        'STX $argument,Y',
        2
    ], '8E' => [
        'STX $argument',
        3
    ], '84' => [
        'STY $argument',
        2
    ], '94' => [
        'STY $argument,X',
        2
    ], '8C' => [
        'STY $argument',
        3
    ], 'AA' => [
        'TAX',
        1
    ], 'A8' => [
        'TAY',
        1
    ], 'BA' => [
        'TSX',
        1
    ], '8A' => [
        'TXA',
        1
    ], '9A' => [
        'TXS',
        1
    ], '98' => [
        'TYA',
        1
    ],
];

Тут я должен сказать пару слов о длине команды, она зависит от адресации допустим если абсолютная адресация то длина будет 3, если команда без аргументов 1, в остальных случаях длина будет 2 байта. К примеру

LDA $1234 ; AD 34 12 - hex последовательность 3 байта
          ; команда и аргумент (младший и старший байт)
LDA #$0A ; A9 0A - hex последовательность 2 байт сама команда и аргумент
TAX ; AA - комманда без аргумента и занимает 1 байт

И так с этим разобрались. Далее нам необходимо прочитать файл nes, это делается просто через file_get_contents в php, далее приводим бинарные данные в 16-ричную строку bin2hex. И разбиваем строку на 2 символа в массив инструкцией str_split. Далее последовательно идем перебирая каждый байт. И сравниваем с инструкциями которые загруженны в переменную. И относительно длины формируем значение аргумента инструкции.

И здесь Я понял довольно важную вещь, файл iNES содержит не только код но и заголовки, информацию о мапере (512кб если есть мапер), код и графику. Это основные секции кода в файле. И теперь для того что бы корректно прочитать код нам не обходимо игнорировать 16 первых байт, 512 байт пока не учитываем (подопытный будет supper mario bros 2 который не имеет маппера), далее берем только код в размере 16384 байта и уже проходя скриптом заменяем байткоды соответствиями команд.

Скрипт мини-дизассемблер под спойлером

Код дизассемблера 6502
<?php
/**
 * Created by PhpStorm.
 * User: roman
 * Date: 11.06.2023
 * Time: 22:02
 */

class Disassembler
{
    private $instructions = [];

    public function __construct()
    {
        $this->instructions = include 'opcodes.php';
    }

    public function disassembly($code)
    {
        $stringCode = bin2hex($code);
        $arrayInstructions = str_split($stringCode, 2);
        $skipKeys = 0;
        $resultString = '.headers ';

        foreach ($arrayInstructions as $_key => $_value) {
            // 16 byte nes header
            if ($_key < 16) {
                $resultString .= ' ' . $arrayInstructions[$_key];
                continue;
            }

            if ($_key == 16) {
                $resultString .= PHP_EOL . '.code ' . PHP_EOL;
            }

            if ($_key == 16384 + 16) {
                break;
            }

            $value = null;
            $capitalKey = strtoupper($_value);

            if ($skipKeys) {
                $skipKeys--;
                continue;
            }

            if (isset($this->instructions[$capitalKey])) {
                if (
                    $this->instructions[$capitalKey][1] == 3
                ) {
                    $value = $arrayInstructions[$_key+2] . $arrayInstructions[$_key+1];
                    $resultString .= str_replace('$argument', '$' . $value, $this->instructions[$capitalKey][0]) . PHP_EOL;
                    $skipKeys = 2;
                } elseif (
                    $this->instructions[$capitalKey][1] = 2
                ) {
                    $value = $arrayInstructions[$_key + 1];
                    $skipKeys = 1;
                    $resultString .= str_replace('$argument', '$' . $value, $this->instructions[$capitalKey][0]) . PHP_EOL;
                } else {
                    $resultString .= $this->instructions[$capitalKey][0] . PHP_EOL;
                }
            }
        }

        file_put_contents('result.asm', $resultString);
    }
}

$binary = file_get_contents('../smb.nes');
$disasm = new Disassembler();
$disasm->disassembly($binary);

Теперь остается проверить лишь его работоспособность, для этого запускаем ld65 по файлу smb.nes а так же наш скрипт результат выполнения следующий.

ld65 дизасемблер
ld65 дизасемблер
наш мини-дизассемблер
наш мини-дизассемблер

Как вы видите код идентичный да da65 генерирует метки для перехода и ZeroPage. Но мы пока разбираемся в корне механизма дизассемблирования.

В плане написать скрипт ассемблера на php из кода который был дизассемблирован. Это даст чуть более широкие возможности в ром хакинге не просто поменять какой то ресурс а изменить логику. Не так давно Я изменил логику в игре Dick Tracy там при стрельбе если у врага нет оружия отнимается хит у игрока. И пришлось изменить только аргумент вместо того что бы убрать вызов сабрутины отнимающего жизнь у игрока.

В качестве заключения приведу ссылку на документацию которую я использовал при написание скрипта:

Комментарии (9)


  1. SIISII
    11.06.2023 23:11
    +2

    В порядке придирок -- весьма и весьма много ошибок в русском языке, начиная с экспЕрИмента.

    По существу вопроса: как минимум, адреса команд стоило б выводить в шестнадцатеричном виде, при реальной работе это обычно намного удобней, чем в десятичном.


    1. himysay Автор
      11.06.2023 23:11

      В упор не вижу где в десятичном виде адреса команд?


      1. SIISII
        11.06.2023 23:11

        "Наш дизассемблер" -- справа столбец адресов в явно десятичном виде: 6099, за ним 6100 и т.д.

        ADD. А, блин, это я ступил: это номера строк, а не адреса. Ну, тогда адреса надо б добавить.


        1. himysay Автор
          11.06.2023 23:11
          +1

          Да именно просто номер строки в пхп шторме, а так как минимум внутренний указатель адреса должен быть в любом случае, но хочу прежде написать простой скрипт ассемблера.


  1. FSA
    11.06.2023 23:11
    +1

    Разобрать код 8 битного процессора не такая уж и сложная задача. Я, например, изучал в детстве ассемблер Z80. Сначала казалось странным такие расположение команд. Но потом до меня дошло, что просто код команды записан в определённых битах команды. Главное эту логику уловить, если у тебя просто есть набор команд. Но с Z80 посложнее было, потому что были и двухбайтовые команды.


  1. Lord_Ahriman
    11.06.2023 23:11
    +1

    Так ведь есть уже несколько, которыми ромхакеры и сценеры пользуются, и даже, НЯП, плагин к Иде есть, причем там функционал весьма приличный. Целью было просто собственное саморазвитие? И сравнивали ли вы вашу разработку с другими дизасмами?


    1. himysay Автор
      11.06.2023 23:11
      +1

      Больше саморазвитие


  1. Fockker
    11.06.2023 23:11

    Спасибо, очень интересно!

    Немного покоробило include 'opcodes.php'; в конструкторе, я бы конечно загружал отдельно. Это, все-таки, жесткая зависимость, а их в объектах следует избегать. Ну и чисто на рефлексах добавил бы __DIR__ к include.

    И примерно из той же серии мелкое замечание про file_put_contents прямо внутри метод disassembly(). Всё-таки, лучше это делать снаружи :) Или добавил два метода в класс, load() и write()

    Ну и сам код я бы чуть-чуть оптимизировал,

    if (isset($this->instructions[$capitalKey])) {
        $skipKeys = $this->instructions[$capitalKey][1] - 1;
        if ($skipKeys) {
            $value = ($skipKeys == 2) ? "" : $arrayInstructions[$_key + 2];
            $value .=  $arrayInstructions[$_key + 1];
            $resultString .= str_replace('$argument', '$' . $value, $this->instructions[$capitalKey][0]) . PHP_EOL;
        } else {
            $resultString .= $this->instructions[$capitalKey][0] . PHP_EOL;
        }
    } else {
        trigger_error("Incorrect instruction $capitalKey", E_USER_ERROR);
    }
    

    Хотя не уверен, стоит ли


    1. himysay Автор
      11.06.2023 23:11

      Вам спасибо за замечание, но тут ключевое функционал а не перфекционизм, можно конечно всё по солиду разложить, сделать фабричный метод, и DI это Я сделаю чуть позже, в планах сделать полноценный дизассемблер с поддержкой маперов. Что касается обработки ошибки, там пока в ней нет необходимости.