第一章:Go语言BCC内核探针开发认证题库概览
BCC(BPF Compiler Collection)是Linux内核可观测性领域的核心工具集,而Go语言通过github.com/iovisor/gobpf/bcc和更现代的github.com/aquasecurity/libbpfgo生态,为BCC风格的eBPF程序开发提供了类型安全、内存可控且易于集成的实现路径。本题库聚焦于真实工程场景中的内核探针开发能力认证,覆盖kprobe、tracepoint、uprobe及perf event等主流探测机制,并强调Go与eBPF字节码协同生命周期管理、事件环形缓冲区(ring buffer)解析、以及用户态结构体与内核态数据布局的ABI对齐实践。
核心能力维度
- 内核函数动态插桩:在不修改源码前提下捕获
tcp_connect、do_sys_open等关键路径参数 - 用户态符号追踪:基于
libbcc或libbpfgo加载ELF并定位Go二进制中runtime.mallocgc调用点 - 低开销事件聚合:使用
BPF_MAP_TYPE_PERF_EVENT_ARRAY接收内核事件,配合Go端perf.NewReader()持续消费 - 安全边界控制:严格限制BPF程序指令数(≤1M)、禁止未初始化内存访问,并通过
Verifier日志反向调试
快速验证环境搭建
以下命令可一键部署最小可行开发环境(Ubuntu 22.04+):
# 安装内核头文件与BPF工具链
sudo apt update && sudo apt install -y linux-headers-$(uname -r) bpfcc-tools libbpf-dev clang llvm
# 初始化Go模块并引入libbpfgo(推荐v1.1.0+)
go mod init bcc-go-lab
go get github.com/aquasecurity/libbpfgo@v1.1.0
# 验证BPF运行时支持(需root权限)
sudo cat /proc/sys/net/core/bpf_jit_enable # 应输出1
典型题型示例结构
| 题型类别 | 考察重点 | 输出要求 |
|---|---|---|
| 探针注入类 | kprobe附加/分离时机与错误处理 | Go代码+sudo ./probe执行日志 |
| 数据解析类 | struct data_t字段对齐与binary.Read转换 |
解析后JSON格式事件流 |
| 性能约束类 | BPF辅助函数调用合规性(如禁用bpf_trace_printk) |
bpftool prog dump xlated汇编片段分析 |
所有题目均提供标准输入/输出契约,确保结果可自动化判分。
第二章:BCC基础架构与Go语言绑定原理
2.1 BCC核心组件与eBPF程序生命周期解析
BCC(BPF Compiler Collection)通过封装底层复杂性,使开发者能以 Python/C++ 快速构建可观测性工具。其核心由三部分构成:
- 前端接口:
BPF类提供load_kprobe、attach_kprobe等方法,屏蔽 eBPF 验证器与加载细节 - 中间编译层:调用
clang + llc将 C 源码编译为 eBPF 字节码,并自动注入辅助函数(如bpf_trace_printk) - 内核交互模块:通过
bpf()系统调用完成程序加载、映射创建与事件挂载
from bcc import BPF
# 加载并附着到 do_sys_open 函数入口
bpf = BPF(text="""
#include <uapi/linux/ptrace.h>
int trace_entry(struct pt_regs *ctx) {
bpf_trace_printk("open() called\\n");
return 0;
}""")
bpf.attach_kprobe(event="do_sys_open", fn_name="trace_entry")
该代码中
text参数为内联 eBPF C 代码;attach_kprobe自动解析符号地址、注册 kprobe handler,并在内核态触发时执行字节码。bpf_trace_printk输出至/sys/kernel/debug/tracing/trace_pipe,是调试阶段的轻量日志通道。
eBPF 程序典型生命周期
graph TD
A[源码编写] --> B[Clang 编译为 ELF]
B --> C[libbcc 解析节区并重定位]
C --> D[调用 bpf syscall 加载验证]
D --> E[挂载到内核事件点]
E --> F[运行时受 verifier 动态检查]
F --> G[卸载或随进程退出自动清理]
| 阶段 | 关键约束 | 触发方式 |
|---|---|---|
| 加载验证 | 指令数 ≤ 1M,无循环 | bpf(BPF_PROG_LOAD) |
| 映射初始化 | BPF_MAP_TYPE_HASH 大小需预设 | bpf(BPF_MAP_CREATE) |
| 事件挂载 | kprobe/ftrace 接口需 root | bpf(BPF_PROG_ATTACH) |
2.2 libbpf-go与gobpf源码级对比与选型实践
核心架构差异
libbpf-go 直接绑定 libbpf C 库(v1.0+),通过 CGO 调用 bpf_object__open() 等原生 API;gobpf 则基于旧版 bcc 工具链,依赖内核头文件解析与运行时 BPF 字节码重写。
关键能力对照
| 维度 | libbpf-go | gobpf |
|---|---|---|
| eBPF 程序加载 | LoadPinnedObjects() 支持持久化 |
仅支持临时加载,无 pin 支持 |
| CO-RE 兼容性 | ✅ 原生支持 bpf_core_read() |
❌ 无 CO-RE 抽象层 |
| Go 模块化设计 | 按对象粒度封装(Map/Program/Link) | 单一 Module 结构体统管全部 |
// libbpf-go 加载带 CO-RE 的程序示例
obj := &ebpf.ProgramSpec{
Type: ebpf.SchedCLS,
Instructions: progInsns,
License: "GPL",
}
prog, err := ebpf.NewProgram(obj) // 自动触发 libbpf 的 relo 处理
该调用触发 libbpf 内部 bpf_object__load() 流程,自动解析 .rodata 中的 btf_ext 和 relo 段,完成字段偏移动态修正——这是 CO-RE 运行时适配的核心机制。
选型决策树
- 新项目且需跨内核版本部署 → 选
libbpf-go - 仅需在固定内核(如 4.18)上快速验证 →
gobpf仍可接受
graph TD
A[是否需 CO-RE?] -->|是| B[libbpf-go]
A -->|否| C{是否需 Map pin?}
C -->|是| B
C -->|否| D[gobpf]
2.3 Go中加载BPF对象的底层机制与错误注入模拟
Go 通过 github.com/cilium/ebpf 库封装 bpf(2) 系统调用,实现 BPF 对象(如程序、maps)的加载与验证。
加载核心流程
obj := &ebpf.ProgramSpec{
Type: ebpf.SchedCLS,
Instructions: progInsns,
License: "MIT",
}
prog, err := ebpf.NewProgram(obj) // 触发 bpf(BPF_PROG_LOAD, ...)
该调用最终执行 bpf(2) 系统调用,内核校验指令安全性、寄存器状态及辅助函数权限;失败时返回 errno 并填充 Verifier Log。
错误注入模拟方式
- 修改
ProgramSpec.Instructions插入非法跳转(如jmp +10000) - 设置
LogSize=1<<16强制返回完整 verifier 日志 - 使用
LD_PRELOAD拦截syscall.Syscall注入ENOSPC或EACCES
| 错误类型 | 触发条件 | 用户态可见信号 |
|---|---|---|
EACCES |
权限不足(非 root) | operation not permitted |
EINVAL |
指令越界或 map 类型不匹配 | invalid argument |
ENOSPC |
verifier 内存超限 | cannot allocate memory |
graph TD
A[NewProgram] --> B[bpf(BPF_PROG_LOAD)]
B --> C{内核校验}
C -->|通过| D[返回fd并映射到Prog]
C -->|失败| E[填充errno+log_buf]
E --> F[Go层err.Error()含verifier日志]
2.4 BCC Map在Go中的类型安全封装与内存布局验证
BCC(BPF Compiler Collection)的 BPF_MAP_TYPE_HASH 等内核映射需在 Go 中实现零拷贝、类型安全的访问层。
类型安全封装核心设计
使用 unsafe.Slice + reflect.TypeOf 动态校验键/值结构体字段对齐与大小,禁止非 //go:packed 结构体直接映射。
内存布局验证示例
type CounterKey struct {
Pid uint32 `bpf:"pid"`
Cpu uint32 `bpf:"cpu"`
} // 必须显式 packed://go:binary
逻辑分析:
CounterKey的Sizeof必须为 8 字节(无填充),否则内核bpf_map_lookup_elem()返回-EINVAL。bpf:tag 用于生成 eBPF 字段偏移元数据。
验证维度对比
| 维度 | 允许值 | 违规后果 |
|---|---|---|
| 字段对齐 | 1/2/4/8 | EPERM 映射加载失败 |
| 总大小 | ≤ 65535 B | E2BIG |
| 字段数量 | ≤ 64 | 编译期 panic |
graph TD
A[Go struct] --> B{是否 //go:packed?}
B -->|否| C[panic: unaligned layout]
B -->|是| D[计算 offsetof+size]
D --> E[匹配 BCC map_def.key_size]
2.5 事件驱动模型:perf event与ring buffer的Go接口实现
Linux perf_event_open 系统调用提供内核级性能事件采集能力,而 ring buffer 是其高效无锁数据通道。Go 语言需通过 syscall 和内存映射(mmap)桥接二者。
核心交互流程
// 打开 perf event 并映射 ring buffer
fd, _ := unix.PerfEventOpen(&unix.PerfEventAttr{
Type: unix.PERF_TYPE_HARDWARE,
Config: unix.PERF_COUNT_HW_INSTRUCTIONS,
Sample: 1,
}, 0, -1, -1, unix.PERF_FLAG_FD_CLOEXEC)
buf, _ := unix.Mmap(fd, 0, 4*unix.Getpagesize(),
unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
PerfEventAttr.Sample = 1启用采样模式,触发 ring buffer 写入Mmap映射大小必须为页对齐(通常PAGE_SIZE × (2^n)),首页含struct perf_event_mmap_page
ring buffer 结构解析
| 偏移量 | 字段 | 说明 |
|---|---|---|
|
data_head |
生产者指针(只读,内核更新) |
8 |
data_tail |
消费者指针(只写,用户更新) |
4096 |
data[] |
实际样本区(从 page 2 开始) |
数据同步机制
graph TD
A[内核采集指令事件] --> B[原子更新 data_head]
C[Go 程序轮询 data_head ≠ data_tail] --> D[解析 perf_event_header]
D --> E[按 type 字段分发至 handler]
- 同步依赖
data_head/data_tail的内存顺序语义(atomic.LoadUint64保障) - 样本解析需跳过
perf_event_header头部,依据size字段定位下一事件
第三章:eBPF verifier报错深度剖析
3.1 verifier拒绝逻辑的五大典型场景及Go侧复现方法
verifier在eBPF程序加载阶段执行严格校验,以下为高频拒绝场景及对应Go复现方式:
场景一:越界内存访问
// 使用 cilium/ebpf 加载含非法指针偏移的程序
prog := ebpf.ProgramSpec{
Type: ebpf.SchedCLS,
Instructions: asm.Instructions{
asm.LoadMem(asm.R1, asm.R1, -8, asm.Word), // R1未初始化,-8越界
asm.Return(),
},
}
LoadMem 指令中源寄存器 R1 未经验证即参与负偏移计算,verifier判定“uninitialized stack access”,拒绝加载。
典型拒绝原因速查表
| 场景 | 触发条件 | verifier错误码 |
|---|---|---|
| 未验证指针解引用 | ldxw [rX+off] 且 rX 非 PTR_TO_CTX/MAP_VALUE |
invalid mem access |
| 循环不可达终止 | for { if cond { break } } 无确定上界 |
loop is not bounded |
拒绝路径示意
graph TD
A[Prog Load Request] --> B{Verifier Pass?}
B -->|No| C[Reject with error]
B -->|Yes| D[Attach to Hook]
3.2 静态分析路径限制(max instructions、stack depth)的Go调试策略
当 go vet 或静态分析工具(如 staticcheck)因路径过长被截断时,核心限制常来自 max instructions(默认约10万条)与 stack depth(默认20层)。这些阈值会误判合法递归或复杂表达式。
调试参数调优
可通过环境变量或命令行显式放宽:
go vet -vettool=$(which staticcheck) \
-f 'checks=all' \
-args '-max-instructions=200000,-max-stack-depth=30' \
./...
-max-instructions:控制IR指令数上限,适用于深度嵌套闭包或大型switch;-max-stack-depth:影响递归调用图遍历深度,对泛型展开/模板化代码尤为关键。
常见触发场景对比
| 场景 | max-instructions 影响 | stack-depth 影响 |
|---|---|---|
深度泛型链(A[B[C[D]]]) |
中等 | 高 ✅ |
百行 if/else if 链 |
高 ✅ | 低 |
reflect.Value.Call 动态调用链 |
高 ✅ | 中 |
分析流程可视化
graph TD
A[源码解析] --> B[生成SSA IR]
B --> C{指令数 ≤ max?}
C -->|否| D[中止分析,报“path too long”]
C -->|是| E[构建调用图]
E --> F{调用深度 ≤ max?}
F -->|否| D
F -->|是| G[执行检查规则]
3.3 verifier log逐行翻译:从LLVM IR到Go可读语义映射表构建
Verifier log 是 eBPF 程序加载时内核验证器输出的原始诊断流,其语句混合了 LLVM IR 片段与底层寄存器约束描述。构建可读映射表的核心在于建立三元关联:LLVM 指令位置 → 验证器错误码 → Go 结构化语义。
映射表核心字段
llvm_line: 原始.ll文件行号(如%5 = add i64 %4, 1)verifier_msg: 内核输出(如R1 type=ctx expected=fp)go_semantic: 对应 Go 类型提示(如ctxPtrMustNotBeUsedAsFramePointer)
典型翻译逻辑示例
// 将 verifier 日志中的寄存器类型冲突映射为结构化错误
mapEntry := VerifierMapping{
LLVMLine: "27",
VerifierMsg: "R3 type=inv expected=sock",
GoSemantic: "SockPtrRequiredButGotInvalidPtr",
}
该结构将非结构化字符串 R3 type=inv expected=sock 转为可嵌入 Go 错误处理链的语义标识,LLVMLine 用于反向定位源码,GoSemantic 支持 errors.Is(err, SockPtrRequiredButGotInvalidPtr) 断言。
映射关系表(节选)
| LLVM Line | Verifier Message | Go Semantic |
|---|---|---|
| 42 | R1 type=ctx expected=fp |
CtxPtrUsedAsFramePointer |
| 89 | invalid bpf_ld imm off=0 |
InvalidLoadImmOffsetZero |
graph TD
A[Verifier Log Line] --> B{Parse Register & Type}
B --> C[Match LLVM IR Line via debug info]
C --> D[Select Go Semantic Tag]
D --> E[Build Mapping Struct]
第四章:认证题库高频考点实战精解
4.1 网络流量追踪题:基于tc/bpf+Go的L3/L4协议栈探针开发
传统内核模块开发门槛高、热更新难,而 eBPF 提供了安全、可验证的运行时追踪能力。结合 tc(traffic control)的 cls_bpf 分类器,可在数据包进入网络协议栈早期(ingress)或离开时(egress)注入 BPF 程序,实现零侵入式 L3/L4 流量观测。
核心架构设计
- 使用
libbpf-go封装 eBPF 加载与 map 交互 - tc hook 绑定在 veth 或物理网卡的 cls_bpf 分类器上
- BPF 程序解析
skb->protocol、ip_hdr()和tcp_hdr()提取五元组
关键代码片段(Go + libbpf-go)
// 加载并附加 eBPF 程序到 tc ingress 钩子
prog, err := obj.TcFilterPrograms.TraceFlow
if err != nil { panic(err) }
qdisc := tc.NewQdisc(&tc.Qdisc{LinkIndex: ifIndex, Parent: tc.HANDLE_ROOT, Kind: "clsact"})
qdisc.Add()
filter := tc.NewFilter(&tc.Filter{LinkIndex: ifIndex, Parent: tc.HANDLE_CLSACT_INGRESS, Kind: "bpf", BpfFd: uint32(prog.FD())})
filter.Add()
逻辑说明:
clsactqdisc 提供 ingress/egress 无队列钩子;BpfFd指向已验证的 eBPF 程序描述符;Parent: HANDLE_CLSACT_INGRESS确保在 IP 层之前捕获原始 skb。
数据采集字段对照表
| 字段 | 来源 | 协议层级 |
|---|---|---|
src_ip |
ip_hdr(skb)->saddr |
L3 |
dst_port |
tcp_hdr(skb)->dest |
L4 |
packet_len |
skb->len |
L2/L3 |
graph TD
A[原始数据包] --> B{tc clsact ingress}
B --> C[eBPF 程序解析 skb]
C --> D[提取五元组+时间戳]
D --> E[写入 percpu_hash map]
E --> F[Go 用户态轮询读取]
4.2 进程行为审计题:tracepoint探针在Go中实现execve/execveat全链路捕获
Linux内核5.15+原生支持syscalls/sys_enter_execve与syscalls/sys_enter_execveat tracepoint,为无侵入式进程启动审计提供基石。
核心探针绑定
// 使用libbpf-go绑定execveat入口探针
tp := "syscalls/sys_enter_execveat"
prog, err := linker.LoadProgram(tp)
if err != nil {
log.Fatal(err) // 需CAP_SYS_ADMIN权限
}
该代码加载eBPF程序并挂载至execveat系统调用入口;linker.LoadProgram()自动解析内核符号表,tp字符串需严格匹配tracepoint路径,错误将导致静默失败。
关键字段提取逻辑
| 字段名 | 来源 | 说明 |
|---|---|---|
filename |
args->filename |
用户态传入的二进制路径指针(需probe_read_user) |
argv |
args->argv |
字符串数组指针,需逐级解引用 |
flags |
args->flags |
execveat特有标志(如AT_EMPTY_PATH) |
全链路捕获流程
graph TD
A[用户调用execveat] --> B[内核触发tracepoint]
B --> C[eBPF程序解析regs/args]
C --> D[通过bpf_probe_read_user提取用户空间字符串]
D --> E[通过ringbuf推送至userspace Go程序]
E --> F[Go侧构建进程启动事件并打标]
4.3 内存异常检测题:kprobe+uprobe混合探针在Go中的协同调度与上下文还原
Go运行时的栈帧管理与C ABI存在语义鸿沟,直接hook runtime.mallocgc(kprobe)与用户函数(uprobe)需统一上下文视图。
协同触发机制
- kprobe捕获内核侧内存分配事件(
__kmalloc),提取goid与pc; - uprobe在Go函数入口注入,通过
/proc/[pid]/maps定位runtime.g结构偏移; - 双探针通过perf event ring buffer共享
struct probe_ctx,含goroutine_id、stack_id、timestamp_ns。
上下文还原关键字段
| 字段 | 来源 | 用途 |
|---|---|---|
g_stackbase |
uprobe读取g->stack.lo |
定位Go栈边界 |
k_stackptr |
kprobe读取pt_regs->sp |
对齐内核栈指针 |
user_pc |
uprobe $rdi寄存器 |
关联Go源码行号 |
// perf event payload struct (shared between k/u-probe)
struct probe_ctx {
__u64 timestamp; // nanosecond monotonic clock
__u32 goid; // extracted from runtime.g->goid
__u16 stack_id; // bpf_get_stackid() for Go stack trace
__u8 is_malloc; // 1: kernel alloc, 0: user func entry
};
该结构由eBPF verifier校验内存安全,stack_id经bpf_get_stackid()统一采集Go runtime栈,规避CGO调用链断裂问题。goid字段在uprobe中通过bpf_probe_read_kernel()从runtime.g结构体偏移0x10处读取,确保与kprobe侧current->pid映射一致。
4.4 文件系统监控题:vfs_read/vfs_write探针的Go侧数据采样与零拷贝优化
核心挑战
vfs_read/vfs_write 是内核文件 I/O 的统一入口,高频调用下传统 bpf_perf_event_output 易触发内存拷贝开销。Go 用户态需低延迟捕获路径、字节数、PID 等上下文,同时规避 unsafe.Pointer → []byte 的冗余复制。
零拷贝采样设计
采用 BPF_MAP_TYPE_PERF_EVENT_ARRAY + 自定义 ringbuf(eBPF 5.8+)配合 Go 的 mmap 直接映射:
// perfMap 是已 mmap 的 perf event ring buffer
data, err := perfMap.ReadBytes() // 零拷贝读取(仅移动 consumer index)
if err != nil { return }
// data 指向内核 ringbuf 物理页,生命周期由 eBPF map 管理
逻辑分析:
ReadBytes()调用底层perf_event_mmap映射页,返回[]byte底层指针直接指向内核 ringbuf 数据区;len(data)为就绪样本字节数,无需copy()即可解析结构体。
关键字段布局(eBPF 端)
| 字段 | 类型 | 说明 |
|---|---|---|
| pid | u32 | 进程 ID |
| bytes | u64 | 本次读/写字节数 |
| path_off | u16 | 路径字符串在 sample 中偏移 |
| path_len | u16 | 路径长度(含 ‘\0’) |
数据同步机制
graph TD
A[eBPF: vfs_write] -->|bpf_perf_event_output| B[Perf Ringbuf]
B --> C[Go mmap 区域]
C --> D[Go goroutine 解析]
D --> E[无拷贝交付至 metrics pipeline]
第五章:结语:通往eBPF内核可观测性专家的最后一公里
从perf_event_open到bpf_trace_printk的演进路径
在某金融核心交易系统故障复盘中,团队最初依赖perf_event_open捕获CPU周期异常,但采样精度不足导致漏掉微秒级调度延迟。切换至eBPF后,通过bpf_kprobe挂载在__schedule入口,配合bpf_ringbuf_output零拷贝推送128字节结构化事件(含prev_pid、next_pid、rq_clock),使调度抖动定位时间从47分钟压缩至93秒。关键代码片段如下:
SEC("kprobe/__schedule")
int trace_schedule(struct pt_regs *ctx) {
struct sched_event_t event = {};
event.prev_pid = bpf_get_current_pid_tgid() >> 32;
event.timestamp = bpf_ktime_get_ns();
bpf_ringbuf_output(&rb, &event, sizeof(event), 0);
return 0;
}
生产环境热加载策略
某云厂商在Kubernetes节点上部署eBPF监控时,采用双缓冲热加载机制:新程序编译后先注入/sys/fs/bpf/tc/globals/new_prog,通过bpf_obj_get验证校验和与符号表完整性,再原子替换/sys/fs/bpf/tc/globals/active_prog的fd引用。该方案实现毫秒级无损升级,避免传统rmmod/insmod导致的3.2秒观测断点。
内核版本兼容性矩阵
| 内核版本 | BTF支持 | map_in_map | perf_buffer可用性 | 典型适配方案 |
|---|---|---|---|---|
| 5.4 | ❌ | ✅ | ✅ | 使用bpf_perf_event_output替代ringbuf |
| 5.10 | ✅ | ✅ | ✅ | 启用BTF调试信息生成 |
| 6.1 | ✅ | ✅ | ✅ | 默认启用bpf_iter接口 |
安全边界实践
在PCI-DSS合规场景中,所有eBPF程序必须通过bpf_verifier的三重校验:① 指令数≤1M条(max_insns=1000000);② 内存访问偏移量经bpf_probe_read_kernel封装;③ 网络钩子程序强制启用CAP_SYS_ADMIN能力隔离。某支付网关通过bpftool prog dump xlated反编译验证,确认无call bpf_map_lookup_elem之外的任意内存读取指令。
观测数据流拓扑
使用Mermaid描述真实生产环境的数据链路:
flowchart LR
A[内核kprobe] --> B[bpf_ringbuf]
B --> C{用户态消费者}
C --> D[Prometheus Exporter]
C --> E[实时日志分析]
C --> F[异常检测模型]
D --> G[Alertmanager]
F --> H[自动扩容决策]
性能压测基准
在48核服务器运行ebpf-bench工具集,对比不同eBPF程序类型对系统开销的影响:
tracepoint/syscalls/sys_enter_write: 平均延迟增加0.8μs(P99为2.3μs)kretprobe/tcp_sendmsg: CPU占用率峰值达1.2%(单核)xdp_redirect_map: 网络吞吐下降0.03%(10Gbps线速)
调试工具链组合
某CDN厂商构建了三级调试体系:第一层用bpftool prog tracelog捕获verifier日志;第二层通过llvm-objdump -S反汇编定位寄存器溢出;第三层启用CONFIG_DEBUG_INFO_BTF=y后,用pahole -C bpf_prog解析程序结构体布局。当遇到R1 invalid mem access 'imm'错误时,该组合可将根因定位时间缩短至平均4.7分钟。
混沌工程验证方法
在eBPF监控程序上线前,执行以下混沌测试:
- 使用
stress-ng --vm 4 --vm-bytes 8G制造内存压力 - 通过
tc qdisc add dev eth0 root netem delay 100ms 20ms注入网络抖动 - 运行
fio --name=randwrite --ioengine=libaio --rw=randwrite触发I/O密集负载
实测表明,在上述复合压力下,eBPF程序仍保持99.999%的事件捕获成功率,且bpf_map_update_elem失败率低于0.002%。
