powerpc-linux内核start_kernel之前启动分析-开门见山

Posted kerneler_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了powerpc-linux内核start_kernel之前启动分析-开门见山相关的知识,希望对你有一定的参考价值。

前段时间做了公司内核从arm处理器到一款powerpc处理器的移植(公司处理器换核,前期用FPGA仿真板进行了芯片验证和软件移植),借这个机会也学习了powerpc处理器的一些知识,对powerpc的内核启动也有所了解。
完成了arm版本内核start_kernel之前汇编分析,也想尝试写几篇文章对powerpc的启动进行分析,与大家分享。

在分析之前,首先罗嗦几句,这次移植工作之前我从来没有接触过powerpc相关的知识,因此是作为一个初学者来完成这次移植工作的。
初学者要完成一次完整的内核移植,我觉得首要一点是认真学习该处理器核的芯片手册,以我移植的处理器(ppc460s)为例,首先要找到该处理器核的芯片手册,不像SOC芯片手册会介绍SOC的外设资源,处理器核芯片手册介绍的是处理器内部逻辑以及特性,手册需要重点关注的几点有:
(1)地址空间管理,包括上电取指地址,MMU如何配置使用等
(2)寄存器定义,包括通用寄存器,特殊功能寄存器
(3)中断异常的定义,异常向量表如何定义和使用
(4)cache管理
(5)处理器核reset后如何初始化,处理器核手册会给出一些标准的步骤
(6)处理器核特定资源介绍,如ppc460s核内集成了timer,有指定指令进行操作

以上所述都是处理器核的特性,肯定会在处理器核的手册中进行介绍,学习了这些,再对基于该处理器核的SOC地址空间布局(看SOC手册,地址空间分布是由处理器核外的地址总线仲裁决定的)加以学习,对这款处理器就有了内在的了解。
当然处理器指令集也是一项处理器核特性,各架构处理器表面看最大的区别就是指令集不一样,但是我个人感觉指令集并不需要特别的关注,只需要看懂指令含义,会写最基本的指令即可,指令是我们操纵处理器的工具。处理器的核心区别还是在上述的几点。
每种架构处理器都会有些通用的处理器特性定义,也有专门的资料来介绍,可以与处理器核手册配合着看,如powerpc的BOOKE,MIPS的《see mips run》,ARM的《ARM指令体系架构》等。这些资料都对处理器的上述特性做了一些通用的定义。
linux内核在start_kernel之前的大部分工作都是对处理器核的一个初始化工作,将整个内核启动过程全部了解后你就会发现,内核启动其实就是一个处理器SOC由内到外进行初始化的过程!
了解了处理器核的特性,接下来就可以进行内核移植,内核移植的大体思路我总结了一篇文章,连接如下:
http://blog.csdn.net/skyflying2012/article/details/43281565

linux内核的入口是在_start,对于如何根据链接脚本来判断内核入口函数可以看我的arm内核启动分析第一篇文章:
http://blog.csdn.net/skyflying2012/article/details/41344377

kernel版本:3.4.55
处理器:ppc460s

今天首先来分析_start的前几行汇编,ppc460s属于ppc的44x系列,根据arch/powerpc/kernel/Makefile,_start在arch/powerpc/kernel/head_44x.S中。powerpc各个系列处理器差别较大,因此head文件也有好多个,本文只分析460s所属的head_44x.S,首先来看前几行汇编:

_ENTRY(_stext);
_ENTRY(_start);
    /*
     * nop指令在后面会被abatron_pteptrs页表指针替换掉
     * 
     */
    nop
    mr  r31,r3      /* save device tree ptr */
    li  r24,0       /* CPU number */

    //没有选RELOCATABLE,不走该分支
#ifdef CONFIG_RELOCATABLE
/*
 * Relocate ourselves to the current runtime address.
 * This is called only by the Boot CPU.
 * "relocate" is called with our current runtime virutal
 * address.
 * r21 will be loaded with the physical runtime address of _stext
 */
    bl  0f              /* Get our runtime address */
