目录
引言
在理解“了解Linux内核”中有关内存管理的章节中,我认为尝试编写将虚拟内存地址转换为物理地址的程序会很有趣。而且,我想在用户空间上使用它。再进一步,为什么不尝试获取缓冲区的物理地址,转到内存中的该位置,对其进行修改,然后使用虚拟地址查看更改。
尝试在用户空间中完成此任务存在一些问题:
- 虚拟内存背后的想法是提供连续内存的地址空间。进程的内存很可能存储在不连续的块中。
- 没有保证page在系统的物理内存中。它可以在交换区或缓存中。可能没有实际地址!
- 出于安全原因,即使进程的UID为0,进程也无权访问系统的原始内存。
我们可以采用两种方法来获取物理地址:
- 给内核添加一个系统调用,给定一个虚拟地址,它将返回物理地址。但是,修改内核违反了从用户空间执行所有操作的规则,因此我们必须将其排除在外。
- 将page映射文件用于进程(内核2.6.25中已添加)以获取页面映射到的帧,然后使用该页面查找
/dev/mem
并修改那里的缓冲区。
使用这种方法,完全有可能在用户空间中将虚拟地址转换为物理地址。但是,要验证我们的翻译是正确的,需要阅读/dev/mem
。这确实需要对内核进行一次小的修改(更改配置选项),但稍后会进行更多修改。
内存由大量word或array组成,每个word或array都有与之关联的地址。现在,CPU的工作是从基于内存的程序计数器中获取指令。现在,这些指令可能会导致加载或存储到特定的存储器地址。地址绑定是从一个地址空间映射到另一地址空间的过程。逻辑地址是CPU在执行过程中生成的地址,而物理地址是指存储单元(已加载到内存中的单元)中的位置。请注意,用户仅处理逻辑地址(虚拟地址)。逻辑地址尤其由MMU或地址转换单元进行转换。该过程的输出是适当的物理地址或代码/数据在RAM中的位置。
地址绑定
地址绑定可以通过三种不同的方式完成:
- 编译时间–如果您知道在编译期间进程将驻留在内存中,则会生成绝对地址,即在编译过程中将物理地址嵌入到程序的可执行文件中。将可执行文件作为进程加载到内存中的速度非常快。但是,如果生成的地址空间被其他进程所占用,则程序将崩溃,并且有必要重新编译程序以更改地址空间。
- 加载时间–如果在编译时不知道进程将驻留在哪里,则将生成可重定位的地址。加载程序将可重定位地址转换为绝对地址。加载程序将主存储器中进程的基地址添加到所有逻辑地址中,以生成绝对地址。在这种情况下,如果进程的基址发生更改,则我们需要再次重新加载进程。
- 执行时间-指令在内存中,正在由CPU处理。此时可以分配和/或释放其他内存。如果可以在执行期间将进程从一个内存移动到另一个内存(动态链接-在加载或运行时完成链接),则使用此方法。例如–压实。
MMU(内存管理单元)
虚拟地址和物理地址之间的运行时映射是通过称为MMU的硬件设备完成的。
在内存管理中,操作系统将处理进程并在磁盘和内存之间移动进程以执行。它跟踪可用和已使用的内存。
指令执行周期包括以下步骤:
- 从存储器中获取第一条指令,例如ADD A,B
- 然后将这些指令解码,即A和B的加法
- 然后在某个特定的存储位置进行进一步的加载或存储。
基本硬件
由于主存储器和寄存器内置在处理器中,CPU只能访问它们,因此每条指令都应写入直接访问存储
设备中。
- 如果从寄存器访问CPU指令,则由于寄存器内置在CPU中,因此可以在一个CPU时钟周期内完成。
- 如果指令驻留在主存储器中,那么它将通过存储器总线进行访问,这将花费大量时间。因此,对此的补救措施是在CPU和主内存之间添加快速内存,即为事务添加缓存。
- 现在,我们应该确保流程位于法定地址内。
- 合法地址由基址寄存器(拥有最小的物理地址)和限制寄存器(范围的大小)组成。
例如:
基址寄存器= 300040
限制寄存器= 120900
那么合法地址=(300040 + 120900)= 420940(含)。
法定地址=基本寄存器+限制寄存器
进程如何从磁盘映射到内存
- 通常,进程以二进制可执行文件的形式驻留在磁盘中。
- 因此,执行过程应驻留在主存储器中。
- 根据使用的内存管理,进程从磁盘移动到内存。
- 进程以就绪队列的形式在磁盘中等待以获取内存。
磁盘和内存的映射过程
正常过程是从输入队列中选择进程并将其加载到内存中。进程执行时,它将访问内存中的数据和指令,一旦完成,它将释放内存,现在内存可用于其他进程。
MMU方案
CPU ------- MMU ------内存
- CPU将为例如346生成逻辑地址
- MMU将为例如:14000生成重定位寄存器(基址寄存器)
- 内存中的物理地址位于例如:(346 + 14000 = 14346)
图来源:https://users.dimi.uniud.it/~antonio.dangelo/OpSys/materials/Operating_System_Concepts.pdf
本文由Vaishali Bhatia贡献。如果您喜欢GeeksforGeeks并且愿意做出贡献,那么您也可以使用contribution.geeksforgeeks.org撰写文章或将您的文章邮寄至contribution@geeksforgeeks.org。查看您的文章出现在GeeksforGeeks主页上,并帮助其他Geeks。
开始编码
创建我们的缓冲区
除了通常的malloc()
调用之外,创建缓冲区以查找地址的过程还需要另外一步。内核不保证虚拟地址空间中的地址实际映射到内存中的物理地址。它可以存储在交换空间中,某个地方或整个地方的缓存中。为了避免这种可能性,我们可以mlock()
用来强制将页面保留在系统的物理内存中。幸运的是,这很简单。只需将mlock()
指针传递到缓冲区和缓冲区的大小,它将处理其余的内容。看起来像这样:
void* create_buffer(void) {
size_t buf_size = strlen(ORIG_BUFFER) + 1;
// Allocate some memory to manipulate
void *buffer = malloc(buf_size);
if(buffer == NULL) {
fprintf(stderr, "Failed to allocate memory for buffer\n");
exit(1);
}
// Lock the page in memory
// Do this before writing data to the buffer so that any copy-on-write
// mechanisms will give us our own page locked in memory
if(mlock(buffer, buf_size) == -1) {
fprintf(stderr, "Failed to lock page in memory: %s\n", strerror(errno));
exit(1);
}
// Add some data to the memory
strncpy(buffer, ORIG_BUFFER, strlen(ORIG_BUFFER));
return buffer;
}
请注意,锁定后我将数据复制到缓冲区中。这是因为如果缓冲区所在的页面与父进程共享,则OS可能会采用写时复制分页机制。为了强制操作系统提供我们自己的页面,我们将数据锁定后写入缓冲区。
/proc/[pid]/pagemap
页面映射为用户提供了访问空间,以访问内核如何管理进程页面。这是一个二进制文件,因此从中提取信息有些棘手。
从文档1中可以看到,每页有64位信息。我们对页帧号0-54位感兴趣。
我们如何从页面地图中获取给定页面的页面框架号?首先,我们需要确定要映射到页面地图中的偏移量。可以这样进行:
#define PAGEMAP_LENGTH 8
offset = (unsigned long)addr / getpagesize() * PAGEMAP_LENGTH
给定一个地址,我们将其除以页面大小,然后乘以8。为什么是8?每页有64位或8字节的信息。
然后,我们在文件中寻找该位置并读取前7个字节。为什么是7?我们对咬伤0-54感兴趣。总共有55位。因此,我们读取了前7个字节(56位)并清除了位55。位55是我们不关心的软脏标志。
unsigned long get_page_frame_number_of_address(void *addr) {
// Open the pagemap file for the current process
FILE *pagemap = fopen("/proc/self/pagemap", "rb");
// Seek to the page that the buffer is on
unsigned long offset = (unsigned long)addr / getpagesize() * PAGEMAP_LENGTH;
if(fseek(pagemap, (unsigned long)offset, SEEK_SET) != 0) {
fprintf(stderr, "Failed to seek pagemap to proper location\n");
exit(1);
}
// The page frame number is in bits 0-54 so read the first 7 bytes and clear the 55th bit
unsigned long page_frame_number = 0;
fread(&page_frame_number, 1, PAGEMAP_LENGTH-1, pagemap);
page_frame_number &= 0x7FFFFFFFFFFFFF;
fclose(pagemap);
return page_frame_number;
}
现在我们有了页面帧号,我们可以轻松计算出缓冲区的物理地址,例如2:
physcial_addr = (page_frame_number << PAGE_SHIFT) + distance_from_page_boundary_of_buffer
PAGE_SHIFT
内核#define在哪里。对于我的x86_64系统,它定义为12,但这可能因您而异。您应该自己查看内核源代码来确认该值。
写 /dev/mem
现在我们已经确定了物理地址,我们可以继续在内存中找到该位置并对其进行修改。
Linux通过/dev/mem
块设备提供对系统内存的直接访问。但是,由于明显的安全隐患,即使是root用户,也无法读取(更不用说写入)该文件了。这是由于CONFIG_STRICT_DEVMEM
内核配置选项。作为配置选项,必须在编译时设置它,以便更改它,您必须重新编译内核。
内核的编译和安装会因发行版的不同而有所差异,因此在此不再赘述。如果您已经熟悉该过程,则只需CONFIG_STRICT_DEVMEM=n
在配置中设置,重新编译,安装和重新启动即可。希望所有虚拟机都存在,因为这显然会带来巨大的安全漏洞。
假设您的内核已CONFIG_STRICT_DEVMEM
禁用,我们可以继续。首先是要知道在哪里寻找/dev/mem
我们放入缓冲区的字符串。实际上,这很简单。我们需要寻找的偏移量等于我们上面计算的物理地址。
// Find the difference from the buffer to the page boundary
unsigned int distance_from_page_boundary = (unsigned long)buffer % getpagesize();
// Determine how far to seek into memory to find the buffer
uint64_t offset = (page_frame_number << PAGE_SHIFT) + distance_from_page_boundary;
现在让我们打开/dev/mem
并寻找我们计算出的偏移量:
int open_memory(void) {
// Open the memory (must be root for this)
int fd = open("/dev/mem", O_RDWR);
if(fd == -1) {
fprintf(stderr, "Error opening /dev/mem: %s\n", strerror(errno));
exit(1);
}
return fd;
}
void seek_memory(int fd, unsigned long offset) {
unsigned pos = lseek(fd, offset, SEEK_SET);
if(pos == -1) {
fprintf(stderr, "Failed to seek /dev/mem: %s\n", strerror(errno));
exit(1);
}
}
int mem_fd = open_memory();
seek_memory(mem_fd, offset);
快完成了!我们在内部找到了正确的文件描述符,/dev/mem
所以现在我们只需要写3。
if(write(mem_fd, NEW_BUFFER, strlen(NEW_BUFFER)) == -1) {
fprintf(stderr, "Write failed: %s\n", strerror(errno));
}
请注意,NEW_BUFFER
长度必须等于或短于ORIG_BUFFER
。就我而言,我将它们定义为相同的长度,因此我不必费心复制NUL终止符。
最后,我们可以从原始缓冲区中读取内容,如果一切正常,我们将看到通过写入更改了缓冲区的内容/dev/mem
。
printf("Buffer: %s\n", buffer);
结论和完整代码清单
值得注意的是,这只是一个实验。这并不是要依赖的行为。实际上,在我的测试中,我体验了内核在计算偏移量以查找该偏移量并将数据写入该偏移量之间的时间在物理地址周围乱码的情况。底线是:坚持虚拟内存;它真的很好。而且,如果您需要从用户空间修改物理内存,请寻找另一种方法。
完整代码清单:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
// ORIG_BUFFER will be placed in memory and will then be changed to NEW_BUFFER
// They must be the same length
#define ORIG_BUFFER "Hello, World!"
#define NEW_BUFFER "Hello, Linux!"
// The page frame shifted left by PAGE_SHIFT will give us the physcial address of the frame
// Note that this number is architecture dependent. For me on x86_64 with 4096 page sizes,
// it is defined as 12. If you're running something different, check the kernel source
// for what it is defined as.
#define PAGE_SHIFT 12
#define PAGEMAP_LENGTH 8
void* create_buffer(void);
unsigned long get_page_frame_number_of_address(void *addr);
int open_memory(void);
void seek_memory(int fd, unsigned long offset);
int main(void) {
// Create a buffer with some data in it
void *buffer = create_buffer();
// Get the page frame the buffer is on
unsigned int page_frame_number = get_page_frame_number_of_address(buffer);
printf("Page frame: 0x%x\n", page_frame_number);
// Find the difference from the buffer to the page boundary
unsigned int distance_from_page_boundary = (unsigned long)buffer % getpagesize();
// Determine how far to seek into memory to find the buffer
uint64_t offset = (page_frame_number << PAGE_SHIFT) + distance_from_page_boundary;
// Open /dev/mem, seek the calculated offset, and
// map it into memory so we can manipulate it
// CONFIG_STRICT_DEVMEM must be disabled for this
int mem_fd = open_memory();
seek_memory(mem_fd, offset);
printf("Buffer: %s\n", buffer);
puts("Changing buffer through /dev/mem...");
// Change the contents of the buffer by writing into /dev/mem
// Note that since the strings are the same length, there's no purpose in
// copying the NUL terminator again
if(write(mem_fd, NEW_BUFFER, strlen(NEW_BUFFER)) == -1) {
fprintf(stderr, "Write failed: %s\n", strerror(errno));
}
printf("Buffer: %s\n", buffer);
// Clean up
free(buffer);
close(mem_fd);
return 0;
}
void* create_buffer(void) {
size_t buf_size = strlen(ORIG_BUFFER) + 1;
// Allocate some memory to manipulate
void *buffer = malloc(buf_size);
if(buffer == NULL) {
fprintf(stderr, "Failed to allocate memory for buffer\n");
exit(1);
}
// Lock the page in memory
// Do this before writing data to the buffer so that any copy-on-write
// mechanisms will give us our own page locked in memory
if(mlock(buffer, buf_size) == -1) {
fprintf(stderr, "Failed to lock page in memory: %s\n", strerror(errno));
exit(1);
}
// Add some data to the memory
strncpy(buffer, ORIG_BUFFER, strlen(ORIG_BUFFER));
return buffer;
}
unsigned long get_page_frame_number_of_address(void *addr) {
// Open the pagemap file for the current process
FILE *pagemap = fopen("/proc/self/pagemap", "rb");
// Seek to the page that the buffer is on
unsigned long offset = (unsigned long)addr / getpagesize() * PAGEMAP_LENGTH;
if(fseek(pagemap, (unsigned long)offset, SEEK_SET) != 0) {
fprintf(stderr, "Failed to seek pagemap to proper location\n");
exit(1);
}
// The page frame number is in bits 0-54 so read the first 7 bytes and clear the 55th bit
unsigned long page_frame_number = 0;
fread(&page_frame_number, 1, PAGEMAP_LENGTH-1, pagemap);
page_frame_number &= 0x7FFFFFFFFFFFFF;
fclose(pagemap);
return page_frame_number;
}
int open_memory(void) {
// Open the memory (must be root for this)
int fd = open("/dev/mem", O_RDWR);
if(fd == -1) {
fprintf(stderr, "Error opening /dev/mem: %s\n", strerror(errno));
exit(1);
}
return fd;
}
void seek_memory(int fd, unsigned long offset) {
unsigned pos = lseek(fd, offset, SEEK_SET);
if(pos == -1) {
fprintf(stderr, "Failed to seek /dev/mem: %s\n", strerror(errno));
exit(1);
}
}
DPDK是如何操作的?
#define _FILE_OFFSET_BITS 64
#include <errno.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/queue.h>
#include <sys/file.h>
#include <unistd.h>
#include <limits.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <signal.h>
#include <setjmp.h>
#ifdef RTE_EAL_NUMA_AWARE_HUGEPAGES
#include <numa.h>
#include <numaif.h>
#endif
#define phys_addr_t uint64_t
static bool phys_addrs_available = true;
typedef uint64_t rte_iova_t;
#define RTE_BAD_IOVA ((rte_iova_t)-1)
#define RTE_LOG(l, t, fmt...) printf(fmt)
#define PFN_MASK_SIZE 8
enum rte_iova_mode {
RTE_IOVA_DC = 0, /* Don't care mode */
RTE_IOVA_PA = (1 << 0), /* DMA using physical address */
RTE_IOVA_VA = (1 << 1) /* DMA using virtual address */
};
/*
* Get physical address of any mapped virtual address in the current process.
*/
phys_addr_t rte_mem_virt2phy(const void *virtaddr)
{
int fd, retval;
uint64_t page, physaddr;
unsigned long virt_pfn;
int page_size;
off_t offset;
/* Cannot parse /proc/self/pagemap, no need to log errors everywhere */
if (!phys_addrs_available)
return RTE_BAD_IOVA;
/* standard page size */
page_size = getpagesize();
fd = open("/proc/self/pagemap", O_RDONLY);
if (fd < 0) {
RTE_LOG(ERR, EAL, "%s(): cannot open /proc/self/pagemap: %s\n",
__func__, strerror(errno));
return RTE_BAD_IOVA;
}
virt_pfn = (unsigned long)virtaddr / page_size;
offset = sizeof(uint64_t) * virt_pfn;
if (lseek(fd, offset, SEEK_SET) == (off_t) -1) {
RTE_LOG(ERR, EAL, "%s(): seek error in /proc/self/pagemap: %s\n",
__func__, strerror(errno));
close(fd);
return RTE_BAD_IOVA;
}
retval = read(fd, &page, PFN_MASK_SIZE);
close(fd);
if (retval < 0) {
RTE_LOG(ERR, EAL, "%s(): cannot read /proc/self/pagemap: %s\n",
__func__, strerror(errno));
return RTE_BAD_IOVA;
} else if (retval != PFN_MASK_SIZE) {
RTE_LOG(ERR, EAL, "%s(): read %d bytes from /proc/self/pagemap "
"but expected %d:\n",
__func__, retval, PFN_MASK_SIZE);
return RTE_BAD_IOVA;
}
/*
* the pfn (page frame number) are bits 0-54 (see
* pagemap.txt in linux Documentation)
*/
if ((page & 0x7fffffffffffffULL) == 0)
return RTE_BAD_IOVA;
physaddr = ((page & 0x7fffffffffffffULL) * page_size)
+ ((unsigned long)virtaddr % page_size);
RTE_LOG(ERR, EAL, "phyaddr %p\n", (void*)physaddr);
return physaddr;
}
rte_iova_t rte_mem_virt2iova(const void *virtaddr)
{
// if (rte_eal_iova_mode() == RTE_IOVA_VA)
// return (uintptr_t)virtaddr;
return rte_mem_virt2phy(virtaddr);
}
int main()
{
int *addr1 = malloc(10);
printf("addr1 = %p\n", addr1);
rte_iova_t rte_iova = rte_mem_virt2iova(addr1);
printf("addr1 = %p, 0x%x\n", addr1, (void*)rte_iova);
return 0;
}
推荐文章
https://www.kernel.org/doc/Documentation/vm/pagemap.txt
https://www.kernel.org/doc/gorman/html/understand/understand005.html