共计 4225 个字符,预计需要花费 11 分钟才能阅读完成。
上一篇文章,我们介绍了 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 脚本运行的最后两个阶段 – 编译内核模块和运行。