使用PVS-Studio测试GCC 10编译器

PVS-Studio和GCC 10

GCC编译器是使用大量宏编写的。使用PVS-Studio对GCC代码进行的另一次检查再次证实了我们团队的观点,即宏是错误的。这样的代码不仅对于静态分析器而且对于程序员而言都难以理解。当然,GCC开发人员已经习惯了该项目,并且对此非常精通。但是从侧面看,很难理解一些东西。实际上,由于存在宏,因此无法完全执行代码验证。尽管如此,PVS-Studio分析仪一如既往地表明,即使在编译器中,它也可以发现错误。

是时候仔细检查GCC编译器代码了


四年前, 我上次检查 GCC编译器。时间过得很快而且潜移默化,以某种方式,我都忘了回到这个项目并重新检查它。出版物“ GCC 10中的静态分析推回了这个想法

实际上,编译器具有自己的内置静态代码分析器,并且还在开发中,这已经不是什么秘密了。因此,我们不时撰写文章,说PVS-Studio静态分析器甚至在编译器内部也可以发现错误,而且我们白白吃面包也没有白费。

实际上,您无法将经典的静态分析器与编译器进行比较。静态分析器不仅可以搜索代码中的错误,而且还可以开发基础架构。例如,这是与SonarQube,PlatformIO,Azure DevOps,Travis CI,CircleCI,GitLab CI / CD,Jenkins,Visual Studio等系统集成。这些是开发的用于批量警告抑制的机制,即使在一个大型的旧项目中,也可以使您快速开始使用PVS-Studio。这是一个通知邮件列表。等等等等。但是,无论如何,第一个要问的问题是:“ PVS-Studio可以找到编译器找不到的东西吗?”。因此,我们将一再撰写有关检查这些编译器本身的文章。

让我们回到GCC项目验证的主题。无需专门研究这个项目,而不要说出它是什么。让我们更好地谈谈该项目的内容。

并且内部有大量的宏会干扰验证。首先,PVS-Studio分析仪会产生大量的误报。没什么错,但是要开始研究他发布的报告并非易事。以良好的方式,您需要做一些工作来抑制宏中的错误警告。否则,有用的警告只会淹没在噪音中。此设置超出了本文编写任务的范围。实际上,我将完全诚实-我只是懒惰地这样做,尽管这并不复杂由于噪音,查看报告是很肤浅的。

其次,作为一个不熟悉项目的人,我很难理解代码。宏,宏...您必须查看它们的部署方式,以了解分析仪为何生成警告。很难。我不喜欢宏有人可能会说,没有C语言中的宏就无法做到。但是GCC很久没有用C语言编写了,是的,出于历史原因,文件的扩展名为.c,但是您可以在其中查找:

//  alias.c
....
struct alias_set_hash : int_hash <int, INT_MIN, INT_MIN + 1> {};
struct GTY(()) alias_set_entry {
  alias_set_type alias_set;
  bool has_zero_child;
  bool is_pointer;
  bool has_pointer;
  hash_map<alias_set_hash, int> *children;
};
....

显然这不是C,而是C ++。

通常,宏和编码风格使分析器报告的研究变得非常困难。因此,这次我将不希望列出各种各样的错误。带着困难,喝了几杯咖啡,我写下了10个有趣的片段,这让我很孤单。

10个可疑代码段


片段N1,似乎复制粘贴失败

static bool
try_crossjump_to_edge (int mode, edge e1, edge e2,
                       enum replace_direction dir)
{
  ....
  if (FORWARDER_BLOCK_P (s->dest))
    s->dest->count += s->count ();

  if (FORWARDER_BLOCK_P (s2->dest))
    s2->dest->count -= s->count ();
  ....
}

PVS-Studio警告:V778找到两个相似的代码片段。也许这是一个错字,应该使用“ s2”变量代替“ s”。cfgcleanup.c 2126

