CSAPP:程序的结构和执行

CSAPP:程序的结构和执行

一、计算机系统概述

学习方法

  1. 把书读薄,参照书籍的目录列出知识框架(做成一份重难点笔记,对照着章节目录时刻明确现在学的知识是什么地位,通过写书上的例子来学习,每看几段就要写自己的总结复述,提高读书的效率)
  2. 把书读厚,看一集视频,看一章的书,反汇编之前写的C++基本语法代码,边学边玩C++(查找相关的书籍和文章,超越书本理解,独立完成实验与习题,一定要实践每个环节)
  3. 把书读薄(画思维导图,压缩本书的内容)
  4. 输出,学以致用:优化程序性能(复述内容,重做实验)

二、数据的机器级表示与处理

信息的存储

大多数计算机使用8位的块,作为最小可寻址的内存单位,机器级程序将内存视为大数组,称为虚拟内存,数组的元素是由一个个字节组成的,每个字节为8个比特(bit),每个字节都由唯一的数字表示,称为地址(adress),所有地址的集合称为虚拟地址空间,虚拟地址空间是各种特殊硬件和操作系统结合起来共同实现的,目的是为程序提供逻辑上看是一个统一的大字节数组

程序对象:数据、指令、控制信息

它们的存放与管理都是在虚拟地址空间完成的,例如:

C指针无论指向一个整数、一个结构或者其他程序对象,都是某个存储块的第一个字节的虚拟地址

C编译器还把每个指针和类型联系在一起

C编译器维护这些信息,但是实际机器级程序不包含关于数据类型的信息,每个程序对象就是一堆字节序列

进位计数制、字节(Byte)

一字节由8个位组成,最终决定一个字节大小是8个比特的,是1964年出现的大名鼎鼎的 IBM System/360,当时IBM为System/360设计了一套8位EBCDIC编码,涵盖了数字、大小写字母和大部分常用符号,同时又兼容广泛用于打孔卡的6位BCDIC编码,System/360很成功,也奠定了字符存储单位采用8位长度的基础,这就是1字节=8位的由来。

一个字节在10进制的值域是:0—255

使用二进制太冗长,故使用16进制,一个字节在16进制的值域是:00—FF

16进制转换为2进制技巧:记住A=10;C=12;F=16

展开16进制的每个数字,将它们一一转换为2进制

2进制转换为16进制:

从右向左,以4位为一组将二进制数转换为对应的16进制数,最左端不够4位的要补0

10进制转换为16进制,使用辗转相除法(欧几里得算法):

/*
欧几里得算法:辗转求余
原理: gcd(a,b)=gcd(b,a mod b)
当b为0时,两数的最大公约数即为a
getchar()会接受前一个scanf的回车符
*/
#include<stdio.h>
unsigned int MaxCommonFactor(int a,int b)
{
    if(b<=0)
      return a;
    return MaxCommonFactor(b,a%b);
}
   
unsigned int Gcd(unsigned int M,unsigned int N)
{
    unsigned int Rem;
    while(N > 0)
    {
        Rem = M % N;
        M = N;
        N = Rem;
    }
    return M;
}
int main(void)
{
    int a,b;
    scanf("%d %d",&a,&b);
    printf("the greatest common factor of %d and %d is ",a,b);
    printf("%d\n",Gcd(a,b));
    printf("recursion:%d\n",MaxCommonFactor(a,b));
    return 0;
}

16进制转换为10进制:按位展开相乘

字长(Word size)和位模式

  1. 所有地址的集合称为虚拟地址空间
  2. 字长用于表明指针数据的标称大小
  3. 字长决定了虚拟地址空间的最大值

下表显示了C语言不同数据类型在64位机器与32位机器上所占字节的大小:

由上表可以看出,很多数据类型都占用了多个字节空间,对于我们需要存储的数据,我们需要搞清楚该数据的地址是什么,以及数据在内存中如何排布

例题:字长用于表明指针数据的标称大小

/*32位机器上,指针占4个字节,64位机器上,指针占8个字节(选择上面的x86或者x64会有不同的结果)*/
cout << "ptr的内存空间大小为:" << sizeof(ptr) << endl;

例题:int的存储方法

int占4个字节,有两种排布方法:

大端法

最高有效字节存储在最前面,也就是低地址处,4个2进制数=1个16进制数,即1个16进制数占4位;2个16进制数占8位,也即1字节

小端法

最低有效字节存储在最前面

应用:

  1. 网络应用程序代码编写,确保字节序列在发送方和接受方的含义相同

  2. 阅读机器级代码(反汇编)

  3. 使用强制类型转换或者联合体,以下代码可以查看程序对象的字节表示:

    #include<stdio.h>
    #include
    using namespace std;
    typedef unsigned char* byte_pointer;

    void show_byte(byte_pointer start, size_t len) {
    size_t i; //字节数指定为size_t
    for (i = 0; i < len; i++) {
    printf("%.2x", start[i]); //表示程序按照“至少两个数字的16进制格式输出”
    }
    cout << endl;
    }

    void show_int(int x) {
    show_byte((byte_pointer)&x, sizeof(int));
    }

    void show_float(float x) {
    show_byte((byte_pointer)&x, sizeof(float));
    }

    void show_pointer(void* x) {
    show_byte((byte_pointer)&x, sizeof(void*));
    }

    void test_show_bytes(int val) {
    int ival = val;
    float fval = (float)ival;
    int* pval = &ival;

     show_int(ival);
     show_float(fval);
     show_pointer(pval);
    

    }

    int main() {
    int num = 10;
    test_show_bytes(num);
    system(“pause”);
    return 0;
    }

C的位运算

例题:不借助中间变量,交换两个数的值

原理:对于任意位向量a,有a^a=0(异或:相同为0,不同为1)

#include<stdio.h>
#include <iostream>
using namespace std;
typedef unsigned char* byte_pointer;

//采用异或运算
void inplace_swap(int* x, int* y) {
	*y = *x ^ *y;
	*x = *x ^ *y;
	*y = *x ^ *y;
}

int main() {
	int x = 10;
	int y = 20;
	cout << "x的值为:" << x << endl;
	cout << "y的值为:" << y << endl;
	inplace_swap(&x, &y);
	cout << "x的值为:" << x << endl;
	cout << "y的值为:" << y << endl;

	system("pause");
	return 0;
}

例题:利用“不借助中间变量,交换两个数的值”,逆置数组(只有当数组长度为偶数时成立)

#include<stdio.h>
#include <iostream>
using namespace std;
typedef unsigned char* byte_pointer;

//采用异或运算交换两数的位置
void inplace_swap(int* x, int* y) {
	*y = *x ^ *y;
	*x = *x ^ *y;
	*y = *x ^ *y;
}

//逆置数组(只有当数组长度为偶数时成立)
void reverse_array(int a[], int cnt) {
	int first, last;
	for (first = 0, last = cnt - 1;first<=last; first++, last--) {
		inplace_swap(&a[first], &a[last]);
	}
}

int main() {
	int x = 10;
	int y = 20;
	cout << "x的值为:" << x << endl;
	cout << "y的值为:" << y << endl;
	inplace_swap(&x, &y);
	cout << "x的值为:" << x << endl;
	cout << "y的值为:" << y << endl;
	cout << "---------------------------------------" << endl;

	int arr[4] = { 1,2,3,4 };
	cout << "交换前" << endl;
	for (int i = 0; i < 4; i++) {
		cout << arr[i] << endl;
	}
	reverse_array(arr, 4);
	cout << "交换后" << endl;
	for (int i = 0; i < 4; i++) {
		cout << arr[i] << endl;
	}

	system("pause");
	return 0;
}

分析与改进:奇数长度的数组也成立的代码

由于a^a=0,当原代码中数组长度为奇数时,reverse_array最后一次循环中,变量first和last均为最中间的数,所以二者异或结果为0

修改:去掉 “=”

//逆置数组(当数组长度为奇数时也成立)
void reverse_array(int a[], int cnt) {
	int first, last;
	for (first = 0, last = cnt - 1;first<last; first++, last--) {
		inplace_swap(&a[first], &a[last]);
	}
}

C语言的移位运算

左移(左移一位就是丢弃最高的一位,并且在右端补一个0)

逻辑右移(类似左移,只是方向不一样)

算数右移

  • 若最高位为0,则在左端填充0
    若最高位为1,则在左端填充1

整数的表示

无符号数

假设有一个整数的数据类型有w位,用向量x表示,向量中每个元素表示一个二进制位(0,1);
用函数B2U(binary to unsigned)表示一个长度为w的0,1串如何映射到无符号数,这种方式(原码表示法)只能表示非负数

有符号数

采用补码,补码的本质:最高位表示负权重,其余位表示正权重

图形化表示方法:

无符号数的最大值

所有位全为1时,无符号数最大

有符号数的最大值

对于有符号正数,当符号位为0,其余位均为1时,表示有符号数最大值

有符号数的最小值

对于有符号负数,当符号位为1,其余位均为0时,表示有符号数最小值

分析:结合上面的图形化表示补码,当只存在负权重,而不存在正权重的时候,表示为最小负数

有符号数和无符号数之间的转换

大多数情况下,转换方式为:位模式不变,但是解释这些位的方式改变了

	//强转前后的机器数不变,改变的是解释这些位的方式

	cout << "2的32次方-1(全1码)" << endl;
	unsigned u1 = 4294967295u;
	int tu = (int)u1;

	cout << "作为无符号数解释,补码同原码,全1码相当于2的32次方-1,故v=" << u1 << endl;
	cout << "作为有符号数解释,补码的最高位为负权重,故uv=" << tu << endl;


	int x = -1;
	unsigned u2 = 2147483648;		//2的31次方(1后面31个0)

	printf("x = %u = %d\n", x, x);
	printf("u2 = %u = %d\n", u2, u2);

//当执行运算时,若一个操作数为有符号,而另一个为带符号,则会将有符号数隐式的转换为无符号数,并假设这两个数都是非负数
//注意,类似1,2,3,0,-1,(int)1231u这样的数为有符号数;
//类似(unsigned)-1,-3u这样的数为无符号数

cout <<"0 == 0u的结果为:"<< (0 == 0u) << endl;
cout << "-1<0的结果为:" << (-1 < 0) << endl;
//-1的机器码(全1)会被当做无符号数解释,所以结果反常
cout << "-1<0u的结果为:" << (-1 < 0u) << endl;		

整数的运算

无符号数加法

有符号数加法

正溢出

负溢出

总结:

浮点数的表示

类比十进制:

如下图所示:

IEEE对于浮点数的格式要求:

例如C语言中float类型的变量占4个字节,32比特位,这32比特位被划分为3个字段来解释

类似于科学计数法:

S:符号位,s=0:正数;s=1:负数
E:阶码——阶码的值决定这个数属于哪一类
M:尾数——即小数字段

浮点数分为3类:阶码的值决定这个数属于哪一类

规格化的值——阶码的值不全为0或全为1

阶码e最小为1,最大为254,用e表示这个8位二进制数,注意:阶码的值不等于e(8位二进制)所表示的值,而是e的值减去一个偏置量,偏置量大小于阶码字段的位数相关

当表示单精度的值:阶码字段长为8,偏置量为127
当表示双精度的值:阶码字段长为11,偏置量为1023

小数(尾数)字段M定义为1+f

非规格化的值——阶码的值全为0

当符号位s=0;阶码全为0;小数字段全为0——正0
当符号位s=1;阶码全为0;小数字段全为0——负0

非规格化的值另一个用途就是表示非常接近0的数

阶码字段全为0,阶码E=1-偏置

特殊值——阶码的值全为1

无穷大&无穷小

阶码全为1;小数字段全为0——无穷大(正负无穷大)

不是一个数(NaN)

阶码全为1;小数字段不为0——不是一个数(NaN)

浮点数的运算

三、程序的转换及机器级表示

预处理:插入所有#include,#include"函数的分文件编写.h",#define定义的文件

编译:产生汇编代码

汇编:产生二进制代码

链接:目标文件与库函数代码合并

程序的机器级表示

例题:理解C代码与汇编代码的对应关系是理解计算机程序执行的关键一步

注意:[]和()在汇编中类似于C中的解引用

