在Linux用户层使用MMAP对寄存器进行读写

7 篇文章 3 订阅
4 篇文章 0 订阅

0. 前言

最近需要在嵌入式系统上调试驱动程序,需要在用户态下频繁读取ARM的寄存器的值。

为了方便测试,发现可以在用户态下,通过mmap函数将设备节点/dev/mem进行映射,实现在用户态下将物理地址映射到虚拟地址,并通过对虚拟地址的修改来实现寄存器的修改。

1. 原理

1.1 /dev/mem设备节点

简单一点说,/dev/mem是Linux系统下的物理内存的全镜像,可通过该节点实现对物理内存的访问。

一般用于在嵌入式中以用户态形式直接访问寄存器/物理IO设备等。

通常用法是open这个设备节点文件,然后mmap进行内存映射,就可以使用map之后的地址访问物理内存。

1.2 linux下的mmap函数和munmap函数

1.2.1 mmap函数

mmap函数可以将一个文件或者其它对象映射进内存。

这里我们使用mmap函数将设备节点/dev/mem映射到内存中。

在操作结束后,需要使用munmap函数反映射。

函数头文件:<sys/mman.h>

mmap函数原型:

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

关键参数如下:

  • start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址。需要按照页面大小对齐,否则会出错。
  • length:映射区的长度。长度单位是以字节为单位,不足一内存页按一内存页处理
  • prot:期望的内存保护标志,类似于可读可写等
  • flags:指定映射对象的类型
  • fd:文件标识符
  • offset:被映射对象内容的起点,需要按照页面大小对齐,否则会出错。

返回值:成功执行时,mmap()返回被映射区的指针,失败时,mmap()返回MAP_FAILED[其值为(void *)-1],错误原因会被errno记录。

注:获取系统页面大小的方式:

#include <unistd.h>
long page_size = sysconf(_SC_PAGESIZE);

1.2.2 munmap函数

munmap函数是mmap函数的反过程,取消对某地址的映射。

函数原型:

int munmap(void* start,size_t length);

参数:

  • start:虚拟内存起始地址
  • length:映射长度

返回值:

成功返回0,失败返回-1。错误原因也会被errno记录。

2. 代码及解析

2.1 使用mmap的关键流程分析

使用mmap函数映射一块物理地址并进行读写操作的基本流程如下:

2.1.1 使用open打开设备文件

首先要打开/dev/mem文件,才能通过mmap对物理地址进行操作。

打开文件的代码如下,如果fd大于0则说明打开成功:

int fd = open("/dev/mem", O_RDWR | O_NDELAY);
if (fd < 0)  
{
	printf("open(/dev/mem) failed.");    
	return 0;
}

2.1.2 使用mmap进行地址的映射

有了/dev/mem文件的文件描述符,就可以对其进行mmap操作了。

具体操作代码如下,将物理地址addr转换成虚拟地址map_base:

//↓获取页面大小
long page_size = sysconf(_SC_PAGESIZE);
//↓将指定物理地址addr映射为虚拟地址,并作为uchar型指针
unsigned char *map_base = (unsigned char * )mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED, dev_fd, addr );
//↓指针类型转换为uint型指针
unsigned int *map_base_i =  (unsigned int * )map_base;

这里注意看mmap函数的参数:

  1. start = NULL = 0,由系统决定映射区起始地址
  2. length = page_size = sysconf(_SC_PAGESIZE),即一整个内存页大小。由于这个参数是按照内存页大小向上取整,如果这个参数小于一个页面也按照一个页面处理。
  3. prot = PROT_READ | PROT_WRITE,页面内存可读可写
  4. flags = MAP_SHARED,与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。
  5. fd = dev_fd:文件描述符
  6. offset = addr,这里的offset是要写入的物理地址。这个地址需要按照页面大小对齐,否则会出错。如果要操作的地址不能被页面大小整除,则可将该地址所在的页进行mmap,之后根据偏移值对指定地址进行修改。

例如,要对0x01C4001C这个地址进行操作:
首先:我们根据页面大小算出该地址的页面首地址为0x01C40000,偏移量为0x0000001C
接下来对0x01C40000这个物理地址进行mmap得到虚拟地址map_base
就可以通过map_base+0x1C的方式对该物理地址进行访问。

2.1.3 对地址进行操作

获取到映射的虚拟地址后,就可以很方便的进行读写操作了。

由于我们操作的平台是32位,于是下面的说明都按照无符号整型进行操作。

以无符号整型方式读取偏移量为offset的地址的值:

printf("Before Modify : 0x%08X\r\n",*(volatile unsigned int *)(map_base+offset));

以无符号整型方式将data写入偏移量为offset的地址的值:

*(volatile unsigned int *)(map_base + offset) = data;

2.1.4 反映射操作

操作结束后需要munmap反映射并关闭设备文件:

munmap(map_base,page_size); //反映射,这里的map_base是前面获取的映射地址,page_size是映射的大小
close(fd); //关闭/dev/mem文件

2.2 完整代码

整个程序的完整代码如下。

本程序可以实现对某块物理地址的读取和写入,方便嵌入式在用户态调试寄存器。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <string.h>

#define ARGV_ADDR_POS 2
#define ARGV_DATA_POS 3
#define ARGV_WR_OFF_POS 3
#define ARGV_WR_DATA_POS 4
#define ARGV_CMD_POS 1

//显示使用方法
void printUsage()
{
	printf("Usage: \r\n Read address as Byte: mymm r [ADDR] [LEN] \r\n Read address as int: mymm i [ADDR] [LEN] \r\n");
	printf(" Write to address: mymm w [ADDR] [OFF] [DATA]\r\n");
}

//读取某个基地址addr+byte字节的数据并逐个字节显示。
//从DataSheet上看,页大小为0x400,因此这里的基地址addr必须是能够被0x400整除,否则Segment Error.
void readMMap(int dev_fd,unsigned int addr,unsigned int byte)
{
	int mmapByte = (byte/4*4+4),iloop;
	int realByte = (byte/4*4);
	//这里的mmapByte计算多此一举,因为这里只要进行映射都会按照页面大小向上取整,一般不用担心超出地址的问题
	unsigned char *map_base = (unsigned char * )mmap(NULL, mmapByte, PROT_READ | PROT_WRITE, MAP_SHARED, dev_fd, addr );  //使用MMAP直接映射基地址的内存数据到map_base
	int i,j;
	if(map_base == (unsigned char *)-1)
	{
		printf("MMAP Error.\r\n");
		return;
	}
	printf("           | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\r\n");
	printf("------------------------------------------------------------\r\n");
	iloop = realByte/16+((realByte%16)?1:0);
	//printf("iloop is %d\r\n",iloop);
	for(i = 0;i < iloop;i++)
	{
		int loopbyte = (byte-i*16 > 16)?16:(byte-i*16);
		//printf("loopbyte = %d\n",loopbyte);ARGV_WR_OFF_POS
		printf("0x%08X | ",addr+i*16);
		for(j = 0;j < loopbyte;j++)
		{
			printf("%02X ",*(volatile unsigned char *)(map_base+i*16+j));
		}
		printf("\r\n");
	}
	munmap(map_base,mmapByte);
}

//读取某个基地址addr+byte字节的数据并按照逐个int显示。
//从DataSheet上看,页大小为0x400,因此这里的基地址addr必须是能够被0x400整除,否则Segment Error.
void readMMapByUINT(int dev_fd,unsigned int addr,unsigned int byte)
{
	int mmapByte = (byte/4*4+4),iloop;
	int realByte = (byte/4*4);
	unsigned char *map_base = (unsigned char * )mmap(NULL, mmapByte, PROT_READ | PROT_WRITE, MAP_SHARED, dev_fd, addr );
	unsigned int *map_base_i =  (unsigned int * )map_base;
	int i,j;
	if(map_base == (unsigned char *)-1)
	{
		printf("MMAP Error.\r\n");
		return;
	}
	printf("           | +0x00      +0x04      +0x08      +0x0C         \r\n");
	printf("------------------------------------------------------------\r\n");
	iloop = realByte/16+((realByte%16 > 0)?1:0);
	//printf("iloop = %d\r\n",iloop);
	for(i = 0;i < iloop;i++)
	{
		int loopcnt = (byte-i*4 > 4)?4:byte-i*4;
		//printf("loopbyte = %d\n",loopbyte);
		printf("0x%08X | ",addr+i*16);
		for(j = 0;j < loopcnt;j++)
		{
			printf("0x%08X ",*(volatile unsigned int *)(map_base_i+i*4+j));
		}
		printf("\r\n");
	}
	munmap(map_base,mmapByte);
}

