两个LIBCC的故事

2020-09-27 17:02:34

今天我收到Debian的一个bug报告,他把一些垃圾放到了scdoc中,它给了他们一个SIGSEGV反馈,深入这个问题给了我一个很好的机会来比较musl libc和glibc。让我们从堆栈跟踪开始:

==26267==错误:地址清洁器:未知地址0x7f9925764184上的SEGV(PC 0x0000004c5d4d BP 0x000000000002 Sp 0x7ffe7f8574d0 T0)==26267==信号由读存储器访问引起。0 0x4c5d4d位于parse_text/scdoc/src/main.c:223:61 1 0x4c476c位于parse_document/scdoc/src/main.c 2 0x4c3544位于Main/scdoc/src/main.c:763:2 3 0x7f99252ab0b2 in___libc_start_main/build/glibc-YYA7BZ/glibc-2.31/csu/../csu/libc-start.c:308:16 4 0x41b3fd in_start(/scdoc/scdoc+0x41b3fd)。

提示:P是有效的指针。“last”和“next”都是uint32_t,段错误在第二次调用isalnum时出现。关键是:它只能在glibc上重现,不能在MUSL libc上重现。如果你做了双重拍摄,你并不孤单。这里没有任何可能导致分段故障的东西。

因为它被缩小到glibc,所以我调出了源代码,开始挖掘isalnum实现,以为会有一些愚蠢的胡说八道。但在我进入他们愚蠢的胡说八道之前,我可以向你保证,他们有很多愚蠢的胡说八道,让我们简要回顾一下快乐的版本。MUSL libc isalnum实现如下所示:

Int isalnum(Int C){return isalpha(C)||isdigit(C);}int isalpha(Int C){return((Unsign)c|32)-';a';<;26;}int isdigit(Int C){return(Unsign)c-&';0';<;10;}。

正如预期的那样,对于任何c值,isalnum永远不会出现Segerror。因为为什么他妈的isalnum会出现分段故障?好的,现在,让我们将其与glibc实现进行比较。当打开这个头文件时,您会看到典型的GNU胡说八道,但是让我们长途跋涉,并为isalnum执行grep。

枚举{_IS大写=_ISbit(0),/*大写。*/_ISLOWER=_ISbit(1),/*小写。*/..._ISalnum=_ISbit(11)/*字母数字。*/};

好吧,显然那只是原型。不知道为什么他们觉得有必要为此编写一个宏命令。下一个搜索结果…

#ifndef__cplusplus#定义__isctype(c,type)\((*__ctype_b_loc())[(Int)(C)]&;(无符号短整型)#Elif定义__use_extern_inlines#定义__isctype_f(Type)\__extern_inline int\is##type(Int_C)__throw\{\return(*__ctype_b_loc()[(Int)(_C)]]&;(无符号短整型)_is##type;\}#endif。

哦,…。。哦,亲爱的。没关系,我们会一起解决这件事的。让我们看看,__isctype_f是某种内联函数…。等等,这是#ifndef__cplusplus的Else分支。死胡同。他妈的isalnum到底在哪里定义的?再次grep…。好的,…。我们到了?。

#if!Defined__no_ctype#ifdef__isctype_f__isctype_f(Alnum)//...#Elif定义__isctype#定义isalnum(C)__isctype((C),_ISalnum)//<;-就是这样。

枚举{_IS大写=_ISbit(0),/*大写。*/_ISLOWER=_ISbit(1),/*小写。*/..._ISalnum=_ISbit(11)/*字母数字。*/};

#include<;bits/endian.h>;#if__byte_order==__BIG_ENTER#DEFINE_ISbit(BIT)(1<;<;(BIT))#ELSE/*__BYTE_ORDER==__Little_ENDER*/#DEFINE_ISbit(BIT)((BIT)<;8?(1<;<;(BIT))<;8):((1<;<;(BIT))>;>;8))#endif。

哦,看在他妈的份上。不管怎样,让我们继续,假设这是一个魔数。另一个宏是__isctype,类似于我们刚才看到的__isctype_f。让我们再来看看ifndef__cplusplus分支:

#ifndef__cplusplus#定义__isctype(c,type)\((*__ctype_b_loc())[(Int)(C)]&;(无符号短整型)类型)#Elif定义__USE_EXTERN_INLINES//...#endif。

好吧,至少我们现在有了一个指针取消引用,这可以解释这个错误。什么是__ctype_b_loc?

/*这些在ctype-info.c中定义。这里的声明必须与localeinfo.h中的声明匹配。在线程特定的区域设置模型中(请参阅<;locale.h>;中的`uselocale';),我们不能像过去那样使用全局变量。相反,以下访问器函数返回每个变量的地址,如果是多线程的,则该地址是当前线程的本地地址。它们指向384个数组,因此可以通过任何“无符号字符”值[0,255]、通过EOF(-1)或任何“有符号字符”值[-128,-1)对它们进行索引。ISO C要求ctype函数适用于`unsign char&39;值和EOF;我们还支持对损坏的旧程序使用负的`sign char';值。大小写转换数组是`int#39;s而不是`unsign char&39;s,因为Tolower(EOF)必须是EOF,而EOF不适合`unsign char&39;s。但今天更重要的是,数组也用于多字节字符集。*/extern const无符号短int**__ctype_b_loc(Void)__throt__Attribute__((__Const__));extern const__int32_t**__ctype_tolower_loc(Void)__throt__Attribute__((__Const__));外部const__int32_t**_ctype_Toupper_loc(Void)__掷出__属性__((__Const__));

真是太棒了,你真是太酷了,Glibc。我就是喜欢与本地化打交道。不管怎么说,我的分段进程位于gdb中,并且配备了所有这些信息,我写了下面这个庞然大物:

发现段故障。再次阅读该注释,我们会看到“ISO C要求类型函数对‘unsign char’值和EOF起作用”。如果我们将其与规范交叉引用:

在[由ctype.h定义的函数]的所有情况下,参数都是一个int,它的值应该可以表示为无符号字符,或者应该等于宏EOF的值。

因此,在这一点上,解决办法是显而易见的。好吧,好吧,是我的错。我的代码是错误的。我显然不能简单地把一个UCS-32码点交给isalnum,然后指望它告诉我它是在0x30-0x39、0x41-0x5A还是0x61-0x7A之间。

但是,我在这里要冒险一试:也许isalnum永远不会导致程序分段错误,无论您给它什么输入。也许因为镜面上说你可以,并不意味着你应该这样做。也许,仅仅是可能,这个函数的行为不应该依赖于五个宏,无论您是否使用ac++编译器、您机器的字节顺序、查找表、线程本地存储和两个指针取消引用。

Int isalnum(Int C){return isalpha(C)||isdigit(C);}int isalpha(Int C){return((Unsign)c|32)-';a';<;26;}int isdigit(Int C){return(Unsign)c-&';0';<;10;}。

对我的一篇帖子有什么评论吗?通过发送电子邮件至~sircmpwn/[email protected][邮件列表礼仪],在我的公共收件箱中开始讨论