1. 把调试器搬进编辑器:这件事到底在解决什么问题
很多人第一次接触"将调试集成到编辑器"这个说法时,脑子里冒出来的画面是:装个插件,点个绿色三角,程序跑起来,完事。真上手之后才发现,事情远没有这么简单——断点打上去不生效、变量窗口一片空白、程序跑飞了却停在某个看不懂的汇编行、明明本地能跑远程却连不上。这些坑,几乎每个把调试流程从"命令行打印日志"迁移到"图形化调试器"的人都踩过。
所谓"将调试集成到编辑器",核心是把原本分散在终端、日志文件、独立调试工具里的调试能力,收拢到同一个工作界面里。你写代码的地方,同时也是你观察程序运行状态、控制执行流、检查内存与变量、复现问题的地方。它解决的是"上下文切换"这个隐形杀手:改一行代码、切到终端、敲命令、看输出、再切回来改,这个循环每多一次,注意力就碎一次。集成之后,断点、单步、调用栈、变量监视、条件断点、表达式求值全部在一个窗口内完成,调试从"体力活"变成"观察活"。
这篇内容适合三类人看。第一类是刚学编程、还在用打印语句排查问题的同学,你需要一套比print更系统的观察手段;第二类是有一定经验、但调试习惯还停留在"改代码加日志"的开发者,你想知道图形化调试器到底能省多少事;第三类是要给团队搭建统一调试环境的人,你需要知道配置项背后的逻辑,而不是照抄一份配置文件。下面我会从整体设计思路讲起,把调试集成的核心机制、配置细节、实操流程和踩坑经验一层层拆开,尽量做到你照着做就能复现。
2. 整体设计与思路拆解:为什么是"集成"而不是"外挂"
2.1 调试能力的两种组织方式及其取舍
调试器本质上是一个能控制目标进程、读写其内存、拦截其执行流的程序。它和编辑器之间的关系,历史上主要有两种组织方式。
第一种是外挂式:编辑器只负责写代码,调试交给独立的调试器程序,两者通过命令行参数、进程附加或者文件交换来协作。这种方式的好处是解耦彻底,调试器可以单独升级、单独使用,编辑器挂了不影响调试。坏处也很明显:界面割裂,源码位置和调试状态对不上号,断点要在调试器里重新定位,变量要在另一个窗口里查,来回切换的成本很高。
第二种是集成式:编辑器通过一套标准协议与调试器通信,把调试器的能力"翻译"成编辑器界面里的断点、单步按钮、变量面板、调用栈视图。编辑器负责展示和交互,调试器负责真正的进程控制,两者各司其职但界面统一。这种方式的关键在于那套"标准协议"——它定义了一组请求和响应消息,比如"在某文件的某行设置断点""继续执行""读取某个变量的值""获取当前调用栈",编辑器和调试器只要都遵守这套协议,就能自由组合。
选择集成式的理由很直接:调试是一个高频、短周期、强上下文的活动。你打断点的时候脑子里想的是"这一行的这个变量为什么是这个值",这个念头存在的时间很短,任何界面切换都会打断它。集成式把这个念头和观察结果放在同一个视野里,认知负担最小。代价是配置复杂度上升——你需要让编辑器知道用哪个调试器、怎么启动目标程序、源码路径怎么映射,这些就是后面要讲的配置文件。
提示:集成式调试的前提是编辑器与调试器之间有一套双方都认的通信协议。理解这一点,后面遇到"断点变灰""连上了但停不下来"这类问题时,你就能顺着"协议握手是否成功"这条线去排查,而不是盲目改配置。
2.2 一次调试会话的完整生命周期
要把配置写对,先得知道一次调试会话从头到尾发生了什么。我把它拆成五个阶段,每个阶段对应不同的配置项和不同的故障表现。
阶段一:启动请求。你在编辑器里按下调试按钮,编辑器读取配置文件,拼出一条"启动目标程序"的请求发给调试器。这条请求里包含可执行文件路径、工作目录、命令行参数、环境变量。配置写错,这个阶段就会报"找不到文件""启动失败"。
阶段二:调试器接管。调试器启动目标进程,但让它停在入口处而不是直接跑完。这一步依赖调试器对操作系统的进程控制能力。如果目标程序需要特殊权限、或者被安全软件拦截,就会卡在这里。
阶段三:断点绑定。编辑器把你在源码里打的断点位置发给调试器,调试器把它们转换成实际的内存地址。源码行号和内存地址之间的换算依赖调试符号信息。符号缺失或版本不匹配,断点就会"绑定失败",表现为断点是空心圆或者灰色。
阶段四:执行控制与状态查询。程序停在断点后,你单步、继续、查看变量,每一次操作都是一次请求-响应往返。变量显示不出来,通常是这个阶段的求值请求失败,原因可能是变量被优化掉了、作用域不对、或者表达式语法不被调试器接受。
阶段五:会话结束。程序退出或被终止,调试器回收资源,编辑器恢复普通编辑状态。如果进程残留,下次启动可能端口冲突或附加失败。
把这五个阶段记在心里,排查问题时先判断"卡在哪一阶段",比漫无目的地翻配置高效得多。这也是我建议新手不要一上来就抄复杂配置的原因——你不知道每一行是给哪个阶段用的,出了问题就无从下手。
2.3 配置文件的结构逻辑:为什么分成"启动"和"附加"两类
集成调试的配置文件通常围绕两种调试模式组织:启动模式和附加模式。
启动模式是你从编辑器发起,由调试器负责把目标程序拉起来。它适合"我要从头复现一个问题"的场景,配置里要写清楚程序在哪、参数是什么、工作目录在哪。附加模式是目标程序已经在跑,你让调试器挂上去。它适合"服务已经启动、问题在运行中才出现"的场景,配置里要写清楚进程标识或者监听端口。
为什么要把这两类分开?因为它们的失败模式完全不同。启动模式失败,多半是路径、参数、权限问题;附加模式失败,多半是进程找不到、端口没监听、权限不足。分开配置,排查时能快速缩小范围。很多配置模板会把两类都写进去,用不同的名字区分,你在下拉菜单里选哪个,就走哪条路径。
理解了这层结构,你就明白配置文件不是一堆散乱的键值对,而是"描述一次会话如何建立"的声明。每个字段都在回答五个阶段里的某个问题。
3. 核心细节解析与实操要点:配置项背后的真实含义
3.1 可执行路径与工作目录:两个最容易混淆的字段
配置文件里最容易被轻视、又最容易出问题的两个字段,是可执行文件路径和工作目录。
可执行文件路径回答的是"启动哪个程序"。它可以是绝对路径,也可以是相对于配置文件所在目录的路径。这里有个常见误区:很多人以为填了源码文件路径就行,实际上调试器要的是编译产物的路径——编译型语言是编译出来的可执行文件,解释型语言是解释器加上脚本入口。填错这一项,报错通常是"文件不存在"或者"不是有效的可执行文件"。
工作目录回答的是"程序在哪个目录下运行"。这个字段影响的是程序内部的相对路径解析——读配置文件、写日志、加载资源,全都相对于工作目录。我见过太多"本地跑得好好的、一调试就找不到配置文件"的案例,根因就是工作目录没设对:程序以为自己在某个目录下,实际被调试器拉到了另一个目录。
实操建议是这样:可执行路径尽量用变量占位符拼出来,工作目录显式写成项目根目录。用变量占位符的好处是换机器、换编译输出目录时不用改配置;显式写工作目录的好处是行为可预期,不依赖调试器从哪个目录被唤起。
{ "program": "${workspaceFolder}/build/app", "cwd": "${workspaceFolder}", "args": ["--config", "config/app.conf"] }上面这段配置里,program用工作区变量拼出可执行文件位置,cwd锁定在项目根目录,args传入相对路径的配置文件。三者配合,程序无论在谁的机器上启动,读到的都是项目根目录下的配置。这里的关键是理解cwd决定了args里相对路径的基准点——如果cwd写错,args里的相对路径就会指向别处。
注意:不要用"当前打开的文件所在目录"作为工作目录的默认值。调试时你打开的文件可能在任何子目录里,工作目录跟着文件走会导致行为飘忽不定。工作目录应该是一个稳定的、与项目结构绑定的位置。
3.2 调试符号:断点能不能停下来的命门
断点打上去不生效,十有八九是调试符号的问题。这里需要把原理讲清楚,否则你只会机械地"重新编译一遍试试"。
编译型语言在编译时,可以选择是否生成调试符号。调试符号记录了源码行号、变量名、函数名与编译后机器码地址之间的映射关系。没有符号,调试器只知道"程序在地址 0x1234 处停住了",不知道这对应哪一行源码、哪个变量。图形化调试器拿到这种信息,只能显示反汇编,断点自然也就绑不上源码行。
所以配置调试环境时,编译选项必须包含生成调试符号的设置。以常见的编译工具链为例,通常是一个类似-g的开关,配合关闭优化或者降低优化等级。为什么要关闭优化?因为优化器会重排指令、合并变量、删除"看起来没用"的代码,导致源码行号和实际执行流对不上,变量被优化进寄存器后调试器也读不到。调试构建和发布构建用不同的编译选项,这是行业惯例,不是多此一举。
| 构建类型 | 调试符号 | 优化等级 | 适用场景 |
|---|---|---|---|
| 调试构建 | 开启 | 关闭或最低 | 断点调试、变量观察、单步跟踪 |
| 发布构建 | 可选保留 | 较高 | 性能测试、上线部署 |
解释型语言没有编译这一步,但同样存在"符号"问题——源码路径映射。调试器需要知道运行中的代码对应磁盘上哪个文件。如果程序是从打包产物、容器内路径或者远程机器上运行的,源码路径就对不上,断点会显示为"未绑定"。这时候需要配置路径映射,把运行环境的路径翻译成本地路径。
3.3 断点类型的选择:普通断点只是入门
大部分人只用过普通行断点,但集成调试器提供的断点类型远不止这一种。用对了断点类型,能省掉大量"改代码加判断再重跑"的循环。
条件断点:只有满足某个表达式时才停下来。典型场景是循环里某个特定迭代出问题,你不想停几百次。配置方式是给断点附加一个布尔表达式,比如索引等于某个值、某个字段等于特定内容。条件表达式由调试器求值,语法要符合调试器的规则,写错了断点会静默失效或者报求值错误。
命中计数断点:执行到第 N 次时才停。适合"问题在若干次调用后才出现"的场景,比条件断点更轻量,因为不需要每次求值表达式。
日志断点:不停下来,只往调试控制台输出一条消息。这是"打印日志"和"断点"的合体——你保留了观察能力,又不打断执行流。适合高频调用的函数,停下来会严重拖慢程序,但你又想知道它被调用的参数。
异常断点:程序抛出异常时自动停下,停在抛出点而不是被捕获后的位置。排查"异常被吞掉"的问题时特别有用,因为你能看到异常发生时的完整调用栈。
这几种断点的组合使用,是调试效率的分水岭。我的习惯是:先用日志断点摸清调用频率和参数范围,再用条件断点精确停在异常那次,最后用异常断点确认抛出位置。整个过程不改一行代码,不重新编译。
3.4 变量查看与表达式求值:为什么有些变量显示不出来
程序停住之后,变量面板里有些变量显示正常,有些显示"不可用"或者"已优化",这不是调试器坏了,而是有明确原因的。
第一种原因是作用域。变量面板默认展示当前栈帧的局部变量。如果你要看的是外层函数的变量或者全局变量,需要切换栈帧或者在监视窗口里手动添加表达式。很多人不知道栈帧可以切换,看到变量面板空空如也就开始怀疑配置,其实只是看错了地方。
第二种原因是优化。前面讲过,优化构建会把变量放进寄存器、合并、甚至删除。调试构建下这个问题基本消失,所以"变量看不到"的第一反应应该是检查构建类型。
第三种原因是求值时机。有些变量的值需要调用函数才能得到(比如属性的 getter),调试器求值时可能触发副作用,所以默认不自动求值。你需要手动在监视窗口里添加这个表达式,明确表示"我知道可能有副作用,求给我看"。
表达式求值窗口是集成调试器里被低估的功能。它不只是看变量,还能做计算、调用方法、检查数据结构。排查复杂数据结构时,我经常在求值窗口里写一段遍历逻辑,直接看结果,比在代码里加打印再重跑快得多。需要注意的是,求值表达式运行在目标进程的上下文里,如果表达式本身有副作用或者会阻塞,会影响程序状态,用的时候心里要有数。
4. 实操过程与核心环节实现:从零搭起一条可用的调试链路
4.1 环境准备与前置检查清单
动手配置之前,先把前置条件确认一遍,能省掉后面一半的排查时间。我整理了一份检查清单,按顺序过一遍。
- 编译器或解释器版本确认:确认工具链已安装且版本符合项目要求。版本不匹配是"能编译但调试行为诡异"的常见根因。
- 调试器本体确认:集成调试依赖一个独立的调试器程序,确认它已安装且在命令行下能正常调用。编辑器插件通常只是"前端",真正的调试能力来自这个后端程序。
- 调试符号生成确认:检查构建配置里调试符号开关是否打开,优化等级是否适合调试。
- 编辑器调试扩展确认:确认对应语言或运行时的调试扩展已安装并启用。不同语言需要不同的扩展来"翻译"协议消息。
- 权限确认:确认当前用户有权限启动目标程序、附加到目标进程、监听所需端口。权限不足的表现往往是"启动即退出"或"附加被拒绝"。
这份清单看起来基础,但实际排查中,相当比例的问题就出在其中某一项没确认。尤其是调试器本体和编辑器扩展的版本匹配——扩展更新了但调试器没更新,协议版本对不上,表现就是连接建立后立刻断开,日志里一堆看不懂的握手错误。
4.2 生成配置文件:手写还是自动生成
配置文件可以手写,也可以由编辑器根据项目类型自动生成模板。我的建议是:第一次用自动生成,之后手写维护。
自动生成的好处是模板里包含了该语言最常见的字段组合,能让你快速跑通第一次会话。坏处是模板往往包含大量注释掉的备选项,字段含义不直观,直接拿去用容易"看起来配了其实没生效"。
手写维护的好处是每个字段都是你明确需要的,出了问题能定位到具体行。手写时建议从最小配置开始:只写可执行路径、工作目录、调试器类型三个字段,跑通一次最简单的"启动并停在入口",然后逐步加参数、加环境变量、加路径映射。每加一项验证一次,出问题就知道是哪一项引入的。
{ "type": "调试器类型标识", "request": "launch", "name": "本地调试", "program": "${workspaceFolder}/build/app", "cwd": "${workspaceFolder}", "stopOnEntry": true }这段最小配置里,type决定用哪个调试扩展,request区分启动还是附加,name是下拉菜单里显示的名字,stopOnEntry让程序停在入口方便验证链路是否通。跑通之后,再往里加args、env、sourceMap等字段。这种增量式的配置方式,比一次性抄一份几十行的配置然后对着报错猜要可靠得多。
4.3 第一次会话:验证链路是否打通
配置写好后,第一次会话的目标不是"调试业务逻辑",而是"验证链路"。具体做法是:在入口函数的第一行打一个断点,启动调试,观察程序是否停在那一行。
停住了,说明启动、接管、断点绑定三个阶段都通了。这时候做三件事验证后续阶段:在变量面板看一个已知值的局部变量,验证状态查询;按一次单步,验证执行控制;按继续让程序跑完,验证会话结束是否干净。
没停住,按阶段排查:程序根本没启动,查可执行路径和工作目录;程序启动了但没停在断点,查调试符号和断点绑定状态;停在了奇怪的位置(比如反汇编窗口),查符号是否加载、源码路径是否匹配。
这一步的验证价值在于建立基线。后面加任何复杂配置,只要基线还在,出问题就能对比"是新增配置引入的"还是"环境本身变了"。
4.4 附加模式实战:调试已经在运行的程序
附加模式的配置和启动模式差别不大,核心是把request改成附加,并指定如何找到目标进程。常见方式有两种:按进程标识附加,按监听端口附加。
按进程标识附加适合本地进程。配置里写一个进程选择策略,启动调试时编辑器会弹出进程列表让你选。这里有个实操细节:进程列表里同名进程可能有好几个,选错了会附加到无关进程上,表现是"断点不生效但程序也没报错"。附加前先在系统进程列表里确认目标进程的准确标识,能避免这个坑。
按监听端口附加适合服务类程序。程序启动时带调试监听参数,调试器通过网络连上去。这种方式的好处是可以调试远程机器上的程序,坏处是网络配置、防火墙、监听地址任何一个环节出问题都连不上。排查顺序是:先确认程序确实在监听(看启动日志或端口状态),再确认监听地址允许外部连接,最后确认调试器的连接目标地址和端口写对了。
提示:附加模式下断点绑定失败的常见原因是源码版本不一致。你本地打开的是最新代码,运行中的程序是旧版本编译的,行号对不上,断点自然绑不上。附加前确认运行中的程序和你正在看的源码是同一版本。
4.5 多目标调试:一次会话调试多个进程
稍微复杂一点的项目会涉及多个进程协作,比如一个主程序加若干工作进程。集成调试器通常支持在一个配置里定义多个调试目标,启动时同时拉起或附加。
配置方式是把多个目标写成一个数组,每个元素是一套完整的启动或附加配置。启动后,调试面板会出现多个会话,可以分别暂停、单步、查看变量。这里的关键细节是共享状态的处理:多个进程如果通过共享内存、文件、网络通信,你在一个进程里停住时,另一个进程可能还在跑,观察到的状态可能不一致。调试这类场景时,通常需要让相关进程同时停住,或者接受"观察的是某一瞬间的快照"这个事实。
多目标调试的配置复杂度上升明显,我的建议是:先用单目标把每个进程单独调通,确认每个进程的配置都正确,再把它们合并成一个多目标配置。合并后如果某个目标起不来,问题一定在合并环节,排查范围小很多。
5. 常见问题与排查技巧实录
5.1 断点变灰、空心、不生效的分层排查法
断点问题是集成调试里最高频的故障,表现相似但根因分散。我总结了一个分层排查的顺序,从外到内逐层排除。
第一层,断点是否被调试器接受。断点图标的状态直接反映绑定结果:实心红点表示已绑定,空心或灰色表示未绑定。未绑定的第一嫌疑是符号问题,回到构建配置确认调试符号已生成、优化已关闭。
第二层,源码路径是否匹配。符号里有行号信息,但调试器还要把符号里的文件路径和本地文件对应起来。路径不匹配的典型场景是:程序在容器里编译、在另一台机器上编译、或者用了符号链接。解决办法是配置路径映射,把运行环境的路径前缀映射到本地路径前缀。
第三层,代码是否真的被执行。断点绑定成功但程序跑完都没停,可能是那段代码根本没走到。这时候用日志断点或者临时在函数入口打断点验证调用链,比盯着断点怀疑配置有效。
第四层,断点是否被条件或命中计数过滤。条件表达式写错、命中计数设得太大,都会让断点"看起来没生效"。检查断点的附加属性,把条件临时清空验证一次。
| 现象 | 最可能原因 | 排查动作 |
|---|---|---|
| 断点空心/灰色 | 符号缺失或路径不匹配 | 检查构建符号开关、配置路径映射 |
| 断点实心但不停 | 代码未执行或条件过滤 | 加日志断点验证调用链、清空条件 |
| 停在反汇编窗口 | 符号未加载 | 确认调试符号文件存在且版本匹配 |
| 附加后断点全灰 | 源码版本与运行程序不一致 | 重新编译运行程序或切换到对应源码版本 |
5.2 程序启动即退出、看不到任何输出
这个现象在启动模式下很常见,尤其是配置刚写好第一次跑的时候。程序一闪而过,调试控制台没有输出,断点也没停。
排查顺序是这样的。先看调试控制台的启动日志,确认调试器是否真的发出了启动请求、目标程序路径解析成了什么。路径解析错误是最常见的原因——变量占位符拼出来的路径和你以为的不一样。把解析后的路径复制出来,在命令行里手动执行一次,能快速验证路径和参数是否正确。
如果路径没问题,检查程序是否在入口之前就退出了。有些程序在加载动态库、读取配置、初始化环境时失败会直接退出,这些失败发生在调试器接管之前或者接管瞬间,断点来不及生效。这时候把stopOnEntry打开,让程序停在入口,再单步往下走,就能看到具体在哪一步退出。
还有一种情况是程序需要交互式输入,而调试控制台没有正确连接标准输入,程序等待输入时看起来像卡住或者退出。检查配置里是否有标准输入相关的设置,必要时把输入重定向配置好。
5.3 变量显示"不可用"的四种典型场景
变量面板显示异常,按场景分类处理效率最高。
场景一:优化构建。变量被优化进寄存器或删除,调试器读不到。切换到调试构建,问题消失。这是最常见也最容易解决的一类。
场景二:作用域不对。当前栈帧里确实没有这个变量。切换栈帧,或者在监视窗口手动添加表达式。新手常犯的错误是盯着变量面板找外层函数的变量,找不到就以为调试器坏了。
场景三:求值有副作用被跳过。某些语言的调试器对可能触发副作用的求值采取保守策略,不自动展示。手动在监视窗口添加,明确请求求值。
场景四:类型不支持展示。复杂对象、自定义类型、循环引用结构,调试器的默认展示器可能处理不了。这时候用表达式求值窗口手动展开,或者调用对象的字符串化方法看内容。
这四类场景对应四种不同的处理动作,先分类再动手,比盲目重启调试会话有效。
5.4 性能断崖:为什么一开调试程序就慢十倍
调试会话下程序变慢是正常现象,但慢到影响观察就需要处理。变慢的来源主要有三个。
断点求值开销。条件断点和日志断点每次命中都要在目标进程里求值表达式,高频调用的位置会显著拖慢程序。把条件断点换成命中计数断点,或者把日志断点移到调用频率更低的位置,能明显改善。
单步开销。单步执行每次都要和调试器往返通信,逐行单步一个热循环会慢到无法忍受。改用"运行到光标处"或者"条件断点加继续",减少往返次数。
调试器附加本身的开销。调试器接管进程后,异常处理、内存访问的路径都变长了。这部分开销无法完全消除,只能接受。如果性能问题本身就是要调试的对象,考虑用采样分析工具而不是断点调试。
理解这三个来源,你就知道"调试时慢"不一定是程序的问题,也不一定是配置的问题,很多时候是调试手段本身的开销。选对调试手段,比优化配置更有效。
5.5 几个我踩过的坑和对应的经验
第一个坑是配置文件放错位置。集成调试器通常按约定在项目根目录的特定子目录里找配置文件,放错位置会导致"配置明明写了却不生效"。确认配置文件的约定位置,或者用编辑器提供的"打开配置"入口直接定位。
第二个坑是环境变量继承问题。从编辑器启动的程序,环境变量继承自编辑器进程,而不是你的终端会话。终端里配好的路径、密钥、代理设置在调试会话里可能不存在。解决办法是在配置里显式声明需要的环境变量,不依赖继承。
第三个坑是端口冲突。附加模式或者带调试监听的服务,端口被占用时连接失败,报错信息往往很隐晦。排查时先确认端口占用情况,再检查是否有上次调试残留的进程没退干净。
第四个坑是符号缓存过期。改了代码重新编译,但调试器用了缓存的旧符号,断点位置对不上。清理符号缓存或者重启调试会话能解决。这个坑的隐蔽之处在于"代码明明改了,断点却停在旧位置",很容易怀疑到编辑器头上。
这些坑的共同特点是:报错信息不直接指向根因,需要你理解调试链路的每个环节才能定位。这也是为什么前面花大篇幅讲原理——原理清楚了,报错信息才能被正确解读。
6. 把调试集成用成习惯:一些长期有效的做法
配置跑通只是起点,真正让调试集成产生价值的是把它变成日常习惯。我自己的做法是给每个项目维护一份"调试配置基线":最小可用的启动配置、常用的附加配置、必要的路径映射,全部写进版本控制。新环境拉下来,改一改变量占位符就能用,不用每次从零配。
另一个习惯是给断点"留注释"。条件断点的表达式、日志断点的输出格式,过几天自己都忘了为什么这么写。在配置文件的注释里记一句"这个条件断点用于复现某类输入下的异常",下次接手的人(包括未来的自己)能省很多时间。
还有一点是区分"调试构建"和"发布构建"的使用边界。调试构建慢但可观察,发布构建快但难调试。日常开发用调试构建,性能验证和上线用发布构建,两者不要混用。混用的代价是你在发布构建上调试,然后花大量时间怀疑人生。
最后分享一个我常用的技巧:遇到"偶发、难复现"的问题,不要急着打断点,先用日志断点把关键路径的参数和返回值打出来,跑一段时间收集数据,找到规律后再用条件断点精确停在异常那次。这个"先观察后拦截"的顺序,比一上来就单步跟踪高效得多,尤其是在多线程或者异步场景下。
调试集成的本质,是把"猜测程序在做什么"变成"观察程序在做什么"。配置只是手段,观察习惯才是目的。配置写对一次,后面每次调试都在还这份配置的利息。