学习汇编的一些记录
一、一些常用汇编指令
汇编指令(书上比较重要的)
jmp word ptr 内存单元地址(段内转移)
功能:从内存单元地址处开始存放一个字,是转会目的地的偏移地址。(实际上要去IP)
jmp dword ptr
功能:内存单元地址处开始存放着两个字,高地址处的字是转移地址的目的段地址,低地址处是转移的目的偏移地址
ret
pop IP
retf
pop IP
pop CS
call
push IP//将call指令下一句的IP入栈,在call内部会有个ret将IP又出栈,从而返回到call指令下一句。
jmp near ptr 标号
更多细节,和例子还是在书上看。
汇编指令(书上没有的)
var_4 = dword ptr -4
var_4 = dword ptr -4; 这是解释代码,可解释成var_4 是 esp - 4处的空间;
var_0 = dword ptr 8; var_0 是 esp +8处的空间;
在IDA中VAR 是代表的函数中的局部变量.
你要知道在汇编中访问局部变量,是通过ebp-x的来定位栈中位置.
var a = dwod -10h
var b = dwod - 8h
var c = dwod - 4h
_main proc nea
push ebp
mov ebp,esp
sub esp,10h
mov [ebp-4],0 //同过这种方式访问局部变量,
等价于mov [ebp+c],0
那么我把var c 换个有意思的名字如 var Max = dword - 4h
mov [ebp+Max],0 //这个时候,就更清楚了。
lea
LEA 取有效地址指令 (Load Effective Address )
指令格式:LEA 目的,源
指令功能:取源操作数地址的偏移量,并把它传送到目的操作数所在的单元。
LEA 指令要求原操作数必须是 存储单元 ,而且目的操作数必须是一个除段寄存器之外的16位或32位寄存器。当目的操作数是16位通用寄存器时,那么只装入有效地址的低16位。使用时要注意它与MOV指令的区别,MOV指令传送的一般是源操作数中的内容而不是地址。
例1 假设:SI=1000H , DS=5000H, (51000H)=1234H
执行指令 LEA BX , [SI]后,BX=1000H
执行指令 MOV BX , [SI]后,BX=1234H
有时,LEA指令也可用取偏移地址的MOV指令替代。
例2 下面两条指令就是等价的,他们都取TABLE的偏移地址,然后送到BX中,即
LEA BX,TABLE
MOV BX,OFFSET TABLE
MOVZX指令
汇编语言数据传送指令MOV的变体。无符号扩展,并传送。
movzx是将源操作数的内容拷贝到目的操作数,并将该值0扩展至16位或者32位。但是它只适用于无符号整数。 他大致下面的三种格式。
movzx 32位通用寄存器, 8位通用寄存器/内存单元
movzx 32位通用寄存器, 16位通用寄存器/内存单元
movzx 16位通用寄存器, 8位通用寄存器/内存单元
举个例子。例如
令eax=00304000h
若执行 movzx eax, ax后 eax = 00004000h 。
若执行 movzx eax, ah后 eax = 00000040h。
又如:
MOV BL,80H
MOVZX AX,BL
运行完以上汇编语句之后,AX的值为0080H。由于BL为80H,最高位也即符号位为1,但在进行无符号扩展时,其扩展的高8位均为0,故赋值AX为0080H。
总结:
movzx其实就是将我们的源操作数取出来,然后置于目的操作数,目的操作数其余位用0填充。
text指令
test 对两个参数(目标,源)执行AND逻辑操作,并根据结果设置标志寄存器,结果本身不会保存。
test eax ,eax 不改变值的大小,只改变标志位ZF。运算结果为0,则zf=1 ,否则zf=0
所有的对操作数进行算术和逻辑运算的指令,都会根据运算结果修改ZF标志。
jnz指令
JNZ : jump if not zero 结果不为零则转移
cmd
cmp(compare)指令进行比较两个操作数的大小
例:cmp oprd1,oprd2
为第一个操作减去第二个操作数,但不影响第两个操作数的值,它影响flag的CF,ZF,OF,AF,PF.
CDQ
把EAX的第31位(这个位数是从右往左数的)复制到EDX的每个位去,常和除法连用。
如果EAX = 0x0000000A
那么执行CDQ后 EDX = 0x00000000,这样EDX:EAX就是0x000000000x0000000A的64bit的了,用来作为被除数(如果是除数是32位,就需要64位来作为被除数)
如果EAX = 0xFFFFFFFA
那么执行CDQ后 EDX = 0xFFFFFFFF
sar
sar是算术右移指令,功能是将操作数右移,符号位保持不变。
例如,sar eax,1//假设eax是0x0000000A,执行完之后eax就会变成0x00000005,相对于除以2。
imul和mul
imul 有符号乘法,将被乘数与乘数均作为有符号数。
mul 无符号乘法,将被乘数及乘数均作为无符号数。
可以有三个操作数:imul eax,eax,0Ch
第3操作数是乘数,
第2操作数是被乘数,
运算结果放入第1操作数。
也可以是两个操作数:imul eax,0Ch
目标操作数(第一个操作数)乘以源操作数(第二个操作数)
idiv(带符号数除法)/div(无符号除法)
写法:idiv reg;/div reg
作用:如果操作数是8位,结果商在AL、余数在AH中;
如果操作数是16位,结果商在AX,余数在DX中;
如果操作数是32位,结果商在EAX,余数在EDX中;
注意:不能直接实现8位数除8位数、16位数除16位数、32除32,若需要这样,则必须先把除数符号扩展或零扩展到16、32、64位,然后用除法指令。
跳转
JE ;等于则跳转
JNE ;不等于则跳转
JZ ;为 0 则跳转
JNZ ;不为 0 则跳转
JS ;为负则跳转
JNS ;不为负则跳转
JC ;进位则跳转
JNC ;不进位则跳转
JO ;溢出则跳转
JNO ;不溢出则跳转
JA ;无符号大于则跳转
JNA ;无符号不大于则跳转
JAE ;无符号大于等于则跳转
JNAE ;无符号不大于等于则跳转
JG ;有符号大于则跳转
JNG ;有符号不大于则跳转
JGE ;有符号大于等于则跳转
JNGE ;有符号不大于等于则跳转
JB ;无符号小于则跳转
JNB ;无符号不小于则跳转
JBE ;无符号小于等于则跳转
JNBE ;无符号不小于等于则跳转
JL ;有符号小于则跳转
JNL ;有符号不小于则跳转
JLE ;有符号小于等于则跳转
JNLE ;有符号不小于等于则跳转
JP ;奇偶位置位则跳转
JNP ;奇偶位清除则跳转
JPE ;奇偶位相等则跳转
JPO ;奇偶位不等则跳转
retn
pop ip
pop n//sp+n
前言:主要还是给自己看的。
二、堆栈平衡
堆栈平衡的原理
https://www.cnblogs.com/cat47/p/12285209.html这个上面讲的还可以。下面总结总结。
堆栈平衡主要分为两中情况
情况1:
就是call调用函数的时候,函数内部不能改变栈顶sp,如果有改变(例如函数内部有push或者pop指令,但是一般函数调用时,函数内部都会把esp给ebp,最后又把ebp给esp,所以一般不会变),也必须要在ret执行之前改为原来的栈顶sp,否则ret,pop的IP,可能将不是call的下一句的IP。
情况2:
用堆栈来传递参数,先改变原来的堆栈,在函数内使用了这个堆栈来传递参数,回到主函数后,要恢复原来的堆栈。
具体恢复方法有两种,一种是直接在主函数中用add或者sub修改sp的值。
另一种是,在函数内部就直接将sp修改。
其中的retn 8的意思就是
pop ip
pop 8
retn在最下面也有简单介绍
实验目的
自己动手了解堆栈平衡的原理,并观察调试过程中堆栈的变化情况。
实验样本1-调用一个空函数
由vc++6.0编译的 堆栈平衡.exe文件,代码如下
#include<stdio.h>
void kong()
{
}
int main(void)
{
int a = 1, b = 2;
kong();
printf("%d\n", a+b);
getchar();//主要是为了找到main函数。
}
调试过程
找到main函数
先F9到达EIP处,然后F8一直调,会在main函数处停下来。进入main函数。
注意这时候ESP,EBP的值,和在堆栈中的情况。
ESP:0019FF34
EBP:0019FF70
执行
push ebp
将ebp的值入栈,ESP-4:0019FF30,EBP不变,堆栈变化如下,注意灰色那一行
执行
mov ebp esp
将esp的值给ebp,ebp的作用可以理解为esp的替身(ebp的值只要在pop ebp之前都不会发生改变,不会受esp期间被改变的影响),用来访问栈内局部变量,参数,函数返回地址。这时候
ESP:0019FF30
EBP:0019FF30
执行灰色部分代码
不用了解具体过程,大概就是开辟一个4c空间的堆栈,执行后
ESP:0019FED8//灰色部分有3个push,esp-4c-12
EBP:0019FF30
堆栈情况为
继续执行下面两句指令。
将1存入ebp-4的地址,将2存入ebp-8的地址。堆栈情况如下
由于接下来调用的一个函数是空函数所以,不需要用push指令来开辟一段栈空间,用来传递参数。如果函数用来传递了参数,最后需要将开辟的堆栈还原,意思就是是esp要回到原来的值。
执行下一句语句
call 堆栈平衡1.401005
//相对于 pop eip//eip是call指令的下一句的地址。esp的值会变成esp-4:0019FED4
jmz 00401005
进入call的函数,一个空函数。
第一句和上面的一样,直接看堆栈变化
EBP:0019FF30
ESP:0019FED0
0019FED0处存着ebp的值。
这些指令配合使用
push ebp
mov ebp,esp
...//中间的指令大概就是将esp-40,又push了一些东西,又pop了一些东西
mov esp,ebp
pop ebp
直接看mov esp,ebp执行前的堆栈情况
ESP:0019FE90
EBP:0019FED0
执行后
ESP:0019FED0
EBP:0019FED0
这时候ESP重新回到了原来进入这个函数的值,再
pop ebp
EBP变为0019FF30,ESP变为0019FED4,存着返回主函数的地址。
执行ret指令(相对于pop ip)回到主函数。ESP+4
ESP:0019FF30
EBP:0019FED0
由于是个空函数,没有传递参数,前面也没有开辟新的堆栈,所以不需要将esp进行加减,但如果之前调用了参数,就必须要在函数内将用rent指令将esp改了,或者在函数外用,add或者sub来改esp的值,来维持堆栈平衡。
继续下两条指令
mov eax,dword ptr ss:[ebp-4]
add eax,dword ptr ss:[ebp-8]
将
中ebp-4的1先给eax寄存器,再将ebp-8的2和eax的1用add指令加起来,将最后的结果传个eax寄存器,得到结果3。
最后的输出函数
知道EAX:累加(Accumulator)寄存器,常用于函数返回值,这几条语句是用来输出最后结果的,就可以了。
实验样本-裸函数1
void __declspec(naked)plus()
{
__asm
{
ret
}
}
int main(int argc,char*argv[])
{
plus();
return 0;
}
整个主函数
调用的裸函数
实验样本-裸函数2
#include<stdio.h>
int __declspec(naked)plus(int a,int b)
{
__asm
{
mov eax,dword ptr ds:[esp+8]
add eax,dword ptr ds:[esp+4]
ret
}
}
int main(int argc,char*argv[])
{
int a;
a=plus(1,2);
printf("%d",a);
return 0;
}
三、不同C语言语句在汇编中的显示情况及简单分析
1.if-else
源代码如下
#include<stdio.h>
int main(void)
{
int a=1,b=2,c;
if(a<b)
{
c=0;
}
else
{
c=1;
}
return 0;
}
对应汇编代码如下
1: #include<stdio.h>
2:
3: int main(void)
4: {
00401010 push ebp
00401011 mov ebp,esp
00401013 sub esp,4Ch
00401016 push ebx
00401017 push esi
00401018 push edi
00401019 lea edi,[ebp-4Ch]
0040101C mov ecx,13h
00401021 mov eax,0CCCCCCCCh
00401026 rep stos dword ptr [edi]
5: int a=1,b=2,c;
00401028 mov dword ptr [ebp-4],1
0040102F mov dword ptr [ebp-8],2
6: if(a<b)
00401036 mov eax,dword ptr [ebp-4]
00401039 cmp eax,dword ptr [ebp-8]//cmp是比较指令,相对于sub指令,并改变一些标志寄存器的值。
0040103C jge main+37h (00401047)//前面的数大于或等于后面的数就跳转。实际上与SF,OF两个寄存器有关。
7: {
8: c=0;
0040103E mov dword ptr [ebp-0Ch],0
9: }
10: else
00401045 jmp main+3Eh (0040104e)
11: {
12: c=1;
00401047 mov dword ptr [ebp-0Ch],1
13: }
14: return 0;
0040104E xor eax,eax
15: }
2.for循环
简单的累加代码
#include<stdio.h>
int main(void)
{
int i,sum;
sum=0;
for(i=0;i<5;i++)
{
sum=i+sum;
}
printf("%d",sum);
}
对应汇编代码
1: #include<stdio.h>
2:
3: int main(void)
4: {
00401010 push ebp
00401011 mov ebp,esp
00401013 sub esp,48h
00401016 push ebx
00401017 push esi
00401018 push edi
00401019 lea edi,[ebp-48h]
0040101C mov ecx,12h
00401021 mov eax,0CCCCCCCCh
00401026 rep stos dword ptr [edi]
5: int i,sum;
6: sum=0;
00401028 mov dword ptr [ebp-8],0
7:
8: for(i=0;i<5;i++)
0040102F mov dword ptr [ebp-4],0
00401036 jmp main+31h (00401041)
00401038 mov eax,dword ptr [ebp-4]
0040103B add eax,1
0040103E mov dword ptr [ebp-4],eax
00401041 cmp dword ptr [ebp-4],5
00401045 jge main+42h (00401052)
9: {
10: sum=i+sum;
00401047 mov ecx,dword ptr [ebp-4]
0040104A add ecx,dword ptr [ebp-8]
0040104D mov dword ptr [ebp-8],ecx
11: }
00401050 jmp main+28h (00401038)
//8到11就是for循环
12:
13: printf("%d",sum);
00401052 mov edx,dword ptr [ebp-8]
00401055 push edx
00401056 push offset string "%d" (0042201c)
0040105B call printf (00401090)
00401060 add esp,8
14: }
3.while循环
#include<stdio.h>
int main(void)
{
int i=0;
int sum=0;
while(i<5)
{
sum=sum+i;
i++;
}
printf("%d",sum);
}
while循环对应的汇编代码
1: #include<stdio.h>
2:
3: int main(void)
4: {
00401010 push ebp
00401011 mov ebp,esp
00401013 sub esp,48h
00401016 push ebx
00401017 push esi
00401018 push edi
00401019 lea edi,[ebp-48h]
0040101C mov ecx,12h
00401021 mov eax,0CCCCCCCCh
00401026 rep stos dword ptr [edi]
5: int i=0;
00401028 mov dword ptr [ebp-4],0
6: int sum=0;
0040102F mov dword ptr [ebp-8],0
7: while(i<5)
00401036 cmp dword ptr [ebp-4],5
0040103A jge main+40h (00401050)
8: {
9: sum=sum+i;
0040103C mov eax,dword ptr [ebp-8]
0040103F add eax,dword ptr [ebp-4]
00401042 mov dword ptr [ebp-8],eax
10: i++;
00401045 mov ecx,dword ptr [ebp-4]
00401048 add ecx,1
0040104B mov dword ptr [ebp-4],ecx
11: }
0040104E jmp main+26h (00401036)
12: printf("%d",sum);
00401050 mov edx,dword ptr [ebp-8]
00401053 push edx
00401054 push offset string "%d" (0042201c)
00401059 call printf (00401080)
0040105E add esp,8
13: }
4.do_while的汇编语句
#include<stdio.h>
int main(void)
{
int i=0;
int sum=0;
do
{
sum=sum+i;
i++;
}
while(i<5);
printf("%d",sum);
}
do_while的汇编代码
1: #include<stdio.h>
2:
3: int main(void)
4: {
00401010 push ebp
00401011 mov ebp,esp
00401013 sub esp,48h
00401016 push ebx
00401017 push esi
00401018 push edi
00401019 lea edi,[ebp-48h]
0040101C mov ecx,12h
00401021 mov eax,0CCCCCCCCh
00401026 rep stos dword ptr [edi]
5: int i=0;
00401028 mov dword ptr [ebp-4],0
6: int sum=0;
0040102F mov dword ptr [ebp-8],0
7:
8: do
9: {
10: sum=sum+i;
00401036 mov eax,dword ptr [ebp-8]
00401039 add eax,dword ptr [ebp-4]
0040103C mov dword ptr [ebp-8],eax
11: i++;
0040103F mov ecx,dword ptr [ebp-4]
00401042 add ecx,1
00401045 mov dword ptr [ebp-4],ecx
12: }
13: while(i<5);
00401048 cmp dword ptr [ebp-4],5
0040104C jl main+26h (00401036)
14: printf("%d",sum);
0040104E mov edx,dword ptr [ebp-8]
00401051 push edx
00401052 push offset string "%d" (0042201c)
00401057 call printf (00401080)
0040105C add esp,8
15: }
5.数组的在堆栈中的情况
#include<stdio.h>
int main(void)
{
int b;
int a[]={1,2,3,4,5};
scanf("%d",b);
}
堆栈中的样子
刚开始将数组中的数放到堆栈里面
放完之后
需要注意的点,数组中的数是在堆栈中是从上到下开始排的(我认为这可能和大端序和小端序,《逆向核心原理》中第3章有关),而且ebp-4位置存的是cccccccc,这应该是那个参数b
6.一个用函数来实现两个乘除的简单代码
c源代码
原c代码如下,其中的运算都为有符号运算,由于之前没有写关于乘除的汇编会是怎么样,所以这次顺便弄一个乘除的。:
#include<stdio.h>
int function(int x,int y)
{
int z;
z=(x*y)/5;//注意这里面是故意取5的,为了看余数的值会储存在哪个寄存器中。
return z;
}
int main(void)
{
int a=3,b=4,c;
c=function(a,b);
printf("%d",c);
return 0;
}
实际上,如果你如果是z=(x*y)/2,其对应的汇编代码会有一些不同。就像这样。
00401038 mov eax,dword ptr [ebp+8]
0040103B imul eax,dword ptr [ebp+0Ch]//前两句实际上就是x*y
0040103F cdq
00401040 sub eax,edx
00401042 sar eax,1//这是通过向右位移1位来达到除以2的目的,好像只有除以2的时候汇编代码会成为这样。
00401044 mov dword ptr [ebp-4],eax
imul,cdq,sar这三个指令请在上面第一部分查看具体用法。
开始调试源程序
主函数
初始堆栈空间
执行这一句后的堆栈空间
401060 55 push ebp
执行这几句后的堆栈空间
401066 53 push ebx
401067 56 push esi
401068 57 push edi
401069 8D7D B4 lea edi,dword ptr ss:[ebp-4C]
40106C B9 13000000 mov ecx,13
401071 B8 CCCCCCCC mov eax,CCCCCCCC
401076 F3:AB rep stosd
执行这几句后的堆栈空间
401078 C745 FC 03000000 mov dword ptr ss:[ebp-4],3
40107F C745 F8 04000000 mov dword ptr ss:[ebp-8],4
401086 8B45 F8 mov eax,dword ptr ss:[ebp-8]
401089 50 push eax
40108A 8B4D FC mov ecx,dword ptr ss:[ebp-4]
40108D 51 push ecx
function函数框架
进入function函数后的堆栈空间
执行这一句后的堆栈空间
401020 55 push ebp
执行完这几句的堆栈空间
401021 8BEC mov ebp,esp
401023 83EC 44 sub esp,44
401026 53 push ebx
401027 56 push esi
401028 57 push edi
401029 8D7D BC lea edi,dword ptr ss:[ebp-44]
0040102C B9 11000000 mov ecx,11
401031 B8 CCCCCCCC mov eax,CCCCCCCC
401036 F3:AB rep stosd
接下来是计算的指令,就自己根据本文第一部分常用的汇编指令,来慢慢分析了
401038 8B45 08 mov eax,dword ptr ss:[ebp+8]
0040103B 0FAF45 0C imul eax,dword ptr ss:[ebp+C]
0040103F 99 cdq
401040 B9 05000000 mov ecx,5
401045 F7F9 idiv ecx
401047 8945 FC mov dword ptr ss:[ebp-4],eax
0040104A 8B45 FC mov eax,dword ptr ss:[ebp-4]
然后直到ret指令前的堆栈空间
执行ret后,回到主函数
执行这一句,保持堆栈平衡
add esp,8
后面的内容就不在调试了。
7.结构体指针
调试一个返回结构体指针的文件
c代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct _Student
{
char stuName[20];
int stuNo;
int stuAge;
}Student,*STUDENT;
int function(Student **p)
{
STUDENT q;//在子函数内部定义了一个结构体指针变量
q=(Student*)malloc(sizeof(Student));//向堆申请一段内存空间,返回的地址值给结构体指针变量。
strcpy(q->stuName,"wuhao"); //给结构体赋值
q->stuNo=2020122188;
q->stuAge=18;
*p=q;//将这个结构体地址返回给主函数的结构体指针变量。
return 0;
}
int main()
{
Student *p;//定义了一个结构体指针变量
function(&p);//传入结构体指针变量的地址
if(p->stuNo==2020122188 && p->stuAge==18 && !strcmp(p->stuName,"wuhao"))//检验
{
printf("sucess!\n");
printf("姓名:%s\n",p->stuName);
printf("学号:%d\n",p->stuNo);
printf("年龄:%d\n",p->stuAge);
}
else
{
printf("wrong!");
}
getchar();
}
调试过程
进入子函数前
进入子函数
回到主函数查看