CSAPP:程序的结构和执行
一、计算机系统概述
学习方法
- 把书读薄,参照书籍的目录列出知识框架(做成一份重难点笔记,对照着章节目录时刻明确现在学的知识是什么地位,通过写书上的例子来学习,每看几段就要写自己的总结复述,提高读书的效率)
- 把书读厚,看一集视频,看一章的书,反汇编之前写的C++基本语法代码,边学边玩C++(查找相关的书籍和文章,超越书本理解,独立完成实验与习题,一定要实践每个环节)
- 把书读薄(画思维导图,压缩本书的内容)
- 输出,学以致用:优化程序性能(复述内容,重做实验)
二、数据的机器级表示与处理
信息的存储
大多数计算机使用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)和位模式
- 所有地址的集合称为虚拟地址空间
- 字长用于表明指针数据的标称大小
- 字长决定了虚拟地址空间的最大值
下表显示了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字节
小端法
最低有效字节存储在最前面
应用:
-
网络应用程序代码编写,确保字节序列在发送方和接受方的含义相同
-
阅读机器级代码(反汇编)
-
使用强制类型转换或者联合体,以下代码可以查看程序对象的字节表示:
#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]的内存空间里
寄存器与数据传送指令
- 程序计数器PC:用%rip表示,表示将要执行的下一条指令在内存中的地址
- 整数寄存器:可以用来保存地址(对应C语言指针)或整数数据
- 条件码寄存器:保存算数、逻辑状态指令,用于实现if/while
- 向量寄存器:存放整数、浮点数值
寄存器
- 寄存器就是你的口袋。身上只有那么几个,只装最常用或者马上要用的东西
- 内存就是你的背包。有时候拿点什么放到口袋里,有时候从口袋里拿出点东西放在背包里。
- 辅存就是你家里的抽屉。可以放很多东西,但存取不方便
- 断电了就相当于你人没了,在家里复活,家里抽屉里东西还在,但包里口袋里的装备都爆没了
数据格式
访问信息
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的倍数
例题:联合体大小
由于每个结点不是叶子结点就是内部结点,故可以用联合体定义二叉树结点