शुरुआती के लिए X86 असेंबलर गाइड

आजकल, शुद्ध असेंबलर में लिखना बहुत कम आवश्यक है, लेकिन मैं निश्चित रूप से प्रोग्रामिंग में रुचि रखने वाले किसी व्यक्ति को यह सलाह देता हूं। आप चीजों को एक अलग कोण से देखेंगे, और अन्य भाषाओं में कोड डीबग करने पर कौशल काम आएगा।

इस लेख में, हम शुद्ध x86 असेंबलर में एक रिवर्स पोलिश नोटेशन (RPN) कैलकुलेटर को स्क्रैच से लिखेंगे। जब हम कर लेते हैं, हम इसे इस तरह उपयोग कर सकते हैं:

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

लेख के लिए सभी कोड यहाँ है । यह बहुतायत से टिप्पणी की गई है और उन लोगों के लिए शैक्षिक सामग्री के रूप में काम कर सकती है जो पहले से ही कोडांतरक जानते हैं।

चलो मूल हैलो विश्व कार्यक्रम लिखकर शुरू करते हैं ! पर्यावरण सेटिंग्स की जांच करने के लिए। फिर सिस्टम कॉल, कॉल स्टैक, स्टैक फ़्रेम और x86 कॉलिंग कन्वेंशन पर आगे बढ़ते हैं। फिर, अभ्यास के लिए, हम x86 असेंबलर में कुछ बुनियादी कार्य लिखेंगे - और आरपीएन कैलकुलेटर लिखना शुरू करेंगे।

यह माना जाता है कि पाठक को सी और कंप्यूटर आर्किटेक्चर के बुनियादी ज्ञान में कुछ प्रोग्रामिंग अनुभव है (उदाहरण के लिए, प्रोसेसर रजिस्टर क्या है)। चूंकि हम लिनक्स का उपयोग कर रहे हैं, इसलिए आपको लिनक्स कमांड लाइन का उपयोग करने में भी सक्षम होना चाहिए।

पर्यावरण की स्थापना


जैसा कि पहले ही उल्लेख किया गया है, हम लिनक्स (64-बिट या 32-बिट) का उपयोग करते हैं। उपरोक्त कोड विंडोज या मैक ओएस एक्स पर काम नहीं करता है।

स्थापना के लिए, आपको केवल binutils से GNU ld लिंकर की आवश्यकता होती है, जो कि अधिकांश वितरणों पर पहले से स्थापित है, और NASM कोडांतरक। उबंटू और डेबियन पर, आप इन दोनों को एक कमांड से इंस्टॉल कर सकते हैं:

 $ 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 असेंबलर गाइड देखें । सिस्टम कॉल की आगे की चर्चा के साथ, यह सभी अधिक आवश्यक होगा।

निम्न कमांड कोड फ़ाइल को एक ऑब्जेक्ट फ़ाइल में इकट्ठा करते हैं, और फिर निष्पादन योग्य फ़ाइल संकलित करते हैं:

 $ 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 

फिर, उपरोक्त निर्देशों के बजाय, बस रन बनाएं।

सिस्टम कॉल


लिनक्स सिस्टम कॉल ओएस को हमारे लिए कुछ करने के लिए कहता है। इस लेख में, हम केवल दो सिस्टम कॉल का उपयोग करते हैं: एक फ़ाइल या स्ट्रीम पर एक लाइन लिखने के लिए 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 रजिस्टर में सिस्टम कॉल स्टोर करके कॉन्फ़िगर किया गया है, और फिर उस क्रम में edx , edx , edx में इसके तर्क। आप देख सकते हैं कि exit() केवल एक तर्क है - इस मामले में ecx और edx कोई फर्क नहीं पड़ता।

eaxEBXECXEDX
सिस्टम कॉल नंबरARG1aRG2arg3


कॉल स्टैक




एक कॉल स्टैक एक डेटा संरचना है जो प्रत्येक कॉल के बारे में किसी फ़ंक्शन के बारे में जानकारी संग्रहीत करता है। स्टैक में प्रत्येक कॉल का अपना खंड होता है - "फ्रेम"। यह वर्तमान कॉल के बारे में कुछ जानकारी संग्रहीत करता है: इस फ़ंक्शन के स्थानीय चर और रिटर्न पता (जहां फ़ंक्शन निष्पादित होने के बाद कार्यक्रम जाना चाहिए)।

तुरंत मैं एक गैर-स्पष्ट बात पर ध्यान देता हूं: स्टैक स्मृति नीचे बढ़ता है । जब आप स्टैक के शीर्ष पर कुछ जोड़ते हैं, तो इसे पिछले आइटम की तुलना में कम स्मृति पते पर डाला जाता है। दूसरे शब्दों में, जैसे ही स्टैक बढ़ता है, स्टैक के शीर्ष पर स्थित मेमोरी एड्रेस कम हो जाता है। भ्रम से बचने के लिए, मैं आपको हमेशा इस तथ्य की याद दिलाता रहूंगा।

push निर्देश स्टैक के शीर्ष पर कुछ pop , और वहां से डेटा पॉप करता है। उदाहरण के लिए, push स्टैक के शीर्ष पर एक जगह आवंटित करता है और eax रजिस्टर से मूल्य रखता है, और pop स्टैक के शीर्ष से किसी भी डेटा को pop स्थानांतरित करता है और इस मेमोरी क्षेत्र को मुक्त करता है।

esp रजिस्टर का उद्देश्य स्टैक के शीर्ष पर इंगित करना है। esp ऊपर किसी भी डेटा को स्टैक को हिट नहीं करने के लिए माना जाता है, यह कचरा डेटा है। एक push (या pop ) कथन को निष्पादित push esp । यदि आप अपने कार्यों की रिपोर्ट देते हैं, तो आप सीधे esp हेरफेर कर सकते हैं।

ebp रजिस्टर esp समान है, केवल यह हमेशा वर्तमान स्टैक फ्रेम के मध्य को इंगित करता है, वर्तमान फ़ंक्शन के स्थानीय चर से पहले (हम इस बारे में बाद में बात करेंगे)। हालांकि, किसी अन्य फ़ंक्शन को कॉल करने पर ebp स्वचालित रूप से नहीं चलता है, इसे हर बार मैन्युअल रूप से किया जाना चाहिए।

X86 वास्तुकला बुला सम्मेलन


X86 में, उच्च-स्तरीय भाषाओं की तरह फ़ंक्शन की कोई अंतर्निहित अवधारणा नहीं है। call goto मूल रूप से सिर्फ jmp ( goto ) से दूसरे मेमोरी एड्रेस पर है। अन्य भाषाओं में फ़ंक्शन के रूप में उपयोग करने के लिए (जो तर्क ले सकते हैं और डेटा वापस कर सकते हैं), आपको कॉलिंग कन्वेंशन का पालन करने की आवश्यकता है (कई सम्मेलन हैं, लेकिन हम सीडीईसीएल का उपयोग करते हैं, सी कंपाइलर और कोडांतरक प्रोग्रामर के बीच x86 के लिए सबसे लोकप्रिय सम्मेलन)। यह यह भी सुनिश्चित करता है कि किसी अन्य फ़ंक्शन को कॉल करते समय नियमित रजिस्टर भ्रमित नहीं होते हैं।

कॉलर नियम


फ़ंक्शन को कॉल करने से पहले, कॉलर को यह करना होगा:

  1. रजिस्टर को सेव करें कि कॉलर को स्टैक पर सेव करना आवश्यक है। कहा जाता है कि फ़ंक्शन कुछ रजिस्टरों को बदल सकता है: डेटा खोने के लिए नहीं, कॉल करने वाले को इसे मेमोरी में सहेजना होगा जब तक कि इसे स्टैक पर धक्का न दिया जाए। ये edx ecx edx और edx । यदि आप उनमें से किसी का उपयोग नहीं करते हैं, तो आप उन्हें बचा नहीं सकते हैं।
  2. रिवर्स ऑर्डर में स्टैक के लिए फ़ंक्शन तर्क लिखें (पहले अंतिम तर्क, अंत में पहला तर्क)। यह आदेश यह सुनिश्चित करता है कि कहा गया फ़ंक्शन सही क्रम में स्टैक से अपने तर्क प्राप्त करता है।
  3. सबरूटिन को बुलाओ।

यदि संभव हो, तो फ़ंक्शन eax में परिणाम बचाएगा। call तुरंत बाद call कॉलर को चाहिए:

  1. स्टैक से फ़ंक्शन तर्क निकालें। यह आमतौर पर जासूसी करने के लिए बाइट्स की संख्या को जोड़कर किया जाता है। यह मत भूलो कि स्टैक नीचे बढ़ता है, इसलिए स्टैक से निकालने के लिए, आपको बाइट्स जोड़ना होगा।
  2. रिवर्स ऑर्डर में स्टैक से पॉपिंग करके सहेजे गए रजिस्टरों को पुनर्स्थापित करें। कहा गया फ़ंक्शन किसी अन्य रजिस्टर को नहीं बदलेगा।

