显示标签为“C”的博文。显示所有博文
显示标签为“C”的博文。显示所有博文

星期四, 四月 26, 2007

C语言 网络地址

在网络编程中,或多或少都会与网络地址打交道,我们从易到难,再来一个三层境界,菜鸟级,入门级,进阶级。下面一一说明。

菜鸟级:
菜鸟都知道,网络编程中要用到IP地址,IP地址是一个32为的整数,是一个unsigned int或unsigned long, 我编程中见到的int与long都是4个自己的!
将一个doted字符串IP地址转化为整型IP地址: unsigned long inet_addr( const char* cp ); 失败的化返回INADDR_NONE(0xffffffff) 竟然是一个广播地址,所以这个操作完成后,一定要检查返回值!
整形IP地址其实就是下面的结构(这个结构被称为最恶心的结构),你可以直接转过去:
typedef struct in_addr {
union {
struct {
u_char s_b1,s_b2,s_b3,s_b4;
} S_un_b;
struct {
u_short s_w1,s_w2;
} S_un_w;
u_long S_addr;
} S_un;
} in_addr;
将一个整型IP地址转华为doted字符串IP地址: char* FAR inet_ntoa( struct in_addr in ); 你完全可以直接参数整数!这里要注意的仍然是返回值,这个值保存在被Socket实现内部的内存中,MSDN中说明Windows下,同一线程中下一次调用inet_ntoa将会破坏这个值,所以你可能需要保存这个返回值。这个内存也不需要释放。
一种可能的实现如下:
char FAR * inet_ntoa (struct in_addr in)
{
static char strRet[16];
// ...
return strRet;
}

以后请不要对这个指针的使用有太多的疑惑!
黄金规则: 所有被返回的地址都是网络地址(字节序从左到右)
Linux下的表中C库中,IP地址的操作与转化可以使用下面的函数,不多说了,请大家man!
     #include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int
inet_aton(const char *cp, struct in_addr *pin);

in_addr_t
inet_addr(const char *cp);

in_addr_t
inet_network(const char *cp);

char *
inet_ntoa(struct in_addr in);

const char *
inet_ntop(int af, const void * restrict src, char * restrict dst,
socklen_t size);

int
inet_pton(int af, const char * restrict src, void * restrict dst);

struct in_addr
inet_makeaddr(in_addr_t net, in_addr_t lna);

in_addr_t
inet_lnaof(struct in_addr in);

in_addr_t
inet_netof(struct in_addr in);

char *
inet_net_ntop(int af, const void *src, int bits, char *dst, size_t size);

int
inet_net_pton(int af, const char *src, void *dst, size_t size);
除了上面的两个函数外,Windows下不再有其他的inet_开头的兼容函数!

诸位可能要嚷嚷了,我老早以前就不是菜鸟了,好的!让我们马上进入下一个阶段!

入门级:
入门级的两个代表是函数是 struct HOSTENT* FAR gethostbyaddr( const char* addr, int len, int type ); 与 struct hostent* FAR gethostbyname( const char* name );
我们再来看另外一个重要的结构:
typedef struct hostent {
char FAR* h_name;
char FAR FAR** h_aliases;
short h_addrtype;
short h_length;
char FAR FAR** h_addr_list;
} hostent;

h_name
Official name of the host (PC). If using the DNS or similar resolution system, it is the Fully Qualified Domain Name (FQDN) that caused the server to return a reply. If using a local hosts file, it is the first entry after the IP address.
h_aliases
Null-terminated array of alternate names.
h_addrtype
Type of address being returned.
h_length
Length of each address, in bytes.
h_addr_list
Null-terminated list of addresses for the host. Addresses are returned in network byte order. The macro h_addr is defined to be h_addr_list[0] for compatibility with older software.
struct hostent* FAR gethostbyname( const char* name ) 这个函数的恶心之处在于在Windows下,输入的参数不能是doted字符串IP地址,只能时域名,要不然工作不正常.Linux没有这样的限制。
struct HOSTENT* FAR gethostbyaddr( const char* addr, int len, int type ) 的恶心之处在于参数过于复杂且相互影响,各参数描述如下
addr
[in] Pointer to an address in network byte order. (一个描述地址的结构转化为字符串指针)
len
[in] Length of the address, in bytes. (地址结构的长度,不同的长度代表不同的地址类型)
type
[in] Type of the address, such as the AF_INET address family type (defined as TCP, UDP, and other associated Internet protocols). Address family types and their corresponding values are defined in the Winsock2.h header file. (地址组类型,到现在位置,我页不知道到底有多少地址类型)
这三个恶心的参数导致了这个函数强大的扩展能力。
整形IP地址 unsigned long addr = inet_addr("192.168.1.39"); struct hostend* remoteHost = gethostbyaddr((char *) &addr, sizeof(unsigned long)(4), AF_INET);
sockaddr型的地址: struct sockaddr addr; struct hostend* remoteHost = gethostbyaddr((char *) &addr, sizeof(struct sockaddr)(16), addr.sa_family);

我们说道sockaddr结构了,他的定义如下:
struct sockaddr {
unsigned short sa_family;
char sa_data[14];
};
这个结构也不太稳定,容易变异. 如果是internet地址组,也就是sa_family==AF_INET的情况下,这个结构变成
struct sockaddr_in{
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
上面的结构里出现了结构in_addr, 怎么,忘了,到菜鸟级再去看看!另外sockaddr类的结构中包含了应用成信息,有个端口号! hostent 结构里的 short h_addrtype; short h_length; char FAR FAR** h_addr_list; 直接受到gethostbyaddr的三个参数的控制!
下面还有几个我没有用过的,也不知道Linux下是否对应!
struct sockaddr_in6 {
short sin6_family;
u_short sin6_port;
u_long sin6_flowinfo;
struct in6_addr sin6_addr;
u_long sin6_scope_id;
};
struct sockaddr_in6_old {
short sin6_family;
u_short sin6_port;
u_long sin6_flowinfo;
struct in6_addr sin6_addr;
};

struct in6_addr {
union {
u_char Byte[16];
u_short Word[8];
} u;
};
typedef struct _SOCKADDR_IRDA {
u_short irdaAddressFamily;
u_char irdaDeviceID[4];
char irdaServiceName[25];
} SOCKADDR_IRDA,
typedef struct sockaddr_storage {
short ss_family;
char __ss_pad1[_SS_PAD1SIZE];
__int64 __ss_align;
char __ss_pad2[_SS_PAD2SIZE];
} SOCKADDR_STORAGE,
gethostbyname 和 gethostbyaddr在Windows下已经被deprecated,用getaddrinfo 代替!我们进入入第三个阶段来看看这个函数!

进阶级:
先来看看下面的函数与结构
int getaddrinfo( const TCHAR* nodename, const TCHAR* servname, const struct addrinfo* hints, struct addrinfo** res );
nodename
[in] Pointer to a null-terminated string containing a host (node) name or a numeric host address string. The numeric host address string is a dotted-decimal IPv4 address or an IPv6 hex address.
servname
[in] Pointer to a null-terminated string containing either a service name or port number.
hints
[in] Pointer to an addrinfo structure that provides hints about the type of socket the caller supports. See Remarks.
res
[out] Pointer to a linked list of one or more addrinfo structures containing response information about the host.
typedef struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
TCHAR* ai_canonname;
struct sockaddr* ai_addr;
struct addrinfo* ai_next;
} addrinfo;

ai_flags
Flags that indicate options used in the getaddrinfo function. See AI_PASSIVE, AI_CANONNAME, and AI_NUMERICHOST.
ai_family
Protocol family, such as PF_INET.
ai_socktype
Socket type, such as SOCK_RAW, SOCK_STREAM, or SOCK_DGRAM.
ai_protocol
Protocol, such as IPPROTO_TCP or IPPROTO_UDP. For protocols other than IPv4 and IPv6, set ai_protocol to zero.
ai_addrlen
Length of the ai_addr member, in bytes.
ai_canonname
Canonical name for the host.
ai_addr
Pointer to a sockaddr structure.
ai_next
Pointer to the next structure in a linked list. This parameter is set to NULL in the last addrinfo structure of a linked list.

