要运行可执行目标文件 prog,我们可以在 Linux shell 的命令行中输入它的名字:
linux> ./prog
因为 prog 不是一个内置的 shell 命令,所以 shell 会认为 prog 是一个可执行目标文件,通过调用某个驻留在存储器中称为加载器(loader)的操作系统代码来运行它
任何 Linux 程序都可以通过调用 execve函数 来调用加载器

每个 Linux 程序都有一个运行时内存映像

代码段总是从地址 0x400000 处开始
后面是数据段
运行时在数据段之后,通过调用 malloc 库往上增长
堆后面的区域是为共享模块保留的
用户栈总是从最大的合法用户地址 开始,向较小内存地址增长(也就是向下)
栈上的区域,从地址 开始,是为内核(kernel)中的代码和数据保留的,所谓内核就是操作系统驻留在内存的部分


图中,我们把堆、数据和代码段画得彼此相邻,,并且把栈顶放在了最大的合法用户地址处(0x400000)
实际上,由于 .data 段有对齐要求,所以代码段和数据段之间是有间隙的
同时,在分配栈、共享库和堆段运行时地址的时候,链接器还会使用地址空间布局随机化ASLR
虽然每次程序运行时这些区域的地址都会改变,它们的相对位置是不变的

当加载器运行时,它创建类似于下图的内存映像

程序头部表的引导下,加载器将可执行文件的片(chunk)复制到代码段和数据段
接下来,加载器跳转到程序的入口点,也就是 _start 函数的地址,这个函数是在系统目标文件 ctrl.o 中定义的,对所有的 C 程序都是一样的
_start 函数调用系统启动函数 __libc_start_main,该函数定义在 libc.so 中
它初始化执行环境,调用用户层的 main 函数,处理 main 函数的返回值,并且在需要的时候把控制返回给内核

Note

旁注 - 加载器实际是如何工作的?

我们对于加载的描述从概念上来说是正确的,但也不是完全准确,这是有意为之。要理解加载实际是如何工作的,你必须理解进程、虚拟内存和内存映射的概念,这些我们还没有加以讨论。在后面第 8 章和第 9 章中遇到这些概念时,我们将重新回到加载的问题上,并逐渐向你揭开它的神秘面纱。

对于不够有耐心的读者,下面是关于加载实际是如何工作的一个概述:Linux 系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当 shell 运行一个程序时,父 shell 进程生成一个子进程,它是父进程的一个复制。子进程通过 execve 系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk),新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到_start 地址,它最终会调用应用程序的 main 函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到 CPU 引用一个被映射的虚拟页时才会进行复制,此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。