您的位置:首页 > 运维架构 > Linux

arm-linux内核启动学习笔记(一)

2015-12-02 21:43 686 查看

arm-linux 内核的启动

这里的分析是从./arch/arm/boot/compressed/head.S:start开始的,这个start标签是zImage的入口代码。 ./arch/arm/kernel里也有个head.S,这个head.S中的stext标签,就是zImage中decompress_kernel之后要跳转的地址,即Image的入口代码(见 arm-linux内核编译过程小结)。

arm启动后执行的第一条指令并不是head.S:start,在此之前一般都会有一个平台相关的bootloader(如mt6582的代码就是平台相关的代码,在mtk平台上bootloader分为preloader和lk两部分)来初始化一些平台相关的信息,然后再跳转到start标签,来执行体系结构相关的代码(如arm的代码就是体系结构相关的代码)。

bootloader部分(如mtk平台中preloader和lk部分)的代码会完成对硬件的默认初始化,准备start的结构ID和atags两个参数。

Part1: [start ,cache_on]

这部分组要是找到平台相关的cache_on函数,然后调用之。cache_on函数的左右就是开启缓存,

一般需要通过初始化页表并使能mmu来实现。

//部分关系不大代码已省略
start:
//指定start这个符号是函数类型
.type   start,#function
//.rept count .endr 把rept和endr之间的指令重复count次
//mov r0,r0 实际上就是nop指令,这里的意思应该是是清空指令流水线
.rept   7
mov r0, r0
.endr
ARM(mov r0, r0)
//跳到前面(下面的1)
ARM(b   1f)

//这个魔数是在bootloader中用于判断zImage存在的,是内核和bootloader约定好的(mtk中参见lk代码)
.word   0x016f2818
//这里存的是start的默认加载地址,这个地址是编译时确定的,与代码最终加载到哪里无关,
//但如果start最终加载的物理地址(zImage这会没开mmu,所以物理地址)
//不是这个编译地址的话,可能需要重定位。
.word   start
//zImage的结束地址,也是编译地址,这个值是在连接的时候确定的,其定义在vmlinux.lds中
.word   _edata          @ zImage end address
//THUMB宏在这里没有定义的,所以这里没啥用,如果定义了,.thumb表示往下都是用thumb指令集。
THUMB(.thumb)

//r1, r2分别存着bootloader传递过来的结构ID和atags
1:      mov r7, r1          @ save architecture ID
mov r8, r2          @ save atags pointer

//关中断
#ifndef __ARM_ARCH_2__
//__ARM_ARCH_2__下的关中断方式,goldfish中也是这种方式
mrs r2, cpsr        @ get current mode
tst r2, #3          @ not user?
bne not_angel
mov r0, #0x17       @ angel_SWIreason_EnterSVC
ARM(swi 0x123456)   @ angel_SWI_ARM
THUMB(svc 0xab)     @ angel_SWI_THUMB
not_angel:
mrs r2, cpsr        @ turn off interrupts to
orr r2, r2, #0xc0   @ prevent angel from runing
msr cpsr_c, r2
#else
//arm 版本2,3的核心的关中断方式
teqp    pc, #0x0c000003     @ turn off interrupts
#endif
.text
#ifdef CONFIG_AUTO_ZRELADDR
@ determine final kernel image address
mov r4, pc
and r4, r4, #0xf8000000
add r4, r4, #TEXT_OFFSET
#else
//我的内核配置了zreladdr,将其加载到r4,这是Image的加载地址
ldr r4, =zreladdr
#endif
bl  cache_on


反汇编后的代码:

ROM:00000000                 AREA ROM, CODE, READWRITE, ALIGN=0
ROM:00000000                 CODE32
; mov r0, r0 被解释成了nop指令,这里应该是用来清空指令流水的
ROM:00000000                 NOP
ROM:00000004                 NOP
ROM:00000008                 NOP
ROM:0000000C                 NOP
ROM:00000010                 NOP
ROM:00000014                 NOP
ROM:00000018                 NOP
ROM:0000001C                 NOP
ROM:00000020                 B close_IRQ
ROM:00000020 ; ----------------------------------------
; zImage魔数
ROM:00000024                 DCD 0x16F2818
; start 标签的编译地址,这里编译地址就是0,也就是说zImage默认被加载到0地址
ROM:00000028                 DCD 0
; 这个数与 ll zImage的大小完全一样,也是zImage编译的结束地址。
ROM:0000002C                 DCD 0x27A1F8
ROM:00000030 ; ----------------------------------------
ROM:00000030
ROM:00000030 close_IRQ      ; CODE XREF: ROM:00000020j
ROM:00000030                 MOV             R7, R1
ROM:00000034                 MOV             R8, R2
ROM:00000038                 MRS             R2, CPSR
ROM:0000003C                 TST             R2, #3
ROM:00000040                 BNE             not_angel
ROM:00000044                 MOV             R0, #0x17
ROM:00000048                 SVC             0x123456
ROM:0000004C
ROM:0000004C not_angel      ; CODE XREF: ROM:00000040j
ROM:0000004C                 MRS             R2, CPSR
ROM:00000050                 ORR             R2, R2, #0xC0
ROM:00000054                 MSR             CPSR_c, R2
ROM:00000058                 ANDEQ           R0, R0, R0
ROM:0000005C                 ANDEQ           R0, R0, R0
; 在goldfish上,r4 存入立即数 0x00008000,为Image的默认加载地址
ROM:00000060                 LDR         R4,=dword_8000
ROM:00000064                 BL              cache_on


cache_on

cache_on函数主要的功能是根据处理器ID,找到处理器对应的体系结构(armXXX)的cache_on函数并调用。kernel支持的所有arm型号的cache_on函数都会包含在zImage里面(这个head.S文件中),如果有新的arm处理器,内核想要支持的话,就需要在kernel.org提交这部分的patch。

//这个8 是proc_types数组中的第三个元素,代表某个处理器型号中cache_on函数的地址。
cache_on:   mov r3, #8          @ cache_on function
b   call_cache_fn


cache_on->call_cache_fn

//call_cache_fn为一个循环,根据当前cpu的魔数(可能是从cp15中获取,或编译时指定的),找到并调用对应处理器的cache_on函数
call_cache_fn:  adr r12, proc_types

//获取处理器ID,goldfish中是通过cp15获取的处理器ID
#ifdef CONFIG_CPU_CP15
mrc p15, 0, r9, c0, c0  @ get processor ID
#else
ldr r9, =CONFIG_PROCESSOR_ID
#endif

