TL;DR
WSL2 启用 systemd 后,所有 Windows 可执行文件都会报
cannot execute binary file: Exec format error。
原因:WSL 在 init 阶段向 binfmt_misc 注册 WSLInterop 条目,
内核据此识别 PE 文件头(MZ)并交由 /init 执行。
而 systemd=true 引入的 systemd-binfmt.service 会在 ExecStop 中以 --unregister
注销全部条目,ExecStart 又只从 /etc/binfmt.d、/usr/lib/binfmt.d 重新加载——
WSLInterop 不在其中,注销后无从恢复。
解决:将条目交由 systemd-binfmt 自身管理,使注销与重注册同源。
sudo mkdir -p /etc/binfmt.d
echo ':WSLInterop:M::MZ::/init:PF' | sudo tee /etc/binfmt.d/WSLInterop.conf > /dev/null
sudo systemctl restart systemd-binfmt.service验证,应输出 enabled 与 interpreter /init:
cat /proc/sys/fs/binfmt_misc/WSLInterop完整排查过程
这是一次与 Claude Code(Anthropic CLI)对话排查问题的完整记录。
从一个简单的 PATH 问题,一路追踪到 systemd 与 WSL interop 的已知冲突。
环境
- WSL2 (Arch Linux)
- Shell: Zsh
- VS Code 安装在 Windows 侧 D 盘
/etc/wsl.conf配置如下:
[boot]
systemd=true
[network]
hostname=arch
[user]
default=kaifeng
[interop]
enabled=true
appendWindowsPath=false问题:code 命令找不到
在 WSL 中执行 code . 直接报 command not found。
原因很直接——appendWindowsPath=false 禁止了 Windows PATH 注入到 WSL 的 $PATH 中,
而 code 命令实际是 Windows 侧的可执行文件。这个配置是我故意关掉的,为了保持 Linux PATH 干净。
第一次尝试:软链接
既然不想污染 PATH,那创建一个软链接应该是最干净的方案。先确认 VS Code 的实际路径:
$ ls /mnt/d/Microsoft/Microsoft\ VS\ Code/bin
code code.cmd code-tunnel.exe创建软链接到已经在 $PATH 中的 ~/.local/bin:
ln -s "/mnt/d/Microsoft/Microsoft VS Code/bin/code" ~/.local/bin/code新问题:Exec format error
软链接创建成功,但执行时翻车了:
$ code .
/home/kaifeng/.local/bin/code: line 62: /mnt/d/Microsoft/Microsoft VS Code/Code.exe: cannot execute binary file: Exec format errorcode 脚本本身能运行(它是个 shell 脚本),但它内部调用 Code.exe 时失败了。
WSL 竟然无法执行 Windows .exe 文件?但 interop 明明是 enabled=true 啊。
根因:systemd 重置了 binfmt_misc
这是一个已知问题:systemd=true 会重置 binfmt_misc 注册表,
导致 WSL 用于识别和执行 Windows .exe 的 interop 入口丢失。
具体来说,WSL 的 init 进程启动时会向 /proc/sys/fs/binfmt_misc/ 注册一个 WSLInterop 条目,
让内核能识别 MZ(PE)格式的 Windows 可执行文件。但当 systemd=true 启用后,
systemd-binfmt.service 会接管 binfmt_misc,在此过程中清掉了 WSL 注册的条目。
内核不再认识 .exe 文件,所有 Windows 二进制调用都会报 Exec format error。
注意这条链路里没有 appendWindowsPath 的位置。它只决定 Windows 的目录会不会被追加进 $PATH,
管的是「找不找得到 code」;而 Exec format error 发生在文件已经被找到、打开之后,
是内核不认识它的格式。所以只开了 systemd=true、appendWindowsPath 保持默认的人一样会中招,
只不过症状是「本来好好的 .exe 突然集体失效」,而不是像我这样从 command not found 查起。
这个问题在 GitHub 上有大量讨论:
- microsoft/WSL#8843 — 最早的报告,启用 systemd 后无法执行 Windows 二进制文件(2022-09)
- microsoft/WSL#8952 —
binfmt_misc/WSLInterop条目丢失(2022-10) - microsoft/WSL#10363 — 与本文相同的场景,WSL2 无法启动 VS Code(2023-08)
- microsoft/vscode#189694 — VS Code 仓库侧的相关报告
验证一下:
$ cat /proc/sys/fs/binfmt_misc/WSLInterop
# 文件不存在或显示 disabled果然如此。
修复
手动重新注册 interop:
sudo sh -c 'echo :WSLInterop:M::MZ::/init:PF > /proc/sys/fs/binfmt_misc/register'执行后 code . 恢复正常。
持久化:systemd service
Warning
这一节的方案有缺陷:它能撑过重启,但撑不过运行期。正确做法见下文《后续》一节。
每次重启 WSL 都要手动注册太麻烦,创建一个 systemd service 来自动处理:
sudo tee /etc/systemd/system/wsl-binfmt.service > /dev/null << 'EOF'
[Unit]
Description=Register WSL interop binfmt
After=systemd-binfmt.service
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo :WSLInterop:M::MZ::/init:PF > /proc/sys/fs/binfmt_misc/register'
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable wsl-binfmt.service后续:修好了,一段时间后又坏了
补记。上面那个
wsl-binfmt.service方案撑了一阵,然后code .又开始报Exec format error。
先看 service 状态:
$ systemctl status wsl-binfmt.service
○ wsl-binfmt.service - Register WSL interop binfmt
Loaded: loaded (/etc/systemd/system/wsl-binfmt.service; enabled)
Active: inactive (dead) since Fri 2026-08-28 00:49:29 CST
Main PID: 442781 (code=exited, status=0/SUCCESS)inactive (dead) 看着吓人,其实是障眼法:Type=oneshot 没写 RemainAfterExit=yes 时,跑完就是这个状态,
status=0/SUCCESS 恰恰说明开机那次注册是成功的。
真正该看的是 binfmt 表本身:
$ cat /proc/sys/fs/binfmt_misc/status
enabled
$ cat /proc/sys/fs/binfmt_misc/WSLInterop
cat: /proc/sys/fs/binfmt_misc/WSLInterop: No such file or directorybinfmt_misc 是 enabled 的,但 WSLInterop 条目不见了。
开机时注册成功,之后在某个时间点被人清掉了。
谁清的:systemd-binfmt 的 ExecStop
翻一眼 systemd 自带的那个 service:
# /usr/lib/systemd/system/systemd-binfmt.service
[Unit]
After=proc-sys-fs-binfmt_misc.automount
After=proc-sys-fs-binfmt_misc.mount
ConditionPathIsMountPoint=/proc/sys/fs/binfmt_misc
ConditionDirectoryNotEmpty=|/usr/lib/binfmt.d
ConditionDirectoryNotEmpty=|/etc/binfmt.d
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/lib/systemd/systemd-binfmt
ExecStop=/usr/lib/systemd/systemd-binfmt --unregisterExecStop 那行就是答案。systemd-binfmt --unregister 往 /proc/sys/fs/binfmt_misc/status 写 -1,
清空整张表——不管条目是谁注册的。
于是任何一次 systemctl restart systemd-binfmt 都是这个流程:
- ExecStop:清空全表,
WSLInterop阵亡 - ExecStart:把
/etc/binfmt.d、/usr/lib/binfmt.d里的规则装回去
第 2 步只认那两个目录。而我的 WSLInterop 是在自己的 service 里手写 echo 注册的,不在目录里,
所以有去无回。
更糟的是这个 restart 根本不需要我手动触发——Arch 的 pacman hook 在升级任何附带 binfmt.d
配置的包时就会自动执行它。说到底,跟最初把 WSLInterop 冲掉的是同一个机制,
只不过第一次发生在开机,这次发生在运行期。
还有第二条路径:/proc/sys/fs/binfmt_misc 是通过 proc-sys-fs-binfmt_misc.automount 挂上的。
挂载点空闲过期被卸载、之后再被访问触发重挂,拿到的是一张全新的空表。
而 systemd-binfmt.service 因为 RemainAfterExit=yes 已经处于 active,不会再跑一次。
无论哪条路径,我那个 WantedBy=multi-user.target 的 oneshot 都只在开机跑一次,
对运行期清表毫无防御能力。
正确的做法:别自己开 service
问题的本质是:清表的人和注册的人不是同一个,所以两者永远不同步。
那就别跟 systemd-binfmt 抢,把规则交给它自己管——放进 /etc/binfmt.d/:
sudo mkdir -p /etc/binfmt.d
sudo tee /etc/binfmt.d/WSLInterop.conf > /dev/null <<'EOF'
:WSLInterop:M::MZ::/init:PF
EOF
sudo systemctl restart systemd-binfmt.service配置文件的内容就是原来 echo 进 register 的那一行,一字不差(binfmt.d(5) 用的就是内核
register 接口的格式)。这样“清表”和“重新注册”变成同一个动作的两半,永远同步:
它每次 restart 清完表,紧接着就会把 WSLInterop 一并装回去。
验证:
$ cat /proc/sys/fs/binfmt_misc/WSLInterop
enabled
interpreter /init
flags: PF
offset 0
magic 4d5amagic 4d5a 就是 MZ 的十六进制,PE 文件头。
顺带一个容易忽略的收益:注意前面那串 ConditionDirectoryNotEmpty=|...——
所有 binfmt.d 目录都为空时,systemd-binfmt.service 会直接跳过不执行。
放了这个文件之后条件才成立,它每次开机才会真正跑起来。
最后把旧的 service 删掉。它现在不只是多余:如果它排在 systemd-binfmt 之后跑,
echo 会因为条目已存在而拿到 EEXIST,service 变成 failed。
sudo systemctl disable --now wsl-binfmt.service
sudo rm /etc/systemd/system/wsl-binfmt.service
sudo systemctl daemon-reload如果还复发
针对 automount 重挂那条路径,可以再加个 drop-in,让挂载点重挂时把 systemd-binfmt 一并带起来:
sudo mkdir -p /etc/systemd/system/systemd-binfmt.service.d
sudo tee /etc/systemd/system/systemd-binfmt.service.d/wsl.conf > /dev/null <<'EOF'
[Unit]
# binfmt_misc 被重新挂载时跟着重启,把规则装回去
PartOf=proc-sys-fs-binfmt_misc.mount
EOF
sudo systemctl daemon-reload想确认某次失效具体是哪条路径触发的,对一下时间线就行:
journalctl -u systemd-binfmt.service --no-pager
journalctl -u proc-sys-fs-binfmt_misc.mount --no-pager
grep -E 'upgraded|installed' /var/log/pacman.log | tail -30如果 pacman 的升级时间点和失效时间吻合,就是 ExecStop 清表那条。
小结
| 表面问题 | 实际原因 |
|---|---|
code 命令找不到 | appendWindowsPath=false 屏蔽了 Windows PATH |
| 软链接后 Exec format error | systemd=true 重置了 binfmt_misc,WSL 无法识别 .exe |
| 修好后过一阵又坏 | systemd-binfmt 的 ExecStop --unregister 会清空全表,只在开机注册的 oneshot 管不到运行期 |
一个看似简单的 PATH 问题,背后其实是两个互不相干的坑叠在了一起:appendWindowsPath=false
让 code 找不到,systemd=true 让找到之后也执行不了。两者没有因果关系,也不需要同时满足——
只开 systemd=true 的人照样会踩到后一个,而且症状更隐蔽:所有 Windows 程序一起失效。
而修复它的经验是:当某个系统组件会周期性地重置某项状态时,不要另起一个 service 去和它赛跑, 而是把你的配置塞进它自己的配置目录,让它替你恢复。 前者是竞态,后者是同步。
参考
- microsoft/WSL#8843 - Unable to Execute Windows Binary when systemd enabled
- microsoft/WSL#8952 - WSL2 cannot run .exe files: exec format error
- microsoft/WSL#10363 - WSL2 cannot start VS Code
- microsoft/vscode#189694 - WSL2: Code.exe Exec format error
binfmt.d(5)— 配置文件格式systemd-binfmt.service(8)- Kernel docs - binfmt_misc —
register接口与各 flag 的含义