幕后如何工作?

San*_*996 6 c pointers

这可能是一些非常基本的问题.我试图了解strcpy在幕后的实际效果.例如,在此代码中

#include <stdio.h>
#include <string.h>
int main ()
{
  char s[6] = "Hello";
  char a[20] = "world isnsadsdas";
  strcpy(s,a);

  printf("%s\n",s);
  printf("%d\n", sizeof(s));
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

因为我声称s是一个大小小于源的静态数组.我认为它不打印整个单词,但它确实打印world isnsadsdas..所以,我认为如果目标小于源,这个strcpy函数可能会分配新的大小.但是现在,当我检查sizeof(s)时,它仍然是6,但它的打印输出不止于此.实际工作怎么样?

Car*_*rum 12

您刚刚导致了未定义的行为,因此任何事情都可能发生.在你的情况下,你很幸运,它没有崩溃,但你不应该依赖于这种情况.这是一个简化的strcpy实现(但它与许多真实的实现并不太远):

char *strcpy(char *d, const char *s)
{
   char *saved = d;
   while (*s)
   {
       *d++ = *s++;
   }
   *d = 0;
   return saved;
}
Run Code Online (Sandbox Code Playgroud)

sizeof只是从编译时返回数组的大小.如果你使用strlen,我想你会看到你的期望.但正如我上面提到的,依赖未定义的行为是一个坏主意.


Jes*_*xum 5

http://natashenka.ca/wp-content/uploads/2014/01/strcpy8x11.png

由于您所演示的原因,strcpy 被认为是危险的。您创建的两个缓冲区是存储在函数堆栈帧中的局部变量。堆栈框架大致如下: http://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Call_stack_layout.svg/342px-Call_stack_layout.svg.png

仅供参考,事物被放在堆栈的顶部,这意味着它通过内存向后增长(这并不意味着内存中的变量被向后读取,只是较新的变量被放置在较旧变量的“后面”)。因此,这意味着如果您在函数堆栈帧的局部部分中写入足够远的内容,则您将在要复制到的变量之后向前写入所有其他堆栈变量并中断到其他部分,并最终覆盖返回指针。结果是,如果您足够聪明,您就可以完全控制函数返回的位置。你确实可以让它做任何事情,但关心的不是你。

正如您似乎知道的那样,通过使第一个缓冲区的长度为 5 个字符的字符串的 6 个字符,C 字符串以空字节 \x00 结尾。strcpy 函数复制字节直到源字节为 0,但它不会检查目标字节是否那么长,这就是它可以越过数组边界进行复制的原因。这也是为什么你的打印读取的缓冲区超过了它的大小,它读取到\x00。有趣的是,strcpy 可能已经写入 s 的数据,具体取决于编译器在堆栈中给出的顺序,因此一个有趣的练习可能是打印 a 并看看是否得到类似“snsadsdas”的内容,但我不能确保它会是什么样子,即使它污染了 s,因为有时由于各种原因,堆栈条目之间存在字节)。

如果这个缓冲区包含一个密码,用于使用散列函数签入代码,并且您可以从任何获取它的地方(网络数据包,如果服务器或文本框等)将其复制到堆栈中的缓冲区,那么您很好可能会从源复制比目标缓冲区可以容纳的更多的数据,并将程序的控制权返回给能够向您发送数据包或尝试密码的任何用户。他们只需输入正确数量的字符,然后输入代表 ram 中要跳转到的某个地址的正确字符。

如果检查边界并可能修剪源字符串,则可以使用 strcpy,但这被认为是不好的做法。有更现代的函数需要最大长度,例如http://www.cplusplus.com/reference/cstring/strncpy/

哦,最后,这就是所谓的缓冲区溢出。一些编译器在每个堆栈条目之前和之后添加操作系统随机选择的一小块字节。每次复制后,操作系统都会对照其副本检查这些字节,如果不同则终止程序。这解决了很多安全问题,但仍然可以将字节复制到足够远的堆栈中,以覆盖指向函数的指针,以处理这些字节更改时发生的情况,从而让您执行相同的操作。正确行事变得更加困难。