`
isiqi
  • 浏览: 16466089 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

Linux 内核启动分析

阅读更多

Linux 内核启动分析

1. 内核启动地址

1.1. 名词解释

ZTEXTADDR

解压代码运行的开始地址。没有物理地址和虚拟地址之分,因为此时MMU处于关闭状态。这个地址不一定时RAM的地址,可以是支持读写寻址的flash等存储中介。

Start address of decompressor. here's no point in talking about virtual or physical addresses here, since the MMU will be off at the time when you call the decompressor code. You normally call the kernel at this address to start it booting. This doesn't have to be located in RAM, it can be in flash or other read-only or read-write addressable medium.

ZRELADDR

内核启动在RAM中的地址。压缩的内核映像被解压到这个地址,然后执行。

This is the address where the decompressed kernel will be written, and eventually executed. The following constraint must be valid:

__virt_to_phys(TEXTADDR) == ZRELADDR

The initial part of the kernel is carefully coded to be position independent.

TEXTADDR

内核启动的虚拟地址,与ZRELADDR相对应。一般内核启动的虚拟地址为RAM的第一个bank地址加上0x8000。

TEXTADDR = PAGE_OFFSET + TEXTOFFST

Virtual start address of kernel, normally PAGE_OFFSET + 0x8000.This is where the kernel image ends up. With the latest kernels, it must be located at 32768 bytes into a 128MB region. Previous kernels placed a restriction of 256MB here.

TEXTOFFSET

内核偏移地址。在arch/arm/makefile中设定。

PHYS_OFFSET

RAM第一个bank的物理起始地址。

Physical start address of the first bank of RAM.

PAGE_OFFSET

RAM第一个bank的虚拟起始地址。

Virtual start address of the first bank of RAM. During the kernel

boot phase, virtual address PAGE_OFFSET will be mapped to physical

address PHYS_OFFSET, along with any other mappings you supply.

This should be the same value as TASK_SIZE.

1.2. 内核启动地址确定

内核启动引导地址由bootp.lds决定。 Bootp.lds : arch/arm/bootp

OUTPUT_ARCH(arm)

ENTRY(_start)

SECTIONS

{

. = 0;

.text : {

_stext = .;

*(.start)

*(.text)

initrd_size = initrd_end - initrd_start;

_etext = .;

}

}

由上 .= 0可以确定解压代码运行的开始地址在0x0的位置。ZTEXTADDR的值决定了这个值得选取。

Makefile : arch/arm/boot/compressed

如果设定内核从ROM中启动的话,可以在make menuconfig 的配置界面中设置解压代码的起始地址,否则解压代码的起始地址为0x0。实际上,默认从ROM启动时,解压代码的起始地址也是0x0。

feq ($(CONFIG_ZBOOT_ROM),y)

ZTEXTADDR := $(CONFIG_ZBOOT_ROM_TEXT)

ZBSSADDR := $(CONFIG_ZBOOT_ROM_BSS)

else

ZTEXTADDR :=0 ZBSSADDR := ALIGN(4)

endif

SEDFLAGS = s/TEXT_START/$(ZTEXTADDR)/;s/BSS_START/$(ZBSSADDR)/

……

$(obj)/vmlinux.lds: $(obj)/vmlinux.lds.in arch/arm/mach-s3c2410/Makefile .config

@sed "$(SEDFLAGS)" < $< > $@

@sed "$(SEDFLAGS)" < $< > $@ 规则将TEXT_START设定为ZTEXTADDR。TEXT_START在arch/arm/boot/compressed/vmlinux.lds.in 中被用来设定解压代码的起始地址。

OUTPUT_ARCH(arm)

ENTRY(_start)

SECTIONS

{

. = TEXT_START;

_text = .;

.text : {

_start = .;

*(.start)

*(.text)

*(.text.*)

……

}

}

内核的编译依靠vmlinux.lds,vmlinux.lds由vmlinux.lds.s 生成。从下面代码可以看出内核启动的虚拟地址被设置为PAGE_OFFSET + TEXT_OFFSET,而内核启动的物理地址ZRELADDR在arch/arm/boot/Makefile中设定。

OUTPUT_ARCH(arm)

ENTRY(stext)

SECTIONS

{

#ifdef CONFIG_XIP_KERNEL

. = XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);

#else

. = PAGE_OFFSET + TEXT_OFFSET;

#endif

.init : { /* Init code and data */

_stext = .;

_sinittext = .;

*(.init.text)

_einittext = .;

……

}

}

# arch/arm/boot/Makefile

# Note: the following conditions must always be true:

# ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET)

# PARAMS_PHYS must be within 4MB of ZRELADDR

# INITRD_PHYS must be in RAM

ZRELADDR := $(zreladdr-y)

#---> zrealaddr-y is specified with 0x30008000 in arch/arm/boot/makefile.boot

PARAMS_PHYS := $(params_phys-y)

INITRD_PHYS := $(initrd_phys-y)

export ZRELADDR INITRD_PHYS PARAMS_PHYS

通过下面的命令编译内核映像,由参数-a, -e设置其入口地址为ZRELADDR,此值在上面ZRELADDR := $(zreladdr-y)指定。

quiet_cmd_uimage= UIMAGE $@

cmd_uimage = $(CONFIG_SHELL) $(MKIMAGE) -A arm -O linux -T kernel \

-C none -a $(ZRELADDR) -e $(ZRELADDR) \

-n 'Linux-$(KERNELRELEASE)' -d $< $@

1.3. 小结

从上面分析可知道,linux内核被bootloader拷贝到RAM后,解压代码从ZTEXTADDR开始运行(这段代码是与位置无关的PIC)。内核被解压缩到ZREALADDR处,也就是内核启动的物理地址处。相应地,内核启动的虚拟地址被设定为TEXTADDR,满足如下条件:

TEXTADDR = PAGE_OFFSET + TEXT_OFFSET

内核启动的物理地址和虚拟地址满足入下条件:

ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET)= virt_to_phys(TEXTADDR)

假定开发板为smdk2410,则有:

内核启动的虚拟地址

TEXTADDR = 0xC0008000

内核启动的物理地址

ZRELADDR = 0x30008000

如果直接从flash中启动还需要设置ZTEXTADDR地址。

2. 内核启动过程分析

内核启动过程经过大体可以分为两个阶段:内核映像的自引导;linux内核子模块的初始化。

clip_image002

2.1. 内核映像的自引导

这阶段的主要工作是实现压缩内核的解压和进入内核代码的入口。

Bootloader完成系统引导后,内核映像被调入内存指定的物理地址ZTEXTADDR。典型的内核映像由自引导程序和压缩的VMlinux组成。因此在启动内核之前需要先把内核解压缩。内核映像的入口的第一条代码就是自引导程序。它在arch/arm/boot/compressed/head.S文件中。

Head.S文件主要功能是实现压缩内核的解压和跳转到内核vmlinux内核的入口。Decompress_kernel(): arch/arm/boot/compressed/misc.c 和call_kernel这两个函数实现了上述功能。在调用decompress_kernel()解压内核之前,需要确保解压后的内核代码不会覆盖掉原来的内核映像。以及设定内核代码的入口地址ZREALADDR。

.text

adr r0, LC0

ldmia r0, {r1, r2, r3, r4, r5, r6, ip, sp}

.type LC0, #object

LC0: .word LC0 @ r1

.word __bss_start @ r2

.word _end @ r3

.word zreladdr @ r4

.word _start @ r5

.word _got_start @ r6

.word _got_end @ ip

.word user_stack+4096 @ sp

上面这段代码得到内核代码的入口地址,保存在r4中。

/*

* Check to see if we will overwrite ourselves.

* r4 = final kernel address

* r5 = start of this image

* r2 = end of malloc space (and therefore this image)

* We basically want:

* r4 >= r2 -> OK

* r4 + image length <= r5 -> OK

*/

cmp r4, r2

bhs wont_overwrite

add r0, r4, #4096*1024 @ 4MB largest kernel size

cmp r0, r5

bls wont_overwrite

mov r5, r2 @ decompress after malloc space

mov r0, r5

mov r3, r7

bl decompress_kernel

b call_kernel

上面代码判断解压后的内核代码会不会覆盖原来的内核映像,然后调用内核解压缩函数decompress_kernel()。

ulg

decompress_kernel(ulg output_start, ulg free_mem_ptr_p, ulg free_mem_ptr_end_p,

int arch_id)

{

output_data = (uch *)output_start; /* 指定内核执行地址,保存在r4中*/

free_mem_ptr = free_mem_ptr_p;

free_mem_ptr_end = free_mem_ptr_end_p;

__machine_arch_type = arch_id;

arch_decomp_setup(); /*解压缩前的初始化和设置,包括串口波特率设置等*/

makecrc(); /*CRC校验*/

putstr("Uncompressing Linux...");

gunzip(); /*调用解压缩函数*/

putstr(" done, booting the kernel.\n");

return output_ptr;

}

把内核映像解压到ZERALADDR地址后,调用call_kernel函数进入内核代码的入口地址。

call_kernel: bl cache_clean_flush

bl cache_off

mov r0, #0 @ must be zero

mov r1, r7 @ restore architecture number

mov r2, r8 @ restore atags pointer

mov pc, r4 @ call kernel

我们知道r4寄存器内保存的是内核的执行地址,mov pc, r4使得程序指针指向了内核的执行地址,所以下面将进入内核代码执行阶段。

2.2. linux内核子模块的初始化

2.2.1. 预备工作

进入真正的内核代码,首先执行的也是一个叫做head.S(arch/arm/kernel/)的文件。同时head.S也包含了同目录下head-common.S(arch/arm/kernel/)。这两个文件联合起来主要负责下面几项工作:

clip_image001 判断CPU类型,查找运行的CPU ID值与Linux编译支持的ID值是否支持

clip_image001[1] 判断体系类型,查看R1寄存器的Architecture Type值是否支持

clip_image001[2] 创建页表

clip_image001[3] 开启MMU

clip_image001[4] 跳转到start_kernel()(内核子模块初始化程序)

注: 暂时不对各个子程序实现作细节性的分析。

2.2.2. 内核各子模块初始化

Start_kernel函数是Linux内核通用的初始化函数。无论对于什么体系结构的Linux,都要执行这个函数。Start_kernel()函数是内核初始化的基本过程。下面按照函数对内核模块初始化的先后顺序进行分析。

asmlinkage void __init start_kernel(void)

{

char * command_line;

extern struct kernel_param __start___param[], __stop___param[];

smp_setup_processor_id(); /*指定当前的cpu的逻辑号,这个函数对应于对称多处理器的设置,当系统中只有一个cpu的情况,此函数为空,什么也不做*/

lockdep_init(); /* 初始化lockdep hash 表 */

/* 初始化irq */

local_irq_disable();

early_boot_irqs_off();

early_init_irq_lock_class();

/* 锁定内核、设置cpu的状态为’present’,’online’等状态、初始化页表、打印内核版本号等信息、设置体系结构、为cpu分配启动内存空间 ,在此期间中断仍然处于关闭状态*/

lock_kernel();

boot_cpu_init();

page_address_init();

printk(KERN_NOTICE);

printk(linux_banner);

setup_arch(&command_line);

setup_per_cpu_areas();

smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */

/*在打开任何中断之前打开调度器 */

sched_init();

/*关闭任务抢占功能,因为早期的调度器功能比较脆弱,直到第一次调用cpu_idle()*/

preempt_disable();

/* 建立内存区域链表节点,对于单cpu节点数为1 */

build_all_zonelists();

/* 发通知给每个CPU,处理每个CPU的内存状态*/

page_alloc_init();

/* 分析早期没命令参数*/

printk(KERN_NOTICE "Kernel command line: %s\n", saved_command_line);

parse_early_param();

/* 分析命令参数 */

parse_args("Booting kernel", command_line, __start___param,

__stop___param - __start___param,

&unknown_bootoption);

/* 排序内核创建的异常表 */

sort_main_extable();

unwind_init();

/*设置陷阱门和中断门 */

trap_init();

/*初始化内核中的读-拷贝-更新(Read-Copy-Update RCU)子系统 */

rcu_init();

/*初始化IRQ */

init_IRQ();

/* 按照开发办上的物理内存初始化pid hash表 */

pidhash_init();

/*初始化计时器 */

init_timers();

/* 高解析度&高精度的计时器 (high resolution)初始化 */

hrtimers_init();

/*初始化软中断 */

softirq_init();

/* 初始化时钟资源和普通计时器的值 */

timekeeping_init();

/* 初始化系统时间*/

time_init();

/*为内核分配内存以存储收集的数据*/

profile_init();

/* 开中断 */

if (!irqs_disabled())

printk("start_kernel(): bug: interrupts were enabled early\n");

early_boot_irqs_on();

local_irq_enable();

/*

* HACK ALERT! This is early. We're enabling the console before

* we've done PCI setups etc, and console_init() must be aware of

* this. But we do want output early, in case something goes wrong.

*/

/* 初始化控制台,为了能够尽早地帮助调试,显示系统引导的信息*/

console_init();

if (panic_later)

panic(panic_later, panic_param);

/*如果定义了CONFIG_LOCKDEP宏,则打印锁依赖信息,否则什么也不做 */

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:

*/

/* 如果定义CONFIG_DEBUG_LOCKING_API_SELFTESTS宏,则locking_selftest()是一个空函数,否则执行锁自测*/

locking_selftest();

#ifdef CONFIG_BLK_DEV_INITRD

if (initrd_start && !initrd_below_start_ok &&

initrd_start < min_low_pfn << PAGE_SHIFT) {

printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "

"disabling it.\n",initrd_start,min_low_pfn << PAGE_SHIFT);

initrd_start = 0;

}

#endif

/*

(1)dcache_init()创建SLAB缓存,该缓存保存目录项描述符。传存本身被称作dentry_cache。当进程访问文件或目录时所涉及的目录名有多个目录分量组成,目录项描述符就是针对每个分量而创建的。目录项各结构把文件或目录分量与其索引结点结合起来,因而可以通过该目录项可以更快地找到与其对应的索引结点

(2)inode_init()初始化哈希表索引结点和等待队列对头,该队头存放内核要锁存的哈希索引结点。

(3)file_init()确定给每个进程呢个zhogn的文件所分配的最大内存量

(4)mnt_init()创建了保存vfsmount对象且名为mnt_cache的缓存,VFS利用这协对吸纳给来挂载文件系统。该例程也创建mount_hashtable队列,该队列存放mnt_cache中引用的快速访问对象。然后该例程发出调用来初始化sysfs文件系统并挂载root文件系统。

*/

vfs_caches_init_early();

cpuset_init_early();

/* Mem_init()函数为mem_map中的自由区作标记并且打印出自由内存的大小。这个函数在系统的各个部分申请过内存后执行 */

mem_init();

/* 初始化cache相关的链表,函数在初始化页表分配器后smp_init()之前执行 */

kmem_cache_init();

/* 为逻辑号为0的cpu初始化页面。如果是smp情况下,只要cpu表现为online态,此函数就会执行 */

setup_per_cpu_pageset();

/* numa内存策略器初始化 */

numa_policy_init();

/* 内存初始化后调用 */

if (late_time_init)

late_time_init();

/*计算并打印许多著名的"BogoMips"的值,该值度量处理器在一个时钟节拍内可以反复执行多少个delay().对不同速度的处理器,cali_brate_delay()允许的延迟大约相同*/

calibrate_delay();

/* 初始化pidmap_array,分配pid=0给当前进程 */

pidmap_init();

/* 初始化页表高速缓存 */

pgtable_cache_init();

/*初始化优先级树index_bits_to_maxindex数组*/

prio_tree_init();

/*创建anon_vma结构对象slab缓存*/

anon_vma_init();

#ifdef CONFIG_X86

if (efi_enabled)

efi_enter_virtual_mode();

#endif

/*根据可用内存大小来建立用户缓冲区uid_cache,初始化最大线程数max_threads,为init_task配置RLIMIT_NPROC的值为max_threads/2 */

fork_init(num_physpages);

/*建立各种块缓冲区,比如VFS, VM等*/

proc_caches_init();

buffer_init();

unnamed_dev_init();

key_init();

security_init();

vfs_caches_init(num_physpages);

radix_tree_init();

signals_init();

/* rootfs populating might need page-writeback */

page_writeback_init();

#ifdef CONFIG_PROC_FS

proc_root_init();

#endif

cpuset_init();

taskstats_init_early();

delayacct_init();

/*检查错误,其实是调用check_writebuffer_bugs()函数检查是否有物理地址混淆的现象 */

check_bugs();

/* advanced configuration and power management interface */

acpi_early_init(); /* before LAPIC and SMP init */

/* Do the rest non-__init'ed, we're now alive */

/* 创建init进程,删除内核锁,启动idle线程 */

rest_init();

}

进入init进程后,将执行init()函数负责完成挂接根文件系统、初始化设备驱动和启动用户空间的init进程。(sunny 负责研究这部分)

分享到:
评论

相关推荐

    linux内核启动分析

    ### Linux内核启动过程分析 #### 一、概述 Linux内核启动是嵌入式Linux系统启动的关键步骤之一。在嵌入式系统中,Linux系统的软件结构主要包括四个部分:Bootloader、Linux内核、文件系统以及应用程序。本文将对...

    Linux内核启动过程分析.pdf

    ### Linux内核启动过程分析 #### 一、引言 随着技术的发展,Linux作为一个免费开源的Unix类操作系统,因其强大的功能、高效的性能以及良好的可移植性,在嵌入式系统乃至高性能服务器领域获得了广泛应用。理解Linux...

    ARM-Linux内核启动的分析

    ### ARM-Linux内核启动分析 #### 一、引言 ARM-Linux内核启动是一个复杂而精细的过程,本文将重点分析`arch/arm/kernel/head-armv.S`文件,该文件作为整个内核启动的入口点,在Bootloader执行完毕后直接控制着后续...

    linux2.6内核启动分析

    linux2.6内核启动分析

    linux内核启动流程分析及移植步骤

    linux内核启动流程分析及移植步骤(需要Mindjet MindManager软件打开)

    linux内核启动流程1

    Linux 内核启动流程分析 在 Linux 内核启动流程中,内核自解压过程是非常重要的一步。这个过程中,uboot 将 uImage 头部进行解析,并将里面的 zImage 搬移到指定的内存位置,然后跳转到该内存位置处开始执行。 在 ...

    Linux内核启动过程分析

    ### Linux内核启动过程分析 #### 一、引言 随着技术的发展,Linux作为一个免费开源的操作系统,在嵌入式系统到高性能服务器等多个领域都获得了广泛的应用。Linux内核以其高效、稳定、可移植性强等特点备受青睐。...

    LINUX内核分析课件

    湘潭大学信息工程学院,lINUX内核分析课件,全部章节在内,包括复习要点

    Linux内核源码分析--内核启动之(1)zImage自解压过程(Linux-3.0 ARMv7)1

    zImage自解压过程是Linux内核启动过程中的一个重要步骤,涉及到Bootloader的准备、zImage的生成和工作原理、MMU的设置等方面。通过对zImage自解压过程的分析,可以更好地理解Linux内核的启动过程,并为后续的内核...

    linux内核启动过程

    ### Linux内核启动过程 #### 一、引言 Linux作为一个免费开源的Unix类操作系统,在全球范围内得到了广泛应用,从嵌入式系统到超级服务器均有其身影。Linux内核以其紧凑的结构、强大的功能、高效的性能以及优秀的可...

    linux内核源代码情景分析 pdf版 高清

    《Linux内核源代码情景分析》是一本深入探讨Linux内核源码的权威书籍,它为读者揭示了操作系统内核的奥秘。本书通过详细的情景分析,将复杂的内核概念与实际应用相结合,帮助读者逐步理解Linux内核的工作原理。 在...

    Linux内核源代码分析视频课-视频教程网盘链接提取码下载.txt

    本课程从理解计算机硬件的核心工作机制...9.跟踪调试Linux内核的启动过程 10.用户态、内核态和中断 11.系统调用概述 12.使用库函数API和C代码中嵌入汇编代码触发同一个系统调用 13.给MenuOS增加time和time-asm命令

    这是一篇对armlinux内核启动的分析.pdf

    Linux内核启动分析 本文对 ARM Linux 内核启动过程进行了深入分析,重点介绍了 head-armv.S 文件中的代码实现细节。head-armv.S 文件是内核的入口点,负责初始化内核空间、设置寄存器值、创建页表等任务。 1. 介绍...

Global site tag (gtag.js) - Google Analytics