wsl

install

https://aka.ms/wsl2kernel

https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi

1
2
3
4
5
6
Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart
Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart

wsl --list --online

wsl.exe --install Ubuntu-22.04

原理

wsl1

subsystem

NT 架构中有 subsystem 概念,每个 subsystem 针对一个平台,ntdll.dll 是所有 subsystem 的基础

或者说 ntdll.dll 统一提供 NT 系统的 API 接口,subsystem 为各个平台的应用程序提供包装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// winnt.h subsystem 定义
#define IMAGE_SUBSYSTEM_UNKNOWN 0
#define IMAGE_SUBSYSTEM_NATIVE 1
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3
#define IMAGE_SUBSYSTEM_OS2_CUI 5
#define IMAGE_SUBSYSTEM_POSIX_CUI 7
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9
#define IMAGE_SUBSYSTEM_EFI_APPLICATION 10
#define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER 11
#define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER 12
#define IMAGE_SUBSYSTEM_EFI_ROM 13
#define IMAGE_SUBSYSTEM_XBOX 14
#define IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION 16

用户态看起来很底层的东西,例如 Win32 subsystem 的核心:kernel32.dll、user32.dll、gdi32.dll,基本上只是 ntdll.dll 的一个包装

而 ntdll.dll 包装了从用户态到核心态的 system call,也称作“Native System Service”

用户态不能访问核心态的任何函数和变量,所以 system call 不同于一般的 API 调用。system call 可以被看作:将要调用的功能 ID 放到 eax,然后执行 INT 2e ntdll.dll 通过 system call 使用核心态的 ntoskrnl.exe 和 win32k.sys 提供的功能

ntoskrnl.exe 被尊称为“Executive”,可以看作是 NT 的大脑级模块。win32k.sys 提供 NT 图形库接口的 API

WSL Components

  1. 用户态会话管理服务处理 Linux 实例的生命周期
  2. Pico provider drivers (lxss.sys, lxcore.sys)“翻译”系统调用,以模拟 Linux 内核
  3. Pico 进程管理原生的用户态 Linux(比如/bin/bash)

Windows 内核引入 Pico 进程和 Pico 驱动,未经修改的 Linux 二进制文件放置于 Pico 进程中,把 Linux 系统调用直接导入 Windows 内核中。lxss.sys, lxcore.sys 驱动将 Linux 系统调用翻译为 NT APIs,来模拟 Linux 内核

Pico process 基于 Minimal process 原理,但是映射专属的 kernel driver,不受限于原本的 NT process 一样要许多必要信息,能有效利用剩余的 user mode address spac

pico process 里一开始不会含有任何必要信息,不由 NT kernel 处理,交由其 extension 的 driver 来处理,所以这个 driver 做了许多的安全防护以确保有安全的 pico process Pico process 之间的沟通除了透过 file,signal 部分是透过修改 Asynchronous Procedure Call (APC) 来仿真 Linux,当发生 exception 则会先由 NT kernel 拦截再转交给 driver 执行时 driver 会分配给 process 一个 Virtual Dynamic Shared Object (VDSO) 以及些许的必要信息来初始化,VDSO 如同 NTDLL,让 process 知道如何链接至 kernel,其他信息像是内存分配等都是在 user mode 完成,Linux 的 ELF 即是 load 到 pico process,由 driver 负责处理其产生的 syscalls

System Calls

一般 syscall 会根据 OS 和处理器的架构有所区别

user mode 和 kernel mode 主要通过 Application Binary Interface (ABI) 来沟通,一个 syscall 会经过以下步骤:

  1. Marshall parameters – user mode puts the syscall parameters and number at locations defined by the ABI.
  2. Special instruction – user mode uses a special processor instruction to transition to kernel mode for the syscall.
  3. Handle the return – after the syscall is serviced, the kernel uses a special processor instruction to return to user mode and user mode checks the return value.

Linux 和 Windows 都遵守上述步骤,但彼此 ABI 差异导致不兼容,即便两者有相同的 ABI,也并非所有的 syscalls 都是可以直接映射的。Linux fork、open 和 kill,Windows 则有 NtCreateProcess、NtOpenFile 和 NtTerminateProcess

Linux (ls) 和 Window (DIR) C 简化指令:

Linux:Result = syscall(__NR_getdents64, Fd, Buffer, sizeof(Buffer));

  • 1~4: Marshall parameters: C 通过 rax 知道 syscall 的 number,再分别把 parameters 放入 registers (rdi, rsi, rdx)
  • 5: Special instruction: syscall 这个 instruction 让 CPU ring transition 进入 kernel,并执行 kernel 的 syscall dispatcher 以及建构映射的环境,同时将 user registers 纪录在 ABI 里,让 user 和 kernel 切换时能维持原本的状态
  • 6: Handle the return: 回复 user registers,将回传值放入 rax,同时用 special instruction (通常是 sysret 或 iretq) 通知 CPU ring transition 回到 user mode

Windows:Status = NtQueryDirectoryFile(Foo, Bar, Baz);

  • 1~4: ABI 的差异导致不同的 registers (rcx, rdx, r8)。
  • 5: kernel 在此并不会纪录 volatile registers,在 syscall 之前 compiler 会先把需要的 registers 纪录下来,省略额外的 instructions
  • 6: 回传会以 NTSTATUS 形式,失败为负,有别于 Linux 会落在特定范围

WSL 透过 pico process 和 driver 来处理 Linux syscall,driver 实际上不包含任何 Linux 的 code,而是作为一个 Linux 兼容的接口

  1. 当 NT kernel 可以透过检查数据结构的不同,得知指令来自 pico process
  2. 因为 NT kernel 看不懂这些 syscall,便会将信息给 pico driver 处理
  3. Pico driver 会根据 Linux syscall 的调用逻辑,协助转译这些 syscall
  4. 处理完后会回复寄存器的状态,并跳回 user mode

FileSystem

Linux 用 Virtual File System (VFS) ,提供各文件系统 (ext4、rfs、FAT 等) 挂载 VFS 不直接处理文件格式,而是规定这些处理请求的接口及操作语意,然后交由真实的文件系统处理,为了管理这些文件系统,VFS 使用(inode、directory entriy 和 file 等)数据结构 来记录

WSL 添加了 VFS 组件来仿真 Linux 的文档系统,程序产生的 syscall 会先进到 syscall layer,但一般碰到文件处理相关的 syscall 会直接交由 VFS 处理。VFS 先从 directory entry cache 检查,若不在就由底下的 file sysyem plugins 来新建 inode,VFS 透过 inode 来创建 file 物件并回传 file descriptor

WSL 的 VFS 提供了不少 file system plugins,VolFs 和 DrvFs 用來表示 disk file,TmpFs 用來表示 in-memory file,剩下的表示 pseudo file

  • VolFS 用来完整支持 Linux 文档系统

  • DrvFs 达到与 Windows 之间的互通性

    mount 命令可以看到,它实际上是一个 9pfs

    9pfs 类似 NFS,是一个远程的文件系统访问协议,只不过不走网络,而是走的 virtio(trans=virtio),从 PCIe 总线上也能看到一堆 virtio 设备:lspci

这些 plugins 仍是基于 NTFS 去实现

wsl2

WSL1 实现的是 proxy kernel 模式,虽然跑的是 linux 程序,但其实还是 windows 内核,中间包了一层 syscall 的转换,没有开一个虚拟机。这个套路和 MinGW/Cygwin 类似,只不过是在不同层次上做的 syscall 转换

WSL2 回归传统的虚拟机模式,基于 Hyper-V 开了虚拟机,在虚拟机里跑一份 Linux 内核,有微软自己的一些修改, bootloader,虚拟机启动的流程都被隐藏起来了,这里就有很多可以做骚操作的地方了,例如植入一些程序,提前做一些配置等

运行 Windows 程序

WSL2 里运行 Windows 程序通过 binfmt_misc 实现

1
2
3
4
5
6
cat /proc/sys/fs/binfmt_misc/WSLInterop
enabled
interpreter /init
flags: PF
offset 0
magic 4d5a

Linux 尝试执行 PE 文件的时候,匹配上 magic 4d5aMZ),用 /init 去执行它。之后就由 /init 和 Windows 进行通信,把命令和参数传过去,最后在 Windows 上执行

/init 则是一个 WSL2 自带的程序:它会作为 WSL2 内部的 init 程序,也就是 PID 1。同时它会跑多个进程,虽然都是用同一个可执行文件

WSL2 可以打开 systemd,见 Advanced settings configuration in WS,此时 PID 1 是 systemd,而 PID 2 还有其他几个进程就是 /init:可以猜测,启动过程中,首先启动还是的 /init,然后 PID 1 的 /init 调用 exec 切换到 systemd PID 1,其余进程继续执行

kernel 和 initramfs

dmesg,看 Linux 启动 cmdline

1
[    0.000000] Command line: initrd=\initrd.img WSL_ROOT_INIT=1 panic=-1 nr_cpus=20 bonding.max_bonds=0 dummy.numdummies=0 fb_tunnels=none swiotlb=force cgroup_no_v1=all console=hvc0 debug pty.legacy_count=0

initrd.img 可以在 C:\Windows\System32\lxss\tools 下面找到,也可以从 WSL 安装包 中解包出来。进一步可以发现 initrd.img 内部只有一个 /init 文件:

1
2
3
$ cpio -itv < initrd.img
-rwxrwxrwx   0 root     root      1978872 Apr 20 06:55 init
3866 blocks

也就是说所有启动以后的准备工作都在这个 init 程序里做完了

/init

WSL2 文件系统内,有多个到 /init 的符号链接:

1
2
/usr/bin/wslpath -> /init
/usr/sbin/mount.drvfs -> /init

plan9,其实也是 /init 去执行的,说明 /init 内部会有逻辑,去判断 argv[0] 是什么,然后进行相应的操作

1
wslpath

WSL 2.0.0 引入 mirrored networking mode

大概预测一下它的工作模式:观测 Linux 上的 bind 系统调用,如果发现有进程要 bind 端口,就去 Windows 上也分配同一个端口,然后启动一个端口转发程序,让 Windows 上的程序可以访问到 Linux 里面的服务

kernel

WSL2 打了一些自己的 patch,涵盖:

  • 内存相关:memory-reclaim,page-reporting
  • WSLg 相关:Hyper-V vGPU
  • ARM64 相关:Hyper-V 支持

ref

https://learn.microsoft.com/zh-cn/windows/wsl/wsl-config