利用GDB跟踪分析linux内核启动
2016-03-12 18:23
691 查看
罗冲 + 原创 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
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
弹出如下窗口:
利用上下箭头,选择kernel hacking,并进入
选择“Compile-time checks and compiler options ”这个选项,并进入
选择”Compile the kernel with debug info”, 按键盘上的Y键可以选中它
选Save保存。 执行make命令,这一过程很长。
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
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之前,也可以在之后
对于start_kernel,需要重点关注上面这两句。
第一句
用于启动0号进程。它的主体动作都是是init_task中进行的,其内核栈通过静态方式分配的。重点分析rest_init,观察1号进程的启动过程.
对于1号进程,我们可以把它为三个过程:初始化、调度过程以及执行过程
而执行是在kernel_init中进行的。 首先分析初始化的过程
这里的start_stack对应的值:
这里sp的值为3245760928,换算成十六进制即为: 0xc17661a0 ,查看kernel_init的地址:
从上面的函数中,可以看出来,0号进程会将自己复制一份作为新进程。接着将kernel_init的入口地址设置到新进程中,接着设置pid的值。而这个pid的值设置是根据宏:
从这里可以看到,linux保证了pid的不重复。
1号进程 的内存信息创建完成后,linux会将保存在一个list中,此时kernel_thread的工作就完成了。但是此时1号进程并没有运行起来。
而:
而2870行的代码是__schedule(),因此我们可以断定所有的调度工作都是通过__schedule()来进行的。最终系统会调用entry_32.S的汇编语言来执行到程序中(怎么调到汇编中没有看明白。)
只需要重点关注上面这句话即可。查看ramdisk_execute_command的值:
从这里可以很清楚看到它会执行/init,而init就是我们之前编译出来的进程。因此我们可以看到0号进程会调用execute()命令将init作为一个可执行程序运行起来。
1号进程是第一个用户态进程,它是由0号进程来创建,创建过程是0号进程先将自己的内存空间复制一份,然后再将1号进程的信息写入
1号进程的实际启动是通过kernel_init来执行。
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 socket 初步
- Linux Kernel 4.0 RC5 发布!
- linux lsof详解
- linux 文件权限
- Linux 执行数学运算
- 10 篇对初学者和专家都有用的 Linux 命令教程
- Linux 与 Windows 对UNICODE 的处理方式
- Ubuntu12.04下QQ完美走起啊!走起啊!有木有啊!
- 解決Linux下Android开发真机调试设备不被识别问题
- 运维入门
- 运维提升
- Linux 自检和 SystemTap
- 神器SystemTap
- Ubuntu Linux使用体验
- c语言实现hashmap(转载)
- Linux 信号signal处理机制
- linux下mysql添加用户
- Scientific Linux 5.5 图形安装教程