]> granicus.if.org Git - php/commitdiff
Rewrite watchpoints to be much more stable
authorBob Weinand <bobwei9@hotmail.com>
Mon, 11 Jul 2016 21:28:14 +0000 (23:28 +0200)
committerBob Weinand <bobwei9@hotmail.com>
Mon, 11 Jul 2016 21:58:20 +0000 (23:58 +0200)
This mainly involves a separate abstraction layer for elements (e.g. $a->b) and watchpoints (on pointer of the Bucket for example).
Also better comparison handling (value backup vs. page dumps).

It is not yet finished (there are sometimes false positives announced and names not yet perfect), but the functionality is working and not crashing as far as I have tested.
Future scope is also relative watchpoints, e.g. "w $this->val expression()" which does not have the symbol tables as basis, but the value (in this example: return value of expression()) as basis.

18 files changed:
Zend/zend.c
main/SAPI.c
sapi/phpdbg/phpdbg.c
sapi/phpdbg/phpdbg.h
sapi/phpdbg/phpdbg_btree.c
sapi/phpdbg/phpdbg_btree.h
sapi/phpdbg/phpdbg_list.c
sapi/phpdbg/phpdbg_list.h
sapi/phpdbg/phpdbg_prompt.c
sapi/phpdbg/phpdbg_utils.c
sapi/phpdbg/phpdbg_watch.c
sapi/phpdbg/phpdbg_watch.h
sapi/phpdbg/tests/watch_001.phpt
sapi/phpdbg/tests/watch_002.phpt [new file with mode: 0644]
sapi/phpdbg/tests/watch_003.phpt [new file with mode: 0644]
sapi/phpdbg/tests/watch_004.phpt [new file with mode: 0644]
sapi/phpdbg/tests/watch_005.phpt [new file with mode: 0644]
sapi/phpdbg/tests/watch_006.phpt [new file with mode: 0644]

index 91f5cd696600ada1e32d82c815088d551474e3ee..118bd6c4961ba900d55a42a5461682347fc7fd42 100644 (file)
@@ -300,6 +300,9 @@ ZEND_API void zend_print_flat_zval_r(zval *expr) /* {{{ */
                        ZEND_PUTS(")");
                        break;
                }
+               case IS_REFERENCE:
+                       zend_print_flat_zval_r(Z_REFVAL_P(expr));
+                       break;
                default:
                        zend_print_variable(expr);
                        break;
@@ -355,6 +358,9 @@ static void zend_print_zval_r_to_buf(smart_str *buf, zval *expr, int indent) /*
                case IS_LONG:
                        smart_str_append_long(buf, Z_LVAL_P(expr));
                        break;
+               case IS_REFERENCE:
+                       zend_print_zval_r_to_buf(buf, Z_REFVAL_P(expr), indent);
+                       break;
                default:
                        {
                                zend_string *str = zval_get_string(expr);
index 31b8bea9a70776285d00895fb7703eeb2e2b62a0..da76e00b92df0d7faed3e10b99f8b42ad9181fae 100644 (file)
@@ -479,10 +479,9 @@ SAPI_API void sapi_activate(void)
 
                /* Cookies */
                SG(request_info).cookie_data = sapi_module.read_cookies();
-
-               if (sapi_module.activate) {
-                       sapi_module.activate();
-               }
+       }
+       if (sapi_module.activate) {
+               sapi_module.activate();
        }
        if (sapi_module.input_filter_init) {
                sapi_module.input_filter_init();
index e4fa7a5874193d2eb4400826761127a69f8c5122..0d0b4b41cadf992e3e5062e7d3f1cf68824a75dc 100644 (file)
@@ -75,6 +75,7 @@ PHP_INI_END()
 
 static zend_bool phpdbg_booted = 0;
 static zend_bool phpdbg_fully_started = 0;
+zend_bool use_mm_wrappers = 0;
 
 static inline void php_phpdbg_globals_ctor(zend_phpdbg_globals *pg) /* {{{ */
 {
@@ -189,79 +190,27 @@ static void php_phpdbg_destroy_registered(zval *data) /* {{{ */
        destroy_zend_function(function);
 } /* }}} */
 
+static void php_phpdbg_destroy_file_source(zval *data) /* {{{ */
+{
+       phpdbg_file_source *source = (phpdbg_file_source *) Z_PTR_P(data);
+       destroy_op_array(&source->op_array);
+       if (source->buf) {
+               efree(source->buf);
+       }
+       efree(source);
+} /* }}} */
+
 
 static PHP_RINIT_FUNCTION(phpdbg) /* {{{ */
 {
-       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_FILE], 8, NULL, php_phpdbg_destroy_bp_file, 0);
-       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_FILE_PENDING], 8, NULL, php_phpdbg_destroy_bp_file, 0);
-       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_SYM], 8, NULL, php_phpdbg_destroy_bp_symbol, 0);
-       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_FUNCTION_OPLINE], 8, NULL, php_phpdbg_destroy_bp_methods, 0);
-       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_METHOD_OPLINE], 8, NULL, php_phpdbg_destroy_bp_methods, 0);
-       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_FILE_OPLINE], 8, NULL, php_phpdbg_destroy_bp_methods, 0);
-       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_OPLINE], 8, NULL, php_phpdbg_destroy_bp_opline, 0);
-       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_OPCODE], 8, NULL, php_phpdbg_destroy_bp_opcode, 0);
-       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_METHOD], 8, NULL, php_phpdbg_destroy_bp_methods, 0);
-       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_COND], 8, NULL, php_phpdbg_destroy_bp_condition, 0);
-       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_MAP], 8, NULL, NULL, 0);
-
-       zend_hash_init(&PHPDBG_G(seek), 8, NULL, NULL, 0);
-       zend_hash_init(&PHPDBG_G(registered), 8, NULL, php_phpdbg_destroy_registered, 0);
+       /* deactivate symbol table caching to have these properly destroyed upon stack leaving (especially important for watchpoints) */
+       EG(symtable_cache_limit) = EG(symtable_cache) - 1;
 
        return SUCCESS;
 } /* }}} */
 
 static PHP_RSHUTDOWN_FUNCTION(phpdbg) /* {{{ */
 {
-       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_FILE]);
-       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_FILE_PENDING]);
-       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_SYM]);
-       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_FUNCTION_OPLINE]);
-       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_METHOD_OPLINE]);
-       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_FILE_OPLINE]);
-       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_OPLINE]);
-       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_OPCODE]);
-       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_METHOD]);
-       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_COND]);
-       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_MAP]);
-       zend_hash_destroy(&PHPDBG_G(file_sources));
-       zend_hash_destroy(&PHPDBG_G(seek));
-       zend_hash_destroy(&PHPDBG_G(registered));
-       zend_hash_destroy(&PHPDBG_G(watchpoints));
-       zend_llist_destroy(&PHPDBG_G(watchlist_mem));
-
-       if (PHPDBG_G(buffer)) {
-               free(PHPDBG_G(buffer));
-               PHPDBG_G(buffer) = NULL;
-       }
-
-       if (PHPDBG_G(exec)) {
-               efree(PHPDBG_G(exec));
-               PHPDBG_G(exec) = NULL;
-       }
-
-       if (PHPDBG_G(oplog)) {
-               fclose(PHPDBG_G(oplog));
-               PHPDBG_G(oplog) = NULL;
-       }
-
-       if (PHPDBG_G(ops)) {
-               destroy_op_array(PHPDBG_G(ops));
-               efree(PHPDBG_G(ops));
-               PHPDBG_G(ops) = NULL;
-       }
-
-       if (PHPDBG_G(oplog_list)) {
-               phpdbg_oplog_list *cur = PHPDBG_G(oplog_list);
-               do {
-                       phpdbg_oplog_list *prev = cur->prev;
-                       efree(cur);
-                       cur = prev;
-               } while (cur != NULL);
-
-               zend_arena_destroy(PHPDBG_G(oplog_arena));
-               PHPDBG_G(oplog_list) = NULL;
-       }
-
        return SUCCESS;
 } /* }}} */
 
@@ -845,8 +794,86 @@ static void php_sapi_phpdbg_log_message(char *message, int syslog_type_int) /* {
 }
 /* }}} */
 
+static int php_sapi_phpdbg_activate(void) /* {{{ */
+{
+       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_FILE], 8, NULL, php_phpdbg_destroy_bp_file, 0);
+       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_FILE_PENDING], 8, NULL, php_phpdbg_destroy_bp_file, 0);
+       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_SYM], 8, NULL, php_phpdbg_destroy_bp_symbol, 0);
+       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_FUNCTION_OPLINE], 8, NULL, php_phpdbg_destroy_bp_methods, 0);
+       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_METHOD_OPLINE], 8, NULL, php_phpdbg_destroy_bp_methods, 0);
+       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_FILE_OPLINE], 8, NULL, php_phpdbg_destroy_bp_methods, 0);
+       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_OPLINE], 8, NULL, php_phpdbg_destroy_bp_opline, 0);
+       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_OPCODE], 8, NULL, php_phpdbg_destroy_bp_opcode, 0);
+       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_METHOD], 8, NULL, php_phpdbg_destroy_bp_methods, 0);
+       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_COND], 8, NULL, php_phpdbg_destroy_bp_condition, 0);
+       zend_hash_init(&PHPDBG_G(bp)[PHPDBG_BREAK_MAP], 8, NULL, NULL, 0);
+
+       zend_hash_init(&PHPDBG_G(seek), 8, NULL, NULL, 0);
+       zend_hash_init(&PHPDBG_G(registered), 8, NULL, php_phpdbg_destroy_registered, 0);
+
+       zend_hash_init(&PHPDBG_G(file_sources), 0, NULL, php_phpdbg_destroy_file_source, 0);
+       phpdbg_setup_watchpoints();
+
+       return SUCCESS;
+}
+
 static int php_sapi_phpdbg_deactivate(void) /* {{{ */
 {
+       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_FILE]);
+       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_FILE_PENDING]);
+       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_SYM]);
+       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_FUNCTION_OPLINE]);
+       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_METHOD_OPLINE]);
+       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_FILE_OPLINE]);
+       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_OPLINE]);
+       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_OPCODE]);
+       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_METHOD]);
+       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_COND]);
+       zend_hash_destroy(&PHPDBG_G(bp)[PHPDBG_BREAK_MAP]);
+       zend_hash_destroy(&PHPDBG_G(file_sources));
+       zend_hash_destroy(&PHPDBG_G(seek));
+       zend_hash_destroy(&PHPDBG_G(registered));
+       phpdbg_destroy_watchpoints();
+
+       /* hack to restore mm_heap->use_custom_heap in order to receive memory leak info */
+       if (use_mm_wrappers) {
+               /* ASSUMING that mm_heap->use_custom_heap is the first element of the struct ... */
+               *(int *) zend_mm_get_heap() = 0;
+       }
+
+       if (PHPDBG_G(buffer)) {
+               free(PHPDBG_G(buffer));
+               PHPDBG_G(buffer) = NULL;
+       }
+
+       if (PHPDBG_G(exec)) {
+               efree(PHPDBG_G(exec));
+               PHPDBG_G(exec) = NULL;
+       }
+
+       if (PHPDBG_G(oplog)) {
+               fclose(PHPDBG_G(oplog));
+               PHPDBG_G(oplog) = NULL;
+       }
+
+       if (PHPDBG_G(ops)) {
+               destroy_op_array(PHPDBG_G(ops));
+               efree(PHPDBG_G(ops));
+               PHPDBG_G(ops) = NULL;
+       }
+
+       if (PHPDBG_G(oplog_list)) {
+               phpdbg_oplog_list *cur = PHPDBG_G(oplog_list);
+               do {
+                       phpdbg_oplog_list *prev = cur->prev;
+                       efree(cur);
+                       cur = prev;
+               } while (cur != NULL);
+
+               zend_arena_destroy(PHPDBG_G(oplog_arena));
+               PHPDBG_G(oplog_list) = NULL;
+       }
+
        fflush(stdout);
        if (SG(request_info).argv0) {
                free(SG(request_info).argv0);
@@ -994,7 +1021,7 @@ static sapi_module_struct phpdbg_sapi_module = {
        php_sapi_phpdbg_module_startup, /* startup */
        php_module_shutdown_wrapper,    /* shutdown */
 
-       NULL,                           /* activate */
+       php_sapi_phpdbg_activate,       /* activate */
        php_sapi_phpdbg_deactivate,     /* deactivate */
 
        php_sapi_phpdbg_ub_write,       /* unbuffered write */
@@ -1236,11 +1263,11 @@ void phpdbg_signal_handler(int sig, siginfo_t *info, void *context) /* {{{ */
        switch (sig) {
                case SIGBUS:
                case SIGSEGV:
-                       if (PHPDBG_G(sigsegv_bailout)) {
-                               LONGJMP(*PHPDBG_G(sigsegv_bailout), FAILURE);
-                       }
                        is_handled = phpdbg_watchpoint_segfault_handler(info, context);
                        if (is_handled == FAILURE) {
+                               if (PHPDBG_G(sigsegv_bailout)) {
+                                       LONGJMP(*PHPDBG_G(sigsegv_bailout), FAILURE);
+                               }
                                zend_sigaction(sig, &PHPDBG_G(old_sigsegv_signal), NULL);
                        }
                        break;
@@ -1308,7 +1335,6 @@ int main(int argc, char **argv) /* {{{ */
        FILE* stream = NULL;
        char *print_opline_func;
        zend_bool ext_stmt = 0;
-       zend_bool use_mm_wrappers = 0;
        zend_bool is_exit;
        int exit_status;
 
@@ -1635,8 +1661,6 @@ phpdbg_main:
 
                use_mm_wrappers = !_malloc && !_realloc && !_free;
 
-               phpdbg_init_list();
-
                PHPDBG_G(original_free_function) = _free;
                _free = phpdbg_watch_efree;
 
@@ -1650,14 +1674,14 @@ phpdbg_main:
                        zend_mm_set_custom_handlers(mm_heap, _malloc, _free, _realloc);
                }
 
-               phpdbg_setup_watchpoints();
-
 #if defined(ZEND_SIGNALS) && !defined(_WIN32)
                zend_try {
                        zend_signal_activate();
                } zend_end_try();
 #endif
 
+               phpdbg_init_list();
+
                PHPDBG_G(sapi_name_ptr) = sapi_name;
 
                if (exec) { /* set execution context */
@@ -1976,11 +2000,6 @@ phpdbg_out:
                        }
                }
 
-               /* hack to restore mm_heap->use_custom_heap in order to receive memory leak info */
-               if (use_mm_wrappers) {
-                       /* ASSUMING that mm_heap->use_custom_heap is the first element of the struct ... */
-                       *(int *) mm_heap = 0;
-               }
                zend_try {
                        php_request_shutdown(NULL);
                } zend_end_try();
index 2f95a80f04ea9eb67a1f5ef759641eaa302ad497..227aaebe41960da701464005aee8f6ba42259b77 100644 (file)
@@ -253,12 +253,15 @@ ZEND_BEGIN_MODULE_GLOBALS(phpdbg)
 #endif
        phpdbg_btree watchpoint_tree;                /* tree with watchpoints */
        phpdbg_btree watch_HashTables;               /* tree with original dtors of watchpoints */
-       HashTable watchpoints;                       /* watchpoints */
+       HashTable watch_elements;                    /* user defined watch elements */
        HashTable watch_collisions;                  /* collision table to check if multiple watches share the same recursive watchpoint */
-       zend_llist watchlist_mem;                    /* triggered watchpoints */
+       HashTable watch_recreation;                  /* watch elements pending recreation of their respective watchpoints */
+       HashTable watch_free;                        /* pointers to watch for being freed */
+       HashTable *watchlist_mem;                    /* triggered watchpoints */
+       HashTable *watchlist_mem_backup;             /* triggered watchpoints backup table while iterating over it */
        zend_bool watchpoint_hit;                    /* a watchpoint was hit */
        void (*original_free_function)(void *);      /* the original AG(mm_heap)->_free function */
-       phpdbg_watchpoint_t *watch_tmp;              /* temporary pointer for a watchpoint */
+       phpdbg_watch_element *watch_tmp;             /* temporary pointer for a watch element */
 
        char *exec;                                  /* file to execute */
        size_t exec_len;                             /* size of exec */
index 9e7dc86e8e063f64b8441b71e99a2a28027e9155..7e1937fc7392a8de08118a07140c4a6d5db5d775 100644 (file)
        branch = branch->branches[!!(n)];
 
 #ifdef _Win32
-# define emalloc malloc
-# define efree free
+# undef pemalloc
+# undef pefree
+# define pemalloc(size, persistent) malloc(size)
+# define pefree(ptr, persistent) free(ptr)
 #endif
 
 /* depth in bits */
 void phpdbg_btree_init(phpdbg_btree *tree, zend_ulong depth) {
        tree->depth = depth;
        tree->branch = NULL;
+       tree->persistent = 0;
        tree->count = 0;
 }
 
@@ -157,7 +160,7 @@ int phpdbg_btree_insert_or_update(phpdbg_btree *tree, zend_ulong idx, void *ptr,
                }
 
                {
-                       phpdbg_btree_branch *memory = *branch = emalloc((i + 2) * sizeof(phpdbg_btree_branch));
+                       phpdbg_btree_branch *memory = *branch = pemalloc((i + 2) * sizeof(phpdbg_btree_branch), tree->persistent);
                        do {
                                (*branch)->branches[!((idx >> i) % 2)] = NULL;
                                branch = &(*branch)->branches[(idx >> i) % 2];
@@ -199,14 +202,14 @@ check_branch_existence:
        tree->count--;
 
        if (i_last_dual_branch == -1) {
-               efree(tree->branch);
+               pefree(tree->branch, tree->persistent);
                tree->branch = NULL;
        } else {
                if (last_dual_branch->branches[last_dual_branch_branch] == last_dual_branch + 1) {
                        phpdbg_btree_branch *original_branch = last_dual_branch->branches[!last_dual_branch_branch];
 
                        memcpy(last_dual_branch + 1, last_dual_branch->branches[!last_dual_branch_branch], (i_last_dual_branch + 1) * sizeof(phpdbg_btree_branch));
-                       efree(last_dual_branch->branches[!last_dual_branch_branch]);
+                       pefree(last_dual_branch->branches[!last_dual_branch_branch], tree->persistent);
                        last_dual_branch->branches[!last_dual_branch_branch] = last_dual_branch + 1;
 
                        branch = last_dual_branch->branches[!last_dual_branch_branch];
@@ -214,7 +217,7 @@ check_branch_existence:
                                branch = (branch->branches[branch->branches[1] == ++original_branch] = last_dual_branch + i_last_dual_branch - i + 1);
                        }
                } else {
-                       efree(last_dual_branch->branches[last_dual_branch_branch]);
+                       pefree(last_dual_branch->branches[last_dual_branch_branch], tree->persistent);
                }
 
                last_dual_branch->branches[last_dual_branch_branch] = NULL;
@@ -223,6 +226,26 @@ check_branch_existence:
        return SUCCESS;
 }
 
+void phpdbg_btree_clean_recursive(phpdbg_btree_branch *branch, zend_ulong depth, zend_bool persistent) {
+       phpdbg_btree_branch *start = branch;
+       while (depth--) {
+               zend_bool use_branch = branch + 1 == branch->branches[0];
+               if (branch->branches[use_branch]) {
+                       phpdbg_btree_clean_recursive(branch->branches[use_branch], depth, persistent);
+               }
+       }
+
+       pefree(start, persistent);
+}
+
+void phpdbg_btree_clean(phpdbg_btree *tree) {
+       if (tree->branch) {
+               phpdbg_btree_clean_recursive(tree->branch, tree->depth, tree->persistent);
+               tree->branch = NULL;
+               tree->count = 0;
+       }
+}
+
 void phpdbg_btree_branch_dump(phpdbg_btree_branch *branch, zend_ulong depth) {
        if (branch) {
                if (depth--) {
index 05fb6b833a567d231c76bdce5f2f16b0b9316c0b..3eae81b997b29d5d95bbbf1a1e78db69f741a202 100644 (file)
@@ -37,6 +37,7 @@ union _phpdbg_btree_branch {
 typedef struct {
        zend_ulong count;
        zend_ulong depth;
+       zend_bool persistent;
        phpdbg_btree_branch *branch;
 } phpdbg_btree;
 
@@ -47,6 +48,7 @@ typedef struct {
 } phpdbg_btree_position;
 
 void phpdbg_btree_init(phpdbg_btree *tree, zend_ulong depth);
+void phpdbg_btree_clean(phpdbg_btree *tree);
 phpdbg_btree_result *phpdbg_btree_find(phpdbg_btree *tree, zend_ulong idx);
 phpdbg_btree_result *phpdbg_btree_find_closest(phpdbg_btree *tree, zend_ulong idx);
 phpdbg_btree_position phpdbg_btree_find_between(phpdbg_btree *tree, zend_ulong lower_idx, zend_ulong higher_idx);
index ca73209114b5ff07561fc34fba3c2a3a55bdb0c3..86a2132b7401410a23543814511655b7fa57a299 100644 (file)
@@ -372,22 +372,9 @@ zend_op_array *phpdbg_compile_string(zval *source_string, char *filename) {
        return op_array;
 }
 
-void phpdbg_free_file_source(zval *zv) {
-       phpdbg_file_source *data = Z_PTR_P(zv);
-
-       if (data->buf) {
-               efree(data->buf);
-       }
-
-       destroy_op_array(&data->op_array);
-
-       efree(data);
-}
-
 void phpdbg_init_list(void) {
        PHPDBG_G(compile_file) = zend_compile_file;
        PHPDBG_G(compile_string) = zend_compile_string;
-       zend_hash_init(&PHPDBG_G(file_sources), 1, NULL, (dtor_func_t) phpdbg_free_file_source, 0);
        zend_compile_file = phpdbg_compile_file;
        zend_compile_string = phpdbg_compile_string;
 }
index c011b9598a2e79894e346218eb8834b03522d5db..39e1d388d0cbc1eb3d680e1284fef40c31e3fb7f 100644 (file)
@@ -49,7 +49,6 @@ typedef struct {
        void *map;
 #endif
        zend_op_array op_array;
-       zend_bool destroy_op_array;
        uint lines;
        uint line[1];
 } phpdbg_file_source;
index 2ca1368751f7e999f39461c1224a7bdeb74b8540..88bd8d638b05e8595c84620a65d7518bd5dccf8f 100644 (file)
@@ -1325,9 +1325,7 @@ PHPDBG_COMMAND(watch) /* {{{ */
                phpdbg_list_watchpoints();
        } else switch (param->type) {
                case STR_PARAM:
-                       if (phpdbg_create_var_watchpoint(param->str, param->len) != FAILURE) {
-                               phpdbg_notice("watch", "variable=\"%.*s\"", "Set watchpoint on %.*s", (int) param->len, param->str);
-                       }
+                       phpdbg_create_var_watchpoint(param->str, param->len);
                        break;
 
                phpdbg_default_switch_case();
index 6f42665809b1002375130d9adb43fcb1f3779330..331409990e1ca449fe259e909508f9e667450b8d 100644 (file)
@@ -532,7 +532,7 @@ PHPDBG_API int phpdbg_parse_variable_with_arg(char *input, size_t len, HashTable
                        last_index[index_len] = 0;
                        if (!(zv = zend_symtable_str_find(parent, last_index, index_len))) {
                                if (!silent) {
-                                       phpdbg_error("variable", "type=\"undefined\" variable=\"%.*s\"", "%.*s is undefined", (int) i, input);
+                                       phpdbg_error("variable", "type=\"undefined\" variable=\"%.*s\"", "%.*s is undefined", (int) input[i] == ']' ? i + 1 : i, input);
                                }
                                return FAILURE;
                        }
index 0224ff4fd1e18d4066325bc2da69839484b5a3df..e01e9a787dbc55aea0a33c873e5df24befd054ed 100644 (file)
 
 /* Some information for the reader...
  *
- * Watchpoints are either simple, recursive or implicit (PHPDBG_WATCH_* flags)
+ * The main structure managing the direct observations is the watchpoint (phpdbg_watchpoint_t). There are several types of watchpoints currently:
+ * WATCH_ON_BUCKET: a watchpoint on a Bucket element, used to monitor values inside HashTables (largely handled equivalently to WATCH_ON_ZVAL, it just monitors also for IS_UNDEF and key changes)
+ * WATCH_ON_ZVAL: a watchpoint on a bare zval (&zend_reference.val, zval.value.indirect)
+ * WATCH_ON_STR: a watchpoint on a zend_string (put on &ZSTR_LEN() in order to not watch refcount/hash)
+ * WATCH_ON_HASHTABLE: a watchpoint on a HashTable (currently only used to observe size changes, put after flags in order to not watch refcount)
+ * WATCH_ON_REFCOUNTED: a watchpoint on a zend_refcounted, observes the refcount and serves as reference pointer in the custom efree handler
+ * WATCH_ON_HASHDATA: special watchpoint to watch for HT_GET_DATA_ADDR(ht) being efree()'d to be able to properly relocate Bucket watches
+ *
+ * Watch elements are either simple, recursive or implicit (PHPDBG_WATCH_* flags)
  * Simple means that a particular watchpoint was explicitely defined
- * Recursive watchpoints are created recursively and substitute simple watchpoints
- * Implicit watchpoints are implicitely created on all ancestors of simple or recursive watchpoints
- * Recursive and (simple or implicit) watchpoints are mutually exclusive
+ * Recursive watch elements are created recursively (recursive root flag is to distinguish the root element easily from its children recursive elements)
+ * Implicit  watch elements are implicitely created on all ancestors of simple or recursive watch elements
+ * Recursive and (simple or implicit) watch elements are mutually exclusive
+ * Array/Object to distinguish watch elements on arrays
+ *
+ * Watch elements all contain a reference to a watchpoint (except if scheduled for recreation); a "watch" is a watch element created by the user with a specific id
+ * Each watch has its independent structure of watch elements, watchpoints are responsible for managing collisions and preventing pointers being watched multiple times
  *
  * PHPDBG_G(watchpoint_tree) contains all watchpoints identified by the watch target address
- * PHPDBG_G(watch_HashTables) contains the dtors of the HashTables to call in our custom dtor (we substitute the dtor of HashTables containing watched zvals by our own dtor)
- * PHPDBG_G(watchpoints) contains all watchpoints (except the ones managed by watch collision)
- * PHPDBG_G(watch_collisions) is indexed by a zend_reference * pointer. It stores information about collisions (everything which contains a zend_reference * may be referenced by multiple watches)
+ * PHPDBG_G(watch_HashTables) contains the addresses of parent_containers of watch elements
+ * PHPDBG_G(watch_elements) contains all directly defined watch elements (i.e. those which have an individual id)
+ * PHPDBG_G(watch_collisions) is indexed by a zend_refcounted * pointer (phpdbg_watchpoint_t.ref). It stores information about collisions (everything which contains a zend_refcounted * may be referenced by multiple watches)
+ * PHPDBG_G(watch_free) is a set of pointers to watch for being freed (like HashTables referenced by phpdbg_watch_element.parent_container)
+ * PHPDBG_G(watch_recreation) is the list of watch elements whose watchpoint has been removed (via efree() for example) and needs to be recreated
+ * PHPDBG_G(watchlist_mem) is the list of unprotected memory pages; used to watch which pages need their PROT_WRITE attribute removed after checking
+ *
+ * Watching on addresses:
+ * * Address and size are transformed into memory page aligned address and size
+ * * mprotect() enables or disables them (depending on flags) - Windows has a transparent compatibility layer in phpdbg_win.c
+ * * segfault handler stores the address of the page and marks it again as writable
+ * * later watchpoints pointing inside these pages are compared against their current value and eventually reactivated (or deleted)
  *
- * Creating a watchpoint:
- * * Create watchpoints with PHPDBG_WATCH_IMPLICIT set on each zval and HashTable in hierarchy except the last zval or HashTable fetch. (if already existing PHPDBG_WATCH_IMPLICIT flag is added)
- * * Create a PHPDBG_WATCH_SIMPLE watch for simple watches or a PHPDBG_WATCH_RECURSIVE watch for recursive watches
- * * When the target zval is an IS_REFERENCE, create a watchpoint on it too
- * * Each time a watchpoints parent is a zval and it is Z_REFCOUNTED(), put a watchpoint (WATCH_ON_REFCOUNTED) on it and add a watchpoint collision
- * * When in recursive mode and encountering a not-refcounted PHPDBG_WATCH_SIMPLE, remove it and recreate it with a PHPDBG_WATCH_RECURSIVE (handled via watch collision)
- * * Make attention to not add something twice or iterate over it twice
+ * Creating a watch:
+ * * Implicit watch elements for each element in the hierarchy (starting from base, which typically is current symbol table) except the last one
+ * * Create a watch element with either simple flag or recursive [+ root] flags
+ * * If the element has recursive flag, create elements recursively for every referenced HashTable and zval
  *
- * Deleting a watchpoint:
- * * Only allow deletion of recursive watches at their root and simple watches
- * * Go to referenced variable. And remove watch collision on *parent* (if there is a parent)
- * * If it is Z_REFCOUNTED(), remove that watch collision
+ * Creating a watch element:
+ * * For each watch element a related watchpoint is created, if there's none yet; add itself then into the list of parents of that watchpoint
+ * * If the watch has a parent_container, add itself also into a phpdbg_watch_ht_info (inside PHPDBG_G(watch_HashTables)) [and creates it if not yet existing]
+ *
+ * Creation of watchpoints:
+ * * Watchpoints create a watch collision for each refcounted or indirect on the zval (if type is WATCH_ON_BUCKET or WATCH_ON_ZVAL)
+ * * Backs the current value of the watched pointer up
+ * * Installs the watchpoint in PHPDBG_G(watchpoint_tree) and activates it (activation of a watchpoint = remove PROT_WRITE from the pages the watched pointer resides on)
  *
  * Watch collisions:
- * * hold a counter for recursive, if it is incremented from 0 to 1, create recursive watchpoint
- * * holds a HashTable for normal (not implicit) watchpoints ... it is used to get the fetch type of the HashTable (depending on whether it is empty or not)
- * * holds a HashTable for implicit watchpoints ... (some sort of a refcounter, but ensure that there are no duplicates)
- * * if normal and implicit watchpoints are empty, drop that watch collision and remove WATCH_ON_REFCOUNT alongside with watchpoint on an eventual reference
+ * * Manages a watchpoint on the refcount (WATCH_ON_REFCOUNTED) or indirect zval (WATCH_ON_ZVAL)
+ * * Guarantees that every pointer is watched at most once (by having a pointer to collision mapping in PHPDBG_G(watch_collisions), which have the unique watchpoints for the respective collision)
+ * * Contains a list of parents, i.e. which watchpoints reference it (via watch->ref)
+ * * If no watchpoint is referencing it anymore, the watch collision and its associated watchpoints (phpdbg_watch_collision.ref/reference) are removed
  *
- * Watching on addresses:
- * * Address and size are transformed into memory page aligned address and size
- * * mprotect() enables or disables them (depending on flags) - Windows has a transparent compatibility layer in phpdbg_win.c
- * * segfault handler dumps watched memory segment and deactivates watchpoint
- * * later watches inside these memory segments are compared against their current value and eventually reactivated (or deleted)
+ * Deleting a watch:
+ * * Watches are stored by an id in PHPDBG_G(watch_elements); the associated watch element is then deleted
+ * * Deletes all parent and children implicit watch elements
+ *
+ * Deleting a watch element:
+ * * Removes itself from the parent list of the associated watchpoints; if that parent list is empty, also delete the watchpoint
+ * * Removes itself from the related phpdbg_watch_ht_info if it has a parent_container
  *
- * A watched zval was removed:
- * * trigger a memory copy (in segv handler) and an automatic deactivation
- * * change a type flag _zval_struct.u1.v.type_flags (add PHPDBG_DESTRUCTED_ZVAL flag) in memory dump
+ * Deleting a watchpoint:
+ * * Remove itself from watch collisions this watchpoint participates in
+ * * Removes the watchpoint from PHPDBG_G(watchpoint_tree) and deactivates it (deactivation of a watchpoint = add PROT_WRITE to the pages the watched pointer resides on)
+ *
+ * A watched pointer is efree()'d:
+ * * Needs immediate action as we else may run into dereferencing a pointer into freed memory
+ * * Deletes the associated watchpoint, and for each watch element, if recursive, all its children elements
+ * * If the its watch elements are implicit, recursive roots or simple, they and all their children are dissociated from their watchpoints (i.e. removed from the watchpoint, if no other element is referencing it, it is deleted); adds these elements to PHPDBG_G(watch_recreation)
  *
- * A watched zval was changed:
- * * check if parent container has a different reference for referenced zval - recursively update all watches and drop them if necessary
- * * if _zval_struct.u1.v.type_flags & PHPDBG_DESTRUCTED_ZVAL, add it to a list of zvals to be handled at the end (if location was not changed, remove it finally)
- * * display changes if watch->flags & PHPDBG_WATCH_NORMAL (means: not implicit)
- * * handle case where Z_RECOUNTED() or Z_PTR() changed (remove/add collison(s))
- * * if necessary ... on zvals: handle references, if recursive also objects and arrays ... on arrays: if recursive, add new elements
- * * drop destructed zval watchpoints which were not updated
+ * Recreating watchpoints:
+ * * Upon each opcode, PHPDBG_G(watch_recreation) is checked and all its elements are searched for whether the watch is still reachable via the tree given by its implicits
+ * * In case they are not reachable, the watch is deleted (and thus all the related watch elements), else a new watchpoint is created for all the watch elements
+ * * The old and new values of the watches are compared and shown if changed
+ *
+ * Comparing watchpoints:
+ * * The old and new values of the watches are compared and shown if changed
+ * * If changed, it is checked whether the refcounted/indirect changed and watch collisions removed or created accordingly
+ * * If a zval/bucket watchpoint is recursive, watch elements are added or removed accordingly
+ * * If an array watchpoint is recursive, new array watchpoints are added if there are new ones in the array
+ * * If the watch (element with an id) is not reachable anymore due to changes in implicits, the watch is removed
  */
 
 #include "zend.h"
@@ -84,32 +117,111 @@ ZEND_EXTERN_MODULE_GLOBALS(phpdbg)
 
 const phpdbg_command_t phpdbg_watch_commands[] = {
        PHPDBG_COMMAND_D_EX(array,      "create watchpoint on an array", 'a', watch_array,     &phpdbg_prompt_commands[24], "s", 0),
-       PHPDBG_COMMAND_D_EX(delete,     "delete watchpoint",             'd', watch_delete,    &phpdbg_prompt_commands[24], "s", 0),
+       PHPDBG_COMMAND_D_EX(delete,     "delete watchpoint",             'd', watch_delete,    &phpdbg_prompt_commands[24], "n", 0),
        PHPDBG_COMMAND_D_EX(recursive,  "create recursive watchpoints",  'r', watch_recursive, &phpdbg_prompt_commands[24], "s", 0),
        PHPDBG_END_COMMAND
 };
 
-//#define HT_FROM_WATCH(watch) (watch->type == WATCH_ON_OBJECT ? watch->addr.obj->handlers->get_properties(watch->parent_container.zv) : watch->type == WATCH_ON_ARRAY ? &watch->addr.arr->ht : NULL)
 #define HT_FROM_ZVP(zvp) (Z_TYPE_P(zvp) == IS_OBJECT ? Z_OBJPROP_P(zvp) : Z_TYPE_P(zvp) == IS_ARRAY ? Z_ARRVAL_P(zvp) : NULL)
 
 #define HT_WATCH_OFFSET (sizeof(zend_refcounted *) + sizeof(uint32_t)) /* we are not interested in gc and flags */
 #define HT_PTR_HT(ptr) ((HashTable *) (((char *) (ptr)) - HT_WATCH_OFFSET))
 #define HT_WATCH_HT(watch) HT_PTR_HT((watch)->addr.ptr)
 
-typedef struct {
-       void *page;
-       size_t size;
-       char reenable_writing;
-       /* data must be last element */
-       void *data;
-} phpdbg_watch_memdump;
+/* ### PRINTING POINTER DIFFERENCES ### */
+zend_bool phpdbg_check_watch_diff(phpdbg_watchtype type, void *oldPtr, void *newPtr) {
+       switch (type) {
+               case WATCH_ON_BUCKET:
+                       if (memcmp(&((Bucket *) oldPtr)->h, &((Bucket *) newPtr)->h, sizeof(Bucket) - sizeof(zval) /* key/val comparison */) != 0) {
+                               return 2;
+                       }
+               case WATCH_ON_ZVAL:
+                       return memcmp(oldPtr, newPtr, sizeof(zend_value) + sizeof(uint32_t) /* value + typeinfo */) != 0;
+               case WATCH_ON_HASHTABLE:
+                       return zend_hash_num_elements(HT_PTR_HT(oldPtr)) != zend_hash_num_elements(HT_PTR_HT(newPtr));
+               case WATCH_ON_REFCOUNTED:
+                       return memcmp(oldPtr, newPtr, sizeof(uint32_t) /* no zend_refcounted metadata info */) != 0;
+               case WATCH_ON_STR:
+                       return memcmp(oldPtr, newPtr, *(size_t *) oldPtr + XtOffsetOf(zend_string, val) - XtOffsetOf(zend_string, len)) != 0;
+               case WATCH_ON_HASHDATA:
+                       ZEND_ASSERT(0);
+       }
+       return 0;
+}
+
+void phpdbg_print_watch_diff(phpdbg_watchtype type, zend_string *name, void *oldPtr, void *newPtr) {
+       int32_t elementDiff;
+
+       PHPDBG_G(watchpoint_hit) = 1;
+
+       phpdbg_notice("watchhit", "variable=\"%s\"", "Breaking on watchpoint %.*s", (int) ZSTR_LEN(name), ZSTR_VAL(name));
+       phpdbg_xml("<watchdata %r>");
+
+       switch (type) {
+               case WATCH_ON_BUCKET:
+               case WATCH_ON_ZVAL:
+                       if (Z_REFCOUNTED_P((zval *) oldPtr)) {
+                               phpdbg_writeln("watchvalue", "type=\"old\" inaccessible=\"inaccessible\"", "Old value inaccessible or destroyed");
+                       } else if (Z_TYPE_P((zval *) oldPtr) == IS_INDIRECT) {
+                               phpdbg_writeln("watchvalue", "type=\"old\" inaccessible=\"inaccessible\"", "Old value inaccessible or destroyed (was indirect)");
+                       } else {
+                               phpdbg_out("Old value: ");
+                               phpdbg_xml("<watchvalue %r type=\"old\">");
+                               zend_print_flat_zval_r((zval *) oldPtr);
+                               phpdbg_xml("</watchvalue>");
+                               phpdbg_out("\n");
+                       }
+
+                       while (Z_TYPE_P((zval *) newPtr) == IS_INDIRECT) {
+                               newPtr = Z_INDIRECT_P((zval *) newPtr);
+                       }
+
+                       phpdbg_out("New value%s: ", Z_ISREF_P((zval *) newPtr) ? " (reference)" : "");
+                       phpdbg_xml("<watchvalue %r%s type=\"new\">", Z_ISREF_P((zval *) newPtr) ? " reference=\"reference\"" : "");
+                       zend_print_flat_zval_r((zval *) newPtr);
+                       phpdbg_xml("</watchvalue>");
+                       phpdbg_out("\n");
+                       break;
+
+               case WATCH_ON_HASHTABLE:
+                       elementDiff = zend_hash_num_elements(HT_PTR_HT(oldPtr)) - zend_hash_num_elements(HT_PTR_HT(newPtr));
+                       if (elementDiff > 0) {
+                               phpdbg_writeln("watchsize", "removed=\"%d\"", "%d elements were removed from the array", (int) elementDiff);
+                       } else if (elementDiff < 0) {
+                               phpdbg_writeln("watchsize", "added=\"%d\"", "%d elements were added to the array", (int) -elementDiff);
+                       }
+                       break;
+
+               case WATCH_ON_REFCOUNTED:
+                       phpdbg_writeln("watchrefcount", "type=\"old\" refcount=\"%d\"", "Old refcount: %d", GC_REFCOUNT((zend_refcounted *) oldPtr));
+                       phpdbg_writeln("watchrefcount", "type=\"new\" refcount=\"%d\"", "New refcount: %d", GC_REFCOUNT((zend_refcounted *) newPtr));
+                       break;
+
+               case WATCH_ON_STR:
+                       phpdbg_out("Old value: ");
+                       phpdbg_xml("<watchvalue %r type=\"old\">");
+                       zend_write((char *) oldPtr + XtOffsetOf(zend_string, val) - XtOffsetOf(zend_string, len), *(size_t *) oldPtr);
+                       phpdbg_xml("</watchvalue>");
+                       phpdbg_out("\n");
+
+                       phpdbg_out("New value: ");
+                       phpdbg_xml("<watchvalue %r type=\"new\">");
+                       zend_write((char *) newPtr + XtOffsetOf(zend_string, val) - XtOffsetOf(zend_string, len), *(size_t *) newPtr);
+                       phpdbg_xml("</watchvalue>");
+                       phpdbg_out("\n");
+                       break;
 
-#define MEMDUMP_SIZE(size) (sizeof(phpdbg_watch_memdump) - sizeof(void *) + (size))
+               case WATCH_ON_HASHDATA:
+                       ZEND_ASSERT(0);
+       }
 
+       phpdbg_xml("</watchdata>");
+}
 
+/* ### LOW LEVEL WATCHPOINT HANDLING ### */
 static phpdbg_watchpoint_t *phpdbg_check_for_watchpoint(void *addr) {
        phpdbg_watchpoint_t *watch;
-       phpdbg_btree_result *result = phpdbg_btree_find_closest(&PHPDBG_G(watchpoint_tree), (zend_ulong)phpdbg_get_page_boundary(addr) + phpdbg_pagesize - 1);
+       phpdbg_btree_result *result = phpdbg_btree_find_closest(&PHPDBG_G(watchpoint_tree), (zend_ulong) phpdbg_get_page_boundary(addr) + phpdbg_pagesize - 1);
 
        if (result == NULL) {
                return NULL;
@@ -139,1030 +251,1183 @@ static inline void phpdbg_deactivate_watchpoint(phpdbg_watchpoint_t *watch) {
        phpdbg_change_watchpoint_access(watch, PROT_READ | PROT_WRITE);
 }
 
-static inline void phpdbg_store_watchpoint(phpdbg_watchpoint_t *watch) {
+/* Note that consecutive pages need to be merged in order to avoid watchpoints spanning page boundaries to have part of their data in the one page, part in the other page */
+#ifdef _WIN32
+int phpdbg_watchpoint_segfault_handler(void *addr) {
+#else
+int phpdbg_watchpoint_segfault_handler(siginfo_t *info, void *context) {
+#endif
+
+       void *page = phpdbg_get_page_boundary(
+#ifdef _WIN32
+               addr
+#else
+               info->si_addr
+#endif
+       );
+
+       /* perhaps unnecessary, but check to be sure to not conflict with other segfault handlers */
+       if (phpdbg_check_for_watchpoint(page) == NULL) {
+               return FAILURE;
+       }
+
+       /* re-enable writing */
+       mprotect(page, phpdbg_pagesize, PROT_READ | PROT_WRITE);
+
+       zend_hash_index_add_empty_element(PHPDBG_G(watchlist_mem), (zend_ulong) page);
+
+       return SUCCESS;
+}
+
+/* ### REGISTER WATCHPOINT ### To be used only by watch element and collision managers ### */
+static inline void phpdbg_store_watchpoint_btree(phpdbg_watchpoint_t *watch) {
+       phpdbg_btree_result *res;
+       ZEND_ASSERT((res = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) watch->addr.ptr)) == NULL || res->ptr == watch);
        phpdbg_btree_insert(&PHPDBG_G(watchpoint_tree), (zend_ulong) watch->addr.ptr, watch);
 }
 
-static inline void phpdbg_remove_watchpoint(phpdbg_watchpoint_t *watch) {
+static inline void phpdbg_remove_watchpoint_btree(phpdbg_watchpoint_t *watch) {
        phpdbg_btree_delete(&PHPDBG_G(watchpoint_tree), (zend_ulong) watch->addr.ptr);
 }
 
-void phpdbg_create_addr_watchpoint(void *addr, size_t size, phpdbg_watchpoint_t *watch) {
+/* ### SET WATCHPOINT ADDR ### To be used only by watch element and collision managers ### */
+void phpdbg_set_addr_watchpoint(void *addr, size_t size, phpdbg_watchpoint_t *watch) {
        watch->addr.ptr = addr;
        watch->size = size;
+       watch->ref = NULL;
+       watch->coll = NULL;
+       zend_hash_init(&watch->elements, 8, brml, NULL, 0);
 }
 
-void phpdbg_create_zval_watchpoint(zval *zv, phpdbg_watchpoint_t *watch) {
-       phpdbg_create_addr_watchpoint(zv, sizeof(zval), watch);
+void phpdbg_set_zval_watchpoint(zval *zv, phpdbg_watchpoint_t *watch) {
+       phpdbg_set_addr_watchpoint(zv, sizeof(zval) - sizeof(uint32_t), watch);
        watch->type = WATCH_ON_ZVAL;
 }
 
-void phpdbg_create_ht_watchpoint(HashTable *ht, phpdbg_watchpoint_t *watch) {
-       phpdbg_create_addr_watchpoint(((char *) ht) + HT_WATCH_OFFSET, sizeof(HashTable) - HT_WATCH_OFFSET, watch);
-       watch->type = WATCH_ON_HASHTABLE;
-       watch->implicit_ht_count = 0;
+void phpdbg_set_bucket_watchpoint(Bucket *bucket, phpdbg_watchpoint_t *watch) {
+       phpdbg_set_addr_watchpoint(bucket, sizeof(Bucket), watch);
+       watch->type = WATCH_ON_BUCKET;
 }
 
-static int phpdbg_create_recursive_ht_watch(phpdbg_watchpoint_t *watch);
-static int phpdbg_create_recursive_zval_watch(phpdbg_watchpoint_t *watch);
-
-void phpdbg_watch_HashTable_dtor(zval *ptr);
+void phpdbg_set_ht_watchpoint(HashTable *ht, phpdbg_watchpoint_t *watch) {
+       phpdbg_set_addr_watchpoint(((char *) ht) + HT_WATCH_OFFSET, sizeof(HashTable) - HT_WATCH_OFFSET, watch);
+       watch->type = WATCH_ON_HASHTABLE;
+}
 
-static void phpdbg_free_watch(phpdbg_watchpoint_t *watch) {
-       zend_string_release(watch->str);
-       zend_string_release(watch->name_in_parent);
+void phpdbg_watch_backup_data(phpdbg_watchpoint_t *watch) {
+       switch (watch->type) {
+               case WATCH_ON_BUCKET:
+               case WATCH_ON_ZVAL:
+               case WATCH_ON_REFCOUNTED:
+                       memcpy(&watch->backup, watch->addr.ptr, watch->size);
+                       break;
+               case WATCH_ON_STR:
+                       if (watch->backup.str) {
+                               zend_string_release(watch->backup.str);
+                       }
+                       watch->backup.str = zend_string_init((char *) watch->addr.ptr + XtOffsetOf(zend_string, val) - XtOffsetOf(zend_string, len), *(size_t *) watch->addr.ptr, 1);
+                       break;
+               case WATCH_ON_HASHTABLE:
+                       memcpy((char *) &watch->backup + HT_WATCH_OFFSET, watch->addr.ptr, watch->size);
+               case WATCH_ON_HASHDATA:
+                       break;
+       }
 }
 
-static int phpdbg_delete_watchpoint(phpdbg_watchpoint_t *tmp_watch);
-static void phpdbg_delete_ht_watchpoints_recursive(phpdbg_watchpoint_t *watch);
-static void phpdbg_delete_zval_watchpoints_recursive(phpdbg_watchpoint_t *watch);
-static void phpdbg_delete_watchpoints_recursive(phpdbg_watchpoint_t *watch);
-
-/* Store all the possible watches the refcounted may refer to (for displaying & deleting by identifier) [collision] */
-static phpdbg_watchpoint_t *phpdbg_create_refcounted_watchpoint(phpdbg_watchpoint_t *parent, zend_refcounted *ref) {
-       phpdbg_watchpoint_t *watch = emalloc(sizeof(phpdbg_watchpoint_t));
-       watch->flags = parent->flags;
-       watch->parent = parent;
-       watch->str = parent->str;
-       ++GC_REFCOUNT(parent->str);
-       phpdbg_create_addr_watchpoint(&GC_REFCOUNT(ref), sizeof(uint32_t), watch);
-       watch->type = WATCH_ON_REFCOUNTED;
+/* ### MANAGE WATCH COLLISIONS ### To be used only by watch element manager and memory differ ### */
+/* watch collisions are responsible for having only one watcher on a given refcounted/refval and having a mapping back to the parent zvals */
+void phpdbg_delete_watch_collision(phpdbg_watchpoint_t *watch) {
+       phpdbg_watch_collision *coll;
+       if ((coll = zend_hash_index_find_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref))) {
+               zend_hash_index_del(&coll->parents, (zend_ulong) watch);
+               if (zend_hash_num_elements(&coll->parents) == 0) {
+                       phpdbg_deactivate_watchpoint(&coll->ref);
+                       phpdbg_remove_watchpoint_btree(&coll->ref);
+
+                       if (coll->ref.type == WATCH_ON_ZVAL) {
+                               phpdbg_delete_watch_collision(&coll->ref);
+                       } else if (coll->reference.addr.ptr) {
+                               phpdbg_deactivate_watchpoint(&coll->reference);
+                               phpdbg_remove_watchpoint_btree(&coll->reference);
+                               phpdbg_delete_watch_collision(&coll->reference);
+                               if (coll->reference.type == WATCH_ON_STR) {
+                                       zend_string_release(coll->reference.backup.str);
+                               }
+                       }
 
-       return watch;
+                       zend_hash_index_del(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref);
+                       zend_hash_destroy(&coll->parents);
+                       efree(coll);
+               }
+       }
 }
 
-/* Must prevent duplicates ... if there are duplicates, replace new by old! */
-static void phpdbg_add_watch_collision(phpdbg_watchpoint_t *watch) {
-       phpdbg_watch_collision *cur;
-
-       /* Check for either recursive or (simple and/or implicit) */
-       ZEND_ASSERT(((watch->flags & PHPDBG_WATCH_RECURSIVE) == 0) ^ ((watch->flags & (PHPDBG_WATCH_IMPLICIT | PHPDBG_WATCH_SIMPLE)) == 0));
+void phpdbg_update_watch_ref(phpdbg_watchpoint_t *watch) {
+       phpdbg_watch_collision *coll;
 
-       if ((cur = zend_hash_index_find_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->addr.ref))) {
-               phpdbg_watchpoint_t *old;
-               int flags = 0;
-               if ((old = zend_hash_find_ptr(&cur->watches, watch->str)) || (old = zend_hash_find_ptr(&cur->implicit_watches, watch->str))) {
-                       if (((old->flags ^ watch->flags) & (PHPDBG_WATCH_NORMAL|PHPDBG_WATCH_IMPLICIT)) == 0) {
-                               return; /* there was no change ... */
-                       }
+       ZEND_ASSERT(watch->type == WATCH_ON_ZVAL || watch->type == WATCH_ON_BUCKET);
+       if (Z_REFCOUNTED_P(watch->addr.zv)) {
+               if (Z_COUNTED_P(watch->addr.zv) == watch->ref) {
+                       return;
+               }
 
-                       flags = old->flags;
+               if (watch->ref != NULL) {
+                       phpdbg_delete_watch_collision(watch);
+               }
 
-                       if (flags & PHPDBG_WATCH_RECURSIVE) {
-                               if (!(watch->flags & PHPDBG_WATCH_RECURSIVE) && !--cur->refs) {
-                                       phpdbg_delete_watchpoints_recursive(watch);
-                               }
-                       }
-                       if (flags & PHPDBG_WATCH_NORMAL) {
-                               zend_hash_del(&cur->watches, watch->str);
-                               if (zend_hash_num_elements(&cur->watches) > 0) {
-                                       cur->watch = Z_PTR_P(zend_hash_get_current_data_ex(&cur->watches, NULL));
-                               } else {
-                                       cur->watch = Z_PTR_P(zend_hash_get_current_data_ex(&cur->implicit_watches, NULL));
-                               }
-                       }
-                       if (flags & PHPDBG_WATCH_IMPLICIT) {
-                               zend_hash_del(&cur->implicit_watches, watch->str);
+               watch->ref = Z_COUNTED_P(watch->addr.zv);
+
+               if (!(coll = zend_hash_index_find_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref))) {
+                       coll = emalloc(sizeof(*coll));
+                       coll->ref.type = WATCH_ON_REFCOUNTED;
+                       phpdbg_set_addr_watchpoint(Z_COUNTED_P(watch->addr.zv), sizeof(uint32_t), &coll->ref);
+                       coll->ref.coll = coll;
+                       phpdbg_store_watchpoint_btree(&coll->ref);
+                       phpdbg_activate_watchpoint(&coll->ref);
+                       phpdbg_watch_backup_data(&coll->ref);
+
+                       if (Z_ISREF_P(watch->addr.zv)) {
+                               phpdbg_set_zval_watchpoint(Z_REFVAL_P(watch->addr.zv), &coll->reference);
+                               coll->reference.coll = coll;
+                               phpdbg_update_watch_ref(&coll->reference);
+                               phpdbg_store_watchpoint_btree(&coll->reference);
+                               phpdbg_activate_watchpoint(&coll->reference);
+                               phpdbg_watch_backup_data(&coll->reference);
+                       } else if (Z_TYPE_P(watch->addr.zv) == IS_STRING) {
+                               coll->reference.type = WATCH_ON_STR;
+                               phpdbg_set_addr_watchpoint(&Z_STRLEN_P(watch->addr.zv), XtOffsetOf(zend_string, val) - XtOffsetOf(zend_string, len) + Z_STRLEN_P(watch->addr.zv) + 1, &coll->reference);
+                               coll->reference.coll = coll;
+                               phpdbg_store_watchpoint_btree(&coll->reference);
+                               phpdbg_activate_watchpoint(&coll->reference);
+                               coll->reference.backup.str = NULL;
+                               phpdbg_watch_backup_data(&coll->reference);
+                       } else {
+                               coll->reference.addr.ptr = NULL;
                        }
 
-                       old->flags = watch->flags;
-                       phpdbg_free_watch(watch);
-                       efree(watch);
-                       watch = old;
+                       zend_hash_init(&coll->parents, 8, shitty stupid parameter, NULL, 0);
+                       zend_hash_index_add_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref, coll);
                }
-               if (watch->flags & PHPDBG_WATCH_RECURSIVE) {
-                       if (!(flags & PHPDBG_WATCH_RECURSIVE) && !cur->refs++) {
-                               phpdbg_create_recursive_zval_watch(watch->parent);
-                       }
+               zend_hash_index_add_ptr(&coll->parents, (zend_long) watch, watch);
+       } else if (Z_TYPE_P(watch->addr.zv) == IS_INDIRECT) {
+               if ((zend_refcounted *) Z_INDIRECT_P(watch->addr.zv) == watch->ref) {
+                       return;
                }
-       } else {
-               phpdbg_watch_collision coll;
-               coll.refs = (watch->flags & PHPDBG_WATCH_RECURSIVE) != 0;
-               coll.watch = watch;
-               zend_hash_init(&coll.watches, 8, arghs, NULL, 0);
-               zend_hash_init(&coll.implicit_watches, 8, ..., NULL, 0);
-               cur = zend_hash_index_add_mem(&PHPDBG_G(watch_collisions), (zend_ulong) watch->addr.ref, &coll, sizeof(phpdbg_watch_collision));
-               phpdbg_store_watchpoint(cur->watch);
-               phpdbg_activate_watchpoint(cur->watch);
-               if (coll.refs) {
-                       phpdbg_create_recursive_zval_watch(watch->parent);
+
+               if (watch->ref != NULL) {
+                       phpdbg_delete_watch_collision(watch);
                }
-       }
 
-       if (watch->flags & PHPDBG_WATCH_NORMAL) {
-               cur->watch = watch;
-               zend_hash_add_ptr(&cur->watches, watch->str, watch->parent);
-       }
-       if (watch->flags & PHPDBG_WATCH_IMPLICIT) {
-               zend_hash_add_ptr(&cur->implicit_watches, watch->str, watch->parent);
+               watch->ref = (zend_refcounted *) Z_INDIRECT_P(watch->addr.zv);
+
+               if (!(coll = zend_hash_index_find_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref))) {
+                       coll = emalloc(sizeof(*coll));
+                       phpdbg_set_zval_watchpoint(Z_INDIRECT_P(watch->addr.zv), &coll->ref);
+                       coll->ref.coll = coll;
+                       phpdbg_update_watch_ref(&coll->ref);
+                       phpdbg_store_watchpoint_btree(&coll->ref);
+                       phpdbg_activate_watchpoint(&coll->ref);
+                       phpdbg_watch_backup_data(&coll->ref);
+                       
+                       zend_hash_init(&coll->parents, 8, shitty stupid parameter, NULL, 0);
+                       zend_hash_index_add_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) watch->ref, coll);
+               }
+               zend_hash_index_add_ptr(&coll->parents, (zend_long) watch, watch);
+       } else if (watch->ref) {
+               phpdbg_delete_watch_collision(watch);
+               watch->ref = NULL;
        }
 }
 
-static void phpdbg_remove_watch_collision(phpdbg_watchpoint_t *watch) {
-       phpdbg_watch_collision *cur;
-       if ((cur = zend_hash_index_find_ptr(&PHPDBG_G(watch_collisions), (zend_ulong) Z_COUNTED_P(watch->addr.zv)))) {
-               if (cur->refs && !--cur->refs) {
-                       phpdbg_delete_watchpoints_recursive(watch);
-               }
+/* ### MANAGE WATCH ELEMENTS ### */
+/* watchpoints must be unique per element. Only one watchpoint may point to one element. But many elements may point to one watchpoint. */
+void phpdbg_recurse_watch_element(phpdbg_watch_element *element);
+void phpdbg_remove_watch_element_recursively(phpdbg_watch_element *element);
+void phpdbg_free_watch_element(phpdbg_watch_element *element);
+void phpdbg_remove_watchpoint(phpdbg_watchpoint_t *watch);
+void phpdbg_watch_parent_ht(phpdbg_watch_element *element);
 
-               zend_hash_del(&cur->watches, watch->str);
-               zend_hash_del(&cur->implicit_watches, watch->str);
+phpdbg_watch_element *phpdbg_add_watch_element(phpdbg_watchpoint_t *watch, phpdbg_watch_element *element) {
+       phpdbg_btree_result *res;
+       if ((res = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) watch->addr.ptr)) == NULL) {
+               phpdbg_watchpoint_t *mem = emalloc(sizeof(*mem));
+               *mem = *watch;
+               watch = mem;
+               phpdbg_store_watchpoint_btree(watch);
+               if (watch->type == WATCH_ON_ZVAL || watch->type == WATCH_ON_BUCKET) {
+                       phpdbg_update_watch_ref(watch);
+               }
+               phpdbg_activate_watchpoint(watch);
+               phpdbg_watch_backup_data(watch);
+       } else {
+               phpdbg_watch_element *old_element;
+               watch = res->ptr;
+               if ((old_element = zend_hash_find_ptr(&watch->elements, element->str))) {
+                       phpdbg_free_watch_element(element);
+                       return old_element;
+               }
+       }
 
-               if (zend_hash_num_elements(&cur->watches) > 0) {
-                       cur->watch = Z_PTR_P(zend_hash_get_current_data_ex(&cur->watches, NULL));
-               } else if (zend_hash_num_elements(&cur->implicit_watches) > 0) {
-                       cur->watch = Z_PTR_P(zend_hash_get_current_data_ex(&cur->implicit_watches, NULL));
-               } else {
-                       phpdbg_deactivate_watchpoint(cur->watch);
-                       phpdbg_remove_watchpoint(cur->watch);
+       element->watch = watch;
+       zend_hash_add_ptr(&watch->elements, element->str, element);
 
-                       zend_hash_index_del(&PHPDBG_G(watch_collisions), (zend_ulong) Z_COUNTED_P(watch->addr.zv));
-               }
+       if (element->flags & PHPDBG_WATCH_RECURSIVE) {
+               phpdbg_recurse_watch_element(element);
        }
+
+       return element;
 }
 
-static phpdbg_watchpoint_t *phpdbg_create_watchpoint(phpdbg_watchpoint_t *watch);
+phpdbg_watch_element *phpdbg_add_bucket_watch_element(Bucket *bucket, phpdbg_watch_element *element) {
+       phpdbg_watchpoint_t watch;
+       phpdbg_set_bucket_watchpoint(bucket, &watch);
+       element = phpdbg_add_watch_element(&watch, element);
+       phpdbg_watch_parent_ht(element);
+       return element;
+}
 
-static phpdbg_watchpoint_t *phpdbg_create_reference_watch(phpdbg_watchpoint_t *watch) {
-       phpdbg_watchpoint_t *ref = emalloc(sizeof(phpdbg_watchpoint_t));
-       watch->reference = ref;
-       ref->flags = watch->flags;
-       ref->str = watch->str;
-       ++GC_REFCOUNT(ref->str);
-       ref->parent = watch;
-       ref->parent_container = NULL;
-       phpdbg_create_zval_watchpoint(Z_REFVAL_P(watch->addr.zv), ref);
+phpdbg_watch_element *phpdbg_add_ht_watch_element(zval *zv, phpdbg_watch_element *element) {
+       phpdbg_watchpoint_t watch;
+       HashTable *ht = HT_FROM_ZVP(zv);
 
-       phpdbg_create_watchpoint(ref);
+       if (!ht) {
+               return NULL;
+       }
 
-       return ref;
+       element->flags |= Z_TYPE_P(zv) == IS_ARRAY ? PHPDBG_WATCH_ARRAY : PHPDBG_WATCH_OBJECT;
+       phpdbg_set_ht_watchpoint(ht, &watch);
+       return phpdbg_add_watch_element(&watch, element);
 }
 
-static phpdbg_watchpoint_t *phpdbg_get_refcount_watch(phpdbg_watchpoint_t *parent) {
-       zend_refcounted *ref;
-       phpdbg_btree_result *res;
-
-       if (parent->type == WATCH_ON_HASHTABLE) {
-               parent = parent->parent;
-               if (!parent) {
-                       return NULL;
+zend_bool phpdbg_is_recursively_watched(void *ptr, phpdbg_watch_element *element) {
+       phpdbg_watch_element *next = element;
+       do {
+               element = next;
+               if (element->watch->addr.ptr == ptr) {
+                       return 1;
                }
-       }
+               next = element->parent;
+       } while (!(element->flags & PHPDBG_WATCH_RECURSIVE_ROOT));
 
-       ZEND_ASSERT(parent->type == WATCH_ON_ZVAL);
-       ref = Z_COUNTED_P(parent->addr.zv);
+       return 0;
+}
+
+void phpdbg_add_recursive_watch_from_ht(phpdbg_watch_element *element, zend_long idx, zend_string *str, zval *zv) {
+       phpdbg_watch_element *child;
+       if (phpdbg_is_recursively_watched(zv, element)) {
+               return;
+       }
 
-       res = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) ref);
-       if (res) {
-               return res->ptr;
+       child = emalloc(sizeof(*child));
+       child->flags = PHPDBG_WATCH_RECURSIVE;
+       if (str) {
+               child->str = strpprintf(0, (element->flags & PHPDBG_WATCH_ARRAY) ? "%.*s[%s]" : "%.*s->%s", (int) ZSTR_LEN(element->str) - 2, ZSTR_VAL(element->str), phpdbg_get_property_key(ZSTR_VAL(str)));
+       } else {
+               child->str = strpprintf(0, (element->flags & PHPDBG_WATCH_ARRAY) ? "%.*s[" ZEND_LONG_FMT "]" : "%.*s->" ZEND_LONG_FMT, (int) ZSTR_LEN(element->str) - 2, ZSTR_VAL(element->str), idx);
        }
-       return NULL;
+       if (!str) {
+               str = zend_long_to_str(idx); // TODO: hack, use proper int handling for name in parent
+       } else { str = zend_string_copy(str); }
+       child->name_in_parent = str;
+       child->parent = element;
+       child->child = NULL;
+       child->parent_container = HT_WATCH_HT(element->watch);
+       zend_hash_add_ptr(&element->child_container, child->str, child);
+       phpdbg_add_bucket_watch_element((Bucket *) zv, child);
 }
 
-static phpdbg_watchpoint_t *phpdbg_create_watchpoint(phpdbg_watchpoint_t *watch) {
-       phpdbg_watchpoint_t *ret = watch;
+void phpdbg_recurse_watch_element(phpdbg_watch_element *element) {
+       phpdbg_watch_element *child;
+       zval *zv;
 
-       if (watch->type == WATCH_ON_ZVAL) {
-               switch (Z_TYPE_P(watch->addr.zv)) {
-                       case IS_NULL:
-                       case IS_UNDEF:
-                       case IS_TRUE:
-                       case IS_FALSE:
-                               memset(watch->addr.zv, 0, sizeof(zend_value));
+       if (element->watch->type == WATCH_ON_ZVAL || element->watch->type == WATCH_ON_BUCKET) {
+               zv = element->watch->addr.zv;
+               while (Z_TYPE_P(zv) == IS_INDIRECT) {
+                       zv = Z_INDIRECT_P(zv);
                }
-       }
+               ZVAL_DEREF(zv);
 
-       /* exclude references & refcounted */
-       if (!watch->parent || watch->parent->type != WATCH_ON_ZVAL || watch->type == WATCH_ON_HASHTABLE) {
-               phpdbg_watchpoint_t *old_watch = zend_hash_find_ptr(&PHPDBG_G(watchpoints), watch->str);
-
-               if (old_watch) {
-#define return_and_free_watch(x) { \
-       phpdbg_watchpoint_t *ref = phpdbg_get_refcount_watch(old_watch); \
-       if (ref) { \
-               phpdbg_add_watch_collision(ref); \
-       } \
-       if (watch != old_watch) { \
-               phpdbg_free_watch(watch); \
-               efree(watch); \
-       } \
-       return (x); \
-}
-                       if (watch->flags & PHPDBG_WATCH_RECURSIVE) {
-                               if (old_watch->flags & PHPDBG_WATCH_RECURSIVE) {
-                                       return_and_free_watch(NULL);
-                               } else {
-                                       old_watch->flags &= ~(PHPDBG_WATCH_SIMPLE | PHPDBG_WATCH_IMPLICIT);
-                                       old_watch->flags |= PHPDBG_WATCH_RECURSIVE;
-                                       return_and_free_watch(old_watch);
-                               }
-                       } else {
-                               if (!(old_watch->flags & PHPDBG_WATCH_RECURSIVE)) {
-                                       old_watch->flags |= watch->flags & (PHPDBG_WATCH_IMPLICIT | PHPDBG_WATCH_SIMPLE);
-                               }
-                               return_and_free_watch(NULL);
+               if (element->child) {
+                       phpdbg_remove_watch_element_recursively(element->child);
+               }
+
+               if ((Z_TYPE_P(zv) != IS_ARRAY && Z_TYPE_P(zv) != IS_OBJECT)
+                   || phpdbg_is_recursively_watched(HT_WATCH_OFFSET + (char *) HT_FROM_ZVP(zv), element)) {
+                       if (element->child) {
+                               phpdbg_free_watch_element(element->child);
+                               element->child = NULL;
                        }
+                       return;
+               }
+
+               if (element->child) {
+                       child = element->child;
                } else {
-                       if (watch->parent && watch->parent->type == WATCH_ON_HASHTABLE) {
-                               watch->parent->implicit_ht_count++;
-                       }
-                       zend_hash_add_ptr(&PHPDBG_G(watchpoints), watch->str, watch);
+                       child = emalloc(sizeof(*child));
+                       child->flags = PHPDBG_WATCH_RECURSIVE;
+                       child->str = strpprintf(0, "%.*s[]", (int) ZSTR_LEN(element->str), ZSTR_VAL(element->str));
+                       child->name_in_parent = NULL;
+                       child->parent = element;
+                       child->child = NULL;
+                       element->child = child;
                }
+               zend_hash_init(&child->child_container, 8, NULL, NULL, 0);
+               phpdbg_add_ht_watch_element(zv, child);
+       } else if (zend_hash_num_elements(&element->child_container) == 0) {
+               zend_string *str;
+               zend_long idx;
+
+               ZEND_ASSERT(element->watch->type == WATCH_ON_HASHTABLE);
+               ZEND_HASH_FOREACH_KEY_VAL(HT_WATCH_HT(element->watch), idx, str, zv) {
+                       phpdbg_add_recursive_watch_from_ht(element, idx, str, zv);
+               } ZEND_HASH_FOREACH_END();
        }
+}
+
+void phpdbg_watch_parent_ht(phpdbg_watch_element *element) {
+       if (element->watch->type == WATCH_ON_BUCKET) {
+               phpdbg_btree_result *res;
+               HashPosition pos;
+               phpdbg_watch_ht_info *hti;
+               ZEND_ASSERT(element->parent_container);
+               if (!(res = phpdbg_btree_find(&PHPDBG_G(watch_HashTables), (zend_ulong) element->parent_container))) {
+                       hti = emalloc(sizeof(*hti));
+                       hti->ht = element->parent_container;
+
+                       zend_hash_init(&hti->watches, 0, grrrrr, ZVAL_PTR_DTOR, 0);
+                       phpdbg_btree_insert(&PHPDBG_G(watch_HashTables), (zend_ulong) hti->ht, hti);
+
+                       phpdbg_set_addr_watchpoint(HT_GET_DATA_ADDR(hti->ht), HT_HASH_SIZE(hti->ht->nTableMask), &hti->hash_watch);
+                       hti->hash_watch.type = WATCH_ON_HASHDATA;
+                       phpdbg_store_watchpoint_btree(&hti->hash_watch);
+                       phpdbg_activate_watchpoint(&hti->hash_watch);
+               } else {
+                       hti = (phpdbg_watch_ht_info *) res->ptr;
+               }
 
-       phpdbg_store_watchpoint(watch);
+               zend_hash_internal_pointer_end_ex(hti->ht, &pos);
+               hti->last = hti->ht->arData + pos;
+               hti->last_str = hti->last->key;
+               hti->last_idx = hti->last->h;
 
-       if (watch->parent && watch->parent->type == WATCH_ON_ZVAL && Z_REFCOUNTED_P(watch->parent->addr.zv)) {
-               phpdbg_add_watch_collision(phpdbg_create_refcounted_watchpoint(watch, Z_COUNTED_P(watch->parent->addr.zv)));
+               zend_hash_add_ptr(&hti->watches, element->name_in_parent, element);
        }
+}
+
+void phpdbg_unwatch_parent_ht(phpdbg_watch_element *element) {
+       if (element->watch->type == WATCH_ON_BUCKET) {
+               phpdbg_btree_result *res = phpdbg_btree_find(&PHPDBG_G(watch_HashTables), (zend_ulong) element->parent_container);
+               ZEND_ASSERT(element->parent_container);
+               if (res) {
+                       phpdbg_watch_ht_info *hti = res->ptr;
 
-       if (watch->type == WATCH_ON_ZVAL) {
-               if (watch->parent_container) {
-                       HashTable *ht_watches;
-                       phpdbg_btree_result *find;
-                       if (!(find = phpdbg_btree_find(&PHPDBG_G(watch_HashTables), (zend_ulong) watch->parent_container))) {
-                               phpdbg_watch_ht_info *hti = emalloc(sizeof(*hti));
-                               hti->dtor = watch->parent_container->pDestructor;
-                               ht_watches = &hti->watches;
-                               zend_hash_init(ht_watches, 0, grrrrr, ZVAL_PTR_DTOR, 0);
-                               phpdbg_btree_insert(&PHPDBG_G(watch_HashTables), (zend_ulong) watch->parent_container, hti);
-                               watch->parent_container->pDestructor = (dtor_func_t) phpdbg_watch_HashTable_dtor;
+                       if (zend_hash_num_elements(&hti->watches) == 1) {
+                               zend_hash_destroy(&hti->watches);
+                               phpdbg_btree_delete(&PHPDBG_G(watch_HashTables), (zend_ulong) hti->ht);
+                               phpdbg_deactivate_watchpoint(&hti->hash_watch);
+                               phpdbg_remove_watchpoint_btree(&hti->hash_watch);
+                               efree(hti);
                        } else {
-                               ht_watches = &((phpdbg_watch_ht_info *) find->ptr)->watches;
+                               zend_hash_del(&hti->watches, element->name_in_parent);
                        }
-                       zend_hash_add_ptr(ht_watches, watch->name_in_parent, watch);
-               }
-
-               if (Z_ISREF_P(watch->addr.zv)) {
-                       ret = phpdbg_create_reference_watch(watch);
                }
        }
+}
 
-       phpdbg_activate_watchpoint(watch);
+/* ### DE/QUEUE WATCH ELEMENTS ### to be used by watch element manager only */
+/* implicit watchpoints may change (especially because of separation); elements updated by remove & re-add etc.; thus we need to wait a little bit (until next opcode) and then compare whether the watchpoint still exists and if not, remove it */
 
-       return ret;
-}
+void phpdbg_dissociate_watch_element(phpdbg_watch_element *element, phpdbg_watch_element *until);
+void phpdbg_free_watch_element_tree(phpdbg_watch_element *element);
 
-static int phpdbg_create_simple_watchpoint(phpdbg_watchpoint_t *watch) {
-       watch->flags |= PHPDBG_WATCH_SIMPLE;
+void phpdbg_queue_element_for_recreation(phpdbg_watch_element *element) {
+       /* store lowermost element */
+       phpdbg_watch_element *prev;
 
-       phpdbg_create_watchpoint(watch);
+       if ((prev = zend_hash_find_ptr(&PHPDBG_G(watch_recreation), element->str))) {
+               phpdbg_watch_element *child = prev;
+               do {
+                       if (child == element) {
+                               return;
+                       }
+                       child = child->child;
+               } while (child);
+       }
+       zend_hash_update_ptr(&PHPDBG_G(watch_recreation), element->str, element);
 
-       return SUCCESS;
-}
+       /* dissociate from watchpoint to avoid dangling memory watches */
+       phpdbg_dissociate_watch_element(element, prev);
 
-static int phpdbg_create_array_watchpoint(phpdbg_watchpoint_t *zv_watch) {
-       zval *zv = zv_watch->addr.zv;
-       phpdbg_watchpoint_t *watch = emalloc(sizeof(phpdbg_watchpoint_t));
-       HashTable *ht = HT_FROM_ZVP(zv);
+       if (!element->parent) {
+               /* HERE BE DRAGONS; i.e. we assume HashTable is directly allocated via emalloc() ... (which *should be* the case for every user-accessible array and symbol tables) */
+               zend_hash_index_add_empty_element(&PHPDBG_G(watch_free), (zend_ulong) element->parent_container);
+       }
+}
 
-       watch->parent = zv_watch;
+zend_bool phpdbg_try_readding_watch_element(zval *parent, phpdbg_watch_element *element) {
+       zval *zv;
+       HashTable *ht = HT_FROM_ZVP(parent);
 
        if (!ht) {
-               return FAILURE;
-       }
+               return 0;
+       } else if (element->flags & (PHPDBG_WATCH_ARRAY | PHPDBG_WATCH_OBJECT)) {
+               char *htPtr = ((char *) ht) + HT_WATCH_OFFSET;
+               char *oldPtr = ((char *) &element->backup.ht) + HT_WATCH_OFFSET;
+               if (phpdbg_check_watch_diff(WATCH_ON_HASHTABLE, oldPtr, htPtr)) {
+                       phpdbg_print_watch_diff(WATCH_ON_HASHTABLE, element->str, oldPtr, htPtr);
+               }
 
-       phpdbg_create_ht_watchpoint(ht, watch);
+               phpdbg_add_ht_watch_element(parent, element);
+       } else if ((zv = zend_symtable_find(ht, element->name_in_parent))) {
+               if (element->flags & PHPDBG_WATCH_IMPLICIT) {
+                       zval *next = zv;
 
-       if (phpdbg_create_watchpoint(watch) == NULL) {
-               return SUCCESS;
-       }
+                       while (Z_TYPE_P(next) == IS_INDIRECT) {
+                               next = Z_INDIRECT_P(next);
+                       }
+                       if (Z_ISREF_P(next)) {
+                               next = Z_REFVAL_P(next);
+                       }
+
+                       if (!phpdbg_try_readding_watch_element(next, element->child)) {
+                               return 0;
+                       }
+               } else if (phpdbg_check_watch_diff(WATCH_ON_ZVAL, &element->backup.zv, zv)) {
+                       phpdbg_print_watch_diff(WATCH_ON_ZVAL, element->str, &element->backup.zv, zv);
+               }
 
-       if (Z_TYPE_P(zv) == IS_ARRAY) {
-               watch->flags |= PHPDBG_WATCH_ARRAY;
+               element->parent_container = ht;
+               phpdbg_add_bucket_watch_element((Bucket *) zv, element);
+               phpdbg_watch_parent_ht(element);
        } else {
-               watch->flags |= PHPDBG_WATCH_OBJECT;
+               return 0;
        }
 
-       phpdbg_add_watch_collision(phpdbg_create_refcounted_watchpoint(watch, Z_COUNTED_P(zv)));
-
-       return SUCCESS;
+       return 1;
 }
 
-static int phpdbg_create_recursive_watchpoint(phpdbg_watchpoint_t *watch) {
-       if (watch->type != WATCH_ON_ZVAL) {
-               return FAILURE;
+void phpdbg_automatic_dequeue_free(phpdbg_watch_element *element) {
+       phpdbg_watch_element *child = element;
+       while (child->child && !(child->flags & PHPDBG_WATCH_RECURSIVE_ROOT)) {
+               child = child->child;
        }
-
-       watch->flags |= PHPDBG_WATCH_RECURSIVE;
-       watch = phpdbg_create_watchpoint(watch);
-
-       return SUCCESS;
+       PHPDBG_G(watchpoint_hit) = 1;
+       phpdbg_notice("watchdelete", "variable=\"%.*s\" recursive=\"%s\"", "%.*s has been removed, removing watchpoint%s", (int) ZSTR_LEN(child->str), ZSTR_VAL(child->str), (child->flags & PHPDBG_WATCH_RECURSIVE_ROOT) ? " recursively" : "");
+       zend_hash_index_del(&PHPDBG_G(watch_elements), child->id);
+       phpdbg_free_watch_element_tree(element);
 }
 
-static int phpdbg_create_recursive_ht_watch(phpdbg_watchpoint_t *watch) {
-       zval *zv;
-       zend_string *key;
-       zend_long h;
-
-       ZEND_ASSERT(watch->type == WATCH_ON_HASHTABLE);
-
-       ZEND_HASH_FOREACH_KEY_VAL(HT_WATCH_HT(watch), h, key, zv) {
-               phpdbg_watchpoint_t *new_watch = emalloc(sizeof(phpdbg_watchpoint_t));
-
-               new_watch->flags = PHPDBG_WATCH_RECURSIVE;
-               new_watch->parent = watch;
-               new_watch->parent_container = HT_WATCH_HT(watch);
-
-               if (key) {
-                       new_watch->name_in_parent = key;
-                       ++GC_REFCOUNT(key);
+void phpdbg_dequeue_elements_for_recreation() {
+       phpdbg_watch_element *element;
+
+       ZEND_HASH_FOREACH_PTR(&PHPDBG_G(watch_recreation), element) {
+               ZEND_ASSERT(element->flags & (PHPDBG_WATCH_IMPLICIT | PHPDBG_WATCH_RECURSIVE_ROOT | PHPDBG_WATCH_SIMPLE));
+               if (element->parent || zend_hash_index_find(&PHPDBG_G(watch_free), (zend_ulong) element->parent_container)) {
+                       zval _zv, *zv = &_zv;
+                       if (element->parent) {
+                               ZEND_ASSERT(element->parent->watch->type == WATCH_ON_ZVAL || element->parent->watch->type == WATCH_ON_BUCKET);
+                               zv = element->parent->watch->addr.zv;
+                               while (Z_TYPE_P(zv) == IS_INDIRECT) {
+                                       zv = Z_INDIRECT_P(zv);
+                               }
+                               ZVAL_DEREF(zv);
+                       } else {
+                               ZVAL_ARR(zv, element->parent_container);
+                       }
+                       if (!phpdbg_try_readding_watch_element(zv, element)) {
+                               phpdbg_automatic_dequeue_free(element);
+                       }
                } else {
-                       new_watch->name_in_parent = strpprintf(0, ZEND_LONG_FMT, h);
-               }
-
-               new_watch->str = strpprintf(0, "%.*s%s%s%s", (int) ZSTR_LEN(watch->str) - 2, ZSTR_VAL(watch->str), (watch->flags & PHPDBG_WATCH_ARRAY) ? "[" : "->", phpdbg_get_property_key(ZSTR_VAL(new_watch->name_in_parent)), (watch->flags & PHPDBG_WATCH_ARRAY) ? "]" : "");
-
-               while (Z_TYPE_P(zv) == IS_INDIRECT) {
-                       zv = Z_INDIRECT_P(zv);
+                       phpdbg_automatic_dequeue_free(element);
                }
-
-               phpdbg_create_zval_watchpoint(zv, new_watch);
-               new_watch = phpdbg_create_watchpoint(new_watch);
-               phpdbg_create_recursive_zval_watch(new_watch);
        } ZEND_HASH_FOREACH_END();
 
-       return SUCCESS;
+       zend_hash_clean(&PHPDBG_G(watch_recreation));
+       zend_hash_clean(&PHPDBG_G(watch_free));
 }
 
-static int phpdbg_create_recursive_zval_watch(phpdbg_watchpoint_t *watch) {
-       HashTable *ht;
-       zval *zvp;
-
-       ZEND_ASSERT(watch->type == WATCH_ON_ZVAL);
+/* ### WATCH ELEMENT DELETION ### only use phpdbg_remove_watch_element from the exterior */
+void phpdbg_clean_watch_element(phpdbg_watch_element *element);
 
-       zvp = watch->addr.zv;
-       ZVAL_DEREF(zvp);
+void phpdbg_free_watch_element(phpdbg_watch_element *element) {
+       zend_string_release(element->str);
+       if (element->name_in_parent) {
+               zend_string_release(element->name_in_parent);
+       }
+       efree(element);
+}
 
-       if ((ht = HT_FROM_ZVP(zvp))) {
-               phpdbg_watchpoint_t *new_watch = emalloc(sizeof(phpdbg_watchpoint_t));
+/* note: does *not* free the passed element, only clean */
+void phpdbg_remove_watch_element_recursively(phpdbg_watch_element *element) {
+       if (element->child) {
+               phpdbg_remove_watch_element_recursively(element->child);
+               phpdbg_free_watch_element(element->child);
+               element->child = NULL;
+       } else if (element->flags & (PHPDBG_WATCH_ARRAY | PHPDBG_WATCH_OBJECT)) {
+               phpdbg_watch_element *child;
+               ZEND_HASH_FOREACH_PTR(&element->child_container, child) {
+                       phpdbg_remove_watch_element_recursively(child);
+                       phpdbg_free_watch_element(child);
+               } ZEND_HASH_FOREACH_END();
+               zend_hash_destroy(&element->child_container);
+       }
 
-               new_watch->flags = PHPDBG_WATCH_RECURSIVE;
-               new_watch->parent = watch;
-               new_watch->parent_container = watch->parent_container;
-               new_watch->name_in_parent = watch->name_in_parent;
-               ++GC_REFCOUNT(new_watch->name_in_parent);
-               new_watch->str = strpprintf(0, "%.*s[]", (int) ZSTR_LEN(watch->str), ZSTR_VAL(watch->str));
+       phpdbg_clean_watch_element(element);
+}
 
-               if (Z_TYPE_P(zvp) == IS_ARRAY) {
-                       new_watch->flags |= PHPDBG_WATCH_ARRAY;
+/* remove single watch (i.e. manual unset) or implicit removed */
+void phpdbg_remove_watch_element(phpdbg_watch_element *element) {
+       phpdbg_watch_element *parent = element->parent, *child = element->child;
+       while (parent) {
+               phpdbg_watch_element *cur = parent;
+               parent = parent->parent;
+               phpdbg_clean_watch_element(cur);
+               phpdbg_free_watch_element(cur);
+       }
+       while (child) {
+               phpdbg_watch_element *cur = child;
+               child = child->child;
+               if (cur->flags & PHPDBG_WATCH_RECURSIVE_ROOT) {
+                       phpdbg_remove_watch_element_recursively(cur);
+                       child = NULL;
                } else {
-                       new_watch->flags |= PHPDBG_WATCH_OBJECT;
+                       phpdbg_clean_watch_element(cur);
                }
-
-               phpdbg_create_ht_watchpoint(ht, new_watch);
-
-               phpdbg_create_recursive_ht_watch(new_watch);
-
-               phpdbg_create_watchpoint(new_watch);
+               phpdbg_free_watch_element(cur);
        }
-
-       return SUCCESS;
+       if (element->flags & PHPDBG_WATCH_RECURSIVE_ROOT) {
+               phpdbg_remove_watch_element_recursively(element);
+       } else {
+               phpdbg_clean_watch_element(element);
+       }
+       zend_hash_index_del(&PHPDBG_G(watch_elements), element->id);
+       phpdbg_free_watch_element(element);
 }
 
-static void phpdbg_delete_implicit_parents(phpdbg_watchpoint_t *watch) {
-       phpdbg_watchpoint_t *parent = watch->parent;
-       if (!parent) {
-               return;
-       }
+void phpdbg_backup_watch_element(phpdbg_watch_element *element) {
+       memcpy(&element->backup, &element->watch->backup, /* element->watch->size */ sizeof(element->backup));
+}
 
-       ZEND_ASSERT(!(parent->flags & PHPDBG_WATCH_RECURSIVE));
-       ZEND_ASSERT(parent->flags & PHPDBG_WATCH_IMPLICIT);
+/* until argument to prevent double remove of children elements */
+void phpdbg_dissociate_watch_element(phpdbg_watch_element *element, phpdbg_watch_element *until) {
+       phpdbg_watch_element *child = element;
+       ZEND_ASSERT((element->flags & (PHPDBG_WATCH_RECURSIVE_ROOT | PHPDBG_WATCH_RECURSIVE)) != PHPDBG_WATCH_RECURSIVE);
 
-       if (parent->type == WATCH_ON_HASHTABLE && --parent->implicit_ht_count) {
+       if (element->flags & PHPDBG_WATCH_RECURSIVE_ROOT) {
+               phpdbg_backup_watch_element(element);
+               phpdbg_remove_watch_element_recursively(element);
                return;
        }
 
-       parent->flags &= ~PHPDBG_WATCH_IMPLICIT;
-       if (!(parent->flags & PHPDBG_WATCH_SIMPLE)) {
-               if (parent->type == WATCH_ON_ZVAL && Z_REFCOUNTED_P(watch->addr.zv)) {
-                       phpdbg_remove_watch_collision(parent);
+       while (child->child != until) {
+               child = child->child;
+               if (child->flags & PHPDBG_WATCH_RECURSIVE_ROOT) {
+                       phpdbg_backup_watch_element(child);
+                       phpdbg_remove_watch_element_recursively(child);
+                       child->child = NULL;
+                       break;
+               }
+               if (child->child == NULL || (child->flags & PHPDBG_WATCH_RECURSIVE_ROOT)) {
+                       phpdbg_backup_watch_element(child);
                }
-               zend_hash_del(&PHPDBG_G(watchpoints), parent->str);
+               phpdbg_clean_watch_element(child);
        }
+       /* element needs to be removed last! */
+       if (element->child == NULL) {
+               phpdbg_backup_watch_element(element);
+       }
+       phpdbg_clean_watch_element(element);
 }
 
-/* delete watchpoint, recursively (and inclusively) */
-static int phpdbg_delete_watchpoint_recursive(phpdbg_watchpoint_t *watch, zend_bool user_request) {
-       if (watch->type == WATCH_ON_HASHTABLE) {
-               if (user_request) {
-                       phpdbg_delete_ht_watchpoints_recursive(watch);
-               } else {
-                       HashTable *ht;
-                       phpdbg_btree_result *result;
+/* unlike phpdbg_remove_watch_element this *only* frees and does not clean up element + children! Only use after previous cleanup (e.g. phpdbg_dissociate_watch_element) */
+void phpdbg_free_watch_element_tree(phpdbg_watch_element *element) {
+       phpdbg_watch_element *parent = element->parent, *child = element->child;
+       while (parent) {
+               phpdbg_watch_element *cur = parent;
+               parent = parent->parent;
+               phpdbg_clean_watch_element(cur);
+               phpdbg_free_watch_element(cur);
+       }
+       while (child) {
+               phpdbg_watch_element *cur = child;
+               child = child->child;
+               phpdbg_free_watch_element(cur);
+       }
+       phpdbg_free_watch_element(element);
+}
 
-                       ht = HT_FROM_ZVP(watch->addr.zv);
+void phpdbg_update_watch_element_watch(phpdbg_watch_element *element) {
+       if (element->flags & PHPDBG_WATCH_IMPLICIT) {
+               phpdbg_watch_element *child = element->child;
+               while (child->flags & PHPDBG_WATCH_IMPLICIT) {
+                       child = child->child;
+               }
 
-                       if ((result = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) ht))) {
-                               phpdbg_delete_watchpoint_recursive((phpdbg_watchpoint_t *) result->ptr, user_request);
-                       }
+               ZEND_ASSERT(element->watch->type == WATCH_ON_ZVAL || element->watch->type == WATCH_ON_BUCKET);
+               phpdbg_queue_element_for_recreation(element);
+       } else if (element->flags & (PHPDBG_WATCH_RECURSIVE_ROOT | PHPDBG_WATCH_SIMPLE)) {
+               phpdbg_queue_element_for_recreation(element);
+       } else if (element->flags & PHPDBG_WATCH_RECURSIVE) {
+               phpdbg_remove_watch_element_recursively(element);
+               if (element->parent->flags & (PHPDBG_WATCH_OBJECT | PHPDBG_WATCH_ARRAY)) {
+                       zend_hash_del(&element->parent->child_container, element->str);
+               } else {
+                       element->parent->child = NULL;
                }
-       } else if (watch->type == WATCH_ON_ZVAL) {
-               phpdbg_delete_zval_watchpoints_recursive(watch);
+               phpdbg_free_watch_element(element);
        }
-
-       return zend_hash_del(&PHPDBG_G(watchpoints), watch->str);
 }
 
-static void phpdbg_delete_ht_watchpoints_recursive(phpdbg_watchpoint_t *watch) {
-       zend_string *str, *strkey;
-       zend_long numkey;
-       phpdbg_watchpoint_t *watchpoint;
+void phpdbg_update_watch_collision_elements(phpdbg_watchpoint_t *watch) {
+       phpdbg_watchpoint_t *parent;
+       phpdbg_watch_element *element;
 
-       ZEND_HASH_FOREACH_KEY(HT_WATCH_HT(watch), numkey, strkey) {
-               if (strkey) {
-                       str = strpprintf(0, "%.*s%s%s%s", (int) ZSTR_LEN(watch->str), ZSTR_VAL(watch->str), (watch->flags & PHPDBG_WATCH_ARRAY) ? "[" : "->", phpdbg_get_property_key(ZSTR_VAL(strkey)), (watch->flags & PHPDBG_WATCH_ARRAY) ? "]" : "");
+       ZEND_HASH_FOREACH_PTR(&watch->coll->parents, parent) {
+               if (parent->coll) {
+                       phpdbg_update_watch_collision_elements(parent);
                } else {
-                       str = strpprintf(0, "%.*s%s" ZEND_LONG_FMT "%s", (int) ZSTR_LEN(watch->str), ZSTR_VAL(watch->str), (watch->flags & PHPDBG_WATCH_ARRAY) ? "[" : "->", numkey, (watch->flags & PHPDBG_WATCH_ARRAY) ? "]" : "");
+                       ZEND_HASH_FOREACH_PTR(&parent->elements, element) {
+                               phpdbg_update_watch_element_watch(element);
+                       } ZEND_HASH_FOREACH_END();
                }
+       } ZEND_HASH_FOREACH_END();
+}
 
-               if ((watchpoint = zend_hash_find_ptr(&PHPDBG_G(watchpoints), str))) {
-                       phpdbg_delete_watchpoint_recursive(watchpoint, 1);
-               }
+void phpdbg_remove_watchpoint(phpdbg_watchpoint_t *watch) {
+       phpdbg_watch_element *element;
+
+       phpdbg_deactivate_watchpoint(watch);
+       phpdbg_remove_watchpoint_btree(watch);
+       phpdbg_delete_watch_collision(watch);
+
+       if (watch->coll) {
+               phpdbg_update_watch_collision_elements(watch);
+               return;
+       }
 
-               zend_string_release(str);
+       watch->elements.nNumOfElements++; /* dirty hack to avoid double free */
+       ZEND_HASH_FOREACH_PTR(&watch->elements, element) {
+               phpdbg_update_watch_element_watch(element);
        } ZEND_HASH_FOREACH_END();
+       zend_hash_destroy(&watch->elements);
+
+       efree(watch);
 }
 
-static void phpdbg_delete_zval_watchpoints_recursive(phpdbg_watchpoint_t *watch) {
-       if (Z_REFCOUNTED_P(watch->addr.zv)) {
-               phpdbg_remove_watch_collision(watch);
+void phpdbg_clean_watch_element(phpdbg_watch_element *element) {
+       HashTable *elements = &element->watch->elements;
+       phpdbg_unwatch_parent_ht(element);
+       zend_hash_del(elements, element->str);
+       if (zend_hash_num_elements(elements) == 0) {
+               phpdbg_remove_watchpoint(element->watch);
        }
 }
 
-/* delete watchpoints recusively (exclusively!) */
-static void phpdbg_delete_watchpoints_recursive(phpdbg_watchpoint_t *watch) {
-       if (watch->type == WATCH_ON_ZVAL) {
-               phpdbg_delete_zval_watchpoints_recursive(watch);
-       } else if (watch->type == WATCH_ON_HASHTABLE) {
-               phpdbg_delete_ht_watchpoints_recursive(watch);
+/* TODO: compile a name of all hit watchpoints (ids ??) */
+zend_string *phpdbg_watchpoint_change_collision_name(phpdbg_watchpoint_t *watch) {
+       phpdbg_watchpoint_t *parent;
+       phpdbg_watch_element *element;
+       zend_string *name = NULL;
+       if (watch->coll) {
+               ZEND_HASH_FOREACH_PTR(&watch->coll->parents, parent) {
+                       if (name) {
+                               zend_string_release(name);
+                       }
+                       name = phpdbg_watchpoint_change_collision_name(parent);
+               } ZEND_HASH_FOREACH_END();
+               return name;
        }
-}
+       ZEND_HASH_FOREACH_PTR(&watch->elements, element) {
+               if (element->flags & PHPDBG_WATCH_IMPLICIT) {
+                       if ((watch->type == WATCH_ON_ZVAL || watch->type == WATCH_ON_BUCKET) && Z_REFCOUNTED(watch->backup.zv)) {
+                               phpdbg_update_watch_element_watch(element->child);
+                       }
+                       continue;
+               }
+               name = element->str;
+       } ZEND_HASH_FOREACH_END();
 
-static int phpdbg_delete_watchpoint(phpdbg_watchpoint_t *tmp_watch) {
-       int ret;
-       phpdbg_watchpoint_t *watch;
-       phpdbg_btree_result *result;
+       return name ? zend_string_copy(name) : NULL;
+}
 
-       if ((result = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) tmp_watch->addr.ptr)) == NULL) {
-               return FAILURE;
-       }
+/* ### WATCHING FOR CHANGES ### */
+/* TODO: enforce order: first parents, then children, in order to avoid false positives */
+void phpdbg_check_watchpoint(phpdbg_watchpoint_t *watch) {
+       zend_string *name = NULL;
+       void *comparePtr;
 
-       watch = result->ptr;
+       if (watch->type == WATCH_ON_HASHTABLE) {
+               phpdbg_watch_element *element;
+               zend_string *str;
+               zend_long idx;
+               zval *zv;
+               ZEND_HASH_FOREACH_PTR(&watch->elements, element) {
+                       if (element->flags & PHPDBG_WATCH_RECURSIVE) {
+                               phpdbg_btree_result *res = phpdbg_btree_find(&PHPDBG_G(watch_HashTables), (zend_ulong) HT_WATCH_HT(watch));
+                               phpdbg_watch_ht_info *hti = res ? res->ptr : NULL;
+
+                               ZEND_HASH_REVERSE_FOREACH_KEY_VAL(HT_WATCH_HT(watch), idx, str, zv) {
+                                       if (!str) {
+                                               str = zend_long_to_str(idx); // TODO: hack, use proper int handling for name in parent
+                                       } else {
+                                               str = zend_string_copy(str);
+                                       }
+                                       if (hti && zend_hash_find(&hti->watches, str)) {
+                                               zend_string_release(str);
+                                               break;
+                                       }
+                                       ZEND_HASH_FOREACH_PTR(&watch->elements, element) {
+                                               if (element->flags & PHPDBG_WATCH_RECURSIVE) {
+                                                       phpdbg_add_recursive_watch_from_ht(element, idx, str, zv);
+                                               }
+                                       } ZEND_HASH_FOREACH_END();
+                                       phpdbg_notice("watchadd", "element=\"%.*s\"", "Element %.*s has been added to watchpoint", (int) ZSTR_LEN(str), ZSTR_VAL(str));
+                                       zend_string_release(str);
+                                       PHPDBG_G(watchpoint_hit) = 1;
+                               } ZEND_HASH_FOREACH_END();
 
-       if (!(watch->flags & PHPDBG_WATCH_NORMAL) || (watch->parent && (watch->parent->flags & PHPDBG_WATCH_RECURSIVE))) {
-               return FAILURE; /* TODO: better error message for recursive (??) */
+                               break;
+                       }
+               } ZEND_HASH_FOREACH_END();
+       }
+       if (watch->type == WATCH_ON_HASHDATA) {
+               return;
        }
 
-       if (watch->flags & PHPDBG_WATCH_RECURSIVE) {
-               ret = phpdbg_delete_watchpoint_recursive(watch, 1);
-       } else {
-               ret = zend_hash_del(&PHPDBG_G(watchpoints), watch->str);
+       switch (watch->type) {
+               case WATCH_ON_STR:
+                       comparePtr = &ZSTR_LEN(watch->backup.str);
+                       break;
+               case WATCH_ON_HASHTABLE:
+                       comparePtr = (char *) &watch->backup.ht + HT_WATCH_OFFSET;
+                       break;
+               default:
+                       comparePtr = &watch->backup;
+       }
+       if (!phpdbg_check_watch_diff(watch->type, comparePtr, watch->addr.ptr)) {
+               return;
+       }
+       if (watch->type == WATCH_ON_REFCOUNTED && !(PHPDBG_G(flags) & PHPDBG_SHOW_REFCOUNTS)) {
+               phpdbg_watch_backup_data(watch);
+               return;
        }
+       if (watch->type == WATCH_ON_BUCKET) {
+               if (watch->backup.bucket.key != watch->addr.bucket->key || (watch->backup.bucket.key != NULL && watch->backup.bucket.h != watch->addr.bucket->h)) {
+                       phpdbg_watch_element *element;
+                       zval *new;
 
-       phpdbg_free_watch(tmp_watch);
-       efree(tmp_watch);
+                       ZEND_HASH_FOREACH_PTR(&watch->elements, element) {
+                               break;
+                       } ZEND_HASH_FOREACH_END();
 
-       return ret;
-}
+                       new = zend_symtable_find(element->parent_container, element->name_in_parent);
 
-static int phpdbg_watchpoint_parse_wrapper(char *name, size_t namelen, char *key, size_t keylen, HashTable *parent, zval *zv, int (*callback)(phpdbg_watchpoint_t *)) {
-       int ret;
-       phpdbg_watchpoint_t *watch = ecalloc(1, sizeof(phpdbg_watchpoint_t));
-       watch->str = zend_string_init(name, namelen, 0);
-       watch->name_in_parent = zend_string_init(key, keylen, 0);
-       watch->parent_container = parent;
-       phpdbg_create_zval_watchpoint(zv, watch);
+                       if (!new) {
+                               /* dequeuing will take care of appropriate notification about removal */
+                               phpdbg_remove_watchpoint(watch);
+                               return;
+                       }
 
-       ret = callback(watch);
+                       phpdbg_deactivate_watchpoint(watch);
+                       phpdbg_remove_watchpoint_btree(watch);
+                       watch->addr.zv = new;
+                       phpdbg_store_watchpoint_btree(watch);
+                       phpdbg_activate_watchpoint(watch);
 
-       efree(name);
-       efree(key);
-
-       if (ret != SUCCESS) {
-               phpdbg_free_watch(watch);
-               efree(watch);
+                       if (!phpdbg_check_watch_diff(WATCH_ON_ZVAL, &watch->backup.bucket.val, watch->addr.ptr)) {
+                               phpdbg_watch_backup_data(watch);
+                               return;
+                       }
+               } else if (Z_TYPE_P(watch->addr.zv) == IS_UNDEF) {
+                       /* dequeuing will take care of appropriate notification about removal */
+                       phpdbg_remove_watchpoint(watch);
+                       return;
+               }
        }
 
-       PHPDBG_G(watch_tmp) = NULL;
+       name = phpdbg_watchpoint_change_collision_name(watch);
 
-       return ret;
-}
-
-PHPDBG_API int phpdbg_watchpoint_parse_input(char *input, size_t len, HashTable *parent, size_t i, int (*callback)(phpdbg_watchpoint_t *), zend_bool silent) {
-       return phpdbg_parse_variable_with_arg(input, len, parent, i, (phpdbg_parse_var_with_arg_func) phpdbg_watchpoint_parse_wrapper, NULL, 0, callback);
-}
-
-static int phpdbg_watchpoint_parse_step(char *name, size_t namelen, char *key, size_t keylen, HashTable *parent, zval *zv, int (*callback)(phpdbg_watchpoint_t *)) {
-       phpdbg_watchpoint_t *watch;
-
-       if ((watch = zend_hash_str_find_ptr(&PHPDBG_G(watchpoints), name, namelen))) {
-               watch->flags |= PHPDBG_WATCH_IMPLICIT;
-               PHPDBG_G(watch_tmp) = watch;
-               return SUCCESS;
+       if (name) {
+               phpdbg_print_watch_diff(watch->type, name, comparePtr, watch->addr.ptr);
+               zend_string_release(name);
        }
 
-       watch = ecalloc(1, sizeof(phpdbg_watchpoint_t));
-       watch->flags = PHPDBG_WATCH_IMPLICIT;
-       watch->str = zend_string_init(name, namelen, 0);
-       watch->name_in_parent = zend_string_init(key, keylen, 0);
-       watch->parent_container = parent;
-       watch->parent = PHPDBG_G(watch_tmp);
-       phpdbg_create_zval_watchpoint(zv, watch);
-
-       phpdbg_create_watchpoint(watch);
-
-       efree(name);
-       efree(key);
+       if (watch->type == WATCH_ON_ZVAL || watch->type == WATCH_ON_BUCKET) {
+               phpdbg_watch_element *element;
+               phpdbg_update_watch_ref(watch);
+               ZEND_HASH_FOREACH_PTR(&watch->elements, element) {
+                       if (element->flags & PHPDBG_WATCH_RECURSIVE) {
+                               phpdbg_recurse_watch_element(element);
+                       }
+               } ZEND_HASH_FOREACH_END();
+       }
 
-       PHPDBG_G(watch_tmp) = watch;
-       return SUCCESS;
+       phpdbg_watch_backup_data(watch);
 }
 
-static int phpdbg_watchpoint_parse_symtables(char *input, size_t len, int (*callback)(phpdbg_watchpoint_t *)) {
-       zend_class_entry *scope = zend_get_executed_scope();
-
-       if (scope && len >= 5 && !memcmp("$this", input, 5)) {
-               zend_hash_str_add(EG(current_execute_data)->symbol_table, ZEND_STRL("this"), &EG(current_execute_data)->This);
-       }
-
-       if (phpdbg_is_auto_global(input, len) && phpdbg_watchpoint_parse_input(input, len, &EG(symbol_table), 0, callback, 1) != FAILURE) {
-               return SUCCESS;
-       }
+void phpdbg_reenable_memory_watches(void) {
+       zend_ulong page;
+       phpdbg_btree_result *res;
+       phpdbg_watchpoint_t *watch;
 
-       return phpdbg_parse_variable_with_arg(input, len, EG(current_execute_data)->symbol_table, 0, (phpdbg_parse_var_with_arg_func) phpdbg_watchpoint_parse_wrapper, (phpdbg_parse_var_with_arg_func) phpdbg_watchpoint_parse_step, 0, callback);
+       ZEND_HASH_FOREACH_NUM_KEY(PHPDBG_G(watchlist_mem), page) {
+               /* Disble writing again if there are any watchers on that page */
+               res = phpdbg_btree_find_closest(&PHPDBG_G(watchpoint_tree), page + phpdbg_pagesize - 1);
+               if (res) {
+                       watch = res->ptr;
+                       if ((char *) page < (char *) watch->addr.ptr + watch->size) {
+                               mprotect((void *) page, phpdbg_pagesize, PROT_READ);
+                       }
+               }
+       } ZEND_HASH_FOREACH_END();
+       zend_hash_clean(PHPDBG_G(watchlist_mem));
 }
 
-PHPDBG_WATCH(delete) /* {{{ */
-{
-       switch (param->type) {
-               case STR_PARAM:
-                       if (phpdbg_delete_var_watchpoint(param->str, param->len) == FAILURE) {
-                               phpdbg_error("watchdelete", "type=\"nowatch\"", "Nothing was deleted, no corresponding watchpoint found");
-                       } else {
-                               phpdbg_notice("watchdelete", "variable=\"%.*s\"", "Removed watchpoint %.*s", (int) param->len, param->str);
-                       }
-                       break;
+int phpdbg_print_changed_zvals(void) {
+       int ret;
+       zend_ulong page;
+       phpdbg_watchpoint_t *watch;
+       phpdbg_btree_result *res;
+       HashTable *mem_list = NULL;
 
-               phpdbg_default_switch_case();
+       if (zend_hash_num_elements(&PHPDBG_G(watch_elements)) == 0) {
+               return FAILURE;
        }
 
-       return SUCCESS;
-} /* }}} */
+       if (zend_hash_num_elements(PHPDBG_G(watchlist_mem)) > 0) {
+               /* we must not add elements to the hashtable while iterating over it (resize => read into freed memory) */
+               mem_list = PHPDBG_G(watchlist_mem);
+               PHPDBG_G(watchlist_mem) = PHPDBG_G(watchlist_mem_backup);
 
-PHPDBG_WATCH(recursive) /* {{{ */
-{
-       if (phpdbg_rebuild_symtable() == FAILURE) {
-               return SUCCESS;
-       }
+               ZEND_HASH_FOREACH_NUM_KEY(mem_list, page) {
+                       phpdbg_btree_position pos = phpdbg_btree_find_between(&PHPDBG_G(watchpoint_tree), page, page + phpdbg_pagesize);
 
-       switch (param->type) {
-               case STR_PARAM:
-                       if (phpdbg_watchpoint_parse_symtables(param->str, param->len, phpdbg_create_recursive_watchpoint) != FAILURE) {
-                               phpdbg_notice("watchrecursive", "variable=\"%.*s\"", "Set recursive watchpoint on %.*s", (int)param->len, param->str);
+                       while ((res = phpdbg_btree_next(&pos))) {
+                               watch = res->ptr;
+                               phpdbg_check_watchpoint(watch);
                        }
-                       break;
-
-               phpdbg_default_switch_case();
-       }
-
-       return SUCCESS;
-} /* }}} */
-
-PHPDBG_WATCH(array) /* {{{ */
-{
-       if (phpdbg_rebuild_symtable() == FAILURE) {
-               return SUCCESS;
-       }
-
-       switch (param->type) {
-               case STR_PARAM:
-                       if (phpdbg_watchpoint_parse_symtables(param->str, param->len, phpdbg_create_array_watchpoint) != FAILURE) {
-                               phpdbg_notice("watcharray", "variable=\"%.*s\"", "Set array watchpoint on %.*s", (int)param->len, param->str);
+                       if ((res = phpdbg_btree_find_closest(&PHPDBG_G(watchpoint_tree), page - 1))) {
+                               watch = res->ptr;
+                               if ((char *) page < (char *) watch->addr.ptr + watch->size) {
+                                       phpdbg_check_watchpoint(watch);
+                               }
                        }
-                       break;
-
-               phpdbg_default_switch_case();
+               } ZEND_HASH_FOREACH_END();
        }
 
-       return SUCCESS;
-} /* }}} */
+       phpdbg_dequeue_elements_for_recreation();
 
-void phpdbg_watch_HashTable_dtor(zval *zv) {
-       phpdbg_btree_result *result;
-       zval *orig_zv = zv;
+       phpdbg_reenable_memory_watches();
 
-       while (Z_TYPE_P(zv) == IS_INDIRECT) {
-               zv = Z_INDIRECT_P(zv);
+       if (mem_list) {
+               PHPDBG_G(watchlist_mem) = mem_list;
+               phpdbg_reenable_memory_watches();
        }
 
-       if ((result = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) zv))) {
-               phpdbg_watchpoint_t *watch = result->ptr;
+       ret = PHPDBG_G(watchpoint_hit) ? SUCCESS : FAILURE;
+       PHPDBG_G(watchpoint_hit) = 0;
 
-               if (watch->flags & PHPDBG_WATCH_NORMAL) {
-                       PHPDBG_G(watchpoint_hit) = 1;
+       return ret;
+}
 
-                       phpdbg_notice("watchdelete", "variable=\"%.*s\" recursive=\"%s\"", "%.*s was removed, removing watchpoint%s", (int) ZSTR_LEN(watch->str), ZSTR_VAL(watch->str), (watch->flags & PHPDBG_WATCH_RECURSIVE) ? " recursively" : "");
-               }
+void phpdbg_watch_efree(void *ptr) {
+       phpdbg_btree_result *result;
 
-               if ((result = phpdbg_btree_find(&PHPDBG_G(watch_HashTables), (zend_ulong) watch->parent_container))) {
-                       phpdbg_watch_ht_info *hti = result->ptr;
-                       hti->dtor(orig_zv);
-                       zend_hash_del(&hti->watches, watch->name_in_parent);
-                       if (zend_hash_num_elements(&hti->watches) == 0) {
-                               watch->parent_container->pDestructor = hti->dtor;
-                               zend_hash_destroy(&hti->watches);
-                               phpdbg_btree_delete(&PHPDBG_G(watch_HashTables), (zend_ulong) watch->parent_container);
-                               efree(hti);
+       /* only do expensive checks if there are any watches at all */
+       if (zend_hash_num_elements(&PHPDBG_G(watch_elements))) {
+               if ((result = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), (zend_ulong) ptr))) {
+                       phpdbg_watchpoint_t *watch = result->ptr;
+                       if (watch->type != WATCH_ON_HASHDATA) {
+                               phpdbg_remove_watchpoint(watch);
+                       } else {
+                               /* remove all linked watchpoints, they will be dissociated from their elements */
+                               phpdbg_watch_element *element;
+                               phpdbg_watch_ht_info *hti = (phpdbg_watch_ht_info *) watch;
+
+                               ZEND_HASH_FOREACH_PTR(&hti->watches, element) {
+                                       zend_ulong num = zend_hash_num_elements(&hti->watches);
+                                       phpdbg_remove_watchpoint(element->watch);
+                                       if (num == 1) { /* prevent access into freed memory */
+                                               break;
+                                       }
+                               } ZEND_HASH_FOREACH_END();
                        }
-               } else {
-                       zval_ptr_dtor_wrapper(orig_zv);
                }
 
-               if (watch->flags & PHPDBG_WATCH_RECURSIVE) {
-                       phpdbg_delete_watchpoint_recursive(watch, 0);
-               } else {
-                       zend_hash_del(&PHPDBG_G(watchpoints), watch->str);
+               /* special case watchpoints as they aren't on ptr but on ptr + HT_WATCH_OFFSET */
+               if ((result = phpdbg_btree_find(&PHPDBG_G(watchpoint_tree), HT_WATCH_OFFSET + (zend_ulong) ptr))) {
+                       phpdbg_watchpoint_t *watch = result->ptr;
+                       if (watch->type == WATCH_ON_HASHTABLE) {
+                               phpdbg_remove_watchpoint(watch);
+                       }
                }
-       }
-}
 
-int phpdbg_create_var_watchpoint(char *input, size_t len) {
-       if (phpdbg_rebuild_symtable() == FAILURE) {
-               return FAILURE;
+               zend_hash_index_del(&PHPDBG_G(watch_free), (zend_ulong) ptr);
        }
 
-       return phpdbg_watchpoint_parse_symtables(input, len, phpdbg_create_simple_watchpoint);
+       if (PHPDBG_G(original_free_function)) {
+               PHPDBG_G(original_free_function)(ptr);
+       }
 }
 
-int phpdbg_delete_var_watchpoint(char *input, size_t len) {
-       if (phpdbg_rebuild_symtable() == FAILURE) {
-               return FAILURE;
-       }
+/* ### USER API ### */
+void phpdbg_list_watchpoints(void) {
+       phpdbg_watch_element *element;
+
+       phpdbg_xml("<watchlist %r>");
 
-       return phpdbg_watchpoint_parse_input(input, len, EG(current_execute_data)->symbol_table, 0, phpdbg_delete_watchpoint, 0);
+       ZEND_HASH_FOREACH_PTR(&PHPDBG_G(watch_elements), element) {
+               phpdbg_writeln("watchvariable", "variable=\"%.*s\" on=\"%s\" type=\"%s\"", "%.*s (%s, %s)", (int) ZSTR_LEN(element->str), ZSTR_VAL(element->str), (element->flags & (PHPDBG_WATCH_ARRAY|PHPDBG_WATCH_OBJECT)) ? "array" : "variable", (element->flags & PHPDBG_WATCH_RECURSIVE) ? "recursive" : "simple");
+       } ZEND_HASH_FOREACH_END();
+
+       phpdbg_xml("</watchlist>");
 }
 
-#ifdef _WIN32
-int phpdbg_watchpoint_segfault_handler(void *addr) {
-#else
-int phpdbg_watchpoint_segfault_handler(siginfo_t *info, void *context) {
-#endif
-       void *page;
-       phpdbg_watch_memdump *dump;
-       phpdbg_watchpoint_t *watch;
-       size_t size;
+static int phpdbg_create_simple_watchpoint(zval *zv, phpdbg_watch_element *element) {
+       element->flags = PHPDBG_WATCH_SIMPLE;
+       phpdbg_add_bucket_watch_element((Bucket *) zv, element);
+       return SUCCESS;
+}
 
-       watch = phpdbg_check_for_watchpoint(
-#ifdef _WIN32
-               addr
-#else
-               info->si_addr
-#endif
-               );
+static int phpdbg_create_array_watchpoint(zval *zv, phpdbg_watch_element *element) {
+       phpdbg_watch_element *new;
+       zend_string *str;
+       zval *orig_zv = zv;
 
-       if (watch == NULL) {
+       ZVAL_DEREF(zv);
+       if (Z_TYPE_P(zv) != IS_ARRAY && Z_TYPE_P(zv) != IS_OBJECT) {
                return FAILURE;
        }
 
-       page = phpdbg_get_page_boundary(watch->addr.ptr);
-       size = phpdbg_get_total_page_size(watch->addr.ptr, watch->size);
-
-       /* re-enable writing */
-       mprotect(page, size, PROT_READ | PROT_WRITE);
-
-       dump = malloc(MEMDUMP_SIZE(size));
-       dump->page = page;
-       dump->size = size;
-       dump->reenable_writing = 0;
+       new = ecalloc(1, sizeof(phpdbg_watch_element));
 
-       memcpy(&dump->data, page, size);
-
-       zend_llist_add_element(&PHPDBG_G(watchlist_mem), &dump);
+       str = strpprintf(0, "%.*s[]", (int) ZSTR_LEN(element->str), ZSTR_VAL(element->str));
+       zend_string_release(element->str);
+       element->str = str;
+       element->flags = PHPDBG_WATCH_IMPLICIT;
+       phpdbg_add_bucket_watch_element((Bucket *) orig_zv, element);
+       element->child = new;
 
+       new->flags = PHPDBG_WATCH_SIMPLE;
+       new->str = zend_string_copy(str);
+       new->parent = element;
+       phpdbg_add_ht_watch_element(zv, new);
        return SUCCESS;
 }
 
-void phpdbg_watchpoints_clean(void) {
-       zend_hash_clean(&PHPDBG_G(watchpoints));
+static int phpdbg_create_recursive_watchpoint(zval *zv, phpdbg_watch_element *element) {
+       element->flags = PHPDBG_WATCH_RECURSIVE | PHPDBG_WATCH_RECURSIVE_ROOT;
+       element->child = NULL;
+       phpdbg_add_bucket_watch_element((Bucket *) zv, element);
+       return SUCCESS;
 }
 
-/* due to implicit delete... MUST BE DESTROYED MANUALLY */
-static void phpdbg_watch_dtor(zval *pDest) {
-       phpdbg_watchpoint_t *watch = (phpdbg_watchpoint_t *) Z_PTR_P(pDest);
+typedef struct { int (*callback)(zval *zv, phpdbg_watch_element *); zend_string *str; } phpdbg_watch_parse_struct;
 
-       if (watch->flags & PHPDBG_WATCH_IMPLICIT) {
-               watch->flags = PHPDBG_WATCH_SIMPLE; // tiny hack for delete_implicit_parents
+static int phpdbg_watchpoint_parse_wrapper(char *name, size_t namelen, char *key, size_t keylen, HashTable *parent, zval *zv, phpdbg_watch_parse_struct *info) {
+       int ret;
+       phpdbg_watch_element *element = ecalloc(1, sizeof(phpdbg_watch_element));
+       element->str = zend_string_init(name, namelen, 0);
+       element->name_in_parent = zend_string_init(key, keylen, 0);
+       element->parent_container = parent;
+       element->parent = PHPDBG_G(watch_tmp);
+       element->child = NULL;
 
-               if (watch->type == WATCH_ON_ZVAL) {
-                       phpdbg_delete_zval_watchpoints_recursive(watch);
-               } else if (watch->type == WATCH_ON_HASHTABLE) {
-                       phpdbg_watchpoint_t *watchpoint;
+       ret = info->callback(zv, element);
 
-                       watch->implicit_ht_count++;
+       efree(name);
+       efree(key);
 
-                       ZEND_HASH_FOREACH_PTR(&((phpdbg_watch_ht_info *) phpdbg_btree_find(&PHPDBG_G(watch_HashTables), (zend_ulong) HT_WATCH_HT(watch))->ptr)->watches, watchpoint) {
-                               phpdbg_delete_watchpoint_recursive(watchpoint, 1);
-                       } ZEND_HASH_FOREACH_END();
+       if (ret != SUCCESS) {
+               phpdbg_remove_watch_element(element);
+       } else {
+               if (PHPDBG_G(watch_tmp)) {
+                       PHPDBG_G(watch_tmp)->child = element;
                }
-       }
 
-       phpdbg_delete_implicit_parents(watch);
-
-       phpdbg_deactivate_watchpoint(watch);
-       phpdbg_remove_watchpoint(watch);
-
-       phpdbg_free_watch(watch);
-       efree(watch);
-}
-
-static void phpdbg_watch_mem_dtor(void *llist_data) {
-       phpdbg_watch_memdump *dump = *(phpdbg_watch_memdump **) llist_data;
+               if (element->child) {
+                       element = element->child;
+               }
+               element->id = PHPDBG_G(watch_elements).nNextFreeElement;
+               zend_hash_index_add_ptr(&PHPDBG_G(watch_elements), element->id, element);
 
-       /* Disble writing again */
-       if (dump->reenable_writing) {
-               mprotect(dump->page, dump->size, PROT_READ);
+               phpdbg_notice("watchadd", "index=\"%d\" variable=\"%.*s\"", "Added%s watchpoint #%d for %.*s", (element->flags & PHPDBG_WATCH_RECURSIVE_ROOT) ? " recursive" : "", element->id, (int) ZSTR_LEN(element->str), ZSTR_VAL(element->str));
        }
 
-       free(dump);
-}
+       PHPDBG_G(watch_tmp) = NULL;
 
-static void phpdbg_watch_free_ptr_dtor(zval *ptr) {
-       efree(Z_PTR_P(ptr));
+       return ret;
 }
 
-void phpdbg_setup_watchpoints(void) {
-#if _SC_PAGE_SIZE
-       phpdbg_pagesize = sysconf(_SC_PAGE_SIZE);
-#elif _SC_PAGESIZE
-       phpdbg_pagesize = sysconf(_SC_PAGESIZE);
-#elif _SC_NUTC_OS_PAGESIZE
-       phpdbg_pagesize = sysconf(_SC_NUTC_OS_PAGESIZE);
-#else
-       phpdbg_pagesize = 4096; /* common pagesize */
-#endif
-
-       zend_llist_init(&PHPDBG_G(watchlist_mem), sizeof(void *), phpdbg_watch_mem_dtor, 1);
-       phpdbg_btree_init(&PHPDBG_G(watchpoint_tree), sizeof(void *) * 8);
-       phpdbg_btree_init(&PHPDBG_G(watch_HashTables), sizeof(void *) * 8);
-       zend_hash_init(&PHPDBG_G(watchpoints), 8, NULL, phpdbg_watch_dtor, 0);
-       zend_hash_init(&PHPDBG_G(watch_collisions), 8, NULL, phpdbg_watch_free_ptr_dtor, 0);
+PHPDBG_API int phpdbg_watchpoint_parse_input(char *input, size_t len, HashTable *parent, size_t i, phpdbg_watch_parse_struct *info, zend_bool silent) {
+       return phpdbg_parse_variable_with_arg(input, len, parent, i, (phpdbg_parse_var_with_arg_func) phpdbg_watchpoint_parse_wrapper, NULL, 0, info);
 }
 
-static void phpdbg_print_changed_zval(phpdbg_watch_memdump *dump) {
-       /* fetch all changes between dump->page and dump->page + dump->size */
-       phpdbg_btree_position pos = phpdbg_btree_find_between(&PHPDBG_G(watchpoint_tree), (zend_ulong) dump->page, (zend_ulong) dump->page + dump->size);
-       phpdbg_btree_result *result;
-       int elementDiff;
-       void *curTest;
+static int phpdbg_watchpoint_parse_step(char *name, size_t namelen, char *key, size_t keylen, HashTable *parent, zval *zv, phpdbg_watch_parse_struct *info) {
+       phpdbg_watch_element *element;
 
-       dump->reenable_writing = 0;
+       /* do not install watch elements for references */
+       if (PHPDBG_G(watch_tmp) && Z_ISREF_P(PHPDBG_G(watch_tmp)->watch->addr.zv) && Z_REFVAL_P(PHPDBG_G(watch_tmp)->watch->addr.zv) == zv) {
+               efree(name);
+               efree(key);
+               return SUCCESS;
+       }
 
-       while ((result = phpdbg_btree_next(&pos))) {
-               phpdbg_watchpoint_t *watch = result->ptr;
-               void *oldPtr = (char *) &dump->data + ((size_t) watch->addr.ptr - (size_t) dump->page);
-               char reenable = 1;
-               int removed = 0;
+       element = ecalloc(1, sizeof(phpdbg_watch_element));
+       element->flags = PHPDBG_WATCH_IMPLICIT;
+       element->str = zend_string_copy(info->str);
+       element->name_in_parent = zend_string_init(key, keylen, 0);
+       element->parent_container = parent;
+       element->parent = PHPDBG_G(watch_tmp);
+       element = phpdbg_add_bucket_watch_element((Bucket *) zv, element);
 
-               if ((size_t) watch->addr.ptr < (size_t) dump->page || (size_t) watch->addr.ptr + watch->size > (size_t) dump->page + dump->size) {
-                       continue;
-               }
+       efree(name);
+       efree(key);
 
-               /* Test if the zval was separated or replaced and if necessary move the watchpoint */
-               if ((watch->type == WATCH_ON_HASHTABLE || watch->type == WATCH_ON_ZVAL) && watch->parent_container) {
-                       if ((curTest = zend_symtable_find(watch->parent_container, watch->name_in_parent))) {
-                               while (Z_TYPE_P((zval *) curTest) == IS_INDIRECT) {
-                                       curTest = Z_INDIRECT_P((zval *) curTest);
-                               }
+       if (PHPDBG_G(watch_tmp)) {
+               PHPDBG_G(watch_tmp)->child = element;
+       }
+       PHPDBG_G(watch_tmp) = element;
 
-                               if (watch->type == WATCH_ON_HASHTABLE) {
-                                       switch (Z_TYPE_P((zval *) curTest)) {
-                                               case IS_ARRAY:
-                                                       curTest = (void *) Z_ARRVAL_P((zval *) curTest);
-                                                       break;
-                                               case IS_OBJECT:
-                                                       curTest = (void *) Z_OBJPROP_P((zval *) curTest);
-                                                       break;
-                                       }
-                               }
+       return SUCCESS;
+}
 
-                               if (curTest != watch->addr.ptr) {
-                                       phpdbg_deactivate_watchpoint(watch);
-                                       phpdbg_remove_watchpoint(watch);
-                                       watch->addr.ptr = curTest;
-                                       phpdbg_store_watchpoint(watch);
-                                       phpdbg_activate_watchpoint(watch);
+static int phpdbg_watchpoint_parse_symtables(char *input, size_t len, int (*callback)(zval *, phpdbg_watch_element *)) {
+       zend_class_entry *scope = zend_get_executed_scope();
+       phpdbg_watch_parse_struct info;
+       int ret;
 
-                                       reenable = 0;
-                               }
-                       } else {
-                               removed = 1;
-                       }
-               }
+       if (scope && len >= 5 && !memcmp("$this", input, 5)) {
+               zend_hash_str_add(EG(current_execute_data)->symbol_table, ZEND_STRL("this"), &EG(current_execute_data)->This);
+       }
 
-               /* Show to the user what changed and delete watchpoint upon removal */
-               {
-                       zend_bool do_break = 0;
+       if (callback == phpdbg_create_array_watchpoint) {
+               info.str = strpprintf(0, "%.*s[]", (int) len, input);
+       } else {
+               info.str = zend_string_init(input, len, 0);
+       }
+       info.callback = callback;
 
-                       switch (watch->type) {
-                               case WATCH_ON_ZVAL:
-                                       do_break = memcmp(oldPtr, watch->addr.zv, sizeof(zend_value) + sizeof(uint32_t) /* value + typeinfo */) != 0;
-                                       if (!do_break) {
-                                               goto end;
-                                       }
-                                       break;
-                               case WATCH_ON_HASHTABLE:
-                                       do_break = zend_hash_num_elements(HT_PTR_HT(oldPtr)) != zend_hash_num_elements(HT_WATCH_HT(watch));
-                                       if (!do_break) {
-                                               goto end;
-                                       }
-                                       break;
-                               case WATCH_ON_REFCOUNTED:
-                                       do_break = memcmp(oldPtr, watch->addr.ref, sizeof(uint32_t) /* no zend_refcounted metadata info */) != 0;
-                                       if (!do_break) {
-                                               goto end;
-                                       }
-                                       if (!(PHPDBG_G(flags) & PHPDBG_SHOW_REFCOUNTS)) {
-                                               do_break = 0;
-                                       }
-                                       break;
-                       }
+       if (phpdbg_is_auto_global(input, len) && phpdbg_watchpoint_parse_input(input, len, &EG(symbol_table), 0, &info, 1) != FAILURE) {
+               zend_string_release(info.str);
+               return SUCCESS;
+       }
 
-                       if (!(watch->flags & PHPDBG_WATCH_NORMAL)) {
-                               do_break = 0;
-                       }
+       ret = phpdbg_parse_variable_with_arg(input, len, EG(current_execute_data)->symbol_table, 0, (phpdbg_parse_var_with_arg_func) phpdbg_watchpoint_parse_wrapper, (phpdbg_parse_var_with_arg_func) phpdbg_watchpoint_parse_step, 0, &info);
 
-                       if (do_break) {
-                               PHPDBG_G(watchpoint_hit) = 1;
+       zend_string_release(info.str);
+       return ret;
+}
 
-                               phpdbg_notice("watchhit", "variable=\"%s\"", "Breaking on watchpoint %.*s", (int) ZSTR_LEN(watch->str), ZSTR_VAL(watch->str));
-                               phpdbg_xml("<watchdata %r>");
+PHPDBG_WATCH(delete) /* {{{ */
+{
+       phpdbg_watch_element *element;
+       switch (param->type) {
+               case NUMERIC_PARAM:
+                       if ((element = zend_hash_index_find_ptr(&PHPDBG_G(watch_elements), param->num))) {
+                               phpdbg_remove_watch_element(element);
+                               phpdbg_notice("watchdelete", "variable=\"%.*s\"", "Removed watchpoint %d", (int) param->num);
+                       } else {
+                               phpdbg_error("watchdelete", "type=\"nowatch\"", "Nothing was deleted, no corresponding watchpoint found");
                        }
+                       break;
 
-                       switch (watch->type) {
-                               case WATCH_ON_ZVAL: {
-                                       zend_bool show_value = memcmp(oldPtr, watch->addr.zv, sizeof(zend_value) + sizeof(uint32_t) /* no metadata info */) != 0;
-
-                                       if ((watch->flags & PHPDBG_WATCH_NORMAL) && (removed || show_value)) {
-/* TODO: Merge with refcounting watches, store if watched ref value is to be dropped etc. [for example: manually increment refcount transparently for displaying and drop it if it decrements to 1] */
-                                               if (Z_REFCOUNTED_P((zval *) oldPtr)) {
-                                                       phpdbg_writeln("watchvalue", "type=\"old\" inaccessible=\"inaccessible\"", "Old value inaccessible or destroyed");
-                                               } else {
-                                                       phpdbg_out("Old value: ");
-                                                       phpdbg_xml("<watchvalue %r type=\"old\">");
-                                                       zend_print_flat_zval_r((zval *) oldPtr);
-                                                       phpdbg_xml("</watchvalue>");
-                                                       phpdbg_out("\n");
-                                               }
-                                       }
+               phpdbg_default_switch_case();
+       }
 
-                                       /* check if zval was removed */
-                                       if (removed) {
-                                               if (watch->flags & PHPDBG_WATCH_NORMAL) {
-                                                       phpdbg_notice("watchdelete", "variable=\"%.*s\"", "Watchpoint %.*s was unset, removing watchpoint", (int) ZSTR_LEN(watch->str), ZSTR_VAL(watch->str));
-                                               }
-                                               zend_hash_del(&PHPDBG_G(watchpoints), watch->str);
+       return SUCCESS;
+} /* }}} */
 
-                                               reenable = 0;
+int phpdbg_create_var_watchpoint(char *input, size_t len) {
+       if (phpdbg_rebuild_symtable() == FAILURE) {
+               return FAILURE;
+       }
 
-                                               if (Z_REFCOUNTED_P((zval *) oldPtr)) {
-                                                       phpdbg_remove_watch_collision(watch);
-                                               }
-                                               break;
-                                       }
+       return phpdbg_watchpoint_parse_symtables(input, len, phpdbg_create_simple_watchpoint);
+}
 
-                                       if ((watch->flags & PHPDBG_WATCH_NORMAL) && show_value) {
-                                               phpdbg_out("New value%s: ", Z_ISREF_P(watch->addr.zv) ? " (reference)" : "");
-                                               phpdbg_xml("<watchvalue %r%s type=\"new\">", Z_ISREF_P(watch->addr.zv) ? " reference=\"reference\"" : "");
-                                               zend_print_flat_zval_r(watch->addr.zv);
-                                               phpdbg_xml("</watchvalue>");
-                                               phpdbg_out("\n");
-                                       }
+PHPDBG_WATCH(recursive) /* {{{ */
+{
+       if (phpdbg_rebuild_symtable() == FAILURE) {
+               return SUCCESS;
+       }
 
-                                       /* add new watchpoints if necessary */
-                                       if (Z_PTR_P(watch->addr.zv) != Z_PTR_P((zval *) oldPtr) || Z_TYPE_P(watch->addr.zv) != Z_TYPE_P((zval *) oldPtr)) {
-                                               if (Z_REFCOUNTED_P((zval *) oldPtr)) {
-                                                       zval *new_zv = watch->addr.zv;
-                                                       watch->addr.ptr = oldPtr;
-                                                       phpdbg_remove_watch_collision(watch);
-                                                       watch->addr.zv = new_zv;
-                                               }
-                                               if (Z_REFCOUNTED_P(watch->addr.zv)) {
-                                                       if ((watch->flags & PHPDBG_WATCH_NORMAL) && (PHPDBG_G(flags) & PHPDBG_SHOW_REFCOUNTS)) {
-                                                               phpdbg_writeln("watchrefcount", "type=\"new\" refcount=\"%d\"", "New refcount: %d", Z_REFCOUNT_P(watch->addr.zv));
-                                                       }
-                                                       if (watch->flags & PHPDBG_WATCH_RECURSIVE) {
-                                                               phpdbg_create_recursive_watchpoint(watch);
-                                                       } else if (Z_ISREF_P(watch->addr.zv)) {
-                                                               phpdbg_create_reference_watch(watch);
-                                                       }
-                                               }
-                                       }
+       switch (param->type) {
+               case STR_PARAM:
+                       phpdbg_watchpoint_parse_symtables(param->str, param->len, phpdbg_create_recursive_watchpoint);
+                       break;
 
-                                       break;
-                               }
-                               case WATCH_ON_HASHTABLE:
-                                       /* We should be safely able to assume the HashTable to be consistent (inconsistent HashTables should have been caught by phpdbg_watch_efree() */
-                                       elementDiff = zend_hash_num_elements(HT_PTR_HT(oldPtr)) - zend_hash_num_elements(HT_WATCH_HT(watch));
-                                       if ((watch->flags & PHPDBG_WATCH_NORMAL) && elementDiff) {
-                                               if (elementDiff > 0) {
-                                                       phpdbg_writeln("watchsize", "removed=\"%d\"", "%d elements were removed from the array", elementDiff);
-                                               } else {
-                                                       phpdbg_writeln("watchsize", "added=\"%d\"", "%d elements were added to the array", -elementDiff);
-                                               }
-                                       }
-                                       /* add new watchpoints if necessary */
-                                       if (watch->flags & PHPDBG_WATCH_RECURSIVE) {
-                                               phpdbg_create_recursive_ht_watch(watch);
-                                       }
-                                       if ((watch->flags & PHPDBG_WATCH_NORMAL) && HT_WATCH_HT(watch)->nInternalPointer != HT_PTR_HT(oldPtr)->nInternalPointer) {
-                                               phpdbg_writeln("watcharrayptr", "", "Internal pointer of array was changed");
-                                       }
-                                       break;
-                               case WATCH_ON_REFCOUNTED: {
-                                       if ((watch->flags & PHPDBG_WATCH_NORMAL) && (PHPDBG_G(flags) & PHPDBG_SHOW_REFCOUNTS)) {
-                                               phpdbg_writeln("watchrefcount", "type=\"old\" refcount=\"%d\"", "Old refcount: %d", GC_REFCOUNT((zend_refcounted *) oldPtr));
-                                               phpdbg_writeln("watchrefcount", "type=\"new\" refcount=\"%d\"", "New refcount: %d", GC_REFCOUNT(watch->addr.ref));
-                                       }
-                                       break;
-                               }
-                       }
+               phpdbg_default_switch_case();
+       }
 
-                       if (do_break) {
-                               phpdbg_xml("</watchdata>");
-                       }
-               } end:
+       return SUCCESS;
+} /* }}} */
 
-               dump->reenable_writing = dump->reenable_writing | reenable;
+PHPDBG_WATCH(array) /* {{{ */
+{
+       if (phpdbg_rebuild_symtable() == FAILURE) {
+               return SUCCESS;
        }
-}
 
-int phpdbg_print_changed_zvals(void) {
-       zend_llist_position pos;
-       phpdbg_watch_memdump **dump;
-       int ret;
+       switch (param->type) {
+               case STR_PARAM:
+                       phpdbg_watchpoint_parse_symtables(param->str, param->len, phpdbg_create_array_watchpoint);
+                       break;
 
-       if (zend_llist_count(&PHPDBG_G(watchlist_mem)) == 0) {
-               return FAILURE;
+               phpdbg_default_switch_case();
        }
 
-       dump = (phpdbg_watch_memdump **) zend_llist_get_last_ex(&PHPDBG_G(watchlist_mem), &pos);
-
-       do {
-               phpdbg_print_changed_zval(*dump);
-       } while ((dump = (phpdbg_watch_memdump **) zend_llist_get_prev_ex(&PHPDBG_G(watchlist_mem), &pos)));
+       return SUCCESS;
+} /* }}} */
 
-       zend_llist_clean(&PHPDBG_G(watchlist_mem));
 
-       ret = PHPDBG_G(watchpoint_hit) ? SUCCESS : FAILURE;
-       PHPDBG_G(watchpoint_hit) = 0;
+void phpdbg_setup_watchpoints(void) {
+#if _SC_PAGE_SIZE
+       phpdbg_pagesize = sysconf(_SC_PAGE_SIZE);
+#elif _SC_PAGESIZE
+       phpdbg_pagesize = sysconf(_SC_PAGESIZE);
+#elif _SC_NUTC_OS_PAGESIZE
+       phpdbg_pagesize = sysconf(_SC_NUTC_OS_PAGESIZE);
+#else
+       phpdbg_pagesize = 4096; /* common pagesize */
+#endif
 
-       return ret;
+       phpdbg_btree_init(&PHPDBG_G(watchpoint_tree), sizeof(void *) * 8);
+       phpdbg_btree_init(&PHPDBG_G(watch_HashTables), sizeof(void *) * 8);
+       zend_hash_init(&PHPDBG_G(watch_elements), 8, NULL, NULL, 0);
+       zend_hash_init(&PHPDBG_G(watch_collisions), 8, NULL, NULL, 0);
+       zend_hash_init(&PHPDBG_G(watch_recreation), 8, NULL, NULL, 0);
+       zend_hash_init(&PHPDBG_G(watch_free), 8, NULL, NULL, 0);
+
+       /* put these on a separate page, to avoid conflicts with other memory */
+       PHPDBG_G(watchlist_mem) = malloc(phpdbg_pagesize > sizeof(HashTable) ? phpdbg_pagesize : sizeof(HashTable));
+       zend_hash_init(PHPDBG_G(watchlist_mem), phpdbg_pagesize / (sizeof(Bucket) + sizeof(uint32_t)), NULL, NULL, 1);
+       PHPDBG_G(watchlist_mem_backup) = malloc(phpdbg_pagesize > sizeof(HashTable) ? phpdbg_pagesize : sizeof(HashTable));
+       zend_hash_init(PHPDBG_G(watchlist_mem_backup), phpdbg_pagesize / (sizeof(Bucket) + sizeof(uint32_t)), NULL, NULL, 1);
 }
 
-void phpdbg_list_watchpoints(void) {
-       phpdbg_watchpoint_t *watch;
-
-       phpdbg_xml("<watchlist %r>");
+void phpdbg_destroy_watchpoints(void) {
+       phpdbg_watch_element *element;
+       phpdbg_btree_position pos;
+       phpdbg_btree_result *res;
 
-       ZEND_HASH_FOREACH_PTR(&PHPDBG_G(watchpoints), watch) {
-               if (watch->flags & PHPDBG_WATCH_NORMAL) {
-                       phpdbg_writeln("watchvariable", "variable=\"%.*s\" on=\"%s\" type=\"%s\"", "%.*s (%s, %s)", (int) ZSTR_LEN(watch->str), ZSTR_VAL(watch->str), watch->type == WATCH_ON_HASHTABLE ? "array" : watch->type == WATCH_ON_REFCOUNTED ? "refcount" : "variable", watch->flags == PHPDBG_WATCH_RECURSIVE ? "recursive" : "simple");
-               }
+       /* unconditionally free all remaining elements to avoid memory leaks */
+       ZEND_HASH_FOREACH_PTR(&PHPDBG_G(watch_recreation), element) {
+               phpdbg_automatic_dequeue_free(element);
        } ZEND_HASH_FOREACH_END();
 
-       phpdbg_xml("</watchlist>");
-}
-
-void phpdbg_watch_efree(void *ptr) {
-       phpdbg_btree_result *result;
-
-       result = phpdbg_btree_find_closest(&PHPDBG_G(watchpoint_tree), (zend_ulong) ptr);
-
-       if (result) {
-               phpdbg_watchpoint_t *watch = result->ptr;
-
-               if ((size_t) watch->addr.ptr + watch->size > (size_t) ptr) {
-                       if (watch->type == WATCH_ON_REFCOUNTED) {
-                               /* remove watchpoint here from btree, zval watchpoint will remove it via remove_watch_collison */
-                               phpdbg_deactivate_watchpoint(watch);
-                               phpdbg_remove_watchpoint(watch);
-                       } else {
-                               if (watch->type == WATCH_ON_ZVAL) {
-                                       phpdbg_remove_watch_collision(watch);
-                               }
-                               if (watch->type == WATCH_ON_HASHTABLE && (watch->flags & PHPDBG_WATCH_SIMPLE)) {
-                                       /* when a HashTable is freed, we can safely assume the other zvals all were dtor'ed */
-                                       phpdbg_notice("watchdelete", "variable=\"%.*s\" recursive=\"%s\"", "Array %.*s was removed, removing watchpoint%s", (int) ZSTR_LEN(watch->str), ZSTR_VAL(watch->str), (watch->flags & PHPDBG_WATCH_RECURSIVE) ? " recursively" : "");
-                               }
-                               if (watch->type == WATCH_ON_HASHTABLE || watch->parent == NULL || watch->parent->type != WATCH_ON_ZVAL) { /* no references */
-                                       zend_hash_del(&PHPDBG_G(watchpoints), watch->str);
-                               }
-                       }
-               }
+       /* upon fatal errors etc. (i.e. CG(unclean_shutdown) == 1), some watchpoints may still be active. Ensure memory is not watched anymore for next run. Do not care about memory freeing here, shutdown is unclean and near anyway. */
+       pos = phpdbg_btree_find_between(&PHPDBG_G(watchpoint_tree), 0, -1);
+       while ((res = phpdbg_btree_next(&pos))) {
+               phpdbg_deactivate_watchpoint(res->ptr);
        }
 
-       if (PHPDBG_G(original_free_function)) {
-               PHPDBG_G(original_free_function)(ptr);
-       }
+       zend_hash_destroy(&PHPDBG_G(watch_elements)); PHPDBG_G(watch_elements).nNumOfElements = 0; /* phpdbg_watch_efree() is checking against this arrays size */
+       zend_hash_destroy(&PHPDBG_G(watch_recreation));
+       zend_hash_destroy(&PHPDBG_G(watch_free));
+       zend_hash_destroy(&PHPDBG_G(watch_collisions));
+       zend_hash_destroy(PHPDBG_G(watchlist_mem));
+       free(PHPDBG_G(watchlist_mem));
+       zend_hash_destroy(PHPDBG_G(watchlist_mem_backup));
+       free(PHPDBG_G(watchlist_mem_backup));
 }
index 82c925e01126261b4c06f1e5f5dd46e632142f68..4f19e5c4d66d70a9f063cfc991d4c5a2117bcc15 100644 (file)
@@ -45,6 +45,9 @@ typedef enum {
        WATCH_ON_ZVAL,
        WATCH_ON_HASHTABLE,
        WATCH_ON_REFCOUNTED,
+       WATCH_ON_STR,
+       WATCH_ON_HASHDATA,
+       WATCH_ON_BUCKET,
 } phpdbg_watchtype;
 
 
@@ -54,41 +57,68 @@ typedef enum {
 #define PHPDBG_WATCH_OBJECT     0x08
 #define PHPDBG_WATCH_NORMAL     (PHPDBG_WATCH_SIMPLE | PHPDBG_WATCH_RECURSIVE)
 #define PHPDBG_WATCH_IMPLICIT   0x10
+#define PHPDBG_WATCH_RECURSIVE_ROOT 0x20
 
-#define PHPDBG_DESTRUCTED_ZVAL 0x80
+typedef struct _phpdbg_watch_collision phpdbg_watch_collision;
 
-typedef struct _phpdbg_watchpoint_t phpdbg_watchpoint_t;
-
-struct _phpdbg_watchpoint_t {
+typedef struct _phpdbg_watchpoint_t {
        union {
                zval *zv;
                zend_refcounted *ref;
+               Bucket *bucket;
                void *ptr;
        } addr;
        size_t size;
        phpdbg_watchtype type;
-       char flags;
-       unsigned int implicit_ht_count;
-       phpdbg_watchpoint_t *parent;
-       phpdbg_watchpoint_t *reference;
-       HashTable *parent_container;
-       zend_string *name_in_parent;
-       zend_string *str;
+       zend_refcounted *ref; /* key to fetch the collision on parents */
+       HashTable elements;
+       phpdbg_watch_collision *coll; /* only present on *children* */
+       union {
+               zval zv;
+               Bucket bucket;
+               zend_refcounted ref;
+               HashTable ht;
+               zend_string *str;
+       } backup;
+} phpdbg_watchpoint_t;
+
+struct _phpdbg_watch_collision {
+       phpdbg_watchpoint_t ref;
+       phpdbg_watchpoint_t reference;
+       HashTable parents;
 };
 
-typedef struct {
+typedef struct _phpdbg_watch_element {
+       uint32_t id;
        phpdbg_watchpoint_t *watch;
-       unsigned int refs;
-       HashTable watches;
-       HashTable implicit_watches;
-} phpdbg_watch_collision;
+       char flags;
+       struct _phpdbg_watch_element *child; /* always set for implicit watches */
+       struct _phpdbg_watch_element *parent;
+       HashTable child_container; /* children of this watch element for recursive array elements */
+       HashTable *parent_container; /* container of the value */
+       zend_string *name_in_parent;
+       zend_string *str;
+       union {
+               zval zv;
+               zend_refcounted ref;
+               HashTable ht;
+       } backup; /* backup for when watchpoint gets dissociated */
+} phpdbg_watch_element;
 
 typedef struct {
-       dtor_func_t dtor;
-       HashTable watches;
+       /* to watch rehashes (yes, this is not *perfect*, but good enough for everything in PHP...) */
+       phpdbg_watchpoint_t hash_watch; /* must be first element */
+       Bucket *last;
+       zend_string *last_str;
+       zend_ulong last_idx;
+
+       HashTable *ht;
+       size_t data_size;
+       HashTable watches; /* contains phpdbg_watch_element */
 } phpdbg_watch_ht_info;
 
 void phpdbg_setup_watchpoints(void);
+void phpdbg_destroy_watchpoints(void);
 
 #ifndef _WIN32
 int phpdbg_watchpoint_segfault_handler(siginfo_t *info, void *context);
index 1183bb0e5728074636f20d0315a21f117fe67d4c..f1d14711623e5676eebbe0d04cfa178d7300cbc1 100644 (file)
@@ -16,7 +16,7 @@ prompt> [Breakpoint #0 at %s:3, hits: 1]
 >00003: $a = 1;
  00004: $b = [$a];
  00005: 
-prompt> [Set recursive watchpoint on $b]
+prompt> [Added recursive watchpoint #0 for $b]
 prompt> [Breaking on watchpoint $b]
 Old value: 
 New value: Array ([0] => 1)
@@ -33,7 +33,7 @@ Old value:
 New value: 2
 >00007: $b = 2;
  00008: 
-prompt> [$b was removed, removing watchpoint recursively]
+prompt> [$b has been removed, removing watchpoint recursively]
 [Script ended normally]
 prompt> 
 --FILE--
diff --git a/sapi/phpdbg/tests/watch_002.phpt b/sapi/phpdbg/tests/watch_002.phpt
new file mode 100644 (file)
index 0000000..a12400e
--- /dev/null
@@ -0,0 +1,33 @@
+--TEST--
+Test simple array watchpoint with replace
+--PHPDBG--
+b 6
+r
+w a $a
+c
+
+q
+--EXPECTF--
+[Successful compilation of %s]
+prompt> [Breakpoint #0 added at %s:6]
+prompt> [Breakpoint #0 at %s:6, hits: 1]
+>00006: $a[0] = 2;
+ 00007: 
+ 00008: $a = [0 => 3, 1 => 4];
+prompt> [Added watchpoint #0 for $a[]]
+prompt> [Breaking on watchpoint $a[]]
+1 elements were added to the array
+>00008: $a = [0 => 3, 1 => 4];
+ 00009: 
+prompt> [$a[] has been removed, removing watchpoint]
+[Script ended normally]
+prompt> 
+--FILE--
+<?php
+
+$a = [];
+
+$a[0] = 1;
+$a[0] = 2;
+
+$a = [0 => 3, 1 => 4];
diff --git a/sapi/phpdbg/tests/watch_003.phpt b/sapi/phpdbg/tests/watch_003.phpt
new file mode 100644 (file)
index 0000000..cb09bd4
--- /dev/null
@@ -0,0 +1,40 @@
+--TEST--
+Test simple watchpoint with replace
+--PHPDBG--
+b 6
+r
+w $a[0]
+c
+
+
+q
+--EXPECTF--
+[Successful compilation of %s]
+prompt> [Breakpoint #0 added at %s:6]
+prompt> [Breakpoint #0 at %s:6, hits: 1]
+>00006: $a[0] = 2;
+ 00007: 
+ 00008: $a = [0 => 3, 1 => 4];
+prompt> [Added watchpoint #0 for $a[0]]
+prompt> [Breaking on watchpoint $a[0]]
+Old value: 1
+New value: 2
+>00008: $a = [0 => 3, 1 => 4];
+ 00009: 
+prompt> [Breaking on watchpoint $a[0]]
+Old value: 2
+New value: 3
+>00008: $a = [0 => 3, 1 => 4];
+ 00009: 
+prompt> [$a[0] has been removed, removing watchpoint]
+[Script ended normally]
+prompt> 
+--FILE--
+<?php
+
+$a = [];
+
+$a[0] = 1;
+$a[0] = 2;
+
+$a = [0 => 3, 1 => 4];
diff --git a/sapi/phpdbg/tests/watch_004.phpt b/sapi/phpdbg/tests/watch_004.phpt
new file mode 100644 (file)
index 0000000..46b97e1
--- /dev/null
@@ -0,0 +1,37 @@
+--TEST--
+Test detection of inline string manipulations on zval watch
+--PHPDBG--
+b 3
+r
+w $a
+c
+
+
+q
+--EXPECTF--
+[Successful compilation of %s]
+prompt> [Breakpoint #0 added at %s:3]
+prompt> [Breakpoint #0 at %s:3, hits: 1]
+>00003: $b = "a";
+ 00004: $a = $b.$b;
+ 00005: $a[1] = "b";
+prompt> [Added watchpoint #0 for $a]
+prompt> [Breaking on watchpoint $a]
+Old value: 
+New value: aa
+>00005: $a[1] = "b";
+ 00006: 
+prompt> [Breaking on watchpoint $a]
+Old value: aa
+New value: ab
+>00005: $a[1] = "b";
+ 00006: 
+prompt> [$a has been removed, removing watchpoint]
+[Script ended normally]
+prompt> 
+--FILE--
+<?php
+
+$b = "a";
+$a = $b.$b;
+$a[1] = "b";
diff --git a/sapi/phpdbg/tests/watch_005.phpt b/sapi/phpdbg/tests/watch_005.phpt
new file mode 100644 (file)
index 0000000..d6bae9d
--- /dev/null
@@ -0,0 +1,48 @@
+--TEST--
+Test proper watch comparisons when having multiple levels of indirection from a zval to its value
+--PHPDBG--
+b 3
+r
+w r $a
+c
+
+
+
+q
+--EXPECTF--
+[Successful compilation of %s]
+prompt> [Breakpoint #0 added at %s:3]
+prompt> [Breakpoint #0 at %s:3, hits: 1]
+>00003: $b = "a";
+ 00004: $a = $b.$b;
+ 00005: $c = &$a;
+prompt> [Added recursive watchpoint #0 for $a]
+prompt> [Breaking on watchpoint $a]
+Old value: 
+New value: aa
+>00005: $c = &$a;
+ 00006: $a[1] = "b";
+ 00007: 
+prompt> [Breaking on watchpoint $a]
+Old value inaccessible or destroyed
+New value (reference): aa
+>00006: $a[1] = "b";
+ 00007: 
+ 00008: exit;
+prompt> [Breaking on watchpoint $a]
+Old value: aa
+New value: ab
+>00008: exit;
+ 00009: 
+prompt> [$a has been removed, removing watchpoint recursively]
+[Script ended normally]
+prompt> 
+--FILE--
+<?php
+
+$b = "a";
+$a = $b.$b;
+$c = &$a;
+$a[1] = "b";
+
+exit;
diff --git a/sapi/phpdbg/tests/watch_006.phpt b/sapi/phpdbg/tests/watch_006.phpt
new file mode 100644 (file)
index 0000000..b8f6206
--- /dev/null
@@ -0,0 +1,72 @@
+--TEST--
+Test multiple watch elements pointing to the same watchpoint
+--PHPDBG--
+b 4
+r
+w $a[0]
+w r $b
+c
+
+
+
+
+
+
+q
+--EXPECTF--
+[Successful compilation of %s]
+prompt> [Breakpoint #0 added at %s:4]
+prompt> [Breakpoint #0 at %s:4, hits: 1]
+>00004: $a[0] = 1;
+ 00005: $b = &$a;
+ 00006: $a[0] = 2;
+prompt> [Added watchpoint #0 for $a[0]]
+prompt> [Added recursive watchpoint #1 for $b]
+prompt> [Breaking on watchpoint $a[0]]
+Old value: 0
+New value: 1
+>00005: $b = &$a;
+ 00006: $a[0] = 2;
+ 00007: $a[1] = 3;
+prompt> [Breaking on watchpoint $b]
+Old value: 
+New value (reference): Array ([0] => 1)
+>00006: $a[0] = 2;
+ 00007: $a[1] = 3;
+ 00008: $c = [1];
+prompt> [Breaking on watchpoint $a[0]]
+Old value: 1
+New value: 2
+>00007: $a[1] = 3;
+ 00008: $c = [1];
+ 00009: $b = &$c;
+prompt> [Element 1 has been added to watchpoint]
+[Breaking on watchpoint $b[]]
+1 elements were added to the array
+>00008: $c = [1];
+ 00009: $b = &$c;
+ 00010: 
+prompt> [Breaking on watchpoint $b]
+Old value inaccessible or destroyed
+New value (reference): Array ([0] => 2,[1] => 3)
+>00009: $b = &$c;
+ 00010: 
+prompt> [Breaking on watchpoint $b]
+Old value inaccessible or destroyed
+New value (reference): Array ([0] => 1)
+>00009: $b = &$c;
+ 00010: 
+prompt> [$b has been removed, removing watchpoint recursively]
+[$a[0] has been removed, removing watchpoint]
+[Script ended normally]
+prompt> 
+--FILE--
+<?php
+
+$a = [0];
+$a[0] = 1;
+$b = &$a;
+$a[0] = 2;
+$a[1] = 3;
+$c = [1];
+$b = &$c;