C++反汇编 利用反汇编分析常见C/C++语句的底层实现(硬核)
本节我们利用反汇编技术来对我们最常见的C语言语句进行解析,C++反汇编技术可以让你更好的理解C++/C语言的底层含义,相信我,读完本节,一定会让你感到醍醐灌顶,瞬间通透C++/C语言的底层含义
我们假设你已经基本了解了x86汇编的基本指令:
mov ax,06h: 基本赋值指令 ax=0006h
add ax,cx: 相加指令:ax=ax+cx
sub ax,cx: 相减指令:ax=ax-cx
call 地址: 跳转指令:跳转到这个地址位置,常用于跳转到函数
ret : 跳转指令:和call指令配合使用,相当于return,从函数返回
jmp 地址: jmp:无条件跳转指令,跳转到此地址
jne | je : 有条件跳转指令,jne表示如果nor equal则跳转,je表示如果equal则跳转
lea ax,[地址]:地址赋值指令,把这个地址赋值给ax
等等等等…
基本汇编单位:
- byte ptr : 表示一个字节
- word ptr:一个字,表示两个字节
- dword ptr:两个字,表示四个字节
- qword ptr:四个字:表示八个字节
赋值操作
int a = 10; int b = 20;
就是这么简单的两条赋值语言,你知道他在汇编里是什么样的吗?
mov dword ptr [a],0Ah mov dword ptr [b],14h
我们把十六进制的 0Ah(H:10)送到 a所表示的内存地址空间。
我们把十六进制的14h(H:20)送到b表示的内存地址空间
dword ptr表示他们是四个字节,正好可以表示出:我们的int类型的sizeof(int)为4,即4个字节。
if条件判断
if (a == b) { printf("a==b"); } else { printf("a!=b"); }
它的汇编指令是怎样的呢?
mov eax,dword ptr [b] //b的内存空间里的内容送往eax寄存器 cmp dword ptr [a],eax // cmp和a内存空间里的内容比较 jne __$EncStackInitStart+52h (07FF7C6895CB1h) //如果不相等,则跳转,否则,继续往下执行 lea rcx,[string "a==b" (07FF7C689AE38h)] //将表示字符串的地址赋值给rcx,配合下面的跳转到printf的函数打印此字符串 call printf (07FF7C6891195h) 07FF7C6895CB1h: jmp __$EncStackInitStart+5Eh (07FF7C6895CBDh) //cmp相等则直接跳转到此处 lea rcx,[string "a!=b" (07FF7C689AE40h)] call printf (07FF7C6891195h)
我们来分析一下:
- 两条call的指令相同,可以推断出call的作用(在此处)为跳转到printf 函数所在的位置,然后在此函数内部,一定还有一个ret返回主程序。
- jne的作用:在cmp比较后,如果相等则不会触发jne,则跳过此jne,继续往下执行;如果cmp比较后不相等,则触发jne,有条件跳转到下方,即到了else的位置,接着打印不相等的信息。
- 注意cmp的比较:在此处我们只是简单的比较他们,如果相等或者不相等分别干什么,但是请注意,
cmp指令的执行过程不会这么简单,它也包含复杂的比较条件,在此我们不再赘述。
指针和引用的实质
一个很重要的问题: 引用就是指针
,为什么?
int c = 5; int d = 15; int* p1 = &c; int& p2 = d;
看一下汇编代码:
mov dword ptr [c],5 mov dword ptr [d],0Fh //指针的操作 lea rax,[c] //将c的地址值给rax mov qword ptr [p1],rax //将rax(c的地址值)给p1所在的内存空间 //引用的操作 lea rax,[d] mov qword ptr [p2],rax
- 我们把5这个值送到c的内存空间,把0F值送到d的内存空间,即完成了对c和d的赋值操作。
- lea指令:把c的地址值赋值给rax寄存器,注意:就是c的地址,不是其里面的内容。
- p1指针:注意指针的单位是qword ptr 表示八个字节(32位 msvc编译器),我们把rax(存储c的地址)给到p1所表示内存空间,即完成了 int* p1=&c
- p2引用:我们把d的地址给到了rax,再把rax的值(存储d的地址)给到了p2所在的内存空间,即完成了int &p2=d。
注意: 指针和引用的内存空间里存储的是c和d的地址,我们再通过解引用,从他们的内存空间中获取c和d的地址,这就是我们所知道的指针赋值和解引用操作。
大家有没有注意到一个问题: 指针和引用的汇编指令是一样的:
lea 寄存器,[地址值] mov qword ptr [指针变量地址],寄存器
他们的汇编指令一样,所以说他们的实质是一样的: 引用的本质就是指针。
同时,如果我们看到这两条指令,则99%的概率是引用或者指针在赋值。
跳转函数
void fun() { int a = 10; int c = 20; a += c; } int main() { fun(); return 0; }
call fun (07FF63C6A13FCh) //函数跳转 .............. ..... //int a=10; mov dword ptr [a],0Ah //int b=20; mov dword ptr [c],14h //a=a+c; mov eax,dword ptr [c] mov ecx,dword ptr [a] add ecx,eax mov eax,ecx mov dword ptr [a],eax .... ret //返回主程序
我们进入函数时,实际上就是call 一个地址,这个地址指向的就是我们的函数地址,然后进入函数,我们完成对int类型的a和c的赋值,接着我们在完成 a+=c的操作:
分解:
- a+c: 将a的内存空间的内容和c的内容分别放到两个寄存器中,在由寄存器完成 add操作,则ecx就表示了a+c的值,此时和放在ecx寄存器中。
- a=a+c:把ecx寄存器传给eax寄存器,再由eax送到a所指向的内存空间,完成了a的赋值。
为什么不能把 [a]和[c]直接相加呢?为什么还要借助好几个寄存器中转?
原因:mov汇编指令不支持两个内存地址的相互操作,必须借助一个寄存器完成中转,也就是说mov的左右两边一定要有一个寄存器。
在最后,我们会有 一个ret的指令,此指令 完成return的返回操作,即返回主程序。
两个数字的交换操作
这是一个我们非常熟悉的两数字交换的指针操作:
void swap(int* a, int* b) { int temp = *a; *a = *b; *b = temp; }
//传递形参地址 lea rdx,[b] //取b的地址给rdx寄存器 lea rcx,[a] //取a的地址给rcx寄存器 call swap (07FF721F1132Fh) //进入函数 ..... ... //int temp=*a; mov rax,qword ptr [a] mov eax,dword ptr [rax] mov dword ptr [temp],eax //*a=*b; mov rax,qword ptr [a] mov rcx,qword ptr [b] mov ecx,dword ptr [rcx] mov dword ptr [rax],ecx //*b=temp; mov rax,qword ptr [b] mov ecx,dword ptr [temp] mov dword ptr [rax],ecx ... ret
解析:
-
进入函数时,如果我们有形参,并且是指针类型,我们要完成对形参的取地址的行为,即用lea指令获得地址,分别存储在两个寄存器中。
-
int temp=*a 的操作过程:
- 把 a的内存单元所存储的内容放到rax寄存器中,注意,这时是qword ptr即是指针的解引用操作,解引用后的值放入到eax寄存器中存储,eax把这个值放到temp所在的内存空间中。
-
*a=*b的操作过程:
- 分别对两个指针变量解引用,分别存储在两个寄存器中,我们要让a的值等于b的值,所以我们要把b的值转换为整形,再存储到ecx寄存器中,把ecx所存储的值送入到rax所在的存储空间,注意这里是把原来的a的值给覆盖了。
-
*b=temp的过程:
- 对指针解引用,即把b的值放入到rax寄存器中,把temp所在的内存空间的内容送到ecx寄存器中,ecx的值最后再送到b的内存空间。
数组的赋值及 -858993460数字的由来
int ar[5]={0};
是如何进行的呢?
lea rax,[ar] mov rdi,rax xor eax,eax mov ecx,14h rep stos byte ptr [rdi]
rep stos指令的作用:重复执行上述指令,以ecx的值为执行次数,把eax的值送到之后的目标地址处。
把ar的地址送到rax寄存器中,注意:ar表示的是数组首地址,rax表示的是数组的首地址,再把rax送到rdi所造的内存单元,即rdi所在的内存单元里存储者数组的首地址,通过这个地址,我们可以根据偏移来获得整个数组的地址。
eax表示我们要送往目标的值,我们一共要重复执行ecx次:即20次
注意:我们每次移动一个 byte ptr:即一个字节,但是我们的数组每个元素都是int,所以每个元素会执行4次,一共执行20次,正好和ecx总的重复次数相对应
。
我们通过看内存可以得知:
数组的初始内存全部都是CCCC…, 即我们的eax默认也是存放的eax,我们对eax执行XOR操作(异或操作),
异或操作: 两数相同则为0,两数相异则为1 .
我们对eax和eax执行异或操作,每次执行一个字节,执行过程如下:
二进制数中,开始1表示负数,0表示正数
eax: CCCC CCCC
原码:1100 1100 1100 1100 1100 1100 1100 1100
反码:1011 0011 0011 0011 0011 0011 0011 0011
(对第二个数往后的数取反,第一个数是符号位)
补码:1011 0011 0011 0011 0011 0011 0011 0100
(反码在最后加1,得到补码)
第一个数字表示符号位: 负数
计算除第一位以外的补码转换为十进制:
第二个数字开始,转换为十进制,(再加上最开始的符号位)我们会发现一个挺熟悉的值: - 858993460
这是个啥??
如果我们打印一个未经初始化的数组的值:
int ar[5] ; for (int i = 0; i < 5; i++) { cout << ar[i]<<" "; }
它的值就是 -858993460 这个值。
接着我们回到这段汇编上来:
-
一共执行20次,注意:·我们每次处理一个字节!!
-
- 第一次:eax= CCCC CCCC 异或操作后: eax : CCCC CC00 ,数组首元素的值:
- 858,993,664
- 第一次:eax= CCCC CCCC 异或操作后: eax : CCCC CC00 ,数组首元素的值:
-
- 第二次:eax=CCCC CC00 异或操作后: eax:CCCC 0000 , 数组首元素的值:
-859045888
- 第二次:eax=CCCC CC00 异或操作后: eax:CCCC 0000 , 数组首元素的值:
-
- 第三次:eax=CCCC 0000 异或操作后: eax:CC00 0000 , 数组首元素的值:
-872415232
- 第三次:eax=CCCC 0000 异或操作后: eax:CC00 0000 , 数组首元素的值:
-
- 第四次:eax=CC00 0000 异或操作后: eax:0000 0000 , 数组首元素的值:
0
- 第四次:eax=CC00 0000 异或操作后: eax:0000 0000 , 数组首元素的值:
-
这样我们就完成了对数组首元素的赋值,接着我们循环执行ecx次,即20次,再完成对剩下4个 赋值为0
总结
本节内容比较硬核,解释了底层C语言的语句执行情况,在此后,我也会写很多有意义的C++反汇编的代码,帮助大家理解C/C++的底层含义。
本文来自博客园,作者:hugeYlh,转载请注明原文链接:https://www.cnblogs.com/helloylh/p/17209704.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)