1 คะแนน โดย GN⁺ 17 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • โค้ดที่ยึดตามมาตรฐาน ISO C อย่างเดียวพบได้น้อย และโค้ดเบส C ในโลกจริงมักพึ่งพาส่วนขยายที่ไม่เป็นมาตรฐาน เพื่อเพิ่มความสามารถและหลบช่องโหว่ของคอมไพเลอร์·ไลบรารีแต่ละตัว
  • คอมไพเลอร์ C ที่ใช้งานได้จริงต้องจัดการได้ตั้งแต่ system header อย่าง <stdio.h> แต่ glibc สร้างกำแพงด้วยส่วนขยายและสมมติฐานแบบ GNU เช่น __attribute__((packed)), #include_next
  • ลอจิก byteswapping ของ SDL สามารถเลือกใช้ inline assembly ได้ถ้ามีแมโคร ISA ทำให้คอมไพเลอร์ที่ไม่ใช่ GCC·clang ก็อาจถูกคาดหวังให้รองรับส่วนขยายสไตล์ GCC
  • การจัดการ extern inline ของ OpenBSD และ Gnulib ทำให้ความเข้ากันได้ของ semantic ของ inline ซับซ้อนขึ้น เพราะความต่างระหว่าง C99 กับ GCC, การแยกเงื่อนไขตามแพลตฟอร์ม, และเงื่อนไข _FORTIFY_SOURCE
  • คอมไพเลอร์ C ขนาดเล็กต้องเลือกระหว่างการส่งแพตช์ upstream, แพตช์ downstream, การทำ guard เฉพาะทาง, หรือ เลียนแบบความเข้ากันได้กับ GCC โดยการขยายการใช้ feature test macro ดูเป็นทิศทางที่ดีกว่า

กำแพงด่านแรกจาก header ของ glibc

  • หากจะเป็นคอมไพเลอร์ C ที่ใช้งานได้จริง ก็ต้อง preprocess และ parse header ของ system C library ได้ และถ้าจัดการ <stdio.h> ไม่ได้ ก็ยากแม้แต่จะให้ hello world ผ่าน
  • ในสภาพแวดล้อม GNU/Linux กำแพงนี้ก็คือ glibc
  • glibc ใช้ sys/cdefs.h ซึ่งถูก include ทางอ้อมโดย header ของ libc เกือบทั้งหมด เพื่อตรวจแมโครที่คอมไพเลอร์ predefine ไว้ แล้วตัดสินว่าส่วนขยายใดรองรับ
  • ส่วนขยายที่ไม่รองรับจะถูกจัดการด้วยการลบ definition ที่เกี่ยวข้องออก แต่ลอจิกความเข้ากันได้นี้เองก็อาจพังได้ในทางปฏิบัติ
  • struct epoll_event และ __attribute__((packed))

    • struct epoll_event ใน sys/epoll.h ของ Linux เป็น packed struct ที่ใช้ GNU __attribute__((packed))
    • แอททริบิวต์นี้เปลี่ยน layout ของ struct บน 64 บิต ดังนั้นถ้ามองข้ามไป ABI จะพัง
    • ต่อให้คอมไพเลอร์ implement __attribute__((packed)) แล้วก็ยังไม่พอ
    • ใน sys/cdefs.h มีโค้ดที่ถ้าไม่ใช่ GCC, clang, หรือ tcc จะ define __attribute__(xyz) เป็นแมโครว่าง
    • ผลคือคอมไพเลอร์อื่นแม้จะรองรับ packed attribute ก็อาจถูกลบแอททริบิวต์นี้ออกจาก header ของ glibc
    • เพราะ header ของ epoll ใช้เฉพาะ Linux จึงอาจมีข้อโต้แย้งว่าไม่ควรเอามาตรฐานการพกพาของ C มาตัดสินตรง ๆ
  • limits.h และ #include_next

    • header C บางตัวเช่น stddef.h, stdint.h, limits.h, float.h จำเป็นแม้กับ implementation แบบ freestanding ดังนั้นคอมไพเลอร์ต้องเป็นผู้จัดหา
    • POSIX กำหนดให้ limits.h ต้องนิยามค่าคงที่เฉพาะของ POSIX เพิ่มจากค่าคงที่มาตรฐาน C ด้วย จึงต้องมี limits.h ของแพลตฟอร์มซ้อนอยู่บน limits.h ของคอมไพเลอร์
    • <limits.h> ของ glibc จะ define ค่า ANSI limits.h เองถ้าไม่ใช่ GNU C และในสภาพแวดล้อม GCC จะดึง header ของคอมไพเลอร์ด้วย #include_next <limits.h>
    • โครงสร้างนี้ตั้งอยู่บนสมมติฐานว่า builtin limits.h เฉพาะของ GCC จะ define แมโครบ้างตัว และยังพึ่งพาส่วนขยาย #include_next ด้วย
    • clang เองก็ต้องแก้ทางโครงสร้างนี้

การตรวจจับความสามารถของ SDL กับปัญหา inline assembly

  • ฟังก์ชัน byteswapping ใน SDL_endian.h จะใช้ compiler builtin หรือ inline assembly ถ้าเป็นไปได้ และสุดท้ายค่อย fallback ไปใช้ implementation แบบ bit operation ธรรมดา
  • ลอจิกการตรวจจับทำงานโดยคร่าว ๆ ตามลำดับนี้
    • ถ้าเป็น GCC หรือ clang และมี __has_builtin(__builtin_bswapX) ให้ใช้ builtin
    • ถ้าเป็น MSVC 8.0 ขึ้นไป ให้ใช้ MSVC intrinsic #pragma
    • ถ้ามีการ define แมโครเฉพาะ ISA อย่าง __x86_64__ ให้ใช้ inline assembly
    • นอกนั้นจึงใช้ implementation แบบ bit operation ทั่วไป
  • ถ้าคอมไพเลอร์ที่ไม่ใช่ GCC หรือ clang มีเหตุผลสมควรที่จะ define predefined macro ตาม ISA ลำดับนี้ก็จะกลายเป็นปัญหา
  • ต่อให้คอมไพเลอร์นั้นมี bswap builtin และ special operator __has_builtin อยู่แล้ว ลอจิกนี้ก็ยังอาจพยายามใช้ inline assembly แบบ GCC
  • ผลก็คือโครงสร้างนี้กลายเป็นการคาดหวังว่า คอมไพเลอร์ที่ไม่รู้จัก ก็รองรับ inline assembly สไตล์ GCC เช่นกัน

ความสับสนของ OpenBSD libc กับ extern inline

  • header บางตัวของ OpenBSD มีนิยามฟังก์ชัน inline ที่คอมไพเลอร์จะเลือกใช้ได้เมื่อมีการ optimize
  • ฟังก์ชันเหล่านี้นิยามผ่านแมโคร __only_inline และถ้าคอมไพเลอร์ไม่ inline ให้จริง ก็ต้อง fallback ไปใช้ external symbol แทน
  • กล่าวคือจำเป็นต้องมี inline function ที่มี extern linkage
  • ความต่างของ semantic ของ inline ระหว่าง C99 กับ GCC

    • inline ถูกระบุไว้ใน C99 แต่พฤติกรรมตามมาตรฐานกลับขัดกับพฤติกรรมแบบ GCC ที่ไม่เป็นมาตรฐานก่อนยุค C99
    • นิยาม inline ใน header ต้องใช้ extern inline พร้อมกับตัวฟังก์ชัน และในกรณีนี้จะไม่ emit exported function จริง
    • ใน translation unit ต้องประกาศด้วย inline อย่างเดียวเพื่อ export นิยามของฟังก์ชัน
    • ความหมายของ inline ก็ยังต่างกันระหว่าง C++ กับ C ด้วย
    • ความต่างนี้อธิบายไว้อย่างละเอียดใน บทความของ Youtao Guo
  • __only_inline ของ OpenBSD

    • OpenBSD พึ่งพา GCC inline semantics
    • เพื่อกลบความต่างของ GCC แต่ละเวอร์ชัน แมโคร __only_inline ใน sys/cdefs.h จะระบุ gnu89 inline semantics แบบเก่าอย่างชัดเจนด้วย __attribute__ บน GCC รุ่นใหม่
    • บนคอมไพเลอร์ที่ไม่ใช่ GNU __only_inline จะถูก define ให้เป็น linkage แบบ static
    • ผลคือฟังก์ชันอาจถูกประกาศและนิยามด้วย linkage ที่ขัดกันจนพังได้
  • ทางเลี่ยงด้วย _ANSI_LIBRARY

    • OpenBSD ให้ความสำคัญกับแมโคร _ANSI_LIBRARY
    • ถ้า define แมโครนี้ จะข้ามการใช้ __only_inline ที่ทำให้พังใน standard header อย่าง signal.h ไปทั้งหมด
    • แม้จะไม่ได้เวอร์ชันที่ optimize แต่ก็อย่างน้อยยัง build ได้
  • โค้ดความเข้ากันได้ของ extern inline ใน Gnulib

    • โค้ดความเข้ากันได้ของ extern inline ใน Gnulib ยังโผล่มาตอน build Guile และ nano ด้วย
    • extern-inline.m4 มีเงื่อนไขแยกซับซ้อนเพื่อรองรับ implementation ที่พังและประหลาดของ corner case ใน C นี้
    • เงื่อนไขเหล่านี้สะท้อนความต่างของสภาพแวดล้อมอย่าง Apple, DragonFly, FreeBSD, GCC, clang, PCC, HP cc, PGI, SunPro C, _FORTIFY_SOURCE, __GNUC_STDC_INLINE__, __GNUC_GNU_INLINE__

สมมติฐานเรื่อง clang ใน Android bionic

  • bionic คือ libc ของ Android และ header ของมันตั้งสมมติฐานเรื่อง clang หนักยิ่งกว่า GCC
  • header ของ bionic ใช้ส่วนขยายเฉพาะของ clang อย่าง _Nonnull, _Null_unspecified จำนวนมากเพื่อ ตรวจสอบ nullability
  • แมโครเหล่านี้ไม่ได้ยากที่จะปิดทิ้งด้วยการ #define จาก command line
  • ปัญหานี้โผล่ชัดเมื่อใช้มือถือ Android เป็นสภาพแวดล้อมพัฒนา aarch64 แบบ native ผ่าน Termux
  • _Null_unspecified ยังถูกเรียกว่า __BIONIC_COMPLICATED_NULLNESS ด้วย และนิยามที่เกี่ยวข้องอยู่ใน sys/cdefs.h ของ bionic

