0

我正在重新创建整个标准 C 库,并且正在为strlen 开发一个实现,我希望它成为我所有其他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 中读取过去的初始化值的潜在后果是什么?

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

4

6 回答 6

2

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

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'\0'。因此,您可以将字符串存储在 a 中char[],并且当您使用字符串时,您不需要传递长度,因为已经定义了标记值:每个字符串都以'\0'. 但这也意味着在 first 之后可能出现的任何'\0'内容都不是字符串的一部分

因此,根据您的想法,您将两件事混为一谈。您以某种方式希望拥有一个返回数组大小的函数,这在一般情况下是不可能的。您正在使用您的数组来存储一个小于数组的字符串。不过,调用的函数strlen()应该返回字符串的长度,这与用于保存字符串的数组的大小完全不同。

你甚至可以这样写:

char foo[3] = "hi!";

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


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

于 2017-07-18T07:10:00.493 回答
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!";初始化所有6 个str[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 的一般结果。每次运行代码时可能会有所不同的常见结果包括:

  1. 读取零。
  2. 读取一致的垃圾值。
  3. 读取了不一致的垃圾值。
  4. 读取陷阱值。unsigned char(但从不适用。)
  5. 段错误或其他代码停止。
  6. 代码调用执行处理程序(典型黑客攻击的一个步骤)
  7. 代码冒险离开并做其他事情
于 2017-07-18T16:40:54.207 回答
0

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

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

于 2017-07-18T06:34:43.983 回答
0

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

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

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

于 2017-07-18T06:39:37.407 回答
0

您似乎想要跟踪分配 使用的字符串内存。这并没有错(尽管它与 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()等等。这可能还会向您展示为什么标准库会按照它的方式做事。

于 2017-07-19T08:17:42.973 回答
0

-- 大结局最后编辑 --

所以今天对我的问题的正确“不是我的问题的答案”的答案落到了我的怀里......

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

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

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

所以我的内存块应该是这样的:

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

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

不客气,这个答案适用于 C:

于 2017-08-26T18:22:08.683 回答