开始(环境准备)
新建一个项目目录,并在目录中新建文件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 |
打印值及地址,简写 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
- 尝试过
CLion
和Visual Studio
很多代码都不能进行跳转 - 需自行下载一个与
Dockerfile
中PHP
版本相同的源码用于阅读
增加扩展(可选)
- 依赖
- 下载已经安装的
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
是未定义变量,1
是null
,2
是true
,3
是false
, 不需要存储实际的值- 其它的可根据对应的类型获取相对应的成员
- 当
zval.u1.v.type=4
是IS_LONG
, 就会去获取zval.value.lval
- 当
- 引用类型
- 当
zval.u1.v.type=10
是IS_REFERENCE
,就会去获取zval.value.ref
,是一个zend_reference
类型(_zend_reference
的别名) - 而实际上
_zend_reference
结构体里有一个成员val
是zval
类型, 这个val
才是存储实际的值 - 引用变量修改实际上改的是
zval.value.ref.val
这个结构体内部的值, 因为引用变量指向zval.value.ref
的指针都是一样的, 所以都会修改成功 - 引用变量删除之后(
unset
操作), 只是把当前zval
的u1.v.type
赋值为0
,内部的引用指针还是指向实际存储的zval
- 当所有引用变量都不指向存储值时, 垃圾回收周期才会回收实际存储值的
zval
- 当
- 数组类型 (等待深入了解)
PHP
最令人感受到魅力所在的地方就是数组了- 因为其数组实现了很多语言的数据结构, 包括不限于
Map
,Queue
,Stack
. 特别是对于Map
, 并且PHP
对Map
数组提供了顺序存储, 真的是令人又爱又恨. 使用方便缺容易导致出现问题 - 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 内核与原生扩展开发