Score:11

If/else เสี่ยงต่อการถูกโจมตีจากช่องทางด้านข้างหรือไม่?

ธง tf
Tom

ฉันมีสาขาใน c ++:

ถ้า (x & 1)
{
    x = function_1(x);
}
อื่น
{
    x = function_2(x);
}

ถ้า ฟังก์ชัน_1 และ ฟังก์ชัน_2 เป็นเวลาคงที่และใช้เวลาเท่าๆ กันในการคำนวณ การแตกสาขาดังกล่าวยังเสี่ยงต่อการถูกโจมตีจากช่องทางด้านข้างหรือไม่ ผู้โจมตีสามารถรู้ได้อย่างไรว่าเงื่อนไขใดถูกดำเนินการ?

Tom avatar
tf flag
Tom
@kelalaka โอเค ขอบคุณ แล้ว ((x & 1) * funtcion_1(x)) ^ ((~x & 1) * funtcion_2(x)) ล่ะ ดีขึ้นหรือยัง แย่แล้ว? ฉันสงสัยว่านั่นไม่ใช่วิธีแก้ปัญหาที่ดีเช่นกัน แต่ฉันไม่แน่ใจ
kelalaka avatar
in flag
ฉันไม่เห็นความแตกต่าง นอกจากนี้ ตรวจสอบให้แน่ใจว่าไม่มีสิ่งใดได้รับการปรับให้เหมาะสม นี่คือสาเหตุที่การบล็อก ASM เป็นเรื่องปกติในการใช้งานช่องทางด้านข้าง ด้านบนมีข้อผิดพลาด แก้ไขที่นี่ ( ความคิดเห็นอื่นถูกลบ) $$x = (x \wedge 1)* f_1(x) + ((x \wedge 1)\oplus 0x1 ) * f_2(x)$$
Tom avatar
tf flag
Tom
@kelalaka ใช่ มีข้อผิดพลาด นั่นเป็นเหตุผลที่ฉันคิดว่ามันแตกต่างออกไป ผลลัพธ์ตอนนี้เหมือนกับในสูตรของฉัน แต่ฉันคิดว่าคุณยังดีกว่า เพราะในสูตรของฉันมี "~x & 1" และนี่อาจไม่ใช่เวลาคงที่ (เมื่อเทียบกับ x & 1) โดยเฉพาะอย่างยิ่งถ้า x เป็นจำนวนที่มาก
kelalaka avatar
in flag
มันไม่เกี่ยวกับความไม่คงที่ของ ~ หรือ x-orมันเกี่ยวกับการที่คุณดำเนินการทั้งสองวิธีเสมอโดยไม่คำนึงถึงค่าของ $x$ และในขั้นตอนสุดท้ายคุณเพิ่มด้วยมาสก์ สิ่งนี้ไม่จำเป็นต้องพึ่งพาความตรงเวลาคงที่เหมือนกันของ $f_1$ และ $f_2$
kelalaka avatar
in flag
ดูคำตอบตามรูปแบบบัญญัติของ [Squeamish Ossifrage](https://crypto.stackexchange.com/a/96634/18298)...
Score:14
ธง ng

ใช่ ถ้า/อื่น ๆ เสี่ยงต่อการถูกโจมตีด้วยจังหวะเวลา. ดังนั้นการเลือกฟังก์ชันที่จะเรียกใช้โดยดัชนีอาร์เรย์ก็เช่นกัน คำตอบอื่น ๆ.

โซลูชันที่เชื่อถือได้ถัดไปเพียงตัวเดียวสำหรับการดำเนินการตามเวลาคงที่ในภาษาระดับสูง ซึ่งขาดสิ่งบ่งชี้เกี่ยวกับสภาพแวดล้อมเป้าหมายคือ: ตรวจสอบให้แน่ใจว่าเส้นทางการดำเนินการโค้ดและรูปแบบการเข้าถึงหน่วยความจำไม่ขึ้นกับข้อมูล.

สิ่งนี้ขัดขวางการเรียกเดี่ยวของ ฟังก์ชัน_1 หรือ ฟังก์ชัน_2 (เช่นเดียวกับที่รหัสของคำถามและคำตอบอื่นๆ ทำ) และการจัดทำดัชนีอาร์เรย์ โดยอ้างอิงจากข้อมูลที่เปลี่ยนแปลงภายใต้การควบคุมหรือในลักษณะที่ฝ่ายตรงข้ามสังเกตได้ แม้จะเป็นบางส่วนหรือโดยอ้อมก็ตาม

หากค่าใช้จ่ายเพิ่มเติมในการเรียกใช้ทั้งสองฟังก์ชันสามารถทนได้ และพฤติกรรมของฟังก์ชันนั้นชัดเจนและยอมรับได้โดยไม่คำนึงถึงบิตลำดับต่ำของอินพุต การดำเนินการนี้จะมีผลกับคอมไพเลอร์และฮาร์ดแวร์ C ในปัจจุบันส่วนใหญ่:

   ม = -(x&1); // ตัวแปรมาสก์; ถือว่า m และ x เป็น int
   x = (ฟังก์ชัน_1(x) & ม.) | (function_2(x) & ~m);

หรือเทียบเท่า

   x = function_1(x) & -(x&1) | function_2(x) & ~ -(x&1);

ซึ่งเรียกใช้ทั้งสองฟังก์ชัน และเลือกผลลัพธ์อย่างเหมาะสมโดยใช้มาสก์นั้น -(x&1) มีบิตทั้งหมดที่ 1 สำหรับคี่ xที่ 0 มิฉะนั้น

ฉันไม่ทราบว่าคอมไพเลอร์ CPU + C ใด ๆ ที่เปิดตัวตั้งแต่ปี 1985 ซึ่งจะมีการพึ่งพาเวลา x แนะนำโดยเทคนิคนั้น วรรณกรรมสมัยใหม่เกี่ยวกับการนำ crypto มาใช้ตลอดเวลาถือว่าไม่มีเลยโดยไม่ต้องพูด แม้ว่าความจริงแล้วเวลาดำเนินการของ CPU ประสิทธิภาพสูงสมัยใหม่จะไม่ได้ระบุ และอาจเปลี่ยนแปลงอย่างคาดเดาไม่ได้จากมุมมองของโปรแกรมเมอร์แอปพลิเคชัน (เช่น เนื่องจากคอมไพเลอร์/ลิงเกอร์และตัวเลือกของสิ่งนั้น การจัดตำแหน่งโค้ดและข้อมูล การขัดจังหวะ สถานะการรอของหน่วยความจำ และรอบการรีเฟรช , แคช, ไปป์ไลน์, การดำเนินการเชิงคาดเดา, ตัวทำนายสาขา, เธรดอื่นๆ บนคอร์/CPU/CPU ที่เชื่อมโยงเดียวกัน, การอนุญาโตตุลาการของทรัพยากรระหว่างเธรดบนคอร์เดียวกัน, การจำลองเสมือน, การตั้งค่า BIOS หรือ VMs และการเปลี่ยนแปลงไมโครโค้ดที่มีข้อบกพร่องที่ดูเหมือนจะไม่สิ้นสุดซึ่งประนีประนอม ประสิทธิภาพเพื่อลดการโจมตีช่องทางด้านข้างที่รายงานล่าสุดâ¦)

ที่สำคัญสมมติฐานที่ไม่สมจริงในคำถาม

ฟังก์ชัน_1 และ ฟังก์ชัน_2 เป็นเวลาคงที่และใช้เวลาเท่ากันในการคำนวณ

สามารถถูกแทนที่ด้วยความน่าเชื่อถือและปลอมได้:

การเปลี่ยนแปลงในเวลาดำเนินการของ ฟังก์ชัน_1 ไม่สัมพันธ์กับมูลค่าของ x. ฟังก์ชัน_2 มีคุณสมบัติเหมือนกัน

ข้อแม้ที่ 1: ภาษา C/C++ ให้การประกันเกี่ยวกับผลลัพธ์ที่ได้รับเท่านั้น ไม่มีการประกันเกี่ยวกับเวลาดำเนินการ ฉันรู้ว่าไม่มีคู่มือคอมไพเลอร์ที่ใส่ใจ และ CPU บางตัวที่ไม่มีเอกสารที่เกี่ยวข้อง

คำเตือน 2: -(x&1) เพื่อให้ได้หน้ากากตามคำสั่งขั้นต่ำของ x เป็นสำนวนที่รู้จักกันดีและปัจจุบันใช้ได้กับซีพียูเกือบทั้งหมด นั่นเป็นเพราะพวกเขาใช้ ส่วนประกอบของสอง ประชุมด้วย -1 เป็นตัวแทนของทุกคน แต่ฉันหลงทางว่า C สัญญาว่าจะซ่อนพฤติกรรมที่แตกต่างกันของฮาร์ดแวร์หรือไม่ (ฉันปล่อยให้ความคิดเห็นที่เป็นประโยชน์) และนั่นจำเป็นต้องได้รับการยืนยันไม่ว่าจะเกิดอะไรขึ้นก็ตาม

ข้อแม้ 3: อาจจำเป็นต้องปรับตัวตามประเภทของ x เนื่องจากกฎการเลื่อนระดับ C ฉันมักจะหล่อ 1 ถึงประเภทของ x, เช่น. ถ้า x เป็น uint32_t, ฉันใช้ -(x&(uint32_t)1)และจะมีความสุขกับมันหากไม่มีคำเตือนของคอมไพเลอร์

คอมไพเลอร์บางคนบ่นเมื่อทำการลบนิพจน์ที่ไม่ได้ลงนาม พวกเขาผิดตามมาตรฐาน C เส้นทางที่ง่ายคือการก้มและใช้ (0-(x&1))ด้วยการหล่อค่าคงที่ตามข้างต้น อีกประการหนึ่งคือการปิดการใช้งานข้อร้องเรียนนั้นด้วยลัทธิปฏิบัติหรือตัวเลือกบางอย่าง อีกสิ่งหนึ่งที่สิ้นหวังในประสบการณ์ของฉันคือการส่งรายงานปัญหา อาร์กิวเมนต์อย่างระมัดระวังไปที่ NUL (เทียบเท่าในเครื่องของ /dev/null) รวมถึง: โครงสร้างมีประโยชน์, ทั่วไป, ได้รับการสนับสนุนอย่างชัดเจนโดย ISO C; และการเตือนเกี่ยวกับมันอาจทำให้คำเตือนทั้งหมดถูกปิดเสียงตามอำเภอใจ

หมายเหตุ: ฉันถือว่าประเภทการส่งคืนของ ฟังก์ชัน_1 และ ฟังก์ชัน_2 เป็นประเภทเดียวกับ x.


การดำเนินการรักษาความปลอดภัย / การเข้ารหัสบน CPU มาตรฐานที่ฝ่ายตรงข้ามสามารถรันโค้ดได้นั้นมีความเปราะบางอย่างต่อเนื่องมาตั้งแต่ปี 1980 มีความก้าวหน้าที่เป็นประโยชน์: การแยกกระบวนการด้วย Memory Management Units, วงแหวนรักษาความปลอดภัยหรือวงล้อม มีซอฟต์แวร์ฮิวริสติกเพื่อลดช่องโหว่ให้อยู่ในระดับที่ยอมรับได้ แต่จนถึงตอนนี้ยังไม่มีกระสุนเงิน

ดังนั้นเมื่อเดิมพันสูง มักจะเป็นการดีที่สุดที่จะลดโหลดฟังก์ชันความปลอดภัยไปยังฮาร์ดแวร์ที่เชื่อถือได้ (สมาร์ทการ์ด, CPU ด้านความปลอดภัย, HSM, TPMsâ¦) ที่ออกแบบมาเพื่อความปลอดภัยเป็นอันดับแรก ไม่ใช่ประสิทธิภาพมาก่อน และที่ที่ฝ่ายตรงข้ามไม่สามารถรันโค้ดได้ (วิธีที่ง่ายที่สุดคือดีที่สุด) หรือที่ได้รับการออกแบบมา

SAI Peregrinus avatar
si flag
C ยังไม่ (ยัง) สัญญาพฤติกรรมเสริมของทั้งสอง ในมาตรฐาน C23 ที่กำลังจะมีการรับประกัน ดังนั้นบางแพลตฟอร์มจำเป็นต้องเลียนแบบส่วนประกอบของสองส่วนเพื่อให้มีคอมไพเลอร์ C23 นิพจน์ไม่ได้ลงนามแม้ว่า `x` จะไม่ได้ลงนามก็ตาม `1` เป็นจำนวนเต็มที่มีเครื่องหมาย ดังนั้นทั้ง `x` และ `1` จึงเลื่อนระดับตามกฎในหัวข้อ 6.3.1.8 จากนั้นจึงใช้ `&` และผลลัพธ์จะถูกปฏิเสธ ผลลัพธ์เป็นประเภทจำนวนเต็ม (เลื่อนขั้น) ของ `(x&1)` คุณสามารถใช้ตัวอักษรพิมพ์ (เช่น `1ULL`) เพื่อทำให้ `1` มีอันดับเดียวกับ `x`
in flag
@SAIPeregrinus: POSIX ทำ [promise](https://pubs.opengroup.org/onlinepubs/9699919799/baseefs/stdint.h.html) ส่วนเสริมของทั้งสองตอนนี้ ดังนั้นหากคุณกำลังพัฒนาสำหรับเป้าหมายที่สอดคล้องกับ POSIX (ซึ่ง เป็นส่วนใหญ่รวมถึง Windows ด้วย) ก็เป็นอันเรียบร้อย
Lorenzo Donati support Ukraine avatar
@SAIPeregrinus Nitpick: 1ULL คือ `unsigned long long` ซึ่งมีขนาด *อย่างน้อย* 64 บิต ตามมาตรฐาน C ดังนั้นจึงมีอันดับสูงกว่า `x` (`uint32_t`) การอ้างอิง: ส่วนมาตรฐาน C (ฉบับร่าง N1256 - C99) **5.2.4.2.1 ขนาดของประเภทจำนวนเต็ม**: *ค่าสูงสุดสำหรับวัตถุประเภท unsigned long long int* `ULLONG_MAX 18446744073709551615 // 2^64 â 1`
Tom avatar
tf flag
Tom
นั่นเป็นคำตอบที่ดีและเป็นหัวข้อใหญ่อย่างที่ฉันเห็น BTW ฉันกำลังทำงานกับประเภทข้อมูล __m128i ดังนั้นฉันจึงทำสิ่งต่อไปนี้: _mm_xor_si128(x,k) & -_mm_cvtsi128_si64(x & 1) เพื่อให้ได้บิตที่มีนัยสำคัญต่ำสุด ฉันต้องใช้ _mm_cvtsi128_si64 _mm_cvtsi128_si64 และ __m128i เป็นข้อมูลสองประเภทที่แตกต่างกัน แต่ AND ยังทำงานได้อย่างถูกต้องเนื่องจากคุณสมบัติ __m128i ดังนั้นจึงไม่เสมอไปที่ 1 ต้องเป็นประเภทเดียวกับ x และแม้แต่ x&1 ก็ไม่จำเป็นต้องเป็นประเภทเดียวกับเอาต์พุต function_1 หรือ function_2 แต่ SSE2 เป็นกรณีพิเศษ
Tom avatar
tf flag
Tom
@fgrieu ถ้าฉันเข้าใจทุกอย่างถูกต้อง มันไม่สำคัญเมื่อเราแทนที่สูตรที่นำเสนอด้วย x = function_1(x) & -(x&1) | function_2(x) & -(x&1^1)? เราจึงใช้ xor 1 แทน ~
fgrieu avatar
ng flag
@Tom: ใช่ นั่นก็ใช้ได้เหมือนกัน อย่างไรก็ตาม คอมไพเลอร์ส่วนใหญ่จะสังเกตเห็นว่า `~ -(x&1)` สามารถคำนวณได้อย่างมีประสิทธิภาพจาก `-(x&1)` หรือฟรีหากตัวดำเนินการ `&~` มีการสนับสนุนฮาร์ดแวร์ ซึ่งเป็นเรื่องปกติเนื่องจากมีการใช้ในลักษณะทั่วไปและมีประโยชน์มากมาย สำนวน เช่น ใน `y&m | z&~m` แต่ฉันมีข้อสงสัยว่าคอมไพเลอร์จำนวนมากจะใช้ `-(x&1)` ซ้ำในการประเมิน `& -(x&1^1)`
fgrieu avatar
ng flag
@Tom: ขอบคุณที่ชี้แจง ฉันลบความคิดเห็นของคุณตั้งแต่อ่านแล้ว ดังนั้นจึงไม่จำเป็นอีกต่อไป การโหวตความคิดเห็นก็เพียงพอแล้ว (และด้วยเหตุนี้ ความคิดเห็นปัจจุบันจะถูกทำลาย RSN)
Score:4
ธง us

สิ่งนี้เริ่มต้นจากการแก้ไขคำตอบของ fgrieu แต่มันยาวมาก ดังนั้นฉันจึงตัดสินใจโพสต์เป็นคำตอบแยกต่างหาก

ฉันเห็นด้วยกับทุกสิ่งที่ fgrieu เขียนในคำตอบของเขา

ฉันจะอ้างอิงคำตอบของ fgrieu ที่นี่เพื่อเน้นมากขึ้น:

โซลูชันที่เชื่อถือได้ถัดไปเพียงตัวเดียวสำหรับการดำเนินการตามเวลาคงที่ในภาษาระดับสูง ซึ่งขาดสิ่งบ่งชี้เกี่ยวกับสภาพแวดล้อมเป้าหมายคือ: ตรวจสอบให้แน่ใจว่าเส้นทางการดำเนินการโค้ดและรูปแบบการเข้าถึงหน่วยความจำไม่ขึ้นกับข้อมูล.

หากเราไม่พิจารณารูปแบบการเข้าถึงหน่วยความจำ เนื่องจากไม่อยู่ในขอบเขตของคำถาม ที่ยกมาสามารถ เท่านั้น ในทางทฤษฎีสามารถทำได้ด้วยเวลาเข้าถึงหน่วยความจำคงที่และการดำเนินการตามเวลาคงที่ของทุกคำสั่ง แน่นอนว่าสิ่งเหล่านี้มาพร้อมกับค่าใช้จ่ายของรอบสัญญาณนาฬิกาที่เพิ่มขึ้นและการเข้าถึงหน่วยความจำเพิ่มเติม

รูปแบบการเข้าถึงหน่วยความจำหมายถึงการโจมตีช่องทางด้านข้างประเภทอื่นที่ต้องใช้หน่วยความจำเข้าถึงโดยสุ่มแบบลืมเลือน (ORAM) เพื่อป้องกันการโจมตีช่องทางด้านข้าง


ถ้า function_1 และ function_2 เป็นเวลาคงที่และใช้เวลาเท่ากัน เวลาในการคำนวณ การแตกแขนงเช่นนี้ยังมีช่องโหว่อยู่หรือไม่ การโจมตีช่องทางด้านข้าง?

ส่วนใหญ่ใช่ if/else เสี่ยงต่อการถูกโจมตีด้วยจังหวะเวลาเนื่องจากเมื่อคุณใช้คำสั่ง if/else คุณสามารถคาดหวังจากคอมไพเลอร์หรือสภาพแวดล้อมรันไทม์เพื่อดำเนินการสาขาได้อย่างง่ายดาย ไม่ว่าในกรณีใดคุณมักจะพยายามหลีกเลี่ยง

การเลือกฟังก์ชันที่จะเรียกใช้โดยดัชนีอาร์เรย์ เช่น ในคำตอบของ mentallurg ใน Python นั้นไม่ใช่วิธีการที่แย่นัก แต่ก็มีเวลาเข้าถึงหน่วยความจำคงที่ และโค้ดที่ Python เรียกใช้งานเพื่อให้การเรียกใช้ฟังก์ชันเป็นเวลาต่อเนื่อง


ก่อนที่ฉันจะเริ่ม

  • ในกรณีของการเข้าถึงหน่วยความจำ โปรดทราบว่าเนื่องจากเราสนใจการโจมตีแบบกำหนดเวลา เราจึงสนใจเฉพาะการเข้าถึงหน่วยความจำเท่านั้น เวลา และไม่ รูปแบบ ที่อาจทำให้ข้อมูลรั่วไหลได้ เพราะจะทำให้สถานการณ์ของเราซับซ้อนขึ้นอีกเล็กน้อย
  • ฉันพิจารณาแคชในตอนท้ายเท่านั้น สำหรับตอนนี้ เราไม่พิจารณาแคช เนื่องจากทุกคนทราบดีว่าแคชของ CPU อาจเป็นฝันร้ายที่สุดของนักเข้ารหัสในสถานการณ์เช่นนี้
  • ฉันคิดว่าแต่ละคำสั่งที่ไม่มีการเข้าถึงหน่วยความจำจะใช้รอบสัญญาณนาฬิกาเดียวกันในการดำเนินการ

การวิเคราะห์คำตอบของ Mentallurg :

ลองใช้โค้ด Python ง่ายๆ นี้เป็นตัวอย่าง

def f1(x):
    กลับ x + 1

def f2(x):
    กลับ x + 2

def หลัก ():
    ฟังก์ชัน = [f1, f2]
    x = int(อินพุต("ป้อนค่า:"))
    ผม = x % 2
    ผลลัพธ์ = ฟังก์ชัน[i](x)
    พิมพ์(ผล)

ถ้า __name__=='__main__':
    หลัก()

สิ่งนี้ได้รับการคอมไพล์เป็น Python bytecode ต่อไปนี้ (เอาต์พุตของ pycdas สำหรับ .pyc ที่คอมไพล์ด้วย Python 3.10):

        [รหัส]
            ชื่อไฟล์: test.py
            ชื่อวัตถุ: หลัก
            จำนวนอาร์กิวเมนต์: 0
            Pos Only Arg จำนวน: 0
            KW เท่านั้น Arg จำนวน: 0
            ชาวบ้าน: 4
            ขนาดกอง: 3
            ค่าสถานะ: 0x00000043 (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE)
            [ชื่อ]
                'f1'
                'f2'
                'int'
                'ป้อนข้อมูล'
                'พิมพ์'
            [ชื่อวาร์]
                'ฟังก์ชั่น'
                'x'
                'ผม'
                'ผลลัพธ์'
            [ฟรีวาร์]
            [เซลล์วาร์]
            [ค่าคงที่]
                ไม่มี
                'ป้อนค่า:'
                2
            [การถอดชิ้นส่วน]
                0 LOAD_GLOBAL 0: f1
                2 LOAD_GLOBAL 1: f2
                4 BUILD_LIST 2
                6 STORE_FAST 0: ฟังก์ชัน
                8 LOAD_GLOBAL 2: ภายใน
                10 LOAD_GLOBAL 3: อินพุต
                12 LOAD_CONST 1: 'ป้อนค่า:'
                14 CALL_FUNCTION 1
                16 CALL_FUNCTION 1
                18 STORE_FAST 1:x
                20 LOAD_FAST 1:x
                22 LOAD_CONST 2:2
                24 BINARY_MODULO           
                26 STORE_FAST 2: i
                28 LOAD_FAST 0: ฟังก์ชัน
                30 LOAD_FAST 2: i
                32 BINARY_SUBSCR           
                34 LOAD_FAST 1:x
                36 CALL_FUNCTION1
                38 STORE_FAST 3: ผลลัพธ์
                40 LOAD_GLOBAL 4: พิมพ์
                42 LOAD_FAST 3: ผลลัพธ์
                44 CALL_FUNCTION1
                46 POP_TOP                 
                48 LOAD_CONST 0: ไม่มี
                50 RETURN_VALUE

การค้นหาและการดำเนินการของบรรทัด ผลลัพธ์ = ฟังก์ชัน[i](x) ทำจากบรรทัดไบต์ 26-36 ลองมาดูที่ BINARY_SUBSCR ผู้ประกอบการ ต้องใช้สองอาร์กิวเมนต์ รายการ (อาจเป็น dict หรือ tuple ก็ได้ แต่ขอไม่เน้นเรื่องนี้) เป็นรายการแรกและดัชนีจากสแต็ก (รายการที่โหลดไว้ก่อนหน้านี้ โหลด_FAST) ส่งกลับค่าที่ดัชนีนั้นและลดกองลง 1

ทีนี้ลองมาดูกันว่า BINARY_SUBSCR ถูกนำไปใช้ใน CPython สามารถพบการนำไปใช้งานได้ ที่นี่ และมีดังต่อไปนี้:

        เป้าหมาย (BINARY_SUBSCR) {
            ทำนาย (BINARY_SUBSCR);
            PyObject *sub = POP();
            PyObject *คอนเทนเนอร์ = TOP();
            PyObject *res = PyObject_GetItem(คอนเทนเนอร์, ย่อย);
            Py_DECREF(คอนเทนเนอร์);
            Py_DECREF(ย่อย);
            SET_TOP (ความละเอียด);
            ถ้า (ความละเอียด == NULL)
                ข้ามข้อผิดพลาด;
            กระโดด (INLINE_CACHE_ENTRIES_BINARY_SUBSCR);
            ส่ง();
        }

ตอนนี้สามารถมุ่งเน้นไปที่การวิเคราะห์ทั้งหมด PyObject *res = PyObject_GetItem(คอนเทนเนอร์, ย่อย);. นี่เป็นวิธีการทั่วไปและจนกว่าจะมีการเรียกค้นรายการ วิธีการอื่นๆ จะถูกเรียกในขั้นกลาง แน่นอนเราสามารถคาดหวัง $O(1)$ ความซับซ้อน มันยังคงตรวจสอบ ในตอนท้าย PyList_GetItem เรียกว่ามีดังต่อไปนี้

PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t ฉัน)
{
    ถ้า (!PyList_Check(op)) {
        PyErr_BadInternalCall();
        ส่งคืน NULL;
    }
    ถ้า (!valid_index(i, Py_SIZE(op))) {
        _Py_DECLARE_STR(list_err, "รายการดัชนีอยู่นอกช่วง");
        PyErr_SetObject(PyExc_IndexError, &_Py_STR(list_err));
        ส่งคืน NULL;
    }
    ส่งคืน ((PyListObject *)op) -> ob_item[i];
}

ดังที่เราเห็นในบรรทัดสุดท้าย มันมี $O(1)$ ความซับซ้อนแน่นอนว่าเนื่องจากความซับซ้อนของภาษาระดับสูง ภาษาเหล่านี้จึงไม่เคยถูกนำมาใช้ในทางปฏิบัติสำหรับแอปพลิเคชันดังกล่าว ลองใช้โค้ดนี้ในภาษาระดับล่าง เช่น C เพื่อดูว่ามันสร้างอะไร

#รวม <stdio.h>

int f1(int x) {
    กลับ x + 1;
}

int f2(int x) {
    กลับ x + 2;
}

int หลัก () {
    int x;
    scanf("%d", &x);
    int (*(ฟังก์ชั่น[2]))(int) = {f1, f2};
    int ฉัน;
    ฉัน = x %2;
    ผลลัพธ์ int = ฟังก์ชั่น [i] (x);
}

นี่คือ x86_64 จาก Godbolt พร้อม GCC ล่าสุดที่ไม่มีการเพิ่มประสิทธิภาพ :

f1:
        เพิ่ม   $sp,$sp,-8
        สว      $fp,4($sp)
        เคลื่อนไหว    $fp,$sp
        สว      $4,8($ฉพ)
        ล      $2,8($ฉพ)
        นพ
        เพิ่ม   $2,$2,1
        เคลื่อนไหว    $sp,$ฉพ
        ล      $fp,4($sp)
        เพิ่ม   $sp,$สเป,8
        จูเนียร์ $31
        นพ

f2:
        เพิ่ม   $sp,$sp,-8
        สว      $fp,4($sp)
        เคลื่อนไหว    $fp,$sp
        สว      $4,8($ฉพ)
        ล      $2,8($ฉพ)
        นพ
        เพิ่ม   $2,$2,2
        เคลื่อนไหว    $sp,$ฉพ
        ล      $fp,4($sp)
        เพิ่ม   $sp,$สเป,8
        จูเนียร์ $31
        นพ

$LC0:
    .ascii "%d\000"
หลัก:
    เพิ่ม $สเป,$sp,-56
    สวิส $31,52($sp)
    สวิส $เอฟพี,48($sp)
    ย้าย $เอฟพี,$sp
    เพิ่ม $2,$fp,32
    ย้าย $5,$2
    ลุย $2,%สวัสดี($LC0)
    เพิ่ม $4,$2,%lo($LC0)
        แจล __isoc99_scanf
        นพ

        ลุย     $2,%สูง(f1)
    เพิ่ม $2,$2,%lo(f1)
    สวิส $2,36($fp)
    ลุย $2,%สูง(f2)
        เพิ่ม   $2,$2,%lo(f2)
        สว      $2,40($ฉพ)
        ล      $3,32($ฉพ)
        หลี่      $2,-2147483648 # 0xffffffff80000000
    หรือ $2,$2,0x1
    และ $2,$3,$2
        บีเจซ    $2,$L6
        นพ

        เพิ่ม   $2,$2,-1
        หลี่      $3,-2 # 0xfffffffffffffffe
    หรือ $2,$2,$3
        เพิ่ม   $2,$2,1
$L6:
    สวิส $2,24($fp)
    ลว $2,24($fp)
    นพ
    sll $2,$2,2
    เพิ่ม $3,$fp,24
    เพิ่ม $2,$3,$2
        ล      $2,12($2)
        ล      $3,32($ฉพ)
        นพ
        เคลื่อนไหว    $4,$3
        เคลื่อนไหว    $25,$2
        25 ดอลลาร์สหรัฐฯ
        นพ

        สว      $2,28($ฉพ)
        เคลื่อนไหว    $2,$0
        เคลื่อนไหว    $sp,$ฉพ
        ล      $31,52($sp)
        ล      $fp,48($sp)
        เพิ่ม   $sp,$sp,56
        จูเนียร์ $31
        นพ

โดยเฉพาะอย่างยิ่งเราสนใจคำแนะนำเหล่านี้ในการเรียกใช้ฟังก์ชันที่เหมาะสม :

$2,24($ฉพ)
        นพ
        sll     $2,$2,2
        เพิ่ม   $3,$เอฟพี,24
        แอดดู    $2,$3,$2
    ลว $2,12($2)
    ลว $3,32($fp)
    นพ
    ย้าย $4,$3
    ย้าย $25,$2
    จาร์ $25
        นพ

        สว      $2,28($ฉพ)
        เคลื่อนไหว    $2,$0

อย่างที่เราเห็นว่าไม่มีการสร้างสาขา ยกเว้นจาก jalr ซึ่งเรียกใช้ฟังก์ชันที่เหมาะสม


การวิเคราะห์คำตอบของ fgrieu

แน่นอน มันง่ายที่จะเห็นว่านี่เป็นเวลาคงที่ :

#รวม <stdio.h>

int f1(int x) {
    กลับ x + 1;
}

int f2(int x) {
    กลับ x + 2;
}

int หลัก () {
    int x;
    scanf("%d", &x);
    int (*(ฟังก์ชั่น[2]))(int) = {f1, f2};
    int ม = -(x&1); // ตัวแปรมาสก์ m ต้องเป็นชนิดเดียวกับ x
    x = (ฟังก์ชัน_1(x) & ม.) | (function_2(x) & ~m);
    พิมพ์f(x);
}

อีกครั้ง Goldbolt พร้อมตัวเลือกเดียวกัน :

f1:
        กด rbp
        ย้าย rbp, rsp
        mov DWORD PTR [rbp-4], แก้ไข
        ย้าย eax, DWORD PTR [rbp-4]
        เพิ่ม eax, 1
        ป๊อป rbp
        เกษียณ
f2:
        กด rbp
        ย้าย rbp, rsp
        mov DWORD PTR [rbp-4], แก้ไข
        ย้าย eax, DWORD PTR [rbp-4]
        เพิ่ม eax, 2
        ป๊อป rbp
        เกษียณ
.LC0:
        .string "%d"
หลัก:
        กด rbp
        ย้าย rbp, rsp
        กด rbx
        ย่อย rsp, 40
        เลียแร็กซ์ [rbp-24]
        ย้าย rsi, rax
        ย้าย edi, OFFSET FLAT:.LC0
        ย้าย eax, 0
        โทร __isoc99_scanf
        mov QWORD PTR [rbp-48], OFFSET FLAT:f1
        mov QWORD PTR [rbp-40], OFFSET FLAT:f2
        ย้าย eax, DWORD PTR [rbp-24]
        และ eax, 1
        ไม่เป็นไร
        mov DWORD PTR [rbp-20], eax
        ย้าย eax, DWORD PTR [rbp-24]
        ย้าย edi, eax
        ย้าย eax, 0
        ฟังก์ชันการโทร_1
        และ eax, DWORD PTR [rbp-20]
        ย้าย ebx, eax
        ย้าย eax, DWORD PTR [rbp-24]
        ย้าย edi, eax
        ย้าย eax, 0
        ฟังก์ชันการโทร_2
        mov edx, DWORD PTR [rbp-20]
        ไม่ใช่ edx
        และ eax, edx
        หรือ eax, ebx
        mov DWORD PTR [rbp-24], eax
        ย้าย eax, DWORD PTR [rbp-24]
        ซีดีคิว
        ย้าย rdi, rax
        ย้าย eax, 0
        โทรพิมพ์ฉ
        ย้าย eax, 0
        mov rbx, QWORD PTR [rbp-8]
        ออกจาก
        เกษียณ

เรามุ่งเน้นไปที่ส่วนนี้ซึ่งการมอบหมายงานให้ m เสร็จสิ้นและการแยกสาขาแบบอินไลน์:

        ย้าย eax, DWORD PTR [rbp-24]
        และ eax, 1
        ไม่เป็นไร
        mov DWORD PTR [rbp-20], ea
        ย้าย eax, DWORD PTR [rbp-24]
        ย้าย edi, eax
        ย้าย eax, 0
        ฟังก์ชันการโทร_1
        และ eax, DWORD PTR [rbp-20]
        ย้าย ebx, eax
        ย้าย eax, DWORD PTR [rbp-24]
        ย้าย edi, eax
        ย้าย eax, 0
        ฟังก์ชันการโทร_2
        mov edx, DWORD PTR [rbp-20]
        ไม่ใช่ edx
        และ eax, edx
        หรือ eax, ebx
        mov DWORD PTR [rbp-24], eax

เราสามารถเห็นการประมวลผลเวลาคงที่

ดังนั้นความแตกต่างระหว่างโซลูชันเหล่านี้คืออะไร:

ทั้งสองเป็นไปตามมาตรการวิเคราะห์ต่อต้านเวลา แต่อันที่สองยังซ่อนรูปแบบการเข้าถึง มันเรียกใช้ทั้งสองฟังก์ชันเสมอ เนื่องจากเรายังไม่ได้กล่าวถึงแคช เมื่อมีแคช แคชที่สองจึงดูปลอดภัยกว่าจากการโจมตีช่องทางด้านข้างตามเวลาเพียงเพราะต้องการรหัสของทั้งสองฟังก์ชันให้อยู่ในแคชเพื่อดำเนินการตามคำสั่ง ในกรณีที่สอง เฉพาะอันที่ถูกเรียกใช้เท่านั้นที่ถูกแคช หากเราถือว่าในช่วงเวลาใดเวลาหนึ่ง ฉ.1 ถูกเรียกและ f2 ถูกไล่ออกจากแคช จะมีความแตกต่างในเวลาที่ f2 จะถูกเรียกอีกครั้ง

สำหรับวิธีแก้ปัญหาอื่น ๆ และอ่านเพิ่มเติมเกี่ยวกับเรื่องนี้ที่คุณสามารถพิจารณาได้ [1] และ [2].

Score:-1
ธง kr

คุณสามารถใส่ฟังก์ชันลงในอาร์เรย์และอ้างอิงตามดัชนี

ใน Python จะมีลักษณะดังนี้:

def f1( x ):
    ...

def f2( x ):
    ...

ฟังก์ชัน = [ f1, f2 ]

ผม = x % 2

ผลลัพธ์ = ฟังก์ชัน[ i ]( x )
Tom avatar
tf flag
Tom
ขอบคุณ. นี่ก็ดูเหมือนจะฉลาด แน่นอนว่าฉันแค่พยายามทำให้โค้ดของฉันดีขึ้น ซึ่งไม่ใช่สำหรับมืออาชีพในตอนนี้
kelalaka avatar
in flag
คุณแน่ใจหรือว่าสิ่งนี้ (คือ | จะ) ไม่แปลงเป็น if-else ในพื้นหลัง การพึ่งพาคอมไพเลอร์นั้นเสี่ยงเกินไปตราบใดที่คุณไม่ได้ทำงานกับ [Thomas Pornin T1](https://www.youtube.com/watch?v=IHbtK5Kwt6A)
kr flag
@kelalaka: ฉันแน่ใจว่ามันใช้งานได้ตามที่คาดไว้ กรณีนี้ไม่สามารถตัดสินใจได้โดยคอมไพเลอร์หรือโดยสภาพแวดล้อมรันไทม์ในแพลตฟอร์มทั่วไป เช่น C, C++, C#, Java, Python สิ่งที่มักจะได้รับการปรับให้เหมาะสมคือนิพจน์ **บูลีน** เช่น. หากมีนิพจน์ `a & b` และรันไทม์รู้ว่า `a == false` (หรือ `a == 0`) แสดงว่าในหลาย ๆ แพลตฟอร์มนั้นไม่ได้มอบหมายให้รันไทม์ด้วยซ้ำ แต่ข้อกำหนดภาษาที่กำหนดนั้นจำเป็น รันไทม์ *ต้อง* ข้ามการประเมินของตัวถูกดำเนินการที่ 2
fgrieu avatar
ng flag
นี่อาจเป็นการปรับปรุง แต่ ** นั่นไม่ได้แก้ปัญหาอย่างเต็มที่ ** ทุกสิ่งรวมถึงแคช การจัดตำแหน่ง" วางแผนที่จะทำให้ไม่สามารถสร้างรหัสพกพาสำหรับคอมพิวเตอร์สมัยใหม่ในเวลาคงที่ ในขณะที่มีเส้นทางรหัสหรือรูปแบบการเข้าถึงข้อมูลที่ขึ้นอยู่กับข้อมูล ดังในคำถาม การบรรลุผลดังกล่าวเป็นไปได้ด้วยการควบคุมอย่างเข้มงวดของสภาพแวดล้อมการดำเนินการ (เช่น ในภาษาแอสเซมบลีบน CPU ที่มีรูปแบบรอบการดำเนินการที่ชัดเจน) และภาษาระดับสูงไม่อนุญาตให้ทำเช่นนั้น
kr flag
@fgrieu: เริ่มต้นที่บางคุณ **ไม่สามารถ** ควบคุมการดำเนินการแม้ว่าคุณจะใช้งานในแอสเซมเบลอร์ คุณไม่สามารถควบคุมวิธีที่ OS จัดระเบียบการเข้าถึงหน่วยความจำ วิธีแบ่งหน่วยความจำ วิธี OS ใช้การปรับให้เหมาะสมต่างๆ ในระดับที่ไกลออกไป แม้แต่ OS ก็ไม่สามารถควบคุมวิธีที่คอมพิวเตอร์ใช้แคชประเภทต่างๆ ได้ นอกจากนี้ CPU สมัยใหม่ส่วนใหญ่ในพีซียังใช้ **การคาดคะเนสาขา** เพื่อให้สามารถเรียกใช้โค้ดบางอย่างได้ **ล่วงหน้า** (ก่อนที่จะจำเป็นจริง ๆ) และผลลัพธ์อาจถูกนำมาใช้ในภายหลังหรืออาจไม่ใช้ก็ได้ ใช้แล้วทิ้งไปเลย และไม่มีทางที่จะควบคุมมันได้
kr flag
@fgrieu: ... นั่นเป็นเหตุผลที่เหมาะสมที่จะพูดคุยเกี่ยวกับการลดการรั่วไหลของข้อมูลช่องทางด้านข้างในระดับมหภาคเท่านั้น บางขั้นตอนในการลดการรั่วไหลในระดับต่ำอาจรวมถึงการใช้ระบบปฏิบัติการพิเศษ การใช้ฮาร์ดแวร์พิเศษ การใช้เครื่องมือที่สร้าง "เสียงรบกวน" มากขึ้นระหว่างการดำเนินการ ฉันถือว่า OP เป็นคำถามเกี่ยวกับการลดการรั่วไหลของช่องด้านข้างที่ระดับ *มาโคร*
fgrieu avatar
ng flag
@mentallurg: ฉันเห็นด้วยอย่างเต็มที่เกี่ยวกับ "คุณไม่สามารถควบคุมการดำเนินการได้" และความคิดเห็นที่เหลือฉันถือว่ามันเป็นอาร์กิวเมนต์ของคำตอบที่สรุปไว้ และอะไรก็ตามที่นำไปสู่การดำเนินการของฟังก์ชันเดียวตามความเท่าเทียมกันของ `x` ควรคาดหวังให้ _reduce_ การเปลี่ยนแปลงเวลามีความสัมพันธ์กับพาริตี้ของ `x` เมื่อหนึ่งในนั้น คำตอบของฉัน เมื่อใดและถ้าทำได้ ให้ _removes_ พวกเขาในสถาปัตยกรรมทั้งหมดที่ฉันรู้จัก ไม่ว่าจะเป็น OS, CPU, การทำนายสาขา และสถานะของไมโครโค้ดที่ต้องเผชิญกับอะไรก็ตามที่เป็นการโจมตีครั้งล่าสุด

โพสต์คำตอบ

คนส่วนใหญ่ไม่เข้าใจว่าการถามคำถามมากมายจะปลดล็อกการเรียนรู้และปรับปรุงความสัมพันธ์ระหว่างบุคคล ตัวอย่างเช่น ในการศึกษาของ Alison แม้ว่าผู้คนจะจำได้อย่างแม่นยำว่ามีคำถามกี่ข้อที่ถูกถามในการสนทนา แต่พวกเขาไม่เข้าใจความเชื่อมโยงระหว่างคำถามและความชอบ จากการศึกษาทั้ง 4 เรื่องที่ผู้เข้าร่วมมีส่วนร่วมในการสนทนาด้วยตนเองหรืออ่านบันทึกการสนทนาของผู้อื่น ผู้คนมักไม่ตระหนักว่าการถามคำถามจะมีอิทธิพลหรือมีอิทธิพลต่อระดับมิตรภาพระหว่างผู้สนทนา