在C和C++中比较不同大小的无符号整数时,如何获得警告?

vdzxcuhz  于 2023-08-09  发布在  其他
关注(0)|答案(6)|浏览(117)

C或C++中常见的bug来源是

size_t n = // ...

for (unsigned int i = 0; i < n; i++) // ...

字符串
unsigned int溢出时,其可以无限循环。
例如,在Linux上,unsigned int是32位的,而size_t是64位的,所以如果n = 5000000000,我们得到一个无限循环。
我如何使用GCC或Clang获得有关此问题的警告?
GCC的-Wallx 1 m5n1x不能做到这一点:

#include <stdint.h>

void f(uint64_t n)
{
    for (uint32_t i = 0; i < n; ++i) {
    }
}
gcc-13 -std=c17 \
       -Wall -Wextra -Wpedantic \
       -Warray-bounds -Wconversion \
       -fanalyzer \
       -c -o 76840686.o 76840686.c

的数据
(no输出)

  • 我正在寻找一个不需要n是编译时常数的解决方案。
  • 理想情况下,该解决方案可以在现有的C/C++项目上工作,而不必完全重写它们。
  • 建议使用编译器警告以外的其他工具也会很有用,但编译器警告本身会更好

编辑:上游编译器功能请求

答案表明当前的编译器中不存在此类警告。我已经开始提交上游功能请求:

swvgeqrz

swvgeqrz1#

gccclang中似乎没有内置警告选项来执行所请求的操作。但是,我们可以使用clang-query代替。
下面是一个clang-query命令,它将报告32位和64位整数的比较,假设int是32位,long是64位。(更多内容见下文)。

#!/bin/sh

PATH=$HOME/opt/clang+llvm-14.0.0-x86_64-linux-gnu-ubuntu-18.04/bin:$PATH

# In this query, the comments are ignored because clang-query (not the
# shell) recognizes and discards them.
query='m
  binaryOperator(                            # Find a binary operator expression
    anyOf(                                   #  such that any of:
      hasOperatorName("<"),                  #   is operator <, or
      hasOperatorName("<="),                 #   is operator <=, or
      hasOperatorName(">"),                  #   is operator >, or
      hasOperatorName(">="),                 #   is operator >=, or
      hasOperatorName("=="),                 #   is operator ==, or
      hasOperatorName("!=")                  #   is operator !=;
    ),

    hasEitherOperand(                        #  and where either operand
      implicitCastExpr(                      #   is an implicit cast
        has(                                 #    from
          expr(                              #     an expression
            hasType(                         #      whose type
              hasCanonicalType(              #       after resolving typedefs
                anyOf(                       #        is either
                  asString("int"),           #         int or
                  asString("unsigned int")   #         unsigned int,
                )
              )
            ),
            unless(                          #      unless that expression
              integerLiteral()               #       is an integer literal,
            )
          )
        ),
        hasImplicitDestinationType(          #    and to a type
          hasCanonicalType(                  #     that after typedefs
            anyOf(                           #      is either
              asString("long"),              #       long or
              asString("unsigned long")      #       unsigned long.
            )
          )
        )
      ).bind("operand")
    )
  )
'

# Run the query on test.c.
clang-query \
  -c="set bind-root false" \
  -c="$query" \
  test.c -- -w

# EOF

字符串
当在以下test.c上运行时,它会报告所有指示的情况:

// test.c
// Demonstrate reporting comparisons of different-size operands.

#include <stddef.h>          // size_t
#include <stdint.h>          // int32_t, etc.

void test(int32_t i32, int64_t i64, uint32_t u32, uint64_t u64)
{
  i32 < i32;                 // Not reported: same sizes.
  i32 < i64;                 // reported
  i64 < i64;

  u32 < u32;
  u32 < u64;                 // reported
  u64 < u64;

  i32 < u64;                 // reported
  u32 < i64;                 // reported

  i32 <= i64;                // reported

  i64 > i32;                 // reported
  i64 >= i32;                // reported

  i32 == i64;                // reported
  u64 != u32;                // reported

  i32 + i64;                 // Not reported: not a comparison operator.

  ((int64_t)i32) < i64;      // Not reported: explicit cast.

  u64 < 3;                   // Not reported: comparison with integer literal.

  // Example #1 in question.
  size_t n = 0;
  for (unsigned int i = 0; i < n; i++) {}        // reported
}

// Example #2 in question.
void f(uint64_t n)
{
  for (uint32_t i = 0; i < n; ++i) {             // reported
  }
}

// EOF


关于clang-query命令的一些细节:

  • 该命令将-w传递给clang-query以抑制其他警告。这只是因为我编写测试的方式会引发关于未使用值的警告,而对于普通代码来说是不必要的。
  • 它传递set bind-root false,因此唯一报告的位置是感兴趣的操作数,而不是报告整个表达式。
  • 不幸的是,不可能让查询也打印所涉及的类型的名称。尝试使用绑定执行此操作会导致clang-query报告“Matcher does not support binding(匹配器不支持绑定)”。

该查询的不足之处在于它显式地列出了源和目标类型。不幸的是,clang-query没有匹配器来报告任何32位类型,因此必须单独列出它们。您可能希望在目标端添加[unsigned] long long。如果使用针对IL32平台(如Windows)的编译器选项运行此代码,则可能还需要删除[unsigned] long
相关地,注意clang-query接受--之后的编译器选项,或者在compile_commands.json文件中。不幸的是,没有专门的clang-query命令行文档,甚至它的--help也没有提到--命令行选项。我所能链接的最好的是libtooling的文档,因为clang-query在内部使用该库进行命令行处理。
最后,我要指出,我没有在真实的代码中对这个查询进行任何“调优”。它可能会产生很多噪音,需要进一步调整。关于如何使用clang-query的教程,我推荐Stephen Kelly的博客文章Exploring Clang Tooling Part 2: Examining the Clang AST with clang-query。还有一个AST Matcher Reference,但文档非常简洁。

