pvbemu/app/app/Disassembler.js

893 lines
27 KiB
JavaScript

import { Util } from /**/"../app/Util.js";
// Opcode definition
class Opdef {
constructor(format, mnemonic, signExtend) {
this.format = format;
this.mnemonic = mnemonic;
this.signExtend = !!signExtend;
}
}
// Top-level opcode definition lookup table by opcode
let OPDEFS = [
new Opdef(1, "MOV" ), new Opdef(1, "ADD" ), new Opdef(1, "SUB" ),
new Opdef(1, "CMP" ), new Opdef(1, "SHL" ), new Opdef(1, "SHR" ),
new Opdef(1, "JMP" ), new Opdef(1, "SAR" ), new Opdef(1, "MUL" ),
new Opdef(1, "DIV" ), new Opdef(1, "MULU" ), new Opdef(1, "DIVU" ),
new Opdef(1, "OR" ), new Opdef(1, "AND" ), new Opdef(1, "XOR" ),
new Opdef(1, "NOT" ), new Opdef(2, "MOV" ,1), new Opdef(2, "ADD",1),
new Opdef(2, "SETF" ), new Opdef(2, "CMP" ,1), new Opdef(2, "SHL" ),
new Opdef(2, "SHR" ), new Opdef(2, "CLI" ), new Opdef(2, "SAR" ),
new Opdef(2, "TRAP" ), new Opdef(2, "RETI" ), new Opdef(2, "HALT" ),
new Opdef(0, null ), new Opdef(2, "LDSR" ), new Opdef(2, "STSR" ),
new Opdef(2, "SEI" ), new Opdef(2, null ), new Opdef(3, "Bcond"),
new Opdef(3, "Bcond"), new Opdef(3, "Bcond" ), new Opdef(3, "Bcond"),
new Opdef(3, "Bcond"), new Opdef(3, "Bcond" ), new Opdef(3, "Bcond"),
new Opdef(3, "Bcond"), new Opdef(5,"MOVEA",1), new Opdef(5,"ADDI",1),
new Opdef(4, "JR" ), new Opdef(4, "JAL" ), new Opdef(5, "ORI" ),
new Opdef(5, "ANDI" ), new Opdef(5, "XORI" ), new Opdef(5, "MOVHI"),
new Opdef(6, "LD.B" ), new Opdef(6, "LD.H" ), new Opdef(0, null ),
new Opdef(6, "LD.W" ), new Opdef(6, "ST.B" ), new Opdef(6, "ST.H" ),
new Opdef(0, null ), new Opdef(6, "ST.W" ), new Opdef(6, "IN.B" ),
new Opdef(6, "IN.H" ), new Opdef(6, "CAXI" ), new Opdef(6, "IN.W" ),
new Opdef(6, "OUT.B"), new Opdef(6, "OUT.H" ), new Opdef(7, null ),
new Opdef(6, "OUT.W")
];
// Bit string mnemonic lookup table by sub-opcode
let BITSTRINGS = [
"SCH0BSU", "SCH0BSD", "SCH1BSU", "SCH1BSD",
null , null , null , null ,
"ORBSU" , "ANDBSU" , "XORBSU" , "MOVBSU" ,
"ORNBSU" , "ANDNBSU", "XORNBSU", "NOTBSU"
];
// Floating-point/Nintendo mnemonic lookup table by sub-opcode
let FLOATENDOS = [
"CMPF.S", null , "CVT.WS", "CVT.SW" ,
"ADDF.S", "SUBF.S", "MULF.S", "DIVF.S" ,
"XB" , "XH" , "REV" , "TRNC.SW",
"MPYHW"
];
// Program register names
let PROREGS = { 2: "hp", 3: "sp", 4: "gp", 5: "tp", 31: "lp" };
// System register names
let SYSREGS = [
"EIPC", "EIPSW", "FEPC", "FEPSW",
"ECR" , "PSW" , "PIR" , "TKCW" ,
null , null , null , null ,
null , null , null , null ,
null , null , null , null ,
null , null , null , null ,
"CHCW", "ADTRE", null , null ,
null , null , null , null
];
// Condition mnemonics
let CONDS = [
"V" , ["C" , "L" ], ["E" , "Z" ], "NH",
"N" , "T" , "LT" , "LE",
"NV", ["NC", "NL"], ["NE", "NZ"], "H" ,
"P" , "F" , "GE" , "GT"
];
// Output setting keys
const SETTINGS = [
"bcondMerged", "branchAddress", "condCase", "condCL", "condEZ",
"condNames", "hexCaps", "hexDollar", "hexSuffix", "imm5OtherHex",
"imm5ShiftHex", "imm5TrapHex", "imm16AddiLargeHex", "imm16AddiSmallHex",
"imm16MoveHex", "imm16OtherHex", "jmpBrackets", "memoryLargeHex",
"memorySmallHex", "memoryInside", "mnemonicCaps", "operandReverse",
"proregCaps", "proregNames", "setfMerged", "sysregCaps", "sysregNames"
];
///////////////////////////////////////////////////////////////////////////////
// Line //
///////////////////////////////////////////////////////////////////////////////
// One line of output
class Line {
///////////////////////// Initialization Methods //////////////////////////
constructor(parent, first) {
// Configure instance fields
this.parent = parent;
// Configure labels
this.lblAddress = this.label("tk-address" , first);
this.lblBytes = this.label("tk-bytes" , first);
this.lblMnemonic = this.label("tk-mnemonic", first);
this.lblOperands = this.label("tk-operands", false);
}
///////////////////////////// Package Methods /////////////////////////////
// Update the elements' display
refresh(row, isPC) {
// The row is not available
if (!row) {
this.lblAddress .innerText = "--------";
this.lblBytes .innerText = "";
this.lblMnemonic.innerText = "---";
this.lblOperands.innerText = "";
}
// Update labels with the disassembled row's contents
else {
this.lblAddress .innerText = row.address;
this.lblBytes .innerText = row.bytes;
this.lblMnemonic.innerText = row.mnemonic;
this.lblOperands.innerText = row.operands;
}
// Update style according to selection
let method = isPC ? "add" : "remove";
this.lblAddress .classList[method]("tk-selected");
this.lblBytes .classList[method]("tk-selected");
this.lblMnemonic.classList[method]("tk-selected");
this.lblOperands.classList[method]("tk-selected");
}
// Specify whether the elements on this line are visible
setVisible(visible) {
visible = visible ? "block" : "none";
this.lblAddress .style.display = visible;
this.lblBytes .style.display = visible;
this.lblMnemonic.style.display = visible;
this.lblOperands.style.display = visible;
}
///////////////////////////// Private Methods /////////////////////////////
// Create a display label
label(className, first) {
// Create the label element
let label = document.createElement("div");
label.className = "tk " + className;
// The label is part of the first row of output
let element = label;
if (first) {
// Create a container element
element = document.createElement("div");
element.append(label);
element.max = 0;
// Ensure the container can always fit the column contents
Toolkit.addResizeListener(element, ()=>{
let width = Math.ceil(label.getBoundingClientRect().width);
if (width <= element.max)
return;
element.max = width;
element.style.minWidth = width + "px";
});
}
// Configure elements
this.parent.view.append(element);
return label;
}
}
///////////////////////////////////////////////////////////////////////////////
// Disassembler //
///////////////////////////////////////////////////////////////////////////////
// Text disassembler for NVC
class Disassembler extends Toolkit.ScrollPane {
///////////////////////// Initialization Methods //////////////////////////
constructor(debug) {
super(debug.app, {
className : "tk tk-scrollpane tk-disassembler",
horizontal: Toolkit.ScrollPane.AS_NEEDED,
focusable : true,
tabStop : true,
tagName : "div",
vertical : Toolkit.ScrollPane.NEVER
});
// Configure instance fields
this.address = Util.u32(0xFFFFFFF0);
this.app = debug.app;
this.columns = [ 0, 0, 0, 0 ];
this.data = [];
this.debug = debug;
this.isSubscribed = false;
this.lines = null;
this.pc = this.address;
this.pending = [];
this.rows = [];
this.scroll = 0;
this.sim = debug.sim;
// Default output settings
this.setConfig({
bcondMerged : true,
branchAddress : true,
condCase : false,
condCL : 1,
condEZ : 1,
condNames : true,
hexCaps : true,
hexDollar : false,
hexSuffix : false,
imm5OtherHex : false,
imm5ShiftHex : false,
imm5TrapHex : false,
imm16AddiLargeHex: true,
imm16AddiSmallHex: false,
imm16MoveHex : true,
imm16OtherHex : true,
jmpBrackets : true,
memoryLargeHex : true,
memorySmallHex : false,
memoryInside : false,
mnemonicCaps : true,
operandReverse : false,
proregCaps : false,
proregNames : true,
setfMerged : false,
sysregCaps : true,
sysregNames : true
});
// Configure viewport
this.viewport.classList.add("tk-mono");
// Configure view
let view = document.createElement("div");
view.className = "tk tk-view";
Object.assign(view.style, {
display : "grid",
gridTemplateColumns: "repeat(3, max-content) auto"
});
this.setView(view);
// Font-measuring element
this.metrics = new Toolkit.Component(this.app, {
className: "tk tk-metrics tk-mono",
tagName : "div",
style : {
position : "absolute",
visibility: "hidden"
}
});
this.metrics.element.innerText = "X";
this.append(this.metrics.element);
// First row always exists
this.lines = [ new Line(this, true) ];
// Configure event handlers
Toolkit.addResizeListener(this.viewport, e=>this.onResize(e));
this.addEventListener("keydown", e=>this.onKeyDown (e));
this.addEventListener("wheel" , e=>this.onMouseWheel(e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
let tall = this.tall(false);
// Processing by key
switch (e.key) {
// Navigation
case "ArrowDown" : this.fetch(+1 , true); break;
case "ArrowUp" : this.fetch(-1 , true); break;
case "PageDown" : this.fetch(+tall, true); break;
case "PageUp" : this.fetch(-tall, true); break;
// View control
case "ArrowLeft" : this.horizontal.setValue(
this.horizontal.value - this.horizontal.increment); break;
case "ArrowRight": this.horizontal.setValue(
this.horizontal.value + this.horizontal.increment); break;
// Goto
case "g": case "G":
if (!e.ctrlKey)
return;
this.promptGoto();
break;
// Single step
case "F10":
this.debug.runNext();
break;
// Single step
case "F11":
this.debug.singleStep();
break;
default: return;
}
// Configure event
e.stopPropagation();
e.preventDefault();
}
// Mouse wheel
onMouseWheel(e) {
// User agent scaling action
if (e.ctrlKey)
return;
// No rotation has occurred
let offset = Math.sign(e.deltaY) * 3;
if (offset == 0)
return;
// Update the display address
this.fetch(offset, true);
}
// Viewport resized
onResize(e) {
let fetch = false;
let tall = this.tall(true);
// Add additional lines to the output
for (let x = 0; x < tall; x++) {
if (x >= this.lines.length) {
fetch = true;
this.lines.push(new Line(this));
}
this.lines[x].setVisible(true);
}
// Remove extra lines from the output
for (let x = tall; x < this.lines.length; x++)
this.lines[x].setVisible(false);
// Configure horizontal scroll bar
if (this.metrics)
this.horizontal.setIncrement(this.metrics.getBounds().width);
// Update the display
if (fetch)
this.fetch(0, true);
else this.refresh();
}
///////////////////////////// Public Methods //////////////////////////////
// Produce disassembly text
disassemble(rows) {
// Produce a deep copy of the input list
let copy = new Array(rows.length);
for (let x = 0; x < rows.length; x++) {
copy[x] = {};
Object.assign(copy[x], rows[x]);
}
rows = copy;
// Process all rows
for (let row of rows) {
row.operands = [];
// Read instruction bits from the bus
let bits0 = row.bytes[1] << 8 | row.bytes[0];
let bits1;
if (row.bytes.length == 4)
bits1 = row.bytes[3] << 8 | row.bytes[2];
// Working variables
let opcode = bits0 >> 10;
let opdef = OPDEFS[opcode];
// Sub-opcode mnemonics
if (row.opcode == 0b011111)
row.mnemonic = BITSTRINGS[bits0 & 31] || "---";
else if (row.opcode == 0b111110)
row.mnemonic = FLOATENDOS[bits1 >> 10 & 63] || "---";
else row.mnemonic = opdef.mnemonic;
// Processing by format
switch (opdef.format) {
case 1: this.formatI (row, bits0 ); break;
case 3: this.formatIII(row, bits0 ); break;
case 4: this.formatIV (row, bits0, bits1); break;
case 6: this.formatVI (row, bits0, bits1); break;
case 7: this.formatVII(row, bits0 ); break;
case 2:
this.formatII(row, bits0, opdef.signExtend); break;
case 5:
this.formatV (row, bits0, bits1, opdef.signExtend);
}
// Format bytes
let text = [];
for (let x = 0; x < row.bytes.length; x++)
text.push(row.bytes[x].toString(16).padStart(2, "0"));
row.bytes = text.join(" ");
// Post-processing
row.address = row.address.toString(16).padStart(8, "0");
if (this.hexCaps) {
row.address = row.address.toUpperCase();
row.bytes = row.bytes .toUpperCase();
}
if (!this.mnemonicCaps)
row.mnemonic = row.mnemonic.toLowerCase();
if (this.operandReverse)
row.operands.reverse();
row.operands = row.operands.join(", ");
}
return rows;
}
// Retrieve all output settings in an object
getConfig() {
let ret = {};
for (let key of SETTINGS)
ret[key] = this[key];
return ret;
}
// Update with disassembly state from the core
refresh(data = 0) {
let bias;
// Scrolling prefresh
if (typeof data == "number")
bias = 16 + data;
// Received data from the core thread
else {
this.data = data.rows;
this.pc = data.pc;
if (this.data.length == 0)
return;
this.address = this.data[0].address;
this.rows = this.disassemble(this.data);
bias = 16 +
(data.scroll === null ? 0 : this.scroll - data.scroll);
}
// Update elements
let count = Math.min(this.tall(true), this.data.length);
for (let y = 0; y < count; y++) {
let index = bias + y;
let line = this.data[index];
let row = this.rows[index];
this.lines[y].refresh(row, line && line.address == this.pc);
}
// Refesh scroll pane
this.update();
}
// Bring an address into view
seek(address, force) {
// Check if the address is already in the view
if (!force) {
let bias = 16;
let tall = this.tall(false);
let count = Math.min(tall, this.data.length);
// The address is currently visible in the output
for (let y = 0; y < count; y++) {
let row = this.data[bias + y];
if (!row || Util.u32(address - row.address) >= row.size)
continue;
// The address is on this row
this.refresh();
return;
}
}
// Place the address at a particular position in the view
this.address = address;
this.fetch(null);
}
// Update output settings
setConfig(config) {
// Update settings
for (let key of SETTINGS)
if (key in config)
this[key] = config[key];
// Regenerate output
this.refresh({
pc : this.pc,
rows : this.data,
scroll: null
});
}
// Subscribe to or unsubscribe from core updates
setSubscribed(subscribed) {
subscribed = !!subscribed;
// Nothing to change
if (subscribed == this.isSubscribed)
return;
// Configure instance fields
this.isSubscribed = subscribed;
// Subscribe to core updates
if (subscribed)
this.fetch(0);
// Unsubscribe from core updates
else this.sim.unsubscribe("dasm");
}
///////////////////////////// Private Methods /////////////////////////////
// Select a condition's name
cond(cond) {
let ret = CONDS[cond];
switch (cond) {
case 1: case 9: return CONDS[cond][this.condCL];
case 2: case 10: return CONDS[cond][this.condEZ];
}
return CONDS[cond];
}
// Retrieve disassembly data from the core
async fetch(scroll, prefresh) {
let row;
// Scrolling relative to the current view
if (scroll) {
if (prefresh)
this.refresh(scroll);
this.scroll = Util.s32(this.scroll + scroll);
row = -scroll;
}
// Jumping to an address directly
else row = scroll === null ? Math.floor(this.tall(false) / 3) + 16 : 0;
// Retrieve data from the core
this.refresh(
await this.sim.disassemble(
this.address,
row,
this.tall(true) + 32,
scroll === null ? null : this.scroll, {
subscribe: this.isSubscribed && "dasm"
})
);
}
// Represent a hexadecimal value
hex(value, digits) {
let sign = Util.s32(value) < 0 ? "-" : "";
let ret = Math.abs(Util.u32(value)).toString(16).padStart(digits,"0");
if (this.hexCaps)
ret = ret.toUpperCase();
if (this.hexSuffix)
ret = ("abcdefABCDEF".indexOf(ret[0]) == -1 ? "" : "0") +
ret + "h";
else ret = (this.hexDollar ? "$" : "0x") + ret;
return sign + ret;
}
// Prompt the user to specify a new address
promptGoto() {
// Receive input from the user
let address = prompt(this.app.translate("common.gotoPrompt"));
if (address == null)
return;
// Process the input as an address in hexadecimal
address = parseInt(address, 16);
if (isNaN(address))
return;
// Move the selection and refresh the display
this.seek(Util.u32(address));
}
// Select a program register name
proreg(index) {
let ret = this.proregNames && PROREGS[index] || "r" + index;
return this.proregCaps ? ret.toUpperCase() : ret;
}
// Measure how many rows of output are visible
tall(partial) {
let lineHeight = !this.metrics ? 0 :
Math.ceil(this.metrics.getBounds().height);
return lineHeight <= 0 ? 1 : Math.max(1, Math[partial?"ceil":"floor"](
this.viewport.getBoundingClientRect().height / lineHeight));
}
//////////////////////////// Decoding Methods /////////////////////////////
// Disassemble a Format I instruction
formatI(row, bits0) {
let reg1 = this.proreg(bits0 & 31);
// JMP
if (row.mnemonic == "JMP") {
if (this.jmpBrackets)
reg1 = "[" + reg1 + "]";
row.operands.push(reg1);
}
// Other instructions
else {
let reg2 = this.proreg(bits0 >> 5 & 31);
row.operands.push(reg1, reg2);
}
}
// Disassemble a Format II instruction
formatII(row, bits0, signExtend) {
// Bit-string instructions are zero-operand
if (bits0 >> 10 == 0b011111)
return;
// Processing by mnemonic
switch (row.mnemonic) {
// Zero-operand
case "---" : // Fallthrough
case "CLI" : // Fallthrough
case "HALT": // Fallthrough
case "RETI": // Fallthrough
case "SEI" : return;
// Distinct notation
case "LDSR": return this.ldstsr(row, bits0, true );
case "SETF": return this.setf (row, bits0 );
case "STSR": return this.ldstsr(row, bits0, false);
}
// Retrieve immediate operand
let imm = bits0 & 31;
if (signExtend)
imm = Util.signExtend(bits0, 5);
// TRAP instruction is one-operand
if (row.mnemonic == "TRAP") {
row.operands.push(this.trapHex ?
this.hex(imm, 1) : imm.toString());
return;
}
// Processing by mnemonic
let hex = this.imm5OtherHex;
switch (row.mnemonic) {
case "SAR": // Fallthrough
case "SHL": // Fallthrough
case "SHR": hex = this.imm5ShiftHex;
}
imm = hex ? this.hex(imm, 1) : imm.toString();
// Two-operand instruction
let reg2 = this.proreg(bits0 >> 5 & 31);
row.operands.push(imm, reg2);
}
// Disassemble a Format III instruction
formatIII(row, bits0) {
let cond = this.cond(bits0 >> 9 & 15);
let disp = Util.signExtend(bits0 & 0x1FF, 9);
// Condition merged with mnemonic
if (this.bcondMerged) {
switch (cond) {
case "F": row.mnemonic = "NOP"; return;
case "T": row.mnemonic = "BR" ; break;
default : row.mnemonic = "B" + cond;
}
}
// Condition as operand
else {
if (!this.condCaps)
cond = cond.toLowerCase();
row.operands.push(cond);
}
// Operand as destination address
if (this.branchAddress) {
disp = Util.u32(row.address + disp & 0xFFFFFFFE)
.toString(16).padStart(8, "0");
if (this.hexCaps)
disp = disp.toUpperCase();
row.operands.push(disp);
}
// Operand as displacement
else {
let sign = disp < 0 ? "-" : disp > 0 ? "+" : "";
let rel = this.hex(Math.abs(disp), 1);
row.operands.push(sign + rel);
}
}
// Disassemble a Format IV instruction
formatIV(row, bits0, bits1) {
let disp = Util.signExtend(bits0 << 16 | bits1, 26);
// Operand as destination address
if (this.branchAddress) {
disp = Util.u32(row.address + disp & 0xFFFFFFFE)
.toString(16).padStart(8, "0");
if (this.hexCaps)
disp = disp.toUpperCase();
row.operands.push(disp);
}
// Operand as displacement
else {
let sign = disp < 0 ? "-" : disp > 0 ? "+" : "";
let rel = this.hex(Math.abs(disp), 1);
row.operands.push(sign + rel);
}
}
// Disassemble a Format V instruction
formatV(row, bits0, bits1, signExtend) {
let imm = signExtend ? Util.signExtend(bits1) : bits1;
let reg1 = this.proreg(bits0 & 31);
let reg2 = this.proreg(bits0 >> 5 & 31);
if (
row.mnemonic == "ADDI" ?
Math.abs(imm) <= 256 ?
this.imm16AddiSmallHex :
this.imm16AddiLargeHex
: row.mnemonic == "MOVEA" || row.mnemonic == "MOVHI" ?
this.imm16MoveHex
:
this.imm16OtherHex
) imm = this.hex(imm, 4);
row.operands.push(imm, reg1, reg2);
}
// Disassemble a Format VI instruction
formatVI(row, bits0, bits1) {
let disp = Util.signExtend(bits1);
let reg1 = this.proreg(bits0 & 31);
let reg2 = this.proreg(bits0 >> 5 & 31);
let sign =
disp < 0 ? "-" :
disp == 0 || !this.memoryInside ? "" :
"+";
// Displacement is hexadecimal
disp = Math.abs(disp);
if (disp == 0)
disp = ""
else if (disp <= 256 ? this.memorySmallHex : this.memoryLargeHex)
disp = this.hex(disp, 1);
// Format the displacement figure according to its presentation
disp = this.memoryInside ?
sign == "" ? "" : " " + sign + " " + disp :
sign + disp
;
// Apply operands
row.operands.push(this.memoryInside ?
"[" + reg1 + disp + "]" :
disp + "[" + reg1 + "]",
reg2);
// Swap operands for output and store instructions
switch (row.mnemonic) {
case "OUT.B": case "OUT.H": case "OUT.W":
case "ST.B" : case "ST.H" : case "ST.W" :
row.operands.reverse();
}
}
// Disassemble a Format VII instruction
formatVII(row, bits0) {
let reg1 = this.proreg(bits0 & 31);
let reg2 = this.proreg(bits0 >> 5 & 31);
// Invalid sub-opcode is zero-operand
if (row.mnemonic == "---")
return;
// Processing by mnemonic
switch (row.mnemonic) {
case "XB": // Fallthrough
case "XH": break;
default : row.operands.push(reg1);
}
row.operands.push(reg2);
}
// Format an LDSR or STSR instruction
ldstsr(row, bits0, reverse) {
// System register
let sysreg = bits0 & 31;
sysreg = this.sysregNames && SYSREGS[sysreg] || sysreg.toString();
if (!this.sysregCaps)
sysreg = sysreg.toLowerCase();
// Program register
let reg2 = this.proreg(bits0 >> 5 & 31);
// Operands
row.operands.push(sysreg, reg2);
if (reverse)
row.operands.reverse();
}
// Format a SETF instruction
setf(row, bits0) {
let cond = this.cond (bits0 & 15);
let reg2 = this.proreg(bits0 >> 5 & 31);
// Condition merged with mnemonic
if (!this.bcondMerged) {
row.mnemonic += cond;
}
// Condition as operand
else {
if (!this.condCaps)
cond = cond.toLowerCase();
row.operands.push(cond);
}
row.operands.push(reg2);
}
}
export { Disassembler };