Skip to content

Commit

Permalink
feat: display content of process memory pages
Browse files Browse the repository at this point in the history
This commit extends `memparse` sub-command with the ability to display
the content of process memory pages content in a hexdump-like format when
the `--pid` flag is provided. Additionally, the output can be written to a
file using the `--output` flag.

Signed-off-by: Kouame Behouba Manasse <behouba@gmail.com>
  • Loading branch information
behouba committed Aug 5, 2023
1 parent 119745a commit 54ae5da
Show file tree
Hide file tree
Showing 73 changed files with 1,017 additions and 490 deletions.
60 changes: 46 additions & 14 deletions checkpointctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@ import (
)

var (
name string
version string
format string
stats bool
mounts bool
pID uint32
psTree bool
psTreeCmd bool
psTreeEnv bool
files bool
showAll bool
name string
version string
format string
stats bool
mounts bool
outputFilePath string
pID uint32
psTree bool
psTreeCmd bool
psTreeEnv bool
files bool
showAll bool
)

func main() {
Expand Down Expand Up @@ -275,6 +276,23 @@ func setupMemParse() *cobra.Command {
Args: cobra.MinimumNArgs(1),
}

flags := cmd.Flags()

flags.Uint32VarP(
&pID,
"pid",
"p",
0,
"Specify the PID of a process to analyze",
)
flags.StringVarP(
&outputFilePath,
"output",
"o",
"",
"Specify the output file to be written to",
)

return cmd
}

Expand All @@ -283,9 +301,19 @@ func memparse(cmd *cobra.Command, args []string) error {
metadata.SpecDumpFile, metadata.ConfigDumpFile,
filepath.Join(metadata.CheckpointDirectory, "pstree.img"),
filepath.Join(metadata.CheckpointDirectory, "core-"),
filepath.Join(metadata.CheckpointDirectory, "pagemap-"),
filepath.Join(metadata.CheckpointDirectory, "pages-"),
filepath.Join(metadata.CheckpointDirectory, "mm-"),
}

if pID == 0 {
requiredFiles = append(
requiredFiles,
filepath.Join(metadata.CheckpointDirectory, "pagemap-"),
)
} else {
requiredFiles = append(
requiredFiles,
filepath.Join(metadata.CheckpointDirectory, fmt.Sprintf("pagemap-%d.img", pID)),
filepath.Join(metadata.CheckpointDirectory, fmt.Sprintf("mm-%d.img", pID)),
)
}

tasks, err := createTasks(args, requiredFiles)
Expand All @@ -294,5 +322,9 @@ func memparse(cmd *cobra.Command, args []string) error {
}
defer cleanupTasks(tasks)

if pID != 0 {
return printProcessMemoryPages(tasks[0])
}

return showProcessMemorySizeTables(tasks)
}
14 changes: 14 additions & 0 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,17 @@ func getPsEnvVars(checkpointOutputDir string, pid uint32) (envVars []string, err

return
}

// FindPs performs a short-circuiting depth-first search to find
// a process with a given PID in a process tree.
func findPs(ps *crit.PsTree, pid uint32) *crit.PsTree {
if ps.PID == pid {
return ps
}
for _, child := range ps.Children {
if process := findPs(child, pid); process != nil {
return process
}
}
return nil
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/checkpoint-restore/checkpointctl
go 1.18

require (
github.com/checkpoint-restore/go-criu/v6 v6.3.1-0.20230705155226-f8745d1ee56a
github.com/checkpoint-restore/go-criu/v6 v6.3.1-0.20230803210323-a74ef6248628
github.com/containers/storage v1.48.0
github.com/olekukonko/tablewriter v0.0.5
github.com/opencontainers/runtime-spec v1.1.0
Expand All @@ -25,5 +25,5 @@ require (
github.com/ulikunitz/xz v0.5.11 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.30.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/checkpoint-restore/go-criu/v6 v6.3.1-0.20230705155226-f8745d1ee56a h1:I9EbXzk/kRIcXDGk9VAdYjGTicrok3Bu3WZ+SOJ3K2E=
github.com/checkpoint-restore/go-criu/v6 v6.3.1-0.20230705155226-f8745d1ee56a/go.mod h1:uuIaCJCnWcRzK88xoScsiKHpwBlF8uChRbyhi/BZiZg=
github.com/checkpoint-restore/go-criu/v6 v6.3.1-0.20230803210323-a74ef6248628 h1:N0LDmW0R1U8ewZFru8ocr6vdwr0XSPpKrttShxJcGs4=
github.com/checkpoint-restore/go-criu/v6 v6.3.1-0.20230803210323-a74ef6248628/go.mod h1:3eG5SDBmX8R3iY7kAnrj11UrR7DgmHFiJy+ygS9mlNo=
github.com/containers/storage v1.48.0 h1:wiPs8J2xiFoOEAhxHDRtP6A90Jzj57VqzLRXOqeizns=
github.com/containers/storage v1.48.0/go.mod h1:pRp3lkRo2qodb/ltpnudoXggrviRmaCmU5a5GhTBae0=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
Expand Down Expand Up @@ -53,8 +53,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
119 changes: 119 additions & 0 deletions memparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
package main

import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"

Expand All @@ -14,6 +16,10 @@ import (
"github.com/olekukonko/tablewriter"
)

// chunkSize represents the default size of memory chunk (in bytes)
// to read for each output line when printing memory pages content in hexdump-like format.
const chunkSize = 16

// Display processes memory sizes within the given container checkpoints.
func showProcessMemorySizeTables(tasks []task) error {
// Initialize the table
Expand Down Expand Up @@ -81,3 +87,116 @@ func showProcessMemorySizeTables(tasks []task) error {

return nil
}

func printProcessMemoryPages(task task) error {
c := crit.New(nil, nil, filepath.Join(task.outputDir, metadata.CheckpointDirectory), false, false)
psTree, err := c.ExplorePs()
if err != nil {
return fmt.Errorf("failed to get process tree: %w", err)
}

// Check if PID exist within the checkpoint
if pID != 0 {
ps := findPs(psTree, pID)
if ps == nil {
return fmt.Errorf("no process with PID %d (use `inspect --ps-tree` to view all PIDs)", pID)
}
}

memReader, err := crit.NewMemoryReader(
filepath.Join(task.outputDir, metadata.CheckpointDirectory),
pID, pageSize,
)
if err != nil {
return err
}

// Unpack pages-[pagesID].img file for the given PID
if err := untarFiles(
task.checkpointFilePath, task.outputDir,
[]string{filepath.Join(metadata.CheckpointDirectory, fmt.Sprintf("pages-%d.img", memReader.GetPagesID()))},
); err != nil {
return err
}

// Write the output to stdout by default
var output io.Writer = os.Stdout
var compact bool

if outputFilePath != "" {
// Write to ouput to file if --output is specified
f, err := os.Create(outputFilePath)
if err != nil {
return err
}
defer f.Close()
output = f
fmt.Printf("\nWriting memory pages content for process ID %d from checkpoint: %s to file: %s...\n",
pID, task.checkpointFilePath, outputFilePath,
)
} else {
compact = true // Use a compact format when writing the output to stdout
fmt.Printf("\nDisplaying memory pages content for process ID %d from checkpoint: %s\n\n", pID, task.checkpointFilePath)
}

fmt.Fprintln(output, "Address Hexadecimal ASCII ")
fmt.Fprintln(output, "-------------------------------------------------------------------------------------")

pagemapEntries := memReader.GetPagemapEntries()
for _, entry := range pagemapEntries {
start := entry.GetVaddr()
end := start + (uint64(pageSize) * uint64(entry.GetNrPages()))
buf, err := memReader.GetMemPages(start, end)
if err != nil {
return err
}

hexdump(output, buf, start, compact)
}
return nil
}

// hexdump generates a hexdump of the buffer 'buf' starting at the virtual address 'start'
// and writes the output to 'out'. If compact is true, consecutive duplicate rows will be represented
// with an asterisk (*).
func hexdump(out io.Writer, buf *bytes.Buffer, vaddr uint64, compact bool) {
var prevAscii string
var isDuplicate bool
for buf.Len() > 0 {
row := buf.Next(chunkSize)
hex, ascii := generateHexAndAscii(row)

if compact {
if prevAscii == ascii {
if !isDuplicate {
fmt.Fprint(out, "*\n")
}
isDuplicate = true
} else {
fmt.Fprintf(out, "%016x %s |%s|\n", vaddr, hex, ascii)
isDuplicate = false
}
} else {
fmt.Fprintf(out, "%016x %s |%s|\n", vaddr, hex, ascii)
}

vaddr += chunkSize
prevAscii = ascii
}
}

// generateHexAndAscii takes a byte slice and generates its hexadecimal and ASCII representations.
func generateHexAndAscii(data []byte) (string, string) {
var hex, ascii string
for i := 0; i < len(data); i++ {
if data[i] < 32 || data[i] >= 127 {
ascii += "."
hex += fmt.Sprintf("%02x ", data[i])
} else {
ascii += string(data[i])
hex += fmt.Sprintf("%02x ", data[i])
}
}

return hex, ascii
}
16 changes: 1 addition & 15 deletions tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,21 +121,7 @@ func addDumpStatsToTree(tree treeprint.Tree, dumpStats *stats_pb.DumpStatsEntry)
func addPsTreeToTree(tree treeprint.Tree, psTree *crit.PsTree, checkpointOutputDir string) error {
psRoot := psTree
if pID != 0 {
// dfs performs a short-circuiting depth-first search.
var dfs func(*crit.PsTree) *crit.PsTree
dfs = func(root *crit.PsTree) *crit.PsTree {
if root.PID == pID {
return root
}
for _, child := range root.Children {
if ps := dfs(child); ps != nil {
return ps
}
}
return nil
}

ps := dfs(psTree)
ps := findPs(psTree, pID)
if ps == nil {
return fmt.Errorf("no process with PID %d (use `inspect --ps-tree` to view all PIDs)", pID)
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 54ae5da

Please sign in to comment.