make 构建输出与源码编码乱码 locale 速查
更新时间:2026-10-24。FAQ 速查页,面向在 Linux 上用 GNU make 构建的同学。make 构建输出乱码有两种完全不同的面孔:一种是「显示层乱码」——编译照常成功,但终端/日志里的中文变成
???、菱形问号或测试这类火星文;另一种是「编译层乱码」——make 驱动 gcc 时直接被源文件里的不可见字符(BOM、CRLF、GBK 字节)打翻,报stray '\357' in program之类错误。两者病根都是同一件事:字节流与解读它的编码约定对不上。乱码故障最忌「改一下试试」式碰运气,先用file与hexdump看清字节再动手。如果是语法层面的解析报错而非编码问题,请转 Makefile missing separator 语法解析报错速查。
本文要回答的问题
- 报错里的
stray '\357'、stray '\273'这些八进制数字是什么? - 终端中文变
???是源码坏了还是终端坏了? - UTF-8 BOM 这三个字节为什么会把 gcc 打翻?
- Windows 拷来的代码为什么在 Linux 上编不过?
locale该怎么设,设了为什么没生效?
一、报错原文与典型现象
编译层乱码的典型报错(工程路径以 /proj/demo 占位)——文件开头三个 BOM 字节被 gcc 当成非法字符:
$ make
g++ -O2 -c src/main.cpp -o build/main.o
src/main.cpp:1:1: error: stray '\357' in program
1 | <EF><BB><BF>#include <cstdio>
| ^
src/main.cpp:1:2: error: stray '\273' in program
src/main.cpp:1:3: error: stray '\277' in program显示层乱码的典型长相——构建成功但输出没法看:
$ make test
./run_tests
????: ?? 42 / 42
(本应是「测试: 通过 42 / 42」)读懂现场抓住两件东西:字节真相与谁在解读。\357\273\277(八进制)就是十六进制 EF BB BF——UTF-8 BOM(字节顺序标记);\xxx 八进制转十六进制对不上任何合法字符时,gcc 只能报 stray。显示层乱码则用「三段链路」排查:文件实际编码(file src/main.cpp)→ 程序按什么编码读(locale 决定)→ 终端按什么编码显示(echo $LANG 与终端模拟器设置)。三段里任何一段错配都会乱码,乱码的长相还能反推错配位置:??? 是程序端转码失败丢弃,测试 是 UTF-8 字节被按 Latin-1 解读,菱形问号 `` 是终端端不认识字节序列。
二、病因处置速查表
| 报错现象 / 场景 | 可能原因 | 快速验证 | 处置 |
|---|---|---|---|
stray '\357' 在文件第 1 行第 1 列 | UTF-8 BOM(EF BB BF) | hexdump -C file | head -1 看开头三字节 | 存为无 BOM 的 UTF-8,编辑器设 UTF-8 no BOM |
stray 报错但位置在文件中段 | 源码里混入 GBK 中文(注释/字符串) | file file 看是否 Non-ISO extended-ASCII;iconv -f GBK -t UTF-8 试转 | 全文转 UTF-8:iconv -f GBK -t UTF-8 |
| Windows 来的 makefile 报 missing separator 或怪异错误 | CRLF 行尾,\r 粘在行尾 | file Makefile 显示 CRLF;cat -A 见 ^M$ | dos2unix 转 LF,git 配 .gitattributes 锁 LF |
编译通过但终端中文是 ??? | locale 是 POSIX/C,程序按 C locale 转码 | locale 看 LANG 是否空或为 C | 设 LANG=C.UTF-8 或 zh_CN.UTF-8,写入 shell 配置 |
| 终端显示菱形问号,日志文件里却是好的 | 终端模拟器编码设错(如 GBK 解读 UTF-8) | 换 UTF-8 终端或改设置后复看 | 终端字符集固定 UTF-8 |
| CI 日志乱码、本地终端正常 | CI 环境无 locale(容器镜像常见) | 容器里跑 locale -a | grep -i utf 是否为空 | 镜像装 locales 并设 LANG;或日志统一重定向后转码 |
| 错误信息里文件名乱码 | 源码文件名含非 ASCII 且 locale 是 C | ls 显示文件名是否为问号 | 文件名改 ASCII;或修正 locale 后重新挂载/传输 |
三、排查决策图
下面这张图按「乱码发生在哪一层」分岔:红是乱码入口与命中的根因,蓝是排查动作,绿是通过分支,橙是修复动作,紫是防复发沉淀。
读这张图抓一条主线:先分「编译层」还是「显示层」。编译层(gcc 报 stray)一定去看字节——hexdump -C 让 BOM、GBK、CRLF 全部现形,修法是统一转码;显示层(编译过了但没法读)走「文件编码 → locale → 终端」三段链路逐段核对,修法是统一 UTF-8 约定。两类修复都做完后重编一次确认:无 stray、无问号、日志与终端一致,才算闭环。
四、逐条排查展开
BOM 三字节是编译期乱码头号凶手。 UTF-8 规范里 BOM 可有可无,但 Windows 系编辑器(记事本、部分 IDE 默认配置)保存时必加 EF BB BF。gcc 把它当源码内容解析,于是 stray '\357'(357 八进制 = EF 十六进制)。现形与清除:
hexdump -C src/main.cpp | head -1
# 00000000 ef bb bf 23 69 6e 63 6c ... 开头 ef bb bf 即 BOM
sed -i '1s/^\xef\xbb\xbf//' src/main.cpp # 精准只删首行 BOM批量治理用编辑器或 find . -name '*.cpp' -exec sed -i '1s/^\xef\xbb\xbf//' {} +,之后把编辑器全局设为「UTF-8 无 BOM」。
GBK 混血的识别与转码。 老代码库常见一半 UTF-8 一半 GBK:file 对含 GBK 中文的文件会报 Non-ISO extended-ASCII text。gcc 对注释里的非法字节有时能忍、有时报 stray,字符串字面量里的则会在运行期变乱码。统一转码:
iconv -f GBK -t UTF-8 legacy.cpp -o legacy.cpp.utf8 && mv legacy.cpp.utf8 legacy.cpp转码前先确认源编码——把 UTF-8 文件误按 GBK 再转一次是不可逆的双重损坏,iconv 报错 illegal input sequence 时就停手,说明猜错了源编码。
CRLF 对 make 的伤害不止乱码。 Windows 行尾 CR LF 在 Linux 下让每行多一个 \r:makefile 的续行符变成 \+\r 直接失效(详见 Makefile missing separator 语法解析报错速查);shell 脚本的 shebang 变成 #!/bin/bash\r,报 bad interpreter。dos2unix 一把转,根治靠仓库 .gitattributes:* text=auto eol=lf 与 *.mk text eol=lf,Windows 贡献者 clone 下来也带不回 CRLF。
locale 三段式设置与「设了不生效」。 正确姿势三层齐全:
locale -a | grep -i utf # ① 系统得装了这个 locale
export LANG=C.UTF-8 # ② 环境变量指到它(C.UTF-8 兼容性最好)
locale # ③ 回读确认生效,全行都应是 C.UTF-8「设了不生效」的高发场景:容器镜像(debian slim 之类根本没装 locales 包,locale -a 一片空白,先 apt-get install locales 并 locale-gen);SSH 会话里客户端把本地 locale 转发过去而服务端没有(服务端 /etc/ssh/sshd_config 的 AcceptEnv LANG 或客户端不发);systemd 服务单元环境干净,要在 unit 里显式 Environment=LANG=C.UTF-8。
终端错配的快速鉴别。 日志文件用 cat 看是好的、屏幕上却是菱形问号——文件无罪,终端按错的字符集解读。ssh 客户端(Windows 上的老终端尤其多)字符集设成 GBK 就会这样,改成 UTF-8 立即恢复。反向情况(文件是 GBK、终端是 UTF-8)则显示为乱码块,这时转文件而不是改终端。
日志二次转码。 CI 抓日志时再过一道编码转换(某些构建系统会把输出按系统默认编码重编码),链路越长错配点越多。原则:构建产物日志全程 UTF-8 单编码,任何中间环节不做隐式转码;必须在 GBK 环境展示时在最后一步显式 iconv,并保留 UTF-8 原件。
五、规范做法与预防
- 仓库级锁死约定:
.gitattributes标text=auto eol=lf;.editorconfig统一charset = utf-8、end_of_line = lf、insert_final_newline = true,新文件从第一秒就是对的。 - CI 加编码巡检:一条
file扫描脚本拦住 CRLF、BOM、GBK 文件入库,比事后排查便宜得多。 - 容器镜像预装 locales:基础镜像里装好
locales并ENV LANG=C.UTF-8,应用层不再各自踩坑。 - 文件名与路径纯 ASCII:非 ASCII 文件名在跨平台、跨工具链传输中是乱码与构建失败的长期源头,团队约定禁用。
- 报错信息统一英文:CI 环境
LC_MESSAGES=C让 gcc/make 报错保持英文,既方便检索也让多语言团队读同一份日志。
这一篇在知识体系里的位置
本篇是 Makefile 排错速查簇的编码与 locale 篇,定位「字节与编码约定对不上」:CRLF 引发的 makefile 语法报错深入拆解见 Makefile missing separator 语法解析报错速查;编译器进程崩溃的看 make 编译器内部崩溃速查;构建卡死的看 make 构建卡死无响应速查;头文件问题的看 fatal error 头文件找不到速查;类型报错的看 error: cannot convert 类型不匹配速查;链接期问题的看 undefined reference 链接错误速查;权限问题的看 构建 Permission denied 报错速查;规则缺失的看 make: No rule to make target 报错速查;CMake 侧的看 CMake 找不到编译器速查。「hexdump 看字节、locale 三段核对」这一套对所有乱码类故障通用。
一句话总结
make 构建乱码先分编译层与显示层:gcc 报 stray '\357' 就 hexdump -C 看文件头——EF BB BF 是 BOM 用 sed 精准剥掉、中段非法字节是 GBK 混血用 iconv 转 UTF-8、makefile 怪异报错先 dos2unix 转 CRLF;编译通过但输出问号则走「file 看文件编码 → locale 看程序端 → 终端设置看显示端」三段核对,统一 LANG=C.UTF-8 并确认容器装了 locales。根治靠 .gitattributes 锁 LF、.editorconfig 锁无 BOM UTF-8、CI 加编码巡检,乱码从此不进仓库。