ARM linux启动分析

来源:本站
导读:目前正在解读《ARM linux启动分析》的相关信息,《ARM linux启动分析》是由用户自行发布的知识型内容!下面请观看由(电工技术网 - www.9ddd.net)用户发布《ARM linux启动分析》的详细说明。
简介:我分析的是2.4.19的内核版本,是xscale的平台,参考了网上很多有价值的帖子,也加入了自己的一些看法,陆续总结成文字,内核一般是由bootloader来引导的,通过bootloader启动内核一般要传递三个参数,bootloader首先要将ramdisk(如果有)和内核拷贝到ram当中,然后可以通过c语言的模式启动内核.

linux启动分析(1)---bootloader启动内核过程

我分析的是2.4.19的内核版本,是xscale的平台,参考了网上很多有价值的帖子,也加入了自己的一些看法,

陆续总结成文字,今天是第一篇:

内核一般是由bootloader来引导的,通过bootloader启动内核一般要传递三个参数,

第一个参数放在寄存器0中,一般都为0,r0 = 0;

第二个参数放在寄存器1中,是机器类型id,r1 = Machine Type Number;

第三个参数放在寄存器2中,是启动参数标记列表在ram中的起始基地址;

bootloader首先要将ramdisk(如果有)和内核拷贝到ram当中,然后可以通过c语言的模式启动内核:

void (*startkernel)(int zero, int arch,unsigned intparams_addr) = (void(*)(int, int, unsigned int))KERNEL_RAM_BASE;

startkernel(0, ARCH_NUMBER, (unsigned int)kernel_params_start);

其中KERNEL_RAM_BASE为内核在ram中启动的地址,ARCH_NUMBER是Machine Type Number,kernel_params_start是参数在ram的偏移地址。

这时候就将全力交给了内核。

linux启动分析(2)---内核启动地址的确定

内核编译链接过程是依靠vmlinux.lds文件,以arm为例vmlinux.lds文件位于kernel/arch/arm/vmlinux.lds,

但是该文件是由vmlinux-armv.lds.in生成的,根据编译选项的不同源文件还可以是vmlinux-armo.lds.in,

vmlinux-armv-xip.lds.in。

vmlinux-armv.lds的生成过程在kernel/arch/arm/Makefile中

LDSCRIPT = arch/arm/vmlinux-armv.lds.in

arch/arm/vmlinux.lds: arch/arm/Makefile $(LDSCRIPT) /

$(wildcard include/config/cpu/32.h) /

$(wildcard include/config/cpu/26.h) /

