先来看一个程序:
/
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 行,这种错误极为愚蠢,但是在某些操作系统下居然执行得挺顺利,至少不会崩溃(一种HP的UNIX操作系统上,可惜笔者没有留意版本号) 。
所以 C 程序严重依赖于,CPU,编译器,链接器,操作系统 。正是因为这种不确定性,所以为了保证你写的程序能在各种环境下运行,或者你想能够在任何环境下 debug 你的 C 程序 。你必须知道可执行文件的格式和操作系统如何加载 。否则当你在介绍自己的时候,只能使用类似:“我是X86平台上,VC6.0集成开发环境下的 C 语言高手” 之类的描述 。颇为尴尬 。
为了说明方便 我们的讨论建立在一套 虚拟的环境上 。当然了这仅限于宏观的讨论,一些具体的例子我会给出我调试所用的环境 。 我们假设 虚拟环境满足下列条件:
1. 足够物理内存
2. 操作系统不允许缺页中断
3. 物理 页面 4K
4. 二级页表映射
5. 4G 虚拟地址空间
6. 操作系统不支持 swap 机制
7. I/O 使用独立的地址空间
8. 有若干通用寄存器 r0,r1,r2,r3,......
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',0x5,0xaaaaaaaa,0xbbbbbbbb,0x0 在 .text 段 。
5. "123456789" 在 .rodata 中
第二个问题,程序在第 8 行会崩溃 。程序为什么会崩溃呢?要回答这个问题 我们要知道可执行程序的加载 。
可执行程序的加载
当操作系统装载一个可执行文件的时候,首先操作系统盘但该文件是否是一个合法的可执行文件 。如果是操作系统将按照段表中的指示为可执行程序分配地址空间 。操作系统的内存管理十分复杂, 我们不在这里讨论 。
就上面的例子来说可执行文件在磁盘中的 layout 如下:(假设程序的 虚拟地址从 0x00400000 开始,该平台的 页面大小是 4K)
+----------------------------------+
| |
| 文件头 |
| |
+----------------------------------+------------------
| .text 描述 | ^
| 虚拟地址起始位置 : 0x00400000 | |
| 占用 虚拟空间大小 : 0x00001000 | |
| 实际大小 : 0x00000130 | |
| 属性 :执行/只读 | |
+----------------------------------+ |
| .rwdata 描述 | |
| 虚拟地址起始位置 : 0x00401000 | |
| 占用 虚拟空间大小 : 0x00001000 |
| 实际大小 : 0x00000004 | 段描述表
| 属性 :读写 | |
+----------------------------------+
| .rodata 描述 | |
| 虚拟地址起始位置 : 0x00402000 | |
| 占用 虚拟空间大小 : 0x00001000 | |
| 实际大小 : 0x0000000A | |
| 属性 :只读 | |
+----------------------------------+ |
| .bss 描述 | |
| 虚拟地址起始位置 : 0x00403000 | |
| 占用 虚拟空间大小 : 0x00001000 | |
| 实际大小 : 0x00000000 | |
| 属性 :读写 | v
+----------------------------------+-----------------
| |
| .text 段 | <- 4K对齐,不满补 0
| |
+----------------------------------+-----------------
|0x5 |
| .rwdata 段 | <- 4K对齐,不满补 0
| |
+----------------------------------+-----------------
|123456789 |
| .rodata 段 | <- 4K对齐,不满补 0
| |
+----------------------------------+-----------------
请注意,.bss 段仅仅有描述,在文件中并不存在 。为什么呢?.bss 专用于存放未初始化的数据 。因为未初始化的数据缺省是 0,所以只需要标记出长度就可以了 。操作系统会在加载的时候为它分配清 0 的 页面 。这种技术好像叫做 ZFOD (Zero Filled On Demand) 。
操作系统首先将文件读入物理 页面中,反正大家就认为操作系统找到了一批空闲的物理 页面,将可执行文件全部装载 。如图:
:
+----------------------------------+ <---- 物理 页面对齐
| |
| .text 段 |
| |
+----------------------------------+
:
:
+----------------------------------+ <---- 物理 页面对齐
|0x5 |
| .rwdata 段 |
| |
+----------------------------------+
:
:
+----------------------------------+ <---- 物理 页面对齐
|123456789 |
| .rodata 段 |
| |
+----------------------------------+
:
:
在物理地址中,这几个段并不连续,顺序也不能保证,甚至如果一个段占用几个 页面的时候,段内的连续性和顺序都不能保证 。实际上 我们也不程序关心在物理内存中的 layout 。只需要 页面对齐即可 。
最后操作系统为程序创建 虚拟地址空间,并建立 虚拟地址-物理地址映射( 虚拟地址的管理十分复杂,反正大就认为映射建好了 。另外:注意 我们的假设,系统不支持缺页机制和 swap 机制,否则没有这么简单) 。然后 我们从 虚拟地址空间看来,程序的 layout 如下图:
+----------------------------------+ 0x00400000
| |
| .text 段 |
| |
+----------------------------------+ 0x00401000
|0x5 |
| .rwdata 段 |
| |
+----------------------------------+ 0x00402000
|123456789 |
| .rodata 段 |
| |
+----------------------------------+ 0x00403000
| |
| .bss 段 |
| |
+----------------------------------+
同时操作系统会根据段的属性设置 页面的属性,这就是为什么通常程序的段是 页面对齐的,因为机器只能以 页面为单位设置属性 。
所以第二个问题自然就有了答案 。程序会 crash 。因为 .rodata 段所属的 页面是只读的 。其实有些编译器会将常量 "123456789" 放在 ".text" 中,其实是一样的,两个段都是只读的,写操作都会导致非法访问,甚至同一种编译器,不同的变异参数,这个常量也会出现在不同的位置 。实际上这个保护由编译器,链接器,操作系统,CPU串通好了,共同完成的 。
/
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 行,这种错误极为愚蠢,但是在某些操作系统下居然执行得挺顺利,至少不会崩溃(一种HP的UNIX操作系统上,可惜笔者没有留意版本号) 。
所以 C 程序严重依赖于,CPU,编译器,链接器,操作系统 。正是因为这种不确定性,所以为了保证你写的程序能在各种环境下运行,或者你想能够在任何环境下 debug 你的 C 程序 。你必须知道可执行文件的格式和操作系统如何加载 。否则当你在介绍自己的时候,只能使用类似:“我是X86平台上,VC6.0集成开发环境下的 C 语言高手” 之类的描述 。颇为尴尬 。
为了说明方便 我们的讨论建立在一套 虚拟的环境上 。当然了这仅限于宏观的讨论,一些具体的例子我会给出我调试所用的环境 。 我们假设 虚拟环境满足下列条件:
1. 足够物理内存
2. 操作系统不允许缺页中断
3. 物理 页面 4K
4. 二级页表映射
5. 4G 虚拟地址空间
6. 操作系统不支持 swap 机制
7. I/O 使用独立的地址空间
8. 有若干通用寄存器 r0,r1,r2,r3,......
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',0x5,0xaaaaaaaa,0xbbbbbbbb,0x0 在 .text 段 。
5. "123456789" 在 .rodata 中
第二个问题,程序在第 8 行会崩溃 。程序为什么会崩溃呢?要回答这个问题 我们要知道可执行程序的加载 。
可执行程序的加载
当操作系统装载一个可执行文件的时候,首先操作系统盘但该文件是否是一个合法的可执行文件 。如果是操作系统将按照段表中的指示为可执行程序分配地址空间 。操作系统的内存管理十分复杂, 我们不在这里讨论 。
就上面的例子来说可执行文件在磁盘中的 layout 如下:(假设程序的 虚拟地址从 0x00400000 开始,该平台的 页面大小是 4K)
+----------------------------------+
| |
| 文件头 |
| |
+----------------------------------+------------------
| .text 描述 | ^
| 虚拟地址起始位置 : 0x00400000 | |
| 占用 虚拟空间大小 : 0x00001000 | |
| 实际大小 : 0x00000130 | |
| 属性 :执行/只读 | |
+----------------------------------+ |
| .rwdata 描述 | |
| 虚拟地址起始位置 : 0x00401000 | |
| 占用 虚拟空间大小 : 0x00001000 |
| 实际大小 : 0x00000004 | 段描述表
| 属性 :读写 | |
+----------------------------------+
| .rodata 描述 | |
| 虚拟地址起始位置 : 0x00402000 | |
| 占用 虚拟空间大小 : 0x00001000 | |
| 实际大小 : 0x0000000A | |
| 属性 :只读 | |
+----------------------------------+ |
| .bss 描述 | |
| 虚拟地址起始位置 : 0x00403000 | |
| 占用 虚拟空间大小 : 0x00001000 | |
| 实际大小 : 0x00000000 | |
| 属性 :读写 | v
+----------------------------------+-----------------
| |
| .text 段 | <- 4K对齐,不满补 0
| |
+----------------------------------+-----------------
|0x5 |
| .rwdata 段 | <- 4K对齐,不满补 0
| |
+----------------------------------+-----------------
|123456789 |
| .rodata 段 | <- 4K对齐,不满补 0
| |
+----------------------------------+-----------------
请注意,.bss 段仅仅有描述,在文件中并不存在 。为什么呢?.bss 专用于存放未初始化的数据 。因为未初始化的数据缺省是 0,所以只需要标记出长度就可以了 。操作系统会在加载的时候为它分配清 0 的 页面 。这种技术好像叫做 ZFOD (Zero Filled On Demand) 。
操作系统首先将文件读入物理 页面中,反正大家就认为操作系统找到了一批空闲的物理 页面,将可执行文件全部装载 。如图:
:
+----------------------------------+ <---- 物理 页面对齐
| |
| .text 段 |
| |
+----------------------------------+
:
:
+----------------------------------+ <---- 物理 页面对齐
|0x5 |
| .rwdata 段 |
| |
+----------------------------------+
:
:
+----------------------------------+ <---- 物理 页面对齐
|123456789 |
| .rodata 段 |
| |
+----------------------------------+
:
:
在物理地址中,这几个段并不连续,顺序也不能保证,甚至如果一个段占用几个 页面的时候,段内的连续性和顺序都不能保证 。实际上 我们也不程序关心在物理内存中的 layout 。只需要 页面对齐即可 。
最后操作系统为程序创建 虚拟地址空间,并建立 虚拟地址-物理地址映射( 虚拟地址的管理十分复杂,反正大就认为映射建好了 。另外:注意 我们的假设,系统不支持缺页机制和 swap 机制,否则没有这么简单) 。然后 我们从 虚拟地址空间看来,程序的 layout 如下图:
+----------------------------------+ 0x00400000
| |
| .text 段 |
| |
+----------------------------------+ 0x00401000
|0x5 |
| .rwdata 段 |
| |
+----------------------------------+ 0x00402000
|123456789 |
| .rodata 段 |
| |
+----------------------------------+ 0x00403000
| |
| .bss 段 |
| |
+----------------------------------+
同时操作系统会根据段的属性设置 页面的属性,这就是为什么通常程序的段是 页面对齐的,因为机器只能以 页面为单位设置属性 。
所以第二个问题自然就有了答案 。程序会 crash 。因为 .rodata 段所属的 页面是只读的 。其实有些编译器会将常量 "123456789" 放在 ".text" 中,其实是一样的,两个段都是只读的,写操作都会导致非法访问,甚至同一种编译器,不同的变异参数,这个常量也会出现在不同的位置 。实际上这个保护由编译器,链接器,操作系统,CPU串通好了,共同完成的 。
所以说计算机有些具体问题并没有一定之规,但是他们基本的原理是一样的。我们掌握了基本原理,具体问题可以具体分析。
来自:http://ido.3mt.com.cn/Article/200701/show602577c7p1.html