linux-2.6.20.6/arch/arm/kernel/head.S
这是解压内核后内核入口所在的文件,完成内核解压后将控制权将转移到这里的入口。
先看一下arch/arm/kernel/vmlinux.lds这个链接脚本,在开头
186. OUTPUT_ARCH(arm)
187. ENTRY(stext)
188. jiffies = jiffies_64;
189.
这里指定stext为入口。
下而回过头来看head.S中内容,
74. __INIT
75. .typestext,%function
76. ENTRY(stext)
77. msrcpsr_c,#PSR_F_BIT|PSR_I_BIT|SVC_MODE@ensuresvcmode
78. @andirqsdisabled
79. mrcp15,0,r9,c0,c0@getprocessorid
80. bl__lookup_processor_type@r5=procinfor9=cpuid
81. movsr10,r5@invalidprocessor(r5=0)?//处理器信息结构基地址保存到r10
83. bl__lookup_machine_type@r5=machinfo
84. movsr8,r5@invalidmachine(r5=0)? //机器类型结构的基地址保存到r8
86. bl__create_page_tables
87.
开头的__INIT是一个宏定义在include/linux/init.h中:
55. #define __INIT .section ".init.text","ax"
a表示Section contains allocated data
x表示Section contains executable instructions.
ENTRY(stext)也是一个宏,在include/linux/linkage.h中定义
30. #ifndef ENTRY
31. #define ENTRY(name) /
32. .globl name; /
33. ALIGN; /
34. name:
35. #endif
36.
这段代码首先设置cpu工作模式为svc模式,禁止FIQ、IRQ。然后查找处理器类型、查找机器类型,如果出现错误则进行相应的处理,如果没错,则创建页表。下面分别看看这三个函数。
__create_page_tables在211行定义,这个函数在后面介绍,先看看其他两个。
__lookup_processor_type 这个函数定义在arch/arm/kernel/head-common.S的第146行,
146. __lookup_processor_type:
147. adr r3, 3f
148. ldmda r3, {r5 - r7}
149. sub r3, r3, r7 @ get offset between virt
150. add r5, r5, r3 @ convert virt addresses to
151. add r6, r6, r3 @ physical address space
152. 1: ldmia r5, {r3, r4} @ value, mask
153. and r4, r4, r9 @ mask wanted bits
154. teq r3, r4
155. beq 2f
156. add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)
157. cmp r5, r6
158. blo 1b
159. mov r5, #0 @ unknown processor
160. 2: mov pc, lr
161.
162. /*
163. * This provides a C-API version of the above function.
164. */
165. ENTRY(lookup_processor_type)
166. stmfd sp!, {r4 - r7, r9, lr}
167. mov r9, r0
168. bl __lookup_processor_type
169. mov r0, r5
170. ldmfd sp!, {r4 - r7, r9, pc}
171.
172. /*
173. * Look in include/asm-arm/procinfo.h and arch/arm/kernel/arch.[ch] for
174. * more information about the __proc_info and __arch_info structures.
175. */
176. .long __proc_info_begin
177. .long __proc_info_end
178. 3: .long .
179. .long __arch_info_begin
180. .long __arch_info_end
181.
这里能过查表的方式查找对应处理器的信息结构,如果找到,则把它的基地址放入r5寄存器,没有找到则r5=0。在链接脚本arch/arm/kernel/vmlinux.lds中有
197. __proc_info_begin = .;
198. *(.proc.info.init)
199. __proc_info_end = .;
200.
这三行把所有处理器信息结构组合在一块,就像一个结构数组。这样查找时只要找到__proc_infor_end的地址,很快就能找到处理器信息结构数组。对于机器信息也是一样:
200. __arch_info_begin = .;
201. *(.arch.info.init)
202. __arch_info_end = .;
203.
把这些信息组合在一起。
194. __lookup_machine_type:
195. adr r3, 3b
196. ldmia r3, {r4, r5, r6}
197. sub r3, r3, r4 @ get offset between virt
198. add r5, r5, r3 @ convert virt addresses to
199. add r6, r6, r3 @ physical address space
200. 1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type
201. teq r3, r1 @ matches loader number?
202. beq 2f @ found
203. add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc
204. cmp r5, r6
205. blo 1b
206. mov r5, #0 @ unknown machine
207. 2: mov pc, lr
208.
209. /*
210. * This provides a C-API version of the above function.
211. */
212. ENTRY(lookup_machine_type)
213. stmfd sp!, {r4 - r6, lr}
214. mov r1, r0
215. bl __lookup_machine_type
216. mov r0, r5
217. ldmfd sp!, {r4 - r6, pc}
218.
这是函数__lookup_machine_type:的定义。查找方法和__lookup_processor_type是一样的,在 arch/arm/kernel/head-common.S定义,第194行。
回到head.S中
88. /*
89. *ThefollowingcallsCPUspecificcodeinapositionindependent
90. *manner.Seearch/arm/mm/proc-*.Sfordetails.r10=baseof
91. *xxx_proc_infostructureselectedby__lookup_machine_type
92. *above.Onreturn,theCPUwillbereadyfortheMMUtobe
93. *turnedon,andr0willholdtheCPUcontrolregistervalue.
94. */
95. ldrr13,__switch_data@addresstojumptoafter
96. @mmuhasbeenenabled
97. adrlr,__enable_mmu@return(PIC)address
98. addpc,r10,#PROCINFO_INITFUNC
这里把__switch_data和__enablemmu函数的地址分别存储到r13、lr寄存器中,最后通过
add pc, r10, #PROCINFO_INITFUNC
这条指令,跳转到处理器相关的函数去执行,这里r10中存放着处理器相关信息结构的基地址,PROCINFO_INITFUNC是一个偏移量,arm920t的信息结构在arch/arm/mm/proc-arm920.S的第451行初始化:
451. __arm920_proc_info:
452. .long 0x41009200
453. .long 0xff00fff0
454. .long PMD_TYPE_SECT | /
455. PMD_SECT_BUFFERABLE | /
456. PMD_SECT_CACHEABLE | /
457. PMD_BIT4 | /
458. PMD_SECT_AP_WRITE | /
459. PMD_SECT_AP_READ
460. .long PMD_TYPE_SECT | /
461. PMD_BIT4 | /
462. PMD_SECT_AP_WRITE | /
463. PMD_SECT_AP_READ
464. b __arm920_setup //这条指令跳转到__arm920_setup中对cpu进行设置
465. .long cpu_arch_name
466. .long cpu_elf_name
467. .long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB
468. .long cpu_arm920_name
469. .long arm920_processor_functions
470. .long v4wbi_tlb_fns
471. .long v4wb_user_fns
472. #ifndef CONFIG_CPU_DCACHE_WRITETHROUGH
473. .long arm920_cache_fns
474. #else
475. .long v4wt_cache_fns
476. #endif
477.
通过
add pc, r10, #PROCINFO_INITFUNC
找到
b __arm920_setup
这条指令,然后跳到__arm920_setup这个函数中,这个函数的定义在arm/arm/mm/proc-arm920.S的386行
386. __arm920_setup:
387. mov r0, #0
388. mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4
389. mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4
390. #ifdef CONFIG_MMU
391. mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4
392. #endif
393. adr r5, arm920_crval
394. ldmia r5, {r5, r6}
395. mrc p15, 0, r0, c1, c0 @ get control register v4
396. bic r0, r0, r5
397. orr r0, r0, r6
398. mov pc, lr
399.
这段代码首先使I/Dcache和write buffer无效,使I/D TLB无效,然后加载arm920_crval这个符号的地址,它的定义在408行
408. .typearm920_crval,#object
409. arm920_crval:
410. crvalclear=0x00003f3f,mmuset=0x00003135,ucset=0x00001130
411.
通过ldmia指令加载后,r5=0x00003f3f, r6=0x00003135, 由这两个数对加载的控制寄存器的位进行操作。
bic r0, r0, r5
对照arm920t手册可知,这条指令清除了mmu, I/D cache等位,
orr r0, r0, r6
将mmu,I/D cache等位置位。
最后跳通过398行的
mov pc, lr
指令转到__enable_mmu,head.S第151行定义
151. __enable_mmu:
152. #ifdef CONFIG_ALIGNMENT_TRAP
153. orr r0, r0, #CR_A
154. #else
155. bic r0, r0, #CR_A
156. #endif
157. #ifdef CONFIG_CPU_DCACHE_DISABLE
158. bic r0, r0, #CR_C
159. #endif
160. #ifdef CONFIG_CPU_BPREDICT_DISABLE
161. bic r0, r0, #CR_Z
162. #endif
163. #ifdef CONFIG_CPU_ICACHE_DISABLE
164. bic r0, r0, #CR_I
165. #endif
166. mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | /
167. domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | /
168. domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | /
169. domain_val(DOMAIN_IO, DOMAIN_CLIENT))
170. mcr p15, 0, r5, c3, c0, 0 @ load domain access register
171. mcr p15, 0, r4, c2, c0, 0 @ load page table pointer
172. b __turn_mmu_on
173.
在开头先根据配置,对控制寄存器中的位进行设置,然后设置域访问控制寄存器,把页表基址保存到TTB中,这个页表基址是在__create_page_tables这个函数在加载到r4寄存器中的。这个函数后面再介绍。
接着跳转到函数__turn_mmu_on,在head.S 187行定义
187. __turn_mmu_on:
188. mov r0, r0
189. mcr p15, 0, r0, c1, c0, 0 @ write control reg
190. mrc p15, 0, r3, c0, c0, 0 @ read id reg
191. mov r3, r3
192. mov r3, r3
193. mov pc, r13
194.
这里先将前面对控制寄存器的配置写入控制寄存器,打开mmu,I/Dcache等,然后读处理器ID寄存器到r3中,最后把r13加载到pc,前面提到,__switch_data的地址被加载到r13中,现在就看看这个对象,在/arch/arm/kernel/head-common.S第15行定义:
15. .type __switch_data, %object
16. __switch_data:
17. .long __mmap_switched
18. .long __data_loc @ r4
19. .long __data_start @ r5
20. .long __bss_start @ r6
21. .long _end @ r7
22. .long processor_id @ r4
23. .long __machine_arch_type @ r5
24. .long cr_alignment @ r6
25. .long init_thread_union + THREAD_START_SP @ sp
26.
前面mov pc r13刚好把__mmap_switched的地址加载到pc中,__mmap_switched是个函数。在arch/arm/kernel/head-common.S的第34行定义。
34. .type __mmap_switched, %function
35. __mmap_switched:
36. adr r3, __switch_data + 4 //将__data_loc的地址加载到r3中
37.
38. ldmia r3!, {r4, r5, r6, r7} //加载__data_loc、__data_start、__bss_start及_end的地址
39. cmp r4, r5 @ Copy data segment if needed 比较__data_start与__data_loc是否相等
40. 1: cmpne r5, r6 //如果不相等,进行数据搬运
41. ldrne fp, [r4], #4
42. strne fp, [r5], #4
43. bne 1b
44.
45. mov fp, #0 @ Clear BSS (and zero fp)
46. 1: cmp r6, r7 //给bss段清0
47. strcc fp, [r6],#4
48. bcc 1b
49.
50. ldmia r3, {r4, r5, r6, sp} //这里r3保存的已经是processor_id的地址了
51. str r9, [r4] @ Save processor ID
52. str r1, [r5] @ Save machine type
53. bic r4, r0, #CR_A @ Clear 'A' bit
54. stmia r6, {r0, r4} @ Save control register values
55. b start_kernel
56.
这段代码首先检查数据段的起始地址__data_start是否放到了指定位置__data_loc中,如果不是,则要进行数据搬移。之后,对bss段清零。然加载processor_id、__machine_arch_type、cr_alignment、init_thread_union + THREAD_START_SP 地址到r4、r5、r6、sp。接下来保存处理器ID和机器类型。把r0、r4保存到cr_alignment和cr_no_alignment变量中,最后跳到start_kernel处。
这里说说cr_alignment和init_thread_union这两个参数,cr_alignment在arch/arm/kernel/entry-armv.S中1077行定义:
1077. .globl cr_alignment
1078. .globl cr_no_alignment
1079. cr_alignment:
1080. .space 4 //这里space是指为cr_alignment分配4字节内存。
1081. cr_no_alignment:
1082. .space 4
1083.
所以stmia r6, {r0, r4}把r0存到了cr_alignment,r4存到了cr_no_alignment
init_thread_union在arch/arm/kernel/init_task.c中第33行定义
33. union thread_union init_thread_union
34. __attribute__((__section__(".init.task"))) =
35. { INIT_THREAD_INFO(init_task) };
36.
由此可知,init_thread_union被链接到.init.task section中。
前面提到了__create_page_tables这个函数,现在分析一下,head.S第210行定义:
210. .type__create_page_tables,%function
211. __create_page_tables:
213.
/*
这个pgtbl是个宏定义,在head.S 46行
46. .macropgtbl,rd
47. ldr/rd,=(KERNEL_RAM_PADDR-0x4000)
48. .endm
49.
它把页表基地址,也就是内核起始地址之前的16K起始地址,加载到r4中。
*/
214. /*
215. *Clearthe16Klevel1swapperpagetable
216. */
217. movr0,r4
218. movr3,#0
219. addr6,r0,#0x4000
220. 1:strr3,[r0],#4
221. strr3,[r0],#4
222. strr3,[r0],#4
223. strr3,[r0],#4
224. teqr0,r6
225. bne1b
226. //以上将原来的1:1映射的16K页表清空,在解压内核前创建的
227. ldrr7,[r10,#PROCINFO_MM_MMUFLAGS]@mm_mmuflags
228. //r10是procinfo的基地址,这里加载了proc_infor_list结构中__cpu_mm_mmu_flags到r7
229. /*
230. *CreateidentitymappingforfirstMBofkernelto
231. *caterfortheMMUenable.Thisidentitymapping
232. *willberemovedbypaging_init().Weuseourcurrentprogram
233. *countertodeterminecorrespondingsectionbaseaddress.
234. */
235. movr6,pc,lsr#20@startofkernelsection
236. orrr3,r7,r6,lsl#20@flags+kernelbase
237. strr3,[r4,r6,lsl#2]@identitymapping
238.
首先将当前运行的内核指令所在的物理地址除以1M(右移20位),看这条指令在第几个section,然后通过orr r3, r7, r6, lsl #20形成了这个section对应的描述符,并写入对应的页表入口,因为每个section描述符占4个字节,这里把起始section数乘以4,加上r4中的页表起始地址,找到对应的页表入口。然后写入描述符
设置好页表之后,最终有一条指令是启用MMU的,假设该指令的PA是0x0800 810c,根据我们要做的映射关系,它的VA应该是0xc000 810c,没有启用MMU之前CPU核发出的都是物理地址,从0x0800 810c地址取这条指令来执行,然而该指令执行之后,CPU核发出的地址都要被MMU拦截,CPU核就必须用虚拟地址来取指令了,因此下一条指令应该从0xc000 8110处取得,然而这时pc寄存器(也就是r15寄存器)的值并没有变,CPU核取下一条指令仍然要从0x0800 8110处取得,此时0x0800 8110已经成了非法地址了
为 了解决这个问题,要求启用MMU的那条指令及其附近的指令虚拟地址跟物理地址相同,这样在启用MMU前后,附近指令的地址不会发生变化,从而实现平稳过渡。因此需要将物理地址从0x0800 0000开始的1M再映射到虚拟地址从0x0800 0000开始的1M,也就是做一个等价映射(identity map)(事实上,以上解释并不完全正确,这里还有一个更复杂的细节,启用MMU的指令在执行时,后面两条指令已经预取到CPU流水线里了,如果利用那两条指令跳转到0xc000 8110不就行了?但是流水线是靠不住的,跳转和异常都会清空流水线,[ARM参考手册]的Chapter A2详细解释了这种情况,按该手册的建议应该采用等价映射的方法解决这个问题。)
239. /*
240. *Nowsetupthepagetablesforourkerneldirect
241. *mappedregion.
242. */
243. addr0,r4,#(TEXTADDR&0xff000000)>>18@startofkernel
244. strr3,[r0,#(TEXTADDR&0x00f00000)>>18]!
245.
// TEXTADDR是内核起始虚拟地址(c0008000),(TEXTADDR & 0xff000000) >> 18和(TEXTADDR & 0x00f00000) >> 18获得了虚拟地址的高14位,这14位中最低两位为0,4字节对齐,和ttb中页表基址一起索引到页表中的一个位置,然后将页描述符写入页表,这个页描述符和上一个是一样的,这样,在这第一个1MB空间内,不管cpu发出的是虚拟地址还是物理地址,取的都是同一个存储单元的数据。这就解决了mmu打开是pc中存的还是没打开前的物理地址的问题。
246. ldrr6,=(_end-PAGE_OFFSET-1)@r6=numberofsections
247. movr6,r6,lsr#20@neededforkernelminus1
PAGE_OFFSET是内核空间的起始虚拟地址,这里 减1的原因是因为 _end 是 location counter,它的地址是kernel镜像后面的一个byte的地址,这样就获得了内核大小,然后右移20位,也就是除以1M,就得到了这个内核点的section的数目。存入r6
249. 1: add r3, r3, #1 << 20 //将描述符中前12位的基地址加上1M形成下一个section的描述符
250. str r3, [r0, #4]!
251. subs r6, r6, #1
252. bgt 1b
253.
//按照上面的方法直到把所有的内核占的section都映射完。
254. /*
255. * Then map first 1MB of ram in case it contains our boot params.
256. */
257. add r0, r4, #PAGE_OFFSET >> 18 //获得内核空间起始虚拟地址对应描述符在表中的位置
258. orr r6, r7, #(PHYS_OFFSET & 0xff000000)
259. orr r6, r6, #(PHYS_OFFSET & 0x00e00000) //生成ram起始的1M物理地址描述符
260. str r6, [r0] //将描述符写入页表
261.
262. #ifdef CONFIG_XIP_KERNEL
263. /*
264. * Map some ram to cover our .data and .bss areas.
265. * Mapping 3MB should be plenty.
266. */
267. sub r3, r4, #PHYS_OFFSET
268. mov r3, r3, lsr #20 //这两行获得页表起始地址和ram物理起始地址间的section数
269. add r0, r0, r3, lsl #2 //每个section描述符占四字节,这里乘以4,获得这些描述符占的总字节数,然后与r0相加,找到一个新的页表入口
270. add r6, r6, r3, lsl #20 //每个section描述符描述1M内存,这里乘以1M,与r6相加,形成一个新的物理section描述符
271. str r6, [r0], #4
272. add r6, r6, #(1 << 20)
273. str r6, [r0], #4
274. add r6, r6, #(1 << 20)
275. str r6, [r0] //连续将三个描述符写入三个连续的页表入口,
276. #endif
277.
这里还有一些用于调度的代码就不作分析了,跳到 319行
319. mov pc, lr
320. .ltorg //这个伪指令声明了一个文字池,把ldr伪指令要加载的数据保存在文字池内,再用arm的加载指令读出数据,这里将ldr r6, =(_end - PAGE_OFFSET - 1)中的_end - PAGE_OFFSET – 1表示的地址。
321.
分析完后先对创建页表作个总结,开始时通过一个宏获得页表基地址,然后清空页表,接着在proc_infor_list结构中获得__cpu_mm_mmu_flags,也就是一级描述符的低20位的值。然后对当前运行的内核指令地址所在的section进行等价映射,使其物理地址和虚拟地址一样,接着再把这1M物理地址映射到链接时设置的内核起始虚拟地址TEXTADDR对应的section描述符,这样在打开mmu时cpu发出的地址就不会被映射到错误的物理地址上去。紧接着获得内核所占的section数,把剩下的section描述符写到对应的页表入口。最后还要把第一个1M的ram空间映射相应的页表入口,因为这1M的空间可能存放着内核启动参数。如果定义了CONFIG_XIP_KERNEL还要再映射3M内存,它说是为了cover our .data and .bss areas,不过我还没看懂是怎么回事。
总算分析结束了,现在对整个head.S及其相关文件代码分析做个总结。
首先通过arch/arm/kernel/head-common.S中的__lookup_processor_type和__lookup_machine_type两个函数,找到处理器类型和机器类型然后创建页表,创建页表时因为考虑到打开mmu前后cpu发出的地址由物理地址变成了虚拟地址,所以将内核开始的1M空间先进行等价映射,再将这1M映射到它对就的虚拟地址空间。页表创建结束后,跳转到arch/arm/mm/proc-arm920.S中的__arm920_setup函数,对I/Dcache和TLB进行相关设置,为打开mmu作准备。主要是清空了I/Dcache、tlb及write buffer。然后打开mmu。打开mmu前,先设置好TTB,然后再打开。最后判断是否需要进行数据段搬移,如果数据段已经在RAM中就不要进行搬移。然后清空bss段,跳转到start_kernel,开始执行处理器无关代码。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/BoySKung/archive/2008/12/09/3486026.aspx