lua 代码的断点调试

转载自云风BLOG,转载请注源明出处

lua 代码的断点调试

Lua 5.1 带了一个 debug 库,把所有的 C API 中的 debug 相关 api 都导出了。作为独立的语言使用的话,这些足够搭建一套方便的调试库。

说到最常用的断点调试法,我们能想到的最直接的方法就是利用 lua debug 库中的 hook ,然后记录一张断点位置表,设置行模式的 hook ,每次进入 hook 都检查是否是断点处,若是就停下来等待交互调试。

这个方法很有效,但是很消耗 cpu 。因为每进入一个新的代码行,都需要回调一个函数。当这个函数本身又是用 lua 写的时候,效率更低。

本文提供另一种思路,换一个方法设置断点,让没有断点时不影响运行效率。

简单的说就是在代码中插入硬断点。

C 语言调试时,很多调试器其实是动态的在进程中的程序码中插入 int 3 ,让程序执行到断点处可以触发调试中断,恢复运行时只需要把 int 3 抹去换上原本的机器码即可。

lua 虚拟机并没有提供调试中断指令,向 byte code 中插入代码也没有合适的途径。但是不要忘了,动态语言最大的好处之一就是解释执行,不需要编译链接。所以,我们把调试中断的调用硬编码到代码中也不是难事。调试之后去掉这些多余的代码即可。只要有硬编码进的调试断点,我们再为这些断点写动态的开关就是很简单的事情了。

今天我实现了这么一套简单的调试库,完全用 lua 本身实现的,只需要用 require "bp" 就可以加载进来使用。源代码我贴在了 wiki 上

下面讲讲简单的思路:

我把断点分成两类,一类是匿名断点,一类是具名断点。分别用 bp.bp() 和 bp.bp "name" 来设置。 匿名断点在第一次设置时初始化,并激活。也就是说,程序运行到 bp.bp() 处就会断下来。系统会为这个断点分配一个唯一 id 号。以后可以通过 id 来激活和关闭这个断点。方法是 bp.trigger(id,true/false) 默认 bp.trigger(id) 是激活断点。

这样的断点内部是用 closure 的对象地址做标识的,也就是说,同一个 function 的不同实例可以有相互独立的断点 id 。这样比靠源代码定位的方式更加灵活。closure 在动态语言中的使用非常广泛,相同一份代码可能在不同状态下干着不同的事情,我们可以为特定的 closure 设中断了。

当 closure 垃圾回收后,断点也会被回收。

具名断点适合对一批断点同时开关,因为有字符串名字,所以可以在断点运行到之前就设置好状态。有更高的可控性。

在云风的这个系统中,可以用 bp.list "name" 列出所有命名为 "name" 的断点所在代码位置,也就是说,可以给无限个断点起相同的名字,一致的控制。

bp.list() 可以列出系统中已有的所有断点。

为了配合这套断点调试系统,我附带做了几个小东西。一个就是递归打印 table ,函数名为 bp.print_r(tbl,limit=64)

由于 lua 的 table 有可能循环引用,或是元素太多,我在这里设置上输出上限,默认是 64 。超过上限的以 ... 结束。

另外,这里给 debug.debug() 这个调试控制台增加了方便的 local 变量以及 upvalue 的控制方法。原有的 debug.getlocal 和 debug.getupvalue 实在难用了点。

现在进入控制台后,可以通过 _L 和 _U 两张表,分别控制 local 和 upvalue 了。

可以在控制台试试这样一个例子:

require "bp"

function foo(arg) bp.bp() -- 设置一个匿名断点 return arg end

=foo(0) -- 输出 foo(0)

运行后我们发现立刻进入了调试环境,输出:

break point:    1       on      =stdin(2)
_L=     {arg=0,}
_U=     {}

表示 1 号断点中断,源码行是 stdin 的第 2 行。局部变量有 arg=0 一个,upvalue 没有。 现在可以输入 _L.arg=1 ,然后输入 cont 继续运行程序。你会发现屏幕输出是 1 而不是 0 。arg 变量已经被修改了。


大体上就是这样了,周末如果有空,我会把单步运行加上,基本就可以及的上简单的 gdb 功能了 :D



一个简单的调试系统:


---------------------------
--  bp.lua
---------------------------
 
local type=type
local tostring=tostring
local print=print
local setmetatable=setmetatable
local getfenv=getfenv
local ipairs=ipairs
local pairs=pairs
local xpcall=xpcall
local error=error
 
local table_insert=table.insert
local table_concat=table.concat
local debug=debug
 
module "bp"
 
local nil_value={}
 
local function traversal_r(tbl,num)
	num = num or 64
	local ret={}
	local function insert(v)
		table_insert(ret,v)
		if #ret>num then
			error()
		end
	end
	local function traversal(e)
		if e==nil_value or e==nil then
			insert("nil,")
		elseif type(e)=="table" then
			insert("{")
			local maxn=0
			for i,v in ipairs(e) do 
				traversal(v)
				maxn=i
			end
			for k,v in pairs(e) do
				if not (type(k)=="number" and k>0 and k<=maxn) then
					if type(k)=="number" then
						insert("["..k.."]=")
					else
						insert(tostring(k).."=")
					end
					traversal(v)
				end
			end
			insert("}")
		elseif type(e)=="string" then
			insert('"'..e..'",')
		else
			insert(tostring(e))
			insert(",")
		end
	end
 
	local err=xpcall(
		function() traversal(tbl) end,
		function() end
	)
	if not err then
		table_insert(ret,"...")
	end
 
	return table_concat(ret)
end
 
function print_r(tbl,num)
	print(traversal_r(tbl,num))
end
 
local function init_local(tbl,level)
	local n=1
	local index={}
	while true do
		local name,value=debug.getlocal(level,n)
		if not name then
			break
		end
 
		if name~="(*temporary)" then
			if value==nil then
				value=nil_value
			end
			
			tbl[name]=value
			index["."..name]=n
		end
 
		n=n+1
	end
	setmetatable(tbl,{__index=index})
	return tbl
end
 
local function init_upvalue(tbl,func)
	local n=1
	local index={}
	while true do
		local name,value=debug.getupvalue(func,n)
		if not name then
			break
		end
 
		if value==nil then
			value=nil_value
		end
		
		tbl[name]=value
		index["."..name]=n
 
		n=n+1
	end
	setmetatable(tbl,{__index=index})
	return tbl
end
 
 
function dbg(level)
	level=level and level+2 or 2
 
	local lv=init_local({},level+1)
	local func=debug.getinfo(level,"f").func
	local uv=init_upvalue({},func)
	local _L={}
	setmetatable(_L,{
		__index=function(_,k) 
			local ret=lv[k]
			return ret~=nil_value and ret or nil
		end,
		__newindex=function(_,k,v)
			if lv[k] then
				lv[k]= v~= nil and nil_value or v
				debug.setlocal(level+3,lv["."..k],v)
			else
				print("error:invalid local name:",k)
			end
		end,
		__tostring=function(_)
			return traversal_r(lv)
		end
	})
	print("_L=",traversal_r(lv))
	local _U={}
	setmetatable(_U,{
		__index=function(_,k) 
			local ret=uv[k]
			return ret~=nil_value and ret or nil
		end,
		__newindex=function(_,k,v)
			if uv[k] then
				uv[k]= v~= nil and nil_value or v
				debug.setupvalue(func,uv["."..k],v)
			else
				print("error:invalid upvalue name",k)
			end
		end,
		__tostring=function(_)
			return traversal_r(uv)
		end
	})
	print("_U=",traversal_r(uv))
 
	local _G=getfenv(level)
	_G._L,_G._U=_L,_U
	debug.debug()
	_G._L,_G._U=nil,nil
end
 
 
local _bp_list={}
local _bp_desc={}
 
local function bp_list_name(name)
	local t=type(_bp_desc[name])
	print("["..name.."]",_bp_list[name] and "on" or "off")
	if t=="table" then
		for _,v in ipairs(_bp_desc[name]) do
			print("----",v)
		end
	elseif t=="string" then
		print("---",_bp_desc[name])
	end
end
 
local function bp_list(name)
	if name then
		bp_list_name(name)
	else
		for k,v in pairs(_bp_list) do
			bp_list_name(k)
		end
	end
end
 
