從上一小節讀者可以了解到opcode在PHP內部的實現,那怎么找到某個opcode的處理函數呢?為了方便讀者在追蹤代碼的過程中找到各種opcode對應的處理函數實現,下面介紹幾種方法。
> 從PHP5.1開始,PHP對opcode的分發方式可以用戶自定義,分為CALL,SWITCH和GOTO三種類型。 默認使用的CALL的方式,本文也應用于這種方式。有關Zend虛擬機的介紹請閱讀后面相關內容。
### Debug法
在學習研究PHP內核的過程中,經常通過opcode來查看代碼的執行順序,opcode的執行由在文件Zend/zend_vm_execute.h中的execute函數執行。
ZEND_API void execute(zend_op_array *op_array TSRMLS_DC)
{
...
zend_vm_enter:
....
if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) {
switch (ret) {
case 1:
EG(in_execution) = original_in_execution;
return;
case 2:
op_array = EG(active_op_array);
goto zend_vm_enter;
case 3:
execute_data = EG(current_execute_data);
default:
break;
}
}
...
}
在執行的過程中,EX(opline)->handler(展開后為 *execute_data->opline->handler)存儲了處理當前操作的函數指針。使用gdb調試,在execute函數處增加斷點,使用p命令可以打印出類似這樣的結果:
(gdb) p *execute_data->opline->handler
$1 = {int (zend_execute_data *)} 0x10041f394 <ZEND_NOP_SPEC_HANDLER>
這樣就可以方便的知道當前要執行的處理函數了,這種debug的方法。這種方法比較麻煩,需要使用gdb來調試。
### 計算法
在PHP內部有一個函數用來快速的返回特定opcode對應的opcode處理函數指針:zend_vm_get_opcode_handler()函數:
static opcode_handler_t
zend_vm_get_opcode_handler(zend_uchar opcode, zend_op* op)
{
static const int zend_vm_decode[] = {
_UNUSED_CODE, /* 0 */
_CONST_CODE, /* 1 = IS_CONST */
_TMP_CODE, /* 2 = IS_TMP_VAR */
_UNUSED_CODE, /* 3 */
_VAR_CODE, /* 4 = IS_VAR */
_UNUSED_CODE, /* 5 */
_UNUSED_CODE, /* 6 */
_UNUSED_CODE, /* 7 */
_UNUSED_CODE, /* 8 = IS_UNUSED */
_UNUSED_CODE, /* 9 */
_UNUSED_CODE, /* 10 */
_UNUSED_CODE, /* 11 */
_UNUSED_CODE, /* 12 */
_UNUSED_CODE, /* 13 */
_UNUSED_CODE, /* 14 */
_UNUSED_CODE, /* 15 */
_CV_CODE /* 16 = IS_CV */
};
return zend_opcode_handlers[
opcode * 25 + zend_vm_decode[op->op1.op_type] * 5
+ zend_vm_decode[op->op2.op_type]];
}
由上面的代碼可以看到,opcode到php內部函數指針的查找是由下面的公式來進行的:
opcode * 25 + zend_vm_decode[op->op1.op_type] * 5
+ zend_vm_decode[op->op2.op_type]
然后將其計算的數值作為索引到zend_init_opcodes_handlers數組中進行查找。不過這個數組實在是太大了,有3851個元素,手動查找和計算都比較麻煩。
### 命名查找法
上面的兩種方法其實都是比較麻煩的,在定位某一opcode的實現執行代碼的過程中,都不得不對程序進行執行或者計算中間值。而在追蹤的過程中,筆者發現處理函數名稱是有一定規則的。這里以函數調用的opcode為例,調用某函數的opcode及其對應在php內核中實現的處理函數如下:
//函數調用:
DO_FCALL ==> ZEND_DO_FCALL_SPEC_CONST_HANDLER
?
//變量賦值:
ASSIGN => ZEND_ASSIGN_SPEC_VAR_CONST_HANDLER
ZEND_ASSIGN_SPEC_VAR_TMP_HANDLER
ZEND_ASSIGN_SPEC_VAR_VAR_HANDLER
ZEND_ASSIGN_SPEC_VAR_CV_HANDLER
//變量加法:
ASSIGN_SUB => ZEND_ASSIGN_SUB_SPEC_VAR_CONST_HANDLER,
ZEND_ASSIGN_SUB_SPEC_VAR_TMP_HANDLER,
ZEND_ASSIGN_SUB_SPEC_VAR_VAR_HANDLER,
ZEND_ASSIGN_SUB_SPEC_VAR_UNUSED_HANDLER,
ZEND_ASSIGN_SUB_SPEC_VAR_CV_HANDLER,
ZEND_ASSIGN_SUB_SPEC_UNUSED_CONST_HANDLER,
ZEND_ASSIGN_SUB_SPEC_UNUSED_TMP_HANDLER,
ZEND_ASSIGN_SUB_SPEC_UNUSED_VAR_HANDLER,
ZEND_ASSIGN_SUB_SPEC_UNUSED_UNUSED_HANDLER,
ZEND_ASSIGN_SUB_SPEC_UNUSED_CV_HANDLER,
ZEND_ASSIGN_SUB_SPEC_CV_CONST_HANDLER,
ZEND_ASSIGN_SUB_SPEC_CV_TMP_HANDLER,
ZEND_ASSIGN_SUB_SPEC_CV_VAR_HANDLER,
ZEND_ASSIGN_SUB_SPEC_CV_UNUSED_HANDLER,
ZEND_ASSIGN_SUB_SPEC_CV_CV_HANDLER,
在上面的命名就會發現,其實處理函數的命名是有以下規律的:
ZEND_[opcode]_SPEC_(變量類型1)_(變量類型2)_HANDLER
這里的變量類型1和變量類型2是可選的,如果同時存在,那就是左值和右值,歸納有下幾類:VAR TMP CV UNUSED CONST這樣可以根據相關的執行場景來判定。
### 日志記錄法
這種方法是上面**計算法**的升級,同時也是比較精準的方式。在**zend_vm_get_opcode_handler** 方法中添加以下代碼:
static opcode_handler_t
zend_vm_get_opcode_handler(zend_uchar opcode, zend_op* op)
{
static const int zend_vm_decode[] = {
_UNUSED_CODE, /* 0 */
_CONST_CODE, /* 1 = IS_CONST */
_TMP_CODE, /* 2 = IS_TMP_VAR */
_UNUSED_CODE, /* 3 */
_VAR_CODE, /* 4 = IS_VAR */
_UNUSED_CODE, /* 5 */
_UNUSED_CODE, /* 6 */
_UNUSED_CODE, /* 7 */
_UNUSED_CODE, /* 8 = IS_UNUSED */
_UNUSED_CODE, /* 9 */
_UNUSED_CODE, /* 10 */
_UNUSED_CODE, /* 11 */
_UNUSED_CODE, /* 12 */
_UNUSED_CODE, /* 13 */
_UNUSED_CODE, /* 14 */
_UNUSED_CODE, /* 15 */
_CV_CODE /* 16 = IS_CV */
};
?
//很顯然,我們把opcode和相對應的寫到了/tmp/php.log文件中
int op_index;
op_index = opcode * 25 + zend_vm_decode[op->op1.op_type] * 5 + zend_vm_decode[op->op2.op_type];
?
FILE *stream;
if((stream = fopen("/tmp/php.log", "a+")) != NULL){
fprintf(stream, "opcode: %d , zend_opcode_handlers_index:%d\n", opcode, op_index);
}
fclose(stream);
?
?
return zend_opcode_handlers[
opcode * 25 + zend_vm_decode[op->op1.op_type] * 5
+ zend_vm_decode[op->op2.op_type]];
}
然后,就可以在**/tmp/php.log**文件中生成類似如下結果:
opcode: 38 , zend_opcode_handlers_index:970
前面的數字是opcode的,我們可以這里查到: http://php.net/manual/en/internals2.opcodes.list.php后面的數字是static const opcode_handler_t labels[] 索引,里面對應了處理函數的名稱,對應源碼文件是:Zend/zend_vm_execute.h (第30077行左右)。 這是一個超大的數組,php5.3.4中有3851個元素,在上面的例子里,看樣子我們要數到第970個了,當然,有很多種方法來避免人工去計算,這里就不多介紹了。
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