تاريخ مكتبتين قياسيتين C.

تلقيت اليوم تقرير خطأ من مستخدم Debian الذي قدم بعض الهراء إلى الأداة المساعدة scdoc وحصل عليها SIGSEGV. سمح لي البحث عن المشكلة بإجراء مقارنة ممتازة بين musl libcو glibc. أولاً ، دعنا نلقي نظرة على تتبع المكدس:



==26267==ERROR: AddressSanitizer: SEGV on unknown address 0x7f9925764184
(pc 0x0000004c5d4d bp 0x000000000002 sp 0x7ffe7f8574d0 T0)
==26267==The signal is caused by a READ memory access.
    0 0x4c5d4d in parse_text /scdoc/src/main.c:223:61
    1 0x4c476c in parse_document /scdoc/src/main.c
    2 0x4c3544 in 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)


يقول الكود المصدري في هذا السطر:



if (!isalnum(last) || ((p->flags & FORMAT_UNDERLINE) && !isalnum(next))) {


تلميح: هذا pمؤشر صالح وغير فارغ. المتغيرات lastومن nextالنوع uint32_t. يحدث Segfault في استدعاء الوظيفة الثاني isalnum. والأهم من ذلك ، يمكن استنساخه فقط عند استخدام glibc ، وليس musl libc. إذا كان عليك إعادة قراءة الكود عدة مرات ، فأنت لست وحدك: ببساطة لا يوجد شيء لتشغيل segfault.



نظرًا لأنه كان معروفًا أن الأمر كله يتعلق بمكتبة glibc ، فقد حصلت على مصادرها وبدأت في البحث عن تطبيق isalnum، والاستعداد لمواجهة بعض الهراء الغبي. لكن قبل أن أصل إلى الحماقة الغبية ، والتي ، صدقوني ، بكميات كبيرة ، دعونا أولاً نلقي نظرة سريعة على خيار جيد. هذه هي الطريقة التي يتم بها isalnumتنفيذ الوظيفة في musl libc:



int isalnum(int c)
{
	return isalpha(c) || isdigit(c);
}

int isalpha(int c)
{
	return ((unsigned)c|32)-'a' < 26;
}

int isdigit(int c)
{
	return (unsigned)c-'0' < 10;
}


كما هو متوقع ، لأي قيمة cستعمل الوظيفة بدون segfault ، فلماذا بحق الجحيم isalnumيجب أن يتم طرح segfault على الإطلاق ؟



حسنًا ، دعنا الآن نقارن هذا مع تطبيق glibc . بمجرد أن تفتح العنوان ، سيتم استقبالك بهراء نموذجي من غنو ، لكن دعنا نتخطاه ونحاول العثور عليه isalnum.



النتيجة الأولى هي:



enum
{
  _ISupper = _ISbit (0),        /* UPPERCASE.  */
  _ISlower = _ISbit (1),        /* lowercase.  */
  // ...
  _ISalnum = _ISbit (11)        /* Alphanumeric.  */
};


يبدو أنه أحد تفاصيل التنفيذ ، دعنا ننتقل.



__exctype (isalnum);


لكن ما هذا __exctype؟ نعود بضعة أسطر ...



#define __exctype(name) extern int name (int) __THROW


حسنًا ، يبدو أن هذا مجرد نموذج أولي. ومع ذلك ، ليس من الواضح سبب الحاجة إلى الماكرو هنا. مزيد من البحث ...



#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...


لذلك ، يبدو هذا بالفعل وكأنه شيء مفيد. ما هذا __isctype_f؟ هز ...



#ifndef __cplusplus
# define __isctype(c, type) \
  ((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
#elif defined __USE_EXTERN_INLINES
# define __isctype_f(type) \
  __extern_inline int                                                         \
  is##type (int __c) __THROW                                                  \
  {                                                                           \
    return (*__ctype_b_loc ())[(int) (__c)] & (unsigned short int) _IS##type; \
  }
#endif


حسنًا ، إنها تبدأ ... حسنًا ، سنكتشفها معًا بطريقة ما. على ما يبدو ، __isctype_fهذه وظيفة مضمنة ... توقف ، كلها داخل كتلة else لتعليمات المعالج المسبق #ifndef __cplusplus. نهاية. أين isalnum، والدتها ، في الواقع تعرف؟ مزيد من البحث ... ربما هذا هو؟



#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...
# elif defined __isctype
# define isalnum(c)     __isctype((c), _ISalnum) // <-  


مرحبًا ، هذه هي "تفاصيل التنفيذ" التي رأيناها سابقًا. تذكر؟



enum
{
  _ISupper = _ISbit (0),        /* UPPERCASE.  */
  _ISlower = _ISbit (1),        /* lowercase.  */
  // ...
  _ISalnum = _ISbit (11)        /* Alphanumeric.  */
};


دعنا نحاول بسرعة اختيار هذا الماكرو:



# include <bits/endian.h>
# if __BYTE_ORDER == __BIG_ENDIAN
#  define _ISbit(bit)   (1 << (bit))
# else /* __BYTE_ORDER == __LITTLE_ENDIAN */
#  define _ISbit(bit)   ((bit) < 8 ? ((1 << (bit)) << 8) : ((1 << (bit)) >> 8))
# endif


ما هذا بحق الجحيم؟ حسنًا ، دعنا ننتقل ونعتبر أن هذا مجرد ثابت سحري. يسمى ماكرو آخر __isctype، وهو مشابه لما رأيناه مؤخرًا __isctype_f. لنلق نظرة أخرى على الفرع #ifndef __cplusplus:



#ifndef __cplusplus
# define __isctype(c, type) \
  ((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
#elif defined __USE_EXTERN_INLINES
// ...
#endif


آه ...



حسنًا ، على الأقل وجدنا إشارة مرجعية للمؤشر قد تفسر segfault. ما هذا __ctype_b_loc؟



/*      ctype-info.c.
          localeinfo.h.

     ,   , (. `uselocale'  <locale.h>)
        ,  .
    ,   -,   
    ,    ,   .

        384 ,    
     `unsigned char' [0,255];   EOF (-1);  
    `signed char' value [-128,-1).  ISO C ,   ctype 
      `unsigned char'  EOF;    
    `signed char'      .
          `int`,
     `unsigned char`,   `tolower(EOF)'   EOF,   
       `unsigned char`.     - , 
         .  */
extern const unsigned short int **__ctype_b_loc (void)
     __THROW __attribute__ ((__const__));
extern const __int32_t **__ctype_tolower_loc (void)
     __THROW __attribute__ ((__const__));
extern const __int32_t **__ctype_toupper_loc (void)
     __THROW __attribute__ ((__const__));


كم هو رائع منك ، غليبك! أنا فقط أحب التعامل مع المواقع. على أي حال ، فإن gdb متصل بتطبيقي المعطل ، ومع كل المعلومات التي تلقيتها في الاعتبار ، أكتب هذا القذارة:



(gdb) print ((unsigned int **(*)(void))__ctype_b_loc)()[next]
Cannot access memory at address 0x11dfa68


تم العثور على Segfault. كان هناك سطر حول هذا في التعليق: "تتطلب ISO C دوال ctype للعمل مع قيم مثل" حرف غير موقعة و EOF ". إذا وجدنا هذا في المواصفات ، نرى:



في جميع عمليات التنفيذ [للوظائف المعلنة في ctype.h] ، تكون الوسيطة عبارة عن int ، يجب أن تتناسب قيمتها مع حرف غير موقعة ، أو مساوية لقيمة ماكرو EOF.



الآن أصبح من الواضح كيفية حل المشكلة. مفاصلي. اتضح أنني لا أستطيع إطعام isalnumحرف UCS-32 تعسفيًا للتحقق من حدوثه في النطاقات 0x30-0x39 و 0x41-0x5A و 0x61-0x7A.



لكن هنا سأستفيد من اقتراح: ربما isalnumلا يجب أن تؤدي الوظيفة إلى إجراء segfault على الإطلاق ، بغض النظر عما تحصل عليه؟ ربما حتى لو سمحت المواصفات بذلك ، فهذا لا يعني أنه يجب القيام بذلك بهذه الطريقة ؟ ربما ، كفكرة مجنونة ، لا ينبغي أن يحتوي سلوك هذه الوظيفة على خمسة وحدات ماكرو ، والتحقق من استخدام مترجم C ++ ، وتعتمد على ترتيب البايت الخاص بالعمارة ، وجدول البحث ، وبيانات لغة الدفق ، وإشارة مرجعية مؤشرين؟



دعنا نلقي نظرة أخرى على نسخة الموسل كتذكير سريع:



int isalnum(int c)
{
	return isalpha(c) || isdigit(c);
}

int isalpha(int c)
{
	return ((unsigned)c|32)-'a' < 26;
}

int isdigit(int c)
{
	return (unsigned)c-'0' < 10;
}


هذه هي الفطائر.



ملاحظة المترجم: شكرًا لـ MaxGraey على الارتباط بالأصل.



All Articles