Repository navigation
Expand file tree
/
Copy pathtype_deducer.py
More file actions
175 lines (140 loc) · 5.76 KB
/
Copy pathtype_deducer.py
File metadata and controls
175 lines (140 loc) · 5.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
from llvmlite import ir
class IntTy(ir.IntType):
"""An LLVM integer type that also remembers its signedness.
LLVM integer types are sign-agnostic by design: `i32` is just 32 bits, and
the sign lives in the operations (sdiv/udiv, sext/zext, icmp s*/u*). The
frontend therefore has to carry it. IntTy is a plain ir.IntType for every
purpose LLVM cares about -- it renders as `i32`, compares and hashes equal
to ir.IntType(32), and passes every isinstance check -- with one extra
attribute the compiler reads when choosing between signed and unsigned
forms of an operation.
Invariant: the sign is read only from a *descriptor* -- a Symbol.ir_type
or the type half of an eval_expr result -- never from `value.type`. Values
produced by the IRBuilder (loads, arithmetic, extensions) come back with a
plain ir.IntType, so a sign on a value's own type is lost at the first
operation. Descriptors are constructed by the compiler; that is where the
sign lives.
"""
def __new__(cls, bits: int, signed: bool = True):
# ir.IntType.__new__ memoises one instance per width in a cache shared
# with subclasses. Going through it would (a) merge the signed and
# unsigned flavours of a width into one object and (b) plant an IntTy
# in the cache so that ir.IntType(32) itself started returning one.
# Construct directly instead; equality and hashing are inherited and
# depend only on the width, so an IntTy still compares equal to i32.
self = object.__new__(cls)
self.width = bits
return self
def __init__(self, bits: int, signed: bool = True):
self.signed = signed
def __getnewargs__(self):
return self.width, self.signed
def describe(self) -> str:
return f"{'i' if self.signed else 'u'}{self.width}"
PACKET_KINDS = ("pkt", "pkt_meta", "pkt_end")
class PktPtrTy(IntTy):
"""A packet pointer: 64-bit, unsigned, and tagged with the verifier's kind
for it (packet data, packet metadata, or packet end). It is an IntTy so
every integer path accepts it; the few sites that must treat it as a
pointer check isinstance(ty, PktPtrTy) before the integer rules apply.
See pythonbpf/expr/packet_pointer.py for where kinds come from and the
rules applied to them."""
def __new__(cls, kind: str):
return IntTy.__new__(cls, 64, False)
def __init__(self, kind: str):
if kind not in PACKET_KINDS:
raise ValueError(f"unknown packet pointer kind {kind!r}")
IntTy.__init__(self, 64, False)
self.kind = kind
def __getnewargs__(self): # type: ignore[override]
return (self.kind,)
def describe(self) -> str:
return f"{self.kind}*"
def byte_size(ty) -> int:
"""Bytes an integer type occupies in memory: its width rounded up to whole
bytes, so a 1-bit bool still takes one byte, as C's _Bool does."""
return (ty.width + 7) // 8
def int_literal_type(value: int) -> IntTy:
"""C's type for an integer constant: `int` if the value fits, else
`long long`. Literals and enum constants alike; an enum constant is an
`int` whatever the enum's underlying type is (that type belongs to
variables of the enum type, such as struct fields)."""
return IntTy(32, True) if -(1 << 31) <= value < (1 << 31) else IntTy(64, True)
def field_int_type(ty) -> "IntTy | None":
"""The declared integer type behind a vmlinux Field descriptor (its ctypes
class), or None if the descriptor is not a Field with an integer ctype.
The loaded value may already be wider; C ranks it by the declared width."""
ctype = getattr(getattr(ty, "type", None), "__name__", None)
if ctype in _INT_CTYPE_WIDTHS:
return IntTy(_INT_CTYPE_WIDTHS[ctype], is_signed_ctype(ctype))
return None
def signedness(ty) -> bool:
"""Sign of a descriptor. An IntTy carries it directly; a vmlinux Field
carries a ctypes class in .type, whose name decides; a 1-bit integer is a
bool and never negative, whatever it is wrapped in; a plain ir.IntType, a
site not yet taught to carry a sign, reads as signed, the compiler's
historical behaviour."""
if isinstance(ty, ir.IntType) and ty.width == 1:
return False
if hasattr(ty, "signed"):
return ty.signed
ctype = getattr(getattr(ty, "type", None), "__name__", None)
if ctype in _INT_CTYPE_WIDTHS:
return is_signed_ctype(ctype)
return True
_SIGNED_CTYPES = {
"c_int8",
"c_int16",
"c_int32",
"c_int64",
"c_int",
"c_short",
"c_long",
"c_longlong",
"c_byte",
}
_INT_CTYPE_WIDTHS = {
"c_int8": 8,
"c_uint8": 8,
"c_byte": 8,
"c_ubyte": 8,
"c_int16": 16,
"c_uint16": 16,
"c_short": 16,
"c_ushort": 16,
"c_int32": 32,
"c_uint32": 32,
"c_int": 32,
"c_uint": 32,
"c_int64": 64,
"c_uint64": 64,
"c_long": 64,
"c_ulong": 64,
"c_longlong": 64,
# A pointer-sized integer; treated as unsigned like uintptr_t.
"c_void_p": 64,
# C's _Bool: one bit to LLVM (as True/False locals already are). It is
# never negative, and a value narrows to it by comparing with zero.
"c_bool": 1,
}
def is_signed_ctype(ctype: str) -> bool:
return ctype in _SIGNED_CTYPES
# TODO: THIS IS NOT SUPPOSED TO MATCH STRINGS :skull:
mapping = {
name: IntTy(width, is_signed_ctype(name))
for name, width in _INT_CTYPE_WIDTHS.items()
}
mapping.update(
{
"c_float": ir.FloatType(),
"c_double": ir.DoubleType(),
# Not so sure about this one
"str": ir.PointerType(ir.IntType(8)),
}
)
def ctypes_to_ir(ctype: str):
if ctype in mapping:
return mapping[ctype]
raise NotImplementedError(f"No mapping for {ctype}")
def is_ctypes(ctype: str) -> bool:
return ctype in mapping