从目标文件结构,加载执行阶段,汇编角度来理解C程序内存分区

Posted 一口Linux

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从目标文件结构,加载执行阶段,汇编角度来理解C程序内存分区相关的知识,希望对你有一定的参考价值。

每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段(segment):

代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。

数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

1 目标文件结构

目标文件是源代码编译但未链接的中间文件(Windows的.obj和Linux的.o),Windows的.obj采用 PE 格式,Linux 采用 ELF 格式,两种格式均是基于通用目标文件格式(COFF,Common Object File Format)变化而来,所以二者大致相同。

目标文件一般包含编译后的机器指令代码、数据、调试信息,还有链接时所需要的一些信息,比如重定位信息和符号表等,而且一般目标文件会将这些不同的信息按照不同的属性,以“节(section)”也叫“段(segment)”的形式进行存储。

#include <stdio.h>
#include <malloc.h>

int gInitVar = 1;           // .data   加载阶段加载
int gUninitVar;             // .bss    加载阶段加载
const int gConstVar = 2;    // .rdata  加载阶段加载
// extern可以修饰const用于扩展文件链接性,const默认是文件内链接的
// static修饰全局变量可以限制其文件链接性,其存储属性不变
void foo(int i)             // .text   加载阶段加载

    static int staticLocalInitVar = 3;  // .data  加载阶段加载
    static int staticLocalUninitVar;    // .bss   加载阶段加载
    int stack_localVar = 4;             // 栈帧(每个程序运行时会加载1-2M栈空间)
    const int LocalConstVar = 5;        // 栈帧(运行时自动分配)
    staticLocalUninitVar = LocalConstVar + gConstVar;
    gUninitVar = staticLocalInitVar + gInitVar;
    int *dynamic_heapData = (int*)malloc(sizeof(int)*10000000);
    dynamic_heapData[10000000-1] = 9;   // 堆区(运行时动态申请)
    i += stack_localVar + staticLocalUninitVar + gUninitVar; 
    printf("%d\\n",i+dynamic_heapData[10000000-1]);// .rdata,加载阶段加载"%d\\n"
    free(dynamic_heapData);             // 堆内存需要显式释放
                                       // 栈内存在超出作用域后自动释放
int main()

    foo(6);
    getchar();
    return 0;
                                       // 加载阶段加载的内存要等到程序结束才释放

文件的内容分割为不同的区块(Setion,又称区段,节等),区段中包含代码数据,各个区块按照页边界来对齐,区块没有限制大小,是一个连续的结构。每块都有他自己在内存中的属性,比如:这个块是否可读可写,或者只读等等。

① .text 代码段

代码段存放程序的机器指令;

② .data 已初始化数据段
初始化数据段存放已初始化的全局变量与局部静态变量;

③ .bss 未初始化数据段

未初始化局部静态变量(或初始化为0的局部静态变量)放到.bss段,对于未初始化全局变量(或初始化为0的全局变量)不同语言与编译器的实现有不同的处理,有的只是在.baa段预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在 .bss 段分配空间。编译器会把未初始化的全局变量标记为一个 COMMON 符号,不为其在 .bss 段分配空间的原因是现在的编译器和链接器支持弱符号机制,即允许同一个弱符号定义在多个目标文件中,因为未初始化的全局变量属于弱符号,编译时无法确定符号大小,所以此时无法在 .bss 段为未初始化的全局变量分配空间。

④ .rdata或.rodata 只读数据段

只读数据段存放程序中只读变量,如const修饰的常量和字符串常量;

单独设立.rodata段的好处有很多,比如语义上支持了C的const常量,而且操作系统在加载的时候可以将.rodata段的内容映射为只读区,这样对于这个段的任何修改都会被判为非法,保证了程序的安全性。

⑤ .symtab 符号表段

.symtab段用于存符号表。每个目标文件都有一个相应的符号表(Symbol Table),记录了目标文件中用到的所有符号。每个符号都有一个对应的值,叫做符号值(Symbol Value),符号值可以是符号所对应的数据在段中的偏移量,也可以是该符号的对齐属性。

链接过程的本质就是要把多个不同的目标文件之间像拼图一样拼起来,相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。比如目标文件B用到了目标文件A中的函数foo,那么称目标文件A定义了函数foo,目标文件B引用了函数foo。定义与引用这两个概念同样适用于变量。每个函数和变量都有自己独一的名字,才能避免链接过程中不同变量和函数之间的混淆。在链接中,我们将函数和变量统称为符号(Symbol),函数或变量名就是符号名(Symbol Name)。

符号是链接的粘合剂,没有符号就无法完成链接。每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值叫做符号值(Symbol Value),对于变量和函数来说,符号值就是它们的地址。

除了函数和变量之外,还存在其它几种不常用到的符号。符号表中的符号可分为全局符号、局部符号、段名、行号等,对于链接而言,只关心全局符号。

⑥ .strtab 字符串表

因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串(在汇编中使用offset)。

7:        char * stringLiteral = "stringLiteral";
00401038   mov         dword ptr [ebp-4],offset string "stringLiteral" (004230c0)

 .rela.text 代码段重定位表

