Posted in

【Go测试驱动开发TDD专项】:唯一支持`go test -fuzz`自动生成功能的习题网站,Fuzzing题库已更新

第一章:Go测试驱动开发TDD专项概览

Go语言原生对测试的深度支持使其成为实践测试驱动开发(TDD)的理想选择。go test 工具链、testing 包的简洁接口、内置覆盖率分析及快速反馈循环,共同构成了轻量而严谨的TDD基础设施。与动态语言不同,Go的静态类型系统在TDD流程中不仅不构成阻碍,反而在编写测试桩和重构阶段显著降低意外行为风险。

核心工作流原则

TDD在Go中严格遵循“红—绿—重构”三步闭环:

  • :先编写一个失败的测试(编译通过但断言失败);
  • 绿:以最小可行代码使测试通过(不追求优雅,仅满足当前用例);
  • 重构:在全部测试保持绿色的前提下优化结构、消除重复、提升可读性。

该循环以单个细粒度行为为单位推进,例如为 CalculateTotal() 函数新增折扣逻辑时,应先写 TestCalculateTotal_WithDiscount,再实现分支逻辑,而非一次性补全所有场景。

快速启动示例

创建 calculator.go 与对应测试文件 calculator_test.go

// calculator.go
package calculator

func CalculateTotal(items []float64) float64 {
    return 0 // 初始占位实现,确保编译通过
}
// calculator_test.go
package calculator

import "testing"

func TestCalculateTotal_EmptySlice(t *testing.T) {
    got := CalculateTotal([]float64{})
    want := 0.0
    if got != want {
        t.Errorf("CalculateTotal([]) = %v, want %v", got, want)
    }
}

执行 go test -v 将立即触发“红”状态(因返回值恒为0,但尚未处理非空输入)。此时仅需补充逻辑即可进入“绿”阶段。

Go TDD关键支撑能力

能力 命令/特性 说明
测试运行与调试 go test -v -run=TestName 精确执行单个测试,加速反馈
覆盖率可视化 go test -coverprofile=c.out && go tool cover -html=c.out 生成交互式HTML报告,定位未覆盖路径
模拟依赖(无第三方库) 使用接口+内存实现(如 io.Reader 替换文件读取) 遵循Go惯用法,避免过度抽象

TDD不是测试技术,而是设计方法——每一次测试编写,都在定义接口契约与边界行为。在Go中,它自然引导开发者拥抱小接口、明确职责与组合优先的设计哲学。

第二章:Fuzzing自动化测试原理与go test -fuzz实战解析

2.1 Go Fuzzing引擎工作机制与覆盖率反馈模型

Go 的 go test -fuzz 引擎基于覆盖率驱动的灰盒模糊测试(Greybox Fuzzing),核心依赖运行时插桩获取函数入口、基本块跳转与条件分支等覆盖率信号。

覆盖率信号采集机制

编译器在 -gcflags=-d=ssa/insert_probes 下为每个基本块插入计数器,fuzzer 运行时通过 runtime.fuzzCall 注册回调,将增量覆盖写入共享内存环形缓冲区。

反馈闭环流程

// fuzz.go 中关键反馈钩子(简化示意)
func recordCoverage(pc uintptr) {
    idx := (atomic.AddUint64(&covIndex, 1) - 1) % uint64(len(covBuf))
    covBuf[idx] = pc // 存储程序计数器地址
}

该函数被 SSA 插桩自动调用;pc 指向唯一基本块起始地址,covBuf 为无锁环形缓冲区,避免竞争开销。

组件 作用 更新频率
插桩计数器 标记基本块执行次数 每次进入块时原子递增
环形缓冲区 暂存新覆盖路径 每轮 fuzz iteration 清空
语料裁剪器 基于覆盖增量保留最小有效输入 每发现新路径触发

graph TD A[种子输入] –> B[执行并插桩采集PC] B –> C{是否发现新基本块?} C –>|是| D[保存输入至corpus] C –>|否| E[丢弃/变异] D –> F[生成新变异体] F –> A

2.2 从零构建可Fuzz的接口契约与种子语料设计

接口契约建模:OpenAPI 3.0 基础骨架

