当前位置: 首页 > 工具软件 > BCC > 使用案例 >

使用bcc来调试你的代码

胡曾笑
2023-12-01

bcc之前有一篇介绍:bcc/ebpf使用介绍

使用bcc,除了官方已经提供的一些工具(主要是对系统的调试,例如io、内存、网络、cpu),我们还可以用它来调试我们的应用代码。

我们可以把调试手段分为两类:1. 静态调试 2. 动态调试。
什么意思呢?我们以游戏为例,我们在游戏中进入战斗之前,通常会先对自己的装备、药品等做一些准备,进入战斗之后,这些装备、药品就可以直接使用了,这就是静态调试,也就是预先准备好的调试手段。但是,在战斗的过程中,我们可能发现我们需要其他装备,或者获得了更好的装备需要更换上去,那么我们总不能跟对手说我们先暂停战斗,我先换一下装备,最好的还是在战斗过程中就可以灵活的切换装备。战斗过程中可以切换的,就是动态调试。
动态调试的好处是什么呢?主要就是比较灵活,可以动态的更换,不需要重新编译、重新启动。

bcc就是一种动态调试手段,配合linux内核提供的uprobe,就可以在指定的函数中配置上你需要的“装备”。

我这边目前没有项目上的例子,所以就简单写个测试例程来进行说明吧。

一个简单的程序:

#include <stdlib.h>
#include <iostream>

using namespace std;

long long sum = 0;

void _plus(int num) {
	sum += num;
}

void _minus(int num) {
	sum -= num;
}

int main() {
	srand(10);
	while (1) {
		_plus(random()%100);
		_minus(random()%100);
	}
	cout << sum << endl;
	return 0;
}

例子很简单,就是一个死循环,每次循环,对一个全局变量sum,先调用_plus增加一个随机数,再调用_minus减小一个随机数。

流量统计

对这个例子,我们能做什么样的调试呢?我们可以在_plus和_minus这两个函数上增加uprobe,而我们的参数是num,我们可以统计num的总和,得到一段时间内通过_plus对sum增加的数字总和。这样的调试有什么作用了,我想到的常见用处,就是流量统计,我们可以把_plus想象成一个发送数据的接口,那么我们统计这个发送数据的接口接收到的数据量,就能够得到这个函数处理的流量了。

为了实现这个目的,我们需要写一个python脚本:

from bcc import BPF
from time import sleep
from datetime import datetime
import resource
import argparse
import subprocess
import os
import sys

parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("-p", "--pid", type=int, default=-1)
parser.add_argument("-O", "--obj", type=str, default="c")

args = parser.parse_args()
pid = args.pid
obj = args.obj

bpf_source = """
#include <uapi/linux/ptrace.h>

BPF_HASH(sums, u64);

int _Z5_plusi_enter(struct pt_regs *ctx, int num) {
    u64 key = 1;
    u64 *p_sum = sums.lookup(&key);
    u64 sum = 0;
    if (!p_sum)
        sum = num;
    else
        sum = *p_sum+num;

    sums.update(&key, &sum1);

    return 0;
}
"""

bpf = BPF(text=bpf_source)


def attach_probes(sym, fn_prefix=None, can_fail=False):
    if fn_prefix is None:
        fn_prefix = sym

    try:
        bpf.attach_uprobe(name=obj, sym=sym, fn_name=fn_prefix + "_enter", pid=pid)
    except Exception:
        if can_fail:
            return
        else:
            raise


attach_probes("_Z5_plusi")

while True:
    sums = bpf["sums"]
    print(sums)
    print(sums.items())
    sleep(5)

脚本内容我就不介绍了,有疑问的可以百度python的语法,以及bcc的python库用法。

然后可以这样使用:

root:/$ sudo python3 ../bcc_test.py -p $(pgrep bcc_test) -O $PWD/bcc_test
<bcc.table.HashTable object at 0x7f2e43cab160>
[(c_ulong(1), c_ulong(85610))]
<bcc.table.HashTable object at 0x7f2e43cab160>
[(c_ulong(1), c_ulong(72722654))]

