读入未初始化的内存 space 总是不明智的吗?
Is reading into uninitialized memory space ALWAYS ill advised?
我正在重新创建整个标准 C 库,并且正在为 strle
n 开发一个实现,我希望它成为我所有其他 str
函数的基础。
我目前的实现如下:
int ft_strlen(char const *str)
{
int length;
length = 0;
while(str[length] != '[=11=]' || str[length + 1] == '[=11=]')
length++;
return length;
}
我的问题是,当我传递一个 str
时:
char str[6] = "hi!";
不出所料,内存显示:
['h']['i']['!']['[=13=]']['[=13=]']['[=13=]']['[=13=]']
如果你看一下我的实现,你可以预期我会得到 6 的 return - 而不是 3(我以前的方法),这样我就可以检查 strlen
可能包括额外的分配的内存。
这里要注意的是,我将不得不在初始化内存之外读取 1 个字节,以使最后一个空终止符处的最后一个循环条件失败——这是我想要的行为。然而,这通常被认为是不好的做法,有些人认为这是自动错误。
即使您非常明确地打算读入垃圾值(以确保它不包含“\0”),在您的初始化值之外读取也是一个坏主意吗?
如果是,为什么?
我明白了:
"buffer overruns are a favorite avenue for attacking secure programs"
不过,如果我只是想确保已到达初始化值的末尾,我还是看不到问题...
此外,我意识到这个问题是可以避免的——我已经将值设置为 1,然后只读取初始化值——这不是重点,这更像是一个关于 C、运行时行为和最佳实践 ;)
[编辑:]
对上一个 post 的评论:
好的。很公平 - 但关于问题 "Is it always a bad idea (danger from intentional manipulation or runtime stability) to read after initialized values" - 你有答案吗?请阅读接受的答案以获取问题性质的示例。我真的不需要修复此代码,也不需要更好地了解数据类型、POSIX 规范或通用标准。我的问题与为什么可能存在这样的标准有关——为什么从不读取过去的初始化内存可能很重要(如果存在这样的原因)?一般而言,读取过去的初始化值的潜在后果是什么?
请大家——
我试图更好地了解系统如何运作的各个方面,我有一个非常具体的问题。
当你在 "buffer" 之外阅读时,你听说过 "buffer overflow problem" 也就是未初始化的内存,一个恶意代码隐藏在堆栈中(当你阅读它时,恶意代码可能会被执行)更多这里的信息 https://en.wikipedia.org/wiki/Buffer_overflow
因此,在未初始化的内存之外读取是非常非常糟糕的,但大多数编译器通过不允许您这样做或给您警告以保护堆栈来保护它。
读取未初始化的内存可以 return 以前存储在那里的数据。如果您的程序处理敏感数据(例如密码或加密密钥)并且您将未初始化的数据透露给某些方(期望它是有效的),您可能会泄露机密信息。
此外,如果读取超出数组末尾,内存可能无法映射,您将遇到分段错误和崩溃。
编译器还可以假定您的代码是正确的并且不会读取未初始化的内存,并据此做出优化决策,因此即使 读取 未初始化的内存也会产生任意副作用.
恕我直言,这里只是一个症状,而不是读取未初始化的内存,让我们专注于您的想法和错误原因的解释:
char str[6] = "hi!";
strlen(str); // evaluates to 3
这是 C 标准要求的,也是每个人都期望的。 returning 6
的实现是错误的。这在 C 处理 arrays 和 strings:
的方式中有其原因
让 VLA(可变长度数组)在这里搁置一旁,因为它们只是具有某种相似规则的特例。然后,array 的大小是固定的,在上面的代码中,sizeof(str)
是 6,这是一个编译时常量。此大小 仅当数组在范围 .
中时才知道
根据 C 规范,数组标识符的计算结果为 指向其第一个元素的指针,除非与 sizeof
、[=15= 一起使用] 或 &
。因此,不可能 将数组 传递给函数,您实际传递的是指针。如果你写一个函数来接受一个数组类型,这个类型被调整改为一个指针类型。 ("adjusted"是C标准的写法,一般说数组decays as a pointer)
此规范允许 C 将数组仅视为相同类型的连续对象序列——没有存储元数据(例如长度)。
因此,如果您传递 "arrays",因此只有指向其第一个元素的指针,您如何知道数组的大小?有两种可能:
- 在
size_t
类型的单独参数中传递大小。
- 在数组末尾有一个 标记值。
现在,谈论 C 中的 字符串 :字符串不是 C 中的第一个 class 公民,它没有自己的类型。它被定义为 char
的 序列,以 '[=19=]'
结尾。因此,您可以 store 一个字符串 char[]
并且当您使用字符串时,您不需要传递长度,因为 sentinel 值 已经定义:每个 字符串 都以 '[=19=]'
结尾。但这也意味着第一个 '[=19=]'
之后可能出现的任何内容 都不是字符串的一部分 .
因此,根据您的想法,您混淆了两件事。您不知何故想要一个 return 数组大小的函数,这 通常 是不可能的。您正在使用数组存储小于数组的字符串。尽管如此,一个名为 strlen()
的函数应该 return 字符串的长度,这与您用来保存字符串的数组的大小完全不同。
你甚至可以这样写:
char foo[3] = "hi!";
这将从字符串常量 "hi!"
初始化 foo
,但 foo
不会包含字符串,因为它没有 '[=19=]'
终止符。它仍然是一个有效的 char[]
。但是当然,你不能写一个函数来找出它的大小。
总结:数组的大小与字符串的长度完全不同。你把两者混为一谈了;数组的大小可以在函数中确定的错误假设导致代码带有 UB,当然 ,这是潜在的危险代码,可能会崩溃或更糟(被利用)。
ft_strlen()
可以读取字符串所在的数组之外的内容。这通常是 未定义的行为 (UB)。
即使条件不读入 "un-owned" 内存,结果也不是 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()
不是确定数组大小或字符串长度的可靠代码。
Is it always a bad idea to read after initialized values?
清晰度:
char str[6] = "hi!";
初始化 all 6 of str[6]
。在 C 中,没有部分初始化 - 全有或全无。
赋值可以是部分的。
char str[6]; // str uninitialized
strcpy(str, "Hi!"); // Only first 4 `char` assigned.
在 一些初始化值之后读取意味着读取到另一个对象或更糟的是,读取到代码的可访问内存之外。尝试访问是 未定义的行为 UB,不好。
My question is related to WHY such standards may exist - why it may be important to never read past initialized memory.
这真的是C设计的核心问题,C是一种妥协。它是一种设计用于 许多不同 平台的语言。为实现这一目标,它必须适用于各种内存架构。如果 C 要 指定 "read after initialized values" 的结果,那么 C 将 1) 段错误,2) 边界检查 3) 或其他一些 software/hardware 来实现它检测。这可能会使 C 在错误检测方面更加健壮,但随后 increase/slow 会发出代码。 IOWs,C 相信程序员正在做正确的事情,并且不会尝试捕获此类错误。实施 可能 检测到问题,也可能检测不到。是UB。 C在没有网的情况下在走钢丝
What is the potential fallout of reading past initialized values IN GENERAL (?)
C 未指定尝试执行此类读取的结果,因此没有此 UB 的一般结果。每次代码为 运行 时可能会发生变化的常见结果包括:
- 读到一个零。
- 读取一致的垃圾值。
- 读取了不一致的垃圾值。
- 读取了陷阱值。 (尽管从不应用于
unsigned char
。)
- 段错误或其他代码停止。
- 代码调用执行处理程序(典型黑客攻击的一个步骤)
- 代码冒险并做一些其他事情。
您似乎想要跟踪分配的 和 使用的 字符串内存。这并没有错(尽管它与 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()
等更多函数完成此实现可能是一个很好的练习。这可能还会向您展示 为什么 标准库按照它的方式做事。
-- 大决赛最后编辑--
所以我的问题的正确 "not an answer to my question" 答案今天落在了我的腿上...
事实证明,我并不是第一个认为能够计算可用、分配和初始化 (zero/null term/other) 内存值的人。
处理这种情况的正确方法是使用 ASCII 字符 'us'(十进制:31)来记录特定用途的内存分配。
'us' 是单位分隔符——它的目的是定义一个特定用途的单位。原始 IBM 手册指出:"its specific meaning has to be specified for each application"。在我们的例子中,在数组中发出可用安全写入结束的信号 space。
所以我的内存块应该是:
['h']['i']['!']['[=10=]']['[=10=]']['[=10=]']['[=10=]']['us']
因此无需读取内存之外的内容。
不客气,此答案适用于 C:
我正在重新创建整个标准 C 库,并且正在为 strle
n 开发一个实现,我希望它成为我所有其他 str
函数的基础。
我目前的实现如下:
int ft_strlen(char const *str)
{
int length;
length = 0;
while(str[length] != '[=11=]' || str[length + 1] == '[=11=]')
length++;
return length;
}
我的问题是,当我传递一个 str
时:
char str[6] = "hi!";
不出所料,内存显示:
['h']['i']['!']['[=13=]']['[=13=]']['[=13=]']['[=13=]']
如果你看一下我的实现,你可以预期我会得到 6 的 return - 而不是 3(我以前的方法),这样我就可以检查 strlen
可能包括额外的分配的内存。
这里要注意的是,我将不得不在初始化内存之外读取 1 个字节,以使最后一个空终止符处的最后一个循环条件失败——这是我想要的行为。然而,这通常被认为是不好的做法,有些人认为这是自动错误。
即使您非常明确地打算读入垃圾值(以确保它不包含“\0”),在您的初始化值之外读取也是一个坏主意吗?
如果是,为什么?
我明白了:
"buffer overruns are a favorite avenue for attacking secure programs"
不过,如果我只是想确保已到达初始化值的末尾,我还是看不到问题...
此外,我意识到这个问题是可以避免的——我已经将值设置为 1,然后只读取初始化值——这不是重点,这更像是一个关于 C、运行时行为和最佳实践 ;)
[编辑:]
对上一个 post 的评论:
好的。很公平 - 但关于问题 "Is it always a bad idea (danger from intentional manipulation or runtime stability) to read after initialized values" - 你有答案吗?请阅读接受的答案以获取问题性质的示例。我真的不需要修复此代码,也不需要更好地了解数据类型、POSIX 规范或通用标准。我的问题与为什么可能存在这样的标准有关——为什么从不读取过去的初始化内存可能很重要(如果存在这样的原因)?一般而言,读取过去的初始化值的潜在后果是什么?
请大家—— 我试图更好地了解系统如何运作的各个方面,我有一个非常具体的问题。
当你在 "buffer" 之外阅读时,你听说过 "buffer overflow problem" 也就是未初始化的内存,一个恶意代码隐藏在堆栈中(当你阅读它时,恶意代码可能会被执行)更多这里的信息 https://en.wikipedia.org/wiki/Buffer_overflow
因此,在未初始化的内存之外读取是非常非常糟糕的,但大多数编译器通过不允许您这样做或给您警告以保护堆栈来保护它。
读取未初始化的内存可以 return 以前存储在那里的数据。如果您的程序处理敏感数据(例如密码或加密密钥)并且您将未初始化的数据透露给某些方(期望它是有效的),您可能会泄露机密信息。
此外,如果读取超出数组末尾,内存可能无法映射,您将遇到分段错误和崩溃。
编译器还可以假定您的代码是正确的并且不会读取未初始化的内存,并据此做出优化决策,因此即使 读取 未初始化的内存也会产生任意副作用.
恕我直言,这里只是一个症状,而不是读取未初始化的内存,让我们专注于您的想法和错误原因的解释:
char str[6] = "hi!";
strlen(str); // evaluates to 3
这是 C 标准要求的,也是每个人都期望的。 returning 6
的实现是错误的。这在 C 处理 arrays 和 strings:
让 VLA(可变长度数组)在这里搁置一旁,因为它们只是具有某种相似规则的特例。然后,array 的大小是固定的,在上面的代码中,sizeof(str)
是 6,这是一个编译时常量。此大小 仅当数组在范围 .
根据 C 规范,数组标识符的计算结果为 指向其第一个元素的指针,除非与 sizeof
、[=15= 一起使用] 或 &
。因此,不可能 将数组 传递给函数,您实际传递的是指针。如果你写一个函数来接受一个数组类型,这个类型被调整改为一个指针类型。 ("adjusted"是C标准的写法,一般说数组decays as a pointer)
此规范允许 C 将数组仅视为相同类型的连续对象序列——没有存储元数据(例如长度)。
因此,如果您传递 "arrays",因此只有指向其第一个元素的指针,您如何知道数组的大小?有两种可能:
- 在
size_t
类型的单独参数中传递大小。 - 在数组末尾有一个 标记值。
现在,谈论 C 中的 字符串 :字符串不是 C 中的第一个 class 公民,它没有自己的类型。它被定义为 char
的 序列,以 '[=19=]'
结尾。因此,您可以 store 一个字符串 char[]
并且当您使用字符串时,您不需要传递长度,因为 sentinel 值 已经定义:每个 字符串 都以 '[=19=]'
结尾。但这也意味着第一个 '[=19=]'
之后可能出现的任何内容 都不是字符串的一部分 .
因此,根据您的想法,您混淆了两件事。您不知何故想要一个 return 数组大小的函数,这 通常 是不可能的。您正在使用数组存储小于数组的字符串。尽管如此,一个名为 strlen()
的函数应该 return 字符串的长度,这与您用来保存字符串的数组的大小完全不同。
你甚至可以这样写:
char foo[3] = "hi!";
这将从字符串常量 "hi!"
初始化 foo
,但 foo
不会包含字符串,因为它没有 '[=19=]'
终止符。它仍然是一个有效的 char[]
。但是当然,你不能写一个函数来找出它的大小。
总结:数组的大小与字符串的长度完全不同。你把两者混为一谈了;数组的大小可以在函数中确定的错误假设导致代码带有 UB,当然 ,这是潜在的危险代码,可能会崩溃或更糟(被利用)。
ft_strlen()
可以读取字符串所在的数组之外的内容。这通常是 未定义的行为 (UB)。
即使条件不读入 "un-owned" 内存,结果也不是 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()
不是确定数组大小或字符串长度的可靠代码。
Is it always a bad idea to read after initialized values?
清晰度:
char str[6] = "hi!";
初始化 all 6 of str[6]
。在 C 中,没有部分初始化 - 全有或全无。
赋值可以是部分的。
char str[6]; // str uninitialized
strcpy(str, "Hi!"); // Only first 4 `char` assigned.
在 一些初始化值之后读取意味着读取到另一个对象或更糟的是,读取到代码的可访问内存之外。尝试访问是 未定义的行为 UB,不好。
My question is related to WHY such standards may exist - why it may be important to never read past initialized memory.
这真的是C设计的核心问题,C是一种妥协。它是一种设计用于 许多不同 平台的语言。为实现这一目标,它必须适用于各种内存架构。如果 C 要 指定 "read after initialized values" 的结果,那么 C 将 1) 段错误,2) 边界检查 3) 或其他一些 software/hardware 来实现它检测。这可能会使 C 在错误检测方面更加健壮,但随后 increase/slow 会发出代码。 IOWs,C 相信程序员正在做正确的事情,并且不会尝试捕获此类错误。实施 可能 检测到问题,也可能检测不到。是UB。 C在没有网的情况下在走钢丝
What is the potential fallout of reading past initialized values IN GENERAL (?)
C 未指定尝试执行此类读取的结果,因此没有此 UB 的一般结果。每次代码为 运行 时可能会发生变化的常见结果包括:
- 读到一个零。
- 读取一致的垃圾值。
- 读取了不一致的垃圾值。
- 读取了陷阱值。 (尽管从不应用于
unsigned char
。) - 段错误或其他代码停止。
- 代码调用执行处理程序(典型黑客攻击的一个步骤)
- 代码冒险并做一些其他事情。
您似乎想要跟踪分配的 和 使用的 字符串内存。这并没有错(尽管它与 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()
等更多函数完成此实现可能是一个很好的练习。这可能还会向您展示 为什么 标准库按照它的方式做事。
-- 大决赛最后编辑--
所以我的问题的正确 "not an answer to my question" 答案今天落在了我的腿上...
事实证明,我并不是第一个认为能够计算可用、分配和初始化 (zero/null term/other) 内存值的人。
处理这种情况的正确方法是使用 ASCII 字符 'us'(十进制:31)来记录特定用途的内存分配。
'us' 是单位分隔符——它的目的是定义一个特定用途的单位。原始 IBM 手册指出:"its specific meaning has to be specified for each application"。在我们的例子中,在数组中发出可用安全写入结束的信号 space。
所以我的内存块应该是:
['h']['i']['!']['[=10=]']['[=10=]']['[=10=]']['[=10=]']['us']
因此无需读取内存之外的内容。
不客气,此答案适用于 C: