Browse Source

Checkpoint

master
parent
commit
e8b111f6a0
16 changed files with 855 additions and 138 deletions
  1. +20
    -0
      builtin.go
  2. +35
    -0
      bytes_be.go
  3. +20
    -4
      bytes_le.go
  4. +2
    -2
      cmd/ebpfasm/main.go
  5. +255
    -0
      cmd/ebpfrun/main.go
  6. +36
    -16
      elf.go
  7. +47
    -24
      instruction.go
  8. +4
    -36
      op_alu.go
  9. +58
    -0
      op_byteswap.go
  10. +7
    -7
      op_call.go
  11. +33
    -4
      op_jump.go
  12. +30
    -26
      op_load.go
  13. +12
    -12
      op_load_test.go
  14. +9
    -4
      program.go
  15. +3
    -3
      register.go
  16. +284
    -0
      vm.go

+ 20
- 0
builtin.go View File

@ -0,0 +1,20 @@
package ebpf
import (
"log"
"math"
)
func BuiltinProbeRead(r1, r2, r3, r4, r5 uint64) uint64 {
log.Printf("probe read", r1, r2, r3, r4, r5)
return math.MaxUint64
}
func BuiltinStrcmp(r1, r2, r3, r4, r5 uint64) uint64 {
log.Printf("strcmp", r1, r2, r3, r4, r5)
return math.MaxUint64
}
func (vm *VM) AttachBuiltin() {
vm.Attach(ProbeRead, BuiltinProbeRead)
}

+ 35
- 0
bytes_be.go View File

@ -0,0 +1,35 @@
// +build armbe arm64be ppc64 mips mips64 mips64p32 ppc s390 s390x sparc sparc64
package ebpf
func htole16(n uint16) uint16 {
return (n&0x00FF)<<8 | (n&0xFF00)>>8
}
func htole32(n uint32) uint32 {
return (n&0x000000FF)<<24 | (n&0x0000FF00)<<8 |
(n&0x00FF0000)>>8 | (n&0xFF000000)>>24
}
func htole64(n uint64) uint64 {
return ((n & 0x00000000000000FF) << 56) |
((n & 0x000000000000FF00) << 40) |
((n & 0x0000000000FF0000) << 24) |
((n & 0x00000000FF000000) << 8) |
((n & 0x000000FF00000000) >> 8) |
((n & 0x0000FF0000000000) >> 24) |
((n & 0x00FF000000000000) >> 40) |
((n & 0xFF00000000000000) >> 56)
}
func htobe16(n uint16) uint16 {
return n
}
func htobe32(n uint32) uint32 {
return n
}
func htobe64(n uint64) uint64 {
return n
}

bytes.go → bytes_le.go View File


+ 2
- 2
cmd/ebpfasm/main.go View File

