“ 一次真实的代码运行之旅 ”
01
提出问题
你可能编写过很多程序,但你知道自己的代码,是如何运行起来的吗?或许你已经知道了标准答案,但也别急于盖棺定论。请跟随阿布一起,用CPU的视角,一起感受一个真实、有趣的:程序启动过程。
02
代码分析
打开 Compiler Explorer,定义一个全局变量 a;然后,编写一个简单的 main 函数;定义一个局部变量 b;再申请一段内存,并赋值为:0x1122334;最后,分别打印变量 a,b,c 和 main 函数的内存地址。
让我们看一下,编译得到的可执行文件 a.out 的运行结果,如图所示。

发现问题了吗?虽然 a,b,c 3个变量,几乎是依次、连续定义的。但它们的内存地址,却相隔很远!后面我们会解释原因,此时,我们只要记住这个运行结果就好:a,b,c 3个变量和 main 函数,分别存储在 4 个不同的内存区域里面。
好了,代码写完了,a.out 也运行起来了。但代码跟这个可执行文件 a.out 又有什么关系呢?让我们打开 a.out 文件,再抽出右边的:CPU 指令,如图所示:

如你所见,所有的汇编指令,都可以在 a.out 文件中找到。不仅如此,甚至 password 也可以在 a.out 文件中找到,如图1-7所示:

所以,千万不要把密码写到你的代码里面了。至此,我们已经完全可以信任 a.out 文件了,它存储着我们所写代码对应的:CPU指令、和数据。注意,CPU是无法直接运行我们的C/C++源代码的,它只能运行 CPU 指令,但 CPU 又怎么执行硬盘上的 a.out 文件呢?
不同于直接把 a.out 文件,加载到真实的:计算机内存里面。相反,现代操作系统,会基于:物理内存和 MMU 协处理器,为给我们构建一个巨大的虚拟内存,如图所示:

这可以帮助程序员,编写出:超越物理内存限制的代码,至于,这些虚拟内存最终会被 MMU 映射到哪块真实的物理内存上?这里并不做具体讨论,感兴趣的同学,可以参看:CPU眼里的:虚拟内存
万事具备,现在可以把 a.out 文件从硬盘上加载到虚拟内存里面了,让我们再深入到内存块的内部,看看更多的细节,如图所示:

如你所见,内存地址,由低到高,分别存放着:main 函数的 CPU 指令,我们也称这个区域为:代码段;随后的内存区域,存放着全局变量 a 的值,我们也称这个区域为:数据段;经过更长的一段距离后,来到.heap这个内存区域,在程序运行起来以后,会存储数值:0x11223344,我们也称这个区域为 “堆”(heap)。
而在最上面的内存区域.stack,则存放着变量 b 和 c 的值,没错,这个区域就是我们常说的:“堆栈”(stack)。不过,由于程序还没有运行起来,变量 b 和 c 的值,还没有被 main 函数赋值,因此,它们现在的值,可能是随机的。
至此,我们的程序加载过程,就基本完成了。但尽管完成了程序的加载,我们的程序依旧没有运行机会。为此,操作系统还会为我们的程序,建立一个叫做:“进程”的数据结构,并存储在特定的内存区域里面。其中决定程序运行的信息就是:线程的“上下文”。
简单的说,“上下文”就是 CPU 的寄存器状态,详情可以参看:CPU眼里的:上下文 | Context,简单起见,我们就让 rip 寄存器值等于:main 函数的首地址,如图所示:

这样,一旦操作系统进行任务调度,让我们的进程得以执行时,rip 寄存器,就会引导 CPU 去执行a.out文件里面的 main 函数,如图所示:

至此,代码的编译、加载、运行,全部完成!
03
总结
1. 程序的源代码,在经过编译后,会根据源代码的意义,分析出:代码、数据、等信息,存放在可执行文件上;如果不加调试信息的话,变量和函数的名称,是不需要存储的。
2. 当计算机加载可执行文件的时候,会把代码、数据,从可执行文件,拷贝到不同的内存区域里面;同时,也会分配“堆”和“堆栈”的内存区域,但在程序运行之前,“堆”和“堆栈”里面的内容是不确定的。
3. “堆”和“堆栈”之间,有着巨大的内存空白,这让“堆”和“堆栈”有了充分的生长空间,虽然看上去非常浪费, 但那仅仅是虚拟内存视角上的空白,只有在真正读写这段内存时,操作系统才会为其映射真正的物理内存,而且是用多少,映射多少。
04
热点问题
Q1:既然“堆”和“堆栈”都是在程序运行时,程序用到多少,就会分配多少。那会不会随着程序的运行,可执行程序*ex.e**,所占据的内存会越来越大?
A1:是的,可执行程序*ex.e**在运行的时候,内存确实有不断增大的风险。
风险 1:内存泄露,不断的通过 malloc/new 在 “堆” 上申请内存,但不释放;
风险 2:函数递归调用,随着调用深度不断加深,“堆栈”也被不断的消耗,直至“堆栈”溢出,程序崩溃(因为操作系统一般不会任由“堆栈”无限制的消耗下去)。
如果你注意看自己的Windows任务管理器的话,几乎所有的程序,它们所占据的内存都是变化的,如图所示:

Q2:为什么没有BSS段,为什么需要BSS段?
A2:简单起见,这里没有对数据段作更多的细分。BSS段也叫未初始化的数据段,代码中没有初始化的全局变量、静态变量会被存储在这个区域,在程序运行的时候,它们的值会被统一设置成:0;因为,这些数据不需要存储初值,所以可以节省*ex.e**文件的大小,对于嵌入式系统而言,则可以节省ROM的空间。
05
更多知识
如果喜欢阿布这种解读方式,希望更加系统学习这些编程知识的话,也可以考虑看看由 阿布 亲自编写,并有多位 微软大佬 联袂推荐的新书《CPU眼里的C/C++》
《CPU眼里的C/C++》
¥88.9
购买