//可以先到下面看下proc_types的结构,这里以第一个循环为例
//r1 = proc_types[0] = 0x41560600 代表ARM6/610
1:      ldr r1, [r12, #0]       @ get value
//r2位掩码 r2 = proc_types[1] = 0xffffffe0
ldr r2, [r12, #4]       @ get mask
//EOR <Rd>,<Rn>, <shifter_operand>, 将shifter_operand与Rn做异或操作,结果存于Rd
eor r1, r1, r9      @ (real ^ match)
//TST <Rn>, <shifter_operand>, Rn - shifter_operand,用结果更新标志位
tst r1, r2          @       & mask
//如果r1 = r2,代表处理器匹配,跳转到对应的cache_on函数
ARM(addeq   pc, r12, r3) @ call cache function
THUMB(addeq r12, r3)
THUMB(moveq pc, r12) @ call cache function
//如果不一致,则增加到下一个位置,继续测试
add r12, r12, #PROC_ENTRY_SIZE
//如果不匹配,则往上跳,循环。
b   1b


proc_types里面记录各个arm处理器的魔数,掩码,以及相应的cache函数,如下:

/*
*   - CPU ID match
*   - CPU ID mask
*   - 'cache on' method instruction
*   - 'cache off' method instruction
*   - 'cache flush' method instruction
*/
.align  2
.type   proc_types,#object
proc_types:
.word   0x41560600      @ ARM6/610
.word   0xffffffe0
W(b) __arm6_mmu_cache_off   @ works, but slow
W(b)    __arm6_mmu_cache_off
mov pc, lr
.word   0x41007000      @ ARM7/710
.word   0xfff8fe00
W(b)    __arm7_mmu_cache_off
W(b)    __arm7_mmu_cache_off
mov pc, lr
THUMB(      nop             )
.word   0x41807200  @ ARM720T (writethrough)
.word   0xffffff00
W(b)    __armv4_mmu_cache_on
W(b)    __armv4_mmu_cache_off
mov pc, lr
THUMB(      nop             )
.word   0x41007400      @ ARM74x
.word   0xff00ff00
W(b)    __armv3_mpu_cache_on
W(b)    __armv3_mpu_cache_off
W(b)    __armv3_mpu_cache_flush

.word   0x41009400      @ ARM94x
.word   0xff00ff00
W(b)    __armv4_mpu_cache_on
W(b)    __armv4_mpu_cache_off
W(b)    __armv4_mpu_cache_flush

.word   0x41069260      @ ARM926EJ-S (v5TEJ)
.word   0xff0ffff0
W(b)    __arm926ejs_mmu_cache_on
W(b)    __armv4_mmu_cache_off
W(b)    __armv5tej_mmu_cache_flush

......


这里以__armv4_mmu_cache_on为例(goldfish 应该是armv7的处理器,看/proc/cpuinfo ):

cache_on->call_cache_fn->__armv4_mmu_cache_on

//cache_on函数做了两件事:初始化页表,开启mmu,因为只有在开启mmu的情况下才能使用cache
__armv4_mmu_cache_on:
//这个函数是通过addeq  pc, r12, r3指令进来的,
//这里保存的lr是start标签的那句bl cache_on之后的位置的
mov r12, lr
#ifdef CONFIG_MMU
//开启缓存相关,缓存具体怎么开怎么用,这里先不管,
//主要先看__setup_mmu是如何启动分页机制的
bl  __setup_mmu
//高速缓存相关操作
mov r0, #0
mcr p15, 0, r0, c7, c10, 4
mcr p15, 0, r0, c8, c7, 0   @ flush I,D TLBs
mrc p15, 0, r0, c1, c0, 0   @ read control reg
orr r0, r0, #0x5000
orr r0, r0, #0x0030
//这个函数中有一步是将页表基地址写入cp15.c2寄存器,就是开启mmu。
bl  __common_mmu_cache_on
mov r0, #0
mcr p15, 0, r0, c8, c7, 0
//返回到start标签的bl cache_on后面那一句
mov pc, r12


__setup_mmu

调用流程是:cache_on->call_cache_fn->__armv4_mmu_cache_on->__setup_mmu

/* 跳转到__setup_mmu之前:
* r4 = zreladdr,为Image的加载地址(在goldfish里面为0x00008000,
* 有的系统为0x3/5/7/0008000,这个值在./arch/arm/$(MACH)/
* Makefile.boot中有定义,$(MACH)指的是具体的芯片)。
* Image前的16KB内存(glodfish上默认为0x00004000 - 0x00008000)
* 是内核的整个页表,大小为16K。
*
*  __setup_mmu函数的主要工作是初始化全局页表(Image加载地址向前16KB)
* 将部分区域加上C/B属性(cache和write buffer),其他区域就直接默认属性了,
* 这里将当前pc所在的两页和解压后的内核所在的256MB空间加上了C/B属性
* 估计因为这一片可能为代码区域,加上C/B属性可以提升访问速度???
*/

//r3 = r4 - 0x4000(16K) 最终r3 = 0x00004000,为页表基地址(pgd)
__setup_mmu:    sub r3, r4, #16384
//r3 = r3 & 0xffffc000 (清除r3的0x3fff位)
bic r3, r3, #0xff       @ Align the pointer
bic r3, r3, #0x3f00
//当前页表项指针 r0 = 0x00004000
mov r0, r3
//要初始化为C/B属性的起始物理地址,这里r9 = 0x0
mov r9, r0, lsr #18
mov r9, r9, lsl #18     @ start of RAM
//要初始化为C/B属性的结束物理地址 r10 = r9 + 256MB
add r10, r9, #0x10000000
//r1 = 0xc12, 这是默认的页表属性
mov r1, #0x12
orr r1, r1, #3 << 10
//页表项的结束位置 r2 = 0x00008000
add r2, r3, #16384
//到这里开始初始化页表了,当前的r1是0xc12,同时也能代表虚拟地址0x00000000所在的页
//因为0xc12是个很小的数。每次循环r1都会递增1MB,在每一次循环中都设定r1
//代表的这1MB虚拟地址如何映射。这里做的是一个恒等映射,唯一不同就是各个区段
//的映射属性不同
1:      cmp r1, r9          @ if virt > start of RAM
//如果r1 > r9 则执行 r1 = r1|0x0C = 0x0C1E
orrhs   r1, r1, #0x0c
//如果 r1 > r10(过了0x10004000),则清除缓存标记
cmp r1, r10         @ if virt > end of RAM
bichs   r1, r1, #0x0c
//r1是当前页描述符,r0是当前页表项的地址,这里是向当前页表项写入页描述符: *r0 = r1; r0 += 4;
str r1, [r0], #4
//r1 += 0x100000 = 2^20 = 1MB,设置下一个页描述符的内容
add r1, r1, #1048576
//如果r0 != r2(没到页表项结束位置)则向后(向上)跳转到1,实现循环。
//这里是将整个0x00004000- 0x00008000处的内存全部初始化作为页表。
teq r0, r2
bne 1b
//16KB页表代表内核4GB空间,其初始化到这里结束,后面修正当前pc所在的虚拟地址
//(等于物理地址)对应的页表的属性了,这是怕pc不在前面那个0xC1E的范围内。
//r1 = 0x0C1E
mov r1, #0x1e
orr r1, r1, #3 << 10
//要修正pc所在页的地址
mov r2, pc
//段对齐
mov r2, r2, lsr #20
//加上页属性,r1为要写入的页表项内容(恒等映射)
orr r1, r1, r2, lsl #20
//获取pc所在页的虚拟地址在全局页表pgd中的页表项的地址 -> r0
add r0, r3, r2, lsl #2
//修改pc所在页和其后面一页的页表属性为0xC1E
str r1, [r0], #4
add r1, r1, #1048576
str r1, [r0]
//返回
mov pc, lr
ENDPROC(__setup_mmu)

/* 最终一级页表的内容:
其中:
第一列是各个页描述符的物理地址
第二列是各个页描述符的内容,整个第二列就是物理内存中的一级页表
第三列是表示当前页描述符处于一级页表中的第几个页表项。
/物理地址/页描述符内容/当前是第几个页表项/
|0x00004000|0x00000C1E|0000| //最后单独修改的两项页表项
|0x00004004|0x00100C1E|0001| //页表项内容>0x00004000
|0x00004008|0x00200C1E|0002| //<0x10004000的属于C/B区
|0x0000400C|0x00300C1E|0003|
......
|0x00004400|0x10000C1E|0256| //页表项内容<0x10004000
|0x00004404|0x10100C12|0257| //页表项内容>0x10004000
|0x00004408|0x10200C12|0258|
......
|0x00007FFC|0xFFF00C12|4095|
一个一级页描述符作为段描述符时,可表示的地址范围为2^20 = 1MB(见后面一级页描述符), 32位cpu的寻址空间为4GB,所以一共需要4096个一级页描述符。前面可以看到16KB,刚好能存储4096个页描述符,所以内核初始化的时候,页表的大小为16KB. 16K/4096 = 4B 一个地址。

此恒等映射最终的结果就是,4GB的任意虚拟地址 = 物理地址。
*/


Part2:(cache_on,call_kernel]

vmlinux(小)的链接脚本:

//./arch/arm/boot/compressed/vmlinux.lds.in
//这个脚本是用来链接vmlinux(小)的,故zImage代码执行时
//内核各个段的分布也是在这个脚本中定义的(部分代码已省略)。
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = TEXT_START;
_text = .;
.text : {
_start = .;
*(.start)
*(.text)
*(.text.*)
*(.fixup)
*(.gnu.warning)
*(.glue_7t)
*(.glue_7)
}
.rodata : {
*(.rodata)
*(.rodata.*)
}
//这里是piggy.gz的位置
.piggydata : {
*(.piggydata)
}
. = ALIGN(4);
_etext = .;
.got.plt      : { *(.got.plt) }
_got_start = .;
.got          : { *(.got) }
_got_end = .;

.pad          : { BYTE(0); . = ALIGN(8); }
_edata = .;

. = BSS_START;
__bss_start = .;
.bss          : { *(.bss) }
_end = .;

. = ALIGN(8);     /* the stack must be 64-bit aligned */
.stack        : { *(.stack) }

.stab 0       : { *(.stab) }
.stabstr 0        : { *(.stabstr) }
.stab.excl 0      : { *(.stab.excl) }
.stab.exclstr 0   : { *(.stab.exclstr) }
.stab.index 0     : { *(.stab.index) }
.stab.indexstr 0  : { *(.stab.indexstr) }
.comment 0        : { *(.comment) }
}


piggy.gzip.S

piggy.gzip.S是用来生成piggy.gzip.o的,其实际上相当于将一个piggy.gzip文件当

二进制打包到vmlinux(小)中了。

//./arch/arm/boot/compressed/piggy.gzip.S
.section .piggydata,#alloc
//global只是声明,并不分配空间
.globl  input_data
input_data:
##INCBIN 指令在被汇编的文件内包含一个文件。 该文件按原样包含,没有进行汇编。
.incbin "arch/arm/boot/compressed/piggy.gzip"
.globl  input_data_end
input_data_end:


cache_on之后

cache_on之后段代的代码大体可认为做了三件事:

1. 检查内核是否需要移动,如果需要移动则将其移动并重定向.got的内容。

2. 调用decompress_kernel解压内核。

3. 调用call_kernel跳转到Image。

//cache_on函数的实际作用是开启高速缓存,这样可以加快后续代码的执行速度
//在arm体系结构中,高速缓存TLB必须依赖于mmu,所以cache_on函数内部通过
//调用__setup_mmu来初始化页表(代码段的页表属性允许缓存)
//调用__common_mmu_cache_on来使能mmu
bl  cache_on
//跳转到这里,mmu已经开启了,恒等映射!
/*
LC0的代码本来是在后面的,这里为了方便解释,先在前面列出了
各个代码段的分布见后(这些值都是编译地址!)。
LC0:        .word   LC0         @ r1
//bss段的开始位置
.word   __bss_start     @ r2
//bss的结束位置
.word   _end            @ r3
//数据段的结束位置
.word   _edata          @ r6
//pizzy.gz的结束位置
.word   input_data_end - 4  @ r10 (inflated size location)
.word   _got_start      @ r11
.word   _got_end        @ r12 (ip)
//栈的结束位置
.word   .L_user_stack_end   @ sp
//这个应该是个伪指令,指定LC0大小的
.size   LC0, . - LC0
*/
//adr是相对于当前pc的相对寻址,一般用于获取指令的真是地址,而不是加载地址
restart:    adr r0, LC0
//把上面LC0中的一堆变量载入到各个寄存器中
ldmia   r0, {r1, r2, r3, r6, r10, r11, r12}
//设置临时栈, sp = L_user_stack_end,目前sp还指向栈底
ldr sp, [r0, #28]

/*
zImage的加载地址未必是编译地址,所以这里需要
将各个变量由编译地址修正为真实地址。
*/
//计算LC0的真实地址(既是物理地址,又是虚拟地址)和编译地址的差值
sub r0, r0, r1      @ calculate the delta offset
//将_edata的编译地址转为真实地址
add r6, r6, r0      @ _edata
//将input_data_end的编译地址转为真实地址
add r10, r10, r0        @ inflated kernel size location

//将sp的编译地址修正为真实地址,并向上增加64K,作为栈顶
add sp, sp, r0
add r10, sp, #0x10000

/*
* The kernel build system appends the size of the
* decompressed kernel at the end of the compressed data
* in little-endian form.
*/
/*
r9是piggy.gzip这个gzip文件的最后四个字节,这四个字节记录的
是解压后的Image的大小存的.在我这里piggy.gzip最后: 30c1 b500 0a
Image大小为0xb5c130, 最后那个0a估计最后会去掉的。
这里用ldrb指令是考虑到这个存Image大小的位置可能不是4byte对齐的。
*/
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

/*
*   这一段代码是来检测是否需要复制自身的
*   r4是最终内核要解压到的地址,是通过zreladdr来指定的
*   r9是Image镜像的大小,是从piggy.gz文件末尾获取的
*   r10是piggy.gz的结束位置
* 往下执行需要满足两个条件之一:
*   1) 最终内核要解压到的地址(r4) - 16k页表 >= zImage的基本代码结束位置(r10)
*   2) 最终内核解压后的结束地址(r4 + r9(image length)) <= 后续执行的代码(wont_overwrite的地址)
0x00000000 -----------------------------------------------------------------------------

------ Image(zreladdr)
|
|
----- zImage起始地址                          |
|                                          |
|                                          |
----- 当前代码(pc)                          ------ Image end 在此之上都可以
----- wont_overwrite的代码
|
----- zImage基本代码结束(piggy.gz末尾,r10)
----- Image(zreladdr)
;如果解压到这个位置往下都是可以的
|
|
----- Image end

0xffffffff -----------------------------------------------------------------------------
*/

//这里r10 += 0x4000是预留给页表的
add r10, r10, #16384
cmp r4, r10
//如果r4 >= r10则满足条件1,Image要解压的地址在zImage后面,
//跳转到wont_overwrite,不需移动自身代码。
bhs wont_overwrite
//r10 = r4 + r9, r10为预计解压后的image的结束地址
add r10, r4, r9
//获取wont_overwrite的物理地址
adr r9, wont_overwrite
//如果否满足条件2,即image解压后没有覆盖wont_overwrite
//之后的代码,则也无需移动自身代码。
cmp r10, r9
bls wont_overwrite
/*
* Relocate ourselves past the end of the decompressed kernel.
*   r6  = _edata
*   r10 = end of the decompressed kernel
* Because we always copy ahead, we need to do it from the end and go
* backward in case the source and destination overlap.
*/
/*
* Bump to the next 256-byte boundary with the size of
* the relocation code added. This avoids overwriting
* ourself when the offset is small.
*/
//否则就需要移动代码的位置了,这里的逻辑是把从restart到zImage结束的代码
//移动到Image预计解压后的结束位置的后面,为什么从restart开始见下面。
add r10, r10, #((reloc_code_end - restart + 256) & ~255)
//这里的r10应该就是最终要移动到的地址了
bic r10, r10, #255

/* Get start of code we want to copy and align it down. */
//要复制的代码的起始地址
adr r5, restart
//清除末尾位
bic r5, r5, #31
//要复制的数据大小
sub r9, r6, r5      @ size to copy
add r9, r9, #31     @ rounded up to a multiple
bic r9, r9, #31     @ ... of 32 bytes
add r6, r9, r5
//要复制到的结束位置
add r9, r9, r10

//复制数据
1:      ldmdb   r6!, {r0 - r3, r10 - r12, lr}
cmp r6, r5
stmdb   r9!, {r0 - r3, r10 - r12, lr}
bhi 1b

/* Preserve offset to relocated code. */
sub r6, r9, r6

//代码被移动过了,所以清缓存
bl  cache_clean_flush
//跳转到restart重新执行,因为当前地址变了,所以LC0的当前地址和编译地址
//的差值就变了,前面的好多变量都是根据这个差值算出来的,所以这里跳到
//restart重新来过,所以前面检测的时候是检测wont_overwrite之前
//的代码是否被覆盖,而移动得要从restart的代码开始移动。
adr r0, BSYM(restart)
add r0, r0, r6
mov pc, r0
//到这里

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
*   r5  = appended dtb size (0 if not present)
*   r7  = architecture ID
*   r8  = atags pointer
*   r11 = GOT start
*   r12 = GOT end
*   sp  = stack pointer
*/
//r5在一开始被初始化为0
orrs    r1, r0, r5
//这里是如果运行地址与链接地址相等则跳转到not_relocated
//不执行got段的重定位
beq not_relocated

//否则将r11,r12存的GOT表的编译地址修改为当前地址
add r11, r11, r0
add r12, r12, r0
//修正bbs段的起始,结束地址
add r2, r2, r0
add r3, r3, r0
//对GOT表中的所有元素做重定位
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

not_relocated:  mov r0, #0
//初始化bss段的所有数据为空
1:      str r0, [r2], #4        @ clear bss
str r0, [r2], #4
str r0, [r2], #4
str r0, [r2], #4
cmp r2, r3
blo 1b

/*
* 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
//这个即是将piggy.gz解压为Image的函数,这里是c代码
bl  decompress_kernel
//解压后再次刷新缓存(个人理解应该是代码区域有代码变动就应该刷新缓存)
bl  cache_clean_flush
bl  cache_off
mov r0, #0          @ must be zero
mov r1, r7          @ restore architecture number
mov r2, r8          @ restore atags pointer
//r4是Image的入口地址,也是Image的解压地址,Image
//本身是个binary文件,第一个字节即为指令,这里跳转到Image
//即./arch/arm/kernel/head.s:stext
ARM(   mov pc, r4  )       @ call kernel


decompress_kernel

decompress_kernel是用c函数完成的,其代码如下

//./kernel/arch/arm/boot/compressed/Misc.c
extern char input_data[];
void decompress_kernel(
unsigned long output_start,        //zImage的解压地址(r4)
unsigned long free_mem_ptr_p,      //临时空间,解压用(这里用的是栈)
unsigned long free_mem_ptr_end_p,  //临时空间结尾
int arch_id)                       // 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是内核启动早期的打印函数,一般都是直接向IO端口写的数据
putstr("Uncompressing Linux...");
//内核解压函数,这里用的是gzip解压,这个input_data和input_data_end
//定义在piggy.gzip.S中,是piggy.gzip的起始和结束位置。
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函数中就已经开始调用打印函数了,这一句

putstr("Uncompressing Linux...")


在goldfish启动的时候是可以打印出来的,在真实设备上(如MT6582),通过串口也是可以打出来的,其实现如下:

./kernel/arch/arm/boot/compressed/Misc.c
static void putstr(const char *ptr)
{
char c;
while ((c = *ptr++) != '\0') {
if (c == '\n')
putc('\r');
putc(c);
}
flush();
}


而这个putc,具体平台实现的方式不同,在goldfish上:

//./arch/arm/mach-goldfish/include/mach/uncompress.h
#define GOLDFISH_TTY_PUT_CHAR (*(volatile unsigned int *)0xff002000)
static void putc(int c)
{
//向IO端口0xff002000直接写入字符,这就是goldfish的uart端口
GOLDFISH_TTY_PUT_CHAR = c;
}


在MT6582上,也是通过一个地址写入的,如下:

//./mediatek/platform/mt6582/kernel/core/include/mach/uncompress.h
#define MT_UART_PHY_BASE 0x11002000
#define MT_UART_THR *(volatile unsigned char *)(MT_UART_PHY_BASE+0x0)
static inline void putc(int c)
{
//向IO端口0x11002000直接写入字符,这就是mt6582的uart端口
//这一句while循环是测试控制位的,具体位作用需参考板子的手册。
while (!(MT_UART_LSR & 0x20));
MT_UART_THR = c;
}


这种写入也就是在内核刚开始,有恒等映射的时候,后续page_init二次映射的时候这些地址都会映射到内核的内核空间的其他地方了(后续分析)。

旧版废弃的内容,但应该是正确的

part1

arm MMU 一级页表寻址:

arm协处理器CP15的C2寄存器记录着页基地址(又可以叫做一级页表基地址),对于一个地址addr, 在arm中第一级寻址算法为:C2[31:14] + addr[31:20] + 00,如图:



注1:

1) C2中记录的页表基地址,又可以叫做一级页表基地址

2) 一级页表基地址中的每一项元素叫做一级页表项

3) 一级页表项中的内容叫做一级页表描述符

4) 一级页表描述符,描述的是否为一个二级页表,这个得看描述符的最后两位是什么。

注2: 一级寻址只与页表基地址的前18位(一级页表基地址),虚拟地址的前12位(一级页表项的页内偏移)有关 (+最后两位00),没有任何标记位,mmu通过将二者组合成一个32位物理地址,这个就是一级页表项的物理地址,从这个物理地址中读取到的内容就是一级页表描述符。

一级页表描述符:

arm 开启MMU后,当用户访问一个虚拟地址时,先根据C2和虚拟地址的前12位标记的物理地址中取出一个一级页表描述符,虚拟地址中剩下20位如何解析,是由这个一级页表描述符的低2位决定的。一共四种组合,对应四种不同的解析方式。在__setup_mmu中初始化的一级页表项的最后两位都是10,这里先介绍10。

一级页表描述符[1:0]为0b10,表示该一级描述符为段描述符。该一级描述符应该如下解析:



虚拟地址->物理地址的过程:



这个过程的总体描述如下:

1) 用户访问一个虚拟地址addr。

2) MMU取出addr的前12位,并根据C2定位一级页表描述符X。

3) MMU 发现 X的最后两位为10,则取出x的前12位,拼接addr的后20位,组成物理地址。

4) 访问此物理地址,获取数据返回给用户。

part2

在zImage的反汇编代码中,input_data在_got_start段,值为0x4015,如下:



查看其内容:



查看piggy.gz:



可知整个zImage,实际上就是vmlinux(2.51MB那个)掐头去尾一点,vmlinux是个标准ELF文件,而zImage就是这个vmlinux去了ELF头尾组成的,其二进制差异不过1%20.

也就是说,实际上是vmlinux包含了piggy.gz,而将其裁剪一点,就形成了最终的zImage。

1. vmlinux(小的那个),是一个标准的elf文件,将其掐头去尾后就形成了zImage,这个就是最终的内核镜像。

2. zImage是最终的内核镜像,其内部包含了一个原封不动的piggy.gz,这是一个压缩后的内核。

3. piggy.gz是image通过gzip命令压缩来的。

4. Image文件是一个纯二进制文件,没有elf头,zImage中的最后一句 MOV PC, R4 ; call_kernel,这个R4就是之前kernel解压的地址,就是直接跳到了Image的相对偏移0位置的指令。

5. Image是从vmlinux(大)中抽取出来的,是objcopy -o binary vmlinux(大)来的。vmlinux(大)入口地址的第一条指令,就是Image这个二进制文件的第一条指令。

所以综上所述,zImage解压内核,跳转到Image偏移0处的指令,实际上相当于执行了vmlinux(大)的入口函数。而vmlinux(大)的入口函数为ENTRY(stext),定义在./arch/arm/kernel/head.S(注,zImage的入口函数定义在./arch/arm/boot/compressed/head.S),也就是说vmlinux(大)的入口函数,也是体系结构相关的!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: