首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

PHP7 源码分析:如何理解 PHP 虚拟机(一)

顺风车运营研发团队:李乐

1.从物理机说起

虚拟机也是计算机,设计思想和物理机有很多相似之处;

1.1冯诺依曼体系结构

冯·诺依曼是当之无愧的数字计算机之父,当前计算机都采用的是冯诺依曼体系结构;设计思想主要包含以下几个方面:

指令和数据不加区别混合存储在同一个存储器中,它们都是内存中的数据。现代CPU的保护模式,每个内存段都有段描述符,这个描述符记录着这个内存段的访问权限(可读,可写,可执行)。这就变相的指定了哪些内存中存储的是指令哪些是数据)。

存储器是按地址访问的线性编址的一维结构,每个单元的位数是固定的。

数据以二进制表示。

指令由操作码和操作数组成。操作码指明本指令的操作类型,操作数指明操作数本身或者操作数的地址。操作数本身并无数据类型,它的数据类型由操作码确定;任何架构的计算机都会对外提供指令集合。

运算器通过执行指令直接发出控制信号控制计算机各项操作。由指令计数器指明待执行指令所在的内存地址。指令计数器只有一个,一般按顺序递增,但执行顺序可能因为运算结果或当时的外界条件而改变。

1.2汇编语言简介

任何架构的计算机都会提供一组指令集合。

指令由操作码和操作数组成。

操作码即操作类型,操作数可以是一个立即数或者一个存储地址。

每条指令可以有0、1或2个操作数。

指令就是一串二进制。

汇编语言是二进制指令的文本形式。

、 、 、 等就是操作码。

寄存器。

内存地址。

操作数只是一块可存取数据的存储区。

操作数本身并无数据类型,它的数据类型由操作码确定。如movb传送字节,movw传送字,movl传送双字等。

1.3 函数调用栈

过程(函数)是对代码的封装,对外暴露的只是一组指定的参数和一个可选的返回值。可以在程序中不同的地方调用这个函数。假设过程P调用过程Q,Q执行后返回过程P,为了实现这一功能,需要考虑三点:

指令跳转:进入过程Q的时候,程序计数器必须被设置为Q的代码的起始地址;在返回时,程序计数器需要设置为P中调用Q后面那条指令的地址。

数据传递:P能够向Q提供一个或多个参数,Q能够向P返回一个值。

内存分配与释放:Q开始执行时,可能需要为局部变量分配内存空间,而在返回前,又需要释放这些内存空间。

大多数的语言过程调用都采用了栈数据结构提供的内存管理机制,如下图所示:

函数的调用与返回即对应的是一系列的入栈与出栈操作。

函数在执行时,会有自己私有的栈帧,局部变量就是分配在函数私有栈帧上的。

平时遇到的栈溢出就是因为调用函数层级过深,不断入栈导致的。

2.PHP虚拟机

虚拟机也是计算机,参考物理机的设计,设计虚拟机时,首先应该考虑三个要素:指令、数据存储、函数栈帧。

下面从这三点详细分析PHP虚拟机的设计思路。

2.1指令2.1.1 指令类型

任何架构的计算机都需要对外提供一组指令集,其代表计算机支持的一组操作类型。

PHP虚拟机对外提供186种指令,定义在 文件中。

2.1.2 指令

2.1.2.1指令的表示

指令由操作码和操作数组成。

操作码指明本指令的操作类型,操作数指明操作数本身或者操作数的地址。

PHP虚拟机定义指令格式为:操作码 操作数1 操作数2 返回值。其使用结构体zendop表示一条指令:

2.1.2.2 操作数的表示

从上面可以看到,操作数使用结构体znode_op表示,定义如下:

constant、var、num等都是uint32_t类型的,这怎么表示一个操作数呢(既不是指针不能代表地址,也无法表示所有数据类型)?其实,操作数大多情况采用的相对地址表示方式,constant等表示的是相对于执行栈帧首地址的偏移量。

另外,znodeop结构体中有个zval *zv字段,其也可以表示一个操作数,这个字段是一个指针,指向的是zval结构体,PHP虚拟机支持的所有数据类型都使用zval结构体表示。

2.2 数据存储

PHP虚拟机支持多种数据类型:整型、浮点型、字符串、数组,对象等;PHP虚拟机如何存储和表示多种数据类型?

2.1.2.2 节指出结构体znodeop代表一个操作数,操作数可以是一个偏移量(计算得到一个地址,即zval结构体的首地址),或者一个zval指针。PHP虚拟机使用zval结构体表示和存储多种数据。

zend_value存储具体的数据内容,结构体定义如下:

zendvalue占16字节内存,long、double类型会直接存储在结构体,引用、字符串、数组等类型使用指针存储。

可以看出,字符串使用zendstring表示,数组使用zendarray表示…

如下图为PHP7中字符串结构图:

2.3 再谈指令

2.1.2.1指出,指令使用结构体zendop表示。其中最主要2个属性:操作函数、操作数(两个操作数和一个返回值)。

操作数的类型(常量、临时变量等)不同,同一个指令对应的handler函数也会不同。操作数类型定义在 Zend/zend_compile.h文件:

操作函数命名规则为:ZEND[opcode]SPEC(操作数1类型)(操作数2类型)(返回值类型)HANDLER。

比如赋值语句就有以下多种操作函数:

对于$a=1,其操作函数为: ZENDASSIGNSPECCVCONSTRETVALUNUSED_HANDLER。函数实现为:

2.4 函数栈帧2.4.1指令集

上面分析了指令的结构与表示,PHP虚拟机使用zendop_array表示指令的集合:

注意: lastvar代表ISCV类型变量的个数,这种类型变量存放在vars数组中;在整个编译过程中,每次遇到一个ISCV类型的变量(类似于$something),就会去遍历vars数组,检查是否已经存在,如果不存在,则插入到vars中,并将lastvar的值设置为该变量的操作数;如果存在,则使用之前分配的操作数。

2.4.2 函数栈帧

PHP虚拟机实现了与1.3节物理机类似的函数栈帧结构。

使用zendvm_stack表示栈结构,多个栈之间使用prev字段形成单向链表。top和end指向栈低和栈顶,分别为zval类型的指针。

考虑如何设计函数执行时候的帧结构:当前函数执行时,需要存储函数编译后的指令,需要存储函数内部的局部变量等(2.1.2.2节指出,操作数使用结构体znodeop表示,其内部使用uint32t表示操作数,此时表示的就是当前zval变量相对于当前函数栈帧首地址的偏移量)。

PHP虚拟机使用结构体zendexecute_data存储当前函数执行所需数据。

函数开始执行时,需要为函数分配相应的函数栈帧并入栈,代码如下:

从上面分析可以得到函数栈帧结构图如下所示:

总结

PHP虚拟机也是计算机,有三点是我们需要重点关注的:指令集(包含指令处理函数)、数据存储(zval)、函数栈帧。

此时虚拟机已可以接受指令并执行指令代码。

但是,PHP虚拟机是专用执行PHP代码的,PHP代码如何能转换为PHP虚拟机可以识别的指令呢——编译。

PHP虚拟机同时提供了编译器,可以将PHP代码转换为其可以识别的指令集合。

理论上你可以自定义任何语言,只要实现编译器,能够将你自己的语言转换为PHP可以识别的指令代码,就能被PHP虚拟机执行。

欢迎关注 SegmentFault 微信公众号 :)

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180817B09DI600?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券