Menu

Daxil olun Qeydiyyat

Assembler x86 Linux (64 bit)

Funksiyalar

Funksiyalar

Funksiya çağırmaq üçün call instruksiyasından, funksiyadan geri qayıtmaq üçün isə, ret instruksiyasından istifadə olunur. Funksiya çağırmazdan əvvəl ona ötürüləcək parametrlər və ya onların ünvanları stekə yerləşdirilməlidir.

Gəlin, assembler dilində funksiyadan istifadəyə aid proqramlarla tanış olaq. Funksiyadan istifadə etməklə, iki ədədin cəmini hesablayan proqram tərtib edək.


#prog15.s

   .data

   eded1:
   .long 23

   eded2:
   .long 45

   netice:
   .long
 
   .text
   .globl _start
   .type _start, @function

_start:

#nəticənin ünvanını və ədədləri stekə yerləşdiririk
pushq $netice
pushq eded1
pushq eded2

#cemle funksiyasını çağırırıq
call cemle

#nəticəni %ebx-ə yazırıq
movl netice, %ebx
  
son:
   movl $1, %eax
   int $0x80

#cemle funksiyasının proqram kodu
.type cemle, @function
cemle:
pushq %rbp
movq %rsp, %rbp

#ikinci ədədi %rax-ə köçürək
movq 16(%rbp), %rax

#birinci ədədi %rbx-ə köçürək
movq 24(%rbp), %rbx

#bu iki ədədi cəmləyək
addq %rax, %rbx

#nəticə dəyişəninin ünvanını %rcx-ə yazaq
movq 32(%rbp), %rcx

#aldığımız cəmi nəticə dəyişəninə yazaq
movl %ebx, (%ecx)

#%rbp və %esp registrlərinin qiymətlərini bərpa edək
movq %rbp, %rsp
popq %rbp

#funksiyadan qayıdaq
ret

Proqramı icra edək:


[user@unix progs_as]$ 
[user@unix progs_as]$ as prog15.s -o prog15.o
[user@unix progs_as]$ ld prog15.o -o prog15
[user@unix progs_as]$ ./prog15
[user@unix progs_as]$ echo $?
68
[user@unix progs_as]$ 

İzahı:

Proqramın məlumatlar hissəsində 3 dəyişən elan edirik, eded1eded2 və netice. Daha sonra, aşağıdakı instruksiyalar vasitəsilə, bu məlumatları funksiyaya ötürmək üçün onları stekə yerləşdiririk.

pushq $netice

pushq eded1

pushq eded2

Əvvəlcə stekə netice dəyişəninin ünvanını, daha sonra, eded1 və eded2 dəyişənlərinin qiymətlərini ötürürük. Funksiya eded1 və eded2-nin cəmini hesablayıb netice dəyişəninə yazmalıdır. Gəlin, stekin hazırki vəziyyətinə nəzər salaq.

 


 

Daha sonra, aşağıdakı instruksiya icra olunur:

call cemle

Bu instruksiya yerinə yetirilən zaman icraolunma cemle funksiyasına ötürüləcək. Qeyd edək ki, proqramın hər hansı yerindən icraolunmanı başqa bir yerə yönləndirmək üçün biz jmp instruksiyasından istifadə edirdik. Bəs onda cemle funksiyasının proqram kodu üzərinə sürüşmək üçün niyə jmp instruksiyasından istifadə etmədik? Misal üçün, bu cür: jmp cemle Bu halda da, icraolunma cemle: nişanına sürüşməli idi. Bunun izahını irəlidə funksiyanın çağırılması və ondan geri qayıtma bölməsində verəcəyik. Hələlik isə, onu qeyd edim ki, call instruksiyası əvvəlcə "NÖVBƏTİ İNSTRUKSİYANIN ÜNVANINI" stekə yerləşdirir, daha sonra isə, funksiyanı çağırır. Stekin vəziyyətinə nəzər salaq.

 


 

call cemle instruksiyası icraolunmanı cemle funksiyasına ötürür və cemle nişanına yerləşdirilmiş instruksiyalar icra olunmağa başlayır.

İlk olaraq, aşağıdakı instruksiyalar yerinə yetirilir.


pushq %rbp
movq %rsp, %rbp

pushq %rbp instruksiyası %rbp registrinin qiymətini stekə yerləşdirir, movq %rsp, %rbp instruksiyası isə, stek göstəricisinin qiymətini %rbp registrinə köçürür. Bütün bunlar funksiyanın proqram kodunun icrası zamanı ona ötürülən parametrlərə müraciət edə bilmək və funksiyanın çağırılma yerinə qayıda bilmə üçündür. Stekin vəziyyətinə nəzər salaq.

 


 

Bu işlər hazırlıq işləridir və bir qayda olaraq, bütün funksiyalar məhz bu instruksiyaların icrası ilə başlayır. Daha sonra isə, artıq tələb olunan işlər görülə bilər. Bizdən iki ədədi cəmləmək tələb olunur. Ədədlər stekə yerləşdirilib. Növbəti instruksiya ilə biz ikinci ədədi stekdən %rax registrinə köçürürük. Bunun üçün movq 16(%rbp)%rax instruksiyasından istifadə edirik. Bildiyimiz kimi, bu instruksiya %rbp + 16 ünvanında yerləşən məlumatı %rax registrinə köçürür. Stekə push instruksiyası ilə məlumat yerləşdirəndə stek göstəricisi 8 bayt aşağı sürüşür (azalır). Yəni stekə yerləşdirilən hər bir elementə 8 bayt həcmində yer ayrılır. Aşağıdakı kimi:

 


 

Ən son stekə %rbp registri, ondan əvvəl isə, call instruksiyası tərəfindən növbəti icraolunma ünvanı, başqa sözlə, funksiyanın işini bitirdikdən sonra qayıtmalı olduğu ünvan yerləşdirilib. Analoji olaraq movq 24(%rbp), %rbx instruksiyası birinci ədədin qiymətini %rbx registrinə yazır.

Daha sonra, bu iki qiyməti cəmləyirik və nəticəni %rbx-də saxlayırıq.

addq %rax, %rbx

Cəm hesablanıb, onu netice dəyişəninə yazmalıyıq. netice dəyişəninin ünvanını stekdən %rcx registrinə köçürmək üçün aşağıdakı instruksiyanı icra edirik:

movq 32(%rbp), %rcx

Cəmi ünvanı %ecx-də yerləşən sahəyə, netice nişanına köçürürük:

movl %ebx, (%ecx)

ret instruksiyası ilə funksiyadan qayıtmadan öncə %rsp və %rbp-in funksiya çağırılan andakı qiymətlərini bərpa edirik.


movq %rbp, %rsp
popq %rbp

Funksiyadan qayıtmaq üçün ret instruksiyasını icra edirik. ret instruksiyası stekin üst hissəsindəki məlumatı götürür və həmin ünvana keçid edir. ret instruksiyasından sonra stekin vəziyyəti aşağıdakı kimi olar:

 


 

Funksiyanın çağırılması.

Yuxarıda qeyd elədik ki, hər hansı funksiyanı çağırmaq üçün call instruksiyasından istifadə edirik.

Geri qayıtmaq üçün isə, ret instruksiyasından. Qeyd elədik ki, call instruksiya icra olunan zaman əvvəlcə növbəti icraolunacaq instruksiyanın ünvanını stekə yerləşdirir, ret isə, stekin üst hissəsində yerləşən məlumatı götürüb həmin ünvana keçid edir.

Biz ilk mövzularda icraolunan proqramın strukturu barədə söz açmışdıq. Qeyd elədik ki, proqram yaddaşa yüklənən zaman təxminən aşağıdakı struktura malik olur:

 


 

Proqramın icraolunan kod hissəsi .text seqmentində yerləşir. Əməliyyatlar sistemi .text seqmentini proqramın icraolunabilən faylından oxuyub, fiziki yaddaşa yükləyir və prosessorun %rip registrin proqramın başlanğıc ünvanına (_start nişanı) kökləyir. %rip registri prosessora növbəti icraolunacaq instruksiyanın ünvanını bildirir. Bu registrin qiymətini adi mov instruksiyası vasitəsilə dəyişə bilmərik. Hər instruksiya yaddaşda (.text seqmenti) 8 bayt yer tutur (X86_64).

Hər instruksiya icra olunduqca, %rip registrinin qiyməti avtomatik olaraq 8 bayt artır, yəni o, növbəti icraolunmalı instruksiyanın üzərinə sürüşür. %rip registrinin qiymətini başqa yolla call və jmp instruksiyaları dəyişə bilir. jmp instruksiyası %rip registrinin qiymətini birbaşa verilmiş yeni ünvana kökləyir və prosessor icraolunmanı həmin yeni ünvandan davam edir. jmp instruksiyası əlavə heç bir iş görmür. call instruksiyası isə jmp-dən fərqli olaraq, çağırılan zaman call-dan sonra gələn instruksiyanın yaddaşdakı ünvanını (%rip + 8) stekə yerləşdirir. Daha sonra, %rip registrinin qiymətini verilmiş ünvana kökləyir. Həmin ünvanda istənilən proqram kodu yerləşə bilər. O koddan geri qayıdan instruksiya call-dan sonrakı yerdən bərpa olunmalıdır. Həmin ünvan isə, call tərəfindən stekə yerləşdirilib. ret stekdən həmin ünvanı əldə edir və %rip registrinin qiymətini bu ünvana kökləyir.

Funksiyalardan istifadəyə aid digər proqram nümunələri.

Gəlin, funksiyalardan istifadə etməklə, aşağıdakı kimi bir proqram tərtib edək, hansı ki, istifadəçidən 2 ədəd qəbul edir, onların cəmini hesablayaraq, çap edir.


#prog16.s

.data
     setir1:
.ascii "Birinci ədədi daxil edin\n"

     say1:
.long 25
     
     setir2:
.ascii "İkinci ədədi daxil edin"

     say2:
.long 24
     
     setir3:
.ascii "Sizin daxil etdiyiniz ədədlərin cəmi = "

     say3:
.long 40

     yeni_str:
.ascii "\n"

     str:
.ascii "      "

     eded1:
.long 0

     eded2:
.long 0

     netice:
.long 12345

     onluq_simvollar:
.ascii "0123456789"

     bufer:
.rept 100
.ascii "\0"

.endr

.text

.globl _start
.type _start, @function

_start:

#birinci ədədin daxil edilməsini istə
pushq say1
pushq $setir1
call cap_et

#birinci ədədi sətir şəklində qəbul et
pushq $100
pushq $bufer
call daxil_et

#sətri ədədə çevir
pushq $bufer
pushq $eded1
call setri_edede_cevir

#ikinci ədədin daxil edilməsini istə
pushq say2
pushq $setir2
call cap_et

#ikinci ədədi sətir şəklində qəbul et
pushq $100
pushq $bufer
call daxil_et

#sətri ədədə çevir
pushq $bufer
pushq $eded2
call setri_edede_cevir

#eded1 ilə eded2-ni cəmlə
movl eded1, %eax
addl eded2, %eax

#cəmi nəticəyə yaz
movl %eax, netice

#nəticəni sətrə çevir
pushq netice
pushq $str
call ededi_setre_cevir

#nəticə barədə məlumatı çap et
pushq say3
pushq $setir3
call cap_et

#sətir formasında olan ədədi çap et
pushq $10
pushq $str
call cap_et

#yeni sətir simvolunu çap et
pushq $1
pushq $yeni_str
call cap_et

son:  movl $1, %eax
int $0x80


#==========================================#
#        FUNKSİYALAR        #
#==========================================#

  
#cap_et funksiyası
.type cap_et, @function

cap_et:
pushq %rbp
movq %rsp, %rbp

movl $4, %eax
movl $1, %ebx
movq 16(%rbp),%rcx
movq 24(%rbp),%rdx
int $0x80

popq %rbp
ret

#daxil_et funksiyası
.type cap_et, @function

daxil_et:
pushq %rbp
movq %rsp, %rbp

movl $3, %eax
movl $0, %ebx
movq 16(%rbp),%rcx
movq 24(%rbp),%rdx
int $0x80

popq %rbp
ret

#sətri ədədə çevir
.type setri_edede_cevir, @function

setri_edede_cevir:

pushq %rbp
movq %rsp, %rbp

#buferdəki sətri ədədə çevir
subq $8, %rsp

movl $0, %esi
movl $0, -8(%rbp)
movl $1, %edi

s_e_c_dovr:
movb $'\n', %bh
movl 24(%rbp), %ecx
addl %esi, %ecx
movb (%ecx), %ah
cmpb %bh, %ah
je  s_e_c_dovr_son

sub $48, %ah
movl -8(%rbp), %ebx
imull $10, %ebx
movl %ebx, -8(%rbp)
addb %ah, -8(%rbp)
incl %esi
jmp s_e_c_dovr

s_e_c_dovr_son:
movl 16(%rbp),%ecx
movl -8(%rbp),%ebx
movl %ebx, (%ecx)

addq $8, %rsp
popq %rbp
ret

#ədədi sətrə çevir
.type ededi_setre_cevir, @function

ededi_setre_cevir:

pushq %rbp
movq %rsp, %rbp

subq $8, %rsp
movl $0, -8(%rbp)
e_s_c_dovr:
movl $10, %edi
movl $0, %edx
movl 24(%rbp), %eax
div %edi

movl %eax, 24(%rbp)

xorq %rbx, %rbx
movb onluq_simvollar(%edx), %bh
pushq %rbx

incl -8(%rbp)

movl $0, %ebx
movl 24(%rbp), %eax
cmpl %eax, %ebx
je  e_s_c_dovr_son

jmp e_s_c_dovr

e_s_c_dovr_son:

movl -8(%rbp), %ecx
movl 16(%rbp), %eax
xorl %ebx, %ebx
stekden_setre:
cmpl %ebx, %ecx
je e_s_c_son

popq %rdx
movb %dh, (%eax)
incl %eax
incl %ebx
jmp stekden_setre

e_s_c_son:
addq $8, %rsp
popq %rbp
ret

Kompilyasiya və icra etsək:


[user@unix progs_as]$ 
[user@unix progs_as]$ as prog16.s -o prog16.o
[user@unix progs_as]$ ld prog16.o -o prog16
[user@unix progs_as]$ ./prog16
Birinci ədədi daxil edin
345
İkinci ədədi daxil edin
5678
Sizin daxil etdiyiniz ədədlərin cəmi = 6023
[user@unix progs_as]$


Bizi dəstəkləyənlər