มีการเดาผิดอย่างมาก (เล็กน้อยหรือทั้งหมด) ในความคิดเห็นเกี่ยวกับรายละเอียด / พื้นหลังสำหรับเรื่องนี้
คุณกำลังดูการใช้ C fallback ที่ปรับให้เหมาะสมที่สุดของ glibc (สำหรับอกหักที่ไม่ได้มีการดำเนินงานที่ asm ที่เขียนด้วยมือ) หรือรหัสรุ่นเก่าซึ่งยังคงอยู่ในแผนภูมิแหล่งที่มาของ glibc https://code.woboq.org/userspace/glibc/string/strlen.c.htmlเป็นโค้ดเบราว์เซอร์ที่อ้างอิงจากแผนผัง glibc git ปัจจุบัน เห็นได้ชัดว่ามันยังคงถูกใช้โดยเป้าหมาย glibc หลักบางประการรวมถึง MIPS (ขอบคุณ @zwol)
บน ISAs ยอดนิยมเช่น x86 และ ARM glibc ใช้ asm ที่เขียนด้วยมือ
ดังนั้นแรงจูงใจในการเปลี่ยนแปลงอะไรก็ตามเกี่ยวกับรหัสนี้จึงต่ำกว่าที่คุณคิด
รหัสความผิดพลาดนี้ ( https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord ) ไม่ใช่สิ่งที่ทำงานบนเซิร์ฟเวอร์ / เดสก์ท็อป / สมาร์ทโฟน / ของคุณ มันดีกว่าลูปที่ไร้เดียงสาต่อครั้ง แต่ความผิดพลาดนี้ค่อนข้างแย่เมื่อเทียบกับ asm ที่มีประสิทธิภาพสำหรับซีพียูสมัยใหม่ (โดยเฉพาะอย่างยิ่ง x86 ที่ AVX2 SIMD ช่วยให้ตรวจสอบ 32 ไบต์พร้อมคำแนะนำสองคู่ช่วยให้ 32 ถึง 64 ไบต์ต่อนาฬิกา วนรอบในลูปหลักหากข้อมูลร้อนในแคช L1d บนซีพียูสมัยใหม่ที่มีโหลดเวกเตอร์ 2 / clock และปริมาณงาน ALU เช่นสำหรับสตริงขนาดกลางที่โอเวอร์เฮดเริ่มต้นไม่ได้ทำงาน)
glibc ใช้เทคนิคการลิงก์แบบไดนามิกเพื่อแก้ไขstrlen
เป็นเวอร์ชั่นที่ดีที่สุดสำหรับ CPU ของคุณดังนั้นแม้ใน x86 จะมีรุ่น SSE2 (เวกเตอร์ 16 ไบต์, พื้นฐานสำหรับ x86-64) และรุ่น AVX2 (เวกเตอร์ 32 ไบต์)
x86 มีการถ่ายโอนข้อมูลที่มีประสิทธิภาพระหว่างเวกเตอร์และการลงทะเบียนที่ใช้งานทั่วไปซึ่งทำให้ดีสำหรับการใช้ SIMD เพื่อเพิ่มความเร็วการทำงานของสตริงที่มีความยาวโดยปริยายซึ่งขึ้นอยู่กับการควบคุมข้อมูล pcmpeqb
/ pmovmskb
ทำให้สามารถทดสอบ 16 ไบต์แยกกันในแต่ละครั้ง
glibc มีรุ่น AArch64 เช่นนั้นโดยใช้ AdvSIMDและรุ่นสำหรับ AArch64 ซีพียูที่ vector-> GP ลงทะเบียนแผงลอยไปป์ไลน์ดังนั้นจึงใช้ bithack นี้จริง แต่ใช้เลขศูนย์นำหน้าเพื่อค้นหาจำนวนไบต์ภายในรีจิสเตอร์เมื่อได้รับความนิยมและใช้ประโยชน์จากการเข้าใช้ที่ไม่เป็นศูนย์ของ AArch64 ที่มีประสิทธิภาพหลังจากตรวจสอบการข้ามหน้า
สิ่งที่เกี่ยวข้องด้วย: ทำไมรหัสนี้ 6.5x ช้าลงเมื่อเปิดใช้งานการเพิ่มประสิทธิภาพ มีรายละเอียดเพิ่มเติมเกี่ยวกับสิ่งที่เร็วและช้าใน x86 asm สำหรับstrlen
บัฟเฟอร์ขนาดใหญ่และการใช้ asm อย่างง่ายที่อาจดีสำหรับ gcc เพื่อทราบวิธีการอินไลน์ (บางรุ่น gcc แบบอินไลน์ไม่คล่องrep scasb
ซึ่งช้ามากหรือ 4-byte-at-a-time bithack เช่นนี้ดังนั้นสูตร Inline-Strlen ของ GCC จำเป็นต้องอัปเดตหรือปิดใช้งาน)
Asm ไม่มี C-style "พฤติกรรมไม่ได้กำหนด" ; มันปลอดภัยที่จะเข้าถึงไบต์ในหน่วยความจำตามที่คุณต้องการและการโหลดที่จัดเรียงซึ่งรวมถึงไบต์ที่ถูกต้องใด ๆ จะไม่เป็นความผิด การป้องกันหน่วยความจำเกิดขึ้นพร้อมกับหน้าย่อย เข้าถึงที่จัดชิดแคบกว่าที่ไม่สามารถข้ามขอบเขตหน้า การอ่านจุดจบของบัฟเฟอร์ในหน้าเดียวกันใน x86 และ x64 ปลอดภัยหรือไม่? การใช้เหตุผลเดียวกันกับรหัสเครื่องที่แฮ็ค C นี้ได้รับคอมไพเลอร์เพื่อสร้างสำหรับการใช้งานแบบไม่ใช้อินไลน์ของฟังก์ชันนี้
เมื่อคอมไพเลอร์ส่งเสียงโค้ดเพื่อเรียกใช้ฟังก์ชั่นที่ไม่รู้จักแบบอินไลน์จะต้องสมมติว่าฟังก์ชันแก้ไขตัวแปรโกลบอลใด ๆ / ทั้งหมดและหน่วยความจำใด ๆ ที่อาจมีตัวชี้ไป นั่นคือทุกอย่างยกเว้นคนในท้องถิ่นที่ไม่ได้มีการยกเว้นที่อยู่จะต้องซิงค์ในหน่วยความจำในการโทร สิ่งนี้ใช้กับฟังก์ชั่นที่เขียนด้วย asm, ชัดแจ้ง, แต่รวมถึงฟังก์ชันของไลบรารี หากคุณไม่ได้เปิดใช้งานการเพิ่มประสิทธิภาพเวลาเชื่อมโยงมันยังใช้กับหน่วยการแปลแยกต่างหาก (ไฟล์ต้นฉบับ)
ทำไมจึงมีความปลอดภัยในฐานะเป็นส่วนหนึ่งของ glibcแต่ไม่ใช่อย่างอื่น
ปัจจัยที่สำคัญที่สุดคือสิ่งนี้strlen
ไม่สามารถแทรกเข้าไปในสิ่งอื่นใดได้ มันไม่ปลอดภัยสำหรับสิ่งนั้น มันมีUB เข้มงวดในนามแฝง (อ่านchar
ข้อมูลผ่านunsigned long*
) char*
ได้รับอนุญาตให้อะไรนามแฝงอื่นแต่กลับเป็นความไม่จริง
นี่คือฟังก์ชันไลบรารีสำหรับไลบรารีที่คอมไพล์ล่วงหน้า (glibc) มันจะไม่ได้รับการอินไลน์ด้วยการเพิ่มประสิทธิภาพลิงค์เวลาเข้าไปในผู้โทร strlen
ซึ่งหมายความว่าเพียงแค่มีการรวบรวมรหัสเครื่องที่ปลอดภัยสำหรับรุ่นสแตนด์อะโลนของ ไม่จำเป็นต้องพกพา / ปลอดภัย C.
ไลบรารี GNU C จะต้องคอมไพล์ด้วย GCC เท่านั้น เห็นได้ชัดว่าไม่รองรับการรวบรวมด้วยเสียงดังกราวหรือ ICC แม้ว่าจะรองรับส่วนขยาย GNU GCC เป็นคอมไพเลอร์ล่วงหน้าที่เปลี่ยนไฟล์ต้นฉบับ C เป็นไฟล์ออบเจ็กต์ของรหัสเครื่อง ไม่ใช่ล่ามดังนั้นเว้นแต่ว่ามันจะอินไลน์ในเวลารวบรวมไบต์ในหน่วยความจำเป็นเพียงไบต์ในหน่วยความจำ เช่น UB ที่ใช้นามแฝงอย่างเข้มงวดไม่เป็นอันตรายเมื่อการเข้าถึงที่มีประเภทแตกต่างกันเกิดขึ้นในฟังก์ชั่นที่แตกต่างกันซึ่งไม่ได้รวมเข้าด้วยกัน
โปรดจำไว้ว่าstrlen
พฤติกรรมนั้นถูกกำหนดโดยมาตรฐาน ISO C ชื่อฟังก์ชั่นนั้นโดยเฉพาะนั้นเป็นส่วนหนึ่งของการนำไปใช้งาน คอมไพเลอร์เช่น GCC แม้กระทั่งการรักษาชื่อเป็นฟังก์ชั่นเว้นแต่คุณจะใช้-fno-builtin-strlen
เพื่อให้สามารถคงรวบรวมเวลาstrlen("foo")
3
คำจำกัดความในไลบรารีจะใช้เฉพาะเมื่อ gcc ตัดสินใจที่จะส่งการเรียกไปที่จริงแทนที่จะใช้การทำอินไลน์สูตรของตนเองหรือบางสิ่งบางอย่าง
เมื่อ UB ไม่สามารถมองเห็นคอมไพเลอร์ณ เวลารวบรวมคุณจะได้รับรหัสเครื่องที่มีสติ รหัสเครื่องจะต้องใช้กับเคสที่ไม่มี UB และแม้ว่าคุณจะต้องการก็ตามไม่มีวิธีสำหรับ asm ในการตรวจสอบชนิดของผู้โทรที่ใช้ในการใส่ข้อมูลลงในหน่วยความจำที่แหลม
Glibc ถูกคอมไพล์ไปยังไลบรารีแบบสแตติกหรือไดนามิกแบบสแตนด์อะโลนที่ไม่สามารถอินไลน์ด้วยการปรับให้เหมาะสมเวลาลิงก์ สคริปต์การสร้างของ glibc ไม่สร้างไลบรารี่แบบคงที่ "อ้วน" ที่มีรหัสเครื่อง + gcc การเป็นตัวแทนภายในของ GIMPLE สำหรับการปรับแต่งลิงค์เวลาเมื่อทำการอินไลน์เข้าไปในโปรแกรม (คือlibc.a
จะไม่ได้มีส่วนร่วมใน-flto
การเพิ่มประสิทธิภาพการเชื่อมโยงเวลาเข้าโปรแกรมหลัก.) glibc อาคารว่าวิธีการจะเป็นที่อาจไม่ปลอดภัยกับเป้าหมายที่จริงใช้นี้.c
ในความเป็นจริงตามที่ @zwol ความคิดเห็น LTO ไม่สามารถใช้เมื่อสร้าง glibc เองเพราะรหัส "เปราะ" เช่นนี้ซึ่งอาจแตกถ้า inlining ระหว่างไฟล์ต้นฉบับ glibc เป็นไปได้ (มีการใช้งานภายในบางอย่างstrlen
เช่นอาจเป็นส่วนหนึ่งของการprintf
ใช้งาน)
นี่strlen
ทำให้สมมติฐานบางอย่าง:
CHAR_BIT
มีหลาย 8 เป็นจริงในระบบ GNU ทั้งหมด POSIX 2001 CHAR_BIT == 8
ค้ำประกันแม้กระทั่ง (สิ่งนี้ดูปลอดภัยสำหรับระบบที่มีCHAR_BIT= 16
หรือ32
เช่น DSP บางอันวนลูปที่ไม่ได้จัดแนว - จะรัน 0 ซ้ำเสมอถ้าเป็นsizeof(long) = sizeof(char) = 1
เพราะตัวชี้ทุกตัวอยู่ในแนวเดียวกันเสมอและp & sizeof(long)-1
เป็นศูนย์เสมอ) แต่ถ้าคุณมีชุดอักขระที่ไม่ใช่ ASCII หรือกว้าง 12 บิต0x8080...
เป็นรูปแบบที่ผิด
- (อาจ)
unsigned long
เป็น 4 หรือ 8 ไบต์ หรืออาจใช้งานได้จริงกับขนาดunsigned long
ไม่เกิน 8 และใช้assert()
เพื่อตรวจสอบสิ่งนั้น
UB ทั้งสองนั้นเป็นไปไม่ได้ แต่เป็นไปไม่ได้ที่จะพกพาไปใช้งาน C บางตัว รหัสนี้เป็นส่วนหนึ่งของการติดตั้ง C บนแพลตฟอร์มที่ใช้งานได้ดี
สมมติฐานถัดไปคือ C UB ที่มีศักยภาพ:
- การโหลดแบบจัดเรียงที่ประกอบด้วยไบต์ที่ถูกต้องจะไม่เป็นความผิดและปลอดภัยตราบใดที่คุณไม่สนใจไบต์ที่อยู่นอกวัตถุที่คุณต้องการ (เป็น True ใน asm ในทุกระบบ GNU และใน CPU ปกติทั้งหมดเนื่องจากการป้องกันหน่วยความจำเกิดขึ้นกับ granularity ที่จัดเรียงหน้า มันปลอดภัยที่จะอ่านผ่านจุดสิ้นสุดของบัฟเฟอร์ภายในหน้าเดียวกันใน x86 และ x64ปลอดภัยใน C เมื่อ UB ไม่สามารถมองเห็นได้ในเวลาคอมไพล์โดยไม่มี inlining นี่เป็นกรณีนี้คอมไพเลอร์ไม่สามารถพิสูจน์ได้ว่าการอ่านที่ผ่านมาอันแรก
0
คือ UB มันอาจเป็นchar[]
อาร์เรย์C ที่มี{1,2,0,3}
ตัวอย่าง)
จุดสุดท้ายนั่นคือสิ่งที่ทำให้อ่านจุดสิ้นสุดของวัตถุ C ได้อย่างปลอดภัย มันค่อนข้างปลอดภัยแม้ในขณะที่คอมไพเลอร์กับคอมไพเลอร์ในปัจจุบันเพราะฉันคิดว่าพวกเขาไม่ปฏิบัติต่อนั่นหมายความว่าเส้นทางของการประหารชีวิตนั้นไม่สามารถเข้าถึงได้ แต่อย่างไรก็ตามการใช้นามแฝงที่เข้มงวดนั้นเป็นสิ่งที่ดีถ้าคุณปล่อยให้แบบอินไลน์นี้
ถ้าอย่างนั้นคุณก็มีปัญหาเช่นmemcpy
มาโคร CPPเก่าของเคอร์เนลที่ไม่ปลอดภัยซึ่งใช้การชี้การชี้ไปที่unsigned long
( gcc, aliasing ที่เข้มงวดและเรื่องสยองขวัญ )
นี้strlen
วันที่กลับไปในยุคนั้นเมื่อคุณได้รับไปกับสิ่งที่ชอบในทั่วไป ; มันค่อนข้างปลอดภัยโดยไม่ต้องมีข้อแม้ "เมื่อไม่ได้อยู่ในบรรทัด" ก่อน GCC3
UB ที่มองเห็นได้เฉพาะเมื่อมองข้ามขอบเขตการโทร / สายกลับไม่สามารถทำร้ายเราได้ (เช่นการโทรแบบนี้char buf[]
แทนการunsigned long[]
ส่งไปยัง a const char*
) เมื่อรหัสเครื่องถูกตั้งค่าเป็นเพียงแค่จัดการกับไบต์ในหน่วยความจำ การเรียกใช้ฟังก์ชั่นที่ไม่ใช่แบบอินไลน์จะต้องสมมติว่า callee อ่านหน่วยความจำใด ๆ / ทั้งหมด
เขียนสิ่งนี้อย่างปลอดภัยโดยไม่ใช้นามแฝงที่เข้มงวด
ประเภท GCC แอตทริบิวต์may_alias
char*
ให้ประเภทการรักษานามแฝงอะไรเช่นเดียวกับ (แนะนำโดย @KonradBorowsk) ส่วนหัวของ GCC ในปัจจุบันใช้สำหรับประเภทเวกเตอร์ x 86 SIMD เช่นเพื่อให้คุณสามารถเสมอได้อย่างปลอดภัยทำ__m128i
_mm_loadu_si128( (__m128i*)foo )
(ดูที่`reinterpret_cast` ระหว่างตัวชี้เวกเตอร์ฮาร์ดแวร์กับประเภทที่เกี่ยวข้องกับพฤติกรรมที่ไม่ได้กำหนดหรือไม่สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับสิ่งนี้จะทำและไม่ได้หมายความว่า)
strlen(const char *char_ptr)
{
typedef unsigned long __attribute__((may_alias)) aliasing_ulong;
aliasing_ulong *longword_ptr = (aliasing_ulong *)char_ptr;
for (;;) {
unsigned long ulong = *longword_ptr++; // can safely alias anything
...
}
}
นอกจากนี้คุณยังสามารถใช้ในการแสดงประเภทด้วยaligned(1)
alignof(T) = 1
typedef unsigned long __attribute__((may_alias, aligned(1))) unaligned_aliasing_ulong;
วิธีพกพาในการแสดงภาระการ aliasing ใน ISO ก็คือmemcpy
คอมไพเลอร์สมัยใหม่จะรู้วิธี inline เป็นคำสั่งการโหลดครั้งเดียว เช่น
unsigned long longword;
memcpy(&longword, char_ptr, sizeof(longword));
char_ptr += sizeof(longword);
นอกจากนี้ยังใช้งานได้กับการโหลดที่ไม่ได้จัดแนวเนื่องจากmemcpy
ทำงานเหมือน - โดยchar
- at-a-time access แต่ในทางปฏิบัติคอมไพเลอร์สมัยใหม่เข้าใจmemcpy
ดีมาก
อันตรายที่นี่คือถ้า GCC ไม่ทราบแน่ชัดว่าchar_ptr
มีการจัดเรียงคำมันจะไม่อินไลน์ในบางแพลตฟอร์มที่อาจไม่รองรับการโหลดที่ไม่จัดแนวใน asm เช่น MIPS ก่อน MIPS64r6 หรือ ARM ที่เก่ากว่า หากคุณได้รับการเรียกใช้ฟังก์ชันจริงmemcpy
เพียงเพื่อโหลดคำ (และเก็บไว้ในหน่วยความจำอื่น) นั่นจะเป็นหายนะ GCC บางครั้งสามารถมองเห็นเมื่อรหัสจัดตำแหน่งตัวชี้ หรือหลังจากลูป char-at-a-time ที่ถึงขอบเขตอูลองที่คุณสามารถใช้ได้
p = __builtin_assume_aligned(p, sizeof(unsigned long));
สิ่งนี้ไม่ได้หลีกเลี่ยง UB ที่อ่านได้ในอดีต แต่ใน GCC ปัจจุบันนั้นไม่เป็นอันตรายในทางปฏิบัติ
เหตุใดจึงต้องมีแหล่งที่มา C ที่ปรับให้เหมาะสมด้วยมือ: คอมไพเลอร์ปัจจุบันไม่ดีพอ
asm ที่ได้รับการปรับปรุงด้วยมือนั้นสามารถทำได้ดียิ่งขึ้นเมื่อคุณต้องการประสิทธิภาพที่ลดลงทุกครั้งสำหรับฟังก์ชั่นไลบรารีมาตรฐานที่ใช้กันอย่างแพร่หลาย โดยเฉพาะอย่างยิ่งสำหรับสิ่งที่ต้องการแต่ยังmemcpy
strlen
ในกรณีนี้มันจะไม่ง่ายกว่าการใช้ C ที่มี x86 อินทรินในการใช้ประโยชน์จาก SSE2
แต่ที่นี่เรากำลังพูดถึงรุ่นไร้เดียงสากับ Bithack C ที่ไม่มีคุณสมบัติเฉพาะของ ISA
(ฉันคิดว่าเราสามารถนำไปใช้ได้ตามที่strlen
ใช้อย่างกว้างขวางเพียงพอที่ทำให้มันรันเร็วที่สุดเท่าที่จะเป็นไปได้ดังนั้นคำถามจึงกลายเป็นว่าเราจะได้รับรหัสเครื่องที่มีประสิทธิภาพจากแหล่งที่ง่ายกว่าหรือไม่เราไม่สามารถทำได้)
GCC ในปัจจุบันและเสียงดังกราวไม่สามารถของลูปอัตโนมัติ vectorizing ที่นับซ้ำไม่เป็นที่รู้จักไปข้างหน้าของซ้ำแรก (เช่นต้องตรวจสอบว่าลูปจะทำงานอย่างน้อย 16 ครั้งก่อนที่จะรันการวนซ้ำครั้งแรก) เช่นการบันทึกอัตโนมัติแบบ autovectorizing memcpy เป็นไปได้ (บัฟเฟอร์ความยาวชัดแจ้งอย่างชัดเจน) แต่ไม่ใช่ strcpy หรือ strlen (ความยาวโดยนัย) คอมไพเลอร์
ซึ่งรวมถึงลูปการค้นหาหรือลูปอื่น ๆ ที่ขึ้นอยู่กับข้อมูลif()break
เช่นเดียวกับตัวนับ
ICC (คอมไพเลอร์ของ Intel สำหรับ x86) สามารถทำการวนลูปการค้นหาอัตโนมัติได้ แต่ก็ยังคงสร้าง asm แบบไม่ระบุชื่อในเวลาสำหรับ asm แบบเรียบง่าย / ไร้เดียงสาstrlen
เช่น libc ของ OpenBSD ใช้ ( Godbolt ) (จากคำตอบของ @ Peske )
libc มือที่ดีที่สุดstrlen
เป็นสิ่งที่จำเป็นสำหรับการทำงานกับคอมไพเลอร์ในปัจจุบัน การไปทีละ 1 ไบต์ (ด้วยการคลายอาจ 2 ไบต์ต่อรอบบนซีพียูที่มีความเร็วสูง) เป็นสิ่งที่น่าสมเพชเมื่อหน่วยความจำหลักสามารถรักษาได้ 8 ไบต์ต่อรอบและแคช L1d สามารถส่ง 16 ถึง 64 ต่อรอบ (โหลด 2x 32 ไบต์ต่อรอบในซีพียูกระแสหลัก x86 ที่ทันสมัยตั้งแต่ Haswell และ Ryzen ไม่นับ AVX512 ซึ่งสามารถลดความเร็วสัญญาณนาฬิกาสำหรับการใช้เวกเตอร์ 512 บิตซึ่งเป็นเหตุผลที่ glibc อาจไม่รีบเพิ่มรุ่น AVX512 แม้ว่าจะมีเวกเตอร์ 256 บิต แต่หน้ากาก AVX512VL + BW เปรียบเทียบกับมาสก์และktest
หรือkortest
อาจทำให้strlen
ไฮเปอร์เธรดเป็นมิตรมากขึ้นโดยการลด uops / การวนซ้ำ)
ฉันรวม non-x86 ไว้ที่นี่นั่นคือ "16 bytes" เช่นซีพียู AArch64 ส่วนใหญ่สามารถทำอย่างน้อยฉันคิดว่าและบางอย่างเพิ่มเติม และบางแห่งก็มีปริมาณงานที่เพียงพอสำหรับstrlen
การติดตามแบนด์วิธโหลดนั้น
แน่นอนว่าโปรแกรมที่ทำงานกับสตริงขนาดใหญ่มักจะติดตามความยาวเพื่อหลีกเลี่ยงการทำซ้ำการค้นหาความยาวของสตริง C โดยนัยยาวบ่อยมาก แต่ประสิทธิภาพความยาวสั้นถึงปานกลางยังคงได้รับประโยชน์จากการใช้งานที่เขียนด้วยมือและฉันมั่นใจว่าบางโปรแกรมจะจบลงด้วยการใช้ strlen บนสตริงที่มีความยาวปานกลาง