x86系统下的c语言内存分配解析

       在做软件开发时,不得不知道的一个过程——由源文件通过编译器生成可执行文件,所以首先回味这一过程:

        源文件(在文本编辑器中编写)---预处理(gcc -E)---->预处理之后的文件(记作E文件){由于只做替换,所以不可能有错误产生}---编译(gcc -s....记为P1)---->汇编文件(记作s文件)---汇编(gcc -c){由汇编器as实现}---->目标文件(记作o文件)+库文件(静态库[*.a],动态库[*.so])---链接{由链接器ld实现....记为P2}--->可执行文件(记作exe文件)--->加载器(记为P3)---->以一个进程的形式在内存中运行。

      其实,每个过程都是很复杂的,但是由于开发工具的便利,所以我们很少关注其中的实现细节。实际上这才是我们所要的及只关注软件的开发实现,而其他的则由开发工具实现------这就是人与机器的合理配合。不能说完美配合,因为一次编程过程总会或多或少的碰到问题,其表现为开发的程序运行得跟我们的所想不一样,当然最起码要排除一系列语法与逻辑问题。

      如上所述,最容易碰到问题的过程为P1----编译时的语法问题,P2---链接时找不到库的问题,P3---加载时没有库的问题;很明显,P1为我们语法掌握的问题,P2,P3为开发工具的使用问题。如果没有配置,则成功的实现软件开发,及为完美配合。

     说了一些必要的“废”话,只是为了说明最基本的背景知识,而对程序的理解并且保证程序的顺利而高效地运行,不得不去认识程序对内存的使用,以下就从两个方面去描述:

     其实,背景还缺了点,就是从概念上描述程序对内存的需求:1)代码段---由CPU执行的指令部分组成,为只读段(所以可以将其放在ROM内);2)数据段---其又可以分为两部分:a)初始化数据段---它包含程序中明确给定非零初值的全局变量和静态变量,此段在编译就已经确定,大小不可改变,b)非初始化数据段,储存未给定初值或者初值为零的全局变量与静态变量,这个段的内容不在外存中,而是在程序运行时由内核将段中的数据初始化为0(A1);3)栈段---所有的自动变量与函数调用时所需要保存的信息都在其中;4)堆段---存储用户动态申请的内存空间(A2)。

     看了以上的描述,是不是觉得有点枯燥而不好理解呢,所以有必要来说明一下,很明显程序除了代码段是必需的,其他的都可以少,但是一般的程序,使用的段还有栈段,这两个段是很重要的。其他两个是应需而生的,往往有其他不可告人的用途,使用它们麻烦丛生,但是合理的使用它们却能得到更加合理而高效的设计,这就是水平的体现了。注意:A1,A2两处,因为他们会涉及到操作系统的内核的参与。这就让我们开发程序,有了更多的麻烦,因为涉及到了更多的复杂知识。但是不要太在意这些麻烦,因为只要我们合理的使用,它们被操作系统透明地处理。

      看了上面的描述是不是对auto,static有了些更深的认识呢。理论终究是理论,扯得好,终究只能是蛋;实证才是最重要的,让我们感受到它们的存在并且能够利用它们解决问题才是上策。

        第一方面的描述如下

       从ELF头来看背景二的概念性描述,通过对o文件与exe文件使用readelf命令(-a)及可得到elf头:

       .text---就是代码段的信息

       .rodata---只读数据段,存储程序中使用的复杂常量,例如:字符串等

       .data---就是初始化数据段的信息

       .bss---就是未初始化数据段的信息,仅仅是占位符而已,告知操作系统加载时预留一段空间。它的存在是为了提高磁盘存储空间的利用率。

说明:以上的四个段是要在运行时加载进入内存中的,以下的信息在生成exe文件时已经除去了。但是又添加了一些其他信息用于加载时使用(这些不是本文论述的重点,如需了解请参考最后的描述)。

       .symtab---符号表。存储定义和引用的函数和全局变量。每一个可重定位的文件(o文件,*.a,*.so)都有这样的表。为了链接的重定位操作做准备。

       .rel.text---代码段需要的重定位的信息。存储需要靠重定位操作修改位置的符号的汇总。通常是函数名和标号。

       .rel.data---数据段需要重定位的信息。通常是一些全局变量。

 说明:以上三个段是用于链接时的操作。链接主要是完成两个任务:1)符号解析---将目标文件内的引用符号和该符号的定义联系起来;2)重定位:将符号定义和存储器的位置联系起来,修改对这些符号的引用。

       .debug---调试信息。由gcc -g得出,可由gdb进行解析

       .line---源程序的行号映射。由gcc -g得出,可由gdb进行解析。单步执行使用。

       .strtab--字符串表。存储.symtab符号表和.debug符号表的符号名称。

 说明:以上三段是主要用于调试的。

       有了这些,是不是觉得很烦了,但是了解它们对理解程序运行是很有帮助的,能够更好的解决调试与运行时碰到的问题。

       以上内容主要摘录于《linux程序设计大全》一书,评自己的理解进行整理得到,欢迎观众能对以上描述提出批评与建议,如果觉得看了,还不过瘾,就请看原著,得到更深入的理解。

      第二方面的描述如下

      以下从一个由VC追踪的c程序的汇编调试,来解析代码段与栈段,因为其中只涉及这两部分。

1)   int add(int a,int b)
2)   {
3)      int x;
4)       int y;
5 )     x=a;
6)      y=b;
7)       return (a+b);
8) }
9) void main()
10)  {
11)     int add1;
12)     int add2;
13)     int result;
14)     result = add(add1,add2);
15)  }

   由上可以看出这是个纯粹的c程序。将断点设在第10行,则得到以下的main函数的汇编程序。

10:   {
00401060   push        ebp                                   ;保存ebp
00401061   mov         ebp,esp                             ;将esp赋值给ebp,ebp指向了栈底
00401063   sub         esp,4Ch                              ;将esp偏移4Ch个空间,其实,分配了4Ch个单元的栈空间
00401066   push        ebx
00401067   push        esi
00401068   push        edi                                     ;保存必要的寄存器的值及保存CPU的状态
00401069   lea         edi,[ebp-4Ch]  
0040106C   mov         ecx,13h    
00401071   mov         eax,0CCCCCCCCh
00401076   rep stos    dword ptr [edi]                 ;初始化栈空间为CCCCCCCCh,所以有时使用未定义的变量的值为“烫烫烫”
11:       int add1;
12:       int add2;
13:       int result;
14:       result = add(add1,add2);
00401078   mov         eax,dword ptr [ebp-8]        ;注意:a的值在[ebp-8]所指向的空间,及在刚才分配的空间中,因为栈是向小地址(x86为小端处理器)延伸
0040107B   push        eax                                     ;在调用者(这里是主函数)中用栈空间保存传递的参数,注意:传递参数的顺序                                     
0040107C   mov         ecx,dword ptr [ebp-4]
0040107F   push        ecx
00401080   call        @ILT+0(_add) (00401005)     ;一种调用函数的方式(查表法)
00401085   add         esp,8                                    ;将esp偏移8字节,及释放刚才传递的参数。
00401088   mov         dword ptr [ebp-0Ch],eax     ;用eax保存返回值,这就解释了,为什么返回值只有一个
15:   }
0040108B   pop         edi                                        ;恢复寄存器
0040108C   pop         esi
0040108D   pop         ebx
0040108E   add         esp,4Ch                                ;释放栈空间
00401091   cmp         ebp,esp
00401093   call        __chkesp (004010b0)
00401098   mov         esp,ebp
0040109A   pop         ebp                                       ;恢复ebp
0040109B   ret                                                       ;真正的返回
     单步调试可以得到一个函数表如“call        @ILT+0(_add) (00401005)”描述:

@ILT+0(_add):
00401005   jmp         add (00401020)                   ;函数表的一项跳转到add函数中,这里是不是很像中断向量表呢
@ILT+5(_main):                                                     ;函数表的一项跳转到main函数中
0040100A   jmp         main (00401060)
0040100F   int         3                                            ;内存的未定义处的非法访问产生中断3
00401010   int         3
00401011   int         3
00401012   int         3
00401013   int         3

   单步调试又可以得到add函数的汇编程序,如下:

1:    int add(int a,int b)
2:    {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,48h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-48h]
0040102C   mov         ecx,12h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]                      ;以上过程是不是很主函数是一样的呢,以下为主要程序执行过程
3:        int x;
4:        int y;
5:        x=a;
00401038   mov         eax,dword ptr [ebp+8]            ;这里的[ebp+8]指向的空间就是main函数传递的参数a,所以上面传递的顺序很重要
0040103B   mov         dword ptr [ebp-4],eax             ;所以分配的栈空间是连续的,而且只有一个函数的栈段被使用,这就可以解释自动变量的作用范围与可见性
6:        y=b;                                                                ;同样,也可以说明其他数据段存在的意义与其特殊的作用。
0040103E   mov         ecx,dword ptr [ebp+0Ch]
00401041   mov         dword ptr [ebp-8],ecx
7:        return (a+b);
00401044   mov         eax,dword ptr [ebp+8]
00401047   add         eax,dword ptr [ebp+0Ch]
8:    }
0040104A   pop         edi
0040104B   pop         esi
0040104C   pop         ebx
0040104D   mov         esp,ebp
0040104F   pop         ebp
00401050   ret
--- No source file  ---------------------------------------------------------------------------------------------------------------------------------
00401051   int         3

 

          在以上的注释中已经解释很多了,现在需要的就是从以下三个方面来总结以下了:

        第一,子程序调用的过程(其实,c语言就是一堆函数的使用):

       a)分配一段代码段为,函数列表 ---注册每一个声明的函数(函数入口)

       b)每个函数单独分配一段各自的空间       

       c)函数调用者主动用栈来保存传递的参数

       d)函数的调用过程,如解释中详细描述

       e)主函数将esp偏移形参个数*4的空间,释放形参

       f)返回值用EAX实现

      第二,c语言的内存分配,由栈空间的使用,可以形象地解释自动变量的可见性与作用域;反面也说明,在编译过程中要确定数据段的分配的可见性与作用域;至于动态分配的堆,则看操作系统的内存管理方式了(可以去网上搜一下malloc与free函数的实现,其中核心的概念就是内存控制结构与linux的系统调用sbrk())。

      第三,中断过程,其实呢,这个子函数调用过程是类似中断的相应过程。只不过中断是由硬件传入参数的。函数调用与中断调用都要花费额外的操作及花费CPU时间,如果要优化程序就要少调用函数或者优化函数调用的过程。

       到此就应该说声“bye-bye”了,一篇“又臭又长”的文章就这样放在了这里,希望对有可能需要的人有一点点帮助就值了。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值