2019独角兽企业重金招聘Python工程师标准>>>
一、指令指针
指令指针确定程序中的哪条指令是应该执行的下一条指令。它按照顺序的方式处理应用程序中
编写的指令码。
程序不能直接修改指令指针。程序员不具有使用MOV指令直接将EIP寄存器的值改为指向内存
中的不同位置的能力。但是,可以利用能够改动指令指针值的指令。这些指令称为分
(branch)。分支指令可以改动EIP寄存器的值,要么是无条件改动(无条件分支),要么是按
照条件值改动(条件分支)。
二、无条件分支
无条件分支有3种:
- 跳转
- 调用
- 中断
1、跳转
汇编语言的跳转语句和BASIC语言的GOTO语句是等同的。
跳转指令使用单一指令码:
jmp location
/*其中location是要跳转到的内存地址*/
单一汇编跳转指令被汇编位跳转操作码的3种不同类型之一:
- 短跳转
- 近跳转
- 远跳转
当跳转偏移量小于128字节时使用短跳转。在分段内存模式下,当跳转到另一个段中的指令
时使用远跳转。近跳转用于所有其他跳转。
2、调用
调用指令有两个部分。第一个部分是实际的CALL指令,它需要单一操作数----跳转到位置
的地址:
call address
/*address操作数引用程序中的标签,它被转换为函数中的第一条指令的内存地址*/
调用指令的第二部分是返回指令。它使函数可以返回代码的原始部分,就是紧跟在CALL
指令后面的位置。返回指令没有操作数,只有助记符RET。
下面是用于函数的模板形式:
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是程序要跳转到的位置(通常以标签表示)*/
对于计算无符号整数值,跳转指令使用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。这经常发生在溢出了保存数据的寄存器长度的数学操作的过程中,例如下面
的例子所示:
这个代码片段把带符号字节值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
和其它标志不同,也可以专门修改进位标志的指令。
四、循环
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)的独立单元确定是否
应该跳转到分支。
无条件分支
对于无条件分支,不难确定下一条指令,但根据跳转距离有多远,下一条指令在指令预取
缓存中有可能是不存在的。如果新的指令不存在预取缓存,那么必须清空整个缓存,然后从新
的位置重新加载指令。这对应用程序的性能而言代价很高的。
条件分支
分支预测算法使用3个主要规则:
- 假设会采用向后分支
- 假设不会采用向前分支
- 以前曾经采用过的分支会再次采用
2)优化技巧
消除分支:例如CMOV指令,采用向后分支
首先编写可预测分支的代码:
展开循环:虽然指令数量大增,但是处理器能够把所有这些指令都存放到指令预取缓存
中,并且顺畅迅速地执行它们。