int a1 = 10;
00FE26B8  mov         dword ptr [a1],0Ah  
//理解:a1(变量名)是一个地址,将10=0Ah存放在M[a1]的内存空间里
int b1 = 3;
00FE26BF  mov         dword ptr [b1],3  
//理解:同理,b1(变量名)是一个地址,将3存放在M[b1]的内存空间里
int c1 = a1 + b1;
00FE26C6  mov         eax,dword ptr [a1] 
//理解:将M[a1]的内容存放在eax寄存器里
00FE26C9  add         eax,dword ptr [b1]  
//理解:将M[b1]的内容与存放在eax寄存器里的内容(M[a1]的内容)相加,将结果存放在eax中
00FE26CC  mov         dword ptr [c1],eax  
//理解:将计算结果eax中保存的值存放到M[c1]的内存空间里

寄存器与数据传送指令

  1. 程序计数器PC:用%rip表示,表示将要执行的下一条指令在内存中的地址
  2. 整数寄存器:可以用来保存地址(对应C语言指针)或整数数据
  3. 条件码寄存器:保存算数、逻辑状态指令,用于实现if/while
  4. 向量寄存器:存放整数、浮点数值

寄存器

  • 寄存器就是你的口袋。身上只有那么几个,只装最常用或者马上要用的东西
  • 内存就是你的背包。有时候拿点什么放到口袋里,有时候从口袋里拿出点东西放在背包里。
  • 辅存就是你家里的抽屉。可以放很多东西,但存取不方便
  • 断电了就相当于你人没了,在家里复活,家里抽屉里东西还在,但包里口袋里的装备都爆没了

数据格式

访问信息

x86-64的CPU包含16个64位值的通用目的寄存器,用来存放整数数据和指针,名字以%r开头,mov的后缀大小与寄存器的大小要匹配

内存引用

逻辑上,内存抽象为一个字节数组,编译器会根据数组的类型来确定比例因子的值,例如:char-s=1,int-s=4,double-s=8

MOV指令

目的操作数是一个容器,存放源操作数内容,所以目的操作数不能为立即数;目的操作数和源操作数不能同时为地址,需要两条指令完成:

1、将内存源位置的数值加载到寄存器里

2、将该寄存器的值写入内存的目的位置

例题:C指针就是存放地址的变量

指针就是一个地址,解引用指针就是将指针放在一个寄存器里面,然后再内存引用中使用这个寄存器

long exchange(long* xp, long y) {
	long x = *xp;
	*xp = y;
	return x;
}

int main() {
	long a = 10;
	long b = 20;
	long c = exchange(&a, b);

	cout << "c=" << c << endl;

	system("pause");
	return 0;
}

    
//对于exchange(long* xp, long y)的部分反汇编结果:
long exchange(long* xp, long y) {
	long x = *xp;
003F2518  mov         eax,dword ptr [xp]  
//理解:将M[xp]中的内容(一个内存地址)放在eax寄存器里 
003F251B  mov         ecx,dword ptr [eax] 
//理解:将存放在eax中的地址解引用,对应内存空间里的值存放在ecx中 
003F251D  mov         dword ptr [x],ecx
//理解:将ecx中的值存放在M[x]的内存空间里
	*xp = y;
003F2520  mov         eax,dword ptr [xp]  
//理解:将M[xp]的内容放入eax寄存器中
003F2523  mov         ecx,dword ptr [y] 
//理解:将M[y]的内容放入ecx寄存器中    
003F2526  mov         dword ptr [eax],ecx 
//理解:将ecx中的值放入M[R[eax]]中
	return x;
003F2528  mov         eax,dword ptr [x] 
//理解:将存放在M[x]的内存空间里的值存放在eax中返回
}

例题:lea是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数

例如:
lea eax,[ebx+8]就是将ebx+8这个值直接赋给eax,而不是把ebx+8处的内存地址里的数据赋给eax。

而mov指令则恰恰相反

例如:
mov eax,[ebx+8]则是把内存地址为ebx+8处的数据赋给eax。

/*定义指针*/
	int a = 10;
00007FF61F8624EB  mov         dword ptr [a],0Ah  
//理解:将10=0Ah放在M[a]的内存空间里
	int b = 20;
00007FF61F8624F2  mov         dword ptr [b],14h
//理解:将20=14h放在M[b]的内存空间里
	int* ptr = &a;
00007FF61F8624F9  lea         rax,[a]  
//理解:将地址a直接复制给rax寄存器
00007FF61F8624FD  mov         qword ptr [ptr],rax  
//理解:将rax寄存器中的值复制给M[ptr]的内存空间
    
    
/*空指针*/
	//用途:初始化指针变量,空指针不能访问
	//注意:内存编号为0—255都是系统占用内存,不允许用户访问
	int* p1 = NULL;
00007FF61F8625CD  mov         qword ptr [p1],0  
//理解:将指针置位NULL,即将0值复制给M[p1]的内存空间

例题:const修饰指针的反汇编结果与上面一样

/*const修饰指针*/
    
//1. 常量指针(理解:常量的指针,既然是常量,当然值不可以修改)
	const int* p3 = &a;
00007FF61F8625D5  lea         rax,[a]  
00007FF61F8625D9  mov         qword ptr [p3],rax  
	//可以修改指针的指向,但是不能修改指向的值(*p = 20非法)
	p3 = &b;	//合法
00007FF61F8625E0  lea         rax,[b]  
00007FF61F8625E4  mov         qword ptr [p3],rax  

//2. 指针常量(理解:这个指针是一个常量,既然是常量,当然指向不可以修改)
	int* const p4 = &a;
00007FF61F8625EB  lea         rax,[a]  
00007FF61F8625EF  mov         qword ptr [p4],rax  
	//指针的指向不可以改,指向的值可以改(p4 = &b非法)
	*p4 = 10;
00007FF61F8625F6  mov         rax,qword ptr [p4]  
00007FF61F8625FD  mov         dword ptr [rax],0Ah  

//3. const既修饰指针,又修饰常量(理解:两者都是常量)
	const int* const p5 = &a;
00007FF61F862603  lea         rax,[a]  
00007FF61F862607  mov         qword ptr [p5],rax  
	//指针的指向和指向的值都不可以修改

