asm32.info
Keep it simple — code in asm

Глава 6. Пишем преносимa програма

1. Увод

По принцип, четните статии в цикъла са практически. Затова няма да Ви занимавам с много теория, а ще преминем по-бързичко към примерите.

В тази статия, ще напишем програма на асемблер, която ще може да се компилира за Windows и за Linux.

За целта ще използваме библиотеката FreshLib, която осигурява прост начин за писане на преносими програми на асемблер. Нещо повече, ако в бъдеще FreshLib започне да поддържа друга операционна система, то нашата програма ще може без промени да се компилира и за нея.

Библиотеката е предназначена главно за програмиране в средaта Fresh, но нищо не пречи да се използва с коя да е версия на flat assembler. Ние ще я използваме с FASMW.

За да е упражнението по-полезно и приятно, ще направим програма, която да върши нещо полезно. Да преброява думите в зададен файл и да извежда този брой на конзолата.

Особенното при използване на реален проект в урок като този е, че в първият момент, програмата може да ви се стори твърде сложна. Работата е там, че в реална програма се налага да се извършват дейности, които не са пряко свързани с пряката дейност на програмата. Става въпрос за всякакви проверки от рода на входните параметри, валидността на данните, обработка на всякакви видове грешки и др.под.

Аз обаче съзнателно избрах този подход към примерната програма, защото рано или късно всеки програмист трябва да се научи да прави тези неща правилно и колкото по-рано, толкова по-добре.

Още повече, че на "втори поглед" се оказва, че тази сложност е само видимост, а програмата всъщност е по-проста отколкото ни се струва.

Така че, смело напред!

2. Инсталиране на библиотеката FreshLib

Но първо, ще трябва да се снабдим с библиотеката. Това е просто - сваляме си я от интернет. Разопаковайте архива някъде на знайно място.

3. Настройка на FASMW

Ще ни трябва FASMW с версия от 1.70 нагоре. Как да инсталирате FASMW и как да го настроите, можете да прочетете в Глава 0 от този цикъл статии.

Допълнително, добавете във файла FASMW.INI (от папката на FASMW), в секцията "Environment", следните редове:

lib = знайното място
TargetOS = Win32

В горният код, "знайното място" е директорията, където сте разопаковали библиотеката FreshLib. В тази директория се намират файловете "freshlib.inc" и "freshlib.asm".

Запишете FASMW.INI файла и стартирайте FASMW.

От настройките на FASMW "Options|Compiler setup" задайте памет за компилатора 131072KB.

4. Въвеждане на кода, компилиране за Windows и Linux

Въведете следният сорс (можете да го изтеглите като zip файл. ):

include "%lib%/freshlib.inc"

_BinaryType console     ; казваме на компилатора, че искаме конзолна програма.

include "%lib%/freshlib.asm"

start:
        InitializeAll             ; Инициализираме всички библиотеки.

        stdcall GetCmdArguments   ; тази функция връща масив пълен със стрингове, съдържащи
                                  ; аргументите от командната линия. Първият елемент от
                                  ; масива винаги съдържа името на програмата,
                                  ; тоест, на нас ни трябва вторият елемент от списъка.

        mov     edi, eax          ; функцията връща указател към динамичен масив, който във
                                  ; FreshLib има тип TArray

        cmp     [edi+TArray.count], 2
        jne     .error_arguments        ; ако аргумента не е точно един, печатаме
                                        ; грешка и излизаме.

        stdcall LoadBinaryFile, [edi+TArray.array+4] ; вторият аргумент е името на файла.
                                                     ; прочитаме го в паметта.
        jc      .error_file_read

        mov     esi, eax                ; това е указател към файла в паметта.
        mov     ebx, eax                ; запазваме адреса в ebx, за да можем да освободим
                                        ; паметта, когато приключим.

; проверяваме дали файла започва с BOM (windows notepad слага

        mov     eax, [esi]
        and     eax, $ffffff

        cmp     ax,  $FEFF    ; low endinan unicode - не се поддържа.
        je      .error_format
        cmp     ax,  $FFFE    ; big endian unicode - не се поддържа.
        je      .error_format

        cmp     eax, $BFBBEF    ; UTF-8 BOM
        jne     .utf8_ok

        add     esi, 3  ; прескачаме символа BOM, за да не го преброим като дума.

.utf8_ok:

; започваме да броим думите.
; в AL ще прочитаме всеки пореден символ, а в AH ще пазим символът от
; предишното преминаване през цикъла.
; Дума ще преброяваме тогава, когато срещнем символ за интервал и предишният
; символ е бил различен от интервал.
        xor     ecx, ecx
        xor     eax, eax

.loop:
        mov     al, [esi]
        inc     esi

        cmp     al, ' ' ; ако поредният символ е интервал (всъщност всички контролни
                        ; символи се броят за интервал, тоест и празните редове, табове
                        ; и т.н., защото и те могат да разделят думи.
        jbe     .space

.next:
        mov     ah, al
        jmp     .loop

; ако е интервал...
.space:
        cmp     ah, ' '
        jbe     .not_a_word

        inc     ecx

.not_a_word:
        test    al, al
        jnz     .next

; Извеждаме резултата от броенето по интелигентен начин
.file_end:

; началото на съобщението: "The file contains "
        stdcall FileWrite, [STDOUT], cOutputMessage1, cOutputMessage1.length    ; начало на съобщението

; броят в ECX го превръщаме в стринг.
; за можем да го изведем.
        stdcall NumToStr, ecx, ntsUnsigned or ntsDec
        mov     edx, eax                                ; запазваме стринга за да го освободим по-късно

        stdcall StrLen, edx     ; дължината на стринга отива в ESI
        mov     esi, eax
        stdcall StrPtr, edx     ; адреса на стринга се връща в EAX

        stdcall FileWrite, [STDOUT], eax, esi   ; извеждаме броя.
        stdcall StrDel, edx                     ; изтриваме стринга, защото не ни трябва повече.

; сега трябва да решим дали ще извеждаме "word" ако броят е равен на 1
; или "words" ако броят е повече от 1.

        mov     eax, cOutputMessage3            ; приготвяме се за множествено число.
        mov     edx, cOutputMessage3.length

        cmp     ecx, 1
        jne     .suffix

        mov     eax, cOutputMessage2            ; броят все пак е 1, тоест трябва да извеждаме в единствено число.
        mov     edx, cOutputMessage2.length

.suffix:
        stdcall FileWrite, [STDOUT], eax, edx

        stdcall FreeMem, ebx
        jmp     .finish

; Обработка на различните грешки. Извеждането на съобщението става
; всъщност от .display_error като адреса на съобщението трябва да е в EAX,
; а дължината му в ECX

.error_format:
        mov     eax, cErrorFormat
        mov     ecx, cErrorFormat.length
        jmp     .display_error

.error_arguments:
        mov     eax, cErrorArguments
        mov     ecx, cErrorArguments.length
        jmp     .display_error

.error_file_read:
        mov     eax, cErrorFileRead
        mov     ecx, cErrorFileRead.length

.display_error:
        stdcall FileWrite, [STDERR], eax, ecx

; Приключваме програмата
.finish:
        FinalizeAll             ; финализираме всички библиотеки.
        stdcall Terminate, 0


; Съобщения за грешки.

cErrorArguments text 'Error! Wrong argument count.', 13, 10, 'Usage: word_count filename', 13, 10
cErrorFileRead  text 'Error read file.', 13, 10
cErrorFormat    text 'Error! Unsupported format.', 13, 10

; Текстове, с които се извежда резултата.

cOutputMessage1 text 'The file contains '
cOutputMessage2 text ' word.', 13, 10
cOutputMessage3 text ' words.', 13, 10

; Автоматично генериране на таблицата за импорт и блоковете с данни.
; В случая и данните и импортите се генерират вътре в кодовата секция на програмата.
;
; Ако се използват _AllImportSection и _AllDataSection тези блокове ще се генерират
; като отделни секции. Това обаче ще доведе до по-голям изпълним файл във Windows.

_AllImportEmbeded
_AllDataEmbeded

Запишете кода под името "word_count.asm" и го компилирайте с Ctrl+F9. Ще получите изпълним файл с име "word_count.exe", тъй като до тук компилирахме за Windows - ако си спомняте реда: TargetOS = Win32 в FASMW.INI;

Сега, отворете отново FASMW.INI и сменете въпросният ред на TargetOS=Linux. Компилирайте отново. Сега ще получите файл с име "word_count" - това е програмата, компилирана за Линукс.

Можете да тествате всяка от двете програми, като в конзолата напишете:

word_count word_count.asm

С което ще накараме програмата да преброи думите в собственият си текст.

5. Анализ на кода

Сега е време да анализираме сорса на програмата, блок по блок, за да разберем точно кое как работи.

Ако вече сте разгледали сорса на програмата, сигурно вече сте забелязали, че повече от половината са коментари, които разясняват каква е логиката на отделните редове.

В програмата могат да се обособят няколко логически блока:

5.1. Анализ на входните параметри

        stdcall GetCmdArguments   ; тази функция връща масив пълен със стрингове, съдържащи
                                  ; аргументите от командната линия. Първият елемент от
                                  ; масива винаги съдържа името на програмата,
                                  ; тоест, на нас ни трябва вторият елемент от списъка.

        mov     edi, eax          ; функцията връща указател към динамичен масив, който във
                                  ; FreshLib има тип TArray

        cmp     [edi+TArray.count], 2
        jne     .error_arguments        ; ако аргумента не е точно един, печатаме
                                        ; грешка и излизаме.

В този блок прочитаме от системата, аргументите зададени в конзолата при стартиране и ги анализираме за грешки.

В случая, анализът е простичък - проверяваме само броя на аргументите. Тъй като първият от аргументите е името на програмата, то на нас ни трябват точно 2 аргумента. Ако броят им е различен, отиваме да изведем съобщение и да приключим програмата.

5.2. Прочитане на файла в паметта

        stdcall LoadBinaryFile, [edi+TArray.array+4] ; вторият аргумент е името на файла.
                                                     ; прочитаме го в паметта.
        jc      .error_file_read

        mov     esi, eax                ; това е указател към файла в паметта.
        mov     ebx, eax                ; запазваме адреса в ebx, за да можем да освободим
                                        ; паметта, когато приключим.

Тук използваме функция от FreshLib, която прочита целият файл в паметта и връща указател към прочетената информация в EAX.

Паметта е динамично заделена, така че, когато не ни трябва повече, трябва да я освободим.

Забележете, че ако при четенето е възникнала грешка, флага CF ще има стойност 1 при връщане от процедурата. В този случай, EAX ще съдържа код на грешката, а не адрес в паметта. Трябва да обработваме този случай, за да се избегне обработката на несъществуващи данни и забиване на програмата.

5.3. Проверка на формата на файла

; проверяваме дали файла започва с BOM (windows notepad слага такъв и на UTF-8 файловете)

        mov     eax, [esi]
        and     eax, $ffffff  ; трябват ни само първите 3 символа от файла, затова нулираме 4-тия.

        cmp     ax,  $FEFF    ; low endinan unicode - не се поддържа.
        je      .error_format
        cmp     ax,  $FFFE    ; big endian unicode - не се поддържа.
        je      .error_format

        cmp     eax, $BFBBEF    ; UTF-8 BOM
        jne     .utf8_ok

        add     esi, 3  ; прескачаме символа BOM, за да не го преброим като дума.

.utf8_ok:

Тук се опитваме да установим в какъв формат е файла и можем ли да го обработим. Нашата програма ще обработва файлове в ANSI или UTF-8 формат.

Формати с 2 байта на символ няма да се поддържат и съответно е добре да можем да установим този случай и да завършим програмата културно - със съобщение за грешка.

Допълнително, при кодировка UTF-8 в началото на файла може да има, а може и да няма BOM символ (byte order mark) който да показва, че формата е UTF-8. Нас този символ не ни интересува, но трябва да го прескачаме, ако го има, за да не го преброим като дума от текста.

5.4. Същинската работа

; започваме да броим думите.
; в AL ще прочитаме всеки пореден символ, а в AH ще пазим символът от
; предишното преминаване през цикъла.
; Дума ще преброяваме тогава, когато срещнем символ за интервал и предишният
; символ е бил различен от интервал.
        xor     ecx, ecx
        xor     eax, eax

.loop:
        mov     al, [esi]
        inc     esi

        cmp     al, ' ' ; ако поредният символ е интервал (всъщност всички контролни
                        ; символи се броят за интервал, тоест и празните редове, табове
                        ; и т.н., защото и те могат да разделят думи.
        jbe     .space

.next:
        mov     ah, al
        jmp     .loop

; ако е интервал...
.space:
        cmp     ah, ' '
        jbe     .not_a_word

        inc     ecx

.not_a_word:
        test    al, al
        jnz     .next

В този блок от програмата се извършва същинската дейност, заради която започнахме всичко - броенето на думите.

Как става това? Прочитаме текста символ по символ в цикъл, като за всеки символ проверяваме дали не е "интервал".

В случая за "интервал" се приемат всички символи, които са по-малки или равни на шпация (код 32 десетично и $20 шестнайсетично). По този начин в понятието "интервал" включваме и символите за таб, нов ред и край на файл, които също могат да отделят думите една от друга.

За дума приемаме ситуацията в която срещнем символ за интервал, а предишният обработен символ е бил "не интервал". В този случай преброяваме дума.

За край на файла се смята символ с код 0.

5.5. Извеждане на резултата

; Извеждаме резултата от броенето по интелигентен начин
.file_end:

; началото на съобщението: "The file contains "
        stdcall FileWrite, [STDOUT], cOutputMessage1, cOutputMessage1.length    ; начало на съобщението

; броят в ECX го превръщаме в стринг.
; за можем да го изведем.
        stdcall NumToStr, ecx, ntsUnsigned or ntsDec
        mov     edx, eax                                ; запазваме стринга за да го освободим по-късно

        stdcall StrLen, edx     ; дължината на стринга отива в ESI
        mov     esi, eax
        stdcall StrPtr, edx     ; адреса на стринга се връща в EAX

        stdcall FileWrite, [STDOUT], eax, esi   ; извеждаме броя.
        stdcall StrDel, edx                     ; изтриваме стринга, защото не ни трябва повече.

; сега трябва да решим дали ще извеждаме "word" ако броят е равен на 1
; или "words" ако броят е повече от 1.

        mov     eax, cOutputMessage3            ; приготвяме се за множествено число.
        mov     edx, cOutputMessage3.length

        cmp     ecx, 1
        jne     .suffix

        mov     eax, cOutputMessage2            ; броят все пак е 1, тоест трябва да извеждаме в единствено число.
        mov     edx, cOutputMessage2.length

.suffix:
        stdcall FileWrite, [STDOUT], eax, edx

        stdcall FreeMem, ebx
        jmp     .finish

Извеждането, макар и дълго не е нещо особенно. Извеждаме началото на съобщението "The file contains " след това извеждаме преброеното количество, като първо чрез FreshLib процедурата NumToStr го превръщаме в стринг и го извеждаме на конзолата.

След това извеждаме края на съобщението "words." и символите за нов ред. В този код има една особенност, ако броя на думите е 1, то извеждаме думата "word" в единствено число, а ако броят е различен от 1, то извеждаме "words" в множествено число.

Разбира се това е донякъде излишно, но какво пък... :)

Накрая освобождаваме заетата памет и отиваме на края на програмата.

5.6. Обработка на грешки

; Обработка на различните грешки. Извеждането на съобщението става
; всъщност от .display_error като адреса на съобщението трябва да е в EAX,
; а дължината му в ECX

.error_format:
        mov     eax, cErrorFormat
        mov     ecx, cErrorFormat.length
        jmp     .display_error

.error_arguments:
        mov     eax, cErrorArguments
        mov     ecx, cErrorArguments.length
        jmp     .display_error

.error_file_read:
        mov     eax, cErrorFileRead
        mov     ecx, cErrorFileRead.length

.display_error:
        stdcall FileWrite, [STDERR], eax, ecx

В обработката на грешки няма нищо особенно. Зареждаме адреса на съобщението за грешка в EAX, а дължината му в ECX и отиваме на .display_error, където грешката се извежда на изхода за грешки STDERR.

5.7. Дефиниране на текстовите константи

; Съобщения за грешки.

cErrorArguments text 'Error! Wrong argument count.', 13, 10, 'Usage: word_count filename', 13, 10
cErrorFileRead  text 'Error read file.', 13, 10
cErrorFormat    text 'Error! Unsupported format.', 13, 10

; Текстове, с които се извежда резултата.

cOutputMessage1 text 'The file contains '
cOutputMessage2 text ' word.', 13, 10
cOutputMessage3 text ' words.', 13, 10

Накрая дефинираме и текствовите константи, които са ни нужни в различните части на програмата. Всъщност, не е необходимо тези дефиниции да са на едно място. В случая съм ги събрал, за да могат да се възприемат заедно.

Обикновенно такива константи се декларират в мястото, където са нужни.

Когато дефинираме текстови константи с макроса "text", то те се разполагат в секцията за данни на програмата, а не там където са дефинирани. Това позволява "text" да се използва там където е нужен стринг, а подреждането остава за компилатора.

6. Използвани инструкции и кратък справочник.

Съвсем накратко кои инструкции се използват в примерната програма и какво прави всяка от тях. За подробно описание на инструкциите ще трябва да потърсите в интернет.

stdcall - това всъщност не е инструкция, а макрос, който вкарва зададените аргументи в стека и вика зададената функция, според конвенцията "STDCALL".

stdcall procedure, arg1, arg2, arg3 ще се компилира до следният код:

         push arg3       ; вкарва в стека agr3
         push arg2
         push arg1
         call procedure  ; вика подпрограмата "procedure"

jc - преход при флаг CF=1

jnc - преход при флаг CF=0

je, jz - преход при равно (равно на нула) - това е всъщност една инструкция, която има две имена.

jne, jnz - преход при не равно (не равно на нула) - това също е една инструкция.

jbe - преход при "под" или "равно". Представлява преход при по-малко или равно, при числа без знак.

jae - преход при "над" или "равно". Преход при по-голямо или равно, при числа без знак.

jl, jle, jg, jge - преход съответно при по-малко, по-малко или равно, по-голямо, по-голямо или равно, за числа със знак.

inc - увеличава операнда с единица.

cmp - сравнява двата операнда един с друг и установява съответните стойности на флаговете. Операндите не се променят. След тази инструкция, обикновенно следва условен преход от някакъв вид.

xor ecx, ecx извършва операция "изключващо или" на двата операнда. Ако двата операнда са еднакви, ресултата е нула. Подобен начин за нулиране е общоприет, по-кратък е от mov ecx, 0 и е донякъде по-четлив, защото така декларираме, че искаме именно да нулираме регистъра, а не да вкараме в него някаква константа, която случайно е нула.

and - логическа "операция И".

add - операция аритметично събиране.

7. Заключение

Разгледаната програма, макар и далеч от съвършенството, ще върши работата за която е създадена.

На много места, съзнателно са използвани конструкции, които биха могли да се напишат и по-икономично, но яснотата на кода би пострадала.

В качеството на упражнение, можете да се опитате да потърсите подобни места в програмата и да ги подобрите.

Например, думата "word" и "words" се различават по един символ. Можем да оптимизираме печатането на резултата, като първо напечатаме "word" и след това добавим "s" ако числото е по-различно от 1.

Това е засега. В следващата 7-ма статия ще се занимаваме с някой теоретичен въпрос.

©2012 John Found

Last modified on: 13.06.2012 20:02:46

Preview

Comments

:)Gail:

Passion the site-- extremely user friendly and whole lots to see!

Feel free to surf to my site ... <a href="http://fastestweightlossmethodknown.blogspot.com">ways to lose weight fast</a>

:)Raleigh:

Hmm it looks like your website ate my first comment (it was super long) so I guess I'll just sum it up what I had written and say, I'm thoroughly enjoying your blog. I as well am an aspiring blog writer but I'm still new to the whole thing. Do you have any suggestions for newbie blog writers?

I'd really appreciate it.

:)Alphonso:

I know this website presents quality dependent articles and additional data, is there any other web site which offers these information in quality?

:)Marissa:

After checking out a few of the blog articles on your blog, I truly like your technique of writing a blog. I book marked it to my bookmark webpage list and will be checking back in the near future. Please visit my website as well and tell me your opinion.

6. Пишем преносима програма
Filename:
Title: