在Linux中是否有用于从虚拟地址确定物理地址的API?

47

在Linux操作系统中,是否有用于从虚拟地址确定物理地址的API?


可能是如何在Linux用户空间中查找变量的物理地址?的重复问题。 - Brian Roach
7
我认为这不是重复问题,@Vlad的回答实际上很有帮助。 - karlphillip
1
https://dev59.com/G3E95IYBdhLWcg3wI6ft#28987409 - auselen
5个回答

39

内核和用户空间使用虚拟地址(也称为线性地址),由内存管理硬件将其映射到物理地址。这种映射是由操作系统设置的页表定义的。

DMA设备使用总线地址。在i386 PC上,总线地址与物理地址相同,但其他架构可能有特殊的地址映射硬件来将总线地址转换为物理地址。

在Linux中,您可以使用asm/io.h中的这些函数:

  • virt_to_phys(virt_addr);
  • phys_to_virt(phys_addr);
  • virt_to_bus(virt_addr);
  • bus_to_virt(bus_addr);

所有这些都是关于访问普通内存的。PCI或ISA总线上还有“共享内存”。可以使用ioremap()将其映射到32位地址空间内,然后通过readb()、writeb()等函数使用。

事实上,由于存在各种缓存,因此访问相同物理地址的不同方式可能会产生不同的结果,使得情况变得更加复杂。

此外,虚拟地址背后的真实物理地址可能会发生变化。更甚者,直到访问该内存之前,虚拟地址可能没有与之关联的地址。

就用户空间API而言,我不知道有任何可用的。


感谢您的回复。我想知道为什么没有用户空间API。在Linux中提供该API的复杂性是什么?是否有任何脚本或工具可以提供与虚拟地址对应的物理地址? - Karthik Balaguru
2
@S.Man:无论是否存在用户空间API,都不重要。更重要的是,这个用户空间API能为你做什么?在我看来,它是无用的。许多其他人也这样认为。为什么要投入时间和精力去做无用的事情呢? - user405725
3
关于用户层API,我能想到的一种方法是编写内核空间函数,并让用户空间进行查询(获取值)。获取物理地址的动机可能是用于某些恶意软件检测。告诫他人:不要因为你想不出用途就认为它是无用的。 - Ursa Major
请注意,virt_to_phys 仅适用于 kmalloc 内存:https://dev59.com/dm025IYBdhLWcg3w4Z-u#45128487 - Ciro Santilli OurBigBook.com
1
我看了一下virt_to_phy函数,它所做的只是减去IDNET_ADDRESS而不是进行页表遍历,我认为这不是一个bug(否则现在可能已经被发现了),你能解释一下吗? - tomer.z
作为对其有用性的评论,这是异构系统的必要功能,其中CPU使用虚拟内存,但使用相同内存的附加加速器可能仅具有物理寻址。为了让CPU与加速器共享数据,它需要在其(虚拟)地址空间中写入数据,然后向加速器提供物理地址。 - Chris

35

/proc/<pid>/pagemap用户空间最小可运行示例

virt_to_phys_user.c

#define _XOPEN_SOURCE 700
#include <fcntl.h> /* open */
#include <stdint.h> /* uint64_t  */
#include <stdio.h> /* printf */
#include <stdlib.h> /* size_t */
#include <unistd.h> /* pread, sysconf */

typedef struct {
    uint64_t pfn : 55;
    unsigned int soft_dirty : 1;
    unsigned int file_page : 1;
    unsigned int swapped : 1;
    unsigned int present : 1;
} PagemapEntry;

/* Parse the pagemap entry for the given virtual address.
 *
 * @param[out] entry      the parsed entry
 * @param[in]  pagemap_fd file descriptor to an open /proc/pid/pagemap file
 * @param[in]  vaddr      virtual address to get entry for
 * @return 0 for success, 1 for failure
 */
int pagemap_get_entry(PagemapEntry *entry, int pagemap_fd, uintptr_t vaddr)
{
    size_t nread;
    ssize_t ret;
    uint64_t data;
    uintptr_t vpn;

    vpn = vaddr / sysconf(_SC_PAGE_SIZE);
    nread = 0;
    while (nread < sizeof(data)) {
        ret = pread(pagemap_fd, ((uint8_t*)&data) + nread, sizeof(data) - nread,
                vpn * sizeof(data) + nread);
        nread += ret;
        if (ret <= 0) {
            return 1;
        }
    }
    entry->pfn = data & (((uint64_t)1 << 55) - 1);
    entry->soft_dirty = (data >> 55) & 1;
    entry->file_page = (data >> 61) & 1;
    entry->swapped = (data >> 62) & 1;
    entry->present = (data >> 63) & 1;
    return 0;
}

/* Convert the given virtual address to physical using /proc/PID/pagemap.
 *
 * @param[out] paddr physical address
 * @param[in]  pid   process to convert for
 * @param[in] vaddr virtual address to get entry for
 * @return 0 for success, 1 for failure
 */
int virt_to_phys_user(uintptr_t *paddr, pid_t pid, uintptr_t vaddr)
{
    char pagemap_file[BUFSIZ];
    int pagemap_fd;

    snprintf(pagemap_file, sizeof(pagemap_file), "/proc/%ju/pagemap", (uintmax_t)pid);
    pagemap_fd = open(pagemap_file, O_RDONLY);
    if (pagemap_fd < 0) {
        return 1;
    }
    PagemapEntry entry;
    if (pagemap_get_entry(&entry, pagemap_fd, vaddr)) {
        return 1;
    }
    close(pagemap_fd);
    *paddr = (entry.pfn * sysconf(_SC_PAGE_SIZE)) + (vaddr % sysconf(_SC_PAGE_SIZE));
    return 0;
}

int main(int argc, char **argv)
{
    pid_t pid;
    uintptr_t vaddr, paddr = 0;

    if (argc < 3) {
        printf("Usage: %s pid vaddr\n", argv[0]);
        return EXIT_FAILURE;
    }
    pid = strtoull(argv[1], NULL, 0);
    vaddr = strtoull(argv[2], NULL, 0);
    if (virt_to_phys_user(&paddr, pid, vaddr)) {
        fprintf(stderr, "error: virt_to_phys_user\n");
        return EXIT_FAILURE;
    };
    printf("0x%jx\n", (uintmax_t)paddr);
    return EXIT_SUCCESS;
}

GitHub upstream.

使用方法:

sudo ./virt_to_phys_user.out <pid> <virtual-address>

sudo权限是必须的,即使您具有文件权限也需要如此处所述。要读取/proc/<pid>/pagemap

正如在此处所述,Linux会惰性地分配页表,因此在使用virt_to_phys_user之前,请确保从测试程序中的该地址读取并写入一个字节。

如何测试

测试程序:

#define _XOPEN_SOURCE 700
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

enum { I0 = 0x12345678 };

static volatile uint32_t i = I0;

int main(void) {
    printf("vaddr %p\n", (void *)&i);
    printf("pid %ju\n", (uintmax_t)getpid());
    while (i == I0) {
        sleep(1);
    }
    printf("i %jx\n", (uintmax_t)i);
    return EXIT_SUCCESS;
}

测试程序输出它拥有的变量的地址和其PID,例如:

vaddr 0x600800
pid 110

然后您可以通过以下方式将虚拟地址转换:

sudo ./virt_to_phys_user.out 110 0x600800

最后,可以使用/dev/mem观察/修改内存来测试转换,但在Ubuntu 17.04上您不能这样做,除非重新编译内核,因为它需要: CONFIG_STRICT_DEVMEM=n,另请参见:如何在Linux中从用户空间访问物理地址?然而,Buildroot是一种简单的方法可以克服这一点。

或者,您可以使用像QEMU监视器的xp命令这样的虚拟机:如何解码Linux中的/ proc / pid / pagemap条目?

查看此处以转储所有页面:如何解码Linux中的/ proc / pid / pagemap条目?

此问题的用户空间子集:如何在Linux的用户空间中找到变量的物理地址?

使用/proc /<pid>/maps转储所有进程页面

/ proc /<pid>/maps列出进程的所有地址范围,因此我们可以遍历它来翻译所有页面:/ proc / [pid] / pagemaps和/ proc / [pid] / maps | linux

Kerneland virt_to_phys()仅适用于kmalloc()地址

从内核模块中提到了virt_to_phys()

但是,重要的是要强调它具有此限制。

例如,它无法处理模块变量。arc/x86/include/asm/io.h文档:

返回的物理地址是给定内存地址的物理(CPU)映射。仅对直接映射或通过kmalloc()分配的地址使用此函数有效。

这里有一个内核模块,它展示了如何用户空间测试一起获取虚拟地址的物理地址。

因此这不是一个非常普遍的可能性。请参见:如何在Linux内核模块中完全使用内核模块方法从逻辑地址获取物理地址?


我尝试过了。在某些情况下,它会产生不必要的尾随零,导致答案不正确。这里有一个正确的程序。 - AneesAhmed777
@AneesAhmed777 感谢您的评论,如果您能举个例子说明这些尾随零发生的情况以及它们的样子,那就更好了。 - Ciro Santilli OurBigBook.com
2
哎呀,我误以为程序只应该输出PFN,但实际上它输出了整个物理地址。程序运行正确:+1 赞。 - AneesAhmed777
2
@AneesAhmed777 哦,谢谢你的确认。如果你发现任何不对的地方,请随时告诉我。 - Ciro Santilli OurBigBook.com
1
奇怪,作为普通用户我有读取pagemap的权限,你知道有哪些sysctl配置可以限制只有root才能访问吗?编辑:看起来我只能访问我拥有的进程的pagemap - Roi

19

如前所述,普通程序不需要担心物理地址,因为它们在虚拟地址空间中运行,具有所有方便之处。此外,并非每个虚拟地址都有物理地址,可能属于映射文件或交换页面。然而,有时即使在用户空间中,查看此映射也可能是有趣的。

出于这个目的,Linux内核通过一组文件在/proc中向用户空间公开其映射。文档可以在这里找到。简要概述:

  1. / proc / $pid / maps 提供虚拟地址映射的列表,以及其他信息,例如映射文件的相应文件。
  2. / proc / $pid / pagemap 提供有关每个映射页面的更多信息,包括物理地址(如果存在)。

该网站提供了一个使用此接口转储所有运行进程映射的C程序及其解释。


9
建议的C程序通常可以使用,但至少有两种情况下可能会返回误导性结果:
  1. 页面不存在(但虚拟地址已映射到页面!)。这是由于操作系统的惰性映射:只有在实际访问时才映射地址。
  2. 返回的PFN指向一些可能是临时物理页面,这些页面可能很快就会因写时复制而更改。例如:对于内存映射文件,PFN可能指向只读副本。对于匿名映射,所有映射中的页面的PFN都可能指向一个特定的只读页面,该页面充满0(当写入时,所有匿名页面都从该页面生成)。
总之,为了确保更可靠的结果:对于只读映射,请在查询其PFN之前至少读取每个页面一次。对于可写页面,请在查询其PFN之前至少写入每个页面一次。
当然,理论上,在获得“稳定”的PFN之后,映射始终可以在运行时任意更改(例如将页面移入和移出交换),因此不应依赖它们。

5
我不明白为什么没有用户空间API。
因为用户空间内存的物理地址是未知的。
Linux对用户空间内存使用需求分页。在访问之前,用户空间对象将没有物理内存。当系统缺乏内存时,您的用户空间对象可能会被换出并且失去物理内存,除非页面已锁定该进程。当您再次访问对象时,它会被换入并获得物理内存,但很可能与以前的物理内存不同。您可以拍摄页面映射的快照,但无法保证在下一时刻它仍然相同。
因此,查找用户空间对象的物理地址通常没有意义。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接