Reverse Engineering


Zend Extension Manager 是一个 Zend 公司用于统一管理该公司出品的各种 Zend 扩展的 Zend 扩展。一般来说 Zend 扩展都是高度依赖 Zend Engine 版本的,但是 Zend Extension Manager 却可以不依赖任何具体版本的 PHP 运行库,并且会根据不同的运行环境自动加载不同产品相应版本的 Zend 扩展。相信通过学习 Zend Extension Manager 的实现会对我们统一开发部署 Zend 扩展提供一些帮助。

附件就是 Zend Extension Manager v1.2.0 版本的源代码和 VC++ 工程文件。源代码是根据 ZendOptimizer-3.2.8-Windows-i386 中的 ZendExtensionManager.dll 逆向出来的。编译出来的 ZendExtensionManager.dll 可完全替代原始文件(事实上也没有任何区别)。

点击下载 Zend Extension Manager v1.2.0 源代码

对于扩展来说,最重要的一件事就是如何接收和处理那些通过函数参数传递而来的数据。大多数扩展都是用来处理某些特定的输入数据(或者是根据参数来决定进行某些特定的动作),而函数的参数则是 PHP 代码层和 C 代码层之间交换数据的唯一途径。当然,你也可以通过事先定义好的全局变量来交换数据(这个我们稍后会谈到),不过这种习惯可不太好,我们应该尽量避免。

在 PHP 中并不需要做任何显式的函数声明,这也就是我们为什么说 PHP 的调用语法是动态的而且 PHP 从不会检查任何错误的原因。调用语法是否正确完全是留给用户自己的工作。也就是说,在调用一个函数时完全有可能这次用一个参数而下次用 4 个参数,而且两种情况在语法上都是正确的。

取得参数数量

因为 PHP 不但没法根据函数的显式声明来对调用进行语法检查,而且它还支持可变参数,所以我们就不得不在所调用函数的内部来获取参数个数。这个工作可以交给宏 ZEND_NUM_ARGS 来完成。在(PHP4)以前,这个宏(在 PHP3 中应该指的是宏 ARG_COUNT,因为 ZEND_NUM_ARGS 宏是直到 PHP 4.0 才出现的,并且其定义一直未变。PHP4 及以后虽也有 ARG_COUNT 宏定义,但却仅仅是为兼容性而保留的,并不推荐使用,译者注)是利用所调用的 C 函数中的变量 ht(就是定义在宏 INTERNAL_FUNCTION_PARAMETERS 里面的那个,HashTable * 类型)来获取参数个数的,而现在变量 ht 就只包含函数的参数个数了(int 类型)。与此同时还定义了一个哑宏:ZEND_NUM_ARGS(直接等于 ht,见 Zend.h)。尽量地采用 ZEND_NUM_ARGS 是个好习惯,因为这样可以保证在函数调用接口上的兼容性。

下面的代码展示了如何检查传入函数的参数个数的正确性:

if(ZEND_NUM_ARGS() != 2) 
{
    WRONG_PARAM_COUNT;
}

如果没有为该函数传入两个参数,那么就会退出该函数并且发出一个错误消息。在这段代码中我们使用了一个工具宏:WRONG_PARAM_COUNT,它主要用来抛出一个类似“Warning: Wrong parameter count for firstmodule() in /home/www/htdocs/firstmod.php on line 5”这样的错误信息。

这个宏会主要负责抛出一个默认的错误信息,然后便返回调用者。我们可以在 zend_API.h 中找到它的定义:

ZEND_API void wrong_param_count(void);
#define WRONG_PARAM_COUNT { wrong_param_count(); return; }

正如您所见,它调用了一个内部函数 wrong_param_count() ,这个函数会输出一个警告信息。至于如何抛出一个自定义的错误信息,可以参见后面的“打印信息”一节。

取回参数

对传入的参数进行解析是一件很常见同时也是颇为乏味的事情,而且同时你还得做好标准化的错误检查和发送错误消息等琐事。不过从 PHP 4.1.0 开始,我们就可以用一个新的参数解析 API 来搞定这些事情。这个 API 可以大大简化参数的接收处理工作,尽管它在处理可变参数时还有点弱。但既然绝大部分函数都没有可变参数,那么使用这个 API 也就理所应当地成为了我们处理函数参数时的标准方法。

这个用于参数解析的函数的原型大致如下:

int zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, ...);

第一个参数 num_args 表明了我们想要接收的参数个数,我们经常使用 ZEND_NUM_ARGS() 来表示对传入的参数“有多少要多少”。第二参数应该总是宏 TSRMLS_CC 。第三个参数 type_spec 是一个字符串,用来指定我们所期待接收的各个参数的类型,有点类似于 printf 中指定输出格式的那个格式化字符串。剩下的参数就是我们用来接收 PHP 参数值的变量的指针。

zend_parse_parameters() 在解析参数的同时会尽可能地转换参数类型,这样就可以确保我们总是能得到所期望的类型的变量。任何一种标量类型都可以转换为另外一种标量类型,但是不能在标量类型与复杂类型(比如数组、对象和资源等)之间进行转换。

如果成功地解析和接收到了参数并且在转换期间也没出现错误,那么这个函数就会返回 SUCCESS,否则返回 FAILURE。如果这个函数不能接收到所预期的参数个数或者不能成功转换参数类型时就会抛出一些类似下面这样的错误信息:

Warning - ini_get_all() requires at most 1 parameter, 2 given
Warning - wddx_deserialize() expects parameter 1 to be string, array given

当然,每个错误信息都会带有错误发生时所在的文件名和行数的。

下面这份清单完整地列举出了我们可以指定接收的参数类型:

  • l - 长整数
  • d - 双精度浮点数
  • s - 字符串 (也可能是空字节)和其长度
  • b - 布尔值
  • r - 资源, 保存在 zval*
  • a - 数组, 保存在 zval*
  • o - (任何类的)对象, 保存在 zval*
  • O - (由class entry 指定的类的)对象, 保存在 zval*
  • z - 实际的 zval*

下面的一些字符在类型说明字符串(就是那个 char *type_spec)中具有特别的含义:

  • | - 表明剩下的参数都是可选参数。如果用户没有传进来这些参数值,那么这些值就会被初始化成默认值。
  • / - 表明参数解析函数将会对剩下的参数以 SEPARATE_ZVAL_IF_NOT_REF() 的方式来提供这个参数的一份拷贝,除非这些参数是一个引用。
  • ! - 表明剩下的参数允许被设定为 NULL(仅用在 a、o、O、r和z身上)。如果用户传进来了一个 NULL 值,则存储该参数的变量将会设置为 NULL

当然啦,熟悉这个函数的最好的方法就是举个例子来说明。下面我们就来看一个例子:

/* 取得一个长整数,一个字符串和它的长度,再取得一个 zval 值。 */
long l;
char *s;
int s_len;
zval *param;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "lsz", &l, &s, &s_len, ¶m) == FAILURE) {
   return;
}

/* 取得一个由 my_ce 所指定的类的一个对象,另外再取得一个可选的双精度的浮点数。 */
zval *obj;
double d = 0.5;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "O|d", &obj, my_ce, &d) == FAILURE) {
   return;
}

/* 取得一个对象或空值,再取得一个数组。如果传递进来一个空对象,则 obj 将被设置为 NULL。*/
zval *obj;
zval *arr;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "O!a", &obj, &arr) == FAILURE) {
   return;
}

/* 取得一个分离过的数组。*/
zval *arr;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a/", &arr) == FAILURE) {
   return;
}

/* 仅取得前 3 个参数(这对可变参数的函数很有用)。*/
zval *z;
zend_bool b;
zval *r;
if (zend_parse_parameters(3, "zbr!", &z, &b, &r) == FAILURE) {
   return;
} 

注意,在最后的一个例子中,我们直接用了数值 3 而不是 ZEND_NUM_ARGS() 来作为想要取得参数的个数。这样如果我们的 PHP 函数具有可变参数的话我们就可以只接收最小数量的参数。当然,如果你想操作剩下的参数,你可以用 zend_get_parameters_array_ex() 来得到。

这个参数解析函数还有一个带有附加标志的扩展版本,这个标志可以让你控制解析函数的某些动作。

