Linux内核的异常修复原理

简介: Linux内核的异常修复原理

参考

Linker Script in Linux (3.1.1 Exception Table)

Linux异常表

linux Oops和Panic关系

5. Kernel level exception handling

环境

ARM64

Linux-5.8

场景

   用户通过系统调用给内核传递了一个参数,这个参数有一个该用户地址空间的地址,然后内核在访问时会发生什么情况呢?如果这个用户空间地址处于当前进程的有效vma中,那么正常的缺页异常可以处理。

   但是如果这个参数是一个非法的用户地址,内核访问的话,会不会由于访问了非法地址而导致崩溃呢?

测试程序

驱动

static ssize_t fixup_read (struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
int err;
        err = put_user(10, buf);
        printk("%s: %d\n", __func__, err);
return count;
}

用户程序

int main(int argc, const char *argv[])
{
int fd;
char *buf;
        fd = open("/dev/fixup", O_RDWR);
        buf = 0x4000000;
printf("read buf\n");
        read(fd, buf, 5);
return 0;
}

内核log

[11131.867589] fixup_read: -14

   可以看到put_user返回了错误码:-EFAULT,并没有导致内核崩溃,测试程序也没有异常退出。在这背后发生了什么呢?其实内核在访问这个非法用户地址的时候,确实发生了缺页异常,不过在缺页异常中使用这里说的exception_table进行了修复,返回了错误码。要完成这个功能,需要依赖put_user和缺页异常的支持。

原理

put_user

put_user的实现在arch/arm64/include/asm/uaccess.h中:

#define _ASM_EXTABLE(from, to)            \
  " .pushsection  __ex_table, \"a\"\n"      \
  " .align    3\n"          \
  " .long   (" #from " - .), (" #to " - .)\n" \
  " .popsection\n"
#define __put_mem_asm(store, reg, x, addr, err)       \
  asm volatile(             \
  "1: " store " " reg "1, [%2]\n"     \
  "2:\n"                \
  " .section .fixup,\"ax\"\n"       \
  " .align  2\n"            \
  "3: mov %w0, %3\n"          \
  " b 2b\n"           \
  " .previous\n"            \
  _ASM_EXTABLE(1b, 3b)            \
  : "+r" (err)              \
  : "r" (x), "r" (addr), "i" (-EFAULT))
#define __raw_put_mem(str, x, ptr, err)         \
do {                  \
  __typeof__(*(ptr)) __pu_val = (x);        \
  switch (sizeof(*(ptr))) {         \
  case 1:               \
    __put_mem_asm(str "b", "%w", __pu_val, (ptr), (err)); \
    break;              \
  case 2:               \
    __put_mem_asm(str "h", "%w", __pu_val, (ptr), (err)); \
    break;              \
  case 4:               \
    __put_mem_asm(str, "%w", __pu_val, (ptr), (err)); \
    break;              \
  case 8:               \
    __put_mem_asm(str, "%x", __pu_val, (ptr), (err)); \
    break;              \
  default:              \
    BUILD_BUG();            \
  }               \
} while (0)
#define __raw_put_user(x, ptr, err)         \
do {                  \
  __chk_user_ptr(ptr);            \
  uaccess_ttbr0_enable();           \
  __raw_put_mem("sttr", x, ptr, err);       \
  uaccess_ttbr0_disable();          \
} while (0)
#define __put_user_error(x, ptr, err)         \
do {                  \
  __typeof__(*(ptr)) __user *__p = (ptr);       \
  might_fault();              \
  if (access_ok(__p, sizeof(*__p))) {       \
    __p = uaccess_mask_ptr(__p);        \
    __raw_put_user((x), __p, (err));      \
  } else  {             \
    (err) = -EFAULT;          \
  }               \
} while (0)
#define __put_user(x, ptr)            \
({                  \
  int __pu_err = 0;           \
  __put_user_error((x), (ptr), __pu_err);       \
  __pu_err;             \
})
#define put_user  __put_user

重点关注下面的内容:

#define _ASM_EXTABLE(from, to)            \
  " .pushsection  __ex_table, \"a\"\n"      \
  " .align    3\n"          \
  " .long   (" #from " - .), (" #to " - .)\n" \
  " .popsection\n"
