Archive for the ‘Linux’ category

内核函数copy_process()分析

January 11th, 2016

内核通过调用函数copy_process()创建进程,copy_process()函数主要用来创建子进程的描述符以及与子进程相关数据结构。这个函数内部实现较为复杂,在短时间内,对于内部详细代码原理和实现并不能全部理解。因此,接下来的分析侧重于copy_process()的执行流程。

1. 定义返回值变量和新的进程描述符。

2.  对clone_flags所传递的标志组合进行合法性检查。当出现以下四情况时,返回出错代号:

  • CLONE_NEWNS和CLONE_FS同时被设置。前者标志表示子进程需要自己的命名空间,而后者标志则代表子进程共享父进程的根目录和当前工作目录,两者不可兼容。
  • CLONE_NEWUSER和CLONE_FS同时被设置。
  • CLONE_THREAD被设置,但CLONE_SIGHAND未被设置。如果子进程和父进程属于同一个线程组(CLONE_THREAD被设置),那么子进程必须共享父进程的信号(CLONE_SIGHAND被设置)。
  • CLONE_SIGHAND被设置,但CLONE_VM未被设置。如果子进程共享父进程的信号,那么必须同时共享父进程的内存描述符和所有的页表(CLONE_VM被设置)。

3. 调用security_task_create()和后面的security_task_alloc()执行所有附加的安全性检查。

4. 调用dup_task_struct()为子进程分配一个内核栈、thread_info结构和task_struct结构。

 p = dup_task_struct(current);
        if (!p)
               goto fork_out;

这个dup_task_struct函数首先定义创建了指向task_struct和thread_inof结构体的指针。然后让子进程描述符中的thread_info字段指向ti变量;最后让子进程thread_info结构中的task字段指向tsk变量。然后返回tsk,这个时候子进程和父进程的描述符中的内容是完全相同的。在后面的代码中,我们将会看到子进程逐渐与父进程区分开。

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
        struct task_struct *tsk;
        struct thread_info *ti;
        int node = tsk_fork_get_node(orig);
        int err;
 
        tsk = alloc_task_struct_node(node);
        if (!tsk)
                return NULL;

        ti = alloc_thread_info_node(tsk, node);
        if (!ti)
                goto free_tsk;

        err = arch_dup_task_struct(tsk, orig);
        if (err)
                goto free_ti;
        tsk->stack = ti;

5. 开始设置子进程的task_struct

根据clone_flags的值继续更新子进程的某些属性。将 nr_threads++,表明新进程已经被加入到进程集合中。将total_forks++,以记录被创建进程数量。

这部分工作还包含初始化双链表、互斥锁和描述进程属性的字段等,其中包括大量的与cgroup相关的属性,。它在copy_process函数中占据了相当长的一段的代码,不过考虑到task_struct结构本身的复杂性,也就不奇怪了。

如果上述过程中某一步出现了错误,则通过goto语句跳到相应的错误代码处;如果成功执行完毕,则返回子进程的描述符p。do_fork()执行完毕后,虽然子进程处于可运行状态,但是它并没有立刻运行。至于子进程合适执行这完全取决于调度程序schedule()。

 

http://lxr.free-electrons.com/source/kernel/fork.c#L1242

fork系统调用分析

January 9th, 2016

0 前言

在Linux中,主要是通过fork的方式产生新的进程,我们都知道每个进程都在 内核对应一个PCB块,内核通过对PCB块的操作做到对进程的管理。在Linux内核中,PCB对应着的结构体就是task_struct,也就是所谓的进程描述符(process descriptor)。该数据结构中包含了程相关的所有信息,比如包含众多描述进程属性的字段,以及指向其他与进程相关的结构体的指针。因此,进程描述符内部是比较复杂的。这个结构体的声明位于include/linux/sched.h中。

task_struct中有指向mm_struct结构体的指针mm,也有指向fs_struct结构体的指针fs,这个结构体是对进程当前所在目录的描述;也有指向files_struct结构体的指针files,这个结构体是对该进程已打开的所有文件进行描述。这里我们要注意进程在运行期间中可能处于不同的进程状态,例如:TASK_RUNNING/TASK_STOPPED/TASK_TRACED 等

1 fork调用

在用户态下,使用fork()创建一个进程。除了这个函数,新进程的诞生还可以分别通过vfork()和clone()。fork、vfork和clone三个API函数均由glibc库提供,它们分别在C库中封装了与其同名的系统调用fork()。这几个函数调用对应不同场景,有些时候子进程需要拷贝父进程的整个地址空间,但是子进程创建后又立马去执行exec族函数造成效率低下。

  • 写时拷贝满足了这种需求,同时减少了地址空间复制带来的问题。
  • vfork 则是创建的子进程会完全共享父进程的地址空间,甚至是父进程的页表项,父子进程任意一方对任何数据的修改使得另一方都可以感知到。
  • clone函数创建子进程时灵活度比较大,因为它可以通过传递不同的参数来选择性的复制父进程的资源

系统调用fork、vfork和clone在内核中对应的服务例程分别为sys_fork(),sys_vfork()和sys_clone()。例如sys_fork()声明如下(arch/x86/kernel/process.c):

int sys_fork(struct pt_regs *regs)
{
        return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}
int sys_vfork(struct pt_regs *regs)
{
        return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->sp, regs, 0,
                       NULL, NULL);
}
sys_clone(unsigned long clone_flags, unsigned long newsp,
          void __user *parent_tid, void __user *child_tid, struct pt_regs *regs)
{
        if (!newsp)
                newsp = regs->sp;
        return do_fork(clone_flags, newsp, regs, 0, parent_tid, child_tid);
}

可以看到do_fork()均被上述三个服务函数调用。do_fork()正是kernel创建进程的核心()。通过分析调用过程如下,其中我分析的是最新版4.X Linux源码,在i386体系结构中,采取0x80中断调用syscall:

fork

 

从图中可以看到do_fork()和copy_process()是本文的主要分析对象。

do_fork函数的主要就是复制原来的进程成为另一个新的进程,在一开始,该函数定义了一个task_struct类型的指针p,用来接收即将为新进程(子进程)所分配的进程描述符。但是这个时候要检查clone_flags是否被跟踪就是ptrace,ptrace是用来标示一个进程是否被另外一个进程所跟踪。所谓跟踪,最常见的例子就是处于调试状态下的进程被debugger进程所跟踪。ptrace字段非0时说明debugger程序正在跟踪父进程,那么接下来通过fork_traceflag函数来检测子进程是否也要被跟踪。如果trace为1,那么就将跟踪标志CLONE_PTRACE加入标志变量clone_flags中。没有的话才可以进程创建,也就是copy_process()。

long _do_fork(unsigned long clone_flags,
              unsigned long stack_start,
              unsigned long stack_size,
              int __user *parent_tidptr,
              int __user *child_tidptr,
              unsigned long tls)
{
        struct task_struct *p;
        int trace = 0;
        long nr;
        if (!(clone_flags & CLONE_UNTRACED)) {
                if (clone_flags & CLONE_VFORK)
                        trace = PTRACE_EVENT_VFORK;
                else if ((clone_flags & CSIGNAL) != SIGCHLD)
                        trace = PTRACE_EVENT_CLONE;
                else
                        trace = PTRACE_EVENT_FORK;
                if (likely(!ptrace_event_enabled(current, trace)))
                        trace = 0;
        }

这条语句要做的是整个创建过程中最核心的工作:通过copy_process()创建子进程的描述符,并创建子进程执行时所需的其他数据结构,最终则会返回这个创建好的进程描述符。因为copy_process()函数过于巨大,所以另外开辟一篇文章讲解该函数实现。

 p = copy_process(clone_flags, stack_start, stack_size,
                        child_tidptr, NULL, trace, tls);

如果copy_process函数执行成功,那么将继续下面的代码。定义了一个完成量vfork,之后再对vfork完成量进行初始化。如果使用vfork系统调用来创建子进程,那么必然是子进程先执行。原因就是此处vfork完成量所起到的作用:当子进程调用exec函数或退出时就向父进程发出信号。此时,父进程才会被唤醒;否则一直等待。

 if (!IS_ERR(p)) {
                struct completion vfork;
                struct pid *pid;

                trace_sched_process_fork(current, p);

                pid = get_task_pid(p, PIDTYPE_PID);
                nr = pid_vnr(pid);
                if (clone_flags & CLONE_PARENT_SETTID)
                     put_user(nr, parent_tidptr);
                if (clone_flags & CLONE_VFORK) {
                     p->vfork_done = &vfork;
                        init_completion(&vfork);
                        get_task_struct(p);
                }

下面通过wake_up_new_task函数使得父子进程之一优先运行;如果设置了ptrace,那么需要告诉跟踪器。如果CLONE_VFORK标志被设置,则通过wait操作将父进程阻塞,直至子进程调用exec函数或者退出。

                wake_up_new_task(p);

                /* forking complete and child started to run, tell ptracer */
                if (unlikely(trace))
                        ptrace_event_pid(trace, pid);
                if (clone_flags & CLONE_VFORK) {
                    if (!wait_for_vfork_done(p, &vfork))
                               ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
                }
                put_pid(pid);

如果copy_process()在执行的时候发生错误,则先释放已分配的pid;再根据PTR_ERR()的返回值得到错误代码,保存于pid中。 返回pid。这也就是为什么使用fork系统调用时父进程会返回子进程pid的原因。

        } else {
                nr = PTR_ERR(p);
        }
        return nr;
}

