eBPF—使用符号表offset探测函数调用

上一次,我们通过uprobe实现一个对用户空间自定义程序的特定函数出入口进行代码注入和探测的方法,但它还有很多不完善的地方,比如需要提前准备好编译后的函数符号,同时生产环境产出通常也不带符号、产出的部署位置通常也不固定,给我们在实际场景中使用带来不便。本文就讲解下如何通过导出符号表、产出去符号,通过符号表的offset、类函数名、进程PID来进行用户自定义函数调用的探测,从而更方便直接在生产环境使用。

一、概述

1.符号

符号(Symbol)常用来表示一个地址,这个地址可能是一端程序的起始地址,也可能是一个变量的起始地址,简而言之,将它当做是标记或名称即可。
编译链接的过程,实质上就是将不同的目标文件汇集在一起。链接构成中,目标文件的相互拼合汇集,实际上是目标文件之间对地址的引用。链接过程中,我们将函数和变量统一称作为符号 ,函数名与变量名称就是符号名 ,其记录的地址信息就是其符号值 。

2.符号表

链接过程中,我们需要对各种符号进行管理,每一个目标文件都会有一个对应的符号表 Symbol Table ,用来记录目标文件中所有用到的符号,在静态链接时候,会进行相同性质段的合并。

符号表分类:

  • Symbol Table: 所有符号表
    • 存储所有的符号信息,包含动态链接符号表内的符号
  • Dynamic Symbol Table:动态链接符号表,间接符号表(Indirect Symbol)
    • 存储着所有使用到的位于其他外部动态库中的符号信息,动态链接时进行绑定
  • String Table:符号的名称

读取符号表的方法:

$ readelf -s build/utest

Symbol table '.dynsym' contains 27 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _ZSt4endlIcSt11char_trait@GLIBCXX_3.4 (3)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sleep@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __cxa_atexit@GLIBC_2.2.5 (2)
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _ZStlsISt11char_traitsIcE@GLIBCXX_3.4 (3)
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _ZNSolsEPFRSoS_E@GLIBCXX_3.4 (3)
     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _ZNSt8ios_base4InitC1Ev@GLIBCXX_3.4 (3)
     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _ZNSolsEi@GLIBCXX_3.4 (3)
     8: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     9: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
    10: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    11: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
    12: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _ZNSt8ios_base4InitD1Ev@GLIBCXX_3.4 (3)
    13: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   25 _edata
    14: 00000000000011e9    24 FUNC    GLOBAL DEFAULT   16 _Z9utest_addii
    15: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)
    16: 0000000000001201    22 FUNC    GLOBAL DEFAULT   16 _Z9utest_subii
    17: 0000000000004000     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
    18: 0000000000004158     0 NOTYPE  GLOBAL DEFAULT   26 _end
    19: 0000000000004000     0 NOTYPE  WEAK   DEFAULT   25 data_start
    20: 0000000000002000     4 OBJECT  GLOBAL DEFAULT   18 _IO_stdin_used
    21: 0000000000001217   134 FUNC    GLOBAL DEFAULT   16 main
    22: 0000000000001100    47 FUNC    GLOBAL DEFAULT   16 _start
    23: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    24: 0000000000001310   101 FUNC    GLOBAL DEFAULT   16 __libc_csu_init
    25: 0000000000004040   272 OBJECT  GLOBAL DEFAULT   26 _ZSt4cout@GLIBCXX_3.4 (3)
    26: 0000000000001380     5 FUNC    GLOBAL DEFAULT   16 __libc_csu_fini

Symbol table '.symtab' contains 84 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000318     0 SECTION LOCAL  DEFAULT    1 
     ...
    59: 00000000000011e9    24 FUNC    GLOBAL DEFAULT   16 _Z9utest_addii
    63: 0000000000001217   134 FUNC    GLOBAL DEFAULT   16 main
    66: 0000000000001201    22 FUNC    GLOBAL DEFAULT   16 _Z9utest_subii
    ...

 

二、符号表导出

1.存储符号表的map

vim symbol.proto

syntax = "proto3";

message Symbol {
    map<string, uint64> symbols = 1;
};

2.导出符号表程序

导出自定义程序的符号表,map的key使用函数名(带参数),实现方法较多,这里不再赘述。

3.编译&导出

增加proto编译配置:

cmake_minimum_required(VERSION 3.16)

# proto
find_package(Protobuf REQUIRED)
FILE(GLOB protofiles "${CMAKE_CURRENT_SOURCE_DIR}/*.proto")
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${protofiles})
protobuf_generate_python(PY_SOURCES ${protofiles})

add_library(symbol_proto STATIC ${PROTO_SRCS} ${PROTO_HDRS})
target_link_libraries(symbol_proto ${PROTOBUF_LIBRARIES})

修改ebpf程序的cmake编译配置:

cmake_minimum_required(VERSION 3.16)
project(ebpf_test)

# 1.symbol proto
add_subdirectory(proto)


# 2.user space test app
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -ggdb -rdynamic")   # debug
# .h
include_directories(
    ./
    /usr/include
    /usr/local/include
    ${PROTOBUF_INCLUDE_DIRS}
    ${CMAKE_CURRENT_BINARY_DIR}
)
# utest exec
add_executable(utest utest/utest.cc)
# usymbol exec
add_executable(usymbol utest/usymbol.cc)
target_link_libraries(usymbol protobuf elf symbol_proto)


# 3. ebpf
...

编译并导出符号:

# build
mkdir build && cd build
cmake ..
make

# check symbol
objdump -T utest |grep utest_add
    00000000000011e9 g DF .text 0000000000000018 Base _Z9utest_addii
objdump -T utest |grep utest_sub
    0000000000001201 g DF .text 0000000000000016 Base _Z9utest_subii
# dump
./usymbol utest > symbols.txt

cat symbols.txt 
  1130 : deregister_tm_clones
  1201 : utest_sub(int, int)
  1380 : __libc_csu_fini
  1388 : _fini
  1217 : main
  1310 : __libc_csu_init
  11a0 : __do_global_dtors_aux
  1000 : _init
  11e0 : frame_dummy
  1160 : register_tm_clones
  1100 : _start
  11e9 : utest_add(int, int)
  129d : __static_initialization_and_destruction_0(int, int)

# dump proto map serialize file
cat symbols.dump

这里可以看到导出的符号表文件里,清理了之前_Z9utest_addii的前缀和后缀,其中_Z表示是用户函数或全局变量符号,9表示函数/变量名的长度,末尾的ii表示有2个参数,都是int类型。前边保留的1201、11e9即为符号的offset偏移量。

三、使用offset和函数名直接探测

1.使用offset探测

我们在上次用户自定义程序探测的基础上进行修改,增加符号表加载、根据函数名获取offset:

vim uprobe_symbol.cc

...
#include "proto/symbol.pb.h"

int main(int argc, char **argv)
{
    ...

    // 加载symbols.dump文件
    std::ifstream is("./symbols.dump", std::ios::binary);
    auto symbol = new Symbol();
    symbol->ParseFromIstream(&is);
    is.close();
    auto symbolMap = symbol->symbols();
    
    // 获取函数对应符号表的offset
    std::string func_name = "utest_add(int, int)";
    uint64_t func_offset = 0;
    func_offset = symbolMap[func_name];
    std::cout << "func_offset:" << func_offset << std::endl;
    if (!func_offset) {
        std::cout << "failed to find symbol!" << std::endl;
        return 0;
    }

    /* 附加跟踪点处理 */
    uprobe_opts.retprobe = false;
    skel->links.utest_add = bpf_program__attach_uprobe_opts(
        skel->progs.utest_add,
        -1, // uprobe 的进程 ID,0 表示自身(自己的进程),-1 表示所有进程
        "/home/work/project/libbpf-demo/examples/test/build/utest",
        func_offset, // offset for function
        &uprobe_opts);
    ...

}

编译运行测试:

# 编译
mkdir build && cd build
cmake ..
make

# 运行用户自定义程序
pkill utest
nohup ./utest &

# 清除符号表
strip --strip-all utest
# 启动
sudo ./uprobe_symbol 

# 监听内核log
sudo cat /sys/kernel/debug/tracing/trace_pipe
    utest-289573  [001] d...1 4174234.966051: bpf_trace_printk: utest_add ENTRY: a = 0, b = 1
    utest-289573  [001] d...1 4174234.966051: bpf_trace_printk: utest_add EXIT: return = 1
    utest-289573  [001] d...1 4174234.966051: bpf_trace_printk: utest_sub ENTRY: a = 0, b = 0
    utest-289573  [001] d...1 4174234.966051: bpf_trace_printk: utest_sub EXIT: return = 0

2.类函数的探测

前边我们探测的都是简单的示例函数,为了贴近实际场景,我们这里改为带namespace、class的函数探测。

自定义程序:

/**
 * 用户空间自定义程序
*/
#include <iostream>
#include <stdio.h>
#include <unistd.h>

namespace test {

class UTest {
public:
    UTest(){
        std::cout << "UTest constructor" << std::endl;
    }

    int utest_add(int a, int b)
    {
        return a + b;
    }

    int utest_sub(int a, int b)
    {
        return a - b;
    }
};

}  // namespace test

main入口:

/**
 * 用户空间自定义程序
*/
#include <memory>
#include "utest_class.h"

using namespace test;

int main(int argc, char **argv)
{
    int err, i;
    auto obj = std::make_shared<UTest>();

    for (i = 0;; i++) {
        obj->utest_add(i, i + 1);
        obj->utest_sub(i * i, i);

        std::cout << "i = " << i << std::endl;

        sleep(1);
    }
}

修改ebpf探测程序,使用改为class函数,同时使用pid来自动获取bin文件位置:

int main(int argc, char **argv)
{
    ...
    if(argc < 2){
        std::cout << "usage: ./uprobe_symbol <pid>" << std::endl;
        return 0;
    }
    std::string symbol_dump_file = "./symbols-utest_class.dump";
    std::string func_name = "test::UTest::utest_add(int, int)";
    uint64_t func_offset = 0;
    char binary_path[PATH_MAX];
    pid_t pid = atoi(argv[1]);

    // 通过pid获取可执行文件路径
    if (!pid){
        std::cout << "param pid invalid!" << std::endl;
        return 0;
    }
    if(!get_binary_file_by_pid(pid, binary_path, sizeof(binary_path)) == 0){
        std::cout << "failed to get binary file by pid!" << std::endl;
        return 0;
    }
    std::cout << "binary_path: " << binary_path << std::endl;
    // 加载symbols.dump文件
    std::ifstream is(symbol_dump_file, std::ios::binary);
    auto symbol = new Symbol();
    symbol->ParseFromIstream(&is);
    is.close();
    auto symbolMap = symbol->symbols();

    // 获取函数对应符号表的offset
    func_offset = symbolMap[func_name];
    std::cout << "func_offset: [" << func_offset << "] " << func_name << std::endl;
    if (!func_offset) {
        std::cout << "failed to find symbol!" << std::endl;
        return 0;
    }

    ...

    /* 附加跟踪点处理 */
    uprobe_opts.retprobe = false;
    skel->links.utest_add = bpf_program__attach_uprobe_opts(
        skel->progs.utest_add,
        -1,    // uprobe 的进程 ID,0 表示自身(自己的进程),-1 表示所有进程
        binary_path,
        func_offset,    // offset for function
        &uprobe_opts
    );
    ...

编译运行测试:

# 编译
mkdir build && cd build
cmake ..
make

# 运行用户自定义程序
pkill utest_class
nohup ./utest_class &

# 启动
pid=$(ps -ef | grep utest_class | grep -v grep | awk '{print $2}')
sudo ./uprobe_symbol ${pid}

# 监听内核log
sudo cat /sys/kernel/debug/tracing/trace_pipe
    utest-289573  [001] d...1 4175146.217611: bpf_trace_printk: utest_add ENTRY: a = 0, b = 1
    utest-289573  [001] d...1 4175146.217611: bpf_trace_printk: utest_add EXIT: return = 1
    utest-289573  [001] d...1 4175146.217611: bpf_trace_printk: utest_sub ENTRY: a = 0, b = 0
    utest-289573  [001] d...1 4175146.217611: bpf_trace_printk: utest_sub EXIT: return = 0s

三、总结

今天我们尝试了把用户空间自定义函数探测进行了完善,主要包括以下几点:

  1. 针对清除符号表的产出无法找到符号的问题,改为提前导出符号表,使用offset来进行探测;
  2. 为贴合实际场景,对namespace、class的func进行探测;
  3. 针对不同机器二进制产出位置不同的问题,改为根据pid自动获取二进制文件位置;

好了,今天就先到这里,如果你对哪方面内容感兴趣,希望重点讲解,欢迎给我留言交流~

 

yan 23.12.19

 

参考:

https://hansimov.gitbook.io/csapp/part2/ch07-linking/7.5-symbols-and-symbol-tables

https://workerwork.github.io/posts/sub-bin/

 

欢迎关注下方“非著名资深码农“公众号进行交流~

发表评论

邮箱地址不会被公开。