2013年9月14日星期六

gcc中的constructor属性和destructor属性

   constructor属性可以使函数在main()函数之前执行,destructor属性会让函数在main()函数完成或调用exit()之后被执行。这些属性可以用来在程序运行之前初始化所需的数据,非常有用。而且这两个属性都还可以指定优先级,控制使用修饰的函数的执行顺序,优先级的值必须大于100,因为0到100之间的优先级由gcc来使用,优先级的值越小,优先级越高,会优先执行。另外还有一点需要注意,如果是接收到信号退出,例如SIGSEGV或者SIGKILL信号,destructor属性修饰的函数则不会被调用。具体可以参见《Declaring Attributes of Functions》
我们先来看看不指定优先级时,调用的顺序是什么样的,示例程序如下:
#include <stdio.h>
#include <stdlib.h>

static void __attribute__((constructor)) pre_main1(void)
{
printf("Come to %s\n", __func__);
}

static void __attribute__((constructor)) pre_main2(void)
{
printf("Come to %s\n", __func__);
}

static void __attribute__((constructor)) pre_main3(void)
{
printf("Come to %s\n", __func__);
}

int main(void)
{
printf("Exiting.....\n");
return 0;
}

static void __attribute__((destructor)) back_main1(void)
{
printf("Come to %s\n", __func__);
}

static void __attribute__((destructor)) back_main2(void)
{
printf("Come to %s\n", __func__);
}
编译后执行,输出结果如下:

Come to pre_main3
Come to pre_main2
Come to pre_main1
Exiting.....
Come to back_main1
Come to back_main2
这个结果比较意外,因为我之前测试的时候,没有指定优先级时,执行的顺序和函数在源码中的顺序一样,而这里constructor属性修饰的函数,其执行顺序刚好相反。我又找了一个在线的编译器,发现输出结果和前面的也不一样。
看来如果没有指定优先级时constructor属性和destructor属性修饰的函数的执行顺序和编译器、系统有关。不过也让我很好奇,这些函数是怎么被调用的,所以就继续深入下去。
constructor属性和destructor属性修饰的函数的地址会分别存入.ctors section和.dctors section,这两个段中存放的内容都被当作函数处理,段中的函数分别由__do_global_ctors_aux函数和__do_global_dtors_aux函数去调用执行。下面的讨论主要围绕这两个函数展开。注意,后面看到的一些地址都只是我的机器上看到的,不同的机器可能不一样。
首先来看.ctors section的内容,如下所示:
[root@CentOS_190 debug]# objdump -s -j .ctors a.out

a.out: file format elf64-x86-64

Contents of section .ctors:
600868 ffffffff ffffffff 04054000 00000000 ..........@.....
600878 21054000 00000000 3e054000 00000000 !.@.....>.@.....
600888 00000000 00000000 ........
[root@CentOS_190 debug]#
从上面我们可以看到,.ctors section的开始位置和结束位置的地址分别为0x600868和0x600888(这两个地址非常重要,后面会讲到),对应的值为0xffffffffffffffff和0x0000000000000000,这两个值相当于也是.ctors section的开始和结束标志。在.ctors section中有三个函数地址,分别为0x0000000000400504、0x0000000000400521和0x000000000040053e。如果你对这里的三个地址有疑惑,请注意前面输出的文件格式elf64-x86-64,也就是说这里生成的可执行程序是在x86架构下,x86下的字节模式是小端模式,即数据的低位保存在低地址,高位保存在高地址,所以真实的数据要颠倒过来看。
在我们的示例程序中,pre_main1、pre_main2和pre_main3三个函数使用了constructor属性,所以这三个函数的地址会放在.ctors section中,但是现在还不知道存放的顺序是什么。先来确定这三个函数的地址,如下所示:
[root@CentOS_190 debug]# objdump -d a.out | grep pre_main*
0000000000400504 <pre_main1>:
0000000000400521 <pre_main2>:
000000000040053e <pre_main3>:
[root@CentOS_190 debug]#
现在我们可以确定.ctors section中的结构,如下所示:

