Linux操作系统中是否有任何API可以从虚拟地址中确定物理地址?
5 回答
内核和用户空间使用由内存管理硬件映射到物理地址的虚拟地址(也称为线性地址)。此映射由操作系统设置的页表定义。
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,我不知道。
/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;
}
用法:
sudo ./virt_to_phys_user.out <pid> <virtual-address>
sudo
/proc/<pid>/pagemap
即使您具有文件权限,也需要阅读,如: https ://unix.stackexchange.com/questions/345915/how-to-change-permission-of-proc-self-pagemap-file/383838#383838
如:https ://stackoverflow.com/a/46247716/895245 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
命令:How to decode /proc/pid/pagemap entries in Linux?
看到这个转储所有页面:How to decode /proc/pid/pagemap entries in Linux?
这个问题的用户态子集:如何在 Linux 中从用户空间中找到变量的物理地址?
转储所有进程页面/proc/<pid>/maps
/proc/<pid>/maps
列出进程的所有地址范围,因此我们可以遍历它来翻译所有页面:/proc/[pid]/pagemaps 和 /proc/[pid]/maps | linux
Kernelandvirt_to_phys()
仅适用于kmalloc()
地址
从内核模块中virt_to_phys()
,已经提到过。
然而,重要的是要强调它有这个限制。
例如,模块变量失败。arc/x86/include/asm/io.h
文档:
返回的物理地址是给定内存地址的物理 (CPU) 映射。仅对通过 直接映射或分配的地址使用此功能才有效
kmalloc()
。
所以这不是一个非常普遍的可能性。请参阅:如何从 Linux 内核模块中的逻辑地址获取物理地址?专门用于内核模块方法。
如前所述,普通程序不需要担心物理地址,因为它们在虚拟地址空间中运行,具有所有便利。此外,并非每个虚拟地址都有一个物理地址,它们可能属于映射文件或交换页面。然而,有时看到这个映射可能会很有趣,即使在用户空间中也是如此。
为此,Linux 内核通过/proc
. 文档可以在这里找到。简短的摘要:
/proc/$pid/maps
提供虚拟地址的映射列表以及附加信息,例如映射文件的对应文件。/proc/$pid/pagemap
提供有关每个映射页面的更多信息,包括物理地址(如果存在)。
这个网站提供了一个 C 程序,它使用这个接口转储所有正在运行的进程的映射,并解释它的作用。
上面建议的 C 程序通常可以工作,但它可能以(至少)两种方式返回误导性结果:
- 该页面不存在(但虚拟地址已映射到页面!)。这是由于操作系统的延迟映射而发生的:它仅在实际访问地址时才映射地址。
- 返回的 PFN 指向一些可能是临时的物理页面,由于写时复制,该页面可能很快就会被更改。例如:对于内存映射文件,PFN 可以指向只读副本。对于匿名映射,映射中所有页面的 PFN 可以是一个充满 0 的特定只读页面(写入时会从该页面生成所有匿名页面)。
底线是,为了确保更可靠的结果:对于只读映射,在查询其 PFN 之前至少从每个页面读取一次。对于允许写入的页面,在查询其 PFN 之前至少写入每个页面一次。
当然,理论上,即使在获得“稳定”的 PFN 之后,映射总是可以在运行时任意更改(例如在将页面移入和移出交换时)并且不应依赖。
我想知道为什么没有用户区 API。
因为用户域内存的物理地址是未知的。
Linux 对用户空间内存使用按需分页。您的用户登陆对象在被访问之前将没有物理内存。当系统内存不足时,除非页面被进程锁定,否则您的用户空间对象可能会被换出并丢失物理内存。当您再次访问该对象时,它被换入并被赋予物理内存,但它可能与前一个不同的物理内存。您可以对页面映射进行快照,但不保证在下一刻相同。
因此,寻找用户登陆对象的物理地址通常是没有意义的。