参考: 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. < python PIL - 批量图像处理 - 生成自定义大小图像 >

    < python PIL - 批量图像处理 - 生成自定义大小图像 > 直接用python自带的PIL图像库,对一个文件夹下所有jpg/png的图像进行自定义像素变换 from PIL i ...

  2. ECharts 知识笔记

    涓滴之水终可磨损大石,不是由于它的力量强大,而是由于昼夜不舍的滴坠 定制label样式(图标上显示的对应文字 对文字一些样式的修改) (1)通过“formatter”实现内容自定义: (2)通过“ri ...

  3. String.prototype.includes

    if (!String.prototype.includes) {   String.prototype.includes = function(search, start) {     'use s ...

  4. RTSP取流设备密码含@

    一.rtsp取流格式简介 RTSP的基本取流格式为:rtsp://username:password@ip_addr/... 如海康的ip地址为:rtsp://admin:admin123@10.1. ...

  5. Java-多线程第三篇3种创建的线程方式、线程的生命周期、线程控制、线程同步、线程通信

    1.Java使用Thread类代表线程.     所有的线程对象必须是Thread类或其子类的实例. 当线程继承Thread类时,直接使用this即可获取当前线程,Thread对象的getName() ...

  6. 2、单线性变量的回归(Linear Regression with One Variable)

    2.1 模型表示 我们通过一个例子来开始:这个例子是预测住房价格的,我们要使用一个数据集,数据集包含俄勒冈州波特兰市的住房价格.在这里,我要根据不同房屋尺寸所售出的价格,画出我的数据集.比方说,如果你 ...

  7. Android单位转换 (px、dp、sp之间的转换工具类)

    在Android开发中,涉及到屏幕视频问题的时候,px.dp.sp之间的转换比较重要的一部分,所以杨哥整理了一个工具类给大伙用. package com.zw.express.tool; import ...

  8. sqlite查询语句

    搜索距现在六个月前的月份第一天日期: SELECT date('now','start of month','-6 month','0 day'); 搜索距现在六个月前的日期: SELECT date ...

  9. vue简单的v-for - - 路由跳转

    前几天写了一个特特特简单的小图片页面,主要用到的就是v-for遍历以及路由跳转到详情页.路由跳转需要在router文件夹下index.js引入. 导航栏(element ui导航栏为模板): < ...

  10. JS-03 牛客网练习

    1.很多人都使用过牛客网这个在线编程网站,下面是自己做的该项所有练习,已通过网站和老师检查无误,分享给大家. 2.先说一下题目的位置:牛客网https://www.nowcoder.com/activ ...