zl程序教程

您现在的位置是:首页 >  工具

当前栏目

汇编学习(6), 外部函数,调用约定

学习 函数 调用 外部 汇编 约定
2023-06-13 09:15:46 时间

本篇介绍

本篇介绍下汇编中的外部函数和调用约定。

外部函数

在前面已经多次见过使用printf了,这次我们也可以自己写一些外部函数,下面是一个例子: 首先定义2个外部函数,分别是c_area和c_circum。

; circle.asm
extern pi
section .data                                       
section .bss                            
section .text

global c_area
c_area:
    section .text
        push rbp
        mov  rbp,rsp    
        movsd   xmm1, qword [pi]
        mulsd   xmm0,xmm0       ;radius in xmm0
        mulsd   xmm0, xmm1
        mov rsp,rbp
        pop rbp
        ret
global c_circum
c_circum:
    section .text
        push rbp
        mov  rbp,rsp    
        movsd   xmm1, qword [pi]
        addsd   xmm0,xmm0       ;radius in xmm0
        mulsd   xmm0, xmm1
        mov rsp,rbp
        pop rbp
        ret

这儿再定义2个函数,r_area 和r_circum。

; rect.asm
section .data                                       
section .bss                            
section .text

global r_area
r_area:
    section .text
        push rbp
        mov  rbp,rsp
        mov     rax, rsi    
        imul    rax, rdi
        mov rsp,rbp
        pop rbp
        ret                                 
global r_circum
r_circum:
    section .text
        push rbp
        mov  rbp,rsp
        mov     rax, rsi    
        add     rax, rdi
        add     rax, rax
        mov rsp,rbp
        pop rbp
        ret

接下来调用下上面定义的函数:

; function4.asm
extern printf
extern c_area
extern c_circum
extern r_area
extern r_circum
global pi 
section .data
    pi  dq  3.141592654                         
    radius  dq  10.0                    
    side1   dq  4
    side2   dq  5       
    fmtf    db  "%s %f",10,0
    fmti    db  "%s %d",10,0
    ca  db  "The circle area is ",0
    cc  db  "The circle circumference is ",0
    ra  db  "The rectangle area is ",0
    rc  db  "The rectangle circumference is ",0
section .bss                                                    
section .text                                           
    global main                     
main:
    enter 0,0

; circle area
    movsd xmm0, qword [radius]          ; radius xmm0 argument
    call c_area                 ; area returned in xmm0
    ; print the circle area
        mov rdi, fmtf
        mov rsi, ca
        mov rax, 1
        call printf
; circle circumference
    movsd xmm0, qword [radius]          ; radius xmm0 argument
    call c_circum                   ; circumference returned in xmm0
    ; print the circle circumference
        mov rdi, fmtf
        mov rsi, cc
        mov rax, 1
        call printf
; rectangle area
    mov rdi, [side1]            
    mov rsi, [side2]        
    call r_area                 ; area returned in rax
    ; print the rectangle area
        mov rdi, fmti
        mov rsi, ra
        mov rdx, rax
        mov rax, 0
        call printf
; rectangle circumference
    mov rdi, [side1]            
    mov rsi, [side2]
    call r_circum                   ; circumference returned in rax
    ; print the rectangle circumference
        mov rdi, fmti
        mov rsi, rc
        mov rdx, rax
        mov rax, 0
        call printf
leave
ret

结果:
The circle area is  314.159265
The circle circumference is  62.831853
The rectangle area is  20
The rectangle circumference is  18

这儿的关键信息如下:

  1. 涉及浮点运算的函数,参数是通过xmm0 系列寄存器传递的,返回值是通过xmm0传递的
  2. 涉及整数运算的函数,参数是通过rdi,rsi,rdx等寄存器传递的,返回值是通过rax传递的
  3. 需要使用外部函数,需要使用关键字external, 定义外部函数,需要使用关键字global,变量也一样。

调用约定

调用约定(Calling Convertions)就是调用函数时传参和返回值的约定。不同的平台约定也不一样,比如linux和windows 就都有自己的一套调用约定。 对于非浮点场景,传参规则如下:

The 1st argument goes into rdi.
The 2nd argument goes into rsi.
The 3rd argument goes into rdx.
The 4th argument goes into rcx.
The 5th argument goes into r8.
The 6th argument goes into r9.

如果参数超过6个,比如10个,那么规则继续如下:
The 10th argument is pushed first.
Then the 9th argument is pushed.
Then the 8th argument is pushed.
The 7th argument is pushed.

当调用函数的时候,返回地址rip也会压栈,prologue中保存rbp也会压栈一次,这样如果需要通过rsp拿到第7个参数,就需要是rsp + 16。 浮点传参规则如下:

The 1st argument goes into xmm0.
The 2nd argument goes into xmm1.
The 3rd argument goes into xmm2.
The 4th argument goes into xmm3.
The 5th argument goes into xmm4.
The 6th argument goes into xmm5.
The 7th argument goes into xmm6.
The 8th argument goes into xmm7.

参数超过8个就需要通过栈了,不过不像整数压栈那样,这块等到了SIMD那块继续介绍。

接下来看一个传参的例子:

extern printf
section .data               
    first   db  "A",0                   
    second  db  "B",0
    third   db  "C",0
    fourth  db  "D",0           
    fifth   db  "E",0
    sixth   db  "F",0
    seventh db  "G",0
    eighth  db      "H",0
    ninth   db      "I",0
    tenth   db      "J",0
    fmt1    db  "The string is: %s%s%s%s%s%s%s%s%s%s",10,0 
    fmt2    db  "PI = %f",10,0
    pi   dq      3.14

section .bss                                                    
section .text                                   
    global main                     
main:
    mov rbp, rsp; for correct debugging
    push rbp
    mov rbp,rsp
    mov rdi,fmt1    
    mov rsi, first      ; the correct registers
    mov rdx, second
    mov rcx, third          
    mov r8, fourth
    mov r9, fifth
        
    push 0 ;   16 byte align the stack,保证调用printf时候rsp是16字节对齐
    push tenth      ; now start pushing in
    push ninth      ; reverse order
    push eighth
    push seventh
    push sixth
    mov rax, 0
    ;and rsp , 0xfffffffffffffff0 ; 16 byte align the stack    
    call printf
    ;and rsp , 0xfffffffffffffff0 ; 16 byte align the stack

    movsd xmm0,[pi] ; print a float
    mov rax, 1
    mov rdi, fmt2
    call printf
leave
ret

结果:
The string is: ABCDEFGHIJ
PI = 3.140000

这儿就同时用到了寄存器和栈传参。 再看一个栈传参的例子,看看callee是如何获取栈上参数的:

; function5.asm
extern printf
section .data               
    first   db  "A"                 
    second  db  "B"
    third   db  "C"
    fourth  db  "D"         
    fifth   db  "E"
        sixth   db  "F"
        seventh db  "G"
        eighth  db      "H"
        ninth   db      "I"
        tenth   db      "J"
    fmt db  "The string is: %s",10,0 
section .bss
    flist resb  11          ;length of string plus end 0
section .text                                   
    global main                     
main:
    push rbp
    mov rbp, rsp
    mov rdi, flist      ; length            
    mov rsi, first      ; the correct registers
    mov rdx, second
    mov rcx, third          
    mov r8, fourth
        mov r9, fifth
        push 0 ; 让rsp 16字节对齐
        push tenth      ; now start pushing in
        push ninth      ; reverse order
        push eighth
        push seventh
    push sixth
    call lfunc      ;call the function
        ; print the result
        mov rdi, fmt
                mov rsi, flist
        mov rax, 0
        call printf
leave
ret 
;---------------------------------------------------------------------------                                            
lfunc:  
    push rbp
    mov rbp,rsp
        xor rax,rax             ;clear rax (especially higher bits)
        mov al,byte[rsi]               ; move content argument to al
    mov [rdi], al             ; store al to memory 
        mov al, byte[rdx]          
    mov [rdi+1], al           
        mov al, byte[rcx]
    mov [rdi+2], al
        mov al, byte[r8]
    mov [rdi+3], al
        mov al, byte[r9]
    mov [rdi+4], al
        xor rbx,rbx
        mov rax, qword [rbp+16] ;initial stack + rip + rbp
        mov bl,[rax]
    mov [rdi+5], bl
        mov rax, qword [rbp+24]
        mov bl,[rax]
    mov [rdi+6], bl
        mov rax, qword [rbp+32]
        mov bl,[rax]
    mov [rdi+7], bl
        mov rax, qword [rbp+40]
        mov bl,[rax]
    mov [rdi+8], bl
        mov rax, qword [rbp+48]
        mov bl,[rax]
    mov [rdi+9], bl
        mov bl,0
    mov [rdi+10], bl

mov rsp,rbp
pop rbp
ret                                 

结果:
The string is: ABCDEFGHIJ

可以看到lfunc解析栈上的参数,就是通过rbp 加偏移得到的。 在调用函数时,对于寄存器的保存也有一套约定,有的寄存器值需要caller保存,有的需要callee保存,具体如下:

image.png

image.png

关键信息如下:

  1. 对于callee save 的寄存器,caller会认为寄存器值不会变化,因此callee需要使用这些寄存器,就需要通过push/pop保存并恢复他们的值
  2. 对于caller save的寄存器,callee直接使用就行
  3. 浮点寄存器xmm0全部都是caller save的寄存器