6
8
2015
0

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

终于比较认真地读了一下 CSAPP 的一点点,顺便做点记录,也算是以往经验的小小总结吧。诸如 [DCL13-C] 的标注,是 CERT C 安全编码标准的编号(看来我写的代码还是不错的嘛)。

大端、小端是机器中两种表示数据的方法(字节序),大端法是把最高有效数字排在低地址,小端法是把最高有效数字排在高地址。举例来说,0x12345678 这个数字,在小端机器中会被存储为 78 56 34 12(低->高),相反在大端机器中是 12 34 56 78(低->高)。

以程序验证:

#include <stdio.h>
#include <stdlib.h>

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 <stdio.h>
#include <stdlib.h>

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(&le, sizeof(int));
	printf("big endian:");
	show_bytes(&be, sizeof(int));
	return EXIT_SUCCESS;
}

只实现了 int 的大小端转换,实际上其余类型乃至任意类型的大小端转换都很容易实现,故从略。

提醒:字符串(特指 char[])没有大小端转换,每个 char 就是一个字节,从低地址端开始排列(想想「字节序」这个名称)。实际上我们上面的一切都是依赖这个的。另外还依赖了「64 位、32 位系统 int 都是 4 字节」。如果需要很好的可移植性,这些还不够。以上关于字节序的讨论有一个直接推论:跨机器交换文件应当尽量使用文本文件,二进制文件必须设计良好、经过大量测试才能使用。

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

Category: 编程 | Tags: csapp | Read Count: 1317

登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter

Powered by Chito | Hosted at is-Programmer | Theme: Aeros 2.0 by TheBuckmaker.com