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

SystemTap应用跟踪探测的使用

夹谷阳夏
2023-12-01

用户静态定义探测点USDT

usdt(User-Statically-Defined-Tracepoint)是一种向应用插入跟踪点的技术方案,其特点是跟踪点的插入是静态的,通常需要修改应用的源码并再次编译。该技术方案源于DTrace,不过usdt应用跟踪的功能在Systemtapbcc等内核跟踪调试工具中已有良好的支持。Systemtap开源工具提供了sys/sdt.h头文件,该头文件没有相应用C代码实现,仅仅提供了多个STAP_PROBExxxDTRACE_PROBExxx宏定义。这些宏定义通过GCC编译器的内联汇编和段操作在编译成生的ELF文件中加入一些用于跟踪的信息(主要加入一个nop机器指令和.note.stapsdt段信息)。当应用未被跟踪调试时,这些额外的信息不会影响应用的正常功能,也不会造成性能的消耗;当应用被跟踪调试时,Systemtapbcc工具会通过Linux内核修改应用的代码段,将nop命令替换为陷入Linux内核或调试代码的指令;具体的实现请参考Systemtap相关文档

本文记录用笔者编写的简单usdt调试应用及在64位arm64/Linux设备上的调试过程。

跟踪应用层的内存申请与释放

在编写代码前,笔者需要在x86_64/Linux主机上两次编译Systemtap:分别为主机和嵌入式ARM设备编译;该操作这里不再详述。之后,笔者编写了以下代码mymalloc.c,使用sys/sdt.h中提供的宏,对应用的内存分配函数封装的同时,加入跟踪点:

#include <sys/sdt.h>

extern void * __libc_malloc(size_t);
extern void __libc_free(void * ptr);
extern void * __libc_calloc(size_t nmemb, size_t size);
extern void * __libc_realloc(void * ptr, size_t size);

void * my_malloc(size_t size)
{
	void * ret;
	ret = __libc_malloc(size);
	DTRACE_PROBE2(mymalloc, my_malloc, size, ret);
	return ret;
}

void my_free(void * ptr)
{
	DTRACE_PROBE1(mymalloc, my_free, ptr);
	__libc_free(ptr);
}

void * my_calloc(size_t nmemb, size_t size)
{
	void * ret;
	ret = __libc_calloc(nmemb, size);
	DTRACE_PROBE3(mymalloc, my_calloc, nmemb, size, ret);
	return ret;
}

void * my_realloc(void * ptr, size_t size)
{
	void * ret;
	ret = __libc_realloc(ptr, size);
	DTRACE_PROBE3(mymalloc, my_realloc, ptr, size, ret);
	return ret;
}

使用了以上内存分配函数的封装,由C/C++编写的应用的绝大多数内存分配操作都可以被跟踪到,只需在编译选项中加入-Dmalloc=my_malloc -Dcalloc=my_calloc -Drealloc=my_realloc -Dfree=my_free宏定义参数并重新编译即可(注意,这些编译参数可能有副作用)。其中,DTRACE_PROBEx宏会在mymalloc.o目标文件中引入nop指令和.note.stapsdt段信息,同时也支持一些调试参数的传递;这些信息是生成Systemtap内核调试模块和调试时需要的:

$ aarch64-linux-gnu-objdump -d mymalloc.o
mymalloc.o:     file format elf64-littleaarch64
Disassembly of section .text:
0000000000000000 <my_malloc>:
   0:	a9be7bfd 	stp	x29, x30, [sp, #-32]!
   4:	910003fd 	mov	x29, sp
   8:	f9000bf3 	str	x19, [sp, #16]
   c:	aa0003f3 	mov	x19, x0
  10:	94000000 	bl	0 <__libc_malloc>
  14:	d503201f 	nop
  18:	f9400bf3 	ldr	x19, [sp, #16]
  1c:	a8c27bfd 	ldp	x29, x30, [sp], #32
  20:	d65f03c0 	ret
  24:	d503201f 	nop
0000000000000028 <my_free>:
  28:	d503201f 	nop
  2c:	14000000 	b	0 <__libc_free>
0000000000000030 <my_calloc>:
  30:	a9be7bfd 	stp	x29, x30, [sp, #-32]!
  34:	910003fd 	mov	x29, sp
  38:	a90153f3 	stp	x19, x20, [sp, #16]
  3c:	aa0003f3 	mov	x19, x0
  40:	aa0103f4 	mov	x20, x1
  44:	94000000 	bl	0 <__libc_calloc>
  48:	d503201f 	nop
  4c:	a94153f3 	ldp	x19, x20, [sp, #16]
  50:	a8c27bfd 	ldp	x29, x30, [sp], #32
  54:	d65f03c0 	ret
0000000000000058 <my_realloc>:
  58:	a9be7bfd 	stp	x29, x30, [sp, #-32]!
  5c:	910003fd 	mov	x29, sp
  60:	a90153f3 	stp	x19, x20, [sp, #16]
  64:	aa0003f3 	mov	x19, x0
  68:	aa0103f4 	mov	x20, x1
  6c:	94000000 	bl	0 <__libc_realloc>
  70:	d503201f 	nop
  74:	a94153f3 	ldp	x19, x20, [sp, #16]
  78:	a8c27bfd 	ldp	x29, x30, [sp], #32
  7c:	d65f03c0 	ret
$ aarch64-linux-gnu-readelf -S mymalloc.o
There are 25 section headers, starting at offset 0x20e8:
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 6] .note.stapsdt     NOTE             0000000000000000  000000c8
       0000000000000134  0000000000000000           0     0     4
  [ 7] .rela.note.stapsd RELA             0000000000000000  000014f8
       00000000000000c0  0000000000000018   I      22     6     8
  [ 8] .stapsdt.base     PROGBITS         0000000000000000  000001fc
       0000000000000001  0000000000000000  AG       0     0     1

Systemtap应用调试脚本

笔者将mymalloc.o链接到简单的调试应用,test-malloc;并把test-malloc分别复制到主机和嵌入式设备的/tmp目录下(仅仅是为了保证应用在主机和嵌入式设备上的路程径一致)。编写Systemtap脚本,仅仅是将跟踪到的应用内存分配操作输出到调试终端:

$ cat mymalloc.stp 
probe process("/tmp/test-malloc").provider("mymalloc").mark("my_malloc")
{
	printf("malloc(%lx) has returned: %lx\n", $arg1, $arg2);
}
probe process("/tmp/test-malloc").provider("mymalloc").mark("my_calloc")
{
	printf("calloc(%lx, %lx) has returned: %lx\n", $arg1, $arg2, $arg3);
}
probe process("/tmp/test-malloc").provider("mymalloc").mark("my_realloc")
{
	printf("realloc(%lx, %lx) has returned: %lx\n", $arg1, $arg2, $arg3);
}
probe process("/tmp/test-malloc").provider("mymalloc").mark("my_free")
{
	printf("free(%lx) has been invoked\n", $arg1);
}

之后,在x86_64/Linux主机上使用stap命令为嵌入式arm64/Linux设备生成调试的内核模块(这是一个交叉编译的过程,在上一篇文章中笔者提到过):

# stap -v -a arm64 -B CROSS_COMPILE=aarch64-linux-gnu- \
  -r /home/yejq/program/linux-5.10 -m mymalloc mymalloc.stp

如果以上操作成功执行,就在会操作目录下生名称为mymalloc.ko的内核模块,需要将该模块复制到嵌入式设备上,任意目录可以选择——接下来就是调试过程了。

在嵌入式设备上跟踪应用的内存操作

首先,需要把Linux内核的符号文件Module.symvers和配置文件.config复制到嵌入式设备的固定目录下,因为staprun相关的命令在执行时可能需要读取这些文件:

# mkdir -p /lib/modules/$(uname -r)/build
# cp .config Modules.symvers /lib/modules/$(uname -r)/build/

之后就可以使用staprun命令加载Systemtap内核模块:

# staprun -v -v mymalloc.ko
staprun:main:493 modpath="./mymalloc.ko", modname="mymalloc"
staprun:init_staprun:399 init_staprun
staprun:insert_module:71 inserting module ./mymalloc.ko
staprun:insert_module:97 module options: _stp_bufsize=0
staprun:insert_module:105 module path canonicalized to '/tmp/mymalloc.ko'
staprun:insert_module:191 Module mymalloc inserted from file /tmp/mymalloc.ko
staprun:close_ctl_channel:88 Closed ctl fd 4
execing: /opt/addon/libexec/systemtap/stapio -v -v ./mymalloc.ko -F3 
stapio:main:50 modpath="./mymalloc.ko", modname="mymalloc"
stapio:init_stapio:266 init_stapio
stapio:stp_main_loop:470 in main loop
stapio:stp_main_loop:490 select_supported: 1
stapio:init_relayfs:313 initializing relayfs
stapio:init_relayfs:342 attempting to openat trace0
stapio:init_relayfs:342 attempting to openat trace1
stapio:init_relayfs:342 attempting to openat trace2
stapio:init_relayfs:342 attempting to openat trace3
stapio:init_relayfs:366 ncpus=4, nprocs=4, bulkmode=0
stapio:init_relayfs:368 cpui=0, relayfd=0
stapio:init_relayfs:368 cpui=1, relayfd=1
stapio:init_relayfs:368 cpui=2, relayfd=2
stapio:init_relayfs:368 cpui=3, relayfd=3
stapio:init_relayfs:442 starting threads
stapio:stp_main_loop:690 systemtap_module_init() returned 0

接下来打开设备的另一个终端,手动执行/tmp/test-malloc调试文件,可以看到staprun -v -v ./mymalloc.ko所在的终端会输出test-malloc被调试应用的内存分配信息:

malloc(b) has returned: 3119a260
calloc(c, 8) has returned: 3119a280
realloc(0, d) has returned: 3119a2f0
realloc(3119a2f0, e) has returned: 3119a2f0
free(3119a260) has been invoked
free(3119a280) has been invoked
free(0) has been invoked
free(3119a2f0) has been invoked

可见,usdt跟踪在应用的调试中是很有效的,而且几乎不影响应用的执行效率;这种跟踪调试方案很适合用于大型的嵌入式应用(如音视频服务)的开发过程。在Systemtap调试脚本mymalloc.stp的基础上,可以加入一些Hash表,记录每一个应用分配的内存是否会释放,从一定程度上定位到内存是否存在泄露;或者增加一些放问应用的调用栈的操作,可以得到应用中具体分配内存的某个模块,从而深入地观深应用的行为。

 类似资料: