是否总是不建议读取未初始化的内存空间?
Posted
技术标签:
【中文标题】是否总是不建议读取未初始化的内存空间?【英文标题】:Is reading into uninitialized memory space ALWAYS ill advised? 【发布时间】:2017-07-18 06:24:22 【问题描述】:我正在重新创建整个标准 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 个字节才能使最后一个循环条件在最终空终止符处失败 - 这是我想要的行为。然而,这通常被认为是不好的做法,并且在某些情况下会自动出错。
即使您非常明确地打算读取垃圾值(以确保它不包含“\0”),在您的初始化值之外读取是不是一个坏主意?
如果是,为什么?
我明白:
"buffer overruns are a favorite avenue for attacking secure programs"
不过,如果我只是想确保我已经达到初始化值的末尾,我看不出问题...
另外,我意识到这个问题是可以避免的——我已经回避了一个设置为 1 的值,然后只读取初始化值——这不是重点,这更多是关于 C、运行时行为和最佳实践的基本问题;)
[编辑:]
评论之前的帖子:
好的。很公平 - 但关于“在初始化值后读取是否总是一个坏主意(故意操纵或运行时稳定性的危险)”这个问题 - 你有答案吗?请阅读已接受的答案,以了解问题性质的示例。我真的不需要修复这段代码,也不需要更好地理解数据类型、POSIX 规范或通用标准。我的问题与为什么可能存在这样的标准有关 - 为什么永远不要读取过去的初始化内存(如果存在这样的原因)可能很重要?在 GENERAL 中读取过去的初始化值的潜在后果是什么?
请大家- 我正在尝试更好地了解系统如何运行的各个方面,我有一个非常具体的问题。
【问题讨论】:
无法保证数组后面的字节不为零,因此您的函数可能会超出缓冲区任意数量。或者它可能会遇到未映射到物理存储的地址并因段错误而崩溃。或者...(参见 UB)。 使用未初始化的变量被枚举为UB(未定义的行为)。 J.2 未定义行为 具有自动存储期限的对象的值在不确定时使用 没有“管理未定义的行为”这样的东西。未定义就是未定义,句号。 Also 6.5.6 加法运算符 p8 如果结果指向数组对象的最后一个元素之后,则不应将其用作计算的一元 * 运算符的操作数。 所以未定义就是未定义。每个动作都没有规定。 【参考方案1】: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 将 1) 段错误,2) 边界检查 3) 或其他一些软件/硬件来实现该检测。这可能会使 C 在错误检测方面更加健壮,但随后会增加/减慢发出的代码。 IOWs,C 相信程序员正在做正确的事情,并且不会尝试捕获此类错误。实现可能检测到问题,也可能检测不到。是UB。 C 是在没有网的钢丝绳上编码。
在 GENERAL (?) 中读取过去的初始化值的潜在后果是什么
C 未指定尝试执行此类读取的结果,因此没有此 UB 的一般结果。每次运行代码时可能会有所不同的常见结果包括:
-
读取一个零。
读取了一致的垃圾值。
读取的垃圾值不一致。
读取陷阱值。 (但绝不适用于
unsigned char
。)
段错误或其他代码停止。
代码调用执行处理程序(典型黑客攻击中的一个步骤)
代码冒险离开并做其他事情。
【讨论】:
好的。很公平 - 但关于“在初始化值后读取是否总是一个坏主意(故意操纵或运行时稳定性的危险)”这个问题 - 你有答案吗?请阅读已接受的答案,以了解问题性质的示例。我真的不需要修复这段代码,也不需要更好地理解数据类型、POSIX 规范或通用标准。我的问题与为什么可能存在这样的标准有关 - 为什么永远不要读取过去的初始化内存可能很重要(如果存在这样的原因)阅读过去的初始化值的潜在后果是什么 @MJHd Answers/cmets 不仅针对 OP,而且针对所有观众。所以不时地,附加信息是相关的。 是的——我实际上已经从我的教授那里得到了这个问题的概要(他也表达了他对我收到的回复感到沮丧,我可能会补充)——在这一点上我只是为了那些可能会分享我最初的问题并对完全不同的问题的答案感到困惑的人而做出回应 - 如果有人间接从其他答案中受益,那就太好了!在这一点上只是我不关心 - 不过谢谢你:) 我可以补充一下 - 虽然我非常感谢会员希望为找到这篇文章的其他人留下有用的信息 - 这是很常见的礼貌(和一般逻辑恕我直言)回答首先提出的问题。我确定没有人会像这样亲自和我说话 - 所以我很难相信这是某种误解...... @MJHd "我确定没有人会这样当面跟我说话" -- 我确定你是否去找同事(或者你的导师,如果你是学徒)展示 this 代码并问 this 问题,你会当然得到一些解释为什么你的代码背后的想法是不明智的。接受一些解释而感到被侮辱不会让你走得太远。将您的问题简化为“此 UB 可能产生的后果”将使其成为无数其他问题的重复。【参考方案2】:恕我直言,在这里读取未初始化的内存只是一个症状,让我们专注于你的想法和解释为什么它是错误的:
char str[6] = "hi!";
strlen(str); // evaluates to 3
这是 C 标准所要求的,也是每个人所期望的。在这里返回6
的实现是错误的。这有其原因在于 C 处理 数组 和 字符串 的方式:
将 VLA(可变长度数组)放在一边,因为它们只是一种特殊情况,规则有些相似。然后,array 的大小是固定的,在上面的代码中,sizeof(str)
是 6,这是一个编译时常量。这个大小只有在数组在范围内时才知道。
根据 C 的规范,数组的标识符计算为指向其第一个元素的指针,除非与 sizeof
、_Alignof
或 &
一起使用。因此,不可能将数组传递给函数,您实际传递的是指针。如果您编写一个函数来接受数组类型,则此类型将调整为指针类型。 (“调整”是C标准的用语,通常说数组衰减为指针)
此规范允许 C 将数组视为相同类型的连续对象序列——没有存储元数据(例如长度)。
所以,如果你传递“数组”,因此只有指向它们的第一个元素的指针,你怎么知道数组的大小?有两种可能:
-
在
size_t
类型的单独参数中传递大小。
在数组末尾有一个标记值。
现在,谈谈C 中的字符串:字符串不是C 中的一等公民,它没有自己的类型。它被定义为char
的序列,以'\0'
结尾。因此,您可以在char[]
中存储一个字符串,并且当您使用字符串时,您不需要传递长度,因为 sentinel 值 已经定义: 每个 string 都以'\0'
结尾。但这也意味着第一个 '\0'
之后可能出现的任何内容不是字符串的一部分。
因此,根据您的想法,您将两件事混为一谈。不知何故,您希望拥有一个返回数组大小的函数,而这通常是不可能的。您正在使用您的数组来存储一个小于数组的字符串。不过,一个名为 strlen()
的函数应该返回字符串的长度,这与用于保存字符串的数组的大小完全不同。
你甚至可以这样写:
char foo[3] = "hi!";
这将从字符串常量"hi!"
初始化foo
,但foo
不会包含字符串,因为它没有'\0'
终止符。它仍然是有效的char[]
。但是当然,你不能写一个函数来找出它的大小。
总结:数组的大小与字符串的长度完全不同。你把两者混为一谈了;可以在函数中确定数组大小的错误假设会导致代码带有 UB,当然,这是可能崩溃或更糟(被利用)的潜在危险代码。
【讨论】:
这与我的问题无关 - 由于这个确切的原因,我的问题非常明确......正如我在帖子中所说,我已经重构了函数及其助手 - 我我对您将如何优化我的代码或 Clib 的工作方式不感兴趣。 正如问题所述:这是一个通用问题,关于在初始化内存之外读取是否通常是不好的做法。提供的代码是为了说明这一点。 那你的问题完全没有意义。您不仅可以读取未初始化的内存,甚至还可以取消引用在您的程序中不一定有效的指针。 不过,这个答案非常很重要,因为您的“示例”表明对 C 中的基本概念完全缺乏理解。 @chux 它是 §6.3.2.1 部分 3:“除非它是sizeof
运算符、_Alignof
运算符或一元 &
运算符,或者是用于初始化数组的字符串文字,类型为“类型数组”的表达式将转换为类型为“类型指针”的表达式,该类型指向初始元素数组对象并且不是左值。" -- 我会修复它:)【参考方案3】:
读取未初始化的内存可以返回之前存储在那里的数据。如果您的程序处理敏感数据(例如密码或加密密钥)并且您将未初始化的数据披露给某些方(期望它是有效的),您可能会泄露机密信息。
此外,如果您读取超出数组末尾的内容,则可能无法映射内存,并且您将遇到分段错误和崩溃。
编译器还可以假设您的代码是正确的并且不会读取未初始化的内存,并据此做出优化决策,因此即使读取未初始化的内存也可能会产生任意副作用。
【讨论】:
好的 - 这些都是很好的观点。但是有一些问题:1)如果编译器忽略读取未初始化值的尝试 - 我不会在没有运行时错误或潜在恶意代码注入的情况下获得正确的返回值吗? 2)类似地,如果寄存器被读取但未使用或返回 - 是否可以恢复内部潜在的敏感值?函数返回后,堆栈中的所有本地数据不应该都消失了吗?【参考方案4】:当您在“缓冲区”(即未初始化的内存)之外读取“缓冲区溢出问题”时,您是否听说过“缓冲区溢出问题”,恶意代码隐藏在堆栈中(当您阅读时,恶意代码可能会被执行)更多信息在这里@987654321 @
因此,在未初始化的内存之外读取是非常非常糟糕的,但大多数编译器通过不允许您这样做或给您警告以保护堆栈来保护它。
【讨论】:
True - 但该值从未返回或使用 - 那么它是否真的会影响程序执行以读取恶意代码但不执行任何操作?【参考方案5】:您似乎想跟踪分配的 和 使用的字符串内存。这并没有错(尽管它与 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()
等更多功能来实现此功能可能是一个很好的练习。这也可能会向您展示为什么标准库以它的方式做事。
【讨论】:
【参考方案6】:-- 大结局最后一次编辑--
所以今天对我的问题的正确“不是我的问题的答案”的答案落在了我的腿上......
事实证明,我不是第一个认为能够计算可用、分配和初始化(零/空项/其他)内存值的人。
处理这种情况的正确方法是使用 ASCII 字符“us”(十进制:31)为特定用途预订内存分配。
'us' 是单位分隔符——它的目的是定义一个特定于使用的单位。最初的 IBM 手册指出:“必须为每个应用程序指定其特定含义”。在我们的例子中,表示数组中可用的安全写入空间结束。
所以我的内存块应该是这样的:
['h']['i']['!']['\0']['\0']['\0']['\0']['us']
从而消除了在内存之外读取的需要。
不客气,这个答案适用于 C:
【讨论】:
以上是关于是否总是不建议读取未初始化的内存空间?的主要内容,如果未能解决你的问题,请参考以下文章
运行java,对电脑配置要求多少?我4G内存,总是出现Java heap space
Linux 内核 内存管理虚拟地址空间布局架构 ② ( 用户虚拟地址空间组成 | 内存描述符 mm_struct 结构体源码 )