gcc -finstrument-functions输出程序运行函数记录


当你使用 -finstrument-functions 编译选项时,GCC 会在每个函数的入口和出口处插入特殊的钩子函数,通常是 __cyg_profile_func_enter 和 __cyg_profile_func_exit。这些钩子函数可以用来记录函数的调用和返回,这对于性能分析、代码覆盖测试等场景非常有用。

1. 使用程序自带符号表

#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>

int main() __attribute__((no_instrument_function));

void __cyg_profile_func_enter(void *func, void *caller) __attribute__((no_instrument_function));
void __cyg_profile_func_exit(void *func, void *caller) __attribute__((no_instrument_function));

void __cyg_profile_func_enter(void *func, void *caller) {
    Dl_info func_info, caller_info;
    dladdr(func, &func_info);
    dladdr(caller, &caller_info);
    printf("Enter: %s (called from %s)\n",
           func_info.dli_sname ? func_info.dli_sname : "unknown",
           caller_info.dli_sname ? caller_info.dli_sname : "unknown");
}

// 钩子函数,记录函数退出
void __cyg_profile_func_exit(void *func, void *caller) {
    Dl_info func_info, caller_info;
    dladdr(func, &func_info);
    dladdr(caller, &caller_info);
    printf("Exit: %s (called from %s)\n",
           func_info.dli_sname ? func_info.dli_sname : "unknown",
           caller_info.dli_sname ? caller_info.dli_sname : "unknown");
}

void foo() {
    printf("In foo\n");
}

void bar() {
    printf("In bar\n");
    foo();
}

int main() {
    printf("In main\n");
    bar();
    return 0;
}

__attribute__((no_instrument_function))是一个 GCC 属性,它可以用来标记一个函数,使其不被函数入口和出口的钩子函数所拦截。这对于避免无限递归和性能影响非常有用,特别是在使用像 __cyg_profile_func_enter__cyg_profile_func_exit这样的函数时。
编译需要添加-rdynamic-export-dynamic,否则无法输出函数信息。

-rdynamic
    Pass the flag -export-dynamic to the ELF linker, on targets that support it. This instructs the linker to add all symbols,
    not only used ones, to the dynamic symbol table. This option is needed for some uses of "dlopen" or to allow obtaining
    backtraces from within a program.
gcc -finstrument-functions -g -o my_program main.c -ldl -rdynamic
or
gcc -finstrument-functions -O0 -g -o my_program main.c -ldl -Wl,-export-dynamic
./my_program

2. 自定义符号表

如果通过dladdr无法获取需要的函数符号信息,可以通过自定义符号表实现,自定义符号表可以通过libclang辅助实现。

#include <stdio.h>
#include <stdlib.h>

void foo();
void bar();
int main()  __attribute__((no_instrument_function));
const char *lookup_func_name(void *func) __attribute__((no_instrument_function));

typedef void (*func_ptr)();

static const struct {
    void *func;
    const char *name;
} func_map[] = {
    {(void *)&foo, "foo"},
    {(void *)&bar, "bar"},
    {(void *)&main, "main"}
};

// 查找函数名称
const char *lookup_func_name(void *func) {
    for (size_t i = 0; i < sizeof(func_map) / sizeof(func_map[0]); i++) {
        if (func_map[i].func == func) {
            return func_map[i].name;
        }
    }
    return "unknown";
}

// 钩子函数,记录函数进入
void __cyg_profile_func_enter(void *func, void *caller) __attribute__((no_instrument_function));
void __cyg_profile_func_enter(void *func, void *caller)
{
    printf("Enter: %s (called from %p)\n", lookup_func_name(func), caller);
}

// 钩子函数,记录函数退出
void __cyg_profile_func_exit(void *func, void *caller) __attribute__((no_instrument_function));
void __cyg_profile_func_exit(void *func, void *caller)
{
    printf("Exit: %s (called from %p)\n", lookup_func_name(func), caller);
}

void foo() {
    printf("In foo\n");
}

void bar() {
    printf("In bar\n");
    foo();
}

int main() {
    printf("In main\n");
    bar();
    return 0;
}

gcc -std=c99 -g -finstrument-functions -o my_program main.c
./my_program

[1]. 使用GCC函数插桩功能(-finstrument-functions)找到耗时异常的函数)

3. 其他获取函数路径的工具

3.1. eBPF

使用 eBPF(Extended Berkeley Packet Filter)可以高效地追踪程序函数调用过程。eBPF 是一种强大的内核技术,可以捕获和分析系统事件,而不会对系统性能产生显著影响。这里我们将使用 bcc 工具集来追踪程序中的函数调用。

3.1.1. 使用Python编写和加载eBPF程序

安装 BCC(BPF Compiler Collection) 工具集
在 CentOS 上安装 bcc 工具集:

sudo yum install epel-release
sudo yum install bcc-tools bcc-devel bpfcc bpfcc-tools

在 Ubuntu 上安装 bcc 工具集:

sudo apt-get update
sudo apt-get install bpfcc-tools linux-headers-$(uname -r)
sudo apt-get install bpfcc-tools python3-bpfcc

创建 eBPF 程序
我们将创建一个简单的 eBPF 程序,跟踪特定程序的函数调用。这里假设我们有一个名为 my_program 的可执行文件。

编写 eBPF 程序:
创建一个名为 trace_functions.py 的文件,并添加以下内容:

from bcc import BPF

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

BPF_HASH(start, u64);

int trace_entry(struct pt_regs *ctx) {
    u64 pid = bpf_get_current_pid_tgid();
    u64 ts = bpf_ktime_get_ns();
    start.update(&pid, &ts);
    bpf_trace_printk("Entering function\\n");
    return 0;
}

int trace_return(struct pt_regs *ctx) {
    u64 pid = bpf_get_current_pid_tgid();
    u64 *tsp, delta;
    tsp = start.lookup(&pid);
    if (tsp != 0) {
        delta = bpf_ktime_get_ns() - *tsp;
        start.delete(&pid);
        bpf_trace_printk("Function exited, duration: %llu ns\\n", delta);
    }
    return 0;
}
"""

b = BPF(text=program)
b.attach_uprobe(name="./my_program", sym="main", fn_name="trace_entry")
b.attach_uretprobe(name="./my_program", sym="main", fn_name="trace_return")

print("Tracing... Press Ctrl+C to end.")
while 1:
    try:
        (_, _, _, _, _, msg) = b.trace_fields()
        print(msg)
    except KeyboardInterrupt:
        exit()

运行 eBPF 程序:
确保 my_program 可执行文件存在,然后运行 eBPF 程序:

sudo python3 trace_functions.py

解释

  1. eBPF 程序:eBPF 程序定义了两个探针函数 trace_entrytrace_return,用于跟踪函数的进入和退出事件。
  2. BCC 工具:使用 bcc 库的 BPF 类加载 eBPF 程序,并附加到 my_programmain 函数上,跟踪其进入和退出事件。
  3. 输出:eBPF 程序将在进入和退出函数时打印消息,并计算函数执行的持续时间。

注意

  • 如果需要跟踪更多的函数,可以添加更多的 attach_uprobeattach_uretprobe 调用,指定不同的函数名称。
  • eBPF 程序在内核空间运行,效率很高,但调试和开发可能比较复杂,确保程序正确运行。
  • 对于复杂的应用,可以将 BCC Python 脚本进一步增强,例如添加过滤条件、统计数据等。

3.1.2. 使用C编写和加载eBPF程序

如果你更喜欢使用C语言编写和加载eBPF程序,可以使用libbpf库。下面是一个使用C编写和加载eBPF程序的示例。

安装libbpf
首先,需要安装libbpf。可以从源码编译安装,或者通过包管理器安装(如果你的系统提供libbpf包)。

从源码安装libbpf:

git clone https://github.com/libbpf/libbpf.git
cd libbpf/src
make
sudo make install

编写eBPF程序
创建一个名为trace_functions.bpf.c的文件,编写eBPF程序:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, u64);
    __type(value, u64);
    __uint(max_entries, 1024);
} start SEC(".maps");

SEC("uprobe/trace_entry")
int trace_entry(struct pt_regs *ctx) {
    u64 pid = bpf_get_current_pid_tgid();
    u64 ts = bpf_ktime_get_ns();
    bpf_map_update_elem(&start, &pid, &ts, BPF_ANY);
    bpf_printk("Entering function\\n");
    return 0;
}

SEC("uretprobe/trace_return")
int trace_return(struct pt_regs *ctx) {
    u64 pid = bpf_get_current_pid_tgid();
    u64 *tsp, delta;
    tsp = bpf_map_lookup_elem(&start, &pid);
    if (tsp) {
        delta = bpf_ktime_get_ns() - *tsp;
        bpf_map_delete_elem(&start, &pid);
        bpf_printk("Function exited, duration: %llu ns\\n", delta);
    }
    return 0;
}

char _license[] SEC("license") = "GPL";

编写用户空间程序
创建一个名为trace_functions.c的文件,编写用户空间程序:

#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>

#define BPF_FILE "trace_functions.bpf.o"

int main() {
    struct bpf_object *obj;
    int prog_fd;

    // 设置ulimit,以允许BPF程序加载更多的内核内存
    struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
    setrlimit(RLIMIT_MEMLOCK, &r);

    // 加载和验证BPF程序
    obj = bpf_object__open_file(BPF_FILE, NULL);
    if (libbpf_get_error(obj)) {
        fprintf(stderr, "Error opening BPF object file.\n");
        return 1;
    }

    if (bpf_object__load(obj)) {
        fprintf(stderr, "Error loading BPF object.\n");
        bpf_object__close(obj);
        return 1;
    }

    // 获取BPF程序的文件描述符
    prog_fd = bpf_program__fd(bpf_object__find_program_by_title(obj, "uprobe/trace_entry"));
    if (prog_fd < 0) {
        fprintf(stderr, "Error finding BPF program.\n");
        bpf_object__close(obj);
        return 1;
    }

    // 将BPF程序附加到目标进程的函数上
    int pid = getpid(); // 这里应该是目标进程的PID
    if (bpf_attach_uprobe(prog_fd, pid, "/path/to/your/program", "function_name", BPF_PROBE_ENTRY)) {
        fprintf(stderr, "Error attaching uprobe.\n");
        bpf_object__close(obj);
        return 1;
    }

    // 同样的步骤,附加uretprobe
    prog_fd = bpf_program__fd(bpf_object__find_program_by_title(obj, "uretprobe/trace_return"));
    if (prog_fd < 0) {
        fprintf(stderr, "Error finding BPF program.\n");
        bpf_object__close(obj);
        return 1;
    }

    if (bpf_attach_uretprobe(prog_fd, pid, "/path/to/your/program", "function_name", BPF_PROBE_RETURN)) {
        fprintf(stderr, "Error attaching uretprobe.\n");
        bpf_object__close(obj);
        return 1;
    }

    // 等待和处理输出
    printf("Tracing... Press Ctrl+C to end.\n");
    while (1) {
        // 处理输出,这里可以读取trace_pipe或其他输出方式
        sleep(1);
    }

    bpf_object__close(obj);
    return 0;
}

编译和运行
编译eBPF程序:

clang -O2 -target bpf -c trace_functions.bpf.c -o trace_functions.bpf.o

编译用户空间程序:

gcc -o trace_functions trace_functions.c -l:libbpf.a -lelf -lz

运行用户空间程序:

sudo ./trace_functions

总结
使用eBPF来追踪函数调用,可以选择使用Python的BCC库,也可以使用C语言通过libbpf库来实现。两种方法都可以高效地追踪函数调用过程,并根据需求进行扩展和定制。

3.2. SystemTap

使用 SystemTap 可以有效地跟踪程序的函数调用过程。以下是一个基本示例,展示如何使用 SystemTap 脚本来跟踪特定程序的函数调用过程。

安装 SystemTap
首先,确保已安装 SystemTap 及其依赖项:

sudo apt-get install systemtap systemtap-sdt-dev
sudo apt-get install linux-headers-$(uname -r)

编写 SystemTap 脚本
以下示例脚本将跟踪程序中所有函数的进入和退出,并记录函数名称和时间戳。

创建一个名为 trace_functions.stp 的文件,内容如下:

probe process("/path/to/your/program").function("*@/path/to/your/source/*").call {
    printf("Entering function %s at %s\n", probefunc(), ctime(gettimeofday_s()))
}

probe process("/path/to/your/program").function("*@/path/to/your/source/*").return {
    printf("Exiting function %s at %s\n", probefunc(), ctime(gettimeofday_s()))
}

替换 “/path/to/your/program” 为你要跟踪的程序路径,“/path/to/your/source/*” 为源文件路径。

运行 SystemTap 脚本
使用以下命令运行脚本,并跟踪函数调用过程:

sudo stap trace_functions.stp

运行此命令后,SystemTap 将开始跟踪指定程序中的所有函数调用,并在控制台上输出进入和退出每个函数的时间戳。

示例
假设我们有一个简单的 C 程序 example.c

#include <stdio.h>

void foo() {
    printf("In foo\n");
}

void bar() {
    printf("In bar\n");
    foo();
}

int main() {
    printf("In main\n");
    bar();
    return 0;
}

编译这个程序:

gcc -g -o example example.c

编写一个 SystemTap 脚本 trace_example.stp:

probe process("./example").function("*").call {
    printf("Entering function %s at %s\n", probefunc(), ctime(gettimeofday_s()))
}

probe process("./example").function("*").return {
    printf("Exiting function %s at %s\n", probefunc(), ctime(gettimeofday_s()))
}

运行 SystemTap 脚本:

sudo stap trace_example.stp

运行后,你将看到如下输出:

Entering function main at Tue Aug  3 17:45:12 2023
Entering function bar at Tue Aug  3 17:45:12 2023
Entering function foo at Tue Aug  3 17:45:12 2023
Exiting function foo at Tue Aug  3 17:45:12 2023
Exiting function bar at Tue Aug  3 17:45:12 2023
Exiting function main at Tue Aug  3 17:45:12 2023

通过这种方式,可以使用 SystemTap 跟踪程序的函数调用过程,帮助调试和分析程序的运行行为。

3.3. perf

http://arthurchiao.art/blog/linux-tracing-basis-zh/

3.4. valgrind

  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值