*c程序员之路 选学篇-1 深入理解字节,字节序与字节对齐

时间:2023-01-11 17:54:34

 

                         深入理解字节,字节序与字节对齐

一 总述

   作为一个职业的coder玩家,首先应该对计算机的字节有所了解。

   我们经常谈到的2进制流,字节(字符)流,数据类型流(针对编程),结构流等说法,2进制流,01的操作,属于cpu级。从字符流向上都是我们玩家关心,字节流属于操作系统级。今天谈的就是字节流操作。

 

二 字节

   因为计算机用二进制,所以希望基本存储单位的是2的n次方(应该和硬件有关)。  这样读取字节的时候,开销不会太高,可以达到最大性能,因为刚开始,计算机是美国发明的,西文字符(英文字母大小写,数字,其他特殊字符等将近有1百多个,所以用7位来表示,这样可以把所有西文字符表达完,再加上一位校检位,一共8位,由于ASCⅡ的广泛应用,所有后来,一个字节占8位就成了国际规定的标准了,一直沿用至今(有待研究,但不是今天的主题)。

   一个字节占8个2进制位,数据类型流,就是在c语言里面的数据类型占多少个字节。然后直接操作数据类型。在目前的32位系统中,c语言的基本数据类型有以下几种:

Char  占一个字节 (-2^7 - 2^7-1 ,-128 到 127)最高位 为符号位

Unsigned char 占一个字节   (0-2^8,0到255)

Short   占2个字节 (-2^15 - 2^15-1 , -32768到32767)最高位 为符号位

Unsigned short 占2个字节 (0 - 2^16 , 0到65536)

Int (字长,对于32位机为32位,16位机为16位,长度不固定,和系统平台有关,处理器位数有关,代表寻址空间),在32位机占4个字节(-2^31-2^31 )最高位 为符号位

Unsigned  int 占4个字节(0-2^32 )

Long int 为4个字节,在16,32位机都占4个字节(-2^31-2^31 )最高位 为符号位

Unsigned  long int占4个字节(0-2^32 )

sizeof(short) <= sizeof(int) <= sizeof(long)  

在32位机 int和long都是32位,没有什么大的区别,但还是有些小的区别,有时最好用long,他大小固定,如果到其他平台,比如64位,他还是占4个字节,而int却占8个字节,可以增加代码的可移植性。

Long long int 占8个字节(c99标准)  

Unsigned long long int 占8个字节(c99标准)  

浮点类型 由于浮点类型和整形的编码不一样,所以浮点型需要特殊分析。

Float 占4个字节  

Double 占8个字节 

深入理解浮点类型,请看这边文章:

http://blog.csdn.net/caotiancool/archive/2005/04/29/368375.aspx 

主要是这不是这篇文章的重点。

进入今天的主题

三 字节序

  为什么有字节序这个概念存在呢?

不同的CPU有不同的字节序类型 这些字节序是指整数在内存中保存的顺序 这个叫做主机序 ,就是多个字节在内存中摆放位置顺序和解释顺序。
最常见的有两种 :

1. Little endian:将低序字节存储在起始地址 4321


2. Big endian:将高序字节存储在起始地址    1234
以前还有什么Middle endian,就乱序,也被淘汰。


LE little-endian 
最符合人的思维的字节序 
地址低位存储值的低位 
地址高位存储值的高位 
怎么讲是最符合人的思维的字节序,是因为从人的第一观感来说 
低位值小,就应该放在内存地址小的地方,也即内存地址低位 
反之,高位值就应该放在内存地址大的地方,也即内存地址高位 

BE big-endian 
最直观的字节序 
地址低位存储值的高位 
地址高位存储值的低位 
为什么说直观,不要考虑对应关系 
只需要把内存地址从左到右按照由低到高的顺序写出 
把值按照通常的高位到低位的顺序写出 
两者对照,一个字节一个字节的填充进去 

例子:在内存中双字0x01020304(DWORD)的存储方式 

内存地址 
4000 4001 4002 4003 
LE 04 03 02 01 
BE 01 02 03 04 

例子:如果我们将0x1234abcd写入到以0x0000开始的内存中,则结果为
      big-endian   little-endian
0x0000   0x12       0xcd
0x0001   0x23       0xab
0x0002   0xab       0x34
0x0003   0xcd       0x12
x86系列CPU都是little-endian的字节序. 

网络字节顺序是TCP/IP协议栈中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。

为了进行转换 bsd socket提供了转换的函数 有下面四个

linux的源代码(/include/netinet/in.h)

# if __BYTE_ORDER == __BIG_ENDIAN 
/* The host byte order is the same as network byte order, 
   so these functions are all just identity.  */ 
# define ntohl(x) (x) 
# define ntohs(x) (x) 
# define htonl(x) (x) 
# define htons(x) (x) 
# else 
#  if __BYTE_ORDER == __LITTLE_ENDIAN 
#   define ntohl(x) __bswap_32 (x) 
#   define ntohs(x) __bswap_16 (x) 
#   define htonl(x) __bswap_32 (x) 
#   define htons(x) __bswap_16 (x) 
#  endif 
# endif


htons 把unsigned short类型从主机序转换到网络序
htonl 把unsigned long类型从主机序转换到网络序
ntohs 把unsigned short类型从网络序转换到主机序
ntohl 把unsigned long类型从网络序转换到主机序

在使用little endian的系统中 这些函数会把字节序进行转换 
在使用big endian类型的系统中 这些函数会定义成空宏

 都是4个宏函数:

 分析其中1个,htonl(x)我简化为下:

  #define  htonl(x)  \  //连接符,连接下一行

  ((unsigned long ) \

( \

(((unsigned long)(x)&0x000000ff<<24)| \

(((unsigned long)(x)&0x0000ff00)<<8)| \

(((unsigned long)(x)&0x00ff0000)>>8)| \

(((unsigned long)(x)&0xff000000)>>24)\

))

这样写是方便好看一点,宏的最后最好是用一个括号括起来,或者用do{}while0)包起来。

这样可以防止宏嵌套产生意想不到问题等,如果自己熟悉了就可以简洁一点。上面那个宏的作用是把4321变成1234(字节摆放顺序)。


一般c语言编写程序的字节序都是系统相关的(java的字节码是big-endian 和网络字节序一样,所以他和网络通信不需要关心字节序问题),如果要和其他平台进行通信,都要进行字节序转换,跨平台开发时也应该注意保证只用一种字节序 不然两方的解释不一样就会产生bug。

网络上流传一个测试自己系统是什么字节序函数代码:

byte_type get_sys_byte_order()

{

     union

    {

         int  b;

         char a[4];

     }U;

     U.b = 0x01;

     if(0x01 == U.a[0] )

     {

         return   little_endian_type;

      }

    else

      {

         return   big_endian_type;

      } 
} 
注:
1、网络与主机字节转换函数:htons ntohs htonl ntohl (s 就是short l是long h是host n是network)
2、不同的CPU上运行不同的操作系统,字节序也是不同的,参见下表。
处理器         操作系统     字节排序
Alpha            全部     Little endian
HP-PA             NT      Little endian
HP-PA            UNIX     Big endian
Intelx86         全部     Little endian <-----x86系统是小端字节序系统
Motorola680x()   全部     Big endian
MIPS              NT      Little endian
MIPS             UNIX     Big endian
PowerPC           NT      Little endian
PowerPC          非NT     Big endian   <-----PPC系统是大端字节序系统
RS/6000          UNIX     Big endian
SPARC            UNIX     Big endian
IXP1200 ARM核心  全部     Little endian 

四 字节对齐

首先我看哈程序的优化种类:

Cpu级优化((读内存),流水线,cache,现在多核等)->2进制级(即01代码)优化(现在估计没有人去做了)->汇编级(指令)优化->高级程序里面的代码级优化(位运算,前++和后++,数组和指针,if else和switch case等优化)->算法优化(流程优化)->软件架构级优化......

  而现在我们讨论的字节对齐属于cpu级优化,可以加速cpu读取内存时间。

什么是字节对齐:

现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。 
    对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误(比如高通平台,一般的手机平台都采用美国高通公司开发平台,对于无线上网卡来说,现在都是多核,要么是arm9+arm11,要么是arm9+2QdspQ是表示高通的dsp处理器)等处理器架构,然而在Qdsp中,如果访问了非对齐的内存,就会直接发生错误,直接把系统crush掉)那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那 么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。 

