systemtap-探秘四-函数调用

11次阅读

共计 4225 个字符,预计需要花费 11 分钟才能阅读完成。

上一篇文章,我们介绍了 stp 的类型、变量等基本构成元素。本文将讲解 stp 如何把某些语句编译成对应的 C 代码。
stp 在编译 if / for 这样的控制语句时,基本上就是原样翻译成 C 代码(除了一点:breakcontinue 语句是用 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_exitlinux/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 脚本运行的最后两个阶段 – 编译内核模块和运行。

正文完
 0