汇编学习--控制执行流程

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

一、指令指针

    指令指针确定程序中的哪条指令是应该执行的下一条指令。它按照顺序的方式处理应用程序中

编写的指令码。

     程序不能直接修改指令指针。程序员不具有使用MOV指令直接将EIP寄存器的值改为指向内存

中的不同位置的能力。但是,可以利用能够改动指令指针值的指令。这些指令称为分

(branch)。分支指令可以改动EIP寄存器的值,要么是无条件改动(无条件分支),要么是按

照条件值改动(条件分支)。

                   汇编学习--控制执行流程_第1张图片

二、无条件分支

    无条件分支有3种:

  • 跳转
  • 调用
  • 中断

1、跳转

    汇编语言的跳转语句和BASIC语言的GOTO语句是等同的。

    跳转指令使用单一指令码:

jmp location

/*其中location是要跳转到的内存地址*/

汇编学习--控制执行流程_第2张图片

      单一汇编跳转指令被汇编位跳转操作码的3种不同类型之一:

  • 短跳转
  • 近跳转
  • 远跳转

       当跳转偏移量小于128字节时使用短跳转。在分段内存模式下,当跳转到另一个段中的指令

时使用远跳转。近跳转用于所有其他跳转。

2、调用

        调用指令有两个部分。第一个部分是实际的CALL指令,它需要单一操作数----跳转到位置

的地址:

call address

/*address操作数引用程序中的标签,它被转换为函数中的第一条指令的内存地址*/

         调用指令的第二部分是返回指令。它使函数可以返回代码的原始部分,就是紧跟在CALL

指令后面的位置。返回指令没有操作数,只有助记符RET。

                        汇编学习--控制执行流程_第3张图片

        下面是用于函数的模板形式:

      

function_label:
  pushl %ebp
  movl %ebp, %esp
  
  movl %ebp, %esp
  popl %ebp
  ret

/*保存了EBP寄存器后,就可以使用它作为堆栈的基指针,以便在函数中进行对堆栈的所有访问。在返回
  发出调用的程序之前,ESP寄存器必须被恢复为指向发出调用的内存位置*/

    下面是一个函数简单的调用例子:

.section .data
  output:
    .asciz "This is section %d\n "

.section .text

.globl _start

_start:
  pushl $1
  pushl $output
  call printf
  add $8, %esp  #should clear up stack
  call overhere
  pushl $3
  pushl $output
  call printf
  add $8, %esp  #should clear up stack
  pushl $0
  call exit
overhere:
  pushl %ebp
  movl %esp, %ebp
  pushl $2
  pushl $output
  call printf
  add $8, %esp  #should clear up stack
  movl %ebp, %esp
  popl %ebp
  ret

3、中断

      中断是处理器”中断“当前指令码路径并且切换到不同路径的方式。中断有两种形式:

  • 软件中断
  • 硬件中断

      硬件设备产生硬件中断。使用硬件中断发出信号,表示硬件层发生的事件(比如I/O端口接收

到输入信号时)。程序生成软件中断,它们是把控制交给另一个程序的信号。

       当一个程序被中断调用时,发出调用的程序暂停,被调用的程序接替它运行。指令指针被转

移到被调用的程序,并且从被调用的程序内继续执行。被调用的程序完成时,它可以把控制返

回给发出调用程序(使用中断返回指令)。

       软件中断是操作系统提供的,使应用程序可以使用操作系统内的函数,并且,在某些情况

下,甚至可以接触到底层的BIOS系统。

三、条件分支

        条件分支的结果取决于执行分支时EFLAGS寄存器的状态。

        与条件分支相关的5位:

  •    进位(Carry)标志(CF)-----第0位(借位有效位)
  •    溢出(Overflow)标志(OF)-----第11位
  •    奇偶检验(Parity)标志(PF)-----第2位
  •    符号(SIgn)标志(SF)-----第7位
  •    零(Zero)标志------第6位

        1、条件跳转指令

         条件跳转按照EFLAGS寄存器的当前值来确定是否进行跳转。几种不同的跳转指令使用

EFLAGS寄存器的不同位。条件跳转指令的格式如下:

jxx address

/*其中xx是1个到3个字符的条件代码,address是程序要跳转到的位置(通常以标签表示)*/

            汇编学习--控制执行流程_第4张图片

         对于计算无符号整数值,跳转指令使用above和below关键字。对于带符号整数值,使用

greadter和less。

         条件跳转允许两种跳转类型:

  • 短跳转
  • 近跳转

         短跳转使用8位带符号地址偏移量,而近跳转使用16位或者32位带符号地址偏移量。偏移

量值被加到指令指针上。

        2、比较指令

         比较两个值并且相应地设置EFLAGS寄存器。格式如下:

cmp operand1, operand2

/*CMP指令把第二个操作数和第一个操作数进行比较。在幕后,它对两个操作数执行减法操作
 (operand2 - operand1)。其中GNU汇编器中,operand1和operand2的顺序与Intel
  文档中的顺序是相反的*/

          下面是使用CMP指令的一些例子:

        3、使用标志位的范例

        1)使用零标志

           如果零标志为1(两个操作数相等),JE和JZ指令就跳转到分支。零标志可以由CMP指

令设置,也可以由计算结果为零的数学指令设置,如下:

movl $30, %eax
subl $30, %eax
jz overthere

/*JZ指令将被执行,因为SUB指令的结果为零*/

           也可以在递减寄存器的值时使用零标志,以便确定它是否到达零:

movl $10, %edi
loop1:
  < other code instructions >
  dec %edi
  jz out
  jmp loop1
out:

/*这个代码片段使用EDI寄存器作为变址计数器,它从10递减到1(到它到达零时,JZ指令将推出循环)*/

        2)使用溢出标志 

               溢出标志专门用在处理带符号数字时。当带符号值对于包含它的数据元素来说太大时,

溢出标志被设为1。这经常发生在溢出了保存数据的寄存器长度的数学操作的过程中,例如下面

的例子所示:

 汇编学习--控制执行流程_第5张图片        

          这个代码片段把带符号字节值127加上10。结果是137,这对于字节来说是合法值,但是

对带符号字节数是非法的(带符号字节数只能使用-127到127的值)。因为这个带符号值非法,

所以设置溢出标志为1,并执行JO指令。

        3)使用奇偶校验标志

        奇偶校验标识表明数学运算答案中应该为1的位的数目。可以使用它作为粗略的错误检查系

统,确保数学操作成功执行。

         如果结果中被设置为1的位的数目是偶数,则设置奇偶校验位(置1)。如果设置为1的位

的数目是奇数,则不设置奇偶校验位(置0)。

.section .text

.globl _start

_start:
  movl $1, %eax
  movl $4, %ebx
  subl $3, %ebx
  jp overthere
  int $0x80
overthere:
  movl $100, %ebx
  int $0x80

        4)使用符号标志

        符号标志使用在带符号数中,用于表示寄存器中包含的值的符号改变。在带符号数中,最

后一位(最高位)用作符号位。它表明数字表示是负值(设置为1)还是正值(设置为0)。

        使用符号标志,可以得到值变从0到-1的通知。

.section .data
  value:
    .int 21, 15, 34, 11, 6, 50, 32, 80, 10, 2

output:
  .asciz "The value is: %d\n"

.section .text

.globl _start

_start:
  movl $9, %edi
loop:
  pushl value( , %edi, 4 )
  pushl $output
  call printf
  add $8, %esp
  dec %edi
  jns loop
  movl $1, %eax
  movl $0, %ebx
  int $0x80

        5)使用进位标志    

        进位标志用在数学表达式中,表示无符号数中何时发生溢出(带符号数使用溢出标志)。

当指令导致寄存器超出其数据长度限制时设置进位标识。

         和溢出标志不同,DEC和INC指令不影响进位标志。   例如下面代码不会设置进位标志:

movl $0xffffffff, %ebx
inc %ebx
jc overflow

          但下面这个代码片段会设置进位标志,并且JC指令会跳转到overflow的位置:

movl $0xffffffff, %ebx
addl $1, %ebx
jc overflow

           当无符号值小于零时也会设置进位标志。例如,下面这个代码片段:

movl $2, %eax
subl $4, %eax
jc overflow

             和其它标志不同,也可以专门修改进位标志的指令。

汇编学习--控制执行流程_第6张图片

四、循环

        1)循环指令

         循环指令使用ECX寄存器作为计数器并且随着循环指令的执行自动递减它的值。

          LOOPE/LOOPZ和LOOPNE/LOOPNZ指令提供了监视零标志的附加功能。

          这些指令的格式是:

loop address

/*其中address是要跳转到的程序代码位置的标签名称。循环指令只支持8位偏移量,所以只能进行短跳转*/

          循环开始之前,必须在ECX寄存器中设置执行迭代的次数值。这通常使用下面这样的代码

完成:

  < code before the loop >
  movl $100, %ecx
loop1:
  < code to loop through >
  loop loop1
  < code after the loop >

        2)循环范例

        

.section .data
  output:
    .asciz "The value is: %d\n"

.section .text

.globl _start

_start:
  movl $100, %ecx
  movl $0, %eax
loop1:
  addl %ecx, %eax
  loop loop1
  pushl %eax
  pushl $output
  call printf
  add $8, %esp
  movl $1, %eax
  movl $0, %ebx
  int $0x80

   如果ECX为0,可用JCXZ指令执行条件分支。

_start:
  movl $0, %ecx
  movl $0, %eax
  jcxz done
loop1:
  addl %ecx, %eax
  loop loop1
done:
  pushl %eax
  pushl $output
  call printf
  add $8, %esp
  movl $1, %eax
  movl $0, %ebx
  int $0x80

五、模拟高级条件分支

        1)if语句

#include 

int main()
{
  int a = 100;
  int b = 25;
  if( a > b )
  {
    printf( "The higher value id %d\n", a );
  }
  else
  {
    printf( "The higher value id %d\n", b );
  }

  return 0;
}
	.file	"ifthen.c"
	.section	.rodata
.LC0:
	.string	"The higher value id %d\n"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushl	%ebp      #EBP寄存器压栈,以便可以使用它作为指向程序中的本地堆栈的指针
 	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp  #手动操作堆栈指针ESP,为把本地变量压入堆栈留下空间
	.cfi_def_cfa_register 5
	andl	$-16, %esp
	subl	$32, %esp
	movl	$100, 24(%esp)#手动把变量a的值传送到堆栈中的位置(EBP寄存器指向位置之后24字节)
	movl	$25, 28(%esp)#手动把变量b的值传送到堆栈中的位置(EBP寄存器指向位置之后28字节)
	movl	24(%esp), %eax #变量a的值被传送到EAX寄存器中
	cmpl	28(%esp), %eax #然后把这个值和变量b的值进比较
	jle	.L2 #查看a<=b,如果语句为真,就跳转到.L2标签
	movl	24(%esp), %eax #如果JLE指令的结果为假,则不执行跳转
	movl	%eax, 4(%esp) #把EAX中a的值存放在堆栈中
	movl	$.LC0, (%esp) #然后输出文本的位置(位于.LC0标签处)被存放到堆栈中
	call	printf #调用printf显示答案
	jmp	.L3
.L2:                              #if语句的”else“部分
	movl	28(%esp), %eax #获得b的值,存放到EAX寄存器中
	movl	%eax, 4(%esp) #把EAX中b的值存放在堆栈中
	movl	$.LC0, (%esp) #然后输出文本的位置(位于.LC0标签处)被存放到堆栈中
	call	printf #调用printf显示答案
.L3:
	movl	$0, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
	.section	.note.GNU-stack,"",@progbits

        2)for循环

#include 

int main()
{
  int i = 0;
  int j;
  for( i = 0; i < 1000; i++ )
  {
    j = i * 5;
    printf( "The answer is %d\n", j );
  }
   
  return 0;
}

   

	.file	"for.c"
	.section	.rodata
.LC0:
	.string	"The answer is %d\n"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	andl	$-16, %esp
	subl	$32, %esp
	movl	$0, 24(%esp)
	movl	$0, 24(%esp)
	jmp	.L2
.L3:
	movl	24(%esp), %edx #i的值被传送给EDX寄存器
	movl	%edx, %eax     #EDX中i的值传送给EAX
	sall	$2, %eax       #SALL指令执行两次EAX寄存器的左位移(相当于EAX寄存器中的数乘以4)
	addl	%edx, %eax     #ADDL把EDX寄存器值和EAX寄存器值相加(现在EAX寄存器包含的值是原始值的5倍)
	movl	%eax, 28(%esp) #把这个值乘5以后储存在堆栈中(堆栈开始处后28字节处)
	movl	28(%esp), %eax #把结果再传送到EAX中
	movl	%eax, 4(%esp)  #再把结果传送到堆栈中(堆栈开始处后4字节处)
	movl	$.LC0, (%esp)  #把输出文本的位置存放在堆栈开始处
	call	printf         #调用printf函数
	addl	$1, 24(%esp)   #把i的值加1
.L2:                           #for语句开始位置
	cmpl	$999, 24(%esp) #确定变量是否小于1000
	jle	.L3            #如果条件为真,则执行.L3标签
	movl	$0, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
	.section	.note.GNU-stack,"",@progbits

六、优化分支指令

        1)分支预测

         乱序引擎单元利用称为分支预测前端(branch prediction front end)的独立单元确定是否

应该跳转到分支。

         无条件分支

          对于无条件分支,不难确定下一条指令,但根据跳转距离有多远,下一条指令在指令预取

缓存中有可能是不存在的。如果新的指令不存在预取缓存,那么必须清空整个缓存,然后从新

的位置重新加载指令。这对应用程序的性能而言代价很高的。

      汇编学习--控制执行流程_第7张图片

         条件分支

         分支预测算法使用3个主要规则:

  • 假设会采用向后分支
  • 假设不会采用向前分支
  • 以前曾经采用过的分支会再次采用

        2)优化技巧

        消除分支:例如CMOV指令,采用向后分支

         首先编写可预测分支的代码:

                                  汇编学习--控制执行流程_第8张图片

            展开循环:虽然指令数量大增,但是处理器能够把所有这些指令都存放到指令预取缓存

中,并且顺畅迅速地执行它们。

转载于:https://my.oschina.net/u/2537915/blog/690939

你可能感兴趣的:(汇编学习--控制执行流程)