arm-linux源码分析之cpu初始化

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

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模式,禁止FIQIRQ。然后查找处理器类型、查找机器类型,如果出现错误则进行相应的处理,如果没错,则创建页表。下面分别看看这三个函数。

 

__create_page_tables211行定义,这个函数在后面介绍,先看看其他两个。

 

__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函数的地址分别存储到r13lr寄存器中,最后通过

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.S386

 

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/Dcachewrite buffer无效,使I/D TLB无效,然后加载arm920_crval这个符号的地址,它的定义在408

 

   

408.  .typearm920_crval,#object

409.  arm920_crval:

410.  crvalclear=0x 00003f 3f ,mmuset=0x00003135,ucset=0x00001130

411.   

 

通过ldmia指令加载后,r5=0x 00003f 3f , 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.S151行定义

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.   

 

这里先将前面对控制寄存器的配置写入控制寄存器,打开mmuI/Dcache等,然后读处理器ID寄存器到r3中,最后把r13加载到pc,前面提到,__switch_data的地址被加载到r13中,现在就看看这个对象,在/arch/arm/kernel/head-common.S15行定义:

 

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_typecr_alignmentinit_thread_union + THREAD_START_SP 地址到r4r5r6sp。接下来保存处理器ID和机器类型。把r0r4保存到cr_alignmentcr_no_alignment变量中,最后跳到start_kernel处。

 

这里说说cr_alignmentinit_thread_union这两个参数,cr_alignmentarch/arm/kernel/entry-armv.S1077行定义:

 

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_alignmentr4存到了cr_no_alignment

 

init_thread_unionarch/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.S210行定义:

   

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.  //r10procinfo的基地址,这里加载了proc_infor_list结构中__cpu_mm_mmu_flagsr7

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的,假设该指令的PA0x0800 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&0x 00f 00000)>>18]!

245.   

// TEXTADDR是内核起始虚拟地址(c0008000),(TEXTADDR & 0xff000000) >> 18(TEXTADDR & 0x 00f 00000) >> 18获得了虚拟地址的高14位,这14位中最低两位为04字节对齐,和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描述符,这样在打开mmucpu发出的地址就不会被映射到错误的物理地址上去。紧接着获得内核所占的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/DcacheTLB进行相关设置,为打开mmu作准备。主要是清空了I/Dcachetlbwrite buffer。然后打开mmu。打开mmu前,先设置好TTB,然后再打开。最后判断是否需要进行数据段搬移,如果数据段已经在RAM中就不要进行搬移。然后清空bss段,跳转到start_kernel,开始执行处理器无关代码。

 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值