دليل مجمع X86 للمبتدئين

في الوقت الحاضر ، نادرًا ما يكون من الضروري أن تكتب في مجمع نقي ، لكنني بالتأكيد أوصي بذلك لأي شخص مهتم بالبرمجة. سترى أشياء من زاوية مختلفة ، وستكون المهارات مفيدة عند تصحيح الأخطاء البرمجية بلغات أخرى.

في هذه المقالة ، سنكتب من الصفر آلة حاسبة تدوينًا بولنديًا عكسيًا (RPN) في مجمع x86 خالص. عندما ننتهي ، يمكننا استخدامه على النحو التالي:

$ ./calc "32+6*" # "(3+2)*6"    30 

كل كود المقال هنا . تم التعليق عليها بوفرة ويمكن أن تكون بمثابة مادة تعليمية لأولئك الذين يعرفون بالفعل المجمع.

لنبدأ بكتابة برنامج Hello world الأساسي ! للتحقق من إعدادات البيئة. ثم دعنا ننتقل إلى مكالمات النظام ، مكدس المكالمة ، إطارات مكدس ، اصطلاح استدعاء x86. بعد ذلك ، للممارسة ، سنكتب بعض الوظائف الأساسية في x86 المجمع - ونبدأ في كتابة حاسبة RPN.

من المفترض أن القارئ لديه بعض الخبرة في البرمجة C والمعرفة الأساسية بهندسة الكمبيوتر (على سبيل المثال ، ما هو سجل المعالج). نظرًا لأننا سنستخدم Linux ، يجب أن تكون قادرًا أيضًا على استخدام سطر أوامر Linux.

إعداد البيئة


كما ذكرنا من قبل ، نستخدم Linux (64 بت أو 32 بت). الرمز أعلاه لا يعمل على Windows أو Mac OS X.

للتثبيت ، تحتاج فقط إلى رابط GNU ld من binutils ، وهو مثبت مسبقًا على معظم التوزيعات ، ومجمع NASM. على Ubuntu و Debian ، يمكنك تثبيت كل منهما باستخدام أمر واحد:

 $ sudo apt-get install binutils nasm 

أوصي أيضًا بالحفاظ على جدول ASCII في متناول اليد.

مرحبا بالعالم!


للتحقق من البيئة ، احفظ التعليمات البرمجية التالية في ملف 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 

تشرح التعليقات الهيكل العام. للحصول على قائمة بالسجلات والتعليمات العامة ، راجع دليل جامعة فرجينيا x86 Assembler . مع مزيد من مناقشة مكالمات النظام ، سيكون هذا أكثر ضرورة.

تجمع الأوامر التالية ملف التجميع في ملف كائن ، ثم تقوم بترجمة الملف القابل للتنفيذ:

 $ nasm -f elf_i386 calc.asm -o calc $ ld -m elf_i386 calc.o -o calc 

بعد البدء سترى:

 $ ./calc Hello world! 

Makefile


هذا جزء اختياري ، ولكن يمكنك إنشاء ملف Makefile لتبسيط البناء والتخطيط في المستقبل. احفظه في نفس الدليل مثل 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 

ثم ، بدلاً من الإرشادات المذكورة أعلاه ، قم بتشغيل الأمر.

مكالمات النظام


تخبر مكالمات نظام Linux نظام التشغيل بفعل شيء ما لنا. في هذه المقالة ، نستخدم فقط مكالمتين للنظام: write() لكتابة سطر إلى ملف أو دفق (في حالتنا ، هذا جهاز إخراج قياسي وخطأ قياسي) exit() للخروج من البرنامج:

 syscall 0x01: exit(int error_code) error_code -  0         (  1)   syscall 0x04: write(int fd, char *string, int length) fd —  1   , 2      string —      length 

يتم تكوين مكالمات النظام عن طريق تخزين رقم استدعاء النظام في سجل eax ، ثم ecx في ebx ، ecx ، edx بهذا الترتيب. قد تلاحظ أن exit() وسيطة واحدة فقط - في هذه الحالة لا يهم ecx و edx.

eaxebxecxEDX
رقم استدعاء النظامarg1arg2arg3


مكدس الاستدعاء




مكدس الاستدعاء عبارة عن بنية بيانات تقوم بتخزين معلومات حول كل استدعاء لوظيفة. كل مكالمة لها قسمها الخاص في المكدس - "الإطار". يقوم بتخزين بعض المعلومات حول المكالمة الحالية: المتغيرات المحلية لهذه الوظيفة وعنوان الإرجاع (حيث يجب أن يذهب البرنامج بعد تنفيذ الوظيفة).

