|
| 1 | +# Integer Semantics |
| 2 | + |
| 3 | +PythonBPF programs are Python syntax, but the integers in them behave as C integers: the |
| 4 | +program runs in the kernel as BPF bytecode, where every value is a fixed-width machine |
| 5 | +word. This page describes the rules the compiler applies. They are C's rules, applied to |
| 6 | +the `ctypes` types you declare, so a program's arithmetic matches what the equivalent C |
| 7 | +program compiled with clang would compute. |
| 8 | + |
| 9 | +```{note} |
| 10 | +This is one of the few places where PythonBPF deliberately differs from Python. Python |
| 11 | +integers have arbitrary precision and no unsigned types; BPF has neither. The |
| 12 | +[divergences from Python](#divergences-from-python) are listed at the end of this page. |
| 13 | +``` |
| 14 | + |
| 15 | +## Types |
| 16 | + |
| 17 | +An integer's type is the `ctypes` type it was declared with, and the type carries both |
| 18 | +a width and a sign: |
| 19 | + |
| 20 | +| Signed | Unsigned | Width | |
| 21 | +|---|---|---| |
| 22 | +| `c_int8` | `c_uint8` | 8 | |
| 23 | +| `c_int16` | `c_uint16` | 16 | |
| 24 | +| `c_int32` | `c_uint32` | 32 | |
| 25 | +| `c_int64` | `c_uint64` | 64 | |
| 26 | + |
| 27 | +Every declaration site uses these types: local variables initialised with a constructor |
| 28 | +call, `@bpfglobal` variables, `@struct` fields, map keys and values, and fields read from |
| 29 | +`vmlinux` structures. Helper functions return the type of the kernel's signature, so |
| 30 | +`pid()` and `ktime()` are unsigned while `probe_read`-style helpers return a signed |
| 31 | +`long`. |
| 32 | + |
| 33 | +```python |
| 34 | +count = c_uint32(0) # a 32-bit unsigned local |
| 35 | +delta = c_int64(-1) # a 64-bit signed local |
| 36 | +``` |
| 37 | + |
| 38 | +A local assigned without a constructor takes its type from the expression: |
| 39 | + |
| 40 | +```python |
| 41 | +now = ktime() # c_uint64, the helper's return type |
| 42 | +total = count + 1 # the type of the addition (see below), held in a 64-bit slot |
| 43 | +``` |
| 44 | + |
| 45 | +Undeclared locals are always 64 bits wide; the inferred type only decides their sign. |
| 46 | +Declare the local with a constructor when a narrower width matters. |
| 47 | + |
| 48 | +### Literals |
| 49 | + |
| 50 | +A literal has the type a C compiler gives it: `int` (32-bit signed) if the value fits, |
| 51 | +`long long` (64-bit signed) otherwise. This matters for mixed arithmetic: in |
| 52 | +`count / -2` with `count` a `c_uint32`, the literal `-2` is a 32-bit `int`, so the |
| 53 | +division happens in `c_uint32` exactly as it would in C. |
| 54 | + |
| 55 | +## Assignment and conversion |
| 56 | + |
| 57 | +Assigning a value to a variable of a different integer type converts it, and the |
| 58 | +variable's declared type is what the stored value *is* afterwards: |
| 59 | + |
| 60 | +* **Widening preserves the value.** The conversion looks at the *source*'s sign: an |
| 61 | + unsigned source is zero-extended, a signed source is sign-extended. So a `c_uint32` |
| 62 | + holding `0xFFFFFFFF` stored into a `c_int64` gives `4294967295`, and a `c_int32` |
| 63 | + holding `-1` stored into a `c_uint64` gives `0xFFFFFFFFFFFFFFFF`. This is what C and |
| 64 | + `ctypes` both do. |
| 65 | +* **Narrowing truncates.** Only the low bits survive. |
| 66 | +* **Same width reinterprets.** A `c_uint32` `0xFFFFFFFF` stored into a `c_int32` reads |
| 67 | + as `-1`. |
| 68 | + |
| 69 | +The same rules apply to explicit conversions written as constructor calls |
| 70 | +(`c_int64(count)`), to struct field stores and to `return`. |
| 71 | + |
| 72 | +## Arithmetic |
| 73 | + |
| 74 | +Each binary operation is typed on its own, from its two operands, following C's usual |
| 75 | +arithmetic conversions: |
| 76 | + |
| 77 | +1. Operands narrower than 32 bits are promoted to `c_int32`. |
| 78 | +2. If both operands have the same sign, the result has the wider width and that sign. |
| 79 | +3. If the signs differ, the unsigned type wins when it is at least as wide as the signed |
| 80 | + one; otherwise the signed type wins. |
| 81 | + |
| 82 | +The operation is then performed in that type, and its result has that type. The variable |
| 83 | +receiving the result plays no part until the final store. Two consequences worth knowing: |
| 84 | + |
| 85 | +* **Intermediate results wrap at their own width.** `c_uint32(0x80000000) * c_uint32(2)` |
| 86 | + is a `c_uint32` multiplication, so it wraps to `0` before being stored, even if the |
| 87 | + destination is a `c_uint64`. Widen an operand first if you want a 64-bit product. |
| 88 | +* **Mixed signs go unsigned.** `c_uint32(10) / c_int32(-2)` is an unsigned division by |
| 89 | + `0xFFFFFFFE`, giving `0`, not `-5`. |
| 90 | + |
| 91 | +The sign of the operation's type selects the instruction for the operations where it |
| 92 | +matters: |
| 93 | + |
| 94 | +| Operator | Signed type | Unsigned type | |
| 95 | +|---|---|---| |
| 96 | +| `/`, `//` | truncating signed division | unsigned division | |
| 97 | +| `%` | remainder with the dividend's sign | unsigned remainder | |
| 98 | +| `>>` | arithmetic shift (sign bit shifts in) | logical shift (zeros shift in) | |
| 99 | +| `<`, `<=`, `>`, `>=` | signed comparison | unsigned comparison | |
| 100 | + |
| 101 | +`+`, `-`, `*`, `<<`, `&`, `|`, `^`, `==` and `!=` produce the same bits for either sign. |
| 102 | + |
| 103 | +Unary minus on an unsigned value follows C too: `-x` is `2^N - x` in the value's type. |
| 104 | + |
| 105 | +## Comparisons |
| 106 | + |
| 107 | +A comparison converts both operands with the same usual arithmetic conversions and then |
| 108 | +compares in the resulting type. `c_uint64(10) > c_int64(-1)` is therefore an unsigned |
| 109 | +comparison in which `-1` is the largest possible value, and the result is false. The |
| 110 | +result of a comparison is `1` or `0`, as in C. |
| 111 | + |
| 112 | +## Divergences from Python |
| 113 | + |
| 114 | +Because the semantics are C's, some Python behaviour does not carry over: |
| 115 | + |
| 116 | +* `/` is integer division; there is no floating-point result. |
| 117 | +* `//` and `/` are the same operation, and both truncate toward zero: `-7 // 2` is `-3`, |
| 118 | + where Python gives `-4`. |
| 119 | +* `%` takes the sign of the dividend: `-7 % 2` is `-1`, where Python gives `1`. |
| 120 | +* Integers have a fixed width and wrap on overflow; there is no arbitrary precision. |
| 121 | +* Unsigned types exist, and mixing them with signed values follows C's conversions |
| 122 | + rather than Python's mathematical integers. |
| 123 | + |
| 124 | +The test programs under `tests/passing_tests/signedness/` show each rule with its |
| 125 | +expected value, and `tests/c-form/signedness.bpf.c` is the equivalent C program. |
0 commit comments