debug运行程序的原理

article/2025/9/1 16:13:23

0 代码运行的原理是什么?

代码的运行方式可以分为直接执行和解释执行两类。
不知道平时你有没有注意,可执行文件直接 ./xxx 就可以执行,而执行 js 文件需要node ./xxx,执行python文件需要python ./xxx,这就是编译执行(直接执行)和解释执行的区别。

0.1 直接执行

cpu提供了一套指令集,基于这套指令集就可以控制整个计算机的运转,机器语言的代码就是由这些指令和对应的操作数构成的,这些机器码可以直接跑在计算机上,也就是可直接执行。由它们构成的文件叫做可执行文件。
不同操作系统可执行文件的格式不同,在windows上是pe(Portable Executable)格式,在linux、unix系统上是elf(Executable Linkable Format) 格式,在mac上是 mash-o 格式。它们规定了不同的内容(.text 是代码、.data .bass 等是数据)放在文件中的什么位置。但其中真正可执行的部分还是由cpu提供的机器指令构成的。
在这里插入图片描述

编译型语言会经过编译、汇编、链接的阶段,编译是把源代码转成汇编语言构成的中间代码,汇编是把中间代码变成目标代码,链接会把目标代码组合成可执行文件。这个可执行文件是可以在操作系统上直接执行的。就因为它是由cpu的机器指令构成的,可以直接控制cpu。所以可以直接 ./xxx 就可以执行。
在这里插入图片描述

0.1 解释执行

编译型语言都是生成可执行文件直接在操作系统上来执行的,不需要安装解释器,而js、python等解释型语言的代码需要用解释器来跑。

为什么有了解释器就不需要生成机器码了,cpu仍然不认识这些代码啊?

那是因为解释器是需要编译成机器码的,cpu 知道怎么执行解释器,而解释器知道怎么执行更上层的脚本代码,就这样,由机器码解释执行解释器,再由解释器解释执行上层代码,这就是脚本语言的原理。包括js、python等都是这样。

但是解释器毕竟多了一层,所以有的时候会把它编译成机器码来直接执行,这就是JIT(Just-In-Time Compiler)编译器。比如js引擎一般就是由parser、解释器、JIT编译器、GC构成,大部分代码是由解释器解释执行的,而热点代码会经过 JIT 编译器编译成由机器码,直接在操作系统上执行以提高性能。

编译成机器码直接执行,或者是从源码解释执行,代码就这两种执行方式。两者各有各的好处,编译型速度快,解释型跨平台。这就是代码运行的原理。

王垠说过,计算机的本质就是解释器。就是说cpu用电路解释机器码,解释器用机器码解释更上层的脚本代码,所以计算机的本质是解释器。

为什么需要debugger

我们知道,图灵完备的语言可以解释任何可计算问题,所以不管是编译型还是解释型都能够描述所有可计算的业务逻辑。

我们利用不同的语言描述业务逻辑,然后运行它看效果,当代码的逻辑比较复杂的时候,难免会出错,我们希望能够一步步运行或是运行到某个点停下来,然后看一下当时的环境中的变量,执行某个脚本。完成这个功能的就是 debugger。

也许还有很多初级程序员只会用console.log 打日志,但是日志不能完全展现当时的环境,最好的方式还是 debugger。

1 从JavaScript语言看debugger的原理

我们知道了debugger是调试程序必不可少的,那么它是怎么实现的呢?

1.1 可执行文件的 debugger

其实cpu、操作系统在设计的时候就支持了debugger 的能力(可见 debugger的重要性),cpu里面有4个寄存器可以做硬中断,操作系统提供了系统调用来做软中断。这是编译型语言的debugger实现的基础。

中断

cpu只会不断的执行下一条指令,但程序运行过程中难免要处理一些外部的消息,比如 io、网络、异常等等,所以设计了中断的机制,cpu每执行完一条指令,就会去看下中断标记,是否需要中断了。就像event loop每次loop完都要检查下是否需要渲染一样。

INT指令

cpu支持INT指令来触发中断,中断有编号,不同的编号有不同的处理程序,记录编号和中断处理程序的表叫做中断向量表。其中INT3 (3号中断)可以触发debugger,这是一种约定。

那么可执行文件是怎么利用这个3号中断来debugger 的呢?其实就是运行时替换执行的内容,debugger 程序会在需要设置断点的位置把指令内容换成 INT 3,也就是 0xCC,这就断住了。就可以获取这时候的环境数据来做调试。
在这里插入图片描述

通过机器码替换成 0xcc (INT 3)是把程序断住了,可是怎么恢复执行呢?其实也比较简单,把当时替换的机器码记录下来,需要释放断点的时候再换回去就行了。

这就是可执行文件的debugger的原理了,最终还是靠cpu支持的中断机制来实现的。

中断寄存器

上面说的debugger实现方式是修改内存中的机器码的方式,但有的时候修改不了代码,比如ROM,这种情况就要通过cpu提供的4个中断寄存器(DR0 - DR3)来做了。这种叫做硬中断。

总之,INT 3的软中断,还有中断寄存器的硬中断,是可执行文件实现 debugger的两种方式。

1.2 解释型语言的debugger

编译型语言因为直接在操作系统之上执行,所以要利用cpu和操作系统的中断机制和系统调用来实现debugger。但是解释型语言是自己实现代码的解释执行的,所以不需要那一套,但是实现思路还是一样的,就是插入一段代码来断住,支持环境数据的查看和代码的执行,当释放断点的时候就继续往下执行。

比如javascript中支持debugger语句,当解释器执行到这一条语句的时候就会断住。
解释型语言的debugger相对简单一些,不需要了解cpu的INT 3中断。

1.3 debugger客户端

上面我们了解了直接执行和解释执行的代码的 debugger 分别是怎么实现的。我们知道了代码是怎么断住的,那么断住之后呢?怎么把环境数据暴露出去,怎么执行外部代码?
这就需要debugger客户端了。

比如v8引擎会把设置断点、获取环境信息、执行脚本的能力通过socket 暴露出去,socket传递的信息格式就是v8 debug protocol 。比如:
设置断点:

{"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 debug protocol 的文档中去查看全部的协议。
基于这些协议就可以控制v8的debugger了,所有的能够实现debugger 的都是对接了这个协议,比如 chrome devtools、vscode debugger 还有其他各种 ide的debugger。
在这里插入图片描述

2 不同语言的实现

2.1 编译型程序Windows下调试原理简述

调试器和被调试程序是通过中断系统来实现的。不过在Windows下,这些工作都不需要你来做了,Windows封装了一套完整的调试接口。

你只要建立一个调试器,用它打开(或建立)一个被调试进程,然后根据调试的目标中所包含的调试信息找出源代码与机器码之间的对映关系。

在你要中断的地方加一个int3指令(并记录下原来的值),这个指令的代码是0xcc,当目标执行到int3时,它就产生了一个中断,这时目标程序停止运行,并将它的运行状态保存下来,再由操作系统接管。

在Windows下,你的调试器就会接收到一个中断消息,并得到目标程序运行的状态(所有的寄存器),你在调试器中把状态中的IP寄存器的值减1,然后把那个位置的int3指令还原成它原来的值,再返回中断,这时候目标程序就可以继续运行下去。这样就实现了一个 "断点 "。

当然,你也可以在目标进行中断的时候修改它的内存空间或着寄存器,这样就可以实现动态修改变量的值。

如果你把跟踪标志设为1,这时候你的系统就进入跟踪状态,每执行一条机器指令就会产生一个中断,当进行中断时,系统会自己保存当时的运行状态,然后全速运行你的中断代码。

在Windows下,你的调试器同样会收到相应的消息,这时你只要对目标做相应的处理就可以实现对目标的跟踪。

2.2 nodejs代码的调试

nodejs 可以通过添加–inspect的option来做调试(也可以是–inspect-brk,这个会在首行就断住)。它会起一个debugger的websocket服务端,我们可以用vscode来调试 nodejs代码,也可以用chrome devtools来调试(见 nodejs debugger 文档)。

node --inspect test.jsDebugger listening on ws://127.0.0.1:9229/db309268-623a-4abe-b19a-c4407ed8998d For help see https://nodejs.org/en/docs/inspector

原理就是实现了v8 debug protocol。我们如果自己做调试工具、做 ide,那就要对接这个协议。

DAP(debugger adaptor protocol)

上面介绍的 v8 debug protocol可以实现js代码的调试,那么python、c# 等肯定也有自己的调试协议,如果要实现 ide,都要对接一遍太过麻烦。所以后来出现了一个中间层协议,DAP(debugger adaptor protocol)。

debugger adaptor protocol,顾名思义,就是适配的,一端适配各种 debugger协议,一端提供给客户端统一的协议。这是适配器模式的一个很好的应用。如果要实现调试工具也知道怎么该怎么去对接协议。

我们在说起Debug的时候,一般是在IDE里代码中加断点,一步步跟踪。然后观察变量的值,观察输出等等。

2.3 Java的debug与JPDA

许多IDE像Eclipse, IDEA,NetBean中都有提供debug工具,甚至我们可以直接使用JDK自带的jdb工具进行断点调试。这些工具都支持本地调试和远程调试。
那在我们加断点,debug,单步调试等一系列动作背后,是如何实现的呢?
说到这些,本质上作为解释性语言,Java也提供了自己进行调试的接口设计,那就是JPDA(Java Platform Debugger Architecture)。我们每次使用的debug功能,都是靠JPDA的支撑实现的。

什么是JPDA?

官方文档里这样介绍:
The Java Platform Debugger Architecture (JPDA) consists of three interfaces designed for use by debuggers in development environments for desktop systems.
我们看到,JPDA由三部分组成:
JVMTI(Java Virtual Machine Tool Interface)
JDWP(Java Debugger Wire Protocol)
JDI(Java Debug Interface)
熟悉JVM的朋友可能听说过JVMPI和JVMDI,在JDK1.5他们统一被替换为JVMTI。

JVMTI

以前的文章里我们提到过Class的hotSwap,就是通过Instrument实现class的redefine和retransform。
而本质上JVMTI是一个programming interface,主要用在开发和监控上。而且它提供了接口去观察(inspect) 应用状态和控制应用的执行。工具通过它提供的接口,可以进行如下功能的实现:
profiling
debuging
monitoring
thread analysis
coverage analysis
可以看到,我们使用到的debug,只是JVMTI提供的众从能力中的一种。

JDWP

观察过Java debug进程的同学也许有印象,以debug方式启动的JVM进程,看起来是这样的:

-agentlib:jdwp=transport=dt_socket,address=127.0.0.1:63971,server=y,suspend=n

除了进程名之外,还在启动参数里包含agentlib:jdwp这些。这个就是现在要介绍的JDWP。
什么是JDWP?
Java Debug Wire Protocol,是debugger和它要debug的JVM之间进行通讯的协议。更多具体协议的细节这里不介绍,感兴趣的同学可以到这儿查看:
http://docs.oracle.com/javase/6/docs/technotes/guides/jpda/jdwp-spec.html
注意,这仅仅是一个协议的格式,具体的传输实现不是由JDWP来实现的。我们的debugger执行的操作发送到JDWP的实现上,然后再转给JVMTI来具体控制。

JDI

JDI是三个模块中最高层的一个接口,通过JDI,debugger可以更方便的编写符合JDWP格式的数据,用来进行调试数据传输。JDI的引入,提高了开发debugger的效率。
所以,从整体上看,我们可以把JPDA看作一个两个互相通讯的程序,所以我们可以在任意地点很方便的调试另一个JVM上运行的程序。

我们每次在IDE里进行代码调试时,实质上是通过IDE里的debugger这个界面执行GUI操作,然后通过JDI发送数据到JDWP,再经过JVMTI最终实现程序的高度。

每次我们打开IDE调试一个Java应用的时候,或者远程attach一个Java进程的时候,别忘了这个IDE背后的身影—JPDA。

但是经过对比思考发现,java并未采用前面javaScript采用的接口标准,而是采用自己的标准。

当然,gdb的实现与上述特定语言的也不同,感兴趣的同学可以参考相关的资料进行学习了解。

参考:
JavaScript Debugger 原理揭秘
Debug原理
当我们谈Debug时,我们在谈什么(Debug实现原理)


http://chatgpt.dhexx.cn/article/crjiXkxl.shtml

相关文章

程序的调试技巧。

什么是调试? 调试又叫Debug,又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。 生活中所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心有愧疚&#…

程序入门调试

1.先设置断点 在调试代码之前要先设置一个断点,否则调试无法进行; ps:设置断点的方法 找到自己要测试的那一行代码,按f9或者也可直接用鼠标点击代码前的空白处如下图所示; 2.熟悉常用的命令 常用的命令符合有四个,分…

谱分解实例与理解

这篇博文想从一个实际的矩阵出发,阐述谱分解究竟干了什么。——非数学系的cver

谱分解(SD)

前提:矩阵A必须可相似对角化! 充分条件: A是实对称矩阵A有n个互异特征值A^2 AA^2 Er(A) 1 且 tr(A)!0 谱分解(Spectral Decomposition ),又称特征分解,或相似标准形分解,是将矩阵分解为由…

矩阵分解——谱分解

文章目录 先修知识: 幂等矩阵谱分解定理谱分解的流程 谱分解的推论谱分解的应用 先修知识: 幂等矩阵 谱分解定理 谱分解的流程 谱分解的推论 谱分解的应用

【信号与系统|吴大正】4:信号分解、傅里叶变换与信号谱(上)

信号分解、傅里叶变换与信号谱 信号的分解 在学习【信号分解】这一部分时,脑海里要有两个概念: 其一,我们整本书学习的思路就是围绕着将信号分解成基本信号,将系统的响应转变成基本响应这一思路来开展的;其二&#xf…

机器学习笔记——14 矩阵谱分解与奇异值分解及其背后的线性算子理论 (实战项目:利用SVD进行图像压缩)

机器学习笔记——14 矩阵谱分解与奇异值分解及其背后的线性算子理论 (实战项目:利用SVD进行图像压缩) 本篇文章介绍矩阵的谱分解与奇异值分解 (Singular Values Decomposition,SVD),为了对其有一个更为本质性地认识,本文从线性算子的理论讲起…

从矩阵谱分解到矩形的最少正方形剖分

上次听AK讲到谱分解的时候,若有所思,下面将对思考稍作记录。 矩阵谱分解 关于谱分解有很多定义,主要区别在于条件的强弱,有的要求一个 n n n阶矩阵不仅要求可对角化,而且加强条件至其 n n n个特征值 λ 1 , λ 2 , .…

谱本征正交分解 (SPOD)附matlab代码

✅作者简介:热爱科研的Matlab仿真开发者,修心和技术同步精进,matlab项目合作可私信。 🍎个人主页:Matlab科研工作室 🍊个人信条:格物致知。 更多Matlab仿真内容点击👇 智能优化算法 …

通信基础 7 —— 遍历保密速率、谱分解物理意义

目录 遍历保密速率(ergodic secrecy rate)闭式解(解析解)和数值解闭式解数值解 拉普拉斯变换谱分解/正交分解 遍历保密速率(ergodic secrecy rate) 说遍历容量不十分准确,应该叫各态历经性容量…

【推荐系统】特征值分解(谱分解)和奇异值分解(SVD),即在PCA上的应用

特征值分解(谱分解EVD)和奇异值分解(SVD),即在PCA上的应用 1. 概念 特征值分解和奇异值分解在机器学习领域都有着广泛的应用。两者有着很紧密的关系,二者的目的都是一样,就是提取出一个矩阵最…

R语言主成分分析PCA谱分解、奇异值分解预测分析运动员表现数据和降维可视化

最近我们被客户要求撰写关于主成分分析PCA的研究报告,包括一些图形和统计输出。 本文描述了如何 使用R执行主成分分析 ( PCA )。您将学习如何 使用 PCA预测 新的个体和变量坐标。我们还将提供 PCA 结果背后的理论。 主成分分析PCA降维方法和R语言分析葡萄酒可视化实…

【矩阵论】2. 矩阵分解——单阵谱分解

矩阵论 1. 准备知识——复数域上矩阵,Hermite变换) 1.准备知识——复数域上的内积域正交阵 1.准备知识——Hermite阵,二次型,矩阵合同,正定阵,幂0阵,幂等阵,矩阵的秩 2. 矩阵分解——SVD准备知识——奇异值…

可对角化和谱分解的区别

内容为个人理解,才疏学浅,如有错误,欢迎指正。 谱分解定理:向量空间V上的任意正规算子M,在V的某个标准正交基下可以对角化。反之,任意可对角化的算子都是正规的。 理解: (1&#x…

R语言矩阵特征值分解(谱分解)和奇异值分解(SVD)特征向量分析有价证券数据

最近我们被客户要求撰写关于特征值分解的研究报告,包括一些图形和统计输出。 R语言是一门非常方便的数据分析语言,它内置了许多处理矩阵的方法。 作为数据分析的一部分,我们要在有价证券矩阵的操作上做一些工作,只需几行代码。 …

【矩阵论】2. 矩阵分解——正规谱分解

矩阵论 1. 准备知识——复数域上矩阵,Hermite变换) 1.准备知识——复数域上的内积域正交阵 1.准备知识——Hermite阵,二次型,矩阵合同,正定阵,幂0阵,幂等阵,矩阵的秩 2. 矩阵分解——SVD准备知识——奇异值…

【线性代数】矩阵的特征值分解(对角化、谱分解)

目录 1 前言2 矩阵的特征值分解2.1 从定义的角度理解2.2 从变换的角度理解(来自参考文献[3]) 3 对角矩阵(补充)3.1 对角矩阵的定义3.2 对角矩阵线性变换的几何意义 4 矩阵对角化5 相似矩阵与特征值6 参考文献 1 前言 矩阵的特征值分解又可以称作矩阵的对…

矩阵的谱分解 (详细推导步骤~~~特征值分解特征向量

所谓矩阵的分解,就是将一个矩阵写成结构比较简单的或性质比较熟悉的另一些矩阵的乘积。矩阵的分解方法有很多种,包括三角分解、QR(正交三角)分解、最大秩分解、奇异值分解和谱分解,所有这些分解在数值代数和最优化问题…

c语言-gotoxy实现先全部输出再做全部输入操作

需要用到的头文件&#xff1a; #include<windows.h> #include <iostream>代码&#xff1a; gotoxy(a,b)光标控制函数 a为行&#xff0c;b为列&#xff0c;坐标原点在左上角向右是行正方向&#xff0c;向下为列正方向 中文符号汉字在列方向为2个空间&#xff0c;英…