USDT
usdt(User-Statically-Defined-Tracepoint)是一种向应用插入跟踪点的技术方案,其特点是跟踪点的插入是静态的,通常需要修改应用的源码并再次编译。该技术方案源于DTrace,不过usdt
应用跟踪的功能在Systemtap和bcc等内核跟踪调试工具中已有良好的支持。Systemtap
开源工具提供了sys/sdt.h
头文件,该头文件没有相应用C代码实现,仅仅提供了多个STAP_PROBExxx
和DTRACE_PROBExxx
宏定义。这些宏定义通过GCC
编译器的内联汇编和段操作在编译成生的ELF
文件中加入一些用于跟踪的信息(主要加入一个nop
机器指令和.note.stapsdt
段信息)。当应用未被跟踪调试时,这些额外的信息不会影响应用的正常功能,也不会造成性能的消耗;当应用被跟踪调试时,Systemtap
或bcc
工具会通过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表,记录每一个应用分配的内存是否会释放,从一定程度上定位到内存是否存在泄露;或者增加一些放问应用的调用栈的操作,可以得到应用中具体分配内存的某个模块,从而深入地观深应用的行为。