msgbartop
DoneVII CET & CPPLITE
msgbarbottom

17 May 09 在BREW程序中调用另一个mod的分析

近日读到这样一段程序。可以在程序运行时调用另一个mod文件并执行。这样可以做到应用自升级,压缩程序文件等实用的功能。

 

 要想理解这点,先看一下正常的程序加载过程。

 程序的入口,即相当于c程序的main,是位于AEEModGen.c中的AEEMod_Load函数。这可以从mak文件的连接选项中看出来。

 

LINK_ORDER = -first AEEMod_Load

 

 这个选项使得链接程序将该函数放在程序文件的0地址处。函数的定义如下:

 

int AEEMod_Load(IShell *pIShell, void *ph, IModule **ppMod)

{

   // Invoke helper function to do the actual loading.

   return AEEStaticMod_New(sizeof(AEEMod),pIShell,ph,ppMod,NULL,NULL);

}

  

  底层调用这个函数,需要提供三个参数。pIShell是ISHELL接口的指针,有了它就可以创建和访问其它接口。ph是helper function类函数的函数列表指针,有了它就可以调用MALLOC等函数。ppMod是用于返回给底层的地址,存储生成的module的信息。

  AEEMod_Load函数调用了AEEStaticMod_New函数。在这个函数中初始化了module数据结构,ppMod就是指向它的。函数结束后返回底层。

  底层稍后通过存储在module数据结构中的函数指针,调用同样位于AEEModGen.c中的AEEMod_CreateInstance函数,生成该Module的某一个Applet实例。在这个函数中会调用到我们所编写的AEEClsCreateInstance函数。在我们这个函数中注册了HandleEvent函数和FreeAppDate函数。这些信息同样通过一个applet数据结构指针返回给底层。这之后,会通过给HandleEvent发各种Event,来驱动程序运行。

 

  通过这整个过程,可以总结出程序运行的必要条件。

  程序必须得到IShell指针,Helper Function指针,这样程序中对底层各种函数的调用才能进行。底层必须知道两个地址,通过它们,可以将Event传递给程序,调用FreeAppDate函数。

  因此,想在程序中调用另外一个mod,只需要想办法把程序里的IShell指针,Helper Function指针传递给mod,同时得到mod中的HandleEvent函数,FreeAppDate函数的地址就可以了。

  剩下的问题就是,程序怎样和mod文件交互呢,mod文件的格式是怎样的?

 

  生成mod的mak文件最后一般有这样两句话,

 

ld *.o a.

fromelf –bin a. a.mod

 

  链接器ld生成格式,之后用格式转换工具fromelf将文件转换成mod文件。

  文件是带格式的可执行文件,对它的执行要靠操作系统的解析来进行。而通过–bin选项生成的mod文件,格式却是plain binary,即赤裸裸的二进制机器指令,其实是无格式的。只要将mod文件载入内存,跳转到它的0地址处,就可以一条条指令的执行下来。和mod文件交互,也就是要安排好mod文件0地址处的内容,知道mod文件的调用者会传入什么东西,mod文件会返回什么东西。

 

  

  下面来看一种实际的调用方法吧。代码如下:

 

typedef int (*RunLoadMod)(IShell *pIShell, void *ph, IModule **ppMod);

 

pData = MyLoadZip(pIShell);

if( !pData )

return EFAILED;

DBGPRINTF(”to RunLoadMod”);

if( SUCCESS != ((RunLoadMod)pData)(pIShell,ph,&pOrgMod) )

{

DBGPRINTF(”RunLoadMod_Err”);

goto Crt_Err;

}

  

  这段程序将mod文件读入内存,放在pData缓冲区内,用((RunLoadMod)pData)(pIShell,ph,&pOrgMod)一句执行之。RunLoadMod只不过是用typedef定义的一种函数指针类型。这句话的意思相当于是为待调用的mod文件准备好pIShell,ph,pOrgMod三个数据后,直接跳到内存中的mod文件的第一个字节处,将它当做普通机器指令一样执行。这种方法调用的mod文件,应是一个用完整的框架编译后生成的。0地址处放下的是AEEMod_Load函数。Mod文件直接将Applet信息传递给底层,注册HandleEvent函数。实际上是Mod文件取代了调用者的位置。

  

  但是这段程序很可能会运行失败。因为底层除了传参外,还做了其它事情。这在AEEStdlib.h中可以看出。

 

#define GET_HELPER()      (*(((AEEHelperFuncs **)AEEMod_Load) – 1))

#define GET_HELPER_VER()  (*((uint32 *)(((byte *)AEEMod_Load) – sizeof(AEEHelperFuncs *) – sizeof(uint32))))

 

  这两个宏定义可以看成是两个全局常量。第一个是 Helper Function的函数指针表,第二个是函数指针表的版本号。上面提到的通过参数传递Helper Function函数指针,是只在模拟器上成立的。手机上其实是通过全局常量形式传递的。为保证mod可以正常运行,也应在缓冲区前保存这两个常量。

 

  这种“冒名顶替”的方式,对于只想压缩一下mod的应用来说是完全可以的。但有时,我们希望新调用的mod只是整个应用的一小部分,调用者继续存在。这就需要调用者充当底层的一部分角色,记住mod返回的各种信息,在适当的时候调用mod中的相应函数。新调用的mod如果想要取得主mod的信息,类似于上面提到的HelperFuncs全局常量指针的方法是个可行的选择。

 

  其实知道了程序的加载过程,就可以比较灵活的达到调用mod的目的,甚至程序架构都是可以改动的。但是改动之后,如何在模拟器上进行调试就又成了一个问题。

 

  程序中调用mod的技巧,在国外几年前就已得到了应用。甚至有家公司专门开发了利用这个技巧压缩mod的工具。

 

http://www.s-cradle.com/english/products/sophiacompress_brew/index.html

 

参考文章:

 

1.How to build A mod to load B mod?

 

http://brewforums.qualcomm.com/showthread.php?t=11637

 

2.How to run mod file that is loaded in the HEAP…

 

http://brewforums.qualcomm.com/showthread.php?t=18413&highlight=modloader

 

3.深入模块加载机制

 

http://nicefuture.ycool.com/post.722858.html

 

4.深度剖析实现原理收藏

 

http://blog.csdn.net/Gemsea/archive/2006/09/07/1190206.aspx

Tags: , ,

24 Apr 09 [转] Symbian OS 9 可执行文件格式

原译者:陈啸天(cxt_programmer)
请关注www.cpplite.com及bbs.cpplite.com

声明:原文出自大牛——ScreenShot作者A ntony  P ranata 。本人英语很烂,之所以瞎译出来一是为了以后参考方便一些,二是在翻译的过程中自己能更仔细的看~。强烈建议大家对照英文原版来看哈。另外有些地方没有翻译,大家意会哈。

原文地址:

http://www.antonypranata.com/articles/new-symbian-os-9-executable-file-format-e32image

前言:

  9已经发布,与之前的版本相比, 9增加了许多新特性并有了一些改变,其中之一就是新的可执行文件格式(E32Image)。本文主要讨论了这个新的文件格式,如果你想了解 9之前的可执行文件格式,可以阅读一下我之前关于此话题的文章。

在开始之前,请注意我们仅仅讨论的是真机环境下的可执行文件格式(ARM)。为什么不讨论模拟器环境呢?因为运行在模拟器的可执行文件使用了一种不同的文件格式(Cxt注:模拟器下的可执行文件应该是windows的PE格式),如果你之前不了解任何一个平台的可执行文件格式,下面的介绍会更好的帮助你理解E32文件格式。

这篇文章是在网站或者其他拥有  licensees的厂商(例如Nokia)提供的公开信息(文档)之上完成的。虽然我目前在一家手机厂商工作,但这篇文章涉及内容的正确性并未经过公司的核对。尽管我已经紧握最大的努力确保这篇文章内容的正确,但我仍然无法100%保证(Cxt音:看看,大牛总是很谦虚的,我们要做一个谦虚的人哈)。

EABI介绍( Introduction to EABI ):

ABI(应用程序二进制接口,Application Binary Interface)是由ARM及其合作伙伴们制定的一个标准,它定义了如何编译、链接,以及其他工具怎样生成obj文件和可执行文件。这个标准可以让不同编译器生成的obj文件互通,例如可以把不同编译器生成的obj文件组合在一起。EABI(嵌入式应用程序二进制接口, Embedded Application Binary Interface )也是这样的标准,它就是嵌入式平台的ABI。

写这篇文章的时候,有两种编译器可以用于  9——RVCT( RealView Compilation Tools )和GCCE。RVCT是ARM公司开发的编译器,license费用可达数千美元;与此不同,GCCE是一个由 CodeSourcery 开发的免费编译器;顾名思义,GCCE基于GNU编译器。

    EABI编译器生成的格式为 Executable and Linking Format ),它不同于  9之前的PE格式。然而 并不使用格式,因为 通常在大小受限的ROM上,而标准文件的尺寸通常又很大。所以把标准的转换为特有的E32Image格式。如果你有安装 SDK,可以在epoc32\tools目录下找到用于把转换为E32Image的工具,它叫elf2e32.exe。

下图展示了  9的新工具链。如你所见,最后生成的是由elf2e32.exe转换完的特有格式——E32Image。网站和SDK中有更多关于这方面的介绍。(Cxt注:SDK help:   »   Symbian OS v9.1   »   Symbian OS Tools And Utilities   »   Build tools guide   »  The native build targets 目录下有全面详细的文档介绍)。通常你不会看到下图所示的流程,因为 使用一些脚本工具(例如bldmake、abld等)自动完成。

如果你不熟悉.dso文件也没关系,它其实与  9以及其他平台的.lib文件是一样的。(Cxt注:.dos提供导出的函数的名称和位置,DLL包含实际的函数和数据。)

我在前面说过,文件通常都很大,不适合用于手机。减少尺寸的方法之一是把函数名字替换为序号。例如可以把MyFunction()替换为1,正如你期望的,这种方法可以极大的减小的尺寸,特别是当函数名称很长时(例如 ThisIsVeryLongFunction ())这种效果更加明显。额等等!如此一来,如何找到序号和函数名称之间的对应关系呢?别急,我们有一个.def文件,它提供了序号与函数名之间的对应关系。(Cxt:呼 ……

E32Image概述( Overview of E32Image ):

现在让我们来看看E32Image吧。与其他标准的可执行文件类似,它也包含头(Header)、代码段(Code Section)、数据段(Data Section)、导入段(Import Section)等等。

上图展示了E32文件格式,与其他可执行文件格式一样,最开始的部分是header。下面我会着重介绍E32 header里面都有些什么东东。在Header之下还有一些其它section(Cxt:这部分大家意会哈^_^):

·  Code section, contains all the object files (.o) of your source code as well as export address table that lists all the exported functions.

·  BSS section, contains un-initialized data.

·  Data section, contains initialized data.

·  Import section, contains the information about all imported functions used by your program.

· Relocation section, contains relocation table needed by   loader to load your program.

E32Image头( Header of E32Image ):

头信息也许是最有趣的部分,因为它包含了可执行文件的很多信息。E32ImageHeader的声明可以在SDK的\epoc32\include\f32image.h中找到。看看class  E32ImageHeaderV 的声明,这可是E32Image文件格式完整的头信息。如果你仔细看了class  E32ImageHeaderV ,你会发现它继承了E32HeaderComp(E32HeaderComp继承了E32ImageHeader)。下图对它们的关系做了更好的说明:

下面的代码片段展示了 EImageHeader EImageHeaderComp E32ImageHeaderV 的声明,请注意我删减了部分函数以及注释以便于更清晰的查看:

class  E32ImageHeader

{

public :

  TUint32 iUid1;

  TUint32 iUid2;

  TUint32 iUid3;

  TUint32 iUidChecksum;

  TUint iSignature;  // ’EPOC’

  TUint32 iHeaderCrc;  // CRC-32 of entire header

  TUint32 iModuleVersion;  // Version number for this executable (used in link resolution)

  TUint32 iCompressionType;  // Type of compression used (UID or 0 for none)

  TVersion iToolsVersion;  // Version of PETRAN/ELFTRAN which generated this file

  TUint32 iTimeLo;

  TUint32 iTimeHi;

  TUint iFlags;  // 0 = exe, 1 = dll, 2 = fixed address exe

  TInt iCodeSize;  // size of code, import address table, constant data and export dir

  TInt iDataSize;  // size of initialised data

  TInt iHeapSizeMin;

  TInt iHeapSizeMax;

  TInt iStackSize;

  TInt iBssSize;

  TUint iEntryPoint;  // offset into code of entry point

  TUint iCodeBase;  // where the code is linked for 

  TUint iDataBase;  // where the data is linked for

  TInt iDllRefTableCount;  // filling this in enables E32ROM to leave space for it

  TUint iExportDirOffset;  // offset into the file of the export address table

  TInt iExportDirCount;

  TInt iTextSize;  // size of just the text section, also doubles as the offset for the

  // iat w.r.t. the code section

  TUint iCodeOffset;  // file offset to code section, also doubles as header size

  TUint iDataOffset;  // file offset to data section

  TUint iImportOffset;  // file offset to import section

  TUint iCodeRelocOffset;  // relocations for code and const

  TUint iDataRelocOffset;  // relocations for data

  TUint16 iProcessPriority;  // executables priority

  TUint16 iCpuIdentifier;  // 0×1000 = X86, 0×2000 = ARM

};

class  E32ImageHeaderComp :  public  E32ImageHeader

{

public :

  TUint32 iUncompressedSize;  // Uncompressed size of file

  // For J format this is file size - sizeof(E32ImageHeader)

  // and this is included as part of the compressed data :-(

  // For other formats this is file size - total header size

};

class  E32ImageHeaderV :  public  E32ImageHeaderComp

{

public :

  SSecurityInfo iS;

  // Use iSpare1 as offset to Exception Descriptor

  TUint32 iExceptionDescriptor;  // Offset in bytes from start of code section to Exception Descriptor,

  // bit 0 set if valid

  TUint32 iSpare2;

  TUint16 iExportDescSize;  // size of bitmap section

  TUint8 iExportDescType;  // type of description of holes in export table

  TUint8 iExportDesc[1];  // description of holes in export table - extend

};

     我将逐一解释上面这些字段。从下面的列表中,左端的16进制数表示这个字段在文件中的偏移量,例如iUid2的位置是从文件头部开始0×04的位置。换句话说,如果你把可执行文件用二进制编辑器打开,然后找到偏移量0×04的位置你就可以找到iUid2。注意E32Image使用小端格式(little-endian order)。

E32ImageHeader

0×00:   iUid1 ,可执行文件的第一个UID。这个UID可被看作是一个系统级别的标识符,例如Dlls是 0×1000 0079 ,可执行程序是 0×1000 007A 。如果你想更多的了解  UID相关信息,可以 访问这里

0×04: iUid2 ,可执行文件的第二个UID。当两个对象拥有同一个UID1的时候,就需要用这个UID来区分它们,例如 0×1000 39CE 表示多态接口Dll( polymorphic interface DLLs ),  0×1000 008d 表示静态接口( static interface (shared library) )。

0×08: iUid3 ,可执行文件的第三个UID。   每个程序的UID3都不同。下面这段不翻译了,贴个图看图识字吧^_^。

It is unique for each application. Developers have to request this UID from  Signed service.   9 applications usually have UIDs in the range of 0×200 0000 and 0×2FFF FFFF. Examples from   SDKs, like S60 SDK or UIQ SDK, have the UIDs in the range of 0xA000 0000 and 0xAFFF FFFF. There are also some UID available for testing, which can be chosen from the range 0×0100 0000 to 0×0FFF FFFF.

0×0C: iUidChecksum ,校验前面提到的3个UID。 SDK中提供了一个uidcrc.exe工具完成该功能;下面的例子展示了如何生成这三个UID: 0×1000 007A 0×1000 39CE  0xA000 017F 的校验码:

C:\>uidcrc 0×1000007A 0×100039CE 0xA000017F

0×1000007a 0×100039ce 0xa000017f 0×1e7cca07

0×10: iSignature ,E32文件唯一签名( unique signature )。值统一为“EPOC”。图示如下:

0×14: iHeaderCrc ,整个头信息的完整校验,使用 CCITT CRC-32 算法。

0×18: iModuleVersion ,可执行版本号。该信息用于链接过程(linking process)。在S60 3 rd  iModuleVersion  值为10( 0×0000 000A )(Cxt注:大牛这里可能笔误了,iModuleVersion是T u int32,占4个字节,这个值应该是0×000A 0000)。

0×1C: iCompressionType ,一个UID,表示使用哪种压缩算法压缩了可执行文件。如果值为0就说明未压缩。就我目前所看到的,只使用了一种压缩算法:由RFC 1951定义的 Deflate/Huffman 算法。它的UID是 KUidCompressionDeflate 0×101F 7AFC )。需要注意的是,未来也许会使用其他压缩算法。

