k4if3ngnotes, projects & unfinished thoughts
凯风 的头像 凯风 Kaifeng

决心不过是记忆的奴隶

Writing / wsl

WSL2 启用 systemd 后无法执行 .exe:Exec format error

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 自身管理,使注销与重注册同源。

Bash
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

验证,应输出 enabledinterpreter /init

Bash
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 配置如下:
ini
[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 的实际路径:

Bash
$ ls /mnt/d/Microsoft/Microsoft\ VS\ Code/bin
code  code.cmd  code-tunnel.exe

创建软链接到已经在 $PATH 中的 ~/.local/bin

Bash
ln -s "/mnt/d/Microsoft/Microsoft VS Code/bin/code" ~/.local/bin/code

新问题:Exec format error

软链接创建成功,但执行时翻车了:

plaintext
$ code .
/home/kaifeng/.local/bin/code: line 62: /mnt/d/Microsoft/Microsoft VS Code/Code.exe: cannot execute binary file: Exec format error

code 脚本本身能运行(它是个 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=trueappendWindowsPath 保持默认的人一样会中招, 只不过症状是「本来好好的 .exe 突然集体失效」,而不是像我这样从 command not found 查起。

这个问题在 GitHub 上有大量讨论:

验证一下:

Bash
$ cat /proc/sys/fs/binfmt_misc/WSLInterop
# 文件不存在或显示 disabled

果然如此。

修复

手动重新注册 interop:

Bash
sudo sh -c 'echo :WSLInterop:M::MZ::/init:PF > /proc/sys/fs/binfmt_misc/register'

执行后 code . 恢复正常。

持久化:systemd service

Warning

这一节的方案有缺陷:它能撑过重启,但撑不过运行期。正确做法见下文《后续》一节。

每次重启 WSL 都要手动注册太麻烦,创建一个 systemd service 来自动处理:

Bash
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 状态:

Bash
$ 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 表本身:

Bash
$ 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 directory

binfmt_misc 是 enabled 的,但 WSLInterop 条目不见了。 开机时注册成功,之后在某个时间点被人清掉了。

谁清的:systemd-binfmt 的 ExecStop

翻一眼 systemd 自带的那个 service:

ini
# /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 --unregister

ExecStop 那行就是答案。systemd-binfmt --unregister/proc/sys/fs/binfmt_misc/status-1清空整张表——不管条目是谁注册的。

于是任何一次 systemctl restart systemd-binfmt 都是这个流程:

  1. ExecStop:清空全表,WSLInterop 阵亡
  2. 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/

Bash
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

配置文件的内容就是原来 echoregister 的那一行,一字不差(binfmt.d(5) 用的就是内核 register 接口的格式)。这样“清表”和“重新注册”变成同一个动作的两半,永远同步: 它每次 restart 清完表,紧接着就会把 WSLInterop 一并装回去。

验证:

Bash
$ cat /proc/sys/fs/binfmt_misc/WSLInterop
enabled
interpreter /init
flags: PF
offset 0
magic 4d5a

magic 4d5a 就是 MZ 的十六进制,PE 文件头。

顺带一个容易忽略的收益:注意前面那串 ConditionDirectoryNotEmpty=|...—— 所有 binfmt.d 目录都为空时,systemd-binfmt.service 会直接跳过不执行。 放了这个文件之后条件才成立,它每次开机才会真正跑起来。

最后把旧的 service 删掉。它现在不只是多余:如果它排在 systemd-binfmt 之后跑, echo 会因为条目已存在而拿到 EEXIST,service 变成 failed。

Bash
sudo systemctl disable --now wsl-binfmt.service
sudo rm /etc/systemd/system/wsl-binfmt.service
sudo systemctl daemon-reload

如果还复发

针对 automount 重挂那条路径,可以再加个 drop-in,让挂载点重挂时把 systemd-binfmt 一并带起来:

Bash
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

想确认某次失效具体是哪条路径触发的,对一下时间线就行:

Bash
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 errorsystemd=true 重置了 binfmt_misc,WSL 无法识别 .exe
修好后过一阵又坏systemd-binfmtExecStop --unregister 会清空全表,只在开机注册的 oneshot 管不到运行期

一个看似简单的 PATH 问题,背后其实是两个互不相干的坑叠在了一起:appendWindowsPath=falsecode 找不到,systemd=true 让找到之后也执行不了。两者没有因果关系,也不需要同时满足—— 只开 systemd=true 的人照样会踩到后一个,而且症状更隐蔽:所有 Windows 程序一起失效。

而修复它的经验是:当某个系统组件会周期性地重置某项状态时,不要另起一个 service 去和它赛跑, 而是把你的配置塞进它自己的配置目录,让它替你恢复。 前者是竞态,后者是同步。

参考