local _bp_unnamed={}
local _bp_unnamed_desc={}
local _bp_unnamed_index={}
local _bp_index=1
do
	local weak={__mode="kv"}
	setmetatable(_bp_unnamed,weak)
	setmetatable(_bp_unnamed_desc,{__mode="k"})
	setmetatable(_bp_unnamed_index,weak)
end
 
local function bp_add_index()
	local info=debug.getinfo(3,"Slf")
	local func=info.func
	if _bp_unnamed[info.func]==nil then
		local desc=info.source.."("..info.currentline..")"
		_bp_unnamed[func]=true
		_bp_unnamed_desc[func]=desc
		_bp_unnamed_index[func]=_bp_index
		_bp_unnamed_index[_bp_index]=func
		_bp_index=_bp_index+1
	end
	return _bp_unnamed[func],_bp_unnamed_index[func]
end
 
local _bp_named={}
local _bp_named_desc={}
 
local function bp_add_name(name)
	local info=debug.getinfo(3,"Sl")
	local desc=info.source.."("..info.currentline..")"
	if _bp_named[name]==nil then
		_bp_named[name]=false
	end
 
	local tbl=_bp_named_desc[name]
	if tbl then
		tbl[desc] = true
	else 
		_bp_named_desc[name]={ [desc] = true }
	end
 
	return _bp_named[name],name
end
 
local function bp_show(n)
	if type(n)=="number" then
		local func=_bp_unnamed_index[n]
		if func==nil then
			error "invalid break point id"
		end
		print("break point:",n,
			_bp_unnamed[func] and "on" or "off",
			_bp_unnamed_desc[func]
			)	
	else
		print("break point:",n,_bp_named[n] and "on" or "off")
		if _bp_named_desc[n] then
			for k,v in pairs(_bp_named_desc[n]) do
				print("",k)
			end
		else
			print("\tundefined")
		end
	end
end
 
local function bp_show_all()
	for k,_ in pairs(_bp_unnamed) do
		bp_show(_bp_unnamed_index[k])
	end
	for k,_ in pairs(_bp_named) do
		bp_show(k)
	end
end
 
function bp(n)
	local trigger,name
	if n==nil then
		trigger,name=bp_add_index()
	else 
		trigger,name=bp_add_name(n)
	end
 
	if trigger then
		bp_show(name)
		dbg(1)			
	else
		_bp_list[name]=false
	end
end
 
function trigger(n,on)
	on = on~=false
	if type(n)=="number" then
		local func=_bp_unnamed_index[n]
		if func==nil then
			error "invalid break point id"
		end
		_bp_unnamed[func]=on
	else 
		_bp_named[n]=on
	end
end
 
function list(v)
	if v then 
		bp_show(v)
	else
		bp_show_all()
	end
end
 
function error_handle(msg)
	print(msg)
	dbg(1)
end


可以这样测试一下:

require "bp"
 
function foo(arg)
    bp.bp()    -- 设置一个匿名断点
    return arg+1
end
 
=foo(0)    -- 输出 foo(0) 
 


运行后会进入调试控制台:
可以发现断点被编了号(1 号)并被触发,局部变量放在 _L 这张表里可以直接操作,upvalue 放在了 _U 表中 。

break point:    1       on      =stdin(2)
_L=     {arg=0,}
_U=     {}


我们可以改一下 arg 的值,如 _L.arg=1 然后 cont 退出控制台。可以发现 foo 函数返回了 2 。
这种匿名断点添加方便,而且断点跟随逻辑而不是和代码行做映射。也就是说,同一个 function 的不同实例(不同的 closure )可以有不同 id 的断点,单独控制。

我们也可以使用具名断点,方法是 bp.bp "bpname"
具名断点缺省是 disable 状态的。如果需要激活可以用 bp.trigger "bpname" 。 
给断点起名字的好处是,可以给一组断点起同一个名字,这样可以批量开关。

bp.trigger(name,true/false) 用于开关断点,name 可以是字符串也可以是 id 。

列出所有断点可以用 bp.list()
也可以用 bp.list(name) 来列出一个断点的状态,name 可以是字符串也可以是 id 。

还有一种方式,就是把调试控制台用在错误处理函数中,这样函数一出错就自动切入控制台。
方法是 xpcall(some_function,bp.error_handle)

下一步打算做单步执行 :D
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值