&errno在C语言中是否合法?

53

根据7.5章节的要求,[errno]是一个int类型的可修改的lvalue(可以通过多个库函数设置为正数错误码),其值由宏或具有外部链接的标识符定义。如果禁止宏定义以访问实际对象,或程序定义了名为errno的标识符,则行为未定义。

注175:宏errno不一定是对象的标识符。它可能扩展为可修改的lvalue,这来自于函数调用的结果(例如, *errno())。

不清楚是否有足够的理由要求&errno不违反约束条件。C语言有lvalue(例如寄存器存储类变量;但这些只能是自动的,因此不能将errno定义为此类变量),对于其中的&运算符是约束条件的违规。

如果&errno是合法的C语言,是否需要是常量?


3
不一定。它是一个左值,所以你可以将其赋值。能够取地址与此并不遥远。 - R.. GitHub STOP HELPING ICE
1
也许这个问题很相关:lvalue (union {signed int x:32;}){0}.x 的类型是什么?(假设32是 int 的宽度;根据需要进行替换)。 - R.. GitHub STOP HELPING ICE
1
如果它是可修改的且类型为“int”,那么获取其地址应该是合法的,不是吗?现在这个地址后来是否有效是有争议的;某种实现方法可能只使它在使用它的表达式中有效。 - Seth Carnegie
1
一元&运算符的操作数必须是函数设计器、[]或一元*运算符的结果,或指定一个不是位域且未声明为寄存器存储类别说明符的对象的lvalue。(6.5.3.2) - R.. GitHub STOP HELPING ICE
2
@SethCarnegie:我不这么认为。你明确被禁止使用register存储类别获取变量的地址,但它们仍然可以用作lvalue。 - Kerrek SB
显示剩余8条评论
5个回答

19

所以§6.5.3.2p1指定:

一元&运算符的操作数必须是函数设计器、[]或一元*运算符的结果,或者指定一个不是位域并且没有使用寄存器存储类说明符声明的对象的左值。

认为可以理解为&lvalue适用于那两个类别之外的任何左值。正如您所提到的,errno不能使用寄存器存储类说明符声明,并且我认为(尽管现在不会追查参考文献),您不能拥有具有纯整型类型的位域。

因此,我认为规范要求&(errno)是合法的 C。

如果&errno是合法的C,它是否需要是常量?

据我所知,允许errno成为宏(以及其在例如glibc中的原因)的部分原因是允许它成为对线程本地存储的引用,在这种情况下,它肯定不会跨线程保持不变。 我没有看到任何理由要求它必须是常量。只要errno的值保留指定的语义,我认为一个歪曲的C库可以更改&errno以在程序的执行过程中引用不同的内存地址 - 例如,每次设置errno时释放并重新分配后备存储。

您可以想象维护由库设置的最后N个errno值的环形缓冲区,并且使&errno始终指向最新的值。我认为它不会特别有用,但是我看不到它违反规范的任何方法。


关于地址是否是常量的话题,我知道它在线程之间可能会有所不同;我的想法是关于您是否可以依靠通过其地址存储和检索errno。例如,如果您有一个函数void print_int_at(int *p);,那么调用print_int_at(&errno)来打印errno的值是否有效,或者在函数读取它之前errno是否会移动到另一个地址(在这种情况下,您需要将其存储在临时变量中,或者执行类似于print_int_at((int[]){errno});的操作)? - R.. GitHub STOP HELPING ICE
位域可以是普通的 int 类型;只是语言没有定义它是有符号还是无符号类型(当应用于位域时)。因此,给定 struct b { int b0 : 1; },不清楚该位域的可接受值是什么。 - Jonathan Leffler

16

我很惊讶没有人引用C11规范。抱歉引用了这么长的内容,但我认为它是相关的。

7.5 错误

头文件<errno.h>定义了几个宏...

...以及errno

errno扩展为一个可修改的lvalue(201),其类型为int并具有线程本地存储期,多个库函数会将其值设置为正错误号码。如果压制宏定义以访问实际对象,或者程序定义了名称为errno的标识符,则行为是未定义的。

在程序启动时,初始线程中errno的值为零(其他线程中errno的初始值为不确定值),但是任何库函数都不会将其设置为零。(202)即使没有错误,库函数调用也可能将errno的值设置为非零值,前提是该函数的描述中未记录使用errno

(201) 宏errno不必是对象的标识符。它可以扩展为从函数调用中得到的可修改的lvalue(例如*errno())。

(202) 因此,对于使用errno进行错误检查的程序,应在调用库函数之前将其设置为零,然后在随后的库函数调用之前检查它。当然,库函数可以在进入时保存errno的值,然后将其设置为零,只要在返回之前如果errno的值仍然为零,则恢复原始值即可。

"Thread local" 意味着 register 不再使用。 类型 int 意味着位域不再使用(依我看)。 因此,&errno 在我看来是合法的。
持久使用诸如 "它" 和 "the value" 等词语表明标准的作者并没有考虑到 &errno 是非常数的。 我想人们可以想象一种实现,在特定线程内 &errno 不是常量,但为了按照脚注所说的方式使用它(设置为零,然后在调用库函数后检查),必须是有意对抗的,并且可能需要专门的编译器支持才能对抗。
简而言之,如果规范允许非常量的 &errno,我认为这不是故意的。
[更新]
R. 在评论中提出了一个很好的问题。 经过思考,我相信我现在知道了他的问题和原始问题的正确答案。 让我看看是否能说服你,亲爱的读者。
R. 指出 GCC 允许在顶层使用类似以下内容的东西:
register int errno asm ("r37");  // line R