使用 OpenAPI 3.0 显式定义 HTTP 方法、路径参数、请求体结构及响应状态码,是 Fuzzing 前提。契约需禁用模糊字段(如 x-fuzz-ignore: true)并标注变异敏感域(如 x-fuzz-strategy: "sql-inject")。

种子语料构造原则

  • 优先覆盖边界值:空字符串、超长字符串(1024+ 字符)、整数极值(±2³¹)
  • 混合合法/非法组合:如正确 Content-Type + 错误 JSON 结构
  • 每个种子附带 fuzz_target 元标签,指明待测字段路径(例:#/paths//api/v1/users/post/requestBody/content/application/json/schema/properties/email

示例:用户注册接口种子生成器

# seed_gen.py:基于 OpenAPI schema 动态生成初始语料
from openapi_schema_validator import validate
import json

schema = {
  "type": "object",
  "properties": {
    "email": {"type": "string", "format": "email"},
    "age": {"type": "integer", "minimum": 0, "maximum": 150}
  }
}

# 生成3类种子:正常、边界、畸形
seeds = [
  {"email": "test@example.com", "age": 25},              # 正常
  {"email": "a" * 256 + "@x.y", "age": -1},             # 边界+越界
  {"email": "admin' OR '1'='1", "age": "NaN"}           # 注入候选
]

逻辑分析:该脚本不依赖人工枚举,而是依据 OpenAPI schematype/format/minimum 等关键字推导变异维度;"NaN" 作为非整数字符串触发类型解析异常,暴露反序列化漏洞面;所有种子均通过 json.dumps() 序列化后送入 Fuzzer,确保 UTF-8 编码一致性。

种子质量评估维度

维度 合格阈值 检测方式
字段覆盖率 ≥90% schema 路径 静态 JSONPath 扫描
变异敏感度 ≥3 类策略命中 x-fuzz-strategy 匹配
语法有效性 100% JSON 可解析 json.loads() 校验
graph TD
  A[OpenAPI 3.0 文档] --> B[提取 requestSchema]
  B --> C[生成基础种子集]
  C --> D[注入策略标注]
  D --> E[语法/结构校验]
  E --> F[输出 seed_corpus.json]

2.3 模糊测试失败用例的最小化与可复现性保障

模糊测试产出的崩溃用例常包含大量冗余字节,直接用于调试效率低下。最小化(minimization)旨在剥离非触发路径必需输入,保留最简可复现输入。

最小化核心策略

  • 基于 delta debugging 算法迭代删减、替换、重排字节块
  • 严格校验每次修改后是否仍触发相同异常信号与栈帧哈希
  • 支持多目标约束:如保持 HTTP 请求头完整性、JSON 结构有效性

可复现性保障机制

def is_reproducible(crash_input: bytes, target_bin: str, timeout=5) -> bool:
    # 执行3次独立进程,要求全部触发相同信号(如 SIGSEGV)与相似栈顶符号
    signals = []
    for _ in range(3):
        proc = subprocess.run([target_bin], input=crash_input, 
                              timeout=timeout, capture_output=True)
        signals.append(proc.returncode & 0x7F)  # 提取终止信号编号
    return len(set(signals)) == 1 and signals[0] != 0

该函数通过三次隔离执行验证行为一致性;& 0x7F 提取原始信号值(POSIX 标准),排除退出码混淆;超时防止挂起阻塞 CI 流水线。

最小化效果对比

指标 原始崩溃用例 最小化后
字节数 12,483 87
复现成功率 92% 100%
平均触发延迟 184ms 23ms
graph TD
    A[原始崩溃输入] --> B{delta-debugging 迭代}
    B --> C[删除非关键字段]
    B --> D[收缩边界对齐块]
    C --> E[验证崩溃一致性]
    D --> E
    E -->|通过| F[输出最小输入]
    E -->|失败| B

2.4 基于-fuzztime与-fuzzminimize的性能调优策略

-fuzztime-fuzzminimize 是 AFL++ 中协同优化模糊测试效率与深度的关键参数组合。

参数协同机制

当启用 -fuzztime 300(单位:秒)时,模糊器在限定时间内执行完整 fuzz cycle;配合 -fuzzminimize 可自动精简输入语料库,剔除冗余或低覆盖率种子。

afl-fuzz -i in/ -o out/ -f fuzz_input -- ./target @@ \
  -fuzztime 300 -fuzzminimize

逻辑说明:-fuzztime 强制单轮 fuzz 限时终止,避免长尾低效运行;-fuzzminimize 在每次 -fuzztime 周期结束前触发语料裁剪,仅保留覆盖增量最大的最小化输入。

效能对比(典型场景)

配置 平均路径发现率 内存占用 种子数量(5min后)
-fuzztime 300 127/s 1.8 GB 4,219
+ -fuzzminimize 163/s 1.1 GB 1,306

执行流程示意

graph TD
  A[启动模糊测试] --> B{是否达-fuzztime?}
  B -->|否| C[常规变异 & 执行]
  B -->|是| D[触发-fuzzminimize]
  D --> E[基于覆盖率重排序种子]
  E --> F[保留top-K最小化输入]
  F --> G[重置计时器,进入下一轮]

2.5 Fuzz目标函数的边界约束建模与panic注入验证

Fuzz目标函数需精确刻画输入域边界,避免无效变异浪费资源。核心在于将业务语义约束(如时间戳范围、枚举值集合、长度上下界)转化为可求解的SMT表达式。

边界约束建模示例

// 假设目标函数处理HTTP请求头中的Content-Length字段
fn parse_content_length(input: &str) -> Result<u32, ParseError> {
    let n = input.parse::<u32>()?;                 // ① 基础解析
    if n > 10_000_000 { return Err(ParseError::TooLarge); } // ② 上界硬约束(10MB)
    Ok(n)
}

逻辑分析:input.parse::<u32>() 触发整数溢出路径;n > 10_000_000 是关键分支条件,fuzzer需生成满足 0 ≤ n ≤ 10_000_000 的有效输入以覆盖正常路径,同时突破该条件触发panic。

panic注入验证策略

  • 构造超长数字字符串(如"10000001")触发TooLarge错误分支
  • 输入非数字字符(如"abc")触发parse()底层panic(若未被?捕获)
  • 使用cargo-fuzz配合libfuzzer启用-detect_leaks=0 -handle_segv=1捕获非法内存访问
约束类型 示例值 对应panic场景
数值上界 "10000001" 显式Err(TooLarge)
类型非法 "9223372036854775808" u32::parse溢出panic
长度越界 "1" + 1GB空格 &str切片越界(若后续索引)

graph TD A[原始输入] –> B{parse::} B –>|成功| C[检查n > 10M] B –>|失败| D[ParseIntError → panic if unhandled] C –>|true| E[return Err TooLarge] C –>|false| F[return Ok n]

第三章:习题网站Fuzzing题库架构与题目设计范式

3.1 题目难度分级体系:从基础断言到内存安全漏洞识别

难度分级并非主观判断,而是基于可量化的检测维度构建的渐进式标尺。

核心分级维度

  • 语义复杂度:断言数量、嵌套深度、约束条件耦合性
  • 执行路径数:由分支与循环引发的可达路径爆炸程度
  • 内存敏感性:是否涉及指针解引用、堆分配、越界访问等

典型示例对比

难度等级 示例特征 检测耗时(平均)
L1(基础) assert(x > 0)
L3(进阶) assert(p && p[off] == '\0') ~120ms
L5(高危) 堆块重用后二次释放(UAF) > 2s(需符号执行)
// L4 级别:潜在 Use-After-Free 场景
char *buf = malloc(64);
free(buf);
if (rand() % 2) return; // 控制流不可预测
assert(buf[0] == 0); // ❗未定义行为:use-after-free

该断言在静态分析中易被忽略——buf 虽已释放,但指针值未置 NULL;动态检测需追踪内存生命周期与别名关系。assert 本身不触发崩溃,却暴露了深层内存状态不一致。

graph TD
    A[基础断言] --> B[边界条件验证]
    B --> C[指针有效性检查]
    C --> D[堆生命周期建模]
    D --> E[并发内存竞态识别]

3.2 自动化判题器对Fuzz崩溃报告的结构化解析逻辑

自动化判题器需从原始崩溃日志中精准提取可判定字段,核心在于模式识别与上下文消歧。

解析阶段划分

  • 预处理:清洗 ANSI 转义符、截断冗余堆栈(保留前8帧)
  • 关键字段抽取signalfault_addrcrash_funcstack_hash
  • 语义归一化:将 SIGSEGV (address access error) 映射为 SEGV_MAPERR

崩溃类型映射表

原始信号描述 标准化类型 判定依据
SIGSEGV on unknown address SEGV_ACCERR fault_addr == 0x0 或未对齐
SIGABRT (assert failed) ASSERT_FAIL assertion failed 关键词
def parse_crash_line(line: str) -> dict:
    # 匹配形如 "Program received signal SIGSEGV, Segmentation fault."
    match = re.search(r"signal (\w+), ([^.\n]+)\.", line)
    if match:
        return {"signal": match[1], "desc": match[2].strip()}
    return {}

该函数仅捕获首行信号声明,避免被后续 #0 0x... in foo() 行干扰;返回字典为后续规则引擎提供原子输入。

graph TD
    A[原始崩溃日志] --> B{是否含 core dump?}
    B -->|是| C[解析elf + ptrace reg dump]
    B -->|否| D[正则提取 signal/fault_addr]
    C & D --> E[生成 stack_hash + crash_signature]

3.3 题目元数据规范:fuzz tag、build constraints与依赖隔离声明

在 Go 语言 fuzzing 场景中,元数据需精准控制测试边界与环境适配。

fuzz 标签语义

//go:fuzz 注释必须紧邻函数签名前,且仅作用于 func([]byte) int 类型函数:

//go:fuzz
func FuzzParseJSON(f *testing.F) {
    f.Fuzz(func(t *testing.T, data []byte) {
        // ...
    })
}

//go:fuzz 是编译器识别 fuzz 入口的硬性标记;缺失则 go test -fuzz 忽略该函数。

构建约束与依赖隔离

约束类型 示例 作用
//go:build !race 禁用竞态检测 避免 fuzz runtime 冲突
//go:build go1.21 版本限定 保障 F.Fuzz API 可用

依赖隔离策略

  • 使用 //go:build ignore 排除非 fuzz 模块
  • 通过 replace ./internal/fuzz => ./fuzzgo.mod 中隔离测试依赖
graph TD
    A[源码文件] --> B{含 //go:fuzz?}
    B -->|是| C[启用 fuzz 构建]
    B -->|否| D[普通测试构建]
    C --> E[应用 build constraints]
    E --> F[过滤不兼容依赖]

第四章:TDD工作流在Fuzzing习题中的闭环实践

4.1 红-绿-重构循环在模糊测试驱动编码中的映射实现

模糊测试驱动编码(Fuzz-Driven Development, FDD)将传统TDD的红-绿-重构循环重新语义化为:

  • 红阶段:构造最小崩溃输入,触发目标函数未处理的边界行为
  • 绿阶段:添加防御性检查或修复逻辑,使模糊器不再报告该崩溃
  • 重构阶段:提升输入解析鲁棒性、抽象异常路径、增强覆盖率反馈闭环

模糊器与单元测试的协同调度

# fuzz_driver.py:嵌入式红-绿钩子
def run_fuzz_iteration(fuzzer, target_func):
    crash_input = fuzzer.generate()           # 红:生成潜在崩溃输入
    try:
        target_func(crash_input)              # 执行被测函数
        return "green"                        # 无崩溃 → 当前实现通过该输入
    except (ValueError, IndexError):
        patch_defense(crash_input)            # 绿:动态注入防护逻辑
        return "patched"

crash_input 是由AFL++变异引擎生成的原始字节流;patch_defense() 触发源码级插桩,如自动插入 if len(buf) < 4: raise ValueError

循环状态迁移表

阶段 触发条件 输出产物
libFuzzer报告SIGSEGV 崩溃最小化样本(.crash)
绿 样本不再触发崩溃 补丁diff + 新断言
重构 连续10轮无新崩溃路径 提取通用校验模块
graph TD
    A[红:运行模糊器] -->|发现崩溃| B[绿:定位缺陷+插入防护]
    B -->|验证通过| C[重构:泛化校验逻辑]
    C -->|提升覆盖率| A

4.2 基于习题模板的test-first fuzz target生成器使用指南

该工具将编程习题(如LeetCode风格函数签名+约束说明)自动转化为可编译、可模糊测试的C/C++ fuzz target,遵循 test-first 原则——先定义正确性契约,再生成桩代码与变异入口。

核心工作流

  • 解析YAML格式习题模板(含函数原型、输入约束、预期行为)
  • 注入LLVMFuzzerTestOneInput骨架与类型安全解包逻辑
  • 自动生成边界检查断言与sanitizer友好的输入验证

示例:生成两数之和fuzz target

// fuzz_target.c —— 由模板自动生成
#include <stddef.h>
#include "fuzzer_utils.h"
int two_sum(int* nums, int nums_size, int target, int* out_idx);
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
  if (size < 12) return 0; // 至少需:2×int32 + 1×int32
  int nums[100], target, out_idx[2];
  size_t offset = 0;
  safe_read_int32(data, &offset, size, &nums[0]); // 带越界防护的解包
  safe_read_int32(data, &offset, size, &nums[1]);
  safe_read_int32(data, &offset, size, &target);
  two_sum(nums, 2, target, out_idx); // 被测函数调用
  return 0;
}

逻辑分析safe_read_int32确保从data中按序提取32位整数,offset跟踪已读字节;若size不足则提前返回,避免OOM或UB。参数nums_size=2由模板静态推导,非运行时猜测。

支持的模板字段对照表

模板字段 生成作用 是否必需
function_name 生成调用语句与符号引用
input_constraints 插入assert()__builtin_trap()防护
fuzz_buffer_size 控制LLVMFuzzerTestOneInput最小输入长度
graph TD
  A[习题YAML模板] --> B[语法解析与约束校验]
  B --> C[类型感知解包代码生成]
  C --> D[Sanitizer-ready fuzz入口]
  D --> E[编译为libFuzzer兼容目标]

4.3 CI/CD中集成-fuzz模式的持续模糊测试流水线配置

在现代CI/CD流水线中,将-fuzz模式嵌入构建阶段可实现漏洞的左移发现。核心在于使模糊测试具备可重复、可度量、可中断恢复的能力。

流水线关键阶段编排

# .gitlab-ci.yml 片段(支持 fuzz 模式自动触发)
fuzz-unit:
  stage: test
  image: llvm:18
  script:
    - clang++ -fsanitize=fuzzer,address -g src/parser.cpp -o parser_fuzzer
    - timeout 300 ./parser_fuzzer -max_total_time=180 -print_final_stats=1

逻辑分析:使用LLVM Fuzzer编译器插件生成fuzz target;-max_total_time=180确保单次作业不阻塞CI队列;timeout 300为兜底超时保护。-print_final_stats=1输出崩溃数、路径覆盖等关键指标供后续归档。

模糊测试结果归因能力对比

能力项 传统单元测试 -fuzz 模式CI集成
缺陷发现时效 编码后手动触发 提交即触发,
覆盖深度 静态路径覆盖 动态路径探索+覆盖率反馈
失败可复现性 弱(依赖输入) 强(自动生成并保存crash corpus)
graph TD
  A[代码提交] --> B{CI触发}
  B --> C[编译含Sanitizer的fuzz target]
  C --> D[启动带超时与统计的fuzz进程]
  D --> E{发现crash?}
  E -->|是| F[自动存档corpus+堆栈]
  E -->|否| G[上报覆盖率增量]

4.4 学员提交代码的自动Fuzz回归比对与diff敏感度分析

核心流程概览

graph TD
    A[学员提交代码] --> B[触发CI流水线]
    B --> C[执行Fuzz测试套件]
    C --> D[提取覆盖率与崩溃路径]
    D --> E[与基线版本diff比对]
    E --> F[量化语义差异敏感度]

差异敏感度建模

采用三维度加权评估:

  • 语法层:AST节点编辑距离(Levenshtein on serialized AST)
  • 行为层:Fuzz输入触发的路径分支偏移率
  • 稳定性层:相同种子下crash复现概率变化

关键比对逻辑示例

def compute_diff_sensitivity(old_trace, new_trace):
    # old_trace, new_trace: list of (func_name, line_no, hit_count)
    return abs(len(set(old_trace)) - len(set(new_trace))) / max(1, len(set(old_trace)))

该函数计算执行轨迹集合的基数差,归一化为相对扰动值;分母加1防除零,适用于轻量级实时反馈场景。

指标 基线阈值 风险等级 触发动作
轨迹差异率 > 0.35 0.35 暂停合并,人工复核
Crash复现率下降 >40% 40% 启动深度Fuzz重测

第五章:未来演进与生态协同展望

多模态AI驱动的运维闭环实践

某头部云服务商在2023年Q4上线“智巡Ops平台”,将LLM推理能力嵌入现有Zabbix+Prometheus+Grafana技术栈。当GPU显存使用率连续5分钟超92%时,系统自动调用微调后的Llama-3-8B模型解析Kubernetes事件日志、NVML指标及历史告警文本,生成根因假设(如“CUDA内存泄漏由PyTorch DataLoader persistent_workers=True引发”),并推送可执行修复脚本至Ansible Tower。该流程将平均故障定位时间(MTTD)从17.3分钟压缩至2.1分钟,误报率低于4.7%。

开源协议兼容性治理矩阵

组件类型 Apache 2.0兼容 GPL-3.0限制场景 实际落地约束
模型权重文件 ✅ 允许商用 ❌ 禁止闭源分发 Hugging Face Hub强制标注许可证字段
微服务SDK ✅ 可动态链接 ⚠️ 静态链接需开源衍生代码 Kubernetes Operator SDK采用双许可
硬件驱动固件 ❌ 不适用 ✅ 必须提供源码及构建工具 NVIDIA GPU Operator v24.3.0新增BSD兼容层

边缘-云协同推理架构演进

某智能工厂部署的视觉质检系统采用三级协同策略:

  • 边缘端(Jetson AGX Orin)运行量化YOLOv8n模型,实时过滤92%无缺陷图像;
  • 区域边缘节点(NVIDIA A10服务器)对可疑样本执行高精度YOLOv8x+CLIP多模态重检;
  • 中心云(Azure NC24ads_A100_v4集群)聚合全厂数据训练联邦学习模型,每72小时向边缘节点下发增量权重更新包(平均体积

生态工具链深度集成验证

# 验证OpenTelemetry Collector与LangChain Tracer兼容性
otelcol --config ./otel-config.yaml \
  --set=exporters.otlp.endpoint=collector.langchain.internal:4317 \
  --set=processors.batch.timeout=5s
# 启动后自动注入span_id至LangChain回调钩子,实现LLM调用链路与传统微服务trace无缝关联

跨云资源编排标准化路径

graph LR
    A[用户提交Kueue Workload] --> B{调度器识别资源类型}
    B -->|GPU任务| C[匹配NVIDIA Device Plugin标签]
    B -->|大模型推理| D[绑定vLLM CustomResource]
    C --> E[调度至AWS p4d.24xlarge集群]
    D --> F[调度至Azure ND96amsr_A100_v4集群]
    E & F --> G[统一通过Kubernetes Gateway API暴露/llm/v1/chat/completions]

硬件抽象层接口演进趋势

Linux内核6.8正式合并drivers/accel/npu/ascend子系统,使华为昇腾910B芯片获得原生支持。某金融风控平台基于此重构特征工程流水线:原始TensorFlow模型经Ascend CANN 8.0编译后,在昇腾集群上实现单卡吞吐量12.8万次/秒(较同规格A100提升1.7倍),且通过/sys/class/accel/ascend*/power_limit接口实现毫秒级功耗动态调控——在交易高峰时段锁定300W,盘后批处理时自动升频至350W。

开源模型社区协作机制创新

Hugging Face Transformers v4.42引入“Delta Weights”机制,允许贡献者仅提交参数差分而非完整模型。当Meta发布Llama-3-70B时,国内团队在48小时内完成中文增强版llama-3-zh-delta,体积仅2.1GB(占原模型3.7%),经LoRA微调后在CMRC2018榜单超越基线1.9个点。该delta包已通过Hugging Face Hub的git lfs自动挂载至下游企业私有模型仓库。

安全合规自动化验证流水线

某政务云平台将等保2.0三级要求拆解为137项机器可读规则,集成至CI/CD流程:

  • terraform-validator扫描IaC模板中是否存在明文密钥;
  • trivy检测容器镜像CVE-2023-45803漏洞;
  • model-card-toolkit自动生成符合GB/T 42371-2023标准的AI模型安全声明书。
    每次模型上线前触发全量检查,平均耗时8分23秒,拦截高危配置变更12类。

用实验精神探索 Go 语言边界,分享压测与优化心得。

发表回复

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