今天这个话题要从stackoverflow上的一个帖子(http://stackoverflow.com/questions/23254418/getaddrinfo-fails-to-resolve-address-in-hosts-file-when-using-mocha/23331397#23331397)谈起。帖子作者遇到的问题简单地说,就是DTrace输出的getaddrinfo的返回值不对:
pid$target::getaddrinfo:return { printf("%d", arg1); }
getaddrinfo的返回值应该是0或者-1,但是帖子的作者说输出了“some big number”。为了验证一下,小编做了个测试。小编用的是Solaris 10(SPARC处理器)的环境,结果输出果然是一些很大的数,看起来像是地址。会是什么地址呢?小编百思不得其解。于是就在dtrace-discuss的邮件讨论组里问了一下:http://www.listbox.com/member/archive/184261/2014/04/sort/time_rev/page/1/entry/0:15/20140425044157:7327DBF0-CC55-11E3-8545-F2C1F3063A1C/,得到的回复是和函数尾调用优化(Tail-call Optimization)有关,并让我参考这个链接:Tail-call Optimization(http://docs.oracle.com/cd/E19253-01/817-6223/chp-fbt-11/index.html)。
尾调用在维基百科里的解释是:“在计算机科学里,尾调用是指一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回的情形。”。也就是像下面这样:
return bar();
而尾调用优化则是:“由于当前函数帧上包含局部变量等等大部分的东西都不需要了,当前的函数帧经过适当的更动以后可以直接当作被尾调用的函数的帧使用,然后程序即可以跳到到被尾调用的函数。产生这种函数帧更动代码与 “jump”(而不是一般常规函数调用的代码)的过程称作尾调用消除(Tail Call Elimination)或尾调用优化(Tail Call Optimization, TCO)。”。通俗地讲,也就是如果一个函数(caller)的结尾是调用另一个函数(callee),或者以callee的返回值作为caller的返回值,或者什么都不返回。那么尾调用优化就可以让callee复用caller的堆栈,不用为callee再分配堆栈空间。关于尾调用优化,大家可参考stackowverflow上的一个帖子:http://stackoverflow.com/questions/13017516/how-is-tail-call-optimization-in-fpls-implemented-at-the-assembly-level。
在Tail-call Optimization(http://docs.oracle.com/cd/E19253-01/817-6223/chp-fbt-11/index.html)这篇文档里,介绍了如何判断是否发生了尾调用优化:
(1)因为arg0是函数返回时的指令地址,在return probe里打印arg0的值。
(2)通过mdb或者gdb工具检查函数的汇编代码。如果arg0指向的是一处函数调用,而不是类似return这样的指令,那就是发生了尾调用优化。
文档还以dup系统调用作为例子。dup的汇编代码:
# echo "dup::dis" | mdb -k dup: sra %o0, 0, %o0 dup+4: mov %o7, %g1 dup+8: clr %o2 dup+0xc: clr %o1 dup+0x10: call -0x1278 <fcntl> dup+0x14: mov %g1, %o7
用以下DTrace命令输出arg0:
# dtrace -q -n fbt::dup:return'{printf("%s+0x%x", probefunc, arg0);}'
结果是0x10,正好是“call -0x1278 ”,所以发生了尾调用优化。
为了验证getaddrinfo是否也是属于尾调用优化这种情况,小编查看了一下getaddrinfo的汇编代码:
(gdb) disassemble getaddrinfo Dump of assembler code for function getaddrinfo: 0xff344834 <+0>: mov %o7, %g1 0xff344838 <+4>: clr %o4 0xff34483c <+8>: call 0xff3440f0 <_getaddrinfo> 0xff344840 <+12>: mov %g1, %o7 End of assembler dump.
果然只是调用了_getaddrinfo这个函数,没有压栈,出栈这些操作(SPARC处理器压栈用save指令,出栈用ret,retl指令,具体可参考手册)。小编用下面的DTrace命令测试了一下:
pid$target::getaddrinfo:return { printf("%d", arg0); }
发现getaddrinfo函数返回时,指令的地址偏移是12,也就是对应的是“ mov %g1, %o7”这条指令,而不是“call 0xff3440f0 <_getaddrinfo>”。小编考虑会不会是由于fbt和pid这两个不同的provider导致的,所以小编又写了个简单的测试脚本,针对dup做个测试(fbt provider没有提供getaddrinfo这个函数):
#!/usr/sbin/dtrace -s #pragma D option quiet fbt::dup:return { printf("fbt: %s+0x%x\n", probefunc, arg0); } pid$target::dup:return { printf("pid: %s+0x%x\n", probefunc, arg0); }
结果输出:
fbt: dup+0x10 pid: dup+0x14
说明的确如小编所推测的:对于不同provider,return probe所记录的函数return address是不同的。
综上所述,当遇到return probe记录的函数返回值不对时,需要考虑是不是发生了函数尾调用优化,具体的方法可以参考本文。
补充一下,要想深入地了解DTrace,对DTrace所运行的环境(包括操作系统,CPU体系结构等)可能都要有所了解。在这里,小编推荐两篇讲解SPARC处理器的文章:The SPARC Architecture(http://web.cecs.pdx.edu/~apt/cs577_2008/sparc_s.pdf)和 Understanding stacks and registers in the Sparc architecture(s)(http://www.sics.se/~psm/sparcstack.html)。