配色: 字号:
ROW和RAW的了解
2012-08-06 | 阅:  转:  |  分享 
  
一直以来对于ARM体系中所描述的RO,RW和ZI数据存在似是而非的理解,这段时间对其仔细了解了一番,发现了一些规律,理解了一些以前书本上有的但是不理解的东西,我想应该有不少人也有和我同样的困惑,因此将我的一些关于RO,RW和ZI的理解写出来,希望能对大家有所帮助。

要了解RO,RW和ZI需要首先了解以下知识:

ARM程序的组成

此处所说的“ARM程序”是指在ARM系统中正在执行的程序,而非保存在ROM中的bin映像(image)文件,这一点清注意区别。

一个ARM程序包含3部分:RO,RW和ZI

RO是程序中的指令和常量

RW是程序中的已初始化变量

ZI是程序中的未初始化的变量

由以上3点说明可以理解为:

RO就是readonly,

RW就是read/write,

ZI就是zero

ARM映像文件的组成

所谓ARM映像文件就是指烧录到ROM中的bin文件,也成为image文件。以下用Image文件来称呼它。

Image文件包含了RO和RW数据。

之所以Image文件不包含ZI数据,是因为ZI数据都是0,没必要包含,只要程序运行之前将ZI数据所在的区域一律清零即可。包含进去反而浪费存储空间。

Q:为什么Image中必须包含RO和RW?

A:因为RO中的指令和常量以及RW中初始化过的变量是不能像ZI那样“无中生有”的。

ARM程序的执行过程

从以上两点可以知道,烧录到ROM中的image文件与实际运行时的ARM程序之间并不是完全一样的。因此就有必要了解ARM程序是如何从ROM中的image到达实际运行状态的。

实际上,RO中的指令至少应该有这样的功能:

1.将RW从ROM中搬到RAM中,因为RW是变量,变量不能存在ROM中。

2.将ZI所在的RAM区域全部清零,因为ZI区域并不在Image中,所以需要程序根据编译器给出的ZI地址及大小来将相应得RAM区域清零。ZI中也是变量,同理:变量不能存在ROM中

在程序运行的最初阶段,RO中的指令完成了这两项工作后C程序才能正常访问变量。否则只能运行不含变量的代码。

说了上面的可能还是有些迷糊,RO,RW和ZI到底是什么,下面我将给出几个例子,最直观的来说明RO,RW,ZI在C中是什么意思。

1;RO

看下面两段程序,他们之间差了一条语句,这条语句就是声明一个字符常量。因此按照我们之前说的,他们之间应该只会在RO数据中相差一个字节(字符常量为1字节)。

Prog1:

#include

voidmain(void)

{

;

}

Prog2:

#include

constchara=5;

voidmain(void)

{

;

}

Prog1编译出来后的信息如下:

================================================================================

CodeRODataRWDataZIDataDebug

948600960GrandTotals

================================================================================

TotalROSize(Code+ROData)1008(0.98kB)

TotalRWSize(RWData+ZIData)96(0.09kB)

TotalROMSize(Code+ROData+RWData)1008(0.98kB)

================================================================================

Prog2编译出来后的信息如下:

================================================================================

CodeRODataRWDataZIDataDebug

948610960GrandTotals

================================================================================

TotalROSize(Code+ROData)1009(0.99kB)

TotalRWSize(RWData+ZIData)96(0.09kB)

TotalROMSize(Code+ROData+RWData)1009(0.99kB)

================================================================================

以上两个程序编译出来后的信息可以看出:

Prog1和Prog2的RO包含了Code和ROData两类数据。他们的唯一区别就是Prog2的ROData比Prog1多了1个字节。这正和之前的推测一致。

如果增加的是一条指令而不是一个常量,则结果应该是Code数据大小有差别。

2;RW

同样再看两个程序,他们之间只相差一个“已初始化的变量”,按照之前所讲的,已初始化的变量应该是算在RW中的,所以两个程序之间应该是RW大小有区别。

Prog3:

#include

voidmain(void)

{

;

}

Prog4:

#include

chara=5;

voidmain(void)

{

;

}

Prog3编译出来后的信息如下:

================================================================================

CodeRODataRWDataZIDataDebug

948600960GrandTotals

================================================================================

TotalROSize(Code+ROData)1008(0.98kB)

TotalRWSize(RWData+ZIData)96(0.09kB)

TotalROMSize(Code+ROData+RWData)1008(0.98kB)

================================================================================

Prog4编译出来后的信息如下:

================================================================================

CodeRODataRWDataZIDataDebug

948601960GrandTotals

================================================================================

TotalROSize(Code+ROData)1008(0.98kB)

TotalRWSize(RWData+ZIData)97(0.09kB)

TotalROMSize(Code+ROData+RWData)1009(0.99kB)

================================================================================

可以看出Prog3和Prog4之间确实只有RWData之间相差了1个字节,这个字节正是被初始化过的一个字符型变量“a”所引起的。

3;ZI

再看两个程序,他们之间的差别是一个未初始化的变量“a”,从之前的了解中,应该可以推测,这两个程序之间应该只有ZI大小有差别。

Prog3:

#include

voidmain(void)

{

;

}

Prog4:

#include

chara;

voidmain(void)

{

;

}

Prog3编译出来后的信息如下:

================================================================================

CodeRODataRWDataZIDataDebug

948600960GrandTotals

================================================================================

TotalROSize(Code+ROData)1008(0.98kB)

TotalRWSize(RWData+ZIData)96(0.09kB)

TotalROMSize(Code+ROData+RWData)1008(0.98kB)

================================================================================

Prog4编译出来后的信息如下:

================================================================================

CodeRODataRWDataZIDataDebug

948600970GrandTotals

================================================================================

TotalROSize(Code+ROData)1008(0.98kB)

TotalRWSize(RWData+ZIData)97(0.09kB)

TotalROMSize(Code+ROData+RWData)1008(0.98kB)

================================================================================

编译的结果完全符合推测,只有ZI数据相差了1个字节。这个字节正是未初始化的一个字符型变量“a”所引起的。

注意:如果一个变量被初始化为0,则该变量的处理方法与未初始化华变量一样放在ZI区域。

即:ARMC程序中,所有的未初始化变量都会被自动初始化为0。

总结:

1;C中的指令以及常量被编译后是RO类型数据。

2;C中的未被初始化或初始化为0的变量编译后是ZI类型数据。

3;C中的已被初始化成非0值的变量编译后市RW类型数据。

附:

程序的编译命令(假定C程序名为tst.c):

armcc-c-otst.otst.c

armlink-noremove-elf-nodebug-infototals-infosizes-map-listaa.map-otst.elftst.o

编译后的信息就在aa.map文件中。

ROM主要指:NANDFlash,NorFlash

RAM主要指:PSRAM,SDRAM,SRAM,DDRAM



Image$$??$$Limit的含义



对于刚学习ARM的人来说,如果分析它的启动代码,往往不明白下面几个变量的含义:|Image$$RO$$Limit|、|Image$$RW$$Base|、|Image$$ZI$$Base|。



首先申明我使用的调试软件为ADS1.2,当我们把程序编写好以后,就要进行编译和链接了,在ADS1.2中选择MAKE按钮,会出现一个ErrorsandWarnings的对话框,在该栏中显示编译和链接的结果,如果没有错误,在文件的最后应该能看到Imagecomponentsizes,后面紧跟的依次是Code,ROData,RWData,ZIData,Debug各个项目的字节数,最后会有他们的一个统计数据:



Code163632,ROData20939,RWData53,ZIData17028



TatalROsize(Code+ROData)184571(180.25kB)



TatalRWsize(RWData+ZIData)17081(16.68kB)



TatalROMsize(Code+ROData+RWData)184624(180.30kB)



后面的字节数是根据用户不同的程序而来的,下面就以上面的数据为例来介绍那几个变量的计算。



在ADS的DebugSettings中有一栏是Linker/ARMLinker,在output选项中有一个RObase选项,下面应该有一个地址,我这里是0x0c100000,后面的RWbase地址是0x0c200000,然后在Options选项中有Imageentrypoint,是一个初始程序的入口地址,我这里是0x0c100000。



有了上面这些信息我们就可以完全知道这几个变量是怎么来的了:



|Image$$RO$$Base|=Imageentrypoint=0x0c100000;表示程序代码存放的起始地址



|Image$$RO$$Limit|=程序代码起始地址+代码长度+1=0x0c100000+TatalROsize+1



=0x0c100000+184571+1=0x0c100000+0x2D0FB+1



=0x0c12d0fc



|Image$$RW$$Base|=0x0c200000;由RWbase地址指定



|Image$$RW$$Limit|=|Image$$RW$$Base|+RWData53=0x0c200000+0x37(4的倍数,0到55,共56个单元)



=0x0c200037



|Image$$ZI$$Base|=|Image$$RW$$Limit|+1=0x0c200038



|Image$$ZI$$Limit|=|Image$$ZI$$Base|+ZIData17028



=0x0c200038+0x4284



=0x0c2042bc



也可以由此计算:



|Image$$ZI$$Limit|=|Image$$RW$$Base|+TatalRWsize(RWData+ZIData)17081



=0x0c200000+0x42b9+3(要满足4的倍数)



=0x0c2042bc

Part1简介



一概述



Scatterfile(分散加载描述文件)用于armlink的输入参数,他指定映像文件内部各区域的download与运行时位置。Armlink将会根据scatterfile生成一些区域相关的符号,他们是全局的供用户建立运行时环境时使用。(注意:当使用了scatterfile时将不会生成以下符号Image$$RW$$Base,Image$$RW$$Limit,Image$$RO$$Base,Image$$RO$$Limit,Image$$ZI$$Base,andImage$$ZI$$Limit)



二什么时候使用scatterfile



当然首要的条件是你在利用ADS进行项目开发,下面我们看看更具体的一些情况。



1存在复杂的地址映射:例如代码和数据需要分开放在在多个区域。



2存在多种存储器类型:例如包含Flash,ROM,SDRAM,快速SRAM。我们根据代码与数据的特性把他们放在不同的存储器中,比如中断处理部分放在快速SRAM内部来提高响应速度,而把不常用到的代码放到速度比较慢的Flash内。



3函数的地址固定定位:可以利用Scatterfile实现把某个函数放在固定地址,而不管其应用程序是否已经改变或重新编译。



4利用符号确定堆与堆栈:



5内存映射的IO:采用scatterfile可以实现把某个数据段放在精确的地指处。



因此对于嵌入式系统来说scatterfile是必不可少的,因为嵌入式系统采用了ROM,RAM,和内存映射的IO。



三scatterfile实例



1简单的内存映射



LOAD_ROM0x00000x8000



{



EXEC_ROM0x00000x8000



{



(RO)



}



RAM0x100000x6000



{



(RW,ZI)



}



}







LOAD_ROM(下载区域名称)0x0000(下载区域起始地址)0x8000(下载区域最大字节数)



{



EXEC_ROM(第一执行区域名称)0x0000(第一执行区域起始地址)0x8000(第一执行区域最大字节数)



{



(RO(代码与只读数据))



}



RAM(第二执行区域名称)0x10000(第二执行区域起始地址)0x6000(第二执行区域最大字节数)



{



(RW(读写变量),ZI(未初始化变量))



}



}



2复杂内存映射



LOAD_ROM_10x0000



{



EXEC_ROM_10x0000



{



program1.o(RO)



}



DRAM0x180000x8000



{



program1.o(RW,ZI)



}



}







LOAD_ROM_20x4000



{



EXEC_ROM_20x4000



{



program2.o(RO)



}



SRAM0x80000x8000



{



program2.o(RW,ZI)



}



}







LOAD_ROM_10x0000(下载区域一起始地址)



{



EXEC_ROM_10x0000(第一执行区域开始地址)



{



program1.o(RO)(program1.o内的Code与ROdata放在第一执行区域)



}



DRAM0x18000(第二执行区域开始地址)0x8000(第二执行区域最大字节数)



{



program1.o(RW,ZI)(program1.o内的RWdata与ZIdata放在第二执行区域)



}



}



LOAD_ROM_20x4000(下载区域二起始地址)



{



EXEC_ROM_20x4000



{



program2.o(RO)(program2.o内的Code与ROdata放在第一执行区域)



}



SRAM0x80000x8000



{



program2.o(RW,ZI)(program2.o内的RWdata与ZIdata放在第二执行区域)



}



}



Part2基本语法



2.1BNF符号与语法



":由引号赖标示的符号保持其字面原意,如A””B标示AB。



A::=B:定义A为B。



[A]:标示可选部分,如A[B]C用来标示ABC或AC。



A:用来标示A可以重复任意次,如A可标示A,AA,AAA,…



A:同A。



A|B:用来标示选择其一,不能全选。如A|B用来标示A或者B。



(AB):标示一个整体,当和|符号或复杂符号的多次重复一起使用时尤其强大,如(AB)(C|D)标示ABC,ABD,ABABC,ABABD,…



2.2分散加载文件各部分描述



(2.1)



如图2.1所示为一个完整的分散加载脚本描述结构图。下面我们对图示中各个部分进行讲述。



2.2.1加载区描述



每个加载区有:



ó名称:供连接器确定不同下载区域



ó基地址:相对或绝对地址



ó属性:可选



ó最大字节数:可选



ó执行区域列:确定执行时各执行区域的类型与位置



load_region_name(base_address|(""offset))[attribute_list][max_size]



"{"



execution_region_description



"}"







load_region_name:下载区域名称,最大有效字符数31。(并不像执行区域段名用于Load$$region_name,而是仅仅用于标示下载区域)。



base_address:本区域内部目标被连接到的地址(按字对齐)。



offset:相对前一个下载区域的偏移量(4的整数倍,如果为第一个区域)。







2.2.2执行区描述



每个执行区有:



ó名称:供连接器确定不同下载区域



ó基地址:相对或绝对地址



ó属性:确定执行区域的属性



ó最大字节数:可选



ó输入段:确定放在该执行区域的模块



exec_region_name(base_address|""offset)[attribute_list][max_size]



"{"



input_section_description



"}"



exec_region_name:执行区域名称,最大有效字符数31。



base_address:本执行区域目标要被联接到的位置,按字对齐。



offset:相对于前一个执行区域结束地址的偏移量,4的整数倍;如果没有前继之能够行区域(本执行区域为该下载区域的第一个执行区域),则该偏移量是相对于该下载区域的基址偏移量。



attribute_list:PI,OVERLAY,ABSOLUTE,FIXED,UNINIT。



PI:位置独立。



OVERLAY:覆盖。



ABSOLUTE:绝对地址。



FIXED:固定地址,下载地址与执行地址具有该地址指示确定。



UNINIT:未初始化数据。



RELOC:无法明确指定执行区域具有该属性,而只能通过继承前一个执行区或父区域获得。



对于PI,OVERLAY,ABSOLUTE,FIXED,我们只能选择一个,缺省属性为ABSOLUTE。一个执行区域要么直接继承其前面的执行区域的属性或者具有属性为ABSOLUTE。



具有PI,OVERLAY,RELOC属性的执行区域允许其地址空间重叠,对于BSOLUTE,FIXED属性执行区域地址空间重叠Armlink会报错。



max_size:可选,他用于指使Armlink在实际分配空间大于指定值时报错。



input_section_description:指示输入段的内容。







2.2.3输入段描述



输入段:



ó模块名:目标文件名,库成员名,库文件名。名称可以使用通配符。



ó输入段名,或输入段属性(READ-ONLY,CODE)。



module_select_pattern



["("



(""input_section_attr|input_section_pattern)



([","]""input_section_attr|","input_section_pattern))



")"]



2.2.3.1



module_select_pattern:选择的模块名称(目标文件,库文件成员,库文件),模块名可以使用通配符(匹配任意多个字符,?匹配任意一个字符),名称不区分字母大小写,它是供选择的样本。



例1:libtx.a(RO)



libtx.a为threadX库文件。



例2:tx_ill.o(INIT)



tx_ill.o为threadX中断向量目标文件。



2.2.3.2



input_section_attr:输入段属性选择子,每个选择子以””开头,选择子不区分大小写字符。



选择子可选RO-CODE,RO-DATA,RO(selectsbothRO-CODEandRO-DATA),RW-DATA,RW-CODE,RW(selectsbothRW-CODEandRW-DATA),ZI,ENTRY(thatisasectioncontaininganENTRYpoint)。



以下同义词可以选择:CODE(forRO-CODE),CONST(forRO-DATA),TEXT(forRO),DATA(forRW),BSS(forZI)。



还有两个伪属性:FIRST,LAST。如果各段的先后顺序比较重要时,可以使用FIRST,LAST标示一个执行区域的第一个和最后一个段。



例1:os_main_init.o(INIT,FIRST)



FIRST表示放于本执行区域的开始处。



例2:libtx.a(RO)



RO表示libtx.a的只读部分。



2.2.3.3



input_section_pattern:输入段名。



例1:os_main_init.o(INIT,FIRST)



INIT为os_main_init.o的一个段。



例2:os_stackheap.o(heap)



heap为os_stackheap.o的一个段。



例3:os_stackheap.o(stack)



stack为os_stackheap.o的一个段。



















//--------------------------------------------------------------------------------------------------------------------------



分散加载文件事例







ADS下的分散加载文件应用实例



load_region_namestart_address|""offset[attributes][max_size]



{



execution_region_namestart_address|""offset[attributes][max_size]



{



module_select_pattern["("



(""input_section_attr|input_section_pattern)



([","]""input_section_attr|","input_section_pattern))



")"]



}



}



load_region:加载区,用来保存永久性数据(程序和只读变量)的区域;



execution_region:执行区,程序执行时,从加载区域将数据复制到相应执行区后才能被正确执行;



load_region_name:加载区域名,用于“Linker”区别不同的加载区域,最多31个字符;



start_address:起始地址,指示区域的首地址;



offset:前一个加载区域尾地址+offset做为当前的起始地址,且“offset”应为“0”或“4”的倍数;



attributes:区域属性,可设置如下属性:



PI与地址无关方式存放;



RELOC重新部署,保留定位信息,以便重新定位该段到新的执行区;



OVERLAY覆盖,允许多个可执行区域在同一个地址,ADS不支持;



ABSOLUTE绝对地址(默认);



max_size:该区域的大小;



execution_region_name:执行区域名;



start_address:该执行区的首地址,必须字对齐;



offset:同上;



attributes:同上;



PI与地址无关,该区域的代码可任意移动后执行;



OVERLAY覆盖;



ABSOLUTE绝对地址(默认);



FIXED固定地址;



UNINIT不用初始化该区域的ZI段;



module_select_pattern:目标文件滤波器,支持通配符“”和“?”;



.o匹配所有目标,(或“.ANY”)匹配所有目标文件和库。



input_section_attr:每个input_section_attr必须跟随在“+”后;且大小写不敏感;



RO-CODE或CODE



RO-DATA或CONST



RO或TEXT,selectsbothRO-CODEandRO-DATA



RW-DATA



RW-CODE



RW或DATA,selectsbothRW-CODEandRW-DATA



ZI或BSS



ENTRY,thatisasectioncontaininganENTRYpoint.



FIRST,用于指定存放在一个执行区域的第一个或最后一个区域;



LAST,同上;



input_section_pattern:段名;



汇编中指定段:



AREAvectors,CODE,READONLY



C中指定段:



#pragmaarmsection[sort_type[[=]"name"]][,sort_type="name"]



sort_type:code、rwdata、rodata、zidata



如果“sort_type”指定了但没有指定“name”,那么之前的修改的段名将被恢复成默认值。



#pragmaarmsection//恢复所有段名为默认设置。



应用:



#pragmaarmsectionrwdata="SRAM",zidata="SRAM"



staticOS_STKSecondTaskStk[256];//“rwdata”“zidata”将定位在“sram”段中。



#pragmaarmsection//恢复默认设置



分散加载文件中定义如下:



Exec_Sram0x800000000x40000



{



(sram)



}



“PI”属性使用示例:



LR_10x010000PI;Thefirstloadregionisat0x010000.



{



ER_RO0;ThePIattributeisinheritedfromparent.



;Thedefaultexecutionaddressis0x010000,butthecodecanbemoved.



{



(RO);AlltheROsectionsgohere.



}



ER_RW0ABSOLUTE;PIattributeisoverriddenbyABSOLUTE.



{



(RW);TheRWsectionsareplacednext.Theycannotbemoved.



}



ER_ZI0;ER_ZIregionplacedafterER_RWregion.



{



(ZI);AlltheZIsectionsareplacedconsecutivelyhere.



}



}



LR_10x010000;Thefirstloadregionisat0x010000.



{



ER_RO0;DefaultABSOLUTEattributeisinheritedfromparent.Theexecutionaddress



;is0x010000.Thecodeandrodatacannotbemoved.



{



(RO);AlltheROsectionsgohere.



}



ER_RW0x018000PI;PIattributeoverridesABSOLUTE



{



(RW);TheRWsectionsareplacedat0x018000andtheycanbemoved.



}



ER_ZI0;ER_ZIregionplacedafterER_RWregion.



{



(ZI);AlltheZIsectionsareplacedconsecutivelyhere.



}



}



程序中对某区域地址等的引用方法:



Load$$region_name$$BaseLoadaddressoftheregion.



Image$$region_name$$BaseExecutionaddressoftheregion.



Image$$region_name$$LengthExecutionregionlengthinbytes(multipleof4).



Image$$region_name$$LimitAddressofthebytebeyondtheendoftheexecutionregion.



Image$$region_name$$ZI$$BaseExecutionaddressoftheZIoutputsectioninthisregion.



Image$$region_name$$ZI$$LengthLengthoftheZIoutputsectioninbytes(multipleof4).



Image$$region_name$$ZI$$LimitAddressofthebytebeyondtheendoftheZIoutputsectionintheexecutionregion.



SectionName$$BaseInputAddressofthestartoftheconsolidatedsectioncalledSectionName.



SectionName$$LimitInputAddressofthebytebeyondtheendoftheconsolidatedsectioncalledSectionName.



Load:加载区,即存放地址;



Image:执行区,即运行地址;



Base:区首地址;



Limit:区尾地址;



Length:区长度;



region_name:RO、RW、ZI、load_region_name、execution_region_name;



例如:



“RAM1”区域的首地址:Image$$RAM1$$Base



上例中“sram”段首地址:sram$$Base



汇编引用示例:



IMPORT|Load$$Exec_RAM1$$Base|//Exec_RAM1为“RW”段



IMPORT|Image$$Exec_RAM1$$Base|



IMPORT|Image$$Exec_RAM1$$Length|



IMPORT|Image$$Exec_RAM1$$Limit|



LDRR0,=|Load$$Exec_RAM1$$Base|



LDRR1,=|Image$$Exec_RAM1$$Base|



LDRR2,=|Image$$Exec_RAM1$$Limit|



0



CMPR1,R2



LDRCCR3,[R0],#4



STRCCR3,[R1],#4



BCC%b0



C引用:



externunsignedcharLoad$$Exec_RAM1$$Base;



externunsignedcharImage$$Exec_RAM1$$Base;



externunsignedcharImage$$Exec_RAM1$$Length;



voidMoveRO(void)



{



unsignedcharpsrc,pdst;



unsignedintcount;



count=(unsignedint)&Image$$Exec_RAM1$$Length;



psrc=(unsignedchar)&Load$$Exec_RAM1$$Base;



pdst=(unsignedchar)&Image$$Exec_RAM1$$Base;



while(count--){



pdst=psrc;



}



}



加载文件示例一:



起始地址大小



ROM:0x00000000256K;0x1fc保留为加密字,程序在ROM中运行;



RAM0x4000000016K;用于全局变量及任务堆栈;



SRAM0x80000000512K;SRAM速度慢,主要用于存放大的数据表;



LOAD_ROM10x000000000x1f8;指定该加载区域首地址、大小



{



EXEC_ROM100x1f8;没有前一加载区域,所以该执行区域首地址为加载去首地址



;并指定该区域长度



{



Startup.o(vectors,FIRST);目标文件的“vectors”段放在该执行区域的第一段



irq.o(RO);目标文件的所有“RO”段放在该执行区域



}



}



LOAD_ROM20x00000200;第二个加载区域



{



EXEC_ROM200x3e600



{



(RO);所有目标文件和库文件中的“RO”段存放在该区域



}



RAM10x400000000x4000



{



(RW,ZI);所有目标文件和库文件的“RW”和“ZI”段存放在该区域



}



SRAM20x800000000x80000



{



(sram);所有目标文件中的“sram”段存放在该区域



}



}



示例二:



“iap.o”定义在“Exec_RAM1”中运行,所以设置“PI”属性;



在调用“iap.c”中函数之前应该将其从“Load$$Exec_IAP$$Base”复制到指定的“Exec_RAM1”区域;



Load_region10x000000000x1fc



{



EXEC_ROM10



{



Startup.o(vectors,FIRST)



irq.o(RO)



}



}



Load_region20x000002000x3e600



{



EXEC_ROM20



{



(RO)



}



Exec_IAP0PI//可能引起链接器未使用该属性警告,忽略



{



iap.o(RO)



}



Exec_RAM10x400000000x4000



{



(RW,ZI)



}



Exec_Sram0x800000000x40000



{



(SRAM)



}



}



//移动“IAP.o”中的所有函数到“ImageExecIAPBase”加载区,并调用其中的函数



externunsignedcharLoad$$Exec_IAP$$Base;



externunsignedcharImage$$Exec_IAP$$Length;



#defineImageExecIAPBase(0x400000000x1000)//加载区首址



voidMoveIAPRO(void)



{



unsignedcharpsrc,pdst;



unsignedintcount;



count=(unsignedint)&Image$$Exec_IAP$$Length;



psrc=(unsignedchar)&Load$$Exec_IAP$$Base;



pdst=(unsignedchar)ImageExecIAPBase;



while(count--){



pdst=psrc;



}



}



//调用“IAP.O”中的某函数



{



void(pfnIAPWrite)(unsignedlong,int);



pfnIAPWrite=(void()(unsignedlong,int))



(ImageExecIAPBase



(unsignedint)IAPWrite-//被调用函数名



(unsignedint)&Load$$Exec_IAP$$Base);



pfnIAPWrite((int)((CUPDATA)CODESTARTADDR)->data,



((CUPDATA)CODESTARTADDR)->length);



}



















//————————————————————————————————————————————————————————————

ARM编译程序参考

介绍ARM编译程序的ARM特有方面,包括:

Pragmas编译指示

Functionkeywords函数关键字

Variabledeclarationkeywords变量声明关键字



Pragmas

ARM编译程序可识别一下格式的编译指示:

#pragma[no_]feature-name

编译指示优于相关的命令行选项。

能识别的编译选项如下:





Pragmaname



Default



Reference

armsection



Off



Pragmascontrollingcodegeneration

check_printf_formats



Off



Pragmascontrollingprintfandscanfargumentchecking

check_scanf_formats



Off



Pragmascontrollingprintfandscanfargumentchecking

check_stack



On



Pragmascontrollingcodegeneration

debug



On



Pragmascontrollingdebugging

import







codegeneration

Ospace







optimization

Otime







optimization

Onum







optimization

softfp_linkage



Off



codegeneration







check_printf_formats



该编译指示标记类似于printf的函数,如果存在文字格式串,则对照进行类型检查。

#pragmacheck_printf_formats

externvoidmyprintf(constcharformat,…);

#pragmano_check_printf_formats



check_scanf_formats



该编译指示对声明为类似于scanf的函数做标记,以便对照文字格式串检查自变量的格式。

#pragmacheck_scanf_formats

externvoidmyformat(constcharformat,…);

#pragmano_check_scanf_formats



debug该编译指示可打开或关闭调试表生成,如果指定#pragmano_debug,则不会为随后的声明和函数生成调试信息表条目,直到下一个#pragmadebug出现。

Pragmascontrollingoptimization



Ospace

Otime

Onum



Pragmascontrollingcodegeneration

ocheck_stack如果已经使用了#pragmano_check_stack和-apcs/swst命令行选项禁止栈检查,则该编译指示可使的检查是否违反了栈限制的函数入口代码的重新生成。

oonce同#ifndef…#endif效果相类似,用于头文件。但一般推荐使用#ifndef…#define。

osoftfp_linkage该编译指示指定了至下一个#pragmano_softfp_linkage之间的所有函数声明描述了使用软件浮点链接的函数。__softfp关键字与该编译指示的效果相同

oimport(symbol_name)该编译指示生成对symbol_name的导入引用。同如下汇编语言相同:IMPORTsymbol_name。符号名作为外部符号放在映像的符号表中。

oarmsectionsection_sort_listThispragmaspecifiesthecodeordatasectionnamethatusedforsubsequentfunctionorobjects.Thisincludedefinitionsofanonymousobjectsthecompilercreatesforinitializations.该编译指示可指定代码或数据段的名称用于随后的函数或对象。包括编译程序为初始化而创建的匿名对象的定义。该选项对一下情况没有影响:



内联函数(及其局部静态变量)

模板实例(及其局部静态变量)

消除未使用的变量和函数

将定义写入目标文件中的顺序

该编译指示完整语法为:

#pragmaarmsection[sort_type[[=]“name”]][,sort_type=

“name”]

此处name用于段名称,sort_type可为如下之一code,rwdata,rodata

和zidata。若指定sort_type,没有指定name,则sort_type的段名被

重新设置为默认值。单独输入#pragmaarmsection,则所以对象段的

恢复为其默认值

intx1=5;//in.data(default)

inty1[100];//in.bss(default)

intconstz1[3]={1,2,3};//in.constdata(default)

#pragmaarmsectionrwdata="foo",rodata="bar"



intx2=5;//infoo(datapartofregion)

inty2[100];//in.bss

intconstz2[3]={1,2,3};//inbar

chars2="abc";//s2infoo,"abc"inbar

#pragmaarmsectionrodata

intx3=5;//infoo

inty3[100];//in.bss

intconstz3[3]={1,2,3};//in.constdata

chars3="abc";//s3infoo,"abc"in.constdata

#pragmaarmsectioncode="foo"

intadd1(intx)//infoo(codepartofregion)

{

returnx1;

}

#pragmaarmsectioncode





使用分散加载描述文件和链接程序,以控制将命名段放置在存储器中

的特定地址。

·Functionkeywords

一些关键字指示编译程序对其某个函数进行特殊处理。包括函数内的声明,函数限定符及函数存储类限定符。即Declarationsinsidefunction,FunctionqualifiersandFunctionstorage.

__asm{assembler-code}指示编译程序该语句是用汇编语言编写的。

__irqThisenablesaCorCfunctiontobeusedasaninterruptroutinecalledbytheIRQ,orFIQvectors.Allcorruptedregistersexceptfloating-pointregistersarepreserved,notonlythosethatarenormallypreservedundertheATPCS.ThedefaultATPCSmodemustbeused.Thefunctionexitsbysettingthepctolr-4andtheCPSRtothevalueinSPSR.Itisnotavailableintccortcpp.Noargumentsorreturnvaluescanbeusedwith__irqfunctions.

__pure指明函数声明为纯的。纯函数没有了公共子表达式。默认情况下,函数假定是不纯的(产生副作用)。纯函数需要满足:其结果仅取决于其自变量的值;没有副作用,其不能调用非纯函数。不能使用全局变量或废弃指针,同一参数两次调用纯函数,返回应该相同

一般而言,一个程序包括只读的代码段和可读写的数据段。在ARM的集成开发环境中,只读的代码段和常量被称作RO段(ReadOnly);可读写的全局变量和静态变量被称作RW段(ReadWrite);RW段中要被初始化为零的变量被称为ZI段(ZeroInit)。对于嵌入式系统而言,程序映象都是存储在Flash存储器等一些非易失性器件中的,而在运行时,程序中的RW段必须重新装载到可读写的RAM中。这就涉及到程序的加载时域和运行时域。简单来说,程序的加载时域就是指程序烧入Flash中的状态,运行时域是指程序执行时的状态。对于比较简单的情况,可以在ADS集成开发环境的ARMLINKER选项中指定ROBASE和RWBASE,告知连接器RO和RW的连接基地址。对于复杂情况,如RO段被分成几部分并映射到存储空间的多个地方时,需要创建一个称为“分布装载描述文件”的文本文件,通知连接器把程序的某一部分连接在存储器的某个地址空间。需要指出的是,分布装载描述文件中的定义要按照系统重定向后的存储器分布情况进行。在引导程序完成初始化的任务后,应该把主程序转移到RAM中去运行,以加快系统的运行速度。

什么是arm的映像文件,arm映像文件其实就是可执行文件,包括bin或hex两种格式,可以直接烧到rom里执行。在axd调试过程中,我们调试的是axf文件,其实这也是一种映像文件,它只是在bin文件中加了一个文件头和一些调试信息。映像文件一般由域组成,域最多由三个输出段组成(RO,RW,ZI)组成,输出段又由输入段组成。所谓域,指的就是整个bin映像文件所处在的区域,它又分为加载域和运行域。加载域就是映像文件被静态存放的工作区域,一般来说flash里的整个bin文件所在的地址空间就是加载域,当然在程序一般都不会放在flash里执行,一般都会搬到sdram里运行工作,它们在被搬到sdram里工作所处的地址空间就是运行域。我们输入的代码,一般有代码部分和数据部分,这就是所谓的输入段,经过编译后就变成了bin文件中ro段和rw段,还有所谓的zi段,这就是输出段。对于加载域中的输出段,一般来说ro段后面紧跟着rw段,rw段后面紧跟着zi段。在运行域中这些输出段并不连续,但rw和zi一定是连着的。zi段和rw段中的数据其实可以是rw属性。

|Image$$RO$$Base||Image$$RO$$Limit||Image$$RW$$Base||Image$$ZI$$Base||Image$$ZI$$Limit|这几个变量是编译器通知的,我们在makefile文件中可以看到它们的值。它们指示了在运行域中各个输出段所处的地址空间|Image$$RO$$Base|就是ro段在运行域中的起始地址,|Image$$RO$$Limit|是ro段在运行域中的截止地址。其它依次类推。我们可以在linker的output中指定,在simple模式中,robase对应的就是|Image$$RO$$Base|,rwbase对应的是|Image$$RW$$Base|,由于rw和zi相连,|Image$$ZI$$Base|就等于|Image$$ZI$$limit|.其它的值都是编译器自动计算出来的。

下面是2410启动代码的搬运部分,我给出注释

BaseOfROMDCD|Image$$RO$$Base|

TopOfROMDCD|Image$$RO$$Limit|

BaseOfBSSDCD|Image$$RW$$Base|

BaseOfZeroDCD|Image$$ZI$$Base|

EndOfBSSDCD|Image$$ZI$$Limit|

adrr0,ResetEntry;ResetEntry是复位运行时域的起始地址,在boot

nand中一般是0

ldrr2,BaseOfROM;

cmpr0,r2

ldreqr0,TopOfROM;TopOfROM=0x30001de0,代码段地址的结束

beqInitRam

ldrr3,TopOfROM

;part1,通过比较,将ro搬到sdram里,搬到的目的地址从|Image$$RO$$Base|开始,到|Image$$RO$$Limit|结束



0

ldmiar0!,{r4-r7}

stmiar2!,{r4-r7}

cmpr2,r3

bcc%B0;



;part2,搬rw段到sdram,目的地址从|Image$$RW$$Base|开始,到|Image$$ZI$$Base|结束

subr2,r2,r3;r2=0

subr0,r0,r2

InitRam;carryrwtobaseofBSS

ldrr2,BaseOfBSS;TopOfROM=0x30001de0,baseofrw

ldrr3,BaseOfZero;BaseOfZero=0x30001de0

0

cmpr2,r3

ldrccr1,[r0],#4

strccr1,[r2],#4

bcc%B0

;part3,将sdramzi初始化为0,地址从|Image$$ZI$$Base|到|Image$$ZI$$Limit|

movr0,#0;init0

ldrr3,EndOfBSS;EndOfBSS=30001e40

1

cmpr2,r3

strccr0,[r2],#4

bcc%B1

ARM映像文件简介及简单的初始化C运行环境-[ARM]



Tag:



版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明



http://kellycan.blogbus.com/logs/10962408.html



1.ARM映像文件



ARM中的各种源文件(包括汇编文件,C语言程序及C++程序等)经过ARM编译器编译后生成ELF格式的目标文件。这些目标文件和相应的C/C++运行时用到的库经过ARM连接器处理后,生成ELF格式的映像文件(image),这种ELF格式的映像文件是一种可执行文件,可被写入嵌入式设备的ROM中。



ARM映像文件的组成:ARM映像文件是一个层次性结构的文件,包括了域(region),输出段(outputsection)和输入段(inputsection)。



一个映像文件由一个或者多个域组成;每个域最多由三个输出段组成组成;每个输出段又包含一个或者输入段;各输入端包含了目标文件中的代码和数据。



所谓域,指的就是整个bin映像文件所处在的区域,它又分为加载域和运行域。加载域就是映像文件被静态存放的工作区域,一般来说flash里的整个bin文件所在的地址空间就是加载域,当然程序一般都不会放在flash里执行,一般都会搬到sdram里运行工作,它们在被搬到sdram里工作所处的地址空间就是运行域。



我们输入的代码,一般有代码部分和数据部分,这就是所谓的输入段,每个输入段都有相应的属性,可以为只读(ro),可读写的(rw)以及初始化成0的(zi)。ARM连接器根据各输入段的属性将这些输入段分组,再组成对应属性的输出段。对于加载域中的输出段,一般来说ro段后面紧跟着rw段,rw段后面紧跟着zi段。在运行域中这些输出段并不连续,但rw和zi一定是连着的。zi段和rw段中的数据其实可以是rw属性。



通常一个映像文件中包含若干个域,各个域又包含若干的输出段。ARM连接器就需要知道如下信息以决定生成相应的映像文件。



分组信息:决定如何各将输入段组织成相应的输出段和域。



定位信息:决定各个域在存储器空间中的起始地址。



根据映像文件中地址映射的复杂程度有两种方法告诉ARM连接器这些相关的信息。



(1)当映像文件中最多包含两个域,每个域最多有三个输出段时,可以使用连接器选项告诉连接器相关的地址映射关系。选项有-ropi,-rwpi,-ro_base,-rw_base,-split等。



(2)当映像文件地址映射关系更复杂时,可以使用一个配置文件(分散加载文件)告诉连接器相关的地址映射关系。



2.简单的初始化用户程序的执行环境



ARM映像文件一开始总是存储在ROM/Flash里面的,其RO部分既可以在ROM/Flash里面执行,也可以转移到速度更快的RAM中执行;而RW和ZI这两部分是必须转移到可写的RAM里去,其实RW包括ZI区域,ZI区域放的是未赋值的全局变量,RW区域放的是已赋值(赋0除外)的全局变量。所谓应用程序执行环境的初始化,就是完成必要的从ROM到RAM的数据传输和内容清零。



先介绍几个必要的符号,编译器使用下列符号来记录各段的起始和结束地址:



|Image$$RO$$Base|:RO段起始地址



|Image$$RO$$Limit|:RO段结束地址加1



|Image$$RW$$Base|:RW段起始地址



|Image$$RW$$Limit|:ZI段结束地址加1



|Image$$ZI$$Base|:ZI段起始地址



|Image$$ZI$$Limit|:ZI段结束地址加1



这些符号的值是根据链接器中设置的中ro-base和rw-base的设置来计算的。由于rw和zi相连,|Image$$ZI$$Base|就等于|Image$$RW$$Limit|.其它的值都是编译器自动计算出来的。我们还可以通过scatter文件更详细得指定各个输出段的工作地址。



初始化用户执行环境主要是把ro、rw、zi三段拷贝到指定的位置。



下面的程序是rw、zi段在运行域中的搬运过程:



IMPORT|Image$$RO$$Limit|/表示RO区末地址后面的地址,即RW数据源的起始地址/



IMPORT|Image$$RW$$Base|/RW区在RAM里的执行区起始地址,也就是编译器选项RW_Base指定的地址/



IMPORT|Image$$ZI$$Base|/ZI区在RAM里面的起始地址/



IMPORT|Image$$ZI$$Limit|/ZI区在RAM里面的结束地址后面的一个地址/



IMPORTMain;声明C程序中的Main()函数



AREAStart,CODE,READONLY;声明代码段Start



ENTRY;标识程序入口



CODE32;声明32位ARM指令







ResetLDRSP,=0x40003F00



;初始化C程序的运行环境



LDRR0,=|Image$$RO$$Limit|/取ROM区中数据段的首地址/



LDRR1,=|Image$$RW$$Base|/取RAM区中RW段的目标首地址/



LDRR3,=|Image$$ZI$$Base|/取RAM区中ZI段的首地址/







CMPR0,R1/比较ROM区中数据段首地址和RAM区中RW段目标首地址/



BEQLOOP1/相等代表当前是在RAM中运行/



LOOP0CMPR1,R3/不相等则和RAM区中ZI段的目标地址比较/



LDRCCR2,[R0],#4/如果r1


STRCCR2,[R1],#4/如果r1


BCCLOOP0/如果r1






LOOP1LDRR1,=|Image$$ZI$$Limit|/取ZI段的结束地址/



MOVR2,#0



LOOP2CMPR3,R1



STRCCR2,[R3],#4/如果r3


BCCLOOP2/如果r3


BMain;跳转到C程序代码Main()函数









在硬件工程师和普通用户看来,内存就是插在或固化在主板上的内存条,它们有一定的容量——比如64MB。但在应用程序员眼中,并不过度关心插在主板上的内存容量,而是他们可以使用的内存空间——他们可以开发一个需要占用1GB内存的程序,并让其在OS平台上运行,哪怕这台运行主机上只有128MB的物理内存条。而对于OS开发者而言,则是介于二者之间,他们既需要知道物理内存的细节,也需要提供一套机制,为应用程序员提供另一个内存空间,这个内存空间的大小可以和实际的物理内存大小之间没有任何关系。



我们将主板上的物理内存条所提供的内存空间定义为物理内存空间;将应用程序员看到的内存空间定义为线性空间。物理内存空间大小在不同的主机上可以是不一样的,随着主板上所插的物理内存条的容量不同而不同;但为应用程序员提供的线性空间却是固定的,不会随物理内存的变化而变化,这样才能保证应用程序的可移植性。尽管物理内存的大小可以影响应用程序运行的性能,并且很多情况下对物理内存的大小有一个最低要求,但这些因素只是为了让一个OS可以正常的运行。



线性空间的大小在32-bit平台上为4GB的固定大小,对于每个进程都是这样(一个应用可以是多进程的,在OS眼中,是以进程为单位的)。也就是说线性空间不是进程共享的,而是进程隔离的,每个进程都有相同大小的4GB线性空间。一个进程对于某一个内存地址的访问,与其它进程对于同一内存地址的访问绝不冲突。比如,一个进程读取线性空间地址1234ABCDh可以读出整数8,而另外一个进程读取线性空间地址1234ABCDh可以读出整数20,这取决于进程自身的逻辑。



在任意一个时刻,在一个CPU上只有一个进程在运行。所以对于此CPU来讲,在这一时刻,整个系统只存在一个线性空间,这个线性空间是面向此进程的。当进程发生切换的时候,线性空间也随着切换。所以结论就是每个进程都有自己的线性空间,只有此进程运行的时候,其线性空间才被运行它的CPU所知。在其它时刻,其线性空间对于CPU来说,是不可知的。所以尽管每个进程都可以有4GB的线性空间,但在CPU眼中,只有一个线性空间的存在。线性空间的变化,随着进程切换而变化。



尽管线性空间的大小和物理内存的大小之间没有任何关系,但使用线性空间的应用程序最终还是要运行在物理内存中。应用所给出的任何线性地址最终必须被转化为物理地址,才能够真正的访问物理内存。所以,线性内存空间必须被映射到物理内存空间中,这个映射关系需要通过使用硬件体系结构所规定的数据结构来建立。我们不妨先称其为映射表。一个映射表的内容就是某个线性内存空间和物理内存空间之间的映射关系。OSKernel一旦告诉某个CPU一个映射表的位置,那么这个CPU需要去访问一个线性空间地址时,就根据这张映射表的内容,将这个线性空间地址转化为物理空间地址,并将此物理地址送到地址线,毕竟地址线只知道物理地址。



所以,我们很容易得出一个结论,如果我们给出不同的映射表,那么CPU将某一线性空间地址转化的物理地址也会不同。所以我们为每一个进程都建立一张映射表,将每个进程的线性空间根据自己的需要映射到物理空间上。既然某一时刻在某一CPU上只能有一个应用在运行,那么当任务发生切换的时候,将映射表也更换为响应的映射表就可以实现每个进程都有自己的线性空间而互不影响。所以,在任意时刻,对于一个CPU来说,也只需要有一张映射表,以实现当前进程的线性空间到物理空间的转化。



--------------------------------------------------------------------------------







2.OSKernelSpace&ProcessSpace





由于OSKernel在任意时刻都必须存在于内存中,而进程却可以切换,所以在任意时刻,内存中都存在两部分,OSKernel和用户进程。而在任意时刻,对于一个CPU来说只存在一个线性空间,所以这个线性空间必须被分成两部分,一部分供OSKernel使用,另一部分供用户进程使用。既然OSKernel在任何时候都占用线性空间中的一部分,那么对于所有进程的线性空间而言,它们为OSKernel所留出的线性空间可以是完全相同的,也就是说,它们各自的映射表中,也分为两部分,一部分是进程私有映射部分,对于OSKernel映射部分的内容则完全相同。



从这个意义上来说,我们可以认为,对于所有的进程而言,它们共享OSKernel所占用的线性空间部分,而每个进程又各自有自己私有的线性空间部分。假如,我们将任意一个4GB线性空间分割为1GB的OSKernel空间部分和3GB的进程空间部分,那么所有进程的4GB线性空间中1GB的OSKernel空间是共享的,而剩余的3GB进程空间部分则是各个进程私有的。Linux就是这么做的,而WindowsNT则是让OSKernel和进程各使用2GB线性空间。



--------------------------------------------------------------------------------







3.SegmentMapping&PageMapping





所有的线性空间的内容只有被放置到物理内存中才能够被真正的运行和操作。所以,尽管OSKernel和进程都被放在线性空间中,但它们最终必须被放置到物理内存中。所以OSKernel和所有的进程都最终共享物理内存。在现阶段,物理内存远没有线性空间那么大——线性空间是4GB,而物理内存空间往往只有几百兆,甚至更小。另外即使物理内存有4GB,但由于每个进程都可以有3GB线性空间(假如进程私有线性空间是3GB的话),如果把所有进程的线性空间内容都放在物理内存中,明显是不现实的。所以OSKernel必须将某些进程暂时用不到的数据或代码放在物理内存之外,将有限的内存提供给当前最需要的进程。另外,由于OSKernel在任何时候都有可能运行,所以OSKernel最好被永远放在物理内存中。我们仅仅将进程数据进行换入换出。



从线性空间到物理空间的映射需要映射表,映射表的内容是将某段线性空间映射到相同大小的物理内存空间上。从理论上,我们可以使用两种映射方法:变长映射,和定长映射。变长映射指的是根据不同的需要,将一个一个变长段映射到物理内存上,其格式可以如下(线性空间段起始地址,物理空间段起始地址,段长度)。假如一个进程有3个段:10M的数据段,5M的代码段,和8K的堆栈段,那么就可以在映射表中建立3项内容,每一项针对一个段。这看起来没有问题。但假如现在我们的实际的内存只有32M,其中10M被内核占用,留给进程的物理空间只有22M,那么此进程在运行时,就占据了10M+5M+8K的内存空间。随后当进程发生切换时,假如另一个进程和其有相同的内存要求,那么剩余的22M-(10M+5M+8K)明显就不够用了,这时只能将原进程的某些段换出,并且必须是整段的换出。这就意味着我们必须至少换出一个10M的数据段,而换出的成本很高,因为我们必须将这10M的内容拷贝到磁盘上,磁盘I/O是很慢的。



所以,使用变长的段映射的结果就是一个段要么被全部换入,要么被全部换出。但在现实中,一个程序中并非所有的代码和数据都能够被经常访问,往往被经常访问的只占全部代码数据的一部分,甚至是一小部分。所以更有效的策略是我们最好只换出那些并不经常使用的部分,而保留那些经常被使用的部分。而不是整个段的换入换出。这样可以避免大块的慢速磁盘操作。



这就是定长映射策略,我们将内存空间分割为一个个定长块,每个定长块被称为一个页。映射表的基本格式为(物理空间页起始地址),由于页是定长的,所以不需要指出它的长度,另外,我们不需要在映射表中指定线性地址,我们可以将线性地址作为索引,到映射表中检索出相应的物理地址。当使用页时,其策略为:当换出的时候,我们只将那些不活跃的,也就是不经常使用的页换出,而保留那些活跃的页。在换入的时候,只有被请求访问的页才被换入,没有被请求访问的页将永远不会被换入到物理内存。这就是请求页(DemandPage)算法的核心思想。



这就引出一个页大小的问题:首先我们不可能以字节为单位,这样映射表的大小和线性空间大小相同——假如整个线性空间都被映射的话——我们不可能将全部线性空间用作存放这个映射表。由此,我们也可以得知,页越小,则映射表的容量越大。而我们不能让映射表占用太多的空间。但如果页太大,则面临着和不定长段映射同样的问题,每次换出一个页,都需要大量的磁盘操作。另外,由于为一个进程分配内存的最小单位是页,假如我们的页大小为4MB,那么即使一个进程只需要使用4KB的内存,也不得不占用整个4MB页,这明显是一种很大的浪费。所以我们必须在两者之间进行折衷,一般平台所规定的页大小为1KB到8KB,IA-32所规定的页大小为4KB。(IA-32也支持4MB页,你可以根据你的OS的用途进行选择,一般都是使用4KB页)。



献花(0)
+1
(本文系小云蔡首藏)