分类: 嵌入式
2014-07-14 22:59:07
修改Linux内核,让Linux启动后不执行init进程,而执行自己编写的程序。
当内核被引导并进行初始化之后,内核就可以启动自己的第一个用户空间应用程序init进程。
Linux内核初始化过程
l 内核映像:
在Bootloader的第二阶段会调用内核镜像,当内核映像被加载到内存中,并且Bootloader阶段 2 的引导加载程序释放控制权之后,内核阶段就开始了。内核映像并不是一个可执行的内核,而是一个压缩过的内核映像。在这个内核映像前面是一个例程,它实现少量硬件设置,并对内核映像中包含的内核进行解压,然后将其放入高端内存中,如果有初始 RAM 磁盘映像,就会将它移动到内存中,并标明以后使用。然后该例程会调用内核,并开始启动内核引导的过程。
l 内核引导过程:
当 bzImage(用于 i386 映像)被调用时,我们从/arch/i386/boot/head.S 的 start 汇编例程开始执行。这个例程会执行一些基本的硬件设置,并调用 /arch/i386/boot/compressed/head.S 中的 startup_32 例程。此例程会设置一个基本的环境(堆栈等),并清除 Block Started by Symbol(BSS)。然后调用一个叫做 decompress_kernel 的 C 函数(在 /arch/i386/boot/compressed/misc.c 中)来解压内核。当内核被解压到内存中之后,就可以调用它了。这是另外一个 startup_32 函数,但是这个函数在 /arch/i386/kernel/head.S 中。
在这个新的 startup_32 函数(也称为清除程序或进程 0)中,会对页表进行初始化,并启用内存分页功能。然后会为任何可选的浮点单元(FPU)检测 CPU 的类型,并将其存储起来供以后使用。然后调用 start_kernel 函数(在 init/main.c 中),它会将您带入与体系结构无关的 Linux 内核部分。实际上,这就是 Linux 内核的 main 函数。
通过调用 start_kernel,会调用一系列初始化函数来设置中断,执行进一步的内存配置,并加载初始 RAM 磁盘。最后,要调用 kernel_thread(在 arch/i386/kernel/process.c 中)来启动 init 函数,这是第一个用户空间进程(user-space process)。最后,启动空任务,现在调度器就可以接管控制权了(在调用 cpu_idle 之后)。
l 深入分析内核源代码
根据上面对内核引导过程的分析,我们可以进入Linux内核源代码跟踪其具体过程。这里我们将图示从./arch/i386/kernel/head.S的Startup_32开始的过程。
Init/main.c中的asmlinkage void __init start_kernel(void)函数最后调用rest_init();
static noinline void __init_refok rest_init(void)
__releases(kernel_lock)
{
int pid;
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
unlock_kernel();
/*
* The boot idle thread must execute schedule()
* at least once to get things moving:
*/
init_idle_bootup_task(current);
rcu_scheduler_starting();
preempt_enable_no_resched();
schedule();
preempt_disable();
/* Call into cpu_idle with preempt disabled */
cpu_idle();
}
我们可以看到kernel_thread函数的定义,我们关心的是rest_init函数中对kernel_thread的调用,从上面我们可以看到这样的语句:
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
在这里,kernel_init(再调用init)当作函数指针传给kernel_thread,这样init将被执行,也就成为Linux启动后第一个用户进程。可以设想,如果我们将自己的函数指针传给kernel_thread,并作适当的初始化,就可以实现我们实验目的。
Kernel_thread函数在/arch/i386/kernel/process.c中:
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
struct pt_regs regs;
memset(®s, 0, sizeof(regs));
regs.ARM_r1 = (unsigned long)arg;
regs.ARM_r2 = (unsigned long)fn;
regs.ARM_r3 = (unsigned long)do_exit;
regs.ARM_pc = (unsigned long)kernel_thread_helper;
regs.ARM_cpsr = SVC_MODE;
return do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, ®s, 0, NULL, NULL);
}
EXPORT_SYMBOL(kernel_thread);
2. 修改Linux内核,创建自己的init进程
经过上面的分析,我们找到init的原型,它在init/main.c中:static int kernel_init(void *);
它的定义在init/main.c最后,具体内容如下:
static int __init kernel_init(void * unused)
{
lock_kernel();
/*
* init can run on any cpu.
*/
set_cpus_allowed_ptr(current, cpu_all_mask);
/*
* Tell the world that we're going to be the grim
* reaper of innocent orphaned children.
*
* We don't want people to have to make incorrect
* assumptions about where in the task array this
* can be found.
*/
init_pid_ns.child_reaper = current;
cad_pid = task_pid(current);
smp_prepare_cpus(setup_max_cpus);
do_pre_smp_initcalls();
start_boot_trace();
smp_init();
sched_init_smp();
do_basic_setup();
/*
* check if there is an early userspace init. If yes, let it do all
* the work
*/
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}
/*
* Ok, we have completed the initial bootup, and
* we're essentially up and running. Get rid of the
* initmem segments and start the user-mode stuff..
*/
init_post();
return 0;
}
其中的init_post()函数调用init 。
static noinline int init_post(void)
__releases(kernel_lock)
{
/* need to finish all async __init code before freeing the memory */
async_synchronize_full();
free_initmem();
unlock_kernel();
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
(void) sys_dup(0);
(void) sys_dup(0);
current->signal->flags |= SIGNAL_UNKILLABLE;
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n",
ramdisk_execute_command);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command) {
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s. Attempting "
"defaults...\n", execute_command);
}
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel.");
}
可以看到,init函数首先做一些基本的初始化设置,清除初始化过程用到的内存空间后,就真正开始执行用户进程。首先尝试打开控制台,然后执行内核配置命令行参数中给的程序,完成一些初始化工作。这里我们可以看到一个if语句,这个execute_command是一个char* 类型的指针,在main.c中第426行的static void __init parse_options(char *line)中,将命令行参数传给了它(第456行,execute_command= line;)像我们在配置内核时,指定这样的字眼:init=/linuxrc,这样linuxrc就是在这开始执行的,当然,它可能还会递归调用其它的需要执行的程序。如果没有设置参数,就尝试执行指定的初始进程,这也是真正的用户进程了。在上面的定义中我们可以看到,系统依次尝试执行/sbin/init, /ect/init/, /bin/init, /bin/sh进程,直到有一个运行成功;如果全都执行失败,系统也就启动失败。
下面开始我们的最终任务,那就是修改init,让系统启动时不执行/sbin/init。简单的方法是先建立自己的可执行程序,如/sbin/myinit,将init中对/sbin/init的调用改为对/sbin/myinit的调用即可。
这里给出本人对init的修改,如下:
将static int __init kernel_init(void * unused)改成static int __init my_kernel_init(void * unused)
再将init_post()函数中的
run_init_process("/sbin/init"); 改成run_init_process("/sbin/myinit");
在/sbin下新建可执行文件myinit,其源代码定义如下:
#!/bin/sh
echo "******************************"
echo "Haha,This is my init process !"
echo "Oh Yeah"
exec /sbin/init #装入并执行/sbin/init,这样就跟默认的执行流程一样了
最后,转到/init/main.c的rest_init(void)函数,修改其对kernel_thread的调用如下,
kernel_thread(my_kernel_init, NULL, CLONE_FS | CLONE_SIGHAND); 这样,我们就实现了内核启动的第一个用户进程是我们自己定义的myinit函数。