【问题标题】:how to prevent a segfault in C (when taking an argument of the wrong type)如何防止 C 中的段错误(当采用错误类型的参数时)
【发布时间】:2016-09-02 15:42:12
【问题描述】:

我有两个文件,User.h 和 test.c:

用户.h

include <stdio.h>                                                              
#include <string.h>                                                             

struct User {                                                                 
    char name[21];                                                              
};                                                                              

struct User newUser(char* name) {                                           
    struct User newUser;                                                    
    memset(newUser.name, '\0', 21); // ensure string ends with '\0'           
    memcpy(newUser.name, name, 20); // copy first 20 chars of string          
    return newUser;                                                           
} 

test.c

#include "User.h"                                                             

int main() {                                                                    
    struct User testUser = newUser(34);                                           
    printf("name is: %s\n", testUser.name);                                           
    return 0;                                                                   
} 

我故意将错误类型的参数传递给函数,但试图避免段错误:

如果我要向 newUser() 传递任意长度的字符串,该函数将占用最多 20 个字符并将其存储在 .name 中而不会出现段错误,因为它保证以空字节结尾。

这里我传递的是一个 int。因为它需要一个字符串,所以我收到一个编译器警告,但它无论如何都会编译。运行时,出现分段错误。

我想当函数读取到 name[21] 数组时会发生分段错误,但是如果保证它有一个空字节,为什么它会继续读取它呢?它期待一个字符串,它不应该将任何参数视为字符串并在'\ 0'处终止吗?

看来我的逻辑有缺陷,有人可以告诉我这里到底发生了什么吗?

【问题讨论】:

  • C 没有运行时类型检查。这意味着您无法避免分段错误。但你可以抓住它。请参阅here 怎么做。或者使用language,它具有运行时类型检查。
  • 我一直想学一个口齿不清的人,鸡计划看起来棒极了!谢谢你的参考。

标签: c string segmentation-fault int parameter-passing


【解决方案1】:

我故意将错误类型的参数传递给函数,但试图避免段错误。

这就像说我要去海里但尽量避免弄湿。

当你做某事非法时,你最终只能调用undefined behavior,这可能会导致段错误。

避免最好的方法是编写正确的代码。


问题是,函数需要char*,而您传递的是int。无论如何,这是不允许的。这是错误的,您不得忽略编译器警告。

更详细地说,该函数需要一个指向char (char*) 类型的指针,此外,代码涉及从指针指向的地址位置读取。当您将 int 传递给函数时(忽略编译器警告),代码会尝试访问由提供的整数值指向的 memory,这很可能是一个 invalid em> 内存位置。因此,这种访问无效内存位置的尝试会调用 UB。

【讨论】:

  • 好的,所以这会触发未定义的行为?所以所有的赌注都没有了,它是特定于实现的,任何事情都可能发生?
  • @ridthyself 好吧,从技术上讲,它是未定义的,所以我不会使用“特定于实现”这个术语,因为正式它具有另一种含义。是的,一旦你点击了 UB,任何事情都可能发生,任何事情都可能发生。
  • 谢谢!这很有帮助。您能否将您的评论移至您的答案,也许详细说明这是如何导致“UB”的,我会选择它。
  • @ridthyself 现在好点了吗? :)
【解决方案2】:

标准的 C 答案是该代码具有未定义的行为,因此所有的赌注都被取消了。 (传递-Werror 将所有编译器警告视为错误,传递-pedantic 以获得标准所需的所有诊断。)正如Keith Thompson 所指出的,C 标准实际上需要此(损坏的)代码的诊断消息,并且编译器可能会拒绝编译它。

实际上,代码可能会将数字34 重新解释为内存地址,然后memcpy 尝试从(char *)34 中读取。这通常会导致分段错误,因为该地址位于内存的第一页内,该地址未映射以检测何时有人取消引用空指针。

【讨论】:

  • 你的意思是 34 可能被解释为内存地址吗?
  • 并将-pedantic-errors-Wall 添加到-Werror。类型检查在 C 中的编译时完成。如果在编译时不启用类型检查,则没有类型检查。
  • @ridthyself 是的,可能就是这样。 34 可能被解释为虚拟内存地址,并且虚拟内存的第一页未映射以捕获空指针的意外取消引用。
  • 这不仅仅是未定义的行为。这是违反约束的。编译器可能(恕我直言应该)无法编译test.c。如果它仍然成功编译(在发出强制诊断消息之后),那么生成的代码的行为是未定义的。
  • @ridthyself 是的,现已修复。
【解决方案3】:
struct User newUser(char* name)
...
struct User testUser = newUser(34);

所有相关声明都在错误调用处可见。您的newUser 函数需要char* 类型的参数,而您将其传递给int。这是一个约束冲突,这意味着任何符合要求的编译器都需要发出诊断消息。

不幸的是(但在法律上),一些编译器会针对此特定错误发出非致命警告,至少在其默认模式下是这样。

解决方案是以将其视为致命错误的模式调用编译器。例如,如果您使用 gcc 或 clang,则可以添加一个或多个命令行选项,例如 -Werror-pedantic-errors

如果您选择忽略任何错误(您确实不应该这样做),那么您的程序就没有避免该问题的好方法。唯一的解决方案是首先避免编写无效调用(并使用编译器的诊断来帮助您做到这一点)。

话虽如此,您的代码中存在一些不相关的问题。

struct User newUser(char* name)

由于函数不会修改name指向的数据,所以参数应该定义为const char *name

memset(newUser.name, '\0', 21);

21 是一个幻数。没有什么可以告诉读者为什么 21 是正确的字节数,如果您以后决定更改name 的长度,则必须手动更新对它的所有引用。定义一个常量并使用它。

memcpy(newUser.name, name, 20);

如果字符串长度少于 20 个字符怎么办?您希望复制不超过 21 个字节并且不超过参数长度到newUser.name。 (这样做比应有的复杂得多strncpy 是显而易见的解决方案,但也几乎可以肯定是错误的解决方案。请参阅[我对这个话题的咆哮]https://the-flat-trantor-society.blogspot.com/2012/03/no-strncpy-is-not-safer-strcpy.html)。

你在头文件中有一个函数定义。不要那样做。函数应在.h 文件中声明并在.c 文件中定义。如果该标头在整个程序中只包含一次,您可以在 .h 文件中定义一个函数。对于较大的程序,情况并非如此。 (关于如何构造多文件 C 程序有很多话要说,但这超出了这个答案的范围。)

【讨论】:

  • 有趣的故事,我实际上在尝试解决这个问题时遇到了你的文章,这就是导致我完全放弃 strncpy 和 strcpy 并改用 memcpy 的原因,事实证明,这也是错误的回答。去图:)
  • 在函数签名中添加 const 有什么不同?这只是为了程序员的意识吗?
  • @ridthyself:如果参数上没有const,则不能将指向const 的指针作为参数传递。例如:void func(char *arg); /* ... */ const char *str = "hello"; func(str); 无效。
猜你喜欢
  • 2021-04-09
  • 2022-01-09
  • 1970-01-01
  • 2018-12-25
  • 2011-01-30
  • 2015-02-06
  • 1970-01-01
  • 1970-01-01
  • 2013-01-04
相关资源
最近更新 更多