一般的规则是,

1 对于基本数据类型对齐要求:

Char 类型一个字节对齐,可以从任何内存地址读取;

Short2个字节,要求是从偶内存地址读取;

Int 4个字节(32位系统),要求是变量的内存地址必须被4整除;

Longint一样(在32位系统中)

Double 8个字节,要求是变量的内存地址必须被8整除。

c语言中存在structunion结构体类型,属于复杂类型:

2  对于structunion对齐要求是:

其成员中自身对齐值最大的那个值 。

注:结构的总大小为结构的字节边界数(即该结构中占用最大空间的变量的类型所占用的字节数)的倍数,对于结构体最终大小,还要参考,指定对齐值n

1)对于struct我们有:

s表示这个结构体,X(i)表示其第i个成员(按顺序从上往下)所占的大小;假使结构体有m个成员;

结构体的最终对齐值  

  其中基本类型对齐值:

  A(char) = 1;

  A(short) = 2;

  A(int) = 4;

  A(long) = 4;

  A(float) = 4;

  A(double) = 8;

 

    基本类型成员对齐值 Ax) = min(A(type),n );    公式1

A(type) 为基本类型成员对应的基本类型。

     A(s) = min(max(A(1),A(2),...,A(m)),n)       公式2

(这个一个递归函数,主要原因是成员Xi有可能也是一个结构体,嵌套类型)

 其中:

 由规则1可以得到公式 2的初始值

  struct结构体最终占内存大小Xs

  定义H(x)表示前面x个成员最终占内存大小;A(x)表示第x个成员的对齐值。可以有公式1给出,则:

If(H(x)%A(x+1) == 0)

  H(x+1)= H(x)+X(x+1);

Else

  H(x+1)= H(x)+A(x+1)-H(x)%A(x+1)+X(x+1); 

 

其中 H(1) = X1,A(1)= X1;  

                                    公式3

 

则:

IfH(m)%A(s) == 0)

   Xs = H(m);

Else

   Xs = H(m)+A(s)-H(m)%A(s); 

 

                                    公式

2)对于union我们有:

     U表示这个结构体,Xi表示其第i个成员(按顺序从上往下)所占的大小;假使结构体有m个成员;

      结构体的最终对齐值  A(u) = min(max(A(1),A(2),...,A(m)),n) 公式5

Union结构体最终占内存大小Xu

   定义H(x)表示前面x个成员最终占内存大小;A(x)表示第x个成员的对齐值。可以有公式1给出,则:

  H(x+1= max(X(x+1), H(x));

其中 H(1) = X1;

 

                                    公式6

   则:

IfH(m)%A(u) == 0)

   Xu = H(m);

Else

   Xu = H(m)+A(u)-H(m)%A(u); 

 

                                    公式

 

3  指定对齐值:

     如果我们想指定对齐值,可以在VC IDE中,可以这样修改:[Project]|[Settings],c/c++选项卡CategoryCode Generation选项的Struct Member Alignment中修改,默认是8字节,针对全部变量,如果想动态改变部分,在vc中可以用宏命令 #pragma pack (n)时的指定对齐值n,#pragma pack()取消,之间的数据都是指定为n,但不一定为对齐n 最终的数据成员对齐值为: 自身对齐值和指定对齐值中小的那个值 。(这个很重要,请好好理解)。

 

4  结构中带有结构
    不必考虑整个子结构,只考虑子结构的基本类型并参照前面的规则来分配空间。
 空结构(即不带任何的方法和数据)占用的1字节的空间。

5  枚举中(enum 
   枚举始终占用4字节的空间。

 

6  结构中成员

    结构的静态成员不对结构的大小产生影响,因为静态变量的存储位置与结构的实例地址无关。  

要理解上面的对齐规则,最好是分析一些典型的对齐例子:

例子1

   struct MyStruct 

  char dda; 
  double dda1; 
  int type 

}; 

 

默认指定对齐值n = 8(vc),其他自己查看,n = 8

由公式得到此结构体的最终对齐值为 A(s) = 8

有上面公式2 ,3 可以得到 X(MyStruct)(=sizeof(MyStruct)) :

H(1) = 1, H(2) =(H(1)) 1+7+8 = 16; H(3) =(H(2)) 16+4 = 20;

X(s) = (H(3))20 + (A(s)-H(3)%A(s)) 4 = 24;

所以sizeofMyStruct) = 24

例子2

#pragma  pack2

 struct MyStruct 

  char dda;   A1) = 1 
  double dda1; A2) = 2 
  int type   A3) = 2 
}; 

#pragma  pack()

由公式1,2得到此结构体的最终对齐值为 A(s) = 2

有上面公式3 ,4 可以得到 X(MyStruct)(sizeof(MyStruct)) :