重定位表,也叫作重定位段,用于链接器在处理目标文件时,重定位代码段中那些对绝对地址的引用的位置。比如 .text 段中对外部 printf() 函数的调用。每个要被重定位的地方叫重定位入口(Relocation Entry),OFFSET 表示该入口在所在段中的偏移位置,TYPE 表示重定位入口的类型,VALUE 表示重定位入口的符号名称。

2 加载与执行

项目全部相关文件最终会由链接器链接到一起形成一个可执行文件,Linux 系统中的每个可执行文件都运行在一个进程上下文中,有自己的虚拟地址空间。当shell 运行一个程序时,父shell 进程生成一个子进程,它是父进程的一个复制。子进程通过系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk), 新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main 函数。

一个系统中的进程是与其他进程共享CPU和主存资源的。然而,共享主存会形成一些特殊的挑战。如果太多的进程需要太多的内存,那么它们中的一些就根本无法运行。当一个程序没有空间可用时,那就是它运气不好了。内存还很容易被破坏。如果某个进程不小心写了另一个进程使用的内存,它就可能以某种完全和程序逻辑无关的令人迷惑的方式失败。

为了更加有效地管理内存并且少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟内存(VM)。虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。通过一个很清晰的机制,虚拟内存提供了三个重要的能力:

1) 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。

2) 它为每个进程提供了一致的地址空间,从而简化了内存管理。

3) 它保护了每个进程的地址空间不被其他进程破坏。

虚拟内存是计算机系统最重要的概念之一。它成功的一个主要原因就是因为它是沉默地、自动地工作的,不需要应用程序员的任何干涉。

3 汇编代码分析

看以下源代码与汇编代码的对应,以及数据(变量)对应的地址值:

1:    #include <stdio.h>
2:    #include <malloc.h>
3:
4:    int gInitVar = 1;           // .data   加载阶段加载,所以这里无汇编对应
5:    int gUninitVar;             // .bss    加载阶段加载
6:    const int gConstVar = 2;    // .rdata  加载阶段加载
7:
8:    void foo(int i)             // .text   加载阶段加载
9:    
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,4Ch
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-4Ch]
0040102C   mov         ecx,13h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]
10:       static int staticLocalInitVar = 3;  // .data  加载阶段加载,所以这里无汇编对应
11:       static int staticLocalUninitVar;    // .bss   加载阶段加载
12:       int stack_localVar = 4;             // 栈帧(每个程序运行时会加载若干M栈空间)
00401038   mov         dword ptr [ebp-4],4 // 局部变量保存在栈上,由ebp及其偏移表示
13:       const int LocalConstVar = 5;        // 栈帧(运行时自动分配)
0040103F   mov         dword ptr [ebp-8],5 // 局部常量放在栈区
14:       staticLocalUninitVar = LocalConstVar + gConstVar;
00401046   mov         dword ptr [gUninitVar+4 (00428e40)],7
15:       gUninitVar = staticLocalInitVar + gInitVar;
00401050   mov         eax,[global_data+4 (00425a34)]
00401055   add         eax,dword ptr [gInitVar (00425a30)]
0040105B   mov         [gUninitVar (00428e3c)],eax
16:       int *dynamic_heapData = (int*)malloc(sizeof(int)*10000000);
00401060   push        2625A00h
00401065   call        malloc (00401190)
0040106A   add         esp,4
0040106D   mov         dword ptr [ebp-0Ch],eax
17:       dynamic_heapData[10000000-1] = 9;   // 堆区(运行时动态申请)
00401070   mov         ecx,dword ptr [ebp-0Ch]
00401073   mov         dword ptr [ecx+26259FCh],9
18:       i += stack_localVar + staticLocalUninitVar + gUninitVar;
0040107D   mov         edx,dword ptr [ebp-4]
00401080   add         edx,dword ptr [gUninitVar+4 (00428e40)]
00401086   add         edx,dword ptr [gUninitVar (00428e3c)]
0040108C   mov         eax,dword ptr [ebp+8]
0040108F   add         eax,edx
00401091   mov         dword ptr [ebp+8],eax
19:       printf("%d\\n",i+dynamic_heapData[10000000-1]);// .rdata,加载阶段加载"%d\\n"
00401094   mov         ecx,dword ptr [ebp-0Ch]
00401097   mov         edx,dword ptr [ebp+8]
0040109A   add         edx,dword ptr [ecx+26259FCh]
004010A0   push        edx
004010A1   push        offset string "\\xd4\\xcb\\xd0\\xd0\\xbd\\xd7\\xb6\\xce\\xa3\\xba\\xb6\\xaf\\xcc\\xac\\xc9\\xea\\xc7\\xeb\\
004010A6   call        printf (00403110)
004010AB   add         esp,8
20:       free(dynamic_heapData);             // 堆内存需要显式释放
004010AE   mov         eax,dword ptr [ebp-0Ch]
004010B1   push        eax
004010B2   call        free (00401c10)
004010B7   add         esp,4
21:                                          // 栈内存在超出作用域后自动释放

以上是关于从目标文件结构,加载执行阶段,汇编角度来理解C程序内存分区的主要内容,如果未能解决你的问题,请参考以下文章

深入理解计算机系统-读书笔记

学习总结

c与汇编

计算机系统-概述

从汇编角度来理解linux下多层函数调用堆栈运行状态

程序编译流程