cppcheck invalidFunctionArgBool 检查详解:如何用 <not-bool> 库配置捕获"伪布尔"实参
cppcheck invalidFunctionArgBool 检查详解:如何用 库配置捕获"伪布尔"实参
导读:本文围绕 cppcheck 的 invalidFunctionArgBool 检查器(正确性 / Error 级别)展开,说明它如何在 C/C++ 代码中识别"把普通 0/1 整数或布尔表达式传给本应接收非布尔参数"的调用,并从 std.cfg、checkfunctions.cpp 与 testfunctions.cpp 出发,还原库配置声明、AST 判定、错误报告三个环节的完整实现链路,帮助你理解触发条件、修复方式,以及如何为自己的库编写 <not-bool/> 配置。
检查器速览:一条错误信息背后的四个维度
invalidFunctionArgBool 是 cppcheck 函数调用检查族(CheckFunctions)的一员,其完整定义见 invalidFunctionArgBool.md:
| 维度 | 取值 |
|---|---|
| Message | Invalid $symbol() argument nr N. A non-boolean value is required. |
| Category | Correctness |
| Severity | Error |
| Language | C/C++ |
| CWE | CWE-628(reportError 调用中传入,见 checkfunctions.cpp) |
| Certainty | normal |
它描述的场景是:调用方传入了纯 0/1 整数(或值为布尔表达式的实参),而被调函数根据库配置(.cfg 文件)明确要求"该参数必须是真正的非布尔值"。某些函数会刻意区分"真正的布尔参数"和"碰巧是 0 或 1 的任意整数"——传错类型可能被静默接受,但并不会执行预期的语义。
为什么需要它:memset(ptr, 0, sizeof(x) != 12) 这类"隐形错误"
C/C++ 中 bool 与整数可以隐式互转,编译器通常不会对这种混用发出警告,于是两类问题很容易漏网:
- 布尔表达式被当作数值参数:
memset(a, b, sizeof(a) != 12)中第三个参数sizeof(a) != 12是布尔表达式,求值结果只能是 0 或 1,而memset的第三个参数n是size_t字节数。把"布尔结果"当作"缓冲区大小"传入,语义完全错误,编译却能通过。 bool变量被传给整数语义的参数:void f(bool c) { memset(a, b, c); }同样是"布尔值流入非布尔参数"。
cppcheck 对这类问题的动机总结是:某些函数区分"真正的布尔实参"与"恰好为 0/1 的任意整数",传入错误种类的值可能被静默接受但不会达到预期效果(见 invalidFunctionArgBool.md)。例如 div()、strtol() 等函数的基参数、模式参数只接受特定范围的整数,而不是布尔折叠值——这正是同族检查器 invalidFunctionArg(数值范围)的职责边界。
实现链路一:库配置 <not-bool/> 从何而来
触发条件的第一步是 函数在某个 .cfg 库配置中被声明"该实参不接受布尔值"。配置格式为 <function> + <arg nr="N"> + <not-bool/>。
以标准库配置 std.cfg 中 memset 为例:
<!-- void *memset(void *s, int c, size_t n); -->
<function name="memset,std::memset">
<returnValue type="void *">arg1</returnValue>
<noreturn>false</noreturn>
<leak-ignore/>
<arg nr="1" direction="out" indirect="1">
<not-null/>
<minsize type="argvalue" arg="3"/>
</arg>
<arg nr="2" direction="in">
<not-uninit/>
</arg>
<arg nr="3" direction="in">
<not-uninit/>
<not-bool/>
<valid>0:</valid>
</arg>
</function>
- 第 3 个参数(
n)同时带<not-bool/>和<valid>0:</valid>,表示"非布尔"与"合法值 ≥ 0"两种约束叠加; - 同族的
wmemset(std.cfg)也在第 3 个参数上声明<not-bool/>; <not-bool/>在整个仓库的 cfg 目录中被大量使用:除了 std.cfg,avr.cfg、boost.cfg、bsd.cfg 等平台与第三方库配置均出现上百次,覆盖memcpy系列、各种数值处理与平台 API。
配置解析发生在 library.cpp:遍历 <arg> 的子元素时,遇到名为 not-bool 的节点即把 ac.notbool 置为 true,并存入该函数的 ArgumentChecks 结构。之后 library.h 提供查询接口:
bool isboolargbad(const Token *ftok, int argnr) const {
const ArgumentChecks *arg = getarg(ftok, argnr);
return arg && arg->notbool;
}
即"给定函数调用 token 与参数序号,返回该参数是否被配置为禁止布尔值"。若目标函数未被任何 .cfg 描述,则 getarg 返回空指针,isboolargbad 返回 false,检查不会触发——这是该检查器完全依赖库配置的原因,也解释了为什么自定义库需要显式添加 <not-bool/>。
实现链路二:astIsBool 如何判定"这是一个布尔实参"
第二步是判定实参是否真的是布尔。核心函数为 astutils.cpp 中的 astIsBool:
bool astIsBool(const Token *tok)
{
return tok && (tok->isBoolean() || (tok->valueType() && tok->valueType()->type == ValueType::Type::BOOL && !tok->valueType()->pointer));
}
它接受两种情况的 token:
- 直接就是布尔字面量/布尔关键字(
true、false,或 C 中由tok->isBoolean()识别的布尔表达式 token); - 值类型(
ValueType)为BOOL且不是指针——例如bool类型变量、比较表达式a < b、逻辑运算!c、sizeof(x) != 12等,cppcheck 通过类型推导系统(valueType)将它们识别为布尔类型。
因此以下表达式都会命中 astIsBool:true、false、!c、sizeof(a)!=12、sizeof(a)!=0、bool 类型变量 c、n < 255。而 n < 255 ? n : 255 这样的三目表达式结果是数值而非布尔,不命中。
实现链路三:invalidFunctionUsage 主循环与三种出路
检查主逻辑位于 checkfunctions.cpp 的 CheckFunctionsImpl::invalidFunctionUsage():遍历符号数据库中每个函数作用域内的调用 token(Token::Match(tok, "%name% ( !!)")),对每个实参依次执行三项检查。
针对布尔实参,checkfunctions.cpp 的分支逻辑如下:
if (astIsBool(argtok)) {
// check <not-bool>
if (mSettings.library.isboolargbad(functionToken, argnr))
invalidFunctionArgBoolError(argtok, functionToken->str(), argnr);
// Are the values 0 and 1 valid?
else if (!mSettings.library.isIntArgValid(functionToken, argnr, 0, mSettings))
invalidFunctionArgError(argtok, functionToken->str(), argnr, nullptr, mSettings.library.validarg(functionToken, argnr));
else if (!mSettings.library.isIntArgValid(functionToken, argnr, 1, mSettings))
invalidFunctionArgError(argtok, functionToken->str(), argnr, nullptr, mSettings.library.validarg(functionToken, argnr));
}
即布尔实参有三种出路:
- 命中
<not-bool/>→ 报告invalidFunctionArgBool(本主题); - 未声明 not-bool,但 0 或 1 不在
<valid>范围内 → 报告invalidFunctionArg。例如div(a, param > 0)中param > 0是布尔,而div第 2 个参数合法值是-1,1,0 不在其中,于是报Invalid div() argument nr 2. The value is 0 or 1 (boolean) but the valid values are ':-1,1:'. <a href="https://link.gitcode.com/i/60eb2e1a114d9e1a044226cff1b80364" target="_blank">invalidFunctionArg](测试见 [testfunctions.cpp);strtol(a, b, param > 0)同理,合法值0,2:36不含 1(testfunctions.cpp); - 0 和 1 都在合法范围内 → 不报错。
错误报告的最终实现是 checkfunctions.cpp 的 invalidFunctionArgBoolError:
void CheckFunctionsImpl::invalidFunctionArgBoolError(const Token *tok, const std::string &functionName, int argnr)
{
std::ostringstream errmsg;
errmsg << "$symbol:" << functionName << '\n';
errmsg << "Invalid $symbol() argument nr " << argnr << ". A non-boolean value is required.";
reportError(tok, Severity::error, "invalidFunctionArgBool", errmsg.str(), CWE628, Certainty::normal);
}
注意 $symbol 会被替换为实际函数名,因此输出形如 Invalid memset() argument nr 3. A non-boolean value is required. [invalidFunctionArgBool]。
如何修复:把真正的布尔换成数值/指针语义
官方文档 invalidFunctionArgBool.md 给出的修复示例是通用场景:
修复前(把普通 1 当作布尔传给 bool 参数):
void setFlag(bool enabled);
void f() {
setFlag(1); // <- treat this as a real boolean, not an arbitrary int
}
修复后(使用真正的布尔字面量):
void setFlag(bool enabled);
void f() {
setFlag(true);
}
说明:文档示例展示的是"把 1 视为真正的布尔"这一思想。当被检查函数声明
<not-bool/>时,实际场景恰好相反——需要把布尔表达式换成非布尔数值,详见下方memset实测。
结合源码与测试,针对 <not-bool/> 参数(如 memset 第 3 参数)的实战修复是把布尔表达式改写为真正的 size_t 数值:
// 修复前:布尔表达式被当作字节数
void record(char* buf, int n) {
memset(buf, 0, n < 255); // error: 第 3 参只能是 0 或 1
}
// 修复后:三目表达式返回数值而非布尔
void record(char* buf, int n) {
memset(buf, 0, n < 255 ? n : 255); // ok:n<255 时传 n,否则传 255
}
实测证据:testfunctions.cpp 中的 8 组断言
测试用例集中在 testfunctions.cpp 的 invalidFunctionUsage1(),可直接验证上述触发与修复规则:
| 被测代码 | 期望输出 |
|---|---|
void f() { memset(a,b,sizeof(a)!=12); } |
Invalid memset() argument nr 3. A non-boolean value is required. |
void f() { memset(a,b,sizeof(a)!=0); } |
同上 |
void f() { memset(a,b,!c); } |
同上 |
void f(bool c) { memset(a,b,c); }(Ticket #6990) |
同上 |
void f() { memset(a,b,true); } |
同上 |
void record(...) { memset(buf, 0, n < 255); }(C 模式,Ticket #6588) |
同上 |
void record(...) { memset(buf, 0, n < 255); }(C++ 模式) |
同上 |
void record(...) { memset(buf, 0, n < 255 ? n : 255); } |
无输出 |
这组测试同时证明了:无论实参来自比较表达式、逻辑非、布尔变量还是 true 字面量,只要 astIsBool 判定为布尔且配置了 <not-bool/>,就会报告 invalidFunctionArgBool;而返回数值的三目表达式则不会误报。
与同族检查器的分工
该检查器在 cppcheck 中属于"函数实参合法性"三兄弟之一,官方文档互相关联(原文档 invalidFunctionArgBool.md 的 Related checkers 一节):
| 检查器 | 针对的实参问题 | 示例 |
|---|---|---|
| invalidFunctionArgBool | 传入"伪布尔"(纯 0/1 或布尔表达式),而参数要求真正的非布尔值 | memset(a,b,sizeof(a)!=12) |
| invalidFunctionArg | 数值实参超出函数可接受的合法范围 | strtol(a, b, 1)(基数 1 非法) |
| invalidFunctionArgStr | 非 NUL 结尾的 char/wchar_t 缓冲区被当作 C 字符串传入 |
strlen(&x)(单字符) |
三者共享同一套 invalidFunctionUsage() 主循环(checkfunctions.cpp),分别由 <not-bool/>、<valid>、<strz/> 配置驱动,且全部以 CWE628 报告错误(invalidFunctionArgStr 归类为 Undefined Behaviour 类别,其余为 Correctness)。
如何为自己的库添加 <not-bool/> 配置
如果某个第三方库的 API 也要求"实参必须是非布尔值",只需在对应的 .cfg 文件(或 --library 指定的自定义配置)中声明:
<def>
<function name="my_lib_set_mode">
<arg nr="1">
<not-bool/>
</arg>
</function>
</def>
随后运行:
cppcheck --library=my_lib --enable=warning,style --check-level=exhaustive src/
关键点:
<not-bool/>必须写在<arg nr="N">内,解析器在 library.cpp 处识别该节点并设置notbool标记;- 若同时声明
<valid>,布尔实参(值只能是 0/1)与合法范围冲突时,astIsBool分支会优先走<not-bool/>路径;未声明<not-bool/>时则退回invalidFunctionArg的 0/1 合法性判断; - 由于该检查依赖
astIsBool的值类型推断,--check-level=exhaustive能提供更完整的数据流信息,减少漏报(参考 checkfunctions.cpp 中getInvalidValue与值类型的使用)。
小结
invalidFunctionArgBool 是 cppcheck 中"配置驱动的实参语义检查"的典型代表:库配置(<not-bool/>)声明约束,AST 类型推断(astIsBool)识别布尔实参,CheckFunctions 主循环完成报告。理解这三环,你既能准确解读 Invalid memset() argument nr 3 这类错误,也能为自己的库编写精确的参数约束配置,把"碰巧是 0/1 的整数"与"真正的布尔"这两类语义在静态分析阶段彻底区分开。