Хотя публиковать разборы старых и сложных багов идея интересная. Особенно очень глубокие (вплоть до физики) баги, как например известный баг с перезагрузкой бортовых компьютеров.
Я повторюсь: мне был интересен этот баг, поскольку он был очень нестандартным: стоило поменять порядок аргументов у f() и всё работает. Стоит объявить f() до вызова и всё работает. Баг редкий и напорись на него кто-нибудь другой искали бы его в PHP коде несколько недель.
Хотя да, багов тысячи. Однако интересных не так много, и наверное не стоило публиковать её всем: не всем интересно программирование.
Потому что стоило поменять местами аргументы в вызове f() и баг пропадал. Стоило объявить функцию до вызова и баг пропадал. Стоило вместо $x = 1 написать ${x} = 1 и баг пропадал. То есть он был очень нестабильный, и оказался достаточно серьёзным.
Теперь я могу рассказать, почему это происходило. (Версия PHP 5.3.2)
Во-первых, если вызвать функцию до объявления (или если вызвать её как статическую функцию), будет инициирован вызов по имени (иначе на этапе компиляции по известном имени будет вызвана функция). При вызове по имени аргументы передаются через байткод, помещающий их в стэк аргументов. При вызове по указателю (объявление до вызова) аргументы передаются непосредственно. (Поправьте, если я не прав).
Вызов по имени приводит к тому, что при вызове zend_do_end_variable_parse передаётся arg_offset, нужный для помещения аргументов в стэк (Zend/zend_compile.c стр. 2169). В Zend/zend_compile.c стр. 1066 устанавливалось в этом случае ZEND_FETCH_MAKE_REF, но только если arg_offset был > 0 (для аргументов начиная со второго, что возможно тоже баг).
На этом интересная нам часть компиляции заканчивается.
Теперь, при выполнении, из небытия восстаёт новая переменная: $$var. В этом случае компилятор помещает её в локальный scope EG(active_symbol_table), беря в качестве структуры, хранящей данные, предназначенный для этого &EG(uninitialized_zval). Вернее используется указатель на указатель &EG(uninitialized_zval_ptr).
Далее для получения аргументов функцией вызывается вспомогательная функция zend_fetch_var_address_helper (zend_vm_def.h:4431), которой очень не нравится, что это не reference, и пытается сделать reference «вызовом» SEPARATE_ZVAL_TO_MAKE_IS_REF.
Однако она работает неправильно и перезатирает *variable_ptr_ptr, указывающий на EG(uninitialized_zval). После этого в качестве uninitialized_zval используется уже неправильная структура. У этой структуры refcount=1 (поскольку на неё ссылка лишь одна), в то время как у uninitialized_zval он равен 3.
И теперь фините ля комедия. Из небытия восстают $x, $y, $z, которые так же используют EG(unitialized_zval_ptr), который уже был переписан вызовом SEPARATE_ZVAL_TO_MAKE_IS_REF в другое значение. У этого значения refcount, напомню, равен 1, поэтому оно считается свободным. Из-за этого не создаётся новая структура в Zend/zend_execute.c стр 703 и ниже, а используется существующая все три раза. И последний раз затирает структуру последним значением (стр 708 и ниже в том же файле).
Если написать ${x} = 1, то это вызовет создание переменной не в CV и потому бага не будет.
Хотя да, багов тысячи. Однако интересных не так много, и наверное не стоило публиковать её всем: не всем интересно программирование.
Думаю, у разработчика PHP бы это заняло меньше времени.
<?php
$var = 't'; #$t = 10;
f(0, $$var);
$x = 1;
$y = 2;
$z = 3;
echo "$x, $z, $y, $t\n";
function f($a, $b) {}
В вышеприведённом случае функция так же вызывается динамически. Похоже статически вызываются только функции с явно глобальным пространством времён.
Я сейчас at least напишу хабратопик на тему поиска.
Во-первых, если вызвать функцию до объявления (или если вызвать её как статическую функцию), будет инициирован вызов по имени (иначе на этапе компиляции по известном имени будет вызвана функция). При вызове по имени аргументы передаются через байткод, помещающий их в стэк аргументов. При вызове по указателю (объявление до вызова) аргументы передаются непосредственно. (Поправьте, если я не прав).
Вызов по имени приводит к тому, что при вызове zend_do_end_variable_parse передаётся arg_offset, нужный для помещения аргументов в стэк (Zend/zend_compile.c стр. 2169). В Zend/zend_compile.c стр. 1066 устанавливалось в этом случае ZEND_FETCH_MAKE_REF, но только если arg_offset был > 0 (для аргументов начиная со второго, что возможно тоже баг).
На этом интересная нам часть компиляции заканчивается.
Теперь, при выполнении, из небытия восстаёт новая переменная: $$var. В этом случае компилятор помещает её в локальный scope EG(active_symbol_table), беря в качестве структуры, хранящей данные, предназначенный для этого &EG(uninitialized_zval). Вернее используется указатель на указатель &EG(uninitialized_zval_ptr).
Далее для получения аргументов функцией вызывается вспомогательная функция zend_fetch_var_address_helper (zend_vm_def.h:4431), которой очень не нравится, что это не reference, и пытается сделать reference «вызовом» SEPARATE_ZVAL_TO_MAKE_IS_REF.
Однако она работает неправильно и перезатирает *variable_ptr_ptr, указывающий на EG(uninitialized_zval). После этого в качестве uninitialized_zval используется уже неправильная структура. У этой структуры refcount=1 (поскольку на неё ссылка лишь одна), в то время как у uninitialized_zval он равен 3.
И теперь фините ля комедия. Из небытия восстают $x, $y, $z, которые так же используют EG(unitialized_zval_ptr), который уже был переписан вызовом SEPARATE_ZVAL_TO_MAKE_IS_REF в другое значение. У этого значения refcount, напомню, равен 1, поэтому оно считается свободным. Из-за этого не создаётся новая структура в Zend/zend_execute.c стр 703 и ниже, а используется существующая все три раза. И последний раз затирает структуру последним значением (стр 708 и ниже в том же файле).
Если написать ${x} = 1, то это вызовет создание переменной не в CV и потому бага не будет.
В этом случае, похоже, часть аргументов каким-то образом передаётся иначе. Самое странное это конечно зависимость от порядка аргументов.
Свободный язык не должен глючить от свободы. Если он глючит — то он не свободный. К тому же, баг скорее всего не единственный в этом месте.
bugs.php.net/bug.php?id=52001
Будем искать ещё.
Проверить хочу, связано ли это с архитектурой/компилятором.
Или объявить f до вызова.
Вызывается код referenc'инга, который не равен коду присваивания в байткоде (я и не знал до часа назад, что PHP уже байткодный).