สำหรับฉันดูเหมือนว่าเป็น MOV ที่ขี้ขลาด วัตถุประสงค์คืออะไรและควรใช้เมื่อใด
สำหรับฉันดูเหมือนว่าเป็น MOV ที่ขี้ขลาด วัตถุประสงค์คืออะไรและควรใช้เมื่อใด
คำตอบ:
ตามที่คนอื่น ๆ ได้ชี้ให้เห็น LEA (ที่อยู่ที่มีประสิทธิภาพในการโหลด) มักใช้เป็น "เคล็ดลับ" เพื่อทำการคำนวณบางอย่าง แต่นั่นไม่ใช่จุดประสงค์หลัก ชุดการเรียนการสอน x86 ได้รับการออกแบบมาเพื่อรองรับภาษาระดับสูงเช่น Pascal และ C โดยที่อาร์เรย์ - โดยเฉพาะอาร์เรย์ของ ints หรือ structs ขนาดเล็ก - เป็นเรื่องปกติ พิจารณาตัวอย่างเช่นโครงสร้างที่เป็นตัวแทน (x, y) พิกัด:
struct Point
{
int xcoord;
int ycoord;
};
ตอนนี้คิดว่าคำสั่งเช่น:
int y = points[i].ycoord;
ที่เป็นอาร์เรย์ของpoints[]
Point
สมมติว่าฐานของอาร์เรย์มีอยู่แล้วEBX
และตัวแปรi
อยู่ในEAX
และxcoord
และycoord
แต่ละบิต 32 บิต (ดังนั้นycoord
จะอยู่ที่ออฟเซ็ต 4 ไบต์ในโครงสร้าง) คำสั่งนี้สามารถรวบรวมเพื่อ:
MOV EDX, [EBX + 8*EAX + 4] ; right side is "effective address"
ซึ่งจะขึ้นฝั่งในy
EDX
สเกลแฟกเตอร์ของ 8 เป็นเพราะแต่ละตัวPoint
มีขนาด 8 ไบต์ ตอนนี้ให้พิจารณานิพจน์เดียวกันกับที่ใช้กับ "ที่อยู่ของ" ตัวดำเนินการ &:
int *p = &points[i].ycoord;
ในกรณีนี้คุณไม่ต้องการค่าของycoord
แต่เป็นที่อยู่ นั่นคือที่อยู่LEA
(โหลดที่อยู่ที่มีประสิทธิภาพ) เข้ามาแทนที่จะเป็นMOV
คอมไพเลอร์สามารถสร้างได้
LEA ESI, [EBX + 8*EAX + 4]
ESI
ซึ่งจะโหลดที่อยู่ใน
mov
สอนและละทิ้งวงเล็บไป? MOV EDX, EBX + 8*EAX + 4
MOV
กับแหล่งอ้อมยกเว้นก็เพียง MOV
แต่ความร้ายและไม่ ไม่ได้อ่านจากที่อยู่ที่คำนวณได้จริงเพียงคำนวณมัน
จาก"Zen of Assembly"โดย Abrash:
LEA
คำสั่งเดียวที่ดำเนินการคำนวณหน่วยความจำ แต่ไม่ได้ระบุหน่วยความจำLEA
ยอมรับตัวถูกดำเนินการที่อยู่หน่วยความจำมาตรฐาน แต่ไม่ทำอะไรมากไปกว่าการจัดเก็บหน่วยความจำที่คำนวณได้ในออฟเซ็ตที่ระบุซึ่งอาจเป็นการลงทะเบียนวัตถุประสงค์ทั่วไปนั่นให้อะไรเรา สองสิ่งที่
ADD
ไม่มีให้:
- ความสามารถในการเพิ่มด้วยตัวถูกดำเนินการสองหรือสามตัวและ
- ความสามารถในการเก็บผลในการลงทะเบียนใด ๆ ; ไม่ใช่แค่หนึ่งในตัวถูกดำเนินการแหล่งที่มา
และLEA
ไม่เปลี่ยนแปลงธง
ตัวอย่าง
LEA EAX, [ EAX + EBX + 1234567 ]
คำนวณEAX + EBX + 1234567
(นั่นคือสามตัวถูกดำเนินการ)LEA EAX, [ EBX + ECX ]
คำนวณEBX + ECX
โดยไม่มีการแทนที่ด้วยผลลัพธ์LEA EAX, [ EBX + N * EBX ]
(N สามารถเป็น 1,2,4,8)กรณีอื่นคือประโยชน์ในลูป: ความแตกต่างระหว่างLEA EAX, [ EAX + 1 ]
และINC EAX
คือการเปลี่ยนแปลงที่หลังEFLAGS
แต่อดีตไม่ได้; นี้จะรักษาCMP
สถานะ
LEA EAX, [ EAX + EBX + 1234567 ]
คำนวณผลรวมของEAX
, EBX
และ1234567
(ที่สามตัวถูกดำเนินการ) LEA EAX, [ EBX + ECX ]
คำนวณEBX + ECX
โดยไม่มีการแทนที่ด้วยผลลัพธ์ สิ่งที่สามLEA
ใช้สำหรับ (ไม่ได้ระบุไว้โดย Frank) คือการคูณด้วยค่าคงที่ (สอง, สาม, ห้าหรือเก้า) ถ้าคุณใช้มันเช่นLEA EAX, [ EBX + N * EBX ]
( N
อาจเป็น 1,2,4,8) กรณีอื่นคือประโยชน์ในลูป: ความแตกต่างระหว่างLEA EAX, [ EAX + 1 ]
และINC EAX
คือการเปลี่ยนแปลงที่หลังEFLAGS
แต่อดีตไม่ได้; นี้จะรักษาCMP
สถานะ
LEA
สามารถใช้สำหรับ ... (ดู "LEA (ที่อยู่ที่มีประสิทธิภาพในการโหลด) มักใช้เป็น" เคล็ดลับ "เพื่อทำการคำนวณบางอย่าง" ในคำตอบยอดนิยมของ IJ Kennedy ด้านบน)
คุณสมบัติที่สำคัญอีกประการของการLEA
เรียนการสอนคือมันไม่ได้เปลี่ยนรหัสเงื่อนไขเช่นCF
และZF
ในขณะที่คำนวณที่อยู่ด้วยคำแนะนำทางคณิตศาสตร์เช่นADD
หรือMUL
ไม่ คุณลักษณะนี้จะลดระดับการพึ่งพาระหว่างคำแนะนำและทำให้มีที่ว่างสำหรับการปรับแต่งเพิ่มเติมโดยคอมไพเลอร์หรือตัวกำหนดเวลาฮาร์ดแวร์
lea
บางครั้งมีประโยชน์สำหรับคอมไพเลอร์ (หรือมนุษย์ coder) ที่จะทำคณิตศาสตร์โดยไม่ต้องปิดกั้นผลธง แต่ไม่ได้เร็วกว่าlea
add
คำแนะนำ x86 ส่วนใหญ่เขียนการตั้งค่าสถานะ การใช้งาน x86 ที่มีประสิทธิภาพสูงต้องเปลี่ยนชื่อ EFLAGS หรือหลีกเลี่ยงอันตรายจากการเขียนหลังจากเขียนเพื่อให้รหัสปกติทำงานได้อย่างรวดเร็วดังนั้นคำแนะนำที่หลีกเลี่ยงการเขียนแบบธงจะไม่ดีขึ้นเพราะสิ่งนั้น ( สิ่งที่ตั้งค่าสถานะบางส่วนสามารถสร้างปัญหาให้ดูที่การเรียนการสอน INC เทียบกับเพิ่ม 1: มันเป็นเรื่องสำคัญ? )
แม้จะมีคำอธิบายทั้งหมด LEA คือการดำเนินการทางคณิตศาสตร์:
LEA Rt, [Rs1+a*Rs2+b] => Rt = Rs1 + a*Rs2 + b
มันเป็นเพียงชื่อที่โง่สุดขั้วสำหรับการดำเนินการ shift + add เหตุผลที่อธิบายไว้ในคำตอบที่ได้รับความนิยมสูงสุด (เช่นถูกออกแบบมาเพื่อแมปการอ้างอิงหน่วยความจำระดับสูงโดยตรง)
LEA
กับ AGUs แต่ใช้กับเลขจำนวนเต็ม ALU ปกติ เราต้องอ่านรายละเอียดซีพียูอย่างใกล้ชิดในวันนี้เพื่อค้นหา "สิ่งที่เรียกใช้" ...
LEA
ให้ที่อยู่ซึ่งเกิดขึ้นจากโหมดการกำหนดแอดเดรสที่เกี่ยวข้องกับหน่วยความจำ มันไม่ได้เป็นกะและเพิ่มการดำเนินงาน
อาจเป็นอีกเรื่องหนึ่งเกี่ยวกับการสอนของ LEA นอกจากนี้คุณยังสามารถใช้ LEA สำหรับการลงทะเบียนทวีคูณอย่างรวดเร็วด้วย 3, 5 หรือ 9
LEA EAX, [EAX * 2 + EAX] ;EAX = EAX * 3
LEA EAX, [EAX * 4 + EAX] ;EAX = EAX * 5
LEA EAX, [EAX * 8 + EAX] ;EAX = EAX * 9
LEA EAX, [EAX*3]
?
shl
คำแนะนำสำหรับการคูณการลงทะเบียน 2,4,8,16 ... มันเร็วขึ้นและสั้นลง แต่สำหรับการคูณด้วยจำนวนที่แตกต่างกันของกำลังสองเราใช้mul
คำสั่งปกติซึ่งอวดอ้างมากกว่าและช้ากว่า
lea eax,[eax*3]
lea eax,[eax+eax*2]
lea
เป็นตัวย่อของ "address effective load" มันโหลดที่อยู่ของการอ้างอิงตำแหน่งโดยตัวถูกดำเนินการแหล่งที่มาไปยังตัวถูกดำเนินการปลายทาง ตัวอย่างเช่นคุณสามารถใช้มันเพื่อ:
lea ebx, [ebx+eax*8]
เพื่อย้ายรายการebx
ตัวชี้eax
เพิ่มเติม (ในอาร์เรย์ 64- บิต / องค์ประกอบ) ด้วยคำสั่งเดียว โดยทั่วไปคุณจะได้รับประโยชน์จากโหมดการกำหนดแอดเดรสที่ซับซ้อนที่สนับสนุนโดยสถาปัตยกรรม x86 เพื่อจัดการกับพอยน์เตอร์ได้อย่างมีประสิทธิภาพ
เหตุผลที่ใหญ่ที่สุดที่คุณใช้LEA
มากกว่า a MOV
คือถ้าคุณต้องการคำนวณเลขคณิตในรีจิสเตอร์ที่คุณใช้ในการคำนวณที่อยู่ ได้อย่างมีประสิทธิภาพคุณสามารถดำเนินการสิ่งที่ยอดเงินให้ตัวชี้เลขคณิตในการลงทะเบียนหลายอย่างรวมกันได้อย่างมีประสิทธิภาพสำหรับ "ฟรี"
สิ่งที่ทำให้เกิดความสับสนเกี่ยวกับเรื่องนี้ก็คือโดยปกติแล้วคุณมักจะเขียนสิ่งที่LEA
คล้ายกันMOV
แต่คุณไม่ได้อ้างถึงความจำจริง ในคำอื่น ๆ :
MOV EAX, [ESP+4]
นี้จะย้ายเนื้อหาของสิ่งที่จุดที่จะเข้าสู่ESP+4
EAX
LEA EAX, [EBX*8]
นี่จะย้ายที่อยู่ที่มีประสิทธิภาพEBX * 8
ไปยัง EAX ไม่ใช่สิ่งที่พบในที่ตั้งนั้น อย่างที่คุณเห็นมีความเป็นไปได้ที่จะคูณด้วยปัจจัยสอง (การขยาย) ในขณะที่ a MOV
ถูก จำกัด ไว้ที่การเพิ่ม / การลบ
LEA
ทำ
8086 มีชุดคำสั่งขนาดใหญ่ซึ่งยอมรับตัวถูกดำเนินการลงทะเบียนและที่อยู่ที่มีประสิทธิภาพดำเนินการคำนวณบางอย่างเพื่อคำนวณส่วนออฟเซ็ตของที่อยู่ที่มีประสิทธิภาพนั้นและดำเนินการบางอย่างที่เกี่ยวข้องกับรีจิสเตอร์และหน่วยความจำที่อ้างถึง มันค่อนข้างง่ายที่จะมีคำแนะนำอย่างใดอย่างหนึ่งในตระกูลนั้นดังกล่าวข้างต้นยกเว้นการข้ามการดำเนินการของหน่วยความจำจริง นี่คือคำแนะนำ:
mov ax,[bx+si+5]
lea ax,[bx+si+5]
ถูกนำไปใช้เกือบเหมือนกันภายใน ความแตกต่างเป็นขั้นตอนที่ข้าม คำแนะนำทั้งสองทำงานคล้ายกับ:
temp = fetched immediate operand (5)
temp += bx
temp += si
address_out = temp (skipped for LEA)
trigger 16-bit read (skipped for LEA)
temp = data_in (skipped for LEA)
ax = temp
สำหรับเหตุผลที่ Intel คิดว่าคำสั่งนี้มีค่ารวมถึงฉันไม่แน่ใจ แต่ความจริงที่ว่าราคาถูกในการติดตั้งอาจเป็นปัจจัยสำคัญ อีกปัจจัยหนึ่งก็คือความจริงที่ว่าแอสเซมเบลอร์ของ Intel อนุญาตให้มีการกำหนดสัญลักษณ์ที่สัมพันธ์กับการลงทะเบียน BP หากfnord
ถูกกำหนดเป็นสัญลักษณ์ที่สัมพันธ์กับ BP (เช่น BP + 8) เราสามารถพูดได้ว่า:
mov ax,fnord ; Equivalent to "mov ax,[BP+8]"
หากใครต้องการใช้บางอย่างเช่น stosw เพื่อจัดเก็บข้อมูลไปยังที่อยู่ที่สัมพันธ์กับความสามารถในการพูด
mov ax,0 ; Data to store
mov cx,16 ; Number of words
lea di,fnord
rep movs fnord ; Address is ignored EXCEPT to note that it's an SS-relative word ptr
สะดวกกว่า:
mov ax,0 ; Data to store
mov cx,16 ; Number of words
mov di,bp
add di,offset fnord (i.e. 8)
rep movs fnord ; Address is ignored EXCEPT to note that it's an SS-relative word ptr
โปรดทราบว่าการลืมโลก "ชดเชย" จะทำให้เนื้อหาของสถานที่ตั้ง [BP + 8] แทนที่จะเพิ่มค่า 8 ลงใน DI อุ่ย
ดังที่คำตอบที่มีอยู่แล้วกล่าวถึงLEA
ข้อดีของการดำเนินการกับหน่วยความจำเลขคณิตโดยไม่ต้องเข้าถึงหน่วยความจำบันทึกผลลัพธ์ทางคณิตศาสตร์ไปยังรีจิสเตอร์อื่นแทนที่จะเป็นรูปแบบง่าย ๆ ของการเพิ่มคำสั่ง ประโยชน์ด้านประสิทธิภาพที่แท้จริงคือโปรเซสเซอร์ที่ทันสมัยมีหน่วย LEA ALU และพอร์ตแยกต่างหากสำหรับการสร้างที่อยู่ที่มีประสิทธิภาพ (รวมถึงLEA
และที่อยู่อ้างอิงหน่วยความจำอื่น ๆ ) ซึ่งหมายความว่าการดำเนินการทางคณิตศาสตร์ในLEA
และการดำเนินการทางคณิตศาสตร์อื่น ๆ ใน ALU แกน
ตรวจสอบบทความของสถาปัตยกรรม Haswell นี้เพื่อดูรายละเอียดเกี่ยวกับหน่วยงาน LEA: http://www.realworldtech.com/haswell-cpu/4/
อีกจุดที่สำคัญที่ไม่ได้กล่าวถึงในคำตอบอื่น ๆ คือLEA REG, [MemoryAddress]
คำสั่งคือ PIC (รหัสอิสระของตำแหน่ง) ซึ่งเข้ารหัสที่อยู่ PC ญาติในการอ้างอิงMemoryAddress
นี้ สิ่งนี้แตกต่างจากการMOV REG, MemoryAddress
เข้ารหัสที่อยู่เสมือนแบบสัมพัทธ์และต้องการการย้าย / แก้ไขในระบบปฏิบัติการสมัยใหม่ (เช่น ASLR เป็นคุณสมบัติทั่วไป) ดังนั้นLEA
สามารถใช้ในการแปลง PIC ที่ไม่ใช่เช่น PIC
lea
การกับ ALU เดียวกันหนึ่งตัวหรือมากกว่าที่รันคำสั่งทางคณิตศาสตร์อื่น ๆ (แต่โดยทั่วไปจะน้อยกว่าเลขคณิตอื่น ๆ ) ตัวอย่างเช่น Haswell CPU ที่กล่าวถึงสามารถดำเนินการadd
หรือsub
การดำเนินการทางคณิตศาสตร์ขั้นพื้นฐานอื่น ๆ ส่วนใหญ่ในสี่ ALUs ที่แตกต่างกันแต่สามารถดำเนินการlea
ในหนึ่ง (ซับซ้อนlea
) หรือสอง (ง่ายlea
) ที่สำคัญกว่านั้นlea
ALU ที่มีความสามารถสองตัวนั้นเป็นเพียงสองในสี่ที่สามารถดำเนินการคำสั่งอื่น ๆ ได้ดังนั้นจึงไม่มีประโยชน์ในการขนานที่อ้างว่า
คำสั่ง LEA สามารถใช้เพื่อหลีกเลี่ยงการคำนวณที่อยู่ที่มีประสิทธิภาพโดย CPU หากมีการใช้ที่อยู่ซ้ำ ๆ จะมีประสิทธิภาพมากกว่าในการจัดเก็บไว้ในการลงทะเบียนแทนการคำนวณที่อยู่ที่มีประสิทธิภาพทุกครั้งที่มีการใช้งาน
[esi]
จะไม่ค่อยมีราคาถูกกว่าการพูดและเป็นเพียงไม่ค่อยมีราคาถูกกว่า[esi + 4200]
[esi + ecx*8 + 4200]
[esi]
[esi + ecx*8 + 4200]
แต่ทำไมต้องเปรียบเทียบ พวกเขาจะไม่เทียบเท่า ถ้าคุณต้องการให้อดีตกำหนดตำแหน่งหน่วยความจำเดียวกันกับหลังคุณต้องการคำแนะนำเพิ่มเติม: คุณต้องเพิ่มesi
ค่าของการecx
คูณด้วย 8 เอ๊ะโอ้การคูณจะทำให้ธง CPU ของคุณแย่ลง! จากนั้นคุณต้องเพิ่ม 4200 คำแนะนำเพิ่มเติมเหล่านี้จะเพิ่มขนาดรหัส (ใช้พื้นที่ในแคชคำสั่ง, รอบเพื่อดึงข้อมูล)
[esi + 4200]
ซ้ำ ๆ กันในลำดับของคำสั่งนั้นจะเป็นการดีกว่าถ้าคุณโหลดที่อยู่ที่มีประสิทธิภาพลงในการลงทะเบียนและใช้มัน ตัวอย่างเช่นแทนที่จะเขียนadd eax, [esi + 4200]; add ebx, [esi + 4200]; add ecx, [esi + 4200]
คุณควรชอบlea edi, [esi + 4200]; add eax, [edi]; add ebx, [edi]; add ecx, [edi]
ซึ่งไม่ค่อยเร็วกว่า อย่างน้อยนั่นเป็นการตีความที่ธรรมดาของคำตอบนี้
[esi]
และ[esi + 4200]
(หรือ[esi + ecx*8 + 4200]
เป็นที่นี้ทำให้เข้าใจง่าย OP จะเสนอ (ตามที่ผมเข้าใจมัน): ว่าคำแนะนำ N กับที่อยู่ที่ซับซ้อนเหมือนกันจะกลายเป็นคำแนะนำ N กับง่าย (หนึ่ง reg) ที่อยู่, บวกหนึ่งlea
, เนื่องจากการระบุที่ซับซ้อนคือ "ใช้เวลานาน" ในความเป็นจริงมันช้าลงแม้ในรุ่น x86 ที่ทันสมัย แต่มีความหน่วงแฝงที่ไม่น่าจะมีความสำคัญสำหรับคำสั่งที่ต่อเนื่องซึ่งมีที่อยู่เดียวกัน
lea
เพื่อเพิ่มความกดดันในกรณีนั้น โดยทั่วไปการจัดเก็บตัวกลางเป็นสาเหตุของความกดดันในการลงทะเบียนไม่ใช่วิธีแก้ปัญหา แต่ฉันคิดว่าในสถานการณ์ส่วนใหญ่จะเป็นการล้าง @Kaz
คำสั่ง LEA (Load Effective Address) เป็นวิธีการรับที่อยู่ซึ่งเกิดขึ้นจากโหมดการกำหนดแอดเดรสหน่วยความจำของโปรเซสเซอร์ Intel
กล่าวคือหากเรามีการย้ายข้อมูลดังนี้:
MOV EAX, <MEM-OPERAND>
มันย้ายเนื้อหาของตำแหน่งหน่วยความจำที่กำหนดไว้ในการลงทะเบียนเป้าหมาย
หากเราแทนที่MOV
ด้วยLEA
ดังนั้นที่อยู่ของตำแหน่งหน่วยความจำจะถูกคำนวณในลักษณะเดียวกันโดยการ<MEM-OPERAND>
แสดงออกของที่อยู่ แต่แทนที่จะเป็นเนื้อหาของที่ตั้งหน่วยความจำเราได้สถานที่นั้นเข้าสู่ปลายทาง
LEA
ไม่ใช่คำสั่งเลขคณิตเฉพาะ เป็นวิธีการดักจับที่อยู่ที่มีประสิทธิภาพซึ่งเกิดขึ้นจากโหมดการกำหนดแอดเดรสหน่วยความจำของโปรเซสเซอร์
ตัวอย่างเช่นเราสามารถใช้ที่LEA
อยู่โดยตรงได้อย่างง่ายดาย ไม่มีเลขอะไรเกี่ยวข้องเลย:
MOV EAX, GLOBALVAR ; fetch the value of GLOBALVAR into EAX
LEA EAX, GLOBALVAR ; fetch the address of GLOBALVAR into EAX.
สิ่งนี้ถูกต้อง; เราสามารถทดสอบได้ที่พร้อมท์ Linux:
$ as
LEA 0, %eax
$ objdump -d a.out
a.out: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: 8d 04 25 00 00 00 00 lea 0x0,%eax
ที่นี่ไม่มีการเพิ่มค่าที่ปรับขนาดและไม่มีการชดเชย ศูนย์ถูกย้ายไปที่ EAX เราสามารถทำได้โดยใช้ MOV กับตัวถูกดำเนินการทันที
นี่คือเหตุผลที่คนที่คิดว่าวงเล็บในLEA
ฟุ่มเฟือยจะเข้าใจผิดอย่างรุนแรง; วงเล็บไม่ใช่LEA
ไวยากรณ์ แต่เป็นส่วนหนึ่งของโหมดการกำหนดแอดเดรส
หน่วยงาน LEA เป็นของจริงในระดับฮาร์ดแวร์ คำสั่งที่สร้างขึ้นจะเข้ารหัสโหมดการกำหนดแอดเดรสจริงและโปรเซสเซอร์ดำเนินการจนถึงจุดที่คำนวณที่อยู่ จากนั้นจะย้ายที่อยู่นั้นไปยังปลายทางแทนที่จะสร้างการอ้างอิงหน่วยความจำ (เนื่องจากการคำนวณที่อยู่ของโหมดการกำหนดแอดเดรสในคำสั่งอื่นไม่มีผลกับแฟล็ก CPU LEA
จึงไม่มีผลกับแฟล็ก CPU)
ตรงกันข้ามกับการโหลดค่าจากศูนย์ที่อยู่:
$ as
movl 0, %eax
$ objdump -d a.out | grep mov
0: 8b 04 25 00 00 00 00 mov 0x0,%eax
มันเป็นการเข้ารหัสที่คล้ายกันมากเห็นไหม เพียงแค่8d
การมีการเปลี่ยนแปลงไปLEA
8b
แน่นอนว่าการLEA
เข้ารหัสนี้มีความยาวมากกว่าการย้ายศูนย์ทันทีไปสู่EAX
:
$ as
movl $0, %eax
$ objdump -d a.out | grep mov
0: b8 00 00 00 00 mov $0x0,%eax
ไม่มีเหตุผลใดที่LEA
จะยกเว้นความเป็นไปได้นี้เพียงเพราะมีทางเลือกที่สั้นกว่า มันเป็นเพียงการรวมกันในมุมฉากกับโหมดที่อยู่ที่มีอยู่
นี่คือตัวอย่าง
// compute parity of permutation from lexicographic index
int parity (int p)
{
assert (p >= 0);
int r = p, k = 1, d = 2;
while (p >= k) {
p /= d;
d += (k << 2) + 6; // only one lea instruction
k += 2;
r ^= p;
}
return r & 1;
}
ด้วย -O (ปรับให้เหมาะสม) เป็นตัวเลือกคอมไพเลอร์ gcc จะค้นหาคำสั่ง lea สำหรับบรรทัดโค้ดที่ระบุ
ดูเหมือนว่าคำตอบจำนวนมากเสร็จสมบูรณ์แล้วฉันต้องการเพิ่มโค้ดตัวอย่างอีกหนึ่งตัวอย่างเพื่อแสดงว่าคำสั่งการย้ายและการย้ายทำงานแตกต่างกันอย่างไรเมื่อพวกเขามีรูปแบบนิพจน์เหมือนกัน
เพื่อให้เนื้อเรื่องสั้น ๆ คุณสามารถใช้คำสั่ง lea และคำสั่ง mov ได้พร้อมกับวงเล็บที่ล้อมรอบ src operand ของคำสั่ง เมื่อมีการล้อมรอบด้วย()นิพจน์ใน()จะถูกคำนวณด้วยวิธีเดียวกัน อย่างไรก็ตามสองคำสั่งจะตีความค่าที่คำนวณได้ในตัวถูกดำเนินการ src ในวิธีที่ต่างกัน
ไม่ว่าจะเป็นนิพจน์ที่ใช้กับ lea หรือ mov ค่า src จะถูกคำนวณดังนี้
D (Rb, Ri, S) => (Reg [Rb] + S * Reg [Ri] + D)
อย่างไรก็ตามเมื่อใช้กับคำสั่ง mov มันจะพยายามเข้าถึงค่าที่ชี้ไปตามที่อยู่ที่สร้างโดยนิพจน์ด้านบนและจัดเก็บไปยังปลายทาง
ในทางตรงกันข้ามเมื่อคำสั่ง lea ถูกดำเนินการด้วยนิพจน์ด้านบนมันจะโหลดค่าที่สร้างขึ้นตามไปยังปลายทาง
โค้ดด้านล่างดำเนินการคำสั่ง lea และคำสั่ง mov ด้วยพารามิเตอร์เดียวกัน อย่างไรก็ตามเพื่อจับความแตกต่างฉันได้เพิ่มตัวจัดการสัญญาณระดับผู้ใช้เพื่อตรวจจับข้อผิดพลาดในการแบ่งเซ็กเมนต์ที่เกิดจากการเข้าถึงที่อยู่ผิดเนื่องจากคำสั่ง mov
รหัสตัวอย่าง
#define _GNU_SOURCE 1 /* To pick up REG_RIP */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <signal.h>
uint32_t
register_handler (uint32_t event, void (*handler)(int, siginfo_t*, void*))
{
uint32_t ret = 0;
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;
ret = sigaction(event, &act, NULL);
return ret;
}
void
segfault_handler (int signum, siginfo_t *info, void *priv)
{
ucontext_t *context = (ucontext_t *)(priv);
uint64_t rip = (uint64_t)(context->uc_mcontext.gregs[REG_RIP]);
uint64_t faulty_addr = (uint64_t)(info->si_addr);
printf("inst at 0x%lx tries to access memory at %ld, but failed\n",
rip,faulty_addr);
exit(1);
}
int
main(void)
{
int result_of_lea = 0;
register_handler(SIGSEGV, segfault_handler);
//initialize registers %eax = 1, %ebx = 2
// the compiler will emit something like
// mov $1, %eax
// mov $2, %ebx
// because of the input operands
asm("lea 4(%%rbx, %%rax, 8), %%edx \t\n"
:"=d" (result_of_lea) // output in EDX
: "a"(1), "b"(2) // inputs in EAX and EBX
: // no clobbers
);
//lea 4(rbx, rax, 8),%edx == lea (rbx + 8*rax + 4),%edx == lea(14),%edx
printf("Result of lea instruction: %d\n", result_of_lea);
asm volatile ("mov 4(%%rbx, %%rax, 8), %%edx"
:
: "a"(1), "b"(2)
: "edx" // if it didn't segfault, it would write EDX
);
}
ผลการดำเนินการ
Result of lea instruction: 14
inst at 0x4007b5 tries to access memory at 14, but failed
=d
เพื่อบอกคอมไพเลอร์ผลที่ได้คือใน EDX, mov
ประหยัด คุณยังคงออกประกาศการอุดตันก่อนหน้านี้ในการส่งออก สิ่งนี้แสดงให้เห็นถึงสิ่งที่คุณพยายามแสดง แต่ก็เป็นตัวอย่างที่ไม่ดีของอินไลน์ asm ที่ทำให้เข้าใจผิดซึ่งจะแตกถ้าใช้ในบริบทอื่น ๆ นั่นเป็นสิ่งที่ไม่ดีสำหรับคำตอบล้นสแต็ค
%%
ชื่อลงทะเบียนทั้งหมดใน Extended asm ให้ใช้ข้อ จำกัด การป้อนข้อมูล asm("lea 4(%%ebx, %%eax, 8), %%edx" : "=d"(result_of_lea) : "a"(1), "b"(2));
เช่น การให้ผู้ลงทะเบียนคอมไพเลอร์เริ่มต้นหมายความว่าคุณไม่ต้องประกาศตัวอุดตัน คุณกำลังทำเรื่องซับซ้อนด้วย xor-zeroing ก่อนที่ mov-ทันทีจะเขียนทับการลงทะเบียนทั้งหมดเช่นกัน
mov 4(%ebx, %eax, 8), %edx
ไม่ถูกต้องที่ไหน อย่างไรก็ตามใช่เพราะmov
มันสมเหตุสมผลแล้วที่จะเขียน"a"(1ULL)
เพื่อบอกคอมไพเลอร์ว่าคุณมีค่า 64- บิตดังนั้นจึงจำเป็นต้องตรวจสอบให้แน่ใจว่าได้ขยายเวลาเพื่อเติมเต็มการลงทะเบียนทั้งหมด ในทางปฏิบัติมันจะยังคงใช้อยู่mov $1, %eax
เพราะการเขียน EAX จะขยายเข้าไปใน RAX เป็นศูนย์เว้นแต่ว่าคุณมีสถานการณ์แปลก ๆ ของรหัสโดยรอบที่คอมไพเลอร์รู้ว่า RAX = 0xff00000001
หรือบางอย่าง สำหรับlea
คุณยังคงใช้ตัวถูกดำเนินการขนาด 32 บิตดังนั้นบิตเรจิสเรย์ที่สูงในเรจิสเตอร์อินพุตจึงไม่มีผลกับผลลัพธ์แบบ 32 บิต
LEA: แค่คำสั่ง "คณิตศาสตร์" ..
MOV ถ่ายโอนข้อมูลระหว่างตัวถูกดำเนินการ แต่หน่วยกำลังเริ่มต้นกำลังคำนวณ
mov eax, offset GLOBALVAR
แทน คุณสามารถใช้ LEA แต่มันมีขนาดรหัสที่ใหญ่กว่าmov r32, imm32
และทำงานในพอร์ตที่น้อยลงเล็กน้อยเนื่องจากยังคงผ่านกระบวนการคำนวณที่อยู่ lea reg, symbol
มีประโยชน์เฉพาะใน 64 บิตสำหรับ LEA ที่สัมพันธ์กับ RIP เมื่อคุณต้องการ PIC และ / หรือที่อยู่นอก 32 บิตต่ำ ในรหัส 32 หรือ 16 บิตไม่มีข้อได้เปรียบ LEA เป็นคำสั่งทางคณิตศาสตร์ที่แสดงถึงความสามารถของ CPU ในการถอดรหัส / คำนวณโหมดการกำหนดแอดเดรส
imul eax, edx, 1
ได้ว่าไม่ได้คำนวณ: มันแค่คัดลอก edx ไปยัง eax แต่จริงๆแล้วมันจะเรียกใช้ข้อมูลของคุณผ่านตัวคูณด้วยเวลาแฝง 3 รอบ หรือrorx eax, edx, 0
เพียงแค่คัดลอก (หมุนเป็นศูนย์)
คำแนะนำ "การคำนวณ" ปกติทั้งหมดเช่นการเพิ่มการคูณแบบเอกสิทธิ์เฉพาะบุคคลหรือตั้งค่าสถานะสถานะเช่นศูนย์เครื่องหมาย หากคุณใช้แอดเดรสที่ซับซ้อนAX xor:= mem[0x333 +BX + 8*CX]
แฟล็กจะถูกตั้งค่าตามการดำเนินการ xor
ตอนนี้คุณอาจต้องการใช้ที่อยู่หลายครั้ง การโหลดที่อยู่ดังกล่าวไปยังการลงทะเบียนไม่ได้มีจุดประสงค์เพื่อตั้งค่าสถานะและโชคดีที่ไม่มี วลี "ที่อยู่ที่มีประสิทธิภาพในการโหลด" ทำให้โปรแกรมเมอร์รับรู้สิ่งนั้น นั่นคือสิ่งที่การแสดงออกแปลก ๆ มาจาก
เป็นที่ชัดเจนว่าเมื่อโปรเซสเซอร์สามารถใช้ที่อยู่ที่ซับซ้อนในการประมวลผลเนื้อหาได้ก็สามารถคำนวณได้เพื่อวัตถุประสงค์อื่น แน่นอนมันสามารถใช้ในการทำการเปลี่ยนแปลงx <- 3*x+1
ในคำสั่งเดียว นี่เป็นกฎทั่วไปในการเขียนโปรแกรมการประกอบ: ใช้คำแนะนำ แต่มันสั่นสะเทือนเรือของคุณ
สิ่งเดียวที่นับได้ก็คือการเปลี่ยนแปลงที่เป็นตัวเป็นตนโดยคำสั่งนั้นมีประโยชน์สำหรับคุณหรือไม่
บรรทัดล่าง
MOV, X| T| AX'| R| BX|
และ
LEA, AX'| [BX]
มีผลเหมือนกันกับAXแต่ไม่ส่งผลต่อสถานะ (นี่คือสัญกรณ์ciasdis )
call lbl
lbl: pop rax
"การทำงาน" ในทางเทคนิคเพื่อให้ได้มาซึ่งคุณค่าrip
แต่คุณจะทำให้การคาดคะเนสาขาไม่มีความสุข ใช้คำแนะนำที่คุณต้องการ แต่ไม่ต้องแปลกใจถ้าคุณทำอะไรที่ยุ่งยากและมันมีผลตามมาที่คุณไม่คาดคิด
ยกโทษให้ฉันถ้ามีคนพูดถึงแล้ว แต่ในยุค x86 เมื่อการแบ่งส่วนหน่วยความจำยังคงมีความเกี่ยวข้องคุณอาจไม่ได้ผลลัพธ์เดียวกันจากคำแนะนำสองคำนี้:
LEA AX, DS:[0x1234]
และ
LEA AX, CS:[0x1234]
seg:off
คู่ LEA ไม่ได้รับผลกระทบจากฐานกลุ่ม ทั้งคำแนะนำเหล่านั้น (อย่างไม่มีประสิทธิภาพ) จะใส่0x1234
ลงใน AX x86 น่าเสียดายที่ไม่มีวิธีง่ายๆในการคำนวณที่อยู่เชิงเส้นแบบเต็ม (ฐาน + ส่วนที่มีประสิทธิภาพ) ลงในทะเบียนหรือคู่