$(wildcard include/config/arch/*.h)

@echo ' Generating$@'

@sed 's/TEXTADDR/$(TEXTADDR)/;s/DATAADDR/$(DATAADDR)/' $(LDSCRIPT) >$@

vmlinux-armv.lds.in文件的内容:

OUTPUT_ARCH(arm)

ENTRY(stext)

SECTIONS

{

. = TEXTADDR;

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

_stext = .;

__init_begin = .;

*(.text.init)

__proc_info_begin = .;

*(.proc.info)

__proc_info_end = .;

__arch_info_begin = .;

*(.arch.info)

__arch_info_end = .;

__tagtable_begin = .;

*(.taglist)

__tagtable_end = .;

*(.data.init)

. = ALIGN(16);

__setup_start = .;

*(.setup.init)

__setup_end = .;

__initcall_start = .;

*(.initcall.init)

__initcall_end = .;

. = ALIGN(4096);

__init_end = .;

}

其中TEXTADDR就是内核启动的虚拟地址,定义在kernel/arch/arm/Makefile中:

ifeq ($(CONFIG_CPU_32),y)

PROCESSOR = armv

TEXTADDR = 0xC0008000

LDSCRIPT = arch/arm/vmlinux-armv.lds.in

endif

需要注意的是这里是虚拟地址而不是物理地址。

一般情况下都在生成vmlinux后,再对内核进行压缩成为zImage,压缩的目录是kernel/arch/arm/boot。

下载到flash中的是压缩后的zImage文件,zImage是由压缩后的vmlinux和解压缩程序组成,如下图所示:

|-----------------|/ |-----------------|

| | / | |

| | / | decompress code |

| vmlinux | / |-----------------| zImage

| | /| |

| | | |

| | | |

| | | |

| | /|-----------------|

| | /

| | /

| | /

|-----------------|/

zImage链接脚本也叫做vmlinux.lds,位于kernel/arch/arm/boot/compressed。

是由同一目录下的vmlinux.lds.in文件生成的,内容如下:

OUTPUT_ARCH(arm)

ENTRY(_start)

SECTIONS

{

. = LOAD_ADDR;

_load_addr = .;

. = TEXT_START;

_text = .;

.text : {

_start = .;

其中LOAD_ADDR就是zImage中解压缩代码的ram偏移地址,TEXT_START是内核ram启动的偏移地址,这个地址是物理地址。

在kernel/arch/arm/boot/Makefile文件中定义了:

ZTEXTADDR =0

ZRELADDR = 0xa0008000

ZTEXTADDR就是解压缩代码的ram偏移地址,ZRELADDR是内核ram启动的偏移地址,这里看到指定ZTEXTADDR的地址为0,

明显是不正确的,因为我的平台上的ram起始地址是0xa0000000,在Makefile文件中看到了对该地址设置的几行注释:

# We now have a PIC decompressor implementation. Decompressors running

# from RAM should not define ZTEXTADDR. Decompressors running directly

# from ROM or Flash must define ZTEXTADDR (preferably via the config)

他的意识是如果是在ram中进行解压缩时,不用指定它在ram中的运行地址,如果是在flash中就必须指定他的地址。所以

这里将ZTEXTADDR指定为0,也就是没有真正指定地址。

在kernel/arch/arm/boot/compressed/Makefile文件有一行脚本:

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

使得TEXT_START = ZTEXTADDR,LOAD_ADDR = ZRELADDR。

这样vmlinux.lds的生成过程如下:

vmlinux.lds: vmlinux.lds.in Makefile $(TOPDIR)/arch/$(ARCH)/boot/Makefile $(TOPDIR)/.config

@sed "$(SEDFLAGS)" < vmlinux.lds.in > $@

以上就是我对内核启动地址的分析,总结一下内核启动地址的设置:

1、设置kernel/arch/arm/Makefile文件中的

TEXTADDR = 0xC0008000

内核启动的虚拟地址

2、设置kernel/arch/arm/boot/Makefile文件中的

ZRELADDR = 0xa0008000

内核启动的物理地址

如果需要从flash中启动还需要设置

ZTEXTADDR地址。

linux启动分析(3)---内核解压缩过程

内核压缩和解压缩代码都在目录kernel/arch/arm/boot/compressed,

编译完成后将产生vmlinux、head.o、misc.o、head-xscale.o、piggy.o这几个文件,

head.o是内核的头部文件,负责初始设置;

misc.o将主要负责内核的解压工作,它在head.o之后;

head-xscale.o文件主要针对Xscale的初始化,将在链接时与head.o合并;

piggy.o是一个中间文件,其实是一个压缩的内核(kernel/vmlinux),只不过没有和初始化文件及解压文件链接而已;

vmlinux是(没有--lw:zImage是压缩过的内核)压缩过的内核,就是由piggy.o、head.o、misc.o、head-xscale.o组成的。

在BootLoader完成系统的引导以后并将Linux内核调入内存之后,调用bootLinux(),

这个函数将跳转到kernel的起始位置。如果kernel没有压缩,就可以启动了。

如果kernel压缩过,则要进行解压,在压缩过的kernel头部有解压程序。

压缩过得kernel入口第一个文件源码位置在arch/arm/boot/compressed/head.S。

它将调用函数decompress_kernel(),这个函数在文件arch/arm/boot/compressed/misc.c中,

decompress_kernel()又调用proc_decomp_setup(),arch_decomp_setup()进行设置,

然后使用在打印出信息“Uncompressing Linux...”后,调用gunzip()。将内核放于指定的位置。

以下分析head.S文件:

(1)对于各种ArmCPU的DEBUG输出设定,通过定义宏来统一操作。

(2)设置kernel开始和结束地址,保存architecture ID。

(3)如果在ARM2以上的CPU中,用的是普通用户模式,则升到超级用户模式,然后关中断。

(4)分析LC0结构delta offset,判断是否需要重载内核地址(r0存入偏移量,判断r0是否为零)。

这里是否需要重载内核地址,我以为主要分析arch/arm/boot/Makefile、arch/arm/boot/compressed/Makefile

和arch/arm/boot/compressed/vmlinux.lds.in三个文件,主要看vmlinux.lds.in链接文件的主要段的位置,

LOAD_ADDR(_load_addr)=0xA0008000,而对于TEXT_START(_text、_start)的位置只设为0,BSS_START(__bss_start)=ALIGN(4)。

对于这样的结果依赖于,对内核解压的运行方式,也就是说,内核解压前是在内存(RAM)中还是在FLASH上,

因为这里,我们的BOOTLOADER将压缩内核(zImage)移到了RAM的0xA0008000位置,我们的压缩内核是在内存(RAM)从0xA0008000地址开始顺序排列,

因此我们的r0获得的偏移量是载入地址(0xA0008000)。接下来的工作是要把内核镜像的相对地址转化为内存的物理地址,即重载内核地址。

(5)需要重载内核地址,将r0的偏移量加到BSS region和GOT table中。

(6)清空bss堆栈空间r2-r3。

(7)建立C程序运行需要的缓存,并赋于64K的栈空间。

(8)这时r2是缓存的结束地址,r4是kernel的最后执行地址,r5是kernel境象文件的开始地址。检查是否地址有冲突。

将r5等于r2,使decompress后的kernel地址就在64K的栈之后。

(9)调用文件misc.c的函数decompress_kernel(),解压内核于缓存结束的地方(r2地址之后)。此时各寄存器值有如下变化:

r0为解压后kernel的大小

r4为kernel执行时的地址

r5为解压后kernel的起始地址

r6为CPU类型值(processor ID)

r7为系统类型值(architecture ID)

(10)将reloc_start代码拷贝之kernel之后(r5+r0之后),首先清除缓存,而后执行reloc_start。

(11)reloc_start将r5开始的kernel重载于r4地址处。

(12)清除cache内容,关闭cache,将r7中architecture ID赋于r1,执行r4开始的kernel代码。

下面简单介绍一下解压缩过程,也就是函数decompress_kernel实现的功能:

解压缩代码位于kernel/lib/inflate.c,inflate.c是从gzip源程序中分离出来的。包含了一些对全局数据的直接引用。

在使用时需要直接嵌入到代码中。gzip压缩文件时总是在前32K字节的范围内寻找重复的字符串进行编码,

在解压时需要一个至少为32K字节的解压缓冲区,它定义为window[WSIZE]。inflate.c使用get_byte()读取输入文件,

它被定义成宏来提高效率。输入缓冲区指针必须定义为inptr,inflate.c中对之有减量操作。inflate.c调用flush_window()

来输出window缓冲区中的解压出的字节串,每次输出长度用outcnt变量表示。在flush_window()中,还必

须对输出字节串计算CRC并且刷新crc变量。在调用gunzip()开始解压之前,调用makecrc()初始化CRC计算表。

最后gunzip()返回0表示解压成功。

我们在内核启动的开始都会看到这样的输出:

Uncompressing Linux...done, booting the kernel.

这也是由decompress_kernel函数内部输出的,它调用了puts()输出字符串,

puts是在kernel/include/asm-arm/arch-pxa/uncompress.h中实现的。

执行完解压过程,再返回到head.S中,启动内核:

call_kernel: bl cache_clean_flush

bl cache_off

mov r0, #0

mov r1, r7 @ restore architecture number

mov pc, r4 @ call kernel

下面就开始真正的内核了。

linux启动分析(4)---汇编部分(1)

在网上参考很多高手的文章,又加入了自己的一点儿内容,整理了一下,里面还有很多不明白的地方,而且也会有理解错误的地方,望高手指点,自己也会不断进行修改

当进入linux内核后,arch/arm/kernel/head-armv.S是内核最先执行的一个文件,包括从内核入口ENTRY(stext)到

start_kernel之间的初始化代码,下面以我所是用的平台intel pxa270为例,说明一下他的汇编代码:

1 .section ".text.init",#alloc,#execinstr

2 .type stext, #function

/*内核入口点*/

3 ENTRY(stext)

4 mov r12, r0

/*程序状态,禁止FIQ、IRQ,设定SVC模式*/

5 mov r0, #F_BIT | I_BIT | MODE_SVC @ make sure svc mode

6 msr cpsr_c, r0 @ and all irqs disabled

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

7 bl __lookup_processor_type

/*判断如果r10的值为0,则表示函数执行错误,跳转到出错处理,*/

/*出错处理函数__error的实现代码定义在debug-armv.S中,这里就不再作过多介绍了*/

8 teq r10, #0 @ invalid processor?

9 moveq r0, #'p' @ yes, error 'p'

10beq __error

/*判断体系类型,查看R1寄存器的Architecture Type值是否支持*/

11bl __lookup_architecture_type

/*判断如果r7的值为0,则表示函数执行错误,跳转到出错处理,*/

12teq r7, #0 @ invalid architecture?

13moveq r0, #'a' @ yes, error 'a'

14beq __error

/*创建核心页表*/

15bl __create_page_tables

16adr lr, __ret @ return address

17add pc, r10, #12 @ initialise processor

@ (return control reg)

第5行,准备进入SVC工作模式,同时关闭中断(I_BIT)和快速中断(F_BIT)

第7行,查看处理器类型,主要是为了得到处理器的ID以及页表的flags。

第11行,查看一些体系结构的信息。

第15行,建立页表。

第17行,跳转到处理器的初始化函数,其函数地址是从__lookup_processor_type中得到的,

需要注意的是第16行,当处理器初始化完成后,会直接跳转到__ret去执行,

这是由于初始化函数最后的语句是mov pc, lr。

linux启动分析(4)---汇编部分(2)

前面一篇文章,简单介绍了内核启动的汇编主流程,这篇介绍其中调用的汇编子函数__lookup_processor_type

函数__lookup_processor_type介绍:

内核中使用了一个结构struct proc_info_list,用来记录处理器相关的信息,该结构定义在

kernel/include/asm-arm/procinfo.h头文件中。

/*

* Note! struct processor is always defined if we're

* using MULTI_CPU, otherwise this entry is unused,

* but still exists.

*

* NOTE! The following structure is defined by assembly

* language, NOT C code. For more information, check:

* arch/arm/mm/proc-*.S and arch/arm/kernel/head-armv.S

*/

struct proc_info_list {

unsigned int cpu_val;

unsigned int cpu_mask;

unsigned long __cpu_mmu_flags; /* used by head-armv.S */

unsigned long __cpu_flush; /* used by head-armv.S */

const char *arch_name;

const char *elf_name;

unsigned int elf_hwcap;

struct proc_info_item *info;

struct processor *proc;

};

在arch/arm/mm/proc-xscale.S文件中定义了所有和xscale有关的proc_info_list,我们使用的pxa270定义如下:

.section ".proc.info", #alloc, #execinstr

.type __bva0_proc_info,#object

__bva0_proc_info:

.long 0x69054110 @ Bulverde A0: 0x69054110, A1 : 0x69054111.

.long 0xfffffff0 @ and this is the CPU id mask.

#if CACHE_WRITE_THROUGH

.long 0x00000c0a

#else

.long 0x00000c0e

#endif

b __xscale_setup

.long cpu_arch_name

.long cpu_elf_name

.long HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP|HWCAP_XSCALE

.long cpu_bva0_info

.long xscale_processor_functions

.size __bva0_proc_info, . - __bva0_proc_info

由于.section指示符,上面定义的__bva0_proc_info信息在编译的时候被放到了.proc.info段中,这是由linux的

链接脚本文件vmlinux.lds指定的,参考如下:

SECTIONS

{

. = 0xC0008000;

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

_stext = .;

__init_begin = .;

*(.text.init)

__proc_info_begin = .;

*(.proc.info)

__proc_info_end = .;

这里的符号__proc_info_begin指向.proc.info的起始地址,而符号__proc_info_end指向.proc.info的结束地址。

后面就会引用这两个符号,来指向.proc.info这个段。

下面来来看看函数的源代码,为了分析方便将函数按行进行编号,其中17-18行就是前面提到的对.proc.info的引用,

第2行将17行的地址放到寄存器r5中,adr是小范围的地址读取伪指令。第3行将r5所指向的数据区的数据读出到r7,r9

r10,执行结果是r7=__proc_info_end,r9=__proc_info_begin,r10=第19行的地址,第4-6行的结果应该是r10指向

__proc_info_begin的地址,第7行读取cpu的id,这是一个协处理器指令,将processor ID存储在r9中,第8行将r10指向

的__bva0_proc_info开始的数据读出放到寄存器r5,r6,r8,结果r5=0x69054110(cpu_val),r6=0xfffffff0(cpu_mask),

r8=0x00000c0e(__cpu_mmu_flags),第9-10行将读出的id和结构中的id进行比较,如果id相同则返回,返回时r9存储

processor ID,如果id不匹配,则将指针r10增加36(proc_info_list结构的长度),如果r10小于r7指定的地址,也就是

__proc_info_end,则继续循环比较下一个proc_info_list中的id,如第11-14行的代码,如果查找到__proc_info_end

仍未找到一个匹配的id,则将r10清零并返回,如15-16行,也就是说如果函数执行成功则r10指向匹配的proc_info_list

结构地址,如果函数返回错误则r10为0。

/*

* Read processor ID register (CP#15, CR0), and look up in the linker-built

* supported processor list. Note that we can't use the absolute addresses

* for the __proc_info lists since we aren't running with the MMU on

* (and therefore, we are not in the correct address space). We have to

* calculate the offset.

*

* Returns:

* r5, r6, r7 corrupted

* r8 = page table flags

* r9 = processor ID

* r10 = pointer to processor structure

*/

1 __lookup_processor_type:

2adr r5,2f

3ldmia r5, {r7, r9, r10}

4sub r5, r5, r10 @ convert addresses

5add r7, r7, r5 @ to our address space

6add r10, r9, r5

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

81: ldmia r10, {r5, r6, r8} @ value, mask, mmuflags

9and r6, r6, r9 @ mask wanted bits

10teq r5, r6

11moveq pc, lr

12add r10, r10, #36 @ sizeof(proc_info_list)

13cmp r10, r7

14blt 1b

15mov r10, #0 @ unknown processor

16mov pc, lr

/*

* Look in include/asm-arm/procinfo.h and arch/arm/kernel/arch.[ch] for

* more information about the __proc_info and __arch_info structures.

*/

17 2: .long __proc_info_end

18 .long __proc_info_begin

19 .long 2b

20 .long __arch_info_begin

21 .long __arch_info_end

linux启动分析(4)---汇编部分(3)

前一篇介绍了汇编函数__lookup_processor_type,这一篇介绍__lookup_architecture_type函数

函数__lookup_architecture_type介绍:

每个机器(一般指的是某一个电路板)都有自己的特殊结构,如物理内存地址,物理I/O地址,显存起始地址等等,

这个结构为struct machine_desc,定义在asm-arm/mach/arch.h中:

struct machine_desc {

/*

* Note! The first four elements are used

* by assembler code in head-armv.S

*/

unsigned intnr;/* architecture number*/

unsigned intphys_ram;/* start of physical ram */

unsigned intphys_io;/* start of physical io*/

unsigned intio_pg_offst;/* byte offset for io page table entry*/

const char*name;/* architecture name*/

unsigned intparam_offset;/* parameter page*/

unsigned intvideo_start;/* start of video RAM*/

unsigned intvideo_end;/* end of video RAM*/

unsigned intreserve_lp0 :1;/* never has lp0*/,

unsigned intreserve_lp1 :1;/* never has lp1*/

unsigned intreserve_lp2 :1;/* never has lp2*/

unsigned intsoft_reboot :1;/* soft reboot*/

void(*fixup)(struct machine_desc *,

struct param_struct *, char **,

struct meminfo *);

void(*map_io)(void);/* IO mapping function*/

void(*init_irq)(void);

};

这个结构一般都定义在(以arm平台为例)kernel/arch/arm/mach-xxx/xxx.c中,是用宏来定义的,以mainstone的开发板为例:

定义在kernel/arch/arm/mach-pxa/mainstone.c文件中,如下所示:

MACHINE_START(MAINSTONE, "Intel DBBVA0 Development Platform")

MAINTAINER("MontaVista Software Inc.")

BOOT_MEM(0xa0000000, 0x40000000, io_p2v(0x40000000))

FIXUP(fixup_mainstone)

MAPIO(mainstone_map_io)

INITIRQ(mainstone_init_irq)

MACHINE_END

这些宏也定义在kernel/include/asm-arm/mach/arch.h中,以MACHINE_START为例:

#define MACHINE_START(_type,_name) /

const struct machine_desc __mach_desc_##_type /

__attribute__((__section__(".arch.info"))) = { /

.nr = MACH_TYPE_##_type, /

.name = _name,

展开之后结构的是:

__mach_desc_MAINSTONE = {

.nr = MACH_TYPE_MAINSTIONE,

.name = "Intel DBBVA0 Development Platform",

中间的1行__attribute__((__section__(".arch.info"))) = {说明将这个结构放到指定的段.arch.info中,这和前面的

.proc.info是一个意思,__attribute__((__section__的含义参考GNU手册。后面的宏都是类似的含义,这里就不再一一

介绍。下面开始说明源码:

第1行实现r4指向2b的地址,2b如__lookup_processor_type介绍的第19行,将machine_desc结构中的数据存放到r2, r3, r5, r6, r7。

读取__mach_desc_MAINSTONE结构中的nr参数到r5中,如第7行,比较r5和r1中的机器编号是否相同,如第8行,

r5中的nr值MACH_TYPE_MAINSTONE定义在kernel/include/asm-arm/mach-types.h中:

#define MACH_TYPE_MAINSTONE 303

r1中的值是由bootloader传递过来的,这在<<linux启动流程分析(1)---bootloader启动内核过程>>中有说明,

如果机器编号相同,跳到15行执行,r5=intphys_ram,r6=intphys_io,r7=intio_pg_offst,并返回。如果

不同则将地址指针增加,在跳到7行继续查找,如10--12行的代码,如果检索完所有的machine_desc仍然没

有找到则将r7清零并返回。

/*

* Lookup machine architecture in the linker-build list of architectures.

* Note that we can't use the absolute addresses for the __arch_info

* lists since we aren't running with the MMU on (and therefore, we are

* not in the correct address space). We have to calculate the offset.

*

* r1 = machine architecture number

* Returns:

* r2, r3, r4 corrupted

* r5 = physical start address of RAM

* r6 = physical address of IO

* r7 = byte offset into page tables for IO

*/

1 __lookup_architecture_type:

2 adr r4, 2b

3 ldmia r4, {r2, r3, r5, r6, r7} @ throw away r2, r3

4 sub r5, r4, r5 @ convert addresses

5 add r4, r6, r5 @ to our address space

6 add r7, r7, r5

7 1: ldr r5, [r4] @ get machine type

8 teq r5, r1

9 beq2f

10 add r4, r4, #SIZEOF_MACHINE_DESC

11 cmp r4, r7

12 blt 1b

13 mov r7, #0 @ unknown architecture

14 mov pc, lr

15 2: ldmib r4, {r5, r6, r7} @ found, get results

16 mov pc, lr

linux启动分析(4)---汇编部分(4)

函数__create_page_tables介绍:

假设内核起始物理地址是0xA0008000,虚拟地址是0xC0008000,下面的代码是建立内核起始处4MB空间的映射,

采用了一级映射方式,即段式(section)映射方式,每段映射范围为1MB空间。于是需要建立4个表项,实现:

虚拟地址0xC0000000~0xC0300000,映射到物理地址0xA0000000~0xA0300000。

.macro pgtbl, reg, rambase

adr /reg, stext

sub /reg, /reg, #0x4000

.endm

.macro krnladr, rd, pgtable, rambase

bic /rd, /pgtable, #0x000ff000

.endm

/*

* Setup the initial page tables. We only setup the barest

* amount which are required to get the kernel running, which

* generally means mapping in the kernel code.

*

* We only map in 4MB of RAM, which should be sufficient in

* all cases.

*

* r5 = physical address of start of RAM

* r6 = physical IO address

* r7 = byte offset into page tables for IO

* r8 = page table flags

*/

1 __create_page_tables:

/* r5中存放着内核启动的地址0xa0008000 */

/* pgtbl将启动地址减去0x4000,存放到r4=0xa0004000 */

2 pgtbl r4, r5 @ page table address

/*

* Clear the 16K level 1 swapper page table

*/

/* r0 = 0xa0004000 */

3 mov r0, r4

4 mov r3, #0

/* r2 = 0xa0008000 */

5 add r2, r0, #0x4000

/*清除16k空间,addr 0xa0004000: 0xa0008000 is page table, total 16K*/

6 1: str r3, [r0], #4

7 str r3, [r0], #4

8 str r3, [r0], #4

9 str r3, [r0], #4

10 teq r0, r2

11 bne 1b

/*

* Create identity mapping for first MB of kernel to

* cater for the MMU enable. This identity mapping

* will be removed by paging_init()

*/

/* r2 = 0xa0040000 & 0x000ff000 = 0xa00000000 */

12 krnladr r2, r4, r5 @ start of kernel

/* r3 = 0xa0000000 + 0x00000c0e = 0xa00000c0e */

/* r8 = 0x00000c0e在__lookup_processor_type函数中初始化*/

13 add r3, r8, r2 @ flags + kernel base

/* value r3=0xa0000c0e store to addr 0xa0006800*/

/* r4 = 0xa0006800 */

14 str r3, [r4, r2, lsr #18] @ identity mapping

/*

* Now setup the pagetables for our kernel direct

* mapped region. We round TEXTADDR down to the

* nearest megabyte boundary.

*/

/* TEXTADDR= 0xC0008000有关TEXTADDR参考<<linux启动流程分析(2)---内核启动地址的确定>> */

/* start of kernel, r0=0xa0007000 */

15 add r0, r4, #(TEXTADDR & 0xff000000) >> 18 @ start of kernel

/* r2=0xa0000c0e */

16 bic r2, r3, #0x00f00000

/* 0xa0000c0e的数据写入到0xa00070000 */

17 str r2, [r0] @ PAGE_OFFSET + 0MB

/* r0=0xa0007000, no change */

18 add r0, r0, #(TEXTADDR & 0x00f00000) >> 18

19 str r3, [r0], #4 @ KERNEL + 0MB

20 add r3, r3, #1 << 20

21 str r3, [r0], #4 @ KERNEL + 1MB

22 add r3, r3, #1 << 20

23 str r3, [r0], #4 @ KERNEL + 2MB

24 add r3, r3, #1 << 20

25 str r3, [r0], #4 @ KERNEL + 3MB

/*

* Ensure that the first section of RAM is present.

* we assume that:

* 1. the RAM is aligned to a 32MB boundary

* 2. the kernel is executing in the same 32MB chunk

* as the start of RAM.

*/

26 bic r0, r0, #0x01f00000 >> 18 @ round down

27 and r2, r5, #0xfe000000 @ round down

28 add r3, r8, r2 @ flags + rambase

29 str r3, [r0]

30 bic r8, r8, #0x0c @ turn off cacheable

31 mov pc, lr

我已经把每一步涉及的地址详细列出了,读者可以自行对照阅读。第11~16行,清空页表项从0xA0004000到0xA00,8000,共16KB。

第28行,取得__cpu_mmu_flags。第35~45行,填写页表项,共4项。读者可以对照XScale的地址映射手册,

因为采用的是段式映射方式,所以每1MB虚拟空间映射到相同的页表表项,根据手册说明,段式映射只有一级表索引,

是虚拟地址的前12位;而页式映射的页目录表是前12位,页表是接着的8位,最后12位才是页内偏移,

读者一定不要和386的10位页目录表,10位页表的机制相混淆。我们举个例子说明,对于虚拟地址0xC00x,xxxxx,

其前12位为C00,页表基址为0xA000,4000,所以表项地址为0xA000,4000+0xC00<<2=0xA000,7000,

而这个地址内容为0xA0000C0E,其前12位0xA00为段基地址,后20位为一些flags,这是从刚才__bva0_proc_info中取得的。

linux启动分析(4)---汇编部分(5)

函数__mmap_switched介绍:

/*

* The following fragment of code is executed with the MMU on, and uses

* absolute addresses; this is not position independent.

*

* r0 = processor control register

* r1 = machine ID

* r9 = processor ID

*/

/*下面按4字节对齐*/

1 .align 5

2 __mmap_switched:

/* r3 = __bss_start */

3 adr r3, __switch_data + 4

4 ldmia r3, {r4, r5, r6, r7, r8, sp}@ r2 = compat

@ sp = stack pointer

5 mov fp, #0 @ Clear BSS (and zero fp)

6 1: cmp r4, r5

7 strcc fp, [r4],#4

8 bcc 1b

9 str r9, [r6] @ Save processor ID

10 str r1, [r7] @ Save machine type

11 orr r0, r0, #2 @ ...........A.

12 bic r2, r0, #2 @ Clear 'A' bit

13 stmia r8, {r0, r2} @ Save control register values

14 b SYMBOL_NAME(start_kernel)

程序的4行执行完成之后的结果是r4=__bss_start,r5=_end,r6=processor_id,r7=__machine_arch_type,

r8=cr_alignment,sp=init_task_union+8192,第5-8行将__bss_start到_end清零,定义在vmlinux.lds文件中,如下:

.bss : {

__bss_start = .; /* BSS */

*(.bss)

*(COMMON)

_end = . ;

}

第9、10行分别将处理器类型和机器类型存储到变量processor_id和__machine_arch_type中,这些变量以后会

在start_kernel->setup_arch中使用,来得到当前处理器的struct proc_info_list结构和当前系统的machine_desc结构的数据。

第10-13将processor control register保存到cr_alignment中,14行跳转到init/main.c中的start_kernel进入内核启动的第二阶段。

linux启动分析(5)---C程序入口函数start_kernel

内核从现在开始就进入了c语言部分,内核启动第二阶段从init/main.c的start_kernel()函数开始到函数结束。

这一阶段对整个系统内存、cache、信号、设备等进行初始化,最后产生新的内核线程init后,

调用cpu_idle()完成内核第二阶段。有很多书籍介绍这一部分的内容,我们这里仅仅讲述与xscale结构相关的部分。

首先我们看一下start_kernel开始部分的源代码

asmlinkage void __init start_kernel(void)

{

char * command_line;

extern char saved_command_line[];

/*

* Interrupts are still disabled. Do necessary setups, then

* enable them

*/

lock_kernel();

printk(linux_banner);

setup_arch(&command_line);

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

parse_options(command_line);

trap_init();

init_IRQ();

sched_init();

softirq_init();

time_init();

.......

.....

...

start_kernel使用了asmlinkage进行修饰,该修饰符定义在kernel/include/linux/linkage.h中,如下所示:

#ifdef __cplusplus

#define CPP_ASMLINKAGE extern "C"

#else

#define CPP_ASMLINKAGE

#endif

#if defined __i386__

#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))

#elif defined __ia64__

#define asmlinkage CPP_ASMLINKAGE __attribute__((syscall_linkage))

#else

#define asmlinkage CPP_ASMLINKAGE

#endif

应为我们使用的是arm平台,所以这些定义没有意义,不过还是简单介绍一下regparm的意思,察看gcc手册,原文

介绍如下:

On the Intel 386, the regparm attribute causes the compiler to pass arguments

number one to number if they are of integral type in registers EAX, EDX,

and ECX instead of on the stack. Functions that take a variable number of

arguments will continue to be passed all of their arguments on the stack.

Beware that on some ELF systems this attribute is unsuitable for global functions

in shared libraries with lazy binding (which is the default). Lazy binding

will send the first call via resolving code in the loader, which might assume

EAX, EDX and ECX can be clobbered, as per the standard calling conventions.

Solaris 8 is affected by this. GNU systems with GLIBC 2.1 or higher,

and FreeBSD, are believed to be safe since the loaders there save all registers.

(Lazy binding can be disabled with the linker or the loader if desired, to avoid

the problem.)

在网上还看到一个比较好的英文说明:

The asmlinkage tag is one other thing that we should observe about this simple function.

This is a #define for some gcc magic that tells the compiler that the function should not

expect to find any of its arguments in registers (a common optimization),

but only on the CPU's stack. Recall our earlier assertion that system_call consumes its

first argument, the system call number, and allows up to four more arguments that are

passed along to the real system call. system_call achieves this feat simply by leaving

its other arguments (which were passed to it in registers) on the stack. All system calls

are marked with the asmlinkage tag, so they all look to the stack for arguments. Of course,

in sys_ni_syscall's case, this doesn't make any difference, because sys_ni_syscall doesn't

take any arguments, but it's an issue for most other system calls. And, because you'll be

seeing asmlinkage in front of many other functions, I thought you should know what it was about.

简单描述一下他的功能:

asmlinkage是个宏,使用它是为了保持参数在stack中。因为从汇编语言到C语言代码参数

的传递是通过stack的,它也可能从stack中得到一些不需要的参数。Asmlinkage将要

解析那些参数。regparm(0)表示不从寄存器传递参数。如果是__attribute__((regparm(3))),

那么调用函数的时候参数不是通过栈传递,而是直接放到寄存器里,被调用函数直接从寄存器取参数。

这一点可以从下面的定义可以看出:

#define fastcall __attribute__((regparm(3)))

这些都必须是在i386平台下才有意义。

linux启动分析(5)---start_kernel续

说完asmlinkage,开始看源代码,第一个函数:lock_kernel(),

这是为了在SMP系统下设计的,它定义在kernel/include/linux/smp_lock.h,如果是SMP系统,则会

定义CONFIG_SMP,否则lock_kernel()将是空函数,如果定义CONFIG_SMP的话,则会包含kernel/include/

asm/smplock.h头文件,lock_kernel()就定一在该文件中,首先我们来看一下smp_lock.h文件:

#ifndef CONFIG_SMP

#define lock_kernel() do { } while(0)

#define unlock_kernel() do { } while(0)

#define release_kernel_lock(task, cpu) do { } while(0)

#define reacquire_kernel_lock(task) do { } while(0)

#define kernel_locked() 1

#else

#include <asm/smplock.h>

#endif /* CONFIG_SMP */

我们的平台是单cpu的(没有定义CONFIG_SMP),所以lock_kernel是空函数,不过仍然对它进行一下说明,

如果定义了CONFIG_SMP,则include kernel/include/asm-arm/smplock.h文件,看一下该文件:

static inline void lock_kernel(void)

{

if (!++current->lock_depth)

spin_lock(&kernel_flag);

}

static inline void unlock_kernel(void)

{

if (--current->lock_depth < 0)

spin_unlock(&kernel_flag);

}

找到两个比较好的说明如下

1

kernel_flag是一个内核大自旋锁,所有进程都通过这个大锁来实现向内核态的迁移。只有

获得这个大自旋锁的处理器可以进入内核,如中断处理程序等。在任何一对lock_kernel/

unlock_kernel函数里至多可以有一个程序占用CPU。进程的lock_depth成员初始化为-1,

在kerenl/fork.c文件中设置。在它小于0时(恒为-1),进程不拥有内核锁;当大于或等

于0时,进程得到内核锁。

2

kernel_flag,定义为自旋锁,因为很多核心操作(例如驱动中)需要保证当前仅由一个进程执行,

所以需要调用lock_kernel()/release_kernel()对核心锁进行操作,它在锁定/解锁kernel_flag的

同时还在task_struct::lock_depth上设置了标志,lock_depth小于0表示未加锁。当发生进程切换的时候,

不允许被切换走的进程握有kernel_flag锁,所以必须调用release_kernel_lock()强制释放,同时,

新进程投入运行时如果lock_depth>0,即表明该进程被切换走之前握有核心锁,

必须调用reacquire_kernel_lock()再次锁定;

代码printk(linux_banner)将linux的一些标语打印在内核启动的开始部分,需要说明的是虽然这是

在内核一开始运行时就打印了,但是它没有马上输出到控制台上,它只是将liunx_banner存储到printk

的内部缓冲中,因为这时printk的输出设备,一般都是串口还没有初始化,只有到输出设备初始化完毕

在缓冲中的数据才被输出,后面会看到在哪个位置linux_banner才真正输出到终端。linux_banner定义在

kernel/init/version.c中:

const char *linux_banner =

"Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@"

LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "/n";

这里面的字符串定义在文件kernel/include/linux/compile.h和kernel/include/linux/version.h中,

compile.h中的内容:

#define UTS_VERSION "#1 Thu, 01 Feb 2007 13:32:14 +0800"

#define LINUX_COMPILE_TIME "13:32:14"

#define LINUX_COMPILE_BY "taoyue"

#define LINUX_COMPILE_HOST "swlinux.cecwireless.com.cn"

#define LINUX_COMPILE_DOMAIN "cecwireless.com.cn"

#define LINUX_COMPILER "gcc version3.2.1"

version.h中的内容:

#define UTS_RELEASE "2.4.19-rmk7-pxa2"

#define LINUX_VERSION_CODE 132115

#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))

这两个文件都是在编译时候生成的,看一下kernel/Makefile文件:

include/linux/compile.h: $(CONFIGURATION) include/linux/version.h newversion

@echo -n /#`cat .version` > .ver1

@if [ -n "$(CONFIG_SMP)" ] ; then echo -n " SMP" >> .ver1; fi

@if [ -f .name ]; then echo -n /-`cat .name` >> .ver1; fi

@LANG=C echo ' '`date -R` >> .ver1

@echo /#define UTS_VERSION /"`cat .ver1 | $(uts_truncate)`/" > .ver

@LANG=C echo /#define LINUX_COMPILE_TIME /"`date +%T`/" >> .ver

@echo /#define LINUX_COMPILE_BY /"`whoami`/" >> .ver

@echo /#define LINUX_COMPILE_HOST /"`hostname | $(uts_truncate)`/" >> .ver

@([ -x /bin/dnsdomainname ] && /bin/dnsdomainname > .ver1) || /

([ -x /bin/domainname ] && /bin/domainname > .ver1) || /

echo > .ver1

@echo /#define LINUX_COMPILE_DOMAIN /"`cat .ver1 | $(uts_truncate)`/" >> .ver

@echo /#define LINUX_COMPILER /"`$(CC) $(CFLAGS) -v 2>&1 | tail -1`/" >> .ver

@mv -f .ver $@

@rm -f .ver1

include/linux/version.h: ./Makefile

@expr length "$(KERNELRELEASE)" /<= $(uts_len) > /dev/null || /

(echo KERNELRELEASE /"$(KERNELRELEASE)/" exceeds $(uts_len) characters >&2; false)

@echo /#define UTS_RELEASE /"$(KERNELRELEASE)/" > .ver

@echo /#define LINUX_VERSION_CODE `expr $(VERSION) //* 65536 + $(PATCHLEVEL) //* 256 + $(SUBLEVEL)` >> .ver

@echo '#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))' >>.ver

@mv -f .ver $@

可以修改的参数是:

VERSION = 2

PATCHLEVEL = 4

SUBLEVEL = 19

EXTRAVERSION = -rmk7-pxa2

提醒:《ARM linux启动分析》最后刷新时间 2024-03-14 01:05:15,本站为公益型个人网站,仅供个人学习和记录信息,不进行任何商业性质的盈利。如果内容、图片资源失效或内容涉及侵权,请反馈至,我们会及时处理。本站只保证内容的可读性,无法保证真实性,《ARM linux启动分析》该内容的真实性请自行鉴别。