龙芯内核启动流程(一)

  • 了解龙芯内核启动流程。

1.MIPS CPU Address

  MIPS CPU运行时有三种状态:用户模式(User Mode);核心模式(Kernel Mode);管理模式(Supervisor Mode)。其中管理模式不常用。用户模式下,CPU只能访问KUSeg;当需要访问KSeg0、Kseg1和Kseg2时,必须使用核心模式或管理模 式。

  32位MIPS CPU将程序地址空间分为4部分:
在这里插入图片描述

  • Kuseg:0×00000000~0×7FFFFFFF(2G)。这些地址是用户态可用的地址。 在有MMU的机器里,这些地址将一概被转换。除非MMU已经设置好,否则不应该使用这些地址;对于没有MMU的处理器,这些地址的行为与具体处理器有关。 如果想要你的代码能够移植到无MMU的处理器上,或者能够在无MMU的处理器间移植,应避免使用这块区域。

  • Kseg0:0×80000000~0×9FFFFFFF(512M)。只要把最高位清零,这些地址就会转换成物理地址,映射到连续的 低端512M的物理空间上。这段区域的地址几乎总是要通过高速缓存来存取(write-back,或write-through模式),所以在高速缓存初 始化之前,不能使用。因为这种转换极为简单,且通过Cache访问,因而常称为这段地址为Unmapped Cached。这个区域在无MMU的系统中用来存放大多数程序和数据;在有MMU的系统中用来存放操作系统核心。

  • KSeg1:0xA0000000~0xBFFFFFFF(512M)。这些地址通过把最高三位清零,映射到连续的第算512M的物理地址,即 Kseg1和Kseg0对应同一片物理内存。但是与通过Kseg0的访问不同,通过Kseg1访问的话,不经过高速缓存。 Kseg1是唯一的在系统加电/复位时,能正常访问的地址空间,这也是为什么复位入口点(0xBFC00000)放在这个区域的原因,即对应复位物理地址 为0×1FC00000。因而,一般情况下利用该区域存储Bootlooder;大多数人也把该区域用作IO寄存器(这样可以保证访问时,是直接访问 IO,而不是访问cache中内容),因而建议IO相关内容也要放在物理地址的512M空间内。

  • KSeg2:0xC0000000~0xFFFFFFFF(1G)。这块区域只能在核心态下使用,并且要经过MMU的转换,因而在MMU设置好之前,不要存取该区域。除非你在写一个真正的操作系统,否则没有理由使用Kseg2。

2.内核启动

在这里插入图片描述
(1)主核core0 启动linux的入口是 kernel_entry(在arch/mips/kerenl/head.S中定义),跳转到kernel_entry_setup(在arch\mips\include\asm\mach-cavium-octeon\kernel-entry-init.h定义),因为BootLoader将主核core0的寄存器a2设置为1,所以跳转到octeon_main_processor,继而跳转到start_kernel继续启动linux;

(2)从核启动linux的入口也是kernel_entry,因为BootLoader将从核的寄存器a2设置为0,于是进入octeon_spin_wait_boot等待主核core0唤醒;

(3)主核core0执行start_kernel,调用boot_cpu_init,设置core0点cpu状态为online(可被 调度 ),active(可被 迁移 ), present( 被内核接管 ) , possible(系统存在,但没有被内核接管);

(4)主核 core0调用setup_arch进行mips体系结构相关初始化,与SMP相关的有:prom_init调用octeon_setup_smp注册octeon_smp_ops结构体,赋值给全局变量struct plat_smp_ops *mp_ops,是多核启动的操作函数集;plat_smp_setup调用 plat_smp_ops函数集octeon_smp_ops 的octeon_smp_setup根据coremask参数设置可以被启动点从核的各cpu状态为 present( 被内核接管 )和 possible(系统存在,但没有被内核接管),设置各从核cpu点logic map。

(5)主核 core0在kernel_init线程执行smp_prepare_cpus,调用octeon_smp_ops 的octeon_prepare_cpus初始化mailbox并申请mailbox中断;

(6)主核 core0执行smp_init,调用idle_threads_init初始化idle线程,然后调用cpu,最终调用octeon_smp_ops的octeon_boot_secondary,将 octeon_processor_sp和octeon_processor_gp指向idle线程的stack,将octeon_processor_sp赋值为待启动从核点逻辑core id;

(7)从核 octeon_spin_wait_boot检测到 octeon_processor_sp和自己的core id匹配了,就跳出 octeon_spin_wait_boot,执行smp_bootstrap,调用start_secondary,进而调用 octeon_smp_ops 的octeon_init_secondary和octeon_smp_finish;

(8)从核执行cpu_startup_entry进入idle loop。

2.1.启动流程分析

  系统加电启动后,任何MIPS Core都是从系统的虚拟地址0xbfc00000(BIOS入口地址)启动的,其对应的物理地址为0x1FC00000。因为上述地址处于kseg1中,所以此时系统不需要TLB映射就能够运行(这段空间通过去掉最高的三位来获得物理地址)。CPU从物理地址0x1FC00000开始取第一条指令,这个地址在硬件上已经确定为FLASH(BIOS)的位置,BIOS将Linux内核镜像文件拷贝到RAM中某个空闲地址(LOAD地址)处,然后一般有个内存移动的操作(Entry point(EP)的地址),最后BIOS跳转到EP指定的地址运行,此时开始运行Linux kernel。

  CPU根据型号不同,一个CPU里面可能集成了多个Core。这些Core里面,在上电时,只有core 0(一般称之为主核,其它核称为从核)会从reset状态跳转到物理地址0x1FC00000(我们的flash起始地址会映射成这个值),开始执行相关的一系列初始化代码,如内存、外设等等。而另外一些核则仍处于reset状态,只有core 0主动去唤醒它们时,从核才有可能开始正常运转。

  在编译完内核时,一般情况下生成两个版本的内核vmlinux与vmlinuz。其中vmlinux为非压缩版内核,vmlinuz为压缩版内核(包含内核自解压程序)。使用readelf -l vmlinux 命令可以读到LOAD地址,这个地址是由arch/mips/kernel/vmlinux.lds决定的:

  1 OUTPUT_ARCH(mips)                                                                                                                                                                                              
  2 ENTRY(kernel_entry)
  3 PHDRS {
  4  text PT_LOAD FLAGS(7);
  5  note PT_NOTE FLAGS(4);
  6 }
  7  jiffies = jiffies_64;
  8 SECTIONS
  9 {
 10  . = 0xffffffff80200000;
 11  _text = .;
 12  .text : {
 13   . = ALIGN(8); *(.text.hot .text .text.fixup .text.unlikely) *(.text..refcount) *(.ref.text) *(.meminit.text*) *(.memexit.text*)
 14   . = ALIGN(8); __sched_text_start = .; *(.sched.text) __sched_text_end = .;
 15   . = ALIGN(8); __cpuidle_text_start = .; *(.cpuidle.text) __cpuidle_text_end = .;
 16   . = ALIGN(8); __lock_text_start = .; *(.spinlock.text) __lock_text_end = .;
 17   . = ALIGN(8); __kprobes_text_start = .; *(.kprobes.text) __kprobes_text_end = .;
 18   . = ALIGN(8); __irqentry_text_start = .; *(.irqentry.text) __irqentry_text_end = .;
 19   . = ALIGN(8); __softirqentry_text_start = .; *(.softirqentry.text) __softirqentry_text_end = .;
 20   *(.text.*)
 21   *(.fixup)
 22   *(.gnu.warning)
 23  } :text = 0
 24  _etext = .;
 25  . = ALIGN(16); __ex_table : AT(ADDR(__ex_table) - 0) { __start___ex_table = .; KEEP(*(__ex_table)) __stop___ex_table = .; }
 26  __dbe_table : {
 27   __start___dbe_table = .;
 28   *(__dbe_table)
 29   __stop___dbe_table = .;
 30  }

关于Entry point(EP)的一些说明:

  EP(ELF可以读到)地址是BIOS移动完内核后,直接跳转的地址(控制权由BIOS转移到KERNEL)。这个地址由ld写入ELF的头中,会依次用下面的方法尝试设置入口地址,当遇见成功时则停止:

a.命令行选项 -e entry; 
b.脚本(vmlinux.lds)中的ENTRY(xxx); 
c.如果有定义start符号,则使用start符号(xxx); 
d.如果存在.text节,则使用第一个字节的地址; 
e.地址0

  由于上述ld 脚本(vmlinux.lds)中,用ENTRY宏设置了内核的EP是kernel_entry (KE)函数的地址,所以内核取得控制权(BIOS跳转之后)后执行的第一条指令就是 KE函数。

注意:这种情况只是vmlinux(非压缩版的内核),对于vmlinuz(压缩版的内核),EP会被设置成内核自解压缩的程序代码的地址,这样固件就会跳转到内核自解压代码(此时的EP为解压程序的代码地址),最后还是会到KE函数去执行。
vmlinux : 执行入口是arch/mips/boot//head.S中的kernel_entry;
vmlinuz : 解压前真正执行入口是arch/mips/boot/compressed/head.S中的start标号;

  由以上分析可知无论是压缩版还是非压缩版的Linux内核,内核第一个执行的函数是KE。kernel_entry()函数是体系结构相关的汇编语言,它首先初始化内核堆栈段,来为创建系统中的第一个进程进行准备,接着用一段循环将内核映像的未初始化数据段(bss段,在_edata和_end之间)清零,最后跳转到 /init/main.c 中的 start_kernel()初始化硬件平台相关的代码。源代码如下:

NESTED(kernel_entry, 16, sp)            # KE函数定义,函数栈的大小为16字节

    kernel_entry_setup          # 对CPU的配置,详情见kernel_entry_setup函数分析NOTE1

    setup_c0_status_pri         #设置mips协处理器(cp0)中的寄存器,详情见NOTE2

    PTR_LA  t0, 0f
    jr  t0
0:

#ifdef CONFIG_MIPS_MT_SMTC     #硬件多线程
    mtc0    zero, CP0_TCCONTEXT
    mfc0    t0, CP0_STATUS
    ori t0, t0, 0xff1f
    xori    t0, t0, 0x001e
    mtc0    t0, CP0_STATUS
#endif /* CONFIG_MIPS_MT_SMTC */

    PTR_LA      t0, __bss_start     # 清除BSS段,详情见NOTE3
    LONG_S      zero, (t0)
    PTR_LA      t1, __bss_stop - LONGSIZE
1:
    PTR_ADDIU   t0, LONGSIZE
    LONG_S      zero, (t0)
    bne     t0, t1, 1b

    LONG_S      a0, fw_arg0     # BIOS传参数,详情见NOTE4
    LONG_S      a1, fw_arg1
    LONG_S      a2, fw_arg2
    LONG_S      a3, fw_arg3

    MTC0        zero, CP0_CONTEXT   # NOTE5 
    PTR_LA      $28, init_thread_union    #为0号进程准备内核栈,详情见NOTE6
    PTR_LI      sp, _THREAD_SIZE - 32 - PT_SIZE
    PTR_ADDU    sp, $28
    back_to_back_c0_hazard #NOTE7
    set_saved_sp    sp, t0, t1 #NOTE6
    PTR_SUBU    sp, 4 * SZREG       #NOTE8

    j       start_kernel  #NOTE9
    END(kernel_entry)

    __CPUINIT

1).NOTE1(kernel_entry_setup函数分析):

  KE第一个调用函数则是kernel_entry_setup(arch/mips/include/asm/mach-loongson/kernel-entry-init.h)
源代码:


#ifndef __ASM_MACH_LOONGSON_KERNEL_ENTRY_H
#define __ASM_MACH_LOONGSON_KERNEL_ENTRY_H
    .macro  kernel_entry_setup
#ifdef CONFIG_CPU_LOONGSON3
    .set    push
    .set    mips64
    /* Set LPA on LOONGSON3 config3 */
    mfc0    t0, $16, 3
    or  t0, (0x1 << 7)
    mtc0    t0, $16, 3
    /* Set ELPA on LOONGSON3 pagegrain */
    mfc0    t0, $5, 1
    or  t0, (0x1 << 29)
    mtc0    t0, $5, 1
#ifdef CONFIG_LOONGSON3_ENHANCEMENT
    /* Enable STFill Buffer */
    mfc0    t0, $16, 6
    or  t0, 0x100
    mtc0    t0, $16, 6
#endif
    _ehb
    .set    pop
#endif
    .endm

  通常情况下这个函数的实现与具体的CPU有关,阅读这段代码得结合LOONGSON CPU手册。 这个函数的作用是设置CPU,由于LOONGSON是基于MIPS架构架构的,所以对CPU的设置是对CPU协处理器(CP0)的寄存器进行设置来设置CPU。kernel_entry_setup函数主要做了俩件事情:

  • 使LOONGSON CPU支持大物理地址;
  • 使LOONGSON CPU支持合并写功能。