参考:

http://cs.lmu.edu/~ray/notes/linuxsyscalls/

http://www.x86-64.org/documentation/abi.pdf

http://www.tldp.org/LDP/tlk/ds/ds.html

再议kprobe机制

December 9th, 2015

在之前的博文《SystemTap Kprobe原理》中简要的对kprobe进行了介绍,Kprobe机制是内核提供的一种调试机制,它提供了一种方法,能够在不修改现有代码的基础上,灵活的跟踪内核函数的执行。

kprobe工作原理是:
1)在注册探测点的时候,将被探测函数的指令码替换为int 3的断点指令;
2)在执行int 3的异常执行中,CPU寄存器的内容会被保存,通过通知链的方式调用kprobe的异常处理函数;
3)在kprobe的异常处理函数中,首先判断是否存在pre_handler钩子,存在钩子则执行pre_handler;
4)进入单步调试,通过设置EFLAGS中的TF标志位,并且把异常返回的地址修改为保存的原指令码;
5)代码返回,执行原有指令,执行结束后触发单步异常;
6)在单步异常的处理中,清除标志位,执行post_handler流程,并最终返回执行正常流程;

Kprobe提供了三种形式的探测点,一种是最基本的kprobe,能够在指定代码执行前、执行后进行探测,但此时不能访问被探测函数内的相关变量信息;一种是jprobe,用于探测某一函数的入口,并且能够访问对应的函数参数;一种是kretprobe,用于完成指定函数返回值的探测功能。而jprobe与kretprobe都是基于kprobe实现的。

下面对kprobe代码简要分析:

在init_kprobes()初始化函数中,将kprobe注册到kprobe_exceptions_notify通知链

static int __init init_kprobes(void)
{
     ...
     err = arch_init_kprobes();
     if (!err)
        err = register_die_notifier(&kprobe_exceptions_nb);
     if (!err)
        err = register_module_notifier(&kprobe_module_nb);

     kprobes_initialized = (err == 0);

     if (!err)
       init_test_probes();
     return err;
}
static struct notifier_block kprobe_exceptions_nb = {
    .notifier_call = kprobe_exceptions_notify,
    .priority = 0x7fffffff /* we need to be notified first */
};

int __kprobes register_kprobe(struct kprobe *p)函数则是我们在内核模块中所调用的,经过对调用点一系列的检查,最后将将kprobe加入到相应的hash表内,并将将探测点的指令码修改为int 3指令: __arm_kprobe(p);

int __kprobes register_kprobe(struct kprobe *p) {
...
 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))
     __arm_kprobe(p);
...

当内核调用到这个探测点时,触发int 3,经过中断处理,调用到do_int3() 函数,如果我们使能了CONFIG_KPROBES选项,那么跳入kprobe_int3_handler执行,该函数为kprobe的核心函数。

dotraplinkage void notrace do_int3(struct pt_regs *regs, long error_code)
{
...
#ifdef CONFIG_KPROBES
         if (kprobe_int3_handler(regs))
                 goto exit;
#endif
         if (notify_die(DIE_INT3, "int3", regs, error_code, X86_TRAP_BP,
                         SIGTRAP) == NOTIFY_STOP)
                 goto exit;
...
 exit:
         ist_exit(regs);
}
static int __kprobes kprobe_handler(struct pt_regs *regs)
{
...
    addr = (kprobe_opcode_t *)(regs->ip - sizeof(kprobe_opcode_t));
//对于int 3中断,那么异常发生时EIP寄存器内指向的为异常指令的后一条指令
    preempt_disable();

    kcb = get_kprobe_ctlblk();
    /*获取addr对应的kprobe*/
    p = get_kprobe(addr);
    if (p) {
//如果异常的进入是由kprobe导致,则进入reenter_kprobe
        if (kprobe_running()) {
            if (reenter_kprobe(p, regs, kcb))
                return 1;
        } else {
            set_current_kprobe(p, regs, kcb);
            kcb->kprobe_status = KPROBE_HIT_ACTIVE;

            /*
             * If we have no pre-handler or it returned 0, we
             * continue with normal processing. If we have a
             * pre-handler and it returned non-zero, it prepped
             * for calling the break_handler below on re-entry
             * for jprobe processing, so get out doing nothing
             * more here.
             */
//执行在此地址上挂载的pre_handle函数
            if (!p->pre_handler || !p->pre_handler(p, regs))
//设置单步调试模式,为post_handle函数的执行做准备
                setup_singlestep(p, regs, kcb, 0);
            return 1;
        }
    } else if (*addr != BREAKPOINT_INSTRUCTION) {
...
    } else if (kprobe_running()) {
...
    } /* else: not a kprobe fault; let the kernel handle it */

    preempt_enable_no_resched();
    return 0;
}

setup_singlestep() 函数为单步调试函数,在该函数内会打开EFLAGS的TF标志位,清除IF标志位(禁止中断),并设置异常返回的指令为保存的被探测点的指令。

static void __kprobes setup_singlestep(struct kprobe *p, struct pt_regs *regs,
                 struct kprobe_ctlblk *kcb, int reenter)
{
    if (setup_detour_execution(p, regs, reenter))
        return;
...
    /*jprobe*/
    if (reenter) {
        save_previous_kprobe(kcb);
        set_current_kprobe(p, regs, kcb);
        kcb->kprobe_status = KPROBE_REENTER;
    } else
        kcb->kprobe_status = KPROBE_HIT_SS;
    /* Prepare real single stepping */
    /*准备单步模式,设置EFLAGS的TF标志位,清除IF标志位(禁止中断)*/
    clear_btf();
    regs->flags |= X86_EFLAGS_TF;
    regs->flags &= ~X86_EFLAGS_IF;
    /* single step inline if the instruction is an int3 */
    if (p->opcode == BREAKPOINT_INSTRUCTION)
        regs->ip = (unsigned long)p->addr;
    else
	/*设置异常返回的指令为保存的被探测点的指令*/
        regs->ip = (unsigned long)p->ainsn.insn;
}

setup_singlestep() 执行完毕后,程序继续执行保存的被探测点的指令,由于开启了单步调试模式,执行完指令后会继续触发异常,这次的是do_debug异常处理流程。然后在kprobe_debug_handler恢复现场,清除TF标志位等操作,完成kprobe调用。

dotraplinkage void do_debug(struct pt_regs *regs, long error_code)
{
...
#ifdef CONFIG_KPROBES
         if (kprobe_debug_handler(regs))
                 goto exit;
#endif
...
}
int kprobe_debug_handler(struct pt_regs *regs)
{
...
         resume_execution(cur, regs, kcb);
         regs->flags |= kcb->kprobe_saved_flags;

         if ((kcb->kprobe_status != KPROBE_REENTER) && cur->post_handler) {
                 kcb->kprobe_status = KPROBE_HIT_SSDONE;
                 cur->post_handler(cur, regs, 0);
         }

         /* Restore back the original saved kprobes variables and continue. */
         if (kcb->kprobe_status == KPROBE_REENTER) {
                 restore_previous_kprobe(kcb);
                 goto out;
         }
         reset_current_kprobe();
...
}

 总结一下kprobe流程就是下图:

Unnamed QQ Screenshot20131218210006

参考

https://lwn.net/Articles/132196/
http://www.lxway.com/82244406.htm
http://blog.chinaunix.net/uid-22227409-id-3420260.html

再议PLT与GOT

December 8th, 2015

PLT(Procedure Linkage Table)的作用是将位置无关的符号转移到绝对地址。当一个外部符号被调用时,PLT 去引用 GOT 中的其符号对应的绝对地址,然后转入并执行。

GOT(Global Offset Table):用于记录在 ELF 文件中所用到的共享库中符号的绝对地址。在程序刚开始运行时,GOT 表项是空的,当符号第一次被调用时会动态解析符号的绝对地址然后转去执行,并将被解析符号的绝对地址记录在 GOT 中,第二次调用同一符号时,由于 GOT 中已经记录了其绝对地址,直接转去执行即可(不用重新解析)。

其中PLT 对应.plt section ,而GOT对应 .got.plt ,主要对应函数的绝对地址。通过readelf查看可执行文件,我们发现还存在.got section,这个section主要对应动态链接库中变量的绝对地址。我们还要注意PLT section在代码链接的时候已经存在,存在于代码段中,而GOT存在于数据段中。

在我先前的博文ELF文件的加载,我简单的对PLT和GOT进行了介绍。这里我们增加复杂度,重新对这个动态链接机制进行分析。

foo.c:

#include <stdio.h>

void foo(int i)
{
     printf("Test %d\n",i);
}

main.c:

#include <stdio.h>
#include <unistd.h>

extern void foo(int i);

int main()
{
     printf("Test 1\n");
     printf("Test 2\n");
     foo(3);
     foo(4);
     return 0;
}

然后我们将foo.c 编译成为liba.so: gcc -shared -fPIC -g3 -o libfoo.so foo.c
下面编译主main函数:gcc -Wall -g3 -o main main.c -lfoo -L /tmp/libfoo.so

下面我们对这个main进行调试,我们在printf与foo上都打上断点,然后开始运行,调试

(gdb) disassemble
Dump of assembler code for function main:
   0x00000000004006e6 <+0>:	push   %rbp
   0x00000000004006e7 <+1>:	mov    %rsp,%rbp
=> 0x00000000004006ea <+4>:	mov    $0x4007a0,%edi
   0x00000000004006ef <+9>:	callq  0x4005b0 <puts@plt>
   0x00000000004006f4 <+14>:	mov    $0x4007a0,%edi
   0x00000000004006f9 <+19>:	callq  0x4005b0 <puts@plt>
   0x00000000004006fe <+24>:	callq  0x4005e0 <foo@plt>
   0x0000000000400703 <+29>:	callq  0x4005e0 <foo@plt>
   0x0000000000400708 <+34>:	mov    $0x0,%eax
   0x000000000040070d <+39>:	pop    %rbp
   0x000000000040070e <+40>:	retq
End of assembler dump.
...
(gdb) si
0x00000000004005b0 in puts@plt ()

这里我们进入了.plt 中寻找puts函数,查看0x601018中的值,我们发现地址0x004005b6,而0x004005b6正是jmpq的下一条指令,执行完跳如GOT表中查找函数绝对地址。这样做避免了GOT表表是否为真实值检查,如果为空那么寻址,否则直接调用。

(gdb) disassemble
Dump of assembler code for function puts@plt:
=> 0x00000000004005b0 <+0>:	jmpq   *0x200a62(%rip)        # 0x601018 <[email protected]>
   0x00000000004005b6 <+6>:	pushq  $0x0
   0x00000000004005bb <+11>:	jmpq   0x4005a0
End of assembler dump.
(gdb) x/32x 0x601018
0x601018 <[email protected]>:	0x004005b6	0x00000000	0x25e20610	0x0000003e
0x601028 <[email protected]>:	0x004005d6	0x00000000	0x004005e6	0x00000000
...
End of assembler dump.
(gdb) si
0x00000000004005a0 in ?? ()
(gdb)
0x00000000004005a6 in ?? ()
(gdb)
0x0000003e25615b70 in _dl_runtime_resolve () from /lib64/ld-linux-x86-64.so.2
(gdb) disassemble
Dump of assembler code for function _dl_runtime_resolve:
=> 0x0000003e25615b70 <+0>:	sub    $0x78,%rsp
   0x0000003e25615b74 <+4>:	mov    %rax,0x40(%rsp)
...
   0x0000003e25615b9c <+44>:	bndmov %bnd1,0x10(%rsp)
   0x0000003e25615ba2 <+50>:	bndmov %bnd2,0x20(%rsp)
   0x0000003e25615ba8 <+56>:	bndmov %bnd3,0x30(%rsp)
   0x0000003e25615bae <+62>:	mov    0x8i0(%rsp),%rsi
   0x0000003e25615bb6 <+70>:	mov    0x78(%rsp),%rd
   0x0000003e25615bbb <+75>:	callq  0x3e2560e990 <_dl_fixup>
...

这个_dl_runtime_resolve 来自于ld-linux-x86-64.so.2文件,然后在ld中调用_dl_fixup 将真实的puts函数地址填入GOT表中,当程序再次调入puts函数中时,直接jmpq跳转到0x25e6fa70地址执行。

(gdb) disassemble 
Dump of assembler code for function puts@plt:
=> 0x00000000004005b0 <+0>:	jmpq   *0x200a62(%rip)        # 0x601018 <[email protected]>
   0x00000000004005b6 <+6>:	pushq  $0x0
   0x00000000004005bb <+11>:	jmpq   0x4005a0
End of assembler dump.
(gdb) x/32 0x601018
0x601018 <[email protected]>:	0x25e6fa70	0x0000003e	0x25e20610	0x0000003e
0x601028 <[email protected]>:	0x004005d6	0x00000000	0x004005e6	0x00000000
(gdb) n
Single stepping until exit from function _dl_runtime_resolve,
which has no line number information.
0x0000003e25e6fa70 in puts () from /lib64/libc.so.6
...

下面来说明foo的执行:当代码第一次执行foo函数,进程查找GOT表,找不到该函数,这个时候跳转到PLT[0] 使用_dl_runtime_resolve查找foo函数的绝对地址,当找到该函数绝对地址后,进入foo函数执行,foo函数中存在printf () 函数,这个函数和之前main函数中的printf() 不同,重新使用_dl_runtime_resolve 查找libc中的puts函数,将其插入到GOT表中。

(gdb) disassemble
Dump of assembler code for function foo@plt:
   0x00000000004005e0 <+0>:	jmpq   *0x200a4a(%rip)        # 0x601030 <[email protected]>
=> 0x00000000004005e6 <+6>:	pushq  $0x3
   0x00000000004005eb <+11>:	jmpq   0x4005a0
End of assembler dump.
...
(gdb)
0x00007ffff7df85a0 in puts@plt () from libfoo.so
(gdb)
0x00007ffff7df85a6 in puts@plt () from libfoo.so
(gdb)
0x00007ffff7df85ab in puts@plt () from libfoo.so
(gdb)
0x00007ffff7df8590 in ?? () from libfoo.so
(gdb)
0x00007ffff7df8596 in ?? () from libfoo.so
(gdb)
0x0000003e25615b70 in _dl_runtime_resolve () from /lib64/ld-linux-x86-64.so.2

当再次使用libfoo.so 中的foo函数,直接跳转GOT执行即可,无需再次查找。

(gdb)
0x00007ffff7df86db	5	    printf(...);
(gdb)
0x00007ffff7df85a0 in puts@plt () from libfoo.so
(gdb)
0x0000003e25e6fa70 in puts () from /lib64/libc.so.6
...

总结:

ld-linux-x86-64.so.2 是一个动态链接库,负责查找程序所使用的函数绝对地址,并将其写入到GOT表中,以供后续调用。其中GOT[0]为空,GOT[1]和GOT[2]用于保存查找的绝对函数地址,GOT[1]保存的是一个地址,指向已经加载的共享库的链表地址;GOT[2]保存的是一个函数的地址,定义如下:GOT[2] = &_dl_runtime_resolve,这个函数的主要作用就是找到某个符号的地址,并把它写到与此符号相关的GOT项中,然后将控制转移到目标函数,而后面的GOT[3],GOT[4]…都是通过_dl_fixup 添加的。

289baeed-3f91-3651-b81b-159632d1cf45

参考:

http://www.lizhaozhong.info/archives/524
http://flint.cs.yale.edu/cs422/doc/ELF_Format.pdf
http://rickgray.me/2015/08/07/use-gdb-to-study-got-and-plt.html
http://blog.csdn.net/anzhsoft/article/details/18776111

为 LXC 配置网络

October 16th, 2015

LXC是一个基于cgroup 与 namespace 机制的轻量级虚拟机,在Ubuntu平台下有专门的源,可以直接通过apt-get安装,但是在debian平台下,软件仓库中lxc版本太低,导致很多新特性无法使用,推荐源码安装。截止到我写这篇博客,lxc版本已经更新至1.1.4 。