#define __put_mem_asm(store, reg, x, addr, err)       \
  asm volatile(             \
  "1: " store " " reg "1, [%2]\n"     \
  "2:\n"                \
  " .section .fixup,\"ax\"\n"       \
  " .align  2\n"            \
  "3: mov %w0, %3\n"          \
  " b 2b\n"           \
  " .previous\n"            \
  _ASM_EXTABLE(1b, 3b)            \
  : "+r" (err)              \
  : "r" (x), "r" (addr), "i" (-EFAULT))

   上面用到了两个section,.fixup__ex_table,其中前者是修复程序的入口,后者用于存放触发异常的指令所在的地址跟对应的修复程序的入口地址之间的映射关系。比如当1b处的代码写addr触发了异常后,陷入内核缺页异常,然后在内核缺页异常里搜索1b对应的__ex_table,这个表里记录了异常指令地址跟对应的修复程序的入口地址的映射关系,找到后在异常返回时会跳转到修复程序入口,也就是跳转到上面.fixup3b处,然后将错误码-EFAULT存入err中,最后跳转到异常指令1b的下一行2b继续执行。

   在每一个__ex_table中存放了两个偏移量,分别是异常指令的地址1b和修改指令的地址3b跟当前地址的偏移,这样遍历__ex_table数组的时候可以很容易获得1b3b的地址。

   在内核的链接脚本中有专门存放__ex_table.fixup的段:

__ex_table:

/*
 * Exception table
 */
#define EXCEPTION_TABLE(align)            \
  . = ALIGN(align);           \
  __ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) {   \
    __start___ex_table = .;         \
    KEEP(*(__ex_table))         \
    __stop___ex_table = .;          \
  }

.fixup:

.text : ALIGN(SEGMENT_ALIGN) {  /* Real text segment    */
    _stext = .;   /* Text and read-only data  */
      IRQENTRY_TEXT
      SOFTIRQENTRY_TEXT
      ENTRY_TEXT
      TEXT_TEXT
      SCHED_TEXT
      CPUIDLE_TEXT
      LOCK_TEXT
      KPROBES_TEXT
      HYPERVISOR_TEXT
      IDMAP_TEXT
      HIBERNATE_TEXT
      TRAMP_TEXT
      *(.fixup)
      *(.gnu.warning)
    . = ALIGN(16);
    *(.got)     /* Global offset table    */
  }

内核缺页

do_translation_fault

   ----> do_page_fault

     ---->__do_kernel_fault

       ----> fixup_exception

int fixup_exception(struct pt_regs *regs)
{
  const struct exception_table_entry *fixup;
  fixup = search_exception_tables(instruction_pointer(regs));
  if (!fixup)
    return 0;
  if (in_bpf_jit(regs))
    return arm64_bpf_fixup_exception(fixup, regs);
  regs->pc = (unsigned long)&fixup->fixup + fixup->fixup;
  return 1;
}

 instruction_pointer(regs)返回异常指令的地址,也就是上面的1b,然后调用search_exception_tables搜索,搜索顺序是先从内核的__start___ex_table ~ __stop___ex_table搜索,如果没有找到,那么会依次从内核module和bpf里搜索。

/* 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_kernel_exception_table(addr); // 静态编译到内核中的
  if (!e)
    e = search_module_extables(addr);    // 从内核module里搜索
  if (!e)
    e = search_bpf_extables(addr);       // 从bpf里搜索
  return e;
}

search_kernel_exception_table的实现如下:

/* Given an address, look for it in the kernel exception table */
const
struct exception_table_entry *search_kernel_exception_table(unsigned long addr)
{
  return search_extable(__start___ex_table,
            __stop___ex_table - __start___ex_table, addr);
}

search_extable的实现如下:

struct exception_table_entry
{
  int insn, fixup;
};
static inline unsigned long ex_to_insn(const struct exception_table_entry *x)
{
  return (unsigned long)&x->insn + x->insn;
}
static int cmp_ex_search(const void *key, const void *elt)
{
  const struct exception_table_entry *_elt = elt;
  unsigned long _key = *(unsigned long *)key;
  /* avoid overflow */
  if (_key > ex_to_insn(_elt))
    return 1;
  if (_key < ex_to_insn(_elt))
    return -1;
  return 0;
}
/*
 * 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 *base,
         const size_t num,
         unsigned long value)
{
  return bsearch(&value, base, num,
           sizeof(struct exception_table_entry), cmp_ex_search);
}

   对于遍历到的每一个__ex_table都会调用cmp_ex_search,第一个参数key中存放的是&value,第二个参数存放的是当前__ex_table的地址。_key中存放的是异常指令1b的地址,ex_to_insn(_elt)返回记录的异常指令1b的地址(__ex_table的地址+偏移),如果二者相等,表示当前__ex_table就是我们想要的。

   最后回到fixup_exception中,search_exception_tables返回找到的__ex_table,然后计算修复指令的地址(unsigned long)&fixup->fixup + fixup->fixup(地址+偏移),将结果赋值给regs->pc,这样当异常返回后就会跳转到修复指令的地址开始执行,也就是上面提到的3b位置。

   上面分析了put_user,对于get_usercopy_to_user以及copy_from_user都采用了类似的方法来处理用户传递了非法的地址的情况,防止内核崩溃。

相关文章
|
23天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
64 4
|
27天前
|
缓存 算法 Linux
深入理解Linux内核调度器:公平性与性能的平衡####
真知灼见 本文将带你深入了解Linux操作系统的核心组件之一——完全公平调度器(CFS),通过剖析其设计原理、工作机制以及在实际系统中的应用效果,揭示它是如何在众多进程间实现资源分配的公平性与高效性的。不同于传统的摘要概述,本文旨在通过直观且富有洞察力的视角,让读者仿佛亲身体验到CFS在复杂系统环境中游刃有余地进行任务调度的过程。 ####
44 6
|
13天前
|
缓存 网络协议 Linux
深入探索Linux操作系统的内核优化策略####
本文旨在探讨Linux操作系统内核的优化方法,通过分析当前主流的几种内核优化技术,结合具体案例,阐述如何有效提升系统性能与稳定性。文章首先概述了Linux内核的基本结构,随后详细解析了内核优化的必要性及常用手段,包括编译优化、内核参数调整、内存管理优化等,最后通过实例展示了这些优化技巧在实际场景中的应用效果,为读者提供了一套实用的Linux内核优化指南。 ####
38 1
|
18天前
|
算法 Linux 开发者
Linux内核中的锁机制:保障并发控制的艺术####
本文深入探讨了Linux操作系统内核中实现的多种锁机制,包括自旋锁、互斥锁、读写锁等,旨在揭示这些同步原语如何高效地解决资源竞争问题,保证系统的稳定性和性能。通过分析不同锁机制的工作原理及应用场景,本文为开发者提供了在高并发环境下进行有效并发控制的实用指南。 ####
|
26天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
58 9
|
25天前
|
缓存 负载均衡 Linux
深入理解Linux内核调度器
本文探讨了Linux操作系统核心组件之一——内核调度器的工作原理和设计哲学。不同于常规的技术文章,本摘要旨在提供一种全新的视角来审视Linux内核的调度机制,通过分析其对系统性能的影响以及在多核处理器环境下的表现,揭示调度器如何平衡公平性和效率。文章进一步讨论了完全公平调度器(CFS)的设计细节,包括它如何处理不同优先级的任务、如何进行负载均衡以及它是如何适应现代多核架构的挑战。此外,本文还简要概述了Linux调度器的未来发展方向,包括对实时任务支持的改进和对异构计算环境的适应性。
40 6
|
26天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
39 5
|
26天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
27天前
|
存储 监控 安全
Linux内核调优的艺术:从基础到高级###
本文深入探讨了Linux操作系统的心脏——内核的调优方法。文章首先概述了Linux内核的基本结构与工作原理,随后详细阐述了内核调优的重要性及基本原则。通过具体的参数调整示例(如sysctl、/proc/sys目录中的设置),文章展示了如何根据实际应用场景优化系统性能,包括提升CPU利用率、内存管理效率以及I/O性能等关键方面。最后,介绍了一些高级工具和技术,如perf、eBPF和SystemTap,用于更深层次的性能分析和问题定位。本文旨在为系统管理员和高级用户提供实用的内核调优策略,以最大化Linux系统的效率和稳定性。 ###
|
26天前
|
Java Linux Android开发
深入探索Android系统架构:从Linux内核到应用层
本文将带领读者深入了解Android操作系统的复杂架构,从其基于Linux的内核到丰富多彩的应用层。我们将探讨Android的各个关键组件,包括硬件抽象层(HAL)、运行时环境、以及核心库等,揭示它们如何协同工作以支持广泛的设备和应用。通过本文,您将对Android系统的工作原理有一个全面的认识,理解其如何平衡开放性与安全性,以及如何在多样化的设备上提供一致的用户体验。