C语言 读取未初始化的内存空间总是不明智



我正在重新创建整个标准 C 库,并且正在为strlen 开发一个实现,我想成为所有其他str函数的基础。

我目前的实现如下:

int     ft_strlen(char const *str)
{
int length;
length = 0;
while(str[length] != '' || str[length + 1] == '')
length++;
return length;
}

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

char str[6] = "hi!";

正如预期的那样,内存读取:

['h']['i']['!']['']['']['']['']

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

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

读取初始化值之外的读数是否是一个坏主意,即使您非常明确地打算读取垃圾值(以确保它不包含"\0")?

如果是这样,为什么?

我明白:

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

不过,如果我只是想确保我已经到达初始化值的末尾,我看不到问题......

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

[编辑:]

对上一篇文章的评论:

好的。 很公平 - 但是关于"在初始化值之后读取总是一个坏主意(故意操作或运行时稳定性的危险)"的问题 - 你有答案吗?请阅读接受的答案,了解问题性质的示例。我真的不需要修复这段代码,也不需要更好地了解数据类型、POSIX 规范或通用标准。我的问题与为什么可能存在这样的标准有关 - 为什么永远不要读取过去的初始化内存(如果存在这些原因)可能很重要?读取过去的初始化值的潜在后果是什么?

请大家—— 我试图更好地了解系统如何运作的各个方面,我有一个非常具体的问题。

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("%dn", ft_strlen(x.str));  --> 11 loop was stopped by "x"
char str[6] = "1234y";
strcpy(str, "Hi!");
printf("%dn", 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 将 1) 隔离错误,2) 边界检查 3) 或其他一些软件/硬件来实现该检测。 这可能会使 C 在错误检测方面更加健壮,但随后会增加/减慢发出的代码。IOW,C相信程序员正在做正确的事情,并且不会试图捕获此类错误。 实现可能会检测到问题,但可能不会。 它是 UB。 C 在没有网的钢丝上编码。

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

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

  1. 读取零。
  2. 读取一致的垃圾值。
  3. 读取不一致的垃圾值。
  4. 读取陷阱值。 (不过,从不适用于unsigned char
  5. 隔离错误或其他代码停止。
  6. 代码调用执行处理程序(典型黑客攻击中的一个步骤)
  7. 代码冒险并做其他事情

与其读取未初始化的内存,恕我直言,这只是这里的症状,让我们专注于您的想法和解释为什么它是错误的:

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

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

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

根据 C 的规范,数组的标识符计算为指向其第一个元素的指针,除非与sizeof_Alignof&一起使用。结果是,不可能将数组传递给函数,您实际传递的是指针。如果编写函数以接受数组类型,则此类型将调整为指针类型。("调整"是C标准的措辞,通常说数组衰减为指针)

这个规范允许 C 将数组视为只不过是相同类型的对象的连续序列——没有存储元数据(例如长度)。

所以,如果你传递"数组",因此只有指向它们的第一个元素的指针,你怎么知道数组的大小?有两种可能性:

  1. 在类型为size_t的单独参数中传递大小。
  2. 在数组的末尾有一个哨兵值

现在,在C中谈论字符串:字符串在C中不是一等公民,它没有自己的类型。它被定义为一个char序列,以''结尾。因此,您可以将字符串存储在char[]中,并且在处理字符串时,不需要传递长度,因为已经定义了哨兵值:每个字符串都以''结尾。但这也意味着在第一次''之后可能出现的任何内容都不是字符串的一部分

所以,有了你的想法,你把两件事混为一谈。你以某种方式想要一个返回数组大小的函数,这在一般情况下是不可能的。你正在使用数组来存储比数组小的字符串。尽管如此,一个名为strlen()的函数应该返回字符串的长度,这与用于保存字符串的数组大小完全不同。

你甚至可以写这样的东西:

char foo[3] = "hi!";

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


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

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

此外,如果在数组末尾之外读取,则可能无法映射内存,并且将出现分段错误和崩溃。

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

您是否听说过"缓冲区溢出问题",当您在"缓冲区"(即未初始化的内存)之外读取恶意代码隐藏在堆栈中(当您读取恶意代码时,恶意代码可能会被执行)更多信息在这里 https://en.wikipedia.org/wiki/Buffer_overflow

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

您似乎想要跟踪分配使用的字符串内存。这没有错(尽管它与 C 的标准库方法相反)。然而,错误的是试图将其建立在依赖UB的基础上。有更简单的方法可以在自己的脚上开枪。

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

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()str_cpy()等)来实现这一点可能是一个很好的练习。这可能还会告诉你为什么标准库就是这样做事的。

--

大决赛最后编辑 --

所以今天我的问题的正确"不是我问题的答案"答案落到了我的腿上......

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

处理这种情况的正确方法是使用 ASCII 字符"us"(十进制:31)将特定用途的内存分配记入书签。

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

所以我的mem块应该读到:

['h']['i']['!']['']['']['']['']['us']

从而消除了在内存之外读取的需要。

不客气,这个答案是给C的:

相关内容

  • 没有找到相关文章

最新更新