news 2026/10/4 14:53:38

cppcheck invalidPrintfArgType_n 检查器深入解析:printf `%n` 实参必须是可写指针

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
cppcheck invalidPrintfArgType_n 检查器深入解析:printf `%n` 实参必须是可写指针
  • 开发工具
  • 静态分析
  • 代码质量
  • 质量保障

【免费下载链接】cppcheck

static analysis of C/C++ code

项目地址:https://gitcode.com/gh_mirrors/cpp/cppcheck
点击查看免费下载

本篇文章围绕 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++
关联 CWECWE-686(函数调用参数类型错误)

消息中的no. N表示该实参相对于格式字符串的第 N 个可变参数位置(从 1 开始),X为实际推导出的实参类型。从 lib/checkio.cpp 可以看到该告警以Certainty::normal(正常置信度)上报,无需--inconclusive即可触发;同时告警级别受getSeverity()控制(详见下文"严重级别切换"一节)。

它检测什么:%n与可写指针

printf家族中,%n是一个特殊的存在:它不像%s、%d那样从参数读取数据,而是将"到目前为止已输出的字符数"写入实参所指向的内存。因此标准要求%n的实参必须是一个指向整数的可写指针(int *)。

该检查器的职责是:当%n收到的实参"明显不是可写指针"时报警。官方文档明确了两类被检出的情形:

  1. 普通整数(如int、long等非指针值);
  2. 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

项目地址:https://gitcode.com/gh_mirrors/cpp/cppcheck
点击查看免费下载

相关推荐

上一篇:fish-shell 终端标题定制指南:全面解析 fish_title 与 fish_tab_title
下一篇:Mem Reduct中文界面终极配置指南:3种方法解决语言切换难题

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 14:53:25

插件系统工作原理与加载失败排查:从日志到实战

我昨天帮一个朋友排查他本地开发环境的问题&#xff0c;打开他的应用日志&#xff0c;一排一模一样的红字&#xff1a;failed to load plugins web boot: 2 entries did not activate。他问我这到底什么意思&#xff0c;是不是电脑中毒了。我解释了半天&#xff0c;后来发现不仅…

作者头像 李华
网站建设 2026/10/4 14:51:46

Claude API 缓存命中率优化四步法:大幅降低计费成本

1. 为什么缓存命中率是 Claude API 成本控制的命门做过大模型应用落地的朋友应该都有体会&#xff0c;API 账单里最让人肉疼的不是单次调用贵&#xff0c;而是同一段内容被反复计费。尤其是做 RAG 检索增强、多轮对话、Agent 工具调用这类场景&#xff0c;系统提示词、知识库片…

作者头像 李华
网站建设 2026/10/4 14:49:56

插件加载失败根因解析:plugin.json、TS SDK与CLI契约体系

1. 插件系统不是“附加功能”&#xff0c;而是现代开发工具的神经中枢你打开 Cursor、VS Code、JetBrains IDE&#xff0c;甚至 GitLab Web UI 或某些 CI/平台控制台时看到的那个“Extensions”或“Plugins”标签页——它从来不只是个可有可无的装饰栏。真正懂行的人知道&#…

作者头像 李华
网站建设 2026/10/4 14:49:22

AI Agent 的 TCP/IP 时刻:MCP 协议深度解析与 TaoToken 统一接入实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华