Panduan Perakitan X86 untuk Pemula

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*" # "(3+2)*6"    30 

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.

EaxebxECXedx
Nomor panggilan sistemarg1arg2arg3


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:

  1. 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.
  2. 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.
  3. Panggil subrutin.

Jika memungkinkan, fungsi akan menyimpan hasilnya dalam eax . Segera setelah call penelepon harus:

  1. 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.
  2. 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:

  1. Simpan pointer register dasar ebp dari frame sebelumnya dengan menuliskannya ke stack.
  2. Sesuaikan ebp dari frame sebelumnya ke saat ini (nilai esp saat ini).
  3. 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 .
  4. 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:

  1. Kembalikan register yang disimpan dengan mengeluarkannya dari tumpukan dengan urutan terbalik.
  2. Kosongkan ruang pada tumpukan yang dialokasikan oleh variabel lokal di langkah 3, jika perlu: dengan hanya menginstal esp di ebp
  3. Kembalikan penunjuk dasar ebp dari frame sebelumnya dengan mengeluarkannya dari tumpukan.
  4. 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) { //   length++; s++; } //   return length; } 

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:

LangkahSimbolTumpuk sebelumnyaSusun setelah
18[][8]
24[8][8, 4]
3/[8, 4][2]
43[2][2, 3]
5+[2, 3][5]
66[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]; // , 256      int stack_size = 0; int main(int argc, char *argv[]) { char *input = argv[0]; size_t input_length = strlen(input); for (int i = 0; i < input_length; i++) { char c = input[i]; if (c >= '0' && c <= '9') { //   β€”   push(c - '0'); //          } else { int b = pop(); int a = pop(); if (c == '+') { push(a+b); } else if (c == '-') { push(ab); } else if (c == '*') { push(a*b); } else if (c == '/') { push(a/b); } else { error("Invalid input\n"); exit(1); } } } if (stack_size != 1) { error("Invalid input\n"); exit(1); } print_answer(stack[0]); exit(0); } 

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_msgke 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:

  1. Kembalikan pesan kesalahan alih-alih segfault jika program tidak menerima argumen.
  2. Tambahkan dukungan untuk ruang ekstra antara operan dan operator dalam input.
  3. Tambahkan dukungan untuk operan multi-bit.
  4. Izinkan angka negatif.
  5. Ganti _strlendengan fungsi dari pustaka C standar , dan _print_answerganti dengan panggilan printf.

Bahan tambahan


Source: https://habr.com/ru/post/id423077/


All Articles