在处理通讯协议时,经常需要按照字节甚至是位进行处理,例如 MySQL 协议,那么对于 C 而言如何方便的进行处理呢? 另外,网络通讯时采用的是大端,到底是啥意思?
简介
在编写代码的时候,很大一部分工作是在不同的格式之间进行转换,从外部的数据结构转换成内部使用的结构,例如网络包 (TCP/IP、MySQL协议等)、磁盘文件 (GIF、JPEG等图片格式) 等等。
其中很重要的一部分就是整数的字节顺序问题,也就是当整数的大小超过了一个字节之后,如何进行表示,这就是所谓的字节序的问题。
CPU
不同的 CPU 对应的字节序略有区别:
- 大端,PowerPC、IBM、Sun、51
- 小端,x86、DEC
其中 ARM 两种模式都可以支持,另外,网络协议中大部分使用的是大端字节序,所以就有一系列的 API 对整数进行转换。
# if __BYTE_ORDER == __BIG_ENDIAN
# define ntohl(x) (x)
# define ntohs(x) (x)
# define htonl(x) (x)
# define htons(x) (x)
# else
# 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
其中 ntohs
为 network to host short
的简写,这些函数一般在头文件 <arpa/inet.h>
中进行定义,也就是,当前 CPU 为大端则直接使用,为小端时才会进行转换。
如下,详细介绍大小端的概念。
大小端
当数据类型大于一个字节时,其所占用的字节在内存中的顺序存在两种模式:小端模式 (little endian) 和大端模式 (big endian),其中 MSB(Most Significant Bit) 最高有效位,LSB(Least Significant Bit) 最低有效位.
例如,0x1234
使用两个字节储存,其中 高位字节(MSB) 是 0x12
,低位字节(LSB) 是 0x34
。
小端模式
MSB LSB
+-------------------------------+
| 1 | 2 | 3 | 4 | int 0x01020304
+-------------------------------+
0x03 0x02 0x01 0x00 Address
大端模式
MSB LSB
+-------------------------------+
| 1 | 2 | 3 | 4 | int 0x01020304
+-------------------------------+
0x00 0x01 0x02 0x03 Address
两种类型的字节序介绍如下:
- 大端字节序。高位字节在前,低位字节在后,也是人类读写数值的方法。
- 小端字节序:低位字节在前,高位字节在后,更适合计算机。
在计算都是从低位开始的,那么计算机先处理低位字节时效率比较高,计算机的内部处理都是小端字节序。但是,人类还是习惯读写大端字节序。
所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。
测试程序
如下是一个测试程序。
#include <stdio.h>
void main(void)
{
int test = 0x41424344;
char *ptr = (char*)&test;
#ifdef DEBUG
printf("int Address:%x Value:%x\n", (unsigned int)&test, test);
printf("\n------------------------------------\n");
int j;
for(j = 0; j <= 3; j++) {
printf("char Address:%x Value:%c\n", (unsigned int)ptr, *ptr);
ptr++;
}
printf("------------------------------------\n\n");
pAddress = (char*)&test;
#endif
if(*ptr == 0x44)
printf("Little-Endian\n");
else if(*ptr == 0x41)
printf("Big-Endian\n");
else
printf("Something Error!\n");
}
如果采用大端模式,则在向某一个函数通过向下类型装换来传递参数时可能会出错。如一个变量为 int i=1;
经过函数 void foo(short *j);
的调用,即 foo((short*)&i);
,在 foo()
中将 i
修改为 3
则最后得到的 i
为 0x301
。
为了支持跨平台,建议使用类似 uint32_t
这类固定长度的类型,在 C 中一般定义在 stdint.h
头文件中。
转换方式
假设,在保存一个二进制文件时采用的是大端字节序,那么读取一个 32bits 的数据方式如下。
uint32_t length = (data[3]<<0) | (data[2]<<8) | (data[1]<<16) | (data[0]<<24);
对于小端字节序的读取方式如下。
uint32_t length = (data[0]<<0) | (data[1]<<8) | (data[2]<<16) | (data[3]<<24);
GCC 使用
实际上 GCC 已经提供了大小端判断和数据转换的函数,可以直接使用。
GCC 内置了 __BYTE_ORDER__
__ORDER_LITTLE_ENDIAN__
__ORDER_BIG_ENDIAN__
这三种与字节序相关的宏定义,在代码中可以直接使用。
GCC 会内置了很多的宏定义,可以通过
gcc -posix -E -dM - </dev/null
命令进行查看,内置的函数可以查看 Other Built-in Functions Provided by GCC 中的介绍。
位域
在保存一些信息的时候,并不需要占用一个完整的字节,可能只需要几个二进制位即可,例如一个开关量。这时候,就可以通过 C 语言中的位段 (或者称为 “位域”) 进行处理。
所谓 “位域” 是把一个字节中的二进位划分为几个不同的区域,并标明每个区域的位数,每个域有一个域名,允许在程序中按域名进行操作。
struct bitfield {
int a:8;
int b:2;
int c:6;
};
如下简单介绍其使用方法。
简单使用
可以参考如下的示例。
#include <stdio.h>
#include <string.h>
#include <stdint.h>
struct header {
uint32_t length:16;
uint32_t :8; /* reserved */
uint32_t sequence:8;
};
int main(void)
{
int i;
uint8_t *ptr;
struct header hdr;
memset(&hdr, 0, sizeof(struct header));
hdr.length = 0x10;
hdr.sequence = 0x22;
ptr = (uint8_t *)&hdr;
printf("header size: %ld\n", sizeof(struct header));
for (i = 0; i < (int)sizeof(struct header); i++)
printf(" 0x%02X", ptr[i]);
puts("\n");
return 0;
}
注意事项
在使用时需要注意如下的内容。
- 其中类型必须为整形,也就是
int
unsigned int
signed int
三种之一,不能是浮点类型或者char
类型。 - 二进制位数不能超过基本类型表示的最大位数,例如 x64 上的最多不能超过 64 位。
- 可以使用空的位域,此时可以占用空间但是不能直接引用,下个位域从新存储单元开始存放。
- 不能对位域进行取地址操作。
其它
在头文件 <netinet/ip.h>
中,有类似如下的结构体定义,将大小端和位域进行耦合适配。
struct ip
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int ip_hl:4; /* header length */
unsigned int ip_v:4; /* version */
#endif
#if __BYTE_ORDER == __BIG_ENDIAN
unsigned int ip_v:4; /* version */
unsigned int ip_hl:4; /* header length */
#endif
uint8_t ip_tos; /* type of service */
unsigned short ip_len; /* total length */
unsigned short ip_id; /* identification */
unsigned short ip_off; /* fragment offset field */
#define IP_RF 0x8000 /* reserved fragment flag */
#define IP_DF 0x4000 /* dont fragment flag */
#define IP_MF 0x2000 /* more fragments flag */
#define IP_OFFMASK 0x1fff /* mask for fragmenting bits */
uint8_t ip_ttl; /* time to live */
uint8_t ip_p; /* protocol */
unsigned short ip_sum; /* checksum */
struct in_addr ip_src, ip_dst; /* source and dest address */
};
在定义结构体时,有关于大下端的直接优化,可以忽略字节序的转换。其实这种转换对于 CPU 来说开销很小,相比来说分支预测、执行依赖反而会占用更多的 CPU 。
参考
可以参考 How to teach endian 中的介绍。