All streams
Search
Write a publication
Pull to refresh
51
0
Павел Болдин @davinchi

User

Send message
PHP довольно сложно изнутри устроен: опять же Zend, байткод. Потому в нём и бывают такие глюки: очень много оптимизаций.
Хотя публиковать разборы старых и сложных багов идея интересная. Особенно очень глубокие (вплоть до физики) баги, как например известный баг с перезагрузкой бортовых компьютеров.
Я повторюсь: мне был интересен этот баг, поскольку он был очень нестандартным: стоило поменять порядок аргументов у f() и всё работает. Стоит объявить f() до вызова и всё работает. Баг редкий и напорись на него кто-нибудь другой искали бы его в PHP коде несколько недель.

Хотя да, багов тысячи. Однако интересных не так много, и наверное не стоило публиковать её всем: не всем интересно программирование.
vim, gcc, gdb, палкой-копалкой.

Думаю, у разработчика PHP бы это заняло меньше времени.
Потому что стоило поменять местами аргументы в вызове f() и баг пропадал. Стоило объявить функцию до вызова и баг пропадал. Стоило вместо $x = 1 написать ${x} = 1 и баг пропадал. То есть он был очень нестабильный, и оказался достаточно серьёзным.
Кстати:
<?php
$var = 't'; #$t = 10;
f(0, $$var);
$x = 1;
$y = 2;
$z = 3;
echo "$x, $z, $y, $t\n";
function f($a, $b) {}
Бог здесь не при чём. Я пять часов подряд читал исходники, а не молитвы.
Не понятно, запатчил ли я его правильно. Это должны узнавать сами разработчики PHP.

В вышеприведённом случае функция так же вызывается динамически. Похоже статически вызываются только функции с явно глобальным пространством времён.

Я сейчас at least напишу хабратопик на тему поиска.
Да не, зачем. Там просто история разбора бага и запатчивания.
Теперь я могу рассказать, почему это происходило. (Версия 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 и потому бага не будет.
Нашёл, смотрите по ссылке в баг трекере.
Это связано с тем, что вызов таких функций (у которых имя не известно до исполнения) происходит динамически, через zend_do_dynamic_function_call.

В этом случае, похоже, часть аргументов каким-то образом передаётся иначе. Самое странное это конечно зависимость от порядка аргументов.
Да, спасибо, я заметил уже это. Очень странное поведение. Я вот только проснулся, сейчас продолжим искать.
Похоже, что нет. Вернее, да, программиста. Но не того.

Свободный язык не должен глючить от свободы. Если он глючит — то он не свободный. К тому же, баг скорее всего не единственный в этом месте.
Похоже, что нашёл причину бага: неправильный dereferencing EG(unitialized_zval_ptr).

bugs.php.net/bug.php?id=52001

Будем искать ещё.
А если поменять местами?
Проверить хочу, связано ли это с архитектурой/компилятором.
А ещё можно поменять местами аргументы у f и всё будет работать (кстати, проверьте, правда ли это).
Или объявить f до вызова.
А вот если закрыть написать ${x} = 1; то получите на выходе 1.

Вызывается код referenc'инга, который не равен коду присваивания в байткоде (я и не знал до часа назад, что PHP уже байткодный).

Information

Rating
Does not participate
Location
Россия
Date of birth
Registered
Activity