0×20: iToolsVersion ,生成这个可执行文件的 ELFTRAN 工具版本。

0×24: iTimeLo ,时间戳。 the lowest word of the timestamp when the file is created.

0×28: iTimeHi ,时间戳。 the lowest word of the timestamp when the file is created.

0×2C: iFlags ,可执行文件的一个标记,比如 KImageDll  KImageNoCallEntryPoint  等。这些标记定义在f32image.h中。f32image.h中有一些函数定义用来解释这些标记的意思。例如:如果flag值为 0×1200 002A ,我们可以把它看作: 0×1000 0000 + 0×0200 0000 + 0×0000 00020 + 0×0000 0008 + 0×0000 00002 ,结合f32image.h中的常量声明,我们可以发现:

·  0×10000000 = KImageImpFmt_PE ,可执行文件使用-derived入口。

·  0×02000000 = KImageHdrFmt_V ,header的版本支持。

·  0×00000020 = KImageEpt_Eka2 ,EKA2可执行文件。

·  0×00000008 = KImageABI_EABI ,可执行文件为EABI image file。

·  0×00000002 = KimageNoCallEntryPoint ,no call to entry point。

    

0×30: iCodeSize is the size of code section, import address table, constant data and export dir.

0×34: iDataSize , size of initialised data.

0×38: iHeapSizeMin , the minimum size of the heap.

0×3C: iHeapSizeMax , the maximum size of the heap.

0×40: iStackSize , the size of the stack.

0×44: iBssSize , the size of the un-initialized data section.

0×48: iEntryPoint , offset into code of entry point.

0×4C: iCodeBase , where the code is linked for.

0×50: iDataBase , where the data is linked for.

0×54: iDllRefTableCount , the number of DLLs imported by this program.

0×58: iExportDirOffset , offset into the file of the export address table.

0×5C: iExportDirCount , the offset of the export address table.

0×60: iTextSize , size of just the text section, also doubles as the offset for the iat w.r.t. the code section.

0×64: iCodeOffset , file offset to code section, also doubles as header size.

0×68: iDataOffset , file offset to data section.

0×6C: iImportOffset , file offset to import section.

0×70: iCodeRelocOffset , relocations for code and const.

0×74: iDataRelocOffset , relocations for data.

0×78: iProcessPriority , executables priority.

0×7A: iCpuIdentifier , the identifier of CPU. Look at the following constant for all possible values:

enum TCpu

{

  ECpuUnknown=0, 

  ECpuX86=0×1000, 

  ECpuArmV4=0×2000, 

  ECpuArmV5=0×2001, 

  ECpuArmV6=0×2002, 

  ECpuMCore=0×4000

};                 

E32ImageHeaderComp

    开始之前,我们需要知道2个结构:定义在e32cmn.h的 ScapabilitySet SSSecurityInfo   9可以看到它们,它们保存了平台安全相关信息,比如能力(capability)、安全标识符(secure identifier)和厂商标识符(vendor identifier)。

struct SCapabilitySet

{

enum {ENCapW=2};

TUint32 iCaps[ENCapW];

};

struct SSecurityInfo

{

TUint32 iSecureId;

TUint32 iVendorId;

SCapabilitySet iCaps; // Capabilities re. platform security

};

好了,让我们回到 E32ImageHeaderV

0×80: iS.iSecureId ,可执行文件的安全ID(secure ID)。就是可执行文件的UID3。

0×84: iS.iVendorId ,可执行文件的厂商ID(vendor ID)。对于第三方应用程序,值为0。

0×88: iS.iCaps.iCaps ,运行可执行文件需要的能力。 所有的能力定义可以在 e32capability.h 文件中找到。

enum TCapability

{

  ECapabilityTCB = 0,

  ECapabilityCommDD = 1,

  ECapabilityPowerMgmt = 2,

  ECapabilityMultimediaDD = 3,

  ECapabilityReadDeviceData = 4,

  ECapabilityWriteDeviceData = 5,

  ECapabilityDRM = 6,

  ECapabilityTrustedUI = 7,

  ECapabilityProtServ = 8,

  ECapabilityDiskAdmin = 9,

  ECapabilityNetworkControl = 10,

  ECapabilityAllFiles = 11,

  ECapabilitySwEvent = 12,

  ECapabilityNetworkServices = 13,

  ECapabilityLocalServices = 14,

  ECapabilityReadUserData = 15,

  ECapabilityWriteUserData = 16,

  ECapabilityLocation = 17,

  ECapabilitySurroundingsDD = 18,

  ECapabilityUserEnvironment = 19,

};

    能力以bit的形式表示,例如 ECapabilityTCB 意味着最低有效位(LSB),如果设置了最低有效位,可执行文件就具有TCB能力。

0×90: iExceptionDescriptor is offset in bytes from start of code section to Exception Descriptor, bit 0 set if valid

0×94: iSpare2 , reserved.

0×98: iExportDescSize , size of bitmap section.

0×9A: iExportDescType[1] , type of description of holes in export table.

0×9B: iExportDesc[1] , is description of holes in export table.

Example

    下面我们来看一个例子,来帮助你更好的理解上面说的那些字段。我们将使用标准的  build command来编译3.0 SDK提供的Helloworld Basic例子。

C:\\9.1\S60_3rd\S60Ex\helloworldbasic>bldmake bldfiles

C:\\9.1\S60_3rd\S60Ex\helloworldbasic>abld build gcce urel

现在切换到 \epoc32 \r elease\gcce\urel 目录,使用 Elf2E32 工具读取E32 image头信息。做法如下:

C:\\9.1\S60_3rd\S60Ex\helloworldbasic>elf2e32 –e32input=helloworldbasic.exe

然后你将会看到 helloworldbasic.exe 的头信息;输出的过程可能会很长,所以一个比较好的方法是将他转储到文件,便于我们进一步分析:

C:\\9.1\S60_3rd\S60Ex\helloworldbasic>elf2e32 –e32input=helloworldbasic.exe > helloworldbasic.txt

打开 helloworldbasic.txt ,你将会看到头信息(我已经用绿色的注释标明了 E32ImageHeaderV 的字段):

  E32ImageFile ’helloworldbasic.exe’

  V2.00(505) Time Stamp: 00e0eb0a,d2525b80  // iTimeStampHi, iTimeStampLo

  EPOC Exe for ARMV5 CPU  // iCpuIdentifier = 0×20001 (ARMv5)

  Flags: 1200002a  // iFlags

  Priority Foreground

  Entry points are not called

  Image header is format 2

  Image is compressed using the DEFLATE algorithm  // iCompressionType

  Uncompressed size 0000b788

  Image FPU support : Soft VFP

  Secure ID: a000017f  // iSecureId

  Vendor ID: 00000000  // iVendorId

  Capabilities: 00000000 00000000  // iSs.iCaps.iCaps

  Exception Descriptor Offset: 00002561  // iExceptionDescriptor

  Exception Index Table Base: 00012dfc

  Exception Index Table Limit: 000130bc

   Segment Base: 00008001

   Segment Limit: 0000a77c

  Export Description: Size=000, Type=01  // iExportDescSize = 000 iExportDescType = 01

  Export description consistent

  Module Version: 10.0  // iModuleVersion

  Imports are -style

  ARM EABI

  Built against EKA2

  Uids: 1000007a 100039ce a000017f (1e7cca07)

  // iUid1 = 1000007a, iUid2 = 100039ce, iUid3 = a000017f, iUidChecksum = 1e7cca07

  Header CRC: 023aca0d  // iHeaderCrc

  File Size: 0000b788  // iUncompressedSize

  Code Size: 0000b0bc  // iCodeSize

  Data Size: 00000000  // iDataSize

  Compression: 101f7afc  // iCompressionType

  Min Heap Size: 00001000  // iHeapSizeMin

  Max Heap Size: 00100000  // iHeapSieMax

  Stack Size: 00005000  // iStackSize

  Code link addr: 00008000  // iCodeBase

  Data link addr: 00400000  // iDataBase

  Code reloc offset: 0000b650  // iCodeRelocOffset

  Data reloc offset: 00000000  // iDataRelocOffset

  Dll ref table count: 10  // iDllRefTableCount

  Offset Size Relocs #Relocs

  Code 00009c 00b0bc 00b650 00007d +002504 (entry pnt)

  // iCodeOffset = 00009c iCodeSize = 00b0bc iCodeRelocOffset = 00b650

  Data 000000 000000

  // iDataOffset iDataSize

  Bss 000000  // iBssSize

  Import 00b158  // iImportOffset

    全都在这了!!希望你能够喜欢这篇文章!

Tags: , , ,

18 Jan 09 《深入BREW》 – BREW原理

我们PC的Windows操作系统功能是如此的强大,以至于我们可以获得任何一个我们想要的程序,任何一个人都可以为Windows开发应用程序,或者给自己,或者为他人。而这一切源于操作系统的开放性和硬件平台的不断发展,尤其是存储器的发展,使得我们在编写程序的时候不必在意需要多大的存储空间了。

       然而,嵌入式系统可就没那么幸运了。至今为止,在嵌入式系统里仍然没有一个能够像Windows这样应用如此广泛的操作系统,也没有可以不考虑存储空间的硬件平台。在数以亿计的嵌入式设备使用者中,都还在用着一成不变的应用程序,单调同时也令人乏味。我们能不能也像在Windows下面一样,在嵌入式系统中可以安装应用程序呢?应该怎样克服嵌入式系统的限制而实现这个功能呢?
       有梦想才会不断的追求!我们知道,在Windows中程序都是以文件的形式存储在文件系统中的,然后通过操作系统控制这些程序的运行,我们可以说它的程序是“分散式”的。而在嵌入式系统中通常是将程序烧录在一个Flash芯片中,文件系统在另一个Flash芯片中(也可以二者在同一个芯片中),CPU是直接从程序Flash芯片中读取指令执行的,没有经过文件系统,我们可以叫这种程序是“一体式”的。Windows的“分散式”程序体通过文件的形式存在,可以把程序的不同部分分割成不同的文件,当我们只需要更新一个模块内容的时候,只更新这个文件就可以了。熟悉它的朋友们可能已经知道了,这个文件就是在Windows操作系统中的DLL文件。这样的方式可以很容易的实现程序分发,这给了我们一个很好的启示:嵌入式系统中也有文件系统,把程序放在文件系统里不就可以了吗?真是个好主意!
       在我们庆幸找到了好方法的时候,问题不偏不倚地出现了:系统如何运行文件系统中的程序,文件中的程序又如何调用平台中的函数?要实现“分散式”的程序运行,这两个问题是必须要解决的,而其中第二个问题就更为重要了。或许您现在还不是十分的明白这些问题的意义,不要着急,这一章里我会逐一的向您讲解如何理解并解决这两个问题。当然,现在我们知道BREW已经在嵌入式系统中解决了这两个问题,从现在开始就让我们沿着开发者的足迹去追寻BREW的本质吧。
1.平台的作用
       如果想要清楚的了解我们在嵌入式系统中所面临的问题,那么我们就首先需要了解“分散式”系统的结构。一个“分散式”系统需要有三个部分组成:平台、软件开发工具包(SDK: Software Development Kit)和应用程序。“分散式”应用程序的运行需要平台的支持,就像是DLL文件只有在Windows操作系统平台下才有作用,而到了Linux平台则不起任何作用一样;应用程序则通过SDK进行开发,开发出的源程序经过编译之后可以运行在运行平台之上。平台又分为开发平台和运行平台,开发平台是SDK运行的平台,用来开发可以在运行平台上运行的应用程序,对于一些系统还会提供模拟运行平台的模拟器,以便于在没有显而易见的运行设备的时候也可以看到开发的效果;运行平台是应用程序运行的平台,它提供应用程序运行的环境,同时肩负着控制应用程序的作用。开发平台和运行平台可以是同一个平台,也可以两个不同的平台,比如现在的Winows平台的应用就可以使用VC等工具开发基于Windows的应用程序,而BREW SDK则是运行在Windows环境下,但应用程序却在嵌入式系统中运行。它们之间的关系如下图:

 

