使用 GDB 跟踪 Lua 内存分配

Posted by zhuizhuhaomeng Blog on February 5, 2024

我们想要了解一个某一个软件的内存是如何分配的,最好的方式就是看看内存分配的函数的调用栈。 我们可以使用 GDB 来跟踪内存的分配和释放。

设置日志输出文件

因为程序都会涉及大量的内存分配和释放,所以我们需要把调试信息导出到文件。 这样我们就可以使用其它工具对导出的日志进一步的分析。

这就涉及 logging 这个命令。

set logging file gdb.log
set logging enabled on

因为我们不希望输出到一半就卡住,因此还需要加上禁止分页.

set pagination off

监控内存分配

一般情况下,跟踪内存分配和释放,我们只要把函数探针打在特定的函数即可。

比如,把函数探针打在 luaM_realloc_luaM_malloc_luaM_free_ 来观察 Lua 的内存分配和释放。 因为我们不能一直手动按回车,因此就需要 command 命令来配合。

break luaM_realloc_
  commands
    bt
    print "--- luaM_realloc_ end"
    continue
  end

break luaM_malloc_
  commands
    bt
    print "--- luaM_malloc_ end"
    continue
  end

break luaM_free_
  commands
    bt
    print "--- luaM_free_ end"
    continue
  end

监控内存分配的结果

我们不仅仅要知道分配的调用栈,还希望知道分配的内存指针,那么这时候就需要在代码行上打上探针了。

很幸运的是,lua 的内存分配只有一个返回的地方, 我们只要在一个地方打上探针即可(忽略异常返回 NULL的情况)。

break lmem.c:213
  commands
    bt
    info registers rax
    print "--- luaM_malloc_ ret"
    continue
  end

break lmem.c:188
  commands
    bt
    info registers rax
    print "--- luaM_realloc_ ret"
    continue
  end

探针打在 ret 指令上

有时候打在代码行上并不灵光,这时候需要把函数探针打在指令上,这时候就可以用 dis 来查看 ret 指令。

因为一个函数又多个 ret 指令,每一个都打上断点是没有问题的,但是这样子显然没有必要。 但是对于不熟悉汇编的人来说,想要判断哪个 ret 真是太难了。

因此,这里应该使用 disasemble /s, 因为这样子会把对应的代码行给出来,方便我们对照分析。

(gdb) disassemble /s luaM_malloc_
Dump of assembler code for function luaM_malloc_:
lmem.c:
201	void *luaM_malloc_ (lua_State *L, size_t size, int tag) {
202	  if (size == 0)
   0x000000000040c999 <+0>:	test   %rsi,%rsi
   0x000000000040c99c <+3>:	je     0x40ca01 <luaM_malloc_+104>

201	void *luaM_malloc_ (lua_State *L, size_t size, int tag) {
   0x000000000040c99e <+5>:	push   %r13
   0x000000000040c9a0 <+7>:	push   %r12
   0x000000000040c9a2 <+9>:	push   %rbp
   0x000000000040c9a3 <+10>:	push   %rbx
   0x000000000040c9a4 <+11>:	sub    $0x8,%rsp
   0x000000000040c9a8 <+15>:	mov    %rdi,%r12
   0x000000000040c9ab <+18>:	mov    %rsi,%rbx

204	  else {
205	    global_State *g = G(L);
   0x000000000040c9ae <+21>:	mov    0x18(%rdi),%r13

206	    void *newblock = firsttry(g, NULL, tag, size);
   0x000000000040c9b2 <+25>:	movslq %edx,%rbp
   0x000000000040c9b5 <+28>:	mov    0x8(%r13),%rdi
   0x000000000040c9b9 <+32>:	mov    %rsi,%rcx
   0x000000000040c9bc <+35>:	mov    %rbp,%rdx
   0x000000000040c9bf <+38>:	mov    $0x0,%esi
   0x000000000040c9c4 <+43>:	call   *0x0(%r13)

207	    if (l_unlikely(newblock == NULL)) {
   0x000000000040c9c8 <+47>:	test   %rax,%rax
   0x000000000040c9cb <+50>:	je     0x40c9dc <luaM_malloc_+67>

211	    }
212	    g->GCdebt += size;
   0x000000000040c9cd <+52>:	add    %rbx,0x18(%r13)

213	    return newblock;
   0x000000000040c9d1 <+56>:	add    $0x8,%rsp
   0x000000000040c9d5 <+60>:	pop    %rbx

214	  }
215	}
   0x000000000040c9d6 <+61>:	pop    %rbp
   0x000000000040c9d7 <+62>:	pop    %r12
   0x000000000040c9d9 <+64>:	pop    %r13
   0x000000000040c9db <+66>:	ret    

208	      newblock = tryagain(L, NULL, tag, size);
   0x000000000040c9dc <+67>:	mov    %rbx,%rcx
   0x000000000040c9df <+70>:	mov    %rbp,%rdx
   0x000000000040c9e2 <+73>:	mov    $0x0,%esi
   0x000000000040c9e7 <+78>:	mov    %r12,%rdi
   0x000000000040c9ea <+81>:	call   0x40c7f4 <tryagain>

209	      if (newblock == NULL)
   0x000000000040c9ef <+86>:	test   %rax,%rax
   0x000000000040c9f2 <+89>:	jne    0x40c9cd <luaM_malloc_+52>

210	        luaM_error(L);
   0x000000000040c9f4 <+91>:	mov    $0x4,%esi
   0x000000000040c9f9 <+96>:	mov    %r12,%rdi
   0x000000000040c9fc <+99>:	call   0x4087b5 <luaD_throw>

203	    return NULL;  /* that's all */
   0x000000000040ca01 <+104>:	mov    $0x0,%eax

214	  }
215	}
   0x000000000040ca06 <+109>:	ret    
End of assembler dump.

这里我们选择 0x000000000040c9db 这个地址的 ret 指令。

我们也可以选择 0x000000000040c9d1, 因为从 0x000000000040c9d1 开始到 ret 指令属于汇编的 epilogue。 因此我们也可以这么设置断点。

break *0x000000000040c9d1
    command
      bt
      info registers rax
      print "--- luaM_malloc_ ret"
      continue
    end

保存断点,方便重复执行

save breakpoints trace-mem.gdb

总结

以代码行设置断点为例,我们保存上面的指令为如下的文件,假设文件名为 trace-mem.gdb。

set logging file gdb.log
set logging enabled on
set pagination off

break lmem.c:188
  commands
    info registers rax
    print "--- luaM_realloc_ ret"
    continue
  end

break lmem.c:213
  commands
    info registers rax
    print "--- luaM_malloc_ ret"
    continue
  end

break luaM_free_
  commands
    bt
    print "--- luaM_free_ end"
    continue
  end

那么如何利用这个脚本呢?这个一个简单的例子。

1
2
3
$ gdb lua
source trace-mem.gdb
run test.lua

而且我们也可以在编辑器里面对 trace-mem.lua 进行编辑,而不需要在gdb 里面一个字母一个字母慢慢的敲了。