int zend_parse_parameters_ex(int flags, int num_args TSRMLS_DC, char *type_spec, ...);

这个标志(flags)目前仅接受 ZEND_PARSE_PARAMS_QUIET 这一个值,它表示这个函数不输出任何错误信息。这对那些可以传入完全不同类型参数的函数非常有用,但这样你也就不得不自己输出错误信息。

下面就是一个如何既可以接收 3 个长整形数又可以接收一个字符串的例子:

long l1, l2, l3;
char *s;

if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET,
                            ZEND_NUM_ARGS() TSRMLS_CC,
                            "lll", &l1, &l2, &l3) == SUCCESS) {
   /* manipulate longs */
} else if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET,
                                   ZEND_NUM_ARGS(), "s", &s, &s_len) == SUCCESS) {
   /* manipulate string */
} else {
   php_error(E_WARNING, "%s() takes either three long values or a string as argument",
             get_active_function_name(TSRMLS_C));
   return;
}

我想你通过上面的那些例子就可以基本掌握如何接收和处理参数了。如果你想看更多的例子,请翻阅 PHP 源码包中那些自带的扩展的源代码,那里面包含了你可能遇到的各种情况。

以前的老式的获取参数的的方法(不推荐)

获取函数参数这件事情我们还可以通过 zend_get_parameters_ex() 来完成(不推荐使用这些旧式的 API,我们推荐您使用前面所述的新式的参数解析函数):

zval **parameter;
if(zend_get_parameters_ex(1, ¶meter) != SUCCESS)
{
    WRONG_PARAM_COUNT;
}

所有的参数都存储在一个二次指向的 zval 容器里面(其实就是一个 zval* 数组,译者注)。上面的这段代码尝试接收 1 个参数并且将其保存在 parameter 所指向的位置。

zend_get_parameters_ex() 至少需要两个参数。第一个参数表示我们想要接收参数的个数(这个值通常是对应于 PHP 函数参数的个数,由此也可以看出事先对调用语法正确性的检查是多么重要)。第二个参数(包括剩下的所有参数)指向一个二次指向 zval 的指针。(即 ***zval,是不是有点糊涂了?^_^)这些指针是必须的,因为 Zend 内部是使用 **zval 进行工作的。为了能被在我们函数内部定义的 **zval 局部变量所访问,我们就必须在用一个指针来指向它。

zend_get_parameters_ex() 的返回值可以是 SUCCESSFAILURE,分别表示参数处理的成功或失败。如果处理失败,那最大的可能就是由于没有指定一个正确的参数个数。如果处理失败,则应该使用宏 WRONG_PARAM_COUNT 来退出函数。

如果想接收更多的的参数,可以用类似下面一段的代码来处理:

zval **param1, **param2, **param3, **param4;
if(zend_get_parameters_ex(4, &param1, ¶m2, ¶m3, ¶m4) != SUCCESS)
{
    WRONG_PARAM_COUNT;
}

zend_get_parameters_ex() 仅检查你是否在试图访问过多的参数。如果函数有 5 个参数,而你仅仅接收了其中的 3 个,那么你将不会收到任何错误信息,zend_get_parameters_ex() 仅返回前三个参数的值。再次调用 zend_get_parameters_ex() 也不会获得剩下两个参数的值,而还是返回前三个参数的值。

接收可变(可选)参数

如果你想接收一些可变参数,那用前面我们刚刚讨论的方法就不太合适了,主要是因为我们将不得不为每个可能的参数个数来逐行调用 zend_get_parameters_ex(),显然这很不爽。

为了解决这个问题,我们可以借用一下 zend_get_parameters_array_ex() 这个函数。它可以帮助我们接收不定量的参数并将其保存在我们指定的地方:

zval **parameter_array[4];

/* 取得参数个数 */
argument_count = ZEND_NUM_ARGS();

/* 看一下参数个数是否满足我们的要求:最少 2 个,最多 4个。 */
if(argument_count < 2 || argument_count > 4)
{
    WRONG_PARAM_COUNT;
}

