All of lore.kernel.org
 help / color / mirror / Atom feed
From: Ye Liu <ye.liu@linux.dev>
To: Andrew Morton <akpm@linux-foundation.org>
Cc: SeongJae Park <sj@kernel.org>,
	 Stephen Brennan <stephen.s.brennan@oracle.com>,
	linux-mm@kvack.org,  linux-kernel@vger.kernel.org,
	Ye Liu <liuye@kylinos.cn>
Subject: [PATCH] tools/mm: rewrite show_page_info.py with modular design and new features
Date: Mon, 19 Oct 2026 11:10:50 +0800	[thread overview]
Message-ID: <20261019-show_page_info_refactor-v1-1-42518e2d7de7@linux.dev> (raw)

From: Ye Liu <liuye@kylinos.cn>

Rewrite show_page_info.py from a monolithic function into a modular
field-collector architecture. Fix several bugs in memcg/slab/compound
page field access, and add rmap, PTE walk, NUMA node/zone, page type,
and other debugging information.

Key changes:

* Modularize show_page_state() (~130 lines) into small field-collector
  functions (_base_fields, _page_fields, _slab_fields, _memcg_fields,
  _rmap_fields, _pte_fields, etc.), making the main function a thin
  orchestrator

* Add four input modes: --pfn (by page frame number), --page (by struct
  page address), --user PID VADDR (by process + virtual address), and
  --selftest (built-in self-test), replacing the original positional
  PID+VADDR-only arguments

* Add input validation for PFN range and page address alignment

* Fix memcg info retrieval for slab pages:
  - Iterate all slab objects' slabobj_ext entries instead of only
    reading the first one
  - Handle codetag-enabled stride (CONFIG_MEM_ALLOC_PROFILING doubles
    slabobj_ext entry spacing)
  - Check memcg_data == 0 early to avoid NULL pointer dereference on
    uncharged pages
  - Display all distinct memcgs when a slab page has objects charged
    to different cgroups

* Fix slab page field access: use slab struct fields (slab_cache,
  objects, inuse, frozen) instead of reading garbage from overlaid
  page mapping/refcount/mapcount

* Fix compound page field access: resolve to head page via
  compound_head() before reading mapping/memcg_data/refcount, so
  tail pages no longer produce garbage data

* Fix vma_name() crash on unnamed VMAs (returns None, not bytes)

* Add new debugging information:
  - NUMA Node and Zone (PFN range lookup)
  - Page Type (dynamically discovered from enum pagetype)
  - Page Status (Free / In use / In use (slab) / In use (unmapped))
  - LRU type (Active/Inactive Anon/File/Unevictable)
  - Swap cache detection and swap entry value
  - Filesystem type (i_sb->s_id) and file path with inode number
  - Folio Index and Folio Page Index (position within compound page)
  - Rmap (reverse mapping): walk anon_vma or address_space->i_mmap
    interval tree to find all processes mapping a page, with VMA and
    PTE details for the first 2 mappings
  - PTE page table walk (x86_64 4-level paging, PGD->PUD->PMD->PTE,
    with huge page detection and flag decoding; arch-guarded)
  - VMA flags dynamically discovered from kernel enum (VMA_*_BIT)
  - VMA name display with None-safe handling

* Replace prose print lines with structured field output; add
  multi-line field support for memcg/rmap entries with alignment

* Change license from LGPL-2.1 to GPL-2.0-only for kernel tree

Signed-off-by: Ye Liu <liuye@kylinos.cn>
---
This patch is a complete rewrite of the show_page_info.py drgn script.

It supersedes and abandons the previous patch series:

  https://lore.kernel.org/all/20261014-show_page_info-v1-0-7a68aee0114a@linux.dev/

The previous series split changes into 3 separate patches. This new
patch contains all modifications in a single comprehensive rewrite
that addresses all review comments and adds new debugging features.

Please drop the previous series.

Key changes:

* Modularize show_page_state() into small field-collector functions

* Add four input modes: --pfn, --page, --user PID VADDR, --selftest

* Fix memcg retrieval: iterate all slab objects, handle codetag stride,
  early NULL check, display all distinct memcgs

* Fix slab page fields: use slab struct fields instead of overlaid
  page mapping/refcount garbage

* Fix compound page: resolve head page via compound_head() before
  reading fields

* Fix vma_name() crash on unnamed VMAs

* Add: NUMA Node/Zone, Page Type, Page Status, LRU type, Swap cache,
  Filesystem type, File path+inode, Rmap, PTE page table walk (x86_64),
  VMA flags dynamic discovery, Folio Index

* Replace prose print lines with structured field output

* Change license to GPL-2.0-only for kernel tree

Tested with --selftest mode on live systems and kdump vmcores.

./tools/mm/show_page_info.py --selftest
Scanning pages...

============================================================
Test 1: Single Anonymous page
PFN: 0x12ab57
============================================================
RAW: 0017ffffc0020028 fffff389c5605e48 fffff389c5633788 ffff8edb2e0326c1
RAW: 00000007ff5d4001 0000000000000000 0000000100000000 ffff8edb2caaa8c0
Page Address:    0xfffff389c4aad5c0
Page Flags:      PG_uptodate|PG_lru|PG_swapbacked|PG_movable_ops|PG_reported
Page Size:       4096
Page PFN:        0x12ab57
Page Physical:   0x12ab57000
Page Virtual:    0xffff8edb2ab57000
Page Node:       0
Page Zone:       Normal
Page Status:     In use
Page Mapping:    0xffff8edb2e0326c1
Page Refcount:   1
Page Mapcount:   0
LRU:             Inactive Anon
Page Anon/File:  Anon
Folio Index:     0x7ff5d4001
Page Memcg Data: 0xffff8edb2caaa8c0
Memcg Name:      session-c1.scope
Memcg Path:      /sys/fs/cgroup/memory/user.slice/user-991.slice/session-c1.scope
Rmap Count:      1
Rmap:            3369(ukui-settings-d) 0x7ff5d4000000-0x7ff5d4026000
VMA:             0xffff8edb37edf6c0
VMA Start:       0x7ff5d4000000
VMA End:         0x7ff5d4026000
VMA Flags:       0x8100073 (READ|WRITE|MAYREAD|MAYWRITE|MAYEXEC|ACCOUNT|SOFTDIRTY)
VMA Name:        [unnamed]
PGD:             0x105b597f8 => 0x129e07067
PUD:             0x129e07eb8 => 0x128e77067
PMD:             0x128e77500 => 0x12e7cf067
PTE:             0x12e7cf008 => 0x840000012ab57825 (PRESENT|USER|ACCESSED|NX)

============================================================
Test 2: Single File-backed page
PFN: 0x12ab56
============================================================
RAW: 0017ffffc002003c fffff389c4aeda48 fffff389c4b10648 ffff8edb040f76c0
RAW: 00000000000000b2 0000000000000000 0000000200000000 ffff8edb29c460c0
Page Address:        0xfffff389c4aad580
Page Flags:          PG_referenced|PG_uptodate|PG_dirty|PG_lru|PG_swapbacked|PG_savepinned|PG_movable_ops|PG_reported
Page Size:           4096
Page PFN:            0x12ab56
Page Physical:       0x12ab56000
Page Virtual:        0xffff8edb2ab56000
Page Node:           0
Page Zone:           Normal
Page Status:         In use
Page Mapping:        0xffff8edb040f76c0
Page Refcount:       2
Page Mapcount:       0
LRU:                 Inactive Anon
Page Anon/File:      File
Filesystem:          tmpfs
File Path:           log/journal/8c6f15bb5dcc45c8a770b5d79c6335c1/system.journal (inode 68)
Folio Index:         0xb2
Page Memcg Data:     0xffff8edb29c460c0
Memcg Name:          system.slice
Memcg Path:          /sys/fs/cgroup/memory/system.slice
Rmap Count:          2
Rmap:                743(systemd-journal) 0x7f8d00bc6000-0x7f8d013c6000
                     2228(rsyslogd) 0x7f783b120000-0x7f783b920000
Rmap 743 VMA:        0xffff8edb2c8a9000
Rmap 743 VMA Start:  0x7f8d00bc6000
Rmap 743 VMA End:    0x7f8d013c6000
Rmap 743 VMA Flags:  0x80000fb (READ|WRITE|SHARED|MAYREAD|MAYWRITE|MAYEXEC|MAYSHARE|SOFTDIRTY)
Rmap 743 VMA Name:   /run/log/journal/8c6f15bb5dcc45c8a770b5d79c6335c1/system.journal
Rmap 743 PGD:        0x127c7b7f8 => 0x12c91c067
Rmap 743 PUD:        0x12c91c1a0 => 0x12c887067
Rmap 743 PMD:        0x12c887030 => 0x124d0b067
Rmap 743 PTE:        0x124d0b3c0 => 0x800000012ab56867 (PRESENT|RW|USER|ACCESSED|DIRTY|NX)
Rmap 2228 VMA:       0xffff8edb37f9f000
Rmap 2228 VMA Start: 0x7f783b120000
Rmap 2228 VMA End:   0x7f783b920000
Rmap 2228 VMA Flags: 0x80000d1 (READ|MAYREAD|MAYEXEC|MAYSHARE|SOFTDIRTY)
Rmap 2228 VMA Name:  /run/log/journal/8c6f15bb5dcc45c8a770b5d79c6335c1/system.journal
Rmap 2228 PGD:       0x12dc037f0 => 0x1064d1067
Rmap 2228 PUD:       0x1064d1f00 => 0x124c8e067
Rmap 2228 PMD:       0x124c8eec0 => 0x129df1067
Rmap 2228 PTE:       not present

============================================================
Test 3: Compound Anonymous page
PFN: 0x131c00
============================================================
RAW: 0017ffffc0020868 fffff389c5b80008 fffff389c5b88008 ffff8edb2c9f2601
RAW: 00000007f2c4b000 0000000000000000 00000001ffffffff ffff8edb2e3a5140
Page Address:     0xfffff389c4c70000
Page Flags:       PG_uptodate|PG_lru|PG_head|PG_owner_2|PG_swapbacked|PG_anon_exclusive|PG_mappedtodisk|PG_movable_ops|PG_reported
Page Size:        4096
Page PFN:         0x131c00
Page Physical:    0x131c00000
Page Virtual:     0xffff8edb31c00000
Page Node:        0
Page Zone:        Normal
Page Status:      In use
Page Mapping:     0xffff8edb2c9f2601
Page Refcount:    1
Page Mapcount:    -1
LRU:              Inactive Anon
Page Anon/File:   Anon
Folio Index:      0x7f2c4b000
Compound Order:   9
Folio Page Index: 0/512
Page Memcg Data:  0xffff8edb2e3a5140
Memcg Name:       lightdm.service
Memcg Path:       /sys/fs/cgroup/memory/system.slice/lightdm.service
Rmap Count:       1
Rmap:             2570(Xorg) 0x7f2c4ac42000-0x7f2c4b81d000
VMA:              0xffff8edb06633c00
VMA Start:        0x7f2c4ac42000
VMA End:          0x7f2c4b81d000
VMA Flags:        0x8100073 (READ|WRITE|MAYREAD|MAYWRITE|MAYEXEC|ACCOUNT|SOFTDIRTY)
VMA Name:         [unnamed]
PGD:              0x137ee27f0 => 0x1055c5067
PUD:              0x1055c5588 => 0x105571067
PMD:              0x1055712c0 => 0x8000000131c008e7 (PRESENT|RW|USER|ACCESSED|DIRTY|PS|NX)

============================================================
Test 4: Compound File-backed page
PFN: 0x12a800
============================================================
RAW: 0017ffffc000016c fffff389c516e8c8 fffff389c62a0008 ffff8edb2bfabac8
RAW: 0000000000000400 0000000000000000 0000020dffffffff ffff8edb104d0080
Page Address:        0xfffff389c4aa0000
Page Flags:          PG_referenced|PG_uptodate|PG_lru|PG_head|PG_active|PG_movable_ops|PG_reported|PG_has_hwpoisoned
Page Size:           4096
Page PFN:            0x12a800
Page Physical:       0x12a800000
Page Virtual:        0xffff8edb2a800000
Page Node:           0
Page Zone:           Normal
Page Status:         In use
Page Mapping:        0xffff8edb2bfabac8
Page Refcount:       525
Page Mapcount:       -1
LRU:                 Active File
Page Anon/File:      File
Filesystem:          dm-0
File Path:           usr/lib/locale/locale-archive (inode 114085310)
Folio Index:         0x400
Compound Order:      9
Folio Page Index:    0/512
Page Memcg Data:     0xffff8edb104d0080
Memcg Name:          kysdk-conf2.service
Memcg Path:          /sys/fs/cgroup/memory/system.slice/kysdk-conf2.service
Rmap Count:          29
Rmap:                1236(conf2-server.py) 0x7f0561800000-0x7f0562a42000
                     1224(accounts-daemon) 0x7f2f74c00000-0x7f2f75e42000
                     1252(upowerd) 0x7f6a18e00000-0x7f6a1a042000
                     1251(ukui-input-gath) 0x7fe811600000-0x7fe812842000
                     1429(firewalld) 0x7f0df1400000-0x7f0df2642000
                     1463(NetworkManager) 0x7f04cb600000-0x7f04cc842000
                     1634(tuned) 0x7ff9a3a00000-0x7ff9a4c42000
                     1666(gssproxy) 0x7fda89800000-0x7fda8aa42000
                     1707(crond) 0x7f9535200000-0x7f9536442000
                     3208(ukui-session) 0x7f5261200000-0x7f5262442000
                     3340(xdg-document-po) 0x7fa39dc00000-0x7fa39ee42000
                     3360(xdg-permission-) 0x7f8bc2a00000-0x7f8bc3c42000
                     3361(kglobalaccel5) 0x7f1244e00000-0x7f1246042000
                     3369(ukui-settings-d) 0x7ff5ea000000-0x7ff5eb242000
                     3366(ukui-screensave) 0x7f7d1ac00000-0x7f7d1be42000
                     3355(kwin_x11) 0x7f2c45400000-0x7f2c46642000
                     3434(ukui-screensave) 0x7f533e400000-0x7f533f642000
                     3721(pulseaudio) 0x7f3081600000-0x7f3082842000
                     3891(gvfsd) 0x7f60bd600000-0x7f60be842000
                     3936(sshd) 0x7faca3600000-0x7faca4842000
                     3969(sshd) 0x7faca3600000-0x7faca4842000
                     3974(bash) 0x7f0856800000-0x7f0857a42000
                     4057(sshd) 0x7fbde1c00000-0x7fbde2e42000
                     4069(sshd) 0x7fbde1c00000-0x7fbde2e42000
                     4075(bash) 0x7f7f29400000-0x7f7f2a642000
                     5981(sshd) 0x7fb00ea00000-0x7fb00fc42000
                     5992(sshd) 0x7fb00ea00000-0x7fb00fc42000
                     5997(bash) 0x7ff2b2c00000-0x7ff2b3e42000
                     20781(drgn) 0x7f5801600000-0x7f5802842000
Rmap 1236 VMA:       0xffff8edb2a638240
Rmap 1236 VMA Start: 0x7f0561800000
Rmap 1236 VMA End:   0x7f0562a42000
Rmap 1236 VMA Flags: 0x8000071 (READ|MAYREAD|MAYWRITE|MAYEXEC|SOFTDIRTY)
Rmap 1236 VMA Name:  /usr/lib/locale/locale-archive
Rmap 1236 PGD:       0x12c90e7f0 => 0x123cdf067
Rmap 1236 PUD:       0x123cdf0a8 => 0x105d6a067
Rmap 1236 PMD:       0x105d6a870 => 0x800000012a8000a5 (PRESENT|USER|ACCESSED|PS|NX)
Rmap 1224 VMA:       0xffff8edb109b56c0
Rmap 1224 VMA Start: 0x7f2f74c00000
Rmap 1224 VMA End:   0x7f2f75e42000
Rmap 1224 VMA Flags: 0x8000071 (READ|MAYREAD|MAYWRITE|MAYEXEC|SOFTDIRTY)
Rmap 1224 VMA Name:  /usr/lib/locale/locale-archive
Rmap 1224 PGD:       0x12e3747f0 => 0x12e653067
Rmap 1224 PUD:       0x12e6535e8 => 0x12eeae067
Rmap 1224 PMD:       0x12eeaed40 => 0x800000012a8000a5 (PRESENT|USER|ACCESSED|PS|NX)

============================================================
Test 5: Slab page with multiple memcgs
PFN: 0x1000e8
============================================================
RAW: 0017ffffc0000040 ffff8edb00046500 dead000000000100 dead000000000122
RAW: 0000000000000000 0000000200120012 00000000f5000000 ffff8edb000ef981
Page Address:     0xfffff389c4003a00
Page Flags:       PG_head
Page Size:        4096
Page PFN:         0x1000e8
Page Physical:    0x1000e8000
Page Virtual:     0xffff8edb000e8000
Page Node:        0
Page Zone:        Normal
Page Status:      In use (slab)
Slab Cache:       mm_struct
Slab Objects:     18
Slab Inuse:       18
Slab Frozen:      0
Compound Order:   3
Folio Page Index: 0/8
Page Memcg Data:  0xffff8edb000ef981
Memcg Name:       systemd-logind.service
                  sshd.service
                  user@991.service
                  systemd-tmpfiles-clean.service
                  user@0.service
                  systemd-udevd.service
                  (6 total)
Memcg Path:       /sys/fs/cgroup/memory/system.slice/systemd-logind.service
                  /sys/fs/cgroup/memory/system.slice/sshd.service
                  /sys/fs/cgroup/memory/user.slice/user-991.slice/user@991.service
                  /sys/fs/cgroup/memory/system.slice/systemd-tmpfiles-clean.service
                  /sys/fs/cgroup/memory/user.slice/user-0.slice/user@0.service
                  /sys/fs/cgroup/memory/system.slice/systemd-udevd.service

============================================================
Test 6: Slab page with single memcg
PFN: 0x100334
============================================================
RAW: 0017ffffc0000040 ffff8edb00328000 dead000000000122 0000000000000000
RAW: 0000000000000000 0000000200170017 00000000f5000000 ffff8edb2ca55c01
Page Address:     0xfffff389c400cd00
Page Flags:       PG_head
Page Size:        4096
Page PFN:         0x100334
Page Physical:    0x100334000
Page Virtual:     0xffff8edb00334000
Page Node:        0
Page Zone:        Normal
Page Status:      In use (slab)
Slab Cache:       shmem_inode_cache
Slab Objects:     23
Slab Inuse:       23
Slab Frozen:      0
Compound Order:   2
Folio Page Index: 0/4
Page Memcg Data:  0xffff8edb2ca55c01
Memcg Name:       systemd-udevd.service
Memcg Path:       /sys/fs/cgroup/memory/system.slice/systemd-udevd.service

============================================================
Test 7: Slab page without objcg (codetag only)
PFN: 0x100049
============================================================
RAW: 0017ffffc0000000 ffff8edb00042300 dead000000000100 dead000000000122
RAW: 0000000000000000 0000000000150015 00000000f5000000 ffff8edb0004a001
Page Address:    0xfffff389c4001240
Page Flags:      0
Page Size:       4096
Page PFN:        0x100049
Page Physical:   0x100049000
Page Virtual:    0xffff8edb00049000
Page Node:       0
Page Zone:       Normal
Page Status:     In use (slab)
Slab Cache:      kmalloc-192
Slab Objects:    21
Slab Inuse:      21
Slab Frozen:     0
Page Memcg Data: 0xffff8edb0004a001
Memcg Name:      none
Memcg Path:      /sys/fs/cgroup/memory/

============================================================
Test 8: Uncharged page (memcg_data == 0)
PFN: 0x0
============================================================
RAW: 0007ffffc0002000 fffff389c0000008 fffff389c0000008 0000000000000000
RAW: 0000000000000000 0000000000000000 00000001ffffffff 0000000000000000
Page Address:    0xfffff389c0000000
Page Flags:      PG_reserved
Page Size:       4096
Page PFN:        0x0
Page Physical:   0x0
Page Virtual:    0xffff8eda00000000
Page Zone:       Unknown
Page Status:     In use (unmapped)
Page Mapping:    0x0
Page Anon/File:  Unmapped
Page Refcount:   1
Page Mapcount:   -1
Page Memcg Data: 0x0
Memcg Name:      none
Memcg Path:      /sys/fs/cgroup/memory/

============================================================
Test 9: Invalid PFN (out of range)
============================================================
Correctly rejected: PFN 0x480001 out of range (valid: 0 - 0x47ffff)

============================================================
Test 10: Invalid page address (unaligned)
============================================================
Correctly rejected: Page address 0xfffff389c0000001 not aligned to struct page size (64), nearest valid: 0xfffff389c0000000

============================================================
All 10 tests completed!
============================================================
---
 tools/mm/show_page_info.py | 908 +++++++++++++++++++++++++++++++++++++++------
 1 file changed, 799 insertions(+), 109 deletions(-)

diff --git a/tools/mm/show_page_info.py b/tools/mm/show_page_info.py
index c46d8ea283d7..eea1f8b8fa9f 100644
--- a/tools/mm/show_page_info.py
+++ b/tools/mm/show_page_info.py
@@ -4,166 +4,856 @@
 
 import argparse
 import sys
-from drgn import Object, FaultError, PlatformFlags, cast
-from drgn.helpers.linux import find_task, follow_page, page_size
+from drgn import Object, FaultError, PlatformFlags, Architecture, cast
+from drgn.helpers.linux import find_task, follow_page
 from drgn.helpers.linux.mm import (
-    decode_page_flags, page_to_pfn, page_to_phys, page_to_virt, vma_find,
-    PageSlab, PageCompound, PageHead, PageTail, compound_head, compound_order, compound_nr
+    decode_page_flags, page_to_pfn, page_to_phys, page_to_virt,
+    pfn_to_page, phys_to_virt, virt_to_phys,
+    vma_find, vma_name,
+    PageSlab, PageCompound, compound_head, compound_order, compound_nr,
+    PageLRU, PageActive, PageUnevictable, PageSwapBacked,
 )
+from drgn.helpers.linux.rbtree import rbtree_inorder_for_each_entry
 from drgn.helpers.linux.cgroup import cgroup_name, cgroup_path
+from drgn.helpers.linux.fs import inode_path
+from drgn.helpers.common.format import decode_flags
 
 DESC = """
 This is a drgn script to show the page state.
 For more info on drgn, visit https://github.com/osandov/drgn.
 """
 
-def format_page_data(page):
-    """
-    Format raw page data into a readable hex dump with "RAW:" prefix.
 
-    :param page: drgn.Object instance representing the page.
-    :return: Formatted string of memory contents.
-    """
+# ---------------------------------------------------------------------------
+# Utility functions
+# ---------------------------------------------------------------------------
+
+def format_page_data(page):
+    """Format raw page struct data as hex dump with 'RAW:' prefix."""
     try:
         address = page.value_()
-        size = prog.type("struct page").size
+        word_size = 8 if prog.platform.flags & PlatformFlags.IS_64_BIT else 4
+        num_words = prog.type("struct page").size // word_size
+        words = [f"{prog.read_word(address + i * word_size):0{word_size * 2}x}"
+                 for i in range(num_words)]
+        return "\n".join(f"RAW: {' '.join(words[i:i+4])}"
+                         for i in range(0, len(words), 4))
+    except FaultError as e:
+        return f"Error reading memory: {e}"
 
-        if prog.platform.flags & PlatformFlags.IS_64_BIT:
-            word_size = 8
-        else:
-            word_size = 4
-        num_words = size // word_size
 
-        values = []
-        for i in range(num_words):
-            word_address = address + i * word_size
-            word = prog.read_word(word_address)
-            values.append(f"{word:0{word_size * 2}x}")
+def _print_fields(fields):
+    """Print key-value fields with aligned colons."""
+    max_len = max(len(k) for k in fields)
+    indent = ' ' * (max_len + 2)
+    for key, value in fields.items():
+        lines = str(value).split('\n')
+        print(f"{key}:".ljust(max_len + 2) + lines[0])
+        for line in lines[1:]:
+            print(indent + line)
 
-        lines = [f"RAW: {' '.join(values[i:i + 4])}" for i in range(0, len(values), 4)]
 
-        return "\n".join(lines)
+# ---------------------------------------------------------------------------
+# Memory cgroup info
+# ---------------------------------------------------------------------------
+
+def _memcg_name_path(memcg):
+    """Get (name, path) from a mem_cgroup object."""
+    cgrp = memcg.css.cgroup
+    return (cgroup_name(cgrp).decode(),
+            f"/sys/fs/cgroup/memory{cgroup_path(cgrp).decode()}")
+
+
+def _slab_obj_ext_stride(slab):
+    """Determine stride between slabobj_ext entries for a slab."""
+    base = prog.type("struct slabobj_ext").size
+    try:
+        if slab.obj_exts_in_object.value_():
+            return slab.slab_cache.size.value_()
+    except AttributeError:
+        pass
+    if _slab_obj_ext_has_codetag():
+        base *= 2
+    return base
+
+
+_codetag_enabled = None
+
+def _slab_obj_ext_has_codetag():
+    """Check if slab obj_exts include codetag entries (doubles stride)."""
+    global _codetag_enabled
+    if _codetag_enabled is not None:
+        return _codetag_enabled
+    try:
+        key = prog["slab_obj_ext_has_codetag_key"]
+        try:
+            enabled = key.key.enabled.counter.value_()
+        except AttributeError:
+            enabled = key.enabled.counter.value_()
+        _codetag_enabled = enabled > 0
+    except (KeyError, AttributeError):
+        _codetag_enabled = False
+    return _codetag_enabled
+
+
+def _collect_memcg_ptrs(base_addr, num_objects, stride):
+    """Collect distinct memcg pointers from a slabobj_ext array."""
+    ptrs = set()
+    for i in range(num_objects):
+        entry = Object(prog, "struct slabobj_ext *", base_addr + i * stride)
+        try:
+            objcg = entry._objcg
+        except AttributeError:
+            objcg = entry.objcg
+        try:
+            val = objcg.memcg.value_()
+            if val:
+                ptrs.add(val)
+        except FaultError:
+            continue
+    return ptrs
+
+
+def _slab_memcg_info(page, mask):
+    """Retrieve memcg info for a slab page by iterating all objects."""
+    base_addr = page.memcg_data.read_() & ~mask
+    if base_addr == 0:
+        return "unknown", "obj_exts allocation failed"
+
+    slab = cast("struct slab *", page)
+    try:
+        if not slab.obj_exts_needs_objcg.value_():
+            return "none", "/sys/fs/cgroup/memory/"
+    except AttributeError:
+        pass
+    try:
+        num_objects = slab.objects.value_()
+    except (AttributeError, FaultError):
+        num_objects = 1
+
+    stride = _slab_obj_ext_stride(slab)
+
+    memcg_ptrs = _collect_memcg_ptrs(base_addr, num_objects, stride)
+
+    if not memcg_ptrs:
+        return "none", "/sys/fs/cgroup/memory/"
+
+    # Resolve names
+    resolved = {}
+    for addr in memcg_ptrs:
+        try:
+            resolved[addr] = _memcg_name_path(
+                Object(prog, "struct mem_cgroup *", addr))
+        except FaultError:
+            continue
+
+    if not resolved:
+        return "unknown", f"{len(memcg_ptrs)} memcg(s), cgroup unreadable"
+    names = [v[0] for v in resolved.values()]
+    paths = [v[1] for v in resolved.values()]
+    if len(resolved) == 1:
+        return names[0], paths[0]
+    nl = '\n'
+    return f"{nl.join(names)}\n({len(resolved)} total)", nl.join(paths)
 
-    except FaultError as e:
-        return f"Error reading memory: {e}"
-    except Exception as e:
-        return f"Unexpected error: {e}"
 
 def get_memcg_info(page):
     """Retrieve memory cgroup information for a page."""
     try:
-        MEMCG_DATA_OBJEXTS = prog.constant("MEMCG_DATA_OBJEXTS").value_()
-        MEMCG_DATA_KMEM = prog.constant("MEMCG_DATA_KMEM").value_()
         mask = prog.constant('__NR_MEMCG_DATA_FLAGS').value_() - 1
         memcg_data = page.memcg_data.read_()
-        if memcg_data & MEMCG_DATA_OBJEXTS:
-            slabobj_ext = cast("struct slabobj_ext *", memcg_data & ~mask)
-            memcg = slabobj_ext.objcg.memcg.value_()
-        elif memcg_data & MEMCG_DATA_KMEM:
-            objcg = cast("struct obj_cgroup *", memcg_data & ~mask)
-            memcg = objcg.memcg.value_()
-        else:
-            memcg = cast("struct mem_cgroup *", memcg_data & ~mask)
 
+        if memcg_data == 0:
+            return "none", "/sys/fs/cgroup/memory/"
+
+        if memcg_data & prog.constant("MEMCG_DATA_OBJEXTS").value_():
+            return _slab_memcg_info(page, mask)
+
+        objcg_addr = memcg_data & ~mask
+        if objcg_addr == 0:
+            return "none", "/sys/fs/cgroup/memory/"
+        memcg = Object(prog, "struct obj_cgroup *", objcg_addr).memcg
         if memcg.value_() == 0:
             return "none", "/sys/fs/cgroup/memory/"
-        cgrp = memcg.css.cgroup
-        return cgroup_name(cgrp).decode(), f"/sys/fs/cgroup/memory{cgroup_path(cgrp).decode()}"
+        return _memcg_name_path(memcg)
     except FaultError as e:
         return "unknown", f"Error retrieving memcg info: {e}"
     except Exception as e:
         return "unknown", f"Unexpected error: {e}"
 
-def show_page_state(page, addr, mm, pid, task):
-    """Display detailed information about a page."""
+
+# ---------------------------------------------------------------------------
+# Reverse mapping (rmap)
+# ---------------------------------------------------------------------------
+
+def _vma_owner(vma, folio_index, page_size):
+    """Extract (pid, comm, vm_start, vm_end, mm_ptr, vaddr) from a VMA."""
+    mm_ptr = vma.vm_mm
+    owner = mm_ptr.read_().owner.read_()
+    vaddr = vma.vm_start.value_() + \
+        (folio_index - vma.vm_pgoff.value_()) * page_size
+    return (owner.pid.value_(), owner.comm.string_().decode(),
+            vma.vm_start.value_(), vma.vm_end.value_(), mm_ptr, vaddr)
+
+
+def get_rmap_info(page):
+    """Find processes that map this page via reverse mapping."""
+    results = []
+    head = compound_head(page) if PageCompound(page) else page
+    mapping = head.mapping.value_()
+    if mapping == 0:
+        return results
+
+    page_size = prog["PAGE_SIZE"].value_()
+    try:
+        folio_index = head.__folio_index.value_()
+    except (AttributeError, FaultError):
+        folio_index = 0
+
+    try:
+        if mapping & 0x1:
+            # Anonymous page: walk anon_vma interval tree.
+            # Note: this returns all VMAs sharing the anon_vma, which may
+            # include VMAs that don't actually map this page. The PTE walk
+            # distinguishes actual mappings (shows "not present" otherwise).
+            anon_vma = Object(prog, "struct anon_vma *", mapping & ~3)
+            for avc in rbtree_inorder_for_each_entry(
+                "struct anon_vma_chain", anon_vma.rb_root.rb_root, "rb"):
+                try:
+                    results.append(
+                        _vma_owner(avc.vma.read_(), folio_index, page_size))
+                except FaultError:
+                    continue
+        else:
+            addr_space = Object(prog, "struct address_space *", mapping & ~3)
+            for vma in rbtree_inorder_for_each_entry(
+                "struct vm_area_struct", addr_space.i_mmap.rb_root,
+                "shared.rb"):
+                try:
+                    vma_pgoff = vma.vm_pgoff.value_()
+                    vma_pages = (vma.vm_end.value_() -
+                                 vma.vm_start.value_()) >> 12
+                    if vma_pgoff <= folio_index < vma_pgoff + vma_pages:
+                        results.append(
+                            _vma_owner(vma, folio_index, page_size))
+                except FaultError:
+                    continue
+    except (FaultError, AttributeError):
+        pass
+
+    return results
+
+
+# ---------------------------------------------------------------------------
+# VMA flags
+# ---------------------------------------------------------------------------
+
+def format_vma_flags(flags):
+    """Format VMA flags into a human-readable string."""
+    try:
+        enum_type = prog.constant("VMA_READ_BIT").type_
+        vm_flags = [(name[4:-4], 1 << value)
+                    for name, value in enum_type.enumerators
+                    if name.startswith("VMA_") and name.endswith("_BIT")]
+        if vm_flags:
+            return decode_flags(flags, vm_flags, bit_numbers=False)
+    except (LookupError, AttributeError):
+        pass
+    return hex(flags)
+
+
+# ---------------------------------------------------------------------------
+# Node / Zone / Page Type lookup
+# ---------------------------------------------------------------------------
+
+def _page_node_zone(page):
+    """Find node ID and zone name via PFN range lookup.
+
+    Uses prog["node_data"] which is x86-specific. Falls back to
+    contig_page_data for UMA (single-node) systems. May return
+    (-1, "Unknown") on architectures where neither symbol exists.
+    """
+    pfn = page_to_pfn(page).value_()
+    max_zones = prog.constant("__MAX_NR_ZONES").value_()
     try:
-        print(f'PID: {pid} Comm: {task.comm.string_().decode()} mm: {hex(mm)}')
+        nr_nodes = prog["nr_node_ids"].value_()
+        node_data = prog["node_data"]
+        for nid in range(nr_nodes):
+            try:
+                pgdat = node_data[nid].read_()
+            except (IndexError, FaultError):
+                continue
+            for zid in range(max_zones):
+                zone = pgdat.node_zones[zid]
+                start = zone.zone_start_pfn.value_()
+                spanned = zone.spanned_pages.value_()
+                if spanned and start <= pfn < start + spanned:
+                    return nid, zone.name.string_().decode()
+    except (KeyError, FaultError):
+        pass
+    # UMA fallback
+    try:
+        pgdat = prog["contig_page_data"].address_of_()
+        for zid in range(max_zones):
+            zone = pgdat.node_zones[zid]
+            start = zone.zone_start_pfn.value_()
+            spanned = zone.spanned_pages.value_()
+            if spanned and start <= pfn < start + spanned:
+                return 0, zone.name.string_().decode()
+    except (KeyError, FaultError):
+        pass
+    return -1, "Unknown"
+
+
+def _page_type(page):
+    """Decode page->page_type field via enum pagetype discovery."""
+    try:
+        pt = page.page_type.value_()
+    except AttributeError:
+        return None
+    try:
+        enum_type = prog.constant("PGTY_buddy").type_
+        for name, value in enum_type.enumerators:
+            if value == pt:
+                return name[5:]  # strip PGTY_ prefix
+    except (LookupError, AttributeError):
+        pass
+    return None
+
+
+# ---------------------------------------------------------------------------
+# PTE walk (x86_64 4-level paging)
+# ---------------------------------------------------------------------------
+
+# x86_64 PTE flag bits — preprocessor macros, not in DWARF
+_PTE_FLAGS = [
+    ("PRESENT",  1 << 0),
+    ("RW",       1 << 1),
+    ("USER",     1 << 2),
+    ("PWT",      1 << 3),
+    ("PCD",      1 << 4),
+    ("ACCESSED", 1 << 5),
+    ("DIRTY",    1 << 6),
+    ("PS",       1 << 7),
+    ("GLOBAL",   1 << 8),
+    ("NX",       1 << 63),
+]
+
+
+def _walk_page_table(mm, vaddr):
+    """Walk 4-level page table. Returns list of (level, entry_pa, entry_val).
+
+    Note: only supports x86_64 4-level paging (48-bit VA). Does not support
+    5-level paging (LA57, 57-bit VA) where an additional P4D level exists.
+    """
+    page_mask = ~(prog["PAGE_SIZE"].value_() - 1)
+    entries = []
+    try:
+        def _read(table_va, idx):
+            return prog.read_word(table_va + idx * 8)
+
+        base_va = mm.pgd.value_()
+
+        # PGD
+        pgd_idx = (vaddr >> 39) & 0x1FF
+        pgd_val = _read(base_va, pgd_idx)
+        pgd_pa = virt_to_phys(prog, base_va).value_() + pgd_idx * 8
+        entries.append(("PGD", pgd_pa, pgd_val))
+        if not (pgd_val & 1):
+            return entries
+
+        # PUD
+        pud_table_pa = pgd_val & page_mask
+        pud_idx = (vaddr >> 30) & 0x1FF
+        pud_val = _read(phys_to_virt(prog, pud_table_pa).value_(), pud_idx)
+        entries.append(("PUD", pud_table_pa + pud_idx * 8, pud_val))
+        if not (pud_val & 1):
+            return entries
+        if pud_val & (1 << 7):
+            return entries  # 1GB huge page
+
+        # PMD
+        pmd_table_pa = pud_val & page_mask
+        pmd_idx = (vaddr >> 21) & 0x1FF
+        pmd_val = _read(phys_to_virt(prog, pmd_table_pa).value_(), pmd_idx)
+        entries.append(("PMD", pmd_table_pa + pmd_idx * 8, pmd_val))
+        if not (pmd_val & 1):
+            return entries
+        if pmd_val & (1 << 7):
+            return entries  # 2MB huge page
+
+        # PTE
+        pte_table_pa = pmd_val & page_mask
+        pte_idx = (vaddr >> 12) & 0x1FF
+        pte_val = _read(phys_to_virt(prog, pte_table_pa).value_(), pte_idx)
+        entries.append(("PTE", pte_table_pa + pte_idx * 8, pte_val))
+    except FaultError:
+        pass
+    return entries
+
+
+def _pte_fields(mm, addr, fields):
+    """Collect page table entries via page table walk."""
+    if prog.platform.arch != Architecture.X86_64:
+        return
+    entries = _walk_page_table(mm, addr)
+    for i, (level, pa, val) in enumerate(entries):
+        if not val:
+            fields[level] = "not present"
+            continue
+        if i == len(entries) - 1:
+            # Final entry (PTE or huge page): show decoded flags
+            flags = '|'.join(n for n, m in _PTE_FLAGS if val & m)
+            fields[level] = f"{hex(pa)} => {hex(val)} ({flags})"
+        else:
+            # Intermediate level: just address => value
+            fields[level] = f"{hex(pa)} => {hex(val)}"
+
+
+# ---------------------------------------------------------------------------
+# Field collectors
+# ---------------------------------------------------------------------------
+
+def _base_fields(page):
+    """Collect basic page fields."""
+    fields = {
+        "Page Address": hex(page.value_()),
+        "Page Flags": decode_page_flags(page),
+        "Page Size": prog["PAGE_SIZE"].value_(),
+        "Page PFN": hex(page_to_pfn(page).value_()),
+        "Page Physical": hex(page_to_phys(page).value_()),
+        "Page Virtual": hex(page_to_virt(page).value_()),
+    }
+    nid, zone = _page_node_zone(page)
+    if nid >= 0:
+        fields["Page Node"] = nid
+    fields["Page Zone"] = zone
+    ptype = _page_type(page)
+    if ptype is not None:
+        fields["Page Type"] = ptype
+    return fields
+
+
+def _page_status(head):
+    """Determine page status string."""
+    if PageSlab(head):
+        return "In use (slab)"
+    if head.mapping.value_() != 0:
+        return "In use"
+    if head._refcount.counter.value_() > 0:
+        return "In use (unmapped)"
+    return "Free"
+
+
+def _slab_fields(head, fields):
+    """Collect slab-specific fields."""
+    try:
+        slab = cast("struct slab *", head)
+        fields["Slab Cache"] = slab.slab_cache.name.string_().decode()
+        fields["Slab Objects"] = slab.objects.value_()
+        fields["Slab Inuse"] = slab.inuse.value_()
+        fields["Slab Frozen"] = slab.frozen.value_()
+    except (AttributeError, FaultError):
+        fields["Slab Cache"] = "Unknown"
+
+
+def _lru_type(head):
+    """Determine LRU list type from page flags."""
+    if PageUnevictable(head):
+        return "Unevictable"
+    if not PageLRU(head):
+        return None
+    active = PageActive(head)
+    anon = PageSwapBacked(head)
+    return f"{'Active' if active else 'Inactive'} {'Anon' if anon else 'File'}"
+
+
+def _swapcache_info(head):
+    """Get swap entry value if page is in swap cache."""
+    # drgn does not provide a PageSwapCache() helper, so we check the
+    # decoded page flags string for PG_swapcache.
+    flags_str = str(decode_page_flags(head))
+    if "PG_swapcache" not in flags_str:
+        return None
+    try:
+        return hex(head.private.value_())
+    except (AttributeError, FaultError):
+        return "Unknown"
+
+
+def _page_fields(head, fields):
+    """Collect non-slab page fields (refcount, mapping, anon/file, etc.)."""
+    mapping = head.mapping.value_()
+    fields["Page Mapping"] = hex(mapping)
+
+    if mapping == 0:
+        fields["Page Anon/File"] = "Unmapped"
+        refcount = head._refcount.counter.value_()
+        if refcount > 0:
+            fields["Page Refcount"] = refcount
+            fields["Page Mapcount"] = head._mapcount.counter.value_()
+        return
+
+    fields["Page Refcount"] = head._refcount.counter.value_()
+    fields["Page Mapcount"] = head._mapcount.counter.value_()
+
+    lru = _lru_type(head)
+    if lru:
+        fields["LRU"] = lru
+
+    swap = _swapcache_info(head)
+    if swap:
+        fields["Swap Entry"] = swap
+
+    if mapping & 0x1:
+        fields["Page Anon/File"] = "Anon"
+    else:
+        fields["Page Anon/File"] = "File"
         try:
-            print(format_page_data(page))
-        except FaultError as e:
-            print(f"Error reading page data: {e}")
-        fields = {
-            "Page Address": hex(page.value_()),
-            "Page Flags": decode_page_flags(page),
-            "Page Size": prog["PAGE_SIZE"].value_(),
-            "Page PFN": hex(page_to_pfn(page).value_()),
-            "Page Physical": hex(page_to_phys(page).value_()),
-            "Page Virtual": hex(page_to_virt(page).value_()),
-            "Page Refcount": page._refcount.counter.value_(),
-            "Page Mapcount": page._mapcount.counter.value_(),
-            "Page Index": hex(page.__folio_index.value_()),
-            "Page Memcg Data": hex(page.memcg_data.value_()),
-        }
-
-        memcg_name, memcg_path = get_memcg_info(page)
-        fields["Memcg Name"] = memcg_name
-        fields["Memcg Path"] = memcg_path
-        fields["Page Mapping"] = hex(page.mapping.value_())
-        fields["Page Anon/File"] = "Anon" if page.mapping.value_() & 0x1 else "File"
+            inode = Object(prog, "struct address_space *",
+                           mapping & ~3).host
+            ino = inode.i_ino.value_()
+            fields["Filesystem"] = inode.i_sb.s_id.string_().decode()
+            path = inode_path(inode)
+            if path:
+                fields["File Path"] = f"{path.decode()} (inode {ino})"
+            else:
+                fields["File Path"] = f"Unknown (inode {ino})"
+        except (FaultError, AttributeError):
+            fields["File Path"] = "Unknown"
+
+    try:
+        fields["Folio Index"] = hex(head.__folio_index.value_())
+    except (AttributeError, FaultError):
+        fields["Folio Index"] = "Unavailable"
+
+
+def _compound_fields(page, head, fields):
+    """Collect compound page fields."""
+    if not PageCompound(page):
+        return
+    fields["Compound Order"] = compound_order(page).value_()
+    offset = page_to_pfn(page).value_() - page_to_pfn(head).value_()
+    fields["Folio Page Index"] = f"{offset}/{compound_nr(page).value_()}"
+    if head.value_() != page.value_():
+        fields["Head Page"] = hex(head.value_())
+
+
+def _memcg_fields(head, fields):
+    """Collect memcg data and resolved name/path."""
+    fields["Page Memcg Data"] = hex(head.memcg_data.value_())
+    name, path = get_memcg_info(head)
+    fields["Memcg Name"] = name
+    fields["Memcg Path"] = path
+
+
+def _rmap_fields(head, fields, show_detail=False):
+    """Collect rmap (reverse mapping) info."""
+    if PageSlab(head) or head.mapping.value_() == 0:
+        return
+    rmap = get_rmap_info(head)
+    fields["Rmap Count"] = len(rmap)
+    if not rmap:
+        fields["Rmap"] = "No mappings found"
+        return
+
+    entries = [f"{pid}({comm}) {hex(s)}-{hex(e)}"
+               for pid, comm, s, e, _, _ in rmap]
+    fields["Rmap"] = '\n'.join(entries)
 
+    # Show VMA and PTE details for first 2 mappings (--pfn/--page mode only)
+    if not show_detail:
+        return
+    detail = min(2, len(rmap))
+    for i in range(detail):
+        pid, comm, s, e, mm, vaddr = rmap[i]
+        prefix = f"Rmap {pid} " if detail > 1 else ""
         try:
-            vma = vma_find(mm, addr)
-            fields["Page VMA"] = hex(vma.value_())
-            fields["VMA Start"] = hex(vma.vm_start.value_())
-            fields["VMA End"] = hex(vma.vm_end.value_())
-        except FaultError as e:
-            fields["Page VMA"] = "Unavailable"
-            fields["VMA Start"] = "Unavailable"
-            fields["VMA End"] = "Unavailable"
-            print(f"Error retrieving VMA information: {e}")
-
-        # Calculate the maximum field name length for alignment
-        max_field_len = max(len(field) for field in fields)
-
-        # Print aligned fields
-        for field, value in fields.items():
-            print(f"{field}:".ljust(max_field_len + 2) + f"{value}")
-
-        # Additional information about the page
-        if PageSlab(page):
-            print("This page belongs to the slab allocator.")
-
-        if PageCompound(page):
-            print("This page is part of a compound page.")
-            if PageHead(page):
-                print("This page is the head page of a compound page.")
-            if PageTail(page):
-                print("This page is the tail page of a compound page.")
-            print(f"{'Head Page:'.ljust(max_field_len + 2)}{hex(compound_head(page).value_())}")
-            print(f"{'Compound Order:'.ljust(max_field_len + 2)}{compound_order(page).value_()}")
-            print(f"{'Number of Pages:'.ljust(max_field_len + 2)}{compound_nr(page).value_()}")
+            vma = vma_find(mm, vaddr)
+            fields[f"{prefix}VMA"] = hex(vma.value_())
+            fields[f"{prefix}VMA Start"] = hex(vma.vm_start.value_())
+            fields[f"{prefix}VMA End"] = hex(vma.vm_end.value_())
+            flags_hex = hex(vma.vm_flags.value_())
+            flags_str = format_vma_flags(vma.vm_flags.value_())
+            fields[f"{prefix}VMA Flags"] = f"{flags_hex} ({flags_str})"
+            name = vma_name(vma)
+            fields[f"{prefix}VMA Name"] = name.decode() if name else "[unnamed]"
+        except (FaultError, AttributeError):
+            fields[f"{prefix}VMA"] = "Unavailable"
+        if prog.platform.arch == Architecture.X86_64:
+            pte_entries = _walk_page_table(mm, vaddr)
+            for j, (level, pa, val) in enumerate(pte_entries):
+                if not val:
+                    fields[f"{prefix}{level}"] = "not present"
+                elif j == len(pte_entries) - 1:
+                    flags = '|'.join(n for n, m in _PTE_FLAGS if val & m)
+                    fields[f"{prefix}{level}"] = f"{hex(pa)} => {hex(val)} ({flags})"
+                else:
+                    fields[f"{prefix}{level}"] = f"{hex(pa)} => {hex(val)}"
+
+
+def _vma_fields(mm, addr, fields):
+    """Collect VMA fields for a user-space address lookup."""
+    try:
+        vma = vma_find(mm, addr)
+        fields["Page VMA"] = hex(vma.value_())
+        fields["VMA Start"] = hex(vma.vm_start.value_())
+        fields["VMA End"] = hex(vma.vm_end.value_())
+        flags_hex = hex(vma.vm_flags.value_())
+        flags_readable = format_vma_flags(vma.vm_flags.value_())
+        fields["VMA Flags"] = f"{flags_hex} ({flags_readable})"
+        name = vma_name(vma)
+        fields["VMA Name"] = name.decode() if name else "[unnamed]"
+    except FaultError as e:
+        for key in ("Page VMA", "VMA Start", "VMA End", "VMA Flags"):
+            fields[key] = "Unavailable"
+        print(f"Error retrieving VMA information: {e}")
+
+
+# ---------------------------------------------------------------------------
+# Main page state display
+# ---------------------------------------------------------------------------
+
+def show_page_state(page, addr=0, mm=None, pid=None, task=None):
+    """Display detailed information about a page."""
+    try:
+        if pid is not None and task is not None:
+            comm = task.comm.string_().decode()
+            print(f"PID: {pid} Comm: {comm} mm: {hex(mm)}")
+        print(format_page_data(page))
+
+        head = compound_head(page) if PageCompound(page) else page
+
+        fields = _base_fields(page)
+        fields["Page Status"] = _page_status(head)
+
+        if PageSlab(head):
+            _slab_fields(head, fields)
         else:
-            print("This page is not part of a compound page.")
+            _page_fields(head, fields)
+
+        _compound_fields(page, head, fields)
+        _memcg_fields(head, fields)
+        _rmap_fields(head, fields, show_detail=(mm is None))
+
+        if mm is not None:
+            _vma_fields(mm, addr, fields)
+            _pte_fields(mm, addr, fields)
+
+        _print_fields(fields)
     except FaultError as e:
         print(f"Error accessing page state: {e}")
     except Exception as e:
         print(f"Unexpected error: {e}")
 
-def main():
-    """Main function to parse arguments and display page state."""
-    parser = argparse.ArgumentParser(description=DESC, formatter_class=argparse.RawTextHelpFormatter)
-    parser.add_argument('pid', metavar='PID', type=int, help='Target process ID (PID)')
-    parser.add_argument('vaddr', metavar='VADDR', type=str, help='Target virtual address in hexadecimal format (e.g., 0x7fff1234abcd)')
-    args = parser.parse_args()
 
+# ---------------------------------------------------------------------------
+# Input validation
+# ---------------------------------------------------------------------------
+
+def _validate_pfn(pfn):
+    """Validate PFN is within the valid range."""
+    max_pfn = prog["max_pfn"].value_()
+    if pfn < 0 or pfn >= max_pfn:
+        sys.exit(f"PFN {hex(pfn)} out of range (valid: 0 - {hex(max_pfn - 1)})")
+    return pfn
+
+
+def _validate_page_addr(addr):
+    """Validate page struct address is within vmemmap range."""
     try:
