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: