【问题标题】:tracking uninitialized static variables跟踪未初始化的静态变量
【发布时间】:2011-10-19 14:21:05
【问题描述】:

我需要调试一个丑陋而庞大的数学 C 库,可能曾经由 f2c 生成。该代码滥用了 local 静态变量,不幸的是,它似乎在某个地方利用了这些自动初始化为 0 的事实。如果使用相同的输入调用其入口函数两次,则会给出不同的结果。如果我卸载库并重新加载它,它会正常工作。它需要很快,所以我想摆脱加载/卸载。

我的问题是如何在不手动遍历整个代码的情况下使用 valgrind 或任何其他工具发现这些错误。

我正在寻找声明局部静态变量的地方,先读取,然后再写入。由于静态变量有时通过指针进一步传递(是的 - 它太难看),这个问题甚至更加复杂。

我知道有人可能会争辩说,这样的错误不应该被自动工具检测到,因为在某些情况下,这正是预期的行为。不过,有没有办法让自动初始化的局部静态变量“变脏”?

【问题讨论】:

  • 有趣的问题,但 valgrind 不知道“本地”或“声明”。我认为这必须通过代码分析来完成,而不是可执行分析。
  • 我们使用 PC Lint,但它会产生大量警告,因此找到真正的热点有时就像在黑暗中钓鱼。

标签: c valgrind static-analysis


【解决方案1】:

魔鬼在细节中,但这可能对你有用:

首先,获取Frama-C。如果您使用的是 Unix,您的发行版可能有一个包。该软件包不会是最后一个版本,但它可能已经足够好了,如果您以这种方式安装它,它将为您节省一些时间。

假设您的示例如下所示,只是大得多以至于不明显是什么问题:

int add(int x, int y)
{
  static int state;
  int result = x + y + state; // I tested it once and it worked.
  state++;
  return result;
}

键入如下命令:

frama-c -lib-entry -main add -deps ugly.c

选项-lib-entry -main add 的意思是“查看函数add”。选项-deps 计算函数依赖关系。你会在日志中找到这些“功能依赖”:

[from] Function add:
     state FROM state; (and default:false)
     \result FROM x; y; state; (and default:false)

这列出了add 的结果所依赖的实际 输入,以及根据这些输入计算的实际 输出,包括读取和修改的静态变量。在使用之前正确初始化的静态变量通常不会作为输入出现,除非分析器无法确定它总是在被读取之前被初始化。

日志将state 显示为\result 的依赖项。如果您希望返回的结果仅取决于参数(意味着具有相同参数的两个调用产生相同的结果),这暗示这里可能有问题,变量state

上述行中显示的另一个提示是该函数修改了state

这可能有帮助,也可能没有。选项-lib-entry 意味着分析器不假定任何非常量静态变量在调用分析的函数时保持其值,因此这对于您的代码来说可能过于不精确。有很多方法可以解决这个问题,但您是否愿意花时间学习这些方法取决于您。

编辑:这是一个更复杂的例子:

void initialize_1(int *p)
{
  *p = 0;
}

void initialize_2(int *p)
{
  *p; // I made a mistake here.
}

int add(int x, int y)
{
  static int state1;
  static int state2;

  initialize_1(&state1);
  initialize_2(&state2);

  // This is safe because I have initialized state1 and state2:
  int result = x + y + state1 + state2; 

  state1++;
  state2++;
  return result;
}

在此示例中,相同的命令会产生结果:

[from] Function initialize_1:
         state1 FROM p
[from] Function initialize_2:
[from] Function add:
         state1 FROM \nothing
         state2 FROM state2
         \result FROM x; y; state2

您看到的initialize_2 是一个空的依赖项列表,这意味着该函数没有分配任何内容。我将通过显示一个明确的消息而不是一个空列表来使这个案例更清楚。如果您知道initialize_1initialize_2add 中的任何一个函数应该做什么,您可以将此先验知识与分析结果进行比较,并发现initialize_2 和@987654342 有问题@。

第二次编辑:现在我的示例对initialize_1 显示了一些奇怪的东西,所以也许我应该解释一下。变量state1 取决于p,因为p 用于写入state1,如果p 不同,那么state1 的最终值将不同。这是最后一个例子:

int t[10];

