[0x07] nasm汇编 [求和]

问题描述:输入两个整数,使用函数求出两个整数的和,并打印。

这节介绍函数的使用,并对两个整数进行求和。这个过程中用到了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,使得程序继续执行。

 

至此,求和函数程序分析完成。

相关文章:

[0x06] nasm汇编 [调用C库函数]

[0x05] nasm汇编 [调试]

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值