Linux内核历史悠久,特性丰富,但是代码量庞大,各个子系统交叉繁琐。对于想要将操作系统内核各个特性研究一遍的人,有时候也只好"望Linux兴叹"。Xvisor是一个较新的Type-1 Hypervisor,其官方地址在:http://xhypervisor.org/。Xvisor代码逻辑清晰,编码规范,虽然其也借鉴了Linux内核的一些特性,但它有很多地方都是完全重构,因为其目的是要实现一个Hypervisor,而不是一个通用的操作系统(尽管很多操作系统内核的基本元素都是必要的)。从这一个角度看,Xvisor可以是系统软件开发者的一个很好的研究项目,因为他不仅包含了操作系统内核的课题,还包含了虚拟化这一现代系统软件开发者的必修课。最主要的是,Xvisor还很新,许多地方都需要进一步丰富,这给了想要上手玩一玩的系统软件开发者许多Contribute的机会。最近笔者就业余把Xvisor拿来玩,做一些移植和分析的事情,期间会做一些记录,准备分享在本博客上。本文就从其启动部分开始分析。由于仅仅是个人业余玩玩,不保证内容的完全准确和清晰。
根目录下有一个主Makefile。其中我们可以看到建立了许多规则。下面的规则建立起来的执行顺序要求先调用compile_cpp命令生成build_dir下面的linker.ld文件。
312$(build_dir)/vmm.bin: $(build_dir)/vmm.elf
313 $(call compile_objcopy,$@,$<)
314
315$(build_dir)/vmm.elf: $(build_dir)/linker.ld $(all-y) $(build_dir)/system.o
316 $(call compile_ld,$@,$(build_dir)/linker.ld,$(all-y) $(build_dir)/system.o)
317
318$(build_dir)/system.map: $(build_dir)/vmm_tmp.elf
319 $(call compile_nm,$@,$<)
320
321$(build_dir)/vmm_tmp.elf: $(build_dir)/linker.ld $(all-y) $(build_dir)/system_tmp1.o
322 $(call compile_ld,$@,$(build_dir)/linker.ld,$(all-y) $(build_dir)/system_tmp1.o)
323
324$(build_dir)/system_tmp1.map: $(build_dir)/vmm_tmp1.elf
325 $(call compile_nm,$@,$<)
326
327$(build_dir)/vmm_tmp1.elf: $(build_dir)/linker.ld $(all-y)
328 $(call compile_ld,$@,$(build_dir)/linker.ld,$(all-y))
329
330$(build_dir)/linker.ld: $(cpu_dir)/linker.ld
331 $(call compile_cpp,$@,$<)
而这里的compile_cpp命令如下:
compile_cpp = $(V)mkdir -p `dirname $(1)`; \
echo " (cpp) $(subst $(build_dir)/,,$(1))"; \
$(cpp) $(cppflags) $(2) | grep -v "\#" > $(1)
对于ATM32而言,这个规则实际是要从【/XVisor/arch/arm/cpu/arm32/linker.ld】这个文件生成build_dir下面的linker.ld。在用cpp进行预处理的过程中,cppflags会包含cpu-cppflags:
cppflags+=$(cpu-cppflags)
cppflags+=$(board-cppflags)
cppflags+=$(libs-cppflags-y)
这样,在【/XVisor/arch/arm/cpu/arm32/objects.mk】中的cpu-cppflags就会被包含进来,如下:
41cpu-cppflags+=-DCPU_TEXT_START=0xFF000000
因此【/XVisor/arch/arm/cpu/arm32/linker.ld】的下面一段:
24OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
25OUTPUT_ARCH("arm")
26ENTRY(_start_vect)
27
28SECTIONS
29{
30 . = CPU_TEXT_START;
31
32 . = ALIGN(0x100000); /* Need this to create proper sections */
33
34 PROVIDE(_code_start = .);
35
36 .text :
37 {
38 PROVIDE(_text_start = .);
39 *(.entry)
40 *(.text)
41 . = ALIGN(4);
42 PROVIDE(_text_end = .);
43 }
44
45 .data :
46 {
47 PROVIDE(_data_start = .);
48 *(.data)
49 . = ALIGN(4);
50 PROVIDE(_data_end = .);
51 }
52
53 .rodata :
54 {
55 PROVIDE(_rodata_start = .);
56 *(.rodata .rodata.*)
57 . = ALIGN(4);
58 PROVIDE(_rodata_end = .);
59 }
60
61 .percpu :
62 {
63 PROVIDE(_percpu_start = .);
64 *(.percpu)
65 . = ALIGN(4);
66 PROVIDE(_percpu_end = .);
67 }
就会被定为在0xFF000000这个地方。这里正好可以稍微先分析链接脚本。显然,这个连接器脚本直接使用ENTRY(_start_vect)将_start_vect这个标号作为最终映像的入口地址。这个_start_vect定为于【/XVisor/arch/arm/cpu/arm32/cpu_entry.S】这个文件。
431 /*
432 * Exception vector start.
433 */
434 .section .entry, "ax", %progbits
435 .globl _start_vect
436_start_vect:
437 ldr pc, __reset
438 ldr pc, __undefined_instruction
439 ldr pc, __software_interrupt
440 ldr pc, __prefetch_abort
441 ldr pc, __data_abort
442 ldr pc, __not_used
443 ldr pc, __irq
444 ldr pc, __fiq
445__reset:
446 .word _reset
447__undefined_instruction:
448 .word _undef_inst
449__software_interrupt:
450 .word _soft_irq
451__prefetch_abort:
452 .word _prefetch_abort
453__data_abort:
454 .word _data_abort
455__not_used:
456 .word _not_used
457__irq:
458 .word _irq
459__fiq:
460 .word _fiq
461 .global _end_vect
462_end_vect:
463 b .
464
465 /*
466 * Exception stacks.
467 */
468__svc_stack_end:
469 .word _svc_stack_end
470__und_stack_end:
471 .word _und_stack_end
472__abt_stack_end:
473 .word _abt_stack_end
474__irq_stack_end:
475 .word _irq_stack_end
476__fiq_stack_end:
477 .word _fiq_stack_end
478
479 /*
480 * Reset exception handler.
481 * Reset hardware state before starting Xvisor.
482 */
483 .globl _reset
484_reset:
因此,我们可能会认为,当从bootloader跳转到映像执行时会先执行这里的_reset这个标号处的代码。然而,实际上并不是这样的。因为【/XVisor/arch/arm/cpu/arm32/cpu_entry.S】文件本身作为一个编译单元,会生成cpu_entry.o,该对象文件会被整体放置到一起,而_reset这个标号处于这个cpu_entry.S文件中,因此cpu_entry.o会被放置在.text段中的开始处。
SECTIONS
{
. = 0xFF000000;
. = ALIGN(0x100000);
PROVIDE(_code_start = .);
.text :
{
PROVIDE(_text_start = .);
*(.entry)
*(.text)
. = ALIGN(4);
PROVIDE(_text_end = .);
}
这里的*(.entry)也指定名为.entry的代码段应该在.text段开始处。也就是说,这个名为.entry的代码段才是第一个要执行的代码(这也符合了我们的预期)。
38 .section .entry, "ax", %progbits
39 .globl _start
40 .globl _start_secondary
41_start:
在Makefile规定的编译顺序中,第一步会编译并链接vmm_tmp1.elf这个目标:
$(build_dir)/vmm_tmp1.elf: $(build_dir)/linker.ld $(all-y)
$(call compile_ld,$@,$(build_dir)/linker.ld,$(all-y))
第二步会对vmm_tmp1.elf进行进一步处理,提取出来这个映像中的所有符号:
$(build_dir)/system_tmp1.map: $(build_dir)/vmm_tmp1.elf
$(call compile_nm,$@,$<)
这里的compile_nm命令如下:
compile_nm = $(V)mkdir -p `dirname $(1)`; \
echo " (nm) $(subst $(build_dir)/,,$(1))"; \
$(nm) -n $(2) | grep -v '\( [aNUw] \)\|\(__crc_\)\|\( \$[adt]\)' > $(1)
这样,vmm_tmp1.elf中的所有符号被使用nm -n vmm_tmp1.elf提取出来,进一步使用grep处理,提取出全局的符号(非static的符号,包括全局函数和全局变量),生成system_tmp1.map。注意到在【/XVisor/tools/rules.mk】中有如下规则:
68$(build_dir)/%.S: $(build_dir)/%.map $(build_dir)/tools/kallsyms/kallsyms
69 $(V)mkdir -p `dirname $@`
70 $(if $(V), @echo " (kallsyms) $(subst $(build_dir)/,,$@)")
71 $(V)$(build_dir)/tools/kallsyms/kallsyms --all-symbols < $< > $@
因此,为了满足下面的编译目标,这个system_tmp1.map就会被上面这条规则处理,也就是使用kallsyms工具将system_tmp1.map转换成一个system_tmp1.S文件。
$(build_dir)/vmm_tmp.elf: $(build_dir)/linker.ld $(all-y) $(build_dir)/system_tmp1.o
$(call compile_ld,$@,$(build_dir)/linker.ld,$(all-y) $(build_dir)/system_tmp1.o)
这个规则将再次进行链接,这次链接加入了一个所有全局符号信息组成的system_tmp1.o,从而可以对这些符号按照名字和地址进行处理。在vmm_tmp.elf中,会增加下面几个全局变量:
kallsyms工具对这些符号和地址进行了压缩,以减小映像尺寸。更进一步,为了将这些符号也包括进入kallsyms库的分析中,下面的规则对vmm_tmp.elf进行了再次类似的处理:
$(build_dir)/vmm.elf: $(build_dir)/linker.ld $(all-y) $(build_dir)/system.o
$(call compile_ld,$@,$(build_dir)/linker.ld,$(all-y) $(build_dir)/system.o)
$(build_dir)/system.map: $(build_dir)/vmm_tmp.elf
$(call compile_nm,$@,$<)
这样,vmm.elf中的符号就包含了上面这些新加入的符号的地址和名称,从而在【/XVisor/libs/include/libs/kallsyms.h】中的对这些符号的弱引用就是实际的引用了。
32/*
33 * Tell the compiler that the count isn't in the small data section if the arch
34 * has one (eg: FRV).
35 */
36extern const unsigned long kallsyms_num_syms
37 __attribute__ ((weak, section(".rodata")));
38extern const unsigned long kallsyms_addresses[] __attribute__ ((weak));
39extern const unsigned char kallsyms_names[] __attribute__ ((weak));
40extern const unsigned char kallsyms_token_table[] __attribute__ ((weak));
41extern const unsigned short kallsyms_token_index[] __attribute__ ((weak));
42extern const unsigned long kallsyms_markers[] __attribute__ ((weak));
下面接着分析【/XVisor/arch/arm/cpu/arm32/cpu_entry.S】这个文件。
31 * _start: Primary CPU startup code
32 * _start_secondary: Secondary CPU startup code
34 * Note: Xvisor could be loaded any where in memory by boot loaders.
35 * The _start ensures that Xvisor executes from intended base address
36 * provided at compile time.
38 .section .entry, "ax", %progbits
46 * r10 -> core# in case of SMP
正如注释所言,Xvisor可以被加载到任何内存位置。这里标号_start会被定位于在链接器脚本中指定的起始地址,并且由PROVIDE(_code_start = .);指定。
28SECTIONS
29{
30 . = CPU_TEXT_START;
31
32 . = ALIGN(0x100000); /* Need this to create proper sections */
33
34 PROVIDE(_code_start = .);
这里为了防止CPU_TEXT_START被指定为一个没有对齐到1MB的地址,还专门用ALIGN(0x100000)对映像起始位置进行了对齐。尽管如此,映像也可能被bootloader随意加载到某处。因此,在代码一开始,就用一条"add r4, pc, #-0x8"指令把实际的加载地址(bootloader加载映像的地址)存储于r4中,这条指令相当于"ADR R4, _code_start"。这里的__exec_start实际上是存放_code_start这个值的地方,这个_code_start符号是连接器脚本提供的符号,用于表示在链接过程中最后对齐之后的实际代码其实地址(因此可能跟编译时指定的CPU_TEXT_START不一样,如果CPU_TEXT_START没有1MB对齐的话)。
310__exec_start:
311 .word _code_start
312__exec_end:
313 .word _code_end
如果代码真的被加载到了一个没有对齐的地方,启动代码也会尝试将代码自动转移到一个对齐的地方。
155align_1m_boundary:
156 /* Relocate code if load start is not 1MB aligned */
157 mov r0, r4
158 mov r0, r0, lsr #20
159 mov r0, r0, lsl #20
160 cmp r0, r4
161 /* Skip relocation if already aligned */
162 beq _start_mmu_init
163
164 /* Relocate copy function at end after load end address */
165 ldr r0, __copy_start
166 ldr r1, __copy_end
167 sub r2, r1, r0 /* r2 -> __copy size */
168 sub r0, r0, r6
169 add r0, r0, r4 /* r0 -> load_address of copy_start */
170 mov r1, r5 /* r1 -> load end */
171 bl _copy /* copy the _copy function after the code */
172
173 /* Use newly relocated copy function to relocate entire code
174 * to 1MB boundary
175 */
176 mov r0, r4 /* r0 -> load start */
177 mov r1, r4
178 mov r1, r1, lsr #20
179 mov r1, r1, lsl #20 /* r1 -> load start aligned to 1MB boundary */
180 sub r2, r5, r4 /* r2 -> code size */
181 bl _start_nextpc1
182_start_nextpc1:
183 add lr, lr, #16 /* Adjust return address (lr) for jump to */
184 sub lr, lr, r4 /* relocated address on return from _copy */
185 add lr, lr, r1
186 bx r5 /* call _copy */
187 /* Update load start and load end */
188 mov r0, r4
189 mov r0, r0, lsr #20
190 mov r0, r0, lsl #20 /* r0 -> load_start aligned to 1MB boundary */
191 subs r1, r4, r0 /* r1 -> offset between load start and aligned address */
192 subs r4, r4, r1 /* r4 -> new load start */
193 subs r5, r5, r1 /* r5 -> new load end */
194 ldr r0, __load_start
195 sub r0, r0, r6
196 add r0, r0, r4
197 str r4, [r0]
198 ldr r0, __load_end
199 sub r0, r0, r6
200 add r0, r0, r4
201 str r5, [r0]
这段代码无须更多解释。
如果对齐了的话,则会接着执行MMU初始化。
203_start_mmu_init:
204 ldr r0, __tmpl1_mem
205 sub r0, r0, r6
206 add r0, r0, r4
207 ldr r1, __tmpl1_mem_addr
208 sub r1, r1, r6
209 add r1, r1, r4
210 str r0, [r1]
211 /* r0 -> default Level1 Table base address */
212 mov r1, #1
213 lsl r1, #14 /* r1 -> 16K */
214 mov r2, #0
215 mov r3, r0
记住在cpu_entry.S文件开始的注释说了的如下寄存器在这段代码中的作用:
这里,__tmpl1_mem是存放tmpl1_mem变量值的地方:
326__tmpl1_mem_addr:
327 .word __tmpl1_mem
328__tmpl1_mem:
329 .word tmpl1_mem
而tmpl1_mem是在文件【/XVisor/arch/arm/cpu/arm32/cpu_mmu.c】中的一个数组变量。
52u8 __attribute__((aligned(TTBL_L1TBL_SIZE))) tmpl1_mem[TTBL_L1TBL_SIZE];
53u8 __attribute__((aligned(TTBL_L1TBL_SIZE))) defl1_mem[TTBL_L1TBL_SIZE];
为了方便理解,我们用一次实际的编译结果来说明。在一次编译之后,通过分析前面说过的system.map文件,得知:
因此,前面start_mmu_init的开始处,实际上是从__tmpl1_mem地址处获得tmpl1_mem变量的地址,将tmpl1_mem变量的地址加载到r0,并且减去r6再加上r4,实际上是为了得到这个tmpl1_mem变量加载后的实际内存地址存于r0。而__tmpl1_mem_addr实际上是保存了__tmpl1_mem变量加载后的实际地址存于r1。接着的"str r0, [r1]"实际上是将tmpl1_mem变量的地址再次存于__tmpl1_mem变量加载后的实际地址处。接着执行下面的代码:
217 /* Clear default Level1 Table */
218_start_mmu_tmpl1_clear:
219 str r2, [r3]
220 add r3, r3, #4
221 sub r1, r1, #4
222 cmp r1, #0
223 bgt _start_mmu_tmpl1_clear
这里的意思很明白,就是要清零tmpl1_mem变量(数组),大小为(1<<14),即为16KB。接着:
224
225 /* Create entries in default Level1 Table
226 * for execution & load addresses
227 */
228 ldr r3, __mmu_section_attr
229 /* r1 -> load entry start address */
230 mov r1, r4 /* r4 -> load start address */
231 lsr r1, #18 /* r1 >> 20, r1 << 2 */
232 add r1, r0, r1
233 /* Setup load entry */
234 orr r2, r3, r4
235 str r2, [r1]
236 /* r1 -> execute entry start address */
237 mov r1, r6
238 lsr r1, #18 /* r1 >> 20, r1 << 2 */
239 add r1, r0, r1
240 /* r2 -> execute entry end address */
241 sub r2, r7, r6
242 lsr r2, #18 /* r2 >> 20, r2 << 2 */
243 add r2, r1, r2
244 /* r5 -> temporary value */
245 mov r5, #0
246_start_mmu_tmpl1_set:
247 orr r5, r3, r4
248 str r5, [r1]
249 lsr r4, #20
250 add r4, r4, #1
251 lsl r4, #20
252 add r1, r1, #4
253 cmp r1, r2
254 blt _start_mmu_tmpl1_set
我们看到:
300__mmu_section_attr:
301 .word SECTION_ATTR
280#if (__ARM_ARCH_VERSION__ < 6)
281#define TTBL_L1TBL_TTE_ARCH_ATTR TTBL_L1TBL_TTE_REQ_MASK
282#else
283#define TTBL_L1TBL_TTE_ARCH_ATTR (TTBL_L1TBL_TTE_C_MASK | \
284 TTBL_L1TBL_TTE_B_MASK)
285#endif
286
287#define SECTION_ATTR ((TTBL_AP_SRW_U << TTBL_L1TBL_TTE_AP_SHIFT) | \
288 (TTBL_L1TBL_TTE_DOM_RESERVED << TTBL_L1TBL_TTE_DOM_SHIFT) | \
289 TTBL_L1TBL_TTE_ARCH_ATTR | \
290 TTBL_L1TBL_TTE_TYPE_SECTION)
接着,执行下面的代码来将Translation Table的基地址设置到TTBR0中。
256 /*
257 * Secondary CPU startup code
258 *
259 * Note: From this point primary CPU startup is same as secondary CPU
260 */
261_start_secondary:
262 /* Setup Translation Table Base Register 0 */
263 ldr r0, __tmpl1_mem
264 mcr p15, 0, r0, c2, c0, 0
再接着,更新Domain Access Control Register,将TTBL_L1TBL_TTE_DOM_RESERVED这个0号domain的访问权限设置为Client模式,即要求按照Translation Table的entry指定的属性字段来判断是否允许访问。
265
266 /* Setup Domain Control Register */
267 ldr r1, __dacr_mmu_val
268 mcr p15, 0, r1, c3, c0, 0
302__dacr_mmu_val:
303 .word (TTBL_DOM_CLIENT << (2 * TTBL_L1TBL_TTE_DOM_RESERVED))
在【/XVisor/arch/arm/cpu/arm32/include/cpu_defines.h】定义了TTBL_L1TBL_TTE_DOM_RESERVED:
282#define TTBL_L1TBL_TTE_DOM_RESERVED 0x0
283#define TTBL_L1TBL_TTE_DOM_VCPU_SUPER 0x1
284#define TTBL_L1TBL_TTE_DOM_VCPU_SUPER_RW_USER_R 0x2
285#define TTBL_L1TBL_TTE_DOM_VCPU_USER 0x3
在初始化页表并将其基地址写入TTBR0之后,需要进一步初始化系统控制寄存器从而使能MMU。
270 /* Setup System Control Register */
271 bl proc_setup
272 mcr p15, 0, r0, c1, c0, 0
这里调用了一个函数proc_setup,该函数实现在【/XVisor/arch/arm/cpu/arm32/cpu_proc_v7.S】中。
95/*
96 * Boot-time processor setup
97 *
98 * Initialise TLB, Caches, and MMU state ready to switch the MMU
99 * on. Return in r0 the new CP15 C1 control register setting.
100 *
101 * This should be able to cover all ARMv7 cores.
102 *
103 * It is assumed that:
104 * - cache type register is implemented
105 *
106 * Note: We blindly use all registers because this will be
107 * called at boot-time when there is not stack
108 */
109 .globl proc_setup
110proc_setup:
从函数的注释看,这个函数式要做一些跟MMU和Cache相关的系统级初始化,并且返回一个值用来在调用函数proc_setup后初始化System Control Register。下面我们一步步来看都做了些啥?
111#ifdef CONFIG_SMP
112#if defined(CONFIG_CPU_CORTEX_A9)
113 mov r10, #(1 << 0) @ TLB ops broadcasting
114#elif defined(CONFIG_CPU_CORTEX_A15)
115 mov r10, #0
116#endif
117 mrc p15, 0, r0, c1, c0, 1
118#if 0
119 ALT_UP(mov r0, #(1 << 6)) @ fake it for UP
120#endif
121 tst r0, #(1 << 6) @ SMP/nAMP mode enabled?
122 orreq r0, r0, #(1 << 6) @ Enable SMP/nAMP mode
123 orreq r0, r0, r10 @ Enable CPU-specific SMP bits
124 mcreq p15, 0, r0, c1, c0, 1
125#endif
这里首先是读取一个实现特定的Auxiliary Control Register使能SMP模式,并且使能了Cache and TLB maintenance broadcast这种保持一致性的广播操作。ARMv7的TRM文档中对这个寄存器描述如下:
那么,对于我们的i.MX6,属于Cortex A9,因此应该看Cortex A9的TRM文档里是怎么描述的:
接下来是读取芯片的版本信息,并更具版本信息来应用一些特定于某些版本的Errata修复。
126 mrc p15, 0, r0, c0, c0, 0 @ read main ID register
127 and r10, r0, #0xff000000 @ ARM?
128 teq r10, #0x41000000
129 bne 3f
130 and r5, r0, #0x00f00000 @ variant
131 and r6, r0, #0x0000000f @ revision
132 orr r6, r6, r5, lsr #20-4 @ combine variant and revision
133 ubfx r0, r0, #4, #12 @ primary part number
134
135 /* Cortex-A8 Errata */
136 ldr r10, =0x00000c08 @ Cortex-A8 primary part number
137 teq r0, r10
138 bne 2f
139#ifdef CONFIG_ARM_ERRATA_430973
140 teq r5, #0x00100000 @ only present in r1p*
141 mrceq p15, 0, r10, c1, c0, 1 @ read aux control register
142 orreq r10, r10, #(1 << 6) @ set IBE to 1
143 mcreq p15, 0, r10, c1, c0, 1 @ write aux control register
144#endif
145#ifdef CONFIG_ARM_ERRATA_458693
146 teq r6, #0x20 @ only present in r2p0
147 mrceq p15, 0, r10, c1, c0, 1 @ read aux control register
148 orreq r10, r10, #(1 << 5) @ set L1NEON to 1
149 orreq r10, r10, #(1 << 9) @ set PLDNOP to 1
150 mcreq p15, 0, r10, c1, c0, 1 @ write aux control register
151#endif
152#ifdef CONFIG_ARM_ERRATA_460075
153 teq r6, #0x20 @ only present in r2p0
154 mrceq p15, 1, r10, c9, c0, 2 @ read L2 cache aux ctrl register
155 tsteq r10, #1 << 22
156 orreq r10, r10, #(1 << 22) @ set the Write Allocate disable bit
157 mcreq p15, 1, r10, c9, c0, 2 @ write the L2 cache aux ctrl register
158#endif
159 b 3f
160
161 /* Cortex-A9 Errata */
1622: ldr r10, =0x00000c09 @ Cortex-A9 primary part number
163 teq r0, r10
164 bne 3f
165#ifdef CONFIG_ARM_ERRATA_742230
166 cmp r6, #0x22 @ only present up to r2p2
167 mrcle p15, 0, r10, c15, c0, 1 @ read diagnostic register
168 orrle r10, r10, #1 << 4 @ set bit #4
169 mcrle p15, 0, r10, c15, c0, 1 @ write diagnostic register
170#endif
171#ifdef CONFIG_ARM_ERRATA_742231
172 teq r6, #0x20 @ present in r2p0
173 teqne r6, #0x21 @ present in r2p1
174 teqne r6, #0x22 @ present in r2p2
175 mrceq p15, 0, r10, c15, c0, 1 @ read diagnostic register
176 orreq r10, r10, #1 << 12 @ set bit #12
177 orreq r10, r10, #1 << 22 @ set bit #22
178 mcreq p15, 0, r10, c15, c0, 1 @ write diagnostic register
179#endif
180#ifdef CONFIG_ARM_ERRATA_743622
181 teq r5, #0x00200000 @ only present in r2p*
182 mrceq p15, 0, r10, c15, c0, 1 @ read diagnostic register
183 orreq r10, r10, #1 << 6 @ set bit #6
184 mcreq p15, 0, r10, c15, c0, 1 @ write diagnostic register
185#endif
186#if defined(CONFIG_ARM_ERRATA_751472) && defined(CONFIG_SMP)
187 cmp r6, #0x30 @ present prior to r3p0
188 mrclt p15, 0, r10, c15, c0, 1 @ read diagnostic register
189 orrlt r10, r10, #1 << 11 @ set bit #11
190 mcrlt p15, 0, r10, c15, c0, 1 @ write diagnostic register
1911:
192#endif
我们假设i.MX6不存在这几个Errata,因此就会执行到下面的代码,执行一个Instruction cache invalidate all操作:
1943: mov r10, #0
195 mcr p15, 0, r10, c7, c5, 0 @ I+BTB cache invalidate
196 dsb
在Invalidate所有的指令Cache之后,执行下面的代码:
198 adr r5, v7_crval
199 ldmia r5, {r5, r6}
200#ifdef CONFIG_SWP_EMULATE
201 orr r5, r5, #(1 << 10) @ set SW bit in "clear"
202 bic r6, r6, #(1 << 10) @ clear it in "mmuset"
203#endif
204 mrc p15, 0, r0, c1, c0, 0 @ read control register
205 bic r0, r0, r5 @ clear bits them
206 orr r0, r0, r6 @ set them
207 mov pc, lr
208
209 /* AT
210 * TFR EV X F I D LR S
211 * .EEE ..EE PUI. .T.T 4RVI ZWRS BLDP WCAM
212 * rxxx rrxx xxx0 0101 xxxx xxxx x111 xxxx < forced
213 * 0 0 110 0001 1100 .111 1101 < we want
214 */
215 .align 2
216 .type v7_crval, #object
217v7_crval:
218 .word 0x0120c302 /* clear */
219 .word 0x00c01c7d /* mmuset */
这里是要将System Control Register寄存器读取到r0中,并且使用r5里的值作为掩码清除掉一些字段,然后用r6中的值设置上一些字段。我们看了这个系统控制寄存器的格式之后就知道具体清除或者设置了哪些字段。
因此,使用0x0120c302这个值清掉的字段包括:
类似,使用0x00c01c7d这个值设置的指端包括:
注意上面的操作中没有碰那个TRE字段,应该是采用了从bootloader加载过来的值。根据前面分析的Section页表属性:
280#if (__ARM_ARCH_VERSION__ < 6)
281#define TTBL_L1TBL_TTE_ARCH_ATTR TTBL_L1TBL_TTE_REQ_MASK
282#else
283#define TTBL_L1TBL_TTE_ARCH_ATTR (TTBL_L1TBL_TTE_C_MASK | \
284 TTBL_L1TBL_TTE_B_MASK)
285#endif
287#define SECTION_ATTR ((TTBL_AP_SRW_U << TTBL_L1TBL_TTE_AP_SHIFT) | \
288 (TTBL_L1TBL_TTE_DOM_RESERVED << TTBL_L1TBL_TTE_DOM_SHIFT) | \
289 TTBL_L1TBL_TTE_ARCH_ATTR | \
290 TTBL_L1TBL_TTE_TYPE_SECTION)
我们注意到这里的Section属性设置了下面的字段:
当代码返回proc_setp的调用者后,r0寄存器中存放着用来设置系统控制寄存器的值,因此直接写入该寄存器,就实现了MMU的使能。
270 /* Setup System Control Register */
271 bl proc_setup
272 mcr p15, 0, r0, c1, c0, 0
到此为止,MMU已经在发挥作用了。我们可以看出,对于MMU的配置,目前主要有两个内存区间:
274 /* Jump to reset code */
275 ldr pc, __reset
445__reset:
446 .word _reset
479 /*
480 * Reset exception handler.
481 * Reset hardware state before starting Xvisor.
482 */
483 .globl _reset
484_reset:
485 /* Clear a register for temporary usage */
486 mov r8, #0
487 /* Disable IRQ & FIQ */
488 mrs r8, cpsr_all
489 orr r8, r8, #(CPSR_IRQ_DISABLED | CPSR_FIQ_DISABLED)
490 msr cpsr_cxsf, r8
这样,代码转而进入_reset,接着执行,但是现在已经在"执行内存空间"中了。进来的第一步就是禁止中断(IRQ和FIQ),这样以后的代码就可以一线执行下去,直到后面使能中断。
491 /* Set Supervisor Mode Stack */
492 CMODE r8, CPSR_MODE_SUPERVISOR
493 ldr sp, __svc_stack_end
494#ifdef CONFIG_SMP
495 mrc p15, 0, r10, c0, c0, 5
496 ands r10, r10, #0xFF
497 mov r9, #CONFIG_IRQ_STACK_SIZE
498 mul r9, r9, r10
499 sub sp, sp, r9
500#endif
501 /* Set Undefined Mode Stack */
502 CMODE r8, CPSR_MODE_UNDEFINED
503 ldr sp, __und_stack_end
504#ifdef CONFIG_SMP
505 mrc p15, 0, r10, c0, c0, 5
506 ands r10, r10, #0xFF
507 mov r9, #0x100
508 mul r9, r9, r10
509 sub sp, sp, r9
510#endif
511 /* Set Abort Mode Stack */
512 CMODE r8, CPSR_MODE_ABORT
513 ldr sp, __abt_stack_end
514#ifdef CONFIG_SMP
515 mrc p15, 0, r10, c0, c0, 5
516 ands r10, r10, #0xFF
517 mov r9, #0x100
518 mul r9, r9, r10
519 sub sp, sp, r9
520#endif
521 /* Set IRQ Mode Stack */
522 CMODE r8, CPSR_MODE_IRQ
523 ldr sp, __irq_stack_end
524#ifdef CONFIG_SMP
525 mrc p15, 0, r10, c0, c0, 5
526 ands r10, r10, #0xFF
527 mov r9, #0x100
528 mul r9, r9, r10
529 sub sp, sp, r9
530#endif
531 /* Set FIQ Mode Stack */
532 CMODE r8, CPSR_MODE_FIQ
533 ldr sp, __fiq_stack_end
534#ifdef CONFIG_SMP
535 mrc p15, 0, r10, c0, c0, 5
536 ands r10, r10, #0xFF
537 mov r9, #0x100
538 mul r9, r9, r10
539 sub sp, sp, r9
540#endif
这一段代码其实很简单,就是切换到不同的处理器模式来设置在该模式下的堆栈指针,因此无需多说。要注意的是,对于SMP的情形,这段代码是BSP和AP都会走过的,因此对于AP而言,需要相应地调整堆栈指针。接下来,代码切换回到Supervisor Mode,并且跳转到C代码cpu_init处执行。cpu_init()会进而调用vmm_init(),进而在初始化其他子系统,必然中断和调度等,因此不会再返回。
541 /* Set to Supervisor Mode */
542 CMODE r8, CPSR_MODE_SUPERVISOR
543 /* Call CPU init function */
544 b cpu_init
545 /* We should never reach here */
546 b .
对于初始化的基本流程就分析到这里。下面是这个周末移植到i.MX6Q上的结果(基本的串口,时钟,中断,调度已经可用,但是还没有调试SMP从核的启动,也还没有调试Guest的初始化部分,因此输出中还有一些错误消息可以看到,不过这个是下一步的事情了!)。