在《跨平台PHP调试器设计及使用方法——协议解析》一文中介绍了如何将pydbgp返回的数据转换成我们需要的数据。我们使用该问中的接口已经可以构建一个简单的调试器。但是由于pydbgp存在的一些问题,以及调试器需要的一些高级功能,我们还需要对这些接口进行组合和封装。(转载请指明出于breaksoftware的csdn博客)
首先我们需要做的便是一个简单的状态机。在前一文中,我们介绍了调试器会处于session(会话)和no session(无会话)阶段,在session阶段又存在如下状态
- 开始调试状态。该状态下,调试器还没有进入PHP代码层面。
- 中断状态。
- 停止中状态。该状态下,调试器已经不在PHP代码层面。
- 停止状态。该状态下,调试器调试该会话已经结束。
- 等待状态。如果PHP执行某操作很耗时,可能会在此时命中该状态。
作为一款调试器,应该隐藏一些底层的操作,而暴露给用户一些他们关心的东西。比如处在开始调试状态下,用户一般不会去关心这个状态,因为它不在PHP代码层面。这个时候我们就需要在状态机中将上述状态通过相关操作转换成用户关心的状态,比如执行Run指令,让调试器命中一些断点,从而处在用户关心的中断状态。再比如调试器处于停止中状态,用户也不会关心这个状态,状态机就会通过相关操作让调试器处于停止状态。而如果调试器处于停止状态,它也是处于不能做有意义事情的状态,状态机就让它退出session阶段,等待其他调试请求的接入。状态机我放在一个线程中执行,并通过信号量与外部通信。
def _debug_routine(self):
while False == self._state_machine_stop_event.isSet():
time.sleep(0.3)
sessions = self._debugger_helper.do("sessions", "")
for session_id in sessions:
self._debug_session(session_id)
if self._state_machine_stop_event.isSet():
break
self._debugger_helper.do("quit","")
self._reset_all_breakpoint()
def _debug_session(self,session_id):
select_ret = self._debugger_helper.do("select", session_id)
if select_ret["ret"] == 0:
print "select error"
return
self._accept_user_action_event.clear()
self._reset_all_breakpoint()
self._add_all_breakpoint()
status_ret = self._debugger_helper.do("status","")
while False == self._state_machine_stop_event.isSet():
if status_ret["ret"] == 0:
break
if status_ret["status"] == 1:
if len(self._breakpoint_list):
self._debugger_helper.do("run","")
else:
self._debugger_helper.do("step_over","")
if status_ret["status"] == 2:
self._accept_user_action_event.set()
time.sleep(5)
if status_ret["status"] == 3:
self._accept_user_action_event.clear()
self._debugger_helper.do("run","")
if status_ret["status"] == 4:
self._accept_user_action_event.clear()
break
status_ret = self._debugger_helper.do("status","")
time.sleep(1)
if status_ret["status"] != 0:
self._debugger_helper.do("exit","")
self._pre_variables = {}
self._cur_variables = {}
self._accept_user_action_event.set()
该状态机中一直通过第4行获取session状态。如果有session可以被调试,则进入_debug_session函数对其进行调试。首先调用select方法,让调试器从no session阶段进入session阶段。如果这个调试会话无法调试,则会退出_debug_session函数,继续等待其他会话的接入。如果进入调试会话,则要根据用户设置情况,对该会话设置若干断点。然后不停通过status指令获取调试器的状态。如果调试器处在开始调试状态,则查看用户设置断点的情况决定是执行run执行还是执行step_over指令。如果用户设置了断点,则我们认为用户希望程序可以直接中断在断点处,于是就直接执行run指令。如果用户没有设置断点,则可能是要从头开始调试,则我们执行step_over指令,让调试进入PHP代码层面。如果调试器处在中断状态,则通知线程外面,可以执行其他指令了。如果处在停止中状态,则直接执行run指令,让该状态直接进入停止状态。如果处于停止状态,则跳出本次调试会话。
解决了状态机问题,我们就要看断点的实现。断点是调试器非常重要的功能,一般我们都会通过断点快速定位问题。由于用户设置断点的时候,调试器可能不处在session阶段,所以没法让调试器设置断点信息。于是我们只能让其先保存在一个数组中
def add_breakpoint(self,param):
update_keys = ["id", "state", "hit_value", "hit_condition", "hit_count"]
param_de = base64.b64decode(param)
param_json = json.loads(param_de)
(breakpoint_key, breakpoint_value) = self._get_breakpoint_info(param_json)
#breakpoint_set_keys = ["type", "filename", "lineno", "function", "state", "exception", "expression", "temporary", "hit_count", "hit_value", "hit_condition"]
if breakpoint_key not in self._breakpoint_list.keys():
self._breakpoint_list[breakpoint_key] = breakpoint_value
self._breakpoint_list[breakpoint_key]["state"] = "disable"
if self._debugger_helper and self._debugger_helper.is_session():
add_ret = self._debugger_helper.do("add_breakpoint", breakpoint_value)
if add_ret["ret"] == 1:
for item in update_keys:
if item in add_ret["breakpoint"].keys():
self._breakpoint_list[breakpoint_key][item] = add_ret["breakpoint"][item]
return {"ret":1}
else:
return {"ret":0}
return {"ret":1}
通过第5行调用_get_breakpoint_info,将调试器界面传入的断点信息转换成断点唯一性Key和断点信息。然后将该断点信息保存在_breakpoint_list中。如果此时处在session阶段,则调用pydbgp设置该断点,并用返回的信息更新我们保存的断点信息。这儿有个地方需要注意下,我们需要更新的断点信息的Key只是update_keys中的,而像exception和expression等都没更新,为什么?因为pydbgp有个问题,就是如果我们设置了一个条件断点或者异常断点,返回的断点信息将不再包含条件表达式或者异常名。我们只有在用户设置断点的这一个时机来保存这些不再返回的信息。
删除断点也要经过类似的过程
def remove_breakpoint(self,param):
param_de = base64.b64decode(param)
param_json = json.loads(param_de)
print self._breakpoint_list
(breakpoint_key, breakpoint_value) = self.get_breakpoint_info_by_param(param_json)
print breakpoint_key, breakpoint_value
if self._debugger_helper and self._debugger_helper.is_session():
if breakpoint_key in self._breakpoint_list.keys():
remove_ret = self._debugger_helper.do("remove_breakpoint", breakpoint_value["id"])
if remove_ret["ret"] == 1:
del self._breakpoint_list[breakpoint_key]
return {"ret":1}
else:
return {"ret":0}
else:
if breakpoint_key in self._breakpoint_list.keys():
del self._breakpoint_list[breakpoint_key]
return {"ret":1}
先将断点的Key和信息分析出来。如果当前处在session阶段,则通知pydbgp删除该断点。pydbgp删除成功后再在_breakpoint_list中删除;否则不删除。如果不处在session阶段,则直接从_breakpoint_list中删除。
断点还有很多其他的细节问题,比如断点key的生成规则,通过行号查找断点信息等本我都不在讲述,详细可以参见代码。
下一个比较实用的就是变量信息查看。在之前我们讲过,变量分为全局变量和栈上变量。如果我们每次都获取全部栈上的变量,其效率是非常低的。所以我让默认的变量信息只显示当前栈的。
def get_variables(self,param):
if self._all_stack_parameters:
return self._debugger_helper.do("get_variables", param)
else:
return self._debugger_helper.do("get_cur_stack_variables", param)
为了记录一些变量在执行某步操作前后的变化,我需要在这些步骤前后记录全部变量的值
def step_over(self,param):
if self._variable_watch:
self._pre_variables = self.get_variables("")
ret = self._debugger_helper.do("step_over", param)
self._cur_variables = self.get_variables("")
else:
ret = self._debugger_helper.do("step_over", param)
return ret
def step_in(self,param):
if self._variable_watch:
self._pre_variables = self.get_variables("")
ret = self._debugger_helper.do("step_in", param)
self._cur_variables = self.get_variables("")
else:
ret = self._debugger_helper.do("step_in", param)
return ret
def step_out(self,param):
if self._variable_watch:
self._pre_variables = self.get_variables("")
ret = self._debugger_helper.do("step_out", param)
self._cur_variables = self.get_variables("")
else:
ret = self._debugger_helper.do("step_out", param)
return ret
def run(self,param):
if self._variable_watch:
self._pre_variables = self.get_variables("")
ret = self._debugger_helper.do("run", param)
self._cur_variables = self.get_variables("")
else:
ret = self._debugger_helper.do("run", param)
return ret
我使用_variable_watch变量控制是否在每步操作时记录这些信息。因为这些信息量非常大,非常影响调试效率,所以我使用一个配置用来开关这个功能。默认这个功能是关闭的。
还有一个功能用的也稍微多点,就是修改变量值。我们在调试时,可能遇到逻辑运算不在我们预计之内,导致执行的流程出现无法抵达的现象。这个时候我们就可以动态的修改变量的值来影响之后的执行结果。这块功能比较简单
def modify_variable(self,param):
param_de = base64.b64decode(param)
param_json = json.loads(param_de)
if "value" not in param_json.keys() or "name" not in param_json.keys():
return {"ret":0}
exucte_cmd = param_json["name"] + "=" + base64.b64decode(param_json["value"])
data = self._debugger_helper.do("eval", exucte_cmd)
return data
最后一个是我在实践中发现的很必要的一个功能:请求记录。我们调试一个问题时,请求可能来源于我们在网页中输入数据并提交,如果一次调试出问题,想再次调试则可能要重新填写页面。这样我做了一个请求记录功能,用户可以在下次请求时发送和上次一样的请求
def save_request(self,param):
param_de = base64.b64decode(param)
db = request_db()
if db.is_request_name_exist(param_de):
return {"ret":0, "msg":"name exist"}
variables = self.get_variables("")
get_data_org = self._search_variable(variables, "$_GET")
post_data_org = self._search_variable(variables, "$_POST")
get_data_new = {}
if "value" in get_data_org.keys():
get_data_new = self.generate_get_request_map(get_data_org["value"], "$_GET['", "']")
post_data_new = {}
if "value" in post_data_org.keys():
post_data_new = self.generate_get_request_map(post_data_org["value"], "$_POST['", "']")
all_data = {"get":get_data_new, "post":post_data_new, "url":""}
db.add_request(param_de, all_data)
return {"ret":1}
经过上述封装,我们便可以在界面层通过简单的调用实现丰富的功能。下一博文我将讲解界面相关内容。