Chromium插件(Plugin)机制简要介绍和学习计划

3128次阅读  |  发布于5年以前

在Chromium中,除了可以使用Extension增强浏览器功能,还可以使用Plugin。两者最大区别是前者用JS开发,后者用C/C++开发。这意味着Plugin以Native Code运行,在性能上要优于Extension,适合执行计算密集型工作。不过,以Native Code运行,使得Plugin在安全上面临更大挑战。本文接下来对Chromium的Plugin机制进行简要介绍和制定学习计划。

Chromium最初支持两种类型的Plugin:NPAPI Plugin和PPAPI Plugin。NPAPI的全称是Netscape Plugin Application Programming Interface,PPAPI的全称是Pepper Plugin Application Programming Interface。从表面上看,两者的区别在于使用了不同的API规范。其中,NPAPI来自于Mozilla,而PPAPI来自于Google。但实际上,PPAPI与另外一种称为Native Client(NaCl)的技术是紧密联系在一起的。

Native Client是一个由编译工具链(Toolchain)和运行时(Runtime)组成的集合。Toolchain包含在NaCl SDK中。此外,NaCl SDK还提供了PPAPI接口。也就是,通过NaCl SDK,我们可以开发PPAPI Plugin。这些PPAPI Plugin要使用NaCl SDK提供的Toolchain进行编译。Toolchain使用了修改过的GCC对PPAPI Plugin进行编译。这个修改过的GCC为PPAPI Plugin生成的每一条跳转指令的目标地址都是32位对齐的,并且PPAPI Plugin的第一条指令的地址也是32位对齐的。对齐的目的是出于安全考虑,防止恶意代码跳转到任意地址执行指令。同时,有些被认为是危险的指令是禁止使用的,例如ret指令。

除此之外,这个修改过的GCC还会设置一个系统API白名单。在这个白名单里面的系统API,被认为是安全的,也就是对系统不会造成威胁和破坏,它们可以被PPAPI Plugin调用。不在这个白名单内的系统API,将会被禁止调用。这意味着,我们在开发PPAPI Plugin的时候,只可以使用由Chromium提供的PPAPI,以及位于白名单内的系统API。

对PPAPI Plugin的CPU指令以及API调用进行限制,目的都是为了安全。这些安全理论基础可以参考Native Client: A Sandbox for Portable, Untrusted x86 Native CodeGoogle Native Client: Analysis Of A Secure Browser Plugin Sandbox这两个PDF文档,我们在这里就不展开来讲了。

单单靠Toolchain对PPAPI Plugin的CPU指令以及API调用进行限制是不足够的,因为黑客可以对编译后的PPAPI Plugin进行修改。因此,就需要有一套Runtime,在PPAPI Plugin加载时进行验证。这套Runtime由一个NaCl PPAPI Plugin和一个Service Runtime组成,如图1所示:

图1 NaCl Architecture

NaCl PPAPI Plugin是一个内置在Chromium中的PPAPI Plugin,它属于一个Trusted Plugin,运行在Render进程中。Chromium还内置了一个PPAPI Flash,它同样也是一个Trusted Plugin,运行在Render进程中。此外,负责解析网页的WebKit,以及运行网页中的JS的V8引擎,也是运行在Render进程中的。

注意,内置的PPAPI Plugin不需要使用NaCl技术,因为它们是Trusted Code。为了区分使用了NaCl技术的PPAPI Plugin和内置的PPAPI Plugin,我们将前者称为NaCl Module。NaCl Module是Untrusted Code,它们以两种形式存在:Portable Executable(PEXE)和Native Executable(NEXE)。PEXE是跨平台的,编译一次,可以在所有平台运行。NEXE是平台相关的,只可以运行在指定的平台。实际上,PEXE在加载之前,会先被翻译成NEXE再加载和执行。

在网页中,可以通过标签使用一个NaCl模块,如下所示:

<embed name="NaCl_module"
      id="hello_world"
      width=200 height=200
      src="hello_world.nmf"
      type="application/x-NaCl" />

其中,src属性指定了一个NaCl Module清单文件。这个清单文件以json格式描述了它由哪些文件组成,如下所示:

{ "files": {
      "libgcc_s.so.1": { "x86-32": { "url": "lib32/libgcc_s.so.1" } },
      "main.nexe": { "x86-32": { "url": "hw.nexe" } },
      "libc.so.3c8d1f2e": { "x86-32": { "url": "lib32/libc.so.3c8d1f2e" } },
      "libpthread.so.3c8d1f2e": { "x86-32": { "url": "lib32/libpthread.so.3c8d1f2e" } } },
      "program": { "x86-32": { "url": "lib32/runnable-ld.so" } }
    }

WebKit会将标签当作是一个Plugin,于是就会请求Chromium加载这个Plugin。Chromium发现要加载的Plugin的MIME Type为"application/x-NaCl",于是就会在当前的Render进程中创建一个NaCl PPAPI Plugin。这个NaCl PPAPI Plugin将会负责去加载真正的NaCl Module,也就是标签的src属性指定的NaCl Module。

NaCl PPAPI Plugin首先会解析要加载的NaCl Module的清单文件,然后通过Chromium提供的PPAPI接口去下载清单文件里面指定的文件。随后,NaCl PPAPI Plugin又会请求Chromium的Browser进程(即图1中的Broker进程)为它创建一个sel_ldr进程。这个sel_ldr进程与Render进程一样,运行在一个同样的Sandbox中。这个Sandbox称为Outer Sandbox。

上述sel_ldr进程启动起来之后,会加载一个Service Runtime。这个Service Runtime会加载真正的NaCl Module。不过,在加载之前,Service Runtime会对NaCl Module的指令及其调用的API进行验证。如果NaCl Module违反规则,那么它就不会被加载。这样就可以解决我们前面提到的NaCl Module使用修改过的GCC编译之后又被Hacked的问题。

Service Runtime作为NaCl技术的一部分,它是Trusted Code。NaCl Module是Untrusted Code。Service Runtime除了负责加载NaCl Module之外,还会为NaCl Module提供一些服务。例如,NaCl Module在调用白名单允许的系统API时,它不是直接调用的,而是要通过Service Runtime进行调用。尽管NaCl Module与Service Runtime在同一个进程中,但是NaCl Module是不可以直接执行Service Runtime的代码的。

当NaCl Module要执行Service Runtime的代码时,需要通过Trampoline和Springboard间接执行。Trampoline和Springboard的作用有点类似x86的调用门(Call Gate),目的就是为了保护Service Runtime的代码不会被随意执行,就像User Space的代码不能随意执行Kernel Space的代码一样。这套保护机制,以及前面提到的对NaCl Module的指令限制和系统API调用限制,称为Inner Sandbox。

NaCl Module是以间接方式调用Chromium提供的PPAPI接口的,表现它要通过NaCl PPAPI Plugin来调用Chromium提供的PPAPI接口。这意味着NaCl Module与NaCl PPAPI Plugin之间需要有一种通信机制。这种通信机制使用的协议称为Simple-RPC(Remote Procedure Call)协议,简称SRPC。SRPC协议建立在Inter-Module Communication(IMC)之上。IMC又是什么呢?实际上就是Unix Socket。总结来说,就是NaCl Module通过Unix Socket请求NaCl PPAPI Plugin为它调用Chromium提供的PPAPI接口。当然,这些远程请求会被NaCl SDK封装为简单的函数调用,这也是SRPC的由来。

从前面的分析就可以知道,一个使用了NaCl技术的PPAPI Plugin在执行时,被限制在两个Sandbox中。一个是Inner Sandbox,另一个是Outer Sandbox。其中,Inner Sandbox施加了强有力的保护。即使这层强有力的保护被突破,还有Outer Sandbox继续保护着。这样就可以很大程度上保证系统的安全。与此相反的是,NPAPI Plugin缺乏Inner Sandbox这样强有力的保护,就会很容易出现安全问题。同样,IE浏览器的ActiveX Plugin,也面临着NPAPI Plugin同样的安全问题。

虽然前面我们一直把PPAPI Plugin和NaCl联系在一起,但事实上,PPAPI Plugin也可以完全脱离NaCl而存在,例如,图1所示的内置在Chromium中的NaCl PPAPI Plugin和PPAPI Flash Plugin。Chromium提供了一个PPAPI SDK。通过这个SDK,就可以开发与NaCl技术无关的PPAPI Plugin。不过,这些PPAPI Plugin是不能通过Web Store分发的,只能由用户在本地安装。

接下来,基于越简单越容易理解的原则,我们将忽略掉NaCl技术,分析Chromium的PPAPI Plugin机制,并且假设它们是运行在独立的Plugin进程中的,就如我们在前面Chromium的Plugin进程启动过程分析一文所述的。

从前面Chromium的Plugin进程启动过程分析一文可以知道,WebKit在解析网页时碰到标签时,就会请求Browser进程启动一个Plugin进程加载该标签描述的Plugin。Plugin进程启动起来之后,会通过Unix Socket分别与Browser进程以及负责加载网页的Render进程建立通信通道,如图2所示:

图2 Plugin进程、Render进程和Browser进程的关系