lea指令也可以表示加法、有限的乘法,不能一步得出结果的原因——比例因子只能取(1,2,4,8),所以要将数字例如12分解

移位指令

移位量可以是一个立即数或者是存放在寄存器cl中的数,对于移位指令只允许以特定的寄存器cl为操作数,其他的寄存器不行

例题:移位指令更高效的实现乘法操作

为什么不直接使用乘法指令?
编译器在生成汇编指令的时候,会优先考虑更高效的方式

指令与条件码

在C语言中,有一类语句需要满足条件才可以执行,如条件语句、循环语句,它们需要通过数据测试结果来决定操作执行的顺序,以下介绍与控制流相关的指令

例题:

减法指令的执行,需要用到算数逻辑单元ALU,ALU从寄存器中读取到数据,执行相应的运算,然后再将运算的结果返回到目的寄存器rdx中

ALU除了执行算数和逻辑运算指令,还会根据该运算的结果去设置条件码寄存器

条件码寄存器

条件码寄存器由CPU维护,长度为单个比特位,它描述了最近执行的属性
条件码寄存器的值是由ALU在执行算术和运算指令时写入的

例题:

int cmp(long a, long b) {
00DC18F0  push        ebp  
00DC18F1  mov         ebp,esp  
00DC18F3  sub         esp,0C4h  
00DC18F9  push        ebx  
00DC18FA  push        esi  
00DC18FB  push        edi  
00DC18FC  lea         edi,[ebp-0C4h]  
00DC1902  mov         ecx,31h  
00DC1907  mov         eax,0CCCCCCCCh  
00DC190C  rep stos    dword ptr es:[edi]  
00DC190E  mov         ecx,offset _773DFF07_04 条件码\04 条件码@cpp (0DCC029h)  
00DC1913  call        @__CheckForDebuggerJustMyCode@4 (0DC132Fh)  
	return (a == b);
00DC1918  mov         eax,dword ptr [a]  
00DC191B  cmp         eax,dword ptr [b]  
//cmp指令:根据两个操作数的差设置条件码寄存器,但不会更新寄存器的值
//根据符号标志(SF)和溢出标志(OF)的异或情况可以判断两数的大小
//对于无符号数,采用进位标志和零标志的组合
//test指令:只设置条件码寄存器的值,但不会更新寄存器的值
00DC191E  jne         cmp+3Ch (0DC192Ch)  
00DC1920  mov         dword ptr [ebp-0C4h],1  
00DC192A  jmp         cmp+46h (0DC1936h)  
00DC192C  mov         dword ptr [ebp-0C4h],0  
00DC1936  mov         eax,dword ptr [ebp-0C4h]  
}

跳转指令与循环

条件结构

例题:计算两数之差的绝对值

//计算两数之差的绝对值
long absdiff_se(long x, long y) {
007B18F0  push        ebp  
007B18F1  mov         ebp,esp  
007B18F3  sub         esp,0CCh  
007B18F9  push        ebx  
007B18FA  push        esi  
007B18FB  push        edi  
007B18FC  lea         edi,[ebp-0CCh]  
007B1902  mov         ecx,33h  
007B1907  mov         eax,0CCCCCCCCh  
007B190C  rep stos    dword ptr es:[edi]  
007B190E  mov         ecx,offset _D7AEB086_05 跳转@cpp (07BC029h)  
007B1913  call        @__CheckForDebuggerJustMyCode@4 (07B132Fh)  
	long reuslt;
	if (x < y) {
//条件语句:x小于y由指令cmp实现,指令cmp会根据(x-y)的结果来设置符号标志(SF)和溢出标志(OF)
007B1918  mov         eax,dword ptr [x]  
007B191B  cmp         eax,dword ptr [y]  
007B191E  jge         absdiff_se+3Bh (07B192Bh)  
//跳转指令jge:跳转指令会根据条件寄存器的某种组合来决定是否进行跳转,即根据符号标志(SF)和溢出标志(OF)的异或结果来判断是顺序执行还是跳转执行
		reuslt = y - x;
007B1920  mov         eax,dword ptr [y]  
007B1923  sub         eax,dword ptr [x]  
007B1926  mov         dword ptr [reuslt],eax  
	}
007B1929  jmp         absdiff_se+44h (07B1934h)  
//当x>y:指令顺序执行,然后返回结果,(07B1934h)处指令不会执行
//当x<y:程序跳转到(07B1934h)处执行,然后返回结果
	else {
		reuslt = x - y;
007B192B  mov         eax,dword ptr [x]  
007B192E  sub         eax,dword ptr [y]  
007B1931  mov         dword ptr [reuslt],eax  
	}
	return reuslt;
007B1934  mov         eax,dword ptr [reuslt]  
}

上述机制简单通用,但是效率低,利用数据的条件转移,代替控制的条件转移提高效率

例题:将上题的代码改进为下面的代码

对应的汇编:

//计算两数之差的绝对值
long cmovdiff_se(long x, long y) {
007B4120  push        ebp  
007B4121  mov         ebp,esp  
007B4123  sub         esp,0E8h  
007B4129  push        ebx  
007B412A  push        esi  
007B412B  push        edi  
007B412C  lea         edi,[ebp-0E8h]  
007B4132  mov         ecx,3Ah  
007B4137  mov         eax,0CCCCCCCCh  
007B413C  rep stos    dword ptr es:[edi]  
007B413E  mov         ecx,offset _D7AEB086_05 跳转@cpp (07BC029h)  
007B4143  call        @__CheckForDebuggerJustMyCode@4 (07B132Fh)  
		long rval = y - x;
007B4148  mov         eax,dword ptr [y]  
007B414B  sub         eax,dword ptr [x]  
007B414E  mov         dword ptr [rval],eax  
		long eval = x - y;;
007B4151  mov         eax,dword ptr [x]  
007B4154  sub         eax,dword ptr [y]  
007B4157  mov         dword ptr [eval],eax  
		long ntest = x >= y;
007B415A  mov         eax,dword ptr [x]  
007B415D  cmp         eax,dword ptr [y]  
007B4160  jl          cmovdiff_se+4Eh (07B416Eh)  
007B4162  mov         dword ptr [ebp-0E8h],1  
007B416C  jmp         cmovdiff_se+58h (07B4178h)  
007B416E  mov         dword ptr [ebp-0E8h],0  
007B4178  mov         ecx,dword ptr [ebp-0E8h]  
007B417E  mov         dword ptr [ntest],ecx  
		if (ntest) 
007B4181  cmp         dword ptr [ntest],0  
007B4185  je          cmovdiff_se+6Dh (07B418Dh) 
//cmov根据条件码的某种组合来进行有条件值的传送数据,当满足规定的条件时,将寄存器rdx值复制到寄存器rax中
//编译器会自己优化汇编的结果,VS的返汇编结果不一样
			rval = eval;
007B4187  mov         eax,dword ptr [eval]  
007B418A  mov         dword ptr [rval],eax  
		return rval;
007B418D  mov         eax,dword ptr [rval]  
}

循环结构

汇编中没有专门用来表示循环的指令,循环语句是通过条件测试与跳转的结合来实现的

例题:用3中循环结构实现阶乘

do-while循环

//do-while循环
long fact_do(long n) {
00007FF75B1D17F0  mov         dword ptr [rsp+8],ecx  
00007FF75B1D17F4  push        rbp  
00007FF75B1D17F5  push        rdi  
00007FF75B1D17F6  sub         rsp,108h  
00007FF75B1D17FD  lea         rbp,[rsp+20h]  
00007FF75B1D1802  mov         rdi,rsp  
00007FF75B1D1805  mov         ecx,42h  
00007FF75B1D180A  mov         eax,0CCCCCCCCh  
00007FF75B1D180F  rep stos    dword ptr [rdi]  
00007FF75B1D1811  mov         ecx,dword ptr [rsp+128h]  
00007FF75B1D1818  lea         rcx,[__D7AEB086_05 跳转@cpp (07FF75B1E2029h)]  
00007FF75B1D181F  call        __CheckForDebuggerJustMyCode (07FF75B1D1375h)  
	long result = 1;
00007FF75B1D1824  mov         dword ptr [result],1  
	do {
		result *= n;
00007FF75B1D182B  mov         eax,dword ptr [result]  
00007FF75B1D182E  imul        eax,dword ptr [n]  
00007FF75B1D1835  mov         dword ptr [result],eax  
		n = n - 1;
00007FF75B1D1838  mov         eax,dword ptr [n]  
00007FF75B1D183E  dec         eax  
00007FF75B1D1840  mov         dword ptr [n],eax  
	} while (n > 1);
00007FF75B1D1846  cmp         dword ptr [n],1  
00007FF75B1D184D  jg          fact_do+3Bh (07FF75B1D182Bh) 
//指令cmp和跳转指令的组合实现了循环
	return result;
00007FF75B1D184F  mov         eax,dword ptr [result]  
}
00007FF75B1D1852  lea         rsp,[rbp+0E8h]  
00007FF75B1D1859  pop         rdi  
00007FF75B1D185A  pop         rbp  
00007FF75B1D185B  ret  

while循环

//while循环
long fact_while(long n) {
00007FF610C31F50  mov         dword ptr [rsp+8],ecx  
00007FF610C31F54  push        rbp  
00007FF610C31F55  push        rdi  
00007FF610C31F56  sub         rsp,108h  
00007FF610C31F5D  lea         rbp,[rsp+20h]  
00007FF610C31F62  mov         rdi,rsp  
00007FF610C31F65  mov         ecx,42h  
00007FF610C31F6A  mov         eax,0CCCCCCCCh  
00007FF610C31F6F  rep stos    dword ptr [rdi]  
00007FF610C31F71  mov         ecx,dword ptr [rsp+128h]  
00007FF610C31F78  lea         rcx,[__D7AEB086_05 跳转@cpp (07FF610C42029h)]  
00007FF610C31F7F  call        __CheckForDebuggerJustMyCode (07FF610C31375h)  
	long result = 1;
00007FF610C31F84  mov         dword ptr [result],1  
	while (n > 1) {
00007FF610C31F8B  cmp         dword ptr [n],1  
00007FF610C31F92  jle         fact_while+61h (07FF610C31FB1h)  
//测试的位置不同
		result *= n;
00007FF610C31F94  mov         eax,dword ptr [result]  
00007FF610C31F97  imul        eax,dword ptr [n]  
00007FF610C31F9E  mov         dword ptr [result],eax  
		n = n - 1;
00007FF610C31FA1  mov         eax,dword ptr [n]  
00007FF610C31FA7  dec         eax  
00007FF610C31FA9  mov         dword ptr [n],eax  
	}
00007FF610C31FAF  jmp         fact_while+3Bh (07FF610C31F8Bh)  
	return result;
00007FF610C31FB1  mov         eax,dword ptr [result]  
}

for循环

//for循环
long fact_for(long n) {
00007FF62CFD1A90  mov         dword ptr [rsp+8],ecx  
00007FF62CFD1A94  push        rbp  
00007FF62CFD1A95  push        rdi  
00007FF62CFD1A96  sub         rsp,108h  
00007FF62CFD1A9D  lea         rbp,[rsp+20h]  
00007FF62CFD1AA2  mov         rdi,rsp  
00007FF62CFD1AA5  mov         ecx,42h  
00007FF62CFD1AAA  mov         eax,0CCCCCCCCh  
00007FF62CFD1AAF  rep stos    dword ptr [rdi]  
00007FF62CFD1AB1  mov         ecx,dword ptr [rsp+128h]  
00007FF62CFD1AB8  lea         rcx,[__D7AEB086_05 跳转@cpp (07FF62CFE2029h)]  
00007FF62CFD1ABF  call        __CheckForDebuggerJustMyCode (07FF62CFD1375h)  
	long result = 1;
00007FF62CFD1AC4  mov         dword ptr [result],1  
	for(; n > 1; n = n - 1) {
00007FF62CFD1ACB  jmp         fact_for+4Bh (07FF62CFD1ADBh)  
00007FF62CFD1ACD  mov         eax,dword ptr [n]  
00007FF62CFD1AD3  dec         eax  
00007FF62CFD1AD5  mov         dword ptr [n],eax  
00007FF62CFD1ADB  cmp         dword ptr [n],1  
00007FF62CFD1AE2  jle         fact_for+63h (07FF62CFD1AF3h)  
		result *= n;
00007FF62CFD1AE4  mov         eax,dword ptr [result]  
00007FF62CFD1AE7  imul        eax,dword ptr [n]  
00007FF62CFD1AEE  mov         dword ptr [result],eax  
	}
00007FF62CFD1AF1  jmp         fact_for+3Dh (07FF62CFD1ACDh)  
//对比for循环和while的汇编代码,除了跳转指令的位置不同,其他地方没有差异
	return result;
00007FF62CFD1AF3  mov         eax,dword ptr [result]  
}

switch语句

switch语句通过一个整数的索引值进行多重分支,switch语句通过跳转表的数据结构是自己更高效

与使用一连串的if-else相比,使用跳转表的优点在于switch语句的执行时间与case的数量无关

过程(函数调用)

函数调用栈

栈是内存的一个区域,栈的增长方向是从内存的高地址向低地址,例如保存寄存器rax的数据0x123,使用pushq指令,pushq指令执行过程分为两步:

  • 指向栈顶的寄存器rsp进行一个减法操作,比如:一开始栈顶的位置是0x108,减8后变成0x100
  • 然后将需要保存的数据复制到新的栈顶地址,此时内存0x108处将保存寄存器rax的数据0x123

与之类似,出栈操作pop指令也可以分为两步:

  • 首先从栈顶的位置读出数据,复制到寄存器rbx中,此时栈顶指针指向的内存地址为0x100
  • 然后将栈顶指针加8,栈顶指针指向的内存地址为0x108

实际上pop指令是通过修改栈顶指针所指向的内存地址来实现数据删除的,此时内存地址0x100中所保存的数据0x123任然存在,直到下次push操作,值才会被覆盖

函数的栈帧:当函数调用所需要的空间大于寄存器的空间时,需要借助栈上的空间,这部分空间即栈帧,若一个函数参数多于6时,就要借助栈传递

以swap函数为例:

main()函数中的函数调用:

swap(&a, &b);
00007FF6F3E2271A  lea         rdx,[b]  
00007FF6F3E2271E  lea         rcx,[a]  
//先放入靠右边的参数
00007FF6F3E22722  call        swap (07FF6F3E2137Fh) 
//将函数swap的第一条指令地址(07FF6F3E2137Fh)写入程序指令寄存器rip中
//将返回地址压入栈中,返回地址就是swap执行完之后下一条指令的地址

swap函数实现:

void swap(int* a, int* b) {
00007FF6F3E225B0  mov         qword ptr [rsp+10h],rdx  
//理解:传入函数的参数:b的地址,存放在rdx中
00007FF6F3E225B5  mov         qword ptr [rsp+8],rcx  
//理解:传入函数的参数:a的地址,存放在rcx中
00007FF6F3E225BA  push        rbp  
//压栈保存栈帧的底部寄存器:rbp
00007FF6F3E225BB  push        rdi  
//压栈目标变址的寄存器:rdi 
00007FF6F3E225BC  sub         rsp,108h  
//栈指针rsp指向栈顶元素:sub指令将栈指针减小一个适当的量为没有指定初始值的数据在栈上分配空间
00007FF6F3E225C3  lea         rbp,[rsp+20h]  
//栈指针保存的是栈帧的底部(栈是倒着放的)
00007FF6F3E225C8  mov         rdi,rsp 
//将rsp中栈顶元素移入目标变址的寄存器rdi中
00007FF6F3E225CB  mov         ecx,42h  
00007FF6F3E225D0  mov         eax,0CCCCCCCCh  
00007FF6F3E225D5  rep stos    dword ptr [rdi]  
//rep指令:按照计数寄存器(ECX)中指定的次数重复执行字符串指令
//这两行代码用于调试,如检测缓冲区溢出、非法篡改局部变量等
00007FF6F3E225D7  mov         rcx,qword ptr [rsp+128h]  
00007FF6F3E225DF  lea         rcx,[__B3A5FD74_08 指针与函数\08 指针与函数@cpp (07FF6F3E34029h)]  
00007FF6F3E225E6  call        __CheckForDebuggerJustMyCode (07FF6F3E213F2h)  
	//解引用之后交换值
	int temp;
	temp = *a;
00007FF6F3E225EB  mov         rax,qword ptr [a]  
//将M[a](是一个地址)赋值给rax
00007FF6F3E225F2  mov         eax,dword ptr [rax]  
//将M[R[rax]]中的值(是真正的值)给eax
00007FF6F3E225F4  mov         dword ptr [temp],eax  
//将eax中的值(是真正的值)给M[temp],可见局部变量temp是在内存上(栈区)开辟的空间
    
//栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
//注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放

	*a = *b;
00007FF6F3E225F7  mov         rax,qword ptr [a]  
//将M[a](是一个地址)赋值给rax
00007FF6F3E225FE  mov         rcx,qword ptr [b]  
//将M[b](是一个地址)赋值给rcx
00007FF6F3E22605  mov         ecx,dword ptr [rcx]  
//将M[R[rcx]]值给ecx
00007FF6F3E22607  mov         dword ptr [rax],ecx  
//将ecx给M[R[rax]](也就是将b值给了a)
	*b = temp;
00007FF6F3E22609  mov         rax,qword ptr [b]  
//将M[b](存放的是地址)赋值给rax
00007FF6F3E22610  mov         ecx,dword ptr [temp]  
//将M[temp]赋值给ecx
00007FF6F3E22613  mov         dword ptr [rax],ecx  
//将ecx赋值给M[rax]
}
00007FF6F3E22615  lea         rsp,[rbp+0E8h]  
//释放栈空间
00007FF6F3E2261C  pop         rdi  
//出栈保存目标变址的寄存器:rdi 
00007FF6F3E2261D  pop         rbp  
//出栈保存栈帧的底部寄存器:rbp
00007FF6F3E2261E  ret  
//返回

以冒泡排序函数为例:

main()函数中的函数调用:

//利用数组,函数,指针,实现冒泡排序函数
	int arr[10] = { 1,3,6,7,8,5,2,8,9,10 };
00007FF6F3E227A8  mov         dword ptr [arr],1  
00007FF6F3E227AF  mov         dword ptr [rbp+4Ch],3  
00007FF6F3E227B6  mov         dword ptr [rbp+50h],6  
00007FF6F3E227BD  mov         dword ptr [rbp+54h],7  
00007FF6F3E227C4  mov         dword ptr [rbp+58h],8  
00007FF6F3E227CB  mov         dword ptr [rbp+5Ch],5  
00007FF6F3E227D2  mov         dword ptr [rbp+60h],2  
00007FF6F3E227D9  mov         dword ptr [rbp+64h],8  
00007FF6F3E227E0  mov         dword ptr [rbp+68h],9  
00007FF6F3E227E7  mov         dword ptr [rbp+6Ch],0Ah  
    
    	bubbleSort(arr, 10);
00007FF6F3E2285E  mov         edx,0Ah  
00007FF6F3E22863  lea         rcx,[arr]  
//先放入靠右边的参数
00007FF6F3E22867  call        bubbleSort (07FF6F3E2138Eh) 

函数实现:

void bubbleSort(int* arr, int len) {
00007FF6F3E22350  mov         dword ptr [rsp+10h],edx  
00007FF6F3E22354  mov         qword ptr [rsp+8],rcx  
00007FF6F3E22359  push        rbp  
00007FF6F3E2235A  push        rdi  
00007FF6F3E2235B  sub         rsp,128h  
00007FF6F3E22362  lea         rbp,[rsp+20h]  
00007FF6F3E22367  mov         rdi,rsp  
00007FF6F3E2236A  mov         ecx,4Ah  
00007FF6F3E2236F  mov         eax,0CCCCCCCCh  
00007FF6F3E22374  rep stos    dword ptr [rdi]  
00007FF6F3E22376  mov         rcx,qword ptr [rsp+148h]  
00007FF6F3E2237E  lea         rcx,[__B3A5FD74_08 指针与函数\08 指针与函数@cpp (07FF6F3E34029h)]  
00007FF6F3E22385  call        __CheckForDebuggerJustMyCode (07FF6F3E213F2h)  
	for (int i = 0; i < len - 1; i++) {
00007FF6F3E2238A  mov         dword ptr [rbp+4],0  
00007FF6F3E22391  jmp         bubbleSort+4Bh (07FF6F3E2239Bh)  
00007FF6F3E22393  mov         eax,dword ptr [rbp+4]  
00007FF6F3E22396  inc         eax  
00007FF6F3E22398  mov         dword ptr [rbp+4],eax  
00007FF6F3E2239B  mov         eax,dword ptr [len]  
00007FF6F3E223A1  dec         eax  
00007FF6F3E223A3  cmp         dword ptr [rbp+4],eax  
00007FF6F3E223A6  jge         bubbleSort+0CCh (07FF6F3E2241Ch)  
		for (int j = 0; j < len - 1 - i; j++) {
00007FF6F3E223A8  mov         dword ptr [rbp+24h],0  
00007FF6F3E223AF  jmp         bubbleSort+69h (07FF6F3E223B9h)  
00007FF6F3E223B1  mov         eax,dword ptr [rbp+24h]  
00007FF6F3E223B4  inc         eax  
00007FF6F3E223B6  mov         dword ptr [rbp+24h],eax  
00007FF6F3E223B9  mov         eax,dword ptr [len]  
00007FF6F3E223BF  dec         eax  
00007FF6F3E223C1  sub         eax,dword ptr [rbp+4]  
00007FF6F3E223C4  cmp         dword ptr [rbp+24h],eax  
00007FF6F3E223C7  jge         bubbleSort+0C7h (07FF6F3E22417h)  
			if (arr[j] > arr[j + 1]) {
00007FF6F3E223C9  movsxd      rax,dword ptr [rbp+24h]  
00007FF6F3E223CD  mov         ecx,dword ptr [rbp+24h]  
00007FF6F3E223D0  inc         ecx  
00007FF6F3E223D2  movsxd      rcx,ecx  
00007FF6F3E223D5  mov         rdx,qword ptr [arr]  
00007FF6F3E223DC  mov         r8,qword ptr [arr]  
00007FF6F3E223E3  mov         ecx,dword ptr [r8+rcx*4]  
00007FF6F3E223E7  cmp         dword ptr [rdx+rax*4],ecx  
00007FF6F3E223EA  jle         bubbleSort+0C5h (07FF6F3E22415h)  
				swap(&arr[j], &arr[j + 1]);
00007FF6F3E223EC  mov         eax,dword ptr [rbp+24h]  
00007FF6F3E223EF  inc         eax  
00007FF6F3E223F1  cdqe  
00007FF6F3E223F3  mov         rcx,qword ptr [arr]  
00007FF6F3E223FA  lea         rax,[rcx+rax*4]  
00007FF6F3E223FE  movsxd      rcx,dword ptr [rbp+24h]  
00007FF6F3E22402  mov         rdx,qword ptr [arr]  
00007FF6F3E22409  lea         rcx,[rdx+rcx*4]  
00007FF6F3E2240D  mov         rdx,rax  
00007FF6F3E22410  call        swap (07FF6F3E2137Fh)  
				//int temp = arr[j];
				//arr[j] = arr[j + 1];
				//arr[j + 1] = temp;
			}
		}
00007FF6F3E22415  jmp         bubbleSort+61h (07FF6F3E223B1h)  
	}
00007FF6F3E22417  jmp         bubbleSort+43h (07FF6F3E22393h)  
}
00007FF6F3E2241C  lea         rsp,[rbp+108h]  
00007FF6F3E22423  pop         rdi  
00007FF6F3E22424  pop         rbp  
00007FF6F3E22425  ret  

注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放

//栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
//注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放

