สแกนจำนวนเต็มและพิมพ์ช่วงเวลา (1, จำนวนเต็ม) ใน NASM

ฉันกำลังพยายามเรียนรู้ภาษาแอสเซมบลีจาก Linux Ubuntu 16.04 x64 ตอนนี้ฉันมีปัญหาดังต่อไปนี้: - สแกนจำนวนเต็ม n แล้วพิมพ์ตัวเลขตั้งแต่ 1 ถึง n

สำหรับ n = 5 ฉันควรมี 1 2 3 4 5 ฉันพยายามทำโดยใช้ scanf และ printf แต่หลังจากที่ฉันป้อนหมายเลข มันก็ออก

รหัสคือ:

;nasm -felf64 code.asm && gcc code.o && ./a.out

SECTION .data
    message1: db "Enter the number: ",0
    message1Len: equ $-message1
    message2: db "The numbers are:", 0
    formatin: db "%d",0
    formatout: db "%d",10,0 ; newline, nul
    integer: times 4 db 0 ; 32-bits integer = 4 bytes

SECTION .text
    global main
    extern scanf
    extern printf

main:

    mov eax, 4
    mov ebx, 1
    mov ecx, message1
    mov edx, message1Len
    int 80h

    mov rdi, formatin
    mov rsi, integer
    mov al, 0
    call scanf
    int 80h

    mov rax, integer
    loop:
        push rax
        push formatout
        call printf
        add esp, 8
        dec rax
    jnz loop

    mov rax,0

ret

ฉันรู้ว่าในลูปนี้ ฉันจะมีเอาต์พุตผกผัน (5 4 3 2 1 0) แต่ฉันไม่รู้ว่าจะตั้งเงื่อนไขอย่างไร

คำสั่งที่ฉันใช้มีดังต่อไปนี้:

nasm -felf64 code.asm && gcc code.o && ./a.out

คุณช่วยฉันค้นหาว่าฉันผิดพลาดตรงไหนได้ไหม?


