PHP 7.4/8.x FFI的使用例子

本篇主要讲例子,至于关于FFI的知识点,可以参考以下文档:

官网文档FFI介绍入口 http://php.p2hp.com/manual/zh/class.ffi.php

https://www.kancloud.cn/a173512/php_note/1690512

鸟哥关于FFI的文章 https://www.laruence.com/2020/03/11/5475.html

cpp命令 https://blog.csdn.net/leibris/article/details/120195897

cpp命令 https://blog.csdn.net/he3913/article/details/2031050

首先检查 ffi 扩展是否已经安装 (配置文件是否存在,动态库是否存在)。ffi包含在 php8.0-common 软件包中,因此一般都是安装了的,毕竟它是语言基本特性。

sjg@sjg-PC:~/garbage$ php -r "phpinfo();" | grep ffi
/etc/php/8.0/cli/conf.d/20-ffi.ini,
ffi.enable => preload => preload
ffi.preload => no value => no value
sjg@sjg-PC:~/garbage$ php -r "phpinfo();" | grep extension_dir
extension_dir => /usr/lib/php/20200930 => /usr/lib/php/20200930
sqlite3.extension_dir => no value => no value
sjg@sjg-PC:~/garbage$ ll /usr/lib/php/20200930/ | grep ffi
-rw-r--r-- 1 root root  165992 2月  15 02:29 ffi.so

我们来写一个动态库,它实现加盐的 SHA1算法(其实就是把RFC3174抄写一遍稍加改写),如果没有指定盐,就使用默认盐zime87772676。打开Clion,创建项目类型为 C Library,然后头文件library.h 和实现文件 library.c 如下:

#ifndef MYSHA1SO_LIBRARY_H
#define MYSHA1SO_LIBRARY_H

#include <stdint-gcc.h>

enum {
    shaSuccess = 0,
    shaNull,            /* Null pointer parameter */
    shaInputTooLong,    /* input data too long */
    shaStateError       /* called Input after Result */
};
#define SHA1HashSize 20   // sha1 结果为 20字节 长度,常表示成 40个 16进制数字

/*
 * 消息长度指的是 bit数, bit数 是8的倍数就可以用 16进制数字 表达
 * SHA1要求消息长度为512的倍数,即 512*n bit,不是512的倍数就需要后续“补白操作”
 * 补白操作步骤:设原始消息长度为l,则在后面先补白1个bit “1”,然后是一堆 “0”,再
 * 补上64bit来表示原始消息长度l,具体中间补多少个 “0”,是要保证最后补上的64bit(8字节)
 * 凑齐总共 512*n bit 或者说 16*n 个 word(32-bit),亦或 64*n 个字节
 * 原始消息长度l存放时是Big-Endian的(左边字节是高位)
 */
typedef struct SHA1Context {
    uint32_t Intermediate_Hash[SHA1HashSize / 4]; /* Message Digest 中间摘要或最终摘要  */

    uint32_t Length_Low;            /* Message length in bits      */
    uint32_t Length_High;           /* Message length in bits      */

    /* Index into message block array   block缓冲区 为 512-bit,即 64 字节   */
    int_least16_t Message_Block_Index;
    uint8_t Message_Block[64];      /* 512-bit message blocks */

    int Computed;               /* Is the digest computed?         */
    int Corrupted;             /* Is the message digest corrupted? */
} SHA1Context;

int SHA1Reset(SHA1Context *);

int SHA1Input(SHA1Context *, const uint8_t *, unsigned int);

int SHA1Result(SHA1Context *, uint8_t Message_Digest[SHA1HashSize]);

void SHA1ProcessMessageBlock(SHA1Context *context);

void SHA1PadMessage(SHA1Context *context);

uint32_t mySHA1(const uint8_t *in, uint8_t *out, const char *salt);

#endif
#include "library.h"

#include <stdio.h>
#include <string.h>


// 参考 https://www.ietf.org/rfc/rfc3174.txt

// 一个 word 为 32-bit,可以表示 0 到 2^32-1 的整数,以下操作为 32-bit word的循环左移操作
#define SHA1CircularShift(bits, word) \
                (((word) << (bits)) | ((word) >> (32-(bits))))

//void SHA1PadMessage(SHA1Context *);
//
//void SHA1ProcessMessageBlock(SHA1Context *);

int SHA1Reset(SHA1Context *context) {               // 初始化 context
    if (!context) {
        return shaNull;
    }

    context->Length_Low = 0;
    context->Length_High = 0;
    context->Message_Block_Index = 0;

    context->Intermediate_Hash[0] = 0x67452301;     // H0
    context->Intermediate_Hash[1] = 0xEFCDAB89;     // H1
    context->Intermediate_Hash[2] = 0x98BADCFE;     // H2
    context->Intermediate_Hash[3] = 0x10325476;     // H3
    context->Intermediate_Hash[4] = 0xC3D2E1F0;     // H4

    context->Computed = 0;
    context->Corrupted = 0;

    return shaSuccess;
}

int SHA1Result(SHA1Context *context, uint8_t Message_Digest[SHA1HashSize]) {
    int i;

    if (!context || !Message_Digest) {
        return shaNull;
    }

    if (context->Corrupted) {
        return context->Corrupted;
    }

    if (!context->Computed) {     // 未计算过
        SHA1PadMessage(context);  // 可能最终长度不满足要求,补白(包含了摘要计算)
        for (i = 0; i < 64; ++i) {
            /* message may be sensitive, clear it out 计算完清除原始消息块 */
            context->Message_Block[i] = 0;
        }
        context->Length_Low = 0;    /* and clear length */
        context->Length_High = 0;
        context->Computed = 1;    // 标记“已经计算”
    }
    // H0 >> 24, H0 >> 16, H0 >> 8, H0 >> 0, H1 >> 24, H1 >> 16, H1 >> 8 ...
    for (i = 0; i < SHA1HashSize; ++i) {
        Message_Digest[i] = context->Intermediate_Hash[i >> 2]
                >> 8 * (3 - (i & 0x03));
    }

    return shaSuccess;
}

int SHA1Input(SHA1Context *context, const uint8_t *message_array, unsigned length) {
    if (!length) {
        return shaSuccess;
    }

    if (!context || !message_array) {
        return shaNull;
    }

    if (context->Computed) {
        context->Corrupted = shaStateError;
        return shaStateError;
    }

    if (context->Corrupted) {
        return context->Corrupted;
    }

    while (length-- && !context->Corrupted) {
        context->Message_Block[context->Message_Block_Index++] =
                (*message_array & 0xFF);

        context->Length_Low += 8;
        if (context->Length_Low == 0) {
            context->Length_High++;
            if (context->Length_High == 0) {
                /* Message is too long */
                context->Corrupted = 1;
            }
        }

        if (context->Message_Block_Index == 64) {  // 缓冲区满,先计算摘要
            SHA1ProcessMessageBlock(context);
        }

        message_array++;  // 下一字节
    }

    return shaSuccess;
}

void SHA1ProcessMessageBlock(SHA1Context *context) {
    const uint32_t K[] = {       /* Constants defined in SHA-1, K(t)=K[t/20] */
            0x5A827999,     // for t in [0, 19]
            0x6ED9EBA1,     // for t in [20, 39]
            0x8F1BBCDC,     // for t in [40, 59]
            0xCA62C1D6      // for t in [60, 79]
    };
    int t;                 /* Loop counter                */
    uint32_t temp;              /* Temporary word value        */
    uint32_t W[80];             /* Word sequence               */
    uint32_t A, B, C, D, E;     /* Word buffers                */

    /*
     *  Initialize the first 16 words in the array W
     */
    for (t = 0; t < 16; t++) {    // W[0]...W[15] 相当于从块缓冲读入 16个 32-bit 整数(左边高位)
        W[t] = context->Message_Block[t * 4] << 24;
        W[t] |= context->Message_Block[t * 4 + 1] << 16;
        W[t] |= context->Message_Block[t * 4 + 2] << 8;
        W[t] |= context->Message_Block[t * 4 + 3];
    }

    for (t = 16; t < 80; t++) {  // W[16]...W[79] 是 0x1 循环左移 x位 得到,x依赖前面16个整数
        W[t] = SHA1CircularShift(1, W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16]);
    }

    A = context->Intermediate_Hash[0];
    B = context->Intermediate_Hash[1];
    C = context->Intermediate_Hash[2];
    D = context->Intermediate_Hash[3];
    E = context->Intermediate_Hash[4];

    for (t = 0; t < 20; t++) {
        temp = SHA1CircularShift(5, A) +
               ((B & C) | ((~B) & D)) + E + W[t] + K[0];   // f(t;B,C,D), where t in [0,19]
        E = D;
        D = C;
        C = SHA1CircularShift(30, B);
        B = A;
        A = temp;
    }

    for (t = 20; t < 40; t++) {
        temp = SHA1CircularShift(5, A) +
                (B ^ C ^ D) + E + W[t] + K[1];          // f(t;B,C,D), where t in [20,39]
        E = D;
        D = C;
        C = SHA1CircularShift(30, B);
        B = A;
        A = temp;
    }

    for (t = 40; t < 60; t++) {
        temp = SHA1CircularShift(5, A) +
               ((B & C) | (B & D) | (C & D)) + E + W[t] + K[2];// f(t;B,C,D), where t in [40,59]
        E = D;
        D = C;
        C = SHA1CircularShift(30, B);
        B = A;
        A = temp;
    }

    for (t = 60; t < 80; t++) {
        temp = SHA1CircularShift(5, A) +
                (B ^ C ^ D) + E + W[t] + K[3];              // f(t;B,C,D), where t in [60,79]
        E = D;
        D = C;
        C = SHA1CircularShift(30, B);
        B = A;
        A = temp;
    }

    context->Intermediate_Hash[0] += A;     // H0 ~ H4 是计算后的消息摘要
    context->Intermediate_Hash[1] += B;
    context->Intermediate_Hash[2] += C;
    context->Intermediate_Hash[3] += D;
    context->Intermediate_Hash[4] += E;

    context->Message_Block_Index = 0;       // 摘要计算完总是重置消息块索引为 0
}

void SHA1PadMessage(SHA1Context *context) {     // 补白中包含了摘要计算!!
    /*
     *  Check to see if the current message block is too small to hold
     *  the initial padding bits and length.  If so, we will pad the
     *  block, process it, and then continue padding into a second
     *  block.
     */
    if (context->Message_Block_Index > 55) {  // 块缓冲比较满,不能常规补白,先计算部分摘要
        context->Message_Block[context->Message_Block_Index++] = 0x80;
        while (context->Message_Block_Index < 64) {
            context->Message_Block[context->Message_Block_Index++] = 0;
        }

        SHA1ProcessMessageBlock(context);

        while (context->Message_Block_Index < 56) {
            context->Message_Block[context->Message_Block_Index++] = 0;
        }
    } else {
        context->Message_Block[context->Message_Block_Index++] = 0x80;
        while (context->Message_Block_Index < 56) {
            context->Message_Block[context->Message_Block_Index++] = 0;
        }
    }

    /*
     *  Store the message length as the last 8 octets
     */
    context->Message_Block[56] = context->Length_High >> 24;
    context->Message_Block[57] = context->Length_High >> 16;
    context->Message_Block[58] = context->Length_High >> 8;
    context->Message_Block[59] = context->Length_High;
    context->Message_Block[60] = context->Length_Low >> 24;
    context->Message_Block[61] = context->Length_Low >> 16;
    context->Message_Block[62] = context->Length_Low >> 8;
    context->Message_Block[63] = context->Length_Low;

    SHA1ProcessMessageBlock(context);
}

const char defaultSalt[] = "zime87772676";

uint32_t mySHA1(const uint8_t *in, uint8_t *out, const char *salt) {
    char mySalt[64] = {0};
    uint8_t buf[4096];
    unsigned int len = 0;
    strcpy(mySalt, *salt != '\0' ? salt : defaultSalt);  // 使用指定的salt或默认salt (传入空串用默认串)
    uint8_t *p = buf, *q = (uint8_t *)in;
    while (*q) {        // 将 in[] 装入 buf[]
        *p = *q;
        ++p, ++q, ++len;
    }
    q = (uint8_t *)mySalt;
    while (*q)  {       // 将 mySalt[] 装入 buf[]
        *p = *q;
        ++p, ++q, ++len;
    }
    SHA1Context context;
    uint32_t myLastError = SHA1Reset(&context);  // 初始化
    if (myLastError) return myLastError;
    myLastError = SHA1Input(&context, buf, len);  // 从buf输入,包含了摘要计算
    if (myLastError) return myLastError + 100;
    myLastError = SHA1Result(&context, out);  // 输出结果到out,包含了可能的补白和摘要计算
    if (myLastError) return myLastError + 200;
    return myLastError;
}

把项目 build 生成 libmySHA1so.so (会自动在项目名前加lib,然后后缀.so)。当然,直接终端下用gcc生成.so也不会太麻烦(gcc -shared -fPIC library.c -o libmySHA1so.so),不是我们的重点,忽略。

我们先写一个C程序来大概测试一下 (library.h头文件和.so动态库拷贝到和以下文件同一目录下)

#include "library.h"
#include <stdio.h>

int main()
{
  uint8_t in[1024] = "sjg";
  uint8_t out[200];
  int i;
  const char salt[] = "beta";
  if (0 == mySHA1(in, out, salt)) {
    for (i = 0; i < 20; i++) {
      printf("%02x", out[i]);
    }
    printf("\n");
  } else {
    printf("error cacl %s%s\n", in, salt);
  }
  
  if (0 == mySHA1(in, out, "")) {
    for (i = 0; i < 20; i++) {
      printf("%02x", out[i]);
    }
    printf("\n");
  } else {
    printf("error cacl %s%s\n", in, "");
  }
  
  return 0;
}
sjg@sjg-PC:~/garbage$ gcc test1.c -L. -lmySHA1so -o test1
sjg@sjg-PC:~/garbage$ ./test1
./test1: error while loading shared libraries: libmySHA1so.so: cannot open shared object file: No such file or directory

注意:-L编译时库搜索路径 -l库名 (库名不包含前缀lib和后缀.so)。运行时,提示找不到动态库,我们要把.so文件所在路径添加到环境变量LD_LIBRARY_PATH中(动态库路径细节可以参考 https://www.bbsmax.com/A/ZOJP4lZeJv/

sjg@sjg-PC:~/garbage$ export  LD_LIBRARY_PATH=/home/sjg/garbage:$LD_LIBRARY_PATH
sjg@sjg-PC:~/garbage$ ./test1
1662f332c84db3fc0590bf39eca2a3912c9bfc27
94576415f34481c90986912998f0e3e5ba79cc4e

我们写一个php来看如何动态加载这个so里面的函数并使用。主要步骤包含创建FFI对象(相当于访问代理),创建一些C数据结构用于参数的传入传出用FFI对象调用有关函数。下面的例子中,FFI对象“包裹”了.so文件中的 mySHA1 函数,$a 是传入参数FFI\CData对象,$b和$c是传出参数FFI\CData对象。用mySHA1分别计算 密码sjg + 用户设定盐beta 和 密码sjg + 无用户设定盐(使用默认盐zime87772676),同时和PHP自身的sha1函数结果对比。

<?php

$libPath = __DIR__.'/libmySHA1so.so';  // .so库文件路径
$ffi = \FFI::cdef(<<<EOF
uint32_t mySHA1(const uint8_t *in, uint8_t *out, const char *salt);
EOF, $libPath);

$passwordText = 'sjg';
$salt = 'beta';
$a = FFI::new("uint8_t[1024]");
$b = FFI::new("uint8_t[100]");
$c = FFI::new("uint8_t[100]");
for ($i = 0; $i < strlen($passwordText); $i++)
    $a[$i] = ord($passwordText[$i]);

$error = $ffi->mySHA1($a, $b, $salt);
echo $error.PHP_EOL;

$hash = '';
for ($i = 0; $i < 20; $i++)
    $hash .= $b[$i] > 0xf ? dechex($b[$i]) : '0'.dechex($b[$i]);
echo $hash.PHP_EOL;
echo sha1($passwordText . $salt).PHP_EOL;

$error = $ffi->mySHA1($a, $c, "");
echo $error.PHP_EOL;
$hash = '';
for ($i = 0; $i < 20; $i++)
    $hash .= $c[$i] > 0xf ? dechex($c[$i]) : '0'.dechex($c[$i]);
echo $hash.PHP_EOL;
$salt = 'zime87772676';
echo sha1($passwordText.$salt).PHP_EOL;

运行结果如下:

sjg@sjg-PC:~/garbage$ php testFFI.php
0
1662f332c84db3fc0590bf39eca2a3912c9bfc27
1662f332c84db3fc0590bf39eca2a3912c9bfc27
0
94576415f34481c90986912998f0e3e5ba79cc4e
94576415f34481c90986912998f0e3e5ba79cc4e

在已经有.h头文件的情况下,自己手工去“包裹”需要用到的函数后结构,不够方便,而且当函数比较多的时候更是如此(包裹的内容并非都可以从.h拷贝就可以完事的)。我们可以利用已有的.h头文件,生成一个“中介”.h头文件,然后利用该“中介”头文件来加载创建FFI对象。

先创建“中介”头文件mySHA1.h,里面添加以下内容 (FFI::load()用于从C头文件加载声明,宏定义FFI_LIB指明哪个.so应当被加载,尽管似乎没有什么用,但似乎此种方式要求宏定义FFI_SCOPE存在)

#define FFI_SCOPE "MYSHA1"
#define FFI_LIB  "/home/sjg/garbage/libmySHA1so.so"

执行命令 cpp -P library.h >> mySHA1.h 将library.h头文件进行文件包含和宏替换预处理后,将预处理结果追加到mySHA1.h中,这样可以避免手动添加类型定义和函数声明。

最后,使用动态库中函数的例子开头部分改写成如下(后面部分代码一样)。(说明:这里的做法类似PHP8源码中测试文件 ext/ffi/tests/301.phpt 这个例子)

<?php

$ffi = FFI::load(__DIR__.'/mySHA1.h');

$passwordText = 'sjg';
$salt = 'beta';

无论是官网关于FFI的说明还是鸟哥文章,都提到FFI的性能问题:每次请求都加载动态库并使用里面的函数和数据结构是开销很高的,减轻影响的办法是使用“预加载(preload)”。

“预加载(preload)”有2种做法:一种是预加载“中介”.h头文件;另一种是使用额外的“中介”预加载php文件,用此“中介”php加载“中介”.h头文件,同时将此php缓存到opcache。

先来看第一种。修改php配置文件php.ini (可以php -r "phpinfo();" | more 查看Loaded Configuration File)差不多最后部分(事实上,我是看配置中注释部分含义来推断应该直接写.h头文件的):

[ffi]
; FFI API restriction. Possible values:
; "preload" - enabled in CLI scripts and preloaded files (default)
; "false"   - always disabled
; "true"    - always enabled
ffi.enable=true

; List of headers files to preload, wildcard patterns allowed.
ffi.preload="/home/sjg/garbage/mySHA1.h"

使用的部分只需要把创建FFI对象实例的部分改一下就行(用FFI::scope()创建),其余不变:

<?php

$ffi = FFI::scope('MYSHA1');

$passwordText = 'sjg';
$salt = 'beta';

再来看第二种。参照PHP8源码中测试文件 ext/ffi/tests/300.phpt,修改php配置文件php.ini,在[opcache]小节,设置如下 (在 opcache 预加载 ffi_preload.php文件

[opcache]
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.preload="/home/sjg/garbage/ffi_preload.php"
opcache.file_cache_only=0

而[ffi]小节只启用ffi,修改设置如下 (这里启用ffi为preload,即默认值,此时,对于CLI模式,ffi总是启用的,而非CLI模式,就是只有opcache预加载的文件才能使用ffi了,这就是鸟哥文章中说的考虑“安全”的情况了)

[ffi]
; FFI API restriction. Possible values:
; "preload" - enabled in CLI scripts and preloaded files (default)
; "false"   - always disabled
; "true"    - always enabled
ffi.enable=preload

; List of headers files to preload, wildcard patterns allowed.
;ffi.preload="/home/sjg/garbage/mySHA1.h"

ffi_preload.php文件比较简单,就是用FFI::load()加载“中介”.h文件(mySHA1.h同前)

<?php

$ffi = FFI::load(__DIR__.'/mySHA1.h');

使用的方式和前面是一样的(故代码就是前面的代码),即 $ffi = FFI::scope('MYSHA1'); 创建 FFI实例对象。

鸟哥的文章中“安全”的做法,指的是预加载的php文件中,把需要的功能打包好,然后避免FFI有关功能被其他PHP用到(除了调用我们打包的函数以外)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值