Dövrlər
Proqramda bəzən hansısa əməliyyatları müxtəlif şərtdən asılı olaraq, bir neçə dəfə təkrarlamaq lazım gəlir. Yüksək səviyyəli dillərdə bunun üçün xüsusi dövr operatorlarından istifadə olunur (for, while...). Assembler dilində isə hər hansı kod parçasın təkrarlamaq üçün cmp və jmp instruksiyalarından istifadə edirlər. Əslində yüksək səviyyəli dillərdə istifadə olunan dövr operatorları da aşağı səviyyədə cmp və jmp instruksiyaları ilə realizə olunur.
Sadə dövrdən istifadə edən proqram nümunəsi ilə tanış olaq. Verilmiş ədədlər ardıcıllığı içərisində ən böyüyünü tapan proqram.
Tutaq ki, bizə 7 ədəd verilmişdir. Bu ədədlər içərisində ən böyüyünü tapan proqram tərtib edək. Bu zaman biz cərgədən (massiv) istifadə edəcəyik. Cərgənin elan olunması ilə yuxarıda tanış olmuşuq.
#prog8.s
.data
eded_ard:
.long 241, 15, 242, 123, 50, 100, 240
say:
.long 7
.text
.globl _start
.type _start,@function
_start:
movl $0, %ebx
movl $0, %edx
dovr:
cmpl say, %edx
je son
movl eded_ard(,%edx,4), %eax
cmpl %eax, %ebx
jg boyuk
movl %eax, %ebx
boyuk:
incl %edx
jmp dovr
son:
movl $1, %eax
int $0x80
Proqramın izahı:
Proqramın məlumat hissəsində (.data) biz aşağıdakı məlumatları yerləşdiririk.
Əvvəl biz 7 ədəddən ibarət ardıcıllıq elan edirik.
eded_ard:
.long 220, 15, 3, 123, 50, 100, 240
Daha sonra isə, say nişanı.
say:
.long 7
Say dəyişənində biz ədədlərin sayını yerləşdiririk. Bu bizə dövrün bitməsi şərtini yoxlamaq üçün lazımdır.
Daha sonra, proqramın instruksiyalar hissəsini elan edirik.
.text
%edx registrində biz nəzərdən keçirdiyimiz ədədlərin sayını saxlayırıq. Ona görə ilk başlanğıcda bu registrə 0 qiyməti yerləşdiririk. Hələlik heç bir ədədin qiymətini yoxlamamışıq.
movl $0, %edx
%ebx-də isə, ədədlər ardıcıllığından nəzərdən keçirdiyimiz ədədlər içərisindən ən böyüyünü yerləşdiririk. Dövr hər dəfə təkrarlandıqca, cərgənin növbəti elementinin qiyməti %eaxregistrinə köçürülür və onun qiyməti %ebx ilə müqayisə olunur. Əgər böyükdürsə, həmin qiymət %ebx-ə yazılır. Beləliklə, %ebx-də həmişə baxılan ədədlər içərisində ən böyüyü yerləşir. Başlanğıcda isə %ebx-də 0 qiyməti yerləşdirməliyik.
Növbəti sətirdə biz dovr nişanını elan edirik.
dovr:
Bu nişan dövrün başlanğıcı hesab olunur. Daha sonra, biz say dəyişəni ilə %edx-də olan qiyməti müqayisə edirik (cmpl say, %edx). Əgər onlar bərabərdirsə, deməli, bütün ədədlər yoxlanılıb, dövrdən çıxırıq (jmp son).
cmpl say, %edx
je son
%edx-də baxdığımız ədədlərin sayını saxlayırıq və başlanğıcda ona 0 mənimsətmişik. Dövr hər dəfə təkrarlandıqda, biz %edx-in qiymətini 1 vahid artırırıq.
Növbəti instruksiya, hər dəfə dövr təkrar olduqda eded_ard cərgəsinin növbəti elementini %eax registrinə köçürür.
movl eded_ard(,%edx,4), %eax
Burada FİZİKİ ÜNVANIN hesablanma düsturunu yada salsaq, mənbə ünvan aşağıdakı kimi hesablanır:
eded_ard + 0 + %edx*4
eded_ard məlumatın yaddaşdakı ünavanıdır. Mötərizədən sonrakı birinci hədd buraxıldığından, onun qiyməti 0 götürülür. Cəmin üzərinə %edx-lə 4-ün hasili əlavə olunur. Beləliklə, köçürülməli olan məlumatın yekun ünvanı hesablanır. Dövrün başlanğıcında %edx-in qiyməti 0 olduğundan, düsturun nəticəsi elə eded_ard olacaq. Bu isə cərgənin ilk elementinin ünvanıdır. Beləliklə, dövrün başlanğıcında bu instruksiya icra olunduqda cərgənin ilk elementi, yəni 220 %eax-ə yazılır. Dövr hər dəfə təkrarlandıqda, qeyd etdiyimiz kimi, %edx-in qiyməti 1 vahid artır. Bu isə yuxarıdakı düstura görə ünvanın qiymətini 4 vahid artırır və cərgənin növbəti elementinin ünvanını almış oluruq. Cərgənin elementlərinin tipini long elan etdiyimizdən, onun hər bir elementi yaddaşda 4 bayt yer tutur və cərgənin elementləri yaddaşda ardıcıl yerləşir. Buna görə k-cı elementin ünvanını almaq üçün ilk elementin ünvanının üzərinə (k-1)*4 əlavə etməliyik.
Növbəti instruksiyalar aşağıdakı kimidir:
cmpl %eax, %ebx
jg boyuk
movl %eax, %ebx
Burada %ebx nəzərdən keçirdiyimiz ədərlər içərisində ən böyüyünü, %eax isə, cərgənin növbəti elementinin qiymətini özündə saxlayır. Əgər %ebx %eax-dən böyükdürsə, onda %eax-də olan qiymət bizim üçün maraqlı deyil və biz jb boyuk instruksiyası ilə boyuk nişanına keçid edirik. Harada ki, yeni dovrə keçid işləri üçün hazırlıq işləri görülür və yeni dövrə keçid edilir. Lakin əks halda, yəni %eax %ebx-dən böyük olarsa, deməli cərgənin hal-hazırda baxılan qiyməti indiyə kimi baxdığımız qiymətlərdən böyükdür və maksimum olaraq onu götürməliyik. Bu halda artıq jg boyuk (jump great) instruksiyası icra olunmur və növbəti instruksiya - movl %eax, %ebx instruksiyası icra olunur və %eax-in qiymətini %ebx-ə yazır. Nəticədə %ebx-də baxılan ədədlərin içərisindən ən böyüyü yerləşir.
Daha sonra, proqram kodu aşağıdakı kimidir:
boyuk:
incl %edx
jmp dovr
Bu arada, artıq qeyd elədiyimiz kimi, "boyuk" nişanı elan olunur. incl %edx instruksiyası %edx-in qiymətini 1 vahid artırır. Daha sonra, "jmp dovr" instruksiyası vastəsilə dövrün başlanğıcına (dovr nişanı) keçid edilir.
Hər dəfə dövr təkrarlandıqda, %edx-in qiyməti 1 vahid artdığından, %edx-in qiyməti say qiymətinə bərabər olduqda, son nişanına keçid edilir və proqram sona çatır.
incl və decl instruksiyaları
incl, decl instruksiyaları müvafiq olaraq arqumentlərinin qiymətlərini bir vahid artırır, azaldır. Bu instruksiyaların əvəzinə addl $1, arq və ya subl %1, arq-dan da istifadə edə bilərik, lakin bunun üçün hazır instruksiya mövcud olduğundan, ondan istifadə edirik. Həm də, bu zaman eyni nəticənin alınmasına daha az CPU vaxtı sərf olunur.
"Salam dünya" proqramı
Artıq biz assembler dilində bir qədər mürəkkəb proqramlar tərtib edə bilərik. İndiyə kimi biz nəticəni yoxlamaq üçün ancaq %ebx registrinin ilk baytından istifadə edə bilirdik. İndi isə, bizə lazım olan məlumatı ekranda çap etməni öyrənək. Elə proqram tərtib edək ki, ekranda %ecx registrinin qiymətini çap etsin.
Qeyd edim ki, ilk başlanğıcda bu, kifayət qədər çətin məsələ olduğundan, biz hələ ki, daha asan məsələ üzərində çalışaq. Ekranda verilmiş simvolu, məsələn 'a' simvolunu çap edən proqram tərtib edək. Əvvəlcə proqramı daxil edək, daha sonra izahını verərik.
#proq9.s
#Ekranda kiçik 'a' simvolu çap edən proqram
.data
simvol:
.ascii "a"
.text
.globl _start
.type _start, @function
_start:
movl $4, %eax
movl $1, %ebx
movl simvol, %ecx
movl $1, %edx
int $0x80
movl $1, %eax
int $0x80
Əgər biz bu proqramı icra eləsək, onda aşağıdakı nəticəni alarıq:
[user@unix progs_as]$
[user@unix progs_as]$ as prog9.s -o prog9.o
[user@unix progs_as]$ ld prog9.o -o prog9
[user@unix progs_as]$ ./prog9
a[user@unix progs_as]$
[user@unix progs_as]$
Proqramın nəticəsinin daha aydın seçilməsi üçün biz 'a' simvolundan sonra yeni sətir - '\n' simvolunu da çap etməliyik. Müvafiq proqram aşağıdakı kimi olar:
.data
simvol:
.ascii "a\n"
.text
.globl _start
.type _start, @function
_start:
movl $4, %eax
movl $1, %ebx
movl $simvol, %ecx
movl $2, %edx
int $0x80
movl $1, %eax
int $0x80
Nəticə:
[user@unix progs_as]$
[user@unix progs_as]$ as prog10.s -o prog10.o
[user@unix progs_as]$ ld prog10.o -o prog10
[user@unix progs_as]$ ./prog10
a
[user@unix progs_as]$
proq9.s proqramının izahı (proq10.s analojidir):
Proqramın əvvəlində biz .data hissəsində ascii tipindən olan simvol nişanı elan edirik və bu yerə "a" məlumatını, ekranda çap etmək istədiyimiz sətri yerləşdiririk.
Bizim məqsədimiz verilmiş sətri ekranda çap etməkdir. Bunun üçün %eax, %ebx, %ecx və %edx registrlərinə lazım olan qiymətləri yerləşdirməli, daha sonra isə, int $0x80 instruksiyasını icra etməliyik. %eax və %ebx registrlərinə müvafiq olaraq 4 və 1 qiymətləri yerləşdirilməlidir. %ecx registrinə çap olunmalı sətrin yaddaşdakı ünvanı, %edx-ə isə, həmin ünvandan başlayaraq çap olunmalı simvolların sayı yerləşdirilmədir.
Proqramın .text hissəsində ilk iki instruksiyası aşağıdakı kimidir:
movl $4, %eax
movl $1, %ebx
Bu instruksiyalar %eax və %ebx registrlərinə müvafiq olaraq 4 və 1 qiymətlərini yazırlar. Daha sonra, movl $simvol, %ecx instruksiyası ilə simvol nişanın ünvanını %ecx-ə yerləşdiririk. movl $1, %edx isə, %edx-ə 1 qiymətini yazır. Yəni "cəmi 1 simvol çap et". Bütün bunlar hamısı hazırlıq işləri idi, sadəcə prosessorun registrlərinə lazımı məlumatlar yerləşdirirdi və bu zaman ekranda heç nə çap olunmur. Real iş isə, int $0x80 instruksiyanı yerinə yetirən zaman baş verir. Bu zaman ekranda istədiyimiz sətir çap olunar. int instruksiyası kəsilmə instruksiyasıdır.
Ekranda tələb olunan simvolu çap etdikdən sonra proqram başa çatır, bunun üçün
movl $1, %eax
int $0x80
instruksiyalarını icra edirik.
Başqa bir nümunə, ekranda "Salam dünya" sətrini çap edən proqram tərtib edək. Artıq bu proqramı özünüz sərbəst tərtib edə bilməlisiniz. Proqram aşağıdakı kimi olar:
.data
simvol:
.ascii "Salam dünya\n"
.text
.globl _start
.type _start, @function
_start:
movl $4, %eax
movl $1, %ebx
movl $simvol, %ecx
movl $12, %edx
int $0x80
movl $1, %eax
int $0x80
Nəticə:
[user@unix progs_as]$
[user@unix progs_as]$ as prog11.s -o prog11.o
[user@unix progs_as]$ ld prog11.o -o prog11
[user@unix progs_as]$ ./prog11
Salam dünya
[user@unix progs_as]$
Ekranda simvol və ya sətir çap etməni öyrəndik. İndi bundan daha mürəkkəb olan məsələ ilə, verilmiş ədədin ekranda çap olunması ilə məşğul olaq. Əvvəl proqramı daxil edək, sonra izahı verərik.
#prog12.s
#məlumatlar hissəsi
.data
#çap etmək istədiyimiz ədəd
dey:
.long 907841
onluq_simvollar:
.ascii "0123456789"
say:
.long 0
yeni_setir:
.ascii "\n"
#kod hissəsi
.text
.globl _start
.type _start, @function
_start:
dovr:
movl $10, %edi
movl $0, %edx
movl dey, %eax
div %edi
pushq %rdx
movl %eax, dey
incl say
movl $0, %esi
cmpl dey, %esi
je dovr_son
jmp dovr
dovr_son:
movl $0, %esi
cap_et:
cmpl %esi, say
je yeni_str_cap
popq %rdi
movl $4, %eax
movl $1, %ebx
movl $onluq_simvollar, %ecx
addl %edi, %ecx
movl $1, %edx
int $0x80
incl %esi
jmp cap_et
yeni_str_cap:
#yeni sətir çap et
movl $4, %eax
movl $1, %ebx
movl $yeni_setir, %ecx
movl $1, %edx
int $0x80
son:
movl $1, %eax
int $0x80
Proqramı icra edək:
[user@unix progs_as]$
[user@unix progs_as]$ as prog12.s -o prog12.o
[user@unix progs_as]$ ld prog12.o -o prog12
[user@unix progs_as]$ ./prog12
907841
[user@unix progs_as]$
Proqramın izahı:
Proqramın məlumatlar hissəsində (.data) long tipindən dey nişanı elan edirik və buraya çap etmək istədiyimiz ədədi - 907841 ədədini yerləşdiririk. Bu ədədi ekranda çap etmək üçün aşağıdakı qaydadan istifadə edirik. Əvvəlcə verilmiş ədədin rəqəmlərini sonuncudan birinciyə kimi bir-bir tapıb stekə yerləşdiririk. Stekin qısa izahı irəlidə verilir, daha ətraflı izah isə, növbəti mövzularda verilir.
Daha sonra, stekdən bu ədədləri bir-bir çıxarıb onların uyğun gəldikləri simvolu ekranda çap edirik. Burada əsas məsələlərdən biri verilmiş ədədin təşkil olduğu rəqəmləri tapmaqdır. Bunun üçün müxtəlif üsullar olsa da, biz 10-a bölmə və qalığı hesablama qaydasından istifadə edirik. Misal üçün, tutaq ki, 497 ədədinin rəqəmlərini tapmaq istəyirik. Bunun üçün yuxarıdakı qaydanı tətbiq eləsək, əvvəlcə 497-ni 10-a bölək. Qismət 49, qalıq isə 7 alarıq. 7 rəqəmlərdən biridir. Daha sonra, eyni qaydanı qismətə tətbiq edirik. 49-u 10-a bölsək, qismət 4, qalıq isə 9 alarıq. Beləliklə, 2-ci rəqəmi də tapdıq. Bu prosesi qismətdə 0 alana kimi davam etdirsək, verilmiş ədədin bütün rəqəmlərini alarıq. Lakin bu üsulun çatışmayan bir cəhəti ondadır ki, bu üsulla ədədin rəqəmləri əks ardıcıllıqla, əvvəldən axıra alınır. Misal üçün, yuxarıdakı nümunədə 497 ədədi üçün biz 7, 9 və 4 kimi nəticə alacayıq. Bu şəkildə biz onu çap edə bilmərik. Bu problemi stekdən istifadə etməklə asanlıqla həll etmək mümkündür. Stek yaddaşda müəyyən bir sahədir. Bu sahəyə məlumat yerləşdirmək və stekdə olan məlumatı götürmək üçün pushl və popl instruksiyalarından istifadə olunur. Məlumatlar yerləşmə ardıcıllığının əksi istiqamətində stekdən çıxarılır.
Beləliklə, bütün rəqəmləri stekə yerləşdirdikdən sonra, onları bir-bir stekdən çıxarıb, uyğun gəldikləri ascii simvolun ekranda çap etməliyik. Bunun üçün .data hissəsindəonluq_simvollar nişanı elan edirik və burada 0-dan 9-a kimi ascii simvolları yerləşdiririk. Simvol çap etmə ilə yuxarıda tanış olmuşuq. onluq_simvollar nişanı bu simvollar sətrinin ilk elementinin, "0" simvolunun ünvanını özündə saxlayır. Hər bir ascii simvolu yaddaşda bir bayt yer tutduğundan, verilmiş rəqəmə uyğun simvolun ünvanın almaq üçün onluq_simvollar nişanının üzərinə həmin rəqəmi əlavə etməliyik.
Çalışmalar:
1. Verilmiş registrin qiymətini ekranda çap edən proqram tərtib edin.
2. Verilmiş registrin qiymətini ekranda 16-lıq (hex) say sistemində çap edən proqram tərtib edin.
3. Proqramda hər hansı dəyişən elan edin, onun ünvanını ekranda çap edən proqram tərtib edin.
4. _start nişanının ünvanını çap edən proqram tərtib edin.