图1 分散式系统结构图
       从这个图中我们可以看出,SDK需要使用运行平台的接口声明来开发应用程序,运行平台负责根据用户的输入启动应用程序,而应用程序则通过运行平台的接口调用运行平台的函数库来实现功能,我们的问题主要集中在应用程序和运行平台的互动关系上。
       从前面的编译器基础一章中我们可以知道,在固定链接的模式下,各个函数的地址是固定的,我们可以在同一个映像文件中调用任何函数。而存储在文件系统中的程序就不一样了,文件系统中的程序没有固定的位置并且地址也不连续,我们该怎样实现应用程序的启动呢?可选的方案就是将应用程序复制到一个连续的内存块中去,然后在内存中执行程序。在这里需要特别的说明一下,在PC的Windows操作系统中,Windows将全部的程序载入内存中运行,并且其中包含了复杂的内存管理功能,但是在嵌入式系统中通常程序是在Flash芯片中运行的,只有可读写的数据是放在RAM中的,具体的细节可以参考编译器基础一章。BREW主要是应用程序在嵌入式系统中的,因此将程序复制到内存中执行是需要特殊处理的。这个特殊的处理就是我们所面临的第一个问题了——系统如何运行文件系统中的程序。
       在我一开始理解BREW的时候,我曾经认为系统如何运行存在于文件系统中的程序是我们所面临的主要问题,但当深入BREW内部的时候发现根本不是这么回事。现在我们可以假设运行平台分配了一个足够大的内存,这块内存地址是已知的,可以想象的到我们可以从这个地址开始执行程序。现在我们暂时忽略那些特殊的处理,文件系统中的这个程序现在正在运行,就像在Flash中的程序一样的在运行。从理论上来讲这个是行得通的,因此系统如何运行文件系统中的程序的问题并不是我们所面临的难题。实际上BREW也是按照这个思路做的,只是实现时还有细节的东西在,关于这些细节我们将在Shell内幕一章进行详细的介绍。
       进一步的,假设现在程序运行到了需要调用平台函数的时候了,问题就来了,由于当前的应用程序是开发者使用SDK开发的,就像平台不知道应用程序的地址一样,应用程序也不知道平台的函数地址,因此,我们现在面临的问题是怎么能够知道应用程序中所调用的平台函数的地址。SDK可以提供运行平台中的每个函数的地址吗?可以提供,但是行不通。因为平台是会经常升级的,导致每个函数的链接地址不固定,如果由SDK提供所有函数的地址带来的问题是,只要运行平台一升级,那么SDK和应用程序都需要同时升级。如果这样的话我们的分散式程序就不能实现“分散式”的升级了,这种程序的运行方式也就没有任何意义了。看来我们还要寻找更为高级的方法,这种方法要能够提供一种应用程序与运行平台之间无关的机制。我们现在所需要的这个“机制”就是第二个问题了——文件系统中的程序如何调用平台的函数。
       从分散式系统结构图中我们可以看到,SDK使用的是运行平台的接口声明,应用程序调用真正的运行平台接口。或者换句话说开发过程中使用运行平台的接口声明,而在运行时应用程序使用真正的二进制接口,并且在二进制层面调用接口函数。那么,现在无论是SDK还是应用程序都与接口相关,那么,可以想象的到的解决第二个问题的方式就是让接口和接口实现之间分离。接口与实现间分离的方法就是BREW的核心,也是接下来我们主要阐述的议题。
2.软件分发和C语言
       为了更好的理解“实现接口和实现分离”所面临的问题,让我们先来看看通常的C语言软件库是如何分发的,这对于我们的理解非常有用。为了能够更加清楚地理解问题,我们在接下来几节的论述中不会仅仅局限于嵌入式系统,因为同样的问题也存在于PC系统中,更为重要的是PC系统所面临的问题更加典型,并且这些问题在软件系统中是具有普遍性的。
       想象现在有一个C语言库的开发厂商,它开发了一个算法,可以在O(1)时间效率内实现子字符串的搜索,O(1)时间效率的意思是指搜索时间为常数,与目标字符串的长度没有关系。为了实现这个功能,软件厂商生成了一个头文件FastString.h,包含如下内容:
FastString.h接口声明文件第一版
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
 
// 要求使用者不能直接使用此结构体中的内容
typedef struct _IFastString {
char *m_pString;   // 指向字符串的指针
} IFastString;
 
// 创建目标字符串对象
void IFastString_CreateObject(IFastString * pIFastString, char *pStr);
 
// 释放目标字符串对象
void IFastString_Release(IFastString *pIFastString);
 
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString);
 
// 查找字符串,返回偏移量
int IFastString_Find(IFastString *pIFastString, char *pFindStr);
#endif // FASTSTRING_H_
       除了这个头文件之外软件厂商还提供了接口的实现文件FastString.c
FastString.c接口实现文件第一版
#include ”FastString.h”
#include <string.h>
 
// 创建目标字符串对象
void IFastString_ CreateObject (IFastString * pIFastString, char *pStr)
{
IFastString *pMe = pIFastString;
 
if(pMe == NULL || pStr == NULL)
{
    return;
}
 
pMe->m_pString = malloc(strlen(pStr) +1);
strcpy(pMe->m_pString,pStr);
}
 
// 释放目标字符串对象
void IFastString_Release(IFastString *pIFastString)
{
    IFastString *pMe = pIFastString;
   
if(pMe == NULL)
{
    return;
}
 
if(pMe->m_pString)
{
    free(pMe->m_pString);
}
}
 
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString)
{
IFastString *pMe = pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
    return strlen(pMe->m_pString);
}
 
// 查找字符串,返回偏移量
int IFastString_Find(IFastString *pIFastString, char *pFindStr)
{
IFastString *pMe = pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
// 搜索算法省略,因为这里仅仅假设存在这样的一个搜索算法
}
       这个接口总共有四个接口:CreateObject、Release、GetLength和Find。CreateObject用来创建IFastString接口,从实现中我们可以看到它构建了IFastString结构体的内容。Release用来释放由CreatObject分配的内存。GetLength获得字符串的长度。Find用来在目标字符串中查找指定的字符串。在这个接口的实现中,使用了一个初始化的技巧(CreateObject和Release),目的是为了再使用前构建IFastString结构体,使用后可以通过Release释放构建时分配的内存。
       一般来讲这个库的使用者会将.lib库链接到自己的工程中,通过接口声明的头文件使用库中的函数,这是一个非常可行的做法。这样做带来的结果是库的可执行代码将成为客户应应用程序中不可分割的一部分。
       现在假设对于FastString库文件占用了大约16M的代码空间(这里假设为了完成O(1)算法可能需要十分复杂的程序,并且采取了一些空间换时间的策略等,这可能占用大量的存储空间)。如图2所示,如果现在有三个应用程序都在使用FastString库,那么每一个应用程序都将使用16M的空间来存储这些代码,总共花费了48M的空间。如果一个用户安装了这三个程序,也就是说有32M的空间浪费了,去存储了同样的FastString.lib中的代码。
 
                                                        图2 多个应用程序使用FastString库
       在这种情况下的另一个问题是,如果当前FastString库的厂商发现了程序中的缺陷,那么就没有任何办法可以替换已经存在的缺陷代码。一旦FastString的代码链接到了应用程序中,我们就不可能在用户的设备上替换这部分的代码。因此,库厂商不得不重新为每个应用程序的开发者广播发布新的库文件,并且希望他们能够重新的编译他们的应用程序,以便能够使用新的代码。很显然,这可真是一件麻烦的事情,一旦应用程序开发者链接了这个库文件,FastString便失去了模块化的特征。跟进一步说,这在嵌入式系统中是完全不可能的,在这里FastString的角色就是运行平台,我们总不能每一个应用程序都包含一个运行平台去啊。
3.动态链接
       解决上面问题的一种技术是使用动态链接技术(Dynamic Link)将FastString包含起来,这种技术的典型应用是Windows操作系统中的动态链接库(DLL文件)。这种方法是将FastString源文件编译成特殊的独立的二进制文件,并强迫FastString将所有的接口从二进制文件中引用出去,建立相应的引出表,以便于在运行时把每个接口的名字映射到对应的二进制接口地址上。与此同时还需要为使用者生成相应的引入库,通过这个引入库FastString的使用者可以获得FastString中每个接口的符号。引入库中并没有包含实际的代码,它只是简单的包含FastString引出的符号名字。当客户链接引入库的时候,这些符号信息会加入到当前的可执行文件中,运行时动态的装载FastString二进制库文件,并在执行时调用相应的程序。当然这些需要一些辅助工具的支持,例如编译器的支持等。此时应用程序的结构如图3:
                                                                            图3 动态链接示意图
       上图就是FastString在动态链接方式下的运行模式,这里面的引入库非常的小,所以可以忽略它占用的空间。在这种情况下,FastString的代码库就只需要一份了。运行时所有的应用程序调用同一个库中的内容,理论上当发现FastString中有缺陷的时候,我们可以更新FastString二进制组件而不影响应用程序。可以看到,我们已经迈出了重要一步,不过还没有完全解决我们所面临的问题。
4.封装性
       我们现在已经找到了一种可以实现动态链接的方法,那么下一个问题则与封装有关。考虑这样的情形:一个组织使用了FastString,同时希望能够在2个月内完成开发和测试。假设在这两个月中,某些具有特殊的怀疑精神的开发人员打算在他们的应用程序上测试一下FastString的性能,以便于测试O(1)时间效率的搜索算法。令人惊讶的是Find方法的搜索速度很快,并且与字符串的长度无关,但是GetLength方法的速度不是很理想,因为在GetLength方法中使用的是strlen计算字符串的长度,它查找字符串中的NULL结束符,它的算法需要遍历正个字符串的内容,因此它的执行效率是O(n),当字符串很长,而且调用次数很多的时候,执行的速度很慢。于是开发人员要求厂商提高GetLength操作的执行速度,使它在常数时间内完成。但是现在有一个问题,开发人员已经开发完成了他们的应用程序,他们不希望由于使用新的GetLength方法而更改任何现有的程序。而且其他的厂商可能已经发布了使用这个现有版本的基于FastString的产品,从任何方面将库厂商都不应该影响这些已经面世的产品。
       这个时候我们要查看我们的FastString的实现,以便确定哪些可以改变,哪些不可以改变。幸运的是,我们已经要求使用者不能直接使用IFastString结构体中的内容,假设所有的使用者都遵循了这一约定,于是我们很快的修改了GetLength的方法,将头文件改成了如下的方式(未修改部分未列出):
FastString.h接口声明文件第二版
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
 
// 要求使用者不能直接使用此结构体中的内容
typedef struct _IFastString {
char *m_pString;   // 指向字符串的指针
int m_nLen;       // 存储字符串的长度
} IFastString;
 
#endif // FASTSTRING_H_
将实现文件改成了如下方式(未修改部分未列出):
FastString.c接口实现文件第二版
// 创建目标字符串对象
void IFastString_ CreateObject (IFastString * pIFastString, char *pStr)
{
IFastString *pMe = pIFastString;
 
if(pMe == NULL || pStr == NULL)
{
    return;
}
 
pMe->m_nLen = strlen(pStr);
pMe->m_pString = malloc(pMe->m_nLen +1);
strcpy(pMe->m_pString,pStr);
}
 
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString)
{
IFastString *pMe = pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
    return pMe->m_nLen;
}
       很快的修改后重新发布了FastString的第二个版本。在这里一个显著的改进是在CreateObject时将字符串长度存储起来了,当用户调用GetLength方法时直接返回存储的长度。这样没有修改任何接口的内容,因此应用程序也就不需要修改了。
       客户收到了第二版的FastString后,替换了FastString的动态链接库,重新编译链接全部的应用程序,测试后发现不但原始代码不需要任何修改,而且GetLength方法的速度也大大加快了。最终这个第二版的FastString会随着这个产品而发布到用户手中。在安装应用程序的时候,第二版的FastString会将第一版的替换掉。这通常不会有问题,因为修改并没有影响公开的接口,因该只会增强原先已经安装的使用FastString的应用程序的功能而以。
       请想象这样的情形,当用户更新了第二版的FastString后,运行新版的应用程序,用户惊喜的发现程序运行的速度提高了。然后用户关闭了新的应用程序,而打开了另一个以前安装的使用旧版本FastSting的应用程序。现在的FastString已经替换成了第二版,因此用户发现这个应用程序的性能也增强了。然而不久异常出现了,系统出现了未知的错误。不过没关系,对于已经习惯了现代商业软件的人士来说,这不算什么问题,于是重新卸载并重新安装了两个应用程序,还是不起作用啊,异常依然发生!到底是怎么回事?
       原因在于IFastString结构体的修改。在未修改前sizeof(IFastString) == 4,因为只有一个char *m_ pString变量(假设系统是32位的)。修改后sizeof(IFastString) == 8,增加了4个字节。新版本的软件已经重新编译了,因此相应的IFastString结构体已经增加了空间。但是对于使用第一版FastString编译的应用程序来说,此时在应用程序里面分配的IFastString的空间依然是4,于是当第一版的应用程序调用第二版FastString的时候,将本该属于其他用途的4个字节用作了m_nLen的区间,这是十分粗暴的,产生异常也就不足为奇了。
       还记得前面的约定吗?我们要求FastString的使用者不可以直接对IFastString结构体中的数据进行直接操作,以达到应用程序与数据结构间的无关性。但实际上这样的约定基本上不可能被遵守,因为在实际中不管出于什么样的目的,总会有些开发者直接使用结构体中的内容(这些开发者使用的使FastString的C语言库文件形式,没有使用动态链接技术)。加上前面的异常,这一切的根源是我们没有一个实现二进制数据封装的方式。如果现在能够有一种可以将全部的数据结构封装在FastString内部的方法就好了。C语言是灵活的,只要我们找到了问题,我们就可以实现它。于是第三版的FastString新鲜出炉了,首先是头文件:
FastString.h接口声明文件第三版
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
 
typedef void IFastString;
 
// 创建目标字符串对象
void IFastString_CreateObject(IFastString ** ppIFastString, char *pStr);
 
// 释放目标字符串对象
void IFastString_Release(IFastString *pIFastString);
 
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString);
 
// 查找字符串,返回偏移量
int IFastString_Find(IFastString *pIFastString, char *pFindStr);
#endif // FASTSTRING_H_
接下来是实现的源文件:
FastString.c接口实现文件第三版
#include ”FastString.h”
#include <string.h>
 
typedef struct _CFastString {
char *m_pString;   // 指向字符串的指针
int m_nLen;       // 存储字符串的长度
} CFastString;
 
// 创建目标字符串对象
void IFastString_CreateObject (IFastString **ppIFastString, char *pStr)
{
CFastString *pMe = malloc(sizeof(CFastString));
 
if(pMe == NULL || pStr == NULL)
{
    return;
}
 
pMe->m_nLen = strlen(pStr);
pMe->m_pString = malloc(pMe->m_nLen +1);
strcpy(pMe->m_pString,pStr);
 
* ppIFastString = (IFastString *)pMe;
}
 
// 释放目标字符串对象
void IFastString_Release(IFastString *pIFastString)
{
    CFastString *pMe = (CFastString *)pIFastString;
   
if(pMe == NULL)
{
    return;
}
 
if(pMe->m_pString)
{
    free(pMe->m_pString);
}
 
free(pMe)
}
 
// 获取目标字符串长度
int IFastString_GetLength(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
    return strlen(pMe->m_pString);
}
 
