3、内核的启动过程

发布者:MagicGarden最新更新时间:2025-02-17 来源: cnblogs关键字:内核  启动过程  s5pv210 手机看文章 扫描二维码
随时随地手机看文章

1.内核分析准备

删除无用文件

官方版本的kernel中是支持各种硬件架构、各种开发板的,因此有很多文件夹和文件和我们无关,在建立SI工程前应该删掉这些家伙。

我们现在分析的是开发板厂商九鼎科技移植好的针对X210开发板的kernel,因此其中一些无用文件已经被删掉了。

 

建立SI工程并解析

建立方法和uboot中当时讲的是一样的。

 

Makefile分析

kernel的Makefile写法和规则等和uboot的Makefile是一样的,甚至Makefile中的很多内容都是一样的。

kernel的Makefile比uboot的Makefile要复杂,这里我们并不会一行一行的详细分析。

Makefile中只有一些值得关注的我会强调一下,其他不强调的地方暂时可以不管。

Makefile中刚开始定义了kernel的内核版本号。这个版本号挺重要(在模块化驱动安装时会需要用到),要注意会查,会改。

在make编译内核时,也可以通过命令行给内核makefile传参(跟uboot配置编译时传参一样)。譬如make O=xxx可以指定不在源代码目录下编译,而到另外一个单独文件夹下编译。

kernel的顶层Makefile中定义了2个很重要的变量,一个是ARCH,一个是CROSS_COMPILE。ARCH决定当前配置编译的路径,譬如ARCH = arm的时候,将来在源码目录下去操作的arch/arm目录。CROSS_COMPILE用来指定交叉编译工具链的路径和前缀。

CROSS_COMPILE = xxx和ARCH = xxx和O=xxx这些都可以在make时通过命令行传参的方式传给顶层Makefile。

所以有时候你会看到别人编译内核时:make O=/tmp/mykernel ARCH=arm CROSS_COMPILE=/usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-

链接脚本分析

分析连接脚本的目的就是找到整个程序的entry

kernel的连接脚本并不是直接提供的,而是提供了一个汇编文件vmlinux.lds.S,然后在编译的时候再去编译这个汇编文件得到真正的链接脚本vmlinux.lds。

vmlinux.lds.S 在 arch/arm/kernel/目录下。

思考:为什么linux kernel不直接提供vmlinux.lds而要提供一个vmlinux.lds.S然后在编译时才去动态生成vmlinux.lds呢?

猜测:.lds文件中只能写死,不能用条件编译。但是我们在kernel中链接脚本确实有条件编译的需求(但是lds格式又不支持),于是乎kernel工作者找了个投机取巧的方法,就是把vmlinux.lds写成一个汇编格式,然后汇编器处理的时候顺便条件编译给处理了,得到一个不需要条件编译的vmlinux.lds。

入门在哪里?从vmlinux.lds中ENTRY(stext)可以知道入口符号是stext,在SI中搜索这个符号,发现在arch/arm/kernel/head.S 和head-nommu.S中都有。

head.S是启用了MMU情况下的kernel启动文件,相当于uboot中的start.S。head-nommu.S是未使用mmu情况下的kernel启动文件。

2.head.S文件分析1

内核运行的物理地址与虚拟地址

KERNEL_RAM_VADDR(VADDR就是virtual address),这个宏定义了内核运行时的虚拟地址。值为0xC0008000

KERNEL_RAM_PADDR(PADDR就是physical address),这个宏定义内核运行时的物理地址。值为0x30008000

总结:内核运行的物理地址是0x30008000,对应的虚拟地址是0xC0008000。

#define KERNEL_RAM_VADDR    (PAGE_OFFSET + TEXT_OFFSET)    //内核运行的虚拟地址 c0000000+8000

#define KERNEL_RAM_PADDR    (PHYS_OFFSET + TEXT_OFFSET)    //内核运行的物理地址 30000000+8000


内核的真正入口

内核的真正入口就是ENTRY(stext)处

前面的__HEAD定义了后面的代码属于段名为.head.text的段

 __HEAD                /* .section '.head.text','ax'  用户自定义段 */

ENTRY(stext)        //内核入口

    setmode    PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode    //把中断快速中断禁掉,设置SVC模式


内核运行的硬件条件

内核的起始部分代码是被解压代码调用的。回忆之前讲zImage的时候,uboot启动内核后实际调用运行的是zImage前面的那段未经压缩的解压代码,解压代码运行时先将zImage后段的内核解压开,然后再去调用运行真正的内核入口。

内核启动是有一定的先决条件,这个条件由启动内核的bootloader(我们这里就是uboot)来构建保证。(The requirements * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0, * r1 = machine nr, r2 = atags pointer.)


/*

 * Kernel startup entry point.

 * ---------------------------

 *

 * This is normally called from the decompressor code.  The requirements

 * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,

 * r1 = machine nr, r2 = atags pointer.

 *

 * This code is mostly position independent, so if you link the kernel at

 * 0xc0008000, you call this at __pa(0xc0008000).

 *

 * See linux/arch/arm/tools/mach-types for the complete list of machine

 * numbers for r1.

 *

 * We're trying to keep crap to a minimum; DO NOT add any machine specific

 * crap here - that's what the boot loader (or in extreme, well justified

 * circumstances, zImage) is for.

 */


ARM体系中,函数调用时实际是通过寄存器传参的(函数调用时传参有两种设计:一种是寄存器传参,另一种是栈内存传参)。所以uboot中最后 theKernel (0, machid, bd->bi_boot_params);执行内核时,运行时实际把0放入r0中,machid放入到了r1中,bd->bi_boot_params放入到了r2中。ARM的这种处理技巧刚好满足了kernel启动的条件和要求。

kernel启动时MMU是关闭的,因此硬件上需要的是物理地址。但是内核是一个整体(zImage)只能被连接到一个地址(不能分散加载),这个连接地址肯定是虚拟地址。因此内核运行时前段head.S中尚未开启MMU之前的这段代码就很难受。所以这段代码必须是位置无关码,而且其中涉及到操作硬件寄存器等时必须使用物理地址。__pa()自动将虚拟地址转变成物理地址

 

内核启动要求的传参方式


3.内核启动的汇编阶段

s内核启动汇编部分程序如下


 __HEAD                /* .section '.head.text','ax'  用户自定义段 */

ENTRY(stext)        //内核入口

    setmode    PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode    //把中断快速中断禁掉,设置SVC模式

                        @ and irqs disabled

    mrc    p15, 0, r9, c0, c0        @ get processor id

    bl    __lookup_processor_type        @ r5=procinfo r9=cpuid 校验处理器ID

    movs    r10, r5                @ invalid processor (r5=0)?

    beq    __error_p            @ yes, error 'p'

    bl    __lookup_machine_type        @ r5=machinfo                         校验机器码

    movs    r8, r5                @ invalid machine (r5=0)?

    beq    __error_a            @ yes, error 'a'

    bl    __vet_atags        @ 校验传参

    bl    __create_page_tables        //建立段式页表(粗页表)


    /*

     * The following calls CPU specific code in a position independent

     * manner.  See arch/arm/mm/proc-*.S for details.  r10 = base of

     * xxx_proc_info structure selected by __lookup_machine_type

     * above.  On return, the CPU will be ready for the MMU to be

     * turned on, and r0 will hold the CPU control register value.

     */

    ldr    r13, __switch_data        @ address to jump to after        @

                        @ mmu has been enabled

    adr    lr, BSYM(__enable_mmu)        @ return (PIC) address

 ARM(    add    pc, r10, #PROCINFO_INITFUNC    )

 THUMB(    add    r12, r10, #PROCINFO_INITFUNC    )

 THUMB(    mov    pc, r12                )

ENDPROC(stext)


__lookup_processor_type

我们从cp15协处理器的c0寄存器中读取出硬件的CPU ID号,然后调用这个函数来进行合法性检验。如果合法则继续启动,如果不合法则停止启动,转向__error_p启动失败。

该函数检验cpu id的合法性方法是:内核会维护一个本内核支持的CPU ID号码的数组,然后该函数所做的就是将从硬件中读取的cpu id号码和数组中存储的各个id号码依次对比,如果没有一个相等则不合法,如果有一个相等的则合法。

内核启动时设计这个校验,也是为了内核启动的安全性着想。


__lookup_processor_type:

    adr    r3, 3f

    ldmia    r3, {r5 - r7}

    add    r3, r3, #8

    sub    r3, r3, r7            @ get offset between virt&phys

    add    r5, r5, r3            @ convert virt addresses to

    add    r6, r6, r3            @ physical address space

1:    ldmia    r5, {r3, r4}            @ value, mask

    and    r4, r4, r9            @ mask wanted bits

    teq    r3, r4

    beq    2f

    add    r5, r5, #PROC_INFO_SZ        @ sizeof(proc_info_list)

    cmp    r5, r6

    blo    1b

    mov    r5, #0                @ unknown processor

2:    mov    pc, lr

ENDPROC(__lookup_processor_type)


__lookup_machine_type

该函数的设计理念和思路和上面校验cpu id的函数一样的。不同之处是本函数校验的是机器码。


__lookup_machine_type:

    adr    r3, 4b

    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

1:    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, #0                @ unknown machine

2:    mov    pc, lr

ENDPROC(__lookup_machine_type)


__vet_atags

该函数的设计理念和思路和上面2个一样,不同之处是用来校验uboot给内核的传参ATAGS格式是否正确。这里说的传参指的是uboot通过tag给内核传的参数(主要是板子的内存分布memtag、uboot的bootargs)

内核认为如果uboot给我的传参格式不正确,那么我就不启动。

uboot给内核传参的部分如果不对,是会导致内核不启动。譬如uboot的bootargs设置不正确内核可能就会不启动。


__vet_atags:

    tst    r2, #0x3            @ aligned?

    bne    1f


    ldr    r5, [r2, #0]            @ is first tag ATAG_CORE?

    cmp    r5, #ATAG_CORE_SIZE

    cmpne    r5, #ATAG_CORE_SIZE_EMPTY

    bne    1f

    ldr    r5, [r2, #4]

    ldr    r6, =ATAG_CORE

    cmp    r5, r6

    bne    1f


    mov    pc, lr                @ atag pointer is ok


1:    mov    r2, #0

    mov    pc, lr

ENDPROC(__vet_atags)


__create_page_tables

顾名思义,这个函数用来建立页表。

linux内核本身被连接在虚拟地址处,因此kernel希望尽快建立页表并且启动MMU进入虚拟地址工作状态。但是kernel本身工作起来后页表体系是非常复杂的,建立起来也不是那么容易的。kernel想了一个好办法

kernel建立页表其实分为2步。第一步,kernel先建立了一个段式页表(和uboot中之前建立的页表一样,页表以1MB为单位来区分的),这里的函数就是建立段式页表的。段式页表本身比较好建立(段式页表1MB一个映射,4GB空间需要4096个页表项,每个页表项4字节,因此一共需要16KB内存来做页表),坏处是比较粗不能精细管理内存;第二步,再去建立一个细页表(4kb为单位的细页表),然后启用新的细页表废除第一步建立的段式映射页表。

内核启动的早期建立段式页表,并在内核启动前期使用;内核启动后期就会再次建立细页表并启用。等内核工作起来之后就只有细页表了。

__switch_data

建立了段式页表后进入了 __switch_data 部分,这东西是个函数指针数组。

[1] [2] [3]
关键字:内核  启动过程  s5pv210 引用地址:3、内核的启动过程

上一篇:4、移植三星官方内核
下一篇:2、内核的配置和移植

小广播
最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

厂商技术中心

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

 
机器人开发圈

电子工程世界版权所有 京ICP证060456号 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2026 EEWORLD.com.cn, Inc. All rights reserved