您的位置:首页 > 运维架构 > Linux

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

2016-03-12 18:23 691 查看
罗冲 + 原创 + 《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编译选项,并编译

执行命令:make menuconfig



弹出如下窗口:



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



选择“Compile-time checks and compiler options ”这个选项,并进入



选择”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的地址:



从上面的函数中,可以看出来,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处打上断点,然后查看其堆栈信息:



而:

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",

4000
ramdisk_execute_command, ret);
}
... ...
}


只需要重点关注上面这句话即可。查看ramdisk_execute_command的值:



从这里可以很清楚看到它会执行/init,而init就是我们之前编译出来的进程。因此我们可以看到0号进程会调用execute()命令将init作为一个可执行程序运行起来。



3. 总结

0号是通过直接给定内存地址来启动的。因此它是一个很特殊的进程

1号进程是第一个用户态进程,它是由0号进程来创建,创建过程是0号进程先将自己的内存空间复制一份,然后再将1号进程的信息写入

1号进程的实际启动是通过kernel_init来执行。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  linux kernel