IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    CSAPP 2.1:字节序、大端与小端

    Ma Kai发表于 2015-06-08 10:01:31
    love 0

    终于比较认真地读了一下 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 上的例程。有如下修改:

    1. 将 byte_pointer 修改为了 const void *:任何指针都能被隐式转换为 void * 而没有警告,方便使用。另外加上 const 确保不会误修改传入数据。[DCL13-C]
    2. 将 len 的类型修改为了 size_t:对 len 来说有符号是毫无意义的,len 不可能小于 0。实际上,在运行中如果传入负数将会溢出(指针都无符号)。[INT01-C]
    3. 这里的计数器 i 并不是通常的 int 类型,而是与和它比较的 len 一致,关于这一点可以看 CS:APP (中文版)的第 53 页关于 FreeBSD getpeername 漏洞的讨论。[INT02-C]

    那么如何判断大小端呢?在运行时可以用类似上面的方法这样判断:

    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 字节」。如果需要很好的可移植性,这些还不够。以上关于字节序的讨论有一个直接推论:跨机器交换文件应当尽量使用文本文件,二进制文件必须设计良好、经过大量测试才能使用。

    (翻到后面才发现好多内容都是作业……)



沪ICP备19023445号-2号
友情链接