void initialize_index(int i)
{
  t[i] = 1;
}

int main(int argc, char **argv)
{
  initialize_index(argv[1][0]-'0');
}

使用命令frama-c -deps t.c,为initialize_index 计算的依赖关系是:

[from] Function initialize_index:
         t[0..9] FROM i (and SELF)

这意味着每个单元格都依赖于i(如果i 是该特定单元格的索引,则可以对其进行修改)。每个单元格也可以保留其值(如果i 表示另一个单元格):这在最新版本中用(and SELF) 提及,在以前的版本中用更模糊的(and default:true) 表示。

【讨论】:

  • 谢谢!这听起来很有用;不过我不会尝试,因为我已经用下面最原始的方法找到了我的错误。顺便说一句,Frama-C 可以跟踪通过指针传递的变量吗?在我的例子中,静态是发送的,它们可以在不同的函数中初始化。
  • @takbal 是的,它可以,但是在存在指针的情况下,您可能需要更精确地描述调用函数的内存状态。我将用一个更完整的例子来编辑我的答案。在frama-c.com/download/frama-c-value-analysis.pdf 2.5.2 节中,此分析用于检查完整的加密哈希函数的依赖关系,包括指针和所有内容(但不是最引人注目的示例,因为代码很干净,没什么可说的。
【解决方案2】:

静态代码分析工具非常擅长发现典型的编程错误,例如使用未初始化的变量。 Here 是为 C 执行此操作的免费工具列表。

很遗憾,我无法推荐列表中的任何工具。我只熟悉两个商业产品,CoverityKlocwork。覆盖率非常好(而且价格昂贵)。 Klocwork 是如此(但更便宜)。

【讨论】:

  • 为什么不将所有局部变量在声明时初始化为零?这是一个很大的代码库吗?
  • 是的,它很大。你是对的,我当然可以做一些正则表达式技巧来将所有定义转换为基于 0 的初始化,甚至是双精度的 NaN。这是一条我最后没有尝试过的路。
【解决方案3】:

我最后所做的是通过“#define static”从代码中删除所有静态限定符。这会将未初始化的静态使用变为无效使用,并且我正在寻找的滥用类型可以通过工具发现。

在我的实际情况中,这足以确定错误的位置,但在更一般的情况下,如果静态实际上正在做一些重要的事情,则应该通过在代码无法继续时逐渐重新添加“静态”来改进它.

【讨论】:

    【解决方案4】:

    我不知道有任何库可以为您执行此操作,但我会考虑使用正则表达式来查找它们。像

    rgrep "static\s*int" 路径/to/src/root | grep -v = | grep -v "("

    这应该返回所有声明为没有等号的静态 int 变量,并且最后一个管道应该删除其中带有括号的任何内容(摆脱函数)。有一个很好的变化,它并不完全适合你,但使用 grep 可能是你追踪它的最快方法。

    当然,一旦你找到一个有效的变量,你可以用所有其他类型的变量替换 int 来搜索这些变量。高温

    【讨论】:

    • 很遗憾,但实际上所有变量都在没有初始化的情况下声明为静态...这可能是 f2c 的错误。
    【解决方案5】:

    我的问题是如何发现这些错误...

    但是这些不是错误:静态变量被初始化为 0 的期望是完全有效的,就像给它分配一些其他值一样。

    因此,要求一个能够自动发现错误的工具不太可能产生令人满意的结果。

    根据您的描述,somefunc() 似乎在第一次调用时返回正确的结果,而在随后的调用中返回错误的结果。

    调试此类问题的最简单方法是并排设置两个 GDB 会话:一个是新加载的(将计算正确答案),另一个是“第二次迭代”(将计算错误答案)。然后“并行”遍历两个会话,看看它们的计算或控制流从哪里开始分歧。

    由于您通常可以有效地将问题一分为二,因此通常很快就能找到错误。 总是重现的错误是最容易找到的。做吧。

    【讨论】:

    • 正如我在原帖中所说,我理解从编译器/链接器的角度来看,这不是一个错误。问题更像是如何将这种用法变成可以自动检测到的错误。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-09-26
    • 1970-01-01
    • 2011-08-22
    • 2010-12-22
    • 1970-01-01
    相关资源
    最近更新 更多