على الفور ألاحظ شيئًا واحدًا غير واضح: المكدس ينمو في الذاكرة. عندما تضيف شيئًا إلى الجزء العلوي من المكدس ، يتم إدراجه في عنوان ذاكرة أقل من العنصر السابق. بمعنى آخر ، مع نمو المكدس ، ينخفض ​​عنوان الذاكرة في الجزء العلوي من المكدس. لتجنب الالتباس ، سأذكرك دائمًا بهذه الحقيقة.

تظهر تعليمات push شيئًا ما فوق المكدس ، وتنبثق البيانات من هناك. على سبيل المثال ، يقوم push بتخصيص مكان في أعلى المكدس ويضع القيمة من سجل eax هناك ، ويقوم pop بتحويل أي بيانات من أعلى المكدس إلى eax ويحرر مساحة الذاكرة هذه.

الغرض من تسجيل esp هو الإشارة إلى أعلى المكدس. تعتبر أي بيانات أعلاه esp لا تصل إلى المكدس ، هذه بيانات القمامة. تنفيذ عبارة push (أو pop ) تحركات esp . يمكنك التلاعب في esp بشكل مباشر ، إذا قدمت تقريرًا لأفعالك.

ebp سجل ebp esp ، فهو يشير فقط دائمًا تقريبًا إلى منتصف إطار المكدس الحالي ، مباشرة قبل المتغيرات المحلية للوظيفة الحالية (سنتحدث عن هذا لاحقًا). ومع ذلك ، لا يؤدي استدعاء وظيفة أخرى إلى نقل ebp تلقائيًا ، بل يجب إجراؤه يدويًا في كل مرة.

اصطلاح استدعاء العمارة X86


في الإصدار x86 ، لا يوجد مفهوم مدمج للوظيفة كما هو الحال في اللغات عالية المستوى. goto call goto الأساس مجرد jmp ( goto إلى) إلى عنوان ذاكرة آخر. لاستخدام الإجراءات كوظائف في لغات أخرى (والتي يمكنها استعادة الحجج وإرجاع البيانات) ، تحتاج إلى اتباع اصطلاح الاستدعاء (هناك العديد من الاصطلاحات ، ولكننا نستخدم CDECL ، المصطلح الأكثر شيوعًا لـ x86 بين المبرمجين C ومبرمجي التجميع). كما يضمن عدم الخلط بين السجلات الروتينية عند استدعاء وظيفة أخرى.

قواعد المتصل


قبل استدعاء الوظيفة ، يجب على المتصل:

  1. احفظ السجلات التي يطلب من المتصل حفظها في المكدس. يمكن للوظيفة المطلوبة تغيير بعض التسجيلات: حتى لا تفقد البيانات ، يجب على المتصل حفظها في الذاكرة حتى يتم دفعها إلى المكدس. هذه هي ecx eax و ecx و edx . إذا كنت لا تستخدم أيًا منها ، فلا يمكنك حفظها.
  2. اكتب وسيطات الدالة إلى المكدس بترتيب عكسي (أول حجة أخيرة ، أول حجة أولية في النهاية). يضمن هذا الترتيب أن تستدعي الدالة المطلوبة وسيطاتها من المكدس بالترتيب الصحيح.
  3. استدعاء روتين.

إن أمكن ، eax الوظيفة النتيجة في eax . فور انتهاء call يجب على المتصل:

  1. إزالة وسائط الدالة من المكدس. يتم ذلك عادةً عن طريق إضافة عدد البايتات إلى esp . لا تنس أن المكدس ينمو ، لذا للإزالة من المكدس ، يجب عليك إضافة بايت.
  2. قم باستعادة السجلات المحفوظة عن طريق إخراجها من المكدس بالترتيب العكسي. لن تغير الوظيفة المطلوبة أي تسجيلات أخرى.

يوضح المثال التالي كيفية تطبيق هذه القواعد. افترض أن الدالة _subtract تأخذ _subtract (4 بايت) وتُرجع الوسيطة الأولى ناقص الثانية. في روتين _mysubroutine استدعاء _subtract مع الوسائط 10 و 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 ; ... 

قواعد الروتين المسمى


قبل الاتصال ، يجب أن يكون الروتين الفرعي:

  1. احفظ مؤشر تسجيل قاعدة ebp للإطار السابق عن طريق كتابته إلى المكدس.
  2. اضبط ebp من الإطار السابق إلى التيار (قيمة esp الحالية).
  3. قم بتخصيص مساحة أكبر على المكدس للمتغيرات المحلية ، إذا لزم الأمر ، حرك المؤشر esp . مع نمو المكدس ، تحتاج إلى طرح الذاكرة المفقودة من esp .
  4. احفظ سجلات الروتين المطلوب على المكدس. هذه هي ebx و edi و esi . ليس من الضروري حفظ السجلات التي ليس من المخطط تغييرها.

