Posted in

Golang环境配置「静默失败」现象深度溯源(strace+gdb双工具链抓取初始化阶段17个关键系统调用)

第一章:Golang环境配置和安装

Go 语言以简洁、高效和内置并发支持著称,而可靠的本地开发环境是所有 Go 项目起步的基础。本章将指导你完成跨平台(Windows / macOS / Linux)的 Go 环境部署,确保后续开发工作流顺畅无阻。

下载与安装

前往官方下载页面 https://go.dev/dl/ 获取对应操作系统的最新稳定版安装包(推荐使用 go1.22.x 或更高版本)。

  • macOS:下载 .pkg 文件并双击运行安装向导,默认安装路径为 /usr/local/go
  • Windows:运行 .msi 安装程序,勾选“Add go to PATH”选项;
  • Linux:推荐解压 tar.gz 包至 /usr/local,执行以下命令:
# 下载后解压(以 go1.22.5.linux-amd64.tar.gz 为例)
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz

# 验证解压结果
ls /usr/local/go/bin  # 应列出 go、gofmt 等可执行文件

配置环境变量

安装后需确保 go 命令全局可用。编辑 shell 配置文件(如 ~/.bashrc~/.zshrc~/.profile),添加以下两行:

export GOROOT=/usr/local/go          # Go 标准库与工具链根目录(Windows 通常为 C:\Go)
export GOPATH=$HOME/go               # 工作区路径(存放第三方包与自定义项目)
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin  # 将 go 和 go install 生成的二进制加入 PATH

执行 source ~/.zshrc(或对应配置文件)使变更生效,随后验证:

go version     # 输出类似 go version go1.22.5 darwin/arm64
go env GOPATH  # 应返回 $HOME/go

验证开发环境

创建一个最小可运行程序确认环境就绪:

mkdir -p $GOPATH/src/hello && cd $GOPATH/src/hello
go mod init hello
echo 'package main\nimport "fmt"\nfunc main() { fmt.Println("Hello, Go!") }' > main.go
go run main.go  # 输出:Hello, Go!
关键目录 用途说明
$GOROOT Go 安装根目录,含 src, pkg, bin,不可与 $GOPATH 混用
$GOPATH 用户工作区,默认包含 src(源码)、pkg(编译缓存)、bin(可执行文件)
$GOPATH/bin go install 安装的命令行工具(如 gopls, stringer)默认落在此处

建议启用 Go Modules(Go 1.11+ 默认开启),避免依赖 $GOPATH/src 的传统路径约束。

第二章:Go安装包解压与二进制部署的静默行为解析

2.1 tar解包过程中的文件权限继承与umask影响实测

tar 解包时不恢复原始 umask,而是直接应用归档中记录的权限位(mode),但受当前 shell 的 umask 实时屏蔽——这是关键误区。

实测环境准备

# 创建测试归档:含 0644 文件和 0755 目录
echo "hello" > test.txt && chmod 0644 test.txt
mkdir bin && chmod 0755 bin && tar -cf archive.tar test.txt bin

tar -cf 保存的是文件系统实际 stat.st_mode 值(含 setuid/setgid 等),不记录 umask。

umask 干预机制

当前会话 umask 0022 → 解包时 0644 & ~0022 = 0644;若 umask 00020644 & ~0002 = 0642(组/其他写权限被强制移除)。

归档内权限 umask 实际解出权限 原因
0644 0022 0644 ~0022 = 0755, 0644 & 0755 = 0644
0644 0002 0642 ~0002 = 0775, 0644 & 0775 = 0642
# 强制忽略 umask:使用 --no-same-permissions(默认行为)
tar -xf archive.tar --no-same-permissions

--no-same-permissions 禁用 tarumask 的自动应用,但仍受限于进程有效权限(如非 root 无法还原 setuid)。

2.2 /usr/local/go路径写入时的SELinux/AppArmor策略拦截验证

当尝试将 Go 二进制文件写入 /usr/local/go 时,强制访问控制(MAC)机制可能静默拒绝操作。

SELinux 拦截现象复现

sudo cp go /usr/local/go/bin/go
# 报错:Permission denied (即使 root 用户且目录权限为 755)

