Posted in

【Go调试环境配置紧急补丁】:解决VSCode v1.86+与Go 1.22.0+因debug adapter protocol v3变更引发的断点注册失败

第一章:Go调试环境配置紧急补丁概述

当Go应用在生产或开发环境中出现难以复现的竞态、内存泄漏或崩溃时,标准go rungo build流程往往无法提供足够可观测性。此时需快速启用一套轻量、可复现、无需重启服务即可生效的调试能力——即“调试环境配置紧急补丁”,它不是功能更新,而是对现有开发链路的可观测性加固。

安装并启用Delve调试器

确保系统已安装dlv(Delve)最新稳定版(v1.23+):

# 从源码安装(推荐,避免包管理器版本滞后)
go install github.com/go-delve/delve/cmd/dlv@latest
# 验证安装
dlv version | grep "Version"

注意:若使用Go 1.21+,需确保CGO_ENABLED=1,否则dlv test等命令可能因缺少符号表而失败。

启用运行时调试支持

在启动程序前注入关键调试标志,避免重新编译:

# 启用pprof HTTP端点(默认不开启)
GODEBUG="mmap=1" \
  GIN_MODE=release \
  go run -gcflags="all=-N -l" main.go --addr=:8080

其中-gcflags="all=-N -l"禁用内联与优化,保留完整调试信息;GODEBUG=mmap=1增强内存分配追踪精度,对排查runtime: out of memory类问题尤为关键。

快速接入核心诊断工具

工具 启动方式 适用场景
pprof CPU curl "http://localhost:8080/debug/pprof/profile?seconds=30" 长时间CPU热点分析
pprof Goroutine curl "http://localhost:8080/debug/pprof/goroutine?debug=2" 查看所有goroutine堆栈(含阻塞状态)
Delve Attach dlv attach $(pgrep -f 'main.go') --headless --api-version=2 动态附加到正在运行的进程

所有上述操作均无需修改业务代码,可在5分钟内完成部署,适用于CI/CD流水线卡点、预发环境突袭压测后的问题定界,以及Kubernetes Pod中exec -it直接调试等应急场景。

第二章:DAPv3协议变更深度解析与影响评估

2.1 DAPv3核心变更点:InitializeRequest与LaunchRequest语义演进

DAPv3重构了调试会话的初始化契约,InitializeRequest不再隐式触发目标准备,仅声明客户端能力;而LaunchRequest首次承担完整执行环境构建职责

语义解耦对比

字段 DAPv2(隐式) DAPv3(显式)
path 在Initialize中传递 仅出现在LaunchRequest中
supportsConfigurationDone 布尔响应字段 移除,由configurationDoneRequest显式调用

关键流程变化

// DAPv3 LaunchRequest 示例(带必要约束)
{
  "command": "launch",
  "arguments": {
    "program": "/app/main.js",
    "stopOnEntry": true,
    "env": {"NODE_ENV": "debug"} // 新增环境隔离字段
  }
}

逻辑分析:stopOnEntry现为必选参数(非默认true),强制客户端明确初始断点策略;env字段启用沙箱级环境变量注入,避免污染宿主进程。

状态机演进

graph TD
  A[InitializeRequest] -->|仅协商协议能力| B[ClientCapabilities]
  B --> C[LaunchRequest]
  C -->|验证路径/权限/依赖| D[SpawnProcess]
  D --> E[AttachDebugger]

2.2 Go 1.22.0+ runtime/debug 模块对DAPv3的适配行为分析

Go 1.22.0 起,runtime/debug 新增 SetDebugFlagsReadGCStats 的 DAPv3 兼容字段映射,显著增强调试器协议一致性。

DAPv3 关键字段映射

  • goroutineIDid(保持 uint64 语义)
  • stacktraceDepthmaxFrameCount(默认 50,可配置)
  • 新增 debugFlags.goroutineLabels = true 控制标签注入

运行时调试标识适配

// 启用 DAPv3 兼容模式(需在 init 或 early main 中调用)
debug.SetDebugFlags("dapv3,labels,stackframes")

该调用将强制 runtime 在 Stack()Goroutines() 输出中插入 dapv3 元数据头,并启用 goroutine label 字段序列化(如 "label":"http-handler"),供 DAPv3 threadsstackTrace 请求消费。

