- คุณสมบัติ constant-time ที่สำคัญในโค้ดเข้ารหัสอาจถูกทำลายได้เพียงจากการปรับแต่งของคอมไพเลอร์ จึงมีการทดลองใส่แพตช์เตือนภายใน LLVM เพื่อค้นหารูปแบบที่เสี่ยง
- “การ optimize” ของคอมไพเลอร์อาจทำให้บางส่วนของ benchmark เร็วขึ้นได้ แต่ในเส้นทางหลักจริง ๆ มักพึ่งพา intrinsics และ assembly และต้นทุนของบั๊กที่เกิดจากการ optimize ก็สะสมแยกต่างหาก
- ในเดือนมิถุนายน 2024 Antoon Purnal ยืนยันว่าโค้ดอ้างอิงของ Kyber เมื่อใช้ Clang 15 ขึ้นไปกับตัวเลือก optimize บางอย่าง อาจถูกแปลงเป็น การ branch ตามค่าลับ และเปิดทางให้โจมตีผ่าน timing ได้
- TIMECOP 2 ตรวจผลลัพธ์ที่คอมไพล์ซึ่งประกาศว่าเป็น constant-time ภายใน SUPERCOP แต่มีข้อจำกัดทั้งจากคำสั่งที่ Valgrind รองรับและ data flow ที่ปรากฏในการรันทดสอบจริง
- แนวทางรับมือในงานจริงคือใช้ฟังก์ชันอย่าง
crypto_{int,uint}{8,16,32,64}.h เพื่อไม่ให้คอมไพเลอร์มองผลลัพธ์ 1 บิตเป็น bool หรือย้ายไปใช้ assembly ที่ผ่านการตรวจสอบ ภาษาเน้นความปลอดภัย หรือคอมไพเลอร์เฉพาะทาง
ช่องว่างความรับผิดชอบที่เกิดจาก “การ optimize” ของคอมไพเลอร์
- ในประวัติการเปลี่ยนแปลงของ LLVM และ GCC รุ่นใหม่ ๆ มีคำว่า “optimization”, การทดสอบ “optimization”, การแก้ไขการทดสอบ และการแก้บั๊ก “optimization” ปรากฏอยู่ต่อเนื่อง
- แม้โค้ดที่เคยทำงานได้ดีก่อนคอมไพล์จะเปลี่ยนพฤติกรรมหลังคอมไพเลอร์เปลี่ยนแปลง ในหลายกรณีความรับผิดชอบกลับถูกโยนไปให้โปรแกรมเมอร์ที่ไปเหยียบ “undefined behavior”
- “language standards” เหล่านี้ถูกสร้างโดยผู้เขียนคอมไพเลอร์ ส่งผลให้โครงสร้างความรับผิดชอบกลายเป็นว่าโค้ดของโปรแกรมเมอร์หลายล้านคนต้องแบกรับภาระมากกว่าการเปลี่ยนแปลงของกลุ่มผู้เขียนคอมไพเลอร์ขนาดเล็ก
- ในตัวอย่างโค้ดเข้ารหัส บน benchmark ของ CPU หลายรุ่น implementation
avx2 ของ kyber768 เร็วกว่าโค้ดพกพาที่คอมไพล์ด้วยคอมไพเลอร์แบบ “optimize” ประมาณ 4 เท่า
ข้อจำกัดของการวัดประสิทธิภาพการ optimize
- ในปี 2000 Todd A. Proebsting เขียนไว้ใน Proebsting's Law ว่า “ความก้าวหน้าของคอมไพเลอร์ทำให้พลังประมวลผลเพิ่มเป็นสองเท่าทุก 18 ปี” และสรุปว่าบทบาทของ compiler optimization เป็นเรื่องรอบข้าง
- ในปี 2022 Arseny Kapoulkine สรุปจาก benchmark ว่า LLVM 11 ใช้เวลา compile แบบ optimize นานกว่า LLVM 2.7 2 เท่า และโค้ดที่รันได้โดยทั่วไปเร็วขึ้น 10~20%
- ทั้งสองข้อถกเถียงต่างพลาดการวัดประสิทธิภาพที่ผู้ใช้จริงสัมผัสได้
- hotspot ที่ประสิทธิภาพกระจุกตัวอยู่มักเต็มไปด้วย intrinsics และ assembly
- FFmpeg มี assembly 160,000 บรรทัด เมื่อนับจากไฟล์
.asm และ .S
- เมื่อคอมพิวเตอร์และเครือข่ายประมวลผลข้อมูลมากขึ้น เวลา CPU จริงก็ยิ่งไปอยู่กับ hotspot เหล่านี้มากขึ้น
- ต้นทุนด้านความปลอดภัยก็ขยายขึ้นแยกต่างหากในการถกเถียงเรื่อง optimization
- Deloitte รายงานว่าในปี 2023 งบประมาณด้านความปลอดภัย IT อยู่ที่ 0.5% ของรายได้บริษัท
- เมื่อนำไปเทียบกับตัวเลขที่ว่ารายได้รวมของบริษัททั่วโลกในปี 2022 มากกว่า 48 ล้านล้านดอลลาร์ ขนาดรวมอาจอยู่ในระดับหลายแสนล้านดอลลาร์
- อย่างไรก็ตาม มีข้อสังเกตว่า 0.5% ของ Deloitte อาจเป็นค่าเฉลี่ยอย่างง่ายรายบริษัท และไม่ใช่ทุกบริษัทที่ตอบแบบสำรวจ
Timing leakage และกรณีของ Kyber
- ปัญหาความปลอดภัยที่คอมไพเลอร์แบบ “optimize” สร้างขึ้นไม่ได้มีแค่บั๊กแบบดั้งเดิม แต่รวมถึง timing leakage ที่ข้อมูลลับรั่วผ่านเวลาในการทำงานด้วย
- บทความ EuroS&P 2018 ของ Laurent Simon, David Chisnall และ Ross Anderson เตือนว่าการอัปเกรดคอมไพเลอร์อาจเปิด timing channel ในโค้ดที่เคยปลอดภัยมาก่อนโดยไม่แจ้งล่วงหน้า
- ตัวอย่างที่เน้นในบทความปี 2018 คือโค้ดที่ใช้
bool เลือกระหว่างสองค่า และ bool ทำให้คอมไพเลอร์สร้าง conditional jump
- ใน implementation ด้านการเข้ารหัส มีแนวปฏิบัติในการหลีกเลี่ยงสิ่งนี้ด้วยการเอา
bool ออกจากโค้ดสำคัญ และสร้างฟังก์ชันเปรียบเทียบแบบ constant-time แยกต่างหาก
- มีการอ้างว่า OpenSSL ประกาศ 37 ฟังก์ชัน เพื่อการนี้
- กรณี
curve25519-donna กับ MSVC 2015 ในปี 2015 ถูกสรุปในบทความว่าเป็นความเข้าใจผิด
- จริง ๆ แล้วเมื่อคอมไพล์สำหรับ x86 แบบ 32 บิต การทำงาน
int64 ถูกแปลงเป็นการเรียกไลบรารี int64 แบบ 32 บิตของ Microsoft ชื่อ llmul.asm
- timing leakage เกิดจาก branch ที่ขึ้นกับข้อมูลใน
llmul.asm และบทความมองว่าไลบรารีนี้ก็ควรถูกนับรวมอยู่ในแนวคิดซอร์สโค้ดที่สมเหตุสมผลด้วย
- ในเดือนมิถุนายน 2024 Antoon Purnal ยืนยันว่าโค้ดอ้างอิงของ Kyber เมื่อใช้ Clang 15 ขึ้นไปกับตัวเลือก optimize บางอย่าง อาจเปิดให้โจมตีผ่าน timing ได้
- รูปแบบปัญหาคือ
(-((x>>j)&1))&y ซึ่งเป็นการคำนวณที่ให้ y หากบิตที่ j ของ x ถูกตั้งค่าไว้ และให้ 0 หากไม่ใช่
- Clang ใช้คำสั่ง bit test เพื่อแปลงบิตนั้นเป็น
bool แล้วสร้าง conditional branch บนพื้นฐานของ bool นั้น
- ภายใน LLVM ฟังก์ชัน
combineShiftAnd1ToBitTest ใน lib/CodeGen/SelectionDAG/DAGCombiner.cpp จัดการ “optimization” นี้
- ฟังก์ชันนี้ถูก เพิ่ม โดย Sanjay Patel ในเดือนกันยายน 2019 และหลังจากนั้นมีหลายคนแก้ไข
- GCC ก็มีกรณีล้ำเส้นคล้ายกัน
- แพตช์ GCC ของ ARM ในเดือนพฤศจิกายน 2021 แปลง
(-x)>>31 เป็น -(x>0)
- ในเดือนเมษายน 2024 มีการเตือนเกี่ยวกับเรื่องนี้
TIMECOP และการตรวจ constant-time
- TIMECOP 2 ฝังอยู่ในเฟรมเวิร์กทดสอบการเข้ารหัส SUPERCOP และตรวจอัตโนมัติหา conditional branch ที่สืบจากค่าลับ ในโค้ดที่คอมไพล์แล้วและประกาศว่าเป็น constant-time
- เป้าหมายการตรวจไม่ได้มีแค่ conditional branch แต่รวมถึง array index ที่สืบจากค่าลับด้วย
- TIMECOP 1 เป็นเครื่องมือที่ Moritz Neikes สร้างโดยแก้ SUPERCOP และทำให้แนวทาง ctgrind ของ Adam Langley เป็นอัตโนมัติ
- TIMECOP 2 ขยายจากวิธีเดิมหลายอย่าง
- ทำเครื่องหมาย output ของ RNG เป็น ค่าลับ โดยอัตโนมัติ
- รองรับ “declassification”
- รองรับการระบุ “public inputs”
- รันบนหลายคอร์
- TIMECOP มีข้อจำกัดที่ชัดเจน
- จัดการได้เฉพาะคำสั่งที่ Valgrind รองรับ จึงหยุดเมื่อเจอคำสั่งอย่าง AMD XOP
- ตรวจได้เฉพาะ data flow ที่มองเห็นในการรันทดสอบจริง
- งานเกี่ยวกับเครื่องมือตรวจพฤติกรรม constant-time ยังดำเนินต่อไป และรายชื่อเครื่องมือที่เกี่ยวข้องอยู่ที่ ct-tools
- การตรวจแบบเดียวกับ TIMECOP ถูกใส่เข้าไปในชุดทดสอบของ libmceliece แล้ว และอาจแพร่ไปยังไลบรารีอื่นได้
วิธีเขียนใหม่ให้เป็น constant-time
- หลังจากพบชิ้นโค้ดแบบ variable-time แล้ว จำเป็นต้องมีวิธีเขียนใหม่ให้เป็น constant-time โดยไม่มีบั๊ก
- งานนำเสนอเดือนกรกฎาคม 2024 แนะนำฟังก์ชัน constant-time บางส่วนที่ libmceliece และ SUPERCOP มีให้
- ชื่อไฟล์คือ
crypto_{int,uint}{8,16,32,64}.h
- ไฟล์เหล่านี้สามารถคัดลอกไปใช้ในโปรเจกต์อื่นได้
- ฟังก์ชันตัวอย่าง
crypto_uint32_bitmod_mask(x,j) ให้ผลเทียบเท่า -((x>>(j&31))&1) แต่ทำให้คอมไพเลอร์มองไม่เห็น ผลลัพธ์ 1 บิต
- ตัวอย่างที่ซับซ้อนกว่านั้นคือ
crypto_uint32_max(x,y)
- บทความปี 2018 กล่าวถึง tweak ที่เพิ่มฟังก์ชัน constant-time
__builtin_ct_choose(bool cond, x, y) ให้ Clang/LLVM
- บทความนั้นเสนอผิดว่าฟังก์ชันเดียวนี้ก็เพียงพอแล้ว
- แม้ฟังก์ชันนี้อาจเข้าไปอยู่ในคอมไพเลอร์สักวันหนึ่ง แต่คงใช้เวลานานกว่าที่โปรเจกต์ต่าง ๆ จะพึ่งพาได้
- มีการประเมินว่าวิธี implementation นี้ดูเปราะบางกว่า
crypto_{int,uint}{8,16,32,64}.h
วิธีหลีกเลี่ยงปัญหาล่วงหน้า
- หากการทดสอบก่อนเผยแพร่ไลบรารีที่คอมไพล์แล้วจับ timing leakage ที่คอมไพเลอร์นำเข้ามาได้ ระหว่างที่เขียนโค้ดใหม่ก็ยังสามารถใช้คอมไพเลอร์เวอร์ชันเดิมสำหรับการเผยแพร่ได้
- วิธีนี้เป็นมาตรการชั่วคราวที่ยังคงทำให้ผู้ใช้ปลอดภัย
- วิธีแก้อย่างหนึ่งคือเผยแพร่ไลบรารีเป็น assembly
- อย่างไรก็ตาม ในซอฟต์แวร์เข้ารหัสที่ยังไปไม่ถึงระดับนี้ ปัญหาความยากในการ audit assembly ยังคงอยู่
- สำหรับโค้ดที่เขียนด้วย C, C++ ฯลฯ ยังมีการสำรวจวิธีใส่ “วัคซีน” ป้องกัน timing leakage อย่างรวดเร็วด้วย
การทดลองแพตช์ clang-vs-clang
- จุดร่วมของ
x&1 และ x>>31 คือผลลัพธ์ที่เป็นไปได้มีแค่สองค่า
x&1 คือ 0 หรือ 1
x>>31 ของ uint32 คือ 0 หรือ 1
x>>31 ของ int32 คือ 0 หรือ -1
- รูปแบบเหล่านี้ทำให้ผู้เขียน “optimization” ของคอมไพเลอร์นำผลลัพธ์ 1 บิตไปใส่เป็น
bool ได้ง่าย
- มีคำแนะนำให้คอมไพล์ด้วย
-fwrapv เสมอ เพื่อให้ GCC และ Clang สมมติว่าใช้ two's-complement arithmetic
- แม้การสแกนซอร์สเพื่อหา
&1, 1&, >>31 ฯลฯ แบบง่าย ๆ จะพบตัวอย่างจำนวนมาก แต่บทความสแกนด้วยวิธีอื่นโดยใส่แพตช์เข้าไปใน “optimizer” ของ LLVM โดยตรง
- แพตช์ เริ่มจาก LLVM commit
68df06a0b2998765cb0a41353fcf0919bbf57ddb เพื่อหา &1 และ >>31 แล้วออกคำเตือนต่อไปนี้
please take this away before clang does something bad
- ตัวอย่างคำสั่งคอมไพล์คือ
clang -Rpass-analysis=clang-vs-clang -O -c x.c
- ฟังก์ชันทดสอบมีดังนี้
int sra31(int x)
{
x >>= 31;
return x;
}
- การที่คำเตือนเดียวกันซ้ำหลายครั้งไม่น่าแปลกใจ
- คอมไพเลอร์จะพยายามใช้ “optimization” ต่อไปจนกว่าจะไม่คืบหน้าแล้ว
- output ของ
clang-vs-clang แยกความต่างระหว่าง signed และ unsigned ใน shift
- ความต่างนี้สำคัญต่อการเขียนใหม่ด้วยมือหรืออัตโนมัติโดยใช้
crypto_{int,uint}{8,16,32,64}.h
- หนึ่งในวิธีทำ source transformation อัตโนมัติคือ
clang-tidy
- โค้ดที่ถูกตัดออกด้วย
#ifdef หรือถูกลบก่อนถึงขั้น “optimization” นี้ จะไม่ทำให้เกิดคำเตือน clang-vs-clang
ผลการรัน SUPERCOP และกรณีที่พบ
- รัน SUPERCOP 20240716 บน dual EPYC 7742 ด้วย
./data-do-biglittle
- ปิดการ overclock
- ปรับรายการคอมไพเลอร์ของ SUPERCOP ให้ใช้
clang-vs-clang โดยเพิ่ม -Rpass-analysis=clang-vs-clang ในบรรทัด clang ของ okcompilers/{c,cpp}
- ผลลัพธ์พร้อมหลังจาก 3 ชั่วโมง
- output ของ Clang รวม 675,752 บรรทัด
- ขนาดต้นฉบับ 210,786,494 ไบต์
- ผลลัพธ์ที่บีบอัดคือ
20240803-fromclang.txt.gz ขนาด 3,595,199 ไบต์
- output มี noise มากจาก source branch ที่อิง public data ซึ่งสร้าง
&1 ภายใน Clang
- ตัวอย่างที่ชัดเจนว่าควรแก้ล่วงหน้ามีดังนี้
a0 += (a0>>15)&106;
- ตัวอย่างที่ต้องใช้ความพยายามในการ parse C หากจะหาโดยสแกนซอร์สแบบง่ายมีดังนี้
- macro
ONE8 ถูกนิยามเป็น ((uint8_t)1)
*pk2^=(((* pk_cp)>>ir)&ONE8)<<jr;
- ตัวอย่างที่หาได้ยากกว่านั้นมาจาก macro บน AVX2 intrinsic
signmask_x16(x) ถูกนิยามเป็น _mm256_srai_epi16((x),15)
- นี่คือการ shift ขวา 15 บิตของชิ้น signed 16 บิตแต่ละชิ้นในเวกเตอร์ 256 บิต
mask = signmask_x16(sub_x16(x,const_x16((q+1)/2)));
- กรณี AVX2 นี้ยังไม่ใช่ลำดับความสำคัญสูง
- เพื่อให้ vector operation ถูกเปลี่ยนเป็น conditional branch ต้องคอมไพล์เป็น AVX-512 และคอมไพเลอร์ตัดสินใจแปลก ๆ ในการแปลง
bool แบบ vectorized เป็น conditional branch ของ bool แบบ serial
- TIMECOP ใช้ Valgrind และ Valgrind ไม่รองรับ AVX-512
- ขณะนี้ยังไม่แนะนำให้คอมไพล์เป็น AVX-512
int128 และทิศทางรับมือที่กว้างขึ้น
- สิ่งที่ค้นพบซึ่งน่าสนใจที่สุดคือกรณีที่การ shift ขวา 64 บิตของ
int128 ทำให้เกิดคำเตือน >>
- implementation ของ
int128 อาจใช้การ shift ขวา 63 บิตภายใน เพื่อหาสัญญาณของ word 64 บิตด้านบน
- หาก Clang เพิ่มการรองรับแบบ GCC ที่เปลี่ยนการ shift ขวา 63 บิตให้เป็น
bool แล้วเปลี่ยนเป็น conditional branch ต่อ โค้ด int128 จำนวนมากอาจกลายเป็น variable-time ทันที
- ในกรณีนี้จะคล้ายกับสถานการณ์ที่ชื่อบทความปี 2015 เคยกล่าวอ้าง แต่ครั้งนี้เกิดขึ้นได้จริงแม้ในซอร์สจะไม่มี
bool
- วิธีป้องกันที่ง่ายที่สุดในระดับซอร์สคือหลีกเลี่ยง implementation
int128 เดิมของคอมไพเลอร์ และใช้ฟังก์ชัน crypto_int128
crypto_int128 ต่างจาก int128 ของ GCC และ Clang ตรงที่สามารถทำงานได้แม้บนแพลตฟอร์ม 32 บิตขนาดเล็ก
- แนวทางเพิ่มชนิดข้อมูลลับให้ GCC และ Clang ดูดี แต่จากโครงสร้างของคอมไพเลอร์ทั้งสองยังไม่เห็นวิธีทำให้แข็งแรงได้ชัดเจน
- ความคาดหวังจึงไปอยู่กับคอมไพเลอร์ที่ออกแบบมาเพื่อความปลอดภัยตั้งแต่ต้นมากกว่า
- คอมไพเลอร์เน้นความปลอดภัยที่ต้องใช้ภาษา input ใหม่ ได้แก่ FaCT และ Jasmin ที่กำลังพัฒนาอย่างแข็งขัน
- แม้มีความกังวลเรื่องเวลาที่ต้องใช้เขียนโค้ดใหม่ แต่เมื่อดูวิธีที่คอมไพเลอร์ปัจจุบันจัดการโค้ดเดิม ก็จำเป็นต้องมีมาตรการในรูปแบบใดรูปแบบหนึ่ง
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
การเรียกสิ่งที่เป็นบั๊กของคอมไพเลอร์เพราะโค้ดที่มี พฤติกรรมที่ไม่ได้กำหนดไว้ ไม่ทำงานตามที่ต้องการนั้นไม่ถูกต้อง
คล้ายกับการรัน
ddด้วยอาร์กิวเมนต์ที่ผิดจนข้อมูลหาย แล้วบอกว่าddมีบั๊กจะมองว่าเป็นบั๊กของซอร์สโค้ดหรือคอมไพเลอร์ก็คงยากกว่า ควรมองว่ามาตรฐาน C ระบุรายละเอียดไว้น้อยเกินไปตามเกณฑ์ของผู้เขียน จนทำให้เกิดบั๊กด้านความปลอดภัยบนบาง target มากกว่า
สุดท้ายแล้วผู้เขียนมาตรฐาน C ก็ไม่สามารถกำหนดไปถึงพฤติกรรมของฮาร์ดแวร์ได้ ทำได้เพียงกำหนด semantics ของภาษา ดังนั้นฝั่งการเข้ารหัสลับก็ต้องลำบากกับบั๊กที่เกิดจากฮาร์ดแวร์อย่างเลี่ยงไม่ได้
หนึ่งในข้อดีของ Rust คือจำกัดพฤติกรรมที่อาจไม่ได้กำหนดไว้ให้อยู่ภายในบล็อก
unsafeถึงอย่างนั้น แม้ Rust จะกำหนดพฤติกรรมหลายอย่างที่ใน C ถือว่าไม่ได้กำหนดไว้แล้ว แต่เมื่อเข้าไปในโค้ดunsafeก็ยังเผลอเหยียบพฤติกรรมที่ไม่ได้กำหนดไว้อันละเอียดอ่อนได้ง่ายมากโมเดลที่สามซึ่งล้มเหลวแบบเงียบ ๆ แล้วสร้างโค้ดที่คาดเดาไม่ได้ มีประโยชน์เฉพาะกับผู้เขียนคอมไพเลอร์เท่านั้น การหลบอยู่หลังสเปกไม่ได้ให้ประโยชน์แก่ผู้ใช้จริง
การปรับแต่งนั้นทำให้โค้ดที่เคยทำงานได้ดีพัง ผู้เขียนคอมไพเลอร์อาจให้ความสำคัญกับความเข้ากันได้ย้อนหลังได้ แต่ก็ไม่ได้ทำ
แถมการปรับแต่งแบบนี้ยังไม่ได้ช่วยปรับปรุงประสิทธิภาพของโค้ดจริงอย่างมีนัยสำคัญด้วย ดังนั้นจึงต้องโต้แย้งให้ได้ว่าการแลกเปลี่ยนที่ทำให้โค้ดพังนี้คุ้มค่า
ผมชอบ Bernstein แต่บางครั้งเขาก็จับทิศผิดแล้วกลายเป็นสุดโต่ง ซึ่งบทความนี้เป็นตัวอย่างที่ดี ตอนท้ายเขาเองก็ยอมรับอยู่ครึ่งหนึ่ง
ส่วนใหญ่ของบทความเป็นประเด็นรองว่าอานิสงส์จากการปรับแต่งดีแค่ไหน และต่อให้มีข้อมูล ก็ยังเป็นการตัดสินที่ขึ้นกับ use case
ความไม่พอใจหลักคือคอมไพเลอร์ C ไม่คำนึงถึง semantics ที่ไม่สามารถแสดงด้วยภาษาได้ ซึ่งก็ไม่ใช่เรื่องน่าประหลาดใจ
ตอนท้ายเขาบอกว่า “ให้ใช้ภาษาที่สามารถแสดง semantics ที่ต้องการได้” ซึ่งทั้งบทความอาจแทนได้ด้วยประโยคเดียวนี้
ในจำนวนนี้หลายอย่างมีเหตุผลที่น่าสงสัย และทำให้การเขียนโปรแกรมที่ถูกต้องยากขึ้น
C และ C++ ไม่เหมาะกับการเขียนอัลกอริทึมที่รับประกัน constant-time
ในมาตรฐานแทบไม่มีแนวคิดเรื่อง real-time และคอมไพเลอร์ก็ไม่ได้ให้การรับประกันเพิ่มเติมผ่าน extension ด้วย
แต่การโยนความผิดให้ผู้พัฒนาคอมไพเลอร์ในเรื่องนี้เป็นทิศทางที่ผิด
บน CPU ของ Intel ไม่ว่าจะเป็น
clangหรืออะไรก็ตาม ไม่สามารถสร้างโค้ดที่ถูกต้องใน user mode ได้ เพราะตั้งแต่แรกไม่มีโค้ดที่ถูกต้องอยู่แล้วhttps://www.intel.com/content/www/us/en/developer/articles/t...
ถ้าดู
DOITMในเอกสาร จะเห็นว่าการให้ไลบรารีเข้ารหัสลับใน user space ตั้งบิตที่จำเป็นนั้นเป็นไปไม่ได้เลยเมื่อเปิดแล้วก็ทำงานใน user space ได้ดี ดังนั้น เช่น ทำให้เป็นแฟล็กรายโปรเซสที่เปิดใช้งานผ่าน system call
prctlแล้วให้ปรับMSRตอน scheduler สลับงาน ก็เป็นไปได้แค่เห็นประโยคที่ว่า “เมื่อใดก็ตามที่ทำได้ ผู้เขียนคอมไพเลอร์ปฏิเสธที่จะรับผิดชอบต่อบั๊กที่ตนเองสร้างขึ้น” ก็แทบไม่ค่อยเห็นกรณีที่ความเป็นมืออาชีพของบทความบล็อกพังลงเร็วขนาดนี้
ถ้าตามลิงก์ไปดู ก็เป็นเพียงเนื้อหา C พื้นฐานมาก ๆ ว่า undefined behavior ไม่ได้หมายความว่าจะสร้าง “ค่าใด ๆ ก็ได้”
แม้จะมี undefined behavior ซอร์สโค้ดก็เป็นบั๊กได้ แต่โปรแกรมที่สร้างออกมายังถูกต้องอยู่บ่อยครั้ง ต่อมาเมื่อผู้เขียนคอมไพเลอร์ใส่ optimization ใหม่ แล้วใช้ undefined behavior นั้นเป็นเหตุให้สร้างโปรแกรมที่มีบั๊กออกมา การโต้เถียงเรื่องความรับผิดชอบก็เริ่มขึ้น
ส่วนที่ไม่อยากยอมรับคือความรับผิดชอบต่อผู้ใช้นั้นแบ่งกันอยู่ทุกฝ่าย ถ้าแอป CRUD ทำให้แบตเตอรี่ไหม้เพียงเพราะ dereference
NULLคนปกติคงไม่โทษแค่ว่าผู้เขียนแอปลืมตรวจNULLเท่านั้นคอมไพเลอร์ ระบบปฏิบัติการ และผู้ผลิตฮาร์ดแวร์ก็ต้องรับผิดชอบต่อผลิตภัณฑ์ที่ออกแบบอย่างไร้ความรับผิดชอบด้วย ไม่ใช่จบแค่คำว่า “undefined behavior” ตามมาตรฐาน ISO สมาชิกทุกส่วนในซัพพลายเชนร่วมกันมีหน้าที่คาดการณ์ว่าผลิตภัณฑ์อาจถูกใช้งานผิดวิธีอย่างไร และจัดการอย่างสมเหตุสมผล
undefined behavior มีอยู่เพื่อมอบคุณค่า สามารถสร้างภาษาโดยไม่มีสิ่งนี้ก็ได้ แต่เหตุผลที่ยังจงใจมีอยู่คือเพื่อ portability และความยืดหยุ่นที่ให้แก่ผู้เขียนคอมไพเลอร์
แก่นของบทความคือความยืดหยุ่นนั้นคุ้มค่าหรือไม่ เมื่อเทียบกับความยากในการเขียนโปรแกรมโดยไม่มี undefined behavior
ผู้เขียนบทความมองว่าเงินที่เสียไปกับบั๊กดูจะมากกว่าเงินที่ประหยัดได้จาก bytecode ที่เร็วขึ้น และเพราะผู้เขียนคอมไพเลอร์มีอิทธิพลมากในการกำหนดสิ่งที่จะใส่ในมาตรฐานภาษา จึงมีแรงจูงใจน้อยที่จะปรับแก้เรื่องนี้
สำหรับข้อมูลอ้างอิง ใน
clangมีแอตทริบิวต์clang::optnoneที่ปิดการปรับแต่งประสิทธิภาพทั้งหมดเป็นรายฟังก์ชัน และใน GCC มีแอตทริบิวต์gnu::optimizeที่ยอดเยี่ยม ซึ่งสามารถเพิ่มหรือลบการปรับแต่งประสิทธิภาพตามชื่อ หรือกำหนดระดับการปรับแต่งประสิทธิภาพได้โดยไม่ขึ้นกับแฟล็กของคอมไพเลอร์gnu::optimize(0)คล้ายกับแฟล็กของclangนั้น ในclangยังมีclang::no_builtinsซึ่งปิดการปรับแต่งประสิทธิภาพของmemcpyและmemsetโดยเฉพาะด้วยoptimizeควรใช้เพื่อ วัตถุประสงค์ในการดีบัก เท่านั้น และไม่เหมาะกับโค้ดโปรดักชัน”https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attribute...
ค่อนข้างเห็นด้วยกับเป้าหมายที่คนสายคริปโตต้องการ เช่น การประเมินผลแบบเวลาคงที่ และการซ่อนค่าลับ
แต่คอมไพเลอร์เอนกประสงค์ไม่ได้คิดถึงเรื่องแบบนั้นเป็นส่วนใหญ่ จึงดูยากที่จะเป็นอะไรได้มากกว่าแฮ็กที่พอทำงานได้โดยทั่วไป
ถ้าจะทำจริงจัง ก็น่าจะต้องมีคอมไพเลอร์เฉพาะของตัวเอง หรือไม่ก็ต้องไปทาง assembly ต่อไป
สักวันหนึ่งเราอาจมองยุคนี้ว่าเป็น “วันเก่า ๆ อันเลวร้าย” และหลุดพ้นจาก C ไปใช้ ภาษาที่มีพฤติกรรมไม่ถูกกำหนดน้อยกว่ามาก
ใน C เราสามารถเขียนนิพจน์ที่คอมไพล์ผ่านได้ง่ายเกินไป ทั้งที่คอมไพเลอร์ไม่มีทางรู้ได้เลยว่าตั้งใจให้ทำอะไร
ตัวอย่างเช่น ใน Python เราเขียนโค้ดอย่าง
result = [something(value) for value in set_object]ได้ อ็อบเจ็กต์setไม่มีลำดับ ดังนั้นจึงชัดเจนว่าลำดับการประมวลผลรายการและลำดับผลลัพธ์ไม่สำคัญ และสิ่งนี้เปิดทางให้ปรับแต่งประสิทธิภาพได้มากในระดับภาษา โดยที่คอมไพเลอร์ไม่ต้องเดาเจตนาของผู้เขียนโค้ดลักษณะคล้ายกันในภาษาอื่นที่มีข้อมูลแบบไม่เปลี่ยนรูปไปได้อีกขั้น เพราะ
something(value1)ไม่สามารถส่งผลต่อsomething(value2)ได้ จึงสามารถรันขนานกันได้ ไม่ว่าจะเป็นเธรดหรือโปรเซสการปรับแต่งประสิทธิภาพของคอมไพเลอร์ C ส่วนใหญ่คือการดูแพตเทิร์นโค้ด แล้วหาวิธีทำให้สิ่งที่ผู้เขียนน่าจะตั้งใจทำเร็วขึ้น C มีความสามารถในการสื่อเจตนาน้อยกว่าภาษาสมัยใหม่ จึงมีอิสระให้เดาได้ แต่ถ้าจะให้ได้ประสิทธิภาพที่พอใช้ได้ ก็ต้องทำการอนุมานแบบนั้น
ถึงอย่างนั้น มันอาจเป็นพรที่แฝงมาในคราบปัญหา เหมือนกรณีที่กล้องโทรทรรศน์ฮับเบิลต้องการ “แว่นตา” ก็ได้ เราสร้างเทคนิคชั้นยอดขึ้นมาเพื่อเอาชนะข้อจำกัด และหลังจากแก้ปัญหาแล้ว เทคนิคเหล่านั้นก็ให้ประสิทธิภาพสูงกว่าที่คาดไว้เดิมมาก หากนำการปรับแต่งประสิทธิภาพของคอมไพเลอร์ C ไปใช้กับภาษาที่ไม่ใช่ C มันอาจทำงานได้ราวกับพลังพิเศษ
โดยพื้นฐานแล้วคล้ายกับพฤติกรรมไม่ถูกกำหนด เพียงแต่ไม่ได้ปรากฏเป็นปัญหาความปลอดภัยทันที แต่อาจเป็นผลลัพธ์ที่ผิด แน่นอนว่าผลลัพธ์ที่ผิดอาจนำไปสู่ปัญหาความปลอดภัยในภายหลังได้
ต่างจากพฤติกรรมไม่ถูกกำหนด การสร้าง “sanitizer” ที่ตรวจว่าโค้ดทำงานได้กับทุกลำดับที่เป็นไปได้ของ
setนั้นแทบเป็นไปไม่ได้ในทางปฏิบัติgccและclangมี hint ระดับต่ำจำนวนมากที่มักไม่มีในภาษาอื่น เช่น__builtin_expect/__builtin_unpredictable,__builtin_unreachable/__builtin_assume,#pragma clang loop vectorize(assume_safety)/#pragma GCC ivdep, รวมถึง pragma สำหรับปิดการ unroll ลูปหรือ vectorization หรือเลือกค่าบางค่าสิ่งที่ขาดหายไปมากที่สุดน่าจะเป็น optimization barrier ที่ระบุชัดเจนเพื่อกันไม่ให้คอมไพเลอร์อนุมานจากที่มาของค่าได้
__asm__ทำได้ในระดับหนึ่ง แต่มีผลข้างเคียงที่ไม่ต้องการ และต้องใช้ชื่อชนิดรีจิสเตอร์เฉพาะแพลตฟอร์มศักยภาพของการปรับแต่งประสิทธิภาพเชิงเจตนาระดับสูงก็มีอยู่ชัดเจนเช่นกัน เช่น จองพื้นที่ array list ก่อน
pushnครั้งในลูป, รวมการค้นหา hashmap แบบcontains→get→putด้วยคีย์เดียวกัน, หรืออนุมานพฤติกรรมการจัดสรรแบบ global ในขอบเขต local เพื่อกำจัดอ็อบเจ็กต์และการจัดสรรC อยู่ใกล้กับฮาร์ดแวร์จริงมากพอที่โปรแกรมเมอร์จะบอกได้ตรง ๆ ว่าจะทำอะไร ดังนั้นคอมไพเลอร์จึงไม่จำเป็นต้องเดาเจตนาของโปรแกรมเมอร์
ภาษาที่นำ optimization ด้านหน่วยความจำแบบนั้นไปใช้ส่วนใหญ่เป็นตระกูล Java และเหตุผลที่มีแรงจูงใจให้ทำ optimization แบบนั้นก็เพราะเดิมทีมีการทำให้แย่ลงล่วงหน้าอย่างรุนแรงอยู่แล้ว แต่แม้ optimization นั้นก็ยังชดเชยความเสียหายไม่ได้
ประเด็นคือ C ก็ไม่ได้ดีนัก แต่ฝั่งอื่นแย่กว่า
ถ้าไม่ชอบ semantics ของ C ก็ไม่ต้องไปโกรธวิศวกรคอมไพเลอร์ แค่ใช้ ภาษาโปรแกรมมิงอื่น ก็พอ
qhasmของเขาเองหรือเปล่า แม้แต่ Zig ก็เถอะ ดังนั้นความเห็นครั้งนี้จากเขาจึงไม่ได้ทำให้แปลกใจนักเป็นบทความที่สดใหม่ เพราะนำเสนอมุมมองที่ไม่ค่อยได้ยินบ่อย น่าอ่านคู่กัน: https://gavinhoward.com/2023/08/the-scourge-of-00ub/