关于c:程序人生-C语言字节对齐问题详解-对齐字节序位序网络序等下

32次阅读

共计 10524 个字符,预计需要花费 27 分钟才能阅读完成。

本文首发于 2014-07-21 15:35:30

6. 附录

6.1. 字节序与网络序

6.1.1. 字节序

字节序 ,顾名思义就是 字节的高下位寄存程序

对于单字节,大部分处理器以雷同的程序解决比特位,因而单字节的寄存和传输方式个别雷同。

对于多字节数据,如整型(32 位机中个别占 4 字节),在不同的处理器的寄存形式次要有两种(以内存中 0x0A0B0C0D 的寄存形式为例)。

6.1.1.1. 大字节序(Big-Endian,又称大端序或大尾序)

在计算机中,存储介质以上面形式存储整数 0x0A0B0C0D,则称为 大字节序

数据以 8bit 为单位:
低地址方向 -> 0x0A 0x0B 0x0C 0x0D -> 高地址方向

数据以 16bit 为单位:
低地址方向 -> 0x0A0B 0x0C0D -> 高地址方向

其中,最高无效位(MSB,Most Significant Byte)0x0A 存储在最低的内存地址处。下个字节 0x0B 存在前面的地址处。同时,最高的 16bit 单元 0x0A0B 存储在低位。

简而言之,大字节序就是 高字节存入低地址,低字节存入高地址

这里讲个词源典故:“endian”一词来源于乔纳森·斯威夫特的小说《格列佛游记》。小说中,小人国为水煮蛋该从大的一端 (Big-End) 剥开还是小的一端 (Little-End) 剥开而争执,争执的单方别离被称为 Big-endians 和 Little-endians。

1980 年,Danny Cohen 在其驰名的论文 ”On Holy Wars and a Plea for Peace“ 中为平息一场对于字节该以什么样的程序传送的争执而援用了该词。

借用下面的典故,设想一下要把熟鸡蛋旋转着稳立起来,大头(高字节)必定在上面(低地址)^_^

6.1.1.2. 小字节序(Little-Endian,又称小端序或小尾序)

在计算机中,存储介质以上面形式存储整数 0x0A0B0C0D 则称为 小字节序

数据以 8bit 为单位:
高地址方向 -> 0x0A 0x0B 0x0C 0x0D -> 低地址方向

数据以 16bit 为单位:
高地址方向 -> 0x0A0B 0x0C0D -> 低地址方向

其中,最低无效位(LSB,Least Significant Byte)0x0D 存储在最低的内存地址处。前面字节顺次存在前面的地址处。同时,最低的 16bit 单元 0x0A0B 存储在低位。

可见,小字节序就 高字节存入高地址,低字节存入低地址

C 语言中的位域构造也要遵循 比特序(相似字节序)。例如:

struct bitfield{
    unsigned char a: 2;
    unsigned char b: 6;
}

该位域构造占 1 个字节,假如赋值 a =0x01 和 b =0x02,则大字节机器上该字节为(01)(000010),小字节机器上该字节为(000010)(01)。因而在编写可移植代码时,须要加条件编译。

留神,在蕴含位域的 C 构造中,若位域 A 在位域 B 之前定义,则位域 A 所占用的内存空间地址低于位域 B 所占用的内存空间

另见以下联合体,在小字节机器上若 low=0x01,high=0x02,则 hex=0x21:

int main(void){
    union{
        unsigned char hex;
        struct{
            unsigned char low  : 4;
            unsigned char high : 4;
        };
    }convert;
    convert.low = 0x01;
    convert.high = 0x02;
    printf("hex = 0x%0x\n", convert.hex);
    return 0;
}

6.1.1.3. 注意事项

无论是大字节序,还是小字节序,变量的地址都等于变量所占字节中的低地址。例如,下述程序中,小字节序输入 0x0D,大字节序输入 0x0A。

 int32_t a = 0x0A0B0C0D;
 printf("0x%0x\n", *((int8_t*)&dwData));

6.1.2. 网络序

网络传输个别采纳 大字节序 ,也称为 网络字节序 网络序。IP 协定中定义大字节序为网络字节序。

对于可移植的代码来说,将接管的网络数据转换成主机的字节序是必须的,个别会有成对的函数用于把网络数据转换成相应的主机字节序或反之(若主机字节序与网络字节序雷同,通常将函数定义为空宏)。

伯克利 socket API 定义了一组转换函数,用于 16 和 32 位整数在网络序和主机字节序之间的转换。htonl、htons用于 主机序转换到网络序 ntohl、ntohs 用于 网络序转换到本机序

留神:在大小字节序转换时,必须思考待转换数据的长度 (如 5.1.1 节的数据单元)。另外 对于单字符或小于单字符的几个 bit 数据,是不用转换的 ,因为 在机器存储和网络发送的一个字符内的 bit 位存储程序是统一的

6.1.3. 位序

用于形容 串行设施的传输程序 个别硬件传输采纳小字节序(先传低位),但 I2C 协定采纳大字节序 。网络协议中只有 数据链路层 的底端会波及到。

6.1.4. 处理器字节序

不同处理器体系的字节序如下所示:

  • X86、MOS Technology 6502、Z80、VAX、PDP-11 等处理器为 Little endian
  • Motorola 6800、Motorola 68000、PowerPC 970、System/370、SPARC(除 V9 外) 等处理器为 Big endian
  • ARM、PowerPC (除 PowerPC 970 外)、DEC Alpha,SPARC V9,MIPS,PA-RISC and IA64 等的字节序是 可配置的

6.1.5. 字节序编程

请看上面的语句:

printf("%c\n", *((short*)"AB") >> 8);

在大字节序下输入为 ’A’,小字节序下输入为 ’B’。

上面的代码可用来判断本地机器字节序:

// 字节序枚举类型
typedef enum{ENDIAN_LITTLE = (INT8U)0X00,
    ENDIAN_BIG    = (INT8U)0X01
}E_ENDIAN_TYPE;
 
E_ENDIAN_TYPE GetEndianType(VOID)
{
    INT32U dwData = 0x12345678;
    
    // 取数都从低地址开始拜访
    if(0x78 == *((INT8U*)&dwData))
        return ENDIAN_LITTLE;
    else
        return ENDIAN_BIG;
}
 
//Start of GetEndianTypeTest//
#include <endian.h>
VOID GetEndianTypeTest(VOID)
{
#if _BYTE_ORDER == _LITTLE_ENDIAN
    printf("[%s]<Test Case> Result: %s, EndianType = %s!\n", __FUNCTION__, 
           (ENDIAN_LITTLE != GetEndianType()) ? "ERROR" : "OK", "Little");
#elif _BYTE_ORDER == _BIG_ENDIAN
    printf("[%s]<Test Case> Result: %s, EndianType = %s!\n", __FUNCTION__, 
           (ENDIAN_BIG != GetEndianType()) ? "ERROR" : "OK", "Big");
#endif
}
//End of GetEndianTypeTest//

在字节序不同的平台间的替换数据时,必须进行转换。比方对于 int 类型,大字节序写入文件:

int i = 100;
write(fd, &i, sizeof(int));

小字节序读出后:

int i;
read(fd, &i, sizeof(int));
char buf[sizeof(int)];
memcpy(buf, &i, sizeof(int));
for(i = 0; i < sizeof(int); i++)
{int v = buf[sizeof(int) - i - 1];
    buf[sizeof(int) - 1] =  buf[i];
    buf[i] = v;
}
memcpy(&i, buf, sizeof(int));

下面仅仅是个例子。在不同平台间即便不存在字节序的问题,也尽量不要间接传递二进制数据。作为可选的形式就是应用文原本替换数据,这样至多能够防止字节序的问题。

很多的加密算法为了谋求速度,都会采取字符串和数字之间的转换,在计算结束后,必须留神字节序的问题,在某些实现中能够见到应用预编译的形式来实现,这样很不不便,如果应用后面的语句来判断,就能够主动适应。

字节序问题不仅影响异种平台间传递数据,还影响诸如读写一些非凡格式文件之类程序的可移植性。此时应用预编译的形式来实现也是一个好方法。

6.2. 对齐时的填充字节

代码如下:

struct A{ 
    char  c; 
    int   i; 
    short s;
};

int main(void){  
    struct A a; 
    a.c = 1; a.i = 2; a.s = 3;
    printf("sizeof(A)=%d\n", sizeof(struct A));
    return 0;
}

执行后输入为 sizeof(A)=12。

6.3. pragma pack 语法阐明

#pragma pack(n)
#pragma pack(push, 1)
#pragma pack(pop)

1)#pragma pack(n)

该指令指定构造和联结成员的紧凑对齐。而一个残缺的转换单元的构造和联结的紧凑对齐由 /Zp 选项设置。紧凑对齐用 pack 编译批示在数据阐明层设置。该编译批示在其呈现后的第一个构造或者联合声明处失效。该编译批示对定义有效。

当应用 #pragma pack (n) 时,n 为 1、2、4、8 或 16。第一个构造成员后的每个构造成员都被存储在更小的成员类型或 n 字节界线内。如果应用无参量的#pragma pack,构造成员被紧凑为以/Zp 指定的值。该缺省 /Zp 紧凑值为/Zp 8

2)编译器也反对以下增强型语法:

#pragma  pack([ [ { push | pop} , ] [identifier,] ] [n] )

若不同的组件应用 pack 编译批示 指定不同的紧凑对齐, 这个语法容许你把程序组件组合为一个独自的转换单元。

带 push 参量的 pack 编译批示 的每次呈现将以后的紧凑对齐存储到一个外部编译器堆栈中。编译批示的参量表从左到右读取。如果应用 push,则以后紧凑值被存储起来;如果给出一个 n 值,该值将成为新的紧凑值。若指定一个标识符,即选定一个名称,则该标识符将和这个新的的紧凑值分割起来。

带一个 pop 参量的 pack 编译批示 的每次呈现都会检索外部编译器堆栈顶的值,并使该值为新的紧凑对齐值。如果应用 pop 参量且外部编译器堆栈是空的,则紧凑值为命令行给定的值,并将产生一个正告信息。若应用 pop 且指定一个 n 值,该值将成为新的紧凑值。

若应用 pop 且指定一个标识符,所有存储在堆栈中的值将从栈中删除,直到找到一个匹配的标识符。这个与标识符相干的紧凑值也从栈中移出,并且这个仅在标识符入栈之前存在的紧凑值成为新的紧凑值。如果未找到匹配的标识符, 将应用命令行设置的紧凑值,并且将产生一个一级正告。缺省紧凑对齐为 8。

pack 编译批示 的新的加强性能让你在编写头文件时,确保在遇到该头文件的前后的紧凑值是一样的

6.4. Intel 对于内存对齐的阐明

以下内容节选自《Intel Architecture 32 Manual》。

字、双字和四字在天然边界上不须要在内存中对齐。(对于字、双字和四字来说,天然边界别离是偶数地址,能够被 4 整除的地址,和能够被 8 整除的地址。)

无论如何,为了进步程序的性能,数据结构 (尤其是栈) 应该尽可能地在天然边界上对齐。起因在于,为了拜访未对齐的内存,处理器须要作两次内存拜访;然而,对齐的内存拜访仅须要一次拜访。

一个字或双字操作数逾越了 4 字节边界,或者一个四字操作数逾越了 8 字节边界,被认为是未对齐的,从而须要两次总线周期来拜访内存。一个字起始地址是奇数但却没有逾越字边界被认为是对齐的,可能在一个总线周期中被拜访。

某些操作双四字的指令须要内存操作数在天然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用爱护异样(#GP)。双四字的天然边界是可能被 16 整除的地址。其余操作双四字的指令容许未对齐的拜访(不会产生通用爱护异样),然而,须要额定的内存总线周期来拜访内存中未对齐的数据。

6.5. 不同架构处理器的对齐要求

RISC 指令集处理器(MIPS/ARM):这种处理器的设计以效率为先,要求所拜访的多字节数据(short/int/long)的地址必须是此数据大小的倍数,如 short 数据地址应为 2 的倍数,long 数据地址应为 4 的倍数,也就是说是对齐的。

CISC 指令集处理器(X86):没有上述限度。

对齐解决策略

拜访非对齐多字节数据时(pack 数据),编译器会将指令拆成多条(因为非对齐多字节数据可能逾越地址对齐边界),保障每条指令都从正确的起始地址上获取数据,但也因而效率比拟低。

拜访对齐数据时则只用一条指令获取数据,因而对齐数据必须确保其起始地址是在对齐边界上。如果不是在对齐的边界,对 X86 CPU 是平安的,但对 MIPS/ARM 这种 RISC CPU 会呈现 总线拜访异样

为什么 X86 是平安的呢?

X86 CPU 是如何进行数据对齐的?

X86 CPU 的 EFLAGS 寄存器 中蕴含一个非凡的位标记,称为 AC(对齐查看的英文缩写) 标记。

依照默认设置,当 CPU 首次加电时,该标记被设置为 0。

当该标记是 0 时,CPU 可能主动执行它应该执行的操作,以便胜利地拜访未对齐的数据值。

然而,如果该标记被设置为 1,每当零碎试图拜访未对齐的数据时,CPU 就会收回一个INT 17H 中断

X86 的 Windows 2000 和 Windows 98 版本从来不扭转这个 CPU 标记位。因而,当应用程序在 X86 处理器上运行时,你基本看不到应用程序中呈现数据未对齐的异样条件。

为什么 MIPS/ARM 不平安呢?

因为 MIPS/ARM CPU 不能主动解决对未对齐数据的拜访。当未对齐的数据拜访产生时,CPU 就会将这一状况告诉操作系统。这时,操作系统将会确定它是否应该引发一个数据未对齐异样条件,对 vxworks 是会触发这个异样的。

6.6. ARM 下的对齐解决

有局部摘自 ARM 编译器文档对齐局部。

对齐的应用:

  1. __align(num) 用于批改最高级别对象的字节边界。

    • 在汇编中应用 LDRD 或 STRD 时就要用到此命令 __align(8) 进行润饰限度。来保证数据对象是相应对齐。
    • 这个润饰对象的命令最大是 8 个字节限度,能够让 2 字节的对象进行 4 字节对齐,但不能让 4 字节的对象 2 字节对齐。
    • __align是存储类批改,只润饰最高级类型对象,不能用于构造或者函数对象。
  2. __packed 进行一字节对齐。需注意:

    • 不能对 packed 的对象进行对齐;
    • 所有对象的读写访问都进行非对齐拜访;
    • float 及蕴含 float 的构造联结及未用__packed 的对象将不能字节对齐;
    • __packed 对部分整型变量无影响。
    • 强制由 unpacked 对象向 packed 对象转化时未定义。整型指针能够非法定义为 packed,如__packed int* p(__packed int 则没有意义)

对齐或非对齐读写访问可能存在的问题:

// 定义如下构造,b 的起始地址不对齐。在栈中拜访 b 可能有问题,因为栈上数据对齐拜访
__packed struct STRUCT_TEST{
    char a;
    int  b;
    char c;
};

// 将上面的变量定义成全局动态(不在栈上)
static char *p;
static struct STRUCT_TEST a;

void main(){
    __packed int *q; // 定义成__packed 来润饰以后 q 指向为非对齐的数据地址上面的拜访则能够
    p = (char*)&a; 
    q = (int*)(p + 1); 
    *q = 0x87654321;

    /* 失去赋值的汇编指令很分明
    ldr      r5,0x20001590 ; = #0x12345678
    [0xe1a00005]   mov     r0,r5
    [0xeb0000b0]   bl      __rt_uwrite4  // 在此处调用一个写 4 字节的操作函数
    [0xe5c10000]   strb    r0,[r1,#0]    // 函数进行 4 次 strb 操作而后返回,正确拜访数据
    [0xe1a02420]   mov     r2,r0,lsr #8
    [0xe5c12001]   strb    r2,[r1,#1]
    [0xe1a02820]   mov     r2,r0,lsr #16
    [0xe5c12002]   strb    r2,[r1,#2]
    [0xe1a02c20]   mov     r2,r0,lsr #24
    [0xe5c12003]   strb    r2,[r1,#3]
    [0xe1a0f00e]   mov     pc,r14
    
    若 q 未加__packed 润饰则汇编进去指令如下(会导致奇地址处拜访失败):[0xe59f2018]   ldr      r2,0x20001594 ; = #0x87654321
    [0xe5812000]   str     r2,[r1,#0]
    */
    // 这样很分明地看到非对齐拜访如何产生谬误,以及如何打消非对齐拜访带来的问题
    // 也可看到非对齐拜访和对齐拜访的指令差别会导致效率问题
}

6.7.《The C Book》之位域篇

While we’re on the subject of structures, we might as well look at bitfields. They can only be declared inside a structure or a union, and allow you to specify some very small objects of a given number of bits in length. Their usefulness is limited and they aren’t seen in many programs, but we’ll deal with them anyway. This example should help to make things clear:

struct{
    unsigned field1 :4; //field 4 bits wide
    unsigned        :3; //unnamed 3 bit field(allow for padding)
    signed field2   :1; //one-bit field(can only be 0 or -1 in two's complement)
    unsigned        :0; //align next field on a storage unit
    unsigned field3 :6;
}full_of_fields;

Each field is accessed and manipulated as if it were an ordinary member of a structure. The keywords signed and unsigned mean what you would expect, except that it is interesting to note that a 1-bit signed field on a two’s complement machine can only take the values 0 or -1. The declarations are permitted to include the const and volatile qualifiers.

The main use of bitfields is either to allow tight packing of data or to be able to specify the fields within some externally produced data files. C gives no guarantee of the ordering of fields within machine words, so if you do use them for the latter reason, you program will not only be non-portable, it will be compiler-dependent too. The Standard says that fields are packed into‘storage units’, which are typically machine words. The packing order, and whether or not a bitfield may cross a storage unit boundary, are implementation defined. To force alignment to a storage unit boundary, a zero width field is used before the one that you want to have aligned.

Be careful using them. It can require a surprising amount of run-time code to manipulate these things and you can end up using more space than they save.

Bit fields do not have addresses—you can’t have pointers to them or arrays of them.

6.8. C 语言字节相干面试题

6.8.1. Intel/ 微软 C 语言面试题

请看上面的问题:

#pragma pack(8)
struct s1{short a; // 按 min(1,8) 对齐
    long  b; // 按 min(4,8) 对齐
};
struct s2{
    char c;
    s1   d;
    long long e;  //VC6.0 下可能要用__int64 代替双 long
};
#pragma pack()

问题:

  1. sizeof(s2) =?
  2. s2 的 s1 中的 a 前面空了几个字节接着是 b?

剖析:

成员对齐有一个重要的条件,即 每个成员别离按本人的形式对齐

也就是说下面尽管指定了按 8 字节对齐,但并不是所有的成员都是以 8 字节对齐。其对齐的规定是:每个成员按 其类型的对齐参数(通常是这个类型的大小)指定对齐参数(这里是 8 字节) 中较小的一个对齐,并且构造的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。

s1 中成员 a 是 1 字节,默认按 1 字节对齐,而指定对齐参数为 8,两值中取 1,即 a 按 1 字节对齐;成员 b 是 4 个字节,默认按 4 字节对齐,这时就按 4 字节对齐,所以 sizeof(s1) 应该为 8

s2 中 c 和 s1 中 a 一样,按 1 字节对齐。而 d 是个 8 字节构造体,其默认对齐形式就是所有成员应用的对齐参数中最大的一个,s1 的就是 4。所以,成员 d 按 4 字节对齐。成员 e 是 8 个字节,默认按 8 字节对齐,和指定的一样,所以它对到 8 字节的边界上。这时,曾经应用了 12 个字节,所以又增加 4 个字节的空,从第 16 个字节开始搁置成员 e。此时长度为 24,并可被 8(成员 e 按 8 字节对齐)整除。这样,一共应用了 24 个字节。

各个变量在内存中的布局为:

c***aa**
bbbb****
dddddddd ——这种“矩阵写法”很不便看出构造体理论大小!

因而,sizeof(S2)后果为 24,a 前面空了 2 个字节接着是 b

这里有三点很重要:

  1. 每个成员别离按本人的形式对齐,并能最小化长度;
  2. 简单类型 (如构造) 的默认对齐形式是其最长的成员的对齐形式,这样在成员是简单类型时能够最小化长度;
  3. 对齐后的长度必须是成员中最大对齐参数的整数倍,这样在解决数组时可保障每一项都边界对齐。

还要留神,“空构造体”(不含数据成员)的大小为 1,而不是 0。试想如果不占空间的话,一个空构造体变量如何取地址、两个不同的空构造体变量又如何得以辨别呢?

6.8.2 上海网宿科技面试题

假如硬件平台是 intel x86(little endian),以下程序输入什么:

// 假如硬件平台是 intel x86(little endian)
typedef unsigned int uint32_t; 
void inet_ntoa(uint32_t in){char  b[18];
    register  char  *p;
    p = (char *)∈
#define UC(b) (((int)b)&0xff) //byte 转换为无符号 int 型
    sprintf(b, "%d.%d.%d.%d\n", UC(p[0]), UC(p[1]), UC(p[2]), UC(p[3]));
    printf(b);
}

int main(void){inet_ntoa(0x12345678);
    inet_ntoa(0x87654321);
    return 0;
}

先看如下程序:

int main(void){  
    int a = 0x12345678;
    char *p = (char *)&a;
    char str[20];
    sprintf(str,"%d.%d.%d.%d\n", p[0], p[1], p[2], p[3]);
    printf(str);
    return 0;
}

依照小字节序的规定,变量 a 在计算机中存储形式为:

高地址方向 ————–> 低地址方向
0x12 0x34 0x56 0x78
p[3] p[2] p[1] p[0]

留神: p 并不是指向 0x12345678 的结尾 0x12,而是指向 0x78。p[0]到 p[1]的操作是 &p[0]+1,因而 p[1]地址比 p[0]地址大。输入后果为120.86.52.18

反过来的话,令int a = 0x87654321,则输入后果为33.67.101.-121

为什么有负值呢?

因为零碎默认的 char 是有符号的,原本是 0x87 也就是 135,大于 127 因而就减去 256 失去 -121。

想要失去正值的话只需将 char *p = (char *)&a 改为 unsigned char *p = (unsigned char *)&a 即可。

综上不难得出,网宿面试题的答案为 120.86.52.1833.67.101.135

阐明:
本文转载自 https://www.cnblogs.com/clove…


欢送关注我的微信公众号【数据库内核】:分享支流开源数据库和存储引擎相干技术。

题目 网址
GitHub https://dbkernel.github.io
知乎 https://www.zhihu.com/people/…
思否(SegmentFault) https://segmentfault.com/u/db…
掘金 https://juejin.im/user/5e9d3e…
开源中国(oschina) https://my.oschina.net/dbkernel
博客园(cnblogs) https://www.cnblogs.com/dbkernel

正文完
 0