Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

stackplz is an eBPF-based dynamic tracing tool for Android. It supports syscall tracing, uprobe hooking, hardware breakpoints, and stack unwinding on ARM64/ARM32 devices running kernel 5.10+.

The codebase consists of two parts:
- **eBPF C programs** (`src/`) — kernel-space probes compiled to BPF bytecode
- **Go user-space code** (`user/`, `cli/`, `main.go`) — loads eBPF programs, processes events, provides CLI

## Build Commands

**Prerequisites:** The project requires three sibling repos in the same parent directory:
```bash
# Must be cloned alongside stackplz at the same level
git clone https://github.com/SeeFlowerX/ebpf # modified cilium/ebpf
git clone https://github.com/SeeFlowerX/ebpfmanager # modified ehids/ebpfmanager
```
Also requires: `libbpf`, `bpftool v7.2.0`, Android NDK r25c, Go 1.18. Run `./build_env.sh` to download libbpf, bpftool, and BTF files.

**Build (ARM64):**
```bash
make clean && make
# Output: bin/stackplz_arm64
```

**Build (ARM32):**
```bash
make clean && BUILD_TAGS=forarm make
# Output: bin/stackplz_arm
```

**Build with debug prints:**
```bash
make clean && DEBUG=1 make
```

**Deploy to device:**
```bash
adb push bin/stackplz_arm64 /data/local/tmp
```

The `Makefile` pipeline: compile eBPF C → generate min BTF → pack assets via go-bindata → cross-compile Go binary for Android.

## Architecture

### Data Flow

```
CLI (cobra) → Module Manager → eBPF Programs (kernel)
Perf Ring Buffer
Event Processor (worker pool) → Output (stdout/file/json)
```

### Module System (`user/module/`)

All modules implement the `IModule` interface (`imodule.go`). Each module manages its own eBPF program loading, map configuration, and event parsing.

| Module | eBPF Source | Purpose |
|--------|-------------|---------|
| Stack (`stack.go`) | `src/stack.c` | uprobe/uretprobe hooking |
| Syscall (`syscall.go`) | `src/syscall.c` | raw_tracepoint sys_enter/sys_exit |
| Brk (`brk.go`) | — | Hardware breakpoints (via perf_event) |
| PerfMmap (`perf_mmap.go`) | `src/perf_mmap.c` | Process memory mapping tracking |

### Key Packages

- **`cli/cmd/`** — Cobra CLI entry point, all flag definitions in `root.go`
- **`user/config/`** — Configuration management: global config, module configs, JSON config file parsing, filter rules
- **`user/event/`** — Event types implementing `IEventStruct` interface (uprobe, syscall, brk, mmap, fork, exit, comm)
- **`user/argtype/`** — Argument type definitions and parsing (struct, string, sockaddr, buffer, register expressions)
- **`user/event_processor/`** — Concurrent worker pool for event processing
- **`user/event_parser/`** — Event binary data parsing
- **`user/rpc/`** — TCP RPC server (port 41718) for Frida integration to set hardware breakpoints
- **`user/util/`** — Android-specific helpers, BPF utilities

### eBPF Side (`src/`)

- `src/common/` — Shared headers for filtering, argument capture, buffer management
- `src/types.h` — Shared type definitions between kernel and user space
- `src/maps.h` — eBPF map definitions
- `src/vmlinux_510.h` — Auto-generated kernel struct definitions (110k lines, do not edit)
- Conditional compilation via `-D__MODULE_STACK` / `-D__MODULE_SYSCALL` and `-D__TARGET_ARCH_arm64` / `-D__TARGET_ARCH_arm`

### Asset Embedding

eBPF object files (`.o`), BTF files, preload libraries (`preload_libs/*.so`), and syscall config JSONs are packed into `assets/ebpf_probe.go` via `go-bindata` during build. This file is auto-generated — do not edit.

## go.mod `replace` Directives

The project uses `replace` directives pointing to `../ebpf` and `../ebpfmanager`. These are modified forks that add perf event options (stack sampling, register sampling). Standard cilium/ebpf will not work.

## Testing

There are no unit tests. The `tests/` directory contains JSON config files for manual functional testing on Android devices:
```bash
# Example: run with a test config on device
./stackplz -n com.example.app -c tests/config_uprobe_test.json
```

## CI

GitHub Actions (`.github/workflows/build.yml`) builds both ARM64 and ARM32 binaries on push/PR to `master` or `dev` branches.

## Language

All documentation, comments, and commit messages are in Chinese (Simplified).
25 changes: 21 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ TARGET_ARCH = arm
endif

.PHONY: all
all: ebpf_stack ebpf_syscall ebpf_perf_mmap genbtf assets build
all: ebpf_stack ebpf_syscall ebpf_perf_mmap ebpf_binder genbtf assets build
@echo $(shell date)


Expand Down Expand Up @@ -79,15 +79,32 @@ ebpf_perf_mmap:
-o user/assets/perf_mmap.o \
src/perf_mmap.c

.PHONY: ebpf_binder
ebpf_binder:
clang \
-D__TARGET_ARCH_$(TARGET_ARCH) \
-D__MODULE_BINDER \
--target=bpf \
-c \
-nostdlibinc \
-no-canonical-prefixes \
-O2 \
$(DEBUG_PRINT) \
-I libbpf/src \
-I src \
-g \
-o user/assets/binder.o \
src/binder.c

.PHONY: genbtf
genbtf:
cd ${ASSETS_PATH} && ./$(CMD_BPFTOOL) gen min_core_btf rock5b-5.10-f9d1b1529-arm64.btf rock5b-5.10-arm64_min.btf stack.o syscall.o
cd ${ASSETS_PATH} && ./$(CMD_BPFTOOL) gen min_core_btf a12-5.10-arm64.btf a12-5.10-arm64_min.btf stack.o syscall.o
cd ${ASSETS_PATH} && ./$(CMD_BPFTOOL) gen min_core_btf rock5b-5.10-f9d1b1529-arm64.btf rock5b-5.10-arm64_min.btf stack.o syscall.o binder.o
cd ${ASSETS_PATH} && ./$(CMD_BPFTOOL) gen min_core_btf a12-5.10-arm64.btf a12-5.10-arm64_min.btf stack.o syscall.o binder.o

.PHONY: assets
assets:
$(CMD_GO) run github.com/shuLhan/go-bindata/cmd/go-bindata -pkg assets -o "assets/ebpf_probe.go" $(wildcard ./user/config/config_syscall_*.json ./user/assets/*.o ./user/assets/*_min.btf ./preload_libs/*.so)

.PHONY: build
build:
GOARCH=arm64 GOOS=android CGO_ENABLED=1 CC=aarch64-linux-android29-clang $(CMD_GO) build $(BUILD_TAGS) -ldflags "-w -s -extldflags '-Wl,--hash-style=sysv'" -o bin/stackplz_$(TARGET_ARCH) .
GOARCH=arm64 GOOS=android CGO_ENABLED=1 CC=aarch64-linux-android29-clang $(CMD_GO) build $(BUILD_TAGS) -ldflags "-w -s -extldflags '-Wl,--hash-style=sysv'" -o bin/stackplz_$(TARGET_ARCH) .
52 changes: 45 additions & 7 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,22 @@ func persistentPreRunEFunc(command *cobra.Command, args []string) error {
mconfig.BrkAddr = brk_base + addr
}

// 5. binder trace
if gconfig.BinderTrace {
binderSconfig := &config.StackUprobeConfig{}
err = gconfig.Parse_Libinfo(gconfig.BinderLib, binderSconfig)
if err != nil {
return fmt.Errorf("parse binder library failed: %v", err)
}
mconfig.BinderConf.Enable = true
mconfig.BinderConf.LibPath = binderSconfig.LibPath
mconfig.BinderConf.RealFilePath = binderSconfig.RealFilePath
mconfig.BinderConf.Filter = gconfig.BinderFilter
mconfig.BinderConf.NoReply = gconfig.BinderNoReply
mconfig.BinderConf.DumpHex = gconfig.DumpHex
logger.Printf("binder trace enabled, lib:%s", binderSconfig.LibPath)
}

// 检查hook设定
enable_hook := false
if len(mconfig.StackUprobeConf.Points) > 0 {
Expand All @@ -357,8 +373,12 @@ func persistentPreRunEFunc(command *cobra.Command, args []string) error {
}
logger.Printf("set breakpoint at kernel:%t, addr:0x%x, type:%d", mconfig.BrkKernel, mconfig.BrkAddr, mconfig.BrkType)
}
if mconfig.BinderConf != nil && mconfig.BinderConf.Enable {
enable_hook = true
logger.Printf("binder trace enabled")
}
if !enable_hook {
logger.Fatal("hook nothing, plz set -w/--point or -s/--syscall or --brk")
logger.Fatal("hook nothing, plz set -w/--point or -s/--syscall or --brk or --binder")
}
if gconfig.ParseFile != "" {
parser := event_parser.NewEventParser()
Expand All @@ -384,16 +404,29 @@ func runFunc(command *cobra.Command, args []string) {
var wg sync.WaitGroup

var modNames []string
hasPerf := false
if mconfig.BrkAddr != 0 {
modNames = append(modNames, module.MODULE_NAME_BRK)
} else if mconfig.SysCallConf.Enable {
modNames = append(modNames, module.MODULE_NAME_PERF)
}
if mconfig.SysCallConf.Enable {
if !hasPerf {
modNames = append(modNames, module.MODULE_NAME_PERF)
hasPerf = true
}
modNames = append(modNames, module.MODULE_NAME_SYSCALL)
} else if len(mconfig.StackUprobeConf.Points) > 0 {
modNames = append(modNames, module.MODULE_NAME_PERF)
}
if len(mconfig.StackUprobeConf.Points) > 0 {
if !hasPerf {
modNames = append(modNames, module.MODULE_NAME_PERF)
hasPerf = true
}
modNames = append(modNames, module.MODULE_NAME_STACK)
} else {
Logger.Fatal("hook nothing, plz set -w/--point or -s/--syscall or --brk")
}
if mconfig.BinderConf != nil && mconfig.BinderConf.Enable {
modNames = append(modNames, module.MODULE_NAME_BINDER)
}
if len(modNames) == 0 {
Logger.Fatal("hook nothing, plz set -w/--point or -s/--syscall or --brk or --binder")
}
for _, modName := range modNames {
// 现在合并成只有一个模块了 所以直接通过名字获取
Expand Down Expand Up @@ -675,4 +708,9 @@ func init() {
rootCmd.PersistentFlags().StringVar(&gconfig.NoSysCall, "no-syscall", "", "syscall black list, max 20")
// config hook
rootCmd.PersistentFlags().StringArrayVarP(&gconfig.ConfigFiles, "config", "c", []string{}, "hook config file")
// binder trace
rootCmd.PersistentFlags().BoolVar(&gconfig.BinderTrace, "binder", false, "enable binder transact tracing")
rootCmd.PersistentFlags().StringVar(&gconfig.BinderLib, "binder-lib", "libbinder.so", "path to libbinder.so")
rootCmd.PersistentFlags().StringVar(&gconfig.BinderFilter, "binder-filter", "", "filter by interface descriptor")
rootCmd.PersistentFlags().BoolVar(&gconfig.BinderNoReply, "binder-no-reply", false, "skip reply events")
}
Loading