那么为什么要通过.section定义独立的段呢?为了解开这个问题的答案,我们需要进一步看看我们所写的代码在可执行文件中是如何表示的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| $objdump --disassemble --section=.text hello
hello: file format elf32-i386
Disassembly of section .text:
8048498: 8b 45 c4 mov 0xffffffc4(%ebp),%eax
804849b: 83 e0 03 and $0x3,%eax
804849e: 8b 55 c4 mov 0xffffffc4(%ebp),%edx
80484a1: 89 d1 mov %edx,%ecx
80484a3: c1 e9 02 shr $0x2,%ecx
80484a6: 8d 7d c8 lea 0xffffffc8(%ebp),%edi
80484a9: 8b 75 f4 mov 0xfffffff4(%ebp),%esi
80484ac: f3 a5 repz movsl %ds%esi),%es%edi)
80484ae: 89 c1 mov %eax,%ecx
80484b0: f3 a4 repz movsb %ds%esi),%es%edi)
80484b2: 89 c8 mov %ecx,%eax
|
前面的hello.s中的汇编片断在可执行文件中就是通过上面的11条指定来表达,读者也许会问,由.section伪操作定义的段怎么不见了?别着急,慢慢往下看,由.section伪操作定义的段并不在正常的程序执行路径上,它们是被安排在可执行文件的其它地方了:
1
2
3
4
5
6
| $objdump --disassemble --section=.fixup hello
hello: file format elf32-i386
Disassembly of section .fixup:
08048530 <.fixup>:
8048530: 8d 4c 88 00 lea 0x0(%eax,%ecx,4),%ecx
8048534: e9 79 ff ff ff jmp 80484b2 <main+0x42>
|
由此可见,.fixup是作为一个单独的段出现在可执行程序中的,而此段中所包含的语句则正好是和源程序hello.c中的两条语句相对应的。
将.fixup段和.text段独立开来的目的是为了提高CPU流水线的利用率。熟悉体系结构的读者应该知道,当前的CPU引入了流水线技术来加快指令的执行,即在执行当前指令的同时,要将下面的一条甚至多条指令预取到流水线中。这种技术在面对程序执行分支的时候遇到了问题:如果预取的指令并不是程序下一步要执行的分支,那么流水线中的所有指令都要被排空,这对系统的性能会产生一定的影响。在我们的这个程序中,如果将.fixup段的指令安排在正常执行的.text段中,当程序执行到前面的指令时,这几条很少执行的指令会被预取到流水线中,正常的执行必然会引起流水线的排空操作,这显然会降低整个系统的性能。
下面我们就可以看到异常表是如何形成的了:
1
2
3
4
| $objdump --full-contents --section=__ex_table hello
hello: file format elf32-i386
Contents of section __ex_table:
8048578 ac840408 30850408 b0840408 b2840408 ....0...........
|
由于x86使用小尾端的编址方式,上面的这段数据比较凌乱。让我把上面的__ex_table中的内容转变成大家通常看到的样子,相信会更容易理解一些:
1
| 8048578 80484ac 8048530 80484b0 80484b2 ....0...........
|
上面的红色部分就是我们最感兴趣的地方,而这段数据是如何形成的呢?将前面objdump生成的可执行程序中的汇编语句和hello.c中的源程序结合起来看,就可以发现一些有趣的东西了!
先让我们回头看看hello.c中__ex_table段的语句 .long 0b,3b。其中标签0b(b代表backward,即往回的标签0)是可能出现异常的指令的地址。结合objdump生成的可执行程序.text段的汇编语句可以知道标签0就是80484ac:
原始的汇编语句: 0: rep; movsl
链接到可执行程序后: 80484ac: f3 a5 repz movsl %ds%esi),%es%edi)
而标签3就是处理异常的指令的地址,在我们的这个例子中就是80484b0:
原始的汇编语句: 3: lea 0(%eax,%ecx,4),%ecx
链接到可执行程序后: 8048530: 8d 4c 88 00 lea 0x0(%eax,%ecx,4),%ecx
因此,相应的汇编语句
1
2
3
| .section __ex_table,"a"
.align 4
.long 0b,3b
|
就变成了: 8048578 80484ac 8048530 …………
这样,异常表中的地址对(80484ac,8048530)就诞生了,而对于地址对(80484b0 80484b2)的生成,情况相同,不再赘述。
读到这儿了,有一件事要告诉读者的是,其实例子中异常表的安排在用户空间是不会得到执行的。当运行在用户态的进程访问到标签0处的指令出现缺页异常时,do_page_fault只会将该指令对应的进程页调入内存中,使指令能够重新正确执行,或者直接就杀死该进程,并不会到达函数search_exception_table处。
也许有的读者会问了,既然不执行,前面的例子和围绕例子所展开的讨论又有什么作用呢?大家大可打消这样的疑虑,我们前面的分析并没有白费,因为真正的内核异常表中地址对的生成机制和前面讲述的原理是完全一样的,笔者通过一个运行在用户空间的程序来讲解也是希望让读者能够更加容易的理解异常表的机制,不至于陷入到内核源码的汪洋大海中去。现在,我们可以自己通过objdump工具查看一下内核中的异常表:
1
2
3
4
5
| $objdump --full-contents --section=__ex_table vmlinux
vmlinux: file format elf32-i386
Contents of section __ex_table:
c024ac80 e36d10c0 e66d10c0 8b7110c0 6c7821c0
……………………
|
做一下转化:
1
| c024ac80 c0106de3 c0106de6 c010718b c021786c
|
上面的vmlinux就是编译内核所生成的内核可执行程序。和本文给出的例子相比,唯一的不同就是此时的地址对中的异常指令地址和修复地址都是内核空间的虚拟地址。也正是在内核中,异常表才真正发挥着它应有的作用。 |