int* func(int b) {		//形参数据也会放在栈区
00B118F0  push        ebp  
00B118F1  mov         ebp,esp  
00B118F3  sub         esp,0D0h  
00B118F9  push        ebx  
00B118FA  push        esi  
00B118FB  push        edi  
00B118FC  lea         edi,[ebp-0D0h]  
00B11902  mov         ecx,34h  
00B11907  mov         eax,0CCCCCCCCh  
00B1190C  rep stos    dword ptr es:[edi]  
00B1190E  mov         eax,dword ptr [__security_cookie (0B1A004h)]  
00B11913  xor         eax,ebp  
00B11915  mov         dword ptr [ebp-4],eax  
00B11918  mov         ecx,offset _C0FD5A25_11 栈区@cpp (0B1C029h)  
00B1191D  call        @__CheckForDebuggerJustMyCode@4 (0B1132Fh)  
	b = 100;
00B11922  mov         dword ptr [b],64h  
	//局部变量:存放在栈区,栈区的数据在函数执行完后自动释放
	int a = 10;
00B11929  mov         dword ptr [a],0Ah  
	//返回局部变量的地址(错误)
	return &a;
00B11930  lea         eax,[a]  
}

int main() {

	int b = 0;
	int* p = func(b);

	cout << *p << endl;		//第一次可以打印正确的数据因为编译器做了智能保留
    //猜测因为00B11930  lea         eax,[a]  
	cout << *p << endl;		//第二次确实无法得到争取结果

	system("pause");
	return 0;
}

堆区:由程序员分配释放,若程序员不释放,程序结束时由OS回收

//堆区:由程序员分配释放,若程序员不释放,程序结束时由OS回收
//在C++中主要利用new在堆区开辟内存

int* func() {
002C1A20  push        ebp  
002C1A21  mov         ebp,esp  
002C1A23  sub         esp,0DCh  
002C1A29  push        ebx  
002C1A2A  push        esi  
002C1A2B  push        edi  
002C1A2C  lea         edi,[ebp-0DCh]  
002C1A32  mov         ecx,37h  
002C1A37  mov         eax,0CCCCCCCCh  
002C1A3C  rep stos    dword ptr es:[edi]  
002C1A3E  mov         ecx,offset _9DFB856B_12 堆区@cpp (02CC029h)  
002C1A43  call        @__CheckForDebuggerJustMyCode@4 (02C13ACh)  
	//new关键字,可以将数据开辟到堆区
	//指针本质上也是局部变量,放在栈上,指针保存的数据放在堆区
	int* p = new int(10);
002C1A48  push        4  
002C1A4A  call        operator new (02C1118h)  
002C1A4F  add         esp,4  
002C1A52  mov         dword ptr [ebp-0D4h],eax  
002C1A58  cmp         dword ptr [ebp-0D4h],0  
002C1A5F  je          func+5Bh (02C1A7Bh)  
002C1A61  mov         eax,dword ptr [ebp-0D4h]  
002C1A67  mov         dword ptr [eax],0Ah  
002C1A6D  mov         ecx,dword ptr [ebp-0D4h]  
002C1A73  mov         dword ptr [ebp-0DCh],ecx  
002C1A79  jmp         func+65h (02C1A85h)  
002C1A7B  mov         dword ptr [ebp-0DCh],0  
002C1A85  mov         edx,dword ptr [ebp-0DCh]  
002C1A8B  mov         dword ptr [p],edx  
	return p;
002C1A8E  mov         eax,dword ptr [p]  
}


int main() {
	//在堆区开辟数据
	int* p = func();
	cout << *p << endl;

	system("pause");
	return 0;
}

数组的分配和访问

一维数组

/*一维数组三种定义方法*/
//1. 数据类型 数组名[数组长度 ]
int arr1[5];

//2. 数据类型 数组名[数组长度 ] = {值}
int arr2[5] = { 1,2,3,4,5 };
005325A2  mov         dword ptr [arr2],1  
//将1放入M[arr2]的内存空间里,可以看出数组名arr2就是首地址
005325A9  mov         dword ptr [ebp-34h],2  
005325B0  mov         dword ptr [ebp-30h],3  
005325B7  mov         dword ptr [ebp-2Ch],4  
005325BE  mov         dword ptr [ebp-28h],5  

//3. 数据类型 数组名[] = {值}
int arr3[] = { 1,2,3,4,5, };
005325C5  mov         dword ptr [arr3],1  
005325CC  mov         dword ptr [ebp-50h],2  
005325D3  mov         dword ptr [ebp-4Ch],3  
005325DA  mov         dword ptr [ebp-48h],4  
005325E1  mov         dword ptr [ebp-44h],5  

二维数组(嵌套数组)

/*二维数组的定义*/
//1. 数据类型 数组名[行数][列数];
int mat1[2][3];

//2. 数据类型 数组名[行数][列数] = {{数据1,数据2},{数据3,数据4}};
int mat2[2][3] = { {1,2,3},{4,5,6} };
00532B38  mov         dword ptr [mat2],1  
//可以看出,数组名mat2仍是首地址,所有的数据都是按顺序存放的
00532B42  mov         dword ptr [ebp-128h],2  
00532B4C  mov         dword ptr [ebp-124h],3  
00532B56  mov         dword ptr [ebp-120h],4  
00532B60  mov         dword ptr [ebp-11Ch],5  
00532B6A  mov         dword ptr [ebp-118h],6  
    
//3. 数据类型 数组名[行数][列数] = {数据1,数据2,数据3,数据4};
int mat3[2][3] = { 1,2,3,4,5,6 };
00532B74  mov         dword ptr [mat3],1  
00532B7E  mov         dword ptr [ebp-148h],2  
00532B88  mov         dword ptr [ebp-144h],3  
00532B92  mov         dword ptr [ebp-140h],4  
00532B9C  mov         dword ptr [ebp-13Ch],5  
00532BA6  mov         dword ptr [ebp-138h],6  
    
//4. 数据类型 数组名[][列数] = {数据1,数据2,数据3,数据4};
int mat4[2][3] = { 1,2,3,4,5,6 };
00532BB0  mov         dword ptr [mat4],1  
00532BBA  mov         dword ptr [ebp-168h],2  
00532BC4  mov         dword ptr [ebp-164h],3  
00532BCE  mov         dword ptr [ebp-160h],4  
00532BD8  mov         dword ptr [ebp-15Ch],5  
00532BE2  mov         dword ptr [ebp-158h],6  

结构体与联合体

例题:结构体中无论是单个变量还是数组元素,都是通过起始地址+偏移量访问

例题:结构体数据对齐

原则:任何K字节的基本对象的地址必须是K的倍数

例题:联合体大小

由于每个结点不是叶子结点就是内部结点,故可以用联合体定义二叉树结点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值