第一章:Go构建二进制可执行性失效的系统性归因
Go 二进制可执行性失效并非孤立现象,而是由编译期、运行时与环境三重因素交织导致的系统性问题。其根本诱因常隐匿于构建链路的细微环节,需从源码语义、工具链行为及目标平台约束三个维度协同诊断。
构建环境不一致引发的符号缺失
当 GOOS 或 GOARCH 在构建时被显式指定,而目标环境未满足对应运行时依赖(如 CGO_ENABLED=1 时缺失 libc 头文件或动态链接器),将生成无法加载的二进制。验证方式如下:
# 检查二进制是否为静态链接(无动态依赖)
ldd ./myapp || echo "statically linked"
# 若输出 'not a dynamic executable',则为纯静态;否则需确认目标系统是否存在对应.so版本
CGO 与跨平台构建的隐式耦合
启用 CGO 后,go build 默认依赖宿主机 C 工具链,导致在 macOS 上构建的二进制可能嵌入 libSystem.B.dylib 符号,在 Linux 环境下直接报 No such file or directory。禁用 CGO 可强制纯 Go 运行时:
CGO_ENABLED=0 go build -o myapp .
# 注意:此模式下 os/user、net 等包将回退至纯 Go 实现,DNS 解析策略亦随之改变
模块校验与不可重现构建
go.sum 文件缺失或校验失败时,go build 可能静默降级使用本地缓存模块,引入非预期的 API 行为变更。典型表现是相同代码在 CI 与本地构建出功能迥异的二进制。强制清理并重建:
go clean -modcache && go mod verify && go build -trimpath -ldflags="-s -w" -o myapp .
# -trimpath 移除绝对路径信息,-s -w 剥离调试符号与 DWARF 数据,提升可重现性
常见失效场景对照表:
| 失效现象 | 根本原因 | 快速验证命令 |
|---|---|---|
exec format error |
GOARCH 不匹配(如 arm64 二进制在 amd64 运行) | file ./myapp |
standard_init_linux.go:228: exec user process caused: no such file or directory |
动态链接器路径不存在(如 /lib64/ld-linux-x86-64.so.2) |
readelf -l ./myapp \| grep interpreter |
启动后 panic: user: lookup uid for : no such file or directory |
CGO_ENABLED=1 且容器内缺失 /etc/passwd |
strace -e trace=openat ./myapp 2>&1 \| grep passwd |
第二章:LD_LIBRARY_PATH环境变量的动态链接影响机制与实证分析
2.1 LD_LIBRARY_PATH优先级规则与glibc加载时序验证
LD_LIBRARY_PATH 在动态链接器 ld-linux.so 启动早期即被解析,但其生效时机严格晚于 glibc 自身(如 libc.so.6)的加载——后者由内核直接通过 .interp 段指定,完全绕过环境变量控制。
动态链接器加载时序关键点
- 内核读取 ELF 的
PT_INTERP段 → 加载ld-linux-x86-64.so.2 ld-linux自身依赖的libc.so.6由硬编码路径(如/lib64/libc.so.6)加载LD_LIBRARY_PATH仅影响后续dlopen()或可执行文件DT_NEEDED条目的解析
验证命令链
# 查看解释器与依赖
readelf -l /bin/ls | grep interpreter
ldd /bin/ls | grep libc
上述
readelf输出program interpreter: /lib64/ld-linux-x86-64.so.2;ldd显示libc.so.6 => /lib64/libc.so.6—— 二者路径均不受LD_LIBRARY_PATH影响,证实 glibc 加载早于环境变量介入。
| 阶段 | 主体 | 是否受 LD_LIBRARY_PATH 影响 |
|---|---|---|
| 1. 解释器加载 | 内核 | 否 |
| 2. libc 加载 | ld-linux(硬编码路径) |
否 |
| 3. 应用依赖解析 | ld-linux(遍历 LD_LIBRARY_PATH) |
是 |
graph TD
A[内核加载 /bin/ls] --> B[读取 PT_INTERP]
B --> C[加载 /lib64/ld-linux-x86-64.so.2]
C --> D[硬编码加载 /lib64/libc.so.6]
D --> E[解析 LD_LIBRARY_PATH]
E --> F[定位应用所需 .so]
2.2 使用ldd和readelf定位缺失共享库路径的实际案例
当执行 ./myapp 报错 error while loading shared libraries: libjson.so.5: cannot open shared object file,需快速定位依赖链断裂点。
快速依赖扫描
ldd ./myapp | grep "not found"
输出:
libjson.so.5 => not found
ldd模拟动态链接器行为,显示运行时查找路径(LD_LIBRARY_PATH、/etc/ld.so.cache、默认路径),但不揭示该库在编译时被期望的确切 SONAME。
深度符号与路径溯源
readelf -d ./myapp | grep 'NEEDED\|RUNPATH\|RPATH'
输出含:
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/../lib]
-d读取动态段:NEEDED声明所需库名(如libjson.so.5),RUNPATH指定优先搜索路径($ORIGIN表示可执行文件所在目录)。
关键路径对照表
| 工具 | 揭示信息 | 是否含路径语义 |
|---|---|---|
ldd |
运行时实际查找结果 | 否(仅显示“not found”) |
readelf -d |
编译期嵌入的依赖名与搜索路径 | 是(RUNPATH/RPATH) |
修复流程
- 在
$ORIGIN/../lib下创建软链:ln -s libjson.so.5.1.0 libjson.so.5 - 更新缓存(若用系统路径):
sudo ldconfig
graph TD
A[执行失败] --> B{ldd 查依赖}
B --> C[发现 not found]
C --> D{readelf -d 查 RUNPATH}
D --> E[定位 $ORIGIN/../lib]
E --> F[检查该目录下是否存在匹配 SONAME]
2.3 容器化环境中LD_LIBRARY_PATH传播失效的复现与修复
失效复现步骤
在多阶段构建中,若仅在 build 阶段设置 LD_LIBRARY_PATH,运行时阶段将无法继承该变量:
FROM ubuntu:22.04 AS build
ENV LD_LIBRARY_PATH=/opt/mylib:$LD_LIBRARY_PATH
RUN echo $LD_LIBRARY_PATH # 输出:/opt/mylib
FROM ubuntu:22.04
COPY --from=build /opt/mylib /opt/mylib
# 此处 LD_LIBRARY_PATH 为空!
逻辑分析:
ENV指令作用域限于当前构建阶段;LD_LIBRARY_PATH不会跨阶段自动传递,且容器运行时未显式声明,导致动态链接器(ld.so)无法定位自定义库路径。
修复方案对比
| 方案 | 是否持久 | 是否需重编译 | 安全性 |
|---|---|---|---|
ENV LD_LIBRARY_PATH=...(运行时镜像) |
✅ | ❌ | ⚠️(易被覆盖) |
RUN echo "/opt/mylib" > /etc/ld.so.conf.d/mylib.conf && ldconfig |
✅ | ❌ | ✅(系统级注册) |
RPATH 编译时嵌入 |
✅ | ✅ | ✅(最健壮) |
推荐实践
优先使用 ldconfig 注册路径,避免环境变量依赖:
RUN echo "/opt/mylib" > /etc/ld.so.conf.d/mylib.conf && \
ldconfig -v 2>/dev/null | grep mylib
参数说明:
ldconfig -v验证路径是否生效;2>/dev/null屏蔽冗余日志,聚焦关键输出。
2.4 跨主机部署时LD_LIBRARY_PATH误配导致段错误的strace取证模板
当跨主机部署二进制时,若目标机器缺失对应 .so 版本或 LD_LIBRARY_PATH 指向错误路径,程序常在 dlopen 或符号解析阶段触发段错误——而 strace 是定位该问题的黄金起点。
关键 strace 命令模板
strace -e trace=openat,open,openat,stat,mmap,mprotect -f -o strace.log ./myapp 2>&1
-e trace=...精准捕获动态链接关键系统调用;-f跟踪子进程(如 fork 后的 loader);openat/open揭示.so实际搜索路径与失败点;mmap异常(如MAP_FAILED)常伴随ENOENT或ELIBBAD错误码。
典型失败模式对照表
| 错误现象 | strace 中线索示例 | 根因 |
|---|---|---|
openat(..."/lib64/libc.so.6"...ENOENT) |
路径存在但文件缺失 | 宿主机 libc 版本不兼容 |
mmap(NULL, ..., PROT_READ|PROT_EXEC, MAP_PRIVATE, ...) → ENOMEM |
权限/地址空间冲突 | 混合 32/64 位库或 ASLR 干扰 |
动态链接失败路径
graph TD
A[程序启动] --> B[ld-linux.so 加载]
B --> C{LD_LIBRARY_PATH 是否设置?}
C -->|是| D[按路径顺序搜索 .so]
C -->|否| E[查 /etc/ld.so.cache + 默认路径]
D --> F[openat 失败?]
E --> F
F -->|ENOENT/ELIBBAD| G[段错误前 mmap 失败]
2.5 环境变量注入测试:通过env -i模拟纯净环境验证依赖链断裂点
在微服务启动阶段,环境变量常被用作配置注入的“快捷通道”,但隐式依赖易导致环境迁移失败。env -i 是剥离所有继承环境变量、构建最小可信执行上下文的关键工具。
模拟脆弱依赖链
# 清空环境后仅注入必要变量,触发应用启动失败
env -i PATH=/usr/bin:/bin NODE_ENV=production ./app.js
-i 参数强制清空父 shell 所有 env,避免 LD_LIBRARY_PATH、PYTHONPATH 等隐式污染;显式传入 PATH 和 NODE_ENV 可精准定位缺失的 DATABASE_URL 是否为启动阻断点。
常见断裂点对照表
| 变量名 | 必需性 | 断裂表现 |
|---|---|---|
DATABASE_URL |
强依赖 | 连接池初始化失败 |
PORT |
弱依赖 | 默认回退至 3000 |
NODE_ENV |
中依赖 | 日志级别/缓存策略异常 |
依赖链验证流程
graph TD
A[env -i 启动] --> B{加载 config/index.js}
B --> C[读取 process.env]
C --> D[缺失 DATABASE_URL?]
D -->|是| E[抛出 ENOENT 或 timeout]
D -->|否| F[继续初始化]
第三章:RPATH与RUNPATH的编译期嵌入策略与运行时解析行为
3.1 go build -ldflags=”-rpath”对cgo依赖库路径的静态绑定实践
在混合使用 C 库(如 OpenSSL、SQLite)的 Go 项目中,运行时动态链接器常因 libxxx.so 路径缺失而报 error while loading shared libraries。-rpath 是 ELF 动态段中指定运行时库搜索路径的关键机制。
为什么 -rpath 比 LD_LIBRARY_PATH 更可靠?
LD_LIBRARY_PATH易受环境干扰,不适用于容器或多租户部署;-rpath写入二进制文件自身,由动态链接器ld-linux.so优先解析。
使用方式示例
go build -ldflags="-rpath='$ORIGIN/lib:/usr/local/lib'" -o app main.go
'$ORIGIN/lib'表示可执行文件所在目录下的lib/子目录;$ORIGIN是运行时求值的伪变量,非 shell 展开。单引号防止 shell 提前解析$。
rpath 生效流程(mermaid)
graph TD
A[执行 ./app] --> B[ld-linux.so 读取 ELF .dynamic 段]
B --> C{查找 DT_RPATH 或 DT_RUNPATH}
C -->|存在| D[依次搜索各路径中的依赖 SO]
C -->|不存在| E[回退至 /etc/ld.so.cache 和默认路径]
常见陷阱对照表
| 场景 | 错误写法 | 正确写法 | 原因 |
|---|---|---|---|
| 路径含空格 | -rpath=/opt/my lib |
-rpath='/opt/my lib' |
ld 不支持未引号路径分词 |
| 多路径分隔 | -rpath=a;b |
-rpath='a:b' |
必须用冒号分隔,且整体加单引号 |
注:Go 1.20+ 支持
CGO_LDFLAGS="-rpath=..."环境变量注入,更利于跨平台构建。
3.2 使用patchelf修改已生成二进制的rpath并验证加载路径变更效果
patchelf 是 Linux 下轻量级修改 ELF 二进制元信息的利器,尤其适用于修复部署时动态库搜索路径问题。
修改 rpath 的基本操作
patchelf --set-rpath '$ORIGIN/../lib:/usr/local/lib' ./myapp
--set-rpath:覆盖原有DT_RPATH或DT_RUNPATH字段;$ORIGIN表示可执行文件所在目录(POSIX 标准,被广泛支持);- 多路径用冒号分隔,优先级从左到右。
验证变更效果
使用 readelf 和 ldd 双重确认: |
工具 | 命令 | 作用 |
|---|---|---|---|
readelf |
readelf -d ./myapp \| grep rpath |
查看原始 ELF 动态段 rpath | |
ldd |
ldd ./myapp |
模拟运行时实际解析路径 |
加载路径生效流程
graph TD
A[执行 ./myapp] --> B{loader 解析 DT_RUNPATH}
B --> C[依次尝试 $ORIGIN/../lib → /usr/local/lib]
C --> D[找到 libfoo.so → 加载成功]
C --> E[全部失败 → “not found” 错误]
3.3 RPATH vs RUNPATH:ELF动态段差异及其在Go交叉编译中的陷阱
动态链接器的搜索路径机制
RPATH 和 RUNPATH 均是 ELF 文件 .dynamic 段中指定的运行时库搜索路径,但语义与优先级不同:
RPATH由DT_RPATH条目承载,优先级高于环境变量LD_LIBRARY_PATH(除非启用--disable-new-dtags);RUNPATH由DT_RUNPATH承载,优先级低于LD_LIBRARY_PATH,更符合现代安全策略。
关键差异对比
| 特性 | RPATH | RUNPATH |
|---|---|---|
| 对应动态标签 | DT_RPATH |
DT_RUNPATH |
| 环境变量优先级 | 高于 LD_LIBRARY_PATH |
低于 LD_LIBRARY_PATH |
| Go 工具链默认 | ❌ 不生成(除非显式 -ldflags="-r") |
✅ Go 1.20+ 交叉编译默认使用 |
Go 交叉编译中的典型陷阱
# 构建带 RPATH 的二进制(非推荐)
go build -ldflags="-r /opt/mylib" -o app-linux-amd64 main.go
🔍 此命令强制写入
DT_RPATH,导致在目标系统中若/opt/mylib不存在或权限受限,dlopen直接失败且不回退到LD_LIBRARY_PATH。而 Go 默认启用RUNPATH(通过--enable-new-dtags),更利于容器/嵌入式环境调试。
动态段验证流程
graph TD
A[go build] --> B{ldflags 含 -r?}
B -- 是 --> C[写入 DT_RPATH]
B -- 否 --> D[写入 DT_RUNPATH 默认]
C --> E[链接器忽略 LD_LIBRARY_PATH]
D --> F[链接器尊重 LD_LIBRARY_PATH]
第四章:CGO_ENABLED与动态链接混合模式下的三重校验法
4.1 CGO_ENABLED=0/1下符号解析差异对比:nm + objdump实战分析
Go 构建时 CGO_ENABLED 状态直接影响二进制中符号的构成与导出行为。
符号可见性差异根源
当 CGO_ENABLED=0 时,Go 使用纯静态链接,所有依赖(如 net, os/user)被编译为 Go 原生实现,不引入 libc 符号;而 CGO_ENABLED=1(默认)会链接 libc,暴露大量 C 运行时符号(如 getaddrinfo, pthread_create)。
实战命令对比
# 构建两个版本
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o app-static .
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -o app-dynamic .
# 提取动态符号(仅 CGO=1 版本含 libc 符号)
nm -D app-dynamic | grep getaddrinfo
# 输出:0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 getaddrinfo
nm -D列出动态符号表;*UND*表示未定义(需动态链接),GLIBC_2.2.5是符号版本约束。CGO_ENABLED=0下该符号完全缺失。
关键差异总结
| 维度 | CGO_ENABLED=0 | CGO_ENABLED=1 |
|---|---|---|
| 符号来源 | 纯 Go 实现(无 libc) | 混合 Go + libc / libpthread |
nm -D 符号数量 |
≈ 0(无动态依赖) | 数百个(含 _init, __libc_start_main 等) |
objdump -t 中 .text 段 |
仅 Go runtime & 用户代码 | 额外包含 cgo stubs 和 C ABI 胶水代码 |
graph TD
A[go build] -->|CGO_ENABLED=0| B[静态链接<br>no libc symbols]
A -->|CGO_ENABLED=1| C[动态链接<br>libc symbols in .dynsym]
B --> D[nm -D → empty]
C --> E[nm -D → getaddrinfo, pthread_create...]
4.2 cgo调用C库时-D_FORTIFY_SOURCE引发的运行时abort问题溯源
当 Go 程序通过 cgo 调用启用了 _FORTIFY_SOURCE=2 编译的 C 库时,若传入缓冲区长度不匹配(如 strcpy 目标空间小于源字符串长度),glibc 的 fortify 检查会触发 __fortify_fail_abort() 并调用 abort(),导致进程非预期终止。
触发条件示例
// test.c —— 以 -D_FORTIFY_SOURCE=2 编译
#include <string.h>
void unsafe_copy(char *dst) {
strcpy(dst, "hello world!"); // dst 若为 char[5],则触发 abort
}
strcpy在_FORTIFY_SOURCE=2下被重定义为__strcpy_chk,后者在编译期推导dst大小;若推导值(如sizeof(dst)误为指针大小)或运行时显式传入的__bos(dst)小于实际需求,即调用__fortify_fail。
常见根因归类
- ✅ cgo 中 C 字符串目标缓冲区未显式声明长度(如
C.CString返回指针,无 size 信息) - ✅ Go 分配的
C.char数组未对齐 glibc 的__builtin_object_size推断逻辑 - ❌ 静态链接 musl libc(无 fortify 实现)可绕过,但牺牲安全边界
| 环境变量 | 影响 |
|---|---|
GODEBUG=cgocheck=0 |
关闭 cgo 指针检查,不抑制 fortify |
LD_PRELOAD= |
无法绕过 __fortify_fail_abort |
graph TD
A[cgo 调用 C 函数] --> B{glibc 检测 __bos(dst) < len(src)?}
B -->|是| C[__fortify_fail_abort → abort]
B -->|否| D[正常执行]
4.3 构建阶段-D_GLIBCXX_USE_CXX11_ABI=0兼容性验证与ABI冲突检测
当混合链接 C++11 ABI(_GLIBCXX_USE_CXX11_ABI=1)与旧 ABI(=0)编译的库时,符号名修饰(name mangling)差异将引发 undefined reference 或运行时崩溃。
常见冲突信号
- 链接错误中出现类似
std::string::_M_rep() const@GLIBCXX_3.4.21与std::string::_M_rep() const@GLIBCXX_3.4并存 nm -C libfoo.a | grep 'basic_string'显示两套符号共存
ABI一致性检查脚本
# 检测目标文件使用的 ABI 版本
readelf -Ws $(find . -name "*.o" -o -name "*.so" | head -5) 2>/dev/null | \
awk '/_ZNSs/ {print $NF}' | sort | uniq -c | sed 's/^ *//'
逻辑说明:
_ZNSs是std::string的典型 mangled 前缀;readelf -Ws提取符号表;sort | uniq -c统计不同 ABI 符号出现频次。若输出含多行计数(如1 ..._3.4.21和1 ..._3.4),即存在 ABI 混用。
兼容性验证矩阵
| 组件 | 编译时 -D_GLIBCXX_USE_CXX11_ABI=0 |
运行时 libstdc++.so.6 版本 |
|---|---|---|
| Python 扩展 | 必须启用 | ≥ GCC 5.1(含旧 ABI 支持) |
| 第三方静态库 | 必须匹配主工程设置 | 不可晚于构建该库的 GCC 版本 |
graph TD
A[源码编译] -->|GCC 7+ 默认 ABI=1| B(符号 _ZNSsC1Ev)
A -->|加 -D_GLIBCXX_USE_CXX11_ABI=0| C(符号 _ZNSsC1Ev@GLIBCXX_3.4)
B --> D[链接失败:符号未定义]
C --> E[ABI 对齐,链接成功]
4.4 综合strace调试模板:捕获openat、mmap、statx等关键系统调用链
当排查文件访问异常或内存映射失败时,需联动观察文件打开、元数据获取与映射行为。以下为高保真调试模板:
strace -e trace=openat,mmap,statx,close,read -f -s 256 -o trace.log ./app arg1
-e trace=...精确过滤目标系统调用,避免噪声干扰-f跟踪子进程(如 execve 启动的 helper)-s 256防止路径截断(默认仅32字节)-o输出结构化日志便于 grep/awk 分析
常见调用链模式
openat(AT_FDCWD, "/etc/config.json", ...) → statx(...) → mmap(...)- 失败时
statx返回-1 ENOENT常早于mmap的EINVAL
关键参数语义对照表
| 系统调用 | 核心参数示例 | 诊断意义 |
|---|---|---|
openat |
dirfd=AT_FDCWD, flags=O_RDONLY |
检查路径解析起点与权限 |
statx |
mask=STATX_BASIC_STATS |
验证文件存在性、大小、mtime一致性 |
mmap |
prot=PROT_READ, flags=MAP_PRIVATE |
判断映射类型是否匹配 openat 权限 |
graph TD
A[openat] --> B{成功?}
B -->|yes| C[statx]
B -->|no| D[检查路径/权限]
C --> E{size > 0?}
E -->|yes| F[mmap]
E -->|no| G[空文件或 trunc 未生效]
第五章:构建健壮Go二进制的工程化建议与未来演进方向
构建可复现的二进制分发流水线
在生产环境中,我们为某金融风控服务构建了基于GitHub Actions + Docker Buildx的多平台交叉编译流水线。通过固定GOCACHE路径、启用-trimpath、锁定go.mod哈希值,并将所有构建步骤容器化(使用golang:1.22-alpine基础镜像),实现了从main分支推送后3分钟内生成Linux/amd64、Linux/arm64、Darwin/x86_64三套校验和一致的二进制包。关键配置如下:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -buildid=" -o ./bin/riskd-linux-amd64 .
静态链接与符号剥离的权衡实践
某边缘AI推理网关因依赖glibc版本冲突导致在CentOS 7节点启动失败。我们改用CGO_ENABLED=0并引入-ldflags="-linkmode external -extldflags '-static'"(需额外安装musl-gcc),但发现net包DNS解析失效。最终采用折中方案:仅对crypto/x509等敏感模块启用-tags netgo,其余保留动态链接,并通过strip --strip-unneeded减少体积37%。
运行时可观测性嵌入规范
所有二进制默认集成pprof(绑定/debug/pprof)与结构化日志(使用zerolog),且强制要求-gcflags="all=-l"禁用内联以保障堆栈可读性。在Kubernetes部署中,通过livenessProbe调用/healthz?full=1端点验证goroutine数阈值(>5000触发重启),该机制在一次内存泄漏事故中提前23分钟捕获异常。
模块依赖图谱治理
我们使用go mod graph | grep -v 'golang.org' | awk '{print $1,$2}' | sort -u > deps.dot生成依赖关系图,并用Mermaid可视化关键路径:
graph LR
A[auth-service] --> B[github.com/segmentio/kafka-go]
A --> C[cloud.google.com/go/storage]
B --> D[golang.org/x/exp/slog]
C --> E[google.golang.org/api/option]
通过定期扫描go list -deps -f '{{if not .Standard}}{{.ImportPath}}{{end}}' ./... | grep -E "^(github\.com|golang\.org)" | sort -u识别非标准依赖,将第三方SDK替换为轻量级替代品(如用minio/minio-go替代aws-sdk-go),使二进制体积从42MB降至11MB。
安全加固落地清单
- 所有生产构建启用
-buildmode=pie生成位置无关可执行文件 - 使用
go version -m ./binary验证go.sum哈希一致性 - 在CI阶段运行
govulncheck ./...并阻断CVE-2023-45858等高危漏洞 - 通过
objdump -T ./binary | grep -E "(malloc|free|memcpy)"确认无危险符号残留
Go工具链演进追踪机制
团队建立自动化脚本每日拉取https://go.dev/dl/最新版本JSON,对比go env GOROOT版本号,当检测到go1.23beta1发布时自动触发兼容性测试矩阵(覆盖-gcflags="-d=checkptr"、-buildvcs=false等新特性)。最近一次升级中发现embed.FS在//go:embed注释含空格时解析失败,已向golang/go提交issue #62107。
持续交付流程中,每个二进制均附加SBOM(Software Bill of Materials)元数据,通过syft ./binary -o cyclonedx-json > sbom.cdx.json生成,并由Sigstore Cosign签名后上传至私有Harbor仓库。
