一个下午被两个假象骗了两次:构建脚本秒回,让我以为编译完成了;
strings | grep零命中,又让我以为二进制里根本没有我刚加的那行中文。两个坑单独看都不难,叠在一起就足够让人开始怀疑自己写的代码。
🧩 场景:确认新二进制里有没有那句话
给一个 Go 写的小服务加了一行中文日志,编译完想确认「跑起来的确实是新的二进制」。最直觉的验证方式是去二进制里翻这串字面量:
$ strings ./demo | grep -c "同步完成"
0
零。第一反应当然是:构建没生效,跑的还是旧的。于是我掉头去查构建脚本——方向就此错了半小时。
🔍 strings 默认只认 ASCII
真相是这条中文在二进制里,好好地待着。换个工具立刻就能看见:
$ grep -a -c "同步完成" ./demo
1
$ strings -e S ./demo | grep -c "同步完成"
1
GNU strings 默认的编码是 -e s,也就是「7 位 ASCII 的可打印字符序列」。UTF-8 的中文每个字节都 ≥ 0x80,在这个规则下统统算不可打印。于是中文不仅不会被输出,还会充当分隔符,把一条完整的日志切成两段。
拿一条混合串 [sync-worker] 同步完成 done=3 skipped=0 做对照,默认模式下它是这样出来的:
[sync-worker]
done=3 skipped=0
而 strings -e S(单字节 8 位编码)看到的是原样的一整条:
[sync-worker] 同步完成 done=3 skipped=0
这也解释了为什么我当时那么容易被骗:grep "sync-worker" 是有命中的,grep "done=3" 也有——ASCII 部分全都在,唯独我拿去当证据的那半句中文不见了。工具没坏,是我拿它做了它默认不做的事。
顺便提一个容易踩到的连带问题:strings 默认只输出长度 ≥ 4 的序列(-n 4)。被中文切碎之后的残片如果不足 4 个字符,会直接被丢掉,连碎片都看不到。
🧱 顺便说说 Go 二进制里的字符串长什么样
既然已经打开了,索性多看一眼。Go 不给字符串常量留结尾的 NUL——长度存在 header 里,字节紧挨着排。所以整块只读数据在 strings 眼里是一条条巨长的血肉模糊的东西,我这个只有一行代码的程序里,最长的一条有三千多个字符,全是互不相关的 runtime 报错首尾相接:
runqsteal: runq overflowdouble traceGCSweepStartbad use of trace.seqlock…
这带来一个实际影响:别指望靠 strings 的换行来判断字符串边界。想确认边界就用精确整串去匹配,顺手让 grep 报出字节偏移:
$ grep -abo "同步完成" ./demo
660537:同步完成
-b 给偏移,配合 xxd -s <offset> -l 64 就能直接去现场看上下文。
🛠 三种靠得住的验证方式
按省事程度排序:
grep -a:把二进制当文本按字节匹配,中文原样可搜,-c数次数、-o打印命中内容。验证「某个字面量在不在」这件事,这是最短路径。这里还有个小陷阱:不加-a的时候,GNU grep 匹配上了也不会把那一行打给你,只在 stderr 甩一句binary file matches。命中和没命中在屏幕上长得几乎一样,肉眼扫过去很容易读成「没有」——要么加-a,要么老老实实看退出码。strings -e S:仍然想要「提取所有字符串」的语义时用它,把编码从 7 位换成单字节 8 位,UTF-8 就完整了。- 别只用内容当证据:更可靠的是产物本身的指纹——
ls -l看 mtime 是不是这次构建的时间,sha256sum比对,或者干脆在构建时埋一个版本串:
go build -ldflags "-X main.buildID=$(date +%s)" -o demo .
有了这个,验证就变成 ./demo -version,不用再对二进制做考古。
⏱ 上游那个坑:秒回不等于干完了
回头说被我冤枉的构建脚本。它开头有这么一手,把自己丢到后台再立刻返回:
_BG=1 nohup bash "$0" "$@" &
echo "[build] 后台启动 PID=$!"
命令行秒回,实际上后台还在拉依赖、编三个二进制,前后几十秒。我在它返回的瞬间就去 grep 产物,于是拿到了一个「真·假阴性」——那一刻新二进制确实还没落盘。两个假象叠起来,指向了一个完全不存在的 bug。
对付异步脚本,别靠感觉,轮询等一个明确的完成信号:
for i in $(seq 1 10); do
sleep 7
grep -aq "同步完成" ./demo && { echo "已包含新字面量"; break; }
done
或者 tail -f 构建日志等那句「编译完成」。更好的做法是让脚本自己提供 --wait / 前台模式,把「什么时候算干完」写进接口,而不是留给使用者猜。
收束
两条经验,都不新,但值得贴在显示器边上:
- 工具的默认值是历史,不是常识。
strings的默认编码来自还没有 UTF-8 的年代,它没错,只是不为你今天的用法服务。 - 「没找到」有两种:真的没有,和你的工具看不见。 所以排查的第一步不是分析结论,而是自证工具可见性——先拿一条你百分之百确信存在的串做对照实验。这次只要顺手
grep一下同一行里的done=3,半小时就省下来了。
顺带一提:这个坑对任何非 ASCII 字面量都成立,日文、韩文、emoji、带重音的拉丁字母一样。如果你的项目里有中文提示语,把 strings | grep 从验证流程里删掉吧。
💬 评论
加载评论中...
发表评论