接下来我们看看__do_global_ctors_aux()函数是如何来处理.ctors section的。__do_global_ctors_aux函数由.init section的_init函数调用,其反汇编结果如下所示:
0000000000400650 <__do_global_ctors_aux>:
400650: 55 push %rbp
400651: 48 89 e5 mov %rsp,%rbp
400654: 53 push %rbx
400655: 48 83 ec 08 sub $0x8,%rsp
400659: 48 8b 05 20 02 20 00 mov 0x200220(%rip),%rax # 600880 <__CTOR_LIST__+0x18>
400660: 48 83 f8 ff cmp $0xffffffffffffffff,%rax
400664: 74 19 je 40067f <__do_global_ctors_aux+0x2f>
400666: bb 80 08 60 00 mov $0x600880,%ebx
40066b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
400670: 48 83 eb 08 sub $0x8,%rbx
400674: ff d0 callq *%rax
400676: 48 8b 03 mov (%rbx),%rax
400679: 48 83 f8 ff cmp $0xffffffffffffffff,%rax
40067d: 75 f1 jne 400670 <__do_global_ctors_aux+0x20>
40067f: 48 83 c4 08 add $0x8,%rsp
400683: 5b pop %rbx
400684: c9 leaveq
400685: c3 retq
400686: 90 nop
400687: 90 nop
地址为400659处的指令将.ctors section结尾处(结尾处的值为0x0000000000000000)的前一个位置的值保存到rax寄存器中。如果.ctors section为空,则rax中的值为0xffffffffffffffff,即.ctors section开始位置的值。如果.ctors section为空,则400660处的指令比较后会执行je指令,跳转到40067f处执行,然后直接从__do_global_ctors_aux()函数退出。如果.ctors section不为空,则rax中存储的是一个函数的地址,结合上面的.ctors section的结构图,我们不难看出,此时rax中的值就是pre_main3()函数的地址值。如果不为空,则开始执行400666处的指令,这条指令将0x600880这个常数值保存在rbx寄存器中,这个常数值就是pre_main3()函数的地址值在.ctors section中的地址值。注意,此时rbx中只是存储的是一个地址值,并不是具体的函数地址值。接着调用sub指令,将rbx减8,此时rbx的值为0x600878,也就是pre_main2()函数的地址值在.ctors section中的地址值。接着会使用callq指令,调用rax寄存器中保存的函数地址,此时的函数地址是pre_main3()函数的地址。在调用完函数后,将rbx寄存器中存储的地址中的数值保存到rax中,这里是pre_main2()函数的地址值,也就是说在调用完pre_main3()函数后,rax中存储的是pre_main2()函数的地址。依次类推,在执行完pre_main1()后,rax中存储的是.ctors section的开始位置的值,即0x0xffffffffffffffff,此时在执行完400679处的判断后,会接着后面的指令执行然后退出。分析到这里,不难理解为什么函数的执行顺序和在源码位置中的相反。
现在来分析.dtors section的处理。过程和前面的类似,先来看.dtors section的内容,如下所示:
[root@CentOS_190 debug]# objdump -s -j .dtors a.out

a.out: file format elf64-x86-64

Contents of section .dtors:
600890 ffffffff ffffffff 70054000 00000000 ........p.@.....
6008a0 8d054000 00000000 00000000 00000000 ..@.............
[root@CentOS_190 debug]#
接着确定函数的地址,如下所示:
[root@CentOS_190 debug]# objdump -d a.out | grep back_main*
0000000000400570 <back_main1>:
000000000040058d <back_main2>:
[root@CentOS_190 debug]#
同样我们可以得到.dtors section的结构,如下所示:

.dtors section中的内容由 __do_global_dtors_aux()函数处理,其反汇编结果如下所示:
0000000000400470 <__do_global_dtors_aux>:
400470: 55 push %rbp
400471: 48 89 e5 mov %rsp,%rbp
400474: 53 push %rbx
400475: 48 83 ec 08 sub $0x8,%rsp
400479: 80 3d 08 06 20 00 00 cmpb $0x0,0x200608(%rip) # 600a88 <completed.6349>
400480: 75 4b jne 4004cd <__do_global_dtors_aux+0x5d>
400482: bb a8 08 60 00 mov $0x6008a8,%ebx
400487: 48 8b 05 02 06 20 00 mov 0x200602(%rip),%rax # 600a90 <dtor_idx.6351>
40048e: 48 81 eb 90 08 60 00 sub $0x600890,%rbx
400495: 48 c1 fb 03 sar $0x3,%rbx
400499: 48 83 eb 01 sub $0x1,%rbx
40049d: 48 39 d8 cmp %rbx,%rax
4004a0: 73 24 jae 4004c6 <__do_global_dtors_aux+0x56>
4004a2: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
4004a8: 48 83 c0 01 add $0x1,%rax
4004ac: 48 89 05 dd 05 20 00 mov %rax,0x2005dd(%rip) # 600a90 <dtor_idx.6351>
4004b3: ff 14 c5 90 08 60 00 callq *0x600890(,%rax,8)
4004ba: 48 8b 05 cf 05 20 00 mov 0x2005cf(%rip),%rax # 600a90 <dtor_idx.6351>
4004c1: 48 39 d8 cmp %rbx,%rax
4004c4: 72 e2 jb 4004a8 <__do_global_dtors_aux+0x38>
4004c6: c6 05 bb 05 20 00 01 movb $0x1,0x2005bb(%rip) # 600a88 <completed.6349>
4004cd: 48 83 c4 08 add $0x8,%rsp
4004d1: 5b pop %rbx
4004d2: c9 leaveq
4004d3: c3 retq
4004d4: 66 66 66 2e 0f 1f 84 data32 data32 nopw %cs:0x0(%rax,%rax,1)
前面的部分我们不关心,直接从400482处的指令开始。400482处的指令将常数0x6008a8存储到ebx寄存器(相当于是rax寄存器)中,结合.dtors section的结构图,我们可以看到0x6008a8是.dtors section结束位置的地址。400487处的指令将dtor_index.6351变量(后面直接叫dtor_index)的值保存到rax寄存器中,dtor_index是在.bss section中,这个section中存放的是为初始化的全局变量和静态变量,在程序执行之前.bss section会自动清零,所以dtor_index的值为0,rax中的值也是0。40048e处的sub指令相当于是rbx=rbx-0x600890,0x600890是.dtors section的开始位置的地址值,计算的结果是0x18,rbx的值也是0x18。接着的sar指令将rbx的值右移3位,rbx中的值变为0x3,然后又使用sub指令将rbx减1,此时rbx的值变为2,也就是.dtors section中函数地址的个数。初始化操作完成之后,比较rax和rbx的值,如果rax大于等于rbx,则跳转到退出函数的位置执行。到这里已经可以明白是怎么回事了,rax就相当于是执行循环时的索引,rbx是循环的次数。4004a8处的指令将rax加1,运算后的值存储到dtor_index中,然后调用callq函数执行地址0x600890+rax*8处存储的函数。第一次循环时rax的值为1,所以计算的结果为0x600898,这个地址处存储的是back_main1()函数的地址值(参见.dtor section的结构图)。下次循环的时候,rax的值为2,计算的结果为0x6008a0,这个地址存储的是back_main2()函数的地址值。两个函数执行完之后,退出循环。
通过前面的分析,对constructor和destructor属性以及.dtors section和.ctors section都有了较深的理解和认识,下面我们结合前面了解到的内容,写一个程序,主动去调用.ctors section和.dtors section中的内容。参考的这篇文章《of ctors and dtors》 ,示例程序如下:

#include <stdio.h>
#include <stdlib.h>

static void empty(void)
{
/* empty */
}

typedef void (*fptr)(void);

static fptr ctor_list[1] __attribute((section(".ctors"))) = { (fptr)-1 };
static fptr dtor_list[1] __attribute((section(".dtors"))) = { (fptr)empty };

static int ctor_list_enable;
static int dtor_list_enable;

static void invoke_ctors(void)
{
fptr *ptr;

ctor_list_enable = 1;

for (ptr = ctor_list + 1; *ptr != NULL; ptr++) {
(**ptr)();
}
}

static void invoke_dtors(void)
{
fptr *ptr;

dtor_list_enable = 1;

for (ptr = dtor_list + 1; *ptr != NULL; ptr++) {
(**ptr)();
}
}

static __attribute((constructor)) void pre_main(void)
{
if (ctor_list_enable) {
printf("Come to %s, called by invoke_ctors\n", __func__);

} else {
printf("Come to %s\n", __func__);

}
}

static __attribute((constructor)) void pre_main2(void)
{
if (ctor_list_enable) {
printf("Come to %s, called by invoke_ctors\n", __func__);

} else {
printf("Come to %s\n", __func__);

}
}

static __attribute((destructor)) void back_main(void)
{
if (ctor_list_enable) {
printf("Come to %s, called by invoke_ctors\n", __func__);

} else {
printf("Come to %s\n", __func__);

}
}

int main(void)
{
invoke_ctors();
printf("Exiting....\n");
invoke_dtors();
exit(0);
}
这里之所以在定义ctor_list时指定的是-1,而不是其他值,是因为我们前面讲了,.ctors section中的处理是在检测到0xffffffffffffffff后停止,如果是NULL或者是其他不是合法函数的地址,则会产生段错误。定义dtor_list时没有使用-1,而是定义了一个空函数,是因为.dtors section的处理和.ctors section的处理不一样,它是先根据偏移计算出段中函数指针的个数,然后再去逐个执行每个函数,如果设置成-1,则会产生段错误。
示例程序的输出如下图所示:
[root@CentOS_190 debug]# ./a.out
Come to pre_main2
Come to pre_main
Come to pre_main, called by invoke_ctors
Come to pre_main2, called by invoke_ctors
Exiting....
Come to back_main, called by invoke_ctors
Come to back_main, called by invoke_ctors
[root@CentOS_190 debug]#

2013年9月12日星期四

C语言中没有main函数生成可执行程序的几种方法

1、define预处理指令
这种方式很简单,只是简单地将main字符串用宏来代替,或者使用##拼接字符串。示例程序如下:
#include <stdio.h>

#define begin main

int begin(void)
{
printf("Hello, World!\n");
return 0;
}


#include <stdio.h>

#define begin m##a##i##n

int begin(void)
{
printf("Hello, World!\n");
return 0;
}
严格来说,这种方式只算是一种技巧......
2、_start函数
_start函数是C程序的入口函数,会调用main函数。在调用main函数之前,会先执行_start函数分配必要的资源,然后再调用main函数。但是在用gcc编译程序时可以使用-nostartfiles选项来重写_start函数。示例程序如下:
#include <stdio.h>
#include <stdlib.h>

_start(void) {
printf("Hello, World!\n");
exit(0);
}
编译上面的程序的命令为:
gcc -nostartfiles _start.c -o a.out
反汇编生成的可执行程序,如下所示:
a.out: file format elf64-x86-64


Disassembly of section .plt:

0000000000400320 <puts@plt-0x10>:
400320: ff 35 ea 01 20 00 pushq 0x2001ea(%rip) # 600510 <_GLOBAL_OFFSET_TABLE_+0x8>
400326: ff 25 ec 01 20 00 jmpq *0x2001ec(%rip) # 600518 <_GLOBAL_OFFSET_TABLE_+0x10>
40032c: 0f 1f 40 00 nopl 0x0(%rax)

0000000000400330 <puts@plt>:
400330: ff 25 ea 01 20 00 jmpq *0x2001ea(%rip) # 600520 <_GLOBAL_OFFSET_TABLE_+0x18>
400336: 68 00 00 00 00 pushq $0x0
40033b: e9 e0 ff ff ff jmpq 400320 <puts@plt-0x10>

0000000000400340 <exit@plt>:
400340: ff 25 e2 01 20 00 jmpq *0x2001e2(%rip) # 600528 <_GLOBAL_OFFSET_TABLE_+0x20>
400346: 68 01 00 00 00 pushq $0x1
40034b: e9 d0 ff ff ff jmpq 400320 <puts@plt-0x10>

Disassembly of section .text:

0000000000400350 <_start>:
400350: 55 push %rbp
400351: 48 89 e5 mov %rsp,%rbp
400354: bf 68 03 40 00 mov $0x400368,%edi
400359: e8 d2 ff ff ff callq 400330 <puts@plt>
40035e: bf 00 00 00 00 mov $0x0,%edi
400363: e8 d8 ff ff ff callq 400340 exit@plt
上面的结果是完整的反汇编结果,我们可以看到_start函数中只有我们调用printf和exit函数相关的一些指令,并且.txt段中只有_start函数,没有看到main函数。如果将源代码中的_start替换为main,重新编译程序,反汇编的结果中会看到_start函数会调用到main
另外还有一点需要注意,因为这里重写了_start函数,所以gcc为默认的main函数准备的清理动作就没用上,所以如果退出的时候直接使用return,会导致程序崩溃。所以这里要使用exit()来退出程序。具体的原因可以参见这篇文章
3、gcc的-e选项
示例程序如下:
#include <stdio.h>
#include <stdlib.h>

int nomain(int i, int j, int k) {
printf("Hello, World!\n");
exit(0);
}
将上面的程序保存为m.c,编译命令如下所示:
gcc -nostartfiles -e nomain m.c -o a.out
继续使用objdump反汇编生成的可执行程序,结果如下:
a.out: file format elf64-x86-64


Disassembly of section .plt:

0000000000400320 <puts@plt-0x10>:
400320: ff 35 f2 01 20 00 pushq 0x2001f2(%rip) # 600518 <_GLOBAL_OFFSET_TABLE_+0x8>
400326: ff 25 f4 01 20 00 jmpq *0x2001f4(%rip) # 600520 <_GLOBAL_OFFSET_TABLE_+0x10>
40032c: 0f 1f 40 00 nopl 0x0(%rax)

0000000000400330 <puts@plt>:
400330: ff 25 f2 01 20 00 jmpq *0x2001f2(%rip) # 600528 <_GLOBAL_OFFSET_TABLE_+0x18>
400336: 68 00 00 00 00 pushq $0x0
40033b: e9 e0 ff ff ff jmpq 400320 <puts@plt-0x10>

0000000000400340 <exit@plt>:
400340: ff 25 ea 01 20 00 jmpq *0x2001ea(%rip) # 600530 <_GLOBAL_OFFSET_TABLE_+0x20>
400346: 68 01 00 00 00 pushq $0x1
40034b: e9 d0 ff ff ff jmpq 400320 <puts@plt-0x10>

Disassembly of section .text:

0000000000400350 <nomain>:
400350: 55 push %rbp
400351: 48 89 e5 mov %rsp,%rbp
400354: 48 83 ec 10 sub $0x10,%rsp
400358: 89 7d fc mov %edi,-0x4(%rbp)
40035b: 89 75 f8 mov %esi,-0x8(%rbp)
40035e: 89 55 f4 mov %edx,-0xc(%rbp)
400361: bf 75 03 40 00 mov $0x400375,%edi
400366: e8 c5 ff ff ff callq 400330 <puts@plt>
40036b: bf 00 00 00 00 mov $0x0,%edi
400370: e8 cb ff ff ff callq 400340 <exit@plt>
从上面我们可以看到指定的nomain函数位于.text段的开始位置,同样在函数结束的时候没有gcc为main函数准备的清理动作,所以在这里也只能使用exit()来退出程序,而不能使用return。
4、nostartfiles选项
前面已经多次使用了该选项,不过都是配合其他选项使用的,这个选项也可以单独使用,其含义为"Do not use the standard system startup files when linking"。
示例程序如下:
#include <stdio.h>
#include <stdlib.h>

void func() {
printf("I am func....\n");
}

int nomain1(int i, int j, int k) {
func();
printf("%s: Hello, World!\n", __func__);
exit(0);
}
上面的程序保存为k.c,然后使用下面的命令编译:
[root@CentOS_190 ~]# gcc -nostartfiles p.c
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000400398

在单独使用nostartfiles选项时会报警告,生成的可执行程序可以执行,但是会产生段错误,去掉对func()函数的调用就不会产生段错误了。将生成的可执行程序反汇编,和使用前面的方法生成可执行程序的反汇编结果比较,发现除了函数名不一样外,没有其他区别,不知道为什么会产生段错误。知道的麻烦告知一声,拜谢!

Linux下可执行程序调试信息的分离及release程序的调试

  前两天在群里看到在讨论如何把debug版中的符号表加到release版本中,觉得这个非常有用,所以学习一下。
使用的工具是objcopy。
如果要生成单独的调试信息文件,命令如下:
objcopy –only-keep-debug foo foo.dbg
如果要去除调试信息(还可以使用strip命令),命令如下:
objcopy --only-keep-debug foo foo.dbg
如果要添加调试信息文件链接,命令如下:
objcopy --add-gnu-debuglink=foo.dbg foo
确认调试信息文件链接,命令如下:
objdump -s -j .gnu_debuglink foo
调试release程序前首先需要从debug版本中获取符号表信息,这个是通过objcopy这个利器,命令如下:
objcopy --only-keep-debug debug info.dbg
其中debug是带有符号表的可执行程序,info.dbg是保存符号表的文件。
在使用gdb调试release程序的时候,使用--symbol命令来加载前面生成的符号表,命令如下:

[root@CentOS_192 debug]#gdb -q --symbol=info.dbg --exec=release
Reading symbols from /root/debug/info.dbg...done.
(gdb) start
Temporary breakpoint 1 at 0x4004ef: file test.c, line 11.
Starting program: /root/debug/release
Temporary breakpoint 1, main () at test.c:11
11 return func();
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.80.el6.x86_64
(gdb)
其中--exec选项指定的release就是release版本的程序,这个程序原本是没有符号表信息的。

使用perf生成Flame Graph(火焰图)

使用SystemTap脚本制作火焰图,内存较少时,分配存储采样的数组可能失败,需要编写脚本,还要安装kernel的debuginfo包。使用perf的话,相对来说要简单一些。不过在有kernel的debuginfo包的时候,采样显示的信息要更丰富一些。
为了使用perf制作火焰图方便,我编写了下面的脚本,贴上来备忘,也方便需要的人。
脚本如下:

