[置頂] PHP如何擴展和如何在linux底層對php擴展?

   雖然大部分php工程師都不須要知道php的C代碼核心是如何運做的,有些人可能知道有個dl()函數.或者使用過一些第三方的類庫,這些正是本文的重點之一.      
 
   但願對那些想把php帶向更寬的邊界的工程師有所幫助.


   先來看看php的一個基本的運行流程:
   
   瀏覽器用戶--->web服務器(apache,nginx)--->Zend引擎從文件系統讀取php代碼文件--->Zend解釋器工做
   
   --->執行解釋後的代碼-->Zend引擎註冊的函數接口-->內置模塊或者各個須要的外部模塊擴展-->數據庫memcache等後端資源
   
   其中 
   Zend引擎註冊的函數接口 就是php工程師常常接觸的各類php函數.
   外部模塊擴展 就是php編譯的各個so文件(linux)或者dll文件(windwos).
   執行解釋後的代碼 瀏覽器的內容就是從這裏返回的.
   內置模塊 也就是php每次啓動的時候會攜帶啓動的模塊. 
   從上面的流程圖能夠知道php能夠從3個點進行擴展.1 外部模塊擴展 2 Zend引擎 3 內置模塊,下面我將一一討論.
   
   外部模塊擴展.
       若是你使用過dl()你就接觸過這些外部的擴展模塊.外部的擴展模塊文件就放在你的硬盤裏,他在php腳本運行時被加載到內存中,並且只有須要的時候才被加載.
   當這次的腳本運行完以後他就會被內存釋放掉,總的來講它運行的慢可是不佔資源.不須要你從新編譯一個php.
   
   內置模塊
       雖然也是Zend引擎以外的模塊,可是與外部模塊擴展有些不一樣,他已經在php裏邊了.他會使得你編譯的php體積變大,若是有改變,必須從新編譯php才行.內置模塊會使得
   php內存變大,可是調用起來也會更加的快速.在咱們的測試中一些模塊運行在內置模式會有30%以上的速度提高.
   
   Zend引擎
      首先,我絕對不建議你去修改Zend引擎.一些php語言的特性只要在Zend引擎中才可以實現.好比你要修改數組關鍵字的名字,你能夠在這裏實現.
   在你下載的php源代碼裏,以zend開頭的都是zend引擎的相關代碼.
   
   通常php源碼目錄結構相似下面:
   main php的主要源代碼,
   ext php的擴展 
   sapi 與不一樣服務器的api交互層代碼
   zend zend引擎部分
   TSRM 線程安全相關模塊代碼
   
   下面以一個簡單的模塊爲例子說明PHP如何擴展:
   
   首先php的代碼有本身的一套標準,你須要遵照,否則可能會致使你的模塊沒法釋放變量或者其餘的問題,這些標準包括 宏定義,變量聲明等.你能夠到官方瀏覽詳細的說明.
   
   
/* 擴展的標準頭 */
#include "php.h"


/* 聲明這個so被導出的函數 */
ZEND_FUNCTION(helloworld_module);


/* Zend引擎註冊的函數接口 */
zend_function_entry helloworldmod_interfaces[] =
{
    ZEND_FE(helloworld_module, NULL)
    {NULL, NULL, NULL}
};


/* 這是這個模塊的聲明實體,它的值對模塊編譯的時候起實際做用 */
zend_module_entry helloworldmod_module_entry =
{
    STANDARD_MODULE_HEADER,
    "Hello world",
    helloworldmod_interfaces,
    NULL, 
    NULL, 
    NULL, 
    NULL, 
    NULL,
    NO_VERSION_YET,
    STANDARD_MODULE_PROPERTIES
};


/* 向zend引擎聲明一個備案,能夠說明 helloworldmod_module_entry屬於helloworldmod.so這個動態庫*/
#if COMPILE_DL_helloworld_module
ZEND_GET_MODULE(helloworldmod)
#endif


/* 這就是咱們新增的函數的真正代碼 */
ZEND_FUNCTION(helloworld_module)
{
    return "Hello,world";
}


   

   
   咱們能夠根據其餘擴展的config.m4文件來修改爲咱們的必要編譯配置信息。這裏這個模塊幾乎是一個空的config.m4文件就行,
   
   而後利用phpize來生成configure文件而後是 ./configure && make && make install執行就能編譯一份咱們的動態庫
   
   test.php
<?php


echo helloworld_module();


?>      
   輸出:
      "Hello,world"
      
      
   完成了PHP擴展,咱們已經深刻了php的c代碼內部,可是在一些狀況下,這還不夠。咱們須要深刻到c語言調用c庫的過程中,在linux下面一個很給力的工具是LD_PRELOAD環境變量
   
   LD_PRELOAD環境變量是編譯器找到程序中所引用的函數或全局變量所存在的位置的一個過濾器,好比在php的c代碼裏調用一個開始網絡鏈接的方法connect,事實上就是經過動態連接
   
   去尋找linux的c庫的函數connect,這些連接文件通常放在lib下面,這也就爲咱們影響php的代碼執行提供了一個切入點。由於php程序在動態載入lib下面的函數connect以前會檢查LD_PRELOAD
   
   提供的動態庫裏有沒有這個connect函數,咱們能夠在這裏對php的行爲進行干涉。
   
   下面以一個簡單的過濾網絡訪問的例子說明如何實現:
   
   先是一個準備做爲LD_PRELOAD環境變量的值的so文件的代碼。
   lp_demo.c


#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <dlfcn.h>


//定義咱們本身的connect函數
int  connect(int  sockfd,  const  struct sockaddr *serv_addr, socklen_t
             addrlen){
  static int (*connect_linuxc)(int, const  struct sockaddr*, socklen_t)=NULL;
  unsigned char *ip_char;
 
  //利用 lsym的RTLD_NEXT選項繞過LD_PRELOAD環境變量的connect方法找到c庫的函數
  if (!connect_linuxc) connect_linuxc=dlsym(RTLD_NEXT,"connect");
 
    ip_char=serv_addr->sa_data;
    ip_char+=2;
 
    
     //192.168.2.3 找到了
    if ((*ip_char==192)&&(*(ip_char+1)==168)&&(*(ip_char+2)==2)&&(*(ip_char+3)==3)) {
    
         //簡單返回一個權限錯誤的代碼
            return EACCES;
    }


 
  
  // 調用真正的connect方法
   return connect_linuxc(sockfd,serv_addr,addrlen);
  
}


編譯成so文件


$ gcc -o lp_demo.so -shared lp_demo.c -ldl


測試文件 test.php


<?php


file_get_contents("http://192.168.2.3/");


?>




使用方法
LD_PRELOAD=lp_demo.so php test.php


這樣他將不可能訪問的到192.168.2.3這種咱們內部的網址。起到一個很好的沙盒做用。


除此以外咱們還能夠利用fwrite fopen等函數將php對文件系統的讀寫操做轉移到mencache,nosql之類的後端資源當中。


 
   最後,即便咱們已經深刻了c庫的內部,也不意味着咱們走到了最底層,在c庫下面,還有一堆sys_開頭的函數,他們纔是內核空間裏的真正函數,在此就不在探討了。
php

相關文章
相關標籤/搜索