阅读未初始化的内存空间总是不明智吗?

时间:2017-07-18 06:24:22

标签: c posix standards c-standard-library data-handling

我正在重新创建整个标准C库,我正在研究strle n的实现,我希望它成为我所有其他str函数的基础。

我目前的实施如下:

int     ft_strlen(char const *str)
{
int length;

length = 0;
while(str[length] != '\0' || str[length + 1] == '\0')
    length++;

return length;
}

我的问题是,当我通过str时:

char str[6] = "hi!";

正如预期的那样,内存为:

['h']['i']['!']['\0']['\0']['\0']['\0']

如果你看看我的实现,你可以期望我得到6的返回 - 而不是3(我之前的方法),这样我就可以检查strlen可能包括额外分配的内存。

这里的问题是我将不得不在初始化内存之外读取1个字节,以便在最终的null终结符处失败我的最后一个循环条件 - 这是我想要的行为。然而,这通常被认为是不好的做法,有些则是自动错误。

即使您非常具体地想要读入垃圾值(确保它不包含'\ 0'),读取初始化值之外的内容也是个坏主意吗?

如果是这样,为什么?

我理解:

"buffer overruns are a favorite avenue for attacking secure programs"

但是,如果我只是想确保我已经达到初始化值的结束,我就看不出问题......

此外,我意识到这个问题可以避免 - 我已经回避了将值设置为1然后只读取初始化值 - 这不是重点,这更多是关于C,运行时行为和最佳实践的基本问题;)

[EDITS:]

对上一篇文章的评论:

行。足够公平 - 但问题是“在初始化值之后阅读是否总是一个坏主意(来自故意操纵或运行时稳定性的危险)” - 你有答案吗?请阅读接受的答案,以获得问题性质的示例。我真的不需要修复此代码,也不需要更好地理解数据类型,POSIX规范或通用标准。我的问题与为什么这样的标准可能存在有关 - 为什么从未读过过初始化的内存(如果存在这样的原因)可能很重要?读取过去初始化值的潜在影响是什么?

请全部 - 我正在努力更好地理解系统如何运作,我有一个非常具体的问题。

6 个答案:

答案 0 :(得分:2)

不要在这里读取未初始化的记忆,而恕我直言的症状,让我们专注于你的想法并解释为什么它是错误的:

char str[6] = "hi!";
strlen(str); // evaluates to 3

这是C标准所要求的,也是每个人都期望的。在这里返回6的实现是错误的。这就是C处理数组字符串的方式的原因:

让VLAs(可变长度数组)放在这里,因为它们只是一个具有相似规则的特殊情况。然后,数组的大小是固定的,在上面的代码中,sizeof(str)是6,这是一个编译时常量。此大小仅在数组在范围内时才知道

根据C的规范,除了与sizeof_Alignof或{{1一起使用时, }}。因此,不可能 将数组传递给函数,实际传递的是指针。如果编写一个函数来接受数组类型,则此类型调整而不是指针类型。 ("调整"是C标准的措辞,它通常表示数组衰减为指针

此规范允许C将数组视为相同类型的连续对象序列 - 没有与之一起存储的元数据(例如长度)。

所以,如果你正在传递"阵列"周围,​​因此只是指向他们的第一个元素,你怎么知道数组的大小?有两种可能性:

  1. &类型的单独参数中传递大小。
  2. 在数组末尾有 sentinel值
  3. 现在,在C中讨论字符串:字符串不是C中的一等公民,它没有自己的类型。它被定义为size_t序列,以char 结尾。因此,您可以<{> 1}} 存储字符串,当您使用字符串时,您不需要传递长度,因为 sentinel值< / em>已定义:每个字符串'\0'结尾。但这也意味着在第一个char[] 不属于字符串之后可能发生的任何事情。

    所以,根据你的想法,你混淆了两件事。你想要一个能够返回数组大小的函数,一般是不可能的。您正在使用数组来存储比数组小的字符串。仍然,一个名为'\0'的函数应该返回字符串的长度,这与用于保存字符串的数组的大小完全不同。

    你甚至可以这样写:

    '\0'

    这会从字符串常量strlen()初始化char foo[3] = "hi!"; ,但foo不会包含字符串,因为它没有"hi!"终止符。它仍然是有效的foo。但是,当然,你不能写一个找出其大小的函数。

    摘要:数组的大小与字符串的长度完全不同。你把两者混在一起;可以在函数中确定数组大小的错误假设导致代码具有UB,而当然,这可能是危险的代码,可能崩溃或更糟(被利用)。

答案 1 :(得分:2)

ft_strlen()可以读取字符串所在的数组。这通常是未定义的行为(UB)。

即使条件没有读入“非拥有”内存,结果也不是6或取决于数组长度的值。

int main(void) {

  struct xx {
    char str_pre[6];
    char str[6];
    char str_post[6];
    char str_postpost[6];
  } x = { "", "Hi!", "", "x" };
  printf("%d\n", ft_strlen(x.str));  --> 11 loop was stopped by "x"

  char str[6] = "1234y";
  strcpy(str, "Hi!");
  printf("%d\n", ft_strlen(str));  --> 3  loop was stopped by "y"

  return 0;
}

ft_strlen()不是确定数组大小和字符串长度的可靠代码。

  

在初始化值之后阅读总是一个坏主意吗?

净度:

char str[6] = "hi!";初始化str[6]所有 6。在C中,没有部分初始化 - 它是全部或全部。

作业可以是部分的。

char str[6];        // str uninitialized
strcpy(str, "Hi!"); // Only first 4 `char` assigned.

之后读取一些初始化值意味着读入另一个对象,或者更糟糕的是,在代码的可访问内存之外。尝试访问未定义的行为 UB并且错误

  

我的问题与为什么这样的标准可能存在有关 - 为什么从来没有读过过的初始化内存可能很重要。

这实际上是关于C. C设计的核心问题.C是妥协。它是一种专门用于许多不同平台的语言。要实现这一点,它必须适用于各种存储器架构。如果C要指定“读取初始化值”的结果,那么C将1)seg-faulting,2)边界检查3)或一些其他软件/硬件来实现该检测。这可能使C在错误检测时更加健壮,但随后增加/减慢发出的代码。 IOWs,C相信程序员正在做正确的事情并且不会尝试捕获这样的错误。实现可能检测到问题,但可能没有。这是UB。 C是在没有网的情况下用紧绳编码的。

  

读取过去初始化值的潜在后果是什么?(?)

C没有指定尝试进行此类读取的结果,因此没有此UB的一般结果。常见结果(每次运行代码时可能会有所不同)包括:

  1. 读取零。
  2. 读取一致的垃圾值。
  3. 读取不一致的垃圾值。
  4. 读取陷阱值。 (绝不适用于unsigned char。)
  5. Seg-fault或其他代码停止。
  6. 代码调用执行处理程序(典型的黑客攻击中的一步)
  7. 代码冒险关闭某事其他。

答案 2 :(得分:0)

您是否听说过&#34;缓冲区溢出问题&#34;当你在&#34;缓冲区外面阅读时#34;也就是未初始化的内存,恶意代码隐藏在堆栈中(当你读取恶意代码时可以执行)更多信息https://en.wikipedia.org/wiki/Buffer_overflow

因此,在未初始化的内存之外读取是非常非常糟糕的,但是大多数编译器通过不允许您这样做或者给出警告来保护堆栈来保护它。

答案 3 :(得分:0)

读取未初始化的内存可以返回先前存储在那里的数据。如果您的程序处理敏感数据(例如密码或加密密钥)并且您将未初始化的数据透露给某方(期望它是有效的),您可能会泄露机密信息。

此外,如果您读取超出数组末尾的内容,则可能无法映射内存,并且您将收到分段错误和崩溃。

编译器还可以假设您的代码是正确的,并且不会读取未初始化的内存,并根据它做出优化决策,因此即使读取未初始化的内存也会产生任意的副作用。

答案 4 :(得分:0)

您似乎想要跟踪已分配的 已使用的字符串内存。这没有任何问题(尽管它与C&C的标准库方法相反)。 然而, 错误是试图在依赖于UB的基础上构建它。有更简单的方法可以用脚射击自己。

如果做得对,您应该遵循依赖于干净代码的路径。一种可能的方法是:

open-picker-infos click

然后你必须提供一组合适的函数来处理你自己的字符串类型,如

struct string_t
{
    int length;
    char strdata[length];
};

使用struct string_t *str_alloc(int length) { struct string_t *s; s = malloc(sizeof(struct string_t) + length + 1); if (s) s->length = length; return s; } void str_free(struct string_t *s) { free(s); } str_cat()等更多功能来完成此功能可能是一项很好的练习。这可能也会向您显示为什么标准库的功能就像它一样。

答案 5 :(得分:0)

- 最后的最后一次编辑 -

所以正确的“不回答我的问题”回答我的问题今天落到了我的腿上......

事实证明,我不是第一个认为能够计算可用,分配和初始化(零/空术语/其他)内存值的人。

处理这种情况的正确方法是使用ASCII字符'us'(十进制:31)为特定用途进行内存分配。

'us'是单位分隔符 - 它的目的是定义特定于用途的单位。最初的IBM手册指出:“必须为每个应用程序指定其特定含义”。在我们的例子中,表示数组中可用安全写入空间的结束。

所以我的mem块应该是:

['h']['i']['!']['\0']['\0']['\0']['\0']['us']

因此无需在内存外读取。

欢迎您,这个答案适用于C: