ARM linux解析之压缩内核zImage的启动过程

  • Post author:
  • Post category:linux





ARM linux


解析之压缩内核


zImage


的启动过程





semilog@163.com




首先,我们要知道在


zImage


的生成过程中,是把


arch/arm/boot/compressed/head.s


和解压代码


misc.c





decompress.c


加在压缩内核的最前面最终生成


zImage


的,那么它的启动过程就是从这个


head.s


开始的,并且如果代码从


RAM


运行的话,是与位置无关的,可以加载到内存的任何地方。


下面以


arch/arm/boot/compressed/head.s


为主线进行启动过程解析。




1.









head.s





debug


宏定义部分


最开始的一段都是


head.s





debug


宏定义部分,这部分可以方便我们调试时使用。


如下:


#ifdef


DEBUG


#if defined(


CONFIG_DEBUG_ICEDCC


)


#if defined(CONFIG_CPU_V6) || defined(CONFIG_CPU_V6K) || defined(CONFIG_CPU_V7)














.macro loadsp, rb, tmp














.endm














.macro writeb, ch, rb














mcr







p14, 0, \ch, c0, c5, 0














.endm


#elif defined(CONFIG_CPU_XSCALE)














.macro loadsp, rb, tmp














.endm














.macro writeb, ch, rb














mcr







p14, 0, \ch, c8, c0, 0














.endm


#else














.macro loadsp, rb, tmp














.endm














.macro writeb, ch, rb














mcr







p14, 0, \ch, c1, c0, 0














.endm


#endif


#else


#include


<mach/debug-macro.S>
















.macro writeb, ch, rb














senduart \ch, \rb














.endm


#if defined(CONFIG_ARCH_SA1100)














.macro loadsp, rb, tmp














mov






\rb, #0x80000000



@ physical base address


#ifdef CONFIG_DEBUG_LL_SER3














add \rb, \rb, #0x00050000 @ Ser3


#else














add \rb, \rb, #0x00010000 @ Ser1


#endif














.endm


#elif defined(CONFIG_ARCH_S3C2410)














.macro loadsp, rb, tmp














mov






\rb, #0x50000000














add \rb, \rb, #0x4000 * CONFIG_S3C_LOWLEVEL_UART_PORT














.endm


#else














.macro loadsp, rb, tmp














addruart \rb, \tmp














.endm


#endif


#endif


#endif


如果开启


DEBUGging


宏的话,这部分代码分两段


CONFIG_DEBUG_ICEDCC


是用


ARMv6


以上的加构支持的


ICEDCC


技术进行调试,


DCC





Debug Communications Channel


)是


ARM


的一个调试通信通道,在串口无法使用的时候可以使用这个通道进行数据的通信,具体的技术参前


ARM


公司文档《


ARM Architecture Reference Manual


》。


第二部分首先


#include <mach/debug-macro.S>


,这个文件定义位于


arch/arm/mach-xxxx/include/mach/debug-macro.S


里面,所以这个是和平台相关的,里面定义了每个平台的相关的串口操作,因这个时候系统还没有起来,所以它所用的串口配置参数是依赖于前一级


bootloader


所设置好的,如我们使用的


u-boot


设置好所有的参数。如我们的


EVB





ARM


的实现如下:


#include <mach/hardware.h>


#include <mach/platform.h>











.macro



addruart



, rp, rv









ldr \rp, =ARM_EVB_UART0_BASE

























@ System peripherals (phys address)









ldr \rv, =(IO_BASE+ ARM_EVB _UART0_BASE)












@ System peripherals (virt address)









.endm











.macro



senduart



,rd,rx









strb







