第一章:C语言语法进阶
一、条件运算符与逗号运算符
1、条件运算符:
条件运算符是C语言中唯一的一种三目运算符。三目运算符代表有三个操作数;双目运算符代表有两个操作数,如逻辑与运算符就是双目运算符;单目运算符代表有一个操作数,如逻辑非运算符就是单目运算符。运算符也称操作符,三目运算符通过判断问号之前的表达式的真假,来确定整体表达式的值,如下例所示:如果a>b为真,那么三目表达式整体的值为 a,所以max 的值等于a,如果 a>b为假、那么三目表达式整体的值为b,所以 max的值等于 b。
基础语法:
条件表达式 ? 表达式1 : 表达式2
执行逻辑:
完整代码:
#include <stdio.h>
int main() {
int a,b,max;
while(scanf("%d%d",&a,&b))
{
max=a>b?a:b;
printf("max=%d\n",max);
}
//想让上述代码结束可以输入单个字符a
return 0;
}
2、逗号运算符:
逗号运算符的优先级最低,我们需要掌握的是,逗号表达式的整体值是最后一个表达式的值。
基础语法:
表达式1, 表达式2, ..., 表达式N
执行逻辑:
完整代码:
#include <stdio.h>
int main() {
int i,j;
i=10;
j=1;
if(i,j-1)
{
printf("if excute\n");
}
for(i=0,j=1;i<10;i++)
{
}
return 0;
}
二、自增自减运算符
1、自增、自减运算符和其他运算符有很大的区别:
因为其他运算符除赋值运算符可以改变量本身的值外,不会有这种效果。自增、自减就是对变量自身进行加1、减1操作,那么有了加法和减法运算符为什么还要发明这种运算符呢?原因是自增和自减来源于B语言,当时KenThompson 和 Dennis M.Ritchie(C语言的发明者)为了不改变程序员的编写习惯,在C语言中保留了 B语言中的自增和自减。因为自增、自减会改变变量的值,所以自增和自减不能用于常量!
2、例题:
下例中的j=i++>-1,对于后++或者后--,首先我们需要去掉++或--运算符,也就是首先计算j=i>-1,因为i本身等于-1,所以得到j的值为0, 接着单独计算i++,也就是对i加 1,所以i从-1加1得到 0,因此 printf("i=%d,j=%d\n",i,j);语句的执行结果是0和0。
完整代码:
#include <stdio.h>
//考研初试如果使用++,或者--,最好单独使用,不要和其他运算符混到一起
int main() {
int i=-1,j;
//5++;//如果打开该句,会造成编译不通
j=i++>-1;//后++等价于j=i>-1;i=i+1;
printf("i=%d,j=%d\n",i,j);
return 0;
}
3、自增自减运算符与取值运算符(408 初试不需要掌握,初试考C的学校需要,部分机试答案含有)
#include <stdio.h>
#include <stdlib.h>
//只有比后增优先级高的操作符,才会作为一个整体,如:[]、()
int main() {
int a[3]={3,7,8};
int *p;
int j;
p=a;//p指向数组起始元素
j=*p++;//先把*p的值赋给j,然后对p加1。j=*p;p++;
printf("a[0]=%d,j=%d,*p=%d\n",a[0],j,*p);//输出3 3 7
j=p[0]++; //先把 p[0]赋给j,然后对 p[0]加1
printf("a[0]=%d,j=%d,*p=%d\n",a[0],j,*p);//输出3 7 8
return 0;
}
三、位运算符
1、位运算符<<、>>、~、|、^、&依次是左移、右移、按位取反、按位或、按位异或、按位与.
- 位运算符只能用于对整型数据进行操作。
- 左移:高位丢弃,低位补0,相当于乘以2.工作中很多时候申请内存时会用左移,
例如:要申请1GB大小的空间,可以使用malloc(1<<30).
- 右移:低位丢弃,正数的高位补0(无符号数我们认为是正数),负数的高位补1,相当于除以 2.移位比乘法和除法的效率要高,负数右移,对偶数来说是除以2,但对奇数来说是先减1后除以2.
例如,-8>>1,得到的是-4,但-7>>1得到的并不是-3而是-4.另外,对于-1来说,无论右移多少位,值永远为-1.
- C语言的左移和右移相当于是算术左移与算术右移,考研中的逻辑左移与右移,左移和右移空位都补 0.
- 异或:相同的数进行异或时,结果为0,任何数和0异或的结果是其本身。
- 按位取反:数位上的数是1变为 0,0变为1.
- 按位与和按位或:用两个数的每一位进行与和或
#include <stdio.h>
int main() {
short i=5;//short是short int的缩写,short是整型,是2个字节的整型,int是4个字节
short j;
//左移 右移
j=i<<1;//一个变量移动以后自身不会变化
printf("i=%d\n",j);//左移是乘以2,结果为10
j=i>>1;
printf("j=%d\n",j);//右移是除2,结果是2
printf("------------------\n");
//无符号数的左移与右移
i=0x8011;
unsigned short s=0x8011;//在short前加unsigned就是无符号数
unsigned short r=0;
j=i>>1;//对i右移,对有符号数进行右移
r=s>>1;//对s右移,对无符号数进行右移
printf("j=%d,r=%u\n",j,r);//有符号数和无符号数右移之后的结果是不一样的
printf("------------------\n");
//接下来看 按位与,按位或,按位异或,按位取反
i=5,j=7;
printf("i & j=%d\n",i&j);
printf("i | j=%d\n",i|j);
printf("i ^ j=%d\n",i^j);
printf("~i=%d\n",~i);
return 0;
}
2、异或运算符特性:
异或运算符有以下特性:
- 归零律:
a ^ a = 0
(相同数异或结果为0) - 恒等律:
a ^ 0 = a
(任何数与0异或结果不变) - 交换律:
a ^ b = b ^ a
- 结合律:
(a ^ b) ^ c = a ^ (b ^ c)
我们可以完成下面的题目,在一堆数中找出出现1次的那个数,其他数是出现2次.
#include <stdio.h>
int main() {
int i;
int arr[5]={8,5,3,5,8};
int result = 0;
for(i=0;i<5;i++)
{
result ^= arr[i];
}
printf("%d\n",result);//输出为3
return 0;
}
四、switch & do-while
1、switch
判断的一个变量可以等于几个值或几十个值时,使用if和elseif语句会导致 elseif分支非常多,这时可以考虑使用switch语句,switch语句的语法格式如下:
switch(表达式)
{
case 常量表达式 1:语句1
case 常量表达式 2:语句2
...
case 常量表达式 n:语句n
default:语句 n+1
}
下面来看一个使用switch语句的例子。
如例1所示,输入一个年份和月份,然后打印对应月份的天数,如输入一个闰年和2月,则输出为29天.具体代码如下所示,switch语句中 case后面的常量表达式的值不是按照1到12的顺序排列的,这里要说明的是,switch语句匹配并不需要常量表达式的值有序排列,输人值等于哪个常量表达式的值,就执行其后的语句,每条语句后需要加上 break语句,代表匹配成功一个常量表达式时就不再匹配并跳出switch 语句.
如果一个 case语句后面没有break语句,那么程序会继续匹配下面的case 常量表达式例2中的代码是对例1中代码的优化。例2的代码执行效果和上面的代码执行效果一致,原理是只要匹配到1、3、5、7、8、10、12 中的任何一个,就不再拿 mon 与case 后的常量表达式的值进行比较, 而执行语句 printf("mon=%dis 31days\n",mon),完毕后执行 break 语句跳出switch语句.switch语句最后加入default 的目的是,在所有case后的常量表达式的值都未匹配时,打印输出错误标志或者一些提醒,以便让程序员快速掌握代码的执行情况。
#include <stdio.h>
int main() {
int mon,year;
while(scanf("%d%d",&year,&mon))
{
switch (mon)
{
case 2:printf("mon=%d is %d days\n",mon,28+(year%4==0&&year%100!=0||year%400==0));break;
case 1:printf("mon=%d is 31days\n",mon);break;
case 3:printf("mon=%d is 31days\n",mon);break;
case 5:printf("mon=%d is 31days\n",mon);break;
case 7:printf("mon=%d is 31days\n",mon);break;
case 8:printf("mon=%d is 31days\n",mon);break;
case 10:printf("mon=%d is 31days\n",mon);break;
case 12:printf("mon=%d is 31days\n",mon);break;
case 4:printf("mon=%d is 30days\n",mon);break;
case 6:printf("mon=%d is 30days\n",mon);break;
case 9:printf("mon=%d is 30days\n",mon);break;
case 11:printf("mon=%d is 30days\n",mon);break;
default:
printf("error mon\n");
}
}
return 0;
}
升级版:
#include <stdio.h>
int main() {
int mon,year;
while(scanf("%d%d",&year,&mon))
{
switch (mon)
{
case 2:printf("mon=%d is %d days\n",mon,28+(year%4==0&&year%100!=0||year%400==0));break;
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:printf("mon=%d is 31days\n",mon);break;
case 4:
case 6:
case 9:
case 11:printf("mon=%d is 30days\n",mon);break;
default:
printf("error mon\n");
}
}
return 0;
}
2、do while语句的特点是:
先执行循环体,后判断循环条件是否成立,其一般形式为
do
{
循环体语句;
}while(表达式);
执行过程如下:首先执行一次指定的循环体语句,然后判断表达式,当表达式的值为非零(真)时,返回重新执行循环体语句,如此反复,直到表达式的值等于0为止,例3是使用dowhile语句计算 1到100之间所有整数之和的例子,do while语句与while 语句的差别是,do while语句第一次执行循环体语句之前不会判断表达式的值, 也就是如果i的初值为101,那么依然会进入循环体,实际工作中 do while 语句应用较少.
do while 语句计算1到100 之间的所有整数之和:
#include <stdio.h>
int main() {
int i=1,total=0;
do{
total=total+i;
i++;
}while(i<=100);//无论这里判断条件是什么循环都会先执行一次
printf("total=%d\n",total);
return 0;
}
五、二维数组&二位指针
1、二维数组
二维数组定义的一般形式如下:
类型说明符 数组名[常量表达式][常量表达式];
例如,定义a为3x4(3行4列)的数组,b为5x10(5行10列)的数组:
float a[3][4].b[5][10];
可以将二维数组视为一种特殊的一维数组:一个数组中的元素类型是一维数组的一维数组.
例如,可以把二维数组 a[3][4]视为一个一维数组,它有3个元素 a[0]、a[1]和 a[2],每个元素又是一个包含 4个元素的一维数组,如图1所示。
二维数组中的元素在内存中的存储规则是按行存储,即先顺序存储第一行的元素,后顺序存储第二行的元素,数组元素的获取依次是从 a[0][0]到 a[0][1],直到最后一个元素 a[2][3].
图2中显示了存储二维数组 a[3][4]中每个元素时的顺序
#include <stdio.h>
int main()
{
//通过调试查看元素存放顺序
int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12 };
printf("sizeof(a)=%d\n",sizeof(a));//掌握二维数组大小
printf("a[2][3]=%d\n", a[2][3]);//最后一个元素是 a[2][3]
return 0;
}
2、二级指针(不在 408 大纲范围,408 初试不考,初试考C 的学校可能需要,机试用到概率极低))
如果掌握了 C++的引用,其实不需要去学习二级指针,对于考研必要性很低,二级指针是指针的指针,二级指针的作用是服务于一级指针变量,对一级指针变量实现间接访问。下面我们通过一个实例来让大家理解一下二级指针。
#include <stdio.h>
int main()
{
int i=10;
int *p=&i;
int **p2=&p;//如果我们需要把一个一级指针变量的地址存起来,那么就需要二级指针类型
printf("sizeof(p2)=%d\n",sizeof(p2));//p2和p同样大,都是8个字节
printf("**p2=%d\n",**p2);//通过两次取值可以拿到ic
return 0;
}
第二章:数据的的机器级表示
一、补码与内存
1、补码讲解及内存实战演示(组成常考选择题)
计算机的CPU无法做减法操作(硬件上没有减法器),只能做加法操作。CPU中有一个逻辑单元叫加法器。计算机所做的减法,都是通过加法器将其变化为加法实现的.那么减法具体是如何通过加法实现的呢?实现 2-5的方法是2+(-5),由于计算机只能存储0和1.因此我们编写程序来查看计算机是如何存储-5的,5的二进制数为101.称为原码。计算机用补码表示-5,补码是对原码取反后加1的结果,即计算机表示-5时会对5的二进制数(101)取反后加1,如图1所示.-5在内存中存储为0xffffb,因为对5取反后得0xffffffa,加1后得 OxffffHb(由于是X86架构是小端存储,小端存储是低字节在前,即低字节在低地址,高字节在后,即高字节在高地址,fb对于0xfffffffb是最低的字节,因此fb在最前面,大端和小端相反),对其加2后得0xfffffd,见图2,它就是k的值。当最高位为1(代表负数)时,要得到原码才能知道 0xfffffffd 的值,即对其取反后加1(当然,也可以减1后取反,结果是一样的)得到3,所以其值为-3.
考研注意,假设变量A的值为-5,通过8位表示,那么A 值为11111011,A厕的值为 10000101,符号位是不动的,只有值的部分是5,通过符号位不动,其他值取反加1,这个是考研考的一个原理,和不少C编程书籍上讲的存在差异,正数的补码和原码一致.
2、反码(代码不好演示,考试考的概率低)
反码是一种在计算机中数的机器码表示。对于单个数值(二进制的0和1)而言,对其进行取反操作就是将0为1,1为0.正数的反码和原码一样,负数的反码就是在原码的基础上符号位保持不变,其他位取反.
6是正数,补码与原码一致,-3的补码是11111101
二、整型的不同类型&&溢出
1、整型不同类型解析(组成要考,选择题)
如图1所示,整型变量包括6种类型,如图2所示,其中有符号短整型与无符号短整型的最高位所代表的意义不同。不同整型变量表示的整型数的范围如表1所示,超出范围会发生溢出现象,导致计算出错。
(有符号位第一位0是正数,1是负数,无符号位第一位不代表正负)
考研补充说明:
考研会考8位的,也就是1个字节的整型数的大小,对于1个字节的有符号类型的数值范围是-2的7次幂~(2的7次幂-1),也就是-128到127,对于8位的无符号(unsigned)类型的数值范围是0~(2的8次幂--1),也就是 0-255.
最小的负数是-32768,最大的负数是-1,有符号整数个数为32768,无符号整数个数为65535。
2、溢出解析
如下例所示,有符号短整型数可以表示的最大值为32767,当我们对其加1时,b的值会变为多少呢?实际运行打印得到的是-32768。为什么会这样?因为32767对应的十六进制数为0x7ff,加1后变为0x8000,其首位为1,因此变成了一个负数。取这个负数的原码后,就是其本身,值为 32768,所以 0x8000是最小的负数,即-32768.这时就发生了溢出,我们对32767加1,希望得到的值是 32768,但结果却是-32768,因此导致计算结果错误。在使用整型变量时,一定要注意数值的大小,数值不能超过对应整型数的表示范围.有的读者可能会问在编写的程序中数值大于 2的64次幂-1 时怎么办?答案是可以自行实现大整数加法。
#include <stdio.h>
//整型不同类型的演示,以及溢出演示
int main() {
int i=10;
short a=32767;
short b=0;
long c;//32位的程序是4个字节,64位的是8个字节
i=a+1;//发生了溢出,解决溢出的办法是用更大的空间来存储,比如:将long换成int
printf("%d\n",i);//b并不是32767
printf("----------------------\n");
unsigned int m=3;
unsigned short n=0x8056;//无符号类型,最高位不认为是符号位
unsigned long k=5;
b=0x8056;
printf("b=%d\n",b);//b是有符号类型,所以输出是负值
printf("n=%u\n",n);//无符号类型要用%u,用%d是不规范的
return 0;
}
三、浮点数IEEE754标准
1、浮点数IEEE754 标准解析及实战计算演示(组成要考选择题)
在C语言中,要使用 foat 关键字或 double 关键字定义浮点型变量.float型变量占用的内存空间为4字节,double型变量占用的内存空间为8字节。与整型数据的存储方式不同,浮点型数据是按照指数形式存储的,系统把一个浮点型数据分成小数部分(用M表示)和指数部分(用E表示)并分别存放。指数部分采用规范化的指数形式,指数也分正、负(符号位,用S表示),如图1所示
数符(即符号位)占1位,是0时代表正数,是1时代表负数,表1是IEEE-754浮点型变量存储标准。
S:S是符号位,用来表示正、负,是1时代表负数,是0时代表正数.
E:E代表指数部分(指数部分的值规定只能是1到254,不能是全0,全1),指数部分运算前都要减去127(这是IEEE-754的规定),因为还要表示负指数,这里的10000001转换为十进制数为129,129-127=2,即实际指数部分为2.
M:M 代表小数部分,这里为00100000000000000000000.底数左边省略存储了一个1(这是IEEE-754 的规定),使用的实际底数表示为1.00100000000000000000000
(1.001 小数部分 1.125(1+0+0+0.125) 小数部分乘以指数部分就是4.5)
(移位算法:1001.1 4+0.5=4.5)
上面表1可以变为如下表格:
2、执行方法如下:
#include <stdio.h>
int main() {
float f = 4.5;
float f1 = 1.456;
return 0;
}
在上图中我们可以看到 4.5,也就是变量f的内存是0000 9040,因为是小端存储,所以实际值是 0x40900000.
首先先看f的小数部分,如下表2所示,M(灰色)代表小数部分,这里为00100000000000000000000,总计23位,底数左边省略存储了一个1(这是IEEE-754的规定),使用的实际底数表示为1.00100000000000000000000
接着看指数部分,计算机并不能直接计算 10的幂次,f的指数部分是表2中的EEEEEEEE所对应的部分,也就是10000001,其十进制值为129,129-127=2,即实际指数部分为2指数值为2,代表2的2次幂。因此将1.001向左移动2位即可,也就是100.1;然后转换为十进制数,整数部分是4,小数部分是2-1,刚好等于0.5,因此十进制数为 4.5.浮点数的小数部分是通过2-1+2-2+2-+…来近似一个浮点数的.
可能你会疑惑,不应该是小数部分乘以指数部分么,怎么变成左移 2位,其实是等价的,对于小数部分 1.001,其十进制值为2°+2-3=1.125.那么 1.125*指数部分,也就会4,就是 1.125*4=4.5,也就是等于 4.5.
在上图中我们可以看到1.456,也就是变量f1的内存是35 5e ba 3f,因为是小端存储,所以实际值是 0x 3f ba 5e 35.
首先先看f1的小数部分,如下表3所示,M(灰色)代表小数部分,这里为0111010010111100011 0101,总计23位。底数左边省略存储了一个1(这是IEEE-754的规定),使用的实际底数表示为1.011 1010 0101 1110 0011 0101.
接着看指数部分,计算机并不能直接计算10的幂次,f1的指数部分是表3中的 EEEEEEEE所对应的部分,也就是01111 111,其十进制值为127,127-127=0,即实际指数部分为0指数值为0,代表2的0次幂。因此1.01110100101111000110101无需做移动.
浮点数的小数部分是通过 2°+22+23+2-+2-…来近似一个浮点数的,1+0.25+0.125+0.0625+0.015625=1.453125
四、浮点数精度丢失
浮点型变量分为单精度(foat)型、双精度(double)型,如表1所示,因为浮点数使用的是指数表示法,需要记忆数值的范围(考研会考),同时我们需要注意浮点数的精度问题。
上表中 double 类型是-1022到 1023,是通过 1-2046(因为不能是全0,全1,全1是2047)减去 1023,得到-1022到 1023。
如下例所示,我们赋给 a的值为1.23456789e10,加20 后,应该得到的值为 1.234567892e10但b输出结果却是b=12345678848.000000,变得更小了。我们将这种现象称为精度丢失,因为 float 型数据能够表示的有效数字为 7位,最多只保证 1.234567e10 的正确性,要使结果正确就需要把 a和b均改为 double 型,因为 double 可以表示的精度为 15~16 位。
#include <stdio.h>
//提醒 scanf 读取 double类型时,要用 if,如 double d;scanf("%f",&d);初试用不到,机试可能考
int main() {
//赋值的一瞬间发生 精度丢失,因为浮点常量默认是8 个字节存储,double 型
double a= 1.23456789e10;
double b;
b=a+ 20;//计算时,精度丢失 12345678920
printf("b=%f\n",b);
//%f 即可以输出 float,也可以输出 double类型
return 0;
}
另外针对强制类型转换,int 转foat 可能造成精度丢失,因为int是有10 位有效数字的,但是int强制转为 double 不会,foat转为 double也不会丢失精度。
第三章:汇编语言零基础入门
一、汇编指令格式讲解-C语言转汇编方法讲解
1、汇编指令格式
在去看汇编指令前,我们来看下CPU是如何执行我们的程序的,如下图所示,我们编译后的可执行程序,也就是 main.exe,是放在代码段的,PC指针寄存器存储了一个指针,始终指向要执行的指令,读取了代码段的某一条指令后,会交给译码器来解析,这时候译码器就知道要做什么事情了,CPU 中的计算单元加法器不能直接对栈上的某个变量a,直接做加1操作的,需要首先将栈,也就是内存上的数据,加载到寄存器中,然后再用加法器做加1操作,再从寄存器搬到内存上去。
(CPU 读写寄存器的速度比读写内存的速度要快很多)
操作码字段(你要实施什么操作):表征指令的操作特性与功能(指令的唯一标识)不同的
指令操作码(要操作哪里的内存):不能相同地址码字段:指定参与操作的操作数的地址码
指令中指定操作数存储位置的字段称为地址码,地址码中可以包含存储器地址,也可包含寄存器编号.
指令中可以有一个、两个或者三个操作数,也可没有操作数,根据一条指令有几个操作数地址可将指令分为零地址指令。一地址指令、二地址指令、三地址指令。4个地址码的指令很少被使用(考研考不到,这里不列了)
零地址指令:只有操作码,没有地址码(空操作止等)。
一地址指令:指令编码中只有一个地址码,指出了参加操作的一个操作数的存储位置,如果还有另一个操作数则隐含在累加器中。
eg:INC AL 自加指令
二地址指令:指令编码中有两个地址,分别指出了参加操作的两个操作数的存储位置,结果存储在其中一个地址中。
(op a1,a2) a1和 a2 的操作结果放人a1
eg: MOV AL,BL
ADD AL,30
三地址指令:指令编码中有3个地址码,指出了参加操作的两个操作数的存储位置和一个结果的地址(考研很少考)。
(op al,a2,a3:al和a2的结果放人 a3)
地址指令格式中,从操作数的物理位置来说有可归为三种类型
寄存器-寄存器(RR)型指令:需要多个通用寄存器或个别专用寄存器,从寄存器中取操作数把操作结果放入另一个寄存器,机器执行寄存器-寄存器型的指令非常快,不需要访存。
寄存器-存储器(RS)型指令:执行此类指令时,既要访问内存单元,又要访问寄存器。
存储器-存储器(SS)型指令:操作时都是涉及内存单元,参与操作的数都是放在内存里从内存某单元中取操作数,操作结果存放至内存另一单元中,因此机器执行指令需要多次访问内。
寄存器英文:register
存储器英文:storage
(*)复杂指令集:变长 x86 CISC Complex Instruction Set Computer
精简指令集:等长/定长 arm RISC Reduced Instruction Set Computinar
2、生成汇编方法
掌握把 C 语言转换汇编的手法,那么就不会再害怕汇编了!
编译过程(了解即可,是大纲范围,考的概率不高)
第一步:main.c-->编译器--》main.ss文件(.s文件就是汇编文件,文件内是汇编代码);
第二步:我们的 main.s汇编文件--》汇编器--》main.obj(main.obj 里面是机器码);
第三步:main.obj 文件-》链接器-》可执行文件exe。
CLion 生成汇编的步骤(很多书籍不讲这个,直接就给你看汇编指令)
1、首先我们需要设置环境变量,对 此电脑右键点击,选择属性
环境变量设置好后,永久生效的,后面生成汇编不需要再次设置!
Mac 电脑无需设置环境变量,直接进行下面操作即可。
首先完成C代码的编写,如下图所示
然后再终端窗口中输人 gcc -S- fverbose-asm main.c就可以生成汇编文件 main.s,如下图所示
因为 CLion 看汇编代码没有颜色,因此大家阅读汇编代码,通过《VScode 安装流程》安装VScode 及 asm 插件后,来看代码高亮后效果.
VS 生成汇编的步骤(针对部分同学使用的 VS,没有安装 CLion 的如何操作)1、如图1所示,右键项目,点击属性。
如果2所示,选择输出文件,汇编程序输出选择程序集、机器码和源代码
VS 的汇编后缀是 xx.cod
二、汇编常用指令
1、相关寄存器
除 EBP和 ESP外,其他几个寄存器的用途是比较任意的,也就是什么都可以存。
2、常用指令
汇编指令通常可以分为数据传送指令、逻辑计算指令和控制流指令,下面以Imtel格式为例(考研考的就是 Intel 的汇编),介绍一些重要的指令。以下用于操作数的标记分别表示寄存器、内存和常数。
- <reg>:表示任意寄存器,若其后带有数字,则指定其位数,如<reg32>表示32位寄存器(eax、ebx、ecx、edx、esi、edi、esp或ebp);<reg16>表示16 位寄存器(ax、bx、cx或dx);<reg8>表示8位寄存器(ah、al、bh、bl、ch、cl、dh、dl)。
- <mem>:表示内存地址(如[eax]、[var+4]或 dword ptr [eax+ebx])。
- <con>:表示8位、16位或32位常数。<con8>表示8位常数:<con16>表示16位常数;<con32>表示32 位常数。(也称为立即数)
(1)数据传送指令
1)mov 指令
将第二个操作数(寄存器的内容、内存中的内容或常数值)复制到第个操作数(寄存器或内存),但不能用于直接从内存复制到内存.其语法如下:
- mov <reg>,<reg>
- mov <reg>,<mem>
- mov <mem>,<reg>
- mov <reg>,<con>
- mov <mem>,<con>
举例:
- mov eax,ebx #将 ebx值复制到 eax
- mov byte ptr [var] 5 #将5保存到var值指示的内存地址的一字节中
2)push 指令
将操作数压人内存的栈,常用于函数调用。ESP是栈顶,压栈前先将ESP值减4(栈增长方向与内存地址增长方向相反),然后将操作数压人ESP指示的地址,其语法如下:
push <reg32>
push <mem>
push <con32>
举例(注意,栈中元素固定为32位):
push eax
:将eax的值压栈。
push [var]
:将var所指示的内存地址的4字节值压栈。
3)pop
指令
与push
指令相反,pop
指令执行的是出栈工作,出栈前先将ESP指示的地址中的内容出栈,然后将ESP值加4。其语法如下:
pop <reg>
pop <mem>
举例:
pop edi
:弹出栈顶元素送到edi。
pop [ebx]
:弹出栈顶元素送到ebx所指示的内存地址的4字节中。
(2)算术和逻辑运算指令
1)add/sub
指令
add
指令将两个操作数相加,相加的结果保存到第一个操作数中;sub
指令用于两个操作数相减,相减的结果保存到第一个操作数中。它们的语法如下:
add <reg>,<reg>
add <reg>,<mem>
add <mem>,<reg>
add <reg>,<con>
add <mem>,<con>
【或】
sub <reg>,<reg>
sub <reg>,<mem>
sub <mem>,<reg>
sub <reg>,<con>
sub <mem>,<con>
举例:
sub eax,10 #
eax <-eax - 10
add byte ptr [var],10 #
10与var所指示的内存地址的一字节值相加,并将结果保存在var所指示的内存地址的字节中。
2)inc/dec
指令
inc
、dec
指令分别表示将操作数自加1、自减1。它们的语法如下:
inc <reg>
inc <mem>
【或】
dec <reg>
dec <mem>
举例:
dec eax
:eax的值自减1。
inc dword ptr [var]
:var所指示的内存地址的4字节值自加1。
3)imul
指令
带符号整数乘法指令,有两种格式:①两个操作数,将两个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器;②三个操作数,将第二个和第三个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器。其语法如下:
imul <reg32>,<reg32>
imul <reg32>,<mem>
imul <reg32>,<reg32>,<con>
imul <reg32>,<mem>,<con>
举例:
imul eax,[var]
:eax = eax × [var]。
imul esi,edi,25
:esi = edi × 25。
操作结果可能溢出,则编译器置溢出标志OF=1,以使CPU调出溢出异常处理程序。
4)idiv
指令
带符号整数除法指令,它只有一个操作数,即除数,而被除数则为edx:eax中的内容(64位整数),操作结果有两部分:商和余数,商送到eax,余数则送到edx。其语法如下:
idiv <reg32>
idiv <mem>
举例:
idiv ebx
idiv dword ptr [var]
5)and/or/xor
指令
and
、or
、xor
指令分别是按位与、按位或、按位异或操作指令,用于操作数的位操作(按位与,按位或,异或),操作结果放在第一个操作数中。它们的语法如下:
and <reg>,<reg>
and <reg>,<mem>
and <mem>,<reg>
and <reg>,<con>
and <mem>,<con>
【或】
or <reg>,<reg>
or <reg>,<mem>
or <mem>,<reg>
or <reg>,<con>
or <mem>,<con>
【或】
xor <reg>,<reg>
xor <reg>,<mem>
xor <mem>,<reg>
xor <reg>,<con>
xor <mem>,<con>
举例:
and eax,0xF #
将eax中的前28位全部置为0,最后4位保持不变。
xor edx,edx #
将edx中的内容置为0。
6)not
指令
位翻转指令,将操作数中的每一位翻转,即0→1、1→0。其语法如下:
not <reg>
not <mem>
举例:
not byte ptr [var]
:将var所指示的内存地址的一字节的所有位翻转。
7)neg
指令
取负指令。其语法如下:
neg <reg>
neg <mem>
举例:
neg eax
:eax = -eax。
8)shl/shr
指令
逻辑移位指令,shl
为逻辑左移、shr
为逻辑右移,第一个操作数表示被操作数,第二个操作数指示移位的位数。它们的语法如下:
shl <reg>,<con8>
shr <reg>,<con8>
shl <mem>,<con8>
shr <mem>,<con8>
【或】
shl <reg>,cl
shr <reg>,cl
shl <mem>,cl
shr <mem>,cl
举例:
shl eax,1
:将eax值左移1位,相当于乘以2。
shr ebx,cl
:将ebx值右移n位(n为cl中的值),相当于除以2。
9)lea
指令。
地址传送指令,将有效地址传送到指定的寄存器。
举例:
lea eax, DWORD PTR _arrs[ebp]
:将DWORD PTR _arrs[ebp]对应空间的内存地址值放到eax中。
(3)控制流指令
x86处理器维持着一个指示当前执行指令的指令指针(IP),当一条指令执行后,此指针自动指向下一条指令。IP寄存器不能直接操作,但可以用控制流指令更新。通常用标签(label)指示程序中的指令地址,在x86汇编代码中,可在任何指令前加入标签。例如:
mov esi, [ebp+8]
begin: xor ecx,ecx
mov eax, [esi]
这样就用begin
(begin
代表标签名,可以为别的名字)指示了第二条指令,控制流指令通过标签就可以实现程序指令的跳转。
1)jmp
指令
jmp
指令控制IP转移到label所指示的地址(从label中取出指令执行)。其语法如下:
jmp <label>
举例:
jmp begin
:跳转到begin
标记的指令执行。
2)jcondition
指令
条件转移指令,依据CPU状态字中的一系列条件状态转移。CPU状态字中包括指示最后一个算术运算结果是否为0,运算结果是否为负数等。其语法如下:
je <label>
(jump when equal)
jne <label>
(jump when not equal)
jz <label>
(jump when last result was zero)
jg <label>
(jump when greater than)
jge <label>
(jump when greater than or equal to)
jl <label>
(jump when less than)
jle <label>
(jump when less than or equal to)
举例:
cmp eax, ebx
jle done
:如果eax的值小于等于ebx的值,跳转到done
指示的指令执行,否则执行下一条指令。
3)cmp/test
指令
cmp
指令用于比较两个操作数的值,test
指令对两个操作数进行逐位与运算,这两类指令都不保存操作结果,仅根据运算结果设置CPU状态字中的条件码。其语法如下:
cmp <reg>,<reg>
cmp <reg>,<mem>
cmp <mem>,<reg>
cmp <reg>,<con>
test <reg>,<reg>
test <reg>,<mem>
test <mem>,<reg>
test <reg>,<con>
举例:
cmp dword ptr [var], 10
:将var所指示的内存地址的4字节内容,与10比较。
jne loop
:如果不相等则跳转到loop
处执行。
test eax,eax
:测试eax是否为零。
jz xxxx
:如果为零则跳转到xxxx
处执行。
4)call/ret
指令
分别用于实现子程序(过程、函数等)的调用及返回。其语法如下:
call <label>
ret
call
指令首先将当前执行指令地址入栈,然后无条件转移到由标签指示的指令。与其他简单的跳转指令不同,call
指令保存调用之前的地址信息(当call
指令结束后,返回调用之前的地址)。ret
指令实现子程序的返回机制,ret
指令弹出栈中保存的指令地址,然后无条件跳转到保存的指令地址执行。call
和ret
是程序(函数)调用中最关键的两条指令。
3、条件码
编译器通过条件码(标志位)设置指令和各类转移指令来实现程序中的选择结构语句。3.2中的控制流指令中的Jcondition
条件转移指令,就是根据条件码来实现跳转。
(1)条件码(标志位)
除了整数寄存器,CPU还维护着一组条件码(标志位)寄存器,它们描述了最近的算术或逻辑运算操作的属性。可以检测这些寄存器来执行条件分支指令,最常用的条件码有:
CF:进(借)位标志,最近无符号整数加运算后的进位,或者无符号正数减法运算时有借位情况。有进(借)位CF=1;否则CF=0。例如:(unsigned)t < (unsigned)a,因为判断大小是相减,如果t是3,a是4,则CF=1。
ZF:零标志,最近的操作的运算结果是否为0。若结果为0,ZF=1;否则ZF=0。例如:(t=0)。
SF:符号标志,最近的带符号数运算结果的符号,负数时,SF=1;否则SF=0。
OF:溢出标志,最近带符号数运算的结果是否溢出,若溢出,OF=1;否则OF=0。
可见,OF和SF对无符号数运算来说没有意义,而CF对带符号数运算来说没有意义。ZF标志位既可以用于有符号数,也可以用于无符号数。
如何判断溢出:
简单来说,正数相加变负数为溢出,负数相加变正数为溢出。但考研通常会给出十六进制的两个数来考溢出,判断方法如下(针对有符号数):
数据位高位进位,符号位高位未进位,溢出(两个大正数相加,例如120和25)。
数据位高位未进位,符号位高位进位,溢出(两个小负数相加,例如-128和-113)。
数据位高位进位,符号位高位进位,不溢出(两个大负数相加,例如-1和-1)。
数据位高位未进位,符号位未进位,不溢出(两个小正数相加,例如2和3)。
简单一句话就是数据位高位和符号位高位进位不一样的时候会溢出。
如果是针对无符号数,判断方法如下:
数据位高位进位,进位发生,溢出(两个大正数相加,例如120和140)。
数据位高位未进位,进位未发生,无溢出(两个小正数相加,例如2和3)。
常见的算术逻辑运算指令(如add
、sub
、imul
、or
、and
、shl
、inc
、dec
、not
、sal
等)会设置条件码。但有两类指令只设置条件码而不改变任何其他寄存器,即cmp
和test
指令。cmp
指令和sub
指令的行为一样,test
指令与and指令的行为类似,但它们只设置条件码,而不更新目的寄存器。(在后面 408的组成原理课程中,将进行详细的讲解,这里先记住哪些指令会设置条件码即可)
【注意】乘法溢出后,可以跳转到“溢出自陷指令",例如imnt 0x2e 就是一条自陷指令,但是考研只需要掌握溢出,可以跳转到“溢出自陷指令”即可,不需要记自陷指令有哪些.
三、各种变量赋值汇编
我们针对整型、整型数组、整型指针变量的赋值(浮点与字符等价的),对应的汇编进行解析,首先我们编写下面的C代码:
#include<stdio.h>
int main() {
int arr[3] = {1, 2, 3};
int *p;
int i = 5;
int j = 10;
i = arr[2];
p = arr;
printf("=%d\n", i);
return 0;
}
然后根据上一小节生成汇编的方法,在终端窗口输入
gcc -m32 -masm=intel -S -fverbose-asm main.c
(苹果的M1电脑如果不可用,可以直接看课件我的汇编即可,也可以去掉-masm=intel
,这样汇编和考研大题格式有差异);
如下图所示,得到汇编文件main.s
。
接下来我们来分析转换后的汇编代码,首先#
号代表注释,我们从main
标签位置开始看。我们的C代码在让CPU去运行时,其实所有的变量名都已经消失了,实际是数据从一个空间,拿到另一个空间的过程。
栈内变量先定义在低地址还是高地址,取决于操作系统与CPU的组合,你的可能和我的不一样,因此不用去研究,没有意义(也不属于考研大纲范围)。
我们访问所有变量的空间都是通过栈指针(esp
时刻都存着栈指针,也可以称为栈顶指针)的偏移,来获取对应变量内存空间的数据的。
.text
.def _main; scl 2; type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "=%d\n\0"
.text
.globl _main
_main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 48
# main.c:4: int arr[3] = {1, 2, 3};
mov DWORD PTR [esp+24], 1
mov DWORD PTR [esp+28], 2
mov DWORD PTR [esp+32], 3
# main.c:6: int i = 5;
mov DWORD PTR [esp+44], 5
# main.c:7: int j = 10;
mov DWORD PTR [esp+40], 10
# main.c:8: i = arr[2];
mov eax, DWORD PTR [esp+32]
mov DWORD PTR [esp+44], eax
# main.c:9: p = arr;
lea eax, [esp+24]
mov DWORD PTR [esp+36], eax
# main.c:10: printf("=%d\n", i);
mov eax, DWORD PTR [esp+44]
mov DWORD PTR [esp+4], eax
mov DWORD PTR [esp], OFFSET FLAT:LC0
call _printf
# main.c:11: return 0;
mov eax, 0
# main.c:12:
leave
ret
.ident "GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
.def printf; scl 2; type 32; .endef
大家转的汇编的偏移值可能和我这里有一些差异,这个没关系,这一节大家理解变量赋值的汇编指令及原理即可,主要掌握的指令是mov
,还有lea
,还有PTR
,下面是PTR
介绍。
PTR
(Pointer,即指针)的缩写。汇编里面PTR
是规定的字(既保留字,是用来临时指定类型的)。
(可以理解为,PTR
是临时的类型转换,相当于C语言中的强制类型转换)
如:
mov ax, bx
:是把BX寄存器“里”的值赋予AX,由于二者都是寄存器,长度已定(word型),所以没有必要加WORD PTR
。
mov ax, word ptr [bx]
:是把内存地址等于“BX寄存器的值”的地方所存放的数据,赋予AX,由于只是给出一个内存地址,不知道希望赋予AX的是byte
还是word
,所以可以用word ptr
明确指出。如果不用(即mov ax, [bx]
),则在8086中默认传递一个字(两个字节)给AX。
Intel中的:
dword ptr
:长字(4字节)
word ptr
:双字(2字节)
byte ptr
:一字节
四、选择与循环汇编
#include <stdio.h>
int main() {
int i = 5;
int j = 10;
if (i < j)
printf("i is small\n");
for (i = 0; i < 5; i++)
printf("this is loop\n");
return 0;
}
然后根据之前小节生成汇编的方法,在终端窗口输入gcc -m32 -masm=intel -S -fverbose-asm main.c
,如下图所示,得到汇编文件main.s
。
接下来我们来分析转换后的汇编代码,首先#
号代表注释,我们从main
标签位置开始看即可。我们的C代码在让CPU去运行时,其实所有的变量名都已经消失了,实际是数据从一个空间,拿到另一个空间的过程。
栈内变量先定义在低地址还是高地址,取决于操作系统与CPU的组合,你的可能和我的不一样,因此不用去研究,没有意义(也不属于考研大纲范围)。我们访问所有变量的空间都是通过栈指针(esp
时刻都存着栈指针,也可以称为栈顶指针)的偏移,来获取对应变量内存空间的数据的。
.text # 这里是文字常量区,放了我们的字符串常量,LC0和LC1分别是我们要用到的两个字符串常量的起始地址
.def _main; scl 2; type 32; .endef
.section .rdata, "dr"
LC0:
.ascii "i is small\n\0"
LC1:
.ascii "this is loop\n\0"
.text
.globl _main
_main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 32
# main.c:5: int i = 5;
mov DWORD PTR [esp+28], 5 # 把常量5放入栈指针(esp寄存器存的栈指针)偏移28个字节的位置,这个位置是变量i的空间
# main.c:6: int j = 10;
mov DWORD PTR [esp+24], 10 # 把常量10放入栈指针(esp寄存器存的栈指针)偏移24个字节的位置,这个位置是变量j的空间
# main.c:7: if (i < j)
mov eax, DWORD PTR [esp+28] # 把栈指针(esp寄存器存的栈指针)偏移28个字节的位置内的值,放入eax寄存器
cmp eax, DWORD PTR [esp+24] # 比较eax寄存器内的值和栈指针偏移24个字节位置的值的大小,拿eax寄存器值减去DWORD PTR [esp+24],然后设置条件码
jge L2 # 如果eax寄存器大于等于DWORD PTR [esp+24],那么跳转到L2,否则直接往下执行,jge是根据条件码ZF和SF来判断的
# main.c:9: printf("i is small\n");
mov DWORD PTR [esp], OFFSET FLAT:LC0 # 把LC0(也就是上面那个字符串)的地址,放到寄存器栈指针指向的内存位置
call _puts
L2:
# main.c:11: for (i = 0; i < 5; i++)
mov DWORD PTR [esp+28], 0 # 把常量0放入栈指针(esp寄存器存的栈指针)偏移28个字节的位置,这个位置是变量i的空间
jmp L3 # 无条件跳转到L3
L4:
# main.c:13: printf("this is loop\n");
mov DWORD PTR [esp], OFFSET FLAT:LC1 # 把LC1(也就是上面那个字符串)的地址,放到寄存器栈指针指向的内存位置
call _puts
# main.c:11: for (i = 0; i < 5; i++)
add DWORD PTR [esp+28], 1
L3:
# main.c:11: for (i = 0; i < 5; i++)
cmp DWORD PTR [esp+28], 4 # 比较栈指针偏移28个字节位置的值与4的大小,拿DWORD PTR [esp+28]减去4,然后设置条件码
jle L4 # 如果小于等于4,跳转到L4
# main.c:15: return 0;
mov eax, 0
# main.c:16:
leave
ret
这一节大家理解选择、循环的汇编指令及原理即可,主要掌握的指令是cmp
、jge
、jmp
、jle
等,以及了解一下字符串常量是存在文字常量区的。
五、函数调用汇编
这一小节我们针对C语言的指针的间接访问,函数调用,返回值,对应的汇编进行解析,首先我们编写下面C代码:
#include <stdio.h>
int add(int a, int b) {
int ret;
ret = a + b;
return ret;
}
int main() {
int a, b, ret;
int *p;
p = &a;
b = *p + 2;
ret = add(a, b);
printf("add result=%d\n", ret);
return 0;
}
然后根据之前小节生成汇编的方法,在终端窗口输入gcc -m32 -masm=intel -S -fverbose-asm main.c
,如下图所示,得到汇编文件main.s
。
接下来我们来分析转换后的汇编代码,首先#
号代表注释,我们从main
标签位置开始看即可。我们的C代码在让CPU去运行时,其实所有的变量名都已经消失了,实际是数据从一个空间,拿到另一个空间的过程。
栈内变量先定义在低地址还是高地址,取决于操作系统与CPU的组合,你的可能和我的不一样,因此不用去研究,没有意义(也不属于考研大纲范围)。我们访问所有变量的空间都是通过栈指针(esp
时刻都存着栈指针,也可以称为栈顶指针)的偏移,来获取对应变量内存空间的数据的。
函数调用的汇编原理解析
先必须明确的一点是,函数栈是向下生长的。所谓向下生长,是指从内存高地址向低地址的路径延伸。于是,栈就有栈底和栈顶,栈顶的地址要比栈底的低。
对于x86体系的CPU而言,寄存器ebp
可称为基指针或基址指针(base pointer),寄存器esp
可称为栈指针(stack pointer)。这里需要说明的几点如下:
-
ebp
在未改变之前始终指向栈底的开始,所以ebp
的用途是在堆栈中寻址(寻址的作用会在下面详细介绍)。 -
esp
会随着数据的入栈和出栈而移动,即esp
始终指向栈顶。
如下图2所示,假设函数A调用函数B,称函数A为调用者,称函数B为被调用者,则函数调用过程可以描述如下:
-
首先将调用者(A)的堆栈的基址(
ebp
)入栈,以保存之前任务的信息。 -
然后将调用者(A)的栈顶指针(
esp
)的值赋给ebp
,作为新的基址(即被调用者B的栈底)。原有函数的栈顶,是新函数的栈底。 -
再后在这个基址(被调用者B的栈底)上开辟(一般用
sub
指令)相应的空间用作被调用者B的栈空间。 -
函数B返回后,当前栈顶的
ebp
恢复为调用者A的栈顶(esp
),使栈顶恢复函数B被调用前的位置;然后调用者A从恢复后的栈顶弹出之前的ebp
值(因为这个值在函数调用前一步被压入堆栈)。
图2 函数调用过程
高地址
A函数的ebp
A函数的栈空间
A函数的esp
返回地址
push ebp
后,A的ebp
保存在这里,esp
指向这里
B函数的ebp
B函数的栈空间
B函数的esp
低地址
图3 main调用add的汇编原理图
高地址
main函数的ebp
main函数的栈空间
main函数的esp
返回地址
push ebp
后,main的ebp
保存在这里,esp
指向这里
add函数的ebp
add函数的栈空间
add函数的esp
低地址
.text
.globl _add
.def _add; scl 2; type 32; .endef
_add: # add函数的入口,这里阅读需要结合咱们的函数调用图(图3)
push ebp # 把原有函数,也就是main函数的栈基指针压栈,压栈是把ebp的值保存到内存上,位置就是esp指向的位置
mov ebp, esp # 把main的栈顶指针esp,作为add函数的栈基指针ebp
sub esp, 16 # 由于add函数自身要使用栈空间,把esp减去16,表示add函数的栈空间大小是16个字节
# main.c:5: ret = a + b;
mov edx, DWORD PTR [ebp+8] # 拿到实参,也就是a的值,放入edx
mov eax, DWORD PTR [ebp+12] # 拿到实参,也就是b的值,放入eax
add eax, edx # 将eax和edx相加
mov DWORD PTR [ebp-4], eax # 把eax,也就是ret的值,放入ebp减4个字节位置
# main.c:6: return ret;
mov eax, DWORD PTR [ebp-4] # 把ebp-4位置的值(ret)放入eax
# main.c:7:
leave
ret # 函数返回,弹出压栈的指令返回地址,回到main函数执行
.text
.globl _add
.def _add; scl 2; type 32; .endef
_add: # add函数的入口,这里阅读需要结合咱们的函数调用图(图3)
push ebp # 把原有函数,也就是main函数的栈基指针压栈,压栈是把ebp的值保存到内存上,位置就是esp指向的位置
mov ebp, esp # 把main的栈顶指针esp,作为add函数的栈基指针ebp
sub esp, 16 # 由于add函数自身要使用栈空间,把esp减去16,表示add函数的栈空间大小是16个字节
# main.c:5: ret = a + b;
mov edx, DWORD PTR [ebp+8] # 拿到实参,也就是a的值,放入edx
mov eax, DWORD PTR [ebp+12] # 拿到实参,也就是b的值,放入eax
add eax, edx # 将eax和edx相加
mov DWORD PTR [ebp-4], eax # 把eax,也就是ret的值,放入ebp减4个字节位置
# main.c:6: return ret;
mov eax, DWORD PTR [ebp-4] # 把ebp-4位置的值(ret)放入eax
# main.c:7:
leave
ret # 函数返回,弹出压栈的指令返回地址,回到main函数执行
.def main; scl 2; type 32; .endef
.section .rdata, "dr"
LC0:
.ascii "add result=%d\12\0"
.text
.globl _main
_main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 32
# main.c:12: a = 5;
mov DWORD PTR [esp+16], 5 # 把常量5放入栈指针(esp寄存器存的栈指针)偏移16个字节的位置,这个位置是变量a的空间
# main.c:13: p = &a;
lea eax, [esp+16] # 这里用的lea,和mov不一样,是将esp+16位置的地址,放到eax寄存器中
mov DWORD PTR [esp+28], eax # 把eax中的值放到栈指针偏移28字节位置,也就是指针变量p中
# main.c:14: b = *p + 2;
mov eax, DWORD PTR [esp+28] # 把指针偏移28字节位置,也就是指针变量p的值,放到eax寄存器
mov eax, DWORD PTR [eax] # 把eax寄存器中的值作为地址,去内存访问到对应的数据,放入eax中
add eax, 2 # 对eax中的值加2,结果还是在eax中
mov DWORD PTR [esp+24], eax # 把eax中的值放到栈指针偏移24字节位置,也就是变量b中
# main.c:15: ret = add(a, b);
mov eax, DWORD PTR [esp+16] # 把栈指针偏移16字节位置,也就是变量a的值,放到eax寄存器
mov edx, DWORD PTR [esp+24] # 把栈指针偏移24字节位置,也就是变量b的值,放到edx寄存器
mov DWORD PTR [esp+4], edx # 把edx中的值(变量b),放到栈指针偏移4字节的内存位置
mov DWORD PTR [esp], eax # 把eax中的值(变量a),放到栈指针指向的内存位置
call _add # 调用add函数
mov DWORD PTR [esp+20], eax # 把add函数返回的值放入栈指针偏移20字节位置,也就是变量ret中
# main.c:16: printf("add result=%d\n", ret);
mov eax, DWORD PTR [esp+20] # 把栈指针偏移20字节位置的值(ret)放入eax
mov DWORD PTR [esp+4], eax # 把eax中的值(ret)放到栈指针偏移4字节位置
mov DWORD PTR [esp], OFFSET FLAT:LC0 # 把LC0(格式化字符串)的地址放入栈指针指向的内存位置
call _printf
# main.c:17: return 0;
mov eax, 0
# main.c:18:
leave
ret
这一节大家理解指针变量的间接访问原理