参考: 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, #] /*
* 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, #]
ldrb lr, [r10, #]
orr r9, r9, lr, lsl #
ldrb lr, [r10, #]
ldrb r10, [r10, #]
orr r9, r9, lr, lsl #
orr r9, r9, r10, lsl # #ifndef CONFIG_ZBOOT_ROM
/* malloc space is above the relocated stack (64k max) */
add sp, sp, r0
add r10, sp, #0x10000
#endif
mov r5, # @ init dtb size to

代码基本逻辑与 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, #
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
*/
: ldr r1, [r11, #] @ 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], # @ 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, #
: str r0, [r2], #
str r0, [r2], #
str r0, [r2], #
str r0, [r2], #
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, #
bic r4, r4, #
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.\n");
}

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 at 0x60b698f4: file arch/arm/boot/compressed/misc.c, line .
(gdb) c
Continuing. Breakpoint , decompress_kernel (output_start=,
free_mem_ptr_p=, free_mem_ptr_end_p=, arch_id=)
at arch/arm/boot/compressed/misc.c:
warning: Source file is more recent than executable.
{
(gdb) n
output_data = (unsigned char *)output_start;
(gdb)
free_mem_ptr = free_mem_ptr_p;
(gdb)
free_mem_end_ptr = free_mem_ptr_end_p;
(gdb)
__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
putstr("Uncompressing Linux...");
(gdb) n
ret = do_decompress(input_data, input_data_end - input_data,
(gdb) n
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, , 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, , r0, c1, c0
#ifdef CONFIG_MMU
bic r0, r0, #0x000d
#else
bic r0, r0, #0x000c
#endif
mcr p15, , r0, c1, c0 @ turn MMU and cache off
mov r12, lr
bl __armv7_mmu_cache_flush
mov r0, #

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

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

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

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

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

选中了 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, # @ must be
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从head.s到start_kernelstart_kernel之---内核重定位后分析的更多相关文章

  1. linux从head.s到start_kernelstart_kernel之---内核解压到重定位分析

    一: arm linux 内核生成过程 1. 依据arch/arm/kernel/vmlinux.lds 生成linux内核源码根目录下的vmlinux,这个vmlinux属于未压缩,带调试信息.符号 ...

  2. kexec 内核快速启动流程分析

    一.命令行 1. kexec -l $kpwd --append="$arg" 其中$kpwd =目标内核的路径 $arg =传给内核的参数,与/proc/cmdline一致时表示 ...

  3. Linux内核--网络栈实现分析(十一)--驱动程序层(下)

    本文分析基于Linux Kernel 1.2.13 原创作品,转载请标明http://blog.csdn.net/yming0221/article/details/7555870 更多请查看专栏,地 ...

  4. Linux内核--网络栈实现分析(七)--数据包的传递过程(下)

    本文分析基于Linux Kernel 1.2.13 原创作品,转载请标明http://blog.csdn.net/yming0221/article/details/7545855 更多请查看专栏,地 ...

  5. Linux内核--网络栈实现分析(一)--网络栈初始化

    本文分析基于内核Linux Kernel 1.2.13 原创作品,转载请标明http://blog.csdn.net/yming0221/article/details/7488828 更多请看专栏, ...

  6. linux内核SPI总线驱动分析(一)(转)

    linux内核SPI总线驱动分析(一)(转) 下面有两个大的模块: 一个是SPI总线驱动的分析            (研究了具体实现的过程) 另一个是SPI总线驱动的编写(不用研究具体的实现过程) ...

  7. Linux内核--网络栈实现分析(二)--数据包的传递过程--转

    转载地址http://blog.csdn.net/yming0221/article/details/7492423 作者:闫明 本文分析基于Linux Kernel 1.2.13 注:标题中的”(上 ...

  8. Linux内核--网络栈实现分析(一)--网络栈初始化--转

    转载地址 http://blog.csdn.net/yming0221/article/details/7488828 作者:闫明 本文分析基于内核Linux Kernel 1.2.13 以后的系列博 ...

  9. Linux内核态抢占机制分析

    http://blog.sina.com.cn/s/blog_502c8cc401012pxj.html [摘要]本文首先介绍非抢占式内核(Non-Preemptive Kernel)和可抢占式内核( ...

随机推荐

  1. Mac002--Git安装

    Mac--Git安装 注意:在安装Git前,可先安装homebrew,应用brew命令安装Git即可. 一.Mac 安装homebrew 参考博客:https://blog.csdn.net/yuex ...

  2. JavaScript List

    function List() {    this.listSize = 0;    this.pos = 0;    this.dataSource = [];    this.clear = fu ...

  3. tab区域折叠

    <!DOCTYPE html><html><head> <meta charset="utf-8"> <title>co ...

  4. 洛谷 P1972 [SDOI2009]HH的项链(树状数组,离线)

    传送门 解题思路 因为是求区间的不同种类数,所以我们用树状数组(貌似并没有什么直接联系) (...表示到) 还是和原来一样,用s[i]来表示a[i-lowbit(i)]...a[i]的种类数. 因为有 ...

  5. python学习第六天运算符总结大全

    python学习第六天运算符总结大全,玖乐网络(www.96net.com.cn)列出下面详细 1,算术运算符 + 加 - 减 *乘 / 除 % 求模 ** 乘方 // 取整除 - 返回商的整数部分 ...

  6. avaScript —— 常用正则表达式

    用户名 /^[a-z0-9_-]{3,16}$/ 密码 /^[a-z0-9_-]{6,18}$/ 十六进制值 /^#?([a-f0-9]{6}|[a-f0-9]{3})$/ 电子邮箱 /^([a-z0 ...

  7. Java JNA (二)—— dll回调函数实现

    java调用dll文件需要使用回调函数作为公开函数的参数时,用以下方法实现: 首先,看c++中定义的dll公开函数: typedef void (*ccback)(char *name ,int le ...

  8. Splay平衡树入门小结

    学习到这部分算是数据结构比较难的部分了,平衡树不好理解代码量大,但在某些情况下确实是不可替代的,所以还是非学不可. 建议先学Treap之后在学Splay,因为其实Splay有不少操作和Treap差不多 ...

  9. 【Leetcode周赛】从contest-91开始。(一般是10个contest写一篇文章)

    Contest 91 (2018年10月24日,周三) 链接:https://leetcode.com/contest/weekly-contest-91/ 模拟比赛情况记录:第一题柠檬摊的那题6分钟 ...

  10. Vue 列表渲染中的key

    首先看一下官网的论述: 当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略.如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单 ...