32位与64位编程注意事项总结

 

http://www.oschina.net/p/flowvisor

 

http://www.cnblogs.com/centimeter/articles/2395405.html

 

http://www.360doc.com/content/13/0520/21/7662927_286868466.shtml

 

http://zhidao.baidu.com/link?url=E8M7y7JbRm8K9gYnxlUW7kLZwpozFT-t4YyTSzW6eb8RKfJKnmszzC17U_6R7-fgfk1IgKxsdY9A8iBXKNYGO_

 

http://bbs.csdn.net/topics/390426743

 

http://www.cnblogs.com/xwj-pandababy/articles/3418470.html

 

http://zhidao.baidu.com/link?url=R7c6hAgB0b8PTKMSLN1JQhWRDHO81pnaS3ZoGSfVuqYQoH8BETssO6p7Xy_87Y7JEyfJeFTZpPjnahgBiXCm3K

 

http://zhidao.baidu.com/link?url=02zo160Hq9EMTYL3ACVxD2gGdhqmU9JnpN_moQWnWXOHaBagjs4fHzKdzuBVAvCQvOFSs1x6jwCudWs4Yl3qda

 

http://www.csdn123.com/html/exception/408/408361_408359_408357.htm

 

http://blog.csdn.net/liuruihan/article/details/24792491

 

 

http://blog.csdn.net/david_xtd/article/details/7669028

 

 

http://blog.csdn.net/thanklife/article/details/7362893

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

 

http://www.csdn123.com/html/exception/408/408361_408359_408357.htm

 

http://blog.csdn.net/liuruihan/article/details/24792491

 

http://blog.csdn.net/david_xtd/article/details/12652197

 

http://blog.csdn.net/david_xtd/article/details/18708731

 

http://blog.csdn.net/david_xtd/article/details/7177041

 

http://blog.csdn.net/david_xtd/article/details/8682455

 

http://blog.csdn.net/david_xtd/article/details/9240549

 

http://blog.csdn.net/thanklife/article/details/7362893

 

 

新近的64位平台在二进制上与32位应用程序兼容,这意味着可以非常简单地移植现有的程序。许多目前在32位平台上运行良好的程序也许不必移植,除非程序有以下要求:
·需要多于4GB的内存。
·使用的文件大小常大于2GB。
·密集浮点运算,需要利用64位架构的优势。
·能从64位平台的优化数学库中受益。
否则,只需简单地重新编译一下,就已经足够了。大多数编写良好的程序不费吹灰之力就可移植到64位平台之上,在此假定你的程序编写良好,并熟悉本文将要讨论的问题。 
ILP32和LP64数据模型
32位环境涉及"ILP32"数据模型,是因为C数据类型为32位的int、long、指针。而64位环境使用不同的数据模型,此时的long和指针已为64位,故称作"LP64"数据模型。
现今所有64位的类Unix平台均使用LP64数据模型,而64位Windows使用LLP64数据模型,除了指针是64位,其他基本类型都没有变。我们在此主要探讨ILP32到LP64的移植问题,表1显示了ILP32与LP64数据模型的差异。
向64位移植代码时的所有问题差不多都可以总结出一个简单的规律:千万不要认为int、long、指针的长度一样。任何违反这条规律的代码,当运行在 LP64数据模型下时,都会出现不同的问题,而且很难找出原因所在。例1中有许多违反这条规律的地方,其在移植到64位平台上时都需要重写。
例1:

1 int *myfunc(int i)
2 {
3  return(&i);
4 }
5
6 int main(void)
7 {
8  int myint;
9  long mylong;
10 int *myptr;
11
12  char *name = (char * ) getlogin();
13
14  printf("Enter a number %s: ", name);
15  (void) scanf("%d", &mylong);
16  myint = mylong;
17  myptr = myfunc(mylong);
18  printf("mylong: %d pointer: %x \n", mylong, myptr);
19  myint = (int)mylong;
20  exit(0);
21
22 }


第一步是要求编译器捕捉到移植时的问题,因所用编译器的不同,选项可能也有所不同,但对IBM XL编译器系列,可用的选项有-qwarn64-qinfo=pro,为了得到64位可执行文件,可使用选项-q64(如果使用GCC,选项应为-m64,表2中列出了其他可用的GCC选项)。图1是编译例1中代码时的情况。


编译例1中代码时的情况

缺少原型的截断
如果一个函数被调用时没有指定函数原型,返回值将是32位的int。不使用原型的代码可能会发生意料之外的数据截断,由此导致一个分割错误。编译器捕捉到了例1中第12行的这个错误。
char *name = (char *) getlogin();
编译器假定函数返回一个int值,并截短结果指针。这行代码在ILP32数据模型下工作正常,因为此时的int和指针是同样长度,换到LP64模型中,就不一定正确了,甚至于类型转换都不能避免这个错误,因为getlogin()在返回之后已经被截断了。
要修正这个问题,需包括头文件<unistd.h>,其中有getlogin()的函数原型。
格式指定符
如果对64位long、指针使用了32位格式指定符,将导致程序错误。编译器捕捉到了例1中第15行的这个错误。
(void) scanf("%d", &mylong);
注意,scanf将向变量mylong中插入一个32位的值,而剩下的4字节就不管了。要修正这个问题,请在scanf中使用%ld指定符。
第18行也演示了在printf中的一个类似的问题:
printf("mylong: %d pointer: %x \n", mylong, myptr);
要修正此处的错误,mylong应使用%ld,对myptr使用 %p而不是%x。
赋值截断
有关编译器发现赋值截断的一个例子在第16行中:
myint = mylong;
这在ILP32模型下不会有任何问题,因为此时的int、long都是32位,而在LP64中,当把mylong赋值给myint时,如果数值大于32位整数的最大值时,数值将被截短。
被截断的参数
编译器发现的下一个错误在第17行中,虽然myfunc函数只接受一个int参数,但调用时却用了一个long,参数在传递时会悄无声息地被截断。
转换截断
转换截断发生在把long转换成int时,比如说例1中的第19行:

myint = (int) mylong;


导致转换截断的原因是int与long非同样长度。这些类型的转换通常在代码中以如下形式出现:

int length = (int) strlen(str);


strlen返回size_t(它在LP64中是unsigned long),当赋值给一个int时,截断是必然发生的。而通常,截断只会在str的长度大于2GB时才会发生,这种情况在程序中一般不会出现。虽然如此,也应该尽量使用适当的多态类型(如size_t、uintptr_t等等),而不要去管它最下面的基类型是什么。
一些其他的细小问题
编译器可捕捉到移植方面的各种问题,但不能总指望编译器为你找出一切错误。
那些以十六进制或二进制表示的常量,通常都是32位的。例如,无符号32位常量0xFFFFFFFF通常用来测试是否为-1:

#define INVALID_POINTER_VALUE 0xFFFFFFFF


然而,在64位系统中,这个值不是-1,而是4294967295;在64位系统中,-1正确的值应为0xFFFFFFFFFFFFFFFF。要避免这个问题,在声明常量时,使用const,并且带上signed或unsigned。

const signed int INVALID_POINTER_VALUE = 0xFFFFFFFF;


这行代码将会在32位和64位系统上都运行正常。
其他有关于对常量硬编码的问题,都是基于对ILP32数据模型的不当认识,如下:

int **p; p = (int**)malloc(4 * NO_ELEMENTS);


这行代码假定指针的长度为4字节,而这在LP64中是不正确的,此时是8字节。正确的方法应使用sizeof():

int **p; p = (int**)malloc( sizeof(*p) * NO_ELEMENTS);


注意对sizeof()的不正确用法,例如:

sizeof(int) = = sizeof(int *);


这在LP64中是错误的。
符号扩展
要避免有符号数与无符号数的算术运算。在把int与long数值作对比时,此时产生的数据提升在LP64和ILP32中是有差异的。因为是符号位扩展,所以这个问题很难被发现,只有保证两端的操作数均为signed或均为unsigned,才能从根本上防止此问题的发生。
例2:

long k;
int i = -2;
unsigned int j = 1;
k = i + j;
printf("Answer: %ld\n", k);


你无法期望例2中的答案是-1,然而,当你在LP64环境中编译此程序时,答案会是4294967295。原因在于表达式(i+j)是一个 unsigned int表达式,但把它赋值给k时,符号位没有被扩展。要解决这个问题,两端的操作数只要均为signed或均为unsigned就可。像如下所示:

k = i + (int) j

联合体问题(Union)
当联合本中混有不同长度的数据类型时,可能会导致问题。如例3是一个常见的开源代码包,可在ILP32却不可在LP64环境下运行。代码假定长度为2的unsigned short数组,占用了与long同样的空间,可这在LP64平台上却不正确。
例3:

 

typedef struct {
 unsigned short bom;
 unsigned short cnt;
 union {
unsigned long bytes;
unsigned short len[2];
 } size;
} _ucheader_t;

 


要在LP64上运行,代码中的unsigned long应改为unsigned int。要在所有代码中仔细检查联合体,以确认所有的数据成员在LP64中都为同等长度。
字节序问题(Endian)
因64位平台的差异,在移植32位程序时,可能会失败,原因可归咎于机器上字节序的不同。Intel、IBM PC等CISC芯片使用的是Little-endian,而Apple之类的RISC芯片使用的是Big-endian;小尾字节序(Little- endian)通常会隐藏移植过程中的截断bug。
例4:

 

long k;
int *ptr;
int main(void)
{
 k = 2 ;
 ptr = &k;
 printf("k has the value %ld, value pointed to by ptr is %ld\n", k, *ptr);
 return 0;
}

 


例4是一个有此问题的明显例子,一个声明指向int的指针,却不经意间指向了long。在ILP32上,这段代码打印出2,因为int与long长度一样。但到了LP64上,因为int与long的长度不一,而导致指针被截断。不管怎么说,在小尾字节序的系统中,代码依旧会给出k的正确答案2,但在大尾字节序(Big-endian)系统中,k的值却是0。


找出64位移植问题的可用的GCC鄙夷选项

表3说明了为什么在不同的字节序系统中,会因截断问题而产生不同的答案。在小尾字节序中,被截断的高位地址中全为0,所以答案仍为2;而在大尾字节序中,被截断的高位地址中包含值2,这样就导致结果为0,所以在两种情况下,截断都是一种 bug。但要意识到,小尾字节序会隐藏小数值的截断错误,而这个错误只有在移植到大尾字节序系统上时才可能被发现。

移植到64位平台之后的性能降低
当代码移植到64位平台之后,也许发现性能实际上降低了。原因与在LP64中的指针长度和数据大小有关,并由此引发的缓存命中率降低、数据结构膨胀、数据对齐等问题。 
由于64位环境中指针所占用的字节更大,致使原来运行良好的32位代码出现不同程度的缓存问题,具体表现为执行效率降低。可使用工具来分析缓存命中率的变化,以确认性能降低是否由此引起。
在迁移到LP64之后,数据结构的大小可能会改变,此时程序可能会需要更多的内存和磁盘空间。例如,图2中的结构在ILP32中只需要16字节,但在 LP64中,却需要32字节,整整增长了100%。这缘于此时的long已是64位,编译器为了对齐需要而加入了额外的填充数据。
通过改变结构中数据排列的先后顺序,能将此问题所带来的影响降到最小,并能减少所需的存储空间。如果把两个32位int值放在一起,会因为少了填充数据,存储空间也随之减少,现在存储整个结构只需要24字节。
在重排数据结构之前,在根据数据使用的频度仔细衡量,以免因降低缓存命中率而带来性能上的损失。
如何生成64位代码
在一些情况中,32位和64位程序在源代码级别的接口上很难区分。不少头文件中,都是通过一些测试宏来区分它们,不幸的是,这些特定的宏依赖于特定的平台、特定的编译器或特定的编译器版本。举例来说,GCC 3.4或之后的版本都定义了__LP64__,以便为所有的64位平台通过选项-m64编译产生64位代码。然而,GCC 3.4之前的版本却是特定于平台和操作系统的。 
也许你的编译器使用了不同于 __LP64__的宏,例如IBM XL的编译器当用-q64编译程序时,使用了__64bit__宏,而另一些平台使用_LP64,具体情况可用__WORDSIZE来测试一下。请查看相关编译器文档,以便找出最适合的宏。例5可适用于多种平台和编译器:
例5:

#if defined (__LP64__) || defined (__64BIT__) || defined (_LP64) || (__WORDSIZE == 64)
printf("I am LP64\n");
#else
printf("I am ILP32 \n");
#endif


共享数据
在移植到64位平台时的一个典型问题是,如何在32位和64位程序之间读取和共享数据。例如一个32位程序可能把结构体作为二进制文件存储在磁盘上,现在你要在64位代码中读取这些文件,很可能会因LP64环境中结构大小的不同而导致问题。
对那些必须同时运行在32位和64位平台上的新程序而言,建议不要使用可能会因LP64和ILP32而改变长度的数据类型(如long),如果实在要用,可使用头文件<inttypes.h>中的定宽整数,这样不管是通过文件还是网络,都可在32位和64位的二进制层面共享数据。
例6:

#include <stdio.h>
#include <inttypes.h>
struct on_disk
{
 /* ILP32|LP64共享时,这个应该使用int32_t */
 long foo;
};
int main()
{
 FILE *file;
 struct on_disk data;
 #ifdef WRITE
file=fopen("test","w");
data.foo = 65535;
fwrite(&data, sizeof(struct on_disk), 1, file);
 #else
file = fopen("test","r");
fread(&data, sizeof(struct on_disk), 1, file);
printf("data: %ld\n", data.foo);
 #endif
 fclose(file);
}


来看一下例6,在理想的情况下,这个程序在32位和64位平台上都可正常运行,并且可以读取对方的数据。但实际上却不行,因为long在ILP32和 LP64之中长度会变化。结构on_disk里的变量foo应该声明为int32_t,这个定宽类型可保证在当前ILP32或移植到的LP64数据模型下,都生成相同大小的数据。
混合Fortran和C的问题
许多科学运算程序从C/C++中调用 Fortran的功能,Fortran从它本身来说并不存在移植到64位平台的问题,因为Fortran的数据类型有明确的比特大小。然而,如果混合 Fortran和C语言,问题就来了,如下:例7中C语言程序调用例8中Fortran语言的子例程。
例7:

void FOO(long *l);
main ()
{
 long l = 5000;
 FOO(&l);
}


例8:

subroutine foo( i )
integer i
write(*,*) 'In Fortran'
write(*,*) i
return
end subroutine foo


例9:

% gcc -m64 -c cfoo.c
% /opt/absoft/bin/f90 -m64 cfoo.o foo.f90 -o out
% ./out
In Fortran
0


当链接这两个文件后,程序将打印出变量i的值为"5000"。而在LP64中,程序打印出"0",因为在LP64模式下,子例程foo通过地址传递一个 64位的参数,而实际上,Fortran子例程想要的是一个32位的参数。如果要改正这个错误,在声明Fortran子例程变量i时,把它声明为 INTEGER*8,此时和C语言中的long为一样长度。
结论
64位平台是解决大型复杂科学及商业问题的希望,大多数编写良好的程序可轻松地移植到新平台上,但要注意ILP32和LP64数据模型的差异,以保证有一个平滑的移植过程。

 

 

 

 

 

linux源码包,是不是不分64位包和32位包啊?在64位服务器上,可以安装;在32位服务器上也可以安装?

 

楼主你想累死开发人员啊?
现在的 Linux 下面程序的源代码都是一套,不管是 64还是 32 ,也不管是在 PowerPC 上还是在 SUN 的服务器上,还是在手机上还是在你的计算机上,都是只一套源代码。
不过这一套源代码要看是不是真的能用,一些老软件只能字啊 32位系统下面编译,有的新软件可能针对 64位有优化。
跨架构和跨字节数,一般都需要源代码里面有相应的处理的。尤其是大型软件。

Linux
是遵循 POSIX规范的,所以这种一个源代码支持全部的架构理论上是存在的,但实际上各种条件都可能影响源代码的可移植性
其中的各种原因什么的一句话两句话说不清楚。

对,源码包是不分3264位的。

如果是RPM包,就是已经编译的那种有分64位和32位,二进制类型的可执行文件一般也有分6432位,但是源码包没有分,都可以用来编译安装,但是部分源码包64位和32位编译的时候加的参数有点区别而已

 

 

 

 

 

 

第一讲  什么是64位系统

 

截至本课程编写的时间为止,市场上有两种受欢迎的64位微处理器体系结构:IA64Intel64

1.   IA-64是由 IntelHP 合作开发的64位微处理机体系结构。ItaniumItanium2 微处理机中就是用了这种体系结构。如想了解更多关于IA-64的信息,请查看Itanium

2.   Intel 64 (EM64T / AMD64 / x86-64 / x64)x86体系的继承,同时保持向后兼容。这种体系结构的名字有不同变型,因而导致了一些疑惑,但是以下这些名字所指的都是同样的事物:x86-64, AA-64,Hammer Architecture, AMD64, Yamhill Technology, EM64T, IA-32e, Intel 64, x64想了解更多为什么会有这么多不同的名字,请参看文章

你需要了解到IA-64Intel64是完全不同、不能相互兼容的微处理机体系结构。在本文的范围内,我们仅讨论在Windows软件开发人员中比较受欢迎的Intel64x64/AMD64)结构。相对应的,当我们说起Windows操作系统的时候,我们指的对应Intel64体系的64位操作系统。例如,Windows XP Professional x64 Edition,Windows Vista x64, Windows 7 x64 Intel64所对应的编程模型,对于基于64windows开发的程序员来说,简称为Win64

Intel 64 体系结构

以下给出的信息是基于 "AMD64 Architecture Programmer's Manual. Volume 1.Application Programming".

Intel64 体系结构,在我们看来,就是一个简单但是非常有效的对于现有的商用x86体系结构的反向兼容。Intel64加入了64位地址寻址的内容,同时扩展了资源来更好支持高性能的64位程序。陈旧的16位和32位的应用和操作系统,不需要进行额外的修改或者是重新编译,就可以在64位体系结构上运行。

64位体系结构出现的原因在于应用需要更大的寻址空间。这些应用可能是高性能的服务器,数据管理系统,CAD或者游戏。这些应用将从64位地址空间和更多地寄存器中得到大量的性能提升。在陈旧的x86系统中,只有少量的寄存器存在,因而限制了计算任务的性能。寄存器数量的增加使得能够进一步提高应用的性能。

让我们首先来看一下x64体系的优势。

l  64位寻址空间

扩展的寄存器组

开发者熟悉的命令集

可以在64位结构的操作系统上运行32位程序

可以直接使用32位操作系统

64位操作系统

基本上所有现在的操作系统都有支持64位体系结构的版本。例如,Mircosoft就发布了Windows XP x64。大型的UNIX的开发者也发布了64位版,例如Linux Debian 3.5x86-64,但是这不代表着这些系统的全部代码是64位的。因为64位系统提供反向兼容,有些操作系统和许多应用仍然是32位。因此,64位版的Windows使用了一个特殊的模型 WoW64 (Windows-on-Windows64),这个模型能够翻译32位应用的调用来使用64位操作系统的资源。

地址空间

虽然64位处理器理论上能够支持16 Ebytes (2^64)的内存空间,Win64现在仅仅支持16Tbytes (2^44)的空间。造成这个现象的原因有一些:现代的处理器仅仅能够提供1Tbyte (2^40)的物理存储的寻址。这个体系(不是这个硬件部分)可以扩展到支持4 Pbytes (2^52)的空间,但是在这种情况下你需要大量的内存来存储分页表。

除了上述描述的局限以外,每一种64Windows版本的上内存的大小取决于Mircosoft的商业决定。不同的Windows版本有不同的限制如下表所示。

 

                      

             1不同Windows版本所支持的地址空间

Win64 编程模型

Win32类似的,Win64的页面大小也是4 Kbyte。最靠前的64Kbyte地址空间是不开放的,所以最低的正确的地址是0x10000。而不像在Win32中,系统的DLL占据了超过4 Gbyte的空间。

Intel64 的编译器有一个特性:它们可以用寄存器来更有效率的传递参数给函数,而不是使用堆栈。这就使得使用Win64体系的开发人员可以丢弃调用约定(calling convention)的概念。在Win32中,你可以使用许多约定,比如__stdcall, __cdecl, __fastcall。在Win64种,只有一个调用约定。下面的例子,是来描述4个整型参数是怎样通过寄存器的。

l  RCX: 第一个参数

l  RDX: 第二个参数

l  R8: 第三个参数

l  R9: 第四个参数

在第四个参数之后的参数通过堆栈来传递。如果是要传递浮点型的参数,需要使用XMM0-XMM3寄存器和堆栈。

在调用约定方面的区别,使得开发人员不能够在同一个程序中同时使用64位和32位的内容。用另一句话来说,如果一个应用是通过64位来编译的,所有的动态链接库也要是64位。

通过寄存器来传递参数,是使得64位程序比32位程序快的一个创新。你可以通过64位的数据结构来取得进一步的性能提升。在下一讲中我们将讨论这方面的问题。

 

第二讲 64Windows环境对32位应用的支持

在我们开始讨论64位程序前,让我们谈论一下64Windows系统对32位应用的反向兼容。反向兼容是通过WoW64中的机制来实现的。

WoW64(Windows-on-Windows 64-bit) Windows操作系统的一个子系统,它使得能够在64Windows系统上执行32位应用。

WoW64子系统不支持以下程序:

1.   16位操作系统所编译的程序

2.   32位操作系统所编译的内核(kernel-mode)程序

间接费用

不同处理器体系的WoW64有一些不同。例如,为Intel Itanium 2开发的64Windows版本,使用WoW64来模拟x86指令。这个模拟比起Intel64WoW64体系需要更多资源,因为系统在执行32位程序的时候需要从64位模式转换到兼容模式。

Intel 64 (AMD64/ x64) 上的WoW64不需要指令模拟。在这个系统上,WoW64子系统仅仅是通过在32位应用和64Windows API之间添加额外的一层,来模拟32位的环境。在有些地方,这新加的一层可能比较薄,在另一些地方这一层比较厚。平均来说,对于一个程序,你需要期待因为这一个层次所带来性能上2%的损失。对于有些程序来说,这个数值可能更大。2%可能并不是一个很大的数字,但是你需要铭记在心的是32位程序在64位系统中比它们在32位系统中运行的要慢。

把程序编译为64位的代码,不仅使你避免使用WoW64,同时能使你得到了性能上提升。这个可以通过体系结构上的改变,如更多的通用寄存器来解释。平均来说,对于一个程序,通过简单的重新编译,你可以期待着在性能上有5%-15%的提升。

64位环境上运行32位程序的好处

因为WoW6432位程序在64位系统中比它们在32位系统中运行的要慢。但是简单的32位程序可以从在64位系统上执行获得一个好处。或许你知道,如果在32Windows系统选择“/3gb”,程序编译时选择"/LARGEADDRESSAWARE:YES",它可以分配到最高达3 Gbytes的内存空间。同样的32位程序在64位系统可以分配大最高达4Gbytes的内存空间(现实中大约是3.5 Gbytes的内存空间)。

重定位

WoW64子系统是通过将调用重定位至文件和寄存器,从而将32位程序与64位程序分离开来。这样使得32位程序不会意外的接触64程序的数据。例如,一个从"%systemroot%\System32"中调用了DLL文件的32位应用程序,如果不进行隔离,那么它可能调用了一个32位程序无法兼容的64DLL文件。为了避免这样的情况发生,WoW64位子系统将进入"%systemroot%\System32"文件夹的调用重定位到调用"%systemroot%\SysWOW64"文件夹中的文件。这种重定位使得开发者能避免兼容性的问题,因为32位应用程序需要能与它们兼容的特殊的DLL文件。

如想了解更多关于文件系统或寄存器的重定位,可以参考MSDN相关章节"Running32-bit Applications"

为什么32DLL不能在64位程序中使用? 是否有方式可以避免这种局限?

现在是不能够在64位进程中调用32DLL并执行它的代码。这种不可行是因为64位系统的设计造成的。所以说是从根本上的不可能。没有任何技术,也没有任何文档没有记录的方式可以帮助到你。为了做到这个,你必须要装载并初始化WoW64,更不用说内核结构的不同了。这代表着64位的处理器必须在运行中被处理为32位。这个话题在"Why can't you thunk between 32-bit and 64-bitWindows?". 中有更为详尽的描述。有一件事情是我建议可以尝试的,就是建立一个使用COM技术创建一个替代程序,你可以阅读"Accessing32-bit DLLs from 64-bit code".

但是,从32DLL将资源导入到64位进程中去,是比较容易的,你所需要的就是在调用LoadLibraryEx的时候制定以下的flagLOAD_LIBRARY_AS_DATAFILE

逐渐放弃对32位程序的支持

微软公司为了进一步促进程序向64位迁移,会逐渐在一些版本的Windows操作系统中取消对32位程序的支持。这是一个自然的过程。当然这个过程会比较缓慢,但这个行为已经开始了。

许多管理员可能会知道一个较新的操作系统上的服务器安装和操作模式,叫做Server Core这个模块正是在持久的“Windows VS Linux”大战中被广泛提起。其中一个支持使用Linux的原因就是Linux支持不需要图像接口就可以安装服务器操作。但是,现在Windows Server

也有了这种能力。如果你在这种模式中安装系统,你将只获得系统的命令行,而不需要用户界面。

这种能力(Server Core安装)出现在Windows Server 2008.但是在 Windows Server2008 R2中,另外一种创新出现使得系统更接近64位。在Windows Server 2008 R2 (Server Core )中,你可以启动或者禁用系统对32位应用程序的支持。更重要的是对32位应用程序的支持默认是被禁用的。所以当你尝试在Server Core mode中启动32位程序的时候,你会得到一条系统消息告诉你,这是不可以的。如果你需要额外手动启用这种支持,可以通过以下命令实现:

start /w ocsetupServerCore-WOW64

在普通的模式(完全安装),执行32位程序的支持默认是启用的。

这种趋势是十分明显的,因而现在开始创建64位版本的应用是理智的,因为64位的程序能保证在更多的操作系统版本上使用。

额外信息

Alexey Pahunov Russian blog 也是获取WoW64资料的好地方。AlexeyMicrosoft的员工,他参与了WoW64子系统的开发。

 

第三讲 将代码导入64位系统的优缺点

你需要带着以下的问题来学习64位系统:64位系统重新编译项目的合理性有多少?回答这个问题,你需要花费一些时间和精力来思考。在一方面,你可能会因为要提供64位系统的应用支持,使得你在市场上落后于你的对手。在另一方面,你可能会花费了时间在开发64位系统应用上,而这一努力并不能给你带来竞争优势。

以下是一些建议可以用来帮助你做出选择。

应用的生命期

当你的应用有一个比较短的生命期,你暂时还没有必要开发应用的64位版本。WoW64子系统使得陈旧的32位应用在64位机上也能取得比较好的表现。如果你在两年内会停止运营你的产品,你是不需要现在建立一个64位版本的。实践证明向64位系统的迁移是一个非常缓慢平和的过程。可能你的大多数用户在未来的一段时间里还是会仅仅使用你系统的32位版本。你需要知道,本教程是在2009年编写的,这时候大部分用户都还是在使用32位系统。但是很快32位程序,就会变得越来越不自然,会变得落伍的。

如果你的项目有一比较长的开发和维护时间,你需要开始建立你产品的64位版本。当然你可以慢慢来,但是你要记住的是,越迟拥有产品的64位版本,在64位系统上维护32位版本所带来的问题将越来越多。

程序性能要求

当一个程序重新编译以适应64位版本后,程序可以应用大量的内存资源,它的速度将提高5%-15%5%-10%的系统提升是因为64位体系结构的特点,如更多的寄存器,所导致的。另外的1%-5%的性能提升是因为抛弃了翻译32位应用使之在64位系统环境运行的WoW64层所带来。

例如,Adobe公司称新版本64位的"Photoshop CS4"32位版本的快了12%

需要大量的内存的应用可以期待着有更高的性能提升。比如说,图像编辑器,CAD系统,GSI CAD,数据库和其他的模型包。能将所有的数据存储在内存中,而避免了多余的从硬盘中导入的工作,使得这些应用的速度,可能不是提升了几个百分点,而是成倍的提高。

例如,Alfa-Bank曾经在他们的IT 基础设施中使用了基于Itanium 2的平台。原因在于他们业务的发展使得现有系统无法支持不断增长的数据量。每个用户的服务延迟变得非常严重。分析显示,系统的瓶颈不在于处理器的表现,而在于32位体系结构与内存子系统的关系上,因为32位体系结构使得只能使用4 Gbyte的服务器地址空间。而它们的数据库却大于9 Gbyte。因而导致了子系统的输入输出的临界负荷。Alfa-Bank决定购买一个由2个服务器组成的集群。这两个服务器都拥有4处理器、是基于Itanium 2的、拥有12Gbyte内存的。这个决定使得他们的性能要求达到了可以容忍的程度。这个公司的代表称,引入基于Itanium2的服务器使得他们解决了重要问题,同时也节约了花费。

在项目中使用第三方库

在决定是否开发你产品的64位版本前,请先确认你所依赖的第三方库是否有64位版本。你需要找出第三方软件64位版本的定价是多少。这些你都可以在库开发者的网站上找到。如果,没有依赖库的64位版本的支持,可以寻找其他支持64位的库作为替代。

你所开发的库的第三方开发者

如果你在开发库,组件,或者其他需要给第三方开发者使用的程序时,你需要尽快的开发一个你的库的64位版本。否则需要64位支持的你的客户会转向其它的替代产品。例如,一些软件和硬件安全方面的开发者,都非常迟的开发64位版本,使得他们的一些客户转向了其他工具来保护自己的产品。

发布你所开发库的64位版本有另一个好处,你可以把它作为一个单独模块来出售。因而想要开发32位和64位的客户会要购买2个不同的授权。例如Spatial Corporation,就是按这个方式来卖他们的Spatial ACIS库的。

16位应用

如果你的应用仍然有16位的模块,你需要丢弃它们。64位的Windows版本完全不支持16位的应用。

关于使用16位安装程序的人员,我需要解释一个事情。这样的安装程序仍然在被使用于安装一些32位应用程序。因为对于一些比较流行的16位的安装程序,它们之中包含了一些机制,使得它们能够在运行中被更新的32位安装程序。这可能使得你觉得16位的程序,在64位的系统环境中仍能使用,但这是错误的,请牢记。

汇编器代码

不要忘记了,大量的汇编器代码片是使得创建应用的64位版本更为困难的原因之一。

工具箱

如果考虑以上提到各种因素后,你决定创建一个你的应用的64位版本,成功不是一定的。你还需要一些必要的工具,以下就是一些你可能会碰到的不愉快的事情。

首先最令人不开心的事情,就是缺少64位的编译器。当我们写本教程的时候(2009),Embarcadero还没有64C++编译器,它有期望在2009年年底发布一个。除非你重新编写你自己的部署部分,如使用Microsoft Visual Studio时可以快速改变配置,否者你没有办法回避这个问题。但其他的问题,就没有像缺少编译器这样,容易被察觉,可能会在你将程序导入一个新的体系结构的时候发生。你可以在开始工作前,做一个调研,看所有需要的组件是否都有64位版本可以使用。不然,你将面临不愉快的事情。

做决定时,还需要注意到一个我们还没提到的问题,修改你的代码使之可以在64位模式下编译所需要的花销。我们会在之后的课程里,告诉你怎样估计这个花销。这个花销可能是非常高昂的,需要经过系统的计划和详细的时间安排。

 

 

 

第五讲 编译64位应用    

我们需要向读者说明,我们不可能涵盖编译一个64位应用的所有细节问题。每个项目有自己的特有的设定,所以你需要特别注意这些设定。本讲只会涵盖对任何程序都重要的步骤,这些步骤只是一个开始,更多的问题需要读者自己去解决。

当你尝试编译一个你产品的64位版本时,首先确认所有必须的64位的库已经成功安装了,而且它们的路径是正确的。例如32位与64位的以“lib”结尾的库文件是不同的,而且在不同的目录中,如果有bug请修改。

注意:如果库是以源代码的形式存在的,那么会存在库项目的64位的配置。注意到,当你修改一个源代码的配置,以编译适合你的64位版本时,你存在可能侵犯了版权的可能性,请谨慎。

汇编器

Visual C++ 不支持64位的内联汇编(inline assembler,你可以选择一个外部的64位汇编器(如,MASM)或者是重写这些汇编语言。

编译错误和警告的实例

当开始编译你的程序时,你会碰到许多因为显式类型转换和隐式类型转换所带来的问题(explicit and implicit type conversions)。以下是一个例子。

这段代码在32位上成功编译,但在64位上不行,Visual C++会产生如下warning

因为函数strlen()返回的类型是size_t,32位系统上,类型size_tunsigned int的类型一致,所以编译器选择"voidfoo(unsigned int)"来调用。但在64位的模式下,size_tunsigned int的类型不一致。 size_t变成了64位,但unsigned int类型仍然保持了32位。因而编译器不知道哪一个foo()函数来调用。

现在来考虑一段由Visual C++编译64位代码时,所产生的错误:

GetSize()函数返回的类型是 INT_PTR,在32位时,与int类型一致。在64位时,INT_PTR64位而隐式的转换为32位。因为可能会导致高位值丢失,这就是系统给出警告的原因。当数组中元素数量超过了INT_MAX,一个隐式类型转换就有可能会导致错误。去除这个警告和可能错误的方式就是,你应该将len的类型写为INT_PTR或者是ptrdiff_t 类型。

在你已经完全了解了64位错误的规律前,都不要去隐藏warning。你有可能会隐藏一个错误,使得之后更难发现它。你可以在之后的课程中,学到更多的关于64位错误的规律和发现他们的方式。你可以同样参考一下文章  "20issues of porting C++ code on the 64-bit platform", "A 64-bit horse that can count"

size_t ptrdiff_t 类型

在大多数关于数据不兼容的编译错误和警告中,我们应该尤其考虑两种类型 size_tptrdiff_t,这两者在编译64位代码中最容易出现问题。如果你使用的是Visual C++编译器,这些类型可能被整合到编译器中,你不需要增加库描述文件。但是如果你使用的是GCC你可能需要添加头文件“stddef.h”

size_t 是一个C/C++基本unsigned integer类型。它是sizeof操作的返回值。这种类型的大小之所以这样选择,是因为它可以用来存储理论上的最大数组的大度值。例如,size_t32位系统上是32位,在64位系统上是64位。换句话说,你可以安全的用size_t类型的变量存储一个指针,但是不包括指向函数的指针。这个类型经常作为在循环中的计数器的类型,作为数组索引,用来存储长度,或者用于地址的计算中。以下的这些类型与size_t类似:SIZE_T, DWORD_PTR, WPARAM, ULONG_PTR虽然你可以在size_t中存储一个指针,但是最好使用另外一个unsigned integer类型uintptr_t ——它的名字就反映了它的用途。size_tuintptr_t是同义的。

ptrdiff_t是一个C/C++基本signed integer类型。这种类型的大小之所以这样选择,是因为它可以用来存储理论上的最大数组的大度值。例如,ptrdiff _t32位系统上是32位,在64位系统上是64位。与size_t类似,你可以安全的用ptrdiff _t 类型的变量存储一个指针,但是不包括指向函数的指针。ptrdiff_t类型也是表达式"ptr1-ptr2"的返回值。这个类型经常作为在循环中的计数器的类型,作为数组索引,用来存储长度,或者用于地址的计算中。与它类似的是SSIZE_T, LPARAM,INT_PTR, LONG_PTR与它同义的是intptr_tintptr_t可以用来存储一个指针。

size_t ptrdiff_t被创造就是为保证地址运算的正确性。在较长的时间内,int的类型都与机器字(machine word)的长度一致,因而int类型变量经常被作为数组索引,或者是用来存储物体和指针大小。因而地址运算也是通过intunsigned类型来编译的。int类型在大多数的C/C++教程中出现在循环体做作为索引。下面是一个经典的例子:

当处理器不断的发展,继续扩展int的长度显得不那么合理了。这是由许多因素造成的如:节约使用的内存,最大的兼容性等。因而,许多的数据模式出现,来描述基本CC++类型之间的关系。所以现在选择一个类型来存储指针和物体大小就不那么容易了。size_tptrdiff_t类型的出现就成了最好的解决方案。它们肯定可以在地址运算中使用。现在如下的代码将成为经典:

这段代码才是能提供最好的安全性,移植性和高性能的代码。在之后的课程中你将学到原因。

size_t ptrdiff_t类型可以被称为memsize类型。Memsize的出现是为了简单的描述所有能作为存储最大数组的长度值,和作为这样数组的索引的值的类型。当说到memsize类型时,你需要能立刻想到它们在32位系统上是32位的,在64位系统上是64位的。以下是一些memsize类型:size_t, ptrdiff_t,pointers, SIZE_T, LPARAM

 

 

 

 

 

32移位植到64位 注意事项

32位移植到64位 注意事项

32bit-64bit porting work注意事项

64位服务器逐步普及,各条产品线对64位升级的需求也不断加大。在本文中,主要讨论向64位平台移植现有32位代码时,应注意的一些细小问题。

什么样的程序需要升级到64位?

理论上说,64位的操作系统,对32位的程序具有良好的兼容性,即使全部换成64位平台,依然可以良好的运行32位的程序。因此,许多目前在32位平台上运行良好的程序也许不必移植,有选择,有甄别的进行模块的升级,对我们工作的展开,是有帮助的。

什么样的程序需要升级到64位呢?

除非程序有以下要求:

l  需要多于4GB的内存。

l  使用的文件大小常大于2GB。

l  密集浮点运算,需要利用64位架构的优势。

l  能从64位平台的优化数学库中受益。

ILP32和LP64数据模型

32位环境涉及"ILP32"数据模型,是因为C数据类型为32位的int、long、指针。而64位环境使用不同的数据模型,此时的long和指针已为64位,故称作"LP64"数据模型。下面的表中列出了常见的类型大小比较:

Data type

Data length(32bit)

Data length(64bit)

Signed

char

8

8

Y

unsigned char

8

8

N

short

16

16

Y

unsigned short

16

16

N

int

32

32

Y

unsigned int

32

32

N

long

32

64

Y

unsigned long

32

64

N

long long

64

64

Y

point

32

64

N

size_t

32

64

N

ssize_t

32

64

Y

off_t

32

64

Y

 

 

 

 

   由上表我们可以看出,32位到64位的porting工作,主要就是处理长度变化所引发的各种问题。在32位平台上很多正确的操作,在64位平台上都不再成立。例如:long->int等,会出现截断问题等。下面将详细阐述具体遇到的问题,并给出修改策略。

截断问题

截断问题是在32-64porting工作中最容易遇到的问题。

部分的截断问题能够被编译器捕捉到,采用-Wall –W进行编译,永远没有坏处。这种问题处理方法也非常简单,举个例子来说:

        long mylong;

        (void)scanf("%d",&mylong);// warning: int format, different type arg(arg 2)

        long mylong;

        (void)scanf("%ld",&mylong);// ok

但有很多情况下,一些截断性问题并不能被良好的诊断出来。

例如:

long a;

int b;

b = a;

在这种情况下,编译器会直接进行转换(截断处理),编译阶段不报任何警告。当a的数据范围在2G范围内时,不会出问题,但是超出范围,数据将出现问题。

另外,采用了强制转换的方式,使一些隐患被保留了下来,例如:

       long mylong;

        (void)scanf("%d",(int*)&mylong);//编译成功,但mylong的高位未被赋值,有可能导致问题。

   采用pclint可以有效的检查这种问题,但是,在繁多的warning 中,找到需要的warning,并不是一件容易的事情。

因此,在做平台移植的时候,对于截断问题,最根本的还是逐行阅读代码,详细检测。

在编码设计的时候,尽量保持使用变量类型的一致性,避免发生截断问题。

建议:在接口以及数据结构的定义中不要使用指针,long以及用long定义的类型(size_t, ssize_t, off_t, time_t),由于字长的变化,这些类型不能32/64位兼容。

 

一个讨厌的类型size_t:32bit平台上,它的原形是unsigned int,而在64bit平台上,它的原形式unsigned long。这导致在printf等使用时:无论使用%u或者%lu都会有一个平台报warning。目前我们的解决办法是:采用%lu打印,并且size_t强制转换为unsingedlong。在小尾字节序(Little-endian)的系统中,这种转换是安全的。

常量有效性问题

那些以十六进制或二进制表示的常量,通常都是32位的。例如,无符号32位常量0xFFFFFFFF通常用来测试是否为-1;   

#define INVALID_POINTER_VALUE 0xFFFFFFFF

然而,在64位系统中,这个值不是-1,而是4294967295;在64位系统中,-1正确的值应为0xFFFFFFFFFFFFFFFF。要避免这个问题,在声明常量时,使用const,并且带上signed或unsigned。

例如:

const signed int INVALID_POINTER_VALUE =0xFFFFFFFF;

上面一行代码将会在32位和64位系统上都运行正常。或者,根据需要适当地使用 “L” 或 “U” 来声明整型常量。

又比如对最高位的设置,通常我们的做法是定义如下的常量0x80000000,但是可移植性更好的方法是使用一个位移表达式:1L <<((sizeof(long) * 8) - 1);

 

 

参数问题

在参数的数据类型是由函数原型定义的情况中,参数应该根据标准规则转换成这种类型

。在参数类型没有指定的情况中,参数会被转换成更大的类型

。在 64 位系统上,整型被转换成 64 位的整型值,单精度的浮点类型被转换成双精度的浮点类型

。如果返回值没有指定,那么函数的缺省返回值是 int 类型的

。避免将有符号整型和无符号整型的和作为 long 类型传递(见符号扩展问题)

请看下面的例子:

long function (long l);

int main () {

        int i = -2;

        unsigned k = 1U;

        long n = function (i + k);

}

上面这段代码在 64 位系统上会失败,因为表达式 (i + k) 是一个无符号的 32 位表达式,在将其转换成long 类型时,符号并没有得到扩展。解决方案是将一个操作数强制转换成 64 位的类型。

扩充问题(指针范围越界)

扩充问题与代码截断问题刚好相反。请看下面的例子:

int baidu_gunzip(char*inbuf,int len,char* outbuf,int* size)

{

   ……

ret=bd_uncompress((Byte*)outbuf,(uLongf*)size,

                      (Byte*)(inbuf+beginpos),inlen);

}

这是ullib库中baidugz模块的一段代码。在这段代码中,将int型的指针改为long型的指针传递给了bd_uncompress函数。在32位系统中,由于int与long都是32bit,程序没有任何问题,但在64位系统中,将导致指针控制的范围在调用函数中扩展为原来的2倍,这将有可能导致程序出core,或指示的值不正确。这种问题比较隐蔽,很难发现,但危害极大,需要严格注意。

解决方法:加强对指针型参数的检查,看是否有范围扩充的问题。

符号扩展问题

要避免有符号数与无符号数的算术运算。在把int与long数值作对比时,此时产生的数据提升在LP64和ILP32中是有差异的。因为是符号位扩展,所以这个问题很难被发现,只有保证两端的操作数均为signed或均为unsigned,才能从根本上防止此问题的发生。

例如:

long k;

int i = -2;

unsigned int j = 1;

k = i + j;

printf("Answer:%ld\n", k);

你无法期望例2中的答案是-1,然而,当你在LP64环境中编译此程序时,答案会是4294967295。原因在于表达式(i+j)是一个unsigned int表达式,但把它赋值给k时,符号位没有被扩展。要解决这个问题,两端的操作数只要均为signed或均为unsigned就可。像如下所示:

k = i + (int) j

C/C++ 中,表达式是基于结合律、操作符的优先级和一组数学计算规则的。要想让表达式在 32 位和 64 位系统上都可以正确工作,请注意以下规则:

两个有符号整数相加的结果是一个有符号整数。

l int  long 类型的两个数相加,结果是一个long 类型的数。

如果一个操作数是无符号整数,另外一个操作数是有符号整数,那么表达式的结果就是无符号整数。   

l  int  doubule 类型的两个数相加,结果是一个 double 类型的数。此处 int 类型的数在执行加法运算之前转换成 double 类型。

将字符指针和字符字节声明为无符号类型的,这样可以防止位字符的符号扩展问题。

联合体问题(Union)

当联合本中混有不同长度的数据类型时,如果单独使用里面定义的成员,一般没有问题。但在一些复杂的操作中,例如几种类型的混用,可能会导致问题。如例3是一个常见的开源代码包,可在ILP32却不可在LP64环境下运行。代码假定长度为2的unsigned short数组,占用了与long同样的空间,可这在LP64平台上却不正确。

union{

unsigned long bytes;

unsigned short len[2];

} size;

    正确的方法是检查是否对结构体有特殊的应用,如果有,那么需要在所有代码中仔细检查联合体,以确认所有的数据成员在LP64中都为同等长度。

对齐问题

现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32 位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。

g++默认对齐方式是按结构中相临数据类型最长的进行

在32位上,这些类型默认使用一个机器字(4字节)对齐。

在64位上,这些类型默认使用最大两个机器字(8×2=16字节)对齐(按16字节对齐会有好处么?估计于编译器的具体实现相关)

举两个例子说明一下:

struct asdf {

     int a;

     long long b;

};

这个结构在32bit操作系统中,sizeof(asdf) =12,在64bit操作系统中,sizeof(asdf)=16

struct asdf {

     int a;

     long double b;

};

这个结构在32bit操作系统中,sizeof(asdf) =16,在64bit操作系统中,sizeof(asdf)=32.这里需要说明的是,32位sizeof(long double) = 12, 64位sizeof(longdouble)=16

在跨平台的网络传输过程中,或者文件存取过程中,我们经常会遇到与数据对齐相关的问题,并且为此烦恼不已。64位porting工作,同样需要详细处理这种问题。

幸好在我们的移植过程中,因为平台比较一致,字节对齐问题并不是非常突出。但需要注意字节长度改变带来的问题,举例说明如下:

struct t

{

        char b;

        short c;

        size_t a;

};

例如,我们经常需要把结构存储到文件中去,常规的写法会如下进行:

写操作:

struct t st;

fwrite(&st, sizeof(struct t), 1, fp)

读操作:

struct t st;

fread(&st,sizeof(struct t),1,fp)

这种操作,如果针对同一平台,当然没有问题,但是,如果在32位机器上存储的文件,copy到64位系统中去,就会造成严重问题,因为size_t在64位平台上定义为unsigned long型,导致结构按8字节对齐,并且sizeof(struct t)=16,不再是32位平台上的8字节。在网络传输中,问题同样有可能发生,需要严格注意。

我们可以采用的解决问题的方法有如下几种:

了解平台对齐差异,以及长度变化,修改结构:

例如上例:

struct t

{

        char b;

        short c;

        u_int a;    //不使用size_t类型

};即可保证两个平台的一致性。

在复杂的机器环境以及网络环境中,还需要进一步采用单字节对齐的方式避免出现其他问题:

struct t

{

        char b;

        short c;

        u_int a;

} __attribute__ ((packed));

此时sizeof(struct t) = 7(强制结构安1字节对齐)。

 

内存分配问题以及指针跳转问题

通常我们写程序的时候,容易引入一些不安全的内存分配方式,以及进行一些不安全的指针跳转操作。例如下面的例子:

int **p; p = (int**)malloc(4 * NO_ELEMENTS);

在上面的代码中,同样只对ILP32有效,对LP64来讲,将是致命的错误。在64位平台上,指针已经是8字节,这将导致空间分配不足,越界操作等。正确的方法是严格使用sizeof()进行处理:int **p; p =(int**)malloc(sizeof(int*)* NO_ELEMENTS);

 

在比如:在处理网络交互的报文过程中,经常要处理各种数据结构

typedef struct A{

long a;

int b;

}*pA;

A a;

char * p = &a;

long * p1 = (long*)p;

int * p2 = (int *)(p + 4);

……

在上面的例子里,同样是进行了错误的偏移计算,导致出错,另外,上面的用法还有可能因为字节对齐方式的不同,产生不同,坚决不推荐使用。

正确的使用方式如下:

long * p1 = &(pA)p->a;

int * p2 = &(pA)p->b;

或者如下:

long * p1 = p + (long)(((pA)NULL)->a);

int * p2 = p + (long)(((pA)NULL)->b);

内存消耗问题与性能

在升级到64位操作系统后,内存消耗也会随之增加。例如复杂的结构中(b+tree dict等),会保存大量的指针类型,而这些指针类型在LP64模型中,自身占用内存会大量增加。在内存消耗较多的程序中,升级时要评估升级代价。在必要的情况下,需要修改程序内部逻辑,采用局部偏移代替指针类型,以便节约空间消耗。

另外,由于字节对齐问题,也容易导致结构体的不正常膨胀。通过改变结构中数据排列的先后顺序,能将此问题所带来的影响降到最小,并能减少所需的存储空间。例如把两个32位int值放在一起,会因为少了填充数据,存储空间也随之减少。

另外,由于64位环境中指针所占用的字节更大,致使原来运行良好的32位代码出现不同程度的缓存问题,具体表现为执行效率降低。可使用工具来分析缓存命中率的变化,以确认性能降低是否由此引起。

字节序问题

字节序问题由来已久,不过,很幸运的是,我们采用的都是x86的结构,因此字节序问题并不会困扰我们的开发工作,简单介绍如下:

一个机器的字长通常包含数个byte,在存储数据的方法上出现了大端点(big endian)和小端点(little endian)两种结构,前者如PowerPC和Sun Sparc,后者如Intel x86系列。大端点机使用机器字长的高字节存储数字逻辑编码的低字节,字节的屋里顺序和逻辑顺序相反;小端点机使用机器字长的高字节存储数字逻辑编码的高字节,字节的屋里顺序和逻辑顺序相同。TCP/IP等传输协议使用的都是大端点序。大端点机和小端点机实际上各有优点,   由于Little Endian提供了逻辑顺序与物理顺序的一致性,让编程者摆脱了不一致性所带来的困扰,C语言开发者可以无所顾忌的按照自己的意愿进行强制类型转换,所以现代体系结构几乎都支持Little Endian。但Big Endian也有其优点,尤其对于汇编程序员:他们对于任意长度的整数,总是可以通过判断Byte 0的bit-7来查看一个整数的正负;对于Little Endian则不得不首先知道当前整数的长度,然后查看最高byte的bit-7来判断其正负。对于这种情况,big endian的开发者可以写出非常高效的代码。

这个差别却给跨平台的程序编写和不同平台主机间的通信带来了相当的困扰。在c/c++中使用强制类型转换,如int到char数组的转换在有些时候可以写出简洁高效的程序,但字节序的不同确实这种写法有些时候变得很困难,跨平台的程序,以及处理网络数据传输和文件数据转换年的时候,必须要考虑字节序不同的问题。其实在C++中检测和转换字节序并不困难,写出的程序可以多种多样,基本的思想却是相同的。

检测平台的Endian基本上都是用联合体union来实现的,在union中的数据占有的是最长的那个类型的长度,这样在union中加入不同字节长度的数据并将较长的那个赋值为1就可以判断了:

typedef union uEndianTest{

struct

{

   boolflittle_endian;

   bool fill[3];

};

long value;

}EndianTest;

static const EndianTest __Endian_Test__ = { (long)1 };

const bool platform_little_endian = __Endian_Test__.flittle_endian;

这样使用这个 platform_little_endian 就可以检测到当前平台是否是little_endian

Glibc升级以及内核升级带来的问题

升级到64位平台后,由于glibc的升级,以及内核版本的升级,导致个别的函数调用行为与原函数不再一致,对于这种问题,需要不断的总结和积累,慢慢克服。下面介绍一下目前在升级过程中已知的问题。

 

sendfile函数:在2.4的内核中,sendfile是可以支持文件间的相互copy的,但是在2.6以上的内核中,却只支持文件向socket的copy。为什么去掉这样一个优秀的特性,另人费解。

 

gethostbyname等函数:包括gethostbyname,gethostbyname_r,getservbyname_r,getservbyport_r等函数,在升级到64位平台后,程序进行-static编译的过程中,会报warning,并且无法消除。经过调查,在使用静态连接的程序中,调用上述函数,的确是存在一定风险的,如果编译机器和运行机器的glibc版本号不一致,会导致严重问题,例如程序crash等。因此,建议调用了上述函数的程序,最好不要使用-static进行编译。事实上,在老版本的gcc上,同样存在上述风险,只是没有报出warning而已。

 

另外,在不同的版本中,gethostbyname_r的返回值也存在差异,在异常返回的时候,因此,这几个函数在判断是否成功的时候,需要注意,并不能单纯的判断返回值。这个问题在ul_gethostbyname_r中已经做了处理,可放心使用。

 

其他

如果在make的过程中,采用的是gcc的编译器,而不是g++,可能遇到很多的错误,别紧张,这是由于gcc无法自动和c++使用的库相连接而导致的。改成g++进行编译就ok,或者在make的时候指定-lstdc++。

另外有一些编译要求,请参考《技术部64位开发规范》

 

 

参考

http://www.sawin.cn/doc/SP/SPExperience/TheEdge225.htm

 

http://www-128.ibm.com/developerworks/cn/linux/l-port64.html

 

ullib支持32位和64位环境的考虑.doc 丘志东

 

 

 

 

构建64位程序

由于每个为64位系统构建的64位应用程序都有它唯一的设置,我们只描述通用的步骤。
库(libraries
  在试图构建你的64位程序前,请球包你需要的64位库的版本和路径都安装正确。例如:32bit64bit库文件应该是在不同的目录下。
  注意:如果库是以源码的形式,那么64位的程序用到它时,该库必须有64位的配置。
汇编程序(Assembler
VC不提供64位内联的汇编程序。你必须用一个额外的64bit的汇编程序(例如MASM)或者改写c/c++的汇编代码
兼容性错误和警告的例子
  在开始编译程序时,你也许会遇到很多关于"类型转换"的兼容性错误或警告。例如:
void foo(unsigned char) {} 
void foo(unsigned int) {}  
void a(const char *str) 

foo(strlen(str)); 

  这段代码在32位模式下是编译成功的,但是有警告:
warning C4267: 'argument' : conversion from 'size_t' to'unsigned int', possible loss of data
  但是在64位模式下,VC编译器会有如下错误:
error C2668: 'foo' : ambiguous call to overloadedfunction 
   .\xxxx.cpp(16): could be 'void foo(unsignedint)' 
   .\xxxx.cpp(15): or 'void foo(unsignedchar)' 
   while trying to match the argument list'(size_t)' 
strlen()函数返回的类型是size_t。在32位系统上,size_tunsigned int一致,因而编译器会选择"voidfoo(unsigned int)"函数。在64位模式下,size_t就不是unsigned int了,size_t变成了64位的,而unsigned int还是32位。因而,编译器就不知道该选哪个foo()函数了。
  现在让我们看看用64位编译代码时,VC的另一个警告
CArray<char, char> v; 
int len = v.GetSize();  
warning C4244: 'initializing' : conversion from 'INT_PTR'to 'int', 
                possibleloss of data 
GetSize()函数返回的是INT_PTR,在32位的代码里,是和int一致的。但是在64位的代码里,INT_PTR64位,因而会被隐式的转换成32位的int。这样就会有bit位的丢失。一个隐式的类型转换也许会产生一个错误,例如当一个数组的数目扩展到INT_MAX。为了消除警告和可能的错误,你应该使用INT_PTR或者ptrdiff_t来表示长度。
  不要尝试屏蔽警告。因为你可能会隐藏一个错误,而且也让他更难被发现。你将会在接下来的课程里学习64位下的错误类型和检查纠正他们的方法。
size_tptrdiff_t
size_tptrdiff_t,是64位编程里和我们最紧密的两种类型,也常常产生兼容性的错误。如果你用VC,这些类型是集成进VC的,因而你不需要库文件。但是如果你用GCC,你将需要用头文件"stddef.h"
size_tc/c+=的无符号整数。他是sizeof操作返回的结果的数据类型。这个类型的大小是可选的,以便它能够储存一个任意类型的数组的最大的size。例如:size_t32位系统上是32位,在64位系统上是64位。换言之,你可以用size_t来安全的存储一个指针。不过有一种情况例外,就是指向类函数的指针。size_t常常被用于循环里的计数,数组的索引,大小的表示,和地址之间的计算。下面的几种类型和size_t是类似的作用:SIZE_T, DWORD_PTR, WPARAM, ULONG_PTR。尽管你可以把指针存储在size_t里,但是最好还是用另一个无符号的整数类型uintptr_t,因为他的名字更能反映它的功能。size_tuintptr_t实质上是一样的。
ptrdiff_tc/c++的有符号类型。32位系统上他是32位,64位系统上他是64位。像size_t一样,ptrdiff_t能用于安全的存储一个指针(除了指向类函数的指针)。ptrdiff_t是两个指针相减后的结果所用的类型。ptrdiff_t常常被用来做循环的计数、数组的索引和地址之间的计算。下面的几种类型和它是类似的作用:SSIZE_T, LPARAM,INT_PTR, LONG_PTRptrdiff_tintptr_t是同义词,不过intptr_t更能表现出它储存的是一个指针。
size_tptrdiff_t都可以做地址运算。以前int被认为和机器字一致,能用来表示指针或者对象的索引和大小。因而地址运算使用int或者unsigned来表示。以前常见的代码如下:
for(int i = 0; i < n; i++)
a = 0;
  但是当处理器的能力变强大后,上述这样的代码就可能不合理了。需要改成下面这种,在64位上
for(ptrdiff_t i = 0; i < n; i++)
a = 0;
  这段代码更安全,性能也很好,可移植。下一课我们会具体介绍。
size_tptrdiff_t,是memsize类型。memsize表示所有能储存指针大小或者是储存最大的数组索引的类型。memsize表示一类数据,它在32位机器上是32位的,在64位机器上是64位的。例如:size_tptrdiff_t,指针,SIZE_TLPARAM

64位代码里常见的错误

即便你修改了编译时的所有错误和警告,也不意味着你的64位应用程序就能良好工作。因而我们需要学会如何诊断64位错误。另外i,不要依赖于切换到/WP64,虽然它常常被描述为是查找64位代码问题的好工具。
/WP64切换
  切换到/WP64允许程序员找到一些编译64位程序的错误。检查是用下述方式实行的:在32位的代码里,类型用关键字_w64来修饰,这样在检查时会被作为64位的类型来解释。例如:

typedef int MyInt32;




#ifdef _WIN64



typedef __int64 MySSizet; 

#else 

typedef int MySSizet; 

#endif 

void foo() { 

MyInt32 value32 = 10; 

MySSizet size = 20; 

value32 = size; 

}


  表达式"value32 = size",将会在64位系统上截断size,因而可能会有个错误。我们想要诊断这个错误。但是当我们试图去编译32位程序时,都是正确的,而且没有警告。
  为了把程序移植到64位系统上,我们需要切换/Wp64,并且在32位程序里增加关键字__w64。代码如下:
typedef int MyInt32; 

#ifdef _WIN64


typedef __int64 MySSizet; 
#else 
typedef int __w64 MySSizet; //Add __w64关键字
#endif 
void foo() { 
MyInt32 value32 = 10; 
MySSizet size = 20; 
value32 = size; //C4244 64-bitint assigned to 32-bit int

}
  这段代码编译完,我们会获得C4244的警告,帮助我们写出可移植到64位平台的代码。
  注意:/Wp6464位兼容模式下会被忽略,因为所有的类型已经有了确定的size,并且编译器会进行必要的检查。所以,我们即便禁用了/Wp64,也能在编译64位版本时获得C4244的警告。
  所以,切换到/Wp64帮助32位应用程序的开发者位64位编译器做准备。切换到/Wp64,构建64位代码时,会产生编译警告或错误。你需要修改它。
  另外,/Wp64VC2008里会被弃用,因为我们早该编译64位的应用程序,而不是为可移植到64位上做准备。
64-bit错误
  当我们谈论64位错误时,我们是指一段代码在32位下工作良好,但是编译成64位时却产生错误。64位错误常常以如下形式出现:
*错误的假定类型的大小(例如:假定指针的大小总是4字节)
*处理64位系统上超过2GB的大小的数组
*数据的读写时
*bit操作的代码
*复杂的地址计算
*过时的代码
  事实上,所有的错误都发生在当他重新编译为64位系统时。我们的课程目的是为了总结一些64位的错误,帮助你发现和消除他们。
64位错误的例子
  我们将用2个例子来帮助你弄明白64位的错误是什么。
  第一个底子是用魔术常量数字"4",来表示指针的大小(在64位的代码下是错误的)。注意这段代码在32位版本上工作良好,也不会有任何警告
size_t pointersCount= 100;
int **arrayOfPointers = (int **)malloc(pointersCount * 4)
  第二个例子是数据读取的机制。这段代码在32位版本上是正确的,但是64位的版本时,这段代码读32位应用程序保存的数据会失败
size_t PixelCount
fread(&PixelCount, sizeof(PixelCount), 1, inFile)
  接下来的课程我们会讨论更多的64位错误的类型和代码例子。不过人们常常会辩解说当移植代码到非64位架构上也会出现很多错误。
  是的,但是我们的目标不仅仅是学习可移植代码的问题。我们还将解决一些问题,来帮助开发者掌握64平台。
  当我们谈到64位错误时,我们是指这样一类代码:它在32位系统上是正确的,但是移植到64位处理器上却是错误的

检测64位错误的问题

有很多技术来检测程序代码里的错误。让我们来看看最常用的检测方法
Code Reviewer
  这个最古老,最可靠和最经得起检验的找错误的方法是code Review。这个方法依赖于让一些开发者一起阅读代码。不幸的是,这种方法不能应用于针对现代的巨大规模的程序的大规模的测试。
code review也许是个好的方法,能避免64位错误。但是这个方法很昂贵。
静态代码分析
  静态代码分析能帮助开发者减少code review的量和时间。相当多的人喜欢静态代码分析,并且提供大量的方法来检查潜在的问题和风险。静态代码分析的好处是它的可扩展性。你能在一个合理的时间段测试这个程序。并且静态代码分析可以帮助你侦测很多写代码时的错误。
  静态代码分析是侦测64位错误的最常用的方法。以后,当我们讨论64位错误时,我们将会告诉你怎样通过PVS-STUDIO里的Viva64来检查这些错误。下一课我们会学到更多的关于PVS-Studio的静态代码分析的技术
白盒方法
  通过白盒,我们能弄懂函数执行的各个不同代码分支。通过分析和测试我们能覆盖更多的代码。当然,白盒测试作为简单的调试引用程序的方法能找到一些错误。毕竟单步调试是很昂贵的(晕啊,这里的白盒测试居然是单步跟踪!)
黑盒方法(单元测试)
  黑盒方法会显得更好些。这里是指单元测试。单元测试的原则是为独立的单元和方法写一个测试集合,来检查他们的操作的主要模式。一些作者更喜欢单元测试是因为他依赖于程序组织结构的知识。但是我们认为函数测试和单元测试应该被认为是黑盒的,因为单元测试不应该重视函数内部的实现。当测试是在函数写之前就开发了的话,那么单元测试应该是提供一个关于函数的增量的保证。
  单元测试已经证明是很有效的。单元测试的一个优点是你可以独立于你的开发程序来检查程序发生的变化是不是正确的。这就可能让你的测试在很短的时间内跑完,这样开发可以发现错误后立刻修改它。如果一次跑所有的测试不可能,那么可以把长时间的测试分开来执行,例如在晚上跑。这样第二天就可以发现错误了。
  当用单元测试来检查64位错误时,你也许会遇到一些不愉快的事情。为了进行更快的测试,程序员会试图用少量的记录和数据。例如,当你为从一个数组里进行查找功能的函数,设计一个测试时,他不关心是有100项数据,还是10000000项数据。一百项数据也许足够,但是如果函数处理10000000时,他的速度就会极大的衰减。但是如果你想设计一个有效地测试,来检测你的函数在64位系统上是否正确时,你也许需要多余40亿的数据项。你设想这个函数用100项和几十亿项的区别。例如下面的例子:

boolFooFind(char *Array, char Value, size_t Size)



  for (unsigned i = 0; i != Size; ++i) 
    if (i % 5 == 0 &&Array == Value) 
      return true; 
  return false; 

#ifdef _WIN64 
  const size_t BufSize = 5368709120ui64; 
#else 
  const size_t BufSize = 5242880; 
#endif 
int _tmain(int, _TCHAR *) { 
  char *Array =  (char*)calloc(BufSize, sizeof(char)); 
  if (Array == NULL) 
    std::cout << "Errorallocate memory" << std::endl; 
  if (FooFind(Array, 33, BufSize)) 
    std::cout <<"Find" << std::endl; 
  free(Array); 
}  
  这个错误在于用循环计数使用了Unsigned。因此这个计数在64位系统上会溢出,产生死循环。
  看到这个例子时,你会发现如果你的程序在64位操作系统上处理大量的数据时,你是不能依靠过时的单元测试的。你必须在测试里补充大数据量的处理。
  不幸的是,写新的单元测试是不够的。我们虽然增加了处理大量数据的用例,但是它的执行时间也会变成。这样你的测试时间可能会增加到超过一天。这个也是我们在修改关于64位版本的测试时需要考虑的问题
手工测试
  这个方法被认为是开发的最后阶段。手工测试必须存在因为自动化测试不能检测所有错误。但是你不能完全依赖他。

静态代码分析来检查64位错误

静态代码分析
  静态代码分析是一项依赖于学习标记的代码片段来检查错误的技术。标记的代码片段通常包括一些特殊类型的错误。换句话说,一个静态代码检查工具检查的是有错误倾向或者有坏的格式的代码。这样的代码片段是开发者要学习和决定是否要修改的。
  静态分析器也许是通用的目的(例如,prefast pc lint, Parasoft c++test)和特殊的用于查找特定类型错误的目的(例如Chord来检查并行的java程序)。通常静态代码分析工具时相当昂贵的,并且要求你们学会如何使用他们。他们常常提供相当复杂和灵活的子系统。因为静态代码检查工具可能会被很多公司使用和定制规则,来提高软件的质量,因而他以复杂为代价,让开发者在早期阶段检查很多错误。静态代码分析还能帮助管理者更好的管理年轻的新手。
  静态代码分析的主要优点是减少了消除缺陷的代价。早期一个错误被检测,修复它的代价就会越低。因而根据《代码大全》里,在测试阶段修复错误的成本是设计阶段的5倍:

  因而静态代码检查工具减少了开发成本,因为他在设计代码阶段侦测除了很多错误。
静态分析来检查64位错误
  静态代码分析的优点如下:
1、你能检测整个代码。分析器能提供代码的接近完全的覆盖。它保证了你的代码在移植到64位代码以前,已经被检查过一次。
2、可扩展性。无论程序大小,静态代码分析都可以使用。你可以在开发者之间很容易的传播。
3、当刚开始开发一个工程时,开发者会忽略可能的问题,因为不了解64位的特性。分析器能指出危险的区域,并告诉你存在整个问题
4、错误修复的代价降低
5、无论你是移植代码到64位系统上,或者是开发一个64位的代码,你都可以有效的使用静态分析工具
PVS-StudioViva64分析器
PVS-Studio是一个的静态分析器的包,它包括一个特殊的静态分析器Viva64来检查64位错误.PVS-Studio分析器是用于windows平台的。它集成在VC2005\2008\2010里。PVS-Studio的接口允许你过滤警告,保存和加载警告条目

  分析器的系统要求和VC的要求是一致的:
*操作系统:Win7/Win2000/XP/2003/Vista/2008x86x64。注意你的操作系统不需要64位的。
*开发环境:VC2005/2008/2010

StandardEdition, Professional Edition, Team Systems

)。你必须有一个组件"x64 compliers and tools"已经安装了,才能测试64位程序。VC的版本里已经集成了,安装时可以选择安装它。
*硬件:PVS-Studio能在不少于1GB的系统上工作(推荐是2GB或者更多)。分析器能利用多核(核越多,则越快)

魔法数字

留意代码里的魔术数字,如果魔术数字是用来参与地址计算,或者大小,bit操作时,要特别留意

  下面的表列出常见的会影响可移植的魔术数字

Table 1 - The basic magic numbers which are dangerouswhen porting 32-bit applications to a 64-bit platform

小心检查你的代码,尽量用sizeof()或者<limits.h><inttypes.h>里的特殊值来取代你的魔术数字

下面是一些和魔法数字有关的错误例子,最常见的错误是写类型大小时,如下:

错误:

1. 1) size_t ArraySize = N * 4; 

2.    intptr_t *Array = (intptr_t *)malloc(ArraySize);

 正确:

·        1) size_t ArraySize = N * sizeof(intptr_t); 

·           intptr_t *Array = (intptr_t *)malloc(ArraySize);

·         

 

*********

 错误:

1. 2) size_t values[ARRAY_SIZE]; 

2.    memset(values, ARRAY_SIZE * 4, 0); 

正确: 

1. 2) size_t values[ARRAY_SIZE]; 

2.    memset(values, ARRAY_SIZE * sizeof(size_t), 0);

3.   

memset(values, sizeof(values), 0); //preferred alternative

*********

错误:

1. 3) size_t n, r; 

2.    n = n >> (32 - r); 

 正确:

1. 3) size_t n, r; 

2.    n = n >> (CHAR_BIT * sizeof(n) - r);  

有时我们需要一个特定的常量,例如:当我们用size_t类型的变量来填充低位的4个字节。在32位程序里,常常如下定义:

1. // constant '1111..110000' 

2. const size_t M = 0xFFFFFFF0u; 

64位系统上这就是错的。要发现类似的错误要花费大量的时间,而且不幸的是,没有其他途径可以查找和纠正这种类型的代码,除非用#ifdef或者一个特殊的宏

1. #ifdef _WIN64 

2.   #define CONST3264(a) (a##i64) 

3. #else 

4.   #define CONST3264(a)  (a) 

5. #endif 

6.  

7. const size_t M = ~CONST3264(0xFu); 

有时'-1'备用来表示错误码,或者特殊的标志,常常被写为"0xffffffff"。这种表达式在64位平台上是错误的,你应该显示的定义该数值位-1.下面就是一个错误的将0xffffffff作为错误码的代码例子:

1. #define INVALID_RESULT (0xFFFFFFFFu) 

2.  

3. size_t MyStrLen(const char *str) { 

4.   if (str == NULL) 

5.     return INVALID_RESULT; 

6.   ... 

7.   return n; 

8. 

9.  

10.size_t len = MyStrLen(str); 

11.if (len == (size_t)(-1)) 

12.  ShowError();

为什么是错的呢?因为"(size_t)(-1)"64位平台上,它的数值,会转换成大类型的有符号数。-10xffffffffffffffff,而不是0xffffffff;因而如果你要用宏的话,那么请这样使用:

1. #define INVALID_RESULT (size_t(-1)) 

2. ... 

0xffffffff有关的错误例子还有:

·        hFileMapping = CreateFileMapping ( 

·            (HANDLE) 0xFFFFFFFF, 

·            NULL, 

·            PAGE_READWRITE, 

·            (DWORD) 0, 

·            (DWORD) (szBufIm), 

·            (LPCTSTR) &FileShareNameMap[0]); 

 

这里0xffffffff也会导致64位系统的错误。

让我们来看下一个错误的0xffffffff例子。

·        void foo(void *ptr) 

·        

·          cout << ptr << endl; 

·        

·        int _tmain(int, _TCHAR *[]) 

·        

·          cout << "-1\t\t"; 

·          foo((void *)-1); 

·          cout << "0xFFFFFFFF\t"; 

·          foo((void *)0xFFFFFFFF); 

·        }

32位的结果

·        -1              FFFFFFFF 

·        0xFFFFFFFF      FFFFFFFF 

64的结果

·        -1              FFFFFFFFFFFFFFFF 

·        0xFFFFFFFF      00000000FFFFFFFF

 

变参的函数

64位上不正确的使用printfscanf的例子

例一:

·        const char *invalidFormat = "%u"; 

·        size_t value = SIZE_MAX; 

·        printf(invalidFormat, value); 

例二:

·        char buf[5]; 

·        sprintf(buf, "%p", pointer); 

  第一个例子里,64位平台上size_t不等价于unsigned类型,如果value的值大于UINT_MAX时,会打印错误

  第二个例子里,指针的大小可能比32位大。因而64位平台上它会导致溢出

  变参的函数被不正确使用是常见的错误,不仅仅是64位平台上才会出现。因此你可以尝试用cout代替printboost::formatstd::stringstream来代替sprintf。例如对于size_t

windows应用程序如下: 

1. size_t s = 1;  

2. printf("%Iu", s);

      linux应用程序如下:

1. size_t s = 1; 

2. printf("%zu", s);

同样在使用sccanf时,你也需要注意类似的关于size的使用问题。为了可移植,你的代码需要如下:

·        // PR_SIZET on Win64 = "I" 

·        // PR_SIZET on Win32 = "" 

·        // PR_SIZET on Linux64 = "z" 

·        // ... 

·        size_t u; 

·        scanf("%" PR_SIZET "u", &u); 



开始进行64Windows系统编程之前需要了解的所有信息

本文讨论:

64 位版本 Windows 的背景信息

适当地利用 x64 体系结构

使用 Visual C++ 2005 进行 x64 开发

针对 x64 版本的调试技术

本文使用以下技术:
Windows
Win64Visual Studio2005


本页内容

 

x64操作系统

 

适当利用 x64

 

使用 Visual C++进行 x64 开发

 

使代码与 Win64兼容

 

调试

 

关于托管代码

 

小结

使用 Windows® 先锋产品的乐趣之一是能够探究新技术以了解它的工作方式。实际上,我不太喜欢使用操作系统,直到对其内部结构有了一点深入了解之后。因此,当 Windows XP 64位版本和 Windows Server® 2003出现时,我简直快完蛋了。
Win64 x64 CPU体系结构的优点是:它们与其前任完全不同,但不需要很长的学习过程。尽管开发人员认为迁移到 x64只是一个重新编译的过程,但事实是我们仍然要在调试器中花费很多时间。拥有 OS CPU 的应用知识十分宝贵。
本文,我将本人在 Win64 x64 体系结构方面的经验归结为一个高手 Win32® 程序员迁移到 x64必备的几个要点。我假设您了解基本的 Win32概念、基本的 x86 概念以及为什么代码应该在 Win64上运行。这使我可以将关注的重点放在更重要的内容上。通过本概述,您可以在已经理解的 Win32 x86 体系结构基础上了解到一些重要差异。
有关 x64 系统的一个优点是:与基于 Itanium的系统不同,您可以在同一台计算机上使用 Win32 Win64,而不会导致严重的性能损失。此外,除了 Intel AMD x64 实现之间的几个模糊差异,与 x64 兼容的同一个 Windows 版本应该能够在这两个系统上运行。您不需要在 AMD x64系统上使用一个 Windows版本,在 Intel x64系统上使用另一个版本。
我将讨论分为三大领域:OS实现细节、适当地利用 x64 CPU体系结构以及使用 Visual C++® 进行 x64开发。
x64 操作系统
Windows 体系结构的所有概述中,我一般喜欢从内存和地址空间开始。尽管 64位处理器在理论上寻址 16 EB的内存 (264),但 Win64目前支持 16 TB(由 44位表示)。为什么不能在计算机中加载到 16 EB以使用全部 64 位呢?原因有很多。
对初级用户而言,当前的 x64 CPU通常只允许访问 40 位(1 TB)的物理内存。体系结构(不包括当前硬件)可以将其扩展到 52位(4 PB)。即使没有该限制,映射如此大内存的页表大小也是巨大的。
Win32 中一样,可寻址范围分为用户模式区和内核模式区。每个进程都在底部获得其唯一的 8 TB,而内核模式的代码存在于顶部的 8 TB中,并由所有进程共享。不同版本的 64 Windows 具有不同的物理内存限制,如 1  2 所示。
同样,与 Win32 中一样,x64 页大小为 4 KB。前 64 KB的地址空间始终不映射,因此您看到的最低有效地址应该是 0x10000。与在 Win32中不同,系统 DLL 在用户模式的地址范围顶部附近没有默认的加载地址。相反,它们在 4 GB内存以上加载,通常在 0x7FF00000000附近的地址上加载。
许多较新的 x64 处理器的一个出色功能是:支持 Windows用于实现硬件数据执行保护 (DEP) CPU No Execute位。x86 平台上存在许多错误和病毒,这是因为 CPU可以将数据当作合法代码字节执行。CPU在供数据存储使用的内存中执行从而可终止缓冲区溢出(有意或无意)。通过 DEPOS可以在有效代码区域周围设置更清晰的边界,从而使 CPU在执行超出这些预期边界时捕获到该事件。这推动着为使 Windows减少受到的攻击而付出的不懈努力。
在为捕获错误而设计的活动中,x64链接器将可执行文件默认的加载地址指定为在 32 (4 GB) 之上。这可以帮助在代码迁移到 Win64之后能够在现有代码中快速找到这些区域。具体说,如果将指针存储为一个 32位大小的值(如 DWORD),那么在 Win64版本中运行时,它将被有效地截断,从而导致指针无效,进而触发访问冲突。该技巧使查找这些令人讨厌的指针错误变得非常简单。
有关指针和 DWORD的主题将在 Win64类型系统中继续讨论。指针有多大?LONG怎么样?那么句柄(如 HWND)呢?幸好,Microsoft在进行从 Win16 Win32 的复杂转换时,使新的类型模型能够轻松地进一步扩展到 64位。一般地,除了个别几种情况外,新的 64位环境中的所有类型(除了指针和 size_t)均与 Win32中的完全相同。也就是说,64位指针是 8 字节,而 intlongDWORD HANDLE 仍然是 4 字节。在随后讨论进行 Win64开发时,我将讨论更多有关类型的内容。
Win64 的文件格式称为 PE32+。几乎从每个角度看,该格式在结构上都与 Win32 PE文件完全相同。只是扩展了少数几个字段(例如,头结构中的 ImageBase),删除了一个字段,并更改了一个字段以反映不同的 CPU类型。 3 显示已更改的字段。
PE 头之外,没有太多的更改。有几个结构(例如,IMAGE_LOAD_CONFIG IMAGE_THUNK_DATA)只是将某些字段扩展到 64 位。添加的 PDATA区段很有趣,因为它突出了 Win32 Win64 实现之间的一个主要差异:异常处理。
x86 环境中,异常处理是基于堆栈的。如果 Win32函数包含 try/catch try/finally代码,则编译器将发出在堆栈上创建小型数据块的指令。此外,每个 try数据块指向先前的 try数据结构,从而形成了一个链表,其中最新添加的结构位于表头。随着函数的调用和退出,该链表头会不断更新。如果发生异常,OS将遍历堆栈上的数据块链表,以查找相应的处理程序。我在 1997 1 月的 MSJ 文章中非常详细地描述了该过程,因此这里只做简要说明。
Win32 异常处理相比,Win64(包括 x64 Itanium 版本)使用了基于表的异常处理。它不会在堆栈上生成任何 try数据块链表。相反,每个 Win64可执行文件都包含一个运行时函数表。每个函数表项都包含函数的起始和终结地址,以及一组丰富数据(有关函数中异常处理代码)的位置和函数的堆栈帧布局。请参见 WINNT.H x64 SDK 中的IMAGE_RUNTIME_FUNCTION_ENTRY结构,了解这些结构的实质。
当异常发生时,OS 会遍历常规的线程堆栈。当堆栈审核遇到每个帧和保存的指令指针时,OS会确定该指令指针属于哪一个可执行的模块。随后,OS会在该模块中搜索运行时函数表,查找相应的运行时函数项,并根据这些数据制定适当的异常处理决策。
如果您是一位火箭科学家,并直接在内存中生成了代码而没有使用基本的 PE32+模块,该怎么办呢?这种情况也包含在内。Win64有一个 RtlAddFunctionTable API,它可让您告诉 OS有关动态生成的代码的信息。
基于表的异常处理的缺点(相对于基于堆栈的 x86模型)是:在代码地址中查找函数表项所需的时间比遍历链表的时间要长。但优点是:函数没有在每次执行时设置 try数据块的开销。
请记住,这只是一个简要介绍,而不是 x64异常处理的完整描述,但是很令人激动,不是吗?有关 x64异常模型的进一步概述,请参阅 Kevin Frei网络日记项
x64 兼容的 Windows 版本不包含最新 API 的具体数量;大部分新的 Win64 API都添加到针对 Itanium处理器的 Windows版本。简言之,现有的两个重要的 API,分别是 IsWow64Process GetNativeSystemInfo。它们允许 Win32应用程序确定是否在 Win64上运行,如果是,则可以看到系统的真正功能。否则,调用 GetSystemInfo 32 位进程只能看到 32 位系统的系统功能。例如,GetSystemInfo只会报告 32 位进程的地址范围。 4 显示的 API 以前在 x86上不可用,但可用于 x64
尽管运行完全的 64 Windows 系统听起来很不错,但事实是,在某些情况下,您很可能需要运行 Win32代码。为此,x64 版本的 Windows 包含 WOW64 子系统,以允许 Win32 Win64 进程在同一个系统上并行运行。但是,将 32 DLL 载入 64 位进程(反之亦然)则不受支持。(相信我,这是件好事。)您终于可以向 16位旧式代码吻别了!
x64 版本的 Windows 中,从 64 位可执行文件启动的进程(如 Explorer.exe)只能加载 Win64 DLL,而从 32 位可执行文件启动的进程只能加载 Win32 DLL。当 Win32进程调用内核模式(例如,读取文件)时,WOW64代码会安静地截断该调用,并在适当的位置调用正确的 x64等效代码。
当然,不同系统(32 位与 64 位)的进程需要能够互相通信。幸运的是,Win32中您知道并喜爱的所有常规进程间通信机制也可以在 Win64中工作,包括共享内存、命名管道以及命名同步对象。
您可能在想,"那么系统目录呢?同一个目录不能同时保存 32位和 64 位版本的系统 DLL(例如,KERNEL32 USER32),不是吗"?通过执行可选择的文件系统重定向,WOW64魔法般地为您解决了这个问题。来自 Win32进程的文件活动通常转到 System32目录,而不是在名为 SysWow64的目录中。在内部,WOW64会默默地更改这些请求以指向 SysWow64目录。Win64 系统实际上有两个 \Windows\System32目录 - 一个用于 x64 二进制文件,另一个用于 Win32等效文件。
这看上去没什么,但会令人混淆。例如,我在某一点上使用了 32位命令行提示(我自己并不知道)。当我针对 System32目录中的 Kernel32.dll运行 DIR 时,所得到的结果与我在 SysWow64目录中执行相同操作后所得到的结果完全相同。我绞尽脑汁后才发现,文件系统重定向的工作方式就是这样。也就是说,即使我认为是在 \Windows\System32目录中工作,但 WOW64实际上已将调用重定向到 SysWow64目录。顺便说一下,如果您确实希望从 x64应用程序访问 32 \Windows\System32目录,则GetSystemWow64Directory API会提供正确的路径。请一定阅读 MSDN® 文档,了解完整的信息。
除了文件系统重定向之外,WOW64施加的另一个小魔法是注册表重定向。请考虑我前面提到的 Win32 DLL不能载入 Win64 进程的内容,然后再考虑一下 COM 及其使用注册表加载进程内服务器 DLL的情况。如果 64 位应用程序要使用 CoCreateInstance创建一个在 Win32 DLL中实现的对象,该怎么办呢?该 DLL不能加载,对吗?WOW64通过将来自 32 位应用程序的访问重定向到\Software\Classes(以及相关的)注册节点,再一次节省了时间。实际结果是,Win32应用程序的注册表视图与 x64应用程序的不同(但大部分是相同的)。如您所料,OS通过在调用 RegOpenKey及友元时指定新的标记值,为 32位应用程序提供了一个读取实际 64位注册表值的应急方法。
更进一步说,后几个正中我下怀的 OS差异涉及线程的局部数据。在 x86版本的 Windows中,FS 寄存器用于指向每个线程的内存区域,包括"最后一个错误"和线程的本地存储(分别是 GetLastError TlsGetValue)。在 x64版本的 Windows中,FS 寄存器由 GS 寄存器取代。另外,它们的工作方式几乎完全相同。
虽然本文主要从用户模式角度讨论 x64,但有一项重要的内核模式体系结构附加内容需要说明。针对 x64 Windows 中有一项称为 PatchGuard 的新技术,该技术主要针对安全性和健壮性。简言之,能够更改关键内核数据结构(例如,系统调用表和中断调度表 (IDT))的用户模式程序或驱动程序会导致安全漏洞和潜在的稳定性问题。对于 x64体系结构而言,Windows家族决定不允许以不受支持的方式修改核心内存。强制该操作的技术是 PatchGuard。它使用内核模式线程监视对关键核心内存位置的更改。如果该内存被更改,则错误检测时系统将停止。
总之,如果您熟悉 Win32体系结构,并且了解如何编写在它上面运行的本机代码,那么在迁移到 Win64的过程中您就不会感到很惊奇了。您可以在很大程度上将其视为一个更广阔的环境。
返回页首
适当利用 x64
现在,我们看一下 CPU体系结构本身,因为对 CPU指令集有一个基本的了解可以使开发(特别是调试)工作更轻松。在编译器生成的 x64代码中,您将注意到的第一件事是,它与您了解并喜爱的 x86代码是多么地相似。这对于了解 Intel IA64编码的人们则完全不同。
随后您将注意到的第二件事是,注册名称与您所熟悉的略有不同,并且有很多名称。通用 x64寄存器的名称以 R 开头,如 RAXRBX等等。这是针对 32 x86 寄存器的基于 E的旧命名方案的发展演化。就像过去一样,16 AX 寄存器变为 32 EAX16 BX 变为 32 EBX,以此类推。如果从 32位版本转换,所有 E 寄存器都会变为其 64 位形态的 R 寄存器。因此,RAX EAX 的继承者,RBX 超越 EBXRSI取代 ESI,以此类推。
此外,还添加了 8 个新的通用寄存器 (R8-R15)。主要的 64位通用寄存器清单如 5 所示。
此外,32 EIP 寄存器也会变为 RIP寄存器。当然,32 位指令必须继续执行,以便这些寄存器(EAXAXALAH等)的原始、较小类型的版本仍然可用。
为了照顾到图形和科学编程人员,x64 CPU还有 16 128 SSE2寄存器,分别以 XMM0 XMM15 命名。由 Windows 保存的 x64 寄存器的完整集合位于 WINNT.H中定义的相应 #ifdef'ed_CONTEXT结构中。
在任何时候,x64 CPU不是以旧式的 32 位模式操作,就是以 64 位模式操作。在 32 位模式中,CPU 与任何其他 x86类别的 CPU 一样对指令进行解码和操作。在 64 位模式中,CPU 对某些指令编码进行了少量调整,以支持新的寄存器和指令。
如果您熟悉 CPU 操作码编码模型,就会记得为新的指令编码提供的空间会很快消失,并且在 8个新寄存器中挤出空间也不是一项轻松的任务。为此,一种方法是删除一些极少使用的指令。到目前为止,我留下的指令只有 64位版本的 PUSHAD POPAD,它们用于在堆栈上保存和恢复所有通用寄存器。释放指令编码空间的另一种方法是,在 64位模式中完全消除区段。这样,CSDSESSSFS GS 的生命周期就结束了。没有太多人会想念它们的。
由于地址是 64 位的,您可能会担心代码大小。例如,下面是一个常见的 32位指令:
[pre]
CALL DWORD PTR[XXXXXXXX][/pre]这里,用 X表示的部分是一个 32位地址。在 64 位模式中,这会变为 64 位地址,从而将 5 字节的指令变为 9字节吗?幸运的是,答案是""。指令大小保持不变。在 64位模式中,指令的 32位操作数部分被视为相对于当前指令的数据偏移。一个示例可以更清楚地说明这一点。在 32位模式中,以下是调用地址 00020000h中存储的 32 位指针值的指令:
[pre]
00401000: CALLDWORD PTR [00020000h][/pre] 64位模式中,相同的操作码字节调用地址 00421000h (4010000h + 20000h)中存储的 64 位指针值。这可以使您联想到,如果是自己生成代码,则这种相对寻址模式会造成重大分歧。您不能仅在指令中指定 8字节的指针值,而是需要为实际 64位目标地址驻留的内存位置指定一个 32位相对地址。因而,有一个未提出的假设是:64位目标指针必须在使用它的指令的 2GB空间中。对大多数人而言,这并不是一个大问题,但如果您要生成动态代码或者修改内存中的现有代码,就会出现问题!
所有 x64 寄存器的一个主要优势是,编译器能够最终生成在寄存器中(而非堆栈上)传递大部分参数的代码。将参数推入堆栈会引发内存访问。我们都需要牢记,在 CPU缓存中找不到的内存访问会导致 CPU延迟许多个周期,以等待可用的常规 RAM内存。
在设计调用约定时,x64体系结构利用机会清除了现有 Win32调用约定(如 __stdcall__cdecl__fastcall_thiscall等)的混乱。在 Win64中,只有一个本机调用约定和 __cdecl之类的修饰符被编译器忽略。除此之外,减少调用约定行为还为可调试性带来了好处。
您需要了解的有关 x64调用约定的主要内容是:它与 x86 fastcall约定的相似之处。使用 x64约定,会将前 4 个整数参数(从左至右)传入指定的 64位寄存器:
[pre]
RCX: 1st integerargumentRDX: 2nd integer argumentR8: 3rd integer argumentR9: 4th integerargument[/pre] 4个以外的整数参数将传递到堆栈。该指针被视为整数参数,因此始终位于 RCX寄存器内。对于浮点参数,前 4个参数将传入 XMM0 XMM3 的寄存器,后续的浮点参数将放置到线程堆栈上。
更进一步探究调用约定,即使参数可以传入寄存器,编译器仍然可以通过消耗 RSP寄存器在堆栈上为其预留空间。至少,每个函数必须在堆栈上预留 32个字节(4 64 位值)。该空间允许将传入函数的寄存器轻松地复制到已知的堆栈位置。不要求被调用函数将输入寄存器参数溢出至堆栈,但需要时,堆栈空间预留确保它可以这样做。当然,如果要传递 4个以上的整数参数,则必须预留相应的额外堆栈空间。
让我们看一个示例。请考虑一个将两个整数参数传递给子函数的函数。编译器不仅会将值赋给 RCX RDX,还会从 RSP堆栈指针寄存器中减去 32个字节。在被调用函数中,可以在寄存器(RCX RDX)中访问参数。如果被调用代码因其他目的而需要寄存器,可将寄存器复制到预留的 32字节堆栈区域中。图 6显示在传递 6 个整数参数之后的寄存器和堆栈。

6 传递整数



x64
系统上的参数堆栈清除比较有趣。从技术上说,调用方(而非被调用方)负责清除堆栈。但是,您很少看到在起始代码和结束代码之外的位置调整 RSP。与通过 PUSH POP 指令在堆栈中显式添加和移除参数的 x86编译器不同,x64 代码生成器会预留足够的堆栈空间,以调用最大目标函数(参数方法)所使用的任何内容。随后,在调用子函数时,它重复使用相同的堆栈区域来设置参数。
另一方面,RSP 很少更改。这与 x86 代码大不相同,在 x86 代码中,ESP 值随着参数在堆栈中的添加和移除而不断变化。
有一个示例可帮助说明这一点。请考虑一个调用三个其他函数的 x64函数。第一个函数接受 4个参数(0x20 个字节),第二个接受 12 个参数(0x60 个字节),第三个接受 8个参数(0x40 个字节)。在起始代码中,生成的代码只需在堆栈上预留 0x60个字节,并将参数值复制到 0x60字节中的适当位置,以便目标函数能够找到它们。
您可以在 Raymond Chen的网络日记中看到一个有关 x64调用约定更详细的描述。我不会过多地讨论所有细节,仅在这里强调一些要点。首先,小于 64位的整数参数进行了符号扩展,然后仍然通过相应的寄存器传递(如果在前 4个整数参数内)。其次,任何参数所处的堆栈位置都应该是 8字节的倍数,从而保持 64位对齐。不是 124 8 字节的任何参数(包括结构)都是通过引用传递的。最后,81632 64 位的结构和联合作为相同长度的整数传递。
函数的返回值存储在 RAX寄存器中。如果返回到 XMM0中的是浮点类型,就会引发异常。在所有调用中,以下寄存器必须保留:RBXRBPRDIRSIR12R13R14 R15。以下寄存器不稳定,可能会被毁坏:RAXRCXRDXR8R9R10 R11
我在前面提到过,作为异常处理机制的一部分,OS会遍历堆栈帧。如果您曾经编写过堆栈遍历代码,就会知道 Win32帧布局的这一特性可巧妙处理该过程。这种情况在 x64系统上要好得多。如果某个函数需要分配堆栈空间,调用其他函数,保留任何寄存器或者使用异常处理,则该函数必须使用一组定义良好的指令来生成标准的起始代码和结束代码。
实行创建函数堆栈帧的标准方法是 OS确保(在理论上)能够始终遍历堆栈的一种方法。除了一致、标准的起始代码,编译器和链接器还必须创建关联的函数表数据项。奇怪的是,所有这些函数项都在IMAGE_FUNCTION_ENTRY64的数组表(在 winnt.h中定义)中结束。如何找到这个表呢?它由 PE头的 DataDirectory字段中的 IMAGE_DIRECTORY_ENTRY_EXCEPTION项指出。
我在短短的一段中讨论了许多体系结构内容。但是,通过大体了解这些概念以及 32位程序集语言的现有知识,您应该能够在一段较短的时间内了解调试器中的 x64指令。总是实践出真知。
返回页首
使用 Visual C++进行 x64 开发
尽管可以使用 Visual Studio® 2005之前的 Microsoft® C++编译器编写 x64 代码,但这在 IDE 中是一项沉闷的体验。因此,在本文中,我假定您使用的是 Visual Studio 2005,并选择在默认安装中未启用的 x64工具。我还假定您在 C++中拥有要为 x86 x64 平台构建的现有 Win32用户模式项目。
针对 x64 构建的第一步是创建 64 位生成配置。作为一个优秀的 Visual Studio用户,您应该已经知道项目在默认情况下有两种配置:Debug Retail。这里,您只需创建另外两个配置:x64形态下的 Debug Retail
首先,加载现有项目/解决方案。在 Build菜单上,选择 Configuration Manager。在 ConfigurationManager对话框中,从 Active solution platform下拉菜单中选择 New(参见图 7)。现在,您应该看到另一个标题为 New Solution Platform 的对话框。

7 创建新的生成配置



选择 x64 作为您的新平台(参见图 8),并将另一个配置保留为默认状态;然后单击 OK。就这么简单!现在,您应该拥有四个可能的生成配置:Win32 DebugWin32 Retailx64 Debug x64 Retail。使用 ConfigurationManager,您可以轻松地在它们之间切换。
现在,我们看一下您的代码与 x64的兼容性。将 x64 Debug配置设为默认值,然后生成项目。除非代码不重要,否则可能会收到一些不会在 Win32配置中发生的编译器错误。除非您已经完全摒弃了编写可移植 C++代码的所有原则,否则修正这些问题以使代码能够随时用于 Win32 x64 相对比较轻松,而无需大量的条件编译代码。

8 选择生成平台



返回页首
使代码与 Win64 兼容
Win32 代码转换为 x64,所需的最重要的工作可能是确保类型定义正确。还记得先前讨论的 Win64类型系统吗?通过使用 Windows typedef类型而非 C++编译器的本机类型(intlong等),Windows 头使得编写干净的 Win32 x64代码很轻松。您应该在自己的代码中继续保持这一点。例如,如果 Windows将一个 HWND 传递给您,请不要仅仅为了方便就将其存储在 FARPROC中。
升级完许多代码之后,我看到的最常见而简单的错误可能就是:假定指针值可以存储或传递到 32位类型(如 int long)甚至 DWORD中。Win32 Win64 中的指针长度视需要而不同,而整数类型长度保持不变。但是,让编译器不允许指针存储在整数类型中也是不现实的。这是一个根深蒂固的 C++习惯。
解救方法是 Windows头中定义的 _PTR 类型。DWORD_PTRINT_PTR LONG_PTR 之类的类型可让您声明整数类型的变量,并且这些变量始终足够长以便在目标平台上存储指针。例如,定义为 DWORD_PTR类型的变量在针对 Win32编译时是 32 位整数,在针对 Win64 编译时是 64 位整数。经过实践,我已经习惯了声明类型以询问"这里是否需要 DWORD或者实际是指 DWORD_PTR吗?"
正如您期望的,可能有机会明确指定整数类型需要多少字节。定义 DWORD_PTR及其友元的同一头文件 (Basetsd.h)还可以定义特定长度的整数,如 INT32INT64INT16UINT32 DWORD64
与类型大小差异相关的另一个问题是 printf sprintf 格式化。我对于在过去使用 %X %08X 格式化指针值感到懊悔万分,并且在 x64系统上运行该代码时还遇到了阻碍。正确的方法是使用 %p%p可以在目标平台上自动考虑指针大小。此外,对于与大小相关的类型,printf sprintf 还具有 I 前缀。例如,您可能使用 %Iu来打印 UINT_PTR变量。同样,如果您知道该变量始终是 64位标记值,则可以使用 %I64d
在清除了无法用于 Win64的类型定义所导致的错误之后,可能还有只能在 x86模式下运行的代码。或者,您可能需要编写函数的两个版本,一个用于 Win32,另一个用于 x64。这就是一组预处理器宏的用武之地:
[pre]
_M_IX86_M_AMD64_WIN64[/pre]正确使用预处理器宏对于编写正确的跨平台代码而言至关重要。_M_IX86 _M_AMD64 仅在针对特定处理器编译时进行定义。_WIN64在针对任何 64 位版本的 Windows(包括 Itanium版)编译时定义。
在使用预处理器宏时,请仔细考虑您的需要。例如,只需要代码真正特定于 x64处理器,没有别的需要了吗?然后,使用与以下类似的代码:
[pre]
#ifdef _M_AMD64[/pre]另一方面,如果同一代码既可以在 x64又可以在 Itanium上工作,则使用如下所示的代码可能更好:
[pre]
#ifdef _WIN64[/pre]我发现一个有用的习惯是:只要使用其中一个宏,就始终显式创建 #else情况,以便提前知道是否忘记了某些情况。请考虑以下编写错误的代码:
[pre]
#ifdef_M_AMD64// My x64 code here#else// My x86 code here#endif[/pre]如果现在针对第三个 CPU体系结构编译该代码,会发生什么情况?系统将无意识地编译我的 x86代码。上面代码的一个更好的表达方式如下:
[pre]
#ifdef_M_AMD64// My x64 code here#elif defined (_M_IX86)// My x86 codehere#else#error !!! Need to write code for this architecture#endif[/pre]在我的 Win32 代码中无法轻松移植到 x64的一部分代码是内联汇编,Visual C++不支持它的 x64 目标。不要害怕,汇编有办法。它提供了一个 64 MASM (ML64.exe),这在 MSDN中有所说明。ML64.exe和其他 x64 工具(包括 CL.EXE LINK.EXE)可以从命令行调用。您可以只运行 VCVARS64.BAT文件,该文件可以将它们添加到您的路径中。
返回页首
调试
最后,您需要在 Win32 x64 版本上干净地编译代码。最后一个难题是运行和调试代码。无论是否在 x64盒上生成 x64 版本,您都需要使用 Visual Studio远程调试功能在 x64模式下进行调试。幸运的是,如果您在 64位计算机上运行 Visual StudioIDE,则 IDE将为您执行以下所有步骤。如果您出于某些原因无法使用远程调试,则另一个选项是使用 x64版本的WinDbg。但是,您会失去 Visual Studio调试器提供的许多调试优势。
如果您从未使用过远程调试,也不需要过于担心。一旦设置好,远程调试就可以像在本地一样无缝使用。
第一步是在目标计算机上安装 64 MSVSMON。这通常是通过运行 Visual Studio随附的 RdbgSetup程序来完成的。一旦 MSVSMON运行,请使用 Tools菜单为 32 Visual Studio MSVSMON 实例之间的连接配置适当的安全设置(或者缺失)。
接下来,您需要在 Visual Studio中将项目配置为针对 x64代码使用远程调试,而不是尝试进行本地调试。您可以从调试项目的属性开始启动这个过程(参见图 9)。

9 调试属性



确定 64 位配置是当前配置,然后选择 ConfigurationProperties下面的 Debugging。靠近顶端是标题为 Debugger tolaunch的下拉菜单。通常,它设置为 Local Windows Debugger。将其更改为 Remote WindowsDebugger。在下面,您可以指定在启动调试时要执行的远程命令(例如,程序名),以及远程计算机名和连接类型。
如果您正确设置了所有内容,就可以使用与启动 Win32应用程序相同的方式开始调试 x64目标应用程序。您可以知道是否已经成功连接到 MSVSMON,因为每次调试器成功连接后,MSVSMON的跟踪窗口都会显示一个"connected"字符串。在这里,通常都是您知道并喜爱的同一个 Visual Studio调试器。确保屏幕显示寄存器窗口,并查看所有这些出色的 64位寄存器,然后转到反汇编窗口以查看"非常熟悉但略有不同的"x64程序集代码。
请注意,不能将 64 位小型转储直接加载到 Visual Studio之类的 32 位转储中,而是需要使用远程调试。此外,Visual Studio2005目前不支持本机 64 位代码和托管 64 位代码之间的互操作调试。
返回页首
关于托管代码
使用 Microsoft .NET Framework进行编码的一个优势是,大部分基础操作系统都归纳为通用代码。此外,IL指令格式是 CPU 不可知的。因此,从理论上说,在 Win32系统上生成的基于 .NET的程序二进制文件应该无需修改就可以在 x64系统上运行。但实际情况却有一点复杂。
.NET Framework2.0 提供了 x64版本。在 x64 计算机上安装 .NET Framework 2.0之后,我能够运行先前在 Win32环境中运行的 .NET可执行文件。这真棒!当然,虽然不能保证每个基于 .NET的程序无需重新编译就可以在 Win32 x64 上都运行良好,但它确实在一段合理的时间内"很有用"
如果您的托管代码显式调用本机代码(例如,通过 C# Visual Basic® 中的平台调用),则在尝试针对 64 CLR 运行时可能会遇到问题。但是,有一个编译器开关 (/platform)可让您更清楚地了解代码应该在哪个平台上运行。例如,您可能希望托管代码在 WOW64中运行,即使可以使用 64 CLR
返回页首
小结
总之,对于我而言,迁移到 x64版本的 Windows是一个相对比较轻松的经历。一旦您很好地掌握了 OS体系结构和工具中相对较小的差异,就可以轻松地使一个代码基在这两个平台上运行。Visual Studio2005可从根本上使这些工作更加轻松。此外,由于每天都会出现更多特定于 x64版本的设备驱动程序和工具(如SysInternals.com提供的 Process Explorer),因此没有理由不进行讨论!
MattPietrek 与他人合著有几本关于 Windows系统级编程的书籍,以及 MSDNMagazine Under the Hood专栏。他以前曾是 NuMega/Compuware BoundsChecker系列产品的首席架构师。现在,他是 Microsoft Visual Studio小组的一员。

 

 

 

 

使用#if defined()组成复杂的预编译控制指令

分类: 编译 c语言 c++语言 编程思想2013-10-12 17:49 733人阅读 评论(0) 收藏 举报

动态配置

目录(?)[+]

问题描述:

想使用互斥的宏定义控制源代码中该编译哪一段代码,该如何使用预编译伪指令来控制呢?

比如,对于通用型的源码来说,为了适应不同的设备配置,需要使用不同的配置文件,

对于combo,使用cfg_file ="/usr/local/combo.ini";

对于pdu,使用cfg_file ="/usr/local/pdu.ini";

解决办法:

1. 综合运用#if#defined()#elif#else#endif来组成复杂的编译控制;

#ifdefined(COMBO_ENABLE) && !defined(PDU_ENABLE)

/* COMBOconfiguration */

#elif defined(PDU_ENABLE) &&!defined(COMBO_ENABLE)

/* PDUconfiguration */

#else

/* COMBOconfiguration is used */

#endif

2. #ifdef只能判断单一的宏是否定义,而#if defined()可以组成复杂的判别条件;

对于单一的宏AAA来说,#ifdef AAA#if defined(AAA)是完全相同的。

而要组成复杂的判别条件,用#if defined()就灵活方便了,比如:#if defined(AAA)&& (BBB >= 10)

如果改用#ifdef则没法表示条件BBB>=10了。

 

 

 

关于Windows与Linux下32位与64位开发中的数据类型长度的一点汇总

分类: Linux windows 数据类型 c语言 c++语言2013-07-04 10:36 975人阅读 评论(0) 收藏 举报

数据类型

32位与64位的数据类型长度是不一样的,而且windowslinux也有些许区别,下面把64位下的数据长度列表如下(无符号unsigned和有符号的长度一样):

 

                  linux64            windows64       linux32         windows32

char             8                       8                      8                     8

short           16                      16                   16                    16

int               32                      32                    32                  32

long             64                      32                    32                   32 

longlong     64                     64                   64                    64

size_t          64                     64                   32                     32

 

 

需要注意的是long类型在win64下是32位,千万别搞错了。size_t类型随着CPU位数变化,也要注意。

总结:没事不要用longsize_t是系统类型,使用时心里要留意,不能认为它和unsigned int等同,否则当你想要以64位编译时warning就铺天盖地了。

 

 

 

 

#ifdef __cplusplus有什么作用

分类: C/C++积累2012-03-16 22:29 3797人阅读 评论(0) 收藏 举报

c编译器语言汇编modulegcc

百度知道:

一般用于将C++代码以标准C形式输出(即以C的形式被调用),这是因为C++虽然常被认为是C的超集,但是C++的编译器还是与C的编译器不同的。C中调用C++中的代码这样定义会是安全的。

 

一般的考虑跨平台使用方法如下:

 

#ifdefined(__cplusplus)||defined(c_plusplus)//跨平台定义方法

extern"C"{

#endif

//... 正常的声明段

#ifdefined(__cplusplus)||defined(c_plusplus)

}

#endif

 

简单的用在windows下可以如下定义:

#ifdef  __cplusplus

extern"C"{

//... 正常的声明段

}

#endif

 

 

某一网文:

 

#ifdef__cplusplus是什么意思?

时常在cpp的代码之中看到这样的代码:

#ifdef__cplusplus

extern"C"{

#endif

//一段代码

#ifdef__cplusplus

}

#endif

这样的代码到底是什么意思呢?

首先,__cplusplus是cpp中的自定义宏,那么定义了这个宏的话表示这是一段cpp的代码,也就是说,上面的代码的含义是:

    如果这是一段cpp的代码,那么加入extern"C"{和}处理其中的代码。

 

    要明白为何使用extern"C",还得从cpp中对函数的重载处理开始说起。在c++中,为了支持重载机制,在编译生成的汇编码中,要对函数的名字进行一些处理,加入比如函数的返回类型等等.而在C中,只是简单的函数名字而已,不会加入其他的信息.也就是说:C++和C对产生的函数名字的处理是不一样的.

比如下面的一段简单的函数,我们看看加入和不加入extern"C"产生的汇编代码都有哪

些变化:

 

intf(void)

{

return1;

}

在加入extern"C"的时候产生的汇编代码是:

.file"test.cxx"

.text

.align2

.globl_f

.def_f;.scl2;.type32;.endef

_f:

pushl%ebp

movl%esp,%ebp

movl$1,%eax

popl%ebp

ret

但是不加入了extern"C"之后

.file"test.cxx"

.text

.align2

.globl__Z1fv

.def__Z1fv;.scl2;.type32;.endef

__Z1fv:

pushl%ebp

movl%esp,%ebp

movl$1,%eax

popl%ebp

ret

两段汇编代码同样都是使用gcc-S命令产生的,所有的地方都是一样的,唯独是产生的

函数名,一个是_f,一个是__Z1fv。

明白了加入与不加入extern"C"之后对函数名称产生的影响,我们继续我们的讨论:为什

么需要使用extern"C"呢?C++之父在设计C++之时,考虑到当时已经存在了大量的C代码,

为了支持原来的C代码和已经写好C库,需要在C++中尽可能的支持C,而extern"C"就是

其中的一个策略。

试想这样的情况:一个库文件已经用C写好了而且运行得很良好,这个时候我们需要使

用这个库文件,但是我们需要使用C++来写这个新的代码。如果这个代码使用的是C++的

方式链接这个C库文件的话,那么就会出现链接错误.我们来看一段代码:首先,我们使用C

的处理方式来写一个函数,也就是说假设这个函数当时是用C写成的:

//f1.c

extern"C"

{

voidf1()

{

return;

}

}

编译命令是:gcc-cf1.c-of1.o 产生了一个叫f1.o的库文件。再写一段代码调用这个f1

函数:

//test.cxx

//这个extern表示f1函数在别的地方定义,这样可以通过

//编译,但是链接的时候还是需要

//链接上原来的库文件.

externvoidf1();

intmain()

{

f1();

return0;

}

通过gcc-ctest.cxx-otest.o产生一个叫test.o的文件。然后,我们使用gcctest.of1.o来

链接两个文件,可是出错了,错误的提示是:

test.o(.text+0x1f):test.cxx:undefinereferenceto'f1()'

也就是说,在编译test.cxx的时候编译器是使用C++的方式来处理f1()函数的,但是实

际上链接的库文件却是用C的方式来处理函数的,所以就会出现链接过不去的错误:因为链

接器找不到函数。

因此,为了在C++代码中调用用C写成的库文件,就需要用extern"C"来告诉编译器:

这是一个用C写成的库文件,请用C的方式来链接它们。

比如,现在我们有了一个C库文件,它的头文件是f.h,产生的lib文件是f.lib,那么我

们如果要在C++中使用这个库文件,我们需要这样写:

extern"C"

{

#include"f.h"

}

回到上面的问题,如果要改正链接错误,我们需要这样子改写test.cxx:

extern"C"

{

externvoidf1();

}

intmain()

{

f1();

return0;

}

重新编译并且链接就可以过去了.

总结

C和C++对函数的处理方式是不同的.extern"C"是使C++能够调用C写作的库文件的一

个手段,如果要对编译器提示使用C的方式来处理函数的话,那么就要使用extern"C"来说

明。 

另一篇文章:

 

#ifdef__cplusplus

#ifdef__cplusplus倒底是什么意思?

时常在cpp的代码之中看到这样的代码:

#ifdef__cplusplus

extern"C" {

#endif

//一段代码

#ifdef__cplusplus

}

#endif

  这样的代码到底是什么意思呢?首先,__cplusplus是cpp中的自定义宏,那么定义了这个宏的话表示这是一段cpp的代码,也就是说,上面的代码的含义是:如果这是一段cpp的代码,那么加入extern"C"{和}处理其中的代码。

  要明白为何使用extern"C",还得从cpp中对函数的重载处理开始说起。在c++中,为了支持重载机制,在编译生成的汇编码中,要对函数的名字进行一些处理,加入比如函数的返回类型等等.而在C中,只是简单的函数名字而已,不会加入其他的信息.也就是说:C++和C对产生的函数名字的处理是不一样的.

  比如下面的一段简单的函数,我们看看加入和不加入extern "C"产生的汇编代码都有哪些变化:

int f(void)

{

return 1;

}

  在加入extern"C"的时候产生的汇编代码是:

.file"test.cxx"

.text

.align 2

.globl _f

.def _f; .scl 2;.type 32; .endef

_f:

pushl %ebp

movl %esp, %ebp

movl $1, %eax

popl %ebp

ret

  但是不加入了extern"C"之后

.file"test.cxx"

.text

.align 2

.globl __Z1fv

.def __Z1fv; .scl2; .type 32; .endef

__Z1fv:

pushl %ebp

movl %esp, %ebp

movl $1, %eax

popl %ebp

ret

  两段汇编代码同样都是使用gcc -S命令产生的,所有的地方都是一样的,唯独是产生的函数名,一个是_f,一个是__Z1fv。

  明白了加入与不加入extern"C"之后对函数名称产生的影响,我们继续我们的讨论:为什么需要使用extern "C"呢?C++之父在设计C++之时,考虑到当时已经存在了大量的C代码,为了支持原来的C代码和已经写好C库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。

  试想这样的情况:一个库文件已经用C写好了而且运行得很良好,这个时候我们需要使用这个库文件,但是我们需要使用C++来写这个新的代码。如果这个代码使用的是C++的方式链接这个C库文件的话,那么就会出现链接错误.我们来看一段代码:首先,我们使用C的处理方式来写一个函数,也就是说假设这个函数当时是用C写成的:

//f1.c

extern"C"

{

void f1()

{

return;

}

}

  编译命令是:gcc -c f1.c -of1.o 产生了一个叫f1.o的库文件。再写一段代码调用这个f1函数:

// test.cxx

//这个extern表示f1函数在别的地方定义,这样可以通过

//编译,但是链接的时候还是需要

//链接上原来的库文件.

extern void f1();

int main()

{

f1();

return 0;

}

  通过gcc -c test.cxx -otest.o 产生一个叫test.o的文件。然后,我们使用gcctest.o f1.o来链接两个文件,可是出错了,错误的提示是:

test.o(.text +0x1f):test.cxx: undefine reference to 'f1()'

  也就是说,在编译test.cxx的时候编译器是使用C++的方式来处理f1()函数的,但是实际上链接的库文件却是用C的方式来处理函数的,所以就会出现链接过不去的错误:因为链接器找不到函数。

  因此,为了在C++代码中调用用C写成的库文件,就需要用extern "C"来告诉编译器:这是一个用C写成的库文件,请用C的方式来链接它们。

  比如,现在我们有了一个C库文件,它的头文件是f.h,产生的lib文件是f.lib,那么我们如果要在C++中使用这个库文件,我们需要这样写:

extern"C"

{

#include"f.h"

}

  回到上面的问题,如果要改正链接错误,我们需要这样子改写test.cxx:

extern"C"

{

extern void f1();

}

int main()

{

f1();

return 0;

}

  重新编译并且链接就可以过去了.

  总结

C和C++对函数的处理方式是不同的.extern"C"是使C++能够调用C写作的库文件的一个手段,如果要对编译器提示使用C的方式来处理函数的话,那么就要使用extern "C"来说明。

1.引言

C++语言的创建初衷是“a better C”,但是这并不意味着C++中类似C语言的全局变量和函数所采用的编译和连接方式与C语言完全相同。作为一种欲与C兼容的语言,C++保留了一部分过程式语言的特点(被世人称为“不彻底地面向对象”),因而它可以定义不属于任何类的全局变量和函数。但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C有明显的不同。

2.从标准头文件说起

某企业曾经给出如下的一道面试题:

面试题

为什么标准头文件都有类似以下的结构?

#ifndef__INCvxWorksh

#define__INCvxWorksh

#ifdef__cplusplus

extern"C" {

#endif

#ifdef__cplusplus

}

#endif

#endif

分析

显然,头文件中的编译宏“#ifndef__INCvxWorksh、#define __INCvxWorksh、#endif” 的作用是防止该头文件被重复引用。

那么

#ifdef__cplusplus

extern"C" {

#endif

#ifdef__cplusplus

}

#endif

的作用又是什么呢?我们将在下文一一道来。

3.深层揭密extern "C"

extern"C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。让我们来详细解读这两重含义。

(1) 被extern "C"限定的函数或变量是extern类型的;

extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:

extern int a;

仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配内存空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。

通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块 A编译生成的目标代码中找到此函数。

与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

(2) 被extern "C"修饰的变量和函数是按照C语言方式编译和连接的;

未加extern “C”声明时的编译方式

首先看看C++中对类似C的函数是怎样编译的。

作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:

void foo( int x,int y );

该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x,float y )编译生成的符号是不相同的,后者为_foo_int_float。

同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。

未加extern "C"声明时的连接方式

假设在C++中,模块A的头文件如下:

// 模块A头文件 moduleA.h

#ifndef MODULE_A_H

#define MODULE_A_H

int foo( int x, inty );

#endif

在模块B中引用该函数:

// 模块B实现文件 moduleB.cpp

#i nclude"moduleA.h"

foo(2,3);

实际上,在连接阶段,连接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!

加extern "C"声明后的编译和连接方式

加extern "C"声明后,模块A的头文件变为:

// 模块A头文件 moduleA.h

#ifndef MODULE_A_H

#define MODULE_A_H

extern"C" int foo( int x, int y );

#endif

在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:

(1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;

(2)连接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。

如果在模块A中函数声明了foo为extern "C"类型,而模块B中包含的是extern int foo( int x, int y ) ,则模块B找不到模块A中的函数;反之亦然。

所以,可以用一句话概括extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):

实现C++与C及其它语言的混合编程。

明白了C++中extern "C"的设立动机,我们下面来具体分析extern"C"通常的使用技巧。

4.extern"C"的惯用法

(1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:

extern"C"

{

#i nclude"cExample.h"

}

而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern"C"声明,在.c文件中包含了extern"C"时会出现编译语法错误。

笔者编写的C++引用C函数例子工程中包含的三个文件的源代码如下:

#ifndef C_EXAMPLE_H

#define C_EXAMPLE_H

extern int add(intx,int y);

#endif

#i nclude"cExample.h"

int add( int x, inty )

{

return x + y;

}

// c++实现文件,调用add:cppFile.cpp

extern"C"

{

#i nclude"cExample.h"

}

int main(int argc,char* argv[])

{

add(2,3);

return 0;

}

如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" { }。

(2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。

笔者编写的C引用C++函数例子工程中包含的三个文件的源代码如下:

//C++头文件 cppExample.h

#ifndefCPP_EXAMPLE_H

#defineCPP_EXAMPLE_H

extern"C" int add( int x, int y );

#endif

//C++实现文件 cppExample.cpp

#i nclude"cppExample.h"

int add( int x, inty )

{

return x + y;

}

extern int add( intx, int y );

int main( int argc,char* argv[] )

{

add( 2, 3 );

return 0;

}

 

 

 

 

可变参数宏

分类: c语言2012-01-05 09:12 465人阅读 评论(0) 收藏 举报

macrospreprocessor编译器gcc扩展

目录(?)[+]

原文地址:http://www.upsdn.net/html/2006-08/692.html

 

GNU C中,宏可以接受可变数目的参数,就象函数一样,例如:
#define pr_debug(fmt,arg...) \
printk(KERN_DEBUG fmt,##arg)


用可变参数宏(variadic macros)传递可变参数表
你可能很熟悉在函数中使用可变参数表,如:

voidprintf(const char* format, …);

直到最近,可变参数表还是只能应用在真正的函数中,不能使用在宏中。

C99编译器标准终于改变了这种局面,它允许你可以定义可变参数宏(variadic macros),这样你就可以使用拥有可以变化的参数表的宏。可变参数宏就像下面这个样子:

#define debug(…)printf(__VA_ARGS__)

缺省号代表一个可以变化的参数表。使用保留名 __VA_ARGS__把参数传递给宏。当宏的调用展开时,实际的参数就传递给printf()了。例如:

Debug(“Y =%d\n”, y);

而处理器会把宏的调用替换成:

printf(“Y =%d\n”, y);

因为debug()是一个可变参数宏,你能在每一次调用中传递不同数目的参数:

debug(“test”); //一个参数

可变参数宏不被ANSI/ISO C++所正式支持。因此,你应当检查你的编译器,看它是否支持这项技术。

 

GCCC99的可变参数宏,更方便地打印调试信息

gcc的预处理提供的可变参数宏定义真是好用: 

#ifdef DEBUG

         #define dbgprint(format,args...) \

         fprintf(stderr, format, ##args)

#else

         #define dbgprint(format,args...)

#endif

如此定义之后,代码中就可以用dbgprint了,例如dbgprint("aaa %s", __FILE__);。感觉这个功能比较Cool  :em11:

下面是C99的方法: 

#define dgbmsg(fmt,...) \

            printf(fmt,__VA_ARGS__)

 

新的C99规范支持了可变参数的宏

具体使用如下:

以下内容为程序代码:

[cpp] view plaincopyprint?

1.  #include <stdarg.h>  

2.  #include <stdio.h>  

3.  #define LOGSTRINGS(fm, ...) printf(fm,__VA_ARGS__)  

4.  int main() {  

5.      LOGSTRINGS("hello, %d ", 10);  

6.      return 0;   

7.  }   

但现在似乎只有gcc才支持。

可变参数的宏里的‘##’操作说明

带有可变参数的宏(Macros with a Variable Number ofArguments

1999年版本的ISO C标准中,宏可以象函数一样,定义时可以带有可变参数。宏的语法和函数的语法类似。下面有个例子:

#definedebug(format, ...) fprintf (stderr, format, __VA_ARGS__)

这里,‘…’指可变参数。这类宏在被调用时,它(这里指‘…’)被表示成零个或多个符号,包括里面的逗号,一直到到右括弧结束为止。当被调用时,在宏体(macro body)中,那些符号序列集合将代替里面的__VA_ARGS__标识符。更多的信息可以参考CPP手册。

GCC始终支持复杂的宏,它使用一种不同的语法从而可以使你可以给可变参数一个名字,如同其它参数一样。例如下面的例子:

#definedebug(format, args...) fprintf (stderr, format, args)

这和上面举的那个ISO C定义的宏例子是完全一样的,但是这么写可读性更强并且更容易进行描述。

GNU CPP还有两种更复杂的宏扩展,支持上面两种格式的定义格式。

在标准C里,你不能省略可变参数,但是你却可以给它传递一个空的参数。例如,下面的宏调用在ISO C里是非法的,因为字符串后面没有逗号:

debug ("Amessage")

GNU CPP在这种情况下可以让你完全的忽略可变参数。在上面的例子中,编译器仍然会有问题(complain),因为宏展开后,里面的字符串后面会有个多余的逗号。

为了解决这个问题,CPP使用一个特殊的‘##’操作。书写格式为:

#define debug(format,...) fprintf (stderr, format,## __VA_ARGS__)

备注:本人使用中实际用的宏定义为

//******************************************************************************
// Uncomment the following line to disable printf debug info
#undef DEBUG
// Uncomment the following line to enable printf debug info
// #define DEBUG


#if defined(DEBUG)
#define dprintf(format, ...)   printf(format, ##__VA_ARGS__)
#else
#define dprintf(format, ...)
#endif

 

这里,如果可变参数被忽略或为空,‘##’操作将使预处理器(preprocessor)去除掉它前面的那个逗号。如果你在宏调用时,确实提供了一些可变参数,GNU CPP也会工作正常,它会把这些可变参数放到逗号的后面。象其它的pasted macro参数一样,这些参数不是宏的扩展。

 

 

 

 

C语言的函数形参是从右向左压入堆栈的,以保证栈顶是第一个参数,而且x86平台内存分配顺序是从高地址到低地址。因此函数fun(int var1,int var2,...,int varN)内存分配大致上是这样的:(可变参数在中间)

栈区:

|栈顶             低地址

|第一个参数var1   <-- &v

|第二个参数var2   <-- va_start(ap,v)ap指向地址       

|...

|函数的最后varN

|...

|函数的返回地址

|...

|栈底    高地址

 

 

 

http://blog.csdn.net/vcmman/article/details/6043751

 

 

Mikrokopter MK代码分析1

分类: 学习2010-11-29 20:17 1797人阅读 评论(1) 收藏 举报

代码分析list平台加密编译器table

1.可变参数va_list的使用

先来个简单的例子:

 

#include <stdio.h>

#include <stdarg.h>

int sum(int num,...);

int sum(int num,...)

  int result = 0;

  va_list argptr;

  va_start(argptr, num);

  while(num--)

 { printf("%s ",va_arg(argptr, char *));}

 va_end(argptr);

 return result;

}

int main()

{

 sum(3, "hello", "world", "!");       

 // output: hello world !

 return 0;

 

    可变参数中个数不定;可变参数中的每个参数的类型可以不同;可变参数的每个参数并没有实际的名称与之相对应,用起来是很灵活。可变参数是由宏实现的,但是由于硬件平台的不同,编译器的不同,宏的定义也不相同,下面是VC6.0x86平台的定义: 

typedef char * va_list;      // TC中定义为void*

#define _INTSIZEOF(n)    ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) ) 

//为了满足需要内存对齐的系统

#define va_start(ap,v)    ( ap = (va_list)&v + _INTSIZEOF(v) ) 

#define va_arg(ap,t)       ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) 

#define va_end(ap)  ( ap = (va_list)0 ) 

C语言的函数形参是从右向左压入堆栈的,以保证栈顶是第一个参数,而且x86平台内存分配顺序是从高地址到低地址。因此函数fun(int var1,int var2,...,int varN)内存分配大致上是这样的:(可变参数在中间)

栈区:

|栈顶             低地址

|第一个参数var1   <-- &v

|第二个参数var2   <-- va_start(ap,v)ap指向地址       

|...

|函数的最后varN

|...

|函数的返回地址

|...

|栈底    高地址

va_start(ap,v)ap = (va_list)&v + _INTSIZEOF(v)指向第二个参数地址。调用va_arg(ap,t), ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )取出当前ap指针所指的值,并使ap指向下一个参数 

2.循环冗余校验

·循环校验码:英文cyclic redundancy check的简称,是数据通信领域中最常用的一种高效差错校验码,其特征是信息字段和校验字段的长度可以任意选定。

·生成CRC的基本原理:任意一个由二进制位串组成的代码都可以和一个系数仅为‘0’‘1’取值的多项式一一对应。例如:代码1010111对应的多项式为x6+x4+x2+x+1,而多项式为x5+x3+x2+x+1对应的代码101111

·CRC码集选择的原则:若设码字长度为N,信息字段为K位,校验字段为R(N=K+R),则对于CRC码集中的任一码字,存在且仅存在一个R次多项式g(x),使得V(x)=A(x)g(x)=xRm(x)+r(x)其中,m(x)K次信息多项式,r(x)R-1次校验多项式,g(x)称为生成多项式:g(x)=g0+g1x+g2x2+...+g(R-1)x(R-1)+gRxR。发送方通过指定的g(x)产生CRC码字,接收方则通过该g(x)来验证收到的CRC码字。 

·CRC校验码计算过程:借助于多项式除法,其余数为校验字段。例如:信息字段代码为: 1011001;对应m(x)=x6+x4+x3+1,假设生成多项式为:g(x)=x4+x3+1;则对应g(x)的代码为: 11001x4m(x)=x10+x8+x7+x4,对应的代码记为:10110010000;采用多项式除法得余数为: 1010。这里的除法没有数学上的含义,而是采用计算机的模二除法,即除数和被除数做异或运算。进行异或运算时除数和被除数最高位对齐,按位异或。发送方:发出的传输字段为:  10110011010 信息字段校验字段接收方:使用相同的生成码进行校验:接收到的字段/生成码(二进制除法)如果能够除尽,则正确。 

·常用CRC校验多项式:常用的CRC码是,CRC-CCITTCRC-16,它们的生成多项式分别是:CRC-CCITT=x^16+x^12+x^5+1 CRC-16=x^16+x^15+x^2+1CRC-16例程: 

 

uint crc_16(uchar *ptr,uchar len)

uint crc = 0;

uchar i;

while(len--)

for(i=0x80; i!=0; i>>=1)

{if((crc&0x8000)!=0)

crc<<=1;

crc^=0x1021;}

else

crc<<=1;

if((*ptr&i)!=0)

crc^=0x1021;}

        ptr++;}

            return(crc); }

 

3. Base64编码

各位看官应该都是资深的网虫了,小弟斗胆在此问问大家,平时上网时,除了泡MM、到论坛灌水、扔版砖……之外,进行的最多的是什么活动?对了,你一定会说:是收发电子邮件!(谁敢说自己没收/发过电子邮件的?拉出去枪毙了!!)  

/E-mail的时候有一个安全性的问题——假想一下,你花了一整天时间给系花写的情书,在发送的过程中被隔壁宿舍张三那小子截获了(难道他是黑客??),更糟的是他是你的情敌啊……天,后果不堪设想!!因此,我们必须有一种比较可靠的加密方法,能够对电子邮件的明文进行转换,至少要得出一个无法被别人一眼就看出内容来的东西,而且编码/解码的速度还要足够快。(这时你可以再假想一下啦,张三那家伙截获了你的肉麻情书,可是他一看:咦?怎么乱七八糟的?垃圾邮件!!”——这样一来你不就逃过大难了?!)
Base64就是在这种背景下产生的加密方法。它的特点是:1、速度非常快。2、能够将字符串A转换成字符串B,而且如果你光看字符串B,是绝对猜不出字符串A的内容来的。不信吗?让我们来看看下面这串东西:xOO6w6Osu7bTrbniwdnAz8LetcTnzbfXzOy12KOh
呵呵,是什么啊?猜出来了吗?其实它就是下面这段文字经过Base64编码产生的东东:
你好,欢迎光临老罗的缤纷天地!介绍说完啦,让我们开始探讨实质性的东西。
Base64
是网络上最常见的用于传输8Bit字节代码的编码方式之一,大家可以查看RFC2045RFC2049,上面有MIME的详细规范。
Base64
要求把每三个8Bit的字节转换为四个6Bit的字节(3*8 = 4*6 = 24),然后把6Bit再添两位高位0,组成四个8Bit的字节,也就是说,转换后的字符串理论上将要比原来的长1/3
这样说会不会太抽象了?不怕,我们来看一个例子:

转换前

aaaaaabb

ccccdddd

eeffffff

转换后

00aaaaaa

00bbcccc

00ddddee

00ffffff

应该很清楚了吧?上面的三个字节是原文,下面的四个字节是转换后的Base64编码,其前两位均为0。转换后,我们用一个码表来得到我们想要的字符串(也就是最终的Base64编码),这个表是这样的:(摘自RFC2045
                            Table 1: The Base64 Alphabet
      Value Encoding  Value Encoding  Value Encoding  Value Encoding
           0 A            17 R            34 i            51 z
           1 B            18 S            35 j            52 0
           2 C            19 T            36 k            53 1
           3 D            20 U            37 l            54 2
           4 E            21 V            38 m            55 3
           5 F            22 W            39 n            56 4
           6 G            23 X            40 o            57 5
           7 H            24 Y            41 p            58 6
           8 I            25 Z            42 q            59 7
           9 J            26 a            43 r            60 8
         
 10 K            27 b            44 s            61 9
          11 L            28 c            45 t            62 +
          12 M            29 d            46 u            63 /
          13 N            30 e            47 v
          14 O            31 f            48 w           (pad) =
          15 P            32 g            49 x
          16 Q            33 h            50 y
解码只是编码的逆过程,在此我就不多说了,另外有关MIMERFC还是有很多的,如果需要详细情况请自行查找。
用更接近于编程的思维来说,编码的过程是这样的:第一个字符通过右移2位获得第一个目标字符的Base64表位置,根据这个数值取到表上相应的字符,就是第一个目标字符。
然后将第一个字符左移4位加上第二个字符右移4位,即获得第二个目标字符。再将第二个字符左移2位加上第三个字符右移6位,获得第三个目标字符。最后取第三个字符的右6位即获得第四个目标字符。在以上的每一个步骤之后,再把结果与 0x3F 进行 AND 位操作,就可以得到编码后的字符了。(感谢 Athena 指出以上描述中原有的一些错误!^_^
So easy! That’s all!!!
可是等等……聪明的你可能会问到,原文的字节数量应该是3的倍数啊,如果这个条件不能满足的话,那该怎么办呢?
我们的解决办法是这样的:原文的字节不够的地方可以用全0来补足,转换时Base64编码用=号来代替。这就是为什么有些Base64编码会以一个或两个等号结束的原因,但等号最多只有两个。因为:
余数 = 原文字节数 MOD 3
所以余数任何情况下都只可能是012这三个数中的一个。如果余数是0的话,就表示原文字节数正好是3的倍数(最理想的情况啦)。如果是1的话,为了让Base64编码是4的倍数,就要补2个等号;同理,如果是2的话,就要补1个等号。 讲到这里,大伙儿应该全明白了吧?如果还有不清楚的话就返回去再仔细看看,其实不难理解的。

 

 

 

c语言中可变参数的实现

分类: c语言2013-03-16 22:56 442人阅读 评论(0) 收藏 举报

c

()写一个简单的可变参数的C函数 

下面我们来探讨,如何写一个简单的可变参数的C函数。写可变参数的C函数要在程序中用到以下这些宏
void va_start( va_list arg_ptr, prev_param ); 

type va_arg( va_list arg_ptr, type ); 

void va_end( va_list arg_ptr ); 
va
在这里是variable-argument(可变参数)的意思。

这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件。

下面我们写一个简单的可变参数的函数,该函数至少有一个整数参数,第二个参数也是整数,是可选的。函数只是打印这两个参数的值。

void simple_va_fun(int i, ...) 

va_list arg_ptr; 
int j=0; 

va_start(arg_ptr, i); 
j=va_arg(arg_ptr, int); 
va_end(arg_ptr); 
printf("%d %d\n", i, j); 
return; 

我们可以在我们的头文件中这样声明我们的函数
extern void simple_va_fun(int i, ...); 
我们在程序中可以这样调用
simple_va_fun(100); 
simple_va_fun(100,200); 
从这个函数的实现可以看到,我们使用可变参数应该有以下步骤: 
1)
首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针。
2)
然后va_start宏初始化变量arg_ptr,这个宏的第二个参数是该可变参数函数的第一个参数,是一个固定的参数。
3)
然后va_arg返回可变的参数,并赋值给整数j va_arg的第二个参数是你要返回的参数的类型,这里是int型。
4)
最后用va_end宏结束可变参数的获取,然后你就可以在函数里使用第二个参数了。如果函数有多个可变参数的,依次调用va_arg获取各个参数。
如果我们用下面三种方法调用的话,都是合法的,但结果却不一样
1) simple_va_fun(100); 
结果是:100 -123456789(会变的值
2) simple_va_fun(100,200); 
结果是:100 200 
3) simple_va_fun(100,200,300); 
结果是:100 200 

我们看到第一种调用有错误,第二种调用正确,第三种调用尽管结果正确,但和我们函数最初的设计有冲突。

下面一节我们探讨出现这些结果的原因和可变参数在编译器中是如何处理的。


(
)可变参数在编译器中的处理 

我们知道va_startva_argva_end是在stdarg.h中被定义成宏的,由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同,下面以VC++stdarg.hx86平台的宏定义摘录如下(’\’号表示折行)

typedef char * va_list; 

#define _INTSIZEOF(n) \ 
((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) ) 

#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) 

#define va_arg(ap,t) \ 
( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) 

#define va_end(ap) ( ap = (va_list)0 ) 

定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统。C语言的函数是从右向左压入堆栈的,(1)是函数的参数在堆栈中的分布位置。我们看到va_list被定义成char*,有一些平台或操作系统定义为void*。再看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的地址,所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在堆栈的地址,如图:

高地址|-----------------------------| 
|
函数返回地址
|-----------------------------| 
|....... | 
|-----------------------------| 
|
n个参数(第一个可变参数) | 
|-----------------------------|<--va_start
ap指向 
|
n-1个参数(最后一个固定参数)| 
低地址|-----------------------------|<-- &v 
( 1 ) 

然后,我们用va_arg()取得类型t的可变参数值,以上例为int型为例,我们看一下va_argint型的返回值:
j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) ); 
首先ap+=sizeof(int),已经指向下一个参数的地址了。然后返回ap-sizeof(int)int*指针,这正是第一个可变参数在堆栈里的地址 

 

然后用*取得这个地址的内容(参数值)赋给j


高地址|-----------------------------| 
|
函数返回地址
|-----------------------------| 
|....... | 
|-----------------------------|<--va_arg
ap指向 
|
n个参数(第一个可变参数) | 
|-----------------------------|<--va_start
ap指向 
|
n-1个参数(最后一个固定参数)| 
低地址|-----------------------------|<-- &v 
( 2 ) 

最后要说的是va_end宏的意思,x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样。有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcclinuxx86平台就是这样定义的。
在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型。
关于va_start, va_arg, va_end的描述就是这些了,我们要注意的是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的。

(
)可变参数在编程中要注意的问题 

因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢。

可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智地识别不同参数的个数和类型。

有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数printf是从固定参数format字符串来分析出参数的类型,再调用va_arg的来获取可变参数的。也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的。

另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利。

如果simple_va_fun()改为:

void simple_va_fun(int i, ...) 

va_list arg_ptr; 
char *s=NULL; 

va_start(arg_ptr, i); 
s=va_arg(arg_ptr, char*); 
va_end(arg_ptr); 
printf("%d %s\n", i, s); 
return; 

可变参数为char*型,当我们忘记用两个参数来调用该函数时,就会出现core dump(Unix)或者页面非法的错误(window平台)。但也有可能不出错,但错误却是难以发现,不利于我们写出高质量的程序。
以下提一下va系列宏的兼容性。
System V Unix
va_start定义为只有一个参数的宏:
va_start(va_list arg_ptr); 
ANSI C则定义为
va_start(va_list arg_ptr, prev_param); 
如果我们要用system V的定义,应该用vararg.h头文件中所定义的宏,ANSI C的宏跟system V的宏是不兼容的,我们一般都用ANSI C,所以用ANSI C的定义就够了,也便于程序的移植。

 

 

 

 

http://blog.csdn.net/gzshun/article/details/7081736

 

 

C语言sscanf函数的总结

分类: LinuxC/C++2011-12-18 16:10 14571人阅读 评论(9) 收藏 举报

语言stringcandroidubuntuiphone

在处理字符串的程序当中,经常会分析字符串,从一大长串的字符串截取我们需要的数据,这如果通过自己手写函数来分析,虽然可以,但当你知道sscanf的功能,那就自己写函数分析就显得多此一举。

这些函数的使用都很简单,总结一下,等下次使用一目了然。俗话说:好记性不如烂笔头,记录下来就是效率。

以下源代码是本人测试的源文件,附带讲解注释。

 

[cpp] view plaincopy

1.  /***************************************************** 

2.  ** Name         : sscanf.c  

3.  ** Author       : gzshun 

4.  ** Version      : 1.0 

5.  ** Date         : 2011-12 

6.  ** Description  : sscanf function 

7.  ******************************************************/  

8.  #include <stdio.h>  

9.  #include <stdlib.h>  

10. #include <string.h>  

11.   

12. static void sscanf_test(void);  

13.   

14. static void sscanf_test(void)  

15. {  

16.     int ret;  

17.     char *string;  

18.     int  digit;  

19.     char buf1[255];  

20.     char buf2[255];  

21.     char buf3[255];  

22.     char buf4[255];  

23.   

24.     /*1.最简单的用法*/  

25.     string = "china beijing 123";  

26.     ret = sscanf(string, "%s %s %d", buf1, buf2, &digit);  

27.     printf("1.string=%s\n", string);  

28.     printf("1.ret=%d, buf1=%s, buf2=%s, digit=%d\n\n", ret, buf1, buf2, digit);  

29.     /* 

30.     **执行结果: 

31.     **1.ret=3, buf1=china, buf2=beijing, digit=123 

32.     **可以看出,sscanf的返回值是读取的参数个数 

33.     */  

34.   

35.     /*2.取指定长度的字符串*/  

36.     string = "123456789";  

37.     sscanf(string, "%5s", buf1);  

38.     printf("2.string=%s\n", string);  

39.     printf("2.buf1=%s\n\n", buf1);  

40.     /* 

41.     **执行结果: 

42.     **2.buf1=12345 

43.     */  

44.   

45.     /*3.取到指定字符为止的字符串*/  

46.     string = "123/456";  

47.     sscanf(string, "%[^/]", buf1);  

48.     printf("3.string=%s\n", string);  

49.     printf("3.buf1=%s\n\n", buf1);  

50.     /* 

51.     **执行结果: 

52.     **3.buf1=123 

53.     */  

54.   

55.     /*4.取到指定字符集为止的字符串*/  

56.     string = "123abcABC";  

57.     sscanf(string, "%[^A-Z]", buf1);  

58.     printf("4.string=%s\n", string);  

59.     printf("4.buf1=%s\n\n", buf1);  

60.     /* 

61.     **执行结果: 

62.     **4.buf1=123abc 

63.     */  

64.   

65.     /*5.取仅包含指定字符集的字符串*/  

66.     string = "0123abcABC";  

67.     sscanf(string, "%[0-9]%[a-z]%[A-Z]", buf1, buf2, buf3);  

68.     printf("5.string=%s\n", string);  

69.     printf("5.buf1=%s, buf2=%s, buf3=%s\n\n", buf1, buf2, buf3);  

70.     /* 

71.     **执行结果: 

72.     **5.buf1=123, buf2=abc, buf3=ABC 

73.     */  

74.   

75.     /*6.获取指定字符中间的字符串*/  

76.     string = "ios<android>wp7";  

77.     sscanf(string, "%*[^<]<%[^>]", buf1);  

78.     printf("6.string=%s\n", string);  

79.     printf("6.buf1=%s\n\n", buf1);  

80.     /* 

81.     **执行结果: 

82.     **6.buf1=android 

83.     */  

84.   

85.     /*7.指定要跳过的字符串*/  

86.     string = "iosVSandroid";  

87.     sscanf(string, "%[a-z]VS%[a-z]", buf1, buf2);  

88.     printf("7.string=%s\n", string);  

89.     printf("7.buf1=%s, buf2=%s\n\n", buf1, buf2);  

90.     /* 

91.     **执行结果: 

92.     **7.buf1=ios, buf2=android 

93.     */  

94.   

95.     /*8.分割以某字符隔开的字符串*/  

96.     string = "android-iphone-wp7";  

97.     /* 

98.     **字符串取道'-'为止,后面还需要跟着分隔符'-', 

99.     **起到过滤作用,有点类似于第7 

100.     */  

101.     sscanf(string, "%[^-]-%[^-]-%[^-]", buf1, buf2, buf3);  

102.     printf("8.string=%s\n", string);  

103.     printf("8.buf1=%s, buf2=%s, buf3=%s\n\n", buf1, buf2, buf3);  

104.     /* 

105.     **执行结果: 

106.     **8.buf1=android, buf2=iphone, buf3=wp7 

107.     */  

108.   

109.     /*9.提取邮箱地址*/  

110.     string = "Email:beijing@sina.com.cn";  

111.     sscanf(string, "%[^:]:%[^@]@%[^.].%s", buf1, buf2, buf3, buf4);  

112.     printf("9.string=%s\n", string);  

113.     printf("9.buf1=%s, buf2=%s, buf3=%s, buf4=%s\n\n", buf1, buf2, buf3, buf4);  

114.     /* 

115.     **执行结果: 

116.     **9.buf1=Email, buf2=beijing, buf3=sina, buf4=com.cn 

117.     */  

118.   

119.     /*10.过滤掉不想截取或不需要的字符串--补充, 

120.     **%号后面加一*,代表过滤这个字符串,不读取 

121.     */  

122.     string = "android iphone wp7";  

123.     sscanf(string, "%s %*s %s", buf1, buf2);  

124.     printf("10.string=%s\n", string);  

125.     printf("10.buf1=%s, buf2=%s\n\n", buf1, buf2);  

126.     /* 

127.     **执行结果: 

128.     **10.android wp7 

129.     */  

130. }  

131.   

132. int main(int argc, char **argv)  

133. {  

134.     sscanf_test();  

135.       

136.     return 0;  

137. }  

138.   

139. /* 

140. **测试程序 

141. **环境: 

142. **Linux ubuntu 2.6.32-24-generic-pae #39-Ubuntu SMP Wed Jul 28 07:39:26 UTC 2010 i686 GNU/Linux 

143. **gcc version 4.4.3 (Ubuntu 4.4.3-4ubuntu5) 

144. ** 

145. gzshun@ubuntu:~/c/sscanf$ gcc sscanf.c -o sscanf 

146. gzshun@ubuntu:~/c/sscanf$ ./sscanf 

147. 1.string=china beijing 123 

148. 1.ret=3, buf1=china, buf2=beijing, digit=123 

149.  

150. 2.string=123456789 

151. 2.buf1=12345 

152.  

153. 3.string=123/456 

154. 3.buf1=123 

155.  

156. 4.string=123abcABC 

157. 4.buf1=123abc 

158.  

159. 5.string=0123abcABC 

160. 5.buf1=0123, buf2=abc, buf3=ABC 

161.  

162. 6.string=ios<android>wp7 

163. 6.buf1=android 

164.  

165. 7.string=iosVSandroid 

166. 7.buf1=ios, buf2=android 

167.  

168. 8.string=android-iphone-wp7 

169. 8.buf1=android, buf2=iphone, buf3=wp7 

170.  

171. 9.string=Email:beijing@sina.com.cn 

172. 9.buf1=Email, buf2=beijing, buf3=sina, buf4=com.cn 

173.  

174. 10.string=android iphone wp7 

175. 10.buf1=android, buf2=wp7 

176. */  

 

 

 

 

 

CGI之C语言篇

分类: web开发2014-01-23 16:43 597人阅读 评论(0) 收藏 举报

cgi

目录(?)[+]

为什么要进行CGI编程? 


   在HTML中,当客户填写了表单,并按下了发送(submit)按钮后,表单的内容被发送到了服务器端,一般的,这时就需要有一个服务器端脚本来对表单的内容进行一些处理,或者是把它们保存起来,或者是按内容进行一些查询,或者是一些别的什么。没有了CGI,WEB的世界就完全失去了它的交互性,所有的信息都变成单向的了,而不能够有任何的反馈。 


   有的人认为可以用JavaScript来代替CGI程序,这其实是一个概念上的错误。JavaScript只能够在客户浏览器中运行,而CGI却是工作在服务器上的。他们所做的工作有一些交集,比如表单数据验证一类的,但是JavaScript是绝对无法取代CGI的。但可以这样说,如果一项工作即能够用JavaScript来做,又可以用CGI来做,那么绝对要使用JavaScript,在执行的速度上,JavaScript比CGI有着先天的优势只有那些在客户端解决不了的问题,比如和某个远程数据库交互,这时就应该使用CGI。 


   简单的说来,CGI是用来沟通HTML表单和服务器端程序的接口(interface)。说它是接口,也就是说CGI并不是一种语言,而是可以被其他语言所应用的一个规范集。理论上讲,你可以用任何的程序语言来编写CGI程序,只要在编程的时候符合CGI规范所定义的一些东西就可以了。由于C语言在平台无关性上表现不错(几乎在任何的系统平台下都有其相应编译器),而且对大多数程序员而言都算得上很熟悉(不像Perl),因此,C是CGI编程的首选语言之一。这儿我们介绍的,就是如何使用C来编写CGI程序。 


   作为CGI编程的最为简单的例子,就是进行表单的处理。因而在这篇文章中,我们主要介绍的就是如何用C来编写CGI程序来进行表单处理。 

GET表单的处理 


   对于那些使用了属性“METHOD=GET”的表单(或者没有METHOD属性,这时候GET是其缺省值),CGI定义为:当表单被发送到服务器断后,表单中的数据被保存在服务器上一个叫做QUERY_STRING的环境变量中这种表单的处理相对简单,只要读取环境变量就可以了。这一点对不同的语言有不同的做法。在C语言中,你可以用库函数getenv(定义在标准库函数stdlib中)来把环境变量的值作为一个字符串来存取。你可以在取得了字符串中的数据后,运用一些小技巧进行类型的转换,这都是比较简单的了。在CGI程序中的标准输出(output)(比如在C中的stdout文件流)也是经过重定义了的。它并没有在服务器上产生任何的输出内容,而是被重定向到客户浏览器。这样,如果编写一个C的CGI程序的时候,把一个HTML文档输出到它的stdout上,这个HTML文档会被在客户端的浏览器中显示出来。这也是CGI程序的一个基本原理。 

   我们来看看具体的程序实现,下面是一段HTML表单: 

[html] view plaincopyprint?

1.  <FORM ACTION="/cgi-bin/multi.cgi">   

2.    

3.      <P>请在下面填入乘数和被乘数,按下确定后可以看到结果。 </p>  

4.    

5.  <INPUT NAME="m" SIZE="5">   

6.    

7.  <INPUT NAME="n" SIZE="5"></BR>   

8.    

9.  <INPUT TYPE="SUBMIT" VALUE="确定">   

10.   

11. </FORM>   

 

   我们要实现的功能很简单,就是把表单中输入的数值乘起来,然后输出结果。其实这个功能完全可以用JavaScript来实现,但为了让程序尽量的简单易懂,我还是选择了这个小小的乘法来作为示例。 


   下面就是处理这个表单的CGI程序,对应于FORM标签中的ACTION属性值。 

[cpp] view plaincopyprint?

1.  #include <stdio.h>    

2.  #include <stdlib.h>    

3.      

4.  int main(void) {    

5.      char *data;    

6.      long m;    

7.      long n;    

8.          

9.      printf("%s%c%c\n""Content-Type:text/html;charset=gb2312", 13, 10);    

10.     printf("<TITLE>Result of multiplication</TITLE>\n");    

11.     printf("<H3>Result of multiplication</H3>\n");    

12.     data = getenv("QUERY_STRING");    

13.     if (data == NULL) {    

14.         printf("<P>ERRORData is not input or data transfer has problem</P>\n");    

15.     }    

16.     else if(sscanf(data,"m=%ld&n=%ld", &m, &n) != 2) {    

17.         printf("<P>ERRORThe input data is invalid, you must input digit number</P>\n");    

18.     }    

19.     else {    

20.         printf("<P>%ld and %ld equals: %ld.\n", m, n, m*n);    

21.     }    

22.     

23.     return 0;    

24. }    


   具体的C语法就不多讲了,我们来看看它作为CGI程序所特殊的地方。 

   前面已经提到标准输出的内容就是要被显示在浏览器中的内容。第一行的输出内容是必须的,也是一个CGI程序所特有的:printf("%s%c%c ","Content-Type:text/html",13,10),这个输出是作为HTML的文件头。因为CGI不仅可以像浏览器输出HTML文本,而且可以输出图像,声音之类的东西。这一行告诉浏览器如何处理接受到的内容。在Content-Type的定义后面跟有两行的空行,这也是不可缺少的。因为所有CGI程序的头部输出都是相近的,因而可以为其定义一个函数,来节省编程的时间。这是CGI编程常用的一个技巧。 

   程序在后面调用了库函数getevn来得到QUERY_STRING的内容,然后使用sscanf函数把每个参数值取出来,要注意的是sscanf函数的用法。其他的就没有什么了,和一般的C程序没有区别。 

   把程序编译后,改名为mult.cgi放在/cgi-bin/目录下面,就可以被表单调用了。这样,一个处理GET方式表单的CGI程序就大功告成了。 

POST表单处理 

   下面我们来考虑另外一种表单传送方法:POST。假设我们要实现的任务是这样的:把表单中客户输入的一段文本内容添加到服务器上的一个文本文件的后面。这可以看作是一个留言版程序的雏形。显然,这个工作是无法用JavaScript这种客户端脚本来实现,也算得上真正意义上的CGI程序了。 

   看起来这个问题和上面讲的内容很相近,仅仅是用不同的表单和不同的脚本(程序)而已。但实际上,这中间是有一些区别的。在上面的例子中,GET的处理方法可以看作是“纯查询(pure query)”类型的,也就是说,它与状态无关。同样的数据可以被提交任意的次数,而不会引起任何的问题(除了服务器的一些小小的开销)。但是现在的任务就不同了,至少它要改变一个文件的内容。因而,可以说POST的处理方法是与状态有关的。这也算是POST和GET的区别之一。而且,GET对于表单的长度是有限制的,而POST则不然,这也是在这个任务中选用POST方法的主要原因。但相对的,对GET的处理速度就要比POST快一些。 

   在CGI的定义中,对于POST类型的表单,其内容被送到CGI程序的标准输入(在C语言中是stdin),而被传送的长度被放在环境变量CONTENT_LENGTH。因而我们要做的就是,在标准输入中读入CONTENT_LENGTH长度的字符串。从标准输出读入数据听起来似乎要比从环境变量中读数据来的要容易一些,其实则不然,有一些细节地方要注意,这在下面的程序中可以看到。特别要注意的一点就是:CGI程序和一般的程序有所不同一般的程序在读完了一个文件流的内容之后,会得到一个EOF的标志。但在CGI程序的表单处理过程中,EOF是永远不会出现的,所以千万不要读多于CONTENT_LENGTH长度的字符,否则会有什么后果,谁也不知道(CGI规范中没有定义,一般根据服务器不同而有不同得处理方法)。 

   我们来看看到底如何从POST表单收集数据到CGI程序,下面給出了一個比较简单的C源代碼: 

[cpp] view plaincopyprint?

1.  #include <stdio.h>  

2.  #include <stdlib.h>  

3.    

4.    

5.  #define MAXLEN 80  

6.  /* 4 for field name "data", 1 for "=" */  

7.  #define EXTRA 5  

8.  /* 1 for added line break, 1 for trailing NUL */  

9.  #define MAXINPUT MAXLEN+EXTRA+2  

10. #define DATAFILE "./data.txt"  

11.    

12. void unencode(char *src, char *last, char *dest) {  

13.     for(; src != last; src++, dest++)  

14.         if (*src == '+') {  

15.             *dest = ' ';  

16.         }  

17.         else if(*src == '%') {  

18.             int code;  

19.             if (sscanf(src+1, "%2x", &code) != 1) {  

20.                 code = '?';  

21.             }  

22.             *dest = code;  

23.             src +=2;  

24.         }       

25.         else {  

26.             *dest = *src;  

27.         }  

28.     *dest = '\n';  

29.     *++dest = '\0';  

30. }  

31.    

32. int main(void) {  

33.     char *lenstr;  

34.     char input[MAXINPUT];  

35.     char data[MAXINPUT];  

36.     long len;  

37.   

38.   

39.     printf("%s%c%c\n""Content-Type:text/html;charset=iso-8859-1",13,10);  

40.     printf("<TITLE>Response</TITLE>\n");  

41.   

42.     lenstr = getenv("CONTENT_LENGTH");  

43.     if (lenstr == NULL || sscanf(lenstr,"%ld",&len) != 1 || len > MAXLEN) {  

44.         printf("<P>Error in invocation - wrong FORM probably.\n");  

45.     }  

46.     else {  

47.         printf("<p>CONTENT_LENGTH is %ld %d.</p>\n", len, atoi(lenstr));  

48.         // fgets(input, len+1, stdin);  

49.         fgets(input, len+1, stdin);  

50.         unencode(input+EXTRA, input+len, data);  

51.   

52.   

53.         FILE *fp;  

54.         fp = fopen(DATAFILE, "a+");  

55.         if(fp == NULL) {  

56.             printf("<p>Sorry, cannot store your data.</p>");  

57.         }  

58.         else {  

59.             printf("<p>Your data is: %s.</p>\n",  data);  

60.             fputs(data, fp);  

61.         }  

62.         fclose(fp);  

63.   

64.   

65.         printf("<p>Thank you! The following contribution of yours has been stored:</br></p>%s\n",data);  

66.     }  

67.   

68.   

69.     return 0;  

70. }  


   从本质上来看,程序先从CONTENT_LENGTH环境变量中得到数据的字长,然后读取相应长度的字符串。因为数据内容在传输的过程中是经过了编码的,所以必须进行相应的解码。编码的规则很简单,主要的有这几条: 


   1. 表单中每个字段用字段名后跟等号,再接上这个字段的值来表示,每个字段之间的内容用&连结; 

   2. 所有的空格符号用加号代替,所以在编码码段中出现空格是非法的; 

   3. 特殊的字符比如标点符号,和一些有特定意义的字符如“+”,用百分号后跟其对应的ACSII码值来表示。 

   例如:如果用户输入的是: 

   Hello there! 

   那么数据传送到服务器的时候经过编码,就变成了data=Hello+there%21 上面的unencode()函数就是用来把编码后的数据进行解码的。在解码完成后,数据被添加到data.txt文件的尾部,并在浏览器中回显出来。 

   把文件编译完成后,把它改名为collect.cgi后放在CGI目录中就可以被表单调用了。下面给出了其相应的表单: 

[html] view plaincopyprint?

1.  <FORM ACTION="/cgi-bin/collect.cgi" METHOD="POST">   

2.    

3.      <P>请输入您的留言(最多80个字符):</BR><INPUT NAME="data" SIZE="60" MAXLENGTH="80"></BR></p>  

4.    

5.  <INPUT TYPE="SUBMIT" VALUE="确定">   

6.    

7.  </FORM>  


   事实上,这个程序只能作为例子,是不能够正式的使用的。它漏掉了很关键的一个问题:当有多个用户同时向文件写入数据时,肯定会有错误发生。而对于一个这样的程序而言,文件被同时写入的几率是很大的。因此,在比较正式的留言版程序中,都需要做一些更多的考虑,比如加入一个信号量,或者是借助于一个钥匙文件等。因为那只是编程的技巧问题,在这儿就不多说了。 


   最后,我们来写一个浏览data.txt文件的的CGI程序,这只需要把内容输出到stdout就可以了: 

[cpp] view plaincopyprint?

1.  #include <stdio.h>  

2.  #include <stdlib.h>  

3.    

4.    

5.  #define DATAFILE "data.txt"  

6.    

7.    

8.  int main(void) {  

9.      FILE *f = fopen(DATAFILE,"r");  

10.     if(f == NULL) {  

11.         printf("%s%c%c\n""Content-Type:text/html;charset=iso-8859-1", 13, 10);  

12.         printf("<TITLE>Failure</TITLE>\n");  

13.         printf("<P><EM>Unable to open data file, sorry!</EM></p>");  

14.     }  

15.     else {  

16.         printf("%s%c%c\n""Content-Type:text/plain;charset=iso-8859-1", 13, 10);  

17.         int ch;  

18.         while ( (ch=getc(f)) != EOF) {  

19.             putchar(ch);  

20.         }  

21.         fclose(f);  

22.     }  

23.   

24.   

25.     return 0;  

26. }  


   这个程序唯一要注意的是:它并没有把data.txt包装成HTML格式后再输出,而是直接作为简单文本(plaintext)输出,这只要在输出的头部用text/plain类型代替text/html就可以了,浏览器会根据Content-Type的类型自动的选择相应的处理方法。 


   要触发这个程序也很简单,因为没有数据要输入,所以只需一个按钮就可以搞定了: 

[html] view plaincopyprint?

1.  <form action="http://127.0.0.1/cgi-bin/view_data.cgi">  

2.      <p>Click the button to view the text content in data.txt</p>  

3.  <div><input type="submit" value="View"></div>  

4.  </form>   


   到这儿,一些基本的用C编写CGI程序的原理就讲完了。当然,就凭讲的这些内容,还很难编写出一个好的CGI程序,这需要进一步的学习CGI的规范定义,以及一些其他的CGI编程特有的技巧。 

   这篇文章的目的,也就是要你了解一下CGI编程的概念。事实上,现在的一些主流的服务器端脚本编程语言如ASP,PHP,JSP等,都基本上具备了CGI编程的大部分的功能,但他们在使用上的,确实是比无论用什么语言进行CGI编程都要容易的多。所以在进行服务器端编程的时候,一般都会首先考虑使用这些脚本编程语言。只有当他们也解决不了,比如要进行一些更为底层的编程的时候,才会用到CGI。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值