Chinaunix首页 | 论坛 | 博客
  • 博客访问: 9464899
  • 博文数量: 1750
  • 博客积分: 12961
  • 博客等级: 上将
  • 技术积分: 20091
  • 用 户 组: 普通用户
  • 注册时间: 2009-01-09 11:25
个人简介

偷得浮生半桶水(半日闲), 好记性不如抄下来(烂笔头). 信息爆炸的时代, 学习是一项持续的工作.

文章分类

全部博文(1750)

文章存档

2024年(26)

2023年(26)

2022年(112)

2021年(217)

2020年(157)

2019年(192)

2018年(81)

2017年(78)

2016年(70)

2015年(52)

2014年(40)

2013年(51)

2012年(85)

2011年(45)

2010年(231)

2009年(287)

分类: LINUX

2009-09-14 17:01:45

linux源码分析之cpu初始化 kernel/head.s 收藏
来自: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. 

81.  movsr10,r5@invalidprocessor(r5=0)? // 处理器信息结构基地址保存到r10

82.  'p'

83. 

84.  movsr8,r5@invalidmachine(r5=0)? // 机器类型结构的基地址保存到r8

85.  '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. 

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 ,开始执行处理器无关代码。

发表于 @ 2009年06月22日 10:00:00 | 评论( 0 ) | 编辑| 举报| 收藏

旧一篇:uboot到linux启动内核需要注意的经验之谈 | 新一篇:linux内核的移植与遭遇问题的解决 启动调试 printascii给lanmanck的留言只有注册用户才能发表评论!登录注册姓   名:
校验码:
 Csdn Blog version 3.1a
Copyright © lanmanck  

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/lanmanck/archive/2009/06/22/4288048.aspx

阅读(1429) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~