【问题标题】:Return statement does not get executed in creturn 语句不会在 c 中执行
【发布时间】:2019-01-03 03:07:26
【问题描述】:

所以,我有一个奇怪的案例,无法完全弄清楚我做错了什么。这是场景:

我编写了一个创建函数,它应该返回一个指向函数的指针。为了用数据填充结构,我读入了一个文本文件。根据我用作输入的文本文件,错误要么发生,要么不发生。 (错误发生在约 4000 行的文本文件中,而不是约 200 行的文件,如果这有所不同)。奇怪的是代码一直执行到return 语句之前。但它不会返回,只是挂起。没有错误,没有编译器警告(英特尔编译器)。我想知道是否有人经历过类似的事情或知道可能出了什么问题。

下面的代码被简化以说明问题。实际代码稍微复杂一些,因为我使用 Schreiner 的方法来处理 C 中的对象。

struct Somestruct {
    int A;
    int B;
    int C;
}

static void *Somestruct_ctor(void *_self) {
    struct Somestruct *self = _self;
    fillDataFromFile(self);
    printf("This line gets executed.\n");
    return self; // <- this one doesn't
}

int main(int argc, char *argv[]) {
    void *myObject;

    myObject = Somestruct_ctor(myObject);
    printf("The code does NOT get until here\n");
    return 0;
}

【问题讨论】:

  • void * myObject; 未初始化。读取其值(将其作为 arg 按值传递给 Somestruct_ctor(myObject) 是未定义的行为。如果您的编译器默认情况下没有对此发出警告,则启用像 gcc -Wall 这样的警告。您可能需要为编译器启用优化以注意,例如gcc -Og -Wall它可能恰好指向堆栈上的某个位置,导致缓冲区溢出,最终进入无限循环。
  • C11 Standard - 6.3.2.1 Lvalues, arrays, and function designators(p1) 左值是一个表达式(对象类型不是 void),它可能指定一个对象;64) 如果一个左值在计算时没有指定一个对象,则行为未定义.

标签: c return intel icc


【解决方案1】:

void * myObject; 未初始化,并且未指向有效存储。 读取其值(将其作为 arg 按值传递给 Somestruct_ctor(myObject))是未定义的行为。

您的代码并不总是崩溃的事实告诉我们,在 ICC 的代码生成中,它恰好指向某个有效的位置,可能是堆栈上的某个位置。对于较大的文件,我们可能会遇到缓冲区溢出,覆盖局部变量和/或返回地址并最终进入无限循环。 令人惊讶的是,由于它是偶然发生的,它仍然没有崩溃。(在禁用优化的 ICC 的 x86-64 asm 中,它只是加载一些未初始化的堆栈内存作为 @987654325 的 arg @.)

或者它可能是一个指向 stdio 数据结构的指针,在main 之前从 stdio 的 init 遗留下来。也许让fillDataFromFile 乱涂乱画FILE *stdout 指向的数据(例如)使其处于“锁定”状态,因此您的单线程被卡住,等待其他东西解锁互斥锁。 如果您了解 asm,单步执行printf 中的无限循环或“死锁”并查看究竟发生了什么可能会很有趣。


如果您使用gcc -O3 编译,编译器会将寄存器归零作为fillDataFromFile 的arg(在内联Somestruct_ctor 之后),因此它传递了一个NULL 指针。假设函数取消引用指针,这可能总是会崩溃。

clang 选择不初始化 rdi(x86-64 System V 调用 conventino 中的第一个 arg 传递寄存器),因此当 main 调用 fillDataFromFile 时它仍然保持 argc。这也会可靠地崩溃。


您忘记启用编译器警告。

所有主要的 x86 编译器(gcc、clang、MSVC、ICC)都对此有警告,但并非所有编译器都默认启用(仅在 MSVC 中)。可能是因为在某些情况下,如果有一些条件内容,编译器可能不确定 var 是否未初始化。在这种情况下,可以 100% 确定它肯定是在未初始化的情况下使用的,但如果 init 和 use 位于不同的 if() 块内,编译器可能无法证明仅在 init 发生时才发生使用。

使用 clang 和 gcc,您通常应该使用 -Wall 并将所有警告静音。

使用 ICC,-diag-enable:warn 更接近 gcc -Wall。 (ICC 的-Wall 没有启用这个非常重要的警告。不要误以为你已经用icc -Wall 启用了所有重要的警告。)

 # from icc -diag-enable:warn on your code
<source>(21): warning #592: variable "myObject" is used before its value is set
    myObject = Somestruct_ctor(myObject);
                               ^

how to turn on icc/icpc warnings? 有一些信息。与 gcc 相比,icc 的 -Wall 非常简单。所以也许-Wall -Wextra 对 icc 有用。它建议将-w2-w3 作为可能有用的警告级别。


Clang 通常有最好的警告,在这种情况下:

<source>:21:30: warning: variable 'myObject' is uninitialized when used here [-Wuninitialized]
  myObject = Somestruct_ctor(myObject);
                             ^~~~~~~~
<source>:19:18: note: initialize the variable 'myObject' to silence this warning
  void * myObject;
                 ^
                  = NULL
1 warning generated.

我通过编译您的源代码on the Godbolt compiler explorer 得到了上述输出(在修复语法错误后:结构后缺少分号,Struct 关键字的大写。)-xc 告诉 Godbolt 上的 C++ 编译器进行编译作为 C.

事实证明,您无需启用 icc 和 gcc 优化即可注意到此错误。一些警告仅在启用优化的情况下起作用,编译器对程序逻辑进行更多分析并且可以注意到更多,但即使在 -O0 处它们也会跟踪未初始化。


更有意义的构造函数代码:

// C
int main(void){
  struct Somestruct myObject;     // automatic storage for the object value
  Somestruct_ctor(&myObject);     // pass a pointer to that storage
}

对象需要存在于某个地方。我们可以通过自动(本地)、静态(static 本地或全局)或动态存储(malloc)为其获取空间。

如果struct Somestruct在struct/class定义中声明了一个C++默认构造函数,那么自动存储+调用构造函数就相当于C++这样。

// C++
int main(void){
  Somestruct myObject;     // calls the default constructor, if there is one
  // destructor will be called at some point when myObject goes out of scope
}

【讨论】:

    猜你喜欢
    • 2014-04-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-17
    • 1970-01-01
    • 2019-08-12
    相关资源
    最近更新 更多