堆栈的作用
汇编语言中的堆栈就是高级语言中的栈。
堆栈主在汇编程序设计中主要有三个作用:
- 过程调用&返回指令
- 参数传递
- 局部变量
过程调用&返回指令
过程调用中的过程指什么?
汇编语言中的过程就是高级语言里面说的子程序,调用子程序(过程、函数)的本质就是控制转移,它与无条件转移的区别是调用子程序需要考虑返回。
过程调用指令用于由主程序转移到子程序;
过程返回指令用于由子程序返回到主程序。
指令 | 中文名 | 格式 | 解释 | 备注 |
---|---|---|---|---|
CALL | 过程调用指令 | CALL LABEL | 段内直接调用LABEL | 与jmp的区别在于call指令会在调用label之前保存返回地址(call 中return之后主程序还可以继续执行,jmp 当label执行完毕后不能返回主程序继续执行) |
RET | 段内过程返回指令 | RET | 使子程序结束,继续执行主程序 |
call指令的背后
段内直接调用的背后操作其实是两步:
(1)把返回地址(EIP内容)压入堆栈
(2)使得EIP内容为目标地址偏移,从而实现转移
返回地址:紧随过程调用指令的下一条指令的地址(有效地址)
目标地址:子程序开始处的地址(有效地址)
与无条件转移相比,过程调用指令call只是多了第一步(保护现场)。
ret指令的背后
过程返回指令的执行其实进行的是如下操作:
从堆栈中弹出地址偏移,送到指令指针寄存器EIP中,这个返回地址通常就是在执行相应的调用指令时所压入堆栈的返回地址。
参数传递
入口参数:主程序传给子程序的参数
出口参数:子程序传给主程序的参数
参数传递的方法主要有:寄存器传递法、堆栈传递法、约定内存单元传递法、call后续区传递法等。具体情况需要事先约定好。
一般c语言的习惯是使用堆栈传递入口参数,使用寄存器传递出口参数,因为一般入口参数比较多,出口参数比较少。
局部变量
这里就一个结论,堆栈可以用于安排动态局部变量。
算术逻辑运算指令
乘除指令
无符号数乘法指令(MUL)
指令格式:
MUL OPRD
该指令实现两个无符号数的乘法运算,乘数是OPRD,被乘数位于AL、AX或EAX中(由OPRD的尺寸决定)。
需要注意的是乘积之后尺寸翻倍,两个8位的数乘积为16位,结果存放在AX中,类似的,两个16位数的乘积结果为32位,放在DX:AX中,最后,64位的乘积放在EDX:EAX中。
操作数OPRD可以是通用寄存器、存储单元,但是不能是立即数。
有符号数乘法指令(IMUL)
有符号乘法指令有三种使用形式:
IMUL OPRD;
IMUL DEST,SRC;
IMUL DEST,SRC1,SEC2;
具体解释如下:
-
IMUL OPRD
单操作数乘法指令和无符号数乘法的规则差不多,只是在乘的时候把乘数和被乘数都当成有符号数。
-
IMUL DEST,SRC
数据流方向是DEST<=DEST*SRC
要求目的操作数DEST只能是16位或32位通用寄存器,源操作数SRC可以是通用寄存器或存储单元,需与目的操作数尺寸一致,可以是一个立即数(尺寸不能超过目的操作数)。
乘数和被乘数均作为有符号数。
-
IMUL DEST,SRC1,SEC2
数据流方向为DEST<=SRC1*SRC2
目的操作数DEST只能是16位或32位通用寄存器。
SRC1可以是通用寄存器或者存储单元,须与目的操作数尺寸一致,但不能是立即数。
SRC2只能是一个立即数,尺寸不能超过目的操作数。
被乘数和乘数均为有符号数。
无符号数除法指令(DIV)
一般格式:
DIV OPRD
OPRD是除数,被除数位于AX、DX:AX或EDX:EAX中,由OPRD的尺寸决定,被除数的尺寸翻倍,商在AL、AX或者EAX中,余数在AH、DX或者EDX中,商和余数的尺寸和OPRD相同。
操作数OPRD可以是通用寄存器,可以是存储单元,但不能是立即数。
注意使用DIV指令时要防止除溢出,比如:
比如上图,除完之后应该商300余0,可是300超出了AL的表示范围,这时候就产生了溢出情况,在实际使用中要注意防范类似情况。
有符号数除法指令(IDIV)
指令格式:
IDIV OPRD
基本原则和DIV一样,不同之处在于:
- 除法时有符号的
- 如果不能整除,余数的符号与被除数符号一致,而且余数的绝对值小于除数的绝对值。
总结
指令 | 中文名 |
---|---|
MUL | 无符号数乘法指令 |
IMUL | 有符号数乘法指令 |
IMUL DEST,SRC | 有符号数乘法指令 |
IMUL DEST,SRC1,SRC2 | 有符号数乘法指令 |
DIV | 无符号数除法指令 |
IDIV OPRD | 有符号数除法指令 |
符号拓展指令
符号拓展指令的实质是用被拓展寄存器的符号位占据目标拓展寄存器。
指令 | 中文名 | 格式 | 解释 |
---|---|---|---|
CBW | 字节转化为字指令 | CBW | 把寄存器AL中的符号拓展到寄存器AH; 如果AL最高有效位、为0,则AH=0,如果AL最高位为1,则AH=FFH |
CWD | 字转化为双字指令 | CWD | 把寄存器AX中的符号拓展到寄存器DX; AX最高位0和1不同情况的拓展策略同CBW |
CDQ | 双字转化为四字指令 | CDQ | 把寄存器EAX中的符号拓展到EDX; AX最高位0和1不同情况的拓展策略同CBW |
CWDE | 字转化为双字指令 | CWDE | 把AX中的符号拓展到EAX的高16位; AX最高位0和1不同情况的拓展策略同CBW |
使用举例:
符号拓展传送指令(MOVSX)
一般格式:
MOVSX DEST,SRC
把SRC符号拓展后送到DEST。
目的操作数的尺寸必须大于源操作数的尺寸。源操作数的尺寸可以是8位或16位,目的操作数的尺寸可以是8位或16位。
使用举例:
零拓展传送指令(MOVZX)
一般格式:
MOVZX DEST,SRC
把SRC零拓展后送到DEST。
源操作数可以是8位或16位,目的操作数可以是16位或32位。
使用举例:
逻辑运算指令
需要注意:
只有通用寄存器或者存储单元可作为目的操作数,用于存放运算结果。
指令 | 中文名 | 格式 | 解释 | 备注 |
---|---|---|---|---|
NOT | 否运算指令 | NOT OPRD | 把操作数OPRD按位取反,然后送回OPRD | |
AND | 与运算指令 | AND DEST,SRC | 把两个操作数进行与运算之后结果送回DEST | 同1得1,否则得0 |
OR | 或运算指令 | OR DEST,SRC | 把两个操作数进行或运算之后结果送回DEST | 同0得0,否则得1 |
XOR | 异或运算 | XOR DEST,SRC | 把两个操作数进行异或运算之后结果送回DEST | 相同得0不同得1 |
TEST | 测试指令 | TEST DEST,SRC | 与AND指令类似,将各位相与,但是结果不送回DEST,仅影响状态位标志,指令执行后,ZF、PF、SF反映运算结果,CF和OF被清零 | 通常用于检测某些位是否为1,但又不希望改变操作数的值 |
test使用举例:
判断AL中的位6和位2是否有一位为1:
test al,01000100B;
随后,判断标志位ZF,如果ZF为0,说明al第6位和第2位都为0,否则说明二者有一个为1.
移位指令
一般移位指令
指令 | 中文名 | 格式 | 解释 | 备注 |
---|---|---|---|---|
SAL | 算术左移 | SAL OPRD,count | 把操作数oprd左移count位,右边补0 | 与shl指令一样 通过截取count的低5位,实际的移位数被限于0到31之间。 |
SHL | 逻辑左移 | SHL OPRD,count | 把操作数oprd左移count位,右边补0 | 与sal指令一样 通过截取count的低5位,实际的移位数被限于0到31之间。 |
SAR | 算术右移 | SAR OPRD,count | 把操作数oprd右移count位,同时每右移一位,左边补符号位,移出的最低位进入标志位CF | 通过截取count的低5位,实际的移位数被限于0到31之间。 |
SHR | 逻辑右移 | SHR OPRD,count | 把操作数oprd右移count位,左边补0,移出的最低位进入标志位CF | 通过截取count的低5位,实际的移位数被限于0到31之间。 |
循环移位指令
指令 | 中文名 | 格式 | 解释 | 备注 |
---|---|---|---|---|
ROL | 左循环移位指令 | ROL OPRD,count | 左循环移一位之后最高位移到最低位的同时也进入CF | 通过截取count的低5位,实际的移位数被限于0到31之间。 |
ROR | 右循环移位指令 | ROR OPRD,count | 右循环移一位之后最低位移到最高位的同时也进入CF | 通过截取count的低5位,实际的移位数被限于0到31之间。 |
RCL | 带进位左循环移位 | RCL OPRD,count | 相当于CF在最高位直接参与循环移位 | 大循环左移 通过截取count的低5位,实际的移位数被限于0到31之间。 |
RCR | 带进位右循环移位 | RCR OPRD,count | 相当于CF在最高位直接参与循环移位 | 大循环右移 通过截取count的低5位,实际的移位数被限于0到31之间。 |
使用实例:
实现把al的最低位送到bl的最低位,仍保持al不变。
ror bl,1;//bl循环右移一位
ror al,1;//al循环右移一位,最低位进入cf
rcl bl,1;//bl带进位左移,带进了来自al的最低位(cf)
rol al,1;//恢复al
双精度移位指令
双精度移位指令是为了方便地把一个操作数的部分内容通过移位复制到另一个操作数。
格式:
- 双精度左移:SHLD OPRD1,OPRD2,count
- 双精度右移:SHRD OPRD1,OPRD2,count
解释:
-
SHLD OPRD1,OPRD2,count
将OPRD1左移指定的count位,在低端空出的位用操作数OPRD2高端的count位填补,但是OPRD2内容保持不变,操作数OPRD1中最后移出的位保留在进位标志CF中。
-
SHRD OPRD1,OPRD2,count
将OPRD1右移指定的count位,空出的位用OPRD2低端的count位填补,但是OPRD2内容保持不变,操作数OPRD1中最后移出的位保留在进位标志CF中。
分支程序设计
无条件和条件转移指令
段内转移和段间转移
- 段内转移(近转移):仅仅重新设置指令指针寄存器EIP的转移,由于没有调整CS,所以转移后继续执行的指令仍在同一代码段中。
- 段间转移(远转移):不仅重新设置EIP,而且重新设置代码段寄存器CS的转移,由于重置了CS,转移后继续执行的指令在另一代码段中。
对于段内转移和段间转移需要注意:
- 条件转移指令和循环指令只能实现段内转移;
- 无条件转移指令和过程调用指令以及返回指令,既可以是段内转移,也可以是段间转移;
- 软中断指令和中断返回指令一定是段间转移;
直接转移和间接转移
- 直接转移:转移指令中直接给出转移目标地址的转移;
- 间接转移:转移指令中给出包含转移目标地址的寄存器或者存储单元的转移;
需要注意无条件转移指令和过程调用指令集可以是直接转移也可以是间接转移。
无条件转移指令
无条件转移指令分为4种:
- 段内直接转移
- 段内间接转移
- 段间直接转移
- 段间间接转移
需要说明的是无条件转移指令均不影响标志寄存器的状态标志。
无条件段内直接转移
JMP LABEL;
标号LABEL表示要转移的目标位置(转移目的地)。
无条件段内直接转移的机器码构成如下:
操作码OP 地址差rel
地址差rel实际上是LABEL所指定的指令的地址偏移与紧跟JMP指令的下一条指令的地址偏移之间的差值,rel可正可负,这样才可以实现前后的跳转。地址差rel可以用一个字节表示,也可用4个字节或2字节表示,如果只用一个字节表示,就称之为短(short)转移,否则称为近(near)转移。一般如果当汇编器汇编到某条转移指令时可以计算出地址差rel,汇编器会自动判断出应该用1字节表示rel还是4字节或2字节,否则汇编器会使用较多的位数来表示地址差。所以,当程序员在写程序时能顾及出用8位就可以表示出地址差,那么可以在标号前加一个汇编器操作符**“SHORT”**来指定用一个字节表示地址差,表示转移的目的地就在附近。
无条件段内间接转移
JMP OPRD
OPRD是32位通用寄存器或者双字存储单元,比如:
JMP ECX;
JMP DWORD PTR [EBX];
无条件段间转移指令JMP
段间转移指令和段内转移指令差不多,只是涉及到改变代码段寄存器CS的内容,情况较为复杂,在之后的文章中介绍。
条件转移指令
条件转移指令在前一篇文章已经介绍,这里不再赘述,只是需要特别明确一下rel偏移量的概念。
多路分支的实现
多路分支实际上指的就是switch-case的汇编实现,这里的实现原理主要是通过无条件间接转移指令和目标地址表来实现多路分支,举个例子:
考虑一下多路分支程序:
int cf319(int x, int operation)
{
int y;
//多路分支
switch ( operation ) {
case 1:
y = 3*x;
break;
case 2:
y = 5*x+6;
break;
case 4:
case 5:
y = x*x ;
break;
case 8:
y = x*x+4*x;
break;
default:
y = x ;
}
if ( y > 1000 )
y = 1000;
return y;
}
上面的程序为了更加具有一般性,刻意没有安排连续的case值,其汇编的实现如下:
push ebp
mov ebp, esp
; switch ( operation ) {
mov eax, DWORD PTR [ebp+12] ;取得参数operation(case值)
dec eax ;从0开始计算,所以先减去1
cmp eax, 7 ;从0开始计算,最多就是7
ja SHORT LN2cf319 ;超过,则转default(LN2cf319对应defalut处理语句)
;
jmp DWORD PTR LN12cf319[ eax*4 ] ;这句是关键,这里实现了多路分支
;
LN12cf319: ;多向分支目标地址表
DD LN6cf319 ; case 1
DD LN5cf319 ; case 2
DD LN2cf319 ; default
DD LN4cf319 ; case 4
DD LN4cf319 ; case 5
DD LN2cf319 ; default
DD LN2cf319 ; default
DD LN3cf319 ; case 8
看上面的汇编实现,其核心思想是巧妙地运用了无条件段内间接转移和目标地址表,因为目标地址表每项占4个字节,所以跳转的地址是目标地址表的起始地址加case对象(这里是operation)乘以4,如果operation为0,跳到目标地址表的首地址,即就是LN6c,如果operation为1,跳到LN5c,以此类推就实现了多路分支的巧妙跳转。
使用该方法的一般建议是:当多路分支数超过5时,考虑无条件间接转移方式和目标地址表结合实现多路分支会更高效。
循环程序设计
循环指令
循环指令类似于条件转移指令,其采用的是段内相对转移的方式,是通过在指令指针寄存器EIP上加一个地址差的方式实现的转移,需要注意的是循环指令中的这个地址差只用了一个字节(8位)来表示,所以转移范围仅在-128-127之间。在保护方式(32位代码段)下,ECX作为循环计数器,实方式下,以CX为循环计数器,循环指令不影响各标志位。
指令 | 中文名 | 格式 | 解释 | 备注 |
---|---|---|---|---|
LOOP | 计数循环指令 | LOOP LABEL | 使ECX的值减1,当ECX的值不为0的时候跳转至LABEL,否则执行LOOP之后的语句 | |
LOOPE | 等于循环指令 | LOOPE LABEL | 使ECX的值减1,如果结果不等于0并且零标志ZF等于1(表示相等),那么就转移到LABEL,否则执行LOOPE之后的语句 | ECX的减1并不影响标志位,ZF是否为1取决于循环指令之前指令对其的影响。 |
LOOPZ | 零循环指令 | LOOPZ LABEL | 使ECX的值减1,如果结果不等于0并且零标志ZF等于1(表示相等),那么就转移到LABEL,否则执行LOOPZ之后的语句 | ECX的减1并不影响标志位,ZF是否为1取决于循环指令之前指令对其的影响。 |
LOOPNE | 不等于循环指令 | LOOPE LABEL | 使ECX的值减1,如果结果不等于0并且零标志ZF等于0(表示不相等),那么就转移到LABEL,否则执行LOOPNE之后的语句 | ECX的减1并不影响标志位,ZF是否为1取决于循环指令之前指令对其的影响。 |
LOOPNZ | 非零循环指令 | LOOPNZ LABEL | 使ECX的值减1,如果结果不等于0并且零标志ZF等于0(表示不相等),那么9就转移到LABEL,否则执行LOOPNZ之后的语句 | ECX的减1并不影响标志位,ZF是否为1取决于循环指令之前指令对其的影响。 |
JECXZ | 计数转移指令 | JECXZ LABEL | 当寄存器ECX的值为0时转移到LABEL,否则顺序执行 | 注意与LOOP的关系是JECXZ是直接判断ECX,没有先减ECX 通常在循环开始之前使用该指令,所以循环次数为0时,就可以跳过循环体 |
子程序设计
调用约定
-
-_cdecl被称为 C 调用约定。缺省调用约定。参数按照从右至左的顺序入堆栈,函数本身不清理堆栈。
-
_stdcall被称为 pascal 调用约定。参数按照从右至左的顺序入堆栈,函数自身清理堆栈。
-
_fastcall 是快速调用约定。通过 寄存器传递参数。前两个参数由 ECX 和 EDX 传送,其他参数按照从右至左的顺序入堆栈,函数自身清理堆栈。