第一章:Go编译器底层漏洞利用图谱总览
Go 编译器(gc)作为静态类型语言的核心基础设施,其设计强调安全性与性能平衡,但历史版本中仍存在若干影响深远的底层缺陷——涵盖 SSA 中间表示生成错误、逃逸分析绕过、内联优化引发的内存布局篡改,以及 go:linkname 等不安全机制被恶意滥用等维度。这些漏洞并非孤立存在,而是构成一张跨编译阶段、跨运行时语义、跨工具链版本的可组合利用图谱。
关键攻击面分类
- 前端解析层:
go/parser对 malformed struct tags 的非预期处理,可能导致 AST 构造异常,为后续注入提供入口 - 中端优化层:SSA 重写规则在
nil检查消除(如if p != nil { p.x })中未充分验证指针有效性,引发空指针解引用绕过 - 后端代码生成层:x86-64 目标平台下,
MOVQ指令对未对齐栈偏移的硬编码生成,可在特定函数签名下触发 SIGBUS
典型复现路径示例
以下 Go 代码片段在 Go 1.20.5 及更早版本中可触发 SSA 逃逸分析失效,导致堆分配对象被错误地栈分配,进而通过反射修改其字段:
package main
import "unsafe"
func triggerEscapeBypass() *int {
x := 42
// go:noinline 阻止内联,强制逃逸分析介入
// 但 SSA pass 在某些条件下误判 x 为栈局部变量
return &x // 实际应逃逸至堆,却保留在栈帧中
}
func main() {
ptr := triggerEscapeBypass()
// 利用 unsafe.Pointer 修改已返回栈地址的值
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&ptr))
hdr.Data = uintptr(unsafe.Pointer(ptr)) + 8 // 偏移指向相邻栈槽
}
执行需启用
-gcflags="-d=ssa/check/on"观察 SSA 日志,并配合GODEBUG=gocacheverify=0绕过构建缓存干扰。
漏洞影响范围矩阵
| 漏洞类别 | 影响版本范围 | 最小触发条件 | 是否可远程利用 |
|---|---|---|---|
| SSA 内存布局混淆 | 1.19–1.20.5 | 含嵌套闭包+指针返回函数 | 否(需本地执行) |
| go:linkname 滥用 | 所有支持版本 | 导出符号名匹配 runtime 内部 | 是(结合 WebAssembly) |
| CGO 符号解析缺陷 | 1.16–1.18.3 | 特定动态库加载顺序 | 是 |
第二章:gc编译流程深度剖析与篡改路径
2.1 Go 1.16–1.17 gc编译器中间表示(IR)生成机制逆向分析
Go 1.16 起,gc 编译器彻底弃用旧 AST→SSA 的两阶段流程,转向统一 IR 树(*ir.Node)驱动的单遍式生成。核心变化在于 n.Op 操作码直接映射语义,而非依赖 SSA 构建时机。
IR 节点构造关键路径
// src/cmd/compile/internal/noder/expr.go
func (p *noder) expr(n ast.Node) ir.Node {
switch n := n.(type) {
case *ast.BinaryExpr:
left := p.expr(n.X)
right := p.expr(n.Y)
return ir.NewBinaryExpr(base.Pos(n), ir.OADD, left, right) // OADD 等操作码即 IR 语义原子
}
}
ir.NewBinaryExpr 创建带位置、操作符、子节点的 IR 节点;ir.OADD 是编译期确定的常量 opcode,不依赖后端目标。
IR 层级结构对比
| 版本 | 表示形式 | 遍历方式 | 可修改性 |
|---|---|---|---|
| Go 1.15 | AST + SSA 分离 | 多遍 | SSA 只读 |
| Go 1.16+ | 统一 IR 树 | 单遍递归 | 全节点可就地重写 |
graph TD
A[ast.Node] --> B[ir.Node 构造]
B --> C[类型检查注入]
C --> D[逃逸分析注入]
D --> E[SSA 构建入口]
IR 生成后立即注入类型与逃逸信息,为后续优化提供统一上下文。
2.2 类型检查与 SSA 构建阶段的可控注入点定位与验证
在类型检查完成后、SSA 形式生成前,编译器会维护一个语义等价但结构可插桩的中间表示快照。该节点天然支持注入点注册。
注入点注册接口
// RegisterInjectionPoint 注册类型检查后、SSA构建前的可控钩子
func (c *Compiler) RegisterInjectionPoint(
phase string, // 必须为 "post-typecheck"
predicate func(*ast.Node) bool, // 节点过滤条件
action func(*ssa.Builder) error, // 注入逻辑(接收SSA构建器)
) {
c.injectors = append(c.injectors, injector{phase, predicate, action})
}
phase 确保仅在类型系统验证完毕、符号表冻结后触发;predicate 在 AST 层筛选目标表达式(如 *ast.CallExpr);action 直接操作即将构建的 SSA 指令流。
可控性验证矩阵
| 验证维度 | 通过条件 | 工具链支持 |
|---|---|---|
| 时序性 | 注入发生在 types.Checker 完成后、ssa.CreatePackage 前 |
go/types + golang.org/x/tools/go/ssa |
| 可逆性 | 注入指令可被 ssa.Instruction.Parent() 回溯到原始 AST 节点 |
✅ |
注入流程示意
graph TD
A[AST Parsing] --> B[Type Checking]
B --> C{Injectable Snapshot}
C --> D[Predicate Match]
D --> E[SSA Builder Hook]
E --> F[Insert Phi/Store/Call]
2.3 函数内联与逃逸分析绕过技术:构造非法指针传播链
Go 编译器通过逃逸分析决定变量分配在栈还是堆,而函数内联可能意外改变逃逸决策边界。
指针传播的隐式逃逸触发点
当内联函数返回局部变量地址,且该地址被上层函数进一步传递时,编译器可能误判为“未逃逸”,实则形成非法栈指针传播链。
func makePtr() *int {
x := 42
return &x // ❌ 栈变量地址被返回
}
func inlineMe() *int {
return makePtr() // 若此函数被内联,逃逸分析可能失效
}
逻辑分析:makePtr 显式逃逸(&x),但若 inlineMe 被内联,中间调用层消失,编译器可能将 &x 的传播路径压缩为单层,弱化逃逸判定。参数 x 生命周期仅限于 makePtr 栈帧,返回后即悬空。
绕过检测的关键模式
- 利用接口类型擦除静态逃逸信息
- 借助
unsafe.Pointer中断编译器跟踪 - 多层间接引用(如
**int → *int → int)干扰分析流
| 技术手段 | 是否干扰逃逸分析 | 风险等级 |
|---|---|---|
| 接口包装指针 | 是 | ⚠️⚠️ |
unsafe.Pointer 转换 |
是 | ⚠️⚠️⚠️ |
| 内联+闭包捕获 | 条件性 | ⚠️ |
graph TD
A[局部变量x] --> B[&x传入内联函数]
B --> C[返回值被赋给全局map]
C --> D[栈内存被回收后仍被访问]
2.4 汇编代码生成阶段(Plan9 asm)的指令级劫持实践
在 Plan9 工具链中,5c(amd64)、8c(arm64)等汇编器将 Go 中间表示(SSA)翻译为 Plan9 格式汇编。劫持关键在于拦截 .s 文件生成前的 obj.Writer.WriteInst() 调用。
注入时机选择
- 修改
src/cmd/internal/obj/plist.go中Plist.Write方法 - 在
inst.Encode()后、w.Write()前插入自定义PatchInst()
示例:NOP 替换劫持
// 原始指令(由 SSA 生成)
MOVQ AX, BX
// 劫持后注入:
MOVQ $0x1234, R12 // 保存原始目标寄存器上下文
JMP hijack_entry // 跳转至外挂逻辑
hijack_entry:
NOP // 占位,实际由 runtime 注入动态 stub
逻辑分析:
R12作为 Plan9 ABI 保留寄存器(非 callee-save),用于暂存被劫持指令的语义上下文;JMP使用绝对地址跳转,避免相对位移计算误差;NOP为后续mprotect+memcpy提供可写执行槽位。
支持的劫持类型对比
| 类型 | 触发点 | 是否需重定位 | 典型用途 |
|---|---|---|---|
| 指令替换 | WriteInst() |
否 | 函数入口监控 |
| 指令前置插桩 | Encode() 返回前 |
是 | 参数快照捕获 |
| 指令后置钩子 | Write() 后追加段 |
是 | 返回值审计 |
graph TD
A[SSA 生成] --> B[Inst 对象构造]
B --> C{劫持点:WriteInst}
C -->|yes| D[调用 PatchInst]
C -->|no| E[原生 Encode→Write]
D --> F[注入 stub 地址]
F --> G[生成 .s 文件]
2.5 编译缓存(build cache)污染与增量编译劫持实战
当 Gradle 构建缓存命中时,若输入哈希未涵盖环境变量或隐式依赖(如 JAVA_HOME 版本、本地 ~/.m2 快照时间戳),将导致缓存污染——正确源码产出错误字节码。
常见污染源识别
- 未声明的系统属性(
System.getenv("CI")) - 非
@InputFile注解的资源路径(如src/main/resources/config-${profile}.yml) - 编译器插件动态生成的 AST 节点(Lombok 1.18.28+ 的
@Builder生成逻辑受 JDK 版本影响)
劫持增量编译的实操路径
// build.gradle
tasks.withType(JavaCompile).configureEach {
// 强制将 JDK 版本纳入输入哈希
inputs.property("javaVersion", JavaVersion.current().toString())
// 显式排除易变路径
excludes += ['**/target/**', '**/logs/**']
}
此配置使
JavaCompile任务的@TaskInputs哈希包含javaVersion字符串,避免不同 JDK 下复用同一缓存条目;excludes则防止非源文件意外触发重编译。
| 污染类型 | 检测方式 | 修复动作 |
|---|---|---|
| 环境变量泄露 | ./gradlew --scan 查看 Inputs Hash |
使用 inputs.property() 显式声明 |
| 本地 Maven 快照 | mvn dependency:tree -Dverbose |
改用 mavenLocal() + 时间戳校验 |
graph TD
A[源码变更] --> B{Gradle 计算输入哈希}
B --> C[哈希匹配缓存?]
C -->|是| D[直接解压缓存产物]
C -->|否| E[执行真实编译]
D --> F[若JDK不一致→字节码语义错误]
E --> F
第三章:二进制级函数热替换原理与约束突破
3.1 Go runtime 函数调用约定与栈帧布局的动态重写可行性验证
Go 的函数调用采用栈传递 + 寄存器优化混合约定,SP、BP 及 PC 共同维护动态栈帧。栈帧起始处固定存放返回地址与参数副本,后续为局部变量与逃逸对象指针。
栈帧关键字段布局(x86-64)
| 偏移 | 字段 | 说明 |
|---|---|---|
| -8 | 返回地址 | CALL 指令自动压入 |
| -16 | caller BP | 帧基址链,用于 traceback |
| +0 | 参数区首地址 | 按 ABI 对齐填充 |
// 示例:内联汇编观测栈帧结构
func observeFrame() {
asm volatile (
"movq %rsp, %rax\n\t" // 当前 SP
"movq (%rax), %rbx\n\t" // 返回地址
"movq 8(%rax), %rcx\n\t" // 第一个参数(若存在)
: "rax", "rbx", "rcx"
)
}
该内联汇编捕获 RSP 初始值,通过偏移解引用获取返回地址与参数;%rax 存储栈顶,%rbx 提取 RET 目标,验证了 Go 栈帧的连续性与可预测性。
动态重写约束条件
- ✅ 栈指针偏移在编译期固定(非 ASLR 影响)
- ❌
runtime.stackmap由编译器静态生成,不可运行时篡改 - ⚠️
g0栈与goroutine栈隔离,跨栈重写需同步g.status
graph TD
A[触发重写请求] --> B{是否在 GC 安全点?}
B -->|是| C[暂停 M 并切换至 g0]
B -->|否| D[排队等待安全点]
C --> E[校验 stackmap 一致性]
E --> F[原子更新 FP/SP 寄存器]
3.2 .text段内存保护绕过:mprotect+memmove组合实现运行时代码patch
.text 段默认为 PROT_READ | PROT_EXEC,写入会触发 SIGSEGV。绕过需两步:先改权限,再写入。
权限动态修改
// 将.text段起始页设为可写可执行
if (mprotect((void*)(uintptr_t)func & ~0xfff, 4096,
PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
perror("mprotect failed");
}
mprotect 要求地址对齐到页边界(~0xfff 掩码取页首),长度至少一页;参数 PROT_WRITE 是关键前提。
原地代码修补
uint8_t patch[] = {0xb8, 0x01, 0x00, 0x00, 0x00, // mov eax, 1
0xc3}; // ret
memmove((void*)func, patch, sizeof(patch));
memmove 安全处理重叠内存,此处将5字节机器码覆写函数入口——无需跳转,直接生效。
| 步骤 | 系统调用 | 关键参数 | 目的 |
|---|---|---|---|
| 1 | mprotect |
PROT_WRITE |
解锁写权限 |
| 2 | memmove |
原地址+补丁 | 原子级指令替换 |
graph TD
A[定位.text函数地址] --> B[mprotect设PROT_WRITE]
B --> C[构造机器码补丁]
C --> D[memmove覆写]
D --> E[CPU执行新指令]
3.3 GC元数据同步失效与指针扫描规避:基于runtime.gcbits的定向篡改
数据同步机制
Go运行时通过runtime.gcbits字节序列标记堆对象字段的指针性(bit=1表示该字段为指针)。GC扫描时依赖此元数据精准遍历,但若该字节被非法覆写,将导致指针漏扫或误扫。
关键篡改路径
- 修改
obj.gcdata指向的gcbits内存区域 - 利用
unsafe.Pointer绕过写保护(需GOEXPERIMENT=nogc或调试模式) - 在对象分配后、首次GC前完成篡改
篡改后果示例
// 假设 obj 是 *struct{ a, b *int },原 gcbits = 0b11
// 强制改为 0b01 → 隐藏字段 a 的指针,触发漏扫
*(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(obj)) + uintptr(8))) = 0x01
该操作使GC忽略a字段,若a指向短生命周期对象,将引发悬垂指针与use-after-free。
| 字段偏移 | 原gcbits | 篡改后 | 扫描行为 |
|---|---|---|---|
| 0 | 1 | 1 | 正常扫描 |
| 8 | 1 | 0 | 完全跳过 |
graph TD
A[对象分配] --> B[gcdata绑定gcbits]
B --> C[GC扫描读取gcbits]
C --> D{bit==1?}
D -->|是| E[递归扫描该字段]
D -->|否| F[跳过,可能泄漏]
C -.-> G[篡改gcbits内存]
第四章:CVE-2021-XXXX漏洞复现与PoC工程化交付
4.1 漏洞成因溯源:cmd/compile/internal/ssa中Phi节点验证缺失分析
Phi节点是SSA形式中表示控制流汇聚处变量值的关键结构,其正确性依赖于支配边界与前驱块严格匹配。
Phi节点的语义约束
- 每个Phi操作数必须来自对应前驱基本块的末尾定义
- 前驱块数量必须等于Phi操作数数量
- 所有前驱块必须支配当前块(即在支配树中为祖先)
验证缺失导致的漏洞路径
// src/cmd/compile/internal/ssa/verify.go(简化示意)
func (v *verifier) verifyPhi(b *Block, phi *Value) {
// ❌ 缺失:未校验phi.Args[i]是否来自b.Preds[i].Controls
// ❌ 缺失:未检查preds[i]是否真正支配b
for i, a := range phi.Args {
if a.Block != b.Preds[i].Block { // 仅做块ID粗检,忽略控制流路径有效性
v.errorf(phi, "phi arg %d from wrong block", i)
}
}
}
该逻辑仅比对块ID,未验证b.Preds[i]是否为实际控制流前驱,导致非法Phi被接受,引发后续寄存器分配错误或生成无效机器码。
典型触发场景对比
| 场景 | 是否通过现有验证 | 是否实际合法 | 后果 |
|---|---|---|---|
| 正常汇合(if/else) | ✅ | ✅ | 正常编译 |
| 不可达前驱块引用 | ✅ | ❌ | SSA破坏,后端崩溃 |
| 循环头Phi缺少LoopEntry | ✅ | ❌ | 寄存器重用错误 |
graph TD
A[Block A] -->|Pred[0]| C[Phi Block]
B[Block B] -->|Pred[1]| C
C --> D[Use of Phi]
subgraph Vulnerability
C -.->|Missing dominance check| E[Invalid Phi accepted]
end
4.2 PoC构造:从源码注入到ELF节区重写的一站式exploit链搭建
源码级注入:__attribute__((constructor)) 触发点
利用 GCC 构造器属性,在目标二进制加载时自动执行恶意逻辑:
// inject.c —— 编译为位置无关对象,后续注入 .text 或 .init_array
__attribute__((constructor))
void payload() {
setuid(0); system("/bin/sh");
}
逻辑分析:
constructor属性使函数在libc初始化后、main()前被调用;需确保目标启用了DT_INIT或.init_array可写(常见于未启用RELRO的二进制)。
ELF节区重写:动态修补 .dynamic 与 .shstrtab
通过 patchelf 或自定义工具修改节头表,将 .text 权限设为 W+X,并追加 payload 节:
| 字段 | 原值(hex) | 新值(hex) | 作用 |
|---|---|---|---|
sh_flags |
0x6 |
0x60000001 |
启用 SHF_WRITE+SHF_EXECINSTR |
sh_addr |
0x401000 |
0x402000 |
指向新分配的 payload 区域 |
exploit 链编排流程
graph TD
A[源码编译为 PIC object] --> B[解析目标ELF节布局]
B --> C[定位 .init_array 或 .text 可写段]
C --> D[重写节头 + 注入shellcode]
D --> E[修复校验和 & 动态符号重定位]
4.3 跨平台适配:Linux/amd64与darwin/arm64下的寄存器上下文劫持差异处理
寄存器上下文劫持在不同平台存在根本性差异:x86_64 ABI 依赖 rbp, rsp, rip 构建栈帧,而 ARM64(darwin/arm64)以 fp(x29)、sp、pc 为核心,且无隐式栈帧指针。
寄存器映射差异
- Linux/amd64:
uc_mcontext.gregs[REG_RIP]获取指令指针 - darwin/arm64:
ucontext->uc_mcontext->__ss.__pc访问程序计数器
关键代码适配示例
// 统一获取 PC 的跨平台封装
uintptr_t get_pc(const ucontext_t *ctx) {
#ifdef __x86_64__
return ctx->uc_mcontext.gregs[REG_RIP];
#elif defined(__aarch64__)
return ctx->uc_mcontext->__ss.__pc;
#endif
}
该函数屏蔽了 ABI 差异:REG_RIP 是 glibc 定义的 x86_64 寄存器索引常量;__ss.__pc 是 Darwin 内核导出的 ARM64 程序计数器字段,二者语义等价但内存布局与偏移完全不同。
上下文保存字段对照表
| 字段 | Linux/amd64 | darwin/arm64 |
|---|---|---|
| 指令指针 | gregs[REG_RIP] |
__ss.__pc |
| 栈指针 | gregs[REG_RSP] |
__ss.__sp |
| 帧指针 | gregs[REG_RBP] |
__ss.__fp (x29) |
graph TD
A[捕获信号] --> B{平台检测}
B -->|__x86_64__| C[读取 gregs[REG_RIP]]
B -->|__aarch64__| D[读取 __ss.__pc]
C --> E[构造跳转上下文]
D --> E
4.4 隐蔽性增强:利用go:linkname伪指令隐藏符号与反调试对抗策略
go:linkname 是 Go 编译器提供的底层伪指令,允许将 Go 函数绑定到未导出的 runtime 符号,绕过常规符号可见性检查。
基础用法示例
//go:linkname hiddenRuntimeFunc runtime.nanotime
func hiddenRuntimeFunc() int64
该声明将 hiddenRuntimeFunc 直接链接至 runtime.nanotime,但不暴露 nanotime 符号名——编译后 ELF 中无 nanotime 符号条目,仅保留 hiddenRuntimeFunc(若未内联)。
关键约束与风险
- 必须严格匹配签名(参数/返回值类型、调用约定)
- 仅在
unsafe包或runtime相关构建标签下启用 - 跨 Go 版本易失效(符号重命名或 ABI 变更)
反调试协同策略
| 技术手段 | 作用层级 | 触发条件 |
|---|---|---|
go:linkname |
符号表隐蔽 | readelf -s binary 不见目标符号 |
//go:noinline |
阻止内联 | 确保函数体独立存在,便于运行时 patch |
debug.ReadBuildInfo() 检查 |
构建环境验证 | 排除非 release 构建 |
graph TD
A[调用 hiddenRuntimeFunc] --> B[链接至 runtime.nanotime]
B --> C{是否启用 -ldflags=-s}
C -->|是| D[符号表剥离,无 nanotime 条目]
C -->|否| E[仅隐藏符号名,仍可被 addr2line 定位]
第五章:防御纵深构建与编译器安全演进趋势
编译时控制流完整性(CFI)的工业级落地实践
Clang 14+ 在 Linux 内核 v6.1 中启用 -fsanitize=cfi 后,针对 Spectre v1 的 gadget 检测覆盖率提升至 92.7%。某金融支付网关在 GCC 12.3 中启用 -fcf-protection=full,成功拦截了 3 类利用 ret 指令劫持的 ROP 链,日均阻断恶意 payload 217 次。该配置需配合内核 KCFI 补丁及 CONFIG_CFI_CLANG=y 编译选项,且要求所有模块(含第三方驱动)统一使用 Clang 构建。
Rust 编译器对内存安全漏洞的根因消减效果
根据 2023 年 Mozilla 安全团队统计,在 Firefox 浏览器核心组件迁移到 Rust 后,内存安全类 CVE 数量下降 83%(从年均 47 个降至 8 个)。关键在于 rustc 默认启用 borrow checker,并在编译期强制验证所有权语义。例如以下代码在编译阶段即报错:
fn use_after_free() {
let s = String::from("hello");
let ptr = s.as_ptr();
drop(s); // s 被释放
unsafe { println!("{}", *ptr); } // 编译错误:`s` 已 move,`ptr` 失效
}
多层防御链在 CI/CD 流水线中的协同编排
| 防御层级 | 工具链集成点 | 触发条件 | 响应动作 |
|---|---|---|---|
| 编译期 | Bazel + --copt=-Werror=return-type |
发现未处理的函数返回路径 | 构建失败并标记 commit |
| 链接期 | LLD + --orphan-handling=error |
检测到未引用的 .init_array 条目 |
中断链接并输出符号溯源报告 |
| 运行期 | eBPF verifier + bpf_ktime_get_ns() |
用户态进程调用 mmap() 时检测到 PROT_EXEC 标志 |
通过 tracepoint 注入 SIGKILL |
LLVM Pass 对二进制加固的自动化注入
某云原生平台在 CI 流程中插入自定义 LLVM IR Pass,实现以下加固逻辑:
- 扫描所有
call指令,识别调用strcpy等不安全函数的位置; - 自动替换为
__strcpy_chk并注入栈 cookie 校验; - 对所有全局字符串常量添加
section(".rodata.str1.1,aw,progbits,local,0x1")属性,防止运行时重写。
该 Pass 已集成至 Jenkins Pipeline,平均增加编译耗时 1.8%,但使 OWASP ZAP 扫描中“缓冲区溢出”告警归零。
flowchart LR
A[源码提交] --> B{Clang -O2 -fsanitize=address}
B --> C[ASan 报告内存越界]
C --> D[自动定位行号+堆栈]
D --> E[GitLab MR 评论标注缺陷]
E --> F[开发者修复后触发二次构建]
F --> G[LLVM Pass 插入 Control Flow Guard]
G --> H[生成带 CFG 检查的 ELF]
开源项目对编译器安全特性的渐进式采纳
Linux 内核自 v5.15 起默认启用 CONFIG_STACK_PROTECTOR_STRONG,要求 GCC ≥ 4.9;v6.3 引入 CONFIG_INIT_STACK_ALL_ZERO,强制清零所有栈变量——该特性依赖 GCC 11 的 -ftrivial-auto-var-init=zero。相比之下,OpenSSL 3.0 仍兼容 GCC 4.8,仅启用 -DOPENSSL_SECURE_MEMORY 进行运行时防护,暴露了编译器能力差异带来的安全水位落差。
WASM 编译目标的安全边界扩展
Bytecode Alliance 的 Wasmtime 运行时在编译 WebAssembly 模块时,将 wabt 工具链升级至 1.0.32 后,新增对 memory64 和 tail-call 指令的 sandboxed 验证。某边缘计算网关通过 cargo build --target wasm32-wasi --release 生成的模块,在加载时被 wasmtime validate 拦截了 17 个非法 global.get 指令,这些指令试图读取未导出的线程局部存储地址。
