Skip to content

Commit

Permalink
Lua Tools for BCC
Browse files Browse the repository at this point in the history
  • Loading branch information
vmg committed Mar 30, 2016
1 parent ff9f231 commit 39468d9
Show file tree
Hide file tree
Showing 15 changed files with 3,052 additions and 0 deletions.
54 changes: 54 additions & 0 deletions src/lua/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Lua Tools for BCC
-----------------

This directory contains Lua tooling for [BCC](https://github.com/iovisor/bcc)
(the BPF Compiler Collection).

BCC is a toolkit for creating userspace and kernel tracing programs. By
default, it comes with a library `libbcc`, some example tooling and a Python
frontend for the library.

Here we present an alternate frontend for `libbcc` implemented in LuaJIT. This
lets you write the userspace part of your tracer in Lua instead of Python.

Since LuaJIT is a JIT compiled language, tracers implemented in `bcc-lua`
exhibit significantly reduced overhead compared to their Python equivalents.
This is particularly noticeable in tracers that actively use the table APIs to
get information from the kernel.

If your tracer makes extensive use of `BPF_MAP_TYPE_PERF_EVENT_ARRAY` or
`BPF_MAP_TYPE_HASH`, you may find the performance characteristics of this
implementation very appealing, as LuaJIT can compile to native code a lot of
the callchain to process the events, and this wrapper has been designed to
benefit from such JIT compilation.

## Quickstart Guide

The following instructions assume Ubuntu 14.04 LTS.

1. Install a **very new kernel**. It has to be new and shiny for this to work. 4.3+

```
VER=4.4.2-040402
PREFIX=http:https://kernel.ubuntu.com/~kernel-ppa/mainline/v4.4.2-wily/
REL=201602171633
wget ${PREFIX}/linux-headers-${VER}-generic_${VER}.${REL}_amd64.deb
wget ${PREFIX}/linux-headers-${VER}_${VER}.${REL}_all.deb
wget ${PREFIX}/linux-image-${VER}-generic_${VER}.${REL}_amd64.deb
sudo dpkg -i linux-*${VER}.${REL}*.deb
```

2. Install the `libbcc` binary packages and `luajit`

```
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys D4284CDD
echo "deb http:https://52.8.15.63/apt trusty main" | sudo tee /etc/apt/sources.list.d/iovisor.list
sudo apt-get update
sudo apt-get install libbcc luajit
```

3. Test one of the examples to ensure `libbcc` is properly installed

```
sudo ./bcc-probe examples/lua/task_switch.lua
```
38 changes: 38 additions & 0 deletions src/lua/bcc-probe
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env luajit
--[[
Copyright 2016 GitHub, Inc

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:https://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.
]]
package.path = "./?/init.lua;"..package.path

local BCC = require("bcc")
local BPF = BCC.BPF

BPF.script_root(arg[1])

local utils = {
argparse = require("bcc.vendor.argparse"),
posix = require("bcc.vendor.posix"),
sym = BCC.sym
}

local tracefile = table.remove(arg, 1)
local command = require(tracefile:gsub("%.lua", ""))
local res, err = pcall(command, BPF, utils)

if not res then
io.stderr:write("[ERROR] "..err.."\n")
end

BPF.cleanup_probes()
249 changes: 249 additions & 0 deletions src/lua/bcc/bpf.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
--[[
Copyright 2016 GitHub, Inc
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:https://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 libbcc = require("bcc.libbcc")

local TracerPipe = require("bcc.tracerpipe")
local Table = require("bcc.table")
local LD = require("bcc.ld")

local Bpf = class("BPF")

Bpf.static.open_kprobes = {}
Bpf.static.open_uprobes = {}
Bpf.static.process_symbols = {}
Bpf.static.KPROBE_LIMIT = 1000
Bpf.static.tracer_pipe = nil

function Bpf.static.check_probe_quota(n)
local cur = table.count(Bpf.static.open_kprobes) + table.count(Bpf.static.open_uprobes)
assert(cur + n <= Bpf.static.KPROBE_LIMIT, "number of open probes would exceed quota")
end

function Bpf.static.cleanup_probes()
local function detach_all(probe_type, all_probes)
for key, probe in pairs(all_probes) do
libbcc.perf_reader_free(probe)
if type(key) == "string" then
local desc = string.format("-:%s/%s", probe_type, key)
-- TODO: why so noisy?
--libbcc.bpf_detach_kprobe(desc)
end
all_probes[key] = nil
end
end

detach_all("kprobes", Bpf.static.open_kprobes)
detach_all("uprobes", Bpf.static.open_uprobes)
if Bpf.static.tracer_pipe ~= nil then
Bpf.static.tracer_pipe:close()
end
end

function Bpf.static.num_open_kprobes()
return table.count(Bpf.static.open_kprobes)
end

function Bpf.static.usymaddr(pid, addr, refresh)
local proc_sym = Bpf.static.process_symbols[pid]

if proc_sym == nil then
proc_sym = ProcSymbols(pid)
Bpf.static.process_symbols[pid] = proc_sym
elseif refresh then
proc_sym.refresh()
end

return proc_sym.decode_addr(addr)
end

Bpf.static.SCRIPT_ROOT = "./"
function Bpf.static.script_root(root)
local dir, file = root:match'(.*/)(.*)'
Bpf.static.SCRIPT_ROOT = dir or "./"
return Bpf
end

local function _find_file(script_root, filename)
if filename == nil then
return nil
end

if os.exists(filename) then
return filename
end

if not filename:starts("/") then
filename = script_root .. filename
if os.exists(filename) then
return filename
end
end

assert(nil, "failed to find file "..filename.." (root="..script_root..")")
end

function Bpf:initialize(args)
self.do_debug = args.debug or false
self.funcs = {}
self.tables = {}

if args.text then
log.info("\n%s\n", args.text)
self.module = libbcc.bpf_module_create_c_from_string(args.text, args.llvm_debug or 0, nil, 0)
elseif args.src_file then
local src = _find_file(Bpf.SCRIPT_ROOT, args.src_file)

if src:ends(".b") then
local hdr = _find_file(Bpf.SCRIPT_ROOT, args.hdr_file)
self.module = libbcc.bpf_module_create_b(src, hdr, args.llvm_debug or 0)
else
self.module = libbcc.bpf_module_create_c(src, args.llvm_debug or 0, nil, 0)
end
end

assert(self.module ~= nil, "failed to compile BPF module")
end

function Bpf:load_func(fn_name, prog_type)
if self.funcs[fn_name] ~= nil then
return self.funcs[fn_name]
end

assert(libbcc.bpf_function_start(self.module, fn_name), "unknown program: "..fn_name)

local fd = libbcc.bpf_prog_load(prog_type,
libbcc.bpf_function_start(self.module, fn_name),
libbcc.bpf_function_size(self.module, fn_name),
libbcc.bpf_module_license(self.module),
libbcc.bpf_module_kern_version(self.module), nil, 0)

assert(fd >= 0, "failed to load BPF program "..fn_name)
log.info("loaded %s (%d)", fn_name, fd)

local fn = {bpf=self, name=fn_name, fd=fd}
self.funcs[fn_name] = fn
return fn
end

function Bpf:attach_uprobe(args)
Bpf.check_probe_quota(1)

local path, addr = LD.check_path_symbol(args.name, args.sym, args.addr)
local fn = self:load_func(args.fn_name, 'BPF_PROG_TYPE_KPROBE')
local ptype = args.retprobe and "r" or "p"
local ev_name = string.format("%s_%s_0x%x", ptype, path:gsub("[^%a%d]", "_"), addr)
local desc = string.format("%s:uprobes/%s %s:0x%x", ptype, ev_name, path, addr)

log.info(desc)

local res = libbcc.bpf_attach_uprobe(fn.fd, ev_name, desc,
args.pid or -1,
args.cpu or 0,
args.group_fd or -1, nil, nil) -- TODO; reader callback

assert(res ~= nil, "failed to attach BPF to uprobe")
self:probe_store("uprobe", ev_name, res)
return self
end

function Bpf:attach_kprobe(args)
-- TODO: allow the caller to glob multiple functions together
Bpf.check_probe_quota(1)

local fn = self:load_func(args.fn_name, 'BPF_PROG_TYPE_KPROBE')
local event = args.event or ""
local ptype = args.retprobe and "r" or "p"
local ev_name = string.format("%s_%s", ptype, event:gsub("[%+%.]", "_"))
local desc = string.format("%s:kprobes/%s %s", ptype, ev_name, event)

log.info(desc)

local res = libbcc.bpf_attach_kprobe(fn.fd, ev_name, desc,
args.pid or -1,
args.cpu or 0,
args.group_fd or -1, nil, nil) -- TODO; reader callback

assert(res ~= nil, "failed to attach BPF to kprobe")
self:probe_store("kprobe", ev_name, res)
return self
end

function Bpf:pipe()
if Bpf.tracer_pipe == nil then
Bpf.tracer_pipe = TracerPipe:new()
end
return Bpf.tracer_pipe
end

function Bpf:get_table(name, key_type, leaf_type)
if self.tables[name] == nil then
self.tables[name] = Table(self, name, key_type, leaf_type)
end
return self.tables[name]
end

function Bpf:probe_store(t, id, reader)
if t == "kprobe" then
Bpf.open_kprobes[id] = reader
elseif t == "uprobe" then
Bpf.open_uprobes[id] = reader
else
error("unknown probe type '%s'" % t)
end

log.info("%s -> %s", id, reader)
end

function Bpf:probe_lookup(t, id)
if t == "kprobe" then
return Bpf.open_kprobes[id]
elseif t == "uprobe" then
return Bpf.open_uprobes[id]
else
return nil
end
end

function Bpf:_kprobe_array()
local kprobe_count = table.count(Bpf.open_kprobes)
local readers = ffi.new("struct perf_reader*[?]", kprobe_count)
local n = 0

for _, r in pairs(Bpf.open_kprobes) do
readers[n] = r
n = n + 1
end

assert(n == kprobe_count, "wtf")
return readers, n
end

function Bpf:kprobe_poll_loop()
local probes, probe_count = self:_kprobe_array()
return pcall(function()
while true do
libbcc.perf_reader_poll(probe_count, probes, -1)
end
end)
end

function Bpf:kprobe_poll(timeout)
local probes, probe_count = self:_kprobe_array()
libbcc.perf_reader_poll(probe_count, probes, timeout or -1)
end

return Bpf
23 changes: 23 additions & 0 deletions src/lua/bcc/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--[[
Copyright 2016 GitHub, Inc
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:https://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.
]]
require("bcc.vendor.strict")
require("bcc.vendor.helpers")
class = require("bcc.vendor.middleclass")

return {
BPF = require("bcc.bpf"),
sym = require("bcc.sym"),
}
Loading

0 comments on commit 39468d9

Please sign in to comment.