hi

hi 查看完整档案

郑州编辑商丘师范学院  |  计算机科学与技术专业 编辑phper  |  PHP 工程师 编辑填写个人主网站
编辑

从事C++服务端开发两年半,又转从web 开发,现在是个phper。开发过linux 下安全软件开发,彩票后台平台开发以及摄影平台项目,目前从事聚合支付的项目。

个人动态

hi 赞了回答 · 2019-02-19

解决面试经典问题:Cookie禁用了,Session还能用吗?

这个问题下面的所有答案都被人恶意的踩,麻烦管理员出来主持公道

默认SESSION配置

默认的JSP、PHP配置中,SessionID是需要存储在Cookie中的,默认Cookie名为:

  • PHPSESSIONID

  • JSESSIONID

以下以PHP为例:

  1. 你第一次访问网站时,

  2. 服务端脚本中开启了Sessionsession_start();

  3. 服务器会生成一个不重复的 SESSIONID 的文件session_id();,比如在/var/lib/php/session目录

  4. 并将返回(Response)如下的HTTP头 Set-Cookie:PHPSESSIONID=xxxxxxx

  5. 客户端接收到Set-Cookie的头,将PHPSESSIONID写入cookie

  6. 当你第二次访问页面时,所有Cookie会附带的请求头(Request)发送给服务器端

  7. 服务器识别PHPSESSIONID这个cookie,然后去session目录查找对应session文件,

  8. 找到这个session文件后,检查是否过期,如果没有过期,去读取Session文件中的配置;如果已经过期,清空其中的配置

如果客户端禁用了Cookie,那PHPSESSIONID都无法写入客户端,Session还能用?

答案显而易见:不能

并且服务端因为没有得到PHPSESSIONID的cookie,会不停的生成session_id文件

取巧传递session_id

但是这难不倒服务端程序,聪明的程序员想到,如果一个Cookie都没接收到,基本上可以预判客户端禁用了Cookie,那将session_id附带在每个网址后面(包括POST),
比如:

GET http://www.xx.com/index.php?session_id=xxxxx
POST http://www.xx.com/post.php?session_id=xxxxx

然后在每个页面的开头使用session_id($_GET['session_id']),来强制指定当前session_id

这样,答案就变成了:

聪明的你肯定想到,那将这个网站发送给别人,那么他将会以你的身份登录并做所有的事情
(目前很多订阅公众号就将openid附带在网址后面,这是同样的漏洞)。

其实不仅仅如此,cookie也可以被盗用,比如XSS注入,通过XSS漏洞获取大量的Cookie,也就是控制了大量的用户,腾讯有专门的XSS漏洞扫描机制,因为大量的QQ盗用,发广告就是因为XSS漏洞

所以Laravel等框架中,内部实现了Session的所有逻辑,并将PHPSESSIONID设置为httponly并加密,这样,前端JS就无法读取和修改这些敏感信息,降低了被盗用的风险。

Cookie在现代

禁用Cookie是 IE6 那个年代的事情,现在的网站都非常的依赖Cookie,禁用Cookie会造成大量的麻烦。

在Flash还流行的年代,Flash在提交数据会经常出现用户无法找到的情况,其实是因为Flash在IE下是独立的程序,无法得到IE下的Cookie。
所以在Flash的flash_var中,一般都会指定当前的session_id,让Flash提交数据的时候,将这个session_id附带着提交过去
Chrome中使用 Flash沙箱 已经解决了cookie的问题,但是为了兼容IE,比如swfupload等flash程序都要求开发者附带一个session_id

面试者的用意

面试者出此题也是为了考察你对HTTP协议和服务器会话的理解。

关注 50 回答 13

hi 赞了回答 · 2019-01-10

解决java 子类的构造器提示报错

  • 某个类如果没有构造函数,编译器会自动给添加一个无参构造函数;如果已经有了构造函数,就不会自动添加。

  • 子类的构造函数必须调用父类的构造函数。如果没有显式地调用父类的构造函数,虚拟机就会自动地去调父类的无参构造函数。这种情况下,如果父类没有无参构造函数,编译器就会报错。

你的Student第二个构造函数会去调父类Person的无参构造函数,然而父类只有一个带参数的构造 ,所以报错了。

关注 4 回答 2

hi 关注了用户 · 2019-01-10

LNMPR源码研究 @php7internal

一群热爱代码的人 研究Nginx PHP Redis Memcache Beanstalk 等源码 以及一群热爱前端的人
希望交流的朋友请加微信 289007301 注明:思否 拉到交流群

《PHP7底层设计与源码分析》勘误https://segmentfault.com/a/11...

《Redis5命令设计与源码分析》https://item.jd.com/12566383....

景罗 陈雷 李乐 黄桃 施洪宝 季伟滨 闫昌 李志 王坤 肖涛 谭淼 张仕华 方波 周生政 熊浩含 张晶晶(女) 李长林 朱栋 张晶晶(男) 陈朝飞 巨振声 杨晓伟 闫小坤 韩鹏 夏达 周睿 李仲伟 张根红 景罗 欧阳 孙伟 李德 twosee

关注 4310

hi 赞了文章 · 2018-09-18

Laravel核心——Ioc服务容器

服务容器


在说 Ioc 容器之前,我们需要了解什么是 Ioc 容器。

Laravel 服务容器是一个用于管理类依赖和执行依赖注入的强大工具。

在理解这句话之前,我们需要先了解一下服务容器的来龙去脉: laravel神奇的服务容器。这篇博客告诉我们,服务容器就是工厂模式的升级版,对于传统的工厂模式来说,虽然解耦了对象和外部资源之间的关系,但是工厂和外部资源之间却存在了耦和。而服务容器在为对象创建了外部资源的同时,又与外部资源没有任何关系,这个就是 Ioc 容器。

所谓的依赖注入和控制反转: 依赖注入和控制反转,就是

只要不是由内部生产(比如初始化、构造函数 __construct 中通过工厂方法、自行手动 new 的),而是由外部以参数或其他形式注入的,都属于依赖注入(DI)

也就是说:

依赖注入是从应用程序的角度在描述,可以把依赖注入描述完整点:应用程序依赖容器创建并注入它所需要的外部资源;

控制反转是从容器的角度在描述,描述完整点:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。

Laravel中的服务容器


Laravel服务容器主要承担两个作用:绑定与解析,服务容器的结构如下:

Markdown

绑定

所谓的绑定就是将接口与实现建立对应关系。几乎所有的服务容器绑定都是在服务提供者中完成,也就是在服务提供者中绑定。

如果一个类没有基于任何接口那么就没有必要将其绑定到容器。容器并不需要被告知如何构建对象,因为它会使用 PHP 的反射服务自动解析出具体的对象。

也就是说,如果需要依赖注入的外部资源如果没有接口,那么就不需要绑定,直接利用服务容器进行解析就可以了,服务容器会根据类名利用反射对其进行自动构造。

bind绑定

绑定有多种方法,首先最常用的是bind函数的绑定:

  • 绑定自身

$this->app->bind('App\Services\RedisEventPusher', null);
  • 绑定闭包

$this->app->bind('name', function () {
  return 'Taylor';
});//闭包返回变量

$this->app->bind('HelpSpot\API', function () {
  return HelpSpot\API::class;
});//闭包直接提供类实现方式

public function testSharedClosureResolution()
{
  $container = new Container;
  $class = new stdClass;
  $container->bind('class', function () use ($class) {
      return $class;
  });

  $this->assertSame($class, $container->make('class'));
}//闭包返回类变量

$this->app->bind('HelpSpot\API', function () {
  return new HelpSpot\API();
});//闭包直接提供类实现方式

$this->app->bind('HelpSpot\API', function ($app) {
  return new HelpSpot\API($app->make('HttpClient'));
});//闭包返回需要依赖注入的类
  • 绑定接口

public function testCanBuildWithoutParameterStackWithConstructors()
{
  $container = new Container;
  $container->bind('Illuminate\Tests\Container\IContainerContractStub', 
                   'Illuminate\Tests\Container\ContainerImplementationStub');
  
  $this->assertInstanceOf(ContainerDependentStub::class, 
                          $container->build(ContainerDependentStub::class));
}

interface IContainerContractStub
{
}

class ContainerImplementationStub implements IContainerContractStub
{
}

class ContainerDependentStub
{
  public $impl;
  public function __construct(IContainerContractStub $impl)
  {
      $this->impl = $impl;
  }
}

这三种绑定方式中,第一种绑定自身一般用于绑定单例。

bindif绑定

public function testBindIfDoesntRegisterIfServiceAlreadyRegistered()
{
    $container = new Container;
    $container->bind('name', function ()
         return 'Taylor';
     });

    $container->bindIf('name', function () {
         return 'Dayle';
    });

    $this->assertEquals('Taylor', $container->make('name'));
}

singleton绑定

singleton 方法绑定一个只需要解析一次的类或接口到容器,然后接下来对容器的调用将会返回同一个实例:

$this->app->singleton('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

值得注意的是,singleton绑定在解析的时候若存在参数重载,那么就自动取消单例模式。

public function testSingletonBindingsNotRespectedWithMakeParameters()
{
    $container = new Container;

    $container->singleton('foo', function ($app, $config) {
        return $config;
    });

    $this->assertEquals(['name' => 'taylor'], $container->makeWith('foo', ['name' => 'taylor']));
    $this->assertEquals(['name' => 'abigail'], $container->makeWith('foo', ['name' => 'abigail']));
    }

instance绑定

我们还可以使用 instance 方法绑定一个已存在的对象实例到容器,随后调用容器将总是返回给定的实例:

$api = new HelpSpot\API(new HttpClient);
$this->app->instance('HelpSpot\Api', $api);

Context绑定

有时侯我们可能有两个类使用同一个接口,但我们希望在每个类中注入不同实现,例如,两个控制器依赖 IlluminateContractsFilesystemFilesystem 契约的不同实现。Laravel 为此定义了简单、平滑的接口:

use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\VideoController;
use App\Http\Controllers\PhotoControllers;
use Illuminate\Contracts\Filesystem\Filesystem;

$this->app->when(StorageController::class)
          ->needs(Filesystem::class)
          ->give(function () {
            Storage::class
          });//提供类名

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
             return new Storage();
          });//提供实现方式

$this->app->when(VideoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
            return new Storage($app->make(Disk::class));
          });//需要依赖注入

原始值绑定

我们可能有一个接收注入类的类,同时需要注入一个原生的数值比如整型,可以结合上下文轻松注入这个类需要的任何值:

$this->app->when('App\Http\Controllers\UserController')
          ->needs('$variableName')
          ->give($value);

数组绑定

数组绑定一般用于绑定闭包和变量,但是不能绑定接口,否则只能返回接口的实现类名字符串,并不能返回实现类的对象。

public function testArrayAccess()
{
    $container = new Container;
    $container[IContainerContractStub::class] = ContainerImplementationStub::class;

    $this->assertTrue(isset($container[IContainerContractStub::class]));
    $this->assertEquals(ContainerImplementationStub::class, 
                        $container[IContainerContractStub::class]);

    unset($container['something']);
    $this->assertFalse(isset($container['something']));
}

标签绑定

少数情况下,我们需要解析特定分类下的所有绑定,例如,你正在构建一个接收多个不同 Report 接口实现的报告聚合器,在注册完 Report 实现之后,可以通过 tag 方法给它们分配一个标签:

$this->app->bind('SpeedReport', function () {
  //
});

$this->app->bind('MemoryReport', function () {
  //
});

$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');

这些服务被打上标签后,可以通过 tagged 方法来轻松解析它们:

$this->app->bind('ReportAggregator', function ($app) {
    return new ReportAggregator($app->tagged('reports'));
});
  public function testContainerTags()
  {
         $container = new Container;
         $container->tag('Illuminate\Tests\Container\ContainerImplementationStub', 'foo', 'bar');
         $container->tag('Illuminate\Tests\Container\ContainerImplementationStubTwo', ['foo']);

         $this->assertCount(1, $container->tagged('bar'));
         $this->assertCount(2, $container->tagged('foo'));
         $this->assertInstanceOf('Illuminate\Tests\Container\ContainerImplementationStub', $container->tagged('foo')[0]);
         $this->assertInstanceOf('Illuminate\Tests\Container\ContainerImplementationStub', $container->tagged('bar')[0]);
         $this->assertInstanceOf('Illuminate\Tests\Container\ContainerImplementationStubTwo', $container->tagged('foo')[1]);

         $container = new Container;
         $container->tag(['Illuminate\Tests\Container\ContainerImplementationStub', 'Illuminate\Tests\Container\ContainerImplementationStubTwo'], ['foo']);
         $this->assertCount(2, $container->tagged('foo'));
         $this->assertInstanceOf('Illuminate\Tests\Container\ContainerImplementationStub', $container->tagged('foo')[0]);
         $this->assertInstanceOf('Illuminate\Tests\Container\ContainerImplementationStubTwo', $container->tagged('foo')[1]);

          $this->assertEmpty($container->tagged('this_tag_does_not_exist'));
  }

extend扩展

extend是在当原来的类被注册或者实例化出来后,可以对其进行扩展,而且可以支持多重扩展:

public function testExtendInstancesArePreserved()
{
    $container = new Container;
    $container->bind('foo', function () {
        $obj = new StdClass;
        $obj->foo = 'bar';

        return $obj;
    });
 
    $obj = new StdClass;
    $obj->foo = 'foo';
    $container->instance('foo', $obj);
  
    $container->extend('foo', function ($obj, $container) {
        $obj->bar = 'baz';
        return $obj;
    });

    $container->extend('foo', function ($obj, $container) {
        $obj->baz = 'foo';
        return $obj;
    });

    $this->assertEquals('foo', $container->make('foo')->foo);
    $this->assertEquals('baz', $container->make('foo')->bar);
    $this->assertEquals('foo', $container->make('foo')->baz);
}

Rebounds与Rebinding

绑定是针对接口的,是为接口提供实现方式的方法。我们可以对接口在不同的时间段里提供不同的实现方法,一般来说,对同一个接口提供新的实现方法后,不会对已经实例化的对象产生任何影响。但是在一些场景下,在提供新的接口实现后,我们希望对已经实例化的对象重新做一些改变,这个就是 rebinding 函数的用途。
下面就是一个例子:

abstract class Car
{
    public function __construct(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }

    public function refuel($litres)
    {
        return $litres * $this->fuel->getPrice();
    }

    public function setFuel(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }

}

class JeepWrangler extends Car
{
  //
}

interface Fuel
{
    public function getPrice();
}

class Petrol implements Fuel
{
    public function getPrice()
    {
        return 130.7;
    }
}

我们在服务容器中是这样对car接口和fuel接口绑定的:

$this->app->bind('fuel', function ($app) {
    return new Petrol;
});

$this->app->bind('car', function ($app) {
    return new JeepWrangler($app['fuel']);
});

$this->app->make('car');

如果car被服务容器解析实例化成对象之后,有人修改了 fuel 接口的实现,从 Petrol 改为 PremiumPetrol:

$this->app->bind('fuel', function ($app) {
    return new PremiumPetrol;
});

由于 car 已经被实例化,那么这个接口实现的改变并不会影响到 car 的实现,假若我们想要 car 的成员变量 fuel 随着 fuel 接口的变化而变化,我们就需要一个回调函数,每当对 fuel 接口实现进行改变的时候,都要对 car 的 fuel 变量进行更新,这就是 rebinding 的用途:

$this->app->bindShared('car', function ($app) {
    return new JeepWrangler($app->rebinding('fuel', function ($app, $fuel) {
        $app['car']->setFuel($fuel);
    }));
});

服务别名

什么是服务别名

在说服务容器的解析之前,需要先说说服务的别名。什么是服务别名呢?不同于上一个博客中提到的 Facade 门面的别名(在 config/app 中定义),这里的别名服务绑定名称的别名。通过服务绑定的别名,在解析服务的时候,跟不使用别名的效果一致。别名的作用也是为了同时支持全类型的服务绑定名称以及简短的服务绑定名称考虑的。

通俗的讲,假如我们想要创建 auth 服务,我们既可以这样写:

$this->app->make('auth')

又可以写成:

$this->app->make('\Illuminate\Auth\AuthManager::class')

还可以写成

$this->app->make('\Illuminate\Contracts\Auth\Factory::class')

后面两个服务的名字都是 auth 的别名,使用别名和使用 auth 的效果是相同的。

服务别名的递归

需要注意的是别名是可以递归的:

app()->alias('service', 'alias_a');
app()->alias('alias_a', 'alias_b');
app()-alias('alias_b', 'alias_c');

会得到:

'alias_a' => 'service'
'alias_b' => 'alias_a'
'alias_c' => 'alias_b'

服务别名的实现

那么这些别名是如何加载到服务容器里面的呢?实际上,服务容器里面有个 aliases 数组:

$aliases = [
  'app' => [\Illuminate\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class],
  'auth' => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class],
  'auth.driver' => [\Illuminate\Contracts\Auth\Guard::class],
  'blade.compiler' => [\Illuminate\View\Compilers\BladeCompiler::class],
  'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
...
]

而服务容器的初始化的过程中,会运行一个函数:

public function registerCoreContainerAliases()
{
  foreach ($aliases as $key => $aliases) {
    foreach ($aliases as $alias) {
      $this->alias($key, $alias);
    }
  }
}

public function alias($abstract, $alias)
{
  $this->aliases[$alias] = $abstract;

  $this->abstractAliases[$abstract][] = $alias;
}

加载后,服务容器的aliases和abstractAliases数组:

$aliases = [
  'Illuminate\Foundation\Application' = "app"
  'Illuminate\Contracts\Container\Container' = "app"
  'Illuminate\Contracts\Foundation\Application' = "app"
  'Illuminate\Auth\AuthManager' = "auth"
  'Illuminate\Contracts\Auth\Factory' = "auth"
  'Illuminate\Contracts\Auth\Guard' = "auth.driver"
  'Illuminate\View\Compilers\BladeCompiler' = "blade.compiler"
  'Illuminate\Cache\CacheManager' = "cache"
  'Illuminate\Contracts\Cache\Factory' = "cache"
  ...
]
$abstractAliases = [
  app = {array} [3]
  0 = "Illuminate\Foundation\Application"
  1 = "Illuminate\Contracts\Container\Container"
  2 = "Illuminate\Contracts\Foundation\Application"
  auth = {array} [2]
  0 = "Illuminate\Auth\AuthManager"
  1 = "Illuminate\Contracts\Auth\Factory"
  auth.driver = {array} [1]
  0 = "Illuminate\Contracts\Auth\Guard"
  blade.compiler = {array} [1]
  0 = "Illuminate\View\Compilers\BladeCompiler"
  cache = {array} [2]
  0 = "Illuminate\Cache\CacheManager"
  1 = "Illuminate\Contracts\Cache\Factory"
  ...
]

服务解析

make 解析

有很多方式可以从容器中解析对象,首先,你可以使用 make 方法,该方法接收你想要解析的类名或接口名作为参数:

public function testAutoConcreteResolution()
{
  $container = new Container;
  $this->assertInstanceOf('Illuminate\Tests\Container\ContainerConcreteStub', 
         $container->make('Illuminate\Tests\Container\ContainerConcreteStub'));
}

//带有依赖注入和默认值的解析
public function testResolutionOfDefaultParameters()
{
     $container = new Container;
     $instance = $container->make('Illuminate\Tests\Container\ContainerDefaultValueStub');
     $this->assertInstanceOf('Illuminate\Tests\Container\ContainerConcreteStub', 
                              $instance->stub);
     $this->assertEquals('taylor', $instance->default);
}

//
public function testResolvingWithArrayOfParameters()
{
  $container = new Container;
  
  $instance = $container->makeWith(ContainerDefaultValueStub::class, ['default' => 'adam']);
  $this->assertEquals('adam', $instance->default);
  
  $instance = $container->make(ContainerDefaultValueStub::class);
  $this->assertEquals('taylor', $instance->default);
  
  $container->bind('foo', function ($app, $config) {
      return $config;
  });
  $this->assertEquals([1, 2, 3], $container->makeWith('foo', [1, 2, 3]));
}

public function testNestedDependencyResolution()
{
  $container = new Container;
  $container->bind('Illuminate\Tests\Container\IContainerContractStub', 'Illuminate\Tests\Container\ContainerImplementationStub');
  $class = $container->make('Illuminate\Tests\Container\ContainerNestedDependentStub');
  $this->assertInstanceOf('Illuminate\Tests\Container\ContainerDependentStub', $class->inner);
  $this->assertInstanceOf('Illuminate\Tests\Container\ContainerImplementationStub', $class->inner->impl);
  }

class ContainerDefaultValueStub
{
  public $stub;
  public $default;
  public function __construct(ContainerConcreteStub $stub, $default = 'taylor')
  {
      $this->stub = $stub;
      $this->default = $default;
  }
}

class ContainerConcreteStub
{
}

class ContainerImplementationStub implements IContainerContractStub
{
}

class ContainerDependentStub
{
  public $impl;
  public function __construct(IContainerContractStub $impl)
  {
      $this->impl = $impl;
  }
}
class ContainerNestedDependentStub
{
  public $inner;
  public function __construct(ContainerDependentStub $inner)
  {
      $this->inner = $inner;
  }
}

如果你所在的代码位置访问不了 $app 变量,可以使用辅助函数resolve:

$api = resolve('HelpSpot\API');

自动注入

namespace App\Http\Controllers;

use App\Users\Repository as UserRepository;

class UserController extends Controller{
  /**
  * 用户仓库实例
  */
  protected $users;

  /**
  * 创建一个控制器实例
  *
  * @param UserRepository $users 自动注入
  * @return void
  */
  public function __construct(UserRepository $users)
  {
    $this->users = $users;
  }
}

call 方法注入

make 解析是服务容器进行解析构建类对象时所用的方法,在实际应用中,还有另外一个需求,那就是当前已经获取了一个类对象,我们想要调用它的一个方法函数,这时发现这个方法中参数众多,如果一个个的 make 会比较繁琐,这个时候就要用到 call 解析了。我们可以看这个例子:

class TaskRepository{

    public function testContainerCall(User $user,Task $task){
        $this->assertInstanceOf(User::class, $user);

        $this->assertInstanceOf(Task::class, $task);
    }

    public static function testContainerCallStatic(User $user,Task $task){
        $this->assertInstanceOf(User::class, $user);

        $this->assertInstanceOf(Task::class, $task);
    }

    public function testCallback(){
        echo 'call callback successfully!';
    }

    public function testDefaultMethod(){
        echo 'default Method successfully!';
    }
}

闭包函数注入

  public function testCallWithDependencies()
  {
      $container = new Container;
      $result = $container->call(function (StdClass $foo, $bar = []) {
          return func_get_args();
      });

      $this->assertInstanceOf('stdClass', $result[0]);
      $this->assertEquals([], $result[1]);

      $result = $container->call(function (StdClass $foo, $bar = []) {
          return func_get_args();
      }, ['bar' => 'taylor']);

      $this->assertInstanceOf('stdClass', $result[0]);
      $this->assertEquals('taylor', $result[1]);
}

普通函数注入

public function testCallWithGlobalMethodName()
{
    $container = new Container;
    $result = $container->call('Illuminate\Tests\Container\containerTestInject');
    $this->assertInstanceOf('Illuminate\Tests\Container\ContainerConcreteStub', $result[0]);
    $this->assertEquals('taylor', $result[1]);
}

静态方法注入

服务容器的 call 解析主要依靠 call_user_func_array() 函数,关于这个函数可以查看 Laravel学习笔记之Callback Type - 来生做个漫画家,这个函数对类中的静态函数和非静态函数有一些区别,对于静态函数来说:

class ContainerCallTest
{
    public function testContainerCallStatic(){
        App::call(TaskRepository::class.'@testContainerCallStatic');
        App::call(TaskRepository::class.'::testContainerCallStatic');
        App::call([TaskRepository::class,'testContainerCallStatic']);
    }
}

服务容器调用类的静态方法有三种,注意第三种使用数组的形式,数组中可以直接传类名 TaskRepository::class;

非静态方法注入

对于类的非静态方法:

class ContainerCallTest
{
    public function testContainerCall(){
        $taskRepo = new TaskRepository();
        App::call(TaskRepository::class.'@testContainerCall');
        App::call([$taskRepo,'testContainerCall']);
    }
}

我们可以看到非静态方法只有两种调用方式,而且第二种数组传递的参数是类对象,原因就是 call_user_func_array函数的限制,对于非静态方法只能传递对象。

bindmethod 方法绑定

服务容器还有一个 bindmethod 的方法,可以绑定类的一个方法到自定义的函数:

public function testContainCallMethodBind(){

    App::bindMethod(TaskRepository::class.'@testContainerCallStatic',function () {
         $taskRepo = new TaskRepository();
         $taskRepo->testCallback();
    });

    App::call(TaskRepository::class.'@testContainerCallStatic');
    App::call(TaskRepository::class.'::testContainerCallStatic');
    App::call([TaskRepository::class,'testContainerCallStatic']);

    App::bindMethod(TaskRepository::class.'@testContainerCall',function (TaskRepository $taskRepo) { $taskRepo->testCallback(); });

    $taskRepo = new TaskRepository();
    App::call(TaskRepository::class.'@testContainerCall');
    App::call([$taskRepo,'testContainerCall']);
}

从结果上看,bindmethod 不会对静态的第二种解析方法( :: 解析方式)起作用,对于其他方式都会调用绑定的函数。

  public function testCallWithBoundMethod()
  {
      $container = new Container;
      $container->bindMethod('Illuminate\Tests\Container\ContainerTestCallStub@unresolvable', function ($stub) {
          return $stub->unresolvable('foo', 'bar');
      });
      $result = $container->call('Illuminate\Tests\Container\ContainerTestCallStub@unresolvable');
      $this->assertEquals(['foo', 'bar'], $result);

      $container = new Container;
      $container->bindMethod('Illuminate\Tests\Container\ContainerTestCallStub@unresolvable', function ($stub) {
          return $stub->unresolvable('foo', 'bar');
  });
      $result = $container->call([new ContainerTestCallStub, 'unresolvable']);
      $this->assertEquals(['foo', 'bar'], $result);
  }

class ContainerTestCallStub
{
    public function unresolvable($foo, $bar)
    {
        return func_get_args();
    }
}

默认函数注入

public function testContainCallDefultMethod(){

    App::call(TaskRepository::class,[],'testContainerCall');

    App::call(TaskRepository::class,[],'testContainerCallStatic');

    App::bindMethod(TaskRepository::class.'@testContainerCallStatic',function () {
        $taskRepo = new TaskRepository();
        $taskRepo->testCallback();
    });

    App::bindMethod(TaskRepository::class.'@testContainerCall',function (TaskRepository $taskRepo) {  $taskRepo->testCallback(); });

    App::call(TaskRepository::class,[],'testContainerCall');

    App::call(TaskRepository::class,[],'testContainerCallStatic');

}

值得注意的是,这种默认函数注入的方法使得非静态的方法也可以利用类名去调用,并不需要对象。默认函数注入也回受到 bindmethod 函数的影响。

数组解析

app()['service'];

app($service)的形式

app('service');

服务容器事件

每当服务容器解析一个对象时就会触发一个事件。你可以使用 resolving 方法监听这个事件:

$this->app->resolving(function ($object, $app) {
  // 解析任何类型的对象时都会调用该方法...
});
$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
  // 解析「HelpSpot\API」类型的对象时调用...
});
$this->app->afterResolving(function ($object, $app) {
  // 解析任何类型的对象后都会调用该方法...
});
$this->app->afterResolving(HelpSpot\API::class, function ($api, $app) {
  // 解析「HelpSpot\API」类型的对象后调用...
});

服务容器每次解析对象的时候,都会调用这些通过 resolving 和 afterResolving 函数传入的闭包函数,也就是触发这些事件。
注意:如果是单例,则只在解析时会触发一次

public function testResolvingCallbacksAreCalled()
{
    $container = new Container;
    $container->resolving(function ($object) {
        return $object->name = 'taylor';
    });
    $container->bind('foo', function () {
        return new StdClass;
    });
    $instance = $container->make('foo');

    $this->assertEquals('taylor', $instance->name);
}

public function testResolvingCallbacksAreCalledForType()
{
    $container = new Container;
    $container->resolving('StdClass', function ($object) {
        return $object->name = 'taylor';
    });
    $container->bind('foo', function () {
          return new StdClass;
    });
    $instance = $container->make('foo');

    $this->assertEquals('taylor', $instance->name);
}
public function testResolvingCallbacksShouldBeFiredWhenCalledWithAliases()
{
    $container = new Container;
    $container->alias('StdClass', 'std');
    $container->resolving('std', function ($object) {
        return $object->name = 'taylor';
    });
    $container->bind('foo', function () {
        return new StdClass;
    });
    $instance = $container->make('foo');

    $this->assertEquals('taylor', $instance->name);
}

装饰函数

容器的装饰函数有两种,wrap用于装饰call,factory用于装饰make:

public function testContainerWrap()
{
      $result = $container->wrap(function (StdClass $foo, $bar = []) {
          return func_get_args();
      }, ['bar' => 'taylor']);

      $this->assertInstanceOf('Closure', $result);
      $result = $result();

      $this->assertInstanceOf('stdClass', $result[0]);
      $this->assertEquals('taylor', $result[1]);
  }

public function testContainerGetFactory()
{
    $container = new Container;
    $container->bind('name', function () {
        return 'Taylor’;
    });
    $factory = $container->factory('name');
    $this->assertEquals($container->make('name'), $factory());
}

容器重置flush

容器的重置函数flush会清空容器内部的aliases、abstractAliases、resolved、bindings、instances

public function testContainerFlushFlushesAllBindingsAliasesAndResolvedInstances()
{
    $container = new Container;
    $container->bind('ConcreteStub', function () {
        return new ContainerConcreteStub;
    }, true);
    $container->alias('ConcreteStub', 'ContainerConcreteStub');
    
    $concreteStubInstance = $container->make('ConcreteStub');
    $this->assertTrue($container->resolved('ConcreteStub'));
    $this->assertTrue($container->isAlias('ContainerConcreteStub'));
    $this->assertArrayHasKey('ConcreteStub', $container->getBindings());
    $this->assertTrue($container->isShared('ConcreteStub'));
    
    $container->flush();
    $this->assertFalse($container->resolved('ConcreteStub'));
    $this->assertFalse($container->isAlias('ContainerConcreteStub'));
    $this->assertEmpty($container->getBindings());
    $this->assertFalse($container->isShared('ConcreteStub'));
}

Written with StackEdit.

查看原文

赞 7 收藏 7 评论 2

hi 赞了文章 · 2018-07-30

【Redis学习笔记】Redis跳表简析(插入过程)

作者: 顺风车运营研发团队 闫昌

node和list的定义如下:

typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;
typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

1、node的创建:

zskiplistNode *zslCreateNode(int level, double score, sds ele) {
    zskiplistNode *zn =
        zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    zn->score = score;
    zn->ele = ele;
    return zn;
}

从代码中可以看出,创建节点的内存为node本身大小+level个(struct zskiplistLevel)大小;

2、createList函数

/* Create a new skiplist. */
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;

    zsl = zmalloc(sizeof(*zsl));
    zsl->level = 1;
    zsl->length = 0;
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);//创建一个新节点, 节点的level为32, score为0
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {//循环32遍
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    zsl->header->backward = NULL;
    zsl->tail = NULL;
    return zsl;
}

创建结果如图:

clipboard.png

3、zslInsert函数

zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    serverAssert(!isnan(score));
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        /* store rank that is crossed to reach the insert position */
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        while (x->level[i].forward &&
                (x->level[i].forward->score < score ||
                    (x->level[i].forward->score == score &&
                    sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    /* we assume the element is not already inside, since we allow duplicated
     * scores, reinserting the same element should never happen since the
     * caller of zslInsert() should test in the hash table if the element is
     * already inside or not. */
    level = zslRandomLevel();
    if (level > zsl->level) {
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;
            update[i] = zsl->header;
            update[i]->level[i].span = zsl->length;
        }
        zsl->level = level;
    }
    x = zslCreateNode(level,score,ele);
    for (i = 0; i < level; i++) {
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;

        /* update span covered by update[i] as x is inserted here */
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    /* increment span for untouched levels */
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }

    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    zsl->length++;
    return x;
}

下面详细阐述下插入过程:

一. 插入节点: level = 1, score = 1

第138行: x = zsl->header, 现在将x赋值为header, 参考图一

第139行: i = zsl->level-1 ===> 0, 所以这个for循环可以进入一次

第141行: i =0, zsl->level-1 = 0, 两个值相等. 所以rank[0] = 0

第142行: x->level[0]->forward = NULL, 所以这个while进不去

第150行: update[0] = x, 所以现在update[0]的值为header指向的节点

clipboard.png

第156行. level = 1

第157行: zsl->level = 1, 所以这个if进不去

第165行: 新建一个node节点x, level = 1, score = 1

第166行: for循环可以进入一次

第167行: x->level[0]->forward = NULL

第168行: update[0]->level[0].forward = x

第171行: x->level[0].span = update[0]->level[0].span = 0

第172行: update[0]->level[0].span = 1

clipboard.png

第176行: for条件不满足

第180行: x->backward = NULL

clipboard.png

第181行: if条件不满足, 进入else

第185行: zsl->length++

clipboard.png

二. 插入level = 1, score = 2

第138行: x = zsl->header, 现在将x赋值为header, 参考图一

第139行: i = zsl->level-1 ===> 0, 所以这个for循环可以进入一次

第141行: i =0, zsl->level-1 = 0, 两个值相等. 所以rank[0] = 0

第142行: x->level[0]->forward 不为空, x->level[0].forward->score = 1, 小于要插入的2, 所以while可以进入

第147行: rank[0] = 1

第148行: x = 上一步插入的节点

第150行: update[0] = 上一步插入的节点

clipboard.png

第156行: level = 1

第157行: zsl->level = 1, 所以这个if进不去

第165行: 新建一个node节点x, level = 1, score = 2

第166行: for循环可以进入一次

第167行: x->level[0]->forward = NULL

第168行: update[0]->level[0].forward = x

第171行: x->level[0].span = update[0]->level[0].span = 0

第172行: update[0]->level[0].span = 1

clipboard.png

第176行: for条件不满足

第180行: x->backward = update[0]

clipboard.png

第181行: if条件不满足, 进入else

第185行: zsl->length++

clipboard.png

三. 插入level = 2, score = 3

第138行: x = zsl->header, 现在将x赋值为header, 参考图一

第139行: i = zsl->level-1 ===> 0, 所以这个for循环可以进入一次

第141行: i =0, zsl->level-1 = 0, 两个值相等. 所以rank[0] = 0

第142行: x->level[0]->forward 不为空, x->level[0].forward->score = 1, 小于要插入的2, 所以while可以进入, 且由于x->level[0].forward->level[0]->foreard不为空, 所以while可以循环两次

第147行: 第一次while循环, rank[0] = 1, 第二次while循环, rank[0] = rank[0] + 1 = 2

第148行: x = 上一步插入的节点

第150行: update[0] = 上一步插入的节点

clipboard.png

第156行: level = 2

第157行: level = 2, zsl->level = 1, 所以这个if可以进去

第158行: for循环可以进入一次

第159行: rank[1] = 0

第160行: update[1] = header

第161行: update[1]->level[1].span = zsl->length = 2

第163行: zsl->level = 2

clipboard.png

第165行: 新建一个node节点x, level = 2, score = 3

第166行: for循环可以进入两次

第一次for循环:

第167行: x->level[0]->forward = NULL

第168行: update[0]->level[0].forward = x

第171行: x->level[0].span = update[0]->level[0].span = 0

第172行: update[0]->level[0].span = 1

第二次for循环:

第167行: x->level[1]->forward = NULL

第168行: update[1]->level[1].forward = x

第171行: x->level[1].span = update[1]->level[1].span = 2

第172行: update[1]->level[1].span = 3

clipboard.png

第176行: for条件不满足

第180行: update[0] != zsl->header, 所以x->backward=update[0]

clipboard.png

第181行: if条件不满足

第184行: zsl->header = x

第185行: zsl->length++

clipboard.png

四. 插入level=2, score = 1.5

第138行: x = zsl->header, 现在将x赋值为header, 参考图一

第139行: i = zsl->level-1 ===> 1, 所以这个for循环可以进入两次

第一次for循环

第141行: i =1, zsl->level-1 = 0, 两个值相等. 所以rank[1] = 0

第142行: x->level[1]->forward 不为空, x->level[1].forward->score = 3, 大于要插入的0.5, 所以while进不去

第150行: update[1] = header

第二次for循环

第141行: i =0, zsl->level-1 = 1, 两个值不相等. 所以rank[0] = rank[1] = 0

第142行: x->level[0]->forward 不为空, x->level[0].forward->score = 1, 小于要插入的1.5, 所以while可以进入

第147行: header->level[0].span = 1, rank[0] += 1, 所以rank[0] = 1

第148行: x = header->level[0].forward, 由于此时x的score = 2所以while循环不会再进入

第150行: update[0] = 之前插入的第一个节点, 即score = 1的节点

clipboard.png

第156行: level = 2

第157行: level = 2, zsl->level = 2, 所以这个if进不去

第165行: 新建一个node节点x, level = 2, score = 1.5

clipboard.png

第166行: for循环可以进入两次

第一次for循环:

第167行: x->level[0]->forward = score为2的节点

第168行: update[0]->level[0].forward = x

第171行: x->level[0].span = update[0]->level[0].span = 1

第172行: update[0]->level[0].span = 1

clipboard.png

第二次for循环:

第167行: x->level[1]->forward = 最后一个插入的score=3的节点

第168行: update[1]->level[1].forward = x

第171行: x->level[1].span = update[1]->level[1].span - (1 - 0) = 2

第172行: update[1]->level[1].span = (1-0) + 1 = 2

clipboard.png

第176行: for条件不满足

第180行: x->backward = update[0]

clipboard.png

第181行: if条件满足

第182行: score=2的节点的backward指向新生成的节点

第185行: length++

clipboard.png

到此我们了解了跳跃表的插入过程。

待续

查看原文

赞 5 收藏 5 评论 0

hi 收藏了文章 · 2018-07-24

【PHP7源码分析】PHP7源码研究之浅谈Zend虚拟机

作者 陈雷
编程语言的虚拟机是一种可以运行中间语言的程序。中间语言是抽象出的指令集,由原生语言编译而成,作为虚拟机执行阶段的输入。很多语言都实现了自己的虚拟机,比如Java、C#和Lua。PHP语言也有自己的虚拟机,称为Zend虚拟机

PHP7完成基本的准备工作后,会启动Zend引擎,加载注册的扩展模块,然后读取对应的脚本文件,Zend引擎会对文件进行词法和语法分析,生成抽象语法树,接着抽象语法树被编译成Opcodes,如果开启了Opcache,编译的环节会被跳过从Opcache中直接读取Opcodes进行执行。

PHP7中词法语法分析,生成抽象语法树,然后编译成Opcodes及被执行均由Zend虚拟机完成。这里将详细阐述抽象语法树编译成Opcodes的过程,以及Opcodes被执行的过程,来阐述Zend虚拟机的实现原理及关键的数据结构。

1 基础知识

Zend虚拟机(称为Zend VM)是PHP语言的核心,承担了语法词法解析、抽象语法树编译以及指令的执行工作,下面我们讨论一下Zend虚拟机的基础架构以及相关的基础知识。

1.1 Zend虚拟机架构

Zend虚拟机主要分为解释层、中间数据层和执行层,下面给出各层包含的内容,如图1所示。

clipboard.png
图1 Zend虚拟机架构图

下面解释下各层的作用。

(1)解释层

这一层主要负责把PHP代码进行词法和语法分析,生成对应的抽象语法树;另一个工作就是把抽象语法树进行编译,生成符号表和指令集;

(2)中间数据层

这一层主要包含了虚拟机的核心部分,执行栈的维护,指令集和符号表的存储,而这三个是执行引擎调度执行的基础;

(3)执行层

这一层是执行指令集的引擎,这一层是最终的执行并生成结果,这一层里面实现了大量的底层函数。

为了更好地理解Zend虚拟机各层的工作,我们先了解一下物理机的一些基础知识,读者可以对照理解虚拟机的原理。

1.2 符号表

符号表是在编译过程中,编译程序用来记录源程序中各种名字的特性信息,所以也称为名字特性表。名字一般包含程序名、过程名、函数名、用户定义类型名、变量名、常量名、枚举值名、标号名等。特性信息指的是名字的种类、类型、维数、参数个数、数值及目标地址(存储单元地址)等。

符号表有什么作用呢?一是协助进行语义检查,比如检查一个名字的引用和之前的声明是否相符,二是协助中间代码生成,最重要的是在目标代码生成阶段,当需要为名字分配地址时,符号表中的信息将是地址分配的主要依据。

clipboard.png
图2 符号表创建示例

符号表一般有三种构造和处理方法,分别是线性查找,二叉树和Hash技术,其中线性查找法是最简单的,按照符号出现的顺序填表,每次查找从第一个开始顺序查找,效率比较低;二叉树实现了对折查找,在一定程度上提高了效率;效率最高的是通过Hash技术实现符号表,相信大家对Hash技术有一定的了解,而PHP7中符号表就是使用的HashTable实现的。

1.3 函数调用栈

为了更清晰地了解虚拟机中函数调用的过程,我们先了解一下物理机的简单原理,主要涉及函数调用栈的概念,而Zend虚拟机参照了物理机的基本原理,做了类似的设计。

下面以一段C代码描述一下系统栈和函数过程调用,代码如下:

  int funcB(int argB1, int argB2)

  {

         int varB1, varB2;

         return argB1+argB2;

  }

  int funcA(int argA1, int argA2)

  {

       int varA1, varA2;

        return argA1+argA2+funcB( 3, 4);

  }    

  int main()

  {

      int varMain;

      return funcA(1, 2);

     }

这段代码运行时,首先main函数会压栈, 首先将局部变量varMain入栈,main函数调用了funcA函数,C语言会从后往前,将函数参数压栈,先压第二个参数argA2=2,再压第一个参数argA1=1,同时对于funcA的返回会产生一个临时变量等待赋值,也会被压栈,这些称为main函数的栈帧;接着将funcA压栈,同样的先将局部变量varA1和varA2压入栈中,因为调用了函数funcB,会将参数argB2=4和argB1=3压入栈中,同时把funcB的返回产生的临时变量压入栈中,这部分称为funcA的栈帧;同样,funcB被压入栈中,如图3所示。

clipboard.png
图3 函数调用压栈过程示意图

funcB函数执行,对argB1和argB2进行相加操作,执行后得到返回值为7,然后funcB的栈帧出栈,funcA中临时变量TempB被赋值为7,继而进行相加操作,得到结果为10,然后funcA出栈,main函数中临时变量TempA被赋值为10,最终main函数返回并出栈,整个函数调用结束。如图4所示。

clipboard.png
图4 函数调用出栈过程示意图

1.4 指令

汇编语句中的指令语句一般格式为:

     [标号:]     [前缀]  指令助记符    [操作数]     [;注释]

其中:

  • 1)标识符字段由各种有效字符组成,一般表示符号地址,具有段基址、偏移量、类型三种属性。通常情况下这部分是可选部分,主要为便于程序的读写方便而使用。
  • 2)助记符,规定指令或伪指令的操作功能,是语句中唯一不可缺少的部分。对于指令,汇编程序会将其翻译成机器语言指令:
MOV   AX, 100  →   B8 00 01
  • 3)操作数,指令语句中提供给指令的操作对象、存放位置。操作数可以是1个、2个或0个,2个时用逗号‘,’分开。比如“RET;”对应的操作数个数是0个,“INC BX;”对应的操作数个数是1,“MOV AX,DATA;”对应的操作数个数是2个。
  • 4)注释,以“ ;”开始,给以编程说明。

    符号表、函数调用栈以及指令基本构成了物理机执行的基本元素,Zend虚拟机也同样实现了符号表,函数调用栈及指令,来运行PHP代码,下面我先讨论一下Zend虚拟机相关的数据结构。

2相关数据结构

Zend虚拟机包含了词法语法分析,抽象语法树的编译,以及Opcodes的执行,本文主要详细介绍抽象语法树和Opcodes的执行过程,在展开介绍之前,先阐述一下用到的基本的数据结构,为后面原理性的介绍奠定基础。

2.1 EG(v)

首先介绍的是全局变量executor_globals,EG(v)是对应的取值宏,executor_globals对应的是结构体_zend_executor_globals,是PHP生命周期中非常核心的数据结构。这个结构体中维护了符号表(symbol_table, function_table,class_table等),执行栈(zend_vm_stack)以及包含执行指令的zend_execute_data,另外还包含了include的文件列表,autoload函数,异常处理handler等重要信息,下面给出_zend_executor_globals的结构图,然后分别阐述其含义,如图5所示。

clipboard.png
图5 EG(v)结构图

这个结构体比较复杂,下面我们介绍几个核心的成员。

  • 1)symbol_table:符号表,这里面主要是存的全局变量,以及一些魔术变量,比如$_GET、$_POST等;
  • 2)function_table:函数表,主要存放函数,包括大量的内部函数,以及用户自定义的函数,比如zend_version,func_num_args,str系列函数,等等;
  • 3)class_table:类表,主要存放内置的类以及用户自定义的类,比如stdclass、throwable、exception等类;
  • 4)zend_constants:常量表,存放PHP中的常量,比如E_ERROR、E_WARNING等;
  • 5)vm_stack:虚拟机的栈,执行时压栈出栈都在这上面操作;
  • 6)current_execute_data:对应_zend_execute_data结构体,存放执行时的数据。

下面针对于符号表、指令集、执行数据和执行栈进行详细介绍。

2.2 符号表

PHP7中符号表分为了symbol_table、function_table和class_table等。

  • (1)symbol_table

symbol_table里面存放了变量信息,其类型是HashTable,下面我们看一下具体的定义:

       //符号表缓存

       zend_array *symtable_cache[SYMTABLE_CACHE_SIZE];

      zend_array **symtable_cache_limit;

      zend_array **symtable_cache_ptr;

       //符号表

       zend_array symbol_table;

symbol_table里面有什么呢,代码”$a=1;”对应的symnol_table,如图6所示。

clipboard.png
图6 symbol_table示意图

从图6中可以看出,符号表中有我们常见的超全局变量$_GET、$_POST等,还有全局变量$a。在编译过程中会调用zend_attach_symbol_table函数将变量加入symbol_table中。

  • (2)function_table

function_table对应的是函数表,其类型也是HashTable,见代码:

       HashTable *function_table;  /* function symbol table */

函数表中存储哪些函数呢?同样以上述代码为例,我们利用GDB印一下function_table的内容:

(gdb) p *executor_globals.function_table

$1 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 0}, type_info = 7}}, u = {v = {

      flags = 25 '\031', nApplyCount = 0 '\000', nIteratorsCount = 0 '\000', consistency = 0 '\000'},

    flags = 25}, nTableMask = 4294966272, arData = 0x12102b0, nNumUsed = 848, nNumOfElements = 848,

  nTableSize = 1024, nInternalPointer = 0, nNextFreeElement = 0, pDestructor = 0x8d0dc3 <zend_function_dtor>}

可以看出,函数表中有大量的函数,上面打印显示有848个之多,这里面主要是内部函数,比如zend_version、func_num_args、cli_get_process_title,等等。

  • (3)class_table

class_table对应的是类表,其也是HashTable:

 HashTable *class_table; /* class table */

类表里面也有大量的内置类,比如stdclass、traversable、xmlreader等。

符号表里面存放了执行时需要的数据,比如在symbol_table中,key为_GET的Bucket对应的又是个HashTable,里面存放的是$_GET[xxx],执行时会从中取对应的值。

2.3 指令

Zend虚拟机的指令称为opline,每条指令对应一个opcode。PHP代码在编译后生成opline,Zend虚拟机根据不同的opline完成PHP代码的执行,opline由操作指令、操作数和返回值组成,与机器指令非常类似,opline对应的结构体为zend_op,见代码:

  struct _zend_op {

      const void *handler; //操作执行的函数

      znode_op op1; //操作数1

      znode_op op2; //操作数2

      znode_op result; //返回值

      uint32_t extended_value; //扩展值

      uint32_t lineno; //行号

      zend_uchar opcode; //opcode值

      zend_uchar op1_type; //操作数1的类型

      zend_uchar op2_type; //操作数2的类型

      zend_uchar result_type; //返回值的类型

};

对应的内存占用图如图7所示。

clipboard.png
图7 zend_op结构图

PHP代码会被编译成一条一条的opline,分解为最基本的操作,举个例子,如果把opcode当成一个计算器,只接受两个操作数op1和 op2,执行一个操作handler,比如加减乘除,然后它返回一个结果result,再稍加处理算术溢出的情况存在extended_value中。下面详细介绍下各个字段。

  • (1) Opcode

Opcode有时候被称为所谓的字节码(Bytecode),是被软件解释器解释执行的指令集。这些软件指令集通常会提供一些比对应的硬件指令集更高级的数据类型和操作。

注意:Opcode和Bytecode其实是两个含义不同的词,但经常会把它们当作同一个意思来交互使用。

Zend虚拟机有非常多Opcode,对应可以做非常多事情,并且随着PHP的发展, Opcode也越来越多,意味着PHP可以做越来越多的事情。所有的Opcode都在PHP的源代码文件Zend/zend_vm_opcodes.h中定义。Opcode的名称是自描述的,比如:

  • ZEND_ASSGIN:赋值操作;
  • ZEND_ADD:两个数相加操作;
  • ZEND_JMP:跳转操作。

PHP 7.1.0中有186个Opcode:

#define ZEND_NOP                               0

#define ZEND_ADD                               1

#define ZEND_SUB                               2

#define ZEND_MUL                               3

#define ZEND_DIV                               4

#define ZEND_MOD                               5

#define ZEND_SL                                6

…

#define ZEND_FETCH_THIS                      184

#define ZEND_ISSET_ISEMPTY_THIS              186

#define ZEND_VM_LAST_OPCODE                  186
  • (2)操作数

op1和op2都是操作数,但不一定全部使用,也就是说每个Opcode对应的hanlder最多可以使用两个操作数(也可以只使用其中一个,或者都不使用)。每个操作数都可以理解为函数的参数,返回值result是hanlder函数对操作数op1和op2计算后的结果。op1、op2和result对应的类型都是znode_op,其定义为一个联合体:

typedef union _znode_op {

      uint32_t      constant;

      uint32_t      var;

      uint32_t      num;

      uint32_t      opline_num; /*  Needs to be signed */

#if ZEND_USE_ABS_JMP_ADDR

      zend_op       *jmp_addr;

#else

      uint32_t      jmp_offset;

#endif

#if ZEND_USE_ABS_CONST_ADDR

      zval          *zv;

#endif

} znode_op;

这样其实每个操作数都是uint32类型的数字,一般表示的是变量的位置。操作数有5种不同的类型,具体在Zend/zend_compile.h中定义:

#define IS_CONST        (1<<0)

#define IS_TMP_VAR      (1<<1)

#define IS_VAR          (1<<2)

#define IS_UNUSED       (1<<3)   /* Unused variable */

#define IS_CV           (1<<4)   /* Compiled variable */

这些类型是按位表示的,具体含义如下。

  • 1)IS_CONST:值为1,表示一个常量,都是只读的,值不可改变,比如$a=”hello world” 中的hello world。
  • 2)IS_VAR:值为4,是PHP变量,这个变量并不是PHP代码中声明的变量,常见的是返回的临时变量,比如$a=time(), 函数time返回值的类型就是IS_VAR,这种类型的变量是可以被其他Opcode对应的handler重复使用的。
  • 3)IS_TMP_VAR :值为2,也是PHP变量,跟IS_VAR不同之处是,不能与其他Opcode重复使用,举个例子,$a=”123”.time(); 这里拼接的临时变量”123”.time()的类型就是IS_TMP_VAR,一般用于操作的中间结果。
  • 4)IS_UNUSED :值为8,表示这个操作数没有包含任何有意义的东西。
  • 5)IS_CV :值为16,编译变量(Compiled Variable):这个操作数类型表示一个PHP变量:以$something形式在PHP脚本中出现的变量。
  • (3)handler

handler为Opcode对应的是实际的处理函数,Zend虚拟机对每个Opcode的工作方式是完全相同的,都有一个handler的函数指针,指向处理函数的地址,这是一个C函数,包含了执行Opcode对应的代码,使用op1,op2做为参数,执行完成后,会返回一个结果result,有时也会附加一段信息extended_value。文件Zend/zend_vm_execute.h包含了所有的handler对应的函数,php-7.1.0中这个文件有62000+行。

注意:Zend/zend_vm_execute.h并非手动编写的,而是由zend_vm_gen.php这个PHP脚本解析zend_vm_def.h和zend_vm_execute.skl后生成,这个很有意思,先有鸡还是先有蛋?没有PHP 哪来的这个php脚本呢?这个是后期产物,早期php版本不用这个。这个类似于GO语言的自举,自己编译自己。

同一个Opcode对应的handler函数会根据操作数的类型而不同,比如ZEND_ASSIGN对应的handler就有多个:

ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CV_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CV_RETVAL_USED_HANDLER,

其函数命名是有如下规则的:

ZEND_[opcode]_SPEC_(操作数1类型)_(操作数2类型)_(返回值类型)_HANDLER

举个例子,对于PHP代码:

$a = 1;

对应的handler为ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,其定义为:

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

{

      USE_OPLINE

 

      zval *value;

      zval *variable_ptr;

 

      SAVE_OPLINE();

        //获取op2对应的值,也就是1

      value = EX_CONSTANT(opline->op2);

        //在execute_data中获取op1的位置,也就是$a

      variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);

        /*代码省略*/

       //将1赋值给$a

      value = zend_assign_to_variable(variable_ptr, value, IS_CONST);

              

      }

         /*代码省略*/

      ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

}

从代码中可以非常直观的看出,常量1是如何赋值给CV类型的$a的。

  • (4)extended_value

extended_value是存的扩展信息,Opcodes和CPU的指令类似,有一个标示指令字段handler,以及这个Opcode所操作的操作数op1和op2,但PHP不像汇编那么底层, 在脚本实际执行的时候可能还需要其他更多的信息,extended_value字段就保存了这类信息;

  • (5)lineno

lineno对应源代码文件中的行号。

到这里,相信读者对指令opline有了比较深刻的认识,在Zend虚拟机执行时,这些指令被组装在一起,成为指令集,下面我们介绍一下指令集。

2.4 指令集

在介绍指令集前,需要先介绍一个编译过程用到的一个基础的结构体znode,其结构如下:

  typedef struct _znode { /* used only during compilation */

      zend_uchar op_type;//变量类型

      zend_uchar flag;

      union {

               //表示变量的位置

               znode_op op;

                 //常量

               zval constant; /* replaced by literal/zv */

      } u;

} znode

znode只会在编译过程中使用,其中op_type对应的是变量的类型,u是联合体,u.op是操作数的类型,zval constant用来存常量。znode在后续生成指令集时会使用到。

Zend虚拟机中的指令集对应的结构为zend_op_array,其结构如下:

  struct _zend_op_array {

      /* Common elements */

      /*代码省略common是为了函数能够快速访问Opcodes而设定的*/

      /* END of common elements */

      //这部分是存放opline的数组,last为总个数

      uint32_t last;

      zend_op *opcodes;

     

      int last_var;//变量类型为IS_CV的个数

      uint32_t T;//变量类型为IS_VAR和IS_TMP_VAR的个数

      zend_string **vars;//存放IS_CV类型变量的数组

       /*代码省略*/

      /* static variables support */

      HashTable *static_variables; //静态变量

        /*代码省略*/

      int last_literal;//常量的个数

      zval *literals;//常量数组

 

      int  cache_size;//运行时缓存数组大小

      void **run_time_cache;//运行时缓存

 

      void *reserved[ZEND_MAX_RESERVED_RESOURCES];

};

其结构图如图8所示。

clipboard.png
图8 zend_op_array结构图

这个结构体中有几个关键变量。

  • 1)last和opcodes,这部分是存放Opline的数组,也就是指令集存放的位置,其中last为数组中Opline的个数。
  • 2)last_var代表IS_CV类型变量的个数,这种类型变量存放在vars数组中;在整个编译过程中,每次遇到一个IS_CV类型的变量(类似于$something),就会去遍历vars数组,检查是否已经存在,如果不存在,则插入到vars中,并将last_var的值设置为该变量的操作数;如果存在,则使用之前分配的操作数,见代码:
 result->op_type = IS_CV;

 result->u.op.var = lookup_cv(CG(active_op_array), name);

 

  //lookup_cv:

  static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{

      int i = 0;

      zend_ulong hash_value = zend_string_hash_val(name);

 

       //遍历vars

      while (i < op_array->last_var) {

                  //如果存在直接返回

               if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) ||

                   (ZSTR_H(op_array->vars[i]) == hash_value &&

                    ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) &&

                    memcmp(ZSTR_VAL(op_array->vars[i]), ZSTR_VAL(name), ZSTR_LEN(name)) == 0)) {

                        zend_string_release(name);

                        return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

               }

               i++;

      }

       //否则插入到vars中,并将last_var的值设置为该变量的操作数

      i = op_array->last_var;

      op_array->last_var++;

      if (op_array->last_var > CG(context).vars_size) {

               CG(context).vars_size += 16; /* FIXME */

               op_array->vars = erealloc(op_array->vars, CG(context).vars_size * sizeof(zend_string*));

      }

 

      op_array->vars[i] = zend_new_interned_string(name);

      return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

}
  • 3)T为IS_VAR和IS_TMP_VAR类型变量的总数,编译时遇到这种类型,T就会加一,用于后续在执行栈上分配空间。
  • 4)static_variables是用于存放静态变量的HashTable。
  • 5)literals是用来存放常量(IS_CONST)类型的数组,last_literal为常量的个数。
  • 6)run_time_cache用作运行时缓存的操作,本书不展开讨论。

2.5 执行数据

Zend在栈上执行的数据为zend_execute_data,其结构体为:

struct _zend_execute_data {

      const zend_op       *opline;           /* 要执行的指令 */

      zend_execute_data   *call;             /* current call*/

      zval                *return_value;     /* 返回值 */

      zend_function       *func;             /* 执行函数 */

      zval                 This;             /* this + call_info + num_args */

      zend_execute_data   *prev_execute_data;

      zend_array          *symbol_table; /*符号表*/

      void               **run_time_cache;   /* 执行时缓存 */

      zval                *literals;         /* 缓存常量 */

};

下面我们介绍下各字段。

  • 1)opline对应的是zend_op_array中Opcodes数组里面的zend_op,表示正在执行的opline。
  • 2)prev_execute_data: op_array上下文切换的时候,这个字段用来保存切换前的op_array,此字段非常重要,能将每个op_array的execute_data按照调用的先后顺序连接成一个单链表,每当一个op_array执行结束要还原到调用前op_array的时候,就通过当前的execute_data中的prev_execute_data字段来得到调用前的执行器数据。
  • 3)symbol_table,当前使用的符号表,一般会取EG(symbol_table)。
  • 4)literals,常量数组,用来缓存常量。

zend_execute_data是在执行栈上运行的关键数据,可以用EX宏来取其中的值,见代码:

#define EX(element) ((execute_data)->element)

了解完执行数据,下面接下来讨论一下执行栈。

2.6 执行栈

Zend虚拟机中有个类似函数调用栈的结构体,叫_zend_vm_stack。EG里面的vm_stack也是这种类型的。其定义如下:

struct _zend_vm_stack {

      zval *top; //栈顶位置

      zval *end; //栈底位置

      zend_vm_stack prev;

};

typedef struct _zend_vm_stack *zend_vm_stack;

可以看出,栈的结构比较简单,有三个变量top指向栈使用到的位置,end指向栈底,pre是指向上一个栈的指针,也就意味着所有栈在一个单向链表上。
PHP代码在执行时,参数的压栈操作,以及出栈调用执行函数都是在栈上进行的,下面介绍下栈操作的核心函数。

  • 1)初始化

初始化调用的函数为zend_vm_stack_init,主要进行内存申请,以及对_zend_vm_stack成员变量的初始化,见代码:

ZEND_API void zend_vm_stack_init(void)

{

      EG(vm_stack) = zend_vm_stack_new_page(ZEND_VM_STACK_PAGE_SIZE(0 /* main stack */), NULL);

      EG(vm_stack)->top++;

      EG(vm_stack_top) = EG(vm_stack)->top;

      EG(vm_stack_end) = EG(vm_stack)->end;

}

该函数调首先调用zend_vm_stack_new_page为EG(vm_stack)申请内存,申请的大小为161024 sizeof(zval),见代码:

static zend_always_inline zend_vm_stack zend_vm_stack_new_page(size_t size, zend_vm_stack prev) {

      zend_vm_stack page = (zend_vm_stack)emalloc(size);

 

      page->top = ZEND_VM_STACK_ELEMENTS(page);

      page->end = (zval*)((char*)page + size);

      page->prev = prev;

      return page;

}

然后将zend_vm_stack的top指向zend_vm_stack的结束的位置,其中 zend_vm_stack占用24个字节,end指向申请内存的最尾部,pre指向null,如图9所示。

clipboard.png
图9 zend_vm_stack初始化后示意图

可以看出,多个vm_stack构成单链表,将多个栈连接起来,栈初始的大小为16×1024个zval的大小,栈顶部占用了一个*zval和struct _zend_vm_stack的大小,

  • (2)入栈操作

调用的函数为zend_vm_stack_push_call_frame,见代码:

static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame(uint32_t call_info, zend_function *func, uint32_t num_args, zend_class_entry *called_scope, zend_object *object)

{

      uint32_t used_stack = zend_vm_calc_used_stack(num_args, func);

 

      return zend_vm_stack_push_call_frame_ex(used_stack, call_info,

               func, num_args, called_scope, object);

}

该函数会分配一块用于当前作用域的内存空间,并返回zend_execute_data的起始位置。首先调用zend_vm_calc_used_stack计算栈需要的空间,见代码:

 static zend_always_inline uint32_t zend_vm_calc_used_stack(uint32_t num_args, zend_function *func)

{

      uint32_t used_stack = ZEND_CALL_FRAME_SLOT + num_args;

 

      if (EXPECTED(ZEND_USER_CODE(func->type))) {

               used_stack += func->op_array.last_var + func->op_array.T - MIN(func->op_array.num_args, num_args);

      }

      return used_stack * sizeof(zval);

}

这段代码是按照zval的大小对齐,我们知道zval为16字节,那么对于zend_execute_data,大小为80,那么对应5个zval;同时对应IS_CV类型变量个数(last_var)以及变量类型为IS_VAR和IS_TMP_VAR的个数(T),如图10所示。

clipboard.png
图10 压栈过程

到此,我们了解了Zend虚拟机中符号表、指令集、执行数据以及执行栈相关的数据结构,下面我们基于这些基本知识,来介绍一下指令集生成的过程。

3 抽象语法树编译过程

抽象语法树(AST)的编译是生成指令集Opcodes的过程,词法语法分析后生成的AST会保存在CG(ast)中,然后Zend虚拟机会将AST进一步转换为zend_op_array,以便在虚拟机中执行。下面我们讨论一下AST的编译过程。

编译过程在zend_compile函数中,在该函数里,首先调用zendparse做了词法和语法分析的工作,然后开始对CG(ast)的遍历,根据节点不同的类型编译为不同指令opline,代码如下:

static zend_op_array *zend_compile(int type)

{

      /**代码省略**/

      if (!zendparse()) { //词法语法分析

               /**代码省略**/    

 

                 //初始化zend_op_array       

                 init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);

               /**代码省略**/

 

                 //遍历ast生成opline

               zend_compile_top_stmt(CG(ast));

               /**代码省略**/

 

                //设置handler

               pass_two(op_array);

                /**代码省略**/

      }

       /**代码省略**/

      return op_array;

}

从上面的过程中可以看出,编译的主要过程是op_array的初始化,调用zend_compile_top_stmt遍历抽象语法树成opline,以及调用pass_two函数设置handler。下面我们一一阐述。

3.1 op_array初始化

在遍历抽象语法树之前,需要先初始化指令集op_array,用来存放指令。op_array的初始化工作,调用的函数为init_op_array,该函数会将op_array进行初始化,代码如下:

op_array = emalloc(sizeof(zend_op_array));

init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);

void init_op_array(zend_op_array *op_array, zend_uchar type, int initial_ops_size)

{

      op_array->type = type;

      op_array->arg_flags[0] = 0;

      op_array->arg_flags[1] = 0;

      op_array->arg_flags[2] = 0;

      /**代码省略**/

}

CG(active_op_array) = op_array;

首先通过emalloc申请内存,大小为sizeof(zend_op_array)=208,然后初始化op_array的所有成员变量,把op_array赋值给CG(active_op_array)。

3.2 抽象语法树编译

抽象语法树的编译过程,是遍历抽象语法树,生成对应指令集的过程,编译是在 zend_compile_top_stmt() 中完成,这个函数是总入口,会被多次递归调用。其中传入的参数为CG(ast),这个AST是通过词法和语法分析得到的。下面我们看一下zend_compile_top_stmt的代码:

void zend_compile_top_stmt(zend_ast *ast) /* {{{ */

{

      if (!ast) {

               return;

      }

       //对于kind为ZEND_AST_STMT_LIST的节点,转换为zend_ast_list

      if (ast->kind == ZEND_AST_STMT_LIST) {

               zend_ast_list *list = zend_ast_get_list(ast);

               uint32_t i;

               //根据children的个数进行递归调用

               for (i = 0; i < list->children; ++i) {

                        zend_compile_top_stmt(list->child[i]);

               }

               return;

      }

       //其他kind的节点调用zend_compile_stmt

      zend_compile_stmt(ast);

 

      if (ast->kind != ZEND_AST_NAMESPACE && ast->kind != ZEND_AST_HALT_COMPILER) {

               zend_verify_namespace();

      }

      if (ast->kind == ZEND_AST_FUNC_DECL || ast->kind == ZEND_AST_CLASS) {

               CG(zend_lineno) = ((zend_ast_decl *) ast)->end_lineno;

               zend_do_early_binding();

      }

}

从代码中可以看到,对于zend_compile_top_stmt,会对AST节点的kind进行判断,然后走不同的逻辑,实际上是对AST的深度遍历,我们以下面的代码为例,看一下对AST的遍历过程。

<?php

$a = 1;

$b = $a + 2;

echo $b;

可以得到的AST如图11所示。

clipboard.png
图11 抽象语法树示意图

通过这课抽象语法树。可以很直观的看出,CG(ast)节点下面有三个子女:

  • 1)第一个子女,其kind是ZEND_AST_ASSIGN,有两个子女,分别是ZEND_AST_VAR和ZEND_AST_ZVAL,对应$a=1。
  • 2)第二个子女,其kind也是ZEND_AST_ASSIGN,有两个子女分别是ZEND_AST_VAR和ZEND_AST_BINARY_OP,其中ZEND_AST_BINARY_OP对应的是相加操作,对应的是$b=$a+2。
  • 3)第三个子女,其kind是ZEND_AST_STMT_LIST,有一个子女,为ZEND_AST_ECHO,对应的是echo $b。

下面我们看整棵树的遍历过程。

  • 1. Assign编译过程
  • 1)首先根节点kind为ZEND_AST_STMT,会调用函数zend_ast_get_list将其转换为zend_ast_list *,得到children的个数为2,接着递归调用zend_compile_top_stmt,这样就可以把抽象语法树根节点的最左子女遍历一遍,以便生成对应的指令;
  • 2)遍历第一个子女节点,对应的kind为ZEND_AST_ASSIGN,编译过程是调用函数zend_compile_stmt,继而调用zend_compile_expr函数,见代码:
void zend_compile_stmt(zend_ast *ast) /* {{{ */

{

      /*…代码省略…*/

      switch (ast->kind) {

               /*…代码省略…*/

                 default:

               {

                        znode result;

                        zend_compile_expr(&result, ast);

                        zend_do_free(&result);

               }

                  /*…代码省略…*/

}    

void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */

{

      /*…代码省略…*/

      switch (ast->kind) {

               /*…代码省略…*/

               case ZEND_AST_ASSIGN:

                        zend_compile_assign(result, ast);

                        return; 

         /*…代码省略…*/

}

最终调用的函数为zend_compile_assign,对ZEND_AST_ASSIGN节点进行编译:

void zend_compile_assign(znode *result, zend_ast *ast) /* {{{ */

{

      zend_ast *var_ast = ast->child[0];

      zend_ast *expr_ast = ast->child[1];

 

      znode var_node, expr_node;

      zend_op *opline;

      uint32_t offset;

       /*…代码省略…*/

      

      switch (var_ast->kind) {

               case ZEND_AST_VAR:

               case ZEND_AST_STATIC_PROP:

                        offset = zend_delayed_compile_begin();

                        zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W);

                        zend_compile_expr(&expr_node, expr_ast);

                        zend_delayed_compile_end(offset);

                        zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node);

                        return;

         /*…代码省略…*/

}

从代码中可以看出,kind为ZEND_AST_ASSIGN的抽象语法树有两个子女,左child为var_ast,右child为expr_ast,分别进行处理。

  • 3)调用zend_delayed_compile_begin:
static inline uint32_t zend_delayed_compile_begin(void) /* {{{ */

{

      return zend_stack_count (&CG(delayed_oplines_stack));

}

该函数会获取CG的delayed_oplines_stack栈顶的位置,其中delayed_oplines_stack是对于依赖后续编译动作存储信息的栈。等expr_ast编译后使用,调用zend_delayed_compile_end(offset)来获取栈里的信息。

  • 4)对于左子女var_ast调用zend_delayed_compile_var:
void zend_delayed_compile_var(znode *result, zend_ast *ast, uint32_t type) /* {{{ */

{

      zend_op *opline;

      switch (ast->kind) {

               case ZEND_AST_VAR:

                        zend_compile_simple_var(result, ast, type, 1);

   /**代码省略**/

}

其中kind为ZEND_AST_VAR,继而调用zend_compile_simple_var函数:

static void zend_compile_simple_var(znode *result, zend_ast *ast, uint32_t type, int delayed) /* {{{ */

{

      zend_op *opline;

 

      /*…代码省略…*/

      } else if (zend_try_compile_cv(result, ast) == FAILURE) {

               /*…代码省略…*/

      }

}

继而调用zend_try_compile_cv函数:

static int zend_try_compile_cv(znode *result, zend_ast *ast) /* {{{ */

{

      zend_ast *name_ast = ast->child[0];

      if (name_ast->kind == ZEND_AST_ZVAL) {

               /*…代码省略…*/

 

               result->op_type = IS_CV;

               result->u.op.var = lookup_cv(CG(active_op_array), name);

 

      /*…代码省略…*/

}

核心函数是lookup_cv,在这里面组装了操作数,见代码:

static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{

      int i = 0;

      zend_ulong hash_value = zend_string_hash_val(name);

       //判断变量是否在vars中存在,若存在直接返回对应的位置

      while (i < op_array->last_var) {

               if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) ||

                   (ZSTR_H(op_array->vars[i]) == hash_value &&

                    ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) &&

                    memcmp(ZSTR_VAL(op_array->vars[i]), ZSTR_VAL(name), ZSTR_LEN(name)) == 0)) {

                        zend_string_release(name);

                        return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

               }

               i++;

      }

       //若不存在,则写入vars中,返回新插入的位置

      i = op_array->last_var;

      op_array->last_var++;

      /*…代码省略…*/

 

      op_array->vars[i] = zend_new_interned_string(name);

 

      return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

}

从代码中可以看出,变量是存放到op_array->vars中的,而返回的是一个int型的地址,这个是什么呢?我们看一下宏ZEND_CALL_VAR_NUM的定义:

#define ZEND_CALL_VAR_NUM(call, n) \

      (((zval*)(call)) + (ZEND_CALL_FRAME_SLOT + ((int)(n))))

#define ZEND_CALL_FRAME_SLOT \

      ((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval))))

可以看出,这个值都是sizeof(zval)的整数倍,在笔者的机器上,zval的大小为16,而zend_execute_data大小为80,所以返回的是每个变量的偏移值,即80+16*i,如图12所示。

clipboard.png
图12 左子女var_ast编译示意图

此时,就将赋值语句$a=1中,左侧表达式$a编译完成,赋值给了znode* result,下面继续对右子女常量1的编译。

  • 5)对于右子女,调用函数zend_compile_expr进行编译,见代码:
void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */

{

    /* CG(zend_lineno) = ast->lineno; */

    CG(zend_lineno) = zend_ast_get_lineno(ast);

 

     switch (ast->kind) {

         case ZEND_AST_ZVAL:

            ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast));

            result->op_type = IS_CONST;

            return;

从代码中可以看出,对于常量1,通过ZVAL_COPY,将值复制到result->u.constan中,同时将result->op_type赋值为IS_CONST。这样,对于assign操作的两个操作数都编译完成了,下面我们看一下对应指令opline的生成过程。

  • 6)opline生成调用函数zend_emit_op,见代码:
static zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2) /* {{{ */

{

       //分配和获取opline,并设置其opcode

      zend_op *opline = get_next_op(CG(active_op_array));

      opline->opcode = opcode;

        //设置操作数1

      if (op1 == NULL) {

               SET_UNUSED(opline->op1);

      } else {

               SET_NODE(opline->op1, op1);

      }

        //设置操作数2

      if (op2 == NULL) {

               SET_UNUSED(opline->op2);

      } else {

               SET_NODE(opline->op2, op2);

      }

 

      zend_check_live_ranges(opline);

 

      if (result) {

                  //设置返回值

               zend_make_var_result(result, opline);

      }

      return opline;

}

其中对操作数得到设置,对应的是宏SET_NODE,见代码:

  #define SET_NODE(target, src) do { \

               target ## _type = (src)->op_type; \

               if ((src)->op_type == IS_CONST) { \

                        target.constant = zend_add_literal(CG(active_op_array), &(src)->u.constant); \

               } else { \

                        target = (src)->u.op; \

               } \

      } while (0)

 

int zend_add_literal(zend_op_array *op_array, zval *zv) /* {{{ */

{

      int i = op_array->last_literal;

      op_array->last_literal++;

      if (i >= CG(context).literals_size) {

               while (i >= CG(context).literals_size) {

                        CG(context).literals_size += 16; /* FIXME */

               }

               op_array->literals = (zval*)erealloc(op_array->literals, CG(context).literals_size * sizeof(zval));

      }

      zend_insert_literal(op_array, zv, i);

      return i;

}

从代码中可以看出,对于操作数1,会将编译过程中临时的结构znode传递给zend_op中,对于操作数2,因为是常量(IS_CONST),会调用zend_add_literal将其插入到op_array->literals中。

从返回值的设置,调用的是zend_make_var_result,其代码如下:

  static inline void zend_make_var_result(znode *result, zend_op *opline) /* {{{ */

{

        //返回值的类型设置为IS_VAR

      opline->result_type = IS_VAR;

        //这个是返回值的编号,对应T位置

      opline->result.var = get_temporary_variable(CG(active_op_array));

      GET_NODE(result, opline->result);

}

static uint32_t get_temporary_variable(zend_op_array *op_array) /* {{{ */

{

      return (uint32_t)op_array->T++;

}

返回值的类型为IS_VAR,result.var为T的值,下面我们给出Assign操作对应的指令图,如图13所示。

clipboard.png
图13 Assign指令示意图

从图13中可以看出,生成的opline中opcode等于38;op1的类型为IS_CV,op1.var对应的是vm_stack上的偏移量;op2的类型为IS_CONST,op2.constant对应的是op_array中literals数组的下标;result的类型为IS_VAR,result.var对应的是T的值;此时handler的值为空。

  • 2. Add编译过程

对于“$b =$a+2;”语句,首先是add语句,也就是$a+1,跟assign语句类型类似,不同是调用了函数zend_compile_binary_op,见代码:

void zend_compile_binary_op(znode *result, zend_ast *ast) /* {{{ */

{

      zend_ast *left_ast = ast->child[0];

      zend_ast *right_ast = ast->child[1];

      uint32_t opcode = ast->attr;//通过attr区分加减乘除等等操作

 

      znode left_node, right_node;

      zend_compile_expr(&left_node, left_ast);

      zend_compile_expr(&right_node, right_ast);

      /*…代码省略…*/

       zend_emit_op_tmp(result, opcode, &left_node, &right_node);

      /*…代码省略…*/

}

对于加减乘除等操作,kind都是ZEND_AST_BINARY_OP,具体操作通过AST中的attr区分的,因为$a+1会生成临时变量,因此与Assign操作不同,调用的函数是zend_emit_op_tmp:

 static zend_op *zend_emit_op_tmp(znode *result, zend_uchar opcode, znode *op1, znode *op2) /* {{{ */

{

      /*…代码与zend_emit_op一样…*/

      if (result) {

               zend_make_tmp_result(result, opline);

      }

 

      return opline;

}

zend_emit_op_tmp函数与zend_emit_op类似,opline中的操作数1和操作数2做了同样的操作,而result不同之处在于,其类型是IS_TMP_VAR,因此opline如图14所示。

clipboard.png
图14 Add指令示意图

对于“$b=$a+2;”相当于把临时变量赋值给$b,与Assign编译过程一致,生成opline如图15所示。

clipboard.png
图15 第2条Assign指令示意图

  • 3. Echo编译过程
    对于“echo $b;”,编译过程类似于Assign和Add的编译,不同处是调用函数zend_compile_echo。
void zend_compile_echo(zend_ast *ast) /* {{{ */

{

      zend_op *opline;

      zend_ast *expr_ast = ast->child[0];

 

      znode expr_node;

      zend_compile_expr(&expr_node, expr_ast);

 

      opline = zend_emit_op(NULL, ZEND_ECHO, &expr_node, NULL);

      opline->extended_value = 0;

}

Echo对应的指令只有一个操作数,对于操作数2,SET_UNUSED宏设置为IS_UNUSED。

#define SET_UNUSED(op)  op ## _type = IS_UNUSED

生成的opline如图16所示。

clipboard.png
图16 Echo指令示意图

  • 4. Return编译过程

上面对于AST编译并没有结束,PHP代码中虽然没有return操作,但是默认会生成一条ZEND_RETURN指令,通过zend_emit_final_return含设置,代码如下:

void zend_emit_final_return(int return_one) /* {{{ */

{

      znode zn;

      zend_op *ret;

      /**代码省略**/

 

      zn.op_type = IS_CONST;

      if (return_one) {

               ZVAL_LONG(&zn.u.constant, 1);

      } else {

               ZVAL_NULL(&zn.u.constant);

      }

 

      ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL);

      ret->extended_value = -1;

}

同样通过zend_emit_op设置opline,设置以后的opline如图17所示。

clipboard.png
图17 Return指令示意图

经过对Assign、Add和Echo的编译后,生成的全部oplines如图18所示。

clipboard.png
图18 所有指令集示意图

到这里,我们了解了AST编译生成opline指令集的过程,包括op1、op2和result的生成过程,但是此时opline中的handler还都是空指针,接下来我们看一下handler设置的过程。

3.3 设置指令handler

抽象语法树编译后还有一个重要操作,函数叫pass_two,这个函数中,对opline指令集做了进一步的加工,最主要的工作是设置指令的handler,代码如下:

ZEND_API int pass_two(zend_op_array *op_array)

{

     /**代码省略**/

   while (opline < end) {//遍历opline数组

      if (opline->op1_type == IS_CONST) {

               ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op1);

      } else if (opline->op1_type & (IS_VAR|IS_TMP_VAR)) {

       opline->op1.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op1.var);

          }

     

      if (opline->op2_type == IS_CONST) {

          ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op2);

      } else if (opline->op2_type & (IS_VAR|IS_TMP_VAR)) {

          opline->op2.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op2.var);

               }

    if (opline->result_type & (IS_VAR|IS_TMP_VAR)) {

       opline->result.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->result.var);

     }

     ZEND_VM_SET_OPCODE_HANDLER(opline);

     /**代码省略**/

}

从代码中可以看出,该函数会对opline指令数组进行遍历,对每一条opline指令进行操作,对于op1和op2如果是IS_CONST类型,调用ZEND_PASS_TWO_UPDATE_CONSTANT,见代码:

 /* convert constant from compile-time to run-time */

# define ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, node) do { \

               (node).constant *= sizeof(zval); \

      } while (0)

根据上一节的知识我们知道,对于IS_CONST类型的变量,其值是存在op_array->literals数组中,因此,可以直接使用数组下标乘以sizeof(zval)转换为偏移量。

对于op1和op2如果是IS_VAR或者IS_TMP_VAR类型的变量,跟上一节一样,通过ZEND_CALL_VAR_NUM计算偏移量。

另外一个非常重要的工作是通过ZEND_VM_SET_OPCODE_HANDLER(opline),设置opline对应的hanlder,代码如下:

ZEND_API void zend_vm_set_opcode_handler(zend_op* op)

{

      op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);

}

其中opcode和handler之前的对应关系在Zend/zend_vm_execute.h中定义的。opline数组经过一次遍历后,handler也就设置完毕,设置后的opline数组如图19所示。

clipboard.png
图19 设置handler后的指令集

到此,整个抽象语法树就编译完成了,最终的结果为opline指令集,接下来就是在Zend虚拟机上执行这些指令。

4执行过程

执行的入口函数为zend_execute,在该函数中会针对上一节生成的opline指令集进行调度执行。首先会在EG(vm_stack)上分配空间,然后每一条指令依次压栈并调用对应的handler。代码如下:

ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)

{

      zend_execute_data *execute_data;

      /**代码省略**/

      //压栈生成execute_data

      execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE,

               (zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));

       //设置symbol_table

      if (EG(current_execute_data)) {

               execute_data->symbol_table = zend_rebuild_symbol_table();

      } else {

               execute_data->symbol_table = &EG(symbol_table);

      }

      EX(prev_execute_data) = EG(current_execute_data);

       //初始化execute_data

      i_init_execute_data(execute_data, op_array, return_value);

       //执行

      zend_execute_ex(execute_data);

       //释放execute_data

      zend_vm_stack_free_call_frame(execute_data);

}

这个代码中首先根据op_array中的指令生成对应的execute_data,然后初始化后调用handler执行。下面我们具体分析一下执行的过程。

4.1 执行栈分配

执行栈是通过2.6节介绍的zend_vm_stack_push_call_frame完成的,会在EG(vm_stack)上分配一块内存区域,80个字节用来存放execute_data,紧接着下面是根据last_var和T的数量分配zval大小的空间,以3节编译生成的指令集为例,分配的栈如图20所示。

clipboard.png
图20 执行栈分配示意图

从图20中看出,在EG(vm_stack)上分配空间,空间的大小跟op_array中last_var和T的值相关。

4.2 初始化execute_data

在执行栈上分配空间后,会调用函数i_init_execute_data对执行数据进行初始化,见代码:

    static zend_always_inline void i_init_execute_data(zend_execute_data *execute_data, zend_op_array *op_array, zval *return_value) /* {{{ */

{

         ZEND_ASSERT(EX(func) == (zend_function*)op_array);

 

         EX(opline) = op_array->opcodes;//读取第一条指令

         EX(call) = NULL;

         EX(return_value) = return_value;//设置返回值

 

         if (EX_CALL_INFO() & ZEND_CALL_HAS_SYMBOL_TABLE) {

                    //赋值符号表

                  zend_attach_symbol_table(execute_data);

          /**代码省略**/

 

          //运行时缓存

          if (!op_array->run_time_cache) {

                  if (op_array->function_name) {

                           op_array->run_time_cache = zend_arena_alloc(&CG(arena), op_array->cache_size);

                  } else {

                           op_array->run_time_cache = emalloc(op_array->cache_size);

                  }

                  memset(op_array->run_time_cache, 0, op_array->cache_size);

         }

         EX_LOAD_RUN_TIME_CACHE(op_array);

         EX_LOAD_LITERALS(op_array);//设置常量数组

 

         EG(current_execute_data) = execute_data;

}

从代码中可以看出,初始化工作主要做了几件事:

  • 1)读取op_array中的第一条指令,赋值给EX(opline),其中EX宏是对execute_data的取值宏;
  • 2)设置EX的返回值;
  • 3)赋值符号表;
  • 4)设置运行时缓存;
  • 5)设置常量数组。

做完这些工作后,执行栈中数据的结果如图21所示。

clipboard.png
图21 初始化execute_data示意图

4.3 调用hanlder函数执行

接下来是调用execute_ex进行指令的执行,见代码:

ZEND_API void execute_ex(zend_execute_data *ex)

{

         ZEND_VM_LOOP_INTERRUPT_CHECK();

 

         while (1) { //循环

                  int ret;

                  if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0))

       {

                  if (EXPECTED(ret > 0)) {

                           execute_data = EG(current_execute_data);

                           ZEND_VM_LOOP_INTERRUPT_CHECK();

                  } else {

                           return;

                  }

}

}

从代码中可以看出,整个执行最外层是while循环,直到结束才退出。调用的是opline中对应的handler,下面以3节中生成的指令集进行详细的阐述。

  • 1)对于第一个指令,对应的handler为:
//ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER

//通过op2获取到常量数组里面的值

value = EX_CONSTANT(opline->op2);

//获取到op1对应的位置

variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);

//将常量赋值给对应位置的指针

value = zend_assign_to_variable(variable_ptr, value, IS_CONST);

//将结果复制到result

ZVAL_COPY(EX_VAR(opline->result.var), value);

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

首先通过op2.constant值获取到常量表中的zval值,通过op1.var获取到栈中对应的位置,然后将常量值赋值到对应的位置,同时将其copy到result对应的位置,如图22所示。

clipboard.png
图22 Assign指令执行示意图

完成assign操作后,会调用ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION宏进行下一条指令的执行,也就是opline+1。

  • 2)第二条指令对应的是相加操作,其handler为:
//ZEND_ADD_SPEC_CV_CONST_HANDLER

zval *op1, *op2, *result;

//获取op1对应的位置

op1 = _get_zval_ptr_cv_undef(execute_data, opline->op1.var);

//获取op2对应的值

op2 = EX_CONSTANT(opline->op2);

/**代码省略**/

//执行相加操作,赋值给result

add_function(EX_VAR(opline->result.var), op1, op2);

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

首先根据op1.var获取对应的位置,然后根据op2.constant值获取到常量表中的zval值,最后进行相加操作,赋值给result对应的位置,如图23所示。

clipboard.png
图23 Add指令执行示意图

  • 3)第三条指令依然是assign,但是因为类型与第一条指令不同,因此对应的handler也不同:
//ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_UNUSED_HANDLER

zval *value;

zval *variable_ptr;

//根据op2.var获取临时变量的位置

value = _get_zval_ptr_tmp(opline->op2.var, execute_data, &free_op2);

//根据op1.var获取操作数1 的位置

variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);

//将临时变量赋值给操作数1对应的位置

value = zend_assign_to_variable(variable_ptr, value, IS_TMP_VAR);

//同时拷贝到result对应的位置

ZVAL_COPY(EX_VAR(opline->result.var), value);

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

与第一条指令类似,执行过程如图24所示。

clipboard.png
图24 第2条Assign指令示意图

  • 4)第四条指令是echo操作,对应的handler为:
// ZEND_ECHO_SPEC_CV_HANDLER

zval *z;

//根据op1.var获取对应位置的值

z = _get_zval_ptr_cv_undef(execute_data, opline->op1.var);

//调用zend_write输出

zend_write(ZSTR_VAL(str), ZSTR_LEN(str));

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

这条指令会根据op1.var获取到对应的位置,取出zval值输出,如图25所示。

clipboard.png
图25 Echo指令执行示意图

  • 5)第五条指令为return,对应的handler为:
//ZEND_RETURN_SPEC_CONST_HANDLER

zval *retval_ptr;

zval *return_value;

retval_ptr = EX_CONSTANT(opline->op1);

return_value = EX(return_value);

//调用zend_leave_helper_SPEC函数,返回

ZEND_VM_TAIL_CALL(zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));

这条指令没有做实质性的操作,核心是返回-1,让while循环退出,指令执行结束。

到此,整个的执行过程就阐述完成了,相信读者通过这五条指令的执行,初步理解了Zend虚拟机的执行过程。

4.4 释放execute_data

指令执行完毕后,调用zend_vm_stack_free_call_frame释放execute_data,并回收EG(vm_stack)上使用的空间,这部分比较简单。

5小结

本文主要介绍了Zend虚拟机的实现原理,包括抽象语法树编译生成指令集的过程,以及指令集执行的过程。同时介绍了Zend虚拟机运行中用到的数据结构。希望读者读完本文,能够对Zend虚拟机有一定的认识。

查看原文

hi 赞了文章 · 2018-07-24

【PHP7源码分析】PHP7源码研究之浅谈Zend虚拟机

作者 陈雷
编程语言的虚拟机是一种可以运行中间语言的程序。中间语言是抽象出的指令集,由原生语言编译而成,作为虚拟机执行阶段的输入。很多语言都实现了自己的虚拟机,比如Java、C#和Lua。PHP语言也有自己的虚拟机,称为Zend虚拟机

PHP7完成基本的准备工作后,会启动Zend引擎,加载注册的扩展模块,然后读取对应的脚本文件,Zend引擎会对文件进行词法和语法分析,生成抽象语法树,接着抽象语法树被编译成Opcodes,如果开启了Opcache,编译的环节会被跳过从Opcache中直接读取Opcodes进行执行。

PHP7中词法语法分析,生成抽象语法树,然后编译成Opcodes及被执行均由Zend虚拟机完成。这里将详细阐述抽象语法树编译成Opcodes的过程,以及Opcodes被执行的过程,来阐述Zend虚拟机的实现原理及关键的数据结构。

1 基础知识

Zend虚拟机(称为Zend VM)是PHP语言的核心,承担了语法词法解析、抽象语法树编译以及指令的执行工作,下面我们讨论一下Zend虚拟机的基础架构以及相关的基础知识。

1.1 Zend虚拟机架构

Zend虚拟机主要分为解释层、中间数据层和执行层,下面给出各层包含的内容,如图1所示。

clipboard.png
图1 Zend虚拟机架构图

下面解释下各层的作用。

(1)解释层

这一层主要负责把PHP代码进行词法和语法分析,生成对应的抽象语法树;另一个工作就是把抽象语法树进行编译,生成符号表和指令集;

(2)中间数据层

这一层主要包含了虚拟机的核心部分,执行栈的维护,指令集和符号表的存储,而这三个是执行引擎调度执行的基础;

(3)执行层

这一层是执行指令集的引擎,这一层是最终的执行并生成结果,这一层里面实现了大量的底层函数。

为了更好地理解Zend虚拟机各层的工作,我们先了解一下物理机的一些基础知识,读者可以对照理解虚拟机的原理。

1.2 符号表

符号表是在编译过程中,编译程序用来记录源程序中各种名字的特性信息,所以也称为名字特性表。名字一般包含程序名、过程名、函数名、用户定义类型名、变量名、常量名、枚举值名、标号名等。特性信息指的是名字的种类、类型、维数、参数个数、数值及目标地址(存储单元地址)等。

符号表有什么作用呢?一是协助进行语义检查,比如检查一个名字的引用和之前的声明是否相符,二是协助中间代码生成,最重要的是在目标代码生成阶段,当需要为名字分配地址时,符号表中的信息将是地址分配的主要依据。

clipboard.png
图2 符号表创建示例

符号表一般有三种构造和处理方法,分别是线性查找,二叉树和Hash技术,其中线性查找法是最简单的,按照符号出现的顺序填表,每次查找从第一个开始顺序查找,效率比较低;二叉树实现了对折查找,在一定程度上提高了效率;效率最高的是通过Hash技术实现符号表,相信大家对Hash技术有一定的了解,而PHP7中符号表就是使用的HashTable实现的。

1.3 函数调用栈

为了更清晰地了解虚拟机中函数调用的过程,我们先了解一下物理机的简单原理,主要涉及函数调用栈的概念,而Zend虚拟机参照了物理机的基本原理,做了类似的设计。

下面以一段C代码描述一下系统栈和函数过程调用,代码如下:

  int funcB(int argB1, int argB2)

  {

         int varB1, varB2;

         return argB1+argB2;

  }

  int funcA(int argA1, int argA2)

  {

       int varA1, varA2;

        return argA1+argA2+funcB( 3, 4);

  }    

  int main()

  {

      int varMain;

      return funcA(1, 2);

     }

这段代码运行时,首先main函数会压栈, 首先将局部变量varMain入栈,main函数调用了funcA函数,C语言会从后往前,将函数参数压栈,先压第二个参数argA2=2,再压第一个参数argA1=1,同时对于funcA的返回会产生一个临时变量等待赋值,也会被压栈,这些称为main函数的栈帧;接着将funcA压栈,同样的先将局部变量varA1和varA2压入栈中,因为调用了函数funcB,会将参数argB2=4和argB1=3压入栈中,同时把funcB的返回产生的临时变量压入栈中,这部分称为funcA的栈帧;同样,funcB被压入栈中,如图3所示。

clipboard.png
图3 函数调用压栈过程示意图

funcB函数执行,对argB1和argB2进行相加操作,执行后得到返回值为7,然后funcB的栈帧出栈,funcA中临时变量TempB被赋值为7,继而进行相加操作,得到结果为10,然后funcA出栈,main函数中临时变量TempA被赋值为10,最终main函数返回并出栈,整个函数调用结束。如图4所示。

clipboard.png
图4 函数调用出栈过程示意图

1.4 指令

汇编语句中的指令语句一般格式为:

     [标号:]     [前缀]  指令助记符    [操作数]     [;注释]

其中:

  • 1)标识符字段由各种有效字符组成,一般表示符号地址,具有段基址、偏移量、类型三种属性。通常情况下这部分是可选部分,主要为便于程序的读写方便而使用。
  • 2)助记符,规定指令或伪指令的操作功能,是语句中唯一不可缺少的部分。对于指令,汇编程序会将其翻译成机器语言指令:
MOV   AX, 100  →   B8 00 01
  • 3)操作数,指令语句中提供给指令的操作对象、存放位置。操作数可以是1个、2个或0个,2个时用逗号‘,’分开。比如“RET;”对应的操作数个数是0个,“INC BX;”对应的操作数个数是1,“MOV AX,DATA;”对应的操作数个数是2个。
  • 4)注释,以“ ;”开始,给以编程说明。

    符号表、函数调用栈以及指令基本构成了物理机执行的基本元素,Zend虚拟机也同样实现了符号表,函数调用栈及指令,来运行PHP代码,下面我先讨论一下Zend虚拟机相关的数据结构。

2相关数据结构

Zend虚拟机包含了词法语法分析,抽象语法树的编译,以及Opcodes的执行,本文主要详细介绍抽象语法树和Opcodes的执行过程,在展开介绍之前,先阐述一下用到的基本的数据结构,为后面原理性的介绍奠定基础。

2.1 EG(v)

首先介绍的是全局变量executor_globals,EG(v)是对应的取值宏,executor_globals对应的是结构体_zend_executor_globals,是PHP生命周期中非常核心的数据结构。这个结构体中维护了符号表(symbol_table, function_table,class_table等),执行栈(zend_vm_stack)以及包含执行指令的zend_execute_data,另外还包含了include的文件列表,autoload函数,异常处理handler等重要信息,下面给出_zend_executor_globals的结构图,然后分别阐述其含义,如图5所示。

clipboard.png
图5 EG(v)结构图

这个结构体比较复杂,下面我们介绍几个核心的成员。

  • 1)symbol_table:符号表,这里面主要是存的全局变量,以及一些魔术变量,比如$_GET、$_POST等;
  • 2)function_table:函数表,主要存放函数,包括大量的内部函数,以及用户自定义的函数,比如zend_version,func_num_args,str系列函数,等等;
  • 3)class_table:类表,主要存放内置的类以及用户自定义的类,比如stdclass、throwable、exception等类;
  • 4)zend_constants:常量表,存放PHP中的常量,比如E_ERROR、E_WARNING等;
  • 5)vm_stack:虚拟机的栈,执行时压栈出栈都在这上面操作;
  • 6)current_execute_data:对应_zend_execute_data结构体,存放执行时的数据。

下面针对于符号表、指令集、执行数据和执行栈进行详细介绍。

2.2 符号表

PHP7中符号表分为了symbol_table、function_table和class_table等。

  • (1)symbol_table

symbol_table里面存放了变量信息,其类型是HashTable,下面我们看一下具体的定义:

       //符号表缓存

       zend_array *symtable_cache[SYMTABLE_CACHE_SIZE];

      zend_array **symtable_cache_limit;

      zend_array **symtable_cache_ptr;

       //符号表

       zend_array symbol_table;

symbol_table里面有什么呢,代码”$a=1;”对应的symnol_table,如图6所示。

clipboard.png
图6 symbol_table示意图

从图6中可以看出,符号表中有我们常见的超全局变量$_GET、$_POST等,还有全局变量$a。在编译过程中会调用zend_attach_symbol_table函数将变量加入symbol_table中。

  • (2)function_table

function_table对应的是函数表,其类型也是HashTable,见代码:

       HashTable *function_table;  /* function symbol table */

函数表中存储哪些函数呢?同样以上述代码为例,我们利用GDB印一下function_table的内容:

(gdb) p *executor_globals.function_table

$1 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 0}, type_info = 7}}, u = {v = {

      flags = 25 '\031', nApplyCount = 0 '\000', nIteratorsCount = 0 '\000', consistency = 0 '\000'},

    flags = 25}, nTableMask = 4294966272, arData = 0x12102b0, nNumUsed = 848, nNumOfElements = 848,

  nTableSize = 1024, nInternalPointer = 0, nNextFreeElement = 0, pDestructor = 0x8d0dc3 <zend_function_dtor>}

可以看出,函数表中有大量的函数,上面打印显示有848个之多,这里面主要是内部函数,比如zend_version、func_num_args、cli_get_process_title,等等。

  • (3)class_table

class_table对应的是类表,其也是HashTable:

 HashTable *class_table; /* class table */

类表里面也有大量的内置类,比如stdclass、traversable、xmlreader等。

符号表里面存放了执行时需要的数据,比如在symbol_table中,key为_GET的Bucket对应的又是个HashTable,里面存放的是$_GET[xxx],执行时会从中取对应的值。

2.3 指令

Zend虚拟机的指令称为opline,每条指令对应一个opcode。PHP代码在编译后生成opline,Zend虚拟机根据不同的opline完成PHP代码的执行,opline由操作指令、操作数和返回值组成,与机器指令非常类似,opline对应的结构体为zend_op,见代码:

  struct _zend_op {

      const void *handler; //操作执行的函数

      znode_op op1; //操作数1

      znode_op op2; //操作数2

      znode_op result; //返回值

      uint32_t extended_value; //扩展值

      uint32_t lineno; //行号

      zend_uchar opcode; //opcode值

      zend_uchar op1_type; //操作数1的类型

      zend_uchar op2_type; //操作数2的类型

      zend_uchar result_type; //返回值的类型

};

对应的内存占用图如图7所示。

clipboard.png
图7 zend_op结构图

PHP代码会被编译成一条一条的opline,分解为最基本的操作,举个例子,如果把opcode当成一个计算器,只接受两个操作数op1和 op2,执行一个操作handler,比如加减乘除,然后它返回一个结果result,再稍加处理算术溢出的情况存在extended_value中。下面详细介绍下各个字段。

  • (1) Opcode

Opcode有时候被称为所谓的字节码(Bytecode),是被软件解释器解释执行的指令集。这些软件指令集通常会提供一些比对应的硬件指令集更高级的数据类型和操作。

注意:Opcode和Bytecode其实是两个含义不同的词,但经常会把它们当作同一个意思来交互使用。

Zend虚拟机有非常多Opcode,对应可以做非常多事情,并且随着PHP的发展, Opcode也越来越多,意味着PHP可以做越来越多的事情。所有的Opcode都在PHP的源代码文件Zend/zend_vm_opcodes.h中定义。Opcode的名称是自描述的,比如:

  • ZEND_ASSGIN:赋值操作;
  • ZEND_ADD:两个数相加操作;
  • ZEND_JMP:跳转操作。

PHP 7.1.0中有186个Opcode:

#define ZEND_NOP                               0

#define ZEND_ADD                               1

#define ZEND_SUB                               2

#define ZEND_MUL                               3

#define ZEND_DIV                               4

#define ZEND_MOD                               5

#define ZEND_SL                                6

…

#define ZEND_FETCH_THIS                      184

#define ZEND_ISSET_ISEMPTY_THIS              186

#define ZEND_VM_LAST_OPCODE                  186
  • (2)操作数

op1和op2都是操作数,但不一定全部使用,也就是说每个Opcode对应的hanlder最多可以使用两个操作数(也可以只使用其中一个,或者都不使用)。每个操作数都可以理解为函数的参数,返回值result是hanlder函数对操作数op1和op2计算后的结果。op1、op2和result对应的类型都是znode_op,其定义为一个联合体:

typedef union _znode_op {

      uint32_t      constant;

      uint32_t      var;

      uint32_t      num;

      uint32_t      opline_num; /*  Needs to be signed */

#if ZEND_USE_ABS_JMP_ADDR

      zend_op       *jmp_addr;

#else

      uint32_t      jmp_offset;

#endif

#if ZEND_USE_ABS_CONST_ADDR

      zval          *zv;

#endif

} znode_op;

这样其实每个操作数都是uint32类型的数字,一般表示的是变量的位置。操作数有5种不同的类型,具体在Zend/zend_compile.h中定义:

#define IS_CONST        (1<<0)

#define IS_TMP_VAR      (1<<1)

#define IS_VAR          (1<<2)

#define IS_UNUSED       (1<<3)   /* Unused variable */

#define IS_CV           (1<<4)   /* Compiled variable */

这些类型是按位表示的,具体含义如下。

  • 1)IS_CONST:值为1,表示一个常量,都是只读的,值不可改变,比如$a=”hello world” 中的hello world。
  • 2)IS_VAR:值为4,是PHP变量,这个变量并不是PHP代码中声明的变量,常见的是返回的临时变量,比如$a=time(), 函数time返回值的类型就是IS_VAR,这种类型的变量是可以被其他Opcode对应的handler重复使用的。
  • 3)IS_TMP_VAR :值为2,也是PHP变量,跟IS_VAR不同之处是,不能与其他Opcode重复使用,举个例子,$a=”123”.time(); 这里拼接的临时变量”123”.time()的类型就是IS_TMP_VAR,一般用于操作的中间结果。
  • 4)IS_UNUSED :值为8,表示这个操作数没有包含任何有意义的东西。
  • 5)IS_CV :值为16,编译变量(Compiled Variable):这个操作数类型表示一个PHP变量:以$something形式在PHP脚本中出现的变量。
  • (3)handler

handler为Opcode对应的是实际的处理函数,Zend虚拟机对每个Opcode的工作方式是完全相同的,都有一个handler的函数指针,指向处理函数的地址,这是一个C函数,包含了执行Opcode对应的代码,使用op1,op2做为参数,执行完成后,会返回一个结果result,有时也会附加一段信息extended_value。文件Zend/zend_vm_execute.h包含了所有的handler对应的函数,php-7.1.0中这个文件有62000+行。

注意:Zend/zend_vm_execute.h并非手动编写的,而是由zend_vm_gen.php这个PHP脚本解析zend_vm_def.h和zend_vm_execute.skl后生成,这个很有意思,先有鸡还是先有蛋?没有PHP 哪来的这个php脚本呢?这个是后期产物,早期php版本不用这个。这个类似于GO语言的自举,自己编译自己。

同一个Opcode对应的handler函数会根据操作数的类型而不同,比如ZEND_ASSIGN对应的handler就有多个:

ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_USED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CV_RETVAL_UNUSED_HANDLER,

ZEND_ASSIGN_SPEC_CV_CV_RETVAL_USED_HANDLER,

其函数命名是有如下规则的:

ZEND_[opcode]_SPEC_(操作数1类型)_(操作数2类型)_(返回值类型)_HANDLER

举个例子,对于PHP代码:

$a = 1;

对应的handler为ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,其定义为:

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

{

      USE_OPLINE

 

      zval *value;

      zval *variable_ptr;

 

      SAVE_OPLINE();

        //获取op2对应的值,也就是1

      value = EX_CONSTANT(opline->op2);

        //在execute_data中获取op1的位置,也就是$a

      variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);

        /*代码省略*/

       //将1赋值给$a

      value = zend_assign_to_variable(variable_ptr, value, IS_CONST);

              

      }

         /*代码省略*/

      ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

}

从代码中可以非常直观的看出,常量1是如何赋值给CV类型的$a的。

  • (4)extended_value

extended_value是存的扩展信息,Opcodes和CPU的指令类似,有一个标示指令字段handler,以及这个Opcode所操作的操作数op1和op2,但PHP不像汇编那么底层, 在脚本实际执行的时候可能还需要其他更多的信息,extended_value字段就保存了这类信息;

  • (5)lineno

lineno对应源代码文件中的行号。

到这里,相信读者对指令opline有了比较深刻的认识,在Zend虚拟机执行时,这些指令被组装在一起,成为指令集,下面我们介绍一下指令集。

2.4 指令集

在介绍指令集前,需要先介绍一个编译过程用到的一个基础的结构体znode,其结构如下:

  typedef struct _znode { /* used only during compilation */

      zend_uchar op_type;//变量类型

      zend_uchar flag;

      union {

               //表示变量的位置

               znode_op op;

                 //常量

               zval constant; /* replaced by literal/zv */

      } u;

} znode

znode只会在编译过程中使用,其中op_type对应的是变量的类型,u是联合体,u.op是操作数的类型,zval constant用来存常量。znode在后续生成指令集时会使用到。

Zend虚拟机中的指令集对应的结构为zend_op_array,其结构如下:

  struct _zend_op_array {

      /* Common elements */

      /*代码省略common是为了函数能够快速访问Opcodes而设定的*/

      /* END of common elements */

      //这部分是存放opline的数组,last为总个数

      uint32_t last;

      zend_op *opcodes;

     

      int last_var;//变量类型为IS_CV的个数

      uint32_t T;//变量类型为IS_VAR和IS_TMP_VAR的个数

      zend_string **vars;//存放IS_CV类型变量的数组

       /*代码省略*/

      /* static variables support */

      HashTable *static_variables; //静态变量

        /*代码省略*/

      int last_literal;//常量的个数

      zval *literals;//常量数组

 

      int  cache_size;//运行时缓存数组大小

      void **run_time_cache;//运行时缓存

 

      void *reserved[ZEND_MAX_RESERVED_RESOURCES];

};

其结构图如图8所示。

clipboard.png
图8 zend_op_array结构图

这个结构体中有几个关键变量。

  • 1)last和opcodes,这部分是存放Opline的数组,也就是指令集存放的位置,其中last为数组中Opline的个数。
  • 2)last_var代表IS_CV类型变量的个数,这种类型变量存放在vars数组中;在整个编译过程中,每次遇到一个IS_CV类型的变量(类似于$something),就会去遍历vars数组,检查是否已经存在,如果不存在,则插入到vars中,并将last_var的值设置为该变量的操作数;如果存在,则使用之前分配的操作数,见代码:
 result->op_type = IS_CV;

 result->u.op.var = lookup_cv(CG(active_op_array), name);

 

  //lookup_cv:

  static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{

      int i = 0;

      zend_ulong hash_value = zend_string_hash_val(name);

 

       //遍历vars

      while (i < op_array->last_var) {

                  //如果存在直接返回

               if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) ||

                   (ZSTR_H(op_array->vars[i]) == hash_value &&

                    ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) &&

                    memcmp(ZSTR_VAL(op_array->vars[i]), ZSTR_VAL(name), ZSTR_LEN(name)) == 0)) {

                        zend_string_release(name);

                        return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

               }

               i++;

      }

       //否则插入到vars中,并将last_var的值设置为该变量的操作数

      i = op_array->last_var;

      op_array->last_var++;

      if (op_array->last_var > CG(context).vars_size) {

               CG(context).vars_size += 16; /* FIXME */

               op_array->vars = erealloc(op_array->vars, CG(context).vars_size * sizeof(zend_string*));

      }

 

      op_array->vars[i] = zend_new_interned_string(name);

      return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

}
  • 3)T为IS_VAR和IS_TMP_VAR类型变量的总数,编译时遇到这种类型,T就会加一,用于后续在执行栈上分配空间。
  • 4)static_variables是用于存放静态变量的HashTable。
  • 5)literals是用来存放常量(IS_CONST)类型的数组,last_literal为常量的个数。
  • 6)run_time_cache用作运行时缓存的操作,本书不展开讨论。

2.5 执行数据

Zend在栈上执行的数据为zend_execute_data,其结构体为:

struct _zend_execute_data {

      const zend_op       *opline;           /* 要执行的指令 */

      zend_execute_data   *call;             /* current call*/

      zval                *return_value;     /* 返回值 */

      zend_function       *func;             /* 执行函数 */

      zval                 This;             /* this + call_info + num_args */

      zend_execute_data   *prev_execute_data;

      zend_array          *symbol_table; /*符号表*/

      void               **run_time_cache;   /* 执行时缓存 */

      zval                *literals;         /* 缓存常量 */

};

下面我们介绍下各字段。

  • 1)opline对应的是zend_op_array中Opcodes数组里面的zend_op,表示正在执行的opline。
  • 2)prev_execute_data: op_array上下文切换的时候,这个字段用来保存切换前的op_array,此字段非常重要,能将每个op_array的execute_data按照调用的先后顺序连接成一个单链表,每当一个op_array执行结束要还原到调用前op_array的时候,就通过当前的execute_data中的prev_execute_data字段来得到调用前的执行器数据。
  • 3)symbol_table,当前使用的符号表,一般会取EG(symbol_table)。
  • 4)literals,常量数组,用来缓存常量。

zend_execute_data是在执行栈上运行的关键数据,可以用EX宏来取其中的值,见代码:

#define EX(element) ((execute_data)->element)

了解完执行数据,下面接下来讨论一下执行栈。

2.6 执行栈

Zend虚拟机中有个类似函数调用栈的结构体,叫_zend_vm_stack。EG里面的vm_stack也是这种类型的。其定义如下:

struct _zend_vm_stack {

      zval *top; //栈顶位置

      zval *end; //栈底位置

      zend_vm_stack prev;

};

typedef struct _zend_vm_stack *zend_vm_stack;

可以看出,栈的结构比较简单,有三个变量top指向栈使用到的位置,end指向栈底,pre是指向上一个栈的指针,也就意味着所有栈在一个单向链表上。
PHP代码在执行时,参数的压栈操作,以及出栈调用执行函数都是在栈上进行的,下面介绍下栈操作的核心函数。

  • 1)初始化

初始化调用的函数为zend_vm_stack_init,主要进行内存申请,以及对_zend_vm_stack成员变量的初始化,见代码:

ZEND_API void zend_vm_stack_init(void)

{

      EG(vm_stack) = zend_vm_stack_new_page(ZEND_VM_STACK_PAGE_SIZE(0 /* main stack */), NULL);

      EG(vm_stack)->top++;

      EG(vm_stack_top) = EG(vm_stack)->top;

      EG(vm_stack_end) = EG(vm_stack)->end;

}

该函数调首先调用zend_vm_stack_new_page为EG(vm_stack)申请内存,申请的大小为161024 sizeof(zval),见代码:

static zend_always_inline zend_vm_stack zend_vm_stack_new_page(size_t size, zend_vm_stack prev) {

      zend_vm_stack page = (zend_vm_stack)emalloc(size);

 

      page->top = ZEND_VM_STACK_ELEMENTS(page);

      page->end = (zval*)((char*)page + size);

      page->prev = prev;

      return page;

}

然后将zend_vm_stack的top指向zend_vm_stack的结束的位置,其中 zend_vm_stack占用24个字节,end指向申请内存的最尾部,pre指向null,如图9所示。

clipboard.png
图9 zend_vm_stack初始化后示意图

可以看出,多个vm_stack构成单链表,将多个栈连接起来,栈初始的大小为16×1024个zval的大小,栈顶部占用了一个*zval和struct _zend_vm_stack的大小,

  • (2)入栈操作

调用的函数为zend_vm_stack_push_call_frame,见代码:

static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame(uint32_t call_info, zend_function *func, uint32_t num_args, zend_class_entry *called_scope, zend_object *object)

{

      uint32_t used_stack = zend_vm_calc_used_stack(num_args, func);

 

      return zend_vm_stack_push_call_frame_ex(used_stack, call_info,

               func, num_args, called_scope, object);

}

该函数会分配一块用于当前作用域的内存空间,并返回zend_execute_data的起始位置。首先调用zend_vm_calc_used_stack计算栈需要的空间,见代码:

 static zend_always_inline uint32_t zend_vm_calc_used_stack(uint32_t num_args, zend_function *func)

{

      uint32_t used_stack = ZEND_CALL_FRAME_SLOT + num_args;

 

      if (EXPECTED(ZEND_USER_CODE(func->type))) {

               used_stack += func->op_array.last_var + func->op_array.T - MIN(func->op_array.num_args, num_args);

      }

      return used_stack * sizeof(zval);

}

这段代码是按照zval的大小对齐,我们知道zval为16字节,那么对于zend_execute_data,大小为80,那么对应5个zval;同时对应IS_CV类型变量个数(last_var)以及变量类型为IS_VAR和IS_TMP_VAR的个数(T),如图10所示。

clipboard.png
图10 压栈过程

到此,我们了解了Zend虚拟机中符号表、指令集、执行数据以及执行栈相关的数据结构,下面我们基于这些基本知识,来介绍一下指令集生成的过程。

3 抽象语法树编译过程

抽象语法树(AST)的编译是生成指令集Opcodes的过程,词法语法分析后生成的AST会保存在CG(ast)中,然后Zend虚拟机会将AST进一步转换为zend_op_array,以便在虚拟机中执行。下面我们讨论一下AST的编译过程。

编译过程在zend_compile函数中,在该函数里,首先调用zendparse做了词法和语法分析的工作,然后开始对CG(ast)的遍历,根据节点不同的类型编译为不同指令opline,代码如下:

static zend_op_array *zend_compile(int type)

{

      /**代码省略**/

      if (!zendparse()) { //词法语法分析

               /**代码省略**/    

 

                 //初始化zend_op_array       

                 init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);

               /**代码省略**/

 

                 //遍历ast生成opline

               zend_compile_top_stmt(CG(ast));

               /**代码省略**/

 

                //设置handler

               pass_two(op_array);

                /**代码省略**/

      }

       /**代码省略**/

      return op_array;

}

从上面的过程中可以看出,编译的主要过程是op_array的初始化,调用zend_compile_top_stmt遍历抽象语法树成opline,以及调用pass_two函数设置handler。下面我们一一阐述。

3.1 op_array初始化

在遍历抽象语法树之前,需要先初始化指令集op_array,用来存放指令。op_array的初始化工作,调用的函数为init_op_array,该函数会将op_array进行初始化,代码如下:

op_array = emalloc(sizeof(zend_op_array));

init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);

void init_op_array(zend_op_array *op_array, zend_uchar type, int initial_ops_size)

{

      op_array->type = type;

      op_array->arg_flags[0] = 0;

      op_array->arg_flags[1] = 0;

      op_array->arg_flags[2] = 0;

      /**代码省略**/

}

CG(active_op_array) = op_array;

首先通过emalloc申请内存,大小为sizeof(zend_op_array)=208,然后初始化op_array的所有成员变量,把op_array赋值给CG(active_op_array)。

3.2 抽象语法树编译

抽象语法树的编译过程,是遍历抽象语法树,生成对应指令集的过程,编译是在 zend_compile_top_stmt() 中完成,这个函数是总入口,会被多次递归调用。其中传入的参数为CG(ast),这个AST是通过词法和语法分析得到的。下面我们看一下zend_compile_top_stmt的代码:

void zend_compile_top_stmt(zend_ast *ast) /* {{{ */

{

      if (!ast) {

               return;

      }

       //对于kind为ZEND_AST_STMT_LIST的节点,转换为zend_ast_list

      if (ast->kind == ZEND_AST_STMT_LIST) {

               zend_ast_list *list = zend_ast_get_list(ast);

               uint32_t i;

               //根据children的个数进行递归调用

               for (i = 0; i < list->children; ++i) {

                        zend_compile_top_stmt(list->child[i]);

               }

               return;

      }

       //其他kind的节点调用zend_compile_stmt

      zend_compile_stmt(ast);

 

      if (ast->kind != ZEND_AST_NAMESPACE && ast->kind != ZEND_AST_HALT_COMPILER) {

               zend_verify_namespace();

      }

      if (ast->kind == ZEND_AST_FUNC_DECL || ast->kind == ZEND_AST_CLASS) {

               CG(zend_lineno) = ((zend_ast_decl *) ast)->end_lineno;

               zend_do_early_binding();

      }

}

从代码中可以看到,对于zend_compile_top_stmt,会对AST节点的kind进行判断,然后走不同的逻辑,实际上是对AST的深度遍历,我们以下面的代码为例,看一下对AST的遍历过程。

<?php

$a = 1;

$b = $a + 2;

echo $b;

可以得到的AST如图11所示。

clipboard.png
图11 抽象语法树示意图

通过这课抽象语法树。可以很直观的看出,CG(ast)节点下面有三个子女:

  • 1)第一个子女,其kind是ZEND_AST_ASSIGN,有两个子女,分别是ZEND_AST_VAR和ZEND_AST_ZVAL,对应$a=1。
  • 2)第二个子女,其kind也是ZEND_AST_ASSIGN,有两个子女分别是ZEND_AST_VAR和ZEND_AST_BINARY_OP,其中ZEND_AST_BINARY_OP对应的是相加操作,对应的是$b=$a+2。
  • 3)第三个子女,其kind是ZEND_AST_STMT_LIST,有一个子女,为ZEND_AST_ECHO,对应的是echo $b。

下面我们看整棵树的遍历过程。

  • 1. Assign编译过程
  • 1)首先根节点kind为ZEND_AST_STMT,会调用函数zend_ast_get_list将其转换为zend_ast_list *,得到children的个数为2,接着递归调用zend_compile_top_stmt,这样就可以把抽象语法树根节点的最左子女遍历一遍,以便生成对应的指令;
  • 2)遍历第一个子女节点,对应的kind为ZEND_AST_ASSIGN,编译过程是调用函数zend_compile_stmt,继而调用zend_compile_expr函数,见代码:
void zend_compile_stmt(zend_ast *ast) /* {{{ */

{

      /*…代码省略…*/

      switch (ast->kind) {

               /*…代码省略…*/

                 default:

               {

                        znode result;

                        zend_compile_expr(&result, ast);

                        zend_do_free(&result);

               }

                  /*…代码省略…*/

}    

void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */

{

      /*…代码省略…*/

      switch (ast->kind) {

               /*…代码省略…*/

               case ZEND_AST_ASSIGN:

                        zend_compile_assign(result, ast);

                        return; 

         /*…代码省略…*/

}

最终调用的函数为zend_compile_assign,对ZEND_AST_ASSIGN节点进行编译:

void zend_compile_assign(znode *result, zend_ast *ast) /* {{{ */

{

      zend_ast *var_ast = ast->child[0];

      zend_ast *expr_ast = ast->child[1];

 

      znode var_node, expr_node;

      zend_op *opline;

      uint32_t offset;

       /*…代码省略…*/

      

      switch (var_ast->kind) {

               case ZEND_AST_VAR:

               case ZEND_AST_STATIC_PROP:

                        offset = zend_delayed_compile_begin();

                        zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W);

                        zend_compile_expr(&expr_node, expr_ast);

                        zend_delayed_compile_end(offset);

                        zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node);

                        return;

         /*…代码省略…*/

}

从代码中可以看出,kind为ZEND_AST_ASSIGN的抽象语法树有两个子女,左child为var_ast,右child为expr_ast,分别进行处理。

  • 3)调用zend_delayed_compile_begin:
static inline uint32_t zend_delayed_compile_begin(void) /* {{{ */

{

      return zend_stack_count (&CG(delayed_oplines_stack));

}

该函数会获取CG的delayed_oplines_stack栈顶的位置,其中delayed_oplines_stack是对于依赖后续编译动作存储信息的栈。等expr_ast编译后使用,调用zend_delayed_compile_end(offset)来获取栈里的信息。

  • 4)对于左子女var_ast调用zend_delayed_compile_var:
void zend_delayed_compile_var(znode *result, zend_ast *ast, uint32_t type) /* {{{ */

{

      zend_op *opline;

      switch (ast->kind) {

               case ZEND_AST_VAR:

                        zend_compile_simple_var(result, ast, type, 1);

   /**代码省略**/

}

其中kind为ZEND_AST_VAR,继而调用zend_compile_simple_var函数:

static void zend_compile_simple_var(znode *result, zend_ast *ast, uint32_t type, int delayed) /* {{{ */

{

      zend_op *opline;

 

      /*…代码省略…*/

      } else if (zend_try_compile_cv(result, ast) == FAILURE) {

               /*…代码省略…*/

      }

}

继而调用zend_try_compile_cv函数:

static int zend_try_compile_cv(znode *result, zend_ast *ast) /* {{{ */

{

      zend_ast *name_ast = ast->child[0];

      if (name_ast->kind == ZEND_AST_ZVAL) {

               /*…代码省略…*/

 

               result->op_type = IS_CV;

               result->u.op.var = lookup_cv(CG(active_op_array), name);

 

      /*…代码省略…*/

}

核心函数是lookup_cv,在这里面组装了操作数,见代码:

static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{

      int i = 0;

      zend_ulong hash_value = zend_string_hash_val(name);

       //判断变量是否在vars中存在,若存在直接返回对应的位置

      while (i < op_array->last_var) {

               if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) ||

                   (ZSTR_H(op_array->vars[i]) == hash_value &&

                    ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) &&

                    memcmp(ZSTR_VAL(op_array->vars[i]), ZSTR_VAL(name), ZSTR_LEN(name)) == 0)) {

                        zend_string_release(name);

                        return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

               }

               i++;

      }

       //若不存在,则写入vars中,返回新插入的位置

      i = op_array->last_var;

      op_array->last_var++;

      /*…代码省略…*/

 

      op_array->vars[i] = zend_new_interned_string(name);

 

      return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);

}

从代码中可以看出,变量是存放到op_array->vars中的,而返回的是一个int型的地址,这个是什么呢?我们看一下宏ZEND_CALL_VAR_NUM的定义:

#define ZEND_CALL_VAR_NUM(call, n) \

      (((zval*)(call)) + (ZEND_CALL_FRAME_SLOT + ((int)(n))))

#define ZEND_CALL_FRAME_SLOT \

      ((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval))))

可以看出,这个值都是sizeof(zval)的整数倍,在笔者的机器上,zval的大小为16,而zend_execute_data大小为80,所以返回的是每个变量的偏移值,即80+16*i,如图12所示。

clipboard.png
图12 左子女var_ast编译示意图

此时,就将赋值语句$a=1中,左侧表达式$a编译完成,赋值给了znode* result,下面继续对右子女常量1的编译。

  • 5)对于右子女,调用函数zend_compile_expr进行编译,见代码:
void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */

{

    /* CG(zend_lineno) = ast->lineno; */

    CG(zend_lineno) = zend_ast_get_lineno(ast);

 

     switch (ast->kind) {

         case ZEND_AST_ZVAL:

            ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast));

            result->op_type = IS_CONST;

            return;

从代码中可以看出,对于常量1,通过ZVAL_COPY,将值复制到result->u.constan中,同时将result->op_type赋值为IS_CONST。这样,对于assign操作的两个操作数都编译完成了,下面我们看一下对应指令opline的生成过程。

  • 6)opline生成调用函数zend_emit_op,见代码:
static zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2) /* {{{ */

{

       //分配和获取opline,并设置其opcode

      zend_op *opline = get_next_op(CG(active_op_array));

      opline->opcode = opcode;

        //设置操作数1

      if (op1 == NULL) {

               SET_UNUSED(opline->op1);

      } else {

               SET_NODE(opline->op1, op1);

      }

        //设置操作数2

      if (op2 == NULL) {

               SET_UNUSED(opline->op2);

      } else {

               SET_NODE(opline->op2, op2);

      }

 

      zend_check_live_ranges(opline);

 

      if (result) {

                  //设置返回值

               zend_make_var_result(result, opline);

      }

      return opline;

}

其中对操作数得到设置,对应的是宏SET_NODE,见代码:

  #define SET_NODE(target, src) do { \

               target ## _type = (src)->op_type; \

               if ((src)->op_type == IS_CONST) { \

                        target.constant = zend_add_literal(CG(active_op_array), &(src)->u.constant); \

               } else { \

                        target = (src)->u.op; \

               } \

      } while (0)

 

int zend_add_literal(zend_op_array *op_array, zval *zv) /* {{{ */

{

      int i = op_array->last_literal;

      op_array->last_literal++;

      if (i >= CG(context).literals_size) {

               while (i >= CG(context).literals_size) {

                        CG(context).literals_size += 16; /* FIXME */

               }

               op_array->literals = (zval*)erealloc(op_array->literals, CG(context).literals_size * sizeof(zval));

      }

      zend_insert_literal(op_array, zv, i);

      return i;

}

从代码中可以看出,对于操作数1,会将编译过程中临时的结构znode传递给zend_op中,对于操作数2,因为是常量(IS_CONST),会调用zend_add_literal将其插入到op_array->literals中。

从返回值的设置,调用的是zend_make_var_result,其代码如下:

  static inline void zend_make_var_result(znode *result, zend_op *opline) /* {{{ */

{

        //返回值的类型设置为IS_VAR

      opline->result_type = IS_VAR;

        //这个是返回值的编号,对应T位置

      opline->result.var = get_temporary_variable(CG(active_op_array));

      GET_NODE(result, opline->result);

}

static uint32_t get_temporary_variable(zend_op_array *op_array) /* {{{ */

{

      return (uint32_t)op_array->T++;

}

返回值的类型为IS_VAR,result.var为T的值,下面我们给出Assign操作对应的指令图,如图13所示。

clipboard.png
图13 Assign指令示意图

从图13中可以看出,生成的opline中opcode等于38;op1的类型为IS_CV,op1.var对应的是vm_stack上的偏移量;op2的类型为IS_CONST,op2.constant对应的是op_array中literals数组的下标;result的类型为IS_VAR,result.var对应的是T的值;此时handler的值为空。

  • 2. Add编译过程

对于“$b =$a+2;”语句,首先是add语句,也就是$a+1,跟assign语句类型类似,不同是调用了函数zend_compile_binary_op,见代码:

void zend_compile_binary_op(znode *result, zend_ast *ast) /* {{{ */

{

      zend_ast *left_ast = ast->child[0];

      zend_ast *right_ast = ast->child[1];

      uint32_t opcode = ast->attr;//通过attr区分加减乘除等等操作

 

      znode left_node, right_node;

      zend_compile_expr(&left_node, left_ast);

      zend_compile_expr(&right_node, right_ast);

      /*…代码省略…*/

       zend_emit_op_tmp(result, opcode, &left_node, &right_node);

      /*…代码省略…*/

}

对于加减乘除等操作,kind都是ZEND_AST_BINARY_OP,具体操作通过AST中的attr区分的,因为$a+1会生成临时变量,因此与Assign操作不同,调用的函数是zend_emit_op_tmp:

 static zend_op *zend_emit_op_tmp(znode *result, zend_uchar opcode, znode *op1, znode *op2) /* {{{ */

{

      /*…代码与zend_emit_op一样…*/

      if (result) {

               zend_make_tmp_result(result, opline);

      }

 

      return opline;

}

zend_emit_op_tmp函数与zend_emit_op类似,opline中的操作数1和操作数2做了同样的操作,而result不同之处在于,其类型是IS_TMP_VAR,因此opline如图14所示。

clipboard.png
图14 Add指令示意图

对于“$b=$a+2;”相当于把临时变量赋值给$b,与Assign编译过程一致,生成opline如图15所示。

clipboard.png
图15 第2条Assign指令示意图

  • 3. Echo编译过程
    对于“echo $b;”,编译过程类似于Assign和Add的编译,不同处是调用函数zend_compile_echo。
void zend_compile_echo(zend_ast *ast) /* {{{ */

{

      zend_op *opline;

      zend_ast *expr_ast = ast->child[0];

 

      znode expr_node;

      zend_compile_expr(&expr_node, expr_ast);

 

      opline = zend_emit_op(NULL, ZEND_ECHO, &expr_node, NULL);

      opline->extended_value = 0;

}

Echo对应的指令只有一个操作数,对于操作数2,SET_UNUSED宏设置为IS_UNUSED。

#define SET_UNUSED(op)  op ## _type = IS_UNUSED

生成的opline如图16所示。

clipboard.png
图16 Echo指令示意图

  • 4. Return编译过程

上面对于AST编译并没有结束,PHP代码中虽然没有return操作,但是默认会生成一条ZEND_RETURN指令,通过zend_emit_final_return含设置,代码如下:

void zend_emit_final_return(int return_one) /* {{{ */

{

      znode zn;

      zend_op *ret;

      /**代码省略**/

 

      zn.op_type = IS_CONST;

      if (return_one) {

               ZVAL_LONG(&zn.u.constant, 1);

      } else {

               ZVAL_NULL(&zn.u.constant);

      }

 

      ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL);

      ret->extended_value = -1;

}

同样通过zend_emit_op设置opline,设置以后的opline如图17所示。

clipboard.png
图17 Return指令示意图

经过对Assign、Add和Echo的编译后,生成的全部oplines如图18所示。

clipboard.png
图18 所有指令集示意图

到这里,我们了解了AST编译生成opline指令集的过程,包括op1、op2和result的生成过程,但是此时opline中的handler还都是空指针,接下来我们看一下handler设置的过程。

3.3 设置指令handler

抽象语法树编译后还有一个重要操作,函数叫pass_two,这个函数中,对opline指令集做了进一步的加工,最主要的工作是设置指令的handler,代码如下:

ZEND_API int pass_two(zend_op_array *op_array)

{

     /**代码省略**/

   while (opline < end) {//遍历opline数组

      if (opline->op1_type == IS_CONST) {

               ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op1);

      } else if (opline->op1_type & (IS_VAR|IS_TMP_VAR)) {

       opline->op1.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op1.var);

          }

     

      if (opline->op2_type == IS_CONST) {

          ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op2);

      } else if (opline->op2_type & (IS_VAR|IS_TMP_VAR)) {

          opline->op2.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op2.var);

               }

    if (opline->result_type & (IS_VAR|IS_TMP_VAR)) {

       opline->result.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->result.var);

     }

     ZEND_VM_SET_OPCODE_HANDLER(opline);

     /**代码省略**/

}

从代码中可以看出,该函数会对opline指令数组进行遍历,对每一条opline指令进行操作,对于op1和op2如果是IS_CONST类型,调用ZEND_PASS_TWO_UPDATE_CONSTANT,见代码:

 /* convert constant from compile-time to run-time */

# define ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, node) do { \

               (node).constant *= sizeof(zval); \

      } while (0)

根据上一节的知识我们知道,对于IS_CONST类型的变量,其值是存在op_array->literals数组中,因此,可以直接使用数组下标乘以sizeof(zval)转换为偏移量。

对于op1和op2如果是IS_VAR或者IS_TMP_VAR类型的变量,跟上一节一样,通过ZEND_CALL_VAR_NUM计算偏移量。

另外一个非常重要的工作是通过ZEND_VM_SET_OPCODE_HANDLER(opline),设置opline对应的hanlder,代码如下:

ZEND_API void zend_vm_set_opcode_handler(zend_op* op)

{

      op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);

}

其中opcode和handler之前的对应关系在Zend/zend_vm_execute.h中定义的。opline数组经过一次遍历后,handler也就设置完毕,设置后的opline数组如图19所示。

clipboard.png
图19 设置handler后的指令集

到此,整个抽象语法树就编译完成了,最终的结果为opline指令集,接下来就是在Zend虚拟机上执行这些指令。

4执行过程

执行的入口函数为zend_execute,在该函数中会针对上一节生成的opline指令集进行调度执行。首先会在EG(vm_stack)上分配空间,然后每一条指令依次压栈并调用对应的handler。代码如下:

ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)

{

      zend_execute_data *execute_data;

      /**代码省略**/

      //压栈生成execute_data

      execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE,

               (zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));

       //设置symbol_table

      if (EG(current_execute_data)) {

               execute_data->symbol_table = zend_rebuild_symbol_table();

      } else {

               execute_data->symbol_table = &EG(symbol_table);

      }

      EX(prev_execute_data) = EG(current_execute_data);

       //初始化execute_data

      i_init_execute_data(execute_data, op_array, return_value);

       //执行

      zend_execute_ex(execute_data);

       //释放execute_data

      zend_vm_stack_free_call_frame(execute_data);

}

这个代码中首先根据op_array中的指令生成对应的execute_data,然后初始化后调用handler执行。下面我们具体分析一下执行的过程。

4.1 执行栈分配

执行栈是通过2.6节介绍的zend_vm_stack_push_call_frame完成的,会在EG(vm_stack)上分配一块内存区域,80个字节用来存放execute_data,紧接着下面是根据last_var和T的数量分配zval大小的空间,以3节编译生成的指令集为例,分配的栈如图20所示。

clipboard.png
图20 执行栈分配示意图

从图20中看出,在EG(vm_stack)上分配空间,空间的大小跟op_array中last_var和T的值相关。

4.2 初始化execute_data

在执行栈上分配空间后,会调用函数i_init_execute_data对执行数据进行初始化,见代码:

    static zend_always_inline void i_init_execute_data(zend_execute_data *execute_data, zend_op_array *op_array, zval *return_value) /* {{{ */

{

         ZEND_ASSERT(EX(func) == (zend_function*)op_array);

 

         EX(opline) = op_array->opcodes;//读取第一条指令

         EX(call) = NULL;

         EX(return_value) = return_value;//设置返回值

 

         if (EX_CALL_INFO() & ZEND_CALL_HAS_SYMBOL_TABLE) {

                    //赋值符号表

                  zend_attach_symbol_table(execute_data);

          /**代码省略**/

 

          //运行时缓存

          if (!op_array->run_time_cache) {

                  if (op_array->function_name) {

                           op_array->run_time_cache = zend_arena_alloc(&CG(arena), op_array->cache_size);

                  } else {

                           op_array->run_time_cache = emalloc(op_array->cache_size);

                  }

                  memset(op_array->run_time_cache, 0, op_array->cache_size);

         }

         EX_LOAD_RUN_TIME_CACHE(op_array);

         EX_LOAD_LITERALS(op_array);//设置常量数组

 

         EG(current_execute_data) = execute_data;

}

从代码中可以看出,初始化工作主要做了几件事:

  • 1)读取op_array中的第一条指令,赋值给EX(opline),其中EX宏是对execute_data的取值宏;
  • 2)设置EX的返回值;
  • 3)赋值符号表;
  • 4)设置运行时缓存;
  • 5)设置常量数组。

做完这些工作后,执行栈中数据的结果如图21所示。

clipboard.png
图21 初始化execute_data示意图

4.3 调用hanlder函数执行

接下来是调用execute_ex进行指令的执行,见代码:

ZEND_API void execute_ex(zend_execute_data *ex)

{

         ZEND_VM_LOOP_INTERRUPT_CHECK();

 

         while (1) { //循环

                  int ret;

                  if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0))

       {

                  if (EXPECTED(ret > 0)) {

                           execute_data = EG(current_execute_data);

                           ZEND_VM_LOOP_INTERRUPT_CHECK();

                  } else {

                           return;

                  }

}

}

从代码中可以看出,整个执行最外层是while循环,直到结束才退出。调用的是opline中对应的handler,下面以3节中生成的指令集进行详细的阐述。

  • 1)对于第一个指令,对应的handler为:
//ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER

//通过op2获取到常量数组里面的值

value = EX_CONSTANT(opline->op2);

//获取到op1对应的位置

variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);

//将常量赋值给对应位置的指针

value = zend_assign_to_variable(variable_ptr, value, IS_CONST);

//将结果复制到result

ZVAL_COPY(EX_VAR(opline->result.var), value);

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

首先通过op2.constant值获取到常量表中的zval值,通过op1.var获取到栈中对应的位置,然后将常量值赋值到对应的位置,同时将其copy到result对应的位置,如图22所示。

clipboard.png
图22 Assign指令执行示意图

完成assign操作后,会调用ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION宏进行下一条指令的执行,也就是opline+1。

  • 2)第二条指令对应的是相加操作,其handler为:
//ZEND_ADD_SPEC_CV_CONST_HANDLER

zval *op1, *op2, *result;

//获取op1对应的位置

op1 = _get_zval_ptr_cv_undef(execute_data, opline->op1.var);

//获取op2对应的值

op2 = EX_CONSTANT(opline->op2);

/**代码省略**/

//执行相加操作,赋值给result

add_function(EX_VAR(opline->result.var), op1, op2);

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

首先根据op1.var获取对应的位置,然后根据op2.constant值获取到常量表中的zval值,最后进行相加操作,赋值给result对应的位置,如图23所示。

clipboard.png
图23 Add指令执行示意图

  • 3)第三条指令依然是assign,但是因为类型与第一条指令不同,因此对应的handler也不同:
//ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_UNUSED_HANDLER

zval *value;

zval *variable_ptr;

//根据op2.var获取临时变量的位置

value = _get_zval_ptr_tmp(opline->op2.var, execute_data, &free_op2);

//根据op1.var获取操作数1 的位置

variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);

//将临时变量赋值给操作数1对应的位置

value = zend_assign_to_variable(variable_ptr, value, IS_TMP_VAR);

//同时拷贝到result对应的位置

ZVAL_COPY(EX_VAR(opline->result.var), value);

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

与第一条指令类似,执行过程如图24所示。

clipboard.png
图24 第2条Assign指令示意图

  • 4)第四条指令是echo操作,对应的handler为:
// ZEND_ECHO_SPEC_CV_HANDLER

zval *z;

//根据op1.var获取对应位置的值

z = _get_zval_ptr_cv_undef(execute_data, opline->op1.var);

//调用zend_write输出

zend_write(ZSTR_VAL(str), ZSTR_LEN(str));

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

这条指令会根据op1.var获取到对应的位置,取出zval值输出,如图25所示。

clipboard.png
图25 Echo指令执行示意图

  • 5)第五条指令为return,对应的handler为:
//ZEND_RETURN_SPEC_CONST_HANDLER

zval *retval_ptr;

zval *return_value;

retval_ptr = EX_CONSTANT(opline->op1);

return_value = EX(return_value);

//调用zend_leave_helper_SPEC函数,返回

ZEND_VM_TAIL_CALL(zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));

这条指令没有做实质性的操作,核心是返回-1,让while循环退出,指令执行结束。

到此,整个的执行过程就阐述完成了,相信读者通过这五条指令的执行,初步理解了Zend虚拟机的执行过程。

4.4 释放execute_data

指令执行完毕后,调用zend_vm_stack_free_call_frame释放execute_data,并回收EG(vm_stack)上使用的空间,这部分比较简单。

5小结

本文主要介绍了Zend虚拟机的实现原理,包括抽象语法树编译生成指令集的过程,以及指令集执行的过程。同时介绍了Zend虚拟机运行中用到的数据结构。希望读者读完本文,能够对Zend虚拟机有一定的认识。

查看原文

赞 51 收藏 37 评论 4

hi 发布了文章 · 2018-06-24

设计模式

设计模式 设计模式 设计模式

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 28 次点赞
  • 获得 8 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-04-23
个人主页被 467 人浏览