目录
3、语句(if、switch、for、while、break)结构在asm表示
5、函数调用(栈stack):参数压栈顺序、传递、栈帧概念、返回
1、标识符(名字+类型) 从名字得到?从类型得到?
定义四种变量类型:char,int,double,float,如图所示:
汇编中表示如下:
在C语言中,char:1字节 int:4字节 double:8个字节 float:4个字节
在汇编语言中,指令格式为:操作码 目的操作数 源操作数
movsb、movsw、movsd三个指令都是数据传送指令,都是从源地址向目的地址传送数据。
byte、word、dword分别表示字节、字(2字节)、双字(4字节)。
从上面的汇编表示中可以看出,给变量赋值是使用mov指令的。其中char、int类型赋值都是直接使用mov指令将立即数存储到寄存器中,但是float和double类型进行赋值时,却使用了2次mov指令,这里我不太清楚是怎么回事。如图为float类型和double类型在内存中的存储方式:
汇编语言中,是将操作数放在寄存器中。如图为8086的寄存器结构:
1、通用寄存器:
2、指针和变址寄存器
SP(堆栈指针寄存器): 与SS(堆栈段寄存器)一起确定栈顶的当前位置;
BP(基址指针寄存器): 与SS用来确定堆栈段中的某一存储单元的地址;
SI(源变址寄存器):在串操作指令中,以SI表示源数据串的地址。
DI(目的变址寄存器):在串操作指令中,以DI表示目的数据串的地址。
这四个寄存器经常的用途是在存储器寻址时,提供偏移地址。用它们可实现多种存储器操作数的寻址方式!
CPU中的堆栈指针SP始终指向栈顶,而堆栈段寄存器SS则指明了堆栈段的起始位置!
作为通用寄存器,二者均可用于存放数据;
作为基址寄存器,BX通常用于寻址数据段;BP则通常用于寻址堆栈段。BX一般与DS或ES搭配使用,BP一般与SS搭配使用。
3、指令指针寄存器IP
IP是计算机中很重要的一个控制寄存器,用来存放代码段当中的偏移地址。在程序运行的过程中,它始终指向下一条指令的首地址,它和CS一起形成取下一条指令的实际地址。
4、标志寄存器
5、段寄存器
用于存放逻辑段的段基地址,将程序的不同部分放入相应段(逻辑段的概念后面将要介绍)
CS:代码段寄存器,代码段用于存放指令代码
DS:数据段寄存器
ES:附加段寄存器,数据段和附加段用来存放操作数
SS:堆栈段寄存器,堆栈段用于存放返回地址,保存寄存器内容,传递参数
2 、运算符+表达式(比如,算术+逻辑+关系)
1、四则算术运算
C中定义的四则运算:
汇编语言中的表示形式:
汇编中的四则运算指令有:加法指令ADD/ADC/INC、减法指令SUB/SBB/DEC/NEG/CMP、乘法指令MUL/IMUL、除法指令DIV/IDIV。
加法指令:
ADD指令将源与目的操作数相加,结果送到目的操作数。ADD指令按状态标志的定义相应设置标志位。
ADC指令将源与目的操作数相加,再加上进位CF标志,结果送到目的操作数。ADC指令按状态标志的定义相应设置。ADC指令主要与ADD配合,实现多精度加法运算。
INC指令对操作数加1(增量),INC指令不影响进位CF标志,按定义设置其他状态标志。它主要用于在循环程序中修改地址指针和循环次数等。
减法指令:
SUB指令将目的操作数减去源操作数,结果送到目的操作数。
SBB指令将目的操作数减去源操作数,再减去借位CF(进位),结果送到目的操作数。SBB指令主要与SUB配合,实现多精度减法运算。
DEC指令对操作数减1(减量),DEC指令不影响进位CF标志,按定义设置其他状态标志。
NEG指令对操作数执行求补运算:用零减去操作数,然后结果返回操作数。求补运算也可以表达成:将操作数按位取反后加1。
CMP指令将目的操作数减去源操作数,按照定义相应设置状态标志。CMP指令执行的功能同SUB指令,但结果不回送目的操作数。
乘法指令:
MUL指令:无符号字节(字)相乘。
IMUL指令:有符号字节(字)相乘。乘法指令的源操作数显式给出,隐含使用另一个操作数AX和DX。
除法指令:
DIV指令:无符号字节(字)除法。
IDIV指令:有符号字节(字)除法。除法指令的源操作数显式给出,隐含使用另一个操作数AX和DX。除法指令会产生结果溢出。
2、逻辑运算
C语言中定义:
汇编语言中表示:
汇编中的逻辑运算符有AND、OR、XOR和NOT,它们只能用于数值表达式中。
关系运算符的两个操作数必须都是数字或者是同一段内的两个存储器地址。结果始终是一个数字值。关系为真,结果为0FFFFH,关系为假,结果为0。6个关系运算符分别是:EQ(相等),NE(不等),LT(小于),GT(大于),LE(小于或等于),GE(大于或等于)。
SEG操作符
格式:SEG 变量或标号
功能:分离出其后变量或标号所在段的段首址。
OFFSET操作符
格式:OFFSET 变量或标号
功能:分离出其后变量或标号的偏移地址。
TYPE操作符
格式:TYPE 变量或标号
功能:分离出其后变量或标号的类型。如果是变量,将返回该变量的类型对应字节数;如果是标号,则返回代表标号类型的数值。它们之间的关系见表所示。属性(类型)操作符PTR
功能:用来对存储单元规定类型,通常和伪指令BYTE,WORD等连起来使用。
分析C语言中的逻辑与操作:
test指令:对两个操作数执行逻辑与运算,结果不回送到目的操作数,但要影响标志位,标志位影响同AND指令。
je指令:利用零标志ZF,判断结果是否为零(或相等)。 JZ/JE表示结果为零(或相等),则转移。
jmp指令:无条件跳转指令。
首先将第一个与操作数a1转移到寄存器eax,然后对eax进行与操作,之后使用je指令判断是否为0,如果满足条件(eax=0)则跳转到地址0FF5C65h。如果eax不为0,则将a2的值转移到ecx寄存器中,然后在检查a2(即ecx)是否为0。
3、关系运算
C语言定义:
汇编中表示:
3、语句(if、switch、for、while、break)结构在asm表示
1、if、switch分支结构
C语言中定义if、switch语言结构:
汇编中if语言结构:
汇编中switch语言结构:
汇编语言中的分支结构:
分支控制转移类指令通过改变IP(和CS)值,实现程序执行顺序的改变。
jmp指令:无条件跳转指令。
JZ/JE和JNZ/JNE:利用零标志ZF,判断结果是否为零(或相等)。 JZ/JE表示结果为零(或相等),则转移。
JS和JNS:利用符号标志SF,判断结果是正是负, JS表示结果为负则转移。
JO和JNO:利用溢出标志OF,判断结果是否产生溢出, JO表示溢出则转移。
JP/JPE和JNP/JPO:利用奇偶标志PF,判断结果中“1”的个数是偶是奇。JP表示偶个数个1。 JC和JNC:利用进位标志CF,判断结果是否进位或借位。进位或借位为1则转移。
无符号数高低有4种关系:
⑴高于(不低于等于): JA ( JNBE )
⑵高于等于(不低于): JAE (JNB)
⑶低于(不高于等于):JB(JNAE)
⑷低于等于(不高于):JBE(JNA)
有符号的大小分成4种关系:
⑴大于(不小于等于): JG ( JNLE )
⑵大于等于(不小于): JGE (JNL)
⑶小于(不大于等于):JL(JNGE)
⑷小于等于(不大于):JLE(JNG)
分析if语言结构:
EAX、ECX、EDX、EBX:为ax,bx,cx,dx的延伸,32位。ESI、EDI、ESP、EBP:为si,di,sp,bp的延伸,32位。 CMP指令将目的操作数减去源操作数,CMP指令执行的功能同SUB指令,但结果不回送目的操作数。
if(money > 200)语句对应的汇编语句是:
00E15D66 cmp dword ptr [money],0C8h
00E15D70 jle main+147h (0E15D87h)
首先使用cmp指令执行money-0C8h(200),然后使用jle执行分支跳转,如果结果小于等于0(即money<=200),不执行if中的操作,而是跳转到0E15D87h中执行else中的操作。
在该if语句中就已经执行了一次函数调用功能,它调用了C中iostream包中的cout函数。 std::cout << "今天很开心!!\n";语句对应的汇编语句如下:
00E55D72 push offset string "\xbd\xf1\xcc\xec\xba\xdc\xbf\xaa\xd0\xc4!\n" (0E59B30h)
00E55D77 mov eax,dword ptr [imp?cout@std@@3V?basicostream@DU?basicostream@DU?char_traits@D@std@@@1@A (0E5D0C8h)]
00E55D7C push eax
00E55D7D call std::operator<<std::char_traits<char > (0E51203h)
00E55D82 add esp,8
offest操作符:分离出其后变量或标号的偏移地址。
push指令:压栈指令,PUSH指令首先减少ESP的值,再将源操作数复制到堆栈SS:SP指向的内容单元。操作数是16的,则ESP减2,操作数是32位的,则ESP减4。
首先使用push指令将字符串的偏移地址保存到SP所指向的内容单元,此时EIP = 00E55D77,ESP=004FFA7C,查看内容单元的值,发现的确是将该偏移地址存放在内存单元中。该push指令的作用是将参数保存在栈中。然后使用mov指令向eax赋值,此时EAX = 78A0CA10。将改值也使用push指令保存到栈中。
使用call指令调用函数cout时,此时EIP = 00E55D7D,ESP = 004FFA78,EBP = 004FFC2C。
分析switch语言结构:
switch(today)对应的汇编语句是:
00E55DA4 mov eax,dword ptr [today]
00E55DAA mov dword ptr [ebp-1A0h],eax
00E55DB0 mov ecx,dword ptr [ebp-1A0h]
00E55DB6 sub ecx,1
00E55DB9 mov dword ptr [ebp-1A0h],ecx
00E55DBF cmp dword ptr [ebp-1A0h],6
00E55DC6 ja $LN12+0Fh (0E55E3Eh)
00E55DC8 mov edx,dword ptr [ebp-1A0h]
00E55DCE jmp dword ptr [edx*4+0E55E74h]
该结构中,首先将today的值保存到[ebp-1A0h]内存单元中,此时EBP = 004FFC2C,之后需要将该值减1后与6进行比较(这里不清楚为什么这样做),如果(today-1)>6,则跳转到地址0E55E3Eh,否则的话将该值复制到edx,然后就可以跳转到edx*4+0E55E74h地址执行操作(不清楚为什么)。
break语句也是汇编中的一个分支语句,对应的汇编语句是:jmp $LN12+1Ch (0E55E4Bh)
2、for、while分支结构
C语言中定义for、while语言结构:
汇编中的for语言结构:
汇编语言中的while语言结构:
汇编语言中的循环结构:
循环结构一般是根据某一条件判断为真或假来确定是否重复执行循环体,循环指令和转移指令可以实现循环控制;
循环指令默认利用CX计数器,方便实现计数循环的程序结构。
loop循环指令:
LOOP label ;CX←CX-1,;CX≠0,循环到标号label
LOOPZ label ;CX←CX-1,;CX≠0且ZF=1,循环到标号label
LOOPNZ label ;CX←CX-1,;CX≠0且ZF=0,循环到标号label
JCXZ label ;CX=0,转移到标号label
分析for语言结构:
for (int i = 0; i < 3; i++)语句对应的汇编为:
000F5E55 mov dword ptr [ebp-0F0h],0
000F5E5F jmp LN17+41h (0F5E70h)
000F5E61 mov eax,dword ptr [ebp-0F0h]
000F5E67 add eax,1
000F5E6A mov dword ptr [ebp-0F0h],eax
000F5E70 cmp dword ptr [ebp-0F0h],3
000F5E77 jge $LN17+5Eh (0F5E8Dh)
首先将初始值0复制到内存单元[ebp-0F0h],此时EBP = 008FFA34,[ebp-0F0h]对应的内存单元值为:使用jmp指令无条件跳转到地址0F5E70h,使用cmp指令和3进行比较,如果大于等于3,就跳出循环,跳到地址0F5E8Dh处。否则的话就执行for循环体中的操作。执行完操作后,使用jmp指令再跳回到for循环的判断语句地址处,即地址0F5E61h。然后操作数eax+1,再和3进行比较,如果满足条件则继续执行循环体中的操作。
分析while语言结构:
while循环语句对应的汇编为:
00205EAB cmp dword ptr [sum1],4
00205EB2 jg $LN17+96h (0205EC5h)
因为while循环语句中的判断条件是sum1<=4,所以首先使用cmp指令将sum1的值和4进行比较,如果sum1>4,则使用jg指令跳转到地址0205EC5h出,即跳出循环。否则就执行while循环体中的操作。执行完之后继续使用jmp指令跳转到条件判断地址处0205EABh,判断是否结束循环。
4、数组、结构体、指针
C语言中定义数组、结构体和指针:
1、数组
汇编语言中的数组定义:
汇编中定义数组其实就是分配地址空间,第一行将0赋值到list地址内存空间中,此时系统给list分配的地址为0x010FF7D4,如图所示:
之后会将数组中的值依次存储到0x010FF7D4之后的内存空间中,比如ebp-12Ch=0x010ff7d8,内存结果如图:
2、结构体
汇编语言中的结构体定义:
汇编中的结构体和数组定义类似,都是给结构体分配内存地址,之后再将实例化的结构体数据存储到内存空间即可。
3、指针
汇编语言中的指针定义:
lea指令:将源操作数的有效地址传送至指定的16位寄存器中。指令中“r16”常用的寄存器是BX、BP、SI、DI。一般不使用其他寄存器。该指令常被描述为“取变量的偏移地址”。
如图所示,在汇编中定义指针变量,就是取变量的偏移地址,然后将该偏移地址保存到指针地址的内存空间中。
5、函数调用(栈stack):参数压栈顺序、传递、栈帧概念、返回
1、基本概念
(1)栈不是数据结构中的栈,而是计算机内存中的一块存储区,它的访问方式是“先进后出”。大多数情况下,栈是从高地址向低地址增长的。
(2)栈有很多单元格,通常情况下每个单元格是8位的(即可以存8个0或1),称为数据宽度,是用来存放数据的。每个单元格都会对应一个地址,地址一般是无符号32位的整数。
(3)栈的操作涉及到两个寄存器,即ESP和EBP(什么是寄存器这里就不多说了),另外还有两个指令,POP和PUSH。这两个寄存器分别称为栈指针寄存器和基址指针寄存器,因此这两个寄存器中存的是指针,即地址(这个地址就是前面说到的单元格对应的地址),更确切的说应该是栈帧顶部的地址(ESP)和栈帧底部的地址(EBP)。
(4)栈是从高地址向低地址增长的,高地址对应栈底,低地址对应栈顶,每个存放数据的单元格都有一个对应的地址,每个单元格的宽度为8位。PUSH是向栈中推入一个数放在栈顶,POP是将栈顶的数据弹出。无论是推入新的数据还是弹出数据,ESP始终指向栈顶。
(5)栈帧:栈帧是一块连续的栈区,每个函数都有自己的栈区,这个栈区就称为栈帧。注意,函数调用所占用的栈区才称为栈帧,并且每个函数都会有自己的栈帧,包括main函数。当前栈帧的范围在EBP和ESP指向的区域之间。
2、函数调用大体流程
假设有两个函数A和B,A函数调用B函数,调用B函数需要传入两个参数x和y。
1:在A调用B函数之前,A先把需要传入B函数中的参数x,y推入栈中。(注意x,y被放在了A的栈帧中)
2:在执行函数调用的时候(即执行call指令),A把B函数的返回地址推入栈中(还是在A的栈帧中)。
3:在进入B函数后,推入旧的EBP的值,这时ESP指向的单元格中的内容是旧的EBP,然后令EBP等于当前的ESP,则这个EBP即为B函数的栈帧的栈底,EBP指向的单元格中的数据是旧的EBP。这里比较抽象,下面有一个图可以帮助理解。
4:开辟一块相对合适的空间用来存放非静态局部变量(存放非静态局部变量前会先置成cc)。
5:将被调用者保存寄存器中的内容推入栈中进行保护。
6:执行函数中的内容。
7:函数调用完毕,return。返回的时候将EDI、ESI、EBX依次弹出,然后让ESP指向EBP,将ESP指向的单元格中的内容(即旧的EBP)弹到EBP中,这样EBP又重新变成了A的栈帧的栈底,执行ret指令,弹出B的返回地址,然后ESP根据参数的个数加上相应的数使ESP指向原来A的栈帧的栈顶(一个参数加4,两个加8,以此类推)。
3、实验操作
C语言中定义函数,并进行调用:
汇编中调用函数的代码:
分析过程:
首先push指令将函数中的两个参数压入栈中,此时ESP = 0057F9AC,查看该地址指向的内存空间,如图:然后调用函数Sum,函数地址为:046140Bh。调用函数时,ESP、SIP的值发生了改变,EIP = 0046140B ESP = 0057F9A4,查看ESP地址内存单元,发现在调用函数之前,先将下一条语句的地址00465CEB压入栈顶,此时内存单元内容如图所示:
此时汇编中的函数体代码如图所示:
此时进入新的函数Sum中,将旧的EBP值压入栈中,即将主函数main中的栈底地址压入main函数的栈帧中,此时main函数中的ESP = 0057F9A0,main函数的栈帧内存单元内容为:
然后将main函数的栈顶地址ESP赋值给EBP,该EBP地址就是Sum函数的栈帧的栈底地址。然后对Sum的栈顶指针ESP重新赋值。定义好Sum的栈顶指针地址后,将ebx、esi、edi压入Sum函数的栈顶。如图所示:
然后执行函数体中的内容。执行完之后,使用pop指令弹出edi、esi、ebx值,然后将esp加上Sum函数栈帧的大小,得到主函数main的栈帧的栈顶地址ESP。然后又对EBP和ESP比较大小,这里不清楚为什么比较,可能是为了检查返回的栈顶地址是否正确吧。然后从main栈帧栈顶内存空间中弹出ebp,此时ebp的值就是main函数原来的栈底指针。执行ret指令,弹出栈顶的返回地址。即刚开始保存的函数执行的下一条语句地址。
如此就是一个完整的函数调用和执行过程。
对应C代码语言
#include <iostream>
int Sum(int a, int b)
{
int sum = 0;
sum = a + b;
return sum;
}
int main()
{
int Fsum = Sum(3, 4);
printf("Fsum:%d\n", Fsum);
// 标识符
char c = 'A';
int a = -1;
double b = 12.34;
float d = 123.4f;
// 运算符+表达式
int add = a + 4;
int sub = 5 - 2;
int mul = add * sub;
int div = add / sub;
//逻辑运算
bool a1 = true, a2 = false;
bool c1 = a1 && a2; // 逻辑与
bool c2 = a1 || a2; // 逻辑或
bool c3 = !a2; // 逻辑非
// 关系运算符
bool d1 = 1 > 2;
bool d2 = 1 == 2;
bool d3 = 1 <= 2;
// if、switch语言结构
int money = 234;
if (money > 200)
std::cout << "今天很开心!\n";
else
std::cout << "今天很不开心!\n";
int today = 3;
switch (today) {
case 1:printf("Monday\n"); break;
case 2:printf("Tuesday\n"); break;
case 3:printf("Wednesday\n"); break;
case 4:printf("Thursday\n"); break;
case 5:printf("Friday\n"); break;
case 6:printf("Saturday\n"); break;
case 7:printf("Sunday\n"); break;
default:printf("error\n"); break;
}
// for循环
int sum = 0;
for (int i = 0; i < 3; i++)
{
sum += i;
}
printf("%d\n", sum);
// while循环
int sum1 = 0;
while (sum1 <= 4) {
sum1 += 1;
}
printf("%d\n", sum1);
// 数组
int list[10] = { 0,1,2,3,4,5,6,7,8,9 };
// 结构体
struct stu{
int num;
int age;
char group;
float score;
} stu1 = { 12, 18, 'A', 136.5 };
// 指针
struct stu* pstu = &stu1;
printf("%d\n", pstu->age);
std::cout << "Hello World!\n";
return 0;
}