0:  mflr    r21             /* Make it accessible */
    addis   r21,r21,(_stext - 0b)@ha
    addi    r21,r21,(_stext - 0b)@l     /* Get our current runtime base */

    /*
     * We have the runtime (virutal) address of our base.
     * We calculate our shift of offset from a 256M page.
     * We could map the 256M page we belong to at PAGE_OFFSET and
     * get going from there.
     */
    lis r4,KERNELBASE@h
    ori r4,r4,KERNELBASE@l
    rlwinm  r6,r21,0,4,31           /* r6 = PHYS_START % 256M */
    rlwinm  r5,r4,0,4,31            /* r5 = KERNELBASE % 256M */
    subf    r3,r5,r6            /* r3 = r6 - r5 */
    add r3,r4,r3            /* Required Virutal Address */

    bl  relocate
#endif

    bl  init_cpu_state

内核与bootloader有传参约定,主流bootloader都会做适配,对于powerpc来说,传参约定如r3中存储设备树地址,r4 r5存储ramdisk首尾地址,r6 r7存储命令行首尾地址。
将设备树地址存储在r31之后,_start跳转到init_cpu_state执行,这也是本文重点分析的函数,如下:(将不走的条件编译以及部分注释去掉)

_GLOBAL(init_cpu_state)
    mflr    r22  //返回地址保存在r22寄存器中

/*
 * In case the firmware didn't do it, we apply some workarounds
 * that are good for all 440 core variants here
 */
    mfspr   r3,SPRN_CCR0    
    //CCR0-1寄存器是ppc的核心控制寄存器,配置处理器核的一些特性,如cache
    rlwinm  r3,r3,0,0,27    /* disable icache prefetch */
    isync
    mtspr   SPRN_CCR0,r3
    isync
    sync
/*
 * 对处理器64个TLB进行搜索,找到当前代码所在TLB。
 * 将其余TLB全部无效掉
 */

    //因当前代码全部是指令存储而不是数据存储,因此保证MSR的IS置1
    //再进行tlbsx,查找当前代码所使用TLB编号
    /* Load our current PID->MMUCR TID and MSR IS->MMUCR STS */
    mfspr   r3,SPRN_PID         /* Get PID */
    mfmsr   r4              /* Get MSR */
    andi.   r4,r4,MSR_IS@l          /* TS=1? */
    beq wmmucr              /* If not, leave STS=0 */
    oris    r3,r3,PPC44x_MMUCR_STS@h    /* Set STS=1 */
wmmucr: mtspr   SPRN_MMUCR,r3           /* Put MMUCR */
    sync

    //找代码所使用的TLB,找到后将剩余63个TLB无效掉
    bl  invstr              /* Find our address */
invstr: mflr    r5              /* Make it accessible */
    tlbsx   r23,0,r5            /* Find entry we are in */
    li  r4,0                /* Start at TLB entry 0 */
    li  r3,0                /* Set PAGEID inval value */
1:  cmpw    r23,r4              /* Is this our entry? */
    beq skpinv              /* If so, skip the inval */
    tlbwe   r3,r4,PPC44x_TLB_PAGEID     /* If not, inval the entry */
skpinv: addi    r4,r4,1             /* Increment */
    cmpwi   r4,64               /* Are we done? */
    bne 1b              /* If not, repeat */
    isync                   /* If so, context change */

/*
 * 在63号TLB配置固定的内核起始地址静态映射,63号TLB以后就不会再修改了,之后用户空间动态映射时只是用0-62号TLB
 * 以我的460s处理器为例,这里就需要进行修改移植,内存物理地址起始是0x80000000.需要将下面的0改为0x8000000
 * ARM获取起始物理地址的方法是根据当前的PC值,这种方法兼容性好,移植不需要修改head.c文件。
 * ppc可以借鉴这种方法,不过需要首先根据当前使用的TLB来获取物理地址才可以。这是我提出的一点改进意见。
 * 这里完成的是0x80000000==>0xc0000000的256MB静态映射
 */
    lis r3,PAGE_OFFSET@h
    ori r3,r3,PAGE_OFFSET@l

    /* Kernel is at the base of RAM */
    li r4, 0            /* Load the kernel physical address */
    /* Load the kernel PID = 0 */
    li  r0,0
    mtspr   SPRN_PID,r0
    sync

    /* Initialize MMUCR */
    li  r5,0
    mtspr   SPRN_MMUCR,r5
    sync

    /* pageid fields */
    clrrwi  r3,r3,10        /* Mask off the effective page number */
    ori r3,r3,PPC44x_TLB_VALID | PPC44x_TLB_256M

    /* xlat fields */
    clrrwi  r4,r4,10        /* Mask off the real page number */
                    /* ERPN is 0 for first 4GB page */

    /* attrib fields */
    /* Added guarded bit to protect against speculative loads/stores */
    li  r5,0
    ori r5,r5,(PPC44x_TLB_SW | PPC44x_TLB_SR | PPC44x_TLB_SX | PPC44x_TLB_G)

        li      r0,63                    /* TLB slot 63 */

    tlbwe   r3,r0,PPC44x_TLB_PAGEID /* Load the pageid fields */
    tlbwe   r4,r0,PPC44x_TLB_XLAT   /* Load the translation fields */
    tlbwe   r5,r0,PPC44x_TLB_ATTRIB /* Load the attrib/access fields */

    //这里之前,内核的运行地址与链接地址(0xc开头)不一定一致(这要看bootloader中如何配置TLB),
    //好在都是一些汇编,没有绝对寻址,保证了位置无关。
    //63号TLB填写完毕后,该TLB映射的虚拟地址保证了与内核链接地址一致,接下来就需要跳转到与链接地址一致的地址运行。
    //这里采用的是标号跳转,因为汇编中的标号地址是在链接时确定下来的,标号地址是链接地址。
    //rfi指令会跳转到SRR0中的地址执行,就完成了地址的跳转!
    /* Force context change */
    mfmsr   r0
    mtspr   SPRN_SRR1, r0
    lis r0,3f@h
    ori r0,r0,3f@l
    mtspr   SPRN_SRR0,r0
    sync
    rfi

    //跳转到链接地址后,将原来的TLB无效掉
    /* If necessary, invalidate original entry we used */
3:  cmpwi   r23,63
    beq 4f
    li  r6,0
    tlbwe   r6,r23,PPC44x_TLB_PAGEID
    isync

4:
    //除了内核起始内存地址的256MB静态映射,如果需要早期调试串口
    //则可将串口IO所在地址空间在62号TLB进行静态映射
    //对于移植工作,串口打印还是非常重要的调试工具
#ifdef CONFIG_PPC_EARLY_DEBUG_44x
    /* Add UART mapping for early debug. */

    /* pageid fields */
    lis r3,PPC44x_EARLY_DEBUG_VIRTADDR@h
    ori r3,r3,PPC44x_TLB_VALID|PPC44x_TLB_TS|PPC44x_TLB_64K

    /* xlat fields */
    lis r4,CONFIG_PPC_EARLY_DEBUG_44x_PHYSLOW@h
    ori r4,r4,CONFIG_PPC_EARLY_DEBUG_44x_PHYSHIGH

    /* attrib fields */
    li  r5,(PPC44x_TLB_SW|PPC44x_TLB_SR|PPC44x_TLB_I|PPC44x_TLB_G)
        li      r0,62                    /* TLB slot 0 */

    tlbwe   r3,r0,PPC44x_TLB_PAGEID
    tlbwe   r4,r0,PPC44x_TLB_XLAT
    tlbwe   r5,r0,PPC44x_TLB_ATTRIB

    /* Force context change */
    isync
#endif /* CONFIG_PPC_EARLY_DEBUG_44x */

    //将16种异常的入口偏移量写入IVOR0-15寄存器
    /* Establish the interrupt vector offsets */
    SET_IVOR(0,  CriticalInput);
    SET_IVOR(1,  MachineCheck);
    SET_IVOR(2,  DataStorage);
    SET_IVOR(3,  InstructionStorage);
    SET_IVOR(4,  ExternalInput);
    SET_IVOR(5,  Alignment);
    SET_IVOR(6,  Program);
    SET_IVOR(7,  FloatingPointUnavailable);
    SET_IVOR(8,  SystemCall);
    SET_IVOR(9,  AuxillaryProcessorUnavailable);
    SET_IVOR(10, Decrementer);
    SET_IVOR(11, FixedIntervalTimer);
    SET_IVOR(12, WatchdogTimer);
    SET_IVOR(13, DataTLBError44x);
    SET_IVOR(14, InstructionTLBError44x);
    SET_IVOR(15, DebugCrit);

    b   head_start_common

init_cpu_state完成的主要工作有2个:TLB初始化异常初始化

TLB初始化有一个很有意思也非常重要的前提是powerpc的44x系列处理器核MMU是常开的,上电即开启。
powerpc上电第一条指令是在0xfffffffc,其地址空间最上部的4k空间在上电时是被一个影子TLB映射到物理地址。
该物理地址在何处则由处理器核外地址总线仲裁决定了,我的处理器逻辑是映射在了片内SRAM中。44x系列处理器有64个TLB,影子TLB不属于其中,它对软件来说是透明的,而64个TLB软件可以通过tlbwe tlbsx等指令进行操作,以上这些知识都可以在处理器核手册中看到。

因为460s的MMU常开,因此一上电bootloader就是运行在虚拟地址上。
powerpc的bootloader启动代码一般的设计为0xfffffffc处为跳转指令,跳转到4k空间起始的0xfffff000执行,对64个TLB进行配置,之后就可以访问我们想要访问的地址空间了。
bootloader中会对整个地址空间进行TLB配置,bootloader不需要太大的运行空间,一般都是配置为平映射。

所以与ARM内核不同,powerpc内核在启动时已经是运行在虚拟地址上了。但是否与其链接地址一致,内核是不能保证的。
所以出现了init_cpu_state中前部代码的情况,首先利用tlbsx找到当前代码所使用的TLB,无效掉剩余63个TLB,之后将内存起始物理地址静态映射到内核中的内存起始虚拟地址PAGE_OFFSET,该地址也是内核的链接地址。最后跳转到链接地址运行,之后就不用担心运行地址与链接地址不一致的情况了。内核进入了全新舒适的运行环境!

其实不管哪个平台处理器,内核启动之初,因为运行地址无法保证与链接地址一致,都要完成一次地址的配置和跳转来保证内核运行地址与链接地址的一致性。

ARM内核的做法首先建立开启MMU函数turn_mmu_on所在页的一个平映射,同时创建内核起始地址的线性映射,然后开启MMU,完成turn_mmu_on函数物理运行地址到虚拟运行地址的一个无缝衔接,之后在进行链接地址的跳转。具体的做法可以参考ARM启动分析的第二篇文章,链接如下:
http://blog.csdn.net/skyflying2012/article/details/41447843

这样对比来看,powerpc相比于ARM则更加开门见山,一开始就完成了MMU配置和跳转,进入了舒适的运行环境。这里可以看出,powerpc与ARM在启动之初地址跳转方式不同,究其原因就是powerpc的MMU是常开的!

init_cpu_state的另一个主要工作是异常初始化,异常是处理器异步处理事务的重要手段,处理器设计中都会有异常向量表来表征处理器所有可处理器的异常入口地址。产生该异常,处理器核逻辑就会跳转到对应入口地址取指运行。
对于powerpc处理器中设计2种寄存器,IVPR寄存器来指定异常向量表的入口地址,IVOR0-15寄存器则指定了powerpc处理器16种异常对应入口地址的偏移量。IVOR0-15和16种异常的对应关系如下图,此图截于460s处理器核手册。

我们最常用的是external interrupt,该异常是与外部中断控制器相连。
在init_cpu_state最后阶段调用SET_IVOR分别设置了IVOR0-15的值,该值是SET_IVOR的第二个参数的低16位,SET_IVOR实现如下。

#define SET_IVOR(vector_number, vector_label)       \\
        li  r26,vector_label@l;         \\
        mtspr   SPRN_IVOR##vector_number,r26;   \\
        sync

那init_cpu_state中SET_IVOR的第二个参数如CriticalInput是哪里定义的呢,接着往下看。
init_cpu_state最后调用head_start_common,如下:

head_start_common:
    /* Establish the interrupt vector base */
    lis r4,interrupt_base@h /* IVPR only uses the high 16-bits */
    mtspr   SPRN_IVPR,r4

    /*
     * If the kernel was loaded at a non-zero 256 MB page, we need to
     * mask off the most significant 4 bits to get the relative address
     * from the start of physical memory
     */
    rlwinm  r22,r22,0,4,31
    addis   r22,r22,PAGE_OFFSET@h
    mtlr    r22
    isync
    blr

将interrupt_base的高16位写入IVPR,因此interrupt_base代表了异常向量表的入口地址。interrupt_base定义如下:

interrupt_base:
    /* Critical Input Interrupt */
    CRITICAL_EXCEPTION(0x0100, CriticalInput, unknown_exception)

    /* Machine Check Interrupt */
    CRITICAL_EXCEPTION(0x0200, MachineCheck, machine_check_exception)
    MCHECK_EXCEPTION(0x0210, MachineCheckA, machine_check_exception)

    /* Data Storage Interrupt */
    DATA_STORAGE_EXCEPTION

        /* Instruction Storage Interrupt */
    INSTRUCTION_STORAGE_EXCEPTION

    /* External Input Interrupt */
    EXCEPTION(0x0500, ExternalInput, do_IRQ, EXC_XFER_LITE)
........

都是一些宏定义,来看其中一个EXCEPTION的定义:

/*
 * Exception vectors.
 */
#define EXCEPTION(n, label, hdlr, xfer)     \\
    . = n;                  \\
    DO_KVM n;               \\
label:                      \\
    EXCEPTION_PROLOG;           \\
    addi    r3,r1,STACK_FRAME_OVERHEAD; \\
    xfer(n, hdlr)

#define EXC_XFER_TEMPLATE(n, hdlr, trap, copyee, tfer, ret) \\
    li  r10,trap;                   \\
    stw r10,_TRAP(r11);                 \\
    li  r10,MSR_KERNEL;                 \\
    copyee(r10, r9);                    \\
    bl  tfer;                       \\
i##n:                               \\
    .long   hdlr;                       \\
    .long   ret

这些宏定义这里就不详细说了,定义了32字节对齐的异常处理label,很明显可以看出来EXCEPTION的第二个参数作为了宏定义实现中的label。所以interrupt_base中的宏定义就是定义了以SET_IVOR第二个参数为名的异常处理label(也可称为函数)。

interrupt_base以及EXCEPTION等宏定义定义的CriticalInput等一系列异常处理都是label,label的地址是在链接是确定的。现在内核已经运行在与链接地址一致的地址上了
因此在head_start_common中将interrupt_base的高16位写入IVPR,将CriticalInput等label的低16位写入IVOR0-15,就完成了异常向量表的初始化。

init_cpu_state对TLB重新配置,保证了链接地址与运行地址的一致,但当前的TLB配置与刚进入init_cpu_state是不一致的。因此r22保存的_start中bl init_cpu_state的下条指令地址已经变了,当前的_start已经运行在了新地址上。
所以head_start_common最后对r22中返回地址进行了处理,将其高位修改为链接地址再执行blr,保证可以正确跳回新的地址,内核跳转又重新回到了_start中!

今天的分析就到这里!

以上是关于powerpc-linux内核start_kernel之前启动分析-开门见山的主要内容,如果未能解决你的问题,请参考以下文章

Linux 内核编译 Linux 内核 ⑦ ( 安装内核模块 | 安装内核 | 重启系统 | 查看当前内核版本 )

Linux 内核编译 Linux 内核 ② ( 解压内核源码 | 查询当前 Linux 内核版本号 | 进入并查看 linux 内核源码目录 )

Linux 内核进程管理 ( 内核线程概念 | 内核线程普通进程用户线程 | 内核线程与普通进程区别 | 内核线程主要用途 | 内核线程创建函数 kernel_thread 源码 )

Linux内核模块简介

[转帖]微内核 宏内核

浏览器内核