Since function defined by us are in Userspace so we need to register
uprobe
kprobe
http
/e
/* * Copyright 2018- The Pixie Authors. * * 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. * * SPDX-License-Identifier: Apache-2.0 */ package main import ( "fmt" "net/http" "strconv" ) // computeE computes the approximation of e by running a fixed number of iterations. //go:noinline func computeE(iterations int64) float64 { res := 2.0 fact := 1.0 for i := int64(2); i < iterations; i++ { fact *= float64(i) res += 1 / fact } return res } func main() { addr := ":9090" http.HandleFunc("/e", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } iters := int64(100) keys, ok := r.URL.Query()["iters"] if ok && len(keys[0]) >= 1 { val, err := strconv.ParseInt(keys[0], 10, 64) if err != nil || val <= 0 { w.WriteHeader(http.StatusBadRequest) return } iters = val } w.Write([]byte(fmt.Sprintf("e = %0.4f\n", computeE(iters)))) }) fmt.Printf("Starting server on: %+v\n", addr) err := http.ListenAndServe(addr, nil) if err != nil && err != http.ErrServerClosed { fmt.Printf("Failed to run http server: %v\n", err) } }
We are goin to trace the arguments of the function ComputeE.
The uprobes functions are code snippets that can be run inside the kernel for acessing userspace programs mostly used for debugging and monitoring the app. The diagram below shows how the binary is modified by the Linux kernel with an uprobe. The soft-interrupt instruction (
int3
main.computeE
The logic is after the soft interrupt the bpf code is triggered by the uprobe hook which executes the bpf program after the bpf program writes the data to the
perf buffer
The tracer binary will be the userspace program which is responsible for registering the bpf code and reading the results from the bpf code.
/* * Copyright 2018- The Pixie Authors. * * 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. * * SPDX-License-Identifier: Apache-2.0 */ package main import ( "encoding/binary" "flag" "fmt" "github.com/iovisor/gobpf/bcc" "os" "os/signal" ) const bpfProgram = ` #include <uapi/linux/ptrace.h> // This line includes necssary //header files for the eBPF programs. BPF_PERF_OUTPUT(trace); // This function will be registered to be called everytime // main.computeE is called. inline int computeECalled(struct pt_regs *ctx) { // The input argument is stored in ax. long val = ctx->ax; trace.perf_submit(ctx, &val, sizeof(val)); return 0; } ` var binaryProg string func init() { flag.StringVar(&binaryProg, "binary", "", "The binary to probe") } func main() { flag.Parse() if len(binaryProg) == 0 { panic("Argument --binary needs to be specified") } bccMod := bcc.NewModule(bpfProgram, []string{}) uprobeFD, err := bccMod.LoadUprobe("computeECalled") if err != nil { panic(err) } // Attach the uprobe to be called everytime main.computeE is called. // We need to specify the path to the binary so it can be patched. err = bccMod.AttachUprobe(binaryProg, "main.computeE", uprobeFD, -1) if err != nil { panic(err) } // Create the output table named "trace" that the BPF program writes to. table := bcc.NewTable(bccMod.TableId("trace"), bccMod) ch := make(chan []byte) pm, err := bcc.InitPerfMap(table, ch, nil) if err != nil { panic(err) } // Watch Ctrl-C so we can quit this program. intCh := make(chan os.Signal, 1) signal.Notify(intCh, os.Interrupt) pm.Start() defer pm.Stop() for { select { case <-intCh: fmt.Println("Terminating") os.Exit(0) case v := <-ch: // This is a bit of hack, but we know that iterations is a // 8 bytes int64 value. d := binary.LittleEndian.Uint64(v) fmt.Printf("Value = %v\n", d) } } }
The BPF program basically extracts values stores in the `ax` register using this struct `struct pt_regs` . `trace.perf_submit(ctx, &val, sizeof(val))` this line will submit the perf event using the trace object. In the `init()` we are asking for a binary as our paramet in this case our server's binary should be given. Using `gobpf/bcc` package we are attaching loading the `uprobe` by specifying the function name "computeEcalled".
The
uprobe
ax
main.computeE