问题描述:输入两个整数,使用函数求出两个整数的和,并打印。
这节介绍函数的使用,并对两个整数进行求和。这个过程中用到了C的库函数printf和scanf,因此需要链接C的动态链接库。
思路:使用scanf输入两个整数,然后压入栈中,调用add函数。
话不多说,代码如下add.asm:
; add.asm
; nasm -f elf64 -o add.o add.asm
; gcc -o add add.o
; ./add
extern printf,scanf
section .data
fmt_input: db '%ld',0
msg1: db 'input first number:',0
msg2: db 'input second number:',0
fmt_output: db 'sum = %ld',0xa,0
var1: dq 0
var2: dq 0
section .text
global main
main:
mov rdi, msg1
mov rax, 0
call printf
; get first number
mov rdi, fmt_input
mov rsi, var1
mov rax, 0
call scanf
mov rdi, msg2
mov rax, 0
call printf
; get second number
mov rdi, fmt_input
mov rsi, var2
mov rax, 0
call scanf
; push parameter
mov rdx, [var1]
push rdx
mov rdx, [var2]
push rdx
; call add function
call add
add rsp, 16
mov rdi, fmt_output
mov rsi, rax
mov rax, 0
call printf
mov rax, 60
syscall
ret
global add
add:
push rbp
mov rbp,rsp
mov rax, [rbp+16]
mov rbx, [rbp+24]
add rax, rbx
mov rsp,rbp
pop rbp
ret
在数据段中,定义了fmt_input,msg1,msg2,var1,var2,用于输入,打印提示信息和保存整数。
mov rdi, msg1
mov rax, 0
call printf
上面的这3行代码打印msg1,提示输入第一个整数。还记得使用寄存器传递参数的顺序吗?rdi,rsi,rdx,rcx,r8,r9.
需要注意一定要将rax设置为0,否则会出错。继续往下:
; get first number
mov rdi, fmt_input
mov rsi, var1
mov rax, 0
call scanf
这里调用scanf输入第一个整数var1,rdi保存输入格式的字符串首地址,rsi保存变量的首地址,依然要将rax设置为0,我们知道,调用函数的时候,返回值一般放在rax中,而对于printf,scanf这些函数,它们也是有返回值的。继续往下:
mov rdi, msg2
mov rax, 0
call printf
打印输入第二个数的提示信息。继续往下:
; get second number
mov rdi, fmt_input
mov rsi, var2
mov rax, 0
call scanf
获取第二个整数,现在已经输入了两个整数,需要调用函数进行求和操作。这里使用栈来传递参数,有两种方法可以供我们选择:1,传递整数值;2,传递整数的地址
对于这两种传递来说,使用的内存空间是相同的,因为整数值的长度是8个字节,整数的地址长度也是8个字节。不同的是在函数中使用参数的方式。传递整数的地址其实就相当于在C语言的函数中传递指针。我们选择传递整数值,这样更直观。继续往下:
; push parameter
mov rdx, [var1]
push rdx
mov rdx, [var2]
push rdx
前面我们讲过,使用中括号,里面的内容表示地址,这样用来取地址处的值。这4行代码,取出变量的值,然后压入堆栈。继续往下:
call add
add rsp, 16
调用add函数进行求和,然后将栈顶指针rsp加上16,目的是为了平衡栈。call这个指令有两步操作:将当前的ip或cs和ip压入栈中;跳转;用汇编指令来表示:push ip; jmp add; 当然这是一种用汇编指令来理解汇编指令的方法,不一定有这样的指令存在。
调用完成后,结果存放到rax中,将栈顶指针rsp加16,是因为栈中压入了两个长度为8个字节的整数,rsp加上16,这两个整数就不在栈里面了,当然了,add函数执行前后,栈也必须是没有变化的。这与调用方式有关,C的默认调用方式是_cdecl,这种调用方式从右往左压入参数,需要调用者自己平衡堆栈。
mov rdi, fmt_output
mov rsi, rax
mov rax, 0
call printf
mov rax, 60
syscall
ret
有了上面的介绍,这段代码就比较好理解了,打印rax的值,也就是两个整数的和。最后调用系统调用号为60(exit)的系统调用,退出程序。
前面的程序都是在输入、输出等,接下来是这个程序的关键部分,add函数:
global add
add:
push rbp
mov rbp,rsp
mov rax, [rbp+16]
mov rbx, [rbp+24]
add rax, rbx
mov rsp,rbp
pop rbp
ret
函数在nasm汇编中其实就是一个“标签”。没什么特别的。
函数的前两行:
push rbp
mov rbp,rsp
将rbp压入栈中,然后使rbp=rsp,这样做的目的是可以使用固定不变的ebp,来访问参数或局部变量等。因为rsp会随着局部变量的分配等操作发生变化,让rbp等于函数初始时栈顶指针rsp,可方便地对栈中的数据(地址高于rsp处的数据),以及局部变量(地址低于目前的rsp处的值),进行方便地访问。
当然,在函数退出的时候,要恢复rsp和rbp的值,这样,函数执行前后,栈是平衡的。有种“来也匆匆,去也匆匆”的感觉。
mov rsp,rbp
pop rbp
求和的代码:
mov rax, [rbp+16]
mov rbx, [rbp+24]
add rax, rbx
前两行代码就利用了固定的rbp,来访问函数参数,为什么是16和24呢?上面我们介绍了call的使用,将ip入栈。因此我们观察站的内容,刚开始把变量var1和var2的值压入栈中,如下所示,注意栈是向低地址增长的,因此[var1]在高地址,[var2]在邻接的低地址。
[var1]
[var2]
然后调用了call add,call指令把ip压入栈中,现在栈的内容如下:
[var1]
[var2]
ip ; 返回地址
在函数中,将rbp入栈,现在栈的内容如下:
[var1]
[var2]
ip ;返回地址
old rbp
现在栈顶的部分内容如上面的示意图。这时候,rsp指向栈顶old rbp地址,mov rbp, rsp使得rbp指向了old rbp。
我们知道,变量的值长度是8个字节,地址长度是8个字节,因此[rbp+16]就指向了[var2],而[rbp+24]指向了[var1]。
之后就是使用add指令求和,结果在rax中。
函数的返回指令是ret,ret操作相当于pop ip,在这之前执行了pop rbp,把old rbp给了rbp。因此现在栈顶是返回地址,ret指令将栈顶的返回地址给了ip,使得程序继续执行。
至此,求和函数程序分析完成。
相关文章: