- URL: https://www.laruence.com/en/2010/01/03/1225.html
- Please include attribution when republishing.
An error born of an unwarranted assumption.
The requirement was this: I needed to add a signature string to a binary stream. So, naturally, I first wrote a signing function. Since I wanted to tell whether the signing operation succeeded, I used a reference parameter:
function sign(&$carrier, $fingerprint) {
if (NULL === $fingerprint) {
return FALSE;
}
//add the signature
$carrier = signing logic.
return TRUE;
}
Next, reasoning that if signing fails we should still use the original string as the result, I confidently wrote the following code:
$bin_str = **************;
$success = sign($after_signed = $bin_str, "laruence's fingerprint");
if ($success) {
//use $after_signed
} else {
//use $bin_str
}
Given that simple variables are passed by value, I assumed that after $bin_str is assigned to $after_signed, the reference of $after_signed would be passed on to $carrier...
But it turned out that $after_signed did not have the signature string added to it at all...
So why was it wrong?
After analysing the opcodes, it turned out that when PHP performs a binary assignment, the return value is not the left-hand side but a temporary variable. That is, for:
$a = $b;
the return value is not $a but a temporary variable — call it $2.
So what gets passed to the sign function is not a reference to $a, but $2;
The following is source-level analysis; if you only want the conclusion, you can skip it:
Now, combining this with the material from my earlier article Understanding PHP Internals: Variables Separation/Reference, let's do a detailed analysis:
$a = "laruence";
function ch(&$p) {
debug_zval_dump($p);
$p = 'eve';
debug_zval_dump($p);
}
debug_zval_dump($a);
ch($a);
ch($b=$a);
The output is:
//the initial a string(8) "laruence" refcount(2) //p during the first call to change string(8) "laruence" refcount(1) string(3) "eve" refcount(1) //a during the second call, with the assignment result string(3) "eve" refcount(3) string(3) "eve" refcount(2)
First, at the very start $a's refcount is 2, because simple variables are passed by value: after being handed to debug_zval_dump, $a has the refcount of a copy.
On the first call we pass $a directly. Since the change function declares its parameter by reference, when debug_zval_dump is called inside change, $p is a reference. Passing it by value therefore triggers a separation, giving a count of 1.
The interesting part is the second call, where the refcount at the first debug_zval_dump inside change is 3. How can it be 3?
For it to be 3 here, $p must be a non-reference variable with a refcount of 2.
But didn't we declare that change takes a reference parameter?
No choice but to look at the source. At parse time, the difference becomes visible:
non_empty_function_call_parameter_list:
expr_without_variable
{ Z_LVAL($$.u.constant) = 1;
zend_do_pass_param(&$1, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); }
| variable
{ Z_LVAL($$.u.constant) = 1;
zend_do_pass_param(&$1, ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); }
For the second call, the first reduction rule is the one that matches, so the second argument passed to zend_do_pass_param is ZEND_SEND_VAL...
Following zend_do_pass_param further:
if (op == ZEND_SEND_VAR && zend_is_function_or_method_call(param)) {
//is it the return value of a function or method
/* Method call */
op = ZEND_SEND_VAR_NO_REF;
send_function = ZEND_ARG_SEND_FUNCTION;
} else if (op == ZEND_SEND_VAL && (param->op_type & (IS_VAR|IS_CV))) {
op = ZEND_SEND_VAR_NO_REF;//changed to NO_REF here
}
So, because on the second call the variable passed to change is a "literal" of type IS_VAR, PHP changes the passing mode to by-value.
That's why the refcount is already 3 at the first debug_zval_dump inside change.
In fact, at this point we can draw an analogy with the case of a function's return value used as an argument (the condition in the first if). If you imagine that assignment, like a function call, has a return value, it becomes much easier to understand.
Be First to Comment