版权声明:本文原创,转载需声明作者ID和原文链接地址。

  Hi!大家好,我是CrazyCatJack。今天给大家带来的是Linux内核启动过程概述。希望能够帮助大家更好的理解Linux内核的启动,并且创造出自己的内核^_^

  Linux的启动代码真的挺大,从汇编到C,从Makefile到LDS文件,需要理解的东西很多。毕竟Linux内核是由很多人,花费了巨大的时间和精力写出来的。而且直到现在,这个世界上仍然有成千上万的程序员在不断完善Linux内核的代码。今天我们主要讲解的是Linux-2.6.22.6这个内核版本。说句实话,博主也不确定自己能够讲好今天这个题目,因为这个题目太大太难。但是博主有信心,将自己学会的内容清楚地告诉大家,希望大家也能够有所收获。

1.启动文件head.S和head-common.S 

  首先,我们必须明确“我们为什么要启动Linux内核”。没错,当然是因为我们想要使用Linux系统,要明确我们的最终目的是使用Linux上的应用程序。这些应用程序可以是纯软件的,也可以是硬件相关的。博主是做嵌入式开发的,那么我想要的当然就是用Linux内核来更好的控制我的硬件。无论是做机器人、无人机或者其他智能硬件这都是必然趋势。首先我们来看内核的启动文件head.S。

    .section ".text.head", "ax"
.type stext, %function
ENTRY(stext)
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
@ and irqs disabled
mrc p15, , r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
movs r10, r5 @ invalid processor (r5=)?
beq __error_p @ yes, error 'p'
bl __lookup_machine_type @ r5=machinfo
movs r8, r5 @ invalid machine (r5=)?
beq __error_a @ yes, error 'a'
bl __create_page_tables ldr r13, __switch_data @ address to jump to after
@ mmu has been enabled
adr lr, __enable_mmu @ return (PIC) address
add pc, r10, #PROCINFO_INITFUNC

  首先看这段汇编代码,它主要是用来做一些内核启动前的检测:__lookup_processor_type 检测内核是否支持当前CPU、__lookup_machine_type检测是否支持当前单板,并且__create_page_tables创建页表,__enable_mmu使能MMU。如果在一系列的自检过程后发现不支持,则跳到__error_p或__error_a。这里我们首先打开__lookup_machine_type。

    .type    __lookup_machine_type, %function
__lookup_machine_type:
adr r3, 3b
ldmia r3, {r4, r5, r6}
sub r3, r3, r4 @ get offset between virt&phys
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type
teq r3, r1 @ matches loader number?
beq 2f @ found
add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc
cmp r5, r6
blo 1b
mov r5, # @ unknown machine
: mov pc, lr : .long .
.long __arch_info_begin
.long __arch_info_end

  我们在arch\arm\kernel找到__lookup_machine_type被定义在head-common.S文件中。开始分析代码:首先,读出3b的地址给r3,这里的3b就是下面的那个3:所对应的虚拟地址。然后用ldmia指令将r3存放的虚拟地址分别存入r4,r5,r6。所以现在

r4=. ; r5=__arch_info_begin ; r6=__arch_info_end

然后用r3-r4求出偏移地址,再利用这个偏移地址求出r5和r6的实际物理地址。其中__arch_info_begin和__arch_info_end定义在内核目录arch\arm\kernel下vmlinux.lds文件中,经过起始虚拟地址= (0xc0000000) + 0x00008000逐层叠加得到。

SECTIONS
{ . = (0xc0000000) + 0x00008000; .text.head : {
_stext = .;
_sinittext = .;
*(.text.head)
} .init : { /* Init code and data */
*(.init.text)
_einittext = .;
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;

  这里的__arch_info_begin和__arch_info_end中间存放的是段属性为.arch.info.init的结构体。这里我们可以直接在linux下查询内核中包含.arch.info.init的文件。

Direction:include/asm-arm/arch.h
#define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name, #define MACHINE_END \
};
Direction:arch/arm/mach-s3c2440
MACHINE_START(S3C2440, "SMDK2440")
/* Maintainer: Ben Dooks <ben@fluff.org> */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> ) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100, .init_irq = s3c24xx_init_irq,
.map_io = smdk2440_map_io,
.init_machine = smdk2440_machine_init,
.timer = &s3c24xx_timer,
MACHINE_END

  如图所示,在include/asm-arm/arch.h中找到了定义的结构体类型machine_desc,并且在代码中它的段属性被强制定义成了.arch.info.init。这样做的目的是在刚刚我们看到的vmlinux.lds链接脚本文件中,可以将具有.arch.info.init段属性的结构体统一放在__arch_info_begin和__arch_info_end之间。非常便于处理。那么现在我们将这个结构体展开,看看它的内容。也就是将arch/arm/mach-s3c2440中的参数传入。展开后如下:

#define MACHINE_START(_type,_name)            \
static const struct machine_desc __mach_desc_S3C2440 \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_S3C2440, \
.name = "SMDK2440",
/* Maintainer: Ben Dooks <ben@fluff.org> */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> ) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100, //0x30000100 .init_irq = s3c24xx_init_irq,
.map_io = smdk2440_map_io,
.init_machine = smdk2440_machine_init,
.timer = &s3c24xx_timer,
};

  现在我们看到,定义的结构体类型machine_desc,内容为.nr到.timer。我们可以看出这个结构体大概是存储硬件信息。nr存放机器ID,name存放单板名称,phys_io存放输入输出口,io_pg_offst存放IO的偏移地址,boot_params存放uboot传给内核的启动参数(TAG),init_irq存放的是中断初始化信息,map_io为IO的映射表,init_machine存放的是单板的初始化信息,timer存放的是单板的定时器信息。

struct machine_desc {
/*
* Note! The first four elements are used
* by assembler code in head-armv.S
*/
unsigned int nr; /* architecture number */
unsigned int phys_io; /* start of physical io */
unsigned int io_pg_offst; /* byte offset for io
* page tabe entry */ const char *name; /* architecture name */
unsigned long boot_params; /* tagged list */ unsigned int video_start; /* start of video RAM */
unsigned int video_end; /* end of video RAM */ unsigned int reserve_lp0 :; /* never has lp0 */
unsigned int reserve_lp1 :; /* never has lp1 */
unsigned int reserve_lp2 :; /* never has lp2 */
unsigned int soft_reboot :; /* soft reboot */
void (*fixup)(struct machine_desc *,
struct tag *, char **,
struct meminfo *);
void (*map_io)(void);/* IO mapping function */
void (*init_irq)(void);
struct sys_timer *timer; /* system tick timer */
void (*init_machine)(void);
};

  我们打开arch.h文件,看到对machine_desc结构体的定义确实和我们刚刚所说的一样。再回到head-common.S文件,这里对mmap_switch定义:

    .type    __mmap_switched, %function
__mmap_switched:
adr r3, __switch_data + ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5 @ Copy data segment if needed
: cmpne r5, r6
ldrne fp, [r4], #
strne fp, [r5], #
bne 1b mov fp, # @ Clear BSS (and zero fp)
: cmp r6, r7
strcc fp, [r6],#
bcc 1b ldmia r3, {r4, r5, r6, sp}
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
bic r4, r0, #CR_A @ Clear 'A' bit
stmia r6, {r0, r4} @ Save control register values
b start_kernel

  mmap_switch做了很多工作,这里我们看到有复制数据段,清BSS段,保存CPU的ID,保存机器ID,清‘A’位,保存控制寄存器的值,然后就到了C语言段——start_kernel函数。

2.C语言段——start_kernel

  

asmlinkage void __init start_kernel(void)
{
local_irq_disable();
early_boot_irqs_off();
early_init_irq_lock_class(); /*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
lock_kernel();
tick_init();
boot_cpu_init();
page_address_init();
printk(KERN_NOTICE);
printk(linux_banner);
setup_arch(&command_line);
setup_command_line(command_line);
printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
parse_early_param();
parse_args("Booting kernel", static_command_line, __start___param,
__stop___param - __start___param,
&unknown_bootoption);
init_IRQ();
profile_init();
if (!irqs_disabled())
printk("start_kernel(): bug: interrupts were enabled early\n");
early_boot_irqs_on();
local_irq_enable();
console_init(); rest_init();
}

  接下来进入start_kernel启动内核的C函数。上面是start_kernel的部分代码。这部分代码的主要作用是处理uboot传递来的参数,设置与体系结构相关的环境,初始化控制台,最后执行应用程序,实现功能。这里我把start_kernel函数的几个主要功能的子函数逐层写出,帮助大家理解start_kernel的功能结构。

start_kernel
setup_arch(&command_line);
setup_command_line(command_line);
unknown_bootoption
obsolete_checksetup
parse_early_param
do_early_param
rest_init;
kernel_init
prepare_namespace
mount_root
init_post

  这里每一个退格(TAB)都代表此函数被上一个函数调用(例如obsolete_checksetup是unknown_bootoption调用的函数)。setup_arch(&command_line)和setup_command_line(command_line)就是用来处理uboot传递进来的启动参数的(处理TAG)。obsolete_checksetup从__setup_start到 __setup_end,调用用非early标识的函数;do_early_param从__setup_start到 __setup_end,调用用early标识的函数(但因为__setup_param(str, fn, fn, 0)中early赋值为0,所以不在这里调用),所以我们主要用obsolete_checksetup。这在后面我们会提到。mount_root是挂载根文件系统,因为Linux上的应用程序最终要在根文件系统上运行。最后是init_post中运行应用程序。那么现在就有一个问题,Linux内核是如何接收uboot传来的根文件系统信息的呢?

bootcmd=nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0
bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0

  上面是uboot启动时打印的环境变量。其中我们能够看到根文件系统挂载到第4个分区:root=/dev/mtdblock3 (从0分区开始)。上面我们提到过,setup_arch(&command_line)和setup_command_line(command_line)就是用来处理uboot传递进来的启动参数的(处理TAG)。但这个处理只是简单的复制粘贴而已,这两个函数将TAG保存,但并未进行真正的处理。那么真正告诉内核在哪里挂载的函数是什么呢?我们通过查看prepare_namespace可以看到一个saved_root_name。查找saved_root_name,发现在Do_mounts.c文件中有对它的调用:

static int __init root_dev_setup(char *line)
{
strlcpy(saved_root_name, line, sizeof(saved_root_name));
return ;
} __setup("root=", root_dev_setup); //传入一个字符串,一个函数

  根据我们之前的经验,我们可以猜测这个__setup宏,也是定义了一个结构体。通过查找__setup我们找到了它的宏定义:

Dir:init.h
#define __setup(str, fn) \
__setup_param(str, fn, fn, ) #define __setup_param(str, unique_id, fn, early) \
static char __setup_str_##unique_id[] __initdata = str; \
static struct obs_kernel_param __setup_##unique_id \
__attribute_used__ \
__attribute__((__section__(".init.setup"))) \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_##unique_id, fn, early }

  在init.h文件里,定义__setup等于__setup_param。那么在__setup_param的宏定义里,我们可以知道:它先定义了一个字符串,然后定义了一个结构体类型obs_kernel_param __setup。这个结构体的段属性为.init.setup,内容为一个字符串,一个函数,还有early。具备这个属性的结构体被链接脚本文件放到一起,从__setup_start到 __setup_end搜索调用。在vmlinux.lds中
  __setup_start = .;
   *(.init.setup)
  __setup_end = .;

  但是在Flash里没有分区,只能和uboot一样,将分区在代码里写死。一般在启动Linux的时候,Linux会自动打印出分区的信息。这里我的分区是这样的:

Creating  MTD partitions on "NAND 256MiB 3,3V 8-bit":
0x00000000-0x00040000 : "bootloader"
0x00040000-0x00060000 : "params"
0x00060000-0x00260000 : "kernel"
0x00260000-0x10000000 : "root"

  我们搜索这个分区名 grep "\"bootloader\"" * -nR。在arch/arm/plat-s3c24xx中找到分区代码:

static struct mtd_partition smdk_default_nand_part[] = {
[] = {
.name = "bootloader",
.size = 0x00040000,
.offset = ,
},
[] = {
.name = "params",
.offset = MTDPART_OFS_APPEND,
.size = 0x00020000,
},
[] = {
.name = "kernel",
.offset = MTDPART_OFS_APPEND,
.size = 0x00200000,
},
[] = {
.name = "root",
.offset = MTDPART_OFS_APPEND,
.size = MTDPART_SIZ_FULL,
}
};

  就是这样,在处理完uboot传递的参数,进行CPU和单板的校验,挂载根文件系统等一系列操作后,最终内核执行init_post()中的应用程序。内核启动流程讲解完毕^_^

题外话:最近博主在自学Linux kernel和Linux device driver,感觉有难度。但是还是很有意义的,因为能够看到前辈的代码,心里真的很高兴。我就希望自己也能够修改Linux源代码,写出适合自己硬件的Linux系统。不仅如此,我还希望能够将自己的代码开源,分享给更多的人。完善Linux内核,让它变得更快更方便是博主的最终目标。博主会继续学习,然后把知识更好的分享给大家!

CCJ

2016-12-06 09:41:51

Linux内核启动过程概述的更多相关文章

  1. Linux内核启动过程start_kernel分析

    虽然题目是start_kernel分析,但是由于我在ubuntu环境下配置实验环境遇到了一些问题,我觉得有必要把这些问题及其解决办法写下来. 首先我使用的是Ubuntu14.04 amx64,以下的步 ...

  2. 使用gdb跟踪Linux内核启动过程(从start_kernel到init进程启动)

    本次实验过程如下: 1. 运行MenuOS系统 在实验楼的虚拟机环境里,打击打开shell,使用下面的命令 cd LinuxKernel/ qemu -kernel linux-/arch/x86/b ...

  3. linux内核启动过程

    作者:严哲璟 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 通过qemu以 ...

  4. Linux内核启动

    Linux内核启动过程概述 Linux的启动代码真的挺大,从汇编到C,从Makefile到LDS文件,需要理解的东西很多.毕竟Linux内核是由很多人,花费了巨大的时间和精力写出来的.而且直到现在,这 ...

  5. linux内核启动以及文件系统的加载过程

    Linux 内核启动及文件系统加载过程 当u-boot 开始执行 bootcmd 命令,就进入 Linux 内核启动阶段.普通 Linux 内核的启动过程也可以分为两个阶段.本文以项目中使用的 lin ...

  6. Linux内核启动及根文件系统载入过程

    上接博文<u-boot之u-boot-2009.11启动过程分析> Linux内核启动及文件系统载入过程 当u-boot開始运行bootcmd命令,就进入Linux内核启动阶段.与u-bo ...

  7. 【转载】linux内核启动android文件系统过程分析

    主要介绍linux 内核启动过程以及挂载android 根文件系统的过程,以及介绍android 源代码中文件系统部分的浅析. 主要源代码目录介绍Makefile (全局的Makefile)bioni ...

  8. linux 内核启动流程

    Linux内核启动流程详细分析: http://www.linuxidc.com/Linux/2014-10/108034.htm ARM Linux内核启动过程: http://blog.csdn. ...

  9. Linux内核启动分析过程-《Linux内核分析》week3作业

    环境搭建 环境的搭建参考课件,主要就是编译内核源码和生成镜像 start_kernel 从start_kernel开始,才真正进入了Linux内核的启动过程.我们可以把start_kernel看做平时 ...

随机推荐

  1. ASP.NET Core 折腾笔记二:自己写个完整的Cache缓存类来支持.NET Core

    背景: 1:.NET Core 已经没System.Web,也木有了HttpRuntime.Cache,因此,该空间下Cache也木有了. 2:.NET Core 有新的Memory Cache提供, ...

  2. 前端学HTTP之内容协商

    前面的话 一个URL常常需要代表若干不同的资源.例如那种需要以多种语言提供其内容的网站站点.如果某个站点有说法语的和说英语的两种用户,它可能想用这两种语言提供网站站点信息.理想情况下,服务器应当向英语 ...

  3. Android 5.0 到 Android 6.0 + 的深坑之一 之 .so 动态库的适配

    (原创:http://www.cnblogs.com/linguanh) 目录: 前序 一,问题描述 二,为何会如此"无情"? 三,目前存在该问题的知名SDK 四,解决方案,1 对 ...

  4. 讓TQ2440也用上設備樹(1)

    作者:彭東林 郵箱:pengdonglin137@163.com QQ:405728433 開發板 TQ2440 + 64MB 內存 + 256MB Nand 軟件 Linux: Linux-4.9 ...

  5. PayPal高级工程总监:读完这100篇论文 就能成大数据高手(附论文下载)

    100 open source Big Data architecture papers for data professionals. 读完这100篇论文 就能成大数据高手 作者 白宁超 2016年 ...

  6. java面向对象中的关键字

    1,super关键字 super:父类的意思 1. super.属性名 (调用父类的属性) 2. super.方法名 (调用父类的方法) 3. super([参数列表])(调用父类的构造方法) 注意: ...

  7. arcgis api for js入门开发系列八聚合效果(含源代码)

    上一篇实现了demo的图层控制模块,本篇新增聚合效果,截图如下(源代码见文章底部): 聚合效果实现的思路如下: 1.map.html引用聚合包,项目已经包含进来了的聚合文件夹: <script ...

  8. ILJMALL project过程中遇到Fragment嵌套问题:IllegalArgumentException: Binary XML file line #23: Duplicate id

    出现场景:当点击"分类"再返回"首页"时,发生error退出   BUG描述:Caused by: java.lang.IllegalArgumentExcep ...

  9. 基于Ubuntu Hadoop的群集搭建Hive

    Hive是Hadoop生态中的一个重要组成部分,主要用于数据仓库.前面的文章中我们已经搭建好了Hadoop的群集,下面我们在这个群集上再搭建Hive的群集. 1.安装MySQL 1.1安装MySQL ...

  10. ASP.NET Aries 4.0 开源发布:已完成基础功能优化重写

    主要更新: 1:增加AR.Global.GetUser() 方法返回当前登陆者的用户信息. 2:重写AR.Combobox 支持下拉树. 3:调整及扩展Input下拉的配置参数. 4:优化及新增AR. ...