显示标签为“工具”的博文。显示所有博文
显示标签为“工具”的博文。显示所有博文

2013年12月25日星期三

Kprobes源码分析----kprobe的注册

    kprobes是一个动态地收集调试和性能信息的工具,使用它几乎可以跟踪任何函数或被执行的指令。它的机制也很简单,就是将被探测的位置的指令替换为断点指令(不考虑jmp优化),断点指令被执行后会通过notifier_call_chain机制来通知kprobes,kprobes会首先调用用户指定的pre_handler接口。执行pre_handler接口后会单步执行原始的指令,如果用户也指定了post_handler接口,会在调用post_handler接口后结束处理。
    基本的处理过程如下图所示:

    如果你没接触过kprobes,上面提到的pre_handler接口和post_handler接口可能让你疑惑,所以首先我们来介绍下和这些接口相关的kprobe结构。

    1.kprobe结构

    当你要探测一个函数时,需要首先分配一个kprobe实例,然后设置要探测的函数名称或地址,以及自己的处理函数。kprobe结构描述了探测点的位置以及要做的处理操作,其结构定义如下:
struct kprobe {
    struct hlist_node hlist;
    /* list of kprobes for multi-handler support */
    struct list_head list;
    /*count the number of times this probe was temporarily disarmed */
    unsigned long nmissed;
    /* location of the probe point */
    kprobe_opcode_t *addr;
    /* Allow user to indicate symbol name of the probe point */
    const char *symbol_name;
    /* Offset into the symbol */
    unsigned int offset;
    /* Called before addr is executed. */
    kprobe_pre_handler_t pre_handler;
    /* Called after addr is executed, unless... */
    kprobe_post_handler_t post_handler;
    /*
     * ... called if executing addr causes a fault (eg. page fault).
     * Return 1 if it handled fault, otherwise kernel will see it.
     */

    kprobe_fault_handler_t fault_handler;
    /*
     * ... called if breakpoint trap occurs in probe handler.
     * Return 1 if it handled break, otherwise kernel will see it.
     */

    kprobe_break_handler_t break_handler;
    /* Saved opcode (which has been replaced with breakpoint) */
    kprobe_opcode_t opcode;
    /* copy of the original instruction */
    struct arch_specific_insn ainsn;
    /*
     * Indicates various status flags.
     * Protected by kprobe_mutex after this kprobe is registered.
     */

    u32 flags;
};
    其成员说明如下:
    hlist:所有注册的kprobe都会添加到kprobe_table哈希表中,hlist成员用来链接到某个槽位中。
    list:如果在同一个位置注册了多个kprobe,这些kprobe会形成一个队列,队首是一个特殊的kprobe实例,list成员用来用来链接到这个队列中。当探测点被触发时,队首的kprobe实例中注册的handler会逐个遍历队列中注册的handler。
    nmissed:记录当前的probe没有被处理的次数。
    addr:这个成员有两个作用,一个是用户在注册前指定探测点的基地址(加上偏移得到真实的地址),另一个是在注册后保存探测点的实际地址。在注册前,这个可以不指定,由kprobes来初始化。如果没有指定,必须指定探测的位置的符号信息,例如函数名。
    symbol_name:探测点的符号名称。名称和地址不能同时指定,否则注册时会返回EINVAL错误。
    offset:探测点相对于addr地址的偏移
    pre_handler:这个接口在断点异常触发之后,开始单步执行原始的指令之前被调用
    post_handler:在单步执行原始的指令后会被调用
    fault_handler:如果执行过程中出错,则调用该接口。如果返回1,则表示错误由kprobes处理,否则由内核来处理。
    break_handler:在调用probe的处理函数(比如pre_handler接口)时触发了断点异常会调用该接口,。断点异常是通过中断门来处理的(参见trap_init()),在调用相应的处理函数前会自动关闭中断。关中断的情况下虽然不会接收可屏蔽的中断,但是CPU引发的异常或者NMI还是会接收到,所以有可能会发生断点异常处理嵌套,jprobes的实现就用到了这点。
    opcode:原始指令,在被替换为断点指令(X86下是int 3指令)前保存。
    ainsn:保存了探测点原始指令的拷贝。这里拷贝的指令要比opcode中存储的指令多,拷贝的大小为MAX_INSN_SIZE * sizeof(kprobe_opcode_t)。
    flags:探测点的标志,可取的值为KPROBE_FLAG_GONE和​KPROBE_FLAG_DISABLED。如果设置了KPROBE_FLAG_GONE标志,表示断点指令被移除;如果设置了 KPROBE_FLAG_DISABLED,则表示只注册probe,但是并不启用它,也就是说在断点异常触发时并不会调用该probe的接口。

    2.探测点的注册----register_kprobe()

    2.1 探测点地址的计算
