Commit 676f357a authored by Marek Vavruša's avatar Marek Vavruša Committed by 4ast

src/lua: LuaJIT BPF compiler, examples, tests (#652)

this is initial commit of LuaJIT bytecode to BPF
compiler project that enables writing both kernel
and user-part of the code as Lua
parent 07175d05
#!/usr/bin/env bcc-lua
--[[
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.
]]
-- This example program measures latency of block device operations and plots it
-- in a histogram. It is similar to BPF example:
-- https://github.com/torvalds/linux/blob/master/samples/bpf/tracex3_kern.c
local ffi = require('ffi')
local bpf = require('bpf')
local S = require('syscall')
-- Shared part of the program
local bins = 100
local map = bpf.map('hash', 512, ffi.typeof('uint64_t'), ffi.typeof('uint64_t'))
local lat_map = bpf.map('array', bins)
-- Kernel-space part of the program
local trace_start = bpf.kprobe('myprobe:blk_start_request', function (ptregs)
map[ptregs.parm1] = time()
end, false, -1, 0)
local trace_end = bpf.kprobe('myprobe2:blk_account_io_completion', function (ptregs)
-- The lines below are computing index
-- using log10(x)*10 = log2(x)*10/log2(10) = log2(x)*3
-- index = 29 ~ 1 usec
-- index = 59 ~ 1 msec
-- index = 89 ~ 1 sec
-- index = 99 ~ 10sec or more
local delta = time() - map[ptregs.parm1]
local index = 3 * math.log2(delta)
if index >= bins then
index = bins-1
end
xadd(lat_map[index], 1)
return true
end, false, -1, 0)
-- User-space part of the program
pcall(function()
local counter = 0
local sym = {' ',' ','.','.','*','*','o','o','O','O','#','#'}
while true do
-- Print header once in a while
if counter % 50 == 0 then
print('|1us |10us |100us |1ms |10ms |100ms |1s |10s')
counter = 0
end
counter = counter + 1
-- Collect all events
local hist, events = {}, 0
for i=29,bins-1 do
local v = tonumber(lat_map[i] or 0)
if v > 0 then
hist[i] = hist[i] or 0 + v
events = events + v
end
end
-- Print histogram symbols based on relative frequency
local s = ''
for i=29,bins-1 do
if hist[i] then
local c = math.ceil((hist[i] / (events + 1)) * #sym)
s = s .. sym[c]
else s = s .. ' ' end
end
print(s .. string.format(' ; %d events', events))
S.sleep(1)
end
end)
\ No newline at end of file
#!/usr/bin/env bcc-lua
--[[
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.
]]
-- Simple tracing example that executes a program on
-- return from sys_write() and tracks the number of hits
local ffi = require('ffi')
local bpf = require('bpf')
local S = require('syscall')
-- Shared part of the program
local map = bpf.map('array', 1)
-- Kernel-space part of the program
local probe = bpf.kprobe('myprobe:sys_write', function (ptregs)
xadd(map[0], 1)
end, true)
-- User-space part of the program
pcall(function()
for _ = 1, 10 do
print('hits: ', tonumber(map[0]))
S.sleep(1)
end
end)
#!/usr/bin/env bcc-lua
--[[
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.
]]
-- Simple parsing example of UDP/DNS that counts frequency of QTYPEs.
-- It shows how to parse packet variable-length packet structures.
local ffi = require("ffi")
local bpf = require("bpf")
local S = require("syscall")
-- Shared part of the program
local map = assert(bpf.map('array', 256))
-- Kernel-space part of the program
local prog = bpf.socket('lo', function (skb)
local ip = pkt.ip -- Accept only UDP messages
if ip.proto ~= c.ip.proto_udp then return false end
local udp = ip.udp -- Only messages >12 octets (DNS header)
if udp.length < 12 then return false end
-- Unroll QNAME (up to 2 labels)
udp = udp.data + 12
local label = udp[0]
if label > 0 then
udp = udp + label + 1
label = udp[0]
if label > 0 then
udp = udp + label + 1
end
end
-- Track QTYPE (low types)
if udp[0] == 0 then
local qtype = udp[2] -- Low octet from QTYPE
xadd(map[qtype], 1)
end
end)
-- User-space part of the program
for _ = 1, 10 do
for k,v in map.pairs,map,0 do
v = tonumber(v)
if v > 0 then
print(string.format('TYPE%d: %d', k, v))
end
end
S.sleep(1)
end
\ No newline at end of file
#!/usr/bin/env bcc-lua
--[[
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.
]]
-- Simple parsing example of TCP/HTTP that counts frequency of types of requests
-- and shows more complicated pattern matching constructions and slices.
-- Rewrite of a BCC example:
-- https://github.com/iovisor/bcc/blob/master/examples/networking/http_filter/http-parse-simple.c
local ffi = require("ffi")
local bpf = require("bpf")
local S = require("syscall")
-- Shared part of the program
local map = bpf.map('hash', 64)
-- Kernel-space part of the program
local prog = bpf.socket('lo', function (skb)
-- Only ingress so we don't count twice on loopback
if skb.ingress_ifindex == 0 then return end
local data = pkt.ip.tcp.data -- Get TCP protocol dissector
-- Continue only if we have 7 bytes of TCP data
if data + 7 > skb.len then return end
-- Fetch 4 bytes of TCP data and compare
local h = data(0, 4)
if h == 'HTTP' or h == 'GET ' or
h == 'POST' or h == 'PUT ' or
h == 'HEAD' or h == 'DELE' then
-- If hash key doesn't exist, create it
-- otherwise increment counter
local v = map[h]
if not v then map[h] = 1
else xadd(map[h], 1)
end
end
end)
-- User-space part of the program
for _ = 1, 10 do
local strkey = ffi.new('uint32_t [1]')
local s = ''
for k,v in map.pairs,map,0 do
strkey[0] = bpf.ntoh(k)
s = s..string.format('%s %d ', ffi.string(strkey, 4):match '^%s*(.-)%s*$', tonumber(v))
end
if #s > 0 then print(s..'messages') end
S.sleep(1)
end
\ No newline at end of file
#!/usr/bin/env bcc-lua
--[[
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.
]]
-- This program looks at IP, UDP and ICMP packets and
-- increments counter for each packet of given type seen
-- Rewrite of https://github.com/torvalds/linux/blob/master/samples/bpf/sock_example.c
local ffi = require("ffi")
local bpf = require("bpf")
local S = require("syscall")
-- Shared part of the program
local map = bpf.map('hash', 256)
map[1], map[6], map[17] = 0, 0, 0
-- Kernel-space part of the program
bpf.socket('lo', function (skb)
local proto = pkt.ip.proto -- Get byte (ip.proto) from frame at [23]
xadd(map[proto], 1) -- Atomic `map[proto] += 1`
end)
-- User-space part of the program
for _ = 1, 10 do
local icmp, udp, tcp = map[1], map[17], map[6]
print(string.format('TCP %d UDP %d ICMP %d packets',
tonumber(tcp or 0), tonumber(udp or 0), tonumber(icmp or 0)))
S.sleep(1)
end
\ No newline at end of file
#!/usr/bin/env bcc-lua
--[[
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.
]]
-- This program counts total bytes received per-protocol in 64-bit counters.
-- The map backend is array in this case to avoid key allocations.
-- increments counter for each packet of given type seen
-- Rewrite of https://github.com/torvalds/linux/blob/master/samples/bpf/sock_example.c
local ffi = require("ffi")
local bpf = require("bpf")
local S = require("syscall")
-- Shared part of the program
local map = bpf.map('array', 256, ffi.typeof('uint32_t'), ffi.typeof('uint64_t'))
-- Kernel-space part of the program
bpf.socket('lo', function (skb)
local proto = pkt.ip.proto -- Get byte (ip.proto) from frame at [23]
xadd(map[proto], skb.len) -- Atomic `map[proto] += <payload length>`
end)
-- User-space part of the program
for _ = 1, 10 do
local icmp, udp, tcp = map[1], map[17], map[6]
print(string.format('TCP %d UDP %d ICMP %d bytes',
tonumber(tcp or 0), tonumber(udp or 0), tonumber(icmp or 0)))
S.sleep(1)
end
\ No newline at end of file
#!/usr/bin/env bcc-lua
--[[
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.
]]
-- Summarize off-CPU time by stack trace
-- Related tool: https://github.com/iovisor/bcc/blob/master/tools/offcputime.py
local ffi = require('ffi')
local bpf = require('bpf')
local S = require('syscall')
-- Create BPF maps
-- TODO: made smaller to fit default memory limits
local key_t = 'struct { char name[16]; int32_t stack_id; }'
local starts = assert(bpf.map('hash', 128, ffi.typeof('uint32_t'), ffi.typeof('uint64_t')))
local counts = assert(bpf.map('hash', 128, ffi.typeof(key_t), ffi.typeof('uint64_t')))
local stack_traces = assert(bpf.map('stack_trace', 16))
-- Open tracepoint and attach BPF program
-- The 'arg' parses tracepoint format automatically
local tp = bpf.tracepoint('sched/sched_switch', function (arg)
-- Update previous thread sleep time
local pid = arg.prev_pid
local now = time()
starts[pid] = now
-- Calculate current thread's delta time
pid = arg.next_pid
local from = starts[pid]
if not from then
return 0
end
local delta = (now - from) / 1000
starts[pid] = nil
-- Check if the delta is below 1us
if delta < 1 then
return
end
-- Create key for this thread
local key = ffi.new(key_t)
comm(key.name)
key.stack_id = stack_id(stack_traces, BPF.F_FAST_STACK_CMP)
-- Update current thread off cpu time with delta
local val = counts[key]
if not val then
counts[key] = 0
end
xadd(counts[key], delta)
end, 0, -1)
-- Helper: load kernel symbols
ffi.cdef 'unsigned long long strtoull(const char *, char **, int);'
local ksyms = {}
for l in io.lines('/proc/kallsyms') do
local addr, sym = l:match '(%w+) %w (%S+)'
if addr then ksyms[ffi.C.strtoull(addr, nil, 16)] = sym end
end
-- User-space part of the program
while true do
for k,v in counts.pairs,counts,nil do
local s = ''
local traces = stack_traces[k.stack_id]
if traces then
for i, ip in ipairs(traces) do
s = s .. string.format(" %-16p %s", ip, ksyms[ip])
end
end
s = s .. string.format(" %-16s %s", "-", ffi.string(k.name))
s = s .. string.format(" %d", tonumber(v))
print(s)
end
S.sleep(1)
end
#!/usr/bin/env bcc-lua
--[[
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.
]]
-- Trace readline() call from all bash instances (print bash commands from all running shells).
-- This is rough equivallent to `bashreadline` with output through perf event API.
-- Source: http://www.brendangregg.com/blog/2016-02-08/linux-ebpf-bcc-uprobes.html
local ffi = require('ffi')
local bpf = require('bpf')
local S = require('syscall')
-- Perf event map
local sample_t = 'struct { uint64_t pid; char str[80]; }'
local events = bpf.map('perf_event_array')
-- Kernel-space part of the program
local probe = bpf.uprobe('/bin/bash:readline', function (ptregs)
local sample = ffi.new(sample_t)
sample.pid = pid_tgid()
ffi.copy(sample.str, ffi.cast('char *', ptregs.ax)) -- Cast `ax` to string pointer and copy to buffer
perf_submit(events, sample) -- Write buffer to perf event map
end, true, -1, 0)
-- User-space part of the program
local log = events:reader(nil, 0, sample_t) -- Must specify PID or CPU_ID to observe
print(' TASK-PID TIMESTAMP FUNCTION')
print(' | | | |')
while true do
log:block() -- Wait until event reader is readable
for _,e in log:read() do -- Collect available reader events
print(string.format('%12s%-16s %-10s %s', '', tonumber(e.pid), os.date("%H:%M:%S"), ffi.string(e.str)))
end
end
#!/usr/bin/env bcc-lua
--[[
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.
]]
-- Trace readline() call from all bash instances (print bash commands from all running shells).
-- This is rough equivallent to `bashreadline`
-- Source: http://www.brendangregg.com/blog/2016-02-08/linux-ebpf-bcc-uprobes.html
local ffi = require('ffi')
local bpf = require('bpf')
local S = require('syscall')
-- Kernel-space part of the program
local probe = bpf.uprobe('/bin/bash:readline', function (ptregs)
local line = ffi.new('char [40]') -- Create a 40 byte buffer on stack
ffi.copy(line, ffi.cast('char *', ptregs.ax)) -- Cast `ax` to string pointer and copy to buffer
print('%s\n', line) -- Print to trace_pipe
end, true, -1, 0)
-- User-space part of the program
local ok, err = pcall(function()
local log = bpf.tracelog()
print(' TASK-PID CPU# TIMESTAMP FUNCTION')
print(' | | | | |')
while true do
print(log:read())
end
end)
#!/usr/bin/env bcc-lua
--[[
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.
]]
-- Trace operations on keys matching given pattern in KyotoTycoon daemon.
-- This can show you if certain keys were modified or read during the lifetime
-- even if KT doesn't support this. It also shows how to attach to C++ mangled symbols.
local ffi = require('ffi')
local bpf = require('bpf')
local S = require('syscall')
local function help(err)
print(string.format('%s [get|set] [key]', arg[0]))
if err then print('error: '..err) end
os.exit(1)
end
-- Accept the same format as ktremotemgr for clarity: <get|set> <key>
local writeable, watch_key, klen = 'any', arg[2] or '*', 80
if arg[1] == 'get' then writeable = 0
elseif arg[1] == 'set' then writeable = 1
elseif arg[1] == '-h' or arg[1] == '--help' then help()
elseif arg[1] and arg[1] ~= 'any' then
help(string.format('bad cmd: "%s"', arg[1]))
end
if watch_key ~= '*' then klen = #watch_key end
-- Find a good entrypoint that has both key and differentiates read/write in KT
-- That is going to serve as an attachment point for BPF program
-- ABI: bool accept(void *this, const char* kbuf, size_t ksiz, Visitor* visitor, bool writable)
local key_type = string.format('char [%d]', klen)
local probe = bpf.uprobe('/usr/local/bin/ktserver:kyotocabinet::StashDB::accept',
function (ptregs)
-- Watch either get/set or both
if writeable ~= 'any' then
if ptregs.parm5 ~= writeable then return end
end
local line = ffi.new(key_type)
ffi.copy(line, ffi.cast('char *', ptregs.parm2))
-- Check if we're looking for specific key
if watch_key ~= '*' then
if ptregs.parm3 ~= klen then return false end
if line ~= watch_key then return false end
end
print('%s write:%d\n', line, ptregs.parm5)
end, false, -1, 0)
-- User-space part of the program
local ok, err = pcall(function()
local log = bpf.tracelog()
print(' TASK-PID CPU# TIMESTAMP FUNCTION')
print(' | | | | |')
while true do
print(log:read())
end
end)
......@@ -4,7 +4,8 @@ find_program(LUAJIT luajit)
if (LUAJIT_LIBRARIES AND LUAJIT)
FILE(GLOB_RECURSE SRC_LUA
${CMAKE_CURRENT_SOURCE_DIR}/bcc/*.lua
${CMAKE_CURRENT_SOURCE_DIR}/bcc/vendor/*.lua)
${CMAKE_CURRENT_SOURCE_DIR}/bcc/vendor/*.lua
${CMAKE_CURRENT_SOURCE_DIR}/bpf/*.lua)
ADD_CUSTOM_COMMAND(
OUTPUT bcc.lua
......
Lua Tools for BCC
-----------------
This directory contains Lua tooling for [BCC](https://github.com/iovisor/bcc)
This directory contains Lua tooling for [BCC][bcc]
(the BPF Compiler Collection).
BCC is a toolkit for creating userspace and kernel tracing programs. By
......@@ -52,3 +52,104 @@ The following instructions assume Ubuntu 14.04 LTS.
```
sudo ./bcc-probe examples/lua/task_switch.lua
```
## LuaJIT BPF compiler
Now it is also possible to write Lua functions and compile them transparently to BPF bytecode, here is a simple socket filter example:
```lua
local S = require('syscall')
local bpf = require('bpf')
local map = bpf.map('array', 256)
-- Kernel-space part of the program
local prog = assert(bpf(function ()
local proto = pkt.ip.proto -- Get byte (ip.proto) from frame at [23]
xadd(map[proto], 1) -- Increment packet count
end))
-- User-space part of the program
local sock = assert(bpf.socket('lo', prog))
for i=1,10 do
local icmp, udp, tcp = map[1], map[17], map[6]
print('TCP', tcp, 'UDP', udp, 'ICMP', icmp, 'packets')
S.sleep(1)
end
```
The other application of BPF programs is attaching to probes for [perf event tracing][tracing]. That means you can trace events inside the kernel (or user-space), and then collect results - for example histogram of `sendto()` latency, off-cpu time stack traces, syscall latency, and so on. While kernel probes and perf events have unstable ABI, with a dynamic language we can create and use proper type based on the tracepoint ABI on runtime.
Runtime automatically recognizes reads that needs a helper to be accessed. The type casts denote source of the objects, for example the [bashreadline][bashreadline] example that prints entered bash commands from all running shells:
```lua
local ffi = require('ffi')
local bpf = require('bpf')
-- Perf event map
local sample_t = 'struct { uint64_t pid; char str[80]; }'
local events = bpf.map('perf_event_array')
-- Kernel-space part of the program
bpf.uprobe('/bin/bash:readline' function (ptregs)
local sample = ffi.new(sample_t)
sample.pid = pid_tgid()
ffi.copy(sample.str, ffi.cast('char *', req.ax)) -- Cast `ax` to string pointer and copy to buffer
perf_submit(events, sample) -- Write sample to perf event map
end, true, -1, 0)
-- User-space part of the program
local log = events:reader(nil, 0, sample_t) -- Must specify PID or CPU_ID to observe
while true do
log:block() -- Wait until event reader is readable
for _,e in log:read() do -- Collect available reader events
print(tonumber(e.pid), ffi.string(e.str))
end
end
```
Where cast to `struct pt_regs` flags the source of data as probe arguments, which means any pointer derived
from this structure points to kernel and a helper is needed to access it. Casting `req.ax` to pointer is then required for `ffi.copy` semantics, otherwise it would be treated as `u64` and only it's value would be
copied. The type detection is automatic most of the times (socket filters and `bpf.tracepoint`), but not with uprobes and kprobes.
### Installation
```bash
$ luarocks install bpf
```
### Examples
See `examples/lua` directory.
### Helpers
* `print(...)` is a wrapper for `bpf_trace_printk`, the output is captured in `cat /sys/kernel/debug/tracing/trace_pipe`
* `bit.*` library **is** supported (`lshift, rshift, arshift, bnot, band, bor, bxor`)
* `math.*` library *partially* supported (`log2, log, log10`)
* `ffi.cast()` is implemented (including structures and arrays)
* `ffi.new(...)` allocates memory on stack, initializers are NYI
* `ffi.copy(...)` copies memory (possibly using helpers) between stack/kernel/registers
* `ntoh(x[, width])` - convert from network to host byte order.
* `hton(x[, width])` - convert from host to network byte order.
* `xadd(dst, inc)` - exclusive add, a synchronous `*dst += b` if Lua had `+=` operator
Below is a list of BPF-specific helpers:
* `time()` - return current monotonic time in nanoseconds (uses `bpf_ktime_get_ns`)
* `cpu()` - return current CPU number (uses `bpf_get_smp_processor_id`)
* `pid_tgid()` - return caller `tgid << 32 | pid` (uses `bpf_get_current_pid_tgid`)
* `uid_gid()` - return caller `gid << 32 | uid` (uses `bpf_get_current_uid_gid`)
* `comm(var)` - write current process name (uses `bpf_get_current_comm`)
* `perf_submit(map, var)` - submit variable to perf event array BPF map
* `stack_id(map, flags)` - return stack trace identifier from stack trace BPF map
### Current state
* Not all LuaJIT bytecode opcodes are supported *(notable mentions below)*
* Closures `UCLO` will probably never be supported, although you can use upvalues inside compiled function.
* Type narrowing is opportunistic. Numbers are 64-bit by default, but 64-bit immediate loads are not supported (e.g. `local x = map[ffi.cast('uint64_t', 1000)]`)
* Tail calls `CALLT`, and iterators `ITERI` are NYI (as of now)
* Arbitrary ctype **is** supported both for map keys and values
* Basic optimisations like: constant propagation, partial DCE, liveness analysis and speculative register allocation are implement, but there's no control flow analysis yet. This means the compiler has the visibility when things are used and dead-stores occur, but there's no rewriter pass to eliminate them.
* No register sub-allocations, no aggressive use of caller-saved `R1-5`, no aggressive narrowing (this would require variable range assertions and variable relationships)
* Slices with not 1/2/4/8 length are NYI (requires allocating a memory on stack and using pointer type)
[bcc]: https://github.com/iovisor/bcc
[tracing]: http://www.brendangregg.com/blog/2016-03-05/linux-bpf-superpowers.html
[bashreadline]: http://www.brendangregg.com/blog/2016-02-08/linux-ebpf-bcc-uprobes.html
\ No newline at end of file
......@@ -15,21 +15,25 @@ limitations under the License.
]]
local ffi = require("ffi")
ffi.cdef[[
typedef int clockid_t;
typedef long time_t;
struct timespec {
time_t tv_sec;
long tv_nsec;
};
int clock_gettime(clockid_t clk_id, struct timespec *tp);
int clock_nanosleep(clockid_t clock_id, int flags,
const struct timespec *request, struct timespec *remain);
-- Avoid duplicate declarations if syscall library is present
local has_syscall, _ = pcall(require, "syscall")
if not has_syscall then
ffi.cdef [[
typedef int clockid_t;
typedef long time_t;
struct timespec {
time_t tv_sec;
long tv_nsec;
};
int clock_gettime(clockid_t clk_id, struct timespec *tp);
int clock_nanosleep(clockid_t clock_id, int flags,
const struct timespec *request, struct timespec *remain);
]]
end
ffi.cdef [[
int get_nprocs(void);
uint64_t strtoull(const char *nptr, char **endptr, int base);
]]
......
package = "bpf"
version = "scm-1"
source = {
url = "git://github.com/iovisor/bcc.git"
}
description = {
summary = "BCC - LuaJIT to BPF compiler.",
detailed = [[
]],
homepage = "https://github.com/iovisor/bcc",
license = "BSD"
}
dependencies = {
"lua >= 5.1",
"ljsyscall >= 0.12",
}
external_dependencies = {
LIBELF = {
library = "elf"
}
}
build = {
type = "builtin",
install = {
bin = {
}
},
modules = {
bpf = "src/lua/bpf/bpf.lua",
["bpf.builtins"] = "src/lua/bpf/builtins.lua",
["bpf.cdef"] = "src/lua/bpf/cdef.lua",
["bpf.elf"] = "src/lua/bpf/elf.lua",
["bpf.init"] = "src/lua/bpf/init.lua",
["bpf.ljbytecode"] = "src/lua/bpf/ljbytecode.lua",
["bpf.proto"] = "src/lua/bpf/proto.lua",
}
}
This diff is collapsed.
This diff is collapsed.
--[[
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 S = require('syscall')
local M = {}
ffi.cdef [[
struct bpf {
/* Instruction classes */
static const int LD = 0x00;
static const int LDX = 0x01;
static const int ST = 0x02;
static const int STX = 0x03;
static const int ALU = 0x04;
static const int JMP = 0x05;
static const int ALU64 = 0x07;
/* ld/ldx fields */
static const int W = 0x00;
static const int H = 0x08;
static const int B = 0x10;
static const int ABS = 0x20;
static const int IND = 0x40;
static const int MEM = 0x60;
static const int LEN = 0x80;
static const int MSH = 0xa0;
/* alu/jmp fields */
static const int ADD = 0x00;
static const int SUB = 0x10;
static const int MUL = 0x20;
static const int DIV = 0x30;
static const int OR = 0x40;
static const int AND = 0x50;
static const int LSH = 0x60;
static const int RSH = 0x70;
static const int NEG = 0x80;
static const int MOD = 0x90;
static const int XOR = 0xa0;
static const int JA = 0x00;
static const int JEQ = 0x10;
static const int JGT = 0x20;
static const int JGE = 0x30;
static const int JSET = 0x40;
static const int K = 0x00;
static const int X = 0x08;
static const int JNE = 0x50; /* jump != */
static const int JSGT = 0x60; /* SGT is signed '>', GT in x86 */
static const int JSGE = 0x70; /* SGE is signed '>=', GE in x86 */
static const int CALL = 0x80; /* function call */
static const int EXIT = 0x90; /* function return */
/* ld/ldx fields */
static const int DW = 0x18; /* double word */
static const int XADD = 0xc0; /* exclusive add */
/* alu/jmp fields */
static const int MOV = 0xb0; /* mov reg to reg */
static const int ARSH = 0xc0; /* sign extending arithmetic shift right */
/* change endianness of a register */
static const int END = 0xd0; /* flags for endianness conversion: */
static const int TO_LE = 0x00; /* convert to little-endian */
static const int TO_BE = 0x08; /* convert to big-endian */
/* misc */
static const int PSEUDO_MAP_FD = 0x01;
/* helper functions */
static const int F_CURRENT_CPU = 0xffffffff;
static const int F_USER_STACK = 1 << 8;
static const int F_FAST_STACK_CMP = 1 << 9;
static const int F_REUSE_STACKID = 1 << 10;
};
/* eBPF commands */
struct bpf_cmd {
static const int MAP_CREATE = 0;
static const int MAP_LOOKUP_ELEM = 1;
static const int MAP_UPDATE_ELEM = 2;
static const int MAP_DELETE_ELEM = 3;
static const int MAP_GET_NEXT_KEY = 4;
static const int PROG_LOAD = 5;
static const int OBJ_PIN = 6;
static const int OBJ_GET = 7;
};
/* eBPF helpers */
struct bpf_func_id {
static const int unspec = 0;
static const int map_lookup_elem = 1;
static const int map_update_elem = 2;
static const int map_delete_elem = 3;
static const int probe_read = 4;
static const int ktime_get_ns = 5;
static const int trace_printk = 6;
static const int get_prandom_u32 = 7;
static const int get_smp_processor_id = 8;
static const int skb_store_bytes = 9;
static const int l3_csum_replace = 10;
static const int l4_csum_replace = 11;
static const int tail_call = 12;
static const int clone_redirect = 13;
static const int get_current_pid_tgid = 14;
static const int get_current_uid_gid = 15;
static const int get_current_comm = 16;
static const int get_cgroup_classid = 17;
static const int skb_vlan_push = 18;
static const int skb_vlan_pop = 19;
static const int skb_get_tunnel_key = 20;
static const int skb_set_tunnel_key = 21;
static const int perf_event_read = 22;
static const int redirect = 23;
static const int get_route_realm = 24;
static const int perf_event_output = 25;
static const int skb_load_bytes = 26;
static const int get_stackid = 27;
};
/* BPF_MAP_STACK_TRACE structures and constants */
static const int BPF_MAX_STACK_DEPTH = 127;
struct bpf_stacktrace {
uint64_t ip[BPF_MAX_STACK_DEPTH];
};
]]
-- Compatibility: ljsyscall doesn't have support for BPF syscall
if not S.bpf then
error("ljsyscall doesn't support bpf(), must be updated")
else
-- Compatibility: ljsyscall<=0.12
if not S.c.BPF_MAP.PERCPU_HASH then
S.c.BPF_MAP.PERCPU_HASH = 5
S.c.BPF_MAP.PERCPU_ARRAY = 6
S.c.BPF_MAP.STACK_TRACE = 7
S.c.BPF_MAP.CGROUP_ARRAY = 8
end
if not S.c.BPF_PROG.TRACEPOINT then
S.c.BPF_PROG.TRACEPOINT = 5
end
end
-- Compatibility: metatype for stacktrace
local function stacktrace_iter(t, i)
i = i + 1
if i < #t and t.ip[i] > 0 then
return i, t.ip[i]
end
end
ffi.metatype('struct bpf_stacktrace', {
__len = function (t) return ffi.sizeof(t.ip) / ffi.sizeof(t.ip[0]) end,
__ipairs = function (t) return stacktrace_iter, t, -1 end,
})
-- Reflect cdata type
function M.typename(v)
if not v or type(v) ~= 'cdata' then return nil end
return string.match(tostring(ffi.typeof(v)), '<([^>]+)')
end
-- Reflect if cdata type can be pointer (accepts array or pointer)
function M.isptr(v, noarray)
local ctname = M.typename(v)
if ctname then
ctname = string.sub(ctname, -1)
ctname = ctname == '*' or (not noarray and ctname == ']')
end
return ctname
end
function M.osversion()
-- We have no better way to extract current kernel hex-string other
-- than parsing headers, compiling a helper function or reading /proc
local ver_str, count = S.sysctl('kernel.version'):match('%d+.%d+.%d+'), 2
if not ver_str then -- kernel.version is freeform, fallback to kernel.osrelease
ver_str = S.sysctl('kernel.osrelease'):match('%d+.%d+.%d+')
end
local version = 0
for i in ver_str:gmatch('%d+') do -- Convert 'X.Y.Z' to 0xXXYYZZ
version = bit.bor(version, bit.lshift(tonumber(i), 8*count))
count = count - 1
end
return version
end
function M.event_reader(reader, event_type)
-- Caller can specify event message binary format
if event_type then
assert(type(event_type) == 'string' and ffi.typeof(event_type), 'not a valid type for event reader')
event_type = ffi.typeof(event_type .. '*') -- Convert type to pointer-to-type
end
-- Wrap reader in interface that can interpret read event messages
return setmetatable({reader=reader,type=event_type}, {__index = {
block = function(self)
return S.select { readfds = {reader.fd} }
end,
next = function(self, k)
local len, ev = reader:next(k)
-- Filter out only sample frames
while ev and ev.type ~= S.c.PERF_RECORD.SAMPLE do
len, ev = reader:next(len)
end
if ev and event_type then
-- The perf event reader returns framed data with header and variable length
-- This is going skip the frame header and cast data to given type
ev = ffi.cast(event_type, ffi.cast('char *', ev) + ffi.sizeof('struct perf_event_header') + ffi.sizeof('uint32_t'))
end
return len, ev
end,
read = function(self)
return self.next, self, nil
end,
}})
end
function M.tracepoint_type(tp)
-- Read tracepoint format string
local fp = assert(io.open('/sys/kernel/debug/tracing/events/'..tp..'/format', 'r'))
local fmt = fp:read '*a'
fp:close()
-- Parse struct fields
local fields = {}
for f in fmt:gmatch 'field:([^;]+;)' do
table.insert(fields, f)
end
return string.format('struct { %s }', table.concat(fields))
end
return M
\ No newline at end of file
--[[
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.
]]
-- This is a tiny wrapper over libelf to extract load address
-- and offsets of dynamic symbols
local S = require('syscall')
local ffi = require('ffi')
ffi.cdef [[
/* Type for a 16-bit quantity. */
typedef uint16_t Elf32_Half;
typedef uint16_t Elf64_Half;
/* Types for signed and unsigned 32-bit quantities. */
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;
typedef uint32_t Elf64_Word;
typedef int32_t Elf64_Sword;
/* Types for signed and unsigned 64-bit quantities. */
typedef uint64_t Elf32_Xword;
typedef int64_t Elf32_Sxword;
typedef uint64_t Elf64_Xword;
typedef int64_t Elf64_Sxword;
/* Type of addresses. */
typedef uint32_t Elf32_Addr;
typedef uint64_t Elf64_Addr;
/* Type of file offsets. */
typedef uint32_t Elf32_Off;
typedef uint64_t Elf64_Off;
/* Type for section indices, which are 16-bit quantities. */
typedef uint16_t Elf32_Section;
typedef uint16_t Elf64_Section;
/* Constants */
struct Elf_Cmd
{
static const int READ = 1;
static const int RDWR = 2;
static const int WRITE = 3;
static const int CLR = 4;
static const int SET = 5;
static const int FDDONE = 6;
static const int FDREAD = 7;
static const int READ_MMAP = 8;
static const int RDWR_MMAP = 9;
static const int WRITE_MMAP =10;
static const int READ_MMAP_PRIVATE =11;
static const int EMPTY =12;
static const int NUM =13;
};
/* Descriptor for the ELF file. */
typedef struct Elf Elf;
/* Descriptor for ELF file section. */
typedef struct Elf_Scn Elf_Scn;
/* Container type for metatable */
struct Elf_object { int fd; Elf *elf; };
/* Program segment header. */
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
typedef Elf64_Phdr GElf_Phdr;
/* Section header. */
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
typedef Elf64_Shdr GElf_Shdr;
/* Descriptor for data to be converted to or from memory format. */
typedef struct
{
void *d_buf; /* Pointer to the actual data. */
int d_type; /* Type of this piece of data. */
unsigned int d_version; /* ELF version. */
size_t d_size; /* Size in bytes. */
uint64_t d_off; /* Offset into section. */
size_t d_align; /* Alignment in section. */
} Elf_Data;
/* Symbol table entry. */
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
typedef Elf64_Sym GElf_Sym;
/* Coordinate ELF library and application versions. */
unsigned int elf_version (unsigned int __version);
/* Return descriptor for ELF file to work according to CMD. */
Elf *elf_begin (int __fildes, int __cmd, Elf *__ref);
/* Free resources allocated for ELF. */
int elf_end (Elf *__elf);
/* Get the number of program headers in the ELF file. If the file uses
more headers than can be represented in the e_phnum field of the ELF
header the information from the sh_info field in the zeroth section
header is used. */
int elf_getphdrnum (Elf *__elf, size_t *__dst);
/* Retrieve program header table entry. */
GElf_Phdr *gelf_getphdr (Elf *__elf, int __ndx, GElf_Phdr *__dst);
/* Retrieve section header. */
GElf_Shdr *gelf_getshdr (Elf_Scn *__scn, GElf_Shdr *__dst);
/* Retrieve symbol information from the symbol table at the given index. */
GElf_Sym *gelf_getsym (Elf_Data *__data, int __ndx, GElf_Sym *__dst);
/* Get section with next section index. */
Elf_Scn *elf_nextscn (Elf *__elf, Elf_Scn *__scn);
/* Get data from section while translating from file representation
to memory representation. */
Elf_Data *elf_getdata (Elf_Scn *__scn, Elf_Data *__data);
/* Return pointer to string at OFFSET in section INDEX. */
char *elf_strptr (Elf *__elf, size_t __index, size_t __offset);
]]
local elf = ffi.load('elf')
local EV = { NONE=0, CURRENT=1, NUM=2 }
local PT = { NULL=0, LOAD=1, DYNAMIC=2, INTERP=3, NOTE=4, SHLIB=5, PHDR=6, TLS=7, NUM=8 }
local SHT = { NULL=0, PROGBITS=1, SYMTAB=2, STRTAB=3, RELA=4, HASH=5, DYNAMIC=6, NOTE=7,
NOBITS=8, REL=9, SHLIB=10, DYNSYM=11, INIT_ARRAY=14, FINI_ARRAY=15, PREINIT_ARRAY=16,
GROUP=17, SYMTAB_SHNDX=18, NUM=19 }
local ELF_C = ffi.new('struct Elf_Cmd')
local M = {}
-- Optional poor man's C++ demangler
local cpp_demangler = os.getenv('CPP_DEMANGLER')
if not cpp_demangler then
for prefix in string.gmatch(os.getenv('PATH'), '[^;:]+') do
if S.statfs(prefix..'/c++filt') then
cpp_demangler = prefix..'/c++filt'
break
end
end
end
local cpp_demangle = function (name) return name end
if cpp_demangler then
cpp_demangle = function (name)
local cmd = string.format('%s -p %s', cpp_demangler, name)
local fp = assert(io.popen(cmd, 'r'))
local output = fp:read('*all')
fp:close()
return output:match '^(.-)%s*$'
end
end
-- Metatable for ELF object
ffi.metatype('struct Elf_object', {
__gc = function (t) t:close() end,
__index = {
close = function (t)
if t.elf ~= nil then
elf.elf_end(t.elf)
S.close(t.fd)
t.elf = nil
end
end,
-- Load library load address
loadaddr = function(t)
local phnum = ffi.new('size_t [1]')
if elf.elf_getphdrnum(t.elf, phnum) == nil then
return nil, 'cannot get phdrnum'
end
local header = ffi.new('GElf_Phdr [1]')
for i = 0, tonumber(phnum[0])-1 do
if elf.gelf_getphdr(t.elf, i, header) ~= nil
and header[0].p_type == PT.LOAD then
return header[0].p_vaddr
end
end
end,
-- Resolve symbol address
resolve = function (t, k, pattern)
local section = elf.elf_nextscn(t.elf, nil)
while section ~= nil do
local header = ffi.new('GElf_Shdr [1]')
if elf.gelf_getshdr(section, header) ~= nil then
if header[0].sh_type == SHT.SYMTAB or header[0].sh_type == SHT.DYNSYM then
local data = elf.elf_getdata(section, nil)
while data ~= nil do
if data.d_size % header[0].sh_entsize > 0 then
return nil, 'bad section header entity size'
end
local symcount = tonumber(data.d_size / header[0].sh_entsize)
local sym = ffi.new('GElf_Sym [1]')
for i = 0, symcount - 1 do
if elf.gelf_getsym(data, i, sym) ~= nil then
local name = elf.elf_strptr(t.elf, header[0].sh_link, sym[0].st_name)
if name ~= nil then
-- Demangle C++ symbols if necessary
name = ffi.string(name)
if name:sub(1,2) == '_Z' then
name = cpp_demangle(name)
end
-- Match symbol name against pattern
if pattern and string.match(name, k) or k == name then
return sym[0]
end
end
end
end
data = elf.elf_getdata(section, data)
end
end
end
section = elf.elf_nextscn(t.elf, section)
end
end,
}
})
-- Open an ELF object
function M.open(path)
if elf.elf_version(EV.CURRENT) == EV.NONE then
return nil, 'bad version'
end
local fd, err = S.open(path, 'rdonly')
if not fd then return nil, err end
local pt = ffi.new('Elf *')
pt = elf.elf_begin(fd:getfd(), ELF_C.READ, pt)
if not pt then
fd:close()
return nil, 'cannot open elf object'
end
return ffi.new('struct Elf_object', fd:nogc():getfd(), pt)
end
return M
\ No newline at end of file
--[[
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.
]]
return require('bpf.bpf')
--[[
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 jutil = require("jit.util")
local vmdef = require("jit.vmdef")
local bit = require('bit')
local shr, band = bit.rshift, bit.band
-- Decode LuaJIT 2.0 Byte Format
-- Reference: http://wiki.luajit.org/Bytecode-2.0
-- Thanks to LJ, we get code in portable bytecode with constants folded, basic
-- virtual registers allocated etc.
-- No SSA IR, type inference or advanced optimizations because the code wasn't traced yet.
local function decode_ins(func, pc)
local ins, m = jutil.funcbc(func, pc)
if not ins then return nil end
local op, ma, mb, mc = band(ins, 0xff), band(m, 7), band(m, 15*8), band(m, 15*128)
local a, b, c, d = band(shr(ins, 8), 0xff), nil, nil, shr(ins, 16)
if mb ~= 0 then
d = band(d, 0xff)
b = shr(ins, 24)
end
if ma == 5 then -- BCMuv
a = jutil.funcuvname(func, a)
end
if mc == 13*128 then -- BCMjump
c = pc+d-0x7fff
elseif mc == 9*128 then -- BCMint
c = jutil.funck(func, d)
elseif mc == 10*128 then -- BCMstr
c = jutil.funck(func, -d-1)
elseif mc == 5*128 then -- BCMuv
c = jutil.funcuvname(func, d)
end
-- Convert version-specific opcode to string
op = 6*op
op = string.sub(vmdef.bcnames, op+1, op+6):match('[^%s]+')
return pc, op, a, b, c, d
end
-- Decoder closure
local function decoder(func)
local pc = 0
return function ()
pc = pc + 1
return decode_ins(func, pc)
end
end
-- Hexdump generated code
local function dump(func)
return require('jit.bc').dump(func)
end
return {
decode = decode_ins,
decoder = decoder,
dump = dump,
funcinfo = function (...) return jutil.funcinfo(...) end,
}
\ No newline at end of file
This diff is collapsed.
......@@ -13,5 +13,13 @@ Module "bcc.tracerpipe" "bcc/tracerpipe.lua"
Module "bcc.table" "bcc/table.lua"
Module "bcc.usdt" "bcc/usdt.lua"
Module "bpf" "bpf/init.lua"
Module "bpf.bpf" "bpf/bpf.lua"
Module "bpf.builtins" "bpf/builtins.lua"
Module "bpf.cdef" "bpf/cdef.lua"
Module "bpf.elf" "bpf/elf.lua"
Module "bpf.ljbytecode" "bpf/ljbytecode.lua"
Module "bpf.proto" "bpf/proto.lua"
Main "bcc/run.lua"
Output "bcc.lua"
-- Configuration for unit tests
-- See: http://olivinelabs.com/busted/
return {
default = {
lpath = "./?.lua",
["auto-insulate"] = false,
}
}
\ No newline at end of file
std = "luajit"
ignore = { "211", "212", "411", "412", "421", "431", "542" }
files["examples"] = {
new_globals = { "pkt", "time", "xadd", "c" }
}
files["bpf/builtins.lua"] = {
ignore = { "122" }
}
files["spec"] = {
std = "+busted",
new_globals = { "pkt", "time", "xadd", "c" }
}
\ No newline at end of file
find_program(LUAJIT luajit)
find_program(BUSTED busted)
if(LUAJIT)
add_test(NAME lua_test_clang WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
......@@ -12,4 +13,9 @@ if(LUAJIT)
add_test(NAME lua_test_standalone WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test_standalone.sh)
if(BUSTED)
add_test(NAME lua_test_busted WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND busted --lua=${LUAJIT} -m "${CMAKE_CURRENT_SOURCE_DIR}/../../src/lua/?.lua" -m "${CMAKE_CURRENT_SOURCE_DIR}/../../src/lua/?/init.lua;")
endif()
endif()
# Unit test specs
This directory contains spec files for Lua BPF in [Busted] unit test format.
[Busted]: http://olivinelabs.com/busted/
describe('compile', function()
local ffi = require('ffi')
local bpf = require('bpf')
it('can compile socket filter', function()
-- Create mock BPF map
local mock_map = {
max_entries = 16,
key_type = ffi.typeof('uint64_t [1]'),
val_type = ffi.typeof('uint64_t [1]'),
fd = 1,
__map = true,
}
-- Compile small code example
local code = bpf(function ()
local proto = pkt.ip.proto
xadd(mock_map[proto], 1)
end)
assert.truthy(code)
assert.same(type(code), 'table')
assert.same(code.pc, 15)
end)
end)
describe('decoder', function()
-- Decode simple function
local bytecode = require('bpf.ljbytecode')
local f = function (x) return x + 1 end
it('should decode functions', function()
-- Make sure it calls LJ decoder
local bc = bytecode.decoder(f)
assert.truthy(bc)
-- Decode bytecode bytecode to instructions
local jutil = require("jit.util")
spy.on(jutil, 'funcbc')
local pc, op = bc()
-- Check bytecode for sanity (starts with ADDVN(x, 1))
assert.equal(pc, 1)
assert.equal(op, 'ADDVN')
for pc, op in bc do
assert.truthy(pc and op)
end
assert.spy(jutil.funcbc).was.called()
end)
it('should fail on bad input', function()
assert.has_error(function() bytecode.decoder(nil)() end)
assert.has_error(function() bytecode.decoder(5)() end)
assert.has_error(function() bytecode.decoder('test')() end)
end)
it('should dump bytecode', function()
bytecode.dump(f)
end)
end)
describe('elf reader', function()
local ok, elf = pcall(require, 'bpf.elf')
if not ok then return end
it('should handle C library', function()
-- Open libc
local sh = elf.open('/bin/sh')
assert.truthy(sh)
-- Find load address
local base = sh:loadaddr()
assert.truthy(base)
-- Find something from ISO C
local malloc_addr = sh:resolve('malloc')
assert.truthy(malloc_addr)
-- Find something that doesn't exist
local bad_addr = sh:resolve('thisnotexists')
assert.falsy(bad_addr)
end)
it('should fail on bad input', function()
assert.falsy(elf.open(nil))
assert.falsy(elf.open('/tmp'):loadaddr())
end)
end)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment