Skip to content

Commit

Permalink
strace package
Browse files Browse the repository at this point in the history
This is the start of strace. First things first, this simple package lets
you trace system calls.

It also includes a simple command.

Now that I understand we have to lock things down for PTRACE_TRACEME,
and Ryan pointed out I could use LockOSThread(), this works pretty well.

Signed-off-by: Ronald G. Minnich <rminnich@gmail.com>
  • Loading branch information
rminnich committed Oct 12, 2018
1 parent 2a129e2 commit cd480e0
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 0 deletions.
58 changes: 58 additions & 0 deletions cmds/strace/strace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2012-2018 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// strace is a simple single-process tracer.
// It starts the comand and lets the strace.Run() do all the work.
//
// Synopsis:
// strace <command> [args...]
//
// Description:
// trace a single process given a command name.
package main

import (
"fmt"
"log"
"os"
"os/exec"

flag "github.com/spf13/pflag"
"github.com/u-root/u-root/pkg/strace"
)

var (
cmdUsage = "Usage: strace <command> [args...]"
debug = flag.BoolP("debug", "d", false, "enable debug printing")
)

func usage() {
log.Fatalf(cmdUsage)
}

func main() {
flag.Parse()

if *debug {
strace.Debug = log.Printf
}
a := flag.Args()
if len(a) < 1 {
usage()
}

c := exec.Command(a[0], a[1:]...)
c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr

t, err := strace.New()
if err != nil {
log.Fatal(err)
}

go t.RunTracerFromCmd(c)

for r := range t.Records {
fmt.Printf("%s\n", r.String())
}
}
128 changes: 128 additions & 0 deletions pkg/strace/strace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2018 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package strace supports tracing programs.
// The basic control of tracing is via a Tracer, which returns raw
// TraceRecords via a chan. The easiest way to create a Tracer is via
// RunTracerFromCommand, which uses a filled out exec.Cmd to start a
// process and produce trace records.
// Forking and nice printing are not yet supported.
package strace

import (
"fmt"
"os/exec"
"runtime"
"syscall"

"golang.org/x/sys/unix"
)

var Debug = func(string, ...interface{}) {}

// Tracer has information to trace one process. It can be created by
// starting a command, or attaching. Attaching is not supported yet.
type Tracer struct {
Pid int
Records chan *TraceRecord
Count int
}

func New() (*Tracer, error) {
return &Tracer{Pid: -1, Records: make(chan *TraceRecord)}, nil
}

// StartTracerFromCommand runs a Tracer given an exec.Cmd.
func (t *Tracer) RunTracerFromCmd(c *exec.Cmd) {
defer close(t.Records)
if c.SysProcAttr == nil {
c.SysProcAttr = &syscall.SysProcAttr{}
}
c.SysProcAttr.Ptrace = true
// Because the go runtime forks traced processes with PTRACE_TRACEME
// we need to maintain the parent-child relationship for ptrace to work.
// We've learned this the hard way. So we lock down this thread to
// this proc, and start the command here.
// Note this function will block; if you want it to be nonblocking you
// need to use go etc.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := c.Start(); err != nil {
Debug("Start gets err %v", err)
t.Records <- &TraceRecord{Err: err}
return
}
Debug("Start gets pid %v", c.Process.Pid)
if err := c.Wait(); err != nil {
fmt.Printf("Wait returned: %v\n", err)
t.Records <- &TraceRecord{Err: err}
}
t.Pid = c.Process.Pid
t.Run()
}

func (t *Tracer) Run() error {
x := Enter
for {
t.Count++
r := &TraceRecord{Serial: t.Count, EX: x, Pid: t.Pid}
if err := unix.PtraceGetRegs(t.Pid, &r.Regs); err != nil {
Debug("ptracegetregs for %d gets %v", t.Pid, err)
r.Err = err
t.Records <- r
break
}
t.Records <- r
switch x {
case Enter:
x = Exit
default:
x = Enter
}
if err := unix.PtraceSyscall(t.Pid, 0); err != nil {
r := &TraceRecord{Serial: t.Count, EX: x, Pid: t.Pid}
Debug("ptracesyscall for %d gets %v", t.Pid, err)
r.Err = err
t.Records <- r
break
}
if w, err := Wait(t.Pid); err != nil {
r := &TraceRecord{Serial: t.Count, EX: x, Pid: t.Pid}
Debug("wait4 for %d gets %v, %v", t.Pid, w, err)
r.Err = err
t.Records <- r
break
}

}
Debug("Pushed %d records", t.Count)
return nil
}

// EventType describes whether a record is system call Entry or Exit
type EventType string

const (
Enter EventType = "E"
Exit = "X"
)

// TraceRecord has information about a ptrace event.
type TraceRecord struct {
EX EventType
Regs unix.PtraceRegs
Serial int
Pid int
Err error
}

// String is a stringer for TraceRecords
// TODO: stringer for Regs.
func (t *TraceRecord) String() string {
pre := fmt.Sprintf("%s %d#%d:", t.EX, t.Pid, t.Serial)
if t.Err != nil {
return fmt.Sprintf("%s(%v)", pre, t.Err)
}
return fmt.Sprintf("%s %v", pre, t.Regs)
}
45 changes: 45 additions & 0 deletions pkg/strace/strace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2018 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package strace

import (
"os/exec"
"testing"
)

// It's not really easy to write a full up general tester for this.
// Even the simplest commands on Linux have dozens of system calls.
// The Go assembler should in principle let us write a 3 line assembly
// program that just does an exit system call:
// MOVQ $exit, RARG
// SYSCALL
// But that's for someone else to do :-)

func TestNoCommandFail(t *testing.T) {
Debug = t.Logf
c, err := New()
if err != nil {
t.Fatal(err)
}
go c.RunTracerFromCmd(exec.Command("hi", "/etc/hosts"))
r := <-c.Records
if r.Err == nil {
t.Fatalf("Got nil, want a non-nil error")
}
}

func TestBasicStrace(t *testing.T) {
Debug = t.Logf
c, err := New()
if err != nil {
t.Fatal(err)
}

go c.RunTracerFromCmd(exec.Command("ls", "/etc/hosts"))

for r := range c.Records {
t.Logf("%s", r.String())
}
}
18 changes: 18 additions & 0 deletions pkg/strace/strace_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2018 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package strace

import (
"syscall"

"golang.org/x/sys/unix"
)

func Wait(pid int) (unix.WaitStatus, error) {
var w syscall.WaitStatus
_, err := syscall.Wait4(pid, &w, 0, nil)
uw := unix.WaitStatus(w)
return uw, err
}

0 comments on commit cd480e0

Please sign in to comment.