利用GDB跟踪分析linux内核启动

罗冲 + 原创 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

1. 实验准备

1. 下载代码3.18.6到linux环境下面中,

cd ~/LinuxKernel/
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz
xz -d linux-3.18.6.tar.xz
tar -xvf linux-3.18.6.tar
cd linux-3.18.6
make i386_defconfig

2. 修改linux编译选项,并编译

  1. 执行命令:make menuconfig
    这里写图片描述
  2. 弹出如下窗口:

    利用上下箭头,选择kernel hacking,并进入

    选择“Compile-time checks and compiler options ”这个选项,并进入
    利用GDB跟踪分析linux内核启动_第1张图片
    选择”Compile the kernel with debug info”, 按键盘上的Y键可以选中它

    选Save保存。 执行make命令,这一过程很长。

3.制作根文件系统

cd ~/LinuxKernel/
mkdir rootfs
git clone https://github.com/mengning/menu.git
cd menu
gcc -o init linktable.c menu.c test.c -m32 -static -lpthread
cd ../rootfs
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
(命令都是都从孟宁老师的课件中拷贝过来的)
注:编译的时候,需要注意这里使用的是静态编译, 连接的是libpthread.a

4. 构造gdb跟踪

1) 首先在一个shell窗口中执行命令:
cd ~/LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd linux-3.18.6/rootfs.img -s -S
2) 在启动另一个窗口
gdb
(gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
(gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
(gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后

2. 分析过程

整个linux的启动过程都是main.c中的start_kernel函数


asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    char *after_dashes;

   ... ... 
    set_task_stack_end_magic(&init_task);
     ... ... 

    /* Do the rest non-__init'ed, we're now alive */
    rest_init();
}

对于start_kernel,需要重点关注上面这两句。
第一句

    set_task_stack_end_magic(&init_task);

用于启动0号进程。它的主体动作都是是init_task中进行的,其内核栈通过静态方式分配的。重点分析rest_init,观察1号进程的启动过程.
对于1号进程,我们可以把它为三个过程:初始化、调度过程以及执行过程

static noinline void __init_refok rest_init(void)
{
    int pid;
    ... ... 
    //初始化过程 
    kernel_thread(kernel_init, NULL, CLONE_FS);

    ... ... 
    //准备调度
    schedule_preempt_disabled();
    /* Call into cpu_idle with preempt disabled */
    cpu_startup_entry(CPUHP_ONLINE);
}

而执行是在kernel_init中进行的。 首先分析初始化的过程

1)1号进程的初始化

1号进程的初始化是在kernel_thread中进行的,它会将kernel_init的地址传入。其具体工作在kernel/fork.c中的copy_process()中进行的。

static struct task_struct *copy_process(unsigned long clone_flags,
                    unsigned long stack_start,
                    unsigned long stack_size,
                    int __user *child_tidptr,
                    struct pid *pid,
                    int trace)
{
   .... 


    retval = security_task_create(clone_flags);
    if (retval)
        goto fork_out;

    retval = -ENOMEM;
    //将当前的进程复制, 这个current对应于include/asm/current.h中的get_current()
    //此时第一个线程根据当前线程信息被创建出来
    p = dup_task_struct(current);

  //设置一些线程的权限
    retval = copy_creds(p, clone_flags);
  //接下来设置namespace, mm, cgroup,等信息

    if (retval)
        goto bad_fork_cleanup_namespaces;

  //把kernel_init的入口地址写入到p中
    retval = copy_thread(clone_flags, stack_start, stack_size, p);
     ... ... 

        //这里会给pid赋值,就是我们使用top查看的到的pid值
        __this_cpu_inc(process_counts);

  ... .... 

}

这里的start_stack对应的值:

这里sp的值为3245760928,换算成十六进制即为: 0xc17661a0 ,查看kernel_init的地址:
利用GDB跟踪分析linux内核启动_第2张图片
从上面的函数中,可以看出来,0号进程会将自己复制一份作为新进程。接着将kernel_init的入口地址设置到新进程中,接着设置pid的值。而这个pid的值设置是根据宏:

#define __this_cpu_inc(pcp) __this_cpu_add(pcp, 1)

从这里可以看到,linux保证了pid的不重复。
1号进程 的内存信息创建完成后,linux会将保存在一个list中,此时kernel_thread的工作就完成了。但是此时1号进程并没有运行起来。

2) 1号进程的调度过程

函数的调度是通过函数schedule_preempt_disabled()来启动,在kernel_init处打上断点,然后查看其堆栈信息:
利用GDB跟踪分析linux内核启动_第3张图片
而:

asmlinkage __visible void __sched schedule(void)
{
    struct task_struct *tsk = current;

    sched_submit_work(tsk);
    __schedule();  //2870行
}

而2870行的代码是__schedule(),因此我们可以断定所有的调度工作都是通过__schedule()来进行的。最终系统会调用entry_32.S的汇编语言来执行到程序中(怎么调到汇编中没有看明白。)

3) 1号进程的执行

当kernel_init被调用之后,就开始执行其中的代码

static int __ref kernel_init(void *unused)
{
   ... ... 
    if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return 0;
            pr_err("Failed to execute %s (error %d)\n",
               ramdisk_execute_command, ret);
    }
    ... ... 
}

只需要重点关注上面这句话即可。查看ramdisk_execute_command的值:
这里写图片描述
从这里可以很清楚看到它会执行/init,而init就是我们之前编译出来的进程。因此我们可以看到0号进程会调用execute()命令将init作为一个可执行程序运行起来。

3. 总结

  1. 0号是通过直接给定内存地址来启动的。因此它是一个很特殊的进程
  2. 1号进程是第一个用户态进程,它是由0号进程来创建,创建过程是0号进程先将自己的内存空间复制一份,然后再将1号进程的信息写入
  3. 1号进程的实际启动是通过kernel_init来执行。

你可能感兴趣的:(linux,kernel)