在做软件开发时,不得不知道的一个过程——由源文件通过编译器生成可执行文件,所以首先回味这一过程:
源文件(在文本编辑器中编写)---预处理(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”了,一篇“又臭又长”的文章就这样放在了这里,希望对有可能需要的人有一点点帮助就值了。