一、zImage是怎样炼成的?
zImage是linux内核编译之后产生的最终文件,它的生成过程比较复杂,这里不谈编译过程,只聊聊编译的最后阶段:
1. arm-linux-gnu-ld用arch/arm/kernel/vmlinux.lds、arch/arm/kernel/head.o、
arch/arm/kernel/init_task.o、各子目录下的built-in.o、lib/lib.a 、arch/arm/lib/lib.a生成顶层目录下的vmlinux (根据arch/arm/kernel/vmlinux.lds来链接 0xc0008000)
2. 生成system.map, 置于顶层目录之下。
3. arm-linux-gnu-objcopy,去掉顶层vmlinux两个段-R .note -R .comment
的调试信息,减小映像文件的大小,此时大概3M多,生成arch/arm/boot/Image。
4. gzip -f -9 < arch/arm/boot/compressed/../Image > arch/arm/boot/compressed/piggy.gz,读入arch/arm/boot/Image的内容,以最大压缩比进行压缩,生成arch/arm/boot/compressed/目录下的piggy.gz。
5. arm-linux-gnu-gcc,在arch/arm/boot/compressed/piggy.S文件中是直接引入piggy.gz的内容(piggy.gz其实已经是二进制数据了),然后生成arch/arm/boot/compressed/piggy.o文件。下面是piggy.S的内容
其中所选择的行就是加入了piggy.gz的内容,通过编译生成piggy.o文件,以备后面接下来的ld链接。
6. arm-linux-gnu-ld,在arch/arm/boot/compressed/piggy.o的基础上,加入重定位地址和参数地址的同时,加入解压缩的代码(arch/arm/boot/compressed/head.o、misc.o),最后生成arch/arm/boot/compressed目录的vmlinux,此时在解压缩代码中还含有调试信息(根据arch/arm/boot/compressed/vmlinux.lds来链接 0x0)vmlinux.lds开始处。
注意到了27行的吗?*(.piggydata)就表示需要将piggydata这个段放在这个位置,而piggydata这个段放的是什么呢?往后翻翻,看看第五步的图片,呵呵,其实就是将按最大压缩比压缩之后的Image,压缩之后叫piggy.gz中的二进制数据。
7. arm-linux-gnu-objcopy,去掉解压缩代码中的调试信息段,最后生成arch/arm/boot/目录下的zImage。
8. /bin/sh
/home/farsight/Resources/kernel/linux-2.6.14/scripts/mkuboot.sh -A arm -O linux -T kernel -C none -a 0x30008000 -e 0x30008000 -n 'Linux-2.6.14' -d arch/arm/boot/zImage arch/arm/boot/uImage
调用mkimage在arch/arm/boot/zImage的基础上加入64字节的uImage头,和入口地址,装载地址, 最终生成arch/arm/boot/目录下的uImage文件。
实际上zImage是经过了高压缩之后在和解压缩程序合并在一起生成的。知道了这些之后,我们就可以给linux的启动大致分成3段:zImage解压缩、kernel的汇编启动阶段、kernel的c启动阶段。
前两个阶段因为都是汇编写成的,代码读起来晦涩难懂,内存分布复杂,涉及MMU、解压缩等众多知识。如果有对这部分感兴趣的,可以自行分析,遇到问题可以上网查资料或者找我,这里就不详细分析了。下面是第二阶段汇编启动的主线,可以了解下:
1. 确定 processor type
2. 确定 machine type
3. 手动创建页表
4. 调用平台特定的cpu setup函数,设置中断地址,刷新Cache,开启Cache
(在struct proc_info_list中,in proc-arm920.S)
5. 开启mmu I、D cache ,设置cp15的控制寄存器,设置TTB寄存器为0x30004000
6. 切换数据(根据需要赋值数据段,清bss段,保存processor ID 和 machine type
和 cp15的控制寄存器值)
7. 最终跳转到start_kernel
(在__switch_data的结束的时候,调用了 b start_kernel)
二、linux的c启动阶段
经过解压缩和汇编启动两个阶段,将会进入init/Main.c中的start_kernel()函数去继续执行。(2.6.1x、2.6.2x和2.6.3x之间的差异比较大,下面的分析基于2.6.14)
1. printk(linux_banner)打印内核的一些信息,版本,作者,编译器版本,日期等信
息。
2. 接下来执行是一个及其重要的函数setup_arch(),主要做一些板级初始化,cpu初始
化,tag参数解析,u-boot传递的cmdline解析,建立mmu工作页表(memtable_init),初始化内存布局,调用mmap_io建立GPIO,IRQ,MEMCTRL,UART,及其他外设的静态映射表,对时钟,定时器,uart进行初始化, cpu_init():{打印一些关于cpu的信息,比如cpu id,cache 大小等。另外重要的是设置了IRQ、ABT、UND三种模式的stack空间,分别都是12个字节。最后将系统切换到svc模式}。
3. sched_init():初始化每个处理器的可运行队列,设置系统初始化进程即0号进程。
4. 建立系统内存页区(zone)链表 build_all_zonelists()。
5.printk(KERN_NOTICE "Kernel command line: %s\n", saved_command_line);打印出从uboot传递过来的command_line字符串,在setup_arch函数中获得的。
6. parse_early_param(),这里分析的是系统能够辨别的一些早期参数(这个函数甚至可以去掉,__setup的形式的参数),而且在分析的时候并不是以setup_arch(&command_line)传出来的command_line为基础,而是以最原生态的saved_command_line为基础的。
7. parse_args("Booting kernel", command_line, __start___param,
__stop___param - __start___param,
&unknown_bootoption);
对于比较新的版本真正起作用的函数,与parse_early_param()相比,此处对解析列表的处理范围加大了,解析列表中除了包括系统以setup定义的启动参数,还包括模块中定义的param参数以及系统不能辨别的参数。
__start___param是param参数的起始地址,在System.map文件中能看到
__stop___param - __start___param是参数个数
unknown_bootoption是对应与启动参数不是param的相应处理函数(查看parse_one()就知道怎么回事)。
8. 在前面的setup_arch-àpaging_init-à memtable_init函数中为系统创建页表的时候,中断向量表的虚地址init_maps,是用alloc_bootmem_low_pages分配的,ARM规定中断向量表的地址只能是0或0xFFFF0000,所以该函数里有部分代码的作用就是映射一个物理页到0或0xFFFF0000。
trap_init函数做了以下的工作:把放在.Lcvectors处的系统8个意外入口跳转指令搬到高端中断向量0xffff0000处,再将__stubs_start到__stubs_end之间的各种意外初始化代码搬到0xffff0200处,等。
9. init_IRQ()
初始化系统中所有的中断描述结构数组:irq_desc[NR_IRQS]。接着执行init_arch_irq函数,该函数是在setup_arch函数最后初始化的一个全局函数指针,指向了smdk2410_init_irq函数(in mach-smdk2410.c),实际上是调用了s3c24xx_init_irq函数。在该函数中,首先清除所有的中断未决标志,之后就初始化中断的触发方式和屏蔽位,还有中断句柄初始化,这里不是最终用户的中断函数,而是do_level_IRQ或者do_edge_IRQ函数,在这两个函数中都使用过__do_irq函数来找到真正最终驱动程序注册在系统中的中断处理函数。
10. softirq_init():内核的软中断机制初始化函数。
12. console_init():
初始化系统的控制台结构,该函数执行后调用printk函数将log_buf中所有符合打印级别的系统信息打印到控制台上。
13. profile_init()函数
/* 对系统剖析做相关初始化, 系统剖析用于系统调用*/
//profile是用来对系统剖析的,在系统调试的时候有用
//需要打开内核选项,并且在bootargs中有profile这一项才能开启这个功能/*
profile只是内核的一个调试性能的工具,这个可以通过menuconfig中profiling support打开。
14. vfs_caches_init()
该函数主要完成的是文件系统相关的初始化,cache、inode等高速缓存的建
立,在mnt_init()函数中有注册并初始化sysfs、rootfs文件系统,这里只是在内存中建立他们的架构,创建了超级块,并没有真正挂载上去。关于这个rootfs需要说明的是,这个文件系统生命期更加短暂的,为什么?之前说的ramdisk大家是否还记得,ramdisk即将在后面释放到内存空间,来代替这里的rootfs出现在根目录之下,而这个rootfs则退居二线,隐藏在一个二级目录中。本来在非android的系统上,这个ramdisk也是一个暂时的文件系统,之后也会被真正的yaffs2之类的文件系统替换。不过呢,在android上,这个ramdisk还是挂载在根目录下的,只是将system、userdata等真实文件系统挂载了对应的二级目录下。
关于这部分ramdisk内容,有兴趣的下来可以继续探讨。
15. mem_init():
最后内存初始化,释放前边标志为保留的所有页面,这个函数结束之后就不能再使
用alloc_bootmem(),alloc_bootmem_low(),alloc_bootmem_pages()等申请低端内存的函数来申请内存,也就不能申请大块的连续物理内存了。
16. 中间还省略了很多内容,涉及到很多东西,这里也没有时间详细讨论,有兴趣的自
己研究代码吧!下面直接跳到start_kernel()函数的最后的一个重要函数:rest_init()。
17. rest_init函数创建了两个线程之后,自己调用cpu_idle()函数隐退了。
创建的第一个线程,习惯上我们将其叫做1号内核线程,第二个线程叫2号内核线程,因为创建它们的父进程叫0号启动进程。
说明一下:2.6.14的内核这里只创建了一个内核线程叫init线程,而上面创建两
个线程的内核版本至少都是2.6.2x了,所以为了后面能和android的启动接上,所以这里开始linux转到2.2.29去。
static noinline void __init_refok rest_init(void) __releases(kernel_lock)
{
int pid;
…
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
…
cpu_idle();
}
kthreadd这个线程之前的部门交流会上讨论过,新版本的linux将线程创建这个艰巨的工作专门交给了这个叫kthreadd的线程来完成。
接下来既然0号启动进程idle了,那么剩下的工作就都转移到线程kernel_init中去了。
18. kernel_init()
这个线程的任务还是比较艰巨的,第一个重要任务就是调用函数
do_basic_setup(),先调用driver_init()来构建sysfs的目录架构,然后调用do_initcalls()函数来一次执行linux编译时设置的系统函数。
这里主要工作就是注册系统设备的驱动程序,关于driver和device的注册顺序,是可以互相交换,例如通常的三星平台都有一个struct machine_desc结构体来描述平台相关的启动代码:
MACHINE_START(SMDK2410, "SMDK2410") /* @TODO: request a new identifier and switch
* to SMDK2410 */
/* Maintainer: Jonas Dietsche */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = smdk2410_map_io,
.init_irq = s3c24xx_init_irq,
.init_machine = smdk2410_init,
.timer = &s3c24xx_timer,
MACHINE_END
所有devices的注册都是在smdk2410_init()函数中调用函数:
platform_add_devices(smdk2410_devices, ARRAY_SIZE(smdk2410_devices));
来完成,所以drivers的注册就放在后面了。不过这样注册是有一个坏处的,就是不能准确地控制driver代码中probe的执行先后顺序。
现在mtk平台上的devices和drivers注册顺序想法,也就是先注册上drivers,然后再注册devices,这样的话,就可以控制probe函数的执行先后。
include/linux/init.h文件中有这些优先级的定义:
#define pure_initcall(fn) __define_initcall("0",fn,0)
#define core_initcall(fn) __define_initcall("1",fn,1)
#define core_initcall_sync(fn) __define_initcall("1s",fn,1s)
#define postcore_initcall(fn) __define_initcall("2",fn,2)
#define postcore_initcall_sync(fn) __define_initcall("2s",fn,2s)
#define arch_initcall(fn) __define_initcall("3",fn,3)
#define arch_initcall_sync(fn) __define_initcall("3s",fn,3s)
#define subsys_initcall(fn) __define_initcall("4",fn,4)
#define subsys_initcall_sync(fn) __define_initcall("4s",fn,4s)
#define fs_initcall(fn) __define_initcall("5",fn,5)
#define fs_initcall_sync(fn) __define_initcall("5s",fn,5s)
#define rootfs_initcall(fn) __define_initcall("rootfs",fn,rootfs)
#define device_initcall(fn) __define_initcall("6",fn,6)
#define device_initcall_sync(fn) __define_initcall("6s",fn,6s)
#define late_initcall(fn) __define_initcall("7",fn,7)
#define late_initcall_sync(fn) __define_initcall("7s",fn,7s)
当然函数的执行属性从1~7,通常我们见到的设备都是6、7级的。另外系统中所有的initcalll函数都是可以从linux根目录下的system.map中查看得到。
接下来的一段代码就是来释放前面提到的ramdisk.img的:
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();
}
释放出来的ramdisk呈现出来的目录就是android编译出来之后,在out/…/root的目录一样了,这个目录下有一个init可执行程序,下面就准备启动它。
接着调用init_post()函数,来打开console设备,这个时候我们的控制台就可以操作了,最后会执行以下代码来寻找和启动init程序:
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程序需要我们在u-boot传给kernel的cmdline中使用init=/init
来告知kernel,或者kernel启动代码中直接写死。否则在上面的那些目录中找不到init的话,系统就用panic机制将这个警告信息保存在nand的panic分区,在下次启动的时候,会自动将这个分区的信息输出。
init进程是linux起来之后启动的第一个用户进程,android系统也就是在这个进
程的基础上启动的。进程号是1。