diff --git a/CHANGELOG.md b/CHANGELOG.md index 559908a3..75b3e83e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## 0.9.5 +- Reverse debugging: Conditional breakpoints. - Fixed moving of PC to cursor during reverse debugging. - Changed reverse debug decoration to blue background only due to performance reasons. diff --git a/documentation/Usage.md b/documentation/Usage.md index ce4044d4..e769ee25 100644 --- a/documentation/Usage.md +++ b/documentation/Usage.md @@ -339,6 +339,19 @@ But please note: The history stores only the register values and stack contents. So whenever a memory location is changed from the program code in reverse debugging this will not be reflected in e.g. the memory view. You can only rely on the register values. +#### Breakpoints in Reverse Debug Mode + +You can also use breakpoints during reverse debugging. +The normal (program counter related) breakpoints work just as you would expect. + +It is also possible to add a condition. I.e. you can additionally test for certain register values. + +But please note that during reverse debugging the memory contents is not evaluated. A breakpoint that checks for memory is not evaluated correctly. In such a case the breakpoint will fire always. +E.g. a condition like this +~~~ +b@(HL) == 0 +~~~ +will be evaluated to true always so that you don't miss such a breakpoint. ### Code Coverage diff --git a/src/emulatorfactory.ts b/src/emulatorfactory.ts index 4a007ef0..dfa298d3 100644 --- a/src/emulatorfactory.ts +++ b/src/emulatorfactory.ts @@ -2,6 +2,7 @@ import * as assert from 'assert'; import { ZesaruxEmulator } from './zesaruxemulator'; import { ZesaruxExtEmulator } from './zesaruxextemulator'; +import { EmulatorClass } from './emulator'; /// Different machine emulators. @@ -24,10 +25,10 @@ export class EmulatorFactory { public static createEmulator(emul: EmulatorType) { switch(emul) { case EmulatorType.ZESARUX: - Emulator = new ZesaruxEmulator(); + EmulatorFactory.setEmulator(new ZesaruxEmulator()); break; case EmulatorType.ZESARUX_EXT: // Zesarux with own extensions. - Emulator = new ZesaruxExtEmulator(); + EmulatorFactory.setEmulator(new ZesaruxExtEmulator()); break; case EmulatorType.MAME: assert(false); // needs to be implemented @@ -38,6 +39,14 @@ export class EmulatorFactory { } } + + /** + * Sets the emulator variable. + */ + protected static setEmulator(emulator: EmulatorClass) { + Emulator = emulator; + } + } diff --git a/src/emuldebugadapter.ts b/src/emuldebugadapter.ts index 56e26769..035dcb4b 100644 --- a/src/emuldebugadapter.ts +++ b/src/emuldebugadapter.ts @@ -1208,9 +1208,9 @@ export class EmulDebugSessionClass extends DebugSession { // Send event this.sendEvent(new StoppedEvent('step', EmulDebugSessionClass.THREAD_ID)); - // Show a possible error + // Output a possibly error if(error) - this.showError(error); + vscode.debug.activeDebugConsole.appendLine(error); }); } diff --git a/src/tests/utility.tests.ts b/src/tests/utility.tests.ts index c2a1c471..3b26a91d 100644 --- a/src/tests/utility.tests.ts +++ b/src/tests/utility.tests.ts @@ -2,8 +2,8 @@ import * as assert from 'assert'; import { Utility } from '../utility'; import { Z80Registers } from '../z80Registers'; +import { EmulatorClass } from '../emulator'; import { Emulator, EmulatorType, EmulatorFactory } from '../emulatorfactory'; -//import { EmulatorClass } from '../emulator'; import { Settings } from '../settings'; @@ -308,6 +308,86 @@ suite('Utility', () => { assert.equal(0x0F>>3, res, "Wrong eval result"); }); + + + suite('breakpoints', () => { + setup(() => { + (EmulatorFactory).setEmulator(new EmulatorClass()); + }); + + test('simple', () => { + (Emulator).RegisterCache = ""; + let res = Utility.evalExpression('0x1234 == 0x1234', true); + assert.equal(1, res, "Wrong eval result"); + + res = Utility.evalExpression('0x1235 == 0x1234', true); + assert.equal(0, res, "Wrong eval result"); + }); + + test('register SP', () => { + (Emulator).RegisterCache = "PC=80d3 SP=83fb AF=3f08 BC=0000 HL=4000 DE=2000 IX=ffff IY=5c3a AF'=0044 BC'=0001 HL'=f3f3 DE'=0001 I=00 R=0d IM0 IFF12 (PC)=3e020603 (SP)=80f5"; + let res = Utility.evalExpression('SP == 0x83FB', true); + assert.equal(1, res, "Wrong eval result"); + + res = Utility.evalExpression('0x83FB == SP', true); + assert.equal(1, res, "Wrong eval result"); + + res = Utility.evalExpression('SP == 0x83FA', true); + assert.equal(0, res, "Wrong eval result"); + + res = Utility.evalExpression('0x83FB != SP', true); + assert.equal(0, res, "Wrong eval result"); + }); + + test('All registers', () => { + (Emulator).RegisterCache = "PC=80d3 SP=83fb AF=3f08 BC=1234 HL=5678 DE=9abc IX=fedc IY=5c3a AF'=0143 BC'=2345 HL'=f4f3 DE'=89ab I=ab R=0d IM0 IFF12 (PC)=3e020603 (SP)=80f5"; + + let res = Utility.evalExpression('PC == 0x80D3', true); + assert.equal(1, res, "Wrong eval result"); + + res = Utility.evalExpression('AF == 3F08h', true); + assert.equal(1, res, "Wrong eval result"); + + res = Utility.evalExpression('BC == 0x1234', true); + assert.equal(1, res, "Wrong eval result"); + + res = Utility.evalExpression('DE == 9ABCh', true); + assert.equal(1, res, "Wrong eval result"); + + res = Utility.evalExpression('HL == 5678h', true); + assert.equal(1, res, "Wrong eval result"); + + res = Utility.evalExpression('IX == 0xFEDC', true); + assert.equal(1, res, "Wrong eval result"); + + res = Utility.evalExpression('IY == 0x5C3A', true); + assert.equal(1, res, "Wrong eval result"); + + res = Utility.evalExpression("AF' == 0143h", true); + assert.equal(1, res, "Wrong eval result"); + + res = Utility.evalExpression("BC' == 0x2345", true); + assert.equal(1, res, "Wrong eval result"); + + res = Utility.evalExpression("DE' == 89ABh", true); + assert.equal(1, res, "Wrong eval result"); + + res = Utility.evalExpression("HL' == F4F3h", true); + assert.equal(1, res, "Wrong eval result"); + }); + + + test('memory (exception)', () => { + (Emulator).RegisterCache = "PC=80d3 SP=83fb AF=3f08 BC=1234 HL=5678 DE=9abc IX=fedc IY=5c3a AF'=0143 BC'=2345 HL'=f4f3 DE'=89ab I=ab R=0d IM0 IFF12 (PC)=3e020603 (SP)=80f5"; + + // It is not supported to retrieve memory locations. + // Therefore a test is done on an exception. + assert.throws( () => { + Utility.evalExpression('b@(1000) == 50', true); + }, "Expected an exception"); + }); + }); + }); }); \ No newline at end of file diff --git a/src/utility.ts b/src/utility.ts index 2feede75..2aa2dc7a 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -194,16 +194,17 @@ export class Utility { * Examples: * 2-5*3 => -13, -Dh * LBL_TEST+1 => 32769, 8001h + * HL' != 1111h * @param expr The expression to evaluate. May contain math expressions and labels. * Also evaluates numbers in formats like '$4000', '2FACh', 100111b, 'G'. * @param evalRegisters If true then register names will also be evaluated. * @param modulePrefix An optional prefix to use for each label. (sjasmplus) * @param lastLabel An optional last label to use for local labels. (sjasmplus) - * @returns The evaluated number. + * @returns The evaluated number. (If a boolean expression is evaluated a 1 is returned for true and a 0 for false) * @throws SyntaxError if 'eval' throws an error or if the label is not found. */ public static evalExpression(expr: string, evalRegisters = true, modulePrefix?:string, lastLabel?: string): number { - const exprLabelled = expr.replace(/([\$][0-9a-fA-F]+|[a-fA-F0-9]+h|[0-9]+\S+|0x[a-fA-F0-9]+|[a-zA-Z_\.][a-zA-Z0-9_\.]*|'[\S ]+')/g, (match, p1) => { + const exprLabelled = expr.replace(/([\$][0-9a-fA-F]+|[a-fA-F0-9]+h|[0-9]+\S+|0x[a-fA-F0-9]+|[a-zA-Z_\.][a-zA-Z0-9_\.]*'?|'[\S ]+')/g, (match, p1) => { let res; if(evalRegisters) { // Check if it might be a register name. @@ -239,7 +240,12 @@ export class Utility { }); // Evaluate const result = eval(exprLabelled); - // return + + // Check if boolean + if(typeof(result) == 'boolean') + return (result) ? 1 : 0; + + // Return normal number return result; } diff --git a/src/zesaruxemulator.ts b/src/zesaruxemulator.ts index da7b3f00..7bd0e23d 100644 --- a/src/zesaruxemulator.ts +++ b/src/zesaruxemulator.ts @@ -662,11 +662,10 @@ export class ZesaruxEmulator extends EmulatorClass { this.handleReverseDebugStackForward(currentLine, nextLine); // Check for breakpoint - const bp = this.checkPcBreakpoints(nextLine); - if(bp) { - reason = 'Breakpoint hit at PC='+Utility.getHexString(bp.address,4); - if(bp.condition) - reason += ', condition: ' + bp.condition; + this.RegisterCache = nextLine; + const condition = this.checkPcBreakpoints(nextLine); + if(condition != undefined) { + reason = condition; break; // BP hit and condition met. } @@ -1137,11 +1136,10 @@ export class ZesaruxEmulator extends EmulatorClass { await this.handleReverseDebugStackBack(currentLine, prevLine); // Check for breakpoint - const bp = this.checkPcBreakpoints(currentLine); - if(bp) { - reason = 'Breakpoint hit at PC='+Utility.getHexString(bp.address,4); - if(bp.condition) - reason += ', condition: ' + bp.condition; + this.RegisterCache = currentLine; + const condition = this.checkPcBreakpoints(currentLine); + if(condition != undefined) { + reason = condition; break; // BP hit and condition met. } @@ -1201,8 +1199,10 @@ export class ZesaruxEmulator extends EmulatorClass { while(true) { // Get next line nextLine = this.revDbgNext(); - if(!nextLine) + if(!nextLine) { + errorText = 'Break: Reached start of instruction history.' break; // At end of reverse debugging. Simply get the real call stack. + } // Handle reverse stack this.handleReverseDebugStackForward(currentLine, nextLine); @@ -1217,6 +1217,14 @@ export class ZesaruxEmulator extends EmulatorClass { if(pc == nextPC0 || pc == nextPC1) break; + // Check for "real" breakpoint + this.RegisterCache = nextLine; + const condition = this.checkPcBreakpoints(nextLine); + if(condition != undefined) { + errorText = condition; + break; // BP hit and condition met. + } + // Next currentLine = nextLine as string; } @@ -1453,8 +1461,10 @@ export class ZesaruxEmulator extends EmulatorClass { while(true) { // Get next line nextLine = this.revDbgNext(); - if(!nextLine) + if(!nextLine) { + errorText = 'Break: Reached start of instruction history.'; break; // At end of reverse debugging. Simply get the real call stack. + } // Handle reverse stack this.handleReverseDebugStackForward(currentLine, nextLine); @@ -1471,6 +1481,14 @@ export class ZesaruxEmulator extends EmulatorClass { } } + // Check for breakpoint + this.RegisterCache = nextLine; + const condition = this.checkPcBreakpoints(nextLine); + if(condition != undefined) { + errorText = condition; + break; // BP hit and condition met. + } + // Next currentLine = nextLine as string; } @@ -1907,14 +1925,46 @@ export class ZesaruxEmulator extends EmulatorClass { * Returns the breakpoint at the given address. * Note: Checks only breakpoints with a set 'address'. * @param regs The registers as string, e.g. "PC=0039 SP=ff44 AF=005c BC=ffff HL=10a8 DE=5cb9 IX=ffff IY=5c3a AF'=0044 BC'=174b HL'=107f DE'=0006 I=3f R=06 IM1 IFF-- (PC)=e52a785c (SP)=a2bf" + * @returns A string with the reason. undefined if no breakpoint hit. */ - protected checkPcBreakpoints(regs: string): EmulatorBreakpoint|undefined { + protected checkPcBreakpoints(regs: string): string|undefined { + assert(this.RegisterCache); + let condition; const pc = Z80Registers.parsePC(regs); for(const bp of this.breakpoints) { - if(bp.address == pc) - return bp; + if(bp.address == pc) { + // Check for condition + if(!bp.condition) { + condition = ""; + break; + } + + // Evaluate condition + try { + const result = Utility.evalExpression(bp.condition, true); + if(result != 0) { + condition = bp.condition; + break; + } + } + catch(e) { + // A problem during evaluation happened, + // e.g. a memory location has been tested which is not possible + // during reverse debugging. + condition = "Could not evaluate: " + bp.condition; + break; + } + } + } + + // Text + let reason; + if(condition != undefined) { + reason = 'Breakpoint hit at PC=' + Utility.getHexString(pc,4); + if(condition != "") + reason += ', ' + condition; } - return undefined; + return reason; }