/* 参数个数正确,开始接收。 */
if(zend_get_parameters_array_ex(argument_count, parameter_array) != SUCCESS)
{
    WRONG_PARAM_COUNT;
}

让我们来看看这几行代码。首先代码检查了传入参数的个数,确保在我们可接受的范围内;然后就调用 zend_get_parameters_array_ex() 把所有有效参数值的指针填入 parameter_array

我们可以在 fsockopen() 函数(位于ext/standard/fsock.c )中找到一个更为漂亮的实现。代码大致如下,你也不用担心还没有弄懂全部的函数,因为我们很快就会谈到它们。

例3.6 PHP中带有可变参数的 fsockopen() 函数的实现

pval **args[5];
int *sock=emalloc(sizeof(int));
int *sockp;
int arg_count=ARG_COUNT(ht);
int socketd = -1;
unsigned char udp = 0;
struct timeval timeout = { 60, 0 };
unsigned short portno;
unsigned long conv;
char *key = NULL;

FLS_FETCH();

if (arg_count > 5 || arg_count < 2 || zend_get_parameters_array_ex(arg_count,args)==FAILURE) {
   CLOSE_SOCK(1);
   WRONG_PARAM_COUNT;
}

switch(arg_count) {
   case 5:
       convert_to_double_ex(args[4]);
       conv = (unsigned long) (Z_DVAL_PP(args[4]) * 1000000.0);
       timeout.tv_sec = conv / 1000000;
       timeout.tv_usec = conv % 1000000;
       /* fall-through */
   case 4:
       if (!PZVAL_IS_REF(*args[3])) {
           php_error(E_WARNING,"error string argument to fsockopen not passed by reference");
       }
       pval_copy_constructor(*args[3]);
       ZVAL_EMPTY_STRING(*args[3]);
       /* fall-through */
   case 3:
       if (!PZVAL_IS_REF(*args[2])) {
           php_error(E_WARNING,"error argument to fsockopen not passed by reference");
           return;
       }
       ZVAL_LONG(*args[2], 0);
       break;
}

convert_to_string_ex(args[0]);
convert_to_long_ex(args[1]);
portno = (unsigned short) Z_LVAL_P(args[1]);
key = emalloc(Z_STRLEN_P(args[0]) + 10);

fsockopen() 可以接收 2-5 个参数。在必需的变量声明之后便开始检查参数的数量范围。然后在一个 switch 语句中使用了贯穿(fall-through)法来处理这些的参数。这个 switch 语句首先处理最大的参数个数(即 5),随后依次处理了参数个数为 4 和 3 的情况,最后用 break 关键字跳出 switch 来忽略对其他情况下参数(也就是只含有 2 个参数情况)的处理。这样在经过 switch 处理之后,就开始处理参数个数为最小时(即 2)的情况。

这种像楼梯一样的多级处理方法可以帮助我们很方便地处理一些可变参数。

存取参数

为了存取一些参数,让每个参数都具有一个明确的(C)类型是很有必要的。但 PHP 是一种动态语言,PHP 从不做任何类型检查方面的工作,因此不管你想不想,调用者都可能会把任何类型的数据传到你的函数里。比如说,如果你想接收一个整数,但调用者却可能会给你传递个数组,反之亦然 - PHP 可不管这些的。

为了避免这些问题,你就必须用一大套 API 函数来对传入的每一个参数都做一下强制性的类型转换。(见表3.4 参数类型转换函数)

注意: 所有的参数转换函数都以一个 **zval 来作为参数。

表3.4 参数类型转换函数

函数 说明
convert_to_boolean_ex() 强制转换为布尔类型。若原来是布尔值则保留,不做改动。长整型值0、双精度型值0.0、空字符串或字符串‘0’还有空值 NULL 都将被转换为 FALSE(本质上是一个整数 0)。数组和对象若为空则转换为 FALSE,否则转为 TRUE。除此之外的所有值均转换为 TRUE(本质上是一个整数 1)。
convert_to_long_ex() 强制转换为长整型,这也是默认的整数类型。如果原来是空值NULL、布尔型、资源当然还有长整型,则其值保持不变(因为本质上都是整数 0)。双精度型则被简单取整。包含有一个整数的字符串将会被转换为对应的整数,否则转换为 0。空的数组和对象将被转换为 0,否则将被转换为 1。
convert_to_double_ex() 强制转换为一个双精度型,这是默认的浮点数类型。如果原来是空值 NULL 、布尔值、资源和双精度型则其值保持不变(只变一下变量类型)。包含有一个数字的字符串将被转换成相应的数字,否则被转换为 0.0。空的数组和对象将被转换为 0.0,否则将被转换为 1.0。
convert_to_string_ex() 强制转换为数组。若原来就是一数组则不作改动。对象将被转换为一个以其属性为键名,以其属性值为键值的数组。(方法强制转换为字符串。空值 NULL 将被转换为空字符串。布尔值 TRUE 将被转换为 ‘1’,FALSE 则被转为一个空字符串。长整型和双精度型会被分别转换为对应的字符串,数组将会被转换为字符串‘Array’,而对象则被转换为字符串‘Object’。
convert_to_array_ex(value) 强制转换为数组。若原来就是一数组则不作改动。对象将被转换为一个以其属性为键名,以其属性值为键值的数组。(方法将会被转化为一个‘scalar’键,键值为方法名)空值 NULL 将被转换为一个空数组。除此之外的所有值都将被转换为仅有一个元素(下标为 0)的数组,并且该元素即为该值。
convert_to_object_ex(value) 强制转换为对象。若原来就是对象则不作改动。空值 NULL 将被转换为一个空对象。数组将被转换为一个以其键名为属性,键值为其属性值的对象。其他类型则被转换为一个具有‘scalar’属性的对象,‘scalar’属性的值即为该值本身。
convert_to_null_ex(value) 强制转换为空值 NULL。

图3.2 PHP 的数据交叉转换

PHP 的数据交叉转换

在你的参数上使用这些函数可以确保传递给你的数据都是类型安全的。如果提供的类型不是需要的类型,PHP 就会强制性地返回一个相应的伪值(比如空字符串、空的数组或对象、数值 0 或布尔值的 FALSE 等)来确保结果是一个已定义的状态。

下面的代码是从前面讨论过的模块中摘录的,其中就用到了这些转换函数:

zval **parameter;
if((ZEND_NUM_ARGS() != 1) || (zend_get_parameters_ex(1, ¶meter) != SUCCESS))
{
    WRONG_PARAM_COUNT;
}
convert_to_long_ex(parameter);
RETURN_LONG(Z_LVAL_P(parameter)); 

在收到参数指针以后,参数值就被转换成了一个长整型(或整形),转换的结果就是这个函数的返回值。如果想要弄懂如何存取到这个返回值,我们就需要对 zval 有一点点认识。它的定义如下:

例3.7 PHP/Zend zval 类型的定义

typedef pval zval;

typedef struct _zval_struct zval;

typedef union _zvalue_value {
    long lval; /* long value */
    double dval; /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht; /* hash table value */
    struct {
        zend_class_entry *ce;
        HashTable *properties;
    } obj;
} zvalue_value;

struct _zval_struct {
    /* Variable information */
    zvalue_value value; /* value */
    unsigned char type; /* active type */
    unsigned char is_ref;
    short refcount;
};

实际上,pzval(定义在 php.h)就是 zval(定义在 zend.h)的一个别名,都是 _zval_struct 结构的一个别名。_zval_struct 是一个很有趣的结构,它保存了这个结构的真实值 value、类型 type 和引用信息 is_ref。字段 value 是一个 zvalue_value 联合,根据变量类型的不同,你就可以访问不同的联合成员。对于这个结构的描述,可参见“表3.5 Zend zval 结构”、“表3.6 Zend zvalue_value 结构”和“表3.7 Zend 变量类型”。

表3.5 Zend zval 结构

字段 说明
value 变量内容的联合,参见“表3.6 Zend zvalue_value 结构”。
type 变量的类型。“表3.7 Zend 变量类型”给出了一个完整的变量类型列表。
is_ref 0 表示这个变量还不是一个引用。1 表示这个变量还有被别的变量所引用。
refcount 表示这个变量是否仍然有效。每增加一个对这个变量的引用,这个数值就增加 1。反之,每失去一个对这个变量的引用,该值就会减1。当引用计数减为0的时候,就说明已经不存在对这个变量的引用了,于是这个变量就会自动释放。

表3.6 Zend zvalue_value 结构

字段 说明
lval 如果变量类型为 IS_LONGIS_BOOLEANIS_RESOURCE 就用这个属性值。
dval 如果变量类型为 IS_DOUBLE 就用这个属性值。
str 如果变量类型为 IS_STRING 就访问这个属性值。它的字段 len 表示这个字符串的长度,字段 val 则指向该字符串。由于 Zend 使用的是 C 风格的字符串,因此字符串的长度就必须把字符串末尾的结束符 0x00 也计算在内。
ht 如果变量类型为数组,那这个 ht 就指向数组的哈希表入口。
obj 如果变量类型为 IS_OBJECT 就用这个属性值。

表3.7 Zend 变量类型

类型常量 说明
IS_NULL 表示是一个空值 NULL。
IS_LONG 是一个(长)整数。
IS_DOUBLE 是一个双精度的浮点数。
IS_STRING 是一个字符串。
IS_ARRAY 是一个数组。
IS_OBJECT 是一个对象。
IS_BOOL 是一个布尔值。
IS_RESOURCE 是一个资源(关于资源的讨论,我们以后会在适当的时候讨论到它)。
IS_STRING 是一个常量。

想访问一个长整型数,那你就访问 zval.value.lval;想访问一个双精度数,那你就访问 zval.value.dval,依此类推。不过注意,因为所有的值都是保存在一个联合里面,所以如果你用了不恰当的字段去访问,那就可能会得到一个毫无意义的结果。

访问一个数组和对象可能会稍微复杂些,稍后再说。

处理通过引用传递过来的参数

如果函数里面的参数是通过引用传递进来的,但是你又想去修改它,那就需要多加小心了。

根据我们前面所讨论的知识,我们还没有办法去修改一个经 PHP 函数参数传进来的 zval 。当然你可以修改那些在函数内部创建的局部变量的 zval ,但这并代表你可以修改任何一个指向 Zend 自身内部数据的 zval (也就是那些非局部的 zval)!

这是为什么呢?我想你可能注意到了,我们前面讨论的 API 函数都是类似于 *_ex() 这样子的。比如我们用 zend_get_parameters_ex() 而不用 zend_get_parameters(),用 convert_to_long_ex() 而不用 convert_to_long() 等等。这些 *_ex() 函数被称为新的“扩展”的 Zend API,它们的速度要快于对应的传统 API,但副作用是它们只提供了只读访问机制。

因为 Zend 内部是靠引用机制来运行的,因此不同的变量就有可能引自同一个 valuezval 结构的字段 value)。而修改一个 zval 就要求这个 zvalvalue 必须是独立的,也就是说这个 value 不能被其他 zval 引用。如果有一个 zval 里面的 value 还被其他 zval 引用了,你也同时把这个 value 给修改了,那你也同时就把其他 zvalvalue 给修改了,因为它们的 value 只是简单地指向了这个 value 而已。

zend_get_parameters_ex() 是根本不管这些的,它只是简单地返回一个你所期望的那个 zval 的指针。至于这个 zval 是否还存在其他引用,who care?(所以我们说这些 *_ex() 只提供了只读机制,并没有提供可写机制。你若利用 *_ex() 的结果强行赋值也是可以的,但这样就没法保证数据安全了。译注)。而和这个 API 对应的传统 API zend_get_parameters () 就会即时检查 value 的引用情况。如果它发现了对 value 的引用,它就会马上再重新创建一个独立的 zval ,然后把引用的数据复制一份到新的刚刚申请的空间里面,然后返回这个新的 zval 的指针。

这个动作我们称之为“zval 分离(或者 pval 分离)”。由于 *_ex() 函数并不执行“zval 分离”操作,因此它们虽然快,但是却不能用于进行写操作。

但不管怎样,要想修改参数,写操作是不可避免的。于是 Zend 使用了这样一个特别的方式来处理写操作:无论何时,只要函数的某个参数使用过引用传递的,那它就自动进行 zval 分离。这也就意味着不管什么时间,只要你像下面这样来调用一个 PHP 函数,Zend 就会自动确保传入的是一个独立的 value 并处于“写安全”状态:

my_function(&$parameter);

但这不是一般参数(指不带 & 前缀但也也是引用的参数,译者注)的情况。所有不是直接通过引用(指不带 & 前缀)传递的参数都将只是处在一种“只读”状态(其实这里的“只读”状态可以理解为“写不安全”状态)。