逻辑分析:/usr/local/go 默认属 system_u:object_r:usr_t:s0 类型,而 cp 进程运行在 unconfined_t 域,SELinux 策略禁止 unconfined_t → usr_twrite 权限。需用 semanage fcontext 重标类型或启用 allow_unconfined_write_to_usr 布尔值。

AppArmor 对比行为

发行版 默认配置状态 是否拦截 /usr/local/go 写入
Ubuntu 22.04 启用 是(abstractions/base 未授权该路径)
Debian 12 未启用

验证流程

graph TD
    A[执行 cp] --> B{检查进程 MAC 上下文}
    B -->|SELinux| C[匹配 policydb 中 allow 规则]
    B -->|AppArmor| D[匹配 profile 中 path rules]
    C --> E[拒绝:no allow rule for usr_t:file:write]
    D --> F[拒绝:/usr/local/go/** not in profile]

2.3 go命令软链接创建失败的原子性缺失与竞态复现

竞态根源:ln -sf 非原子操作

ln -sf 实际执行为“删除旧链接 + 创建新链接”两步,中间存在时间窗口。多进程并发调用时,可能观察到链接指向临时失效或断裂状态。

复现脚本(竞态触发)

# 并发创建软链接,模拟高频率 go 命令安装场景
for i in {1..100}; do
  ln -sf "/usr/local/go-v1.22.$i/bin/go" /usr/local/bin/go &
done
wait

逻辑分析& 启动后台任务导致调度不可控;ln -sf 无锁机制,内核不保证 unlink + symlink 的原子性。参数 -s 指定符号链接,-f 强制覆盖——但覆盖动作本身非事务性。

关键现象对比

状态 观察结果
竞态发生中 ls -l /usr/local/bin/goNo such file or directory
竞态结束后 链接指向随机版本(如 v1.22.42)

修复路径示意

graph TD
  A[调用 ln -sf] --> B{是否持有全局锁?}
  B -->|否| C[竞态窗口开启]
  B -->|是| D[原子切换:rename + atomic write]

2.4 GOPATH/GOROOT环境变量初始化时机与shell配置文件加载顺序实验

Go 环境变量的生效时机高度依赖 shell 启动类型(登录/非登录、交互/非交互)及配置文件加载链。

shell 配置文件加载顺序(以 Bash 为例)

启动类型 加载文件顺序
登录 shell /etc/profile~/.bash_profile~/.bash_login~/.profile
非登录交互 shell ~/.bashrc

实验验证流程

# 在 ~/.bash_profile 中添加(不重启终端)
export GOROOT="/usr/local/go"
export GOPATH="$HOME/go"
export PATH="$GOROOT/bin:$GOPATH/bin:$PATH"

此段仅在新登录 shell 中生效;若当前是 tmux 或子 shell,需手动 source ~/.bash_profile 或改写 ~/.bashrc 并确保其被登录 shell 调用。

环境变量初始化时序关键点

  • GOROOT 通常由 Go 安装脚本自动设为安装路径,但若手动解压二进制,必须显式声明;
  • GOPATH 在 Go 1.11+ 后虽非绝对必需(模块模式下),但 go install 仍依赖它定位 bin/
  • GOROOT 错误,go version 将报 cannot find runtime/cgo
graph TD
    A[Shell 启动] --> B{是否为登录 shell?}
    B -->|是| C[/etc/profile → ~/.bash_profile/]
    B -->|否| D[~/.bashrc]
    C --> E[执行 export GOROOT/GOPATH]
    D --> E
    E --> F[go 命令解析路径]

2.5 多版本共存场景下go version输出缓存污染与PATH优先级陷阱

当系统中并存 go1.19go1.21go1.22 时,go version 输出可能被 shell 缓存误导:

$ hash -d go        # 清除命令哈希缓存(关键!)
$ which go          # 检查实际路径
/usr/local/go1.21/bin/go
$ go version        # 仍可能返回旧版本?→ PATH 中存在更高优先级的软链

逻辑分析bashhash 表缓存首次查找到的 go 路径;若 /usr/local/bin/go(指向 go1.19)早于 /usr/local/go1.21/bin 出现在 PATH 中,则即使 which 显示新路径,go version 仍执行旧二进制。

常见 PATH 顺序陷阱:

PATH 片段 风险等级 原因
/usr/local/bin ⚠️ 高 常含旧版软链 /usr/local/bin/go → /usr/local/go1.19/bin/go
/usr/local/go1.21/bin ✅ 安全 显式版本路径,无间接引用

根本解决路径优先级冲突

export PATH="/usr/local/go1.21/bin:$PATH"  # 确保版本化路径前置

缓存污染验证流程

graph TD
    A[执行 go version] --> B{shell 是否命中 hash 缓存?}
    B -->|是| C[返回缓存路径的 go 二进制]
    B -->|否| D[按 PATH 顺序搜索首个 go]
    D --> E[执行该路径下的 go]

第三章:Go工具链初始化阶段的系统调用链路建模

3.1 Go runtime启动前17个关键系统调用的strace捕获与语义标注

Go 程序在 main 函数执行前,runtime 必须完成底层初始化——这依赖于内核提供的原始能力。我们通过 strace -f -e trace=brk,mmap,mprotect,arch_prctl,rt_sigprocmask,rt_sigaction,set_tid_address,clone,prctl,getpid,gettid,futex,openat,read,close,munmap 捕获典型静态链接 Go 二进制(如 hello.go)的启动阶段。

核心调用语义分组

  • 内存准备brk(堆基址设定)、mmap(栈/堆/arena 映射)、mprotect(页保护)
  • 信号与线程rt_sigprocmask(屏蔽信号)、clone(创建 M0 线程)、set_tid_address
  • 进程元信息getpid/gettidprctl(PR_SET_NAME)arch_prctl(ARCH_SET_FS)

mmap 调用示例(带注释)

mmap(NULL, 2097152, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
// 参数解析:
//   addr=NULL → 内核选择起始地址
//   length=2MB → Go 初始 heap arena 大小(非固定,依 GOOS/GOARCH 调整)
//   prot=READ|WRITE → 可读写,但未设 EXEC(防 W^X)
//   flags=MAP_PRIVATE|MAP_ANONYMOUS → 私有匿名映射,无文件后端

该调用为 runtime.mheap_.arena 预留连续虚拟地址空间,是后续垃圾收集器管理堆内存的基石。

系统调用 语义作用 是否可省略
arch_prctl 设置 FS 寄存器指向 g 结构体 ❌ 否
futex 后续 goroutine 调度依赖的基础 ✅ 启动时暂不触发
openat(AT_FDCWD, "/etc/resolv.conf", ...) DNS 初始化(仅 net 包启用时出现) ⚠️ 条件触发
graph TD
    A[execve] --> B[brk/set_thread_area]
    B --> C[mmap: stack & mheap arena]
    C --> D[arch_prctl: FS → g]
    D --> E[clone: 创建 M0]
    E --> F[rt_sigaction: 安装 SIGQUIT/SIGPROF 处理器]

3.2 execve调用中argv/envp内存布局异常导致的静默退出定位

execveargvenvp 指针数组未以 NULL 终止,或其中某元素为非法地址时,内核在遍历参数/环境字符串时可能触发页错误并静默终止进程(无信号、无日志)。

内存布局合规性要求

  • argvenvp 必须是连续指针数组,末尾以 NULL 结束;
  • 所有指向的字符串必须驻留在可读用户空间,且以 \0 结尾。

典型错误示例

char *argv[] = {"/bin/sh", "-c", "echo hello"}; // ❌ 缺少 NULL 终止符
execve("/bin/sh", argv, environ);

逻辑分析execve 内核路径(bprm_execve → bprm_fill_uid → count())会持续递增指针计数直到遇到 NULL。若缺失,将越界读取后续栈内存,可能命中不可读页,触发 do_page_fault 后直接 return -EFAULT,进程无声退出。

调试验证方法

方法 说明
strace -e trace=execve 观察是否打印 execve(...) 后立即退出
gdb --args ./a.out + catch syscall execve execve 返回前检查 argv[0]argv[n] 是否全有效
/proc/<pid>/maps + pstack 确认栈顶附近 argv 数组布局完整性
graph TD
    A[execve syscall] --> B{argv/envp NULL-terminated?}
    B -->|No| C[Page fault on invalid ptr]
    B -->|Yes| D[Copy strings to new mm]
    C --> E[Return -EFAULT → _exit_group]
    E --> F[Silent termination]

3.3 openat(AT_FDCWD, “/proc/self/exe”, …)返回ENOTDIR的内核路径解析缺陷分析

该错误源于内核 path_lookupat() 在处理 /proc/self/exe 这类符号链接时,对 AT_FDCWD 上下文的 nd->flags 判断存在逻辑缺口:当 LOOKUP_DIRECTORY 未被显式设置,但后续路径组件(如 exe)需以目录语义解析时,follow_link() 返回后误触发 ERR_PTR(-ENOTDIR)

关键路径分支

  • openat() 调用 path_at_empty()filename_lookup()
  • /proc/self 解析成功后,exe 作为最后一个组件进入 walk_component()
  • 此时 nd->flags & LOOKUP_DIRECTORY == false,但 proc_pid_link_inode_operationsget_link() 返回 &exe_link,而 link_path_walk() 未重置 nd->last_type

核心代码片段

// fs/namei.c:4210 (v6.1)
if (unlikely(nd->last_type != LAST_NORM)) {
    if (nd->last_type == LAST_DOTDOT)
        err = handle_dots(nd, nd->last_type);
    else
        err = -ENOTDIR; // ← 错误在此处提前触发
    if (err)
        goto out;
}

LAST_SYMLINK 本应允许继续解析,但此处仅校验 LAST_NORM/LAST_DOTDOT,遗漏 LAST_SYMLINK 分支,导致 exe(符号链接)被误判为非法目录组件。

条件 行为 后果
nd->last_type == LAST_SYMLINK 无处理分支 直接返回 -ENOTDIR
nd->flags & LOOKUP_DIRECTORY 未设 不尝试 follow_link 二次解析 链接目标被忽略
graph TD
    A[openat(AT_FDCWD, “/proc/self/exe”)] --> B[filename_lookup]
    B --> C[walk_component for “self”]
    C --> D[follow_link → /proc/1234]
    D --> E[walk_component for “exe”]
    E --> F{nd->last_type == LAST_SYMLINK?}
    F -- No --> G[-ENOTDIR]
    F -- Yes --> H[应调用 link_path_walk]

第四章:基于gdb的Go二进制动态注入与初始化断点调试

4.1 在runtime.args、runtime.osinit、runtime.schedinit等符号处设置条件断点

调试 Go 运行时初始化流程时,精准捕获关键阶段至关重要。runtime.args(解析命令行参数)、runtime.osinit(OS 层初始化)和 runtime.schedinit(调度器初始化)是启动链上三个不可跳过的锚点。

条件断点实战示例

# 在 GDB 中为 runtime.schedinit 设置仅在主线程触发的断点
(gdb) b runtime.schedinit if $rdi == 1

$rdi 是 AMD64 调用约定中第一个整数参数寄存器;Go 启动时传入 m0(主 M 结构体指针),其地址低字节常为 1,可作轻量级线程过滤条件。

常用断点策略对比

符号 触发时机 典型条件表达式
runtime.args 参数解析完成前 $_streq($argv[1], "-test.v")
runtime.osinit 系统调用接口就绪后 $rax != 0(确保 sysconf 成功)
runtime.schedinit GMP 调度器首次配置 $rbp > 0x7fffff000000(栈地址范围校验)

初始化依赖关系

graph TD
    A[runtime.args] --> B[runtime.osinit]
    B --> C[runtime.schedinit]
    C --> D[runtime.mstart]

4.2 跟踪GOROOT检测失败时的errno传递链与errorString构造流程

os/execruntime 初始化尝试读取 GOROOT 时,若 stat("/usr/local/go", &st) 系统调用失败,底层 errno(如 ENOENT)经由 syscall.Errno 封装进入 Go 错误体系。

errno 的捕获与转换

// runtime/proc.go 中 GOROOT 检测片段(简化)
if _, err := os.Stat(GOROOT); err != nil {
    // err 是 *fs.PathError,其 .Err 字段为 syscall.Errno 类型
}

err.Err 值直接映射系统 errno 整数,未做中间转换,确保错误源保真。

errorString 的构造时机

syscall.Errno.Error() 方法触发 errorString 实例化:

  • 调用 strconv.Itoa(int(e)) 获取数字字符串;
  • 查表 syscall.Errstr[e](如 ENOENT → "no such file or directory");
  • 若查表失败,则回退至 "errno " + 数字。
errno 名称 errorString 输出示例
2 ENOENT "no such file or directory"
13 EACCES "permission denied"
graph TD
    A[stat syscall failure] --> B[errno set in r1 register]
    B --> C[syscall.Errno int value]
    C --> D[syscall.Errno.Error()]
    D --> E[errorString{str: “no such file or directory”}]

4.3 分析cgo_enabled=0环境下dlclose调用被跳过引发的模块加载静默终止

CGO_ENABLED=0 时,Go 运行时完全剥离 C 动态链接能力,plugin.Open() 内部对 dlclose 的调用被条件编译直接跳过:

// src/plugin/plugin_dlopen.go(简化)
func (p *Plugin) Close() error {
    if !cgoEnabled { // CGO_ENABLED=0 时恒为 false
        return nil // 静默返回,不释放句柄
    }
    return dlclose(p.handle)
}

逻辑分析:cgoEnabled 是编译期常量,CGO_ENABLED=0 导致 dlclose 调用被彻底移除,插件句柄未释放,但错误被吞没。

影响链路

  • 插件资源泄漏(句柄、内存、文件描述符)
  • 多次 Open/Close 循环后触发 dlopen: cannot load any more object with static TLS
  • 错误无 panic 或日志,仅 plugin.Open 后续调用失败

关键差异对比

场景 CGO_ENABLED=1 CGO_ENABLED=0
plugin.Close() 调用 dlclose 返回 nil
错误可见性 可捕获 dlclose 错误 完全静默
graph TD
    A[plugin.Open] --> B{CGO_ENABLED?}
    B -->|1| C[执行 dlopen + 记录 handle]
    B -->|0| D[模拟加载,无真实 dlopen]
    C --> E[Close 调用 dlclose]
    D --> F[Close 直接 return nil]

4.4 利用gdb Python脚本自动提取初始化阶段所有mmap/mprotect调用的保护标志变异

在程序加载初期,动态链接器与libc初始化会密集调用mmapmprotect,其prot参数(如PROT_READ|PROT_WRITE|PROT_EXEC)常发生非预期变异,成为漏洞利用的关键线索。

核心监控策略

  • _dl_start返回后、__libc_start_main前设置断点,限定为“初始化窗口”;
  • 拦截mmap(第3参数)、mprotect(第3参数)的prot值并记录调用栈深度;
  • 自动过滤重复地址+相同prot组合,仅保留首次变异点。

示例Python脚本片段

import gdb

class ProtWatcher(gdb.Breakpoint):
    def __init__(self, sym):
        super().__init__(sym, gdb.BP_BREAKPOINT, internal=True)
        self.prot_history = set()

    def stop(self):
        prot = gdb.parse_and_eval("$rsi") if "mprotect" in self.location else gdb.parse_and_eval("$rdx")
        addr = gdb.parse_and_eval("$rdi")
        key = (int(addr), int(prot))
        if key not in self.prot_history:
            self.prot_history.add(key)
            print(f"[INIT] {self.location}({addr:#x}, prot={int(prot):#x})")
        return False

ProtWatcher("mmap")
ProtWatcher("mprotect")

逻辑说明:脚本继承gdb.Breakpoint,利用$rdx(mmap第3参数)、$rsi(mprotect第3参数)直接读取寄存器值;internal=True避免干扰用户断点;stop()返回False表示不暂停执行,实现无感日志采集。

常见prot变异模式对照表

原始权限 变异后权限 典型场景
PROT_READ PROT_READ|PROT_EXEC JIT代码页标记可执行
PROT_NONE PROT_READ|PROT_WRITE BSS段延迟初始化
PROT_READ|PROT_WRITE PROT_READ W^X策略强制去写权限
graph TD
    A[程序启动] --> B[_dl_start]
    B --> C[拦截mmap/mprotect]
    C --> D{prot值变更?}
    D -->|是| E[记录地址+prot+调用栈]
    D -->|否| F[忽略]
    E --> G[输出变异序列]

第五章:Golang环境配置和安装

下载与校验官方二进制包

访问 https://go.dev/dl/ 获取最新稳定版 Go(如 go1.22.5.linux-amd64.tar.gz)。下载后务必校验 SHA256 值,避免中间人篡改:

curl -sL https://go.dev/dl/go1.22.5.linux-amd64.tar.gz.sha256 | sha256sum -c --quiet -

返回空输出表示校验通过;若报错则需重新下载。

Linux/macOS 手动安装流程

解压至 /usr/local 并设置 PATH:

sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc

验证安装:go version 应输出 go version go1.22.5 linux/amd64

Windows 安装注意事项

使用 MSI 安装包时,勾选「Add go to system PATH」;若手动解压 ZIP,请将 go\bin 路径添加至系统环境变量 Path(非用户变量),并重启终端。常见错误 go: command not found 多因未刷新环境或路径拼写错误(如误写为 Go\bin)。

GOPATH 与 Go Modules 的协同配置

自 Go 1.11 起默认启用模块模式,但某些遗留项目仍依赖 GOPATH。推荐初始化工作区:

mkdir -p ~/go/{src,bin,pkg}
export GOPATH=$HOME/go
export GOBIN=$GOPATH/bin

同时禁用 GOPATH 模式以强制使用模块:go env -w GO111MODULE=on

多版本共存方案(基于 gvm)

当需同时维护 Go 1.19(兼容旧项目)与 Go 1.22(新特性)时,使用 gvm 管理:

bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)
source ~/.gvm/scripts/gvm
gvm install go1.19.13
gvm install go1.22.5
gvm use go1.22.5

gvm listall 可查看所有可用版本,gvm alias set system go1.22.5 设置默认别名。

代理加速与模块镜像配置

国内开发者需配置 GOPROXY 避免超时:

go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOSUMDB=off  # 仅开发测试阶段临时关闭校验

生产环境建议保留 GOSUMDB=sum.golang.org 并搭配可信代理。

操作系统 推荐安装方式 典型问题排查
Ubuntu 22.04 apt install golang-go apt 版本滞后(当前为 1.18),需手动升级
macOS (Apple Silicon) Homebrew brew install go 若提示 zsh: command not found: go,检查 brew --prefix go 输出路径是否在 $PATH
Windows WSL2 手动解压 + PATH /mnt/c/... 路径性能差,建议解压至 /home/user/go
flowchart TD
    A[下载 go1.22.5.tar.gz] --> B{校验 SHA256}
    B -->|失败| C[重新下载]
    B -->|成功| D[解压到 /usr/local/go]
    D --> E[配置 PATH 环境变量]
    E --> F[运行 go version 验证]
    F --> G[执行 go mod init test && go run main.go]
    G --> H[确认输出预期结果]

IDE 集成验证(VS Code)

安装 Go 扩展后,在 settings.json 中指定 SDK 路径:

{
  "go.goroot": "/usr/local/go",
  "go.toolsEnvVars": {
    "GOPROXY": "https://goproxy.cn"
  }
}

新建 main.go 文件并保存,观察底部状态栏是否显示 “Analyzing…” 后变为 “Ready”,表明语言服务器已加载成功。

Docker 容器内快速验证

构建最小化验证镜像:

FROM golang:1.22.5-alpine
RUN echo 'package main\nimport "fmt"\nfunc main(){fmt.Println("Go OK")}' > /tmp/hello.go && \
    go run /tmp/hello.go

执行 docker build -t go-test .,若输出 Go OK 则容器环境就绪。

企业级 CI/CD 环境适配

在 GitLab CI 中声明 Go 版本:

stages:
  - test
test-go:
  stage: test
  image: golang:1.22.5
  script:
    - go version
    - go mod download
    - go test -v ./...

注意:若项目含 cgo 依赖,需切换为 golang:1.22.5-bullseye 并安装 build-essential

对 Go 语言充满热情,坚信它是未来的主流语言之一。

发表回复

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