• <div id="0yoao"><tr id="0yoao"></tr></div>
    <dl id="0yoao"></dl>
  • <sup id="0yoao"></sup>
    <div id="0yoao"><tr id="0yoao"></tr></div>
  • <div id="0yoao"><tr id="0yoao"></tr></div>
  • ?

    ARM64的启动过程之(四):打开MMU

    作者:linuxer 发布于:2015-10-24 12:35 分类:ARMv8A Arch

    一、前言

    经过漫长的前戏,我们终于迎来了打开MMU的时刻,本文主要描述打开MMU以及跳转到start_kernel之前的代码逻辑。这一节完成之后,我们就会离开痛苦的汇编,进入人民群众喜闻乐见的c代码了。

    二、打开MMU前后的概述

    对CPU以及其执行的程序而言,打开MMU是一件很有意思的事情,好象从现实世界一下子走进了奇妙的虚幻世界,本节,我们一起来看看内核是如何“穿越”的。下面这张图描述了两个不同的世界:

    mmu-on-off

    当没有打开MMU的时候,cpu在进行取指以及数据访问的时候是直接访问物理内存或者IO memory。虽然64bit的CPU理论上拥有非常大的address space,但是实际上用于存储kernel image的物理main memory并没有那么大,一般而言,系统的main memory在低端的一小段物理地址空间中,如上图右侧的图片所示。当打开MMU的时候,cpu对memory系统的访问不能直接触及物理空间,而是需要通过一系列的Translation table进行翻译。虚拟地址空间分成三段,低端是0x00000000_00000000~0x0000FFFF_FFFFFFFF,用于user space。高端是0xFFFF0000_00000000~0xFFFFFFFF_FFFFFFFF,用于kernel space。中间的一段地址是无效地址,对其访问会产生MMU fault。虚拟地址空间如上图右侧的图片所示。

    Linker感知的是虚拟地址,在将内核的各个object文件链接成一个kernel image的时候,kernel image binary code中访问的都是虚拟地址,也就是说kernel image应该运行在Linker指定的虚拟地址空间上。问题来了,kernel image运行在那个地址上呢?实际上,将kernel image放到kernel space的首地址运行是一个最直观的想法,不过由于各种原因,具体的arch在编译内核的时候,可以指定一个offset(TEXT_OFFSET),对于ARM64而言是512KB(0x00080000),因此,编译后的内核运行在0xFFFF8000_00080000的地址上。系统启动后,bootloader会将kernel image copy到main memory,当然,和虚拟地址空间类似,kernel image并没有copy到main memory的首地址,也保持了一个同样size的offset。现在,问题又来了:在kernel的开始运行阶段,MMU是OFF的,也就是说kernel image是直接运行在物理地址上的,但是实际上kernel是被linker链接到了虚拟地址上去的,在这种情况下,在没有turn on MMU之前,kernel能正常运行吗?可以的,如果kernel在turn on MMU之前的代码都是PIC的,那么代码实际上是可以在?#25105;?#22320;址上运行的。你可以仔细观察turn on MMU之前的代码,都是位置无关的代码。

    OK,解决了MMU turn on之前的问题,现在我们可以准备“穿越”了。真正打开MMU就是一条指令而已,就是将某个system register的某个bit设定为1之类的操作。这样我们可以把相关指令分成两组,turn on mmu之前的绿色指令和之后的橘色指令,如下图所示:

    mmu-1

    由于现代CPU的设计引入了pipe, super scalar,out-of-order execution,分支预测等等特性,实际上在turn on MMU的指令执行的那个时刻,该指令附近的指令的具体状态?#34892;?#28151;乱,可能绿色指令执行的数据加载在实际在总线?#25103;?#36215;bus transaction的时候已经启动了MMU,本来它是应该访问physical address space的。而也有可能橘色的指令提前执行,导致其发起的memory操作在MMU turn on之前就完成。为了解决这些混乱,可以采取一种投机取巧的办法,就是建立一致性?#25104;洌?#20551;设kernel image对应的物理地址段是A~B这一段,那么在建立页表的时候就把A~B这段虚拟地址段?#25104;?#21040;A~B这一段的物理地址。这样,在turn on MMU附近的指令是毫无压力的,无论你是通过虚拟地址还是物理地址,访问的都是同样的物理memory。

    还有一?#22336;?#27861;,就是清楚的隔离turn on MMU前后的指令,那就是使用指令同步工具,如下:

    mmu-2

    指令屏障可以清清楚楚的把指令的执行划分成三段,第一段是绿色指令,在执行turn on mmu指令执行之前全部完成,随后启动turn on MMU的指令,随后的指令屏障可以确保turn on MMU的指令完全执行完毕(整个计算机系统的视图切换到了虚拟世界),这时候才启动橘色指令的取指、译码、执行等操作。

    三、打开MMU的代码

    具体打开MMU的代码在__enable_mmu函数中如下:

    __enable_mmu:
        ldr    x5, =vectors
        msr    vbar_el1, x5 ---------------------------(1)
        msr    ttbr0_el1, x25            // load TTBR0 -----------------(2)
        msr    ttbr1_el1, x26            // load TTBR1
        isb
        msr    sctlr_el1, x0 ---------------------------(3)
        isb
        br    x27 -------------跳转到__mmap_switched执行,不设定lr寄存器
    ENDPROC(__enable_mmu)

    传入该函数的参数有四个,一个是x0寄存器,该寄存器中保存了打开MMU时候要设定的SCTLR_EL1的值(在__cpu_setup函数中设定),第二个是个是x25寄存器,保存了idmap_pg_dir的值。第三个参数是x26寄存器,保存了swapper_pg_dir的值。最后一个参数是x27,是执行完毕该函数之后,跳转到哪里去执行(__mmap_switched)。

    (1)VBAR_EL1, Vector Base Address Register (EL1),该寄存器保存了EL1状态的异常向量表。在ARMv8中,发生了一个exception,首先需要确定的是该异常将?#30171;?#21738;一个exception level。如果一个exception最终?#30171;顴L1,那么cpu会跳转到这里向量表来执行。具体异常的处理过程由其他文档描述,这里就不说了。

    (2)idmap_pg_dir是为turn on MMU准备的一致性?#25104;洌?#29289;理地址的高16bit都是0,因此identity mapping必定是选择TTBR0_EL1指向的各级地址翻译表。后续当系统运行之后,在进程切换的时候,会修改TTBR0的值,切换到真实的进程地址空间上去。TTBR1用于kernel space,所有的内核线程都是共享一个空间就是swapper_pg_dir。

    (3)打开MMU。实际上在这条指令的上下都有isb指令,理论上已经可以turn on MMU之前之后的代码执行顺序?#32454;?#30340;定义下来,其实我感觉不必要再启用idmap_pg_dir的那些页表了,当然,这只是猜测。

    四、通向start_kernel

    我痛恨汇编,如果能不使用汇编那绝对不要使用汇编,还好我们马上就要投奔start_kernel:

    __mmap_switched:
        adr_l    x6, __bss_start
        adr_l    x7, __bss_stop

    1:    cmp    x6, x7
        b.hs    2f
        str    xzr, [x6], #8 ---------------clear BSS
        b    1b
    2:
        adr_l    sp, initial_sp, x4 -----------建立和swapper进程的链接
        str_l    x21, __fdt_pointer, x5        // Save FDT pointer
        str_l    x24, memstart_addr, x6        // Save PHYS_OFFSET
        mov    x29, #0
        b    start_kernel
    ENDPROC(__mmap_switched)

    这段代码分成两个部分,一部分是清BSS,另外一部分是为进入c代码做准备(主要是stack)。clear BSS段就?#21069;?#26410;初始化的全?#30452;淞可?#23450;为0的初值,没有什么可说的。要进入start_kernel这样的c代码,没有stack可不行,那么如何设定stack呢?熟悉kernel的人都知道,?#27809;?#31354;间的进程当陷入内核态的时候,stack切换到内核栈,实际上就是该进程的thread info内存段(4K或者8K)的顶部。对于swapper进程,原理是类似的:

    .set    initial_sp, init_thread_union + THREAD_START_SP

    如果说之前的代码执行都处于一个孤魂野鬼的状态,“adr_l    sp, initial_sp, x4”指令执行之后,初始化代码终于找到了归宿,初始化代码有了自己的thread info,有了自己的task struct,有了自己的pid,有了进程(内核线程)应该拥有的一切,从此之后的代码归属idle进程,pid等于0的那个进程。

    为了方便后面的代码的访问,这里还初始化了两个变量,分别是__fdt_pointer(设备树信息,物理地址)和memstart_addr(kernel image所在的物理地址,一般而言是main memory的首地址)。 memstart_addr主要用于main memory中物理地址和虚拟地址的转换,具体可以参考__virt_to_phys和__phys_to_virt的实现。

    五、参考文献

    1、ARM Architecture Reference Manual


    change log:

    1、2015-11-30,强调了初始化代码和idle进程的连接

    2、2015-12-2,修改了物理空间和虚拟空间的视图

    3、2016-9-21,修改对一致性?#25104;?#30340;描述

    原创文章,转发请注明出处。蜗窝科技

    标签: 打开MMU

    评论:

    see
    2019-06-05 10:39
    请教下,文档里说编译后的内核运行在0xFFFF8000_00080000?  但是通过readelf查看内核,入口点地址是0xffff000008080000,怎么和文档说法不一致?
    smcdef
    2019-06-05 17:58
    @see:可能你的内核代码已经把kernel从linear mapping区域搬移到vmalloc区域了。所以造成了这种现象。详情可以看下另外一篇文档:ARM64 Kernel Image Mapping的变化:http://www.71402172.com/memory_management/436.html
    rikeyone
    2018-02-28 09:29
    你好,linuxer:
    我想请教一下,在start kernel之前创建的页表应该只是临时页表,那么在start kernel中的setup arch中会重新创建内核页表的?#25104;洌?#37027;么已经运行在虚拟空间上了,这个?#25104;?#26159;怎么切换的呢?
    rikeyone
    2018-02-28 11:41
    @rikeyone:我看的是arm32的内核操作,里面会调用pmd clear清除一级页表的内容
    leba
    2017-12-12 09:13
    实际上在这条指令的上下都有isb指令,理论上已经可以turn on MMU之前之后的代码执行顺序?#32454;?#30340;定义下来,其实我感觉不必要再启用idmap_pg_dir的那些页表了,当然,这只是猜测。
    ------ 应?#27809;?#19982; pipeline 有关系,所以,那个 idmap 还是需要的。
    linuxer
    2017-12-16 09:38
    @leba:你说得有道理,看来还需要再深入理解一下CPU的架构。
    2freeman
    2018-05-15 11:28
    @leba:idmpa_pg_dir在MMU开启后应?#27809;?#26159;需要的,无论是否跟pipeline有关,在MMU开启后,VA=PA=xxxx,所以,对于这样的VA,MMU的翻译还是需要使用idmap_pg_dir页表,只有在VA不再等于PA,也就是不再是一致性?#25104;?#26102;,idmap_pg_dir页表才真正失效。而VA不等于PA的点,应该在__primary_switch中:ldr x8, =__primary_witched;br  x8,x8里保存的是真正的VA(!=PA),在br指令执行后,完成了VA(=PA)--> VA(!=PA)的转换,这是才是idmap_pg_dir不再起作用的时间点,这也许是这个函数为什么叫__primary_witched的原因。
    breeze
    2017-07-26 14:19
    @linuxer:

    本来我以为memblock是在arm64_memblock_init()中初始化的,但我发现kernel在call arm64_memblock_init()之前就已经初始化过memblock一次了. 如:我在arm64_memblock_init()的入口处调用memblock_dump_all(), 输出如下:
    MEMBLOCK configuration:
    memory size = 0xf0000000 reserved size = 0x0
    memory.cnt  = 0x1
    memory[0x0]    [0x00000010000000-0x000000ffffffff], 0xf0000000 bytes flags: 0x0
    reserved.cnt  = 0x1
    reserved[0x0]    [0x00000000000000-0xffffffffffffffff], 0x0 bytes flags: 0x0

    至少memblock.memory已经初始化过了,region为: [0x00000010000000-0x000000ffffffff].
    请?#20107;?#20027;您知道是在哪里初始化的吗?我百思不得其姐.

    谢谢.
    wowo
    2017-07-28 08:56
    @breeze:你可以检查一下是不是这里初始化的(dts文件中指定的memblock):
    setup_arch
      setup_machine_fdt
        early_init_dt_scan_memory
          early_init_dt_add_memory_arch
            memblock_add
    macooma
    2016-10-10 14:21
    博主的文章对于我这样初学armv8的晚辈来讲真是提供了极大的参?#21450;。行?#21338;主的无私精神!
    有个问题想请教一下,如下:

    idmap_pg_dir是为turn on MMU准备的一致性?#25104;洌?#29289;理地址的高16bit都是0,因此identity mapping必定是选择TTBR0_EL1指向的各级地址翻译表。
    -------------------------------------------------------------------------------------
    请问这里的“必定”是因何而来呢? 谢谢!
    linuxer
    2016-10-10 21:49
    @macooma:identity mapping实际上?#21069;?物理地址?#25104;?#21040;物理地址上去,ARMv8支持的物理地址最大是48个bit,因此高16bit必定等于0
    macooma
    2016-11-08 17:12
    @linuxer:意思是CPU会主动判断要转换的地址前16位来决定使用TTBR0还是TTBR1吧?
    thatman
    2019-03-21 14:49
    @macooma:请问下。这个问题有答案了吗?
    archer
    2016-07-31 14:25
    linuxer,你好,请教一个问题:
    PAGE_OFFSET开始的虚拟地址 会 ?#25104;?#21040;PHYS_OFFSET的物理地址 (线性区),
    PHYS_OFFSET的定义:
    #define PHYS_OFFSET             ({ VM_BUG_ON(memstart_addr & 1); memstart_addr; })

    这个memstart_addr的全?#30452;?#37327;是在arm64_memblock_init这个函数中计算确定的。
    所以线性区的起始物理地址是在这里才确定的?

    我的问题:因为内核的虚拟地址在PAGE_OFFSET的0x80000偏移处,所以Image物理地址也应该在memstart_addr的0x80000偏移处? 但是memstart_addr是在这里才计算确定的,bootloader是怎么知道要把Image加载到这里的呢?
    linuxer
    2016-08-01 10:23
    @archer:这个memstart_addr的全?#30452;?#37327;是在arm64_memblock_init这个函数中计算确定的。
    所以线性区的起始物理地址是在这里才确定的?
    -------------------
    我看的是4.4.6内核,memstart_addr是在内核启动的初始阶段被设定的:
    ENTRY(stext)
        ......
        adrp    x24, __PHYS_OFFSET
            ......
    之后,在__mmap_switched函数中:    
        str_l    x24, memstart_addr, x6        // Save PHYS_OFFSET
        mov    x29, #0

    因为内核的虚拟地址在PAGE_OFFSET的0x80000偏移处,所以Image物理地址也应该在memstart_addr的0x80000偏移处?
    --------------
    是的

    但是memstart_addr是在这里才计算确定的,bootloader是怎么知道要把Image加载到这里的呢?
    ---------------
    这些内容本来就是bootloader和kernel之间接口的一部分,bootloader本来就知道应该copy kernel image到哪里的,如果不知道,系统就没有办法运作了。
    ericzhou
    2016-08-01 22:05
    @linuxer:linuxer,thanks for your reply.
    我看得是4.6的内核,memstart_addr的计算确定过程有点变化。
    是在arm64_memblock_init这个函数里确定的。
    不过你的意思我明白了。谢谢。
    tgn
    2016-05-21 01:09
    请教下博主,内核正式页表中的PXN位一般在哪里设置呀,我看arm64代码里面默认是没有的,能直接在create_mapping函数调用的时候直接把PAGE_KERNEL_EXEC给或上一个PXN吗?
    我是用的4.5的内核
    linuxer
    2016-05-23 17:23
    @tgn?#20309;我?#32473;PAGE_KERNEL_EXEC加上一个PXN的flag?PAGE_KERNEL_EXEC应该对应内核可执行代码的那些memory,因此创建页表的时候没有PXN是合适的,如果置位PXN,那么你的意思是准备让它在特权模式下永远不执行(Privileged execute-never)?
    amusion
    2015-10-28 11:25
    无意中看到了这个网站,拜读了几篇文章后,真是很?#24352;?#21834;,文章分析的很深入,不知能否写些和SMP先关的分析
    linuxer
    2015-10-29 08:59
    @amusion:这位客官,本站暂时不接受“点菜?#20445;?#21621;呵~~~开玩笑的,大家工作都很忙,业余时间写写文章,让自?#26680;?#19968;下,所以,想到哪里写到哪里,SMP的代码分布在各个内核的子系?#25345;校?#20854;实不是很好写的。
    kitty
    2015-10-27 17:31
    博主真的是辛苦了,像博主这样静下心搞钻研,如此认真隐忍的,太少了。
    linuxer
    2015-10-27 18:30
    @kitty:不辛苦,如果真心热爱的话就不辛苦,^_^

    喜欢钻研的人很多,只是没有聚合在一起,蜗窝这个网站就是因此而设立的,欢迎每一个沉醉于技术的人。
    kitty
    2015-10-30 10:38
    @linuxer:hi linuxer,看你写的power相关文章,写的非常详细。但power相关架构,很多要进行调整了,在今年年底,linuro要发布一个新的调度器架构EAS,会把DVFS和cpu idle添加CFS调度器中,而themal也会被IPA机制取代,这是新的研究方向,?#34892;?#36259;的话,可以一起学?#21834;?/div>
    mobz
    2015-10-27 14:16
    Hi linuxer,看到你最近有对系统启动做详细的分析,正好遇到个问题,想请教你下,就是在内核里的函数kernel_execve,有下面这样一段汇编代码,最终将调用到ret_to_user吗?是个什么流程?
        asm(    "add    r0, %0, %1\n\t"
            "mov    r1, %2\n\t"
            "mov    r2, %3\n\t"
            "bl memmove\n\t"    /* copy regs to top of stack */
            "mov    r8, #0\n\t" /* not a syscall */
            "mov    r9, %0\n\t" /* thread structure */
            "mov    sp, r0\n\t" /* reposition stack pointer */
            "b  ret_to_user"
            :
            : "r" (current_thread_info()),
              "Ir" (THREAD_START_SP - sizeof(regs)),
              "r" (&regs),
              "Ir" (sizeof(regs))
            : "r0", "r1", "r2", "r3", "r8", "r9", "ip", "lr", "memory");
    linuxer
    2015-10-27 17:47
    @mobz?#20309;?#35770;是userspace还是kernel space,都有执行程序的需求。例如在内核空间,当把控制权转交给userspace的时候,需要执行/sbin/init(也有可能是其他程序)。?#27809;?#31354;间的使用场?#26696;?#22810;,你在terminal上输入某一个程序的命令行的时候,shell程序会fork,然后调用execve来执行程序。

    所谓执行某一个二进制程序其实就是内核态的loader将当前进程的地址空间(text,data,bss和stack)销毁,使用新的可执行程序的image来创建新的进程的过程,因此返回调用函数是没有任?#25105;?#20041;的(实际上也不存在了)。但是,内核的loader总是要把控制权交给这个新创建的进程,因此,loader在这个新进程的内核栈上模拟了一?#34583;?#20837;内核的过程,在内核栈上构建了一个“现场?#20445;?#28982;后调用ret_to_user返回userspace,开始新的进程的执行,当然,CPU的PC值会设定为二进制程序image的入口函数。
    mobz
    2015-10-27 20:03
    @linuxer:ret_to_user会调用到arch_ret_to_user r1, lr 这里,可是?#19968;?#26159;没搞懂这个arch_ret_to_user又是如何实现的?代码中怎么也搜索不到??还是我没看懂。
    因为我经常遇到内核启动到Freeing init memory后就卡住的问题.定位发现应该就是卡在返回?#27809;?#31354;间来是执行init进程的时候出现的问题,就像类是bootargs设?#20040;?#35823;导致

    ENTRY(ret_to_user)
    ret_slow_syscall:
        disable_irq             @ disable interrupts
    ENTRY(ret_to_user_from_irq)
        ldr r1, [tsk, #TI_FLAGS]
        tst r1, #_TIF_WORK_MASK
        bne work_pending
    no_work_pending:
    #if defined(CONFIG_IRQSOFF_TRACER)
        asm_trace_hardirqs_on
    #endif
        /* perform architecture specific actions before user return */
        arch_ret_to_user r1, lr

        restore_user_regs fast = 0, offset = 0
    ENDPROC(ret_to_user_from_irq)
    ENDPROC(ret_to_user)
    linuxer
    2015-10-28 08:58
    @mobz:我正在读的是4.1.10版本中的ARM64的代码,其中其实都没有kernel_execve这个函数,也没有arch_ret_to_user。

    看起来arm?#25945;?#26377;arch_ret_to_user的定义,在linux/arch/arm/kernel/entry-common.S文件中:

    #ifdef CONFIG_NEED_RET_TO_USER
    #include <mach/entry-macro.S>
    #else
        .macro  arch_ret_to_user, tmp1, tmp2
        .endm
    #endif
    mobz
    2015-10-28 09:49
    @linuxer:恩,是的,但是这个是怎么实?#25191;觬eturn to userspace的?没看懂只有定义,实现呢?不理解这里
    linuxer
    2015-10-28 12:28
    @mobz:与其说arch_ret_to_user是architecture specific,不如说是ARM arch下,machine specific。对于?#34892;?#29305;殊的arm machine(例如ARCH_IOP13XX),在返回?#27809;?#31354;间的时候需要特别的操作,但?#23884;?#20110;大部分的ARM处理器,arch_ret_to_user是空的。
    mobz
    2015-10-28 13:39
    @linuxer:那这么说来,其实从内核到?#27809;?#31354;间就是:下面这三条语句(指令)了咯???
        disable_irq
        ldr r1, [tsk, #TI_FLAGS]
        tst r1, #_TIF_WORK_MASK
    linuxer
    2015-10-28 14:41
    @linuxer:后面不是有一个restore_user_regs嘛,用于从内核?#25442;指磚serspace的上下文
    mobz
    2015-10-28 18:46
    @linuxer:在你最新的答复上回复不了,就回复在这里吧,restore_user_regs 这个和arch_ret_to_user在ARM?#25945;?#19978;也是一样的,都是空,我并没在代码中找到相关的内容,倒是在FRV?#25945;?#37324;找到有
    linuxer
    2015-10-29 08:35
    @linuxer:在arch/arm/kernel/entry-head.S中有定义(我的内核版本是4.1.10,其他版本应该类似吧)。

    发表评论:

    Copyright @ 2013-2015 蜗窝科技 All rights reserved. Powered by emlog
    连码三全中是什么
  • <div id="0yoao"><tr id="0yoao"></tr></div>
    <dl id="0yoao"></dl>
  • <sup id="0yoao"></sup>
    <div id="0yoao"><tr id="0yoao"></tr></div>
  • <div id="0yoao"><tr id="0yoao"></tr></div>
  • <div id="0yoao"><tr id="0yoao"></tr></div>
    <dl id="0yoao"></dl>
  • <sup id="0yoao"></sup>
    <div id="0yoao"><tr id="0yoao"></tr></div>
  • <div id="0yoao"><tr id="0yoao"></tr></div>