来自:http://blog.csdn.net/BoySKung/archive/2008/12/09/3486026.aspx
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@ get processorid
80. bl__lookup_processor_type@r5=procinfor9=cpuid
81. movsr10,r5@invalidprocessor(r5=0)? // 处理器信息结构基地址保存到r10
82. beq__error_p@yes,error 'p'
83. bl__lookup_machine_type@r5=machinfo
84. movsr8,r5@invalidmachine(r5=0)? // 机器类型结构的基地址保存到r8
85. beq__error_a@yes,error 'a'
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:
212. pgtblr4@pagetableaddress
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+kernel base
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@needed for kernelminus1
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 ,开始执行处理器无关代码。