函數的使用

和高級語言一樣,彙編語言在多個項目之間可能會編寫相同的過程和處理,如果使用函數的話就可以不必每次需要時都重複編寫實用程序代碼,從而在需要它的時候調用它。

函數包含完成特定功能所需的代碼,數據從主程序傳遞給函數,然後結果返回給主程序。調用函數時,程序執行路徑被改變,切換到函數代碼的第一條指令。處理器從這個位置開始執行指令,直到函數表明它可以把控制返回到主程序中的原始位置。

在彙編語言中,創建函數需要3個步驟:

  • 1.定義輸入值

函數都需要某種形式的輸入數據,程序員必須定義程序如何把這些信息傳遞給函數,基本有三種技術:使用寄存器、使用全局變量、使用堆棧。類似於C語言的使用全局變量和形式參數。

  • 2.定義函數處理 為了在GNU彙編器中定義函數,必須在程序中把函數名稱聲明為標籤。為彙編器聲明函數名稱,使用.type命令:
.type func, @function
func:

.type 命令通知GNU彙編器,func標籤定義在彙編語言程序中使用的函數的開始。函數的結束由ret指令定義,執行到ret指令時,程序控制返回主程序。

  • 3.定義函數輸出值

函數完成對數據的處理時,很可能希望把結果傳遞迴發出調用的程序的區域。能夠把結果傳遞迴主程序,以便主程序能夠利用此數據做進一步的處理或者顯示。完成傳送結果方式有以下常見兩種: 把結果存放在寄存器中、把結果存在在全局變量內存位置中。

函數的訪問使用call指令,call指令使用單一操作數:

call function

在執行call指令之前,要把所有的輸入值放在正確的位置中。 在彙編語言中,函數可以放在主程序的後面。和C語言不同,函數不必在主程序中調用函數之前定義。

程序調用函數時,程序員應該知道函數使用那些寄存器進行它到的內部處理。當執行返回主程序時,函數中使用用的任何寄存器或內存有可能不是原來的值了。如果掉調用的函數修改主程序使用的寄存器,那麼在調用函數之前保存寄存器的當前狀態,並且在函數返回之後恢復寄存器的狀態。在調用之前,可以使用push指令單獨保存特定寄存器,也可使用pusha指令保存所有寄存器。類似地,可以使用pop指令單獨恢復寄存器的原始I狀態,也可使用popa指令同時恢復所有寄存器的狀態

#func.s
.section .data
base:
    .int 100
.section .bss
    .lcomm result, 4

.section .text
.globl _start
_start:
    nop
    movl $8, %eax
    call cal_func

    movl $1, %eax
    movl result, %ebx
    int $0x80

cal_func:
    addl $10, %eax
    addl base, %eax
    movl %eax, result
    ret

make之後輸出結果:

$ ./func
$ echo $?
118

實際上使用全局變量傳遞參數和返回結果並不是常見的程序設計方法,即使在C語言中也不常這麼做。在C語言中一般使用向函數傳入參數的方式向函數傳入值,參數其實也就是使用的堆棧,程序中任何函數都可以訪問堆棧,所以彙編語言在堆棧之中傳遞函數參數就可以不用擔心使用寄存器和全局變量會造成的混亂的情況。

在調用函數之前,程序把函數所需的輸入參數存放到堆棧的頂部,執行call指令時,把發出調用的程序返回地址也存放到堆棧頂部,堆棧指針(esp)就指向堆棧的頂部。函數就可以根據esp寄存器使用間接尋址的方式訪問輸入參數,並且不必彈出堆棧,以防止返回地址的丟失。一般的做法是進入函數時,把esp寄存器複製到ebp寄存器,這樣就可以確保有一個寄存器永遠保存指向調用函數時堆棧頂部的正確指針,在複製esp寄存器的值之前,ebp寄存器的值也被放到堆棧中。現在ebp寄存器包含堆棧開始位置,主程序的第一個輸入參數位於間接尋址位置8(%ebp),第二個參數位於12(%ebp)等等

在前面講call指令時曾講過彙編語言函數模板,如下:

func_lable:
    pushl %ebp            #函數開頭把ebp的原始值保存到堆棧的頂部
    movl %esp, %ebp    #把當前esp堆棧指針複製到ebp寄存器
    ...
    movl %ebp, %esp    #獲取存儲在ebp寄存器中原始的esp寄存器值。
    popl %ebp
    ret

可以使用enter和leave指令建立函數開頭和結尾,可以使用它們替代手工創建開頭和結尾。 當在函數代碼中處理運行時,很可能需要在某個位置存儲數據元素,就是類似於C語言的局部變量,這個時候我們也可以使用堆棧。ebp寄存器被設置為執行堆棧指針的頂部之後,這個時候函數的局部變量可以放在堆棧中這個指針之後,此時也可以使用而不ebp寄存器引用它們,比如對於一個4字節的變量,可以通過-4(%ebp)訪問第一個局部變量,用-8(%ebp)訪問第二個局部變量。

如下示例是通過向函數傳入兩個參數並計算其和,然後再將其和作為程序的返回碼退出。

.section .data
base:
    .int 100
plus_no:
    .int 8

.section .text
.globl _start
_start:
    nop
    pushl base            #將函數所需參數壓入堆棧頂部
    pushl plus_no
    call cal_func

    movl $1, %eax
    int $0x80

cal_func:
    pushl %ebp
    movl %esp, %ebp

    movl $0, %eax
    addl 8(%ebp), %eax     # 第一個輸入參數位於間接尋址位置8(%ebp)
    addl 12(%ebp), %eax    # 第二個參數位於12(%ebp)
    movl %eax, %ebx        # 將和值放在%ebx寄存器中,最後作為程序返回值返回

    movl %ebp, %esp
    popl %ebp
    ret

make之後編譯輸出如下:

$ ./func
$ echo $?
108

彙編語言也像C語言一樣,函數可以定義在一個單獨的文件中,最後在把它們和主程序文件連接在一起。單獨函數文件和通常創建的主程序文件類似,唯一區別在於不使用_start段,必須把函數名稱聲明為全局標籤,以便其他程序能夠訪問它。如下:

.section .text
.type addfunc, @function
.globle addfunc
addfunc:

type命令聲明addfunc標籤指向 函數的開始。我們將上一個示例中兩個數想加的函數卸載單獨的文件中,修改後如下:

#main.s
.section .data
base:
    .int 100
plus_no:
    .int 8

.section .bss
    .lcomm result, 4

.section .text
.globl _start
_start:
    nop
    pushl base
    pushl plus_no
    call addfunc

    movl $1, %eax
    int $0x80


#func.s
.section .text
.type addfunc, @function
.globl addfunc
addfunc:
    pushl %ebp
    movl %esp, %ebp

    movl $0, %eax
    addl 8(%ebp), %eax
    addl 12(%ebp), %eax
    movl %eax, %ebx

    movl %ebp, %esp
    popl %ebp
    ret

make及輸出結果如下:

$ make
as   -o add.o add.s
as   -o main.o main.s
ld -o func add.o main.o
$ ./func
$ echo $?
108
$