@ -16,8 +16,8 @@ import (
func main() {
app := cli.App{
Name: "bpfasm",
Usage: "Berkeley Packet Filter assembler/disassembler",
Name: "ebpfasm",
Usage: "Extended Berkeley Packet Filter assembler/disassembler",
Commands: []*cli.Command{
&cli.Command{
Name: "assemble",


+ 255
- 0
cmd/ebpfrun/main.go View File

@ -0,0 +1,255 @@
package main
import (
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"maze.io/ebpf"
cli "github.com/urfave/cli/v2"
)
func main() {
app := cli.App{
Name: "ebpfrun",
Usage: "Extended Berkeley Packet Filter virtual machine",
ArgsUsage: "<input>",
Action: run,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "endian",
Aliases: []string{"e"},
Value: "le",
Usage: "Binary byte order (be, le)",
},
&cli.StringFlag{
Name: "format",
Aliases: []string{"f"},
Value: "auto",
Usage: "Input format (auto, bin, elf, asm)",
},
&cli.Uint64Flag{
Name: "memory",
Aliases: []string{"m"},
Usage: "Memory size (in bytes)",
Value: 1024,
},
&cli.StringFlag{
Name: "memory-data",
Aliases: []string{"M"},
Usage: "Memory contents (comma delimited)",
},
&cli.BoolFlag{
Name: "memory-dump",
Aliases: []string{"D"},
Usage: "Dump memory after program exits",
},
&cli.BoolFlag{
Name: "trace",
Aliases: []string{"t"},
Usage: "Enable tracing",
EnvVars: []string{"VM_TRACE"},
Value: false,
},
&cli.StringFlag{
Name: "registers",
Aliases: []string{"r"},
Usage: "Initial register values, comma separated",
},
&cli.StringFlag{
Name: "section",
Aliases: []string{"s"},
Usage: "ELF section to load",
},
},
}
app.Run(os.Args)
}
func run(ctx *cli.Context) error {
if ctx.NArg() != 1 {
return cli.ShowAppHelp(ctx)
}
var (
name = ctx.Args().First()
format = ctx.String("format")
section = ctx.String("section")
f *os.File
byteOrder binary.ByteOrder
program ebpf.Program
err error
)
switch ctx.String("endian") {
case "be":
byteOrder = binary.BigEndian
case "le":
byteOrder = binary.LittleEndian
default:
fmt.Fprintf(os.Stderr, "invalid byte order %q\n", ctx.String("endian"))
os.Exit(1)
}
if f, err = os.Open(name); err != nil {
return err
}
defer f.Close()
if format == "auto" {
switch strings.ToLower(filepath.Ext(name)) {
case ".o", ".elf":
format = "elf"
case "asm", "txt":
format = "asm"
case ".bin":
fallthrough
default:
format = "bin"
}
}
if program, err = loadProgram(format, section, f, byteOrder); err != nil {
fmt.Fprintf(os.Stderr, "error parsing %q: %v\n", name, err)
os.Exit(2)
}
vm := ebpf.New()
vm.AttachBuiltin()
if err = vm.Load(program); err != nil {
fmt.Fprintf(os.Stderr, "error loading %q: %v\n", name, err)
os.Exit(2)
}
if ctx.Bool("trace") {
vm.Tracer = newTracer(vm)
}
var (
memory = make([]byte, ctx.Uint64("memory"))
ret uint64
)
if ret, err = vm.Run(memory); err != nil {
fmt.Fprintf(os.Stderr, "error running %q: %v\n", name, err)
if ctx.Bool("memory-dump") {
fmt.Print(hex.Dump(memory))
}
os.Exit(2)
}
if ctx.Bool("memory-dump") {
fmt.Print(hex.Dump(memory))
}
fmt.Printf("returned: %d (%#x)\n", ret, ret)
return nil
}
func loadProgram(format, section string, f *os.File, byteOrder binary.ByteOrder) (program ebpf.Program, err error) {
switch format {
case "asm":
var source []byte
if source, err = ioutil.ReadAll(f); err != nil {
return
}
program, err = ebpf.Assemble(string(source))
case "bin":
program, err = ebpf.DisassembleReaderOrder(f, byteOrder)
case "elf":
var sections map[string]ebpf.Program
if sections, err = ebpf.LoadELF(f); err != nil {
return
}
var ok bool
if section == "" {
fmt.Printf("%d available ELF sections:\n", len(sections))
for name := range sections {
fmt.Println(name)
}
err = errors.New("please specify what ELF section to load with --section")
} else if program, ok = sections[section]; !ok {
err = fmt.Errorf("ELF section %q was not found", section)
}
default:
err = fmt.Errorf("invalid format %q", format)
}
return
}
type tracer struct {
vm *ebpf.VM
pc uint32
}
func newTracer(vm *ebpf.VM) ebpf.Tracer {
return &tracer{vm: vm}
}
func (t *tracer) TraceStart() {
fmt.Println("--- start ---")
}
func (t *tracer) TracePre(in ebpf.Instruction) {
t.pc = t.vm.PC
fmt.Printf("%-4d %-32s", t.pc, in)
switch in := in.(type) {
case ebpf.ALUOpConstant:
fmt.Printf("; %-3s %#08x -> ", in.Dst, uint32(t.vm.Register[in.Dst]))
case ebpf.ALUOpRegister:
fmt.Printf("; %-3s %#08x -> ", in.Dst, uint32(t.vm.Register[in.Dst]))
case ebpf.ALU64OpConstant:
fmt.Printf("; %-3s %#016x -> ", in.Dst, t.vm.Register[in.Dst])
case ebpf.ALU64OpRegister:
fmt.Printf("; %-3s %#016x -> ", in.Dst, t.vm.Register[in.Dst])
case ebpf.Jump:
fmt.Printf("; PC %d -> ", t.pc)
case ebpf.JumpIf, ebpf.JumpIfX:
fmt.Printf("; PC %#08x -> ", t.pc)
default:
fmt.Println()
}
}
func (t *tracer) TracePost(in ebpf.Instruction) {
switch in := in.(type) {
case ebpf.ALUOpConstant:
fmt.Printf("%#08x\n", uint32(t.vm.Register[in.Dst]))
case ebpf.ALUOpRegister:
fmt.Printf("%#08x\n", uint32(t.vm.Register[in.Dst]))
case ebpf.ALU64OpConstant:
fmt.Printf("%#016x\n", t.vm.Register[in.Dst])
case ebpf.ALU64OpRegister:
fmt.Printf("%#016x\n", t.vm.Register[in.Dst])
case ebpf.Jump, ebpf.JumpIf, ebpf.JumpIfX:
if t.vm.PC == t.pc {
fmt.Println("<unchanged>")
} else {
fmt.Printf("%d\n", t.vm.PC)
}
}
}
func (t tracer) TraceEnded() {
fmt.Println("--- ended ---")
fmt.Printf("PC: %d\n", t.vm.PC)
fmt.Printf("R0: %#016x\n", t.vm.Register[ebpf.R0])
fmt.Printf("R1-5: %#016x, ", t.vm.Register[ebpf.R1])
fmt.Printf("%#016x, ", t.vm.Register[ebpf.R2])
fmt.Printf("%#016x, ", t.vm.Register[ebpf.R3])
fmt.Printf("%#016x, ", t.vm.Register[ebpf.R4])
fmt.Printf("%#016x\n", t.vm.Register[ebpf.R5])
fmt.Printf("R6-9: %#016x, ", t.vm.Register[ebpf.R6])
fmt.Printf("%#016x, ", t.vm.Register[ebpf.R7])
fmt.Printf("%#016x, ", t.vm.Register[ebpf.R8])
fmt.Printf("%#016x\n", t.vm.Register[ebpf.R9])
fmt.Printf("RFP: %#016x\n", t.vm.Register[ebpf.RFP])
}

+ 36
- 16
elf.go View File

@ -6,6 +6,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"io"
"log"
)
@ -22,49 +23,53 @@ var (
errTextSection = errors.New("ebpf: text section not found")
)
func loadELF(name string) error {
file, err := elf.Open(name)
// LoadELF loads eBPF program(s) from an ELF file.
//
// This function is not finished and work-in-progress. Expect breakage.
func LoadELF(r io.ReaderAt) (map[string]Program, error) {
file, err := elf.NewFile(r)
if err != nil {
return err
return nil, err
}
if file.Class != elf.ELFCLASS64 {
return errELFClass
return nil, errELFClass
}
if file.Data != elf.ELFDATA2LSB {
return errELFByteOrder
return nil, errELFByteOrder
}
if file.Version != 1 {
return errELFVersion
return nil, errELFVersion
}
if file.OSABI != 0 {
return errELFOSABI
return nil, errELFOSABI
}
if file.Type != elf.ET_REL {
return errELFRelocatable
return nil, errELFRelocatable
}
if file.Machine != elf.EM_NONE && file.Machine != elf.EM_BPF {
return fmt.Errorf("ebpf: invalid ELF machine, expected BPF, got %d", file.Machine)
return nil, fmt.Errorf("ebpf: invalid ELF machine, expected BPF, got %d", file.Machine)
}
if l := len(file.Sections); l > maxELFSections {
return fmt.Errorf("ebpf: %d ELF sections exceeds maximum of %d", l, maxELFSections)
return nil, fmt.Errorf("ebpf: %d ELF sections exceeds maximum of %d", l, maxELFSections)
}
var textSection *elf.Section
for _, section := range file.Sections {
if section.Type == elf.SHT_PROGBITS && section.Flags == (elf.SHF_ALLOC|elf.SHF_EXECINSTR) {
//log.Printf("text section: %#+v", section)
textSection = section
break
}
}
if textSection == nil {
return errTextSection
return nil, errTextSection
}
var textData = make([]byte, textSection.Size)
data, err := textSection.Data()
if err != nil {
return err
return nil, err
}
copy(textData, data)
@ -76,7 +81,7 @@ func loadELF(name string) error {
log.Printf("rel: %#+v", rel)
if rel.SectionHeader.Link >= uint32(len(file.Sections)) {
return errors.New("ebpf: bad symbol table section index")
return nil, errors.New("ebpf: bad symbol table section index")
}
symbols, _, err := getSymbols(file, file.Sections[rel.SectionHeader.Link])
@ -84,7 +89,7 @@ func loadELF(name string) error {
rs, err := parseRelocations(file, rel)
if err != nil {
return err
return nil, err
}
log.Printf("relocations: %#+v", rs)
for _, r := range rs {
@ -93,14 +98,29 @@ func loadELF(name string) error {
symIndex = r.Info >> 32
)
if symIndex >= uint64(len(symbols)) {
return errors.New("ebpf: bad symbol index")
return nil, errors.New("ebpf: bad symbol index")
}
log.Println("relocate to", symbols[symIndex])
}
}
return nil
programs := make(map[string]Program)
for _, section := range file.Sections {
if section == textSection {
continue
}
if section.Type == elf.SHT_PROGBITS && section.Flags == (elf.SHF_ALLOC|elf.SHF_EXECINSTR) {
var source []byte
if source, err = section.Data(); err != nil {
return nil, err
}
if programs[section.Name], err = Disassemble(source); err != nil {
return nil, fmt.Errorf("ebpf: error disassembling %q section: %w", section.Name, err)
}
}
}
return programs, nil
}
func parseRelocations(file *elf.File, rel *elf.Section) (rs []elf.Rel64, err error) {


+ 47
- 24
instruction.go View File

@ -20,8 +20,8 @@ type RawInstruction struct {
// Operation to execute.
Op Opcode
Dst, Src Register
Offset uint16
Immediate uint32
Offset int16
Immediate uint64
}
// Assemble implements the Instruction Assemble method.
@ -49,10 +49,10 @@ func (ri RawInstruction) Disassemble() Instruction {
switch ri.Op & opMaskMode {
case modeImmediate: // 0x00
if class == opClassLoad {
if size != 4 {
if size != 4 && size != 8 {
return ri
}
return LoadConstant{Dst: ri.Dst, Value: ri.Immediate}
return LoadConstant{Dst: ri.Dst, Size: size, Value: ri.Immediate}
}
case modeAbsolute: // 0x20
if class == opClassLoad {
@ -74,7 +74,7 @@ func (ri RawInstruction) Disassemble() Instruction {
}
case modeMemory: // 0x60
if class == opClassLoadX {
return LoadMemory{
return LoadRegister{
Dst: ri.Dst,
Src: ri.Src,
Offset: ri.Offset,
@ -101,14 +101,14 @@ func (ri RawInstruction) Disassemble() Instruction {
switch ri.Op & opMaskMode {
case modeMemory: // 0x60
if class == opClassStore {
return Store{
return StoreImmediate{
Dst: ri.Dst,
Offset: ri.Offset,
Value: ri.Immediate,
Value: uint32(ri.Immediate),
Size: size,
}
}
return StoreMemory{
return StoreRegister{
Dst: ri.Dst,
Src: ri.Src,
Offset: ri.Offset,
@ -124,7 +124,7 @@ func (ri RawInstruction) Disassemble() Instruction {
return ALUOpConstant{
Op: op,
Dst: ri.Dst,
Value: ri.Immediate,
Value: uint32(ri.Immediate),
}
case aluSourceX:
return ALUOpRegister{
@ -155,7 +155,7 @@ func (ri RawInstruction) Disassemble() Instruction {
return ALU64OpConstant{
Op: op,
Dst: ri.Dst,
Value: ri.Immediate,
Value: uint32(ri.Immediate),
}
case aluSourceX:
return ALU64OpRegister{
@ -171,19 +171,29 @@ func (ri RawInstruction) Disassemble() Instruction {
case opClassJump:
switch op := ri.Op & 0xf0; op {
case jumpOp:
return Jump{ri.Offset}
return Jump{uint16(ri.Offset)}
case jumpOpEqual, jumpOpGreater, jumpOpGreaterOrEqual, jumpOpSet, jumpOpNotEqual,
jumpOpSignedGreater, jumpOpSignedGreaterOrEqual, jumpOpLess, jumpOpLessOrEqual,
jumpOpSignedLess, jumpOpSignedLessOrEqual:
cond := jumpOpToTest(op)
if ri.Op&0x08 == jumpSourceImmediate {
return JumpIf{cond, ri.Dst, ri.Immediate, ri.Offset}
return JumpIf{
Cond: cond,
Dst: ri.Dst,
Value: uint32(ri.Immediate),
Offset: uint16(ri.Offset),
}
}
return JumpIfX{
Cond: cond,
Dst: ri.Dst,
Src: ri.Src,
Offset: uint16(ri.Offset),
}
return JumpIfX{cond, ri.Dst, ri.Src, ri.Offset}
case jumpOpExit:
return Exit{}
case jumpOpCall:
return Builtin(ri.Immediate)
return Call(ri.Immediate)
}
}
return ri
@ -206,28 +216,41 @@ func DisassembleReader(r io.Reader) (Program, error) {
return DisassembleReaderOrder(r, binary.LittleEndian)
}
type marshaledInstruction struct {
Op Opcode
DstSrc uint8
Offset int16
Immediate uint32
}
// DisassembleReaderOrder is like DisassembleReader with a custom byte order.
func DisassembleReaderOrder(r io.Reader, byteOrder binary.ByteOrder) (Program, error) {
var out Program
var (
out Program
)
for {
var load struct {
Op Opcode
DstSrc uint8
Offset uint16
Immediate uint32
}
var load marshaledInstruction
if err := binary.Read(r, byteOrder, &load); err == io.EOF {
break
} else if err != nil {
return nil, err
}
out = append(out, RawInstruction{
ri := RawInstruction{
Op: load.Op,
Dst: Register(load.DstSrc & 0x0f),
Src: Register(load.DstSrc >> 4),
Offset: load.Offset,
Immediate: load.Immediate,
}.Disassemble())
Immediate: uint64(load.Immediate),
}
if ri.Op == 0x18 {
// LDDW takes two opcodes
var next marshaledInstruction
if err := binary.Read(r, byteOrder, &next); err != nil {
return nil, err
}
ri.Immediate |= uint64(next.Immediate) << 32
}
out = append(out, ri.Disassemble())
}
return out, nil
}

+ 4
- 36
op_alu.go View File

@ -1,7 +1,6 @@
package ebpf
import (
"encoding/binary"
"fmt"
)
@ -69,12 +68,12 @@ func (a ALUOpConstant) Assemble() (RawInstruction, error) {
return RawInstruction{
Op: opClassALU | aluSourceImmediate | Opcode(a.Op),
Dst: a.Dst,
Immediate: a.Value,
Immediate: uint64(a.Value),
}, nil
}
func (a ALUOpConstant) String() string {
return fmt.Sprintf("%s32 %s, %d", a.Op.Mnemoic(), a.Dst, a.Value)
return fmt.Sprintf("%s32 %s, %#x", a.Op.Mnemoic(), a.Dst, a.Value)
}
// ALUOpRegister executes Dst = Dst <Op> Src.
@ -126,12 +125,12 @@ func (a ALU64OpConstant) Assemble() (RawInstruction, error) {
return RawInstruction{
Op: opClassALU64 | aluSourceImmediate | Opcode(a.Op),
Dst: a.Dst,
Immediate: a.Value,
Immediate: uint64(a.Value),
}, nil
}
func (a ALU64OpConstant) String() string {
return fmt.Sprintf("%s %s, %d", a.Op.Mnemoic(), a.Dst, a.Value)
return fmt.Sprintf("%s %s, %#x", a.Op.Mnemoic(), a.Dst, a.Value)
}
// ALU64OpRegister executes Dst = Dst <Op> Src.
@ -171,37 +170,6 @@ func (a Negate64) String() string {
return fmt.Sprintf("neg %s", a.Dst)
}
type ByteSwap struct {
Dst Register
ByteOrder binary.ByteOrder
Size uint8
}
// Assemble implements the Instruction Assemble method.
func (a ByteSwap) Assemble() (RawInstruction, error) {
var source Opcode
if a.ByteOrder == binary.BigEndian {
source = aluSourceX
}
switch a.Size {
case 16, 32, 64:
return RawInstruction{
Op: opClassALU | source | Opcode(aluOpByteSwap),
Dst: a.Dst,
Immediate: uint32(a.Size),
}, nil
default:
return RawInstruction{}, fmt.Errorf("ebpf: invalid byte swap size %d", a.Size)
}
}
func (a ByteSwap) String() string {
if a.ByteOrder == binary.LittleEndian {
return fmt.Sprintf("le%d %s", a.Size, a.Dst)
}
return fmt.Sprintf("be%d %s", a.Size, a.Dst)
}
var (
_ Instruction = (*ALUOpConstant)(nil)
_ Instruction = (*ALUOpRegister)(nil)


+ 58
- 0
op_byteswap.go View File

@ -0,0 +1,58 @@
package ebpf
import (
"encoding/binary"
"fmt"
)
type ByteSwap struct {
Dst Register
ByteOrder binary.ByteOrder
Size uint8
}
// Assemble implements the Instruction Assemble method.
func (a ByteSwap) Assemble() (RawInstruction, error) {
var source Opcode
if a.ByteOrder == binary.BigEndian {
source = aluSourceX
}
switch a.Size {
case 16, 32, 64:
return RawInstruction{
Op: opClassALU | source | Opcode(aluOpByteSwap),
Dst: a.Dst,
Immediate: uint64(a.Size),
}, nil
default:
return RawInstruction{}, fmt.Errorf("ebpf: invalid byte swap size %d", a.Size)
}
}
func (a ByteSwap) String() string {
if a.ByteOrder == binary.LittleEndian {
return fmt.Sprintf("le%d %s", a.Size, a.Dst)
}
return fmt.Sprintf("be%d %s", a.Size, a.Dst)
}
func (a ByteSwap) Swap(value uint64) uint64 {
switch {
case a.Size == 16 && a.ByteOrder == binary.LittleEndian:
return uint64(htole16(uint16(value)))
case a.Size == 32 && a.ByteOrder == binary.LittleEndian:
return uint64(htole32(uint32(value)))
case a.Size == 64 && a.ByteOrder == binary.LittleEndian:
return htole64(value)
case a.Size == 16 && a.ByteOrder == binary.BigEndian:
return uint64(htobe16(uint16(value)))
case a.Size == 32 && a.ByteOrder == binary.BigEndian:
return uint64(htobe32(uint32(value)))
case a.Size == 64 && a.ByteOrder == binary.BigEndian:
return htobe64(value)
default:
return value
}
}
var _ Instruction = (*ByteSwap)(nil)

+ 7
- 7
op_call.go View File

@ -2,11 +2,11 @@ package ebpf
import "fmt"
// Builtin function.
type Builtin uint32
// Call function.
type Call uint32
const (
Unspec Builtin = iota
Unspec Call = iota
MapLookupElem
MapUpdateElem
MapDeleteElem
@ -128,17 +128,17 @@ const (
)
// Assemble implements the Instruction Assemble method.
func (a Builtin) Assemble() (RawInstruction, error) {
func (a Call) Assemble() (RawInstruction, error) {
return RawInstruction{
Op: opClassJump | jumpOpCall,
Immediate: uint32(a),
Immediate: uint64(a),
}, nil
}
func (a Builtin) String() string {
func (a Call) String() string {
return fmt.Sprintf("call %d", uint32(a))
}
var (
_ Instruction = (*Builtin)(nil)
_ Instruction = (*Call)(nil)
)

+ 33
- 4
op_jump.go View File

@ -11,7 +11,7 @@ type Jump struct {
func (a Jump) Assemble() (RawInstruction, error) {
return RawInstruction{
Op: opClassJump | jumpOp,
Offset: a.Offset,
Offset: int16(a.Offset),
}, nil
}
@ -38,6 +38,35 @@ const (
JumpSignedLessOrEqual // K <= A
)
func (cond JumpTest) Match(dst, src uint64) bool {
switch cond {
case JumpEqual:
return dst == src
case JumpGreater:
return dst > src
case JumpGreaterOrEqual:
return dst >= src
case JumpSet:
return dst&src != 0
case JumpNotEqual:
return dst != src
case JumpSignedGreater:
return int64(dst) > int64(src)
case JumpSignedGreaterOrEqual:
return int64(dst) >= int64(src)
case JumpLess:
return dst < src
case JumpLessOrEqual:
return dst <= src
case JumpSignedLess:
return int64(dst) < int64(src)
case JumpSignedLessOrEqual:
return int64(dst) <= int64(src)
default:
return false
}
}
func (cond JumpTest) String() string {
switch cond {
case JumpEqual:
@ -159,15 +188,15 @@ func jumpToRaw(test JumpTest, immediate bool, dst Register, value uint32, offset
return RawInstruction{
Op: opClassJump | cond | jumpSourceImmediate,
Dst: dst,
Offset: offset,
Immediate: value,
Offset: int16(offset),
Immediate: uint64(value),
}, nil
}
return RawInstruction{
Op: opClassJump | cond | jumpSourceX,
Dst: dst,
Src: Register(value & 0x0f),
Offset: offset,
Offset: int16(offset),
}, nil
}


+ 30
- 26
op_load.go View File

@ -6,7 +6,8 @@ import (
type LoadConstant struct {
Dst Register
Value uint32
Value uint64
Size uint8
}
func (a LoadConstant) Assemble() (RawInstruction, error) {
@ -23,7 +24,7 @@ func (a LoadConstant) String() string {
type LoadAbsolute struct {
Dst, Src Register
Value uint32
Value uint64
Size uint8 // Size of the load value (in bytes).
}
@ -68,7 +69,7 @@ func (a LoadAbsolute) String() string {
type LoadIndirect struct {
Dst, Src Register
Value uint32
Value uint64
Size uint8 // Size of the load value (in bytes).
}
@ -111,13 +112,13 @@ func (a LoadIndirect) String() string {
return fmt.Sprintf("%s %s, %s, %#x", mnemonic, a.Src, a.Dst, a.Value)
}
type LoadMemory struct {
type LoadRegister struct {
Dst, Src Register
Offset uint16
Offset int16
Size uint8 // Size of the load value (in bytes).
}
func (a LoadMemory) Assemble() (RawInstruction, error) {
func (a LoadRegister) Assemble() (RawInstruction, error) {
var size Opcode
switch a.Size {
case 1:
@ -139,7 +140,7 @@ func (a LoadMemory) Assemble() (RawInstruction, error) {
}, nil
}
func (a LoadMemory) String() string {
func (a LoadRegister) String() string {
var mnemonic string
switch a.Size {
case 1:
@ -151,19 +152,19 @@ func (a LoadMemory) String() string {
case 8:
mnemonic = "ldxdw"
default:
return fmt.Sprintf("# invalid LoadMemory dst=%s, src=%s, size=%d, offset=%d", a.Dst, a.Src, a.Size, a.Offset)
return fmt.Sprintf("# invalid LoadRegister dst=%s, src=%s, size=%d, offset=%d", a.Dst, a.Src, a.Size, a.Offset)
}
return fmt.Sprintf("%s %s, [%s+%d]", mnemonic, a.Dst, a.Src, a.Offset)
}
type Store struct {
type StoreImmediate struct {
Dst Register
Offset uint16
Offset int16
Value uint32
Size uint8
}
func (a Store) Assemble() (RawInstruction, error) {
func (a StoreImmediate) Assemble() (RawInstruction, error) {
var size Opcode
switch a.Size {
case 1:
@ -175,18 +176,18 @@ func (a Store) Assemble() (RawInstruction, error) {
case 8:
size = sizeDoubleWord
default:
return RawInstruction{}, fmt.Errorf("ebpf: invalid store size %d", a.Size)
return RawInstruction{}, fmt.Errorf("ebpf: invalid store immediate size %d", a.Size)
}
return RawInstruction{
Op: opClassStore | size | modeMemory,
Dst: a.Dst,
Offset: a.Offset,
Immediate: a.Value,
Immediate: uint64(a.Value),
}, nil
}
func (a Store) String() string {
var mnemonic string
func (a StoreImmediate) String() string {
var mnemonic, sign string
switch a.Size {
case 1:
mnemonic = "stb"
@ -197,18 +198,21 @@ func (a Store) String() string {
case 8:
mnemonic = "stdw"
default:
return fmt.Sprintf("# invalid Store dst=%s, size=%d, offset=%d, value=%d", a.Dst, a.Size, a.Offset, a.Value)
return fmt.Sprintf("# invalid StoreImmediate dst=%s, size=%d, offset=%d, value=%#x", a.Dst, a.Size, a.Offset, a.Value)
}
if a.Offset >= 0 {
sign = "+"
}
return fmt.Sprintf("%s [%s+%d], %d", mnemonic, a.Dst, a.Offset, a.Value)
return fmt.Sprintf("%s [%s%s%#x], %#x", mnemonic, a.Dst, sign, a.Offset, a.Value)
}
type StoreMemory struct {
type StoreRegister struct {
Dst, Src Register
Offset uint16
Offset int16
Size uint8
}
func (a StoreMemory) Assemble() (RawInstruction, error) {
func (a StoreRegister) Assemble() (RawInstruction, error) {
var size Opcode
switch a.Size {
case 1:
@ -230,7 +234,7 @@ func (a StoreMemory) Assemble() (RawInstruction, error) {
}, nil
}
func (a StoreMemory) String() string {
func (a StoreRegister) String() string {
var mnemonic string
switch a.Size {
case 1:
@ -242,16 +246,16 @@ func (a StoreMemory) String() string {
case 8:
mnemonic = "stxdw"
default:
return fmt.Sprintf("# invalid StoreMemory dst=%s, src=%s, size=%d, offset=%d", a.Dst, a.Src, a.Size, a.Offset)
return fmt.Sprintf("# invalid StoreRegister dst=%s, src=%s, size=%d, offset=%#x", a.Dst, a.Src, a.Size, a.Offset)
}
return fmt.Sprintf("%s [%s+%d], %s", mnemonic, a.Dst, a.Offset, a.Src)
return fmt.Sprintf("%s [%s+%#x], %s", mnemonic, a.Dst, a.Offset, a.Src)
}
var (
_ Instruction = (*LoadConstant)(nil)
_ Instruction = (*LoadAbsolute)(nil)
_ Instruction = (*LoadIndirect)(nil)
_ Instruction = (*LoadMemory)(nil)
_ Instruction = (*Store)(nil)
_ Instruction = (*StoreMemory)(nil)
_ Instruction = (*LoadRegister)(nil)
_ Instruction = (*StoreImmediate)(nil)
_ Instruction = (*StoreRegister)(nil)
)

+ 12
- 12
op_load_test.go View File

@ -158,7 +158,7 @@ func TestLoadMemory(t *testing.T) {
op := uint8(opClassLoadX | modeMemory)
tests := testInstructionSuite{
{
Test: LoadMemory{
Test: LoadRegister{
Dst: R0,
Src: R1,
Size: 1,
@ -173,7 +173,7 @@ func TestLoadMemory(t *testing.T) {
WantString: "ldxb r0, [r1+255]",
},
{
Test: LoadMemory{
Test: LoadRegister{
Dst: R0,
Src: R1,
Size: 2,
@ -188,7 +188,7 @@ func TestLoadMemory(t *testing.T) {
WantString: "ldxh r0, [r1+65535]",
},
{
Test: LoadMemory{
Test: LoadRegister{
Dst: R0,
Src: R1,
Size: 4,
@ -203,7 +203,7 @@ func TestLoadMemory(t *testing.T) {
WantString: "ldxw r0, [r1+4660]",
},
{
Test: LoadMemory{
Test: LoadRegister{
Dst: R0,
Src: R1,
Size: 8,
@ -225,7 +225,7 @@ func TestStore(t *testing.T) {
op := uint8(opClassStore | modeMemory)
tests := testInstructionSuite{
{
Test: Store{
Test: StoreImmediate{
Dst: R0,
Value: 0xff,
Size: 1,
@ -240,7 +240,7 @@ func TestStore(t *testing.T) {
WantString: "stb [r0+255], 255",
},
{
Test: Store{
Test: StoreImmediate{
Dst: R0,
Size: 2,
Offset: 0xffff,
@ -253,7 +253,7 @@ func TestStore(t *testing.T) {
WantString: "sth [r0+65535], 0",
},
{
Test: Store{
Test: StoreImmediate{
Dst: R0,
Size: 4,
Value: math.MaxUint32,
@ -268,7 +268,7 @@ func TestStore(t *testing.T) {
WantString: "stw [r0+4660], 4294967295",
},
{
Test: Store{
Test: StoreImmediate{
Dst: R0,
Size: 8,
Offset: math.MaxUint16,
@ -288,7 +288,7 @@ func TestStoreMemory(t *testing.T) {
op := uint8(opClassStoreX | modeMemory)
tests := testInstructionSuite{
{
Test: StoreMemory{
Test: StoreRegister{
Dst: R0,
Src: R1,
Size: 1,
@ -303,7 +303,7 @@ func TestStoreMemory(t *testing.T) {
WantString: "stxb [r0+255], r1",
},
{
Test: StoreMemory{
Test: StoreRegister{
Dst: R0,
Src: R1,
Size: 2,
@ -318,7 +318,7 @@ func TestStoreMemory(t *testing.T) {
WantString: "stxh [r0+65535], r1",
},
{
Test: StoreMemory{
Test: StoreRegister{
Dst: R0,
Src: R1,
Size: 4,
@ -333,7 +333,7 @@ func TestStoreMemory(t *testing.T) {
WantString: "stxw [r0+4660], r1",
},
{
Test: StoreMemory{
Test: StoreRegister{
Dst: R0,
Src: R1,
Size: 8,


+ 9
- 4
program.go View File

@ -27,6 +27,11 @@ func (p Program) Assemble() ([]RawInstruction, error) {
return ret, nil
}
func (p Program) Verify() error {
// TODO
return nil
}
// Assemble a program from source.
func Assemble(source string) (Program, error) {
var (
@ -264,19 +269,19 @@ func (asm *assembler) VisitAluOperation(ctx *parser.AluOperationContext) interfa
*/
op := asm.Visit(ctx.AluOperator()).(ALUOp)
switch {
case ctx.Immediate() != nil: // aluOperator register ',' immediate
case ctx.Immediate() != nil: // aluOperator Register ',' immediate
return ALUOpConstant{
Op: op,
Dst: asm.Visit(ctx.Register(0)).(Register),
Value: asm.Visit(ctx.Immediate()).(uint32),
}
case ctx.Register(1) != nil: // aluOperator register ',' register
case ctx.Register(1) != nil: // aluOperator Register ',' Register
return ALUOpRegister{
Op: op,
Dst: asm.Visit(ctx.Register(0)).(Register),
Src: asm.Visit(ctx.Register(1)).(Register),
}
case ctx.Register(0) != nil: // aluOperator register
case ctx.Register(0) != nil: // aluOperator Register
return Negate{
Dst: asm.Visit(ctx.Register(0)).(Register),
}
@ -433,7 +438,7 @@ func (asm *assembler) VisitImmediate(ctx *parser.ImmediateContext) interface{} {
return uint32(u)
}
// Visit a parse tree produced by AssemblerParser#register.
// Visit a parse tree produced by AssemblerParser#Register.
func (asm *assembler) VisitRegister(ctx *parser.RegisterContext) interface{} {
switch {
case ctx.REGISTER() != nil:


+ 3
- 3
register.go View File

@ -2,11 +2,11 @@ package ebpf
import "fmt"
// Register is the source or destination register.
// Register is the source or destination Register.
type Register uint8
const (
// Exit value register.
// Exit value Register.
R0 Register = iota
// Function argument registers.
R1
@ -19,7 +19,7 @@ const (
R7
R8
R9
// Read-only frame pointer to access stack.
// Read-only frame pointer to access Stack.
R10
// Named registers.
RAX = R0


+ 284
- 0
vm.go View File

@ -0,0 +1,284 @@
package ebpf
import (
"encoding/binary"
"errors"
"fmt"
"log"
"math"
"unsafe"
)
var (
ErrNoProgram = errors.New("ebpf: no program is loaded")
errDivisionByZero = errors.New("division by zero")
)
const (
// stackSize (in bytes)
stackSize = 512
//stackAddr = math.MaxUint16 - stackSize - 1
//memoryAddr = math.MaxUint16 + 1
// maxInstructions is the maximum number of instructions the VM accepts.
maxInstructions = 4096
)
type Func func(r1, r2, r3, r4, r5 uint64) uint64
type VM struct {
// Register memory.
Register [11]uint64
// Stack memory.
Stack [stackSize]byte
// PC is the program counter.
PC uint32
// Tracer provides callbacks for the VM.
Tracer
memory []byte
memoryAddr uint64
memorySize uint64
stackAddr uint64
program Program
funcs map[Call]Func
}
func New() *VM {
return &VM{
funcs: make(map[Call]Func),
}
}
func (vm *VM) Load(program Program) error {
if l := len(program); l > maxInstructions {
return fmt.Errorf("ebpf: %d instructions exceeds maximum of %d", l, maxInstructions)
}
if err := program.Verify(); err != nil {
return err
}
vm.program = make(Program, len(program))
copy(vm.program, program)
return nil
}
// Attach a function.
func (vm *VM) Attach(call Call, fn Func) {
vm.funcs[call] = fn
}
// Run the eBPF program that was loaded.
func (vm *VM) Run(memory []byte) (uint64, error) {
if len(vm.program) == 0 {
return math.MaxUint64, ErrNoProgram
}
// Reset register and stack.
vm.reset(memory)
// Trace start.
if vm.Tracer != nil {
vm.TraceStart()
}
execution:
for vm.PC < uint32(len(vm.program)) {
var (
pc = vm.PC
in = vm.program[pc]
err error
)
if vm.Tracer != nil {
vm.TracePre(in)
}
switch in := in.(type) {
case ALUOpConstant:
err = vm.aluOp(false, in.Dst, in.Op, uint64(in.Value))
case ALUOpRegister:
err = vm.aluOp(false, in.Dst, in.Op, uint64(uint32(vm.Register[in.Src])))
case ALU64OpConstant:
err = vm.aluOp(true, in.Dst, in.Op, uint64(in.Value))
case ALU64OpRegister:
err = vm.aluOp(true, in.Dst, in.Op, vm.Register[in.Src])
case Negate:
vm.Register[in.Dst] = uint64(^uint32(vm.Register[in.Dst]))
case Negate64:
vm.Register[in.Dst] = ^vm.Register[in.Dst]
case Call:
err = vm.call(in)
case Exit:
break execution
case Jump:
vm.PC += uint32(in.Offset)
err = vm.checkPC()
case JumpIf:
if in.Cond.Match(vm.Register[in.Dst], uint64(in.Value)) {
vm.PC += uint32(in.Offset)
err = vm.checkPC()
}
case JumpIfX:
if in.Cond.Match(vm.Register[in.Dst], vm.Register[in.Src]) {
vm.PC += uint32(in.Offset)
err = vm.checkPC()
}
case LoadConstant:
vm.Register[in.Dst] = in.Value
case LoadIndirect: // TODO(maze): intended to be used in socket filters, and are therefore not general-purpose
case LoadAbsolute: // TODO(maze): intended to be used in socket filters, and are therefore not general-purpose
case LoadRegister:
vm.Register[in.Dst], err = vm.load(uint64(int64(vm.Register[in.Src])+int64(in.Offset)), in.Size)
case StoreImmediate:
err = vm.store(uint64(int64(vm.Register[in.Dst])+int64(in.Offset)), uint64(in.Value), in.Size)
case StoreRegister:
err = vm.store(uint64(int64(vm.Register[in.Dst])+int64(in.Offset)), vm.Register[in.Src], in.Size)
case ByteSwap:
vm.Register[in.Dst] = in.Swap(vm.Register[in.Dst])
default:
err = fmt.Errorf("unhandled instruction %q", in)
}
if err != nil {
return math.MaxUint64, fmt.Errorf("ebpf: at PC=%#04x: %w", pc, err)
}
if vm.Tracer != nil {
vm.TracePost(in)
}
vm.PC++
}
if vm.Tracer != nil {
vm.TraceEnded()
}
return vm.Register[R0], nil
}
func (vm *VM) aluOp(wide bool, dst Register, op ALUOp, value uint64) error {
switch op {
case ALUOpAdd:
vm.Register[dst] += value
case ALUOpSub:
vm.Register[dst] -= value
case ALUOpMul:
vm.Register[dst] *= value
case ALUOpDiv:
if value == 0 {
return errDivisionByZero
}
vm.Register[dst] /= value
case ALUOpOr:
vm.Register[dst] |= value
case ALUOpAnd:
vm.Register[dst] &= value
case ALUOpShiftLeft:
vm.Register[dst] <<= value
case ALUOpShiftRight:
vm.Register[dst] >>= value
case ALUOpMod:
if value == 0 {
return errDivisionByZero
}
vm.Register[dst] %= value
case ALUOpXor:
vm.Register[dst] ^= value
case ALUOpMove:
vm.Register[dst] = value
case ALUOpArithmicShiftRight:
vm.Register[dst] = uint64(int64(vm.Register[dst]) >> value)
}
if !wide {
vm.Register[dst] &= math.MaxUint32
}
return nil
}
func (vm *VM) call(call Call) error {
if fn, ok := vm.funcs[call]; ok {
vm.Register[R0] = fn(vm.Register[R1], vm.Register[R2], vm.Register[R3], vm.Register[R4], vm.Register[R5])
return nil
}
return fmt.Errorf("no function %#08x registered", uint32(call))
}
func (vm *VM) checkPC() error {
if vm.PC >= uint32(len(vm.program)) {
// TODO is this needed?
}
return nil
}
func (vm *VM) load(addr uint64, size uint8) (out uint64, err error) {
var memory []byte
if memory, addr, err = vm.checkMemory(addr, size, "load"); err != nil {
return
}
switch size {
case 1:
return uint64(memory[addr]), nil
case 2:
return uint64(binary.LittleEndian.Uint16(memory[addr:])), nil
case 4:
return uint64(binary.LittleEndian.Uint32(memory[addr:])), nil
case 8:
return binary.LittleEndian.Uint64(memory[addr:]), nil
default:
panic("unreachable")
}
}
func (vm *VM) store(addr, value uint64, size uint8) (err error) {
log.Printf("store at %#x: %#x (size %d)", addr, value, size)
var memory []byte
if memory, addr, err = vm.checkMemory(addr, size, "store"); err != nil {
return err
}
switch size {
case 1:
memory[addr] = uint8(value)
case 2:
binary.LittleEndian.PutUint16(memory[addr:], uint16(value))
case 4:
binary.LittleEndian.PutUint32(memory[addr:], uint32(value))
case 8:
binary.LittleEndian.PutUint64(memory[addr:], value)
}
return nil
}
func (vm *VM) checkMemory(addr uint64, size uint8, operation string) ([]byte, uint64, error) {
if vm.memoryAddr <= addr && addr+uint64(size) <= vm.memoryAddr+vm.memorySize {
// Memory access
return vm.memory, addr - vm.memoryAddr, nil
}
if vm.stackAddr <= addr && addr+uint64(size) <= vm.stackAddr+stackSize {
// Stack access
return vm.Stack[:], addr - vm.stackAddr, nil
}
return nil, 0, fmt.Errorf("out of bounds memory %s at %#x+%d", operation, addr, size)
}
var (
emptyRegisters [11]uint64
emptyStack [stackSize]byte
)
func (vm *VM) reset(memory []byte) {
copy(vm.Register[:], emptyRegisters[:])
copy(vm.Stack[:], emptyStack[:])
vm.memory = memory
vm.memoryAddr = uint64(uintptr(unsafe.Pointer(&memory[0])))
vm.memorySize = uint64(len(memory))
vm.stackAddr = uint64(uintptr(unsafe.Pointer(&vm.Stack[0])))
vm.Register[R1] = vm.memoryAddr
vm.Register[R10] = vm.stackAddr + stackSize - 1 // end of stack
}
type Tracer interface {
TraceStart()
TracePre(instruction Instruction)
TracePost(instruction Instruction)
TraceEnded()
}

Loading…
Cancel
Save