本文采用Linux 内核 v3.10 版本 x86_64架构
一、内核中的段(segment)和节(section)
在 Linux 中,最常用的可执行文件就是 ELF (Executable and Linking Format) 文件。ELF 文件由一个文件头以及多个段(segment)组成。每个段又包括一个或者多个节(section)。每个段和节的长度在 ELF 头中指定。另外,每个段都有自己的访问权限。
可执行文件包含四个标准节,即 .text、 .data、 .rodata 以及 .bss。 .text 节包含可执行代码,并被打包到具有读取和执行访问权限的段。.data 和 .bss 节分别包含初始化和未初始化数据,它们被打包到具有读写访问权限的段。
我们通过 readelf -S 命令,可以查看内核可执行文件 vmliux 中所有的节:
$ readelf -S vmlinux
There are 43 section headers, starting at offset 0x97b6cf8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS ffffffff81000000 00200000
00000000006db5cb 0000000000000000 AX 0 0 4096
[ 2] .notes NOTE ffffffff816db5cc 008db5cc
000000000000017c 0000000000000000 AX 0 0 4
[ 3] __ex_table PROGBITS ffffffff816db750 008db750
00000000000020c0 0000000000000000 A 0 0 8
[ 4] .rodata PROGBITS ffffffff81800000 00a00000
00000000002c5bce 0000000000000000 A 0 0 64
[ 5] __bug_table PROGBITS ffffffff81ac5bd0 00cc5bd0
0000000000007a7c 0000000000000000 A 0 0 1
[ 6] .pci_fixup PROGBITS ffffffff81acd650 00ccd650
0000000000002be0 0000000000000000 A 0 0 8
[ 7] .builtin_fw PROGBITS ffffffff81ad0230 00cd0230
0000000000000120 0000000000000000 A 0 0 8
[ 8] .tracedata PROGBITS ffffffff81ad0350 00cd0350
000000000000003c 0000000000000000 A 0 0 1
[ 9] __ksymtab PROGBITS ffffffff81ad0390 00cd0390
0000000000010110 0000000000000000 A 0 0 16
[10] __ksymtab_gpl PROGBITS ffffffff81ae04a0 00ce04a0
000000000000be30 0000000000000000 A 0 0 16
[11] __ksymtab_strings PROGBITS ffffffff81aec2d0 00cec2d0
0000000000020825 0000000000000000 A 0 0 1
[12] __init_rodata PROGBITS ffffffff81b0cb00 00d0cb00
00000000000020b0 0000000000000000 A 0 0 64
...
...
...
通过 readelf -l 命令,可以查看 vmlinux 中所有的段:
$ readelf -l vmlinux
Elf file type is EXEC (Executable file)
Entry point 0x1000000
There are 5 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000200000 0xffffffff81000000 0x0000000001000000
0x0000000000b11000 0x0000000000b11000 R E 0x200000
LOAD 0x0000000000e00000 0xffffffff81c00000 0x0000000001c00000
0x00000000001300f0 0x00000000001300f0 RW 0x200000
LOAD 0x0000000001000000 0x0000000000000000 0x0000000001d31000
0x0000000000015140 0x0000000000015140 RW 0x200000
LOAD 0x0000000001147000 0xffffffff81d47000 0x0000000001d47000
0x000000000018c000 0x00000000007b8000 RWE 0x200000
NOTE 0x00000000008db5cc 0xffffffff816db5cc 0x00000000016db5cc
0x000000000000017c 0x000000000000017c 0x4
Section to Segment mapping:
Segment Sections...
00 .text .notes __ex_table .rodata __bug_table .pci_fixup .builtin_fw .tracedata __ksymtab __ksymtab_gpl __ksymtab_strings __init_rodata __param __modver
01 .data .vvar
02 .data..percpu
03 .init.text .init.data .x86_cpu_dev.init .parainstructions .altinstructions .altinstr_replacement .iommu_table .apicdrivers .exit.text .smp_locks .data_nosave .bss .brk
04 .notes
可以看到,vmlinux 中有 5 个程序头,对应着文件中的 5 个段。这些程序头是在链接脚本中定义的。
// file: arch/x86/kernel/vmlinux.lds.S
PHDRS {
text PT_LOAD FLAGS(5); /* R_E */
data PT_LOAD FLAGS(6); /* RW_ */
#ifdef CONFIG_X86_64
#ifdef CONFIG_SMP
percpu PT_LOAD FLAGS(6); /* RW_ */
#endif
init PT_LOAD FLAGS(7); /* RWE */
#endif
note PT_NOTE FLAGS(0); /* ___ */
}
另外,还可以看到,每个段可以包含一个或多个节。每个段中包含哪些节,也是在链接脚本中定义的。以 00 段来说,该段包括 .text、 .notes、 __ex_table 、.rodata 等多个节:
// file: arch/x86/kernel/vmlinux.lds.S
/* Text and read-only data */
.text : AT(ADDR(.text) - LOAD_OFFSET) {
_text = .;
/* bootstrapping code */
HEAD_TEXT
. = ALIGN(8);
_stext = .;
TEXT_TEXT
SCHED_TEXT
LOCK_TEXT
KPROBES_TEXT
ENTRY_TEXT
IRQENTRY_TEXT
*(.fixup)
*(.gnu.warning)
/* End of text section */
_etext = .;
} :text = 0x9090
NOTES :text :note
EXCEPTION_TABLE(16) :text = 0x9090
RO_DATA(PAGE_SIZE)
其中 :text 指示该节(section)被分配到程序头 text 对应的段(segment)。
宏 NOTES 指示链接器将所有输入文件中以 .note 开头的节合并到输出文件的 .notes 节中,该宏定义如下:
// file: include/asm-generic/vmlinux.lds.h
#define NOTES \
.notes : AT(ADDR(.notes) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(__start_notes) = .; \
*(.note.*) \
VMLINUX_SYMBOL(__stop_notes) = .; \
}
宏 EXCEPTION_TABLE 指示链接器将所有输入文件中的 __ex_table节合并到输出文件的 __ex_table 节,该宏定义如下:
// file: include/asm-generic/vmlinux.lds.h
/*
* Exception table
*/
#define EXCEPTION_TABLE(align) \
. = ALIGN(align); \
__ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(__start___ex_table) = .; \
*(__ex_table) \
VMLINUX_SYMBOL(__stop___ex_table) = .; \
}
宏 RO_DATA 宏定义了一些只读的节,比如 .rodata、.rodata1、.pci_fixup、__ksymtab、__init_rodata 等等。该宏的定义比较长,我们就不把代码贴出来了。
可以看到, .text、 .notes 以及 __ex_table 节,都被分配给了 text 段。另外,还可以看到,.notes 节被分配给了两个段,分别是 text 和 note 段。
段和节的映射关系示意图:
链接脚本详细资料,请参考 ld 官方文档。
比较普通可执行文件和 Linux 内核可执行文件中的节,就会发现 Linux 内核比普通可执行文件多出几个特殊的节,比如 __init_rodata、.init.text 、.init.data、.data..percpu、__ex_table 等。
其中 __init_rodata、.init.text 、.init.data 这三个节,只在内核初始化时使用。初始化完成后,这些节占用的内存空间会被释放。
.data..percpu 节用来存储 per-cpu 数据。
在这篇文章里,我们关注的是 __ex_table 这个特殊的节。
二、异常表 __ex_table 工作原理
__ex_table 节中存储了多个地址对,每对地址中的第一个是可能产生异常的地址, 第二个是修复代码地址。当内核执行到第一处代码时,如果出现异常,就会去执行第二处的修复代码。__ex_table 节需要和 .fixup 节配合使用,其中 .fixup 节内定义了异常修复代码。
完整的工作流程如下:
- 程序执行时遇到异常
- 执行异常处理程序
- 异常处理程序会到
__ex_table中查找异常地址对应的修复代码地址。 - 用修复地址替代原先的返回地址
- 从修复代码处恢复执行
我们知道,当异常发生时,处理器会自动将栈段寄存器 SS、栈指针寄存器 RSP、状态寄存器 RFLAFS、代码段寄存器 CS 以及指令指针寄存器 RIP 保存到栈中。由于在 Linux 内核中,全部使用的中断门来安装异常处理程序,所以栈中的 RIP 就是发生异常的地址。当异常处理完成,使用 iret 指令返回时,就会使用栈中保存的值来恢复寄存器。然后,程序从恢复后的 RIP 处继续执行。
在修复程序执行时,会把栈中保存的 RIP 替换成修复代码的地址。这样,当使用 iret 指令返回时,指令指针寄存器中就是修复代码的地址,然后从修复代码处恢复执行。
三、代码实现
为了更好的理解工作原理,我们通过代码来分析其实现过程。
3.1 异常表的创建
可以通过宏 _ASM_EXTABLE 生成异常表 ,该宏定义如下:
// file: arch/x86/include/asm/asm.h
/* Exception table entry */
#ifdef __ASSEMBLY__
# define _ASM_EXTABLE(from,to) \
.pushsection "__ex_table","a" ; \
.balign 8 ; \
.long (from) - . ; \
.long (to) - . ; \
.popsection
...
#else
# define _ASM_EXTABLE(from,to) \
" .pushsection \"__ex_table\",\"a\"\n" \
" .balign 8\n" \
" .long (" #from ") - .\n" \
" .long (" #to ") - .\n" \
" .popsection\n"
...
#endif
可以看到,在汇编语言和 C 语言中,该宏扩展后的格式略有不同。
宏 _ASM_EXTABLE(addr1, addr2) 接收两个地址(可能产生异常的地址和修复代码地址),并分别将其与位置计数器 “.”的差值保存到 __ex_table 节(异常表)中。也就是说,异常表中保存的是相对地址,而不是绝对地址。
.pushsection 指令是 .section 的同义词。该指令把当前 section 压入 section stack 的顶部,并用指令后的名称替代当前 section。
.popsection 指令使用 section stack 顶部的 section 来替代当前 section。
换句话说,.pushsection 和.popsection指令将两者之间的代码汇编到指定的节(section)中。
GNU 汇编器一共有五个 section stack 操作指令,分别是 .section、.subsection、.pushsection、.popsection 以及 .previous。
创建后,通过链接脚本,将源文件中的 __ex_table 节,合并输出到可执行文件的__ex_table 节中:
// file: arch/x86/kernel/vmlinux.lds.S
EXCEPTION_TABLE(16) :text = 0x9090
宏 EXCEPTION_TABLE 定义如下:
// file: include/asm-generic/vmlinux.lds.h
/*
* Exception table
*/
#define EXCEPTION_TABLE(align) \
. = ALIGN(align); \
__ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(__start___ex_table) = .; \
*(__ex_table) \
VMLINUX_SYMBOL(__stop___ex_table) = .; \
}
__ex_table 节对齐到 16 字节的倍数。同时,链接器创建了一对标识符 __start___ex_table 和 __stop___ex_table , 来表示__ex_table 节的起始和结束地址。Linux 中的函数可以使用这些标识符来迭代访问 __ex_table 节中的数据。
在 vmlinux 文件中可以查看到这两个符号:
$ readelf -s vmlinux|grep ex_table
56490: ffffffff815a7950 0 NOTYPE GLOBAL DEFAULT 3 __start___ex_table
73029: ffffffff815a9910 0 NOTYPE GLOBAL DEFAULT 3 __stop___ex_table
3.2 相关数据结构
内核将 __start___ex_table 和 __stop___ex_table 声明为数组。因为这些标识符是在链接脚本中定义的,所以必须把它们申明为带有 extern 修饰符的变量:
// file: kernel/extable.c
extern struct exception_table_entry __start___ex_table[];
extern struct exception_table_entry __stop___ex_table[];
数组中的每个元素为 exception_table_entry 结构体类型,对应着一个异常表项,即 __ex_table 节中的一对相对地址。
// file: arch/x86/include/asm/uaccess.h
/*
* The exception table consists of pairs of addresses relative to the
* exception table enty itself: the first is the address of an
* instruction that is allowed to fault, and the second is the address
* at which the program should continue. No registers are modified,
* so it is entirely up to the continuation code to figure out what to
* do.
*
* All the routines below use bits of fixup code that are out of line
* with the main instruction path. This means when everything is well,
* we don't even have to jump over them. Further, they do not intrude
* on our cache or tlb entries.
*/
struct exception_table_entry {
int insn, fixup;
};
其中,insn 表示异常指令的相对地址,fixup 表示修复代码的相对地址。
3.3 异常表的排序
在系统启动时,需要对异常表 __ex_table 进行排序。排序的原因,是由于当发生异常时,需要根据异常地址对 __ex_table 进行搜索,以判断该异常是否有修复程序。而搜索使用的是二分查找法,该算法要求数组必须是有序的。
系统启动时,在 start_kernel 函数中,调用 sort_main_extable 函数对异常表进行排序。
// file: init/main.c
asmlinkage void __init start_kernel(void)
{
...
sort_main_extable();
...
}
sort_main_extable 函数内部,又调用了sort_extable 函数来实现排序功能。
// file: kernel/extable.c
/* Cleared by build time tools if the table is already sorted. */
u32 __initdata main_extable_sort_needed = 1;
/* Sort the kernel's built-in exception table */
void __init sort_main_extable(void)
{
if (main_extable_sort_needed) {
pr_notice("Sorting __ex_table...\n");
sort_extable(__start___ex_table, __stop___ex_table);
}
}
调用 sort_extable 函数时,传入的参数分别为 __start___ex_table 和 __stop___ex_table,即__ex_table 节的起始和结束地址。
// file: arch/x86/mm/extable.c
void sort_extable(struct exception_table_entry *start,
struct exception_table_entry *finish)
{
struct exception_table_entry *p;
int i;
/* Convert all entries to being relative to the start of the section */
i = 0;
for (p = start; p < finish; p++) {
p->insn += i;
i += 4;
p->fixup += i;
i += 4;
}
sort(start, finish - start, sizeof(struct exception_table_entry),
cmp_ex, NULL);
/* Denormalize all entries */
i = 0;
for (p = start; p < finish; p++) {
p->insn -= i;
i += 4;
p->fixup -= i;
i += 4;
}
}
由于异常表中的相对地址是基于当前变量的地址计算的,基准不统一。所以,在排序前,将各异常表项中相对地址的基准统一调整为 __ex_table 节的起始地址;排序完成后,再恢复到原始值。
在 sort_extable 函数中,调用 sort 函数对数组进行堆排序。
// file: lib/sort.c
/**
* sort - sort an array of elements
* @base: pointer to data to sort
* @num: number of elements
* @size: size of each element
* @cmp_func: pointer to comparison function
* @swap_func: pointer to swap function or NULL
*
* This function does a heapsort on the given array. You may provide a
* swap_func function optimized to your element type.
*
* Sorting time is O(n log n) both on average and worst-case. While
* qsort is about 20% faster on average, it suffers from exploitable
* O(n*n) worst-case behavior and extra memory requirements that make
* it less suitable for kernel use.
*/
void sort(void *base, size_t num, size_t size,
int (*cmp_func)(const void *, const void *),
void (*swap_func)(void *, void *, int size))
{
/* pre-scale counters for performance */
int i = (num/2 - 1) * size, n = num * size, c, r;
if (!swap_func)
swap_func = (size == 4 ? u32_swap : generic_swap);
/* heapify */
for ( ; i >= 0; i -= size) {
for (r = i; r * 2 + size < n; r = c) {
c = r * 2 + size;
if (c < n - size &&
cmp_func(base + c, base + c + size) < 0)
c += size;
if (cmp_func(base + r, base + c) >= 0)
break;
swap_func(base + r, base + c, size);
}
}
/* sort */
for (i = n - size; i > 0; i -= size) {
swap_func(base, base + i, size);
for (r = 0; r * 2 + size < i; r = c) {
c = r * 2 + size;
if (c < i - size &&
cmp_func(base + c, base + c + size) < 0)
c += size;
if (cmp_func(base + r, base + c) >= 0)
break;
swap_func(base + r, base + c, size);
}
}
}
3.4 Apis
3.4.1 获取异常地址 -- ex_insn_addr
ex_insn_addr 函数用于获取异常表项中的产生异常的指令地址,该函数定义如下:
// file: arch/x86/mm/extable.c
static inline unsigned long
ex_insn_addr(const struct exception_table_entry *x)
{
return (unsigned long)&x->insn + x->insn;
}
&x->insn 是异常表项中成员 insn 的地址,也就是宏 _ASM_EXTABLE 中 .long (from) - . ; 指令内位置计数器 “.” 对应的值。x->insn 是异常表项中成员 insn 的值,也就是 (from) - . 的值。两者相加,得到的就是 from 的值,也就是异常发生的地址。
3.4.2 获取修复代码地址 -- ex_fixup_addr
ex_fixup_addr 函数用于获取异常表项中的修复代码地址,该函数定义如下:
static inline unsigned long
ex_fixup_addr(const struct exception_table_entry *x)
{
return (unsigned long)&x->fixup + x->fixup;
}
&x->fixup 是异常表项中成员 fixup 的地址,也就是宏 _ASM_EXTABLE 中 .long (to) - . ; 指令内位置计数器 “.” 对应的值。x->fixup 是异常表项中成员 fixup 的值,也就是 (to) - . 的值。两者相加,得到的就是 to 的值,也就是修复代码的地址。
3.4.3 查找异常地址对应的表项 -- search_exception_tables
search_exception_tables 函数接收一个参数,即异常发生的地址。然后去异常表中,查找该地址对应的异常表项。如果能够查到,说明该地址有对应的修复地址,返回对应的表项指针;否则,返回 NULL。
// file: kernel/extable.c
/* Given an address, look for it in the exception tables. */
const struct exception_table_entry *search_exception_tables(unsigned long addr)
{
const struct exception_table_entry *e;
e = search_extable(__start___ex_table, __stop___ex_table-1, addr);
if (!e)
e = search_module_extables(addr);
return e;
}
search_exception_tables 函数内部,又调用了 search_extable 函数来完成具体功能。search_extable 函数接收三个参数:
- first - 数组首元素地址
- last - 数组末元素地址
- addr - 需要查找的地址
search_extable 使用二分查找法从异常表中查找包含异常地址的表项。如果找到,返回该表项的地址;否则,返回 NULL。
// file: arch/x86/mm/extable.c
/*
* Search one exception table for an entry corresponding to the
* given instruction address, and return the address of the entry,
* or NULL if none is found.
* We use a binary search, and thus we assume that the table is
* already sorted.
*/
const struct exception_table_entry *
search_extable(const struct exception_table_entry *first,
const struct exception_table_entry *last,
unsigned long value)
{
while (first <= last) {
const struct exception_table_entry *mid;
unsigned long addr;
mid = ((last - first) >> 1) + first;
addr = ex_insn_addr(mid);
if (addr < value)
first = mid + 1;
else if (addr > value)
last = mid - 1;
else
return mid;
}
return NULL;
}
search_extable 函数内部,调用了 ex_insn_addr 函数用于获取异常表项中的正常指令地址。
Linux 定义了多个异常表。主异常表是在内核编译时由编译器自动生成的,也就是我们上文讲到的异常表。除此之外,内核中每一个动态加载的模块也有自己的异常表。这是在编译内核模块时由编译器自动生成的。当该模块被插入到运行的 Linux 内核时,异常表也被加载到内存中。
所以,如果 search_extable 函数没有搜索到对应的异常表项,就需要到模块相关的异常表中去搜索。这个功能是通过 search_module_extables 函数来完成的。模块相关内容,我们暂不涉及。
3.4.4 异常修复 -- fixup_exception
fixup_exception 函数定义如下:
// file: arch/x86/mm/extable.c
int fixup_exception(struct pt_regs *regs)
{
const struct exception_table_entry *fixup;
unsigned long new_ip;
#ifdef CONFIG_PNPBIOS
...
#endif
fixup = search_exception_tables(regs->ip);
if (fixup) {
new_ip = ex_fixup_addr(fixup);
if (fixup->fixup - fixup->insn >= 0x7ffffff0 - 4) {
/* Special hack for uaccess_err */
current_thread_info()->uaccess_err = 1;
new_ip -= 0x7ffffff0;
}
regs->ip = new_ip;
return 1;
}
return 0;
}
该函数接收一个参数,即 pt_regs 结构体类型的指针。结构体 pt_regs 中保存着异常发生时,各寄存器的值,也就是所谓的上下文。关于异常发生时保存及恢复上下文的详细流程,请参考 Linux Kernel:异常处理程序的实现。
pt_regs 结构体定义如下:
// file: arch/x86/include/asm/ptrace.h
struct pt_regs {
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long bp;
unsigned long bx;
/* arguments: non interrupts/non tracing syscalls only save up to here*/
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long ax;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
unsigned long orig_ax;
/* end of arguments */
/* cpu exception frame or undefined */
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
/* top of stack page */
};
该结构体中的 ip 字段,保存的就是产生异常的地址。
首先,通过 regs->ip 获取异常地址,然后调用 search_exception_tables 函数,根据异常地址到异常表 __ex_table 中查找对应的表项。
fixup = search_exception_tables(regs->ip);
如果表项存在,说明指定了修复代码,通过 ex_fixup_addr 函数获取修复代码地址,并赋值给变量 new_ip。
new_ip = ex_fixup_addr(fixup);
如果 fixup->fixup - fixup->insn >= 0x7ffffff0 - 4 为真,说明是访问用户内存空间导致的异常。则把线程对应的 uaccess_err 字段设置为 1, 表明出现了用户内存空间访问错误。然后把修复地址 new_ip 减去 0x7ffffff0 获得真实的修复代码地址。
/* Special hack for uaccess_err */
current_thread_info()->uaccess_err = 1;
new_ip -= 0x7ffffff0;
先来说说 0x7ffffff0 是咋回事。
在内核中,除了可以使用 _ASM_EXTABLE 宏来创建异常表之外,还有一个宏 _ASM_EXTABLE_EX 也能实现同样的功能。
// file: arch/x86/include/asm/asm.h
# define _ASM_EXTABLE_EX(from,to) \
" .pushsection \"__ex_table\",\"a\"\n" \
" .balign 8\n" \
" .long (" #from ") - .\n" \
" .long (" #to ") - . + 0x7ffffff0\n" \
" .popsection\n"
与 _ASM_EXTABLE 不同的是,该宏保存的修复地址加了个 0x7ffffff0 常量。该常量相当于一个魔数,用于指示异常是由于访问用户空间错误导致的。也就是说,当异常肯定是由于访问用户空间出错引起时,内核会使用 _ASM_EXTABLE_EX 宏来创建异常表项,而不会使用 _ASM_EXTABLE 宏。 fixup->fixup - fixup->insn >= 0x7ffffff0 - 4 ,通过转换就能得到 to - from >= 0x7ffffff0 ,其中 to 是修复地址,from 是异常地址。 由于 to 和 from 都属于内核代码,而内核镜像最大不能超过 512M,在虚拟内存中也只为内核代码分配了 512 MB 空间,所以正常情况下 to 和 from 的差值不可能大于 512M,更不可能大于 0x7ffffff0(约 2G )。 如果两者之差大于0x7ffffff0,肯定是修复地址中加了 0x7ffffff0,也就是使用宏 _ASM_EXTABLE_EX 创建的异常表项,必然是由于访问用户空间出错引起的异常。
宏 KERNEL_IMAGE_SIZE 定义了内核镜像最大尺寸:
// file: arch/x86/include/asm/page_64_types.h
/*
* Kernel image size is limited to 512 MB (see level2_kernel_pgt in
* arch/x86/kernel/head_64.S), and it is mapped here:
*/
#define KERNEL_IMAGE_SIZE (512 * 1024 * 1024)
内核代码映射区的空间分配,请参考下图:
current_thread_info() 用来获取当前线程的信息,即 thread_info 结构体:
// file: arch/x86/include/asm/thread_info.h
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
int preempt_count; /* 0 => preemptable,
<0 => BUG */
mm_segment_t addr_limit;
struct restart_block restart_block;
void __user *sysenter_return;
#ifdef CONFIG_X86_32
unsigned long previous_esp; /* ESP of the previous stack in
case of nested (IRQ) stacks
*/
__u8 supervisor_stack[0];
#endif
unsigned int sig_on_uaccess_error:1;
unsigned int uaccess_err:1; /* uaccess failed */
};
thread_info 中的 uaccess_err 字段用于指示访问用户空间时是否出现错误。当该字段为 1 时,说明访问用户空间失败。所以,此处将当前线程的 thread_info 结构体中的 uaccess_err 字段设置为 1。
current_thread_info 函数定义如下:
// file: arch/x86/include/asm/thread_info.h
static inline struct thread_info *current_thread_info(void)
{
struct thread_info *ti;
ti = (void *)(this_cpu_read_stable(kernel_stack) +
KERNEL_STACK_OFFSET - THREAD_SIZE);
return ti;
}
kernel_stack 是 per-cpu 变量,表示进程内核栈的栈底地址,其定义如下:
// file: arch/x86/include/asm/thread_info.h
DECLARE_PER_CPU(unsigned long, kernel_stack);
每次进程切换时,都会把切换到的进程的内核栈的栈底地址写入 kernel_stack 中:
// file: arch/x86/kernel/process_64.c
__notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
...
this_cpu_write(kernel_stack,
(unsigned long)task_stack_page(next_p) +
THREAD_SIZE - KERNEL_STACK_OFFSET);
...
}
这里 next_p 是切换后进程的进程描述符 task_struct 结构体的指针。宏 task_stack_page 用来获取当前进程描述符中的 stack 成员,即内核栈的最低地址。
// file: include/linux/sched.h
#define task_stack_page(task) ((task)->stack)
task_struct 结构体中的 stack 成员:
// file: include/linux/sched.h
struct task_struct {
......
void *stack;
......
}
thread_info 和 内核栈 task_struct->stack 是共生的关系,它们共同存在于 thread_union 结构体中:
// file: include/linux/sched.h
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
另外,宏 THREAD_SIZE 表示内核栈的大小,扩展为 2 个 PAGE_SIZE(4K)大小,即 8KB。
// file: arch/x86/include/asm/page_64_types.h
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
宏 KERNEL_STACK_OFFSET扩展为 40,意味着在栈底保留了 40 字节的空间,该宏定义如下:
// file: arch/x86/include/asm/thread_info.h
#define KERNEL_STACK_OFFSET (5*8)
thread_info、task_struct和内核栈的关系见下图:
由于写入的数据为 task->stack + HREAD_SIZE - KERNEL_STACK_OFFSET,读取后反向操作加上 KERNEL_STACK_OFFSET 后再减去HREAD_SIZE,就得到 task->stack 的值,也是 thread_info 的地址。
都处理完成后,把修复地址 new_ip 保存到 regs->ip 中,并返回 1。
regs->ip = new_ip;
return 1;
上文已经说过,异常发生时,处理器会自动把栈段寄存器 SS、栈指针寄存器 RSP、状态寄存器 RFLAFS、代码段寄存器 CS 以及指令指针寄存器 RIP 保存到栈中,也就是保存到 pt_regs 结构体中。当从中断返回时,iret 指令会用 pt_regs 结构体中的值恢复寄存器。现在,我们把 pt_regs 结构体中的 ip 成员修改为修复代码的地址,所以从中断返回后,就会执行修复代码。
最后,如果未找到异常地址对应的表项,则返回 0。
四、使用示例
4.1 通用保护异常(#GP)处理程序
在 Linux 内核中,有多处使用了宏 _ASM_EXTABLE,比如在 loadsegment 中:
// file: arch/x86/include/asm/segment.h
/*
* Load a segment. Fall back on loading the zero
* segment if something goes wrong..
*/
#define loadsegment(seg, value) \
do { \
unsigned short __val = (value); \
\
asm volatile(" \n" \
"1: movl %k0,%%" #seg " \n" \
\
".section .fixup,\"ax\" \n" \
"2: xorl %k0,%k0 \n" \
" jmp 1b \n" \
".previous \n" \
\
_ASM_EXTABLE(1b, 2b) \
\
: "+r" (__val) : : "memory"); \
} while (0)
从 loadsegment 的名字中也能看到,该宏用于段寄存器的加载。其使用方法如下所示:
// file: arch/x86/kernel/cpu/common.c
void __cpuinit cpu_init(void)
{
...
loadsegment(fs, 0);
...
}
在上述代码片段中,通过 loadsegment 将 fs段寄存器设置为 0。
根据上文所述的宏定义:
// file: arch/x86/include/asm/asm.h
# define _ASM_EXTABLE(from,to) \
" .pushsection \"__ex_table\",\"a\"\n" \
" .balign 8\n" \
" .long (" #from ") - .\n" \
" .long (" #to ") - .\n" \
" .popsection\n"
宏 loadsegment(seg, value) 会扩展为:
do { \
unsigned short __val = (value); \
\
asm volatile(" \n" \
"1: movl %k0,%%" #seg " \n" \
\
".section .fixup,\"ax\" \n" \
"2: xorl %k0,%k0 \n" \
" jmp 1b \n" \
".previous \n" \
\
" .pushsection \"__ex_table\",\"a\"\n" \
" .balign 8\n" \
" .long (" 1b ") - .\n" \
" .long (" 2b ") - .\n" \
" .popsection\n"
\
: "+r" (__val) : : "memory"); \
} while (0)
其中标签 1b 和 2b 的后缀 b,表示 backward,即搜索之前的代码找到对应的标签。所以标签 1b 处,是 movl 指令的地址,这是可能发生异常的指令地址,该指令把变量 __val 的值加载到指定的段寄存器中;标签 2b 处,是 xorl %k0,%k0 指令的地址,表示修复指令的地址,该指令把 __val的值设置为 0,然后跳转到标签 1 处,把__val的值(为 0)加载到段寄存前器中 ,也就是将段寄存器设置为 0。
有的同学可能会有疑问,标签 1 处的指令执行完成后,不应该直接执行标签 2 处的指令么?标签 2 处的指令执行后,又跳转到标签 1 处执行,好像是个死循环啊。其实,标签 1 处的指令与标签 2 处的指令不在一个节(section)里。标签 1 处的代码位于 .text 节;而 .section .fixup, ax 和 .previous 之间的代码位于 .fixup 节里。链接脚本会将这两个节的代码分开处理,所以它们的地址是不连续的。
// file: arch/x86/kernel/vmlinux.lds.S
/* Text and read-only data */
.text : AT(ADDR(.text) - LOAD_OFFSET) {
_text = .;
/* bootstrapping code */
HEAD_TEXT
. = ALIGN(8);
_stext = .;
TEXT_TEXT
SCHED_TEXT
LOCK_TEXT
KPROBES_TEXT
ENTRY_TEXT
IRQENTRY_TEXT
*(.fixup)
*(.gnu.warning)
/* End of text section */
_etext = .;
} :text = 0x9090
其中宏 TEXT_TEXT 表示 .text 相关的节,*(.fixup) 表示 .fixup 节。
宏 TEXT_TEXT 定义如下:
// file: include/asm-generic/vmlinux.lds.h
/* .text section. Map to function alignment to avoid address changes
* during second ld run in second ld pass when generating System.map */
#define TEXT_TEXT \
ALIGN_FUNCTION(); \
*(.text.hot) \
*(.text) \
*(.ref.text) \
DEV_KEEP(init.text) \
DEV_KEEP(exit.text) \
CPU_KEEP(init.text) \
CPU_KEEP(exit.text) \
MEM_KEEP(init.text) \
MEM_KEEP(exit.text) \
*(.text.unlikely)
可以看到,.text 相关的节,会合并到一起输出;而 .fixup 节的内容会单独输出。在链接后,这两个节的代码是分开的。也就是说,在生成的可执行文件中,标签 1 处的代码和标签 2 处的代码地址是不连续的。
接下来,.pushsection __ex_table,a 和 .popsection 之间的代码,把异常指令的相对地址以及修复指令的相对地址存入 __ex_table 节中。
在标签 1 处执行 movl 命令时,根据Intel 文档说明,可能出现多种异常:
我们以通用保护异常(#GP)为例,来看下异常表 __ex_table 是如何使用的。
通用保护异常的处理程序为 do_general_protection ,该函数接收两个参数:
- regs -
pt_regs结构体类型的指针,结构体内保存着异常发生时各寄存器的值 - error_code - 异常的错误码
// file: arch/x86/kernel/traps.c
dotraplinkage void __kprobes
do_general_protection(struct pt_regs *regs, long error_code)
{
struct task_struct *tsk;
enum ctx_state prev_state;
prev_state = exception_enter();
...
if (!user_mode(regs)) {
if (fixup_exception(regs))
goto exit;
...
}
...
exit:
exception_exit(prev_state);
}
如果异常发生在内核空间,就会调用 fixup_exception 函数对异常进行修复。
其中 user_mode 函数用来判断异常是否发生在用户空间。判断方法也很简单,就是检查异常发生时 CS 寄存器的当前特权级 CPL 是否为 3。如果为 3,说明异常发生在用户空间;否则,异常发生在内核空间。
// file: arch/x86/include/asm/ptrace.h
/*
* user_mode_vm(regs) determines whether a register set came from user mode.
* This is true if V8086 mode was enabled OR if the register set was from
* protected mode with RPL-3 CS value. This tricky test checks that with
* one comparison. Many places in the kernel can bypass this full check
* if they have already ruled out V8086 mode, so user_mode(regs) can be used.
*/
static inline int user_mode(struct pt_regs *regs)
{
#ifdef CONFIG_X86_32
...
#else
return !!(regs->cs & 3);
#endif
}
4.2 其它异常处理程序
除了通用保护异常(#GP)之外,其它大部分异常处理程序也使用了异常修复机制。比如用宏 DO_ERROR 和 DO_ERROR_INFO 定义的异常处理程序。
// file: arch/x86/kernel/traps.c
DO_ERROR_INFO(X86_TRAP_DE, SIGFPE, "divide error", divide_error, FPE_INTDIV,
regs->ip)
DO_ERROR(X86_TRAP_OF, SIGSEGV, "overflow", overflow)
DO_ERROR(X86_TRAP_BR, SIGSEGV, "bounds", bounds)
DO_ERROR_INFO(X86_TRAP_UD, SIGILL, "invalid opcode", invalid_op, ILL_ILLOPN,
regs->ip)
DO_ERROR(X86_TRAP_OLD_MF, SIGFPE, "coprocessor segment overrun",
coprocessor_segment_overrun)
DO_ERROR(X86_TRAP_TS, SIGSEGV, "invalid TSS", invalid_TSS)
DO_ERROR(X86_TRAP_NP, SIGBUS, "segment not present", segment_not_present)
DO_ERROR_INFO(X86_TRAP_AC, SIGBUS, "alignment check", alignment_check,
BUS_ADRALN, 0)
这些异常处理程序内部,会调用 do_trap 函数。
// file: arch/x86/kernel/traps.c
#define DO_ERROR(trapnr, signr, str, name) \
dotraplinkage void do_##name(struct pt_regs *regs, long error_code) \
{ \
...
do_trap(trapnr, signr, str, regs, error_code, NULL); \
...
}
// file: arch/x86/kernel/traps.c
#define DO_ERROR_INFO(trapnr, signr, str, name, sicode, siaddr) \
dotraplinkage void do_##name(struct pt_regs *regs, long error_code) \
{ \
...
do_trap(trapnr, signr, str, regs, error_code, &info); \
...
}
而 do_trap 函数会调用 do_trap_no_signal 函数:
static void __kprobes
do_trap(int trapnr, int signr, char *str, struct pt_regs *regs,
long error_code, siginfo_t *info)
{
struct task_struct *tsk = current;
if (!do_trap_no_signal(tsk, trapnr, str, regs, error_code))
return;
...
}
do_trap_no_signal 函数会调用 fixup_exception 函数对异常进行修复。
static int __kprobes
do_trap_no_signal(struct task_struct *tsk, int trapnr, char *str,
struct pt_regs *regs, long error_code)
{
...
if (!user_mode(regs)) {
if (!fixup_exception(regs)) {
...
}
return 0;
}
return -1;
}
类似的,Page-Fault 异常(#PF)的处理程序 do_page_fault 函数内部也会调用异常修复函数 fixup_exception 。
其调用链为:__do_page_fault -> bad_area_nosemaphore -> __bad_area_nosemaphore -> no_context -> fixup_exception;
或者: __do_page_fault -> bad_area -> bad_area_nosemaphore -> __bad_area_nosemaphore -> no_context -> fixup_exception。
另外,协处理器异常(9号)处理程序 do_coprocessor_error 、x87 浮点异常(#MF,16号)处理程序 do_simd_coprocessor_error、断点异常处理程序(#BP,3号) do_int3、栈段错误异常(#SS, 12号)处理程序 do_stack_segment 的内部,都调用了 fixup_exception 函数进行异常修复。
五、参考资料
1、链接脚本文档
2、.section、.subsection、.pushsection、.popsection 以及 .previous。
5、Special sections in Linux binaries