【版权声明:尊重原创,转载请保留出处:blog.csdn.net/shallnet,文章仅供学习交流,请勿用于商业用途】
在此之前我们使用的汇编代码示例都是从第一条指令开始,直到最后最后一条指令程序退出。但实际上和高级语言类似,汇编代码也提供指令来改变程序处理数据方式。
正常情况下,程序要执行要执行的下一条指令是在指令指针寄存器中,指令指针确定程序中哪条指令是应该执行的下一条指令。 当指令指针在程序指令中移动时,EIP寄存器会递增。指令长度可能是多个字节,所以指向下一条指令不仅仅是每次是指令指针递增一。指令指针寄存器(EIP)跟踪要执行程序的下一条指令代码,应用程序不能修改指令指针本身,不能使用指定的内存地址放在EIP中,相反必须使用能够改变指令指针的指令来改变预存取缓存的下一条指令,这些指令称为分支指令。分支指令可以改变EIP寄存器的值,要么是无条件改变,要么是按照条件改变。
当程序遇到跳转、调用、中断时,指令指针自动跳转到另一个位置。
- 跳转指令
跳转指令使用单一指令码:
jmp location
其中location是要跳转到的内存地址。在汇编语言中这个位置是程序代码中的标签, 类似于C语言中的goto语句。遇到该指令时,指令指针改变为该标签后面的指令码的内存地址。下面示例演示跳转指令操作:
# jmp.s .section .text .globl _start _start: nop movl $1, %eax jmp gotohere movl $10, %ebx int $0x80 gotohere: movl $20, %ebx int $0x80
编译执行,查看程序返回结果:
$ make as -o jmp.o jmp.s --gstabs ld -o jmp jmp.o $ ./jmp $ echo $? 20
程序简单调用系统调用exit,通过查看程序执行返回码就可以确定跳转发生了。我们也可以在调试程序中单步运行查看运行的每行代码来确定跳转的发生。如下:
(gdb) b *_start Breakpoint 1 at 0x8048054: file jmp.s, line 5. (gdb) r Starting program: /home/allen/as/4_jmp/jmp Breakpoint 1, _start () at jmp.s:5 5 nop (gdb) s 6 movl $1, %eax (gdb) s 7 jmp gotohere (gdb) s 11 movl $20, %ebx (gdb) s 12 int $0x80 (gdb) s Program exited with code 024. (gdb)
重新编译程序,去掉调试信息,使用objdump程序反汇编可执行程序,可以了解指令码在内存中是如何安排的:
$ as -o jmp.o jmp.s $ ld -o jmp jmp.o $ objdump -D jmp jmp: file format elf32-i386 Disassembly of section .text: 08048054 <_start>: 8048054: 90 nop 8048055: b8 01 00 00 00 mov $0x1,%eax 804805a: eb 07 jmp 8048063 <gotohere> 804805c: bb 0a 00 00 00 mov $0xa,%ebx 8048061: cd 80 int $0x80 08048063 <gotohere>: 8048063: bb 14 00 00 00 mov $0x14,%ebx 8048068: cd 80 int $0x80
现在可以通过程序make编译程序,在调试程序中对照上面反汇编中指令码内存位置查看eip寄存器的值。
Breakpoint 1, _start () at jmp.s:5 5 nop (gdb) n 6 movl $1, %eax (gdb) n 7 jmp gotohere (gdb) print $eip $1 = (void (*)()) 0x804805a <_start+6> (gdb) n 11 movl $20, %ebx (gdb) print $eip $2 = (void (*)()) 0x8048063 <gotohere> (gdb)
可以看到,输出eip地址0x8048063就是gotohere标签指向的内存位置。
- 调用指令
调用指令类似跳转指令,但是它保存发生跳转的位置,在必要时可以返回这个位置。在汇编语言中,实现函数就使用调用指令。类似C语言,汇编语言函数也是分割功能模块,避免多次编写相同代码。调用指令用法:
call addr
addr为操作数引用程序中的标签,其被转换为函数中第一条指令的内存地址。函数返回代码原始部分使用助记符ret。执行call指令时,指令把EIP寄存器的值放到堆栈中,然后修改EIP寄存器以指向被调用的函数地址。当被调用的函数完成后,它从堆栈获得过去的EIP寄存器值,并且把控制权返回给原始程序,因为在函数中可能对堆栈进行操作,所以EBP经常用作堆栈的基指针,因此在函数的开始通常也把ESP寄存器复制到EBP寄存器中。我们可以因此给出一个汇编语言函数的模板:
func_lable: push1 %ebp movl %esp, %ebp <function code here> movl %ebp, %esp popl %ebp ret
在保存EBP寄存器之后就可以使用它作为堆栈的基指针,在函数中进行对堆栈的所有访问。在函数返回之前,ESP寄存器必须被恢复为指向发出调用的内存位置。
下面演示一个简单的call示例:
#call.s .section .data msg: .asciz "this is as call test!\n" len=.-msg .section .text .globl _start _start: nop call output_func movl $0, %ebx movl $1, %eax int $0x80 output_func: pushl %ebp movl %esp, %ebp #<function code here> movl $len, %edx movl $msg, %ecx movl $1, %ebx movl $4, %eax int $0x80 movl %ebp, %esp popl %ebp ret
程序调用函数output_func输出一串字符串。make并执行结果如下:
$ make as -o call.o call.s --gstabs ld -o call call.o $ ./call this is as call test! $
- 中断
中断也可以更改当前指令指针。中断分软中断和硬中断,当一个程序被中断时,指针指针被转移到被调用的程序,并且从被调用的程序内继续执行,被调用的程序完成时,它可以把控制返回给发出调用的程序。在之前几节给出的示例中,已经使用过中断。简单的使用0x80值的INT指令把控制转移给linux系统调用程序,在中断发生时,按照eax寄存器的值执行子函数,有关中断详细信息在后面会讲到。
条件跳转
条件跳转按照EFLAGS中的值来判断是否该跳转。每个条件跳转指令都检查特定的标志位以便确定是否符合跳转的条件。EFLAGS寄存器中有很多位,和条件跳转有关的有5位:0位(进位标志CF)、11位(溢出标志OF)、2位(奇偶校验标志PF)、7位(符号标志SF)、第6位(零标志ZF)。结合这几个不同的标志位可以执行多种跳转组合。条件跳转指令格式如下:
jxx addr
其中xx是1个到3个字符的条件代码,addr是程序要跳转到的位置。下面是所有可用的条件跳转指令。
a 大于时跳转
ae 大于等于
b 小于
be 小于等于
c 进位
cxz 如果CX寄存器为0
ecxz 如果ECS寄存器为0
e 相等
na 不大于
nae 不大于或者等于
nb 不小于
nbe 不小于或等于
nc 无进位
ne 不等于
g 大于(有符号)
ge 大于等于(有符号)
l 小于(有符号)
le 小于等于(有符号)
ng 不大于(有符号)
nge 不大于等于(有符号)
nl 不小于
nle 不小于等于
no 不溢出
np 不奇偶校验
ns 无符号
nz 非零
o 溢出
p 奇偶校验
pe 如果偶校验
po 如果奇校验
s 如果带符号
z 如果为零
EFLAGS寄存器可以通过比较指令比较两个值来设置,比较指令CMP格式如下:
cmp operand1, operand2
指令将第二个操作数和第一个操作数进行比较,它对两个操作数执行减法操作(operand2-operand1),然后设置EFALGS寄存器。如下示例:
#cmp.s .section .text .globl _start _start: nop movl $11, %eax movl $24, %ebx cmp %eax, %ebx jae greater movl $1, %eax int $0x80 greater: movl $11, %ebx movl $1, %eax int $0x80
make,运行结果如下:
$ make as -o cmp.o cmp.s --gstabs ld -o cmp cmp.o $ ./cmp $ echo $? 11
通过查看程序返回输出结果说明发生了条件跳转,ebx寄存器的值大于大小寄存器中值,所以代码跳转到greater标签处执行,也可以在调试器中单步运行查看代码执行顺序。