无需操作系统直接运行 Python 代码

jopen 10年前

Josh Triplett以一个“笑点”开始了他在PyCon 2015上的演讲:移植Python使其无需操作系统运行:他和他的英特尔同事让解释器能够在 GRUB引导程序、BIOS或EFI系统上运行。连演讲的休息时间也没放过,他有很多有趣的要说的事情,还有许多让人大开眼界的演示。

Python在Boot Loader上运行的最初想法是能够测试硬件,像BIOS,可扩展固件接口(EFI)以及高级配置和电源接口(ACPI),而无需去写一些“一次性测试项目“程序集。传统来说,英特尔已经写了很多针对DOS(BIOS系统)或EFI系统的测试程序。无论是DOS还是EFI都不提供环境保护,这样程序就能够驻入在内存和硬件中去做他们所需的任何事情。

无需操作系统直接运行 Python 代码他不过是想用脚本来写测试代码而已,“因为这样比较有趣”。他既不想写太多的 C 语言代码,也不想像以前那样用那个能计算 C-类 表达式的 GRUB shell。 其实, 他说,“C 代码写的越少, 我就越轻松"。

随着时间的推移, 移植到 GRUB 中的 Python 已经变成操控硬件的利器。它又把我们带回到使用 PEEK 和 POKE 在 Commodore 64(or DOS) 上面操控硬件的美好时光。“那些事是现在的硬件设备无法完成的”他说。 

GRUB中的PYTHON

BIOS Implementation Test Suite(BITS),正如其名,将会运行在多种固件上的GRUB中:32位BIOS或32/64位EFI。他使用原始的GRUB或GRUB 2。基于标准的PYTHON解释器(如CPython),但是他道歉道:它使用PYTHON2.7。这个工具的目标受众对这个版本的语言相当熟悉。如果不是这样,他更喜欢在以后迁移到Python 3.

有一个“读取-求值-输出 循环” 交互环境[read-eval-print loop (REPL)]让你完全访问Python语言。它包括Tab完成,历史记录,和行编辑。一个标准库的“大量碎片”已经被一直BITS上运行。最重要的是,这个项目已经添加了一些对平台支持的模块:CPU,SMP(symmetric multi-processing),ACPI,EFI以及其他。INTEL已经创建了一个测试集以及 使用Python写了使用以上模块的一些试探性的工具。

Triplett然后从幻灯片切换到了虚拟机的GRUB中运行一个Python解释器的提示界面。他输入了两句语句到解释器来展示它支持列表解析和任意大的整数(如:bignums)。

要获得一个python交互环境,GRUB需要调用一个单独的函数:


PyRun_InteractiveLoop(stdin, "<stdin>");

它会处理所有REPL[读取-执行-输出 循环],包括对输入的解析和执行、行编辑等等。
这两个参数简单的表明了在哪里获取输入 和 当发生异常时在traceback里要输出什么来当做源文件。但是想要能在GRUB里调用那个函数还有一些工作要做。

因为不能使用来自于 Linux 主机的工具链和特性,这个项目不能像平常那样安装和配置 Python。对于 GRUB 来说,没有 GNU 目标声明(例如:用于交叉编译的 cpu-vendor-od-triple)和目标头文件可以使用。因此,BITS 将所有的 Python 源文件添加到了 GRUB 的构件系统中。本质上说那仅仅是一些GRUB 添加 Python 所必需的 C 语言文件。通常,autoconf 将创建 Python 构件程序中的apyconfig.h 文件来说明哪些功能在平台上存在。相反的,这个项目手动的创建 apyconfig.h 文件大量“不,我没有这个功能”的配置参数和一小撮“是”的条目。

许多在 pyconfig.h 文件中被列出的功能是被(或不被)操作系统所提供的,但是在这种情况下是没有操作系统的。Python 的确需要最低限度的一些支持功能,以及一些额外被配置的特性。这个项目需要去做的是提供任何被渴望而又不存在功能。