register_kprobe()用来在指定的位置注册探测点。它会首先调用kprobe_addr()计算需要插入探测点的地址,源码如下所示:
int __kprobes register_kprobe(struct kprobe *p)
{
    int ret = 0;
    struct kprobe *old_p;
    struct module *probed_mod;
    kprobe_opcode_t *addr;

    addr = kprobe_addr(p);
    if (!addr)
        return -EINVAL;
    p->addr = addr;
    ......
}
    kprobe_addr()会首先检查用户是否设置探测点的地址,如果指定了地址,并且指定了符号信息(symbol_name成员也设置了),它会直接返回NULL,不会去检查符号信息和指定的地址是否一致。通过这里的代码不难发现,如果同时指定了符号信息和地址,register_kprobe()会返回EINVAL错误。
   如果没有指定探测点的地址,而是指定了符号信息,kprobe_addr()会调用kprobe_lookup_name()在内核符号表中查找符号对应的地址,在找到对应的符号地址后,加上偏移就得到探测点的实际位置。
  如果只指定了探测点的地址,kprobe_addr()会将这个地址直接加上偏移返回。
  kprobe_addr()返回的地址会被设置到kprobe的addr成员中,注册后通过这个成员就可以拿到被探测位置的地址。利用这个特性,你可以通过kprobe来获取内核中某一个函数的运行时地址。假设你在编写一个内核模块,但是你要使用函数内核没有导出,这个特性就很有用了。
    2.2 检查探测点地址
   计算探测点的地址后,接着就是要检查这个地址是否可以被探测,源码如下所示:
    ......
    preempt_disable();
    if (!kernel_text_address((unsigned long) p->addr) ||
        in_kprobes_functions((unsigned long) p->addr)) {
        preempt_enable();
        return -EINVAL;
    }

    /* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */
    p->flags &= KPROBE_FLAG_DISABLED;

    /*
     * Check if are we probing a module.
     */

    probed_mod = __module_text_address((unsigned long) p->addr);
    if (probed_mod) {
        /*
         * We must hold a refcount of the probed module while updating
         * its code to prohibit unexpected unloading.
         */

        if (unlikely(!try_module_get(probed_mod))) {
            preempt_enable();
            return -EINVAL;
        }
        /*
         * If the module freed .init.text, we couldn't insert
         * kprobes in there.
         */

        if (within_module_init((unsigned long)p->addr, probed_mod) &&
            probed_mod->state != MODULE_STATE_COMING) {
            module_put(probed_mod);
            preempt_enable();
            return -EINVAL;
        }
    }
    preempt_enable();
    ......
    kprobes只能用作内核函数的探测,所以在注册前必须检查探测点的地址是否是在内核地址空间中。探测点的地址要么是在内核映像中(_stext和_etext之间,如果是在系统启动阶段,要在_sinittext和_einittext之间),要么是是在某个内核模块的地址空间中。具体的判断是在kernel_text_address()函数中处理的。
    实现kprobes的内核函数或者一些特殊的函数是不能探测的,所以还要进一步检测探测点的地址是否在这些限制空间内,这是由in_kprobes_functions()函数来处理的。kprobes把所有不能探测的函数都放在.kprobes.text section(使用__kprobes宏)中,这个段的起始地址是__kprobes_text_start和__kprobes_text_end。如果探测的地址在__kprobes_text_start和__kprobes_text_end之间,会返回EINVAL错误。某些函数也是不能探测的,但是已经放在其他section了,这些函数会放在kprobe_blacklist数组中。如果用户探测的函数在这个黑名单中,也会返回EINVAL错误。2.6.32中只有preempt_schedule()函数在kprobe_blacklist数组中,3.12中增加了native_get_debugreg()、irq_entries_start()、common_interrupt()、mcount()。
    如果探测点的地址是在一个内核模块中,需要增加对该模块的引用,以免模块提前卸载。但是如果模块已经开始卸载,此时也是不能注册探测点的,同样会返回EINVAL错误。模块中.init.text section占用的内存,在模块加载后,如果不再使用是可以被释放掉的。如果探测点在模块的.init.text section中,而该section已经被释放,此时是不能注册探测点的,同样会返回EINVAL错误。
    前面说了一大堆会返回EINVAL错误的情况,是时候要总结一下了。在以下情况中,会注册失败,返回EINVAL错误:
      1)    函数被__kprobes宏修饰的函数,这些函数会被放在.kprobes.text section中。这种类型大多数是实现kprobes的代码
      2)    kprobe_blacklist数组中列出的函数。2.6.32中只有preempt_schedule()函数,最新的3.12内核中又增加了native_get_debugreg()、irq_entries_start()、    
             common_interrupt()、mcount()。
      3)    要探测的地址位于一个模块的.init.text section,但是这个section已经被释放。
2.3 注册kprobe
    经过前面的处理后,可以开始执行真正的注册操作了,源码如下所示:
    p->nmissed = 0;
    INIT_LIST_HEAD(&p->list);
    mutex_lock(&kprobe_mutex);
    old_p = get_kprobe(p->addr);
    if (old_p) {
        ret = register_aggr_kprobe(old_p, p);
        goto out;
    }

    mutex_lock(&text_mutex);
    ret = arch_prepare_kprobe(p);
    if (ret)
        goto out_unlock_text;

    INIT_HLIST_NODE(&p->hlist);
    hlist_add_head_rcu(&p->hlist,
               &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);

    if (!kprobes_all_disarmed && !kprobe_disabled(p))
        arch_arm_kprobe(p);

out_unlock_text:
    mutex_unlock(&text_mutex);
out:
    mutex_unlock(&kprobe_mutex);
    在初始化kprobe的相关成员后,首先要获取kprobe_mutex互斥锁,这个互斥锁用来保护kprobe_table哈希表,系统中所有已注册的kprobe实例都保存在这个哈希表中。
    kprobes允许在同一个探测点注册多个kprobe,如果调用get_kprobe()能找到一个kprobe实例,说明已经在当前的探测点注册了一个kprobe,这种情况下会调用register_aggr_kprobe()来处理。如果已注册的kprobe只有一个,register_aggr_kprobe()会分配一个新的kprobe实例,将已注册的kprobe中的内容拷贝到新分配的kprobe中,然后把已注册的kprobe的hanlder设置为kprobes提供的特殊的hanlder,这些handler在异常触发时会逐个遍历用户注册的kprobe中的handler。接着会将新分配的kprobe(不是当前调用register_kprobe()注册的那个)链接到前面已经注册的kprobe,形成一个队列。之后如果还有新的kprobe注册到当前的探测点,会直接添加到队列的末尾。注意,前面描述的register_aggr_kprobe()过程中还没有操作当前要注册的kprobe实例,在前面的操作都做完之后,就直接把新注册的kprobe实例添加到队尾,然后返回。这个过程中结构的变化如下图所示:

    如果在当前探测点没有注册过kprobe,则调用arch_prepare_kprobe()将被探测位置的指令保存到kprobe结构的ainsn成员中,并且被探测位置的第一条指令保存到opcode成员中。
    接着就可以将krobe实例添加到kprobe_table哈希表中,这个操作必须在将探测点的指令替换成断点指令前完成,否则有可能在注册完成前断点异常已经发生了。
    经过前面的处理,现在就剩下最后一步了,将探测点的指令替换为断点指令。这是通过调用text_poke()来完成,它可以将替换指定位置的指令。不过在调用text_poke()替换指令前,必须要获取text_mutex互斥锁。text_poke()真是太好用了,可以做很多事情.......
  至此,整个注册过程就完成了。

2013年10月30日星期三

解决mv命令导致的cman启动失败问题

解决mv命令导致的cman启动失败问题   参考资料:https://bugzilla.redhat.com/show_bug.cgi?format=multiple&id=667703
                    http://www.qingsword.com/qing/1086.html
  今天使用cman来管理集群,在启动时报错,错误信息如下:
[root@CentOS____102 ~]# service cman start
Starting cluster: 
   Checking if cluster has been disabled at boot...        [  OK  ]
   Checking Network Manager...                             [  OK  ]
   Global setup...                                         [  OK  ]
   Loading kernel modules...                               [  OK  ]
   Mounting configfs...                                    [  OK  ]
   Starting cman... I/O warning : failed to load external entity "/etc/cluster/cluster.conf"

Unable to get the configuration
I/O warning : failed to load external entity "/etc/cluster/cluster.conf"
corosync [MAIN  ] Corosync Cluster Engine ('1.4.1'): started and ready to provide service.
corosync [MAIN  ] Corosync built-in features: nss dbus rdma snmp
corosync [MAIN  ] Unable to read config from /etc/cluster/cluster.conf
corosync [MAIN  ] Corosync Cluster Engine exiting with status 8 at main.c:1695.
corosync died: Could not read cluster configuration Check cluster logs for details
                                                           [FAILED]
Stopping cluster: 
   Leaving fence domain...                                 [  OK  ]
   Stopping gfs_controld...                                [  OK  ]
   Stopping dlm_controld...                                [  OK  ]
   Stopping fenced...                                      [  OK  ]
   Stopping cman...                                        [  OK  ]
   Unloading kernel modules...                             [  OK  ]
   Unmounting configfs...                                  [  OK  ]
[root@CentOS____102 ~]#
  产生问题的原因是corosync无法读取配置文件/etc/cluster/cluster.conf,文件属性如下所示:
[root@CentOS____102 ~]# ls -l /etc/cluster/cluster.conf 
-rw-r--r--. 1 root root 995 Oct 30 16:30 /etc/cluster/cluster.conf
  这个文件权限在机器A(192.168.56.101)是没有问题的,出问题的是机器B(192.168.56.102)。
  机器B上的文件是使用scp命令从机器A上先拷贝到机器B的/root目录下,然后使用mv命令移到/etc/cluster目录的。之所以出现这样的问题,是因为在机器B上文件是/root目录下创建的,在使用mv命令移动这个文件的时候会加上admin_home_t label,如下所示:
 [root@CentOS____102 ~]# ls -Z /etc/cluster/cluster.conf 
-rw-r--r--. root root unconfined_u:object_r:admin_home_t:s0 /etc/cluster/cluster.conf
  因为这个label的存在,SELinux会阻止corosync读取这个文件,admin_home_t表示这个文件是root用户目录下的文件。之所以打不开是因为cluster.conf文件的SELinux配置信息是继承原来那个目录的,与/etc/cluster目录不同。查看SELinux的日志信息(日志文件位置为/var/log/audit/audit.log),可以看到下面的内容:
22109 type=AVC msg=audit(1383121897.046:28821): avc:  denied  { getattr } for  pid=2645 comm="corosync" path="/etc/cluster/cluster.conf" dev=sda2 ino=1311842 scontext=unconfined_u:system_r:corosync_t:s0 tcontext=unconfined_u:object_r:admin_home_t:s0 tclass=file
  正确的SELinux配置信息是scontext对应的内容,它描述了corosync进程的SELinux上下文,其中unconfined_u表示的是SELinux用户,这里的就是没有限制特定的用户。system_r表示corosync是一个进程,corosync_t是它的类型(或者叫域),每个进程或文件都有一个类型。cluster.conf的SELinux上下文由tcontext描述,第一个字段表示没有限制特定的用户,第二个字段表示她是一个目录或文件,第三个字段则是它的类型,这个类型只有root用户才可以访问。
  现在知道了问题原因,要解决这个问题见就很简单了。最容易想到的方法就是关闭SELinux。如果不想关闭SELinux,可以使用下面的命令恢复原来的文件标签,如下所示:
[root@CentOS____102 ~]# restorecon -v /etc/cluster/cluster.conf 
restorecon reset /etc/cluster/cluster.conf context unconfined_u:object_r:admin_home_t:s0->unconfined_u:object_r:cluster_conf_t:s0
  恢复后再使用ls -Z命令查看文件的信息,和机器A上的一样。修改后重新启动cman服务正常。

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()函数的调用就不会产生段错误了。将生成的可执行程序反汇编,和使用前面的方法生成可执行程序的反汇编结果比较,发现除了函数名不一样外,没有其他区别,不知道为什么会产生段错误。知道的麻烦告知一声,拜谢!

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()在获取组播报文的路由缓存项时确实是返回这个错误码。这个输出结果验证了前面的结论。

2013年8月25日星期日

Linux中查看是否是固态硬盘(SSD)

   最近在准备测试,需要看看哪些机器挂载的是ssd硬盘,Google了一圈看到了许多方法,但都云里雾里的,不知道怎么确定。ssd硬盘貌似使用的也是scsi接口,所以根据盘符的名称也是判断不出来的。最后群里eric大神告知lsscsi工具,试了一下,非常简单,显示的也很直接,分享一下。
废话不多说,直接上图和结果,如下所示:

[root@FWD_YF_009_110 ~]# lsscsi
[0:0:0:0] disk SEAGATE ST3300657SS ES62 -
[0:0:1:0] disk ATA INTEL SSDSA2CW16 0362 /dev/sda
[0:0:2:0] disk ATA INTEL SSDSA2CW16 0362 /dev/sdb
[0:0:3:0] disk ATA INTEL SSDSA2CW16 0362 /dev/sdc
[0:1:0:0] disk Dell VIRTUAL DISK 1028 /dev/sdd
[3:0:0:0] cd/dvd TEAC DVD-ROM DV-28SW R.2A /dev/sr0
看第四列就知道是否是SSD硬盘了,感兴趣的可以试下