├── .gitignore ├── cmd └── ptexplore │ └── main.go ├── README.md └── ptexplore.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /cmd/ptexplore/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/gianlucaborello/ptexplore" 11 | ) 12 | 13 | func main() { 14 | flag.Usage = usage 15 | 16 | pid := flag.Int("pid", 0, "Pid of the process to analyze (e.g. 42)") 17 | areaFilter := flag.String("areas", "", "Comma separated list of memory areas (even patterns) to analyze (e.g. 'stack,heap,libc')") 18 | addressFilter := flag.String("address", "", "Analyze a single address (e.g. '0x7f66a002ab70')") 19 | quiet := flag.Bool("quiet", false, "Don't print page table details, just a summary of the memory areas") 20 | 21 | flag.Parse() 22 | 23 | if *pid == 0 { 24 | fmt.Fprintln(os.Stderr, "Pid not specified") 25 | os.Exit(1) 26 | } 27 | 28 | var address uint64 29 | var err error 30 | if *addressFilter != "" { 31 | address, err = getHexAddress(*addressFilter) 32 | if err != nil { 33 | fmt.Fprintln(os.Stderr, err) 34 | os.Exit(1) 35 | } 36 | } 37 | 38 | state := &ptexplore.PtExplorerState{} 39 | 40 | err = state.ParseMemAreas(*pid) 41 | if err != nil { 42 | fmt.Fprintln(os.Stderr, err) 43 | os.Exit(1) 44 | } 45 | 46 | err = state.OpenSystemFiles(*pid) 47 | if err != nil { 48 | fmt.Fprintln(os.Stderr, err) 49 | os.Exit(1) 50 | } 51 | 52 | err = state.PrintAreas(*areaFilter, address, *quiet) 53 | if err != nil { 54 | fmt.Fprintln(os.Stderr, err) 55 | os.Exit(1) 56 | } 57 | } 58 | 59 | func usage() { 60 | fmt.Fprintf(os.Stderr, "Explore page table of a process under Linux.\n") 61 | fmt.Fprintf(os.Stderr, "Works by attaching to a process and printing each memory area. Optionally, memory areas can be restricted via a filter.\n") 62 | fmt.Fprintf(os.Stderr, "\n") 63 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 64 | flag.PrintDefaults() 65 | } 66 | 67 | func getHexAddress(hex string) (address uint64, err error) { 68 | if strings.HasPrefix(hex, "0x") { 69 | hex = hex[2:] 70 | } 71 | 72 | address, err = strconv.ParseUint(hex, 16, 64) 73 | if err != nil { 74 | return 0, err 75 | } 76 | 77 | return address, nil 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ptexplore (page table explorer) 2 | 3 | A simple tool to print the page table content for a process in Linux. 4 | 5 | Works by attaching to a process and printing each memory area. Optionally, memory areas can be restricted via a filter. 6 | 7 | ## Install 8 | 9 | ``` 10 | $ go get github.com/gianlucaborello/ptexplore/cmd/ptexplore 11 | ``` 12 | 13 | ## Use cases 14 | * Explore what portion of mapped memory areas of a process are actually present in memory 15 | * Check if a memory address dereference will cause a page fault 16 | * Analyze what portion of mapped memory areas are swapped out 17 | * Look up the physical address corresponding to a virtual address (useful for NUMA) 18 | 19 | ## Usage 20 | 21 | ``` 22 | $ ptexplore --help 23 | Explore page table of a process under Linux. 24 | Works by attaching to a process and printing each memory area. Optionally, memory areas can be restricted via a filter. 25 | 26 | Usage of ptexplore: 27 | -address string 28 | Analyze a single address (e.g. '0x7f66a002ab70') 29 | -areas string 30 | Comma separated list of memory areas (even patterns) to analyze (e.g. 'stack,heap,libc') 31 | -pid int 32 | Pid of the process to analyze (e.g. 42) 33 | -quiet 34 | Don't print page table details, just a summary of the memory areas 35 | ``` 36 | 37 | Print a summary of all the memory areas (similar to `/proc/PID/maps`): 38 | 39 | ``` 40 | $ ptexplore -pid 121361 --quiet 41 | Area '/usr/bin/cadvisor', range 0x0000000000400000 - 0x00000000010a7000 (13 MB) 42 | Area '/usr/bin/cadvisor', range 0x00000000012a6000 - 0x0000000001343000 (643 kB) 43 | Area 'anonymous', range 0x0000000001343000 - 0x0000000001370000 (184 kB) 44 | Area '[heap]', range 0x000000000290c000 - 0x000000000292f000 (143 kB) 45 | Area 'anonymous', range 0x000000c000000000 - 0x000000c000005000 (20 kB) 46 | Area 'anonymous', range 0x000000c41ff68000 - 0x000000c421200000 (20 MB) 47 | Area '/usr/glibc-compat/lib/libc-2.23.so', range 0x00007f669e3bc000 - 0x00007f669e3c0000 (16 kB) 48 | Area '/usr/glibc-compat/lib/libc-2.23.so', range 0x00007f669e3c0000 - 0x00007f669e3c2000 (8.2 kB) 49 | Area '[stack]', range 0x00007fff5e57d000 - 0x00007fff5e59e000 (135 kB) 50 | Area '[vvar]', range 0x00007fff5e5e4000 - 0x00007fff5e5e7000 (12 kB) 51 | Area '[vdso]', range 0x00007fff5e5e7000 - 0x00007fff5e5e9000 (8.2 kB) 52 | ``` 53 | 54 | Print a filtered summary of all the memory areas: 55 | 56 | ``` 57 | $ ptexplore -pid 121361 --areas stack,heap --quiet 58 | Area '[stack]', range 0x00007fff5e57d000 - 0x00007fff5e59e000 (135 kB) 59 | Area '[heap]', range 0x000000000290c000 - 0x000000000292f000 (143 kB) 60 | ``` 61 | 62 | Print a filtered view of the page table by memory area. For every memory areas, present and swapped pages are shown, along with their count, physical address and flags: 63 | 64 | ``` 65 | $ ptexplore -pid 121361 --areas stack,heap 66 | 67 | Area '[stack]', range 0x00007fff5e57d000 - 0x00007fff5e59e000 (135 kB) 68 | 69 | ... 30 non mapped pages ... 70 | 0x00007fff5e59b000: physical address: 0x000000022762a000 exclusive soft-dirty count:1 flags:UPTODATE,LRU,ACTIVE,MMAP,ANON,SWAPBACKED 71 | 0x00007fff5e59c000: physical address: 0x0000000221c39000 exclusive soft-dirty count:1 flags:UPTODATE,LRU,ACTIVE,MMAP,ANON,SWAPBACKED 72 | 0x00007fff5e59d000: physical address: 0x00000001a3f4b000 exclusive soft-dirty count:1 flags:REFERENCED,UPTODATE,LRU,ACTIVE,MMAP,ANON,SWAPBACKED 73 | 74 | Area '[heap]', range 0x000000000290c000 - 0x000000000292f000 (143 kB) 75 | 76 | 0x000000000290c000: physical address: 0x00000002282fe000 exclusive soft-dirty count:1 flags:UPTODATE,LRU,ACTIVE,MMAP,ANON,SWAPBACKED 77 | 0x000000000290d000: physical address: 0x000000022bbae000 exclusive soft-dirty count:1 flags:REFERENCED,UPTODATE,LRU,ACTIVE,MMAP,ANON,SWAPBACKED 78 | 0x000000000290e000: physical address: 0x000000022bbaf000 exclusive soft-dirty count:1 flags:UPTODATE,LRU,ACTIVE,MMAP,ANON,SWAPBACKED 79 | 0x000000000290f000: physical address: 0x0000000199fce000 exclusive soft-dirty count:1 flags:REFERENCED,UPTODATE,LRU,ACTIVE,MMAP,ANON,SWAPBACKED 80 | 0x0000000002910000: physical address: 0x00000001aa074000 exclusive soft-dirty count:1 flags:REFERENCED,UPTODATE,LRU,ACTIVE,MMAP,ANON,SWAPBACKED 81 | 0x0000000002911000: physical address: 0x000000022006e000 exclusive soft-dirty count:1 flags:REFERENCED,UPTODATE,LRU,ACTIVE,MMAP,ANON,SWAPBACKED 82 | ... 29 non mapped pages ... 83 | ``` 84 | 85 | Print the details for a specific page given a virtual memory address: 86 | 87 | ``` 88 | $ ptexplore --pid 121361 --address 0x7f66a002ab70 89 | 90 | Area 'anonymous', range 0x00007f669f82c000 - 0x00007f66a002c000 (8.4 MB) 91 | 92 | 0x00007f66a002a000: physical address: 0x000000019fe8c000 exclusive soft-dirty count:1 flags:UPTODATE,LRU,ACTIVE,MMAP,ANON,SWAPBACKED 93 | ``` 94 | -------------------------------------------------------------------------------- /ptexplore.go: -------------------------------------------------------------------------------- 1 | package ptexplore 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "strconv" 11 | "strings" 12 | 13 | humanize "github.com/dustin/go-humanize" 14 | ) 15 | 16 | const pageMapReadChunk uint64 = 8 17 | 18 | const pfnMask uint64 = 0x7FFFFFFFFFFFFF 19 | const swapTypeMask uint64 = 0x1F 20 | const swapOffsetMask uint64 = 0x7FFFFFFFFFFFE0 21 | 22 | const ( 23 | pageSoftDirty uint64 = 1 << 55 24 | pageExclusive uint64 = 1 << 56 25 | pageFile uint64 = 1 << 61 26 | pageSwapped uint64 = 1 << 62 27 | pagePresent uint64 = 1 << 63 28 | ) 29 | 30 | var pageFlagsMap = map[int64]string{ 31 | 1 << 0: "LOCKED", 32 | 1 << 1: "ERROR", 33 | 1 << 2: "REFERENCED", 34 | 1 << 3: "UPTODATE", 35 | 1 << 4: "DIRTY", 36 | 1 << 5: "LRU", 37 | 1 << 6: "ACTIVE", 38 | 1 << 7: "SLAB", 39 | 1 << 8: "WRITEBACK", 40 | 1 << 9: "RECLAIM", 41 | 1 << 10: "BUDDY", 42 | 1 << 11: "MMAP", 43 | 1 << 12: "ANON", 44 | 1 << 13: "SWAPCACHE", 45 | 1 << 14: "SWAPBACKED", 46 | 1 << 15: "COMPOUND_HEAD", 47 | 1 << 16: "COMPOUND_TAIL", 48 | 1 << 17: "HUGE", 49 | 1 << 18: "UNEVICTABLE", 50 | 1 << 19: "HWPOISON", 51 | 1 << 20: "NOPAGE", 52 | 1 << 21: "KSM", 53 | 1 << 22: "THP", 54 | 1 << 23: "BALLOON", 55 | 1 << 24: "ZERO_PAGE", 56 | 1 << 25: "IDLE", 57 | } 58 | 59 | var pageSize = uint64(os.Getpagesize()) 60 | 61 | type memArea struct { 62 | start uint64 63 | end uint64 64 | pathName string 65 | } 66 | 67 | func (area *memArea) String() string { 68 | return fmt.Sprintf("Area '%s', range %0#16x - %0#16x (%v)", 69 | area.pathName, area.start, area.end, humanize.Bytes(area.end-area.start)) 70 | } 71 | 72 | type PtExplorerState struct { 73 | pageMapFile *os.File 74 | pageCountFile *os.File 75 | pageFlagsFile *os.File 76 | memAreas []memArea 77 | } 78 | 79 | func (p *PtExplorerState) OpenSystemFiles(pid int) error { 80 | pageMap := fmt.Sprintf("/proc/%v/pagemap", pid) 81 | var err error 82 | p.pageMapFile, err = os.Open(pageMap) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | p.pageCountFile, err = os.Open("/proc/kpagecount") 88 | if err != nil { 89 | return err 90 | } 91 | 92 | p.pageFlagsFile, err = os.Open("/proc/kpageflags") 93 | if err != nil { 94 | return err 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (p *PtExplorerState) ParseMemAreas(pid int) error { 101 | pmap := fmt.Sprintf("/proc/%v/maps", pid) 102 | content, err := ioutil.ReadFile(pmap) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | p.memAreas = make([]memArea, 0) 108 | 109 | lines := strings.Split(string(content), "\n") 110 | for _, line := range lines { 111 | fields := strings.Fields(line) 112 | 113 | var area memArea 114 | if len(fields) == 0 { 115 | continue 116 | } 117 | 118 | memRange := strings.Split(fields[0], "-") 119 | area.start, err = strconv.ParseUint(memRange[0], 16, 64) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | area.end, err = strconv.ParseUint(memRange[1], 16, 64) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | if len(fields) == 6 { 130 | area.pathName = fields[5] 131 | } else { 132 | area.pathName = "anonymous" 133 | } 134 | 135 | p.memAreas = append(p.memAreas, area) 136 | } 137 | 138 | return nil 139 | } 140 | 141 | func (p *PtExplorerState) PrintAreas(areaFilters string, addressFilter uint64, quiet bool) error { 142 | areas := p.getAreasToPrint(areaFilters) 143 | 144 | for _, area := range areas { 145 | err := p.printArea(area, addressFilter, quiet) 146 | if err != nil { 147 | return err 148 | } 149 | } 150 | 151 | return nil 152 | } 153 | 154 | func (p *PtExplorerState) getAreasToPrint(filters string) []memArea { 155 | var areasToParse []memArea 156 | if filters != "" { 157 | filterList := strings.Split(filters, ",") 158 | for _, filter := range filterList { 159 | for _, area := range p.memAreas { 160 | if strings.Contains(area.pathName, filter) { 161 | areasToParse = append(areasToParse, area) 162 | } 163 | } 164 | } 165 | } else { 166 | areasToParse = p.memAreas 167 | } 168 | 169 | return areasToParse 170 | } 171 | 172 | func (p *PtExplorerState) printArea(area memArea, addressFilter uint64, quiet bool) error { 173 | if addressFilter != 0 && (addressFilter < area.start || addressFilter >= area.end) { 174 | return nil 175 | } 176 | 177 | if !quiet { 178 | fmt.Printf("\n") 179 | } 180 | 181 | fmt.Printf("%v\n", area.String()) 182 | 183 | if !quiet { 184 | fmt.Printf("\n") 185 | } 186 | 187 | if quiet { 188 | return nil 189 | } 190 | 191 | nonMapped := 0 192 | 193 | for addr := area.start; addr < area.end; addr += pageSize { 194 | if addressFilter != 0 && (addressFilter < addr || addressFilter >= addr+pageSize) { 195 | continue 196 | } 197 | 198 | err := p.printPage(addr, &nonMapped) 199 | if err != nil { 200 | return err 201 | } 202 | } 203 | 204 | printNonMapped(nonMapped) 205 | 206 | return nil 207 | } 208 | 209 | func (p *PtExplorerState) printPage(address uint64, nonMapped *int) error { 210 | 211 | pageEntry, err := p.getPageEntry(address) 212 | if err != nil { 213 | return err 214 | } 215 | 216 | if pageEntry&pagePresent != 0 || pageEntry&pageSwapped != 0 { 217 | printNonMapped(*nonMapped) 218 | *nonMapped = 0 219 | } 220 | 221 | if pageEntry&pagePresent != 0 { 222 | fmt.Printf("%0#16x: ", address) 223 | 224 | pfn := pageEntry & pfnMask 225 | fmt.Printf("physical address: %0#16x ", pfn*pageSize) 226 | 227 | if pageEntry&pageFile != 0 { 228 | fmt.Printf("file-page ") 229 | } 230 | 231 | if pageEntry&pageExclusive != 0 { 232 | fmt.Printf("exclusive ") 233 | } 234 | 235 | if pageEntry&pageSoftDirty != 0 { 236 | fmt.Printf("soft-dirty ") 237 | } 238 | 239 | pageCount, err := p.getPageCount(pfn) 240 | if err != nil { 241 | return err 242 | } 243 | 244 | fmt.Printf("count:%v ", pageCount) 245 | 246 | pageFlags, err := p.getPageFlags(pfn) 247 | if err != nil { 248 | return err 249 | } 250 | 251 | fmt.Printf("flags:") 252 | first := true 253 | var i uint 254 | for i = 0; i < 64; i++ { 255 | if pageFlags&(1<> 5 278 | fmt.Printf("offset: %0#16x ", swapOffset) 279 | 280 | fmt.Printf("\n") 281 | } else { 282 | pfn := pageEntry & pfnMask 283 | if pfn != 0 { 284 | return errors.New("pfn != 0 for non present page") 285 | } 286 | *nonMapped++ 287 | } 288 | 289 | return nil 290 | } 291 | 292 | func (p *PtExplorerState) getPageEntry(address uint64) (entry uint64, err error) { 293 | pageNumber := address / pageSize 294 | off := int64(pageNumber * pageMapReadChunk) 295 | buf := make([]byte, pageMapReadChunk) 296 | _, err = p.pageMapFile.ReadAt(buf, off) 297 | if err != nil { 298 | return 0, err 299 | } 300 | reader := bytes.NewReader(buf) 301 | err = binary.Read(reader, binary.LittleEndian, &entry) 302 | if err != nil { 303 | return 0, err 304 | } 305 | 306 | return entry, nil 307 | } 308 | 309 | func (p *PtExplorerState) getPageFlags(pfn uint64) (count uint64, err error) { 310 | buf := make([]byte, pageMapReadChunk) 311 | off := int64(pfn * pageMapReadChunk) 312 | _, err = p.pageFlagsFile.ReadAt(buf, off) 313 | if err != nil { 314 | return 0, err 315 | } 316 | var flags uint64 317 | reader := bytes.NewReader(buf) 318 | err = binary.Read(reader, binary.LittleEndian, &flags) 319 | if err != nil { 320 | return 0, err 321 | } 322 | 323 | return flags, nil 324 | } 325 | 326 | func (p *PtExplorerState) getPageCount(pfn uint64) (count uint64, err error) { 327 | buf := make([]byte, pageMapReadChunk) 328 | off := int64(pfn * pageMapReadChunk) 329 | 330 | _, err = p.pageCountFile.ReadAt(buf, off) 331 | if err != nil { 332 | return 0, err 333 | } 334 | reader := bytes.NewReader(buf) 335 | err = binary.Read(reader, binary.LittleEndian, &count) 336 | if err != nil { 337 | return 0, err 338 | } 339 | 340 | return count, nil 341 | } 342 | 343 | func printNonMapped(count int) { 344 | if count != 0 { 345 | fmt.Printf("... %v non mapped pages ...\n", count) 346 | } 347 | } 348 | --------------------------------------------------------------------------------