跟踪未初始化的静态变量
我需要调试一个丑陋而庞大的数学C库,可能是由f2c生成的。 代码滥用本地静态变量,不幸的是它在某处似乎利用了这些自动初始化为0的事实。如果使用相同的输入两次调用其入口函数,则它会给出不同的结果。 如果我卸载库并重新加载它,它可以正常工作。 它需要很快,所以我想摆脱加载/卸载。
我的问题是如何使用valgrind或任何其他工具发现这些错误,而无需手动遍历整个代码。
我正在寻找声明本地静态变量的地方,先读取,然后再写。 由于静态变量有时会通过指针进一步传递(是的 – 它太丑了),这个问题更加复杂了。
我理解人们可以争辩说,自动工具不应该检测到这样的错误,因为在某些情况下,这正是预期的行为。 还有,有没有办法让自动初始化的本地静态变量“脏”?
魔鬼在细节中,但这可能适合你:
首先,获得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
计算function依赖性。 您将在日志中找到这些“function依赖项”:
[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_1
, initialize_2
或add
应该执行的操作,您可以将此先验知识与分析结果进行比较,并查看initialize_2
和add
错误。
第二次编辑:现在我的例子显示了initialize_1
一些奇怪的东西,所以也许我应该解释一下。 变量state1
在p
用于写入state1
的意义上取决于p
,如果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 tc
,为initialize_index
计算的依赖项是:
[from] Function initialize_index: t[0..9] FROM i (and SELF)
这意味着每个单元依赖于i
(如果i
是该特定单元的索引,则可以修改它)。 每个单元格也可以保留其值(如果i
指示另一个单元格):这在最新版本中用(and SELF)
提及表示, (and default:true)
在先前版本中用更隐蔽(and default:true)
。
静态代码分析工具非常擅长查找典型的编程错误,例如使用未初始化的变量。 以下是为C执行此操作的免费工具列表。
不幸的是,我不能推荐列表中的任何工具。 我只熟悉两种商业产品, Coverity和Klocwork 。 覆盖率非常好(而且价格昂贵)。 Klocwork是如此(但更便宜)。
我最后做的是通过’#define static’从代码中删除所有静态限定符。 这会将未初始化的静态用法转换为无效使用,并且可以通过工具发现我正在搜索的滥用类型。
在我的实际情况中,这足以确定错误的位置,但在更一般的情况下,如果静态实际上正在做一些重要的事情,应该通过在代码无法继续时逐渐重新添加“静态”来改进它。
我不知道有任何库为你做这个,但我会考虑使用正则表达式来找到它们。 就像是
rgrep“static \ s * int”path / to / src / root | grep -v = | grep -v“(”
这应该返回所有声明没有等号的静态int变量,最后一个管道应该删除任何带括号的东西(摆脱function)。 有一个很好的改变,这对你来说无法正常工作,但玩grep可能是你跟踪它的最快方法。
当然,一旦找到一个有效的,你可以用所有其他类型的变量替换int来搜索它们。 HTH
我的问题是如何发现这些错误……
但这些都不是错误:静态变量初始化为0的期望是完全有效的,就像为其分配一些其他值一样。
因此,要求一个能够自动找到非错误的工具不太可能产生令人满意的结果。
根据您的描述,似乎somefunc()
在第一次调用时返回正确的结果,并在后续调用时返回错误的结果。
调试此类问题的最简单方法是并排进行两个GDB会话:一个是新加载的(将计算正确的答案),另一个是“第二次迭代”(将计算错误的答案)。 然后“并行”逐步执行两个会话,并查看其计算或控制流程开始发散的位置。
由于您通常可以有效地将问题分成两半,因此查找错误通常不需要很长时间。 总是重现的错误是最容易找到的。 去做就对了。