zoukankan      html  css  js  c++  java
  • linux从head.s到start_kernelstart_kernel之---内核重定位后分析

    参考: https://biscuitos.github.io/blog/ARM-BOOT/

    zImage 重定位之后实践

    zImage 重定位之后,ARM 将 pc 指针指向了重定位 zImage restart 处继续执行,执行 代码如下:

    restart:        adr     r0, LC0
                    ldmia   r0, {r1, r2, r3, r6, r10, r11, r12}
                    ldr     sp, [r0, #28]
    
                    /*
                     * We might be running at a different address. We need
                     * to fix up various pointers.
                     */
                    sub     r0, r0, r1              @ caclculate the delta offset
                    add     r6, r6, r0              @ _edata
                    add     r10, r10, r0            @ inflated kernel size location
    
                    /*
                     * The kernel build system appends the size of the
                     * decompressed kernel at the end of the compressed data
                     * in little-endian form.
                     */
                    ldrb    r9, [r10, #0]
                    ldrb    lr, [r10, #1]
                    orr     r9, r9, lr, lsl #8
                    ldrb    lr, [r10, #2]
                    ldrb    r10, [r10, #3]
                    orr     r9, r9, lr, lsl #16
                    orr     r9, r9, r10, lsl #24
    
    #ifndef CONFIG_ZBOOT_ROM
                    /* malloc space is above the relocated stack (64k max) */
                    add     sp, sp, r0
                    add     r10, sp, #0x10000
    #endif
                    mov     r5, #0                  @ init dtb size to 0

    代码基本逻辑与 zImage 一直,细节请参看上面内容,重定位之后,会重新执行一遍 restart 之后的代码。这段代码的主要任务就是通过 zImage 的 LC0 表计算出目前各个必要信息的地址, 并调整这些地址到一个正确的重定位地址。通过上面的代码,可以获得重定位之后 zImage 的结束地址,以及 Image 的长度。这里不做过多讲解,接下来的代码是:

    /*
     * Check to see if we will overwrite ourselves.
     *   r4  = final kernel address (possibly with LSB set)
     *   r9  = size of decompressed image
     *   r10 = end of this image, including bss/stack/malloc space if non XIP
     * We basically want:
     *   r4 - 16k page directory >= r10 -> OK
     *   r4 + image lenght <= address of wont_overwrite -> OK
     * Note: the possible LSB in r4 is harmless here.
     */
                    add     r10, r10, #16384
                    cmp     r4, r10
                    bhs     wont_overwrite
                    add     r10, r4, r9
                    adr     r9, wont_overwrite
                    cmp     r10, r9
                    bls     wont_overwrite

    从上面的运行可知,此处 r10 寄存器存储了堆栈再加 64K 的地址,也就是堆栈加 malloc 之后的地址。然后跟 r4 寄存器对比,r4 寄存器是真正内核运行的起始地址。因为重定位 的原因,r4 的值决定比 r10 小,因此第一次 r4 与 r10 比较的结果不会导致 “bhs wont_overwrite” 执行。接着将 r4 寄存器和 r9 寄存器之和存储到 r10 寄存器, 这里 r10 寄存器代表真正内核运行的结束地址,又将 wont_overwrite 重定位之后的地址 赋值给 r9,通过对比 r10 和 r9 之间的大小,此时由于 zImage 已经重定位,r10 的值一定小于 r9, 那么执行 “bls wont_overwrite” 跳转到 wont_overwrite 处 继续执行。

    接下来的代码如下:

    wont_overwrite:
    /*
     * If delta is zero, we are running at the address we were linked at.
     *   r0  = delta
     *   r2  = BSS start
     *   r3  = BSS end
     *   r4  = kernel execution address (possibly with LSB set)
     *   r5  = appended dtb size (0 if not present)
     *   r7  = architecture ID
     *   r8  = atags pointer
     *   r11 = GOT start
     *   r12 = GOT end
     *   sp  = stack pointer
     */
                    orrs    r1, r0, r5
                    beq     not_relocated
    
                    add     r11, r11, r0
                    add     r12, r12, r0

    这段代码的任务就是确定自己是否重定位了,已经校正重定位之后的地址。在运行这段代码之前, 再次确定了此时每个寄存器的含义,r0 指向 LC0 表,也是 LC0 表内各项的偏移基地址; r2 寄存器指向 zImage 的 BSS 段的起始地址;r3 寄存器指向了 zImage 的 BSS 段的终止地址, r4 指向了内核运行的起始地址;r5 指向了 DTB 的大小;r7 存储体系相关的 ID 信息;r8 指向了 uboot 传递给内核的 atags 参数;r11 指向了 zImage 的 GOT 表的起始地址;r12 指向了 GOT 表的终止地址。sp 指向了重定位之后堆栈地址。

    代码首先调用 orrs 指令将 r0 的值与 r5 相或,结果存储到 r1 寄存器中,如果结果为零, 那么跳转到 not_relocated 处继续执行;如果结果不为零,那么继续执行下面代码。接着调整 r11 和 r12 寄存器重定位之后的值。

    根据调试结果,代码继续执行下面代码:

    #ifndef CONFIG_ZBOOT_ROM
                    /*
                     * If we're running fully PIC == CONFIG_ZBOOT_ROM = n,
                     * we need to fix up pointers into the BSS region.
                     * Note that the stack pointer has already been fixed up.
                     */
                    add     r2, r2, r0
                    add     r3, r3, r0
    
                    /*
                     * Relocate all entries in the GOT table.
                     * Bump bss entries to _edata + dtb size
                     */
    1:              ldr     r1, [r11, #0]           @ relocate entries in the GOT
                    add     r1, r1, r0              @ This fixes up C references
                    cmp     r1, r2                  @ if entry >= bss_start &&
                    cmphs   r3, r1                  @       bss_end > entry
                    addhi   r1, r1, r5              @    entry += dtb size
                    str     r1, [r11], #4           @ next entry
                    cmp     r11, r12
                    blo     1b
    
                    /* bump our bss pointers too */
                    add     r2, r2, r5
                    add     r3, r3, r5
    #endif

    这段代码的主要任务是校正 GOT 表的入口项,由于代码完全支持 PIC 的,所有重定位之后, 需要校正所有的 GOT 表入口项,但 BSS 段除外。代码首先通过命令 “ldr r1, [r11, #0]” 获得 GOT 表入口项的地址,然后将该地址校正为重定位之后的地址。连续使用两个 cmp 指令 确定该入口地址是否位于 BSS 段内,如果不在 BSS 段内,那么将 r1 入口项的地址加上 DTB 的值;如果不在,那么不做特殊处理;接着调用 str 将校正后的入口地址重写到 GOT 表内, 然后将 r11 指向下一个 GOT 表的入口。如果当前 GOT 表入口小于 GOT 表的结束地址,那么 跳转到 1b 处继续校正下一个 GOT 表入口。最后,也校正了 BSS 段的起始地址和终止地址, 更多 GOT 表原理实践可以查看:

    GOT 表原理实践

    接下来运行的代码如下:

    not_relocated:  mov     r0, #0
    1:              str     r0, [r2], #4
                    str     r0, [r2], #4
                    str     r0, [r2], #4
                    str     r0, [r2], #4
                    cmp     r2, r3
                    blo     1b

    这段代码的主要任务就是清除 BSS 段的内容。调用 str 指令,将 r2 对应的地址写入 0, 然后 r2 地址增加 4,重复操作 4 次之后对比当前 r2 地址是否小于 r3,如果小于,那么 继续重复 1。

    接着执行代码如下:

                    /*
                     * Did we skip the cache setup earlier?
                     * That is indicated by the LSB in r4
                     * Do it now if so.
                     */
                    tst     r4, #1
                    bic     r4, r4, #1
                    blne    cache_on

    这段代码的主要任务就是判断 cache 是否已经启动,如果没有就开启 cache。从之前的代码 可以知道,如果 cache 没有被开启,r4 寄存器的最低位会被置位。代码通过 tst 指令 查看 r4 寄存器的最低位情况,并使用 bic 指令清除最低位,因为这里一定要确保 cache 打开, 如果 cache 没有启用,那么会调用命令 “blne cache_on” 启用 cache。

    此时 cache 已经启用,所以不会跳转到 cache_on 处执行。 接下来执行的代码是:

    /*
     * The C runtime environment should now be setup sufficiently.
     * Set up some pointers, and start decompressing.
     *   r4  = kernel execution address
     *   r7  = architecture ID
     *   r8  = atags pointer
     */
                    mov     r0, r4
                    mov     r1, sp                  @ malloc space above stack
                    add     r2, sp, #0x10000        @ 64k max
                    mov     r3, r7
                    bl      decompress_kernel

    这段代码是为运行 C 函数 decompress_kernel 做准备。首先在执行代码之前,r4 寄存器指向了 内核执行的起始地址,r7 存储体系相关的 ID;r8 指向了 atags 参数。汇编调用 C 函数其中一种规则 就是 C 函数从左到右的第一个参数通过 r0 寄存器传递,C 函数的第二个参数通过 r1 寄存器传递, 依次类推。decompress_kernel 需要四个参数。代码首先将 r0 设置为内核执行的起始地址, 然后将堆栈的地址赋值给 r1 寄存器,再将 64 K 的空间赋值给 r2 寄存器,最后将 r7 的值 赋值给 r3 寄存器,通过上面的设置之后,最后调用 bl 指令跳转到 decompress_kernel 处 继续执行。记下来执行的代码位于 arch/arm/boot/compressed/misc.c 中,如下:

    void decompress_kernel(unsigned long output_start,
                            unsigned long free_mem_ptr_p,
                    unsigned long free_mem_ptr_end_p, int arch_id)
    {
            int ret;
    
            output_data             = (unsigned char *)output_start;
            free_mem_ptr            = free_mem_ptr_p;
            free_mem_end_ptr        = free_mem_ptr_end_p;
            __machine_arch_type     = arch_id;
    
            arch_decomp_setup();
    
            putstr("Uncompressing Linux...");
    
            ret = do_decompress(input_data, input_data_end - input_data,
                                output_data, error);
            if (ret)
                    error("decompressor returned an error");
            else
                    putstr(" done, booting the kernel.
    ");
    }

    decompress_kernel 函数的功能很简单,就是把 Image 从 zImage 中解压出来。通过 传入的参数,可以知道内核被解压到 output_start 位置,并将这个参数传递给全局变量 output_data。定义了两个全局变量 free_mem_ptr 和 free_mem_end_ptr 供解压程序使 用的内存空间。arch_id 将值传递给全局变量 __machine_arch_type。接下来就是调用 arch_decomp_setup() 函数做平台相关的解压设置。最后调 do_decompress() 函数进行 解压工作。开发者可以将 decompress_kernel 作为断点进行 GDB 调试,调试之前,使用 bless 二进制查看工具查看内核的数据内容,使用命令:

    bless arch/arm/boot/Image

    查看 Image 起始处的数据如下图:

    查看 Image 结尾处的数据如下图:

    接着调用 GDB 查看实际运行效果,根据上图,特别查看 do_decompress() 函数运行前后, 内存 0x60008000 和 0x60b6911a 处内存内容的变化情况,实际调试如下图:

    (gdb) b decompress_kernel
    Breakpoint 1 at 0x60b698f4: file arch/arm/boot/compressed/misc.c, line 82.
    (gdb) c
    Continuing.
    
    Breakpoint 1, decompress_kernel (output_start=1610645504,
        free_mem_ptr_p=1627022200, free_mem_ptr_end_p=1627087736, arch_id=2272)
        at arch/arm/boot/compressed/misc.c:82
    warning: Source file is more recent than executable.
    82    {
    (gdb) n
    85        output_data        = (unsigned char *)output_start;
    (gdb)
    86        free_mem_ptr        = free_mem_ptr_p;
    (gdb)
    87        free_mem_end_ptr    = free_mem_ptr_end_p;
    (gdb)
    88        __machine_arch_type    = arch_id;
    (gdb) x/16x 0x60008000
    0x60008000:    0x00000000    0x00000000    0x00000000    0x00000000
    0x60008010:    0x00000000    0x00000000    0x00000000    0x00000000
    0x60008020:    0x00000000    0x00000000    0x00000000    0x00000000
    0x60008030:    0x00000000    0x00000000    0x00000000    0x00000000
    (gdb) x/16x 0x60b6911a
    0x60b6911a:    0x00000000    0x00000000    0x00000000    0x00000000
    0x60b6912a:    0x00000000    0x00000000    0x00000000    0x00000000
    0x60b6913a:    0x00000000    0x00000000    0x00000000    0x00000000
    0x60b6914a:    0x00000000    0x00000000    0x00000000    0x00000000
    (gdb) n
    92        putstr("Uncompressing Linux...");
    (gdb) n
    94        ret = do_decompress(input_data, input_data_end - input_data,
    (gdb) n
    96        if (ret)
    (gdb) x/16x 0x60008000
    0x60008000:    0xeb043156    0xe10f9000    0xe229901a    0xe319001f
    0x60008010:    0xe3c9901f    0xe38990d3    0x1a000004    0xe3899c01
    0x60008020:    0xe28fe00c    0xe16ff009    0xe12ef30e    0xe160006e
    0x60008030:    0xe121f009    0xee109f10    0xeb03e973    0xe1b0a005
    (gdb) x/16x 0x60b6911a
    0x60b6911a:    0xa3680000    0xb9b08071    0x012f8094    0x00000000
    0x60b6912a:    0x00000000    0x00000000    0x00000000    0x00000000
    0x60b6913a:    0x00000000    0x00000000    0x00000000    0x00000000
    0x60b6914a:    0x00000000    0x00000000    0x00000000    0x00000000
    (gdb)

    从上面实践结果可以看出,当执行 do_decompress() 函数之前,0x60008000 和 0x60b6911a 的内存都是 0。但当执行完 do_decompress() 函数之后,0x60008000 和 0x60b6911a 的内容与上图中 Image 的开始和结束处的内容一致,因此内核解压正确。接着继续 分析一下 do_decompress() 函数,函数定义在 arch/arm/boot/decompress.c

    // SPDX-License-Identifier: GPL-2.0
    #define _LINUX_STRING_H_
    
    #include <linux/compiler.h>     /* for inline */
    #include <linux/types.h>        /* for size_t */
    #include <linux/stddef.h>       /* for NULL */
    #include <linux/linkage.h>
    #include <asm/string.h>
    #include "misc.h"
    
    #define STATIC static
    #define STATIC_RW_DATA  /* non-static please */
    
    #define Assert(cond,msg)
    #define Trace(x)
    #define Tracev(x)
    #define Tracevv(x)
    #define Tracec(x)
    #define Tracecv(c,v)
    
    /* Not needed, but used in some headers pulled in by decompressors */
    extern char *strstr(const char *s1, const char *s2);
    extern size_t strlen(const char *s);
    extern int memcmp(const void *cs, const void *ct, size_t count);
    
    #ifdef CONFIG_KERNEL_GZIP
    #include "../../../../lib/decompress_inflate.c"
    #endif
    
    int do_decompress(u8 *input, int len, u8 *output, void (*error)(char *x))
    {
            return __decompress(input, len, NULL, NULL, output, 0, NULL, error);
    }

    整个文件很简单,就是将 gzip 对应的库函数直接在预处理阶段包含到这个文件里,然后通过 调用库函数 __decompress() 进行解压,如果开发者感兴趣 gzip 解压的过程,可以查看 文件 lib/decompress_inflate.c,这里不做解释。

    至此,内核已经正确的解压到指定位置,zImage 再做一些收尾工作之后就将 CPU 的执行权移交 给真正内核。接下来要执行的代码是:

                    bl      cache_clean_flush
                    bl      cache_off

    在将 CPU 执行权交给真正内核之前,需要将 MMU 关闭,并刷新 cache。cache_clean_flush 之前就讨论过,源码分析请看 cache_clean_flush。至于 cache_off, 其实现过程和 cache_on 类似,也是从 armv7 的 CACHE 表中读取对应的 off 操作,最终会调用: __armv7_mmu_cache_off,

    __armv7_mmu_cache_off:
                    mrc     p15, 0, r0, c1, c0
    #ifdef CONFIG_MMU
                    bic     r0, r0, #0x000d
    #else
                    bic     r0, r0, #0x000c
    #endif
                    mcr     p15, 0, r0, c1, c0      @ turn MMU and cache off
                    mov     r12, lr
                    bl      __armv7_mmu_cache_flush
                    mov     r0, #0

    代码原理很简单,就是将 SCTR 控制器中,关于 CACHE 和 MMU 的位设置成指定状态, SCTR 寄存器的位图如下:

    MMU

    首先调用 mcr 指令获得 SCTR 寄存器的值,存储到 r0 寄存器中,然后执行 bic 指令,将 bit0, bit2 和 bit3 清零,并写入到 SCTR 寄存器中,这样就关闭了 MMU 和 cache, 接着将返回地址存储到 r12 寄存器中。调用 __armv7_mmu_cache_flush,刷新 LoC 数据缓存 的数据,接下来执行的代码是:

    #ifdef CONFIG_MMU
                    mcr     p15, 0, r0, c8, c7, 0   @ invalidate whole TLB
    #endif
                    mcr     p15, 0, r0, c7, c5, 6   @ invalidate BTC
                    mcr     p15, 0, r0, c7, c10, 4  @ DSB
                    mcr     p15, 0, r0, c7, c5, 4   @ ISB
                    mov     pc, r12

    函数继续调用 mcr 执行向 CP15 C8 寄存器写值,此时布局如下:

    MMU

    选中了 TLBIALL 寄存器,当往寄存器执行写操作时会将所有的 TLB 无效。接着调用 mcr 执行 操作 CP15 c7 寄存器,此时寄存器布局如下:

    MMU

    选中了 BPILL 寄存器,当向寄存器写入值之后会使分支预取无效。最后执行两次内存屏障 DSB 和 ISB 将所有指令和设置同步到最新。最后将 r12 寄存器的传给 pc 实现返回到调用点。

    至此,zImage 的收尾工作已经完成,接下来 zImage 将执行权转交给真正的内核。zImage 最后 的任务就是将执行权移交给真正的 kernel,执行代码如下:

    #ifdef CONFIG_ARM_VIRT_EXT
                    mrs     r0, spsr                @ Get saved CPU boot mode
                    and     r0, r0, #MODE_MASK
                    cmp     r0, #HYP_MODE           @ if not booted in HYP mode...
                    bne     __entry_kernel
    #endif
    
    
    __entry_kernel:
                    mov     r0, #0                  @ must be 0
                    mov     r1, r7                  @ restore architecture number
                    mov     r2, r8                  @ restore atags pointer
     ARM(           mov     pc, r4          )       @ call kernel

    zImage 首先判断当前模式是否是 HYP 模式,如果不是就直接跳转到 __entry_kernel 处执行。__entry_kernel 没有做其他特别的处理,就是设置 r0 寄存器为 0, r1 寄存器 为与体系相关的 ID,r2 设置为 atags 的参数,就直接将内核执行的起始地址 r4 寄存器内 的值直接传递给 PC,那么 PC 直接跳转到 0x60008000 处开始执行,至此 zImage 将 CPU 执行权全部移交给内核。

  • 相关阅读:
    学习使用linux下tags文件
    uboot常用命令详解
    U-boot中TFTP 解释
    eth0: ERROR while getting interface flags: No such device 没有eth0 有其他的eth
    取消挂载 umount 时出现的 “Device is busy”
    Linux的log日志功能
    oracle的安装
    SOA架构设计分析
    立方体模型
    质量属性的六个常见属性场景(淘宝网)
  • 原文地址:https://www.cnblogs.com/edver/p/12011596.html
Copyright © 2011-2022 走看看