第一次打印出来的值是85610,第二次是72722654,大约是5秒,也就是5秒后sum增加了大概七千万。

PS. 我们的函数是_plus,但在python脚本中跟踪的符号是_Z5_plusi,只是因为c++中有重载,所以同一个函数名可能有不同的实现,所以其符号是不同的。可以通过nm来获取符号:

root:/$ nm ./bcc_test | grep _plus
00000000000011a9 T _Z5_plusi

有多个的,我们可以通过c++filt来判断是不是我们需要的:

root:/$ nm ./bcc_test | grep _plus | c++filt
00000000000011a9 T _plus(int)

带栈信息的流量统计

我们把代码改的复杂一点:

#include <stdlib.h>
#include <iostream>

using namespace std;

long long sum = 0;

void _plus(int num) {
	sum += num;
}

void _plus(int num1, int num2) {
	for (int i = 0; i < num2; ++i) {
		_plus(num1);
	}
}

int main() {
	srand(10);
	while (1) {
		int i = abs(random()%100);
		_plus(i);
		_plus(i, i);
	}
	cout << sum << endl;
	return 0;
}

sum的增加有两种调用路径:

  • main --> void _plus(int num)
  • main --> void _plus(int num1, int num2) --> void _plus(int num)

显然第二种的调用路径,数据量会比第一种大的多。这是代码比较简单的情况,而在实际使用中,我们去发送数据也会有很多调用路径,但是并不清楚他们的比例,那么我们就可以通过bcc来统计一下。

python脚本:

from bcc import BPF
from time import sleep
from datetime import datetime
import resource
import argparse
import subprocess
import os
import sys

parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("-p", "--pid", type=int, default=-1)
parser.add_argument("-O", "--obj", type=str, default="c")

args = parser.parse_args()
pid = args.pid
obj = args.obj

bpf_source = """
#include <uapi/linux/ptrace.h>

BPF_STACK_TRACE(stack_traces, 10240);
BPF_HASH(pluses, int, u64);

int _Z5_plusi_enter(struct pt_regs *ctx, int num) {
    u64 address = PT_REGS_RC(ctx);
    if (address != 0) {
        int stack_id = stack_traces.get_stackid(ctx, BPF_F_REUSE_STACKID | BPF_F_USER_STACK);
        u64 *p_sum = pluses.lookup(&stack_id);
        u64 sum = 0;
        if (p_sum)
            sum = *p_sum + num;
        else
            sum = num;
        pluses.update(&stack_id, &sum);
    }

    return 0;
}
"""

bpf = BPF(text=bpf_source)


def attach_probes(sym, fn_prefix=None, can_fail=False):
    if fn_prefix is None:
        fn_prefix = sym

    try:
        bpf.attach_uprobe(name=obj, sym=sym, fn_name=fn_prefix + "_enter", pid=pid)
    except Exception:
        if can_fail:
            return
        else:
            raise


attach_probes("_Z5_plusi")

while True:
    print("<loop>")
    pluses = bpf["pluses"]
    stack_traces = bpf["stack_traces"]
    for stack_id in stack_traces:
        print(">>>stack", stack_id.value, "total", pluses[stack_id].value)
        print(list(stack_traces.walk(stack_id.value)))
        for addr in stack_traces.walk(stack_id.value):
            print(" ", bpf.sym(addr, pid, show_module=True, show_offset=True))
    sleep(5)

输出结果:

<loop>
>>>stack 6317 total 9306
[94224919544233, 94224919544456, 140527832748163]
  b'_plus(int)+0x0 [bcc_test]'
  b'main+0x86 [bcc_test]'
  b'__libc_start_main+0xf3 [libc-2.31.so]'
>>>stack 13596 total 1042
[94224919544233, 140527832748163]
  b'_plus(int)+0x0 [bcc_test]'
  b'__libc_start_main+0xf3 [libc-2.31.so]'

<loop>
>>>stack 6317 total 76110497
[94224919544233, 94224919544456, 140527832748163]
  b'_plus(int)+0x0 [bcc_test]'
  b'main+0x86 [bcc_test]'
  b'__libc_start_main+0xf3 [libc-2.31.so]'
>>>stack 13596 total 1150457
[94224919544233, 140527832748163]
  b'_plus(int)+0x0 [bcc_test]'
  b'__libc_start_main+0xf3 [libc-2.31.so]'

可以看到有两种栈:

  • __libc_start_main -> main -> _plus(int)
  • __libc_start_main -> main -> _plus(int,int) -> _plus(int)
    第二种的数量是76110497,第一种是1150457,符合预期。

打印中的栈缺少了倒数第二个栈帧,原因不大清楚,可以参考:https://github.com/iovisor/bcc/issues/3261

异常的数字

我们在项目中,可能经常会遇到参数异常,但又不清楚异常的原因的情况,通常的解决办法可以是加打印、挂gdb设置断点,通过bcc也可以进行这个检查。

例如,我们认为_plus中,num>=99就是异常的,我们可以修改脚本,使得每次num>=99时就输出打印:

from bcc import BPF
from time import sleep
from datetime import datetime
import resource
import argparse
import subprocess
import os
import sys

parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("-p", "--pid", type=int, default=-1)
parser.add_argument("-O", "--obj", type=str, default="c")

args = parser.parse_args()
pid = args.pid
obj = args.obj

bpf_source = """
#include <uapi/linux/ptrace.h>

BPF_STACK_TRACE(stack_traces, 10240);

int _Z5_plusi_enter(struct pt_regs *ctx, int num) {
    u64 address = PT_REGS_RC(ctx);
    if (address != 0) {
        int stack_id = stack_traces.get_stackid(ctx, BPF_F_REUSE_STACKID | BPF_F_USER_STACK);
        if (num >= 99)
            bpf_trace_printk("abnomal num: %d, stack_id: %d\\n", num, stack_id);
    }

    return 0;
}
"""

bpf = BPF(text=bpf_source)


def attach_probes(sym, fn_prefix=None, can_fail=False):
    if fn_prefix is None:
        fn_prefix = sym

    try:
        bpf.attach_uprobe(name=obj, sym=sym, fn_name=fn_prefix + "_enter", pid=pid)
    except Exception:
        if can_fail:
            return
        else:
            raise


attach_probes("_Z5_plusi")

while True:
    stack_traces = bpf["stack_traces"]
    for stack_id in stack_traces:
        print(">>>stack", stack_id.value)
        for addr in stack_traces.walk(stack_id.value):
            print(" ", bpf.sym(addr, pid, show_module=True, show_offset=True))
    sleep(5)

脚本会输出stack_id对应的栈,然后在/sys/kernel/debug/tracing/trace中查看输出:

root/$ sudo cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
# entries-in-buffer/entries-written: 30245/6863374   #P:2
#
#                                _-----=> irqs-off
#                               / _----=> need-resched
#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| /     delay
#           TASK-PID     CPU#  ||||   TIMESTAMP  FUNCTION
#              | |         |   ||||      |         |
        bcc_test-12823   [000] .... 5548271.843787: 0: abnomal num: 99, stack_id: 7898
        bcc_test-12823   [000] .... 5548271.843790: 0: abnomal num: 99, stack_id: 7898
        bcc_test-12823   [000] .... 5548271.843793: 0: abnomal num: 99, stack_id: 7898
        bcc_test-12823   [000] .... 5548271.843796: 0: abnomal num: 99, stack_id: 7898
        bcc_test-12823   [000] .... 5548271.843799: 0: abnomal num: 99, stack_id: 7898
        bcc_test-12823   [000] .... 5548271.843802: 0: abnomal num: 99, stack_id: 7898
        bcc_test-12823   [000] .... 5548271.843805: 0: abnomal num: 99, stack_id: 7898
        bcc_test-12823   [000] .... 5548271.843808: 0: abnomal num: 99, stack_id: 7898
        bcc_test-12823   [000] .... 5548271.843811: 0: abnomal num: 99, stack_id: 7898
        bcc_test-12823   [000] .... 5548271.843814: 0: abnomal num: 99, stack_id: 7898
 类似资料: