├── README.md └── php-memory.py /README.md: -------------------------------------------------------------------------------- 1 | # [PoC] PHP Memory Map Visualizer for GDB 2 | 3 | This repository contains a Python script for GDB that visualizes the PHP memory map as a color-coded memory map in the GDB TUI (text user interface). The script is useful for debugging memory-related issues in PHP applications and provides a convenient way to visualize the memory usage of a PHP process in real time. 4 | 5 | ## Introduction 6 | 7 | The PHP Memory Map Visualizer for GDB is a powerful tool for PHP developers who need to diagnose and fix memory-related issues in their applications. By providing a visual representation of the PHP memory map, the tool allows developers to quickly identify memory leaks, fragmentation, and other issues that may be affecting application performance. 8 | 9 | ## Disclaimer 10 | 11 | This script is a **proof-of-concept** with extremely limited functionality and should not be used in production environments without thorough testing and modification. The script is provided as-is, without any warranty or support, and the author is not responsible for any damage or data loss that may result from its use. Use at your own risk. 12 | 13 | ## Installation 14 | 15 | To use the PHP Memory Map Visualizer for GDB, you will need to install GDB, PHP with debug symbols, and the dependencies required to build PHP. Here are the basic steps to install the tool: 16 | 17 | ### Step 1. Clone this repository 18 | 19 | ``` bash 20 | $ git clone git@github.com:DmitryKirillov/gdb-php-memory.git 21 | ``` 22 | 23 | ### Step 2. Install required dependencies 24 | 25 | ``` bash 26 | $ sudo apt-get install \ 27 | build-essential \ 28 | autoconf \ 29 | automake \ 30 | bison \ 31 | flex \ 32 | re2c \ 33 | libtool \ 34 | make \ 35 | pkgconf \ 36 | git \ 37 | libxml2-dev \ 38 | libsqlite3-dev \ 39 | gdb 40 | ``` 41 | 42 | ### Step 3. Install PHP with debug symbols 43 | 44 | ``` bash 45 | $ git clone https://github.com/php/php-src.git \ 46 | --branch=PHP-8.1.14 47 | $ cd php-src 48 | $ ./buildconf --force 49 | $ ./configure \ 50 | --disable-all \ 51 | --enable-cli \ 52 | --enable-debug \ 53 | CFLAGS="-DDEBUG_ZEND=2" 54 | $ make && make test && make install 55 | ``` 56 | 57 | ### Step 4. Copy files to the home directory 58 | 59 | ``` bash 60 | $ cp .gdbinit ~ 61 | ``` 62 | 63 | ## Usage 64 | 65 | To use the PHP Memory Map Visualizer for GDB, you will need to run your PHP script or attach GDB to a running PHP process. 66 | 67 | **Running PHP scripts:** 68 | 69 | ``` bash 70 | $ gdb --args php script.php 71 | ``` 72 | 73 | **Attaching to a running process:** 74 | 75 | ``` 76 | $ ps aux | grep worker.php 77 | root 357167 ... 0:00 php worker.php 78 | $ gdb –p 357167 79 | ``` 80 | 81 | Once GDB is running, you can load and run the PHP Memory Map Visualizer by executing the following commands in the GDB TUI: 82 | 83 | ``` gdb 84 | (gdb) source php-memory.py 85 | (gdb) layout php-memory 86 | ``` 87 | 88 | ## Current Limitations 89 | 90 | The PHP Memory Map Visualizer for GDB is a proof-of-concept with several limitations that users should be aware of: 91 | 92 | - The tool has only been tested with PHP 8.1.14 and may display unexpected results with other versions of PHP 93 | - The tool works only in CLI mode and may not work with web applications or other types of PHP scripts 94 | - The tool displays only the main chunk of memory (the first 2 Mb) and ignores huge allocations, so it may not provide a complete picture of the memory usage of a PHP process 95 | 96 | ## Known Issues 97 | 98 | Please report any bugs or issues with the PHP Memory Map Visualizer to the project maintainers by opening an issue in the GitHub repository. 99 | 100 | ## Further Reading 101 | 102 | - [The PHP Interpreter](https://github.com/php/php-src) 103 | - [nikic's Blog](https://www.npopov.com/) 104 | - [GDB Documentation](https://sourceware.org/gdb/onlinedocs/gdb/index.html) 105 | -------------------------------------------------------------------------------- /php-memory.py: -------------------------------------------------------------------------------- 1 | class PhpMemoryPage: 2 | 3 | def __init__(self, page_type, has_changed, is_highlighted): 4 | self.page_type = page_type 5 | self.has_changed = has_changed 6 | self.is_highlighted = is_highlighted 7 | 8 | 9 | class PhpMemoryWindow: 10 | PAGE_FREE = 0 11 | PAGE_INIT = 1 12 | PAGE_SMALL = 2 13 | PAGE_LARGE = 4 14 | 15 | # See zend_alloc.c 16 | ZEND_MM_IS_LRUN = 0x40000000 17 | ZEND_MM_IS_SRUN = 0x80000000 18 | ZEND_MM_LRUN_PAGES_MASK = 0x000003ff 19 | ZEND_MM_SRUN_BIN_NUM_MASK = 0x0000001f 20 | 21 | # See zend_alloc_sizes.h 22 | PAGES_PER_BIN = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 3, 1, 1, 5, 3, 2, 2, 5, 3, 7, 4, 5, 3] 23 | 24 | def __init__(self, tui_window): 25 | self._chunk_map = [0] * 512 26 | self._chunk_free_map = [0] * 8 27 | self._memory_map = [None] * 512 28 | for i in range(512): 29 | self._memory_map[i] = PhpMemoryPage(self.PAGE_FREE, False, False) 30 | self._memory_map[0] = PhpMemoryPage(self.PAGE_INIT, False, False) 31 | 32 | self._tui_window = tui_window 33 | self._tui_window.title = 'PHP Memory Map' 34 | 35 | self._update_map() 36 | self._render() 37 | 38 | self._before_prompt_listener = lambda: self._before_prompt() 39 | gdb.events.before_prompt.connect(self._before_prompt_listener) 40 | 41 | def _before_prompt(self): 42 | self._update_map() 43 | self._render() 44 | 45 | def _update_map(self): 46 | if (not self._is_program_running()): 47 | return 48 | 49 | heap = gdb.parse_and_eval("zend_mm_get_heap()") 50 | chunk = heap['main_chunk'] 51 | 52 | for i in range(512): 53 | self._chunk_map[i] = int(chunk['map'][i]) 54 | for i in range(8): 55 | self._chunk_free_map[i] = int(chunk['free_map'][i]) 56 | 57 | self._memory_map_has_changed = False 58 | i = 1 # We always start at index 1 (page 0 contains chunk metadata) 59 | while (i < 512): 60 | page = self._chunk_map[i] 61 | count = 1 62 | if (page == 0): # ZEND_MM_IS_FRUN 63 | self._update_pages(i, count, self.PAGE_FREE) 64 | elif ((page & self.ZEND_MM_IS_LRUN) and (page & self.ZEND_MM_IS_SRUN)): # ZEND_MM_IS_NRUN 65 | # todo Consider using a dedicated page type 66 | bin_num = page & self.ZEND_MM_SRUN_BIN_NUM_MASK 67 | count = self.PAGES_PER_BIN[bin_num] 68 | self._update_pages(i, count, self.PAGE_SMALL) 69 | elif (page & self.ZEND_MM_IS_SRUN): # ZEND_MM_IS_SRUN 70 | self._update_pages(i, count, self.PAGE_SMALL) 71 | elif (page & self.ZEND_MM_IS_LRUN): # ZEND_MM_IS_LRUN 72 | count = (page & self.ZEND_MM_LRUN_PAGES_MASK) 73 | self._update_pages(i, count, self.PAGE_LARGE) 74 | i += count 75 | 76 | if (self._memory_map_has_changed): 77 | for current_page in self._memory_map: 78 | current_page.is_highlighted = current_page.has_changed 79 | 80 | def _update_pages(self, i, count, page_type): 81 | max_i = i + count 82 | while (i < max_i): 83 | # PHP sometimes reports small pages that are not listed 84 | # in the heap->free_map. 85 | # We ignore these pages for now. 86 | current_page_type = page_type 87 | free_map_i = i // 64 88 | free_map_j = i % 64 89 | free_map_segment = self._chunk_free_map[free_map_i] 90 | bit_is_set = free_map_segment & (1 << free_map_j) 91 | if ((not bit_is_set) and (page_type != self.PAGE_FREE)): 92 | current_page_type = self.PAGE_FREE 93 | # todo Investigate this bug 94 | elif (bit_is_set and (page_type == self.PAGE_FREE)): 95 | current_page_type = self.PAGE_LARGE 96 | 97 | current_page = self._memory_map[i] 98 | if (current_page.page_type != current_page_type): 99 | self._memory_map_has_changed = True 100 | current_page.page_type = current_page_type 101 | current_page.has_changed = True 102 | else: 103 | current_page.has_changed = False 104 | i += 1 105 | 106 | def _render(self): 107 | self._tui_window.erase() 108 | 109 | if (not self._is_program_running()): 110 | self._tui_window.write('The program is not being run.') 111 | return 112 | 113 | for i in range(8): 114 | s = '' 115 | for j in range(64): 116 | ch = '' 117 | current_page = self._memory_map[i * 64 + j] 118 | if (current_page.page_type == self.PAGE_FREE): 119 | if (current_page.is_highlighted): 120 | ch += '\x1b[33;m' 121 | ch += '░' 122 | if (current_page.is_highlighted): 123 | ch += '\x1b[m' 124 | elif (current_page.page_type == self.PAGE_INIT): 125 | ch += '◙' 126 | elif (current_page.page_type == self.PAGE_SMALL): 127 | if (current_page.is_highlighted): 128 | ch += '\x1b[33;m' 129 | else: 130 | ch += '\x1b[37;m' 131 | ch += '◘' 132 | ch += '\x1b[m' 133 | elif (current_page.page_type == self.PAGE_LARGE): 134 | if (current_page.is_highlighted): 135 | ch += '\x1b[93;1m' 136 | else: 137 | ch += '\x1b[37;1m' 138 | ch += '◘' 139 | ch += '\x1b[m' 140 | s += ch 141 | s += '\n' 142 | self._tui_window.write(s) 143 | 144 | def _is_program_running(self): 145 | return len(gdb.selected_inferior().threads()) and gdb.selected_inferior().threads()[0].is_valid() 146 | 147 | def close(self): 148 | gdb.events.before_prompt.disconnect(self._before_prompt_listener) 149 | 150 | 151 | gdb.register_window_type('php_memory', PhpMemoryWindow) 152 | 153 | gdb.execute('tui new-layout php_memory php_memory 1 cmd 1 status 0') 154 | --------------------------------------------------------------------------------