- 开发工具
- 静态分析
- 代码质量
- 质量保障
【免费下载链接】cppcheck
static analysis of C/C++ code
本篇文章围绕 cppcheck 静态分析器中的invalidPrintfArgType_n检查器展开:它专门用于发现printf族调用中%n格式说明符与实参类型不匹配的问题。读完本文,你将掌握%n未定义行为的成因、该检查器在 lib/checkio.cpp 中的判定逻辑、完整修复示例,以及如何借助仓库内测试用例验证其行为。
检查器速览
invalidPrintfArgType_n是 cppcheck 的 I/O 检查族(CheckIO,实现于 lib/checkio.cpp)中针对%n格式说明符的类型检查器,官方文档位于 man/checkers/invalidPrintfArgType_n.md。其元信息如下:
| 属性 | 值 |
|---|---|
| 错误标识(Error ID) | invalidPrintfArgType_n |
| 消息模板 | %n in format string (no. N) requires 'int *' but the argument type is 'X'. |
| 类别(Category) | Undefined Behaviour(未定义行为) |
| 严重级别(Severity) | Warning / Portability |
| 适用语言 | C / C++ |
| 关联 CWE | CWE-686(函数调用参数类型错误) |
消息中的no. N表示该实参相对于格式字符串的第 N 个可变参数位置(从 1 开始),X为实际推导出的实参类型。从 lib/checkio.cpp 可以看到该告警以Certainty::normal(正常置信度)上报,无需--inconclusive即可触发;同时告警级别受getSeverity()控制(详见下文"严重级别切换"一节)。
它检测什么:%n与可写指针
printf家族中,%n是一个特殊的存在:它不像%s、%d那样从参数读取数据,而是将"到目前为止已输出的字符数"写入实参所指向的内存。因此标准要求%n的实参必须是一个指向整数的可写指针(int *)。
该检查器的职责是:当%n收到的实参"明显不是可写指针"时报警。官方文档明确了两类被检出的情形:
- 普通整数(如
int、long等非指针值); const限定的指针(不可写,典型如字符串字面量,其类型为const char *)。
值得强调的是,文档指出该检查器只验证实参是某个可写指针,而并不严格校验它必须是int *——也就是说,void *、char *等其它非const指针不会被本检查器报错,这是其与"类型完全精确匹配"类检查的差异点。
源码中的判定条件
在 lib/checkio.cpp 的checkFormatString()逐格式符解析逻辑中,case 'n'分支的触发条件如下:
case 'n': if ((argInfo.isKnownType() && (!argInfo.isArrayOrPointer() || argInfo.typeToken->strAt(-1) == "const")) || argListTok->tokType() == Token::eString) invalidPrintfArgTypeError_n(tok, numFormat, &argInfo); done = true; break;该条件可以拆解为三条触发路径:
- 实参是字符串字面量:
argListTok->tokType() == Token::eString,即传入"..."或宽字符串字面量(类型为const char */const wchar_t *,不可写)时必然报警; - 实参是已知类型的非指针/非数组值:
isKnownType() && !isArrayOrPointer(),即明确的标量(如int、long、std::string对象等)直接报警; - 实参指针带有
const修饰:typeToken->strAt(-1) == "const",即类型令牌前紧邻const的声明,属于不可写指针路径。
而一个普通的int *指针实参满足isArrayOrPointer()且不带const前缀,不会触发本检查器,从而避免对正确代码产生误报。
为什么这是未定义行为(Motivation)
%n会把当前已打印的字符个数写回实参指针所指向的地址。当传入的不是指针而是普通整数时,printf会把这个整数值当作地址去写入——这等同于向一个随机内存地址执行写操作,属于标准意义上的未定义行为,可能造成:
- 直接崩溃(访问了无效/受保护的内存页);
- 内存破坏(恰好命中某个可写地址,静默篡改数据);
- 安全风险:正因
%n可向任意地址写入,历史上被广泛用于格式化字符串攻击(%n配合精心构造的格式串可改写返回地址等),因此文档明确指出,部分平台与库实现出于安全考虑会直接禁用%n的写入。依赖%n的代码在这些平台上会得到完全不同(甚至被忽略)的行为,这也是其严重级别被标注为含 Portability(可移植性)的原因。
与%s(要求char *,读取字符串)和%p(要求指针,读取地址值)不同,%n是写操作,所以实参错误造成的后果通常更严重——读取错误类型最多产生垃圾值,而写入错误地址则直接破坏内存。这组"同族"检查器分别由invalidPrintfArgType_s与invalidPrintfArgType_p负责,实现思路一致。
如何修复(How to fix)
官方文档给出了最典型的错误写法与修复方式:
修复前(错误):
#include <cstdio> void f(int x) { printf("%n", x); // <- 需要一个 'int *',而不是 'int' }修复后(正确):
#include <cstdio> void f(int *x) { printf("%n", x); }更多修复场景
结合仓库测试用例(见下文),实际工程中常见的错误与正确形态还包括:
- 字符串字面量误用:
printf("%n", "s4");会以'const char *'类型被报告,应改为传入真正的可写指针; - 宽字符串字面量误用:
printf("%n", L"s5W");会以'const wchar_t *'类型被报告; - 类对象误用:
printf("%n", s);(std::string对象)会被报告为'std::string'类型; - 非指针整数值:
int i; printf("%n", i);会以'signed int'类型被报告,必须改为printf("%n", &i);; - 别名类型(typedef)实参:当实参类型是
size_t、ptrdiff_t等 typedef 别名时,告警级别会从 Warning 提升为 Portability(见下节),因为这类代码在不同平台上表现不一致的风险更高。
正确使用%n的典型场景是统计已输出的字符数,例如:
#include <cstdio> void f() { int n = 0; printf("hello%n", &n); // 将已输出的字符数(5)写入 n }若你的真实意图是打印某个指针的地址,请使用%p而非%n;若意图是输出整数,请使用%d/%u等整数格式符,并参考同族的 invalidPrintfArgType_sint 与 invalidPrintfArgType_uint 检查器。
源码实现:报错路径与严重级别切换
消息生成
当检测命中时,调用链为checkFormatString()→invalidPrintfArgTypeError_n()(声明于 lib/checkio.h,实现于 lib/checkio.cpp):
void CheckIOImpl::invalidPrintfArgTypeError_n(const Token* tok, nonneg int numFormat, const ArgumentInfo* argInfo) { const Severity severity = getSeverity(argInfo); if (!mSettings.severity.isEnabled(severity)) return; std::ostringstream errmsg; errmsg << "%n in format string (no. " << numFormat << ") requires \'int *\' but the argument type is "; argumentType(errmsg, argInfo); errmsg << "."; reportError(tok, severity, "invalidPrintfArgType_n", errmsg.str(), CWE686, Certainty::normal); }这段实现同时验证了官方文档中的多个细节:消息模板中的'int *'期望类型、no. N参数位置编号,以及错误标识invalidPrintfArgType_n与 CWE-686 的绑定。
严重级别切换
getSeverity() 决定了本检查器的告警最终以warning还是portability呈现:
Severity CheckIOImpl::getSeverity(const ArgumentInfo *argInfo) { return (argInfo && argInfo->typeToken && !argInfo->typeToken->originalName().empty()) ? Severity::portability : Severity::warning; }当实参类型带有originalName(即通过typedef引入的别名类型,如size_t、intptr_t等)时按Portability上报;普通内建类型按Warning上报。这与官方文档标明的 "Warning/Portability" 双重级别完全对应,也解释了为什么这类告警既提示未定义行为风险、又提示跨平台可移植性风险。若对应级别被--enable=/--disable=或--suppress=关闭,则invalidPrintfArgTypeError_n会在上报前直接返回。
测试用例验证
仓库的单元测试 test/testio.cpp 对该检查器行为有直接验证。printf侧的核心用例(test/testio.cpp)构造了多种实参形态并断言了精确的告警输出:
void foo(const int* cpi, const int ci, int i, int* pi, std::string s) { printf("%n", cpi); // 不报:const int * 仍被视为指针 printf("%n", ci); // 报:signed int printf("%n", i); // 报:signed int printf("%n", pi); // 不报:int * 正确 printf("%n", s); // 报:std::string printf("%n", "s4"); // 报:const char * }期望输出恰好覆盖了本文前述的全部触发路径:
[test.cpp:3]: (warning) %n in format string (no. 1) requires 'int *' but the argument type is 'signed int'. [test.cpp:4]: (warning) %n in format string (no. 1) requires 'int *' but the argument type is 'signed int'. [test.cpp:6]: (warning) %n in format string (no. 1) requires 'int *' but the argument type is 'std::string'. [test.cpp:7]: (warning) %n in format string (no. 1) requires 'int *' but the argument type is 'const char *'.紧随其后的用例还验证了宽字符串字面量场景(test/testio.cpp):
printf("%n", L"s5W"); // 报:(warning) %n ... requires 'int *' but the argument type is 'const wchar_t *'.此外,scanf侧的%n由invalidScanfArgType_int家族负责(test/testio.cpp 有大量TEST_SCANF_WARN/TEST_SCANF_NOWARN用例),可见 cppcheck 对读写两侧的%n均有覆盖。
相关检查器
- invalidPrintfArgType_s:同一思路用于
%s(要求char *); - invalidPrintfArgType_p:同一思路用于
%p(要求地址/指针); - invalidPrintfArgType_sint / invalidPrintfArgType_uint:
%d/%u等整数格式符的符号/无符号类型匹配检查; - invalidScanfArgType_s / invalidScanfArgType_int:
scanf侧的同类检查。
小结
invalidPrintfArgType_n是 cppcheck I/O 检查族中针对%n写操作特性的专项检查器。它从"实参必须是可写指针"这一语义出发,将普通整数、const限定指针(含字符串字面量)等明显非法实参识别为未定义行为,并以 Warning/Portability 双重级别、绑定 CWE-686 上报;其判定条件、消息生成与严重级别切换逻辑均可直接在 lib/checkio.cpp 中溯源,行为正确性由 test/testio.cpp 中的多组用例锁定。修复时只需将实参改为真正的非const指针(如&i),并在需要打印地址时改用%p。
- 开发工具
- 静态分析
- 代码质量
- 质量保障
【免费下载链接】cppcheck
static analysis of C/C++ code
相关推荐
Cppcheck 检查器详解:arithOperationsOnVoidPointer 与 void* 指针算术的可移植性隐患
Cppcheck 检查器详解:arithOperationsOnVoidPointer 与 void 指针算术的可移植性隐患 导读 arithOperation
开发工具静态分析代码质量质量保障cppcheck 检查器实战:constVariablePointer 与指针 const 正确性(const-correctness)
cppcheck 检查器实战:constVariablePointer 与指针 const 正确性(const correctness) 导读 本文围绕 cpp
开发工具静态分析代码质量质量保障Cppcheck 指针与整数赋值移植性检查(AssignmentAddressToInteger / AssignmentIntegerToAddress)深度解析
Cppcheck 指针与整数赋值移植性检查(AssignmentAddressToInteger / AssignmentIntegerToAddress)深度
开发工具静态分析代码质量质量保障
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考