Sejarah Dua Perpustakaan C Standar

Hari ini saya mendapat laporan bug dari pengguna Debian yang memasukkan beberapa omong kosong ke dalam utilitas scdoc dan mendapatkannya SIGSEGV. Meneliti masalah memungkinkan saya membuat perbandingan yang sangat baik antara musl libcdan glibc. Pertama, mari kita lihat jejak tumpukan:



==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)


Kode sumber di baris ini mengatakan ini:



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


Petunjuk: Ini padalah pointer non-null yang valid. Variabel lastdan nexttipe uint32_t. Segfault terjadi pada pemanggilan fungsi kedua isalnum. Dan, yang paling penting, hanya dapat direproduksi saat menggunakan glibc, bukan musl libc. Jika Anda harus membaca ulang kode beberapa kali, Anda tidak sendirian: tidak ada yang memicu segfault.



Karena diketahui bahwa semuanya ada di perpustakaan glibc, saya mendapatkan sumbernya dan mulai mencari implementasi isalnum, bersiap-siap untuk menghadapi omong kosong bodoh. Tapi sebelum saya sampai ke omong kosong bodoh, yaitu, percayalah, dalam jumlah besar , pertama-tama mari kita lihat sekilas pilihan yang baik. Ini adalah bagaimana fungsi tersebut isalnumdiimplementasikan di 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;
}


Seperti yang diharapkan, untuk nilai apa pun cfungsi tersebut akan berfungsi tanpa segfault, karena mengapa isalnumharus segfault dilemparkan sama sekali ?



Oke, sekarang mari bandingkan ini dengan implementasi glibc . Segera setelah Anda membuka judul, Anda akan disambut dengan omong kosong khas GNU, tapi mari kita lewati dan coba menemukannya isalnum.



Hasil pertama adalah ini:



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


Sepertinya detail implementasi, mari kita lanjutkan.



__exctype (isalnum);


Tapi apa itu __exctype? Kami mundur beberapa baris ...



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


Oke, ternyata ini hanya prototipe. Namun, tidak jelas mengapa makro diperlukan di sini. Melihat lebih jauh ...



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


Jadi, ini sudah terlihat seperti sesuatu yang berguna. Apa itu __isctype_f? Gemetar ...



#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


Yah, itu dimulai ... Oke, kita akan mencari tahu bagaimana caranya. Rupanya, __isctype_fini adalah fungsi inline ... hentikan, semuanya ada di blok else dari instruksi preprocessor #ifndef __cplusplus. Jalan buntu. Di mana isalnum, ibunya, sebenarnya didefinisikan? Melihat lebih jauh ... Mungkinkah ini dia?



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


Hei, ini adalah "detail implementasi" yang kita lihat sebelumnya. Ingat?



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


Mari kita coba dengan cepat memilih makro ini:



# 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


Apa-apaan ini? Oke, mari kita lanjutkan dan pertimbangkan bahwa ini hanyalah konstanta ajaib. Makro lain dipanggil __isctype, yang mirip dengan yang kita lihat baru-baru ini __isctype_f. Mari kita lihat lagi cabangnya #ifndef __cplusplus:



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


Uh ...



Yah, setidaknya kami menemukan referensi penunjuk yang mungkin menjelaskan segfault. Apa itu __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__));


Betapa kerennya dirimu, glibc! Saya suka berurusan dengan lokal. Bagaimanapun, gdb terhubung ke aplikasi saya yang rusak, dan dengan semua informasi yang saya terima, saya menulis kemelaratan ini:



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


Segfault ditemukan. Ada baris tentang ini di komentar: "ISO C membutuhkan fungsi ctype untuk bekerja dengan nilai seperti สปunsigned char 'dan EOF". Jika kami menemukan ini di spesifikasi, kami melihat:



Dalam semua implementasi [fungsi yang dideklarasikan di ctype.h], argumennya adalah int, yang nilainya harus sesuai dengan karakter unsigned, atau sama dengan nilai makro EOF.



Sekarang menjadi jelas bagaimana memperbaiki masalah tersebut. Sendi saya. Ternyata saya tidak dapat memberi makan isalnumkarakter UCS-32 sewenang-wenang untuk memeriksa kemunculannya dalam rentang 0x30-0x39, 0x41-0x5A, dan 0x61-0x7A.



Tapi di sini saya akan dengan leluasa menyarankan: mungkin fungsi isalnumseharusnya tidak membuang segfault sama sekali, terlepas dari apa yang didapatnya? Mungkin meskipun spesifikasinya memungkinkan , bukan berarti harus dilakukan seperti ini ? Mungkin, ide gila saja, perilaku fungsi ini tidak boleh berisi lima makro, periksa penggunaan kompiler C ++, bergantung pada urutan byte arsitektur Anda, tabel pencarian, data lokal aliran, dan dereferensi dua petunjuk?



Mari kita lihat lagi versi musl sebagai pengingat cepat:



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;
}


Ini pai.



Catatan Penerjemah: Terima kasih kepada MaxGraey karena telah menautkan ke aslinya.



All Articles