别人分析的mips架构的linux启动第一步(作为对比)

系统加电启动后,MIPS处理器默认的程序入口是0xBFC00000(虚拟地址),此地址在KSEG1(无缓存)区域内,对应的物理地址是0x1FC00000高3位清零),所以CPU从物理地址0x1FC00000开始取第一条指令,这个地址在硬件上已经确定为FLASHBIOS)的位置,BIOS将Linux内核镜像文件拷贝到RAM中某个空闲地址(LOAD地址)处,然后一般有个内存移动的操作(Entry point(EP)的地址),最后BIOS跳转到EP指定的地址运行,此时开始运行Linux kernel。

关于LOAD地址的一些说明:

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

OUTPUT_ARCH(mips) 
ENTRY(kernel_entry) 
jiffies = jiffies_64; 
SECTIONS 
{ 
. = 0xFFFFFFFF80200000; 
/* read-only */ 
_text = .; /* Text and read-only data */ 
.text : { 
*(.text) 
…
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

关于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函数去执行。

由以上分析可知无论是压缩版还是非压缩版的Linux内核,内核第一个执行的函数是KE。接下来就是对KE函数的分析,看看它到底都做了些什么事?

kernel_entry(KE)分析:

内核版本:3.10.X
源代码文件:arch/mips/kernel/head.S
KE函数是体系相关的汇编语言实现的,源代码中汇编指令的含义(64位指令)为:

PTR_LA          dla
LONG_S          sd
PTR_ADDIU       daddiu
MTC0            dmtc0
PTR_LI          dli
PTR_ADDU        daddu
PTR_SUBU        dsubu
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

源代码:

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

NOTE1(kernel_entry_setup函数分析):

Linux内核犹如一座巨大的迷宫,只有找到了正确的入口,才有可能找到出口。

之前的分析得出的结论是Linux内核第一个调用的函数是KE,而KE第一个调用函数则是kernel_entry_setup,这才是真正执行的第一个函数,那么我们就从它开始吧。

函数名称: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
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

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

对CP0寄存器操作的说明:

MIPS刚刚出现的时候,最多可以有32个CP0寄存器。但是MIPS32/64可以允许多达256个寄存器。为了保持指令向前兼容,这是通过在CP0号(实际上是指令中以前编码为0的域)后附加3位的select域来实现的。这样代码

mfc0    t0, $16, 3
  
  
  • 1

解释为将寄存器号为16,查询号为3的寄存器的值读到t0寄存器中。

代码分析:

代码一:

mfc0    t0, $16, 3
or  t0, (0x1 << 7)
mtc0    t0, $16, 3
  
  
  • 1
  • 2
  • 3

将Config3寄存器的第7位(LPA)置一。

代码二:

mfc0    t0, $5, 1
or  t0, (0x1 << 29)
mtc0    t0, $5, 1
  
  
  • 1
  • 2
  • 3

将PageGrain寄存器的第29位(ELPA)置一。

代码三:

mfc0    t0, $16, 6
or  t0, 0x100
mtc0    t0, $16, 6
  
  
  • 1
  • 2
  • 3

将GSConfig3寄存器的第8位(STFill)置一。

LOONGSON2000芯片手册:

说明

LPA:当支持大物理地址范围时为1,此时允许物理地址的范围超过236字节(此时LOONGSON物理地址的范围为248字节)。

ELPA:当有LPA支持的时候,还会有一个额外的寄存器(PageGrain),同时EntryLo0-1EntryHi中的域的布局也会变化(如上图芯片手册所示)。

STFill:GSConfig 寄存器用于对处理器核部分微结构相关的功能进行动态配置。自动写合并功能属于处理器核部分微结构的功能。

自动写合并功能介绍

对于现代CPU而言,性能瓶颈则是对于内存的访问。CPU的速度往往都比主存的高至少两个数量级。因此CPU都引入了L1_cache与L2_cache,更加高端的cpu还加入了L3_cache.很显然,这个技术引起了下一个问题:

如果一个CPU在执行的时候需要访问的内存都不在cache中,CPU必须要通过内存总线到主存中取,那么在数据返回到CPU这段时间内(这段时间大致为cpu执行成百上千条指令的时间,至少两个数据量级)干什么呢? 答案是CPU会继续执行其他的符合条件的指令。比如CPU有一个指令序列 指令1 指令2 指令3 …, 在指令1时需要访问主存,在数据返回前CPU会继续后续的和指令1在逻辑关系上没有依赖的”独立指令“,CPU一般是依赖指令间的内存引用关系来判断的指令间的”独立关系”,具体细节可参见各CPU的文档。这也是导致CPU乱序执行指令的根源之一。

以上方案是CPU对于读取数据延迟所做的性能补救的办法。对于写数据则会显得更加复杂一点:

当CPU执行存储指令时,它会首先试图将数据写到离CPU最近的L1_cache, 如果此时CPU出现L1未命中,则会访问下一级缓存。速度上L1_cache基本能和CPU持平,其他的均明显低于CPU,L2_cache的速度大约比CPU慢20-30倍,而且还存在L2_cache不命中的情况,又需要更多的周期去主存读取。其实在L1_cache未命中以后,CPU就会使用一个另外的缓冲区,叫做合并写存储缓冲区。这一技术称为合并写入技术。在请求L2_cache缓存行的所有权尚未完成时,CPU会把待写入的数据写入到合并写存储缓冲区,该缓冲区大小和一个Cache Line大小相同,一般都是64字节。这个缓冲区允许CPU在写入或者读取该缓冲区数据的同时继续执行其他指令,这就缓解了CPU写数据时Cache Miss时的性能影响。

当后续的写操作需要修改相同的缓存行时,这些缓冲区变得非常有趣。在将后续的写操作提交到L2缓存之前,可以进行缓冲区写合并。 这些64字节的缓冲区维护了一个64位的字段,每更新一个字节就会设置对应的位,来表示将缓冲区交换到外部缓存时哪些数据是有效的

经过上述步骤后,缓冲区的数据还是会在某个延时的时刻更新到外部的缓存(L2_cache)。如果我们能在缓冲区传输到缓存之前将其尽可能填满,这样的效果就会提高各级传输总线的效率,以提高程序性能。

也许你要问,如果程序要读取已被写入缓冲区的某些数据,会怎么样?我们的硬件工程师已经考虑到了这点,在读取缓存之前会先去读取缓冲区的。

这一切对我们的程序意味着什么?

如果我们能在缓冲区被传输到外部缓存之前将其填满,那么将大大提高各级传输总线的效率。如何才能做到这一点呢?好的程序将大部分时间花在循环处理任务上。

这些缓冲区的数量是有限的,且随CPU模型而异。在LOONGSON CPU中,同一时刻只能拿到4个。这意味着,在一个循环中,你不应该同时写超过4个不同的内存位置,否则你将不能体验到合并写的好处。

从下面这个具体的例子来看吧:

下面一段测试代码,从代码本身就能看出它的基本逻辑。

测试代码:

#include <unistd.h>
#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <limits.h>

static const int iterations = 10000000;
static const int items = 1<<24;
static int mask;
static int arrayA[1<<24];
static int arrayB[1<<24];
static int arrayC[1<<24];
static int arrayD[1<<24];
static int arrayE[1<<24];
static int arrayF[1<<24];
static int arrayG[1<<24];
static int arrayH[1<<24];

double run_one_case_for_8(){
        double start_time;
        double end_time;
        struct timeval start;
        struct timeval end;
        int i = iterations;

    gettimeofday(&start, NULL);

    while(--i != 0){
        int slot = i & mask;
        int value = i;
        arrayA[slot] = value;
        arrayB[slot] = value;
        arrayC[slot] = value;
        arrayD[slot] = value;
        arrayE[slot] = value;
        arrayF[slot] = value;
        arrayG[slot] = value;
        arrayG[slot] = value;
    }

    gettimeofday(&end, NULL);
        start_time = (double)start.tv_sec + (double)start.tv_usec/1000000.0;
        end_time = (double)end.tv_sec + (double)end.tv_usec/1000000.0;
        return end_time - start_time;
}
double run_two_case_for_4(){
    double start_time;
    double end_time;
    struct timeval start;
    struct timeval end;
    int i = iterations;
    gettimeofday(&start, NULL);

    while(--i != 0){
        int slot = i & mask;
        int value = i;
        arrayA[slot] = value;
        arrayB[slot] = value;
        arrayC[slot] = value;
        arrayD[slot] = value;
    }

    i = iterations;

    while(--i != 0){
        int slot = i & mask;
        int value = i;
        arrayE[slot] = value;
        arrayF[slot] = value;
        arrayG[slot] = value;
        arrayH[slot] = value;
    }
    gettimeofday(&end, NULL);
    start_time = (double)start.tv_sec + (double)start.tv_usec/1000000.0;
    end_time = (double)end.tv_sec + (double)end.tv_usec/1000000.0;
    return end_time - start_time;
}

int main(){
    mask = items -1;
    int i;
    printf("Test Begin---->\n");
    for(i=0;i<3;i++){
        printf("%d, run_one_case_for_8: %lf\n",i, run_one_case_for_8());
        printf("%d, run_two_case_for_4: %lf\n",i, run_two_case_for_4());
    }
    printf("Test End\n");
    return 0;
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90

测试环境:Fedora 21 64bits, 8G DDR3内存,LOONGSON-3A2000@999MHz
相信很多人会认为run_two_case_for_4 的运行时间肯定要比run_one_case_for_8的长,因为至少前者多了一遍循环的i++操作。但是事实却不是这样。
测试结果:
测试截图
原理:上面提到的合并写存入缓冲区离CPU很近,容量为64字节,很小了,估计很贵。数量也是有限的,个数是依赖CPU模型的,LOONGSON的CPU在同一时刻只能拿到4个(将上面的代码做改写可以证明)。

因此,run_one_case_for_8函数中连续写入8个不同位置的内存,那么当4个数据写满了合并写缓冲时,cpu就要等待合并写缓冲区更新到L2cache中,因此CPU就被强制暂停了。然而在run_two_case_for_4函数中是每次写入4个不同位置的内存,可以很好的利用合并写缓冲区,因合并写缓冲区满到引起的CPU暂停的次数会大大减少,当然如果每次写入的内存位置数目小于4,也是一样的。虽然多了一次循环的i++操作(实际上你可能会问,i++也是会写入内存的啊,其实i这个变量保存在了寄存器上), 但是它们之间的性能差距依然非常大。

从上面的例子可以看出,这些CPU底层特性对程序员并不是透明的。程序的稍微改变会带来显著的性能提升。对于存储密集型的程序,更应当考虑到此到特性。

小结:

kernel_entry_setup函数主要做了俩件事情:
(1)使LOONGSON CPU支持大物理地址;
(2)使LOONGSON CPU支持合并写功能。

NOTE2(setup_c0_status_pri函数分析):

内核版本:linux-3.10.X
源代码文件:arch/mips/kernel/head.S
源代码:

    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
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

setup_c0_status_pri函数与具体的CPU有关的汇编实现的,所以必须参考LOONGSON CPU手册才能知道这个函数到底做了什么。初步可以看出来对于LOONGSON 3A-2000来说这个函数 调用setup_c0_status函数,这个函数有个CONFIG_MIPS_MT_SMTC宏,这个宏是个开关,决定LOONGSON CPU是否支持硬件多线程,而LOONGSON CPU不支持硬件多线程(关于MIPS的多线程,请看See MIPS Run Linux和MIPS硬件多线程介绍)。我们结合手册看看setup_c0_status函数到底做了啥事,依照代码初步可以看出来这个函数主要设置CP0 Status 寄存器。
芯片手册:
这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

小结:

根据芯片手册及代码可以看出这个函数主要做了一下几个事情:
(1)使能XTLB Refill列外向量;
(2)使能协处理器2;
(3)使能协处理器0;
(4)关闭中断;
(5)还有一些其他位设置,详情请看LOONGSON 3A-2000用户手册下册(7.21)。

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
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

小结:

这段代码做清除整个BSS段。

NOTE4(BIOS传参):

源代码:

    LONG_S      a0, fw_arg0     
    LONG_S      a1, fw_arg1
    LONG_S      a2, fw_arg2
    LONG_S      a3, fw_arg3
  
  
  • 1
  • 2
  • 3
  • 4

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

小结:

这段代码通过传递地址间接做参数传递。

NOTE5:

MTC0        zero, CP0_CONTEXT   
  
  
  • 1
小结:

清除CP0的Context寄存器,这个寄存器用来保存页表的起始地址(详情见芯片手册)。

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

源代码:
代码片段1:

    PTR_LA      $28, init_thread_union    
    PTR_LI      sp, _THREAD_SIZE - 32 - PT_SIZE
    PTR_ADDU    sp, $28
    set_saved_sp    sp, t0, t1 
  
  
  • 1
  • 2
  • 3
  • 4

源文件:arch/mips/include/asm/stackframe.h
代码片段2:

    .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
  
  
  • 1
  • 2
  • 3
  • 4
  • 5

这段代码主要做了什么?
一图胜万语:
这里写图片描述
代码片段2将SP保存到kernelsp数组中去。
其中kernelsp数组定义在arch/mips/kernel/setup.c中。

unsigned long kernelsp[NR_CPUS]; #NR_CPUS CPU核的个数
  
  
  • 1

注意:代码片段2将SP最终保存到kernelsp数组中,它是以CPUID号作为数组的偏移,而CPUID是存在CP0 Context寄存器中的,虽然前面已经清零,但是在这一刻,CPU将ID存到了这个寄存器中。
由上图引发的一些问题:
1、init_thread_union 是何物?它存在哪里?
2、为何要将它的地址保存到GP?
3、PT_SIZE作甚?
4、为何将最后的SP保存到kernelsp数组中去?
一图胜万语:
这里写图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于MIPS体系结构当前发展的资料是最新的,相对于第一版而言,增加了许多新材料,包括对MIPS32和MIPS64对体系结构的标准化、多线程等全新的扩展,对广为流行的Linux系统在MIPS体系结构上的实现也做了很好的处理。是初学者较好的了解MIPS体系结构的材料,也是有经验开发人员好的参考材料。 译者说明 译为主。个别情况下参考某些译法进行慘订,或者根捃译者的理解直接翻详。例 如对于 cache alias的翻译,以及第七章对于两种非数NaN的翻译等等 第十一章对于 JTAG probe的翻译,处理有所不同。一种较为广泛的译法 是译成“探针,”译者也采用了这种译法。但“探针”一词在缺乏上下文的情况 下容易引起歧义,也容易让不熟悉的人认为它是一种细长的针形设备。有鉴于 此,文中也给出∫另一种译法,即“接探器。”“接探”一词漾于“JTAG"的音译“接 探格”-一又可以理解为意译,即“·种集成电路的连接测试、探测的标准规 格。”这样理解, probe作为符合“接探格”的设备,译成“接探器”就很自然,兼顾 了音和意,也消除∫歧义。译文中同时采用∫两种译法,但基木侏持一鈫。哪种 译法更好,读者可自己判断 从207年7月27日的草稿,到2007年9月7日的初稿,其作了较大的 修订。此后又有两次局部的修订,更正了译文中的一些错误。2007年12月31日 的版本作了更全而的修订,修订范闱主要是针对书的后半部分。该次修订的幅 度较大,除了纠止笔误外,对些译得不够准确、比较生硬、或者衔接不好的地 方也作了不少调整,某些不够清晰的插图重新制作,个别表格排版有轻微变动。 此外,该次修订在书后增加了《译者补遗》一节,介绍了几条与PC代码有关的 MIPS汇编指小语句的说明,希望对部分读者有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值