吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1445|回复: 11
收起左侧

[Windows] 通过IDA发现C++函数与引用的底层真相(含视频)

  [复制链接]
小菜鸟一枚 发表于 2026-6-21 10:17

特别说明:近期找不到工作特别着急,借论坛破解版块录制一节课,准备作为简历上的作品试试能不能入行IT教培行业。

0x1 课前准备

1. Dev-C++ 下载与安装

下载地址:https://sourceforge.net/projects/orwelldev```/

推荐版本:5.11(稳定版,内置MinGW编译器,安装后可直接使用)

安装注意:

  • 安装语言选 English,完成后首次启动时可切换为简体中文。
  • 安装路径建议保持默认,避免后续编译时出现路径问题。

2. IDA Pro 下载与准备

下载地址:https://pan.baidu.com/s/1z5VmZ5Pz2tp_KzNNvn7aJw?pwd=52pj

提取码:52pj

版本说明:该版本为吾爱破解论坛提供的,本人电脑 win7 不能选用高版本,故采用 7.7 版本,适合分析我们编译的 32 位 C++ 程序。

安装注意:

  • 下载后解压即可使用,无需安装。
  • 首次启动时,如果提示“是否加载插件”,选择“是”即可。

0x2函数与引用的底层真相

1. 为什么要学习 C++?

同学们好,欢迎来到我们的第一节课。

在开始写代码之前,我想先问大家一个问题:我们为什么要学 C++?现在市面上有 Python、Java、Go 那么多编程语言,为什么我们偏偏选择 C++?

C++ 是理解计算机底层的“最佳窗口”。 Python 像自动挡汽车,你只管踩油门,它帮你搞定一切;但 C++ 像手动挡,你得自己控制离合器、换挡,虽然难,但开过一次之后,你就彻底理解汽车是怎么工作的了。学完 C++,你再去看其他语言,都会觉得“原来如此”。

所以,C++ 很难,但正因为难,它才值得学。今天这节课,我们不只学语法,更要学一个很多老师不会教的东西——用逆向工具,看到 C++ 程序在内存中的真实样子。

准备好了吗?那我们开始。

2. 写代码:从 0 开始构建程序

2.1 头文件:程序从哪里来?

我们先写第一行:

#include <iostream>

这一行是什么意思呢?

include 是“包含”的意思。iostream 是 C++ 标准库里的一个文件,它里面装着很多我们写程序时需要用到的工具。

打个比方:#include <iostream> 就像你写作业时翻开一本工具书,里面有你需要的公式和定理。你不必自己发明这些公式,直接用就行。

我们接下来要用到 cout 和 cin 这些东西来输入输出,所以我们需要把这本“工具书”包含进来。

再写第二行:

using namespace std;

这一行又是什么意思呢?

std 是 C++ 标准库的“名字空间”。简单来说,iostream 这本工具书里的所有工具,都存放在一个叫 std 的抽屉里。using namespace std; 就是告诉编译器:“我要用 std 这个抽屉里的所有工具,不用每次都告诉我它们是哪个抽屉的。”

如果你不写这一行,你每次用 cout 的时候都要写成 std::cout,很麻烦。所以我们写了这一行,方便自己。

2.2 main 函数:程序的入口

接下来,我们要写程序的“身体”——也就是主函数。

int main() {

}

main 是“主要”的意思。main 函数是程序的入口:也就是说,程序一运行,CPU 就会自动找到这个 main 函数,然后从它的第一行代码开始执行。没有 main 函数,程序就不知道从哪里开始跑。

int 是“整数”的意思,它表示这个 main 函数在运行结束后,会返回一个整数给操作系统。0 表示程序正常运行结束,其他数字表示出错了。

() 是函数的参数列表,这里我们暂时不传参数,所以是空的。

{} 里面的内容,就是程序实际要执行的指令。

2.3 第一个函数:加法器

现在我们要在 {} 里面写具体的指令了。

但是等一下,如果我们的程序只有 main 函数,它只能做一些基本的事情。如果我要让程序帮我算 3 + 5 等于多少呢?

我可以直接在 main 里写 int result = 3 + 5;,这样当然可以。但如果我有很多地方都要做加法,比如我要算 3+5、10+20、100+200,每一次都要写 3+5、10+20、100+200,太麻烦了。

所以我们定义一个“加法函数”,给它两个数,它帮我们算出结果,然后返回给我们。

int add(int a, int b) {
    return a + b;
}

int add(int a, int b) —— add 是我们给这个函数起的名字;(int a, int b) 是它的参数,意思是“我需要你给我两个整数,我会分别把它们叫做 a 和 b”;前面的 int 表示这个函数会返回一个整数结果。

return a + b; —— 这是函数的核心工作:把 a 和 b 加起来,然后把结果返回给调用它的地方。

现在我们在 main 里调用它:

int main() {
    int result = add(3, 5);
    return 0;
}

这一行的意思是:调用 add 函数,把 3 和 5 传给它。add 函数算出 3+5=8,然后把 8 返回给 main 函数,存到 result 这个变量里。

2.4 问题来了:如果我想加小数呢?

好,现在我们的加法函数写好了,它只能处理整数。如果我今天想加两个小数,比如 3.5 + 2.7,这个函数还能用吗?

不行。因为 add 的参数是 int 类型,你传 3.5 进去,编译器会把它“截断”成 3,结果就错了。

那怎么办?最简单的办法是——再写一个加法函数,只不过参数改成 double 类型(双精度小数)。

double add_double(double a, double b) {
    return a + b;
}

这个函数叫 add_double,参数是两个 double,返回的也是 double。它在 main 里可以这样调用:

double result2 = add_double(3.5, 2.7);

这样做当然可以。但同学们想一想:如果以后我还要加三个数、四个数,或者加 float、加 long,我是不是要写一堆不同名字的函数?add_three、add_four、add_float……名字越来越长,越来越难记。

C++ 觉得这样太麻烦了。于是 C++ 说:“你不用起那么多名字,我帮你搞定。”

2.5 C++ 的解决方案:函数重载

在 C++ 里,多个函数可以共用一个名字,只要它们的参数类型或数量不同,编译器就能自动区分。

我们把刚才的两个函数改成这样:

int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

现在注意看:有两个函数都叫 add。

第一个 add 接受两个 int,处理整数加法。第二个 add 接受两个 double,处理小数加法。

它们的名字完全相同,但参数类型不同。当你调用 add(3, 5) 时,编译器一看,“哦,传的是整数”,它就自动调用第一个版本。当你调用 add(3.5, 2.7) 时,编译器一看,“哦,传的是小数”,它就自动调用第二个版本。

这就是函数重载——同一个函数名,多种用法,编译器自动帮你匹配最适合的那个版本。

这种写法在 C 语言里是不允许的。C 语言只允许每个函数有唯一的名字。所以如果你写 C 语言,加整数用 add_int,加小数用 add_double,名字永远不一样。这就是 C++ 比 C 更方便的地方之一。

2.6 第二个问题:引用和指针又是什么?

好,函数重载我们讲完了。现在我们来看第二个知识点:引用和指针。

在 main 函数里继续添加代码:

int main() {
    int a = 10;
    int &r = a;   // 引用
    int *p = &a;  // 指针

    r = 20;
    *p = 30;

    return 0;
}

第一行:int a = 10; —— 在内存里找了一个小格子,命名为 a,里面存了数字 10。

第二行:int &r = a; —— 这里的 & 不是取地址,而是声明引用。意思就是:“给 a 起个小名叫 r”。从此以后,你叫 r 或者叫 a,都是同一个人,同一个内存格子。

第三行:int p = &a; —— 这里的 表示声明指针,&a 是取 a 的内存地址。意思就是:“p 这个变量里存的是 a 的内存地址”。

引用和指针的区别,很多书上讲得很复杂。今天我们用一个简单的比喻来理解:

引用:就像你的大名和小名,都是指同一个人。

指针:就像你家门口的门牌号,它不是你家本身,但你可以通过门牌号找到你家。

但是问题又来了。

在电脑 CPU 的眼里,引用和指针真的有区别吗?

书上说它们是不同的。但我们今天不做书呆子,我们要用 IDA 亲眼看一下,在内存里,引用和指针到底长什么样。

3. 编译与反编译:用 IDA 看底层真相

3.1 完整代码展示

经过前面一步步的修改,我们最终的完整代码如下:

#include <iostream>
using namespace std;

int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

