UBOOT中start.S中源码的指令级的详尽解析

来源:互联网 发布:淘宝旺旺客服回复模板 编辑:程序博客网 时间:2024/05/17 22:18


转自:http://www.crifan.com/files/doc/docbook/uboot_starts_analysis/release/htmls/index.html

 

在看的Uboot的start.S中文件时候,发现其最开始初始化系统,做的第一件事情,就是将CPU设置为SVC模式,但是S3C2440的CPU的核心是ARM920T,其有7种模式,为何非要设置为SVC模式,而不是设置为其他模式呢对此,经过一些求证,得出如下原因?

首先,先要了解ARM的CPU的7种模式是哪些:

http://www.docin.com/p-73665362.html

表3.1。ARM CPU中的模式

处理器模式说明备注用户(USR)正常程序工作模式此模式下程序不能够访问一些受操作系统保护的系统资源,应用程序也不能直接进行处理器模式的切换。系统(SYS)用于支持操作系统的特权任务等与用户模式类似,但具有可以直接切换到其它模式等特权快中断(FIQ)支持高速数据传输及通道处理FIQ异常响应时进入此模式中断(IRQ)用于通用中断处理IRQ异常响应时进入此模式管理(SVC)操作系统保护代码系统复位和软件中断响应时进入此模式中止(ABT)用于支持虚拟内存和/或存储器保护在ARM7TDMI没有大用处未定义(UND)支持硬件协处理器的软件仿真未定义指令异常响应时进入此模式


另外,7种模式中,除用户USR模式外,其它模式均为特权模式。

对于为何此处是SVC的模式,而不是其他某种格式,其原因,可以从两方面来看:

  1. 我们先简单的来分析一下那7种模式:

    1. 中止生根粉和未定义UND模式

      首先可以排除的是,中止ABT和未定义UND模式,那都是不太正常的模式,此处程序是正常运行的,所以不应该设置CPU为其中任何一种模式,所以可以排除。

    2. 快中断FIQ和中断IRQ模式

      其次,对于快中断FIQ和中断IRQ来说,此处的uboot初始化的时候,也还没啥中断要处理和能够处理,而且即使是注册了终端服务程序后,能够处理中断,那么这两种模式,也是自动切换过去的,所以,此处也不应该设置为其中任何一种模式。

    3. 用户USR模式

      虽然从理论上来说,可以设置CPU为用户USR模式,但是由于此模式无法直接访问很多的硬件资源,而UBOOT初始化,就必须要去访问这类资源,所以此处可以排除,不能设置为用户USR模式。

    4. 系统sys模式vs管理svc模式

      首先,SYS模式和USR模式相比,所用的寄存器组,都是一样的,但是增加了一些访问一些在USR模式下不能访问的资源。

      而SVC模式本身就属于特权模式,本身就可以访问那些受控资源,而且,比SYS模式还多了些自己模式下的影子寄存器,所以,相对SYS模式来说,可以访问资源的能力相同,但是拥有更多的硬件资源。

      所以,从理论上来说,虽然可以设置为SYS和SVC模式的任一种,但是从UBOOT方面考虑,其要做的事情是初始化系统相关硬件资源,需要获取尽量多的权限,以方便操作硬件,初始化硬件。

    从UBOOT的目的是初始化硬件的角度来说,设置为SVC模式,更有利于其工作。

    因此,此处将CPU设置为SVC模式。

  2. UBOOT作为一个引导程序来说,最终目的是为了启动的Linux内核的,在做好准备工作(即初始化硬件,准备好内核和根文件系统等)跳转到内核之前,本身就要满足一些条件,其中一个条件,就是要求CPU处于SVC模式的。

    所以,UBOOT在最初的初始化阶段,就将CPU设置为SVC模式,也是最合适的。

    [提示]提示

    关于满足哪些条件,详情请参考

    ARM Linux内核引导要求

    或者Linux的内核文档:

    kernel_source_root\documentation\arm\booting

    中也是同样的解释:

    CPU必须处于SVC模式

    所以,UBOOT在最初的初始化阶段,就将CPU设置为SVC模式,也是最合适的。

综上所述,UBOOT在初始化阶段,就应该将CPU设置为SVC模式。

第1章开始。详解

目录

1.1。设置CPU模式
1.1.1。globl
1.1.2。_开始
1.1.3。LDR
1.1.4。。字
1.1.5。.balignl
1.1.6。_TEXT_BASE _armboot_start
1.1.7。_bss_start _bss_end
1.1.8。FREE_RAM_END FREE_RAM_SIZE
1.1.9。IRQ_STACK_START FIQ_STACK_START
1.1.10。CPSR
1.1.11。BIC
1.1.12。ORR
1.1.13。MSR
1.2。关闭看门狗
1.2.1。pWTCON INTMOD INTMSK INTSUBMSK CLKDIVN
1.2.2。ldr pWTCON
1.2.3。MOV
1.2.4。海峡
1.3。关闭中断
1.3.1。设置INTMSK
1.3.2。设置INTSUBMSK
1.3.3。设置CLKDIVN
1.3.4。BL
1.4。设置堆栈SP指针
1.4.1。stack_setup
1.4.2。计算堆栈
1.4.3。clock_init
1.4.4。ADR
1.4.5。clear_bss
1.4.6。cal armboot大小
1.4.7。cal armboot大小
1.5。清除BSS段
1.5.1。clear_bss
1.5.2。清除css循环
1.5.3。ldr pc
1.5.4。cpu_init_crit
1.5.5。禁用MMU
1.5.6。清除位
1.5.7。bl lowlevel_init
1.6。异常中断处理
1.6.1。宏观stmia
1.6.2。cal reg值和存储
1.6.3。irq_save_user_regs irq_restore_user_regs
1.6.4。异常处理程序
1.6.5。发射
1.6.6。int_return

下面将详细解释的uboot中的start.S中中的每一行代码。详细到,每个指令的语法和含义,都进行详细讲解,使得此文读者可以真正搞懂具体的含义,即什么。

以及对于一些相关的问题,深入探究为何要这么做,即为什么。

对于UBOOT的start.S中,主要做的事情就是系统的各个方面的初始化。

从大的方面分,可以分成这几个部分:

  • 设置CPU模式
  • 关闭看门狗
  • 关闭中断
  • 设置堆栈SP指针
  • 清除BSS段
  • 异常中断处理

下面来对start.S中进行详细分析,看看每一个部分,是如何实现的。

1.1。设置CPU模式

1.1.1。globl,使链接器可见的符号。

/ * * armboot  -  ARM920 CPU内核的启动代码 * *版权所有(c)2001 Marius Gr鰃er <mag@sysgo.de> *版权所有(c)2002 Alex Z黳ke <azu@sysgo.de> *版权所有(c)2002 Gary Jennejohn <gj@denx.de> * *参见文件信用证作为此的人员列表 *项目。 * *这个程序是免费的软件; 你可以重新分配它和/或 *根据GNU通用公共许可证的条款进行修改 *由自由软件基金会发布; 版本2的 许可证,或(在您的选择)任何更新的版本。 * *这个程序是分发的,希望它是有用的, *但没有任何保证; 甚至没有隐含的保证 *适销性或适用于特定目的。见 * GNU通用公共许可证,以获得更多细节。 * *您应该收到GNU通用公共许可证的副本 *与这个程序一起; 如果没有,请写入自由软件 * Foundation,Inc.,59 Temple Place,Suite 330,Boston, * MA 02111-1307 USA * /#include <config.h>#include <version.h>/ * ************************************************** *********************** * *跳转矢量表如表3.1中[1] * ************************************************** *********************** * /.globl 1_start        

1

globl是个关键字,对应含义为:

http://re-eject.gbadev.org/files/GasARMRef.pdf

表1.1。全球性的语法

指示描述句法例。全球使 符号对链接器可见.global符号.global MyAsmFunc.globl.global相同.globl符号.globl MyOtherAsmFunc

所以,意思很简单,就是相当于Ç语言中的EXTERN,声明此变量,并且告诉链接器此变量是全局的,外部可以访问

所以,你可以看到

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\u-boot.lds

中,有用到此变量:

ENTRY(_start)

