1可执行文件的格式
在UNIX传统的操作系统中,所有编译生成的输出文件都缺省地使用同一个名字a.out,在现代操作系统中,a.out格式的可执行文件是链接器的输出,而不是汇编程序的输出(在计算机的远古时代,a.out是汇编器的输出,那个时候还没有链接器)。
目标文件和可执行文件有几种不同的格式,大多数都采用了一种ELF的格式,更多的格式可以使用下面的命令来查看(有的系统中可能找不到手册):
$ man a.out
你可以在linux系统中,对编译链接生成的可执行文件使用:
$ file可执行文件名
可以看到他的输出,说明这是一个ELF格式的可执行文件:
yuanlu@bear-labpc:~/workspace/wifi/wifi_hello$ file hello.ko
hello.ko: ELF 32-bit LSB relocatable, ARM, version 1 (SYSV), not stripped
2 UNIX中段的概念
在NUIX系统中,有很多的不同格式,但他们都有一个共同的概念—段。
所谓的段是目标文件的概念,一个目标文件有多个段,他们是二进制文件中简单的的区域,里面保存了和某种特定类型相关的所有信息,如:符号表条目。这里不要把UNIX和Intel X86中的段概念混淆,后者中的段表示一种内存模型的设计结果,在这种设计中,地址空寂并非一个整体,而是分成一些固定大小的区域,称之为段。
对于一个目标文件,运行size命令可以告诉你这个文件的三个段的大小。这三个段分别是:代码段(文本段),数据段和bss段:
text data bss dec hex filename
224 300 0 524 20c hello.ko
这里对三个段中存放的内容做一个说明:
(1) 文本段:即代码段,存放要执行的指令代码
(2) 数据段:存放全局或静态的已经初始化的数据变量
(3) bss段:存放全局或静态的尚未初始化的数据变量。由于BSS段只保存没有值得变量,所以事实上他并不需要保存这些变量的映像,而只是将BSS段在运行时需要的大小记录在目标文件中,因此BSS段并不占据目标文件的任何空间。
需要注意的是一个a.out可执行文件的组成是下面这个样子的:
a.a.out神奇数字
b.a.out的其他内容
c.BSS数据段所需大小
d.数据段(初始化后的全局和静态变量)
e.文本段(可执行文件的指令)
其中文件的头部有一个a.out的神奇数字,它是一种能够确认一组随机的二进制位集合的什么数字,我们暂且不用理会;对于局部变量而言,它不存在a.out中而是在运行时创建。下面我们借用号称最简单的hello world驱动来证明上面的内容的正确性。
1 #include
2 #include
3
4 MODULE_LICENSE("GPL");
5
6 /* the init function*/
7 static __init int hello_init(void)
8
9 {
10
11 printk(KERN_WARNING "Hello world !/n");
12
13 return 0;
14 }
15
16 /* the distory function*/
17 static __exit void hello_exit(void)
18 {
19 printk(KERN_WARNING "Goodbye!/n");
20 }
21
22 module_init(hello_init);
23 module_exit(hello_exit);
编译生成hello.ko可执行文件后,使用size hello.ko命令查看每个段的使用情况:
text data bss dec hex filename
224 300 0 524 20c hello.ko
现在的bss段的内容为空,这里的0表示bss段的大小,data段的大小为300(单位都是字节)。我们增加分别两个全局和静态已经初始化的变量和未初始化的变量,在函数中也增加一个大数组的声明:
1 #include
2 #include
3
4 MODULE_LICENSE("GPL");
5
6 int i;
7 int m = 2;
8
9 static int j;
10 static int n = 1;
11
12 /* the init function*/
13 static __init int hello_init(void)
14
15 {
16 printk(KERN_WARNING "Hello world !/n");
17
18 return 0;
19 }
20
21 /* the distory function*/
22 static __exit void hello_exit(void)
23 {
24 printk(KERN_WARNING "Goodbye!/n");
25 }
26
27 module_init(hello_init);
28 module_exit(hello_exit);
编译后使用size工具:
text data bss dec hex filename
224 300 8 532 214 hello.ko
这里发现bss段的大小变成了8,data段却没有增加,通过后面的继续求证发现这样一个事实:
(1)对于函数内部的局部变量没有存放在可执行文件里
(2)对于全局变量而言,未初始化的变量存放在bss段,初始化的变量分两种:第一,如果被初始化为0,仍然被放在bss段,否则放在数据段(这一点有点不同哦)
(3)对于静态全局变量而言,不管有没有初始化,都不存放在可执行文件中
如果说前面两点很好理解的话,俺么对于第三点的实际表现与理论上的比较大的出入,有待后面继续求证,这里只能猜测是编译器的差异(我的环境是交叉编译环境)。
3 a.out的内存布局
段可以被方便地映射到连接器在运行时可以直接载入的对象中,载入器只是去可执行文件的每一个段的一个映像,本质上段就是正在执行的程序中的一块内存区域。连接器把每个段从文件拷贝到内存中,一般使用mmap()系统调用。
下面是可执行文件中的段在内存中的布局图:
堆栈段
空洞
a.a.out神奇数字
b.a.out的其他内容
c.BSS数据段所需大小----------------------------------------> BSS段
d.数据段(初始化后的全局和静态变量)-------------------->数据段
e.文本段(可执行文件的指令) --------------------> 文本段
代码段包含要执行的指令,数据段包含经过初始化的全局和静态变量以及他们的值(可能不同的编译器的某些特性稍有差异),BSS段的大小从可执行文件的bss段得到,紧跟在数据段之后,当bss内采取进入程序的地址空间后全部清0,数据段和BSS段统称数据区。
这些区域还不足以满足一个程序的所有需求,因为一个进程还需要保存局部变量、临时变量、传递到函数中的参数以及返回值等,堆栈段就是用于这个目的。当然也少不了堆空间,用于满足进程的动态分配内存的需求。
要注意的是一个用户进程的虚拟地址空间最低部分并未被映射,就是说在进程的最低虚拟地址的几K字节没有做页表映射(赋予物理地址),这样它可以用于捕捉空指针和小整形值得指针引用内存情况。
如果考虑共享库,进程的地址空间图如下:
堆栈段
|
连接器
共享库数据段
共享库文本段
可执行程序数据段
可执行程序文本段
4 C运行时系统的作为
C语言怎样组织正在执行的程序的数据结构是这篇文章的重点,运行时的数据包括:堆栈、活动记录和数据等。下面逐一分析:
(1) 堆栈段
运行时系统维护一个指针sp,这就是堆栈指针,它指向堆栈当前的顶部位置,堆栈具有“先进先出”的特性,他的主要用途有三个:
A为函数内部声明的局部变量提供存储空间,这些变量被称为自动变量
B在函数调用时,存储于此有关的维护性信息。它可能包括:函数调用地址(用户返回)、任何不便装入寄存器的参数和一些寄存器值得保护(现场的保护和恢复)。
C作为暂时存储区,如在做很长的计算时将计算中间值压栈,需要时在取出来。
这里要注意的是:根据计算机架构和操作系统不同,堆栈的位置也可能不同,但大多数处理器中堆栈是向下增长的。
(2) 过程活动记录
C语言自动提供服务之一就是跟踪函数调用链,解决这个问题的经典机制就是堆栈中的过程活动记录,使用堆栈记录也是由它的特性决定的。过程活动记录是一种数据结构,用于支持过程调用并记录调用结束以后返回调用点所需的全部信息。
下面是过程活动记录的规范描述:
这个描述具有说明性,先后顺序和大小随着编译器的不同也不同,有的过程活动记录很大,他提供了保持寄存器窗口的空间。运行时系统维护fp指针,用于提示活动堆栈结构(最靠近堆栈顶部的过程活动记录的地址)。
下面图示程序执行不同点是堆栈中活动记录的情况:
(3)数据
如果一位程序员编写如下代码,你会怎么说:
Char * my_func()
{
Char str[] = “my string!”;
Return str;
}
你觉得他能够正常的使用返回的字符串吗?
答案当然是否定的,对上面堆栈怎样实现函数调用的描述时页同时解释了这个问题的答案:不能从函数中返回一个指向函数局部自动变量的指针,因为他在函数返回时过程活动记录已经被系统回收。
如果非要从一个函数返回一个字符串指针怎么办?那你就使用static限定符吧,因为它能保证该变量被保存在数据段而不是堆栈段,该变量的生命期就和程序一样了,不过要记得一点:下次在进入该函数时,不会对该变量重新声明和初始化,而是直接使用上次保存的内容,就是说该static变量只被初始化一次而可以使用多次。