\rd, [\rx, #(0x00)]



@ Write to Transmitter Holding Register









.endm











.macro



waituart



,rd,rx


1001:




ldr \rd, [\rx, #(0x18)]

























@ Read Status Register









tst \rd, #0x20










































@when TX FIFO



Full, then wait









bne 1001b









.endm











.macro



busyuart



,rd,rx


1001:




ldr \rd, [\rx, #(0x18)]








@ Read Status Register









tst \rd, #0x08


























@ when uart is busy then wait









bne 1001b









.endm


主要实现



addruart





senduart





waituart





busyuart



这四个函数的具体实施。这个是调试函数打印的基础。


下面是调试打印用到的



kputc





kphex














.macro


kputc


,val














mov






r0, \val














bl





putc














.endm
















.macro


kphex


,val,len














mov






r0, \val














mov






r1, #\len














bl





phex














.endm


它所调用的



putc







phex



是在


head.s


最后的一段定义的,如下


#ifdef DEBUG














.align




2














.type





phexbuf,#object



phexbuf



:




.space



12














.size






phexbuf, . – phexbuf


上面是分配打印


hex





buffer


,下面是具体的实现:


@ phex corrupts {r0, r1, r2, r3}



phex



:









adr r3, phexbuf














mov






r2, #0














strb







r2, [r3, r1]


1:









subs






r1, r1, #1














movmi r0, r3














bmi







puts














and r2, r0, #15














mov






r0, r0, lsr #4














cmp






r2, #10














addge




r2, r2, #7














add r2, r2, #’0′














strb







r2, [r3, r1]














b





1b




@ puts corrupts {r0, r1, r2, r3}



puts



:










loadsp



r3, r1


1:









ldrb







r2, [r0], #1














teq r2, #0














moveq



pc, lr


2:









writeb



r2, r3














mov






r1, #0x00020000


3:









subs






r1, r1, #1














bne 3b














teq r2, #’\n’














moveq



r2, #’\r’














beq 2b














teq r0, #0














bne 1b














mov






pc, lr


@ putc corrupts {r0, r1, r2, r3}



putc



:














mov






r2, r0














mov






r0, #0














loadsp



r3, r1














b





2b




@ memdump corrupts {r0, r1, r2, r3, r10, r11, r12, lr}



memdump



:





mov






r12, r0














mov






r10, lr














mov






r11, #0


2:









mov






r0, r11, lsl #2














add r0, r0, r12














mov






r1, #8














bl




phex














mov






r0, #’:’














bl




putc


1:









mov






r0, #’ ‘














bl




putc














ldr



r0, [r12, r11, lsl #2]














mov






r1, #8














bl




phex














and r0, r11, #7














teq r0, #3














moveq



r0, #’ ‘














bleq






putc














and r0, r11, #7














add r11, r11, #1














teq r0, #7














bne 1b














mov






r0, #’\n’














bl




putc














cmp






r11, #64














blt



2b














mov






pc, r10


#endif


嘿嘿,还有



memdump



这个函数可以用,不错。


好了,言归正传,再往下看,代码如下:














.macro



debug_reloc_start


#ifdef DEBUG














kputc




#’\n’














kphex




r6, 8
























kputc




#’:’














kphex




r7, 8












#ifdef CONFIG_CPU_CP15














kputc




#’:’














mrc







p15, 0, r0, c1, c0














kphex




r0, 8












#endif














kputc




#’\n’














kphex




r5, 8
























kputc




#’-‘














kphex




r9, 8
























kputc




#’>’














kphex




r4, 8
























kputc




#’\n’


#endif














.endm
















.macro



debug_reloc_end


#ifdef DEBUG














kphex




r5, 8
























kputc




#’\n’














mov






r0, r4














bl




memdump









#endif














.endm


debug_reloc_start


用来打印出一些代码重定位后的信息,关于重定位,后面会说,











debug_reloc_end


用来把解压后的内核的


256


字节的数据


dump


出来,查看是否正确。很不幸的是,这个不是必须调用的,调试的时候,这些都是要自己把这些调试函数加上去的。好


debug


部分到这里就完了。




2.









head.s





.start


部分,进入或保持在


svc


模式,并关中断


继续向下分析,下面是定义


.start


段,这段在链接时被


链接到代码的最开头,那么


zImage


启动时,最先执行的代码也就是下面这段代码



start



开始的,如下:
















.section





.start


“, #alloc, #execinstr














.align














.arm






















@ Always enter in ARM state


start:














.type





start,#function














.rept





7














mov






r0, r0














.endr






ARM(












mov






r0, r0










)






ARM(












b





1f









)





THUMB(










adr r12, BSYM(1f)



)





THUMB(










bx



r12







)
















.word




0x016f2818












@ Magic numbers to help the loader














.word




start

















@ absolute load/run zImage address














.word




_edata














@ zImage end address





THUMB(










.thumb













)


1:









mov






r7, r1















@ save architecture ID














mov






r8, r2















@ save atags pointer




#ifndef __ARM_ARCH_2__



























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 running














msr







cpsr_c, r2


#else














teqp






pc, #0x0c000003










@ turn off interrupts


#endif


为何这个会先执行呢?问的好。



那么来个中断吧:



这个是由


arch/arm/boot/compressed/


vmlinux.lds


的链接脚本决定的,如下:





.text : {







_start = .;








*(.start)










*(.text)







*(.text.*)







*(.fixup)







*(.gnu.warning)







*(.rodata)







*(.rodata.*)







*(.glue_7)







*(.glue_7t)







*(.piggydata)







. = ALIGN(4);





}


怎么样,看到没,


.text


段最开始的一部分就是


.start


段,所以这就注定了它就是最先执行的代码。


好了,中断结束,再回到先前面的代码,这段代码的最开始是会被编译器编译成


8





nop





这个是为了留给


ARM


的中断向量表的,但是整个


head.s


都没有用到中断啊,谁知道告诉我一下,谢了。


然后呢,把


u-boot


传过来的放在


r1,r2


的值,存在


r7,r8


中,


r1


存是的


evb


板的


ID


号,而


r2


存的是内核要用的参数地址,这两个参数在解压内核的时候不要用到,所以暂时保存一下,解压内枋完了,再传给


linux


内核。


再然后是几个宏定义的解释,


ARM()





BSYM()





THUMB()


,再加上


W()


吧,


这几个个宏定义都是在


arch/arm/include/asm/unified.h


里面定义的,好了,这里也算个中断吧,如下:



#ifdef



CONFIG_THUMB2_KERNEL


……


#define



ARM



(x…)


#define



THUMB



(x…)




x


#ifdef __ASSEMBLY__


#define



W



(instr)





instr.w


#endif


#define



BSYM



(sym)






sym + 1



#else








……


#define



ARM



(x…)



x


#define



THUMB



(x…)


#ifdef __ASSEMBLY__


#define



W



(instr)





instr


#endif


#define



BSYM



(sym)






sym



#endif


好的看到上面的定义你就会明白了,这里是为了兼容


THUMB2


指令的内核。


关于


#define



ARM



(x…)


里面的“





”,没有见过吧,这个是


C


语言的


C99


的新标准,变参宏,就是在


x


里,你可以随便你输入多少个参数。别急还没有完,因为没有看见文件里有什么方包含这个头文件。是的文件中确实没有包含,它的定义是在:


arch/arm/makefile


中加上的:


KBUILD_AFLAGS





+= -include asm/unified.h


行,这些宏解释到此,下面再出现,我就无视它了。


好了,再回来,读取


cpsr


并判断是否处理器处于


supervisor


模式——从


u-boot


进入


kernel


,系统已经处于


SVC32


模式;而利用


angel


进入则处于


user


模式,还需要额外两条指令。之后是再次确认中断关闭,并完成


cpsr


写入。



注:



Angel





ARM


公司的一种调试方法,它本身就是一个调试监控程序,是一组运行在目标机上的程序,可以接收主机上调试器发送的命令,执行诸如设置断点、单步执行目标程序、观察或修改寄存器、存储器内容之类的操作。与基于


jtag


的调试代理不同,


Angel


调试监控程序需要占用一定的系统资源,如内存、串行端口等。使用


angel


调试监控程序可以调试在目标系统运行的


arm


程序或


thumb


程序。


好了,里面有一句:


teqp



pc, #0x0c000003










@ turn off interrupts


是否很奇怪,不过大家千万不要纠结它,因为它是


ARMv2


架构以前的汇编方法,用于模式变换,和中断关闭的,看不明白也没关系,因为我们以后也用不到。这里知道一下有这个事就行了。











行,到这里


.start


段就完了,代码那么多,其实就是做一件事,保证运行下面的代码时已经进入了


SVC


模式,并保证中断是关的,完了


.start


部分结束。




3.












text


段开始,先是内核解压地址的确定











再往下看,代码如下:

















.text


#ifdef


CONFIG_AUTO_ZRELADDR














@ determine final kernel image address














mov






r4, pc














and r4, r4, #0xf8000000














add r4, r4, #


TEXT_OFFSET


#else














ldr



r4, =


zreladdr


#endif





~~~~


不要小这一段代码,东西好多啊。如哪入手呢?好吧,先从


linux


基本参数入手吧,见表


.1


,里面我写的很详细,因为表格我要放一页,解释我就写在上面了。


TEXT_OFFSET


是代码相对于物理内存的偏移,通常选为


32k=0x8000


。这个是有原因的,具体的原因后面会说。先看


CONFIG_AUTO_ZRELADDR


这个宏所含的内容,它的意思是如果你不知道



ZRELADDR



地址要定在内存什么地方,那么这段代码就可以帮你。看到


0xf8000000


了吧,那么后面有多少个


0


呢?答案是


27


个,那么


2





27


次方就是


128M


,这就明白了,只要你把解压程序放在你最后解压完成后的内核空间的


128M


之内的偏移的话,就可以自动设定好解压后内核要运行的地址



ZRELADDR





如果你没有定义的话,那么,就会去取


zreladdr


作为最后解压的内核运行地。那么这个


zreladdr


是从哪里来的呢?答案是在:


arch/arm/boot/compressed/Makefile


中定义的


# Supply ZRELADDR to the decompressor via a linker symbol.


ifneq ($(CONFIG_AUTO_ZRELADDR),y)


LDFLAGS_vmlinux += –defsym

zreladdr

=$(

ZRELADDR

)


endif



ZRELADDR



这又是哪里定义的呢?答案是在:


arch/arm/boot/Makefile


中定义的


ifneq ($(MACHINE),)


include $(srctree)/$(MACHINE)/Makefile.boot


endif


# Note: the following conditions must always be true:


#




ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET)


#




PARAMS_PHYS must be within 4MB of ZRELADDR


#




INITRD_PHYS must be in RAM


ZRELADDR







:= $(zreladdr-y)


PARAMS_PHYS


:= $(params_phys-y)


INITRD_PHYS


:= $(initrd_phys-y)


而里面的几个参数是在每个


arch/arm/Mach-xxx/ Makefile.boot


里面定义的,内容如下:










zreladdr-y







:= 0x20008000


params_phys-y


:= 0x20000100


initrd_phys-y







:= 0x21000000


这下知道了,绕了一大圈,终于知道


r4


存的是什么了,就是最后内核解压的起址,也是最后解压后的内核的运行地址,记住,这个地址很重要。





解压内核参数


解压时


symbol




解释


ZTEXTADDR


千成不要看成


ZTE


啊,呵,这里是


zImage


的运行的起始地址,当内核从


nor flash


中运行的时候很重要,如果在


ram


中运行,这个设为


0


ZBSSADDR


这个地址也是一样的,这个是


BSS


的地址,如果在


nor


中运行解压的话,这个地址很重要。这个要放在


RAM




ZRELADDR


这个地址很重要,这个是解压后内核存放的地址,也是最后解压后内核的运行起址。


一般设为内存起址的


32K


之后,如


ARM: 0x20008000



ZRELADDR = PHYS_OFFSET + TEXT_OFFSET


INITRD_PHYS


RAM disk


的物理地址


INITRD_VIRT


RAM disk


的虚拟地址


__virt_to_phys(INITRD_VIRT) = INITRD_PHYS


PARAMS_PHYS


内核参数的物理地址



内核参数


PHYS_OFFSET


实际


RAM


的物理地址


对于当前


ARM


来说,就是


0x20000000


PAGE_OFFSET


内核空间的如始虚拟地址,通常


: 0xC0000000


,高端


1G


__virt_to_phys(PAGE_OFFSET) = PHYS_OFFSET


TASK_SIZE


用户进程的内存的最太值(以字节为单位)


TEXTADDR


内核启运行的虚拟地址的起址,通常设为


0xC0008000


TEXTADDR = PAGE_OFFSET + TEXT_OFFSET


__virt_to_phys(TEXTADDR) = ZRELADDR


TEXT_OFFSET


相对于内存起址的内核代码存放的偏移,通常设为


32k (0x8000)


DATAADDR


这个是内核数据段的虚拟地址的起址,当用


zImage


的时候不要定义。





.1


内核参数解释




4.









打开


ARM


系统的


cache


,为加快内核解压做好准备











可以看到,打开


cache


的就一个函数,如下:



bl










cache_on


看起来很少,其实展开后内容还是很多的。我们来看看这个


cache_on


在哪里,可以找到代码如下:














.align




5



cache_on



:



mov











r3, #8

















@ cache_on function














b







call_cache_fn


这里设计的很精妙的,只可意会,注意



mov







r3, #8



,不多解释,跟进去


call_cache_fn




call_cache_fn


:



adr r12,



proc_types


#ifdef


CONFIG_CPU_CP15














mrc







p15, 0, r9, c0, c0




@ get processor ID


#else














ldr



r9, =CONFIG_PROCESSOR_ID


#endif


1:











ldr



r1, [r12, #0]











@ get value














ldr



r2, [r12, #4]











@ get mask














eor r1, r1, r9










@ (real ^ match)














tst



r1, r2















@








& mask





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



是什么,定义如下:



proc_types:














……




.word




0x000f0000












@ new CPU Id














.word




0x000f0000














W(b)





__armv7_mmu_cache_on














W(b)





__armv7_mmu_cache_off














W(b)





__armv7_mmu_cache_flush











…….
















.word




0
















@ unrecognised type














.word




0














mov






pc, lr





THUMB(










nop
























)














mov






pc, lr





THUMB(










nop
























)














mov






pc, lr





THUMB(










nop
























)


可以看到这是一个以



proc_types



为起始地址的表,上面我列出了第一个表项,和最后一个表项,如果查表不成功,则走最后一个表项返回。它实现的功能就是存两个数据,三条跳转指令,我们可以第一条是它的值,第二条是它的


mask


值,三条跳转分别是:


cache_on,cache_off,cache_flush







我想从


ARMv4


指令向下都是有


CP15


协处理器的吧,故:


CONFIG_CPU_CP15


是定义的,那下面我们来分析指令吧。


mrc







p15, 0, r9, c0, c0




@ get processor ID


这个意思是取得


ARM


处理器的


ID


,这个又要看《


ARM Architecture Reference Manual


》了,这里我找了


arm1176jzfs


的架构手册,也是我用的


ARM


所用的架构。里面的解释如下:






这里我们主要关心


Architecture


这项,我们的


ARM


这个值是


: 0x410FB767


,说明用的是


r0p7





release




好了读取了这个值存入


r9


寄存器,然后使用算法


(real ^ match) & mask


,程序中:


( r9 ^r1)&r2





这里


r1


存是是表中的第一个


CPU





ID


值,


r2





mask


值,对于我们的


ARM


,结果如下:


0x410FB767 ^ 0x000f0000 = 0x4100B767


0x4100B767 & 0x000f0000 = 0





match


上了,这个时候就会如下:


ARM(










addeq




pc, r12,



r3











) @ call cache function


我们知道


r3


的值是


0x8


,那么


r12


表项的基址加上


0x8


就正好是表中的第一条跳转指令:


W(b)





__armv7_mmu_cache_on


明白了,为何


r3


要等于


0x8


了吧,如果要调用


cache_off


,那么只要把


r3


设为


0xC


就可以了。精妙吧。行接着往下看


__armv7_mmu_cache_on


,如下:



__armv7_mmu_cache_on



:














mov






r12, lr


#ifdef CONFIG_MMU














mrc







p15, 0, r11, c0, c1, 4




@ read ID_MMFR0














tst



r11, #0xf










@



VMSA







见注:

















blne











__setup_mmu



注:



VMSA (Virtual Memory System Architecture)


,其实就是虚拟内存,通俗地地说就是否支持


MMU




首先是保存


lr


寄存器到


r12


中,因为我们马上就要调用



__setup_mmu



了,最后返回也只要用


r12


就可以了。然后再查看


cp15





c7,c10,4


看是否支持


VMSA


,具体的见注解。我们

在这里我们的



ARM


肯定是支持的,所以就要建立页表,准备打开


MMU


,从而可以使能


cache




好了下面,就是跳到


__setup_mmu


进行建产页表的过程,代码如下:



__setup_mmu



:





sub r3, r4, #16384








@ Page directory size














bic



r3, r3, #0xff











@ Align the pointer














bic



r3, r3, #0x3f00














mov






r0, r3














mov






r9, r0, lsr #18














mov






r9, r9, lsl #18









@ start of RAM














add r10, r9, #0x10000000



@ a reasonable RAM size














mov






r1, #0x12














orr r1, r1, #3 << 10














add r2, r3, #16384


1:









cmp






r1, r9















@ if virt > start of RAM


#ifdef CONFIG_CPU_DCACHE_WRITETHROUGH














orrhs





r1, r1, #0x08










@ set cacheable


#else














orrhs





r1, r1, #0x0c










@ set cacheable, bufferable


#endif














cmp






r1, r10














@ if virt > end of RAM














bichs





r1, r1, #0x0c










@ clear cacheable, bufferable














str



r1, [r0], #4







@ 1:1 mapping














add r1, r1, #1048576














teq r0, r2














bne 1b


关于


MMU


的知识又有好多啊,同样可以参看《


ARM Architecture Reference Manual


》,还可以看《


ARM


体系架构与编程》关于


MMU


的部分,我这里只简单介绍一下我们这里用到


MMU


。这里只使用到了


MMU


的段映,故我只介绍与此相关的部分。


对于段页的大小


ARM


中为


1M


大小,对于


32


位的


ARM


,可寻址空间为


4G=4096M


,故每一个页表项表示


1M


空间的话,需要


4096


个页表项,也就是


4K


大小,而每一个页表项的大小是


4


字节,这就是说我们进行段映射的话,需要


16K


的大小存储段页表。


下面来看一下段页表的格式,如下:






.1


段页表项的具体内容


可以知道对于进行


mmu


段映射这种方式,一共有


4K


个这样的页表项,点大小


16K


字节。在这里我们的


16k


页表放哪呢?看程序第一句:



__setup_mmu



:





sub r3, r4, #16384








@ Page directory size


我们知道


r4


存内核解压后的基址,那么这句就是把页表放在解压后的内核地址的前面


16K


空间如下图所示:








.2 linux


内核地址空间


(里面地址是用的是以我用的


ARM


为例的)


好了,再回到


MMU


,从


MMU_PAGE_BASE (0x20004000)


建立好页表后,


ARM





cpu


如何知道呢?这个就是要用到


CP15





C2


寄存器了,页表基址就是存在这里面的,其中


[31:14]


为内存中页表的基址,


[13:0]


应为


0


如下图:








.3 CP15





C2


寄存器中的页表项基址格式


所以我们初始化完段页表后,就要把页表基址


MMU_PAGE_BASE (0x20004000)


存入


CP15





C2


寄存器,这样


ARM


就知道到哪里去找那些页表项了。下面我们来看一下整个


MMU


的虚拟地址的寻址过程,如图


4


所示。


简单解释一下。首先,


ARM





CPU





CP15





C2


寄存器中找取出页表基地址,然后把虚拟地址的最高


12


位左移两位变为


14


位放到页表基址的低


14


位,组合成对应


1M


空间的页表项在


MMU


页表中的地址。然后,再取出页表项的值,检查


AP


位,域,判断是否有读写的权限,如果没有权限测会抛出数据或指令异常,如果有权限,就把最高


12


位取出加上虚拟地址的低


20


位段内偏移地址组合成最终的物理地址。到这里整个


MMU


从虚拟地址到物理地址的转换过程就完成了。


这段代码里,只会开启页表所在代码的开始的


256K


对齐的一个


0x10000000





256M


)空间的大小(这个空间必然包含解压后的内核),使能


cache





write buffer


,其他的


4G-256M


的空间不开启。这里使用的是


1





1


的映射。到这里也很容易明白


MMU





cache





write buffer


的关系了,为什么不开


MMU


无法使用


cache


了。








.4 MMU


的段页表的虚拟地址与物理地址的转换过程


这里的


4G


空间全部映射完成之后,还会做一个映射,代码如下:














mov






r1, #0x1e














orr r1, r1, #3 << 10














mov






r2, pc














mov






r2, r2, lsr #20














orr r1, r1, r2, lsl #20














add r0, r3, r2, lsl #2














str



r1, [r0], #4














add r1, r1, #1048576














str



r1, [r0]














mov






pc, lr


通过注释就可以知道把当前


PC


所在地址1


M


对齐的地方的


2M


空间开启


cache





write buffer


为了加快代码在


nor flash


中运行的速度。然后反回,到这里


16K





MMU


页表就完全建立好了。


然后再反回到建立页表后的代码,如下:









mov






r0, #0














mcr







p15, 0, r0, c7, c10, 4




@ drain write buffer














tst



r11, #0xf










@ VMSA














mcrne



p15, 0, r0, c8, c7, 0






@ flush I,D TLBs


#endif














mrc







p15, 0, r0, c1, c0, 0






@ read control reg














bic



r0, r0, #1 << 28





@ clear SCTLR.TRE














orr r0, r0, #0x5000












@ I-cache enable, RR cache replacement














orr r0, r0, #0x003c












@ write buffer


#ifdef CONFIG_MMU


#ifdef CONFIG_CPU_ENDIAN_BE8














orr r0, r0, #1 << 25





@ big-endian page tables


#endif














orrne





r0, r0, #1









@ MMU enabled














movne



r1, #-1














mcrne



p15, 0, r3, c2, c0, 0






@ load page table pointer














mcrne



p15, 0, r1, c3, c0, 0






@ load domain access control


#endif














mcr







p15, 0, r0, c1, c0, 0






@ load control register














mrc







p15, 0, r0, c1, c0, 0






@ and read it back














mov






r0, #0














mcr







p15, 0, r0, c7, c5, 4






@ ISB














mov






pc, r12


这段代码就不具体解释了,多数是关于


CP15


的控制寄存器的操作,主要是


flush I-cache,D-cache, TLBS





write buffer





然后存页表基址啊,最后打开


MMU


这个是最后一步,前面所有东西都设好之后再使用


MMU


,否则系统就会挂掉。最后用保存在


r12


中的地址,反回到


BL cache_on


的下一句代码。如下:



restart:









adr r0,


LC0














ldmia




r0, {r1, r2, r3, r6, r10, r11, r12}














ldr



sp, [r0, #28]































sub r0, r0, r1










@ calculate the delta offset














add r6, r6, r0










@ _edata














add r10, r10, r0












@ inflated kernel size location


好了,先来看一下


LC0


是什么东西吧。














.align




2














.type





LC0, #object



LC0:








.word






LC0




















@ r1














.word




__bss_start












@ r2














.word




_end
















@ r3














.word




_edata














@ r6














.word




input_data_end – 4 @ r10 (inflated size location)














.word




_got_start








@ r11














.word




_got_end










@ ip














.word




.L_user_stack_end



@ sp














.size






LC0, . – LC0


好吧,要理解它,再把


arch/arm/boot/vmlinux.lds.in


搬出来吧:






_got_start = .;





.got

















: { *(.got) }






_got_end = .;





.got.plt










: { *(.got.plt) }






_edata = .;













. = BSS_START;






__bss_start = .;





.bss















: { *(.bss) }






_end = .;







. = ALIGN(8);
















.stack












: { *(.stack) }














.align














.section “.stack”, “aw”, %nobits


再加上最后一段代码,关于


stack


的空间的大小分配:


.L_user_stack:



.space



4096



.L_user_stack_end



:


这里不仅可以看到各个寄存器里所存的值的意思,还可以看到



. = BSS_START;



在这里的作用


arch/arm/boot/compressed/Makefile


里面:


ifeq ($(CONFIG_ZBOOT_ROM),y)


ZTEXTADDR






:= $(CONFIG_ZBOOT_ROM_TEXT)


ZBSSADDR := $(CONFIG_ZBOOT_ROM_BSS)


else


ZTEXTADDR






:= 0


ZBSSADDR := ALIGN(8)


endif


SEDFLAGS = s/TEXT_START/$(ZTEXTADDR)/;s/



BSS_START



/


$(ZBSSADDR)


/


对应到这里的话,就是



BSS_START =



ALIGN(8)





这个替换过程会在


vmlinux.lds.in





vmlinux.lds


的过程中完成,这个过程主要是为了有些内核在


nor flash


中运行而设置的。


好了,再次言归正传,从


vmlinux.lds


文件,可以看到链接后各个段的位置,如下。








.5 zImage


各个段的位置


从这里可以看到,


zImage





RAM


中运行和在


NorFlash


中直接运行是有些区别的,这就是为何前面要区分


ZTEXTADDR





ZBSSADDR


的原因了。


好了,再看下面这两句的区别,如果这个地方弄明白了,那么,下面的内容就会变得很简单,往下看:







restart:









adr r0,


LC0











add



r0,pc,#0x10C








LC0:








.word






LC0






















@ r1











dcd



0x17C


故可知,





zImage


加到


0x20008000


运行时,


PC


值为:


0x20008070


,这个时候


r0=0x2000817C


而通过


ldmia







r0, {r1, r2, r3, r6, r10, r11, r12}


加载内存值后,


r1=0x17C


那么我们看一看这句:


sub r0, r0, r1










@ calculate the delta offset


的值是多少?如下:


r0=


0x2000817C






0x17C =


0x20008000


see~~~


看出来什么没有,这个就是我们的加载


zImage


运行的内存起始地址,这个很重要,后面就要靠它知道我们当前的代码在哪里,搬移到哪里。然后再下一条指令把堆栈指针设置好。然后再把实际代码偏移量加在


r6=_edata





(r10=input_data_end-4)


上面,这就是实际的内存中的地址。好继续往下看:



























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


压缩的工具会把所压缩后的文件的最后加上用小端格式表示的


4


个字节的尾,用来存储所压内容的原始大小,这个信息很要,是我们后面分配空间,代码重定位的重要依据。这里为何要一个字节,一个字节地取,只因为要兼容


ARM


代码使用大端编译的情况,保证读取的正确无误。好了,再往下:


#ifndef CONFIG_ZBOOT_ROM



























add sp, sp, r0














add r10, sp, #0x10000


#else



























mov






r10, r6


#endif


我们这里在


RAM


中运行,所以加上重定位


SP


的指针,加上偏移里,变成实际所在内存的堆栈指针地址。这里主要是为了后面的检查代码是否要进行重定位的时候所提前设置的,因为如果代码不重定位,就不会再设堆栈指针了,重定位的话,则还要重设一次。然后再在堆栈指针的上面开辟一块


64K


大小的空间,用于解压内核时的临时


buffer




再往下看:














add r10, r10, #16384





//16K MMU


页表也不能被覆盖哦,否则解压到复盖后,


ARM


就挂了。














cmp






r4, r10














bhs


wont_overwrite














add r10, r4, r9






ARM(












cmp






r10, pc








)





THUMB(










mov






lr, pc











)





THUMB(










cmp






r10, lr









)














bls





wont_overwrite


这段的检测有点绕人,两种情况都画个图看一下,如图


.6


所示,下面我们来看分析两种不会覆盖的情况:


第一种情况是加载运行的


zImage


在下,解压后内核运行地址


zreladdr


在上,这种情况如果最上面的


64k


的解压


buffer


不会覆盖到内核前的


16k


页表的话,就不用重定位代码跳到


wont_overwrite


执行。


第二种情况是加载运行的


zImage


在上,而解压的内核运行地址


zreladdr


在下面,只要最后解压后的内核的大小加上


zreladdr


不会到当前


pc


值,则也不会出现代码覆盖的情况,这种情况下,也不用重位代码,直接跳到


wont_overwrite


执行就可以了。










.6


内核的两种解压不要重定位的情况


可以我们一般加载的


zImage


的地址,和最后解压的


zreladdr


的地址是相同的,那么,就必然会发生代码覆盖的问题,这时候就要进行代码的自搬移和重定位。具体实现如下:































add r10, r10, #((

reloc_code_end



restart

+ 256) & ~255)














bic



r10, r10, #255





























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


这段代码就是实现代码的自搬移,最开始两句是取得所要搬移代码的大小,进行了


256


字节的对齐,注释上说了,为了避免偏移很小时产生自我覆盖(这个地方暂没有想明白,不过不影响下面分析)。这里还是再画个图表示一下整个搬移过程吧,以


zImage


加载地下和


zreladdr


都为


0x20008000


为例,其他的类似。








.7 zImage


的代码自搬移和内核解压的全程图解





.7


中我已经标好了序号,代码的自搬移和内核解的整个过程都在这里面下面一步步来分解:


①.首先计算要搬移的代码的


.text


段代码的大小,从


restart


开始,到


reloc_code_end


结束,这个就是剩下的


.text


段的内容,这段内容是接在打开


cache


的函数之后的。然后把这段代码搬到核实际解压后


256


字节对齐的边界,然后进行搬移,搬移时一次搬运


32


个字节,故存有搬移大小的


r9


寄存器进行了一下


32


字节对齐的扩展。


②.搬移完成后,会保存一下新旧代码间的


offset


值,存于


r6


中。再重新设置一下新的堆栈的地址,位置如图所示,代码如下:

































sub



r6, r9, r6


#ifndef CONFIG_ZBOOT_ROM

































add



sp, sp, r6


#endif


③.然后进行


cache





flush


,因为马上要进行代码的跳转了,接着就计算新的


restart


在哪里,接着跳过去执行新的重定位后的代码。

















bl





cache_clean_flush



















adr




r0, BSYM(restart)

















add



r0, r0, r6

















mov pc, r0


这个时候就又会到


restart


处执行,会把前面的代码再执行一次,不过这次在执行时,会进入图


.6


所示的代码不用重定位的情况,意料之后的事,接着跳到


wont_overwirte


执行,如下:

















teq




r0, #0

















beq




not_relocated


这两行代码的意思是,看一下只什么时候跳过来的,如果


r0


的值为


0


,说明没有进行代码的重定位,那这个时候跳到


no_relocated


处执行,这段就会跳过


.got


符号表的搬移,因为位置没有变啊。代码写得好严谨啊,佩服。


④.我们这种经过代码重定位的情况下,


r0


的值一定不会零,那么这个时候就要进行


.got


表的重搬移,如图中所示,代码如下:


1:









ldr



r1, [r11, #0]











@ relocate entries in the GOT














add r1, r1, r0










@ table.



This fixes up the














str



r1, [r11], #4











@ C references.














cmp






r11, r12














blo 1b


⑤.下面就来初始化我们一直没有进行初始化的


.bss


段,其实就是清零,位置如图所示。我虽画了一个箭头,但是其实并没有进行任何搬移动作,仅仅清零,代码如下:


not_relocated:





mov






r0, #0


1:









str



r0, [r2], #4







@ clear bss














str



r0, [r2], #4














str



r0, [r2], #4














str



r0, [r2], #4














cmp






r2, r3














blo 1b


这里看到我们可爱的


not_relocated


标号了吧,这个标号就是前面所见到的如果没有进行重定位,就直接跳过来进行


bss


的初始化。


⑥.设置好


64K


的解压缓冲区在堆栈之后,代码如下:

















mov r0, r4

















mov r1, sp





















@ malloc space above stack

















add



r2, sp, #0x10000





@ 64k max

















mov r3, r7


⑦.进行内核的解压过程














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


)


这个函数是


C


下面的函数,那些堆栈的设置啊,


.got


表啊,


64k


的解压缓冲啊,都是为它准备的。第一个参数是内核解压后所存放的地址,第二,第三参数是


64k


解压缓冲起始地址和结束地址,最后一个参数


ID


号,这个由


u-boot


传入。


⑧.这是最后一步了,终于到最后一步了。代码如下:

















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

















mov pc, r4





















@ call kernel


这里先进行


cache





flush


,然后关掉


cache


,再准备好


linux


内核要启动的几个参数,最后跳到


zreladdr


处,进入解压后的内核,到这里压缩内核的使命就完成了。但是它的功劳可不小啊。下面就是真真正正的


linux


内核的启动过程了,这里会进入到


arch/arm/kernel/head.s


这个文件的


stext


这个地址开始执行第一行代码。





版权声明:本文为semilog原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。