首页 > 代码库 > 在调试器里看Windows 10的Linux子系统

在调试器里看Windows 10的Linux子系统

Windows 10是微软第三代NT团队的力挽狂澜之作,大刀阔斧地改造革新,目标是重塑Windows(Reinvent Windows)!在众多新特征中,Linux环境子系统(WSL)无疑是最具开创性和最拉风的一个。

启用WSL

在2016年3月30日开幕的Build大会上,微软向广大开发者宣布Windows 10将支持Linux应用。在2016年4月7号推送的 Windows 10 build 14328 fast ring中首次包含了WSL。在Windows 10的周年更新(Anniversary Update)中包含了相对完整的Beta版本。

值得说明的是,不是所有的Windows 10系统都能运行WSL,要满足两个基本要求:64位和Build号不低于14393.0(笔者使用的两个版本分别是14393.1198和14393.479)。

在满足上述条件的Windows 10中,WSL也不是默认安装的,而是需要通过以下两个步骤来启用。

第一步是通过Settings -> Update and Security -> For developers打开开发者模式(参见图1)。 
技术分享

图1 打开开发者模式

 

第二步是通过“Turn Windows features on or off”打开WSL功能(图2)。 
技术分享

图2 打开WSL功能

 

点击图2所示的确定按钮后,Windows 10会安装WSL的组件,需要一点时间。安装完成后,要重启系统。重启后,在开始菜单区输入bash,如果看到图3所示的菜单项就代表WSL启用成功了。Bash是GNU旗下的命令处理器和外壳(shell)程序,包括Ubuntu在内的很多Linux发行版本和苹果macOS都使用它作为默认的控制台外壳和命令处理器。Windows 10中使用的就是Ubuntu版本的Bash。因此,WSL有时也被称为Bash on Ubuntu on Windows。

技术分享

图3 Bash on Ubuntu on Windows

 

启用WSL成功代表着在Windows系统中搭建好了运行Linux应用的基本环境,这个环境就是所谓的Windows Subsystem for Linux(WSL)。环境子系统是NT内核固有的机制,目的是可以在一个NT内核运行不同类型的应用程序。在支持WSL之前,有三种子系统:Windows子系统、OS/2子系统和POSIX子系统。

每个子系统通常都有一个子系统服务进程和一个内核态驱动,对于Windows子系统,分别是著名的CSRSS.exe和Win32K.sys。对于WSL,它的组成主要有以下几个部分:

  • 以系统服务形式运行的子系统服务进程LxssManager。在服务管理器里,可以看到这个服务。服务的描述信息很详实,不妨引用一下:LXSS Manager服务支持运行本机ELF二进制文件。该服务提供在Windows上运行ELF二进制文件所需的基础结构。如果停止或禁用该服务,这些二进制文件将不再运行。LxssManager的核心代码是一个DLL,位于system32\lxss子目录下。它使用svchost.exe作为宿主运行。 
    技术分享

    图4 WSL的子系统服务DLL

     

  • 运行在内核空间的Linux子系统驱动,有两个,一个文件名叫lxss.sys,另一个叫LxCore.sys,都位于system32\drivers目录中,图5是lxss驱动在注册表中的安装选项。 
    技术分享

    图5 Linux子系统驱动的注册表表项

     

  • 用于与Windows环境接口的Bash启动(Bash Launcher)程序bash.exe,位于system32目录下,图3中的菜单项指向的就是这个程序。
  • 一个用于管理和维护WSL的工具程序,名为LxRun.exe。

安装Ubuntu

有了上面的基本运行环境就可以运行Linux应用程序了么?不是的,还需要安装Linux系统,包括系统程序、库文件和必要的工具程序。不过不用担心,微软已经和Canonical(Ubuntu背后的公司)合作准备好了一个特殊版本的Ubuntu,称为Ubuntu On Windows(以下简称UoW)。第一次启动Bash Launcher时,它就会提示安装UoW,如图6所示。

技术分享

图6 安装Ubuntu On Windows

 

安装UoW后,便可以在当前用户的AppData\Local\lxss下看到Ubuntu的各个子目录和文件了,图7所示的便是根文件系统下的子目录和文件,可以看到很多熟悉的名字:tmp、boot、etc、home、sbin、bin、usr、var等。

技术分享

图7 磁盘上的Ubuntu根目录

 

安装其它应用

UoW里已经包含了很多常用的命令和工具,包括著名的软件包管理工具apt-get。所以很容易在UoW里安装其它软件包,只要使用以下这些命令就可以了。

sudo apt-get update
sudo apt-get install packagename
sudo apt-get remove packagename
sudo apt-cache search word

对老雷来说,最想安装的当然就是GDB,这只要执行sudo apt-get install gdb就可以了,安装过程如图8所示。

技术分享

图8 安装GDB

 

观察Linux实例的创建过程

做好以上准备工作后,我们的下一个目标就是在调试器里理解WSL的工作原理。首先我们要观察的是在WSL中启动Linux实例的过程。

在内核调试会话中对nt!MmInitializeProcessAddressSpace设置断点,然后启动bash.exe,第一次命中后直接放行(bash.exe是普通的Windows进程,不感兴趣),第二次命中时,看到的是services.exe在创建svchost.exe,切换到bash.exe,观察其栈回溯,可以看到它正在构建用以与WSL服务进程通信的SvcComm对象(图9)。此对象的构造函数内部会调用CoCreateInstance创建由WSL服务进程实现的进程外COM对象。这会触发创建COM对象的宿主进程,即svchost.exe。

技术分享

图9 Bash Launcher进程在与WSL服务进程建立通信

 

图10是来自WSL团队官方博客的WSL组件协作图,左侧的bash.exe和Linux会话管理服务之间的黑色箭头代表它们之间是通过COM技术交互的。

技术分享

图10 WSL组件协作图

 

恢复目标执行后,断点很快又命中,栈回溯如图11所示。

技术分享

图11 创建Linux实例

 

从栈帧12-17可以看出,这次命中断点的正是WSL服务进程,它在通过I/O控制(I/O Control)方式与内核空间的LxCore通信。栈帧9-b中的Adss代表的是Android subsystem,是微软放弃了的Astoria项目(用于在Windows上运行Android应用程序)留下的痕迹。

根据笔者的分析和图10、图11所示栈回溯是在创建Linux系统中的init进程。当init进程启动后,会创建真正的bash进程。图12显示了创建bash进程后的WSL有关进程列表。

技术分享

图12 创建bash进程后的WSL有关进程列表

 

图12中一共有四个进程,第一个是bash launcher进程,从操作系统的角度看,它是典型的Windows程序。第二个进程是WSL的子系统服务进程,第三个是它发起创建的init进程,第四个是init进程创建的bash进程。通过ParentCid(父进程ID)字段,可以看到后三个进程有父子关系。图13是使用Process Explorer观察的结果。

技术分享

图13 WSL的进程关系图

 

最小进程和Pico进程

要深入理解WSL,必须先了解最近几年里NT内核引入的两个新概念:最小进程和Pico进程。最小进程是Windows 8.1引入的,代表一个最小化的进程对象,它具有名字、令牌、保护级别等基本属性,但是进程空间是空的,没有PEB,也没有NTDLL,也没有句柄表。EPROCESS结构体中的Minimal为1代表该进程是最小进程。比如观察图12中的init进程,可以看到它是最小进程。

kd> dt _EPROCESS ffffce8d779b9780 -y Minimal
nt!_EPROCESS
   +0x6c4 Minimal : 0y1

除了WSL使用最小进程外,Windows 10中的内存压缩进程也是最小进程。

Pico进程是最小进程的一种,它的特点是有一个驱动程序与其关联,当这个进程内发生系统调用时,内核会把系统调用转给这个驱动,这个驱动被称为Pico Provider,这样的进程被称为Pico进程。图14是来自WSL官方博客的插图,用以说明NT系统中的三种进程。

技术分享

图14 三种进程

 

Pico的意思是微小,常常出现在容器技术中。从容器技术的角度来看,可以把Pico进程看作一个沙盒。事实上,Pico进程便源于Stony Brook University、Cambridge和微软研究院联合开发的Drawbridge项目。在Channel9上可以找到一个名为Drawbridge: A New Form of Virtualization for Application Sandboxing的录像,介绍了Library OS的思想和Drawbridge项目实现的技术原型。

用于WSL的Pico进程空间中可以执行Linux的原生程序,当发生系统调用时,内核会将这些调用转发给与它关联的驱动(Pico Provider)。比如图15所示的栈回溯显示的便是NT内核将系统调用转发给LxCore的过程。

技术分享

图15 转发系统调用

 

文章收尾之际再介绍一个调试小技巧。对于已经有20多年历史的NT内核来说,WSL绝对是新生事物,很多配套设施还不完善,比如在WinDBG中列进程时所有Pico进程的名字都显示为System Process(图12中下面两个),很不方便。如何知道它们的Linux进程名呢?是有办法的。在EPROCESS中有个名为PicoContext的字段(目前偏移0x708),指向的是一个记录Pico属性的结构体,它的详细定义没有公开,笔者通过反汇编了解到在它的偏移0x180处存放的便是进程的可执行文件路径。基于此,便可以使用如下命令来显示了:

kd> dU poi(poi(ffffce8d779b9780+708)+180)
ffffa782`0e43d960  "/init"
kd> dU poi(poi(ffffce8d78f31080+708)+180)
ffffa782`19840fa0  "/bin/bash"

如果有人问执行uname -a会返回什么,试一下便知道了。

gedu@DESKTOP-4NBEECU:/mnt/c/Windows/System32$ uname -a
Linux DESKTOP-4NBEECU 3.4.0+ #1 PREEMPT Thu Aug 1 17:06:05 CST 2013 x86_64 x86_64 x86_64 GNU/Linux

在LXCORE!LxpSyscall_NEWUNAME设个断点便可以跟踪这个结果的产生过程,有些来自字符串常量,有些是动态拼接的。预知其详,笔者强烈建议读者亲自动手试一下。

回望历史,微软一度对Linux充满恐惧和仇恨,想趁其未壮而屠之于襁褓。2002年9月,被称为视窗之父的微软高管Jim Allchin在与很多列客户交流和调查后,写了封很长的邮件给微软的另一些高管,其中有这样一段板面:

My conclusion: We are net on a path to win against Linux We must change some things and we must do it immediately. The current white papers, etc. are too high level and they are not going to cut it, Here are specific actions that I have concluded that we must take.

接下来详细部署了一系列任务……

但事实上,Linux一天天壮大了。那只好成为盟友吧,一起拥抱新的时代,一起干杯(背后也可能骂娘)!

无论如何,一个新的时代开始了,二进制的Linux程序可以以原生形式直接运行在NT内核之上,这是件多么激动人心的事啊。它具有划时代的意义,开一代先河。它在封闭的视窗系统上打开一扇门,代表自由开发的小企鹅走了进去,有了新的乐园。它会激发很多想象,很多创新,当然也可能有很多邪念(你懂的,我在说病毒、勒索和安全)……

在调试器里看Windows 10的Linux子系统