diff --git a/ebpfcat/arraymap.py b/ebpfcat/arraymap.py index 9552eb35d5c2995297f2b02d7354e3c233e64e11..88bec70dfdbdd14e5abef7e4fa04ce8a59117c89 100644 --- a/ebpfcat/arraymap.py +++ b/ebpfcat/arraymap.py @@ -45,6 +45,7 @@ class ArrayGlobalVarDesc(MemoryDesc): class ArrayMapAccess: + """This is the array map proper""" def __init__(self, fd, write_size, size): self.fd = fd self.write_size = write_size @@ -52,12 +53,31 @@ class ArrayMapAccess: self.data = bytearray(size) def read(self): + """read all variables in the map from EBPF to user space""" self.data = lookup_elem(self.fd, b"\0\0\0\0", self.size) def write(self): + """write all variables in the map from user space to EBPF + + *all* variables are written, even those not marked ``write=True`` + """ update_elem(self.fd, b"\0\0\0\0", self.data, 0) def readwrite(self): + """read variables from EBPF and write them out immediately + + This reads all variables, swaps in the user-modified values for + the ``write=True`` variables, and writes the out again + immediately. + + This means that even the read-only variables will be overwritten + with the values we have just read. If the EBPF program changed + the value in the meantime, that may be a problem. + + Note that after this method returns, all *write* variables + will have the value from the EBPF program in user space, and + vice-versa. + """ write = self.data[:self.write_size] data = lookup_elem(self.fd, b"\0\0\0\0", self.size) self.data[:] = data @@ -66,6 +86,7 @@ class ArrayMapAccess: class ArrayMap(Map): + """A descriptor for an array map""" def globalVar(self, fmt="I", write=False): return ArrayGlobalVarDesc(self, fmt, write) diff --git a/ebpfcat/bpf.py b/ebpfcat/bpf.py index c1208a0872153eebb3a984d2b99b5d56ae29f520..9e055071c563dbc8f5f754a0c6e7ba1208400176 100644 --- a/ebpfcat/bpf.py +++ b/ebpfcat/bpf.py @@ -1,3 +1,6 @@ +"""\ +A module that wraps the `bpf` system call in Python, using `ctypes`. +""" from ctypes import CDLL, c_int, get_errno, cast, c_void_p, create_string_buffer, c_char_p, addressof, c_char from enum import Enum from struct import pack, unpack diff --git a/ebpfcat/ebpf.py b/ebpfcat/ebpf.py index 090dc42e3c0fc92de8be0c55907ec9f4f3b15f58..a12801421d448bff362c2fbdade14f7c95ed84ad 100644 --- a/ebpfcat/ebpf.py +++ b/ebpfcat/ebpf.py @@ -1,3 +1,4 @@ +from abc import ABC, abstractmethod from collections import namedtuple from contextlib import contextmanager, ExitStack from struct import pack, unpack, calcsize @@ -258,7 +259,9 @@ def comparison(uposop, unegop, sposop=None, snegop=None): return ret -class Comparison: +class Comparison(ABC): + """Base class for all logical operations""" + def __init__(self, ebpf): self.ebpf = ebpf self.else_origin = None @@ -279,9 +282,25 @@ class Comparison: self.ebpf.owners, self.owners = \ self.ebpf.owners & self.owners, self.ebpf.owners - def retarget_one(self): - op, dst, src, off, imm = self.ebpf.opcodes[self.origin] - self.ebpf.opcodes[self.origin] = Instruction(op, dst, src, off+1, imm) + @abstractmethod + def compare(self, negative): + """issue the actual comparison code + + the issued code should either jump to a position later + determined by the `target` method, or just fall through. + If `negative` is true, the code should jump away if the + condition in question is false, and vice versa. + """ + raise NotImplementedError + + @abstractmethod + def target(self, retarget=False): + """modify the already issued jumps to jump here + + you may re-set the target a second time, but then `retarget` needs + to be true. + """ + raise NotImplementedError def Else(self): self.else_origin = len(self.ebpf.opcodes) @@ -303,6 +322,8 @@ class Comparison: class SimpleComparison(Comparison): + """A simple numerical comparison, results in a jump instruction""" + def __init__(self, ebpf, left, right, opcode): super().__init__(ebpf) self.left = left @@ -385,6 +406,7 @@ def rbinary(opcode): class Expression: + """the base class for all numerical expressions""" __radd__ = __add__ = binary(Opcode.ADD) __sub__ = binary(Opcode.SUB) __rsub__ = rbinary(Opcode.SUB) @@ -428,6 +450,32 @@ class Expression: @contextmanager def calculate(self, dst, long, signed, force=False): + """issue the code that calculates the value of this expression + + this method returns three values: + + - the number of the register with the result + - a boolean indicating whether this is a 64 bit value + - and a booleand indicating whether the result is to be + considered signed. + + this method is a contextmanager to be used in a `with` + statement. At the end of the `with` block the result is + freed again, i.e. the register will not be reserved for the + result anymore. + + the default implementation calls `get_address` for values + which actually are in memory and moves that into a register. + + :param dst: the number of the register to put the result in, + or `None` if that does not matter. + :param long: True if the result is supposed to be 64 bit. None + if it does not matter. + :param signed: True if the result should be considered signed. + None if it does not matter. + :param force: if true, `dst` must be respected, otherwise this + is optional. + """ with self.ebpf.get_free_register(dst) as dst: with self.get_address(dst, long, signed) as (src, fmt): self.ebpf.append(Opcode.LD + Memory.fmt_to_opcode[fmt], @@ -436,16 +484,29 @@ class Expression: @contextmanager def get_address(self, dst, long, signed, force=False): + """get the address of the value of this expression + + this method returns the address of the result of this expression, + and its format letter. The default implementation uses + `calculate` to evaluate the expression and pushes the result onto + the stack. + """ with self.ebpf.get_stack(4 + 4 * long) as stack: with self.calculate(dst, long, signed) as (src, _, _): self.ebpf.append(Opcode.STX + Opcode.DW * long, 10, src, stack, 0) - self.ebpf.append(Opcode.MOV + Opcode.LONG + Opcode.REG, dst, 10, 0, 0) + self.ebpf.append(Opcode.MOV + Opcode.LONG + Opcode.REG, + dst, 10, 0, 0) self.ebpf.append(Opcode.ADD + Opcode.LONG, dst, 0, 0, stack) - yield + yield dst, "Q" if long else "I" + + def contains(self, no): + """return whether this expression contains the register `no`""" + return False class Binary(Expression): + """represent all binary expressions""" def __init__(self, ebpf, left, right, operator): self.ebpf = ebpf self.left = left @@ -532,6 +593,10 @@ class Negate(Expression): class Sum(Binary): + """represent the sum of one register and a constant value + + this is used to optimize memory addressing code. + """ def __init__(self, ebpf, left, right): super().__init__(ebpf, left, right, Opcode.ADD) @@ -551,6 +616,7 @@ class Sum(Binary): class AndExpression(SimpleComparison, Binary): + """The & operator may also be used as a comparison""" def __init__(self, ebpf, left, right): Binary.__init__(self, ebpf, left, right, Opcode.AND) SimpleComparison.__init__(self, ebpf, left, right, Opcode.JSET) @@ -580,15 +646,18 @@ class AndExpression(SimpleComparison, Binary): len(self.ebpf.opcodes) - self.else_origin + 1, imm) def Else(self): - if self.ebpf.opcodes[self.origin][0] == Opcode.JMP: + op, dst, src, off, imm = self.ebpf.opcodes[self.origin] + if op is Opcode.JMP: self.invert = self.origin else: - self.retarget_one() + self.ebpf.opcodes[self.origin] = \ + Instruction(op, dst, src, off+1, imm) self.else_origin = len(self.ebpf.opcodes) self.ebpf.opcodes.append(None) return self class Register(Expression): + """represent one EBPF register""" offset = 0 def __init__(self, no, ebpf, long, signed): @@ -613,8 +682,6 @@ class Register(Expression): @contextmanager def calculate(self, dst, long, signed, force=False): - #if signed is not None and signed != self.signed: - # raise AssembleError("cannot compile") if self.no not in self.ebpf.owners: raise AssembleError("register has no value") if dst != self.no and force: @@ -629,6 +696,7 @@ class Register(Expression): class IAdd: + """represent an in-place addition""" def __init__(self, value): self.value = value @@ -678,6 +746,12 @@ class Memory(Expression): class MemoryDesc: + """A base class used by descriptors for memory + + All memory access is relative to a base register. This is + defined by the member variable `base_register` in deriving + classes. + """ def __get__(self, instance, owner): if instance is None: return self @@ -710,6 +784,7 @@ class MemoryDesc: class LocalVar(MemoryDesc): + """variables on the stack""" base_register = 10 def __init__(self, fmt='I'): @@ -771,15 +846,18 @@ class MemoryMap: return Memory(self.ebpf, self.fmt, addr, self.signed, self.long) -class Map: +class Map(ABC): + """The base class for all maps""" + @abstractmethod def init(self, ebpf): - pass + """create the map and initialize its values""" def load(self, ebpf): - pass + """called after the program has been loaded""" class PseudoFd(Expression): + """represent a file descriptor to a map""" def __init__(self, ebpf, fd): self.ebpf = ebpf self.fd = fd @@ -793,6 +871,7 @@ class PseudoFd(Expression): class ktime(Expression): + """a function that returns the current ktime in ns""" def __init__(self, ebpf): self.ebpf = ebpf @@ -880,6 +959,11 @@ class TemporaryDesc(RegisterDesc): class EBPF: + """The base class for all EBPF programs + + This class may even be instantiated directly, in which case you + can just issue the program before the it is loaded. + """ stack = 0 name = None license = None @@ -920,12 +1004,13 @@ class EBPF: v.init(self) def program(self): - pass + """overwrite this method with your program while subclassing""" def append(self, opcode, dst, src, off, imm): self.opcodes.append(Instruction(opcode, dst, src, off, imm)) def assemble(self): + """return the assembled program""" self.program() return b"".join( pack("<BBHI", i.opcode.value, i.dst | i.src << 4, @@ -933,6 +1018,7 @@ class EBPF: for i in self.opcodes) def load(self, log_level=0, log_size=10 * 4096): + """load the program into the kernel""" ret = bpf.prog_load(self.prog_type, self.assemble(), self.license, log_level, log_size, self.kern_version, name=self.name) @@ -945,10 +1031,12 @@ class EBPF: return ret def jumpIf(self, comp): + """jump if `comp` is true to a later defined `target`""" comp.compare(False) return comp def jump(self): + """unconditionally jump to a later defined `target`""" comp = SimpleComparison(self, None, 0, Opcode.JMP) comp.origin = len(self.opcodes) comp.dst = 0 @@ -958,15 +1046,18 @@ class EBPF: return comp def get_fd(self, fd): + """return the file descriptor `fd` of a map""" return PseudoFd(self, fd) def call(self, no): + """call the kernel function `no` from enum `FuncId`""" assert isinstance(no, FuncId) self.append(Opcode.CALL, 0, 0, 0, no.value) self.owners.add(0) self.owners -= set(range(1, 6)) def exit(self, no=None): + """Exit the program with return value `no`""" if no is not None: self.r0 = no.value self.append(Opcode.EXIT, 0, 0, 0, 0) diff --git a/ebpfcat/ebpf.rst b/ebpfcat/ebpf.rst index d1dab1cc2985633d5fd4f9945ddc963dabe401a0..0548159875f5521a9e02ff881a81c5dd2dbdfad5 100644 --- a/ebpfcat/ebpf.rst +++ b/ebpfcat/ebpf.rst @@ -98,9 +98,9 @@ generate code that the static code checker understands, like so:: self.some_variable = p.pH[22] # read word at position 22 in this code, the variable ``p`` returned by the ``with`` statement also -allows to access the content of the packet. There are eight access modes +allows to access the content of the packet. There are six access modes to access different sizes in the packet, whose naming follows the Python -``struct`` module, indicated by the letters "BHIQbhiq". +``struct`` module, indicated by the letters "BHIQiq". Knowing this, we can modify the above example code to only count IP packets:: @@ -112,3 +112,48 @@ packets:: with p.pH[12] == 8: self.count += 1 self.exit(XDPExitCode.PASS) + +Maps +---- + +Maps are used to communicate to the outside world. They look like instance +variables. They may be used from within the EBPF program, and once it is +loaded also from everywhere else. There are two flavors: `hashmap.HashMap` +and `arraymap.ArrayMap`. They have different use cases: + +Hash Maps +~~~~~~~~~ + +all hash map variables have a fixed size of 8 bytes. Accessing them is +rather slow, but is done with proper locking: concurrent access is possible. +When accessing them from user space, they are read from the kernel each time +anew. They are declared as follows:: + + class MyProgram(EBPF): + hash_map = HashMap() + a_variable = hash_map.globalVar() + +They are used as normal variables, like in `self.a_variable = 5`, both +in EBPF and from user space once loaded. + +Array Maps +~~~~~~~~~~ + +from an EBPF program's perspective, all EPBF programs are accessing the same +variables at the same time. So concurrent access may lead to problems. An +exception is the in-place addition operator `+=`, which works under a lock, +but only if the variable is of 4 or 8 bytes size. + +Otherwise variables may be declared in all sizes. Additionally, one can mark +which variables are supposed to be written from user space to the EBPF, +as opposed to just being read. The declaration is like so:: + + class MyProgram(EBPF): + array_map = ArrayMap() + a_read_variable = array_map("B") # one byte read-only variable + a_write_variable = array_map("i", write=True) # a read-write integer + +the array map has methods to access the variables: + +.. autoclass:: ebpfcat.arraymap.ArrayMapAccess + :members: diff --git a/ebpfcat/ethercat.py b/ebpfcat/ethercat.py index c9d2956c397c6a701d083eda10d67800eb4da8cb..093f8d64111227c01150a4e45e739b7cf01bedd2 100644 --- a/ebpfcat/ethercat.py +++ b/ebpfcat/ethercat.py @@ -1,4 +1,6 @@ -"""Low-level access to EtherCAT +"""\ +Low-level access to EtherCAT +============================ this modules contains the code to actually talk to EtherCAT terminals. """ diff --git a/ebpfcat/ethercat.rst b/ebpfcat/ethercat.rst index 1b041195facf3386d7142b58343f1ad24244515f..004991e698bad161825e9f0884228c35b4efef7f 100644 --- a/ebpfcat/ethercat.rst +++ b/ebpfcat/ethercat.rst @@ -94,6 +94,9 @@ The communication with the terminals can happen in three different ways: frequency exceeds 10 kHz. +Reference Documentation +----------------------- + .. automodule:: ebpfcat.devices :members: