Linux内核启动代码分析二之开发板相关驱动程序加载分析

时间:2024-01-11 08:15:20

Linux内核启动代码分析二之开发板相关驱动程序加载分析

1 从linux开始启动的函数start_kernel开始分析,该函数位于linux-2.6.22/init/main.c
 start_kernel()
   --2>setup_arch(&command_line);//该函数位于arch/arm/kernel/setup.c
          //在这个函数中定义了一个描述开发板的属性的结构体struct machine_desc *mdesc
          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 :1; /* never has lp0 */
      unsigned int  reserve_lp1 :1; /* never has lp1 */
      unsigned int  reserve_lp2 :1; /* never has lp2 */
      unsigned int  soft_reboot :1; /* 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);
     };
          //这个结构体中包括机器ID,物理地址、IO地址偏移、IO映射函数、IRQ中断函数初始化
          //开发板初始化函数:完成平台驱动程序初始化注册函数的调用
       --3>mdesc = setup_machine(machine_arch_type);
           //machine_arch_type为外部定义的一个全局变量,用来标识机器ID
           --4>struct machine_desc *list = lookup_machine_type(machine_arch_type)
               //用于搜索开发板机器ID,lookup_machine_type是一个汇编函数
                 位于linux-2.6.22/arch/arm/kernel/head-common.S
                 lookup_machine_type中对机器ID号码从r0寄存器复制到r1寄存器中,调用汇编函数:__lookup_machine_type
                 --5>__lookup_machine_type从.arch.info.init段中比较是否存在相同机器ID号码的机器描述结构体,
                 在使用MACHINE_START(_type,_name)宏定义一个开发板机器描述结构体时,会把这个结构体变量放到.arch.info.init段内
      
      //宏定义和宏开如下所述:
      #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    \
      };
      
      MACHINE_START(CSB337, "Cogent CSB337")
       /* Maintainer: Bill Gatliff */
       .phys_io = AT91_BASE_SYS,
       .io_pg_offst = (AT91_VA_BASE_SYS >> 18) & 0xfffc,
       .boot_params = AT91_SDRAM_BASE + 0x100,
       .timer  = &at91rm9200_timer,
       .map_io  = csb337_map_io,
       .init_irq = csb337_init_irq,
       .init_machine = csb337_board_init,
      MACHINE_END
      
      //宏展开如下:
      
      MACHINE_START(CSB337, "Cogent CSB337")
      
      static const struct machine_desc __mach_desc_CSB337 __used __attribute__((__section__(".arch.info.init"))) = { 
       .nr  = MACH_TYPE_CSB337,  
       .name  = Cogent CSB337,
      
      /* Maintainer: Bill Gatliff */
       .phys_io = AT91_BASE_SYS,
       .io_pg_offst = (AT91_VA_BASE_SYS >> 18) & 0xfffc,
       .boot_params = AT91_SDRAM_BASE + 0x100,
       .timer  = &at91rm9200_timer,
       .map_io  = csb337_map_io,
       .init_irq = csb337_init_irq,
       .init_machine = csb337_board_init,
      };
      
      __lookup_machine_type函数:位于arch/arm/kernel/head-common.s
       __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
      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
      这个函数中的#MACHINFO_TYPE,不是太明白,搜索源码发现它出现在一个c文件的main函数中,被main函数调用
      很不理解,按理说一条宏命令应该出现在一个h文件或函数外才对。google后的结果如下,没有进一步地观察跟踪
      编译过程是不是这样,不过觉得还是挺有道理的。留待以后考证。
      #define DEFINE(sym, val) \
              asm volatile("\n->" #sym " %0 " #val : : "i" (val))
        根据上面的宏定义,进行宏展开
        DEFINE(SIZEOF_MACHINE_DESC, sizeof(struct machine_desc));
        asm volatile("\n->" SIZEOF_MACHINE_DESC " %0 " sizeof(struct machine_desc) : : "i" (sizeof(struct machine_desc)))
        google后的结果是:
        asm volatile("\n->" #sym " %0 " #val : :"i" (val))估计是  #define  sym  val 的意思
        也就是#define SIZEOF_MACHINE_DESC sizeof(struct machine_desc)
        只不过这里是动态的定义。只需要传一个sym和val进来,就可以帮你完成#define  sym  val的功能
        那个宏定义在asm-offsets.c中,这个.c文件根本就不是用来编译运行的,只是在编译内核的时候,
        用它生成一个asm-offsets.s文件,然后使用一个脚本将这个asm-offsets.s再转换为asm-offsets.h。
        这个头文件遵循汇编语法,用来被汇编文件include的。
        DEFINE(MACHINFO_TYPE,  offsetof(struct machine_desc, nr));
        #define MACHINFO_TYPE offsetof(struct machine_desc, nr)
       
        函数调用--4>如果lookup_machine_type(nr)查找到了目标开发板机器ID,返回一个指向该机器描述结构体的指针,否则返回0
        而函数调用--3>setup_machine(machine_arch_type)中的实参machine_arch_type (也就是作为实参传入函数lookup_machine_type
        函数的变量nr) 是在哪里定义的呢?
       
        在arch/arm/tools下有三个文件:gen-mach-types、mach-types、Makefile
        1 阅读gen-mach-types,发现这是一个shell脚本文件,看注释是:
          generate include/asm-arm/mach-types.h用于产生mach-types.h的头文件
          mach-types.h里的生成的是板子相关的宏定义
        2 mach-types文件中是很多块板子的配置文件,所以当为内核添加一款新的开发板时,需要修改这个文件,
          参考别的板子的定义方法,添加一项配置,其中配置的最后一项number就是机器ID号码
        3 mach-types.h源码中是没有这个文件的,这个文件是编译过程中动态生成的。
          # machine_is_xxx CONFIG_xxxx  MACH_TYPE_xxx  number
                 csb337   MACH_CSB337  CSB337       399
          mach-types.h首先根据mach-types配置文件生成#define MACH_TYPE_XXX  number的宏定义机器ID
          如:#define MACH_TYPE_CSB337  399
          然后成如所有配置项的其他部分,如:
          #ifdef CONFIG_MACH_CSB337
        # ifdef machine_arch_type
        #  undef machine_arch_type
        #  define machine_arch_type __machine_arch_type
        # else
        #  define machine_arch_type MACH_TYPE_CSB337
        # endif
        # define machine_is_csb337() (machine_arch_type == MACH_TYPE_CSB337)
        #else
        # define machine_is_csb337() (0)
        #endif
        在这里我们看到了machine_arch_type的踪影。。。。
        解析这段宏命令:如果定义了machine_arch_type,则结束machine_arch_type原来的宏定义,
        然后把machine_arch_type重新定义成__machine_arch_type;如果没有定义过machine_arch_type,
        那么就把machine_arch_type定义成mach-types中配置的机器ID号码。
        
        Makefile中把mach-types.h动态包含到工程去。
       4 如果定义了machine_arch_type宏的话,machine_arch_type又被重定义为__machine_arch_type;
         __machine_arch_type又是在哪里定义的呢?
         在源码中搜索__machine_arch_type,在arch/arm/boot/compressed/misc.c中发现了:
         unsigned int __machine_arch_type;的定义,
         并在该文件下的函数:
           decompress_kernel()中对__machine_arch_type进行了初始化赋值为arch_id。
           其实这个arch_id是由uboot等bootloader传入内核的一个参数。
       5 追踪decompress_kernel函数
         decompress_kernel是使解压内核的意义,该函数应该是在内核运行之前先运行的函数,用于把内核
         从uImage镜像解压到ram中去,便于内核在内存中运行。(以上都是自己的猜测:根据这个函数中打印
         出的字符和内核运行初输出到终端上的字符一致)
         uImage在arch/arm/boot目录下生成,那么阅读该目录下的Makefile文件。
         看不太懂什么意思,不过最后一句:subdir-     := bootp compressed
         那么继续看这两个子目录中的Makefile
         bootp目录里都是些配置链接相关的代码,看不懂,查看linux主机发现,这个目录上没有生成*.o文件,暂时跳过。
         compressed目录
         这个目录下有个文件vmlinux.lds.in,编译时会生成一个vmlinux.lds
         阅读这个链接脚本,好多看不太明白,不过链接脚本中.text段中放在最前面的是_start,那么可以在本目录下
         寻找一个含有_start的汇编文件,发现head.s含有这个start标号,可以猜测内核最先就是从这个会变文件开始
         执行下去的。分析下这个head.s汇编
       5.1 保存uboot传入的参数,关闭中断进入管理模式
         uboot跳入到linux内核时传入三个参数:(内核偏移地址:我自己猜的)0,机器ID,参数tags指针
         分别把这三个参数保存到r0,r7,r8三个寄存器。
       5.2 内核代码的重定位
         adr r0, LC0//把LC0表的地址加载到r0中
        ldmia r0, {r1, r2, r3, r4, r5, r6, ip, sp}//把LC0表的数据分别加载到r1 r2 r3 r4 r5 r6 r12 r13中
        subs r0, r0, r1 
        beq not_relocated //通过比较r0 r1也就是比较LC0表的加载地址和链接地址是否相同,相同就调用not_relocated
                       函数,就不用再进行代码重定位了,否则还需要对代码在内存进行搬移重定位到链接地址。
        标号LC0是指在内存中的地址,而这个标号作为地址的内存单元中存储的LC0是指由链接脚本指定的内存地址数据

.type LC0, #object
         LC0:  .word LC0   @ r1        表LC0地址
           .word __bss_start  @ r2      BSS段开始地址:由链接脚本确定
           .word _end   @ r3          BSS段结束地址:由链接脚本确定
           .word zreladdr  @ r4        由arch/arm/mach-xx/Makefile.boot文件配置内核加载地址
           .word _start   @ r5        压缩内核的开始地址
           .word _got_start  @ r6      got段开始地址
           .word _got_end  @ ip        got段结束地址
           .word user_stack+4096  @ sp  栈指针sp
         LC1:  .word reloc_end - reloc_start
           .size LC0, . - LC0
         假如r0 与r1值不一样,需要进行重定位:
           add r5, r5, r0   重定位内核地址
          add r6, r6, r0   重定位got段开始地址
          add ip, ip, r0   重定位got段结束地址
          
          add r2, r2, r0
          add r3, r3, r0
          add sp, sp, r0
          个人理解:LC0表中除LC0 zreladdr外都是相对LC0的相对位移地址,这样就能明白这6句指令了
        重定位got表:
        1:  ldr r1, [r6, #0]  @ relocate entries in the GOT
           add r1, r1, r0  @ table.  This fixes up the
           str r1, [r6], #4  @ C references.
           cmp r6, ip
           blo 1b
      5.3 完成重定位后
          《A》 清bss段
            mov r0, #0
        1: str r0, [r2], #4  @ clear bss
          str r0, [r2], #4
          str r0, [r2], #4
          str r0, [r2], #4
          cmp r2, r3
          blo 1b
        《B》为解压缩内核设置堆栈空间
           bl cache_on  //打开cache
           mov r1, sp   @ malloc space above stack
          add r2, sp, #0x10000 @ 64k max 在栈上方开辟64K堆空间,现在r2是堆的底地址。。
        《C》调整内核空间与堆栈空间的地址,然后填充decompress_kernel的四个参数
        /*检查堆栈空间是否和内核uImage所站的空间重合
          *   r4 = final kernel address 同makefile.boot配置的内核地址(2410配置为0x30008000)
          *   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
         sub r3, sp, r5  @ > compressed kernel size
         add r0, r4, r3, lsl #2 @ allow for 4x expansion
         cmp r0, r5
         bls wont_overwrite     
           //一般地r4不会比r2大,r0比r5小,个人猜测,这部分不是太明白什么意思
         mov r5, r2   @ decompress after malloc space
         mov r0, r5
         mov r3, r7
         bl decompress_kernel
         。。。。。
         wont_overwrite: mov r0, r4
         mov r3, r7
         bl decompress_kernel
         b call_kernel
       《D》跳入到解压缩后的内核里去运行linux操作系统
         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
       《E》运行Linux操作系统的前奏
          阅读bootp.lds连接脚本,发现连接到真正内核可执行文件的最前面的是 _stext 段。
          该段在arch/arm/kernel/head.S中
          分析下面这段代码
         ENTRY(stext)
        msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
             @ and irqs disabled //进行管理模式 关中断
        mrc p15, 0, r9, c0, c0  @ get processor id //通过协处理器命令读处理器ID号
        bl __lookup_processor_type  @ r5=procinfo r9=cpuid
        //该函数位于arch/arm/kernel/head-common.s中
        movs r10, r5    @ invalid processor (r5=0)?
        beq __error_p   @ yes, error 'p'
        //没有找到r5为0
        bl __lookup_machine_type  @ r5=machinfo
        //该函数位于arch/arm/kernel/head-common.s中
        movs r8, r5    @ invalid machine (r5=0)?
        beq __error_a   @ yes, error 'a' 
        //没有找到r5为0
        bl __create_page_tables  
        //创建页表
        ldr r13, __switch_data  @ address to jump to after
        @ mmu has been enabled
        adr lr, __enable_mmu  @ return (PIC) address
        //打开MMU
        add pc, r10, #PROCINFO_INITFUNC
        
        《F》运行Linux操作系统的前奏迷云
        PROCINFO_INITFUNC 是__cpu_flush成员变量在结构体proc_info_list中的便宜量,
        这可以从arch/arm/kernel/asm-offset.c中的一条语句如下得知。
        DEFINE(PROCINFO_INITFUNC, offsetof(struct proc_info_list, __cpu_flush));
        在arch/arm/kernel/vmlinux.lds.s中有
        __proc_info_begin = .;
        *(.proc.info.init)
         __proc_info_end = .;
        这三行把所有处理器信息结构组合在一块,就像一个结构数组。
        这样查找时只要找到 __proc_infor_end 的地址,很快就能找到处理器信息结构数组。
        对于机器信息也是一样
        __arch_info_begin = .;
        *(.arch.info.init)
         __arch_info_end = .;
         由Makefile可知对于arm920t系列的处理器proc-arm920.S被编译进内核
         在arch/arm/mm/proc-arm920.S的448行有如下代码:
         第一行伪汇编可能是把下面的这段数据存放到.proc.info.init代码段中
         .section ".proc.info.init", #alloc, #execinstr

.type __arm920_proc_info,#object
        __arm920_proc_info:
         .long 0x41009200
         .long 0xff00fff0
         .long   PMD_TYPE_SECT | \
          PMD_SECT_BUFFERABLE | \
          PMD_SECT_CACHEABLE | \
          PMD_BIT4 | \
          PMD_SECT_AP_WRITE | \
          PMD_SECT_AP_READ
         .long   PMD_TYPE_SECT | \
          PMD_BIT4 | \
          PMD_SECT_AP_WRITE | \
          PMD_SECT_AP_READ
         b __arm920_setup
         .long cpu_arch_name
         .long cpu_elf_name
         .long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB
         .long cpu_arm920_name
         .long arm920_processor_functions
         .long v4wbi_tlb_fns
         .long v4wb_user_fns
        #ifndef CONFIG_CPU_DCACHE_WRITETHROUGH
         .long arm920_cache_fns
        #else
         .long v4wt_cache_fns
        #endif
         .size __arm920_proc_info, . - __arm920_proc_info
         
       结合以上代码及add pc, r10, #PROCINFO_INITFUNC 指令可知
       r10存储的是proc.info.init的中某一项的起始地址,而PROCINFO_INITFUNC是__cpu_flush
       在结构体proc_info_list的偏移值,而这个值对应的正是__arm920_proc_info中的第五项
       b __arm920_setup ,由此可知,接下来跳转到 __arm920_setup 处运行。
       
        .type __arm920_setup, #function
        __arm920_setup:
         mov r0, #0
         mcr p15, 0, r0, c7, c7  @ invalidate I,D caches on v4
         mcr p15, 0, r0, c7, c10, 4  @ drain write buffer on v4
        #ifdef CONFIG_MMU
         mcr p15, 0, r0, c8, c7  @ invalidate I,D TLBs on v4
        #endif
         adr r5, arm920_crval
         ldmia r5, {r5, r6}
         mrc p15, 0, r0, c1, c0  @ get control register v4
         bic r0, r0, r5
         orr r0, r0, r6
         mov pc, lr
         。。。。。
          .type arm920_crval, #object
         arm920_crval:
          crval clear=0x00003f3f, mmuset=0x00003135, ucset=0x00001130
       这段代码先使I/D CACHE无效写buffer TLB无效,然后加载arm920_crval值,用来设置r0,
       mov pc,lr会跳转到 arch/arm/kernel/head.s中的__enable_mmu 函数中去执行。
       __enable_mmu
         b __turn_mmu_on
         mov pc, r13  //r13中的值是__switch_data在上面出现指令 ldr r13, __switch_data
         在源码中搜索 grep '__switch_data' -nR ./*
         搜索结果:./arch/arm/kernel/head-common.S:14: .type   __switch_data, %object
          .type __switch_data, %object
         __switch_data:
          .long __mmap_switched
          .long __data_loc   @ r4
          .long __data_start   @ r5
          .long __bss_start   @ r6
          .long _end    @ r7
          .long processor_id   @ r4
          .long __machine_arch_type  @ r5
          .long cr_alignment   @ r6
          .long init_thread_union + THREAD_START_SP @ sp
          。。。。。。。
         .type __mmap_switched, %function
         __mmap_switched:
          adr r3, __switch_data + 4
         
          ldmia r3!, {r4, r5, r6, r7}
          cmp r4, r5    @ Copy data segment if needed
         1: cmpne r5, r6
          ldrne fp, [r4], #4
          strne fp, [r5], #4
          bne 1b
         
          mov fp, #0    @ Clear BSS (and zero fp)
         1: cmp r6, r7
          strcc fp, [r6],#4
          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
         分析上述代码:
         mov pc ,r13
         r13中存储的是__switch_data目标内存区的首地址,这个地址上存储的是
         函数 __mmap_switched 的入口地址。
         这个函数完成了__data_loc数据段到__data_start数据段的数据复制、
         __bss_start BSS数据段的清0、设置SP指针、保存处理器ID到全局变量processor_id中、
         保存开发板机器架构类型到全局变量__machine_arch_type中、r0,r4压入栈、
         最后调用start_kernel,这个函数在init/main.c中开始正式运行操作系统。

总结:
  1 内核的启动过程:
    a uboot传入参数
    b 跳入到内核的起始地址;内核=head.o+misc.o+压缩内核镜像
    c 保存uboot传入的参数,然后检查、重定位代码
    d 打开cache,调整内核镜像地址、堆栈地址等为运行解压缩内核函数做准备
    e 调用 decompress_kernel 函数
         --> 调用平台相关的接口函数 arch_decomp_setup();
         --> 调用 makecrc();
         --> 调用 gunzip();
    f 调用汇编函数b call_kernel,call_kernel中刷新cache,关cache,然后把
      uboot传入的三个参数再传入到解压缩后的内核中。
    g 通过指令 mov pc,r4,跳转到内核入口
      r4中保存的是解压缩内核的入口地址,该地址为arch/arm/kernel/head.s中的
      .type stext, %function ENTRY(stext)
      -->读processor ID
         调用汇编函数 __lookup_processor_type 检查内核是否支持该处理器
      -->调用汇编函数 __lookup_machine_type   检查内核是否支持该目标板
      -->调用汇编函数 __create_page_tables    创建页表
    h 接下来调用arch/arm/mm/proc-arm920.S中的汇编函数 __arm920_setup
    i 接下来返回调用arch/arm/kernel/head.s中的__enable_mmu
         -->__turn_mmu_on
         -->mov pc, r13即 ./arch/arm/kernel/head-common.S中的标号: __switch_data
            -->再调用 __mmap_switched 完成数据段的重定位、BSS段清0、设置SP、保存全局变量
    J 接下来就是b start_kernel 该函数位于init/main.c中
    此次分析完毕,接开始部分。。
   
    在分析start_kernel启动过程时,
        -->setup_arch
           这个函数中搜索支持的单板,然后对这三个全局变量进行赋值
            init_arch_irq = mdesc->init_irq;
      system_timer = mdesc->timer;
      init_machine = mdesc->init_machine;
      来完成对目标板的指针接口。
      以后再来分析其他的驱动启动过程。。。。。。。
参考:
  http://blog.csdn.net/lanmanck/article/details/4288048
  http://blog.163.com/fj_ltls/blog/static/1380271112011610101118227/
  http://blog.chinaunix.net/uid-20672257-id-2891129.html