“郭孟琦 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
之前听说过gdb这一个调试器,但是因为一直都在用windows下的IDE,所以也仅仅是知道而已。这回使用gdb跟踪调试linux内核实际上一开始我是拒绝的,因为你不能叫我用,我就马上去用,第一我要试一下,因为我不愿意我调完了以后再加一些特技上去,调试器“duang”一下很快,很好用,这样出来程序员一定要骂我,根本没有这样的调试器,就证明上面那个是假的。后来我也经过证实他们确实是命令行的,我用了大概一天左右,感觉还不错,后来我在敲代码的时候也要求他们不要加特技,因为我要让观众看到,我用的时候是这个样子,你们用的时候也是这个样子!
其实就是一句话,从图形式到命令式,真的尺有所短,寸有所长。
首先配置环境
# 下载内核源代码编译内核
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
make # 一般要编译很长时间,少则20分钟多则数小时
# 制作根文件系统
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
# 启动MenuOS系统
cd ~/LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
#在原来配置的基础上,make menuconfig选中如下选项重新配置Linux,使之携带调试信息
kernel hacking—>
[*] compile the kernel with debug info
总之ok了以后
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
# -S freeze CPU at startup (use ’c’ to start execution)
# -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项
会启动一个stopped的qemu
然后再打开一个terminal,启动gdb并且读取调试的symbols,设置break在 start_kernel上
下面就正式开始跟踪分析Linux内核的启动过程(从start_kernel到init进程)。
为了方便对照我将
start_kernel
的代码
粘了过来
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
/*
* Need to run as early as possible, to initialize the
* lockdep hash:
*/
lockdep_init();
set_task_stack_end_magic(&init_task);
smp_setup_processor_id();
debug_objects_early_init();
/*
* Set up the the initial canary ASAP:
*/
boot_init_stack_canary();
cgroup_init_early();
local_irq_disable();
early_boot_irqs_disabled = true;
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
boot_cpu_init();
page_address_init();
pr_notice("%s", linux_banner);
setup_arch(&command_line);
mm_init_cpumask(&init_mm);
setup_command_line(command_line);
setup_nr_cpu_ids();
setup_per_cpu_areas();
smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks
build_all_zonelists(NULL, NULL);
page_alloc_init();
pr_notice("Kernel command line: %s\n", boot_command_line);
parse_early_param();
after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, &unknown_bootoption);
if (!IS_ERR_OR_NULL(after_dashes))
parse_args("Setting init args", after_dashes, NULL, 0, -1,
set_init_arg);
jump_label_init();
/*
* These use large bootmem allocations and must precede
* kmem_cache_init()
*/
setup_log_buf(0);
pidhash_init();
vfs_caches_init_early();
sort_main_extable();
trap_init();
mm_init();
/*
* Set up the scheduler prior starting any interrupts (such as
* timer interrupt). Full topology setup happens at smp_init()
* time - but meanwhile we still have a functioning scheduler.
*/
sched_init();
/*
* Disable preemption - early bootup scheduling is extremely
* fragile until we cpu_idle() for the first time.
*/
preempt_disable();
if (WARN(!irqs_disabled(),
"Interrupts were enabled *very* early, fixing it\n"))
local_irq_disable();
idr_init_cache();
rcu_init();
context_tracking_init();
radix_tree_init();
/* init some links before init_ISA_irqs() */
early_irq_init();
init_IRQ();
tick_init();
rcu_init_nohz();
init_timers();
hrtimers_init();
softirq_init();
timekeeping_init();
time_init();
sched_clock_postinit();
perf_event_init();
profile_init();
call_function_init();
WARN(!irqs_disabled(), "Interrupts were enabled early\n");
early_boot_irqs_disabled = false;
local_irq_enable();
kmem_cache_init_late();
/*
* HACK ALERT! This is early. We're enabling the console before
* we've done PCI setups etc, and console_init() must be aware
* this. But we do want output early, in case something goes wrong.
*/
console_init();
if (panic_later)
panic("Too many boot %s vars at `%s'", panic_later,
panic_param);
lockdep_info();
/*
* Need to run this when irqs are enabled, because it wants
* to self-test [hard/soft]-irqs on/off lock inversion bugs
* too:
*/
locking_selftest();
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start && !initrd_below_start_ok &&
page_to_pfn(virt_to_page((void *)initrd_start)) < _low_pfn) {
pr_crit("initrd overwritten (0x%08lx < 0x%08lx) - abling it.\n",
page_to_pfn(virt_to_page((void *)initrd_start)),
min_low_pfn);
initrd_start = 0;
}
#endif
page_cgroup_init();
debug_objects_mem_init();
kmemleak_init();
setup_per_cpu_pageset();
numa_policy_init();
if (late_time_init)
late_time_init();
sched_clock_init();
calibrate_delay();
pidmap_init();
anon_vma_init();
acpi_early_init();
#ifdef CONFIG_X86
if (efi_enabled(EFI_RUNTIME_SERVICES))
efi_enter_virtual_mode();
#endif
#ifdef CONFIG_X86_ESPFIX64
/* Should be run before the first non-init thread is created */
init_espfix_bsp();
#endif
thread_info_cache_init();
cred_init();
fork_init(totalram_pages);
proc_caches_init();
buffer_init();
key_init();
security_init();
dbg_late_init();
vfs_caches_init(totalram_pages);
signals_init();
/* rootfs populating might need page-writeback */
page_writeback_init();
proc_root_init();
cgroup_init();
cpuset_init();
taskstats_init_early();
delayacct_init();
check_bugs();
sfi_init_late();
if (efi_enabled(EFI_RUNTIME_SERVICES)) {
efi_late_init();
efi_free_boot_services();
}
ftrace_init();
/* Do the rest non-__init'ed, we're now alive */
rest_init();
}
可以看到,start_kernel里基本上都是init,比如一上来就初始化了lockdep(死锁)模块,然后就是初始化了init_PCB,进入到该函数发现实际上是设置了PCB的尾巴
而且发现在初始化的时候cpu的中断是被关闭了,应该是防止在初始化阶段被打断吧。
老师在课上提到了
trap_init()
进去看一下(b trap_init)
确实都是一些硬件中断的设置,但其实都是看不懂啊~和cortex-M系列得NVIC比起来x86太高大上了!
这里还有调度器的初始化
sched_init();
感觉基本上各个主要模块基本都会在start_kernel进行初始化。
在函数的最后面执行的rest_init 就包含了init进程启动。那么先进去瞧瞧
可以看到新建了一个kernel_init的内核线程,参阅一些资料发现在linux上实际是没有windowx NT的线程(thread)概念的,这里感觉只是执行了kernel_init这个函数。
在追到kernel_init里终于发现了run_init_process也就是1号用户态进程init终于启动了!
有意思的是如果在磁盘根目录下找不到init时,内核会在按照启动参数时指定的目录找init,如果还没有就会在"/etc/init/","/sbin/init/","/bin/init/","/bin/sh/"
如果还没有,那么就gg了。内核会给出提示,告诉你要有个init。
其实到这里init进程已经启动了,但是init到底是怎么启动的,可以看看run_init_process为何方神圣?
设置断点
b run_init_process
do_execve又是什么鬼?继续查!
呃在追查的过程中,越到底层越是蒙。好在找到了一篇文章
雨儿的博客 http://blog.sina.com.cn/s/blog_4ba5b45e0102e3to.html
内核系统调用函数do_execve()解析
大致明白了这个函数启动init的过程。
在init之后就是管理内核的kthreadd这个内核线程,以及init_idle这个0号进程。
简单总结的话就是,当计算机启动运行完汇编指令以后运行cs:ip指向start_kernel,在里面各个模块都init后在rest_init中首先建立init内核线程,该线程各种找init,一旦找到就建立init 1号进程。然后管理内核的kthreadd的内核线程被建立,在建立init_idle这个0号进程。此时函数rest_init实行完毕,函数start_kernel进而执行完毕,linux内核已启动。