转载

systemtap 探秘(四)- 函数调用

上一篇文章,我们介绍了 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 脚本运行的最后两个阶段 - 编译内核模块和运行。

原文  https://segmentfault.com/a/1190000020459894
正文到此结束
Loading...