printf()中%n格式说明符

本文探讨了C语言中不常见的%n格式说明符的作用及其使用场景,并通过多个实例展示了如何利用%n来记录已打印字符的数量。此外,还讨论了在不同编译环境下%n的表现差异及调试技巧。
一 遇到%n

昨天在写scanf 的输入异常处理时遇到了一个从未见过的格式说明符:%n

sscanf(str, "%d %n", &v, &c)

从运行结果来看,c的值是str的长度。


二 Stack Overflow 上关于%n 的QA

于是我在stack overflow上找到了关于这个格式说明符的QA。
What is the use of the %n format specifier in C?

第一个回答是:

Nothing printed. The argument must be a pointer to a signed int, where the number of characters written so far is stored.

意思是,不会打印任何东西。 这个参数必须是一个有符号整数的指针,它存储它出现之前打印的所有字符数。

并给了一个例子:

#include <stdio.h>

int main()
{
  int val;

  printf("blah %n blah\n", &val);

  printf("val = %d\n", val);

  return 0;

}

结果是:

blah  blah
val = 5

第二个答案给了另一个例子:

int n;
printf("%s: %nFoo\n", "hello", &n);
printf("%*sBar\n", n, "");

结果是:

hello: Foo
       Bar

三 我的运行异常和调试情况

我决定手动运行一下。

我的程序如下:

#include <stdio.h>

int main(void) 
{
    /* test 1*/
    int val;
    printf("blah %n blah\n", &val);
    printf("val = %d\n", val);

    /* test 2*/
    int n;
    printf("%s: %nFoo\n", "hello", &n);
    printf("%*sBar\n", n, "");

    return 0;
}

我先用MinGW的gcc进行编译运行:

gcc -g %n.c -o %n

结果运行时,一直在打印空格,控制台旁边的滑块迅速向下滑行,过了好一会才有字符出现。

我试着用gdb进行调试:

7               printf("blah %n blah\n", &val);
(gdb) p val
$1 = 4194432
(gdb) n
warning: Invalid parameter passed to C runtime function.

blah 8          printf("val = %d\n", val);
(gdb) p val
$2 = 4194432

发现声明变量时和运行printf("blah %n blah\n", &val) 后,val的值并没有发生改变,而且抛出了一个warning:Invalid parameter passed to C runtime function. 对于test 2也是同样的情况。

接着我试着用Developer Command Prompt for VS 2017对该程序进行编译运行:

cl %n.c
用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.10.25019 版
版权所有(C) Microsoft Corporation。保留所有权利。

%n.c
Microsoft (R) Incremental Linker Version 14.10.25019.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:%n.exe
%n.obj

运行:%n ,结果:
停止工作


四 用在线IDE进行测试

为什么我这里运行异常呢?

我另外找了两个在线IDE测试。以下是截图:

1、http://ide.geeksforgeeks.org/

Web IDE test1

2、http://codepad.org
Web IDE test2

结果都是正常。


五 如何解决“Invalid parameter passed to C runtime function”?

最后我的注意力转移到如何解决“ Invalid parameter passed to C runtime function.”这个问题上来了。

于是,我又跑到Stack Overflow上寻找答案了。how-to-debug-invalid-parameter-passed-to-c-runtime-function

第一个答案建议用gcc 配合选项进行设置调试命令。我试了一个用c99标准和c11标准,竟然都可以成功运行!

gcc -g %n.c -o %n_c99 -std=c99
%n_c99

blah  blah
val = 5
hello: Foo
       Bar

gcc -g %n.c -o %n_c11 -std=c11
%n_c11

blah  blah
val = 5
hello: Foo
       Bar

但是单纯的不加选项,就不能成功运行。

接着我找到了:warning-invalid-parameter-passed-to-c

按照里面的方式,我设置了断点并run:

(gdb) b OutputDebugStringA
(gdb) r
Starting program: C:\Users\\Desktop/%n_.exe
[New Thread 2748.0x26d8]
[New Thread 2748.0x167c]

