Featured image of post PHP 源码阅读

PHP 源码阅读

深入了解 PHP

开始(环境准备)

新建一个项目目录,并在目录中新建文件Dockerfile

FROM centos:7

# 安装依赖工具
RUN yum -y install gcc gcc-c++ gdb autoconf libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel glibc glibc-devel glib2 glib2-devel bzip2 bzip2-devel ncurses ncurses-devel curl curl-devel e2fsprogs e2fsprogs-devel krb5 krb5-devel libidn libidn-devel openssl openssl-devel openldap openldap-devel nss_ldap openldap-clients openldap-servers gd gd2 gd-devel gd2-devel perl-CPAN pcre-devel libicu-devel wget

# 下载指定版本源码,如果需要调试其它版本,可自行切换
RUN wget -O /tmp/php.tar.gz https://www.php.net/distributions/php-7.1.0.tar.gz
RUN mkdir ~/php71 && tar -xvf /tmp/php.tar.gz --strip-components 1  -C ~/php71

# 安装目录 /var/php71
# 源码目录 /var/www
###################################################################
# 1. 生成 Makefile (看是否要指定安装目录, 和开启的扩展, 这里安装到了 /var/php71)
# 2. 编译(根据生成的 Makefile)
# 3/ 安装(执行 Makefile 中的 install部分)
RUN cd ~/php71 && \
    ./configure --prefix=/var/php71 --enable-fpm --enable-debug --enable-phpdbg-debug CFLAGS="-g3 -gdwarf-4" && \
    make && \
    make install

# 1. 复制 php 配置文件
# 2. 复制 fpm 主配置文件
# 3. 加入环境变量
RUN cp ~/php71/php.ini-production /var/php71/lib/php.ini  && \
    cp /var/php71/etc/php-fpm.conf.default /var/php71/etc/php-fpm.conf  && \
    echo $'export PATH=$PATH:/var/php71/bin:/var/php71/sbin' >> ~/.bashrc
#
## 安装 nginx, 用于调试 php-fpm
RUN yum -y install epel-release && \
    yum -y install nginx

## 针对 nginx 对 fpm 的配置
RUN echo $'server {\n\
               listen       9999;\n\
               root         /var/www;\n\
               # \n\
               location / {\n\
                   root   /var/www;\n\
                   index  index.php index.html index.htm;\n\
               }\n\
               # \n\
               error_page   500 502 503 504  /50x.html;\n\
               # \n\
               location ~ \.php$ {\n\
                   fastcgi_pass   127.0.0.1:9000;\n\
                   fastcgi_index  index.php;\n\
                   fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;\n\
                   include        fastcgi_params;\n\
               }\n\
}' > /etc/nginx/conf.d/fpm.conf

## fpm 的配置
RUN echo $'[www]\n\
           user = nobody\n\
           group = nobody\n\
           listen = 127.0.0.1:9000\n\
           pm = static\n\
           pm.max_children = 1' > var/php71/etc/php-fpm.d/www.conf

在项目目录中新建文件docker-compose.yml

version: '3'
services:
  centos:
    build: ./
    tty: true
    # 不开启这个会导致`debug php-fpm`进程的时候会提示`No symbol table is loaded`
    cap_add:
      - SYS_PTRACE
    working_dir: /var/www
    volumes:
      - ./:/var/www
    ports:
      - 9999:9999

构建容器并启动

  • docker-compose up -d && docker-compose exec centos bash

使用GDB调试

docker-compose exec centos bash
// tui 模式运行 也可先调试, 然后 CTRL+X+A
gdb --tui
// 调试可执行文件
gdb php
// 调试进程号
gdb --pid=xxx
常用命令 说明
run 重新开始运行文件
start 单步执行,运行程序,停在第一执行语句
list 查看原代码,简写 l
set 设置变量的值
next 单步调试(逐过程,函数直接执行), 简写 n
step 单步调试(逐语句:跳入自定义函数内部执行), 简写 s
break 断点, 简写 b, 参数 function filename:linenum filename:function
delete 删除断点,可跟断点 number
finish 结束当前函数,返回到函数调用点
continue 继续运行,简写 c
print 打印值及地址,简写 p
quit 退出 gdb, 简写 q
info 查看函数内部局部变量的数值,简写 i