首先我们首先要编译安装最新版的LXC,根据教程INSTALL,我们需要运行autogen.sh ./configure 生成Makefile,这里必须将LXC 中的Security feature 全部安装,否则无法通过lxc-start 启动容器。

为容器配置网络有两种形式:1) 使用网桥    2) 直接使用物理网卡

1) 使用网桥

假设我们主机只有eth0的物理网卡,在主机/etc/network/interfaces中,直接加入下面的字段:

auto br0
iface br0 inet dhcp
        bridge_ports eth0
        bridge_fd 0
        bridge_maxwait 0

然后重启网络 /etc/init.d/networking restart 之后可以发现主机网络出现br0的网桥。

如果LXC在编译时没有配置路径,容器的config默认路径在/usr/local/var/lib/lxc/xxx/config ,我们需要在这个文件中加入网络选项

lxc.network.type = veth
lxc.network.flags = up

# that's the interface defined above in host's interfaces file
lxc.network.link = br0

# name of network device inside the container,
# defaults to eth0, you could choose a name freely
# lxc.network.name = lxcnet0 

lxc.network.hwaddr = 00:FF:AA:00:00:01

然后我们在容器的/etc/network/interfaces中,添加

auto eth0
iface eth0 inet dhcp

如果容器中没有开启dhclient服务,最好将其加到 /etc/rc.local中即可。

2) 直接使用物理网卡

比如物理宿主主机拥有两张网卡:eth0 与 eth1,我把eth0作为主机使用,eth1作为LXC使用。那么我们在config中添加

xc.network.type=phys
lxc.network.link=eth1
lxc.network.flags=up
#lxc.network.hwaddr = 00:16:3e:f9:ad:be #注释掉#

lxc.network.flags 用于指定网络的状态,up 表示网络处于可用状态。
lxc.network.link 用于指定用于和容器接口通信的真实接口,比如一个网桥 br0 ,eth0等。

在主机/etc/network/interfaces中加入

auto eth1
iface eth1 inet dhcp

然后重新启动网络服务 #/etc/init.d/networking restart
重新启动 LXC 容器 # lxc-start -n xxx

一旦 LXC 虚拟计算机启动成功,在宿主计算机上使用〝ifconfig -a〞查看主机网络接口,用户会发现此时网络接口 eth1 消失了,只有 eth0 。这是因为 eth1 已经让 LXC 虚拟计算机给使用了。然后我们使用如下命令“ lxc-attach -n xxx”登录 LXC 虚拟计算机发现此时 LXC 虚拟计算机的网络接口是 eth1。然后我们可以使用 ping 命令测试一下 LXC 虚拟计算机和互联网是否联通。

3) 容器配置静态IP

如果我们使用静态IP的话,宿主机可以使用静态IP或者是DHCP,我们假定宿主机是DHCP,容器是静态IP,注意最后两个字段:

lxc.network.type = veth
lxc.network.flags = up

# that's the interface defined above in host's interfaces file
lxc.network.link = br0

# name of network device inside the container,
# defaults to eth0, you could choose a name freely
# lxc.network.name = lxcnet0 

lxc.network.hwaddr = 00:FF:AA:00:00:01
lxc.network.ipv4 = 192.168.1.110/24#注意设置为宿主机的网段
lxc.network.ipv4.gateway = 192.168.1.1#注意设置为宿主机的网段

在容器内的/etc/network/interfaces中加入,记住不加auto eth0!

iface eth0 inet static
       address <container IP here, e.g. 192.168.1.110>
       netmask 255.255.255.0
       network <network IP here, e.g. 192.168.1.0>
       broadcast <broadcast IP here, e.g. 192.168.1.255>
       gateway <gateway IP address here, e.g. 192.168.1.1>
       # dns-* options are implemented by the resolvconf package, if installed
       dns-nameservers <name server IP address here, e.g. 192.168.1.1>
       dns-search your.search.domain.here

结束:

根据我与CRIU团队的交流,目前CRIU不支持对于LXC独占物理网卡的c/r ,对于某些application使用 SOCK_PACKET 的套接字目前也不支持!这个特性已被加到criu新特性中,https://github.com/xemul/criu/issues/73 。预计在之后的版本中支持!

 

https://www.ibm.com/developerworks/cn/linux/1312_caojh_linuxlxc/

https://wiki.debian.org/LXC/SimpleBridge