这将声明errno为一个在寄存器r37中保持的全局值。显然,它将是一个可修改的线程本地lvalue。那么,符合C实现能像这样声明errno吗?
答案是否定的。当你或我使用“声明”这个词时,我们通常会有一个口语和直观的概念。但标准并不以口语或直观的方式进行表述; 它精确地讲话,并且只旨在使用定义明确的术语。在“声明”的情况下,标准本身定义了这个术语; 当它使用该术语时,它正在使用自己的定义。
通过阅读规范,您可以了解“声明”是什么,以及它不是什么。换句话说,标准描述了语言“C”。它没有描述“不是C的某种语言”。就标准而言,“具有扩展功能的C”只是“不是C的某种语言”。
因此,从标准的角度来看,R行根本不是声明。它甚至无法解析!它可能与以下内容相同:
long long long __Foo_e!r!r!n!o()blurfl??/**

就规范而言,这与R行一样只是一个“声明”,也就是说,根本不算声明。
因此,当C11规范在第6.5.3.2节中说:
“一元&运算符的操作数应该是函数指示器、[]或一元*运算符的结果,或者是指定了不是位域并且没有使用register存储类说明符声明的对象的lvalue。”
它意味着非常精确的事情,与R行之类的东西无关。
现在,考虑errno所引用的int对象的声明。(注意:我指的不是errno名称的声明,因为如果errno是宏,则可能没有这样的声明。我指的是底层int对象的声明。)
上述语言表示,除非它指示了一个位域或指示了一个使用了“register”存储类说明符的对象,否则您可以获取lvalue的地址。底层errno对象的规范说明它是具有线程本地持续时间的可修改int lvalue。现在,的确规范没有说底层errno对象必须被声明。也许它只是通过一些实现定义的编译器魔法出现。但再次地,当规范说“使用了寄存器存储类说明符进行了声明”时,它正在使用自己的术语。因此,底层errno对象要么在标准意义下被“声明”,在这种情况下它不能同时是register和thread-local;要么根本没有被声明,这种情况下它就不是register声明的。无论哪种方式,由于它是lvalue,您都可以获取其地址。(除非它是位域,但我认为我们都同意位域不是int类型的对象。)

1
标准定义的register已经被淘汰了,因为它只能用于自动存储期。但是GCC作为扩展有全局register,并且它们始终是(固有的)线程本地的。这样实现errno是否合法? - R.. GitHub STOP HELPING ICE
无论如何,我仍然认为这是迄今为止最具信息量的答案之一。 - R.. GitHub STOP HELPING ICE
@R:哇,这是一个非常好的问题。一方面,全局“register”声明在标准下从技术上讲并不规范。另一方面,标准并没有说errno本身必须在标准描述的语言中实现。另一方面,取地址操作中的register例外应该是针对规范的程序而言的……难道不是吗?整个问题都非常棒。 - Nemo
通常情况下,errno并没有被声明。规范实现中,该宏展开为一个表达式,通过使用*运算符在函数返回的地址上进行求值,从而得到一个lvalue。因此,我不知道声明如何涉及其中。 - R.. GitHub STOP HELPING ICE
@R: "...并且没有使用register存储类别说明符声明"是可能会阻止您获取lvalue地址的措辞。(这不是errno的声明,而是这里相关的errno 对象 的声明。)“已声明”一词意味着“声明”。我将重新表述我的更新以明确这一点。 - Nemo
显示剩余3条评论

4
errno的最初实现是作为全局int变量,各种标准C库组件用它来指示错误值,如果它们遇到错误。然而,即使在那些日子里,人们也必须小心可重入代码或使用库函数调用时可能将errno设置为不同的值,因为您正在处理一个错误。通常,如果需要长时间使用错误代码,则会将该值保存在临时变量中,因为某些其他函数或代码片段可能会将errno的值显式地或通过库函数调用进行设置。
因此,对于这个全局int的最初实现,使用地址运算符并依赖地址保持不变基本上已经融入了库的结构中。
但是,随着多线程的出现,不再存在单个全局变量,因为单个全局变量不是线程安全的。因此,使用线程本地存储的想法可能会使用返回指向分配区域的指针的函数。因此,您可能会看到像以下完全虚构的示例构造:
#define errno (*myErrno())

typedef struct {
    // various memory areas for thread local stuff
    int  myErrNo;
    // more memory areas for thread local stuff
} ThreadLocalData;

ThreadLocalData *getMyThreadData () {
    ThreadLocalData *pThreadData = 0;   // placeholder for the real thing
    // locate the thread local data for the current thread through some means
    // then return a pointer to this thread's local data for the C run time
    return pThreadData;
}

int *myErrno () {
    return &(getMyThreadData()->myErrNo);
}

然后,通过errno = 0;或类似if(errno == 22) {//处理错误}和int *pErrno = &errno的方式使用errno,就好像它是一个单一的全局变量而不是线程安全的int变量。所有这些都能够正常工作,因为最终会分配线程本地数据区并保持不动,宏定义使errno看起来像是extern int,隐藏了其实际实现的管道。我们唯一不想要的是,在访问值时,errno的地址突然在具有某种动态分配、克隆、删除序列的线程的时间片之间移动。当你的时间片用完时,它就用完了,除非你涉及某种同步或某种方式来在时间片过期后保持CPU,否则线程本地区域移动似乎对我来说是一个非常危险的建议。
这意味着您可以依赖地址运算符为特定线程提供常量值,尽管常量值在不同线程之间会有所不同。我可以很好地看到库使用errno的地址,以减少每次调用库函数时进行某种线程本地查找的开销。
在线程内将errno的地址作为常量也提供了与旧版源代码的向后兼容性,这些代码使用了errno.h包含文件(请参见此linux上关于errno的man页面,其中明确警告不要像旧时代那样使用extern int errno;)。
我阅读标准的方式是允许这种类型的线程本地存储,同时保持语义和语法类似于旧的extern int errno;当使用errno时,并允许旧的用法,以便为不支持多线程的嵌入式设备的某种交叉编译器提供支持。然而,由于使用了宏定义,因此语法可能类似,因此不应使用旧式快捷方式声明,因为该声明不是实际errno的真实情况。

0
我们可以找到一个反例:因为位域可以具有类型int,所以errno可以是位域。在这种情况下,&errno将无效。标准的行为在于不明确地说明您可以编写&errno,因此未定义行为的定义适用于此处。

C11(n1570), § 4.一致性
在本国际标准中,通过‘‘undefined behavior’’一词或省略任何明确的行为定义来表明未定义行为。


我不清楚位域的类型是什么。它是 int 还是 "int 位域,宽度为 n"? - R.. GitHub STOP HELPING ICE

0

这似乎是一个有效的实现,其中&errno将是一个约束违规:

struct __errno_struct {
    signed int __val:12;
} *__errno_location(void);

#define errno (__errno_location()->__val)

所以我认为答案很可能是否定的...


"errno" 必须是 "int" 类型;而你的不是。 - Antti Haapala -- Слава Україні
它是什么类型?据我所知,是int类型,但当然这似乎不太合理。这就是问题的关键所在。 - R.. GitHub STOP HELPING ICE
这是一个12位宽的有符号位域。 - Antti Haapala -- Слава Україні

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