终于比较认真地读了一下 CSAPP 的一点点,顺便做点记录,也算是以往经验的小小总结吧。诸如 [DCL13-C] 的标注,是 CERT C 安全编码标准的编号(看来我写的代码还是不错的嘛)。
大端、小端是机器中两种表示数据的方法(字节序),大端法是把最高有效数字排在低地址,小端法是把最高有效数字排在高地址。举例来说,0x12345678 这个数字,在小端机器中会被存储为 78 56 34 12(低->高),相反在大端机器中是 12 34 56 78(低->高)。
以程序验证:
#include#include void show_bytes(const void *bytes, size_t len) { const unsigned char *bytes_; // [DCL00-C], [INT07-C] size_t i; bytes_ = bytes; for (i = 0; i < len; i++) { printf(" %.2X", bytes_[i]); } printf("\n"); } int main(void) { int val = 0x12345678; show_bytes(&val;, sizeof(val)); return EXIT_SUCCESS; }
运行该程序,输出 78 56 34 12,说明我的机器就是小端的。
注意,在这里我稍微修改了一下 CSAPP 上的例程。有如下修改:
那么如何判断大小端呢?在运行时可以用类似上面的方法这样判断:
bool bigendian() { static int val = 0x12345678; if (*(((char *)&val;)+3) == 0x78) // warning: magic number 3 { return true; } return false; }
或者用 union:
union { int val; char ch[4]; } _bigendianstub; bool bigendian() // requires C99-compliant compilers; stdbool.h must be included { _bigendianstub.val = 0x12345678; if (_bigendianstub.ch[3] == 0x78) { return true; } return false; }
一般来说,在一个机器中只能是大端或小端,而操作系统也必然与之对应,于是可以直接在编译时确定:
const char ch[4] = { 0x00, 0xff, 0xff, 0xff }; // [DCL00-C] #define LITTLEENDIAN ((*(int*)ch)<0) // [PRE02-C] #define BIGENDIAN (!LITTLEENDIAN) // [PRE02-C]
这里比较流氓,直接把数字写到数据段里面去了,然后利用了 int 最高位表示符号的特点解决问题。相当于直接把 0x00FFFFFF 当作 int,显然我们发现,在大端机器中这个数字表示 0x00FFFFFF,在小端机器中这个数字表示 -256。
当然,以上不过是示例而已,在真实环境中,我们有编译器提供的宏来确定。gcc 的相关宏是 __BYTE_ORDER。
说了这么多,大小端究竟有什么影响呢?主要影响就是与其他机器交互了。个人电脑基本都是小端,而网络字节序则是大端,意味着我们要在发送数据时进行转换。操作系统提供了 ntoh(network to host)、hton (host to network)系列函数可以实现这个目的。但是在大端机器上,这两个函数显然无法得到小端结果!因此我们自己实现一套,以下是完整的测试代码:
#include#include const char ch[4] = { 0x00, 0xff, 0xff, 0xff }; #define LITTLEENDIAN ((*(int*)ch)<0) #define BIGENDIAN (!LITTLEENDIAN) void show_bytes(const void *bytes, size_t len) { const unsigned char *bytes_; size_t i; bytes_ = bytes; for (i = 0; i < len; i++) { printf(" %.2X", bytes_[i]); } printf("\n"); } int swap_byteorder(int orig) { int ret; char *ptrf, *ptrl; ret = orig; ptrf = (char*)&ret; ptrl = ((char*)&ret;)+3; // warning: magic number 3 for (; ptrf < ptrl; ptrf++, ptrl--) { int t; t = *ptrf; *ptrf = *ptrl; *ptrl = t; } return ret; } inline int tobe(int orig) // requires C99-compliant compilers { if(BIGENDIAN) return orig; return swap_byteorder(orig); } inline int tole(int orig) // requires C99-compliant compilers { if(LITTLEENDIAN) return orig; return swap_byteorder(orig); } int main(void) { int orig, le, be; orig = 0x12345678; le = tole(orig); be = tobe(orig); printf("original (%s):", LITTLEENDIAN?"little endian":"big endian"); show_bytes(&orig;, sizeof(int)); printf("little endian:"); show_bytes(≤, sizeof(int)); printf("big endian:"); show_bytes(&be;, sizeof(int)); return EXIT_SUCCESS; }
只实现了 int 的大小端转换,实际上其余类型乃至任意类型的大小端转换都很容易实现,故从略。
提醒:字符串(特指 char[])没有大小端转换,每个 char 就是一个字节,从低地址端开始排列(想想「字节序」这个名称)。实际上我们上面的一切都是依赖这个的。另外还依赖了「64 位、32 位系统 int 都是 4 字节」。如果需要很好的可移植性,这些还不够。以上关于字节序的讨论有一个直接推论:跨机器交换文件应当尽量使用文本文件,二进制文件必须设计良好、经过大量测试才能使用。
(翻到后面才发现好多内容都是作业……)