// 查找字符串,返回偏移量
int IFastString_Find(IFastString *pIFastString, char *pFindStr)
{
CFastString *pMe = (CFastString *)pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
// 搜索算法省略,因为这里仅仅假设存在这样的一个搜索算法
}
       在这个实现中,我们使用了CFastString做为FastString的内部数据结构,定义了void型的IFastString类型做为接口指针传递,还有重要的一条是通过CreateObject获得数据结构的存储空间。通过这样的实现方法,我们将全部的数据类型封装在了FastString库中,这样,无论新的还是老的应用程序,使用的都是统一的IFastString指针,真正的数据是在CreateObject中进行创建的,也就不会出现上面的两种情况了。对于FastString的客户来说,他们所能看到的就是IFastString的void类型和四个接口函数,内部的CFastString的结构已经被隐藏起来了。不过这个第三版的FastString修改了接口函数CreateObject,因此不能够与前两版兼容。不过不要紧,我们现在只是在说明一个更好的方法而已。
4.虚拟函数表
       封装性的本质是实现了接口与实现之间在二进制层次的分离,第三版的FastString似乎已经解决了我们所面临的第二个问题。不过现在我们的接口仍然在使用着动态链接用的引入库文件,而且相应的动态链接库也需要提供由符号名到二进制函数地址映射的内容,为了支持这些特性,我们必须要修改相应的编译器才行。修改编译器,很复杂不是吗?
       让我们再回到嵌入式系统上来吧, CPU是在嵌入式系统中使用最广泛的CPU,因此相应的编译器也是应用最广泛的,基本上成为了一种通用的编译器。我们怎么修改这个编译器呢?似乎难度有点大。更近一步的,嵌入式系统中大大小小的CPU有好多种,我们也不可能把所有的这些编译器都修改了啊,看来修改编译器的可能性不大。
       又一次让我们体验到了理想与现实之间的差距!不过别灰心,看看我们现在的接口,它是二进制层面上的,我们能不能把这个引入库变成标准的C语言的头文件同时又能够实现接口与实现间的分离呢?如果真的能够实现这样的方法,那么将意味着我们可以通过通用编译器来实现动态链接的技术。这可真是令人兴奋,这个方法要比动态链接技术还要好。
       按照这个思路进一步分析,如果将现在的第三版FastString使用的C头文件做为标准的接口文件,那么将意味着各个接口需要静态的链接到应用程序中,接口和实现之间还是没有实现分离,难道我们转了一圈又回到原点了?真的又回来了,不过,不同的是我们现在已经得到了第三版的FastString。现在我们已经知道了通过CreateObject来获得内部的空间,那么我们是否也可以通过一个函数来获得接口呢?可以,这个技术就是虚拟函数表(VTBL)。
       这可真是“山重水复疑无路,柳暗花明又一村”啊,先看看我们这个第四版的FastString的头文件吧:
FastString.h接口声明文件第四版
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
 
typedef struct _IFastString IFastString;
typedef struct _IFastStringVtbl IFastStringVtbl;
typedef void (*PFNCreateObject)( IFastString **ppIFastString, char *pStr);
 
struct _IFastString
{
 struct IFastStringVtbl *pvt;
};
 
struct _IFastStringVtbl
{
void (*Release) (IFastString *pIFastString);
int (*GetLength) (IFastString *pIFastString);
int (*Find) (IFastString *pIFastString, char *pFindStr);
};
 
// 释放目标字符串对象
#define IFASTSTRING_Release(p) ((IFastString*)p->pvt)->Release(p)
 
// 获取目标字符串长度
#define IFASTSTRING_GetLength(p) ((IFastString*)p->pvt)->GetLength(p)
 
// 查找字符串,返回偏移量
#define IFASTSTRING_Find(p,s) ((IFastString*)p->pvt)->Find(p,s)
#endif // FASTSTRING_H_
       接下来是C语言的源文件:
FastString.c接口实现文件第四版
#include ”FastString.h”
#include <string.h>
 
typedef struct _CFastString {
    IFastStringVtbl *pvt; // 指向虚拟函数表的指针
char *m_pString;    // 指向字符串的指针
int m_nLen;        // 存储字符串的长度
} CFastString;
 
// 函数声明
static void IFastString_Release(IFastString *pIFastString);
static int IFastString_GetLength(IFastString *pIFastString)
static int IFastString_Find(IFastString *pIFastString, char *pFindStr);
 
IFastStringVtbl gvtFastString = { IFastString_Release,
                           IFastString_GetLength,
                           IFastString_Find
                          };
 
// 创建目标字符串对象
void IFastString_CreateObject (IFastString **ppIFastString, char *pStr)
{
CFastString *pMe = malloc(sizeof(CFastString));
 
if(pMe == NULL || pStr == NULL)
{
    return;
}
 
pMe->pvt = &gvtFastString;
pMe->m_nLen = strlen(pStr);
pMe->m_pString = malloc(pMe->m_nLen +1);
strcpy(pMe->m_pString,pStr);
 
* ppIFastString = (IFastString *)pMe;
}
 
// 释放目标字符串对象
static void IFastString_Release(IFastString *pIFastString)
{
    CFastString *pMe = (CFastString *)pIFastString;
   
if(pMe == NULL)
{
    return;
}
 
if(pMe->m_pString)
{
    free(pMe->m_pString);
}
 
free(pMe)
}
 
// 获取目标字符串长度
static int IFastString_GetLength(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
    return strlen(pMe->m_pString);
}
 
// 查找字符串,返回偏移量
static int IFastString_Find(IFastString *pIFastString, char *pFindStr)
{
CFastString *pMe = (CFastString *)pIFastString;
 
if(pMe == NULL)
{
    return;
}
 
// 搜索算法省略,因为这里仅仅假设存在这样的一个搜索算法
}
       首先对这个第四版的FastString程序做一个说明。在FastString头文件中我们定义了两个类型:IFastString和IFastStringVtbl。IFastStringVtbl类型是虚拟函数表的类型,IFastString中包含了指向虚拟函数表的指针。在接口定义的时候,我们使用了((IFastString*)p->pvt)来调用虚拟函数表中的函数指针,这也说明了如果要使用接口,必须要提供IFastString的指针类型。可以看出Release、GetLength和Find已经实现了在C语言定义的接口与实现函数之间的分离。
       接着看一下源文件中的情况。CFastString结构体与第三版的FastString不同的是增加了一个IFastStringVtbl类型的指针,而且这个指针在结构体的最顶部,如果将IFastString和CFastString对比一下我们可以发现他们都在顶部包含了IFastStringVtbl的指针,这就意味着CFastString是IFastString的超集。这一点是很重要的,我们可以看见在CreateObject函数中返回的IFastString指针其实是指向CFastString的指针的,只有CFastString是IFastString的超集的时候我们才可以这么做。在这个源文件中还定义了一个IFastStringVtbl的变量gvtFastString,并为这个变量初始化成了各个对应的函数,这个变量就使我们的虚拟函数表。虚拟函数表的示意图如下:
图4 虚拟函数表
       在这个第四版的FastString中,我们可以看到,除了CreateObject成员之外,其余的三个成员函数都已经添加到了虚拟函数表中,而且这个虚拟函数表还可以随着需求的增加而进行无限的扩大,我们只付出了一个函数CreateObject的代价就实现了无限多个接口和实现之间的分离了。
       由于用户需要使用CreateObject来获得IFastString的指针,因此我们没有办法将其与实现分离开,怎么办?现在只有这一根“线”还在困扰着我们,我们难道要功败垂成了吗?当然不能了。开动脑筋,回到我们应用程序的启动过程,对于一个程序,不管是由main函数或者其他的什么函数做为启动函数,都允许启动的时候传递参数,可能您已经想到了吧,我们把这个CreateObject函数做为参数传递给应用程序不就可以了吗?恍然大悟!这也就是为什么我们将CreateObject函数定义成了一个PFNCreateObject的函数类型的原因了,目的就是为了让使用者可以在应用程序中定义这种类型的函数指针。
       现在我们的应用程序、接口和实现之间已经分离了,中间使用了CreateObject这根细线连接了起来,只要接口不变,应用程序和实现之间就不会有任何的联系,包括二进制层面和C语言层面的。只不过这要求我们应用程序和接口的实现之间需要使用同一种编译器,或许这就叫做有得有失吧,不过对于嵌入式系统来说这是必须的,因为没有哪一种编译器可以支持全部的嵌入式CPU。
5.支持多个接口
       到现在为止所展示的技术已经解决了我们所面临的问题,不过对于一个平台来说不可能只有一个FastString接口,可能还有诸如FastNumber的接口。我们总不能把FastString和FastNumber两个接口的CreateObject都做为参数传递给应用程序的启动函数吧?看来我们现有的FastString需要进行一些扩展,来实现只传递一个参数给应用程序就可以创建多个接口的功能。在这里我们将增加一个叫做Shell的接口来管理其他的接口,相关的代码如下:
shell.h接口声明文件
#ifndef SHELL_H_
#define SHELL_H_
 
#define CLASSID_FASTSTRING 0×00000001
#define CLASSID_FASTNUMBER 0×00000002
 
typedef struct _IShell IShell;
typedef struct _IShellVtbl IShellVtbl;
 
struct _IShell
{
 struct IShellVtbl *pvt;
};
 
struct _IShellVtbl
{
void (*CreateInstance) (IShell *pIShell,
                    int nClassID,
                    void **ppObj,
                    unsigned int nUserData);
};
 
// 释放目标字符串对象
#define ISHELL_CreateInstance(p,c,pp,u) ((IShell*)p->pvt)->CreateInstance(p,c,pp,u)
#endif // SHELL_H_
源文件如下:
Shell.c接口实现文件
#include ”Shell.h”
 
typedef struct _CShell {
    IShellVtbl *pvt; // 指向虚拟函数表的指针
} CShell;
 
// 函数声明
static void IShell_CreateInstance(IShell *pIShell,
                           int nClassID,
                           void **ppObj,
                           unsigned int nUserData);
 
IShellVtbl gvtShell = { IShell_CreateInstance };
 
// 创建Shell对象
void IShell_CreateObject (void**ppObj, unsigned int nUserData)
{
CShell *pMe = malloc(sizeof(CShell));
 
if(pMe == NULL)
{
    return;
}
 
pMe->pvt = &gvtShell;
 
* ppObj = (void*)pMe;
}
 
// 创建由ClsID指定的
static void IShell_CreateInstance(IShell *pIShell,
                           int nClassID,
                           void **ppObj,
                           unsigned int nUserData)
{
    CShell *pMe = (CShell *)pIShell;
   
if(pMe == NULL)
{
    return;
}
 
switch(nClassID){
    case CLASSID_FASTSTRING:
        break;
 
    case CLASSID_FASTNUMBER:
        IFastNumber_CreateObject((IFastNumber **)ppObj, nUserData);
        break;
 
    default:
        break;
}
}
       在这个Shell接口中,我们定义了一个接口函数CreateInstance。它的作用是通过参数nClassID来创建指定的接口实例。IShell_CreateObject函数用来创建Shell接口本身,在使用的时候必须由系统直接调用IShell_CreateObject来产生Shell对象,然后再通过Shell接口来创建其他的接口(如FastString)。同时在这里包含了一个在本书中尚未实现的接口FastNumber,使用它仅仅是为了方便举例而已,因此有兴趣的读者可以仿照FastString接口实现FastNumber接口。
       从shell.h和shell.c文件中可以看到,Shell接口的实现方式与第四版的FastString实现方式是相同的。更进一步的,从CreateInstance接口函数的内部实现我们可以知道,它使用了一个Class ID来识别用户创建的是哪一个接口,并通过switch语句实现相关接口的CreateObject函数的调用。通过这样的Shell管理,应用程序只需要知道一个Shell接口的指针就可以创建其他的接口了。换句话说,在启动应用程序的时候,我们先调用IShell_CreateObject函数创建一个Shell指针,并将这个Shell指针做为参数传递给应用程序的启动函数,那么理所当然的,我们就可以在应用程序中使用Shell的接口ISHELL_CreateInstance来创建其他的接口了。通过这样的方式,我们不但可以实现接口的管理工作,而且同时也为接口的扩展性提供了足够的灵活性。
6.接口的扩展性
       到现在为止所展示的技术使得用户可以通过调用统一的C语言声明的接口,实现动态的二进制库的装载,这样可以无限制的升级库程序而不影响已有的应用程序,并且客户也不需要重新编译他们基于当前库文件所开发的应用程序,这对于创建一个复杂的运行平台来说是非常有用的。然而,这个接口却不能够随着时间而变化。这是因为客户在编译的时候需要有精确的接口定义,对接口的任何变化都需要客户重新编译他们的应用程序,以适应这种变化。更为糟糕的是,改变接口的定义完全违背了我们对接口封装性的要求。即便是最无伤大雅的变化,比如修改了接口的用途但是保留接口函数的原形不变,也会导致应用程序不再发生作用。这意味着接口的定义绝对不能改变,它既是语义上的约定,同时也是二进制层次上的约定。为了拥有一个稳定的,行为可预测性的运行时环境,接口的不变性这一要求是十分重要的。
       尽管接口具有不变性的原则,但是我们通常需要在一个接口定义好之后,希望能够加入原先设计时没有预测到的新功能。此时我们可以利用对虚拟函数表布局结构的知识,只是简单的把新的方法追加到现有接口的底部,就可以实现对接口的扩展。考虑下面的FastString接口声明:
typedef struct _IFastString IFastString;
typedef struct _IFastStringVtbl IFastStringVtbl;
 
struct _IFastString
{
 struct IFastStringVtbl *pvt;
};
 
struct _IFastStringVtbl
{
void (*Release) (IFastString *pIFastString);
int (*GetLength) (IFastString *pIFastString);
int (*Find) (IFastString *pIFastString, char *pFindStr);
};
       简单的修改接口vtbl的声明,在现有的结构体内增加新的接口函数的类型声明,这样得到的二进制声明结构是原有的声明的超集,因为新的方法总是出现在旧版本方法之后。在针对新方法的接口实现中,我们可以填充在这个结构体中的函数指针:
typedef struct _IFastString IFastString;
typedef struct _IFastStringVtbl IFastStringVtbl;
 
struct _IFastString
{
 struct IFastStringVtbl *pvt;
};
 
struct _IFastStringVtbl
{
    // 第一版接口
void (*Release) (IFastString *pIFastString);
int (*GetLength) (IFastString *pIFastString);
int (*Find) (IFastString *pIFastString, char *pFindStr);
// 第二版接口
int (*FindN)( IFastString *pIFastString, char *pFindStr, int n);
};
       这种方式完全可以正常工作。在第一版接口上开发的应用程序将忽略前三个接口之外的其他接口的信息。当老的应用程序使用第二版接口实现的二进制程序的时候,它仍然可以正常的工作。然而,新的客户总是希望可以使用新的方法FindN,以便于能够获得子字符串第N次出现的位置。如果此时应用程序的运行平台依然使用的是第一个版本的实现,那么不幸的,问题发生了。当调用一个未曾实现的接口时,显而易见的,程序崩溃了。
       这项技术的问题是修改了公开的接口,从而影响了接口的封装性。就像是只修改了C语言函数的声明会产生编译错误一样,改变了二进制的接口结构也会引起运行时的代码错误。这意味着接口必须是不可改变的,一旦公开后就不能再变化。解决这个问题有两种方法:一是允许接口的实现暴露多个接口,或者换句话说就是如果需要扩展老的接口,那么就重新定义一组新的接口,这样的话就可以在不支持新接口的旧平台上运行的时候判断接口的有效性,当然,这要求应用程序要对创建接口时的异常进行处理;二是支持在运行时对接口版本的判断或者在应用程序和运行平台之间有一种版本的比较机制,比如通过提供一个可以获得接口版本的API来进行版本的比较工作。不管是使用哪一种方法,无疑都会增加开发的负担,包括接口的开发者和用户的开发者,因此最好的方法是尽可能的不要去修改已经定义好的接口。