调试php-fpm

  • php-fpm已设置为只有一个worker进程,方便跟踪调试)
  • 宿主机项目目录可直接新建文件,已挂载进容器
docker-compose exec centos bash
php-fpm
nginx

# 查看 worker 进程号
ps aux | grep fpm
gdb --pid=xxx

阅读工具

  • 推荐使用Understand
  • 尝试过CLionVisual Studio 很多代码都不能进行跳转
  • 需自行下载一个与DockerfilePHP版本相同的源码用于阅读

增加扩展(可选)

  • 依赖
    • 下载已经安装的PHP按本的PHP源码
  • 进入扩展源码目录比如curl
    • cd ~/php71/ext/curl
  • 执行phpize(编译PHP扩展的工具,主要是根据系统信息生成对应的configure文件)
    • /var/php71/bin/phpize
  • 生成Makefile
    • ./configure -with-php-config=/var/php71/bin/php-config
  • 编译 && 安装
    • make
    • make install

字节对齐


## 假设默认对齐 4 个字节
struct A 
{
    int a;
    char b;
    short c;
    char d;
}

成员 a 占用 4 个字节 
成员 b 占用 1 个字节
成员 c 占用 2 个字节, 对齐是 2n (b 成员后的填空 1 个字节)
成员 d 占用 1 个字节, 偏移 8
最后填充的字节为默认字节位填满, 就是填充空到 11
总占用字节为: 0 ~ 11 = 12 个字节
## c 是找到 2n 的位置
aaaa b0cc d000

大小端模式

  • 大端小端是不同的字节顺序存储方式,统称为字节序
  • 假设一个数值为0x1A2B3C4D
  • 大端存储
    • 0x1A | 0x2B | 0x3C | 0x4D
    • 即高位字节放在内存的低地址端
    • 低位字节放在内存的高地址端
  • 小端模式
    • 0x4D | 0x3C | 0x2B | 0x1A
    • 即低位字节放在内存的低地址端
    • 高位字节放在内存的高地址端

变量存储

// 其他文件会使用
typedef struct _zend_refcounted zend_refcounted;
typedef struct _zend_string     zend_string;
typedef struct _zend_array      zend_array;
typedef struct _zend_object     zend_object;
typedef struct _zend_resource   zend_resource;
typedef struct _zend_reference  zend_reference;
typedef struct _zend_ast_ref    zend_ast_ref;
typedef struct _zend_ast        zend_ast;

