CPU眼里的C/C++
上QQ阅读APP看书,第一时间看更新

1.2 CPU眼里的程序运行

●提出问题

你可能编写过很多程序,但你知道自己的代码是如何运行起来的吗?或许你已经知道了标准答案,但也别急于盖棺定论。请跟随阿布一起,站在CPU的视角,一起感受一个真实、有趣的程序启动过程。

●代码分析

打开Compiler Explorer,定义一个全局变量a;然后编写一个简单的main函数;定义一个局部变量b;再申请一段内存,并赋值为:0x1122334;最后,分别打印变量a、b、c和main函数的内存地址。让我们看一下,编译得到的可执行文件a.out的运行结果,如图1-5所示。

发现问题了吗?虽然a、b、c 3个变量,几乎是依次、连续定义的。但它们的内存地址,却相隔很远!后面我们会解释原因,此时,我们只要记住运行结果就好:a、b、c 3个变量和main函数,分别存储在4个不同的内存区域里面。

好了,代码写完了,a.out也运行起来了。但代码跟这个可执行文件a.out又有什么关系呢?让我们打开a.out文件,再抽出右边的CPU指令,如图1-6所示。

图1-5

图1-6

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

图1-7

所以,千万不要把密码写到你的代码里面了。至此,我们已经完全可以信任a.out文件了,它存储着我们所写代码对应的CPU指令和数据。注意,CPU是无法直接运行我们的C/C++源代码的,它只能运行CPU指令,但CPU又怎么执行硬盘上的a.out文件呢?

不同于直接把a.out文件加载到真实的计算机内存里面。相反,现代操作系统,会基于物理内存和MMU协处理器,为给我们构建一个巨大的虚拟内存,如图1-8所示。

图1-8

这可以帮助程序员,编写出超越物理内存限制的代码,至于这些虚拟内存最终会被MMU映射到哪块真实的物理内存上?这里并不做具体讨论,感兴趣的同学,可以参看5.1节“CPU眼里的虚拟内存”。

万事俱备,现在可以把a.out文件从硬盘上加载到虚拟内存里面了,让我们再深入内存块的内部,看看更多的细节,如图1-9所示。

图1-9

如你所见,内存地址由低到高分别存放着main函数的CPU指令,我们称这个区域为“代码段”;随后的内存区域,存放着全局变量a的值,我们称这个区域为“数据段”;经过更长的一段距离后,来到.heap内存区域,在程序运行起来以后,会存储数值0x11223344,我们称这个区域为“堆”(heap)。

而在最上面的内存区域.stack,则存放着变量b和c的值,没错,这个区域就是我们常说的“堆栈”(stack)。不过,由于程序还没有运行起来,变量b和c的值,还没有被main函数赋值,因此,它们现在的值可能是随机的。

至此,我们的程序加载过程就基本完成了。但尽管完成了程序的加载,我们的程序依旧没有运行机会。为此,操作系统还会为我们的程序建立一个叫作“进程”的数据结构,并存储在特定的内存区域里面。其中决定程序运行的信息就是:线程的“上下文”。

简单地说,“上下文”就是CPU的寄存器状态,详情可以参看:5.8节“CPU眼里的上下文”,简单起见,我们就让rip寄存器值等于main函数的首地址,如图1-10所示。

图1-10

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

图1-11

至此,代码的编译、加载、运行,全部完成!

●总结

(1)程序的源代码,在经过编译后,会根据源代码的意义分析出代码、数据等信息,存放在可执行文件上;如果不加调试信息的话,变量和函数的名称是不需要存储的。

(2)当计算机加载可执行文件的时候,会把代码、数据从可执行文件拷贝到不同的内存区域里面;同时,也会分配“堆”和“堆栈”的内存区域,但在程序运行之前,“堆”和“堆栈”里面的内容是不确定的。

(3)“堆”和“堆栈”之间有着巨大的内存空白,这让“堆”和“堆栈”有了充分的生长空间,虽然看上去非常浪费(如图1-12所示),但那仅仅是虚拟内存视角上的空白,只有在真正读写这段内存时,操作系统才会为其映射真正的物理内存,而且是用多少,映射多少。

图1-12

●热点问题

Q1:既然“堆”和“堆栈”都是在程序运行时,程序用到多少,就会分配多少。那会不会随着程序的运行,可执行程序.exe所占据的内存会越来越大?

A1:是的,可执行程序.exe在运行的时候,内存确实有不断增大的风险。风险1:内存泄露,不断地通过malloc/new在“堆”上申请内存,但不释放。风险2:函数递归调用,随着调用深度不断加深,“堆栈”也被不断地消耗,直至“堆栈”溢出,程序崩溃(因为操作系统一般不会任由“堆栈”无限制地消耗下去)。如果你注意看自己的Windows任务管理器的话,几乎所有的程序所占据的内存都是变化的,如图1-13所示。

图1-13

Q2:为什么没有BSS段?为什么需要BSS段?

A2:简单起见,这里没有对数据段做更多的细分。BSS段也叫未初始化的数据段,代码中没有初始化的全局变量、静态变量会被存储在这个区域,在程序运行的时候,它们的值会被统一设置成0;因为,这些数据不需要存储初值,所以可以节省.exe文件的大小,对于嵌入式系统而言,则可以节省ROM的空间。