这就要求你确认是否真的在同一个引用打交道,否则你可能会收到你不太想要的结果。我们可以使用宏 PZVAL_IS_REF 来检查一个参数是否是通过引用传递的。这个宏接收一个 zval* 参数。“例3.8 检查参数是否经引用传递”给出了这样一个例子:

例3.8 检查参数是否经引用传递

zval *parameter;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", ¶meter) == FAILURE)
{
    return;
}

/* 检查参数是否经引用传递 */
if (!PZVAL_IS_REF(parameter)) {
{
    zend_error(E_WARNING, "Parameter wasn't passed by reference");
    RETURN_NULL();
}

/* 改变这个参数 */
ZVAL_LONG(parameter, 10);

确保其他情况下某些参数的写安全

有时候你可能会遇到过这种情况:你想对用 zend_get_parameters_ex() 接收的但是没有通过引用传递的一个参数进行写操作。这时你可以用宏 SEPARATE_ZVAL 来手工进行 zval 分离操作。这样可以得到一个新创建的与原来内部数据独立的 zval,但这个 zval 仅在局部有效,它可以被修改或销毁而不影响外部的全局 zval

zval **parameter;

/* 接收参数 */
zend_get_parameters_ex(1, ¶meter);

/* 此时  仍然关联在 Zend 的内部数据缓冲区 */

/* 现在将  “写安全”化 */
SEPARATE_ZVAL(parameter);

/* 现在你可以放心大胆去修改  了,无需担心外部的 zval 会受到影响 */
……

因为宏 SEPARATE_ZVAL 通过 emalloc() 函数来申请一个新的 zval ,所以这也就意味着如果你不主动去释放这段内存的话,那它就会直到脚本中止时才被释放。如果你大量调用这个宏却没有释放,那它可能会瞬间塞满你的内存。

注意:因为现在已经很少遇到和需要传统 API(诸如 zend_get_parameters() 等等)了(貌似它有点过时了),所以有关这些 API 本节不再赘述。

这份代码转换自 OllyDbg 的反汇编引擎。我只转换了反汇编器部分,而没有转换汇编器部分。这本来是用于我自用某个程序的一部分,但是后来见到了 pvDasm,觉得 pvDasm 代码结构上的设计更为合理,因此决定采用 pvDasm 的引擎(当然还得转换为 Delphi 代码 🙂 )。不过使用 Ollydbg 的引擎也有另外一个好处:就是反汇编出来的代码和在 Ollydbg 里面是一样的,我们会很感到很亲切。:D

大家各取所需吧。

OllyDbg Disassembler for Delphi 下载

作为目前最优秀 Delphi 反编译器(其实称之为针对 Delphi 的反汇编器会更为恰当些),DeDe 已经有三年没有更新了。随着越来越多的 Delphi 程序员开始转到 BDS2006,它也似乎有点力不从心了。除了不能正确识别高于 Delphi 7 编译的程序的编译器版本外,还有以下几个方面有待改进:

  1. 缺乏对应的 VCL 符号识别文件。这本应该在设计时考虑到的,但制作相应版本 VCL 符号文件却异常困难。似乎除了作者自己外,他人很难操刀;
  2. Dump DCU 引擎比较古老,现在已经有新版本出来了;
  3. 对 Delphi 窗体可视化编辑也不太好,对中文字符串也不能正确显示;
  4. 整个反汇编项目不能保存。下次只能重新“处理”。

所幸网上流传有 DeDe 较早版本(v3.10,DeDe 最新版本为 v3.50)的源代码,可供我们瞻仰学习一番。希望我能彻底读懂 DeDe 源代码,因为它的源代码实在太难读了~ 🙁

'