字段名 DAPv3 类型 Go 1.22+ 行为
id integer 保持与 Goid() 一致,无符号截断防护
name string 自动 fallback 到 GoroutineName()
labels object 仅当 debugFlags.labels 启用时存在
graph TD
    A[DAPv3 Request: threads] --> B[runtime/debug.Goroutines]
    B --> C{debugFlags.dapv3?}
    C -->|yes| D[Inject dapv3 metadata headers]
    C -->|no| E[Legacy format]
    D --> F[Serialize labels + maxFrameCount]

2.3 VSCode v1.86+ debug adapter 架构重构导致断点注册路径断裂

v1.86 起,VSCode 将 DebugAdapterDescriptorFactory 的生命周期与 DebugSession 强解耦,移除了 createDebugAdapterDescriptor 中对 session 的隐式依赖。

断点注册链路变更

  • 旧路径:setBreakpoints → initialize → launch/attach → registerBreakpoints
  • 新路径:setBreakpoints 现在需在 initialize 完成后、configurationDone 前触发,否则被静默丢弃

关键适配代码

// extension.ts(适配后)
export class MyDebugAdapterDescriptorFactory implements DebugAdapterDescriptorFactory {
  createDebugAdapterDescriptor(_session: DebugSession): ProviderResult<DebugAdapterDescriptor> {
    return new DebugAdapterInlineImplementation(new MyDebugSession());
  }
}

MyDebugSession 必须重写 setBreakpoints,并在内部校验 this.initialized === true,否则调用 this.sendErrorResponse() 报错 "Breakpoint registration rejected: session not ready"

阶段 v1.85 行为 v1.86+ 行为
setBreakpoints 调用时机 可在 initialize 仅在 initialized === true 时有效
错误响应方式 无显式错误 返回 ErrorResponse 并中断调试会话
graph TD
  A[Client: setBreakpoints] --> B{Session initialized?}
  B -->|No| C[sendErrorResponse]
  B -->|Yes| D[Forward to DA]
  D --> E[DA registers via DAP 'setBreakpoints' event]

2.4 断点注册失败的典型日志特征与协议层抓包验证方法

常见日志模式识别

断点注册失败时,调试日志中高频出现以下特征:

  • ERR_BP_REG_TIMEOUT(超时未收到ACK)
  • WARN_BP_ID_CONFLICT(断点ID已被占用)
  • ERR_PROTO_MISMATCH: expected v3, got v2(协议版本不兼容)

协议层验证关键步骤

  1. 使用 tshark -Y "tcp.port == 8080 && tcp.len > 0" 捕获注册流量
  2. 过滤并导出注册请求/响应报文(PDU)
  3. 比对 BP-Register header 中的 seq, version, nonce 字段一致性

典型失败报文结构对比

字段 成功注册响应 失败响应(ID冲突)
HTTP Status 201 Created 409 Conflict
X-BP-ID bp_7f3a9c1e bp_7f3a9c1e(重复)
Retry-After 30(秒级退避建议)
# 抓包后提取注册PDU载荷(含Base64解码)
tshark -r reg.pcap -Y "http.request.method==POST && http.host contains 'debug'" \
  -T fields -e http.request.uri -e data.text | head -n 1 | \
  awk '{print $2}' | base64 -d | hexdump -C

该命令提取HTTP POST载荷并以十六进制呈现原始PDU。关键校验点:0x0001(注册类型码)后紧跟4字节session_id,若其值为全零或与已注册会话重复,则触发服务端拒绝逻辑。nonce字段(偏移+12字节)若未满足单调递增约束,亦会导致校验失败。

2.5 兼容性矩阵构建:go version × vscode version × delve version 交叉验证实践

Go 调试链路高度依赖三者协同:go 编译器生成符合 DWARF 标准的调试信息,vscode-go 扩展作为桥梁调用 dlv,而 delve 必须能正确解析对应 Go 版本生成的符号与运行时结构。

验证策略

  • 每次 Go 大版本发布后,需在 CI 中固定 vscode(1.89+)与 dlv(1.23+)组合,执行跨版本调试用例(断点命中、变量展开、goroutine 切换)
  • 使用 go version -m 检查二进制是否含完整调试元数据;dlv version 确认其构建所用 Go 版本

典型兼容性表(节选)

Go Version VS Code ≥ Delve ≥ 关键限制
1.21.x 1.85 1.21.0 需启用 "dlvLoadConfig": {"followPointers": true}
1.22.x 1.89 1.22.2 dlv dap 模式下需 --check-go-version=false
# 启动 DAP 调试服务,显式指定 Go 版本兼容性绕过检查
dlv dap --check-go-version=false --log-output=dap,debug \
  --headless --listen=:2345 --api-version=2

该命令禁用 Go 版本强校验(避免 1.22.3 与旧 dlv 的误报),同时启用 dapdebug 双日志通道,便于定位协议层与调试器内核交互异常。--api-version=2 确保与 vscode-go v0.39+ DAP 客户端语义对齐。

graph TD
    A[VS Code 启动调试会话] --> B[vscode-go 调用 dlv dap]
    B --> C{dlv 解析 go binary DWARF}
    C -->|Go 1.22+| D[识别 new runtime·gcBgMarkWorker 符号]
    C -->|Go <1.21| E[回退至 legacy goroutine stack 解析]
    D --> F[正确停靠在 GC worker 断点]
    E --> G[可能丢失 goroutine 状态]

第三章:VSCode Go调试栈关键组件诊断与校准

3.1 验证delve(dlv)版本兼容性及DAPv3启用状态的自动化检测脚本

Delve v1.21.0 起正式支持 DAPv3 协议,但旧版 dlv version 输出格式不统一,需解析二进制元数据与运行时能力双校验。

核心检测维度

  • Go 运行时版本兼容性(≥1.21)
  • dlv 二进制是否启用 --dap-v3 CLI 标志支持
  • 实际监听端口是否响应 DAPv3 initialize 请求

自动化验证脚本(Bash)

#!/bin/bash
# 检测 dlv 是否满足 DAPv3 启用条件
DLV=$(command -v dlv)
[ -z "$DLV" ] && { echo "ERROR: dlv not found"; exit 1; }

# 提取语义化版本(适配 v1.21.0+ 与 pre-release 格式)
VERSION=$($DLV version 2>/dev/null | grep "Delve Version" | sed -E 's/.*Delve Version: ([^[:space:]]+).*/\1/')
MAJOR_MINOR=$(echo "$VERSION" | sed -E 's/^v?([0-9]+\.[0-9]+)\..*/\1/')

# 检查最低版本要求(1.21+)
if awk -v a="$MAJOR_MINOR" 'BEGIN{exit !(a >= 1.21)}'; then
  echo "✅ Version $VERSION meets DAPv3 baseline"
else
  echo "❌ Version $VERSION too old for DAPv3"
  exit 1
fi

逻辑分析:脚本先定位 dlv 可执行路径,再通过正则提取主版本号(如 1.21),避免 v1.21.0-rc.1 等变体解析失败;awk 浮点比较确保语义化版本判定准确,规避字符串字典序误判(如 "1.9" > "1.21")。

DAPv3 支持能力对照表

特性 dlv v1.20 dlv v1.21+ 检测方式
--dap-v3 CLI flag dlv help | grep --dap-v3
initialize 响应 HTTP 404 JSON-RPC 2.0 curl -s -X POST ...
graph TD
  A[启动检测] --> B[定位 dlv 二进制]
  B --> C[解析版本字符串]
  C --> D{≥ v1.21?}
  D -->|否| E[终止并报错]
  D -->|是| F[检查 --dap-v3 是否在 help 中]
  F --> G[发起轻量 DAP initialize 探测]

3.2 VSCode launch.json中debugAdapterPath与apiVersion字段的精准配置实践

debugAdapterPathapiVersion 是 VSCode 调试协议演进的关键控制点,直接影响调试器兼容性与功能可用性。

debugAdapterPath 的路径语义

该字段指定自定义 Debug Adapter 的可执行入口(如 Node.js 脚本或二进制),必须为绝对路径或相对于工作区根目录的相对路径

{
  "debugAdapterPath": "./out/debugAdapter.js"
}

✅ 正确:./out/ 表示工作区根下的构建产物;❌ 错误:node_modules/.bin/pwa-node(未经 vscode-debugadapter 规范封装将导致 handshake 失败)

apiVersion 的语义约束

该字段声明适配器所实现的 DAP(Debug Adapter Protocol)版本,当前主流为 "v2"(对应 DAP v1.67+):

apiVersion 支持特性 兼容最低 VSCode 版本
"v1" 基础断点、变量查看 1.40
"v2" 按步执行暂停、热重载调试支持 1.75

配置协同逻辑

{
  "version": "0.2.0",
  "configurations": [{
    "type": "pwa-node",
    "request": "launch",
    "name": "Debug App",
    "program": "${workspaceFolder}/src/index.js",
    "debugAdapterPath": "./node_modules/vscode-js-debug/out/src/debugAdapter.js",
    "apiVersion": "v2"
  }]
}

此配置要求 vscode-js-debug@v1.75+apiVersion: "v2" 启用异步堆栈跟踪;若 debugAdapterPath 指向旧版 adapter,VSCode 将静默降级至 v1 并禁用新特性。

3.3 Go extension(golang.go)v0.39+ 对DAPv3的桥接机制与fallback策略实测

DAPv3桥接核心流程

Go扩展通过dapAdapter抽象层封装底层调试器(dlv-dap),在v0.39+中引入bridgeSession对象统一处理协议转换:

// bridgeSession.go 片段:DAPv3兼容性桥接逻辑
func (b *bridgeSession) HandleRequest(req *dap.Request) error {
    if b.capabilities.SupportsDAPv3() {
        return b.v3Handler.Process(req) // 直接转发至DAPv3原生处理器
    }
    return b.fallbackV2Adapter.AdaptAndForward(req) // 向下适配DAPv2语义
}

该逻辑基于capabilities动态探测调试器支持等级;SupportsDAPv3()通过initialize响应中的supportsRunInTerminalRequest等v3专属能力字段判定,避免硬编码版本号。

Fallback触发条件与行为

  • 当dlv-dap未启用--api-version=3启动时,自动降级至DAPv2桥接模式
  • 所有v3特有请求(如attachToProcessterminateThreads)被映射为等效v2序列操作

协议兼容性对照表

DAPv3 请求 v2 fallback 行为
setExceptionBreakpoints 保留,v2已支持
readMemory 拒绝并返回NotSupportedError
disassemble 转换为evaluate + unsafe内存读取模拟
graph TD
    A[Client sends DAPv3 request] --> B{dlv-dap --api-version=3?}
    B -->|Yes| C[Native DAPv3 handler]
    B -->|No| D[Adapt to DAPv2 semantics]
    D --> E[Filter/rewrite unsupported fields]
    E --> F[Forward to legacy adapter]

第四章:生产级Go调试环境重建方案与灰度验证

4.1 基于vscode-dev-containers的DAPv3兼容调试沙箱快速部署

VS Code Dev Containers 已原生支持 DAPv3(Debug Adapter Protocol v3),为现代语言服务器提供更稳定的断点管理与异步堆栈追踪能力。

核心配置要点

  • devcontainer.json 中需声明 "debugConfiguration": { "protocolVersion": "3" }
  • 基础镜像须预装 DAPv3 兼容的调试适配器(如 ms-python/debugpy:latest

示例 devcontainer.json 片段

{
  "image": "mcr.microsoft.com/devcontainers/python:3.12",
  "customizations": {
    "vscode": {
      "extensions": ["ms-python.python"],
      "settings": {
        "python.defaultInterpreterPath": "/usr/local/bin/python"
      }
    }
  },
  "features": {
    "ghcr.io/devcontainers/features/python:1": {}
  }
}

该配置启用容器化 Python 环境,并通过 features 自动注入 DAPv3 就绪的 debugpymcr.microsoft.com/devcontainers/python:3.12 镜像已预置 debugpy ≥1.8,原生支持 DAPv3 的 setExceptionBreakpoints 扩展协议。

调试能力 DAPv2 支持 DAPv3 支持
异步调用堆栈
条件断点持久化 ⚠️(依赖客户端) ✅(服务端托管)
graph TD
  A[VS Code 启动调试] --> B[Dev Container 加载]
  B --> C[DAPv3 Adapter 初始化]
  C --> D[建立双向流式会话]
  D --> E[支持并发线程断点同步]

4.2 launch.json多环境模板:支持legacy DAPv2回退与DAPv3强制启用双模式

现代调试器需兼容新旧协议演进。VS Code 的 launch.json 可通过条件化配置实现 DAP 协议版本智能切换。

双模式核心策略

  • DAPv3 强制启用:设置 "debugProtocolVersion": "3.0" + "useV3Protocol": true
  • DAPv2 回退路径:依赖 "legacy": true 标志触发兼容逻辑

典型配置片段

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug (DAPv3)",
      "type": "pwa-node",
      "request": "launch",
      "runtimeExecutable": "node",
      "protocolVersion": "3.0",
      "useV3Protocol": true,
      "env": { "DAP_FORCE_V3": "1" }
    }
  ]
}

protocolVersion 声明期望协议语义;useV3Protocol 是调试适配器层开关;DAP_FORCE_V3 环境变量供 runtime 动态决策,避免硬编码回退逻辑。

协议协商流程

graph TD
  A[启动调试] --> B{DAP_FORCE_V3=1?}
  B -->|是| C[初始化 DAPv3 Session]
  B -->|否| D[尝试 DAPv3 handshake]
  D --> E{失败?}
  E -->|是| F[降级为 DAPv2 Session]
模式 触发条件 兼容性
DAPv3 强制 useV3Protocol: true Node.js ≥18.17
DAPv2 回退 handshake timeout/fail Node.js ≥14

4.3 断点注册全流程追踪:从VSCode UI点击→Extension转发→Delve Server响应的端到端链路注入调试

当用户在 VSCode 编辑器中单击行号左侧设置断点,UI 层触发 vscode.debug.addBreakpoints 事件,经由 go extensionDebugSession 实例封装为 DAP 协议 setBreakpoints 请求:

{
  "command": "setBreakpoints",
  "arguments": {
    "source": { "name": "main.go", "path": "/src/main.go" },
    "breakpoints": [{ "line": 15, "column": 5 }],
    "lines": [15]
  }
}

此请求由 vscode-go 扩展通过 debugAdapterDescriptorFactory 转发至本地 Delve Server(dlv dap --listen=:2345),经 DAP-to-Delve 协议桥接后,调用 rpc2.CreateBreakpoint 方法。Delve 解析源码位置,执行 proc.FindFileLine 定位机器码地址,并向目标进程注入 int3 指令。

关键链路阶段

  • UI 层:BreakpointManager.setBreakpoints() 触发事件广播
  • Extension 层:GoDebugSession.setBreakpoints() 序列化并发送 DAP 请求
  • Delve Server 层:handleSetBreakpointscreateBreakpointInTargetwriteBreakpointInstruction

协议转换映射表

DAP 字段 Delve 内部参数 说明
arguments.line Loc.Line 源码行号(1-indexed)
source.path Proc.BinInfo.Source 调试符号路径解析依据
verified Breakpoint.Verified proc.ValidBreakpoint 异步校验
graph TD
  A[VSCode UI 点击行号] --> B[触发 setBreakpoints DAP 请求]
  B --> C[vscode-go Extension 封装并转发]
  C --> D[Delve DAP Server 接收并解析]
  D --> E[proc.FindFileLine → 生成硬件/软件断点]
  E --> F[ptrace 注入 int3 / patch 指令]

4.4 CI/CD集成调试验证:GitHub Actions中复现并固化DAPv3断点注册成功检查点

为在CI流水线中可靠捕获DAPv3调试会话初始化状态,需精准识别Target::breakpoint_register()返回值与DAP响应包中的STATUS = 0x00字段。

关键断点注册校验逻辑

- name: Verify DAPv3 breakpoint registration
  run: |
    # 发送DAP_CMD_WRITE_DEBUG_REG(BPU_CTRL)并解析响应
    dap_response=$(dap-cli --cmd write-reg --addr 0x00000010 --data 0x00000001 --dap-version 3)
    status_byte=$(echo "$dap_response" | tail -n1 | cut -d' ' -f2)  # 提取STATUS字节
    if [ "$status_byte" != "00" ]; then
      echo "❌ DAPv3 breakpoint registration failed: STATUS=$status_byte"
      exit 1
    fi
    echo "✅ Breakpoint controller registered successfully under DAPv3"

该脚本调用自研dap-cli工具模拟调试器行为;--dap-version 3强制启用DAPv3协议栈,确保STATUS字段语义符合ARM Debug Interface v3.0规范;tail -n1规避日志干扰,精准定位响应末行的状态码。

GitHub Actions环境适配要点

  • 使用ubuntu-22.04基础镜像(内核≥5.15,支持CONFIG_DEBUG_FS=y
  • 预装openocd@v0.12.0+(含DAPv3补丁)
  • 通过secrets.DAP_HW_TOKEN注入硬件调试密钥
检查项 期望值 CI失败阈值
DAP_ACK响应周期 ≤ 8ms >12ms
STATUS字段稳定性 连续10次=0x00 ≥1次非零
graph TD
  A[CI触发] --> B[启动OpenOCD+DAPv3 server]
  B --> C[执行dap-cli写断点控制寄存器]
  C --> D{解析STATUS==0x00?}
  D -->|Yes| E[标记check-dapv3-breakpoint@success]
  D -->|No| F[上传dap-trace.bin供离线分析]

第五章:未来调试生态演进与开发者应对策略

调试工具链的云原生重构

现代微服务架构下,本地调试已无法覆盖真实运行态问题。以某电商大促期间订单超时为例,开发团队通过将 OpenTelemetry Collector 部署在 Kubernetes 集群中,结合 Jaeger UI 实时追踪跨 17 个服务的请求链路,定位到 Redis 连接池耗尽源于支付网关未正确复用连接。该案例中,调试行为从 IDE 单点断点迁移至可观测性平台的分布式上下文关联分析。

AI 辅助调试的工程化落地

GitHub Copilot X 与 VS Code 的 Debugger for Chrome 插件深度集成后,可基于异常堆栈自动生成修复建议并高亮可疑代码段。某前端团队在排查 React 18 并发模式下的状态不一致问题时,AI 工具识别出 useTransition 中未包裹 setState 调用,并给出符合 Concurrent Rendering 规范的重构代码片段:

// 原始错误代码
startTransition(() => {
  setCount(count + 1); // ❌ 非异步更新,触发警告
});

// AI 推荐修正
startTransition(() => {
  setTimeout(() => setCount(c => c + 1), 0); // ✅ 确保延迟调度
});

多模态调试界面的协同实践

某车联网企业构建了融合日志、指标、链路、设备传感器数据的统一调试看板。当车载终端上报“CAN 总线通信中断”告警时,系统自动联动展示:

  • 实时 CPU 温度曲线(来自 Prometheus)
  • 最近 3 分钟 CAN 驱动模块 dmesg 日志(按时间戳对齐)
  • 对应时间段内蓝牙模块的功耗突增事件(IoT 设备遥测数据)
调试维度 数据源 响应延迟 关联精度
分布式链路追踪 Jaeger + OTLP 毫秒级时间戳
内存快照分析 eBPF + BCC 工具集 实时 函数级符号
硬件信号捕获 USB 逻辑分析仪直连 50μs 纳秒级采样

开发者技能树的动态演进

某金融科技公司要求 SRE 团队每季度完成两项强制性调试能力认证:一是使用 eBPF 编写内核态探针捕获 TCP 重传异常;二是基于 Grafana Loki 构建日志模式聚类看板,识别高频错误组合(如 NullPointerExceptionConnectionTimeoutException 在同一 Pod 的共现率 >87%)。2024 年 Q2 的实战考核中,63% 的故障平均定位时间从 42 分钟压缩至 9 分钟。

跨信任域调试的安全边界设计

在政务云多租户环境中,某省大数据中心采用 SPIFFE 标准为调试代理颁发短时效身份证书,确保调试流量仅能访问授权命名空间内的 Pod。当审计发现某调试会话试图读取 /proc/1/environ 时,SPIRE Agent 自动终止连接并触发 SOC 平台告警——该机制已在 3 次红蓝对抗演练中拦截越权调试尝试。

开源调试工具的定制化增强路径

Apache SkyWalking Rover 项目被某 CDN 厂商深度改造:新增 WebAssembly 沙箱模块,允许运维人员上传 Rust 编写的轻量分析脚本(如 TLS 握手失败根因分类器),脚本在隔离环境执行且内存占用严格限制在 4MB 内。上线后,HTTPS 错误诊断效率提升 5.8 倍,且杜绝了脚本注入风险。

记录 Go 学习与使用中的点滴,温故而知新。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注