-
Notifications
You must be signed in to change notification settings - Fork 23
如何编写自定义规则检查器
注:在此文档中,将使用//
来表示仓库的根目录。
创建自定义规则集的过程如下:
- 创建规则集的基础文件和相关改动
- 编写自定义规则检查器(checker)
- 集成到容器镜像
-
在仓库的根目录新建一个以规则集命名的文件夹(比如
//toy_rules
),并在其中添加.gitignore
(参考//toy_rules/.gitignore
) -
为这个规则集新增
Run
函数(参考//toy_rules/analyzer/run.go
),并在//misra/analyzer/cmd/main.go
中调用它:func selectRun(rulePrefix string) (runFuncType, error) { switch rulePrefix { ... case "toy_rules": return toy_rules.Run, nil ... } }
-
在
//misra/analyzer/cmd/main.go
的ruleSets
里添加新建的规则集及其对应的语言(C/C++/both),它让镜像能找到这个新增的规则集。 -
如果需要用
go test
测试,在//cruleslib/testlib/testlib.go
的checkingStandards
里添加新建的规则集,它让testlib
能找到这个新增的规则集。
以//toy_rules/rule_1
为例,可以看到它的文件夹结构类似:
rule_1
├── _bad0001
│ ├── bad.cc
│ ├── expected.textproto
│ └── Makefile
├── _good0001
├── libtooling
├── rule_1_test.go
└── rule_1.go
其中,_bad0001
和_good0001
是存放测试用例的文件夹,_bad0001
是违规测试用例,_good0001
是合规测试用例。expected.textproto
是期望的测试结果,指定违规出现的位置与报错信息内容,由开发者编写,格式类似:
results {
path: "bad.cc"
line_number: 6
error_message: "NULL不得用作整型值"
}
在测试过程中,如果运行结果和期望测试结果完全一致,则测试通过。注意,即使是合规测试数据,也需要一个空的expected.textproto
。
libtooling
文件夹中存放的是该规则的 libtooling 实现。如果没有 libtooling 实现,则无此文件夹。
rule_1.go
中是调用 checker binary/script 的逻辑,它将调用//cruleslib/runner/runner.go
中不同工具的 runner(如RunLibtooling
、RunCppcheck
等)来指定所要运行检查的 checker。
rule_1_test.go
中是go test
测试的逻辑,比如
func TestBad0001(t *testing.T) {
tc := testcase.NewWithSystemHeader(t, "_bad0001")
tc.ExpectOK(testlib.ToTestResult(Analyze(tc.Srcdir, tc.Options)))
}
}
ExpectOK
即代表Analyze
函数的返回结果和expected.textproto
中的内容一致,ExpectFailure
则代表不一致。NewWithSystemHeader
函数是为了新建一个测试用例并添加一些系统库的路径。
- 在
//podman_image/bigmain/BUILD
里添加toy_rules_deps
和bigmain_toy_rules
cc_library(
name = "toy_rules_deps",
deps = [
"//toy_rules/rule_1/libtooling:rule_1_lib",
# 可以继续添加新的自定义规则
],
)
cc_binary(
name = "bigmain_toy_rules",
srcs = ["main.cc"],
deps = [
":rule",
":toy_rules_deps",
"//libtooling_includes:cmd_options",
"@com_github_google_glog//:glog",
"@com_google_absl//absl/strings:str_format",
],
)
- 在
//podman_image/bigmain_symlink
里添加
mkdir /opt/naivesystems/toy_rules
ln -s /opt/naivesystems/bigmain /opt/naivesystems/toy_rules/rule_1
- 新增一个文件
//podman_image/Containerfile.toyrules
。如果新增的规则集只检查 C 语言,则基于misrac
中创建镜像,如果要检查 C++,则基于misracpp
中创建,再将bigmain_toy_rules
拷入镜像。如果需要中文镜像,这里base_tag
用dev
而不是dev_en
。
ARG base_tag=dev
FROM naive.systems/analyzer/misracpp:${base_tag}
COPY "bigmain_toy_rules" "/opt/naivesystems/bigmain"
- 最后在
//podman_image/Makefile
中添加bigmain_toy_rules
和build-toy-rules-en
目标来生成镜像。
经过这些步骤之后,在//podman_image
下运行make build-toy-rules-en
,便可以得到一个包含新规则集的容器镜像。在待测试项目的.naivesystems/check_rules
文件中,添加toy_rules/rule_1
,便可运行以下指令,使用所生成的镜像进行代码静态分析:
podman run -v $PWD:/src:O -v $PWD/.naivesystems:/config:Z \
-v $PWD/output:/output:Z -w /src \
naive.systems/analyzer/toyrules:dev_en \
/opt/naivesystems/misra_analyzer -show_results -alsologtostderr
检测的所有结果将保存至所测试项目的output/results.nsa_results
。
一个 checker 需要选择合适的检查工具并实现,然后对其结果进行相应的处理,生成符合需求的 resultsList。
NaiveSytems Analyze 是用来检查项目中代码是否违规,包括但不限于资源泄漏、内存越界、栈地址逃逸等等。比如对于下列代码
long l = 100000;
int8_t i = 0;
i = l;
该代码可能存在精度丢失的问题,再比如对于下列代码
int i = 8 / 0;
该代码中存在除零错误。
我们一般将需要检查的问题分成两类,一类是 STU(single translation unit), 即只在单个翻译单元中就能查出来的错误,比如上面的精度缺失问题。另一类是 CTU(cross translation unit), 即需要跨多个翻译单元才能检查出来的错误,比如对于除零错误,可能有如下例子:
// test.h
int getDiv(int a, int b);
// test.cc
int getDiv(int a, int b) {
return a / b;
}
// main.cc
#include "test.h"
int main() {
getDiv(10, 0);
return 0;
}
这就需要test.cc
和main.cc
两个翻译单元一起看,才能检查出来错误。
从另一个角度,我们还可以将问题分成两种,一种是能直接在 AST 上看出来的问题,一种是需要深度分析才能解决的问题。比如对于精度缺失问题:
void test(void)
{
long l = 100000;
int i = 1;
i = l;
}
我们用 Clang dump 出 AST
$ clang -Xclang -ast-dump -fsyntax-only test.c
`-FunctionDecl 0x1208e23e8 <test.c:1:1, line:6:1> line:1:6 test 'void (void)'
`-CompoundStmt 0x1208e26e8 <line:2:1, line:6:1>
|-DeclStmt 0x1208e2588 <line:3:5, col:20>
| `-VarDecl 0x1208e24e8 <col:5, col:14> col:10 used l 'long' cinit
| `-ImplicitCastExpr 0x1208e2570 <col:14> 'long' <IntegralCast>
| `-IntegerLiteral 0x1208e2550 <col:14> 'int' 100000
|-DeclStmt 0x1208e2640 <line:4:5, col:14>
| `-VarDecl 0x1208e25b8 <col:5, col:13> col:9 used i 'int' cinit
| `-IntegerLiteral 0x1208e2620 <col:13> 'int' 1
`-BinaryOperator 0x1208e26c8 <line:5:5, col:9> 'int' '='
|-DeclRefExpr 0x1208e2658 <col:5> 'int' lvalue Var 0x1208e25b8 'i' 'int'
`-ImplicitCastExpr 0x1208e26b0 <col:9> 'int' <IntegralCast>
`-ImplicitCastExpr 0x1208e2698 <col:9> 'long' <LValueToRValue>
`-DeclRefExpr 0x1208e2678 <col:9> 'long' lvalue Var 0x1208e24e8 'l' 'long'
从 AST 上我们可以看出,有一些ImplicitCastExpr
是从long
到int
,这代表代码违规。这类问题的主要特征是可以从代码结构上直接看出代码违规。
但对于除零问题,除数可能是一个通过复杂计算得到的结果,比如:
int d = 5;
int i = 10 / (d - d);
这里我们需要简单算一下d-d
的结果,才能检查到错误。对于更复杂的情况,这个计算可能非常复杂,也可能是从另外一个地方得到的结果,需要使用深度分析去解决问题。
对于可以直接从 AST 上得出结论的问题,我们一般使用 libtooling、cppcheck、ClangSema、ClangTidy 等。对于需要深度分析才能解决的问题,我们一般使用 CSA 和 Infer。
libtooling 是 Clang 官方的工具,可以理解为 Clang 的一种插件。使用者通过编写 AST Matcher,匹配到想要 的 AST 节点,然后会自动调用预先设定好的回调(Callback)函数进行处理。
比如对于 MISRA C++:2008 4.10.1 NULL 不得用作整型值,我们所编写的代码如下:
class Callback : public MatchFinder::MatchCallback {
public:
void Init(ResultsList* results_list, MatchFinder* finder) {
results_list_ = results_list;
finder->addMatcher(
implicitCastExpr(hasSourceExpression(expr(gnuNullExpr())),
hasImplicitDestinationType(isInteger()),
unless(isExpansionInSystemHeader()))
.bind("cast"),
this);
}
void run(const MatchFinder::MatchResult& result) override {
const Expr* expr = result.Nodes.getNodeAs<Expr>("cast");
string error_message = "NULL不得用作整型值";
string path = GetFilename(expr, result.SourceManager);
int line = GetLine(expr, result.SourceManager);
AddResultToResultsList(results_list_, path, line, error_message);
}
private:
ResultsList* results_list_;
};
这段代码匹配了不在系统库中的,所有从gnuNullExpr
的sourceExpression
到isInteger
的implicitDestinationType
的implicitCastExpr
,匹配到后会自动调用回调函数run
,这里直接报错,结果添加至 resultsList 里。
它是 STU 类型的问题,所以调用时使用checker_integration.Libtooling_STU
指定类型:
runner.RunLibtooling(srcdir, "misra_cpp_2008/rule_4_10_1", checker_integration.Libtooling_STU, opts)
完整代码参考//misra_cpp_2008/rule_4_10_1
。
更多关于如何编写 libtooling checker 的细节,请查看 libtooling 的官方文档及相关教程。
关于 libtooling 文件夹的结构,以//toy_rules/rule_1
为例:
rule_1
├── libtooling
│ ├── BUILD
│ ├── checker.cc
│ ├── checker.h
│ ├── lib.h
│ ├── main.cc
│ └── rule_1.cc
-
checker.h
和checker.cc
中存放的是 checker 的具体实现 -
BUILD
存放的是 Bazel 的定义 -
main.cc
中包含调用 libtooling checker 的逻辑、参数的解析和 checker 分析结果写入指定文件的逻辑
除了 checker 的具体实现逻辑以外,其他的部分基本上所有实现都是差不多的。开发时,可以在此文件夹运行bazel build rule_1
来生成可供调用的 binary。
cppcheck 是一个开源工具,它已经被 check in 到我们的代码库中://third_party/cppcheck
。当我们编译后,会在该文件夹下生成一个名为cppcheck
的 binary。
我们分析代码的时候,会先用这个 binary 生成一个 dumpfile, 该文件是一个类似于 Clang AST 的文件信息结构,最后使用一个脚本去分析这个 dumpfile。比如假设我们有代码
#include <clocale> // Non-compliant
int main() {
return 0;
}
先生成 dumpfile
~/analyze/third_party/cppcheck/cppcheck --dump main.cpp
然后使用命令
~/analyze/third_party/cppcheck/cppcheck --abspath --dump --std=c99 --dump-file=main.cpp.c99.dump main.cpp
~/analyze/third_party/cppcheck/addons/misra.py --check_rules=misra_c_2012/rule_15_1 --output_dir output bad1.cpp.c99.dump
就能得到分析结果。这些命令一般用于开发,实际调用时被包装在了 runner 里:
runner.RunCppcheck(srcdir, "misra_c_2012/rule_15_1", checker_integration.Cppcheck_STU, opts)
需要注意的是,cppcheck 对于只有 directives 的文件不能正常生成 dumpfile, 编写测试用例时需要再加入一个main
函数。
我们的 cppcheck 实现都在//third_party/cppcheck/addons/misra.py
里,可以搜索相关规则的名字(如misra_15_1
)来查看对应 checker 的内容。
以//toy_rules/rule_2
为例,在//third_party/cppcheck/addons/misra.py
需要添加一些调用的逻辑:
假如 toy_rules 是一个从未在misra.py
中出现过的规则集,需要
- 新建一个
executeToyRuleCheck
函数用来执行 checker
def executeToyRuleCheck(self, check_function, *args):
check_function(*args)
- 新建一个
ToyRuleResult
类用于封装 checker 的结果
class ToyRuleResult:
def __init__(self, path, line_num, err_msg, other_locations = None):
self.path = path
self.line_number = line_num
self.error_message = f'{err_msg}'
self.locations = [ErrorLocation(path, line_num)]
if other_locations is not None:
for loc in other_locations:
self.locations.append(ErrorLocation(loc.file, loc.linenr))
- 新建一个
reportToyRuleError
用于向 JSON list 添加结果和向 stdout/stderr 输出结果。error_message 具体格式可根据需求自行定义,此处只传入一个 error_id。
def reportToyRuleError(self, location, rule_num, other_locations = None):
if self.settings.verify:
self.verify_actual.append('%s:%d %d.%d.%d' % (location.file, location.linenr, rule_num))
else:
error_id = f"Rule-{rule_num}"
toyrule_severity = 'Required'
this_violation = '{}-{}-{}-{}'.format(location.file, location.linenr, location.column, rule_num)
# If this is new violation then record it and show it. If not then
# skip it since it has already been displayed.
if not this_violation in self.existing_violations:
self.existing_violations.add(this_violation)
self.current_json_list.append(ToyRuleResult(location.file, location.linenr, error_id, other_locations))
cppcheckdata.reportError(location, toyrule_severity, "", "toy", error_id)
if toyrule_severity not in self.violations:
self.violations[toyrule_severity] = []
self.violations[toyrule_severity].append('toy' + "-" + error_id)
结果的 JSON list 将存在//toy_rules/rule_2/_bad0001/output/tmp/test_run/test_run-*/cppcheck_out.json
中,内容大致为:
[
{
"path": "/home/username/analyze/toy_rules/rule_2/_bad0001/bad.c",
"line_number": 3,
"error_message": "Rule-2",
"locations": [
{
"path": "/home/username/analyze/toy_rules/rule_2/_bad0001/bad.c",
"line_number": 3
}
]
}
]
假如规则集已存在,将其中某条规则的 checker 实现添加至规则集:
- 添加实现的函数
toy_rule_2
(参考 MISRA C:2012 15.1 不应使用goto语句)
def toy_rule_2(self, data):
for token in data.tokenlist:
if token.str == "goto":
self.reportToyRuleError(token, 2)
- 将函数
toy_rule_2
添加至parseDump
列表,参数根据规则需要传入cfg
,data.rawTokens
或者dumpfile
等
if "toy_rules/rule_2" in rules_list or check_rules == "all":
self.executeToyRuleCheck(self.toy_rule_2, cfg)
之后runner
便可用toy_rules/rule_2
调用其对应的 cppcheck 实现并检查其输出:
runner.RunCppcheck(srcdir, "toy_rules/rule_2", checker_integration.Cppcheck_STU, opts)
ClangSema 是我们利用 Clang diagnostic flags 来检查一些问题的工具,一般来说,如果 vscode 的 Clang 插件提示代码有问题或者在编译时报了 warning,以 AUTOSAR A-5-3-3 Pointers to incomplete class types shall not be deleted. 为例,编译时它会报 warning:
bad1.cpp:6:13: warning: deleting pointer to incomplete type 'C::Impl' may cause undefined behavior [-Wdelete-incomplete]
delete pimpl;
^ ~~~~~
我们就可以在 Analyze 里直接以 -Wdelete-incomplete
这个 diagnostic flag 调用 ClangSema,并根据其返回的 err 里是否有相关关键词来检查:
results, err := runner.RunClangSema(srcdir, "-Wdelete-incomplete", opts)
完整示例在//autosar/rule_A5_3_3
。
ClangSema 能检查的问题列表在 DiagnosticsReference。
ClangTidy 也是 Clang 官方提供的工具, 它内部也是用 libtooling 实现的,相当于他帮我们写好了一些 libtooling 的 checker, 我们可以直接使用:
runner.RunClangTidy(srcdir, args, opts)
其中 args 是一个 string list,可以同时用多个 checker 检查同一个规则,比如示例//autosar/rule_A7_5_2
。
ClangTidy 能检查的问题列表在 https://clang.llvm.org/extra/clang-tidy/checks/list.html
请先阅读 CSA(Clang Static Analyzer)的官方文档。总的来说,CSA 是一个基于符号执行技术的路径敏感的过程间分析工具,我们用它来解决需要依赖状态的问题,比如上面的除零问题。
我们同样将 CSA check in 到了我们的代码库当中。你可以在//third_party/llvm-project/clang/lib/StaticAnalyzer/Checkers
中找到当前已经实现的
checker,其中一部分是 Clang 自带的,带有规则集名字的是我们自己实现的。
以DivZeroChecker
这个 checker 为例,文件中的checkPreStmt
是一个 callback 函数,CSA 会在每次遇到新的Stmt
的时候自动调用这个函数,这时我们检查BinaryOperator
是不是除法,如果是的话,拿到他的 rhs,也就是除数。通过ConstraintManager
来判断 rhs 有没有为 0 的可能。其中ConstraintManager
和CheckerContext
会存储变量和符号的状态,如果可能为 0 则报错。
具体的 checker 实现后,可以用-analyzer-checker
来调用相关的 CSA checker,可以同时调用多个 CSA checker,用,
分隔。
runner.RunCSA(srcdir, "-analyzer-checker=core.DivideZero", opts)
相关示例在//autosar/rule_A5_6_1
和//autosar/rule_A0_4_4
。
Infer 是一个类似于 CSA 的工具,它能检查的问题列表在 all-issue-types。
我们用对应的 issue type 来调用
runner.RunInfer(srcdir, "--liveness", opts)
相关示例在//misra_c_2012_crules/rule_2_2
。
还有一些问题我们只需要对文件进行字符串处理就可以解决,比如//autosar/rule_A13_6_1
Semgrep 是一个基于模式(Pattern)
匹配来识别代码中的特定结构或模式,从而进行静态分析的工具,允许用户更灵活地自定义检测规则。它支持多种编程语言,包括但不限于 Python、JavaScript、Go、Java、C/C++ 和 Ruby。
想要自定义 Semgrep 规则,请按照以下步骤进行:
- 根据 规则语法,在
.naivesystems
编写*.semgrep
文件,它将描述所自定义规则的所有相关信息。以下为示例//semgrep/double_free.semgrep
:
rules:
- id: double-free
patterns:
- pattern-not: |
free($VAR);
...
$VAR = NULL;
...
free($VAR);
- pattern-not: |
free($VAR);
...
$VAR = malloc(...);
...
free($VAR);
- pattern-inside: |
free($VAR);
...
$FREE($VAR);
- metavariable-pattern:
metavariable: $FREE
pattern: free
- focus-metavariable: $FREE
message: Variable '$VAR' was freed twice. This can lead to undefined behavior.
languages:
- c
severity: ERROR
其中,各个模式之间的逻辑关系由各个 运算符 定义:
-
pattern-not
查找与其表达式不匹配的代码 -
pattern-inside
保留在其表达式内的匹配结果,一般用于在代码块(block)中查找代码 -
metavariable-pattern
将元变量与模式公式进行匹配,一般用于根据元变量的值过滤结果 -
focus-metavariable
将焦点置于单个元变量或元变量列表
patterns
的评估顺序不受声明顺序影响,将按正模式(pattern-inside
、pattern
等)-> 负模式(pattern-not
等)-> 条件(metavariable-pattern
等) -> 焦点元变量(focus-metavariable
)的顺序进行。
模式的编写需要遵循 模式语法:
-
free()
匹配名为 free 的 函数调用 -
...
省略号 匹配零个或多个item的序列,例如参数、语句、参数、字段、字符 -
$
匹配 元变量,即事先不知道值或内容时匹配代码的抽象,可用于跟踪特定代码范围内的值,包括变量、函数、参数、类、对象方法、导入、异常等
除了模式,此文件中必须指定的字段还包括 id
、message
、languages
和 severity
。
在 Playground 进行更多的尝试,查看更多的 示例。
- 在
.naivesystems
新建文件semgrep_rules
,将编写好的*.semgrep
规则文件的相对路径(或者绝对路径)列在里面:
double_free.semgrep
-
运行镜像得到 Semgrep 的规则结果,将同样保存至
output/results.nsa_results
。 -
如果需要
go test
测试,在*test.go
里引用runner.RunSemgrep
:
func TestBad0001(t *testing.T) {
tc := testcase.NewWithoutOptions(t, "_bad0001")
tc.ExpectOK(testlib.ToTestResult(runner.RunSemgrep(tc.Srcdir, filepath.Dir(tc.Srcdir), testlib.GetSemgrepBinPath())))
}
Coccinelle 是一个程序匹配和转换引擎,它基于 SmPL(Semantic Patch Language),用于在 C 代码中寻找指定的匹配和转换。Coccinelle 最初是被用来进行代码修改,比如重命名函数、修改函数参数、重新组织数据结构等,但由于它的特性,现在也被用于寻找代码中的错误。
想要自定义 Coccinelle 规则,请按照以下步骤进行:
- 根据 SmPL 语法 ,在
.naivesystems
编写*.cocci
文件,用以匹配指定的代码模式。以下为示例 //coccinelle/find_alloca.cocci:
@@ expression E; @@
-alloca(E)
+malloc(E)
SmPL 格式上有些类似 diff 文件,以上模式只会匹配 alloca()
的调用,而不会匹配到注释中出现的 alloca()
。
- 在
.naivesystems
新建文件cocci_rules
,将编写好的*.cocci
规则文件的相对路径(或者绝对路径)列在里面。另外,如果需要自定义错误信息,可以以 json 的形式加在路径之后,如以下示例:
find_alloca.cocci {"error-message": "alloca() should not be used"}
-
运行镜像得到 Coccinelle 的规则结果,将同样保存至
output/results.nsa_results
。 -
如果需要
go test
测试,在*test.go
里引用runner.RunCoccinelle
:
func TestBad0001(t *testing.T) {
tc := testcase.NewWithoutOptions(t, "_bad0001")
tc.ExpectOK(testlib.ToTestResult(runner.RunCoccinelle(tc.Srcdir, filepath.Dir(tc.Srcdir))))
}
AST-GREP 是一个基于 AST 模式用于结构搜索和替换的代码分析工具,可用于代码结构搜索、lint、大规模重写,支持 C、Golang、Python 等多种编程语言。该工具无需对项目进行编译,可以自动检测项目代码的语言。用户可以使用直观的语法,轻松地添加新的自定义规则。
想要自定义 AST-GREP 规则,请按照以下步骤进行:
- 在
.naivesystems
编写*.yaml
文件,用以匹配指定的代码模式。以下为示例 //ast-grep/method_receiver/method_receiver.yaml:
id: method_receiver
message: Rewrite method to function call
language: c
rule:
pattern: $R.$METHOD($$$ARGS)
与官网示例 Rewrite Method to Function Call 相比,注意这里只需要指定 rule
,而不需要指定 transform
和 fix
,因为我们只进行 linting。其中的 pattern
遵循 Pattern 语法 编写,可以查看更多的 示例。
- 在
.naivesystems
新建文件ast_grep_rules
,将编写好的*.yaml
规则文件的相对路径(或者绝对路径)列在里面,如以下示例:
method_receiver.yaml
-
运行镜像得到 AST-GREP 的规则结果,将同样保存至
output/results.nsa_results
。 -
如果需要
go test
测试,在*test.go
里引用runner.RunAstGrep
:
func TestBad0001(t *testing.T) {
tc := testcase.NewWithoutOptions(t, "_bad0001")
tc.ExpectOK(testlib.ToTestResult(runner.RunAstGrep(tc.Srcdir, filepath.Dir(tc.Srcdir), testlib.GetAstGrepBinPath())))
}
我们还集成了一些其它的工具,比如可以根据 Clang(RunClangForErrorsOrWarnings
)、GCC(RunGCC
)、Cpplint(RunCpplint
)的报错来分析,具体可参考//cruleslib/runner/runner.go
。
如果只有一种 checker,一般 runner 输出的 resultsList 便已符合要求。如果需要使用多种 checker 来检查同一条规则,只需要最后将所有 checker 产生的 results 合并起来即可,相关示例在//misra_c_2012_crules/rule_2_2
。