مكدس الاتصال بعد الخطوة 1:



مكدس المكالمة بعد الخطوة 2:



مكدس الاتصال بعد الخطوة 4:



في هذه الرسوم البيانية ، يشار إلى عنوان المرسل في كل إطار مكدس. يتم دفعها تلقائيًا إلى المكدس من خلال بيان call . تسترجع ret العنوان من أعلى المكدس وتنتقل إليه. نحن لسنا بحاجة إلى هذه التعليمات ، لقد أوضحت للتو لماذا المتغيرات المحلية للدالة هي 4 بايت فوق ebp ، لكن حجج الدالة هي 8 بايت تحت ebp .

في الرسم التخطيطي الأخير ، يمكنك أيضًا ملاحظة أن المتغيرات المحلية للدالة تبدأ دائمًا 4 بايت فوق ebp من عنوان ebp-4 (الطرح هنا ، لأننا ebp-4 المكدس) ، وتبدأ الحجج الخاصة بالوظيفة دائمًا 8 بايت تحت ebp من عنوان ebp+8 address ebp+8 (بالإضافة ، لأننا نتحرك إلى أسفل المكدس). إذا اتبعت قواعد هذه الاتفاقية ، فسيكون الأمر كذلك مع المتغيرات والحجج الخاصة بأي وظيفة.

عند اكتمال الوظيفة وتريد إرجاعها ، يجب عليك أولاً تعيين eax على قيمة الإرجاع للدالة ، إذا لزم الأمر. بالإضافة إلى ذلك ، تحتاج إلى:

  1. قم باستعادة السجلات المحفوظة عن طريق إخراجها من المكدس بترتيب عكسي.
  2. قم بتحرير مساحة على المكدس المخصص بواسطة المتغير المحلي في الخطوة 3 ، إذا لزم الأمر: ببساطة عن طريق تثبيت esp في ebp
  3. قم باستعادة مؤشر قاعدة ebp للإطار السابق عن طريق ebp من المكدس.
  4. العودة مع ret

الآن نقوم بتطبيق دالة _subtract من مثالنا:

 _subtract: push ebp ;      mov ebp, esp ;  ebp ;          ,      ;       ,     ;   ;    mov eax, [ebp+8] ;      eax.  ;       ebp+8 sub eax, [ebp+12] ;      ebp+12   ;  ;   , eax     ;     ,     ;       ,       pop ebp ;      ret 

الدخول والخروج


في المثال أعلاه ، يمكنك ملاحظة أن الوظيفة تعمل دائمًا بنفس الطريقة: push ebp و mov ebp و esp وتخصيص الذاكرة للمتغيرات المحلية. تحتوي مجموعة x86 على تعليمات ملائمة تقوم بكل ذلك: enter ab ، حيث a عدد وحدات البايت التي تريد تخصيصها للمتغيرات المحلية ، و b هو "مستوى التداخل" ، والذي سنضبطه دائمًا على 0 . بالإضافة إلى ذلك ، تنتهي الوظيفة دائمًا بالتعليمات pop ebp و mov esp و ebp (على الرغم من أنها ضرورية فقط عند تخصيص ذاكرة للمتغيرات المحلية ، ولكن على أي حال لا تضر). يمكن أيضًا استبدال هذا ببيان واحد: leave . نجري تغييرات:

 _subtract: enter 0, 0 ;        ebp ;       ,     ;   ;    mov eax, [ebp+8] ;      eax.  ;       ebp+8 sub eax, [ebp+12] ;      ebp+12  ;   ;   , eax     ;     ,     leave ;      ret 

كتابة بعض الوظائف الأساسية


بعد إتقان اتفاقية الاتصال ، يمكنك البدء في كتابة بعض الإجراءات. لماذا لا تعمم الكود الذي يعرض "Hello world!" لإخراج أي أسطر: دالة _print_msg .

هنا نحتاج إلى وظيفة _strlen أخرى لحساب طول السلسلة. في لغة C ، قد تبدو كالتالي:

 size_t strlen(char *s) { size_t length = 0; while (*s != 0) { //   length++; s++; } //   return length; } 

بمعنى آخر ، منذ بداية السطر ، نضيف 1 إلى القيمة المرجعة لكل حرف باستثناء الصفر. بمجرد ملاحظة الحرف الفارغ ، نعيد القيمة المتراكمة في الحلقة. في أداة التجميع ، يعد هذا _subtract بسيطًا للغاية: يمكنك استخدام وظيفة _subtract المكتوبة مسبقًا كقاعدة:

 _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 

بالفعل ليس سيئا ، أليس كذلك؟ قد تساعد كتابة كود C أولاً ، لأنه يتم تحويل معظمه مباشرة إلى المجمع. يمكنك الآن استخدام هذه الوظيفة في _print_msg ، حيث نطبق كل المعارف المكتسبة:

 _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 

وشاهد ثمار عملنا الشاق ، باستخدام هذه الوظيفة في البرنامج الكامل "Hello، world!".

 _start: enter 0, 0 ;     (    ) push hello_world ;    _print_msg call _print_msg mov eax, 0x01 ; 0x01 = exit() mov ebx, 0 ; 0 =   int 0x80 

صدق أو لا تصدق ، لقد غطينا جميع الموضوعات الرئيسية اللازمة لكتابة برامج تجميع x86 الأساسية! الآن لدينا جميع المواد والنظرية التمهيدية ، لذلك سنركز تمامًا على الشفرة ونطبق المعرفة المكتسبة لكتابة حاسبة RPN الخاصة بنا. ستكون الوظائف أطول بكثير وستستخدم بعض المتغيرات المحلية. إذا كنت تريد أن ترى البرنامج النهائي على الفور ، ها هو .

بالنسبة لأولئك منكم الذين ليسوا على دراية بالرموز البولندية العكسية (تسمى أحيانًا الترميز البولندي العكسي أو تدوين postfix) ، يتم تقييم التعبيرات باستخدام المكدس. لذلك ، تحتاج إلى إنشاء مكدس ، بالإضافة إلى _push و _push لمعالجة هذا المكدس. ستحتاج أيضًا _print_answer وظيفة _print_answer ، التي _print_answer تمثيل سلسلة للنتيجة العددية في نهاية الحساب.

إنشاء المكدس


أولاً ، نحدد المساحة في الذاكرة stack_size ، وكذلك متغير stack_size . من المستحسن تغيير هذه المتغيرات بحيث لا تقع في قسم .rodata ، ولكن في .data .

 section .data stack_size: dd 0 ;   dword (4 )   0 stack: times 256 dd 0 ;    

يمكنك الآن تنفيذ _push و _push :

 _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 

إخراج العدد


_print_answer أكثر تعقيدًا: يجب عليك تحويل الأرقام إلى سلاسل واستخدام العديد من الوظائف الأخرى. _putc وظيفة _putc ، التي _putc حرفًا واحدًا ، ووظيفة mod لحساب ما تبقى من قسم (وحدة) من الوسيطتين و _pow_10 إلى قوة 10. _pow_10 ذلك ستفهم سبب الحاجة إليها. هذا بسيط جدًا ، إليك الرمز:

 _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 

إذن ، كيف نحصل على أرقام فردية في رقم؟ أولاً ، لاحظ أن الرقم الأخير من الرقم هو باقي القسمة على 10 (على سبيل المثال ، 123 % 10 = 3 ) ، والرقم التالي هو باقي القسمة على 100 ، مقسومًا على 10 (على سبيل المثال ، (123 % 100)/10 = 2 ). بشكل عام ، يمكنك العثور على رقم معين لرقم (من اليمين إلى اليسار) من خلال إيجاد ( % 10**n) / 10**(n-1) ، حيث سيكون عدد الوحدات n = 1 ، وعدد العشرات n = 2 وهكذا.

باستخدام هذه المعرفة ، يمكنك العثور على جميع أرقام العدد من n = 1 إلى n = 10 (هذا هو أقصى عدد من البتات في عدد صحيح موقّع من 4 بايت). ولكن من الأسهل الانتقال من اليسار إلى اليمين - حتى نتمكن من طباعة كل حرف بمجرد العثور عليه والتخلص من الأصفار على الجانب الأيسر. لذلك ، نقوم بفرز الأرقام من n = 10 إلى n = 1 .

في لغة C ، سيبدو البرنامج على النحو التالي:

 #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'); } } 

, . :

 %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 

! , . : « printf("%d") ?», , !

, _start — !


, . , .

, 84/3+6* ( 6384/+* ), :

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]

, — , . 30.

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); } 

, , .

 _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 

error_msg .rodata :

 section .rodata ;     error_msg.  db  NASM ;    ,     ; . 0xA =  , 0x0 =    error_msg: db "Invalid input", 0xA, 0x0 

! , . , , , , , RollerCoaster Tycoon!

. ! , .


, :

  1. segfault , .
  2. .
  3. .
  4. .
  5. _strlen C , _print_answer printf .


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


All Articles