PHP数组/Hash表的实现/操作、PHP变量内核实现、PHP常量内核实现 - [ PHP内核学习 ]
catalogue
1. PHP Hash表 2. PHP数组定义 3. PHP变量实现 4. PHP常量实现
1. PHP Hash表
0x1: 基本概念
哈希表在实践中使用的非常广泛,例如编译器通常会维护的一个符号表来保存标记,很多高级语言中也显式的支持哈希表。 哈希表通常提供查找(Search),插入(Insert),删除(Delete)等操作,这些操作在最坏的情况
下和链表的性能一样为O(n)。 不过通常并不会这么坏,合理设计的哈希算法能有效的避免这类情况,通常哈希表的这些操作时间复杂度为O(1)。 这也是它被钟爱的原因
正是因为哈希表在使用上的便利性及效率上的表现,目前大部分动态语言的实现中都使用了哈希表
哈希表是一种通过哈希函数,将特定的键映射到特定值的一种数据结构,它维护键和值之间一一对应关系
1. 键(key): 用于操作数据的标示,例如PHP数组中的索引,或者字符串键等等 2. 槽(slot/bucket): 哈希表中用于保存数据的一个单元,也就是数据真正存放的容器 3. 哈希函数(hash function): 将key映射(map)到数据应该存放的slot所在位置的函数 4. 哈希冲突(hash collision): 哈希函数将两个不同的key映射到同一个索引的情况
哈希表可以理解为数组的扩展或者关联数组,数组使用数字下标来寻址,如果关键字(key)的范围较小且是数字的话, 我们可以直接使用数组来完成哈希表,而如果关键字范围太大,如果直接使用数组我们需要为所有可能的key申请空间。 很多情况下这是不现实的。即使空间足够,空间利用率也会很低,这并不理想。同时键也可能并不是数字, 在PHP中尤为如此,所以人们使用一种映射函数(哈希函数)来将key映射到特定的域中
h(key) -> index
通过合理设计的哈希函数,我们就能将key映射到合适的范围,因为我们的key空间可以很大(例如字符串key), 在映射到一个较小的空间中时可能会出现两个不同的key映射被到同一个index上的情况, 这就是我们所说的出现了冲突。 目前解决hash冲突的方法主要有两种:链接法和开放寻址法
1. 冲突解决: 链接法
链接法通过使用一个链表来保存slot值的方式来解决冲突,也就是当不同的key映射到一个槽中的时候使用链表来保存这些值。 所以使用链接法是在最坏的情况下,也就是所有的key都映射到同一个槽中了,这样哈希表就退化成了一个链表, 这样的话操作链表的时间复杂度则成了O(n),这样哈希表的性能优势就没有了, 所以选择一个合适的哈希函数是最为关键的
由于目前大部分的编程语言的哈希表实现都是开源的,大部分语言的哈希算法都是公开的算法, 虽然目前的哈希算法都能良好的将key进行比较均匀的分布,而这个假使的前提是key是随机的,正是由于算法的确定性, 这就导致了别有用心的黑客能利用已知算法的可确定性来构造一些特殊的key,让这些key都映射到同一个槽位导致哈希表退化成单链表,导致程序的性能急剧下降,从而造成一些应用的吞吐能力急剧下降, 尤其是对于高并发的应用影响很大,通过大量类似的请求可以让服务器遭受DoS(服务拒绝攻击)
哈希冲突攻击利用的哈希表最根本的弱点是:
开源算法和哈希实现的确定性以及可预测性, 这样攻击者才可以利用特殊构造的key来进行攻击。要解决这个问题的方法则是让攻击者无法轻易构造 能够进行攻击的key序列
目前PHP中HashTable的哈希冲突解决方法就是链接法
2. 冲突解决: 开放寻址法
通常还有另外一种解决冲突的方法:开放寻址法。使用开放寻址法是槽本身直接存放数据,在插入数据时如果key所映射到的索引已经有数据了,这说明发生了冲突,这是会寻找下一个槽, 如果该槽也被占用了则继续寻找下一个槽,直到寻找到没有被占用的槽,在查找时也使用同样的策略来进行
由于开放寻址法处理冲突的时候占用的是其他槽位的空间,这可能会导致后续的key在插入的时候更加容易出现 哈希冲突,所以采用开放寻址法的哈希表的装载因子不能太高,否则容易出现性能下降
装载因子是哈希表保存的元素数量和哈希表容量的比 1. 通常采用链接法解决冲突的哈希表的装载,因子最好不要大于1(等于1意味着Hash表已经存满了,接下来开始保存的键值都会导致冲突发生即链表增长,而链表的效率是低于Hash表的) 2. 而采用开放寻址法的哈希表最好不要大于0.5
0x2: 哈希表的实现
实现一个哈希表也很容易,主要需要完成的工作只有三点
1. 实现哈希函数 2. 冲突的解决 3. 操作接口的实现
在开始学习PHP原生内核的Hash表实现前,我们自己可以先手工实现一个简易版的Hash表
1. 基础数据结构定义
#ifndef _HASH_TABLE_H_ #define _HASH_TABLE_H_ 1 typedef struct _Bucket { char *key; void *value; struct _Bucket *next; } Bucket; typedef struct _HashTable { int size; //HashTable size/lines int elem_num; //total elements count Bucket** buckets; } HashTable; #endif
2. 哈希函数实现
哈希函数需要尽可能的将不同的key映射到不同的槽(slot或者bucket)中,首先我们采用一种最为简单的哈希算法实现: 将key字符串的所有字符加起来,然后以结果对哈希表的大小取模,这样索引就能落在数组索引的范围之内了
//hashtable.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include "hashtable.h" int hash_str(char *key); int hash_str(char *key) { int hash = 0; char *cur = key; while(*cur != '\0') { hash += *cur; ++cur; } return hash; } //hashtable.h #define HASH_INDEX(ht, key) (hash_str((key)) % (ht)->size)
3. 操作接口的实现
为了操作哈希表,实现了如下几个操作接口函数
int hash_init(HashTable *ht); // 初始化哈希表 int hash_lookup(HashTable *ht, char *key, void **result); // 根据key查找内容 int hash_insert(HashTable *ht, char *key, void *value); // 将内容插入到哈希表中 int hash_remove(HashTable *ht, char *key); // 删除key所指向的内容 int hash_destroy(HashTable *ht);
4. 完整源代码
hashtable.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "hashtable.h" static void resize_hash_table_if_needed(HashTable *ht); static int hash_str(char *key); int hash_init(HashTable *ht) { ht->size = HASH_TABLE_INIT_SIZE; ht->elem_num = 0; ht->buckets = (Bucket **)calloc(ht->size, sizeof(Bucket *)); if(ht->buckets == NULL) return FAILED; LOG_MSG("[init]\tsize: %i\n", ht->size); return SUCCESS; } int hash_lookup(HashTable *ht, char *key, void **result) { int index = HASH_INDEX(ht, key); Bucket *bucket = ht->buckets[index]; if(bucket == NULL) goto failed; while(bucket) { if(strcmp(bucket->key, key) == 0) { LOG_MSG("[lookup]\t found %s\tindex:%i value: %p\n", key, index, bucket->value); *result = bucket->value; return SUCCESS; } bucket = bucket->next; } failed: LOG_MSG("[lookup]\t key:%s\tfailed\t\n", key); return FAILED; } int hash_insert(HashTable *ht, char *key, void *value) { // check if we need to resize the hashtable resize_hash_table_if_needed(ht); int index = HASH_INDEX(ht, key); Bucket *org_bucket = ht->buckets[index]; Bucket *tmp_bucket = org_bucket; // check if the key exits already while(tmp_bucket) { if(strcmp(key, tmp_bucket->key) == 0) { LOG_MSG("[update]\tkey: %s\n", key); tmp_bucket->value = value; return SUCCESS; } tmp_bucket = tmp_bucket->next; } Bucket *bucket = (Bucket *)malloc(sizeof(Bucket)); bucket->key = key; bucket->value = value; bucket->next = NULL; ht->elem_num += 1; if(org_bucket != NULL) { LOG_MSG("[collision]\tindex:%d key:%s\n", index, key); bucket->next = org_bucket; } ht->buckets[index]= bucket; LOG_MSG("[insert]\tindex:%d key:%s\tht(num:%d)\n", index, key, ht->elem_num); return SUCCESS; } int hash_remove(HashTable *ht, char *key) { int index = HASH_INDEX(ht, key); Bucket *bucket = ht->buckets[index]; Bucket *prev = NULL; if(bucket == NULL) return FAILED; // find the right bucket from the link list while(bucket) { if(strcmp(bucket->key, key) == 0) { LOG_MSG("[remove]\tkey:(%s) index: %d\n", key, index); if(prev == NULL) { ht->buckets[index] = bucket->next; } else { prev->next = bucket->next; } free(bucket); return SUCCESS; } prev = bucket; bucket = bucket->next; } LOG_MSG("[remove]\t key:%s not found remove \tfailed\t\n", key); return FAILED; } int hash_destroy(HashTable *ht) { int i; Bucket *cur = NULL; Bucket *tmp = NULL; for(i=0; i < ht->size; ++i) { cur = ht->buckets[i]; while(cur) { tmp = cur; cur = cur->next; free(tmp); } } free(ht->buckets); return SUCCESS; } static int hash_str(char *key) { int hash = 0; char *cur = key; while(*cur != '\0') { hash += *cur; ++cur; } return hash; } static int hash_resize(HashTable *ht) { // double the size int org_size = ht->size; ht->size = ht->size * 2; ht->elem_num = 0; LOG_MSG("[resize]\torg size: %i\tnew size: %i\n", org_size, ht->size); Bucket **buckets = (Bucket **)calloc(ht->size, sizeof(Bucket **)); Bucket **org_buckets = ht->buckets; ht->buckets = buckets; int i = 0; for(i=0; i < org_size; ++i) { Bucket *cur = org_buckets[i]; Bucket *tmp; while(cur) { // rehash: insert again hash_insert(ht, cur->key, cur->value); // free the org bucket, but not the element tmp = cur; cur = cur->next; free(tmp); } } free(org_buckets); LOG_MSG("[resize] done\n"); return SUCCESS; } // if the elem_num is almost as large as the capacity of the hashtable // we need to resize the hashtable to contain enough elements static void resize_hash_table_if_needed(HashTable *ht) { if(ht->size - ht->elem_num < 1) { hash_resize(ht); } }
hashtable.h
#ifndef _HASH_TABLE_H_ #define _HASH_TABLE_H_ 1 #define HASH_TABLE_INIT_SIZE 6 #define HASH_INDEX(ht, key) (hash_str((key)) % (ht)->size) #if defined(DEBUG) # define LOG_MSG printf #else # define LOG_MSG(...) #endif #define SUCCESS 0 #define FAILED -1 typedef struct _Bucket { char *key; void *value; struct _Bucket *next; } Bucket; typedef struct _HashTable { int size; // 哈希表的大小 int elem_num; // 已经保存元素的个数 Bucket **buckets; } HashTable; int hash_init(HashTable *ht); int hash_lookup(HashTable *ht, char *key, void **result); int hash_insert(HashTable *ht, char *key, void *value); int hash_remove(HashTable *ht, char *key); int hash_destroy(HashTable *ht); #endif
main.c
#include <stdio.h> #include <stdlib.h> #include <assert.h> #include <string.h> #include "hashtable.h" #define TEST(tcase) printf(">>> [START CASE] " tcase "<<<\n") #define PASS(tcase) printf(">>> [PASSED] " tcase " <<<\n") int main(int argc, char **argv) { HashTable *ht = (HashTable *)malloc(sizeof(HashTable)); int result = hash_init(ht); assert(result == SUCCESS); /* Data */ int int1 = 10; int int2 = 20; char str1[] = "Hello TIPI"; char str2[] = "Value"; /* to find data container */ int *j = NULL; char *find_str = NULL; /* Test Key insert */ TEST("Key insert"); hash_insert(ht, "KeyInt", &int1); hash_insert(ht, "asdfKeyStrass", str1); hash_insert(ht, "K13eyStras", str1); hash_insert(ht, "KeyStr5", str1); hash_insert(ht, "KeyStr", str1); PASS("Key insert"); /* Test key lookup */ TEST("Key lookup"); hash_lookup(ht, "KeyInt", (void **)&j); hash_lookup(ht, "KeyStr", (void **)&find_str); assert(strcmp(find_str, str1) == 0); assert(*j = int1); PASS("Key lookup"); /* Test Key update */ TEST("Test key update"); hash_insert(ht, "KeyInt", &int2); hash_lookup(ht, "KeyInt", (void **)&j); assert(*j = int2); PASS("Test key update"); TEST(">>> Test key not found <<< "); result = hash_lookup(ht, "non-exits-key", (void **)&j); assert(result == FAILED); PASS("non-exist-key lookup"); TEST("Test key not found after remove"); char strMyKey[] = "My-Key-Value"; find_str = NULL; hash_insert(ht, "My-Key", &strMyKey); result = hash_remove(ht, "My-Key"); assert(result == SUCCESS); result = hash_lookup(ht, "My-Key", (void **)&find_str); assert(find_str == NULL); assert(result == FAILED); PASS("Test key not found after remove"); PASS(">>> Test key not found <<< "); TEST("Add many elements and make hashtable rehash"); hash_insert(ht, "a1", &int2); hash_insert(ht, "a2", &int1); hash_insert(ht, "a3", &int1); hash_insert(ht, "a4", &int1); hash_insert(ht, "a5", &int1); hash_insert(ht, "a6", &int1); hash_insert(ht, "a7", &int1); hash_insert(ht, "a8", str2); hash_insert(ht, "a9", &int1); hash_insert(ht, "a10", &int1); hash_insert(ht, "a11", &int1); hash_insert(ht, "a12", &int1); hash_insert(ht, "a13", &int1); hash_insert(ht, "a14", &int1); hash_insert(ht, "a15", &int1); hash_insert(ht, "a16", &int1); hash_insert(ht, "a17", &int1); hash_insert(ht, "a18", &int1); hash_insert(ht, "a19", &int1); hash_insert(ht, "a20", &int1); hash_insert(ht, "a21", &int1); hash_insert(ht, "a22", &int1); hash_insert(ht, "a23", &int1); hash_insert(ht, "a24", &int1); hash_insert(ht, "a24", &int1); hash_insert(ht, "a24", &int1); hash_insert(ht, "a25", &int1); hash_insert(ht, "a26", &int1); hash_insert(ht, "a27", &int1); hash_insert(ht, "a28", &int1); hash_insert(ht, "a29", &int1); hash_insert(ht, "a30", &int1); hash_insert(ht, "a31", &int1); hash_insert(ht, "a32", &int1); hash_insert(ht, "a33", &int1); hash_lookup(ht, "a23", (void **)&j); assert(*j = int1); hash_lookup(ht, "a30", (void **)&j); assert(*j = int1); PASS("Add many elements and make hashtable rehash"); hash_destroy(ht); free(ht); printf("Woohoo, It looks like HashTable works properly\n"); return 0; }
编译运行
gcc -g -Wall -DDEBUG -o a.out main.c hashtable.c
0x3: 数据结构
在PHP中所有的数据,变量、常量、类、属性、数组都用Hash表来实现
\php-5.6.17\Zend\zend_hash.h
typedef struct bucket { ulong h; /* Used for numeric indexing */ uint nKeyLength; //key长度 void *pData; //指向 Bucke保存的数据 指针 void *pDataPtr; //指针数据 struct bucket *pListNext; //下一个元素指针 struct bucket *pListLast; //上一个元素指针 struct bucket *pNext; struct bucket *pLast; const char *arKey; } Bucket; typedef struct _hashtable { uint nTableSize; //HashTable的大小 uint nTableMask; //等于nTableSize-1 uint nNumOfElements; //对象个数 ulong nNextFreeElement; //指向下一个空元素位置 nTableSize+1 Bucket *pInternalPointer; /* Used for element traversal 保存当前遍历的指针 */ Bucket *pListHead; //头元素指针 Bucket *pListTail; //尾元素指针 Bucket **arBuckets; //存储hash数组数据 dtor_func_t pDestructor; //类似于析构函数 zend_bool persistent; //用哪种方法分配内存空间 PHP统一管理内存还是用普通的malloc unsigned char nApplyCount; //当前hash bucket被访问的次数,是否遍历过数据,防止无限递归循环 zend_bool bApplyProtection; #if ZEND_DEBUG int inconsistent; #endif } H
Relevant Link:
http://www.imsiren.com/archives/6 http://www.php-internals.com/book/?p=chapt03/03-01-01-hashtable https://github.com/reeze/tipi/tree/master/book/sample/chapt03/03-01-01-hashtable
2. PHP数组定义
PHP中的数组实际上是一个有序映射。映射是一种把 values 关联到 keys 的类型。此类型在很多方面做了优化,因此可以把它当成
1. 真正的数组 2. 列表(向量) 3. 散列表(是映射的一种实现) 4. 字典 5. 集合 6. 栈 7. 队列以及更多可能性
数组元素的值也可以是另一个数组。树形结构和多维数组也是允许的,PHP中经常使用数组,使用数组最大的好处便是速度!读写都可以在O(1)内完成,因为它每个元素的大小都是一致的,只要知道下标,便可以瞬间计算出其对应的元素在内存中的位置,从而直接取出或者写入
PHP大部分功能,都是通过HashTable来实现,其中就包括数组
HashTable即具有双向链表的优点,PHP中的定义的变量保存在一个符号表里,而这个符号表其实就是一个HashTable,它的每一个元素都是一个zval*类型的变量。不仅如此,保存用户定义的函数、类、资源等的容器都是以HashTable的形式在内核中实现的
因此,PHP的数组读写都可以在O(1)内完成,这是非常高效的,因此开销和C++、Java相比也就是hashtable的创建了,我们看一下PHP定义数组
<?php $array = array(); $array["key"] = "values"; ?>
在内核中使用宏来实现
0x1: 数组初始化
Zend/zend_vm_execute.h
static int ZEND_FASTCALL ZEND_INIT_ARRAY_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
array_init(&EX_T(opline->result.var).tmp_var);
if (IS_CV == IS_UNUSED) {
ZEND_VM_NEXT_OPCODE();
#if 0 || IS_CV != IS_UNUSED
} else {
return ZEND_ADD_ARRAY_ELEMENT_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);
#endif
}
}
\php-5.6.17\Zend\zend_API.c
ZEND_API int _array_init(zval *arg, uint size ZEND_FILE_LINE_DC) /* {{{ */
{
ALLOC_HASHTABLE_REL(Z_ARRVAL_P(arg));
_zend_hash_init(Z_ARRVAL_P(arg), size, ZVAL_PTR_DTOR, 0 ZEND_FILE_LINE_RELAY_CC);
Z_TYPE_P(arg) = IS_ARRAY;
return SUCCESS;
}
\php-5.6.17\Zend\zend_hash.c
ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC)
{
uint i = 3;
SET_INCONSISTENT(HT_OK);
if (nSize >= 0x80000000) {
/* prevent overflow */
//HASH表大小大于0x80000000则初始化为0x80000000
ht->nTableSize = 0x80000000;
} else {
while ((1U << i) < nSize) {
i++;
}
//申请的数组Size空间调整为2的n次方,这样有利于内存对齐,i=3,nTableSize最小值为8
ht->nTableSize = 1 << i;
}
ht->nTableMask = 0; /* 0 means that ht->arBuckets is uninitialized */
ht->pDestructor = pDestructor; //一个函数指针,当HashTable发生增,删,改时调用
ht->arBuckets = (Bucket**)&uninitialized_bucket;
ht->pListHead = NULL;
ht->pListTail = NULL;
ht->nNumOfElements = 0;
ht->nNextFreeElement = 0;
ht->pInternalPointer = NULL;
ht->persistent = persistent; //如果persisient为TRUE,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数
ht->nApplyCount = 0;
ht->bApplyProtection = 1;
return SUCCESS;
}
ZEND_API int _zend_hash_init_ex(HashTable *ht, uint nSize, dtor_func_t pDestructor, zend_bool persistent, zend_bool bApplyProtection ZEND_FILE_LINE_DC)
{
int retval = _zend_hash_init(ht, nSize, pDestructor, persistent ZEND_FILE_LINE_CC);
ht->bApplyProtection = bApplyProtection;
return retval;
}
0x2: 数组添加键值
以三种典型的键值插入/取值操作为例
<?php $patterns = array(); $patterns[0] = '!quick!e'; $patterns['1'] = '{brown}'; $patterns['string'] = 'littlehann'; echo $patterns[0]; echo $patterns'1']; echo $patterns['string']; foreach($patterns as $Item){ echo $Item; }
1. 内核对PHP添加数字索引的处理方式
对int型数字键的操作来说,在PHP中不管是对数组的添加操作(zend_hash_add),还是对数组的更新操作(zend_hash_update),其最终都是调用_zend_hash_add_or_update函数完成
\php-5.6.17\Zend\zend_hash.c
ZEND_API int _zend_hash_index_update_or_next_insert(HashTable *ht, ulong h, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC) { uint nIndex; Bucket *p; #ifdef ZEND_SIGNALS TSRMLS_FETCH(); #endif IS_CONSISTENT(ht); CHECK_INIT(ht); if (flag & HASH_NEXT_INSERT) { h = ht->nNextFreeElement; } //生成hash值,通过与nTableMask执行与操作,获取在arBuckets数组中的Bucket位置 nIndex = h & ht->nTableMask; p = ht->arBuckets[nIndex]; //如果Bucket中已经存在元素,则遍历整个Bucket链表(HASH很可能冲突),查找是否存在相同的key值元素,如果有并且是update调用,则执行update数据操作 while (p != NULL) { if ((p->nKeyLength == 0) && (p->h == h)) { if (flag & HASH_NEXT_INSERT || flag & HASH_ADD) { return FAILURE; } ZEND_ASSERT(p->pData != pData); HANDLE_BLOCK_INTERRUPTIONS(); if (ht->pDestructor) { ht->pDestructor(p->pData); } UPDATE_DATA(ht, p, pData, nDataSize); HANDLE_UNBLOCK_INTERRUPTIONS(); if (pDest) { *pDest = p->pData; } return SUCCESS; } p = p->pNext; } //当前数组中没有该键,创建新的Bucket元素,初始化数据 p = (Bucket *) pemalloc_rel(sizeof(Bucket), ht->persistent); p->arKey = NULL; p->nKeyLength = 0; /* Numeric indices are marked by making the nKeyLength == 0 */ p->h = h; INIT_DATA(ht, p, pData, nDataSize); if (pDest) { *pDest = p->pData; } //将新元素添加到当前hash值对应的Bucket链表的最前面(CONNECT_TO_BUCKET_DLLIST),即放在第一个的位置 CONNECT_TO_BUCKET_DLLIST(p, ht->arBuckets[nIndex]); HANDLE_BLOCK_INTERRUPTIONS(); ht->arBuckets[nIndex] = p; //将新的Bucket元素添加到数组的链接表的最后面(CONNECT_TO_GLOBAL_DLLIST),即队列的FIFO CONNECT_TO_GLOBAL_DLLIST(p, ht); HANDLE_UNBLOCK_INTERRUPTIONS(); //PHP中可以不指定索引值向数组中添加元素,这时将默认使用数字作为索引,和C语言中的枚举类似 //当不指定索引时,新插入的元素的索引由nNextFreeElement字段决定了 //如果数组中存在了数字key,则会默认使用最新使用的key + 1,例如上例中已经存在了10作为key的元素,这样新插入的默认索引就为11 if ((long)h >= (long)ht->nNextFreeElement) { ht->nNextFreeElement = h < LONG_MAX ? h + 1 : LONG_MAX; } ht->nNumOfElements++; //当HASH表到达当前上限时(2次方对齐的上限),进行扩容,并且将所有元素键重新进行哈希并进行索引映射 ZEND_HASH_IF_FULL_DO_RESIZE(ht); return SUCCESS; }
2. 内核对PHP中字符串索引的处理方式
与数字索引相比,多了一步将字符串转换为整型。用到的算法是time33,即对字符串的每个字符转换为ASCII码乘上33并且相加得到的结果
\php-5.6.17\Zend\zend_hash.c
ZEND_API int _zend_hash_add_or_update(HashTable *ht, const char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC) { ulong h; uint nIndex; Bucket *p; #ifdef ZEND_SIGNALS TSRMLS_FETCH(); #endif IS_CONSISTENT(ht); ZEND_ASSERT(nKeyLength != 0); CHECK_INIT(ht); //调用zend_inline_hash_func将字符串key,尝试转化为int值 //后续的代码逻辑和int型的array操作一致 h = zend_inline_hash_func(arKey, nKeyLength); nIndex = h & ht->nTableMask;
\php-5.6.17\Zend\zend_hash.h
static inline ulong zend_inline_hash_func(const char *arKey, uint nKeyLength) { register ulong hash = 5381; /* variant with the hash unrolled eight times */ for (; nKeyLength >= 8; nKeyLength -= 8) { hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; hash = ((hash << 5) + hash) + *arKey++; } switch (nKeyLength) { case 7: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ case 6: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ case 5: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ case 4: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ case 3: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ case 2: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */ case 1: hash = ((hash << 5) + hash) + *arKey++; break; case 0: break; EMPTY_SWITCH_DEFAULT_CASE() } return hash; }
Relevant Link:
http://www.kancloud.cn/kancloud/php-internals/42760 http://blog.csdn.net/a600423444/article/details/8850617 http://www.imsiren.com/archives/250
0x3: 操作PHP数组的API
//初始化PHP数组 array_init(zval *arg); array_init_size(zval *arg, uint size); //关联数组赋值的操作函数,等同于$array[$stringKey] = $value; add_assoc_null(zval *aval, char *key); add_assoc_bool(zval *aval, char *key, zend_bool bval); add_assoc_long(zval *aval, char *key, long lval); add_assoc_double(zval *aval, char *key, double dval); add_assoc_string(zval *aval, char *key, char *strval, int dup); add_assoc_stringl(zval *aval, char *key,char *strval, uint strlen, int dup); add_assoc_zval(zval *aval, char *key, zval *value); //上述函数均为宏函数,都是对add_assoc_*_ex函数的封装 //数字索引数组赋值的操作函数,等效于$array[$numKey] = $value; ZEND_API int add_index_long(zval *arg, ulong idx, long n); ZEND_API int add_index_null(zval *arg, ulong idx); ZEND_API int add_index_bool(zval *arg, ulong idx, int b); ZEND_API int add_index_resource(zval *arg, ulong idx, int r); ZEND_API int add_index_double(zval *arg, ulong idx, double d); ZEND_API int add_index_string(zval *arg, ulong idx, const char *str, int duplicate); ZEND_API int add_index_stringl(zval *arg, ulong idx, const char *str, uint length, int duplicate); ZEND_API int add_index_zval(zval *arg, ulong index, zval *value); //使用内置数字索引的数组赋值的操作函数,等效于$array[] = $value; ZEND_API int add_next_index_long(zval *arg, long n); ZEND_API int add_next_index_null(zval *arg); ZEND_API int add_next_index_bool(zval *arg, int b); ZEND_API int add_next_index_resource(zval *arg, int r); ZEND_API int add_next_index_double(zval *arg, double d); ZEND_API int add_next_index_string(zval *arg, const char *str, int duplicate); ZEND_API int add_next_index_stringl(zval *arg, const char *str, uint length, int duplicate); ZEND_API int add_next_index_zval(zval *arg, zval *value); //数组元素赋值并返回,等效于{ $array[$key] = $value; return $value; } ZEND_API int add_get_assoc_string_ex(zval *arg, const char *key, uint key_len, const char *str, void **dest, int duplicate); ZEND_API int add_get_assoc_stringl_ex(zval *arg, const char *key, uint key_len, const char *str, uint length, void **dest, int duplicate); #define add_get_assoc_string(__arg, __key, __str, __dest, __duplicate) add_get_assoc_string_ex(__arg, __key, strlen(__key)+1, __str, __dest, __duplicate) #define add_get_assoc_stringl(__arg, __key, __str, __length, __dest, __duplicate) add_get_assoc_stringl_ex(__arg, __key, strlen(__key)+1, __str, __length, __dest, __duplicate) ZEND_API int add_get_index_long(zval *arg, ulong idx, long l, void **dest); ZEND_API int add_get_index_double(zval *arg, ulong idx, double d, void **dest); ZEND_API int add_get_index_string(zval *arg, ulong idx, const char *str, void **dest, int duplicate); ZEND_API int add_get_index_stringl(zval *arg, ulong idx, const char *str, uint length, void **dest, int duplicate);
0x4: 数组的foreach遍历
在词法分析阶段,foreach会被识别为一个TOKEN:T_FOREACH
\php-5.6.17\Zend\zend_language_parser.y
T_FOREACH '(' variable T_AS { zend_do_foreach_begin(&$1, &$2, &$3, &$4, 1 TSRMLS_CC); } foreach_variable foreach_optional_arg ')' { zend_do_foreach_cont(&$1, &$2, &$4, &$6, &$7 TSRMLS_CC); } foreach_statement { zend_do_foreach_end(&$1, &$4 TSRMLS_CC); } | T_FOREACH '(' expr_without_variable T_AS { zend_do_foreach_begin(&$1, &$2, &$3, &$4, 0 TSRMLS_CC); } foreach_variable foreach_optional_arg ')' { zend_do_foreach_cont(&$1, &$2, &$4, &$6, &$7 TSRMLS_CC); } foreach_statement { zend_do_foreach_end(&$1, &$4 TSRMLS_CC); }
实现foreach的核心就是如下3个函数
zend_do_foreach_begin
zend_do_foreach_cont
zend_do_foreach_end
1. zend_do_foreach_begin
\php-5.6.17\Zend\zend_compile.c
void zend_do_foreach_begin(znode *foreach_token, znode *open_brackets_token, znode *array, znode *as_token, int variable TSRMLS_DC) /* {{{ */ { zend_op *opline; zend_bool is_variable; zend_op dummy_opline; if (variable) { if (zend_is_function_or_method_call(array)) { is_variable = 0; } else { is_variable = 1; } /* save the location of FETCH_W instruction(s) */ open_brackets_token->u.op.opline_num = get_next_op_number(CG(active_op_array)); zend_do_end_variable_parse(array, BP_VAR_W, 0 TSRMLS_CC); if (zend_is_function_or_method_call(array)) { opline = get_next_op(CG(active_op_array) TSRMLS_CC); opline->opcode = ZEND_SEPARATE; SET_NODE(opline->op1, array); SET_UNUSED(opline->op2); opline->result_type = IS_VAR; opline->result.var = opline->op1.var; } } else { is_variable = 0; open_brackets_token->u.op.opline_num = get_next_op_number(CG(active_op_array)); } /* save the location of FE_RESET */ //记录当前的opline行数(为以后跳转而记录)1 foreach_token->u.op.opline_num = get_next_op_number(CG(active_op_array)); opline = get_next_op(CG(active_op_array) TSRMLS_CC); /* Preform array reset */ //对数组进行RESET(将内部指针指向第一个元素) opline->opcode = ZEND_FE_RESET; opline->result_type = IS_VAR; //获取临时变量($val) opline->result.var = get_temporary_variable(CG(active_op_array)); SET_NODE(opline->op1, array); SET_UNUSED(opline->op2); opline->extended_value = is_variable ? ZEND_FE_RESET_VARIABLE : 0; COPY_NODE(dummy_opline.result, opline->result); zend_stack_push(&CG(foreach_copy_stack), (void *) &dummy_opline, sizeof(zend_op)); /* save the location of FE_FETCH */ as_token->u.op.opline_num = get_next_op_number(CG(active_op_array)); //设置获取变量的OPCODE FE_FETCH,结果存第3步的临时变量 opline = get_next_op(CG(active_op_array) TSRMLS_CC); opline->opcode = ZEND_FE_FETCH; opline->result_type = IS_VAR; opline->result.var = get_temporary_variable(CG(active_op_array)); COPY_NODE(opline->op1, dummy_opline.result); opline->extended_value = 0; SET_UNUSED(opline->op2); //记录获取变量的OPCODES的行数 opline = get_next_op(CG(active_op_array) TSRMLS_CC); opline->opcode = ZEND_OP_DATA; SET_UNUSED(opline->op1); SET_UNUSED(opline->op2); SET_UNUSED(opline->result); } /* }}} */
2. zend_do_foreach_cont
void zend_do_foreach_cont(znode *foreach_token, const znode *open_brackets_token, const znode *as_token, znode *value, znode *key TSRMLS_DC) /* {{{ */ { zend_op *opline; znode dummy, value_node; zend_bool assign_by_ref=0; opline = &CG(active_op_array)->opcodes[as_token->u.op.opline_num]; if (key->op_type != IS_UNUSED) { znode *tmp; /* switch between the key and value... */ tmp = key; key = value; value = tmp; /* Mark extended_value in case both key and value are being used */ opline->extended_value |= ZEND_FE_FETCH_WITH_KEY; } if ((key->op_type != IS_UNUSED)) { if (key->EA & ZEND_PARSED_REFERENCE_VARIABLE) { zend_error_noreturn(E_COMPILE_ERROR, "Key element cannot be a reference"); } if (key->EA & ZEND_PARSED_LIST_EXPR) { zend_error_noreturn(E_COMPILE_ERROR, "Cannot use list as key element"); } } //根据foreach_variable的u.EA.type来判断是否引用 if (value->EA & ZEND_PARSED_REFERENCE_VARIABLE) { assign_by_ref = 1; /* Mark extended_value for assign-by-reference */ opline->extended_value |= ZEND_FE_FETCH_BYREF; CG(active_op_array)->opcodes[foreach_token->u.op.opline_num].extended_value |= ZEND_FE_RESET_REFERENCE; } else { //根据是否引用来调整zend_do_foreach_begin中生成的FE_FETCH方式 zend_op *fetch = &CG(active_op_array)->opcodes[foreach_token->u.op.opline_num]; zend_op *end = &CG(active_op_array)->opcodes[open_brackets_token->u.op.opline_num]; /* Change "write context" into "read context" */ fetch->extended_value = 0; /* reset ZEND_FE_RESET_VARIABLE */ while (fetch != end) { --fetch; if (fetch->opcode == ZEND_FETCH_DIM_W && fetch->op2_type == IS_UNUSED) { zend_error_noreturn(E_COMPILE_ERROR, "Cannot use [] for reading"); } if (fetch->opcode == ZEND_SEPARATE) { MAKE_NOP(fetch); } else { fetch->opcode -= 3; /* FETCH_W -> FETCH_R */ } } } //根据zend_do_foreach_begin中记录的取变量的OPCODES的行数 GET_NODE(&value_node, opline->result); //获取HashTable的头指针 if (value->EA & ZEND_PARSED_LIST_EXPR) { if (!CG(list_llist).head) { zend_error_noreturn(E_COMPILE_ERROR, "Cannot use empty list"); } //遍历HashTable双链表 zend_do_list_end(&dummy, &value_node TSRMLS_CC); zend_do_free(&dummy TSRMLS_CC); } else { if (assign_by_ref) { zend_do_end_variable_parse(value, BP_VAR_W, 0 TSRMLS_CC); /* Mark FE_FETCH as IS_VAR as it holds the data directly as a value */ zend_do_assign_ref(NULL, value, &value_node TSRMLS_CC); } else { zend_do_assign(&dummy, value, &value_node TSRMLS_CC); zend_do_free(&dummy TSRMLS_CC); } } if (key->op_type != IS_UNUSED) { znode key_node; opline = &CG(active_op_array)->opcodes[as_token->u.op.opline_num+1]; opline->result_type = IS_TMP_VAR; opline->result.opline_num = get_temporary_variable(CG(active_op_array)); GET_NODE(&key_node, opline->result); zend_do_assign(&dummy, key, &key_node TSRMLS_CC); zend_do_free(&dummy TSRMLS_CC); } //循环的主要逻辑 do_begin_loop(TSRMLS_C); INC_BPC(CG(active_op_array)); } /* }}} */
3. zend_do_foreach_end
void zend_do_foreach_end(const znode *foreach_token, const znode *as_token TSRMLS_DC) /* {{{ */ { zend_op *container_ptr; zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC); //根据zend_do_foreach_begin中记录的行数信息,设置ZEND_JMP OPCODES,准备跳回foreach的开始处继续循环 opline->opcode = ZEND_JMP; //根据当前行数,设置循环体下一条opline, 用以跳出循环 opline->op1.opline_num = as_token->u.op.opline_num; SET_UNUSED(opline->op1); SET_UNUSED(opline->op2); CG(active_op_array)->opcodes[foreach_token->u.op.opline_num].op2.opline_num = get_next_op_number(CG(active_op_array)); /* FE_RESET */ CG(active_op_array)->opcodes[as_token->u.op.opline_num].op2.opline_num = get_next_op_number(CG(active_op_array)); /* FE_FETCH */ // 结束循环(处理循环内循环:do_end_loop) do_end_loop(as_token->u.op.opline_num, 1 TSRMLS_CC); //清理临时变量 zend_stack_top(&CG(foreach_copy_stack), (void **) &container_ptr); generate_free_foreach_copy(container_ptr TSRMLS_CC); zend_stack_del_top(&CG(foreach_copy_stack)); DEC_BPC(CG(active_op_array)); } /* }}} */
除此之外,在zend_do_foreach_cont 和 zend_do_foreach_end之间 会在语法分析阶段被填充foreach_satement的语句代码,基本上这就是foreach的git执行过程
PHP的HashTable(包括数组)是一种链表队列(HashTable结构体中的pListHead和pListTail维护整个哈希表的头元素指针和最后一个元素的指针),元素按照FIFO的顺序进行排列,所以foreach遍历的顺序和入队的顺序有关,同时每个队列元素又是一个Bucket双向链表(相同Hash的Bucket以拉链法链接在同一个链表上,pNext和pLast指针分别指向本槽位所在的链表的关系),每个Bucket中存储了对应的Hash索引值,这个Hash索引值将数组的键直接映射到对应的内存位置,故为0(1)时间复杂度
<?php $arr[2] = 'littlehann'; $arr[1] = 2007; $arr[0] = 2008; foreach ($arr as $key => $val) { var_dump($val); }
使用foreach并不是按照ASCII顺序进行遍历的,而是按照FIFO顺序,即遍历HashTable队列,所以如果希望按照下标ASC/DEC顺序遍历,最好用for($i=0; $i < count; $i++);;这种结构,明确告诉PHP按照指定下标进行HASH索引取值
Relevant Link:
http://thiniki.sinaapp.com/?p=155 http://www.imsiren.com/archives/250 http://www.cnblogs.com/ohmygirl/p/internal-4.html http://weizhifeng.net/write-php-extension-part2-1.html http://blog.csdn.net/a600423444/article/details/7073854 http://www.laruence.com/2008/11/20/630.html http://www.laruence.com/2009/08/23/1065.html
3. PHP变量实现
0x1: 变量概述
所有的编程语言都要提供一种数据的存储与检索机制,PHP也不例外。其它语言大都需要在使用变量之前先定义,并且它的类型也是无法再次改变的,而PHP却允许程序猿自由的使用变量而无须提前定义,甚至可以随时随意的对已存在的变量转换成其它任何PHP支持的数据类型。在程序在运行的时候,PHP还会自动的根据需求转换变量的类型
从一般编程语言的角度来看,变量具有三个基本特性
1. 名称: 变量的标示符 1) 变量命名上,PHP继承了Perl的语法风格,变量以美元符号开始,后面跟变量名。一个有效的变量名由字母或者下划线开头,后面跟上任意数量的字母,数字,或者下划线 2) PHP同时还支持复合变量,也就是类似$$a的变量,它会进行两次的解释。这给PHP带来了非常灵活的动态特性 3) PHP中组成变量名的字母可以是英文字母 a-z,A-Z 4) 变量名还可以是 ASCII 字符从 127 到 255(0x7f-0xff),这给一些恶意脚本进行代码加密提供的语言基础 5) 变量名是区分大小写的 2. 类型: 变量的类型 1) 在很多静态语言中,变量在定义时就指定了,在程序运行过程中都不允许进行变更 2) 但是PHP属于弱类型语言,可以随便赋予它任何类型的值 3. 值内容 1) 标示所代表的具体内容
0x2: 变量的类型
前言中提到变量的三个基本特性,其中的有一个特性为变量的类型,变量都有特定的类型, 如:字符串、数组、对象等等。编程语言的类型系统可以分为强类型和弱类型两种
1. 强类型语言是一旦某个变量被申明为某个类型的变量,则在程序运行过程中,该不能将该变量的类型以外的值赋予给它(当然并不完全如此,这可能会涉及到类型的转换),C/C++/Java等语言就属于这类 2. PHP及Ruby,JavaScript等脚本语言属于弱类型语言: 一个变量可以表示任意的数据类型。PHP之所以成为一个简单而强大的语言,很大一部分的原因是它拥有弱类型的变量。 但是有些时候这也是一把双刃剑,使用不当也会带来一些问题。就像仪器一样,越是功能强大, 出现错误的可能性也就越大
PHP在内核中是通过zval这个结构体来存储变量的,它的定义在Zend/zend.h文件里,这就是PHP弱类型的核心
//在Zend/zend.h文件里 struct _zval_struct { /* Variable information */ zvalue_value value; /* value 变量的值 */ zend_uint refcount__gc; //表示引用计数 zend_uchar type; /* active type 变量当前的数据类型 */ zend_uchar is_ref__gc; //表示是否为引用 }; typedef struct _zval_struct zval; //在Zend/zend_types.h里定义的: typedef unsigned int zend_uint; typedef unsigned char zend_uchar;
保存变量值的value则是zvalue_value类型,它是一个union,这里使用联合体而不是用结构体是出于空间利用率的考虑,因为一个变量同时只能属于一种类型。 如果使用结构体的话将会不必要的浪费空间,而PHP中的所有逻辑都围绕变量来进行的,这样的话, 内存浪费将是十分大的。这种做法成本小但收益非常大
Zend/zend.h
typedef union _zvalue_value { long lval; /* long value */ double dval; /* double value */ struct { char *val; int len; } str; HashTable *ht; /* hash table value */ zend_object_value obj; zend_ast *ast; } zvalue_value;
在以上实现的基础上,PHP语言得以实现了8种数据类型,这些数据类型在内核中的分别对应于特定的常量,它们分别是
1. IS_NULL 1) 第一次使用的变量如果没有初始化过,则会自动的被赋予这个常量,当然我们也可以在PHP语言中通过null这个常量来给予变量null类型的值 2) 这个类型的值只有一个,就是NULL,它和0与false是不同的 2. IS_BOOL 1) 布尔类型的变量有两个值,true或者false。在PHP语言中 2) while、if等语句会自动的把表达式的值转成这个类型的 3. IS_LONG 1) PHP语言中的整型,在内核中是通过所在操作系统的signed long数据类型来表示的。 在最常见的32位操作系统中,它可以存储从-2147483648 ~ +2147483647范围内的任一整数,因为使用了signed long来作为载体,所以这也就解释了为什么PHP语言中的整型数据都是带符号的了 2) 如果PHP语言中的整型变量超出最大值或者最小值,它并不会直接溢出, 而是会被内核转换成IS_DOUBLE类型的值然后再参与计算。 再者,。 ```c $a=2147483647; $a++; echo $a;//会正确的输出 2147483648 4. IS_DOUBLE 1) PHP中的浮点数据是通过C语言中的signed double型变量来存储的,这最终取决与所在操作系统的浮点型实现,计算机是无法精准的表示浮点数的,而是采用了科学计数法来保存某个精度的浮点数。 用科学计数法,计算机只用8位便可以保存2.225x10^(-308) ~ 1.798x10^308之间的浮点数 2) 用计算机来处理浮点数会存在问题,十进制的0.5转成二进制是0.1, 0.8转换后是0.1100110011....。 但是当我们从二进制转换回来的时候,往往会发现并不能得到0.8。 我们用1除以3这个例子来解释这个现象:1/3=0.3333333333.....,它是一个无限循环小数, 但是计算机可能只能精确存储到0.333333,当我们再乘以三时, 其实计算机计算的数是0.333333*3=0.999999,而不是我们平时数学中所期盼的1.0. 5. IS_STRING 1) PHP中最常用的数据类型: 字符串,在内存中的存储和C差不多, 就是一块能够放下这个变量所有字符的内存,并且在这个变量的zval实现里会保存着指向这块内存的指针 2) 与C不同的是,PHP内核还同时在zval结构里保存着这个字符串的实际长度, 这个设计使PHP可以在字符串中嵌入"\0"字符,也使PHP的字符串是二进制安全的,可以安全的存储二进制数据 3) 内核只会为字符串申请它长度+1的内存, 最后一个字节存储的是'\0'字符,所以在不需要二进制安全操作的时候, 我们可以像通常C语言的方式那样来使用它 6. IS_ARRAY 1) 数组是一个非常特殊的数据类型,它唯一的功能就是聚集别的变量 2) 在C语言中,一个数组只能承载一种类型的数据,而PHP语言中的数组则灵活的多,它可以承载任意类型的数据,这一切都是HashTable的功劳,每个HashTable中的元素都有两部分组成: 索引与值,每个元素的值都是一个独立的zval(确切的说应该是指向某个zval的指针),正是因为这种架构设计,使得PHP可以在数组中以任意方式存储任意类型的变量 7. IS_OBJECT 1) 和数组一样,对象也是用来存储复合数据的,但是与数组不同的是,对象还需要保存以下信息: 方法、访问权限、类常量以及其它的处理逻辑 2) 相对与zend engine V1,V2中的对象实现已经被彻底修改,所以我们PHP扩展开发者如果需要自己的扩展支持面向对象的工作方式, 则应该对PHP5和PHP4分别对待 8. IS_RESOURCE 1) 有一些数据的内容可能无法直接呈现给PHP用户的,比如与某台mysql服务器的链接,或者直接呈现出来也没有什么意义 2) 但用户还需要这类数据,因此PHP中提供了一种名为Resource(资源)的数据类型
zval结构体里的type成员的值便是以上某个IS_*常量之一。内核通过检测变量的这个成员值来知道他是什么类型的数据并做相应的后续处理
zend头文件中定义了大量的宏,供我们检测、操作变量使用,使用这些宏不但让我们的程序更易读,还具有更好的兼容性,例如检测变量类型的宏Z_TYPE_P、Z_TYPE、Z_TYPE_PP
我们结合PHP API函数gettype的内核实现
\php-5.6.17\ext\standard\type.c
/* {{{ proto string gettype(mixed var) Returns the type of the variable */ PHP_FUNCTION(gettype) { ////arg间接指向调用gettype函数时所传递的参数。是一个zval**结构 zval **arg; ////这个if的操作主要是让arg指向参数 if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "Z", &arg) == FAILURE) { return; } //调用Z_TYPE_PP宏来获取arg指向zval的类型。 //然后是一个switch结构,RETVAL_STRING宏代表这gettype函数返回的字符串类型的值 switch (Z_TYPE_PP(arg)) { case IS_NULL: RETVAL_STRING("NULL", 1); break; case IS_BOOL: RETVAL_STRING("boolean", 1); break; case IS_LONG: RETVAL_STRING("integer", 1); break; case IS_DOUBLE: RETVAL_STRING("double", 1); break; case IS_STRING: RETVAL_STRING("string", 1); break; case IS_ARRAY: RETVAL_STRING("array", 1); break; case IS_OBJECT: RETVAL_STRING("object", 1); /* { char *result; int res_len; res_len = sizeof("object of type ")-1 + Z_OBJCE_P(arg)->name_length; spprintf(&result, 0, "object of type %s", Z_OBJCE_P(arg)->name); RETVAL_STRINGL(result, res_len, 0); } */ break; case IS_RESOURCE: { const char *type_name = zend_rsrc_list_get_rsrc_type(Z_LVAL_PP(arg) TSRMLS_CC); if (type_name) { RETVAL_STRING("resource", 1); break; } } default: RETVAL_STRING("unknown type", 1); } } /* }}} */
以上三个宏的定义在Zend/zend_operators.h里,定义分别是
#define Z_TYPE(zval) (zval).type #define Z_TYPE_P(zval_p) Z_TYPE(*zval_p) #define Z_TYPE_PP(zval_pp) Z_TYPE(**zval_pp)
0x3: 变量的值
PHP是一种若类型语言,在对变量进行读写前需要先判断变量的类型,PHP内核提供了三个基础宏来方便我们对变量的值进行操作,这几个宏同样以Z_开头,并且P结尾和PP结尾的同上一节中的宏一样,分别代表这参数是指针还是指针的指针。此外,为了进一步方便我们的工作,内核中针对具体的数据类型分别定义了相应的宏。如针对IS_BOOL型的BVAL组合(Z_BVAL、Z_BVAL_P、Z_BVAL_PP)和针对IS_DOUBLE的DVAL组合(Z_DVAL、ZDVAL_P、ZDVAL_PP)等等。我们通过下面这个例子来应用一下这几个宏
void display_value(zval zv,zval *zv_p,zval **zv_pp) { if( Z_TYPE(zv) == IS_NULL ) { php_printf("类型是 IS_NULL!\n"); } if( Z_TYPE_P(zv_p) == IS_LONG ) { php_printf("类型是 IS_LONG,值是:%ld" , Z_LVAL_P(zv_p)); } if(Z_TYPE_PP(zv_pp) == IS_DOUBLE ) { php_printf("类型是 IS_DOUBLE,值是:%f" , Z_DVAL_PP(zv_pp) ); } }
0x4: 创建PHP变量
PHP对内核变亮表示ZVAL的申请、初始化、赋值操作等都封装了宏实现,实现了代码整洁性和升级兼容性
1. MAKE_STD_ZVAL(pzv): 这个宏会用内核的方式来申请一块内存并将其地址付给pzv,并初始化它的refcount和is_ref两个属性,它不但会自动的处理内存不足问题,还会在内存中选个最优的位置来申请 2. ALLOC_INIT_ZVAL(): 这个宏函数也是用来申请内存的, 和MAKE_STD_ZVAL宏唯一的不同便是它会将pzv所指的zval的类型设置为IS_NULL; 3. ZVAL_NULL(pvz); 4. ZVAL_BOOL(pzv, b); 5. ZVAL_TRUE(pzv); 6. ZVAL_FALSE(pzv); 7. ZVAL_LONG(pzv, l); 8. ZVAL_DOUBLE(pzv, d); 9. ZVAL_STRINGL(pzv,str,len,dup); dup参数的意思其实很简单,它指明了该字符串是否需要被复制 1) 值为 1 将先申请一块新内存并赋值该字符串,然后把新内存的地址复制给pzv 2) 值为 0 时则是直接把str的地址赋值给zval 10. ZVAL_STRING(pzv, str, dup); ZVAL_STRING相比于ZVAL_STRINGL的区别在于ZVAL_STRING使用strlen()计算字符串长度,它不是二进制安全的,二进制安全的做法是显式使用len字段标明该字符串期望的处理长度 11. ZVAL_RESOURCE(pzv, res); PHP中的资源类型的值其实就是一个整数,所以ZVAL_RESOURCE和ZVAL_LONG的工作差不多,只不过它会把zval的类型设置为 IS_RESOURCE
0x5: 变量的存储方式
我们接下来学习一下内核是怎样来组织用户在PHP中定义的变量的。用户在PHP中定义的变量我们都可以在一个HashTable中找到,当PHP中定义了一个变量,内核会自动的把它的信息储存到一个用HashTable实现的符号表里。全局作用域的符号表是在调用扩展的RINIT方法(一般都是MINIT方法里)前创建的,并在RSHUTDOWN方法执行后自动销毁(这就是我们熟悉的$GLOBALS超全局变量)
当用户在PHP中调用一个函数或者类的方法时,内核会创建一个新的符号表并激活之,这也就是为什么我们无法在函数中使用在函数外定义的变量的原因(因为它们分属两个符号表,一个当前作用域的,一个全局作用域的),如果一定要在函数中使用全局变量,则需要使用global显示声明
Zend/zend_globals.h
struct _zend_executor_globals { ... //symbol_table元素可以通过EG宏来访问,它代表着PHP的全局变量,如$GLOBALS,其实从根本上来讲,$GLOBALS是EG(symbol_table)的一层封装 HashTable symbol_table; //active_symbol_table元素也可以通过EG(active_symbol_table)的方法来访问,它代表的是处于当前作用域的变量符号表 HashTable *active_symbol_table; ... };
用一段例子来解释下上面说的理论
<?php $foo = 'bar'; ?>
上面是一段PHP语言的例子,我们创建了一个变量,并把它的值设置为'bar',在以后的代码中我们便可以使用$foo变量,在PHP内核中的实现框架如下
1. 创建一个zval结构,并设置其类型 2. 设置值为'bar' 3. 将其加入当前作用域的符号表,只有这样用户才能在PHP里使用这个变量
具体的代码为
{ //声明一个zval指针,并申请一块内存 zval *fooval; MAKE_STD_ZVAL(fooval); //通过ZVAL_STRING宏将值设置为'bar' ZVAL_STRING(fooval, "bar", 1); //将这个zval加入到当前的符号表里去,并将其label定义成foo,这样用户就可以在代码里通过$foo来使用它了 ZEND_SET_SYMBOL( EG(active_symbol_table) , "foo" , fooval); }
0x6: 变量的检索
用户在PHP语言里定义的变量,在PHP内核中可以通过zend_hash_find()函数来找到当前某个作用域下用户已经定义好的变量
/* Returns SUCCESS if found and FAILURE if not. The pointer to the * data is returned in pData. The reason is that there's no reason * someone using the hash table might not want to have NULL data */ ZEND_API int zend_hash_find(const HashTable *ht, const char *arKey, uint nKeyLength, void **pData) { ulong h; uint nIndex; Bucket *p; IS_CONSISTENT(ht); h = zend_inline_hash_func(arKey, nKeyLength); nIndex = h & ht->nTableMask; p = ht->arBuckets[nIndex]; while (p != NULL) { if (p->arKey == arKey || ((p->h == h) && (p->nKeyLength == nKeyLength) && !memcmp(p->arKey, arKey, nKeyLength))) { *pData = p->pData; return SUCCESS; } p = p->pNext; } return FAILURE; }
需要明白的是,PHP使用HashTable来保存、读写变量,但是内核定义HashTable这个结构,并不是单单用来储存PHP语言里的变量的,PHP内核里很多地方都在使用HashTable
0x7: 类型转换
在纯语法层面,PHP是一种若类型语言,可以在运行时改变当前变量的数据类型
内核中提供了好多函数专门来帮我们实现类型转换的功能,这一类函数有一个统一的形式: convertto*()
\php-5.6.17\Zend\zend_operators.c
ZEND_API void _convert_to_cstring(zval *op ZEND_FILE_LINE_DC); ZEND_API void _convert_to_string(zval *op ZEND_FILE_LINE_DC); ZEND_API void convert_to_long(zval *op); ZEND_API void convert_to_double(zval *op); ZEND_API void convert_to_long_base(zval *op, int base); ZEND_API void convert_to_null(zval *op); ZEND_API void convert_to_boolean(zval *op); ZEND_API void convert_to_array(zval *op); ZEND_API void convert_to_object(zval *op); #define convert_to_cstring(op) if ((op)->type != IS_STRING) { _convert_to_cstring((op) ZEND_FILE_LINE_CC); } #define convert_to_string(op) if ((op)->type != IS_STRING) { _convert_to_string((op) ZEND_FILE_LINE_CC); }
0x8: 预定义变量
PHP脚本在执行的时候用户全局变量(在用户空间显式定义的变量)会保存在一个HashTable数据类型的符号表(symbol_table)中
除此之外,在PHP中有一些比较特殊的全局变量例如: $_GET,$_POST,$_SERVER等变量,我们并没有在程序中定义这些变量,并且这些变量也同样保存在符号表中,PHP是在脚本运行之前就将这些特殊的变量加入到了符号表中了
1. $GLOBALS的初始化
我们以cgi模式为例说明$GLOBALS的初始化,从cgi_main.c文件main函数开始。整个调用顺序如下所示
\php-5.6.17\Zend\zend.c zend_register_auto_global("GLOBALS", sizeof("GLOBALS") - 1, 1, php_auto_globals_create_globals TSRMLS_CC); static zend_bool php_auto_globals_create_globals(const char *name, uint name_len TSRMLS_DC) /* {{{ */ { zval *globals; ALLOC_ZVAL(globals); Z_SET_REFCOUNT_P(globals, 1); Z_SET_ISREF_P(globals); Z_TYPE_P(globals) = IS_ARRAY; Z_ARRVAL_P(globals) = &EG(symbol_table); zend_hash_update(&EG(symbol_table), name, name_len + 1, &globals, sizeof(zval *), NULL); return 0; } /* }}} */ [main() -> php_request_startup() -> zend_activate() -> init_executor() ] .. zend_hash_init(&EG(symbol_table), 50, NULL, ZVAL_PTR_DTOR, 0);
php_request_startup函数在PHP的生命周期中属于请求初始化阶段,即每个请求都会执行这个函数。 因此,对于每个用户请求,其用到的这些预定义的全局变量都会不同。 $GLOVALS的关键点在于zend_hash_update函数的调用,它将变量名为GLOBALS的变量注册到EG(symbol_table)中, EG(symbol_table)是一个HashTable的结构,用来存放顶层作用域的变量。 通过这个操作,GLOBAL变量与其它顶层的变量一样都会注册到了变量表, 也可以和其它变量一样直接访问了
2. $_GET、$_POST等变量的初始化
$_GET、$_COOKIE、$_SERVER、$_ENV、$_FILES、$_REQUEST这六个变量都是通过如下的调用序列进行初始化
[main() -> php_cli_startup() -> php_module_startup() -> php_startup_auto_globals(TSRMLS_C) ] void php_startup_auto_globals(TSRMLS_D) { zend_register_auto_global(ZEND_STRL("_GET"), 0, php_auto_globals_create_get TSRMLS_CC); zend_register_auto_global(ZEND_STRL("_POST"), 0, php_auto_globals_create_post TSRMLS_CC); zend_register_auto_global(ZEND_STRL("_COOKIE"), 0, php_auto_globals_create_cookie TSRMLS_CC); zend_register_auto_global(ZEND_STRL("_SERVER"), PG(auto_globals_jit), php_auto_globals_create_server TSRMLS_CC); zend_register_auto_global(ZEND_STRL("_ENV"), PG(auto_globals_jit), php_auto_globals_create_env TSRMLS_CC); zend_register_auto_global(ZEND_STRL("_REQUEST"), PG(auto_globals_jit), php_auto_globals_create_request TSRMLS_CC); zend_register_auto_global(ZEND_STRL("_FILES"), 0, php_auto_globals_create_files TSRMLS_CC); }
以$_POST为例说明整个初始化的过程
static zend_bool php_auto_globals_create_post(const char *name, uint name_len TSRMLS_DC) { zval *vars; if ( PG(variables_order) && (strchr(PG(variables_order),'P') || strchr(PG(variables_order),'p')) && SG(request_info).request_method && !strcasecmp(SG(request_info).request_method, "POST")) { //按照PG(variables_order)指定的顺序(在php.ini中指定),通过调用sapi_module.treat_data处理数据 sapi_module.treat_data(PARSE_POST, NULL, NULL TSRMLS_CC); vars = PG(http_globals)[TRACK_VARS_POST]; } else { ALLOC_ZVAL(vars); array_init(vars); INIT_PZVAL(vars); if (PG(http_globals)[TRACK_VARS_POST]) { zval_ptr_dtor(&PG(http_globals)[TRACK_VARS_POST]); } PG(http_globals)[TRACK_VARS_POST] = vars; } //将POST变量添加到全局符号表中,所以我们能在$GLOBALS中直接访问到POST参数 zend_hash_update(&EG(symbol_table), name, name_len + 1, &vars, sizeof(zval *), NULL); Z_ADDREF_P(vars); return 0; /* don't rearm */ }
0x9: 预定义变量的获取
在某个局部函数中使用类似于$GLOBALS变量这样的预定义变量,如果在此函数中有改变的它们的值的话,这些变量在其它局部函数调用时会发现也会同步变化,从PHP中间代码的执行来看,这些变量是存储在一个集中的地方: 即EG(symbol_table)
在模块初始化时,$GLOBALS在zend_startup函数中通过调用zend_register_auto_global将GLOBALS注册为预定义变量,$_GET、$_POST等在php_startup_auto_globals函数中通过zend_register_auto_global将_GET、_POST等注册为预定义变量
在通过$获取变量时,PHP内核都会通过这些变量名区分是否为全局变量(ZEND_FETCH_GLOBAL),其调用的判断函数为zend_is_auto_global,这个过程是在生成中间代码过程中实现的。如果是ZEND_FETCH_GLOBAL或ZEND_FETCH_GLOBAL_LOCK(global语句后的效果),则在获取获取变量表时(zend_get_target_symbol_table),直接返回EG(symbol_table)。则这些变量的所有操作都会在全局变量表进行
0x10: 静态变量的实现
通常意义上静态变量是静态分配的,他们的生命周期和程序的生命周期一样,只有在程序退出时才结束期生命周期,这和局部变量相反,有的语言中全局变量也是静态分配的。例如PHP和Javascript中的全局变量
静态变量可以分为
1. 静态全局变量: PHP中的全局变量也可以理解为静态全局变量,因为除非明确unset释放,在程序运行过程中始终存在 2. 静态局部变量: 在函数内定义的静态变量,函数在执行时对变量的操作会保持到下一次函数被调用 3. 静态成员变量: 这是在类中定义的静态变量,和实例变量相对应,静态成员变量可以在所有实例中共享
下面通过一个具体的实例来看PHP在用户态语法层面和内核态层面分别是怎么处理静态变量的
<?php function t() { static $i = 0; $i++; echo $i, ' '; } t(); t(); t(); ?>
static是PHP的关键字,我们需要从词法分析,语法分析,中间代码生成到执行中间代码这几个部分探讨整个实现过程
1. 词法分析
Zend/zend_language_scanner.l
<ST_IN_SCRIPTING>"static" { return T_STATIC; }
PHP词法分析向语法分析器传入T_STATIC这个词素
2. 语法分析
Zend/zend_language_parser.y
| T_STATIC static_var_list ';' .. static_var_list: static_var_list ',' T_VARIABLE { zend_do_fetch_static_variable(&$3, NULL, ZEND_FETCH_STATIC TSRMLS_CC); } | static_var_list ',' T_VARIABLE '=' static_scalar { zend_do_fetch_static_variable(&$3, &$5, ZEND_FETCH_STATIC TSRMLS_CC); } | T_VARIABLE { zend_do_fetch_static_variable(&$1, NULL, ZEND_FETCH_STATIC TSRMLS_CC); } | T_VARIABLE '=' static_scalar { zend_do_fetch_static_variable(&$1, &$3, ZEND_FETCH_STATIC TSRMLS_CC); } ;
语法分析的过程中如果匹配到相应的模式则会进行相应的处理动作,通常是进行opcode的编译。 在本例中的static关键字匹配中,是由函数zend_do_fetch_static_variable处理的
3. 生成opcode中间代码
zend_do_fetch_static_variable函数的作用就是生成opcode
\php-5.6.17\Zend\zend_compile.c
void zend_do_fetch_static_variable(znode *varname, const znode *static_assignment, int fetch_type TSRMLS_DC) /* {{{ */ { zval *tmp; zend_op *opline; znode lval; znode result; ALLOC_ZVAL(tmp); if (static_assignment) { *tmp = static_assignment->u.constant; } else { INIT_ZVAL(*tmp); } //在解释成中间代码时,静态变量是存放在CG(active_op_array)->static_variables中的 if (!CG(active_op_array)->static_variables) { /* 初始化此时的静态变量存放位置 */ if (CG(active_op_array)->scope) { CG(active_op_array)->scope->ce_flags |= ZEND_HAS_STATIC_IN_METHODS; } // 将新的静态变量放进来 ALLOC_HASHTABLE(CG(active_op_array)->static_variables); zend_hash_init(CG(active_op_array)->static_variables, 2, NULL, ZVAL_PTR_DTOR, 0); } zend_hash_update(CG(active_op_array)->static_variables, Z_STRVAL(varname->u.constant), Z_STRLEN(varname->u.constant)+1, &tmp, sizeof(zval *), NULL); if (varname->op_type == IS_CONST) { if (Z_TYPE(varname->u.constant) != IS_STRING) { convert_to_string(&varname->u.constant); } } opline = get_next_op(CG(active_op_array) TSRMLS_CC); opline->opcode = (fetch_type == ZEND_FETCH_LEXICAL) ? ZEND_FETCH_R : ZEND_FETCH_W; /* the default mode must be Write, since fetch_simple_variable() is used to define function arguments */ opline->result_type = IS_VAR; opline->result.var = get_temporary_variable(CG(active_op_array)); SET_NODE(opline->op1, varname); if (opline->op1_type == IS_CONST) { CALCULATE_LITERAL_HASH(opline->op1.constant); } SET_UNUSED(opline->op2); opline->extended_value = ZEND_FETCH_STATIC; GET_NODE(&result, opline->result); if (varname->op_type == IS_CONST) { zval_copy_ctor(&varname->u.constant); } fetch_simple_variable(&lval, varname, 0 TSRMLS_CC); /* Relies on the fact that the default fetch is BP_VAR_W */ if (fetch_type == ZEND_FETCH_LEXICAL) { znode dummy; zend_do_begin_variable_parse(TSRMLS_C); zend_do_assign(&dummy, &lval, &result TSRMLS_CC); zend_do_free(&dummy TSRMLS_CC); } else { // 赋值操作中间代码生成 zend_do_assign_ref(NULL, &lval, &result TSRMLS_CC); } CG(active_op_array)->opcodes[CG(active_op_array)->last-1].result_type |= EXT_TYPE_UNUSED; } /* }}} */
由此再次可以看出,PHP的语法解析过程,就是从Token词素向opcode的生成过程,对于PHP来说,词法产生式就是对应于zend的opcode产生函数
4. 执行中间代码
opcode的编译阶段完成后就开始opcode的执行了。 在Zend/zend_vm_opcodes.h文件中包含所有opcode的宏定义,Zend为每个函数(准确的说是zend_op_array)分配了一个私有的符号表来保存该函数的静态变量,这就是函数内静态变量生命周期的底层原理
0x11: 变量类型提示
http://www.nowamagic.net/librarys/veda/detail/1409
0x12: 变量的生命周期
http://www.nowamagic.net/librarys/veda/detail/1414
0x13: 变量赋值与销毁
http://www.nowamagic.net/librarys/veda/detail/1415
0x14: 变量作用域
http://www.nowamagic.net/librarys/veda/detail/1420
Relevant Link:
http://udn.yyuap.com/doc/wiki/project/extending-embedding-php/2.html http://www.walu.cc/phpbook/2.md http://www.nowamagic.net/librarys/veda/detail/1326 http://www.nowamagic.net/librarys/veda/detail/1327 http://www.nowamagic.net/librarys/veda/detail/1386 http://www.nowamagic.net/librarys/veda/detail/1387 http://www.nowamagic.net/librarys/veda/detail/1388 http://www.nowamagic.net/librarys/veda/detail/1389 http://www.php-internals.com/book/?p=chapt03/03-03-pre-defined-variable http://www.nowamagic.net/librarys/veda/detail/1390 http://www.nowamagic.net/librarys/veda/detail/1402
4. PHP常量实现
值得注意的是,PHP内核中变量(_zval_struct)和常量(_zend_constant)是两个独立的结构表示,它们在更底层都复用了zval以实现值表示
0x1: 数据结构
常量顾名思义是一个常态的量值。它与值只绑定一次,它的作用在于有肋于增加程序的可读性和可靠性。 在PHP中,常量的名字是一个简单值的标识符,在脚本执行期间该值不能改变。 和变量一样,常量默认为大小写敏感,但是按照我们的习惯常量标识符总是大写的。 常量名和其它任何 PHP 标签遵循同样的命名规则。合法的常量名以字母或下划线开始,后面跟着任何字母,数字或下划线
常量是在变量的zval结构的基础上添加了一额外的元素。如下所示为PHP中常量的内部结构
\php-5.6.17\Zend\zend_constants.h
typedef struct _zend_constant { zval value; /* zval结构,PHP内部变量的存储结构 */ int flags; /* 常量的标记如 CONST_PERSISTENT | CONST_CS */ char *name; /* 常量名称 */ uint name_len; int module_number; /* 模块号 */ } zend_constant;
0x2: define定义常量
define是PHP的内置函数
\php-5.6.17\Zend\zend_builtin_functions.c
/* {{{ proto bool define(string constant_name, mixed value, boolean case_insensitive=false) Define a new constant */ ZEND_FUNCTION(define) { char *name; int name_len; zval *val; zval *val_free = NULL; zend_bool non_cs = 0; int case_sensitive = CONST_CS; zend_constant c; //接受传入参数,即要定义的常量值 if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sz|b", &name, &name_len, &val, &non_cs) == FAILURE) { return; } if(non_cs) { case_sensitive = 0; } /* class constant, check if there is name and make sure class is valid & exists */ if (zend_memnstr(name, "::", sizeof("::") - 1, name + name_len)) { zend_error(E_WARNING, "Class constants cannot be defined or redefined"); RETURN_FALSE; } repeat: switch (Z_TYPE_P(val)) { case IS_LONG: case IS_DOUBLE: case IS_STRING: case IS_BOOL: case IS_RESOURCE: case IS_NULL: break; case IS_OBJECT: if (!val_free) { if (Z_OBJ_HT_P(val)->get) { val_free = val = Z_OBJ_HT_P(val)->get(val TSRMLS_CC); goto repeat; } else if (Z_OBJ_HT_P(val)->cast_object) { ALLOC_INIT_ZVAL(val_free); if (Z_OBJ_HT_P(val)->cast_object(val, val_free, IS_STRING TSRMLS_CC) == SUCCESS) { val = val_free; break; } } } /* no break */ default: zend_error(E_WARNING,"Constants may only evaluate to scalar values"); if (val_free) { zval_ptr_dtor(&val_free); } RETURN_FALSE; } //将传递的参数传递给新建的zend_constant结构 c.value = *val; zval_copy_ctor(&c.value); if (val_free) { zval_ptr_dtor(&val_free); } c.flags = case_sensitive; /* non persistent */ c.name = str_strndup(name, name_len); if(c.name == NULL) { RETURN_FALSE; } c.name_len = name_len+1; c.module_number = PHP_USER_CONSTANT; //将这个结构体注册到常量列表中 if (zend_register_constant(&c TSRMLS_CC) == SUCCESS) { RETURN_TRUE; } else { RETURN_FALSE; } } /* }}} */
Relevant Link:
http://www.nowamagic.net/librarys/veda/detail/1368
Copyright (c) 2016 Little5ann All rights reserved