在有效学习如何使用GDB前,你必须理解框架(frame)。因为它们是组成栈(stack)的框架,所以也被称为调用栈框(call stack frames)。要学习栈,我们需要知道一个执行的程序的内存布局。
每当一个进程被创建时,内核提供一块可以放置在任何地方的内存。然而,通过虚拟内存 (virtual memory,VM)的魔力,进程相信它拥有计算机上的所有内存。你可以已经听说过当RAM用完时使用硬盘空间作为内存的情况下的“虚拟内存”。那也被称为虚拟内存,但是和我们这里说的完全没有关系。
VM由以下原则组成:
1、每个进程被给定被称为进程虚拟内存空间的物理内存。
2、进程不知道它的物理内存的细节(也说是它物理上的地址)。进程所知道的只是内存块有多大,以及它的内存块从地址0开始。
3、每个进程都不知道任何属于进程的VM块。
4、即使进程知道了VM的其它块,它在物理上被阻止访问那块内存。
每次一个进程想要读要者写内存时,它的请求必须从一个VM地址翻译到一个物理内存地址。相反地,当内核需要访问一个进程的VM,它必须把一个物理内存地址翻译成一个虚拟地址。这样做有两个主要问题:
1、计算机持续地访问内存,所以翻译非常普遍。它们必须是及其迅速的。
2、OS如何保证一个进程没有践踏另一个进程的VM?
这两个问题的答案在于OS没有自己管理VM;它从CPU得到了帮助。许多CPU包含一个被称为MMU的设备:内存管理单元(memory management unit)。MMU和OS共同负责管理VM,在虚拟和物理内存之间进行翻译;决定哪些进程被允许访问哪些内存地址,以及控制一个VM空间上区域的读写权限,即使是对于拥有这个空间的进程。
过去Linux通常只能被移植到有一个MMU的架构上,所以说Linux不能在x286上运行。然而,在1998年,Linux被移植到没有MMU的68000上。这也造就了嵌入式Linux和如Palm Pilot(PDA)之类的设备上的Linux。
MMU有
时也称为paged memory management
unit(PMMU),一个计算机硬件组件。它处理CPU所请求的对内存的访问。它的功能包括虚拟内存到物理内存的翻译,内存保护,缓存控制,总线仲裁和
简易计算机架构(特别是8位系统)的bank switching。
MMU把虚拟地址空间(处理器可寻址的范围)分成页,地址的底部n个位
(一个页里的偏移量)被保持不变。更多的地址位是虚拟页号。MMU通常通过一个相关的页表缓存(Translation Lookaside
Buffer,
TLB)来把物理页号翻译成物理页号。当TLB没有缓存翻译时,一个涉及硬件数据结构或软件协助的更慢的机制被使用。在这样的数据结构里找到的数据通常被
称为页表项(page table entries, PTE),而数据结构本身被称为页表。物理页号和页偏移量结合,来给出完整的物理地址。
PTE或TLB项也可能包含页是否被写(dirty bit)的信息,它上次何时被使用(accessed bit,用于最近使用内存替换算法),何种类型的进程(用户模式、超级用户模式)可以读写它,以及它是否应该被缓存。
有
时,一个TLB项或PTE禁止对一个虚拟页的访问,可能因为还没有为那个虚拟页分配RAM。在这种情况下MMU发送一个页面失效的信号给CPU。OS然后
处理这种情况,可能在RAM找到一个空间的框架并建立一个新的PTE来把它映射到所请求的虚拟地址上。如果没有RAM可用,那么它可能必须选择一个已有的
页(称为受害者),使用某种替换算法,把它存储到磁盘(称为换页)。对于一些MMU,PTE或TLB项可能有限,在这种情况下OS必须释放一个来映射新
的。
在一些情况下,一个页面失效可能指明一个软件bug。MMU的一个关键好处是内存保护:OS可以使用它来防护出格的程序,通过禁一个特定程序访问其没有访问权限的内存。通常,一个OS为每个程序分配它自己的虚拟地址空间。
MMU也减少了内存碎片的问题。在各内存块被分配和释放后,释放的内存可能变为碎片(不连续的)以致最大的连续空闲内存比总内存量小得多。使用虚拟内存,一个连续的虚拟内存地址范围可以被映射成多个不连续的物理内存块。
内存布局
每个进程的VM空间以一个相似的可预测的方式排列:
High AddressArgs and env vars
Stack
|
V
Unused memory
^
|
Heap
Uninitialized Data Segment (bss)
Initialized Data Segment
Low AddressText Segment这个布局也没什么好再说的了。
写一个最简单的程序:int main(void) { },再用gcc -c和-o命令分别编译成object文件和可执行文件。用size可以查看它们的(在硬盘上的)每个区域的尺寸:
$ size foo.o foo
text data bss dec hex filename
61 0 0 61 3d foo.o
1056 252 8 1316 524 foo
我们也可以通过objdump -h或objdump -x命令可以得到object文件各区域的尺寸:
$ objdump -h foo.o
foo.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000005 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000000 00000000 00000000 0000003c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 0000003c 2**2
ALLOC
3 .comment 0000002b 00000000 00000000 0000003c 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 00000000 00000000 00000067 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 00000000 00000000 00000068 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
size
命令没有列出栈和堆,因为它们没有存储在文件里。此外,foo程序没有全局数据,但foo.o的数据段和bss段的尺寸为0,而可执行程序的却不为0,这
是由于linker的参与造成的。还有,size命令和objdump产生的text段的结果不同,这是为什么呢?首先objdump是以十六进制显示
的,当然这不会造成这里的差别。如果用size -A -x命令查看的话,就一目了然了:
size -A -x foo.o
foo.o :
section size addr
.text 0x5 0x0
.data 0x0 0x0
.bss 0x0 0x0
.comment 0x2b 0x0
.note.GNU-stack 0x0 0x0
.eh_frame 0x38 0x0
Total 0x68
其实差别的部分在于.eh_frame的段。size里的text段包含了该段。该段也有时也被称为.rodata,用来存储常量数据。