Breakpoint 1, 0x773cb540 in OutputDebugStringA () from C:\WINDOWS\SysWOW64\KernelBase.dll
(gdb) bt
#0  0x773cb540 in OutputDebugStringA () from C:\WINDOWS\SysWOW64\KernelBase.dll
#1  0x7634811f in msvcrt!_invalid_parameter () from C:\WINDOWS\SysWOW64\msvcrt.dll
#2  0x762f5a40 in wctype () from C:\WINDOWS\SysWOW64\msvcrt.dll
#3  0x00010001 in ?? ()
#4  0x7636a9b7 in vswprintf () from C:\WINDOWS\SysWOW64\msvcrt.dll
#5  0x7636419b in printf () from C:\WINDOWS\SysWOW64\msvcrt.dll
#6  0x763a2628 in msvcrt!_iob () from C:\WINDOWS\SysWOW64\msvcrt.dll
#7  0x00405064 in __register_frame_info ()
#8  0x00401482 in main () at %n.c:7

看来问题比想象中复杂。我的初步猜想是Window环境下C语言在某些版本中,不允许 在printf中使用%n对于有符号整数的赋值。把这个问题放在这里,希望以后随着能力的增长,能够解决。

在 C++ 中,`printf` 的 `%n` 格式说明符用于将**已输出的字符数**写入到指定的变量中,而非直接输出内容。虽然功能强大,但它可能引发严重的**安全风险**,尤其是缓冲区溢出或未初始化内存写入。以下是详细分析和示例: --- ### **`%n` 的功能** - **作用**:将当前已输出的字符数写入到对应的参数指针指向的变量中。 - **示例**: ```cpp #include <cstdio> int main() { int count; printf("Hello%n world!\n", &count); // 输出 "Hello world!",并将 5 写入 count printf("Characters printed so far: %d\n", count); // 输出 5 return 0; } ``` - `"Hello"` 有 5 个字符,`%n` 会将 `5` 写入 `count`。 --- ### **安全风险** #### **1. 缓冲区溢出** - **问题**:如果 `%n` 对应的指针指向的内存区域未正确分配或过小,可能导致写入越界。 - **示例**: ```cpp char buffer[10]; int *ptr = (int*)buffer; // 假设 buffer 未对齐或空间不足 printf("%s%n", "Hello", ptr); // 可能破坏 buffer 后的内存 ``` - 若 `buffer` 不足以容纳 `int`(通常 4 字节),会覆盖相邻内存。 #### **2. 未初始化指针** - **问题**:传递未初始化的指针给 `%n` 会导致写入任意内存地址。 - **示例**: ```cpp int *ptr; // 未初始化! printf("Test%n", ptr); // 未定义行为:可能崩溃或破坏数据 ``` #### **3. 格式化字符串漏洞(Format String Attack)** - **风险**:如果格式字符串由用户输入控制(如 `printf(user_input)`),攻击者可构造 `%n` 恶意写入内存。 - **示例**: ```cpp // 危险代码:格式字符串来自用户输入 char user_input[100]; scanf("%s", user_input); printf(user_input); // 若 user_input 包含 "%n",可能写入任意地址 ``` - 攻击者可通过输入如 `"%x%x%x%n"` 泄露栈信息或修改关键变量。 --- ### **如何安全使用 `%n`?** 1. **确保指针有效**: - 仅传递已分配且足够大的内存地址。 ```cpp int count; printf("Safe%n", &count); // 正确:count 是栈上变量 ``` 2. **避免用户控制的格式字符串**: - 永远不要直接使用 `printf(user_input)`,改用 `printf("%s", user_input)`。 3. **使用更安全的替代方案**: - 在 C++ 中,优先使用 `std::cout` 或 `std::format`(C++20),避免 `printf` 的潜在风险。 --- ### **替代方案示例** #### **C++ 的 `cout`(无 `%n` 功能)** ```cpp #include <iostream> #include <string> using namespace std; int main() { string s = "Hello"; cout << s << " (Length: " << s.size() << ")" << endl; // 手动计算长度 return 0; } ``` #### **C++20 的 `std::format`** ```cpp #include <format> #include <iostream> using namespace std; int main() { int count = format("{}", "Hello").size(); // 计算字符数 cout << format("Hello (Length: {})\n", count); return 0; } ``` --- ### **关键点总结** 1. **`%n` 的功能**:将已输出字符数写入指针指向的变量。 2. **主要风险**: - 缓冲区溢出(指针指向内存不足)。 - 未初始化指针(写入任意地址)。 - 格式化字符串漏洞(用户控制 `%n` 的位置)。 3. **最佳实践**: - 避免在不可信输入中使用 `printf`。 - 在 C++ 中优先使用 `cout` 或 `std::format`。 ---
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值