int main() {
    int x = 1, y = 2;
    double m = 1.5, n = 2.5;

    add(x, y);      // 调用整数版本
    add(m, n);      // 调用小数版本

    int a = 10;
    int &r = a;     // 引用
    int *p = &a;    // 指针

    r = 20;
    *p = 30;

    int result = add(r, *p);  // r 是 a 的引用,*p 是 a 的值,所以相当于 a+a
    cout << "add(r, *p)的结果是:" << result;
    system("pause");

    return 0;
}
3.2 函数入口:建立栈帧
push    ebp
mov     ebp, esp
and     esp, 0FFFFFFF0h
sub     esp, 40h
call    ___main
指令 含义 通俗解释
push ebp 把旧的 ebp 值压入栈中保存 保存现场,等函数结束时要恢复
mov ebp, esp 把当前栈顶地址赋给 ebp 建立新的栈帧基址,以后用 ebp+偏移 来访问参数
and esp, 0FFFFFFF0h 把 esp 按 16 字节对齐 编译器优化,让内存访问更快
3.3 整数加法:第一次调用 add(int, int)
mov     [esp+40h+var_4], 1        ; x = 1
mov     [esp+40h+var_8], 2        ; y = 2

mov     eax, [esp+40h+var_8]      ; 把 y(2)取到 eax
mov     dword ptr [esp+40h+var_40+4], eax  ; 第2个参数 = 2
mov     eax, [esp+40h+var_4]      ; 把 x(1)取到 eax
mov     dword ptr [esp+40h+var_40], eax   ; 第1个参数 = 1
call    __Z3addii                  ; 调用 add(int, int)

C++ 的函数调用约定(__cdecl)是从右往左压参数:先压 y,再压 x。

3.4 小数加法:第二次调用 add(double, double)
fld     ds:dbl_488000              ; 加载常量 1.5
fstp    [esp+40h+var_10]           ; m = 1.5
fld     ds:dbl_488008              ; 加载常量 2.5
fstp    [esp+40h+var_18]           ; n = 2.5

注意:double 是浮点数,用的是 FPU 指令(fld、fstp),而不是普通的 mov。

fld     [esp+40h+var_18]           ; 加载 n
fstp    [esp+40h+var_38]           ; 第2个参数 = n
fld     [esp+40h+var_10]           ; 加载 m
fstp    [esp+40h+var_40]           ; 第1个参数 = m
call    __Z3adddd                  ; 调用 add(double, double)
fstp    st                         ; 清理 FPU 栈

Z3adddd 和 Z3addii 名字不同,这就是函数重载的底层实现!

3.5 核心对比:引用 vs 指针
mov     [ebp+a], 0Ah        ; a = 10
lea     eax, [ebp+a]        ; 取 a 的地址
mov     [ebp+r], eax        ; r = a 的地址
lea     eax, [ebp+a]        ; 再次取 a 的地址
mov     [ebp+p], eax        ; p = a 的地址

注意看:r 和 p 的赋值方式完全相同——都是 lea eax, [ebp+a],然后把 eax 存入对应的变量,两条汇编指令完全一样!

mov     eax, [ebp+r]        ; 取出 r 的值(a 的地址)
mov     dword ptr [eax], 14h ; r = 20
mov     eax, [ebp+p]        ; 取出 p 的值(a 的地址)
mov     dword ptr [eax], 1Eh ; *p = 30

mov     eax, [ebp+p]        ; 取出 p 的值
mov     edx, [eax]          ; *p = 30
mov     eax, [ebp+r]        ; 取出 r 的值
mov     eax, [eax]          ; r = 30
mov     [esp+4], edx        ; 第2个参数 = 30
mov     [esp], eax          ; 第1个参数 = 30
call    __Z3addii           ; add(30, 30) = 60
mov     [ebp+result], eax   ; result = 60

无论是通过 r 还是 p 修改,都是先取出地址,然后向该地址写入值。方式完全一致。

所以,引用本质上就是指针,只是语法上更安全而已。

4. 黑客视角:修改程序逻辑(实战演示)

我们看完了引用和指针的底层原理。现在,如果我是一个黑客,不想改源代码,能不能直接修改这个程序的行为?

大家看,我们程序里有一个 add 函数,它返回两个数的和。之前 add(r, *p) 返回的是 30 + 30 = 60。

但如果我们不修改源代码,直接在 IDA 里改掉它的参数,让它返回别的值呢?

在 IDA 中,找到修改 a 的指令。我们找到了这一行:

mov     dword ptr [eax], 1Eh ; *p = 30(1Eh = 30)

选中这行指令,选择 Edit -> Patch program -> Assemble,把 1Eh 改成 0Ah(10):

mov     dword ptr [eax], 0Ah ; *p = 10

然后选择 Edit -> Patch program -> Apply patches to input file,保存修改后的文件。

改完之后,我们再运行修改后的程序——add(r, *p) 的结果从 60 变成了 20!

同学们看到了吗?我们没改一行源代码,只是修改了内存中的一个值,就改变了程序的执行结果。

