woojean的博客 模仿、熟练、超越

编程问题总结-PHP

2017-04-05

foreach循环中使用引用存在的问题

问题 求以下代码的打印值:

<?php
$array = [1, 2, 3]; 
foreach ($array as &$value) {}   
foreach ($array as $value) {}        
var_dump($array);

分析

<?php
$array = [1, 2, 3]; 

foreach ($array as &$value) {
  // do nothing
}   

var_dump($array);  // 1 2 3
var_dump($value);  // 3 $value在这里仍然有作用域

// unset($value);  // fix

foreach ($array as $value) {
  // do nothing
}        
var_dump($array);  // 1 2 2

总结

  • PHP foreach循环中定义的“临时变量”的作用域会延伸到循环体之外;
  • 引用自己并没有维护一个值,而是指向引用的变量,对引用的修改等于对被引用对象的修改;

__autoload()和spl_autoload_register()的主要区别

函数原型

<?php
void __autoload(string $class)

bool spl_autoload_register ([ callable $autoload_function [, bool $throw = true [, bool $prepend = false ]]] )

区别

  • spl_autoload_register()函数会将Zend Engine中的__autoload()函数取代为spl_autoload()或spl_autoload_call()。
  • spl_autoload_register()可以很好地处理需要多个加载器的情况,这种情况下spl_autoload_register会按顺序依次调用之前注册过的加载器。作为对比,__autoload 因为是一个函数,所以只能被定义一次。

举例

<?php
spl_autoload_register(function ($class) {
  include 'classes/' . $class . '.class.php';
});

使用spl_autoload_register()调用静态方法

<?php
class Loader {
  public static function loadPrinter( $class ) {
    $file = $class . '.class.php';  
    if (is_file($file)) {  
      require_once($file);  
    } 
  }
} 

spl_autoload_register(['Loader','loadPrinter']);
// 另一种写法:spl_autoload_register("Loader::loadPrinter"); 

$obj = new Printer();  // 假设存在Printer.class.php这个文件
$obj->doPrint();

4种字符串表示方法

  • 单引号(不会转义)
  • 双引号(会转义)
  • heredoc语法结构(类似双引号)
  • nowdoc语法结构(类似单引号) 字符串会被按照该脚本文件相同的编码方式来编码。因此如果一个脚本的编码是 ISO-8859-1,则其中的字符串也会被编码为 ISO-8859-1。

PHP中的闭包无须像js(非ES6)一样通过self=this的形式来扩展作用域

<?php
class Demo{
  private $list = [1,2,3,4,5,6,7,8,9,10];
  public $delList = [];
  
  public function test(){
    $arr = array_filter($this->list,function($n){
      if($n % 2 == 0){
        return true;
      }
      else{
        $this->delList[] = $n;  // 此$this指向Demo的对象,而非闭包函数
        return false;
      }
    });
    
    return $arr;
  }
}

$demo = new Demo();
$arr = $demo->test();
var_dump($arr);   // 1 3 5 7 9
var_dump($demo->delList); // 2 4 6 8 10

在PHP中使用异步非阻塞函数

  • swoole_client异步模式
  • mysql-async库
  • redis-async库
  • swoole_timer_tick/swoole_timer_after
  • swoole_event系列函数
  • swoole_table/swoole_atomic/swoole_buffer
  • swoole_server->task/finish函数

PHP对象注入漏洞

PHP支持对象的序列化和反序列化操作(serialize、unserialize)。 如:

<?php
class User{
  public $age = 0;
  public $name = '';
  public function PrintData(){
    echo 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />';
  }
}

$usr = unserialize('O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"John";}');
$usr->PrintData();

输出:

User John is 20 years old. 

当一个对象进行序列化和反序列化操作时也会自动调用其他相应的魔术方法:

  • 当对象进行序列化操作时魔术方法__sleep()会被自动调用(必须返回一个包含序列化的类变量名的数组)。
  • 当对象进行反序列化操作时魔术方法__wakeup()会被自动调用。

攻击者可以操作类变量来攻击web应用,比如:

<?php
$usr = unserialize('O:7:"LogFile":1:{s:8:"filename";s:9:".htaccess";}');
$usr->PrintData();

从而意外地执行了LogFile的__construct()和_destruct()。 在处理由用户提供数据的地方不要使用unserialize(),可以使用json_decode(…)。

总结 实际就是PHP的反序列化机制会自动执行一些魔方方法,从而引起一些意外情况。

PHP数据类型

PHP共支持8种原始数据类型,其中包括:

  • 4种标量类型:boolean、integer、float(double)、string
  • 2种复合类型:array、object
  • 2种特殊类型:resource、NULL

double和float是相同的,由于一些历史的原因,这两个名称同时存在。 变量的类型通常不是由程序员设定的,确切地说,是由PHP根据该变量使用的上下文在运行时决定的。

PHP变量作用域

6项基本的作用域规则

  • 超级全局变量(内置)可以在脚本的任何地方使用和可见。
  • 常量一旦被声明,将全局可见,即可以在函数内外使用。
  • 在一个脚本中声明的全局变量在整个脚本中是可见的。
  • 函数内部使用的变量声明为全局变量时,其名称要与全局变量一致。
  • 在函数内部创建的静态变量,在函数外部不可见,但是可以在函数的多次执行中保持值。
  • 在函数内部创建的非静态变量,当函数终止时就不存在了。

SAPI

无论是Web模式还是Cli模式运行,PHP的工作原理都是一样的, 都是作为一种SAPI在运行(Server Application Programming Interface:the API used by PHP to interface with Web Servers)。SAPI就是PHP和外部环境的代理器。它把外部环境抽象后, 为内部的PHP提供一套固定的,统一的接口,使得PHP自身实现能够不受错综复杂的外部环境影响,保持一定的独立性。

PHP扩展的基本执行方式

  • MINITPHP启动时, 会把自己所有已加载扩展的MINIT方法(全称Module Initialization,是由每个模块自己定义的函数)都执行一遍。 在这个时间里,扩展可以定义一些自己的常量、类、资源等所有会被用户端的PHP脚本用到的东西。
  • RINIT

当一个页面请求到来时,PHP会迅速的开辟一个新的环境,并重新扫描自己的各个扩展, 遍历执行它们各自的RINIT方法(俗称Request Initialization), 这时候一个扩展可能会初始化在本次请求中会使用到的变量等, 还会初始化稍后用户端(即PHP脚本)中的变量之类的。

  • RSHUTDOWN

请求完成(或者别die等终结),PHP便会启动回收程序,执行所有已加载扩展的RSHUTDOWN(Request Shutdown)方法, 这时候扩展可以抓紧利用内核中的变量表之类的做一些事情, 因为一旦PHP把所有扩展的RSHUTDOWN方法执行完, 便会释放掉这次请求使用过的所有东西, 包括变量表的所有变量、所有在这次请求中申请的内存等等。

  • MSHUTDOWN

当PHP要停止运行的时候,PHP便进入MSHUTDOWN(Module Shutdown)阶段。一旦PHP把扩展的MSHUTDOWN执行完,便会进入自毁程序,所以这里一定要把自己擅自申请的内存给释放掉。

一个最简单的例子 walu.c

int time_of_minit;  // 每次请求都不变
PHP_MINIT_FUNCTION(walu)
{
  time_of_minit=time(NULL);
  return SUCCESS;
}

int time_of_rinit;  // 每次请求都改变
PHP_RINIT_FUNCTION(walu)
{
  time_of_rinit=time(NULL);
  return SUCCESS;
}

// 每次页面请求都会往time_rshutdown.txt中写入数据
PHP_RSHUTDOWN_FUNCTION(walu)
{
  FILE *fp=fopen("/cnan/www/erzha/time_rshutdown.txt","a+");
  fprintf(fp,"%d\n",time(NULL));
  fclose(fp);
  return SUCCESS;
}

// 只有在apache结束后time_mshutdown.txt才写入有数据
PHP_MSHUTDOWN_FUNCTION(walu)
{
  FILE *fp=fopen("/cnan/www/erzha/time_mshutdown.txt","a+");
  fprintf(fp,"%d\n",time(NULL));
  return SUCCESS;
}

PHP_FUNCTION(walu_test)
{
  php_printf("%d&lt;br /&gt;",time_of_minit);
  php_printf("%d&lt;br /&gt;",time_of_rinit);
  return;
}

PHP的生命周期

PHP扩展程序的两种init(MINIT、RINIT)和两种shutdown(RSHUTDOWN、MSHUTDOWN)各会执行多少次、各自的执行频率有多少取决于PHP是用什么SAPI与宿主通信的。最常见的四种SAPI通信方式如下: 1.直接以CLI/CGI模式调用 PHP的生命周期完全在一个单独的请求中完成,两种init和两种shutdown仍然都会被执行。 以php -f test.php为例,执行过程如下:

  • (1)调用每个扩展的MINIT;
  • (2)请求test.php文件;
  • (3)调用每个扩展的RINIT;
  • (4)执行test.php;
  • (5)调用每个扩展的RSHUTDOWN;
  • (6)执行清理操作;
  • (7)调用每个扩展的MSHUTDOWN;
  • (8)终止php; fpm的每个请求都是在执行RINIT到RSHUTDOWN的步骤。 opcode cache是把第(4)步的词法分析、语法分析、生成opcode代码这几个操作给缓存起来了,从而达到加速的作用。 swoole在第(4)步接管了PHP,进入swoole的生命周期。

2.多进程模块 如编译成Apache2的Pre-fork MPM,当Apache启动的时候,会立即把自己fork出好几个子进程,每一个进程都有自己独立的内存空间,在每个进程里的PHP的工作方式如下:

  • (1)调用每个扩展的MINIT;
  • (2)循环:{ a.调用每个扩展的RINIT; b.执行脚本; c.调用每个扩展的RSHUTDOWN;}
  • (3)调用每个扩展的MSHUTDOWN;

3.多线程模块 如IIS的isapi和Apache MPM worker,只有一个服务器进程在运行着,但会同时运行很多线程,这样可以减少一些资源开销,像Module init和Module shutdown就只需要运行一次就行了,一些全局变量也只需要初始化一次, 因为线程独具的特质,使得各个请求之间方便的共享一些数据成为可能。

4.Embedded(嵌入式,在自己的C程序中调用Zend Engine) Embed SAPI是一种比较特殊但不常用的SAPI,允许在C/C++语言中调用PHP/ZE提供的函数。 这种SAPI和上面的三种一样,按Module Init、Request Init、Rshutdown、mshutdown的流程执行着。

open_basedir配置项的作用

open_basedir是PHP配置中为了防御跨目录进行文件(目录)读写的配置,所有PHP中有关文件读、写的函数都会经过open_basedir的检查。实际上是一些目录的集合,在定义了open_basedir以后,PHP可以读写的文件、目录都将被限制在这些目录中。在Linux下,不同的目录由:分割,如/var/www/:/tmp/。 注意用open_basedir指定的限制实际上是前缀,而不是目录名。

Apache运行PHP有三种配置open_basedir的方法:

  • (1)在php.ini里配置
    open_basedir = .:/tmp/
    
  • (2)在Apache配置的VirtualHost里设置(httpd-vhosts.conf)
    php_admin_value open_basedir .:/tmp/
    
  • (3)在Apache配置的Direcotry里设置
    php_admin_value open_basedir .:/tmp/
    

关于三个配置方法的解释

  • 优先级:方法(3)>方法(2)>方法(1);
  • 配置目录里加了/tmp/是因为PHP默认的临时文件(如上传的文件、session等)会放在该目录,所以一般需要添加该目录,否则部分功能将无法使用;
  • 配置目录里加了.是指运行PHP文件的当前目录,这样做可以避免每个站点一个一个设置;
  • 如果站点还使用了站点目录外的文件,需要单独在对应VirtualHost设置该目录;

使用PHP数组模拟队列操作的性能问题

PHP的数组提供了array_pop和array_shift可以使用数组模拟队列数据结构。虽然使用Array可以实现队列,但实际上性能会非常差。在一个大并发的服务器程序上,建议使用SplQueue作为队列数据结构。 一项实测结果:100万条数据随机入队、出队,使用SplQueue仅用2312.345ms即可完成,而使用Array模拟的队列的程序根本无法完成测试,CPU一直持续在100%,降低到1万条后,也需要260ms才能完成测试。 详略。

PHP不必做编译优化

C等需要编译执行的程序通常一次编译,会多次运行或长时间运行,因此在编译上多耗些时间、多做些优化被认为是值得的。而解释型语言往往作为胶水语言,也就是完成一项用后即弃的特定任务。官方的答复是,PHP程序运行时间往往很短暂,比如10ms;如果花100ms做编译优化,把运行时间压缩到1ms,总的时间消耗是101ms,反而更慢了(不考虑中间代码缓存)。

在被包含文件中仍然要使用<?php起始标志

当一个文件被包含时,语法解析器在目标文件的开头脱离PHP模式并进入HTML模式,到文件结尾处恢复。由于此原因,目标文件中需要作为PHP代码执行的任何代码都必须被包括在有效的PHP起始和结束标记之中。

应该关闭register_globals配置

这个配置影响到PHP如何接收传递过来的参数。register_globals的意思就是注册为全局变量,所以当On的时候,传递过来的值会被直接的注册为(与HTML控件的name属性同名的)全局变量直接使用,而Off的时候,需要到特定的全局变量数组里去得到它。

PHP中NULL值的判断

NULL类型唯一可能的值就是NULL(不区分大小写)。在下列情况下一个变量被认为是NULL:

  • 被赋值为NULL
  • 尚未被赋值
  • 被unset() 递减NULL值也没有效果,但是递增NULL的结果是1。
<?php
$a = null;
$b = null;

$a--;
$b++;

var_dump($a);  // null
var_dump($b);  // 1

不能使用可变变量的变量

超全局变量不能用作可变变量。$this变量也是一个特殊变量,不能被动态引用。

使用yield实现双向异步信息传递

包含yield关键字的函数比较特殊,返回值是一个Generator对象,此时函数内语句尚未真正执行。Generator对象是Iterator接口实例,可以通过rewind()、current()、next()、valid()系列接口进行操纵。Generator可以视为一种“可中断”的函数,而yield构成了一系列的“中断点”。

<?php
function gen() {
  for($i=1; $i<=100; $i++) {
    // yield既是语句,又是表达式,既具备类似return语句的功能,同时也有类似表达式的返回值(通过send得到的值)
    $cmd = (yield $i);  
    if($cmd=='stop') {
      return;
    }
  }
}

$gen = gen();
$i=0;
foreach($gen as $item) {
  echo $item."\n";
  if($i>=10) {
    $gen->send('stop');  // 向Generator发送值
  }
  $i++;
}

控制数组json_encode后为json对象或者json数组

<?php
$foo = [
  "item1" => (Object)[],  // 关键点:将Array转换成Object类型
  "item2" => []
];

echo json_encode($foo);

输出:

{"item1":{},"item2":[]}

return的使用场景和行为

  • 如果在全局范围中调用,则当前脚本文件中止运行;
  • 如果当前脚本文件是被include的或者require的,则控制交回调用文件;且如果当前脚本是被include的,则return的值会被当作include调用的返回值
  • 如果当前脚本文件是在php.ini中的配置选项auto_prepend_file或者auto_append_file所指定的,则此脚本文件中止运行;

return是语言结构而不是函数,因此其参数没有必要用括号将其括起来。通常都不用括号,实际上也应该不用,这样可以降低PHP的负担。 如果没有提供参数,则一定不能用括号,此时返回NULL。如果调用return时加上了括号却又没有参数会导致解析错误。

cgi.fix_pathinfo配置项的作用

如果Web Server为Nginx,则须在PHP的配置文件php.ini中配置cgi.fix_pathinfo = 0,防止Nginx文件解析漏洞。 在cgi.fix_pathinfo = 1的情况下,假设有如下的URL:http://xxx.net/foo.jpg,当访问http://xxx.net/foo.jpg/a.php时,foo.jpg将会被执行,如果foo.jpg是一个普通文件,那么foo.jpg的内容会被直接显示出来,但是如果把一段php代码保存为foo.jpg,那么问题就来了,这段代码就会被直接执行。

启用并配置OpCache

php.ini

; 开关打开
opcache.enable=1

; 可用内存, 酌情而定, 单位megabytes(M)
opcache.memory_consumption=256

...

检查

php -v

PHP 5.5.3-1ubuntu2.2 (cli) (built: Feb 28 2014 20:06:05) 
Copyright (c) 1997-2013 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2013 Zend Technologies
with Zend OPcache v7.0.3-dev, Copyright (c) 1999-2013, by Zend Technologies

注意其中多出了:

with Zend OPcache v7.0.3-dev

需要提醒的是,在生产环境中使用上述配置之前,必须经过严格测试。 因为上述配置存在一个已知问题,它会引发一些框架和应用的异常, 尤其是在存在文档使用了备注注解的时候。 如果在更新代码之后,发现没有执行的还是旧代码,可使用函数opcache_reset()来清除缓存。

收集PHP错误日志

display_errors = Off
log_errors = On

# 该文件必须允许Web Server的用户和组具有写的权限
error_log = /usr/local/apache2/logs/php_error.log 

禁止在PHP中使用危险函数

disable_functions = dl,assert,exec,popen,system,passthru,shell_exec,proc_close,proc_open,pcntl_exec

使用define和const定义常量的区别

  • 用define定义的常量可以不用理会变量的作用域而在任何地方定义和访问;
  • 使用const关键字定义常量必须处于最顶端的作用区域,因为用此方法是在编译时定义的。这就意味着不能在函数内、循环内以及if语句之内用const来定义常量;

allow_url_fopen和allow_url_include配置

允许访问URL远程资源(即允许fopen这样的函数打开url)使得PHP应用程序的漏洞变得更加容易被利用,php脚本若存在远程文件包含漏洞会使得攻击者直接获取网站权限及上传web木马。一般会在php配置文件中关闭该功能,若需要访问远程服务器建议采用其他方式如libcurl库。

allow_url_fopen = Off
allow_url_include = Off

比如有这样的代码:

<?php
if(isset($HTTP_GET_VARS)){
  reset($HTTP_GET_VARS);
  while ( list($var, $val) = each($HTTP_GET_VARS) ) {
    $$var=$val;
  }
}

一些较偶然的场景会导致将以http://开头的get参数所表示的远程文件直接包含进来,然后执行。

魔术常量

  • LINE 文件中的当前行号
  • FILE 文件的完整路径和文件名
  • DIR 文件所在的目录
  • FUNCTION 函数名称(PHP 4.3.0 新加)
  • CLASS 类的名称(PHP 4.3.0 新加)
  • TRAIT Trait 的名字(PHP 5.4.0 新加)
  • METHOD 类的方法名(PHP 5.0.0 新加)
  • NAMESPACE 当前命名空间的名称(区分大小写)

用于属性重载的魔术方法

  • 在给不可访问属性赋值时,__set()会被调用。
  • 读取不可访问属性的值时,__get()会被调用。
  • 当对不可访问属性调用isset()或empty()时,__isset()会被调用。
  • 当对不可访问属性调用 unset() 时,__unset()会被调用。
  • 属性重载只能在对象中进行。在静态方法中,这些魔术方法将不会被调用。

用于方法重载的魔术方法

  • 在对象中调用一个不可访问方法时,__call()会被调用。
  • 用静态方式中调用一个不可访问方法时,__callStatic()会被调用。

在声明静态变量时不能使用表达式进行赋值

静态变量的声明是在编译时解析的,因此在声明静态变量时不能用表达式进行赋值。

<?php
function foo(){
  static $int = 0;       	// ok
  static $int = 1+2;    	// error  (as it is an expression)
  static $int = sqrt(121);  // error  (as it is an expression too)

  $int++;
  echo $int;
}

将非对象类型转换为对象

  • 如果将一个对象转换成对象,它将不会有任何变化;
  • 如果其它任何类型的值被转换成对象,将会创建一个内置类stdClass的实例;
  • 如果该值为NULL,则新的实例为空;
  • 数组转换成对象将使键名成为属性名并具有相对应的值
  • 对于任何其它的值,名为**scalar**的成员变量将包含该值
    <?php
    $a = (Object)'abc';
    echo $a->scalar;  // abc
    echo gettype($a); // object
    

被包含文件的搜索顺序

  • 先按参数给出的路径寻找;
  • 如果没有给出目录(只有文件名)时则按照include_path指定的目录寻找;
  • 如果在include_path下没找到该文件则include最后才在调用脚本文件所在的目录当前工作目录下寻找;
  • 如果最后仍未找到文件则include结构会发出一条警告;这一点和require不同,后者会发出一个致命错误;
  • 如果定义了路径,不管是绝对路径还是当前目录的相对路径,include_path都会被完全忽略;

包含文件的作用域关系

  • 当一个文件被包含时,其中所包含的代码继承了include所在行的变量范围。从该处开始,调用文件在该行处可用的任何变量在被调用的文件中也都可用。不过所有在包含文件中定义的函数和类都具有全局作用域;
  • 如果include出现于调用文件中的一个函数里,则被调用的文件中所包含的所有代码将表现得如同它们是在该函数内部定义的一样。所以它将遵循该函数的变量范围。此规则的一个例外是魔术常量,它们是在发生包含之前就已被解析器处理的;

几种动态内容缓存方式

  • Smarty缓存
require '../libsmarty/Smarty.class.php';
$this->smarty = new Smarty();
$this->smarty->caching = true;

$this->template_page = 'place_posts.html';
$this->cache_id = $this->marker_id;

if( $this->smarty->is_cached($this->template_page, $this->cache_id) ){
   $this->smarty->display( $this->template_page, $this->cache_id ); 
   exit(0);
}
do_some_db_query();
...
$this->smarty->display( $this->template_page, $this->cache_id ); 

缓存保存、查找、过期检查等,主要是Smarty的API,略。

  • APC Smarty的存储基于磁盘,而APC基于内存
$this->key = $this->template_page . $this->cache_id;
$html = apc_fetch( $this->key );
if( $html !== false ){
    echo $html;
    exit(0);
}
do_some_db_query();
$html = $this->smarty->fetch($this->template_page , $this->cache_id);
apc_add($this->key, $html, $this->smarty->cache_lifetime);
echo $html;
  • XCache 和APC类似,略。

  • memcached memcached支持将缓存存储在独立的缓存服务器中。 有Redis了,略。

动态内容局部无缓存的实现

基本实现方式都是自定义一组标签,然后在模板中将局部无缓存的内容用标签包含,比如Smarty:

function smarty_block_dynamic( $params, $content, &$smarty){
    return $content;
}
$this->smarty->register_block('dynamic', 'smarty_block_dynamic', false);

{dynamic}
$user->user_nick;
{dynamic}

PHP脚本加速常见方式

  • opcode 脚本语言的解释器会事先经过词法分析、语法分析、语义分析等一系列步骤将源代码编译为操作码(Operate Code),然后再执行。PHP解释器的核心引擎为Zend Engine。

PHP的parsekit扩展提供运行时API来查看PHP代码的opcode。

var_dump( parsekit_compile_string('print 1+1;') );

opcode的格式类似于汇编代码(都是三地址码的格式),因此可以方便地翻译为不同平台的本地代码。

  • APC 开启:
apc.cache_by_default = on

也可以配置apc.filters让APC只对特定范围的动态程序进行opcode缓存。 APC同时提供跳过过期检查的机制,如果动态程序长期不会变化,那么可以跳过过期检查以获得更好的性能:

apc.stat = off
  • XCache 和APC差不多,详略。

Xdebug基本功能

xdebug_time_index();  // 返回从脚本开始处执行到当前位置所花费的时间
xdebug_call_line();   // 当前函数在哪一行被调用
xdebug_call_function();  // 当前函数在哪个函数中被调用

// 代码覆盖率
xdebug_start_code_coverage();
...
var_dump(xdebug_get_code_coverage());
  • 函数跟踪 根据程序在实际运行时的执行顺序跟踪记录所有函数的执行时间,以及函数调用时的上下文,包括实际参数和返回值。

在php.ini中配置记录文件的存储目录和文件名前缀:

xdebug.trace_output_dir = /tmp/xdebug
xdebug.trace_output_name trace.%c

%c代表函数调用。

输出示例:

0.0167  1009988  -> MarkerInfo->getMarkerInfo()
0.0167  1009988    -> DataAccess->selectDb(string(11))
0.0168  1010040      -> DataAccess->connect()
...
0.0170  1010288      -> mysql_connect(string(9), string(4), string(0))
0.0207  1011320      -> ...

通过以上片段可以分析出代码执行的时间主要消耗在mysql_connect()处。

  • 瓶颈分析 Xdebug提供性能跟踪器:
xdebug.profiler_output_dir = /tmp/xdebug
xdebug.profiler_output_name = cachegrind.out.%p

其中%p是运行时PHP解释器所在进程的PID。 可以使用图形界面工具CacheGrind分析日志。

PHP CGI路径解析问题

当访问:

http://www.xxx.com/path/test.jpg/notexist.php

时,若notexist.php不存在,会将test.jpg当做PHP进行解析。 这个漏洞的原因与在fastcgi方式下PHP获取环境变量的方式有关。php.ini中cgi.fix_pathinfo开关默认打开,在映射URI时,两个环境变量很重要:PATH_INFO和SCRIPT_FILENAME。上例中PATH_INFO为notexist.php,当cgi.fix_pathinfo打开的情况下,在映射URI时将递归查询路径确认文件的合法性,因为notexist.php不存在,所以将往前递归查询路径。(这个功能原本是为了解决/info.php/test这种URL,使其能够正确地解析到info.php上),此时SCRIPT_FILENAME需要检查文件是否存在,最终递归查询后确认为/path/test.jpg,而PATH_INFO此时还是notexist.php,从而造成最终执行时test.jpg会被当做PHP进行解析。

文件包含漏洞

文件包含漏洞是代码注入的一种,原理就是注入一段用户能够控制的脚本或代码,并让服务器端执行。常见的导致文件包含的函数包括:include()、include_once()、require()、require_once()、fopen()、readfile()等。

当使用include()、include_once()、require()、require_once()这4个函数包含一个新的文件时,该文件将作为PHP代码执行,PHP内核并不会在意该被包含的文件是什么类型。

本地文件包含 即能够打开并包含本地文件的漏洞。 通常服务器端代码可能这样接收用户输入:

include ‘/home/wwwrun/’ .$file. '.php';

看起来无论用户输入什么$file,最终只能包含’.php’文件。实际PHP内核由C语言实现,使用了一些C语言的字符串处理函数,在连接字符串时\x00将作为字符串结束符,所以当攻击者如下输入时:

../../etc/passwd\0

或者在通过Web输入时UrlEncode:

../../etc/passwd%00

就能截断file变量之后的字符串

当PHP配置了open_basedir时,将可以使得这种攻击无效。open_basedir的作用是限制在某个特定目录下PHP能打开的文件,需要注意的是其值是目录的前缀,因此如果设置为/home/app/aaa,那么实际上如下目录都将被允许:

/home/app/aaa
/home/app/aaabbb
/home/app/aaa123

如果要限定一个指定的目录,则需要在最后加上’/’:

open_basedir = /home/app/aaa/

要解决文件包含漏洞应该尽量避免包含动态的变量,尤其是用户可以控制的变量,一种通用方式是使用枚举判断被包含的文件。

远程文件包含 如果PHP设置中allow_url_include为ON,则include、require函数可以加载远程文件,远程文件漏洞可以用来执行任意命令。

本地文件包含漏洞的利用技巧

    1. 包含用户上传的文件;
    1. 包含data://或php://input等伪协议;
    1. 包含Session文件;(需要攻击者能够控制Session文件的部分内容)
    1. 包含日志文件,比如Web Server的access log;(攻击者可以拼凑特定请求来将PHP代码写到日志中)
    1. 包含/proc/self/environ文件;(Web进程运行时的环境变量,其中很多都是用户可以控制的,比如User-Agent)
    1. 包含上传的临时文件;
    1. 包含其他应用程序创建的文件,比如数据库文件、缓存文件、应用日志等。

变量覆盖漏洞

当register_globals为ON时,PHP全局变量的来源可能是多个地方,包括页面的表单、Cookie等(会自动创建全局变量)。PHP4.2.0之后的版本register_globals默认为OFF。

extract()函数能够将变量从数组导入当前的符号表:

int extract( array $var_array [, int $extract_type [, string $prefix]] );

$extract_type指定将变量导入符号表时的行为,为EXTR_OVERWRITE时,若发生变量名冲突,将覆盖已有的变量。当extract()函数从用户可以控制的数组中导出变量时,将可能覆盖变量,从而绕过服务器端逻辑。

需要注意类似$$k的变量赋值方式可能覆盖已有的变量。

import_request_variables(…)函数将GET、POST、Cookie中的变量导入到全局,因而也可能导致变量覆盖问题。

parse_str()函数往往用于解析URL中的query string,当参数值能被用户控制时,很可能导致变量覆盖。

代码执行漏洞

PHP中popen()、system()、passthru()、exec()等函数都可以直接执行系统命令,eval()函数可以执行PHP代码。当攻击者可以控制输入时,将造成代码执行漏洞。

preg_replace()的第一个参数如果存在/e模式修饰符,则允许代码执行。

用户自定义的动态函数也能导致代码执行:

$dyn_func = $_GET['dyn_func'];
$argument = $_GET['argument'];
$dyn_func($argument);

Curly Syntax也能导致代码执行,它将执行花括号间的代码并将结果替换回去:

$var = "I was ...until $(`ls`) appeared here";

很多函数都可以执行回调函数,当回调函数可以被用户控制时,将导致代码执行。 unserialize()在执行时如果定义了__destruct()或者__wakeup()函数,则这两个函数将执行。

定制安全的PHP环境

    1. register_globals = OFF
    1. 设置open_basedir
    1. allow_url_fopen = Off
    1. allow_url_include = Off
    1. display_errors = Off / log_errors = On
    1. magic_quotes_gpc = Off 因为有若干种方法可以绕过它,甚至由于它反而衍生出一些新的安全问题
    1. cgi.fix_pathinfo = 0
    1. session.cookie_httponly = 1
    1. session.cookie_secure = 1 # 当全站HTTPS时,应该开启

safe_mode 开启时,很多函数的行为将发生变化,略。

disable_functions 应该禁止使用一些函数,详细列表略。

延迟静态绑定

延迟静态绑定,就是把本来在定义阶段固定下来的表达式或变量,改在执行阶段才决定,比如当一个子类继承了父类的静态表达式的时候,它的值并不能被改变,有时不希望看到这种情况。

class A{
    public static function echoClass(){
      echo __CLASS__;
    }

    public static function test(){
      self::echoClass();      
    }
  }

  class B extends A{      
    public static function echoClass(){
      echo __CLASS__;
    }
  }

  B::test(); //输出A

延迟静态绑定使用了static关键字,而不是self:

class A{
    public static function echoClass(){
      echo __CLASS__;
    }

    public static function test(){
      static::echoClass();      
    }
  }

  class B extends A{      
    public static function echoClass(){
      echo __CLASS__;
    }
  }

  B::test(); //输出B

克隆对象

PHP提供了clone关键字: $b = clone $a; // 将创建与$a具有相同类的副本,而且具有相同的属性值 要想改变默认的克隆行为,需要重定义__clone()方法。

PHP会话控制

PHP的会话是通过唯一的会话ID来驱动的,会话ID是一个加密的随机数字串,它由PHP生成,在会话的生命周期中都会保存在客户端。可以通过cookie或者url来在网络上传递。

脱离cookie来使用session

有时候会遇到因cookie无法使用从而导致session变量不能跨页传递。原因可能有: 客户端禁用了cookie; 浏览器出现问题,暂时无法存取cookie; php.ini中的session.use_trans_sid = 0,或者编译时没有打开--enable-trans-sid选项。 当代码session_start()时,就在服务器上产生了一个session文件,随之也产生了一个唯一对应的sessionID。跨页后,为了使用session,必须又执行session_start(),将又会产生一个session文件以及新的sessionID,这个新的sessionID无法用来获取前一个sessionID设置的值。除非在session_start()之前加代码session_id($session_id);将不会产生session文件,而是直接读取与这个id对应的session文件。

假设cookie功能不可用,可以通过如下方式实现抛开cookie来使用session: 设置php.ini中的session.use_trans_sid =1,或者编译时打开–enable-trans-sid选项,让PHP自动跨页传递sessionID(基于URL); 手动通过URL传值、隐藏表单传递sessionID,即约定一个键名,读出后手工赋值给session_id();函数; 用文件、数据库等形式保存sessionID,在跨页中手工调用。

注: 清除所有cookie后,加载本地127.0.0.1/test.php,内容如下:

<?php
  session_start(); 
  $_SESSION['var1']="value"; 
  $url="<a href="."'test2.php'>link</a>"; 
  echo $url; 
?>

返回HTTP头部如下:

HTTP/1.1 200 OK
Date: Sat, 09 May 2015 09:30:03 GMT
Server: Apache/2.4.9 (Win64) PHP/5.5.12
X-Powered-By: PHP/5.5.12
Set-Cookie: PHPSESSID=4j6ngd4o2vgeq8bj1otluvvih2; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 28
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html

仅设置session.use_trans_sid =1可能无效,php.ini中还有另外两个选项:

session.use_cookies = 1
session.use_only_cookies = 1

因此如果想要在cookie可用时则使用cookie,不可用时则使用url重写,则应该进行如下配置:

session.use_trans_sid =1
session.use_cookies = 1
session.use_only_cookies = 0

或者在php中:

ini_set(“session.use_trans_sid”,”1”);
ini_set(“session.use_only_cookies”,”0”);
ini_set(“session.use_cookies”,”1”);

开始会话:

开始一个会话有2种方法:

  • session_start()函数:该函数将检查是否有一个会话ID存在,如果不存在就创建一个,如果已经存在,就将这个已经注册的会话变量载入以便使用。
  • 将PHP设置为当有用户访问网站的时候就自动启动一个会话,具体方法是打开php.ini中的session.auto_start选项。这种方法有一个很大的缺点:无法使用对象作为会话变量,因为该对象的类定义必须在创建该对象的会话开始之前载入。

创建会话变量: 可以通过全局数组$_SESSION来注册新的会话变量,如:

$_SESSION[new_var] = value;

会话变量创建后,只有在会话结束,或者手动重置时才会失效。此外,php.ini中的gc_maxlifetime指令确定了会话的持续时间,超时后会话将会被垃圾回收。

使用会话变量:

if(isset($_SESSION[myvar])){
...

销毁会话变量:

unset($_SESSION[myvar]);

一次销毁所有的会话变量:

$_SESSION = array();

清除会话ID:

session_destroy();

配置会话控制:

  • session.auto_start 自动启动会话,默认为0

  • session.cache_expire 为缓存中的会话页设置当前时间 默认180分钟

  • session.cookie_domain 指定会话cookie中的域 默认为none

  • session.cookie_lifetime 指定cookie在用户机器上延续多久,默认值为0,表示延续到浏览器关闭

  • session.cookie_path 指定会话cookie中要设置的路径

  • session.name 会话的名称,在用户系统中用作会话名,默认为PHPSESSID

  • session.save_handler 定义会话数据保存的地方,可以将其设置为指向一个数据库,但是要编写自己函数,默认值为files

  • session.save_path 会话数据存储的路径

  • session.use_cookie 在客户端使用cookie的会话

  • session.cookie_secure 是否该在安全连接中发送cookie

  • session.hash_function 指定用来生成会话ID的哈希算法,如MD5,SHA1

PHP会在输出时自动删除其结束符?>后的一个换行

例1:

<?php   
  echo "XXX";
?>YYY

输出: XXXYYY

例2:一个换行,会被直接删除

<?php   
  echo "XXX";
?>
YYY

输出: XXXYYY

例3:多个换行,效果等同于一个空格

<?php   
  echo "XXX";
?>

YYY

输出: XXX YYY

PHP基本语法注意点

    1. PHP也允许使用短标记,但不鼓励使用。只有通过激活php.ini中的short_open_tag配置指令或者在编译PHP时使用了配置选项–enable-short-tags 时才能使用短标记。ASP 风格标记仅在通过php.ini 配置文件中的指令asp_tags打开后才可用。
    1. 如果文件内容是纯PHP代码,最好在文件末尾删除PHP结束标记。这可以避免在PHP结束标记之后万一意外加入了空格或者换行符,会导致PHP开始输出这些空白,而脚本中此时并无输出的意图。
    1. 当PHP解释器碰到?>结束标记时就简单地将其后内容原样输出,直到碰到下一个开始标记;例外是处于条件语句中间时,此时 PHP 解释器会根据条件判断来决定哪些输出,哪些跳过:
      <?php if ($expression == true): ?>
      This will show if the expression is true.
      <?php else: ?>
      Otherwise this will show.
      <?php endif; ?> 
      

      上例中PHP将跳过条件语句未达成的段落,即使该段落位于 PHP 开始和结束标记之外。

    1. 如果将PHP嵌入到XML或XHTML中则需要使用标记以保持符合标准。
    1. 一段PHP代码中的结束标记隐含表示了一个分号;在一个PHP代码段中的最后一行可以不用分号结束。
    1. 如果文件内容是纯PHP代码,最好在文件末尾删除PHP结束标记 例:
      <?php   
      echo "XXX";
      ?> 
      

      这里在结尾处多了一个空格和一个换行 将输出:

XXX

即,在XXX后面多了一个空格,并换行

而:

<?php   
  echo "XXX";

将输出:

XXX
    1. 一段PHP代码中的结束标记隐含表示了一个分号 例:
      <?php   
      echo "XXX"
      

      输出: Parse error: syntax error, unexpected $end, expecting ‘,’ or ‘;’ in D:\wamp\www\t.php on line 2

<?php   
  echo "XXX"
?>

输出: XXX

在进行布尔运算时被认为false的值

当转换为boolean时,除以下值外,其他所有的值都会被认为是TRUE。被认为是FALSE的值包括:

  • (1)布尔值 FALSE 本身

  • (2)整型值 0(零)

  • (3)浮点型值 0.0(零)

  • (4)空字符串,以及字符串 “0”

  • (5)不包括任何元素的数组

  • (6)不包括任何成员变量的对象(仅 PHP 4.0 适用)

  • (7)特殊类型 NULL(包括尚未赋值的变量)

  • (8)从空标记生成的SimpleXML对象

如果给定的一个数超出了integer的范围,将会被解释为float

<?php
$large_number = 2147483647;
var_dump($large_number);  // int(2147483647)

$large_number = 2147483648;
var_dump($large_number);  // float(2147483648)

$million = 1000000;
$large_number =  50000 * $million;
var_dump($large_number);  // float(50000000000)
?> 

PHP中没有整除的运算符

PHP中没有整除的运算符。1/2 产生出float 0.5。值可以舍弃小数部分强制转换为integer,或者使用round()函数可以更好地进行四舍五入。

<?php
var_dump(25/7);           // float(3.5714285714286) 
var_dump((int) (25/7));   // int(3)
var_dump(round(25/7));    // float(4) 
?> 

浮点数的精度有限

尽管取决于系统,PHP 通常使用IEEE 754双精度格式,则由于取整而导致的最大相对误差为1.11e-16。非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递。以十进制能够精确表示的有理数如0.1或0.7,无论有多少尾数都不能被内部所使用的二进制精确表示,因此不能在不丢失一点点精度的情况下转换为二进制的格式。 如:

<?php
echo (int) ( (0.1+0.7) * 10 ); // 显示 7!

因为该结果内部的表示其实是类似 7.9999999999999991118…

字符串中的变量解析的规则

当字符串用双引号或 heredoc 结构定义时,其中的变量将会被解析。这里有两种变量解析的规则:

  • (1)简单规则:在一个string中嵌入一个变量,一个array的值,或一个object的属性。
  • (2)复杂规则:不是因为其语法复杂而得名,而是因为它可以借助花括号紧接美元符号({$)来使用复杂的表达式: 例:
//只有通过花括号语法才能正确解析带引号的键名
echo "This works: {$arr['key']}";

//当在字符串中使用多重数组时,一定要用括号将它括起来
echo "This works: {$arr['foo'][3]}";

注意:$必须紧挨着{ 例:

$great = 'fantastic';
// {和$之间多了一个空格,无效,输出: This is { fantastic}
echo "This is { $great}";
// 有效,输出: This is fantastic
echo "This is {$great}";

PHP中的字符串是二进制安全的

PHP中的string的实现方式是一个由字节组成的数组再加上一个整数指明缓冲区长度,并无如何将字节转换成字符的信息,由程序员来决定。因此是二进制安全的。

数组key的强制转换

  • (1)包含有合法整型值的字符串会被转换为整型。例如键名 “8” 实际会被储存为 8。但是 “08” 则不会强制转换,因为其不是一个合法的十进制数值。
  • (2)浮点数也会被转换为整型,意味着其小数部分会被舍去。例如键名 8.7 实际会被储存为 8。
  • (3)布尔值也会被转换成整型。即键名 true 实际会被储存为 1 而键名 false 会被储存为 0。
  • (4)Null 会被转换为空字符串,即键名 null 实际会被储存为 ““。
  • (5)数组和对象不能被用为键名。坚持这么做会导致警告:Illegal offset type。 例:
    $array = array(
      1    => "a",
      "1"  => "b",  
      1.5  => "c",
      true => "d",
    );
    var_dump($array);
    

    输出: array(1) { [1]=> string(1) “d” }

上例中所有的键名都被强制转换为1,所以最终值为最后一次的赋值。

unset()删除数组中的某个键将不会重建索引

unset()函数允许删除数组中的某个键。但要注意数组将不会重建索引。 如果需要删除后重建索引,可以用 array_values() 函数: ​

<?php
$a = array(1 => 'one', 2 => 'two', 3 => 'three');
unset($a[2]);
/* will produce an array that would have been defined as
   $a = array(1 => 'one', 3 => 'three');
   and NOT
   $a = array(1 => 'one', 2 =>'three');
*/

$b = array_values($a);
// Now $b is array(0 => 'one', 1 =>'three')

通过引用来拷贝数组

数组拷贝默认是值拷贝。

​```php <?php $arr1 = [2, 3]; $arr2 = $arr1; $arr2[] = 4; // $arr2 is changed, // $arr1 is still array(2, 3)

$arr3 = &$arr1; $arr3[] = 4; // now $arr1 and $arr3 are the same




## 各种类型回调函数 
 
```php
<?php
// An example callback function
function my_callback_function() {
    echo 'hello world!';
}

// An example callback method
class MyClass {
    static function myCallbackMethod() {
        echo 'Hello World!';
    }
}

// Type 1: Simple callback 最简单的回调
call_user_func('my_callback_function'); 

// Type 2: Static class method call 回调类的静态方法
call_user_func(array('MyClass', 'myCallbackMethod')); 

// Type 3: Object method call  回调对象的方法
$obj = new MyClass();
call_user_func(array($obj, 'myCallbackMethod'));

// Type 4: Static class method call (As of PHP 5.2.3)  新的回调静态方法的形式
call_user_func('MyClass::myCallbackMethod');

// Type 5: Relative static class method call (As of PHP 5.3.0) 回调父类静态方法
class A {
    public static function who() {
        echo "A\n";
    }
}

class B extends A {
    public static function who() {
        echo "B\n";
    }
}
call_user_func(array('B', 'parent::who')); // A

只有有名字的变量才可以引用赋值:

<?php
$foo = 25;
$bar = &$foo;      // 合法的赋值
$bar = &(24 * 7);  // 非法; 引用没有名字的表达式

function test()
{
   return 25;
}
$bar = &test();    // 非法

变量的作用域可以包含include或者require引入的文件。

如:

$a = 1;
include 'b.inc';

这里变量$a将会在包含文件b.inc中生效。

局部函数范围

在用户自定义函数中,一个局部函数范围将被引入。任何用于函数内部的变量按缺省情况将被限制在局部函数范围内。 如:

<?php
$a = 1; /* global scope */

function Test()
{
    echo $a; /* reference to local scope variable */
}

Test();

这个脚本不会有任何输出,因为echo语句引用了一个局部版本的变量$a,而且在这个范围内,它并没有被赋值。 PHP 中全局变量在函数中使用时必须声明为global。

对于通过表单或者Cookies传进来的变量,PHP将会自动将变量名中的点(如果有的话)替换成下划线

如:,点击后,将会加上两个变量:sub_x和sub_y。它们包含了用户点击图像的坐标。(这里浏览器发出的是sub.x和sub.y)

require和include的区别

require和include几乎完全一样,除了处理失败的方式不同。require 在出错时产生E_COMPILE_ERROR 级别的错误。换句话说将导致脚本中止而include只产生警告(E_WARNING),脚本会继续运行。

使用Swoole后PHP开发的一些变化理解

使用Swoole扩展执行的PHP脚本需要预先在服务端执行(而不是每次访问时才会执行)。 比如程序中定义了一个类A,在传统模式下每次有用户访问时,类A都需要提前编译到内存中,1万次访问就要编译1万次。而用swoole则只需要在服务端编译一次,只要Swoole的进程存在,类A就会一直存在于内存中。 再比如PHP入门时就必须要掌握的session,对于运用了Swoole扩展的PHP程序而言,完全可以用一个变量来替换。 再比如:平时写PHP代码,完全不必担心内存使用,全局变量/函数/对象等,可以随便使用,因为PHP脚本执行结束后,内存自然会自行释放掉。但用Swoole扩展的PHP程序,则必然要手动注销全局的变量/函数/对象等。 PHP在fork子进程的时候,父进程的资源连接会被子进程获得,父进程本身会断掉。要解决这个问题只能在fork之后重新建连接。 一定不可以多进程或多线程共同一个mysql或redis连接,否则消息会串。每个进程或线程创建一个mysql的连接。连接断掉也就是mysql gone away之后进行重连。子进程退出后php引擎回清理回收所有的内存,关闭所有的连接。然后由于子进程和父进程共享内存,所以父进程里建立的连接等等也会被顺带关闭掉。 fpm本身是leader follower同步阻塞模型,同一时间只能处理一个请求,支持不了异步。

如何不直接使用include返回赋值实现将一个PHP文件的执行结果赋值到一个变量中?

输出控制函数结合include来捕获其输出。 例:使用输出缓冲来将 PHP 文件包含入一个字符串

<?php

function get_include_contents($filename) {
  if (is_file($filename)) {
    ob_start();
    include $filename;
    $contents = ob_get_contents();
    ob_end_clean();
    return $contents;
  }
  return false;
}

$string = get_include_contents('somefile.php');

父类方法是protected,子类重构为private,会发生什么?

会发生fatal错误,因为继承的方法或属性只能维持或放大权限,不能缩小,比如protected重载为public是可行的。

pcntl_fork

<?php
$pid=pcntl_fork();

switch($pid){
case-1:
    echo"couldn't fork";
    break;
case0:
    echo"I'm parent";
    break;
default:
    echo"I'm child";
}

PHP的内存管理机制

php进程死锁产生的原因是什么?怎么自动排查与自动恢复?

php5.2->php7.1的各版本演进历史,新增特性等


公众号
文章目录