stage2_endQstage2_startQstage2_size |
.text .globl _trampoline _trampoline: bl main /* if main ever returns we just call it again */ b _trampoline |
typedef struct memory_area_struct { u32 start; /* the base address of the memory region */ u32 size; /* the byte number of the memory region */ int used; } memory_area_t; |
memory_area_t memory_map[NUM_MEM_AREAS] = { [0 ... (NUM_MEM_AREAS - 1)] = { .start = 0, .size = 0, .used = 0 }, }; |
/* 数组初始?*/ for(i = 0; i < NUM_MEM_AREAS; i++) memory_map.used = 0; /* first write a 0 to all memory locations */ for(addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE) * (u32 *)addr = 0; for(i = 0, addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE) { /* * 从基地址 MEM_START+i*PAGE_SIZE 开?大小?br>* PAGE_SIZE 的地址I间是否是有效的RAM地址I间?br> */ 调用3.1.2节中的算法test_mempage()Q?br> if ( current memory page isnot a valid ram page) { /* no RAM here */ if(memory_map.used ) i++; continue; } /* * 当前已l是一个被映射?RAM 的有效地址范围 * 但是q要看看当前|否只?4GB 地址I间中某个地址늚别名Q?br> */ if(* (u32 *)addr != 0) { /* alias? */ /* q个内存| 4GB 地址I间中某个地址늚别名 */ if ( memory_map.used ) i++; continue; } /* * 当前已l是一个被映射?RAM 的有效地址范围 * 而且它也不是 4GB 地址I间中某个地址늚别名?br> */ if (memory_map.used == 0) { memory_map.start = addr; memory_map.size = PAGE_SIZE; memory_map.used = 1; } else { memory_map.size += PAGE_SIZE; } } /* end of for (…) */ |
while(count) { *dest++ = *src++; /* they are all aligned with word boundary */ count -= 4; /* byte number */ }; |
/* The list ends with an ATAG_NONE node. */ #define ATAG_NONE 0x00000000 struct tag_header { u32 size; /* 注意Q这里size是字Cؓ(f)单位?*/ u32 tag; }; …… struct tag { struct tag_header hdr; union { struct tag_core core; struct tag_mem32 mem; struct tag_videotext videotext; struct tag_ramdisk ramdisk; struct tag_initrd initrd; struct tag_serialnr serialnr; struct tag_revision revision; struct tag_videolfb videolfb; struct tag_cmdline cmdline; /* * Acorn specific */ struct tag_acorn acorn; /* * DC21285 specific */ struct tag_memclk memclk; } u; }; |
params = (struct tag *)BOOT_PARAMS; params->hdr.tag = ATAG_CORE; params->hdr.size = tag_size(tag_core); params->u.core.flags = 0; params->u.core.pagesize = 0; params->u.core.rootdev = 0; params = tag_next(params); |
for(i = 0; i < NUM_MEM_AREAS; i++) { if(memory_map.used) { params->hdr.tag = ATAG_MEM; params->hdr.size = tag_size(tag_mem32); params->u.mem.start = memory_map.start; params->u.mem.size = memory_map.size; params = tag_next(params); } } |
char *p; /* eat leading white space */ for(p = commandline; *p == ' '; p++) ; /* skip non-existent command lines so the kernel will still * use its default command line. */ if(*p == '\0') return; params->hdr.tag = ATAG_CMDLINE; params->hdr.size = (sizeof(struct tag_header) + strlen(p) + 1 + 4) >> 2; strcpy(params->u.cmdline.cmdline, p); params = tag_next(params); |
params->hdr.tag = ATAG_INITRD2; params->hdr.size = tag_size(tag_initrd); params->u.initrd.start = RAMDISK_RAM_BASE; params->u.initrd.size = INITRD_LEN; params = tag_next(params); |
params->hdr.tag = ATAG_RAMDISK; params->hdr.size = tag_size(tag_ramdisk); params->u.ramdisk.start = 0; params->u.ramdisk.size = RAMDISK_SIZE; /* h意,单位是KB */ params->u.ramdisk.flags = 1; /* automatically load ramdisk */ params = tag_next(params); |
static void setup_end_tag(void) { params->hdr.tag = ATAG_NONE; params->hdr.size = 0; } |
void (*theKernel)(int zero, int arch, u32 params_addr) = (void (*)(int, int, u32))KERNEL_RAM_BASE; …… theKernel(0, ARCH_NUMBER, (u32) kernel_params_start); |
?/strong>
Linux是一U很受欢q的操作pȝQ它与UNIXpȝ兼容Q开放源代码。它原本被设计ؓ(f)桌面pȝQ现在广泛应用于服务器领域。而更大的影响在于它正逐渐的应用于嵌入式设备。uClinux正是在这U氛围下产生的。在uClinuxq个英文单词中u表示MicroQ小的意思,C表示ControlQ控制的意思,所以uClinux是Micro-Control-LinuxQ字面上的理解就?针对微控刉域而设计的Linuxpȝ"?br>
uClinux型化的做法
标准Linux可能采用的小型化Ҏ(gu)
1. 重新~译内核
Linux内核采用模块化的设计Q即很多功能块可以独立的加上或卸下,开发h员在设计内核时把q些内核模块作ؓ(f)可选的选项Q可以在~译pȝ内核时指定。因此一U较通用的做法是对Linux内核重新~译Q在~译时仔l的选择嵌入式设备所需要的功能支持模块Q同时删除不需要的功能。通过对内核的重新配置Q可以ɾpȝq行所需要的内核显著减小Q从而羃减资源用量?br>2. 制作root文gpȝ映象
Linuxpȝ在启动时必须加蝲根(rootQ文件系l,因此剪裁pȝ同时包括root file system的剪裁。在x86pȝ下,Linux可以在Dos下,使用Loadlin文g加蝲启动Q?br>uClinux采用的小型化Ҏ(gu)
1QuClinux的内核加载方?br>uClinux的内核有两种可选的q行方式Q可以在flash上直接运行,也可以加载到内存中运行。这U做法可以减内存需要?br>Flashq行方式Q把内核的可执行映象烧写到flash上,pȝ启动时从flash的某个地址开始逐句执行。这U方法实际上是很多嵌入式pȝ采用的方法?br>内核加蝲方式Q把内核的压~文件存攑֜flash上,pȝ启动时读取压~文件在内存里解压,然后开始执行,q种方式相对复杂一些,但是q行速度可能更快Qram的存取速率要比flash高)。同时这也是标准Linuxpȝ采用的启动方式?br>2QuClinux的根QrootQ文件系l?br>uClinuxpȝ采用romfs文gpȝQ这U文件系l相对于一般的ext2文gpȝ要求更少的空间。空间的节约来自于两个方面,首先内核支持romfs文gpȝ比支持ext2文gpȝ需要更的代码Q其ơromfs文gpȝ相对单,在徏立文件系l超U块QsuperblockQ需要更的存储I间。Romfs文gpȝ不支持动态擦写保存,对于pȝ需要动态保存的数据采用虚拟ram盘的Ҏ(gu)q行处理Qram盘将采用ext2文gpȝQ?br>3QuClinux的应用程序库
uClinux型化的另一个做法是重写了应用程序库Q相对于来大且越来越全的glibc库,uClibc对libc做了_?br>uClinux对用L(fng)序采用静态连接的形式Q这U做法会(x)使应用程序变大,但是Z内存理的问题,不得不这样做Q这在下文对uClinux内存理展开分析时进行说明)Q同时这U做法也更接q于通常嵌入式系l的做法?br>
uClinux的开发环?/strong>
GNU开发套?/strong>
Gnu开发套件作为通用的Linux开攑֥Ӟ包括一pd的开发调试工兗主要组Ӟ(x)
GccQ?~译器,可以做成交叉~译的Ş式,卛_宿主Z开发编译目标上可运行的二进制文件?br>BinutilsQ一些辅助工P包括objdumpQ可以反~译二进制文ӞQasQ汇~编译器Q,ldQ连接器Q等{?br>GdbQ调试器Q可使用多种交叉调试方式Qgdb-bdmQ背景调试工PQgdbserverQ用以太网l调试)?br>uClinux的打印终?br>通常情况下,uClinux的默认终端是串口Q内核在启动时所有的信息都打印到串口l端Q用printk函数打印Q,同时也可以通过串口l端与系l交互?br>uClinux在启动时启动了telnetdQ远E登录服务)Q操作者可以远E登录上pȝQ从而控制系l的q行。至于是否允许远E登录可以通过烧写romfs文gpȝ时有用户军_是否启动q程d服务?br>交叉~译调试工具
支持一U新的处理器Q必d备一些编译,汇编工具Q用这些工具可以Ş成可q行于这U处理器的二q制文g。对于内怋用的~译工具同应用程序用的有所不同。在解释不同点之前,需要对gccq接做一些说明:(x)
.ldQlink descriptionQ文Ӟ(x)ld文g是指接时内存映象格式的文件?br>crt0.SQ应用程序编译连接时需要的启动文gQ主要是初始化应用程序栈?br>picQposition independence code Q与位置无关的二q制格式文gQ在E序D中必须包括relocD,从而的代码加载时可以q行重新定位?br>内核~译q接Ӟ使用ucsimm.ld文gQŞ成可执行文g映象Q所形成的代码段既可以用间接寻址方式Q即使用relocD进行寻址Q,也可以用绝对寻址方式。这样可以给~译器更多的优化I间。因为内核可能用绝对寻址Q所以内核加载到的内存地址I间必须与ld文g中给定的内存I间完全相同?#160;
应用E序的连接与内核q接方式不同。应用程序由内核加蝲Q可执行文g加蝲器将在后面讨论)Q由于应用程序的ld文gl出的内存空间与应用E序实际被加载的内存位置可能不同Q这样在应用E序加蝲的过E中需要一个重新地位的q程Q即对relocD进行修正,使得E序q行间接d时不至于出错。(q个问题在i386{高U处理器上方法有所不同Q本文将在后面进一步分析)?br>׃q讨论,臛_需要两套编译连接工兗在讨论quClinux的内存管理后本文给出整个系l的工作程以及pȝ在flash和ram中的I间分布?br>可执行文件格?/strong>
先对一些名词作一些说明:(x)
coffQcommon object file formatQ:(x)一U通用的对象文件格?br>elfQexcutive linked fileQ:(x)一Uؓ(f)Linuxpȝ所采用的通用文g格式Q支持动态连?br>flatQelf格式有很大的文g_flat文gҎ(gu)件头和一些段信息做了?br>uClinuxpȝ使用flat可执行文件格式,gcc的编译器不能直接形成q种文g格式Q但是可以Ş成coff或elf格式的可执行文gQ这两种文g需要coff2flt或elf2flt工具q行格式转化QŞ成flat文g?br>当用h行一个应用时Q内核的执行文g加蝲器将对flat文gq行q一步处理,主要是对relocD进行修正(可执行文件加载器的详见fs/binfmt_flat.cQ。以下对relocD进一步讨论?br>需要relocD늚Ҏ(gu)原因是,E序在连接时q接器所假定的程序运行空间与实际E序加蝲到的内存I间不同。假如有q样一条指令:(x)
jsr app_start;
q一条指令采用直接寻址Q蟩转到app_start地址处执行,q接E序在~译完成是计出app_start的实际地址Q设若实际地址?x10000Q,q个实际地址是根据ld文g计算出来Q因接器假定该程序将被加载到由ld文g指明的内存空_。但实际上由于内存分配的关系Q操作系l在加蝲时无法保证程序将按ld文g加蝲。这时如果程序仍然蟩转到l对地址0x10000处执行,通常情况q是不正的。一个解军_法是增加一个存储空_用于存储app_start的实际地址Q设若用变量addr表示q个存储I间。则以上q句E序改为:(x)
movl addr, a0;
jsr (a0);
增加的变量addr在数据D中占用一?字节的空_q接器将app_start的绝对地址存储到该变量。在可执行文件加载时Q可执行文g加蝲器根据程序将要加载的内存I间计算出app_start在内存中的实际位|,写入addr变量。系l在实际处理是不需要知道这个变量的切存储位置Q也不可能知道)Q系l只要对整个relocD进行处理就可以了(relocD|标识Q系l可以读出来Q。处理很单只需要对relocD中存储的值统一加上一个偏|(如果加蝲的空间比预想的要靠前Q实际上是减M个偏U量Q。偏|由实际的物理地址起始值同ld文g指定的地址起始值相减计出?br>q种reloc的方式部分是由uClinux的内存分配问题引L(fng)Q这一点将在uClinux内存理分析时说明?br>针对实时性的解决Ҏ(gu)
uClinux本nq没有关注实旉题,它ƈ不是ZLinux的实时性而提出的。另外有一ULinux--Rt-linuxx实时问题。Rt-linux执行理器把普通Linux的内核当成一个Q务运行,同时q管理了实时q程。而非实时q程则交l普通Linux内核处理。这U方法已l应用于很多的操作系l用于增强操作系l的实时性,包括一些商用版UNIXpȝQW(xu)indows NT{等。这U方法优点之一是实现简单,且实时性能Ҏ(gu)验。优点之二是׃非实时进E运行于标准LinuxpȝQ同其它Linux商用版本之间保持了很大的兼容性。优点之三是可以支持实时时钟的应用。uClinux可以使用Rt-linux的patchQ从而增强uClinux的实时性,使得uClinux可以应用于工业控制、进E控制等一些实时要求较高的应用?br>
uClinux的内存管?/strong> Z支持虚拟存储理器的理QLinuxpȝ采用分页QpagingQ的方式来蝲入进E。所谓分|是把实际的存储器分割为相同大的D,例如每个D?024个字节,q样1024个字节大的D便UCؓ(f)一个页面(pageQ?#160;
应该说uClinux同标准Linux的最大区别就在于内存理Q同时也׃uClinux的内存管理引发了一些标准Linux所不会(x)出现的问题。本文将把uClinux内存理同标准Linux的那内存理部分q行比较分析?br>标准Linux使用的虚拟存储器技?br>标准Linux使用虚拟存储器技术,q种技术用于提供比计算机系l中实际使用的物理内存大得多的内存空间。用者将感觉到好像程序可以用非常大的内存空_从而得编Eh员在写程序时不用考虑计算Z的物理内存的实际定w?/p>
虚拟存储器由存储器管理机制及一个大定w的快速硬盘存储器支持。它的实现基于局部性原理,当一个程序在q行之前Q没有必要全部装入内存,而是仅将那些当前要运行的那些部分面或段装入内存q行Qcopy-on-writeQ,其余暂时留在盘上程序运行时如果它所要访问的(D)已存在,则程序l运行,如果发现不存在的(D)Q操作系l将产生一个页错误Qpage faultQ,q个错误D操作pȝ把需要运行的部分加蝲到内存中。必要时操作pȝq可以把不需要的内存(D)交换到磁盘上。利用这L(fng)方式理存储器,便可把一个进E所需要用到的存储器以化整为零的方式,视需求分批蝲入,而核心程序则凭借属于每个页面的늠来完成寻址各个存储器区D늚工作?br>标准Linux是针Ҏ(gu)内存理单元的处理器设计的。在q种处理器上Q虚拟地址被送到内存理单元QMMUQ,把虚拟地址映射为物理地址?br>通过赋予每个d不同的虚?-物理地址转换映射Q支持不同Q务之间的保护。地址转换函数在每一个Q务中定义Q在一个Q务中的虚拟地址I间映射到物理内存的一个部分,而另一个Q务的虚拟地址I间映射到物理存储器中的另外区域。计机的存储管理单元(MMUQ一般有一l寄存器来标识当前运行的q程的{换表。在当前q程CPU攑ּl另一个进E时Q一ơ上下文切换Q,内核通过指向新进E地址转换表的指针加蝲q些寄存器。MMU寄存器是有特权的Q只能在内核态才能访问。这׃证了一个进E只能访问自qL(fng)间内的地址Q而不?x)访问和修改其它q程的空间。当可执行文件被加蝲Ӟ加蝲器根据缺省的ld文gQ把E序加蝲到虚拟内存的一个空_因ؓ(f)q个原因实际上很多程序的虚拟地址I间是相同的Q但是由于{换函C同,所以实际所处的内存区域也不同。而对于多q程理当处理器q行q程切换q执行一个新dӞ一个重要部分就是ؓ(f)CQ务切换Q务{换表。我们可以看到Linuxpȝ的内存管理至实C以下功能Q?br>q行比内存还要大的程序。理x况下应该可以q行L大小的程?br>◇可以运行只加蝲了部分的E序Q羃短了E序启动的时?br>◇可以多个E序同时ȝ在内存中提高CPU的利用率
◇可以运行重定位E序。即E序可以方于内存中的M一处,而且可以在执行过E中Ud?br>◇写机器无关的代码。程序不必事先约定机器的配置情况?br>◇减ȝ序员分配和管理内存资源的负担?br>◇可以进行共?-例如Q如果两个进E运行同一个程序,它们应该可以׃nE序代码的同一个副本?br>◇提供内存保护,q程不能以非授权方式讉K或修攚w面,内核保护单个q程的数据和代码以防止其它进E修改它们。否则,用户E序可能?x)偶Ӟ或恶意)的破坏内核或其它用户E序?#160;
虚存pȝq不是没有代L(fng)。内存管理需要地址转换表和其他一些数据结构,留给E序的内存减了。地址转换增加了每一条指令的执行旉Q而对于有额外内存操作的指令会(x)更严重。当q程讉K不在内存的页面时Q系l发生失效。系l处理该失效Qƈ页面加载到内存中,q需要极耗时间的盘I(y)/O操作。M内存理zd占用了相当一部分cpu旉Q在较忙的系l中大约?0Q)?br>uClinux针对NOMMU的特D处?br>对于uClinux来说Q其设计针对没有MMU的处理器Q即uClinux不能使用处理器的虚拟内存理技术(应该说这U不带有MMU的处理器在嵌入式讑֤中相当普偏)。uClinux仍然采用存储器的分页理Q系l在启动时把实际存储器进行分c在加蝲应用E序时程序分加载。但是由于没有MMU理Q所以实际上uClinux采用实存储器理{略Qreal memeory managementQ。这一点媄响了pȝ工作的很多方面?br>uClinuxpȝ对于内存的访问是直接的,Q它对地址的访问不需要经qMMUQ而是直接送到地址U上输出Q,所有程序中讉K的地址都是实际的物理地址。操作系l对内存I间没有保护Q这实际上是很多嵌入式系l的特点Q,各个q程实际上共享一个运行空_没有独立的地址转换表)?#160;
一个进E在执行前,pȝ必须E分配够的q箋地址I间Q然后全部蝲入主存储器的q箋I间中。与之相对应的是标准Linuxpȝ在分配内存时没有必要保证实际物理存储I间是连l的Q而只要保证虚存地址I间q箋可以了。另外一个方面程序加载地址与预期(ld文g中指出的Q通常都不相同Q这样relocationq程是必须的。此外磁盘交换空间也是无法用的Q系l执行时如果~少内存无法通过盘交换来得到改善?br>uClinux对内存的理减少同时q开发h员提Z更高的要求。如果从易用性这一Ҏ(gu)_uClinux的内存管理是一U倒退Q退回了CUNIX早期或是Dospȝ时代。开发h员不得不参与pȝ的内存管理。从~译内核开始,开发h员必d诉系l这块开发板到底拥有多少的内存(假如你欺骗了pȝQ那在后面q行E序时受到惩|)Q从而系l将在启动的初始化阶D对内存q行分页Qƈ且标记已使用的和未用的内存。系l将在运行应用时使用q些分页内存?br>׃应用E序加蝲时必d配连l的地址I间Q而针对不同硬件^台的可一ơ成块(q箋地址Q分配内存大限制是不同Q目前针对ez328处理器的uClinux?28kQ而针对coldfire处理器的pȝ内存则无此限ӞQ所以开发h员在开发应用程序时必须考虑内存的分配情况ƈx应用E序需要运行空间的大小。另外由于采用实存储器管理策略,用户E序同内总及其它用L(fng)序在一个地址I间Q程序开发时要保证不늊其它E序的地址I间Q以使得E序不至于破坏系l的正常工作Q或D其它E序的运行异常?br>从内存的讉K角度来看Q开发h员的权利增大了(开发h员在~程时可以访问Q意的地址I间Q,但与此同时系l的安全性也大ؓ(f)下降。此外,pȝ对多q程的管理将有很大的变化Q这一点将在uClinux的多q程理中说明?br>虽然uClinux的内存管理与标准Linuxpȝ相比功能相差很多Q但应该说这是嵌入式讑֤的选择。在嵌入式设备中Q由于成本等敏感因素的媄响,普偏的采用不带有MMU的处理器Q这军_了系l没有够的g支持实现虚拟存储理技术。从嵌入式设备实现的功能来看Q嵌入式讑֤通常在某一特定的环境下q行Q只要实现特定的功能Q其功能相对单,内存理的要求完全可以由开发h员考虑?br>标准Linuxpȝ的进E、线E?br>q程Q进E是一个运行程序ƈ为其提供执行环境的实体,它包括一个地址I间和至一个控制点Q进E在q个地址I间上执行单一指o序列。进E地址I间包括可以讉K或引用的内存单元的集合,q程控制炚w过一个一般称为程序计数器Qprogram counter,PCQ的g寄存器控制和跟踪q程指o序列?br>forkQ由于进Eؓ(f)执行E序的环境,因此在执行程序前必须先徏立这个能"?E序的环境。Linuxpȝ提供pȝ调用拯现行q程的内容,以生新的进E,调用fork的进E称为父q程Q而所产生的新q程则称为子q程。子q程?x)承袭父q程的一切特性,但是它有自己的数据段Q也是_管子进E改变了所属的变量Q却不会(x)影响到父q程的变量倹{?br>父进E和子进E共享一个程序段Q但是各自拥有自q堆栈、数据段、用L(fng)间以及进E控制块。换a之,两个q程执行的程序代码是一L(fng)Q但是各有各的程序计数器与自qUh数据?#160;
当内核收到forkhӞ它会(x)先查怸件事Q首先检查存储器是不是够;其次是进E表是否仍有I缺Q最后则是看看用h否徏立了太多的子q程。如果上q说三个条g满Q那么操作系l会(x)l子q程一个进E识别码Qƈ且设定cpu旉Q接着讑֮与父q程׃n的段Q同时将父进E的inode拯一份给子进E运用,最l子q程?x)返回数?以表C它是子q程Q至于父q程Q它可能{待子进E的执行l束Q或与子q程各做个的?br>execpȝ调用Q该pȝ调用提供一个进E去执行另一个进E的能力Qexecpȝ调用是采用覆盖旧有进E存储器内容的方式,所以原来程序的堆栈、数据段与程序段都会(x)被修改,只有用户区维持不变?br>vforkpȝ调用Q由于在使用forkӞ内核?x)将父进E拷贝一份给子进E,但是q样的做法相当浪Ҏ(gu)_因ؓ(f)大多数的情Ş都是E序在调用fork后就立即调用execQ这样刚拯来的q程区域又立卌新的数据覆盖掉。因此Linuxpȝ提供一个系l调用vforkQvfork假定pȝ在调用完成vfork后会(x)马上执行execQ因此vfork不拷贝父q程的页面,只是初始化私有的数据l构与准备够的分页表。这样实际在vfork调用完成后父子进E事实上׃n同一块存储器Q在子进E调用exec或是exit之前Q,因此子进E可以更改父q程的数据及堆栈信息Q因此vforkpȝ调用完成后,父进E进入睡眠,直到子进E执行exec。当子进E执行execӞ׃exec要用被执行E序的数据,代码覆盖子进E的存储区域Q这样将产生写保护错误(do_wp_pageQ(q个时候子q程写的实际上是父进E的存储区域Q,
q个错误D内核为子q程重新分配存储I间。当子进E正开始执行后Q将唤醒父进E,使得父进El往后执行?br>uClinux的多q程处理
uClinux没有mmu理存储器,在实现多个进E时Qfork调用生成子进E)需要实现数据保护?br>uClinux的fork和vforkQuClinux的fork{于vfork。实际上uClinux的多q程理通过vfork来实现。这意味着uClinuxpȝfork调用完程后,要么子进E代替父q程执行Q此时父q程已经sleepQ直到子q程调用exit退出,要么调用exec执行一个新的进E,q个时候将产生可执行文件的加蝲Q即使这个进E只是父q程的拷贝,q个q程也不能避免。当子进E执行exit或exec后,子进E用wakeup把父q程唤醒Q父q程l箋往下执行?br>uClinux的这U多q程实现机制同它的内存管理紧密相兟뀂uClinux针对nommu处理器开发,所以被q用一Uflat方式的内存管理模式,启动新的应用E序时系l必Mؓ(f)应用E序分配存储I间Qƈ立即把应用程序加载到内存。缺了MMU的内存重映射机制QuClinux必须在可执行文g加蝲阶段对可执行文greloc处理Q得程序执行时能够直接使用物理内存?/p>