# 2.2.8 关于有符号数与无符号数的建议

就像我们看到的那样,有符号数到无符号数的隐式强制类型转换导致了某些非直观的行为。而这些非直观的特性经常导致程序错误,并且这种包含隐式强制类型转换的细微差别的错误很难被发现。因为这种强制类型转换是在代码中没有明确指示的情况下发生的,程序员经常忽视了它的影响。

下面两个练习题说明了某些由于隐式强制类型转换和无符号数据类型造成的细微的错误。

练习题 2.25 考虑下列代码,这段代码试图计算数组 a 中所有元素的和,其中元素的数量由参数 length 给出。

/* WARNING: This is buggy code */
float sum_elements(float a[], unsigned length) {
    int i;
    float result = 0;

    for (i = 0; i <= length-1; i++)
        result += a[i];
    return result;
}

当参数 length 等于 0 时,运行这段代码应该返回 0.0。但实际上,运行时会遇到一个内存错误。请解释为什么会发生这样的情况,并且说明如何修改代码。

练习题 2.26 现在给你一个任务,写一个函数用来判定一个字符串是否比另一个更长。前提是你要用字符串库函数 strlen,它的声明如下:

/* Prototype for library function strlen */
size_t strlen(const char *s);

最开始你写的函数是这样的:

/* Determine whether string s is longer than string t */
/* WARNING: This function is buggy */
int strlonger(char *s, char *t) {
    return strlen(s) - strlen(t) > 0;
}

当你在一些示例数据上测试这个函数时,一切似乎都是正确的。进一步研究发现在头文件 stdio.h 中数据类型 size_t 是定义成 unsigned int 的。

A. 在什么情况下,这个函数会产生不正确的结果?

B. 解释为什么会出现这样不正确的结果。

C. 说明如何修改这段代码好让它能可靠地工作。

旁注:函数 getpeername 的安全漏洞

2002 年,从事 FreeBSD 开源操作系统项目的程序员意识到,他们对 getpeername 函数的实现存在安全漏洞。代码的简化版本如下:

/*
 * Illustration of code vulnerability similar to that found in
 * FreeBSD's implementation of getpeername()
 */

/* Declaration of library function memcpy */
void *memcpy(void *dest, void *src, size_t n);

/* Kernel memory region holding user-accessible data */
#define KSIZE 1024
char kbuf[KSIZE];

/* Copy at most maxlen bytes from kernel region to user buffer */
int copy_from_kernel(void *user_dest, int maxlen) {
    /* Byte count len is minimum of buffer size and maxlen */
    int len = KSIZE < maxlen ? KSIZE : maxlen;
    memcpy(user_dest, kbuf, len);
    return len;
}

在这段代码里,第 7 行给出的是库函数 memcpy 的原型,这个函数是要将一段指定长度为 n 的字节从内存的一个区域复制到另一个区域。

从第 14 行开始的函数 copy_from_kernel 是要将一些操作系统内核维护的数据复制到指定的用户可以访问的内存区域。对用户来说,大多数内核维护的数据结构应该是不可读的,因为这些数据结构可能包含其他用户和系统上运行的其他作业的敏感信息,但是显示为 kbuf 的区域是用户可以读的。参数 maxlen 给出的是分配给用户的缓冲区的长度,这个缓冲区是用参数 user_dest 指示的。然后,第 16 行的计算确保复制的字节数据不会超出源或者目标缓冲区可用的范围。

不过,假设有些怀有恶意的程序员在调用 copy_from_kernel 的代码中对 maxlen 使用了负数值,那么,第 16 行的最小值计算会把这个值赋给 len,然后 len 会作为参数 n 被传递给 memcpy。不过,请注意参数 n 是被声明为数据类型 size_t 的。这个数据类型是在库文件 stdio.h 中(通过 typedef)被声明的。典型地,对 32 位程序它被定义为 unsigned int,对 64 位程序定义为 unsigned long。既然参数 n 是无符号的,那么 memcpy 会把它当作一个非常大的正整数,并且试图将这样多字节的数据从内核区域复制到用户的缓冲区。虽然复制这么多字节(至少 231 个)实际上不会完成,因为程序会遇到进程中非法地址的错误,但是程序还是能读到它没有被授权的内核内存区域。

我们可以看到,这个问题是由于数据类型的不匹配造成的:在一个地方,长度参数是有符号数;而另一个地方,它又是无符号数。正如这个例子表明的那样,这样的不匹配会成为缺陷的原因,甚至会导致安全漏洞。幸运的是,还没有案例报告有程序员在 FreeBSD 上利用了这个漏洞。他们发布了一个安全建议,“FreeBSD-SA-02:38.signed-error”,建议系统管理员如何应用补丁消除这个漏洞。要修正这个缺陷,只要将 copy_from_kernel 的参数 maxlen 声明为类型 size_t,也就是与 memcpy 的参数 n 一致。同时,我们也应该将本地变量 len 和返回值声明为 size_t。

我们已经看到了许多无符号运算的细微特性,尤其是有符号数到无符号数的隐式转换,会导致错误或者漏洞的方式。避免这类错误的一种方法就是绝不使用无符号数。实际上,除了 C 以外很少有语言支持无符号整数。很明显,这些语言的设计者认为它们带来的麻烦要比益处多得多。比如,Java 只支持有符号整数,并且要求以补码运算来实现。正常的右移运算符 >> 被定义为执行算术右移。特殊的运算符 >>> 被指定为执行逻辑右移。

当我们想要把字仅仅看做是位的集合而没有任何数字意义时,无符号数值是非常有用的。例如,往一个字中放入描述各种布尔条件的标记(flag)时,就是这样。地址自然地就是无符号的,所以系统程序员发现无符号类型是很有帮助的。当实现模运算和多精度运算的数学包时,数字是由字的数组来表示的,无符号值也会非常有用。