int getnameinfo( const struct sockaddr* sa, socklen_t salen, TCHAR* host, DWORD hostlen, TCHAR* serv, DWORD servlen, int flags );
sa
[in] Pointer to a socket address structure containing the address and port number of the socket. For IPv4, the sa parameter points to a sockaddr_in structure; for IPv6, the sa parameter points to a sockaddr_in6 structure.
salen
[in] Length of the structure pointed to in the sa parameter, in bytes.
host
[out] Pointer to the host name. The host name is returned as a Fully Qualified Domain Name (FQDN) by default.
hostlen
[in] Length of the buffer pointed to by the host parameter, in bytes. The caller must provide a buffer large enough to hold the host name, including terminating NULL characters. A value of zero indicates the caller does not want to receive the string provided in host.
serv
[out] Pointer to the service name associated with the port number.
servlen
[in] Length of the buffer pointed to by the serv parameter, in bytes. The caller must provide a buffer large enough to hold the service name, including terminating null characters. A value of zero indicates the caller does not want to receive the string provided in serv.
flags
[in] Flags used to customize processing of the getnameinfo function. See Remarks.



void freeaddrinfo( struct addrinfo* ai );
ai
[in] Pointer to the addrinfo structure or linked list of addrinfo structures to be freed. All dynamic storage pointed to within the addrinfo structure(s) is also freed.
----------------------------------------------------------
...

//--------------------------------
// Declare and initialize variables.
char* ip = "127.0.0.1";
char* port = "27015";
struct addrinfo aiHints;
struct addrinfo *aiList = NULL;
int retVal;

//--------------------------------
// Setup the hints address info structure
// which is passed to the getaddrinfo() function
memset(&aiHints, 0, sizeof(aiHints));
aiHints.ai_family = AF_INET;
aiHints.ai_socktype = SOCK_STREAM;
aiHints.ai_protocol = IPPROTO_TCP;

//--------------------------------
// Call getaddrinfo(). If the call succeeds,
// the aiList variable will hold a linked list
// of addrinfo structures containing response
// information about the host
if ((retVal = getaddrinfo(ip, port, &aiHints, &aiList)) != 0) {
printf("getaddrinfo() failed.n");
}
------------------------------------------------------------
...
//-----------------------------------------
// Declare and initialize variables
struct sockaddr_in saGNI;
char hostName[256];
char servInfo[256];
u_short port;
port = 27015;

//-----------------------------------------
// Set up sockaddr_in structure which is passed
// to the getnameinfo function
saGNI.sin_family = AF_INET;
saGNI.sin_addr.s_addr = inet_addr(ip);
saGNI.sin_port = htons(port);

//-----------------------------------------
// Call getnameinfo
if ((retVal = getnameinfo((SOCKADDR *)&saGNI,
sizeof(sockaddr),
hostName,
256,
servInfo,
256,
NI_NUMERICSERV)) != 0) {
printf("getnameinfo() failed.n");
printf("Error #: %ldn", WSAGetLastError());
}

这几个函数就比较高级,与前面两级的的函数相比,已经上升到应用而不是停留在网络,而且用户自主管理内存,这样的好处是支持可重入的并发使用,到了这个阶段,也不需要多说了,手册就在那里,自己看了!
下面在在第一二阶段的函数基础上对getaddrinfo的简单实现!作为对此文的总结!
int
fake_getaddrinfo(const char *hostname, const char* strport, const struct addrinfo* hints,struct addrinfo **ai)
{
struct hostent *he;
int addr = inet_addr(hostname);
struct sockaddr_in *saddr;
*ai = (struct addrinfo *)malloc(sizeof(struct addrinfo));
/*
The gethostbyname function cannot resolve IP address strings passed to it.
Such a request is treated exactly as if an unknown host name were passed.
Use inet_addr to convert an IP address string the string to an actual IP address,
then use another function, gethostbyaddr, to obtain the contents of the hostent structure.
*/
if(addr==INADDR_NONE)
{
he = gethostbyname(hostname);
if (!he)
return (-1);
ai->ai_family = hints->ai_family;
ai->ai_socktype = hints->ai_socktype; //SOCK_RAW, SOCK_STREAM, or SOCK_DGRAM.
ai->ai_protocol = hints->ai_proocol; //IPPROTO_TCP or IPPROTO_UDP or 0
ai->ai_canonname = hostname;
ai->ai_addrlen = sizeof(struct sockaddr);
if (NULL == (ai->ai_addr = malloc(ai->ai_addrlen)))
return (-1);
saddr = (struct sockaddr_in*)ai->ai_addr;
memset(saddr,0,sizeof(struct sockaddr_in));
saddr->sin_family = AF_INET;
saddr->sin_port = atoi(strport);
memcpy(&saddr->sin_addr, he->h_addr, sizeof(struct in_addr));
ai->ai_next = NULL;
}else
{
he = gethostbyaddr(&addr, sizeof(int), AF_INET );
if (!he)
return (-1);
ai->ai_family = he->h_addrtype;
ai->ai_socktype = hints->ai_socktype; //SOCK_RAW, SOCK_STREAM, or SOCK_DGRAM.
ai->ai_protocol = hints->ai_proocol; //IPPROTO_TCP or IPPROTO_UDP or 0
ai->ai_canonname = he->h_name;
ai->ai_addrlen = sizeof(struct sockaddr);
if (NULL == (ai->ai_addr = malloc(ai->ai_addrlen)))
return (-1);
saddr = (struct sockaddr_in*)ai->ai_addr;
memset(saddr,0,sizeof(struct sockaddr_in));
saddr->sin_family = AF_INET;
saddr->sin_port = atoi(strport);
memcpy(&saddr->sin_addr, he->h_addr, sizeof(struct in_addr));
ai->ai_next = NULL;
}
return (0);
}

