调试器:原理、实现与测试
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 表示、源映射的构建、交互式调试器的实现,到测试交互式应用程序的方法,我们逐步构建了一个完整的调试器系统。同时,通过各项练习,我们进一步优化了调试器的功能,提高了其易用性和可扩展性。在实际开发中,调试器是一个非常重要的工具,掌握调试器的原理和实现方法,有助于我们更快地定位和解决程序中的问题。
超级会员免费看

被折叠的 条评论
为什么被折叠?