7.资源管理
       当我们在使用我们的接口的时候,我们还会遇到另外的一个问题,请看下面的代码:
IFastString *pFastString;
char TargetStr[] = “This is a test example only!”’
 
ISHELL_CreateInstance(pShell, CLASSID_FASTSTRING, (void **)&pFastString, (unsigned int)TargetStr);
 
(void)FastString_Test(pFastString);
       相应的FastString_Test函数如下:
int FastString_Test(IFastString *pFastString)
{
    int nOffset;
nOffset = IFASTSTRING_Find(pFastString, ”test”);
 
IFASTSTRING_Release(pFastString);
return nOffset;
}
       在这个例子里,pFastString在主函数中创建,同时做为参数传递给了FastString_Test函数,然后释放pFastString,这很正常,没有什么问题。在FastString_Test函数中调用了Find方法,并且在使用函数退出的时候释放了pFastString指针,这很正常,也没有什么问题。然而,当我们将两者结合起来的时候,问题出现了:主函数中释放了一次接口指针,而在FastString_Test函数中也释放了一次同样的接口指针。换句话说,在这个程序中由于开发者的疏忽,对同一个接口释放了两次,这将导致不可预测的异常发生。这里面的问题是在增加了对接口指针的引用的时候,没有相应的处理机制来记录当前实例引用的次数,也就是当前接口实例的一个资源管理的问题。
       或许您可以说这个问题可以通过使用者的细心来避免,那么我们再来看另外一个问题:如果当前的接口是可以共用的一些函数,比如这个接口中的方法全部是诸如STRLEN之类的重定义助手函数接口,并且在创建接口的时候需要分配一些公用的内存空间,那么,我们在每一次创建这个接口实例的时候,都必须分配不一样的存储空间吗?如果我们这样做了,实际上不会有什么问题,但是会浪费存储空间,因为他们本来是可以全部共用的。况且,如果我们全部都使用同一个指针在各个函数之间调用,那么对于这个指针来说使用起来会很危险的,因为我们不知道每一个函数是怎样处理这个指针的,这可真是太糟糕了。
       为了解决这个问题,一个可行的做法是为每一个接口增加一个引用计数的管理机制。在增加了这个机制之后的第五版FastString实现如下:
FastString.h接口声明文件第五版
#ifndef FASTSTRING_H_
#define FASTSTRING_H_
 
typedef struct _IFastString IFastString;
typedef struct _IFastStringVtbl IFastStringVtbl;
 
struct _IFastString
{
 struct IFastStringVtbl *pvt;
};
 
struct _IFastStringVtbl
{
int (*AddRef) (IFastString *pIFastString);
int (*Release) (IFastString *pIFastString);
int (*GetLength) (IFastString *pIFastString);
int (*Find) (IFastString *pIFastString, char *pFindStr);
};
 
// 增加接口指针的引用计数
#define IFASTSTRING_AddRef(p) ((IFastString*)p->pvt)->AddRef(p)
 
// 释放目标字符串对象
#define IFASTSTRING_Release(p) ((IFastString*)p->pvt)->Release(p)
 
// 获取目标字符串长度
#define IFASTSTRING_GetLength(p) ((IFastString*)p->pvt)->GetLength(p)
 
// 查找字符串,返回偏移量
#define IFASTSTRING_Find(p,s) ((IFastString*)p->pvt)->Find(p,s)
#endif // FASTSTRING_H_
       相应的实现文件如下:
FastString.c接口实现文件第五版
#include ”FastString.h”
#include <string.h>
 
typedef struct _CFastString {
    IFastStringVtbl *pvt; // 指向虚拟函数表的指针
int m_nRef;
char *m_pString;    // 指向字符串的指针
int m_nLen;        // 存储字符串的长度
} CFastString;
 
// 函数声明
static int IFastString_AddRef(IFastString *pIFastString);
static int IFastString_Release(IFastString *pIFastString);
static int IFastString_GetLength(IFastString *pIFastString)
static int IFastString_Find(IFastString *pIFastString, char *pFindStr);
 
IFastStringVtbl gvtFastString = { IFastString_AddRef,
                           IFastString_Release,
                           IFastString_GetLength,
                           IFastString_Find
                          };
 
// 创建目标字符串对象
void IFastString_CreateObject (IFastString **ppIFastString, unsigned int nUserData)
{
CFastString *pMe = malloc(sizeof(CFastString));
char *pStr = (char *)nUserData;
 
if(pMe == NULL || pStr == NULL)
{
    return;
}
 
pMe->pvt = &gvtFastString;
pMe->m_nLen = strlen(pStr);
pMe->m_pString = malloc(pMe->m_nLen +1);
strcpy(pMe->m_pString,pStr);
 
pMe->m_nRef = 1;
* ppIFastString = (IFastString *)pMe;
 
}
 
// 增加接口指针的引用计数
static int IFastString_AddRef(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
 
return (++pMe->m_nRef);
}
 
// 释放目标字符串对象
static int IFastString_Release(IFastString *pIFastString)
{
    CFastString *pMe = (CFastString *)pIFastString;
   
if(–pMe->m_nRef >0)
{
    return pMe->m_nRef;
}
 
if(pMe->m_pString)
{
    free(pMe->m_pString);
}
 
free(pMe)
}
 
// 获取目标字符串长度
static int IFastString_GetLength(IFastString *pIFastString)
{
CFastString *pMe = (CFastString *)pIFastString;
 
    return strlen(pMe->m_pString);
}
 
// 查找字符串,返回偏移量
static int IFastString_Find(IFastString *pIFastString, char *pFindStr)
{
CFastString *pMe = (CFastString *)pIFastString;
 
// 搜索算法省略,因为这里仅仅假设存在这样的一个搜索算法
}
       在第五版的FastString中,我们主要是增加了接口AddRef。这个接口是在增加指针引用的时候调用的,它仅仅增加了接口内部变量m_nRef。同时为了实现相应的机制,在Release方法内增加了对引用计数的条件判断:如果当前的引用计数不为零,则直接返回引用计数的值,否则释放创建接口实例时所分配的内存。这样,当我们在使用上面的FastString_Test函数之前,调用IFASTSTRING_AddRef方法,就解决了资源管理的问题了。
       增加这个AddRef方法还有一定的人为因素,因为只要程序员能够足够的注意,那么就不会存在资源管理的问题。但是,人生不如意十有八九,我们不能将全部的希望都寄托在程序员的身上,谁都有犯错误的时候。因此增加了AddRef的约定,它与Release方法相对应,形成了一种对称的编程“美感”,约定了是要增加了对指针的引用就调用AddRef,对应的在在适当的位置使用Release释放指针引用。
       细心的读者可能还会发现第五版的FastString的实现中有两处不一样:一是IFastString_CreateObject函数的参数变化了,由原来的char *类型成了现在的unsigned int类型,这样做的目的是为了统一在Shell程序中CreateObject的形式;二是FastString接口函数中if(pMe == NULL)的判断去掉了,我们知道,在第一版的FastString中这句判断是很有必要的,它可以检测当前指针的有效性,那么想象一下对于我们的虚拟函数表NULL指针意味着什么?根据我们已有的虚拟函数表的知识,调用的接口是基于IFastString指针的相对偏移量,例如使用NULL指针来调用Release接口,那么实际上调用的是基于0地址的4字节偏移位置的函数,只有天知道这个地址中存储的是什么东西!因此,为接口传递空指针的时候会不可避免的发生异常,根本就不可能执行到接口函数,所以相应的判断是没有任何意义的。相应的示意图如下:
图5 接口的偏移量
       最后,再让我们清楚地看一下这种虚拟函数表所实现的总体框图吧,看看应用程序、接口定义以及接口实现之间的关系:
图6 应用程序、接口定义以及接口实现的关系
       至此,我们已经完成了全部需要解决的问题,最终我们实现的第五版FastString就是BREW接口的实现方法,只不过在实现的细节上有所不同。同时,我也相信各位在阅读了第二部分之后,一定会对这一部分的介绍颇有感触。接下来我们就趁热打铁,介绍BREW实现方法的一些高级特性,其中涉及了面向对象和COM组件的相关知识,对于没有接触过这两部分内容的读者来说,阅读可能会有一定困难。不过我会尽我最大的努力,争取用最容易理解的方式来阐述这些特性。
7.面向对象的特性
       C语言本身没有面向对象的特性,但是我们使用C语言开发的BREW就具有了面向对象的特性了。面向对象的主要特征是:数据抽象、继承和多态。数据抽象指的是使用一组数据和方法描述一个我们要表达的内容(对象),它的关键点在于将方法和数据结合也叫做封装;继承是指一个对象可以通过某种方式使用另一个对象中的方法和数据,它包含了语法上的继承和二进制层次的继承;多态是指通过虚拟函数实现的成员函数晚捆绑的特性,其核心的特征是成员函数的晚捆绑。
       BREW具有良好的数据抽象特性,它将全部的数据封装在了接口实现的内部,只将接口函数暴露给外部使用。这种方式是实现数据封装的理想方式,我想这一点可以从前面的“封装性”一节看出来。因此,BREW已经具有了面向对象的第一个特性。
       在这个最好的封装实现的基础上,根本没有任何数据直接暴露出来,因此,继承性就体现在接口的继承上面了。由于BREW是使用C语言实现的,因此没有办法实现语法上的直接继承,但是它实现了二进制层次上的继承。继承在二进制上的表现就是在本数据结构中兼容另一个数据结构。例如,假设结构体A中包含了成员int i,现在有结构体B包含了同样的int i类型的成员,并且后面紧跟了int j成员,此时B结构是A结构的超集,也可以说B结构继承了A结构。现在我们已经知道BREW接口的二进制结构,如果需要实现二进制的BREW继承,我们就可以通过定义一个接口A的超集来实现另一个接口B,此时我们就可以说这个接口B是从接口A继承来的。我们可以通过接口A的定义使用接口B中的方法。因此说,BREW具有面向对象的继承的特性,只不过不是在语法上,而是在二进制的数据结构层面。
       面向对象的第三个特性就是多态,可以说这个特性是BREW天生的。我们知道BREW一开始就是通过VTBL来实现的,可以实现运行时的接口函数绑定(也就是迟绑定)。关于这里点我们可以从五个版本的FastString的实现中发现这一过程。晚捆绑与早捆绑的区别在于早捆绑在使用接口的时候再编译链接的时候就已经确切的知道每个接口函数的地址,这就类似于使用一个C语言的库;而晚捆绑则是在运行时才知道每个接口函数的确切地址,因为虚拟函数表是在运行时才赋值给相应的接口的。使用这种技术的一个动机是我们可以在运行的时候控制使用一个接口中的多个实现中的哪一个实现。如果当前FastString接口已经公开了,那么可能会有第三方的厂家按照这个接口规范,来实现一个更好的FastString接口。与最原始的FastString接口布局一样,新的实现的接口函数布局与老的一样,那么此时仅仅需要更新一个Class ID我们就可以切换到新的接口了。使用晚捆绑的另一个动机是,应用程序可以检测到当前的运行平台是否已经实现了此接口,并给予相应的处理,这样可以避免在接口为实现的平台上运行时引起致命的错误。
       综上所述,BREW具有了面向对象的三个主要特性,因此说他是具有面向对象性质的一种开发平台。
8.与COM的比较
       COM是Windows平台上实现的一种跨语言的开发机制,目前在Windows平台的底层,许多功能都是通过COM机制来实现的。COM通过统一的、独立的接口定义语言(IDL:Interfase Definition Language)来定义统一的接口,并规定了相应的接口二进制规范,这样就可以按照这个二进制规范,通过各种不同的开发语言来实现COM程序的开发,而实现这种接口与实现之间完全分离的技术就是虚拟函数表(VTBL)。
       熟悉COM开发的读者对BREW应该有一种似曾相识的感觉,没错,在我的定义中,BREW就是一个简单版本的COM。BREW与COM相比,它们的核心思想是十分一致的,都具有接口与实现分离的特性,都使用了VTBL的技术等等。它们的不同点主要表现在以下几个方面:
       第一,它们的接口与实现间的分离程度不同。COM是接口和实现的完全分离,为此专门规范了统一的接口定义语言,因此而接口的实现可以采用任何一种开发语言,如C/C++和Java等。而BREW则为了简化开发,使用了C语言形式的接口定义,这样就使得BREW的应用程序和实现都需要基于同样规则的编译器。或者换句话说,COM实现是与开发语言无关的,而BREW的实现则是与语言相关的。
       第二,它们的接口创建方式不同。COM通过Windows注册表,使用文本的名字寻找相应的Class ID(这个Class ID需要通过注册程序进行注册),例如FastString接口可以通过传递字符串“FastString”最为参数从而创建FastString接口,实现这个功能的基础是运行时的类型识别技术(RTTI,相关的内容可以参考VC的书籍)。而BREW则为了简便起见,仅仅通过Class ID来创建接口。
       第三,它们的规范层次不同。COM是二进制层次的规范,只要符合COM的二件制规范,我们可以使用任何一种语言进行开发。而BREW则是一种开发语言上的规范(使用了统一的C语言接口定义)。当然,我们也可以把BREW做为一种二进制层次的规范,但是似乎这并不大适合于在嵌入式系统中应用程序,因为当前大部分嵌入式系统都是使用C语言来开发的。不过对于BREW应用程序来说,只要遵循BREW的调用约定,是可以使用其它语言开发的。这里就不详细的讨论这个问题了。
9.小结
       本章介绍了BREW实现的来龙去脉,展示了BREW所需要解决的两个主要问题:如何启动程序以及如何调用平台中的函数。最终,我们通过五个版本的FastString事例解决了全部的问题。BREW的本质是通过虚拟函数表技术实现了接口定义与接口实现之间的分离,这样BERW应用程序就可以存储在文件系统中,并在运行的时候调用接口函数。可以这样说,BREW已经将C语言运用到了极至,理解了BREW的原理可以对程序的开发有更加深入的认识。

Tags:

18 Jan 09 《深入BREW》 – 硬件基础

转至Gemsea大人的BLOG:http://blog.csdn.net/Gemsea

 硬件是软件的运行平台,没有硬件的支撑软件也将不复存在。您能想象没有显示器软件将如何显示图形,没有CPU软件将如何运行吗?反正我想象不到!但是如果把问题反过来问就问到本质了,软件运行需要哪些硬件支持呢?看图1.1:

CPU
RAM
ROM
输出设备
输入设备
存储设备

图1.1 系统结构框图
       我们抛开硬件的什么电器特性等等,去芜存菁,就是上面的这个图了。如果程序要运行没有CPU是不行的,CPU要快速的交换数据,没有RAM也是不行的。因此无论任何系统,CPU和RAM都是必不可少的。您一定会提醒我ROM不也是不变的吗?这种说法不完全对,因为在PC系统和嵌入式系统之间ROM的作用是不一样的。在PC系统中ROM就是那个BIOS芯片,是用来提供系统的启动代码和基本的输入输出功能的;而在嵌入式系统中,ROM存储了全部的代码,它已经将PC中的BIOS和硬盘的与代码相关的功能混合在一起了。
设备
PC系统典型硬件设备
嵌入式系统典型硬件设备
CPU
任何CPU
任何CPU
RAM
任何RAM
任何RAM
ROM
BIOS芯片
Flash芯片
存储设备
硬盘
Flash芯片
输入设备
键盘
键盘
输出设备
显示卡+显示器
LCD显示屏
       PC的ROM ——BIOS芯片可以采用Flash芯片,在这里之所以不写成Flash芯片是因为BIOS的作用和嵌入式系统的Flash作用不大一样,使用BIOS以示区分。
1.1 CPU和RAM
从软件观点来讲,任何CPU和RAM都可以应用于各种系统中,不存在明显的区别,只要CPU可以执行指令控制设备就可以了。但是考虑到耗电以及体积(嵌入式设备通常要求耗电低、体积小)等问题,嵌入式系统就发展出了专用的CPU芯片。当前应用最广泛的是 CPU。 CPU是由英国的公司设计的,由于其执行效率高,体积小,耗电少等特点被广泛应用于嵌入式系统。由于嵌入式系统要求高集成度,通常不会存在单独的CPU芯片,而是将CPU和很多的外围电路集成到一起,做成一块芯片,因此采用授权的方式提供内核芯片设计,以便于使用者进行芯片的集成。
CPU按照次执行指令的数据带宽可以分为16位、32位、64位等。32位CPU一次只能处理32位,也就是4个字节的数据;而64位CPU一次就能处理64位即8个字节的数据。如果我们将总长128位的指令分别按照16位、32位、64位为单位进行编辑的话:旧的16位CPU(如Intel 80286 CPU)需要8个指令,32位的CPU需要4个指令,而64位CPU则只要两个指令。显然,在工作频率相同的情况下,64位CPU的处理速度比16位、32位的更快。
除了运算能力之外,与32位CPU相比,64位CPU的优势还体现在系统对内存的控制上。由于地址使用的是特殊的整数,而64位CPU的一个ALU(算术逻辑运算器)和寄存器可以处理更大的整数,也就是更大的地址。传统32位CPU的寻址空间最大为4GB,使得很多需要大容量内存的大规模的数据处理程序在这时都会显得捉襟见肘,形成了运行效率的瓶颈。而64位的处理器在理论上则可以达到1800万个TB(1TB=1024GB),将能够彻底解决32位计算系统所遇到的瓶颈现象。当然64位寻址空间也有一定的缺点:内存地址值随着位数的增加而变为原来的两倍,这样内存地址将在缓存中占用更多的空间,其他有用的数据就无法载入缓存,从而引起了整体性能一定程度的下降。
在进行系统设计时,会根据不同寻址能力的CPU来进行寻址空间的分配。由于CPU都是通过设备的寄存器(这个寄存器可以理解为设备本身带的RAM)来控制设备的,因此地址空间的划分就显得十分重要。例如,一个具有32位寻址能力的CPU不可能讲全部的地址空间都分配给RAM,好比PC系统需要为BIOS分配存储空间等。也就是说只要是需要CPU直接控制的外部设备都需要为其分配CPU地址空间。
RAM就是可以随机访问,快速读写的存储器。CPU可以直接从RAM中取得数据(CPU可以从所有分配了地址空间的设备寄存器中取得数据)或代码指令,因此RAM的访问速度将直接影响系统的性能。
1.2 ROM存储芯片
       ROM是每个计算机系统必不可少的,但是其实现的方式却不尽相同。在我们熟悉的PC系统中,ROM是一个称作BIOS(Base Input & Output System)的芯片。CPU上电时会从ROM中读取指令,因此没有ROM的系统是不能够运行的,因为如果没有ROM,CPU将无法获得起始的执行指令。在PC系统中BIOS的作用除了提供起始指令以外,还会扫描硬件设备并初始化主板(Main Board)上的硬件接口。由于PC上的接口都遵循着一组通用的协议,因此BIOS就可以实现所有硬件接口的驱动(如USB接口、显示卡、键盘和鼠标等)和硬件的数据输入输出功能,这也是BIOS(基本输入输出系统)名称的由来了。在BIOS控制的硬件接口中也包含了硬盘的控制接口,在BIOS初始化完成后就会到硬盘主分区上查找启动文件(3),后面的事情就交给PC的操作系统了。这个过程请看图1.2。
       通常,在硬盘内有一个主引导记录区,在安装操作系统的时候由操作系统写入Boot程序,BIOS就是取出这段程序然后执行。由于Boot程序是由操作系统写入的,因此从这个Boot程序开始,系统的运行权限就交由操作系统来控制了。以Windows2000操作系统为例,这段代码区域执行时会搜索名叫“NTLDR”的系统文件。之所以分成Boot程序和NTLDR文件的原因是硬盘的Boot Sector很小(只有466Bytes),不可能容纳全部的启动程序。
       图1.2只是一个启动的示意图,在这里我并没有详细的列出每一个必须的步骤,因为我的目的只是让我们能够了解BIOS硬件芯片在系统中的作用。
       对比PC的BIOS,嵌入式系统由于软件规模小,因此将引导代码和操作系统代码全部放到了系统的Flash芯片中了。正如我们所知道的,PC机上大部分的操作系统代码全部放在硬盘上,然后从硬盘上将程序载入内存执行。而嵌入式系统中目前大多数采用直接寻址的方式从Nor Flash芯片中读取代码并执行。因此,实际上嵌入式系统简化了PC系统的设计,将PC系统中的BIOS和硬盘代码全部集中到了一个Flash芯片上。因此BIOS虽然也可以使用Flash芯片,但是相对于嵌入式系统来说,他们的含义和作用却不同。

BIOS自检
显卡BIOS检测
CPU型号检测、内存测试
读取硬盘启动扇区
获取操作系统启动文件

图1.2 PC开机流程
Flash芯片对于我们来说并不陌生,那些可以更新BIOS程序的BIOS芯片也是使用Flash芯片实现的,还有MP3用的SD卡等等也是Flash芯片。Flash芯片是一种可以多次擦写的存储芯片,广泛的应用于嵌入式系统。Flash的特点是耗电低,容量大(相对于嵌入式系统而言),写入之前需要先擦除(因为Flash芯片的存储单元只允许从1变到0)。当前流行的分为NOR Flash和NAND Flash。
NAND与NOR Flash的区别主要有:
1、NAND Flash的空间比Nor Flash大
2、NAND Flash的访问速度比Nor Flash快
3、NAND Flash只有Page访问模式,Nor Flash可以进行Page和直接地址访问(直接地址访问也就是CPU可以直接寻址,或者叫做随机访问)
4、NAND Flash允许有坏块,但是Nor Flash不能有坏块
5、NAND Flash比NOR Flash更加便宜
在嵌入式系统中,NOR 和NAND都可以做为代码区和文件系统区来使用。通常情况下NOR和NAND做为嵌入式文件系统区的时候都使用Page模式。Page模式允许一次读取多个字节,就像硬盘的最小读写单位是扇区一样,只不过Flash的最小读写单位叫做Page。Page模式下可以加快Flash的读写速度。由于NAND Flash只支持Page读写模式,因此使用NAND Flash做为代码区的时候需要外加控制电路。当前使用NAND做为代码区正在成为一种流行的趋势(因为NAND Flash成本更低),主要的实现方式有两种:一是添加仿真电路使得NAND Flash可以支持随机访问;二是增加一个类似硬盘的引导区(通常是第一个Page),系统启动的时候使用引导区的代码将全部NAND中的代码复制到RAM中执行。
当然,可以设想随着将来嵌入式操作系统的发展,动态载入内存的形式也许就会出现了,但是目前嵌入式系统仍然没有发展到这个地步。更进一步,当前应用于嵌入式系统的微型硬盘也已经出现了,或许更为复杂的操作系统也可以应用在嵌入式系统上了。
在这里我们主要介绍的ROM存储芯片是Flash,严格意义上说Flash并不能称作ROM,因为ROM是只读存储器(Read Only Memory),而Flash是一种可读可写的芯片。但是由于ROM“一次成型、终生不变”的特点,不便于升级换代,现在正逐渐的被Flash芯片所取代,但是其功能性的称谓“ROM”还在为大家所用。
在计算机系统中主要存在用户数据、程序数据和代码三种二进制内容。用户数据指用户文件,程序数据是指程序运行时需要修改或使用的非代码内容。下面将就PC系统和嵌入式系统中的这三种二进制内容做一个比较,请看下表:
二进制形态
PC系统
嵌入式系统
用户数据
存储在文件系统中,典型的设备是硬盘
存储在文件系统中,典型的设备是Flash存储芯片
程序数据
可读可写的数据存放在RAM中;只读数据存放在硬盘中,运行时与代码一起读入RAM
可读可写的数据存放在RAM中;只读数据存放在Flash中,与代码存储在同一个区域
代码
存储在文件系统中的文件里,运行时读入RAM由CPU执行
如果存储在NOR Flash等可随机访问的空间中则CPU直接在芯片中取指令运行;如果存储在NAND Flash等不能随机访问的空间中则需要读入RAM中运行
关于程序数据的详细情况将在编译器基础一节详细介绍。
1.3 输出设备
       输出设备有很多种,例如显示器、打印机,在这里我们主要讲一下显示设备。
任何显示设备都是点阵式的,至少目前是这样。还记得初次看见电视里显示的人物时内心的惊异,这个世界竟是如此神奇!后来知道了,如果我愿意,我可以买640*480个灯泡,组成一个640*480的方阵,然后控制每个灯泡的亮灭,我也可以显示一个人物。终于懂了,原来任何的显示设备都是基于这个原理实现的。我想关于这一点我没有必要再多说什么了,毕竟万变不离其宗嘛。
       当前流行的显示设备有CRT显示器(也就是电脑上很大个头的那种显示器),LCD(液晶)等(虽然我在大学实验室里用的可以显示数字的LED灯也是显示设备,但是他太简单了)。CRT显示器经历了从球形到纯平的物理演变,LCD则经历了从黑白到彩色的变化。按照显示器每个点能够显示的颜色数目可以分为黑白两色、灰度、8位色、16位色、24位真彩色等显示设备。这个颜色数目就是所谓的色深(Color Depth),色深越大,每个点能够表达的颜色数就越多,这个点就是“象素”。
       在嵌入式系统中主要使用LCD的显示设备,LCD会集成一个显示的存储空间,在这个空间中存储了对应的每个象素的值。例如16位色的LCD每个象素需要由2个字节来表示,如果显示屏幕的大小是100*100,那么就需要2*100*100 = 20000个字节的存储空间。程序正是通过更新这个存储空间中的内容来控制LCD的显示。
衡量LCD显示效果的还有象素间距,象素间距越小,画面就越细腻,显示效果越好。举个极端的例子,如果一个象素是整个LCD那么大,那就只能看见一个象素点的“灯泡”了,也就没法显示图像了。通过像素个数和每个点的大小就可以换算出显示屏的大小了,例如常用的xx英寸大小的屏幕等等。
1.4 输入设备
       输入设备也有很多种,最典型的是键盘和触摸屏。在这一节里,我将简单的介绍一下它们的实现原理。
       键盘通常是一个矩阵式的电路,当按键按下的时候,接通电路产生信号,如图1.3:

1
2
3
A
B
C

图1.3 键盘原理
       图1.3是一个简版的键盘原理图,图中任何交叉点的横向和纵向都未连通,并假设只要交叉点连通,则相应的行和列就可以连通并发生状态改变。其中1、2、3和A、B、C连接在控制芯片上,通过扫描行和列,确定行的A、B、C是否连通,再扫描列1、2、3是否连通,这样就可以唯一确定一个点是否按下。千万注意,这只是一个示意图,并不是真正的键盘原理图。
       触摸屏是近来应用越来越多的输入器件。典型触摸屏的工作部分一般由三部分组成:两层透明的阻性导体层、两层导体之间的隔离层、电极。阻性导体层选用阻性材料,如铟锡氧化物(ITO)涂在衬底上构成,上层衬底用塑料,下层衬底用玻璃。隔离层为粘性绝缘液体材料,如聚脂薄膜。电极选用导电性能极好的材料(如银粉墨)构成。
       触摸屏在工作时,上下导体层相当于电阻网络。当某一层电极加上电压时,会在该网络上形成电压梯度。如有外力使得上下两层在某一点接触,则在电极未加电压的另一层可以测得接触点处的电压,从而知道接触点处的坐标。比如,在顶层的电极(X+,X-)上加上电压,则在顶层导体层上形成电压梯度,当有外力使得上下两层在某一点接触,在底层就可以测得接触点处的电压,再根据该电压与电极(X+)之间的距离关系,知道该处的X坐标。然后,将电压切换到底层电极(Y+,Y-)上,并在顶层测量接触点处的电压,从而知道Y坐标。通常有专门的控制芯片。很显然,触摸屏的控制芯片要完成两件事情:一是完成电极电压的切换;二是采集接触点处的电压值(即A/D)。虽然还有其他的实现方法,我就不赘述了,因为本书的目的不是讲解硬件的原理,而仅仅是让我们能够基本了解它们。
1.5 小结
       在这一章里介绍了各种硬件的特性和原理,其中包括了最小系统各种组件的介绍,为的是防止我们见到这些东西的时候会一头雾水,不知来由。只要我们见到这些硬件不再感到神秘,那么这个基础就算打好了。
思考题
       在PC计算机系统中硬盘、BIOS、RAM和嵌入式系统中的文件Flash、程序Flash、RAM之间有什么区别?它们在系统中的作用分别是什么?

Tags:

28 Nov 08 RO段、RW段和ZI段

一直以来对于ARM体系中所描述的RO,和ZI数据存在似是而非的理解,这段时间对其仔细了解了一番,发现了一些规律,理解了一些以前书本上有的但是不理解的东西,我想应该有不少人也有和我同样的困惑,因此将我的一些关于RO,和ZI的理解写出来,希望能对大家有所帮助。
要了解RO,和ZI需要首先了解以下知识:
ARM程序的组成
此处所说的“ARM程序”是指在ARM系统中正在执行的程序,而非保存在ROM中的bin映像(image)文件,这一点清注意区别。
一个ARM程序包含3部分:RO,和ZI
RO是程序中的指令和常量
是程序中的已初始化变量
ZI是程序中的未初始化的变量
由以上3点说明可以理解为:
RO就是readonly,
就是read/write,
ZI就是zero
ARM映像文件的组成
所谓ARM映像文件就是指烧录到ROM中的bin文件,也成为image文件。以下用Image文件来称呼它。
Image文件包含了RO和数据。
之所以Image文件不包含ZI数据,是因为ZI数据都是0,没必要包含,只要程序运行之前将ZI数据所在的区域一律清零即可。包含进去反而浪费存储空间。
Q:为什么Image中必须包含RO和
A:因为RO中的指令和常量以及中初始化过的变量是不能像ZI那样“无中生有”的。
ARM程序的执行过程
从以上两点可以知道,烧录到ROM中的image文件与实际运行时的ARM程序之间并不是完全一样的。因此就有必要了解ARM程序是如何从ROM中的image到达实际运行状态的。
实际上,RO中的指令至少应该有这样的功能:
1. 将从ROM中搬到RAM中,因为是变量,变量不能存在ROM中。
2. 将ZI所在的RAM区域全部清零,因为ZI区域并不在Image中,所以需要程序根据编译器给出的ZI地址及大小来将相应得RAM区域清零。ZI中也是变量,同理:变量不能存在ROM中
在程序运行的最初阶段,RO中的指令完成了这两项工作后C程序才能正常访问变量。否则只能运行不含变量的代码。
说了上面的可能还是有些迷糊,RO,和ZI到底是什么,下面我将给出几个例子,最直观的来说明RO,,ZI在C中是什么意思。
1; RO
看下面两段程序,他们之间差了一条语句,这条语句就是声明一个字符常量。因此按照我们之前说的,他们之间应该只会在RO数据中相差一个字节(字符常量为1字节)。
Prog1:
#include <stdio.h>
void main(void)
{
;
}
Prog2:
#include <stdio.h>
const char a = 5;
void main(void)
{
;
}
Prog1编译出来后的信息如下:
================================================================================
Code RO Data Data ZI Data Debug
948 60 0 96 0 Grand Totals
================================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total Size( Data + ZI Data) 96 ( 0.09kB)
Total ROM Size(Code + RO Data + Data) 1008 ( 0.98kB)
================================================================================
Prog2编译出来后的信息如下:
================================================================================
Code RO Data Data ZI Data Debug
948 61 0 96 0 Grand Totals
================================================================================
Total RO Size(Code + RO Data) 1009 ( 0.99kB)
Total Size( Data + ZI Data) 96 ( 0.09kB)
Total ROM Size(Code + RO Data + Data) 1009 ( 0.99kB)
================================================================================
以上两个程序编译出来后的信息可以看出:
Prog1和Prog2的RO包含了Code和RO Data两类数据。他们的唯一区别就是Prog2的RO Data比Prog1多了1个字节。这正和之前的推测一致。
如果增加的是一条指令而不是一个常量,则结果应该是Code数据大小有差别。
2;
同样再看两个程序,他们之间只相差一个“已初始化的变量”,按照之前所讲的,已初始化的变量应该是算在中的,所以两个程序之间应该是大小有区别。
Prog3:
#include <stdio.h>
void main(void)
{
;
}
Prog4:
#include <stdio.h>
char a = 5;
void main(void)
{
;
}
Prog3编译出来后的信息如下:
================================================================================
Code RO Data Data ZI Data Debug
948 60 0 96 0 Grand Totals
================================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total Size( Data + ZI Data) 96 ( 0.09kB)
Total ROM Size(Code + RO Data + Data) 1008 ( 0.98kB)
================================================================================
Prog4编译出来后的信息如下:
================================================================================
Code RO Data Data ZI Data Debug
948 60 1 96 0 Grand Totals
================================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total Size( Data + ZI Data) 97 ( 0.09kB)
Total ROM Size(Code + RO Data + Data) 1009 ( 0.99kB)
================================================================================
可以看出Prog3和Prog4之间确实只有 Data之间相差了1个字节,这个字节正是被初始化过的一个字符型变量“a”所引起的。
3; ZI
再看两个程序,他们之间的差别是一个未初始化的变量“a”,从之前的了解中,应该可以推测,这两个程序之间应该只有ZI大小有差别。
Prog3:
#include <stdio.h>
void main(void)
{
;
}
Prog4:
#include <stdio.h>
char a;
void main(void)
{
;
}
Prog3编译出来后的信息如下:
================================================================================
Code RO Data Data ZI Data Debug
948 60 0 96 0 Grand Totals
================================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total Size( Data + ZI Data) 96 ( 0.09kB)
Total ROM Size(Code + RO Data + Data) 1008 ( 0.98kB)
================================================================================
Prog4编译出来后的信息如下:
================================================================================
Code RO Data Data ZI Data Debug
948 60 0 97 0 Grand Totals
================================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total Size( Data + ZI Data) 97 ( 0.09kB)
Total ROM Size(Code + RO Data + Data) 1008 ( 0.98kB)
================================================================================
编译的结果完全符合推测,只有ZI数据相差了1个字节。这个字节正是未初始化的一个字符型变量“a”所引起的。
注意:如果一个变量被初始化为0,则该变量的处理方法与未初始化华变量一样放在ZI区域。
即:ARM C程序中,所有的未初始化变量都会被自动初始化为0。
总结:
1; C中的指令以及常量被编译后是RO类型数据。
2; C中的未被初始化或初始化为0的变量编译后是ZI类型数据。
3; C中的已被初始化成非0值的变量编译后市类型数据。
附:
程序的编译命令(假定C程序名为tst.c):
armcc -c -o tst.o tst.c
armlink -noremove - -nodebug -info totals -info sizes -map -list aa.map -o tst. tst.o
编译后的信息就在aa.map文件中。
ROM主要指:NAND Flash,Nor Flash
RAM主要指:PSRAM,SDRAM,SRAM,DDRAM

Tags: , , , ,

24 Oct 08 各有所长 四大智能手机操作平台大比拼

转载自http://android.hk.cn

本文将用老谋深算的平台历史悠久的Windows 平台玲珑剔透的 X平台前景光明的Android平台四个章节对目前市面上主流手机操作系统的优势和特色做一个详细的介绍。

       老谋深算的平台

       随着科技和信息技术的发展,人们对手机功能的要求也越来越高。现在,很多人对手机已不仅仅满足于打电话和发短信等基本功能,而是要求它能够播放音乐、上网聊天、安排日程甚至进行GPS导航。因此,很多人已经逐步抛弃传统固化软件的手机,改投智能手机的怀抱。目前,在智能手机市场上,销量最大的平台有三种,分别是、Windows X,其中前两种都有一种以上的手机品牌在使用,而 X仅为苹果iPhone和iPhone 3G独占使用。现在,基于 Android平台的新一代智能手机T- G1马上就要上市,它几乎无可非议的会受到人们的追捧。这样一来,市场上便形成了四大智能手机操作平台鼎立的局面。

       (塞班)

       首先要提到的当然是(塞班)系统。使用在智能手机上的系统分为S60和UIQ两种用户界面,其中前者现已发展到v3版本。得益于世界手机老大诺基亚的大力支持,安装了 S60系统的手机已经广为人们使用。而UIQ的主推者是索尼爱立信,虽然UIQ的界面更加华丽,但由于索尼爱立信自身和手机市场的原因, UIQ一直没有能够得到很好的发展。

       由于由多家传统手机厂商联合研发,因此无论在运行速度还是易用性上跟其它的系统相比都有很大的优势,而且对移动通信协议,如GSM、GPRS、WCDMA和蓝牙等的支持要优于其它系统。由于发布时间较早,其第三方软件经过几年的发展数量已经非常之多,可以实现人们的大多数需求。

       但遗憾的是,由于系统的先天限制,系统的多媒体性能并不突出。刷固件较为困难、内存紧张等问题是一直是人们所诟病的几大缺憾。S60 v3发布后诺基亚开始推行的签名策略也浇灭了很多人开发第三方软件的热情。而且,系统实现触摸操作相当困难,好在诺基亚拥有强大的研发实力。相信继5800 XpressMusic之后,诺基亚还会继续推出触摸屏机型。不过尽管如此,平台凭着强大的电话功能和较强的易用性,仍然博得了很多人的青睐。

       历史悠久的Windows 平台

       Windows

       作为微软下了血本研发的一个操作平台,Windows 系统自然丝毫不能被轻视。Windows 的前身是Windows CE,一个微软专为PDA和掌上电脑开发的嵌入式系统。后来随着智能手机的出现,Windows CE演变成了Windows Pocket PC。2003年,微软又开发了Pocket Phone Edition和Windows Powered Smart Phone操作系统,而这两者的融合,才是严格意义上的Windows

       目前,Windows 最高版本为6.1。它最大的优势在于其PDA(个人数字助理)功能非常强大,并可通过ActiveSync或Windows Center和计算机中Outlook等程序保持同步,商务人士可使用该系统随时处理文件、邮件或进行日程安排。由于这个系统的历史非常悠久,和桌面版Windows系统也有异曲同工之处,其第三方软件的数量已经多到了一个令人难以想象的地步。通过各种第三方软件,安装了Windows 系统的手机可以发挥几乎无限的用途。对于软件开发者来说,由于Windows 系统的软件开发较为简单,再加上新版系统中增加了对.Net Framework的支持,使软件的开发相当便利。而对于最终用户来说,Windows 系统的操作方式跟桌面版Windows非常类似,都有开始菜单等人们所熟悉的组件,熟悉计算机的人可以很快上手使用。

       但Windows 系统的缺陷也是显而易见的。按照通常的说法,它有着三大缺陷。其一是系统运行速度太慢。或许是微软往小小的系统中塞了很多东西,Windows 也延续了Windows系统庞大臃肿的毛病,几乎可以算得上是反应最慢的智能手机操作系统。其二是支持的颜色数仅为65000色。关于这个问题很多年前人们就开始抱怨,到现在还是如此。不知道微软为什么不听听群众们的呼声。其三是系统操作过于复杂。它的很多设置都隐藏在层层菜单和选项下,有时甚至还需动用注册表修改器,仍然继承了桌面版Windows的传统。虽然这可能正好满足了少数人的“变态”控制欲。

       但是总的来说,Windows 以其强大的功能、便捷的信息管理以及第三方软件众多等优势,一直以来都为世人所喜爱。当然,更重要的是它还有微软这样一个重量级的后盾。可以说,只要微软不倒,Windows 也将一直生存下去。

       玲珑剔透的 X平台

       X

       苹果的iPhone手机自从公布的那天起便一直受到人们的关注,现在的iPhone 3G也是如此。iPhone使用的操作系统为精简过的 X,最高版本为2.1,其很多特性与完整版的 X相类似。该系统使用的内核基于BSD Unix,具有抢占式多任务处理(pre-emptive multitasking)的特性。其内存管理功能非常优秀,具有强烈的Unix风格,允许同时运行很多软件,并从实质上消除了一个程序崩溃导致其它程序崩溃的可能性。

       iPhone的 X系统更多的优势来自于与iTunes服务的紧密紧密。iTunes原先仅是一个媒体播放软件,后来当iPod和iPhone发布后,iTunes逐渐演变为苹果在Mac和Windows系统上的一个无穷无尽的媒体源。人们可以通过iTunes登录到苹果的在线商店,购买各种音乐、视频或程序。这也成为苹果的一个新的盈利增长点。

       虽然iPhone和iPhone 3G的 X系统已被大大精简,但在苹果计算机的 X系统中广为人们熟知的Dock和Aqua等关键元素仍然存在。更重要的是,人们可以在安装特定程序后进入Unix命令行进行各种操作,并对系统底层进行控制。此时iPhone已不再是一台手机,它更像是台超便携的苹果电脑,能够处理很多普通人所意想不到的事情。

       不久以前,苹果为iPhone和iPod Touch开放了App Store应用程序商店。手机用户可通过信用卡在商店中购买自己喜欢的程序,而开发者也能通过App Store发布自己编写的程序,其收益与苹果七三分成。苹果的这个创新构思马上得到了人们的响应,短短几个月的时间App Store中的程序便达到了惊人的数量。

       前景光明的Android平台

       Android

       文章最后要提到的当然是本专题的主角—— Android平台。Android 是一个真正意义上的开放性移动设备平台。它包括操作系统、用户界面和应用程序等移动电话工作所需的全部组件,而且不存在任何以往阻碍移动产业创新的专有权障碍。

       说到Android,便不能不提OHA(Open Handset Alliance)开放手机联盟。去年,与包括中国移动、英特尔、摩托罗拉、高通在内的33家业内巨头成立了该组织,这个联盟将共同推动手机平台Android的发展。Android作为企业战略的重要组成部分,将进一步推进“随时随地为每个人提供信息”这一企业目标的实现。

       跟iPhone相比,Android平台同样也拥有自己的在线应用程序商店,名为Android Market。这个在线商店仅需进行简单的注册后便可以自由上传程序。虽然这可能导致一些安全问题,但总的来说这种开放式的平台非常有利于第三方软件的发展。而在对这些软件进行幕后审查的同时,也采取了多项措施,最大限度的保证了软件开发者的利益。

       Android与前面三种系统相比最大的特点在于其开放性。这里所指的开放性包括两个方面,其一是Android以开源Linux系统为基础,对于开源爱好者而言,他们会觉得Android平台更能满足自己的使用需求。其二是Android对第三方软件的开放程度。不会对Android系统的第三方应用程序像苹果那样严格把关,而仅是在用户自行发布之后进行审查。这样一来必将极大的促进该系统第三方软件的发展。

       而就基于Android的第一款手机T- G1来说,其最不能令人忍受的缺陷恐怕就是没有3.5mm耳机插孔了。另外,由于T-背后使下的“阴招”,用户在用完1GB流量后带宽便会自动下降为50Kbps,这将大大影响人们的购买欲望。而且,Android系统目前还没有桌面同步软件,也不支持Exchange同步。虽然的很多在线服务都可以满足人们的需要,但总有上不了网的时候。况且,很多信息保存在计算机中要安全得多。

       四款主流或将要成为主流的智能手机操作平台到这里就介绍完了。俗话说的好,尺有所短,寸有所长,每个平台都有其各自的优点和不足。大家在选择智能手机的时候,需要关注的重点是各种平台的侧重点,这样在今后的使用中才会得心应手。至于该如何选择,相信大家心中都已经有数了吧。

Tags: , , , , ,

24 Oct 08 [GPhone-cn出品]T-mobile G1 购买十个为什么

转载自http://android.hk.cn

现在有很多网友不停询问t- G1 这款Android手机有关在国内购买使用的问题,-cn将一一作答。

 

