【问题标题】:Passing too many arguments to printf将太多参数传递给 printf
【发布时间】:2010-08-26 19:56:10
【问题描述】:

任何工作超过一周的 C 程序员都遇到过由于使用比实际参数更多的格式说明符调用 printf 而导致的崩溃,例如:

printf("Gonna %s and %s, %s!", "crash", "burn");

但是,当您将 太多 参数传递给 printf 时,是否会发生类似的坏事?

printf("Gonna %s and %s!", "crash", "burn", "dude");

我对 x86/x64 汇编的了解使我相信这是无害的,尽管我不相信我没有遗漏一些边缘条件,而且我不知道其他架构。这种情况是否可以保证是无害的,或者这里是否也存在潜在的崩溃陷阱?

【问题讨论】:

  • 不是你的问题的答案,堆垛机是正确的,但对于崩溃。 gcc 应该对此给出很好的警告,所以真的没有理由忽略那个;-)
  • GCC 怎么会给出警告呢?考虑到格式字符串不一定是常量字符串。可以是任何char *
  • GCC 可以在编译时知道格式字符串时给出很好的警告。由于这代表了printf 和朋友的大量合理用例,因此这些警告很有价值,应该引起注意。
  • gcc 也可以在格式字符串不是字符串文字时发出警告,如果我没记错的话。这是为了捕捉像printf(mystring); 这样的愚蠢东西。

标签: c printf


【解决方案1】:

Online C Draft Standard (n1256),第 7.19.6.1 节,第 2 段:

fprintf 函数将输出写入 stream 指向的流,在 format 指向的字符串的控制下,指定后续参数的方式 转换为输出。如果格式的参数不足,则行为是 不明确的。 如果格式已用尽而参数仍然存在,则多余的参数将 评估(一如既往),但在其他方面被忽略。 fprintf 函数在以下情况下返回 遇到格式字符串的结尾。

所有其他*printf() 函数的行为与vprintf() 之外的多余参数相同(显然)。

【讨论】:

  • 这是否适用于用户编写的可变参数函数?
  • @immibis:可能不是的主要原因是某些调用约定在返回时将被调用者从堆栈中弹出参数。这个规则本质上要求实现支持一般情况,或者对printf有很多特殊用途的支持。由于这个原因,大多数具有 callee-pops 调用约定的系统不会将它用于可变参数函数。 (而且 x86 的 ret imm16 指令要求弹出的字节数是编译时常量)。 printf 对于被调用者检测 args 数量的能力来说几乎是最坏的情况:多态,没有哨兵
【解决方案2】:

你可能知道 printf 函数的原型是这样的

int printf(const char *format, ...);

一个更完整的版本实际上是

int __cdecl printf(const char *format, ...);

__cdecl 定义了“调用约定”,它与其他内容一起描述了如何处理参数。在这种情况下,这意味着 args 被压入堆栈,并且堆栈被调用的函数清理。

_cdecl 的一个替代方案是__stdcall,还有其他的。对于__stdcall,约定是参数被压入堆栈并由被调用的函数清理。但是,据我所知,__stdcall 函数不可能接受可变数量的参数。这是有道理的,因为它不知道要清理多少堆栈。

总而言之,在__cdecl 函数的情况下,它可以安全地传递任何你想要的参数,因为清理是在进行调用的代码中执行的。如果您以某种方式将太多参数传递给__stdcall 函数,则会导致堆栈损坏。一个可能发生这种情况的例子是,如果您的原型错误。

有关调用约定的更多信息,请访问 Wikipedia here

【讨论】:

  • __cdecl 是一种 Win32 主义,是由于一些旧的 DOS 编译器同时支持 C 和 pascal 调用约定而创建的。
  • @ninjalj, __cdecl 仅受 MS 编译器支持,但有关调用约定的一般说明适用于所有操作系统。
  • @JSBangs:Borland 编译器 IIRC 也支持 __cdecl。此外,在大多数其他操作系统上,C 仅使用 C 调用约定(从右到左,调用者清理堆栈),可能带有 ISR 的变体,与其他编译器的兼容性,和/或将参数保存在寄存器上(GCC 的 regparm)。 AFAIK,Win32 是唯一可以选择不支持可变参数函数的调用约定的平台。
  • @ninjalj:基于 68000 的 Macintosh 操作系统几乎对所有内容都使用“Pascal”调用约定(称为函数弹出堆栈)。有点讽刺的是,实际上,因为在 68000 上,调用约定需要像这样的序列:“mov.l (A7+),A0 / addq #4,A7 / jmp (A0)”,而 C 调用约定允许使用“返回”指令。
  • -1 表示 MS-isms 好像它们是 C 语言的一部分。
【解决方案3】:

如果堆栈框架被删除,所有参数都将被压入堆栈并被删除。此行为独立于特定处理器。 (我只记得70年代设计的没有堆栈的大型机)所以,是的,第二个例子不会失败。

【讨论】:

    【解决方案4】:

    printf 旨在接受任意数量的参数。 printf 然后读取格式说明符(第一个参数),并根据需要从参数列表中提取参数。这就是为什么太少的参数崩溃的原因:代码只是开始使用不存在的参数、访问不存在的内存或其他一些坏事。但是如果参数太多,多余的参数将被忽略。格式说明符将使用比传入的更少的参数。

    【讨论】:

    • 此外,您的编译器还可能会删除额外的参数,如果它可以检测到它们未被使用。您必须查看程序集输出,以确定额外的参数是否真的传递给printf,或者它们是否被优化掉了。
    • 如果编译器确定它正在调用使用 printf 格式语言的东西(并且 GCC 有一个属性可以用来装饰你自己的类似 printf 的函数),那么它原则上是安全地进行此优化。它仍然必须表现得好像它已经计算了所有参数,以防任何未使用的参数碰巧有副作用。
    【解决方案5】:

    评论:gcc 和 clang 都会产生警告:

    $ clang main.c 
    main.c:4:29: warning: more '%' conversions than data arguments [-Wformat]
      printf("Gonna %s and %s, %s!", "crash", "burn");
                               ~^
    main.c:5:47: warning: data argument not used by format string 
                          [-Wformat-extra-args]
      printf("Gonna %s and %s!", "crash", "burn", "dude");
             ~~~~~~~~~~~~~~~~~~                   ^
    2 warnings generated.
    

    【讨论】:

    • 这个问题主要是在你生成自己的格式字符串时,在编译时是不知道的。
    猜你喜欢
    • 2012-08-09
    • 1970-01-01
    • 2014-02-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-27
    • 2010-11-06
    相关资源
    最近更新 更多