即指定入口为_start,而由下面的_start的含义可以得知,_start就是整个start.S中的最开始,即整个的uboot的代码的开始。

1.1.2。_开始

_start 1:b复位        

1

_start后面加上一个冒号 ':',表示其是一个标号标签,类似于Ç语言转到后面的标号。

而同时,_start的值,也就是这个代码的位置了,此处即为代码的最开始,相对的0的位置。

而此处最开始的相对的0位置,在程序开始运行的时候,如果是从NORFLASH启动,那么其地址是0,

_stat = 0

如果是重新移居代码之后,就是我们定义的值了,即,在

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\config.mk

中的:

TEXT_BASE = 0x33D00000

表示是代码段的基地址,即

_start = TEXT_BASE = 0x33D00000

关于标号的语法解释:

http://sourceware.org/binutils/docs-2.20/as/Labels.html#Labels

一个标签写成一个符号,紧跟着一个冒号`:'。符号然后表示活动位置计数器的当前值,并且例如是适当的指令操作数。如果您使用相同的符号表示两个不同的位置,则会发出警告:第一个定义会覆盖任何其他定义。

而_start标号后面的:

b复位

就是跳转到对应的标号为重的位置。

1.1.3。LDR

ldr 1pc,_undefined_instructionldr pc,_software_interruptldr pc,_prefetch_abortldr pc,_data_abortldr pc,_not_usedldr pc,_irqldr pc,_fiq        

1

LDR命令的语法为:

http://infocenter.arm.com/help/topic/com.arm.doc.dui0206hc/DUI0206HC_rvct_linker_and_utilities_guide.pdf

图1.1。LDR指令的语法

LDR指令的语法

http://wenku.baidu.com/view/f7cc280102020740be1e9bea.html

LDR指令的格式为:

LDR {条件}目的寄存器,<存储器地址>

LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取32位的字数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。该指令在程序设计中比较常用,且寻址方式灵活多样,请读者认真掌握。

指令示例:

LDR R0,[R1] ;将存储器地址为R1的字数据读入寄存器R0。

LDR R0,[R1,R2] ;将存储器地址为R1 + R2的字数据读入寄存器R0。

LDR R0,[R1,#8] ;将存储器地址作为R1 + 8的字数据读入寄存器R0。

LDR R0,[R1,R2]!;将存储器地址为R1 + R2的字数据读入寄存器R0,并将新地址R1 + R2写入R1。

LDR R0,[R1,#8]!;将存储器地址为R1 + 8的字数据读入寄存器R0,并将新地址R1 + 8写入R1。

LDR R0,[R1],R2 ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1 + R2写入R1。

LDR R0,[R1,R2,LSL#2]!将存储器地址为R1 + R2×4的字数据读入寄存器R0,并将新地址R1 + R2×4写入R1。

LDRR0,[R1],R2,LSL#2 ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1 + R2×4写入R1。

http://www.pczpg.com/a/2010/0607/11062.html

ARM RISC是结构,数据从内存到CPU之间的移动只能通过L / S指令来完成,也就是LDR / STR指令。

比如想把数据从内存中某处读取到寄存器中,只能使用LDR

比如:

ldr r0,0x12345678

就是把0×12345678这个地址中的值存放到R0中。

上面那些LDR的作用,以第一个_undefined_instruction为例,就是将地址为_undefined_instruction中的一个字的值,赋值给PC。

1.1.4。。字

_undefined_instruction:.word 1undefined_instruction_software_interrupt:.word software_interrupt_prefetch_abort:.word prefetch_abort_data_abort:.word data_abort_not_used:.word not_used_irq:.word irq_fiq:.word fiq        

1

http://blogold.chinaunix.net/u3/115924/showart_2280163.html

.word .word expr {,expr} ...分配一段字内存单元,并用expr初始化字内存单元(32bit)

http://re-eject.gbadev.org/files/GasARMRef.pdf

表1.2。.word的语法

指示描述句法例。字定义单词expr(32位数).word expr {,...}.word 144511,0x11223

所以上面的含义,以_undefined_instruction为例,就是,此处分配了一个字的32位= 4 =字节的地址空间,里面存放的值是undefined_instruction。

而此处_undefined_instruction也就是该地址空间的地址了用Ç语言来表达就是:

_undefined_instruction =&undefined_instruction

* _undefined_instruction = undefined_instruction

在后面的代码,我们可以看到,undefined_instruction也是一个标号,即一个地址值,对应着就是在发生“未定义指令”的时候,系统所要去执行的代码。

(其他几个对应的“软件中断”,“预取指错误”,“数据错误”,“未定义”,“(普通)中断”,“快速中断”,也是同样的做法,跳转到对应的位置执行对应的代码。)

所以:

ldr pc,标号1......标号1:.word标号2......标号2:......(具体要执行的代码)                

的意思就是,将地址为标号1中内容载入到个人计算机,而地址为标号1中的内容,正好装的是标号2。

用Ç语言表达其实很简单:

PC = *(标号1)=标号2

对PC赋值,即是实现代码跳转,所以整个这段汇编代码的意思就是:

跳转到标号2的位置,执行对应的代码。

1.1.5。.balignl

.balignl 116,0xdeadbeef        

1

balignl这个标号的语法及含义:

http://re-eject.gbadev.org/files/GasARMRef.pdf

表1.3。balignl的语法

指示描述句法例.balignlWord将以下代码对齐对齐字节边界(默认= 4)。填充跳过词语与 填充缺省值= 0或NOP)。如果跳过的字节数大于max,则不要对齐(default = alignment)。.balignl {alignment} {,fill} {,max}.balignl

所以意思就是,接下来的代码,都要16字节对齐,不足之处,用0xdeadbeef填充。

其中关于所要填充的内容0xdeadbeef,刚开始没看懂是啥意思,后来终于搞懂了。

经过(等)多位网友提示和纠正,觉得这样解释会更加合理些:

此处0xdeadbeef本身没有真正的意义,但是很明显,字面上的意思是,(坏)死的牛肉。

虽然其本身没有实际意义,但是其是在十六进制下,能表示出来的,为数不多的,可读的单词之一了。

另外一个相对常见的是:0xbadc0de,意思是坏代码,坏的代码,注意其中的o是0,因为十六进制中是没有o的。

这些“单词”,相对的作用是,使得读代码的人,以及在查看程序运行结果时,容易看懂,便于引起注意。

而关于自己之前,随意杜撰出来的,希望起到搞笑作用,表示好牛肉(好牛牛)的0xgoodbeef,实际上,在十六进制下,会出错的,因为十六进制下没有o和克这两个字母。

1.1.6。_TEXT_BASE _armboot_start

/ * ************************************************** *********************** * *启动代码(复位向量) * *只有当我们不从内存开始时,才能做重要的init 将armboot移动到ram *设置堆栈 *跳到第二阶段 * ************************************************** *********************** * /_TEXT_BASE 1:.word TEXT_BASE2.globl _armboot_start_armboot_start:.word _start        

1

此处和上面的类似,_TEXT_BASE是一个标号地址,此地址中是一个字类型的变量,变量名是TEXT_BASE,此值见名知意,是文本的基础上,即代码的基地址,可以在

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\config.mk

中找到其定义:

TEXT_BASE = 0x33D00000

2

同理,此含义可用Ç语言表示为:

*(_ armboot_start)= _start

1.1.7。_bss_start _bss_end

/ * *这些在板特定的链接描述文件中定义。 * /.globl _bss_start_bss_start:.word __bss_start1.globl _bss_end_bss_end:剑_end        

1

关于_bss_start和_bss_end都只是两个标号,对应着此处的地址。

而两个地址里面分别存放的值是__bss_start和_end,这两个的值,根据注释所说,是定义在开发板相关的链接脚本里面的,我们此处的开发板相关的链接脚本是:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\board\EmbedSky\u-boot.lds

其中可以找到__bss_start和_end的定义:

__bss_start =。.bss:{*(。bss)}_end =。                

而关于_bss_start和_bss_end定义为.glogl即全局变量,是因为UBOOT的其他源码中要用到这两个变量,详情请自己去搜索源码。

1.1.8。FREE_RAM_END FREE_RAM_SIZE

.globl FREE_RAM_ENDFREE_RAM_END:.word 0x0badc0de1.globl FREE_RAM_SIZEFREE_RAM_SIZE:.word 0x0badc0de        

1

关于FREE_RAM_END和FREE_RAM_SIZE,这里只是两个标号,之所以也是声明为全局变量,是因为UBOOT的源码中会用到这两个变量。

但是这里有点特别的是,这两个变量,将在本源码start.S中中的后面要用到,而在后面用到这两个变量之前,UBOOT的Ç源码中,会先去修改这两个值,具体的逻辑是:

本文件start.S中中,后面有这两句:

ldr pc,_start_armboot_start_armboot:.word start_armboot                

意思很明显,就是去调用函数的start_armboot。

而的start_armboot函数是在:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\lib_arm\board.c

中:

init_fnc_t * init_sequence [] = {cpu_init,/ *基本的cpu依赖设置* /......空值,};void start_armboot(void){init_fnc_t ** init_fnc_ptr;......for(init_fnc_ptr = init_sequence; * init_fnc_ptr; ++ init_fnc_ptr){if((* init_fnc_ptr)()!= 0){hang();}}......}                

即在的start_armboot去调用了cpu_init。

cpu_init函数是在:

u-boot-1.1.6_20100601\opt\EmbedSky\u-boot-1.1.6\cpu\arm920t\cpu.c

中:

cpu_init源码。

int cpu_init(void){    / *     *必要时建立堆栈     * /#ifdef CONFIG_USE_IRQ    IRQ_STACK_START = _armboot_start  -  CFG_MALLOC_LEN  -  CFG_GBL_DATA_SIZE  -  4;    FIQ_STACK_START = IRQ_STACK_START  -  CONFIG_STACKSIZE_IRQ;    FREE_RAM_END = FIQ_STACK_START  -  CONFIG_STACKSIZE_FIQ  -  CONFIG_STACKSIZE;    FREE_RAM_SIZE = FREE_RAM_END  -  PHYS_SDRAM_1;#其他        FREE_RAM_END = _armboot_start  -  CFG_MALLOC_LEN  -  CFG_GBL_DATA_SIZE  -  4  -  CONFIG_STACKSIZE;    FREE_RAM_SIZE = FREE_RAM_END  -  PHYS_SDRAM_1;#万一    返回0;}                    

在cpu_init中,根据我们的一些定义,比如堆栈大小等等,去修改了IRQ_STACK_START,FIQ_STACK_START,FREE_RAM_END和FREE_RAM_SIZE的值。

至于为何这么修改,后面遇到的时候会具体再解释。

1.1.9。IRQ_STACK_START FIQ_STACK_START

#ifdef CONFIG_USE_IRQ/ * IRQ堆栈内存(在运行时计算)* /.globl IRQ_STACK_STARTIRQ_STACK_START:.word 0x0badc0de1/ * IRQ堆栈内存(在运行时计算)* /.globl FIQ_STACK_STARTFIQ_STACK_START:.word 0x0badc0de#万一        

1

同上,IRQ_STACK_START和FIQ_STACK_START,也是在cpu_init中用到了。

不过此处,是只有当定义了宏CONFIG_USE_IRQ的时候,才用到这两个变量,其含义也很明显,

只有用到了中断IRQ,才会用到中断的堆栈,才有中端堆栈的起始地址。

快速中断FIQ,同理。

1.1.10。CPSR

/ * *实际复位代码 * /重启:/ * *将cpu设置为SVC32模式 * /mrs 1r0,cpsr2        

2

CPSR是当前的程序状态寄存器(当前程序状态寄存器),

而SPSR是保存的程序状态寄存器(保存的程序状态寄存器)。

具体细节,可参考:

ARM7体系结构

http://www.csie.nctu.edu.tw/~wjtsai/EmbeddedSystemDesign/Ch2-bootloader.pdf

图1.2。CPSR / SPSR的位域结构

CPSR / SPSR的位域结构

表1.4。CPSR位域

31三十2928---76-43210说明ñžCV 一世F M4M3M2M1M0  00000User26模式 00001FIQ26模式 00010IRQ26模式 00011SVC26模式 10000用户模式 10001FIQ模式 10010IRQ模式 10011SVC模式 10111ABT模式 11011UND模式

1

MRS - 从状态寄存器移动

MRS指令的语法为:

四,程序状态寄存器访问指令

1,MRS指令

MRS指令的格式为:

通用寄存器,程序状态寄存器(CPSR或SPSR)

MRS指令用于将程序状态寄存器的内容传送到通用寄存器中该指令一般用在以下两种情况:

Ⅰ。当需要改变程序状态寄存器的内容时​​,可用MRS将程序状态寄存器的内容读入通用寄存器,修改后再写回程序状态寄存器。

Ⅱ。当在异常处理或进程切换时,需要保存程序状态寄存器的值,可先用该指令读出程序状态寄存器的值,然后保存。

指令示例:

MRS R0,CPSR;传送CPSR的内容到R0

MRS R0,SPSR;传送SPSR的内容到R0“

所以,上述汇编代码含义为,将CPSR的值赋给R0寄存器。

1.1.11。BIC

bic 1r0,r0,#0x1f        

1

BIC指令的语法是:

16,BIC指令

BIC指令的格式为:

BIC {条件} {S}目的寄存器,操作数1,操作数2

BIC指令用于清除操作数1​​的某些位,并把结果放置到目的寄存器中。操作数1应是一个寄存器,

操作数2可以是一个寄存器,被移位的寄存器,或一个立即数。操作数2为32位的掩码,如果在掩码中设置了某一位,则清除这一位。未设置的掩码位保持不变。

而为0x1F = 11111b

所以,此行代码的含义就是,清除R0的位[4:0]位。

1.1.12。ORR

orr 1r0,r0,#0xd3        

1

ORR指令的语法是:

14,ORR指令

ORR指令的格式为:

ORR {条件} {S}目的寄存器,操作数1,操作数2

ORR指令用于在两个操作数上进行逻辑或运算,并把结果放置到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个立即数。该指令常用于设置操作数1的某些位。

指令示例:

ORR R0,R0,#3;该指令设置R0的0,1位,其余位保持不变。

所以此行汇编代码的含义为:

而0xd3 = 1101 0111 [4:0]位。

将R0与0xd3算数或运算,然后将结果给R0,即把R0的位[7:6]和位[4]和位[2:0]置为1。

1.1.13。MSR

msr 1cpsr,r0        

1

MSR - 移至状态寄存器

MSR的指令格式是:

四,程序状态寄存器访问指令

......

2,MSR指令

MSR指令的格式为:

MSR {条件}程序状态寄存器(CPSR或SPSR)_ <域>,操作数

MSR指令用于将操作数的内容传送到程序状态寄存器的特定域中。其中,操作数可以为通用寄存器或立即数。<域>用于设置程序状态寄存器中需要操作的位,32位的程序状态寄存器可分为4个域:

位[31:24]为条件标志位域,用˚F表示;

位[23:16]为状态位域,用小号表示;

位[15:8]为扩展位域,用X表示;

位[7:0]为控制位域,用Ç表示;

该指令通常用于恢复或改变程序状态寄存器的内容,在使用时,一般要在MSR指令中指明将要操作的域。

指令示例:

MSR CPSR,R0;传送R0的内容到CPSR

MSR SPSR,R0;传送R0的内容到SPSR

MSR CPSR_c,R0;传送R0的内容到SPSR,但仅仅修改CPSR中的控制位域

此行汇编代码含义为,将R0的值赋给CPSR。

所以,上面四行汇编代码的含义就很清楚了。

先是把CPSR的值放到R0寄存器中,然后清除位[4:0],然后再或上

0xd3 = 11 0 10111b

表1.5。CPSR = 0xD3的位域及含义

CPSR位域76五43210位域含义一世F M4M3M2M1M00xD311010011对应含义关闭中断IRQ关闭快速中断FIQ 设置CPU为SVC模式,这和上面代码注释中的“将cpu设置为SVC32模式”,也是一致的。

关于为何设置CPU为SVC模式,而不是设置为其他模式,请参阅本文档后面的章节(开始的链接)。


原创粉丝点击