1 G1能破解么?
答:可以破解,因为是开源操作,因此软破的几率比iPhone高很多。

2 G1操作系统界面可以汉化么?
答:可以,但是要稍等一段时间。

3 G1有什么配件?
答:目前看来有旅行冲,电池一块,耳机线孔,皮套一个,USB线一条

4 G1支持国内TD 3G网络么?
答:当然不支持。

5 G1有视频通话功能么?
答:没有。

6 G1在国内网络能用么?
答:能用,支持GSM网络。

7 G1在国内售价预计是多少呢?
答:目前看来是3090-4999元这个区间里。

8 国内G1大概多久能到货呢?
答:至少在10.25日以后。

9 G1有网络锁么?
答:签约机器有90天网络锁,90天后自动解除,零售机器没有网络锁。

10 G1有储存卡么?
答:原配没有,支持最高16GB的TF储存卡

Tags: , ,

23 Oct 08 LRC文件结构浅析

概述

   歌词是一种包含着“[*:*]”形式的“标签(tag)”的、基于纯文本的歌词专用格式。最早由郭祥祥先生(Djohan)提出并在其程序中得到应用。这种歌词文件既可以用来实现卡拉OK功能(需要专门程序),又能以普通的文字处理软件查看、编辑。当然,实际操作时通常是用专门的歌词编辑软件进行高效编辑的。以下具体介绍格式中的“标签”。

 时间标签(Time-tag)

  形式为”[mm:ss]“或”[mm:ss.fff]“(分钟数:秒数)。数字须为非负整数, 比如”[12:34.5]“是有效的,而”[0x0C:-34.5]“无效。
  它可以位于某行歌词中的任意位置。一行歌词可以包含多个时间标签(比如歌词中的迭句部分)。根据这些时间标签,用户端程序会按顺序依次高亮显示歌词,从而实现卡拉OK功能。另外,标签无须排序。
(如果我们的网站不错,请把“九九歌词网 =>www.99lrc.net”网址推荐给你的朋友使用,谢谢!)

标识标签(ID-tags)

  其格式为”[标识名:值]“。大小写等价。以下是预定义的标签。

  [ar:艺人名]
  [ti:曲名]
  [al:专辑名]
  [by:编者(指编辑歌词的人)]
  [offset:时间补偿值] 其单位是毫秒,正值表示整体提前,负值相反。这是用于总体调整显示快慢的。

  样例

  [ar:unknown]
  [ti:sample]
  [al:none]
  [by:me]
  [01:02.355][00:00]This line should be sung twice
  [00:05.7]And this one… once only.

开发标准(供程序员阅读参考)

以下列出了开发支持格式的软件时应遵守的一些标准。

无论是否在行首,行内凡具有“[*:*]”形式的都应认为是标签。(注意:其中的冒号并非全角字符“:”)

  • 凡是标签都不应显示。
  • 凡是标签,且被冒号分隔的两部分都为非负数,则应认为是时间标签。因此,对于非标准形式(非“[mm:ss]”)的时间标签也应能识别(如“[0:0]”)。
  • 凡是标签,且非时间标签的,应认为是标识标签。
  • 标识名中大小写等价。
  • 为了向后兼容,应对未定义的新标签作忽略处理。另应对注释标签([:])后的同一行内容作忽略处理。
  • 应允许一行中存在多个标签,并能正确处理。
  • 应能正确处理未排序的标签。

Tags: ,

15 Oct 08 山寨机教父蔡明介:联发科10年迅速崛起的秘诀

“常常有人问我,联发科在过去10年里迅速崛起,有什么秘诀?并且希望这些’秘诀’对正在快速发展中的中国大陆芯片企业有所启发.”给人印象深刻的一副标志性的大眼镜,从外表看,联发科技董事长蔡明介并不具备在商场上拼杀的枭雄造型.然而,他却是台湾赴美引进半导体技术的先驱,被誉为台湾集成电路设计“教父”.
 

山寨机教父蔡明介:联发科10年迅速崛起的秘诀

1995年蔡明介领导台湾“联华电子”的集成电路设计部门独立创业,成立了“联发科技”的前 身多媒体小组,联华电子是仅次于台积电的全球第二大流片厂。IC行业流传着一句难以溯源的话———现在的芯片业,每个非大厂都不希望联发科涉及自己的领 域,那样自己就“离死不远了”。

并非危言耸听,经历10余年的发展,目前联发科已成为世界顶尖的IC专业设计公司,位居全球 消费性IC片组的领航地位。产品领域覆盖数码消费、数字电视、光储存、无线通讯等多个系列,是亚洲唯一连续六年蝉联全球前十大IC设计公司唯一的华人企 业,被美国《福布斯》杂志评为“亚洲企业50强”。

没有听说过联发科,只要是对手机产业链稍有了解的人,也会对这三个字母的组合有些印象。是联发科技股份有限公司的英文简称,英文全称叫MediaTek.

「杀手锏」一站式方案

或许有人不太喜欢这家公司,因为从早年的黑手机到现在的山寨机,草莽气息背后是方案被广泛应用。波导、夏新,老牌手机厂商令人堪忧的一份份财务报告中总会附带“受到黑手机、山寨机冲击”的说明,可被视作始作俑者之一。

但你不得不佩服的创新和成功,甚至也找不出更多口诛笔伐的理由。因为的存在,近年来国内手机产业进入了前所未有的蓬勃发展阶段。事实上除了为山寨机提供方案,也同时为正规厂商提供全面解决方案,包括近两年迅速崛起的天语、金立等正规品牌。

“我们有个客户的工程师曾经很形象地打了个比方,他说的解决方案更像是一本”烹饪手册 “,它只能保证厨师不会把鱼香肉丝做成京酱肉丝,至于这道菜做出来的色香味如何,那还是看终端厂商的水准。”联发科首席财务官兼新闻发言人喻铭铎曾向本报 记者表示,提供的是一个把厂商开发过程中可能遇到的问题尽量提前考虑到的参考性的解决方案。厂商在开发过程中如果遇到一些原先技术能力较为薄弱的地 方,可以从这个参考性的解决方案中获得借鉴与补充,从而不断弥补原来的不足。

凭借这种“菜谱”方式,在大陆市场上结结实实地击败了众多国际芯片厂商。操作软件,甚至液晶、摄像头等部件,都可以“一站解决”,大大缩短了手机的生产环节并降低了研发成本。这正是Turnkeysolution(一站式方案)模式。

Turnkeysolution正是手机平台的核心。在这套解决方案中,将手机芯片和 手机软件平台预先整合到一起。这种方案可以使终端厂商节约成本,加速产品上市周期。公司的产品因为集成较多的多媒体功能和较低的价格在大陆手机公司 和手机设计公司得到广泛的应用。加上的完工率较高,基本上在60%以上,这样手机厂商拿到手机平台基本上就是一个半成品,只要稍稍的加工就可上架出 货了。

数年前,德州仪器(TI)的LoCosto、OMAP平台,英飞凌的ULC解决方案,还一度是这些国际知名厂商广泛采用的模式。然而MTKTurnkey方案的巨大成功则证明了在这一领域平台技术不再是TI、英飞凌这些巨头的专属。

「性价比」到底到什么程度

1999年,当时的联发科在台湾科技行业中还仅是一家生产光存储和DVD芯片的二线厂商。然而当时,蔡明介看中了利润更加丰厚的手机芯片业务,恰逢中国手机市场进入第一次高速增长期,他的眼光自然而然地落在了中国大陆市场。

2001年开始,开始在手机行业逐渐渗透Turnkeysolutio模式,这种模式内蕴着极高的性价比。在国内手机制造仍处于朦胧阶段的时刻,这种性价比着实让人难以抗拒。

号称2007年全年销量达到1500万台的天宇朗通便广泛采用了的产品。“感谢 ,感谢台湾人。”天宇朗通销售总监卢伟冰在今年天津手机展新闻发布会上曾表示,中国拥有全球配套最完善的手机产业集群。都说手机没有核心技术,都说 在欧美人的手里,大家都觉得很失望。但是我们的台湾人做了很多基础芯片研究,的问世把没有技术、没有解决方案这个历史进行了改写。

天宇朗通是近年来当之无愧的国产手机黑马。在其面向下游的销售模式中,和的高性价比、 低价出牌也有着惊人的相似。本地一位零售商代表向记者表示,天宇朗通完全摒弃了过去手机企业的价保模式,取而代之的是以较低的价格出货给代理商,再由下游 定价。“一款产品我们的进货价若只有300元,定价600元销售也有可能,毕竟同等功能配置的洋品牌还要贵上至少100元。”

300元,只是手机厂商给予渠道的价格,他们从手中获得的方案呢,性价比究竟能高到什 么程度?以行业内最普遍使用的联发科()低端方案6223为代表。可支持T-flash扩展卡,彩屏,具备自动应答留言机功能,键盘采用动感炫灯。 如量产二十万台,单价在14美金左右,不到人民币100元。

同样是在2001年,联发科在台湾上市,更素有“台湾股王”之称。直至2007年,其营业收 入已达到新台币804.09亿元,较2006年增长51%.根据最新市场资料显示,2007年联发科年手机晶片出货量高达1.5亿颗,全球市场占有率近 14%,仅次于德州仪器及高通。

「声誉」骂名、美名

联发科真正开始在大陆市场上崭露头角,2005年是关键年。在那个还没有取消手机核准制度的 年代,早前依靠贴牌韩国方案而迅速崛起的国产手机在洋品牌的全线反攻下,跌入了全线溃败的谷底。夏新、波导、TCL,大量的民族手机品牌惨遭洗牌,国产手 机也因此背上了“质量不好”的包袱。

然而就在这一时期,具有中国特色的黑手机跃上了手机产业的历史舞台。这些手机主要通过模仿知 名品牌的外形。以及低廉的价格来吸引消费者。黑手机一词诞生于联发科Turnkeysolutio模式广泛进入中国市场之前,但因为的存在,黑手机 发生了“革命性”的变化。也因此背上了“黑手机之父”的骂名。

早期的黑手机,在没有采用方案之前规模并不大,以翻新、通过各种渠道寻找廉价方案为主 要表相。在全面解决方案进入中国市场后,生产方一次过便可以获得包括基带芯片、操作软件,甚至液晶显示屏、摄像头等部件等全套或半套解决方案。一举 将原来的6-9个月甚至是一年的手机研发周期,缩短到了3个月以内。

直至国家取消手机牌照核准制度,黑手机这个称谓已不合时宜,源于MP3行业的“山寨”一词取而代之。不过并未因此而被改称作“山寨机之父”。在国产手机产业发展到目前的规模,讨论的成分变得丝毫没有意义。

可以说,黑手机、山寨机空前的泛滥与有着必然联系,但无论从技术还是道德的范畴,如今 更为理性的观点大都认为不应该由为这种泛滥负责。Turnkeysolutio模式的崛起、的成功,背后更多体现的是手机行业新兴市场飞速发 展的带动和技术的成熟和全球化,技术门槛降低后的必然结果。

至于黑手机、山寨机偷税漏税等问题,更多体现的是法律环境的不健全。在知识产权保护、税收、产品质量、工人权益、生产和市场准入、走私控制等方面所存在的漏洞,而这些实际才是黑手机为生产、流通和销售创造优越环境的根本。

从未为任何有关于自己的报道辩驳,但依然落入了尴尬的境地。这种情况目前随着越来越多的品牌厂商采用其方案而获得改善,的模式更成为不少IC企业效仿的对象。

联发科发展的几个瞬间

1995年,成立……

蔡明介领导台湾“联华电子”的集成电路设计部门独立创业,成立了“联发科技”的前身多媒体小组,初始只有20余人。

2001年,钟情手机芯片

联发科在台湾成功上市,51岁的蔡明介则必须做出选择———创业前4年,联发科必须在CD- ROM、CD-以及DVD-ROM等几代芯片的稳定发展后找到下一个拳头产品,这可以说是决定联发科生死的问题。按照蔡明介经验,一定要符合市场足够 大;有一定的技术门槛和别人拉开差距;和联发科原来的开发技术有一定的相关性这三个标准。

经过内部激烈的讨论后,联发科决定投入手机芯片研发。当时台湾手机代工行业逐渐兴起,作为上游,联发科最终决定跟着客户走,杀入手机芯片领域。

2003-2004年,成长的低谷

是联发科颇为难熬的一段日子,手机行业的前景在哪里?当时,处于中游的台湾手机代工企业活得并不好,选定道路的联发科则不得不加大研发费用的投入。

此时,联发科之前的光存储和DVD产品市场已经逐渐饱和,价格开始下滑。手机芯片虽然在 2003年底开发成功,却迟迟无法打开市场。更重要的是,想“跟着”的那些客户并没有预想的成功,联发科必须去寻找新的客户。2004年四季度,代工行业 的连锁反应落在了联发科身上,其毛利率大跌40%%,在投资人会议上,蔡明介甚至亲自拿出采用自家芯片的手机进行演示,希望能够给投资人以信心。

据统计,当年全球IC设计产业总体增长率为27%,而联发科营收仅增长了5.23%.

2004年,猛虎出笼……

最终台湾一家叫达智的手机制造小公司因为无力采购国际IC设计巨头的产品,在实属无奈的情况 下采用了联发科的方案。由于当时手机IC设计领域并没有重视媒体功能的植入,大多仍采用双芯片分别控制通话和多媒体的设计,成本较高。而联发科因为此前做 CD、DVD芯片的开发,有这方面基础,迅速打开了市场。

2008年,跻身世界前三

手机芯片部门的收入已经占到联发科营收的50%以上,联发科也仅次于德州仪器及高通,跻身世界前三大IC手机设计厂商。

文/《南方都市报》

Tags: , , ,

14 Sep 08 (- -||) 像SB一样入手ATH AD300

    以400RMB从中关村某JS那入手一部铁三角AD300, 属于Hifi耳机中入门级别的。
    缺点很多,第一是太重,第二是声音有点混人声不太好(不过比之前PHILIPS的好多了),看下官方介绍吧。

    ATH-AD300 空气动圈耳机
    ·三维翼支撑提供更大的舒适性并更加贴服
    ·强大的钕磁体确保声音具有强劲效果
    ·耳垫使用柔软顺滑的布织物
    ·舒适、完全适耳的设计可减轻紧固感
    ·40mm直径驱动器专为开放式耳机设计,提供强劲的声音
    ·高纯度OFC输入耳线 (3.0m)
    ·玻璃钢强化外壳

    型号类别 开放空气动圈型  
    单元直径 Ø40 mm 
    驱动单元 钕磁铁 
    频率响应 20 ~ 25,000Hz 
    最大功率 300 mW 
    灵敏度 96 dB/mW 
    阻抗 32 ohms 
    重量 250 g 
    连接端 Ø6.3 / Ø3.5 mm 镀金立体声插头 
    导线  OFC/ 3.0 m (单侧导线)

    再来张图,外型还凑合,能虎虎不识货的(包括我在内)~

Tags: