上一篇文章,我们介绍了 stp 的类型、变量等基本构成元素。本文将讲解 stp 如何把某些语句编译成对应的 C 代码。
stp 在编译 if
/ for
这样的控制语句时,基本上就是原样翻译成 C 代码(除了一点: break
和 continue
语句是用 goto
实现的)。
因此这里只着重谈谈 stp 函数调用是如何被编译成 C 代码的。
我们先来看个简单的例子:
probe timer.s(1) { exit() }
编译出来的结果:
(void) ({ function___global_exit__overload_0 (c); if (unlikely(c->last_error)) goto out; (void) 0; });
可以看到 exit()
被编译成 function___global_exit__overload_0 (c)
。
还可以看到 stp 采用在每条函数语句后插入 if (...) goto out
这样的判断来实现类似于抛出异常的功能。
下面是 function___global_exit__overload_0
的实现:
static void function___global_exit__overload_0 (struct context* __restrict__ c) { __label__ deref_fault; __label__ out; struct function___global_exit__overload_0_locals * __restrict__ l = & c->locals[c->nesting+1].function___global_exit__overload_0; (void) l; #define CONTEXT c #define THIS l c->last_stmt = "identifier 'exit' at /usr/local/share/systemtap/tapset/logging.stp:63:10"; if (unlikely (c->nesting+1 >= MAXNESTING)) { c->last_error = "MAXNESTING exceeded"; return; } else { c->nesting ++; } c->next = 0; #define STAP_NEXT do { c->next = 1; goto out; } while(0) #define STAP_RETURN() do { goto out; } while(0) #define STAP_PRINTF(fmt, ...) do { _stp_printf(fmt, ##__VA_ARGS__); } while (0) #define STAP_ERROR(...) do { snprintf(CONTEXT->error_buffer, MAXSTRINGLEN, __VA_ARGS__); CONTEXT->last_error = CONTEXT->error_buffer; goto out; } while (0) #define return goto out if (c->actionremaining < 0) { c->last_error = "MAXACTION exceeded";goto out; } { /* unprivileged */ atomic_set (session_state(), STAP_SESSION_STOPPING); _stp_exit (); } #undef return #undef STAP_PRINTF #undef STAP_ERROR #undef STAP_RETURN deref_fault: __attribute__((unused)); out: __attribute__((unused)); c->nesting --; #undef CONTEXT #undef THIS #undef STAP_NEXT #undef STAP_RETVALUE }
c->last_stmt = "identifier 'exit' at /usr/local/share/systemtap/tapset/logging.stp:63:10";
这里出现了一个文件路径: .../logging.stp
。
打开该文件,能看到 exit()
的定义:
function exit () %( runtime != "bpf" %? %{ /* unprivileged */ atomic_set (session_state(), STAP_SESSION_STOPPING); _stp_exit (); %} %: { /* unprivileged */ /* bpf */ _set_exit_status() printf("") } %)
在处理 stp 脚本的第二阶段(语义分析)时,stp 会从 tapset 里查找 exit 函数的定义,并根据这个定义在下个阶段(中间代码生成)时编译出对应的 C 代码。默认的 tapset 路径是 /usr/local/share/systemtap/tapset
,也可以通过 SYSTEMTAP_TAPSET 环境变量覆盖掉它。详情参考 man 7 stappaths
的说明。(如果你已经忘了什么第二阶段、第三阶段,可以回头看下《systemtap 探秘(一)- 基本介绍》)
exit 函数的定义非常简明 - 如果当前 runtime 不是 bpf,那么生成的 C 代码就是第一个 %{ ... %}
的值。
那么 _stp_exit
是在哪里定义的呢?stp 脚本运行时,除了生成代码时依赖一组 tapset 文件,还会在编译时依赖一组 runtime 文件。同样地,这些 runtime 文件默认位于 /usr/local/share/systemtap/runtime
下面。grep 一下可以发现, _stp_exit
在 linux/runtime.h
中声明,在 linux/io.c
中定义。
在 function___global_exit__overload_0
的实现中,我们还能看到诸如
#define CONTEXT c #define THIS l ... #undef CONTEXT #undef THIS
这样的宏定义。在函数里内嵌 C 代码时需要使用它们,而不是直接裸写 c->xx
/ l->xxx
。这样如果 stp 将来改变了传递 CONTEXT 的方式,也不致于 break 掉你的代码。当然,由于 stp 脚本的作者可以直接内嵌 C 代码,没有什么可以阻止他们放飞自我。所以说,systemtap 的安全是相对的,就像糟糕的 Java instrumentation 代码会崩掉你的 Java 应用一样,未经考验的 stp 脚本也会带来 kernel panic。
让我们看个稍微复杂的例子:
probe timer.s(1) { a = execname() print(a) }
编译出来的结果:
(void) ({ ({ c->locals[c->nesting+1].function___global_execname__overload_0.__retvalue = &l->__tmp0[0]; function___global_execname__overload_0 (c); if (unlikely(c->last_error)) goto out; (void) 0; }); strlcpy (l->l___stable___global_execname__overload_0_value, l->__tmp0, MAXSTRINGLEN); l->__tmp0; }); (void) ({ strlcpy (l->__tmp3, l->l___stable___global_execname__overload_0_value, MAXSTRINGLEN); strlcpy (l->l_a, l->__tmp3, MAXSTRINGLEN); l->__tmp3; });
l->l_a
的值,来自于 function___global_execname__overload_0.__retvalue
。
我们看下 function___global_execname__overload_0
的实现:
#define CONTEXT c #define THIS l #define STAP_RETVALUE THIS->__retvalue c->last_stmt = "identifier 'execname' at /usr/local/share/systemtap/tapset/linux/context.stp:17:10"; ... { /* pure */ /* unprivileged */ /* stable */ strlcpy (STAP_RETVALUE, current->comm, MAXSTRINGLEN); }
这个实现多了个 STAP_RETVALUE
的宏。因为 execname 的定义是 func execname:string ()
,有返回值,所以
生成的 C 代码里有 STAP_RETVALUE
。所谓的“返回一个值”其实就是修改 STAP_RETVALUE
的值。
最后让我们看一个既有入参也有返回的例子:
probe timer.s(1) { a = cmdline_args(0, -1, " ") print(a) }
生成的 C 代码里可以看到这些宏:
#define CONTEXT c #define THIS l #define STAP_ARG_n THIS->l_n #define STAP_ARG_m THIS->l_m #define STAP_ARG_delim THIS->l_delim
如果在 %{ ... %}
括起来的 C 代码里访问这些宏,就能接触到传进来的参数。
但实际上 cmdline_args
并没有用到这些宏,因为它是用 stp 实现的,而不是通过内嵌 C 代码来实现代码逻辑。
虽然没有直接使用宏,但是编译成 C 代码时,还是会把对参数 xx 的访问编译成 l->l_xx
。比如 m<0 || __nr<=m
编译成 ((((l->l_m) < (((int64_t)0LL))))) || ((((l->l___nr) <= (l->l_m))))
。
至于 return ""
,则编译成 strlcpy (l->__retvalue, "", MAXSTRINGLEN)
。
总而言之,如果需要返回值,则修改 l->__retvalue
或者 STAP_RETVALUE
;如果需要访问入参,则使用 l->l_arg
或者 STAP_ARG_arg
。
对于 systemtap 如何把 stp 脚本转成 C 代码的介绍,到本文算是告一段落了。从下篇开始,我们来看看 stp 脚本运行的最后两个阶段 - 编译内核模块和运行。