ทางเลือกที่คอมไพเลอร์ C ขนาดเล็กต้องเจอ

  • โค้ดที่ยึดตามมาตรฐาน ISO C อย่างเดียวนั้นพบได้น้อยในโลกจริง และโค้ดเบส C จำนวนมากพึ่งพาพฤติกรรมที่ไม่เป็นมาตรฐานกับส่วนขยายของภาษา
  • การพึ่งพาเหล่านี้ไม่ได้เกิดจากการเพิ่มความสามารถเท่านั้น แต่ยังเกิดจากการพยายามหลบ bug และช่องว่างที่ต่างกันไปของคอมไพเลอร์กับไลบรารีแต่ละชุด
  • โค้ดเบสที่ต้องการรองรับหลายสภาพแวดล้อมมักพึ่งพาการตรวจของ preprocessor และ guard ต่าง ๆ แต่วิธีนี้พังได้ง่ายและดูแลยาก
  • เมื่อสร้างคอมไพเลอร์ C อย่าง antcc ปัญหาความเข้ากันได้แบบนี้จะโผล่ซ้ำแล้วซ้ำอีก
  • หากหลายโปรเจ็กต์โอเพนซอร์สพึ่งพาส่วนขยายและพฤติกรรมที่ไม่เป็นมาตรฐานของคอมไพเลอร์ แม้ในเรื่องที่ไม่จำเป็น ภาระในการรองรับของคอมไพเลอร์ทางเลือกก็จะสูงขึ้น
  • ขณะเดียวกันก็ยากจะเรียกร้องให้นักพัฒนาทุกคนต้องทดสอบโค้ด C บนคอมไพเลอร์หลายตัว รวมถึงตัวเล็ก ๆ ที่ไม่ค่อยมีคนรู้จัก
  • ความพกพาของ C นั้นยากมากอยู่แล้วในตัวมันเอง
  • จากมุมมองของผู้เขียนคอมไพเลอร์ ทางเลือกที่เป็นไปได้มีสี่ข้อ
    • พยายามส่งแพตช์แก้ความไม่เข้ากันไปที่ upstream
    • ทำให้ตัวเองดังพอจนผู้พัฒนายอมเพิ่มการตรวจ #ifdef และการทดสอบพื้นฐานเฉพาะให้
    • จัดการที่ downstream แล้วแจก แพตช์ หรือ แพตช์แยก
    • แกล้งเป็น GCC เวอร์ชันหนึ่งแล้ว implement ส่วนขยายเหล่านั้นให้ได้
  • การส่งแพตช์ upstream ดูเหมือนเป็นศึกที่ชนะได้ยาก และแพตช์ downstream คือวิธีที่ง่ายที่สุด
  • หากต้องการรองรับหลายโค้ดเบสโดยให้ผู้ใช้และนักพัฒนาสับสนน้อยที่สุด การ เลียนแบบความเข้ากันได้กับ GCC เป็นทางเลือกที่สมจริง แต่ก็มีภาระในการ implement สูง
  • clang define __GNUC__=4, __GNUC_MINOR__=2, __GNUC_PATCHLEVEL__=1 เพื่ออ้างความเข้ากันได้กับ GCC 4.2.1
  • ทุกวันนี้ clang แทบเป็นเป้าหมายที่ต้องรองรับแยกต่างหากไปแล้ว แต่กว่าจะทำให้ Linux kernel คอมไพล์ด้วย clang ได้ ก็ต้องอาศัยความพยายามขนาดใหญ่และต้องแพตช์ทั้งสองโปรเจ็กต์

ปัญหาของแมโคร GCC และการไล่ตามให้ทัน

  • วิธีแกล้งเป็น GCC เองก็มีปัญหา
  • โค้ดเบสจำนวนมากตรวจแค่ #ifdef __GNUC__ และอาจใช้ส่วนขยายของ GCC รุ่นใหม่โดยไม่เช็กเวอร์ชัน
  • ในกรณีนี้คอมไพเลอร์ทางเลือกก็ต้องคอย ไล่ตามให้ทัน อยู่เรื่อย ๆ
  • นี่เป็นหนึ่งในเหตุผลที่ clang แม้จะรองรับส่วนขยาย GNU ใหม่กว่าที่ 4.2.1 มี แต่ก็ยังไม่เพิ่มค่าของแมโคร __GNUC__
  • พื้นหลังของเรื่องนี้อยู่ใน การอภิปรายของ LLVM เรื่องการเพิ่ม minor version ของ __GNUC__ ใน clang

ทิศทางที่ดีกว่าและสภาพปัจจุบัน

  • ในอุดมคติแล้ว แทนที่จะใช้ guard เฉพาะคอมไพเลอร์และการตรวจเวอร์ชัน ก็ควรใช้ feature test macro ให้แพร่หลายกว่านี้
  • feature test macro ที่มีประโยชน์ ได้แก่ __has_builtin, __has_feature, __has_attribute
  • แนวทางแบบแมโครมาตรฐานอย่าง __STDC_NO_VLA__ ก็ควรถูกใช้งานให้มากขึ้นเช่นกัน
  • ในโลก *NIX ปัจจุบัน ไม่ว่าชอบหรือไม่ ระบอบกึ่งสองขั้วของ GCC/clang คือสภาพพื้นฐาน
  • การพัฒนาคอมไพเลอร์ C ขนาดเล็กที่เป็นอิสระก็ยังดำเนินต่อไป

1 ความคิดเห็น

 
ความคิดเห็นจาก Lobste.rs
  • (ผู้เขียนคอมไพเลอร์ kefir) จากประสบการณ์ ปัญหา __attribute__ ใน <sys/cdefs.h> เป็นหนึ่งในเรื่องที่ปวดหัวที่สุด มันทำให้ epoll, โครงสร้าง packed ทั่วไป, constructor และการมองเห็นสัญลักษณ์พัง จนต้องใส่ เฮดเดอร์ monkey patch นี้ มาพร้อมกับ kefir
    มันไม่ใช่วิธีที่ ideal แต่คงเป็นทางที่ใช้งานได้จริงที่สุด และในทางปฏิบัติก็ช่วยให้ลบแพตช์คัสตอมส่วนใหญ่ใน external test suite ออกได้
    ปัญหาอีกรูปแบบหนึ่งคือ โค้ดทางเลือกที่มีบั๊ก บางโปรเจ็กต์พยายามตรวจจับคอมไพเลอร์แล้วปรับพฤติกรรมให้เข้ากัน แต่เพราะทดสอบกับคอมไพเลอร์ทางเลือกไม่พอ โค้ด fallback จึงเต็มไปด้วยบั๊กหรือไม่ได้รับการดูแลที่ดี จากมุมมองของผู้เขียนคอมไพเลอร์ มันน่าหงุดหงิดกว่าการ fail ทันทีว่าเป็น “คอมไพเลอร์ที่ไม่รองรับ” มาก เพราะต้องมานั่งดีบัก miscompile แปลก ๆ เอง เช่น ความกว้างของ integer typedef ระหว่างโปรแกรมกับไลบรารีที่คอมไพล์ไว้ล่วงหน้าไม่ตรงกัน

    • เรื่องคล้ายกันก็เกิดในเทอร์มินัล ถ้าไม่ตั้ง $TERM เป็น xterm-256color เพื่อแกล้งทำเป็น xterm ทุกอย่างก็พังไปหมด
      ไม่รู้จริง ๆ ว่าควรแก้อย่างไร สุดท้ายคงต้องให้โปรเจ็กต์ของเราแพร่หลายและดังพอเองมั้ง ง่ายเลย!
    • วิธีใช้เฮดเดอร์ monkey patch ดูเหมือนจะเป็น แนวทางที่ slimcc ก็ใช้ และดูเป็นการประนีประนอมที่ค่อนข้างดี
      ผมก็เหมือนจะเคยเจอ miscompile แปลก ๆ ที่เกิดจาก fallback สำหรับการตรวจจับคอมไพเลอร์ไม่ได้ถูกดูแลดีพออยู่หลายครั้ง และมันน่ารำคาญมาก
  • ผมพัฒนา cproc บน linux-musl เป็นหลัก เลยไม่รู้มาก่อนว่า glibc ปิดการใช้งาน __attribute__ บนคอมไพเลอร์อื่น ๆ ซึ่งจริง ๆ แล้วเป็นสถานการณ์ที่แย่มาก ในคอมเมนต์บอกว่าการใช้ attribute ถูกละเลยได้ไม่มีปัญหา แต่ไม่ได้คำนึงว่ามีโค้ดแอปพลิเคชันจำนวนมากที่ include sys/cdefs.h แบบอ้อม ๆ และอาจใช้ attribute ที่ห้ามละเลย
    นอกจาก packed แล้ว aligned กับ constructor ก็ถูกใช้บ่อยเหมือนกัน
    สงสัยว่ามีการรายงานเรื่องนี้ไว้ใน issue tracker ที่ไหนหรือยัง ดูเหมือนการใช้ attribute ส่วนใหญ่ใน cdefs.h จะถูกครอบด้วย __glibc_has_attribute อยู่แล้ว เลยสงสัยว่าการปิด __attribute__ แบบเหมารวมจริง ๆ แล้วได้อะไร และพอจะเอาออกได้ไหม
    อีกปัญหาคือฟีเจอร์บางอย่างที่เฮดเดอร์ของ libc ใช้ แต่คอมไพเลอร์ไม่มีวิธีบอกได้ดีว่ารองรับหรือไม่ รองรับแบบที่เปิดเผยผ่าน __has_attribute หรือ __has_builtin ไม่ได้ ตัวอย่างที่นึกออกคือ เลเบล __asm__ NetBSD ใช้มันในการเปลี่ยนชื่อสัญลักษณ์ และถ้าไม่มี __GNUC__ หรือ __PCC__ ก็จะ #error แต่อีกด้านหนึ่งก็ไม่รู้ว่าจะเสนออะไรได้นอกจากปล่อยให้ลองใช้ไปเลยแล้วค่อย fail ถ้าไม่รองรับ
    ผมก็เคยเจอปัญหาเกี่ยวกับ __builtin_va_list ด้วย บางครั้ง libc จะ define va_list เป็น void * ถ้าไม่มี __GNUC__ หรือถึงขั้นมีนิยามที่ขัดกัน ซึ่งอันนี้ก็ทดสอบด้วย __has_builtin ไม่ได้เหมือนกัน __has_builtin(__builtin_va_arg) อาจเป็นการทดสอบที่ดีพอ แต่ก็ไม่รู้ว่าจะทำให้ macOS แก้เรื่องนี้ได้อย่างไร

    • ผมลองค้นการใช้ __attribute__ แบบเร็ว ๆ ใน /usr/include/sys กับ /usr/include/bits แล้วพบว่ามีหลายจุดที่ไม่ได้ป้องกันไว้ ส่วนใหญ่เป็น __format__, __aligned__, __noreturn__ ดังนั้นพวกนี้ก็ควรถูกแก้ด้วย
      โดยรวมแล้ว glibc ดูเหมือนจะไม่ได้ให้ความสำคัญกับ ความเข้ากันได้กับคอมไพเลอร์ที่ไม่ใช่ GCC มากนัก เลยไม่แน่ใจว่าจะยอมรับแพตช์แบบนั้นหรือเปล่า ต้นปีนี้หลังอัปเกรดระบบ glibc เพิ่มการใช้ __SIZE_TYPE__ แบบไม่มีการป้องกันในเฮดเดอร์ Linux ทำให้คอมไพเลอร์ของผมคอมไพล์บางโปรเจ็กต์ไม่ได้ ผมรายงานไปแล้ว แต่ยังไม่ถูกแก้ สุดท้ายเลยต้องเพิ่มแมโคร predefined สไตล์ __X_TYPE__ เพื่อให้เข้ากับ GCC
      สำหรับปัญหาเลเบล __asm__ ผมนึกวิธีแก้ที่ดีไม่ค่อยออก แต่ถ้าการเปลี่ยนชื่อด้วย asm จำเป็นต่อการทำงาน 100% ตั้งแต่แรก บางทีการปล่อยให้ลองใช้แล้ว fail ไปเลยอาจดีกว่าการเช็กคอมไพเลอร์
      __builtin_va_list นี่ค่อนข้างร้ายแรง ผมเคยคาดว่า __has_builtin(__builtin_va_list) น่าจะใช้ได้ แต่ดูเหมือนจริง ๆ แล้วไม่เป็นแบบนั้น