2).NOTE2(setup_c0_status_pri函数分析):

    mfc0    t0, CP0_STATUS
    or  t0, ST0_CU0|\set|0x1f|\clr
    xor t0, 0x1f|\clr
    mtc0    t0, CP0_STATUS
    .set    noreorder
    sll zero,3              # ehb
    .set    pop
    .endm
.macro  setup_c0_status_pri 
#ifdef CONFIG_64BIT
#ifdef CONFIG_CPU_LOONGSON3
    setup_c0_status ST0_KX|ST0_MM 0  #(1#else
    setup_c0_status ST0_KX 0
#endif
#else
#ifdef CONFIG_CPU_LOONGSON3
    setup_c0_status ST0_MM 0 
#else
    setup_c0_status 0 0
#endif
#endif
    .endm

  setup_c0_status_pri函数与具体的CPU有关的汇编实现的。根据芯片手册及代码可以看出这个函数主要做了一下几个事情:

  • (1)使能XTLB Refill列外向量;
  • (2)使能协处理器2;
  • (3)使能协处理器0;
  • (4)关闭中断;
  • (5)还有一些其他位设置。

3).NOTE3(清除BBS段):

    PTR_LA      t0, __bss_start     
    LONG_S      zero, (t0)
    PTR_LA      t1, __bss_stop - LONGSIZE
1:
    PTR_ADDIU   t0, LONGSIZE
    LONG_S      zero, (t0)
    bne     t0, t1, 1b

  以bss_start为起始地址,步调为LONGSIZE(LOONGSON 是64位处理器,所以LOONGSIZE为8),终点地址为 bss_stop-LONGSIZE做循环清零的事情。

4).NOTE4(BIOS传参):

    LONG_S      a0, fw_arg0     
    LONG_S      a1, fw_arg1
    LONG_S      a2, fw_arg2
    LONG_S      a3, fw_arg3

  固件将要传递的参数的地址放在了a0,a1,a2,a3寄存器中,通过这段代码将地址赋予fw_arg*等变量。这段代码通过传递地址间接做参数传递。

5).NOTE5:

MTC0        zero, CP0_CONTEXT  

  清除CP0的Context寄存器,这个寄存器用来保存页表的起始地址

6).NOTE6(为0号进程准备内核栈):

    PTR_LA      $28, init_thread_union    
    PTR_LI      sp, _THREAD_SIZE - 32 - PT_SIZE
    PTR_ADDU    sp, $28
    set_saved_sp    sp, t0, t1 

源文件:arch/mips/include/asm/stackframe.h 
    .macro  set_saved_sp stackp temp temp2
    ASM_CPUID_MFC0  \temp, ASM_SMP_CPUID_REG
    LONG_SRL    \temp, SMP_CPUID_PTRSHIFT
    LONG_S  \stackp, kernelsp(\temp)
    .endm

如图所示:
在这里插入图片描述
代码片段2将SP保存到kernelsp数组中去。

其中kernelsp数组定义在arch/mips/kernel/setup.c中。
unsigned long kernelsp[NR_CPUS]; #NR_CPUS CPU核的个数

注意:代码片段2将SP最终保存到kernelsp数组中,它是以CPUID号作为数组的偏移,而CPUID是存在CP0 Context寄存器中的,虽然前面已经清零,但是在这一刻,CPU将ID存到了这个寄存器中。

由上图引发的一些问题:

  • 1、init_thread_union 是何物?它存在哪里?
  • 2、为何要将它的地址保存到GP?
  • 3、PT_SIZE作甚?
  • 4、为何将最后的SP保存到kernelsp数组中去?

如图所示:
在这里插入图片描述
2.2.获取入口地址

arch/mips/Makefile :
entry-y = $(shell $(objtree)/arch/mips/tools/elf-entry vmlinux)

~/work/code/linux/loongson/loongson-kernel/arch/mips/tools$ ./elf-entry ../../../vmlinux
0xffffffff81b62b80

refer to

  • https://techpubs.jurassic.nl/manuals/hdwr/developer/DevDrvrO2_PG/sgi_html/ch01.html
  • https://tams.informatik.uni-hamburg.de/applets/hades/webdemos/mips.html
  • https://jgsun.github.io/2019/05/08/octeon-mips-SMP-linux-startup/
  • https://www.cnblogs.com/morphling/p/3595287.html

猜你喜欢

转载自blog.csdn.net/weixin_41028621/article/details/108809089