首页 » 技术文章 » luahook获取lua性能

luahook获取lua性能

 

背景

目前网络上出现的一些检测lua性能的开源工具,主要利用luahook技术,在lua进入退出函数设置hook,或在每行lua代码执行时安装hook,来得到某个函数或某行lua代码的执行开销。最近项目接触了这一部分内容,这里简单介绍下学到的一些点。

实现原理

lua源码实现中,在解释执行CALL指令和RET等luacode时,会调用安装的hook函数(假定已经调用sethook),在执行的lua code行号发生变化时,也会调用安装的hook函数。hook函数通过计算某个lua函数的CALL与RET两个时机的时间差,可以获取到某个lua函数的时间消耗;通过计算行与行之前的lua代码执行时间差,可以知晓某行lua代码的性能。

部分细节

这里都以函数级hook为例(行hook相似)。当某个lua函数被Call时,会进入我们安装的hook函数,该函数可以调用lua_getinfo来获取即将调用的lua函数信息,代码如下:

static void callhook(lua_State *L, lua_Debug *ar) 
{
 
  // 这部分为了后去究竟在哪行lua代码调用的该函数
  if (lua_getstack(L, 1, &previous_ar) == 0) {
    currentline = -1;
  } else {
    lua_getinfo(L, "l", &previous_ar);
    currentline = previous_ar.currentline;
  }
      
  // 这里获取函数的信息
  lua_getinfo(L, "nS", ar);

  stackIndex = lua_gettop(L);

  //printf("stacklevel = %d", S->stack_level);

  if (!ar->event) 
  {
		  /* entering a function */
		  // do something..
  }
  else 
  { 
	    /* ar->event == "return" */
        // do something
  }

}

lua_getinfo会“尽力”为我们找到待调用的函数信息,包括函数名、文件、定义的行号等等信息,这里“尽力”的意思是:它会进行符号查找,来找到该函数关联的信息,比如如果保存当前函数的寄存器是在前述指令中通过GETGLOBAL获取的,那么它会把GETGLOBAL时传递的key返回给我们,如果是一个局部变量,那么会返回该局部变量的名称(这部分逻辑尝试还挺多的,未全部细看)。

lua_getinfo失效

很多项目会使用C和lua混合编程,这时候lua和C交互时,可能会导致lua_getinfo无法找到函数信息,较为简单的例子为:

print("a")

该函数就可以出现lua_getinfo失效的情形。原因如下

{"print", luaB_print},

static int luaB_print (lua_State *L) {
  int n = lua_gettop(L);  /* number of arguments */
  int i;
  lua_getglobal(L, "tostring");
  for (i=1; i<=n; i++) {
    const char *s;
    lua_pushvalue(L, -1);  /* function to be called */
    lua_pushvalue(L, i);   /* value to print */
    lua_call(L, 1, 1);
    s = lua_tostring(L, -1);  /* get result */
    if (s == NULL)
      return luaL_error(L, LUA_QL("tostring") " must return a string to "
                           LUA_QL("print"));
    if (i>1) fputs("\t", stdout);
    fputs(s, stdout);
    lua_pop(L, 1);  /* pop result */
  }
  fputs("\n", stdout);
  return 0;
}

luaB_print中又调用了lua_call,会导致进入hook函数,此时就获取不到函数信息了(因为此时处在c代码中,没有lua函数信息)。如何处理呢?一般来说,我们的处理方式是:保存它的父函数名(前面加上called from),这样就所有的函数名都不为空了。但也存在缺陷,无法知道具体为哪个C函数(一般来说通过阅读lua代码可以解决这个问题),如果需要该真实信息,可以使用如下方法:

lua_getinfo(L, "nSf", ar);

  char tmpBuf[100] = {0};

  if(ar->source &&  strcmp(ar->source, "=[C]") == 0)
  { 
  		 
     	void* cfun = lua_tocfunction(L, -1);
		Dl_info info;
		dladdr(cfun, &info);        
    	int xx =  (int)((unsigned  int)cfun - (unsigned int )info.dli_fbase);
        sprintf(tmpBuf, "## %s %x %p %p \n", info.dli_fname, xx,  cfun, info.dli_fbase );
  }

  lua_pop(L, 1);

lua_getinfo参数多传递一个f,这样就获取到了C函数的地址,dladdr就能获取到该函数的信息,配合addr2line或google breakpad就能得到C函数的所有信息(如果调试符号没被删除的话,服务器程序一般来说会考虑保留符号)。

元方法会导致hook函数被调用

lua5中源码如下:

static void callTMres (lua_State *L, StkId res, const TValue *f,
                        const TValue *p1, const TValue *p2) {
  ptrdiff_t result = savestack(L, res);
  setobj2s(L, L->top, f);  /* push function */
  setobj2s(L, L->top+1, p1);  /* 1st argument */
  setobj2s(L, L->top+2, p2);  /* 2nd argument */
  luaD_checkstack(L, 3);
  L->top += 3;
  luaD_call(L, L->top - 3, 1);
  res = restorestack(L, result);
  L->top--;
  setobjs2s(L, res, L->top);
}

由于调用了luaD_call,该函数最终也会导致hook函数被调用。在访问table或userdata时,也许不禁意间hook到了一个函数调用。如下述lua代码

local p = player.pos

如果最终获取pos是通过元方法调用实现,那么就会触发hook函数。

本文作者:马良

原文链接:luahook获取lua性能,转载请注明来源!

5