CPython 需要什么

那么,什么情况下你真的需要运行 CPython?Triplett 提供的大量实例来证明什么时候需要运行 CPython。有一些平常的文件操作就需要了,比如说:使用 stat () 来确定一个路径是否包含 __init__或是文件中是否包含 __init__。增加 simpleisatty()(以位为单位,文件描述符是少于三则返回 true)好比经历一个 seek() 执行一样。为了支持那些功能,不得不添加一个简单的文件描述符表,因为 GRUB 的文件功能使用结构体指针,而不是描述符。

解析器把一个字符放回在输入流的时候,Python 也需要使用 ungetc() 。而不是在添加一个字符缓冲区的时候使用,即添加"快速 hack"来寻找后一个字符。添加开放式编码的 qsort() 时也一样一样要使用  ungetc();GRUB 不任何支持排序。

GRUB还没有支持的另一个方面是浮点运算。项目组发现了一个许可的浮点运算库FDLIBM。它没有使用任何浮点硬件加速,这在GRUB环境是非常有用的。这意味啊即使在固件没完全初始化浮点运算硬件时也能使用浮点运算。

在使用Python时,我们大量使用printf()和sprintf()。大部分情况,GRUB版本工作很好,但对“%%”(输出一个”%“)这种特殊格式还不支持。事实证明,Python频繁使用格式化的字符串输出。

在被发现和修复之前,怪异的BUG仍然存在。

这个工程还有一些性能问题需要解决。首先,启动时间出乎意料的长。对硬件来说,这是十分痛苦的事情,但是在 CPU 的模拟电路上也很糟糕(“我们不想花三天时间做引导”)。部分问题来自于 Python 的解释器,每次它读取一个数据的时候都要调用 usesungetc()。GRUB 没有太多高速缓存的磁盘,所以所有 I/O 端口直接访问磁盘。

通过加入对 .pyc (Python 字节代码)文件格式的支持,这个工程能够提前减少许多语法分析工作。主机的版本和 GRUB 的版本在同一时刻编译,用于 Python 文件在启动时的编译工作。

这做出了实质性的提升,但是由于stat()的原因,启动时间仍然有些慢。他说在Linux系统上,stat()仅花费几微秒的时间,但是BITS版本会花费几毫秒。增加对zipimport的支持能让工程把所有的.py文件打包放入一个单一的ZIP文件中来避免对stat()的调用。

这个工程希望做有历史和tab自动补全的REPL(读取﹣求值﹣输出循环),但是一般获得支持的方式是使用GUN的Readline library。这个库由有终端设备的POSIX(可移植操作系统接口)提供环境支持。开发者不想写一个“C代码文件”来支持它,所以他们用Python写了一个读取线支持来替代。CPython的PyOS_ReadlineFunctionPointer被称为一个使用C语言API的新Python函数的C函数集合。

为了能够使用其他的操作和多种的测试套件,仍迫切需要构建 GRUB 的动态菜单。GRUB 已经为设备提供了磁盘和文件系统像磁盘分区和 CD 驱动器(例如:“(hd0)”,"(cd)")因此 BITS 增加了一个的“(python)”设备和一个工作起来像在 Linux 用户空间的文件系统(译者注:打不开请加梯子)。因此 Python 代码能访问任意的内存文件,例如在 (python)/menu.cfg 下的菜单配置文件,“即使我们没有写更多的C代码”,Triplett 说道。

访问硬件

既然目标是提供一个友好的测试硬件环境,Python 需要能够访问它。一个叫做“bits”的模块被添加进来提供访问各种硬件的功能,例如:CPUID,特殊模块寄存器 (MSRs),I/O 端口,和内存映射 I/O。他用几行代码展示了这些能力。

    >>> import bits      >>> from ctypes import *      >>> c = bits.cpuid(0, 0)      >>> c      cpuid_result(eax=0x..., ebx=..., ecx=..., edx=...)

他引入ctypes模块,以便在下一部分演示中“操作原始内存片”。对于那些想要深挖一些的人来说,几乎所有演示都可以在这个油Tube视频的演讲中看到。cpuid()调用返回了CPU0的CPUID,他之后将其打印出来。他问:“这是不是很有趣?我们正从Python中得到处理器的寄存器信息。” 接着,他使用Python来解释这个结果:

    >>> buf = (c_uint32*3)(c.ebx, c.edx, c.ecx)      >>> (c_char*12).from_buffer(buf).value      'GenuineIntel'

三个寄存器包含描述处理器类型的标识符。他使用ctypes模块中的类型,以字符串的形式重新解释这三个寄存器(按照之前的顺序)的信息,结果显示为处理器类型。

Intel希望能够测试高度并行化的系统,但GRUB只了解启动了的CPU的信息。所以BITS在系统中唤醒每个CPU,并把它们放入一个睡眠循环中,使用MWAIT(x86监视器等待指令)等待工作的到来。特定CPU有专门的唤醒函数和执行函数。

这个项目还准备用Python获取ACPI的信息和方法。这参考了ACPI组件架构 (ACPICA)的实现并把它加入BITS中。由于全部是C代码,所以增加了Python绑定。这一做法使得Python可以调用任意ACPI方法——只要先将参数转换成ACPI类型并将结果转成Python类型。他用了一个简单的Python程序演示了如何将虚拟机中所有设备的硬件ID显示出来:

    >>> import acpi      >>> print acpi.dump('_HID')

Triplett声称他不会继续深入讲解BITS硬件探索的细节。他已经在其它演讲中更加详尽地解释过了。 

英特尔也希望系统能使用这个固件而不是BIOS访问EFI。这种扩展名义上是指一切在EFI中都是”协议“,每一个都包含了原生c语言函数调用。要做到这样,通过libffi提供的外部函数接口被移植到GRUB并且添加了支持EFI调用转换的功能。使用这种方式和Python c类型的模块(Python提供的c语言类型的接口和函数)允许解释器访问EFI。他仅使用Python演示了访问EFI的方法:

    >>> import efi      >>> out = efi.system_table.ConOut.contents      >>> out.ClearScreen(out)      [ which clears the screen ]      >>> out.OutputString(out, 'Hello world!\r\n')      Hello world!

访问EFI后,允许Python使用EFI文件协议去创建目录和写文件到EFI文件系统中。这是非常有帮助的,因为GRUB仅仅能够读文件。不仅仅如此,存在着图像输出协议(GOP)能够读写屏幕内容。正如他所解释的,幻灯片就是简单的图像,事实上是通过在笔记本上BITS和EFI显示出来的。在BITS的环境下,做出了这个幻灯片和demo,因此,事实来说,整个演示就是一个demo,他说这些话时周围响起了掌声。这样做是不需要任何一行新的C语言代码的。

最后他保存了认为最好的demo,并从EFI(可扩展固件接口)GOP(画面组)的帧缓冲区中作为Python启动,当他敲完最后的几行代码,很明显机器开始识别了,计算并显示了一个400x400大小的 Mandelbrot set(曼德布洛特集合)的灰度图片。他对周围鼓掌的人说:“在EFI图形协议中仅用八行Python代码显示了不规则图形(Fractal)”。大约要15秒来绘出图像,有点慢,他说,那不是Python的问题,而是因为使用纯软件进行浮点运算了。

在谈话最后,Triplett指出在BITS(后台智能传输服务)里没有中断处理的钩子函数(hook),但是这很容易就添加上的。他说,在像Mirage OS(和其它的“类似操作系统”)也能在BITS上添加Python代码,并且和这没有多大区别。“待办事件清单上的下一个有趣的项目”是添加Python绑定的EFI TCP网络协议和钩子到Python的socket模块,看看能否在那样的环境(BITS)下运行一个简单的HTTP服务(SimpleHTTPServer)。这样就能添加一个“网络REPL(web REPL)”到BITS环境了。