void fake_freeaddrinfo(struct addrinfo *ai)
{
free(ai->ai_addr);
free(ai);
}

星期四, 四月 19, 2007

C语言 结构对齐

在上次的一次结构设计中招到PK后,回家恶补了结构对齐,参考了多人的Blog和一些专业论坛,由于原始内容比较乱且我经过重新整理,就不给出下面内容的原始出处。

什么是内存对齐?

考虑下面的结构:
struct foo
{
char c1;
short s;
char c2;
int i;
};

假设这个结构的成员在内存中是紧凑排列的,假设c1的地址是0,那么s的地址就应该是1,c2的地址就是3,i的地址就是4。也就是
c1 00000000, s 00000001, c2 00000003, i 00000004。

可是,我们在Visual c/c++ 6中写一个简单的程序:

struct foo a;
printf("c1 %p, s %p, c2 %p, i %pn",
(unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.i - (unsigned int)(void*)&amp;a);
运行,输出:
c1 00000000, s 00000002, c2 00000004, i 00000008。

为什么会这样?这就是内存对齐而导致的问题。

在结构中,编译器为结构的每个成员按其自然对界(alignment)条件分配空间;各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。

例如,下面的结构各成员空间分配情况
struct test
{
char x1; //0
short x2; //2
float x3; //4
char x4; //8
};
结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然对界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大对界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。

为什么会有内存对齐?


以下内容节选自《Intel Architecture 32 Manual》。
字,双字,和四字在自然边界上不需要在内存中对齐。(对字,双字,和四字来说,自然边界分别是偶数地址,可以被4整除的地址,和可以被8整除的地址。)
无论如何,为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。
一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期中被访问。
某些操作双四字的指令需要内存操作数在自然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用保护异常(#GP)。双四字的自然边界是能够被16整除的地址。其他的操作双四字的指令允许未对齐的访问(不会产生通用保护异常),然而,需要额外的内存总线周期来访问内存中未对齐的数据。

编译器对内存对齐的处理


缺省情况下,c/c++编译器默认将结构、栈中的成员数据进行内存对齐。因此,
struct foo
{
char c1;
short s;
char c2;
int i;
};
用上面的程序输出就变成了:
c1 00000000, s 00000002, c2 00000004, i 00000008。
编译器将未对齐的成员向后移,将每一个都成员对齐到自然边界上,从而也导致了整个结构的尺寸变大。尽管会牺牲一点空间(成员之间有空洞),但提高了性能。
也正是这个原因,我们不可以断言sizeof(foo) == 8。在这个例子中,sizeof(foo) == 12。

如何避免内存对齐的影响


那么,能不能既达到提高性能的目的,又能节约一点空间呢?有一点小技巧可以使用。比如我们可以将上面的结构改成:

struct bar
{
char c1;
char c2;
short s;
int i;
};
这样一来,每个成员都对齐在其自然边界上,从而避免了编译器自动对齐。在这个例子中,sizeof(bar) == 8。

这个技巧有一个重要的作用,尤其是这个结构作为API的一部分提供给第三方开发使用的时候。第三方开发者可能将编译器的默认对齐选项改变,从而造成这个结构在你的发行的DLL中使用某种对齐方式,而在第三方开发者哪里却使用另外一种对齐方式。这将会导致重大问题。

比如,foo结构,我们的DLL使用默认对齐选项,对齐为
c1 00000000, s 00000002, c2 00000004, i 00000008,同时sizeof(foo) == 12。
而第三方将对齐选项关闭,导致
c1 00000000, s 00000001, c2 00000003, i 00000004,同时sizeof(foo) == 8。

如何使用c/c++中的对齐选项


编译器默认都是8字节对齐,更改C编译器的缺省分配策略,一般地,可以通过下面的两种方法改变缺省的对界条件:
  · 使用伪指令#pragma pack ([n])
  · 在编译时使用命令行参数
#pragma pack ([n])伪指令允许你选择编译器为数据分配空间所采取的对界策略:
  
例如,在使用了#pragma pack (1)伪指令后,test结构各成员的空间分配情况就是按照一个字节对齐了
#pragma pack(push) //保存对齐状态
#pragma pack(1)
#pragma pack(pop)
vc6中的编译选项有 /Zp[1|2|4|8|16] ,/Zp1表示以1字节边界对齐,相应的,/Zpn表示以n字节边界对齐。n字节边界对齐的意思是说,一个成员的地址必须安排在成员的尺寸的整数倍地址上或者是n的整数倍地址上,取它们中的最小值。也就是:
min ( sizeof ( member ), n)
实际上,1字节边界对齐也就表示了结构成员之间没有空洞。
/Zpn选项是应用于整个工程的,影响所有的参与编译的结构。
要使用这个选项,可以在vc6中打开工程属性页,c/c++页,选择Code Generation分类,在Struct member alignment可以选择。

要专门针对某些结构定义使用对齐选项,可以使用#pragma pack编译指令。指令语法如下:
#pragma pack( [ show ] | [ push | pop ] [, identifier ] , n )
意义和/Zpn选项相同。比如:

#pragma pack(1)
struct foo_pack
{
char c1;
short s;
char c2;
int i;
};
#pragma pack()

栈内存对齐


我们可以观察到,在vc6中栈的对齐方式不受结构成员对齐选项的影响。(本来就是两码事)。它总是保持对齐,而且对齐在4字节边界上。验证代码如下:

#include <stdio.h>

struct foo
{
char c1;
short s;
char c2;
int i;
};

struct bar
{
char c1;
char c2;
short s;
int i;
};

#pragma pack(1)
struct foo_pack
{
char c1;
short s;
char c2;
int i;
};
#pragma pack()


int main(int argc, char* argv[])
{
char c1;
short s;
char c2;
int i;

struct foo a;
struct bar b;
struct foo_pack p;

printf("stack c1 %p, s %p, c2 %p, i %pn",
(unsigned int)(void*)&c1 - (unsigned int)(void*)&i,
(unsigned int)(void*)&s - (unsigned int)(void*)&i,
(unsigned int)(void*)&c2 - (unsigned int)(void*)&i,
(unsigned int)(void*)&i - (unsigned int)(void*)&amp;i);

printf("struct foo c1 %p, s %p, c2 %p, i %pn",
(unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.i - (unsigned int)(void*)&amp;a);

printf("struct bar c1 %p, c2 %p, s %p, i %pn",
(unsigned int)(void*)&b.c1 - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.c2 - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.s - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.i - (unsigned int)(void*)&amp;b);

printf("struct foo_pack c1 %p, s %p, c2 %p, i %pn",
(unsigned int)(void*)&p.c1 - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.s - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.c2 - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.i - (unsigned int)(void*)&amp;p);

printf("sizeof foo is %dn", sizeof(foo));
printf("sizeof bar is %dn", sizeof(bar));
printf("sizeof foo_pack is %dn", sizeof(foo_pack));

return 0;
}


内存对齐与ANSI C中struct型数据的内存布局

  当在C中定义了一个结构类型时,它的大小是否等于各字段(field)大小之和?编译器将如何在内存中放置这些字段?ANSI C对结构体的内存布局有什么要求?而我们的程序又能否依赖这种布局?这些问题或许对不少朋友来说还有点模糊,那么本文就试着探究它们背后的秘密。
  
  首先,至少有一点可以肯定,那就是ANSI C保证结构体中各字段在内存中出现的位置是随它们的声明顺序依次递增的,并且第一个字段的首地址等于整个结构体实例的首地址。比如有这样一个结构体:
  
  struct vector{int x,y,z;} s;
  int *p,*q,*r;
  struct vector *ps;
  
  p = &s.x;
  q = &s.y;
  r = &s.z;
  ps = &s;
  
  assert(p < q);
  assert(p < r);
  assert(q < r);
  assert((int*)ps == p);
  // 上述断言一定不会失败
  
  这时,有朋友可能会问:"标准是否规定相邻字段在内存中也相邻?"。 唔,对不起,ANSI C没有做出保证,你的程序在任何时候都不应该依赖这个假设。那这是否意味着我们永远无法勾勒出一幅更清晰更精确的结构体内存布局图?哦,当然不是。不过先让我们从这个问题中暂时抽身,关注一下另一个重要问题————内存对齐。
  
  许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),而称T比S弱(宽松)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。某些处理器在数据不满足对齐要求的情况下可能会出错,但是Intel的IA32架构的处理器则不管数据是否对齐都能正确工作。不过Intel奉劝大家,如果想提升性能,那么所有的程序数据都应该尽可能地对齐。Win32平台下的微软C编译器(cl.exe for 80x86)在默认情况下采用如下的对齐规则: 任何基本数据类型T的对齐模数就是T的大小,即sizeof(T)。比如对于double类型(8字节),就要求该类型数据的地址总是8的倍数,而char类型数据(1字节)则可以从任何一个地址开始。Linux下的GCC奉行的是另外一套规则(在资料中查得,并未验证,如错误请指正):任何2字节大小(包括单字节吗?)的数据类型(比如short)的对齐模数是2,而其它所有超过2字节的数据类型(比如long,double)都以4为对齐模数。
  
  现在回到我们关心的struct上来。ANSI C规定一种结构类型的大小是它所有字段的大小以及字段之间或字段尾部的填充区大小之和。嗯?填充区?对,这就是为了使结构体字段满足内存对齐要求而额外分配给结构体的空间。那么结构体本身有什么对齐要求吗?有的,ANSI C标准规定结构体类型的对齐要求不能比它所有字段中要求最严格的那个宽松,可以更严格(但此非强制要求,VC7.1就仅仅是让它们一样严格)。我们来看一个例子(以下所有试验的环境是Intel Celeron 2.4G + WIN2000 PRO + vc7.1,内存对齐编译选项是"默认",即不指定/Zp与/pack选项):
  
  typedef struct ms1
  {
  char a;
  int b;
  } MS1;
  
  假设MS1按如下方式内存布局(本文所有示意图中的内存地址从左至右递增):

  _____________________________

  |    |          |

  |  a  |    b     |

  |    |          |

  +---------------------------+

  Bytes:  1       4

  
  因为MS1中有最强对齐要求的是b字段(int),所以根据编译器的对齐规则以及ANSI C标准,MS1对象的首地址一定是4(int类型的对齐模数)的倍数。那么上述内存布局中的b字段能满足int类型的对齐要求吗?嗯,当然不能。如果你是编译器,你会如何巧妙安排来满足CPU的癖好呢?呵呵,经过1毫秒的艰苦思考,你一定得出了如下的方案:
  
  _______________________________________   |    |\\\\\|         |   |  a  |\padding\|    b     |   |    |\\\\\|         |   +-------------------------------------+
  Bytes:  1     3       4
  
  这个方案在a与b之间多分配了3个填充(padding)字节,这样当整个struct对象首地址满足4字节的对齐要求时,b字段也一定能满足int型的4字节对齐规定。那么sizeof(MS1)显然就应该是8,而b字段相对于结构体首地址的偏移就是4。非常好理解,对吗?现在我们把MS1中的字段交换一下顺序:
  
typedef struct ms2
{
  int a;
  char b;
} MS2;
  
  或许你认为MS2比MS1的情况要简单,它的布局应该就是
  
  _______________________   |       |    |   |   a    |  b  |   |       |    |   +---------------------+   Bytes:   4      1
  
  因为MS2对象同样要满足4字节对齐规定,而此时a的地址与结构体的首地址相等,所以它一定也是4字节对齐。嗯,分析得有道理,可是却不全面。让我们来考虑一下定义一个MS2类型的数组会出现什么问题。C标准保证,任何类型(包括自定义结构类型)的数组所占空间的大小一定等于一个单独的该类型数据的大小乘以数组元素的个数。换句话说,数组各元素之间不会有空隙。按照上面的方案,一个MS2数组array的布局就是:
  
  |<-  array[1]   ->|<-  array[2]   ->|<- array[3] .....      __________________________________________________________   |       |    |       |   |   |   a    |  b  |   a    |  b |.............   |       |    |       |   |   +----------------------------------------------------------   Bytes: 4     1     4      1
  
  当数组首地址是4字节对齐时,array[1].a也是4字节对齐,可是array[2].a呢?array[3].a ....呢?可见这种方案在定义结构体数组时无法让数组中所有元素的字段都满足对齐规定,必须修改成如下形式:
  
  ___________________________________   |       |    |\\\\\|   |   a    |  b  |\padding\|   |       |    |\\\\\|   +---------------------------------+   Bytes:   4      1     3
  
  现在无论是定义一个单独的MS2变量还是MS2数组,均能保证所有元素的所有字段都满足对齐规定。那么sizeof(MS2)仍然是8,而a的偏移为0,b的偏移是4。
  
  好的,现在你已经掌握了结构体内存布局的基本准则,尝试分析一个稍微复杂点的类型吧。
  
typedef struct ms3
{
  char a;
  short b;
  double c;
} MS3;
  
  我想你一定能得出如下正确的布局图:
  
  padding   |   _____v_________________________________   |  ||   |\\\\|        |   | a || b |padding|    c    |   |  ||   |\\\\|        |   +-------------------------------------+   Bytes: 1 1  2    4      8
  
  sizeof(short)等于2,b字段应从偶数地址开始,所以a的后面填充一个字节,而sizeof(double)等于8,c字段要从8倍数地址开始,前面的a、b字段加上填充字节已经有4 bytes,所以b后面再填充4个字节就可以保证c字段的对齐要求了。sizeof(MS3)等于16,b的偏移是2,c的偏移是8。接着看看结构体中字段还是结构类型的情况:
  
typedef struct ms4 {   char a;   MS3 b; } MS4;
  
  MS3中内存要求最严格的字段是c,那么MS3类型数据的对齐模数就与double的一致(为8),a字段后面应填充7个字节,因此MS4的布局应该是:
  _______________________________________   |    |\\\\\|         |   |  a  |\padding\|    b     |   |    |\\\\\|         |   +-------------------------------------+   Bytes:  1     7       16
  
  显然,sizeof(MS4)等于24,b的偏移等于8。
  
  在实际开发中,我们可以通过指定/Zp编译选项来更改编译器的对齐规则。比如指定/Zpn(VC7.1中n可以是1、2、4、8、16)就是告诉编译器最大对齐模数是n。在这种情况下,所有小于等于n字节的基本数据类型的对齐规则与默认的一样,但是大于n个字节的数据类型的对齐模数被限制为n。事实上,VC7.1的默认对齐选项就相当于/Zp8。仔细看看MSDN对这个选项的描述,会发现它郑重告诫了程序员不要在MIPS和Alpha平台上用/Zp1和/Zp2选项,也不要在16位平台上指定/Zp4和/Zp8(想想为什么?)。改变编译器的对齐选项,对照程序运行结果重新分析上面4种结构体的内存布局将是一个很好的复习。

星期五, 二月 02, 2007

gcc的语法检查

在编译mod_lua 0.5的过程中,下面的代码出现警告
LuaState *L;
void** data = (void**) &L;
warning: dereferencing type-punned pointer will break strict-aliasing rules

-------------
gcc version 4.1.1 20061011 (Red Hat 4.1.1-30)


somebody say that:

Yes, by implying -fstrict-aliasing, so using -fno-strict-aliasing is a
workaround. The problem is caused by the i386 PCPU_GET/PCPU_SET
implementation:

#define __PCPU_GET(name) ({ \
__pcpu_type(name) __result; \
\
[...]
} else if (sizeof(__result) == 4) { \
u_int __i; \
__asm __volatile("movl %%fs:%1,%0" \
: "=r" (__i) \
: "m" (*(u_int *)(__pcpu_offset(name)))); \
__result = *(__pcpu_type(name) *)&__i; \
[...]

In this case, the PCPU_GET is used to retrieve curthread, causing
sizeof(__result) to be 4, so the cast at the end of the code snippet
is from a u_int * to struct thread *, and __i is accessed through the
casted pointer, which violates the C99 aliasing rules.
An alternative is to type-pun via a union, which is also a bit ugly,
but explicitly allowed by C99. Patch attached (but only superficially
tested).