囫囵C语言(一):可执行文件的结构和加载

看到这个标题很多人可能想,老家伙老糊涂了。可执行文件的结构和 C 语言有什么关系。          我们先来看一个程序:          /////////////////////////////////////////////////////////////////

    int global_a = 0x5;                /* 01 */     int global_b;                        /* 02 */                                             /* 03 */     int main()                           /* 04 */     {                                      /* 05 */         char *q = "123456789";    /* 06 */                                            /* 07 */         q[3] = 'A';                      /* 08 */                                            /* 09 */         global_a = 0xaaaaaaaa;   /* 10 */         global_b = 0xbbbbbbbb;   /* 11 */                                            /* 12 */         // strcmp(q, NULL);         /* 13 */         return 0x0;                    /* 14 */     }                                     /* 15 */

    几个问题:

    1. 你能说出程序中出现的变量和常量在可执行程序的哪个段中么?     2. 程序运行的结果是什么?     /////////////////////////////////////////////////////////////////     能正确回答上面问题者,此节可以跳过不读:          那么第一个问题的答案是什么呢?

    不知道!

    因为没讲明目标CPU,编译器,链接器。    

    第二个问题的答案是什么呢?

    还是不知道!

    因为讲明链接器,链接参数,目标操作系统。     比如 "123456789" 在某些编译环境下出现在 ".text" 中,某些编译环境下出现在 ".data" 中。     比如,如果用 VC6.0 环境,编译时加上 /GF 选项,该程序会崩溃(第 8 行)。     再比如第 13 行,这种错误极为愚蠢,但是在某些操作系统下居然执行得挺顺利,至少不会崩溃(一种HPUNIX操作系统上,可惜我没有留意版本号)。          所以C程序严重依赖于,CPU,编译器,链接器,操作系统。正是因为这种不确定性,所以为了保证你写的程序能在各种环境下运行,或者你想能够在任何环境下 debug 你的 C 程序。你必须知道可执行文件的格式和操作系统如何加载。否则当你在介绍自己的时候,只能使用类似:我是X86平台上,VC6.0集成开发环境下的 C 语言高手之类的描述。颇为尴尬。

    为了说明方便我们的讨论建立在一套虚拟的环境上。一些具体的例子我会给出我调试所用的环境。我们假设虚拟环境满足下列条件:     1. 足够物理内存     2. 操作系统不允许缺页中断     3. 物理页面 4K     4. 二级页表映射     5. 4G 虚拟地址空间     6. 操作系统不支持 swap 机制     7. I/O 使用独立的地址空间     8. 有若干通用寄存器 r0r1r2r3......     9. 函数的返回值放在 r0     10. CPU          过于古老的文件结构我们不提(入门的格式请参考 a.out 格式)。现在比较常用的文件格式是 ELF PE/COFF。嵌入式方面 ELF 比较主流。     可执行文件基本上的结构如下图:          +----------------------------------+     |                                                |     |              文件头                          |     |                                                |     +----------------------------------+     |                                                |     |              段描述表                        |     |                                                |     +----------------------------------+     |                                                |     |               1                             |     |                                                |     +----------------------------------+     |                                                |     |                       :                        |     |                                                |     +----------------------------------+     |                                                |     |               n                             |     |                                                |     +----------------------------------+     其中这些段中常见的段有 .text.rodata.rwdata.bss。还有一些段因为编译器和文件格式有细微差别我们不再一一说明。     参考:1. Executable and Linkable Format Specification             2. PE/COFF Sepcification                  .text:正文段,也称为程序段,可执行的代码             .rodata:只读数据段,存放只读数据             .rwdata:可读写数据段,             .bss段:未初始化数据 (下文详述)          有了虚拟的环境就好蒙了:就上面的例子来说,我们先回答第一个问题:     1. a .rwdata     2. b .bss     3. q 程序运行的时候从 stack 中分配     4. 'A'0x50xaaaaaaaa0xbbbbbbbb0x0 .text 段。     5. "123456789" .rodata     第二个问题,程序在第 8 行会崩溃。程序为什么会崩溃呢?要回答这个问题我们要知道可执行程序的加载。     可执行程序的加载          当操作系统装载一个可执行文件的时候,首先操作系统判断该文件是否是一个合法的可执行文件。如果是,操作系统将按照段表中的指示为可执行程序分配地址空间。操作系统的内存管理十分复杂,我们不在这里讨论。     就上面的例子来说可执行文件在磁盘中的 layout 如下:(假设程序的虚拟地址从 0x00400000 开始,该平台的页面大小是 4K           +----------------------------------+      |                                                |      |                   文件头                     |      |                                                |      +----------------------------------+------------------      | .text 描述                                   |         ^      | 虚拟地址起始位置 : 0x00400000       |          |      | 占用虚拟空间大小 : 0x00001000       |          |      | 实际大小 : 0x00000130                 |           |      | 属性 :执行/只读                          |           |      +----------------------------------+          |      | .rwdata 描述                               |          |      | 虚拟地址起始位置 : 0x00401000       |          |      | 占用虚拟空间大小 : 0x00001000       |      | 实际大小 : 0x00000004                 |      段描述表      | 属性 :读写                                |       +----------------------------------+         |      | .rodata 描述                               |          |      | 虚拟地址起始位置 : 0x00402000      |           |      | 占用虚拟空间大小 : 0x00001000      |           |      | 实际大小 : 0x 0000000A                 |           |      | 属性 :只读                                |           |      +----------------------------------+          |      | .bss 描述                                    |          |      | 虚拟地址起始位置 : 0x00403000       |           |      | 占用虚拟空间大小 : 0x00001000       |           |      | 实际大小 : 0x00000000                  |          |      | 属性 :读写                                 |          v      +----------------------------------+-----------------      |                                                |      | .text                                      | <- 4K对齐,不满补 0 (大部分但不一定4K对齐)      |                                                |      +----------------------------------+-----------------      |0x5                                           |      | .rwdata                                  | <- 4K对齐,不满补 0(大部分但不一定4K对齐)      |                                                |      +----------------------------------+-----------------      |123456789                                  |      | .rodata                                   | <- 4K对齐,不满补 0(大部分但不一定4K对齐)      |                                                |      +----------------------------------+-----------------          请注意,.bss 段仅仅有描述,在文件中并不存在。因为.bss 专用于存放未初始化的数据。未初始化的数据缺省是0,所以只需要标记出长度就可以了。操作系统会在加载的时候为它分配清0的页面。这种技术好像叫做 ZFODZero Filled On Demand)。     操作系统首先将文件读入物理页面中(物理页面的管理比较复杂,不属于本文讨论的范围),反正大家就认为操作系统找到了一批空闲的物理页面,将可执行文件全部装载。如图:      :      +----------------------------------+ <---- 物理页面对齐      |                                                |      |                   .text                    |      |                                                |      +----------------------------------+      :      :      +----------------------------------+ <---- 物理页面对齐      |0x5                                           |      | .rwdata                                  |      |                                                |      +----------------------------------+      :      :      +----------------------------------+ <---- 物理页面对齐      |123456789                                  |      | .rodata                                   |      |                                                |      +----------------------------------+      :      :     在物理地址中,这几个段并不连续,顺序也不能保证,甚至如果一个段占用几个页面的时候,段内的连续性和顺序都不能保证。实际上我们也不程序关心在物理内存中的 layout。只需要页面对齐即可。     最后操作系统为程序创建虚拟地址空间,并建立虚拟地址-物理地址映射(虚拟地址的管理十分复杂,反正大就认为映射建好了。另外:注意我们的假设,系统不支 持缺页机制和 swap 机制,否则没有这么简单)。然后我们从虚拟地址空间看来,程序的 layout 如下图:           +----------------------------------+ 0x00400000      |                                                |      |                .text                       |      |                                                |      +----------------------------------+ 0x00401000      |0x5                                            |      | .rwdata                                   |      |                                                 |      +----------------------------------+ 0x00402000      |123456789                                  |      | .rodata                                   |      |                                                |      +----------------------------------+ 0x00403000      |                                                |      | .bss                                       |      |                                                |      +----------------------------------+          同时操作系统会根据段的属性设置页面的属性,这就是为什么通常程序的段是页面对齐的,因为机器只能以页面为单位设置属性。     所以第二个问题自然就有了答案。程序会崩溃。因为 .rodata 段所属的页面是只读的。其实有些编译器会将常量 "123456789" 放在 ".text" 中,其实是一样的,两个段都是只读的,写操作都会导致非法访问,甚至同一种编译器,不同的变异参数,这个常量也会出现在不同的位置。实际上这个保护由编译 器,链接器,操作系统,CPU串通好了,共同完成的。

    (程序会崩溃这点不绝对,如果一个操作系统没有对内存进行保护,程序就不会崩溃)。

    所以说计算机有些具体问题并没有一定之规,但是他们基本的原理是一样的。我们掌握了基本原理,具体问题可以具体分析。

 

你可能感兴趣的:(c,layout,嵌入式,语言,平台,编译器)