Saat ini, jarang diperlukan untuk menulis di assembler murni, tetapi saya merekomendasikan ini kepada siapa pun yang tertarik dalam pemrograman. Anda akan melihat hal-hal dari sudut yang berbeda, dan keterampilan akan berguna ketika men-debug kode dalam bahasa lain.
Pada artikel ini, kita akan menulis dari awal kalkulator
notasi Polandia terbalik (RPN) di assembler x86 murni. Ketika kita selesai, kita dapat menggunakannya seperti ini:
$ ./calc "32+6*"
Semua kode untuk artikel ada di
sini . Ini banyak dikomentari dan dapat berfungsi sebagai materi pendidikan bagi mereka yang sudah tahu assembler.
Mari kita mulai dengan menulis program
Hello world dasar
! untuk memeriksa pengaturan lingkungan. Kemudian mari kita beralih ke panggilan sistem, tumpukan panggilan, susunan bingkai, dan konvensi pemanggilan x86. Kemudian, untuk latihan, kita akan menulis beberapa fungsi dasar dalam assembler x86 - dan mulai menulis kalkulator RPN.
Diasumsikan bahwa pembaca memiliki beberapa pengalaman pemrograman dalam C dan pengetahuan dasar arsitektur komputer (misalnya, apa itu register prosesor). Karena kita akan menggunakan Linux, Anda juga harus dapat menggunakan baris perintah Linux.
Pengaturan lingkungan
Seperti yang telah disebutkan, kami menggunakan Linux (64-bit atau 32-bit). Kode di atas tidak berfungsi pada Windows atau Mac OS X.
Untuk instalasi, Anda hanya perlu GNU
ld
linker dari
binutils
, yang sudah diinstal pada sebagian besar distribusi, dan assembler NASM. Di Ubuntu dan Debian, Anda dapat menginstal keduanya dengan satu perintah:
$ sudo apt-get install binutils nasm
Saya juga merekomendasikan menyimpan
tabel ASCII .
Halo dunia!
Untuk memverifikasi lingkungan, simpan kode berikut dalam file
calc.asm
:
; _start ; . global _start ; .rodata ( ) ; , section .rodata ; hello_world. NASM ; , , ; . 0xA = , 0x0 = hello_world: db "Hello world!", 0xA, 0x0 ; .text, section .text _start: mov eax, 0x04 ; 4 eax (0x04 = write()) mov ebx, 0x1 ; (1 = , 2 = ) mov ecx, hello_world ; mov edx, 14 ; int 0x80 ; 0x80, ; mov eax, 0x01 ; 0x01 = exit() mov ebx, 0 ; 0 = int 0x80
Komentar menjelaskan struktur umum. Untuk daftar register dan instruksi umum, lihat
Panduan Assembler University of Virginia x86 . Dengan diskusi lebih lanjut tentang panggilan sistem, ini akan menjadi lebih penting.
Perintah berikut mengumpulkan file assembler ke file objek, dan kemudian mengkompilasi file yang dapat dieksekusi:
$ nasm -f elf_i386 calc.asm -o calc $ ld -m elf_i386 calc.o -o calc
Setelah memulai, Anda akan melihat:
$ ./calc Hello world!
Makefile
Ini adalah bagian opsional, tetapi Anda bisa membuat
Makefile
untuk menyederhanakan pembuatan dan tata letak di masa depan. Simpan di direktori yang sama dengan
calc.asm
:
CFLAGS= -f elf32 LFLAGS= -m elf_i386 all: calc calc: calc.o ld $(LFLAGS) calc.o -o calc calc.o: calc.asm nasm $(CFLAGS) calc.asm -o calc.o clean: rm -f calc.o calc .INTERMEDIATE: calc.o
Kemudian, alih-alih instruksi di atas, jalankan make.
Panggilan sistem
Panggilan sistem Linux memberitahu OS untuk melakukan sesuatu untuk kita. Dalam artikel ini, kami hanya menggunakan dua panggilan sistem:
write()
untuk menulis baris ke file atau streaming (dalam kasus kami, ini adalah perangkat output standar dan kesalahan standar) dan
exit()
untuk keluar dari program:
syscall 0x01: exit(int error_code) error_code - 0 ( 1) syscall 0x04: write(int fd, char *string, int length) fd β 1 , 2 string β length β
Panggilan sistem dikonfigurasikan dengan menyimpan nomor panggilan sistem dalam register
eax
, dan kemudian argumennya dalam
ebx
,
ecx
,
edx
dalam urutan itu. Anda mungkin memperhatikan bahwa
exit()
hanya
exit()
satu argumen - dalam hal ini ecx dan edx tidak masalah.
Eax | ebx | ECX | edx |
---|
Nomor panggilan sistem | arg1 | arg2 | arg3 |
Tumpukan panggilan

Tumpukan panggilan adalah struktur data yang menyimpan informasi tentang setiap panggilan ke suatu fungsi. Setiap panggilan memiliki bagiannya sendiri di tumpukan - "bingkai". Ini menyimpan beberapa informasi tentang panggilan saat ini: variabel lokal dari fungsi ini dan alamat kembali (di mana program harus pergi setelah fungsi dijalankan).
Segera saya perhatikan satu hal yang tidak jelas: tumpukan memori semakin
berkurang . Ketika Anda menambahkan sesuatu ke bagian atas tumpukan, itu dimasukkan pada alamat memori yang lebih rendah dari item sebelumnya. Dengan kata lain, saat tumpukan bertambah, alamat memori di bagian atas tumpukan berkurang. Untuk menghindari kebingungan, saya akan selalu mengingatkan Anda tentang fakta ini.
Instruksi
push
sesuatu di atas tumpukan, dan
pop
muncul data dari sana. Misalnya,
push
mengalokasikan tempat di bagian atas tumpukan dan menempatkan nilai dari register
eax
sana, dan
pop
mentransfer data apa pun dari atas tumpukan ke
eax
dan membebaskan area memori ini.
Tujuan dari register
esp
adalah untuk menunjuk ke bagian atas tumpukan. Setiap data di atas
esp
dianggap tidak mengenai tumpukan, ini adalah data sampah. Menjalankan pernyataan
push
(atau
pop
) bergerak
esp
. Anda dapat memanipulasi
esp
secara langsung, jika Anda memberikan laporan atas tindakan Anda.
Register
ebp
mirip dengan
esp
, hanya saja selalu menunjuk kira-kira ke tengah frame stack saat ini, tepat sebelum variabel lokal dari fungsi saat ini (kita akan membicarakannya nanti). Namun, memanggil fungsi lain tidak bergerak secara otomatis, itu harus dilakukan secara manual setiap kali.
Konvensi pemanggilan arsitektur X86
Di x86, tidak ada konsep fungsi bawaan seperti pada bahasa tingkat tinggi.
goto
call
pada dasarnya hanya
jmp
(
goto
) ke alamat memori lain. Untuk menggunakan rutinitas sebagai fungsi dalam bahasa lain (yang dapat mengambil argumen dan mengembalikan data), Anda harus mengikuti konvensi pemanggilan (ada banyak konvensi, tetapi kami menggunakan CDECL, konvensi paling populer untuk x86 di antara kompiler C dan programmer assembler). Ini juga memastikan bahwa register rutin tidak bingung ketika memanggil fungsi lain.
Aturan Penelepon
Sebelum memanggil fungsi, penelepon harus:
- Simpan register yang harus disimpan oleh penelepon ke tumpukan. Fungsi yang dipanggil dapat mengubah beberapa register: agar tidak kehilangan data, pemanggil harus menyimpannya dalam memori sampai didorong ke stack. Ini adalah
edx
eax
, ecx
dan edx
. Jika Anda tidak menggunakan salah satunya, maka Anda tidak dapat menyimpannya. - Tulis argumen fungsi ke stack dalam urutan terbalik (argumen terakhir pertama, argumen pertama pertama di akhir). Pemesanan ini memastikan bahwa fungsi yang dipanggil menerima argumennya dari tumpukan dengan urutan yang benar.
- Panggil subrutin.
Jika memungkinkan, fungsi akan menyimpan hasilnya dalam
eax
. Segera setelah
call
penelepon harus:
- Hapus argumen fungsi dari tumpukan. Ini biasanya dilakukan dengan hanya menambahkan jumlah byte ke
esp
. Jangan lupa bahwa tumpukan tumbuh turun, jadi untuk menghapus dari tumpukan, Anda harus menambahkan byte. - Kembalikan register yang disimpan dengan mengeluarkannya dari tumpukan dengan urutan terbalik. Fungsi yang dipanggil tidak akan mengubah register lainnya.
Contoh berikut menunjukkan bagaimana aturan ini berlaku. Asumsikan bahwa fungsi
_subtract
mengambil dua argumen integer (4-byte) dan mengembalikan argumen pertama dikurangi yang kedua. Dalam subrutin
_mysubroutine
panggil
_subtract
dengan argumen
10
dan
2
:
_mysubroutine: ; ... ; - ; ... push ecx ; ( eax) push edx push 2 ; , push 10 call _subtract ; eax 10-2=8 add esp, 8 ; 8 ( 4 ) pop edx ; pop ecx ; ... ; - , eax ; ...
Aturan yang disebut rutin
Sebelum menelepon, subrutin harus:
- Simpan pointer register dasar
ebp
dari frame sebelumnya dengan menuliskannya ke stack. - Sesuaikan
ebp
dari frame sebelumnya ke saat ini (nilai esp
saat ini). - Alokasikan lebih banyak ruang pada stack untuk variabel lokal, jika perlu, pindahkan pointer
esp
. Saat tumpukan bertambah, Anda perlu mengurangi memori yang hilang dari esp
. - Simpan register dari rutin yang dipanggil ke tumpukan. Ini adalah
ebx
, edi
dan esi
. Tidak perlu menyimpan register yang tidak direncanakan untuk diubah.
Tumpukan panggilan setelah langkah 1:

Tumpukan panggilan setelah langkah 2:

Tumpukan panggilan setelah langkah 4:

Dalam diagram ini, alamat pengirim ditunjukkan di setiap bingkai tumpukan. Secara otomatis didorong ke stack oleh pernyataan
call
.
ret
mengambil alamat dari atas tumpukan dan melompat ke sana. Kita tidak memerlukan instruksi ini, saya hanya menunjukkan mengapa variabel lokal dari fungsi adalah 4 byte di atas
ebp
, tetapi argumen fungsi adalah 8 byte di bawah
ebp
.
Dalam diagram terakhir, Anda juga dapat melihat bahwa variabel lokal dari fungsi selalu mulai 4 byte di atas
ebp
dari alamat
ebp-4
(kurangi di sini, karena kita bergerak ke atas tumpukan), dan argumen fungsi selalu dimulai 8 byte di bawah
ebp
dari alamat
ebp+8
(tambahan, karena kita bergerak ke bawah tumpukan). Jika Anda mengikuti aturan konvensi ini, itu akan terjadi dengan variabel dan argumen fungsi apa pun.
Ketika fungsi selesai dan Anda ingin kembali, Anda harus terlebih dahulu mengatur
eax
ke nilai kembali fungsi, jika perlu. Selain itu, Anda perlu:
- Kembalikan register yang disimpan dengan mengeluarkannya dari tumpukan dengan urutan terbalik.
- Kosongkan ruang pada tumpukan yang dialokasikan oleh variabel lokal di langkah 3, jika perlu: dengan hanya menginstal
esp
di ebp - Kembalikan penunjuk dasar
ebp
dari frame sebelumnya dengan mengeluarkannya dari tumpukan. - Kembali dengan
ret
Sekarang kita menerapkan fungsi
_subtract
dari contoh kita:
_subtract: push ebp ; mov ebp, esp ; ebp ; , ; , ; ; mov eax, [ebp+8] ; eax. ; ebp+8 sub eax, [ebp+12] ; ebp+12 ; ; , eax ; , ; , pop ebp ; ret
Masuk dan keluar
Dalam contoh di atas, Anda dapat melihat bahwa fungsi selalu berjalan dengan cara yang sama:
push ebp
,
mov ebp
,
esp
, dan alokasi memori untuk variabel lokal. Set x86 memiliki instruksi praktis yang melakukan semua ini:
enter ab
, di mana
a
adalah jumlah byte yang ingin Anda alokasikan untuk variabel lokal,
b
adalah "level bersarang", yang akan selalu kita atur ke
0
. Selain itu, fungsi selalu berakhir dengan instruksi
pop ebp
dan
mov esp
,
ebp
(meskipun mereka diperlukan hanya ketika mengalokasikan memori untuk variabel lokal, tetapi dalam hal apapun tidak membahayakan). Ini juga dapat diganti dengan satu pernyataan:
leave
. Kami melakukan perubahan:
_subtract: enter 0, 0 ; ebp ; , ; ; mov eax, [ebp+8] ; eax. ; ebp+8 sub eax, [ebp+12] ; ebp+12 ; ; , eax ; , leave ; ret
Menulis Beberapa Fungsi Dasar
Setelah menguasai konvensi pemanggilan, Anda dapat mulai menulis beberapa rutinitas. Mengapa tidak menggeneralisasi kode yang menampilkan "Halo dunia!" Untuk menampilkan setiap baris: fungsi
_print_msg
.
Di sini kita membutuhkan fungsi
_strlen
lain untuk menghitung panjang string. Di C, akan terlihat seperti ini:
size_t strlen(char *s) { size_t length = 0; while (*s != 0) {
Dengan kata lain, dari awal baris, kami menambahkan 1 ke nilai kembali untuk setiap karakter kecuali nol. Segera setelah karakter nol diperhatikan, kami mengembalikan nilai yang terakumulasi dalam loop. Di assembler, ini juga cukup sederhana: Anda dapat menggunakan fungsi
_subtract
ditulis sebelumnya sebagai basis:
_strlen: enter 0, 0 ; ebp ; , ; ; mov eax, 0 ; length = 0 mov ecx, [ebp+8] ; ( ; ) ecx ( ; , ) _strlen_loop_start: ; , cmp byte [ecx], 0 ; . ; 32 (4 ). ; . ; ( ) je _strlen_loop_end ; inc eax ; , 1 add ecx, 1 ; jmp _strlen_loop_start ; _strlen_loop_end: ; , eax ; , leave ; ret
Sudah tidak buruk kan? Menulis kode C terlebih dahulu dapat membantu, karena sebagian besar langsung dikonversi menjadi assembler. Sekarang Anda dapat menggunakan fungsi ini di
_print_msg
, tempat kami menerapkan semua pengetahuan yang diperoleh:
_print_msg: enter 0, 0 ; mov eax, 0x04 ; 0x04 = write() mov ebx, 0x1 ; 0x1 = mov ecx, [ebp+8] ; , ; edx . _strlen push eax ; ( edx) push ecx push dword [ebp+8] ; _strlen _print_msg. NASM ; , , , . ; dword (4 , 32 ) call _strlen ; eax mov edx, eax ; edx, add esp, 4 ; 4 ( 4- char*) pop ecx ; pop eax ; _strlen, int 0x80 leave ret
Dan lihat buah dari kerja keras kami, menggunakan fungsi ini di program lengkap "Halo, dunia!".
_start: enter 0, 0 ; ( ) push hello_world ; _print_msg call _print_msg mov eax, 0x01 ; 0x01 = exit() mov ebx, 0 ; 0 = int 0x80
Percaya atau tidak, kami telah membahas semua topik utama yang diperlukan untuk menulis program assembler x86 dasar! Sekarang kita memiliki semua bahan pengantar dan teori, jadi kita akan benar-benar berkonsentrasi pada kode dan menerapkan pengetahuan yang diperoleh untuk menulis kalkulator RPN kami. Fungsinya akan lebih lama dan bahkan akan menggunakan beberapa variabel lokal. Jika Anda ingin segera melihat program yang sudah selesai, ini
dia .
Bagi Anda yang tidak terbiasa dengan notasi Polandia terbalik (kadang-kadang disebut notasi Polandia terbalik atau notasi postfix), di sini ekspresi dievaluasi menggunakan tumpukan. Oleh karena itu, Anda perlu membuat stack, serta fungsi
_pop
dan
_push
untuk memanipulasi tumpukan ini. Anda juga akan
_print_answer
fungsi
_print_answer
, yang akan menampilkan representasi string dari hasil numerik di akhir perhitungan.
Susun penciptaan
Pertama, kita mendefinisikan ruang dalam memori untuk stack kita, serta
stack_size
variabel global. Dianjurkan untuk mengubah variabel-variabel ini sehingga mereka tidak jatuh di bagian
.rodata
, tetapi dalam
.data
.
section .data stack_size: dd 0 ; dword (4 ) 0 stack: times 256 dd 0 ;
Sekarang Anda dapat menerapkan fungsi
_push
dan
_pop
:
_push: enter 0, 0 ; , push eax push edx mov eax, [stack_size] mov edx, [ebp+8] mov [stack + 4*eax], edx ; . ; dword inc dword [stack_size] ; 1 stack_size ; pop edx pop eax leave ret _pop: enter 0, 0 ; dec dword [stack_size] ; 1 stack_size mov eax, [stack_size] mov eax, [stack + 4*eax] ; eax ; , leave ret
Output angka
_print_answer
jauh lebih rumit: Anda harus mengonversi angka menjadi string dan menggunakan beberapa fungsi lainnya. Anda akan
_putc
fungsi
_putc
, yang menghasilkan satu karakter, fungsi
mod
untuk menghitung sisa pembagian (modul) dari dua argumen dan
_pow_10
untuk menaikkan ke kekuatan 10. Kemudian Anda akan mengerti mengapa mereka dibutuhkan. Ini cukup sederhana, ini kodenya:
_pow_10: enter 0, 0 mov ecx, [ebp+8] ; ecx ( ) ; mov eax, 1 ; 10 (10**0 = 1) _pow_10_loop_start: ; eax 10, ecx 0 cmp ecx, 0 je _pow_10_loop_end imul eax, 10 sub ecx, 1 jmp _pow_10_loop_start _pow_10_loop_end: leave ret _mod: enter 0, 0 push ebx mov edx, 0 ; mov eax, [ebp+8] mov ebx, [ebp+12] idiv ebx ; 64- [edx:eax] ebx. ; 32- eax, edx ; . ; eax, edx. , ; , ; . mov eax, edx ; () pop ebx leave ret _putc: enter 0, 0 mov eax, 0x04 ; write() mov ebx, 1 ; lea ecx, [ebp+8] ; mov edx, 1 ; 1 int 0x80 leave ret
Jadi, bagaimana kita memperoleh angka individu dalam suatu angka? Pertama, perhatikan bahwa digit terakhir dari angka tersebut adalah sisa dari pembagian dengan 10 (misalnya,
123 % 10 = 3
), dan digit berikutnya adalah sisa dari pembagian dengan 100, dibagi dengan 10 (misalnya,
(123 % 100)/10 = 2
). Secara umum, Anda dapat menemukan digit angka tertentu (dari kanan ke kiri) dengan menemukan
( % 10**n) / 10**(n-1)
, di mana jumlah unit akan
n = 1
, jumlah puluhan adalah
n = 2
dan seterusnya.
Dengan menggunakan pengetahuan ini, Anda dapat menemukan semua digit angka dari
n = 1
hingga
n = 10
(ini adalah jumlah bit maksimum dalam bilangan bulat 4-byte yang ditandatangani). Tetapi jauh lebih mudah untuk pergi dari kiri ke kanan - jadi kami dapat mencetak setiap karakter segera setelah kami menemukannya dan menyingkirkan nol di sisi kiri. Oleh karena itu, kami memilah angka-angka dari
n = 10
hingga
n = 1
.
Di C, program akan terlihat seperti ini:
#define MAX_DIGITS 10 void print_answer(int a) { if (a < 0) { // putc('-'); // «» a = -a; // } int started = 0; for (int i = MAX_DIGITS; i > 0; i--) { int digit = (a % pow_10(i)) / pow_10(i-1); if (digit == 0 && started == 0) continue; // started = 1; putc(digit + '0'); } }
Sekarang Anda mengerti mengapa kami membutuhkan ketiga fungsi ini. Mari kita implementasikan ini di assembler: %define MAX_DIGITS 10 _print_answer: enter 1, 0 ; 1 "started" C push ebx push edi push esi mov eax, [ebp+8] ; "a" cmp eax, 0 ; , ; jge _print_answer_negate_end ; call putc for '-' push eax push 0x2d ; '-' call _putc add esp, 4 pop eax neg eax ; _print_answer_negate_end: mov byte [ebp-4], 0 ; started = 0 mov ecx, MAX_DIGITS ; i _print_answer_loop_start: cmp ecx, 0 je _print_answer_loop_end ; pow_10 ecx. ebx "digit" C. ; edx = pow_10(i-1), ebx = pow_10(i) push eax push ecx dec ecx ; i-1 push ecx ; _pow_10 call _pow_10 mov edx, eax ; edx = pow_10(i-1) add esp, 4 pop ecx ; i ecx pop eax ; end pow_10 call mov ebx, edx ; digit = ebx = pow_10(i-1) imul ebx, 10 ; digit = ebx = pow_10(i) ; _mod (a % pow_10(i)), (eax mod ebx) push eax push ecx push edx push ebx ; arg2, ebx = digit = pow_10(i) push eax ; arg1, eax = a call _mod mov ebx, eax ; digit = ebx = a % pow_10(i+1), almost there add esp, 8 pop edx pop ecx pop eax ; mod ; ebx ( "digit" ) pow_10(i) (edx). ; , idiv edx, eax. ; edx , - ; push esi mov esi, edx push eax mov eax, ebx mov edx, 0 idiv esi ; eax () mov ebx, eax ; ebx = (a % pow_10(i)) / pow_10(i-1), "digit" C pop eax pop esi ; end division cmp ebx, 0 ; digit == 0 jne _print_answer_trailing_zeroes_check_end cmp byte [ebp-4], 0 ; started == 0 jne _print_answer_trailing_zeroes_check_end jmp _print_answer_loop_continue ; continue _print_answer_trailing_zeroes_check_end: mov byte [ebp-4], 1 ; started = 1 add ebx, 0x30 ; digit + '0' ; putc push eax push ecx push edx push ebx call _putc add esp, 4 pop edx pop ecx pop eax ; putc _print_answer_loop_continue: sub ecx, 1 jmp _print_answer_loop_start _print_answer_loop_end: pop esi pop edi pop ebx leave ret
Itu adalah ujian yang sulit! Semoga komentar membantu mengatasinya. Jika sekarang Anda berpikir: "Mengapa Anda tidak menulis saja printf("%d")
?", Maka Anda akan menyukai bagian akhir artikel, di mana kami akan mengganti fungsinya hanya dengan itu!Sekarang kita memiliki semua fungsi yang diperlukan, masih menerapkan logika dasar di _start
- dan itu saja!Membalikkan perhitungan notasi Polandia
Seperti yang telah kami katakan, notasi balik Polandia dihitung menggunakan tumpukan. Saat membaca, nomor didorong ke tumpukan, dan saat membaca, operator diterapkan ke dua objek di bagian atas tumpukan.Misalnya, jika kita ingin menghitung 84/3+6*
(ungkapan ini juga bisa ditulis dalam bentuk 6384/+*
), prosesnya adalah sebagai berikut:Langkah | Simbol | Tumpuk sebelumnya | Susun setelah |
---|
1 | 8 | [] | [8] |
2 | 4 | [8] | [8, 4] |
3 | / | [8, 4] | [2] |
4 | 3 | [2] | [2, 3] |
5 | + | [2, 3] | [5] |
6 | 6 | [5] | [5, 6] |
7 | * | [5, 6] | [30] |
Jika input adalah ekspresi postfix yang valid, maka pada akhir perhitungan hanya ada satu elemen yang tersisa di stack - ini adalah jawabannya, hasil dari perhitungan. Dalam kasus kami, angkanya adalah 30.Dalam assembler, Anda perlu mengimplementasikan sesuatu seperti kode ini di C: int stack[256];
Sekarang kita memiliki semua fungsi yang diperlukan untuk mengimplementasikan ini, mari kita mulai. _start: ; _start , . ; esp argc ( ), ; esp+4 argv. , esp+4 ; , esp+8 - mov esi, [esp+8] ; esi = "input" = argv[0] ; _strlen push esi call _strlen mov ebx, eax ; ebx = input_length add esp, 4 ; end _strlen call mov ecx, 0 ; ecx = "i" _main_loop_start: cmp ecx, ebx ; (i >= input_length) jge _main_loop_end mov edx, 0 mov dl, [esi + ecx] ; ; edx. edx . ; edx = c = input[i] cmp edx, '0' jl _check_operator cmp edx, '9' jg _print_error sub edx, '0' mov eax, edx ; eax = c - '0' (, ) jmp _push_eax_and_continue _check_operator: ; _pop b edi, a b - eax push ecx push ebx call _pop mov edi, eax ; edi = b call _pop ; eax = a pop ebx pop ecx ; end call _pop cmp edx, '+' jne _subtract add eax, edi ; eax = a+b jmp _push_eax_and_continue _subtract: cmp edx, '-' jne _multiply sub eax, edi ; eax = ab jmp _push_eax_and_continue _multiply: cmp edx, '*' jne _divide imul eax, edi ; eax = a*b jmp _push_eax_and_continue _divide: cmp edx, '/' jne _print_error push edx ; edx, idiv mov edx, 0 idiv edi ; eax = a/b pop edx ; eax _push_eax_and_continue: ; _push push eax push ecx push edx push eax ; call _push add esp, 4 pop edx pop ecx pop eax ; call _push inc ecx jmp _main_loop_start _main_loop_end: cmp byte [stack_size], 1 ; (stack_size != 1), jne _print_error mov eax, [stack] push eax call _print_answer ; print a final newline push 0xA call _putc ; exit successfully mov eax, 0x01 ; 0x01 = exit() mov ebx, 0 ; 0 = int 0x80 ; _print_error: push error_msg call _print_msg mov eax, 0x01 mov ebx, 1 int 0x80
Anda juga perlu menambahkan baris error_msg
ke bagian ini .rodata
: section .rodata ; error_msg. db NASM ; , ; . 0xA = , 0x0 = error_msg: db "Invalid input", 0xA, 0x0
Dan kita selesai! Kejutkan semua teman Anda jika Anda memilikinya. Saya harap sekarang Anda akan bereaksi lebih hangat terhadap bahasa tingkat tinggi, terutama jika Anda ingat bahwa banyak program lama ditulis sepenuhnya atau hampir sepenuhnya dalam assembler, misalnya, RollerCoaster Tycoon yang asli!Semua kode ada di sini . Terima kasih sudah membaca! Saya dapat melanjutkan jika Anda tertarik.Tindakan selanjutnya
Anda dapat berlatih dengan menerapkan beberapa fungsi tambahan:- Kembalikan pesan kesalahan alih-alih segfault jika program tidak menerima argumen.
- Tambahkan dukungan untuk ruang ekstra antara operan dan operator dalam input.
- Tambahkan dukungan untuk operan multi-bit.
- Izinkan angka negatif.
- Ganti
_strlen
dengan fungsi dari pustaka C standar , dan _print_answer
ganti dengan panggilan printf
.
Bahan tambahan