lin
2025-07-30 fcd736bf35fd93b563e9bbf594f2aa7b62028cc9
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
--[[
Copyright 2016 Marek Vavrusa <mvavrusa@cloudflare.com>
 
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
 
http://www.apache.org/licenses/LICENSE-2.0
 
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
local ffi = require('ffi')
local bit = require('bit')
local cdef = require('bpf.cdef')
 
local BPF, HELPER = ffi.typeof('struct bpf'), ffi.typeof('struct bpf_func_id')
local const_width = {
   [1] = BPF.B, [2] = BPF.H, [4] = BPF.W, [8] = BPF.DW,
}
local const_width_type = {
   [1] = ffi.typeof('uint8_t'), [2] = ffi.typeof('uint16_t'), [4] = ffi.typeof('uint32_t'), [8] = ffi.typeof('uint64_t'),
}
 
-- Built-ins that will be translated into BPF instructions
-- i.e. bit.bor(0xf0, 0x0f) becomes {'alu64, or, k', reg(0xf0), reg(0x0f), 0, 0}
local builtins = {
   [bit.lshift]  = 'LSH',
   [bit.rshift]  = 'RSH',
   [bit.band]    = 'AND',
   [bit.bnot]    = 'NEG',
   [bit.bor]     = 'OR',
   [bit.bxor]    = 'XOR',
   [bit.arshift] = 'ARSH',
   -- Extensions and intrinsics
}
 
local function width_type(w)
   -- Note: ffi.typeof doesn't accept '?' as template
   return const_width_type[w] or ffi.typeof(string.format('uint8_t [%d]', w))
end
builtins.width_type = width_type
 
-- Return struct member size/type (requires LuaJIT 2.1+)
-- I am ashamed that there's no easier way around it.
local function sizeofattr(ct, name)
   if not ffi.typeinfo then error('LuaJIT 2.1+ is required for ffi.typeinfo') end
   local cinfo = ffi.typeinfo(ct)
   while true do
       cinfo = ffi.typeinfo(cinfo.sib)
       if not cinfo then return end
       if cinfo.name == name then break end
   end
   local size = math.max(1, ffi.typeinfo(cinfo.sib or ct).size - cinfo.size)
   -- Guess type name
   return size, builtins.width_type(size)
end
builtins.sizeofattr = sizeofattr
 
-- Byte-order conversions for little endian
local function ntoh(x, w)
   if w then x = ffi.cast(const_width_type[w/8], x) end
   return bit.bswap(x)
end
local function hton(x, w) return ntoh(x, w) end
builtins.ntoh = ntoh
builtins.hton = hton
builtins[ntoh] = function (e, dst, a, w)
   -- This is trickery, but TO_LE means cpu_to_le(),
   -- and we want exactly the opposite as network is always 'be'
   w = w or ffi.sizeof(e.V[a].type)*8
   if w == 8 then return end -- NOOP
   assert(w <= 64, 'NYI: hton(a[, width]) - operand larger than register width')
   -- Allocate registers and execute
   e.vcopy(dst, a)
   e.emit(BPF.ALU + BPF.END + BPF.TO_BE, e.vreg(dst), 0, 0, w)
end
builtins[hton] = function (e, dst, a, w)
   w = w or ffi.sizeof(e.V[a].type)*8
   if w == 8 then return end -- NOOP
   assert(w <= 64, 'NYI: hton(a[, width]) - operand larger than register width')
   -- Allocate registers and execute
   e.vcopy(dst, a)
   e.emit(BPF.ALU + BPF.END + BPF.TO_LE, e.vreg(dst), 0, 0, w)
end
-- Byte-order conversions for big endian are no-ops
if ffi.abi('be') then
   ntoh = function (x, w)
       return w and ffi.cast(const_width_type[w/8], x) or x
   end
   hton = ntoh
   builtins[ntoh] = function(_, _, _) return end
   builtins[hton] = function(_, _, _) return end
end
-- Other built-ins
local function xadd() error('NYI') end
builtins.xadd = xadd
builtins[xadd] = function (e, ret, a, b, off)
   local vinfo = e.V[a].const
   assert(vinfo and vinfo.__dissector, 'xadd(a, b[, offset]) called on non-pointer')
   local w = ffi.sizeof(vinfo.__dissector)
   -- Calculate structure attribute offsets
   if e.V[off] and type(e.V[off].const) == 'string' then
       local ct, field = vinfo.__dissector, e.V[off].const
       off = ffi.offsetof(ct, field)
       assert(off, 'xadd(a, b, offset) - offset is not valid in given structure')
       w = sizeofattr(ct, field)
   end
   assert(w == 4 or w == 8, 'NYI: xadd() - 1 and 2 byte atomic increments are not supported')
   -- Allocate registers and execute
   local src_reg = e.vreg(b)
   local dst_reg = e.vreg(a)
   -- Set variable for return value and call
   e.vset(ret)
   e.vreg(ret, 0, true, ffi.typeof('int32_t'))
   -- Optimize the NULL check away if provably not NULL
   if not e.V[a].source or e.V[a].source:find('_or_null', 1, true) then
       e.emit(BPF.JMP + BPF.JEQ + BPF.K, dst_reg, 0, 1, 0) -- if (dst != NULL)
   end
   e.emit(BPF.XADD + BPF.STX + const_width[w], dst_reg, src_reg, off or 0, 0)
end
 
local function probe_read() error('NYI') end
builtins.probe_read = probe_read
builtins[probe_read] = function (e, ret, dst, src, vtype, ofs)
   e.reg_alloc(e.tmpvar, 1)
   -- Load stack pointer to dst, since only load to stack memory is supported
   -- we have to use allocated stack memory or create a new allocation and convert
   -- to pointer type
   e.emit(BPF.ALU64 + BPF.MOV + BPF.X, 1, 10, 0, 0)
   if not e.V[dst].const or not e.V[dst].const.__base > 0 then
       builtins[ffi.new](e, dst, vtype) -- Allocate stack memory
   end
   e.emit(BPF.ALU64 + BPF.ADD + BPF.K, 1, 0, 0, -e.V[dst].const.__base)
   -- Set stack memory maximum size bound
   e.reg_alloc(e.tmpvar, 2)
   if not vtype then
       vtype = cdef.typename(e.V[dst].type)
       -- Dereference pointer type to pointed type for size calculation
       if vtype:sub(-1) == '*' then vtype = vtype:sub(0, -2) end
   end
   local w = ffi.sizeof(vtype)
   e.emit(BPF.ALU64 + BPF.MOV + BPF.K, 2, 0, 0, w)
   -- Set source pointer
   if e.V[src].reg then
       e.reg_alloc(e.tmpvar, 3) -- Copy from original register
       e.emit(BPF.ALU64 + BPF.MOV + BPF.X, 3, e.V[src].reg, 0, 0)
   else
       e.vreg(src, 3)
       e.reg_spill(src) -- Spill to avoid overwriting
   end
   if ofs and ofs > 0 then
       e.emit(BPF.ALU64 + BPF.ADD + BPF.K, 3, 0, 0, ofs)
   end
   -- Call probe read helper
   ret = ret or e.tmpvar
   e.vset(ret)
   e.vreg(ret, 0, true, ffi.typeof('int32_t'))
   e.emit(BPF.JMP + BPF.CALL, 0, 0, 0, HELPER.probe_read)
   e.V[e.tmpvar].reg = nil  -- Free temporary registers
end
 
builtins[ffi.cast] = function (e, dst, ct, x)
   assert(e.V[ct].const, 'ffi.cast(ctype, x) called with bad ctype')
   e.vcopy(dst, x)
   if e.V[x].const and type(e.V[x].const) == 'table' then
       e.V[dst].const.__dissector = ffi.typeof(e.V[ct].const)
   end
   e.V[dst].type = ffi.typeof(e.V[ct].const)
   -- Specific types also encode source of the data
   -- This is because BPF has different helpers for reading
   -- different data sources, so variables must track origins.
   -- struct pt_regs - source of the data is probe
   -- struct skb     - source of the data is socket buffer
   -- struct X       - source of the data is probe/tracepoint
   if ffi.typeof(e.V[ct].const) == ffi.typeof('struct pt_regs') then
       e.V[dst].source = 'ptr_to_probe'
   end
end
 
builtins[ffi.new] = function (e, dst, ct, x)
   if type(ct) == 'number' then
       ct = ffi.typeof(e.V[ct].const) -- Get ctype from variable
   end
   assert(not x, 'NYI: ffi.new(ctype, ...) - initializer is not supported')
   assert(not cdef.isptr(ct, true), 'NYI: ffi.new(ctype, ...) - ctype MUST NOT be a pointer')
   e.vset(dst, nil, ct)
   e.V[dst].source = 'ptr_to_stack'
   e.V[dst].const = {__base = e.valloc(ffi.sizeof(ct), true), __dissector = ct}
   -- Set array dissector if created an array
   -- e.g. if ct is 'char [2]', then dissector is 'char'
   local elem_type = tostring(ct):match('ctype<(.+)%s%[(%d+)%]>')
   if elem_type then
       e.V[dst].const.__dissector = ffi.typeof(elem_type)
   end
end
 
builtins[ffi.copy] = function (e, ret, dst, src)
   assert(cdef.isptr(e.V[dst].type), 'ffi.copy(dst, src) - dst MUST be a pointer type')
   assert(cdef.isptr(e.V[src].type), 'ffi.copy(dst, src) - src MUST be a pointer type')
   -- Specific types also encode source of the data
   -- struct pt_regs - source of the data is probe
   -- struct skb     - source of the data is socket buffer
   if e.V[src].source and e.V[src].source:find('ptr_to_probe', 1, true) then
       e.reg_alloc(e.tmpvar, 1)
       -- Load stack pointer to dst, since only load to stack memory is supported
       -- we have to either use spilled variable or allocated stack memory offset
       e.emit(BPF.ALU64 + BPF.MOV + BPF.X, 1, 10, 0, 0)
       if e.V[dst].spill then
           e.emit(BPF.ALU64 + BPF.ADD + BPF.K, 1, 0, 0, -e.V[dst].spill)
       elseif e.V[dst].const.__base then
           e.emit(BPF.ALU64 + BPF.ADD + BPF.K, 1, 0, 0, -e.V[dst].const.__base)
       else error('ffi.copy(dst, src) - can\'t get stack offset of dst') end
       -- Set stack memory maximum size bound
       local dst_tname = cdef.typename(e.V[dst].type)
       if dst_tname:sub(-1) == '*' then dst_tname = dst_tname:sub(0, -2) end
       e.reg_alloc(e.tmpvar, 2)
       e.emit(BPF.ALU64 + BPF.MOV + BPF.K, 2, 0, 0, ffi.sizeof(dst_tname))
       -- Set source pointer
       if e.V[src].reg then
           e.reg_alloc(e.tmpvar, 3) -- Copy from original register
           e.emit(BPF.ALU64 + BPF.MOV + BPF.X, 3, e.V[src].reg, 0, 0)
       else
           e.vreg(src, 3)
           e.reg_spill(src) -- Spill to avoid overwriting
       end
       -- Call probe read helper
       e.vset(ret)
       e.vreg(ret, 0, true, ffi.typeof('int32_t'))
       e.emit(BPF.JMP + BPF.CALL, 0, 0, 0, HELPER.probe_read)
       e.V[e.tmpvar].reg = nil  -- Free temporary registers
   elseif e.V[src].const and e.V[src].const.__map then
       error('NYI: ffi.copy(dst, src) - src is backed by BPF map')
   elseif e.V[src].const and e.V[src].const.__dissector then
       error('NYI: ffi.copy(dst, src) - src is backed by socket buffer')
   else
       -- TODO: identify cheap register move
       -- TODO: identify copy to/from stack
       error('NYI: ffi.copy(dst, src) - src is neither BPF map/socket buffer or probe')
   end
end
-- print(format, ...) builtin changes semantics from Lua print(...)
-- the first parameter has to be format and only reduced set of conversion specificers
-- is allowed: %d %u %x %ld %lu %lx %lld %llu %llx %p %s
builtins[print] = function (e, ret, fmt, a1, a2, a3)
   -- Load format string and length
   e.reg_alloc(e.V[e.tmpvar], 1)
   e.reg_alloc(e.V[e.tmpvar+1], 1)
   if type(e.V[fmt].const) == 'string' then
       local src = e.V[fmt].const
       local len = #src + 1
       local dst = e.valloc(len, src)
       -- TODO: this is materialize step
       e.V[fmt].const = {__base=dst}
       e.V[fmt].type = ffi.typeof('char ['..len..']')
   elseif e.V[fmt].const.__base then -- luacheck: ignore
       -- NOP
   else error('NYI: print(fmt, ...) - format variable is not literal/stack memory') end
   -- Prepare helper call
   e.emit(BPF.ALU64 + BPF.MOV + BPF.X, 1, 10, 0, 0)
   e.emit(BPF.ALU64 + BPF.ADD + BPF.K, 1, 0, 0, -e.V[fmt].const.__base)
   e.emit(BPF.ALU64 + BPF.MOV + BPF.K, 2, 0, 0, ffi.sizeof(e.V[fmt].type))
   if a1 then
       local args = {a1, a2, a3}
       assert(#args <= 3, 'print(fmt, ...) - maximum of 3 arguments supported')
       for i, arg in ipairs(args) do
           e.vcopy(e.tmpvar, arg)  -- Copy variable
           e.vreg(e.tmpvar, 3+i-1) -- Materialize it in arg register
       end
   end
   -- Call helper
   e.vset(ret)
   e.vreg(ret, 0, true, ffi.typeof('int32_t')) -- Return is integer
   e.emit(BPF.JMP + BPF.CALL, 0, 0, 0, HELPER.trace_printk)
   e.V[e.tmpvar].reg = nil  -- Free temporary registers
end
 
-- Implements bpf_perf_event_output(ctx, map, flags, var, vlen) on perf event map
local function perf_submit(e, dst, map_var, src)
   -- Set R2 = map fd (indirect load)
   local map = e.V[map_var].const
   e.vcopy(e.tmpvar, map_var)
   e.vreg(e.tmpvar, 2, true, ffi.typeof('uint64_t'))
   e.LD_IMM_X(2, BPF.PSEUDO_MAP_FD, map.fd, ffi.sizeof('uint64_t'))
   -- Set R1 = ctx
   e.reg_alloc(e.tmpvar, 1) -- Spill anything in R1 (unnamed tmp variable)
   e.emit(BPF.ALU64 + BPF.MOV + BPF.X, 1, 6, 0, 0) -- CTX is always in R6, copy
   -- Set R3 = flags
   e.vset(e.tmpvar, nil, 0) -- BPF_F_CURRENT_CPU
   e.vreg(e.tmpvar, 3, false, ffi.typeof('uint64_t'))
   -- Set R4 = pointer to src on stack
   assert(e.V[src].const.__base, 'NYI: submit(map, var) - variable is not on stack')
   e.emit(BPF.ALU64 + BPF.MOV + BPF.X, 4, 10, 0, 0)
   e.emit(BPF.ALU64 + BPF.ADD + BPF.K, 4, 0, 0, -e.V[src].const.__base)
   -- Set R5 = src length
   e.emit(BPF.ALU64 + BPF.MOV + BPF.K, 5, 0, 0, ffi.sizeof(e.V[src].type))
   -- Set R0 = ret and call
   e.vset(dst)
   e.vreg(dst, 0, true, ffi.typeof('int32_t')) -- Return is integer
   e.emit(BPF.JMP + BPF.CALL, 0, 0, 0, HELPER.perf_event_output)
   e.V[e.tmpvar].reg = nil  -- Free temporary registers
end
 
-- Implements bpf_skb_load_bytes(ctx, off, var, vlen) on skb->data
local function load_bytes(e, dst, off, var)
   -- Set R2 = offset
   e.vset(e.tmpvar, nil, off)
   e.vreg(e.tmpvar, 2, false, ffi.typeof('uint64_t'))
   -- Set R1 = ctx
   e.reg_alloc(e.tmpvar, 1) -- Spill anything in R1 (unnamed tmp variable)
   e.emit(BPF.ALU64 + BPF.MOV + BPF.X, 1, 6, 0, 0) -- CTX is always in R6, copy
   -- Set R3 = pointer to var on stack
   assert(e.V[var].const.__base, 'NYI: load_bytes(off, var, len) - variable is not on stack')
   e.emit(BPF.ALU64 + BPF.MOV + BPF.X, 3, 10, 0, 0)
   e.emit(BPF.ALU64 + BPF.ADD + BPF.K, 3, 0, 0, -e.V[var].const.__base)
   -- Set R4 = var length
   e.emit(BPF.ALU64 + BPF.MOV + BPF.K, 4, 0, 0, ffi.sizeof(e.V[var].type))
   -- Set R0 = ret and call
   e.vset(dst)
   e.vreg(dst, 0, true, ffi.typeof('int32_t')) -- Return is integer
   e.emit(BPF.JMP + BPF.CALL, 0, 0, 0, HELPER.skb_load_bytes)
   e.V[e.tmpvar].reg = nil  -- Free temporary registers
end
 
-- Implements bpf_get_stack_id()
local function stack_id(e, ret, map_var, key)
   -- Set R2 = map fd (indirect load)
   local map = e.V[map_var].const
   e.vcopy(e.tmpvar, map_var)
   e.vreg(e.tmpvar, 2, true, ffi.typeof('uint64_t'))
   e.LD_IMM_X(2, BPF.PSEUDO_MAP_FD, map.fd, ffi.sizeof('uint64_t'))
   -- Set R1 = ctx
   e.reg_alloc(e.tmpvar, 1) -- Spill anything in R1 (unnamed tmp variable)
   e.emit(BPF.ALU64 + BPF.MOV + BPF.X, 1, 6, 0, 0) -- CTX is always in R6, copy
   -- Load flags in R2 (immediate value or key)
   local imm = e.V[key].const
   assert(tonumber(imm), 'NYI: stack_id(map, var), var must be constant number')
   e.reg_alloc(e.tmpvar, 3) -- Spill anything in R2 (unnamed tmp variable)
   e.LD_IMM_X(3, 0, imm, 8)
   -- Return R0 as signed integer
   e.vset(ret)
   e.vreg(ret, 0, true, ffi.typeof('int32_t'))
   e.emit(BPF.JMP + BPF.CALL, 0, 0, 0, HELPER.get_stackid)
   e.V[e.tmpvar].reg = nil  -- Free temporary registers
end
 
-- table.insert(table, value) keeps semantics with the exception of BPF maps
-- map `perf_event` -> submit inserted value
builtins[table.insert] = function (e, dst, map_var, value)
   assert(e.V[map_var].const.__map, 'NYI: table.insert() supported only on BPF maps')
   return perf_submit(e, dst, map_var, value)
end
 
-- bpf_get_current_comm(buffer) - write current process name to byte buffer
local function comm() error('NYI') end
builtins[comm] = function (e, ret, dst)
   -- Set R1 = buffer
   assert(e.V[dst].const.__base, 'NYI: comm(buffer) - buffer variable is not on stack')
   e.reg_alloc(e.tmpvar, 1) -- Spill
   e.emit(BPF.ALU64 + BPF.MOV + BPF.X, 1, 10, 0, 0)
   e.emit(BPF.ALU64 + BPF.ADD + BPF.K, 1, 0, 0, -e.V[dst].const.__base)
   -- Set R2 = length
   e.reg_alloc(e.tmpvar, 2) -- Spill
   e.emit(BPF.ALU64 + BPF.MOV + BPF.K, 2, 0, 0, ffi.sizeof(e.V[dst].type))
   -- Return is integer
   e.vset(ret)
   e.vreg(ret, 0, true, ffi.typeof('int32_t'))
   e.emit(BPF.JMP + BPF.CALL, 0, 0, 0, HELPER.get_current_comm)
   e.V[e.tmpvar].reg = nil  -- Free temporary registers
end
 
-- Math library built-ins
math.log2 = function () error('NYI') end
builtins[math.log2] = function (e, dst, x)
   -- Classic integer bits subdivison algorithm to find the position
   -- of the highest bit set, adapted for BPF bytecode-friendly operations.
   -- https://graphics.stanford.edu/~seander/bithacks.html
   -- r = 0
   local r = e.vreg(dst, nil, true)
   e.emit(BPF.ALU64 + BPF.MOV + BPF.K, r, 0, 0, 0)
   -- v = x
   e.vcopy(e.tmpvar, x)
   local v = e.vreg(e.tmpvar, 2)
   if cdef.isptr(e.V[x].const) then -- No pointer arithmetics, dereference
       e.vderef(v, v, {const = {__dissector=ffi.typeof('uint64_t')}})
   end
   -- Invert value to invert all tests, otherwise we would need and+jnz
   e.emit(BPF.ALU64 + BPF.NEG + BPF.K, v, 0, 0, 0)        -- v = ~v
   -- Unrolled test cases, converted masking to arithmetic as we don't have "if !(a & b)"
   -- As we're testing inverted value, we have to use arithmetic shift to copy MSB
   for i=4,0,-1 do
       local k = bit.lshift(1, i)
       e.emit(BPF.JMP + BPF.JGT + BPF.K, v, 0, 2, bit.bnot(bit.lshift(1, k))) -- if !upper_half(x)
       e.emit(BPF.ALU64 + BPF.ARSH + BPF.K, v, 0, 0, k)                       --     v >>= k
       e.emit(BPF.ALU64 + BPF.OR + BPF.K, r, 0, 0, k)                         --     r |= k
   end
   -- No longer constant, cleanup tmpvars
   e.V[dst].const = nil
   e.V[e.tmpvar].reg = nil
end
builtins[math.log10] = function (e, dst, x)
   -- Compute log2(x) and transform
   builtins[math.log2](e, dst, x)
   -- Relationship: log10(v) = log2(v) / log2(10)
   local r = e.V[dst].reg
   e.emit(BPF.ALU64 + BPF.ADD + BPF.K, r, 0, 0, 1)    -- Compensate round-down
   e.emit(BPF.ALU64 + BPF.MUL + BPF.K, r, 0, 0, 1233) -- log2(10) ~ 1233>>12
   e.emit(BPF.ALU64 + BPF.RSH + BPF.K, r, 0, 0, 12)
end
builtins[math.log] = function (e, dst, x)
   -- Compute log2(x) and transform
   builtins[math.log2](e, dst, x)
   -- Relationship: ln(v) = log2(v) / log2(e)
   local r = e.V[dst].reg
   e.emit(BPF.ALU64 + BPF.ADD + BPF.K, r, 0, 0, 1)    -- Compensate round-down
   e.emit(BPF.ALU64 + BPF.MUL + BPF.K, r, 0, 0, 2839) -- log2(e) ~ 2839>>12
   e.emit(BPF.ALU64 + BPF.RSH + BPF.K, r, 0, 0, 12)
end
 
-- Call-type helpers
local function call_helper(e, dst, h, vtype)
   e.vset(dst)
   e.vreg(dst, 0, true, vtype or ffi.typeof('uint64_t'))
   e.emit(BPF.JMP + BPF.CALL, 0, 0, 0, h)
   e.V[dst].const = nil -- Target is not a function anymore
end
local function cpu() error('NYI') end
local function rand() error('NYI') end
local function time() error('NYI') end
local function pid_tgid() error('NYI') end
local function uid_gid() error('NYI') end
 
-- Export helpers and builtin variants
builtins.cpu = cpu
builtins.time = time
builtins.pid_tgid = pid_tgid
builtins.uid_gid = uid_gid
builtins.comm = comm
builtins.perf_submit = perf_submit
builtins.stack_id = stack_id
builtins.load_bytes = load_bytes
builtins[cpu] = function (e, dst) return call_helper(e, dst, HELPER.get_smp_processor_id) end
builtins[rand] = function (e, dst) return call_helper(e, dst, HELPER.get_prandom_u32, ffi.typeof('uint32_t')) end
builtins[time] = function (e, dst) return call_helper(e, dst, HELPER.ktime_get_ns) end
builtins[pid_tgid] = function (e, dst) return call_helper(e, dst, HELPER.get_current_pid_tgid) end
builtins[uid_gid] = function (e, dst) return call_helper(e, dst, HELPER.get_current_uid_gid) end
builtins[perf_submit] = function (e, dst, map, value) return perf_submit(e, dst, map, value) end
builtins[stack_id] = function (e, dst, map, key) return stack_id(e, dst, map, key) end
builtins[load_bytes] = function (e, dst, off, var, len) return load_bytes(e, dst, off, var, len) end
 
return builtins