上一次,我们通过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
三、总结
今天我们尝试了把用户空间自定义函数探测进行了完善,主要包括以下几点:
- 针对清除符号表的产出无法找到符号的问题,改为提前导出符号表,使用offset来进行探测;
- 为贴合实际场景,对namespace、class的func进行探测;
- 针对不同机器二进制产出位置不同的问题,改为根据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/