Plugin进程启动之后,主要涉及到的是它与Render进程之间的通信。首先,Render进程会请求Plugin进程加载指定的Plugin Module。Plugin Module加载完成之后,Render进程再请求Plugin进程创建Plugin Instance。Plugin Instance创建出来之后,就会进入运行状态。在运行的过程中,它会不断地通过Chromium提供的PPAPI接口请求Render进程执行相应的操作,从而完成自身的功能。

要理解Chromium的Plugin机制,重点在于掌握前面我们提到的两个基本概念Plugin Module和Plugin Instance,以及Plugin Instance调用PPAPI接口的过程。为了更好地理解Chromium的Plugin机制,我们结合Chromium在源码中提供的一个GLES2 Example进行说明。

每一个Plugin都对应有一个Module,每一个Module又可以创建多个Instance,如图3所示:

图3 Plugin Module与Plugin Instance的关系

网页中的每一个标签在Render进程中都对应有一个PepperPluginInstanceImpl对象。这些PepperPluginInstanceImpl对象在Plugin进程中又都会有一个对应的pp::Instance对象。这些PepperPluginInstanceImpl对象和pp::Instance对象就是用来描述Plugin Instance的。

当PepperPluginInstanceImpl与pp::Instance需要通信时,就会通过图2所示的Unix Socket相互发送IPC消息。这些IPC消息的发送与接收在Render进程一侧由一个HostDispatcher负责,而在Plugin进程一侧由一个PluginDispatcher负责。

具有相同src值的标签对应的Plugin Instance属于同一个Plugin Module。Render进程为标签创建Plugin Instance时,首先会检查要创建的Plugin Instance所对应的Plugin Module是否已经加载。如果还没有加载,那么就会按照图4所示的流程进行加载:

图4 Plugin Module的加载流程

标签创建Plugin Instance的请求是由WebKit发起的。这个请求会交给Content层处理。Content层检测到要创建的Plugin Instance对应的Plugin Module还没有加载时,就会创建一个Out-of-Process Plugin Module。这里我们假设标签描述的Plugin是一个非内置Plugin,因此需要为它创建一个Out-of-Process Plugin Module。

Content层在创建Out-of-Process Plugin Module的过程中,会请求Browser进程创建一个Plugin进程,并且请求在该Plugin进程中加载Plugin Module。Plugin Module实际上就是一个SO文件。这个SO文件加载完成后,Plugin进程会调用由它导出的一个函数PPP_InitializeModule,用来对刚刚加载的Plugin Module进行初始化。

其中的一个初始化操作就是创建一个pp::Module对象。每一个Plugin都要在自己的Module文件中自定义一个pp::Module类。例如,GLES2 Example自定义的pp::Module类为GLES2DemoModule,如下所示:

// This object is the global object representing this plugin library as long
    // as it is loaded.
    class GLES2DemoModule : public pp::Module {
     public:
      GLES2DemoModule() : pp::Module() {}
      virtual ~GLES2DemoModule() {}

      virtual pp::Instance* CreateInstance(PP_Instance instance) {
        return new GLES2DemoInstance(instance, this);
      }
    };

这个类定义在文件external/chromium_org/ppapi/examples/gles2/gles2.cc中。

同时,每一个Plugin还必须要在自己的Module文件中定义一个函数CreateModule。这个函数就是用来创建自定义的pp::Module对象的。例如,GLES2 Example定义的函数CreateModule的实现如下所示:

namespace pp {
    // Factory function for your specialization of the Module object.
    Module* CreateModule() {
      return new GLES2DemoModule();
    }
    }  // namespace pp

这个函数定义在文件external/chromium_org/ppapi/examples/gles2/gles2.cc中。

有了这个自定义的pp::Module对象之后,以后就可以调用它的成员函数CreateInstance创建Plugin Instance了。GLES2 Example用GLES2DemoInstance类来描述Plugin Instance。这个GLES2DemoInstance类必须要继承于pp::Instance类,它的定义如下所示:

class GLES2DemoInstance : public pp::Instance,
                              public pp::Graphics3DClient {

     public:
      GLES2DemoInstance(PP_Instance instance, pp::Module* module);
      virtual ~GLES2DemoInstance();

      ......
    };

这个类定义在文件external/chromium_org/ppapi/examples/gles2/gles2.cc中。

Plugin Module的其它初始化工作还包括初始化一系列的PPAPI接口。这些PPAPI接口可以被自定义的pp::Instance调用。

回到Render进程中,当Plugin Module加载完成后,Render进程就会基于刚刚加载的Plugin Module创建一个Plugin Instance,也就是一个PepperPluginInstanceImpl对象。这个PepperPluginInstanceImpl对象接下来会被初始化。在初始化的过程中,它会请求Plugin进程创建一个对应的pp::Instance对象,如图5所示:

图5 Plugin Instance的创建过程

Render进程存在一个PPP_INSTANCE_INTERFACE_1_1接口,PepperPluginInstanceImpl对象会通过该接口请求Plugin进程创建一个对应的pp::Instance对象。

PPP_INSTANCE_INTERFACE_1_1接口定义了一个DidCreate操作。执行该操作的时候,Render进程就会向Plugin进程发送一个类型PpapiMsg_PPPInstance_DidCreate的IPC消息。该IPC消息携带了一个API_ID_PPP_INSTANCE参数,表示需要将它分发给一个PPP_Instance_Proxy对象处理。

Plugin进程也存在一个PPP_INSTANCE_INTERFACE_1_1接口。PPP_Instance_Proxy对象接收到类型PpapiMsg_PPPInstance_DidCreate的IPC消息时,就会执行该接口的DidCreate操作。这个操作在执行的过程中,就会调用前面在加载Plugin Module时创建的自定义pp::Module对象的成员函数CreateInstance,结果就会获得一个自定义的pp::Instance对象。

从前面的分析可以知道,对于GLES2 Example来说,它自定义的pp::Instance对象是一个GLES2DemoInstance对象。这个GLES2DemoInstance对象会通过Chromium提供的OpenGL ES 2.0 PPAPI接口为标签上绘制内容。

Plugin进程在加载Module的时候,会初始化一个PPB_OPENGLES_INTERFACE接口。该接口描述的就是一个OpenGL ES 2.0 PPAPI接口,它定义了一系列的OpenGL操作。每一个操作都对应一个OpenGL函数,如图6所示:

图6 OpenGL ES 2.0 PPAPI接口的调用过程

这些OpenGL操作又是通过调用GLES2Implementation类的同名成员函数实现的。例如,PPB_OPENGLES_INTERFACE接口定义的ActiveTexture操作,是通过调用GLES2Implementation类的成员函数ActiveTexture实现的。

从前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文可以知道,GLES2Implementation类描述的是一个Comand Buffer OpenGL接口,它的成员函数被调用的时候,实际上只是将要执行的OpenGL命令写入到一个Command Buffer中去,然后通知GPU进程执行该Command Buffer中的OpenGL命令。

不过,图6所示的GLES2Implementation对象并不是将Command Buffer的OpenGL命令直接传递给GPU进程执行的,而通过一个PpapCommandBufferProxy对象传递给Render进程中的一个CommandBufferProxyImpl对象处理。这个CommandBufferProxyImpl对象又会将传递给它的OpenGL命令写入到自己的Command Buffer中去,然后再通知GPU进程中的一个GpuCommandBufferStub对象执行该Command Buffer中的OpenGL命令。

由此可见,Plugin通过PPB_OPENGLES_INTERFACE接口执行OpenGL命令时,要执行的OpenGL命令会先传递给Render进程,Render进程再传递给GPU进程执行。为什么Plugin不直接将OpenGL命令传递给GPU进程执行呢?这是因为Plugin在运行的过程中,只能够与Render进程交互。因此,当它需要请求其它进程执行某一个操作时,就需要通过Render进程间接执行。

Plugin在执行OpenGL命令之前,首先要初始化一个OpenGL环境。这个初始化操作发生在Plugin对应的标签的视图创建完成时。这时候标签在Render进程中对应的PepperPluginInstanceImpl对象就会通过PPP_INSTANCE_INTERFACE_1_1接口向它在Plugin进程中对应的pp::Instance对象发出通知,如图7所示:

图7 Plugin初始化OpenGL环境的过程

PPP_INSTANCE_INTERFACE_1_1接口定义了一个DidChangeView操作。当标签的视图第一次创建或者以后大小发生变化时,该操作都会被执行。在执行的时候,它会向Plugin进程发送一个类型为PpapiMsg_PPPInstance_DidChangeView的IPC消息。这个IPC消息携带了一个APP_ID_PPP_INSTANCE参数,表示需要将它分发给一个PPP_Instance_Proxy对象处理。

前面提到,Plugin进程也存在一个PPP_INSTANCE_INTERFACE_1_1接口。PPP_Instance_Proxy对象接收到类型PpapiMsg_PPPInstance_DidChangeView的IPC消息时,就会执行该接口的DidChangeView操作。这个操作在执行的过程中,又会找到之前加载Plugin Instance时创建的一个自定义pp::Instance对象,并且调用它的成员函数DidChangeView,用来通知它标签的视图大小发生了变化。

