Skip to content

Commit cedec72

Browse files
committed
支持supportsDisassembleRequest (53/54/55)
1 parent bbb41fb commit cedec72

13 files changed

Lines changed: 1549 additions & 5 deletions

File tree

CLAUDE.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## 构建系统
6+
7+
使用 [luamake](https://github.com/actboy168/luamake) 作为构建工具(基于 Lua)。
8+
9+
```bash
10+
# 先安装 luamake(见 README),然后:
11+
luamake lua compile/download_deps.lua # 下载依赖
12+
luamake -mode release # 构建
13+
luamake test # 仅构建测试
14+
```
15+
16+
## 架构
17+
18+
这是一个 VS Code 调试器扩展,支持调试 Lua 5.1–5.5 和 LuaJIT。扩展入口是 `extension/js/extension.js`,但大部分逻辑在 `extension/script/` 下的 Lua 脚本中。
19+
20+
### 三进程调试模型
21+
22+
```
23+
VS Code (JS) ──DAP──> Frontend (Lua proxy) ──socket──> Master (Lua 线程)
24+
extension/script/frontend/ extension/script/backend/master/
25+
26+
Master ──bee.channel──> Worker (Lua 线程,在目标进程内运行)
27+
extension/script/backend/worker/
28+
```
29+
30+
- **Frontend (`frontend/proxy.lua`)**:桥接 DAP 协议(Content-Length 头 + JSON)和 Master。负责 launch/attach 编排、进程管理、WSL 路径转换。
31+
- **Master (`backend/master/mgr.lua`)**:会话生命周期管理,将 DAP 请求路由到 Worker,管理线程状态,捕获 stdout/stderr。
32+
- **Worker (`backend/worker.lua`)**:在独立 Lua VM 中运行。处理断点、单步、调用栈、变量查看、表达式求值。通过命名 `bee.channel` 通道与 Master 通信。
33+
34+
### 调试器与调试目标的隔离
35+
36+
**Worker 和被调试的 Lua VM 是两个完全独立的 Lua 虚拟机。** 调试器的 Lua 脚本运行在 `luadbg`(内嵌的 Lua 解释器变体)中,被调试的目标程序运行在另一个 Lua VM(可能是不同的 Lua 版本)。两者不能直接传递 Lua 对象(function、userdata 等)。
37+
38+
跨 VM 通信通过原生 C++ 模块桥接:
39+
40+
- **`rdebug_debughost.cpp`** — Worker VM 的入口。`luaopen_luadebug` 在目标进程中创建 Worker VM(`luadbg_State`),将目标 VM 的 `lua_State*` 注册到 Worker 的注册表中。
41+
- **`util/refvalue.h`** — 引用值系统。Worker 中的变量(function、table、userdata 等)实际存储为 **refvalue**(tagged union 的 userdata),描述*如何*在目标 VM 中定位该值(如"第 N 帧的第 M 个局部变量"、"table 数组的第 K 个元素")。通过 `refvalue::eval(v, hL)` 解析引用,将实际值推入目标 VM 的栈上。
42+
- **`rdebug_visitor.cpp`** — 导出给 Worker Lua 的函数表(`luadebug.visitor` 模块)。每个函数通过 `debughost::get(L)` 获取目标 `lua_State*`,然后操作目标 VM。`copy_to_dbg`/`copy_from_dbg` 在两个 VM 之间传递简单类型(nil、bool、number、string、lightuserdata)的值。
43+
- **`rdebug_hookmgr.cpp`** — Hook 管理。直接操作目标 VM 的 hook 机制。
44+
45+
**这意味着**:Worker Lua 脚本中不能对目标 VM 的值直接调用 `string.dump()``pairs()` 等 Lua 标准函数。必须通过 `rdebug` 原生模块提供的函数来间接操作。
46+
47+
### 原生 C++ 库 (`src/luadebug/`)
48+
49+
编译为动态库(`luadebug.dll`/`.so`)。Worker 将其加载到目标进程中以 hook Lua VM:
50+
- `rdebug_init.cpp` — DLL 入口,在目标进程中定位 Lua DLL
51+
- `rdebug_hookmgr.cpp` — Hook 管理(单步/断点 hook)
52+
- `rdebug_visitor.cpp` — 栈帧/变量查看
53+
- `rdebug_debughost.cpp` — Worker VM 生命周期管理,双 VM 绑定
54+
- `compat/` — Lua 版本抽象层(5.x 通过 `5x/`,LuaJIT 通过 `jit/`
55+
- `thunk/` — 各架构的 hook 跳板代码(x86、x64、ARM64,Windows/macOS/Linux)
56+
- `luadbg/` — 内嵌的 Lua 解释器变体(Worker 的运行时)
57+
- `util/refvalue.h` — 引用值系统,跨 VM 值的懒解析
58+
59+
### 启动器 (`src/launcher/`)
60+
61+
一个小型可执行文件,用于启动目标 Lua 进程或附加到运行中的进程,然后注入调试器 DLL。
62+
63+
### 扩展 Lua 脚本 (`extension/script/`)
64+
65+
| 目录 | 用途 |
66+
|---|---|
67+
| `frontend/` | VS Code DAP 代理、进程创建、平台检测 |
68+
| `backend/master/` | 请求路由、线程管理、事件处理 |
69+
| `backend/worker/` | 断点、表达式求值、变量、序列化、源码映射 |
70+
| `common/` | 共享:协议组帧、JSON、IPC、网络、base64 |
71+
72+
## 关键文件
73+
74+
- `make.lua` — 根构建入口
75+
- `extension/package.json` — VS Code 扩展清单
76+
- `extension/script/debugger.lua` — 调试器入口,由目标进程通过 `dofile` 加载
77+
- `extension/script/launch.lua` / `attach.lua` — 注入目标进程的启动脚本
78+
- `extension/script/backend/bootstrap.lua` — 创建 Master 线程,然后在当前线程初始化 Worker
79+
- `extension/script/common/protocol.lua` — DAP 线协议(Content-Length 头 + JSON)
80+
- `extension/script/backend/worker/hookmgr.lua` — Lua hook 管理器,以 `luadebug.hookmgr` 加载
81+
- `compile/common/config.lua` — 默认构建配置(C11、C++17、debug 模式)
82+
83+
## 代码风格
84+
85+
- Lua:4 空格缩进,数学运算符两侧加空格,函数调用括号前不加空格,保留尾部表分隔符
86+
- C/C++:clang-format,配置文件在 `.clang-format``.clang-format-ignore`
87+
- `.editorconfig` 中有完整的 Lua 格式化规则
88+
89+
## Git 子模块
90+
91+
本仓库使用 git submodule。克隆或切换分支后若检测到子模块变更,请始终运行 `git submodule update --init`

docs/capabilities.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
| supportsDataBreakpoints | 🟨 |
3232
| supportsReadMemoryRequest | 🟩 |
3333
| supportsWriteMemoryRequest | 🟩 |
34-
| supportsDisassembleRequest | 🟨 |
34+
| supportsDisassembleRequest | 🟩 |
3535
| supportsCancelRequest | 🟨 |
3636
| supportsBreakpointLocationsRequest | 🟨 |
3737
| supportsClipboardContext | 🟩 |

extension/script/backend/master/request.lua

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,35 @@ function request.writeMemory(req)
633633
})
634634
end
635635

636+
function request.disassemble(req)
637+
local args = req.arguments
638+
local memoryReference = args.memoryReference
639+
-- inst_<threadId>x<rest> (from instructionPointerReference)
640+
local threadId, refId = memoryReference:match("inst_(%d+)x(.+)$")
641+
if not refId then
642+
-- memory_<threadId>x<refId> (from variable with memoryReference)
643+
threadId, refId = memoryReference:match("memory_(%d+)x(%d+)")
644+
end
645+
threadId = tonumber(threadId)
646+
if not threadId or not refId then
647+
response.error(req, "Invalid memoryReference")
648+
return
649+
end
650+
if not checkThreadId(req, threadId) then
651+
return
652+
end
653+
mgr.workerSend(threadId, {
654+
cmd = 'disassemble',
655+
command = req.command,
656+
seq = req.seq,
657+
refId = refId,
658+
offset = args.offset,
659+
instructionOffset = args.instructionOffset,
660+
instructionCount = args.instructionCount,
661+
resolveSymbols = args.resolveSymbols,
662+
})
663+
end
664+
636665
function request.customRequestShowIntegerAsDec(req)
637666
response.success(req)
638667
mgr.workerBroadcast {

extension/script/backend/master/threads.lua

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ function CMD.stackTrace(w, req)
6060
if frame.source and frame.source.sourceReference then
6161
frame.source.sourceReference = (w << 32) | frame.source.sourceReference
6262
end
63+
if frame.instructionPointerReference then
64+
frame.instructionPointerReference = ("inst_%dx%s"):format(w, frame.instructionPointerReference:match("^inst_(.+)$"))
65+
end
6366
end
6467
response.success(req, req.body)
6568
end
@@ -151,6 +154,14 @@ function CMD.writeMemory(_, req)
151154
response.success(req, req.body)
152155
end
153156

157+
function CMD.disassemble(_, req)
158+
if not req.success then
159+
response.error(req, req.message)
160+
return
161+
end
162+
response.success(req, req.body)
163+
end
164+
154165
function CMD.eventMemory(w, req)
155166
req.memoryReference = "memory_" .. w .. "x" .. req.memoryReference
156167
event.memory(req)

extension/script/backend/worker.lua

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ local thread = require 'bee.thread'
1313
local fs = require 'backend.worker.filesystem'
1414
local log = require 'common.log'
1515
local channel = require "bee.channel"
16+
local disassemble = require 'backend.worker.disassemble'
1617
local initialized = false
1718
local suspend = false
1819
local info = {}
@@ -197,6 +198,12 @@ local function stackTrace(res, coid, start, levels)
197198
}
198199
if info.what ~= 'C' then
199200
r.column = 1
201+
if rdebug.currentpc then
202+
local pc = rdebug.currentpc(depth)
203+
if pc >= 0 then
204+
r.instructionPointerReference = ("inst_%dx%dx%d"):format(coid, depth, pc)
205+
end
206+
end
200207
local src = source.create(info.source)
201208
if source.valid(src) then
202209
r.line = source.line(src, info.currentline)
@@ -572,6 +579,95 @@ function CMD.customRequestShowIntegerAsHex()
572579
}
573580
end
574581

582+
function CMD.disassemble(pkg)
583+
if not rdebug.dumpproto then
584+
sendToMaster 'disassemble' {
585+
command = pkg.command,
586+
seq = pkg.seq,
587+
success = false,
588+
message = "Disassemble not supported for this Lua version",
589+
}
590+
return
591+
end
592+
local proto
593+
local defaultOffset = 0
594+
local refId = pkg.refId
595+
if type(refId) == "string" and refId:match("^%d+x%d+x%d+$") then
596+
local coid, depth, pc = refId:match("^(%d+)x(%d+)x(%d+)$")
597+
-- navigate to the correct coroutine
598+
local L = baseL
599+
for _ = 0, tonumber(coid) - 1 do
600+
L = coroutineFrom(L)
601+
if not L then break end
602+
end
603+
if L then
604+
hookmgr.sethost(L)
605+
proto = rdebug.dumpproto(tonumber(depth))
606+
hookmgr.sethost(baseL)
607+
end
608+
defaultOffset = tonumber(pc)
609+
end
610+
if not proto then
611+
local rtype, ref = variables.resolveMemoryRef(tonumber(refId))
612+
if ref and rtype == "function" then
613+
proto = rdebug.dumpproto(ref)
614+
end
615+
end
616+
if not proto or not proto.code or #proto.code == 0 then
617+
sendToMaster 'disassemble' {
618+
command = pkg.command,
619+
seq = pkg.seq,
620+
success = false,
621+
message = "Cannot dump function",
622+
}
623+
return
624+
end
625+
local instructions = disassemble.disassemble_function(proto, luaver.LUAVERSION)
626+
local byteOffset = math.floor((pkg.offset or 0) / 4)
627+
local instOffset = defaultOffset + byteOffset + (pkg.instructionOffset or 0)
628+
local instCount = pkg.instructionCount or #instructions
629+
local result = {}
630+
local firstLocation = nil
631+
if proto.source then
632+
local src = source.create(proto.source)
633+
if source.valid(src) then
634+
firstLocation = source.output(src)
635+
end
636+
end
637+
local start = math.max(1, instOffset + 1)
638+
for i = start, math.min(start + instCount - 1, #instructions) do
639+
local inst = instructions[i]
640+
local r = {
641+
address = inst.address,
642+
instructionBytes = inst.instructionBytes,
643+
instruction = inst.opName .. " " .. inst.operands,
644+
}
645+
if inst.line then
646+
r.line = inst.line
647+
r.column = 1
648+
end
649+
if inst.symbol then
650+
r.symbol = inst.symbol
651+
end
652+
if inst.presentationHint then
653+
r.presentationHint = inst.presentationHint
654+
end
655+
if firstLocation then
656+
r.location = firstLocation
657+
firstLocation = nil
658+
end
659+
result[#result + 1] = r
660+
end
661+
sendToMaster 'disassemble' {
662+
command = pkg.command,
663+
seq = pkg.seq,
664+
success = true,
665+
body = {
666+
instructions = result,
667+
},
668+
}
669+
end
670+
575671
local function runLoop(reason, level)
576672
baseL = hookmgr.gethost()
577673
--TODO: 只在lua栈帧时需要text?
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
-- shared disassembly logic, version dispatch
2+
-- version is always 53/54/55 (luaver.LUAVERSION format)
3+
4+
local modules = {}
5+
6+
-- Lua 5.3: lineinfo stores direct line numbers
7+
local function get_source_line_53(func, pc)
8+
if not func.lineinfo or #func.lineinfo == 0 then return nil end
9+
local idx = pc + 1
10+
if idx <= #func.lineinfo then return func.lineinfo[idx] end
11+
end
12+
13+
-- Lua 5.4+: lineinfo stores signed byte deltas with abslineinfo anchors
14+
local function get_source_line_54plus(func, pc)
15+
if not func.lineinfo or #func.lineinfo == 0 then return nil end
16+
local basepc = -1
17+
local baseline = func.linedefined or 0
18+
if func.abslineinfo and #func.abslineinfo > 0 then
19+
if pc < func.abslineinfo[1].pc then
20+
basepc = -1
21+
baseline = func.linedefined
22+
else
23+
local i = math.floor(pc / 128) - 1
24+
if i < 1 then i = 1 end
25+
while i + 1 <= #func.abslineinfo and pc >= func.abslineinfo[i + 1].pc do
26+
i = i + 1
27+
end
28+
basepc = func.abslineinfo[i].pc
29+
baseline = func.abslineinfo[i].line
30+
end
31+
end
32+
local line = baseline
33+
local cur = basepc
34+
while cur < pc do
35+
cur = cur + 1
36+
if cur > #func.lineinfo then break end
37+
local li = func.lineinfo[cur + 1]
38+
if li == -128 and func.abslineinfo then
39+
for j = 1, #func.abslineinfo do
40+
if func.abslineinfo[j].pc > cur then
41+
cur = func.abslineinfo[j].pc
42+
line = func.abslineinfo[j].line
43+
break
44+
end
45+
end
46+
else
47+
line = line + li
48+
end
49+
end
50+
return line
51+
end
52+
53+
local function get_module(version)
54+
local v = version
55+
if not modules[v] then
56+
if v == 53 then
57+
modules[v] = require "backend.worker.disassemble.lua53"
58+
elseif v == 54 then
59+
modules[v] = require "backend.worker.disassemble.lua54"
60+
elseif v == 55 then
61+
modules[v] = require "backend.worker.disassemble.lua55"
62+
end
63+
end
64+
return modules[v]
65+
end
66+
67+
local function disassemble_function(func, version)
68+
local mod = get_module(version)
69+
if not mod then
70+
return {}
71+
end
72+
local instructions = {}
73+
if not func.code or #func.code == 0 then
74+
return instructions
75+
end
76+
77+
local get_line = version == 53 and get_source_line_53 or get_source_line_54plus
78+
local extra_arg = mod.precompute(func)
79+
80+
for i = 1, #func.code do
81+
local inst = func.code[i]
82+
local d = mod.decode(inst)
83+
local pc = i - 1
84+
local opName = mod.OPCODES[d.op + 1] or ("UNKNOWN_%d"):format(d.op)
85+
local operands = mod.format(d)
86+
local cmt = mod.comment(opName, d, func, pc, extra_arg[pc])
87+
local line = get_line(func, pc)
88+
local hint = nil
89+
if opName == "EXTRAARG" or opName == "VARARGPREP" then
90+
hint = "invalid"
91+
end
92+
local r = {
93+
pc = pc,
94+
opName = opName,
95+
operands = operands,
96+
comment = cmt,
97+
line = line,
98+
address = ("0x%08X"):format(pc),
99+
instructionBytes = ("%08X"):format(inst & 0xFFFFFFFF),
100+
}
101+
if hint then
102+
r.presentationHint = hint
103+
end
104+
if mod.symbol then
105+
local s = mod.symbol(func, pc, d)
106+
if s then r.symbol = s end
107+
end
108+
instructions[#instructions + 1] = r
109+
end
110+
return instructions
111+
end
112+
113+
return {
114+
disassemble_function = disassemble_function,
115+
get_source_line = get_source_line,
116+
}

0 commit comments

Comments
 (0)