-        vaddr = int(args.vaddr, 16)
-    except ValueError:
-        sys.exit(f"Error: Invalid virtual address format: {args.vaddr}")
+        base = prog["vmemmap"].value_()
+    except KeyError:
+        sys.exit("Cannot determine vmemmap base address")
+    page_size = prog.type("struct page").size
+    max_pfn = prog["max_pfn"].value_()
+    end = base + max_pfn * page_size
+    if addr < base or addr >= end:
+        sys.exit(f"Page address {hex(addr)} out of vmemmap range "
+                 f"({hex(base)} - {hex(end - 1)})")
+    if (addr - base) % page_size != 0:
+        nearest = base + ((addr - base) // page_size) * page_size
+        sys.exit(f"Page address {hex(addr)} not aligned to struct page size "
+                 f"({page_size}), nearest valid: {hex(nearest)}")
+    return addr
 
+
+# ---------------------------------------------------------------------------
+# Self-test
+# ---------------------------------------------------------------------------
+
+def _count_distinct_memcg(page, stride, mask):
+    """Count distinct memcg pointers in a slab page's obj_exts array."""
+    base = page.memcg_data.value_() & ~mask
+    if base == 0:
+        return 0
     try:
-        task = find_task(args.pid)
-        mm = task.mm
-        page = follow_page(mm, vaddr)
+        n = cast("struct slab *", page).objects.value_()
+    except (AttributeError, FaultError):
+        return 0
+    return len(_collect_memcg_ptrs(base, n, stride))
+
+
+def _scan_page_types():
+    """Scan all pages and categorize by type. Returns {type: page} dict."""
+    from drgn.helpers.linux.mm import for_each_page
+
+    objexts_flag = prog.constant("MEMCG_DATA_OBJEXTS").value_()
+    mask = prog.constant('__NR_MEMCG_DATA_FLAGS').value_() - 1
+    base_stride = prog.type("struct slabobj_ext").size
+    stride = base_stride * 2 if _slab_obj_ext_has_codetag() else base_stride
+
+    found = {}
+    needed = ["anon", "file", "compound_anon", "compound_file",
+              "slab_multi", "slab_single", "slab_no_objcg", "uncharged"]
+
+    for page in for_each_page(prog):
+        try:
+            head = compound_head(page) if PageCompound(page) else page
+
+            if PageSlab(head):
+                if page.memcg_data.read_() & objexts_flag:
+                    try:
+                        slab = cast("struct slab *", head)
+                        if slab.obj_exts_needs_objcg.value_():
+                            n = _count_distinct_memcg(page, stride, mask)
+                            if n >= 2 and "slab_multi" not in found:
+                                found["slab_multi"] = page
+                            elif n == 1 and "slab_single" not in found:
+                                found["slab_single"] = page
+                        elif "slab_no_objcg" not in found:
+                            found["slab_no_objcg"] = page
+                    except (AttributeError, FaultError):
+                        pass
+                continue
+
+            if PageCompound(page):
+                mapping = head.mapping.value_()
+                if mapping & 0x1 and "compound_anon" not in found:
+                    found["compound_anon"] = page
+                elif mapping and not (mapping & 0x1) and "compound_file" not in found:
+                    found["compound_file"] = page
+                continue
 
-        if page:
-            show_page_state(page, vaddr, mm, args.pid, task)
+            if page.memcg_data.read_() == 0:
+                if "uncharged" not in found:
+                    found["uncharged"] = page
+                continue
+
+            mapping = head.mapping.value_()
+            if mapping == 0:
+                continue
+            mapcount = head._mapcount.counter.value_()
+            if mapping & 0x1:
+                if "anon" not in found and mapcount >= 0:
+                    found["anon"] = page
+            else:
+                if "file" not in found and mapcount >= 0:
+                    found["file"] = page
+        except FaultError:
+            continue
+
+        if all(k in found for k in needed):
+            break
+
+    return found
+
+
+def run_self_test():
+    """Scan for various page types and verify show_page_state output."""
+    print("Scanning pages...")
+    found = _scan_page_types()
+
+    labels = [
+        ("anon", "Single Anonymous page"),
+        ("file", "Single File-backed page"),
+        ("compound_anon", "Compound Anonymous page"),
+        ("compound_file", "Compound File-backed page"),
+        ("slab_multi", "Slab page with multiple memcgs"),
+        ("slab_single", "Slab page with single memcg"),
+        ("slab_no_objcg", "Slab page without objcg (codetag only)"),
+        ("uncharged", "Uncharged page (memcg_data == 0)"),
+    ]
+
+    test_num = 1
+    for key, label in labels:
+        print(f"\n{'='*60}")
+        if key in found:
+            print(f"Test {test_num}: {label}")
+            print(f"PFN: {hex(page_to_pfn(found[key]).value_())}")
+            print(f"{'='*60}")
+            show_page_state(found[key])
         else:
-            sys.exit(f"Address {hex(vaddr)} is not mapped.")
+            print(f"Test {test_num}: {label}")
+            print(f"{'='*60}")
+            print(f"[SKIP] No {label} found")
+        test_num += 1
+
+    # Input validation tests
+    print(f"\n{'='*60}")
+    print(f"Test {test_num}: Invalid PFN (out of range)")
+    print(f"{'='*60}")
+    try:
+        _validate_pfn(prog["max_pfn"].value_() + 1)
+    except SystemExit as e:
+        print(f"Correctly rejected: {e}")
+    test_num += 1
+
+    print(f"\n{'='*60}")
+    print(f"Test {test_num}: Invalid page address (unaligned)")
+    print(f"{'='*60}")
+    try:
+        _validate_page_addr(prog["vmemmap"].value_() + 1)
+    except SystemExit as e:
+        print(f"Correctly rejected: {e}")
+
+    print(f"\n{'='*60}")
+    print(f"All {test_num} tests completed!")
+    print(f"{'='*60}")
+
+
+# ---------------------------------------------------------------------------
+# Main entry point
+# ---------------------------------------------------------------------------
+
+def main():
+    """Parse arguments and display page state."""
+    parser = argparse.ArgumentParser(
+        description=DESC, formatter_class=argparse.RawTextHelpFormatter)
+    group = parser.add_mutually_exclusive_group(required=True)
+    group.add_argument('--pfn', metavar='PFN', type=lambda x: int(x, 0),
+                       help='Page Frame Number (decimal or hex, e.g. 0x12345)')
+    group.add_argument('--page', metavar='ADDR', type=lambda x: int(x, 0),
+                       help='Page struct address (decimal or hex, e.g. 0xffffea0001234000)')
+    group.add_argument('--user', nargs=2, metavar=('PID', 'VADDR'),
+                       help='User-space lookup: PID and virtual address')
+    group.add_argument('--selftest', action='store_true',
+                       help='Run self-test: scan for various page types')
+    args = parser.parse_args()
+
+    try:
+        if args.selftest:
+            run_self_test()
+        elif args.pfn is not None:
+            show_page_state(pfn_to_page(prog, _validate_pfn(args.pfn)))
+        elif args.page is not None:
+            show_page_state(Object(prog, "struct page *",
+                                   _validate_page_addr(args.page)))
+        elif args.user is not None:
+            pid = int(args.user[0], 0)
+            vaddr = int(args.user[1], 0)
+            task = find_task(pid)
+            mm = task.mm
+            page = follow_page(mm, vaddr)
+            if page:
+                show_page_state(page, vaddr, mm, pid, task)
+            else:
+                sys.exit(f"Address {hex(vaddr)} is not mapped.")
     except FaultError as e:
         sys.exit(f"Error accessing task or memory: {e}")
     except Exception as e:
         sys.exit(f"Unexpected error: {e}")
 
+
 if __name__ == "__main__":
     main()

---
base-commit: 2efb7a11f1aa4d2dc285fe2680cca22cb9f72f64
base-tree: git://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git master
change-id: 20261019-show_page_info_refactor-9eb172a04afa

Best regards,
--  
Ye Liu <ye.liu@linux.dev>



             reply	other threads:[~2026-10-19  3:11 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-19  3:10 Ye Liu [this message]
2026-10-20  3:11 ` [PATCH] tools/mm: rewrite show_page_info.py with modular design and new features sashiko-bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261019-show_page_info_refactor-v1-1-42518e2d7de7@linux.dev \
    --to=ye.liu@linux.dev \
    --cc=akpm@linux-foundation.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=liuye@kylinos.cn \
    --cc=sj@kernel.org \
    --cc=stephen.s.brennan@oracle.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.