在C语言中,%n格式说明符有什么用途?

156

%n 格式说明符在 C 语言中有什么用途?能否举个例子解释一下?


35
阅读精细手册这门精艺术何去何从? - Jens
15
我认为真正的问题是这样的一个选项有什么意义?为什么有人想要知道打印字符数的值,更不用说直接将该值写入内存了。就好像开发人员很无聊,决定在内核中引入一个错误一样。 - jia chen
1
这就是为什么Bionic放弃它的原因。 - solidak
10
这实际上是一个合法的问题,而且精细的手册可能无法回答;已经发现 %n 使得 printf 意外地成为图灵完备的,你可以在其中实现 Brainfuck 等语言,参见 https://github.com/HexHive/printbf 和 http://www.oilshell.org/blog/2019/02/07.html#appendix-a-minor-sublanguages。 - John Frazer
12个回答

236

大多数答案都解释了%n功能(即不打印任何内容,将到目前为止打印的字符数写入一个int变量中),但是迄今为止没有人真正给出它的用途的示例。这里有一个:

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

将会打印:

hello: Foo
       Bar

使用Foo和Bar进行对齐。(在这个特定的例子中,没有使用%n也可以轻松实现此功能,而且通常可以拆分第一个printf调用:

int n = printf("%s: ", "hello");
printf("Foo\n");
printf("%*sBar\n", n, "");

使用像%n这样奇怪的东西是否值得为了稍微方便一点点(可能会引入错误)而采用,这还有待商榷。


4
哦,天啊——这是一个基于字符的计算给定字体中字符串像素大小的方法! - user3458
12
@AndrewS &n 是一个指针(&是取地址运算符);由于C语言是传值调用,没有指针,printf不能修改变量n的值。在printf格式字符串中使用%*s,可以使用长度为n个字符的字段宽度打印出一个%s说明符(在这种情况下是空字符串"")。解释基本的printf原理不属于这个问题(和答案)的范围;我建议阅读printf文档或在SO上提出自己的单独问题。 - jamesdlin
很好的答案,只有一件事詹姆斯。为什么字符串格式化符号中有星号%*s? - Andrew S
4
感谢您展示了一个用例。我不明白为什么人们有时只是把手册复制粘贴到SO,并重新措辞。我们是人类,每件事情都有其原因,这应该在答案中解释清楚。"Does nothing" 就像是说“cool这个词的意思是酷” - 几乎没有用的知识。 - the_endian
1
@PSkocik 这已经够复杂和容易出错了,不需要再增加额外的间接层。 - jamesdlin
显示剩余7条评论

165

没有任何输出。该参数必须是指向有符号整数的指针,存储到目前为止写入的字符数。

#include <stdio.h>

int main()
{
  int val;

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

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

  return 0;

}

前面的代码打印出:

blah  blah
val = 5

1
你提到参数必须是指向有符号整数的指针,但在你的示例中使用了无符号整数(可能只是一个笔误)。 - bta
1
@AndrewS:因为该函数将修改变量的值。 - Jack
6
@Jack: int 始终是带符号的。 - jamesdlin
1
@jamesdlin:我的错。对不起。我不知道我在哪里看到的。 - Jack
1
由于某种原因,示例引发了一个带有“n格式指定已禁用”的错误。这是什么原因? - Johnny_D
显示剩余5条评论

21

我没有真正看到过太多实际的现实世界使用%n指示符的例子,但我记得它在相当长一段时间以前与格式化字符串攻击一起在老式printf漏洞中使用。

像这样的东西

void authorizeUser( char * username, char * password){

    ...code here setting authorized to false...
    printf(username);

    if ( authorized ) {
         giveControl(username);
    }
}

一个恶意用户可以利用传递给printf的用户名参数作为格式字符串,并使用组合的%d%c或其他方式来遍历调用堆栈,然后将变量authorized修改为true值。

是的,这是一种晦涩的用法,但在编写守护进程以避免安全漏洞时,了解这一点总是很有用的? :D


3
避免使用未经检查的输入字符串作为 printf 格式字符串,除了 %n 之外还有更多的原因。 - Keith Thompson

16

前几天我遇到了这样一种情况:使用%n可以很好地解决我的问题。与我之前的答案不同,这次我想不出更好的替代方案。

我有一个GUI控件,用于显示某些指定的文本。该控件可以将文本的一部分以粗体(或斜体、下划线等)方式显示,并且我可以通过指定起始和结束字符索引来指定哪一部分。

在我的情况下,我正在使用snprintf向控件生成文本,并且我希望进行一次替换以使其中一个变量加粗。找到要替换的开始和结束索引是很困难的,因为:

  • 字符串包含多个替换值,其中一个替换值是任意用户指定的文本。这意味着对我关心的替换值进行文本搜索可能是模糊的。

  • 格式字符串可能本地化,并且可能使用$ POSIX扩展的位置格式说明符。因此,对于格式说明符本身,在原始格式字符串中进行搜索是不容易的。

  • 本地化方面还意味着我不能轻松地将格式字符串分成多个snprintf调用。

因此,找到特定替换值周围的索引最直接的方法是进行以下操作:

char buf[256];
int start;
int end;

snprintf(buf, sizeof buf,
         "blah blah %s %f yada yada %n%s%n yakety yak",
         someUserSpecifiedString,
         someFloat,
         &start, boldString, &end);
control->set_text(buf);
control->set_bold(start, end);

我会给你+1的使用情况。但是您即将无法通过审核,因此您应该想出另一种标记粗体文本开头和结尾的方法。看起来三个 snprintf 检查返回值就可以了,因为 snprintf 返回已写入字符的数量。也许可以像这样:int begin = snprintf(..., "blah blah %s %f yada yada", ...); 然后 int end = snprintf(..., "%s", ...); 然后是尾部:snprintf(..., "blah blah"); - jww
4
使用多个 snprintf 调用的问题在于,在其他语言环境中可能会重新排列替换,因此不能这样拆分。 - jamesdlin
谢谢提供示例。但是,你能不能写一个终端控制序列,在字段之前将输出加粗,然后在其后写入一个序列?如果不硬编码终端控制序列,你还可以使它们成为位置可调整的(可重新排序)。 - Petr Skocik
3
如果你要输出到终端,那么使用%n是更直接的选择。但如果你在使用Win32富文本控件,那么这并没有什么帮助,除非你想回去解析终端控制序列。这也假设你想在替换文本的其余部分中遵守终端控制序列;如果不是,那么你就必须过滤或转义它们。我并不是说不用%n就不可能实现;我只是认为使用%n比其他方法更简单明了。 - jamesdlin

15
到目前为止,所有的回答都是关于%n的作用,但没有解释为什么有人首先想要它。我发现在使用sprintf/snprintf时,它还是有些用处的,因为存储的值是结果字符串中的数组索引,之后你可能需要拆分或修改结果字符串。然而,这个应用程序在sscanf中更加有用,特别是因为scanf系列函数不返回处理的字符数,而是返回字段数。

另一个真正hackish的用途是在打印数字作为另一个操作的一部分时,同时获得免费的伪log10。


+1 提到了“%n”的用途,尽管我对“所有答案...”有不同的看法。 =P - jamesdlin
1
坏人感谢您使用printf /%n,sprintf和sscanf ;) - jww
11
@noloader: 怎么会呢?使用%n绝对不会有任何被攻击者利用的危险。%n被误解为不安全实际上是因为愚蠢的做法——在格式参数中传递消息字符串而不是格式字符串。当%n真正作为有意的格式字符串的一部分时,这种情况当然永远不会出现。 - R.. GitHub STOP HELPING ICE
%n 允许你写入内存。我认为你假设攻击者不能控制指针(可能我错了)。如果攻击者控制指针(它只是 printf 的另一个参数),他/她可以执行 4 字节的写入。但他/她是否能够从中获利是另一回事。 - jww
12
关于指针的任何用法都是如此,写*p = f();不会有人说“坏人感谢你”。为什么应该认为将结果写入指针所指向的对象的另一种方式%n是“危险”的,而不是认为指针本身是危险的? - R.. GitHub STOP HELPING ICE
显示剩余3条评论

14

这里我们可以看到,它存储了迄今为止已打印的字符数。

n 参数应该为一个指向整数的指针,该整数将被写入到目前为止通过此调用所调用的fprintf()函数输出的字节数。不进行参数转换。

例如用法:

int n_chars = 0;
printf("Hello, World%n", &n_chars);

n_chars 的值将会是 12


13
%n关联的参数将被视为int*并填充为在printf中该点打印的字符总数。

2

它不会打印任何东西。它用于计算在格式字符串中出现%n之前打印了多少个字符,并将其输出到提供的整数中:

#include <stdio.h>

int main(int argc, char* argv[])
{
    int resultOfNSpecifier = 0;
    _set_printf_count_output(1); /* Required in visual studio */
    printf("Some format string%n\n", &resultOfNSpecifier);
    printf("Count of chars before the %%n: %d\n", resultOfNSpecifier);
    return 0;
}

(_set_printf_count_output文档)

(说明:此处为原文,已翻译成中文。)

2
那些想要使用%n格式说明符的人可能需要看一下这个:
不要使用"%n"格式字符串说明符
在C语言中,在printf()和sprintf()等类型的函数中使用"%n"格式说明符可能会改变内存值。这些格式的不当设计/实现可能会导致由于内存内容的更改而产生漏洞。许多格式漏洞,特别是那些带有除"%n"之外的说明符的漏洞,会导致传统的故障,例如分段错误。"%n"说明符已经产生了更严重的漏洞。"%n"漏洞可能会产生次要影响,因为它们也可以成为计算和网络资源的重要消耗者,因为可能必须传输大量数据以生成所需的指针值以进行利用。避免使用"%n"格式说明符。使用其他方法来完成您的目的。
来源: 链接

1
一个现实世界的例子 https://github.com/Hamled/mazda-format-string-bug#readme - mx0
1
感谢mx0提供的现实世界示例,使用用户输入作为格式字符串是愚蠢的,不是%n在格式字符串中出现问题的例子。始终使用printf(“%s”,user_input)输出用户输入。 - Erik

1
它将存储在该printf()函数中迄今为止打印的字符数值。

例如:

int a;
printf("Hello World %n \n", &a);
printf("Characters printed so far = %d",a);

这个程序的输出将会是:
Hello World
Characters printed so far = 12

当我尝试运行你的代码时,它输出了:Hello World Characters printed so far = 36。为什么是36呢?我在Windows机器上使用32位GCC编译器。 - Mohammad Sina Karvandi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接