您的位置:首页 > 运维架构 > Linux

linux源码分析之cpu初始化 kernel/head.s

2009-06-22 10:00 387 查看
来自: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
,开始执行处理器无关代码。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: