Linux内存管理:kmemcheck介绍

目录

Linux内核内存管理第3部分。

Linux内核中的kmemcheck简介

该kmemcheck机制在Linux内核中的实现

结论


读原文:《Linux内存管理:kmemcheck介绍

 

Linux内核内存管理第3部分。

Linux内核中的kmemcheck简介

这是本章的第三部分,描述了Linux内核中的内存管理,在本章的前一部分中,我们遇到了两个与内存管理相关的概念:

  • Fix-Mapped Addresses;
  • ioremap

第一个概念表示虚拟内存中的特殊区域,其相应的物理映射是在编译时计算的。第二个概念提供了将与输入/输出相关的内存映射到虚拟内存的功能。

例如,如果您将查看的输出/proc/iomem

$ sudo cat /proc/iomem

00000000-00000fff : reserved
00001000-0009d7ff : System RAM
0009d800-0009ffff : reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000cffff : Video ROM
000d0000-000d3fff : PCI Bus 0000:00
000d4000-000d7fff : PCI Bus 0000:00
000d8000-000dbfff : PCI Bus 0000:00
000dc000-000dffff : PCI Bus 0000:00
000e0000-000fffff : reserved
...
...
...

您将看到每个物理设备的系统内存映射。在这里,第一列显示了每种不同类型的内存使用的内存寄存器。第二列列出了位于这些寄存器内的存储器的类型。或例如:

$ sudo cat /proc/ioports

0000-0cf7 : PCI Bus 0000:00
  0000-001f : dma1
  0020-0021 : pic1
  0040-0043 : timer0
  0050-0053 : timer1
  0060-0060 : keyboard
  0064-0064 : keyboard
  0070-0077 : rtc0
  0080-008f : dma page reg
  00a0-00a1 : pic2
  00c0-00df : dma2
  00f0-00ff : fpu
    00f0-00f0 : PNP0C04:00
  03c0-03df : vga+
  03f8-03ff : serial
  04d0-04d1 : pnp 00:06
  0800-087f : pnp 00:01
  0a00-0a0f : pnp 00:04
  0a20-0a2f : pnp 00:04
  0a30-0a3f : pnp 00:04
...
...
...

可以向我们显示用于与设备进行输入或输出通信的当前注册端口区域的列表。内核不直接使用所有内存映射的I / O地址。因此,在Linux内核可以使用此类内存之前,它必须将其映射到虚拟内存空间,这是该ioremap机制的主要目的。请注意,我们仅ioremap在上一部分的早期看到过。很快,我们将研究非早期ioremap功能的实现。但是在此之前,我们必须学习其他东西,例如不同类型的内存分配器等,因为以其他方式很难理解它。

因此,在继续进行Linux内核的非早期内存管理之前,我们将看到一些机制,这些机制为调试内存泄漏检查,内存控制等提供了特殊的功能。将更容易理解内存管理的方式。在学习了所有这些内容之后,将它们安排在Linux内核中。

正如您可能已经从本部分的标题中猜到的那样,我们将开始考虑kmemcheck中的内存机制。正如我们在其他章节中经常做的那样,我们将从理论上开始考虑,并了解kmemcheck一般的机制,然后,我们将了解如何在Linux内核中实现它。

因此,让我们开始吧。这是什么kmemcheck在Linux内核?您可能从此机制的名称中猜到了,kmemcheck检查内存。确实如此。该kmemcheck机制的要点是检查某些内核代码是否已访问uninitialized memory。让我们来看下面的简单C程序:

#include <stdlib.h>
#include <stdio.h>

struct A {
        int a;
};

int main(int argc, char **argv) {
        struct A *a = malloc(sizeof(struct A));
        printf("a->a = %d\n", a->a);
        return 0;
}

在这里,我们为A结构分配内存,并尝试打印该a字段的值。如果我们将编译该程序而没有其他选项:

gcc test.c -o test

编译器不会显示我们警告说,a申请不未初始化。但是,如果我们将使用valgrind工具运行该程序,则会看到以下输出:

~$   valgrind --leak-check=yes ./test
==28469== Memcheck, a memory error detector
==28469== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==28469== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==28469== Command: ./test
==28469== 
==28469== Conditional jump or move depends on uninitialised value(s)
==28469==    at 0x4E820EA: vfprintf (in /usr/lib64/libc-2.22.so)
==28469==    by 0x4E88D48: printf (in /usr/lib64/libc-2.22.so)
==28469==    by 0x4005B9: main (in /home/alex/test)
==28469== 
==28469== Use of uninitialised value of size 8
==28469==    at 0x4E7E0BB: _itoa_word (in /usr/lib64/libc-2.22.so)
==28469==    by 0x4E8262F: vfprintf (in /usr/lib64/libc-2.22.so)
==28469==    by 0x4E88D48: printf (in /usr/lib64/libc-2.22.so)
==28469==    by 0x4005B9: main (in /home/alex/test)
...
...
...

实际上,该kmemcheck机制对内核的作用valgrind与对用户空间程序的作用相同。它检查未初始化的内存。

要在Linux内核中启用此机制,您需要在以下位置启用CONFIG_KMEMCHECK内核配置选项:

Kernel hacking
  -> Memory Debugging

Linux内核配置菜单:

Linux内存管理:kmemcheck介绍

 

我们不仅可以kmemcheck在Linux内核中启用对该机制的支持,还可以为我们提供一些配置选项。我们将在本部分的下一段中看到所有这些选项。在我们将考虑如何kmemcheck检查内存之前的最后一个注意事项。现在,仅针对x86_64体系结构实现此机制。您可以确定是否查看与arch / x86 / Kconfig x86相关的内核配置文件,您将看到以下几行:

config X86
  ...
  ...
  ...
  select HAVE_ARCH_KMEMCHECK
  ...
  ...
  ...

因此,没有其他特定于任何体系结构的东西。

好的,所以我们知道它kmemcheck提供了检查uninitialized memoryLinux内核使用情况以及如何启用它的机制。这些检查如何进行?当Linux内核尝试分配一些内存时,即像这样被称为:

struct my_struct *my_struct = kmalloc(sizeof(struct my_struct), GFP_KERNEL);

或者换句话说,有人要访问页面,则会生成页面错误异常。这是通过将kmemcheck内存页面标记为non-present(这一点的更多信息,您可以在专用于Paging的特殊部分中阅读)来实现。如果page fault发生异常,则异常处理程序会知道该异常,并且在kmemcheck启用的情况下,它将控制权转移给该异常处理程序。之后,kmemcheck将完成它的检查,该页面将被标记为present与被中断的代码就可以继续执行。在这条链上几乎没有什么微妙之处。当执行中断代码的第一条指令时,kmemcheck会将页面标记为non-present再次。这样,将再次捕获对内存的下一次访问。

我们只是kmemcheck从理论角度考虑了这一机制。现在让我们考虑如何在Linux内核中实现它。

 

kmemcheck机制在Linux内核中的实现

因此,现在我们知道了它是什么kmemcheck以及它在Linux内核中的作用。是时候看看它在Linux内核中的实现了。的实现kmemcheck分为两部分。第一个是通用部分,位于mm / kmemcheck.c源代码文件中,第二个x86_64特定于体系结构的部分位于arch / x86 / mm / kmemcheck目录中。

让我们从这种机制的初始化开始。我们已经知道要kmemcheck在Linux内核中启用该机制,我们必须启用CONFIG_KMEMCHECK内核配置选项。但是除此之外,我们还需要传递以下参数之一:

  • kmemcheck = 0(禁用)
  • kmemcheck = 1(启用)
  • kmemcheck = 2(单次模式)

到Linux内核命令行。前两个很明确,但最后一个需要一些解释。kmemcheck在检测到首次使用未初始化的内存后将关闭该选项时,它将在特殊模式下切换。实际上,默认情况下在Linux内核中启用此模式:

Linux内存管理:kmemcheck介绍

 

我们知道,从第七部分的章节描述了Linux内核,在内核初始化过程中内核命令行被解析的初始化do_initcall_leveldo_early_param功能。实际上,kmemcheck子系统由两个阶段组成。第一阶段还早。如果我们查看mm / kmemcheck.c源代码文件,我们将看到param_kmemcheck在早期命令行解析期间将调用的函数:

static int __init param_kmemcheck(char *str)
{
    int val;
    int ret;

    if (!str)
        return -EINVAL;

    ret = kstrtoint(str, 0, &val);
    if (ret)
        return ret;
    kmemcheck_enabled = val;
    return 0;
}

early_param("kmemcheck", param_kmemcheck);

正如我们已经看到的,param_kmemcheck可能具有以下值之一:(0启用),1(禁用)或2(一次性)。的实现param_kmemcheck非常简单。我们只是将kmemcheck命令行选项的字符串值转换为整数表示形式并将其设置为kmemcheck_enabled变量。

第二阶段将在Linux内核初始化期间执行,而不是在早期initcall初始化期间执行。第二阶段由以下内容表示kmemcheck_init

int __init kmemcheck_init(void)
{
    ...
    ...
    ...
}

early_initcall(kmemcheck_init);

kmemcheck_init函数的主要目标是调用该kmemcheck_selftest函数并检查其结果:

if (!kmemcheck_selftest()) {
    printk(KERN_INFO "kmemcheck: self-tests failed; disabling\n");
    kmemcheck_enabled = 0;
    return -EINVAL;
}

printk(KERN_INFO "kmemcheck: Initialized\n");

EINVAL如果此检查失败,则返回。在kmemcheck_selftest不同的存储器存取有关的功能检查大小的操作码一样rep movsbmovzwq等,如果操作码的大小等于预期大小时,kmemcheck_selftest将返回truefalse其他方式。

因此,当有人打电话给您时:

struct my_struct *my_struct = kmalloc(sizeof(struct my_struct), GFP_KERNEL);

通过一系列不同的函数调用,该kmem_getpages函数将被调用。此功能在mm / slab.c源代码文件中定义,并且此功能的主要目标是尝试分配具有给定标志的页面。在此函数的结尾,我们可以看到以下代码:

if (kmemcheck_enabled && !(cachep->flags & SLAB_NOTRACK)) {
    kmemcheck_alloc_shadow(page, cachep->gfporder, flags, nodeid);

    if (cachep->ctor)
        kmemcheck_mark_uninitialized_pages(page, nr_pages);
    else
        kmemcheck_mark_unallocated_pages(page, nr_pages);
}

因此,在这里,我们检查是否kmemcheck启用了if并且SLAB_NOTRACK未在我们non-present为刚分配的页面设置位的标志中设置该位。该SLAB_NOTRACK位告诉我们不要跟踪未初始化的内存。另外,我们检查缓存对象是否具有构造函数(详细信息将在接下来的部分中考虑),然后将分配的页面标记为未初始化或以其他方式未分配。该kmemcheck_alloc_shadow函数在mm / kmemcheck.c源代码文件中定义,并执行以下操作:

void kmemcheck_alloc_shadow(struct page *page, int order, gfp_t flags, int node)
{
    struct page *shadow;

       shadow = alloc_pages_node(node, flags | __GFP_NOTRACK, order);

       for(i = 0; i < pages; ++i)
        page[i].shadow = page_address(&shadow[i]);

       kmemcheck_hide_pages(page, pages);
}

首先,它为影子位分配存储空间。如果在页面中设置了该位,则表示该页面被跟踪kmemcheck。在为阴影位分配空间之后,我们用该位填充所有分配的页面。最后,我们仅kmemcheck_hide_pages使用指向已分配页面的指针和这些页面的编号来调用该函数。的kmemcheck_hide_pages是特定体系结构的功能,所以其执行位于拱/ 86 /毫米/ kmemcheck / kmemcheck.c源代码文件。该功能的主要目标是non-present在给定的页面中设置位。让我们看一下该函数的实现:

void kmemcheck_hide_pages(struct page *p, unsigned int n)
{
    unsigned int i;

    for (i = 0; i < n; ++i) {
        unsigned long address;
        pte_t *pte;
        unsigned int level;

        address = (unsigned long) page_address(&p[i]);
        pte = lookup_address(address, &level);
        BUG_ON(!pte);
        BUG_ON(level != PG_LEVEL_4K);

        set_pte(pte, __pte(pte_val(*pte) & ~_PAGE_PRESENT));
        set_pte(pte, __pte(pte_val(*pte) | _PAGE_HIDDEN));
        __flush_tlb_one(address);
    }
}

在这里,我们浏览所有页面并尝试获取page table entry每个页面。如果此操作成功,我们将在每个页面中取消设置当前位并设置隐藏位。最后,我们刷新了转换后备缓冲区,因为某些页面已更改。从这一点开始,已分配的页面将由跟踪kmemcheck。现在,由于present未设置该位,因此将在将指针返回到分配的空间之后立即执行页面错误,kmalloc并且代码将尝试访问该内存。

您可能还记得Linux内核初始化章节的第二部分,该page fault处理程序位于arch / x86 / mm / fault.c源代码文件中,并由do_page_fault函数表示。从do_page_fault函数开始我们可以看到以下检查:

static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long error_code,
        unsigned long address)
{
    ...
    ...
    ...
    if (kmemcheck_active(regs))
        kmemcheck_hide(regs);
    ...
    ...
    ...
}

kmemcheck_active得到kmemcheck_context 每个CPU的结构和返回的比较结果balance这种结构具有零的领域:

bool kmemcheck_active(struct pt_regs *regs)
{
    struct kmemcheck_context *data = this_cpu_ptr(&kmemcheck_context);

    return data->balance > 0;
}

kmemcheck_context是结构描述了的当前状态kmemcheck的机制。它存储了未初始化的地址,此类地址的数量等balance。此结构的字段表示的当前状态,kmemcheck或者换句话说,它可以告诉我们是否kmemcheck已隐藏页面。如果data->balance大于零,kmemcheck_hide则将调用该函数。这意味着kmemecheck已经present为给定页面设置了位,现在我们需要再次隐藏页面以引起下一步页面错误。该功能将通过取消设置present位来再次隐藏页面地址。这意味着发生了一个kmemcheck已经完成和新页面错误的会话。第一步,kmemcheck_active将返回false,因为data->balance开始时的为零,并且kmemcheck_hide不会被调用。接下来,我们可能会在中看到以下代码行do_page_fault

if (kmemcheck_fault(regs, address, error_code))
        return;

首先,该kmemcheck_fault功能检查故障是由正确的原因引起的。首先,我们检查标志寄存器并检查我们是否处于正常内核模式:

if (regs->flags & X86_VM_MASK)
        return false;
if (regs->cs != __KERNEL_CS)
        return false;

如果这些检查不成功,则我们从kmemcheck_fault函数返回,因为它kmemcheck与页面错误无关。此后,我们尝试查找page table entry与错误地址相关的,如果找不到,则返回:

pte = kmemcheck_pte_lookup(address);
if (!pte)
    return false;

kmemcheck_fault函数的最后两个步骤是调用该kmemcheck_access函数,该函数检查对给定页面的访问,并通过在给定页面中设置当前位来再次显示地址。该kmemcheck_access功能完成所有主要工作。它检查导致页面错误的当前指令。如果发现错误,则此错误的上下文将保存kmemcheck到环形队列中:

static struct kmemcheck_error error_fifo[CONFIG_KMEMCHECK_QUEUE_SIZE];

kmemcheck机制声明特殊的tasklet

static DECLARE_TASKLET(kmemcheck_tasklet, &do_wakeup, 0);

计划在计划运行时do_wakeuparch / x86 / mm / kmemcheck / error.c源代码文件运行该功能。

do_wakeup函数将调用该kmemcheck_error_recall函数,该函数将打印由收集的错误kmemcheck。正如我们已经看到的:

kmemcheck_show(regs);

函数将在kmemcheck_fault函数末尾被调用。此功能将再次设置给定页面的当前位:

if (unlikely(data->balance != 0)) {
    kmemcheck_show_all();
    kmemcheck_error_save_bug(regs);
    data->balance = 0;
    return;
}

kmemcheck_show_all函数调用kmemcheck_show_addr每个地址:

static unsigned int kmemcheck_show_all(void)
{
    struct kmemcheck_context *data = this_cpu_ptr(&kmemcheck_context);
    unsigned int i;
    unsigned int n;

    n = 0;
    for (i = 0; i < data->n_addrs; ++i)
        n += kmemcheck_show_addr(data->addr[i]);

    return n;
}

通过kmemcheck_show_addr

int kmemcheck_show_addr(unsigned long address)
{
    pte_t *pte;

    pte = kmemcheck_pte_lookup(address);
    if (!pte)
        return 0;

    set_pte(pte, __pte(pte_val(*pte) | _PAGE_PRESENT));
    __flush_tlb_one(address);
    return 1;
}

kmemcheck_show函数的最后,如果未设置TF标志,则设置它:

if (!(regs->flags & X86_EFLAGS_TF))
    data->flags = regs->flags;

我们需要这样做,因为在处理页面错误之后,我们需要在第一次执行指令后再次隐藏页面。在有TF标志的情况下,因此处理器将在执行第一条指令后切换到单步模式。在这种情况下,debug将会发生异常。从这一刻起,页面将再次被隐藏,并且将继续执行。由于此时隐藏了页面,因此将再次出现页面错误异常,并kmemcheck继续检查/收集错误并不时打印它们。

就这样。

结论

这是有关Linux内核内存管理的第三部分的结尾。如果您有任何疑问或建议,请在twitter 0xAX上ping我,给我发送电子邮件或创建问题。在下一部分中,我们将看到另一个与内存调试相关的工具- kmemleak

请注意,英语不是我的母语,对于给您带来的不便,我深表歉意。如果发现任何错误,请将PR发送给我linux-insides

上一篇:学安全测试需要考什么证书?


下一篇:按图索骥|Chapter5 网络空间安全专业与CISSP、CISP等认证有什么关系?