[0x09] nasm汇编 [循环:冒泡排序]

上节介绍了简单的加减乘除运算,在除法div运算时,被除数存放在寄存器rdx:rax中,执行除法后,商存放在rax中,余数存放在rdx中;乘法mul运算中,一个乘数存放在rax中,乘法运算的结果存放在rdx:rax中。

这节介绍循环,以一个冒泡排序的例子:b_sort.asm

; b_sort.asm
; bubble sort
; nasm -f elf64 -o b_sort.o b_sort.asm
; gcc -o b_sort b_sort.o
; ./b_sort
extern printf

section .data
	array: dq 5,4,3,2,1 
	fmt: db '%ld',0xa,0

section .text
global main
main:
	push rbp
	mov rsi, 0
	mov rdi, 0
L1:
	cmp rsi, 5
	jge L1_END
	mov rdi, rsi
	inc rdi
	L2:
		cmp rdi, 5
		jge L2_END
		mov rax, [array+rsi*8]
		mov rbx, [array+rdi*8]
		cmp rax, rbx
		jg exchange
		inc rdi
		jmp L2
	exchange:
		mov [array+rdi*8], rax
		mov [array+rsi*8], rbx
		inc rdi
		jmp L2
	L2_END:
		inc rsi
		jmp L1
L1_END:
	mov rbx, 0
L3:
	cmp rbx, 5
	jge L3_END
	mov rdi, fmt
	mov rsi, [array+rbx*8]
	mov rax, 0
	call printf
	inc rbx
	jmp L3
L3_END:
	pop rbp
	mov rax, 60
	syscall
	
	ret 

数据段.data中,定义了数组array,还有一个输出格式的字符串fmt。

push rbp
mov rsi, 0
mov rdi, 0

程序的开始,将rbp压入堆栈,这是为了不破坏rbp,然后将寄存器rsi和rdi设置为0,这是双重循环的索引,类似C语言中的i,j。如下面的C代码所示,rsi相当于外层循环的i,rdi相当于内层循环的j,这里将rdi设置为0没有必要,在内层循环开始之前会对rdi赋值。

int i,j;
for( i = 0; i < 5; i++ )
{
    for( j = i+1; j < 5; j++)
    {
        ...
    }
}

 

L1:
	cmp rsi, 5
	jge L1_END
	mov rdi, rsi
	inc rdi

L1表示外层循环的标签开始处,首先将rsi与5进行比较,当rsi>=5时,循环结束,跳出L1循环。然后将rdi设置为rsi+1。

 

L2:
		cmp rdi, 5
		jge L2_END
		mov rax, [array+rsi*8]
		mov rbx, [array+rdi*8]
		cmp rax, rbx
		jg exchange
		inc rdi
		jmp L2

L2是内层循环的标签开始处,同样将rdi与5比较,当rdi>=5时,循环结束,跳出L2循环。然后将数组array中的第rsi个数和第rdi个数赋值给rax和rbx,因为array中每个整数的字节大小为8,因此在取值的时候,将rsi和rdi乘以8.

之后,比较rax和rbx,当rax大于rbx时,跳转到exchange标签,来交换数组中的两个整数,否则,将rdi加1,跳转到L2继续循环执行内层循环。

exchange:
		mov [array+rdi*8], rax
		mov [array+rsi*8], rbx
		inc rdi
		jmp L2
	L2_END:
		inc rsi
		jmp L1
L1_END:

exchange标签执行交换操作,然后将rdi加1,跳转到L2执行;当L2内层循环结束后,将外层循环的索引rsi加1,跳转到L1执行。

mov rbx, 0
L3:
	cmp rbx, 5
	jge L3_END
	mov rdi, fmt
	mov rsi, [array+rbx*8]
	mov rax, 0
	call printf
	inc rbx
	jmp L3
L3_END:

然后是一个单层的循环操作来输出array数组。

 

但是,编译器是这样来将循环代码生成汇编代码的吗?我们编写一个C测试程序,从中看看编译器是如何处理循环的。

; ubuntu 64
; a.c
; gcc -o a.out a.c
#include <stdio.h>
int main()
{
    int i;
    for(i = 0; i < 3; i++)
        printf("%d ",i);
    return 0;
}

上面是一个简单的单重循环,使用objdump -d a.out命令来反汇编,部分反汇编结果如下:

0000000000400536 <main>:
  400536:	55                   	push   %rbp
  400537:	48 89 e5             	mov    %rsp,%rbp
  40053a:	48 83 ec 10          	sub    $0x10,%rsp         ; 为局部变量申请栈空间
  40053e:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)    ; i = 0
  400545:	eb 18                	jmp    40055f <main+0x29>
  400547:	8b 45 fc             	mov    -0x4(%rbp),%eax    ; eax = i
  40054a:	89 c6                	mov    %eax,%esi
  40054c:	bf f4 05 40 00       	mov    $0x4005f4,%edi
  400551:	b8 00 00 00 00       	mov    $0x0,%eax
  400556:	e8 b5 fe ff ff       	callq  400410 <printf@plt> ; printf
  40055b:	83 45 fc 01          	addl   $0x1,-0x4(%rbp)     ; i = i+1
  40055f:	83 7d fc 02          	cmpl   $0x2,-0x4(%rbp)     ; i <= 2?
  400563:	7e e2                	jle    400547 <main+0x11>
  400565:	b8 00 00 00 00       	mov    $0x0,%eax
  40056a:	c9                   	leaveq 
  40056b:	c3                   	retq   
  40056c:	0f 1f 40 00          	nopl   0x0(%rax)

我在上面的代码中加了注释,sub指令为局部变量申请栈空间,我们看到rbp-4处存放i,然后程序跳到了40055f,这里将i与2比较,如果小于等于2,则跳到400547处执行,这就是gcc编译器在对单重循环处理的结果。

    设置初值i=0
    jmp judge
content:
    ...
    循环内容
    ...
    i = i + 1
judge:
    i是否小于等于2
    如果是:jmp L2
   

循环的判断在循环内容的后面,如果需要继续进行循环,则往上跳转。

这是单重循环,如果是双重循环呢?会更复杂,我们来看一看。

; ubuntu 64
; b.c
; gcc -o b.out b.c
#include <stdio.h>
int main()
{
    int i,j;
    for(i = 0; i < 3; i++)
    {
        for( j = 0; j < 5; j++)
            printf("%d ", i+j);
    }
    return 0;
}

上面是个双重循环,同样使用objdump -d b.out来反汇编代码,部分反汇编代码如下:

0000000000400536 <main>:
  400536:	55                   	push   %rbp
  400537:	48 89 e5             	mov    %rsp,%rbp
  40053a:	48 83 ec 10          	sub    $0x10,%rsp
  40053e:	c7 45 f8 00 00 00 00 	movl   $0x0,-0x8(%rbp)      ; i = 0
  400545:	eb 30                	jmp    400577 <main+0x41>
  400547:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)      ; j = 0
  40054e:	eb 1d                	jmp    40056d <main+0x37>
  400550:	8b 55 f8             	mov    -0x8(%rbp),%edx
  400553:	8b 45 fc             	mov    -0x4(%rbp),%eax
  400556:	01 d0                	add    %edx,%eax
  400558:	89 c6                	mov    %eax,%esi
  40055a:	bf 14 06 40 00       	mov    $0x400614,%edi
  40055f:	b8 00 00 00 00       	mov    $0x0,%eax
  400564:	e8 a7 fe ff ff       	callq  400410 <printf@plt>  ; printf 
  400569:	83 45 fc 01          	addl   $0x1,-0x4(%rbp)      ; j = j+1
  40056d:	83 7d fc 04          	cmpl   $0x4,-0x4(%rbp)      ; j <= 4?
  400571:	7e dd                	jle    400550 <main+0x1a>   
  400573:	83 45 f8 01          	addl   $0x1,-0x8(%rbp)      ; i = i+1
  400577:	83 7d f8 02          	cmpl   $0x2,-0x8(%rbp)      ; i <= 2?
  40057b:	7e ca                	jle    400547 <main+0x11>
  40057d:	b8 00 00 00 00       	mov    $0x0,%eax
  400582:	c9                   	leaveq 
  400583:	c3                   	retq   
  400584:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
  40058b:	00 00 00 
  40058e:	66 90                	xchg   %ax,%ax

从上面的代码中,我们也可以看出,判断循环的边界代码位于循环内容的后面,首先是将j加1,判断内层循环是否结束,如果内层循环结束,然后判断外层循环。我们用下面的示意图来描述上面的代码:

    设置初始值i=0
    jmp judge2
L1:
    设置初始值j=0
    jmp judge1
content:
    ...
    循环内容
    ...
    j = j + 1
judge1:   
    j小于等于4?
    如果是,jmp content
    
    i = i + 1
judge2:
    i小于等于2?
    如果是,jmp L1

设置完i的初始值后,跳转到judge2处,判断外层循环是否结束,没结束,就跳转到内层循环,设置初始值j,跳转到内层循环的判断judge1,如果循环没结束,则跳转到循环内容。可以总结如下:

设置外层循环初始值

跳转到外层循环判断处

设置内层循环初始值

跳转到内层循环判断处

循环内容

内层循环索引+1

内层循环判断处

外层循环索引+1

外层循环判断处

 

这样做,结构更加清晰,因此,我们在编写循环的汇编代码时,可以参考使用这样的结构。

编译器为什么要这样组织循环结构呢?其实我们可以很容易猜得出,这样做,执行效率较高。

 

大家可以看看编译器如何处理三重循环。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值