ฟังก์ชั่นของคำสั่ง push / pop ที่ใช้กับ register ใน x86 assembly คืออะไร?


101

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

  • คุณจะกดลงทะเบียนได้อย่างไร? มันดันอยู่ตรงไหน? เหตุใดจึงจำเป็น
  • สิ่งนี้ทำให้คำสั่งโปรเซสเซอร์ตัวเดียวเดือดหรือซับซ้อนกว่านี้หรือไม่?

3
คำเตือน: คำตอบปัจจุบันทั้งหมดมีอยู่ในไวยากรณ์การประกอบของ Intel Push-pop ใน AT & T ไวยากรณ์สำหรับตัวอย่างที่ใช้โพสต์แก้ไขเช่นb, w, lหรือqเพื่อแสดงขนาดของหน่วยความจำที่ถูกครอบงำ เช่นpushl %eaxและpopl %eax
Hawken

5
@hawken ในแอสเซมเบลอร์ส่วนใหญ่สามารถกลืนไวยากรณ์ AT&T (โดยเฉพาะก๊าซ) ขนาด postfix สามารถละเว้นได้หากสามารถอนุมานขนาดตัวถูกดำเนินการออกจากขนาดตัวถูกดำเนินการได้ นี่เป็นกรณีของตัวอย่างที่คุณให้ไว้เนื่องจาก%eaxมีขนาด 32 บิตเสมอ
Gunther Piez

คำตอบ:


153

การผลักค่า (ไม่จำเป็นต้องเก็บไว้ในรีจิสเตอร์) หมายถึงการเขียนลงในสแต็ก

poppingหมายถึงการกู้คืนสิ่งที่อยู่ด้านบนของสแต็กลงในรีจิสเตอร์ นี่คือคำแนะนำพื้นฐาน:

push 0xdeadbeef      ; push a value to the stack
pop eax              ; eax is now 0xdeadbeef

; swap contents of registers
push eax
mov eax, ebx
pop ebx

5
ตัวถูกดำเนินการที่ชัดเจนสำหรับการผลักดันและป๊อปจะไม่เพียงลงทะเบียนเพื่อให้คุณสามารถr/m push dword [esi]หรือแม้กระทั่งpop dword [esp]การโหลดแล้วเก็บค่าเดิมกลับไปยังที่อยู่เดิม ( github.com/HJLebbink/asm-dude/wiki/POP ) ฉันพูดถึงเรื่องนี้เพราะคุณบอกว่า "ไม่จำเป็นต้องเป็นทะเบียน"
Peter Cordes

2
คุณยังสามารถpopเข้าไปในพื้นที่แห่งความทรงจำ:pop [0xdeadbeef]
SS Anne

สวัสดีความแตกต่างระหว่าง push / pop และ pushq / popq คืออะไร? ฉันใช้ macOS / intel
SteakOverflow

46

นี่คือวิธีที่คุณกดลงทะเบียน ฉันคิดว่าเรากำลังพูดถึง x86

push ebx
push eax

มันถูกผลักลงบนกอง ค่าของESPรีจิสเตอร์จะลดลงตามขนาดของค่าที่ผลักเมื่อสแต็กเติบโตลงในระบบ x86

จำเป็นต้องรักษาค่า การใช้งานทั่วไปคือ

push eax           ;   preserve the value of eax
call some_method   ;   some method is called which will put return value in eax
mov  edx, eax      ;    move the return value to edx
pop  eax           ;    restore original eax

A pushคือคำสั่งเดียวใน x86 ซึ่งทำสองสิ่งภายใน

  1. ลดการESPลงทะเบียนตามขนาดของค่าที่ผลัก
  2. จัดเก็บค่าพุชตามที่อยู่ปัจจุบันของการESPลงทะเบียน

@vavan เพิ่งส่งคำขอให้แก้ไข
jgh fun-run

38

มันดันอยู่ตรงไหน?

esp - 4. อย่างแม่นยำมากขึ้น:

  • esp ถูกลบด้วย 4
  • ค่าจะถูกส่งไปที่ esp

pop ย้อนกลับสิ่งนี้

System V ABI บอกให้ Linux rspชี้ไปยังตำแหน่งสแต็กที่เหมาะสมเมื่อโปรแกรมเริ่มทำงาน: สถานะการลงทะเบียนเริ่มต้นคืออะไรเมื่อเปิดโปรแกรม (asm, linux) ซึ่งเป็นสิ่งที่คุณควรใช้เป็นประจำ

คุณจะกดลงทะเบียนได้อย่างไร?

ตัวอย่าง GNU GAS ขั้นต่ำ:

.data
    /* .long takes 4 bytes each. */
    val1:
        /* Store bytes 0x 01 00 00 00 here. */
        .long 1
    val2:
        /* 0x 02 00 00 00 */
        .long 2
.text
    /* Make esp point to the address of val2.
     * Unusual, but totally possible. */
    mov $val2, %esp

    /* eax = 3 */
    mov $3, %ea 

    push %eax
    /*
    Outcome:
    - esp == val1
    - val1 == 3
    esp was changed to point to val1,
    and then val1 was modified.
    */

    pop %ebx
    /*
    Outcome:
    - esp == &val2
    - ebx == 3
    Inverses push: ebx gets the value of val1 (first)
    and then esp is increased back to point to val2.
    */

ข้างต้นใน GitHub พร้อมการยืนยันที่รันได้

เหตุใดจึงจำเป็น

มันเป็นความจริงว่าคำแนะนำเหล่านั้นสามารถดำเนินการได้อย่างง่ายดายผ่านทางmov, และaddsub

เหตุผลเหล่านี้มีอยู่นั่นคือชุดคำสั่งเหล่านั้นเกิดขึ้นบ่อยมากจน Intel ตัดสินใจจัดเตรียมคำแนะนำเหล่านี้ให้กับเรา

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

เพื่อทำความเข้าใจปัญหาให้ลองรวบรวมรหัส C ด้วยตนเอง

ปัญหาสำคัญคือการตัดสินใจว่าจะจัดเก็บตัวแปรแต่ละตัวไว้ที่ใด

ตามหลักการแล้วตัวแปรทั้งหมดจะพอดีกับรีจิสเตอร์ซึ่งเป็นหน่วยความจำที่เร็วที่สุดในการเข้าถึง (ปัจจุบันเร็วกว่า RAM ประมาณ100เท่า)

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

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

จากนั้นเราให้คอมไพเลอร์ปรับแต่งการจัดสรรรีจิสเตอร์ให้เหมาะสมที่สุดเนื่องจาก NP นั้นสมบูรณ์และเป็นส่วนที่ยากที่สุดในการเขียนคอมไพเลอร์ ปัญหานี้จะเรียกว่าการจัดสรรลงทะเบียนและมันก็เป็นไป isomorphic กราฟสี

เมื่อจัดสรรคอมไพเลอร์จะถูกบังคับให้สิ่งที่เก็บไว้ในหน่วยความจำแทนเพียงลงทะเบียนที่เป็นที่รู้จักกันการรั่วไหล

สิ่งนี้ทำให้คำสั่งโปรเซสเซอร์ตัวเดียวเดือดหรือซับซ้อนกว่านี้หรือไม่?

สิ่งที่เรารู้แน่นอนก็คือ Intel จัดทำเอกสาร a pushและpopคำสั่งดังนั้นจึงเป็นคำสั่งเดียวในแง่นั้น

ภายในสามารถขยายเป็นไมโครโค้ดได้หลายตัวตัวหนึ่งต้องปรับเปลี่ยนespและอีกอันใช้ทำหน่วยความจำ IO และใช้เวลาหลายรอบ

แต่ก็เป็นไปได้เช่นกันว่า single pushจะเร็วกว่าคำสั่งอื่น ๆ ที่ใช้ร่วมกันเนื่องจากมีความเฉพาะเจาะจงมากกว่า

สิ่งนี้ส่วนใหญ่เป็นเอกสารที่ไม่มี (der):


4
คุณไม่จำเป็นต้องเดาเกี่ยวกับวิธีpush/ popถอดรหัสเป็น uops ขอขอบคุณที่เคาน์เตอร์วัดประสิทธิภาพการทดสอบการทดลองเป็นไปได้และAgner หมอกได้ทำมันและตารางการเรียนการสอนการตีพิมพ์ Pentium-M และซีพียูรุ่นหลังมี single-uop push/ popต้องขอบคุณ stack engine (ดู microarch pdf ของ Agner) ซึ่งรวมถึงซีพียู AMD รุ่นล่าสุดด้วยข้อตกลงการแบ่งปันสิทธิบัตรของ Intel / AMD
Peter Cordes

@PeterCordes สุดยอด! ดังนั้นตัวนับประสิทธิภาพจึงได้รับการบันทึกโดย Intel เพื่อนับการดำเนินการขนาดเล็ก?
Ciro Santilli 郝海东冠状病六四事件法轮功

นอกจากนี้ตัวแปรท้องถิ่นที่รั่วไหลจาก regs มักจะยังคงร้อนอยู่ในแคช L1 หากมีการใช้งานจริง แต่การอ่านจากการลงทะเบียนนั้นฟรีอย่างมีประสิทธิภาพและไม่มีเวลาแฝง ดังนั้นจึงเร็วกว่าแคช L1 อย่างไม่มีที่สิ้นสุดขึ้นอยู่กับว่าคุณต้องการกำหนดเงื่อนไขอย่างไร สำหรับภาษาท้องถิ่นแบบอ่านอย่างเดียวที่ล้นไปที่สแต็กค่าใช้จ่ายหลักเป็นเพียงการโหลดเพิ่มเติม (บางครั้งตัวถูกดำเนินการหน่วยความจำบางครั้งอาจมีการmovโหลดแยกกัน) สำหรับตัวแปร non-const ที่รั่วไหลการเดินทางไปกลับของการส่งต่อร้านค้าเป็นเวลาแฝงที่มากขึ้น (ค่า ~ 5c พิเศษเทียบกับการส่งต่อโดยตรงและคำแนะนำร้านค้าไม่ถูก)
Peter Cordes

ใช่มีตัวนับสำหรับ uops ทั้งหมดในขั้นตอนไปป์ไลน์ที่แตกต่างกันสองสามขั้นตอน (ปัญหา / ดำเนินการ / เกษียณ) ดังนั้นคุณสามารถนับโดเมนที่หลอมรวมหรือโดเมนที่ไม่ได้ใช้ ดูคำตอบนี้เป็นตัวอย่าง ถ้าฉันเขียนคำตอบนั้นใหม่ตอนนี้ฉันจะใช้ocperf.pyสคริปต์ wrapper เพื่อรับชื่อสัญลักษณ์ที่ง่ายสำหรับตัวนับ
Peter Cordes

23

การผลักดันและการลงทะเบียน popping อยู่เบื้องหลังเทียบเท่ากับสิ่งนี้:

push reg   <= same as =>      sub  $8,%rsp        # subtract 8 from rsp
                              mov  reg,(%rsp)     # store, using rsp as the address

pop  reg    <= same as=>      mov  (%rsp),reg     # load, using rsp as the address
                              add  $8,%rsp        # add 8 to the rsp

โปรดทราบว่านี่คือไวยากรณ์ x86-64 At & t

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


4
ใช่ลำดับเหล่านั้นเลียนแบบพุช / ป๊อปอย่างถูกต้อง (ยกเว้น push / pop ไม่มีผลกับแฟล็ก)
Peter Cordes

2
คุณควรใช้lea rsp, [rsp±8]แทนadd/ subเพื่อเลียนแบบเอฟเฟกต์ของpush/ popบนแฟล็กได้ดีขึ้น
Ruslan

12

ซีพียูเกือบทั้งหมดใช้ stack กองโปรแกรมเป็นเทคนิคLIFOพร้อมฮาร์ดแวร์ที่รองรับการจัดการ

กองซ้อนคือจำนวนหน่วยความจำโปรแกรม (RAM) โดยปกติจะจัดสรรไว้ที่ด้านบนของฮีปหน่วยความจำ CPU และขยาย (ที่คำสั่ง PUSH ตัวชี้สแต็กจะลดลง) ในทิศทางตรงกันข้าม คำมาตรฐานสำหรับการแทรกเข้าไปในสแต็คเป็นPUSHและลบออกจากสแต็คเป็นPOP

สแต็คถูกจัดการผ่านสแต็กที่ต้องการลงทะเบียน CPU หรือที่เรียกว่าตัวชี้สแต็กดังนั้นเมื่อ CPU ดำเนินการPOPหรือPUSHตัวชี้สแต็กจะโหลด / จัดเก็บรีจิสเตอร์หรือค่าคงที่ลงในหน่วยความจำสแต็กและตัวชี้สแต็กจะลด x หรือเพิ่มขึ้นโดยอัตโนมัติตามจำนวนคำที่พุช หรือปรากฏใน (จาก) สแต็ก

ผ่านคำแนะนำแอสเซมเบลอร์เราสามารถจัดเก็บลงในกองซ้อน:

  1. CPU ลงทะเบียนและค่าคงที่
  2. ส่งคืนที่อยู่สำหรับฟังก์ชันหรือขั้นตอน
  3. ฟังก์ชัน / โพรซีเดอร์เข้า / ออกตัวแปร
  4. ฟังก์ชัน / โพรซีเดอร์ตัวแปรท้องถิ่น
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.