H(1) = 1, H(2) =(H(1)) 1+ (A(2)-H(1)%A(2)) 1+(X2)8 = 10; 因为H(2)%A(3)==0

所以 H(3) =(H(2)) 10+4 = 14;

因为H(3)%A(s) == 0;

所以X(s) = (H(3)) 14;

所以sizeofMyStruct) = 14;

例子3

   这里有个结构体嵌套例子,对于结构体中的结构体成员,不要认为它的对齐方式就是他的大小,看下面的例子:

struct s1
{
char a[8]; 
};
struct s2
{
double d; 
};
struct s3
{
s1 s; 
char a; 
};
struct s4
{
s2 s; 
char a; 
};
默认指定对齐值n = 8

As1) = 1A(s2) =min(min(A(double), 8),8) =8 ; 

A(s3) = min(max(A(x1)=min(A(s1) = 1,8) = 1,A(x2) = 1),8) = 1;

A(s3) = min(max(A(x1)=min(A(s2) = 8,8) = 8,A(x2) = 1),8) = 8;

X(s1) = 8;

X(s2) = 8;

X(s3) = 9;

X(s4) = 16;

具体过程自己可以分析一下;

 例子 4

#pragma pack(4)

union u1

{

double b; A(1)  = 4

int a;    A(2)  = 4

}x1;

#pragma pack()

#pragma pack(8)

union u2

{

char a[13]; A(1) = 1

double b;  A(2) = 8

}x2;

#pragma pack()

A(u1)  = min(max(A(1),A(2))=4,n=4) = 4;

A(u2)  =min(max(A(1),A(2))=8,n=8) = 8;

对于u1H(1) = 8; H(2) = max(H(1)8,X(2)4) = 8;

因为H(2)%A(u1) == 0;

所以 X(u1) = H(2) = 8;

对于u2H(1) = 13, H(2)=max(H(1),X(2)8) = 13;

因为 H(2)%A(u2) !=0;

所以 X(u2) = H(2) + A(u2)-H(2)%A(u2)= 13+3=16;

也上只是一些测试例子,真实的结构体都比较庞大,一般用sizeof就可以了,但心里要清楚,每个成员的偏移量和填充的字节,这些都可以由上面的公式推出来,我这里就暂时不推导了(但这个才是我们平时最应该注意的)。

五 总结

     对于字节的理解其实这些还不够,掌握这些只是作为*c程序员最基本的要求(路还很长),有细节需要参考一下cpu的手册。其实在实际编程当中,出现字节对齐的原因是通信的要求,不管是通过tcp/ip(互联网),这样一般协议头部都是一个字节对齐,这样对方解释的时候是只需按协议解析就正确了;或者是动态库调用,给别人的接口函数对应参数,如果是没有满足字节对齐的要求(不相同),如果进行强制类型转换或者按偏移量访问变量,就有可能出现错误(某些嵌入式cpu)或者意想不到问题,所以对于嵌入式开发的程序员最好是心里有数。

    最后请希望自己成为*c程序员的coders,来设计一个memcpy函数,为什么要设计这个函数呢,如果你看过很多大工程的代码,你就明白了,这个函数使用率相当高,strcpy这个的优化版本内部都是调用memcpy来完成,这是系统函数,每个平台自己都实现了这个函数,而且里面充满奇淫巧计,呵呵,看看自己会长见识。

以下是高通平台的
memcpy函数:

typedef  unsigned int uint_t;

typedef  unsigned char uchar_t;

void *

memcpy(void * d, const void *s,size_t n)

{

   size_t i;

   uint_t align = sizeof(uint_t ) -1;

   if( ((uint_t)d&align)| ((uint_t)s&align)| (n&align))

  {

     uchar_t  *dest = (uchar_t  *)d;

     uchar_t  *src = (uchar_t  *)s;

     for(i = 0; i < n; ++i)

     {

        *dest ++ = *src++;

     }

 }

 else

 {

     uint_t  *dest = (uint_t *)d;

     uint_t  *src = (uint_t *)d;

     n /=sizeof(uint_t );

     for(i = 1; i < n; ++i)

      {

        *dest ++ = *src++;

      }

 }

 return d;

}

 

注: 以上只是我学习中文档输出,可能会有错误(什么类型的错误都可能发生),我希望读者能帮我一起改正,希望大家一起努力,朝*c程序员前进,读者有什么意见都可以在回复中提出,我会继续的,就像我前一篇文章所说一样,目的把我学到的东西分享给大家,希望大家好好学