php profiler 的原理分析 - (sunznx) 振翅飞翔
27 December 2020

本文是 php-profiler 的源码分析,该程序的主要功能是读取一个正在运行中的 php 的运行状态,类似 gdb/strace 等调试软件,可以查看当前正在运行的 php 脚本执行到哪一行代码了

通过对这个程序的分析,可以知道一些调试软件的实现原理。

以下分析以 pid=30 的 php 进程进行分析

EgAddress

代码在 GetEgAddressCommand

  1. 查找 libpthread.so
    在 linux shell 下,可以通过如下命令来查找

    ➜ cat /proc/30/maps | grep .*/libpthread.*\.so$
    7f367d577000-7f367d57d000 r--p 00000000 fe:01 788027                     /lib/x86_64-linux-gnu/libpthread-2.28.so
    7f367d57d000-7f367d58c000 r-xp 00006000 fe:01 788027                     /lib/x86_64-linux-gnu/libpthread-2.28.so
    7f367d58c000-7f367d592000 r--p 00015000 fe:01 788027                     /lib/x86_64-linux-gnu/libpthread-2.28.so
    7f367d592000-7f367d593000 r--p 0001a000 fe:01 788027                     /lib/x86_64-linux-gnu/libpthread-2.28.so
    7f367d593000-7f367d594000 rw-p 0001b000 fe:01 788027                     /lib/x86_64-linux-gnu/libpthread-2.28.so
    
  2. 解析 libpthread.so 的 efi 信息
    解析 libpthread 的原因:
    默认情况下,解析一个符号信息是通过 baseAddress+相对地址 来解析的。如果是线程模式, baseAddress 要从程序的寄存器中读取,如果不是线程模式, baseAddress 在程序的 elf 信息中读取。

    如果是线程模式, baseAddress 就是 tlsBlockAddresstlsBlockAddress 是通过读取寄存器 fs_base 的值来确定的。读取另外一个程序的寄存器值是通过 ptrace 来实现的

  3. 获取 EgAddress
    baseAddress+executorGlobalsAddress 即为实际的 EgAddress

GetCurrentFunctionName

  1. 拿到 EgAddress
  2. 根据 EgAddress 拿到真正的 Eg,即 zend_executor_globals
  3. 读取 $eg->current_execute_data ,这时候拿到的是 current_execute_data 的地址
  4. 再通过 current_execute_data 的地址,读取 current_execute_data 的值
  5. 一直读地址,然后取地址对应的结构体,然后就可以知道函数名了

接下来便是拿到当前函数的信息了

这里重要的是如何根据 address 读取对应的变量信息,答案是通过 process_vm_readv 这个调用。

#include <sys/uio.h>

ssize_t process_vm_readv(pid_t pid,
                         const struct iovec *local_iov,
                         unsigned long liovcnt,
                         const struct iovec *remote_iov,
                         unsigned long riovcnt,
                         unsigned long flags);

ssize_t process_vm_writev(pid_t pid,
                          const struct iovec *local_iov,
                          unsigned long liovcnt,
                          const struct iovec *remote_iov,
                          unsigned long riovcnt,
                          unsigned long flags);

struct iovec {
    void  *iov_base;    /* Starting address */
    size_t iov_len;     /* Number of bytes to transfer */
};

通过上面这 2 个系统调用,我们可以知道,系统提供了能力可以让我们去 获取/写入 另外一个进程的内存信息,只要我们知道了内存地址。内存地址可以通过程序的 binary path(例如 readelf -a /usr/local/bin/php) 的 efi 信息中读取

GetTrace

直接读取 execute_data->prev_execute_data ,然后解析