ARM linux启动分析

ARM 107浏览

ARM linux启动分析

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

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

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

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

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

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

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

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

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

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

其中KERNEL_RAM_BASE为内核在ram中启动的地址,ARCH_NUMBERMachine Type Numberkernel_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 = ZTEXTADDRLOAD_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
编译完成后将产生vmlinuxhead.omisc.ohead-xscale.opiggy.o这几个文件,
head.o
是内核的头部文件,负责初始设置;
misc.o
将主要负责内核的解压工作,它在head.o之后;
head-xscale.o
文件主要针对Xscale的初始化,将在链接时与head.o合并;
piggy.o
是一个中间文件,其实是一个压缩的内核(kernel/vmlinux),只不过没有和初始化文件及解压文件链接而已;
vmlinux
(没有--lwzImage是压缩过的内核)压缩过的内核,就是由piggy.ohead.omisc.ohead-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)
对于各种Arm CPUDEBUG输出设定,通过定义宏来统一操作。
(2)
设置kernel开始和结束地址,保存architecture ID
(3)
如果在ARM2以上的CPU中,用的是普通用户模式,则升到超级用户模式,然后关中断。
(4)
分析LC0结构delta offset,判断是否需要重载内核地址(r0存入偏移量,判断r0是否为零)
  
这里是否需要重载内核地址,我以为主要分析arch/arm/boot/Makefilearch/arm/boot/compressed/Makefile
  
arch/arm/boot/compressed/vmlinux.lds.in三个文件,主要看vmlinux.lds.in链接文件的主要段的位置,
   LOAD_ADDR(_load_addr)
0xA0008000,而对于TEXT_START(_text_start)的位置只设为0BSS_START(__bss_start)ALIGN(4)
  
对于这样的结果依赖于,对内核解压的运行方式,也就是说,内核解压前是在内存(RAM)中还是在FLASH上,
  
因为这里,我们的BOOTLOADER将压缩内核(zImage)移到了RAM0xA0008000位置,我们的压缩内核是在内存(RAM)0xA0008000地址开始顺序排列,
  
因此我们的r0获得的偏移量是载入地址(0xA0008000)。接下来的工作是要把内核镜像的相对地址转化为内存的物理地址,即重载内核地址。
(5)
需要重载内核地址,将r0的偏移量加到BSS regionGOT table中。
(6)
清空bss堆栈空间r2r3
(7)
建立C程序运行需要的缓存,并赋于64K的栈空间。
(8)
这时r2是缓存的结束地址,r4kernel的最后执行地址,r5kernel境象文件的开始地址。检查是否地址有冲突。
  
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,将r7architecture ID赋于r1,执行r4开始的kernel代码。

下面简单介绍一下解压缩过程,也就是函数decompress_kernel实现的功能:
解压缩代码位于kernel/lib/inflate.cinflate.c是从gzip源程序中分离出来的。包含了一些对全局数据的直接引用。
在使用时需要直接嵌入到代码中。gzip压缩文件时总是在前32K字节的范围内寻找重复的字符串进行编码,
在解压时需要一个至少为32K字节的解压缓冲区,它定义为window[WSIZE]inflate.c使用get_byte()读取输入文件,
它被定义成宏来提高效率。输入缓冲区指针必须定义为inptrinflate.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
/*
程序状态,禁止FIQIRQ,设定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'
10   beq __error
/*
判断体系类型,查看R1寄存器的Architecture Type值是否支持 */
11   bl  __lookup_architecture_type
/*
判断如果r7的值为0,则表示函数执行错误,跳转到出错处理,*/
12   teq r7, #0              @ invalid architecture?
13   moveq   r0, #'a'            @ yes, error 'a'
14   beq __error
/*
创建核心页表 */
15   bl  __create_page_tables
16   adr lr, __ret           @ return address
17   add 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所指向的数据区的数据读出到r7r9
r10
,执行结果是r7=__proc_info_endr9=__proc_info_beginr10=19行的地址,第4-6行的结果应该是r10指向
__proc_info_begin
的地址,第7行读取cpuid,这是一个协处理器指令,将processor ID存储在r9中,第8行将r10指向
__bva0_proc_info开始的数据读出放到寄存器r5r6r8,结果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
结构地址,如果函数返回错误则r100

/*     
 * 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:   
2    adr r5, 2f
3    ldmia   r5, {r7, r9, r10}
4    sub r5, r5, r10         @ convert addresses
5    add r7, r7, r5          @ to our address space
6    add r10, r9, r5
7    mrc p15, 0, r9, c0, c0      @ get processor id
8 1:   ldmia   r10, {r5, r6, r8}       @ value, mask, mmuflags
9    and r6, r6, r9          @ mask wanted bits
10   teq r5, r6
11   moveq   pc, lr
12   add r10, r10, #36           @ sizeof(proc_info_list)
13   cmp r10, r7
14   blt 1b
15   mov r10, #0             @ unknown processor
16   mov 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行,比较r5r1中的机器编号是否相同,如第8行,
r5
中的nrMACH_TYPE_MAINSTONE定义在kernel/include/asm-arm/mach-types.h中:
#define MACH_TYPE_MAINSTONE            303
r1
中的值是由bootloader传递过来的,这在<<linux启动流程分析(1)---bootloader启动内核过程>>中有说明,
如果机器编号相同,跳到15行执行,r5=intphys_ramr6=intphys_ior7=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          beq 2f 
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
       
 
我已经把每一步涉及的地址详细列出了,读者可以自行对照阅读。第1116行,清空页表项从0xA00040000xA00,8000,共16KB
 
28行,取得__cpu_mmu_flags。第3545行,填写页表项,共4项。读者可以对照XScale的地址映射手册,
 
因为采用的是段式映射方式,所以每1MB虚拟空间映射到相同的页表表项,根据手册说明,段式映射只有一级表索引,
 
是虚拟地址的前12位;而页式映射的页目录表是前12位,页表是接着的8位,最后12位才是页内偏移,
 
读者一定不要和38610位页目录表,10位页表的机制相混淆。我们举个例子说明,对于虚拟地址0xC00x,xxxxx
 
其前12位为C00,页表基址为0xA000,4000,所以表项地址为0xA000,4000+0xC00<<2=0xA000,7000
 
而这个地址内容为0xA0000C0E,其前120xA00为段基地址,后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_startr5=_endr6=processor_idr7=__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 = . ;
  }  
 
910行分别将处理器类型和机器类型存储到变量processor_id__machine_arch_type中,这些变量以后会
start_kernel->setup_arch中使用,来得到当前处理器的struct proc_info_list结构和当前系统的machine_desc结构的数据。
10-13processor control register保存到cr_alignment中,14行跳转到init/main.c中的start_kernel进入内核启动的第二阶段。

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

内核从现在开始就进入了c语言部分,内核启动第二阶段从init/main.cstart_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.hkernel/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 version 3.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