//将数据data写入基地址addr+offset的位置。
void writeMMap(int dev_fd,unsigned int addr,unsigned int offset,unsigned int data)
{
	unsigned int mmapByte = offset + 0x04;
	unsigned char *map_base = (unsigned char * )mmap(NULL, mmapByte, PROT_READ | PROT_WRITE, MAP_SHARED, dev_fd, addr );  
	if(map_base == (unsigned char *)-1)
	{
		printf("MMAP Error.\r\n");
		return;
	}
	printf("Before Modify : addr 0x%08X = 0x%08X\r\n",(addr+offset),*(volatile unsigned int *)(map_base+offset));
	*(volatile unsigned int *)(map_base + offset) = data;
	printf("After Modify : addr 0x%08X = 0x%08X\r\n",(addr+offset),*(volatile unsigned int *)(map_base+offset));
	munmap(map_base,mmapByte);
}

int main(int argc,char* argv[])
{
	int addr = 0,byte = 0,fd = 0,off = 0;
	if(argc < 4)
	{
		printf("ARG ERROR.\r\n");
		printUsage();
		return 0;
	}
	addr = strtoul(argv[ARGV_ADDR_POS],0,0);
	byte = strtoul(argv[ARGV_DATA_POS],0,0);
	
	if(addr == 0)
	{
		printf("Addr Err.\r\n");
		return;
	}
	fd = open("/dev/mem", O_RDWR | O_NDELAY);
	if (fd < 0)  
	{
		printf("open(/dev/mem) failed.");    
		return 0;
	}
	switch(argv[ARGV_CMD_POS][0])
	{
		case 'r':
			if(byte == 0)
			{
				printf("Byte len Err.\r\n");
				close(fd);
				return 0;
			}
			printf("Now Read Memory at 0x%08X by %d\r\n",addr,byte);
			readMMap(fd,addr,byte);
			break;
		case 'i':
			if(byte == 0)
			{
				printf("Byte len Err.\r\n");
				close(fd);
				return 0;
			}
			printf("Now Read Memory By int at 0x%08X by %d\r\n",addr,byte);
			readMMapByUINT(fd,addr,byte);
			break;
		case 'w':
			if(argc < 5)
			{
				printf("Write ARGC Error.");
				close(fd);
				return 0;
			}
			off = strtoul(argv[ARGV_WR_OFF_POS],0,0);
			byte = strtoul(argv[ARGV_WR_DATA_POS],0,0);
			printf("Now Write Memory By int at 0x%08X + 0x%08X by 0x%08X\r\n",addr,off,byte);
			writeMMap(fd,addr,off,byte);
			break;
		default:
			printf("Error Cmd.\r\n");
			break;
	}
	close(fd);
	return 0;
}

2.3 使用方法和示例

交叉编译并将文件命名为mymm放入板子中。

显示使用方法:

[root@xxxxx root]#/mymm                             
ARG ERROR.
Usage: 
 Read address as Byte: mymm r [ADDR] [LEN] 
 Read address as int: mymm i [ADDR] [LEN] 
 Write to address: mymm w [ADDR] [OFF] [DATA]

按照字节读取基地址为0x01C40000的48字节的数据:

[root@xxxxx root]#/mymm r 0x01C40000 0x30
Now Read Memory at 0x01C40000 by 48
           | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
------------------------------------------------------------
0x01C40000 | 00 00 FD 00 45 01 04 00 DA 00 00 00 FF FF 5A B7 
0x01C40010 | 55 C5 44 55 10 00 00 00 05 C0 03 00 03 00 00 00 
0x01C40020 | 00 00 00 00 00 00 00 00 2F E0 83 8B CF 41 11 08

按照整型读取基地址为0x01C40000的48字节的数据,可以看出这个CPU是little-endian的:

[root@xxxxx root]#/mymm i 0x01C40000 0x30
Now Read Memory By int at 0x01C40000 by 48
           | +0x00      +0x04      +0x08      +0x0C         
------------------------------------------------------------
0x01C40000 | 0x00FD0000 0x00040145 0x000000DA 0xB75AFFFF 
0x01C40010 | 0x5544C555 0x00000010 0x0003C005 0x00000003 
0x01C40020 | 0x00000000 0x00000000 0x8B83E02F 0x081141CF

修改地址0x01C4000C的数据为0xB75AFFFF:(从上面分析可知,这里的基地址是0x01C40000,偏移量是0x0C)

[root@xxxxx root]#/mymm w 0x01C40000 0x0C 0xB75AFFFF
Now Write Memory By int at 0x01C40000 + 0x0000000C by 0xB75AFFFF
Before Modify : addr 0x01C4000C = 0xB35AFFFF
After Modify : addr 0x01C4000C = 0xB75AFFFF
  • 8
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值