Я продолжаю изучать ассемблер 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 а так же наш скрипт результат выполнения следующий.
Как вы видите код идентичный да da65 генерирует метки для перехода и ZeroPage. Но мы пока разбираемся в корне механизма дизассемблирования.
В плане написать скрипт ассемблера на php из кода который был дизассемблирован. Это даст чуть более широкие возможности в ром хакинге не просто поменять какой то ресурс а изменить логику. Не так давно Я изменил логику в игре Dick Tracy там при стрельбе если у врага нет оружия отнимается хит у игрока. И пришлось изменить только аргумент вместо того что бы убрать вызов сабрутины отнимающего жизнь у игрока.
В качестве заключения приведу ссылку на документацию которую я использовал при написание скрипта:
http://emuverse.ru/wiki/MOS_Technology_6502/Система_команд#ADC - документация по командам 6502
Комментарии (9)
FSA
11.06.2023 23:11+1Разобрать код 8 битного процессора не такая уж и сложная задача. Я, например, изучал в детстве ассемблер Z80. Сначала казалось странным такие расположение команд. Но потом до меня дошло, что просто код команды записан в определённых битах команды. Главное эту логику уловить, если у тебя просто есть набор команд. Но с Z80 посложнее было, потому что были и двухбайтовые команды.
Lord_Ahriman
11.06.2023 23:11+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); }
Хотя не уверен, стоит ли
himysay Автор
11.06.2023 23:11Вам спасибо за замечание, но тут ключевое функционал а не перфекционизм, можно конечно всё по солиду разложить, сделать фабричный метод, и DI это Я сделаю чуть позже, в планах сделать полноценный дизассемблер с поддержкой маперов. Что касается обработки ошибки, там пока в ней нет необходимости.
SIISII
В порядке придирок -- весьма и весьма много ошибок в русском языке, начиная с экспЕрИмента.
По существу вопроса: как минимум, адреса команд стоило б выводить в шестнадцатеричном виде, при реальной работе это обычно намного удобней, чем в десятичном.
himysay Автор
В упор не вижу где в десятичном виде адреса команд?
SIISII
"Наш дизассемблер" -- справа столбец адресов в явно десятичном виде: 6099, за ним 6100 и т.д.
ADD. А, блин, это я ступил: это номера строк, а не адреса. Ну, тогда адреса надо б добавить.
himysay Автор
Да именно просто номер строки в пхп шторме, а так как минимум внутренний указатель адреса должен быть в любом случае, но хочу прежде написать простой скрипт ассемблера.