captivated 发表于 2020-03-31 18:11

amd64 arch启动代码早期paging相关的疑问

本帖最后由 captivated 于 2020-03-31 18:44 编辑

x86 arch 的启动代码比较复杂,为了说清楚我的具体问题,所以先做一个简述(免得说我自己没把基本问题搞清楚,省点抬杠时间)。
此外以下简述主要针对 x86_64 arch 而不是 i386.


1. 启动映像生成:

x86 arch 的启动映像为 $OUT/arch/x86/boot/bzImage. $OUT 表示 kernel 编译输出目录(如果编译时不指定 -o 输出目录,则和编译源码是同一个目录)。
bzImage 构成如下。为了方便,$OUT/arch/x86/boot 目录简称为 $(boot) 路径.


1) 最终的 bzImage 是由 $(boot)/setup.bin 和 $(boot)/vmlinux.bin 组装成的.
    $(boot)/vmlinux.bin 又由 $(boot)/compressed/vmlinux 通过 objcopy 剥离掉 .comment 和 .note 段而成.
    而 $(boot)/compressed/vmlinux 则由 $(boot)/compressed 下面的各个目标文件组成, 里面最重要的是
    $(boot)/compressed/piggy.o 这个文件.

bzImage <-+ $(boot)/setup.bin
#######-+ $(boot)/vmlinux.bin <-+ $(boot)/compressed/vmlinux <-+ $(boot)/compressed/head_64.o
############################################### $(boot)/compressed/misc.o
############################################### ...
############################################### $(boot)/compressed/piggy.o

2) 为什么说 piggy.o 重要, 因为它是由压缩了的 vmlinux 构成的. 这里默认选 gzip 压缩.
而 vmlinux.bin.gz 又是由 compressed 同目录下的 vmlinux.bin 和 vmlinux.relocs 构成的.
vmlinux.bin 和 vmlinux.relocs, 则来自于 $OUT 目录下的 VMLINUX(实际文件名就是 $OUT/vmlinux. 大写的意思是表示它其实就是编译最终的链接文件, 从它到最终的 bzImage 不过是一堆二进制工具为了启动加载协议做的一堆变换而已).

$(boot)/compressed/piggy.o <+ $(boot)/compressed/vmlinux.bin.gz <-+ $(boot)/compressed/vmlinux.bin
###############################################$(boot)/compressed/vmlinux.relocs

2. x86 arch 启动问题


x86 相关的 boot protocal 有几种. 不管哪一种,总之 bzImage 得要 bootloader 加载到内存里面然后去运行才行.
前面的映像构建过程为什么是那样子, 估计也和 x86 arch “历史悠久”, boot protocal 复杂相关.
但大概来说, 有这么几种 boot protocal:

1) 实模式 boot protocal.
这种情况下, bootloader 是运行在实模式的. 就算 bootloader 自己切换了保护模式, 它加载完 kernel(bzImage)并跳转过去执行前, 也得切回实模式, 因为 protocal 就是协议嘛, 大家要有个商量. 这时 bootloader 切换过去后, 最先从 setup.bin 开始运行(bzImage 是由 setup.bin + vmlinux.bin 包的), 也就是走
arch/x86/boot/header.S -> _start -> calll main
arch/x86/boot/main.c -> main -> go_to_protected_mode
arch/x86/boot/pm.c -> go_to_protected_mode -> protected_mode_jump
arch/x86/boot/pmjump.S -> GLOBAL(protected_mode_jump) -> jmpl *%eax

经过这些步骤, 就会开始执行到 vmlinux.bin 里面的东西了.


2) 32 位 boot protocal
现在 bootloader 已经很强大了, 所以通常 bootloader 自己已经切到 32 位 protected mode, 然后就不走 setup.bin 那一套了.
前面说了 bzImage 是由 setup.bin + vmlinux.bin(都在 $(boot) 目录下)组装而成, 跳过 setup.bin 的话, 当然就直接从 vmlinux.bin 开始执行了.
bootloader 为什么可以这样做, 因为 bzImage 就是 bootloader 加载的, 它当然知道 setup.bin vmlinux.bin 分别在什么内存位置.
一般来说, setup.bin 必须要运行在实模式, 因此其加载地址是物理内存 1MB 以下的, 而 vmlinux.bin 会刚刚好从 1MB 位置开始放置.
anyway, 这时候走的是这个:
arch/x86/boot/compressed/head_64.S -> ENTRY(startup_32) -> ENTRY(startup_64) -> jmp *%eax

这里还要说明一下. 前面映像构建过程说得清楚, 真正的 kernel 是 piggy.o(由 $(OUT)/vmlinux 压缩, 然后生成的目标文件).
startup_32 -> startup_64 主要是从 32 位保护模式切换到 64 bit long mode. 切换之前会建立一个 4GB 简单 identity map(因为切换到 64 bit long mode 的条件之一就是, paging 必须是开的, 即 CR0.PG == 1).
而 startup_64 呢, 则会调用一个解压缩函数将 vmlinux.bin.gz 解压, 然后跳转到解压后的 kernel 入口去执行. 这里面可能还会有重定位处理等等.
如果关掉 KASLR, 那么解压后的 kernel, 也就是真正的 $(OUT)/vmlinux, 默认会放在 16MB 的位置.



3) 64 位 boot protocal

顾名思义, 就是 bootloader 有点过分强大(或者讨厌)了, 跳转到 kernel 之前, 它会自己先切到 64 bit long mode.
也就是直接执行 arch/x86/boot/compressed/head_64.S 中的 startup_64 了.
这时最初的 paging 是 bootloader 建立的.


3. vmlinux entry

好了,前面这些复杂的过程略过. 不管是先从 arch/x86/boot/setup.S 开始, 还是先从 arch/x86/boot/compressed/head_64.S 开始,
最终解压缩后的 kernel 入口点是:
arch/x86/kernel/head_64.S 里面的 startup_64 函数(or 一个全局符号).
有人可能会问这不和 compressed/head_64.S 里面的 startup_64 重名吗, 答案是它们根本没链接在一起, 各管各的, 不会有重定义错误放心.

啊, 约了不知道多少次电影请喝了多少次奶茶爬了多少次山, 终于可以约爬床了, 终于宽衣解带了, 兴奋之情溢于言表啊...

结果才看了特么几行, 我就被郁闷到了...

...

具体问题叙述还有点小麻烦, 呆会贴上来.

...


captivated 发表于 2020-03-31 18:31

回来了

在线代码见

https://elixir.bootlin.com/linux/latest/source/arch/x86/kernel/head_64.S

.Ljump_to_C_code:
        /*
       * Jump to run C code and to be on a real kernel address.
       * Since we are running on identity-mapped space we have to jump
       * to the full 64bit address, this is only possible as indirect
       * jump.In addition we need to ensure %cs is set so we make this
       * a far return.
       *
       * Note: do not change to far jump indirect with 64bit offset.
       *
       * AMD does not support far jump indirect with 64bit offset.
       * AMD64 Architecture Programmer's Manual, Volume 3: states only
       *        JMP FAR mem16:16 FF /5 Far jump indirect,
       *                with the target specified by a far pointer in memory.
       *        JMP FAR mem16:32 FF /5 Far jump indirect,
       *                with the target specified by a far pointer in memory.
       *
       * Intel64 does support 64bit offset.
       * Software Developer Manual Vol 2: states:
       *        FF /5 JMP m16:16 Jump far, absolute indirect,
       *                address given in m16:16
       *        FF /5 JMP m16:32 Jump far, absolute indirect,
       *                address given in m16:32.
       *        REX.W + FF /5 JMP m16:64 Jump far, absolute indirect,
       *                address given in m16:64.
       */
        pushq        $.Lafter_lret        # put return address on stack for unwinder
        xorl        %ebp, %ebp        # clear frame pointer
        movq        initial_code(%rip), %rax
        pushq        $__KERNEL_CS        # set correct cs
        pushq        %rax                # target address in negative space
        lretq这里跳转到 x86_64_start_kernel@head64.c, 在线代码 https://elixir.bootlin.com/linux/latest/source/arch/x86/kernel/head64.c

代码
asmlinkage __visible void __init x86_64_start_kernel(char * real_mode_data)
{
        ...

        cr4_init_shadow();

        /* Kill off the identity-map trampoline */
        reset_early_page_tables();

        clear_bss();

        clear_page(init_top_pgt);

        /*
       * SME support may update early_pmd_flags to include the memory
       * encryption mask, so it needs to be called before anything
       * that may generate a page fault.
       */
        sme_early_init();

        kasan_early_init();

        idt_setup_early_handler();
这里面, reset_early_page_tables 反手就把 pgt 给清了!
/* Wipe all early page tables except for the kernel symbol map */
static void __init reset_early_page_tables(void)
{
        memset(early_top_pgt, 0, sizeof(pgd_t)*(PTRS_PER_PGD-1));
        next_early_pgt = 0;
        write_cr3(__sme_pa_nodebug(early_top_pgt));
}

但这时候,中断处理还没设立,%cr3 里面就是 early_top_pgt,这么把 PGT 一清,不会 paging 异常吗???!!!



captivated 发表于 2020-03-31 21:22

哦,我知道了。

没注意
memset(early_top_pgt, 0, sizeof(pgt_t) * (PTRS_PER_PGD - 1));
而不是
memset(early_top_pgt, 0, sizeof(pgt_t) * PTRS_PER_PGD);

并没有全清, 也就是说 early_top_gpt 是没有清掉的, 那么 l2_kernel_pgt 映射的 512MB 还在, 但是 head64.c 里面 __startup_64 建立的 identity mapping 被清掉了.

艹, 把我困惑了好两天, 原来是自己看代码太不仔细了:-L:-L:-L
页: [1]
查看完整版本: amd64 arch启动代码早期paging相关的疑问