在GLES2 Example中,自定义的pp::Instance对象是一个GLES2DemoInstance对象。当这个GLES2DemoInstance对象的成员函数DidChangeView第一次被调用的时候,会做两件事情。第一件事情是创建一个Graphics3D对象。这个Graphics3D对象在Plugin进程中用来描述一个OpenGL环境。第二件事情是将前面创建的OpenGL环境与当前正在处理的Plugin进行绑定。这两件事情完成之后,一个Plugin的OpenGL环境就创建和初始化完成了。

Plugin的OpenGL环境的创建过程如图8所示:

图8 Plugin的OpenGL环境的创建过程

Plugin进程存在一个PPB_GRAPHICS_3D_INTERFACE_1_0接口。PPB_GRAPHICS_3D_INTERFACE_1_0接口定义了一个Create操作。在Plugin进程中创建OpenGL环境,也就是一个Graphics3D对象的时候,上述的Create操作会被执行。在执行的过程中,它会通过一个APP_ID_RESOURCE_CREATION参数找到一个ResourceCreationProxy对象,然后再通过这个ResourceCreationProxy对象向Render进程发送一个类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息。这个IPC消息携带了一个APP_ID_PPB_GRAPHICS_3D参数,表示要将它分发给一个PPB_Graphics3D_Proxy对象处理。

PPB_Graphics3D_Proxy对象在处理类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息时,会创建一个PPB_Graphics3D_Impl对象。这个PPB_Graphics3D_Impl对象在创建的过程中,会与GPU进程建立一个GPU通道。这个GPU通道建立起来之后,就会在GPU进程中获得一个OpenGL环境。这个OpenGL环境就是用来执行Plugin发出的OpenGL命令的。Render进程与GPU进程建立GPU通道的过程,可以参考前面Chromium硬件加速渲染的OpenGL上下文创建过程分析一文。

给Plugin绑定OpenGL环境的过程如图9所示:

图9 Plugin绑定OpenGL环境的过程

Plugin进程存在一个PPB_INSTANCE_INTERFACE_1_1接口。PPP_INSTANCE_INTERFACE_1_1接口定义了一个BindGraphics操作。该操作在执行的时候,会通过一个APP_ID_PPB_INSTANCE参数找到一个PPB_Instance_Proxy对象,然后再通过这个PPB_Instance_Proxy对象向Render进程发送一个类型为PpapiHostMsg_PPBInstance_BindGraphics的IPC消息。这个IPC消息携带了一个APP_ID_PPB_INSTANCE参数,表示要将它分发给一个PPB_Instance_Proxy对象处理。

PPB_Instance_Proxy对象在处理类型为PpapiHostMsg_PPBInstance_BindGraphics的IPC消息,会找到要绑定OpenGL环境的Plugin Instance,也就是一个PepperPluginInstanceImpl对象,然后调用这个PepperPluginInstanceImpl对象的成员函数BindToInstance,将其绑定到指定的OpenGL环境中去,也就是与前面创建的一个PPB_Graphics3D_Impl对象建立关联。

这样,我们就介绍完了Chromium的Plugin机制。由于Chromium已不再支持NPAPI Plugin,这里我们说的Plugin,指的是PPAPI Plugin,也称为Pepper Plugin。Pepper Plugin可以通过NaCl SDK开发,也可以通过PPAPI SDK开发。通过NaCl SDK开发的Pepper Plugin在NaCl环境中运行,受到两个Sandbox保护,因此它的安全性是非常高的。在Web Store中分发的Pepper Plugin必须要通过NaCl SDK开发,以及运行在NaCl环境中。内置的Pepper Plugin,或者仅仅在本地安装的Pepper Plugin,可以不运行在NaCl环境中,因此它们可以通过PPAPI SDK进行开发。

不管是否运行在NaCl环境中,Pepper Plugin使用Chromium提供的PPAPI接口的方式都是一样的。换句话说,就是NaCl环境对Pepper Plugin来说是透明的。为了简单起见,在接下来的文章中,我们假设Pepper Plugin是基于PPAPI SDK开发的,也就是它没有运行在NaCl环境中。

接下来,我们将结合文中提到的GLES2 Example,按照以下三个情景,对Chromium的Pepper Plugin机制进行更详细的分析:

1. Plugin Module的加载过程

2. Plugin Instance的创建过程

3. Pugin Instance的3D渲染过程

其中,前面两个情景有助于我们理清Plugin Module与Plugin Instance的关系,第三个情景有助于我们理解Plugin调用PPAPI接口的过程,也就是Plugin与浏览器的交互过程。理解了这三个情景之后,我们就可以深刻理解Chromium的Pepper Plugin机制了。敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8