随笔之GoldFish Kernel启动过程中arm汇编分析
2013-07-16 15:30
459 查看
转自:
http://my.oschina.net/innost/blog/93302
http://download.csdn.net/detail/innost/4834459
本节介绍Kernel启动。此时Piggy已经将vimlinux解压,BL将执行权限传给了Kernel。
代码在arch/arm/kernel/head.S中。相关代码如下:
//将采用C/C++注释语句
图1CPSR控制位
由图1可知:
qN/Z/C/V控制位用来表示负/零/进位/溢出,属于UserFlags,即可在UserMode下操作。A
qI/F表示Interrupt和FastInterrupt使能位。
qMode用来控制CPU当前的模式。ARMCPU一共有7种模式。
根据上面的代码,首先将禁止I/F中断,并进入Supervisor模式,也就是OS运行的模式。图2为ARMCPU支持的CPU模式。
图2ARMCPU支持的运行模式
另外,MSR指令操作的格式如下:
图3MSR指令格式
其中最重要的是fields,目前支持:
qc:设置controlbit。对应位为16。
qx:设置extensionbit。对应位为17。
qs:设置statusbit。对应位为18。
qf:设置flagsfield。对应位为19。
图4MSR二进制格式
直接看上面的解释,还不是很清楚,因为设置的是MSR指令本身的内容,具体对应到CPSR呢,则可通过下面的伪语句得到:
图5MSR设置说明
从代码可知:
//上面代码将设置CPSR的0到第7位,刚好是控制I/F和设置CPU模式的。
先看下面这条语句:
MRC是ARM指令,用来从协处理对应的寄存器读取信息到CPU的寄存器,对应写协处理寄存器的指令是MCR。二者的语法格式(注意,是操作CP15的时候)如图6所示:
图6MRC操作CP15的格式说明
qRd:本指令用得是R9,也就是协处理的信息会保存到R9中。
qCRn:MRC中协处理器的主要寄存器。此处用得是C0。标准写法是C0,C1一直到C15。
qCRm:附属信息。如果没有附属信息,则使用C0。
qopcode2,类似附属信息。根据CRn来决定是否需要。如果不指定,则使用0。
CP15有很重要的作用,可通过操作CP15的寄存器来控制它。如图7所示:
图7CP15各个寄存器的作用
先来看此处操作的C0寄存器。
opcode2在指令中默认是0,所以将取出MainIDregister的信息。
得到的结果将怎么使用呢?来看下一句指令:
BL是ARM中的跳转指令,相当于调用函数吧。__lookup_processor_type用来得到CPU信息。注意,这个函数调用的参数是R9,R9的值是从CP15C0寄存器读取出来的,而是是MainID。下面看看此函数如何处理R9。
以上几个值都是虚地址。__proc_info_begin/end是ld在链接时候指定的信息。
图8arc/arm/kernel/vmlinx.lds.S文件
从中可以看出,__proc_info_begin/end包含了代码中定义在.proc.info.init段的内容。如图9所示。
图9proc-V7.s定义的proc.info.init的内容
为什么是proc-v7.S文件呢,因为goldfish编译的就是这个文件。从图9可以看出,其实也就是定义了一个数据结构罢了。
接着来看代码
经过上面的换算,r5,r6现在都指向__proc_info_begin/end的物理地址了。
lookup_process_type其实比较简单,这里就不再多说。但图9的内容以后还要回过头来继续介绍。那里将初始化CPUMMU相关的内容。
否则,将调用__lookup_machine_type获取机器信息。
这里涉及到另一个关键数据结构,也就是定义在.arch.info.init段中的。如图10所示:
图10.arch.info.init段
从图10可知,这个段其实对应了一个数据结构,即machine_desc.在我们的goldfish平台中,它是这么定义的:
[arch/arm/mach-goldfish/board-goldfish.c]
完整的machine_desc定义如图11所示:
图11machine_desc定义
在Goldfish中,nr为1441。详情可参考arch/arm/tools/machine-types.h。
另外,在BootLoader调用kernel之前,传递参数情况如图12所示:
图12arch/arm/boot/head.S调用kernel前传递参数
从图12可知:
qr1保存的是machinenr。
这部分代码属于BootLoader,相当复杂。以后再细说。
假设__lookup_machine_type一切正常
你可以根据上面的信息自行分析__vet_atags函数。
此函数就在head.S中定义,代码如下:
上面代码中把对应PMD_SECT_XXX的值显示出来,可知它无非是定义了一个32位的常量,某些位置的值为1,某些位置的值为0。为什么要怎么做呢?先来看ARMMMU所支持的虚实地址转换机制。
图13ARMMMU虚实地址转换
由图13可知:
q虚地址VA的[20-31]位和CP15CR2的[14-31]位共同构成FirstLevel地址。
q从FirstLevel地址将得到一个FirstLevelDescripter,也就是图13中标明memoryaccess的内容。
qFLD中不同字段表明其内容是段寻址还是页寻址。主要是根据前2位来判断。如果前2位是0b10则是段寻址。
结合图13和前面的代码:
qPMD_TYPE_SECT=2<<0,刚好就是0b10
qC|B控制Cachable和Buffable的,对应为[2,3]位
qAP对应为AccessPoint,对应为[10,11]位。
另外,Domain是ARMCPU的一个重要概念,主要和权限有关。以后碰到再说。
至此,当ldrr7xx执行完后,r7的值包含了sectionbaseaddress对应的[0-12]位的值。而sectionbaseaddress本身却还没有赋值。
接下来的代码就是为了构造一个FLD的值。根据图13,sectionbaseaddress应该是[20-31]位
现在r6存储的是段寻址的基地址,需要把这个值存储到对应表的位置,由于在表中,每一项是4个字节,所以这里需要乘以4,也就是lsl#2。
稍微解释下这里左移4的原因:
1r4存储的是表的起始地址
2r6存储的是offset
3r3存储的是往r4[offset]的值
4由于1个offset实际上是4个字节,所以真实存储的位置就是r4[4*offset]=r3
继续看代码
[head-common.S]
以后再讨论具体作用。
上面代码大多是执行ARMv7CPU的MMU相关设置的,而其中的汇编语句到比较简单。这也是ARMMMU设置的核心内容。下面我们将结合ARMCPURerference简单介绍下这些设置的内容。
请务必从ARM官方网页上下载下面两个文档:
qDDI0344D_cortex_a8_r2p1_trm.pdf:介绍CORTEXA8相关内容
qDDI0406B_arm_architecture_reference_manual_errata_markup_10_0:最新的ARM架构参考手册
1.如何看懂MMU设置并掌握理论知识
以下面这个设置为例:
mcrp15,0,r10,c8,c7,0
打开参考文档DDI0344D_cortex_a8_r2p1_trm.pdf的第112页。从这一页开始,C15协处理器的各个寄存器的配置都有详细的说明。如图14所示
图14C8寄存器的设置
上图中,左边空白区域对应的是C8。可知,c8,c7,0的组合对应的是InvalidateunifiedTLBunlockedentries.详细说明在page3-99。
如果在此文档中碰到有不理解的内容,就需要参考DDI0406B_arm_architecture_reference_manual_errata_markup_10_0。该文档会介绍一些理论知识。
篇幅原因,我就不在这里啰嗦。已经告诉大家如何钓鱼了,请大家自己尝试!
MMU启动后,我们也无需管什么物理地址还是虚拟地址,直接去看对应地址的代码即可。如果您非对这个转换过程很感兴趣,建议您把那两个参考书好好瞅瞅。
早在2010年7月的时候,我就看了那本鼎鼎大名的《ARM体系结构与编程》,这应该是第一本系统介绍ARM体系结构和编程的书。但是没看懂,全是枯燥的ARMCPU设置,纯教科书。
最近因为工作的原因,想把ARM这块重新捡起来,想起2年的痛苦,觉得应该换个思路。ARM也好,汇编也好,我们应该关注它的目的,而不是具体它是怎么实现的。即了解Whattodo比了解Howtodo更重要(仅我个人目的而言,前者重要。不过在某些追求细节的时候,后者重要。需要你自己去判断)。根据这个思路,我选择以LinuxKernel启动为分析对象,大致研究流程如下:
q先花几天时间了解下ARM汇编的大概语句。
q直接上代码分析。不过你得对Kernel启动的流程稍有了解。还好我在《深入理解Android卷I》写完后,花了点时间把这块整理了下。请参考/article/1361287.html
q碰到不懂的汇编语句,就查参考手册。这些还只是针对一些没有背景知识的语句。
q当碰到类似CP15操作的语句时,其背后往往包含了较多的CPU相关的知识,这时候就需要查阅前面提到的两本参考书籍,去真真正正了解ARMCPU运行的相关原理。
大概经过2周先痛苦挣扎,到后面豁然开朗的过程,后续的研究就非常非常流畅了。
随笔之GoldFishKernel启动过程中arm汇编分析
一分析
电子版下载本节介绍Kernel启动。此时Piggy已经将vimlinux解压,BL将执行权限传给了Kernel。
代码在arch/arm/kernel/head.S中。相关代码如下:
//将采用C/C++注释语句
01 | /* |
02 |
03 | .section是GNUASM的语法。格式如下: |
04 |
05 | .sectionname[,"flags"[,@type]]其中,name是必须的,flags是可选。 |
06 |
07 | "ax"表示:a为sectionisallocatable,x为executable。 |
08 |
09 | */ |
10 |
11 | .section ".text.head" , "ax" |
12 | //这个ENTRY(stext)有相当的含义。在kernel/vmlinux.ld.S中,也定义了一个ENTRY。在ld |
13 |
14 | //语法中,ENTRY是一个command,用来定义入口点。所以,这里就是kernel执行的入口点函数。 |
15 |
16 | ENTRY(stext) |
17 |
18 | /* |
19 |
20 | MSR:是ARM汇编指令,用来将数据copy到statusregister寄存器中。cpsr_c表示要操作 |
21 |
22 | CPSR寄存器的Control标志。 |
23 |
24 | */ |
25 |
26 |
27 |
28 | msrcpsr_c,#PSR_F_BIT|PSR_I_BIT|SVC_MODE@ensuresvcmode |
29 |
30 | @andirqsdisabled |
1.1MSR设置I/F和CPUMode
CPSR全称是CurrentProcessStatusRegister,用来表示当前CPU的状态,也可用于控制。相关控制位如图1所示:图1CPSR控制位
由图1可知:
qN/Z/C/V控制位用来表示负/零/进位/溢出,属于UserFlags,即可在UserMode下操作。A
qI/F表示Interrupt和FastInterrupt使能位。
qMode用来控制CPU当前的模式。ARMCPU一共有7种模式。
根据上面的代码,首先将禁止I/F中断,并进入Supervisor模式,也就是OS运行的模式。图2为ARMCPU支持的CPU模式。
图2ARMCPU支持的运行模式
另外,MSR指令操作的格式如下:
图3MSR指令格式
其中最重要的是fields,目前支持:
qc:设置controlbit。对应位为16。
qx:设置extensionbit。对应位为17。
qs:设置statusbit。对应位为18。
qf:设置flagsfield。对应位为19。
图4MSR二进制格式
直接看上面的解释,还不是很清楚,因为设置的是MSR指令本身的内容,具体对应到CPSR呢,则可通过下面的伪语句得到:
图5MSR设置说明
从代码可知:
1 | msrcpsr_c,#PSR_F_BIT|PSR_I_BIT|SVC_MODE |
1.2ARMCP15协处理器控制
设置好CPU模式后,下面的工作就是获取CPU的信息。在ARM中,协处理(coprocessor)15中用于管理CPU信息和MMU相关的工作。CP15也是ARM中最重要的处理器,以后会经常碰到。先看下面这条语句:
1 | mrcp15,0,r9,c0,c0@getprocessorid |
图6MRC操作CP15的格式说明
qRd:本指令用得是R9,也就是协处理的信息会保存到R9中。
qCRn:MRC中协处理器的主要寄存器。此处用得是C0。标准写法是C0,C1一直到C15。
qCRm:附属信息。如果没有附属信息,则使用C0。
qopcode2,类似附属信息。根据CRn来决定是否需要。如果不指定,则使用0。
CP15有很重要的作用,可通过操作CP15的寄存器来控制它。如图7所示:
图7CP15各个寄存器的作用
先来看此处操作的C0寄存器。
opcode2在指令中默认是0,所以将取出MainIDregister的信息。
得到的结果将怎么使用呢?来看下一句指令:
1 | bl__lookup_processor_type@r5=procinfor9=cpuid |
1.3__lookup_procesoor_type分析
该函数在head-Common.S中定义。下面逐行分析它,这里会碰到几个重要的指令及其用法。01 | __lookup_processor_type: |
02 |
03 | //adr是一条伪指令,其作用是将3f标签的地址赋给R3。这个伪指令其实是可拆分成多条指令 |
04 |
05 | //由于后面的3f是相对当前PC位置而言,所以R3实际上存储的是3f的物理地址。 |
06 |
07 | adrr3,3f //f是forward之意。标志3在此代码之后声明 |
08 |
09 | /* |
10 |
11 | ldm是loadmultipleregister的意思,它的作用是将[r3]对应的内存内容存储到r5,r6, |
12 |
13 | r7寄存器中。DA是DecreaseAfter的意思。ARM汇编在这里有4种模式,DA,IA,DB,IB等 |
14 |
15 | 此处的ldmda,将把3F所在的内容依序传递给R7,R6,R5。每传递一次,R3递减4个字节。 |
16 |
17 | */ |
18 |
19 | ldmdar3,{r5-r7} |
20 |
21 | 上面语句执行完后: |
22 |
23 | qR5=__proc_info_begin,这个值是虚地址。 |
24 |
25 | qR6=__proc_info_end。 |
26 |
27 | qR7=.。 |
图8arc/arm/kernel/vmlinx.lds.S文件
从中可以看出,__proc_info_begin/end包含了代码中定义在.proc.info.init段的内容。如图9所示。
图9proc-V7.s定义的proc.info.init的内容
为什么是proc-v7.S文件呢,因为goldfish编译的就是这个文件。从图9可以看出,其实也就是定义了一个数据结构罢了。
接着来看代码
1 | //r3指向3f的物理地址,r7指向虚拟地址,而现在只能访问物理地址,所以需要找到一个offset |
2 |
3 | subr3,r3,r7@getoffsetbetweenvirt&phys |
4 |
5 | addr5,r5,r3@convertvirtaddressesto |
6 |
7 | addr6,r6,r3@physicaladdressspace |
01 | //ldmia将[r5]的内存信息存储到r3,r4中,每完成一次传输,r5自动加4. |
02 |
03 | 1:ldmiar5,{r3,r4}@value,mask |
04 |
05 | //下面将测试R9和mask之后的值是否是我们想要的r3的值。根据图9。应该是0x000f0000。 |
06 |
07 | //在MainIDregister中,这表明[16-19]位是都是1. |
08 |
09 | andr4,r4,r9@maskwantedbits |
10 |
11 | teqr3,r4 |
12 |
13 | beq2f //如果是我们想要的数据,则跳转到2f |
14 |
15 | //否则跳过一个PROC_INFO_SIZE,继续找,一般只有一个PROC_INFO结构体。 |
16 |
17 | addr5,r5,#PROC_INFO_SZ@ sizeof (proc_info_list) |
18 |
19 | cmpr5,r6 |
20 |
21 | blo1b |
22 |
23 | //如果没找到,则设置R5寄存器为0 |
24 |
25 | movr5,#0@unknownprocessor |
26 |
27 | 2:movpc,lr //从函数返回 |
28 |
29 | ENDPROC(__lookup_processor_type) |
30 |
31 |
32 |
33 | /* |
34 |
35 | *提供一个C接口的lookup_process_type函数 |
36 |
37 | */ |
38 |
39 | ENTRY(lookup_processor_type) |
40 |
41 | stmfdsp!,{r4-r7,r9,lr} |
42 |
43 | movr9,r0 |
44 |
45 | bl__lookup_processor_type |
46 |
47 | movr0,r5 |
48 |
49 | ldmfdsp!,{r4-r7,r9,pc} |
50 |
51 | ENDPROC(lookup_processor_type) |
52 |
53 |
54 |
55 | . long __proc_info_begin |
56 |
57 | . long __proc_info_end |
58 |
59 | 3:. long . |
60 |
61 | . long __arch_info_begin |
62 |
63 | . long __arch_info_end |
1 | //如果r5为空,则表示CPU信息获取是否,调用__error_p,退出整个启动 |
2 |
3 | movsr10,r5@invalidprocessor(r5=0)? |
4 |
5 | beq__error_p@yes,error 'p' |
1.4__lookup_machine_type分析
该函数也是在head-comm.S中定义的。01 | __lookup_machine_type: |
02 |
03 | adrr3,3b //b是backward的意思。标志3在此代码之前声明。 |
04 |
05 | //r4,r5,r6分别指向label3,__arch_info_begin和__arch_info_end |
06 |
07 | ldmiar3,{r4,r5,r6} |
08 |
09 | subr3,r3,r4 |
10 |
11 | addr5,r5,r3 |
12 |
13 | addr6,r6,r3 |
14 |
15 | //以上将得到__arch_info_begin/end的物理地址 |
16 |
17 |
18 |
19 | 1:ldrr3,[r5,#MACHINFO_TYPE] |
20 |
21 | //比较r1和MACHINFO_TYPE是不是一致。注意,r1的值是BL传递给它的 |
22 |
23 | teqr3,r1@matchesloadernumber? |
24 |
25 | beq2f@found |
26 |
27 | addr5,r5,#SIZEOF_MACHINE_DESC@nextmachine_desc |
28 |
29 | cmpr5,r6 |
30 |
31 | blo1b |
32 |
33 | movr5,#0@unknownmachine |
34 |
35 | 2:movpc,lr |
36 |
37 | ENDPROC(__lookup_machine_type) |
图10.arch.info.init段
从图10可知,这个段其实对应了一个数据结构,即machine_desc.在我们的goldfish平台中,它是这么定义的:
[arch/arm/mach-goldfish/board-goldfish.c]
01 | //还需要加上: |
02 |
03 | nr=MACH_TYPE_GOLDFISH |
04 |
05 | name= "Goldfish" |
06 |
07 | MACHINE_START(GOLDFISH, "Goldfish" ) |
08 |
09 | .phys_io=IO_START, |
10 |
11 | .io_pg_offst=((IO_BASE)>>18)&0xfffc, |
12 |
13 | .boot_params=0x00000100, |
14 |
15 | .map_io=goldfish_map_io, |
16 |
17 | .init_irq=goldfish_init_irq, |
18 |
19 | .init_machine=goldfish_init, |
20 |
21 | .timer=&goldfish_timer, |
22 |
23 | MACHINE_END |
图11machine_desc定义
在Goldfish中,nr为1441。详情可参考arch/arm/tools/machine-types.h。
另外,在BootLoader调用kernel之前,传递参数情况如图12所示:
图12arch/arm/boot/head.S调用kernel前传递参数
从图12可知:
qr1保存的是machinenr。
这部分代码属于BootLoader,相当复杂。以后再细说。
假设__lookup_machine_type一切正常
1 | bl__lookup_machine_type@r5=machinfo |
2 |
3 | movsr8,r5@invalidmachine(r5=0)? |
4 |
5 | beq__error_a@yes,error 'a' |
1.5__vet_atags分析
接下来的任务就是Kernel校验BL传递的启动参数了。这部分内容和BootLoader有较大关系。01 | bl__vet_atags |
02 |
03 | 此处的核心概念就是ATAG_CORE/END之类的,由BootLoader往Kernel传递参数,主要是tag结构体 |
04 |
05 | 在arch/arm/include/asm/setup.h中。BL传递的是 struct tag的链表,该链表以ATAG_CORE开头,以ATAG_NONE结尾。 |
06 |
07 | #defineATAG_CORE0x54410001 |
08 |
09 | #defineATAG_NONE0x00000000 |
10 |
11 | struct
|
12 |
13 | __u32size; |
14 |
15 | __u32tag; |
16 |
17 | }; |
18 |
19 | struct
|
20 |
21 | struct tag_headerhdr; //首先是一个头,根据头部的tag来判断下面的union是哪个 |
22 |
23 | union { |
24 |
25 | struct tag_corecore; |
26 |
27 | struct tag_mem32mem; |
28 |
29 | struct tag_videotextvideotext; |
30 |
31 | struct tag_ramdiskramdisk; |
32 |
33 | struct tag_initrdinitrd; |
34 |
35 | struct tag_serialnrserialnr; |
36 |
37 | struct tag_revisionrevision; |
38 |
39 | struct tag_videolfbvideolfb; |
40 |
41 | struct tag_cmdlinecmdline; |
42 |
43 | struct tag_acornacorn; |
44 |
45 | struct tag_memclkmemclk; |
46 |
47 | }u; |
48 |
49 | }; |
1.6__create_page_tables分析
下面的任务就是调用__create_page_tables创建pagetable。1 | bl__create_page_tables //调用__create_page_tables函数 |
01 | __create_page_tables: |
02 |
03 | /* |
04 |
05 | pgtbl是head.S中定义的一个宏,见下面的分析 |
06 |
07 | */ |
08 |
09 | pgtblr4 |
10 |
11 | pgtbl定义了一个宏,相关代码如下: |
12 |
13 | //TEXT_OFFSET是kernel镜像在内存中的偏移量。一般定义为0X8000,即32KB处 |
14 |
15 | //PHYS_OFFSET:是内核镜像在内存中的起始物理地址。上面二者之和就是内核镜像在机器上的 |
16 |
17 | //物理地址。Goldfish平台中,PHYS_OFFSET为0。 |
18 |
19 | //PAGE_OFFSET是Kernel镜像在虚拟内存的起始地址,一般是3G处 |
20 |
21 | #defineKERNEL_RAM_VADDR(PAGE_OFFSET+TEXT_OFFSET) |
22 |
23 | #defineKERNEL_RAM_PADDR(PHYS_OFFSET+TEXT_OFFSET) |
24 |
25 | .macropgtbl,rd //此宏调用完毕后,r4的值就是0x4000,即16KB |
26 |
27 | ldr\rd,=(KERNEL_RAM_PADDR-0x4000) |
28 |
29 | .endm |
30 |
31 | 接着看代码。 |
32 |
33 | movr0,r4 |
34 |
35 | movr3,#0 |
36 |
37 | addr6,r0,#0x4000 |
38 |
39 | //STR将寄存器的值往内存中传送。r3为0,故内存的值被设置为0.每调用一次str,r0递增4 |
40 |
41 | //r0是baseaddress,其值可自动增减。由armaddressmode格式控制 |
42 |
43 | 1:strr3,[r0],#4 |
44 |
45 | strr3,[r0],#4 |
46 |
47 | strr3,[r0],#4 |
48 |
49 | strr3,[r0],#4 |
50 |
51 | teqr0,r6 |
52 |
53 | bne1b //此循环调用完毕后,0x4000-0x8000的内存都被设置为0。此时r0=32KB |
54 |
55 |
56 |
57 | //r10存储的是图9中proc_info的第三个long,也就是mmuflas,用于设置MMU参数 |
58 |
59 | ldrr7,[r10,#PROCINFO_MM_MMUFLAGS]@mm_mmuflags |
1.6.1ARMMMU设置介绍
虽然上面最后一条语句是一个简单的ldr,但背后的内容却相当丰富,不把它搞清楚,后面的内容将解释不清。来看proc-V7中mm_mmuflags对应的值是什么1 | . long PMD_TYPE_SECT|\ //#definePMD_TYPE_SECT(2<<0) |
2 |
3 | PMD_SECT_BUFFERABLE|\ //#definePMD_SECT_BUFFERABLE(1<<2) |
4 |
5 | PMD_SECT_CACHEABLE|\ //#definePMD_SECT_CACHEABLE(1<<3) |
6 |
7 | PMD_SECT_AP_WRITE|\ //#definePMD_SECT_AP_WRITE(1<<10) |
8 |
9 | PMD_SECT_AP_READ //#definePMD_SECT_AP_READ(1<<11) |
图13ARMMMU虚实地址转换
由图13可知:
q虚地址VA的[20-31]位和CP15CR2的[14-31]位共同构成FirstLevel地址。
q从FirstLevel地址将得到一个FirstLevelDescripter,也就是图13中标明memoryaccess的内容。
qFLD中不同字段表明其内容是段寻址还是页寻址。主要是根据前2位来判断。如果前2位是0b10则是段寻址。
结合图13和前面的代码:
qPMD_TYPE_SECT=2<<0,刚好就是0b10
qC|B控制Cachable和Buffable的,对应为[2,3]位
qAP对应为AccessPoint,对应为[10,11]位。
另外,Domain是ARMCPU的一个重要概念,主要和权限有关。以后碰到再说。
至此,当ldrr7xx执行完后,r7的值包含了sectionbaseaddress对应的[0-12]位的值。而sectionbaseaddress本身却还没有赋值。
接下来的代码就是为了构造一个FLD的值。根据图13,sectionbaseaddress应该是[20-31]位
1 | //r6的值为当前PC值右移20位 |
2 |
3 | movr6,pc,lsr#20 |
4 |
5 | orrr3,r7,r6,lsl#20@flags+kernelbase |
6 |
7 | //此时,r3的值就是一个基于段寻址的FLD。把它存起来。位置是r4+r6<<2 |
8 |
9 | strr3,[r4,r6,lsl#2] |
稍微解释下这里左移4的原因:
1r4存储的是表的起始地址
2r6存储的是offset
3r3存储的是往r4[offset]的值
4由于1个offset实际上是4个字节,所以真实存储的位置就是r4[4*offset]=r3
1.6.2设置页表
当理解上面代码后,下面就是把kernel虚拟地址的位置存储到r4表中了继续看代码
01 | //立即数的计算比较难理解,网上也没有相关说法。不过,只要知道下面这段代码就是存储kernel |
02 |
03 | //虚拟地址到对应页表位置即可 |
04 |
05 | addr0,r4,#(KERNEL_START&0xff000000)>>18 |
06 |
07 | strr3,[r0,#(KERNEL_START&0x00f00000)>>18]! |
08 |
09 | ldrr6,=(KERNEL_END-1) |
10 |
11 | addr0,r0,#4 //r0=ro+4 |
12 |
13 | addr6,r4,r6,lsr#18 //r6=r4+r6>>18 |
14 |
15 | 1:cmpr0,r6 |
16 |
17 | addr3,r3,#1<<20 //r3+=1<<20,每次递增1M |
18 |
19 | //ls是conditioncode,表示小于等于,即只要r0<=r6,strls就会执行 |
20 |
21 | strlsr3,[r0],#4 |
22 |
23 | bls1b |
24 |
25 |
26 |
27 | //map物理地址前1M到对应位置 |
28 |
29 | addr0,r4,#PAGE_OFFSET>>18 |
30 |
31 | orrr6,r7,#(PHYS_OFFSET&0xff000000) |
32 |
33 | . if (PHYS_OFFSET&0x00f00000) |
34 |
35 | orrr6,r6,#(PHYS_OFFSET&0x00f00000) |
36 |
37 | .endif |
38 |
39 | strr6,[r0] |
40 |
41 |
42 |
43 | movpc,lr |
44 |
45 | ENDPROC(__create_page_tables) |
46 |
47 | .ltorg |
1.6.3总结
建议大家仔细体会create_page_tables这段内容。虽然以后不太可能会使用它们,但把这段代码搞清楚还是一个比较有意思的过程。1.7剩余工作
回到head.S,最后还剩下几句代码:01 | //将__switch_data的位置存储到r13 |
02 |
03 | ldrr13,__switch_data |
04 |
05 | //获取__enable_mmu标签的地址,并保存到lr中 |
06 |
07 | adrlr,__enable_mmu |
08 |
09 | //r10存储的是__v7_proc_info的地址,#PROCINFO_INITFUNC是一个偏移量 |
10 |
11 | //执行完下条语句后,pc指向__v7_proc_info的b__v7_setup,故下面这条语句就是 |
12 |
13 | //执行__v7_setup函数 |
14 |
15 | addpc,r10,#PROCINFO_INITFUNC |
16 |
17 | ENDPROC(stext) |
1.7.1__switch_data说明
__switch_data标签如下,主要存储了一些数据。[head-common.S]
01 | .type__switch_data,%object |
02 |
03 | __switch_data: |
04 |
05 | . long __mmap_switched |
06 |
07 | . long __data_loc@r4 |
08 |
09 | . long _data@r5 |
10 |
11 | . long __bss_start@r6 |
12 |
13 | . long _end@r7 |
14 |
15 | . long processor_id@r4 |
16 |
17 | . long __machine_arch_type@r5 |
18 |
19 | . long __atags_pointer@r6 |
20 |
21 | . long cr_alignment@r7 |
22 |
23 | . long init_thread_union+THREAD_START_SP@sp |
1.7.2__v7_setup
先来看01 | addpc,r10,#PROCINFO_INITFUNC |
02 |
03 | 实际上就是执行__v7_setup函数。代码在mm/proc-v7.S中。 |
04 |
05 | adrr12,__v7_setup_stack@thelocalstack |
06 |
07 | stmiar12,{r0-r5,r7,r9,r11,lr} |
08 |
09 | blv7_flush_dcache_all |
10 |
11 | ldmiar12,{r0-r5,r7,r9,r11,lr} |
12 |
13 | movr10,#0 |
14 |
15 | dsb |
16 |
17 | #ifdefCONFIG_MMU//goldfish定义了这个配置项 |
18 |
19 | mcrp15,0,r10,c8,c7,0@invalidateI+DTLBs |
20 |
21 | mcrp15,0,r10,c2,c0,2@TTBcontrol register |
22 |
23 | orrr4,r4,#TTB_FLAGS |
24 |
25 | mcrp15,0,r4,c2,c0,1@loadTTB1 |
26 |
27 | movr10,#0x1f@domains0,1=manager |
28 |
29 | mcrp15,0,r10,c3,c0,0@loaddomainaccess register |
30 |
31 | #endif |
32 |
33 | ldrr5,=0xff0aa1a8 |
34 |
35 | ldrr6,=0x40e040e0 |
36 |
37 | mcrp15,0,r5,c10,c2,0@writePRRR |
38 |
39 | mcrp15,0,r6,c10,c2,1@writeNMRR |
40 |
41 | adrr5,v7_crval |
42 |
43 | ldmiar5,{r5,r6} |
44 |
45 | mrcp15,0,r0,c1,c0,0@readcontrol register |
46 |
47 | bicr0,r0,r5@clearbitsthem |
48 |
49 | orrr0,r0,r6@setthem |
50 |
51 | //最后一句,将lr赋值给pc。执行完后,将跳到__enable_mmu函数。 |
52 |
53 | movpc,lr@ return tohead.S:__ret |
54 |
55 | ENDPROC(__v7_setup) |
请务必从ARM官方网页上下载下面两个文档:
qDDI0344D_cortex_a8_r2p1_trm.pdf:介绍CORTEXA8相关内容
qDDI0406B_arm_architecture_reference_manual_errata_markup_10_0:最新的ARM架构参考手册
1.如何看懂MMU设置并掌握理论知识
以下面这个设置为例:
mcrp15,0,r10,c8,c7,0
打开参考文档DDI0344D_cortex_a8_r2p1_trm.pdf的第112页。从这一页开始,C15协处理器的各个寄存器的配置都有详细的说明。如图14所示
图14C8寄存器的设置
上图中,左边空白区域对应的是C8。可知,c8,c7,0的组合对应的是InvalidateunifiedTLBunlockedentries.详细说明在page3-99。
如果在此文档中碰到有不理解的内容,就需要参考DDI0406B_arm_architecture_reference_manual_errata_markup_10_0。该文档会介绍一些理论知识。
篇幅原因,我就不在这里啰嗦。已经告诉大家如何钓鱼了,请大家自己尝试!
1.7.3__enable_mmu介绍
__v7_setup最后已经的movpc,lr将使得CPU跳转到__enable_mmu处,其代码如下所示:01 | __enable_mmu: |
02 |
03 | #ifdefCONFIG_ALIGNMENT_TRAP |
04 |
05 | orrr0,r0,#CR_A |
06 |
07 | #else |
08 |
09 | bicr0,r0,#CR_A |
10 |
11 | #endif |
12 |
13 | #ifdefCONFIG_CPU_DCACHE_DISABLE |
14 |
15 | bicr0,r0,#CR_C |
16 |
17 | #endif |
18 |
19 | #ifdefCONFIG_CPU_BPREDICT_DISABLE |
20 |
21 | bicr0,r0,#CR_Z |
22 |
23 | #endif |
24 |
25 | #ifdefCONFIG_CPU_ICACHE_DISABLE |
26 |
27 | bicr0,r0,#CR_I |
28 |
29 | #endif |
30 |
31 | //设置domain的权限,请参考前面的书籍了解DOMAIN在ARMMMU中的意义 |
32 |
33 | movr5,#(domain_val(DOMAIN_USER,DOMAIN_MANAGER)|\ |
34 |
35 | domain_val(DOMAIN_KERNEL,DOMAIN_MANAGER)|\ |
36 |
37 | domain_val(DOMAIN_TABLE,DOMAIN_MANAGER)|\ |
38 |
39 | domain_val(DOMAIN_IO,DOMAIN_CLIENT)) |
40 |
41 | //请参考前面的方法,了解下面这两条语句的实际作用 |
42 |
43 | mcrp15,0,r5,c3,c0,0@loaddomainaccess register |
44 |
45 | mcrp15,0,r4,c2,c0,0@loadpagetablepointer |
46 |
47 | b__turn_mmu_on //跳转到__turn_mmu_on |
48 |
49 | ENDPROC(__enable_mmu) |
50 |
51 | 简单看看__turn_mmu_on: |
52 |
53 | __turn_mmu_on: |
54 |
55 | movr0,r0 //类似nop的空指令,浪费一点CPU时间,怕引起racecondition发生 |
56 |
57 | //c1,c0这两个控制MMU的设置 |
58 |
59 | mcrp15,0,r0,c1,c0,0@writecontrolreg |
60 |
61 | mrcp15,0,r3,c0,c0,0@readidreg |
62 |
63 | movr3,r3 |
64 |
65 | movr3,r3 |
66 |
67 | //此时,MMU就正式启动了 |
68 |
69 | movpc,r13 //r13指向__switch_data |
70 |
71 | ENDPROC(__turn_mmu_on) |
1.7.4__mmaped_switched介绍
__switch_data第一个定义的就是__mmaped_switched,PC将执行这里的指令:01 | __mmap_switched: |
02 |
03 | adrr3,__switch_data+4 |
04 |
05 |
06 |
07 | ldmiar3!,{r4,r5,r6,r7} |
08 |
09 | cmpr4,r5@Copydatasegment if needed |
10 |
11 | 1:cmpner5,r6 |
12 |
13 | ldrnefp,[r4],#4 |
14 |
15 | strnefp,[r5],#4 |
16 |
17 | bne1b |
18 |
19 |
20 |
21 | movfp,#0@ClearBSS(andzerofp) |
22 |
23 | 1:cmpr6,r7 |
24 |
25 | strccfp,[r6],#4 |
26 |
27 | bcc1b |
28 |
29 |
30 |
31 | ldmiar3,{r4,r5,r6,r7,sp} |
32 |
33 | strr9,[r4]@SaveprocessorID |
34 |
35 | strr1,[r5]@Savemachinetype |
36 |
37 | strr2,[r6]@Saveatagspointer |
38 |
39 | bicr4,r0,#CR_A@Clear 'A' bit |
40 |
41 | stmiar7,{r0,r4}@Savecontrol register values |
1 | //上面我就懒得废话了,下面这句代码相信各位都很了解。执行start_kernel函数。 |
1 | <b>bstart_kernel</b> |
1 | ENDPROC(__mmap_switched) |
二总结
我觉得需要说明下为什么写这篇文章:早在2010年7月的时候,我就看了那本鼎鼎大名的《ARM体系结构与编程》,这应该是第一本系统介绍ARM体系结构和编程的书。但是没看懂,全是枯燥的ARMCPU设置,纯教科书。
最近因为工作的原因,想把ARM这块重新捡起来,想起2年的痛苦,觉得应该换个思路。ARM也好,汇编也好,我们应该关注它的目的,而不是具体它是怎么实现的。即了解Whattodo比了解Howtodo更重要(仅我个人目的而言,前者重要。不过在某些追求细节的时候,后者重要。需要你自己去判断)。根据这个思路,我选择以LinuxKernel启动为分析对象,大致研究流程如下:
q先花几天时间了解下ARM汇编的大概语句。
q直接上代码分析。不过你得对Kernel启动的流程稍有了解。还好我在《深入理解Android卷I》写完后,花了点时间把这块整理了下。请参考
q碰到不懂的汇编语句,就查参考手册。这些还只是针对一些没有背景知识的语句。
q当碰到类似CP15操作的语句时,其背后往往包含了较多的CPU相关的知识,这时候就需要查阅前面提到的两本参考书籍,去真真正正了解ARMCPU运行的相关原理。
大概经过2周先痛苦挣扎,到后面豁然开朗的过程,后续的研究就非常非常流畅了。
相关文章推荐
- 随笔之GoldFish Kernel启动过程中arm汇编分析
- 随笔之GoldFish Kernel启动过程中arm汇编分析
- 嵌入式ARM Linux kernel启动过程之浅尝辄止分析start_kernel函数
- ARM Linux启动流程分析——start_kernel前启动阶段(汇编部分)
- 嵌入式 arm平台kernel启动第一阶段汇编head.s分析
- GNU ARM汇编--(十九)u-boot-nand-spl启动过程分析
- Arm linxu启动过程分析(三)
- 详解 ARM Linux启动过程分析
- arm-linux内核start_kernel之前启动分析(1)-接过bootloader的衣钵
- Linux内核---13.启动分析1之arch/arm/kernel/head.S
- Arm linxu启动过程分析(二)
- arm-linux内核start_kernel之前启动分析(2)- 页表的准备
- 详解 ARM Linux启动过程分析
- kernel 启动过程之一, uimge, zimage,arch/arm/boot/compressed/head.S
- Linux内核---14.启动分析2之arch/arm/kernel/head.S
- ARM多核处理器启动过程分析【转】
- ARM S3C2416 启动过程分析 详解 (转)
- Arm linxu启动过程分析(三)
- Arm linxu启动过程分析(四)
- arm-linux内核start_kernel之前启动分析(3)-开启MMU,走进新时代