person lidia901    schedule 19.11.2017    source แหล่งที่มา
comment
ทำไมคุณถึง push อาร์กิวเมนต์สำหรับ printf ในสแต็ก คุณใช้แหล่งข้อมูลใดในการดำเนินการเช่นนี้ (ฉันสงสัยว่าคุณกำลังเปลี่ยนโค้ด 32b/บทช่วยสอนเป็น 64b แต่นั่นจะไม่ได้ผลง่ายๆ มันซับซ้อนกว่า... ในตอนนี้ หากคุณมีทรัพยากร asm 32b ที่ดีสำหรับการเรียนรู้ การสอนจะง่ายกว่ามาก วิธีสร้างไบนารี 32b ภายใต้ 64b linux และทำงานกับสิ่งนั้นแทน) ... อย่างใดอย่างหนึ่ง หรือทรัพยากร 64b ของคุณมีคุณภาพต่ำ และลองใช้สิ่งที่ดีกว่านี้...   -  person Ped7g    schedule 20.11.2017
comment
@ Ped7g ฉันอาจสับสน 32b กับ 64b ... ฉันแก้ไขทุกอย่าง แต่ฉันยังคงมีปัญหากับฟังก์ชัน printf ในลูป ... และฉันไม่พบเอกสารประกอบสำหรับมัน มันยากกว่าที่ฉันคาดไว้ :D   -  person lidia901    schedule 20.11.2017
comment
คุณสามารถแก้ไขคำถามของคุณและแสดงแหล่งที่มา + บรรทัดคำสั่งตัวแปร 32b ว่าคุณสร้างมันได้อย่างไร printf เป็นหนึ่งในวิธีที่ซับซ้อนกว่าในการทำให้ถูกต้อง เนื่องจากมีจำนวนอาร์กิวเมนต์ที่แปรผัน ดังนั้นคุณจำเป็นต้องรู้รูปแบบการเรียกที่เหมาะสมนอกเหนือจากพื้นฐาน คุณสามารถตรวจสอบบทช่วยสอนนี้ (ดูเหมือนว่าจะกำหนดเป้าหมายตรง nasm+libc+32b และดูเหมือนว่าจะได้รับการแสดงความคิดเห็นที่ดี): csee.umbc.edu/portal/help/nasm/sample.shtml และหากคุณเพิ่งเริ่มต้นด้วยการประกอบ ฉันจะข้ามการเรียกใช้ฟังก์ชัน libc ไปเลย และลองเล่นกับ x86 ล้วนๆ คำแนะนำ (ทำคณิตศาสตร์) ตรวจสอบค่าในดีบักเกอร์เท่านั้น   -  person Ped7g    schedule 20.11.2017
comment
และยังมีลิงก์ตัวอย่าง 64b ด้วย แต่อีกครั้ง หากคุณเพิ่งเริ่มต้นด้วยแอสเซมบลี ฉันขอแนะนำให้ใช้ 32b ต่อไป (ตราบใดที่มีการเรียก libc ที่เกี่ยวข้อง สำหรับ x86-64 asm ล้วนๆ โดยไม่มีการโทรจากภายนอก - 64b มีขนาดเล็กเท่านั้น ซับซ้อนกว่าเล็กน้อย มันเป็นแบบแผนการโทรเองซึ่งยุ่งยากกว่า 32b มาก คุณต้องจัดแนวสแต็กไว้ก่อนการโทรแต่ละครั้ง และมีฟีเจอร์โซนสีแดง ฯลฯ .. ) แต่ความสามารถในการใช้ดีบักเกอร์เพื่อข้ามขั้นตอนเดียวและตรวจสอบสถานะของรีจิสเตอร์/แฟล็ก/หน่วยความจำเป็นสิ่งสำคัญ สำคัญกว่าการเรียก printf มาก   -  person Ped7g    schedule 20.11.2017
comment
ยากกว่าที่ฉันคาดไว้ - ในการประกอบ คุณสามารถควบคุมเครื่องจักรได้เต็มที่ ดังนั้นคุณจึงสามารถบอกให้มันทำอะไรก็ได้ ว่ามันทำอะไรได้บ้าง ซึ่งหมายความว่า สำหรับทุกการดำเนินการทางกฎหมาย+ที่ต้องการ คุณมีวิธีการเขียนที่ถูกต้องประมาณสิบวิธี และวิธีที่ไม่ถูกต้องหลายพันรายการ ซึ่งดูเป็นความคิดที่ดีเมื่อคุณเขียนมัน คุณต้องเรียนรู้ที่จะแม่นยำในทุกขั้นตอน ตั้งแต่การกำหนดสิ่งที่คุณต้องการบรรลุ วิธีที่คุณต้องการบรรลุผล และเหตุใดแต่ละคำสั่งในโค้ดจึงอยู่ในนั้น จากนั้นคุณต้องเรียนรู้ที่จะอ่านสิ่งนั้นอีกครั้งและเปรียบเทียบกับ ตรวจสอบความเป็นจริงในดีบักเกอร์แล้วแก้ไข   -  person Ped7g    schedule 20.11.2017
comment
@ Ped7g สำคัญไหมที่ระบบปฏิบัติการของฉันเป็นแบบ 64 บิต? ฉันคิดอย่างนั้นและนั่นคือสาเหตุที่ฉันเริ่มด้วยโปรแกรม 64b   -  person lidia901    schedule 20.11.2017
comment
คือ .. เกี่ยวกับการแก้ไขคำถามและแสดงแหล่งที่มาของตัวแปร 32b .. ฉันทำไม่ได้อีกต่อไปเพราะฉันแก้ไขเหมือนทุกอย่างและฉันไม่พบมันอีกต่อไป แต่ฉันสามารถแสดงสถานะที่แท้จริงของโปรแกรมได้หากช่วยได้   -  person lidia901    schedule 20.11.2017
comment
ใช่อาจเป็นไปได้ แต่โดยปกติแล้ว 64b linux สามารถเรียกใช้ไบนารี 32b ด้วยเลเยอร์ความเข้ากันได้ 32b (64b ubuntu WSL ที่บรรจุใน windows 10 ไม่สามารถเรียกใช้ไบนารี 32b ได้) ดังนั้นการติดตั้ง 64b linux ธรรมดาจึงมีแนวโน้มมาก พร้อมที่จะผลิต + รัน +debug ไบนารี 32b (หรือคุณจะต้องติดตั้งแพ็คเกจเพิ่มเติมเพียงไม่กี่แพ็คเกจเพื่อให้รองรับ 32b ใน gcc/etc) Linux Ubuntu 16.04 x64 ธรรมดาสามารถตั้งค่าให้ทำงานกับ 32b ได้อย่างแน่นอน (ฉันเองอยู่ในการกระจายนีออนของ KDE ตาม 16.04 ตรวจสอบ nasm Q+A สำหรับทั้ง 32b และ 64b asm ได้อย่างง่ายดายโดยใช้ edb-debugger ที่สร้างจากแหล่งที่มาจาก github ).   -  person Ped7g    schedule 20.11.2017
comment
stackoverflow.com/a/36901649/4271923 (อืม จริง ๆ แล้วมันเป็น gcc+ มากเกินไป แต่ค้นหาตามบรรทัดเหล่านี้ nasm 32b linux 64b ควรให้บางสิ่งที่เน้น nasm มากขึ้นในลิงก์ไม่กี่ลิงก์)   -  person Ped7g    schedule 20.11.2017
comment
ขอบคุณมาก @Ped7g! คุณใจดีมาก ๆ! :) ฉันจะพยายามเรียนรู้ก่อนบน 32 บิต   -  person lidia901    schedule 20.11.2017
comment
เกี่ยวกับการเรียนรู้ 32b ก่อน -› ไม่ต้องกังวล ในแง่ของคำสั่ง x86 ล้วนๆ ขั้นตอนจาก 32b ถึง 64b นั้นไม่ใหญ่มาก (รีจิสเตอร์บางอันเพิ่มเติม บางรีจิสเตอร์ไม่พร้อมใช้งาน กฎพิเศษบางอย่างเกี่ยวกับการใช้งาน 32b reg นั่นคือเกือบทั้งหมด) . เป็นเพียงรูปแบบการเรียกบนระบบ 64b ที่ดีกว่ามาก (ในแง่ของประสิทธิภาพ) และซับซ้อน มันยากขึ้นเล็กน้อยในการติดตามสำหรับมนุษย์ (ซึ่งไม่สำคัญเมื่อออกแบบมัน เนื่องจาก 99% ของโค้ดถูกสร้างขึ้นโดยคอมไพเลอร์ ในขณะที่ประสิทธิภาพ เป็นสิ่งสำคัญ)   -  person Ped7g    schedule 20.11.2017
comment
ตอนนี้ฉันจำได้ว่าฉันได้เพิ่มคำตอบล่าสุดซึ่งเป็นตัวอย่างการทำงานเต็มรูปแบบสำหรับ nasm 32b ที่ผสมกับ clib printf: stackoverflow.com/questions/47362660/ ... อย่าลังเลที่จะถามหากมีสิ่งใดไม่ชัดเจนหรือเหมาะกับคุณ (เกี่ยวกับบรรทัดคำสั่งที่ยาว ... ดูอาจจะน่าเบื่อ แต่ฉันใช้โปรแกรมแก้ไขข้อความ Kate พร้อมการตั้งค่าวิธี build ดังนั้นฉันจึงไม่สนใจชื่อที่ยาวเหล่านั้น ... จากนั้นอีกครั้งคุณสามารถจัดเก็บคำสั่งเหล่านั้นในเชลล์สคริปต์ได้ หรือแม้แต่สร้างไฟล์) น่าเศร้าที่ฉันไม่ได้สนใจที่จะเพิ่มตัวแปร 64b   -  person Ped7g    schedule 20.11.2017
comment
และฉันจำมันในทางที่ผิด มันเป็นอีกทางหนึ่ง รหัส C++ เรียกแอสเซมเบลอร์ และฉันก็ลองใช้แบบแผน fastcall โดยตั้งใจ... ดังนั้นมันไม่เกี่ยวกับการเรียก printf จากแอสเซมบลี .. ขออภัย :D.. ยังมีอยู่ มีบทเรียนมากมายบนอินเทอร์เน็ต และฉันเหนื่อยเกินกว่าจะเขียนคำตอบทั้งหมดที่นี่   -  person Ped7g    schedule 20.11.2017
comment
@ Ped7g: คำตอบของฉันใน stackoverflow.com/a/36901649/4271923 ที่คุณเชื่อมโยงก่อนหน้านี้มีส่วน NASM แต่ OP ใช้การลงทะเบียนแบบ 64 บิตและการเรียกแบบแผน ดังนั้นข้อผิดพลาดที่แท้จริงอาจใช้ int 0x80 ในโหมด 64 บิต (stackoverflow.com /คำถาม/46087730/) จริงๆ แล้วปัญหาไม่ได้อยู่ที่การตัดสินใจว่าจะใช้การเรียกของระบบหรือฟังก์ชั่นไลบรารี stdio และแย่กว่านั้นคือการใช้ call scanf / int 80h ดังนั้นหมายเลข syscall จึงถูกกำหนดโดยค่าส่งคืน scanf!!!!   -  person Peter Cordes    schedule 20.11.2017


คำตอบ (2)


มีปัญหาหลายประการ:
1. พารามิเตอร์ที่จะ printf ตามที่กล่าวไว้ในความคิดเห็น ใน x86-64 พารามิเตอร์สองสามตัวแรกจะถูกส่งผ่านในรีจิสเตอร์
2. printf ไม่รักษาค่าของ eax
3. สแต็กไม่ตรงแนว
4. rbx ถูกใช้โดยไม่บันทึกค่าของผู้โทร ค่า
5. กำลังโหลดที่อยู่ของ integer แทนที่จะเป็นค่า
6. เนื่องจาก printf เป็นฟังก์ชัน varargs จึงต้องตั้งค่า eax เป็น 0 ก่อนการโทร
7. Spurious int 80h หลัง การโทรไปที่ scanf

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

main:
    push rbx           ; This fixes problems 3 and 4.

    mov eax, 4
    mov ebx, 1
    mov ecx, message1
    mov edx, message1Len
    int 80h

    mov rdi, formatin
    mov rsi, integer
    mov al, 0
    call scanf

    mov ebx, [integer] ; fix problems 2 and 5
    loop:
        mov rdi, formatout   ; fix problem 1
        mov esi, ebx
        xor eax, eax   ; fix problem 6
        call printf
        dec ebx
    jnz loop

    pop rbx            ; restore caller's value
    mov rax,0

ret

ป.ล. หากต้องการให้นับขึ้นแทนที่จะนับลง ให้เปลี่ยนการวนซ้ำดังนี้:

    mov ebx, 1
    loop:
        <call printf>
        inc ebx
        cmp ebx, [integer]
    jle loop
person prl    schedule 20.11.2017
comment
int 80h ABI แบบ 32 บิตสำหรับ sys_write ในโค้ด 64 บิตนั้นไม่ผิดทางเทคนิค แต่ syscall แบบ 64 บิตจะดีกว่ามาก (นอกจากนี้ คุณไม่ได้พูดถึงปัญหา 0 ซึ่งทำให้โปรแกรมออกจากจริง: int 80h โดยที่ eax = ค่าส่งคืนของ scanf = 1 = __NR_exit (ดูคำตอบของฉัน) - person Peter Cordes; 20.11.2017
comment
@PeterCordes อีกปัญหาหนึ่งคือการผสมฟังก์ชัน clib IO เข้ากับ sys_write ... ฉันหมายถึงฉันเหนื่อยเกินกว่าจะแก้ไขได้อย่างสมบูรณ์ เนื่องจากมีข้อผิดพลาดเกิดขึ้นมากมาย ดังนั้นฉันจึงพยายามเสนอขั้นตอนเล็กๆ น้อยๆ สำหรับการเริ่มต้น (จริงๆ แล้วฉันเกรงว่าการแก้ไขจะมีการเปลี่ยนแปลงไปมากจนยากที่จะเข้าใจเลย โดยไม่ต้องทำตามขั้นตอนเล็กๆ น้อยๆ เหล่านั้นก่อน) - person Ped7g; 20.11.2017
comment
@ Ped7g: มันปลอดภัยจริง ๆ ถ้าคุณ sys_write ก่อน ใช้ฟังก์ชันไลบรารี stdio ใด ๆ ที่อาจบัฟเฟอร์ I/O แทนที่จะทำก่อนส่งคืน แต่ใช่ มันเป็นสิ่งที่ต้องเตือนอย่างแน่นอน - person Peter Cordes; 20.11.2017
comment
@prl ฉันคิดว่ามันควรจะเป็น jge loop ในตอนท้ายใช่ไหม? นอกจากนี้ ขอบคุณมาก! - person lidia901; 20.11.2017
comment
ใช่ ฉันเขียนการเปรียบเทียบย้อนหลัง อันตรายจากการถูกบังคับให้อ่านไวยากรณ์ของ AT&T บ่อยครั้ง ฉันจะแก้ไขมันโดยการกลับการเปรียบเทียบ แทนที่จะเป็นสาขาแบบมีเงื่อนไข - person prl; 20.11.2017

คุณกำลังเรียก scanf อย่างถูกต้อง โดยใช้หลักการเรียก x86-64 System V โดยปล่อยให้ค่าที่ส่งคืนเป็น eax หลังจากการแปลงตัวถูกดำเนินการหนึ่งรายการสำเร็จ (%d) ก็จะกลับมาพร้อมกับ eax = 1

... correct setup for scanf, including zeroing AL.

call scanf    ; correct
int 80h       ; insane: system call with eax = scanf return value

จากนั้นคุณเรียกใช้ int 80h ซึ่งจะทำการเรียกของระบบ ABI รุ่นเก่าแบบ 32 บิต โดยใช้ eax=1 เป็นโค้ดเพื่อพิจารณาว่าการเรียกของระบบ ใด (ดู จะเกิดอะไรขึ้นหากคุณใช้ Linux ABI int 0x80 Linux แบบ 32 บิตในโค้ด 64 บิต)

eax=1 / int 80h คือ sys_exit บน Linux (unistd_32.h มี __NR_exit = 1) ใช้ดีบักเกอร์ นั่นจะแสดงให้คุณเห็นว่าคำสั่งใดที่ทำให้โปรแกรมของคุณออกจากโปรแกรม

ชื่อของคุณ (ก่อนที่ฉันจะแก้ไข) บอกว่าคุณมีข้อผิดพลาดในการแบ่งส่วน แต่ฉันทดสอบบนเดสก์ท็อป x86-64 ของฉันแล้ว แต่นั่นไม่ใช่กรณีนี้ มันออกอย่างหมดจดโดยใช้การเรียกระบบ int 80h ออก (แต่ในโค้ดที่ทำ segfault ให้ใช้ดีบักเกอร์เพื่อดูว่าคำสั่งใด) strace ถอดรหัส int 0x80 การเรียกระบบไม่ถูกต้องในกระบวนการ 64 บิต< /a> โดยใช้หมายเลขโทร syscall แบบ 64 บิตจาก unistd_64.h ไม่ใช่หมายเลขโทร unistd_32.h แบบ 32 บิต


รหัสของคุณใกล้จะใช้งานได้แล้ว: คุณใช้ int 0x80 ABI 32 บิตอย่างถูกต้องสำหรับ sys_write และส่งผ่าน args 32 บิตเท่านั้น (พอยน์เตอร์หาขนาดได้พอดีใน 32 บิต เนื่องจากโค้ด/ข้อมูลแบบคงที่จะถูกวางไว้ในพื้นที่ที่อยู่เสมือน 2GiB ต่ำเสมอในโมเดลโค้ดเริ่มต้นบน x86-64 ด้วยเหตุผลนี้จริงๆ คุณจึงสามารถใช้คำแนะนำแบบย่อ เช่น mov edi, formatin เพื่อใส่ที่อยู่ได้ ในทะเบียน หรือใช้เป็นการแทนที่โดยทันทีหรือ rel32 ที่ลงนามแล้ว)

OTOH ฉันคิดว่าคุณกำลังทำอย่างนั้นด้วยเหตุผลที่ผิด และตามที่ @prl ชี้ให้เห็น คุณลืมที่จะรักษาการจัดตำแหน่งสแต็กขนาด 16 ไบต์

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


วงของคุณเสียในหลายวิธีเช่นกัน ดูเหมือนว่าคุณพยายามโทร printf ด้วยรูปแบบการโทรแบบ 32 บิต (args บนสแต็ก)

แม้จะอยู่ในโค้ด 32 บิต ก็ใช้งานไม่ได้ เนื่องจาก return vale ของ printf อยู่ใน eax ดังนั้นการวนซ้ำของคุณจึงไม่มีที่สิ้นสุด เนื่องจาก printf ส่งคืนจำนวนอักขระที่พิมพ์ นั่นคืออย่างน้อยสองค่าจากสตริงรูปแบบ %d\n ดังนั้น dec rax / jnz จะต้องข้ามเสมอ

ใน x86-64 SysV ABI คุณต้องเป็นศูนย์ al ก่อนที่จะโทร printf (ด้วย xor eax,eax) หากคุณไม่ผ่าน FP args ใด ๆ ในการลงทะเบียน XMM คุณต้องผ่าน args ใน rdi, rsi, ... เช่น scanf

คุณยัง add rsp, 8 หลังจากกดค่า 8 ไบต์สองค่า ดังนั้นสแต็กจะขยายตลอดไป (แต่คุณจะไม่กลับมา ดังนั้น segfault ในที่สุดจะอยู่ในสแต็กโอเวอร์โฟลว์ ไม่ใช่ในการพยายามส่งคืนโดยที่ rsp ไม่ชี้ไปยังที่อยู่ผู้ส่ง)


ตัดสินใจว่าคุณกำลังสร้างโค้ด 32 บิตหรือ 64 บิต และคัดลอก/วางจากตัวอย่างสำหรับโหมดและระบบปฏิบัติการที่คุณกำหนดเป้าหมายเท่านั้น (โปรดทราบว่าโค้ด 64 บิตสามารถใช้ได้และมักใช้รีจิสเตอร์แบบ 32 บิตเป็นส่วนใหญ่)

ดูเพิ่มเติมที่ การประกอบ 32 บิต ไบนารีบนระบบ 64 บิต (GNU toolchain) (ซึ่งรวมถึงส่วน NASM พร้อมด้วยสคริปต์ asm-link ที่มีประโยชน์ซึ่งประกอบและลิงก์ไปยังไบนารีแบบคงที่) แต่เนื่องจากคุณกำลังเขียน main แทนที่จะเป็น _start และใช้ฟังก์ชัน libc คุณจึงควรเชื่อมโยงกับ gcc -m32 (หากคุณตัดสินใจที่จะใช้โค้ด 32 บิต แทนที่จะแทนที่ส่วน 32 บิตของโปรแกรมของคุณด้วยการเรียกใช้ฟังก์ชัน 64 บิต และแบบแผนการเรียกระบบ)

ดู แบบแผนการเรียกคืออะไร สำหรับระบบ UNIX และ Linux เรียกใช้ i386 และ x86-64

person Peter Cordes    schedule 20.11.2017