我们正在使用 C 在 ARM 内核上构建系统(即嵌入式系统)。问题是:我们如何才能以正式的方式避免重入问题,以便我们确信所有重入错误都已消除。我猜这可能不是一个实际的愿望,但对于任何系统来说肯定很重要。
只是为了讨论,我想绘制 UML 图或拥有一个完整的状态机将是一个好的开始(但是在整个系统开发之后如何生成它呢?)。有关如何使用状态机/UML 图进行分析的任何建议?
我们正在使用 C 在 ARM 内核上构建系统(即嵌入式系统)。问题是:我们如何才能以正式的方式避免重入问题,以便我们确信所有重入错误都已消除。我猜这可能不是一个实际的愿望,但对于任何系统来说肯定很重要。
只是为了讨论,我想绘制 UML 图或拥有一个完整的状态机将是一个好的开始(但是在整个系统开发之后如何生成它呢?)。有关如何使用状态机/UML 图进行分析的任何建议?
快速修复,如果您怀疑某事:
int some_func(int x, int y)
{
static volatile int do_not_enter_twice = 0;
assert(!(do_not_enter_twice++));
/* some_func continued */
do_not_enter_twice--;
return whatever;
}
更长的答案:
使用一些工具制作调用图并从那里手动继续。
我不确定您要解决的问题,但让我做一个有根据的猜测。
第一点是确定可能有问题的功能。重入通过递归调用发生,递归调用可能会遍历多个嵌套调用,甚至被回调/依赖注入隐藏,或者被多个线程中使用的函数隐藏。
您可以绘制有向调用图。假设函数 A 调用 B 和 C,函数 B 调用 D、E 和 F,函数 C 什么都不调用,等等。多线程时为每个线程绘制这个。如果图中存在循环,则构成该循环的所有函数都需要是可重入安全的。在这种情况下,您可以忽略子分支。在多个线程中使用的函数也需要是安全的,但现在包括所有子分支,因为您不确切知道每个线程当前在哪里。当使用锁时,事情会变得越来越复杂,所以让我们暂时忽略这一点。
这一步肯定可以通过代码分析工具自动化。
既然功能已经确定,
DMS Software Reengineering Toolkit及其 C 前端是一个可以计算巨大调用图的工具。C前端用于解析C代码;DMS 内置了计算控制和数据流分析、指向分析和提取调用直接和间接调用通过指针事实的机制
DMS 已用于为包含3500 万行 C 代码(= 250,000 个函数)的 C 源代码系统构建调用图,然后从该调用图中提取信息。在构建像这样的大型图时,一个关键问题是尽可能准确地计算指向信息(完美地做到这一点存在理论上的硬性限制),以便间接函数调用保守地针对最少数量的误报目标。
就您而言,正如其他作者所指出的那样,要提取的信息是“有一个循环吗?” 在这个调用图中。
在这种规模下,您不想手动执行此操作,并且每次为生产构建做好准备时都需要重新执行此操作。所以机械化检查会很有意义。
我会做两件事来检查你的代码。彻底的小组代码审查(目的是只发现重入错误,而不是样式或其他错误)。其次,对问题进行实际攻击。
例如:
int myfunction(int x, int y) {
REENTRANCE_CHECK;
... body of function
}
现在您可以将#define REENTRANCE_CHECK 设为空(用于生产)或某些检查函数的代码永远不会重新输入。在启用这些检查的情况下运行你的测试(如果你没有测试,那么在你的设备上运行它并连接调试器),看看是否有任何问题。
同样,您可以添加调试逻辑来检测对全局状态的不正确更新。编写使用锁的代码(如果它们在已经持有时被获取,则断言。
像这样的东西:
int my_global;
DEFINE_GLOBAL_LOCK(my_global);
void my_dangerous_function() {
...
LOCK_GLOBAL(my_global);
.. some critical section of code that uses my_global.
UNLOCK_GLOBAL(my_global);
...
}
同样,DECLARE_GLOBAL_LOCK、LOCK_GLOBAL 和 UNLOCK_GLOBAL 可以被 #defined 为真正的锁定代码(当然你必须编写)用于测试,而对于生产它们可以被 #defined 为空。
这种方法只有在您找到并包装对全局状态的所有访问时才有效,但是通过搜索很容易。