这就是逆向分析的力量——你能看到程序在内存中的真实样子,你就能控制它。

5. 总结

好,我们来回顾一下今天的内容。

第一,我们学会了 C++ 的函数重载——它的底层原理是编译器根据参数类型给函数改名字,add(int,int) 变成了 Z3addii,add(double,double) 变成了 Z3adddd。

第二,我们用 IDA 看到了引用和指针在底层没有任何区别——它们都是存地址,然后通过地址访问内存。

第三,我们动手演示了如何在不改源代码的情况下,通过修改内存值改变程序行为——这就是逆向分析的实战应用。

今天的课就到这里,谢谢大家。

0x3附录:C++ 语法速查卡片

下面是本节课用到的 C++ 语法点,按出现顺序排列,方便你在写代码时随时参考。

1. 头文件 #include

#include <iostream>

作用:引入标准输入输出库

记忆点:写代码前先“请工具书”

2. 命名空间 using namespace std;

using namespace std;

作用:使用 std 命名空间中的工具

记忆点:打开工具箱,随便拿

3. 主函数 main()

int main() {
    return 0;
}

作用:程序的入口点

记忆点:没有 main 函数,程序不知道从哪里开始

4. 函数定义

int add(int a, int b) {
    return a + b;
}

结构:返回值类型 函数名(参数列表) { 函数体 }

记忆点:把重复代码打包,取个名字

5. 函数重载

int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }

作用:同名不同参,编译器自动选

记忆点:同名不同参,编译器自动选

6. 引用 &

int &r = a;  // r 是 a 的引用

作用:给变量起别名

记忆点:大名小名,都是同一个人

7. 指针 *

int *p = &a;  // p 指向 a

作用:存地址

记忆点:门牌号 vs 房子本身

8. 输出 cout

cout << "Hello" << 123;
作用:在控制台打印内容

9. 报错的解决方案

报错信息 可能原因 解决办法
undefined reference to 函数只声明没实现 检查函数名是否拼写正确
expected ';' before 'return' 上一行忘了分号 补上 ;
'cout' was not declared 忘了 #include <iostream> 补上头文件
cannot convert 'double' to 'int' 传参类型不匹配 检查函数参数类型

0x4学习资料

菜鸟教程 C++ 参考:https://www.runoob.com/cplusplus/cpp-tutorial.html

通过网盘分享的文件:课程笔记和视频
链接: https://pan.baidu.com/s/1tj-PHoph0ecQ9eXzvRoq2w?pwd=gd7w 提取码: gd7w

PS:由于电脑硬件不行,没找到合适的投屏软件和录屏软件,视频是分4段录的,顺序观看即可,要是能激起坛友们学习破解的兴趣就更好了。比较紧张,讲的不对地方也欢迎大家批评指正!

免费评分

参与人数 16威望 +1 吾爱币 +35 热心值 +13 收起 理由
Hmily + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
ys1312 + 1 + 1 我很赞同!
池塘春草 + 1 + 1 谢谢@Thanks!
zjf3shadow + 1 + 1 谢谢@Thanks!
Kayay + 1 我很赞同!
zxinyun + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
eeehaha + 1 + 1 谢谢@Thanks!
照片依旧 + 1 + 1 我很赞同!
wshq + 1 + 1 谢谢@Thanks!
buluo533 + 1 + 1 谢谢@Thanks!
carnelian + 1 + 1 谢谢@Thanks!
FZZZP + 1 + 1 用心讨论,共获提升!
2595453382 + 1 谢谢@Thanks!
gcode + 1 + 1 谢谢@Thanks!
o594cql + 1 + 1 谢谢@Thanks!
cioceo + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

ailmail 发表于 2026-6-21 11:27
很好,调理清晰,有实例代码
ldwz 发表于 2026-6-21 12:04
ldwz 发表于 2026-6-21 12:33
fjhscpc 发表于 2026-6-21 15:29
不错的分享,正好学习一下
ldwz 发表于 2026-6-21 18:21
我觉得你用 IDA 反编的,去讲解 C++ 逻辑 是不是误导呀? 毕竟IDA 是反编译。。
TJJFF 发表于 2026-6-21 22:55
支持,讲的很细。正好学学
Kayay 发表于 2026-6-22 13:42
很棒,对初学者很有帮助!
Bigstmart 发表于 2026-6-24 09:35
通俗易懂啊,新手也能看懂了
hcsson 发表于 2026-6-24 10:23
通用可学,可以看看
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-6-26 13:07

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表