实际上,我不确定这是否是一个错误。但是,我非常怀疑此代码是使用Copy-Paste编写的,并且在第二个位置的一个块中,他们忘记了s2替换s也就是说,在我看来,第二段代码应该是这样的:

if (FORWARDER_BLOCK_P (s2->dest))
  s2->dest->count -= s2->count ();

片段N2,错字

tree
vn_reference_lookup_pieces (....)
{
  struct vn_reference_s vr1;
  ....
  vr1.set = set;
  vr1.set = base_set;
  ....
}

PVS-Studio警告:V519为'vr1.set'变量连续分配了两次值。也许这是一个错误。检查行:3448,3449. tree-ssa-sccvn.c 3449

将不同的值连续两次写入同一变量是很奇怪的。这是一个明显的错别字。此文件旁边的代码是:

vr1.set = set;
vr1.base_set = base_set;

最有可能的是,在可疑代码中,其编写方式应该完全相同。

片段N3,为其分配变量

static omp_context *
new_omp_context (gimple *stmt, omp_context *outer_ctx)
{
  omp_context *ctx = XCNEW (omp_context);

  splay_tree_insert (all_contexts, (splay_tree_key) stmt,
         (splay_tree_value) ctx);
  ctx->stmt = stmt;

  if (outer_ctx)
    {
      ctx->outer = outer_ctx;
      ctx->cb = outer_ctx->cb;
      ctx->cb.block = NULL;
      ctx->local_reduction_clauses = NULL;
      ctx->outer_reduction_clauses = ctx->outer_reduction_clauses;  // <=
      ctx->depth = outer_ctx->depth + 1;
    }
  ....
}

PVS-Studio警告:V570为其自身分配了'ctx-> outside_reduction_clauses'变量。omp-low.c 935

给自己分配一个变量很奇怪。

片段N4。0,1,2,弗雷迪会接您。

我最近发表了一篇文章“ 零,一,二,弗雷迪会带你走”在我看来,以下代码片段继续了本文讨论的错误的收集。

#define GET_MODE(RTX)    ((machine_mode) (RTX)->mode)
....
static int
add_equal_note (rtx_insn *insns, rtx target, enum rtx_code code, rtx op0,
                rtx op1, machine_mode op0_mode)
{
  ....
  if (commutative_p
      && GET_MODE (xop0) != xmode0 && GET_MODE (xop1) != xmode1
      && GET_MODE (xop0) == xmode1 && GET_MODE (xop1) == xmode1)
    std::swap (xop0, xop1);
  ....
}

PVS-Studio警告:V560条件表达式的一部分始终为false:
((machine_mode)(xop1)-> mode)== xmode1。optabs.c 1053

请注意这两个子表达式:

  • GET_MODE(xop1)!= Xmode1
  • GET_MODE(xop1)== xmode1

对这些子表达式的结果执行“与”运算,这显然没有任何实际意义。实际上,如果第二个子表达式开始执行,则预先知道它将导致false

最有可能的是,这里的错字是零和一,实际上情况应该是这样的:

&& GET_MODE (xop0) != xmode0 && GET_MODE (xop1) != xmode1
&& GET_MODE (xop0) == xmode1 && GET_MODE (xop1) == xmode0

当然,我不确定我是否正确更改了代码,因为我没有在项目中得到指导。

片段N5。参数值可疑变化

bool
ipa_polymorphic_call_context::set_by_invariant (tree cst,
                                                tree otr_type,
                                                HOST_WIDE_INT off)
{
  poly_int64 offset2, size, max_size;
  bool reverse;
  tree base;

  invalid = false;
  off = 0;                // <=
  ....
  if (otr_type && !contains_type_p (TREE_TYPE (base), off, otr_type))
    return false;

  set_by_decl (base, off);
  return true;
}

PVS-Studio警告:V763参数“ off”始终在使用前在功能体内重写。 ipa-polymorphic-call.c 766 off

参数的值立即替换为0。此外,没有解释性注释。这一切都是非常可疑的。有时,此代码在调试过程中出现。程序员需要查看函数在特定模式下的行为,因此他临时更改了参数的值,然后他们忘记删除此行。结果,错误出现在代码中。当然,这里可能一切都正确,但是显然需要检查并澄清此代码,以确保将来不会出现类似的问题。片段N6。小东西



cgraph_node *
cgraph_node::create_clone (....)
{
  ....
  new_node->icf_merged = icf_merged;
  new_node->merged_comdat = merged_comdat;   // <=
  new_node->thunk = thunk;
  new_node->unit_id = unit_id;
  new_node->merged_comdat = merged_comdat;   // <=
  new_node->merged_extern_inline = merged_extern_inline;
  ....
}

PVS-Studio警告:V519连续两次为'new_node-> merged_comdat'变量分配值。也许这是一个错误。检查行:406,409。cgraphclones.c 409分配是

随机重复的。很可能没有错。但是,实际上总是存在他们忘记执行某些其他任务的风险。

片段N7。看起来很危险的代码

static void
complete_mode (struct mode_data *m)
{
  ....
  if (m->cl == MODE_COMPLEX_INT || m->cl == MODE_COMPLEX_FLOAT)
    alignment = m->component->bytesize;
  else
    alignment = m->bytesize;

  m->alignment = alignment & (~alignment + 1);

  if (m->component)
  ....
}

PVS-Studio警告:V595在针对nullptr进行验证之前,已使用“ m->组件”指针。检查行:407,415。genmodes.c 407

首先,if语句的一个分支中取消引用m->组件指针。我的意思是这个表达式:m-> component-> bytesize 进一步证明该指针可以为空。这来自检查:if(m-> component) 此代码不一定是错误的。仅当指针不为null时才执行解引用分支。也就是说,变量m-> cl的值m->分量之间存在间接关系。



但是无论如何,此代码看起来非常危险。并且没有解释性评论。

片段N8。再检查一遍

void
pointer_and_operator::wi_fold (value_range &r, tree type,
                               const wide_int &lh_lb,
                               const wide_int &lh_ub,
                               const wide_int &rh_lb ATTRIBUTE_UNUSED,
                               const wide_int &rh_ub ATTRIBUTE_UNUSED) const
{
  // For pointer types, we are really only interested in asserting
  // whether the expression evaluates to non-NULL.
  if (wi_zero_p (type, lh_lb, lh_ub) || wi_zero_p (type, lh_lb, lh_ub))
    r = range_zero (type);
  else 
    r = value_range (type);
}

PVS-Studio警告:V501在“ ||”的左侧和右侧有相同的子表达式“ wi​​_zero_p(类型,lh_lb,lh_ub)” 操作员。range-op.cc 2657

某种奇怪的检查。使用相同的实际参数集两次调用wi_zero_p函数可能有人怀疑,实际上,第二个调用应该使用外部接受的参数:rh_lb,rh_ub。但是不,这些参数被标记为未使用(ATTRIBUTE_UNUSED)。

因此,我不清楚为什么不写支票更简单:

if (wi_zero_p (type, lh_lb, lh_ub))
  r = range_zero (type);
else 
  r = value_range (type);

还是这里有错字?还是逻辑错误?我不知道,但是代码很奇怪。

片段N9。危险的阵列访问

struct algorithm
{
  struct mult_cost cost;
  short ops;
  enum alg_code op[MAX_BITS_PER_WORD];
  char log[MAX_BITS_PER_WORD];
};

static void
synth_mult (struct algorithm *alg_out, unsigned HOST_WIDE_INT t,
            const struct mult_cost *cost_limit, machine_mode mode)
{
  int m;
  struct algorithm *alg_in, *best_alg;
  ....
  /* Cache the result.  */
  if (!cache_hit)
  {
    entry_ptr->t = t;
    entry_ptr->mode = mode;
    entry_ptr->speed = speed;
    entry_ptr->alg = best_alg->op[best_alg->ops];
    entry_ptr->cost.cost = best_cost.cost;
    entry_ptr->cost.latency = best_cost.latency;
  }

  /* If we are getting a too long sequence for `struct algorithm'
     to record, make this search fail.  */
  if (best_alg->ops == MAX_BITS_PER_WORD)
    return;
  ....
}

PVS-Studio警告:V781使用“ best_alg-> ops”变量后,将对其进行检查。程序逻辑中可能有一个错误。检查行:3157,3164。expmed.c 3157

让我们缩短代码以明确分析器不喜欢的内容:

if (!cache_hit)
{
  entry_ptr->alg = best_alg->op[best_alg->ops];
}
if (best_alg->ops == MAX_BITS_PER_WORD)

开始时,变量best_alg-> ops用于索引数组。然后,此变量才检查边界值。从理论上讲,可能会发生数组溢出(一种经典的错误类型CWE-193:Off-by-one Error)。

这是一个真正的错误吗?而且由于本文中这种情况一直在发生,所以我不确定:)。此索引的值和cache_hit变量之间可能存在关系。如果索引最大(MAX_BITS_PER_WORD),则可能不会缓存任何内容。函数代码很大,我没有弄清楚。

无论如何,最好检查此代码。即使事实证明是正确的,我还是建议在程序的考虑部分附带注释。它不仅会使我或PVS-Studio困惑,还会使其他人感到困惑。

片段N10。四年未修复的代码,

在上一篇文章中,我提请注意此代码:

static bool
dw_val_equal_p (dw_val_node *a, dw_val_node *b)
{
  ....
  case dw_val_class_vms_delta:
    return (!strcmp (a->v.val_vms_delta.lbl1, b->v.val_vms_delta.lbl1)
            && !strcmp (a->v.val_vms_delta.lbl1, b->v.val_vms_delta.lbl1));
  ....
}

PVS-Studio警告:V501'&&'运算符的左侧和右侧有相同的子表达式'!Strcmp(a-> v.val_vms_delta.lbl1,b-> v.val_vms_delta.lbl1)。dwarf2out.c 1481

两个strcmp函数比较相同的指针。即,执行明显多余的检查。在上一篇文章中,我建议这是一个错字,应该写成:

return (   !strcmp (a->v.val_vms_delta.lbl1, b->v.val_vms_delta.lbl1)
        && !strcmp (a->v.val_vms_delta.lbl2, b->v.val_vms_delta.lbl2));

但是,四年来此代码尚未修复。同时,我们向作者介绍了本文中描述的可疑代码部分。现在我不太确定这是一个错误。也许这只是冗余代码。在这种情况下,可以简化表达式:

return (!strcmp (a->v.val_vms_delta.lbl1, b->v.val_vms_delta.lbl1));

让我们看看GCC开发人员是否会在撰写新文章后更改这段代码。

结论


提醒您,您可以使用此免费许可证选项来检查打开的项目顺便说一下,还有其他免费许可PVS-Studio的选项,甚至包括封闭项目。它们在此处列出:“ PVS-Studio免费许可选项 ”。

谢谢您的关注。并阅读我们的博客有很多有趣的事情。

我们有关检查编译器的其他文章


  1. LLVM检查(Clang)(2011年8月),第二次检查(2012年8月),第三次检查(2016年10月),第四次检查(2019年4月)
  2. GCC评论(2016年8月)
  3. 检查华为方舟编译器(2019年12月)
  4. 验证.NET编译器平台(“ Roslyn”)(2015年12月),第二次检查(2019年4月)
  5. 罗斯林分析仪评论(2019年8月)
  6. 检查PascalABC.NET(2017年3月)



如果您想与讲英语的读者分享这篇文章,请使用翻译链接:Andrey Karpov。使用PVS-Studio检查GCC 10编译器

All Articles