if [ $# -ne 1 ];then
echo "Usage: $0 seconds"
exit 1
fi

perf record -a -g -o perf.data &

PID=`ps aux | grep "perf record" | grep -v grep | awk '{print $2}'`

if [ -n "$PID" ]; then
sleep $1
kill -s INT $PID
fi

# wait until perf exite
sleep 1

perf script -i perf.data &> perf.unfold
perl stackcollapse-perf.pl perf.unfold &> perf.folded
perl flamegraph.pl perf.folded >perf.svg
将上面的脚本保存为perf_flame_graph.sh。根据上面的Usage可以看出在执行的时候,只需要指定采样的时间,单位为秒。
下面是我在测试机上制作的火焰图

我这里制作的火焰图没有指定具体的进程或线程,如果只想关注某个具体的进程,可以在pref record命令后面加上命令或者通过-p选项指定进程ID。更多的参数和选项参见man perf-record。

2013年9月8日星期日

探究Linux下参数传递及查看和修改方法

   X86-64下有16个64位寄存器,其中%rdi、%rsi、%rdx,%rcx、%r8、%r9用作传递函数参数,分别对应第1个参数、第2个参数直到第6个参数,如下图所示(图片来自网络):

如果函数的参数个数超过6个,则超过的参数直接使用栈来传递。在被调用函数执行前,会先将寄存器中的参数压入堆栈,之后的访问会通过栈寄存器加上偏移位置来访问。下面我们结合程序及其反汇编的结果来看一看。C语言程序如下所示:
#include <stdio.h>
#include <stdlib.h>

static int func2(int i, int j)
{
int k;

k = i + j;
return k;
}

static int func(int fd, const char *ptr, int arg3, int arg4, int arg5,
int arg6, int arg7, int arg8)
{
int ret;

ret = arg7 + arg8;

func2(fd, arg3);

return ret;
}

int main(void)
{
func(12, "Hello,World!", 3, 4, 5, 6, 7, 8);

return 0;
}
将上述程序保存为m.c,使用gcc加上-g选项编译,然后使用gdb来进行调试,我们在main调用func的位置及func()和func2()函数三处加上断点,如下所示:

(gdb) b m.c:26
Breakpoint 1 at 0x4004d4: file m.c, line 26.
(gdb) b func
Breakpoint 2 at 0x4004ac: file m.c, line 17.
(gdb) b func2
Breakpoint 3 at 0x40047e: file m.c, line 8.
(gdb)
然后我们在第一个断点处停下,反汇编当前的main函数,查看参数传递方式,如下所示:
(gdb) disassemble /m main
Dump of assembler code for function main:
25 {
0x00000000004004cc <+0>: push %rbp
0x00000000004004cd <+1>: mov %rsp,%rbp
0x00000000004004d0 <+4>: sub $0x10,%rsp

26 func(12, "Hello,World!", 3, 4, 5, 6, 7, 8);
=> 0x00000000004004d4 <+8>: movl $0x8,0x8(%rsp)
0x00000000004004dc <+16>: movl $0x7,(%rsp)
0x00000000004004e3 <+23>: mov $0x6,%r9d
0x00000000004004e9 <+29>: mov $0x5,%r8d
0x00000000004004ef <+35>: mov $0x4,%ecx
0x00000000004004f4 <+40>: mov $0x3,%edx
0x00000000004004f9 <+45>: mov $0x400608,%esi
0x00000000004004fe <+50>: mov $0xc,%edi
0x0000000000400503 <+55>: callq 0x40048f <func>

27
28 return 0;
0x0000000000400508 <+60>: mov $0x0,%eax

29 }
0x000000000040050d <+65>: leaveq
0x000000000040050e <+66>: retq

End of assembler dump.
(gdb)
在func(12, "Hello,World!", 3, 4, 5, 6, 7, 8);这行下面我们可以看到在使用callq指令调用func()函数之前,会使用mov指令将前6个参数的值分别保存在edi、esi、edx、ecx、r8d、r9d这个6个寄存器中,而将第7个和第8个参数存储在栈上。这个结果和我们前面说的一致。
在进入第二个断点之前,我们先来看看当前的寄存器信息,如下所示:
(gdb) i registers
rax 0x351658ff60 228008197984
rbx 0x0 0
rcx 0x0 0
rdx 0x7fffffffe4e8 140737488348392
rsi 0x7fffffffe4d8 140737488348376
rdi 0x1 1
rbp 0x7fffffffe3f0 0x7fffffffe3f0
rsp 0x7fffffffe3e0 0x7fffffffe3e0
r8 0x351658e300 228008190720
r9 0x3515a0e9d0 227996133840
r10 0x7fffffffe240 140737488347712
r11 0x351621ebe0 228004588512
r12 0x400390 4195216
r13 0x7fffffffe4d0 140737488348368
r14 0x0 0
r15 0x0 0
rip 0x4004d4 0x4004d4 <main+8>
eflags 0x202 [ IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb)
我们看到,在第一个断点处,也就是main函数中调用func()的位置,此时调用函数func()所需要的参数还没有存储到寄存器中,此时是在sub $0x10,%rsp汇编指令之后的位置。
接下来我们进入第二个断点,即设置在func()函数的断点,查看此时的寄存器信息,如下所示:
(gdb) i registers
rax 0x351658ff60 228008197984
rbx 0x0 0
rcx 0x4 4
rdx 0x3 3
rsi 0x400608 4195848
rdi 0xc 12
rbp 0x7fffffffe3d0 0x7fffffffe3d0
rsp 0x7fffffffe3a0 0x7fffffffe3a0
r8 0x5 5
r9 0x6 6
r10 0x7fffffffe240 140737488347712
r11 0x351621ebe0 228004588512
r12 0x400390 4195216
r13 0x7fffffffe4d0 140737488348368
r14 0x0 0
r15 0x0 0
rip 0x4004ac 0x4004ac <func+29>
eflags 0x206 [ PF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb)
此时在rsi、rdi等寄存器中已经可以看到我们传递的参数值,我们此时的位置是在callq 0x40048f <func>之后的位置。
反汇编func()函数,如下所示:
(gdb) disassemble /m func
Dump of assembler code for function func:
14 {
0x000000000040048f <+0>: push %rbp
0x0000000000400490 <+1>: mov %rsp,%rbp
0x0000000000400493 <+4>: sub $0x30,%rsp
0x0000000000400497 <+8>: mov %edi,-0x14(%rbp)
0x000000000040049a <+11>: mov %rsi,-0x20(%rbp)
0x000000000040049e <+15>: mov %edx,-0x24(%rbp)
0x00000000004004a1 <+18>: mov %ecx,-0x28(%rbp)
0x00000000004004a4 <+21>: mov %r8d,-0x2c(%rbp)
0x00000000004004a8 <+25>: mov %r9d,-0x30(%rbp)

15 int ret;
16
17 ret = arg7 + arg8;
=> 0x00000000004004ac <+29>: mov 0x18(%rbp),%eax
0x00000000004004af <+32>: mov 0x10(%rbp),%edx
0x00000000004004b2 <+35>: lea (%rdx,%rax,1),%eax
0x00000000004004b5 <+38>: mov %eax,-0x4(%rbp)

18
19 func2(fd, arg3);
0x00000000004004b8 <+41>: mov -0x24(%rbp),%edx
0x00000000004004bb <+44>: mov -0x14(%rbp),%eax
0x00000000004004be <+47>: mov %edx,%esi
0x00000000004004c0 <+49>: mov %eax,%edi
0x00000000004004c2 <+51>: callq 0x400474 <func2>

20
21 return ret;
0x00000000004004c7 <+56>: mov -0x4(%rbp),%eax

22 }
0x00000000004004ca <+59>: leaveq
0x00000000004004cb <+60>: retq
从上面的汇编代码可以看到,在做具体的操作之前,会将寄存器中的参数值压入到栈上,并且在此后的操作中,都是只会去操作栈上的值,而不是直接修改寄存器中的值。
现在有一个问题,我们在第二个断点处,是在将寄存器压入栈之前还是之后?如果此时打印fd,是从栈上取值还是寄存器中取值?这个问题也很好判断,直接使用p命令打印fd即可,具体过程如下所示:
(gdb) p fd
$9 = 12
(gdb) p $rdi
$10 = 12
(gdb) set $rdi=15
(gdb) p fd
$11 = 12
(gdb) set *(int *)($rbp-0x14)=15
(gdb) p fd
$12 = 15
(gdb)
开始的时候修改的是rdi寄存器,但是打印fd时仍然是12,直接修改压栈的位置为15,再次打印fd,此时的值为15.所以在打印fd时,相应的值是从栈上读取的。我们此时的断点的位置也是在将参数压栈之后的位置。fd对应的栈位置可以算出来,不过这里是根据前面的反汇编结果。
接下来尝试利用栈上的信息打印出我们的第二个参数,也就是"Hello,World!"字符串。我们知道C语言中字符串其实就是一段以空字符结尾的内存,通常使用其首地址来访问该字符串。我们这里的字符串的地址通过esi寄存器保存在栈上,位置就是$rbp-0x20,但是这个位置存储的是一个指针的地址,如果将其理解为指针,就是存储指针的指针,也就是二级指针,所以的打印的时候应该是这样:
(gdb) p *(char **)($rbp-0x20)
$6 = 0x400608 "Hello,World!"
(gdb) p (char *)($rbp-0x20)
$7 = 0x7fffffffe3b0 "\b\006@"
(gdb)
其实前面修改变量的方法显得很罗嗦也很繁琐,但是平时如果要对正在运行的程序进行变量的修改,使用gdb则很麻烦,也不够灵活,即使使用gdb脚本。做这种事情,当然是由强大的SystemTap来做,要方便的多。下面还以修改fd为例,来说明如何使用SystemTap脚本来修改。
脚本如下:
probe process("a.out").statement("func@m.c+1") {
printf("func1: %s\n", $$vars);
$fd = 15;
}

probe process("a.out").statement("func2") {
printf("edi = %d\n", register("edi"));
}
上面的a.out就是前面的C程序(保存到m.c文件)编译后生成的。
这个脚本的执行和输出如下:
[root@CentOS_190 ~]# stap -gu -c ./a.out mod_reg.stp
func1: fd=0xc ptr=0x400608 arg3=0x3 arg4=0x4 arg5=0x5 arg6=0x6 arg7=0x7 arg8=0x8 ret=0x0
edi = 15
[root@CentOS_190 ~]#
我们可以看到在第一个probe点process("a.out").statement(func@m.c+1)
将fd设置为15,在第二个probe点通过edi寄存器看到函数func2()的第一个参数i的值为15,而不是12.
细心的同学可能发现在第一个probe点加上了相对函数的偏移,在第二个probe点中使用寄存器来查看参数的信息,而不是使用$i变量。我们通过下面的脚本来说明这个问题,脚本如下:
probe process("a.out").statement("func") {
printf("func1: %s\n", $$vars);
printf("func1: edi = %d\n", register("edi"));
}

probe process("a.out").statement("func2") {
printf("func2: %s\n", $$vars);
printf("func2: edi = %d\n", register("edi"));
}
其输出结果如下:
[root@CentOS_190 ~]# stap -gu -c ./a.out test.stp
func1: fd=0x0 ptr=0x7fffa0a4bff8 arg3=0x0 arg4=0x40036b arg5=0x0 arg6=0x0 arg7=0x7 arg8=0x8 ret=0x0
func1: edi = 12
func2: i=0x0 j=0x1 k=0x35
func2: edi = 12
[root@CentOS_190 ~]#
如果没有加上函数的偏移,在probe点触发时,fd、ptr等参数的值还没有初始化,也就是寄存器中的值还没有压入栈中,所以此时直接使用fd等变量获取的值是未定义的,此时即使修改了fd等变量,在将寄存器压栈的时候也会被覆盖为原来的值。结合我们前面用gdb看到的汇编代码,这个地方理解起来就容易多了。
我们知道在SystemTap脚本中可以嵌入C代码,在嵌入的C代码中也可以使用内联汇编,但是在操作寄存器的时候要注意,在probe点触发时,程序运行时的寄存器会被保存到栈上,所以在probe的处理中修改寄存器时,修改的只是当前的寄存器,在probe点的处理完成后会恢复程序运行时的寄存器,具体细节参见内核文档kprobes.txt。
前面的脚本中不仅实现了修改变量的功能,在程序debug信息少的情况下,也可以选择一些信息动态输出,避免了修改程序及重新编译的重复操作。
再次向大家强烈推荐SystemTap!

2013年9月5日星期四

解决接收不到组播包的问题

   目前用的集群是在应用层实现的,主要功能是实现在机器之间互转请求。今天在部署的时候,发现请求没有在节点之间互转,相同的请求发送一次后miss,第二次发送的时候还是miss。正常来说,第一次miss后会在集群内缓存一份,之后再有关于这个文件的请求不管发送到哪个机器都应该是hit的。
集群之间的探活用的是组播消息,出现这种问题肯定是因为接收组播报文出了问题。之前用的时候都没有问题,所以先从环境入手来查找问题。
先使用tcpdump抓包,看是否能够接收到组播报文。抓包的结果是,机器上接收到其他节点发送过来的组播报文。换了一台机器,结果也一样。现在是有数据包,下一步就是要找到数据包为什么被丢弃。之前遇到过一次是因为网关配置的不一致导致的。这次检查了几台机器,并且请运维的同事也帮忙看了一下,没有发现有啥问题。
接着在机器上安装了dropwatch,看看系统在哪些位置丢弃的数据包,结果如下图所示(这个图是在测试环境中重现问题后截的,结果是一样的):

从上图看来,比较靠谱的位置是在ip_rcv_finish()中丢包。ip_rcv_finish()中在查找路由缓存失败和数据包IP首部出错时才会丢包。数据包损坏的可能性不大,因此确定是在查找路由缓存失败丢的包。
后面使用"netstat -gn"命令来查看当前网卡上加入的组播组。用这个命令在机器上查看,发现加入的组播地址224.0.1.37绑定在eth0上,而本来要接收组播消息的fd绑定的IP地址是eth1上的地址。觉得应该是这里的问题。
《IP Multicast Extensions for 4.3BSD UNIX and related systems上看到,如果在加入组播组时,本地接口地址imr_interface设置的是INADDR_ANY时,选择默认的组播接口,也就是让内核来选择。根据现在的情况来看,内核在选择的时候会选择默认网关使用的设备,我这里使用的就是eth0。如果指定的接口地址的话,就会使用地址所在的网络接口作为组播组使用的网络接口。
现在基本可以确定丢包的原因了。两个机器的eth0和eth1网卡上设置的IP地址是不同网段的,eth0是9段的IP地址,eth1是4段的IP地址。发送组播消息时,使用的是4段的IP地址,所以接收组播消息的机器上数据包由eth1网卡来接收,但是加入组播组的网卡是eth0,所以数据包到达eth1时会查找路由失败,在ip_rcv_finish()中会将数据包丢弃。
找到问题原因,立即修改代码。在加入组播组时,将imr_interface设置为指定的本地IP地址。重新编译,启动后,用“netstat -gn”发现现在组播地址所在的设备和绑定的接口相同,测试没有问题。
为了验证上面的结论,写了一个systemtap脚本,如下所示(比较丑陋,没有封装成函数,海涵):

%{
#include <linux/skbuff.h>
#include <linux/netdevice.h>
#include <linux/ip.h>
%}

global kaddr=0x250100e0
global iph
global daddrs, saddrs

function ip_rcv_finish_helper:long(arg:long) %{
struct sk_buff *skb = (typeof(skb))THIS->arg;
const struct iphdr *iph = ip_hdr(skb);

THIS->__retvalue = (long)iph;
return;
%}

probe kernel.statement("ip_rcv@net/ipv4/ip_input.c+12") {

iph = ip_rcv_finish_helper($skb);
func = probefunc();

saddrs[func] = @cast(iph, "iphdr")->saddr;
daddrs[func] = @cast(iph, "iphdr")->daddr;

}

probe kernel.statement("ip_rcv_finish@net/ipv4/ip_input.c+11") {

iph = ip_rcv_finish_helper($skb);
func = probefunc();

saddrs[func] = @cast(iph, "iphdr")->saddr;
daddrs[func] = @cast(iph, "iphdr")->daddr;

if ((daddrs[func] == kaddr) && $err) {
printf("err = %d\n", $err);
}

}

probe kernel.statement("ip_rcv_finish@net/ipv4/ip_input.c+35") {

if (daddrs[func] == kaddr) {
printf("The result is unexpected\n");
exit();
}
}



probe kernel.function("ip_rcv").return {

func = probefunc();

if (daddrs[func] == kaddr) {
printf("Packet from 0x%X to 0x%X is droped in %s, return=%d\n",
saddrs[func], daddrs[func], func, $return);
}
}

probe kernel.function("ip_rcv_finish").return {

func = probefunc();

if (daddrs[func] == kaddr) {
printf("Packet from 0x%X to 0x%X is droped in %s, return=%d\n",
saddrs[func], daddrs[func], func, $return);
}
}
输出结果如下所示:

从上图可以看出来,ip_rcv()和ip_rcv_finish()的返回值都是1,即为NET_RX_DROP,表示要丢掉数据包。"ip_rcv_finish@net/ipv4/ip_input.c+35"这个probe点没有任何输出,也就是说获取路由缓存项失败。不过这个错误码比较意外是22,即EINVAL,看了ip_route_input()在获取组播报文的路由缓存项时确实是返回这个错误码。这个输出结果验证了前面的结论。