j1dl9f46

j1dl9f462#

这并不能直接回答问题(提供警告),但您会考虑完全避免问题的替代方案吗?

size_t n = // ...

    for (typeof(n) i = 0; i < n; i++) // ...

字符串
现在n是什么类型已经不重要了,因为i总是与n相同的类型,你应该永远不会遇到由于i是一个更小的类型或具有比n更小的范围而导致的无限循环的问题。

q0qdq0h2

q0qdq0h23#

PVS Studio可以发出这样的警告(以及更多),这里是他们文档中几乎相同的示例:
https://pvs-studio.com/en/docs/warnings/v104/
它是一个付费工具,但他们为开源项目给予免费许可。
我在Clang-tidy中没有发现这样的警告,这是LLVM项目中的一个免费linter工具,但是添加一个检查来比较不同大小的整数是非常简单的(Scott McPeak后来的回复用出色的clang-query完成了大部分工作-剩下的部分只是将这个查询插入clang-tidy)。不过会很吵。我们可以通过限制循环条件的检查来限制噪音,这也可以用Clang-tidy来完成,但用AST匹配器需要做更多的工作。

ztyzrc3y

ztyzrc3y4#

最近的gcc版本似乎支持-Warith-conversion
-Warith-conversion
即使将操作数转换为相同类型不能更改其值,也要对算术运算的隐式转换发出警告。这会影响来自-Wconversion-Wfloat-conversion-Wsign-conversion的警告。

void f (char c, int i)
{
    c = c + i; // warns with -Wconversion
    c = c + 1; // only warns with -Warith-conversion
}

字符串
然而,它不适用于your example,可能是因为i < n不是算术表达式。对于泛型二进制表达式,似乎没有此警告的变体。

0ve6wy6x

0ve6wy6x5#

对于C++,你甚至可以做得比编译器警告更好,假设n是编译时常量。这也适用于非gcc编译器。但这种逻辑不适用于C代码。
这个想法基本上是在变量类型中编码值信息,而不是变量值。

template<std::integral T, auto N>
constexpr bool operator<(T value, std::integral_constant<decltype(N), N>)
{
    static_assert(std::is_signed_v<T> == std::is_signed_v<decltype(N)>, "the types have different signs");
    static_assert((std::numeric_limits<T>::max)() >= N, "the maximum of type T is smaller than N");
    return value < N;
}

// todo: overload with swapped operator parameter types

int main()
{
    constexpr std::integral_constant<size_t, 500'000'000> n; // go with 5'000'000'000, and you'll get a compiler error 

    for (unsigned int i = 0; i < n; i++)
    {

    }
}

字符串
如果该值不是编译时常量,您仍然可以为整数创建 Package 器模板类型,并重载<运算符以与整数值进行比较,将static_assert s添加到该运算符的主体中。

template<std::integral T>
class IntWrapper
{
    T m_value;
public:
    constexpr IntWrapper(T value)
        : m_value(value)
    {}

    template<std::integral U>
    friend constexpr bool operator<(U o1, IntWrapper o2)
    {
        static_assert(std::is_signed_v<U> == std::is_signed_v<T>, "types have different signedness");
        static_assert((std::numeric_limits<U>::max)() >= (std::numeric_limits<T>::max)(),
            "the comparison may never yield false because of the maxima of the types involved");

        return o1 < o2.m_value;
    }
};

void f(IntWrapper<uint64_t> n)
{
    for (uint32_t i = 0; i < n; ++i) {
    }
}


请注意,更改比较运算符的操作数之一的类型的必要性既有好处也有缺点:它要求你修改代码,但它也允许你在每个变量的基础上应用检查…

fkaflof6

fkaflof66#

遵循编码标准,在源代码处停止

大多数嵌入式软件的编码标准都禁止使用“int”,原因和你说的一样。C标准要求存在固定长度的等效数据类型(8、16和32位多年来一直是强制性的; 64位是较新的,但仍然支持基本上无处不在)。在任何地方使用它们都是一个很好的做法,但在安全相关的软件中,它通常是强制性的。有许多工具可用于流行的编码标准,如MISRA-C,它们将为您捕获这些问题。
对于上面的许多评论者来说,你的例子似乎很模糊,因为它需要2^32次迭代才能溢出。许多现代编码器忘记的是,int也可以是16位的,这使得溢出在过去变得容易得多。Ariane-5 disaster是由截断为16位时的64位值溢出引起的。Therac-25 disaster也部分是由清 debugging 误状态的整数溢出引起的。
不过,32位中也有示例。Windows 95和98 famously crashed after 49.7 days由32位毫秒计时器溢出引起。在15年的时间里,我们有Y2038问题可以期待。大多数现代系统都为Y2038做好了准备,但我们很可能会到达那里并有一些惊喜!
所有这些都构成了软件工程作为一门工程学科的制度历史的一部分,就像泰桥和塔科马窄桥构成了土木工程制度历史的一部分一样。我有点震惊地看到上面有人说他们在40年的C编码中从未听说过这个。当然,仅仅是一个编码员而不知道这一点是可能的,就像仅仅是一个建筑师而不知道土木工程原理是可能的一样。然而,工程师必须意识到他们设计背后的更深层次的原则。这个问题,看起来微不足道,是软件工程师和单纯的编码员之间明显区分专业水平的一件事。考虑到阿丽亚娜-5和Therac-25,我不会为对此做出判断而道歉。

相关问题