本篇介绍
本篇介绍下汇编中的函数,栈帧内容。
函数
汇编也支持函数调用,如下是一个例子:
代码语言:javascript复制extern printf
section .data
radius dq 10.0
pi dq 3.14
fmt db "The area of the circle is %.2f, radius is %.2f",10,0
section .bss
section .text
global main
;----------------------------------------------
main:
push rbp
mov rbp, rsp
call surface ; call the function
mov rdi,fmt ; print format
movsd xmm1, [radius] ; move float to xmm1
mov rax,2 ; surface in xmm0
call printf
leave
ret
;----------------------------------------------
surface:
push rbp
mov rbp, rsp
movsd xmm0, [radius] ; move float to xmm0
mulsd xmm0, [radius] ; multiply xmm0 by float
mulsd xmm0, [pi] ; multiply xmm0 by float
leave
ret
输出:
The area of the circle is 314.00, radius is 10.00
使用call 标号就可以实现函数调用,对于浮点,返回值会通过xmm0传递。
这儿又出现了一个leave指令,leave就等同于 mov rsp,rbp,pop rbp。
函数也可以拥有自己的代码段和数据段,如下面的例子:
代码语言:javascript复制; function2.asm
extern printf
section .data
radius dq 10.0
section .bss
section .text
area:
section .data
.pi dq 3.141592654 ; local to area
section .text
push rbp
mov rbp, rsp
movsd xmm0, [radius]
mulsd xmm0, [radius]
mulsd xmm0, [.pi]
leave
ret
circum:
section .data
.pi dq 3.14 ; local to circum
section .text
push rbp
mov rbp, rsp
movsd xmm0, [radius]
addsd xmm0, [radius]
mulsd xmm0, [.pi]
leave
ret
circle:
section .data
.fmt_area db "The area is %f",10,0
.fmt_circum db "The circumference is %f",10,0
section .text
push rbp
mov rbp, rsp
call area
mov rdi,.fmt_area
mov rax,1 ; area in xmm0
call printf
call circum
mov rdi,.fmt_circum
mov rax,1 ; circumference in xmm0
call printf
leave
ret
global main
main:
mov rbp, rsp; for correct debugging
push rbp
mov rbp, rsp
call circle
leave
ret
输出:
The area is 314.159265
The circumference is 62.800000
这儿可以看到在函数内部也可以定义数据段,用来存放局部变量。
栈帧
对于intel处理器,在调用函数的时候需要保证rsp是16字节对齐的,这样设计是为了更好的支持SIMD。那体现到代码上是怎样呢?可以看下如下的代码:
代码语言:javascript复制extern printf
section .data
fmt db "2 times pi equals %.14f",10,0
pi dq 3.14159265358979
section .bss
section .text
func3:
push rbp
movsd xmm0, [pi]
addsd xmm0, [pi]
mov rdi,fmt
mov rax,1
call printf ; print a float
pop rbp
ret
func2:
push rbp
call func3 ; call the third function
pop rbp
ret
func1:
push rbp
call func2 ; call the second function
pop rbp
ret
global main
main:
push rbp
;push rbp ; for align 16 byte, uncomment it will cause crash
call func1 ; call the first function
;pop rbp
pop rbp
ret
结果:
2 times pi equals 6.28318530717958
如果把main中注释的代码去掉,那么就会crash,因为这时候调用func1时 rsp就不是16字节对齐的。这儿可以单步看看:
image.png
这时候rsp没有16字节对齐,执行push rbp后,就会对齐。 这就是prologue和epilogue的一个作用,保证调用函数时的rsp 16字节对齐。
本来在调用main函数之前rsp是16字节对齐的,可是在调用main时候,由于会将返回地址压栈,这时候rsp就不是16字节对齐了,就需要prologue中再次执行一个进栈操作,就可以保证是对齐的了。