निम्न उदाहरण दर्शाता है कि ये नियम कैसे लागू होते हैं। मान लें कि _subtract फ़ंक्शन दो पूर्णांक (4-बाइट) तर्क लेता है और पहले तर्क को दूसरा ऋण देता है। _mysubroutine सबरूटीन में _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. पिछले फ्रेम से वर्तमान (वर्तमान esp मूल्य) के लिए ebp समायोजित करें।
  3. स्थानीय चर के लिए स्टैक पर अधिक स्थान आवंटित करें, यदि आवश्यक हो, तो esp पॉइंटर को स्थानांतरित करें। जैसे ही स्टैक नीचे बढ़ता है, आपको esp से लापता मेमोरी को घटाना होगा।
  4. स्टैक पर बुलाया दिनचर्या के रजिस्टर को बचाओ। ये ebx , edi और esi । उन रजिस्टरों को सहेजना आवश्यक नहीं है जिन्हें बदलने की योजना नहीं है।

चरण 1 के बाद कॉल स्टैक:



चरण 2 के बाद कॉल स्टैक:



चरण 4 के बाद कॉल स्टैक:



इन आरेखों में, प्रत्येक स्टैक फ्रेम में एक वापसी पता दर्शाया गया है। इसे call स्टेटमेंट द्वारा स्वचालित रूप से स्टैक पर धकेल दिया जाता है। ret स्टैक के ऊपर से पते को पुनः प्राप्त करता है और इसे जम्प करता है। हमें इस निर्देश की आवश्यकता नहीं है, मैंने अभी दिखाया कि फ़ंक्शन के स्थानीय चर ebp से 4 बाइट्स ऊपर क्यों हैं, लेकिन फ़ंक्शन के तर्क ebp नीचे 8 बाइट्स हैं।

अंतिम आरेख में, आप यह भी देख सकते हैं कि फ़ंक्शन के स्थानीय चर हमेशा ebp-4 पता (घटाव यहाँ, क्योंकि हम स्टैक ऊपर बढ़ रहे हैं) से 4 बाइट्स से ऊपर शुरू करते हैं, और फ़ंक्शन के तर्क हमेशा ebp+8 पते से ebp से नीचे 8 ebp दूरी पर होते हैं। ebp+8 (इसके अलावा, क्योंकि हम स्टैक नीचे जा रहे हैं)। यदि आप इस सम्मेलन के नियमों का पालन करते हैं, तो यह किसी भी फ़ंक्शन के चर और तर्कों के साथ होगा।

जब फ़ंक्शन पूरा हो जाता है और आप वापस लौटना चाहते हैं, तो आपको आवश्यक होने पर, फ़ंक्शन के रिटर्न मान के लिए पहले eax सेट करना होगा। इसके अलावा, आप की जरूरत है:

  1. रिवर्स ऑर्डर में स्टैक से पॉपिंग करके सहेजे गए रजिस्टरों को पुनर्स्थापित करें।
  2. यदि आवश्यक हो, चरण 3 में स्थानीय चर द्वारा आवंटित स्टैक पर खाली स्थान: बस ईबीपी में esp स्थापित करके
  3. स्टैक से पॉपिंग करके पिछले फ्रेम के 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 

कुछ बुनियादी कार्य लेखन


कॉलिंग सम्मेलन में महारत हासिल करने के बाद, आप कुछ रूटीन लिखना शुरू कर सकते हैं। उस कोड को सामान्य क्यों नहीं किया जाता है जो "हैलो वर्ल्ड!" प्रदर्शित करता है। किसी भी लाइन को आउटपुट करने के लिए: _print_msg फ़ंक्शन।

यहां हमें स्ट्रिंग की लंबाई को गिनने के लिए एक और _strlen फ़ंक्शन की आवश्यकता है। सी में, यह इस तरह दिख सकता है:

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

दूसरे शब्दों में, लाइन की शुरुआत से, हम शून्य को छोड़कर हर वर्ण के लिए 1 रिटर्न मान जोड़ते हैं। जैसे ही अशक्त चरित्र पर ध्यान दिया जाता है, हम लूप में संचित मूल्य लौटाते हैं। कोडांतरक में, यह भी काफी सरल है: आप पूर्व लिखित _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 

पहले से ही बुरा नहीं है, है ना? सी कोड लिखना सबसे पहले मदद कर सकता है, क्योंकि इसमें से अधिकांश को सीधे कोडांतरक में बदल दिया जाता है। अब आप इस फ़ंक्शन का उपयोग _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 

और पूर्ण कार्यक्रम "हैलो, दुनिया!" में इस फ़ंक्शन का उपयोग करके, हमारी कड़ी मेहनत का फल देखें।

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

मानो या न मानो, हम बुनियादी x86 कोडांतरक कार्यक्रम लिखने के लिए आवश्यक सभी मुख्य विषयों को कवर किया है! अब हमारे पास सभी परिचयात्मक सामग्री और सिद्धांत हैं, इसलिए हम पूरी तरह से कोड पर ध्यान केंद्रित करेंगे और हमारे आरपीएन कैलकुलेटर को लिखने के लिए अधिग्रहीत ज्ञान को लागू करेंगे। फ़ंक्शंस बहुत लंबे होंगे और कुछ स्थानीय चर का भी उपयोग करेंगे। यदि आप तैयार कार्यक्रम को तुरंत देखना चाहते हैं, तो यह यहाँ है

आप में से जो रिवर्स पोलिश नोटेशन (जिसे कभी-कभी रिवर्स पोलिश नोटेशन या पोस्टफिक्स नोटेशन कहा जाता है) से परिचित नहीं होते हैं, तब स्टैक का उपयोग करके अभिव्यक्तियों का मूल्यांकन किया जाता है। इसलिए, आपको एक स्टैक बनाने की आवश्यकता है, साथ ही इस स्टैक में हेरफेर करने के लिए _pop और _push । आपको _print_answer फ़ंक्शन _print_answer भी _print_answer होगी, जो गणना के अंत में संख्यात्मक परिणाम के एक स्ट्रिंग प्रतिनिधित्व को आउटपुट करेगा।

ढेर निर्माण


सबसे पहले, हम अपने स्टैक के लिए मेमोरी में स्थान को परिभाषित करते हैं, साथ ही वैश्विक चर stack_size । इन चर को बदलने की सलाह दी जाती है ताकि वे .rodata सेक्शन में न हों, लेकिन .rodata में .rodata

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

अब आप _push और _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 

संख्या उत्पादन


_print_answer बहुत अधिक जटिल है: आपको संख्याओं को स्ट्रिंग में परिवर्तित करना होगा और कई अन्य कार्यों का उपयोग करना होगा। आपको _putc फ़ंक्शन _putc होगी, जो एक वर्ण को आउटपुट करता है, दो तर्कों के शेष भाग (मॉड्यूल) और _pow_10 गणना करने के लिए mod फ़ंक्शन। 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 तक हल करते हैं।

सी में, कार्यक्रम कुछ इस तरह दिखेगा:

 #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 है

। असेंबलर में, आपको सी में इस कोड जैसा कुछ लागू करने की आवश्यकता है:

 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 

और हम कर रहे हैं! यदि आपके पास है तो अपने सभी दोस्तों को आश्चर्यचकित करें। मुझे उम्मीद है कि अब आप उच्च-स्तरीय भाषाओं में अधिक गर्मजोशी से प्रतिक्रिया करेंगे, खासकर यदि आपको याद है कि कई पुराने कार्यक्रम पूरी तरह से या लगभग पूरी तरह से कोडांतरक में लिखे गए थे, उदाहरण के लिए, मूल रोलरकोस्टर टाइकून!

सभी कोड यहाँ हैपढ़ने के लिए धन्यवाद! यदि आप रुचि रखते हैं तो मैं जारी रख सकता हूं।

आगे की कार्रवाई


आप कई अतिरिक्त कार्यों को लागू करके अभ्यास कर सकते हैं:

  1. यदि प्रोग्राम को कोई तर्क नहीं मिलता है, तो सेगफ़ॉल्ट के बजाय एक त्रुटि संदेश लौटाएं।
  2. इनपुट में ऑपरेटर्स और ऑपरेटरों के बीच अतिरिक्त रिक्त स्थान के लिए समर्थन जोड़ें।
  3. मल्टी-बिट ऑपरेंड के लिए समर्थन जोड़ें।
  4. नकारात्मक संख्या की अनुमति दें।
  5. मानक C लाइब्रेरी_strlen से फ़ंक्शन के साथ बदलें , और कॉल के साथ बदलें _print_answerprintf

अतिरिक्त सामग्री


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


All Articles