21、调试器:原理、实现与测试

调试器:原理、实现与测试

1. 调试器工作原理概述

调试器在程序员的日常工作中扮演着至关重要的角色,其重要性不亚于版本控制工具,但在教学中却较少被提及。接下来,我们将逐步构建一个简单的单步调试器,并介绍一种测试交互式应用程序的方法。

2. 起点:程序的 JSON 表示

为了调试比汇编代码更高级的语言,同时避免编写解析器或处理抽象语法树(AST),我们采用 JSON 数据结构来表示程序。例如,以下 JavaScript 代码:

const a = [-3, -5, -1, 0, -2, 1, 3, 1];
const b = Array();
let largest = a[0];
let i = 0;
while (i < length(a)) {
    if (a[i] > largest) {
        b.push(a[i]);
    }
    i += 1;
}
i = 0;
while (i < length(b)) {
    console.log(b[i]);
    i += 1;
}

对应的 JSON 表示为:

[
    ["defA", "a", ["data", -3, -5, -1, 0, -2, 1, 3, 1]],
    ["defA", "b", ["data"]],
    ["defV", "largest", ["getA", "a", ["num", 0]]],
    ["append", "b", ["getV", "largest"]],
    ["defV", "i", ["num", 0]],
    ["loop", ["lt", ["getV", "i"], ["len", "a"]],
        ["test", ["gt", ["getA", "a", ["getV", "i"]], ["getV", "largest"]],
            ["setV", "largest", ["getA", "a", ["getV", "i"]]],
            ["append", "b", ["getV", "largest"]]
        ],
        ["setV", "i", ["add", ["getV", "i"], ["num", 1]]]
    ],
    ["setV", "i", ["num", 0]],
    ["loop", ["lt", ["getV", "i"], ["len", "b"]],
        ["print", ["getA", "b", ["getV", "i"]]],
        ["setV", "i", ["add", ["getV", "i"], ["num", 1]]]
    ]
]

我们的虚拟机结构与之前的类似。为了简化操作,我们通过移除注释和空行,然后根据命令名称查找并调用相应的方法来执行程序:

import assert from 'assert';

class VirtualMachineBase {
    constructor(program) {
        this.program = this.compile(program);
        this.prefix = '>>';
    }
    compile(lines) {
        const text = lines
          .map(line => line.trim())
          .filter(line => (line.length > 0) && !line.startsWith('//'))
          .join('\n');
        return JSON.parse(text);
    }
    run() {
        this.env = {};
        this.runAll(this.program);
    }
    runAll(commands) {
        commands.forEach(command => this.exec(command));
    }
    exec(command) {
        const [op, ...args] = command;
        assert(op in this, `Unknown op "${op}"`);
        return this[op](args);
    }
}

export default VirtualMachineBase;

虚拟机中定义新变量并赋予初始值的方法如下:

defV(args) {
    this.checkOp('defV', 2, args);
    const [name, value] = args;
    this.env[name] = this.exec(value);
}

加法操作的方法如下:

add(args) {
    this.checkOp('add', 2, args);
    const left = this.exec(args[0]);
    const right = this.exec(args[1]);
    return left + right;
}

执行 while 循环的方法如下:

loop(args) {
    this.checkBody('loop', 1, args);
    const body = args.slice(1);
    while (this.exec(args[0])) {
        this.runAll(body);
    }
}

检查变量名是否引用数组的方法如下:

checkArray(op, name) {
    this.checkName(op, name);
    const array = this.env[name];
    assert(Array.isArray(array), `Variable "${name}" used in "${op}" is not array`);
}
3. 制作跟踪调试器

为了实现跟踪调试,我们需要一个源映射(source map)来记录每条指令在源文件中的位置。由于 JSON 是 JavaScript 的子集,我们可以使用 Acorn 解析程序来获取行号,但为了简化,我们手动为每个重要语句添加行号。例如:

[
    [1, "defA", "a", ["data", -3, -5, -1, 0, -2, 1, 3, 1]],
    [2, "defA", "b", ["data"]],
    [3, "defV", "largest", ["getA", "a", ["num", 0]]],
    [4, "append", "b", ["getV", "largest"]],
    [5, "defV", "i", ["num", 0]],
    [6, "loop", ["lt", ["getV", "i"], ["len", "a"]],
        [7, "test", ["gt", ["getA", "a", ["getV", "i"]], ["getV", "largest"]],
            [8, "setV", "largest", ["getA", "a", ["getV", "i"]]],
            [9, "append", "b", ["getV", "largest"]]
        ],
        [11, "setV", "i", ["add", ["getV", "i"], ["num", 1]]]
    ],
    [13, "setV", "i", ["num", 0]],
    [14, "loop", ["lt", ["getV", "i"], ["len", "b"]],
        [15, "print", ["getA", "b", ["getV", "i"]]],
        [16, "setV", "i", ["add", ["getV", "i"], ["num", 1]]]
    ]
]

构建源映射的过程如下:

import assert from 'assert';
import VirtualMachineBase from './vm-base.js';

class VirtualMachineSourceMap extends VirtualMachineBase {
    compile(lines) {
        const original = super.compile(lines);
        this.sourceMap = {};
        const result = original.map(command => this.transform(command));
        return result;
    }
    transform(node) {
        if (!Array.isArray(node)) {
            return node;
        }
        if (Array.length === 0) {
            return [];
        }
        const [first, ...rest] = node;
        if (typeof first!== 'number') {
            return [first, null, ...rest.map(arg => this.transform(arg))];
        }
        const [op, ...args] = rest;
        this.sourceMap[first] = [op, first, ...args.map(arg => this.transform(arg))];
        return this.sourceMap[first];
    }
    exec(command) {
        const [op, lineNum, ...args] = command;
        assert(op in this, `Unknown op "${op}"`);
        return this[op](args);
    }
}

export default VirtualMachineSourceMap;

接下来,我们修改虚拟机的 exec 方法,为每个重要操作执行回调函数:

import assert from 'assert';
import VirtualMachineSourceMap from './vm-source-map.js';

class VirtualMachineCallback extends VirtualMachineSourceMap {
    constructor(program, dbg) {
        super(program);
        this.dbg = dbg;
        this.dbg.setVM(this);
    }
    exec(command) {
        const [op, lineNum, ...args] = command;
        this.dbg.handle(this.env, lineNum, op);
        assert(op in this, `Unknown op "${op}"`);
        return this[op](args, lineNum);
    }
    message(prefix, val) {
        this.dbg.message(`${prefix} ${val}`);
    }
}

export default VirtualMachineCallback;

运行程序时,我们创建一个调试器对象并将其传递给虚拟机的构造函数:

import assert from 'assert';
import readSource from './read-source.js';

const main = () => {
    assert(process.argv.length === 5, 'Usage: run-debugger.js ./vm ./debugger input|-');
    const VM = require(process.argv[2]);
    const Debugger = require(process.argv[3]);
    const inFile = process.argv[4];
    const lines = readSource(inFile);
    const dbg = new Debugger();
    const vm = new VM(lines, dbg);
    vm.run();
}

main();

一个简单的调试器可以在运行时跟踪重要语句:

import DebuggerBase from './debugger-base.js';

class DebuggerTrace extends DebuggerBase {
    handle(env, lineNum, op) {
        if (lineNum!== null) {
            console.log(`${lineNum} / ${op}: ${JSON.stringify(env)}`);
        }
    }
}

export default DebuggerTrace;

我们可以在一个对数组元素求和的程序上测试这个调试器:

[
    [1, "defA", "a", ["data", -5, 1, 3]],
    [2, "defV", "total", ["num", 0]],
    [3, "defV", "i", ["num", 0]],
    [4, "loop", ["lt", ["getV", "i"], ["len", "a"]],
        [5, "setV", "total", ["add", ["getV", "total"], ["getA", "a", ["getV", "i"]]]],
        [8, "setV", "i", ["add", ["getV", "i"], ["num", 1]]]
    ],
    [10, "print", ["getV", "total"]]
]

运行结果如下:

1 / defA: {}
2 / defV: {"a":[-5,1,3]}
3 / defV: {"a":[-5,1,3],"total":0}
4 / loop: {"a":[-5,1,3],"total":0,"i":0}
5 / setV: {"a":[-5,1,3],"total":0,"i":0}
8 / setV: {"a":[-5,1,3],"total":-5,"i":0}
5 / setV: {"a":[-5,1,3],"total":-5,"i":1}
8 / setV: {"a":[-5,1,3],"total":-4,"i":1}
5 / setV: {"a":[-5,1,3],"total":-4,"i":2}
8 / setV: {"a":[-5,1,3],"total":-1,"i":2}
10 / print: {"a":[-5,1,3],"total":-1,"i":3}
>> -1
4. 使调试器具有交互性

目前我们构建的调试器类似于一个始终开启的打印语句,为了将其转变为交互式调试器,我们使用 prompt-sync 模块来处理用户输入,支持以下命令:
| 命令 | 描述 |
| ---- | ---- |
| ? help | 列出所有命令 |
| clear # | 清除指定行号的断点 |
| list | 列出所有行和断点 |
| next | 前进一行 |
| print name | 在断点处显示变量的值 |
| run | 运行到下一个断点 |
| stop # | 在指定行号设置断点 |
| variables | 列出所有变量名 |
| exit | 立即退出 |

调试器的整体结构如下:

import prompt from 'prompt-sync';
import DebuggerBase from './debugger-base.js';
const PROMPT_OPTIONS = { sigint: true };

class DebuggerInteractive extends DebuggerBase {
    constructor() {
        super();
        this.singleStep = true;
        this.breakpoints = new Set();
        this.lookup = {
            '?': 'help',
            c: 'clear',
            l: 'list',
            n: 'next',
            p: 'print',
            r: 'run',
            s: 'stop',
            v: 'variables',
            x: 'exit'
        };
    }
    handle(env, lineNum, op) {
        if (lineNum === null) {
            return;
        }
        if (this.singleStep) {
            this.singleStep = false;
            this.interact(env, lineNum, op);
        } else if (this.breakpoints.has(lineNum)) {
            this.interact(env, lineNum, op);
        }
    }
}

export default DebuggerInteractive;

调试器与用户交互的过程如下:

interact(env, lineNum, op) {
    let interacting = true;
    while (interacting) {
        const command = this.getCommand(env, lineNum, op);
        if (command.length === 0) {
            continue;
        }
        const [cmd, ...args] = command;
        if (cmd in this) {
            interacting = this[cmd](env, lineNum, op, args);
        } else if (cmd in this.lookup) {
            interacting = this[this.lookup[cmd]](env, lineNum, op, args);
        } else {
            this.message(`unknown command ${command} (use '?' for help)`);
        }
    }
}
getCommand(env, lineNum, op) {
    const options = Object.keys(this.lookup).sort().join('');
    const display = `[${lineNum} ${options}] `;
    return this.input(display)
      .split(/\s+/)
      .map(s => s.trim())
      .filter(s => s.length > 0);
}
input(display) {
    return prompt(PROMPT_OPTIONS)(display);
}

以下是一些命令处理方法的示例:

next(env, lineNum, op, args) {
    this.singleStep = true;
    return false;
}
print(env, lineNum, op, args) {
    if (args.length!== 1) {
        this.message('p[rint] requires one variable name');
    } else if (!(args[0] in env)) {
        this.message(`unknown variable name "${args[0]}"`);
    } else {
        this.message(JSON.stringify(env[args[0]]));
    }
    return true;
}

由于最初的设计没有考虑到在循环每次运行时停止并知道当前位置,我们对 loop 方法进行了修改:

import VirtualMachineCallback from './vm-callback.js';

class VirtualMachineInteractive extends VirtualMachineCallback {
    loop(args, lineNum) {
        this.checkBody('loop', 1, args);
        const body = args.slice(1);
        while (this.exec(args[0])) {
            this.dbg.handle(this.env, lineNum, 'loop');
            this.runAll(body);
        }
    }
}

export default VirtualMachineInteractive;
5. 测试交互式应用程序

为了测试像调试器这样的交互式应用程序,我们采用将其转换为非交互式的方法,基于 Expect 程序实现。该方法的核心是用回调函数替换被测试应用程序的输入和输出函数,在需要时提供输入并检查输出。

以下是测试代码的示例:

describe('interactive debugger', () => {
    it('runs and prints', (done) => {
        setup('print-0.json')
          .get('[1 ?clnprsvx] ')
          .send('r')
          .get('>> 0')
          .run();
        done();
    });
    it('breaks and resumes', (done) => {
        setup('print-3.json')
          .get('[1 ?clnprsvx] ')
          .send('s 3')
          .get('[1 ?clnprsvx] ')
          .send('r')
          .get('>> 0')
          .get('>> 1')
          .get('[3 ?clnprsvx] ')
          .send('x')
          .run();
        done();
    });
});

Expect 类的实现如下:

import assert from 'assert';

class Expect {
    constructor(subject, start) {
        this.start = start;
        this.steps = [];
        subject.setTester(this);
    }
    send(text) {
        this.steps.push({ op: 'toSystem', arg: text });
        return this;
    }
    get(text) {
        this.steps.push({ op: 'fromSystem', arg: text });
        return this;
    }
    run() {
        this.start();
        assert.strictEqual(this.steps.length, 0, 'Extra steps at end of test');
    }
    toSystem() {
        return this.next('toSystem');
    }
    fromSystem(actual) {
        const expected = this.next('fromSystem');
        assert.strictEqual(expected, actual, `Expected "${expected}" got "${actual}"`);
    }
    next(kind) {
        assert(this.steps.length > 0, 'Unexpected end of steps');
        assert.strictEqual(this.steps[0].op, kind, `Expected ${kind}, got "${this.steps[0].op}"`);
        const text = this.steps[0].arg;
        this.steps = this.steps.slice(1);
        return text;
    }
}

export default Expect;

为了让调试器能够使用测试工具,我们对调试器进行了修改:

import DebuggerInteractive from './debugger-interactive.js';

class DebuggerTest extends DebuggerInteractive {
    constructor() {
        super();
        this.tester = null;
    }
    setTester(tester) {
        this.tester = tester;
    }
    input(display) {
        this.tester.fromSystem(display);
        return this.tester.toSystem();
    }
    message(m) {
        this.tester.fromSystem(m);
    }
}

export default DebuggerTest;

同时,我们编写了一个 setup 函数来确保所有组件正确连接:

import Expect from '../expect.js';
import VM from '../vm-interactive.js';
import Debugger from '../debugger-test.js';
import readSource from '../read-source.js';
import path from 'path';

const setup = (filename) => {
    const lines = readSource(path.join('debugger/test', filename));
    const dbg = new Debugger();
    const vm = new VM(lines, dbg);
    return new Expect(dbg, () => vm.run());
}

最初运行测试时遇到了问题,调试器的 exit 命令在模拟程序结束时调用 process.exit ,导致整个程序(包括虚拟机、调试器和测试框架)在测试完成前就停止了。为了解决这个问题,我们定义了一个自定义异常 HaltException

class HaltException {
}

export default HaltException;

修改调试器,在退出时抛出该异常:

import HaltException from './halt-exception.js';
import DebuggerTest from './debugger-test.js';

class DebuggerExit extends DebuggerTest {
    exit(env, lineNum, op, args) {
        throw new HaltException();
    }
}

export default DebuggerExit;

修改虚拟机,在捕获到该异常时正常结束:

import HaltException from './halt-exception.js';
import VirtualMachineInteractive from './vm-interactive.js';

class VirtualMachineExit extends VirtualMachineInteractive {
    run() {
        this.env = {};
        try {
            this.runAll(this.program);
        } catch (exc) {
            if (exc instanceof HaltException) {
                return;
            }
            throw exc;
        }
    }
}

export default VirtualMachineExit;

经过这些修改,我们终于能够成功测试交互式调试器:

npm run test -- -g 'exitable debugger'

测试结果如下:

exitable debugger
✓ runs and prints
✓ breaks and resumes
2 passing (7ms)
6. 练习
  • 实现制表符补全 :阅读 prompt-sync 的文档,为调试器实现制表符补全功能。
  • 运行时修改变量 :添加一个 set 命令,用于在程序运行时修改变量的值。考虑如何处理数组元素的设置。
  • 使输出更易读 :修改跟踪调试器,对循环和条件语句内的语句进行缩进,提高可读性。
  • 优化循环处理 :改进现有的循环处理方法,使其更加严谨。
  • 使用标志控制执行 :修改调试器和虚拟机,使用一个“继续执行”标志代替抛出异常来控制程序结束。比较两种方法的易理解性和可扩展性。
  • 行号编号 :编写一个工具,为没有行号的 JSON 程序表示添加行号,用于调试。
  • 实现下一次循环迭代命令 :实现一个“下一次循环迭代”命令,使程序运行到当前循环的下一次迭代的当前位置。
  • 使用查找表管理对象依赖 :修改虚拟机和调试器,使用查找表来管理对象之间的相互依赖。
  • 实现观察点 :修改调试器和虚拟机,实现观察点功能,当变量的值发生变化时暂停程序。
  • JSON 到汇编代码的转换 :编写一个工具,将 JSON 程序表示转换为汇编代码。为了简化,增加寄存器数量以确保在进行算术运算时有足够的中间结果存储。

调试器:原理、实现与测试(续)

7. 各项练习的详细分析与实现思路
7.1 实现制表符补全

要为调试器实现制表符补全功能,我们需要借助 prompt-sync 模块提供的相关功能。以下是实现步骤:
1. 了解 prompt-sync 文档 :仔细阅读 prompt-sync 的文档,明确其支持的补全功能接口。
2. 定义补全规则 :确定调试器命令的补全规则,例如根据已输入的命令前缀,从可用命令列表中查找匹配项。
3. 实现补全函数 :编写一个函数,根据用户输入的部分命令,返回可能的补全结果。

import prompt from 'prompt-sync';

const availableCommands = ['?', 'clear', 'list', 'next', 'print', 'run', 'stop', 'variables', 'exit'];

const completer = (input) => {
    const hits = availableCommands.filter((cmd) => cmd.startsWith(input));
    return [hits.length? hits : availableCommands, input];
};

const promptSync = prompt({ completer });

const command = promptSync('Enter command: ');
console.log(`You entered: ${command}`);
7.2 运行时修改变量

要在程序运行时修改变量的值,我们需要添加一个 set 命令。以下是实现步骤:
1. 添加 set 命令处理方法 :在调试器类中添加 set 方法,用于处理 set 命令。
2. 处理变量赋值 :根据用户输入的变量名和新值,更新虚拟机的环境变量。
3. 处理数组元素设置 :考虑如何处理数组元素的设置,例如通过索引指定要修改的数组元素。

class DebuggerInteractive {
    // ... 其他代码 ...

    set(env, lineNum, op, args) {
        if (args.length!== 2) {
            this.message('set requires variable name and value');
            return true;
        }
        const [varName, value] = args;
        if (varName in env) {
            if (Array.isArray(env[varName])) {
                // 处理数组元素设置
                const index = parseInt(value);
                if (!isNaN(index) && index < env[varName].length) {
                    // 假设用户输入的新值是下一个参数
                    const newValue = args[2];
                    env[varName][index] = newValue;
                } else {
                    this.message('Invalid array index');
                }
            } else {
                env[varName] = value;
            }
        } else {
            this.message(`unknown variable name "${varName}"`);
        }
        return true;
    }
}
7.3 使输出更易读

为了使跟踪调试器的输出更易读,我们可以对循环和条件语句内的语句进行缩进。以下是实现步骤:
1. 记录缩进级别 :在虚拟机的 exec 方法中,记录当前的缩进级别。
2. 根据缩进级别输出 :在调试器的 handle 方法中,根据缩进级别对输出进行缩进。

class VirtualMachineCallback {
    constructor(program, dbg) {
        super(program);
        this.dbg = dbg;
        this.dbg.setVM(this);
        this.indentLevel = 0;
    }
    exec(command) {
        const [op, lineNum, ...args] = command;
        this.dbg.handle(this.env, lineNum, op, this.indentLevel);
        assert(op in this, `Unknown op "${op}"`);
        if (op === 'loop' || op === 'test') {
            this.indentLevel++;
        }
        const result = this[op](args, lineNum);
        if (op === 'loop' || op === 'test') {
            this.indentLevel--;
        }
        return result;
    }
}

class DebuggerTrace {
    handle(env, lineNum, op, indentLevel) {
        if (lineNum!== null) {
            const indent = '  '.repeat(indentLevel);
            console.log(`${indent}${lineNum} / ${op}: ${JSON.stringify(env)}`);
        }
    }
}
7.4 优化循环处理

现有的循环处理方法比较松散,我们可以进行优化。以下是优化思路:
1. 分离循环条件和循环体 :将循环条件和循环体的处理逻辑分离,提高代码的可读性和可维护性。
2. 使用更清晰的变量命名 :使用更具描述性的变量名,使代码更易理解。

class VirtualMachineInteractive {
    loop(args, lineNum) {
        this.checkBody('loop', 1, args);
        const condition = args[0];
        const body = args.slice(1);
        while (this.exec(condition)) {
            this.dbg.handle(this.env, lineNum, 'loop');
            this.runAll(body);
        }
    }
}
7.5 使用标志控制执行

我们可以修改调试器和虚拟机,使用一个“继续执行”标志代替抛出异常来控制程序结束。以下是实现步骤:
1. 添加标志变量 :在调试器和虚拟机中添加一个标志变量,用于控制程序是否继续执行。
2. 修改命令处理方法 :在调试器的命令处理方法中,根据用户输入更新标志变量。
3. 修改虚拟机的执行逻辑 :在虚拟机的执行逻辑中,检查标志变量的值,决定是否继续执行。

class DebuggerInteractive {
    constructor() {
        super();
        this.singleStep = true;
        this.breakpoints = new Set();
        this.continueExecuting = true;
        this.lookup = {
            '?': 'help',
            c: 'clear',
            l: 'list',
            n: 'next',
            p: 'print',
            r: 'run',
            s: 'stop',
            v: 'variables',
            x: 'exit'
        };
    }
    exit(env, lineNum, op, args) {
        this.continueExecuting = false;
        return false;
    }
}

class VirtualMachineInteractive {
    run() {
        this.env = {};
        while (this.dbg.continueExecuting) {
            this.runAll(this.program);
        }
    }
}
7.6 行号编号

编写一个工具,为没有行号的 JSON 程序表示添加行号。以下是实现步骤:
1. 遍历 JSON 程序 :遍历 JSON 程序的每个命令,为其添加行号。
2. 定义“有趣”的语句 :根据自己的定义,确定哪些语句是“有趣”的,需要添加行号。

function addLineNumbers(program) {
    let lineNumber = 1;
    const result = [];
    for (const command of program) {
        if (Array.isArray(command) && command.length > 0) {
            result.push([lineNumber, ...command]);
            lineNumber++;
        }
    }
    return result;
}

const program = [
    ["defA", "a", ["data", -3, -5, -1, 0, -2, 1, 3, 1]],
    ["defA", "b", ["data"]]
];

const numberedProgram = addLineNumbers(program);
console.log(numberedProgram);
7.7 实现下一次循环迭代命令

要实现一个“下一次循环迭代”命令,我们需要记录当前循环的状态。以下是实现步骤:
1. 记录循环状态 :在虚拟机中记录当前循环的信息,例如循环的起始位置和迭代次数。
2. 实现命令处理方法 :在调试器中添加一个命令处理方法,用于执行“下一次循环迭代”命令。
3. 运行程序到下一次迭代 :根据记录的循环状态,运行程序直到下一次迭代的当前位置。

class VirtualMachineInteractive {
    constructor(program, dbg) {
        super(program);
        this.dbg = dbg;
        this.dbg.setVM(this);
        this.currentLoop = null;
    }
    loop(args, lineNum) {
        this.checkBody('loop', 1, args);
        const condition = args[0];
        const body = args.slice(1);
        this.currentLoop = { lineNum, condition, body, iteration: 0 };
        while (this.exec(condition)) {
            this.dbg.handle(this.env, lineNum, 'loop');
            this.runAll(body);
            this.currentLoop.iteration++;
        }
        this.currentLoop = null;
    }
}

class DebuggerInteractive {
    nextLoopIteration(env, lineNum, op, args) {
        if (this.vm.currentLoop) {
            const { lineNum: loopLineNum, condition, body, iteration } = this.vm.currentLoop;
            while (this.vm.exec(condition) && this.vm.currentLoop.iteration <= iteration) {
                this.vm.runAll(body);
                this.vm.currentLoop.iteration++;
            }
        }
        return true;
    }
}
7.8 使用查找表管理对象依赖

修改虚拟机和调试器,使用查找表来管理对象之间的相互依赖。以下是实现步骤:
1. 创建查找表 :创建一个查找表对象,用于存储对象的引用。
2. 初始化对象时注册 :在对象的构造函数中,将对象注册到查找表中。
3. 使用查找表获取对象引用 :在需要使用其他对象时,通过查找表获取对象的引用。

const lookupTable = {
    set(name, obj) {
        this[name] = obj;
    },
    lookup(name) {
        return this[name];
    }
};

class VirtualMachine {
    constructor(program) {
        this.program = program;
        lookupTable.set('VirtualMachine', this);
    }
    run() {
        const dbg = lookupTable.lookup('Debugger');
        // 使用 dbg 进行调试操作
    }
}

class Debugger {
    constructor() {
        lookupTable.set('Debugger', this);
    }
}
7.9 实现观察点

要实现观察点功能,我们需要在变量值发生变化时暂停程序。以下是实现步骤:
1. 记录观察变量 :在调试器中记录需要观察的变量名。
2. 修改变量赋值操作 :在虚拟机的变量赋值操作中,检查变量是否为观察变量,如果是,则暂停程序。

class DebuggerInteractive {
    constructor() {
        super();
        this.watchpoints = new Set();
    }
    watch(env, lineNum, op, args) {
        if (args.length!== 1) {
            this.message('watch requires variable name');
            return true;
        }
        const [varName] = args;
        this.watchpoints.add(varName);
        return true;
    }
}

class VirtualMachineInteractive {
    setV(args, lineNum) {
        this.checkOp('setV', 2, args);
        const [name, value] = args;
        const oldValue = this.env[name];
        this.env[name] = this.exec(value);
        const dbg = lookupTable.lookup('Debugger');
        if (dbg.watchpoints.has(name) && this.env[name]!== oldValue) {
            dbg.handle(this.env, lineNum, 'watchpoint');
        }
    }
}
7.10 JSON 到汇编代码的转换

编写一个工具,将 JSON 程序表示转换为汇编代码。以下是实现步骤:
1. 定义汇编指令集 :确定汇编代码的指令集,例如加法、减法、赋值等。
2. 遍历 JSON 程序 :遍历 JSON 程序的每个命令,根据命令类型生成相应的汇编指令。
3. 处理寄存器分配 :为了简化,增加寄存器数量以确保在进行算术运算时有足够的中间结果存储。

function jsonToAssembly(jsonProgram) {
    const assemblyCode = [];
    let registerIndex = 0;

    const getNextRegister = () => {
        return `R${registerIndex++}`;
    };

    for (const command of jsonProgram) {
        const [op, ...args] = command;
        if (op === 'defV') {
            const [name, value] = args;
            const register = getNextRegister();
            assemblyCode.push(`MOV ${register}, ${value}`);
            assemblyCode.push(`MOV ${name}, ${register}`);
        } else if (op === 'add') {
            const [left, right] = args;
            const resultRegister = getNextRegister();
            assemblyCode.push(`ADD ${resultRegister}, ${left}, ${right}`);
        }
    }
    return assemblyCode;
}

const jsonProgram = [
    ["defV", "a", ["num", 5]],
    ["defV", "b", ["num", 3]],
    ["add", ["getV", "a"], ["getV", "b"]]
];

const assemblyCode = jsonToAssembly(jsonProgram);
console.log(assemblyCode);
8. 总结

通过以上的实现和练习,我们深入了解了调试器的工作原理、实现方法以及测试技巧。从程序的 JSON 表示、源映射的构建、交互式调试器的实现,到测试交互式应用程序的方法,我们逐步构建了一个完整的调试器系统。同时,通过各项练习,我们进一步优化了调试器的功能,提高了其易用性和可扩展性。在实际开发中,调试器是一个非常重要的工具,掌握调试器的原理和实现方法,有助于我们更快地定位和解决程序中的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值