"Puts()"函数如何在没有参数的情况下工作?

3

我在一个网站上看到了这段代码。

main(i)
{
  gets(&i);
  puts();
}

这段代码编译并运行良好!

它从用户那里获取一个字符串输入并将其打印出来!!!

但是,我的问题是,怎么做到的呢?

(注意,puts() 函数不含任何参数!)


8
@R: 什么时候这些愚蠢的“你为什么问这个问题?”的评论才会停止呢?他看到了一些让他感到困惑的东西,所以他提出了一个清晰明了地阐述问题的问题。这有什么不好吗?为什么你们认为 _每个人_都已经知道什么是未定义行为,或者 _每个人_都已经理解未定义行为可能不会被编译器捕获?新手开发人员可能什么都不知道,而我们在这里就是要帮助这样的人。 - Nicholas Knight
@Nicholas Knight:我认为这些练习很有趣,因为它们展示了某些东西看起来可能是偶然的。类似这样有缺陷的程序很可能会通过一些开发团队的测试,并被认为是“我们信任的好代码”,但它很容易因为各种原因而崩溃。对于了解这种类型的知识也有助于找出为什么某些事情不起作用或发现一些安全漏洞。 - nategoose
5个回答

5

旧版本的C语言对变量和函数有隐式类型,这段代码利用了这一点以及其他一些东西。它也非常宽松地处理实际返回值的问题。

main(i) // i is implicitly an integer (the default type for old C), and normally named argc 
// int main(int i) or void main(int i)
{ // The stack (which lives in high memory but grows downward) has any arguments and
  // probably the environmental variables and maybe even other (possibly blank/filler)
  // stuff on it in addition to the return address for whatever called main and possibly
  // the argument i, but at this point that could either be on the stack just under the
  // return address or in a register, depending on the ABI (application binary interface)


// extern int gets(int) or extern void gets(int)
// and sizeof(int) is probably sizeof(char *)
 gets(&i); // By taking the address of i even if it wasn't on the stack it will be pushed to
           // it so that it will have an address (some processors have addressable registers
           // but they are rarely used by C for many reasons that I won't go into).


           // The address of i is either also pushed onto the stack or put into a register
           // that the ABI says should be used for the first argument of a function, and
           // and then a call is made to gets (push next address to stack; jump to gets)

           // The function gets does what it does, but according to the ABI there are
           // some registers that it can do whatever it wants to and some that it must
           // make sure are the same as they were before it was called and possibly one
           // or more where it is supposed to store a return value.
           // If the address of i was passed to it on the stack then it probably would be
           // restricted from changing that, but if it was passed in a register it may
           // have just been luckily left unchanged.
           // Another possiblity is that since gets returns the string address it was
           // passed is that it returns that in the same location as the first argument
           // to functions is passed.  

 puts();   // Since, like gets, puts takes one pointer argument it will be passed this
           // this argument in the same way as gets was passed it's argument.  Since we
           // were somehow lucky enough for gets to not overwrite the argument that we
           // passed to it and since the C compiler doesn't think it has anything new to
           // pass to puts it doesn't change any registers' values or do too much to the
           // stack.  This leaves us in the situation where puts is called with the stack
           // and registers set up in the same way as they would be if it were passed the
           // address of i, just the same as gets.

   // The gets call with the stack variable's address (so an address high on the stack)
   // could have left main's return address intact, but also could have overwritten it
   // with garbage.  Garbage as main's return address would likely result in a jump to
   // a random location (probably not part of your program) and cause the OS to kill the
   // program (possibly with an unhandled SIGSEGV) which may have looked to you like a
   // normal exit.  Since puts appended a '\n' to the string it wrote and stdout is
   // line buffered by default it would have been flushed before returning from puts
   // even if the program did not terminate properly. 
}

3

这是因为您只是使用了正确的参数调用了gets(),而对puts()的调用发现栈没有改变。在具有许多寄存器的CPU上,这可能会导致错误,除非gets()不使用包含第一个参数的寄存器。启用优化编译,这可能就足够了。

如果在两个之间放置任何函数调用,它也会出错。

一种干净的方式,与相同数量的代码相同,如下:

puts(gets(&i));

5
使用gets()函数没有可行的简洁方法。 - JeremyP

2

当你说“编译和运行良好”时,实际上意味着(a)你忽略了编译器的警告,(b)代码看起来“运行良好”。你的编译器应该会生成多个警告,例如:

ub.c:2: warning: return type defaults to ‘int’
ub.c: In function ‘main’:
ub.c:3: warning: implicit declaration of function ‘gets’
ub.c:4: warning: implicit declaration of function ‘puts’
ub.c:5: warning: control reaches end of non-void function

同时,如果您在多个平台上尝试此操作,您会发现它并不总是会“正常运行”——它可能会输出垃圾信息和/或崩溃。


允许控制流到达 main 的末尾而没有 return 语句,你的编译器不应该对此发出警告。 - dreamlax
@dreamlax:我可能错了,但我认为这是C语言的一个有效警告(C++可能不同?)。 - Paul R
1
@myself:对于GCC,使用-std=c99可以抑制该警告。Clang不会发出这个警告(对于main来说,它会对其他函数发出警告)。 - dreamlax

0

你的gets(&i)函数实际上是获取字符串。在你声明这两个语句的顺序中,puts()没有任何影响。


0

通过堆栈魔术,它并不确定在每台机器和实现上都能正常工作。 puts(); 简单地意味着你没有传递参数,但是堆栈上有一些东西,它是指向字符串的指针(确实是偶然的);puts 接收它(它不知道你在堆栈上没有推送任何内容,它只是“相信”你已经推送了),然后完成它的工作。由于清理堆栈是调用者的责任,所以一切都顺利进行(如果这是被调用者的任务,就会出现问题)。它能够工作的事实是“偶然”的(可能发生,但是你不能太过依赖);它能够编译通过的事实是由标准或编译器决定的(通常会发出警告但不会停止编译,你可以添加选项来严格遵守特定的标准,那么代码可能无法编译通过)


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