【问题标题】:Unsigned char overflow with subtraction带减法的无符号字符溢出
【发布时间】:2015-09-16 15:56:40
【问题描述】:

我试图弄清楚无符号溢出如何与减法一起工作,所以我编写了以下测试来尝试一下:

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

unsigned char minWrap(unsigned char a, unsigned char b) {
    return a > b ? a - b : a + (0xff - b) + 1;
}

int main(int argc, char *argv[]) {
    unsigned char a = 0x01, b = 0xff;
    unsigned char c = a - b;

    printf("0x%02x 0x%02x 0x%02x\n", a-b, c, minWrap(a,b));

    return EXIT_SUCCESS;
}

作为输出给出:

0xffffff02 0x02 0x02

我本来希望输出是相同的三倍。我的问题是:添加/减去无符号字符并期望它们在0xff 处环绕是否总是安全的?

或者更一般地说,使用uintN_t 计算并期望结果为模 2^N 是否安全?

【问题讨论】:

  • 语言律师:无符号类型不会溢出。

标签: c gcc


【解决方案1】:

添加/减去无符号字符并期望它们在 0xff 处回绕是否总是安全的?

没有。在 C 中,char 类型的对象通过通常的整数提升。因此,如果char 的范围适合int(通常),它将转换为int,否则转换为unsigned。

a - b --> 0x01 - 0xFF --> 1 - 255 --> -254。

以下是未定义的行为,因为%x 与int 不匹配,并且-254 的值不在unsigned 范围内(请参阅@EOF 注释)。典型的行为是转换为unsigned

printf("0x%02x\n", a-b);
// 0xffffff02

使用uintN_t 计算是否安全并期望结果为模 2^N?

是的。但请确保生成uintN_t 类型的结果,并避免意外通常的整数提升。

#include <inttypes.h>

uint8_t a = 0x01, b = 0xff;
uint8_t diff = a - b;

printf("0x%02x\n", (unsigned) diff);
printf("0x%02" PRTx8 "\n", diff);

【讨论】:

  • printf() 仅在减法结果为负数时为未定义行为,因为正符号整数保证与相应的无符号类型具有相同的值。
  • @EOF 不同意。 “如果转换规范无效,则行为未定义”和“o,u,x,X unsigned int 参数已转换”§7.21.6.1 9 不包括范围异常。同意int 正值范围适合unsigned,但两者的大小可能不同。
  • C11 标准草案,6.5.2.2 Function calls, Section 6 明确豁免:[...]the behavior is undefined, except for the following cases: — one promoted type is a signed integer type, the other promoted type is the corresponding unsigned integer type, and the value is representable in both types;
  • @EOF 同意 - 答案已修改。 LSNED
【解决方案2】:

printf 行中的a-b 在a 和b 提升为int 之后进行评估。此外,由于运行时环境在格式说明符中使用了%x,因此该值被视为unsigned int。

相当于:

int a1 = a;
int b1 = b;
int x = a1 - b1;
printf("0x%02x 0x%02x 0x%02x\n", x, c, minWrap(a,b));

C99 标准的6.3.1.8 常用算术转换部分有更多详细信息。

理论上,当printf 中预期unsigned int 时使用int 会导致未定义的行为。像您一样,宽松的运行时环境将int 视为unsigned int 并继续打印该值。

【讨论】:

  • 除非(singned) int 不能代表unsigned char 的所有值,否则表达式将返回int,而不是unsigned int。
  • @RSahu:如果int 足够宽以至于任何unsigned char 都可以表示为(正)int——通常是这种情况——那么通常的算术转换将转换为int 而不是 unsigned int
  • 没有。 C11 标准草案,6.3.1 Arithmetic operands 6.3.1.1 Boolean, characters, and integers, Section 2 [...]If an int can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an int; otherwise, it is converted to an unsigned int.
  • %x 中的printf() 不会影响用于计算参数的表达式的类型。实际上,如果表达式的结果是负数,则行为未定义。
  • 阅读 C11 标准草案,6.5.2.2 Function calls。长话短说,如果函数没有原型,或者是可变参数函数(如printf()),则参数需要更精确地匹配。
【解决方案3】:

来自关于整数转换的标准 6.3.1.8/1:

整数提升在两个操作数上执行。然后将以下规则应用于提升的操作数

如果两个操作数的类型相同,则不再进行转换 需要。

否则,如果两个操作数都具有有符号整数类型或都具有 无符号整数类型,具有较小整数类型的操作数 转换等级转换为具有更大的操作数的类型 排名。

否则,如果无符号整数类型的操作数有秩 大于或等于另一个操作数的类型的等级,则 带符号整数类型的操作数转换为 无符号整数类型的操作数。

否则,如果带符号整数类型的操作数的类型可以 用无符号表示操作数类型的所有值 整数类型,然后转换无符号整数类型的操作数 为有符号整数类型的操作数的类型。

否则,两个操作数都转换为无符号整数类型 对应带符号整数类型的操作数的类型。

在这种情况下,环绕是明确定义的。在表达式a-b 中,因为两个操作数都是unsigned char 类型,所以首先将它们提升为int 并执行操作。如果将此值分配给unsigned char,它将被正确截断。但是,您使用 %x 格式说明符将此值传递给 printf,该说明符需要 unsigned int。要正确显示它,请使用 %hhx,它需要 unsigned char。

【讨论】:

  • 你在报价前剪掉了重要的部分。 Otherwise, the integer promotions are performed on both operands. Then the following rules are applied to the promoted operands:[the part you quoted here].
  • @EOF 谢谢。我更新了报价并进行了一些更正。
  • 您跳过了第 6.3.1.1 节(描述整数提升),这对该问题最重要 - 6.3.1.8 中的转换无关紧要,因为它们不适用(第二段在你的报价中总是正确的)
  • 我不知道%hhx。谢谢
猜你喜欢
  • 2019-02-27
  • 2020-06-07
  • 1970-01-01
  • 1970-01-01
  • 2012-10-28
  • 2020-04-06
  • 1970-01-01
  • 2021-09-22
  • 2020-03-27
相关资源
最近更新 更多