// 只在当前 zend_types.h 文件使用
typedef union _zend_value {
	zend_long         lval;				/* long value */
	double            dval;				/* double value */
	zend_refcounted  *counted;
	zend_string      *str;
	zend_array       *arr;
	zend_object      *obj;
	zend_resource    *res;
	zend_reference   *ref;
	zend_ast_ref     *ast;
	zval             *zv;
	void             *ptr;
	zend_class_entry *ce;
	zend_function    *func;
	struct {
		uint32_t w1;
		uint32_t w2;
	} ww;
} zend_value;

  • zval所有变量的实现(zval_zval_struct的别名)
    • value[zend_value]存储变量的值
    • u1[union] 存储变量类型
    • u2[union] 存储扩展字段
  • zend_value (zend_value_zend_value的别名)
    • 大部分类型都能通过zval.u1去获取到对应的类型值
    • zval.u1.v.type有几种特殊值,0是未定义变量,1null,2true,3false, 不需要存储实际的值
    • 其它的可根据对应的类型获取相对应的成员
      • zval.u1.v.type=4IS_LONG, 就会去获取zval.value.lval
    • 引用类型
      • zval.u1.v.type=10IS_REFERENCE,就会去获取zval.value.ref,是一个zend_reference类型(_zend_reference的别名)
      • 而实际上_zend_reference结构体里有一个成员valzval类型, 这个val才是存储实际的值
      • 引用变量修改实际上改的是zval.value.ref.val这个结构体内部的值, 因为引用变量指向zval.value.ref的指针都是一样的, 所以都会修改成功
      • 引用变量删除之后(unset操作), 只是把当前zvalu1.v.type赋值为0,内部的引用指针还是指向实际存储的zval
      • 当所有引用变量都不指向存储值时, 垃圾回收周期才会回收实际存储值的zval
    • 数组类型 (等待深入了解)
      • PHP最令人感受到魅力所在的地方就是数组了
      • 因为其数组实现了很多语言的数据结构, 包括不限于Map,Queue, Stack. 特别是对于Map, 并且PHPMap数组提供了顺序存储, 真的是令人又爱又恨. 使用方便缺容易导致出现问题
      • PHP7 数组的底层实现
      • PHP 数组底层实现
               zval.value.arr.arData
                        ─┐
                         │
                         │
                         │
                         │
                         ▼
       ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
       │  │  │  │  │  │  │  │  │  │  │  │  │
───────┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴─────────►
      -6 -5 -4 -3 -2 -1  0  1  2  3  4  5  6
       │                 │                 │
       │                 ├─────────────────┘
       │                 │   bucket  size
       │                 │
       └─────────────────┘

          map table size

PHP 代码运行过程

  • 计算机只能识别机器码
    • 编译型语言: 可以先把代码转成机器码再执行
    • 脚本型语言: 如PHP是运行时进行解释或编译
  • 词法扫描分析: 将源文件转换成token
  • 语法分析: 从token流生成抽象语法树(AST)
  • 编译过程: 从抽象语法树生成op code
  • zend虚拟机把op code转成机器码执行

生命周期

CLI

php_module_startup   模块初始化阶段
        │
        ▼
php_request_startup  请求初始化阶段
        │
        ▼
php_execute_script   脚本执行阶段
        │
        ▼
php_request_shutdown 请求关闭阶段
        │
        ▼
php_module_shutdown  模块关闭阶段

FPM

php_module_startup
        │
        ▼
fcgi_accept_request ◄──────┐
        │                  │
        │                  │
        ▼                  │
php_request_startup        │
        │                  │
        │                  │
        ▼                  │
fpm_request_executing      │
        │                  │
        ▼                  │
php_execute_script         │
        │                  │
        │                  │
        ▼                  │
fpm_request_end            │
        │                  │
        │                  │
        │                  │
        ▼                  │
php_request_shutdown       │
        │                  │
        ▼                  │
php_module_shutdown────────┘
  • 进程管理
    • kill fpm-master: master进程会同时把worker进程杀死, 服务不可访问
    • kill -9 fpm-master: master进程直接被杀死, worker进程还存活, 可提供服务
    • kill fpm-worker: worker进程被杀死不影响,master进程会重新调度管理

常见问题

  • 以单下划线_表明是标准库的变量
  • 双下划线__开头表明是编译器的变量
  • typedef说明
    • 如果要在其他文件使用, 会在头文件最开始定义
    • 如果只在当前文件使用, 那么会在结构体声明的时候直接紧随
  • 部分结构体(如zend_string)中字符串为什么不是char *,而是char[1]
    • 关键字查询C struct hack是一种把结构体所有成员分配在同一块内存的技术, 利于cpu cache,也是一种可变长数组的实现方式
    • 网上有些例子会写成char[0], 但是 some compilers would allow [0] here
    • C struct hack at work

学习

php-fpm Remote Code Execution 分析(CVE-2019-11043) gdb in docker container returns “ptrace: Operation not permitted.” FastCGI进程管理器(FPM) PHP 内核与原生扩展开发