在C++(g++)中,将errno作为异常参数时出现意外的控制流(编译器错误?)

9

当使用C++异常来传输errno状态时,g++(4.5.3)为以下代码生成的编译代码

#include <cerrno>
#include <stdexcept>
#include <string>

class oserror : public std::runtime_error {
private:
    static std::string errnotostr(int errno_);
public:
    explicit oserror(int errno_) :
        std::runtime_error(errnotostr(errno_)) {
    }
};

void test() {
    throw oserror(errno);
}

意外地(在Linux、x86_64上)

    .type   _Z4testv, @function
    ...
    movl    $16, %edi
    call    __cxa_allocate_exception
    movq    %rax, %rbx
    movq    %rbx, %r12
    call    __errno_location
    movl    (%rax), %eax
    movl    %eax, %esi
    movq    %r12, %rdi
    call    _ZN7oserrorC1Ei

基本上,这意味着作为C++异常参数的errno几乎是无用的,因为在调用__errno_location(即errno的宏内容)之前调用了__cxa_allocate_exception,后者调用std::malloc并且不保存errno状态(至少根据我所理解的libstdc++中eh_alloc.cc的__cxa_allocate_exception源代码内容如此)。这意味着在内存分配失败的情况下,实际应该传递给异常对象的错误号被覆盖为std::malloc设置的错误号。无论如何,std::malloc甚至在成功退出的情况下也不能保证保存现有的errno状态 - 因此上述代码在一般情况下肯定是错误的。在Cygwin、x86上,使用g++ 4.5.3编译test()的代码没问题。
    .def    __Z4testv;      .scl    2;      .type   32;     .endef
    ...
    call    ___errno
    movl    (%eax), %esi
    movl    $8, (%esp)
    call    ___cxa_allocate_exception
    movl    %eax, %ebx
    movl    %ebx, %eax
    movl    %esi, 4(%esp)
    movl    %eax, (%esp)
    call    __ZN7oserrorC1Ei

这是否意味着,为了使库代码在异常中正确包装errno状态,我总是需要使用一个宏,该宏扩展为类似于以下内容:

    int curerrno_ = errno;
    throw oserror(curerrno_);

实际上我找不到C++标准中关于异常情况下评估顺序的相应部分,但在我看来,在 x86_64(Linux 上)上生成的 g++ 代码由于在收集其构造函数参数之前为异常对象分配内存而导致无法正常工作,并且这在某种程度上是编译器的错误。我是正确的吗?还是我的想法基本上是错误的?


errnotostr()oserror类的(静态)函数,基本上调用了strerror_r()并返回结果的std::string形式。我没有包含代码的那部分,因为它对示例不相关。 - modelnine
抱歉,我脑子一抽了。请忽略。 - irobot
2个回答

1
请注意,在实际调用构造函数之前,__cxa_allocate_exception已经完成。
  32:std_errno.cpp ****     throw oserror( errno );
 352 0007 BF100000      movl    $16, %edi
 ;;;; Exception space allocation:
 355 000c E8000000      call    __cxa_allocate_exception
 356 0011 4889C3        movq    %rax, %rbx
 ;;;; "errno" evaluation:
 357 0014 E8000000      call    __errno_location
 358 0019 8B00          movl    (%rax), %eax
 359 001b 89C6          movl    %eax, %esi
 360 001d 4889DF        movq    %rbx, %rdi
 ;;;; Constructor called here:
 362 0020 E8000000      call    _ZN7oserrorC1Ei

所以这是有道理的。 __cxa_allocate_exception 只分配异常的空间,但不构造它 (libc++abi 规范)。

当您的异常对象被构建时,errno 已经被评估。

我采用了您的示例并实现了 errnotostr

// C++ 中将 errno 作为异常参数时出现意外的控制流 (编译器错误?g++)

#include    <cerrno>
#include    <stdexcept>
#include    <string>

#include    <iostream>
#include    <cstring>
#include    <sstream>

class oserror : public std::runtime_error
{
private:
    static std::string errnotostr(int errno_)
    {
        std::stringstream   ss;

        ss << "[" << errno_ << "] " << std::strerror( errno_ );

        return  ss.str( );
    }

public:
    explicit oserror( int errno_ )
    :    std::runtime_error( errnotostr( errno_ ) )
    {
    }
};

void test( )
{
    throw oserror( errno );
}

int main( )
{
    try
    {
        std::cout << "Enter a value to errno: ";
        std::cin >> errno;

        std::cout << "Test with errno = " << errno << std::endl;
        test( );
    }
    catch ( oserror &o )
    {
        std::cout << "Exception caught: " << o.what( ) << std::endl;
        return  1;
    }

    return  0;
}

然后我使用-O0-O2编译,运行并得到了相同的结果,都符合预期:

> ./std_errno
Enter a value to errno: 1
Test with errno = 1Exception caught: [1] Operation not permitted

> ./std_errno
Enter a value to errno: 11
Test with errno = 11
Exception caught: [11] Resource temporarily unavailable

> ./std_errno
Enter a value to errno: 111
Test with errno = 111
Exception caught: [111] Connection refused

(在64位Opensuse 12.1上运行,G++ 4.6.2)


我并不是在抱怨进入构造函数时errno尚未被评估的事实(我知道它已经被评估了),而是在抱怨在获取errno之前调用__cxa_allocate_exception,因此基础设施代码可能会更改errno状态(请参见有关调用std::malloc()的讨论),然后在查询它的代码有机会运行并将评估的errno传递给构造函数之前。 - modelnine
我明白了。让我再清楚一点。考虑到异常本身可以改变 errno 的值,对于你最初的问题,答案是:是的,你需要在抛出异常之前获取 errno 的值。希望能有所帮助。 - j4x
иҜ·йҳ…иҜ»жҲ‘еҲҡж Үи®°дёәеӣһзӯ”й—®йўҳзҡ„еӣһеӨҚд»ҘеҸҠзӣёе…іиҜ„и®ә - й—®йўҳдёҚеңЁдәҺжһ„йҖ еҮҪж•°ж”№еҸҳerrnoпјҢиҖҢжҳҜе…ідәҺthrowиҜӯеҸҘз”ҹжҲҗзҡ„д»Јз ҒеҸҜиғҪдјҡеңЁе…¶еҖјдј йҖ’з»ҷжһ„йҖ еҮҪж•°д№ӢеүҚжӣҙж”№errnoзҡ„еҖјгҖӮи°ўи°ўпјҒ - modelnine

1
这基本上意味着,将errno作为C++异常的参数是没有什么用的,因为在调用__cxa_allocate_exception之前会调用__errno_location(即errno的宏内容),而前者调用了std::malloc并且不保存errno状态(至少根据我对libstdc++中eh_alloc.cc文件中__cxa_allocate_exception源代码的理解如此)。
这是不正确的。根据我检查的源代码,只有__cxa_allocate_exception内部的malloc()可以改变errno。可能出现两种情况:
1. malloc()成功,errno不变; 2. malloc()失败,然后调用std::terminate(),你的oserror()永远不会被构造。
因此,由于在调用你的构造函数之前调用_cxa_allocate_exception并不会在功能上改变你的程序,我认为g++有权这样做。

首先,是否有保证std::malloc()在分配成功时不会更改errno状态?这可能有点过于多疑了,但据我所知,根据SuSv4,它们只保证不将其设置为零,而不是保持不变。而且,在std::malloc()失败的情况下,不一定会调用std::terminate();在低内存条件下使用静态缓冲区来构造异常,因此即使引发了异常,errno也可能被设置。 - modelnine
首先,如果我正确解释了POSIX规范malloc()只有在发生错误时才会设置errno。其次,处理低内存条件的代码仅调用pthread函数,这些函数将错误代码作为返回值发出,并且不使用errno - user1202136
这就是问题所在 - malloc()的规范只给出了一个正面例子(“否则,它将返回一个空指针并设置errno来指示错误”),在这种情况下,errno已经被覆盖,我本来想传递给oserror()的原始errno代码丢失了(而且在lowmem-path上的pthread_*方法不改变这种情况下的errno也没有关系)。但对于没有错误的情况,在该规范中没有提供任何规定,并且据我所知, SuSv4也没有规定函数在成功时必须不更改errno,只要它们不将其设置为零即可。 - modelnine
1
让我重新聚焦讨论到你最初的问题。g++应该编译任何它想要的汇编代码,以便在将其传递给异常构造函数时保留errno [ISO/IEC 14882:2011, 15.1.3]。只有当它“知道”(例如,因为g++与stdc++捆绑在一起)__cxa_allocate_exception保留了errno,然后g++才允许在调用__errno_location之前调用它。在我看来,__cxa_allocate_exception应该确保它没有不希望的副作用,它“几乎”做到了,除了低内存条件的情况。 - user1202136
好的 - g++ 不知道 __cxa_allocate_exception 没有副作用(仅仅因为它在某些情况下确实有副作用,我们现在已经基本确定了,这也是我的原始问题所在)。因此,这回答了这个问题,即这确实是一个编译器(优化?)bug。我认为我已经将其缩小到了 __errno_location() 的声明上,它在 glibc 上有 __attribute__((const)),但在 cygwin libc 上没有(而且 g++ 生成了“正确”的代码)。谢谢! - modelnine

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接