本文转载自微信公众号《神光的编程秘籍》,作者神说,必有光zxg。转载本文请联系神光编程秘籍公众号。代码写好后,运行一下看看效果。在开发的时候,我们大多使用调试器来单步运行或者断点运行。我们整天都在使用debugger,但是你有没有想过它的实现原理呢?本文将回答以下问题:代码运行的底层原理是什么?为什么需要调试器?debugger的实现原理是什么?不知道大家有没有注意到,./xxx可以直接执行可执行文件,而node执行js文件需要./xxx,python执行python文件需要./xxx。这就是编译执行(直接执行)和解释执行的区别。cpu的直接执行提供了一套指令集,基于这些指令集可以控制整个计算机的运行。机器语言代码由这些指令和相应的操作数组成。这些机器码可以直接在计算机上运行,??或者说是直接可执行的。由它们组成的文件称为可执行文件。不同操作系统中的可执行文件具有不同的格式。在windows上是pe(PortableExecutable)格式,在linux和unix系统上是elf(ExecutableLinkableFormat),在mac上是mash-o格式。它们指定不同内容(.text是代码,.data,.bass等是数据)在文件中的位置。但真正可执行的部分还是由cpu提供的机器指令组成。编译语言将经历编译、汇编和链接阶段。编译就是将源代码转换成由汇编语言组成的中间代码。汇编就是将中间代码转换成目标代码。链接会将目标代码组合成一个可执行文件。这个可执行文件可以直接在操作系统上执行。正因为它是由cpu的机器指令组成的,所以可以直接控制cpu。所以可以直接执行./xxx。解释型、执行型和编译型语言生成可执行文件,直接在操作系统上执行,无需安装解释器,而js、python等解释型语言的代码需要安装解释器才能运行。为什么不用解释器生成机器码,cpu还是不知道这些代码?那是因为解释器需要编译成机器码,cpu知道如何执行解释器,解释器知道如何执行更高层的脚本代码,就这样,被机器码解释执行解释器,然后解释器解释并执行上层代码。这就是脚本语言的原理。这包括js、python等。但是解释器毕竟多了一层,所以有时候会编译成机器码直接执行。这是JIT编译器。比如一个js引擎一般由解析器、解释器、JIT编译器、GC组成。大部分代码由解释器解释执行,热代码会被JIT编译器编译成机器码,直接在操作系统上执行。提高性能。编译成机器码直接执行,或者从源代码解释执行,代码都是通过这两种方式执行的。两者各有优势,编译型快,解释型跨平台。这就是代码的工作原理。王音说,计算机的本质是解释器。也就是说cpu用电路解释机器码,解释器用机器码解释更高层的脚本代码,所以计算机的本质就是解释器。为什么我们需要调试器?我们知道图灵完备的语言可以解释任何可计算的问题,所以无论是编译还是解释,它都可以描述所有可计算的业务逻辑。我们用不同的语言来描述业务逻辑,然后运行看看效果。当代码的逻辑比较复杂的时候,难免会出现错误。我们希望一步步运行或者停在某个点,然后看看当时的环境变量里,执行一个脚本。完成这个功能的是调试器。可能还有很多初级程序员只会用console.log来打日志,但是日志并不能完整的展现当时的环境。最好的方法是使用调试器。狼叔说是否使用debugger是nodejs层面的明显区分。调试器的原理我们知道调试器是调试程序必不可少的,那么它是如何实现的呢?可执行文件的debugger其实在cpu和操作系统设计的时候就支持了debugger的能力(看debugger的重要性),cpu中有4个寄存器可以用于硬中断,操作系统提供系统调用用于软中断。这是编译语言调试器实现的基础。中断cpu只会继续执行下一条指令,但是程序运行过程中难免要处理一些外部消息,比如io,network,exception等,所以设计了中断机制,cpu会去看看中断标志,看是否需要中断。就像事件循环在每次循环结束时检查是否需要渲染一样。INT指令cpu支持INT指令触发中断。中断有一个编号,不同的编号有不同的处理程序。记录编号和中断处理程序的表称为中断向量表。其中INT3(3号中断)可以触发调试器,这是约定俗成的。那么可执行文件如何使用3号中断来debugger呢?实际上,它在运行时替换了执行的内容。调试器程序会在需要设置断点的位置用INT3即0xCC替换指令内容,从而打断Living。此时可以获取环境数据进行调试。用0xcc(INT3)替换机器码会中断程序,但如何恢复执行呢?其实比较简单。记录下当时被替换的机器码,等需要解除断点的时候再改回来。这就是可执行文件调试器的原理,最终通过cpu支持的中断机制来实现。上面提到的调试器在中断寄存器中的实现是修改内存中的机器码,但有时代码是不能修改的,比如ROM。在这种情况下,使用了CPU提供的4个中断寄存器(DR0-DR3)。.这称为硬中断。总之,INT3的软中断和中断寄存器的硬中断是可执行文件实现调试器的两种方式。解释型语言的调试器编译语言是直接在操作系统上执行的,所以利用cpu和操作系统的中断机制和系统调用来实现调试器。不过解释型语言自己实现了代码的解释和执行,所以不需要那一套,但是实现思路还是一样的,就是插入一段代码停止,支持查看环境数据和代码的执行,当断点解除后继续执行。比如javascript支持debugger语句,当解释器执行到这条语句时,就会停止。解释型语言的调试器比较简单,不需要了解cpu的INT3中断。调试器客户端上面我们学习了如何实现直接执行和解释的代码的调试器。我们知道密码是怎么破解的,那么破解之后会发生什么呢?如何暴露环境数据并执行外部代码?这需要调试器客户端。例如,v8引擎将公开通过套接字设置断点、获取环境信息和执行脚本的能力。套接字传输信息的格式是v8调试协议。例如:设置断点:{"seq":117,"type":"request","command":"setbreakpoint","arguments":{"type":"function","target":"f"}移除断点:{"seq":117,"type":"request","command":"clearbreakpoint","arguments":{"type":"function","breakpoint":1}}继续:{"seq":117,"type":"request","command":"continue"}执行代码:{"seq":117,"type":"request","command":"evaluate","arguments":{"expression":"1+2"}}感兴趣的同学可以去v8调试协议文档查看所有协议。基于这些协议,可以控制v8的调试器。所有可以实现调试器的调试器都与该协议对接,如chromedevtools、vscodedebugger等各种ide调试器。调试nodejs代码可以通过添加--inspect选项(或--inspect-brk,将在第一行停止)来调试Nodejs。它会启动一个调试器websocket服务器,我们可以使用vscode来调试nodejs代码,或者使用chromedevtools来调试(参见nodejs调试器文档)。?node--inspecttest.jsDebuggerlisteningonws://127.0.0.1:9229/db309268-623a-4abe-b19a-c4407ed8998d供helpseehttps://nodejs.org/en/docs/inspector,原理是实现v8调试协议。如果我们自己制作调试工具和IDE,我们需要连接到这个协议。上面在调试器适配器协议中介绍的v8调试协议可以实现js代码的调试,所以python、c#等也必须有自己的调试协议。如果要实现ide,重新连接起来太麻烦了。所以后来出现了一个中间层协议,DAP(debuggeradapterprotocol)。调试器适配器协议,顾名思义就是适配,一端适配各种调试器协议,另一端为客户端提供统一的协议。这是适配器模式的一个很好的应用。总结这篇文章,我们已经了解了调试器的实现原理和暴露的调试协议。首先,我们了解了两种运行代码的方式:直接执行和解释执行,然后分析了为什么需要调试器。之后我们探讨了直接执行的代码是如何通过INT3的中断实现调试器和解释型语言自己实现的调试器的。然后调试器的能力会通过socket暴露给客户端,提供调试协议,比如v8调试协议,各种客户端实现包括chromedevtools,ide等,但是这样太麻烦了每种语言实现一次,所以后来出现了适配层协议,屏蔽了不同协议之间的差异,提供了统一的协议接口供客户端使用。希望这篇文章能让你了解调试器的原理,如果你想实现一个调试工具,你也知道如何连接协议。知道为什么chromedevtools和vscode可以调试nodejs代码。
