跨平台PHP调试器设计及使用方法——协议解析

        在《跨平台PHP调试器设计及使用方法——探索和设计》一文中,我介绍了将使用pydbgp作为和Xdebug的通信库,并让pydbgp以(孙)子进程的方式存在。《跨平台PHP调试器设计及使用方法——通信》解决了和pydbgp通信的问题,本文将讲解和pydbgp通信协议的问题。(转载请指明出于breaksoftware的csdn博客)

        和Xdebug的通信协议不同,和pydbgp的通信协议其实就是对其调用规则和对返回结果解析的规则。这块技术并没有什么高深之处,只是pydbgp的资料很少,其规则也没有相关说明,只能靠查看源码和实践来收集和分析。我尽量以调用顺序来讲解相关协议。

        首先,我们需要设置IDE Key参数。这步操作我放在start_debugger函数中

    def start_debugger(self):
        if self._pydbgpd:
            return {"ret":1}
        self._pydbgpd = pydbgpd_stub()
        self._pydbgpd.start()
        self._pydbgpd.query('key netbeans-xdebug')

    def stop_debugger(self):
        if self._pydbgpd:
            self._pydbgpd.stop()
            del self._pydbgpd
            self._pydbgpd = None
            
    def is_session(self):
        if not self._pydbgpd:
            return False
        return self._pydbgpd.is_session()

        pydbpgd_stub是《跨平台PHP调试器设计及使用方法——通信》一文介绍的父程序中的“桩”,对它的调用就如同对pydbgpd(子进程中)的调用一样,感受不到跨进程带来的各种不便。stop_debugger用于关闭调试,is_session用于判断调试器是否处在“session阶段”。这些都是通过对pydbgpd_stub对象操作实现的。之后我们所有要和调试器通信的地方都会看到它。
        接下来,我们需要告知调试器,我们需要在哪个端口开启监听。这样Xdebug可以通过在配置文件中的配置信息连接到我们开启的端口。

    def start_listen(self, param):
        if False == self._listening:
            data = self._pydbgpd.query('listen -p localhost:9000 start')
            #ERROR: dbgp.server: the debugger could not bind on port 9000.
            if "ERROR" in data:
                return {"ret":0}
            self._listening = True
        return {"ret":1}

        start_listen中,我们通过上述第三行的命令启动端口监听。如果调用成功,则没有任何数据返回。如果调用失败,则会返回错误,比如待绑定的端口被占用时,会返回上述第四行的信息。我们通过返回信息中是否包含ERROR来判断该操作是否成功。

        如果此时有PHP执行触发了调试,则我们需要查看有哪些调试连接已经接入

    def sessions(self,param):
        data = self._pydbgpd.query('sessions')
        sessions = []
        arr = data.split('\n')
        #data = "#2344:<dbgp.server.application instance at 0x025839E0>"
        for item in arr:
           try:
               if not len(item):
                   continue
               res = self._sessions_info_pattern.search(item).groups()
               sessions.append(res[0])
           except Exception,errinfo:
               print errinfo, "sessions error:" + data + "\n"
                
        return sessions

        sessions函数中,我们通过向pydbgp发送“sessions”指令来查看调试接入(会话)信息。上述第五行是一个接入信息的返回数据,如果此时有多条调试接入,则会产生多行信息。我们通过对换行符切分,并对每条数据通过正则提取,获取所有会话号。上述例子中的会话号就是2344。
        我们获知会话号后,需要挑选一个需要调试的会话号进行调试,这个时候就需要调用下面的方法

    def select(self,param):
        select_cmd = "select " + param
        ret = self._pydbgpd.query(select_cmd)
        if self.is_session():
            return {"ret":1}
        return {"ret":0}

        select方法传入的是会话号,pydbgp在执行上述第二行的指令后,不会返回任何数据。此时我们可以通过is_session判断调试器是否进入session阶段,如果进入了,则证明执行成功,否则失败。
        进入调试后,我们可能需要设置断点(其实没有调试状态也存在设置断点的可能性,而且可能性非常大,所以这种预设性的断点设计也包含在我的设计中,这块在之后的博文中会有介绍。)。Xdebug提供的断点有多种方式,目前我测试的版本尚不支持watch类型,所以这种类型我们也不讨论。我们看下支持的类型:

  1. 行号断点。这种断点方式非常常见,就是我们需要设定文件路径和断点行号。如果设置成功,则程序执行到该文件该行时将会被中断。
  2. 调用断点。这种断点需要设置被调用的函数函数名,它将使得程序中断在该函数被调用前。
  3. 返回断点。这种断点也需要设置被调用的函数函数名,它将使得程序中断在该函数被调用后。
  4. 异常断点。这种断点需要设置异常的类型名,它将使得程序中断在该种异常被抛出前。
  5. 条件断点。这种断点需要设置中断时发生的条件。比如我们调试一个循环,我们可以设置索引值等于某个值时被中断。

        我们看下这些断点的设置方法

    def add_breakpoint(self,breakpointinfo):
        breakpoint_set_type_keys = {
            "line" : {"filename":"-f","lineno":"-n"},
            "call" : {"function":"-m"},
            "return" : {"function":"-m"},
            "exception" : {"exception":"-x"},
            "conditional" : {"filename":"-f","lineno":"-n","expression":"-c"},
            "watch" : {},
        }
        
        query = "breakpoint_set -t " + breakpointinfo["type"]
        for (key,value) in breakpoint_set_type_keys[breakpointinfo["type"]].items():
            if value == "-c":
                expression_de = base64.b64decode(breakpointinfo[key])
                query = query + " " + value + " '" + expression_de + " '"       #maybe bug if expression_de has '
            else:
                query = query + " " + value + " " + breakpointinfo[key]

        data = self._pydbgpd.query(query)
        iteminfo = self._parse_breakpoint_info(data)
        if not iteminfo:
            ret = 0
        else:
            ret = 1
        return {"ret":ret, "breakpoint":iteminfo}

        以设置行号断点为例,我们最终的调用方式是breakpoint_set -t line -f file:///home/work/xxxx.php -n 10。这儿有点特别的是条件断点的设置,因为条件的内容我们无法控制,所以需要使用base64对其进行编码。pydbgp执行新增断点的请求后会返回该断点的信息(实际信息不全,这也将导致我们之后断点相关的逻辑设计的比较曲折)。

        设置完断点后,我们需要查看我们设置了哪些断点。

    def breakpoint_list(self, param):
        data = self._pydbgpd.query("breakpoint_list")
        #data ="""<dbgp.server.breakpoint: id:11900002 type:line filename:file:///var/www/html/index.php lineno:8 function: state:enabled exception: expression: temporary:0 hit_count:0 hit_value:None hit_condition:None>
#<dbgp.server.breakpoint: id:11900003 type:line filename:file:///var/www/html/index.php lineno:9 function: state:enabled exception: expression: temporary:0 hit_count:0 hit_value:None hit_condition:None>"""
        info = []
        arr = data.split('\n') 
        
        for item in arr:
            if not len(item):
                continue
            iteminfo = self._parse_breakpoint_info(item)
            if iteminfo:
                info.append(iteminfo)
        return info

        第三行给出了断点的样例,我们继而调用_parse_breakpoint_info和parse_breakpoint_info方法去提取断点信息

    def _parse_breakpoint_info(self, info):
        iteminfo = {}
        try:
            iteminfo = self.parse_breakpoint_info(info)
        except Exception,errinfo:
            print errinfo, "_parse_breakpoint_info error:" + info + "\n"
        return iteminfo

    #data = "<dbgp.server.breakpoint: id:65920004 type:conditional filename:file:///D:/nginx-1.11.3/html/index.php lineno:30 function: state:enabled exception: expression:$i ==6 temporary:0 hit_count:0 hit_value:None hit_condition:None>"
    def parse_breakpoint_info(self, data):
        breakpoint_info = {}
        keys = ["id","type","filename","lineno","function","state","exception","expression","temporary","hit_count","hit_value","hit_condition"]
        data_end = data.rfind(">")
        for key_index in range(0, len(keys)):
            search_key = " " + keys[key_index] + ":"
            index_start = data.find(search_key) + len(search_key)
            if -1 == index_start:
                raise debugger_exception("parse_breakpoint_info error: no keys" + keys[key_index] )
            if key_index < len(keys) - 1:
                next_key_index = key_index + 1
                search_key = " " + keys[next_key_index] + ":"
                index_end = data.find(search_key)
                if -1 == index_end:
                    raise debugger_exception("parse_breakpoint_info error: no keys" + keys[index_end] )
            else:
                index_end = data_end
            breakpoint_info[keys[key_index]] = data[index_start:index_end]
        return breakpoint_info

        上述第12行列出了断点信息的类型,它们分别是:标识号、类型、文件路径、行号(为行号断点时有效)、函数名(调用和返回断点时有效)、状态(有效还是失效)、异常类型名(异常断点时有效)、表达式、是否为临时断点(只断一次)、命中次数、命中值(猜测,实际没发现有什么数据)和命中条件。由于实际返回的数据信息不全,我们不能全以其信息为准,这块我们将在之后介绍。

        有新增断点就有删除断点,删除断点比较简单,我们只要传入断点ID即可

    def remove_breakpoint(self,breakpointid):
        query = "breakpoint_remove -d " + breakpointid
        data = self._pydbgpd.query(query)
        if "breakpoint removed" in data:
            ret = 1
        else:
            ret = 0
        return {"ret":ret}

        如果删除成功,则会返回breakpoint removed。我们通过返回值判断操作是否成功。

        设置完断点后,我们需要通过“步过”、“步入”,“步出”,“执行”等操作控制程序执行,它们的执行逻辑很简单,且没有返回值

    def step_over(self, param):
        return self._step_cmd("step over")
    
    def step_in(self, param):
        return self._step_cmd("step in")
    
    def step_out(self, param):
        return self._step_cmd("step out")
    
    def run(self, param):
        return self._step_cmd("run")

    def _step_cmd(self,cmd):
        if False == self._pydbgpd.is_session():
            return {}
        data = self._pydbgpd.query(cmd)
        if len(data):
            return {"ret":0}
        else:
            return {"ret":1}

        如果我们执行run之后,程序被中断了,我们可以通过查看状态的命令查看断点调试器的状态

    #0 out of session 1 starting 2 break 3 stopping 4 stopped 5 waiting
    def status(self,param):
        if not self._pydbgpd.is_session():
            return {"ret":1, "status":0}
        
        data = self._pydbgpd.query('status')
        out_of_sesion_status = "invalid cmd"
        starting_status = "Current Status: status [starting] reason[ok]"
        break_status = "Current Status: status [break] reason[ok]"
        stopping_status = "Current Status: status [stopping] reason[ok]"
        stopped_status =  "command sent after session stopped"
        waiting_status = "session timed out while waiting for response"
        
        status = -1
        
        status_map = {
            out_of_sesion_status:0,
            starting_status:1,
            break_status:2,
            stopping_status:3,
            stopped_status:4,
            waiting_status:5 };
            
        for (key,value) in status_map.items():
            if key in data:
                status = value
                break
                
        if not len(data):
            status = 0
            
        return {"ret":1,"status":status}

        starting状态是启动调试后的第一个状态,此时还没进入PHP代码。break状态就是被我们断点中断的状态,或者我们执行“步过”、“步入”和“步出”后的调试器状态。stopping状态是已经不在PHP代码中,但是即将结束的状态。对于一个没有断点的程序,执行了“run”之后就进入stopping状态,而中间不会经过break状态。stopped状态表示该会话已经彻底结束,我们可以退出该会话了。waiting状态在调用非常耗时的操作时会出现。

        如果调试器处于break状态,则我们可以通过查看调用堆栈的方式查看程序执行路径。

    def stack_get(self,param):
        return {"ret":1, "data":self._get_stack_info()}
    
    def _get_stack_info(self, frame = ""):
        if False == self._pydbgpd.is_session():
            return []
        query = 'stack_get ' + frame
        data = self._pydbgpd.query(query)
        #data = "frame: 0 file:///var/www/html/index.php(8) file {main}"
        
        frame_list = []
        arr = data.split('\n')
        
        for item in arr:
            if not len(item):
                continue
            try:
                res = self._stack_get_pattern.search(item).groups()
                info = {}
                info['frame'] = res[0]
                info['filename'] = res[1]
                #info['path'] = info['path'].replace('/', os.sep)
                info['filename_last'] = info['filename'].split('/')[-1]
                info['lineno'] = res[2]
                info['function'] = res[3]
                m1 = md5.new()   
                m1.update(info['filename']) 
                info['file_id'] = m1.hexdigest()
                frame_list.append(info)
            except Exception,errinfo:
                print errinfo, "stack_get error:" + data + "\n"
                
        return 

        如果我们执行stack_get,则会返回全部的调用堆栈信息。如果给stack_get传入堆栈号,则返回该调用栈的信息。一般堆栈信息包含堆栈号、所处的文件路径、所处的行号和函数名。我们在之后的UI层通过这个函数可以动态的更新代码的执行情况。

        我们调试的一个重要的目的就是可以随时查看变量值,所以查看变量也是调试器的重点。通过Xdebug获取所有栈上的变量要分为三步:

  1. 获取调用堆栈深度
  2. 获取context_names
  3. 获取指定堆栈深度的指定context_names下的所有变量

        这一系列操作通过如下操作完成

    def _get_all_variables(self, cur = False):
        all_data = self._get_stack_variables(cur)
        return {"ret":1, "data":all_data}
    
    def _get_stack_variables(self, cur = False):
        info = {}
        data = self._pydbgpd.query('stack_depth')
        #'Stack Depth: 3'
        pattern = re.compile("Stack Depth: (\d+)")
        try:
            res = pattern.search(data).groups()
            for index in range(0, int(res[0])):
                iteminfo = self._get_context_variables(index)
                key = "Frame " + str(index)
                info[key] = iteminfo
                if cur:
                    break
        except Exception,errinfo:
            print errinfo, "_get_stack_variables error:" + data + "\n"
            
        return info
    
    def _get_context_variables(self, depth_id):
        data = self._pydbgpd.query('context_names')
        #data='''0: Locals
        #1: Superglobals
        #2: User defined constants'''
        
        info = {}
        arr = data.split('\n')
                
        for item in arr:
            if not len(item):
                continue
            try:
                res = self._context_names_pattern.search(item).groups()
                iteminfo = self._get_context(depth_id, res[0])
                info[res[1]] = iteminfo
            except Exception,errinfo:
                print errinfo, "context_names error:" + item + "\n"
        
        return info
            
    def _get_context(self, depth_id, context_id):
        query = 'context_get -d ' + str(depth_id) + ' -c ' + str(context_id)
            
        data = self._pydbgpd.query(query)
        #data = '''name: $a type: string value: 123
        #name: $b type: int value: 234'''

        info = []
        arr = data.split('\n')
        
        for item in arr:
            if not len(item):
                continue
            try:
                res = self._context_get_pattern.search(item).groups()
                iteminfo = {}
                iteminfo["name"] = res[0]
                iteminfo["type"] = res[1]
                iteminfo["value"] = res[2]
                info.append(iteminfo)
            except Exception,errinfo:
                print errinfo, "context_get error:" + item + "\n"
                
        return info

        context_names可能用户不大理解,其实它就是变量类型。比如全局变量里我们可以看到Http请求的相关信息。这步操作相对于其他操作需要多次查询和解析,所以它的效率是非常低的。所以我在设计时没有让其自动更新(除非用户选择的展现页为变量页,这样每步操作都要更新变量),也没让变量对比功能自动开启。

        如果调试会话结束,我们可以通过下面的方法退出调试

    def quit(self,param):
        return self._step_cmd("quit")
    
    def stop(self,param):
        return self._step_cmd("stop")
    
    def exit(self,param):
        return self._step_cmd("exit")    

        这样主流的一些操作我们讲解完了,我们再讲解些不太能用到的。比如查看当前执行到的代码上下文,可以使用source命令

    def source(self,param):
        src = self._pydbgpd.query("source")
        if "(u'stack depth invalid', 301)" in src:
            return {"ret": 0}
        return {"ret": 1, "data": src}

        比如我们在break的情况下,需要修改某个变量值,则可以使用eval指令进行代码执行,其实这块功能非常重要

    def eval(self, param):
        query = "eval " + param
        data = self._pydbgpd.query(query)
        return {"ret":1}

        我还开放了命令行式的调试方式,这样用户就可以自己输入调试命令进行调试,这个和dbg很像,于是我要做的就是命令的传导

    def query(self, cmd):
        return self._pydbgpd.query(cmd)

        有了上述的方法,我们可以构建一个简单的调试器。但是由于pydbgp断点信息返回不全,而且我们需要一些高阶功能,比如调试器状态机、预设断点等,使得更高一层的封装整合成为必需。下一博文我们将重点介绍高阶封装相关的内容。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 协议生成器和协议解析器的设计需要根据具体的协议类型和协议格式来确定。一般来说,协议生成器需要根据协议规范和数据结构,将数据转换成符合协议格式的二进制数据。而协议解析器则需要根据协议规范和数据结构,将接收到的二进制数据解析成对应的数据类型。在设计过程中,需要考虑协议的可扩展性、可靠性、安全性等因素。具体的实现方式可以使用编程语言提供的相关库或者自行实现。 ### 回答2: 协议生成器和协议解析器是计算机网络中常用的工具,用于生成和解析通信协议协议生成器能够通过预定义协议规范,自动生成符合规范的协议报文;而协议解析器则能够将接收到的协议报文解析为可读的数据。 设计协议生成器和协议解析器时,通常可以按照以下步骤进行: 步骤一:确定协议规范。首先需要明确所要生成或解析协议的规范,包括协议报文的结构、字段类型、长度等。可以通过查阅相关文档或研究已有的协议实现来确定规范。 步骤二:设计协议模板。根据协议规范,设计协议生成器和解析器的数据模板。协议生成器的模板将包含规范中定义的字段和对应的取值,而协议解析器的模板应能够逐层解析协议报文。 步骤三:实现生成器和解析器。根据设计的模板,编写相应的代码实现协议生成器和解析器。生成器需要根据模板生成符合规范的协议报文,而解析器则需要按照模板对接收到的数据进行解析。 步骤四:测试和调试。对设计的生成器和解析器进行测试,并进行调试以确保其功能和性能满足需求。可以使用模拟的测试数据或实际的网络数据进行测试,对生成和解析的结果进行验证。 步骤五:优化和扩展。根据实际需求和性能要求,对生成器和解析器进行优化,例如利用缓存、并发处理等技术提高性能。在需要扩展支持新的协议规范时,可以对模板进行扩展或增加新的模板。 设计协议生成器和解析器需要对计算机网络和协议有一定的了解,同时也需要熟悉编程语言和相关的网络编程技术。在设计过程中,还需要注重功能的完整性、效率和可扩展性,以满足不同场景下的应用需求。 ### 回答3: 协议生成器和协议解析器是用于在计算机通信中实现不同系统间的数据传输协议的工具。 协议生成器的设计主要有以下几个步骤: 1. 确定协议的需求:根据通信系统的要求,确定所需支持的数据传输格式和协议规范。 2. 设计协议生成逻辑:根据协议规范,确定如何生成符合该协议的数据包。这包括首部字段的定义、数据格式的封装和加密等。 3. 实现生成逻辑:根据设计的逻辑,编写代码实现协议生成器。这可以使用编程语言和相关的库/框架来完成。 4. 进行测试:对生成的数据包进行测试,确保生成器能够按照预期生成协议要求的数据包。测试主要包括验证数据格式、数据完整性、加密解密等。 协议解析器的设计步骤如下: 1. 确定协议的需求:与生成器类似,根据通信系统的要求,确定所需支持的数据传输格式和协议规范。 2. 设计协议解析逻辑:根据协议规范,确定如何解析接收到的数据包。这包括对首部字段的解析、数据格式的解封装和解密等。 3. 实现解析逻辑:根据设计的逻辑,编写代码实现协议解析器。同样可以选择合适的编程语言和相关的库/框架来完成。 4. 进行测试:对解析器进行测试,确保解析器能够正确解析收到的数据包,提取所需的信息并进行必要的处理。测试主要包括验证数据格式、数据完整性、解密及错误处理等。 协议生成器和解析器是互补的工具,生成器负责将数据按照协议规范打包,解析器则负责将接收到的数据包按照协议规范解析。在通信系统中,这两者经常作为一个整体进行设计和开发,以确保通信的可靠性和安全性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

breaksoftware

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值