cppcheck invalidFunctionArgBool 检查详解:如何用 <not-bool> 库配置捕获"伪布尔"实参

原创2026-10-03 17:18:441,680 阅读
文章标签:静态分析代码质量质量保障开发工具

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 与整数可以隐式互转,编译器通常不会对这种混用发出警告,于是两类问题很容易漏网:

  1. 布尔表达式被当作数值参数:memset(a, b, sizeof(a) != 12) 中第三个参数 sizeof(a) != 12 是布尔表达式,求值结果只能是 0 或 1,而 memset 的第三个参数 n 是 size_t 字节数。把"布尔结果"当作"缓冲区大小"传入,语义完全错误,编译却能通过。
  2. 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));
}

即布尔实参有三种出路:

  1. 命中 <not-bool/> → 报告 invalidFunctionArgBool(本主题);
  2. 未声明 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);
  3. 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 的整数"与"真正的布尔"这两类语义在静态分析阶段彻底区分开。

登录后查看全文
cppcheck